diff --git a/.coveragerc b/.coveragerc index 0ca73662a84..b88db04035a 100644 --- a/.coveragerc +++ b/.coveragerc @@ -202,6 +202,7 @@ omit = homeassistant/components/control4/__init__.py homeassistant/components/control4/director_utils.py homeassistant/components/control4/light.py + homeassistant/components/control4/media_player.py homeassistant/components/coolmaster/coordinator.py homeassistant/components/cppm_tracker/device_tracker.py homeassistant/components/crownstone/__init__.py @@ -250,7 +251,7 @@ omit = homeassistant/components/dormakaba_dkey/lock.py homeassistant/components/dormakaba_dkey/sensor.py homeassistant/components/dovado/* - homeassistant/components/downloader/* + homeassistant/components/downloader/__init__.py homeassistant/components/dsmr_reader/__init__.py homeassistant/components/dsmr_reader/definitions.py homeassistant/components/dsmr_reader/sensor.py @@ -461,6 +462,10 @@ omit = homeassistant/components/frontier_silicon/browse_media.py homeassistant/components/frontier_silicon/media_player.py homeassistant/components/futurenow/light.py + homeassistant/components/fyta/__init__.py + homeassistant/components/fyta/coordinator.py + homeassistant/components/fyta/entity.py + homeassistant/components/fyta/sensor.py homeassistant/components/garadget/cover.py homeassistant/components/garages_amsterdam/__init__.py homeassistant/components/garages_amsterdam/binary_sensor.py @@ -545,7 +550,6 @@ omit = homeassistant/components/homematic/notify.py homeassistant/components/homematic/sensor.py homeassistant/components/homematic/switch.py - homeassistant/components/homeworks/* homeassistant/components/horizon/media_player.py homeassistant/components/hp_ilo/sensor.py homeassistant/components/huawei_lte/__init__.py @@ -744,7 +748,6 @@ omit = homeassistant/components/lyric/climate.py homeassistant/components/lyric/sensor.py homeassistant/components/mailgun/notify.py - homeassistant/components/map/* homeassistant/components/mastodon/notify.py homeassistant/components/matrix/__init__.py homeassistant/components/matrix/notify.py @@ -773,9 +776,11 @@ omit = homeassistant/components/microbees/__init__.py homeassistant/components/microbees/api.py homeassistant/components/microbees/application_credentials.py + homeassistant/components/microbees/binary_sensor.py homeassistant/components/microbees/button.py homeassistant/components/microbees/const.py homeassistant/components/microbees/coordinator.py + homeassistant/components/microbees/cover.py homeassistant/components/microbees/entity.py homeassistant/components/microbees/light.py homeassistant/components/microbees/sensor.py @@ -801,6 +806,11 @@ omit = homeassistant/components/motion_blinds/cover.py homeassistant/components/motion_blinds/entity.py homeassistant/components/motion_blinds/sensor.py + homeassistant/components/motionblinds_ble/__init__.py + homeassistant/components/motionblinds_ble/button.py + homeassistant/components/motionblinds_ble/cover.py + homeassistant/components/motionblinds_ble/entity.py + homeassistant/components/motionblinds_ble/select.py homeassistant/components/motionmount/__init__.py homeassistant/components/motionmount/binary_sensor.py homeassistant/components/motionmount/entity.py @@ -888,6 +898,7 @@ omit = homeassistant/components/notify_events/notify.py homeassistant/components/notion/__init__.py homeassistant/components/notion/binary_sensor.py + homeassistant/components/notion/coordinator.py homeassistant/components/notion/sensor.py homeassistant/components/notion/util.py homeassistant/components/nsw_fuel_station/sensor.py @@ -923,7 +934,6 @@ omit = homeassistant/components/onvif/sensor.py homeassistant/components/onvif/util.py homeassistant/components/open_meteo/weather.py - homeassistant/components/opencv/* homeassistant/components/openevse/sensor.py homeassistant/components/openexchangerates/__init__.py homeassistant/components/openexchangerates/coordinator.py @@ -946,7 +956,9 @@ omit = homeassistant/components/openuv/binary_sensor.py homeassistant/components/openuv/coordinator.py homeassistant/components/openuv/sensor.py + homeassistant/components/openweathermap/__init__.py homeassistant/components/openweathermap/sensor.py + homeassistant/components/openweathermap/weather.py homeassistant/components/openweathermap/weather_update_coordinator.py homeassistant/components/opnsense/__init__.py homeassistant/components/opower/__init__.py @@ -988,7 +1000,9 @@ omit = homeassistant/components/pandora/media_player.py homeassistant/components/pencom/switch.py homeassistant/components/permobil/__init__.py + homeassistant/components/permobil/binary_sensor.py homeassistant/components/permobil/coordinator.py + homeassistant/components/permobil/entity.py homeassistant/components/permobil/sensor.py homeassistant/components/philips_js/__init__.py homeassistant/components/philips_js/light.py @@ -1055,6 +1069,7 @@ omit = homeassistant/components/rabbitair/fan.py homeassistant/components/rachio/__init__.py homeassistant/components/rachio/binary_sensor.py + homeassistant/components/rachio/coordinator.py homeassistant/components/rachio/device.py homeassistant/components/rachio/entity.py homeassistant/components/rachio/switch.py @@ -1127,6 +1142,7 @@ omit = homeassistant/components/rocketchat/notify.py homeassistant/components/romy/__init__.py homeassistant/components/romy/coordinator.py + homeassistant/components/romy/entity.py homeassistant/components/romy/vacuum.py homeassistant/components/roomba/__init__.py homeassistant/components/roomba/binary_sensor.py @@ -1141,7 +1157,6 @@ omit = homeassistant/components/roon/media_player.py homeassistant/components/roon/server.py homeassistant/components/route53/* - homeassistant/components/rova/sensor.py homeassistant/components/rpi_camera/* homeassistant/components/rtorrent/sensor.py homeassistant/components/ruuvi_gateway/__init__.py @@ -1179,7 +1194,6 @@ omit = homeassistant/components/serial_pm/sensor.py homeassistant/components/sesame/lock.py homeassistant/components/seven_segments/image_processing.py - homeassistant/components/seventeentrack/sensor.py homeassistant/components/shodan/sensor.py homeassistant/components/sia/__init__.py homeassistant/components/sia/alarm_control_panel.py @@ -1281,6 +1295,7 @@ omit = homeassistant/components/starlink/device_tracker.py homeassistant/components/starlink/sensor.py homeassistant/components/starlink/switch.py + homeassistant/components/starlink/time.py homeassistant/components/starline/__init__.py homeassistant/components/starline/account.py homeassistant/components/starline/binary_sensor.py @@ -1425,6 +1440,7 @@ omit = homeassistant/components/tolo/number.py homeassistant/components/tolo/select.py homeassistant/components/tolo/sensor.py + homeassistant/components/tolo/switch.py homeassistant/components/toon/__init__.py homeassistant/components/toon/binary_sensor.py homeassistant/components/toon/climate.py @@ -1594,7 +1610,6 @@ omit = homeassistant/components/weatherflow_cloud/const.py homeassistant/components/weatherflow_cloud/coordinator.py homeassistant/components/weatherflow_cloud/weather.py - homeassistant/components/webmin/sensor.py homeassistant/components/wiffi/__init__.py homeassistant/components/wiffi/binary_sensor.py homeassistant/components/wiffi/sensor.py @@ -1677,6 +1692,7 @@ omit = homeassistant/components/yolink/services.py homeassistant/components/yolink/siren.py homeassistant/components/yolink/switch.py + homeassistant/components/yolink/valve.py homeassistant/components/youless/__init__.py homeassistant/components/youless/sensor.py homeassistant/components/zabbix/* diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 553f3bbdf0e..83aa88140cc 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -21,6 +21,7 @@ ], // Please keep this file in sync with settings in home-assistant/.vscode/settings.default.json "settings": { + "python.experiments.optOutFrom": ["pythonTestAdapter"], "python.pythonPath": "/usr/local/bin/python", "python.testing.pytestArgs": ["--no-cov"], "editor.formatOnPaste": false, diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 00000000000..cfc34a08694 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,14 @@ +# Black +4de97abc3aa83188666336ce0a015a5bab75bc8f + +# Switch formatting from black to ruff-format (#102893) +706add4a57120a93d7b7fe40e722b00d634c76c2 + +# Prettify json (component test fixtures) (#68892) +053c4428a933c3c04c22642f93c93fccba3e8bfd + +# Prettify json (tests) (#68888) +496d90bf00429d9d924caeb0155edc0bf54e86b9 + +# Bump ruff to 0.3.4 (#112690) +6bb4e7d62c60389608acf4a7d7dacd8f029307dd diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index 2d6c2521171..217093793d1 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -12,6 +12,8 @@ env: BUILD_TYPE: core DEFAULT_PYTHON: "3.12" PIP_TIMEOUT: 60 + UV_HTTP_TIMEOUT: 60 + UV_SYSTEM_PYTHON: "true" jobs: init: @@ -25,12 +27,12 @@ jobs: publish: ${{ steps.version.outputs.publish }} steps: - name: Checkout the repository - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.1.2 with: fetch-depth: 0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v5.0.0 + uses: actions/setup-python@v5.1.0 with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -49,41 +51,29 @@ jobs: with: ignore-dev: true - build_python: - name: Build PyPi package - environment: ${{ needs.init.outputs.channel }} - needs: ["init", "build_base"] - runs-on: ubuntu-latest - if: github.repository_owner == 'home-assistant' && needs.init.outputs.publish == 'true' - steps: - - name: Checkout the repository - uses: actions/checkout@v4.1.1 - - - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v5.0.0 - with: - python-version: ${{ env.DEFAULT_PYTHON }} + - name: Fail if translations files are checked in + run: | + if [ -n "$(find homeassistant/components/*/translations -type f)" ]; then + echo "Translations files are checked in, please remove the following files:" + find homeassistant/components/*/translations -type f + exit 1 + fi - name: Download Translations run: python3 -m script.translations download env: LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }} - - name: Build package + - name: Archive translations shell: bash - run: | - # Remove dist, build, and homeassistant.egg-info - # when build locally for testing! - pip install twine build - python -m build + run: find ./homeassistant/components/*/translations -name "*.json" | tar zcvf translations.tar.gz -T - - - name: Upload package - shell: bash - run: | - export TWINE_USERNAME="__token__" - export TWINE_PASSWORD="${{ secrets.TWINE_TOKEN }}" - - twine upload dist/* --skip-existing + - name: Upload translations + uses: actions/upload-artifact@v4.3.1 + with: + name: translations + path: translations.tar.gz + if-no-files-found: error build_base: name: Build ${{ matrix.arch }} base core image @@ -95,15 +85,16 @@ jobs: packages: write id-token: write strategy: + fail-fast: false matrix: arch: ${{ fromJson(needs.init.outputs.architectures) }} steps: - name: Checkout the repository - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.1.2 - name: Download nightly wheels of frontend if: needs.init.outputs.channel == 'dev' - uses: dawidd6/action-download-artifact@v3.1.2 + uses: dawidd6/action-download-artifact@v3.1.4 with: github_token: ${{secrets.GITHUB_TOKEN}} repo: home-assistant/frontend @@ -114,7 +105,7 @@ jobs: - name: Download nightly wheels of intents if: needs.init.outputs.channel == 'dev' - uses: dawidd6/action-download-artifact@v3.1.2 + uses: dawidd6/action-download-artifact@v3.1.4 with: github_token: ${{secrets.GITHUB_TOKEN}} repo: home-assistant/intents-package @@ -125,17 +116,20 @@ jobs: - name: Set up Python ${{ env.DEFAULT_PYTHON }} if: needs.init.outputs.channel == 'dev' - uses: actions/setup-python@v5.0.0 + uses: actions/setup-python@v5.1.0 with: python-version: ${{ env.DEFAULT_PYTHON }} - name: Adjust nightly version if: needs.init.outputs.channel == 'dev' shell: bash + env: + UV_PRERELEASE: allow run: | - python3 -m pip install packaging tomli - python3 -m pip install . - version="$(python3 script/version_bump.py nightly)" + python3 -m pip install "$(grep '^uv' < requirements_test.txt)" + uv pip install packaging tomli + uv pip install . + python3 script/version_bump.py nightly --set-nightly-version "${{ needs.init.outputs.version }}" if [[ "$(ls home_assistant_frontend*.whl)" =~ ^home_assistant_frontend-(.*)-py3-none-any.whl$ ]]; then echo "Found frontend wheel, setting version to: ${BASH_REMATCH[1]}" @@ -147,7 +141,7 @@ jobs: sed -i "s|home-assistant-frontend==.*|home-assistant-frontend==${BASH_REMATCH[1]}|" \ homeassistant/package_constraints.txt - python -m script.gen_requirements_all + sed -i "s|home-assistant-frontend==.*||" requirements_all.txt fi if [[ "$(ls home_assistant_intents*.whl)" =~ ^home_assistant_intents-(.*)-py3-none-any.whl$ ]]; then @@ -165,7 +159,7 @@ jobs: sed -i "s|home-assistant-intents==.*|home-assistant-intents==${BASH_REMATCH[1]}|" \ homeassistant/package_constraints.txt - python -m script.gen_requirements_all + sed -i "s|home-assistant-intents==.*||" requirements_all.txt fi - name: Adjustments for armhf @@ -189,10 +183,15 @@ jobs: # are not available. sed -i "s|aiohttp-zlib-ng|aiohttp-zlib-ng\[isal\]|g" requirements_all.txt - - name: Download Translations - run: python3 -m script.translations download - env: - LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }} + - name: Download translations + uses: actions/download-artifact@v4.1.4 + with: + name: translations + + - name: Extract translations + run: | + tar xvf translations.tar.gz + rm translations.tar.gz - name: Write meta info file shell: bash @@ -200,7 +199,7 @@ jobs: echo "${{ github.sha }};${{ github.ref }};${{ github.event_name }};${{ github.actor }}" > rootfs/OFFICIAL_IMAGE - name: Login to GitHub Container Registry - uses: docker/login-action@v3.0.0 + uses: docker/login-action@v3.1.0 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -216,17 +215,6 @@ jobs: --target /data \ --generic ${{ needs.init.outputs.version }} - - name: Archive translations - shell: bash - run: find ./homeassistant/components/*/translations -name "*.json" | tar zcvf translations.tar.gz -T - - - - name: Upload translations - uses: actions/upload-artifact@v3 - with: - name: translations - path: translations.tar.gz - if-no-files-found: error - build_machine: name: Build ${{ matrix.machine }} machine core image if: github.repository_owner == 'home-assistant' @@ -263,7 +251,7 @@ jobs: - green steps: - name: Checkout the repository - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.1.2 - name: Set build additional args run: | @@ -277,7 +265,7 @@ jobs: fi - name: Login to GitHub Container Registry - uses: docker/login-action@v3.0.0 + uses: docker/login-action@v3.1.0 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -300,7 +288,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout the repository - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.1.2 - name: Initialize git uses: home-assistant/actions/helpers/git-init@master @@ -336,9 +324,12 @@ jobs: contents: read packages: write id-token: write + strategy: + matrix: + registry: ["ghcr.io/home-assistant", "docker.io/homeassistant"] steps: - name: Checkout the repository - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.1.2 - name: Install Cosign uses: sigstore/cosign-installer@v3.4.0 @@ -346,13 +337,15 @@ jobs: cosign-release: "v2.2.3" - name: Login to DockerHub - uses: docker/login-action@v3.0.0 + if: matrix.registry == 'docker.io/homeassistant' + uses: docker/login-action@v3.1.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to GitHub Container Registry - uses: docker/login-action@v3.0.0 + if: matrix.registry == 'ghcr.io/home-assistant' + uses: docker/login-action@v3.1.0 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -366,41 +359,37 @@ jobs: function create_manifest() { local tag_l=${1} local tag_r=${2} + local registry=${{ matrix.registry }} - for registry in "ghcr.io/home-assistant" "docker.io/homeassistant" - do + docker manifest create "${registry}/home-assistant:${tag_l}" \ + "${registry}/amd64-homeassistant:${tag_r}" \ + "${registry}/i386-homeassistant:${tag_r}" \ + "${registry}/armhf-homeassistant:${tag_r}" \ + "${registry}/armv7-homeassistant:${tag_r}" \ + "${registry}/aarch64-homeassistant:${tag_r}" - docker manifest create "${registry}/home-assistant:${tag_l}" \ - "${registry}/amd64-homeassistant:${tag_r}" \ - "${registry}/i386-homeassistant:${tag_r}" \ - "${registry}/armhf-homeassistant:${tag_r}" \ - "${registry}/armv7-homeassistant:${tag_r}" \ - "${registry}/aarch64-homeassistant:${tag_r}" + docker manifest annotate "${registry}/home-assistant:${tag_l}" \ + "${registry}/amd64-homeassistant:${tag_r}" \ + --os linux --arch amd64 - docker manifest annotate "${registry}/home-assistant:${tag_l}" \ - "${registry}/amd64-homeassistant:${tag_r}" \ - --os linux --arch amd64 + docker manifest annotate "${registry}/home-assistant:${tag_l}" \ + "${registry}/i386-homeassistant:${tag_r}" \ + --os linux --arch 386 - docker manifest annotate "${registry}/home-assistant:${tag_l}" \ - "${registry}/i386-homeassistant:${tag_r}" \ - --os linux --arch 386 + docker manifest annotate "${registry}/home-assistant:${tag_l}" \ + "${registry}/armhf-homeassistant:${tag_r}" \ + --os linux --arch arm --variant=v6 - docker manifest annotate "${registry}/home-assistant:${tag_l}" \ - "${registry}/armhf-homeassistant:${tag_r}" \ - --os linux --arch arm --variant=v6 + docker manifest annotate "${registry}/home-assistant:${tag_l}" \ + "${registry}/armv7-homeassistant:${tag_r}" \ + --os linux --arch arm --variant=v7 - docker manifest annotate "${registry}/home-assistant:${tag_l}" \ - "${registry}/armv7-homeassistant:${tag_r}" \ - --os linux --arch arm --variant=v7 + docker manifest annotate "${registry}/home-assistant:${tag_l}" \ + "${registry}/aarch64-homeassistant:${tag_r}" \ + --os linux --arch arm64 --variant=v8 - docker manifest annotate "${registry}/home-assistant:${tag_l}" \ - "${registry}/aarch64-homeassistant:${tag_r}" \ - --os linux --arch arm64 --variant=v8 - - docker manifest push --purge "${registry}/home-assistant:${tag_l}" - cosign sign --yes "${registry}/home-assistant:${tag_l}" - - done + docker manifest push --purge "${registry}/home-assistant:${tag_l}" + cosign sign --yes "${registry}/home-assistant:${tag_l}" } function validate_image() { @@ -433,12 +422,14 @@ jobs: validate_image "ghcr.io/home-assistant/armv7-homeassistant:${{ needs.init.outputs.version }}" validate_image "ghcr.io/home-assistant/aarch64-homeassistant:${{ needs.init.outputs.version }}" - # Upload images to dockerhub - push_dockerhub "amd64-homeassistant" "${{ needs.init.outputs.version }}" - push_dockerhub "i386-homeassistant" "${{ needs.init.outputs.version }}" - push_dockerhub "armhf-homeassistant" "${{ needs.init.outputs.version }}" - push_dockerhub "armv7-homeassistant" "${{ needs.init.outputs.version }}" - push_dockerhub "aarch64-homeassistant" "${{ needs.init.outputs.version }}" + if [[ "${{ matrix.registry }}" == "docker.io/homeassistant" ]]; then + # Upload images to dockerhub + push_dockerhub "amd64-homeassistant" "${{ needs.init.outputs.version }}" + push_dockerhub "i386-homeassistant" "${{ needs.init.outputs.version }}" + push_dockerhub "armhf-homeassistant" "${{ needs.init.outputs.version }}" + push_dockerhub "armv7-homeassistant" "${{ needs.init.outputs.version }}" + push_dockerhub "aarch64-homeassistant" "${{ needs.init.outputs.version }}" + fi # Create version tag create_manifest "${{ needs.init.outputs.version }}" "${{ needs.init.outputs.version }}" @@ -459,3 +450,44 @@ jobs: v="${{ needs.init.outputs.version }}" create_manifest "${v%.*}" "${{ needs.init.outputs.version }}" fi + + build_python: + name: Build PyPi package + environment: ${{ needs.init.outputs.channel }} + needs: ["init", "build_base"] + runs-on: ubuntu-latest + if: github.repository_owner == 'home-assistant' && needs.init.outputs.publish == 'true' + steps: + - name: Checkout the repository + uses: actions/checkout@v4.1.2 + + - name: Set up Python ${{ env.DEFAULT_PYTHON }} + uses: actions/setup-python@v5.1.0 + with: + python-version: ${{ env.DEFAULT_PYTHON }} + + - name: Download translations + uses: actions/download-artifact@v4.1.4 + with: + name: translations + + - name: Extract translations + run: | + tar xvf translations.tar.gz + rm translations.tar.gz + + - name: Build package + shell: bash + run: | + # Remove dist, build, and homeassistant.egg-info + # when build locally for testing! + pip install twine build + python -m build + + - name: Upload package + shell: bash + run: | + export TWINE_USERNAME="__token__" + export TWINE_PASSWORD="${{ secrets.TWINE_TOKEN }}" + + twine upload dist/* --skip-existing diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 9a898d11aa5..4a7e38f0110 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -34,11 +34,11 @@ on: env: CACHE_VERSION: 5 - PIP_CACHE_VERSION: 4 - MYPY_CACHE_VERSION: 7 - HA_SHORT_VERSION: "2024.3" - DEFAULT_PYTHON: "3.11" - ALL_PYTHON_VERSIONS: "['3.11', '3.12']" + UV_CACHE_VERSION: 1 + MYPY_CACHE_VERSION: 8 + HA_SHORT_VERSION: "2024.4" + DEFAULT_PYTHON: "3.12" + ALL_PYTHON_VERSIONS: "['3.12']" # 10.3 is the oldest supported version # - 10.3.32 is the version currently shipped with Synology (as of 17 Feb 2022) # 10.6 is the current long-term-support @@ -56,7 +56,7 @@ env: # - 15.2 is the latest (as of 9 Feb 2023) POSTGRESQL_VERSIONS: "['postgres:12.14','postgres:15.2']" PRE_COMMIT_CACHE: ~/.cache/pre-commit - PIP_CACHE: /tmp/pip-cache + UV_CACHE_DIR: /tmp/uv-cache SQLALCHEMY_WARN_20: 1 PYTHONASYNCIODEBUG: 1 HASS_CI: 1 @@ -89,7 +89,7 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Check out code from GitHub - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.1.2 - name: Generate partial Python venv restore key id: generate_python_cache_key run: >- @@ -103,7 +103,7 @@ jobs: echo "key=pre-commit-${{ env.CACHE_VERSION }}-${{ hashFiles('.pre-commit-config.yaml') }}" >> $GITHUB_OUTPUT - name: Filter for core changes - uses: dorny/paths-filter@v3.0.1 + uses: dorny/paths-filter@v3.0.2 id: core with: filters: .core_files.yaml @@ -118,7 +118,7 @@ jobs: echo "Result:" cat .integration_paths.yaml - name: Filter for integration changes - uses: dorny/paths-filter@v3.0.1 + uses: dorny/paths-filter@v3.0.2 id: integrations with: filters: .integration_paths.yaml @@ -222,16 +222,16 @@ jobs: - info steps: - name: Check out code from GitHub - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.1.2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v5.0.0 + uses: actions/setup-python@v5.1.0 with: python-version: ${{ env.DEFAULT_PYTHON }} check-latest: true - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v4.0.0 + uses: actions/cache@v4.0.2 with: path: venv key: >- @@ -243,10 +243,11 @@ jobs: python -m venv venv . venv/bin/activate python --version - pip install "$(cat requirements_test.txt | grep pre-commit)" + pip install "$(grep '^uv' < requirements_test.txt)" + uv pip install "$(cat requirements_test.txt | grep pre-commit)" - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v4.0.0 + uses: actions/cache@v4.0.2 with: path: ${{ env.PRE_COMMIT_CACHE }} lookup-only: true @@ -267,16 +268,16 @@ jobs: - pre-commit steps: - name: Check out code from GitHub - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.1.2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v5.0.0 + uses: actions/setup-python@v5.1.0 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} check-latest: true - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache/restore@v4.0.0 + uses: actions/cache/restore@v4.0.2 with: path: venv fail-on-cache-miss: true @@ -285,7 +286,7 @@ jobs: needs.info.outputs.pre-commit_cache_key }} - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache/restore@v4.0.0 + uses: actions/cache/restore@v4.0.2 with: path: ${{ env.PRE_COMMIT_CACHE }} fail-on-cache-miss: true @@ -307,16 +308,16 @@ jobs: - pre-commit steps: - name: Check out code from GitHub - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.1.2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v5.0.0 + uses: actions/setup-python@v5.1.0 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} check-latest: true - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache/restore@v4.0.0 + uses: actions/cache/restore@v4.0.2 with: path: venv fail-on-cache-miss: true @@ -325,7 +326,7 @@ jobs: needs.info.outputs.pre-commit_cache_key }} - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache/restore@v4.0.0 + uses: actions/cache/restore@v4.0.2 with: path: ${{ env.PRE_COMMIT_CACHE }} fail-on-cache-miss: true @@ -346,16 +347,16 @@ jobs: - pre-commit steps: - name: Check out code from GitHub - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.1.2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v5.0.0 + uses: actions/setup-python@v5.1.0 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} check-latest: true - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache/restore@v4.0.0 + uses: actions/cache/restore@v4.0.2 with: path: venv fail-on-cache-miss: true @@ -364,7 +365,7 @@ jobs: needs.info.outputs.pre-commit_cache_key }} - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache/restore@v4.0.0 + uses: actions/cache/restore@v4.0.2 with: path: ${{ env.PRE_COMMIT_CACHE }} fail-on-cache-miss: true @@ -440,37 +441,37 @@ jobs: python-version: ${{ fromJSON(needs.info.outputs.python_versions) }} steps: - name: Check out code from GitHub - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.1.2 - name: Set up Python ${{ matrix.python-version }} id: python - uses: actions/setup-python@v5.0.0 + uses: actions/setup-python@v5.1.0 with: python-version: ${{ matrix.python-version }} check-latest: true - - name: Generate partial pip restore key - id: generate-pip-key + - name: Generate partial uv restore key + id: generate-uv-key run: >- - echo "key=pip-${{ env.PIP_CACHE_VERSION }}-${{ + echo "key=uv-${{ env.UV_CACHE_VERSION }}-${{ env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v4.0.0 + uses: actions/cache@v4.0.2 with: path: venv lookup-only: true key: >- ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ needs.info.outputs.python_cache_key }} - - name: Restore pip wheel cache + - name: Restore uv wheel cache if: steps.cache-venv.outputs.cache-hit != 'true' - uses: actions/cache@v4.0.0 + uses: actions/cache@v4.0.2 with: - path: ${{ env.PIP_CACHE }} + path: ${{ env.UV_CACHE_DIR }} key: >- ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ - steps.generate-pip-key.outputs.key }} + steps.generate-uv-key.outputs.key }} restore-keys: | - ${{ runner.os }}-${{ steps.python.outputs.python-version }}-pip-${{ env.PIP_CACHE_VERSION }}-${{ env.HA_SHORT_VERSION }}- + ${{ runner.os }}-${{ steps.python.outputs.python-version }}-uv-${{ env.UV_CACHE_VERSION }}-${{ env.HA_SHORT_VERSION }}- - name: Install additional OS dependencies if: steps.cache-venv.outputs.cache-hit != 'true' run: | @@ -492,10 +493,11 @@ jobs: python -m venv venv . venv/bin/activate python --version - PIP_CACHE_DIR=$PIP_CACHE pip install -U "pip>=21.3.1" setuptools wheel - PIP_CACHE_DIR=$PIP_CACHE pip install -r requirements_all.txt - PIP_CACHE_DIR=$PIP_CACHE pip install -r requirements_test.txt - pip install -e . --config-settings editable_mode=compat + pip install "$(grep '^uv' < requirements_test.txt)" + uv pip install -U "pip>=21.3.1" setuptools wheel + uv pip install -r requirements_all.txt + uv pip install -r requirements_test.txt + uv pip install -e . --config-settings editable_mode=compat hassfest: name: Check hassfest @@ -508,16 +510,16 @@ jobs: - base steps: - name: Check out code from GitHub - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.1.2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v5.0.0 + uses: actions/setup-python@v5.1.0 with: python-version: ${{ env.DEFAULT_PYTHON }} check-latest: true - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment id: cache-venv - uses: actions/cache/restore@v4.0.0 + uses: actions/cache/restore@v4.0.2 with: path: venv fail-on-cache-miss: true @@ -540,16 +542,16 @@ jobs: - base steps: - name: Check out code from GitHub - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.1.2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v5.0.0 + uses: actions/setup-python@v5.1.0 with: python-version: ${{ env.DEFAULT_PYTHON }} check-latest: true - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache/restore@v4.0.0 + uses: actions/cache/restore@v4.0.2 with: path: venv fail-on-cache-miss: true @@ -573,16 +575,16 @@ jobs: - base steps: - name: Check out code from GitHub - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.1.2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v5.0.0 + uses: actions/setup-python@v5.1.0 with: python-version: ${{ env.DEFAULT_PYTHON }} check-latest: true - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment id: cache-venv - uses: actions/cache/restore@v4.0.0 + uses: actions/cache/restore@v4.0.2 with: path: venv fail-on-cache-miss: true @@ -617,10 +619,10 @@ jobs: - base steps: - name: Check out code from GitHub - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.1.2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v5.0.0 + uses: actions/setup-python@v5.1.0 with: python-version: ${{ env.DEFAULT_PYTHON }} check-latest: true @@ -633,7 +635,7 @@ jobs: env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment id: cache-venv - uses: actions/cache/restore@v4.0.0 + uses: actions/cache/restore@v4.0.2 with: path: venv fail-on-cache-miss: true @@ -641,7 +643,7 @@ jobs: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ needs.info.outputs.python_cache_key }} - name: Restore mypy cache - uses: actions/cache@v4.0.0 + uses: actions/cache@v4.0.2 with: path: .mypy_cache key: >- @@ -699,16 +701,16 @@ jobs: bluez \ ffmpeg - name: Check out code from GitHub - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.1.2 - name: Set up Python ${{ matrix.python-version }} id: python - uses: actions/setup-python@v5.0.0 + uses: actions/setup-python@v5.1.0 with: python-version: ${{ matrix.python-version }} check-latest: true - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache/restore@v4.0.0 + uses: actions/cache/restore@v4.0.2 with: path: venv fail-on-cache-miss: true @@ -717,13 +719,6 @@ jobs: - name: Register Python problem matcher run: | echo "::add-matcher::.github/workflows/matchers/python.json" - - name: Install Pytest Annotation plugin - run: | - . venv/bin/activate - # Ideally this should be part of our dependencies - # However this plugin is fairly new and doesn't run correctly - # on a non-GitHub environment. - pip install pytest-github-actions-annotate-failures==0.1.3 - name: Register pytest slow test problem matcher run: | echo "::add-matcher::.github/workflows/matchers/pytest-slow.json" @@ -797,10 +792,11 @@ jobs: 2>&1 | tee pytest-${{ matrix.python-version }}-${{ matrix.group }}.txt - name: Upload pytest output if: success() || failure() && (steps.pytest-full.conclusion == 'failure' || steps.pytest-partial.conclusion == 'failure') - uses: actions/upload-artifact@v3.1.2 + uses: actions/upload-artifact@v4.3.1 with: - name: pytest-${{ github.run_number }} + name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ matrix.group }} path: pytest-*.txt + overwrite: true - name: Upload coverage artifact if: needs.info.outputs.skip_coverage != 'true' uses: actions/upload-artifact@v4.3.1 @@ -852,16 +848,16 @@ jobs: ffmpeg \ libmariadb-dev-compat - name: Check out code from GitHub - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.1.2 - name: Set up Python ${{ matrix.python-version }} id: python - uses: actions/setup-python@v5.0.0 + uses: actions/setup-python@v5.1.0 with: python-version: ${{ matrix.python-version }} check-latest: true - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache/restore@v4.0.0 + uses: actions/cache/restore@v4.0.2 with: path: venv fail-on-cache-miss: true @@ -870,20 +866,13 @@ jobs: - name: Register Python problem matcher run: | echo "::add-matcher::.github/workflows/matchers/python.json" - - name: Install Pytest Annotation plugin - run: | - . venv/bin/activate - # Ideally this should be part of our dependencies - # However this plugin is fairly new and doesn't run correctly - # on a non-GitHub environment. - pip install pytest-github-actions-annotate-failures==0.1.3 - name: Register pytest slow test problem matcher run: | echo "::add-matcher::.github/workflows/matchers/pytest-slow.json" - name: Install SQL Python libraries run: | . venv/bin/activate - pip install mysqlclient sqlalchemy_utils + uv pip install mysqlclient sqlalchemy_utils - name: Compile English translations run: | . venv/bin/activate @@ -923,10 +912,12 @@ jobs: 2>&1 | tee pytest-${{ matrix.python-version }}-${mariadb}.txt - name: Upload pytest output if: success() || failure() && steps.pytest-partial.conclusion == 'failure' - uses: actions/upload-artifact@v3.1.2 + uses: actions/upload-artifact@v4.3.1 with: - name: pytest-${{ github.run_number }} + name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ + steps.pytest-partial.outputs.mariadb }} path: pytest-*.txt + overwrite: true - name: Upload coverage artifact if: needs.info.outputs.skip_coverage != 'true' uses: actions/upload-artifact@v4.3.1 @@ -979,16 +970,16 @@ jobs: ffmpeg \ postgresql-server-dev-14 - name: Check out code from GitHub - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.1.2 - name: Set up Python ${{ matrix.python-version }} id: python - uses: actions/setup-python@v5.0.0 + uses: actions/setup-python@v5.1.0 with: python-version: ${{ matrix.python-version }} check-latest: true - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache/restore@v4.0.0 + uses: actions/cache/restore@v4.0.2 with: path: venv fail-on-cache-miss: true @@ -997,20 +988,13 @@ jobs: - name: Register Python problem matcher run: | echo "::add-matcher::.github/workflows/matchers/python.json" - - name: Install Pytest Annotation plugin - run: | - . venv/bin/activate - # Ideally this should be part of our dependencies - # However this plugin is fairly new and doesn't run correctly - # on a non-GitHub environment. - pip install pytest-github-actions-annotate-failures==0.1.3 - name: Register pytest slow test problem matcher run: | echo "::add-matcher::.github/workflows/matchers/pytest-slow.json" - name: Install SQL Python libraries run: | . venv/bin/activate - pip install psycopg2 sqlalchemy_utils + uv pip install psycopg2 sqlalchemy_utils - name: Compile English translations run: | . venv/bin/activate @@ -1051,10 +1035,12 @@ jobs: 2>&1 | tee pytest-${{ matrix.python-version }}-${postgresql}.txt - name: Upload pytest output if: success() || failure() && steps.pytest-partial.conclusion == 'failure' - uses: actions/upload-artifact@v3.1.2 + uses: actions/upload-artifact@v4.3.1 with: - name: pytest-${{ github.run_number }} + name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ + steps.pytest-partial.outputs.postgresql }} path: pytest-*.txt + overwrite: true - name: Upload coverage artifact if: needs.info.outputs.skip_coverage != 'true' uses: actions/upload-artifact@v4.3.1 @@ -1077,14 +1063,14 @@ jobs: timeout-minutes: 10 steps: - name: Check out code from GitHub - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.1.2 - name: Download all coverage artifacts - uses: actions/download-artifact@v4.1.3 + uses: actions/download-artifact@v4.1.4 with: pattern: coverage-* - name: Upload coverage to Codecov (full coverage) if: needs.info.outputs.test_full_suite == 'true' - uses: Wandalen/wretry.action@v1.4.4 + uses: Wandalen/wretry.action@v2.1.0 with: action: codecov/codecov-action@v3.1.3 with: | @@ -1095,7 +1081,7 @@ jobs: attempt_delay: 30000 - name: Upload coverage to Codecov (partial coverage) if: needs.info.outputs.test_full_suite == 'false' - uses: Wandalen/wretry.action@v1.4.4 + uses: Wandalen/wretry.action@v2.1.0 with: action: codecov/codecov-action@v3.1.3 with: | diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index f7d97de0022..aa9822e0131 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -21,14 +21,14 @@ jobs: steps: - name: Check out code from GitHub - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.1.2 - name: Initialize CodeQL - uses: github/codeql-action/init@v3.24.5 + uses: github/codeql-action/init@v3.24.9 with: languages: python - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3.24.5 + uses: github/codeql-action/analyze@v3.24.9 with: category: "/language:python" diff --git a/.github/workflows/translations.yml b/.github/workflows/translations.yml index c8e25cc83ea..e61eef36f0b 100644 --- a/.github/workflows/translations.yml +++ b/.github/workflows/translations.yml @@ -19,10 +19,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout the repository - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.1.2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v5.0.0 + uses: actions/setup-python@v5.1.0 with: python-version: ${{ env.DEFAULT_PYTHON }} diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index bae60e8e945..9f127acb57d 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -28,7 +28,7 @@ jobs: architectures: ${{ steps.info.outputs.architectures }} steps: - name: Checkout the repository - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.1.2 - name: Get information id: info @@ -88,15 +88,15 @@ jobs: arch: ${{ fromJson(needs.init.outputs.architectures) }} steps: - name: Checkout the repository - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.1.2 - name: Download env_file - uses: actions/download-artifact@v4.1.3 + uses: actions/download-artifact@v4.1.4 with: name: env_file - name: Download requirements_diff - uses: actions/download-artifact@v4.1.3 + uses: actions/download-artifact@v4.1.4 with: name: requirements_diff @@ -126,15 +126,15 @@ jobs: arch: ${{ fromJson(needs.init.outputs.architectures) }} steps: - name: Checkout the repository - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.1.2 - name: Download env_file - uses: actions/download-artifact@v4.1.3 + uses: actions/download-artifact@v4.1.4 with: name: env_file - name: Download requirements_diff - uses: actions/download-artifact@v4.1.3 + uses: actions/download-artifact@v4.1.4 with: name: requirements_diff diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4b96b5ee2aa..ef4cdd98efb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.2.1 + rev: v0.3.4 hooks: - id: ruff args: @@ -8,11 +8,11 @@ repos: - id: ruff-format files: ^((homeassistant|pylint|script|tests)/.+)?[^/]+\.py$ - repo: https://github.com/codespell-project/codespell - rev: v2.2.2 + rev: v2.2.6 hooks: - id: codespell args: - - --ignore-words-list=additionals,alle,alot,bund,currenty,datas,farenheit,falsy,fo,haa,hass,iif,incomfort,ines,ist,nam,nd,pres,pullrequests,resset,rime,ser,serie,te,technik,ue,unsecure,withing,zar + - --ignore-words-list=additionals,alle,alot,astroid,bund,caf,convencional,currenty,datas,farenheit,falsy,fo,frequence,haa,hass,iif,incomfort,ines,ist,nam,nd,pres,pullrequests,resset,rime,ser,serie,te,technik,ue,unsecure,vor,withing,zar - --skip="./.*,*.csv,*.json,*.ambr" - --quiet-level=2 exclude_types: [csv, json] @@ -30,7 +30,7 @@ repos: - --branch=master - --branch=rc - repo: https://github.com/adrienverge/yamllint.git - rev: v1.32.0 + rev: v1.35.1 hooks: - id: yamllint - repo: https://github.com/pre-commit/mirrors-prettier diff --git a/.prettierignore b/.prettierignore index b249b537137..c6329099666 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,6 +1,5 @@ *.md .strict-typing -azure-*.yml homeassistant/components/*/translations/*.json homeassistant/generated/* tests/components/lidarr/fixtures/initialize.js diff --git a/.strict-typing b/.strict-typing index 74535719bb3..fb621d3e53a 100644 --- a/.strict-typing +++ b/.strict-typing @@ -228,6 +228,7 @@ homeassistant.components.homekit_controller.select homeassistant.components.homekit_controller.storage homeassistant.components.homekit_controller.utils homeassistant.components.homewizard.* +homeassistant.components.homeworks.* homeassistant.components.http.* homeassistant.components.huawei_lte.* homeassistant.components.humidifier.* diff --git a/.yamllint b/.yamllint index d8387c634ee..aa14d14c173 100644 --- a/.yamllint +++ b/.yamllint @@ -1,5 +1,4 @@ ignore: | - azure-*.yml tests/fixtures/core/config/yaml_errors/ rules: braces: diff --git a/CODEOWNERS b/CODEOWNERS index 759d3cd84d3..85603250b7c 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -309,14 +309,16 @@ build.json @home-assistant/supervisor /tests/components/doorbird/ @oblogic7 @bdraco @flacjacket /homeassistant/components/dormakaba_dkey/ @emontnemery /tests/components/dormakaba_dkey/ @emontnemery +/homeassistant/components/downloader/ @erwindouna +/tests/components/downloader/ @erwindouna /homeassistant/components/dremel_3d_printer/ @tkdrob /tests/components/dremel_3d_printer/ @tkdrob /homeassistant/components/drop_connect/ @ChandlerSystems @pfrazer /tests/components/drop_connect/ @ChandlerSystems @pfrazer /homeassistant/components/dsmr/ @Robbie1221 @frenck /tests/components/dsmr/ @Robbie1221 @frenck -/homeassistant/components/dsmr_reader/ @depl0y @glodenox -/tests/components/dsmr_reader/ @depl0y @glodenox +/homeassistant/components/dsmr_reader/ @sorted-bits @glodenox +/tests/components/dsmr_reader/ @sorted-bits @glodenox /homeassistant/components/duotecno/ @cereal2nd /tests/components/duotecno/ @cereal2nd /homeassistant/components/dwd_weather_warnings/ @runningman84 @stephan192 @andarotajo @@ -453,6 +455,8 @@ build.json @home-assistant/supervisor /tests/components/frontier_silicon/ @wlcrs /homeassistant/components/fully_kiosk/ @cgarwood /tests/components/fully_kiosk/ @cgarwood +/homeassistant/components/fyta/ @dontinelli +/tests/components/fyta/ @dontinelli /homeassistant/components/garages_amsterdam/ @klaasnicolaas /tests/components/garages_amsterdam/ @klaasnicolaas /homeassistant/components/gardena_bluetooth/ @elupus @@ -568,8 +572,8 @@ build.json @home-assistant/supervisor /tests/components/homekit/ @bdraco /homeassistant/components/homekit_controller/ @Jc2k @bdraco /tests/components/homekit_controller/ @Jc2k @bdraco -/homeassistant/components/homematic/ @pvizeli @danielperna84 -/tests/components/homematic/ @pvizeli @danielperna84 +/homeassistant/components/homematic/ @pvizeli +/tests/components/homematic/ @pvizeli /homeassistant/components/homewizard/ @DCSBL /tests/components/homewizard/ @DCSBL /homeassistant/components/honeywell/ @rdfurman @mkmer @@ -837,6 +841,8 @@ build.json @home-assistant/supervisor /tests/components/mopeka/ @bdraco /homeassistant/components/motion_blinds/ @starkillerOG /tests/components/motion_blinds/ @starkillerOG +/homeassistant/components/motionblinds_ble/ @LennP @jerrybboy +/tests/components/motionblinds_ble/ @LennP @jerrybboy /homeassistant/components/motioneye/ @dermotduffy /tests/components/motioneye/ @dermotduffy /homeassistant/components/motionmount/ @RJPoelstra @@ -860,8 +866,8 @@ build.json @home-assistant/supervisor /tests/components/nam/ @bieniu /homeassistant/components/nanoleaf/ @milanmeu /tests/components/nanoleaf/ @milanmeu -/homeassistant/components/neato/ @dshokouhi @Santobert -/tests/components/neato/ @dshokouhi @Santobert +/homeassistant/components/neato/ @Santobert +/tests/components/neato/ @Santobert /homeassistant/components/nederlandse_spoorwegen/ @YarmoM /homeassistant/components/ness_alarm/ @nickw444 /tests/components/ness_alarm/ @nickw444 @@ -927,6 +933,8 @@ build.json @home-assistant/supervisor /homeassistant/components/octoprint/ @rfleming71 /tests/components/octoprint/ @rfleming71 /homeassistant/components/ohmconnect/ @robbiet480 +/homeassistant/components/ollama/ @synesthesiam +/tests/components/ollama/ @synesthesiam /homeassistant/components/ombi/ @larssont /homeassistant/components/omnilogic/ @oliver84 @djtimca @gentoosu /tests/components/omnilogic/ @oliver84 @djtimca @gentoosu @@ -1095,7 +1103,6 @@ build.json @home-assistant/supervisor /tests/components/recovery_mode/ @home-assistant/core /homeassistant/components/refoss/ @ashionky /tests/components/refoss/ @ashionky -/homeassistant/components/rejseplanen/ @DarkFox /homeassistant/components/remote/ @home-assistant/core /tests/components/remote/ @home-assistant/core /homeassistant/components/renault/ @epenet @@ -1191,6 +1198,8 @@ build.json @home-assistant/supervisor /tests/components/senz/ @milanmeu /homeassistant/components/serial/ @fabaff /homeassistant/components/seven_segments/ @fabaff +/homeassistant/components/seventeentrack/ @shaiu +/tests/components/seventeentrack/ @shaiu /homeassistant/components/sfr_box/ @epenet /tests/components/sfr_box/ @epenet /homeassistant/components/sharkiq/ @JeffResc @funkybunch diff --git a/Dockerfile b/Dockerfile index da46f71ad22..2a27402be6c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,47 +6,47 @@ FROM ${BUILD_FROM} # Synchronize with homeassistant/core.py:async_stop ENV \ - S6_SERVICES_GRACETIME=240000 + S6_SERVICES_GRACETIME=240000 \ + UV_SYSTEM_PYTHON=true ARG QEMU_CPU +# Install uv +RUN pip3 install uv==0.1.24 + WORKDIR /usr/src ## Setup Home Assistant Core dependencies COPY requirements.txt homeassistant/ COPY homeassistant/package_constraints.txt homeassistant/homeassistant/ RUN \ - pip3 install \ - --only-binary=:all: \ + uv pip install \ + --no-build \ -r homeassistant/requirements.txt COPY requirements_all.txt home_assistant_frontend-* home_assistant_intents-* homeassistant/ RUN \ - if ls homeassistant/home_assistant_frontend*.whl 1> /dev/null 2>&1; then \ - pip3 install homeassistant/home_assistant_frontend-*.whl; \ - fi \ - && if ls homeassistant/home_assistant_intents*.whl 1> /dev/null 2>&1; then \ - pip3 install homeassistant/home_assistant_intents-*.whl; \ + if ls homeassistant/home_assistant_*.whl 1> /dev/null 2>&1; then \ + uv pip install homeassistant/home_assistant_*.whl; \ fi \ && if [ "${BUILD_ARCH}" = "i386" ]; then \ LD_PRELOAD="/usr/local/lib/libjemalloc.so.2" \ MALLOC_CONF="background_thread:true,metadata_thp:auto,dirty_decay_ms:20000,muzzy_decay_ms:20000" \ - linux32 pip3 install \ - --only-binary=:all: \ + linux32 uv pip install \ + --no-build \ -r homeassistant/requirements_all.txt; \ else \ LD_PRELOAD="/usr/local/lib/libjemalloc.so.2" \ MALLOC_CONF="background_thread:true,metadata_thp:auto,dirty_decay_ms:20000,muzzy_decay_ms:20000" \ - pip3 install \ - --only-binary=:all: \ + uv pip install \ + --no-build \ -r homeassistant/requirements_all.txt; \ fi ## Setup Home Assistant Core COPY . homeassistant/ RUN \ - pip3 install \ - --only-binary=:all: \ + uv pip install \ -e ./homeassistant \ && python3 -m compileall \ homeassistant/homeassistant diff --git a/Dockerfile.dev b/Dockerfile.dev index 453b922cd0b..e60456f7b1f 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/vscode/devcontainers/python:0-3.11 +FROM mcr.microsoft.com/devcontainers/python:1-3.12 SHELL ["/bin/bash", "-o", "pipefail", "-c"] diff --git a/build.yaml b/build.yaml index f6ffac3bd1d..044358b1f9d 100644 --- a/build.yaml +++ b/build.yaml @@ -1,10 +1,10 @@ image: ghcr.io/home-assistant/{arch}-homeassistant build_from: - aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2024.02.1 - armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2024.02.1 - armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2024.02.1 - amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2024.02.1 - i386: ghcr.io/home-assistant/i386-homeassistant-base:2024.02.1 + aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2024.03.0 + armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2024.03.0 + armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2024.03.0 + amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2024.03.0 + i386: ghcr.io/home-assistant/i386-homeassistant-base:2024.03.0 codenotary: signer: notary@home-assistant.io base_image: notary@home-assistant.io diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index 4ea324878ec..4ed80c27bf0 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -1,4 +1,5 @@ """Start Home Assistant.""" + from __future__ import annotations import argparse diff --git a/homeassistant/auth/__init__.py b/homeassistant/auth/__init__.py index f99e90dbc05..969fcc3529e 100644 --- a/homeassistant/auth/__init__.py +++ b/homeassistant/auth/__init__.py @@ -1,4 +1,5 @@ """Provide an authentication layer for Home Assistant.""" + from __future__ import annotations import asyncio @@ -19,13 +20,13 @@ from homeassistant.core import ( HomeAssistant, callback, ) -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.util import dt as dt_util from . import auth_store, jwt_wrapper, models from .const import ACCESS_TOKEN_EXPIRATION, GROUP_ID_ADMIN, REFRESH_TOKEN_EXPIRATION from .mfa_modules import MultiFactorAuthModule, auth_mfa_module_from_config +from .models import AuthFlowResult from .providers import AuthProvider, LoginFlow, auth_provider_from_config EVENT_USER_ADDED = "user_added" @@ -88,9 +89,13 @@ async def auth_manager_from_config( return manager -class AuthManagerFlowManager(data_entry_flow.FlowManager): +class AuthManagerFlowManager( + data_entry_flow.FlowManager[AuthFlowResult, tuple[str, str]] +): """Manage authentication flows.""" + _flow_result = AuthFlowResult + def __init__(self, hass: HomeAssistant, auth_manager: AuthManager) -> None: """Init auth manager flows.""" super().__init__(hass) @@ -98,11 +103,11 @@ class AuthManagerFlowManager(data_entry_flow.FlowManager): async def async_create_flow( self, - handler_key: str, + handler_key: tuple[str, str], *, context: dict[str, Any] | None = None, data: dict[str, Any] | None = None, - ) -> data_entry_flow.FlowHandler: + ) -> LoginFlow: """Create a login flow.""" auth_provider = self.auth_manager.get_auth_provider(*handler_key) if not auth_provider: @@ -110,8 +115,10 @@ class AuthManagerFlowManager(data_entry_flow.FlowManager): return await auth_provider.async_login_flow(context) async def async_finish_flow( - self, flow: data_entry_flow.FlowHandler, result: FlowResult - ) -> FlowResult: + self, + flow: data_entry_flow.FlowHandler[AuthFlowResult, tuple[str, str]], + result: AuthFlowResult, + ) -> AuthFlowResult: """Return a user as result of login flow.""" flow = cast(LoginFlow, flow) diff --git a/homeassistant/auth/auth_store.py b/homeassistant/auth/auth_store.py index 983ba7da6a1..b3481acca3c 100644 --- a/homeassistant/auth/auth_store.py +++ b/homeassistant/auth/auth_store.py @@ -1,4 +1,5 @@ """Storage for auth models.""" + from __future__ import annotations from datetime import timedelta @@ -30,6 +31,17 @@ GROUP_NAME_ADMIN = "Administrators" GROUP_NAME_USER = "Users" GROUP_NAME_READ_ONLY = "Read Only" +# We always save the auth store after we load it since +# we may migrate data and do not want to have to do it again +# but we don't want to do it during startup so we schedule +# the first save 5 minutes out knowing something else may +# want to save the auth store before then, and since Storage +# will honor the lower of the two delays, it will save it +# faster if something else saves it. +INITIAL_LOAD_SAVE_DELAY = 300 + +DEFAULT_SAVE_DELAY = 1 + class AuthStore: """Stores authentication info. @@ -467,12 +479,12 @@ class AuthStore: self._groups = groups self._users = users - self._async_schedule_save() + self._async_schedule_save(INITIAL_LOAD_SAVE_DELAY) @callback - def _async_schedule_save(self) -> None: + def _async_schedule_save(self, delay: float = DEFAULT_SAVE_DELAY) -> None: """Save users.""" - self._store.async_delay_save(self._data_to_save, 1) + self._store.async_delay_save(self._data_to_save, delay) @callback def _data_to_save(self) -> dict[str, list[dict[str, Any]]]: diff --git a/homeassistant/auth/const.py b/homeassistant/auth/const.py index 704f5d1d57c..05b9e6d7ad6 100644 --- a/homeassistant/auth/const.py +++ b/homeassistant/auth/const.py @@ -1,4 +1,5 @@ """Constants for the auth module.""" + from datetime import timedelta ACCESS_TOKEN_EXPIRATION = timedelta(minutes=30) diff --git a/homeassistant/auth/jwt_wrapper.py b/homeassistant/auth/jwt_wrapper.py index c681df66557..58f9260ff8f 100644 --- a/homeassistant/auth/jwt_wrapper.py +++ b/homeassistant/auth/jwt_wrapper.py @@ -4,6 +4,7 @@ Since we decode the same tokens over and over again we can cache the result of the decode of valid tokens to speed up the process. """ + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/auth/mfa_modules/__init__.py b/homeassistant/auth/mfa_modules/__init__.py index aa28710d8c6..fd4072ea88a 100644 --- a/homeassistant/auth/mfa_modules/__init__.py +++ b/homeassistant/auth/mfa_modules/__init__.py @@ -1,7 +1,7 @@ """Pluggable auth modules for Home Assistant.""" + from __future__ import annotations -import importlib import logging import types from typing import Any @@ -14,6 +14,7 @@ from homeassistant.const import CONF_ID, CONF_NAME, CONF_TYPE from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.importlib import async_import_module from homeassistant.util.decorator import Registry MULTI_FACTOR_AUTH_MODULES: Registry[str, type[MultiFactorAuthModule]] = Registry() @@ -148,7 +149,7 @@ async def _load_mfa_module(hass: HomeAssistant, module_name: str) -> types.Modul module_path = f"homeassistant.auth.mfa_modules.{module_name}" try: - module = importlib.import_module(module_path) + module = await async_import_module(hass, module_path) except ImportError as err: _LOGGER.error("Unable to load mfa module %s: %s", module_name, err) raise HomeAssistantError( diff --git a/homeassistant/auth/mfa_modules/insecure_example.py b/homeassistant/auth/mfa_modules/insecure_example.py index a50b762b121..fc696fe1b63 100644 --- a/homeassistant/auth/mfa_modules/insecure_example.py +++ b/homeassistant/auth/mfa_modules/insecure_example.py @@ -1,4 +1,5 @@ """Example auth module.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/auth/mfa_modules/notify.py b/homeassistant/auth/mfa_modules/notify.py index 4c5b3a2380b..72edb195a81 100644 --- a/homeassistant/auth/mfa_modules/notify.py +++ b/homeassistant/auth/mfa_modules/notify.py @@ -2,6 +2,7 @@ Sending HOTP through notify service """ + from __future__ import annotations import asyncio diff --git a/homeassistant/auth/mfa_modules/totp.py b/homeassistant/auth/mfa_modules/totp.py index 0c02ef4bd8d..e9055b45f05 100644 --- a/homeassistant/auth/mfa_modules/totp.py +++ b/homeassistant/auth/mfa_modules/totp.py @@ -1,4 +1,5 @@ """Time-based One Time Password auth module.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/auth/models.py b/homeassistant/auth/models.py index 4cf94401478..2e5f5940544 100644 --- a/homeassistant/auth/models.py +++ b/homeassistant/auth/models.py @@ -1,4 +1,5 @@ """Auth models.""" + from __future__ import annotations from datetime import datetime, timedelta @@ -11,6 +12,7 @@ from attr import Attribute from attr.setters import validate from homeassistant.const import __version__ +from homeassistant.data_entry_flow import FlowResult from homeassistant.util import dt as dt_util from . import permissions as perm_mdl @@ -26,6 +28,8 @@ TOKEN_TYPE_NORMAL = "normal" TOKEN_TYPE_SYSTEM = "system" TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN = "long_lived_access_token" +AuthFlowResult = FlowResult[tuple[str, str]] + @attr.s(slots=True) class Group: diff --git a/homeassistant/auth/permissions/__init__.py b/homeassistant/auth/permissions/__init__.py index 935f9bfbf65..c0574e9f0ea 100644 --- a/homeassistant/auth/permissions/__init__.py +++ b/homeassistant/auth/permissions/__init__.py @@ -1,4 +1,5 @@ """Permissions for Home Assistant.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/auth/permissions/const.py b/homeassistant/auth/permissions/const.py index e6c44036a7e..2a49971954e 100644 --- a/homeassistant/auth/permissions/const.py +++ b/homeassistant/auth/permissions/const.py @@ -1,4 +1,5 @@ """Permission constants.""" + CAT_ENTITIES = "entities" CAT_CONFIG_ENTRIES = "config_entries" SUBCAT_ALL = "all" diff --git a/homeassistant/auth/permissions/entities.py b/homeassistant/auth/permissions/entities.py index 4dc221a9ff4..dbe2fea0021 100644 --- a/homeassistant/auth/permissions/entities.py +++ b/homeassistant/auth/permissions/entities.py @@ -1,4 +1,5 @@ """Entity permissions.""" + from __future__ import annotations from collections import OrderedDict diff --git a/homeassistant/auth/permissions/events.py b/homeassistant/auth/permissions/events.py index aec23331664..3146cd99787 100644 --- a/homeassistant/auth/permissions/events.py +++ b/homeassistant/auth/permissions/events.py @@ -1,4 +1,5 @@ """Permission for events.""" + from __future__ import annotations from typing import Final diff --git a/homeassistant/auth/permissions/merge.py b/homeassistant/auth/permissions/merge.py index 121d87f7848..d0d43e2f088 100644 --- a/homeassistant/auth/permissions/merge.py +++ b/homeassistant/auth/permissions/merge.py @@ -1,4 +1,5 @@ """Merging of policies.""" + from __future__ import annotations from typing import cast @@ -57,10 +58,7 @@ def _merge_policies(sources: list[CategoryType]) -> CategoryType: continue seen.add(key) - key_sources = [] - for src in sources: - if isinstance(src, dict): - key_sources.append(src.get(key)) + key_sources = [src.get(key) for src in sources if isinstance(src, dict)] policy[key] = _merge_policies(key_sources) diff --git a/homeassistant/auth/permissions/models.py b/homeassistant/auth/permissions/models.py index 9b9c384c74d..086fdd7bd76 100644 --- a/homeassistant/auth/permissions/models.py +++ b/homeassistant/auth/permissions/models.py @@ -1,4 +1,5 @@ """Models for permissions.""" + from __future__ import annotations from typing import TYPE_CHECKING diff --git a/homeassistant/auth/permissions/system_policies.py b/homeassistant/auth/permissions/system_policies.py index b45984653fb..0d445802e7e 100644 --- a/homeassistant/auth/permissions/system_policies.py +++ b/homeassistant/auth/permissions/system_policies.py @@ -1,4 +1,5 @@ """System policies.""" + from .const import CAT_ENTITIES, POLICY_READ, SUBCAT_ALL ADMIN_POLICY = {CAT_ENTITIES: True} diff --git a/homeassistant/auth/permissions/types.py b/homeassistant/auth/permissions/types.py index cf3632d06d5..3411ae860fb 100644 --- a/homeassistant/auth/permissions/types.py +++ b/homeassistant/auth/permissions/types.py @@ -1,4 +1,5 @@ """Common code for permissions.""" + from collections.abc import Mapping # MyPy doesn't support recursion yet. So writing it out as far as we need. diff --git a/homeassistant/auth/permissions/util.py b/homeassistant/auth/permissions/util.py index 402d43b7ab7..db85e18f60c 100644 --- a/homeassistant/auth/permissions/util.py +++ b/homeassistant/auth/permissions/util.py @@ -1,4 +1,5 @@ """Helpers to deal with permissions.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/auth/providers/__init__.py b/homeassistant/auth/providers/__init__.py index 7d74dd2dc26..63028f54d2e 100644 --- a/homeassistant/auth/providers/__init__.py +++ b/homeassistant/auth/providers/__init__.py @@ -1,8 +1,8 @@ """Auth providers for Home Assistant.""" + from __future__ import annotations from collections.abc import Mapping -import importlib import logging import types from typing import Any @@ -13,14 +13,14 @@ from voluptuous.humanize import humanize_error from homeassistant import data_entry_flow, requirements from homeassistant.const import CONF_ID, CONF_NAME, CONF_TYPE from homeassistant.core import HomeAssistant, callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.importlib import async_import_module from homeassistant.util import dt as dt_util from homeassistant.util.decorator import Registry from ..auth_store import AuthStore from ..const import MFA_SESSION_EXPIRATION -from ..models import Credentials, RefreshToken, User, UserMeta +from ..models import AuthFlowResult, Credentials, RefreshToken, User, UserMeta _LOGGER = logging.getLogger(__name__) DATA_REQS = "auth_prov_reqs_processed" @@ -157,7 +157,9 @@ async def load_auth_provider_module( ) -> types.ModuleType: """Load an auth provider.""" try: - module = importlib.import_module(f"homeassistant.auth.providers.{provider}") + module = await async_import_module( + hass, f"homeassistant.auth.providers.{provider}" + ) except ImportError as err: _LOGGER.error("Unable to load auth provider %s: %s", provider, err) raise HomeAssistantError( @@ -181,9 +183,11 @@ async def load_auth_provider_module( return module -class LoginFlow(data_entry_flow.FlowHandler): +class LoginFlow(data_entry_flow.FlowHandler[AuthFlowResult, tuple[str, str]]): """Handler for the login flow.""" + _flow_result = AuthFlowResult + def __init__(self, auth_provider: AuthProvider) -> None: """Initialize the login flow.""" self._auth_provider = auth_provider @@ -197,7 +201,7 @@ class LoginFlow(data_entry_flow.FlowHandler): async def async_step_init( self, user_input: dict[str, str] | None = None - ) -> FlowResult: + ) -> AuthFlowResult: """Handle the first step of login flow. Return self.async_show_form(step_id='init') if user_input is None. @@ -207,7 +211,7 @@ class LoginFlow(data_entry_flow.FlowHandler): async def async_step_select_mfa_module( self, user_input: dict[str, str] | None = None - ) -> FlowResult: + ) -> AuthFlowResult: """Handle the step of select mfa module.""" errors = {} @@ -232,7 +236,7 @@ class LoginFlow(data_entry_flow.FlowHandler): async def async_step_mfa( self, user_input: dict[str, str] | None = None - ) -> FlowResult: + ) -> AuthFlowResult: """Handle the step of mfa validation.""" assert self.credential assert self.user @@ -282,6 +286,6 @@ class LoginFlow(data_entry_flow.FlowHandler): errors=errors, ) - async def async_finish(self, flow_result: Any) -> FlowResult: + async def async_finish(self, flow_result: Any) -> AuthFlowResult: """Handle the pass of login flow.""" return self.async_create_entry(data=flow_result) diff --git a/homeassistant/auth/providers/command_line.py b/homeassistant/auth/providers/command_line.py index 4ec2ca18611..43cde284a25 100644 --- a/homeassistant/auth/providers/command_line.py +++ b/homeassistant/auth/providers/command_line.py @@ -1,4 +1,5 @@ """Auth provider that validates credentials via an external command.""" + from __future__ import annotations import asyncio @@ -10,10 +11,9 @@ from typing import Any, cast import voluptuous as vol from homeassistant.const import CONF_COMMAND -from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError -from ..models import Credentials, UserMeta +from ..models import AuthFlowResult, Credentials, UserMeta from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow CONF_ARGS = "args" @@ -138,7 +138,7 @@ class CommandLineLoginFlow(LoginFlow): async def async_step_init( self, user_input: dict[str, str] | None = None - ) -> FlowResult: + ) -> AuthFlowResult: """Handle the step of the form.""" errors = {} diff --git a/homeassistant/auth/providers/homeassistant.py b/homeassistant/auth/providers/homeassistant.py index 6f621b93a6a..d277ce96fe2 100644 --- a/homeassistant/auth/providers/homeassistant.py +++ b/homeassistant/auth/providers/homeassistant.py @@ -1,4 +1,5 @@ """Home Assistant auth provider.""" + from __future__ import annotations import asyncio @@ -12,11 +13,10 @@ import voluptuous as vol from homeassistant.const import CONF_ID from homeassistant.core import HomeAssistant, callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.storage import Store -from ..models import Credentials, UserMeta +from ..models import AuthFlowResult, Credentials, UserMeta from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow STORAGE_VERSION = 1 @@ -321,7 +321,7 @@ class HassLoginFlow(LoginFlow): async def async_step_init( self, user_input: dict[str, str] | None = None - ) -> FlowResult: + ) -> AuthFlowResult: """Handle the step of the form.""" errors = {} diff --git a/homeassistant/auth/providers/insecure_example.py b/homeassistant/auth/providers/insecure_example.py index f7f01e74c27..8bcf7569f5a 100644 --- a/homeassistant/auth/providers/insecure_example.py +++ b/homeassistant/auth/providers/insecure_example.py @@ -1,4 +1,5 @@ """Example auth provider.""" + from __future__ import annotations from collections.abc import Mapping @@ -8,10 +9,9 @@ from typing import Any, cast import voluptuous as vol from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError -from ..models import Credentials, UserMeta +from ..models import AuthFlowResult, Credentials, UserMeta from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow USER_SCHEMA = vol.Schema( @@ -98,7 +98,7 @@ class ExampleLoginFlow(LoginFlow): async def async_step_init( self, user_input: dict[str, str] | None = None - ) -> FlowResult: + ) -> AuthFlowResult: """Handle the step of the form.""" errors = None diff --git a/homeassistant/auth/providers/legacy_api_password.py b/homeassistant/auth/providers/legacy_api_password.py index 98c246d74e4..f04490a354e 100644 --- a/homeassistant/auth/providers/legacy_api_password.py +++ b/homeassistant/auth/providers/legacy_api_password.py @@ -2,6 +2,7 @@ It will be removed when auth system production ready """ + from __future__ import annotations from collections.abc import Mapping @@ -11,12 +12,11 @@ from typing import Any, cast import voluptuous as vol from homeassistant.core import async_get_hass, callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue -from ..models import Credentials, UserMeta +from ..models import AuthFlowResult, Credentials, UserMeta from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow AUTH_PROVIDER_TYPE = "legacy_api_password" @@ -101,7 +101,7 @@ class LegacyLoginFlow(LoginFlow): async def async_step_init( self, user_input: dict[str, str] | None = None - ) -> FlowResult: + ) -> AuthFlowResult: """Handle the step of the form.""" errors = {} diff --git a/homeassistant/auth/providers/trusted_networks.py b/homeassistant/auth/providers/trusted_networks.py index cc195c14c23..32d1934e093 100644 --- a/homeassistant/auth/providers/trusted_networks.py +++ b/homeassistant/auth/providers/trusted_networks.py @@ -3,6 +3,7 @@ It shows list of users if access from trusted network. Abort login flow if not access from trusted network. """ + from __future__ import annotations from collections.abc import Mapping @@ -19,13 +20,12 @@ from typing import Any, cast import voluptuous as vol from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.network import is_cloud_connection from .. import InvalidAuthError -from ..models import Credentials, RefreshToken, UserMeta +from ..models import AuthFlowResult, Credentials, RefreshToken, UserMeta from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow IPAddress = IPv4Address | IPv6Address @@ -226,7 +226,7 @@ class TrustedNetworksLoginFlow(LoginFlow): async def async_step_init( self, user_input: dict[str, str] | None = None - ) -> FlowResult: + ) -> AuthFlowResult: """Handle the step of the form.""" try: cast( diff --git a/homeassistant/backports/enum.py b/homeassistant/backports/enum.py index 871244b4567..3c09d8e7f57 100644 --- a/homeassistant/backports/enum.py +++ b/homeassistant/backports/enum.py @@ -6,6 +6,7 @@ Since we have dropped support for Python 3.10, we can remove this backport. This file is kept for now to avoid breaking custom components that might import it. """ + from __future__ import annotations from enum import StrEnum diff --git a/homeassistant/backports/functools.py b/homeassistant/backports/functools.py index 6271bb87d14..8aab50eeb66 100644 --- a/homeassistant/backports/functools.py +++ b/homeassistant/backports/functools.py @@ -41,12 +41,10 @@ class cached_property(Generic[_T]): ) @overload - def __get__(self, instance: None, owner: type[Any] | None = None) -> Self: - ... + def __get__(self, instance: None, owner: type[Any] | None = None) -> Self: ... @overload - def __get__(self, instance: Any, owner: type[Any] | None = None) -> _T: - ... + def __get__(self, instance: Any, owner: type[Any] | None = None) -> _T: ... def __get__( self, instance: Any | None, owner: type[Any] | None = None diff --git a/homeassistant/block_async_io.py b/homeassistant/block_async_io.py index d7c1a7c9eea..bf805b5ef21 100644 --- a/homeassistant/block_async_io.py +++ b/homeassistant/block_async_io.py @@ -1,4 +1,5 @@ """Block blocking calls being done in asyncio.""" + from http.client import HTTPConnection import time diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 4fc9073b146..5b805b6138e 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -1,12 +1,15 @@ """Provide methods to bootstrap a Home Assistant instance.""" + from __future__ import annotations import asyncio +from collections import defaultdict import contextlib -from datetime import timedelta +from functools import partial +from itertools import chain import logging import logging.handlers -from operator import itemgetter +from operator import contains, itemgetter import os import platform import sys @@ -22,17 +25,35 @@ import yarl from . import config as conf_util, config_entries, core, loader, requirements -# Pre-import config and lovelace which have no requirements here to avoid +# Pre-import frontend deps which have no requirements here to avoid # loading them at run time and blocking the event loop. We do this ahead -# of time so that we do not have to flag frontends deps with `import_executor` +# of time so that we do not have to flag frontend deps with `import_executor` # as it would create a thundering heard of executor jobs trying to import # frontend deps at the same time. from .components import ( api as api_pre_import, # noqa: F401 + auth as auth_pre_import, # noqa: F401 config as config_pre_import, # noqa: F401 - http, + default_config as default_config_pre_import, # noqa: F401 + device_automation as device_automation_pre_import, # noqa: F401 + diagnostics as diagnostics_pre_import, # noqa: F401 + file_upload as file_upload_pre_import, # noqa: F401 + group as group_pre_import, # noqa: F401 + history as history_pre_import, # noqa: F401 + http, # not named pre_import since it has requirements + image_upload as image_upload_import, # noqa: F401 - not named pre_import since it has requirements + logbook as logbook_pre_import, # noqa: F401 lovelace as lovelace_pre_import, # noqa: F401 + onboarding as onboarding_pre_import, # noqa: F401 + recorder as recorder_import, # noqa: F401 - not named pre_import since it has requirements + repairs as repairs_pre_import, # noqa: F401 + search as search_pre_import, # noqa: F401 + sensor as sensor_pre_import, # noqa: F401 + system_log as system_log_pre_import, # noqa: F401 + webhook as webhook_pre_import, # noqa: F401 + websocket_api as websocket_api_pre_import, # noqa: F401 ) +from .components.sensor import recorder as sensor_recorder # noqa: F401 from .const import ( FORMAT_DATETIME, KEY_DATA_LOGGING as DATA_LOGGING, @@ -43,6 +64,7 @@ from .const import ( from .exceptions import HomeAssistantError from .helpers import ( area_registry, + category_registry, config_validation as cv, device_registry, entity, @@ -56,11 +78,13 @@ from .helpers import ( translation, ) from .helpers.dispatcher import async_dispatcher_send +from .helpers.storage import get_internal_store_manager +from .helpers.system_info import async_get_system_info from .helpers.typing import ConfigType from .setup import ( BASE_PLATFORMS, DATA_SETUP_STARTED, - DATA_SETUP_TIME, + async_get_setup_timings, async_notify_setup_error, async_set_domains_to_be_loaded, async_setup_component, @@ -69,11 +93,19 @@ from .util.async_ import create_eager_task from .util.logging import async_activate_log_queue_handler from .util.package import async_get_user_site, is_virtual_env +with contextlib.suppress(ImportError): + # Ensure anyio backend is imported to avoid it being imported in the event loop + from anyio._backends import _asyncio # noqa: F401 + + if TYPE_CHECKING: from .runner import RuntimeConfig _LOGGER = logging.getLogger(__name__) +SETUP_ORDER_SORT_KEY = partial(contains, BASE_PLATFORMS) + + ERROR_LOG_FILENAME = "home-assistant.log" # hass.data key for logging information. @@ -87,7 +119,6 @@ STAGE_2_TIMEOUT = 300 WRAP_UP_TIMEOUT = 300 COOLDOWN_TIME = 60 -MAX_LOAD_CONCURRENTLY = 6 DEBUGGER_INTEGRATIONS = {"debugpy"} CORE_INTEGRATIONS = {"homeassistant", "persistent_notification"} @@ -128,6 +159,7 @@ DEFAULT_INTEGRATIONS = { # These integrations are set up unless recovery mode is activated. # # Integrations providing core functionality: + "analytics", # Needed for onboarding "application_credentials", "backup", "frontend", @@ -168,16 +200,35 @@ CRITICAL_INTEGRATIONS = { "frontend", } -SETUP_ORDER = { +SETUP_ORDER = ( # Load logging as soon as possible - "logging": LOGGING_INTEGRATIONS, - # Setup frontend - "frontend": FRONTEND_INTEGRATIONS, - # Setup recorder - "recorder": RECORDER_INTEGRATIONS, + ("logging", LOGGING_INTEGRATIONS), + # Setup frontend and recorder + ("frontend, recorder", {*FRONTEND_INTEGRATIONS, *RECORDER_INTEGRATIONS}), # Start up debuggers. Start these first in case they want to wait. - "debugger": DEBUGGER_INTEGRATIONS, -} + ("debugger", DEBUGGER_INTEGRATIONS), +) + +# +# Storage keys we are likely to load during startup +# in order of when we expect to load them. +# +# If they do not exist they will not be loaded +# +PRELOAD_STORAGE = [ + "core.network", + "http.auth", + "image", + "lovelace_dashboards", + "lovelace_resources", + "core.uuid", + "lovelace.map", + "bluetooth.passive_update_processor", + "bluetooth.remote_scanners", + "assist_pipeline.pipelines", + "core.analytics", + "auth_module.totp", +] async def async_setup_hass( @@ -315,14 +366,16 @@ async def async_load_base_functionality(hass: core.HomeAssistant) -> None: 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 + _ = platform.uname().processor # Load the registries and cache the result of platform.uname().processor translation.async_setup(hass) entity.async_setup(hass) template.async_setup(hass) await asyncio.gather( + create_eager_task(get_internal_store_manager(hass).async_initialize()), create_eager_task(area_registry.async_load(hass)), + create_eager_task(category_registry.async_load(hass)), create_eager_task(device_registry.async_load(hass)), create_eager_task(entity_registry.async_load(hass)), create_eager_task(floor_registry.async_load(hass)), @@ -332,6 +385,7 @@ async def async_load_base_functionality(hass: core.HomeAssistant) -> None: create_eager_task(template.async_load_custom_templates(hass)), create_eager_task(restore_state.async_load(hass)), create_eager_task(hass.config_entries.async_initialize()), + create_eager_task(async_get_system_info(hass)), ) @@ -571,7 +625,9 @@ class _WatchPendingSetups: """Periodic log and dispatch of setups that are pending.""" def __init__( - self, hass: core.HomeAssistant, setup_started: dict[str, float] + self, + hass: core.HomeAssistant, + setup_started: dict[tuple[str, str | None], float], ) -> None: """Initialize the WatchPendingSetups class.""" self._hass = hass @@ -586,11 +642,15 @@ class _WatchPendingSetups: now = monotonic() self._duration_count += SLOW_STARTUP_CHECK_INTERVAL - remaining_with_setup_started = { - domain: (now - start_time) - for domain, start_time in self._setup_started.items() - } - _LOGGER.debug("Integration remaining: %s", remaining_with_setup_started) + remaining_with_setup_started: defaultdict[str, float] = defaultdict(float) + for integration_group, start_time in self._setup_started.items(): + domain, _ = integration_group + remaining_with_setup_started[domain] += now - start_time + + if remaining_with_setup_started: + _LOGGER.debug("Integration remaining: %s", remaining_with_setup_started) + elif waiting_tasks := self._hass._active_tasks: # pylint: disable=protected-access + _LOGGER.debug("Waiting on tasks: %s", waiting_tasks) self._async_dispatch(remaining_with_setup_started) if ( self._setup_started @@ -600,7 +660,7 @@ class _WatchPendingSetups: # once we take over LOG_SLOW_STARTUP_INTERVAL (60s) to start up _LOGGER.warning( "Waiting on integrations to complete setup: %s", - ", ".join(self._setup_started), + self._setup_started, ) _LOGGER.debug("Running timeout Zones: %s", self._hass.timeout.zones) @@ -640,13 +700,18 @@ async def async_setup_multi_components( """Set up multiple domains. Log on failure.""" # Avoid creating tasks for domains that were setup in a previous stage domains_not_yet_setup = domains - hass.config.components + # Create setup tasks for base platforms first since everything will have + # to wait to be imported, and the sooner we can get the base platforms + # loaded the sooner we can start loading the rest of the integrations. futures = { domain: hass.async_create_task( async_setup_component(hass, domain, config), f"setup component {domain}", eager_start=True, ) - for domain in domains_not_yet_setup + for domain in sorted( + domains_not_yet_setup, key=SETUP_ORDER_SORT_KEY, reverse=True + ) } results = await asyncio.gather(*futures.values(), return_exceptions=True) for idx, domain in enumerate(futures): @@ -663,26 +728,53 @@ async def _async_resolve_domains_to_setup( hass: core.HomeAssistant, config: dict[str, Any] ) -> tuple[set[str], dict[str, loader.Integration]]: """Resolve all dependencies and return list of domains to set up.""" - base_platforms_loaded = False domains_to_setup = _get_domains(hass, config) needed_requirements: set[str] = set() + platform_integrations = conf_util.extract_platform_integrations( + config, BASE_PLATFORMS + ) + # Ensure base platforms that have platform integrations are added to + # to `domains_to_setup so they can be setup first instead of + # discovering them when later when a config entry setup task + # notices its needed and there is already a long line to use + # the import executor. + # + # For example if we have + # sensor: + # - platform: template + # + # `template` has to be loaded to validate the config for sensor + # so we want to start loading `sensor` as soon as we know + # it will be needed. The more platforms under `sensor:`, the longer + # it will take to finish setup for `sensor` because each of these + # platforms has to be imported before we can validate the config. + # + # Thankfully we are migrating away from the platform pattern + # so this will be less of a problem in the future. + domains_to_setup.update(platform_integrations) + + # Load manifests for base platforms and platform based integrations + # that are defined under base platforms right away since we do not require + # the manifest to list them as dependencies and we want to avoid the lock + # contention when multiple integrations try to load them at once + additional_manifests_to_load = { + *BASE_PLATFORMS, + *chain.from_iterable(platform_integrations.values()), + } + + translations_to_load = additional_manifests_to_load.copy() # Resolve all dependencies so we know all integrations - # that will have to be loaded and start rightaway + # that will have to be loaded and start right-away integration_cache: dict[str, loader.Integration] = {} to_resolve: set[str] = domains_to_setup - while to_resolve: + while to_resolve or additional_manifests_to_load: old_to_resolve: set[str] = to_resolve to_resolve = set() - if not base_platforms_loaded: - # Load base platforms right away since - # we do not require the manifest to list - # them as dependencies and we want - # to avoid the lock contention when multiple - # integrations try to resolve them at once - base_platforms_loaded = True - to_get = {*old_to_resolve, *BASE_PLATFORMS} + if additional_manifests_to_load: + to_get = {*old_to_resolve, *additional_manifests_to_load} + additional_manifests_to_load.clear() else: to_get = old_to_resolve @@ -691,13 +783,27 @@ async def _async_resolve_domains_to_setup( integrations_to_process: list[loader.Integration] = [] for domain, itg in (await loader.async_get_integrations(hass, to_get)).items(): - if not isinstance(itg, loader.Integration) or domain not in old_to_resolve: + if not isinstance(itg, loader.Integration): continue - integrations_to_process.append(itg) integration_cache[domain] = itg + needed_requirements.update(itg.requirements) + + # Make sure manifests for dependencies are loaded in the next + # loop to try to group as many as manifest loads in a single + # call to avoid the creating one-off executor jobs later in + # the setup process + additional_manifests_to_load.update( + dep + for dep in chain(itg.dependencies, itg.after_dependencies) + if dep not in integration_cache + ) + + if domain not in old_to_resolve: + continue + + integrations_to_process.append(itg) manifest_deps.update(itg.dependencies) manifest_deps.update(itg.after_dependencies) - needed_requirements.update(itg.requirements) if not itg.all_dependencies_resolved: resolve_dependencies_tasks.append( create_eager_task( @@ -740,6 +846,12 @@ async def _async_resolve_domains_to_setup( "check installed requirements", eager_start=True, ) + + # + # Only add the domains_to_setup after we finish resolving + # as new domains are likely to added in the process + # + translations_to_load.update(domains_to_setup) # Start loading translations for all integrations we are going to set up # in the background so they are ready when we need them. This avoids a # lot of waiting for the translation load lock and a thundering herd of @@ -751,11 +863,22 @@ async def _async_resolve_domains_to_setup( # wait for the translation load lock, loading will be done by the # time it gets to it. hass.async_create_background_task( - translation.async_load_integrations(hass, {*BASE_PLATFORMS, *domains_to_setup}), + translation.async_load_integrations(hass, translations_to_load), "load translations", eager_start=True, ) + # Preload storage for all integrations we are going to set up + # so we do not have to wait for it to be loaded when we need it + # in the setup process. + hass.async_create_background_task( + get_internal_store_manager(hass).async_preload( + [*PRELOAD_STORAGE, *domains_to_setup] + ), + "preload storage", + eager_start=True, + ) + return domains_to_setup, integration_cache @@ -763,10 +886,8 @@ async def _async_set_up_integrations( hass: core.HomeAssistant, config: dict[str, Any] ) -> None: """Set up all the integrations.""" - setup_started: dict[str, float] = {} + setup_started: dict[tuple[str, str | None], float] = {} hass.data[DATA_SETUP_STARTED] = setup_started - setup_time: dict[str, timedelta] = hass.data.setdefault(DATA_SETUP_TIME, {}) - watcher = _WatchPendingSetups(hass, setup_started) watcher.async_start() @@ -778,10 +899,9 @@ async def _async_set_up_integrations( if "recorder" in domains_to_setup: recorder.async_initialize_recorder(hass) - pre_stage_domains: dict[str, set[str]] = { - name: domains_to_setup & domain_group - for name, domain_group in SETUP_ORDER.items() - } + pre_stage_domains = [ + (name, domains_to_setup & domain_group) for name, domain_group in SETUP_ORDER + ] # calculate what components to setup in what stage stage_1_domains: set[str] = set() @@ -807,10 +927,18 @@ async def _async_set_up_integrations( stage_2_domains = domains_to_setup - stage_1_domains - for name, domain_group in pre_stage_domains.items(): + for name, domain_group in pre_stage_domains: if domain_group: stage_2_domains -= domain_group _LOGGER.info("Setting up %s: %s", name, domain_group) + to_be_loaded = domain_group.copy() + to_be_loaded.update( + dep + for domain in domain_group + if (integration := integration_cache.get(domain)) is not None + for dep in integration.all_dependencies + ) + async_set_domains_to_be_loaded(hass, to_be_loaded) await async_setup_multi_components(hass, domain_group, config) # Enables after dependencies when setting up stage 1 domains @@ -825,7 +953,10 @@ async def _async_set_up_integrations( ): await async_setup_multi_components(hass, stage_1_domains, config) except TimeoutError: - _LOGGER.warning("Setup timed out for stage 1 - moving forward") + _LOGGER.warning( + "Setup timed out for stage 1 waiting on %s - moving forward", + hass._active_tasks, # pylint: disable=protected-access + ) # Add after dependencies when setting up stage 2 domains async_set_domains_to_be_loaded(hass, stage_2_domains) @@ -838,7 +969,10 @@ async def _async_set_up_integrations( ): await async_setup_multi_components(hass, stage_2_domains, config) except TimeoutError: - _LOGGER.warning("Setup timed out for stage 2 - moving forward") + _LOGGER.warning( + "Setup timed out for stage 2 waiting on %s - moving forward", + hass._active_tasks, # pylint: disable=protected-access + ) # Wrap up startup _LOGGER.debug("Waiting for startup to wrap up") @@ -846,11 +980,16 @@ async def _async_set_up_integrations( async with hass.timeout.async_timeout(WRAP_UP_TIMEOUT, cool_down=COOLDOWN_TIME): await hass.async_block_till_done() except TimeoutError: - _LOGGER.warning("Setup timed out for bootstrap - moving forward") + _LOGGER.warning( + "Setup timed out for bootstrap waiting on %s - moving forward", + hass._active_tasks, # pylint: disable=protected-access + ) watcher.async_stop() - _LOGGER.debug( - "Integration setup times: %s", - dict(sorted(setup_time.items(), key=itemgetter(1))), - ) + if _LOGGER.isEnabledFor(logging.DEBUG): + setup_time = async_get_setup_timings(hass) + _LOGGER.debug( + "Integration setup times: %s", + dict(sorted(setup_time.items(), key=itemgetter(1), reverse=True)), + ) diff --git a/homeassistant/brands/motionblinds.json b/homeassistant/brands/motionblinds.json new file mode 100644 index 00000000000..67013e75966 --- /dev/null +++ b/homeassistant/brands/motionblinds.json @@ -0,0 +1,5 @@ +{ + "domain": "motionblinds", + "name": "Motionblinds", + "integrations": ["motion_blinds", "motionblinds_ble"] +} diff --git a/homeassistant/components/__init__.py b/homeassistant/components/__init__.py index 839a66af25d..030e23628d6 100644 --- a/homeassistant/components/__init__.py +++ b/homeassistant/components/__init__.py @@ -6,11 +6,13 @@ Component design guidelines: format ".". - Each component should publish services only under its own domain. """ + from __future__ import annotations import logging from homeassistant.core import HomeAssistant, split_entity_id +from homeassistant.helpers.frame import report from homeassistant.helpers.group import expand_entity_ids _LOGGER = logging.getLogger(__name__) @@ -21,6 +23,15 @@ def is_on(hass: HomeAssistant, entity_id: str | None = None) -> bool: If there is no entity id given we will check all. """ + report( + ( + "uses homeassistant.components.is_on." + " This is deprecated and will stop working in Home Assistant 2024.9, it" + " should be updated to use the function of the platform directly." + ), + error_if_core=True, + ) + if entity_id: entity_ids = expand_entity_ids(hass, [entity_id]) else: diff --git a/homeassistant/components/abode/__init__.py b/homeassistant/components/abode/__init__.py index 55ce9e054c3..a27c2d93ead 100644 --- a/homeassistant/components/abode/__init__.py +++ b/homeassistant/components/abode/__init__.py @@ -1,4 +1,5 @@ """Support for the Abode Security System.""" + from __future__ import annotations from dataclasses import dataclass, field diff --git a/homeassistant/components/abode/alarm_control_panel.py b/homeassistant/components/abode/alarm_control_panel.py index 4671b71059d..333462a4d9f 100644 --- a/homeassistant/components/abode/alarm_control_panel.py +++ b/homeassistant/components/abode/alarm_control_panel.py @@ -1,10 +1,13 @@ """Support for Abode Security System alarm control panels.""" + from __future__ import annotations -from jaraco.abode.devices.alarm import Alarm as AbodeAl +from jaraco.abode.devices.alarm import Alarm -import homeassistant.components.alarm_control_panel as alarm -from homeassistant.components.alarm_control_panel import AlarmControlPanelEntityFeature +from homeassistant.components.alarm_control_panel import ( + AlarmControlPanelEntity, + AlarmControlPanelEntityFeature, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, @@ -28,7 +31,7 @@ async def async_setup_entry( ) -class AbodeAlarm(AbodeDevice, alarm.AlarmControlPanelEntity): +class AbodeAlarm(AbodeDevice, AlarmControlPanelEntity): """An alarm_control_panel implementation for Abode.""" _attr_name = None @@ -37,7 +40,7 @@ class AbodeAlarm(AbodeDevice, alarm.AlarmControlPanelEntity): AlarmControlPanelEntityFeature.ARM_HOME | AlarmControlPanelEntityFeature.ARM_AWAY ) - _device: AbodeAl + _device: Alarm @property def state(self) -> str | None: diff --git a/homeassistant/components/abode/binary_sensor.py b/homeassistant/components/abode/binary_sensor.py index a10dbc8e664..4968d5378e1 100644 --- a/homeassistant/components/abode/binary_sensor.py +++ b/homeassistant/components/abode/binary_sensor.py @@ -1,10 +1,17 @@ """Support for Abode Security System binary sensors.""" + from __future__ import annotations from typing import cast -from jaraco.abode.devices.sensor import BinarySensor as ABBinarySensor -from jaraco.abode.helpers import constants as CONST +from jaraco.abode.devices.sensor import BinarySensor +from jaraco.abode.helpers.constants import ( + TYPE_CONNECTIVITY, + TYPE_MOISTURE, + TYPE_MOTION, + TYPE_OCCUPANCY, + TYPE_OPENING, +) from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, @@ -26,11 +33,11 @@ async def async_setup_entry( data: AbodeSystem = hass.data[DOMAIN] device_types = [ - CONST.TYPE_CONNECTIVITY, - CONST.TYPE_MOISTURE, - CONST.TYPE_MOTION, - CONST.TYPE_OCCUPANCY, - CONST.TYPE_OPENING, + TYPE_CONNECTIVITY, + TYPE_MOISTURE, + TYPE_MOTION, + TYPE_OCCUPANCY, + TYPE_OPENING, ] async_add_entities( @@ -43,7 +50,7 @@ class AbodeBinarySensor(AbodeDevice, BinarySensorEntity): """A binary sensor implementation for Abode device.""" _attr_name = None - _device: ABBinarySensor + _device: BinarySensor @property def is_on(self) -> bool: diff --git a/homeassistant/components/abode/camera.py b/homeassistant/components/abode/camera.py index 326e845b16b..8ffa90a9b82 100644 --- a/homeassistant/components/abode/camera.py +++ b/homeassistant/components/abode/camera.py @@ -1,12 +1,14 @@ """Support for Abode Security System cameras.""" + from __future__ import annotations from datetime import timedelta from typing import Any, cast -from jaraco.abode.devices.base import Device as AbodeDev +from jaraco.abode.devices.base import Device from jaraco.abode.devices.camera import Camera as AbodeCam -from jaraco.abode.helpers import constants as CONST, timeline as TIMELINE +from jaraco.abode.helpers import timeline +from jaraco.abode.helpers.constants import TYPE_CAMERA import requests from requests.models import Response @@ -30,8 +32,8 @@ async def async_setup_entry( data: AbodeSystem = hass.data[DOMAIN] async_add_entities( - AbodeCamera(data, device, TIMELINE.CAPTURE_IMAGE) - for device in data.abode.get_devices(generic_type=CONST.TYPE_CAMERA) + AbodeCamera(data, device, timeline.CAPTURE_IMAGE) + for device in data.abode.get_devices(generic_type=TYPE_CAMERA) ) @@ -41,7 +43,7 @@ class AbodeCamera(AbodeDevice, Camera): _device: AbodeCam _attr_name = None - def __init__(self, data: AbodeSystem, device: AbodeDev, event: Event) -> None: + def __init__(self, data: AbodeSystem, device: Device, event: Event) -> None: """Initialize the Abode device.""" AbodeDevice.__init__(self, data, device) Camera.__init__(self) diff --git a/homeassistant/components/abode/config_flow.py b/homeassistant/components/abode/config_flow.py index 56cd673bc1b..57cad604274 100644 --- a/homeassistant/components/abode/config_flow.py +++ b/homeassistant/components/abode/config_flow.py @@ -1,4 +1,5 @@ """Config flow for the Abode Security System component.""" + from __future__ import annotations from collections.abc import Mapping @@ -14,16 +15,15 @@ from jaraco.abode.helpers.errors import MFA_CODE_REQUIRED from requests.exceptions import ConnectTimeout, HTTPError import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from homeassistant.data_entry_flow import FlowResult from .const import CONF_POLLING, DOMAIN, LOGGER CONF_MFA = "mfa_code" -class AbodeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class AbodeFlowHandler(ConfigFlow, domain=DOMAIN): """Config flow for Abode.""" VERSION = 1 @@ -43,7 +43,7 @@ class AbodeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self._polling: bool = False self._username: str | None = None - async def _async_abode_login(self, step_id: str) -> FlowResult: + async def _async_abode_login(self, step_id: str) -> ConfigFlowResult: """Handle login with Abode.""" errors = {} @@ -74,7 +74,7 @@ class AbodeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return await self._async_create_entry() - async def _async_abode_mfa_login(self) -> FlowResult: + async def _async_abode_mfa_login(self) -> ConfigFlowResult: """Handle multi-factor authentication (MFA) login with Abode.""" try: # Create instance to access login method for passing MFA code @@ -92,7 +92,7 @@ class AbodeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return await self._async_create_entry() - async def _async_create_entry(self) -> FlowResult: + async def _async_create_entry(self) -> ConfigFlowResult: """Create the config entry.""" config_data = { CONF_USERNAME: self._username, @@ -118,7 +118,7 @@ class AbodeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") @@ -135,7 +135,7 @@ class AbodeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_mfa( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a multi-factor authentication (MFA) flow.""" if user_input is None: return self.async_show_form( @@ -146,7 +146,9 @@ class AbodeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return await self._async_abode_mfa_login() - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Handle reauthorization request from Abode.""" self._username = entry_data[CONF_USERNAME] @@ -154,7 +156,7 @@ class AbodeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle reauthorization flow.""" if user_input is None: return self.async_show_form( diff --git a/homeassistant/components/abode/const.py b/homeassistant/components/abode/const.py index e24fe066823..7515b83ea07 100644 --- a/homeassistant/components/abode/const.py +++ b/homeassistant/components/abode/const.py @@ -1,4 +1,5 @@ """Constants for the Abode Security System component.""" + import logging LOGGER = logging.getLogger(__package__) diff --git a/homeassistant/components/abode/cover.py b/homeassistant/components/abode/cover.py index d504040ee90..e3fbb1a5b8f 100644 --- a/homeassistant/components/abode/cover.py +++ b/homeassistant/components/abode/cover.py @@ -1,8 +1,9 @@ """Support for Abode Security System covers.""" + from typing import Any -from jaraco.abode.devices.cover import Cover as AbodeCV -from jaraco.abode.helpers import constants as CONST +from jaraco.abode.devices.cover import Cover +from jaraco.abode.helpers.constants import TYPE_COVER from homeassistant.components.cover import CoverEntity from homeassistant.config_entries import ConfigEntry @@ -21,14 +22,14 @@ async def async_setup_entry( async_add_entities( AbodeCover(data, device) - for device in data.abode.get_devices(generic_type=CONST.TYPE_COVER) + for device in data.abode.get_devices(generic_type=TYPE_COVER) ) class AbodeCover(AbodeDevice, CoverEntity): """Representation of an Abode cover.""" - _device: AbodeCV + _device: Cover _attr_name = None @property diff --git a/homeassistant/components/abode/icons.json b/homeassistant/components/abode/icons.json index 89cee031818..00175628d9a 100644 --- a/homeassistant/components/abode/icons.json +++ b/homeassistant/components/abode/icons.json @@ -5,5 +5,10 @@ "default": "mdi:robot" } } + }, + "services": { + "capture_image": "mdi:camera", + "change_setting": "mdi:cog", + "trigger_automation": "mdi:play" } } diff --git a/homeassistant/components/abode/light.py b/homeassistant/components/abode/light.py index 539b89a5546..188d3c18e40 100644 --- a/homeassistant/components/abode/light.py +++ b/homeassistant/components/abode/light.py @@ -1,11 +1,12 @@ """Support for Abode Security System lights.""" + from __future__ import annotations from math import ceil from typing import Any -from jaraco.abode.devices.light import Light as AbodeLT -from jaraco.abode.helpers import constants as CONST +from jaraco.abode.devices.light import Light +from jaraco.abode.helpers.constants import TYPE_LIGHT from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -34,14 +35,14 @@ async def async_setup_entry( async_add_entities( AbodeLight(data, device) - for device in data.abode.get_devices(generic_type=CONST.TYPE_LIGHT) + for device in data.abode.get_devices(generic_type=TYPE_LIGHT) ) class AbodeLight(AbodeDevice, LightEntity): """Representation of an Abode light.""" - _device: AbodeLT + _device: Light _attr_name = None def turn_on(self, **kwargs: Any) -> None: diff --git a/homeassistant/components/abode/lock.py b/homeassistant/components/abode/lock.py index c110b3fd558..1135d3c3b36 100644 --- a/homeassistant/components/abode/lock.py +++ b/homeassistant/components/abode/lock.py @@ -1,8 +1,9 @@ """Support for the Abode Security System locks.""" + from typing import Any -from jaraco.abode.devices.lock import Lock as AbodeLK -from jaraco.abode.helpers import constants as CONST +from jaraco.abode.devices.lock import Lock +from jaraco.abode.helpers.constants import TYPE_LOCK from homeassistant.components.lock import LockEntity from homeassistant.config_entries import ConfigEntry @@ -21,14 +22,14 @@ async def async_setup_entry( async_add_entities( AbodeLock(data, device) - for device in data.abode.get_devices(generic_type=CONST.TYPE_LOCK) + for device in data.abode.get_devices(generic_type=TYPE_LOCK) ) class AbodeLock(AbodeDevice, LockEntity): """Representation of an Abode lock.""" - _device: AbodeLK + _device: Lock _attr_name = None def lock(self, **kwargs: Any) -> None: diff --git a/homeassistant/components/abode/sensor.py b/homeassistant/components/abode/sensor.py index 1b1dbe8b30a..89e5cf574fb 100644 --- a/homeassistant/components/abode/sensor.py +++ b/homeassistant/components/abode/sensor.py @@ -1,12 +1,21 @@ """Support for Abode Security System sensors.""" + from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass from typing import cast -from jaraco.abode.devices.sensor import Sensor as AbodeSense -from jaraco.abode.helpers import constants as CONST +from jaraco.abode.devices.sensor import Sensor +from jaraco.abode.helpers.constants import ( + HUMI_STATUS_KEY, + LUX_STATUS_KEY, + STATUSES_KEY, + TEMP_STATUS_KEY, + TYPE_SENSOR, + UNIT_CELSIUS, + UNIT_FAHRENHEIT, +) from homeassistant.components.sensor import ( SensorDeviceClass, @@ -22,27 +31,22 @@ from . import AbodeDevice, AbodeSystem from .const import DOMAIN ABODE_TEMPERATURE_UNIT_HA_UNIT = { - CONST.UNIT_FAHRENHEIT: UnitOfTemperature.FAHRENHEIT, - CONST.UNIT_CELSIUS: UnitOfTemperature.CELSIUS, + UNIT_FAHRENHEIT: UnitOfTemperature.FAHRENHEIT, + UNIT_CELSIUS: UnitOfTemperature.CELSIUS, } -@dataclass(frozen=True) -class AbodeSensorDescriptionMixin: - """Mixin for Abode sensor.""" - - value_fn: Callable[[AbodeSense], float] - native_unit_of_measurement_fn: Callable[[AbodeSense], str] - - -@dataclass(frozen=True) -class AbodeSensorDescription(SensorEntityDescription, AbodeSensorDescriptionMixin): +@dataclass(frozen=True, kw_only=True) +class AbodeSensorDescription(SensorEntityDescription): """Class describing Abode sensor entities.""" + value_fn: Callable[[Sensor], float] + native_unit_of_measurement_fn: Callable[[Sensor], str] + SENSOR_TYPES: tuple[AbodeSensorDescription, ...] = ( AbodeSensorDescription( - key=CONST.TEMP_STATUS_KEY, + key=TEMP_STATUS_KEY, device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement_fn=lambda device: ABODE_TEMPERATURE_UNIT_HA_UNIT[ device.temp_unit @@ -50,13 +54,13 @@ SENSOR_TYPES: tuple[AbodeSensorDescription, ...] = ( value_fn=lambda device: cast(float, device.temp), ), AbodeSensorDescription( - key=CONST.HUMI_STATUS_KEY, + key=HUMI_STATUS_KEY, device_class=SensorDeviceClass.HUMIDITY, native_unit_of_measurement_fn=lambda _: PERCENTAGE, value_fn=lambda device: cast(float, device.humidity), ), AbodeSensorDescription( - key=CONST.LUX_STATUS_KEY, + key=LUX_STATUS_KEY, device_class=SensorDeviceClass.ILLUMINANCE, native_unit_of_measurement_fn=lambda _: LIGHT_LUX, value_fn=lambda device: cast(float, device.lux), @@ -73,8 +77,8 @@ async def async_setup_entry( async_add_entities( AbodeSensor(data, device, description) for description in SENSOR_TYPES - for device in data.abode.get_devices(generic_type=CONST.TYPE_SENSOR) - if description.key in device.get_value(CONST.STATUSES_KEY) + for device in data.abode.get_devices(generic_type=TYPE_SENSOR) + if description.key in device.get_value(STATUSES_KEY) ) @@ -82,12 +86,12 @@ class AbodeSensor(AbodeDevice, SensorEntity): """A sensor implementation for Abode devices.""" entity_description: AbodeSensorDescription - _device: AbodeSense + _device: Sensor def __init__( self, data: AbodeSystem, - device: AbodeSense, + device: Sensor, description: AbodeSensorDescription, ) -> None: """Initialize a sensor for an Abode device.""" diff --git a/homeassistant/components/abode/switch.py b/homeassistant/components/abode/switch.py index 8443a16ef8f..9a33a04e341 100644 --- a/homeassistant/components/abode/switch.py +++ b/homeassistant/components/abode/switch.py @@ -1,10 +1,11 @@ """Support for Abode Security System switches.""" + from __future__ import annotations from typing import Any, cast -from jaraco.abode.devices.switch import Switch as AbodeSW -from jaraco.abode.helpers import constants as CONST +from jaraco.abode.devices.switch import Switch +from jaraco.abode.helpers.constants import TYPE_SWITCH, TYPE_VALVE from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry @@ -15,7 +16,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import AbodeAutomation, AbodeDevice, AbodeSystem from .const import DOMAIN -DEVICE_TYPES = [CONST.TYPE_SWITCH, CONST.TYPE_VALVE] +DEVICE_TYPES = [TYPE_SWITCH, TYPE_VALVE] async def async_setup_entry( @@ -41,7 +42,7 @@ async def async_setup_entry( class AbodeSwitch(AbodeDevice, SwitchEntity): """Representation of an Abode switch.""" - _device: AbodeSW + _device: Switch _attr_name = None def turn_on(self, **kwargs: Any) -> None: diff --git a/homeassistant/components/accuweather/__init__.py b/homeassistant/components/accuweather/__init__.py index dfbf5119981..26e0c1331be 100644 --- a/homeassistant/components/accuweather/__init__.py +++ b/homeassistant/components/accuweather/__init__.py @@ -1,4 +1,5 @@ """The AccuWeather component.""" + from __future__ import annotations from asyncio import timeout @@ -51,7 +52,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Remove ozone sensors from registry if they exist ent_reg = er.async_get(hass) - for day in range(0, 5): + for day in range(5): unique_id = f"{coordinator.location_key}-ozone-{day}" if entity_id := ent_reg.async_get_entity_id(SENSOR_PLATFORM, DOMAIN, unique_id): _LOGGER.debug("Removing ozone sensor entity %s", entity_id) @@ -134,4 +135,4 @@ class AccuWeatherDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): ) as error: raise UpdateFailed(error) from error _LOGGER.debug("Requests remaining: %d", self.accuweather.requests_remaining) - return {**current, **{ATTR_FORECAST: forecast}} + return {**current, ATTR_FORECAST: forecast} diff --git a/homeassistant/components/accuweather/config_flow.py b/homeassistant/components/accuweather/config_flow.py index b3fc7872c85..af7560d963a 100644 --- a/homeassistant/components/accuweather/config_flow.py +++ b/homeassistant/components/accuweather/config_flow.py @@ -1,4 +1,5 @@ """Adds config flow for AccuWeather.""" + from __future__ import annotations from asyncio import timeout @@ -9,11 +10,9 @@ from aiohttp import ClientError from aiohttp.client_exceptions import ClientConnectorError import voluptuous as vol -from homeassistant import config_entries -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.schema_config_entry_flow import ( @@ -33,20 +32,15 @@ OPTIONS_FLOW = { } -class AccuWeatherFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class AccuWeatherFlowHandler(ConfigFlow, domain=DOMAIN): """Config flow for AccuWeather.""" VERSION = 1 async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" - # Under the terms of use of the API, one user can use one free API key. Due to - # the small number of requests allowed, we only allow one integration instance. - if self._async_current_entries(): - return self.async_abort(reason="single_instance_allowed") - errors = {} if user_input is not None: diff --git a/homeassistant/components/accuweather/const.py b/homeassistant/components/accuweather/const.py index 2e18977d112..31925172d1c 100644 --- a/homeassistant/components/accuweather/const.py +++ b/homeassistant/components/accuweather/const.py @@ -1,4 +1,5 @@ """Constants for AccuWeather integration.""" + from __future__ import annotations from typing import Final diff --git a/homeassistant/components/accuweather/diagnostics.py b/homeassistant/components/accuweather/diagnostics.py index f307c6b5335..e7bc41eaaf2 100644 --- a/homeassistant/components/accuweather/diagnostics.py +++ b/homeassistant/components/accuweather/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for AccuWeather.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/accuweather/manifest.json b/homeassistant/components/accuweather/manifest.json index 2974c36607b..fa651d98efd 100644 --- a/homeassistant/components/accuweather/manifest.json +++ b/homeassistant/components/accuweather/manifest.json @@ -8,5 +8,6 @@ "iot_class": "cloud_polling", "loggers": ["accuweather"], "quality_scale": "platinum", - "requirements": ["accuweather==2.1.1"] + "requirements": ["accuweather==2.1.1"], + "single_config_entry": true } diff --git a/homeassistant/components/accuweather/sensor.py b/homeassistant/components/accuweather/sensor.py index 2219c5de4b6..521dfdfbead 100644 --- a/homeassistant/components/accuweather/sensor.py +++ b/homeassistant/components/accuweather/sensor.py @@ -1,4 +1,5 @@ """Support for the AccuWeather service.""" + from __future__ import annotations from collections.abc import Callable @@ -45,19 +46,11 @@ from .const import ( PARALLEL_UPDATES = 1 -@dataclass(frozen=True) -class AccuWeatherSensorDescriptionMixin: - """Mixin for AccuWeather sensor.""" - - value_fn: Callable[[dict[str, Any]], str | int | float | None] - - -@dataclass(frozen=True) -class AccuWeatherSensorDescription( - SensorEntityDescription, AccuWeatherSensorDescriptionMixin -): +@dataclass(frozen=True, kw_only=True) +class AccuWeatherSensorDescription(SensorEntityDescription): """Class describing AccuWeather sensor entities.""" + value_fn: Callable[[dict[str, Any]], str | int | float | None] attr_fn: Callable[[dict[str, Any]], dict[str, Any]] = lambda _: {} day: int | None = None diff --git a/homeassistant/components/accuweather/strings.json b/homeassistant/components/accuweather/strings.json index 24024ba722f..718f2da6a75 100644 --- a/homeassistant/components/accuweather/strings.json +++ b/homeassistant/components/accuweather/strings.json @@ -17,9 +17,6 @@ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]", "requests_exceeded": "The allowed number of requests to Accuweather API has been exceeded. You have to wait or change API Key." - }, - "abort": { - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" } }, "entity": { diff --git a/homeassistant/components/accuweather/system_health.py b/homeassistant/components/accuweather/system_health.py index df1e607d15d..607a557f333 100644 --- a/homeassistant/components/accuweather/system_health.py +++ b/homeassistant/components/accuweather/system_health.py @@ -1,4 +1,5 @@ """Provide info to system health.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/accuweather/weather.py b/homeassistant/components/accuweather/weather.py index d446b4b58d9..1f2e606f6ea 100644 --- a/homeassistant/components/accuweather/weather.py +++ b/homeassistant/components/accuweather/weather.py @@ -1,4 +1,5 @@ """Support for the AccuWeather service.""" + from __future__ import annotations from typing import cast @@ -145,9 +146,9 @@ class AccuWeatherEntity( """Return the UV index.""" return cast(float, self.coordinator.data["UVIndex"]) - @property - def forecast(self) -> list[Forecast] | None: - """Return the forecast array.""" + @callback + def _async_forecast_daily(self) -> list[Forecast] | None: + """Return the daily forecast in native units.""" if not self.coordinator.forecast: return None # remap keys from library to keys understood by the weather component @@ -176,8 +177,3 @@ class AccuWeatherEntity( } for item in self.coordinator.data[ATTR_FORECAST] ] - - @callback - def _async_forecast_daily(self) -> list[Forecast] | None: - """Return the daily forecast in native units.""" - return self.forecast diff --git a/homeassistant/components/acer_projector/const.py b/homeassistant/components/acer_projector/const.py index 98864ab957f..95e32dc97d4 100644 --- a/homeassistant/components/acer_projector/const.py +++ b/homeassistant/components/acer_projector/const.py @@ -1,4 +1,5 @@ """Use serial protocol of Acer projector to obtain state of the projector.""" + from __future__ import annotations from typing import Final diff --git a/homeassistant/components/acer_projector/switch.py b/homeassistant/components/acer_projector/switch.py index 75e3d6081ba..b29bbf9fa3f 100644 --- a/homeassistant/components/acer_projector/switch.py +++ b/homeassistant/components/acer_projector/switch.py @@ -1,4 +1,5 @@ """Use serial protocol of Acer projector to obtain state of the projector.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/acmeda/__init__.py b/homeassistant/components/acmeda/__init__.py index 0bb7cbdc177..b4a0f237522 100644 --- a/homeassistant/components/acmeda/__init__.py +++ b/homeassistant/components/acmeda/__init__.py @@ -1,4 +1,5 @@ """The Rollease Acmeda Automate integration.""" + from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/acmeda/base.py b/homeassistant/components/acmeda/base.py index 5d1f643418a..7596374684d 100644 --- a/homeassistant/components/acmeda/base.py +++ b/homeassistant/components/acmeda/base.py @@ -1,4 +1,5 @@ """Base class for Acmeda Roller Blinds.""" + from __future__ import annotations import aiopulse diff --git a/homeassistant/components/acmeda/config_flow.py b/homeassistant/components/acmeda/config_flow.py index 56a11aff200..5024507a7d3 100644 --- a/homeassistant/components/acmeda/config_flow.py +++ b/homeassistant/components/acmeda/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Rollease Acmeda Automate Pulse Hub.""" + from __future__ import annotations from asyncio import timeout @@ -8,14 +9,13 @@ from typing import Any import aiopulse import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_ID -from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN -class AcmedaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class AcmedaFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a Acmeda config flow.""" VERSION = 1 @@ -26,7 +26,7 @@ class AcmedaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" if ( user_input is not None @@ -40,12 +40,13 @@ class AcmedaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): entry.unique_id for entry in self._async_current_entries() } - hubs: list[aiopulse.Hub] = [] with suppress(TimeoutError): async with timeout(5): - async for hub in aiopulse.Hub.discover(): - if hub.id not in already_configured: - hubs.append(hub) + hubs: list[aiopulse.Hub] = [ + hub + async for hub in aiopulse.Hub.discover() + if hub.id not in already_configured + ] if not hubs: return self.async_abort(reason="no_devices_found") @@ -66,7 +67,7 @@ class AcmedaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ), ) - async def async_create(self, hub: aiopulse.Hub) -> FlowResult: + async def async_create(self, hub: aiopulse.Hub) -> ConfigFlowResult: """Create the Acmeda Hub entry.""" await self.async_set_unique_id(hub.id, raise_on_progress=False) return self.async_create_entry(title=hub.id, data={CONF_HOST: hub.host}) diff --git a/homeassistant/components/acmeda/const.py b/homeassistant/components/acmeda/const.py index b8712fee4ba..c65efcc02f6 100644 --- a/homeassistant/components/acmeda/const.py +++ b/homeassistant/components/acmeda/const.py @@ -1,4 +1,5 @@ """Constants for the Rollease Acmeda Automate integration.""" + import logging LOGGER = logging.getLogger(__package__) diff --git a/homeassistant/components/acmeda/cover.py b/homeassistant/components/acmeda/cover.py index 32b6cf31ee5..f8116221668 100644 --- a/homeassistant/components/acmeda/cover.py +++ b/homeassistant/components/acmeda/cover.py @@ -1,4 +1,5 @@ """Support for Acmeda Roller Blinds.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/acmeda/errors.py b/homeassistant/components/acmeda/errors.py index f26090df03d..2e54d86b353 100644 --- a/homeassistant/components/acmeda/errors.py +++ b/homeassistant/components/acmeda/errors.py @@ -1,4 +1,5 @@ """Errors for the Acmeda Pulse component.""" + from homeassistant.exceptions import HomeAssistantError diff --git a/homeassistant/components/acmeda/helpers.py b/homeassistant/components/acmeda/helpers.py index a87cbcd1635..9e48124208a 100644 --- a/homeassistant/components/acmeda/helpers.py +++ b/homeassistant/components/acmeda/helpers.py @@ -1,4 +1,5 @@ """Helper functions for Acmeda Pulse.""" + from __future__ import annotations from aiopulse import Roller diff --git a/homeassistant/components/acmeda/hub.py b/homeassistant/components/acmeda/hub.py index 9c6ef6156f0..a5daf27f445 100644 --- a/homeassistant/components/acmeda/hub.py +++ b/homeassistant/components/acmeda/hub.py @@ -1,4 +1,5 @@ """Code to handle a Pulse Hub.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/acmeda/sensor.py b/homeassistant/components/acmeda/sensor.py index 20d0929f341..0b458a8c32a 100644 --- a/homeassistant/components/acmeda/sensor.py +++ b/homeassistant/components/acmeda/sensor.py @@ -1,4 +1,5 @@ """Support for Acmeda Roller Blind Batteries.""" + from __future__ import annotations from homeassistant.components.sensor import SensorDeviceClass, SensorEntity diff --git a/homeassistant/components/actiontec/const.py b/homeassistant/components/actiontec/const.py index de309b68476..beff47f9811 100644 --- a/homeassistant/components/actiontec/const.py +++ b/homeassistant/components/actiontec/const.py @@ -1,4 +1,5 @@ """Support for Actiontec MI424WR (Verizon FIOS) routers.""" + from __future__ import annotations import re @@ -8,7 +9,7 @@ from typing import Final LEASES_REGEX: Final[re.Pattern[str]] = re.compile( r"(?P([0-9]{1,3}[\.]){3}[0-9]{1,3})" - + r"\smac:\s(?P([0-9a-f]{2}[:-]){5}([0-9a-f]{2}))" - + r"\svalid\sfor:\s(?P(-?\d+))" - + r"\ssec" + r"\smac:\s(?P([0-9a-f]{2}[:-]){5}([0-9a-f]{2}))" + r"\svalid\sfor:\s(?P(-?\d+))" + r"\ssec" ) diff --git a/homeassistant/components/actiontec/device_tracker.py b/homeassistant/components/actiontec/device_tracker.py index 40ff869c43b..2afa772421c 100644 --- a/homeassistant/components/actiontec/device_tracker.py +++ b/homeassistant/components/actiontec/device_tracker.py @@ -1,4 +1,5 @@ """Support for Actiontec MI424WR (Verizon FIOS) routers.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/actiontec/model.py b/homeassistant/components/actiontec/model.py index ff28d6d4ac6..ea313529131 100644 --- a/homeassistant/components/actiontec/model.py +++ b/homeassistant/components/actiontec/model.py @@ -1,4 +1,5 @@ """Model definitions for Actiontec MI424WR (Verizon FIOS) routers.""" + from dataclasses import dataclass diff --git a/homeassistant/components/adax/__init__.py b/homeassistant/components/adax/__init__.py index cf60d40631c..d4fe13ee4f6 100644 --- a/homeassistant/components/adax/__init__.py +++ b/homeassistant/components/adax/__init__.py @@ -1,4 +1,5 @@ """The Adax integration.""" + from __future__ import annotations from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/adax/climate.py b/homeassistant/components/adax/climate.py index 6b0adcb52cf..69b89cfe8cc 100644 --- a/homeassistant/components/adax/climate.py +++ b/homeassistant/components/adax/climate.py @@ -1,4 +1,5 @@ """Support for Adax wifi-enabled home heaters.""" + from __future__ import annotations from typing import Any, cast diff --git a/homeassistant/components/adax/config_flow.py b/homeassistant/components/adax/config_flow.py index b614c968d48..3e8ca646cad 100644 --- a/homeassistant/components/adax/config_flow.py +++ b/homeassistant/components/adax/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Adax integration.""" + from __future__ import annotations import logging @@ -8,14 +9,13 @@ import adax import adax_local import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import ( CONF_IP_ADDRESS, CONF_PASSWORD, CONF_TOKEN, CONF_UNIQUE_ID, ) -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import ( @@ -31,14 +31,14 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class AdaxConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Adax.""" VERSION = 2 async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" data_schema = vol.Schema( { @@ -63,7 +63,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_local( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the local step.""" data_schema = vol.Schema( {vol.Required(WIFI_SSID): str, vol.Required(WIFI_PSWD): str} @@ -110,7 +110,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_cloud( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the cloud step.""" data_schema = vol.Schema( {vol.Required(ACCOUNT_ID): int, vol.Required(CONF_PASSWORD): str} diff --git a/homeassistant/components/adax/const.py b/homeassistant/components/adax/const.py index 86c627aa130..306dd52e657 100644 --- a/homeassistant/components/adax/const.py +++ b/homeassistant/components/adax/const.py @@ -1,4 +1,5 @@ """Constants for the Adax integration.""" + from typing import Final ACCOUNT_ID: Final = "account_id" diff --git a/homeassistant/components/adguard/__init__.py b/homeassistant/components/adguard/__init__.py index fbcbea61316..b3cbb3300bf 100644 --- a/homeassistant/components/adguard/__init__.py +++ b/homeassistant/components/adguard/__init__.py @@ -1,4 +1,5 @@ """Support for AdGuard Home.""" + from __future__ import annotations from adguardhome import AdGuardHome, AdGuardHomeConnectionError diff --git a/homeassistant/components/adguard/config_flow.py b/homeassistant/components/adguard/config_flow.py index bc9b11c9a72..c07967ec2c5 100644 --- a/homeassistant/components/adguard/config_flow.py +++ b/homeassistant/components/adguard/config_flow.py @@ -1,4 +1,5 @@ """Config flow to configure the AdGuard Home integration.""" + from __future__ import annotations from typing import Any @@ -7,7 +8,7 @@ from adguardhome import AdGuardHome, AdGuardHomeConnectionError import voluptuous as vol from homeassistant.components.hassio import HassioServiceInfo -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, @@ -16,7 +17,6 @@ from homeassistant.const import ( CONF_USERNAME, CONF_VERIFY_SSL, ) -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN @@ -31,7 +31,7 @@ class AdGuardHomeFlowHandler(ConfigFlow, domain=DOMAIN): async def _show_setup_form( self, errors: dict[str, str] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Show the setup form to the user.""" return self.async_show_form( step_id="user", @@ -50,7 +50,7 @@ class AdGuardHomeFlowHandler(ConfigFlow, domain=DOMAIN): async def _show_hassio_form( self, errors: dict[str, str] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Show the Hass.io confirmation form to the user.""" assert self._hassio_discovery return self.async_show_form( @@ -61,7 +61,7 @@ class AdGuardHomeFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initiated by the user.""" if user_input is None: return await self._show_setup_form(user_input) @@ -104,7 +104,9 @@ class AdGuardHomeFlowHandler(ConfigFlow, domain=DOMAIN): }, ) - async def async_step_hassio(self, discovery_info: HassioServiceInfo) -> FlowResult: + async def async_step_hassio( + self, discovery_info: HassioServiceInfo + ) -> ConfigFlowResult: """Prepare configuration for a Hass.io AdGuard Home add-on. This flow is triggered by the discovery component. @@ -116,7 +118,7 @@ class AdGuardHomeFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_hassio_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm Supervisor discovery.""" if user_input is None: return await self._show_hassio_form() diff --git a/homeassistant/components/adguard/const.py b/homeassistant/components/adguard/const.py index a4ccde68539..7b6827c19d4 100644 --- a/homeassistant/components/adguard/const.py +++ b/homeassistant/components/adguard/const.py @@ -1,4 +1,5 @@ """Constants for the AdGuard Home integration.""" + import logging DOMAIN = "adguard" diff --git a/homeassistant/components/adguard/entity.py b/homeassistant/components/adguard/entity.py index 909acd89b80..8cb71a861e8 100644 --- a/homeassistant/components/adguard/entity.py +++ b/homeassistant/components/adguard/entity.py @@ -1,4 +1,5 @@ """AdGuard Home base entity.""" + from __future__ import annotations from adguardhome import AdGuardHome, AdGuardHomeError @@ -43,7 +44,7 @@ class AdGuardHomeEntity(Entity): async def _adguard_update(self) -> None: """Update AdGuard Home entity.""" - raise NotImplementedError() + raise NotImplementedError @property def device_info(self) -> DeviceInfo: diff --git a/homeassistant/components/adguard/sensor.py b/homeassistant/components/adguard/sensor.py index e1cec6c4d3b..1e95a07bffa 100644 --- a/homeassistant/components/adguard/sensor.py +++ b/homeassistant/components/adguard/sensor.py @@ -1,4 +1,5 @@ """Support for AdGuard Home sensors.""" + from __future__ import annotations from collections.abc import Callable, Coroutine diff --git a/homeassistant/components/adguard/switch.py b/homeassistant/components/adguard/switch.py index 0aa88aa3ffd..ae4bee85d23 100644 --- a/homeassistant/components/adguard/switch.py +++ b/homeassistant/components/adguard/switch.py @@ -1,4 +1,5 @@ """Support for AdGuard Home switches.""" + from __future__ import annotations from collections.abc import Callable, Coroutine diff --git a/homeassistant/components/ads/__init__.py b/homeassistant/components/ads/__init__.py index 84d9e29a518..7041a757a42 100644 --- a/homeassistant/components/ads/__init__.py +++ b/homeassistant/components/ads/__init__.py @@ -1,4 +1,5 @@ """Support for Automation Device Specification (ADS).""" + import asyncio from asyncio import timeout from collections import namedtuple diff --git a/homeassistant/components/ads/binary_sensor.py b/homeassistant/components/ads/binary_sensor.py index b20ef010f1f..2da76382c51 100644 --- a/homeassistant/components/ads/binary_sensor.py +++ b/homeassistant/components/ads/binary_sensor.py @@ -1,4 +1,5 @@ """Support for ADS binary sensors.""" + from __future__ import annotations import pyads diff --git a/homeassistant/components/ads/cover.py b/homeassistant/components/ads/cover.py index a2fb1888cd3..c54b3e14267 100644 --- a/homeassistant/components/ads/cover.py +++ b/homeassistant/components/ads/cover.py @@ -1,4 +1,5 @@ """Support for ADS covers.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/ads/icons.json b/homeassistant/components/ads/icons.json new file mode 100644 index 00000000000..5ab8041fe9b --- /dev/null +++ b/homeassistant/components/ads/icons.json @@ -0,0 +1,5 @@ +{ + "services": { + "write_data_by_name": "mdi:pencil" + } +} diff --git a/homeassistant/components/ads/light.py b/homeassistant/components/ads/light.py index 8dd55775b7a..13ce9ec261c 100644 --- a/homeassistant/components/ads/light.py +++ b/homeassistant/components/ads/light.py @@ -1,4 +1,5 @@ """Support for ADS light sources.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/ads/sensor.py b/homeassistant/components/ads/sensor.py index 17aede2bd2b..4bcc8f776df 100644 --- a/homeassistant/components/ads/sensor.py +++ b/homeassistant/components/ads/sensor.py @@ -1,4 +1,5 @@ """Support for ADS sensors.""" + from __future__ import annotations import voluptuous as vol diff --git a/homeassistant/components/ads/switch.py b/homeassistant/components/ads/switch.py index 3f597fb9f5c..a793a5996cf 100644 --- a/homeassistant/components/ads/switch.py +++ b/homeassistant/components/ads/switch.py @@ -1,4 +1,5 @@ """Support for ADS switch platform.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/advantage_air/__init__.py b/homeassistant/components/advantage_air/__init__.py index 0ef2c0eada5..c89d6f609b8 100644 --- a/homeassistant/components/advantage_air/__init__.py +++ b/homeassistant/components/advantage_air/__init__.py @@ -1,4 +1,5 @@ """Advantage Air climate integration.""" + from datetime import timedelta import logging diff --git a/homeassistant/components/advantage_air/binary_sensor.py b/homeassistant/components/advantage_air/binary_sensor.py index 74a276dc67b..cf813a429e5 100644 --- a/homeassistant/components/advantage_air/binary_sensor.py +++ b/homeassistant/components/advantage_air/binary_sensor.py @@ -1,4 +1,5 @@ """Binary Sensor platform for Advantage Air integration.""" + from __future__ import annotations from homeassistant.components.binary_sensor import ( diff --git a/homeassistant/components/advantage_air/climate.py b/homeassistant/components/advantage_air/climate.py index 6abd0b18fd4..49b8224a902 100644 --- a/homeassistant/components/advantage_air/climate.py +++ b/homeassistant/components/advantage_air/climate.py @@ -1,4 +1,5 @@ """Climate platform for Advantage Air integration.""" + from __future__ import annotations import logging @@ -57,7 +58,7 @@ HVAC_MODES = [ HVACMode.FAN_ONLY, HVACMode.DRY, ] -HVAC_MODES_MYAUTO = HVAC_MODES + [HVACMode.HEAT_COOL] +HVAC_MODES_MYAUTO = [*HVAC_MODES, HVACMode.HEAT_COOL] SUPPORTED_FEATURES = ( ClimateEntityFeature.FAN_MODE | ClimateEntityFeature.TURN_OFF diff --git a/homeassistant/components/advantage_air/config_flow.py b/homeassistant/components/advantage_air/config_flow.py index 7b5acab55f0..df3ee1c3638 100644 --- a/homeassistant/components/advantage_air/config_flow.py +++ b/homeassistant/components/advantage_air/config_flow.py @@ -1,4 +1,5 @@ """Config Flow for Advantage Air integration.""" + from __future__ import annotations from typing import Any @@ -6,9 +7,8 @@ from typing import Any from advantage_air import ApiError, advantage_air import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import ADVANTAGE_AIR_RETRY, DOMAIN @@ -23,7 +23,7 @@ ADVANTAGE_AIR_SCHEMA = vol.Schema( ) -class AdvantageAirConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class AdvantageAirConfigFlow(ConfigFlow, domain=DOMAIN): """Config Advantage Air API connection.""" VERSION = 1 @@ -32,7 +32,7 @@ class AdvantageAirConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Get configuration from the user.""" errors = {} if user_input: @@ -45,7 +45,7 @@ class AdvantageAirConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): port=port, session=async_get_clientsession(self.hass), retry=ADVANTAGE_AIR_RETRY, - ).async_get(1) + ).async_get() except ApiError: errors["base"] = "cannot_connect" else: diff --git a/homeassistant/components/advantage_air/const.py b/homeassistant/components/advantage_air/const.py index 80ce9b6eaa1..6ae0a0e06d5 100644 --- a/homeassistant/components/advantage_air/const.py +++ b/homeassistant/components/advantage_air/const.py @@ -1,4 +1,5 @@ """Constants used by Advantage Air integration.""" + DOMAIN = "advantage_air" ADVANTAGE_AIR_RETRY = 10 ADVANTAGE_AIR_STATE_OPEN = "open" diff --git a/homeassistant/components/advantage_air/cover.py b/homeassistant/components/advantage_air/cover.py index afb38dee931..3c6e3ffa3a6 100644 --- a/homeassistant/components/advantage_air/cover.py +++ b/homeassistant/components/advantage_air/cover.py @@ -1,4 +1,5 @@ """Cover platform for Advantage Air integration.""" + from typing import Any from homeassistant.components.cover import ( diff --git a/homeassistant/components/advantage_air/diagnostics.py b/homeassistant/components/advantage_air/diagnostics.py index 4c440610838..9eebb97d3c5 100644 --- a/homeassistant/components/advantage_air/diagnostics.py +++ b/homeassistant/components/advantage_air/diagnostics.py @@ -1,4 +1,5 @@ """Provides diagnostics for Advantage Air.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/advantage_air/entity.py b/homeassistant/components/advantage_air/entity.py index 9079e69ae09..be2135e4767 100644 --- a/homeassistant/components/advantage_air/entity.py +++ b/homeassistant/components/advantage_air/entity.py @@ -1,4 +1,5 @@ """Advantage Air parent entity class.""" + from typing import Any from advantage_air import ApiError diff --git a/homeassistant/components/advantage_air/icons.json b/homeassistant/components/advantage_air/icons.json new file mode 100644 index 00000000000..a4168f440cf --- /dev/null +++ b/homeassistant/components/advantage_air/icons.json @@ -0,0 +1,5 @@ +{ + "services": { + "set_time_to": "mdi:timer-cog" + } +} diff --git a/homeassistant/components/advantage_air/light.py b/homeassistant/components/advantage_air/light.py index 47c8c7c1768..30617c52acf 100644 --- a/homeassistant/components/advantage_air/light.py +++ b/homeassistant/components/advantage_air/light.py @@ -1,4 +1,5 @@ """Light platform for Advantage Air integration.""" + from typing import Any from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity diff --git a/homeassistant/components/advantage_air/models.py b/homeassistant/components/advantage_air/models.py index f56b3f8823b..77135644d11 100644 --- a/homeassistant/components/advantage_air/models.py +++ b/homeassistant/components/advantage_air/models.py @@ -1,4 +1,5 @@ """The Advantage Air integration models.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/homeassistant/components/advantage_air/select.py b/homeassistant/components/advantage_air/select.py index 013f2cc214d..c3739717ef1 100644 --- a/homeassistant/components/advantage_air/select.py +++ b/homeassistant/components/advantage_air/select.py @@ -21,11 +21,8 @@ async def async_setup_entry( instance: AdvantageAirData = hass.data[ADVANTAGE_AIR_DOMAIN][config_entry.entry_id] - entities: list[SelectEntity] = [] if aircons := instance.coordinator.data.get("aircons"): - for ac_key in aircons: - entities.append(AdvantageAirMyZone(instance, ac_key)) - async_add_entities(entities) + async_add_entities(AdvantageAirMyZone(instance, ac_key) for ac_key in aircons) class AdvantageAirMyZone(AdvantageAirAcEntity, SelectEntity): diff --git a/homeassistant/components/advantage_air/sensor.py b/homeassistant/components/advantage_air/sensor.py index 4af028e6db0..6bfa6bbad4b 100644 --- a/homeassistant/components/advantage_air/sensor.py +++ b/homeassistant/components/advantage_air/sensor.py @@ -1,4 +1,5 @@ """Sensor platform for Advantage Air integration.""" + from __future__ import annotations from decimal import Decimal diff --git a/homeassistant/components/advantage_air/switch.py b/homeassistant/components/advantage_air/switch.py index abc9b795d43..6d21f2e705c 100644 --- a/homeassistant/components/advantage_air/switch.py +++ b/homeassistant/components/advantage_air/switch.py @@ -1,4 +1,5 @@ """Switch platform for Advantage Air integration.""" + from typing import Any from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity @@ -33,9 +34,11 @@ async def async_setup_entry( if ADVANTAGE_AIR_AUTOFAN_ENABLED in ac_device["info"]: entities.append(AdvantageAirMyFan(instance, ac_key)) if things := instance.coordinator.data.get("myThings"): - for thing in things["things"].values(): - if thing["channelDipState"] == 8: # 8 = Other relay - entities.append(AdvantageAirRelay(instance, thing)) + entities.extend( + AdvantageAirRelay(instance, thing) + for thing in things["things"].values() + if thing["channelDipState"] == 8 # 8 = Other relay + ) async_add_entities(entities) diff --git a/homeassistant/components/aemet/config_flow.py b/homeassistant/components/aemet/config_flow.py index bb73311aa55..6b2eca3f5c9 100644 --- a/homeassistant/components/aemet/config_flow.py +++ b/homeassistant/components/aemet/config_flow.py @@ -1,4 +1,5 @@ """Config flow for AEMET OpenData.""" + from __future__ import annotations from typing import Any @@ -7,10 +8,9 @@ from aemet_opendata.exceptions import AuthError from aemet_opendata.interface import AEMET, ConnectionOptions import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.helpers.schema_config_entry_flow import ( SchemaFlowFormStep, @@ -29,12 +29,12 @@ OPTIONS_FLOW = { } -class AemetConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class AemetConfigFlow(ConfigFlow, domain=DOMAIN): """Config flow for AEMET OpenData.""" async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" errors = {} @@ -75,7 +75,7 @@ class AemetConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback def async_get_options_flow( - config_entry: config_entries.ConfigEntry, + config_entry: ConfigEntry, ) -> SchemaOptionsFlowHandler: """Get the options flow for this handler.""" return SchemaOptionsFlowHandler(config_entry, OPTIONS_FLOW) diff --git a/homeassistant/components/aemet/const.py b/homeassistant/components/aemet/const.py index 9623766b64c..337b7e0790c 100644 --- a/homeassistant/components/aemet/const.py +++ b/homeassistant/components/aemet/const.py @@ -1,4 +1,5 @@ """Constant values for the AEMET OpenData component.""" + from __future__ import annotations from aemet_opendata.const import ( @@ -120,8 +121,3 @@ FORECAST_MAP = { AOD_WIND_SPEED: ATTR_FORECAST_NATIVE_WIND_SPEED, }, } - -WEATHER_FORECAST_MODES = { - AOD_FORECAST_DAILY: "daily", - AOD_FORECAST_HOURLY: "hourly", -} diff --git a/homeassistant/components/aemet/coordinator.py b/homeassistant/components/aemet/coordinator.py index 04810077f28..8d179ccdb02 100644 --- a/homeassistant/components/aemet/coordinator.py +++ b/homeassistant/components/aemet/coordinator.py @@ -1,4 +1,5 @@ """Weather data coordinator for the AEMET OpenData service.""" + from __future__ import annotations from asyncio import timeout diff --git a/homeassistant/components/aemet/diagnostics.py b/homeassistant/components/aemet/diagnostics.py index f49170d9576..20b6c208514 100644 --- a/homeassistant/components/aemet/diagnostics.py +++ b/homeassistant/components/aemet/diagnostics.py @@ -1,4 +1,5 @@ """Support for the AEMET OpenData diagnostics.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/aemet/entity.py b/homeassistant/components/aemet/entity.py index b83c0c98807..ba3f7e56193 100644 --- a/homeassistant/components/aemet/entity.py +++ b/homeassistant/components/aemet/entity.py @@ -1,4 +1,5 @@ """Entity classes for the AEMET OpenData integration.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/aemet/sensor.py b/homeassistant/components/aemet/sensor.py index 75f7f5c0f97..0952af19d43 100644 --- a/homeassistant/components/aemet/sensor.py +++ b/homeassistant/components/aemet/sensor.py @@ -1,4 +1,5 @@ """Support for the AEMET OpenData service.""" + from __future__ import annotations from collections.abc import Callable @@ -367,20 +368,16 @@ async def async_setup_entry( name: str = domain_data[ENTRY_NAME] coordinator: WeatherUpdateCoordinator = domain_data[ENTRY_WEATHER_COORDINATOR] - entities: list[AemetSensor] = [] - - for description in FORECAST_SENSORS + WEATHER_SENSORS: - if dict_nested_value(coordinator.data["lib"], description.keys) is not None: - entities.append( - AemetSensor( - name, - coordinator, - description, - config_entry, - ) - ) - - async_add_entities(entities) + async_add_entities( + AemetSensor( + name, + coordinator, + description, + config_entry, + ) + for description in FORECAST_SENSORS + WEATHER_SENSORS + if dict_nested_value(coordinator.data["lib"], description.keys) is not None + ) class AemetSensor(AemetEntity, SensorEntity): diff --git a/homeassistant/components/aemet/weather.py b/homeassistant/components/aemet/weather.py index d49b62c9509..0d5abdcf967 100644 --- a/homeassistant/components/aemet/weather.py +++ b/homeassistant/components/aemet/weather.py @@ -14,7 +14,6 @@ from aemet_opendata.const import ( ) from homeassistant.components.weather import ( - DOMAIN as WEATHER_DOMAIN, Forecast, SingleCoordinatorWeatherEntity, WeatherEntityFeature, @@ -27,7 +26,6 @@ from homeassistant.const import ( UnitOfTemperature, ) from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( @@ -36,7 +34,6 @@ from .const import ( DOMAIN, ENTRY_NAME, ENTRY_WEATHER_COORDINATOR, - WEATHER_FORECAST_MODES, ) from .coordinator import WeatherUpdateCoordinator from .entity import AemetEntity @@ -51,31 +48,14 @@ async def async_setup_entry( domain_data = hass.data[DOMAIN][config_entry.entry_id] weather_coordinator = domain_data[ENTRY_WEATHER_COORDINATOR] - entities = [] - entity_registry = er.async_get(hass) - - # Add daily + hourly entity for legacy config entries, only add daily for new - # config entries. This can be removed in HA Core 2024.3 - if entity_registry.async_get_entity_id( - WEATHER_DOMAIN, - DOMAIN, - f"{config_entry.unique_id} {WEATHER_FORECAST_MODES[AOD_FORECAST_HOURLY]}", - ): - for mode, mode_id in WEATHER_FORECAST_MODES.items(): - name = f"{domain_data[ENTRY_NAME]} {mode_id}" - unique_id = f"{config_entry.unique_id} {mode_id}" - entities.append(AemetWeather(name, unique_id, weather_coordinator, mode)) - else: - entities.append( + async_add_entities( + [ AemetWeather( - domain_data[ENTRY_NAME], - config_entry.unique_id, - weather_coordinator, - AOD_FORECAST_DAILY, + domain_data[ENTRY_NAME], config_entry.unique_id, weather_coordinator ) - ) - - async_add_entities(entities, False) + ], + False, + ) class AemetWeather( @@ -98,14 +78,9 @@ class AemetWeather( name, unique_id, coordinator: WeatherUpdateCoordinator, - forecast_mode, ) -> None: """Initialize the sensor.""" super().__init__(coordinator) - self._forecast_mode = forecast_mode - self._attr_entity_registry_enabled_default = ( - self._forecast_mode == AOD_FORECAST_DAILY - ) self._attr_name = name self._attr_unique_id = unique_id @@ -115,11 +90,6 @@ class AemetWeather( cond = self.get_aemet_value([AOD_WEATHER, AOD_CONDITION]) return CONDITIONS_MAP.get(cond) - @property - def forecast(self) -> list[Forecast]: - """Return the forecast array.""" - return self.get_aemet_forecast(self._forecast_mode) - @callback def _async_forecast_daily(self) -> list[Forecast]: """Return the daily forecast in native units.""" diff --git a/homeassistant/components/aftership/__init__.py b/homeassistant/components/aftership/__init__.py index 66610e6e01b..b079079db08 100644 --- a/homeassistant/components/aftership/__init__.py +++ b/homeassistant/components/aftership/__init__.py @@ -1,4 +1,5 @@ """The AfterShip integration.""" + from __future__ import annotations from pyaftership import AfterShip, AfterShipException diff --git a/homeassistant/components/aftership/config_flow.py b/homeassistant/components/aftership/config_flow.py index 94578091501..99de28b2fc2 100644 --- a/homeassistant/components/aftership/config_flow.py +++ b/homeassistant/components/aftership/config_flow.py @@ -1,4 +1,5 @@ """Config flow for AfterShip integration.""" + from __future__ import annotations import logging @@ -7,12 +8,9 @@ from typing import Any from pyaftership import AfterShip, AfterShipException import voluptuous as vol -from homeassistant.config_entries import ConfigFlow -from homeassistant.const import CONF_API_KEY, CONF_NAME -from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN -from homeassistant.data_entry_flow import FlowResult +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult +from homeassistant.const import CONF_API_KEY from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from .const import DOMAIN @@ -26,7 +24,7 @@ class AfterShipConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors: dict[str, str] = {} if user_input is not None: @@ -48,26 +46,3 @@ class AfterShipConfigFlow(ConfigFlow, domain=DOMAIN): data_schema=vol.Schema({vol.Required(CONF_API_KEY): str}), errors=errors, ) - - async def async_step_import(self, config: dict[str, Any]) -> FlowResult: - """Import configuration from yaml.""" - async_create_issue( - self.hass, - HOMEASSISTANT_DOMAIN, - f"deprecated_yaml_{DOMAIN}", - breaks_in_ha_version="2024.4.0", - is_fixable=False, - issue_domain=DOMAIN, - severity=IssueSeverity.WARNING, - translation_key="deprecated_yaml", - translation_placeholders={ - "domain": DOMAIN, - "integration_title": "AfterShip", - }, - ) - - self._async_abort_entries_match({CONF_API_KEY: config[CONF_API_KEY]}) - return self.async_create_entry( - title=config.get(CONF_NAME, "AfterShip"), - data={CONF_API_KEY: config[CONF_API_KEY]}, - ) diff --git a/homeassistant/components/aftership/const.py b/homeassistant/components/aftership/const.py index dda5fb7e426..385570e145f 100644 --- a/homeassistant/components/aftership/const.py +++ b/homeassistant/components/aftership/const.py @@ -1,4 +1,5 @@ """Constants for the Aftership integration.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/aftership/sensor.py b/homeassistant/components/aftership/sensor.py index 055d31fc16d..c403c4a571d 100644 --- a/homeassistant/components/aftership/sensor.py +++ b/homeassistant/components/aftership/sensor.py @@ -1,28 +1,21 @@ """Support for non-delivered packages recorded in AfterShip.""" + from __future__ import annotations import logging from typing import Any, Final from pyaftership import AfterShip, AfterShipException -import voluptuous as vol -from homeassistant.components.sensor import ( - PLATFORM_SCHEMA as BASE_PLATFORM_SCHEMA, - SensorEntity, -) -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_API_KEY, CONF_NAME +from homeassistant.components.sensor import SensorEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, ServiceCall -from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import Throttle from .const import ( @@ -33,7 +26,6 @@ from .const import ( CONF_SLUG, CONF_TITLE, CONF_TRACKING_NUMBER, - DEFAULT_NAME, DOMAIN, MIN_TIME_BETWEEN_UPDATES, REMOVE_TRACKING_SERVICE_SCHEMA, @@ -44,47 +36,7 @@ from .const import ( _LOGGER: Final = logging.getLogger(__name__) -PLATFORM_SCHEMA: Final = BASE_PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_API_KEY): 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 AfterShip sensor platform.""" - aftership = AfterShip( - api_key=config[CONF_API_KEY], session=async_get_clientsession(hass) - ) - try: - await aftership.trackings.list() - except AfterShipException: - async_create_issue( - hass, - DOMAIN, - "deprecated_yaml_import_issue_cannot_connect", - breaks_in_ha_version="2024.4.0", - is_fixable=False, - issue_domain=DOMAIN, - severity=IssueSeverity.WARNING, - translation_key="deprecated_yaml_import_issue_cannot_connect", - translation_placeholders={ - "integration_title": "AfterShip", - "url": "/config/integrations/dashboard/add?domain=aftership", - }, - ) - - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=config - ) - ) +PLATFORM_SCHEMA: Final = cv.removed(DOMAIN, raise_if_present=False) async def async_setup_entry( diff --git a/homeassistant/components/agent_dvr/__init__.py b/homeassistant/components/agent_dvr/__init__.py index 6723d62e9e0..6dc83d3766d 100644 --- a/homeassistant/components/agent_dvr/__init__.py +++ b/homeassistant/components/agent_dvr/__init__.py @@ -1,4 +1,5 @@ """Support for Agent.""" + from agent import AgentError from agent.a import Agent diff --git a/homeassistant/components/agent_dvr/alarm_control_panel.py b/homeassistant/components/agent_dvr/alarm_control_panel.py index 9e5586b21f4..8dae49aa0ea 100644 --- a/homeassistant/components/agent_dvr/alarm_control_panel.py +++ b/homeassistant/components/agent_dvr/alarm_control_panel.py @@ -1,4 +1,5 @@ """Support for Agent DVR Alarm Control Panels.""" + from __future__ import annotations from homeassistant.components.alarm_control_panel import ( diff --git a/homeassistant/components/agent_dvr/camera.py b/homeassistant/components/agent_dvr/camera.py index cf171987fcb..e2012ee13ca 100644 --- a/homeassistant/components/agent_dvr/camera.py +++ b/homeassistant/components/agent_dvr/camera.py @@ -1,4 +1,5 @@ """Support for Agent camera streaming.""" + from datetime import timedelta import logging diff --git a/homeassistant/components/agent_dvr/config_flow.py b/homeassistant/components/agent_dvr/config_flow.py index 9143d40352f..ac2ff46d9ef 100644 --- a/homeassistant/components/agent_dvr/config_flow.py +++ b/homeassistant/components/agent_dvr/config_flow.py @@ -1,4 +1,5 @@ """Config flow to configure Agent devices.""" + from contextlib import suppress from typing import Any @@ -6,9 +7,8 @@ from agent import AgentConnectionError, AgentError from agent.a import Agent import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_PORT -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN, SERVER_URL @@ -17,12 +17,12 @@ from .helpers import generate_url DEFAULT_PORT = 8090 -class AgentFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class AgentFlowHandler(ConfigFlow, domain=DOMAIN): """Handle an Agent config flow.""" async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle an Agent config flow.""" errors = {} diff --git a/homeassistant/components/agent_dvr/const.py b/homeassistant/components/agent_dvr/const.py index e571edf9800..cd0284ca87c 100644 --- a/homeassistant/components/agent_dvr/const.py +++ b/homeassistant/components/agent_dvr/const.py @@ -1,4 +1,5 @@ """Constants for agent_dvr component.""" + DOMAIN = "agent_dvr" SERVERS = "servers" DEVICES = "devices" diff --git a/homeassistant/components/agent_dvr/icons.json b/homeassistant/components/agent_dvr/icons.json new file mode 100644 index 00000000000..6550d01641e --- /dev/null +++ b/homeassistant/components/agent_dvr/icons.json @@ -0,0 +1,9 @@ +{ + "services": { + "start_recording": "mdi:record-rec", + "stop_recording": "mdi:stop", + "enable_alerts": "mdi:bell-alert", + "disable_alerts": "mdi:bell-off", + "snapshot": "mdi:camera" + } +} diff --git a/homeassistant/components/air_quality/__init__.py b/homeassistant/components/air_quality/__init__.py index c2992cc804b..f23f87019b9 100644 --- a/homeassistant/components/air_quality/__init__.py +++ b/homeassistant/components/air_quality/__init__.py @@ -1,4 +1,5 @@ """Component for handling Air Quality data for your location.""" + from __future__ import annotations from datetime import timedelta @@ -16,6 +17,8 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.typing import ConfigType, StateType +from . import group as group_pre_import # noqa: F401 + _LOGGER: Final = logging.getLogger(__name__) ATTR_AQI: Final = "air_quality_index" @@ -80,7 +83,7 @@ class AirQualityEntity(Entity): @property def particulate_matter_2_5(self) -> StateType: """Return the particulate matter 2.5 level.""" - raise NotImplementedError() + raise NotImplementedError @property def particulate_matter_10(self) -> StateType: diff --git a/homeassistant/components/air_quality/group.py b/homeassistant/components/air_quality/group.py index 2ac081496cd..13a70cc4b6b 100644 --- a/homeassistant/components/air_quality/group.py +++ b/homeassistant/components/air_quality/group.py @@ -1,13 +1,16 @@ """Describe group states.""" +from typing import TYPE_CHECKING -from homeassistant.components.group import GroupIntegrationRegistry from homeassistant.core import HomeAssistant, callback +if TYPE_CHECKING: + from homeassistant.components.group import GroupIntegrationRegistry + @callback def async_describe_on_off_states( - hass: HomeAssistant, registry: GroupIntegrationRegistry + hass: HomeAssistant, registry: "GroupIntegrationRegistry" ) -> None: """Describe group on off states.""" registry.exclude_domain() diff --git a/homeassistant/components/airly/__init__.py b/homeassistant/components/airly/__init__.py index 91208de519b..651caee272c 100644 --- a/homeassistant/components/airly/__init__.py +++ b/homeassistant/components/airly/__init__.py @@ -1,4 +1,5 @@ """The Airly integration.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/airly/config_flow.py b/homeassistant/components/airly/config_flow.py index 27c7b0f91e3..2811156ac90 100644 --- a/homeassistant/components/airly/config_flow.py +++ b/homeassistant/components/airly/config_flow.py @@ -1,4 +1,5 @@ """Adds config flow for Airly.""" + from __future__ import annotations from asyncio import timeout @@ -10,23 +11,22 @@ from airly import Airly from airly.exceptions import AirlyError import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from .const import CONF_USE_NEAREST, DOMAIN, NO_AIRLY_SENSORS -class AirlyFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class AirlyFlowHandler(ConfigFlow, domain=DOMAIN): """Config flow for Airly.""" VERSION = 1 async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" errors = {} use_nearest = False diff --git a/homeassistant/components/airly/const.py b/homeassistant/components/airly/const.py index 76260699dbd..5939bfa62de 100644 --- a/homeassistant/components/airly/const.py +++ b/homeassistant/components/airly/const.py @@ -1,4 +1,5 @@ """Constants for Airly integration.""" + from __future__ import annotations from typing import Final diff --git a/homeassistant/components/airly/coordinator.py b/homeassistant/components/airly/coordinator.py index 9f2a1c96511..6db50950ba1 100644 --- a/homeassistant/components/airly/coordinator.py +++ b/homeassistant/components/airly/coordinator.py @@ -1,4 +1,5 @@ """DataUpdateCoordinator for the Airly integration.""" + from asyncio import timeout from datetime import timedelta import logging diff --git a/homeassistant/components/airly/diagnostics.py b/homeassistant/components/airly/diagnostics.py index bb270e6a664..1d63fbc8277 100644 --- a/homeassistant/components/airly/diagnostics.py +++ b/homeassistant/components/airly/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for Airly.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/airly/sensor.py b/homeassistant/components/airly/sensor.py index f91a242b8d5..3d80a0870d8 100644 --- a/homeassistant/components/airly/sensor.py +++ b/homeassistant/components/airly/sensor.py @@ -1,4 +1,5 @@ """Support for the Airly sensor service.""" + from __future__ import annotations from collections.abc import Callable @@ -180,13 +181,15 @@ async def async_setup_entry( coordinator = hass.data[DOMAIN][entry.entry_id] - sensors = [] - for description in SENSOR_TYPES: - # When we use the nearest method, we are not sure which sensors are available - if coordinator.data.get(description.key): - sensors.append(AirlySensor(coordinator, name, description)) - - async_add_entities(sensors, False) + async_add_entities( + ( + AirlySensor(coordinator, name, description) + for description in SENSOR_TYPES + # When we use the nearest method, we are not sure which sensors are available + if coordinator.data.get(description.key) + ), + False, + ) class AirlySensor(CoordinatorEntity[AirlyDataUpdateCoordinator], SensorEntity): diff --git a/homeassistant/components/airly/system_health.py b/homeassistant/components/airly/system_health.py index b1f6bc36c91..6e56b15ef92 100644 --- a/homeassistant/components/airly/system_health.py +++ b/homeassistant/components/airly/system_health.py @@ -1,4 +1,5 @@ """Provide info to system health.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/airnow/__init__.py b/homeassistant/components/airnow/__init__.py index a494ac0c93f..8fba13164e7 100644 --- a/homeassistant/components/airnow/__init__.py +++ b/homeassistant/components/airnow/__init__.py @@ -1,4 +1,5 @@ """The AirNow integration.""" + import datetime import logging diff --git a/homeassistant/components/airnow/config_flow.py b/homeassistant/components/airnow/config_flow.py index a6fa7aa5088..dd17e7f98db 100644 --- a/homeassistant/components/airnow/config_flow.py +++ b/homeassistant/components/airnow/config_flow.py @@ -1,4 +1,5 @@ """Config flow for AirNow integration.""" + import logging from typing import Any @@ -6,16 +7,15 @@ from pyairnow import WebServiceAPI from pyairnow.errors import AirNowError, EmptyResponseError, InvalidKeyError import voluptuous as vol -from homeassistant import core from homeassistant.config_entries import ( ConfigEntry, ConfigFlow, + ConfigFlowResult, OptionsFlow, OptionsFlowWithConfigEntry, ) from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS -from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResult +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -62,7 +62,7 @@ class AirNowConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors = {} if user_input is not None: @@ -117,7 +117,7 @@ class AirNowConfigFlow(ConfigFlow, domain=DOMAIN): ) @staticmethod - @core.callback + @callback def async_get_options_flow( config_entry: ConfigEntry, ) -> OptionsFlow: @@ -130,7 +130,7 @@ class AirNowOptionsFlowHandler(OptionsFlowWithConfigEntry): async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Manage the options.""" if user_input is not None: return self.async_create_entry(data=user_input) diff --git a/homeassistant/components/airnow/const.py b/homeassistant/components/airnow/const.py index 137c8f1efad..c61136b3eeb 100644 --- a/homeassistant/components/airnow/const.py +++ b/homeassistant/components/airnow/const.py @@ -1,4 +1,5 @@ """Constants for the AirNow integration.""" + ATTR_API_AQI = "AQI" ATTR_API_AQI_LEVEL = "Category.Number" ATTR_API_AQI_DESCRIPTION = "Category.Name" diff --git a/homeassistant/components/airnow/coordinator.py b/homeassistant/components/airnow/coordinator.py index 4bdaadff0da..32185080d25 100644 --- a/homeassistant/components/airnow/coordinator.py +++ b/homeassistant/components/airnow/coordinator.py @@ -1,4 +1,5 @@ """DataUpdateCoordinator for the AirNow integration.""" + from datetime import timedelta import logging from typing import Any diff --git a/homeassistant/components/airnow/diagnostics.py b/homeassistant/components/airnow/diagnostics.py index 284fd65013b..39db915bef9 100644 --- a/homeassistant/components/airnow/diagnostics.py +++ b/homeassistant/components/airnow/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for AirNow.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/airnow/sensor.py b/homeassistant/components/airnow/sensor.py index bfe9e92c4a3..1289b6c2b16 100644 --- a/homeassistant/components/airnow/sensor.py +++ b/homeassistant/components/airnow/sensor.py @@ -1,4 +1,5 @@ """Support for the AirNow sensor service.""" + from __future__ import annotations from collections.abc import Callable @@ -51,19 +52,14 @@ ATTR_LEVEL = "level" ATTR_STATION = "reporting_station" -@dataclass(frozen=True) -class AirNowEntityDescriptionMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class AirNowEntityDescription(SensorEntityDescription): + """Describes Airnow sensor entity.""" value_fn: Callable[[Any], StateType] extra_state_attributes_fn: Callable[[Any], dict[str, str]] | None -@dataclass(frozen=True) -class AirNowEntityDescription(SensorEntityDescription, AirNowEntityDescriptionMixin): - """Describes Airnow sensor entity.""" - - def station_extra_attrs(data: dict[str, Any]) -> dict[str, Any]: """Process extra attributes for station location (if available).""" if ATTR_API_STATION in data: diff --git a/homeassistant/components/airq/__init__.py b/homeassistant/components/airq/__init__.py index 06d7ba30749..dc35cd6ae87 100644 --- a/homeassistant/components/airq/__init__.py +++ b/homeassistant/components/airq/__init__.py @@ -1,4 +1,5 @@ """The air-Q integration.""" + from __future__ import annotations from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/airq/config_flow.py b/homeassistant/components/airq/config_flow.py index 33d76ec75bc..9e51552a309 100644 --- a/homeassistant/components/airq/config_flow.py +++ b/homeassistant/components/airq/config_flow.py @@ -1,4 +1,5 @@ """Config flow for air-Q integration.""" + from __future__ import annotations import logging @@ -8,9 +9,8 @@ from aioairq import AirQ, InvalidAuth from aiohttp.client_exceptions import ClientConnectionError import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN @@ -25,14 +25,14 @@ STEP_USER_DATA_SCHEMA = vol.Schema( ) -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class AirQConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for air-Q.""" VERSION = 1 async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial (authentication) configuration step.""" if user_input is None: return self.async_show_form( diff --git a/homeassistant/components/airq/const.py b/homeassistant/components/airq/const.py index d1a2340b4bc..845fa7f1de8 100644 --- a/homeassistant/components/airq/const.py +++ b/homeassistant/components/airq/const.py @@ -1,4 +1,5 @@ """Constants for the air-Q integration.""" + from typing import Final DOMAIN: Final = "airq" diff --git a/homeassistant/components/airq/coordinator.py b/homeassistant/components/airq/coordinator.py index 6f49303bc6c..b03ce36d776 100644 --- a/homeassistant/components/airq/coordinator.py +++ b/homeassistant/components/airq/coordinator.py @@ -1,4 +1,5 @@ """The air-Q integration.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/airq/sensor.py b/homeassistant/components/airq/sensor.py index ad05202943f..e3ef6504731 100644 --- a/homeassistant/components/airq/sensor.py +++ b/homeassistant/components/airq/sensor.py @@ -1,4 +1,5 @@ """Definition of air-Q sensor platform.""" + from __future__ import annotations from collections.abc import Callable @@ -37,18 +38,13 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -@dataclass(frozen=True) -class AirQEntityDescriptionMixin: - """Class for keys required by AirQ entity.""" +@dataclass(frozen=True, kw_only=True) +class AirQEntityDescription(SensorEntityDescription): + """Describes AirQ sensor entity.""" value: Callable[[dict], float | int | None] -@dataclass(frozen=True) -class AirQEntityDescription(SensorEntityDescription, AirQEntityDescriptionMixin): - """Describes AirQ sensor entity.""" - - # Keys must match those in the data dictionary SENSOR_TYPES: list[AirQEntityDescription] = [ AirQEntityDescription( diff --git a/homeassistant/components/airthings/__init__.py b/homeassistant/components/airthings/__init__.py index a5b962d1bf7..bc12f19a33d 100644 --- a/homeassistant/components/airthings/__init__.py +++ b/homeassistant/components/airthings/__init__.py @@ -1,4 +1,5 @@ """The Airthings integration.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/airthings/config_flow.py b/homeassistant/components/airthings/config_flow.py index 62f66213a0f..eae7d35c62b 100644 --- a/homeassistant/components/airthings/config_flow.py +++ b/homeassistant/components/airthings/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Airthings integration.""" + from __future__ import annotations import logging @@ -7,9 +8,8 @@ from typing import Any import airthings import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_ID -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import CONF_SECRET, DOMAIN @@ -24,14 +24,14 @@ STEP_USER_DATA_SCHEMA = vol.Schema( ) -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class AirthingsConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Airthings.""" VERSION = 1 async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" if user_input is None: return self.async_show_form( diff --git a/homeassistant/components/airthings/sensor.py b/homeassistant/components/airthings/sensor.py index 9d772d11996..fc91d816aca 100644 --- a/homeassistant/components/airthings/sensor.py +++ b/homeassistant/components/airthings/sensor.py @@ -1,4 +1,5 @@ """Support for Airthings sensors.""" + from __future__ import annotations from airthings import AirthingsDevice @@ -47,7 +48,7 @@ SENSORS: dict[str, SensorEntityDescription] = { ), "pressure": SensorEntityDescription( key="pressure", - device_class=SensorDeviceClass.PRESSURE, + device_class=SensorDeviceClass.ATMOSPHERIC_PRESSURE, native_unit_of_measurement=UnitOfPressure.MBAR, ), "battery": SensorEntityDescription( diff --git a/homeassistant/components/airthings_ble/__init__.py b/homeassistant/components/airthings_ble/__init__.py index 8258f7baf3d..e8a2d492ae2 100644 --- a/homeassistant/components/airthings_ble/__init__.py +++ b/homeassistant/components/airthings_ble/__init__.py @@ -1,4 +1,5 @@ """The Airthings BLE integration.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/airthings_ble/config_flow.py b/homeassistant/components/airthings_ble/config_flow.py index 4228fea50d7..5f08f198761 100644 --- a/homeassistant/components/airthings_ble/config_flow.py +++ b/homeassistant/components/airthings_ble/config_flow.py @@ -15,9 +15,8 @@ from homeassistant.components.bluetooth import ( BluetoothServiceInfo, async_discovered_service_info, ) -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_ADDRESS -from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN, MFCT_ID @@ -93,7 +92,7 @@ class AirthingsConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_bluetooth( self, discovery_info: BluetoothServiceInfo - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the bluetooth discovery step.""" _LOGGER.debug("Discovered BT device: %s", discovery_info) await self.async_set_unique_id(discovery_info.address) @@ -114,7 +113,7 @@ class AirthingsConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_bluetooth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm discovery.""" if user_input is not None: return self.async_create_entry( @@ -129,7 +128,7 @@ class AirthingsConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the user step to pick discovered device.""" if user_input is not None: address = user_input[CONF_ADDRESS] diff --git a/homeassistant/components/airthings_ble/sensor.py b/homeassistant/components/airthings_ble/sensor.py index 6abe3e5d174..8031b802eae 100644 --- a/homeassistant/components/airthings_ble/sensor.py +++ b/homeassistant/components/airthings_ble/sensor.py @@ -1,4 +1,5 @@ """Support for airthings ble sensors.""" + from __future__ import annotations import dataclasses @@ -50,12 +51,14 @@ SENSORS_MAPPING_TEMPLATE: dict[str, SensorEntityDescription] = { key="radon_1day_avg", translation_key="radon_1day_avg", native_unit_of_measurement=VOLUME_BECQUEREL, + suggested_display_precision=0, state_class=SensorStateClass.MEASUREMENT, ), "radon_longterm_avg": SensorEntityDescription( key="radon_longterm_avg", translation_key="radon_longterm_avg", native_unit_of_measurement=VOLUME_BECQUEREL, + suggested_display_precision=0, state_class=SensorStateClass.MEASUREMENT, ), "radon_1day_level": SensorEntityDescription( @@ -80,7 +83,7 @@ SENSORS_MAPPING_TEMPLATE: dict[str, SensorEntityDescription] = { ), "pressure": SensorEntityDescription( key="pressure", - device_class=SensorDeviceClass.PRESSURE, + device_class=SensorDeviceClass.ATMOSPHERIC_PRESSURE, native_unit_of_measurement=UnitOfPressure.MBAR, state_class=SensorStateClass.MEASUREMENT, ), @@ -168,6 +171,7 @@ async def async_setup_entry( sensors_mapping[key] = dataclasses.replace( val, native_unit_of_measurement=VOLUME_PICOCURIE, + suggested_display_precision=1, ) entities = [] diff --git a/homeassistant/components/airtouch4/__init__.py b/homeassistant/components/airtouch4/__init__.py index dc5172096a7..5f63fe023dc 100644 --- a/homeassistant/components/airtouch4/__init__.py +++ b/homeassistant/components/airtouch4/__init__.py @@ -1,4 +1,5 @@ """The AirTouch4 integration.""" + from airtouch4pyapi import AirTouch from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/airtouch4/climate.py b/homeassistant/components/airtouch4/climate.py index 89afddad76e..3fdace0f553 100644 --- a/homeassistant/components/airtouch4/climate.py +++ b/homeassistant/components/airtouch4/climate.py @@ -1,4 +1,5 @@ """AirTouch 4 component to control of AirTouch 4 Climate Devices.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/airtouch4/config_flow.py b/homeassistant/components/airtouch4/config_flow.py index e395c71349b..12e01ffde29 100644 --- a/homeassistant/components/airtouch4/config_flow.py +++ b/homeassistant/components/airtouch4/config_flow.py @@ -1,8 +1,9 @@ """Config flow for AirTouch4.""" + from airtouch4pyapi import AirTouch, AirTouchStatus import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_HOST from .const import DOMAIN @@ -10,7 +11,7 @@ from .const import DOMAIN DATA_SCHEMA = vol.Schema({vol.Required(CONF_HOST): str}) -class AirtouchConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class AirtouchConfigFlow(ConfigFlow, domain=DOMAIN): """Handle an Airtouch config flow.""" VERSION = 1 diff --git a/homeassistant/components/airtouch4/coordinator.py b/homeassistant/components/airtouch4/coordinator.py index e78bf62dbd0..5a080566416 100644 --- a/homeassistant/components/airtouch4/coordinator.py +++ b/homeassistant/components/airtouch4/coordinator.py @@ -1,4 +1,5 @@ """DataUpdateCoordinator for the airtouch integration.""" + import logging from airtouch4pyapi.airtouch import AirTouchStatus diff --git a/homeassistant/components/airtouch5/__init__.py b/homeassistant/components/airtouch5/__init__.py index 6ec32eaa021..b8b9a3f765a 100644 --- a/homeassistant/components/airtouch5/__init__.py +++ b/homeassistant/components/airtouch5/__init__.py @@ -1,4 +1,5 @@ """The Airtouch 5 integration.""" + from __future__ import annotations from airtouch5py.airtouch5_simple_client import Airtouch5SimpleClient @@ -26,7 +27,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: await client.connect_and_stay_connected() except TimeoutError as t: - raise ConfigEntryNotReady() from t + raise ConfigEntryNotReady from t # Store an API object for your platforms to access hass.data[DOMAIN][entry.entry_id] = client diff --git a/homeassistant/components/airtouch5/climate.py b/homeassistant/components/airtouch5/climate.py index ee92f68c0ed..157e3b7d643 100644 --- a/homeassistant/components/airtouch5/climate.py +++ b/homeassistant/components/airtouch5/climate.py @@ -1,4 +1,5 @@ """AirTouch 5 component to control AirTouch 5 Climate Devices.""" + import logging from typing import Any @@ -108,8 +109,10 @@ async def async_setup_entry( entities.append(Airtouch5AC(client, ac)) # Add each zone - for zone in client.zones: - entities.append(Airtouch5Zone(client, zone, zone_to_ac[zone.zone_number])) + entities.extend( + Airtouch5Zone(client, zone, zone_to_ac[zone.zone_number]) + for zone in client.zones + ) async_add_entities(entities) diff --git a/homeassistant/components/airtouch5/config_flow.py b/homeassistant/components/airtouch5/config_flow.py index e5df2844653..65755350b47 100644 --- a/homeassistant/components/airtouch5/config_flow.py +++ b/homeassistant/components/airtouch5/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Airtouch 5 integration.""" + from __future__ import annotations import logging @@ -9,7 +10,6 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_HOST -from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN @@ -25,7 +25,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> config_entries.ConfigFlowResult: """Handle the initial step.""" errors: dict[str, str] | None = None if user_input is not None: diff --git a/homeassistant/components/airtouch5/entity.py b/homeassistant/components/airtouch5/entity.py index a6ac76b5187..e5899850e0f 100644 --- a/homeassistant/components/airtouch5/entity.py +++ b/homeassistant/components/airtouch5/entity.py @@ -1,4 +1,5 @@ """Base class for Airtouch5 entities.""" + from airtouch5py.airtouch5_client import Airtouch5ConnectionStateChange from airtouch5py.airtouch5_simple_client import Airtouch5SimpleClient diff --git a/homeassistant/components/airvisual/__init__.py b/homeassistant/components/airvisual/__init__.py index 42cc1e1fade..c0a6b8d38ef 100644 --- a/homeassistant/components/airvisual/__init__.py +++ b/homeassistant/components/airvisual/__init__.py @@ -1,4 +1,5 @@ """The AirVisual component.""" + from __future__ import annotations import asyncio @@ -161,13 +162,13 @@ def _standardize_geography_config_entry( # about, infer it from the data we have: entry_updates["data"] = {**entry.data} if CONF_CITY in entry.data: - entry_updates["data"][ - CONF_INTEGRATION_TYPE - ] = INTEGRATION_TYPE_GEOGRAPHY_NAME + entry_updates["data"][CONF_INTEGRATION_TYPE] = ( + INTEGRATION_TYPE_GEOGRAPHY_NAME + ) else: - entry_updates["data"][ - CONF_INTEGRATION_TYPE - ] = INTEGRATION_TYPE_GEOGRAPHY_COORDS + entry_updates["data"][CONF_INTEGRATION_TYPE] = ( + INTEGRATION_TYPE_GEOGRAPHY_COORDS + ) if not entry_updates: return diff --git a/homeassistant/components/airvisual/config_flow.py b/homeassistant/components/airvisual/config_flow.py index 23a26e2cca6..2d7a0d8886e 100644 --- a/homeassistant/components/airvisual/config_flow.py +++ b/homeassistant/components/airvisual/config_flow.py @@ -1,4 +1,5 @@ """Define a config flow manager for AirVisual.""" + from __future__ import annotations import asyncio @@ -15,8 +16,7 @@ from pyairvisual.cloud_api import ( from pyairvisual.errors import AirVisualError import voluptuous as vol -from homeassistant import config_entries -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.const import ( CONF_API_KEY, CONF_COUNTRY, @@ -26,7 +26,6 @@ from homeassistant.const import ( CONF_STATE, ) from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.helpers.schema_config_entry_flow import ( SchemaFlowFormStep, @@ -70,7 +69,7 @@ OPTIONS_FLOW = { } -class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class AirVisualFlowHandler(ConfigFlow, domain=DOMAIN): """Handle an AirVisual config flow.""" VERSION = 3 @@ -96,7 +95,7 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def _async_finish_geography( self, user_input: dict[str, str], integration_type: str - ) -> FlowResult: + ) -> ConfigFlowResult: """Validate a Cloud API key.""" errors = {} websession = aiohttp_client.async_get_clientsession(self.hass) @@ -155,7 +154,7 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def _async_init_geography( self, user_input: dict[str, str], integration_type: str - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initialization of the integration via the cloud API.""" self._geo_id = async_get_geography_id(user_input) await self._async_set_unique_id(self._geo_id) @@ -173,7 +172,7 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Define the config flow to handle options.""" return SchemaOptionsFlowHandler(config_entry, OPTIONS_FLOW) - async def async_step_import(self, import_data: dict[str, str]) -> FlowResult: + async def async_step_import(self, import_data: dict[str, str]) -> ConfigFlowResult: """Handle import of config entry version 1 data.""" import_source = import_data.pop("import_source") if import_source == "geography_by_coords": @@ -182,7 +181,7 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_geography_by_coords( self, user_input: dict[str, str] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initialization of the cloud API based on latitude/longitude.""" if not user_input: return self.async_show_form( @@ -195,7 +194,7 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_geography_by_name( self, user_input: dict[str, str] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initialization of the cloud API based on city/state/country.""" if not user_input: return self.async_show_form( @@ -206,7 +205,9 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): user_input, INTEGRATION_TYPE_GEOGRAPHY_NAME ) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Handle configuration by re-auth.""" self._entry_data_for_reauth = entry_data self._geo_id = async_get_geography_id(entry_data) @@ -214,7 +215,7 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_reauth_confirm( self, user_input: dict[str, str] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle re-auth completion.""" if not user_input: return self.async_show_form( @@ -229,7 +230,7 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, str] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the start of the config flow.""" if not user_input: return self.async_show_form( diff --git a/homeassistant/components/airvisual/const.py b/homeassistant/components/airvisual/const.py index 0afa7d32d41..c81ea8d8d00 100644 --- a/homeassistant/components/airvisual/const.py +++ b/homeassistant/components/airvisual/const.py @@ -1,4 +1,5 @@ """Define AirVisual constants.""" + import logging DOMAIN = "airvisual" diff --git a/homeassistant/components/airvisual/diagnostics.py b/homeassistant/components/airvisual/diagnostics.py index 05e716367bb..348bb249b0f 100644 --- a/homeassistant/components/airvisual/diagnostics.py +++ b/homeassistant/components/airvisual/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for AirVisual.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/airvisual/manifest.json b/homeassistant/components/airvisual/manifest.json index 4da5c395765..7934d809287 100644 --- a/homeassistant/components/airvisual/manifest.json +++ b/homeassistant/components/airvisual/manifest.json @@ -5,7 +5,6 @@ "config_flow": true, "dependencies": ["airvisual_pro"], "documentation": "https://www.home-assistant.io/integrations/airvisual", - "import_executor": true, "integration_type": "service", "iot_class": "cloud_polling", "loggers": ["pyairvisual", "pysmb"], diff --git a/homeassistant/components/airvisual/sensor.py b/homeassistant/components/airvisual/sensor.py index 69835188750..df0e3da1f45 100644 --- a/homeassistant/components/airvisual/sensor.py +++ b/homeassistant/components/airvisual/sensor.py @@ -1,4 +1,5 @@ """Support for AirVisual air quality sensors.""" + from __future__ import annotations from homeassistant.components.sensor import ( diff --git a/homeassistant/components/airvisual_pro/__init__.py b/homeassistant/components/airvisual_pro/__init__.py index 3e53fc15b4f..88f05d28145 100644 --- a/homeassistant/components/airvisual_pro/__init__.py +++ b/homeassistant/components/airvisual_pro/__init__.py @@ -1,4 +1,5 @@ """The AirVisual Pro integration.""" + from __future__ import annotations import asyncio @@ -53,7 +54,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: await node.async_connect() except NodeProError as err: - raise ConfigEntryNotReady() from err + raise ConfigEntryNotReady from err reload_task: asyncio.Task | None = None diff --git a/homeassistant/components/airvisual_pro/config_flow.py b/homeassistant/components/airvisual_pro/config_flow.py index 23da39150c5..97265b33913 100644 --- a/homeassistant/components/airvisual_pro/config_flow.py +++ b/homeassistant/components/airvisual_pro/config_flow.py @@ -1,4 +1,5 @@ """Define a config flow manager for AirVisual Pro.""" + from __future__ import annotations from collections.abc import Mapping @@ -13,10 +14,8 @@ from pyairvisual.node import ( ) import voluptuous as vol -from homeassistant import config_entries -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD -from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN, LOGGER @@ -72,7 +71,7 @@ async def async_validate_credentials( return ValidationResult(errors=errors) -class AirVisualProFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class AirVisualProFlowHandler(ConfigFlow, domain=DOMAIN): """Handle an AirVisual Pro config flow.""" VERSION = 1 @@ -81,11 +80,15 @@ class AirVisualProFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Initialize.""" self._reauth_entry: ConfigEntry | None = None - async def async_step_import(self, import_config: dict[str, Any]) -> FlowResult: + async def async_step_import( + self, import_config: dict[str, Any] + ) -> ConfigFlowResult: """Import a config entry from configuration.yaml.""" return await self.async_step_user(import_config) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Handle configuration by re-auth.""" self._reauth_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] @@ -94,7 +97,7 @@ class AirVisualProFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the re-auth step.""" if user_input is None: return self.async_show_form( @@ -124,7 +127,7 @@ class AirVisualProFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, str] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" if not user_input: return self.async_show_form(step_id="user", data_schema=STEP_USER_SCHEMA) diff --git a/homeassistant/components/airvisual_pro/const.py b/homeassistant/components/airvisual_pro/const.py index 83a6cc5739c..ac47eff1cf0 100644 --- a/homeassistant/components/airvisual_pro/const.py +++ b/homeassistant/components/airvisual_pro/const.py @@ -1,4 +1,5 @@ """Constants for the AirVisual Pro integration.""" + import logging DOMAIN = "airvisual_pro" diff --git a/homeassistant/components/airvisual_pro/diagnostics.py b/homeassistant/components/airvisual_pro/diagnostics.py index d6e60207214..9fea6e59c1d 100644 --- a/homeassistant/components/airvisual_pro/diagnostics.py +++ b/homeassistant/components/airvisual_pro/diagnostics.py @@ -1,4 +1,5 @@ """Support for AirVisual Pro diagnostics.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/airvisual_pro/sensor.py b/homeassistant/components/airvisual_pro/sensor.py index 2708cc5857d..d53def57959 100644 --- a/homeassistant/components/airvisual_pro/sensor.py +++ b/homeassistant/components/airvisual_pro/sensor.py @@ -1,4 +1,5 @@ """Support for AirVisual Pro sensors.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/airzone/__init__.py b/homeassistant/components/airzone/__init__.py index 1a54be0ac41..1a65b92c3f4 100644 --- a/homeassistant/components/airzone/__init__.py +++ b/homeassistant/components/airzone/__init__.py @@ -1,4 +1,5 @@ """The Airzone integration.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/airzone/binary_sensor.py b/homeassistant/components/airzone/binary_sensor.py index 488c2c96132..e25751f2a47 100644 --- a/homeassistant/components/airzone/binary_sensor.py +++ b/homeassistant/components/airzone/binary_sensor.py @@ -1,4 +1,5 @@ """Support for the Airzone sensors.""" + from __future__ import annotations from dataclasses import dataclass @@ -79,33 +80,31 @@ async def async_setup_entry( """Add Airzone binary sensors from a config_entry.""" coordinator = hass.data[DOMAIN][entry.entry_id] - binary_sensors: list[AirzoneBinarySensor] = [] + binary_sensors: list[AirzoneBinarySensor] = [ + AirzoneSystemBinarySensor( + coordinator, + description, + entry, + system_id, + system_data, + ) + for system_id, system_data in coordinator.data[AZD_SYSTEMS].items() + for description in SYSTEM_BINARY_SENSOR_TYPES + if description.key in system_data + ] - for system_id, system_data in coordinator.data[AZD_SYSTEMS].items(): - for description in SYSTEM_BINARY_SENSOR_TYPES: - if description.key in system_data: - binary_sensors.append( - AirzoneSystemBinarySensor( - coordinator, - description, - entry, - system_id, - system_data, - ) - ) - - for system_zone_id, zone_data in coordinator.data[AZD_ZONES].items(): - for description in ZONE_BINARY_SENSOR_TYPES: - if description.key in zone_data: - binary_sensors.append( - AirzoneZoneBinarySensor( - coordinator, - description, - entry, - system_zone_id, - zone_data, - ) - ) + binary_sensors.extend( + AirzoneZoneBinarySensor( + coordinator, + description, + entry, + system_zone_id, + zone_data, + ) + for system_zone_id, zone_data in coordinator.data[AZD_ZONES].items() + for description in ZONE_BINARY_SENSOR_TYPES + if description.key in zone_data + ) async_add_entities(binary_sensors) diff --git a/homeassistant/components/airzone/climate.py b/homeassistant/components/airzone/climate.py index 2b4cae18086..f5b42c4ccbd 100644 --- a/homeassistant/components/airzone/climate.py +++ b/homeassistant/components/airzone/climate.py @@ -1,4 +1,5 @@ """Support for the Airzone climate.""" + from __future__ import annotations from typing import Any, Final diff --git a/homeassistant/components/airzone/config_flow.py b/homeassistant/components/airzone/config_flow.py index 7a8fdbf884b..24ee37bbcb4 100644 --- a/homeassistant/components/airzone/config_flow.py +++ b/homeassistant/components/airzone/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Airzone.""" + from __future__ import annotations import logging @@ -9,10 +10,10 @@ from aioairzone.exceptions import AirzoneError, InvalidSystem from aioairzone.localapi import AirzoneLocalApi, ConnectionOptions import voluptuous as vol -from homeassistant import config_entries from homeassistant.components import dhcp +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_ID, CONF_PORT -from homeassistant.data_entry_flow import AbortFlow, FlowResult +from homeassistant.data_entry_flow import AbortFlow from homeassistant.helpers import aiohttp_client from homeassistant.helpers.device_registry import format_mac @@ -38,7 +39,7 @@ def short_mac(addr: str) -> str: return addr.replace(":", "")[-4:].upper() -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class AirZoneConfigFlow(ConfigFlow, domain=DOMAIN): """Handle config flow for an Airzone device.""" _discovered_ip: str | None = None @@ -46,7 +47,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" data_schema = CONFIG_SCHEMA errors = {} @@ -91,7 +92,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: + async def async_step_dhcp( + self, discovery_info: dhcp.DhcpServiceInfo + ) -> ConfigFlowResult: """Handle DHCP discovery.""" self._discovered_ip = discovery_info.ip self._discovered_mac = discovery_info.macaddress @@ -118,7 +121,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_discovered_connection( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm discovery.""" assert self._discovered_ip is not None assert self._discovered_mac is not None diff --git a/homeassistant/components/airzone/coordinator.py b/homeassistant/components/airzone/coordinator.py index 6053c587550..8ec2cbe07ca 100644 --- a/homeassistant/components/airzone/coordinator.py +++ b/homeassistant/components/airzone/coordinator.py @@ -1,4 +1,5 @@ """The Airzone integration.""" + from __future__ import annotations from asyncio import timeout diff --git a/homeassistant/components/airzone/diagnostics.py b/homeassistant/components/airzone/diagnostics.py index f56a5106b25..8c75302d692 100644 --- a/homeassistant/components/airzone/diagnostics.py +++ b/homeassistant/components/airzone/diagnostics.py @@ -1,4 +1,5 @@ """Support for the Airzone diagnostics.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/airzone/entity.py b/homeassistant/components/airzone/entity.py index 2c3dba472ef..b360db61897 100644 --- a/homeassistant/components/airzone/entity.py +++ b/homeassistant/components/airzone/entity.py @@ -1,4 +1,5 @@ """Entity classes for the Airzone integration.""" + from __future__ import annotations import logging @@ -43,7 +44,7 @@ class AirzoneEntity(CoordinatorEntity[AirzoneUpdateCoordinator]): def get_airzone_value(self, key: str) -> Any: """Return Airzone entity value by key.""" - raise NotImplementedError() + raise NotImplementedError class AirzoneSystemEntity(AirzoneEntity): diff --git a/homeassistant/components/airzone/select.py b/homeassistant/components/airzone/select.py index 6f69d4454ee..6e92394bb05 100644 --- a/homeassistant/components/airzone/select.py +++ b/homeassistant/components/airzone/select.py @@ -1,4 +1,5 @@ """Support for the Airzone sensors.""" + from __future__ import annotations from dataclasses import dataclass @@ -26,19 +27,14 @@ from .coordinator import AirzoneUpdateCoordinator from .entity import AirzoneEntity, AirzoneZoneEntity -@dataclass(frozen=True) -class AirzoneSelectDescriptionMixin: - """Define an entity description mixin for select entities.""" +@dataclass(frozen=True, kw_only=True) +class AirzoneSelectDescription(SelectEntityDescription): + """Class to describe an Airzone select entity.""" api_param: str options_dict: dict[str, int] -@dataclass(frozen=True) -class AirzoneSelectDescription(SelectEntityDescription, AirzoneSelectDescriptionMixin): - """Class to describe an Airzone select entity.""" - - GRILLE_ANGLE_DICT: Final[dict[str, int]] = { "90deg": GrilleAngle.DEG_90, "50deg": GrilleAngle.DEG_50, @@ -88,22 +84,18 @@ async def async_setup_entry( """Add Airzone sensors from a config_entry.""" coordinator = hass.data[DOMAIN][entry.entry_id] - entities: list[AirzoneBaseSelect] = [] - - for system_zone_id, zone_data in coordinator.data[AZD_ZONES].items(): - for description in ZONE_SELECT_TYPES: - if description.key in zone_data: - entities.append( - AirzoneZoneSelect( - coordinator, - description, - entry, - system_zone_id, - zone_data, - ) - ) - - async_add_entities(entities) + async_add_entities( + AirzoneZoneSelect( + coordinator, + description, + entry, + system_zone_id, + zone_data, + ) + for description in ZONE_SELECT_TYPES + for system_zone_id, zone_data in coordinator.data[AZD_ZONES].items() + if description.key in zone_data + ) class AirzoneBaseSelect(AirzoneEntity, SelectEntity): diff --git a/homeassistant/components/airzone/sensor.py b/homeassistant/components/airzone/sensor.py index c14eaf48ff1..e2f9eabc6f6 100644 --- a/homeassistant/components/airzone/sensor.py +++ b/homeassistant/components/airzone/sensor.py @@ -1,4 +1,5 @@ """Support for the Airzone sensors.""" + from __future__ import annotations from typing import Any, Final @@ -81,44 +82,40 @@ async def async_setup_entry( """Add Airzone sensors from a config_entry.""" coordinator = hass.data[DOMAIN][entry.entry_id] - sensors: list[AirzoneSensor] = [] + sensors: list[AirzoneSensor] = [ + AirzoneZoneSensor( + coordinator, + description, + entry, + system_zone_id, + zone_data, + ) + for system_zone_id, zone_data in coordinator.data[AZD_ZONES].items() + for description in ZONE_SENSOR_TYPES + if description.key in zone_data + ] if AZD_HOT_WATER in coordinator.data: - dhw_data = coordinator.data[AZD_HOT_WATER] - for description in HOT_WATER_SENSOR_TYPES: - if description.key in dhw_data: - sensors.append( - AirzoneHotWaterSensor( - coordinator, - description, - entry, - ) - ) + sensors.extend( + AirzoneHotWaterSensor( + coordinator, + description, + entry, + ) + for description in HOT_WATER_SENSOR_TYPES + if description.key in coordinator.data[AZD_HOT_WATER] + ) if AZD_WEBSERVER in coordinator.data: - ws_data = coordinator.data[AZD_WEBSERVER] - for description in WEBSERVER_SENSOR_TYPES: - if description.key in ws_data: - sensors.append( - AirzoneWebServerSensor( - coordinator, - description, - entry, - ) - ) - - for system_zone_id, zone_data in coordinator.data[AZD_ZONES].items(): - for description in ZONE_SENSOR_TYPES: - if description.key in zone_data: - sensors.append( - AirzoneZoneSensor( - coordinator, - description, - entry, - system_zone_id, - zone_data, - ) - ) + sensors.extend( + AirzoneWebServerSensor( + coordinator, + description, + entry, + ) + for description in WEBSERVER_SENSOR_TYPES + if description.key in coordinator.data[AZD_WEBSERVER] + ) async_add_entities(sensors) diff --git a/homeassistant/components/airzone/water_heater.py b/homeassistant/components/airzone/water_heater.py index 58164edf3e9..4e502776185 100644 --- a/homeassistant/components/airzone/water_heater.py +++ b/homeassistant/components/airzone/water_heater.py @@ -1,4 +1,5 @@ """Support for the Airzone water heater.""" + from __future__ import annotations from typing import Any, Final diff --git a/homeassistant/components/airzone_cloud/__init__.py b/homeassistant/components/airzone_cloud/__init__.py index 697b80942f2..83be481a4de 100644 --- a/homeassistant/components/airzone_cloud/__init__.py +++ b/homeassistant/components/airzone_cloud/__init__.py @@ -1,4 +1,5 @@ """The Airzone Cloud integration.""" + from __future__ import annotations from aioairzone_cloud.cloudapi import AirzoneCloudApi diff --git a/homeassistant/components/airzone_cloud/binary_sensor.py b/homeassistant/components/airzone_cloud/binary_sensor.py index 20b747dfae3..9266ee3445e 100644 --- a/homeassistant/components/airzone_cloud/binary_sensor.py +++ b/homeassistant/components/airzone_cloud/binary_sensor.py @@ -1,4 +1,5 @@ """Support for the Airzone Cloud binary sensors.""" + from __future__ import annotations from dataclasses import dataclass @@ -98,43 +99,41 @@ async def async_setup_entry( """Add Airzone Cloud binary sensors from a config_entry.""" coordinator: AirzoneUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - binary_sensors: list[AirzoneBinarySensor] = [] + binary_sensors: list[AirzoneBinarySensor] = [ + AirzoneAidooBinarySensor( + coordinator, + description, + aidoo_id, + aidoo_data, + ) + for aidoo_id, aidoo_data in coordinator.data.get(AZD_AIDOOS, {}).items() + for description in AIDOO_BINARY_SENSOR_TYPES + if description.key in aidoo_data + ] - for aidoo_id, aidoo_data in coordinator.data.get(AZD_AIDOOS, {}).items(): - for description in AIDOO_BINARY_SENSOR_TYPES: - if description.key in aidoo_data: - binary_sensors.append( - AirzoneAidooBinarySensor( - coordinator, - description, - aidoo_id, - aidoo_data, - ) - ) + binary_sensors.extend( + AirzoneSystemBinarySensor( + coordinator, + description, + system_id, + system_data, + ) + for system_id, system_data in coordinator.data.get(AZD_SYSTEMS, {}).items() + for description in SYSTEM_BINARY_SENSOR_TYPES + if description.key in system_data + ) - for system_id, system_data in coordinator.data.get(AZD_SYSTEMS, {}).items(): - for description in SYSTEM_BINARY_SENSOR_TYPES: - if description.key in system_data: - binary_sensors.append( - AirzoneSystemBinarySensor( - coordinator, - description, - system_id, - system_data, - ) - ) - - for zone_id, zone_data in coordinator.data.get(AZD_ZONES, {}).items(): - for description in ZONE_BINARY_SENSOR_TYPES: - if description.key in zone_data: - binary_sensors.append( - AirzoneZoneBinarySensor( - coordinator, - description, - zone_id, - zone_data, - ) - ) + binary_sensors.extend( + AirzoneZoneBinarySensor( + coordinator, + description, + zone_id, + zone_data, + ) + for zone_id, zone_data in coordinator.data.get(AZD_ZONES, {}).items() + for description in ZONE_BINARY_SENSOR_TYPES + if description.key in zone_data + ) async_add_entities(binary_sensors) diff --git a/homeassistant/components/airzone_cloud/climate.py b/homeassistant/components/airzone_cloud/climate.py index 1bab9dd6c33..8fcdee11535 100644 --- a/homeassistant/components/airzone_cloud/climate.py +++ b/homeassistant/components/airzone_cloud/climate.py @@ -1,4 +1,5 @@ """Support for the Airzone Cloud climate.""" + from __future__ import annotations from typing import Any, Final @@ -10,6 +11,7 @@ from aioairzone_cloud.const import ( API_PARAMS, API_POWER, API_SETPOINT, + API_SPEED_CONF, API_UNITS, API_VALUE, AZD_ACTION, @@ -23,6 +25,8 @@ from aioairzone_cloud.const import ( AZD_NUM_DEVICES, AZD_NUM_GROUPS, AZD_POWER, + AZD_SPEED, + AZD_SPEEDS, AZD_TEMP, AZD_TEMP_SET, AZD_TEMP_SET_MAX, @@ -33,6 +37,10 @@ from aioairzone_cloud.const import ( from homeassistant.components.climate import ( ATTR_HVAC_MODE, + FAN_AUTO, + FAN_HIGH, + FAN_LOW, + FAN_MEDIUM, ClimateEntity, ClimateEntityFeature, HVACAction, @@ -54,6 +62,22 @@ from .entity import ( AirzoneZoneEntity, ) +FAN_SPEED_AUTO: dict[int, str] = { + 0: FAN_AUTO, +} + +FAN_SPEED_MAPS: Final[dict[int, dict[int, str]]] = { + 2: { + 1: FAN_LOW, + 2: FAN_HIGH, + }, + 3: { + 1: FAN_LOW, + 2: FAN_MEDIUM, + 3: FAN_HIGH, + }, +} + HVAC_ACTION_LIB_TO_HASS: Final[dict[OperationAction, HVACAction]] = { OperationAction.COOLING: HVACAction.COOLING, OperationAction.DRYING: HVACAction.DRYING, @@ -274,6 +298,9 @@ class AirzoneDeviceGroupClimate(AirzoneClimate): class AirzoneAidooClimate(AirzoneAidooEntity, AirzoneDeviceClimate): """Define an Airzone Cloud Aidoo climate.""" + _speeds: dict[int, str] + _speeds_reverse: dict[str, int] + def __init__( self, coordinator: AirzoneUpdateCoordinator, @@ -290,9 +317,52 @@ class AirzoneAidooClimate(AirzoneAidooEntity, AirzoneDeviceClimate): ] if HVACMode.OFF not in self._attr_hvac_modes: self._attr_hvac_modes += [HVACMode.OFF] + if ( + self.get_airzone_value(AZD_SPEED) is not None + and self.get_airzone_value(AZD_SPEEDS) is not None + ): + self._initialize_fan_speeds() self._async_update_attrs() + def _initialize_fan_speeds(self) -> None: + """Initialize Aidoo fan speeds.""" + azd_speeds: dict[int, int] = self.get_airzone_value(AZD_SPEEDS) + max_speed = max(azd_speeds) + + fan_speeds: dict[int, str] + if speeds_map := FAN_SPEED_MAPS.get(max_speed): + fan_speeds = speeds_map + else: + fan_speeds = {} + + for speed in azd_speeds: + if speed != 0: + fan_speeds[speed] = f"{int(round((speed * 100) / max_speed, 0))}%" + + if 0 in azd_speeds: + fan_speeds = FAN_SPEED_AUTO | fan_speeds + + self._speeds = {} + for key, value in fan_speeds.items(): + _key = azd_speeds.get(key) + if _key is not None: + self._speeds[_key] = value + + self._speeds_reverse = {v: k for k, v in self._speeds.items()} + self._attr_fan_modes = list(self._speeds_reverse) + + self._attr_supported_features |= ClimateEntityFeature.FAN_MODE + + async def async_set_fan_mode(self, fan_mode: str) -> None: + """Set Aidoo fan mode.""" + params: dict[str, Any] = { + API_SPEED_CONF: { + API_VALUE: self._speeds_reverse.get(fan_mode), + } + } + await self._async_update_params(params) + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set hvac mode.""" params: dict[str, Any] = {} @@ -310,6 +380,14 @@ class AirzoneAidooClimate(AirzoneAidooEntity, AirzoneDeviceClimate): } await self._async_update_params(params) + @callback + def _async_update_attrs(self) -> None: + """Update Aidoo climate attributes.""" + super()._async_update_attrs() + + if self.supported_features & ClimateEntityFeature.FAN_MODE: + self._attr_fan_mode = self._speeds.get(self.get_airzone_value(AZD_SPEED)) + class AirzoneGroupClimate(AirzoneGroupEntity, AirzoneDeviceGroupClimate): """Define an Airzone Cloud Group climate.""" diff --git a/homeassistant/components/airzone_cloud/config_flow.py b/homeassistant/components/airzone_cloud/config_flow.py index 0d04f78245d..e4e6a6ccbf3 100644 --- a/homeassistant/components/airzone_cloud/config_flow.py +++ b/homeassistant/components/airzone_cloud/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Airzone Cloud.""" + from __future__ import annotations from typing import Any @@ -9,9 +10,8 @@ from aioairzone_cloud.const import AZD_ID, AZD_NAME, AZD_WEBSERVERS from aioairzone_cloud.exceptions import AirzoneCloudError, LoginError import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_ID, CONF_PASSWORD, CONF_USERNAME -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import aiohttp_client from homeassistant.helpers.selector import ( SelectOptionDict, @@ -23,14 +23,14 @@ from homeassistant.helpers.selector import ( from .const import DOMAIN -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class AirZoneCloudConfigFlow(ConfigFlow, domain=DOMAIN): """Handle config flow for an Airzone Cloud device.""" airzone: AirzoneCloudApi async def async_step_inst_pick( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the installation selection.""" errors = {} options: dict[str, str] = {} @@ -81,7 +81,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors = {} diff --git a/homeassistant/components/airzone_cloud/coordinator.py b/homeassistant/components/airzone_cloud/coordinator.py index 37b31c68ee7..e510dcfb401 100644 --- a/homeassistant/components/airzone_cloud/coordinator.py +++ b/homeassistant/components/airzone_cloud/coordinator.py @@ -1,4 +1,5 @@ """The Airzone Cloud integration coordinator.""" + from __future__ import annotations from asyncio import timeout @@ -25,6 +26,7 @@ class AirzoneUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): def __init__(self, hass: HomeAssistant, airzone: AirzoneCloudApi) -> None: """Initialize.""" self.airzone = airzone + self.airzone.set_update_callback(self.async_set_updated_data) super().__init__( hass, diff --git a/homeassistant/components/airzone_cloud/diagnostics.py b/homeassistant/components/airzone_cloud/diagnostics.py index 0bce3251d5a..372455a4597 100644 --- a/homeassistant/components/airzone_cloud/diagnostics.py +++ b/homeassistant/components/airzone_cloud/diagnostics.py @@ -1,4 +1,5 @@ """Support for the Airzone Cloud diagnostics.""" + from __future__ import annotations from collections.abc import Mapping diff --git a/homeassistant/components/airzone_cloud/entity.py b/homeassistant/components/airzone_cloud/entity.py index a175167be5a..f53321ce353 100644 --- a/homeassistant/components/airzone_cloud/entity.py +++ b/homeassistant/components/airzone_cloud/entity.py @@ -1,4 +1,5 @@ """Entity classes for the Airzone Cloud integration.""" + from __future__ import annotations from abc import ABC, abstractmethod diff --git a/homeassistant/components/airzone_cloud/manifest.json b/homeassistant/components/airzone_cloud/manifest.json index 3b8247d003c..14f02620c91 100644 --- a/homeassistant/components/airzone_cloud/manifest.json +++ b/homeassistant/components/airzone_cloud/manifest.json @@ -4,7 +4,7 @@ "codeowners": ["@Noltari"], "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/airzone_cloud", - "iot_class": "cloud_polling", + "iot_class": "cloud_push", "loggers": ["aioairzone_cloud"], - "requirements": ["aioairzone-cloud==0.4.5"] + "requirements": ["aioairzone-cloud==0.4.6"] } diff --git a/homeassistant/components/airzone_cloud/sensor.py b/homeassistant/components/airzone_cloud/sensor.py index 965ac24a64f..febbbcc7ef6 100644 --- a/homeassistant/components/airzone_cloud/sensor.py +++ b/homeassistant/components/airzone_cloud/sensor.py @@ -1,4 +1,5 @@ """Support for the Airzone Cloud sensors.""" + from __future__ import annotations from typing import Any, Final @@ -107,46 +108,44 @@ async def async_setup_entry( """Add Airzone Cloud sensors from a config_entry.""" coordinator: AirzoneUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - sensors: list[AirzoneSensor] = [] - # Aidoos - for aidoo_id, aidoo_data in coordinator.data.get(AZD_AIDOOS, {}).items(): - for description in AIDOO_SENSOR_TYPES: - if description.key in aidoo_data: - sensors.append( - AirzoneAidooSensor( - coordinator, - description, - aidoo_id, - aidoo_data, - ) - ) + sensors: list[AirzoneSensor] = [ + AirzoneAidooSensor( + coordinator, + description, + aidoo_id, + aidoo_data, + ) + for aidoo_id, aidoo_data in coordinator.data.get(AZD_AIDOOS, {}).items() + for description in AIDOO_SENSOR_TYPES + if description.key in aidoo_data + ] # WebServers - for ws_id, ws_data in coordinator.data.get(AZD_WEBSERVERS, {}).items(): - for description in WEBSERVER_SENSOR_TYPES: - if description.key in ws_data: - sensors.append( - AirzoneWebServerSensor( - coordinator, - description, - ws_id, - ws_data, - ) - ) + sensors.extend( + AirzoneWebServerSensor( + coordinator, + description, + ws_id, + ws_data, + ) + for ws_id, ws_data in coordinator.data.get(AZD_WEBSERVERS, {}).items() + for description in WEBSERVER_SENSOR_TYPES + if description.key in ws_data + ) # Zones - for zone_id, zone_data in coordinator.data.get(AZD_ZONES, {}).items(): - for description in ZONE_SENSOR_TYPES: - if description.key in zone_data: - sensors.append( - AirzoneZoneSensor( - coordinator, - description, - zone_id, - zone_data, - ) - ) + sensors.extend( + AirzoneZoneSensor( + coordinator, + description, + zone_id, + zone_data, + ) + for zone_id, zone_data in coordinator.data.get(AZD_ZONES, {}).items() + for description in ZONE_SENSOR_TYPES + if description.key in zone_data + ) async_add_entities(sensors) diff --git a/homeassistant/components/aladdin_connect/__init__.py b/homeassistant/components/aladdin_connect/__init__.py index d1c7bc5668b..84710c3f74e 100644 --- a/homeassistant/components/aladdin_connect/__init__.py +++ b/homeassistant/components/aladdin_connect/__init__.py @@ -1,4 +1,5 @@ """The aladdin_connect component.""" + import logging from typing import Final diff --git a/homeassistant/components/aladdin_connect/config_flow.py b/homeassistant/components/aladdin_connect/config_flow.py index d14b7b7c35e..df822086db7 100644 --- a/homeassistant/components/aladdin_connect/config_flow.py +++ b/homeassistant/components/aladdin_connect/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Aladdin Connect cover integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -9,10 +10,9 @@ import AIOAladdinConnect.session_manager as Aladdin from aiohttp.client_exceptions import ClientError import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -48,13 +48,15 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> None: raise InvalidAuth from ex -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class AladdinConnectConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Aladdin Connect.""" VERSION = 1 - entry: config_entries.ConfigEntry | None + entry: ConfigEntry | None - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Handle re-authentication with Aladdin Connect.""" self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) @@ -62,7 +64,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm re-authentication with Aladdin Connect.""" errors: dict[str, str] = {} @@ -102,7 +104,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" if user_input is None: return self.async_show_form( diff --git a/homeassistant/components/aladdin_connect/const.py b/homeassistant/components/aladdin_connect/const.py index 46d5d468f71..bf77c032d1b 100644 --- a/homeassistant/components/aladdin_connect/const.py +++ b/homeassistant/components/aladdin_connect/const.py @@ -1,4 +1,5 @@ """Platform for the Aladdin Connect cover component.""" + from __future__ import annotations from typing import Final diff --git a/homeassistant/components/aladdin_connect/cover.py b/homeassistant/components/aladdin_connect/cover.py index f4104a39365..61c8df92eaf 100644 --- a/homeassistant/components/aladdin_connect/cover.py +++ b/homeassistant/components/aladdin_connect/cover.py @@ -1,4 +1,5 @@ """Platform for the Aladdin Connect cover component.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/aladdin_connect/diagnostics.py b/homeassistant/components/aladdin_connect/diagnostics.py index c49d321631e..b838ff79da3 100644 --- a/homeassistant/components/aladdin_connect/diagnostics.py +++ b/homeassistant/components/aladdin_connect/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for Aladdin Connect.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/aladdin_connect/model.py b/homeassistant/components/aladdin_connect/model.py index 9b250459d3b..73e445f2f3b 100644 --- a/homeassistant/components/aladdin_connect/model.py +++ b/homeassistant/components/aladdin_connect/model.py @@ -1,4 +1,5 @@ """Models for Aladdin connect cover platform.""" + from __future__ import annotations from typing import TypedDict diff --git a/homeassistant/components/aladdin_connect/sensor.py b/homeassistant/components/aladdin_connect/sensor.py index 0a264edc8c2..22aa9c6faf0 100644 --- a/homeassistant/components/aladdin_connect/sensor.py +++ b/homeassistant/components/aladdin_connect/sensor.py @@ -1,4 +1,5 @@ """Support for Aladdin Connect Garage Door sensors.""" + from __future__ import annotations from collections.abc import Callable @@ -23,20 +24,13 @@ from .const import DOMAIN from .model import DoorDevice -@dataclass(frozen=True) -class AccSensorEntityDescriptionMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class AccSensorEntityDescription(SensorEntityDescription): + """Describes AladdinConnect sensor entity.""" value_fn: Callable -@dataclass(frozen=True) -class AccSensorEntityDescription( - SensorEntityDescription, AccSensorEntityDescriptionMixin -): - """Describes AladdinConnect sensor entity.""" - - SENSORS: tuple[AccSensorEntityDescription, ...] = ( AccSensorEntityDescription( key="battery_level", diff --git a/homeassistant/components/alarm_control_panel/__init__.py b/homeassistant/components/alarm_control_panel/__init__.py index 45e1d63e0c2..63c095ea6ce 100644 --- a/homeassistant/components/alarm_control_panel/__init__.py +++ b/homeassistant/components/alarm_control_panel/__init__.py @@ -1,4 +1,5 @@ """Component to interface with an alarm control panel.""" + from __future__ import annotations from datetime import timedelta @@ -32,6 +33,7 @@ from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.typing import ConfigType +from . import group as group_pre_import # noqa: F401 from .const import ( # noqa: F401 _DEPRECATED_FORMAT_NUMBER, _DEPRECATED_FORMAT_TEXT, @@ -171,7 +173,7 @@ class AlarmControlPanelEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_A def alarm_disarm(self, code: str | None = None) -> None: """Send disarm command.""" - raise NotImplementedError() + raise NotImplementedError async def async_alarm_disarm(self, code: str | None = None) -> None: """Send disarm command.""" @@ -179,7 +181,7 @@ class AlarmControlPanelEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_A def alarm_arm_home(self, code: str | None = None) -> None: """Send arm home command.""" - raise NotImplementedError() + raise NotImplementedError async def async_alarm_arm_home(self, code: str | None = None) -> None: """Send arm home command.""" @@ -187,7 +189,7 @@ class AlarmControlPanelEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_A def alarm_arm_away(self, code: str | None = None) -> None: """Send arm away command.""" - raise NotImplementedError() + raise NotImplementedError async def async_alarm_arm_away(self, code: str | None = None) -> None: """Send arm away command.""" @@ -195,7 +197,7 @@ class AlarmControlPanelEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_A def alarm_arm_night(self, code: str | None = None) -> None: """Send arm night command.""" - raise NotImplementedError() + raise NotImplementedError async def async_alarm_arm_night(self, code: str | None = None) -> None: """Send arm night command.""" @@ -203,7 +205,7 @@ class AlarmControlPanelEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_A def alarm_arm_vacation(self, code: str | None = None) -> None: """Send arm vacation command.""" - raise NotImplementedError() + raise NotImplementedError async def async_alarm_arm_vacation(self, code: str | None = None) -> None: """Send arm vacation command.""" @@ -211,7 +213,7 @@ class AlarmControlPanelEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_A def alarm_trigger(self, code: str | None = None) -> None: """Send alarm trigger command.""" - raise NotImplementedError() + raise NotImplementedError async def async_alarm_trigger(self, code: str | None = None) -> None: """Send alarm trigger command.""" @@ -219,7 +221,7 @@ class AlarmControlPanelEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_A def alarm_arm_custom_bypass(self, code: str | None = None) -> None: """Send arm custom bypass command.""" - raise NotImplementedError() + raise NotImplementedError async def async_alarm_arm_custom_bypass(self, code: str | None = None) -> None: """Send arm custom bypass command.""" diff --git a/homeassistant/components/alarm_control_panel/const.py b/homeassistant/components/alarm_control_panel/const.py index fe4be649e19..2e8fe98da3b 100644 --- a/homeassistant/components/alarm_control_panel/const.py +++ b/homeassistant/components/alarm_control_panel/const.py @@ -1,4 +1,5 @@ """Provides the constants needed for component.""" + from enum import IntFlag, StrEnum from functools import partial from typing import Final diff --git a/homeassistant/components/alarm_control_panel/device_action.py b/homeassistant/components/alarm_control_panel/device_action.py index 9c068bb3327..72b1084d072 100644 --- a/homeassistant/components/alarm_control_panel/device_action.py +++ b/homeassistant/components/alarm_control_panel/device_action.py @@ -1,4 +1,5 @@ """Provides device automations for Alarm control panel.""" + from __future__ import annotations from typing import Final diff --git a/homeassistant/components/alarm_control_panel/device_condition.py b/homeassistant/components/alarm_control_panel/device_condition.py index e3c627d17a3..227fc31413e 100644 --- a/homeassistant/components/alarm_control_panel/device_condition.py +++ b/homeassistant/components/alarm_control_panel/device_condition.py @@ -1,4 +1,5 @@ """Provide the device automations for Alarm control panel.""" + from __future__ import annotations from typing import Final diff --git a/homeassistant/components/alarm_control_panel/device_trigger.py b/homeassistant/components/alarm_control_panel/device_trigger.py index e5141a1dfd5..557666720e8 100644 --- a/homeassistant/components/alarm_control_panel/device_trigger.py +++ b/homeassistant/components/alarm_control_panel/device_trigger.py @@ -1,4 +1,5 @@ """Provides device automations for Alarm control panel.""" + from __future__ import annotations from typing import Final diff --git a/homeassistant/components/alarm_control_panel/group.py b/homeassistant/components/alarm_control_panel/group.py index dabe49069d5..e0806822cef 100644 --- a/homeassistant/components/alarm_control_panel/group.py +++ b/homeassistant/components/alarm_control_panel/group.py @@ -1,7 +1,7 @@ """Describe group states.""" +from typing import TYPE_CHECKING -from homeassistant.components.group import GroupIntegrationRegistry from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_CUSTOM_BYPASS, @@ -13,10 +13,13 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback +if TYPE_CHECKING: + from homeassistant.components.group import GroupIntegrationRegistry + @callback def async_describe_on_off_states( - hass: HomeAssistant, registry: GroupIntegrationRegistry + hass: HomeAssistant, registry: "GroupIntegrationRegistry" ) -> None: """Describe group on off states.""" registry.on_off_states( diff --git a/homeassistant/components/alarm_control_panel/icons.json b/homeassistant/components/alarm_control_panel/icons.json index 62a9eee2915..915448a9962 100644 --- a/homeassistant/components/alarm_control_panel/icons.json +++ b/homeassistant/components/alarm_control_panel/icons.json @@ -18,9 +18,9 @@ "alarm_arm_away": "mdi:shield-lock", "alarm_arm_home": "mdi:shield-home", "alarm_arm_night": "mdi:shield-moon", - "alarm_custom_bypass": "mdi:security", + "alarm_arm_custom_bypass": "mdi:security", "alarm_disarm": "mdi:shield-off", "alarm_trigger": "mdi:bell-ring", - "arlam_arm_vacation": "mdi:shield-airplane" + "alarm_arm_vacation": "mdi:shield-airplane" } } diff --git a/homeassistant/components/alarm_control_panel/reproduce_state.py b/homeassistant/components/alarm_control_panel/reproduce_state.py index ad992012c04..5a3d79fe2ed 100644 --- a/homeassistant/components/alarm_control_panel/reproduce_state.py +++ b/homeassistant/components/alarm_control_panel/reproduce_state.py @@ -1,4 +1,5 @@ """Reproduce an Alarm control panel state.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/alarm_control_panel/significant_change.py b/homeassistant/components/alarm_control_panel/significant_change.py index bde6d151393..4a2209e0868 100644 --- a/homeassistant/components/alarm_control_panel/significant_change.py +++ b/homeassistant/components/alarm_control_panel/significant_change.py @@ -1,4 +1,5 @@ """Helper to test significant Alarm Control Panel state changes.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/alarmdecoder/__init__.py b/homeassistant/components/alarmdecoder/__init__.py index 19d1d729a5e..c05c6ea6119 100644 --- a/homeassistant/components/alarmdecoder/__init__.py +++ b/homeassistant/components/alarmdecoder/__init__.py @@ -1,4 +1,5 @@ """Support for AlarmDecoder devices.""" + from datetime import timedelta import logging diff --git a/homeassistant/components/alarmdecoder/alarm_control_panel.py b/homeassistant/components/alarmdecoder/alarm_control_panel.py index ca11b9d6894..2e2db6f070f 100644 --- a/homeassistant/components/alarmdecoder/alarm_control_panel.py +++ b/homeassistant/components/alarmdecoder/alarm_control_panel.py @@ -1,4 +1,5 @@ """Support for AlarmDecoder-based alarm control panels (Honeywell/DSC).""" + from __future__ import annotations import voluptuous as vol diff --git a/homeassistant/components/alarmdecoder/binary_sensor.py b/homeassistant/components/alarmdecoder/binary_sensor.py index 47e6066400c..1d41dcd2364 100644 --- a/homeassistant/components/alarmdecoder/binary_sensor.py +++ b/homeassistant/components/alarmdecoder/binary_sensor.py @@ -1,4 +1,5 @@ """Support for AlarmDecoder zone states- represented as binary sensors.""" + import logging from homeassistant.components.binary_sensor import BinarySensorEntity diff --git a/homeassistant/components/alarmdecoder/config_flow.py b/homeassistant/components/alarmdecoder/config_flow.py index 1b2bcf083ba..a775375b835 100644 --- a/homeassistant/components/alarmdecoder/config_flow.py +++ b/homeassistant/components/alarmdecoder/config_flow.py @@ -1,4 +1,5 @@ """Config flow for AlarmDecoder.""" + from __future__ import annotations import logging @@ -9,14 +10,17 @@ from alarmdecoder.devices import SerialDevice, SocketDevice from alarmdecoder.util import NoDeviceError import voluptuous as vol -from homeassistant import config_entries from homeassistant.components.binary_sensor import ( DEVICE_CLASSES_SCHEMA as BINARY_SENSOR_DEVICE_CLASSES_SCHEMA, ) -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.const import CONF_HOST, CONF_PORT, CONF_PROTOCOL from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult from .const import ( CONF_ALT_NIGHT_MODE, @@ -52,7 +56,7 @@ EDIT_SETTINGS = "Arming Settings" _LOGGER = logging.getLogger(__name__) -class AlarmDecoderFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class AlarmDecoderFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a AlarmDecoder config flow.""" VERSION = 1 @@ -64,14 +68,14 @@ class AlarmDecoderFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback def async_get_options_flow( - config_entry: config_entries.ConfigEntry, + config_entry: ConfigEntry, ) -> AlarmDecoderOptionsFlowHandler: """Get the options flow for AlarmDecoder.""" return AlarmDecoderOptionsFlowHandler(config_entry) async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" if user_input is not None: self.protocol = user_input[CONF_PROTOCOL] @@ -90,7 +94,7 @@ class AlarmDecoderFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_protocol( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle AlarmDecoder protocol setup.""" errors = {} if user_input is not None: @@ -150,12 +154,12 @@ class AlarmDecoderFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) -class AlarmDecoderOptionsFlowHandler(config_entries.OptionsFlow): +class AlarmDecoderOptionsFlowHandler(OptionsFlow): """Handle AlarmDecoder options.""" selected_zone: str | None = None - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + def __init__(self, config_entry: ConfigEntry) -> None: """Initialize AlarmDecoder options flow.""" self.arm_options = config_entry.options.get(OPTIONS_ARM, DEFAULT_ARM_OPTIONS) self.zone_options = config_entry.options.get( @@ -164,7 +168,7 @@ class AlarmDecoderOptionsFlowHandler(config_entries.OptionsFlow): async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Manage the options.""" if user_input is not None: if user_input[EDIT_KEY] == EDIT_SETTINGS: @@ -185,7 +189,7 @@ class AlarmDecoderOptionsFlowHandler(config_entries.OptionsFlow): async def async_step_arm_settings( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Arming options form.""" if user_input is not None: return self.async_create_entry( @@ -214,7 +218,7 @@ class AlarmDecoderOptionsFlowHandler(config_entries.OptionsFlow): async def async_step_zone_select( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Zone selection form.""" errors = _validate_zone_input(user_input) @@ -232,7 +236,7 @@ class AlarmDecoderOptionsFlowHandler(config_entries.OptionsFlow): async def async_step_zone_details( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Zone details form.""" errors = _validate_zone_input(user_input) diff --git a/homeassistant/components/alarmdecoder/sensor.py b/homeassistant/components/alarmdecoder/sensor.py index 1598171649b..e796334a91c 100644 --- a/homeassistant/components/alarmdecoder/sensor.py +++ b/homeassistant/components/alarmdecoder/sensor.py @@ -1,4 +1,5 @@ """Support for AlarmDecoder sensors (Shows Panel Display).""" + from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/alert/__init__.py b/homeassistant/components/alert/__init__.py index 721ed0d0c21..471d32227c2 100644 --- a/homeassistant/components/alert/__init__.py +++ b/homeassistant/components/alert/__init__.py @@ -1,4 +1,5 @@ """Support for repeating alerts when conditions are met.""" + from __future__ import annotations from collections.abc import Callable @@ -25,7 +26,7 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, ) -from homeassistant.core import HassJob, HomeAssistant +from homeassistant.core import Event, HassJob, HomeAssistant from homeassistant.exceptions import ServiceNotFound import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -36,7 +37,7 @@ from homeassistant.helpers.event import ( async_track_state_change_event, ) from homeassistant.helpers.template import Template -from homeassistant.helpers.typing import ConfigType, EventType +from homeassistant.helpers.typing import ConfigType from homeassistant.util.dt import now from .const import ( @@ -198,9 +199,7 @@ class Alert(Entity): return STATE_ON return STATE_IDLE - async def watched_entity_change( - self, event: EventType[EventStateChangedData] - ) -> None: + async def watched_entity_change(self, event: Event[EventStateChangedData]) -> None: """Determine if the alert should start or stop.""" if (to_state := event.data["new_state"]) is None: return diff --git a/homeassistant/components/alert/icons.json b/homeassistant/components/alert/icons.json new file mode 100644 index 00000000000..7f5258706d2 --- /dev/null +++ b/homeassistant/components/alert/icons.json @@ -0,0 +1,7 @@ +{ + "services": { + "toggle": "mdi:bell-ring", + "turn_off": "mdi:bell-off", + "turn_on": "mdi:bell-alert" + } +} diff --git a/homeassistant/components/alert/reproduce_state.py b/homeassistant/components/alert/reproduce_state.py index 1e813768b3a..db540369d84 100644 --- a/homeassistant/components/alert/reproduce_state.py +++ b/homeassistant/components/alert/reproduce_state.py @@ -1,4 +1,5 @@ """Reproduce an Alert state.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/alexa/__init__.py b/homeassistant/components/alexa/__init__.py index 2a9637772b1..eeeb8e53e43 100644 --- a/homeassistant/components/alexa/__init__.py +++ b/homeassistant/components/alexa/__init__.py @@ -1,4 +1,5 @@ """Support for Alexa skill service end point.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/alexa/auth.py b/homeassistant/components/alexa/auth.py index 10a7be4967e..63fa7edc17a 100644 --- a/homeassistant/components/alexa/auth.py +++ b/homeassistant/components/alexa/auth.py @@ -1,4 +1,5 @@ """Support for Alexa skill auth.""" + import asyncio from asyncio import timeout from datetime import datetime, timedelta diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index d30f3f7376d..ecb7d5cb5a8 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -1,4 +1,5 @@ """Alexa capabilities.""" + from __future__ import annotations from collections.abc import Generator @@ -1195,11 +1196,12 @@ class AlexaThermostatController(AlexaCapability): if self.entity.domain == water_heater.DOMAIN: return None - supported_modes: list[str] = [] hvac_modes = self.entity.attributes.get(climate.ATTR_HVAC_MODES) or [] - for mode in hvac_modes: - if thermostat_mode := API_THERMOSTAT_MODES.get(mode): - supported_modes.append(thermostat_mode) + supported_modes: list[str] = [ + API_THERMOSTAT_MODES[mode] + for mode in hvac_modes + if mode in API_THERMOSTAT_MODES + ] preset_modes = self.entity.attributes.get(climate.ATTR_PRESET_MODES) if preset_modes: @@ -1582,7 +1584,7 @@ class AlexaModeController(AlexaCapability): ) modes += 1 - # Alexa requiers at least 2 modes + # Alexa requires at least 2 modes if modes == 1: self._resource.add_mode(f"state.{PRESET_MODE_NA}", [PRESET_MODE_NA]) diff --git a/homeassistant/components/alexa/config.py b/homeassistant/components/alexa/config.py index 02aaed25742..fb589dde566 100644 --- a/homeassistant/components/alexa/config.py +++ b/homeassistant/components/alexa/config.py @@ -1,4 +1,5 @@ """Config helpers for Alexa.""" + from __future__ import annotations from abc import ABC, abstractmethod diff --git a/homeassistant/components/alexa/const.py b/homeassistant/components/alexa/const.py index abdef0cb566..2c615b71166 100644 --- a/homeassistant/components/alexa/const.py +++ b/homeassistant/components/alexa/const.py @@ -1,4 +1,5 @@ """Constants for the Alexa integration.""" + from collections import OrderedDict from homeassistant.components import climate diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index ddc0bc70987..61ab220c60c 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -1,4 +1,5 @@ """Alexa entity adapters.""" + from __future__ import annotations from collections.abc import Generator, Iterable diff --git a/homeassistant/components/alexa/errors.py b/homeassistant/components/alexa/errors.py index f8e3720e160..c341356db86 100644 --- a/homeassistant/components/alexa/errors.py +++ b/homeassistant/components/alexa/errors.py @@ -1,4 +1,5 @@ """Alexa related errors.""" + from __future__ import annotations from typing import Any, Literal diff --git a/homeassistant/components/alexa/flash_briefings.py b/homeassistant/components/alexa/flash_briefings.py index 3361908ce9a..eed700602ce 100644 --- a/homeassistant/components/alexa/flash_briefings.py +++ b/homeassistant/components/alexa/flash_briefings.py @@ -1,4 +1,5 @@ """Support for Alexa skill service end point.""" + import hmac from http import HTTPStatus import logging diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index b5b72bc6dc5..30c2fecccf8 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -1,4 +1,5 @@ """Alexa message handlers.""" + from __future__ import annotations from collections.abc import Callable, Coroutine diff --git a/homeassistant/components/alexa/intent.py b/homeassistant/components/alexa/intent.py index 58319dd44b5..8d266e4a634 100644 --- a/homeassistant/components/alexa/intent.py +++ b/homeassistant/components/alexa/intent.py @@ -1,4 +1,5 @@ """Support for Alexa skill service end point.""" + import enum import logging from typing import Any @@ -49,7 +50,6 @@ async def async_setup_intents(hass: HomeAssistant) -> None: Right now this module does not expose any, but the intent component breaks without it. """ - pass # pylint: disable=unnecessary-pass class UnknownRequest(HomeAssistantError): @@ -64,7 +64,7 @@ class AlexaIntentsView(http.HomeAssistantView): async def post(self, request: http.HomeAssistantRequest) -> Response | bytes: """Handle Alexa.""" - hass: HomeAssistant = request.app["hass"] + hass = request.app[http.KEY_HASS] message: dict[str, Any] = await request.json() _LOGGER.debug("Received Alexa request: %s", message) diff --git a/homeassistant/components/alexa/logbook.py b/homeassistant/components/alexa/logbook.py index cb6835c7ba5..3e641e715f3 100644 --- a/homeassistant/components/alexa/logbook.py +++ b/homeassistant/components/alexa/logbook.py @@ -1,4 +1,5 @@ """Describe logbook events.""" + from collections.abc import Callable from typing import Any diff --git a/homeassistant/components/alexa/resources.py b/homeassistant/components/alexa/resources.py index 3606c5401ee..4bc63f6ccae 100644 --- a/homeassistant/components/alexa/resources.py +++ b/homeassistant/components/alexa/resources.py @@ -1,6 +1,5 @@ """Alexa Resources and Assets.""" - from typing import Any @@ -225,7 +224,7 @@ class AlexaCapabilityResource: Return ModeResources, PresetResources friendlyNames serialized. """ - raise NotImplementedError() + raise NotImplementedError def serialize_labels(self, resources: list[str]) -> dict[str, list[dict[str, Any]]]: """Return serialized labels for an API response. diff --git a/homeassistant/components/alexa/smart_home.py b/homeassistant/components/alexa/smart_home.py index 88f66e93fc1..81ce2981acb 100644 --- a/homeassistant/components/alexa/smart_home.py +++ b/homeassistant/components/alexa/smart_home.py @@ -1,4 +1,5 @@ """Support for alexa Smart Home Skill API.""" + import logging from typing import Any @@ -7,8 +8,11 @@ from yarl import URL from homeassistant import core from homeassistant.auth.models import User -from homeassistant.components.http import HomeAssistantRequest -from homeassistant.components.http.view import HomeAssistantView +from homeassistant.components.http import ( + KEY_HASS, + HomeAssistantRequest, + HomeAssistantView, +) from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.core import Context, HomeAssistant from homeassistant.helpers import entity_registry as er @@ -146,7 +150,7 @@ class SmartHomeView(HomeAssistantView): Lambda, which will need to forward the requests to here and pass back the response. """ - hass: HomeAssistant = request.app["hass"] + hass = request.app[KEY_HASS] user: User = request["hass_user"] message: dict[str, Any] = await request.json() diff --git a/homeassistant/components/alexa/state_report.py b/homeassistant/components/alexa/state_report.py index 3ad863747e5..9c640d76dd4 100644 --- a/homeassistant/components/alexa/state_report.py +++ b/homeassistant/components/alexa/state_report.py @@ -1,4 +1,5 @@ """Alexa state report code.""" + from __future__ import annotations from asyncio import timeout diff --git a/homeassistant/components/alpha_vantage/sensor.py b/homeassistant/components/alpha_vantage/sensor.py index 52427065f68..dc62a734d42 100644 --- a/homeassistant/components/alpha_vantage/sensor.py +++ b/homeassistant/components/alpha_vantage/sensor.py @@ -1,4 +1,5 @@ """Stock market information from Alpha Vantage.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/amazon_polly/const.py b/homeassistant/components/amazon_polly/const.py index e1f7afce174..66084735c39 100644 --- a/homeassistant/components/amazon_polly/const.py +++ b/homeassistant/components/amazon_polly/const.py @@ -1,4 +1,5 @@ """Constants for the Amazon Polly text to speech service.""" + from __future__ import annotations from typing import Final diff --git a/homeassistant/components/amazon_polly/tts.py b/homeassistant/components/amazon_polly/tts.py index 5db46fc019e..bde690a3163 100644 --- a/homeassistant/components/amazon_polly/tts.py +++ b/homeassistant/components/amazon_polly/tts.py @@ -1,4 +1,5 @@ """Support for the Amazon Polly text to speech service.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/amberelectric/config_flow.py b/homeassistant/components/amberelectric/config_flow.py index 765e219b6d7..174e8716e0b 100644 --- a/homeassistant/components/amberelectric/config_flow.py +++ b/homeassistant/components/amberelectric/config_flow.py @@ -1,4 +1,5 @@ """Config flow for the Amber Electric integration.""" + from __future__ import annotations import amberelectric @@ -6,9 +7,8 @@ from amberelectric.api import amber_api from amberelectric.model.site import Site, SiteStatus import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_API_TOKEN -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.selector import ( SelectOptionDict, SelectSelector, @@ -43,7 +43,7 @@ def filter_sites(sites: list[Site]) -> list[Site]: return filtered -class AmberElectricConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class AmberElectricConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow.""" VERSION = 1 @@ -73,7 +73,7 @@ class AmberElectricConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, str] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Step when user initializes a integration.""" self._errors = {} self._sites = None @@ -107,7 +107,7 @@ class AmberElectricConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_site( self, user_input: dict[str, str] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Step to select site.""" self._errors = {} diff --git a/homeassistant/components/amberelectric/const.py b/homeassistant/components/amberelectric/const.py index 6166b21c19f..56324628ed6 100644 --- a/homeassistant/components/amberelectric/const.py +++ b/homeassistant/components/amberelectric/const.py @@ -1,4 +1,5 @@ """Amber Electric Constants.""" + import logging from homeassistant.const import Platform diff --git a/homeassistant/components/amberelectric/coordinator.py b/homeassistant/components/amberelectric/coordinator.py index 3e420be2f68..9fb6293c9a2 100644 --- a/homeassistant/components/amberelectric/coordinator.py +++ b/homeassistant/components/amberelectric/coordinator.py @@ -1,4 +1,5 @@ """Amber Electric Coordinator.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/amberelectric/sensor.py b/homeassistant/components/amberelectric/sensor.py index 547b51a0f67..aafdd730a0c 100644 --- a/homeassistant/components/amberelectric/sensor.py +++ b/homeassistant/components/amberelectric/sensor.py @@ -4,7 +4,6 @@ # Current and forecast will create general, controlled load and feed in as required # At the moment renewables in the only grid sensor. - from __future__ import annotations from typing import Any diff --git a/homeassistant/components/ambiclimate/__init__.py b/homeassistant/components/ambiclimate/__init__.py index 7ed9deec898..75691aebbf8 100644 --- a/homeassistant/components/ambiclimate/__init__.py +++ b/homeassistant/components/ambiclimate/__init__.py @@ -1,4 +1,5 @@ """Support for Ambiclimate devices.""" + import voluptuous as vol from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/ambiclimate/climate.py b/homeassistant/components/ambiclimate/climate.py index 58b2334260e..e9554b08724 100644 --- a/homeassistant/components/ambiclimate/climate.py +++ b/homeassistant/components/ambiclimate/climate.py @@ -1,4 +1,5 @@ """Support for Ambiclimate ac.""" + from __future__ import annotations import asyncio @@ -97,16 +98,16 @@ async def async_setup_entry( _LOGGER.error("No devices found") return - tasks = [] - for heater in data_connection.get_devices(): - tasks.append(asyncio.create_task(heater.update_device_info())) + tasks = [ + asyncio.create_task(heater.update_device_info()) + for heater in data_connection.get_devices() + ] await asyncio.wait(tasks) - devs = [] - for heater in data_connection.get_devices(): - devs.append(AmbiclimateEntity(heater, store)) - - async_add_entities(devs, True) + async_add_entities( + (AmbiclimateEntity(heater, store) for heater in data_connection.get_devices()), + True, + ) async def send_comfort_feedback(service: ServiceCall) -> None: """Send comfort feedback.""" diff --git a/homeassistant/components/ambiclimate/config_flow.py b/homeassistant/components/ambiclimate/config_flow.py index 383a11055e4..9d5848ea899 100644 --- a/homeassistant/components/ambiclimate/config_flow.py +++ b/homeassistant/components/ambiclimate/config_flow.py @@ -1,15 +1,15 @@ """Config flow for Ambiclimate.""" + import logging from typing import Any from aiohttp import web import ambiclimate -from homeassistant import config_entries -from homeassistant.components.http import HomeAssistantView +from homeassistant.components.http import KEY_HASS, HomeAssistantView +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.core import HomeAssistant, callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.network import get_url from homeassistant.helpers.storage import Store @@ -44,7 +44,7 @@ def register_flow_implementation( } -class AmbiclimateFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class AmbiclimateFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a config flow.""" VERSION = 1 @@ -56,7 +56,7 @@ class AmbiclimateFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle external yaml configuration.""" self._async_abort_entries_match() @@ -70,7 +70,7 @@ class AmbiclimateFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_auth( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow start.""" self._async_abort_entries_match() @@ -91,7 +91,7 @@ class AmbiclimateFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_code(self, code: str | None = None) -> FlowResult: + async def async_step_code(self, code: str | None = None) -> ConfigFlowResult: """Received code for authentication.""" self._async_abort_entries_match() @@ -151,7 +151,7 @@ class AmbiclimateAuthCallbackView(HomeAssistantView): """Receive authorization token.""" if (code := request.query.get("code")) is None: return "No code" - hass = request.app["hass"] + hass = request.app[KEY_HASS] hass.async_create_task( hass.config_entries.flow.async_init( DOMAIN, context={"source": "code"}, data=code diff --git a/homeassistant/components/ambient_station/__init__.py b/homeassistant/components/ambient_station/__init__.py index 7dd6b455e73..0984e21a722 100644 --- a/homeassistant/components/ambient_station/__init__.py +++ b/homeassistant/components/ambient_station/__init__.py @@ -1,4 +1,5 @@ """Support for Ambient Weather Station Service.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/ambient_station/binary_sensor.py b/homeassistant/components/ambient_station/binary_sensor.py index 25c95b2e20e..fc21455a00f 100644 --- a/homeassistant/components/ambient_station/binary_sensor.py +++ b/homeassistant/components/ambient_station/binary_sensor.py @@ -1,4 +1,5 @@ """Support for Ambient Weather Station binary sensors.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/homeassistant/components/ambient_station/config_flow.py b/homeassistant/components/ambient_station/config_flow.py index 2c2d231b33e..66e603ba2ff 100644 --- a/homeassistant/components/ambient_station/config_flow.py +++ b/homeassistant/components/ambient_station/config_flow.py @@ -1,19 +1,19 @@ """Config flow to configure the Ambient PWS component.""" + from __future__ import annotations from aioambient import API from aioambient.errors import AmbientError import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_API_KEY -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import aiohttp_client from .const import CONF_APP_KEY, DOMAIN -class AmbientStationFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class AmbientStationFlowHandler(ConfigFlow, domain=DOMAIN): """Handle an Ambient PWS config flow.""" VERSION = 2 @@ -24,7 +24,7 @@ class AmbientStationFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): {vol.Required(CONF_API_KEY): str, vol.Required(CONF_APP_KEY): str} ) - async def _show_form(self, errors: dict | None = None) -> FlowResult: + async def _show_form(self, errors: dict | None = None) -> ConfigFlowResult: """Show the form to the user.""" return self.async_show_form( step_id="user", @@ -32,7 +32,7 @@ class AmbientStationFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors=errors if errors else {}, ) - async def async_step_user(self, user_input: dict | None = None) -> FlowResult: + async def async_step_user(self, user_input: dict | None = None) -> ConfigFlowResult: """Handle the start of the config flow.""" if not user_input: return await self._show_form() diff --git a/homeassistant/components/ambient_station/const.py b/homeassistant/components/ambient_station/const.py index 4e0ec598fb1..eb8994d5f02 100644 --- a/homeassistant/components/ambient_station/const.py +++ b/homeassistant/components/ambient_station/const.py @@ -1,4 +1,5 @@ """Define constants for the Ambient PWS component.""" + import logging DOMAIN = "ambient_station" diff --git a/homeassistant/components/ambient_station/diagnostics.py b/homeassistant/components/ambient_station/diagnostics.py index d18047fe8e4..f3508b8df38 100644 --- a/homeassistant/components/ambient_station/diagnostics.py +++ b/homeassistant/components/ambient_station/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for Ambient PWS.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/ambient_station/entity.py b/homeassistant/components/ambient_station/entity.py index 277b69e8f68..a1a81d97c3f 100644 --- a/homeassistant/components/ambient_station/entity.py +++ b/homeassistant/components/ambient_station/entity.py @@ -1,4 +1,5 @@ """Base entity Ambient Weather Station Service.""" + from __future__ import annotations from aioambient.util import get_public_device_id diff --git a/homeassistant/components/ambient_station/sensor.py b/homeassistant/components/ambient_station/sensor.py index 951bfc5c8ff..db729197a59 100644 --- a/homeassistant/components/ambient_station/sensor.py +++ b/homeassistant/components/ambient_station/sensor.py @@ -1,4 +1,5 @@ """Support for Ambient Weather Station sensors.""" + from __future__ import annotations from datetime import datetime diff --git a/homeassistant/components/amcrest/__init__.py b/homeassistant/components/amcrest/__init__.py index ce07741c37f..cb6abff3f89 100644 --- a/homeassistant/components/amcrest/__init__.py +++ b/homeassistant/components/amcrest/__init__.py @@ -1,4 +1,5 @@ """Support for Amcrest IP cameras.""" + from __future__ import annotations import asyncio @@ -210,9 +211,10 @@ class AmcrestChecker(ApiWrapper): self, *args: Any, **kwargs: Any ) -> AsyncIterator[httpx.Response]: """amcrest.ApiWrapper.command wrapper to catch errors.""" - async with self._async_command_wrapper(), super().async_stream_command( - *args, **kwargs - ) as ret: + async with ( + self._async_command_wrapper(), + super().async_stream_command(*args, **kwargs) as ret, + ): yield ret @asynccontextmanager @@ -441,9 +443,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return entity_ids async def async_service_handler(call: ServiceCall) -> None: - args = [] - for arg in CAMERA_SERVICES[call.service][2]: - args.append(call.data[arg]) + args = [call.data[arg] for arg in CAMERA_SERVICES[call.service][2]] for entity_id in await async_extract_from_service(call): async_dispatcher_send(hass, service_signal(call.service, entity_id), *args) diff --git a/homeassistant/components/amcrest/binary_sensor.py b/homeassistant/components/amcrest/binary_sensor.py index a0b6b4f6527..ccbf5efd8f4 100644 --- a/homeassistant/components/amcrest/binary_sensor.py +++ b/homeassistant/components/amcrest/binary_sensor.py @@ -1,4 +1,5 @@ """Support for Amcrest IP camera binary sensors.""" + from __future__ import annotations from contextlib import suppress diff --git a/homeassistant/components/amcrest/camera.py b/homeassistant/components/amcrest/camera.py index 43201aba77a..1cbf5af4b70 100644 --- a/homeassistant/components/amcrest/camera.py +++ b/homeassistant/components/amcrest/camera.py @@ -1,4 +1,5 @@ """Support for Amcrest IP cameras.""" + from __future__ import annotations import asyncio @@ -12,15 +13,11 @@ from amcrest import AmcrestError from haffmpeg.camera import CameraMjpeg import voluptuous as vol -from homeassistant.components.camera import ( - DOMAIN as CAMERA_DOMAIN, - Camera, - CameraEntityFeature, -) +from homeassistant.components.camera import Camera, CameraEntityFeature from homeassistant.components.ffmpeg import FFmpegManager, get_ffmpeg_manager from homeassistant.const import ATTR_ENTITY_ID, CONF_NAME, STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant -from homeassistant.helpers import config_validation as cv, entity_registry as er +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import ( async_aiohttp_proxy_stream, async_aiohttp_proxy_web, @@ -36,7 +33,6 @@ from .const import ( COMM_TIMEOUT, DATA_AMCREST, DEVICES, - DOMAIN, RESOLUTION_TO_STREAM, SERVICE_UPDATE, SNAPSHOT_TIMEOUT, @@ -140,18 +136,6 @@ async def async_setup_platform( device = hass.data[DATA_AMCREST][DEVICES][name] entity = AmcrestCam(name, device, get_ffmpeg_manager(hass)) - # 2021.9.0 introduced unique id's for the camera entity, but these were not - # unique for different resolution streams. If any cameras were configured - # with this version, update the old entity with the new unique id. - serial_number = await device.api.async_serial_number - serial_number = serial_number.strip() - registry = er.async_get(hass) - entity_id = registry.async_get_entity_id(CAMERA_DOMAIN, DOMAIN, serial_number) - if entity_id is not None: - _LOGGER.debug("Updating unique id for camera %s", entity_id) - new_unique_id = f"{serial_number}-{device.resolution}-{device.channel}" - registry.async_update_entity(entity_id, new_unique_id=new_unique_id) - async_add_entities([entity], True) diff --git a/homeassistant/components/amcrest/const.py b/homeassistant/components/amcrest/const.py index 6c2fe431d43..377c5642b4b 100644 --- a/homeassistant/components/amcrest/const.py +++ b/homeassistant/components/amcrest/const.py @@ -1,4 +1,5 @@ """Constants for amcrest component.""" + DOMAIN = "amcrest" DATA_AMCREST = DOMAIN CAMERAS = "cameras" diff --git a/homeassistant/components/amcrest/helpers.py b/homeassistant/components/amcrest/helpers.py index 306c24a94ac..5da1ea412bf 100644 --- a/homeassistant/components/amcrest/helpers.py +++ b/homeassistant/components/amcrest/helpers.py @@ -1,4 +1,5 @@ """Helpers for amcrest component.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/amcrest/icons.json b/homeassistant/components/amcrest/icons.json new file mode 100644 index 00000000000..efba49d6b56 --- /dev/null +++ b/homeassistant/components/amcrest/icons.json @@ -0,0 +1,15 @@ +{ + "services": { + "enable_recording": "mdi:record-rec", + "disable_recording": "mdi:stop", + "enable_audio": "mdi:volume-high", + "disable_audio": "mdi:volume-off", + "enable_motion_recording": "mdi:motion-sensor", + "disable_motion_recording": "mdi:motion-sensor-off", + "goto_preset": "mdi:pan", + "set_color_bw": "mdi:palette", + "start_tour": "mdi:panorama", + "stop_tour": "mdi:panorama-outline", + "ptz_control": "mdi:pan" + } +} diff --git a/homeassistant/components/amcrest/sensor.py b/homeassistant/components/amcrest/sensor.py index 8ba274e62ee..b54d71c5814 100644 --- a/homeassistant/components/amcrest/sensor.py +++ b/homeassistant/components/amcrest/sensor.py @@ -1,4 +1,5 @@ """Support for Amcrest IP camera sensors.""" + from __future__ import annotations from datetime import timedelta @@ -107,21 +108,21 @@ class AmcrestSensor(SensorEntity): elif sensor_type == SENSOR_SDCARD: storage = await self._api.async_storage_all try: - self._attr_extra_state_attributes[ - "Total" - ] = f"{storage['total'][0]:.2f} {storage['total'][1]}" + self._attr_extra_state_attributes["Total"] = ( + f"{storage['total'][0]:.2f} {storage['total'][1]}" + ) except ValueError: - self._attr_extra_state_attributes[ - "Total" - ] = f"{storage['total'][0]} {storage['total'][1]}" + self._attr_extra_state_attributes["Total"] = ( + f"{storage['total'][0]} {storage['total'][1]}" + ) try: - self._attr_extra_state_attributes[ - "Used" - ] = f"{storage['used'][0]:.2f} {storage['used'][1]}" + self._attr_extra_state_attributes["Used"] = ( + f"{storage['used'][0]:.2f} {storage['used'][1]}" + ) except ValueError: - self._attr_extra_state_attributes[ - "Used" - ] = f"{storage['used'][0]} {storage['used'][1]}" + self._attr_extra_state_attributes["Used"] = ( + f"{storage['used'][0]} {storage['used'][1]}" + ) try: self._attr_native_value = f"{storage['used_percent']:.2f}" except ValueError: diff --git a/homeassistant/components/amcrest/services.yaml b/homeassistant/components/amcrest/services.yaml index cdcaf0e2c04..057f0842f30 100644 --- a/homeassistant/components/amcrest/services.yaml +++ b/homeassistant/components/amcrest/services.yaml @@ -3,46 +3,59 @@ enable_recording: entity_id: example: "camera.house_front" selector: - text: + entity: + integration: amcrest + domain: camera disable_recording: fields: entity_id: example: "camera.house_front" selector: - text: + entity: + integration: amcrest + domain: camera enable_audio: fields: entity_id: example: "camera.house_front" selector: - text: + entity: + integration: amcrest + domain: camera disable_audio: fields: entity_id: example: "camera.house_front" selector: - text: + entity: + integration: amcrest + domain: camera enable_motion_recording: fields: entity_id: example: "camera.house_front" selector: - text: + entity: + integration: amcrest + domain: camera disable_motion_recording: fields: entity_id: example: "camera.house_front" selector: - text: + entity: + integration: amcrest + domain: camera goto_preset: fields: entity_id: + example: "camera.house_front" selector: entity: integration: amcrest @@ -59,7 +72,9 @@ set_color_bw: entity_id: example: "camera.house_front" selector: - text: + entity: + integration: amcrest + domain: camera color_bw: selector: select: @@ -73,21 +88,27 @@ start_tour: entity_id: example: "camera.house_front" selector: - text: + entity: + integration: amcrest + domain: camera stop_tour: fields: entity_id: example: "camera.house_front" selector: - text: + entity: + integration: amcrest + domain: camera ptz_control: fields: entity_id: example: "camera.house_front" selector: - text: + entity: + integration: amcrest + domain: camera movement: required: true selector: diff --git a/homeassistant/components/amcrest/switch.py b/homeassistant/components/amcrest/switch.py index fc7347bff97..0566e26b7ed 100644 --- a/homeassistant/components/amcrest/switch.py +++ b/homeassistant/components/amcrest/switch.py @@ -1,4 +1,5 @@ """Support for Amcrest Switches.""" + from __future__ import annotations from typing import TYPE_CHECKING, Any diff --git a/homeassistant/components/ampio/air_quality.py b/homeassistant/components/ampio/air_quality.py index a423a628367..ce7bff10aa8 100644 --- a/homeassistant/components/ampio/air_quality.py +++ b/homeassistant/components/ampio/air_quality.py @@ -1,4 +1,5 @@ """Support for Ampio Air Quality data.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/ampio/const.py b/homeassistant/components/ampio/const.py index b1a13ce9414..7e2df12fcc2 100644 --- a/homeassistant/components/ampio/const.py +++ b/homeassistant/components/ampio/const.py @@ -1,4 +1,5 @@ """Constants for Ampio Air Quality platform.""" + from datetime import timedelta from typing import Final diff --git a/homeassistant/components/analytics/__init__.py b/homeassistant/components/analytics/__init__.py index ee36aa78e63..a49fe15b41f 100644 --- a/homeassistant/components/analytics/__init__.py +++ b/homeassistant/components/analytics/__init__.py @@ -1,4 +1,5 @@ """Send instance and usage analytics.""" + from typing import Any import voluptuous as vol diff --git a/homeassistant/components/analytics/analytics.py b/homeassistant/components/analytics/analytics.py index d2c0cec20eb..01c8bf22787 100644 --- a/homeassistant/components/analytics/analytics.py +++ b/homeassistant/components/analytics/analytics.py @@ -1,4 +1,5 @@ """Analytics helper class for the analytics integration.""" + from __future__ import annotations import asyncio @@ -192,7 +193,7 @@ class Analytics: system_info = await async_get_system_info(hass) integrations = [] custom_integrations = [] - addons = [] + addons: list[dict[str, Any]] = [] payload: dict = { ATTR_UUID: self.uuid, ATTR_VERSION: HA_VERSION, @@ -266,15 +267,15 @@ class Analytics: for addon in supervisor_info[ATTR_ADDONS] ) ) - for addon in installed_addons: - addons.append( - { - ATTR_SLUG: addon[ATTR_SLUG], - ATTR_PROTECTED: addon[ATTR_PROTECTED], - ATTR_VERSION: addon[ATTR_VERSION], - ATTR_AUTO_UPDATE: addon[ATTR_AUTO_UPDATE], - } - ) + addons.extend( + { + ATTR_SLUG: addon[ATTR_SLUG], + ATTR_PROTECTED: addon[ATTR_PROTECTED], + ATTR_VERSION: addon[ATTR_VERSION], + ATTR_AUTO_UPDATE: addon[ATTR_AUTO_UPDATE], + } + for addon in installed_addons + ) if self.preferences.get(ATTR_USAGE, False): payload[ATTR_CERTIFICATE] = hass.http.ssl_certificate is not None diff --git a/homeassistant/components/analytics/const.py b/homeassistant/components/analytics/const.py index fd253c32f93..6f74cc60f84 100644 --- a/homeassistant/components/analytics/const.py +++ b/homeassistant/components/analytics/const.py @@ -1,4 +1,5 @@ """Constants for the analytics integration.""" + from datetime import timedelta import logging diff --git a/homeassistant/components/analytics/manifest.json b/homeassistant/components/analytics/manifest.json index 6ab6898ec27..955c4a813f4 100644 --- a/homeassistant/components/analytics/manifest.json +++ b/homeassistant/components/analytics/manifest.json @@ -5,7 +5,6 @@ "codeowners": ["@home-assistant/core", "@ludeeus"], "dependencies": ["api", "websocket_api"], "documentation": "https://www.home-assistant.io/integrations/analytics", - "import_executor": true, "integration_type": "system", "iot_class": "cloud_push", "quality_scale": "internal" diff --git a/homeassistant/components/analytics_insights/__init__.py b/homeassistant/components/analytics_insights/__init__.py index 23965a9fcb5..65c3930e97d 100644 --- a/homeassistant/components/analytics_insights/__init__.py +++ b/homeassistant/components/analytics_insights/__init__.py @@ -1,4 +1,5 @@ """The Homeassistant Analytics integration.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/homeassistant/components/analytics_insights/config_flow.py b/homeassistant/components/analytics_insights/config_flow.py index d2ebdd943a2..30b8ca12579 100644 --- a/homeassistant/components/analytics_insights/config_flow.py +++ b/homeassistant/components/analytics_insights/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Homeassistant Analytics integration.""" + from __future__ import annotations from typing import Any @@ -13,11 +14,11 @@ import voluptuous as vol from homeassistant.config_entries import ( ConfigEntry, ConfigFlow, + ConfigFlowResult, OptionsFlow, OptionsFlowWithConfigEntry, ) from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.selector import ( SelectOptionDict, @@ -50,7 +51,7 @@ class HomeassistantAnalyticsConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" self._async_abort_entries_match() errors: dict[str, str] = {} @@ -120,7 +121,7 @@ class HomeassistantAnalyticsOptionsFlowHandler(OptionsFlowWithConfigEntry): async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Manage the options.""" errors: dict[str, str] = {} if user_input is not None: diff --git a/homeassistant/components/analytics_insights/const.py b/homeassistant/components/analytics_insights/const.py index 745c05302a1..56ea3f59794 100644 --- a/homeassistant/components/analytics_insights/const.py +++ b/homeassistant/components/analytics_insights/const.py @@ -1,4 +1,5 @@ """Constants for the Homeassistant Analytics integration.""" + import logging DOMAIN = "analytics_insights" diff --git a/homeassistant/components/analytics_insights/coordinator.py b/homeassistant/components/analytics_insights/coordinator.py index c646288cbe0..759ce567898 100644 --- a/homeassistant/components/analytics_insights/coordinator.py +++ b/homeassistant/components/analytics_insights/coordinator.py @@ -1,4 +1,5 @@ """DataUpdateCoordinator for the Homeassistant Analytics integration.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/homeassistant/components/analytics_insights/manifest.json b/homeassistant/components/analytics_insights/manifest.json index b55e08a8141..d33bb23b1b7 100644 --- a/homeassistant/components/analytics_insights/manifest.json +++ b/homeassistant/components/analytics_insights/manifest.json @@ -4,7 +4,6 @@ "codeowners": ["@joostlek"], "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/analytics_insights", - "import_executor": true, "integration_type": "service", "iot_class": "cloud_polling", "loggers": ["python_homeassistant_analytics"], diff --git a/homeassistant/components/analytics_insights/sensor.py b/homeassistant/components/analytics_insights/sensor.py index 90e9ff51b87..e776ddb9f41 100644 --- a/homeassistant/components/analytics_insights/sensor.py +++ b/homeassistant/components/analytics_insights/sensor.py @@ -1,4 +1,5 @@ """Sensor for Home Assistant analytics.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/android_ip_webcam/__init__.py b/homeassistant/components/android_ip_webcam/__init__.py index 47307fb3690..db50d6d3e1a 100644 --- a/homeassistant/components/android_ip_webcam/__init__.py +++ b/homeassistant/components/android_ip_webcam/__init__.py @@ -1,4 +1,5 @@ """The Android IP Webcam integration.""" + from __future__ import annotations from pydroid_ipcam import PyDroidIPCam diff --git a/homeassistant/components/android_ip_webcam/binary_sensor.py b/homeassistant/components/android_ip_webcam/binary_sensor.py index 6f17616a216..3ec03a59342 100644 --- a/homeassistant/components/android_ip_webcam/binary_sensor.py +++ b/homeassistant/components/android_ip_webcam/binary_sensor.py @@ -1,4 +1,5 @@ """Support for Android IP Webcam binary sensors.""" + from __future__ import annotations from homeassistant.components.binary_sensor import ( @@ -51,7 +52,7 @@ class IPWebcamBinarySensor(AndroidIPCamBaseEntity, BinarySensorEntity): @property def available(self) -> bool: - """Return avaibility if setting is enabled.""" + """Return availability if setting is enabled.""" return MOTION_ACTIVE in self.cam.enabled_sensors and super().available @property diff --git a/homeassistant/components/android_ip_webcam/camera.py b/homeassistant/components/android_ip_webcam/camera.py index a12798a5b91..2149e40b6e1 100644 --- a/homeassistant/components/android_ip_webcam/camera.py +++ b/homeassistant/components/android_ip_webcam/camera.py @@ -1,4 +1,5 @@ """Support for Android IP Webcam Cameras.""" + from __future__ import annotations from homeassistant.components.mjpeg import MjpegCamera, filter_urllib3_logging diff --git a/homeassistant/components/android_ip_webcam/config_flow.py b/homeassistant/components/android_ip_webcam/config_flow.py index 2a26292fdd7..70870debfb1 100644 --- a/homeassistant/components/android_ip_webcam/config_flow.py +++ b/homeassistant/components/android_ip_webcam/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Android IP Webcam integration.""" + from __future__ import annotations from typing import Any @@ -7,10 +8,9 @@ from pydroid_ipcam import PyDroidIPCam from pydroid_ipcam.exceptions import PyDroidIPCamException, Unauthorized import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -50,14 +50,14 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, return errors -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class AndroidIPWebcamConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Android IP Webcam.""" VERSION = 1 async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" if user_input is None: return self.async_show_form( diff --git a/homeassistant/components/android_ip_webcam/sensor.py b/homeassistant/components/android_ip_webcam/sensor.py index e55112b7259..7ccb0661a6c 100644 --- a/homeassistant/components/android_ip_webcam/sensor.py +++ b/homeassistant/components/android_ip_webcam/sensor.py @@ -1,4 +1,5 @@ """Support for Android IP Webcam sensors.""" + from __future__ import annotations from collections.abc import Callable @@ -23,19 +24,11 @@ from .coordinator import AndroidIPCamDataUpdateCoordinator from .entity import AndroidIPCamBaseEntity -@dataclass(frozen=True) -class AndroidIPWebcamSensorEntityDescriptionMixin: - """Mixin for required keys.""" - - value_fn: Callable[[PyDroidIPCam], StateType] - - -@dataclass(frozen=True) -class AndroidIPWebcamSensorEntityDescription( - SensorEntityDescription, AndroidIPWebcamSensorEntityDescriptionMixin -): +@dataclass(frozen=True, kw_only=True) +class AndroidIPWebcamSensorEntityDescription(SensorEntityDescription): """Entity description class for Android IP Webcam sensors.""" + value_fn: Callable[[PyDroidIPCam], StateType] unit_fn: Callable[[PyDroidIPCam], str | None] = lambda _: None @@ -139,7 +132,7 @@ async def async_setup_entry( sensor for sensor in SENSOR_TYPES if sensor.key - in coordinator.cam.enabled_sensors + ["audio_connections", "video_connections"] + in [*coordinator.cam.enabled_sensors, "audio_connections", "video_connections"] ] async_add_entities( IPWebcamSensor(coordinator, description) for description in sensor_types diff --git a/homeassistant/components/android_ip_webcam/switch.py b/homeassistant/components/android_ip_webcam/switch.py index d2a40cb619a..038c3330d82 100644 --- a/homeassistant/components/android_ip_webcam/switch.py +++ b/homeassistant/components/android_ip_webcam/switch.py @@ -1,4 +1,5 @@ """Support for Android IP Webcam settings.""" + from __future__ import annotations from collections.abc import Callable, Coroutine @@ -18,21 +19,14 @@ from .coordinator import AndroidIPCamDataUpdateCoordinator from .entity import AndroidIPCamBaseEntity -@dataclass(frozen=True) -class AndroidIPWebcamSwitchEntityDescriptionMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class AndroidIPWebcamSwitchEntityDescription(SwitchEntityDescription): + """Entity description class for Android IP Webcam switches.""" on_func: Callable[[PyDroidIPCam], Coroutine[Any, Any, bool]] off_func: Callable[[PyDroidIPCam], Coroutine[Any, Any, bool]] -@dataclass(frozen=True) -class AndroidIPWebcamSwitchEntityDescription( - SwitchEntityDescription, AndroidIPWebcamSwitchEntityDescriptionMixin -): - """Entity description class for Android IP Webcam switches.""" - - SWITCH_TYPES: tuple[AndroidIPWebcamSwitchEntityDescription, ...] = ( AndroidIPWebcamSwitchEntityDescription( key="exposure_lock", diff --git a/homeassistant/components/androidtv/__init__.py b/homeassistant/components/androidtv/__init__.py index cd9e42aeb4d..884a06bca68 100644 --- a/homeassistant/components/androidtv/__init__.py +++ b/homeassistant/components/androidtv/__init__.py @@ -1,4 +1,5 @@ """Support for functionality to interact with Android/Fire TV devices.""" + from __future__ import annotations from collections.abc import Mapping diff --git a/homeassistant/components/androidtv/config_flow.py b/homeassistant/components/androidtv/config_flow.py index e688b0a92de..20396b20bb9 100644 --- a/homeassistant/components/androidtv/config_flow.py +++ b/homeassistant/components/androidtv/config_flow.py @@ -1,4 +1,5 @@ """Config flow to configure the Android Debug Bridge integration.""" + from __future__ import annotations import logging @@ -11,11 +12,11 @@ import voluptuous as vol from homeassistant.config_entries import ( ConfigEntry, ConfigFlow, + ConfigFlowResult, OptionsFlowWithConfigEntry, ) from homeassistant.const import CONF_DEVICE_CLASS, CONF_HOST, CONF_PORT from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_validation as cv from homeassistant.helpers.selector import ( ObjectSelector, @@ -81,7 +82,7 @@ class AndroidTVFlowHandler(ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None, error: str | None = None, - ) -> FlowResult: + ) -> ConfigFlowResult: """Show the setup form to the user.""" host = user_input.get(CONF_HOST, "") if user_input else "" data_schema = vol.Schema( @@ -144,7 +145,7 @@ class AndroidTVFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initiated by the user.""" error = None @@ -199,7 +200,7 @@ class OptionsFlowHandler(OptionsFlowWithConfigEntry): self._conf_rule_id: str | None = None @callback - def _save_config(self, data: dict[str, Any]) -> FlowResult: + def _save_config(self, data: dict[str, Any]) -> ConfigFlowResult: """Save the updated options.""" new_data = { k: v @@ -215,7 +216,7 @@ class OptionsFlowHandler(OptionsFlowWithConfigEntry): async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle options flow.""" if user_input is not None: if sel_app := user_input.get(CONF_APPS): @@ -227,14 +228,14 @@ class OptionsFlowHandler(OptionsFlowWithConfigEntry): return self._async_init_form() @callback - def _async_init_form(self) -> FlowResult: + def _async_init_form(self) -> ConfigFlowResult: """Return initial configuration form.""" apps_list = {k: f"{v} ({k})" if v else k for k, v in self._apps.items()} apps = [SelectOptionDict(value=APPS_NEW_ID, label="Add new")] + [ SelectOptionDict(value=k, label=v) for k, v in apps_list.items() ] - rules = [RULES_NEW_ID] + list(self._state_det_rules) + rules = [RULES_NEW_ID, *self._state_det_rules] options = self.options data_schema = vol.Schema( @@ -280,7 +281,7 @@ class OptionsFlowHandler(OptionsFlowWithConfigEntry): async def async_step_apps( self, user_input: dict[str, Any] | None = None, app_id: str | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle options flow for apps list.""" if app_id is not None: self._conf_app_id = app_id if app_id != APPS_NEW_ID else None @@ -297,7 +298,7 @@ class OptionsFlowHandler(OptionsFlowWithConfigEntry): return await self.async_step_init() @callback - def _async_apps_form(self, app_id: str) -> FlowResult: + def _async_apps_form(self, app_id: str) -> ConfigFlowResult: """Return configuration form for apps.""" app_schema = { vol.Optional( @@ -322,7 +323,7 @@ class OptionsFlowHandler(OptionsFlowWithConfigEntry): async def async_step_rules( self, user_input: dict[str, Any] | None = None, rule_id: str | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle options flow for detection rules.""" if rule_id is not None: self._conf_rule_id = rule_id if rule_id != RULES_NEW_ID else None @@ -348,7 +349,7 @@ class OptionsFlowHandler(OptionsFlowWithConfigEntry): @callback def _async_rules_form( self, rule_id: str, default_id: str = "", errors: dict[str, str] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Return configuration form for detection rules.""" rule_schema = { vol.Optional( diff --git a/homeassistant/components/androidtv/const.py b/homeassistant/components/androidtv/const.py index 17936421680..fb43e0af090 100644 --- a/homeassistant/components/androidtv/const.py +++ b/homeassistant/components/androidtv/const.py @@ -1,4 +1,5 @@ """Android Debug Bridge component constants.""" + DOMAIN = "androidtv" ANDROID_DEV = DOMAIN diff --git a/homeassistant/components/androidtv/diagnostics.py b/homeassistant/components/androidtv/diagnostics.py index 0921fecc500..5dba4109f32 100644 --- a/homeassistant/components/androidtv/diagnostics.py +++ b/homeassistant/components/androidtv/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for AndroidTV.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/androidtv/entity.py b/homeassistant/components/androidtv/entity.py index e9cbd435d9b..2185f6d151a 100644 --- a/homeassistant/components/androidtv/entity.py +++ b/homeassistant/components/androidtv/entity.py @@ -1,4 +1,5 @@ """Base AndroidTV Entity.""" + from __future__ import annotations from collections.abc import Awaitable, Callable, Coroutine diff --git a/homeassistant/components/androidtv/media_player.py b/homeassistant/components/androidtv/media_player.py index 5e97396b369..016a7a5a7a2 100644 --- a/homeassistant/components/androidtv/media_player.py +++ b/homeassistant/components/androidtv/media_player.py @@ -1,4 +1,5 @@ """Support for functionality to interact with Android / Fire TV devices.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/androidtv_remote/__init__.py b/homeassistant/components/androidtv_remote/__init__.py index 9e99a93efa6..c64fc273a2a 100644 --- a/homeassistant/components/androidtv_remote/__init__.py +++ b/homeassistant/components/androidtv_remote/__init__.py @@ -1,4 +1,5 @@ """The Android TV Remote integration.""" + from __future__ import annotations from asyncio import timeout diff --git a/homeassistant/components/androidtv_remote/config_flow.py b/homeassistant/components/androidtv_remote/config_flow.py index 03e09c6ecb0..2fd9f607218 100644 --- a/homeassistant/components/androidtv_remote/config_flow.py +++ b/homeassistant/components/androidtv_remote/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Android TV Remote integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -16,11 +17,11 @@ from homeassistant.components import zeroconf from homeassistant.config_entries import ( ConfigEntry, ConfigFlow, + ConfigFlowResult, OptionsFlowWithConfigEntry, ) from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.device_registry import format_mac from .const import CONF_ENABLE_IME, DOMAIN @@ -54,7 +55,7 @@ class AndroidTVRemoteConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors: dict[str, str] = {} if user_input is not None: @@ -78,7 +79,7 @@ class AndroidTVRemoteConfigFlow(ConfigFlow, domain=DOMAIN): errors=errors, ) - async def _async_start_pair(self) -> FlowResult: + async def _async_start_pair(self) -> ConfigFlowResult: """Start pairing with the Android TV. Navigate to the pair flow to enter the PIN shown on screen.""" assert self.host self.api = create_api(self.hass, self.host, enable_ime=False) @@ -88,7 +89,7 @@ class AndroidTVRemoteConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_pair( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the pair step.""" errors: dict[str, str] = {} if user_input is not None: @@ -136,7 +137,7 @@ class AndroidTVRemoteConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_zeroconf( self, discovery_info: zeroconf.ZeroconfServiceInfo - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle zeroconf discovery.""" self.host = discovery_info.host self.name = discovery_info.name.removesuffix("._androidtvremote2._tcp.local.") @@ -152,7 +153,7 @@ class AndroidTVRemoteConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_zeroconf_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initiated by zeroconf.""" if user_input is not None: try: @@ -166,7 +167,9 @@ class AndroidTVRemoteConfigFlow(ConfigFlow, domain=DOMAIN): description_placeholders={CONF_NAME: self.name}, ) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Handle configuration by re-auth.""" self.host = entry_data[CONF_HOST] self.name = entry_data[CONF_NAME] @@ -178,7 +181,7 @@ class AndroidTVRemoteConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Dialog that informs the user that reauth is required.""" errors: dict[str, str] = {} if user_input is not None: @@ -207,7 +210,7 @@ class AndroidTVRemoteOptionsFlowHandler(OptionsFlowWithConfigEntry): async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Manage the options.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) diff --git a/homeassistant/components/androidtv_remote/const.py b/homeassistant/components/androidtv_remote/const.py index 44d7098adc1..9d2a7fcb240 100644 --- a/homeassistant/components/androidtv_remote/const.py +++ b/homeassistant/components/androidtv_remote/const.py @@ -1,4 +1,5 @@ """Constants for the Android TV Remote integration.""" + from __future__ import annotations from typing import Final diff --git a/homeassistant/components/androidtv_remote/diagnostics.py b/homeassistant/components/androidtv_remote/diagnostics.py index 28d16bf94fe..757b3bd4e83 100644 --- a/homeassistant/components/androidtv_remote/diagnostics.py +++ b/homeassistant/components/androidtv_remote/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for Android TV Remote.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/androidtv_remote/entity.py b/homeassistant/components/androidtv_remote/entity.py index 86c8d16260c..fa070e1ec18 100644 --- a/homeassistant/components/androidtv_remote/entity.py +++ b/homeassistant/components/androidtv_remote/entity.py @@ -1,4 +1,5 @@ """Base entity for Android TV Remote.""" + from __future__ import annotations from androidtvremote2 import AndroidTVRemote, ConnectionClosed diff --git a/homeassistant/components/androidtv_remote/helpers.py b/homeassistant/components/androidtv_remote/helpers.py index 41b056269f2..cdd67b029fc 100644 --- a/homeassistant/components/androidtv_remote/helpers.py +++ b/homeassistant/components/androidtv_remote/helpers.py @@ -1,4 +1,5 @@ """Helper functions for Android TV Remote integration.""" + from __future__ import annotations from androidtvremote2 import AndroidTVRemote diff --git a/homeassistant/components/androidtv_remote/manifest.json b/homeassistant/components/androidtv_remote/manifest.json index 02197a61681..f45dee34afe 100644 --- a/homeassistant/components/androidtv_remote/manifest.json +++ b/homeassistant/components/androidtv_remote/manifest.json @@ -4,7 +4,6 @@ "codeowners": ["@tronikos", "@Drafteed"], "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/androidtv_remote", - "import_executor": true, "integration_type": "device", "iot_class": "local_push", "loggers": ["androidtvremote2"], diff --git a/homeassistant/components/androidtv_remote/media_player.py b/homeassistant/components/androidtv_remote/media_player.py index eccfc8ce25b..997f3fb040a 100644 --- a/homeassistant/components/androidtv_remote/media_player.py +++ b/homeassistant/components/androidtv_remote/media_player.py @@ -1,4 +1,5 @@ """Media player support for Android TV Remote.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/androidtv_remote/remote.py b/homeassistant/components/androidtv_remote/remote.py index f4c2ae51ce1..3dc5534e54f 100644 --- a/homeassistant/components/androidtv_remote/remote.py +++ b/homeassistant/components/androidtv_remote/remote.py @@ -1,4 +1,5 @@ """Remote control support for Android TV Remote.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/anel_pwrctrl/switch.py b/homeassistant/components/anel_pwrctrl/switch.py index 827fc0037a7..94cd0a59398 100644 --- a/homeassistant/components/anel_pwrctrl/switch.py +++ b/homeassistant/components/anel_pwrctrl/switch.py @@ -1,4 +1,5 @@ """Support for ANEL PwrCtrl switches.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/anova/__init__.py b/homeassistant/components/anova/__init__.py index 6181d02025d..9b0f649dad9 100644 --- a/homeassistant/components/anova/__init__.py +++ b/homeassistant/components/anova/__init__.py @@ -1,4 +1,5 @@ """The Anova integration.""" + from __future__ import annotations import logging @@ -55,7 +56,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry, data={ **entry.data, - **{CONF_DEVICES: serialize_device_list(devices)}, + CONF_DEVICES: serialize_device_list(devices), }, ) coordinators = [AnovaCoordinator(hass, device) for device in devices] diff --git a/homeassistant/components/anova/config_flow.py b/homeassistant/components/anova/config_flow.py index d0846fbffc7..08a3d4e832f 100644 --- a/homeassistant/components/anova/config_flow.py +++ b/homeassistant/components/anova/config_flow.py @@ -1,12 +1,12 @@ """Config flow for Anova.""" + from __future__ import annotations from anova_wifi import AnovaApi, InvalidLogin, NoDevicesFound import voluptuous as vol -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_DEVICES, CONF_PASSWORD, CONF_USERNAME -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN @@ -20,7 +20,7 @@ class AnovaConfligFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, str] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initiated by the user.""" errors: dict[str, str] = {} if user_input is not None: diff --git a/homeassistant/components/anova/coordinator.py b/homeassistant/components/anova/coordinator.py index 83dc2c295c3..c0261c139c1 100644 --- a/homeassistant/components/anova/coordinator.py +++ b/homeassistant/components/anova/coordinator.py @@ -1,4 +1,5 @@ """Support for Anova Coordinators.""" + from asyncio import timeout from datetime import timedelta import logging diff --git a/homeassistant/components/anova/entity.py b/homeassistant/components/anova/entity.py index d3ed2eb2667..a8e3ce0ae70 100644 --- a/homeassistant/components/anova/entity.py +++ b/homeassistant/components/anova/entity.py @@ -1,4 +1,5 @@ """Base entity for the Anova integration.""" + from __future__ import annotations from homeassistant.helpers.entity import Entity, EntityDescription diff --git a/homeassistant/components/anova/models.py b/homeassistant/components/anova/models.py index a63355b2bbd..4a6338eb081 100644 --- a/homeassistant/components/anova/models.py +++ b/homeassistant/components/anova/models.py @@ -1,4 +1,5 @@ """Dataclass models for the Anova integration.""" + from dataclasses import dataclass from anova_wifi import AnovaPrecisionCooker diff --git a/homeassistant/components/anova/sensor.py b/homeassistant/components/anova/sensor.py index 24bda4dbed6..7e94f8f4b0b 100644 --- a/homeassistant/components/anova/sensor.py +++ b/homeassistant/components/anova/sensor.py @@ -1,4 +1,5 @@ """Support for Anova Sensors.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/anthemav/__init__.py b/homeassistant/components/anthemav/__init__.py index 0a7e36d8a95..4efeb9245c8 100644 --- a/homeassistant/components/anthemav/__init__.py +++ b/homeassistant/components/anthemav/__init__.py @@ -1,4 +1,5 @@ """The Anthem A/V Receivers integration.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/anthemav/config_flow.py b/homeassistant/components/anthemav/config_flow.py index 892c40cde0e..400ac6d5899 100644 --- a/homeassistant/components/anthemav/config_flow.py +++ b/homeassistant/components/anthemav/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Anthem A/V Receivers integration.""" + from __future__ import annotations import logging @@ -9,9 +10,8 @@ from anthemav.connection import Connection from anthemav.device_error import DeviceError import voluptuous as vol -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_MAC, CONF_MODEL, CONF_PORT -from homeassistant.data_entry_flow import FlowResult import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import format_mac @@ -44,7 +44,7 @@ class AnthemAVConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" if user_input is None: return self.async_show_form( diff --git a/homeassistant/components/anthemav/const.py b/homeassistant/components/anthemav/const.py index 7cf586fb05d..8bcdd013a63 100644 --- a/homeassistant/components/anthemav/const.py +++ b/homeassistant/components/anthemav/const.py @@ -1,4 +1,5 @@ """Constants for the Anthem A/V Receivers integration.""" + ANTHEMAV_UPDATE_SIGNAL = "anthemav_update" DEFAULT_NAME = "Anthem AV" diff --git a/homeassistant/components/anthemav/media_player.py b/homeassistant/components/anthemav/media_player.py index a4982b2e9e8..1dbfdf275f2 100644 --- a/homeassistant/components/anthemav/media_player.py +++ b/homeassistant/components/anthemav/media_player.py @@ -1,4 +1,5 @@ """Support for Anthem Network Receivers and Processors.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/aosmith/__init__.py b/homeassistant/components/aosmith/__init__.py index 4da390685ab..c42096cd3a7 100644 --- a/homeassistant/components/aosmith/__init__.py +++ b/homeassistant/components/aosmith/__init__.py @@ -1,4 +1,5 @@ """The A. O. Smith integration.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/homeassistant/components/aosmith/config_flow.py b/homeassistant/components/aosmith/config_flow.py index 899b7382359..ec38460116d 100644 --- a/homeassistant/components/aosmith/config_flow.py +++ b/homeassistant/components/aosmith/config_flow.py @@ -1,4 +1,5 @@ """Config flow for A. O. Smith integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -8,9 +9,8 @@ from typing import Any from py_aosmith import AOSmithAPIClient, AOSmithInvalidCredentialsException import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_EMAIL, CONF_PASSWORD -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import aiohttp_client from .const import DOMAIN @@ -18,7 +18,7 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class AOSmithConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for A. O. Smith.""" VERSION = 1 @@ -44,7 +44,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors: dict[str, str] = {} if user_input is not None: @@ -73,14 +73,16 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Perform reauth if the user credentials have changed.""" self._reauth_email = entry_data[CONF_EMAIL] return await self.async_step_reauth_confirm() async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle user's reauth credentials.""" errors: dict[str, str] = {} if user_input is not None and self._reauth_email is not None: diff --git a/homeassistant/components/aosmith/coordinator.py b/homeassistant/components/aosmith/coordinator.py index a0dd703b800..3bf97e49cae 100644 --- a/homeassistant/components/aosmith/coordinator.py +++ b/homeassistant/components/aosmith/coordinator.py @@ -1,4 +1,5 @@ """The data update coordinator for the A. O. Smith integration.""" + import logging from py_aosmith import ( diff --git a/homeassistant/components/aosmith/diagnostics.py b/homeassistant/components/aosmith/diagnostics.py index a821c980faa..96b049b904f 100644 --- a/homeassistant/components/aosmith/diagnostics.py +++ b/homeassistant/components/aosmith/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for A. O. Smith.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/aosmith/entity.py b/homeassistant/components/aosmith/entity.py index 7407fbac3cb..d35b8b36410 100644 --- a/homeassistant/components/aosmith/entity.py +++ b/homeassistant/components/aosmith/entity.py @@ -1,4 +1,5 @@ """The base entity for the A. O. Smith integration.""" + from typing import TypeVar from py_aosmith import AOSmithAPIClient diff --git a/homeassistant/components/apache_kafka/__init__.py b/homeassistant/components/apache_kafka/__init__.py index c49d2954424..fb29c0d5e49 100644 --- a/homeassistant/components/apache_kafka/__init__.py +++ b/homeassistant/components/apache_kafka/__init__.py @@ -1,4 +1,5 @@ """Support for Apache Kafka.""" + from __future__ import annotations from datetime import datetime @@ -22,7 +23,7 @@ from homeassistant.core import Event, HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entityfilter import FILTER_SCHEMA, EntityFilter from homeassistant.helpers.event import EventStateChangedData -from homeassistant.helpers.typing import ConfigType, EventType +from homeassistant.helpers.typing import ConfigType from homeassistant.util import ssl as ssl_util DOMAIN = "apache_kafka" @@ -116,7 +117,7 @@ class KafkaManager: ) self._topic = topic - def _encode_event(self, event: EventType[EventStateChangedData]) -> bytes | None: + def _encode_event(self, event: Event[EventStateChangedData]) -> bytes | None: """Translate events into a binary JSON payload.""" state = event.data["new_state"] if ( @@ -132,14 +133,14 @@ class KafkaManager: async def start(self) -> None: """Start the Kafka manager.""" - self._hass.bus.async_listen(EVENT_STATE_CHANGED, self.write) # type: ignore[arg-type] + self._hass.bus.async_listen(EVENT_STATE_CHANGED, self.write) await self._producer.start() async def shutdown(self, _: Event) -> None: """Shut the manager down.""" await self._producer.stop() - async def write(self, event: EventType[EventStateChangedData]) -> None: + async def write(self, event: Event[EventStateChangedData]) -> None: """Write a binary payload to Kafka.""" payload = self._encode_event(event) diff --git a/homeassistant/components/apcupsd/__init__.py b/homeassistant/components/apcupsd/__init__.py index 550e1014d2a..73ed721158d 100644 --- a/homeassistant/components/apcupsd/__init__.py +++ b/homeassistant/components/apcupsd/__init__.py @@ -1,4 +1,5 @@ """Support for APCUPSd via its Network Information Server (NIS).""" + from __future__ import annotations import logging diff --git a/homeassistant/components/apcupsd/binary_sensor.py b/homeassistant/components/apcupsd/binary_sensor.py index d200df743f1..bc214e56d80 100644 --- a/homeassistant/components/apcupsd/binary_sensor.py +++ b/homeassistant/components/apcupsd/binary_sensor.py @@ -1,4 +1,5 @@ """Support for tracking the online status of a UPS.""" + from __future__ import annotations import logging @@ -13,12 +14,12 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import DOMAIN, APCUPSdCoordinator +from .const import DOMAIN +from .coordinator import APCUPSdCoordinator _LOGGER = logging.getLogger(__name__) _DESCRIPTION = BinarySensorEntityDescription( key="statflag", - name="UPS Online Status", translation_key="online_status", ) # The bit in STATFLAG that indicates the online status of the APC UPS. @@ -44,6 +45,8 @@ async def async_setup_entry( class OnlineStatus(CoordinatorEntity[APCUPSdCoordinator], BinarySensorEntity): """Representation of a UPS online status.""" + _attr_has_entity_name = True + def __init__( self, coordinator: APCUPSdCoordinator, @@ -53,7 +56,7 @@ class OnlineStatus(CoordinatorEntity[APCUPSdCoordinator], BinarySensorEntity): super().__init__(coordinator, context=description.key.upper()) # Set up unique id and device info if serial number is available. - if (serial_no := coordinator.ups_serial_no) is not None: + if (serial_no := coordinator.data.serial_no) is not None: self._attr_unique_id = f"{serial_no}_{description.key}" self.entity_description = description self._attr_device_info = coordinator.device_info diff --git a/homeassistant/components/apcupsd/config_flow.py b/homeassistant/components/apcupsd/config_flow.py index 25a1ccf7e02..00f757a1fd7 100644 --- a/homeassistant/components/apcupsd/config_flow.py +++ b/homeassistant/components/apcupsd/config_flow.py @@ -1,18 +1,20 @@ """Config flow for APCUPSd integration.""" + from __future__ import annotations +import asyncio from typing import Any +import aioapcaccess import voluptuous as vol -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_PORT -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import selector import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.update_coordinator import UpdateFailed -from . import DOMAIN, APCUPSdCoordinator +from .const import CONNECTION_TIMEOUT, DOMAIN +from .coordinator import APCUPSdData _PORT_SELECTOR = vol.All( selector.NumberSelector( @@ -38,7 +40,7 @@ class ConfigFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" if user_input is None: @@ -50,32 +52,19 @@ class ConfigFlowHandler(ConfigFlow, domain=DOMAIN): self._async_abort_entries_match({CONF_HOST: host, CONF_PORT: port}) # Test the connection to the host and get the current status for serial number. - coordinator = APCUPSdCoordinator(self.hass, host, port) - await coordinator.async_request_refresh() - - if isinstance(coordinator.last_exception, (UpdateFailed, TimeoutError)): + try: + async with asyncio.timeout(CONNECTION_TIMEOUT): + data = APCUPSdData(await aioapcaccess.request_status(host, port)) + except (OSError, asyncio.IncompleteReadError, TimeoutError): errors = {"base": "cannot_connect"} return self.async_show_form( step_id="user", data_schema=_SCHEMA, errors=errors ) - if not coordinator.data: - return self.async_abort(reason="no_status") - # We _try_ to use the serial number of the UPS as the unique id since this field # is not guaranteed to exist on all APC UPS models. - await self.async_set_unique_id(coordinator.ups_serial_no) + await self.async_set_unique_id(data.serial_no) self._abort_if_unique_id_configured() - title = "APC UPS" - if coordinator.ups_name is not None: - title = coordinator.ups_name - elif coordinator.ups_model is not None: - title = coordinator.ups_model - elif coordinator.ups_serial_no is not None: - title = coordinator.ups_serial_no - - return self.async_create_entry( - title=title, - data=user_input, - ) + title = data.name or data.model or data.serial_no or "APC UPS" + return self.async_create_entry(title=title, data=user_input) diff --git a/homeassistant/components/apcupsd/const.py b/homeassistant/components/apcupsd/const.py index cacc9e29369..e24a66fdca1 100644 --- a/homeassistant/components/apcupsd/const.py +++ b/homeassistant/components/apcupsd/const.py @@ -1,4 +1,6 @@ """Constants for APCUPSd component.""" + from typing import Final DOMAIN: Final = "apcupsd" +CONNECTION_TIMEOUT: int = 10 diff --git a/homeassistant/components/apcupsd/coordinator.py b/homeassistant/components/apcupsd/coordinator.py index 98d464ec526..768e9605967 100644 --- a/homeassistant/components/apcupsd/coordinator.py +++ b/homeassistant/components/apcupsd/coordinator.py @@ -1,8 +1,8 @@ """Support for APCUPSd via its Network Information Server (NIS).""" + from __future__ import annotations import asyncio -from collections import OrderedDict from datetime import timedelta import logging from typing import Final @@ -19,14 +19,35 @@ from homeassistant.helpers.update_coordinator import ( UpdateFailed, ) -from .const import DOMAIN +from .const import CONNECTION_TIMEOUT, DOMAIN _LOGGER = logging.getLogger(__name__) UPDATE_INTERVAL: Final = timedelta(seconds=60) REQUEST_REFRESH_COOLDOWN: Final = 5 -class APCUPSdCoordinator(DataUpdateCoordinator[OrderedDict[str, str]]): +class APCUPSdData(dict[str, str]): + """Store data about an APCUPSd and provide a few helper methods for easier accesses.""" + + @property + def name(self) -> str | None: + """Return the name of the UPS, if available.""" + return self.get("UPSNAME") + + @property + def model(self) -> str | None: + """Return the model of the UPS, if available.""" + # Different UPS models may report slightly different keys for model, here we + # try them all. + return self.get("APCMODEL") or self.get("MODEL") + + @property + def serial_no(self) -> str | None: + """Return the unique serial number of the UPS, if available.""" + return self.get("SERIALNO") + + +class APCUPSdCoordinator(DataUpdateCoordinator[APCUPSdData]): """Store and coordinate the data retrieved from APCUPSd for all sensors. For each entity to use, acts as the single point responsible for fetching @@ -52,46 +73,27 @@ class APCUPSdCoordinator(DataUpdateCoordinator[OrderedDict[str, str]]): self._host = host self._port = port - @property - def ups_name(self) -> str | None: - """Return the name of the UPS, if available.""" - return self.data.get("UPSNAME") - - @property - def ups_model(self) -> str | None: - """Return the model of the UPS, if available.""" - # Different UPS models may report slightly different keys for model, here we - # try them all. - for model_key in ("APCMODEL", "MODEL"): - if model_key in self.data: - return self.data[model_key] - return None - - @property - def ups_serial_no(self) -> str | None: - """Return the unique serial number of the UPS, if available.""" - return self.data.get("SERIALNO") - @property def device_info(self) -> DeviceInfo: """Return the DeviceInfo of this APC UPS, if serial number is available.""" return DeviceInfo( - identifiers={(DOMAIN, self.ups_serial_no or self.config_entry.entry_id)}, - model=self.ups_model, + identifiers={(DOMAIN, self.data.serial_no or self.config_entry.entry_id)}, + model=self.data.model, manufacturer="APC", - name=self.ups_name if self.ups_name else "APC UPS", + name=self.data.name or "APC UPS", hw_version=self.data.get("FIRMWARE"), sw_version=self.data.get("VERSION"), ) - async def _async_update_data(self) -> OrderedDict[str, str]: + async def _async_update_data(self) -> APCUPSdData: """Fetch the latest status from APCUPSd. Note that the result dict uses upper case for each resource, where our integration uses lower cases as keys internally. """ - async with asyncio.timeout(10): + async with asyncio.timeout(CONNECTION_TIMEOUT): try: - return await aioapcaccess.request_status(self._host, self._port) + data = await aioapcaccess.request_status(self._host, self._port) + return APCUPSdData(data) except (OSError, asyncio.IncompleteReadError) as error: raise UpdateFailed(error) from error diff --git a/homeassistant/components/apcupsd/diagnostics.py b/homeassistant/components/apcupsd/diagnostics.py new file mode 100644 index 00000000000..d375a8bc248 --- /dev/null +++ b/homeassistant/components/apcupsd/diagnostics.py @@ -0,0 +1,23 @@ +"""Diagnostics support for APCUPSD.""" + +from __future__ import annotations + +from typing import Any + +from homeassistant.components.diagnostics import async_redact_data +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from .const import DOMAIN +from .coordinator import APCUPSdCoordinator, APCUPSdData + +TO_REDACT = {"SERIALNO", "HOSTNAME"} + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + coordinator: APCUPSdCoordinator = hass.data[DOMAIN][entry.entry_id] + data: APCUPSdData = coordinator.data + return async_redact_data(data, TO_REDACT) diff --git a/homeassistant/components/apcupsd/sensor.py b/homeassistant/components/apcupsd/sensor.py index 4a2261f0b30..008171cfe3c 100644 --- a/homeassistant/components/apcupsd/sensor.py +++ b/homeassistant/components/apcupsd/sensor.py @@ -1,4 +1,5 @@ """Support for APCUPSd sensors.""" + from __future__ import annotations import logging @@ -24,7 +25,8 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import DOMAIN, APCUPSdCoordinator +from .const import DOMAIN +from .coordinator import APCUPSdCoordinator _LOGGER = logging.getLogger(__name__) @@ -32,11 +34,10 @@ SENSORS: dict[str, SensorEntityDescription] = { "alarmdel": SensorEntityDescription( key="alarmdel", translation_key="alarm_delay", - name="UPS Alarm Delay", ), "ambtemp": SensorEntityDescription( key="ambtemp", - name="UPS Ambient Temperature", + translation_key="ambient_temperature", native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, @@ -44,40 +45,34 @@ SENSORS: dict[str, SensorEntityDescription] = { "apc": SensorEntityDescription( key="apc", translation_key="apc_status", - name="UPS Status Data", entity_registry_enabled_default=False, ), "apcmodel": SensorEntityDescription( key="apcmodel", translation_key="apc_model", - name="UPS Model", entity_registry_enabled_default=False, ), "badbatts": SensorEntityDescription( key="badbatts", translation_key="bad_batteries", - name="UPS Bad Batteries", ), "battdate": SensorEntityDescription( key="battdate", translation_key="battery_replacement_date", - name="UPS Battery Replaced", ), "battstat": SensorEntityDescription( key="battstat", translation_key="battery_status", - name="UPS Battery Status", ), "battv": SensorEntityDescription( key="battv", - name="UPS Battery Voltage", + translation_key="battery_voltage", native_unit_of_measurement=UnitOfElectricPotential.VOLT, device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, ), "bcharge": SensorEntityDescription( key="bcharge", - name="UPS Battery", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.BATTERY, state_class=SensorStateClass.MEASUREMENT, @@ -85,86 +80,74 @@ SENSORS: dict[str, SensorEntityDescription] = { "cable": SensorEntityDescription( key="cable", translation_key="cable_type", - name="UPS Cable Type", entity_registry_enabled_default=False, ), "cumonbatt": SensorEntityDescription( key="cumonbatt", translation_key="total_time_on_battery", - name="UPS Total Time on Battery", state_class=SensorStateClass.TOTAL_INCREASING, ), "date": SensorEntityDescription( key="date", translation_key="date", - name="UPS Status Date", entity_registry_enabled_default=False, ), "dipsw": SensorEntityDescription( key="dipsw", translation_key="dip_switch_settings", - name="UPS Dip Switch Settings", ), "dlowbatt": SensorEntityDescription( key="dlowbatt", translation_key="low_battery_signal", - name="UPS Low Battery Signal", ), "driver": SensorEntityDescription( key="driver", translation_key="driver", - name="UPS Driver", entity_registry_enabled_default=False, ), "dshutd": SensorEntityDescription( key="dshutd", translation_key="shutdown_delay", - name="UPS Shutdown Delay", ), "dwake": SensorEntityDescription( key="dwake", translation_key="wake_delay", - name="UPS Wake Delay", ), "end apc": SensorEntityDescription( key="end apc", translation_key="date_and_time", - name="UPS Date and Time", entity_registry_enabled_default=False, ), "extbatts": SensorEntityDescription( key="extbatts", translation_key="external_batteries", - name="UPS External Batteries", ), "firmware": SensorEntityDescription( key="firmware", translation_key="firmware_version", - name="UPS Firmware Version", entity_registry_enabled_default=False, ), "hitrans": SensorEntityDescription( key="hitrans", - name="UPS Transfer High", + translation_key="transfer_high", native_unit_of_measurement=UnitOfElectricPotential.VOLT, device_class=SensorDeviceClass.VOLTAGE, ), "hostname": SensorEntityDescription( key="hostname", translation_key="hostname", - name="UPS Hostname", entity_registry_enabled_default=False, ), "humidity": SensorEntityDescription( key="humidity", - name="UPS Ambient Humidity", + translation_key="humidity", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, ), "itemp": SensorEntityDescription( key="itemp", - name="UPS Internal Temperature", + translation_key="internal_temperature", native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, @@ -172,29 +155,26 @@ SENSORS: dict[str, SensorEntityDescription] = { "laststest": SensorEntityDescription( key="laststest", translation_key="last_self_test", - name="UPS Last Self Test", ), "lastxfer": SensorEntityDescription( key="lastxfer", translation_key="last_transfer", - name="UPS Last Transfer", entity_registry_enabled_default=False, ), "linefail": SensorEntityDescription( key="linefail", translation_key="line_failure", - name="UPS Input Voltage Status", ), "linefreq": SensorEntityDescription( key="linefreq", - name="UPS Line Frequency", + translation_key="line_frequency", native_unit_of_measurement=UnitOfFrequency.HERTZ, device_class=SensorDeviceClass.FREQUENCY, state_class=SensorStateClass.MEASUREMENT, ), "linev": SensorEntityDescription( key="linev", - name="UPS Input Voltage", + translation_key="line_voltage", native_unit_of_measurement=UnitOfElectricPotential.VOLT, device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, @@ -202,113 +182,104 @@ SENSORS: dict[str, SensorEntityDescription] = { "loadpct": SensorEntityDescription( key="loadpct", translation_key="load_capacity", - name="UPS Load", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, ), "loadapnt": SensorEntityDescription( key="loadapnt", translation_key="apparent_power", - name="UPS Load Apparent Power", native_unit_of_measurement=PERCENTAGE, ), "lotrans": SensorEntityDescription( key="lotrans", - name="UPS Transfer Low", + translation_key="transfer_low", native_unit_of_measurement=UnitOfElectricPotential.VOLT, device_class=SensorDeviceClass.VOLTAGE, ), "mandate": SensorEntityDescription( key="mandate", translation_key="manufacture_date", - name="UPS Manufacture Date", entity_registry_enabled_default=False, ), "masterupd": SensorEntityDescription( key="masterupd", translation_key="master_update", - name="UPS Master Update", ), "maxlinev": SensorEntityDescription( key="maxlinev", - name="UPS Input Voltage High", + translation_key="input_voltage_high", native_unit_of_measurement=UnitOfElectricPotential.VOLT, device_class=SensorDeviceClass.VOLTAGE, ), "maxtime": SensorEntityDescription( key="maxtime", translation_key="max_time", - name="UPS Battery Timeout", ), "mbattchg": SensorEntityDescription( key="mbattchg", translation_key="max_battery_charge", - name="UPS Battery Shutdown", native_unit_of_measurement=PERCENTAGE, ), "minlinev": SensorEntityDescription( key="minlinev", - name="UPS Input Voltage Low", + translation_key="input_voltage_low", native_unit_of_measurement=UnitOfElectricPotential.VOLT, device_class=SensorDeviceClass.VOLTAGE, ), "mintimel": SensorEntityDescription( key="mintimel", translation_key="min_time", - name="UPS Shutdown Time", ), "model": SensorEntityDescription( key="model", translation_key="model", - name="UPS Model", entity_registry_enabled_default=False, ), "nombattv": SensorEntityDescription( key="nombattv", - name="UPS Battery Nominal Voltage", + translation_key="battery_nominal_voltage", native_unit_of_measurement=UnitOfElectricPotential.VOLT, device_class=SensorDeviceClass.VOLTAGE, ), "nominv": SensorEntityDescription( key="nominv", - name="UPS Nominal Input Voltage", + translation_key="nominal_input_voltage", native_unit_of_measurement=UnitOfElectricPotential.VOLT, device_class=SensorDeviceClass.VOLTAGE, ), "nomoutv": SensorEntityDescription( key="nomoutv", - name="UPS Nominal Output Voltage", + translation_key="nominal_output_voltage", native_unit_of_measurement=UnitOfElectricPotential.VOLT, device_class=SensorDeviceClass.VOLTAGE, ), "nompower": SensorEntityDescription( key="nompower", - name="UPS Nominal Output Power", + translation_key="nominal_output_power", native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, ), "nomapnt": SensorEntityDescription( key="nomapnt", - name="UPS Nominal Apparent Power", + translation_key="nominal_apparent_power", native_unit_of_measurement=UnitOfApparentPower.VOLT_AMPERE, device_class=SensorDeviceClass.APPARENT_POWER, ), "numxfers": SensorEntityDescription( key="numxfers", translation_key="transfer_count", - name="UPS Transfer Count", state_class=SensorStateClass.TOTAL_INCREASING, ), "outcurnt": SensorEntityDescription( key="outcurnt", - name="UPS Output Current", + translation_key="output_current", native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, device_class=SensorDeviceClass.CURRENT, state_class=SensorStateClass.MEASUREMENT, ), "outputv": SensorEntityDescription( key="outputv", - name="UPS Output Voltage", + translation_key="output_voltage", native_unit_of_measurement=UnitOfElectricPotential.VOLT, device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, @@ -316,108 +287,89 @@ SENSORS: dict[str, SensorEntityDescription] = { "reg1": SensorEntityDescription( key="reg1", translation_key="register_1_fault", - name="UPS Register 1 Fault", entity_registry_enabled_default=False, ), "reg2": SensorEntityDescription( key="reg2", translation_key="register_2_fault", - name="UPS Register 2 Fault", entity_registry_enabled_default=False, ), "reg3": SensorEntityDescription( key="reg3", translation_key="register_3_fault", - name="UPS Register 3 Fault", entity_registry_enabled_default=False, ), "retpct": SensorEntityDescription( key="retpct", translation_key="restore_capacity", - name="UPS Restore Requirement", native_unit_of_measurement=PERCENTAGE, ), "selftest": SensorEntityDescription( key="selftest", translation_key="self_test_result", - name="UPS Self Test result", ), "sense": SensorEntityDescription( key="sense", translation_key="sensitivity", - name="UPS Sensitivity", entity_registry_enabled_default=False, ), "serialno": SensorEntityDescription( key="serialno", translation_key="serial_number", - name="UPS Serial Number", entity_registry_enabled_default=False, ), "starttime": SensorEntityDescription( key="starttime", translation_key="startup_time", - name="UPS Startup Time", ), "statflag": SensorEntityDescription( key="statflag", translation_key="online_status", - name="UPS Status Flag", entity_registry_enabled_default=False, ), "status": SensorEntityDescription( key="status", translation_key="status", - name="UPS Status", ), "stesti": SensorEntityDescription( key="stesti", translation_key="self_test_interval", - name="UPS Self Test Interval", ), "timeleft": SensorEntityDescription( key="timeleft", translation_key="time_left", - name="UPS Time Left", state_class=SensorStateClass.MEASUREMENT, ), "tonbatt": SensorEntityDescription( key="tonbatt", translation_key="time_on_battery", - name="UPS Time on Battery", state_class=SensorStateClass.TOTAL_INCREASING, ), "upsmode": SensorEntityDescription( key="upsmode", translation_key="ups_mode", - name="UPS Mode", ), "upsname": SensorEntityDescription( key="upsname", translation_key="ups_name", - name="UPS Name", entity_registry_enabled_default=False, ), "version": SensorEntityDescription( key="version", translation_key="version", - name="UPS Daemon Info", entity_registry_enabled_default=False, ), "xoffbat": SensorEntityDescription( key="xoffbat", translation_key="transfer_from_battery", - name="UPS Transfer from Battery", ), "xoffbatt": SensorEntityDescription( key="xoffbatt", translation_key="transfer_from_battery", - name="UPS Transfer from Battery", ), "xonbatt": SensorEntityDescription( key="xonbatt", translation_key="transfer_to_battery", - name="UPS Transfer to Battery", ), } @@ -484,6 +436,8 @@ def infer_unit(value: str) -> tuple[str, str | None]: class APCUPSdSensor(CoordinatorEntity[APCUPSdCoordinator], SensorEntity): """Representation of a sensor entity for APCUPSd status values.""" + _attr_has_entity_name = True + def __init__( self, coordinator: APCUPSdCoordinator, @@ -493,7 +447,7 @@ class APCUPSdSensor(CoordinatorEntity[APCUPSdCoordinator], SensorEntity): super().__init__(coordinator=coordinator, context=description.key.upper()) # Set up unique id and device info if serial number is available. - if (serial_no := coordinator.ups_serial_no) is not None: + if (serial_no := coordinator.data.serial_no) is not None: self._attr_unique_id = f"{serial_no}_{description.key}" self.entity_description = description diff --git a/homeassistant/components/apcupsd/strings.json b/homeassistant/components/apcupsd/strings.json index c7ebf8a0a3b..93102ac1393 100644 --- a/homeassistant/components/apcupsd/strings.json +++ b/homeassistant/components/apcupsd/strings.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "no_status": "No status is reported from host" + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" @@ -16,5 +15,209 @@ "description": "Enter the host and port on which the apcupsd NIS is being served." } } + }, + "entity": { + "binary_sensor": { + "online_status": { + "name": "Online status" + } + }, + "sensor": { + "alarm_delay": { + "name": "Alarm delay" + }, + "ambient_temperature": { + "name": "Ambient temperature" + }, + "apc_status": { + "name": "Status data" + }, + "apc_model": { + "name": "Model" + }, + "bad_batteries": { + "name": "Bad batteries" + }, + "battery_replacement_date": { + "name": "Battery replaced" + }, + "battery_status": { + "name": "Battery status" + }, + "battery_voltage": { + "name": "Battery voltage" + }, + "cable_type": { + "name": "Cable type" + }, + "total_time_on_battery": { + "name": "Total time on battery" + }, + "date": { + "name": "Status date" + }, + "dip_switch_settings": { + "name": "Dip switch settings" + }, + "low_battery_signal": { + "name": "Low battery signal" + }, + "driver": { + "name": "Driver" + }, + "shutdown_delay": { + "name": "Shutdown delay" + }, + "wake_delay": { + "name": "Wake delay" + }, + "date_and_time": { + "name": "Date and time" + }, + "external_batteries": { + "name": "External batteries" + }, + "firmware_version": { + "name": "Firmware version" + }, + "transfer_high": { + "name": "Transfer high" + }, + "hostname": { + "name": "Hostname" + }, + "humidity": { + "name": "Ambient humidity" + }, + "internal_temperature": { + "name": "Internal temperature" + }, + "last_self_test": { + "name": "Last self test" + }, + "last_transfer": { + "name": "Last transfer" + }, + "line_failure": { + "name": "Input voltage status" + }, + "line_frequency": { + "name": "Line frequency" + }, + "line_voltage": { + "name": "Input voltage" + }, + "load_capacity": { + "name": "Load" + }, + "apparent_power": { + "name": "Load apparent power" + }, + "transfer_low": { + "name": "Transfer low" + }, + "manufacture_date": { + "name": "Manufacture date" + }, + "master_update": { + "name": "Master update" + }, + "input_voltage_high": { + "name": "Input voltage high" + }, + "max_time": { + "name": "Battery timeout" + }, + "max_battery_charge": { + "name": "Battery shutdown" + }, + "input_voltage_low": { + "name": "Input voltage low" + }, + "min_time": { + "name": "Shutdown time" + }, + "model": { + "name": "Model" + }, + "battery_nominal_voltage": { + "name": "Battery nominal voltage" + }, + "nominal_input_voltage": { + "name": "Nominal input voltage" + }, + "nominal_output_voltage": { + "name": "Nominal output voltage" + }, + "nominal_output_power": { + "name": "Nominal output power" + }, + "nominal_apparent_power": { + "name": "Nominal apparent power" + }, + "transfer_count": { + "name": "Transfer count" + }, + "output_current": { + "name": "Output current" + }, + "output_voltage": { + "name": "Output voltage" + }, + "register_1_fault": { + "name": "Register 1 fault" + }, + "register_2_fault": { + "name": "Register 2 fault" + }, + "register_3_fault": { + "name": "Register 3 fault" + }, + "restore_capacity": { + "name": "Restore requirement" + }, + "self_test_result": { + "name": "Self test result" + }, + "sensitivity": { + "name": "Sensitivity" + }, + "serial_number": { + "name": "Serial number" + }, + "startup_time": { + "name": "Startup time" + }, + "online_status": { + "name": "Status flag" + }, + "status": { + "name": "Status" + }, + "self_test_interval": { + "name": "Self test interval" + }, + "time_left": { + "name": "Time left" + }, + "time_on_battery": { + "name": "Time on battery" + }, + "ups_mode": { + "name": "Mode" + }, + "ups_name": { + "name": "Name" + }, + "version": { + "name": "Daemon version" + }, + "transfer_from_battery": { + "name": "Transfer from battery" + }, + "transfer_to_battery": { + "name": "Transfer to battery" + } + } } } diff --git a/homeassistant/components/api/__init__.py b/homeassistant/components/api/__init__.py index 01a84cf606a..82aaefe1288 100644 --- a/homeassistant/components/api/__init__.py +++ b/homeassistant/components/api/__init__.py @@ -1,4 +1,5 @@ """Rest API for Home Assistant.""" + import asyncio from asyncio import shield, timeout from functools import lru_cache @@ -48,7 +49,7 @@ from homeassistant.helpers import config_validation as cv, template from homeassistant.helpers.event import EventStateChangedData from homeassistant.helpers.json import json_dumps, json_fragment from homeassistant.helpers.service import async_get_all_descriptions -from homeassistant.helpers.typing import ConfigType, EventType +from homeassistant.helpers.typing import ConfigType from homeassistant.util.json import json_loads _LOGGER = logging.getLogger(__name__) @@ -117,7 +118,7 @@ class APICoreStateView(HomeAssistantView): Home Assistant core is running. Its primary use case is for supervisor to check if Home Assistant is running. """ - hass: HomeAssistant = request.app[KEY_HASS] + hass = request.app[KEY_HASS] return self.json({"state": hass.state.value}) @@ -130,13 +131,13 @@ class APIEventStream(HomeAssistantView): @require_admin async def get(self, request: web.Request) -> web.StreamResponse: """Provide a streaming interface for the event bus.""" - hass: HomeAssistant = request.app[KEY_HASS] + hass = request.app[KEY_HASS] stop_obj = object() to_write: asyncio.Queue[object | str] = asyncio.Queue() restrict: list[str] | None = None if restrict_str := request.query.get("restrict"): - restrict = restrict_str.split(",") + [EVENT_HOMEASSISTANT_STOP] + restrict = [*restrict_str.split(","), EVENT_HOMEASSISTANT_STOP] async def forward_events(event: Event) -> None: """Forward events to the open request.""" @@ -197,8 +198,7 @@ class APIConfigView(HomeAssistantView): @ha.callback def get(self, request: web.Request) -> web.Response: """Get current configuration.""" - hass: HomeAssistant = request.app[KEY_HASS] - return self.json(hass.config.as_dict()) + return self.json(request.app[KEY_HASS].config.as_dict()) class APIStatesView(HomeAssistantView): @@ -211,7 +211,7 @@ class APIStatesView(HomeAssistantView): def get(self, request: web.Request) -> web.Response: """Get current states.""" user: User = request[KEY_HASS_USER] - hass: HomeAssistant = request.app[KEY_HASS] + hass = request.app[KEY_HASS] if user.is_admin: states = (state.as_dict_json for state in hass.states.async_all()) else: @@ -240,7 +240,7 @@ class APIEntityStateView(HomeAssistantView): def get(self, request: web.Request, entity_id: str) -> web.Response: """Retrieve state of entity.""" user: User = request[KEY_HASS_USER] - hass: HomeAssistant = request.app[KEY_HASS] + hass = request.app[KEY_HASS] if not user.permissions.check_entity(entity_id, POLICY_READ): raise Unauthorized(entity_id=entity_id) @@ -256,7 +256,7 @@ class APIEntityStateView(HomeAssistantView): user: User = request[KEY_HASS_USER] if not user.is_admin: raise Unauthorized(entity_id=entity_id) - hass: HomeAssistant = request.app[KEY_HASS] + hass = request.app[KEY_HASS] try: data = await request.json() except ValueError: @@ -296,8 +296,7 @@ class APIEntityStateView(HomeAssistantView): """Remove entity.""" if not request[KEY_HASS_USER].is_admin: raise Unauthorized(entity_id=entity_id) - hass: HomeAssistant = request.app[KEY_HASS] - if hass.states.async_remove(entity_id): + if request.app[KEY_HASS].states.async_remove(entity_id): return self.json_message("Entity removed.") return self.json_message("Entity not found.", HTTPStatus.NOT_FOUND) @@ -311,8 +310,7 @@ class APIEventListenersView(HomeAssistantView): @ha.callback def get(self, request: web.Request) -> web.Response: """Get event listeners.""" - hass: HomeAssistant = request.app[KEY_HASS] - return self.json(async_events_json(hass)) + return self.json(async_events_json(request.app[KEY_HASS])) class APIEventView(HomeAssistantView): @@ -346,8 +344,7 @@ class APIEventView(HomeAssistantView): if state: event_data[key] = state - hass: HomeAssistant = request.app[KEY_HASS] - hass.bus.async_fire( + request.app[KEY_HASS].bus.async_fire( event_type, event_data, ha.EventOrigin.remote, self.context(request) ) @@ -362,8 +359,7 @@ class APIServicesView(HomeAssistantView): async def get(self, request: web.Request) -> web.Response: """Get registered services.""" - hass: HomeAssistant = request.app[KEY_HASS] - services = await async_services_json(hass) + services = await async_services_json(request.app[KEY_HASS]) return self.json(services) @@ -380,7 +376,7 @@ class APIDomainServicesView(HomeAssistantView): Returns a list of changed states. """ - hass: HomeAssistant = request.app[KEY_HASS] + hass = request.app[KEY_HASS] body = await request.text() try: data = json_loads(body) if body else None @@ -394,14 +390,14 @@ class APIDomainServicesView(HomeAssistantView): @ha.callback def _async_save_changed_entities( - event: EventType[EventStateChangedData], + event: Event[EventStateChangedData], ) -> None: if event.context == context and (state := event.data["new_state"]): changed_states.append(state.json_fragment) cancel_listen = hass.bus.async_listen( EVENT_STATE_CHANGED, - _async_save_changed_entities, # type: ignore[arg-type] + _async_save_changed_entities, run_immediately=True, ) @@ -417,7 +413,7 @@ class APIDomainServicesView(HomeAssistantView): ) ) except (vol.Invalid, ServiceNotFound) as ex: - raise HTTPBadRequest() from ex + raise HTTPBadRequest from ex finally: cancel_listen() @@ -433,8 +429,7 @@ class APIComponentsView(HomeAssistantView): @ha.callback def get(self, request: web.Request) -> web.Response: """Get current loaded components.""" - hass: HomeAssistant = request.app[KEY_HASS] - return self.json(list(hass.config.components)) + return self.json(request.app[KEY_HASS].config.components) @lru_cache @@ -471,7 +466,7 @@ class APIErrorLog(HomeAssistantView): @require_admin async def get(self, request: web.Request) -> web.FileResponse: """Retrieve API error log.""" - hass: HomeAssistant = request.app[KEY_HASS] + hass = request.app[KEY_HASS] response = web.FileResponse(hass.data[DATA_LOGGING]) response.enable_compression() return response diff --git a/homeassistant/components/apple_tv/__init__.py b/homeassistant/components/apple_tv/__init__.py index c369b07de36..9a72a89c876 100644 --- a/homeassistant/components/apple_tv/__init__.py +++ b/homeassistant/components/apple_tv/__init__.py @@ -1,4 +1,5 @@ """The Apple TV integration.""" + from __future__ import annotations import asyncio @@ -53,6 +54,25 @@ SIGNAL_DISCONNECTED = "apple_tv_disconnected" PLATFORMS = [Platform.MEDIA_PLAYER, Platform.REMOTE] +AUTH_EXCEPTIONS = ( + exceptions.AuthenticationError, + exceptions.InvalidCredentialsError, + exceptions.NoCredentialsError, +) +CONNECTION_TIMEOUT_EXCEPTIONS = ( + asyncio.CancelledError, + TimeoutError, + exceptions.ConnectionLostError, + exceptions.ConnectionFailedError, +) +DEVICE_EXCEPTIONS = ( + exceptions.ProtocolError, + exceptions.NoServiceError, + exceptions.PairingError, + exceptions.BackOffError, + exceptions.DeviceIdMissingError, +) + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry for Apple TV.""" @@ -63,27 +83,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: await manager.async_first_connect() - except ( - exceptions.AuthenticationError, - exceptions.InvalidCredentialsError, - exceptions.NoCredentialsError, - ) as ex: + except AUTH_EXCEPTIONS as ex: raise ConfigEntryAuthFailed( f"{address}: Authentication failed, try reconfiguring device: {ex}" ) from ex - except ( - asyncio.CancelledError, - exceptions.ConnectionLostError, - exceptions.ConnectionFailedError, - ) as ex: + except CONNECTION_TIMEOUT_EXCEPTIONS as ex: raise ConfigEntryNotReady(f"{address}: {ex}") from ex - except ( - exceptions.ProtocolError, - exceptions.NoServiceError, - exceptions.PairingError, - exceptions.BackOffError, - exceptions.DeviceIdMissingError, - ) as ex: + except DEVICE_EXCEPTIONS as ex: _LOGGER.debug( "Error setting up apple_tv at %s: %s", address, ex, exc_info=ex ) @@ -96,7 +102,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await manager.disconnect() entry.async_on_unload( - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_hass_stop) + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, on_hass_stop, run_immediately=True + ) ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) @@ -246,7 +254,12 @@ class AppleTVManager(DeviceListener): def _start_connect_loop(self) -> None: """Start background connect loop to device.""" if not self._task and self.atv is None and self.is_on: - self._task = asyncio.create_task(self._connect_loop()) + self._task = self.config_entry.async_create_background_task( + self.hass, + self._connect_loop(), + name=f"apple_tv connect loop {self.config_entry.title}", + eager_start=True, + ) else: _LOGGER.debug( "Not starting connect loop (%s, %s)", self.atv is None, self.is_on diff --git a/homeassistant/components/apple_tv/browse_media.py b/homeassistant/components/apple_tv/browse_media.py index d706c9aa7e9..0b8d2ab06c3 100644 --- a/homeassistant/components/apple_tv/browse_media.py +++ b/homeassistant/components/apple_tv/browse_media.py @@ -1,4 +1,5 @@ """Support for media browsing.""" + from typing import Any from homeassistant.components.media_player import BrowseMedia, MediaClass, MediaType diff --git a/homeassistant/components/apple_tv/config_flow.py b/homeassistant/components/apple_tv/config_flow.py index 2bb4608dca1..19cbb24d8a2 100644 --- a/homeassistant/components/apple_tv/config_flow.py +++ b/homeassistant/components/apple_tv/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Apple TV integration.""" + from __future__ import annotations import asyncio @@ -16,11 +17,17 @@ from pyatv.helpers import get_unique_id from pyatv.interface import BaseConfig, PairingHandler import voluptuous as vol -from homeassistant import config_entries from homeassistant.components import zeroconf +from homeassistant.config_entries import ( + SOURCE_IGNORE, + SOURCE_ZEROCONF, + ConfigEntry, + ConfigFlow, + ConfigFlowResult, +) from homeassistant.const import CONF_ADDRESS, CONF_NAME, CONF_PIN from homeassistant.core import HomeAssistant, callback -from homeassistant.data_entry_flow import AbortFlow, FlowResult +from homeassistant.data_entry_flow import AbortFlow from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.schema_config_entry_flow import ( @@ -85,7 +92,7 @@ async def device_scan( return None, None -class AppleTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class AppleTVConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Apple TV.""" VERSION = 1 @@ -100,7 +107,7 @@ class AppleTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback def async_get_options_flow( - config_entry: config_entries.ConfigEntry, + config_entry: ConfigEntry, ) -> SchemaOptionsFlowHandler: """Get options flow for this handler.""" return SchemaOptionsFlowHandler(config_entry, OPTIONS_FLOW) @@ -141,7 +148,9 @@ class AppleTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return entry.unique_id return None - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Handle initial step when updating invalid credentials.""" self.context["title_placeholders"] = { "name": entry_data[CONF_NAME], @@ -149,22 +158,22 @@ class AppleTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): } self.scan_filter = self.unique_id self.context["identifier"] = self.unique_id - return await self.async_step_reconfigure() + return await self.async_step_restore_device() - async def async_step_reconfigure( + async def async_step_restore_device( self, user_input: dict[str, str] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Inform user that reconfiguration is about to start.""" if user_input is not None: return await self.async_find_device_wrapper( self.async_pair_next_protocol, allow_exist=True ) - return self.async_show_form(step_id="reconfigure") + return self.async_show_form(step_id="restore_device") async def async_step_user( self, user_input: dict[str, str] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors = {} if user_input is not None: @@ -194,7 +203,7 @@ class AppleTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_zeroconf( self, discovery_info: zeroconf.ZeroconfServiceInfo - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle device found via zeroconf.""" if discovery_info.ip_address.version == 6: return self.async_abort(reason="ipv6_not_supported") @@ -276,7 +285,7 @@ class AppleTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): for flow in self._async_in_progress(include_uninitialized=True): context = flow["context"] if ( - context.get("source") != config_entries.SOURCE_ZEROCONF + context.get("source") != SOURCE_ZEROCONF or context.get(CONF_ADDRESS) != host ): continue @@ -290,7 +299,7 @@ class AppleTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_found_zeroconf_device( self, user_input: dict[str, str] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle device found after Zeroconf discovery.""" assert self.atv self.context["all_identifiers"] = self.atv.all_identifiers @@ -306,9 +315,9 @@ class AppleTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_find_device_wrapper( self, - next_func: Callable[[], Awaitable[FlowResult]], + next_func: Callable[[], Awaitable[ConfigFlowResult]], allow_exist: bool = False, - ) -> FlowResult: + ) -> ConfigFlowResult: """Find a specific device and call another function when done. This function will do error handling and bail out when an error @@ -332,7 +341,7 @@ class AppleTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.hass, self.scan_filter, self.hass.loop ) if not self.atv: - raise DeviceNotFound() + raise DeviceNotFound # Protocols supported by the device are prospects for pairing self.protocols_to_pair = deque( @@ -370,16 +379,16 @@ class AppleTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): CONF_IDENTIFIERS: list(combined_identifiers), }, ) - if entry.source != config_entries.SOURCE_IGNORE: + if entry.source != SOURCE_IGNORE: self.hass.async_create_task( self.hass.config_entries.async_reload(entry.entry_id) ) if not allow_exist: - raise DeviceAlreadyConfigured() + raise DeviceAlreadyConfigured async def async_step_confirm( self, user_input: dict[str, str] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle user-confirmation of discovered node.""" assert self.atv if user_input is not None: @@ -407,7 +416,7 @@ class AppleTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): }, ) - async def async_pair_next_protocol(self) -> FlowResult: + async def async_pair_next_protocol(self) -> ConfigFlowResult: """Start pairing process for the next available protocol.""" await self._async_cleanup() @@ -481,7 +490,7 @@ class AppleTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_protocol_disabled( self, user_input: dict[str, str] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Inform user that a protocol is disabled and cannot be paired.""" assert self.protocol if user_input is not None: @@ -493,7 +502,7 @@ class AppleTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_pair_with_pin( self, user_input: dict[str, str] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle pairing step where a PIN is required from the user.""" errors = {} assert self.pairing @@ -520,7 +529,7 @@ class AppleTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_pair_no_pin( self, user_input: dict[str, str] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle step where user has to enter a PIN on the device.""" assert self.pairing assert self.protocol @@ -545,7 +554,7 @@ class AppleTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_service_problem( self, user_input: dict[str, str] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Inform user that a service will not be added.""" assert self.protocol if user_input is not None: @@ -558,7 +567,7 @@ class AppleTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_password( self, user_input: dict[str, str] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Inform user that password is not supported.""" assert self.protocol if user_input is not None: @@ -575,7 +584,7 @@ class AppleTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): await self.pairing.close() self.pairing = None - async def _async_get_entry(self) -> FlowResult: + async def _async_get_entry(self) -> ConfigFlowResult: """Return config entry or update existing config entry.""" # Abort if no protocols were paired if not self.credentials: diff --git a/homeassistant/components/apple_tv/manifest.json b/homeassistant/components/apple_tv/manifest.json index 0a14e11ecb7..1f7ac45372e 100644 --- a/homeassistant/components/apple_tv/manifest.json +++ b/homeassistant/components/apple_tv/manifest.json @@ -5,7 +5,6 @@ "config_flow": true, "dependencies": ["zeroconf"], "documentation": "https://www.home-assistant.io/integrations/apple_tv", - "import_executor": true, "iot_class": "local_push", "loggers": ["pyatv", "srptools"], "requirements": ["pyatv==0.14.3"], diff --git a/homeassistant/components/apple_tv/media_player.py b/homeassistant/components/apple_tv/media_player.py index a7b5957ecff..3f64d10f9ac 100644 --- a/homeassistant/components/apple_tv/media_player.py +++ b/homeassistant/components/apple_tv/media_player.py @@ -1,4 +1,5 @@ """Support for Apple TV media player.""" + from __future__ import annotations from datetime import datetime @@ -152,7 +153,9 @@ class AppleTvMediaPlayer( atv.audio.listener = self if atv.features.in_state(FeatureState.Available, FeatureName.AppList): - self.hass.create_task(self._update_app_list()) + self.manager.config_entry.async_create_task( + self.hass, self._update_app_list(), eager_start=True + ) async def _update_app_list(self) -> None: _LOGGER.debug("Updating app list") diff --git a/homeassistant/components/apple_tv/remote.py b/homeassistant/components/apple_tv/remote.py index 7baa6321f21..822a9c3306a 100644 --- a/homeassistant/components/apple_tv/remote.py +++ b/homeassistant/components/apple_tv/remote.py @@ -1,4 +1,5 @@ """Remote control support for Apple TV.""" + import asyncio from collections.abc import Iterable import logging diff --git a/homeassistant/components/apple_tv/strings.json b/homeassistant/components/apple_tv/strings.json index 8730ffe01d5..4efe80f7bef 100644 --- a/homeassistant/components/apple_tv/strings.json +++ b/homeassistant/components/apple_tv/strings.json @@ -9,7 +9,7 @@ "device_input": "[%key:common::config_flow::data::device%]" } }, - "reconfigure": { + "restore_device": { "title": "Device reconfiguration", "description": "Reconfigure this device to restore its functionality." }, diff --git a/homeassistant/components/application_credentials/__init__.py b/homeassistant/components/application_credentials/__init__.py index 679ff9bfac4..aacd18fc795 100644 --- a/homeassistant/components/application_credentials/__init__.py +++ b/homeassistant/components/application_credentials/__init__.py @@ -5,6 +5,7 @@ of other integrations. Integrations register an authorization server, and then the APIs are used to add one or more client credentials. Integrations may also provide credentials from yaml for backwards compatibility. """ + from __future__ import annotations from dataclasses import dataclass @@ -291,7 +292,7 @@ async def _get_platform( _LOGGER.debug("Integration '%s' does not exist: %s", integration_domain, err) return None try: - platform = integration.get_platform("application_credentials") + platform = await integration.async_get_platform("application_credentials") except ImportError as err: _LOGGER.debug( "Integration '%s' does not provide application_credentials: %s", diff --git a/homeassistant/components/apprise/manifest.json b/homeassistant/components/apprise/manifest.json index dd630ccc872..0c0e816f088 100644 --- a/homeassistant/components/apprise/manifest.json +++ b/homeassistant/components/apprise/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/apprise", "iot_class": "cloud_push", "loggers": ["apprise"], - "requirements": ["apprise==1.7.2"] + "requirements": ["apprise==1.7.4"] } diff --git a/homeassistant/components/apprise/notify.py b/homeassistant/components/apprise/notify.py index acd1db00d93..57a7feb6e5c 100644 --- a/homeassistant/components/apprise/notify.py +++ b/homeassistant/components/apprise/notify.py @@ -1,4 +1,5 @@ """Apprise platform for notify component.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/aprilaire/__init__.py b/homeassistant/components/aprilaire/__init__.py index b5aeea2a55c..4fa5cdac68d 100644 --- a/homeassistant/components/aprilaire/__init__.py +++ b/homeassistant/components/aprilaire/__init__.py @@ -51,7 +51,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: coordinator.stop_listen() - raise ConfigEntryNotReady() + raise ConfigEntryNotReady await coordinator.wait_for_ready(ready_callback) diff --git a/homeassistant/components/aprilaire/config_flow.py b/homeassistant/components/aprilaire/config_flow.py index 0e38b385450..14437e5f3f2 100644 --- a/homeassistant/components/aprilaire/config_flow.py +++ b/homeassistant/components/aprilaire/config_flow.py @@ -10,7 +10,6 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_HOST, CONF_PORT -from homeassistant.data_entry_flow import FlowResult import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import format_mac @@ -34,7 +33,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> config_entries.ConfigFlowResult: """Handle the initial step.""" if user_input is None: diff --git a/homeassistant/components/aprs/device_tracker.py b/homeassistant/components/aprs/device_tracker.py index 8b952f88c7c..0915643340b 100644 --- a/homeassistant/components/aprs/device_tracker.py +++ b/homeassistant/components/aprs/device_tracker.py @@ -1,4 +1,5 @@ """Support for APRS device tracking.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/aqualogic/__init__.py b/homeassistant/components/aqualogic/__init__.py index 28e57c2b351..7c3a5966d1c 100644 --- a/homeassistant/components/aqualogic/__init__.py +++ b/homeassistant/components/aqualogic/__init__.py @@ -1,4 +1,5 @@ """Support for AquaLogic devices.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/aqualogic/sensor.py b/homeassistant/components/aqualogic/sensor.py index 90f87bfde23..bdb582826dc 100644 --- a/homeassistant/components/aqualogic/sensor.py +++ b/homeassistant/components/aqualogic/sensor.py @@ -1,4 +1,5 @@ """Support for AquaLogic sensors.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/homeassistant/components/aqualogic/switch.py b/homeassistant/components/aqualogic/switch.py index e693df0a0c1..0f1a7e34b3c 100644 --- a/homeassistant/components/aqualogic/switch.py +++ b/homeassistant/components/aqualogic/switch.py @@ -1,4 +1,5 @@ """Support for AquaLogic switches.""" + from __future__ import annotations from typing import Any @@ -45,13 +46,12 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the switch platform.""" - switches = [] - processor: AquaLogicProcessor = hass.data[DOMAIN] - for switch_type in config[CONF_MONITORED_CONDITIONS]: - switches.append(AquaLogicSwitch(processor, switch_type)) - async_add_entities(switches) + async_add_entities( + AquaLogicSwitch(processor, switch_type) + for switch_type in config[CONF_MONITORED_CONDITIONS] + ) class AquaLogicSwitch(SwitchEntity): diff --git a/homeassistant/components/aquostv/media_player.py b/homeassistant/components/aquostv/media_player.py index a87756334e2..7160810e0dc 100644 --- a/homeassistant/components/aquostv/media_player.py +++ b/homeassistant/components/aquostv/media_player.py @@ -1,4 +1,5 @@ """Support for interface with an Aquos TV.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/aranet/__init__.py b/homeassistant/components/aranet/__init__.py index 07e19ca2618..3a2bc266653 100644 --- a/homeassistant/components/aranet/__init__.py +++ b/homeassistant/components/aranet/__init__.py @@ -1,4 +1,5 @@ """The Aranet integration.""" + from __future__ import annotations import logging @@ -32,14 +33,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: 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, - update_method=_service_info_to_adv, + coordinator = hass.data.setdefault(DOMAIN, {})[entry.entry_id] = ( + PassiveBluetoothProcessorCoordinator( + hass, + _LOGGER, + address=address, + mode=BluetoothScanningMode.PASSIVE, + update_method=_service_info_to_adv, + ) ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload( diff --git a/homeassistant/components/aranet/config_flow.py b/homeassistant/components/aranet/config_flow.py index 029ee251ae7..cf5f24263dd 100644 --- a/homeassistant/components/aranet/config_flow.py +++ b/homeassistant/components/aranet/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Aranet integration.""" + from __future__ import annotations import logging @@ -7,13 +8,13 @@ from typing import Any from aranet4.client import Aranet4Advertisement, Version as AranetVersion import voluptuous as vol -from homeassistant import config_entries from homeassistant.components.bluetooth import ( BluetoothServiceInfoBleak, async_discovered_service_info, ) +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_ADDRESS -from homeassistant.data_entry_flow import AbortFlow, FlowResult +from homeassistant.data_entry_flow import AbortFlow from .const import DOMAIN @@ -22,7 +23,7 @@ _LOGGER = logging.getLogger(__name__) MIN_VERSION = AranetVersion(1, 2, 0) -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class AranetConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Aranet.""" VERSION = 1 @@ -45,7 +46,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_bluetooth( self, discovery_info: BluetoothServiceInfoBleak - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the Bluetooth discovery step.""" await self.async_set_unique_id(discovery_info.address) self._abort_if_unique_id_configured() @@ -58,7 +59,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_bluetooth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm discovery.""" assert self._discovered_device is not None adv = self._discovered_device @@ -77,7 +78,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the user step to pick discovered device.""" if user_input is not None: address = user_input[CONF_ADDRESS] diff --git a/homeassistant/components/aranet/sensor.py b/homeassistant/components/aranet/sensor.py index 23d3b64fdca..b55fe2bc5ce 100644 --- a/homeassistant/components/aranet/sensor.py +++ b/homeassistant/components/aranet/sensor.py @@ -1,4 +1,5 @@ """Support for Aranet sensors.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/homeassistant/components/arcam_fmj/__init__.py b/homeassistant/components/arcam_fmj/__init__.py index a45dd89e180..ff6bd872065 100644 --- a/homeassistant/components/arcam_fmj/__init__.py +++ b/homeassistant/components/arcam_fmj/__init__.py @@ -1,4 +1,5 @@ """Arcam component.""" + import asyncio from asyncio import timeout import logging diff --git a/homeassistant/components/arcam_fmj/config_flow.py b/homeassistant/components/arcam_fmj/config_flow.py index 09944328c4a..a1aefc3a755 100644 --- a/homeassistant/components/arcam_fmj/config_flow.py +++ b/homeassistant/components/arcam_fmj/config_flow.py @@ -1,4 +1,5 @@ """Config flow to configure the Arcam FMJ component.""" + from __future__ import annotations from typing import Any @@ -8,23 +9,22 @@ from arcam.fmj.client import Client, ConnectionFailed from arcam.fmj.utils import get_uniqueid_from_host, get_uniqueid_from_udn import voluptuous as vol -from homeassistant import config_entries from homeassistant.components import ssdp +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DEFAULT_NAME, DEFAULT_PORT, DOMAIN, DOMAIN_DATA_ENTRIES -def get_entry_client(hass: HomeAssistant, entry: config_entries.ConfigEntry) -> Client: +def get_entry_client(hass: HomeAssistant, entry: ConfigEntry) -> Client: """Retrieve client associated with a config entry.""" client: Client = hass.data[DOMAIN_DATA_ENTRIES][entry.entry_id] return client -class ArcamFmjFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class ArcamFmjFlowHandler(ConfigFlow, domain=DOMAIN): """Handle config flow.""" VERSION = 1 @@ -35,7 +35,7 @@ class ArcamFmjFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(uuid) self._abort_if_unique_id_configured({CONF_HOST: host, CONF_PORT: port}) - async def _async_check_and_create(self, host: str, port: int) -> FlowResult: + async def _async_check_and_create(self, host: str, port: int) -> ConfigFlowResult: client = Client(host, port) try: await client.start() @@ -51,7 +51,7 @@ class ArcamFmjFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a discovered device.""" errors: dict[str, str] = {} @@ -79,7 +79,7 @@ class ArcamFmjFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle user-confirmation of discovered node.""" context = self.context placeholders = { @@ -96,7 +96,9 @@ class ArcamFmjFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): step_id="confirm", description_placeholders=placeholders ) - async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: + async def async_step_ssdp( + self, discovery_info: ssdp.SsdpServiceInfo + ) -> ConfigFlowResult: """Handle a discovered device.""" host = str(urlparse(discovery_info.ssdp_location).hostname) port = DEFAULT_PORT diff --git a/homeassistant/components/arcam_fmj/const.py b/homeassistant/components/arcam_fmj/const.py index e3c5ae3075a..94e8f5a9ee1 100644 --- a/homeassistant/components/arcam_fmj/const.py +++ b/homeassistant/components/arcam_fmj/const.py @@ -1,4 +1,5 @@ """Constants used for arcam.""" + DOMAIN = "arcam_fmj" SIGNAL_CLIENT_STARTED = "arcam.client_started" diff --git a/homeassistant/components/arcam_fmj/device_trigger.py b/homeassistant/components/arcam_fmj/device_trigger.py index 174ffda9622..16061f3a1e1 100644 --- a/homeassistant/components/arcam_fmj/device_trigger.py +++ b/homeassistant/components/arcam_fmj/device_trigger.py @@ -1,4 +1,5 @@ """Provides device automations for Arcam FMJ Receiver control.""" + from __future__ import annotations import voluptuous as vol @@ -32,23 +33,19 @@ async def async_get_triggers( hass: HomeAssistant, device_id: str ) -> list[dict[str, str]]: """List device triggers for Arcam FMJ Receiver control devices.""" - registry = er.async_get(hass) - triggers = [] + entity_registry = er.async_get(hass) - # Get all the integrations entities for this device - for entry in er.async_entries_for_device(registry, device_id): - if entry.domain == "media_player": - triggers.append( - { - CONF_PLATFORM: "device", - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.id, - CONF_TYPE: "turn_on", - } - ) - - return triggers + return [ + { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.id, + CONF_TYPE: "turn_on", + } + for entry in er.async_entries_for_device(entity_registry, device_id) + if entry.domain == "media_player" + ] async def async_attach_trigger( diff --git a/homeassistant/components/arcam_fmj/media_player.py b/homeassistant/components/arcam_fmj/media_player.py index 7ec5bcdfa64..ac8d389304b 100644 --- a/homeassistant/components/arcam_fmj/media_player.py +++ b/homeassistant/components/arcam_fmj/media_player.py @@ -1,4 +1,5 @@ """Arcam media player.""" + from __future__ import annotations from collections.abc import Callable, Coroutine diff --git a/homeassistant/components/arest/binary_sensor.py b/homeassistant/components/arest/binary_sensor.py index 5d65a23335a..71f1c081f2d 100644 --- a/homeassistant/components/arest/binary_sensor.py +++ b/homeassistant/components/arest/binary_sensor.py @@ -1,4 +1,5 @@ """Support for an exposed aREST RESTful API of a device.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/arest/sensor.py b/homeassistant/components/arest/sensor.py index 2e6012e0e6b..917b255ef14 100644 --- a/homeassistant/components/arest/sensor.py +++ b/homeassistant/components/arest/sensor.py @@ -1,4 +1,5 @@ """Support for an exposed aREST RESTful API of a device.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/arest/switch.py b/homeassistant/components/arest/switch.py index 1c67723fc02..4b15e6726fe 100644 --- a/homeassistant/components/arest/switch.py +++ b/homeassistant/components/arest/switch.py @@ -1,4 +1,5 @@ """Support for an exposed aREST RESTful API of a device.""" + from __future__ import annotations from http import HTTPStatus diff --git a/homeassistant/components/arris_tg2492lg/device_tracker.py b/homeassistant/components/arris_tg2492lg/device_tracker.py index bb917af5c39..f9485636365 100644 --- a/homeassistant/components/arris_tg2492lg/device_tracker.py +++ b/homeassistant/components/arris_tg2492lg/device_tracker.py @@ -1,4 +1,5 @@ """Support for Arris TG2492LG router.""" + from __future__ import annotations from arris_tg2492lg import ConnectBox, Device diff --git a/homeassistant/components/aruba/device_tracker.py b/homeassistant/components/aruba/device_tracker.py index 1b449450cf8..dd94a5975f0 100644 --- a/homeassistant/components/aruba/device_tracker.py +++ b/homeassistant/components/aruba/device_tracker.py @@ -1,4 +1,5 @@ """Support for Aruba Access Points.""" + from __future__ import annotations import logging @@ -22,8 +23,8 @@ _LOGGER = logging.getLogger(__name__) _DEVICES_REGEX = re.compile( r"(?P([^\s]+)?)\s+" - + r"(?P([0-9]{1,3}[\.]){3}[0-9]{1,3})\s+" - + r"(?P([0-9a-f]{2}[:-]){5}([0-9a-f]{2}))\s+" + r"(?P([0-9]{1,3}[\.]){3}[0-9]{1,3})\s+" + r"(?P([0-9a-f]{2}[:-]){5}([0-9a-f]{2}))\s+" ) PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( diff --git a/homeassistant/components/arwn/sensor.py b/homeassistant/components/arwn/sensor.py index caf7dc6f45e..ada96c07340 100644 --- a/homeassistant/components/arwn/sensor.py +++ b/homeassistant/components/arwn/sensor.py @@ -1,4 +1,5 @@ """Support for collecting data from the ARWN project.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/aseko_pool_live/__init__.py b/homeassistant/components/aseko_pool_live/__init__.py index b09682fcaf9..5773b3eb5b9 100644 --- a/homeassistant/components/aseko_pool_live/__init__.py +++ b/homeassistant/components/aseko_pool_live/__init__.py @@ -1,14 +1,15 @@ """The Aseko Pool Live integration.""" + from __future__ import annotations import logging -from aioaseko import APIUnavailable, MobileAccount +from aioaseko import APIUnavailable, InvalidAuthCredentials, MobileAccount from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_ACCESS_TOKEN, Platform +from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, Platform from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN @@ -22,11 +23,15 @@ PLATFORMS: list[str] = [Platform.BINARY_SENSOR, Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Aseko Pool Live from a config entry.""" account = MobileAccount( - async_get_clientsession(hass), access_token=entry.data[CONF_ACCESS_TOKEN] + async_get_clientsession(hass), + username=entry.data[CONF_EMAIL], + password=entry.data[CONF_PASSWORD], ) try: units = await account.get_units() + except InvalidAuthCredentials as err: + raise ConfigEntryAuthFailed from err except APIUnavailable as err: raise ConfigEntryNotReady from err @@ -48,3 +53,22 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN].pop(entry.entry_id) return unload_ok + + +async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Migrate old entry.""" + _LOGGER.debug("Migrating from version %s", config_entry.version) + + if config_entry.version == 1: + new = { + CONF_EMAIL: config_entry.title, + CONF_PASSWORD: "", + } + + hass.config_entries.async_update_entry(config_entry, data=new, version=2) + + _LOGGER.debug("Migration to version %s successful", config_entry.version) + return True + + _LOGGER.error("Attempt to migrate from unknown version %s", config_entry.version) + return False diff --git a/homeassistant/components/aseko_pool_live/binary_sensor.py b/homeassistant/components/aseko_pool_live/binary_sensor.py index e0b45ee6d4f..dbbdff38200 100644 --- a/homeassistant/components/aseko_pool_live/binary_sensor.py +++ b/homeassistant/components/aseko_pool_live/binary_sensor.py @@ -1,4 +1,5 @@ """Support for Aseko Pool Live binary sensors.""" + from __future__ import annotations from collections.abc import Callable @@ -20,20 +21,13 @@ from .coordinator import AsekoDataUpdateCoordinator from .entity import AsekoEntity -@dataclass(frozen=True) -class AsekoBinarySensorDescriptionMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class AsekoBinarySensorEntityDescription(BinarySensorEntityDescription): + """Describes an Aseko binary sensor entity.""" value_fn: Callable[[Unit], bool] -@dataclass(frozen=True) -class AsekoBinarySensorEntityDescription( - BinarySensorEntityDescription, AsekoBinarySensorDescriptionMixin -): - """Describes an Aseko binary sensor entity.""" - - UNIT_BINARY_SENSORS: tuple[AsekoBinarySensorEntityDescription, ...] = ( AsekoBinarySensorEntityDescription( key="water_flow", @@ -63,11 +57,11 @@ async def async_setup_entry( data: list[tuple[Unit, AsekoDataUpdateCoordinator]] = hass.data[DOMAIN][ config_entry.entry_id ] - entities: list[BinarySensorEntity] = [] - for unit, coordinator in data: - for description in UNIT_BINARY_SENSORS: - entities.append(AsekoUnitBinarySensorEntity(unit, coordinator, description)) - async_add_entities(entities) + async_add_entities( + AsekoUnitBinarySensorEntity(unit, coordinator, description) + for unit, coordinator in data + for description in UNIT_BINARY_SENSORS + ) class AsekoUnitBinarySensorEntity(AsekoEntity, BinarySensorEntity): diff --git a/homeassistant/components/aseko_pool_live/config_flow.py b/homeassistant/components/aseko_pool_live/config_flow.py index c8f96db3bc8..f4df44aa2d7 100644 --- a/homeassistant/components/aseko_pool_live/config_flow.py +++ b/homeassistant/components/aseko_pool_live/config_flow.py @@ -1,20 +1,16 @@ """Config flow for Aseko Pool Live integration.""" + from __future__ import annotations +from collections.abc import Mapping import logging from typing import Any -from aioaseko import APIUnavailable, InvalidAuthCredentials, MobileAccount, WebAccount +from aioaseko import APIUnavailable, InvalidAuthCredentials, WebAccount import voluptuous as vol -from homeassistant import config_entries -from homeassistant.const import ( - CONF_ACCESS_TOKEN, - CONF_EMAIL, - CONF_PASSWORD, - CONF_UNIQUE_ID, -) -from homeassistant.data_entry_flow import FlowResult +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult +from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_UNIQUE_ID from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN @@ -22,10 +18,19 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class AsekoConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Aseko Pool Live.""" - VERSION = 1 + VERSION = 2 + + data_schema = vol.Schema( + { + vol.Required(CONF_EMAIL): str, + vol.Required(CONF_PASSWORD): str, + } + ) + + reauth_entry: ConfigEntry | None = None async def get_account_info(self, email: str, password: str) -> dict: """Get account info from the mobile API and the web API.""" @@ -34,19 +39,83 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): web_account = WebAccount(session, email, password) web_account_info = await web_account.login() - mobile_account = MobileAccount(session, email, password) - await mobile_account.login() - return { - CONF_ACCESS_TOKEN: mobile_account.access_token, - CONF_EMAIL: web_account_info.email, + CONF_EMAIL: email, + CONF_PASSWORD: password, CONF_UNIQUE_ID: web_account_info.user_id, } async def async_step_user( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + self, user_input: Mapping[str, Any] | None = None + ) -> ConfigFlowResult: """Handle the initial step.""" + + self.reauth_entry = None + errors = {} + + if user_input is not None: + try: + info = await self.get_account_info( + user_input[CONF_EMAIL], user_input[CONF_PASSWORD] + ) + except APIUnavailable: + errors["base"] = "cannot_connect" + except InvalidAuthCredentials: + errors["base"] = "invalid_auth" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + return await self.async_store_credentials(info) + + return self.async_show_form( + step_id="user", + data_schema=self.data_schema, + errors=errors, + ) + + async def async_store_credentials(self, info: dict[str, Any]) -> ConfigFlowResult: + """Store validated credentials.""" + + if self.reauth_entry: + self.hass.config_entries.async_update_entry( + self.reauth_entry, + title=info[CONF_EMAIL], + data={ + CONF_EMAIL: info[CONF_EMAIL], + CONF_PASSWORD: info[CONF_PASSWORD], + }, + ) + await self.hass.config_entries.async_reload(self.reauth_entry.entry_id) + return self.async_abort(reason="reauth_successful") + + await self.async_set_unique_id(info[CONF_UNIQUE_ID]) + self._abort_if_unique_id_configured() + + return self.async_create_entry( + title=info[CONF_EMAIL], + data={ + CONF_EMAIL: info[CONF_EMAIL], + CONF_PASSWORD: info[CONF_PASSWORD], + }, + ) + + async def async_step_reauth( + self, user_input: Mapping[str, Any] + ) -> ConfigFlowResult: + """Perform reauth upon an API authentication error.""" + + self.reauth_entry = self.hass.config_entries.async_get_entry( + self.context["entry_id"] + ) + + return await self.async_step_reauth_confirm(user_input) + + async def async_step_reauth_confirm( + self, user_input: Mapping | None = None + ) -> ConfigFlowResult: + """Dialog that informs the user that reauth is required.""" + errors = {} if user_input is not None: try: @@ -61,21 +130,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" else: - await self.async_set_unique_id(info[CONF_UNIQUE_ID]) - self._abort_if_unique_id_configured() - - return self.async_create_entry( - title=info[CONF_EMAIL], - data={CONF_ACCESS_TOKEN: info[CONF_ACCESS_TOKEN]}, - ) + return await self.async_store_credentials(info) return self.async_show_form( - step_id="user", - data_schema=vol.Schema( - { - vol.Required(CONF_EMAIL): str, - vol.Required(CONF_PASSWORD): str, - } - ), + step_id="reauth_confirm", + data_schema=self.data_schema, errors=errors, ) diff --git a/homeassistant/components/aseko_pool_live/coordinator.py b/homeassistant/components/aseko_pool_live/coordinator.py index 383ab7116b6..a7f2d5ad5ac 100644 --- a/homeassistant/components/aseko_pool_live/coordinator.py +++ b/homeassistant/components/aseko_pool_live/coordinator.py @@ -1,4 +1,5 @@ """The Aseko Pool Live integration.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/aseko_pool_live/entity.py b/homeassistant/components/aseko_pool_live/entity.py index 1defbe18345..cd96b8f59a7 100644 --- a/homeassistant/components/aseko_pool_live/entity.py +++ b/homeassistant/components/aseko_pool_live/entity.py @@ -1,4 +1,5 @@ """Aseko entity.""" + from aioaseko import Unit from homeassistant.helpers.device_registry import DeviceInfo diff --git a/homeassistant/components/aseko_pool_live/manifest.json b/homeassistant/components/aseko_pool_live/manifest.json index 487032bb09d..f7c29277977 100644 --- a/homeassistant/components/aseko_pool_live/manifest.json +++ b/homeassistant/components/aseko_pool_live/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/aseko_pool_live", "iot_class": "cloud_polling", "loggers": ["aioaseko"], - "requirements": ["aioaseko==0.0.2"] + "requirements": ["aioaseko==0.1.1"] } diff --git a/homeassistant/components/aseko_pool_live/sensor.py b/homeassistant/components/aseko_pool_live/sensor.py index 55a40195750..a4ddea9ad89 100644 --- a/homeassistant/components/aseko_pool_live/sensor.py +++ b/homeassistant/components/aseko_pool_live/sensor.py @@ -1,4 +1,5 @@ """Support for Aseko Pool Live sensors.""" + from __future__ import annotations from aioaseko import Unit, Variable @@ -26,11 +27,12 @@ async def async_setup_entry( data: list[tuple[Unit, AsekoDataUpdateCoordinator]] = hass.data[DOMAIN][ config_entry.entry_id ] - entities = [] - for unit, coordinator in data: - for variable in unit.variables: - entities.append(VariableSensorEntity(unit, variable, coordinator)) - async_add_entities(entities) + + async_add_entities( + VariableSensorEntity(unit, variable, coordinator) + for unit, coordinator in data + for variable in unit.variables + ) class VariableSensorEntity(AsekoEntity, SensorEntity): diff --git a/homeassistant/components/aseko_pool_live/strings.json b/homeassistant/components/aseko_pool_live/strings.json index 2a6df30b148..7f77b9ec69b 100644 --- a/homeassistant/components/aseko_pool_live/strings.json +++ b/homeassistant/components/aseko_pool_live/strings.json @@ -6,6 +6,12 @@ "email": "[%key:common::config_flow::data::email%]", "password": "[%key:common::config_flow::data::password%]" } + }, + "reauth_confirm": { + "data": { + "email": "[%key:common::config_flow::data::email%]", + "password": "[%key:common::config_flow::data::password%]" + } } }, "error": { @@ -14,7 +20,8 @@ "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_account%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } }, "entity": { diff --git a/homeassistant/components/assist_pipeline/__init__.py b/homeassistant/components/assist_pipeline/__init__.py index a009cfb1095..f15657d5a91 100644 --- a/homeassistant/components/assist_pipeline/__init__.py +++ b/homeassistant/components/assist_pipeline/__init__.py @@ -1,4 +1,5 @@ """The Assist pipeline integration.""" + from __future__ import annotations from collections.abc import AsyncIterable diff --git a/homeassistant/components/assist_pipeline/const.py b/homeassistant/components/assist_pipeline/const.py index ef1ed1177a6..3463d94fb84 100644 --- a/homeassistant/components/assist_pipeline/const.py +++ b/homeassistant/components/assist_pipeline/const.py @@ -1,4 +1,5 @@ """Constants for the Assist pipeline integration.""" + DOMAIN = "assist_pipeline" DATA_CONFIG = f"{DOMAIN}.config" diff --git a/homeassistant/components/assist_pipeline/logbook.py b/homeassistant/components/assist_pipeline/logbook.py index 0c00c57adb9..50c5176bb22 100644 --- a/homeassistant/components/assist_pipeline/logbook.py +++ b/homeassistant/components/assist_pipeline/logbook.py @@ -1,4 +1,5 @@ """Describe assist_pipeline logbook events.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/assist_pipeline/pipeline.py b/homeassistant/components/assist_pipeline/pipeline.py index bf511f6cff5..01a12b3635b 100644 --- a/homeassistant/components/assist_pipeline/pipeline.py +++ b/homeassistant/components/assist_pipeline/pipeline.py @@ -1,4 +1,5 @@ """Classes for voice assistant pipelines.""" + from __future__ import annotations import array @@ -753,9 +754,9 @@ class PipelineRun: raise DuplicateWakeUpDetectedError(result.wake_word_phrase) # Record last wake up time to block duplicate detections - self.hass.data[DATA_LAST_WAKE_UP][ - result.wake_word_phrase - ] = time.monotonic() + self.hass.data[DATA_LAST_WAKE_UP][result.wake_word_phrase] = ( + time.monotonic() + ) if result.queued_audio: # Add audio that was pending at detection. @@ -764,12 +765,12 @@ class PipelineRun: # spoken, we need to make sure pending audio is forwarded to # speech-to-text so the user does not have to pause before # speaking the voice command. - for chunk_ts in result.queued_audio: - audio_chunks_for_stt.append( - ProcessedAudioChunk( - audio=chunk_ts[0], timestamp_ms=chunk_ts[1], is_speech=False - ) + audio_chunks_for_stt.extend( + ProcessedAudioChunk( + audio=chunk_ts[0], timestamp_ms=chunk_ts[1], is_speech=False ) + for chunk_ts in result.queued_audio + ) wake_word_output = asdict(result) @@ -1374,9 +1375,9 @@ class PipelineInput: raise DuplicateWakeUpDetectedError(self.wake_word_phrase) # Record last wake up time to block duplicate detections - self.run.hass.data[DATA_LAST_WAKE_UP][ - self.wake_word_phrase - ] = time.monotonic() + self.run.hass.data[DATA_LAST_WAKE_UP][self.wake_word_phrase] = ( + time.monotonic() + ) stt_input_stream = stt_processed_stream diff --git a/homeassistant/components/assist_pipeline/vad.py b/homeassistant/components/assist_pipeline/vad.py index 9cc5fe9dfc6..6dacd2ff8e9 100644 --- a/homeassistant/components/assist_pipeline/vad.py +++ b/homeassistant/components/assist_pipeline/vad.py @@ -1,4 +1,5 @@ """Voice activity detection.""" + from __future__ import annotations from abc import ABC, abstractmethod diff --git a/homeassistant/components/assist_pipeline/websocket_api.py b/homeassistant/components/assist_pipeline/websocket_api.py index f7a6d3c43fa..7550f860a9b 100644 --- a/homeassistant/components/assist_pipeline/websocket_api.py +++ b/homeassistant/components/assist_pipeline/websocket_api.py @@ -1,4 +1,5 @@ """Assist pipeline Websocket API.""" + import asyncio # Suppressing disable=deprecated-module is needed for Python 3.11 diff --git a/homeassistant/components/asterisk_cdr/mailbox.py b/homeassistant/components/asterisk_cdr/mailbox.py index 971b893ef6b..fde4826fcee 100644 --- a/homeassistant/components/asterisk_cdr/mailbox.py +++ b/homeassistant/components/asterisk_cdr/mailbox.py @@ -1,4 +1,5 @@ """Support for the Asterisk CDR interface.""" + from __future__ import annotations import datetime diff --git a/homeassistant/components/asterisk_mbox/__init__.py b/homeassistant/components/asterisk_mbox/__init__.py index 7fa1e1f14da..3e3913b7d42 100644 --- a/homeassistant/components/asterisk_mbox/__init__.py +++ b/homeassistant/components/asterisk_mbox/__init__.py @@ -1,4 +1,5 @@ """Support for Asterisk Voicemail interface.""" + import logging from typing import Any, cast diff --git a/homeassistant/components/asterisk_mbox/mailbox.py b/homeassistant/components/asterisk_mbox/mailbox.py index 95b3b7e3b15..14d54596eea 100644 --- a/homeassistant/components/asterisk_mbox/mailbox.py +++ b/homeassistant/components/asterisk_mbox/mailbox.py @@ -1,4 +1,5 @@ """Support for the Asterisk Voicemail interface.""" + from __future__ import annotations from functools import partial diff --git a/homeassistant/components/asuswrt/bridge.py b/homeassistant/components/asuswrt/bridge.py index cb04ccdec3f..35f3a98251f 100644 --- a/homeassistant/components/asuswrt/bridge.py +++ b/homeassistant/components/asuswrt/bridge.py @@ -1,4 +1,5 @@ """aioasuswrt and pyasuswrt bridge classes.""" + from __future__ import annotations from abc import ABC, abstractmethod diff --git a/homeassistant/components/asuswrt/config_flow.py b/homeassistant/components/asuswrt/config_flow.py index 1e320bdd72d..e456b1c55ba 100644 --- a/homeassistant/components/asuswrt/config_flow.py +++ b/homeassistant/components/asuswrt/config_flow.py @@ -14,7 +14,7 @@ from homeassistant.components.device_tracker import ( CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME, ) -from homeassistant.config_entries import ConfigEntry, ConfigFlow +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.const import ( CONF_BASE, CONF_HOST, @@ -25,7 +25,6 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_validation as cv from homeassistant.helpers.schema_config_entry_flow import ( SchemaCommonFlowHandler, @@ -139,7 +138,7 @@ class AsusWrtFlowHandler(ConfigFlow, domain=DOMAIN): self._config_data: dict[str, Any] = {} @callback - def _show_setup_form(self, error: str | None = None) -> FlowResult: + def _show_setup_form(self, error: str | None = None) -> ConfigFlowResult: """Show the setup form to the user.""" user_input = self._config_data @@ -228,7 +227,7 @@ class AsusWrtFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initiated by the user.""" # if there's one entry without unique ID, we abort config flow @@ -276,7 +275,7 @@ class AsusWrtFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_legacy( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow for legacy settings.""" if user_input is None: return self.async_show_form(step_id="legacy", data_schema=LEGACY_SCHEMA) @@ -284,7 +283,7 @@ class AsusWrtFlowHandler(ConfigFlow, domain=DOMAIN): self._config_data.update(user_input) return await self._async_save_entry() - async def _async_save_entry(self) -> FlowResult: + async def _async_save_entry(self) -> ConfigFlowResult: """Save entry data if unique id is valid.""" return self.async_create_entry( title=self._config_data[CONF_HOST], diff --git a/homeassistant/components/asuswrt/const.py b/homeassistant/components/asuswrt/const.py index a60046b50c2..d31d986574e 100644 --- a/homeassistant/components/asuswrt/const.py +++ b/homeassistant/components/asuswrt/const.py @@ -1,4 +1,5 @@ """AsusWrt component constants.""" + DOMAIN = "asuswrt" CONF_DNSMASQ = "dnsmasq" diff --git a/homeassistant/components/asuswrt/device_tracker.py b/homeassistant/components/asuswrt/device_tracker.py index fc0a9ee539e..059a0eeb3fb 100644 --- a/homeassistant/components/asuswrt/device_tracker.py +++ b/homeassistant/components/asuswrt/device_tracker.py @@ -1,4 +1,5 @@ """Support for ASUSWRT routers.""" + from __future__ import annotations from homeassistant.components.device_tracker import ScannerEntity, SourceType @@ -100,9 +101,9 @@ class AsusWrtDevice(ScannerEntity): self._device = self._router.devices[self._device.mac] self._attr_extra_state_attributes = {} if self._device.last_activity: - self._attr_extra_state_attributes[ - ATTR_LAST_TIME_REACHABLE - ] = self._device.last_activity.isoformat(timespec="seconds") + self._attr_extra_state_attributes[ATTR_LAST_TIME_REACHABLE] = ( + self._device.last_activity.isoformat(timespec="seconds") + ) self.async_write_ha_state() async def async_added_to_hass(self) -> None: diff --git a/homeassistant/components/asuswrt/diagnostics.py b/homeassistant/components/asuswrt/diagnostics.py index 0a3cc809c32..47ad1f29363 100644 --- a/homeassistant/components/asuswrt/diagnostics.py +++ b/homeassistant/components/asuswrt/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for Asuswrt.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/asuswrt/router.py b/homeassistant/components/asuswrt/router.py index d868065be47..ed97b1f6871 100644 --- a/homeassistant/components/asuswrt/router.py +++ b/homeassistant/components/asuswrt/router.py @@ -1,4 +1,5 @@ """Represent the AsusWrt router.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/asuswrt/sensor.py b/homeassistant/components/asuswrt/sensor.py index 3399071daa4..80da4b51f0a 100644 --- a/homeassistant/components/asuswrt/sensor.py +++ b/homeassistant/components/asuswrt/sensor.py @@ -1,4 +1,5 @@ """Asuswrt status sensors.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/homeassistant/components/atag/__init__.py b/homeassistant/components/atag/__init__.py index b0cc83ab88e..85732485165 100644 --- a/homeassistant/components/atag/__init__.py +++ b/homeassistant/components/atag/__init__.py @@ -1,4 +1,5 @@ """The ATAG Integration.""" + from asyncio import timeout from datetime import timedelta import logging diff --git a/homeassistant/components/atag/climate.py b/homeassistant/components/atag/climate.py index a5f119e3a2b..ff66839926f 100644 --- a/homeassistant/components/atag/climate.py +++ b/homeassistant/components/atag/climate.py @@ -1,4 +1,5 @@ """Initialization of ATAG One climate platform.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/atag/config_flow.py b/homeassistant/components/atag/config_flow.py index 8dd7020acfb..c1a78da2ac0 100644 --- a/homeassistant/components/atag/config_flow.py +++ b/homeassistant/components/atag/config_flow.py @@ -1,12 +1,12 @@ """Config flow for the Atag component.""" + from typing import Any import pyatag import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_PORT -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from . import DOMAIN @@ -17,14 +17,14 @@ DATA_SCHEMA = { } -class AtagConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class AtagConfigFlow(ConfigFlow, domain=DOMAIN): """Config flow for Atag.""" VERSION = 1 async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" if not user_input: @@ -44,7 +44,9 @@ class AtagConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_create_entry(title=atag.id, data=user_input) - async def _show_form(self, errors: dict[str, str] | None = None) -> FlowResult: + async def _show_form( + self, errors: dict[str, str] | None = None + ) -> ConfigFlowResult: """Show the form to the user.""" return self.async_show_form( step_id="user", diff --git a/homeassistant/components/atag/sensor.py b/homeassistant/components/atag/sensor.py index a006d1dfe05..25a3de34556 100644 --- a/homeassistant/components/atag/sensor.py +++ b/homeassistant/components/atag/sensor.py @@ -1,4 +1,5 @@ """Initialization of ATAG One sensor platform.""" + from homeassistant.components.sensor import SensorDeviceClass, SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( diff --git a/homeassistant/components/atag/water_heater.py b/homeassistant/components/atag/water_heater.py index 8976a3f78ec..8bae3df7436 100644 --- a/homeassistant/components/atag/water_heater.py +++ b/homeassistant/components/atag/water_heater.py @@ -1,4 +1,5 @@ """ATAG water heater component.""" + from typing import Any from homeassistant.components.water_heater import ( diff --git a/homeassistant/components/aten_pe/switch.py b/homeassistant/components/aten_pe/switch.py index 3293a3e7a09..1349014d8fb 100644 --- a/homeassistant/components/aten_pe/switch.py +++ b/homeassistant/components/aten_pe/switch.py @@ -1,4 +1,5 @@ """The ATEN PE switch component.""" + from __future__ import annotations import logging @@ -79,11 +80,9 @@ async def async_setup_platform( sw_version=sw_version, ) - switches = [] - async for outlet in outlets: - switches.append(AtenSwitch(dev, info, mac, outlet.id, outlet.name)) - - async_add_entities(switches, True) + async_add_entities( + (AtenSwitch(dev, info, mac, outlet.id, outlet.name) for outlet in outlets), True + ) class AtenSwitch(SwitchEntity): diff --git a/homeassistant/components/atome/sensor.py b/homeassistant/components/atome/sensor.py index 37a8fd4460f..84751b84855 100644 --- a/homeassistant/components/atome/sensor.py +++ b/homeassistant/components/atome/sensor.py @@ -1,4 +1,5 @@ """Linky Atome.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/august/__init__.py b/homeassistant/components/august/__init__.py index fe16819bf9c..2db2b173f6b 100644 --- a/homeassistant/components/august/__init__.py +++ b/homeassistant/components/august/__init__.py @@ -1,4 +1,5 @@ """Support for August devices.""" + from __future__ import annotations import asyncio @@ -305,6 +306,13 @@ class AugustData(AugustSubscriberMixin): exc_info=err, ) + async def refresh_camera_by_id(self, device_id: str) -> None: + """Re-fetch doorbell/camera data from API.""" + await self._async_update_device_detail( + self._doorbells_by_id[device_id], + self._api.async_get_doorbell_detail, + ) + async def _async_refresh_device_detail_by_id(self, device_id: str) -> None: if device_id in self._locks_by_id: if self.activity_stream and self.activity_stream.pubnub.connected: diff --git a/homeassistant/components/august/activity.py b/homeassistant/components/august/activity.py index 9a41d9bad81..ae920383e40 100644 --- a/homeassistant/components/august/activity.py +++ b/homeassistant/components/august/activity.py @@ -1,4 +1,5 @@ """Consume the august activity stream.""" + from __future__ import annotations from datetime import datetime @@ -78,6 +79,7 @@ class ActivityStream(AugustSubscriberMixin): cooldown=ACTIVITY_DEBOUNCE_COOLDOWN, immediate=True, function=partial(self._async_update_house_id, house_id), + background=True, ) update_debounce[house_id] = debouncer update_debounce_jobs[house_id] = HassJob( diff --git a/homeassistant/components/august/binary_sensor.py b/homeassistant/components/august/binary_sensor.py index 3c2ea5b3faa..14b9dca9b7d 100644 --- a/homeassistant/components/august/binary_sensor.py +++ b/homeassistant/components/august/binary_sensor.py @@ -1,4 +1,5 @@ """Support for August binary sensors.""" + from __future__ import annotations from collections.abc import Callable @@ -105,27 +106,15 @@ def _native_datetime() -> datetime: return datetime.now() -@dataclass(frozen=True) -class AugustBinarySensorEntityDescription(BinarySensorEntityDescription): +@dataclass(frozen=True, kw_only=True) +class AugustDoorbellBinarySensorEntityDescription(BinarySensorEntityDescription): """Describes August binary_sensor entity.""" - -@dataclass(frozen=True) -class AugustDoorbellRequiredKeysMixin: - """Mixin for required keys.""" - value_fn: Callable[[AugustData, DoorbellDetail], bool] is_time_based: bool -@dataclass(frozen=True) -class AugustDoorbellBinarySensorEntityDescription( - BinarySensorEntityDescription, AugustDoorbellRequiredKeysMixin -): - """Describes August binary_sensor entity.""" - - -SENSOR_TYPE_DOOR = AugustBinarySensorEntityDescription( +SENSOR_TYPE_DOOR = BinarySensorEntityDescription( key="open", device_class=BinarySensorDeviceClass.DOOR, ) @@ -217,7 +206,7 @@ class AugustDoorBinarySensor(AugustEntityMixin, BinarySensorEntity): self, data: AugustData, device: Lock, - description: AugustBinarySensorEntityDescription, + description: BinarySensorEntityDescription, ) -> None: """Initialize the sensor.""" super().__init__(data, device) diff --git a/homeassistant/components/august/button.py b/homeassistant/components/august/button.py index 3997a2d72bf..579f0012223 100644 --- a/homeassistant/components/august/button.py +++ b/homeassistant/components/august/button.py @@ -1,4 +1,5 @@ """Support for August buttons.""" + from yalexs.lock import Lock from homeassistant.components.button import ButtonEntity diff --git a/homeassistant/components/august/camera.py b/homeassistant/components/august/camera.py index e5835a69e07..188a55bd4b9 100644 --- a/homeassistant/components/august/camera.py +++ b/homeassistant/components/august/camera.py @@ -1,9 +1,13 @@ """Support for August doorbell camera.""" + from __future__ import annotations +import logging + from aiohttp import ClientSession from yalexs.activity import ActivityType -from yalexs.doorbell import Doorbell +from yalexs.const import Brand +from yalexs.doorbell import ContentTokenExpired, Doorbell from yalexs.util import update_doorbell_image_from_activity from homeassistant.components.camera import Camera @@ -16,6 +20,8 @@ from . import AugustData from .const import DEFAULT_NAME, DEFAULT_TIMEOUT, DOMAIN from .entity import AugustEntityMixin +_LOGGER = logging.getLogger(__name__) + async def async_setup_entry( hass: HomeAssistant, @@ -47,6 +53,7 @@ class AugustCamera(AugustEntityMixin, Camera): self._timeout = timeout self._session = session self._image_url = None + self._content_token = None self._image_content = None self._attr_unique_id = f"{self._device_id:s}_camera" self._attr_motion_detection_enabled = True @@ -62,6 +69,12 @@ class AugustCamera(AugustEntityMixin, Camera): """Return the camera model.""" return self._detail.model + async def _async_update(self): + """Update device.""" + _LOGGER.debug("async_update called %s", self._detail.device_name) + await self._data.refresh_camera_by_id(self._device_id) + self._update_from_data() + @callback def _update_from_data(self) -> None: """Get the latest state of the sensor.""" @@ -69,7 +82,6 @@ class AugustCamera(AugustEntityMixin, Camera): self._device_id, {ActivityType.DOORBELL_MOTION, ActivityType.DOORBELL_IMAGE_CAPTURE}, ) - if doorbell_activity is not None: update_doorbell_image_from_activity(self._detail, doorbell_activity) @@ -81,7 +93,23 @@ class AugustCamera(AugustEntityMixin, Camera): if self._image_url is not self._detail.image_url: self._image_url = self._detail.image_url - self._image_content = await self._detail.async_get_doorbell_image( - self._session, timeout=self._timeout + self._content_token = self._detail.content_token or self._content_token + _LOGGER.debug( + "calling doorbell async_get_doorbell_image, %s", + self._detail.device_name, ) + try: + self._image_content = await self._detail.async_get_doorbell_image( + self._session, timeout=self._timeout + ) + except ContentTokenExpired: + if self._data.brand == Brand.YALE_HOME: + _LOGGER.debug( + "Error fetching camera image, updating content-token from api to retry" + ) + await self._async_update() + self._image_content = await self._detail.async_get_doorbell_image( + self._session, timeout=self._timeout + ) + return self._image_content diff --git a/homeassistant/components/august/config_flow.py b/homeassistant/components/august/config_flow.py index 8aaf1b1a05b..e6803da2ae0 100644 --- a/homeassistant/components/august/config_flow.py +++ b/homeassistant/components/august/config_flow.py @@ -1,4 +1,5 @@ """Config flow for August integration.""" + from collections.abc import Mapping from dataclasses import dataclass import logging @@ -9,10 +10,9 @@ import voluptuous as vol from yalexs.authenticator import ValidationResult from yalexs.const import BRANDS, DEFAULT_BRAND -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult from .const import ( CONF_ACCESS_TOKEN_CACHE_FILE, @@ -75,7 +75,7 @@ class ValidateResult: description_placeholders: dict[str, str] -class AugustConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class AugustConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for August.""" VERSION = 1 @@ -91,13 +91,13 @@ class AugustConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" return await self.async_step_user_validate() async def async_step_user_validate( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle authentication.""" errors: dict[str, str] = {} description_placeholders: dict[str, str] = {} @@ -137,7 +137,7 @@ class AugustConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_validation( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle validation (2fa) step.""" if user_input: if self._mode == "reauth": @@ -174,7 +174,9 @@ class AugustConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._aiohttp_session.detach() self._august_gateway = None - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Handle configuration by re-auth.""" self._user_auth_details = dict(entry_data) self._mode = "reauth" @@ -183,7 +185,7 @@ class AugustConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_reauth_validate( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle reauth and validation.""" errors: dict[str, str] = {} description_placeholders: dict[str, str] = {} @@ -261,7 +263,9 @@ class AugustConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): validation_required, info, errors, description_placeholders ) - async def _async_update_or_create_entry(self, info: dict[str, Any]) -> FlowResult: + async def _async_update_or_create_entry( + self, info: dict[str, Any] + ) -> ConfigFlowResult: """Update existing entry or create a new one.""" self._async_shutdown_gateway() diff --git a/homeassistant/components/august/diagnostics.py b/homeassistant/components/august/diagnostics.py index 57e56795c2d..a1f76bf690b 100644 --- a/homeassistant/components/august/diagnostics.py +++ b/homeassistant/components/august/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for august.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/august/entity.py b/homeassistant/components/august/entity.py index bcd2c6e2503..47cb966bdc1 100644 --- a/homeassistant/components/august/entity.py +++ b/homeassistant/components/august/entity.py @@ -1,4 +1,5 @@ """Base class for August entity.""" + from abc import abstractmethod from yalexs.doorbell import Doorbell, DoorbellDetail diff --git a/homeassistant/components/august/lock.py b/homeassistant/components/august/lock.py index 93e0de018b0..e711edd6893 100644 --- a/homeassistant/components/august/lock.py +++ b/homeassistant/components/august/lock.py @@ -1,4 +1,5 @@ """Support for August lock.""" + from __future__ import annotations from collections.abc import Callable, Coroutine @@ -140,9 +141,9 @@ class AugustLock(AugustEntityMixin, RestoreEntity, LockEntity): ATTR_BATTERY_LEVEL: self._detail.battery_level } if self._detail.keypad is not None: - self._attr_extra_state_attributes[ - "keypad_battery_level" - ] = self._detail.keypad.battery_level + self._attr_extra_state_attributes["keypad_battery_level"] = ( + self._detail.keypad.battery_level + ) async def async_added_to_hass(self) -> None: """Restore ATTR_CHANGED_BY on startup since it is likely no longer in the activity log.""" diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index ec4eb77605c..27c5f11ec6e 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -26,8 +26,7 @@ } ], "documentation": "https://www.home-assistant.io/integrations/august", - "import_executor": true, "iot_class": "cloud_push", "loggers": ["pubnub", "yalexs"], - "requirements": ["yalexs==1.11.4", "yalexs-ble==2.4.2"] + "requirements": ["yalexs==2.0.0", "yalexs-ble==2.4.2"] } diff --git a/homeassistant/components/august/sensor.py b/homeassistant/components/august/sensor.py index 2cf0bb36d08..6ccdccfce7d 100644 --- a/homeassistant/components/august/sensor.py +++ b/homeassistant/components/august/sensor.py @@ -1,4 +1,5 @@ """Support for August sensors.""" + from __future__ import annotations from collections.abc import Callable @@ -150,8 +151,7 @@ async def async_setup_entry( entities.append(keypad_battery_sensor) migrate_unique_id_devices.append(keypad_battery_sensor) - for device in operation_sensors: - entities.append(AugustOperatorSensor(data, device)) + entities.extend(AugustOperatorSensor(data, device) for device in operation_sensors) await _async_migrate_old_unique_ids(hass, migrate_unique_id_devices) diff --git a/homeassistant/components/august/subscriber.py b/homeassistant/components/august/subscriber.py index f2096506c4a..e800b5cb604 100644 --- a/homeassistant/components/august/subscriber.py +++ b/homeassistant/components/august/subscriber.py @@ -1,4 +1,5 @@ """Base class for August entity.""" + from __future__ import annotations from abc import abstractmethod @@ -65,7 +66,9 @@ class AugustSubscriberMixin: self._unsub_interval() self._stop_interval = self._hass.bus.async_listen( - EVENT_HOMEASSISTANT_STOP, _async_cancel_update_interval + EVENT_HOMEASSISTANT_STOP, + _async_cancel_update_interval, + run_immediately=True, ) @callback diff --git a/homeassistant/components/aurora/binary_sensor.py b/homeassistant/components/aurora/binary_sensor.py index 94e1f3fc2da..5c9166a0f60 100644 --- a/homeassistant/components/aurora/binary_sensor.py +++ b/homeassistant/components/aurora/binary_sensor.py @@ -1,4 +1,5 @@ """Support for Aurora Forecast binary sensor.""" + from __future__ import annotations from homeassistant.components.binary_sensor import BinarySensorEntity diff --git a/homeassistant/components/aurora/config_flow.py b/homeassistant/components/aurora/config_flow.py index a1971884ead..744624c2eb8 100644 --- a/homeassistant/components/aurora/config_flow.py +++ b/homeassistant/components/aurora/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Aurora.""" + from __future__ import annotations import logging @@ -8,10 +9,9 @@ from aiohttp import ClientError from auroranoaa import AuroraForecast import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.helpers.schema_config_entry_flow import ( SchemaFlowFormStep, @@ -34,7 +34,7 @@ OPTIONS_FLOW = { } -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class AuroraConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for NOAA Aurora Integration.""" VERSION = 1 @@ -42,14 +42,14 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback def async_get_options_flow( - config_entry: config_entries.ConfigEntry, + config_entry: ConfigEntry, ) -> SchemaOptionsFlowHandler: """Get the options flow for this handler.""" return SchemaOptionsFlowHandler(config_entry, OPTIONS_FLOW) async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors: dict[str, str] = {} diff --git a/homeassistant/components/aurora/coordinator.py b/homeassistant/components/aurora/coordinator.py index 8195f6d30ec..ae1101f8054 100644 --- a/homeassistant/components/aurora/coordinator.py +++ b/homeassistant/components/aurora/coordinator.py @@ -1,4 +1,5 @@ """The aurora component.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/aurora/sensor.py b/homeassistant/components/aurora/sensor.py index 7801a84d58b..e3ae9f9cf1b 100644 --- a/homeassistant/components/aurora/sensor.py +++ b/homeassistant/components/aurora/sensor.py @@ -1,4 +1,5 @@ """Support for Aurora Forecast sensor.""" + from __future__ import annotations from homeassistant.components.sensor import SensorEntity, SensorStateClass diff --git a/homeassistant/components/aurora_abb_powerone/__init__.py b/homeassistant/components/aurora_abb_powerone/__init__.py index c7400f31727..8d236b30d97 100644 --- a/homeassistant/components/aurora_abb_powerone/__init__.py +++ b/homeassistant/components/aurora_abb_powerone/__init__.py @@ -12,14 +12,12 @@ import logging -from aurorapy.client import AuroraError, AuroraSerialClient, AuroraTimeoutError - from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ADDRESS, CONF_PORT, Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .const import DOMAIN, SCAN_INTERVAL +from .const import DOMAIN +from .coordinator import AuroraAbbDataUpdateCoordinator PLATFORMS = [Platform.SENSOR] @@ -50,60 +48,3 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN].pop(entry.entry_id) return unload_ok - - -class AuroraAbbDataUpdateCoordinator(DataUpdateCoordinator[dict[str, float]]): # pylint: disable=hass-enforce-coordinator-module - """Class to manage fetching AuroraAbbPowerone data.""" - - def __init__(self, hass: HomeAssistant, comport: str, address: int) -> None: - """Initialize the data update coordinator.""" - self.available_prev = False - self.available = False - self.client = AuroraSerialClient(address, comport, parity="N", timeout=1) - super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=SCAN_INTERVAL) - - def _update_data(self) -> dict[str, float]: - """Fetch new state data for the sensor. - - This is the only function that should fetch new data for Home Assistant. - """ - data: dict[str, float] = {} - self.available_prev = self.available - try: - self.client.connect() - - # read ADC channel 3 (grid power output) - power_watts = self.client.measure(3, True) - temperature_c = self.client.measure(21) - energy_wh = self.client.cumulated_energy(5) - [alarm, *_] = self.client.alarms() - except AuroraTimeoutError: - self.available = False - _LOGGER.debug("No response from inverter (could be dark)") - except AuroraError as error: - self.available = False - raise error - else: - data["instantaneouspower"] = round(power_watts, 1) - data["temp"] = round(temperature_c, 1) - data["totalenergy"] = round(energy_wh / 1000, 2) - data["alarm"] = alarm - self.available = True - - finally: - if self.available != self.available_prev: - if self.available: - _LOGGER.info("Communication with %s back online", self.name) - else: - _LOGGER.warning( - "Communication with %s lost", - self.name, - ) - if self.client.serline.isOpen(): - self.client.close() - - return data - - async def _async_update_data(self) -> dict[str, float]: - """Update inverter data in the executor.""" - return await self.hass.async_add_executor_job(self._update_data) diff --git a/homeassistant/components/aurora_abb_powerone/config_flow.py b/homeassistant/components/aurora_abb_powerone/config_flow.py index 32295c3bf47..3f635595258 100644 --- a/homeassistant/components/aurora_abb_powerone/config_flow.py +++ b/homeassistant/components/aurora_abb_powerone/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Aurora ABB PowerOne integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -9,9 +10,9 @@ from aurorapy.client import AuroraError, AuroraSerialClient import serial.tools.list_ports import voluptuous as vol -from homeassistant import config_entries, core +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import ATTR_SERIAL_NUMBER, CONF_ADDRESS, CONF_PORT -from homeassistant.data_entry_flow import FlowResult +from homeassistant.core import HomeAssistant from .const import ( ATTR_FIRMWARE, @@ -27,7 +28,7 @@ _LOGGER = logging.getLogger(__name__) def validate_and_connect( - hass: core.HomeAssistant, data: Mapping[str, Any] + hass: HomeAssistant, data: Mapping[str, Any] ) -> dict[str, str]: """Validate the user input allows us to connect. @@ -69,7 +70,7 @@ def scan_comports() -> tuple[list[str] | None, str | None]: return None, None -class AuroraABBConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class AuroraABBConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Aurora ABB PowerOne.""" VERSION = 1 @@ -82,7 +83,7 @@ class AuroraABBConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialised by the user.""" errors = {} diff --git a/homeassistant/components/aurora_abb_powerone/coordinator.py b/homeassistant/components/aurora_abb_powerone/coordinator.py new file mode 100644 index 00000000000..d6e9b241b86 --- /dev/null +++ b/homeassistant/components/aurora_abb_powerone/coordinator.py @@ -0,0 +1,94 @@ +"""DataUpdateCoordinator for the aurora_abb_powerone integration.""" + +import logging +from time import sleep + +from aurorapy.client import AuroraError, AuroraSerialClient, AuroraTimeoutError +from serial import SerialException + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DOMAIN, SCAN_INTERVAL + +_LOGGER = logging.getLogger(__name__) + + +class AuroraAbbDataUpdateCoordinator(DataUpdateCoordinator[dict[str, float]]): # pylint: disable=hass-enforce-coordinator-module + """Class to manage fetching AuroraAbbPowerone data.""" + + def __init__(self, hass: HomeAssistant, comport: str, address: int) -> None: + """Initialize the data update coordinator.""" + self.available_prev = False + self.available = False + self.client = AuroraSerialClient(address, comport, parity="N", timeout=1) + super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=SCAN_INTERVAL) + + def _update_data(self) -> dict[str, float]: + """Fetch new state data for the sensors. + + This is the only function that should fetch new data for Home Assistant. + """ + data: dict[str, float] = {} + self.available_prev = self.available + retries: int = 3 + while retries > 0: + try: + self.client.connect() + + # See command 59 in the protocol manual linked in __init__.py + grid_voltage = self.client.measure(1, True) + grid_current = self.client.measure(2, True) + power_watts = self.client.measure(3, True) + frequency = self.client.measure(4) + i_leak_dcdc = self.client.measure(6) + i_leak_inverter = self.client.measure(7) + temperature_c = self.client.measure(21) + r_iso = self.client.measure(30) + energy_wh = self.client.cumulated_energy(5) + [alarm, *_] = self.client.alarms() + except AuroraTimeoutError: + self.available = False + _LOGGER.debug("No response from inverter (could be dark)") + retries = 0 + except (SerialException, AuroraError) as error: + self.available = False + retries -= 1 + if retries <= 0: + raise UpdateFailed(error) from error + _LOGGER.debug( + "Exception: %s occurred, %d retries remaining", + repr(error), + retries, + ) + sleep(1) + else: + data["grid_voltage"] = round(grid_voltage, 1) + data["grid_current"] = round(grid_current, 1) + data["instantaneouspower"] = round(power_watts, 1) + data["grid_frequency"] = round(frequency, 1) + data["i_leak_dcdc"] = i_leak_dcdc + data["i_leak_inverter"] = i_leak_inverter + data["temp"] = round(temperature_c, 1) + data["r_iso"] = r_iso + data["totalenergy"] = round(energy_wh / 1000, 2) + data["alarm"] = alarm + self.available = True + retries = 0 + finally: + if self.available != self.available_prev: + if self.available: + _LOGGER.info("Communication with %s back online", self.name) + else: + _LOGGER.info( + "Communication with %s lost", + self.name, + ) + if self.client.serline.isOpen(): + self.client.close() + + return data + + async def _async_update_data(self) -> dict[str, float]: + """Update inverter data in the executor.""" + return await self.hass.async_add_executor_job(self._update_data) diff --git a/homeassistant/components/aurora_abb_powerone/icons.json b/homeassistant/components/aurora_abb_powerone/icons.json new file mode 100644 index 00000000000..3a097df67ce --- /dev/null +++ b/homeassistant/components/aurora_abb_powerone/icons.json @@ -0,0 +1,12 @@ +{ + "entity": { + "sensor": { + "r_iso": { + "default": "mdi:omega" + }, + "alarm": { + "default": "mdi:alert-octagon" + } + } + } +} diff --git a/homeassistant/components/aurora_abb_powerone/sensor.py b/homeassistant/components/aurora_abb_powerone/sensor.py index 2ca7fa3e7ef..6e3ebb5f5c9 100644 --- a/homeassistant/components/aurora_abb_powerone/sensor.py +++ b/homeassistant/components/aurora_abb_powerone/sensor.py @@ -1,4 +1,5 @@ """Support for Aurora ABB PowerOne Solar Photovoltaic (PV) inverter.""" + from __future__ import annotations from collections.abc import Mapping @@ -17,7 +18,10 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_SERIAL_NUMBER, EntityCategory, + UnitOfElectricCurrent, + UnitOfElectricPotential, UnitOfEnergy, + UnitOfFrequency, UnitOfPower, UnitOfTemperature, ) @@ -41,6 +45,50 @@ _LOGGER = logging.getLogger(__name__) ALARM_STATES = list(AuroraMapping.ALARM_STATES.values()) SENSOR_TYPES = [ + SensorEntityDescription( + key="grid_voltage", + device_class=SensorDeviceClass.VOLTAGE, + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + state_class=SensorStateClass.MEASUREMENT, + translation_key="grid_voltage", + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="grid_current", + device_class=SensorDeviceClass.CURRENT, + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + state_class=SensorStateClass.MEASUREMENT, + translation_key="grid_current", + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="grid_frequency", + device_class=SensorDeviceClass.FREQUENCY, + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=UnitOfFrequency.HERTZ, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="i_leak_dcdc", + device_class=SensorDeviceClass.CURRENT, + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + state_class=SensorStateClass.MEASUREMENT, + translation_key="i_leak_dcdc", + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="i_leak_inverter", + device_class=SensorDeviceClass.CURRENT, + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + state_class=SensorStateClass.MEASUREMENT, + translation_key="i_leak_inverter", + entity_registry_enabled_default=False, + ), SensorEntityDescription( key="alarm", device_class=SensorDeviceClass.ENUM, @@ -62,6 +110,14 @@ SENSOR_TYPES = [ native_unit_of_measurement=UnitOfTemperature.CELSIUS, state_class=SensorStateClass.MEASUREMENT, ), + SensorEntityDescription( + key="r_iso", + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement="MOhms", + state_class=SensorStateClass.MEASUREMENT, + translation_key="r_iso", + entity_registry_enabled_default=False, + ), SensorEntityDescription( key="totalenergy", device_class=SensorDeviceClass.ENERGY, @@ -78,13 +134,11 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up aurora_abb_powerone sensor based on a config entry.""" - entities = [] coordinator = hass.data[DOMAIN][config_entry.entry_id] data = config_entry.data - for sens in SENSOR_TYPES: - entities.append(AuroraSensor(coordinator, data, sens)) + entities = [AuroraSensor(coordinator, data, sens) for sens in SENSOR_TYPES] _LOGGER.debug("async_setup_entry adding %d entities", len(entities)) async_add_entities(entities, True) diff --git a/homeassistant/components/aurora_abb_powerone/strings.json b/homeassistant/components/aurora_abb_powerone/strings.json index 63ea1cfefd4..319bcb0adc4 100644 --- a/homeassistant/components/aurora_abb_powerone/strings.json +++ b/homeassistant/components/aurora_abb_powerone/strings.json @@ -21,14 +21,29 @@ }, "entity": { "sensor": { + "grid_voltage": { + "name": "Grid voltage" + }, + "grid_current": { + "name": "Grid current" + }, "alarm": { "name": "Alarm status" }, "power_output": { "name": "Power output" }, + "i_leak_dcdc": { + "name": "DC-DC leak current" + }, + "i_leak_inverter": { + "name": "Inverter leak current" + }, "total_energy": { "name": "Total energy" + }, + "r_iso": { + "name": "Isolation resistance" } } } diff --git a/homeassistant/components/aussie_broadband/__init__.py b/homeassistant/components/aussie_broadband/__init__.py index e65abaebb98..1fc7e47ebde 100644 --- a/homeassistant/components/aussie_broadband/__init__.py +++ b/homeassistant/components/aussie_broadband/__init__.py @@ -1,4 +1,5 @@ """The Aussie Broadband integration.""" + from __future__ import annotations from datetime import timedelta @@ -32,15 +33,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) # Ignore services that don't support usage data - ignore_types = FETCH_TYPES + ["Hardware"] + ignore_types = [*FETCH_TYPES, "Hardware"] try: await client.login() services = await client.get_services(drop_types=ignore_types) except AuthenticationException as exc: - raise ConfigEntryAuthFailed() from exc + raise ConfigEntryAuthFailed from exc except ClientError as exc: - raise ConfigEntryNotReady() from exc + raise ConfigEntryNotReady from exc # Create an appropriate refresh function def update_data_factory(service_id): diff --git a/homeassistant/components/aussie_broadband/config_flow.py b/homeassistant/components/aussie_broadband/config_flow.py index c71570b73fb..587c7df2b36 100644 --- a/homeassistant/components/aussie_broadband/config_flow.py +++ b/homeassistant/components/aussie_broadband/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Aussie Broadband integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -9,15 +10,14 @@ from aussiebb.asyncio import AussieBB, AuthenticationException from aussiebb.const import FETCH_TYPES import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import CONF_SERVICES, DOMAIN -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class AussieBroadbandConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Aussie Broadband.""" VERSION = 1 @@ -47,7 +47,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors: dict[str, str] | None = None if user_input is not None: @@ -77,7 +77,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Handle reauth on credential failure.""" self._reauth_username = entry_data[CONF_USERNAME] @@ -85,7 +87,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_reauth_confirm( self, user_input: dict[str, str] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle users reauth credentials.""" errors: dict[str, str] | None = None diff --git a/homeassistant/components/aussie_broadband/const.py b/homeassistant/components/aussie_broadband/const.py index 5747ee86fea..ad19b7d8a27 100644 --- a/homeassistant/components/aussie_broadband/const.py +++ b/homeassistant/components/aussie_broadband/const.py @@ -1,4 +1,5 @@ """Constants for the Aussie Broadband integration.""" + DEFAULT_UPDATE_INTERVAL = 30 DOMAIN = "aussie_broadband" SERVICE_ID = "service_id" diff --git a/homeassistant/components/aussie_broadband/diagnostics.py b/homeassistant/components/aussie_broadband/diagnostics.py index f4e95a99f56..c71cfd090da 100644 --- a/homeassistant/components/aussie_broadband/diagnostics.py +++ b/homeassistant/components/aussie_broadband/diagnostics.py @@ -1,4 +1,5 @@ """Provides diagnostics for Aussie Broadband.""" + from __future__ import annotations from typing import Any @@ -16,13 +17,12 @@ async def async_get_config_entry_diagnostics( hass: HomeAssistant, config_entry: ConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - services = [] - for service in hass.data[DOMAIN][config_entry.entry_id]["services"]: - services.append( + return { + "services": [ { "service": async_redact_data(service, TO_REDACT), "usage": async_redact_data(service["coordinator"].data, ["historical"]), } - ) - - return {"services": services} + for service in hass.data[DOMAIN][config_entry.entry_id]["services"] + ] + } diff --git a/homeassistant/components/aussie_broadband/sensor.py b/homeassistant/components/aussie_broadband/sensor.py index d92ba503412..49796b3f6cd 100644 --- a/homeassistant/components/aussie_broadband/sensor.py +++ b/homeassistant/components/aussie_broadband/sensor.py @@ -1,4 +1,5 @@ """Support for Aussie Broadband metric sensors.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/auth/__init__.py b/homeassistant/components/auth/__init__.py index dd07e137e5e..d0e605e7c1e 100644 --- a/homeassistant/components/auth/__init__.py +++ b/homeassistant/components/auth/__init__.py @@ -122,6 +122,7 @@ This is an endpoint for OAuth2 Authorization callbacks used by integrations that link accounts with other cloud providers using LocalOAuth2Implementation as part of a config flow. """ + from __future__ import annotations from collections.abc import Callable @@ -143,6 +144,7 @@ from homeassistant.auth.models import ( User, ) from homeassistant.components import websocket_api +from homeassistant.components.http import KEY_HASS from homeassistant.components.http.auth import ( async_sign_path, async_user_not_allowed_do_auth, @@ -209,7 +211,7 @@ class RevokeTokenView(HomeAssistantView): async def post(self, request: web.Request) -> web.Response: """Revoke a token.""" - hass: HomeAssistant = request.app["hass"] + hass = request.app[KEY_HASS] data = cast(MultiDictProxy[str], await request.post()) # OAuth 2.0 Token Revocation [RFC7009] @@ -243,7 +245,7 @@ class TokenView(HomeAssistantView): @log_invalid_auth async def post(self, request: web.Request) -> web.Response: """Grant a token.""" - hass: HomeAssistant = request.app["hass"] + hass = request.app[KEY_HASS] data = cast(MultiDictProxy[str], await request.post()) grant_type = data.get("grant_type") @@ -415,7 +417,7 @@ class LinkUserView(HomeAssistantView): @RequestDataValidator(vol.Schema({"code": str, "client_id": str})) async def post(self, request: web.Request, data: dict[str, Any]) -> web.Response: """Link a user.""" - hass: HomeAssistant = request.app["hass"] + hass = request.app[KEY_HASS] user: User = request["hass_user"] credentials = self._retrieve_credentials(data["client_id"], data["code"]) diff --git a/homeassistant/components/auth/indieauth.py b/homeassistant/components/auth/indieauth.py index cf7f38fa32a..232f067b673 100644 --- a/homeassistant/components/auth/indieauth.py +++ b/homeassistant/components/auth/indieauth.py @@ -1,4 +1,5 @@ """Helpers to resolve client ID/secret.""" + from __future__ import annotations from html.parser import HTMLParser @@ -91,9 +92,10 @@ async def fetch_redirect_uris(hass: HomeAssistant, url: str) -> list[str]: parser = LinkTagParser("redirect_uri") chunks = 0 try: - async with aiohttp.ClientSession() as session, session.get( - url, timeout=5 - ) as resp: + async with ( + aiohttp.ClientSession() as session, + session.get(url, timeout=5) as resp, + ): async for data in resp.content.iter_chunked(1024): parser.feed(data.decode()) chunks += 1 @@ -157,7 +159,7 @@ def _parse_client_id(client_id: str) -> ParseResult: # Client identifier URLs # MUST have either an https or http scheme if parts.scheme not in ("http", "https"): - raise ValueError() + raise ValueError # MUST contain a path component # Handled by url canonicalization. @@ -182,7 +184,7 @@ def _parse_client_id(client_id: str) -> ParseResult: # MAY contain a port try: # parts raises ValueError when port cannot be parsed as int - parts.port + _ = parts.port except ValueError as ex: raise ValueError("Client ID contains invalid port") from ex diff --git a/homeassistant/components/auth/login_flow.py b/homeassistant/components/auth/login_flow.py index cc6cb5fc47a..6c33d270f5f 100644 --- a/homeassistant/components/auth/login_flow.py +++ b/homeassistant/components/auth/login_flow.py @@ -66,6 +66,7 @@ associate with an credential if "type" set to "link_user" in "version": 1 } """ + from __future__ import annotations from collections.abc import Callable @@ -79,8 +80,9 @@ import voluptuous_serialize from homeassistant import data_entry_flow from homeassistant.auth import AuthManagerFlowManager, InvalidAuthError -from homeassistant.auth.models import Credentials +from homeassistant.auth.models import AuthFlowResult, Credentials from homeassistant.components import onboarding +from homeassistant.components.http import KEY_HASS from homeassistant.components.http.auth import async_user_not_allowed_do_auth from homeassistant.components.http.ban import ( log_invalid_auth, @@ -144,7 +146,7 @@ class AuthProvidersView(HomeAssistantView): async def get(self, request: web.Request) -> web.Response: """Get available auth providers.""" - hass: HomeAssistant = request.app["hass"] + hass = request.app[KEY_HASS] if not onboarding.async_is_user_onboarded(hass): return self.json_message( message="Onboarding not finished", @@ -197,8 +199,8 @@ class AuthProvidersView(HomeAssistantView): def _prepare_result_json( - result: data_entry_flow.FlowResult, -) -> data_entry_flow.FlowResult: + result: AuthFlowResult, +) -> AuthFlowResult: """Convert result to JSON.""" if result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY: data = result.copy() @@ -237,7 +239,7 @@ class LoginFlowBaseView(HomeAssistantView): self, request: web.Request, client_id: str, - result: data_entry_flow.FlowResult, + result: AuthFlowResult, ) -> web.Response: """Convert the flow result to a response.""" if result["type"] != data_entry_flow.FlowResultType.CREATE_ENTRY: @@ -255,7 +257,7 @@ class LoginFlowBaseView(HomeAssistantView): await process_wrong_login(request) return self.json(_prepare_result_json(result)) - hass: HomeAssistant = request.app["hass"] + hass = request.app[KEY_HASS] if not await indieauth.verify_redirect_uri( hass, client_id, result["context"]["redirect_uri"] @@ -297,7 +299,9 @@ class LoginFlowIndexView(LoginFlowBaseView): vol.Schema( { vol.Required("client_id"): str, - vol.Required("handler"): vol.Any(str, list), + vol.Required("handler"): vol.All( + [vol.Any(str, None)], vol.Length(2, 2), vol.Coerce(tuple) + ), vol.Required("redirect_uri"): str, vol.Optional("type", default="authorize"): str, } @@ -312,15 +316,11 @@ class LoginFlowIndexView(LoginFlowBaseView): if not indieauth.verify_client_id(client_id): return self.json_message("Invalid client id", HTTPStatus.BAD_REQUEST) - handler: tuple[str, ...] | str - if isinstance(data["handler"], list): - handler = tuple(data["handler"]) - else: - handler = data["handler"] + handler: tuple[str, str] = tuple(data["handler"]) try: result = await self._flow_mgr.async_init( - handler, # type: ignore[arg-type] + handler, context={ "ip_address": ip_address(request.remote), # type: ignore[arg-type] "credential_only": data.get("type") == "link_user", diff --git a/homeassistant/components/auth/mfa_setup_flow.py b/homeassistant/components/auth/mfa_setup_flow.py index a7999af666a..aee08186267 100644 --- a/homeassistant/components/auth/mfa_setup_flow.py +++ b/homeassistant/components/auth/mfa_setup_flow.py @@ -1,4 +1,5 @@ """Helpers to setup multi-factor auth module.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 98ec92a3771..0bd2ed87d20 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -1,4 +1,5 @@ """Allow to set up simple automation rules via the config file.""" + from __future__ import annotations from abc import ABC, abstractmethod @@ -232,6 +233,30 @@ def areas_in_automation(hass: HomeAssistant, entity_id: str) -> list[str]: return _x_in_automation(hass, entity_id, "referenced_areas") +@callback +def automations_with_floor(hass: HomeAssistant, floor_id: str) -> list[str]: + """Return all automations that reference the floor.""" + return _automations_with_x(hass, floor_id, "referenced_floors") + + +@callback +def floors_in_automation(hass: HomeAssistant, entity_id: str) -> list[str]: + """Return all floors in an automation.""" + return _x_in_automation(hass, entity_id, "referenced_floors") + + +@callback +def automations_with_label(hass: HomeAssistant, label_id: str) -> list[str]: + """Return all automations that reference the label.""" + return _automations_with_x(hass, label_id, "referenced_labels") + + +@callback +def labels_in_automation(hass: HomeAssistant, entity_id: str) -> list[str]: + """Return all labels in an automation.""" + return _x_in_automation(hass, entity_id, "referenced_labels") + + @callback def automations_with_blueprint(hass: HomeAssistant, blueprint_path: str) -> list[str]: """Return all automations that reference the blueprint.""" @@ -273,8 +298,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: await _async_process_config(hass, config, component) # Add some default blueprints to blueprints/automation, does nothing - # if blueprints/automation already exists - await async_get_blueprints(hass).async_populate() + # if blueprints/automation already exists but still has to create + # an executor job to check if the folder exists so we run it in a + # separate task to avoid waiting for it to finish setting up + # since a tracked task will be waited at the end of startup + hass.async_create_task( + async_get_blueprints(hass).async_populate(), eager_start=True + ) async def trigger_service_handler( entity: BaseAutomationEntity, service_call: ServiceCall @@ -340,6 +370,16 @@ class BaseAutomationEntity(ToggleEntity, ABC): return {CONF_ID: self.unique_id} return None + @cached_property + @abstractmethod + def referenced_labels(self) -> set[str]: + """Return a set of referenced labels.""" + + @cached_property + @abstractmethod + def referenced_floors(self) -> set[str]: + """Return a set of referenced floors.""" + @cached_property @abstractmethod def referenced_areas(self) -> set[str]: @@ -373,7 +413,7 @@ class BaseAutomationEntity(ToggleEntity, ABC): class UnavailableAutomationEntity(BaseAutomationEntity): """A non-functional automation entity with its state set to unavailable. - This class is instatiated when an automation fails to validate. + This class is instantiated when an automation fails to validate. """ _attr_should_poll = False @@ -395,6 +435,16 @@ class UnavailableAutomationEntity(BaseAutomationEntity): """Return the name of the entity.""" return self._name + @cached_property + def referenced_labels(self) -> set[str]: + """Return a set of referenced labels.""" + return set() + + @cached_property + def referenced_floors(self) -> set[str]: + """Return a set of referenced floors.""" + return set() + @cached_property def referenced_areas(self) -> set[str]: """Return a set of referenced areas.""" @@ -482,6 +532,16 @@ class AutomationEntity(BaseAutomationEntity, RestoreEntity): """Return True if entity is on.""" return self._async_detach_triggers is not None or self._is_enabled + @property + def referenced_labels(self) -> set[str]: + """Return a set of referenced labels.""" + return self.action_script.referenced_labels + + @property + def referenced_floors(self) -> set[str]: + """Return a set of referenced floors.""" + return self.action_script.referenced_floors + @cached_property def referenced_areas(self) -> set[str]: """Return a set of referenced areas.""" @@ -701,7 +761,7 @@ class AutomationEntity(BaseAutomationEntity, RestoreEntity): await super().async_will_remove_from_hass() await self.async_disable() - async def _async_enable_automation(self) -> None: + async def _async_enable_automation(self, event: Event) -> None: """Start automation on startup.""" # Don't do anything if no longer enabled or already attached if not self._is_enabled or self._async_detach_triggers is not None: @@ -709,11 +769,6 @@ class AutomationEntity(BaseAutomationEntity, RestoreEntity): self._async_detach_triggers = await self._async_attach_triggers(True) - @callback - def _async_create_enable_automation_task(self, event: Event) -> None: - """Create a task to enable the automation.""" - self.hass.async_create_task(self._async_enable_automation(), eager_start=True) - async def async_enable(self) -> None: """Enable this automation entity. @@ -731,7 +786,9 @@ class AutomationEntity(BaseAutomationEntity, RestoreEntity): return self.hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STARTED, self._async_create_enable_automation_task + EVENT_HOMEASSISTANT_STARTED, + self._async_enable_automation, + run_immediately=True, ) self.async_write_ha_state() diff --git a/homeassistant/components/automation/config.py b/homeassistant/components/automation/config.py index 72fb0101b24..71b4b3c0c6a 100644 --- a/homeassistant/components/automation/config.py +++ b/homeassistant/components/automation/config.py @@ -1,4 +1,5 @@ """Config validation helper for the automation integration.""" + from __future__ import annotations from collections.abc import Mapping diff --git a/homeassistant/components/automation/const.py b/homeassistant/components/automation/const.py index a82c78ded77..e6be35494d7 100644 --- a/homeassistant/components/automation/const.py +++ b/homeassistant/components/automation/const.py @@ -1,4 +1,5 @@ """Constants for the automation integration.""" + import logging CONF_ACTION = "action" diff --git a/homeassistant/components/automation/helpers.py b/homeassistant/components/automation/helpers.py index a7c329a544a..6aefa2b150a 100644 --- a/homeassistant/components/automation/helpers.py +++ b/homeassistant/components/automation/helpers.py @@ -1,4 +1,5 @@ """Helpers for automation integration.""" + from homeassistant.components import blueprint from homeassistant.const import SERVICE_RELOAD from homeassistant.core import HomeAssistant, callback diff --git a/homeassistant/components/automation/logbook.py b/homeassistant/components/automation/logbook.py index e5ab351b0be..7b9c8cf5809 100644 --- a/homeassistant/components/automation/logbook.py +++ b/homeassistant/components/automation/logbook.py @@ -1,4 +1,5 @@ """Describe logbook events.""" + from homeassistant.components.logbook import ( LOGBOOK_ENTRY_CONTEXT_ID, LOGBOOK_ENTRY_ENTITY_ID, diff --git a/homeassistant/components/automation/reproduce_state.py b/homeassistant/components/automation/reproduce_state.py index aa6e6a501b6..06c982f5670 100644 --- a/homeassistant/components/automation/reproduce_state.py +++ b/homeassistant/components/automation/reproduce_state.py @@ -1,4 +1,5 @@ """Reproduce an Automation state.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/automation/trace.py b/homeassistant/components/automation/trace.py index ae0d0339bfa..754c062ec2c 100644 --- a/homeassistant/components/automation/trace.py +++ b/homeassistant/components/automation/trace.py @@ -1,4 +1,5 @@ """Trace support for automation.""" + from __future__ import annotations from collections.abc import Generator diff --git a/homeassistant/components/avea/light.py b/homeassistant/components/avea/light.py index a33fbfeab79..48471b41633 100644 --- a/homeassistant/components/avea/light.py +++ b/homeassistant/components/avea/light.py @@ -1,4 +1,5 @@ """Support for the Elgato Avea lights.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/avion/light.py b/homeassistant/components/avion/light.py index 54d06d50a60..e26676a0169 100644 --- a/homeassistant/components/avion/light.py +++ b/homeassistant/components/avion/light.py @@ -1,4 +1,5 @@ """Support for Avion dimmers.""" + from __future__ import annotations import importlib @@ -52,21 +53,25 @@ def setup_platform( """Set up an Avion switch.""" avion = importlib.import_module("avion") - lights = [] - if CONF_USERNAME in config and CONF_PASSWORD in config: - devices = avion.get_devices(config[CONF_USERNAME], config[CONF_PASSWORD]) - for device in devices: - lights.append(AvionLight(device)) - - for address, device_config in config[CONF_DEVICES].items(): - device = avion.Avion( - mac=address, - passphrase=device_config[CONF_API_KEY], - name=device_config.get(CONF_NAME), - object_id=device_config.get(CONF_ID), - connect=False, + lights = [ + AvionLight( + avion.Avion( + mac=address, + passphrase=device_config[CONF_API_KEY], + name=device_config.get(CONF_NAME), + object_id=device_config.get(CONF_ID), + connect=False, + ) + ) + for address, device_config in config[CONF_DEVICES].items() + ] + if CONF_USERNAME in config and CONF_PASSWORD in config: + lights.extend( + AvionLight(device) + for device in avion.get_devices( + config[CONF_USERNAME], config[CONF_PASSWORD] + ) ) - lights.append(AvionLight(device)) add_entities(lights) diff --git a/homeassistant/components/awair/__init__.py b/homeassistant/components/awair/__init__.py index cb974707e93..aa810bf532b 100644 --- a/homeassistant/components/awair/__init__.py +++ b/homeassistant/components/awair/__init__.py @@ -1,4 +1,5 @@ """The awair component.""" + from __future__ import annotations from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/awair/config_flow.py b/homeassistant/components/awair/config_flow.py index c68d46f7d39..d3c4703e89c 100644 --- a/homeassistant/components/awair/config_flow.py +++ b/homeassistant/components/awair/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Awair.""" + from __future__ import annotations from collections.abc import Mapping @@ -11,10 +12,9 @@ from python_awair.user import AwairUser import voluptuous as vol from homeassistant.components import onboarding, zeroconf -from homeassistant.config_entries import SOURCE_ZEROCONF, ConfigFlow +from homeassistant.config_entries import SOURCE_ZEROCONF, ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_ACCESS_TOKEN, CONF_DEVICE, CONF_HOST from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN, LOGGER @@ -29,7 +29,7 @@ class AwairFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_zeroconf( self, discovery_info: zeroconf.ZeroconfServiceInfo - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle zeroconf discovery.""" host = discovery_info.host @@ -58,7 +58,7 @@ class AwairFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_discovery_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm discovery.""" if user_input is not None or not onboarding.async_is_onboarded(self.hass): title = f"{self._device.model} ({self._device.device_id})" @@ -79,12 +79,12 @@ class AwairFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, str] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" return self.async_show_menu(step_id="user", menu_options=["local", "cloud"]) - async def async_step_cloud(self, user_input: Mapping[str, Any]) -> FlowResult: + async def async_step_cloud(self, user_input: Mapping[str, Any]) -> ConfigFlowResult: """Handle collecting and verifying Awair Cloud API credentials.""" errors = {} @@ -122,14 +122,14 @@ class AwairFlowHandler(ConfigFlow, domain=DOMAIN): for flow in self._async_in_progress(): if flow["context"]["source"] == SOURCE_ZEROCONF: info = flow["context"]["title_placeholders"] - entries[ - flow["context"]["host"] - ] = f"{info['model']} ({info['device_id']})" + entries[flow["context"]["host"]] = ( + f"{info['model']} ({info['device_id']})" + ) return entries async def async_step_local( self, user_input: Mapping[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Show how to enable local API.""" if user_input is not None: return await self.async_step_local_pick() @@ -143,7 +143,7 @@ class AwairFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_local_pick( self, user_input: Mapping[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle collecting and verifying Awair Local API hosts.""" errors = {} @@ -188,13 +188,15 @@ class AwairFlowHandler(ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Handle re-auth if token invalid.""" return await self.async_step_reauth_confirm() async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm reauth dialog.""" errors = {} diff --git a/homeassistant/components/awair/const.py b/homeassistant/components/awair/const.py index 19341ab6050..a1c5781e9a4 100644 --- a/homeassistant/components/awair/const.py +++ b/homeassistant/components/awair/const.py @@ -1,4 +1,5 @@ """Constants for the Awair component.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/awair/coordinator.py b/homeassistant/components/awair/coordinator.py index b687a916a2d..8e554b3b9e0 100644 --- a/homeassistant/components/awair/coordinator.py +++ b/homeassistant/components/awair/coordinator.py @@ -1,4 +1,5 @@ """DataUpdateCoordinators for awair integration.""" + from __future__ import annotations from asyncio import gather, timeout diff --git a/homeassistant/components/awair/sensor.py b/homeassistant/components/awair/sensor.py index 03243e51b7c..b9a226e9c2c 100644 --- a/homeassistant/components/awair/sensor.py +++ b/homeassistant/components/awair/sensor.py @@ -1,4 +1,5 @@ """Support for Awair sensors.""" + from __future__ import annotations from dataclasses import dataclass @@ -50,18 +51,13 @@ from .coordinator import AwairDataUpdateCoordinator, AwairResult DUST_ALIASES = [API_PM25, API_PM10] -@dataclass(frozen=True) -class AwairRequiredKeysMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class AwairSensorEntityDescription(SensorEntityDescription): + """Describes Awair sensor entity.""" unique_id_tag: str -@dataclass(frozen=True) -class AwairSensorEntityDescription(SensorEntityDescription, AwairRequiredKeysMixin): - """Describes Awair sensor entity.""" - - SENSOR_TYPE_SCORE = AwairSensorEntityDescription( key=API_SCORE, native_unit_of_measurement=PERCENTAGE, diff --git a/homeassistant/components/aws/__init__.py b/homeassistant/components/aws/__init__.py index 0638ae94de2..da84c8985f5 100644 --- a/homeassistant/components/aws/__init__.py +++ b/homeassistant/components/aws/__init__.py @@ -1,4 +1,5 @@ """Support for Amazon Web Services (AWS).""" + import asyncio from collections import OrderedDict import logging @@ -128,9 +129,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # validate credentials and create sessions validation = True - tasks = [] - for cred in conf[ATTR_CREDENTIALS]: - tasks.append(_validate_aws_credentials(hass, cred)) + tasks = [_validate_aws_credentials(hass, cred) for cred in conf[ATTR_CREDENTIALS]] if tasks: results = await asyncio.gather(*tasks, return_exceptions=True) for index, result in enumerate(results): diff --git a/homeassistant/components/aws/config_flow.py b/homeassistant/components/aws/config_flow.py index e0829ef2914..8c80b0d487d 100644 --- a/homeassistant/components/aws/config_flow.py +++ b/homeassistant/components/aws/config_flow.py @@ -3,18 +3,19 @@ from collections.abc import Mapping from typing import Any -from homeassistant import config_entries -from homeassistant.data_entry_flow import FlowResult +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from .const import DOMAIN -class AWSFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class AWSFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a config flow.""" VERSION = 1 - async def async_step_import(self, user_input: Mapping[str, Any]) -> FlowResult: + async def async_step_import( + self, user_input: Mapping[str, Any] + ) -> ConfigFlowResult: """Import a config entry.""" if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") diff --git a/homeassistant/components/aws/const.py b/homeassistant/components/aws/const.py index 8be6afec7ff..c885495934f 100644 --- a/homeassistant/components/aws/const.py +++ b/homeassistant/components/aws/const.py @@ -1,4 +1,5 @@ """Constant for AWS component.""" + DOMAIN = "aws" DATA_CONFIG = "aws_config" diff --git a/homeassistant/components/aws/notify.py b/homeassistant/components/aws/notify.py index 2e3ea341f60..47d66900eb0 100644 --- a/homeassistant/components/aws/notify.py +++ b/homeassistant/components/aws/notify.py @@ -1,4 +1,5 @@ """AWS platform for notify component.""" + from __future__ import annotations import asyncio @@ -154,15 +155,14 @@ class AWSLambda(AWSNotify): async with self.session.create_client( self.service, **self.aws_config ) as client: - tasks = [] - for target in kwargs.get(ATTR_TARGET, []): - tasks.append( - client.invoke( - FunctionName=target, - Payload=json_payload, - ClientContext=self.context, - ) + tasks = [ + client.invoke( + FunctionName=target, + Payload=json_payload, + ClientContext=self.context, ) + for target in kwargs.get(ATTR_TARGET, []) + ] if tasks: await asyncio.gather(*tasks) @@ -191,16 +191,15 @@ class AWSSNS(AWSNotify): async with self.session.create_client( self.service, **self.aws_config ) as client: - tasks = [] - for target in kwargs.get(ATTR_TARGET, []): - tasks.append( - client.publish( - TargetArn=target, - Message=message, - Subject=subject, - MessageAttributes=message_attributes, - ) + tasks = [ + client.publish( + TargetArn=target, + Message=message, + Subject=subject, + MessageAttributes=message_attributes, ) + for target in kwargs.get(ATTR_TARGET, []) + ] if tasks: await asyncio.gather(*tasks) @@ -231,15 +230,14 @@ class AWSSQS(AWSNotify): async with self.session.create_client( self.service, **self.aws_config ) as client: - tasks = [] - for target in kwargs.get(ATTR_TARGET, []): - tasks.append( - client.send_message( - QueueUrl=target, - MessageBody=json_body, - MessageAttributes=message_attributes, - ) + tasks = [ + client.send_message( + QueueUrl=target, + MessageBody=json_body, + MessageAttributes=message_attributes, ) + for target in kwargs.get(ATTR_TARGET, []) + ] if tasks: await asyncio.gather(*tasks) @@ -264,7 +262,6 @@ class AWSEventBridge(AWSNotify): async with self.session.create_client( self.service, **self.aws_config ) as client: - tasks = [] entries = [] for target in kwargs.get(ATTR_TARGET, [None]): entry = { @@ -277,10 +274,10 @@ class AWSEventBridge(AWSNotify): entry["EventBusName"] = target entries.append(entry) - for i in range(0, len(entries), 10): - tasks.append( - client.put_events(Entries=entries[i : min(i + 10, len(entries))]) - ) + tasks = [ + client.put_events(Entries=entries[i : min(i + 10, len(entries))]) + for i in range(0, len(entries), 10) + ] if tasks: results = await asyncio.gather(*tasks) diff --git a/homeassistant/components/axis/__init__.py b/homeassistant/components/axis/__init__.py index ae5ffcbdb7a..f955ff398d7 100644 --- a/homeassistant/components/axis/__init__.py +++ b/homeassistant/components/axis/__init__.py @@ -1,4 +1,5 @@ """Support for Axis devices.""" + import logging from homeassistant.config_entries import ConfigEntry @@ -7,8 +8,8 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from .const import DOMAIN as AXIS_DOMAIN, PLATFORMS -from .device import AxisNetworkDevice, get_axis_device from .errors import AuthenticationRequired, CannotConnect +from .hub import AxisHub, get_axis_api _LOGGER = logging.getLogger(__name__) @@ -18,21 +19,22 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b hass.data.setdefault(AXIS_DOMAIN, {}) try: - api = await get_axis_device(hass, config_entry.data) + api = await get_axis_api(hass, config_entry.data) except CannotConnect as err: raise ConfigEntryNotReady from err except AuthenticationRequired as err: raise ConfigEntryAuthFailed from err - device = AxisNetworkDevice(hass, config_entry, api) - hass.data[AXIS_DOMAIN][config_entry.entry_id] = device - await device.async_update_device_registry() + hub = AxisHub(hass, config_entry, api) + hass.data[AXIS_DOMAIN][config_entry.entry_id] = hub + await hub.async_update_device_registry() await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) - device.async_setup_events() + hub.setup() - config_entry.add_update_listener(device.async_new_address_callback) + config_entry.add_update_listener(hub.async_new_address_callback) + config_entry.async_on_unload(hub.teardown) config_entry.async_on_unload( - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, device.shutdown) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, hub.shutdown) ) return True @@ -40,8 +42,8 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Unload Axis device config entry.""" - device: AxisNetworkDevice = hass.data[AXIS_DOMAIN].pop(config_entry.entry_id) - return await device.async_reset() + hass.data[AXIS_DOMAIN].pop(config_entry.entry_id) + return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS) async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/axis/binary_sensor.py b/homeassistant/components/axis/binary_sensor.py index 8e7cda335e6..8cd90ba1554 100644 --- a/homeassistant/components/axis/binary_sensor.py +++ b/homeassistant/components/axis/binary_sensor.py @@ -1,48 +1,177 @@ """Support for Axis binary sensors.""" + from __future__ import annotations from collections.abc import Callable +from dataclasses import dataclass from datetime import datetime, timedelta -from axis.models.event import Event, EventGroup, EventOperation, EventTopic -from axis.vapix.interfaces.applications.fence_guard import FenceGuardHandler -from axis.vapix.interfaces.applications.loitering_guard import LoiteringGuardHandler -from axis.vapix.interfaces.applications.motion_guard import MotionGuardHandler -from axis.vapix.interfaces.applications.vmd4 import Vmd4Handler +from axis.interfaces.applications.fence_guard import FenceGuardHandler +from axis.interfaces.applications.loitering_guard import LoiteringGuardHandler +from axis.interfaces.applications.motion_guard import MotionGuardHandler +from axis.interfaces.applications.vmd4 import Vmd4Handler +from axis.models.event import Event, EventTopic from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, BinarySensorEntity, + BinarySensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_call_later -from .const import DOMAIN as AXIS_DOMAIN -from .device import AxisNetworkDevice -from .entity import AxisEventEntity +from .entity import AxisEventDescription, AxisEventEntity +from .hub import AxisHub -DEVICE_CLASS = { - EventGroup.INPUT: BinarySensorDeviceClass.CONNECTIVITY, - EventGroup.LIGHT: BinarySensorDeviceClass.LIGHT, - EventGroup.MOTION: BinarySensorDeviceClass.MOTION, - EventGroup.SOUND: BinarySensorDeviceClass.SOUND, -} -EVENT_TOPICS = ( - EventTopic.DAY_NIGHT_VISION, - EventTopic.FENCE_GUARD, - EventTopic.LOITERING_GUARD, - EventTopic.MOTION_DETECTION, - EventTopic.MOTION_DETECTION_3, - EventTopic.MOTION_DETECTION_4, - EventTopic.MOTION_GUARD, - EventTopic.OBJECT_ANALYTICS, - EventTopic.PIR, - EventTopic.PORT_INPUT, - EventTopic.PORT_SUPERVISED_INPUT, - EventTopic.SOUND_TRIGGER_LEVEL, +@dataclass(frozen=True, kw_only=True) +class AxisBinarySensorDescription(AxisEventDescription, BinarySensorEntityDescription): + """Axis binary sensor entity description.""" + + +@callback +def event_id_is_int(event_id: str) -> bool: + """Make sure event ID is int.""" + try: + _ = int(event_id) + except ValueError: + return False + return True + + +@callback +def guard_suite_supported_fn(hub: AxisHub, event: Event) -> bool: + """Validate event ID is int.""" + _, _, profile_id = event.id.partition("Profile") + return event_id_is_int(profile_id) + + +@callback +def object_analytics_supported_fn(hub: AxisHub, event: Event) -> bool: + """Validate event ID is int.""" + _, _, profile_id = event.id.partition("Scenario") + return event_id_is_int(profile_id) + + +@callback +def guard_suite_name_fn( + handler: FenceGuardHandler + | LoiteringGuardHandler + | MotionGuardHandler + | Vmd4Handler, + event: Event, + event_type: str, +) -> str: + """Get guard suite item name.""" + if handler.initialized and (profiles := handler["0"].profiles): + for profile_id, profile in profiles.items(): + camera_id = profile.camera + if event.id == f"Camera{camera_id}Profile{profile_id}": + return f"{event_type} {profile.name}" + return "" + + +@callback +def fence_guard_name_fn(hub: AxisHub, event: Event) -> str: + """Fence guard name.""" + return guard_suite_name_fn(hub.api.vapix.fence_guard, event, "Fence Guard") + + +@callback +def loitering_guard_name_fn(hub: AxisHub, event: Event) -> str: + """Loitering guard name.""" + return guard_suite_name_fn(hub.api.vapix.loitering_guard, event, "Loitering Guard") + + +@callback +def motion_guard_name_fn(hub: AxisHub, event: Event) -> str: + """Motion guard name.""" + return guard_suite_name_fn(hub.api.vapix.motion_guard, event, "Motion Guard") + + +@callback +def motion_detection_4_name_fn(hub: AxisHub, event: Event) -> str: + """Motion detection 4 name.""" + return guard_suite_name_fn(hub.api.vapix.vmd4, event, "VMD4") + + +@callback +def object_analytics_name_fn(hub: AxisHub, event: Event) -> str: + """Get object analytics name.""" + if hub.api.vapix.object_analytics.initialized and ( + scenarios := hub.api.vapix.object_analytics["0"].scenarios + ): + for scenario_id, scenario in scenarios.items(): + device_id = scenario.devices[0]["id"] + if event.id == f"Device{device_id}Scenario{scenario_id}": + return f"Object Analytics {scenario.name}" + return "" + + +ENTITY_DESCRIPTIONS = ( + AxisBinarySensorDescription( + key="Input port state", + device_class=BinarySensorDeviceClass.CONNECTIVITY, + event_topic=(EventTopic.PORT_INPUT, EventTopic.PORT_SUPERVISED_INPUT), + name_fn=lambda hub, event: hub.api.vapix.ports[event.id].name, + supported_fn=lambda hub, event: event_id_is_int(event.id), + ), + AxisBinarySensorDescription( + key="Day/Night vision state", + device_class=BinarySensorDeviceClass.LIGHT, + event_topic=EventTopic.DAY_NIGHT_VISION, + ), + AxisBinarySensorDescription( + key="Sound trigger state", + device_class=BinarySensorDeviceClass.SOUND, + event_topic=EventTopic.SOUND_TRIGGER_LEVEL, + ), + AxisBinarySensorDescription( + key="Motion sensors state", + device_class=BinarySensorDeviceClass.MOTION, + event_topic=( + EventTopic.PIR, + EventTopic.MOTION_DETECTION, + EventTopic.MOTION_DETECTION_3, + ), + ), + AxisBinarySensorDescription( + key="Motion detection 4 state", + device_class=BinarySensorDeviceClass.MOTION, + event_topic=EventTopic.MOTION_DETECTION_4, + name_fn=motion_detection_4_name_fn, + supported_fn=guard_suite_supported_fn, + ), + AxisBinarySensorDescription( + key="Fence guard state", + device_class=BinarySensorDeviceClass.MOTION, + event_topic=EventTopic.FENCE_GUARD, + name_fn=fence_guard_name_fn, + supported_fn=guard_suite_supported_fn, + ), + AxisBinarySensorDescription( + key="Loitering guard state", + device_class=BinarySensorDeviceClass.MOTION, + event_topic=EventTopic.LOITERING_GUARD, + name_fn=loitering_guard_name_fn, + supported_fn=guard_suite_supported_fn, + ), + AxisBinarySensorDescription( + key="Motion guard state", + device_class=BinarySensorDeviceClass.MOTION, + event_topic=EventTopic.MOTION_GUARD, + name_fn=motion_guard_name_fn, + supported_fn=guard_suite_supported_fn, + ), + AxisBinarySensorDescription( + key="Object analytics state", + device_class=BinarySensorDeviceClass.MOTION, + event_topic=EventTopic.OBJECT_ANALYTICS, + name_fn=object_analytics_name_fn, + supported_fn=object_analytics_supported_fn, + ), ) @@ -52,32 +181,24 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up a Axis binary sensor.""" - device: AxisNetworkDevice = hass.data[AXIS_DOMAIN][config_entry.entry_id] - - @callback - def async_create_entity(event: Event) -> None: - """Create Axis binary sensor entity.""" - async_add_entities([AxisBinarySensor(event, device)]) - - device.api.event.subscribe( - async_create_entity, - topic_filter=EVENT_TOPICS, - operation_filter=EventOperation.INITIALIZED, + AxisHub.get_hub(hass, config_entry).entity_loader.register_platform( + async_add_entities, AxisBinarySensor, ENTITY_DESCRIPTIONS ) class AxisBinarySensor(AxisEventEntity, BinarySensorEntity): """Representation of a binary Axis event.""" - def __init__(self, event: Event, device: AxisNetworkDevice) -> None: + entity_description: AxisBinarySensorDescription + + def __init__( + self, hub: AxisHub, description: AxisBinarySensorDescription, event: Event + ) -> None: """Initialize the Axis binary sensor.""" - super().__init__(event, device) - self.cancel_scheduled_update: Callable[[], None] | None = None + super().__init__(hub, description, event) - self._attr_device_class = DEVICE_CLASS.get(event.group) self._attr_is_on = event.is_tripped - - self._set_name(event) + self.cancel_scheduled_update: Callable[[], None] | None = None @callback def async_event_callback(self, event: Event) -> None: @@ -94,54 +215,12 @@ class AxisBinarySensor(AxisEventEntity, BinarySensorEntity): self.cancel_scheduled_update() self.cancel_scheduled_update = None - if self.is_on or self.device.option_trigger_time == 0: + if self.is_on or self.hub.config.trigger_time == 0: self.async_write_ha_state() return self.cancel_scheduled_update = async_call_later( self.hass, - timedelta(seconds=self.device.option_trigger_time), + timedelta(seconds=self.hub.config.trigger_time), scheduled_update, ) - - @callback - def _set_name(self, event: Event) -> None: - """Set binary sensor name.""" - if ( - event.group == EventGroup.INPUT - and event.id in self.device.api.vapix.ports - and self.device.api.vapix.ports[event.id].name - ): - self._attr_name = self.device.api.vapix.ports[event.id].name - - elif event.group == EventGroup.MOTION: - event_data: FenceGuardHandler | LoiteringGuardHandler | MotionGuardHandler | Vmd4Handler | None = None - if event.topic_base == EventTopic.FENCE_GUARD: - event_data = self.device.api.vapix.fence_guard - elif event.topic_base == EventTopic.LOITERING_GUARD: - event_data = self.device.api.vapix.loitering_guard - elif event.topic_base == EventTopic.MOTION_GUARD: - event_data = self.device.api.vapix.motion_guard - elif event.topic_base == EventTopic.MOTION_DETECTION_4: - event_data = self.device.api.vapix.vmd4 - if ( - event_data - and event_data.initialized - and (profiles := event_data["0"].profiles) - ): - for profile_id, profile in profiles.items(): - camera_id = profile.camera - if event.id == f"Camera{camera_id}Profile{profile_id}": - self._attr_name = f"{self._event_type} {profile.name}" - return - - if ( - event.topic_base == EventTopic.OBJECT_ANALYTICS - and self.device.api.vapix.object_analytics.initialized - and (scenarios := self.device.api.vapix.object_analytics["0"].scenarios) - ): - for scenario_id, scenario in scenarios.items(): - device_id = scenario.devices[0]["id"] - if event.id == f"Device{device_id}Scenario{scenario_id}": - self._attr_name = f"{self._event_type} {scenario.name}" - break diff --git a/homeassistant/components/axis/camera.py b/homeassistant/components/axis/camera.py index a0c71f101ca..769be676a78 100644 --- a/homeassistant/components/axis/camera.py +++ b/homeassistant/components/axis/camera.py @@ -1,4 +1,5 @@ """Support for Axis camera streaming.""" + from urllib.parse import urlencode from homeassistant.components.camera import CameraEntityFeature @@ -9,9 +10,9 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DEFAULT_STREAM_PROFILE, DEFAULT_VIDEO_SOURCE, DOMAIN as AXIS_DOMAIN -from .device import AxisNetworkDevice +from .const import DEFAULT_STREAM_PROFILE, DEFAULT_VIDEO_SOURCE from .entity import AxisEntity +from .hub import AxisHub async def async_setup_entry( @@ -22,15 +23,15 @@ async def async_setup_entry( """Set up the Axis camera video stream.""" filter_urllib3_logging() - device: AxisNetworkDevice = hass.data[AXIS_DOMAIN][config_entry.entry_id] + hub = AxisHub.get_hub(hass, config_entry) if ( - not (prop := device.api.vapix.params.property_handler.get("0")) + not (prop := hub.api.vapix.params.property_handler.get("0")) or not prop.image_format ): return - async_add_entities([AxisCamera(device)]) + async_add_entities([AxisCamera(hub)]) class AxisCamera(AxisEntity, MjpegCamera): @@ -42,27 +43,27 @@ class AxisCamera(AxisEntity, MjpegCamera): _mjpeg_url: str _stream_source: str - def __init__(self, device: AxisNetworkDevice) -> None: + def __init__(self, hub: AxisHub) -> None: """Initialize Axis Communications camera component.""" - AxisEntity.__init__(self, device) + AxisEntity.__init__(self, hub) self._generate_sources() MjpegCamera.__init__( self, - username=device.username, - password=device.password, + username=hub.config.username, + password=hub.config.password, mjpeg_url=self.mjpeg_source, still_image_url=self.image_source, authentication=HTTP_DIGEST_AUTHENTICATION, - unique_id=f"{device.unique_id}-camera", + unique_id=f"{hub.unique_id}-camera", ) async def async_added_to_hass(self) -> None: """Subscribe camera events.""" self.async_on_remove( async_dispatcher_connect( - self.hass, self.device.signal_new_address, self._generate_sources + self.hass, self.hub.signal_new_address, self._generate_sources ) ) @@ -75,27 +76,27 @@ class AxisCamera(AxisEntity, MjpegCamera): """ image_options = self.generate_options(skip_stream_profile=True) self._still_image_url = ( - f"http://{self.device.host}:{self.device.port}/axis-cgi" + f"http://{self.hub.config.host}:{self.hub.config.port}/axis-cgi" f"/jpg/image.cgi{image_options}" ) mjpeg_options = self.generate_options() self._mjpeg_url = ( - f"http://{self.device.host}:{self.device.port}/axis-cgi" + f"http://{self.hub.config.host}:{self.hub.config.port}/axis-cgi" f"/mjpg/video.cgi{mjpeg_options}" ) stream_options = self.generate_options(add_video_codec_h264=True) self._stream_source = ( - f"rtsp://{self.device.username}:{self.device.password}" - f"@{self.device.host}/axis-media/media.amp{stream_options}" + f"rtsp://{self.hub.config.username}:{self.hub.config.password}" + f"@{self.hub.config.host}/axis-media/media.amp{stream_options}" ) - self.device.additional_diagnostics["camera_sources"] = { + self.hub.additional_diagnostics["camera_sources"] = { "Image": self._still_image_url, "MJPEG": self._mjpeg_url, "Stream": ( - f"rtsp://user:pass@{self.device.host}/axis-media" + f"rtsp://user:pass@{self.hub.config.host}/axis-media" f"/media.amp{stream_options}" ), } @@ -125,12 +126,12 @@ class AxisCamera(AxisEntity, MjpegCamera): if ( not skip_stream_profile - and self.device.option_stream_profile != DEFAULT_STREAM_PROFILE + and self.hub.config.stream_profile != DEFAULT_STREAM_PROFILE ): - options_dict["streamprofile"] = self.device.option_stream_profile + options_dict["streamprofile"] = self.hub.config.stream_profile - if self.device.option_video_source != DEFAULT_VIDEO_SOURCE: - options_dict["camera"] = self.device.option_video_source + if self.hub.config.video_source != DEFAULT_VIDEO_SOURCE: + options_dict["camera"] = self.hub.config.video_source if not options_dict: return "" diff --git a/homeassistant/components/axis/config_flow.py b/homeassistant/components/axis/config_flow.py index cbba23b8b51..30bc653c202 100644 --- a/homeassistant/components/axis/config_flow.py +++ b/homeassistant/components/axis/config_flow.py @@ -1,4 +1,5 @@ """Config flow to configure Axis devices.""" + from __future__ import annotations from collections.abc import Mapping @@ -9,9 +10,14 @@ from urllib.parse import urlsplit import voluptuous as vol -from homeassistant import config_entries from homeassistant.components import dhcp, ssdp, zeroconf -from homeassistant.config_entries import SOURCE_IGNORE, ConfigEntry +from homeassistant.config_entries import ( + SOURCE_IGNORE, + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlowWithConfigEntry, +) from homeassistant.const import ( CONF_HOST, CONF_MAC, @@ -19,10 +25,10 @@ from homeassistant.const import ( CONF_NAME, CONF_PASSWORD, CONF_PORT, + CONF_PROTOCOL, CONF_USERNAME, ) from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.device_registry import format_mac from homeassistant.util.network import is_link_local @@ -33,14 +39,16 @@ from .const import ( DEFAULT_VIDEO_SOURCE, DOMAIN as AXIS_DOMAIN, ) -from .device import AxisNetworkDevice, get_axis_device from .errors import AuthenticationRequired, CannotConnect +from .hub import AxisHub, get_axis_api AXIS_OUI = {"00:40:8c", "ac:cc:8e", "b8:a4:4f"} -DEFAULT_PORT = 80 +DEFAULT_PORT = 443 +DEFAULT_PROTOCOL = "https" +PROTOCOL_CHOICES = ["https", "http"] -class AxisFlowHandler(config_entries.ConfigFlow, domain=AXIS_DOMAIN): +class AxisFlowHandler(ConfigFlow, domain=AXIS_DOMAIN): """Handle a Axis config flow.""" VERSION = 3 @@ -53,12 +61,12 @@ class AxisFlowHandler(config_entries.ConfigFlow, domain=AXIS_DOMAIN): def __init__(self) -> None: """Initialize the Axis config flow.""" - self.device_config: dict[str, Any] = {} + self.config: dict[str, Any] = {} self.discovery_schema: dict[vol.Required, type[str | int]] | None = None async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a Axis config flow start. Manage device specific parameters. @@ -67,29 +75,7 @@ class AxisFlowHandler(config_entries.ConfigFlow, domain=AXIS_DOMAIN): if user_input is not None: try: - device = await get_axis_device(self.hass, MappingProxyType(user_input)) - - serial = device.vapix.serial_number - await self.async_set_unique_id(format_mac(serial)) - - self._abort_if_unique_id_configured( - updates={ - CONF_HOST: user_input[CONF_HOST], - CONF_PORT: user_input[CONF_PORT], - CONF_USERNAME: user_input[CONF_USERNAME], - CONF_PASSWORD: user_input[CONF_PASSWORD], - } - ) - - self.device_config = { - CONF_HOST: user_input[CONF_HOST], - CONF_PORT: user_input[CONF_PORT], - CONF_USERNAME: user_input[CONF_USERNAME], - CONF_PASSWORD: user_input[CONF_PASSWORD], - CONF_MODEL: device.vapix.product_number, - } - - return await self._create_entry(serial) + api = await get_axis_api(self.hass, MappingProxyType(user_input)) except AuthenticationRequired: errors["base"] = "invalid_auth" @@ -97,7 +83,33 @@ class AxisFlowHandler(config_entries.ConfigFlow, domain=AXIS_DOMAIN): except CannotConnect: errors["base"] = "cannot_connect" + else: + serial = api.vapix.serial_number + await self.async_set_unique_id(format_mac(serial)) + + self._abort_if_unique_id_configured( + updates={ + CONF_PROTOCOL: user_input[CONF_PROTOCOL], + CONF_HOST: user_input[CONF_HOST], + CONF_PORT: user_input[CONF_PORT], + CONF_USERNAME: user_input[CONF_USERNAME], + CONF_PASSWORD: user_input[CONF_PASSWORD], + } + ) + + self.config = { + CONF_PROTOCOL: user_input[CONF_PROTOCOL], + CONF_HOST: user_input[CONF_HOST], + CONF_PORT: user_input[CONF_PORT], + CONF_USERNAME: user_input[CONF_USERNAME], + CONF_PASSWORD: user_input[CONF_PASSWORD], + CONF_MODEL: api.vapix.product_number, + } + + return await self._create_entry(serial) + data = self.discovery_schema or { + vol.Required(CONF_PROTOCOL): vol.In(PROTOCOL_CHOICES), vol.Required(CONF_HOST): str, vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str, @@ -106,17 +118,17 @@ class AxisFlowHandler(config_entries.ConfigFlow, domain=AXIS_DOMAIN): return self.async_show_form( step_id="user", - description_placeholders=self.device_config, + description_placeholders=self.config, data_schema=vol.Schema(data), errors=errors, ) - async def _create_entry(self, serial: str) -> FlowResult: + async def _create_entry(self, serial: str) -> ConfigFlowResult: """Create entry for device. Generate a name to be used as a prefix for device entities. """ - model = self.device_config[CONF_MODEL] + model = self.config[CONF_MODEL] same_model = [ entry.data[CONF_NAME] for entry in self.hass.config_entries.async_entries(AXIS_DOMAIN) @@ -129,39 +141,64 @@ class AxisFlowHandler(config_entries.ConfigFlow, domain=AXIS_DOMAIN): if name not in same_model: break - self.device_config[CONF_NAME] = name + self.config[CONF_NAME] = name title = f"{model} - {serial}" - return self.async_create_entry(title=title, data=self.device_config) + return self.async_create_entry(title=title, data=self.config) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reconfigure( + self, user_input: Mapping[str, Any] | None = None + ) -> ConfigFlowResult: + """Trigger a reconfiguration flow.""" + entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) + assert entry + return await self._redo_configuration(entry.data, keep_password=True) + + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Trigger a reauthentication flow.""" self.context["title_placeholders"] = { CONF_NAME: entry_data[CONF_NAME], CONF_HOST: entry_data[CONF_HOST], } + return await self._redo_configuration(entry_data, keep_password=False) + async def _redo_configuration( + self, entry_data: Mapping[str, Any], keep_password: bool + ) -> ConfigFlowResult: + """Re-run configuration step.""" self.discovery_schema = { + vol.Required( + CONF_PROTOCOL, default=entry_data.get(CONF_PROTOCOL, "http") + ): str, vol.Required(CONF_HOST, default=entry_data[CONF_HOST]): str, vol.Required(CONF_USERNAME, default=entry_data[CONF_USERNAME]): str, - vol.Required(CONF_PASSWORD): str, + vol.Required( + CONF_PASSWORD, + default=entry_data[CONF_PASSWORD] if keep_password else "", + ): str, vol.Required(CONF_PORT, default=entry_data[CONF_PORT]): int, } return await self.async_step_user() - async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: + async def async_step_dhcp( + self, discovery_info: dhcp.DhcpServiceInfo + ) -> ConfigFlowResult: """Prepare configuration for a DHCP discovered Axis device.""" return await self._process_discovered_device( { CONF_HOST: discovery_info.ip, CONF_MAC: format_mac(discovery_info.macaddress), CONF_NAME: discovery_info.hostname, - CONF_PORT: DEFAULT_PORT, + CONF_PORT: 80, } ) - async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: + async def async_step_ssdp( + self, discovery_info: ssdp.SsdpServiceInfo + ) -> ConfigFlowResult: """Prepare configuration for a SSDP discovered Axis device.""" url = urlsplit(discovery_info.upnp[ssdp.ATTR_UPNP_PRESENTATION_URL]) return await self._process_discovered_device( @@ -175,7 +212,7 @@ class AxisFlowHandler(config_entries.ConfigFlow, domain=AXIS_DOMAIN): async def async_step_zeroconf( self, discovery_info: zeroconf.ZeroconfServiceInfo - ) -> FlowResult: + ) -> ConfigFlowResult: """Prepare configuration for a Zeroconf discovered Axis device.""" return await self._process_discovered_device( { @@ -186,58 +223,58 @@ class AxisFlowHandler(config_entries.ConfigFlow, domain=AXIS_DOMAIN): } ) - async def _process_discovered_device(self, device: dict[str, Any]) -> FlowResult: + async def _process_discovered_device( + self, discovery_info: dict[str, Any] + ) -> ConfigFlowResult: """Prepare configuration for a discovered Axis device.""" - if device[CONF_MAC][:8] not in AXIS_OUI: + if discovery_info[CONF_MAC][:8] not in AXIS_OUI: return self.async_abort(reason="not_axis_device") - if is_link_local(ip_address(device[CONF_HOST])): + if is_link_local(ip_address(discovery_info[CONF_HOST])): return self.async_abort(reason="link_local_address") - await self.async_set_unique_id(device[CONF_MAC]) + await self.async_set_unique_id(discovery_info[CONF_MAC]) self._abort_if_unique_id_configured( - updates={ - CONF_HOST: device[CONF_HOST], - CONF_PORT: device[CONF_PORT], - } + updates={CONF_HOST: discovery_info[CONF_HOST]} ) self.context.update( { "title_placeholders": { - CONF_NAME: device[CONF_NAME], - CONF_HOST: device[CONF_HOST], + CONF_NAME: discovery_info[CONF_NAME], + CONF_HOST: discovery_info[CONF_HOST], }, - "configuration_url": f"http://{device[CONF_HOST]}:{device[CONF_PORT]}", + "configuration_url": f"http://{discovery_info[CONF_HOST]}:{discovery_info[CONF_PORT]}", } ) self.discovery_schema = { - vol.Required(CONF_HOST, default=device[CONF_HOST]): str, + vol.Required(CONF_PROTOCOL): vol.In(PROTOCOL_CHOICES), + vol.Required(CONF_HOST, default=discovery_info[CONF_HOST]): str, vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str, - vol.Required(CONF_PORT, default=device[CONF_PORT]): int, + vol.Required(CONF_PORT, default=DEFAULT_PORT): int, } return await self.async_step_user() -class AxisOptionsFlowHandler(config_entries.OptionsFlowWithConfigEntry): +class AxisOptionsFlowHandler(OptionsFlowWithConfigEntry): """Handle Axis device options.""" - device: AxisNetworkDevice + hub: AxisHub async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Manage the Axis device options.""" - self.device = self.hass.data[AXIS_DOMAIN][self.config_entry.entry_id] + self.hub = AxisHub.get_hub(self.hass, self.config_entry) return await self.async_step_configure_stream() async def async_step_configure_stream( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Manage the Axis device stream options.""" if user_input is not None: self.options.update(user_input) @@ -245,7 +282,7 @@ class AxisOptionsFlowHandler(config_entries.OptionsFlowWithConfigEntry): schema = {} - vapix = self.device.api.vapix + vapix = self.hub.api.vapix # Stream profiles @@ -254,12 +291,11 @@ class AxisOptionsFlowHandler(config_entries.OptionsFlowWithConfigEntry): and profiles.max_groups > 0 ): stream_profiles = [DEFAULT_STREAM_PROFILE] - for profile in vapix.streaming_profiles: - stream_profiles.append(profile.name) + stream_profiles.extend(profile.name for profile in vapix.streaming_profiles) schema[ vol.Optional( - CONF_STREAM_PROFILE, default=self.device.option_stream_profile + CONF_STREAM_PROFILE, default=self.hub.config.stream_profile ) ] = vol.In(stream_profiles) @@ -278,7 +314,7 @@ class AxisOptionsFlowHandler(config_entries.OptionsFlowWithConfigEntry): video_sources[int(idx) + 1] = video_source.name schema[ - vol.Optional(CONF_VIDEO_SOURCE, default=self.device.option_video_source) + vol.Optional(CONF_VIDEO_SOURCE, default=self.hub.config.video_source) ] = vol.In(video_sources) return self.async_show_form( diff --git a/homeassistant/components/axis/const.py b/homeassistant/components/axis/const.py index a13cae0dd0a..d315214c0e7 100644 --- a/homeassistant/components/axis/const.py +++ b/homeassistant/components/axis/const.py @@ -1,4 +1,5 @@ """Constants for the Axis component.""" + import logging from homeassistant.const import Platform @@ -9,7 +10,6 @@ DOMAIN = "axis" ATTR_MANUFACTURER = "Axis Communications AB" -CONF_EVENTS = "events" CONF_STREAM_PROFILE = "stream_profile" CONF_VIDEO_SOURCE = "video_source" diff --git a/homeassistant/components/axis/device.py b/homeassistant/components/axis/device.py deleted file mode 100644 index 845487b79d7..00000000000 --- a/homeassistant/components/axis/device.py +++ /dev/null @@ -1,277 +0,0 @@ -"""Axis network device abstraction.""" - -from asyncio import timeout -from types import MappingProxyType -from typing import Any - -import axis -from axis.configuration import Configuration -from axis.errors import Unauthorized -from axis.stream_manager import Signal, State -from axis.vapix.interfaces.mqtt import mqtt_json_to_event -from axis.vapix.models.mqtt import ClientState - -from homeassistant.components import mqtt -from homeassistant.components.mqtt import DOMAIN as MQTT_DOMAIN -from homeassistant.components.mqtt.models import ReceiveMessage -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - CONF_HOST, - CONF_MODEL, - CONF_NAME, - CONF_PASSWORD, - CONF_PORT, - CONF_TRIGGER_TIME, - CONF_USERNAME, -) -from homeassistant.core import Event, HomeAssistant, callback -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 -from homeassistant.helpers.httpx_client import get_async_client -from homeassistant.setup import async_when_setup - -from .const import ( - ATTR_MANUFACTURER, - CONF_EVENTS, - CONF_STREAM_PROFILE, - CONF_VIDEO_SOURCE, - DEFAULT_EVENTS, - DEFAULT_STREAM_PROFILE, - DEFAULT_TRIGGER_TIME, - DEFAULT_VIDEO_SOURCE, - DOMAIN as AXIS_DOMAIN, - LOGGER, - PLATFORMS, -) -from .errors import AuthenticationRequired, CannotConnect - - -class AxisNetworkDevice: - """Manages a Axis device.""" - - def __init__( - self, hass: HomeAssistant, config_entry: ConfigEntry, api: axis.AxisDevice - ) -> None: - """Initialize the device.""" - self.hass = hass - self.config_entry = config_entry - self.api = api - - self.available = True - self.fw_version = api.vapix.firmware_version - self.product_type = api.vapix.product_type - - self.additional_diagnostics: dict[str, Any] = {} - - @property - def host(self) -> str: - """Return the host address of this device.""" - host: str = self.config_entry.data[CONF_HOST] - return host - - @property - def port(self) -> int: - """Return the HTTP port of this device.""" - port: int = self.config_entry.data[CONF_PORT] - return port - - @property - def username(self) -> str: - """Return the username of this device.""" - username: str = self.config_entry.data[CONF_USERNAME] - return username - - @property - def password(self) -> str: - """Return the password of this device.""" - password: str = self.config_entry.data[CONF_PASSWORD] - return password - - @property - def model(self) -> str: - """Return the model of this device.""" - model: str = self.config_entry.data[CONF_MODEL] - return model - - @property - def name(self) -> str: - """Return the name of this device.""" - name: str = self.config_entry.data[CONF_NAME] - return name - - @property - def unique_id(self) -> str | None: - """Return the unique ID (serial number) of this device.""" - return self.config_entry.unique_id - - # Options - - @property - def option_events(self) -> bool: - """Config entry option defining if platforms based on events should be created.""" - return self.config_entry.options.get(CONF_EVENTS, DEFAULT_EVENTS) - - @property - def option_stream_profile(self) -> str: - """Config entry option defining what stream profile camera platform should use.""" - return self.config_entry.options.get( - CONF_STREAM_PROFILE, DEFAULT_STREAM_PROFILE - ) - - @property - def option_trigger_time(self) -> int: - """Config entry option defining minimum number of seconds to keep trigger high.""" - return self.config_entry.options.get(CONF_TRIGGER_TIME, DEFAULT_TRIGGER_TIME) - - @property - def option_video_source(self) -> str: - """Config entry option defining what video source camera platform should use.""" - return self.config_entry.options.get(CONF_VIDEO_SOURCE, DEFAULT_VIDEO_SOURCE) - - # Signals - - @property - def signal_reachable(self) -> str: - """Device specific event to signal a change in connection status.""" - return f"axis_reachable_{self.unique_id}" - - @property - def signal_new_address(self) -> str: - """Device specific event to signal a change in device address.""" - return f"axis_new_address_{self.unique_id}" - - # Callbacks - - @callback - def async_connection_status_callback(self, status: Signal) -> None: - """Handle signals of device connection status. - - This is called on every RTSP keep-alive message. - Only signal state change if state change is true. - """ - - if self.available != (status == Signal.PLAYING): - self.available = not self.available - async_dispatcher_send(self.hass, self.signal_reachable) - - @staticmethod - async def async_new_address_callback( - hass: HomeAssistant, entry: ConfigEntry - ) -> None: - """Handle signals of device getting new address. - - Called when config entry is updated. - This is a static method because a class method (bound method), - cannot be used with weak references. - """ - device: AxisNetworkDevice = hass.data[AXIS_DOMAIN][entry.entry_id] - device.api.config.host = device.host - async_dispatcher_send(hass, device.signal_new_address) - - async def async_update_device_registry(self) -> None: - """Update device registry.""" - 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.config.url, - connections={(CONNECTION_NETWORK_MAC, self.unique_id)}, # type: ignore[arg-type] - identifiers={(AXIS_DOMAIN, self.unique_id)}, # type: ignore[arg-type] - manufacturer=ATTR_MANUFACTURER, - model=f"{self.model} {self.product_type}", - name=self.name, - sw_version=self.fw_version, - ) - - 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() - except Unauthorized: - # This means the user has too low privileges - return - if status.status.state == ClientState.ACTIVE: - self.config_entry.async_on_unload( - await mqtt.async_subscribe( - hass, f"{self.api.vapix.serial_number}/#", self.mqtt_message - ) - ) - - @callback - def mqtt_message(self, message: ReceiveMessage) -> None: - """Receive Axis MQTT message.""" - self.disconnect_from_stream() - - event = mqtt_json_to_event(message.payload) - self.api.event.handler(event) - - # Setup and teardown methods - - def async_setup_events(self) -> None: - """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() - self.api.stream.start() - - if self.api.vapix.mqtt.supported: - async_when_setup(self.hass, MQTT_DOMAIN, self.async_use_mqtt) - - @callback - def disconnect_from_stream(self) -> None: - """Stop stream.""" - if self.api.stream.state != State.STOPPED: - self.api.stream.connection_status_callback.clear() - self.api.stream.stop() - - async def shutdown(self, event: Event) -> None: - """Stop the event stream.""" - self.disconnect_from_stream() - - async def async_reset(self) -> bool: - """Reset this device to default state.""" - self.disconnect_from_stream() - - return await self.hass.config_entries.async_unload_platforms( - self.config_entry, PLATFORMS - ) - - -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, - config[CONF_HOST], - port=config[CONF_PORT], - username=config[CONF_USERNAME], - password=config[CONF_PASSWORD], - ) - ) - - try: - async with timeout(30): - await device.vapix.initialize() - - return device - - except axis.Unauthorized as err: - LOGGER.warning( - "Connected to device at %s but not registered", config[CONF_HOST] - ) - raise AuthenticationRequired from err - - except (TimeoutError, axis.RequestError) as err: - LOGGER.error("Error connecting to the Axis device at %s", config[CONF_HOST]) - raise CannotConnect from err - - except axis.AxisException as err: - LOGGER.exception("Unknown Axis communication error occurred") - raise AuthenticationRequired from err diff --git a/homeassistant/components/axis/diagnostics.py b/homeassistant/components/axis/diagnostics.py index 948a36a78a0..d2386047e71 100644 --- a/homeassistant/components/axis/diagnostics.py +++ b/homeassistant/components/axis/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for Axis.""" + from __future__ import annotations from typing import Any @@ -8,8 +9,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_MAC, CONF_PASSWORD, CONF_UNIQUE_ID, CONF_USERNAME from homeassistant.core import HomeAssistant -from .const import DOMAIN as AXIS_DOMAIN -from .device import AxisNetworkDevice +from .hub import AxisHub REDACT_CONFIG = {CONF_MAC, CONF_PASSWORD, CONF_UNIQUE_ID, CONF_USERNAME} REDACT_BASIC_DEVICE_INFO = {"SerialNumber", "SocSerialNumber"} @@ -20,26 +20,26 @@ async def async_get_config_entry_diagnostics( hass: HomeAssistant, config_entry: ConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - device: AxisNetworkDevice = hass.data[AXIS_DOMAIN][config_entry.entry_id] - diag: dict[str, Any] = device.additional_diagnostics.copy() + hub = AxisHub.get_hub(hass, config_entry) + diag: dict[str, Any] = hub.additional_diagnostics.copy() diag["config"] = async_redact_data(config_entry.as_dict(), REDACT_CONFIG) - if device.api.vapix.api_discovery: + if hub.api.vapix.api_discovery: diag["api_discovery"] = [ {"id": api.id, "name": api.name, "version": api.version} - for api in device.api.vapix.api_discovery.values() + for api in hub.api.vapix.api_discovery.values() ] - if device.api.vapix.basic_device_info: + if hub.api.vapix.basic_device_info: diag["basic_device_info"] = async_redact_data( - device.api.vapix.basic_device_info["0"], + hub.api.vapix.basic_device_info["0"], REDACT_BASIC_DEVICE_INFO, ) - if device.api.vapix.params: + if hub.api.vapix.params: diag["params"] = async_redact_data( - device.api.vapix.params.items(), + hub.api.vapix.params.items(), REDACT_VAPIX_PARAMS, ) diff --git a/homeassistant/components/axis/entity.py b/homeassistant/components/axis/entity.py index 81f0b1678fb..b952000cca8 100644 --- a/homeassistant/components/axis/entity.py +++ b/homeassistant/components/axis/entity.py @@ -1,16 +1,23 @@ """Base classes for Axis entities.""" +from __future__ import annotations + from abc import abstractmethod +from collections.abc import Callable +from dataclasses import dataclass +from typing import TYPE_CHECKING from axis.models.event import Event, EventTopic from homeassistant.core import callback from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import Entity, EntityDescription from .const import DOMAIN as AXIS_DOMAIN -from .device import AxisNetworkDevice + +if TYPE_CHECKING: + from .hub import AxisHub TOPIC_TO_EVENT_TYPE = { EventTopic.DAY_NIGHT_VISION: "DayNight", @@ -32,18 +39,30 @@ TOPIC_TO_EVENT_TYPE = { } +@dataclass(frozen=True, kw_only=True) +class AxisEventDescription(EntityDescription): + """Axis event based entity description.""" + + event_topic: tuple[EventTopic, ...] | EventTopic + """Event topic that provides state updates.""" + name_fn: Callable[[AxisHub, Event], str] = lambda hub, event: "" + """Function providing the corresponding name to the event ID.""" + supported_fn: Callable[[AxisHub, Event], bool] = lambda hub, event: True + """Function validating if event is supported.""" + + class AxisEntity(Entity): """Base common to all Axis entities.""" _attr_has_entity_name = True - def __init__(self, device: AxisNetworkDevice) -> None: + def __init__(self, hub: AxisHub) -> None: """Initialize the Axis event.""" - self.device = device + self.hub = hub self._attr_device_info = DeviceInfo( - identifiers={(AXIS_DOMAIN, device.unique_id)}, # type: ignore[arg-type] - serial_number=device.unique_id, + identifiers={(AXIS_DOMAIN, hub.unique_id)}, + serial_number=hub.unique_id, ) async def async_added_to_hass(self) -> None: @@ -51,7 +70,7 @@ class AxisEntity(Entity): self.async_on_remove( async_dispatcher_connect( self.hass, - self.device.signal_reachable, + self.hub.signal_reachable, self.async_signal_reachable_callback, ) ) @@ -59,27 +78,32 @@ class AxisEntity(Entity): @callback def async_signal_reachable_callback(self) -> None: """Call when device connection state change.""" - self._attr_available = self.device.available + self._attr_available = self.hub.available self.async_write_ha_state() class AxisEventEntity(AxisEntity): """Base common to all Axis entities from event stream.""" + entity_description: AxisEventDescription + _attr_should_poll = False - def __init__(self, event: Event, device: AxisNetworkDevice) -> None: + def __init__( + self, hub: AxisHub, description: AxisEventDescription, event: Event + ) -> None: """Initialize the Axis event.""" - super().__init__(device) + super().__init__(hub) + + self.entity_description = description self._event_id = event.id self._event_topic = event.topic_base - self._event_type = TOPIC_TO_EVENT_TYPE[event.topic_base] - self._attr_name = f"{self._event_type} {event.id}" - self._attr_unique_id = f"{device.unique_id}-{event.topic}-{event.id}" + event_type = TOPIC_TO_EVENT_TYPE[event.topic_base] + self._attr_name = description.name_fn(hub, event) or f"{event_type} {event.id}" - self._attr_device_class = event.group.value + self._attr_unique_id = f"{hub.unique_id}-{event.topic}-{event.id}" @callback @abstractmethod @@ -90,7 +114,7 @@ class AxisEventEntity(AxisEntity): """Subscribe sensors events.""" await super().async_added_to_hass() self.async_on_remove( - self.device.api.event.subscribe( + self.hub.api.event.subscribe( self.async_event_callback, id_filter=self._event_id, topic_filter=self._event_topic, diff --git a/homeassistant/components/axis/errors.py b/homeassistant/components/axis/errors.py index 56105b28b1b..caa872c4b18 100644 --- a/homeassistant/components/axis/errors.py +++ b/homeassistant/components/axis/errors.py @@ -1,4 +1,5 @@ """Errors for the Axis component.""" + from homeassistant.exceptions import HomeAssistantError diff --git a/homeassistant/components/axis/hub/__init__.py b/homeassistant/components/axis/hub/__init__.py new file mode 100644 index 00000000000..e68f902b628 --- /dev/null +++ b/homeassistant/components/axis/hub/__init__.py @@ -0,0 +1,4 @@ +"""Internal functionality not part of HA infrastructure.""" + +from .api import get_axis_api # noqa: F401 +from .hub import AxisHub # noqa: F401 diff --git a/homeassistant/components/axis/hub/api.py b/homeassistant/components/axis/hub/api.py new file mode 100644 index 00000000000..8e5d7533631 --- /dev/null +++ b/homeassistant/components/axis/hub/api.py @@ -0,0 +1,60 @@ +"""Axis network device abstraction.""" + +from asyncio import timeout +from types import MappingProxyType +from typing import Any + +import axis +from axis.models.configuration import Configuration + +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_PORT, + CONF_PROTOCOL, + CONF_USERNAME, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.httpx_client import get_async_client + +from ..const import LOGGER +from ..errors import AuthenticationRequired, CannotConnect + + +async def get_axis_api( + hass: HomeAssistant, + config: MappingProxyType[str, Any], +) -> axis.AxisDevice: + """Create a Axis device API.""" + session = get_async_client(hass, verify_ssl=False) + + api = axis.AxisDevice( + Configuration( + session, + config[CONF_HOST], + port=config[CONF_PORT], + username=config[CONF_USERNAME], + password=config[CONF_PASSWORD], + web_proto=config.get(CONF_PROTOCOL, "http"), + ) + ) + + try: + async with timeout(30): + await api.vapix.initialize() + + except axis.Unauthorized as err: + LOGGER.warning( + "Connected to device at %s but not registered", config[CONF_HOST] + ) + raise AuthenticationRequired from err + + except (TimeoutError, axis.RequestError) as err: + LOGGER.error("Error connecting to the Axis device at %s", config[CONF_HOST]) + raise CannotConnect from err + + except axis.AxisException as err: + LOGGER.exception("Unknown Axis communication error occurred") + raise AuthenticationRequired from err + + return api diff --git a/homeassistant/components/axis/hub/config.py b/homeassistant/components/axis/hub/config.py new file mode 100644 index 00000000000..e6d8378b45c --- /dev/null +++ b/homeassistant/components/axis/hub/config.py @@ -0,0 +1,66 @@ +"""Axis network device abstraction.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Self + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_HOST, + CONF_MODEL, + CONF_NAME, + CONF_PASSWORD, + CONF_PORT, + CONF_TRIGGER_TIME, + CONF_USERNAME, +) + +from ..const import ( + CONF_STREAM_PROFILE, + CONF_VIDEO_SOURCE, + DEFAULT_STREAM_PROFILE, + DEFAULT_TRIGGER_TIME, + DEFAULT_VIDEO_SOURCE, +) + + +@dataclass +class AxisConfig: + """Represent a Axis config entry.""" + + entry: ConfigEntry + + host: str + port: int + username: str + password: str + model: str + name: str + + # Options + + stream_profile: str + """Option defining what stream profile camera platform should use.""" + trigger_time: int + """Option defining minimum number of seconds to keep trigger high.""" + video_source: str + """Option defining what video source camera platform should use.""" + + @classmethod + def from_config_entry(cls, config_entry: ConfigEntry) -> Self: + """Create object from config entry.""" + config = config_entry.data + options = config_entry.options + return cls( + entry=config_entry, + host=config[CONF_HOST], + username=config[CONF_USERNAME], + password=config[CONF_PASSWORD], + port=config[CONF_PORT], + model=config[CONF_MODEL], + name=config[CONF_NAME], + stream_profile=options.get(CONF_STREAM_PROFILE, DEFAULT_STREAM_PROFILE), + trigger_time=options.get(CONF_TRIGGER_TIME, DEFAULT_TRIGGER_TIME), + video_source=options.get(CONF_VIDEO_SOURCE, DEFAULT_VIDEO_SOURCE), + ) diff --git a/homeassistant/components/axis/hub/entity_loader.py b/homeassistant/components/axis/hub/entity_loader.py new file mode 100644 index 00000000000..54815ff9a69 --- /dev/null +++ b/homeassistant/components/axis/hub/entity_loader.py @@ -0,0 +1,83 @@ +"""Axis network device entity loader. + +Central point to load entities for the different platforms. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from axis.models.event import Event, EventOperation, EventTopic + +from homeassistant.core import callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from ..entity import AxisEventDescription, AxisEventEntity + +if TYPE_CHECKING: + from .hub import AxisHub + + +class AxisEntityLoader: + """Axis network device integration handling platforms for entity registration.""" + + def __init__(self, hub: AxisHub) -> None: + """Initialize the Axis entity loader.""" + self.hub = hub + + self.registered_events: set[tuple[str, EventTopic, str]] = set() + self.topic_to_entity: dict[ + EventTopic, + list[ + tuple[ + AddEntitiesCallback, + type[AxisEventEntity], + AxisEventDescription, + ] + ], + ] = {} + + @callback + def register_platform( + self, + async_add_entities: AddEntitiesCallback, + entity_class: type[AxisEventEntity], + descriptions: tuple[AxisEventDescription, ...], + ) -> None: + """Register Axis entity platforms.""" + topics: tuple[EventTopic, ...] + for description in descriptions: + if isinstance(description.event_topic, EventTopic): + topics = (description.event_topic,) + else: + topics = description.event_topic + for topic in topics: + self.topic_to_entity.setdefault(topic, []).append( + (async_add_entities, entity_class, description) + ) + + @callback + def _create_entities_from_event(self, event: Event) -> None: + """Create Axis entities from event.""" + event_id = (event.topic, event.topic_base, event.id) + if event_id in self.registered_events: + # Device has restarted and all events are initialized anew + return + self.registered_events.add(event_id) + for ( + async_add_entities, + entity_class, + description, + ) in self.topic_to_entity[event.topic_base]: + if not description.supported_fn(self.hub, event): + continue + async_add_entities([entity_class(self.hub, description, event)]) + + @callback + def initialize_platforms(self) -> None: + """Prepare event listener that can populate platform entities.""" + self.hub.api.event.subscribe( + self._create_entities_from_event, + topic_filter=tuple(self.topic_to_entity.keys()), + operation_filter=EventOperation.INITIALIZED, + ) diff --git a/homeassistant/components/axis/hub/hub.py b/homeassistant/components/axis/hub/hub.py new file mode 100644 index 00000000000..4abd1358417 --- /dev/null +++ b/homeassistant/components/axis/hub/hub.py @@ -0,0 +1,162 @@ +"""Axis network device abstraction.""" + +from __future__ import annotations + +from typing import Any + +import axis +from axis.errors import Unauthorized +from axis.interfaces.mqtt import mqtt_json_to_event +from axis.models.mqtt import ClientState +from axis.stream_manager import Signal, State + +from homeassistant.components import mqtt +from homeassistant.components.mqtt import DOMAIN as MQTT_DOMAIN +from homeassistant.components.mqtt.models import ReceiveMessage +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, format_mac +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.setup import async_when_setup + +from ..const import ATTR_MANUFACTURER, DOMAIN as AXIS_DOMAIN +from .config import AxisConfig +from .entity_loader import AxisEntityLoader + + +class AxisHub: + """Manages a Axis device.""" + + def __init__( + self, hass: HomeAssistant, config_entry: ConfigEntry, api: axis.AxisDevice + ) -> None: + """Initialize the device.""" + self.hass = hass + self.config = AxisConfig.from_config_entry(config_entry) + self.entity_loader = AxisEntityLoader(self) + self.api = api + + self.available = True + self.fw_version = api.vapix.firmware_version + self.product_type = api.vapix.product_type + self.unique_id = format_mac(api.vapix.serial_number) + + self.additional_diagnostics: dict[str, Any] = {} + + @callback + @staticmethod + def get_hub(hass: HomeAssistant, config_entry: ConfigEntry) -> AxisHub: + """Get Axis hub from config entry.""" + hub: AxisHub = hass.data[AXIS_DOMAIN][config_entry.entry_id] + return hub + + # Signals + + @property + def signal_reachable(self) -> str: + """Device specific event to signal a change in connection status.""" + return f"axis_reachable_{self.config.entry.entry_id}" + + @property + def signal_new_address(self) -> str: + """Device specific event to signal a change in device address.""" + return f"axis_new_address_{self.config.entry.entry_id}" + + # Callbacks + + @callback + def connection_status_callback(self, status: Signal) -> None: + """Handle signals of device connection status. + + This is called on every RTSP keep-alive message. + Only signal state change if state change is true. + """ + + if self.available != (status == Signal.PLAYING): + self.available = not self.available + async_dispatcher_send(self.hass, self.signal_reachable) + + @staticmethod + async def async_new_address_callback( + hass: HomeAssistant, config_entry: ConfigEntry + ) -> None: + """Handle signals of device getting new address. + + Called when config entry is updated. + This is a static method because a class method (bound method), + cannot be used with weak references. + """ + hub = AxisHub.get_hub(hass, config_entry) + hub.config = AxisConfig.from_config_entry(config_entry) + hub.api.config.host = hub.config.host + async_dispatcher_send(hass, hub.signal_new_address) + + async def async_update_device_registry(self) -> None: + """Update device registry.""" + 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.config.url, + connections={(CONNECTION_NETWORK_MAC, self.unique_id)}, + identifiers={(AXIS_DOMAIN, self.unique_id)}, + manufacturer=ATTR_MANUFACTURER, + model=f"{self.config.model} {self.product_type}", + name=self.config.name, + sw_version=self.fw_version, + ) + + 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() + except Unauthorized: + # This means the user has too low privileges + return + if status.status.state == ClientState.ACTIVE: + self.config.entry.async_on_unload( + await mqtt.async_subscribe( + hass, f"{status.config.device_topic_prefix}/#", self.mqtt_message + ) + ) + + @callback + def mqtt_message(self, message: ReceiveMessage) -> None: + """Receive Axis MQTT message.""" + self.disconnect_from_stream() + if message.topic.endswith("event/connection"): + return + event = mqtt_json_to_event(message.payload) + self.api.event.handler(event) + + # Setup and teardown methods + + @callback + def setup(self) -> None: + """Set up the device events.""" + self.entity_loader.initialize_platforms() + + self.api.stream.connection_status_callback.append( + self.connection_status_callback + ) + self.api.enable_events() + self.api.stream.start() + + if self.api.vapix.mqtt.supported: + async_when_setup(self.hass, MQTT_DOMAIN, self.async_use_mqtt) + + @callback + def disconnect_from_stream(self) -> None: + """Stop stream.""" + if self.api.stream.state != State.STOPPED: + self.api.stream.connection_status_callback.clear() + self.api.stream.stop() + + async def shutdown(self, event: Event) -> None: + """Stop the event stream.""" + self.disconnect_from_stream() + + @callback + def teardown(self) -> None: + """Reset this device to default state.""" + self.disconnect_from_stream() diff --git a/homeassistant/components/axis/light.py b/homeassistant/components/axis/light.py index cebd2f1206b..af188469a74 100644 --- a/homeassistant/components/axis/light.py +++ b/homeassistant/components/axis/light.py @@ -1,16 +1,46 @@ """Support for Axis lights.""" + +from dataclasses import dataclass from typing import Any -from axis.models.event import Event, EventOperation, EventTopic +from axis.models.event import Event, EventTopic -from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ColorMode, + LightEntity, + LightEntityDescription, +) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN as AXIS_DOMAIN -from .device import AxisNetworkDevice -from .entity import AxisEventEntity +from .entity import TOPIC_TO_EVENT_TYPE, AxisEventDescription, AxisEventEntity +from .hub import AxisHub + + +@callback +def light_name_fn(hub: AxisHub, event: Event) -> str: + """Provide Axis light entity name.""" + event_type = TOPIC_TO_EVENT_TYPE[event.topic_base] + light_id = f"led{event.id}" + light_type = hub.api.vapix.light_control[light_id].light_type + return f"{light_type} {event_type} {event.id}" + + +@dataclass(frozen=True, kw_only=True) +class AxisLightDescription(AxisEventDescription, LightEntityDescription): + """Axis light entity description.""" + + +ENTITY_DESCRIPTIONS = ( + AxisLightDescription( + key="Light state control", + event_topic=EventTopic.LIGHT_STATUS, + name_fn=light_name_fn, + supported_fn=lambda hub, event: len(hub.api.vapix.light_control) > 0, + ), +) async def async_setup_entry( @@ -18,63 +48,41 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up a Axis light.""" - device: AxisNetworkDevice = hass.data[AXIS_DOMAIN][config_entry.entry_id] - - if ( - device.api.vapix.light_control is None - or len(device.api.vapix.light_control) == 0 - ): - return - - @callback - def async_create_entity(event: Event) -> None: - """Create Axis light entity.""" - async_add_entities([AxisLight(event, device)]) - - device.api.event.subscribe( - async_create_entity, - topic_filter=EventTopic.LIGHT_STATUS, - operation_filter=EventOperation.INITIALIZED, + """Set up the Axis light platform.""" + AxisHub.get_hub(hass, config_entry).entity_loader.register_platform( + async_add_entities, AxisLight, ENTITY_DESCRIPTIONS ) class AxisLight(AxisEventEntity, LightEntity): - """Representation of a light Axis event.""" + """Representation of an Axis light.""" + + entity_description: AxisLightDescription _attr_should_poll = True + _attr_color_mode = ColorMode.BRIGHTNESS + _attr_supported_color_modes = {ColorMode.BRIGHTNESS} - def __init__(self, event: Event, device: AxisNetworkDevice) -> None: + def __init__( + self, hub: AxisHub, description: AxisLightDescription, event: Event + ) -> None: """Initialize the Axis light.""" - super().__init__(event, device) + super().__init__(hub, description, event) + self._attr_is_on = event.is_tripped self._light_id = f"led{event.id}" - self.current_intensity = 0 self.max_intensity = 0 - light_type = device.api.vapix.light_control[self._light_id].light_type - self._attr_name = f"{light_type} {self._event_type} {event.id}" - self._attr_is_on = event.is_tripped - - self._attr_supported_color_modes = {ColorMode.BRIGHTNESS} - self._attr_color_mode = ColorMode.BRIGHTNESS - async def async_added_to_hass(self) -> None: """Subscribe lights events.""" await super().async_added_to_hass() - - current_intensity = ( - await self.device.api.vapix.light_control.get_current_intensity( - self._light_id - ) + self.current_intensity = ( + await self.hub.api.vapix.light_control.get_current_intensity(self._light_id) ) - self.current_intensity = current_intensity - - max_intensity = await self.device.api.vapix.light_control.get_valid_intensity( - self._light_id - ) - self.max_intensity = max_intensity.high + self.max_intensity = ( + await self.hub.api.vapix.light_control.get_valid_intensity(self._light_id) + ).high @callback def async_event_callback(self, event: Event) -> None: @@ -90,24 +98,21 @@ class AxisLight(AxisEventEntity, LightEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Turn on light.""" if not self.is_on: - await self.device.api.vapix.light_control.activate_light(self._light_id) + await self.hub.api.vapix.light_control.activate_light(self._light_id) if ATTR_BRIGHTNESS in kwargs: intensity = int((kwargs[ATTR_BRIGHTNESS] / 255) * self.max_intensity) - await self.device.api.vapix.light_control.set_manual_intensity( + await self.hub.api.vapix.light_control.set_manual_intensity( self._light_id, intensity ) async def async_turn_off(self, **kwargs: Any) -> None: """Turn off light.""" if self.is_on: - await self.device.api.vapix.light_control.deactivate_light(self._light_id) + await self.hub.api.vapix.light_control.deactivate_light(self._light_id) async def async_update(self) -> None: """Update brightness.""" - current_intensity = ( - await self.device.api.vapix.light_control.get_current_intensity( - self._light_id - ) + self.current_intensity = ( + await self.hub.api.vapix.light_control.get_current_intensity(self._light_id) ) - self.current_intensity = current_intensity diff --git a/homeassistant/components/axis/manifest.json b/homeassistant/components/axis/manifest.json index f2a2dd40740..1065783d957 100644 --- a/homeassistant/components/axis/manifest.json +++ b/homeassistant/components/axis/manifest.json @@ -26,7 +26,7 @@ "iot_class": "local_push", "loggers": ["axis"], "quality_scale": "platinum", - "requirements": ["axis==58"], + "requirements": ["axis==60"], "ssdp": [ { "manufacturer": "AXIS" diff --git a/homeassistant/components/axis/switch.py b/homeassistant/components/axis/switch.py index c495dfbdc43..895e2a9fa01 100644 --- a/homeassistant/components/axis/switch.py +++ b/homeassistant/components/axis/switch.py @@ -1,16 +1,39 @@ """Support for Axis switches.""" + +from dataclasses import dataclass from typing import Any -from axis.models.event import Event, EventOperation, EventTopic +from axis.models.event import Event, EventTopic -from homeassistant.components.switch import SwitchEntity +from homeassistant.components.switch import ( + SwitchDeviceClass, + SwitchEntity, + SwitchEntityDescription, +) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN as AXIS_DOMAIN -from .device import AxisNetworkDevice -from .entity import AxisEventEntity +from .entity import AxisEventDescription, AxisEventEntity +from .hub import AxisHub + + +@dataclass(frozen=True, kw_only=True) +class AxisSwitchDescription(AxisEventDescription, SwitchEntityDescription): + """Axis switch entity description.""" + + +ENTITY_DESCRIPTIONS = ( + AxisSwitchDescription( + key="Relay state control", + device_class=SwitchDeviceClass.OUTLET, + entity_category=EntityCategory.CONFIG, + event_topic=EventTopic.RELAY, + supported_fn=lambda hub, event: isinstance(int(event.id), int), + name_fn=lambda hub, event: hub.api.vapix.ports[event.id].name, + ), +) async def async_setup_entry( @@ -18,29 +41,23 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up a Axis switch.""" - device: AxisNetworkDevice = hass.data[AXIS_DOMAIN][config_entry.entry_id] - - @callback - def async_create_entity(event: Event) -> None: - """Create Axis switch entity.""" - async_add_entities([AxisSwitch(event, device)]) - - device.api.event.subscribe( - async_create_entity, - topic_filter=EventTopic.RELAY, - operation_filter=EventOperation.INITIALIZED, + """Set up the Axis switch platform.""" + AxisHub.get_hub(hass, config_entry).entity_loader.register_platform( + async_add_entities, AxisSwitch, ENTITY_DESCRIPTIONS ) class AxisSwitch(AxisEventEntity, SwitchEntity): """Representation of a Axis switch.""" - def __init__(self, event: Event, device: AxisNetworkDevice) -> None: + entity_description: AxisSwitchDescription + + def __init__( + self, hub: AxisHub, description: AxisSwitchDescription, event: Event + ) -> None: """Initialize the Axis switch.""" - super().__init__(event, device) - if event.id and device.api.vapix.ports[event.id].name: - self._attr_name = device.api.vapix.ports[event.id].name + super().__init__(hub, description, event) + self._attr_is_on = event.is_tripped @callback @@ -51,8 +68,8 @@ class AxisSwitch(AxisEventEntity, SwitchEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Turn on switch.""" - await self.device.api.vapix.ports.close(self._event_id) + await self.hub.api.vapix.ports.close(self._event_id) async def async_turn_off(self, **kwargs: Any) -> None: """Turn off switch.""" - await self.device.api.vapix.ports.open(self._event_id) + await self.hub.api.vapix.ports.open(self._event_id) diff --git a/homeassistant/components/azure_devops/__init__.py b/homeassistant/components/azure_devops/__init__.py index edd06d69d2e..e2b761708a5 100644 --- a/homeassistant/components/azure_devops/__init__.py +++ b/homeassistant/components/azure_devops/__init__.py @@ -1,4 +1,5 @@ """Support for Azure DevOps.""" + from __future__ import annotations from dataclasses import dataclass @@ -98,6 +99,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: class AzureDevOpsEntity(CoordinatorEntity[DataUpdateCoordinator[list[DevOpsBuild]]]): """Defines a base Azure DevOps entity.""" + _attr_has_entity_name = True + entity_description: AzureDevOpsEntityDescription def __init__( diff --git a/homeassistant/components/azure_devops/config_flow.py b/homeassistant/components/azure_devops/config_flow.py index 81cfc2e8d45..336fd2ca8df 100644 --- a/homeassistant/components/azure_devops/config_flow.py +++ b/homeassistant/components/azure_devops/config_flow.py @@ -1,4 +1,5 @@ """Config flow to configure the Azure DevOps integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -8,8 +9,7 @@ from aioazuredevops.client import DevOpsClient import aiohttp import voluptuous as vol -from homeassistant.config_entries import ConfigFlow -from homeassistant.data_entry_flow import FlowResult +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from .const import CONF_ORG, CONF_PAT, CONF_PROJECT, DOMAIN @@ -27,7 +27,7 @@ class AzureDevOpsFlowHandler(ConfigFlow, domain=DOMAIN): async def _show_setup_form( self, errors: dict[str, str] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Show the setup form to the user.""" return self.async_show_form( step_id="user", @@ -41,7 +41,7 @@ class AzureDevOpsFlowHandler(ConfigFlow, domain=DOMAIN): errors=errors or {}, ) - async def _show_reauth_form(self, errors: dict[str, str]) -> FlowResult: + async def _show_reauth_form(self, errors: dict[str, str]) -> ConfigFlowResult: """Show the reauth form to the user.""" return self.async_show_form( step_id="reauth", @@ -75,7 +75,7 @@ class AzureDevOpsFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, str] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initiated by the user.""" if user_input is None: return await self._show_setup_form() @@ -92,7 +92,9 @@ class AzureDevOpsFlowHandler(ConfigFlow, domain=DOMAIN): return await self._show_setup_form(errors) return self._async_create_entry() - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Handle configuration by re-auth.""" if entry_data.get(CONF_ORG) and entry_data.get(CONF_PROJECT): self._organization = entry_data[CONF_ORG] @@ -121,7 +123,7 @@ class AzureDevOpsFlowHandler(ConfigFlow, domain=DOMAIN): ) return self.async_abort(reason="reauth_successful") - def _async_create_entry(self) -> FlowResult: + def _async_create_entry(self) -> ConfigFlowResult: """Handle create entry.""" return self.async_create_entry( title=f"{self._organization}/{self._project}", diff --git a/homeassistant/components/azure_devops/const.py b/homeassistant/components/azure_devops/const.py index adaf5ebe767..a5bbe9dac54 100644 --- a/homeassistant/components/azure_devops/const.py +++ b/homeassistant/components/azure_devops/const.py @@ -1,4 +1,5 @@ """Constants for the Azure DevOps integration.""" + DOMAIN = "azure_devops" CONF_ORG = "organization" diff --git a/homeassistant/components/azure_devops/icons.json b/homeassistant/components/azure_devops/icons.json new file mode 100644 index 00000000000..de720b46106 --- /dev/null +++ b/homeassistant/components/azure_devops/icons.json @@ -0,0 +1,9 @@ +{ + "entity": { + "sensor": { + "latest_build": { + "default": "mdi:pipe" + } + } + } +} diff --git a/homeassistant/components/azure_devops/sensor.py b/homeassistant/components/azure_devops/sensor.py index 6daf9b434df..a6e4ee95cad 100644 --- a/homeassistant/components/azure_devops/sensor.py +++ b/homeassistant/components/azure_devops/sensor.py @@ -1,4 +1,5 @@ """Support for Azure DevOps sensors.""" + from __future__ import annotations from collections.abc import Callable @@ -17,23 +18,15 @@ from . import AzureDevOpsDeviceEntity, AzureDevOpsEntityDescription from .const import CONF_ORG, DOMAIN -@dataclass(frozen=True) -class AzureDevOpsSensorEntityDescriptionMixin: - """Mixin class for required Azure DevOps sensor description keys.""" - - build_key: int - - -@dataclass(frozen=True) +@dataclass(frozen=True, kw_only=True) class AzureDevOpsSensorEntityDescription( - AzureDevOpsEntityDescription, - SensorEntityDescription, - AzureDevOpsSensorEntityDescriptionMixin, + AzureDevOpsEntityDescription, SensorEntityDescription ): """Class describing Azure DevOps sensor entities.""" - attrs: Callable[[DevOpsBuild], Any] = round - value: Callable[[DevOpsBuild], StateType] = round + build_key: int + attrs: Callable[[DevOpsBuild], Any] + value: Callable[[DevOpsBuild], StateType] async def async_setup_entry( @@ -47,8 +40,8 @@ async def async_setup_entry( coordinator, AzureDevOpsSensorEntityDescription( key=f"{build.project.id}_{build.definition.id}_latest_build", - name=f"{build.project.name} {build.definition.name} Latest Build", - icon="mdi:pipe", + translation_key="latest_build", + translation_placeholders={"definition_name": build.definition.name}, attrs=lambda build: { "definition_id": build.definition.id, "definition_name": build.definition.name, diff --git a/homeassistant/components/azure_devops/strings.json b/homeassistant/components/azure_devops/strings.json index ad8ebaa016e..c163aee5b7f 100644 --- a/homeassistant/components/azure_devops/strings.json +++ b/homeassistant/components/azure_devops/strings.json @@ -28,5 +28,12 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } + }, + "entity": { + "sensor": { + "latest_build": { + "name": "{definition_name} latest build" + } + } } } diff --git a/homeassistant/components/azure_event_hub/__init__.py b/homeassistant/components/azure_event_hub/__init__.py index 4e69bea1a38..0a84ca44141 100644 --- a/homeassistant/components/azure_event_hub/__init__.py +++ b/homeassistant/components/azure_event_hub/__init__.py @@ -1,4 +1,5 @@ """Support for Azure Event Hubs.""" + from __future__ import annotations import asyncio @@ -157,7 +158,7 @@ class AzureEventHub: """ logging.getLogger("azure.eventhub").setLevel(logging.WARNING) self._listener_remover = self.hass.bus.async_listen( - MATCH_ALL, self.async_listen + MATCH_ALL, self.async_listen, run_immediately=True ) self._schedule_next_send() diff --git a/homeassistant/components/azure_event_hub/client.py b/homeassistant/components/azure_event_hub/client.py index f03f4339e69..0bf2cb69583 100644 --- a/homeassistant/components/azure_event_hub/client.py +++ b/homeassistant/components/azure_event_hub/client.py @@ -1,4 +1,5 @@ """File for Azure Event Hub models.""" + from __future__ import annotations from abc import ABC, abstractmethod diff --git a/homeassistant/components/azure_event_hub/config_flow.py b/homeassistant/components/azure_event_hub/config_flow.py index 3573d5e72aa..c088b35a002 100644 --- a/homeassistant/components/azure_event_hub/config_flow.py +++ b/homeassistant/components/azure_event_hub/config_flow.py @@ -1,4 +1,5 @@ """Config flow for azure_event_hub integration.""" + from __future__ import annotations from copy import deepcopy @@ -8,9 +9,8 @@ from typing import Any from azure.eventhub.exceptions import EventHubError import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.schema_config_entry_flow import ( SchemaFlowFormStep, SchemaOptionsFlowHandler, @@ -79,7 +79,7 @@ async def validate_data(data: dict[str, Any]) -> dict[str, str] | None: return None -class AEHConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class AEHConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for azure event hub.""" VERSION: int = 1 @@ -93,14 +93,14 @@ class AEHConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback def async_get_options_flow( - config_entry: config_entries.ConfigEntry, + config_entry: ConfigEntry, ) -> SchemaOptionsFlowHandler: """Get the options flow for this handler.""" return SchemaOptionsFlowHandler(config_entry, OPTIONS_FLOW) async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial user step.""" if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") @@ -116,7 +116,7 @@ class AEHConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_conn_string( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the connection string steps.""" errors = await self.async_update_and_validate_data(user_input) if user_input is None or errors is not None: @@ -136,7 +136,7 @@ class AEHConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_sas( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the sas steps.""" errors = await self.async_update_and_validate_data(user_input) if user_input is None or errors is not None: @@ -154,7 +154,9 @@ class AEHConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): options=self._options, ) - async def async_step_import(self, import_config: dict[str, Any]) -> FlowResult: + async def async_step_import( + self, import_config: dict[str, Any] + ) -> ConfigFlowResult: """Import config from configuration.yaml.""" if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") diff --git a/homeassistant/components/azure_event_hub/const.py b/homeassistant/components/azure_event_hub/const.py index 8c90b5daaa0..174fdddc6a1 100644 --- a/homeassistant/components/azure_event_hub/const.py +++ b/homeassistant/components/azure_event_hub/const.py @@ -1,4 +1,5 @@ """Constants and shared schema for the Azure Event Hub integration.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/azure_service_bus/notify.py b/homeassistant/components/azure_service_bus/notify.py index 4005460ecae..38c57b3db19 100644 --- a/homeassistant/components/azure_service_bus/notify.py +++ b/homeassistant/components/azure_service_bus/notify.py @@ -1,4 +1,5 @@ """Support for azure service bus notification.""" + from __future__ import annotations import json diff --git a/homeassistant/components/backup/__init__.py b/homeassistant/components/backup/__init__.py index 8f19436fb1d..2f9019300db 100644 --- a/homeassistant/components/backup/__init__.py +++ b/homeassistant/components/backup/__init__.py @@ -1,4 +1,5 @@ """The Backup integration.""" + from homeassistant.components.hassio import is_hassio from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers import config_validation as cv diff --git a/homeassistant/components/backup/const.py b/homeassistant/components/backup/const.py index a4a08fff75d..9573d522b56 100644 --- a/homeassistant/components/backup/const.py +++ b/homeassistant/components/backup/const.py @@ -1,4 +1,5 @@ """Constants for the Backup integration.""" + from logging import getLogger DOMAIN = "backup" diff --git a/homeassistant/components/backup/http.py b/homeassistant/components/backup/http.py index a6a7a2346e4..793192aa623 100644 --- a/homeassistant/components/backup/http.py +++ b/homeassistant/components/backup/http.py @@ -1,4 +1,5 @@ """Http view for the Backup integration.""" + from __future__ import annotations from http import HTTPStatus @@ -6,7 +7,7 @@ from http import HTTPStatus from aiohttp.hdrs import CONTENT_DISPOSITION from aiohttp.web import FileResponse, Request, Response -from homeassistant.components.http.view import HomeAssistantView +from homeassistant.components.http import KEY_HASS, HomeAssistantView from homeassistant.core import HomeAssistant, callback from homeassistant.util import slugify @@ -35,7 +36,7 @@ class DownloadBackupView(HomeAssistantView): if not request["hass_user"].is_admin: return Response(status=HTTPStatus.UNAUTHORIZED) - manager: BackupManager = request.app["hass"].data[DOMAIN] + manager: BackupManager = request.app[KEY_HASS].data[DOMAIN] backup = await manager.get_backup(slug) if backup is None or not backup.path.exists(): diff --git a/homeassistant/components/backup/icons.json b/homeassistant/components/backup/icons.json new file mode 100644 index 00000000000..cba4fb22831 --- /dev/null +++ b/homeassistant/components/backup/icons.json @@ -0,0 +1,5 @@ +{ + "services": { + "create": "mdi:cloud-upload" + } +} diff --git a/homeassistant/components/backup/manager.py b/homeassistant/components/backup/manager.py index 4c06f2171b6..e3331836202 100644 --- a/homeassistant/components/backup/manager.py +++ b/homeassistant/components/backup/manager.py @@ -1,4 +1,5 @@ """Backup manager for the Backup integration.""" + from __future__ import annotations import asyncio @@ -125,7 +126,7 @@ class BackupManager: async def load_platforms(self) -> None: """Load backup platforms.""" await integration_platform.async_process_integration_platforms( - self.hass, DOMAIN, self._add_platform + self.hass, DOMAIN, self._add_platform, wait_for_platforms=True ) LOGGER.debug("Loaded %s platforms", len(self.platforms)) self.loaded_platforms = True diff --git a/homeassistant/components/backup/manifest.json b/homeassistant/components/backup/manifest.json index be75e4717ef..1ec9b748cda 100644 --- a/homeassistant/components/backup/manifest.json +++ b/homeassistant/components/backup/manifest.json @@ -4,7 +4,6 @@ "codeowners": ["@home-assistant/core"], "dependencies": ["http", "websocket_api"], "documentation": "https://www.home-assistant.io/integrations/backup", - "import_executor": true, "integration_type": "system", "iot_class": "calculated", "quality_scale": "internal", diff --git a/homeassistant/components/backup/websocket.py b/homeassistant/components/backup/websocket.py index c1eed4294c2..08d6fda3663 100644 --- a/homeassistant/components/backup/websocket.py +++ b/homeassistant/components/backup/websocket.py @@ -1,4 +1,5 @@ """Websocket commands for the Backup integration.""" + from typing import Any import voluptuous as vol diff --git a/homeassistant/components/baf/__init__.py b/homeassistant/components/baf/__init__.py index e685ec6dc8c..d3b29b52e44 100644 --- a/homeassistant/components/baf/__init__.py +++ b/homeassistant/components/baf/__init__.py @@ -1,4 +1,5 @@ """The Big Ass Fans integration.""" + from __future__ import annotations from asyncio import timeout diff --git a/homeassistant/components/baf/binary_sensor.py b/homeassistant/components/baf/binary_sensor.py index 50e8cd78629..e95e197b8be 100644 --- a/homeassistant/components/baf/binary_sensor.py +++ b/homeassistant/components/baf/binary_sensor.py @@ -1,4 +1,5 @@ """Support for Big Ass Fans binary sensors.""" + from __future__ import annotations from collections.abc import Callable @@ -21,20 +22,14 @@ from .entity import BAFEntity from .models import BAFData -@dataclass(frozen=True) -class BAFBinarySensorDescriptionMixin: - """Required values for BAF binary sensors.""" - - value_fn: Callable[[Device], bool | None] - - -@dataclass(frozen=True) +@dataclass(frozen=True, kw_only=True) class BAFBinarySensorDescription( BinarySensorEntityDescription, - BAFBinarySensorDescriptionMixin, ): """Class describing BAF binary sensor entities.""" + value_fn: Callable[[Device], bool | None] + OCCUPANCY_SENSORS = ( BAFBinarySensorDescription( diff --git a/homeassistant/components/baf/climate.py b/homeassistant/components/baf/climate.py index 907e8ff2356..f451c5e7a71 100644 --- a/homeassistant/components/baf/climate.py +++ b/homeassistant/components/baf/climate.py @@ -1,4 +1,5 @@ """Support for Big Ass Fans auto comfort.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/baf/config_flow.py b/homeassistant/components/baf/config_flow.py index 0aaf2189c28..d0a3a82b396 100644 --- a/homeassistant/components/baf/config_flow.py +++ b/homeassistant/components/baf/config_flow.py @@ -1,4 +1,5 @@ """Config flow for baf.""" + from __future__ import annotations from asyncio import timeout @@ -9,10 +10,9 @@ from aiobafi6 import Device, Service from aiobafi6.discovery import PORT import voluptuous as vol -from homeassistant import config_entries from homeassistant.components import zeroconf +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_IP_ADDRESS -from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN, RUN_TIMEOUT from .models import BAFDiscovery @@ -34,7 +34,7 @@ async def async_try_connect(ip_address: str) -> Device: return device -class BAFFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class BAFFlowHandler(ConfigFlow, domain=DOMAIN): """Handle BAF discovery config flow.""" VERSION = 1 @@ -45,7 +45,7 @@ class BAFFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_zeroconf( self, discovery_info: zeroconf.ZeroconfServiceInfo - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle zeroconf discovery.""" if discovery_info.ip_address.version == 6: return self.async_abort(reason="ipv6_not_supported") @@ -61,7 +61,7 @@ class BAFFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_discovery_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm discovery.""" assert self.discovery is not None discovery = self.discovery @@ -83,7 +83,7 @@ class BAFFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors = {} ip_address = (user_input or {}).get(CONF_IP_ADDRESS, "") diff --git a/homeassistant/components/baf/entity.py b/homeassistant/components/baf/entity.py index 82ea0c16092..487e601b542 100644 --- a/homeassistant/components/baf/entity.py +++ b/homeassistant/components/baf/entity.py @@ -1,4 +1,5 @@ """The baf integration entities.""" + from __future__ import annotations from aiobafi6 import Device diff --git a/homeassistant/components/baf/fan.py b/homeassistant/components/baf/fan.py index e2d1c5fcb3a..15c6519747d 100644 --- a/homeassistant/components/baf/fan.py +++ b/homeassistant/components/baf/fan.py @@ -1,4 +1,5 @@ """Support for Big Ass Fans fan.""" + from __future__ import annotations import math diff --git a/homeassistant/components/baf/light.py b/homeassistant/components/baf/light.py index ed5eea8796f..e203e12cf96 100644 --- a/homeassistant/components/baf/light.py +++ b/homeassistant/components/baf/light.py @@ -1,4 +1,5 @@ """Support for Big Ass Fans lights.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/baf/models.py b/homeassistant/components/baf/models.py index de5c4a3498b..c94b73d9abd 100644 --- a/homeassistant/components/baf/models.py +++ b/homeassistant/components/baf/models.py @@ -1,4 +1,5 @@ """The baf integration models.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/baf/number.py b/homeassistant/components/baf/number.py index 9dd4180c7e1..43da381391c 100644 --- a/homeassistant/components/baf/number.py +++ b/homeassistant/components/baf/number.py @@ -1,4 +1,5 @@ """Support for Big Ass Fans number.""" + from __future__ import annotations from collections.abc import Callable @@ -22,18 +23,13 @@ from .entity import BAFEntity from .models import BAFData -@dataclass(frozen=True) -class BAFNumberDescriptionMixin: - """Required values for BAF sensors.""" +@dataclass(frozen=True, kw_only=True) +class BAFNumberDescription(NumberEntityDescription): + """Class describing BAF sensor entities.""" value_fn: Callable[[Device], int | None] -@dataclass(frozen=True) -class BAFNumberDescription(NumberEntityDescription, BAFNumberDescriptionMixin): - """Class describing BAF sensor entities.""" - - AUTO_COMFORT_NUMBER_DESCRIPTIONS = ( BAFNumberDescription( key="comfort_min_speed", diff --git a/homeassistant/components/baf/sensor.py b/homeassistant/components/baf/sensor.py index 5c8d8f2979b..fc052b1e48b 100644 --- a/homeassistant/components/baf/sensor.py +++ b/homeassistant/components/baf/sensor.py @@ -1,4 +1,5 @@ """Support for Big Ass Fans sensors.""" + from __future__ import annotations from collections.abc import Callable @@ -28,20 +29,14 @@ from .entity import BAFEntity from .models import BAFData -@dataclass(frozen=True) -class BAFSensorDescriptionMixin: - """Required values for BAF sensors.""" - - value_fn: Callable[[Device], int | float | str | None] - - -@dataclass(frozen=True) +@dataclass(frozen=True, kw_only=True) class BAFSensorDescription( SensorEntityDescription, - BAFSensorDescriptionMixin, ): """Class describing BAF sensor entities.""" + value_fn: Callable[[Device], int | float | str | None] + AUTO_COMFORT_SENSORS = ( BAFSensorDescription( @@ -105,10 +100,11 @@ async def async_setup_entry( """Set up BAF fan sensors.""" data: BAFData = hass.data[DOMAIN][entry.entry_id] device = data.device - sensors_descriptions: list[BAFSensorDescription] = [] - for description in DEFINED_ONLY_SENSORS: - if getattr(device, description.key): - sensors_descriptions.append(description) + sensors_descriptions: list[BAFSensorDescription] = [ + description + for description in DEFINED_ONLY_SENSORS + if getattr(device, description.key) + ] if device.has_auto_comfort: sensors_descriptions.extend(AUTO_COMFORT_SENSORS) if device.has_fan: diff --git a/homeassistant/components/baf/switch.py b/homeassistant/components/baf/switch.py index ccb8aee36e5..38248e48d09 100644 --- a/homeassistant/components/baf/switch.py +++ b/homeassistant/components/baf/switch.py @@ -1,4 +1,5 @@ """Support for Big Ass Fans switch.""" + from __future__ import annotations from collections.abc import Callable @@ -18,20 +19,14 @@ from .entity import BAFEntity from .models import BAFData -@dataclass(frozen=True) -class BAFSwitchDescriptionMixin: - """Required values for BAF sensors.""" - - value_fn: Callable[[Device], bool | None] - - -@dataclass(frozen=True) +@dataclass(frozen=True, kw_only=True) class BAFSwitchDescription( SwitchEntityDescription, - BAFSwitchDescriptionMixin, ): """Class describing BAF switch entities.""" + value_fn: Callable[[Device], bool | None] + BASE_SWITCHES = [ BAFSwitchDescription( diff --git a/homeassistant/components/baidu/tts.py b/homeassistant/components/baidu/tts.py index 05e2956c10e..859e79adec9 100644 --- a/homeassistant/components/baidu/tts.py +++ b/homeassistant/components/baidu/tts.py @@ -1,4 +1,5 @@ """Support for Baidu speech service.""" + import logging from aip import AipSpeech diff --git a/homeassistant/components/balboa/__init__.py b/homeassistant/components/balboa/__init__.py index 1d2cd042918..d6a80e8fa8f 100644 --- a/homeassistant/components/balboa/__init__.py +++ b/homeassistant/components/balboa/__init__.py @@ -1,4 +1,5 @@ """The Balboa Spa Client integration.""" + from __future__ import annotations from datetime import datetime, timedelta diff --git a/homeassistant/components/balboa/binary_sensor.py b/homeassistant/components/balboa/binary_sensor.py index 8584ed2783c..d3352208cd9 100644 --- a/homeassistant/components/balboa/binary_sensor.py +++ b/homeassistant/components/balboa/binary_sensor.py @@ -1,4 +1,5 @@ """Support for Balboa Spa binary sensors.""" + from __future__ import annotations from collections.abc import Callable @@ -33,20 +34,13 @@ async def async_setup_entry( async_add_entities(entities) -@dataclass(frozen=True) -class BalboaBinarySensorEntityDescriptionMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class BalboaBinarySensorEntityDescription(BinarySensorEntityDescription): + """A class that describes Balboa binary sensor entities.""" is_on_fn: Callable[[SpaClient], bool] -@dataclass(frozen=True) -class BalboaBinarySensorEntityDescription( - BinarySensorEntityDescription, BalboaBinarySensorEntityDescriptionMixin -): - """A class that describes Balboa binary sensor entities.""" - - BINARY_SENSOR_DESCRIPTIONS = ( BalboaBinarySensorEntityDescription( key="Filter1", diff --git a/homeassistant/components/balboa/climate.py b/homeassistant/components/balboa/climate.py index b9cce73de75..456fa0dd081 100644 --- a/homeassistant/components/balboa/climate.py +++ b/homeassistant/components/balboa/climate.py @@ -1,4 +1,5 @@ """Support for Balboa Spa Wifi adaptor.""" + from __future__ import annotations from enum import IntEnum diff --git a/homeassistant/components/balboa/config_flow.py b/homeassistant/components/balboa/config_flow.py index 73f19f0e327..2dc98fbcd69 100644 --- a/homeassistant/components/balboa/config_flow.py +++ b/homeassistant/components/balboa/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Balboa Spa Client integration.""" + from __future__ import annotations import logging @@ -8,11 +9,10 @@ from pybalboa import SpaClient from pybalboa.exceptions import SpaConnectionError import voluptuous as vol -from homeassistant import exceptions -from homeassistant.config_entries import ConfigEntry, ConfigFlow +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.device_registry import format_mac from homeassistant.helpers.schema_config_entry_flow import ( SchemaFlowFormStep, @@ -65,7 +65,7 @@ class BalboaSpaClientFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" errors = {} if user_input is not None: @@ -87,5 +87,5 @@ class BalboaSpaClientFlowHandler(ConfigFlow, domain=DOMAIN): ) -class CannotConnect(exceptions.HomeAssistantError): +class CannotConnect(HomeAssistantError): """Error to indicate we cannot connect.""" diff --git a/homeassistant/components/balboa/const.py b/homeassistant/components/balboa/const.py index 23189a4d7e9..1536959f8be 100644 --- a/homeassistant/components/balboa/const.py +++ b/homeassistant/components/balboa/const.py @@ -1,4 +1,5 @@ """Constants for the Balboa Spa Client integration.""" + DOMAIN = "balboa" CONF_SYNC_TIME = "sync_time" DEFAULT_SYNC_TIME = False diff --git a/homeassistant/components/balboa/entity.py b/homeassistant/components/balboa/entity.py index e02579658da..a7d75bfbdf5 100644 --- a/homeassistant/components/balboa/entity.py +++ b/homeassistant/components/balboa/entity.py @@ -1,4 +1,5 @@ """Balboa entities.""" + from __future__ import annotations from pybalboa import EVENT_UPDATE, SpaClient diff --git a/homeassistant/components/balboa/fan.py b/homeassistant/components/balboa/fan.py index f6edc45c342..24fe3bdd71a 100644 --- a/homeassistant/components/balboa/fan.py +++ b/homeassistant/components/balboa/fan.py @@ -1,4 +1,5 @@ """Support for Balboa Spa pumps.""" + from __future__ import annotations import math diff --git a/homeassistant/components/balboa/icons.json b/homeassistant/components/balboa/icons.json index fb1b6d01ed4..7261f71bd00 100644 --- a/homeassistant/components/balboa/icons.json +++ b/homeassistant/components/balboa/icons.json @@ -2,21 +2,21 @@ "entity": { "binary_sensor": { "filter_1": { - "default": "mdi:sync-off", + "default": "mdi:sync", "state": { - "on": "mdi:sync" + "off": "mdi:sync-off" } }, "filter_2": { - "default": "mdi:sync-off", + "default": "mdi:sync", "state": { - "on": "mdi:sync" + "off": "mdi:sync-off" } }, "circ_pump": { - "default": "mdi:pump-off", + "default": "mdi:pump", "state": { - "on": "mdi:pump" + "off": "mdi:pump-off" } } }, diff --git a/homeassistant/components/balboa/light.py b/homeassistant/components/balboa/light.py index 00b8eb979a2..5dc8d48ef9d 100644 --- a/homeassistant/components/balboa/light.py +++ b/homeassistant/components/balboa/light.py @@ -1,4 +1,5 @@ """Support for Balboa Spa lights.""" + from __future__ import annotations from typing import Any, cast diff --git a/homeassistant/components/bang_olufsen/__init__.py b/homeassistant/components/bang_olufsen/__init__.py index 3071b8fc6b2..2488c2e64f5 100644 --- a/homeassistant/components/bang_olufsen/__init__.py +++ b/homeassistant/components/bang_olufsen/__init__.py @@ -1,4 +1,5 @@ """The Bang & Olufsen integration.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/homeassistant/components/bang_olufsen/config_flow.py b/homeassistant/components/bang_olufsen/config_flow.py index 6a26c4c5984..e3b8f9979d1 100644 --- a/homeassistant/components/bang_olufsen/config_flow.py +++ b/homeassistant/components/bang_olufsen/config_flow.py @@ -1,4 +1,5 @@ """Config flow for the Bang & Olufsen integration.""" + from __future__ import annotations from ipaddress import AddressValueError, IPv4Address @@ -10,9 +11,8 @@ from mozart_api.mozart_client import MozartClient import voluptuous as vol from homeassistant.components.zeroconf import ZeroconfServiceInfo -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_MODEL -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.selector import SelectSelector, SelectSelectorConfig from .const import ( @@ -62,7 +62,7 @@ class BangOlufsenConfigFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" data_schema = vol.Schema( { @@ -121,7 +121,7 @@ class BangOlufsenConfigFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_zeroconf( self, discovery_info: ZeroconfServiceInfo - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle discovery using Zeroconf.""" # Check if the discovered device is a Mozart device @@ -149,7 +149,7 @@ class BangOlufsenConfigFlowHandler(ConfigFlow, domain=DOMAIN): return await self.async_step_zeroconf_confirm() - async def _create_entry(self) -> FlowResult: + async def _create_entry(self) -> ConfigFlowResult: """Create the config entry for a discovered or manually configured Bang & Olufsen device.""" # Ensure that created entities have a unique and easily identifiable id and not a "friendly name" self._name = f"{self._model}-{self._serial_number}" @@ -166,7 +166,7 @@ class BangOlufsenConfigFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_zeroconf_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm the configuration of the device.""" if user_input is not None: return await self._create_entry() diff --git a/homeassistant/components/bang_olufsen/const.py b/homeassistant/components/bang_olufsen/const.py index 3a6638fe31a..4d53daeb510 100644 --- a/homeassistant/components/bang_olufsen/const.py +++ b/homeassistant/components/bang_olufsen/const.py @@ -10,27 +10,27 @@ from mozart_api.models import Source, SourceArray, SourceTypeEnum from homeassistant.components.media_player import MediaPlayerState, MediaType -class SOURCE_ENUM(StrEnum): +class BangOlufsenSource(StrEnum): """Enum used for associating device source ids with friendly names. May not include all sources.""" - uriStreamer = "Audio Streamer" # noqa: N815 - bluetooth = "Bluetooth" - airPlay = "AirPlay" # noqa: N815 - chromeCast = "Chromecast built-in" # noqa: N815 - spotify = "Spotify Connect" - generator = "Tone Generator" - lineIn = "Line-In" # noqa: N815 - spdif = "Optical" - netRadio = "B&O Radio" # noqa: N815 - local = "Local" - dlna = "DLNA" - qplay = "QPlay" - wpl = "Wireless Powerlink" - pl = "Powerlink" - tv = "TV" - deezer = "Deezer" - beolink = "Networklink" - tidalConnect = "Tidal Connect" # noqa: N815 + URI_STREAMER = "Audio Streamer" + BLUETOOTH = "Bluetooth" + AIR_PLAY = "AirPlay" + CHROMECAST = "Chromecast built-in" + SPOTIFY = "Spotify Connect" + GENERATOR = "Tone Generator" + LINE_IN = "Line-In" + SPDIF = "Optical" + NET_RADIO = "B&O Radio" + LOCAL = "Local" + DLNA = "DLNA" + QPLAY = "QPlay" + WPL = "Wireless Powerlink" + PL = "Powerlink" + TV = "TV" + DEEZER = "Deezer" + BEOLINK = "Networklink" + TIDAL_CONNECT = "Tidal Connect" BANG_OLUFSEN_STATES: dict[str, MediaPlayerState] = { @@ -48,7 +48,7 @@ BANG_OLUFSEN_STATES: dict[str, MediaPlayerState] = { # Media types for play_media -class BANG_OLUFSEN_MEDIA_TYPE(StrEnum): +class BangOlufsenMediaType(StrEnum): """Bang & Olufsen specific media types.""" FAVOURITE = "favourite" @@ -57,7 +57,7 @@ class BANG_OLUFSEN_MEDIA_TYPE(StrEnum): TTS = "provider" -class MODEL_ENUM(StrEnum): +class BangOlufsenModel(StrEnum): """Enum for compatible model names.""" BEOLAB_8 = "BeoLab 8" @@ -72,7 +72,7 @@ class MODEL_ENUM(StrEnum): # Dispatcher events -class WEBSOCKET_NOTIFICATION(StrEnum): +class WebsocketNotification(StrEnum): """Enum for WebSocket notification types.""" PLAYBACK_ERROR: Final[str] = "playback_error" @@ -94,14 +94,14 @@ class WEBSOCKET_NOTIFICATION(StrEnum): DOMAIN: Final[str] = "bang_olufsen" # Default values for configuration. -DEFAULT_MODEL: Final[str] = MODEL_ENUM.BEOSOUND_BALANCE +DEFAULT_MODEL: Final[str] = BangOlufsenModel.BEOSOUND_BALANCE # Configuration. CONF_SERIAL_NUMBER: Final = "serial_number" CONF_BEOLINK_JID: Final = "jid" # Models to choose from in manual configuration. -COMPATIBLE_MODELS: list[str] = [x.value for x in MODEL_ENUM] +COMPATIBLE_MODELS: list[str] = [x.value for x in BangOlufsenModel] # Attribute names for zeroconf discovery. ATTR_TYPE_NUMBER: Final[str] = "tn" @@ -113,10 +113,10 @@ ATTR_FRIENDLY_NAME: Final[str] = "fn" BANG_OLUFSEN_ON: Final[str] = "on" VALID_MEDIA_TYPES: Final[tuple] = ( - BANG_OLUFSEN_MEDIA_TYPE.FAVOURITE, - BANG_OLUFSEN_MEDIA_TYPE.DEEZER, - BANG_OLUFSEN_MEDIA_TYPE.RADIO, - BANG_OLUFSEN_MEDIA_TYPE.TTS, + BangOlufsenMediaType.FAVOURITE, + BangOlufsenMediaType.DEEZER, + BangOlufsenMediaType.RADIO, + BangOlufsenMediaType.TTS, MediaType.MUSIC, MediaType.URL, MediaType.CHANNEL, diff --git a/homeassistant/components/bang_olufsen/entity.py b/homeassistant/components/bang_olufsen/entity.py index 76d93ca0635..4f8ff43e0a8 100644 --- a/homeassistant/components/bang_olufsen/entity.py +++ b/homeassistant/components/bang_olufsen/entity.py @@ -1,4 +1,5 @@ """Entity representing a Bang & Olufsen device.""" + from __future__ import annotations from typing import cast @@ -36,7 +37,6 @@ class BangOlufsenBase: # Set the configuration variables. self._host: str = self.entry.data[CONF_HOST] - self._name: str = self.entry.title self._unique_id: str = cast(str, self.entry.unique_id) # Objects that get directly updated by notifications. @@ -54,15 +54,13 @@ class BangOlufsenEntity(Entity, BangOlufsenBase): """Base Entity for BangOlufsen entities.""" _attr_has_entity_name = True + _attr_should_poll = False def __init__(self, entry: ConfigEntry, client: MozartClient) -> None: """Initialize the object.""" super().__init__(entry, client) self._attr_device_info = DeviceInfo(identifiers={(DOMAIN, self._unique_id)}) - self._attr_device_class = None - self._attr_entity_category = None - self._attr_should_poll = False async def _update_connection_state(self, connection_state: bool) -> None: """Update entity connection state.""" diff --git a/homeassistant/components/bang_olufsen/media_player.py b/homeassistant/components/bang_olufsen/media_player.py index eaafddcabd6..935c057efc8 100644 --- a/homeassistant/components/bang_olufsen/media_player.py +++ b/homeassistant/components/bang_olufsen/media_player.py @@ -1,4 +1,5 @@ """Media player entity for the Bang & Olufsen integration.""" + from __future__ import annotations import json @@ -50,16 +51,16 @@ from homeassistant.util.dt import utcnow from . import BangOlufsenData from .const import ( - BANG_OLUFSEN_MEDIA_TYPE, BANG_OLUFSEN_STATES, CONF_BEOLINK_JID, CONNECTION_STATUS, DOMAIN, FALLBACK_SOURCES, HIDDEN_SOURCE_IDS, - SOURCE_ENUM, VALID_MEDIA_TYPES, - WEBSOCKET_NOTIFICATION, + BangOlufsenMediaType, + BangOlufsenSource, + WebsocketNotification, ) from .entity import BangOlufsenEntity @@ -94,11 +95,12 @@ async def async_setup_entry( async_add_entities(new_entities=[BangOlufsenMediaPlayer(config_entry, data.client)]) -class BangOlufsenMediaPlayer(MediaPlayerEntity, BangOlufsenEntity): +class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity): """Representation of a media player.""" - _attr_has_entity_name = False _attr_icon = "mdi:speaker-wireless" + _attr_name = None + _attr_device_class = MediaPlayerDeviceClass.SPEAKER _attr_supported_features = BANG_OLUFSEN_FEATURES def __init__(self, entry: ConfigEntry, client: MozartClient) -> None: @@ -113,12 +115,9 @@ class BangOlufsenMediaPlayer(MediaPlayerEntity, BangOlufsenEntity): identifiers={(DOMAIN, self._unique_id)}, manufacturer="Bang & Olufsen", model=self._model, - name=cast(str, self.name), serial_number=self._unique_id, ) - self._attr_name = self._name self._attr_unique_id = self._unique_id - self._attr_device_class = MediaPlayerDeviceClass.SPEAKER # Misc. variables. self._audio_sources: dict[str, str] = {} @@ -146,7 +145,7 @@ class BangOlufsenMediaPlayer(MediaPlayerEntity, BangOlufsenEntity): self.async_on_remove( async_dispatcher_connect( self.hass, - f"{self._unique_id}_{WEBSOCKET_NOTIFICATION.PLAYBACK_ERROR}", + f"{self._unique_id}_{WebsocketNotification.PLAYBACK_ERROR}", self._update_playback_error, ) ) @@ -154,7 +153,7 @@ class BangOlufsenMediaPlayer(MediaPlayerEntity, BangOlufsenEntity): self.async_on_remove( async_dispatcher_connect( self.hass, - f"{self._unique_id}_{WEBSOCKET_NOTIFICATION.PLAYBACK_METADATA}", + f"{self._unique_id}_{WebsocketNotification.PLAYBACK_METADATA}", self._update_playback_metadata, ) ) @@ -162,35 +161,35 @@ class BangOlufsenMediaPlayer(MediaPlayerEntity, BangOlufsenEntity): self.async_on_remove( async_dispatcher_connect( self.hass, - f"{self._unique_id}_{WEBSOCKET_NOTIFICATION.PLAYBACK_PROGRESS}", + f"{self._unique_id}_{WebsocketNotification.PLAYBACK_PROGRESS}", self._update_playback_progress, ) ) self.async_on_remove( async_dispatcher_connect( self.hass, - f"{self._unique_id}_{WEBSOCKET_NOTIFICATION.PLAYBACK_STATE}", + f"{self._unique_id}_{WebsocketNotification.PLAYBACK_STATE}", self._update_playback_state, ) ) self.async_on_remove( async_dispatcher_connect( self.hass, - f"{self._unique_id}_{WEBSOCKET_NOTIFICATION.REMOTE_MENU_CHANGED}", + f"{self._unique_id}_{WebsocketNotification.REMOTE_MENU_CHANGED}", self._update_sources, ) ) self.async_on_remove( async_dispatcher_connect( self.hass, - f"{self._unique_id}_{WEBSOCKET_NOTIFICATION.SOURCE_CHANGE}", + f"{self._unique_id}_{WebsocketNotification.SOURCE_CHANGE}", self._update_source_change, ) ) self.async_on_remove( async_dispatcher_connect( self.hass, - f"{self._unique_id}_{WEBSOCKET_NOTIFICATION.VOLUME}", + f"{self._unique_id}_{WebsocketNotification.VOLUME}", self._update_volume, ) ) @@ -336,7 +335,10 @@ class BangOlufsenMediaPlayer(MediaPlayerEntity, BangOlufsenEntity): self._source_change = data # Check if source is line-in or optical and progress should be updated - if self._source_change.id in (SOURCE_ENUM.lineIn, SOURCE_ENUM.spdif): + if self._source_change.id in ( + BangOlufsenSource.LINE_IN, + BangOlufsenSource.SPDIF, + ): self._playback_progress = PlaybackProgress(progress=0) async def _update_volume(self, data: VolumeState) -> None: @@ -368,7 +370,7 @@ class BangOlufsenMediaPlayer(MediaPlayerEntity, BangOlufsenEntity): def media_content_type(self) -> str: """Return the current media type.""" # Hard to determine content type - if self.source == SOURCE_ENUM.uriStreamer: + if self.source == BangOlufsenSource.URI_STREAMER: return MediaType.URL return MediaType.MUSIC @@ -426,21 +428,21 @@ class BangOlufsenMediaPlayer(MediaPlayerEntity, BangOlufsenEntity): # Try to fix some of the source_change chromecast weirdness. if hasattr(self._playback_metadata, "title"): # source_change is chromecast but line in is selected. - if self._playback_metadata.title == SOURCE_ENUM.lineIn: - return SOURCE_ENUM.lineIn + if self._playback_metadata.title == BangOlufsenSource.LINE_IN: + return BangOlufsenSource.LINE_IN # source_change is chromecast but bluetooth is selected. - if self._playback_metadata.title == SOURCE_ENUM.bluetooth: - return SOURCE_ENUM.bluetooth + if self._playback_metadata.title == BangOlufsenSource.BLUETOOTH: + return BangOlufsenSource.BLUETOOTH # source_change is line in, bluetooth or optical but stale metadata is sent through the WebSocket, # And the source has not changed. if self._source_change.id in ( - SOURCE_ENUM.bluetooth, - SOURCE_ENUM.lineIn, - SOURCE_ENUM.spdif, + BangOlufsenSource.BLUETOOTH, + BangOlufsenSource.LINE_IN, + BangOlufsenSource.SPDIF, ): - return SOURCE_ENUM.chromeCast + return BangOlufsenSource.CHROMECAST # source_change is chromecast and there is metadata but no artwork. Bluetooth does support metadata but not artwork # So i assume that it is bluetooth and not chromecast @@ -450,9 +452,9 @@ class BangOlufsenMediaPlayer(MediaPlayerEntity, BangOlufsenEntity): ): if ( len(self._playback_metadata.art) == 0 - and self._source_change.name == SOURCE_ENUM.bluetooth + and self._source_change.name == BangOlufsenSource.BLUETOOTH ): - return SOURCE_ENUM.bluetooth + return BangOlufsenSource.BLUETOOTH return self._source_change.name @@ -495,7 +497,7 @@ class BangOlufsenMediaPlayer(MediaPlayerEntity, BangOlufsenEntity): async def async_media_seek(self, position: float) -> None: """Seek to position in ms.""" - if self.source == SOURCE_ENUM.deezer: + if self.source == BangOlufsenSource.DEEZER: await self._client.seek_to_position(position_ms=int(position * 1000)) # Try to prevent the playback progress from bouncing in the UI. self._attr_media_position_updated_at = utcnow() @@ -569,14 +571,14 @@ class BangOlufsenMediaPlayer(MediaPlayerEntity, BangOlufsenEntity): # The "provider" media_type may not be suitable for overlay all the time. # Use it for now. - elif media_type == BANG_OLUFSEN_MEDIA_TYPE.TTS: + elif media_type == BangOlufsenMediaType.TTS: await self._client.post_overlay_play( overlay_play_request=OverlayPlayRequest( uri=Uri(location=media_id), ) ) - elif media_type == BANG_OLUFSEN_MEDIA_TYPE.RADIO: + elif media_type == BangOlufsenMediaType.RADIO: await self._client.run_provided_scene( scene_properties=SceneProperties( action_list=[ @@ -588,10 +590,10 @@ class BangOlufsenMediaPlayer(MediaPlayerEntity, BangOlufsenEntity): ) ) - elif media_type == BANG_OLUFSEN_MEDIA_TYPE.FAVOURITE: + elif media_type == BangOlufsenMediaType.FAVOURITE: await self._client.activate_preset(id=int(media_id)) - elif media_type == BANG_OLUFSEN_MEDIA_TYPE.DEEZER: + elif media_type == BangOlufsenMediaType.DEEZER: try: if media_id == "flow": deezer_id = None diff --git a/homeassistant/components/bang_olufsen/websocket.py b/homeassistant/components/bang_olufsen/websocket.py index fd378a40bd3..7415d0f362b 100644 --- a/homeassistant/components/bang_olufsen/websocket.py +++ b/homeassistant/components/bang_olufsen/websocket.py @@ -24,7 +24,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send from .const import ( BANG_OLUFSEN_WEBSOCKET_EVENT, CONNECTION_STATUS, - WEBSOCKET_NOTIFICATION, + WebsocketNotification, ) from .entity import BangOlufsenBase from .util import get_device @@ -80,12 +80,12 @@ class BangOlufsenWebsocket(BangOlufsenBase): def on_connection(self) -> None: """Handle WebSocket connection made.""" - _LOGGER.debug("Connected to the %s notification channel", self._name) + _LOGGER.debug("Connected to the %s notification channel", self.entry.title) self._update_connection_status() def on_connection_lost(self) -> None: """Handle WebSocket connection lost.""" - _LOGGER.error("Lost connection to the %s", self._name) + _LOGGER.error("Lost connection to the %s", self.entry.title) self._update_connection_status() def on_notification_notification( @@ -93,17 +93,17 @@ class BangOlufsenWebsocket(BangOlufsenBase): ) -> None: """Send notification dispatch.""" if notification.value: - if WEBSOCKET_NOTIFICATION.REMOTE_MENU_CHANGED in notification.value: + if WebsocketNotification.REMOTE_MENU_CHANGED in notification.value: async_dispatcher_send( self.hass, - f"{self._unique_id}_{WEBSOCKET_NOTIFICATION.REMOTE_MENU_CHANGED}", + f"{self._unique_id}_{WebsocketNotification.REMOTE_MENU_CHANGED}", ) def on_playback_error_notification(self, notification: PlaybackError) -> None: """Send playback_error dispatch.""" async_dispatcher_send( self.hass, - f"{self._unique_id}_{WEBSOCKET_NOTIFICATION.PLAYBACK_ERROR}", + f"{self._unique_id}_{WebsocketNotification.PLAYBACK_ERROR}", notification, ) @@ -113,7 +113,7 @@ class BangOlufsenWebsocket(BangOlufsenBase): """Send playback_metadata dispatch.""" async_dispatcher_send( self.hass, - f"{self._unique_id}_{WEBSOCKET_NOTIFICATION.PLAYBACK_METADATA}", + f"{self._unique_id}_{WebsocketNotification.PLAYBACK_METADATA}", notification, ) @@ -121,7 +121,7 @@ class BangOlufsenWebsocket(BangOlufsenBase): """Send playback_progress dispatch.""" async_dispatcher_send( self.hass, - f"{self._unique_id}_{WEBSOCKET_NOTIFICATION.PLAYBACK_PROGRESS}", + f"{self._unique_id}_{WebsocketNotification.PLAYBACK_PROGRESS}", notification, ) @@ -129,7 +129,7 @@ class BangOlufsenWebsocket(BangOlufsenBase): """Send playback_state dispatch.""" async_dispatcher_send( self.hass, - f"{self._unique_id}_{WEBSOCKET_NOTIFICATION.PLAYBACK_STATE}", + f"{self._unique_id}_{WebsocketNotification.PLAYBACK_STATE}", notification, ) @@ -137,7 +137,7 @@ class BangOlufsenWebsocket(BangOlufsenBase): """Send source_change dispatch.""" async_dispatcher_send( self.hass, - f"{self._unique_id}_{WEBSOCKET_NOTIFICATION.SOURCE_CHANGE}", + f"{self._unique_id}_{WebsocketNotification.SOURCE_CHANGE}", notification, ) @@ -145,7 +145,7 @@ class BangOlufsenWebsocket(BangOlufsenBase): """Send volume dispatch.""" async_dispatcher_send( self.hass, - f"{self._unique_id}_{WEBSOCKET_NOTIFICATION.VOLUME}", + f"{self._unique_id}_{WebsocketNotification.VOLUME}", notification, ) diff --git a/homeassistant/components/bayesian/binary_sensor.py b/homeassistant/components/bayesian/binary_sensor.py index 49965a38b77..b6298040b6b 100644 --- a/homeassistant/components/bayesian/binary_sensor.py +++ b/homeassistant/components/bayesian/binary_sensor.py @@ -1,4 +1,5 @@ """Use Bayesian Inference to trigger a binary sensor.""" + from __future__ import annotations from collections import OrderedDict @@ -27,7 +28,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import Event, HomeAssistant, callback from homeassistant.exceptions import ConditionError, TemplateError from homeassistant.helpers import condition import homeassistant.helpers.config_validation as cv @@ -42,7 +43,7 @@ from homeassistant.helpers.event import ( ) from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.template import Template, result_as_boolean -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, EventType +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import DOMAIN, PLATFORMS from .const import ( @@ -233,7 +234,7 @@ class BayesianBinarySensor(BinarySensorEntity): @callback def async_threshold_sensor_state_listener( - event: EventType[EventStateChangedData], + event: Event[EventStateChangedData], ) -> None: """Handle sensor state changes. @@ -259,7 +260,7 @@ class BayesianBinarySensor(BinarySensorEntity): @callback def _async_template_result_changed( - event: EventType[EventStateChangedData] | None, + event: Event[EventStateChangedData] | None, updates: list[TrackTemplateResult], ) -> None: track_template_result = updates.pop() @@ -312,9 +313,9 @@ class BayesianBinarySensor(BinarySensorEntity): self.hass, observations, text=f"{self._attr_name}/{entity}" ) - all_template_observations: list[Observation] = [] - for observations in self.observations_by_template.values(): - all_template_observations.append(observations[0]) + all_template_observations: list[Observation] = [ + observations[0] for observations in self.observations_by_template.values() + ] if len(all_template_observations) == 2: raise_mirrored_entries( self.hass, diff --git a/homeassistant/components/bayesian/helpers.py b/homeassistant/components/bayesian/helpers.py index 6e78de63607..cc8966a90b6 100644 --- a/homeassistant/components/bayesian/helpers.py +++ b/homeassistant/components/bayesian/helpers.py @@ -1,4 +1,5 @@ """Helpers to deal with bayesian observations.""" + from __future__ import annotations from dataclasses import dataclass, field diff --git a/homeassistant/components/bayesian/icons.json b/homeassistant/components/bayesian/icons.json new file mode 100644 index 00000000000..a03163179cb --- /dev/null +++ b/homeassistant/components/bayesian/icons.json @@ -0,0 +1,5 @@ +{ + "services": { + "reload": "mdi:reload" + } +} diff --git a/homeassistant/components/bayesian/issues.py b/homeassistant/components/bayesian/issues.py index fbc3a86258d..b35c788053d 100644 --- a/homeassistant/components/bayesian/issues.py +++ b/homeassistant/components/bayesian/issues.py @@ -1,4 +1,5 @@ """Helpers for generating issues.""" + from __future__ import annotations from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/bbox/device_tracker.py b/homeassistant/components/bbox/device_tracker.py index 9c83aaa1734..5413c75d8e7 100644 --- a/homeassistant/components/bbox/device_tracker.py +++ b/homeassistant/components/bbox/device_tracker.py @@ -1,4 +1,5 @@ """Support for French FAI Bouygues Bbox routers.""" + from __future__ import annotations from collections import namedtuple diff --git a/homeassistant/components/bbox/sensor.py b/homeassistant/components/bbox/sensor.py index 4e89788ddae..858ad6c6e47 100644 --- a/homeassistant/components/bbox/sensor.py +++ b/homeassistant/components/bbox/sensor.py @@ -1,4 +1,5 @@ """Support for Bbox Bouygues Modem Router.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/beewi_smartclim/sensor.py b/homeassistant/components/beewi_smartclim/sensor.py index 08f2410ee06..3aaf4daaa80 100644 --- a/homeassistant/components/beewi_smartclim/sensor.py +++ b/homeassistant/components/beewi_smartclim/sensor.py @@ -1,4 +1,5 @@ """Platform for beewi_smartclim integration.""" + from __future__ import annotations from beewi_smartclim import BeewiSmartClimPoller diff --git a/homeassistant/components/binary_sensor/__init__.py b/homeassistant/components/binary_sensor/__init__.py index 06185489419..4fd99c309bc 100644 --- a/homeassistant/components/binary_sensor/__init__.py +++ b/homeassistant/components/binary_sensor/__init__.py @@ -1,4 +1,5 @@ """Component to interface with binary sensors.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/binary_sensor/device_condition.py b/homeassistant/components/binary_sensor/device_condition.py index 81d2ebf26a2..bbd80959b12 100644 --- a/homeassistant/components/binary_sensor/device_condition.py +++ b/homeassistant/components/binary_sensor/device_condition.py @@ -1,4 +1,5 @@ """Implement device conditions for binary sensor.""" + from __future__ import annotations import voluptuous as vol diff --git a/homeassistant/components/binary_sensor/device_trigger.py b/homeassistant/components/binary_sensor/device_trigger.py index de6dbdbe075..803aff97197 100644 --- a/homeassistant/components/binary_sensor/device_trigger.py +++ b/homeassistant/components/binary_sensor/device_trigger.py @@ -1,4 +1,5 @@ """Provides device triggers for binary sensors.""" + import voluptuous as vol from homeassistant.components.device_automation import ( diff --git a/homeassistant/components/binary_sensor/group.py b/homeassistant/components/binary_sensor/group.py deleted file mode 100644 index 234883ffd5a..00000000000 --- a/homeassistant/components/binary_sensor/group.py +++ /dev/null @@ -1,14 +0,0 @@ -"""Describe group states.""" - - -from homeassistant.components.group import GroupIntegrationRegistry -from homeassistant.const import STATE_OFF, STATE_ON -from homeassistant.core import HomeAssistant, callback - - -@callback -def async_describe_on_off_states( - hass: HomeAssistant, registry: GroupIntegrationRegistry -) -> None: - """Describe group on off states.""" - registry.on_off_states({STATE_ON}, STATE_OFF) diff --git a/homeassistant/components/binary_sensor/significant_change.py b/homeassistant/components/binary_sensor/significant_change.py index 8421483ba0c..4801af1f54d 100644 --- a/homeassistant/components/binary_sensor/significant_change.py +++ b/homeassistant/components/binary_sensor/significant_change.py @@ -1,4 +1,5 @@ """Helper to test significant Binary Sensor state changes.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/bitcoin/sensor.py b/homeassistant/components/bitcoin/sensor.py index 29a7957fde7..e003362ac7e 100644 --- a/homeassistant/components/bitcoin/sensor.py +++ b/homeassistant/components/bitcoin/sensor.py @@ -1,4 +1,5 @@ """Bitcoin information service that uses blockchain.com.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/bizkaibus/sensor.py b/homeassistant/components/bizkaibus/sensor.py index 7078de158e7..ff7d28b96c7 100644 --- a/homeassistant/components/bizkaibus/sensor.py +++ b/homeassistant/components/bizkaibus/sensor.py @@ -1,4 +1,5 @@ """Support for Bizkaibus, Biscay (Basque Country, Spain) Bus service.""" + from __future__ import annotations from contextlib import suppress diff --git a/homeassistant/components/blackbird/const.py b/homeassistant/components/blackbird/const.py index aa8d7e7d514..27a4f1dcffb 100644 --- a/homeassistant/components/blackbird/const.py +++ b/homeassistant/components/blackbird/const.py @@ -1,3 +1,4 @@ """Constants for the Monoprice Blackbird Matrix Switch component.""" + DOMAIN = "blackbird" SERVICE_SETALLZONES = "set_all_zones" diff --git a/homeassistant/components/blackbird/icons.json b/homeassistant/components/blackbird/icons.json new file mode 100644 index 00000000000..f080fb5f857 --- /dev/null +++ b/homeassistant/components/blackbird/icons.json @@ -0,0 +1,5 @@ +{ + "services": { + "set_all_zones": "mdi:home-sound-in" + } +} diff --git a/homeassistant/components/blackbird/media_player.py b/homeassistant/components/blackbird/media_player.py index 61dca6550c0..4006b12738f 100644 --- a/homeassistant/components/blackbird/media_player.py +++ b/homeassistant/components/blackbird/media_player.py @@ -1,4 +1,5 @@ """Support for interfacing with Monoprice Blackbird 4k 8x8 HDBaseT Matrix.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/blebox/__init__.py b/homeassistant/components/blebox/__init__.py index d6c3cda7ef4..ce142101c3e 100644 --- a/homeassistant/components/blebox/__init__.py +++ b/homeassistant/components/blebox/__init__.py @@ -1,4 +1,5 @@ """The BleBox devices integration.""" + import logging from typing import Generic, TypeVar diff --git a/homeassistant/components/blebox/button.py b/homeassistant/components/blebox/button.py index 93a5a4e0c5a..940fe7f8f6f 100644 --- a/homeassistant/components/blebox/button.py +++ b/homeassistant/components/blebox/button.py @@ -1,4 +1,5 @@ """BleBox button entities implementation.""" + from __future__ import annotations from blebox_uniapi.box import Box diff --git a/homeassistant/components/blebox/climate.py b/homeassistant/components/blebox/climate.py index 1350f1f29a2..24f036dcd49 100644 --- a/homeassistant/components/blebox/climate.py +++ b/homeassistant/components/blebox/climate.py @@ -1,4 +1,5 @@ """BleBox climate entity.""" + from datetime import timedelta from typing import Any diff --git a/homeassistant/components/blebox/config_flow.py b/homeassistant/components/blebox/config_flow.py index 977e704eb98..1f04f06a05a 100644 --- a/homeassistant/components/blebox/config_flow.py +++ b/homeassistant/components/blebox/config_flow.py @@ -1,4 +1,5 @@ """Config flow for BleBox devices integration.""" + from __future__ import annotations import logging @@ -14,10 +15,9 @@ from blebox_uniapi.error import ( from blebox_uniapi.session import ApiHost import voluptuous as vol -from homeassistant import config_entries from homeassistant.components import zeroconf +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from . import get_maybe_authenticated_session @@ -65,7 +65,7 @@ LOG_MSG = { } -class BleBoxConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class BleBoxConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for BleBox devices.""" VERSION = 1 @@ -89,7 +89,7 @@ class BleBoxConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_zeroconf( self, discovery_info: zeroconf.ZeroconfServiceInfo - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle zeroconf discovery.""" hass = self.hass ipaddress = (discovery_info.host, discovery_info.port) @@ -126,7 +126,7 @@ class BleBoxConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_confirm_discovery( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle discovery confirmation.""" if user_input is not None: return self.async_create_entry( diff --git a/homeassistant/components/blebox/const.py b/homeassistant/components/blebox/const.py index 533c33f37bb..ff6a6b33af6 100644 --- a/homeassistant/components/blebox/const.py +++ b/homeassistant/components/blebox/const.py @@ -1,6 +1,5 @@ """Constants for the BleBox devices integration.""" - DOMAIN = "blebox" PRODUCT = "product" diff --git a/homeassistant/components/blebox/cover.py b/homeassistant/components/blebox/cover.py index 658a9bc30cc..f9e974991f5 100644 --- a/homeassistant/components/blebox/cover.py +++ b/homeassistant/components/blebox/cover.py @@ -1,4 +1,5 @@ """BleBox cover entity.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/blebox/helpers.py b/homeassistant/components/blebox/helpers.py index 82b8080b61d..8061fff5645 100644 --- a/homeassistant/components/blebox/helpers.py +++ b/homeassistant/components/blebox/helpers.py @@ -1,4 +1,5 @@ """Blebox helpers.""" + from __future__ import annotations import aiohttp diff --git a/homeassistant/components/blebox/light.py b/homeassistant/components/blebox/light.py index 6446949cb89..1f994db7243 100644 --- a/homeassistant/components/blebox/light.py +++ b/homeassistant/components/blebox/light.py @@ -1,4 +1,5 @@ """BleBox light entities implementation.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/blebox/switch.py b/homeassistant/components/blebox/switch.py index 94edc32bc8c..a68b9f01cf2 100644 --- a/homeassistant/components/blebox/switch.py +++ b/homeassistant/components/blebox/switch.py @@ -1,4 +1,5 @@ """BleBox switch implementation.""" + from datetime import timedelta from typing import Any diff --git a/homeassistant/components/blink/__init__.py b/homeassistant/components/blink/__init__.py index e86d07c8780..d21994ecc8f 100644 --- a/homeassistant/components/blink/__init__.py +++ b/homeassistant/components/blink/__init__.py @@ -1,4 +1,5 @@ """Support for Blink Home Camera System.""" + from copy import deepcopy import logging diff --git a/homeassistant/components/blink/alarm_control_panel.py b/homeassistant/components/blink/alarm_control_panel.py index f2c01de4f18..b7dc50a5c51 100644 --- a/homeassistant/components/blink/alarm_control_panel.py +++ b/homeassistant/components/blink/alarm_control_panel.py @@ -1,4 +1,5 @@ """Support for Blink Alarm Control Panel.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/blink/binary_sensor.py b/homeassistant/components/blink/binary_sensor.py index b2a23b0aa31..2f0a56a901c 100644 --- a/homeassistant/components/blink/binary_sensor.py +++ b/homeassistant/components/blink/binary_sensor.py @@ -1,4 +1,5 @@ """Support for Blink system camera control.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/blink/camera.py b/homeassistant/components/blink/camera.py index ff4fa6380a7..318bb18772a 100644 --- a/homeassistant/components/blink/camera.py +++ b/homeassistant/components/blink/camera.py @@ -1,4 +1,5 @@ """Support for Blink system camera.""" + from __future__ import annotations from collections.abc import Mapping diff --git a/homeassistant/components/blink/config_flow.py b/homeassistant/components/blink/config_flow.py index aaacbb9390c..1531728aa79 100644 --- a/homeassistant/components/blink/config_flow.py +++ b/homeassistant/components/blink/config_flow.py @@ -1,4 +1,5 @@ """Config flow to configure Blink.""" + from __future__ import annotations from collections.abc import Mapping @@ -9,10 +10,9 @@ from blinkpy.auth import Auth, LoginError, TokenRefreshFailed from blinkpy.blinkpy import Blink, BlinkSetupError import voluptuous as vol -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_PASSWORD, CONF_PIN, CONF_USERNAME from homeassistant.core import HomeAssistant, callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -51,7 +51,7 @@ class BlinkConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initiated by the user.""" errors = {} if user_input is not None: @@ -86,7 +86,7 @@ class BlinkConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_2fa( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle 2FA step.""" errors = {} if user_input is not None: @@ -113,12 +113,14 @@ class BlinkConfigFlow(ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Perform reauth upon migration of old entries.""" return await self.async_step_user(dict(entry_data)) @callback - def _async_finish_flow(self) -> FlowResult: + def _async_finish_flow(self) -> ConfigFlowResult: """Finish with setup.""" assert self.auth return self.async_create_entry(title=DOMAIN, data=self.auth.login_attributes) diff --git a/homeassistant/components/blink/const.py b/homeassistant/components/blink/const.py index 7aa3d0d388e..a524d2c599a 100644 --- a/homeassistant/components/blink/const.py +++ b/homeassistant/components/blink/const.py @@ -1,4 +1,5 @@ """Constants for Blink.""" + from homeassistant.const import Platform DOMAIN = "blink" diff --git a/homeassistant/components/blink/coordinator.py b/homeassistant/components/blink/coordinator.py index aaf666208a6..e71ff4e449e 100644 --- a/homeassistant/components/blink/coordinator.py +++ b/homeassistant/components/blink/coordinator.py @@ -1,4 +1,5 @@ """Blink Coordinator.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/blink/diagnostics.py b/homeassistant/components/blink/diagnostics.py index 664d1421ac2..88ff2aff928 100644 --- a/homeassistant/components/blink/diagnostics.py +++ b/homeassistant/components/blink/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for Blink.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/blink/manifest.json b/homeassistant/components/blink/manifest.json index 48db78b572c..445a469b141 100644 --- a/homeassistant/components/blink/manifest.json +++ b/homeassistant/components/blink/manifest.json @@ -18,7 +18,6 @@ } ], "documentation": "https://www.home-assistant.io/integrations/blink", - "import_executor": true, "iot_class": "cloud_polling", "loggers": ["blinkpy"], "requirements": ["blinkpy==0.22.6"] diff --git a/homeassistant/components/blink/sensor.py b/homeassistant/components/blink/sensor.py index fb429e79dc8..8a807b9303e 100644 --- a/homeassistant/components/blink/sensor.py +++ b/homeassistant/components/blink/sensor.py @@ -1,4 +1,5 @@ """Support for Blink system camera sensors.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/blink/services.py b/homeassistant/components/blink/services.py index 5c034cdb7c5..e01371c5c09 100644 --- a/homeassistant/components/blink/services.py +++ b/homeassistant/components/blink/services.py @@ -1,4 +1,5 @@ """Services for the Blink integration.""" + from __future__ import annotations import voluptuous as vol diff --git a/homeassistant/components/blink/switch.py b/homeassistant/components/blink/switch.py index 2b25d1bce0c..1bfd257ecbe 100644 --- a/homeassistant/components/blink/switch.py +++ b/homeassistant/components/blink/switch.py @@ -1,4 +1,5 @@ """Support for Blink Motion detection switches.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/blinksticklight/light.py b/homeassistant/components/blinksticklight/light.py index 91d3d7d1b96..3e1f60e0f50 100644 --- a/homeassistant/components/blinksticklight/light.py +++ b/homeassistant/components/blinksticklight/light.py @@ -1,4 +1,5 @@ """Support for Blinkstick lights.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/blockchain/sensor.py b/homeassistant/components/blockchain/sensor.py index 6c65987ef57..dafd47bcb20 100644 --- a/homeassistant/components/blockchain/sensor.py +++ b/homeassistant/components/blockchain/sensor.py @@ -1,4 +1,5 @@ """Support for Blockchain.com sensors.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/bloomsky/__init__.py b/homeassistant/components/bloomsky/__init__.py index 59e224b0b6b..c2a46baaeb3 100644 --- a/homeassistant/components/bloomsky/__init__.py +++ b/homeassistant/components/bloomsky/__init__.py @@ -1,4 +1,5 @@ """Support for BloomSky weather station.""" + from datetime import timedelta from http import HTTPStatus import logging diff --git a/homeassistant/components/bloomsky/binary_sensor.py b/homeassistant/components/bloomsky/binary_sensor.py index b99fdfe0c78..3582b186013 100644 --- a/homeassistant/components/bloomsky/binary_sensor.py +++ b/homeassistant/components/bloomsky/binary_sensor.py @@ -1,4 +1,5 @@ """Support the binary sensors of a BloomSky weather station.""" + from __future__ import annotations import voluptuous as vol diff --git a/homeassistant/components/bloomsky/camera.py b/homeassistant/components/bloomsky/camera.py index 97c451ef178..f07dd1e9d14 100644 --- a/homeassistant/components/bloomsky/camera.py +++ b/homeassistant/components/bloomsky/camera.py @@ -1,4 +1,5 @@ """Support for a camera of a BloomSky weather station.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/bloomsky/sensor.py b/homeassistant/components/bloomsky/sensor.py index 4361af9ad37..1f63b4a7256 100644 --- a/homeassistant/components/bloomsky/sensor.py +++ b/homeassistant/components/bloomsky/sensor.py @@ -1,4 +1,5 @@ """Support the sensor of a BloomSky weather station.""" + from __future__ import annotations import voluptuous as vol @@ -101,13 +102,9 @@ class BloomSkySensor(SensorEntity): self._attr_name = f"{device['DeviceName']} {sensor_name}" self._attr_unique_id = f"{self._device_id}-{sensor_name}" self._attr_device_class = SENSOR_DEVICE_CLASS.get(sensor_name) - self._attr_native_unit_of_measurement = SENSOR_UNITS_IMPERIAL.get( - sensor_name, None - ) + self._attr_native_unit_of_measurement = SENSOR_UNITS_IMPERIAL.get(sensor_name) if self._bloomsky.is_metric: - self._attr_native_unit_of_measurement = SENSOR_UNITS_METRIC.get( - sensor_name, None - ) + self._attr_native_unit_of_measurement = SENSOR_UNITS_METRIC.get(sensor_name) def update(self) -> None: """Request an update from the BloomSky API.""" diff --git a/homeassistant/components/blue_current/__init__.py b/homeassistant/components/blue_current/__init__.py index 16b81c3c1e7..e852dfc8c6e 100644 --- a/homeassistant/components/blue_current/__init__.py +++ b/homeassistant/components/blue_current/__init__.py @@ -1,9 +1,9 @@ """The Blue Current integration.""" + from __future__ import annotations import asyncio from contextlib import suppress -from datetime import datetime from typing import Any from bluecurrent_api import Client @@ -15,24 +15,17 @@ from bluecurrent_api.exceptions import ( ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ATTR_NAME, - CONF_API_TOKEN, - EVENT_HOMEASSISTANT_STOP, - Platform, -) -from homeassistant.core import Event, HomeAssistant +from homeassistant.const import ATTR_NAME, CONF_API_TOKEN, Platform +from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.helpers.event import async_call_later from .const import DOMAIN, EVSE_ID, LOGGER, MODEL_TYPE PLATFORMS = [Platform.SENSOR] CHARGE_POINTS = "CHARGE_POINTS" DATA = "data" -SMALL_DELAY = 1 -LARGE_DELAY = 20 +DELAY = 5 GRID = "GRID" OBJECT = "object" @@ -47,26 +40,19 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b connector = Connector(hass, config_entry, client) try: - await connector.connect(api_token) + await client.validate_api_token(api_token) except InvalidApiToken as err: raise ConfigEntryAuthFailed("Invalid API token.") from err except BlueCurrentException as err: raise ConfigEntryNotReady from err + config_entry.async_create_background_task( + hass, connector.run_task(), "blue_current-websocket" + ) - hass.async_create_background_task(connector.start_loop(), "blue_current-websocket") - await client.get_charge_points() - - await client.wait_for_response() + await client.wait_for_charge_points() hass.data[DOMAIN][config_entry.entry_id] = connector await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) - config_entry.async_on_unload(connector.disconnect) - - async def _async_disconnect_websocket(_: Event) -> None: - await connector.disconnect() - - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_disconnect_websocket) - return True @@ -94,12 +80,6 @@ class Connector: self.client = client self.charge_points: dict[str, dict] = {} self.grid: dict[str, Any] = {} - self.available = False - - async def connect(self, token: str) -> None: - """Register on_data and connect to the websocket.""" - await self.client.connect(token) - self.available = True async def on_data(self, message: dict) -> None: """Handle received data.""" @@ -131,9 +111,9 @@ class Connector: entry[EVSE_ID], entry[MODEL_TYPE], entry[ATTR_NAME] ) for entry in charge_points_data - ) + ), + self.client.get_grid_status(charge_points_data[0][EVSE_ID]), ) - await self.client.get_grid_status(charge_points_data[0][EVSE_ID]) async def handle_charge_point(self, evse_id: str, model: str, name: str) -> None: """Add the chargepoint and request their data.""" @@ -147,44 +127,53 @@ class Connector: def update_charge_point(self, evse_id: str, data: dict) -> None: """Update the charge point data.""" self.charge_points[evse_id].update(data) - self.dispatch_value_update_signal(evse_id) + self.dispatch_charge_point_update_signal(evse_id) - def dispatch_value_update_signal(self, evse_id: str) -> None: - """Dispatch a value signal.""" - async_dispatcher_send(self.hass, f"{DOMAIN}_value_update_{evse_id}") + def dispatch_charge_point_update_signal(self, evse_id: str) -> None: + """Dispatch a charge point update signal.""" + async_dispatcher_send(self.hass, f"{DOMAIN}_charge_point_update_{evse_id}") def dispatch_grid_update_signal(self) -> None: - """Dispatch a grid signal.""" + """Dispatch a grid update signal.""" async_dispatcher_send(self.hass, f"{DOMAIN}_grid_update") - async def start_loop(self) -> None: + async def on_open(self) -> None: + """Fetch data when connection is established.""" + await self.client.get_charge_points() + + async def run_task(self) -> None: """Start the receive loop.""" try: - await self.client.start_loop(self.on_data) - except BlueCurrentException as err: - LOGGER.warning( - "Disconnected from the Blue Current websocket. Retrying to connect in background. %s", - err, - ) + while True: + try: + await self.client.connect(self.on_data, self.on_open) + except RequestLimitReached: + LOGGER.warning( + "Request limit reached. reconnecting at 00:00 (Europe/Amsterdam)" + ) + delay = self.client.get_next_reset_delta().seconds + except WebsocketError: + LOGGER.debug("Disconnected, retrying in background") + delay = DELAY - async_call_later(self.hass, SMALL_DELAY, self.reconnect) + self._on_disconnect() + await asyncio.sleep(delay) + finally: + await self._disconnect() - async def reconnect(self, _event_time: datetime | None = None) -> None: - """Keep trying to reconnect to the websocket.""" - try: - await self.connect(self.config.data[CONF_API_TOKEN]) - LOGGER.debug("Reconnected to the Blue Current websocket") - self.hass.async_create_task(self.start_loop()) - except RequestLimitReached: - self.available = False - async_call_later( - self.hass, self.client.get_next_reset_delta(), self.reconnect - ) - except WebsocketError: - self.available = False - async_call_later(self.hass, LARGE_DELAY, self.reconnect) + def _on_disconnect(self) -> None: + """Dispatch signals to update entity states.""" + for evse_id in self.charge_points: + self.dispatch_charge_point_update_signal(evse_id) + self.dispatch_grid_update_signal() - async def disconnect(self) -> None: + async def _disconnect(self) -> None: """Disconnect from the websocket.""" with suppress(WebsocketError): await self.client.disconnect() + self._on_disconnect() + + @property + def connected(self) -> bool: + """Returns the connection status.""" + return self.client.is_connected() diff --git a/homeassistant/components/blue_current/config_flow.py b/homeassistant/components/blue_current/config_flow.py index 68a30fcdf7f..66070094c29 100644 --- a/homeassistant/components/blue_current/config_flow.py +++ b/homeassistant/components/blue_current/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Blue Current integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -13,24 +14,23 @@ from bluecurrent_api.exceptions import ( ) import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_API_TOKEN -from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN, LOGGER DATA_SCHEMA = vol.Schema({vol.Required(CONF_API_TOKEN): str}) -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class BlueCurrentConfigFlow(ConfigFlow, domain=DOMAIN): """Handle the config flow for Blue Current.""" VERSION = 1 - _reauth_entry: config_entries.ConfigEntry | None = None + _reauth_entry: ConfigEntry | None = None async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors = {} if user_input is not None: @@ -75,7 +75,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="user", data_schema=DATA_SCHEMA, errors=errors ) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Handle a reauthorization flow request.""" self._reauth_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] diff --git a/homeassistant/components/blue_current/entity.py b/homeassistant/components/blue_current/entity.py index c797fec08b0..cae7d420c99 100644 --- a/homeassistant/components/blue_current/entity.py +++ b/homeassistant/components/blue_current/entity.py @@ -1,4 +1,5 @@ """Entity representing a Blue Current charge point.""" + from abc import abstractmethod from homeassistant.const import ATTR_NAME @@ -39,7 +40,7 @@ class BlueCurrentEntity(Entity): @property def available(self) -> bool: """Return entity availability.""" - return self.connector.available and self.has_value + return self.connector.connected and self.has_value @callback @abstractmethod @@ -52,7 +53,7 @@ class ChargepointEntity(BlueCurrentEntity): def __init__(self, connector: Connector, evse_id: str) -> None: """Initialize the entity.""" - super().__init__(connector, f"{DOMAIN}_value_update_{evse_id}") + super().__init__(connector, f"{DOMAIN}_charge_point_update_{evse_id}") chargepoint_name = connector.charge_points[evse_id][ATTR_NAME] diff --git a/homeassistant/components/blue_current/manifest.json b/homeassistant/components/blue_current/manifest.json index cadaac30d68..4f277e83656 100644 --- a/homeassistant/components/blue_current/manifest.json +++ b/homeassistant/components/blue_current/manifest.json @@ -5,5 +5,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/blue_current", "iot_class": "cloud_push", - "requirements": ["bluecurrent-api==1.0.6"] + "loggers": ["bluecurrent_api"], + "requirements": ["bluecurrent-api==1.2.3"] } diff --git a/homeassistant/components/blue_current/sensor.py b/homeassistant/components/blue_current/sensor.py index 02a40e09089..b544b69d2ff 100644 --- a/homeassistant/components/blue_current/sensor.py +++ b/homeassistant/components/blue_current/sensor.py @@ -1,4 +1,5 @@ """Support for Blue Current sensors.""" + from __future__ import annotations from homeassistant.components.sensor import ( @@ -216,13 +217,13 @@ async def async_setup_entry( ) -> None: """Set up Blue Current sensors.""" connector: Connector = hass.data[DOMAIN][entry.entry_id] - sensor_list: list[SensorEntity] = [] - for evse_id in connector.charge_points: - for sensor in SENSORS: - sensor_list.append(ChargePointSensor(connector, sensor, evse_id)) + sensor_list: list[SensorEntity] = [ + ChargePointSensor(connector, sensor, evse_id) + for evse_id in connector.charge_points + for sensor in SENSORS + ] - for grid_sensor in GRID_SENSORS: - sensor_list.append(GridSensor(connector, grid_sensor)) + sensor_list.extend(GridSensor(connector, sensor) for sensor in GRID_SENSORS) async_add_entities(sensor_list) diff --git a/homeassistant/components/bluemaestro/__init__.py b/homeassistant/components/bluemaestro/__init__.py index 45eebedcfb2..c25ceb44759 100644 --- a/homeassistant/components/bluemaestro/__init__.py +++ b/homeassistant/components/bluemaestro/__init__.py @@ -1,4 +1,5 @@ """The BlueMaestro integration.""" + from __future__ import annotations import logging @@ -25,14 +26,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: address = entry.unique_id assert address is not None data = BlueMaestroBluetoothDeviceData() - coordinator = hass.data.setdefault(DOMAIN, {})[ - entry.entry_id - ] = PassiveBluetoothProcessorCoordinator( - hass, - _LOGGER, - address=address, - mode=BluetoothScanningMode.PASSIVE, - update_method=data.update, + coordinator = hass.data.setdefault(DOMAIN, {})[entry.entry_id] = ( + PassiveBluetoothProcessorCoordinator( + hass, + _LOGGER, + address=address, + mode=BluetoothScanningMode.PASSIVE, + update_method=data.update, + ) ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload( diff --git a/homeassistant/components/bluemaestro/config_flow.py b/homeassistant/components/bluemaestro/config_flow.py index ccb548fa42b..be83eafb2ce 100644 --- a/homeassistant/components/bluemaestro/config_flow.py +++ b/homeassistant/components/bluemaestro/config_flow.py @@ -1,4 +1,5 @@ """Config flow for bluemaestro ble integration.""" + from __future__ import annotations from typing import Any @@ -10,9 +11,8 @@ from homeassistant.components.bluetooth import ( BluetoothServiceInfoBleak, async_discovered_service_info, ) -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_ADDRESS -from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN @@ -30,7 +30,7 @@ class BlueMaestroConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_bluetooth( self, discovery_info: BluetoothServiceInfoBleak - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the bluetooth discovery step.""" await self.async_set_unique_id(discovery_info.address) self._abort_if_unique_id_configured() @@ -43,7 +43,7 @@ class BlueMaestroConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_bluetooth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm discovery.""" assert self._discovered_device is not None device = self._discovered_device @@ -62,7 +62,7 @@ class BlueMaestroConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the user step to pick discovered device.""" if user_input is not None: address = user_input[CONF_ADDRESS] diff --git a/homeassistant/components/bluemaestro/device.py b/homeassistant/components/bluemaestro/device.py index 19d955dd945..2d1a33347c3 100644 --- a/homeassistant/components/bluemaestro/device.py +++ b/homeassistant/components/bluemaestro/device.py @@ -1,4 +1,5 @@ """Support for BlueMaestro devices.""" + from __future__ import annotations from bluemaestro_ble import DeviceKey diff --git a/homeassistant/components/bluemaestro/sensor.py b/homeassistant/components/bluemaestro/sensor.py index dcd559a06ef..4024b8b3326 100644 --- a/homeassistant/components/bluemaestro/sensor.py +++ b/homeassistant/components/bluemaestro/sensor.py @@ -1,4 +1,5 @@ """Support for BlueMaestro sensors.""" + from __future__ import annotations from bluemaestro_ble import ( diff --git a/homeassistant/components/blueprint/__init__.py b/homeassistant/components/blueprint/__init__.py index 1fe1ad8e189..92d94708e0f 100644 --- a/homeassistant/components/blueprint/__init__.py +++ b/homeassistant/components/blueprint/__init__.py @@ -1,4 +1,5 @@ """The blueprint integration.""" + from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv from homeassistant.helpers.typing import ConfigType diff --git a/homeassistant/components/blueprint/const.py b/homeassistant/components/blueprint/const.py index 1ca6117f153..18433aa6ba6 100644 --- a/homeassistant/components/blueprint/const.py +++ b/homeassistant/components/blueprint/const.py @@ -1,4 +1,5 @@ """Constants for the blueprint integration.""" + BLUEPRINT_FOLDER = "blueprints" CONF_BLUEPRINT = "blueprint" diff --git a/homeassistant/components/blueprint/errors.py b/homeassistant/components/blueprint/errors.py index fe714542e0f..221279a39ac 100644 --- a/homeassistant/components/blueprint/errors.py +++ b/homeassistant/components/blueprint/errors.py @@ -1,4 +1,5 @@ """Blueprint errors.""" + from __future__ import annotations from collections.abc import Iterable diff --git a/homeassistant/components/blueprint/models.py b/homeassistant/components/blueprint/models.py index 33fb87cc578..2475ccf8d14 100644 --- a/homeassistant/components/blueprint/models.py +++ b/homeassistant/components/blueprint/models.py @@ -1,4 +1,5 @@ """Blueprint models.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/blueprint/schemas.py b/homeassistant/components/blueprint/schemas.py index fd3aa967336..390bb1ddc80 100644 --- a/homeassistant/components/blueprint/schemas.py +++ b/homeassistant/components/blueprint/schemas.py @@ -1,4 +1,5 @@ """Schemas for the blueprint integration.""" + from typing import Any import voluptuous as vol diff --git a/homeassistant/components/blueprint/websocket_api.py b/homeassistant/components/blueprint/websocket_api.py index 1989f0f563c..98cc8131166 100644 --- a/homeassistant/components/blueprint/websocket_api.py +++ b/homeassistant/components/blueprint/websocket_api.py @@ -1,4 +1,5 @@ """Websocket API for blueprint.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/bluesound/const.py b/homeassistant/components/bluesound/const.py index af1a8e5187c..ae5291c6513 100644 --- a/homeassistant/components/bluesound/const.py +++ b/homeassistant/components/bluesound/const.py @@ -1,4 +1,5 @@ """Constants for the Bluesound HiFi wireless speakers and audio integrations component.""" + DOMAIN = "bluesound" SERVICE_CLEAR_TIMER = "clear_sleep_timer" SERVICE_JOIN = "join" diff --git a/homeassistant/components/bluesound/icons.json b/homeassistant/components/bluesound/icons.json new file mode 100644 index 00000000000..8c886f12dfd --- /dev/null +++ b/homeassistant/components/bluesound/icons.json @@ -0,0 +1,8 @@ +{ + "services": { + "join": "mdi:link-variant", + "unjoin": "mdi:link-variant-off", + "set_sleep_timer": "mdi:sleep", + "clear_sleep_timer": "mdi:sleep-off" + } +} diff --git a/homeassistant/components/bluesound/media_player.py b/homeassistant/components/bluesound/media_player.py index 70c19b5fa6f..9377557d025 100644 --- a/homeassistant/components/bluesound/media_player.py +++ b/homeassistant/components/bluesound/media_player.py @@ -1,4 +1,5 @@ """Support for Bluesound devices.""" + from __future__ import annotations import asyncio @@ -365,7 +366,7 @@ class BluesoundPlayer(MediaPlayerEntity): data = None elif response.status == 595: _LOGGER.info("Status 595 returned, treating as timeout") - raise BluesoundPlayer._TimeoutException() + raise BluesoundPlayer._TimeoutException else: _LOGGER.error("Error %s on %s", response.status, url) return None @@ -431,7 +432,7 @@ class BluesoundPlayer(MediaPlayerEntity): self.async_write_ha_state() elif response.status == 595: _LOGGER.info("Status 595 returned, treating as timeout") - raise BluesoundPlayer._TimeoutException() + raise BluesoundPlayer._TimeoutException else: _LOGGER.error( "Error %s on %s. Trying one more time", response.status, url @@ -685,20 +686,15 @@ class BluesoundPlayer(MediaPlayerEntity): if self._status is None or (self.is_grouped and not self.is_master): return None - sources = [] + sources = [source["title"] for source in self._preset_items] - for source in self._preset_items: - sources.append(source["title"]) + sources.extend( + source["title"] + for source in self._services_items + if source["type"] in ("LocalMusic", "RadioService") + ) - for source in [ - x - for x in self._services_items - if x["type"] in ("LocalMusic", "RadioService") - ]: - sources.append(source["title"]) - - for source in self._capture_items: - sources.append(source["title"]) + sources.extend(source["title"] for source in self._capture_items) return sources diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index c2f1724b340..1e091ec32cc 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -1,4 +1,5 @@ """The bluetooth integration.""" + from __future__ import annotations import datetime @@ -165,7 +166,9 @@ async def _async_start_adapter_discovery( """Shutdown debouncer.""" discovery_debouncer.async_shutdown() - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_shutdown_debouncer) + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, _async_shutdown_debouncer, run_immediately=True + ) async def _async_call_debouncer(now: datetime.datetime) -> None: """Call the debouncer at a later time.""" @@ -196,7 +199,9 @@ async def _async_start_adapter_discovery( cancel = usb.async_register_scan_request_callback(hass, _async_trigger_discovery) hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, hass_callback(lambda event: cancel()) + EVENT_HOMEASSISTANT_STOP, + hass_callback(lambda event: cancel()), + run_immediately=True, ) diff --git a/homeassistant/components/bluetooth/active_update_coordinator.py b/homeassistant/components/bluetooth/active_update_coordinator.py index cf8590079bc..df5701a81a3 100644 --- a/homeassistant/components/bluetooth/active_update_coordinator.py +++ b/homeassistant/components/bluetooth/active_update_coordinator.py @@ -2,6 +2,7 @@ Receives data from advertisements but can also poll. """ + from __future__ import annotations from collections.abc import Callable, Coroutine @@ -98,6 +99,7 @@ class ActiveBluetoothDataUpdateCoordinator( cooldown=POLL_DEFAULT_COOLDOWN, immediate=POLL_DEFAULT_IMMEDIATE, function=self._async_poll, + background=True, ) else: poll_debouncer.function = self._async_poll diff --git a/homeassistant/components/bluetooth/active_update_processor.py b/homeassistant/components/bluetooth/active_update_processor.py index d0be6c61811..be4f6553738 100644 --- a/homeassistant/components/bluetooth/active_update_processor.py +++ b/homeassistant/components/bluetooth/active_update_processor.py @@ -2,6 +2,7 @@ Collects data from advertisements but can also poll. """ + from __future__ import annotations from collections.abc import Callable, Coroutine @@ -91,6 +92,7 @@ class ActiveBluetoothProcessorCoordinator( cooldown=POLL_DEFAULT_COOLDOWN, immediate=POLL_DEFAULT_IMMEDIATE, function=self._async_poll, + background=True, ) else: poll_debouncer.function = self._async_poll diff --git a/homeassistant/components/bluetooth/api.py b/homeassistant/components/bluetooth/api.py index 29054a54e72..b1a6bc87728 100644 --- a/homeassistant/components/bluetooth/api.py +++ b/homeassistant/components/bluetooth/api.py @@ -2,6 +2,7 @@ These APIs are the only documented way to interact with the bluetooth integration. """ + from __future__ import annotations import asyncio diff --git a/homeassistant/components/bluetooth/config_flow.py b/homeassistant/components/bluetooth/config_flow.py index 76cf167790f..2b5980fbcd6 100644 --- a/homeassistant/components/bluetooth/config_flow.py +++ b/homeassistant/components/bluetooth/config_flow.py @@ -1,7 +1,8 @@ """Config flow to configure the Bluetooth integration.""" + from __future__ import annotations -from typing import TYPE_CHECKING, Any, cast +from typing import Any, cast from bluetooth_adapters import ( ADAPTER_ADDRESS, @@ -13,7 +14,7 @@ from bluetooth_adapters import ( import voluptuous as vol from homeassistant.components import onboarding -from homeassistant.config_entries import ConfigEntry, ConfigFlow +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.core import callback from homeassistant.helpers.schema_config_entry_flow import ( SchemaFlowFormStep, @@ -24,9 +25,6 @@ from homeassistant.helpers.typing import DiscoveryInfoType from . import models from .const import CONF_ADAPTER, CONF_DETAILS, CONF_PASSIVE, DOMAIN -if TYPE_CHECKING: - from homeassistant.data_entry_flow import FlowResult - OPTIONS_SCHEMA = vol.Schema( { vol.Required(CONF_PASSIVE, default=False): bool, @@ -50,7 +48,7 @@ class BluetoothConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_integration_discovery( self, discovery_info: DiscoveryInfoType - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by discovery.""" self._adapter = cast(str, discovery_info[CONF_ADAPTER]) self._details = cast(AdapterDetails, discovery_info[CONF_DETAILS]) @@ -63,7 +61,7 @@ class BluetoothConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_single_adapter( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Select an adapter.""" adapter = self._adapter details = self._details @@ -86,7 +84,7 @@ class BluetoothConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_multiple_adapters( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" if user_input is not None: assert self._adapters is not None @@ -138,7 +136,7 @@ class BluetoothConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" return await self.async_step_multiple_adapters() diff --git a/homeassistant/components/bluetooth/const.py b/homeassistant/components/bluetooth/const.py index fa8efabcb1d..a3238befbb8 100644 --- a/homeassistant/components/bluetooth/const.py +++ b/homeassistant/components/bluetooth/const.py @@ -1,4 +1,5 @@ """Constants for the Bluetooth integration.""" + from __future__ import annotations from typing import Final diff --git a/homeassistant/components/bluetooth/diagnostics.py b/homeassistant/components/bluetooth/diagnostics.py index 612c51806dd..a45500265cf 100644 --- a/homeassistant/components/bluetooth/diagnostics.py +++ b/homeassistant/components/bluetooth/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for bluetooth.""" + from __future__ import annotations import platform diff --git a/homeassistant/components/bluetooth/manager.py b/homeassistant/components/bluetooth/manager.py index 32589d822d3..3a240e9f01e 100644 --- a/homeassistant/components/bluetooth/manager.py +++ b/homeassistant/components/bluetooth/manager.py @@ -1,4 +1,5 @@ """The bluetooth integration.""" + from __future__ import annotations from collections.abc import Callable, Iterable @@ -134,9 +135,11 @@ class HomeAssistantBluetoothManager(BluetoothManager): self._bluetooth_adapters, self.storage ) self._cancel_logging_listener = self.hass.bus.async_listen( - EVENT_LOGGING_CHANGED, self._async_logging_changed + EVENT_LOGGING_CHANGED, self._async_logging_changed, run_immediately=True + ) + self.hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, self.async_stop, run_immediately=True ) - self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.async_stop) seen: set[str] = set() for address, service_info in itertools.chain( self._connectable_history.items(), self._all_history.items() diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index b8158a06f7e..62296ddd8b8 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -5,7 +5,6 @@ "config_flow": true, "dependencies": ["usb"], "documentation": "https://www.home-assistant.io/integrations/bluetooth", - "import_executor": true, "iot_class": "local_push", "loggers": [ "btsocket", @@ -18,7 +17,7 @@ "bleak==0.21.1", "bleak-retry-connector==3.4.0", "bluetooth-adapters==0.18.0", - "bluetooth-auto-recovery==1.3.0", + "bluetooth-auto-recovery==1.4.0", "bluetooth-data-tools==1.19.0", "dbus-fast==2.21.1", "habluetooth==2.4.2" diff --git a/homeassistant/components/bluetooth/match.py b/homeassistant/components/bluetooth/match.py index 2fd650d9580..a5e1159e04e 100644 --- a/homeassistant/components/bluetooth/match.py +++ b/homeassistant/components/bluetooth/match.py @@ -1,4 +1,5 @@ """The bluetooth integration matchers.""" + from __future__ import annotations from dataclasses import dataclass @@ -248,39 +249,47 @@ class BluetoothMatcherIndexBase(Generic[_T]): def match(self, service_info: BluetoothServiceInfoBleak) -> list[_T]: """Check for a match.""" - matches = [] + matches: list[_T] = [] if (name := service_info.name) and ( local_name_matchers := self.local_name.get( name[:LOCAL_NAME_MIN_MATCH_LENGTH] ) ): - for matcher in local_name_matchers: - if ble_device_matches(matcher, service_info): - matches.append(matcher) + matches.extend( + matcher + for matcher in local_name_matchers + if ble_device_matches(matcher, service_info) + ) if self.service_data_uuid_set and service_info.service_data: - for service_data_uuid in self.service_data_uuid_set.intersection( - service_info.service_data - ): - for matcher in self.service_data_uuid[service_data_uuid]: - if ble_device_matches(matcher, service_info): - matches.append(matcher) + matches.extend( + matcher + for service_data_uuid in self.service_data_uuid_set.intersection( + service_info.service_data + ) + for matcher in self.service_data_uuid[service_data_uuid] + if ble_device_matches(matcher, service_info) + ) if self.manufacturer_id_set and service_info.manufacturer_data: - for manufacturer_id in self.manufacturer_id_set.intersection( - service_info.manufacturer_data - ): - for matcher in self.manufacturer_id[manufacturer_id]: - if ble_device_matches(matcher, service_info): - matches.append(matcher) + matches.extend( + matcher + for manufacturer_id in self.manufacturer_id_set.intersection( + service_info.manufacturer_data + ) + for matcher in self.manufacturer_id[manufacturer_id] + if ble_device_matches(matcher, service_info) + ) if self.service_uuid_set and service_info.service_uuids: - for service_uuid in self.service_uuid_set.intersection( - service_info.service_uuids - ): - for matcher in self.service_uuid[service_uuid]: - if ble_device_matches(matcher, service_info): - matches.append(matcher) + matches.extend( + matcher + for service_uuid in self.service_uuid_set.intersection( + service_info.service_uuids + ) + for matcher in self.service_uuid[service_uuid] + if ble_device_matches(matcher, service_info) + ) return matches diff --git a/homeassistant/components/bluetooth/models.py b/homeassistant/components/bluetooth/models.py index 001a47767a1..a14aaf1d379 100644 --- a/homeassistant/components/bluetooth/models.py +++ b/homeassistant/components/bluetooth/models.py @@ -1,4 +1,5 @@ """Models for bluetooth.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/bluetooth/passive_update_coordinator.py b/homeassistant/components/bluetooth/passive_update_coordinator.py index fcf6fcdf255..81a67f6caef 100644 --- a/homeassistant/components/bluetooth/passive_update_coordinator.py +++ b/homeassistant/components/bluetooth/passive_update_coordinator.py @@ -1,7 +1,10 @@ """Passive update coordinator for the Bluetooth integration.""" + from __future__ import annotations -from typing import TYPE_CHECKING, Any, TypeVar +from typing import TYPE_CHECKING, Any + +from typing_extensions import TypeVar from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers.update_coordinator import ( @@ -20,6 +23,7 @@ if TYPE_CHECKING: _PassiveBluetoothDataUpdateCoordinatorT = TypeVar( "_PassiveBluetoothDataUpdateCoordinatorT", bound="PassiveBluetoothDataUpdateCoordinator", + default="PassiveBluetoothDataUpdateCoordinator", ) diff --git a/homeassistant/components/bluetooth/passive_update_processor.py b/homeassistant/components/bluetooth/passive_update_processor.py index a92a5317ba4..b7a7a165f71 100644 --- a/homeassistant/components/bluetooth/passive_update_processor.py +++ b/homeassistant/components/bluetooth/passive_update_processor.py @@ -1,4 +1,5 @@ """Passive update processors for the Bluetooth integration.""" + from __future__ import annotations import dataclasses @@ -128,9 +129,9 @@ class PassiveBluetoothDataUpdate(Generic[_T]): """Generic bluetooth data.""" devices: dict[str | None, DeviceInfo] = dataclasses.field(default_factory=dict) - entity_descriptions: dict[ - PassiveBluetoothEntityKey, EntityDescription - ] = dataclasses.field(default_factory=dict) + entity_descriptions: dict[PassiveBluetoothEntityKey, EntityDescription] = ( + dataclasses.field(default_factory=dict) + ) entity_names: dict[PassiveBluetoothEntityKey, str | None] = dataclasses.field( default_factory=dict ) @@ -271,7 +272,9 @@ async def async_setup(hass: HomeAssistant) -> None: await _async_save_processor_data(None) hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, _async_save_processor_data_at_stop + EVENT_HOMEASSISTANT_STOP, + _async_save_processor_data_at_stop, + run_immediately=True, ) diff --git a/homeassistant/components/bluetooth/storage.py b/homeassistant/components/bluetooth/storage.py index 41354e95b2e..6b4c7695fd2 100644 --- a/homeassistant/components/bluetooth/storage.py +++ b/homeassistant/components/bluetooth/storage.py @@ -1,4 +1,5 @@ """Storage for remote scanners.""" + from __future__ import annotations from bluetooth_adapters import ( diff --git a/homeassistant/components/bluetooth/update_coordinator.py b/homeassistant/components/bluetooth/update_coordinator.py index 2d495a0659c..eb2f8c0cf82 100644 --- a/homeassistant/components/bluetooth/update_coordinator.py +++ b/homeassistant/components/bluetooth/update_coordinator.py @@ -1,4 +1,5 @@ """Update coordinator for the Bluetooth integration.""" + from __future__ import annotations from abc import ABC, abstractmethod diff --git a/homeassistant/components/bluetooth/util.py b/homeassistant/components/bluetooth/util.py index d531e46f911..0faac9a8613 100644 --- a/homeassistant/components/bluetooth/util.py +++ b/homeassistant/components/bluetooth/util.py @@ -1,4 +1,5 @@ """The bluetooth integration utilities.""" + from __future__ import annotations from bluetooth_adapters import BluetoothAdapters @@ -28,14 +29,14 @@ def async_load_history_from_system( not (existing_all := connectable_loaded_history.get(address)) or history.advertisement_data.rssi > existing_all.rssi ): - connectable_loaded_history[address] = all_loaded_history[ - address - ] = BluetoothServiceInfoBleak.from_device_and_advertisement_data( - history.device, - history.advertisement_data, - history.source, - now_monotonic, - True, + connectable_loaded_history[address] = all_loaded_history[address] = ( + BluetoothServiceInfoBleak.from_device_and_advertisement_data( + history.device, + history.advertisement_data, + history.source, + now_monotonic, + True, + ) ) # Restore remote adapters diff --git a/homeassistant/components/bluetooth_adapters/__init__.py b/homeassistant/components/bluetooth_adapters/__init__.py index 3d5580aabf1..90593bf1018 100644 --- a/homeassistant/components/bluetooth_adapters/__init__.py +++ b/homeassistant/components/bluetooth_adapters/__init__.py @@ -1,4 +1,5 @@ """The Bluetooth Adapters integration.""" + from __future__ import annotations from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/bluetooth_le_tracker/device_tracker.py b/homeassistant/components/bluetooth_le_tracker/device_tracker.py index f85a9506d72..1a88e1c5fa3 100644 --- a/homeassistant/components/bluetooth_le_tracker/device_tracker.py +++ b/homeassistant/components/bluetooth_le_tracker/device_tracker.py @@ -1,4 +1,5 @@ """Tracking for bluetooth low energy devices.""" + from __future__ import annotations from datetime import datetime, timedelta diff --git a/homeassistant/components/bluetooth_tracker/const.py b/homeassistant/components/bluetooth_tracker/const.py index 8257e5554ec..6be453be9ff 100644 --- a/homeassistant/components/bluetooth_tracker/const.py +++ b/homeassistant/components/bluetooth_tracker/const.py @@ -1,4 +1,5 @@ """Constants for the Bluetooth Tracker component.""" + from typing import Final DOMAIN: Final = "bluetooth_tracker" diff --git a/homeassistant/components/bluetooth_tracker/device_tracker.py b/homeassistant/components/bluetooth_tracker/device_tracker.py index 6fecc428c10..7cde6f848d5 100644 --- a/homeassistant/components/bluetooth_tracker/device_tracker.py +++ b/homeassistant/components/bluetooth_tracker/device_tracker.py @@ -1,4 +1,5 @@ """Tracking for bluetooth devices.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/bluetooth_tracker/icons.json b/homeassistant/components/bluetooth_tracker/icons.json new file mode 100644 index 00000000000..650bf0b6d19 --- /dev/null +++ b/homeassistant/components/bluetooth_tracker/icons.json @@ -0,0 +1,5 @@ +{ + "services": { + "update": "mdi:update" + } +} diff --git a/homeassistant/components/bmw_connected_drive/__init__.py b/homeassistant/components/bmw_connected_drive/__init__.py index 079563b1ad3..663003a5e4b 100644 --- a/homeassistant/components/bmw_connected_drive/__init__.py +++ b/homeassistant/components/bmw_connected_drive/__init__.py @@ -1,4 +1,5 @@ """Reads vehicle status from MyBMW portal.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/bmw_connected_drive/binary_sensor.py b/homeassistant/components/bmw_connected_drive/binary_sensor.py index 7ff9ad2d8ab..85a0cbf8812 100644 --- a/homeassistant/components/bmw_connected_drive/binary_sensor.py +++ b/homeassistant/components/bmw_connected_drive/binary_sensor.py @@ -1,4 +1,5 @@ """Reads vehicle status from BMW MyBMW portal.""" + from __future__ import annotations from collections.abc import Callable @@ -109,19 +110,11 @@ def _format_cbs_report( return result -@dataclass(frozen=True) -class BMWRequiredKeysMixin: - """Mixin for required keys.""" - - value_fn: Callable[[MyBMWVehicle], bool] - - -@dataclass(frozen=True) -class BMWBinarySensorEntityDescription( - BinarySensorEntityDescription, BMWRequiredKeysMixin -): +@dataclass(frozen=True, kw_only=True) +class BMWBinarySensorEntityDescription(BinarySensorEntityDescription): """Describes BMW binary_sensor entity.""" + value_fn: Callable[[MyBMWVehicle], bool] attr_fn: Callable[[MyBMWVehicle, UnitSystem], dict[str, Any]] | None = None diff --git a/homeassistant/components/bmw_connected_drive/button.py b/homeassistant/components/bmw_connected_drive/button.py index 74f12c9c721..fe103f0e003 100644 --- a/homeassistant/components/bmw_connected_drive/button.py +++ b/homeassistant/components/bmw_connected_drive/button.py @@ -1,4 +1,5 @@ """Support for MyBMW button entities.""" + from __future__ import annotations from collections.abc import Callable, Coroutine @@ -25,17 +26,11 @@ if TYPE_CHECKING: _LOGGER = logging.getLogger(__name__) -@dataclass(frozen=True) -class BMWRequiredKeysMixin: - """Mixin for required keys.""" - - remote_function: Callable[[MyBMWVehicle], Coroutine[Any, Any, RemoteServiceStatus]] - - -@dataclass(frozen=True) -class BMWButtonEntityDescription(ButtonEntityDescription, BMWRequiredKeysMixin): +@dataclass(frozen=True, kw_only=True) +class BMWButtonEntityDescription(ButtonEntityDescription): """Class describing BMW button entities.""" + remote_function: Callable[[MyBMWVehicle], Coroutine[Any, Any, RemoteServiceStatus]] enabled_when_read_only: bool = False is_available: Callable[[MyBMWVehicle], bool] = lambda _: True diff --git a/homeassistant/components/bmw_connected_drive/config_flow.py b/homeassistant/components/bmw_connected_drive/config_flow.py index 926706397a6..fc274fc0f54 100644 --- a/homeassistant/components/bmw_connected_drive/config_flow.py +++ b/homeassistant/components/bmw_connected_drive/config_flow.py @@ -1,4 +1,5 @@ """Config flow for BMW ConnectedDrive integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -10,10 +11,15 @@ from bimmer_connected.models import MyBMWAPIError, MyBMWAuthError from httpx import RequestError import voluptuous as vol -from homeassistant import config_entries, core, exceptions +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlowWithConfigEntry, +) from homeassistant.const import CONF_PASSWORD, CONF_REGION, CONF_SOURCE, CONF_USERNAME -from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError from . import DOMAIN from .const import CONF_ALLOWED_REGIONS, CONF_GCID, CONF_READ_ONLY, CONF_REFRESH_TOKEN @@ -27,9 +33,7 @@ DATA_SCHEMA = vol.Schema( ) -async def validate_input( - hass: core.HomeAssistant, data: dict[str, Any] -) -> dict[str, str]: +async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, str]: """Validate the user input allows us to connect. Data has the keys from DATA_SCHEMA with values provided by the user. @@ -56,16 +60,16 @@ async def validate_input( return retval -class BMWConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class BMWConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for MyBMW.""" VERSION = 1 - _reauth_entry: config_entries.ConfigEntry | None = None + _reauth_entry: ConfigEntry | None = None async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors: dict[str, str] = {} @@ -112,7 +116,9 @@ class BMWConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form(step_id="user", data_schema=schema, errors=errors) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Handle configuration by re-auth.""" self._reauth_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] @@ -122,24 +128,24 @@ class BMWConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback def async_get_options_flow( - config_entry: config_entries.ConfigEntry, + config_entry: ConfigEntry, ) -> BMWOptionsFlow: """Return a MyBMW option flow.""" return BMWOptionsFlow(config_entry) -class BMWOptionsFlow(config_entries.OptionsFlowWithConfigEntry): +class BMWOptionsFlow(OptionsFlowWithConfigEntry): """Handle a option flow for MyBMW.""" async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Manage the options.""" return await self.async_step_account_options() async def async_step_account_options( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" if user_input is not None: # Manually update & reload the config entry after options change. @@ -166,9 +172,9 @@ class BMWOptionsFlow(config_entries.OptionsFlowWithConfigEntry): ) -class CannotConnect(exceptions.HomeAssistantError): +class CannotConnect(HomeAssistantError): """Error to indicate we cannot connect.""" -class InvalidAuth(exceptions.HomeAssistantError): +class InvalidAuth(HomeAssistantError): """Error to indicate there is invalid auth.""" diff --git a/homeassistant/components/bmw_connected_drive/const.py b/homeassistant/components/bmw_connected_drive/const.py index 96ef152307d..49990977f71 100644 --- a/homeassistant/components/bmw_connected_drive/const.py +++ b/homeassistant/components/bmw_connected_drive/const.py @@ -1,4 +1,5 @@ """Const file for the MyBMW integration.""" + from homeassistant.const import UnitOfLength, UnitOfVolume DOMAIN = "bmw_connected_drive" diff --git a/homeassistant/components/bmw_connected_drive/coordinator.py b/homeassistant/components/bmw_connected_drive/coordinator.py index 4e811d48647..14875c54719 100644 --- a/homeassistant/components/bmw_connected_drive/coordinator.py +++ b/homeassistant/components/bmw_connected_drive/coordinator.py @@ -1,4 +1,5 @@ """Coordinator for BMW.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/bmw_connected_drive/device_tracker.py b/homeassistant/components/bmw_connected_drive/device_tracker.py index a97ed1e1092..d6846d0b88e 100644 --- a/homeassistant/components/bmw_connected_drive/device_tracker.py +++ b/homeassistant/components/bmw_connected_drive/device_tracker.py @@ -1,4 +1,5 @@ """Device tracker for MyBMW vehicles.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/bmw_connected_drive/diagnostics.py b/homeassistant/components/bmw_connected_drive/diagnostics.py index c69d06d818f..c2bd4b6d24a 100644 --- a/homeassistant/components/bmw_connected_drive/diagnostics.py +++ b/homeassistant/components/bmw_connected_drive/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for the BMW Connected Drive integration.""" + from __future__ import annotations from dataclasses import asdict diff --git a/homeassistant/components/bmw_connected_drive/lock.py b/homeassistant/components/bmw_connected_drive/lock.py index 6608206a0ee..9529c135280 100644 --- a/homeassistant/components/bmw_connected_drive/lock.py +++ b/homeassistant/components/bmw_connected_drive/lock.py @@ -1,4 +1,5 @@ """Support for BMW car locks with BMW ConnectedDrive.""" + from __future__ import annotations import logging @@ -30,17 +31,10 @@ async def async_setup_entry( """Set up the MyBMW lock from config entry.""" coordinator: BMWDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] - entities: list[BMWLock] = [] - - for vehicle in coordinator.account.vehicles: - if not coordinator.read_only: - entities.append( - BMWLock( - coordinator, - vehicle, - ) - ) - async_add_entities(entities) + if not coordinator.read_only: + async_add_entities( + BMWLock(coordinator, vehicle) for vehicle in coordinator.account.vehicles + ) class BMWLock(BMWBaseEntity, LockEntity): @@ -108,8 +102,8 @@ class BMWLock(BMWBaseEntity, LockEntity): LockState.LOCKED, LockState.SECURED, } - self._attr_extra_state_attributes[ - "door_lock_state" - ] = self.vehicle.doors_and_windows.door_lock_state.value + self._attr_extra_state_attributes["door_lock_state"] = ( + self.vehicle.doors_and_windows.door_lock_state.value + ) super()._handle_coordinator_update() diff --git a/homeassistant/components/bmw_connected_drive/notify.py b/homeassistant/components/bmw_connected_drive/notify.py index 4a9f7679dc4..84bc2d8459a 100644 --- a/homeassistant/components/bmw_connected_drive/notify.py +++ b/homeassistant/components/bmw_connected_drive/notify.py @@ -1,4 +1,5 @@ """Support for BMW notifications.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/bmw_connected_drive/number.py b/homeassistant/components/bmw_connected_drive/number.py index 21326a59118..defeb3f0f56 100644 --- a/homeassistant/components/bmw_connected_drive/number.py +++ b/homeassistant/components/bmw_connected_drive/number.py @@ -26,18 +26,12 @@ from .coordinator import BMWDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) -@dataclass(frozen=True) -class BMWRequiredKeysMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class BMWNumberEntityDescription(NumberEntityDescription): + """Describes BMW number entity.""" value_fn: Callable[[MyBMWVehicle], float | int | None] remote_service: Callable[[MyBMWVehicle, float | int], Coroutine[Any, Any, Any]] - - -@dataclass(frozen=True) -class BMWNumberEntityDescription(NumberEntityDescription, BMWRequiredKeysMixin): - """Describes BMW number entity.""" - is_available: Callable[[MyBMWVehicle], bool] = lambda _: False dynamic_options: Callable[[MyBMWVehicle], list[str]] | None = None diff --git a/homeassistant/components/bmw_connected_drive/select.py b/homeassistant/components/bmw_connected_drive/select.py index db426b89487..409002b48e9 100644 --- a/homeassistant/components/bmw_connected_drive/select.py +++ b/homeassistant/components/bmw_connected_drive/select.py @@ -1,4 +1,5 @@ """Select platform for BMW.""" + from collections.abc import Callable, Coroutine from dataclasses import dataclass import logging @@ -22,18 +23,12 @@ from .coordinator import BMWDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) -@dataclass(frozen=True) -class BMWRequiredKeysMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class BMWSelectEntityDescription(SelectEntityDescription): + """Describes BMW sensor entity.""" current_option: Callable[[MyBMWVehicle], str] remote_service: Callable[[MyBMWVehicle, str], Coroutine[Any, Any, Any]] - - -@dataclass(frozen=True) -class BMWSelectEntityDescription(SelectEntityDescription, BMWRequiredKeysMixin): - """Describes BMW sensor entity.""" - is_available: Callable[[MyBMWVehicle], bool] = lambda _: False dynamic_options: Callable[[MyBMWVehicle], list[str]] | None = None diff --git a/homeassistant/components/bmw_connected_drive/sensor.py b/homeassistant/components/bmw_connected_drive/sensor.py index 27a5824a7d7..49842305af0 100644 --- a/homeassistant/components/bmw_connected_drive/sensor.py +++ b/homeassistant/components/bmw_connected_drive/sensor.py @@ -1,4 +1,5 @@ """Support for reading vehicle status from MyBMW portal.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/bmw_connected_drive/switch.py b/homeassistant/components/bmw_connected_drive/switch.py index 7c8952f4ecc..5ee31d2efd7 100644 --- a/homeassistant/components/bmw_connected_drive/switch.py +++ b/homeassistant/components/bmw_connected_drive/switch.py @@ -22,19 +22,13 @@ from .coordinator import BMWDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) -@dataclass(frozen=True) -class BMWRequiredKeysMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class BMWSwitchEntityDescription(SwitchEntityDescription): + """Describes BMW switch entity.""" value_fn: Callable[[MyBMWVehicle], bool] remote_service_on: Callable[[MyBMWVehicle], Coroutine[Any, Any, Any]] remote_service_off: Callable[[MyBMWVehicle], Coroutine[Any, Any, Any]] - - -@dataclass(frozen=True) -class BMWSwitchEntityDescription(SwitchEntityDescription, BMWRequiredKeysMixin): - """Describes BMW switch entity.""" - is_available: Callable[[MyBMWVehicle], bool] = lambda _: False dynamic_options: Callable[[MyBMWVehicle], list[str]] | None = None diff --git a/homeassistant/components/bond/__init__.py b/homeassistant/components/bond/__init__.py index 2e60512156f..216a4b501f2 100644 --- a/homeassistant/components/bond/__init__.py +++ b/homeassistant/components/bond/__init__.py @@ -1,4 +1,5 @@ """The Bond integration.""" + from http import HTTPStatus import logging from typing import Any @@ -67,7 +68,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.async_on_unload(_async_stop_event) entry.async_on_unload( - hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, _async_stop_event) + hass.bus.async_listen( + EVENT_HOMEASSISTANT_STOP, _async_stop_event, run_immediately=True + ) ) hass.data.setdefault(DOMAIN, {})[entry.entry_id] = BondData(hub, bpup_subs) diff --git a/homeassistant/components/bond/button.py b/homeassistant/components/bond/button.py index 273ef837f6e..a8a5a890f2c 100644 --- a/homeassistant/components/bond/button.py +++ b/homeassistant/components/bond/button.py @@ -1,4 +1,5 @@ """Support for bond buttons.""" + from __future__ import annotations from dataclasses import dataclass @@ -21,29 +22,21 @@ from .utils import BondDevice, BondHub STEP_SIZE = 10 -@dataclass(frozen=True) -class BondButtonEntityDescriptionMixin: - """Mixin to describe a Bond Button entity.""" - - mutually_exclusive: Action | None - argument: int | None - - -@dataclass(frozen=True) -class BondButtonEntityDescription( - ButtonEntityDescription, BondButtonEntityDescriptionMixin -): +@dataclass(frozen=True, kw_only=True) +class BondButtonEntityDescription(ButtonEntityDescription): """Class to describe a Bond Button entity.""" # BondEntity does not support UNDEFINED, # restrict the type to str | None name: str | None = None + mutually_exclusive: Action | None + argument: int | None STOP_BUTTON = BondButtonEntityDescription( key=Action.STOP, name="Stop Actions", - icon="mdi:stop-circle-outline", + translation_key="stop_actions", mutually_exclusive=None, argument=None, ) @@ -53,175 +46,175 @@ BUTTONS: tuple[BondButtonEntityDescription, ...] = ( BondButtonEntityDescription( key=Action.TOGGLE_POWER, name="Toggle Power", - icon="mdi:power-cycle", + translation_key="toggle_power", mutually_exclusive=Action.TURN_ON, argument=None, ), BondButtonEntityDescription( key=Action.TOGGLE_LIGHT, name="Toggle Light", - icon="mdi:lightbulb", + translation_key="toggle_light", mutually_exclusive=Action.TURN_LIGHT_ON, argument=None, ), BondButtonEntityDescription( key=Action.INCREASE_BRIGHTNESS, name="Increase Brightness", - icon="mdi:brightness-7", + translation_key="increase_brightness", mutually_exclusive=Action.SET_BRIGHTNESS, argument=STEP_SIZE, ), BondButtonEntityDescription( key=Action.DECREASE_BRIGHTNESS, name="Decrease Brightness", - icon="mdi:brightness-1", + translation_key="decrease_brightness", mutually_exclusive=Action.SET_BRIGHTNESS, argument=STEP_SIZE, ), BondButtonEntityDescription( key=Action.TOGGLE_UP_LIGHT, name="Toggle Up Light", - icon="mdi:lightbulb", + translation_key="toggle_up_light", mutually_exclusive=Action.TURN_UP_LIGHT_ON, argument=None, ), BondButtonEntityDescription( key=Action.TOGGLE_DOWN_LIGHT, name="Toggle Down Light", - icon="mdi:lightbulb", + translation_key="toggle_down_light", mutually_exclusive=Action.TURN_DOWN_LIGHT_ON, argument=None, ), BondButtonEntityDescription( key=Action.START_DIMMER, name="Start Dimmer", - icon="mdi:brightness-percent", + translation_key="start_dimmer", mutually_exclusive=Action.SET_BRIGHTNESS, argument=None, ), BondButtonEntityDescription( key=Action.START_UP_LIGHT_DIMMER, name="Start Up Light Dimmer", - icon="mdi:brightness-percent", + translation_key="start_up_light_dimmer", mutually_exclusive=Action.SET_UP_LIGHT_BRIGHTNESS, argument=None, ), BondButtonEntityDescription( key=Action.START_DOWN_LIGHT_DIMMER, name="Start Down Light Dimmer", - icon="mdi:brightness-percent", + translation_key="start_down_light_dimmer", mutually_exclusive=Action.SET_DOWN_LIGHT_BRIGHTNESS, argument=None, ), BondButtonEntityDescription( key=Action.START_INCREASING_BRIGHTNESS, name="Start Increasing Brightness", - icon="mdi:brightness-percent", + translation_key="start_increasing_brightness", mutually_exclusive=Action.SET_BRIGHTNESS, argument=None, ), BondButtonEntityDescription( key=Action.START_DECREASING_BRIGHTNESS, name="Start Decreasing Brightness", - icon="mdi:brightness-percent", + translation_key="start_decreasing_brightness", mutually_exclusive=Action.SET_BRIGHTNESS, argument=None, ), BondButtonEntityDescription( key=Action.INCREASE_UP_LIGHT_BRIGHTNESS, name="Increase Up Light Brightness", - icon="mdi:brightness-percent", + translation_key="increase_up_light_brightness", mutually_exclusive=Action.SET_UP_LIGHT_BRIGHTNESS, argument=STEP_SIZE, ), BondButtonEntityDescription( key=Action.DECREASE_UP_LIGHT_BRIGHTNESS, name="Decrease Up Light Brightness", - icon="mdi:brightness-percent", + translation_key="decrease_up_light_brightness", mutually_exclusive=Action.SET_UP_LIGHT_BRIGHTNESS, argument=STEP_SIZE, ), BondButtonEntityDescription( key=Action.INCREASE_DOWN_LIGHT_BRIGHTNESS, name="Increase Down Light Brightness", - icon="mdi:brightness-percent", + translation_key="increase_down_light_brightness", mutually_exclusive=Action.SET_DOWN_LIGHT_BRIGHTNESS, argument=STEP_SIZE, ), BondButtonEntityDescription( key=Action.DECREASE_DOWN_LIGHT_BRIGHTNESS, name="Decrease Down Light Brightness", - icon="mdi:brightness-percent", + translation_key="decrease_down_light_brightness", mutually_exclusive=Action.SET_DOWN_LIGHT_BRIGHTNESS, argument=STEP_SIZE, ), BondButtonEntityDescription( key=Action.CYCLE_UP_LIGHT_BRIGHTNESS, name="Cycle Up Light Brightness", - icon="mdi:brightness-percent", + translation_key="cycle_up_light_brightness", mutually_exclusive=Action.SET_UP_LIGHT_BRIGHTNESS, argument=STEP_SIZE, ), BondButtonEntityDescription( key=Action.CYCLE_DOWN_LIGHT_BRIGHTNESS, name="Cycle Down Light Brightness", - icon="mdi:brightness-percent", + translation_key="cycle_down_light_brightness", mutually_exclusive=Action.SET_DOWN_LIGHT_BRIGHTNESS, argument=STEP_SIZE, ), BondButtonEntityDescription( key=Action.CYCLE_BRIGHTNESS, name="Cycle Brightness", - icon="mdi:brightness-percent", + translation_key="cycle_brightness", mutually_exclusive=Action.SET_BRIGHTNESS, argument=STEP_SIZE, ), BondButtonEntityDescription( key=Action.INCREASE_SPEED, name="Increase Speed", - icon="mdi:skew-more", + translation_key="increase_speed", mutually_exclusive=Action.SET_SPEED, argument=1, ), BondButtonEntityDescription( key=Action.DECREASE_SPEED, name="Decrease Speed", - icon="mdi:skew-less", + translation_key="decrease_speed", mutually_exclusive=Action.SET_SPEED, argument=1, ), BondButtonEntityDescription( key=Action.TOGGLE_DIRECTION, name="Toggle Direction", - icon="mdi:directions-fork", + translation_key="toggle_direction", mutually_exclusive=Action.SET_DIRECTION, argument=None, ), BondButtonEntityDescription( key=Action.INCREASE_TEMPERATURE, name="Increase Temperature", - icon="mdi:thermometer-plus", + translation_key="increase_temperature", mutually_exclusive=None, argument=1, ), BondButtonEntityDescription( key=Action.DECREASE_TEMPERATURE, name="Decrease Temperature", - icon="mdi:thermometer-minus", + translation_key="decrease_temperature", mutually_exclusive=None, argument=1, ), BondButtonEntityDescription( key=Action.INCREASE_FLAME, name="Increase Flame", - icon="mdi:fire", + translation_key="increase_flame", mutually_exclusive=None, argument=STEP_SIZE, ), BondButtonEntityDescription( key=Action.DECREASE_FLAME, name="Decrease Flame", - icon="mdi:fire-off", + translation_key="decrease_flame", mutually_exclusive=None, argument=STEP_SIZE, ), @@ -234,14 +227,14 @@ BUTTONS: tuple[BondButtonEntityDescription, ...] = ( BondButtonEntityDescription( key=Action.INCREASE_POSITION, name="Increase Position", - icon="mdi:plus-box", + translation_key="increase_position", mutually_exclusive=Action.SET_POSITION, argument=STEP_SIZE, ), BondButtonEntityDescription( key=Action.DECREASE_POSITION, name="Decrease Position", - icon="mdi:minus-box", + translation_key="decrease_position", mutually_exclusive=Action.SET_POSITION, argument=STEP_SIZE, ), diff --git a/homeassistant/components/bond/config_flow.py b/homeassistant/components/bond/config_flow.py index 33b5d2bf2c4..45170a0404f 100644 --- a/homeassistant/components/bond/config_flow.py +++ b/homeassistant/components/bond/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Bond integration.""" + from __future__ import annotations import contextlib @@ -10,12 +11,11 @@ from aiohttp import ClientConnectionError, ClientResponseError from bond_async import Bond import voluptuous as vol -from homeassistant import config_entries, exceptions from homeassistant.components import zeroconf -from homeassistant.config_entries import ConfigEntryState +from homeassistant.config_entries import ConfigEntryState, ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST, CONF_NAME from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResult +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN @@ -66,7 +66,7 @@ async def _validate_input(hass: HomeAssistant, data: dict[str, Any]) -> tuple[st return hub.bond_id, hub.name -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class BondConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Bond.""" VERSION = 1 @@ -98,7 +98,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_zeroconf( self, discovery_info: zeroconf.ZeroconfServiceInfo - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by zeroconf discovery.""" name: str = discovery_info.name host: str = discovery_info.host @@ -132,7 +132,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle confirmation flow for discovered bond hub.""" errors = {} if user_input is not None: @@ -173,7 +173,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" errors = {} if user_input is not None: @@ -191,7 +191,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -class InputValidationError(exceptions.HomeAssistantError): +class InputValidationError(HomeAssistantError): """Error to indicate we cannot proceed due to invalid input.""" def __init__(self, base: str) -> None: diff --git a/homeassistant/components/bond/cover.py b/homeassistant/components/bond/cover.py index a41e188ed9d..06576277520 100644 --- a/homeassistant/components/bond/cover.py +++ b/homeassistant/components/bond/cover.py @@ -1,4 +1,5 @@ """Support for Bond covers.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/bond/diagnostics.py b/homeassistant/components/bond/diagnostics.py index 53e8b5c8225..8b79f36dd0b 100644 --- a/homeassistant/components/bond/diagnostics.py +++ b/homeassistant/components/bond/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for bond.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/bond/entity.py b/homeassistant/components/bond/entity.py index dd307547b81..02137d27b3d 100644 --- a/homeassistant/components/bond/entity.py +++ b/homeassistant/components/bond/entity.py @@ -1,4 +1,5 @@ """An abstract class common to all Bond entities.""" + from __future__ import annotations from abc import abstractmethod diff --git a/homeassistant/components/bond/fan.py b/homeassistant/components/bond/fan.py index 403e0ae01e6..1b7a06fcd37 100644 --- a/homeassistant/components/bond/fan.py +++ b/homeassistant/components/bond/fan.py @@ -1,4 +1,5 @@ """Support for Bond fans.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/bond/icons.json b/homeassistant/components/bond/icons.json new file mode 100644 index 00000000000..35743d20e65 --- /dev/null +++ b/homeassistant/components/bond/icons.json @@ -0,0 +1,107 @@ +{ + "entity": { + "button": { + "stop_actions": { + "default": "mdi:stop-circle-outline" + }, + "toggle_power": { + "default": "mdi:power-cycle" + }, + "toggle_light": { + "default": "mdi:lightbulb" + }, + "increase_brightness": { + "default": "mdi:brightness-7" + }, + "decrease_brightness": { + "default": "mdi:brightness-1" + }, + "toggle_up_light": { + "default": "mdi:lightbulb" + }, + "toggle_down_light": { + "default": "mdi:lightbulb" + }, + "start_dimmer": { + "default": "mdi:brightness-percent" + }, + "start_up_light_dimmer": { + "default": "mdi:brightness-percent" + }, + "start_down_light_dimmer": { + "default": "mdi:brightness-percent" + }, + "start_increasing_brightness": { + "default": "mdi:brightness-percent" + }, + "start_decreasing_brightness": { + "default": "mdi:brightness-percent" + }, + "increase_up_light_brightness": { + "default": "mdi:brightness-percent" + }, + "decrease_up_light_brightness": { + "default": "mdi:brightness-percent" + }, + "increase_down_light_brightness": { + "default": "mdi:brightness-percent" + }, + "decrease_down_light_brightness": { + "default": "mdi:brightness-percent" + }, + "cycle_up_light_brightness": { + "default": "mdi:brightness-percent" + }, + "cycle_down_light_brightness": { + "default": "mdi:brightness-percent" + }, + "cycle_brightness": { + "default": "mdi:brightness-percent" + }, + "increase_speed": { + "default": "mdi:skew-more" + }, + "decrease_speed": { + "default": "mdi:skew-less" + }, + "toggle_direction": { + "default": "mdi:directions-fork" + }, + "increase_temperature": { + "default": "mdi:thermometer-plus" + }, + "decrease_temperature": { + "default": "mdi:thermometer-minus" + }, + "increase_flame": { + "default": "mdi:fire" + }, + "decrease_flame": { + "default": "mdi:fire-off" + }, + "increase_position": { + "default": "mdi:plus-box" + }, + "decrease_position": { + "default": "mdi:minus-box" + } + }, + "light": { + "fireplace": { + "default": "mdi:fireplace-off", + "state": { + "on": "mdi:fireplace" + } + } + } + }, + "services": { + "set_fan_speed_tracked_state": "mdi:fan", + "set_switch_power_tracked_state": "mdi:toggle-switch-variant", + "set_light_power_tracked_state": "mdi:lightbulb", + "set_light_brightness_tracked_state": "mdi:lightbulb-on", + "start_increasing_brightness": "mdi:brightness-7", + "start_decreasing_brightness": "mdi:brightness-1", + "stop": "mdi:stop" + } +} diff --git a/homeassistant/components/bond/light.py b/homeassistant/components/bond/light.py index c5816153c8d..bd1183a3a98 100644 --- a/homeassistant/components/bond/light.py +++ b/homeassistant/components/bond/light.py @@ -1,4 +1,5 @@ """Support for Bond lights.""" + from __future__ import annotations import logging @@ -273,6 +274,7 @@ class BondFireplace(BondEntity, LightEntity): _attr_color_mode = ColorMode.BRIGHTNESS _attr_supported_color_modes = {ColorMode.BRIGHTNESS} + _attr_translation_key = "fireplace" def _apply_state(self) -> None: state = self._device.state @@ -280,7 +282,6 @@ class BondFireplace(BondEntity, LightEntity): flame = state.get("flame") self._attr_is_on = power == 1 self._attr_brightness = round(flame * 255 / 100) if flame else None - self._attr_icon = "mdi:fireplace" if power == 1 else "mdi:fireplace-off" async def async_turn_on(self, **kwargs: Any) -> None: """Turn the fireplace on.""" diff --git a/homeassistant/components/bond/models.py b/homeassistant/components/bond/models.py index 0caa01af7a0..7564961ee78 100644 --- a/homeassistant/components/bond/models.py +++ b/homeassistant/components/bond/models.py @@ -1,4 +1,5 @@ """The bond integration models.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/homeassistant/components/bond/switch.py b/homeassistant/components/bond/switch.py index 887532defd1..aa39f871c95 100644 --- a/homeassistant/components/bond/switch.py +++ b/homeassistant/components/bond/switch.py @@ -1,4 +1,5 @@ """Support for Bond generic devices.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/bond/utils.py b/homeassistant/components/bond/utils.py index 60b9a7b492f..0a1067de709 100644 --- a/homeassistant/components/bond/utils.py +++ b/homeassistant/components/bond/utils.py @@ -1,4 +1,5 @@ """Reusable utilities for the Bond component.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/bosch_shc/__init__.py b/homeassistant/components/bosch_shc/__init__.py index a8b2a389f9f..9a00029412d 100644 --- a/homeassistant/components/bosch_shc/__init__.py +++ b/homeassistant/components/bosch_shc/__init__.py @@ -1,4 +1,5 @@ """The Bosch Smart Home Controller integration.""" + import logging from boschshcpy import SHCSession @@ -75,9 +76,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await hass.async_add_executor_job(session.stop_polling) await hass.async_add_executor_job(session.start_polling) - hass.data[DOMAIN][entry.entry_id][ - DATA_POLLING_HANDLER - ] = hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_polling) + hass.data[DOMAIN][entry.entry_id][DATA_POLLING_HANDLER] = ( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_polling) + ) return True diff --git a/homeassistant/components/bosch_shc/binary_sensor.py b/homeassistant/components/bosch_shc/binary_sensor.py index c9969fcf415..342a3e3e417 100644 --- a/homeassistant/components/bosch_shc/binary_sensor.py +++ b/homeassistant/components/bosch_shc/binary_sensor.py @@ -1,4 +1,5 @@ """Platform for binarysensor integration.""" + from __future__ import annotations from boschshcpy import SHCBatteryDevice, SHCSession, SHCShutterContact @@ -22,39 +23,38 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the SHC binary sensor platform.""" - entities: list[BinarySensorEntity] = [] session: SHCSession = hass.data[DOMAIN][config_entry.entry_id][DATA_SESSION] - for binary_sensor in ( - session.device_helper.shutter_contacts + session.device_helper.shutter_contacts2 - ): - entities.append( - ShutterContactSensor( - device=binary_sensor, - parent_id=session.information.unique_id, - entry_id=config_entry.entry_id, - ) + entities: list[BinarySensorEntity] = [ + ShutterContactSensor( + device=binary_sensor, + parent_id=session.information.unique_id, + entry_id=config_entry.entry_id, ) + for binary_sensor in ( + session.device_helper.shutter_contacts + + session.device_helper.shutter_contacts2 + ) + ] - for binary_sensor in ( - session.device_helper.motion_detectors - + session.device_helper.shutter_contacts - + session.device_helper.shutter_contacts2 - + session.device_helper.smoke_detectors - + session.device_helper.thermostats - + session.device_helper.twinguards - + session.device_helper.universal_switches - + session.device_helper.wallthermostats - + session.device_helper.water_leakage_detectors - ): - if binary_sensor.supports_batterylevel: - entities.append( - BatterySensor( - device=binary_sensor, - parent_id=session.information.unique_id, - entry_id=config_entry.entry_id, - ) - ) + entities.extend( + BatterySensor( + device=binary_sensor, + parent_id=session.information.unique_id, + entry_id=config_entry.entry_id, + ) + for binary_sensor in ( + session.device_helper.motion_detectors + + session.device_helper.shutter_contacts + + session.device_helper.shutter_contacts2 + + session.device_helper.smoke_detectors + + session.device_helper.thermostats + + session.device_helper.twinguards + + session.device_helper.universal_switches + + session.device_helper.wallthermostats + + session.device_helper.water_leakage_detectors + ) + ) async_add_entities(entities) diff --git a/homeassistant/components/bosch_shc/config_flow.py b/homeassistant/components/bosch_shc/config_flow.py index c19ab7726b2..5483c080f39 100644 --- a/homeassistant/components/bosch_shc/config_flow.py +++ b/homeassistant/components/bosch_shc/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Bosch Smart Home Controller integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -15,11 +16,10 @@ from boschshcpy.exceptions import ( ) import voluptuous as vol -from homeassistant import config_entries from homeassistant.components import zeroconf +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_TOKEN from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResult from .const import ( CONF_HOSTNAME, @@ -87,20 +87,22 @@ def get_info_from_host( return {"title": information.name, "unique_id": information.unique_id} -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class BoschSHCConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Bosch SHC.""" VERSION = 1 info: dict[str, str | None] host: str - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Perform reauth upon an API authentication error.""" return await self.async_step_reauth_confirm() async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Dialog that informs the user that reauth is required.""" if user_input is None: return self.async_show_form( @@ -113,7 +115,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors: dict[str, str] = {} if user_input is not None: @@ -136,7 +138,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_credentials( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the credentials step.""" errors: dict[str, str] = {} if user_input is not None: @@ -201,7 +203,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_zeroconf( self, discovery_info: zeroconf.ZeroconfServiceInfo - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle zeroconf discovery.""" if not discovery_info.name.startswith("Bosch SHC"): return self.async_abort(reason="not_bosch_shc") @@ -222,7 +224,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_confirm_discovery( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle discovery confirm.""" errors: dict[str, str] = {} if user_input is not None: diff --git a/homeassistant/components/bosch_shc/cover.py b/homeassistant/components/bosch_shc/cover.py index 8b2a2f65c12..5377f0c6a8f 100644 --- a/homeassistant/components/bosch_shc/cover.py +++ b/homeassistant/components/bosch_shc/cover.py @@ -1,4 +1,5 @@ """Platform for cover integration.""" + from typing import Any from boschshcpy import SHCSession, SHCShutterControl @@ -24,19 +25,16 @@ async def async_setup_entry( ) -> None: """Set up the SHC cover platform.""" - entities = [] session: SHCSession = hass.data[DOMAIN][config_entry.entry_id][DATA_SESSION] - for cover in session.device_helper.shutter_controls: - entities.append( - ShutterControlCover( - device=cover, - parent_id=session.information.unique_id, - entry_id=config_entry.entry_id, - ) + async_add_entities( + ShutterControlCover( + device=cover, + parent_id=session.information.unique_id, + entry_id=config_entry.entry_id, ) - - async_add_entities(entities) + for cover in session.device_helper.shutter_controls + ) class ShutterControlCover(SHCEntity, CoverEntity): diff --git a/homeassistant/components/bosch_shc/entity.py b/homeassistant/components/bosch_shc/entity.py index 8c26d2e6d5a..b7697191d27 100644 --- a/homeassistant/components/bosch_shc/entity.py +++ b/homeassistant/components/bosch_shc/entity.py @@ -1,4 +1,5 @@ """Bosch Smart Home Controller base entity.""" + from __future__ import annotations from boschshcpy import SHCDevice, SHCIntrusionSystem diff --git a/homeassistant/components/bosch_shc/sensor.py b/homeassistant/components/bosch_shc/sensor.py index c9c194bdc08..14da3a4b92b 100644 --- a/homeassistant/components/bosch_shc/sensor.py +++ b/homeassistant/components/bosch_shc/sensor.py @@ -1,4 +1,5 @@ """Platform for sensor integration.""" + from __future__ import annotations from boschshcpy import SHCSession diff --git a/homeassistant/components/bosch_shc/switch.py b/homeassistant/components/bosch_shc/switch.py index 8e542c860d4..e6ccd2aa9aa 100644 --- a/homeassistant/components/bosch_shc/switch.py +++ b/homeassistant/components/bosch_shc/switch.py @@ -1,4 +1,5 @@ """Platform for switch integration.""" + from __future__ import annotations from dataclasses import dataclass @@ -29,23 +30,15 @@ from .const import DATA_SESSION, DOMAIN from .entity import SHCEntity -@dataclass(frozen=True) -class SHCSwitchRequiredKeysMixin: - """Mixin for SHC switch required keys.""" +@dataclass(frozen=True, kw_only=True) +class SHCSwitchEntityDescription(SwitchEntityDescription): + """Class describing SHC switch entities.""" on_key: str on_value: StateType should_poll: bool -@dataclass(frozen=True) -class SHCSwitchEntityDescription( - SwitchEntityDescription, - SHCSwitchRequiredKeysMixin, -): - """Class describing SHC switch entities.""" - - SWITCH_TYPES: dict[str, SHCSwitchEntityDescription] = { "smartplug": SHCSwitchEntityDescription( key="smartplug", @@ -91,65 +84,66 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the SHC switch platform.""" - entities: list[SwitchEntity] = [] session: SHCSession = hass.data[DOMAIN][config_entry.entry_id][DATA_SESSION] - for switch in session.device_helper.smart_plugs: - entities.append( - SHCSwitch( - device=switch, - parent_id=session.information.unique_id, - entry_id=config_entry.entry_id, - description=SWITCH_TYPES["smartplug"], - ) - ) - entities.append( - SHCRoutingSwitch( - device=switch, - parent_id=session.information.unique_id, - entry_id=config_entry.entry_id, - ) + entities: list[SwitchEntity] = [ + SHCSwitch( + device=switch, + parent_id=session.information.unique_id, + entry_id=config_entry.entry_id, + description=SWITCH_TYPES["smartplug"], ) + for switch in session.device_helper.smart_plugs + ] - for switch in session.device_helper.light_switches_bsm: - entities.append( - SHCSwitch( - device=switch, - parent_id=session.information.unique_id, - entry_id=config_entry.entry_id, - description=SWITCH_TYPES["lightswitch"], - ) + entities.extend( + SHCRoutingSwitch( + device=switch, + parent_id=session.information.unique_id, + entry_id=config_entry.entry_id, ) + for switch in session.device_helper.smart_plugs + ) - for switch in session.device_helper.smart_plugs_compact: - entities.append( - SHCSwitch( - device=switch, - parent_id=session.information.unique_id, - entry_id=config_entry.entry_id, - description=SWITCH_TYPES["smartplugcompact"], - ) + entities.extend( + SHCSwitch( + device=switch, + parent_id=session.information.unique_id, + entry_id=config_entry.entry_id, + description=SWITCH_TYPES["lightswitch"], ) + for switch in session.device_helper.light_switches_bsm + ) - for switch in session.device_helper.camera_eyes: - entities.append( - SHCSwitch( - device=switch, - parent_id=session.information.unique_id, - entry_id=config_entry.entry_id, - description=SWITCH_TYPES["cameraeyes"], - ) + entities.extend( + SHCSwitch( + device=switch, + parent_id=session.information.unique_id, + entry_id=config_entry.entry_id, + description=SWITCH_TYPES["smartplugcompact"], ) + for switch in session.device_helper.smart_plugs_compact + ) - for switch in session.device_helper.camera_360: - entities.append( - SHCSwitch( - device=switch, - parent_id=session.information.unique_id, - entry_id=config_entry.entry_id, - description=SWITCH_TYPES["camera360"], - ) + entities.extend( + SHCSwitch( + device=switch, + parent_id=session.information.unique_id, + entry_id=config_entry.entry_id, + description=SWITCH_TYPES["cameraeyes"], ) + for switch in session.device_helper.camera_eyes + ) + + entities.extend( + SHCSwitch( + device=switch, + parent_id=session.information.unique_id, + entry_id=config_entry.entry_id, + description=SWITCH_TYPES["camera360"], + ) + for switch in session.device_helper.camera_360 + ) async_add_entities(entities) diff --git a/homeassistant/components/braviatv/__init__.py b/homeassistant/components/braviatv/__init__.py index ecf119c8a3d..9027a8372ab 100644 --- a/homeassistant/components/braviatv/__init__.py +++ b/homeassistant/components/braviatv/__init__.py @@ -1,4 +1,5 @@ """The Bravia TV integration.""" + from __future__ import annotations from typing import Final diff --git a/homeassistant/components/braviatv/button.py b/homeassistant/components/braviatv/button.py index eb3d2d8797f..0b502a3773b 100644 --- a/homeassistant/components/braviatv/button.py +++ b/homeassistant/components/braviatv/button.py @@ -1,4 +1,5 @@ """Button support for Bravia TV.""" + from __future__ import annotations from collections.abc import Callable, Coroutine @@ -19,20 +20,13 @@ from .coordinator import BraviaTVCoordinator from .entity import BraviaTVEntity -@dataclass(frozen=True) -class BraviaTVButtonDescriptionMixin: - """Mixin to describe a Bravia TV Button entity.""" +@dataclass(frozen=True, kw_only=True) +class BraviaTVButtonDescription(ButtonEntityDescription): + """Bravia TV Button description.""" press_action: Callable[[BraviaTVCoordinator], Coroutine] -@dataclass(frozen=True) -class BraviaTVButtonDescription( - ButtonEntityDescription, BraviaTVButtonDescriptionMixin -): - """Bravia TV Button description.""" - - BUTTONS: tuple[BraviaTVButtonDescription, ...] = ( BraviaTVButtonDescription( key="reboot", diff --git a/homeassistant/components/braviatv/config_flow.py b/homeassistant/components/braviatv/config_flow.py index fd72203b249..b3ad55dbb7d 100644 --- a/homeassistant/components/braviatv/config_flow.py +++ b/homeassistant/components/braviatv/config_flow.py @@ -1,4 +1,5 @@ """Config flow to configure the Bravia TV integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -9,11 +10,9 @@ from aiohttp import CookieJar from pybravia import BraviaAuthError, BraviaClient, BraviaError, BraviaNotSupported import voluptuous as vol -from homeassistant import config_entries from homeassistant.components import ssdp -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_CLIENT_ID, CONF_HOST, CONF_MAC, CONF_NAME, CONF_PIN -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import instance_id from homeassistant.helpers.aiohttp_client import async_create_clientsession from homeassistant.util.network import is_host_valid @@ -29,7 +28,7 @@ from .const import ( ) -class BraviaTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class BraviaTVConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Bravia TV integration.""" VERSION = 1 @@ -69,7 +68,7 @@ class BraviaTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): await self.client.connect(pin=pin, clientid=client_id, nickname=nickname) await self.client.set_wol_mode(True) - async def async_create_device(self) -> FlowResult: + async def async_create_device(self) -> ConfigFlowResult: """Create Bravia TV device from config.""" assert self.client await self.async_connect_device() @@ -85,7 +84,7 @@ class BraviaTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_create_entry(title=title, data=self.device_config) - async def async_reauth_device(self) -> FlowResult: + async def async_reauth_device(self) -> ConfigFlowResult: """Reauthorize Bravia TV device from config.""" assert self.entry assert self.client @@ -97,7 +96,7 @@ class BraviaTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors: dict[str, str] = {} @@ -117,7 +116,7 @@ class BraviaTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_authorize( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle authorize step.""" self.create_client() @@ -138,7 +137,7 @@ class BraviaTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_pin( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle PIN authorize step.""" errors: dict[str, str] = {} client_id, nickname = await self.gen_instance_ids() @@ -177,7 +176,7 @@ class BraviaTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_psk( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle PSK authorize step.""" errors: dict[str, str] = {} @@ -204,7 +203,9 @@ class BraviaTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: + async def async_step_ssdp( + self, discovery_info: ssdp.SsdpServiceInfo + ) -> ConfigFlowResult: """Handle a discovered device.""" parsed_url = urlparse(discovery_info.ssdp_location) host = parsed_url.hostname @@ -234,14 +235,16 @@ class BraviaTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Allow the user to confirm adding the device.""" if user_input is not None: return await self.async_step_authorize() return self.async_show_form(step_id="confirm") - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Handle configuration by re-auth.""" self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) self.device_config = {**entry_data} diff --git a/homeassistant/components/braviatv/const.py b/homeassistant/components/braviatv/const.py index aff02aa9e8b..aadd851fc7f 100644 --- a/homeassistant/components/braviatv/const.py +++ b/homeassistant/components/braviatv/const.py @@ -1,4 +1,5 @@ """Constants for Bravia TV integration.""" + from __future__ import annotations from enum import StrEnum diff --git a/homeassistant/components/braviatv/coordinator.py b/homeassistant/components/braviatv/coordinator.py index 72d2107271f..15e6744ceb8 100644 --- a/homeassistant/components/braviatv/coordinator.py +++ b/homeassistant/components/braviatv/coordinator.py @@ -1,4 +1,5 @@ """Update coordinator for Bravia TV integration.""" + from __future__ import annotations from collections.abc import Awaitable, Callable, Coroutine, Iterable diff --git a/homeassistant/components/braviatv/diagnostics.py b/homeassistant/components/braviatv/diagnostics.py index f1822b545e9..917572ffcca 100644 --- a/homeassistant/components/braviatv/diagnostics.py +++ b/homeassistant/components/braviatv/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for BraviaTV.""" + from typing import Any from homeassistant.components.diagnostics import async_redact_data diff --git a/homeassistant/components/braviatv/entity.py b/homeassistant/components/braviatv/entity.py index 0f941d05e75..ac08543b875 100644 --- a/homeassistant/components/braviatv/entity.py +++ b/homeassistant/components/braviatv/entity.py @@ -1,4 +1,5 @@ """A entity class for Bravia TV integration.""" + from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/braviatv/media_player.py b/homeassistant/components/braviatv/media_player.py index 111f08e441a..ea4f3cce4a8 100644 --- a/homeassistant/components/braviatv/media_player.py +++ b/homeassistant/components/braviatv/media_player.py @@ -1,4 +1,5 @@ """Media player support for Bravia TV integration.""" + from __future__ import annotations from datetime import datetime diff --git a/homeassistant/components/braviatv/remote.py b/homeassistant/components/braviatv/remote.py index f9e3f464dcb..01d1bb6378c 100644 --- a/homeassistant/components/braviatv/remote.py +++ b/homeassistant/components/braviatv/remote.py @@ -1,4 +1,5 @@ """Remote control support for Bravia TV.""" + from __future__ import annotations from collections.abc import Iterable diff --git a/homeassistant/components/bring/__init__.py b/homeassistant/components/bring/__init__.py index aaf11130b8d..7c300a0e013 100644 --- a/homeassistant/components/bring/__init__.py +++ b/homeassistant/components/bring/__init__.py @@ -1,4 +1,5 @@ """The Bring! integration.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/bring/config_flow.py b/homeassistant/components/bring/config_flow.py index efd99fd938a..0b423f5af36 100644 --- a/homeassistant/components/bring/config_flow.py +++ b/homeassistant/components/bring/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Bring! integration.""" + from __future__ import annotations import logging @@ -10,7 +11,6 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_EMAIL, CONF_PASSWORD -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.selector import ( TextSelector, @@ -45,7 +45,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> config_entries.ConfigFlowResult: """Handle the initial step.""" errors: dict[str, str] = {} if user_input is not None: diff --git a/homeassistant/components/bring/coordinator.py b/homeassistant/components/bring/coordinator.py index 550c589aa4e..057e7549503 100644 --- a/homeassistant/components/bring/coordinator.py +++ b/homeassistant/components/bring/coordinator.py @@ -1,4 +1,5 @@ """DataUpdateCoordinator for the Bring! integration.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/bring/manifest.json b/homeassistant/components/bring/manifest.json index 6b905a61b7d..be2c5633362 100644 --- a/homeassistant/components/bring/manifest.json +++ b/homeassistant/components/bring/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/bring", "integration_type": "service", "iot_class": "cloud_polling", - "requirements": ["bring-api==0.5.6"] + "requirements": ["bring-api==0.5.7"] } diff --git a/homeassistant/components/bring/todo.py b/homeassistant/components/bring/todo.py index 5d3fc5bbf68..a1988e667b5 100644 --- a/homeassistant/components/bring/todo.py +++ b/homeassistant/components/bring/todo.py @@ -1,4 +1,5 @@ """Todo platform for the Bring! integration.""" + from __future__ import annotations from typing import TYPE_CHECKING diff --git a/homeassistant/components/broadlink/__init__.py b/homeassistant/components/broadlink/__init__.py index e6a769fd2c4..8dd6cee82cb 100644 --- a/homeassistant/components/broadlink/__init__.py +++ b/homeassistant/components/broadlink/__init__.py @@ -1,4 +1,5 @@ """The Broadlink integration.""" + from __future__ import annotations from dataclasses import dataclass, field diff --git a/homeassistant/components/broadlink/climate.py b/homeassistant/components/broadlink/climate.py index be0eaf78f26..0573c342490 100644 --- a/homeassistant/components/broadlink/climate.py +++ b/homeassistant/components/broadlink/climate.py @@ -1,4 +1,5 @@ """Support for Broadlink climate devices.""" + from typing import Any from homeassistant.components.climate import ( diff --git a/homeassistant/components/broadlink/config_flow.py b/homeassistant/components/broadlink/config_flow.py index 531119db6df..89d540a27fc 100644 --- a/homeassistant/components/broadlink/config_flow.py +++ b/homeassistant/components/broadlink/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Broadlink devices.""" + from collections.abc import Mapping import errno from functools import partial @@ -14,10 +15,15 @@ from broadlink.exceptions import ( ) import voluptuous as vol -from homeassistant import config_entries from homeassistant.components import dhcp +from homeassistant.config_entries import ( + SOURCE_IMPORT, + SOURCE_REAUTH, + ConfigFlow, + ConfigFlowResult, +) from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONF_TIMEOUT, CONF_TYPE -from homeassistant.data_entry_flow import AbortFlow, FlowResult +from homeassistant.data_entry_flow import AbortFlow from homeassistant.helpers import config_validation as cv from .const import DEFAULT_PORT, DEFAULT_TIMEOUT, DEVICE_TYPES, DOMAIN @@ -26,7 +32,7 @@ from .helpers import format_mac _LOGGER = logging.getLogger(__name__) -class BroadlinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class BroadlinkFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a Broadlink config flow.""" VERSION = 1 @@ -58,7 +64,9 @@ class BroadlinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): "host": device.host[0], } - async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: + async def async_step_dhcp( + self, discovery_info: dhcp.DhcpServiceInfo + ) -> ConfigFlowResult: """Handle dhcp discovery.""" host = discovery_info.ip unique_id = discovery_info.macaddress.lower().replace(":", "") @@ -112,7 +120,7 @@ class BroadlinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): else: device.timeout = timeout - if self.source != config_entries.SOURCE_REAUTH: + if self.source != SOURCE_REAUTH: await self.async_set_device(device) self._abort_if_unique_id_configured( updates={CONF_HOST: device.host[0], CONF_TIMEOUT: timeout} @@ -131,7 +139,7 @@ class BroadlinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): _LOGGER.error("Failed to connect to the device at %s: %s", host, err_msg) - if self.source == config_entries.SOURCE_IMPORT: + if self.source == SOURCE_IMPORT: return self.async_abort(reason=errors["base"]) data_schema = { @@ -175,7 +183,7 @@ class BroadlinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): else: await self.async_set_unique_id(device.mac.hex()) - if self.source == config_entries.SOURCE_IMPORT: + if self.source == SOURCE_IMPORT: _LOGGER.warning( ( "%s (%s at %s) is ready to be configured. Click " @@ -305,7 +313,9 @@ class BroadlinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self._async_abort_entries_match({CONF_HOST: import_info[CONF_HOST]}) return await self.async_step_user(import_info) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Reauthenticate to the device.""" device = blk.gendevice( entry_data[CONF_TYPE], diff --git a/homeassistant/components/broadlink/const.py b/homeassistant/components/broadlink/const.py index 2b9e8787a43..91d4358a077 100644 --- a/homeassistant/components/broadlink/const.py +++ b/homeassistant/components/broadlink/const.py @@ -1,4 +1,5 @@ """Constants.""" + from homeassistant.const import Platform DOMAIN = "broadlink" diff --git a/homeassistant/components/broadlink/device.py b/homeassistant/components/broadlink/device.py index 69e1161a65c..8f5cf43ad7e 100644 --- a/homeassistant/components/broadlink/device.py +++ b/homeassistant/components/broadlink/device.py @@ -1,4 +1,5 @@ """Support for Broadlink devices.""" + from contextlib import suppress from functools import partial import logging diff --git a/homeassistant/components/broadlink/heartbeat.py b/homeassistant/components/broadlink/heartbeat.py index 70f6aec0d0f..7777e4da94e 100644 --- a/homeassistant/components/broadlink/heartbeat.py +++ b/homeassistant/components/broadlink/heartbeat.py @@ -1,4 +1,5 @@ """Heartbeats for Broadlink devices.""" + import datetime as dt import logging diff --git a/homeassistant/components/broadlink/helpers.py b/homeassistant/components/broadlink/helpers.py index bec61ba5bbd..43c2715531c 100644 --- a/homeassistant/components/broadlink/helpers.py +++ b/homeassistant/components/broadlink/helpers.py @@ -1,4 +1,5 @@ """Helper functions for the Broadlink integration.""" + from base64 import b64decode from homeassistant import config_entries diff --git a/homeassistant/components/broadlink/light.py b/homeassistant/components/broadlink/light.py index fde6d322bc6..39d6caaa49f 100644 --- a/homeassistant/components/broadlink/light.py +++ b/homeassistant/components/broadlink/light.py @@ -1,4 +1,5 @@ """Support for Broadlink lights.""" + import logging from typing import Any diff --git a/homeassistant/components/broadlink/remote.py b/homeassistant/components/broadlink/remote.py index c0fb80971ca..f8d903c51eb 100644 --- a/homeassistant/components/broadlink/remote.py +++ b/homeassistant/components/broadlink/remote.py @@ -1,4 +1,5 @@ """Support for Broadlink remotes.""" + import asyncio from base64 import b64encode from collections import defaultdict diff --git a/homeassistant/components/broadlink/sensor.py b/homeassistant/components/broadlink/sensor.py index 747418e1e79..b7ae71ff803 100644 --- a/homeassistant/components/broadlink/sensor.py +++ b/homeassistant/components/broadlink/sensor.py @@ -1,4 +1,5 @@ """Support for Broadlink sensors.""" + from __future__ import annotations from homeassistant.components.sensor import ( diff --git a/homeassistant/components/broadlink/switch.py b/homeassistant/components/broadlink/switch.py index b8744865898..f61e726b1d5 100644 --- a/homeassistant/components/broadlink/switch.py +++ b/homeassistant/components/broadlink/switch.py @@ -1,4 +1,5 @@ """Support for Broadlink switches.""" + from __future__ import annotations from abc import ABC, abstractmethod diff --git a/homeassistant/components/broadlink/updater.py b/homeassistant/components/broadlink/updater.py index 10ac4df4bb8..20b241b0d89 100644 --- a/homeassistant/components/broadlink/updater.py +++ b/homeassistant/components/broadlink/updater.py @@ -1,4 +1,5 @@ """Support for fetching data from Broadlink devices.""" + from abc import ABC, abstractmethod from datetime import timedelta import logging diff --git a/homeassistant/components/brother/__init__.py b/homeassistant/components/brother/__init__.py index 56d16ba7731..0bd49ed5d7a 100644 --- a/homeassistant/components/brother/__init__.py +++ b/homeassistant/components/brother/__init__.py @@ -1,4 +1,5 @@ """The Brother component.""" + from __future__ import annotations from asyncio import timeout diff --git a/homeassistant/components/brother/config_flow.py b/homeassistant/components/brother/config_flow.py index 3c60ccba5f0..ca2f1ae5a39 100644 --- a/homeassistant/components/brother/config_flow.py +++ b/homeassistant/components/brother/config_flow.py @@ -1,4 +1,5 @@ """Adds config flow for Brother Printer.""" + from __future__ import annotations from typing import Any @@ -6,10 +7,10 @@ from typing import Any from brother import Brother, SnmpError, UnsupportedModelError import voluptuous as vol -from homeassistant import config_entries, exceptions from homeassistant.components import zeroconf +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_TYPE -from homeassistant.data_entry_flow import FlowResult +from homeassistant.exceptions import HomeAssistantError from homeassistant.util.network import is_host_valid from .const import DOMAIN, PRINTER_TYPES @@ -23,7 +24,7 @@ DATA_SCHEMA = vol.Schema( ) -class BrotherConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class BrotherConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Brother Printer.""" VERSION = 1 @@ -35,14 +36,14 @@ class BrotherConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors = {} if user_input is not None: try: if not is_host_valid(user_input[CONF_HOST]): - raise InvalidHost() + raise InvalidHost snmp_engine = get_snmp_engine(self.hass) @@ -71,7 +72,7 @@ class BrotherConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_zeroconf( self, discovery_info: zeroconf.ZeroconfServiceInfo - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle zeroconf discovery.""" self.host = discovery_info.host @@ -107,7 +108,7 @@ class BrotherConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_zeroconf_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initiated by zeroconf.""" if user_input is not None: title = f"{self.brother.model} {self.brother.serial}" @@ -127,5 +128,5 @@ class BrotherConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -class InvalidHost(exceptions.HomeAssistantError): +class InvalidHost(HomeAssistantError): """Error to indicate that hostname/IP address is invalid.""" diff --git a/homeassistant/components/brother/const.py b/homeassistant/components/brother/const.py index 21f535ec1e4..fda815ceee5 100644 --- a/homeassistant/components/brother/const.py +++ b/homeassistant/components/brother/const.py @@ -1,4 +1,5 @@ """Constants for Brother integration.""" + from __future__ import annotations from typing import Final diff --git a/homeassistant/components/brother/diagnostics.py b/homeassistant/components/brother/diagnostics.py index 4733431f8e2..a4afb385f8d 100644 --- a/homeassistant/components/brother/diagnostics.py +++ b/homeassistant/components/brother/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for Brother.""" + from __future__ import annotations from dataclasses import asdict diff --git a/homeassistant/components/brother/sensor.py b/homeassistant/components/brother/sensor.py index 198fe621246..6f56eb680be 100644 --- a/homeassistant/components/brother/sensor.py +++ b/homeassistant/components/brother/sensor.py @@ -1,4 +1,5 @@ """Support for the Brother service.""" + from __future__ import annotations from collections.abc import Callable @@ -35,20 +36,13 @@ UNIT_PAGES = "p" _LOGGER = logging.getLogger(__name__) -@dataclass(frozen=True) -class BrotherSensorRequiredKeysMixin: - """Class for Brother entity required keys.""" +@dataclass(frozen=True, kw_only=True) +class BrotherSensorEntityDescription(SensorEntityDescription): + """A class that describes sensor entities.""" value: Callable[[BrotherSensors], StateType | datetime] -@dataclass(frozen=True) -class BrotherSensorEntityDescription( - SensorEntityDescription, BrotherSensorRequiredKeysMixin -): - """A class that describes sensor entities.""" - - SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( BrotherSensorEntityDescription( key="status", @@ -345,12 +339,11 @@ async def async_setup_entry( ) entity_registry.async_update_entity(entity_id, new_unique_id=new_unique_id) - sensors = [] - - for description in SENSOR_TYPES: - if description.value(coordinator.data) is not None: - sensors.append(BrotherPrinterSensor(coordinator, description)) - async_add_entities(sensors, False) + async_add_entities( + BrotherPrinterSensor(coordinator, description) + for description in SENSOR_TYPES + if description.value(coordinator.data) is not None + ) class BrotherPrinterSensor( diff --git a/homeassistant/components/brother/utils.py b/homeassistant/components/brother/utils.py index 47b7ae31a67..d7636cdd2e8 100644 --- a/homeassistant/components/brother/utils.py +++ b/homeassistant/components/brother/utils.py @@ -1,4 +1,5 @@ """Brother helpers functions.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/brottsplatskartan/__init__.py b/homeassistant/components/brottsplatskartan/__init__.py index 14e6e383e85..486bee5bcd5 100644 --- a/homeassistant/components/brottsplatskartan/__init__.py +++ b/homeassistant/components/brottsplatskartan/__init__.py @@ -1,4 +1,5 @@ """The brottsplatskartan component.""" + from __future__ import annotations from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/brottsplatskartan/config_flow.py b/homeassistant/components/brottsplatskartan/config_flow.py index 39c7421fa92..ef35b3bd4f1 100644 --- a/homeassistant/components/brottsplatskartan/config_flow.py +++ b/homeassistant/components/brottsplatskartan/config_flow.py @@ -1,4 +1,5 @@ """Adds config flow for Brottsplatskartan integration.""" + from __future__ import annotations from typing import Any @@ -7,9 +8,8 @@ import uuid from brottsplatskartan import AREAS import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_LATITUDE, CONF_LOCATION, CONF_LONGITUDE -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import selector from .const import CONF_APP_ID, CONF_AREA, DEFAULT_NAME, DOMAIN @@ -29,14 +29,14 @@ DATA_SCHEMA = vol.Schema( ) -class BPKConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class BPKConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Brottsplatskartan integration.""" VERSION = 1 async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the user step.""" errors: dict[str, str] = {} diff --git a/homeassistant/components/brottsplatskartan/sensor.py b/homeassistant/components/brottsplatskartan/sensor.py index b30b31be985..6725a32bb40 100644 --- a/homeassistant/components/brottsplatskartan/sensor.py +++ b/homeassistant/components/brottsplatskartan/sensor.py @@ -1,4 +1,5 @@ """Sensor platform for Brottsplatskartan information.""" + from __future__ import annotations from collections import defaultdict @@ -62,9 +63,9 @@ class BrottsplatskartanSensor(SensorEntity): """Update device state.""" incident_counts: defaultdict[str, int] = defaultdict(int) - get_incidents: dict[str, list] | Literal[ - False - ] = self._brottsplatskartan.get_incidents() + get_incidents: dict[str, list] | Literal[False] = ( + self._brottsplatskartan.get_incidents() + ) if get_incidents is False: LOGGER.debug("Problems fetching incidents") diff --git a/homeassistant/components/browser/__init__.py b/homeassistant/components/browser/__init__.py index 9dc3e1fe66a..7f562cd3bed 100644 --- a/homeassistant/components/browser/__init__.py +++ b/homeassistant/components/browser/__init__.py @@ -1,4 +1,5 @@ """Support for launching a web browser on the host machine.""" + import webbrowser import voluptuous as vol diff --git a/homeassistant/components/browser/icons.json b/homeassistant/components/browser/icons.json new file mode 100644 index 00000000000..7c971009fd7 --- /dev/null +++ b/homeassistant/components/browser/icons.json @@ -0,0 +1,5 @@ +{ + "services": { + "browse_url": "mdi:web" + } +} diff --git a/homeassistant/components/brunt/__init__.py b/homeassistant/components/brunt/__init__.py index 660c43f1004..bec281d1902 100644 --- a/homeassistant/components/brunt/__init__.py +++ b/homeassistant/components/brunt/__init__.py @@ -1,4 +1,5 @@ """The brunt component.""" + from __future__ import annotations from asyncio import timeout @@ -50,7 +51,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise UpdateFailed(f"Error communicating with API: {err}") from err except ClientResponseError as err: if err.status == 403: - raise ConfigEntryAuthFailed() from err + raise ConfigEntryAuthFailed from err if err.status == 401: _LOGGER.warning("Device not found, will reload Brunt integration") await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/brunt/config_flow.py b/homeassistant/components/brunt/config_flow.py index cfd3bfa69cb..789a5a48bd9 100644 --- a/homeassistant/components/brunt/config_flow.py +++ b/homeassistant/components/brunt/config_flow.py @@ -1,4 +1,5 @@ """Config flow for brunt integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -10,9 +11,8 @@ from aiohttp.client_exceptions import ServerDisconnectedError from brunt import BruntClientAsync import voluptuous as vol -from homeassistant.config_entries import ConfigEntry, ConfigFlow +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN @@ -60,7 +60,7 @@ class BruntConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" if user_input is None: return self.async_show_form(step_id="user", data_schema=DATA_SCHEMA) @@ -78,7 +78,9 @@ class BruntConfigFlow(ConfigFlow, domain=DOMAIN): data=user_input, ) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Perform reauth upon an API authentication error.""" self._reauth_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] @@ -87,7 +89,7 @@ class BruntConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Dialog that informs the user that reauth is required.""" assert self._reauth_entry username = self._reauth_entry.data[CONF_USERNAME] diff --git a/homeassistant/components/brunt/const.py b/homeassistant/components/brunt/const.py index cc85ac9a415..4c246d28d64 100644 --- a/homeassistant/components/brunt/const.py +++ b/homeassistant/components/brunt/const.py @@ -1,4 +1,5 @@ """Constants for Brunt.""" + from datetime import timedelta from homeassistant.const import Platform diff --git a/homeassistant/components/brunt/cover.py b/homeassistant/components/brunt/cover.py index 1bde667a237..519885fe542 100644 --- a/homeassistant/components/brunt/cover.py +++ b/homeassistant/components/brunt/cover.py @@ -1,4 +1,5 @@ """Support for Brunt Blind Engine covers.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/bsblan/__init__.py b/homeassistant/components/bsblan/__init__.py index def2cfaf56a..9a471329ba9 100644 --- a/homeassistant/components/bsblan/__init__.py +++ b/homeassistant/components/bsblan/__init__.py @@ -1,4 +1,5 @@ """The BSB-Lan integration.""" + import dataclasses from bsblan import BSBLAN, Device, Info, StaticState diff --git a/homeassistant/components/bsblan/climate.py b/homeassistant/components/bsblan/climate.py index 1be595bf1cc..1b300e1e738 100644 --- a/homeassistant/components/bsblan/climate.py +++ b/homeassistant/components/bsblan/climate.py @@ -1,4 +1,5 @@ """BSBLAN platform to control a compatible Climate Device.""" + from __future__ import annotations from typing import Any @@ -149,7 +150,6 @@ class BSBLANClimate( await self.async_set_data(preset_mode=preset_mode) else: raise ServiceValidationError( - "Can't set preset mode when hvac mode is not auto", translation_domain=DOMAIN, translation_key="set_preset_mode_error", translation_placeholders={"preset_mode": preset_mode}, diff --git a/homeassistant/components/bsblan/config_flow.py b/homeassistant/components/bsblan/config_flow.py index e12e6e5c6cf..9732f0a77a9 100644 --- a/homeassistant/components/bsblan/config_flow.py +++ b/homeassistant/components/bsblan/config_flow.py @@ -1,4 +1,5 @@ """Config flow for BSB-Lan integration.""" + from __future__ import annotations from typing import Any @@ -6,10 +7,9 @@ from typing import Any from bsblan import BSBLAN, BSBLANError import voluptuous as vol -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.device_registry import format_mac @@ -30,7 +30,7 @@ class BSBLANFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initiated by the user.""" if user_input is None: return self._show_setup_form() @@ -49,7 +49,7 @@ class BSBLANFlowHandler(ConfigFlow, domain=DOMAIN): return self._async_create_entry() @callback - def _show_setup_form(self, errors: dict | None = None) -> FlowResult: + def _show_setup_form(self, errors: dict | None = None) -> ConfigFlowResult: """Show the setup form to the user.""" return self.async_show_form( step_id="user", @@ -66,7 +66,7 @@ class BSBLANFlowHandler(ConfigFlow, domain=DOMAIN): ) @callback - def _async_create_entry(self) -> FlowResult: + def _async_create_entry(self) -> ConfigFlowResult: return self.async_create_entry( title=format_mac(self.mac), data={ diff --git a/homeassistant/components/bsblan/const.py b/homeassistant/components/bsblan/const.py index 0de9a29a27b..5bca20cb4d4 100644 --- a/homeassistant/components/bsblan/const.py +++ b/homeassistant/components/bsblan/const.py @@ -1,4 +1,5 @@ """Constants for the BSB-Lan integration.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/bsblan/coordinator.py b/homeassistant/components/bsblan/coordinator.py index 15eff37e6db..864daacc562 100644 --- a/homeassistant/components/bsblan/coordinator.py +++ b/homeassistant/components/bsblan/coordinator.py @@ -1,4 +1,5 @@ """DataUpdateCoordinator for the BSB-Lan integration.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/bsblan/diagnostics.py b/homeassistant/components/bsblan/diagnostics.py index 91d959ea0e2..0bceed0bf23 100644 --- a/homeassistant/components/bsblan/diagnostics.py +++ b/homeassistant/components/bsblan/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for BSBLan.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/bsblan/entity.py b/homeassistant/components/bsblan/entity.py index 3c7f41ce34d..a69c4d2217e 100644 --- a/homeassistant/components/bsblan/entity.py +++ b/homeassistant/components/bsblan/entity.py @@ -1,4 +1,5 @@ """Base entity for the BSBLAN integration.""" + from __future__ import annotations from bsblan import BSBLAN, Device, Info, StaticState diff --git a/homeassistant/components/bt_home_hub_5/device_tracker.py b/homeassistant/components/bt_home_hub_5/device_tracker.py index 0ffa3bc699b..8706a04e7ad 100644 --- a/homeassistant/components/bt_home_hub_5/device_tracker.py +++ b/homeassistant/components/bt_home_hub_5/device_tracker.py @@ -1,4 +1,5 @@ """Support for BT Home Hub 5.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/bt_smarthub/device_tracker.py b/homeassistant/components/bt_smarthub/device_tracker.py index 65aa1bd6a61..8b5411e2014 100644 --- a/homeassistant/components/bt_smarthub/device_tracker.py +++ b/homeassistant/components/bt_smarthub/device_tracker.py @@ -1,4 +1,5 @@ """Support for BT Smart Hub (Sometimes referred to as BT Home Hub 6).""" + from __future__ import annotations from collections import namedtuple diff --git a/homeassistant/components/bthome/__init__.py b/homeassistant/components/bthome/__init__.py index 0031f09bb81..dab7a7db158 100644 --- a/homeassistant/components/bthome/__init__.py +++ b/homeassistant/components/bthome/__init__.py @@ -1,8 +1,8 @@ """The BTHome Bluetooth integration.""" + from __future__ import annotations import logging -from typing import cast from bthome_ble import BTHomeBluetoothDeviceData, SensorUpdate from bthome_ble.parser import EncryptionScheme @@ -21,6 +21,7 @@ from homeassistant.helpers.device_registry import ( async_get, ) from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.util.signal_type import SignalType from .const import ( BTHOME_BLE_EVENT, @@ -93,7 +94,7 @@ def process_service_info( hass, format_discovered_event_class(address), event_class, ble_event ) - hass.bus.async_fire(BTHOME_BLE_EVENT, cast(dict, ble_event)) + hass.bus.async_fire(BTHOME_BLE_EVENT, ble_event) async_dispatcher_send( hass, format_event_dispatcher_name(address, event_class), @@ -107,14 +108,16 @@ def process_service_info( return update -def format_event_dispatcher_name(address: str, event_class: str) -> str: +def format_event_dispatcher_name( + address: str, event_class: str +) -> SignalType[BTHomeBleEvent]: """Format an event dispatcher name.""" - return f"{DOMAIN}_event_{address}_{event_class}" + return SignalType(f"{DOMAIN}_event_{address}_{event_class}") -def format_discovered_event_class(address: str) -> str: +def format_discovered_event_class(address: str) -> SignalType[str, BTHomeBleEvent]: """Format a discovered event class.""" - return f"{DOMAIN}_discovered_event_class_{address}" + return SignalType(f"{DOMAIN}_discovered_event_class_{address}") async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -128,20 +131,22 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: data = BTHomeBluetoothDeviceData(**kwargs) device_registry = async_get(hass) - coordinator = hass.data.setdefault(DOMAIN, {})[ - entry.entry_id - ] = BTHomePassiveBluetoothProcessorCoordinator( - hass, - _LOGGER, - address=address, - mode=BluetoothScanningMode.PASSIVE, - update_method=lambda service_info: process_service_info( - hass, entry, data, service_info, device_registry - ), - device_data=data, - discovered_event_classes=set(entry.data.get(CONF_DISCOVERED_EVENT_CLASSES, [])), - connectable=False, - entry=entry, + coordinator = hass.data.setdefault(DOMAIN, {})[entry.entry_id] = ( + BTHomePassiveBluetoothProcessorCoordinator( + hass, + _LOGGER, + address=address, + mode=BluetoothScanningMode.PASSIVE, + update_method=lambda service_info: process_service_info( + hass, entry, data, service_info, device_registry + ), + device_data=data, + discovered_event_classes=set( + entry.data.get(CONF_DISCOVERED_EVENT_CLASSES, []) + ), + connectable=False, + entry=entry, + ) ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/homeassistant/components/bthome/binary_sensor.py b/homeassistant/components/bthome/binary_sensor.py index 02a226d1f7c..6de9506c54b 100644 --- a/homeassistant/components/bthome/binary_sensor.py +++ b/homeassistant/components/bthome/binary_sensor.py @@ -1,4 +1,5 @@ """Support for BTHome binary sensors.""" + from __future__ import annotations from bthome_ble import ( diff --git a/homeassistant/components/bthome/config_flow.py b/homeassistant/components/bthome/config_flow.py index 62dc8cfa99f..5a3d90f1355 100644 --- a/homeassistant/components/bthome/config_flow.py +++ b/homeassistant/components/bthome/config_flow.py @@ -15,9 +15,8 @@ from homeassistant.components.bluetooth import ( BluetoothServiceInfoBleak, async_discovered_service_info, ) -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_ADDRESS -from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN @@ -48,7 +47,7 @@ class BTHomeConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_bluetooth( self, discovery_info: BluetoothServiceInfoBleak - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the bluetooth discovery step.""" await self.async_set_unique_id(discovery_info.address) self._abort_if_unique_id_configured() @@ -68,7 +67,7 @@ class BTHomeConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_get_encryption_key( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Enter a bindkey for an encrypted BTHome device.""" assert self._discovery_info assert self._discovered_device @@ -102,7 +101,7 @@ class BTHomeConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_bluetooth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm discovery.""" if user_input is not None or not onboarding.async_is_onboarded(self.hass): return self._async_get_or_create_entry() @@ -115,7 +114,7 @@ class BTHomeConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the user step to pick discovered device.""" if user_input is not None: address = user_input[CONF_ADDRESS] @@ -158,7 +157,9 @@ class BTHomeConfigFlow(ConfigFlow, domain=DOMAIN): data_schema=vol.Schema({vol.Required(CONF_ADDRESS): vol.In(titles)}), ) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """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 @@ -174,7 +175,9 @@ class BTHomeConfigFlow(ConfigFlow, domain=DOMAIN): # Otherwise there wasn't actually encryption so abort return self.async_abort(reason="reauth_successful") - def _async_get_or_create_entry(self, bindkey: str | None = None) -> FlowResult: + def _async_get_or_create_entry( + self, bindkey: str | None = None + ) -> ConfigFlowResult: data: dict[str, Any] = {} if bindkey: data["bindkey"] = bindkey diff --git a/homeassistant/components/bthome/const.py b/homeassistant/components/bthome/const.py index 780833bf92e..3e7deac9303 100644 --- a/homeassistant/components/bthome/const.py +++ b/homeassistant/components/bthome/const.py @@ -1,4 +1,5 @@ """Constants for the BTHome Bluetooth integration.""" + from __future__ import annotations from typing import Final, TypedDict diff --git a/homeassistant/components/bthome/coordinator.py b/homeassistant/components/bthome/coordinator.py index 837ad58b7c2..0abbf20d655 100644 --- a/homeassistant/components/bthome/coordinator.py +++ b/homeassistant/components/bthome/coordinator.py @@ -1,4 +1,5 @@ """The BTHome Bluetooth integration.""" + from collections.abc import Callable from logging import Logger from typing import Any diff --git a/homeassistant/components/bthome/device.py b/homeassistant/components/bthome/device.py index eecd8161d6c..1afe558db42 100644 --- a/homeassistant/components/bthome/device.py +++ b/homeassistant/components/bthome/device.py @@ -1,4 +1,5 @@ """Support for BTHome Bluetooth devices.""" + from __future__ import annotations from bthome_ble import DeviceKey diff --git a/homeassistant/components/bthome/device_trigger.py b/homeassistant/components/bthome/device_trigger.py index 834b08ad39d..c49664b1146 100644 --- a/homeassistant/components/bthome/device_trigger.py +++ b/homeassistant/components/bthome/device_trigger.py @@ -1,4 +1,5 @@ """Provides device triggers for BTHome BLE.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/bthome/event.py b/homeassistant/components/bthome/event.py index 39ad66d1d13..a0f59c0ddb7 100644 --- a/homeassistant/components/bthome/event.py +++ b/homeassistant/components/bthome/event.py @@ -1,4 +1,5 @@ """Support for bthome event entities.""" + from __future__ import annotations from dataclasses import replace diff --git a/homeassistant/components/bthome/logbook.py b/homeassistant/components/bthome/logbook.py index 158253ec8a7..23976e368ad 100644 --- a/homeassistant/components/bthome/logbook.py +++ b/homeassistant/components/bthome/logbook.py @@ -1,12 +1,12 @@ """Describe bthome logbook events.""" + from __future__ import annotations from collections.abc import Callable from homeassistant.components.logbook import LOGBOOK_ENTRY_MESSAGE, LOGBOOK_ENTRY_NAME -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import Event, HomeAssistant, callback from homeassistant.helpers.device_registry import async_get -from homeassistant.helpers.typing import EventType from .const import BTHOME_BLE_EVENT, DOMAIN, BTHomeBleEvent @@ -15,14 +15,14 @@ from .const import BTHOME_BLE_EVENT, DOMAIN, BTHomeBleEvent def async_describe_events( hass: HomeAssistant, async_describe_event: Callable[ - [str, str, Callable[[EventType[BTHomeBleEvent]], dict[str, str]]], None + [str, str, Callable[[Event[BTHomeBleEvent]], dict[str, str]]], None ], ) -> None: """Describe logbook events.""" dr = async_get(hass) @callback - def async_describe_bthome_event(event: EventType[BTHomeBleEvent]) -> dict[str, str]: + def async_describe_bthome_event(event: Event[BTHomeBleEvent]) -> dict[str, str]: """Describe bthome logbook event.""" data = event.data device = dr.async_get(data["device_id"]) diff --git a/homeassistant/components/bthome/manifest.json b/homeassistant/components/bthome/manifest.json index 5fb90bb5998..7c90c6f3bbc 100644 --- a/homeassistant/components/bthome/manifest.json +++ b/homeassistant/components/bthome/manifest.json @@ -20,5 +20,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/bthome", "iot_class": "local_push", - "requirements": ["bthome-ble==3.8.0"] + "requirements": ["bthome-ble==3.8.1"] } diff --git a/homeassistant/components/bthome/sensor.py b/homeassistant/components/bthome/sensor.py index 17f8f6c7a3c..179979707b2 100644 --- a/homeassistant/components/bthome/sensor.py +++ b/homeassistant/components/bthome/sensor.py @@ -1,4 +1,5 @@ """Support for BTHome sensors.""" + from __future__ import annotations from bthome_ble import SensorDeviceClass as BTHomeSensorDeviceClass, SensorUpdate, Units @@ -333,6 +334,7 @@ SENSOR_DESCRIPTIONS = { Units.VOLUME_FLOW_RATE_CUBIC_METERS_PER_HOUR, ): SensorEntityDescription( key=f"{BTHomeSensorDeviceClass.VOLUME_FLOW_RATE}_{Units.VOLUME_FLOW_RATE_CUBIC_METERS_PER_HOUR}", + device_class=SensorDeviceClass.VOLUME_FLOW_RATE, native_unit_of_measurement=UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR, state_class=SensorStateClass.MEASUREMENT, ), diff --git a/homeassistant/components/buienradar/__init__.py b/homeassistant/components/buienradar/__init__.py index e259dbac692..3bf593b2dab 100644 --- a/homeassistant/components/buienradar/__init__.py +++ b/homeassistant/components/buienradar/__init__.py @@ -1,4 +1,5 @@ """The buienradar integration.""" + from __future__ import annotations from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/buienradar/camera.py b/homeassistant/components/buienradar/camera.py index ba62cbfbb19..72bf6b7a3eb 100644 --- a/homeassistant/components/buienradar/camera.py +++ b/homeassistant/components/buienradar/camera.py @@ -1,4 +1,5 @@ """Provide animated GIF loops of Buienradar imagery.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/buienradar/config_flow.py b/homeassistant/components/buienradar/config_flow.py index 1e77693f7fb..45ad9028eb0 100644 --- a/homeassistant/components/buienradar/config_flow.py +++ b/homeassistant/components/buienradar/config_flow.py @@ -1,4 +1,5 @@ """Config flow for buienradar integration.""" + from __future__ import annotations import copy @@ -6,11 +7,9 @@ from typing import Any, cast import voluptuous as vol -from homeassistant import config_entries -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_COUNTRY_CODE, CONF_LATITUDE, CONF_LONGITUDE from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import selector import homeassistant.helpers.config_validation as cv from homeassistant.helpers.schema_config_entry_flow import ( @@ -73,7 +72,7 @@ OPTIONS_FLOW = { } -class BuienradarFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class BuienradarFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a config flow for buienradar.""" VERSION = 1 @@ -88,7 +87,7 @@ class BuienradarFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" if user_input is not None: lat = user_input.get(CONF_LATITUDE) diff --git a/homeassistant/components/buienradar/sensor.py b/homeassistant/components/buienradar/sensor.py index fe3ce3164fe..fb15aa49001 100644 --- a/homeassistant/components/buienradar/sensor.py +++ b/homeassistant/components/buienradar/sensor.py @@ -1,4 +1,5 @@ """Support for Buienradar.nl weather service.""" + from __future__ import annotations import logging @@ -741,8 +742,9 @@ class BrSensor(SensorEntity): """Initialize the sensor.""" self.entity_description = description self._measured = None - self._attr_unique_id = "{:2.6f}{:2.6f}{}".format( - coordinates[CONF_LATITUDE], coordinates[CONF_LONGITUDE], description.key + self._attr_unique_id = ( + f"{coordinates[CONF_LATITUDE]:2.6f}{coordinates[CONF_LONGITUDE]:2.6f}" + f"{description.key}" ) # All continuous sensors should be forced to be updated @@ -772,13 +774,7 @@ class BrSensor(SensorEntity): self._measured = data.get(MEASURED) sensor_type = self.entity_description.key - if ( - sensor_type.endswith("_1d") - or sensor_type.endswith("_2d") - or sensor_type.endswith("_3d") - or sensor_type.endswith("_4d") - or sensor_type.endswith("_5d") - ): + if sensor_type.endswith(("_1d", "_2d", "_3d", "_4d", "_5d")): # update forecasting sensors: fcday = 0 if sensor_type.endswith("_2d"): @@ -791,7 +787,7 @@ class BrSensor(SensorEntity): fcday = 4 # update weather symbol & status text - if sensor_type.startswith(SYMBOL) or sensor_type.startswith(CONDITION): + if sensor_type.startswith((SYMBOL, CONDITION)): try: condition = data.get(FORECAST)[fcday].get(CONDITION) except IndexError: diff --git a/homeassistant/components/buienradar/util.py b/homeassistant/components/buienradar/util.py index 426f982bafc..b641644cebe 100644 --- a/homeassistant/components/buienradar/util.py +++ b/homeassistant/components/buienradar/util.py @@ -1,4 +1,5 @@ """Shared utilities for different supported platforms.""" + from asyncio import timeout from datetime import datetime, timedelta from http import HTTPStatus diff --git a/homeassistant/components/buienradar/weather.py b/homeassistant/components/buienradar/weather.py index de00faadd64..02e1f444c9c 100644 --- a/homeassistant/components/buienradar/weather.py +++ b/homeassistant/components/buienradar/weather.py @@ -1,4 +1,5 @@ """Support for Buienradar.nl weather service.""" + import logging from buienradar.constants import ( @@ -134,16 +135,17 @@ class BrWeather(WeatherEntity): self._stationname = config.get(CONF_NAME, "Buienradar") self._attr_name = self._stationname or f"BR {'(unknown station)'}" - self._attr_unique_id = "{:2.6f}{:2.6f}".format( - coordinates[CONF_LATITUDE], coordinates[CONF_LONGITUDE] + self._attr_unique_id = ( + f"{coordinates[CONF_LATITUDE]:2.6f}{coordinates[CONF_LONGITUDE]:2.6f}" ) + self._forecast: list | None = None @callback def data_updated(self, data: BrData) -> None: """Update data.""" self._attr_attribution = data.attribution self._attr_condition = self._calc_condition(data) - self._attr_forecast = self._calc_forecast(data) + self._forecast = self._calc_forecast(data) self._attr_humidity = data.humidity self._attr_name = ( self._stationname or f"BR {data.stationname or '(unknown station)'}" @@ -195,4 +197,4 @@ class BrWeather(WeatherEntity): async def async_forecast_daily(self) -> list[Forecast] | None: """Return the daily forecast in native units.""" - return self._attr_forecast + return self._forecast diff --git a/homeassistant/components/button/__init__.py b/homeassistant/components/button/__init__.py index 0acc5b63339..10589aa461f 100644 --- a/homeassistant/components/button/__init__.py +++ b/homeassistant/components/button/__init__.py @@ -1,4 +1,5 @@ """Component to pressing a button as platforms.""" + from __future__ import annotations from datetime import timedelta @@ -122,10 +123,8 @@ class ButtonEntity(RestoreEntity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_ def __set_state(self, state: str | None) -> None: """Set the entity state.""" - try: # noqa: SIM105 suppress is much slower - del self.state - except AttributeError: - pass + # Invalidate the cache of the cached property + self.__dict__.pop("state", None) self.__last_pressed_isoformat = state @final @@ -147,7 +146,7 @@ class ButtonEntity(RestoreEntity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_ def press(self) -> None: """Press the button.""" - raise NotImplementedError() + raise NotImplementedError async def async_press(self) -> None: """Press the button.""" diff --git a/homeassistant/components/button/device_action.py b/homeassistant/components/button/device_action.py index 338b11e765b..f4db7b619f8 100644 --- a/homeassistant/components/button/device_action.py +++ b/homeassistant/components/button/device_action.py @@ -1,4 +1,5 @@ """Provides device actions for Button.""" + from __future__ import annotations import voluptuous as vol diff --git a/homeassistant/components/button/device_trigger.py b/homeassistant/components/button/device_trigger.py index 1b206337f33..f1028a0ca6a 100644 --- a/homeassistant/components/button/device_trigger.py +++ b/homeassistant/components/button/device_trigger.py @@ -1,4 +1,5 @@ """Provides device triggers for Button.""" + from __future__ import annotations import voluptuous as vol diff --git a/homeassistant/components/caldav/api.py b/homeassistant/components/caldav/api.py index 3b524e29370..a48336331ca 100644 --- a/homeassistant/components/caldav/api.py +++ b/homeassistant/components/caldav/api.py @@ -1,6 +1,5 @@ """Library for working with CalDAV api.""" - import caldav from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/caldav/calendar.py b/homeassistant/components/caldav/calendar.py index b2114dfc829..b9f967d1a08 100644 --- a/homeassistant/components/caldav/calendar.py +++ b/homeassistant/components/caldav/calendar.py @@ -1,4 +1,5 @@ """Support for WebDav Calendar.""" + from __future__ import annotations from datetime import datetime diff --git a/homeassistant/components/caldav/config_flow.py b/homeassistant/components/caldav/config_flow.py index f2fa51c7f60..3710f7f1b4b 100644 --- a/homeassistant/components/caldav/config_flow.py +++ b/homeassistant/components/caldav/config_flow.py @@ -9,9 +9,8 @@ from caldav.lib.error import AuthorizationError, DAVError import requests import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME, CONF_VERIFY_SSL -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_validation as cv from .const import DOMAIN @@ -29,15 +28,15 @@ STEP_USER_DATA_SCHEMA = vol.Schema( ) -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class CalDavConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for caldav.""" VERSION = 1 - _reauth_entry: config_entries.ConfigEntry | None = None + _reauth_entry: ConfigEntry | None = None async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors: dict[str, str] = {} if user_input is not None: @@ -88,7 +87,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return "unknown" return None - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Perform reauth upon an API authentication error.""" self._reauth_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] @@ -97,7 +98,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_reauth_confirm( self, user_input: dict[str, str] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm reauth dialog.""" errors = {} assert self._reauth_entry diff --git a/homeassistant/components/caldav/todo.py b/homeassistant/components/caldav/todo.py index 90380805c31..e8cd4fc9334 100644 --- a/homeassistant/components/caldav/todo.py +++ b/homeassistant/components/caldav/todo.py @@ -1,4 +1,5 @@ """CalDAV todo platform.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/calendar/__init__.py b/homeassistant/components/calendar/__init__.py index 670d448a430..47ea10b71b6 100644 --- a/homeassistant/components/calendar/__init__.py +++ b/homeassistant/components/calendar/__init__.py @@ -1,4 +1,5 @@ """Support for Calendar event device sensors.""" + from __future__ import annotations from collections.abc import Callable, Iterable @@ -502,7 +503,7 @@ class CalendarEntity(Entity): @property def event(self) -> CalendarEvent | None: """Return the next upcoming event.""" - raise NotImplementedError() + raise NotImplementedError @final @property @@ -598,11 +599,11 @@ class CalendarEntity(Entity): end_date: datetime.datetime, ) -> list[CalendarEvent]: """Return calendar events within a datetime range.""" - raise NotImplementedError() + raise NotImplementedError async def async_create_event(self, **kwargs: Any) -> None: """Add a new event to calendar.""" - raise NotImplementedError() + raise NotImplementedError async def async_delete_event( self, @@ -611,7 +612,7 @@ class CalendarEntity(Entity): recurrence_range: str | None = None, ) -> None: """Delete an event on the calendar.""" - raise NotImplementedError() + raise NotImplementedError async def async_update_event( self, @@ -621,7 +622,7 @@ class CalendarEntity(Entity): recurrence_range: str | None = None, ) -> None: """Delete an event on the calendar.""" - raise NotImplementedError() + raise NotImplementedError class CalendarEventView(http.HomeAssistantView): @@ -657,7 +658,7 @@ class CalendarEventView(http.HomeAssistantView): try: calendar_event_list = await entity.async_get_events( - request.app["hass"], + request.app[http.KEY_HASS], dt_util.as_local(start_date), dt_util.as_local(end_date), ) @@ -687,11 +688,12 @@ class CalendarListView(http.HomeAssistantView): async def get(self, request: web.Request) -> web.Response: """Retrieve calendar list.""" - hass = request.app["hass"] + hass = request.app[http.KEY_HASS] calendar_list: list[dict[str, str]] = [] for entity in self.component.entities: state = hass.states.get(entity.entity_id) + assert state calendar_list.append({"name": state.name, "entity_id": entity.entity_id}) return self.json(sorted(calendar_list, key=lambda x: cast(str, x["name"]))) diff --git a/homeassistant/components/calendar/trigger.py b/homeassistant/components/calendar/trigger.py index e4fe5d22efd..844232c4b22 100644 --- a/homeassistant/components/calendar/trigger.py +++ b/homeassistant/components/calendar/trigger.py @@ -1,4 +1,5 @@ """Offer calendar automation rules.""" + from __future__ import annotations from collections.abc import Awaitable, Callable, Coroutine diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index ff4687dd493..bfeab601352 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -1,4 +1,5 @@ """Component to interface with cameras.""" + from __future__ import annotations import asyncio @@ -416,7 +417,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: stream.add_provider("hls") await stream.start() - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, preload_stream) + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STARTED, preload_stream, run_immediately=True + ) @callback def update_tokens(t: datetime) -> None: @@ -434,7 +437,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Unsubscribe track time interval timer.""" unsub() - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, unsub_track_time_interval) + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, unsub_track_time_interval, run_immediately=True + ) component.async_register_entity_service( SERVICE_ENABLE_MOTION, {}, "async_enable_motion_detection" @@ -650,7 +655,7 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): self, width: int | None = None, height: int | None = None ) -> bytes | None: """Return bytes of camera image.""" - raise NotImplementedError() + raise NotImplementedError async def async_camera_image( self, width: int | None = None, height: int | None = None @@ -695,7 +700,7 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): def turn_off(self) -> None: """Turn off camera.""" - raise NotImplementedError() + raise NotImplementedError async def async_turn_off(self) -> None: """Turn off camera.""" @@ -703,7 +708,7 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): def turn_on(self) -> None: """Turn off camera.""" - raise NotImplementedError() + raise NotImplementedError async def async_turn_on(self) -> None: """Turn off camera.""" @@ -711,7 +716,7 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): def enable_motion_detection(self) -> None: """Enable motion detection in the camera.""" - raise NotImplementedError() + raise NotImplementedError async def async_enable_motion_detection(self) -> None: """Call the job and enable motion detection.""" @@ -719,7 +724,7 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): def disable_motion_detection(self) -> None: """Disable motion detection in camera.""" - raise NotImplementedError() + raise NotImplementedError async def async_disable_motion_detection(self) -> None: """Call the job and disable motion detection.""" @@ -796,7 +801,7 @@ class CameraView(HomeAssistantView): async def get(self, request: web.Request, entity_id: str) -> web.StreamResponse: """Start a GET request.""" if (camera := self.component.get_entity(entity_id)) is None: - raise web.HTTPNotFound() + raise web.HTTPNotFound authenticated = ( request[KEY_AUTHENTICATED] @@ -807,19 +812,19 @@ class CameraView(HomeAssistantView): # Attempt with invalid bearer token, raise unauthorized # so ban middleware can handle it. if hdrs.AUTHORIZATION in request.headers: - raise web.HTTPUnauthorized() + raise web.HTTPUnauthorized # Invalid sigAuth or camera access token - raise web.HTTPForbidden() + raise web.HTTPForbidden if not camera.is_on: _LOGGER.debug("Camera is off") - raise web.HTTPServiceUnavailable() + raise web.HTTPServiceUnavailable return await self.handle(request, camera) async def handle(self, request: web.Request, camera: Camera) -> web.StreamResponse: """Handle the camera request.""" - raise NotImplementedError() + raise NotImplementedError class CameraImageView(CameraView): @@ -840,7 +845,7 @@ class CameraImageView(CameraView): int(height) if height else None, ) except (HomeAssistantError, ValueError) as ex: - raise web.HTTPInternalServerError() from ex + raise web.HTTPInternalServerError from ex return web.Response(body=image.content, content_type=image.content_type) @@ -860,7 +865,7 @@ class CameraMjpegStream(CameraView): stream = None _LOGGER.debug("Error while writing MJPEG stream to transport") if stream is None: - raise web.HTTPBadGateway() + raise web.HTTPBadGateway return stream try: @@ -870,7 +875,7 @@ class CameraMjpegStream(CameraView): raise ValueError(f"Stream interval must be > {MIN_STREAM_INTERVAL}") return await camera.handle_async_still_stream(request, interval) except ValueError as err: - raise web.HTTPBadRequest() from err + raise web.HTTPBadRequest from err @websocket_api.websocket_command( diff --git a/homeassistant/components/camera/const.py b/homeassistant/components/camera/const.py index 09c4c7c1fb2..ad863f374d1 100644 --- a/homeassistant/components/camera/const.py +++ b/homeassistant/components/camera/const.py @@ -1,4 +1,5 @@ """Constants for Camera component.""" + from enum import StrEnum from functools import partial from typing import Final diff --git a/homeassistant/components/camera/img_util.py b/homeassistant/components/camera/img_util.py index e41e43c3a3c..b9b607d5edf 100644 --- a/homeassistant/components/camera/img_util.py +++ b/homeassistant/components/camera/img_util.py @@ -1,20 +1,28 @@ """Image processing for cameras.""" + from __future__ import annotations +from contextlib import suppress import logging from typing import TYPE_CHECKING, Literal, cast +with suppress(Exception): # pylint: disable=broad-except + # TurboJPEG imports numpy which may or may not work so + # we have to guard the import here. We still want + # to import it at top level so it gets loaded + # in the import executor and not in the event loop. + from turbojpeg import TurboJPEG + + +if TYPE_CHECKING: + from . import Image + SUPPORTED_SCALING_FACTORS = [(7, 8), (3, 4), (5, 8), (1, 2), (3, 8), (1, 4), (1, 8)] _LOGGER = logging.getLogger(__name__) JPEG_QUALITY = 75 -if TYPE_CHECKING: - from turbojpeg import TurboJPEG - - from . import Image - def find_supported_scaling_factor( current_width: int, current_height: int, target_width: int, target_height: int @@ -89,12 +97,6 @@ class TurboJPEGSingleton: def __init__(self) -> None: """Try to create TurboJPEG only once.""" try: - # TurboJPEG checks for libturbojpeg - # when its created, but it imports - # numpy which may or may not work so - # we have to guard the import here. - from turbojpeg import TurboJPEG # pylint: disable=import-outside-toplevel - TurboJPEGSingleton.__instance = TurboJPEG() except Exception: # pylint: disable=broad-except _LOGGER.exception( diff --git a/homeassistant/components/camera/media_source.py b/homeassistant/components/camera/media_source.py index a49ce11413d..4bb6ed5f921 100644 --- a/homeassistant/components/camera/media_source.py +++ b/homeassistant/components/camera/media_source.py @@ -1,4 +1,5 @@ """Expose cameras as media sources.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/camera/prefs.py b/homeassistant/components/camera/prefs.py index 7f3f142378a..2eccaf500e1 100644 --- a/homeassistant/components/camera/prefs.py +++ b/homeassistant/components/camera/prefs.py @@ -1,4 +1,5 @@ """Preference management for camera component.""" + from __future__ import annotations from collections.abc import Mapping diff --git a/homeassistant/components/camera/significant_change.py b/homeassistant/components/camera/significant_change.py index 4fc175b0723..5240e16376c 100644 --- a/homeassistant/components/camera/significant_change.py +++ b/homeassistant/components/camera/significant_change.py @@ -1,4 +1,5 @@ """Helper to test significant Camera state changes.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/canary/__init__.py b/homeassistant/components/canary/__init__.py index bc360f99581..60ce50484d8 100644 --- a/homeassistant/components/canary/__init__.py +++ b/homeassistant/components/canary/__init__.py @@ -1,4 +1,5 @@ """Support for Canary devices.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/canary/alarm_control_panel.py b/homeassistant/components/canary/alarm_control_panel.py index f668da25e2e..445579b9e4a 100644 --- a/homeassistant/components/canary/alarm_control_panel.py +++ b/homeassistant/components/canary/alarm_control_panel.py @@ -1,4 +1,5 @@ """Support for Canary alarm.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/canary/camera.py b/homeassistant/components/canary/camera.py index af78dceca23..e081d24e06a 100644 --- a/homeassistant/components/canary/camera.py +++ b/homeassistant/components/canary/camera.py @@ -1,4 +1,5 @@ """Support for Canary camera.""" + from __future__ import annotations from datetime import timedelta @@ -63,22 +64,22 @@ async def async_setup_entry( ffmpeg_arguments: str = entry.options.get( CONF_FFMPEG_ARGUMENTS, DEFAULT_FFMPEG_ARGUMENTS ) - cameras: list[CanaryCamera] = [] - for location_id, location in coordinator.data["locations"].items(): - for device in location.devices: - if device.is_online: - cameras.append( - CanaryCamera( - hass, - coordinator, - location_id, - device, - ffmpeg_arguments, - ) - ) - - async_add_entities(cameras, True) + async_add_entities( + ( + CanaryCamera( + hass, + coordinator, + location_id, + device, + ffmpeg_arguments, + ) + for location_id, location in coordinator.data["locations"].items() + for device in location.devices + if device.is_online + ), + True, + ) class CanaryCamera(CoordinatorEntity[CanaryDataUpdateCoordinator], Camera): diff --git a/homeassistant/components/canary/config_flow.py b/homeassistant/components/canary/config_flow.py index 6b3176f6bbd..f586a7e4e85 100644 --- a/homeassistant/components/canary/config_flow.py +++ b/homeassistant/components/canary/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Canary.""" + from __future__ import annotations import logging @@ -8,10 +9,14 @@ from canary.api import Api from requests.exceptions import ConnectTimeout, HTTPError import voluptuous as vol -from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME from homeassistant.core import HomeAssistant, callback -from homeassistant.data_entry_flow import FlowResult from .const import ( CONF_FFMPEG_ARGUMENTS, @@ -51,13 +56,13 @@ class CanaryConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_import( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initiated by configuration file.""" return await self.async_step_user(user_input) async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initiated by the user.""" if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") @@ -107,7 +112,7 @@ class CanaryOptionsFlowHandler(OptionsFlow): async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Manage Canary options.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) diff --git a/homeassistant/components/canary/coordinator.py b/homeassistant/components/canary/coordinator.py index 1b47d6d70b7..d58d1da0f79 100644 --- a/homeassistant/components/canary/coordinator.py +++ b/homeassistant/components/canary/coordinator.py @@ -1,4 +1,5 @@ """Provides the Canary DataUpdateCoordinator.""" + from __future__ import annotations import asyncio @@ -45,9 +46,9 @@ class CanaryDataUpdateCoordinator(DataUpdateCoordinator[CanaryData]): for device in location.devices: if device.is_online: - readings_by_device_id[ - device.device_id - ] = self.canary.get_latest_readings(device.device_id) + readings_by_device_id[device.device_id] = ( + self.canary.get_latest_readings(device.device_id) + ) return { "locations": locations_by_id, diff --git a/homeassistant/components/canary/model.py b/homeassistant/components/canary/model.py index 4ed868a7e60..261e59b8cfa 100644 --- a/homeassistant/components/canary/model.py +++ b/homeassistant/components/canary/model.py @@ -1,4 +1,5 @@ """Constants for the Canary integration.""" + from __future__ import annotations from collections.abc import ValuesView diff --git a/homeassistant/components/canary/sensor.py b/homeassistant/components/canary/sensor.py index bdba9d4f130..905214e0d1d 100644 --- a/homeassistant/components/canary/sensor.py +++ b/homeassistant/components/canary/sensor.py @@ -1,4 +1,5 @@ """Support for Canary sensors.""" + from __future__ import annotations from typing import Final @@ -74,11 +75,11 @@ async def async_setup_entry( for device in location.devices: if device.is_online: device_type = device.device_type - for sensor_type in SENSOR_TYPES: - if device_type.get("name") in sensor_type[4]: - sensors.append( - CanarySensor(coordinator, sensor_type, location, device) - ) + sensors.extend( + CanarySensor(coordinator, sensor_type, location, device) + for sensor_type in SENSOR_TYPES + if device_type.get("name") in sensor_type[4] + ) async_add_entities(sensors, True) diff --git a/homeassistant/components/cast/__init__.py b/homeassistant/components/cast/__init__.py index 9aed870d9b4..b41dc9ddb41 100644 --- a/homeassistant/components/cast/__init__.py +++ b/homeassistant/components/cast/__init__.py @@ -1,4 +1,5 @@ """Component to embed Google Cast.""" + from __future__ import annotations from typing import Protocol diff --git a/homeassistant/components/cast/config_flow.py b/homeassistant/components/cast/config_flow.py index e58bcb71b28..6ccd7be19c3 100644 --- a/homeassistant/components/cast/config_flow.py +++ b/homeassistant/components/cast/config_flow.py @@ -1,15 +1,20 @@ """Config flow for Cast.""" + from __future__ import annotations from typing import Any import voluptuous as vol -from homeassistant import config_entries from homeassistant.components import onboarding, zeroconf +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.const import CONF_UUID from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_validation as cv from .const import CONF_IGNORE_CEC, CONF_KNOWN_HOSTS, DOMAIN @@ -19,7 +24,7 @@ KNOWN_HOSTS_SCHEMA = vol.Schema(vol.All(cv.ensure_list, [cv.string])) WANTED_UUID_SCHEMA = vol.Schema(vol.All(cv.ensure_list, [cv.string])) -class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class FlowHandler(ConfigFlow, domain=DOMAIN): """Handle a config flow.""" VERSION = 1 @@ -33,7 +38,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback def async_get_options_flow( - config_entry: config_entries.ConfigEntry, + config_entry: ConfigEntry, ) -> CastOptionsFlowHandler: """Get the options flow for this handler.""" return CastOptionsFlowHandler(config_entry) @@ -47,7 +52,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_zeroconf( self, discovery_info: zeroconf.ZeroconfServiceInfo - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by zeroconf discovery.""" if self._async_in_progress() or self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") @@ -101,10 +106,10 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): } -class CastOptionsFlowHandler(config_entries.OptionsFlow): +class CastOptionsFlowHandler(OptionsFlow): """Handle Google Cast options.""" - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + def __init__(self, config_entry: ConfigEntry) -> None: """Initialize Google Cast options flow.""" self.config_entry = config_entry self.updated_config: dict[str, Any] = {} diff --git a/homeassistant/components/cast/const.py b/homeassistant/components/cast/const.py index f05c2c4c143..c57b686143d 100644 --- a/homeassistant/components/cast/const.py +++ b/homeassistant/components/cast/const.py @@ -1,4 +1,5 @@ """Consts for Cast integration.""" + from __future__ import annotations from typing import TYPE_CHECKING, TypedDict diff --git a/homeassistant/components/cast/discovery.py b/homeassistant/components/cast/discovery.py index 485d2888a41..4d956205990 100644 --- a/homeassistant/components/cast/discovery.py +++ b/homeassistant/components/cast/discovery.py @@ -1,4 +1,5 @@ """Deal with Cast discovery.""" + import logging import threading diff --git a/homeassistant/components/cast/helpers.py b/homeassistant/components/cast/helpers.py index bfe0bc70d79..2d4e1a9dbfa 100644 --- a/homeassistant/components/cast/helpers.py +++ b/homeassistant/components/cast/helpers.py @@ -1,4 +1,5 @@ """Helpers to deal with Cast devices.""" + from __future__ import annotations import configparser @@ -359,7 +360,7 @@ async def parse_pls(hass, url): async def parse_playlist(hass, url): """Parse an m3u or pls playlist.""" - if url.endswith(".m3u") or url.endswith(".m3u8"): + if url.endswith((".m3u", ".m3u8")): playlist = await parse_m3u(hass, url) else: playlist = await parse_pls(hass, url) diff --git a/homeassistant/components/cast/home_assistant_cast.py b/homeassistant/components/cast/home_assistant_cast.py index f7518b9519a..5db37519bdf 100644 --- a/homeassistant/components/cast/home_assistant_cast.py +++ b/homeassistant/components/cast/home_assistant_cast.py @@ -1,4 +1,5 @@ """Home Assistant Cast integration for Cast.""" + from __future__ import annotations import voluptuous as vol diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index 5e907b0a659..eedbd0dd0b1 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -1,4 +1,5 @@ """Provide functionality to interact with Cast devices on the network.""" + from __future__ import annotations from collections.abc import Callable @@ -271,7 +272,8 @@ class CastDevice: self._status_listener.invalidate() self._status_listener = None - async def _async_cast_discovered(self, discover: ChromecastInfo) -> None: + @callback + def _async_cast_discovered(self, discover: ChromecastInfo) -> None: """Handle discovery of new Chromecast.""" if self._cast_info.uuid != discover.uuid: # Discovered is not our device. @@ -737,11 +739,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity): "hlsVideoSegmentFormat": "fmp4", }, } - elif ( - media_id.endswith(".m3u") - or media_id.endswith(".m3u8") - or media_id.endswith(".pls") - ): + elif media_id.endswith((".m3u", ".m3u8", ".pls")): try: playlist = await parse_playlist(self.hass, media_id) _LOGGER.debug( diff --git a/homeassistant/components/ccm15/__init__.py b/homeassistant/components/ccm15/__init__.py index ae48394c732..a35568047ad 100644 --- a/homeassistant/components/ccm15/__init__.py +++ b/homeassistant/components/ccm15/__init__.py @@ -1,4 +1,5 @@ """The Midea ccm15 AC Controller integration.""" + from __future__ import annotations from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/ccm15/climate.py b/homeassistant/components/ccm15/climate.py index 1f90f317fe0..b4038fbbf43 100644 --- a/homeassistant/components/ccm15/climate.py +++ b/homeassistant/components/ccm15/climate.py @@ -1,4 +1,5 @@ """Climate device for CCM15 coordinator.""" + import logging from typing import Any @@ -131,7 +132,7 @@ class CCM15Climate(CoordinatorEntity[CCM15Coordinator], ClimateEntity): @property def available(self) -> bool: - """Return the avalability of the entity.""" + """Return the availability of the entity.""" return self.data is not None @property diff --git a/homeassistant/components/ccm15/config_flow.py b/homeassistant/components/ccm15/config_flow.py index efde47b8d30..f115aa8f6e1 100644 --- a/homeassistant/components/ccm15/config_flow.py +++ b/homeassistant/components/ccm15/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Midea ccm15 AC Controller integration.""" + from __future__ import annotations import logging @@ -7,9 +8,8 @@ from typing import Any from ccm15 import CCM15Device import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_PORT -from homeassistant.data_entry_flow import FlowResult import homeassistant.helpers.config_validation as cv from .const import DEFAULT_TIMEOUT, DOMAIN @@ -24,14 +24,14 @@ STEP_USER_DATA_SCHEMA = vol.Schema( ) -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class CCM15ConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Midea ccm15 AC Controller.""" VERSION = 1 async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors: dict[str, str] = {} if user_input is not None: diff --git a/homeassistant/components/ccm15/coordinator.py b/homeassistant/components/ccm15/coordinator.py index 9d8a0281706..cd3b313f700 100644 --- a/homeassistant/components/ccm15/coordinator.py +++ b/homeassistant/components/ccm15/coordinator.py @@ -1,4 +1,5 @@ """Climate device for CCM15 coordinator.""" + import datetime import logging diff --git a/homeassistant/components/ccm15/diagnostics.py b/homeassistant/components/ccm15/diagnostics.py index b4a3c80f319..08cc239e972 100644 --- a/homeassistant/components/ccm15/diagnostics.py +++ b/homeassistant/components/ccm15/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for CCM15.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/cert_expiry/__init__.py b/homeassistant/components/cert_expiry/__init__.py index d46cecc7edb..717a55b2027 100644 --- a/homeassistant/components/cert_expiry/__init__.py +++ b/homeassistant/components/cert_expiry/__init__.py @@ -1,4 +1,5 @@ """The cert_expiry component.""" + from __future__ import annotations from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/cert_expiry/config_flow.py b/homeassistant/components/cert_expiry/config_flow.py index b3ceb95d301..60863523553 100644 --- a/homeassistant/components/cert_expiry/config_flow.py +++ b/homeassistant/components/cert_expiry/config_flow.py @@ -1,4 +1,5 @@ """Config flow for the Cert Expiry platform.""" + from __future__ import annotations from collections.abc import Mapping @@ -7,9 +8,8 @@ from typing import Any import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import SOURCE_IMPORT, ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_PORT -from homeassistant.data_entry_flow import FlowResult from .const import DEFAULT_PORT, DOMAIN from .errors import ( @@ -23,7 +23,7 @@ from .helper import get_cert_expiry_timestamp _LOGGER = logging.getLogger(__name__) -class CertexpiryConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class CertexpiryConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow.""" VERSION = 1 @@ -57,7 +57,7 @@ class CertexpiryConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: Mapping[str, Any] | None = None, - ) -> FlowResult: + ) -> ConfigFlowResult: """Step when user initializes a integration.""" self._errors = {} if user_input is not None: @@ -73,7 +73,7 @@ class CertexpiryConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): title=title, data={CONF_HOST: host, CONF_PORT: port}, ) - if self.context["source"] == config_entries.SOURCE_IMPORT: + if self.context["source"] == SOURCE_IMPORT: _LOGGER.error("Config import failed for %s", user_input[CONF_HOST]) return self.async_abort(reason="import_failed") else: @@ -97,7 +97,7 @@ class CertexpiryConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_import( self, user_input: Mapping[str, Any] | None = None, - ) -> FlowResult: + ) -> ConfigFlowResult: """Import a config entry. Only host was required in the yaml file all other fields are optional diff --git a/homeassistant/components/cert_expiry/coordinator.py b/homeassistant/components/cert_expiry/coordinator.py index abb0b4ca727..80c91f1d890 100644 --- a/homeassistant/components/cert_expiry/coordinator.py +++ b/homeassistant/components/cert_expiry/coordinator.py @@ -1,4 +1,5 @@ """DataUpdateCoordinator for cert_expiry coordinator.""" + from __future__ import annotations from datetime import datetime, timedelta diff --git a/homeassistant/components/cert_expiry/errors.py b/homeassistant/components/cert_expiry/errors.py index a3b73c84f2a..25a1a9a8358 100644 --- a/homeassistant/components/cert_expiry/errors.py +++ b/homeassistant/components/cert_expiry/errors.py @@ -1,4 +1,5 @@ """Errors for the cert_expiry integration.""" + from homeassistant.exceptions import HomeAssistantError diff --git a/homeassistant/components/cert_expiry/helper.py b/homeassistant/components/cert_expiry/helper.py index 6d10d750705..b35687dc933 100644 --- a/homeassistant/components/cert_expiry/helper.py +++ b/homeassistant/components/cert_expiry/helper.py @@ -1,13 +1,14 @@ """Helper functions for the Cert Expiry platform.""" + import asyncio import datetime -from functools import cache import socket import ssl from typing import Any from homeassistant.core import HomeAssistant from homeassistant.util import dt as dt_util +from homeassistant.util.ssl import get_default_context from .const import TIMEOUT from .errors import ( @@ -18,12 +19,6 @@ from .errors import ( ) -@cache -def _get_default_ssl_context() -> ssl.SSLContext: - """Return the default SSL context.""" - return ssl.create_default_context() - - async def async_get_cert( hass: HomeAssistant, host: str, @@ -35,7 +30,7 @@ async def async_get_cert( asyncio.Protocol, host, port, - ssl=_get_default_ssl_context(), + ssl=get_default_context(), happy_eyeballs_delay=0.25, server_hostname=host, ) diff --git a/homeassistant/components/cert_expiry/sensor.py b/homeassistant/components/cert_expiry/sensor.py index 3e171006bdc..6a55e630a35 100644 --- a/homeassistant/components/cert_expiry/sensor.py +++ b/homeassistant/components/cert_expiry/sensor.py @@ -1,4 +1,5 @@ """Counter for the days until an HTTPS (TLS) certificate will expire.""" + from __future__ import annotations from datetime import datetime, timedelta diff --git a/homeassistant/components/channels/const.py b/homeassistant/components/channels/const.py index 5ae7fdebb0b..d05848d40f4 100644 --- a/homeassistant/components/channels/const.py +++ b/homeassistant/components/channels/const.py @@ -1,4 +1,5 @@ """Constants for the Channels component.""" + DOMAIN = "channels" SERVICE_SEEK_FORWARD = "seek_forward" SERVICE_SEEK_BACKWARD = "seek_backward" diff --git a/homeassistant/components/channels/icons.json b/homeassistant/components/channels/icons.json new file mode 100644 index 00000000000..cbbda1ef623 --- /dev/null +++ b/homeassistant/components/channels/icons.json @@ -0,0 +1,7 @@ +{ + "services": { + "seek_forward": "mdi:skip-forward", + "seek_backward": "mdi:skip-backward", + "seek_by": "mdi:timer-check-outline" + } +} diff --git a/homeassistant/components/channels/media_player.py b/homeassistant/components/channels/media_player.py index a834e9010ce..002ec8d4efb 100644 --- a/homeassistant/components/channels/media_player.py +++ b/homeassistant/components/channels/media_player.py @@ -1,4 +1,5 @@ """Support for interfacing with an instance of getchannels.com.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/circuit/__init__.py b/homeassistant/components/circuit/__init__.py index 488b0e94e09..f71babad3d5 100644 --- a/homeassistant/components/circuit/__init__.py +++ b/homeassistant/components/circuit/__init__.py @@ -1,4 +1,5 @@ """The Unify Circuit component.""" + import voluptuous as vol from homeassistant.const import CONF_NAME, CONF_URL, Platform diff --git a/homeassistant/components/circuit/notify.py b/homeassistant/components/circuit/notify.py index 836c4118df0..23884ebd9be 100644 --- a/homeassistant/components/circuit/notify.py +++ b/homeassistant/components/circuit/notify.py @@ -1,4 +1,5 @@ """Unify Circuit platform for notify component.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/cisco_ios/device_tracker.py b/homeassistant/components/cisco_ios/device_tracker.py index 1424d41006d..8a21b64cb9f 100644 --- a/homeassistant/components/cisco_ios/device_tracker.py +++ b/homeassistant/components/cisco_ios/device_tracker.py @@ -1,4 +1,5 @@ """Support for Cisco IOS Routers.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/cisco_mobility_express/device_tracker.py b/homeassistant/components/cisco_mobility_express/device_tracker.py index a5ca469d101..c156f43942e 100644 --- a/homeassistant/components/cisco_mobility_express/device_tracker.py +++ b/homeassistant/components/cisco_mobility_express/device_tracker.py @@ -1,4 +1,5 @@ """Support for Cisco Mobility Express.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/cisco_webex_teams/notify.py b/homeassistant/components/cisco_webex_teams/notify.py index d2c75d78390..30f56ac4712 100644 --- a/homeassistant/components/cisco_webex_teams/notify.py +++ b/homeassistant/components/cisco_webex_teams/notify.py @@ -1,10 +1,11 @@ """Cisco Webex Teams notify component.""" + from __future__ import annotations import logging -import sys import voluptuous as vol +from webexteamssdk import ApiError, WebexTeamsAPI, exceptions from homeassistant.components.notify import ( ATTR_TITLE, @@ -13,14 +14,9 @@ from homeassistant.components.notify import ( ) from homeassistant.const import CONF_TOKEN from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -if sys.version_info < (3, 12): - from webexteamssdk import ApiError, WebexTeamsAPI, exceptions - - _LOGGER = logging.getLogger(__name__) CONF_ROOM_ID = "room_id" @@ -36,11 +32,6 @@ def get_service( discovery_info: DiscoveryInfoType | None = None, ) -> CiscoWebexTeamsNotificationService | None: """Get the CiscoWebexTeams notification service.""" - if sys.version_info >= (3, 12): - raise HomeAssistantError( - "Cisco Webex Teams is not supported on Python 3.12. Please use Python 3.11." - ) - client = WebexTeamsAPI(access_token=config[CONF_TOKEN]) try: # Validate the token & room_id diff --git a/homeassistant/components/citybikes/sensor.py b/homeassistant/components/citybikes/sensor.py index fc49331c1b7..0cf27c20fa6 100644 --- a/homeassistant/components/citybikes/sensor.py +++ b/homeassistant/components/citybikes/sensor.py @@ -1,4 +1,5 @@ """Sensor for the CityBikes data.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/clementine/media_player.py b/homeassistant/components/clementine/media_player.py index 770f19e9970..84052aa64b9 100644 --- a/homeassistant/components/clementine/media_player.py +++ b/homeassistant/components/clementine/media_player.py @@ -1,4 +1,5 @@ """Support for Clementine Music Player as media player.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/clickatell/notify.py b/homeassistant/components/clickatell/notify.py index 8422f7295b3..70170217af2 100644 --- a/homeassistant/components/clickatell/notify.py +++ b/homeassistant/components/clickatell/notify.py @@ -1,4 +1,5 @@ """Clickatell platform for notify component.""" + from __future__ import annotations from http import HTTPStatus diff --git a/homeassistant/components/clicksend/notify.py b/homeassistant/components/clicksend/notify.py index 36ac21d8dd3..44954211748 100644 --- a/homeassistant/components/clicksend/notify.py +++ b/homeassistant/components/clicksend/notify.py @@ -1,4 +1,5 @@ """Clicksend platform for notify component.""" + from __future__ import annotations from http import HTTPStatus diff --git a/homeassistant/components/clicksend_tts/notify.py b/homeassistant/components/clicksend_tts/notify.py index 8eb3782415e..aeda1b26162 100644 --- a/homeassistant/components/clicksend_tts/notify.py +++ b/homeassistant/components/clicksend_tts/notify.py @@ -1,4 +1,5 @@ """clicksend_tts platform for notify component.""" + from __future__ import annotations from http import HTTPStatus diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index 7e3cb027506..00fd69ce63b 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -1,4 +1,5 @@ """Provides functionality to interact with climate devices.""" + from __future__ import annotations import asyncio @@ -23,6 +24,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.exceptions import ServiceValidationError +from homeassistant.helpers import issue_registry as ir import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, @@ -39,8 +41,10 @@ from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_platform import EntityPlatform from homeassistant.helpers.temperature import display_temp as show_temp from homeassistant.helpers.typing import ConfigType +from homeassistant.loader import async_get_issue_tracker, async_suggest_report_issue from homeassistant.util.unit_conversion import TemperatureConverter +from . import group as group_pre_import # noqa: F401 from .const import ( # noqa: F401 _DEPRECATED_HVAC_MODE_AUTO, _DEPRECATED_HVAC_MODE_COOL, @@ -290,9 +294,9 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): _attr_hvac_mode: HVACMode | None _attr_hvac_modes: list[HVACMode] _attr_is_aux_heat: bool | None - _attr_max_humidity: int = DEFAULT_MAX_HUMIDITY + _attr_max_humidity: float = DEFAULT_MAX_HUMIDITY _attr_max_temp: float - _attr_min_humidity: int = DEFAULT_MIN_HUMIDITY + _attr_min_humidity: float = DEFAULT_MIN_HUMIDITY _attr_min_temp: float _attr_precision: float _attr_preset_mode: str | None @@ -300,13 +304,15 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): _attr_supported_features: ClimateEntityFeature = ClimateEntityFeature(0) _attr_swing_mode: str | None _attr_swing_modes: list[str] | None - _attr_target_humidity: int | None = None + _attr_target_humidity: float | None = None _attr_target_temperature_high: float | None _attr_target_temperature_low: float | None _attr_target_temperature_step: float | None = None _attr_target_temperature: float | None = None _attr_temperature_unit: str + __climate_reported_legacy_aux = False + __mod_supported_features: ClimateEntityFeature = ClimateEntityFeature(0) # Integrations should set `_enable_turn_on_off_backwards_compatibility` to False # once migrated and set the feature flags TURN_ON/TURN_OFF as needed. @@ -402,6 +408,50 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): ClimateEntityFeature.TURN_ON | ClimateEntityFeature.TURN_OFF ) + def _report_legacy_aux(self) -> None: + """Log warning and create an issue if the entity implements legacy auxiliary heater.""" + + report_issue = async_suggest_report_issue( + self.hass, + integration_domain=self.platform.platform_name, + module=type(self).__module__, + ) + _LOGGER.warning( + ( + "%s::%s implements the `is_aux_heat` property or uses the auxiliary " + "heater methods in a subclass of ClimateEntity which is " + "deprecated and will be unsupported from Home Assistant 2024.10." + " Please %s" + ), + self.platform.platform_name, + self.__class__.__name__, + report_issue, + ) + + translation_placeholders = {"platform": self.platform.platform_name} + translation_key = "deprecated_climate_aux_no_url" + issue_tracker = async_get_issue_tracker( + self.hass, + integration_domain=self.platform.platform_name, + module=type(self).__module__, + ) + if issue_tracker: + translation_placeholders["issue_tracker"] = issue_tracker + translation_key = "deprecated_climate_aux_url_custom" + ir.async_create_issue( + self.hass, + DOMAIN, + f"deprecated_climate_aux_{self.platform.platform_name}", + breaks_in_ha_version="2024.10.0", + is_fixable=False, + is_persistent=False, + issue_domain=self.platform.platform_name, + severity=ir.IssueSeverity.WARNING, + translation_key=translation_key, + translation_placeholders=translation_placeholders, + ) + self.__climate_reported_legacy_aux = True + @final @property def state(self) -> str | None: @@ -506,6 +556,11 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): if ClimateEntityFeature.AUX_HEAT in supported_features: data[ATTR_AUX_HEAT] = STATE_ON if self.is_aux_heat else STATE_OFF + if ( + self.__climate_reported_legacy_aux is False + and "custom_components" in type(self).__module__ + ): + self._report_legacy_aux() return data @@ -515,12 +570,12 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): return self._attr_temperature_unit @cached_property - def current_humidity(self) -> int | None: + def current_humidity(self) -> float | None: """Return the current humidity.""" return self._attr_current_humidity @cached_property - def target_humidity(self) -> int | None: + def target_humidity(self) -> float | None: """Return the humidity we try to reach.""" return self._attr_target_humidity @@ -645,8 +700,6 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): elif mode_type == "fan": translation_key = "not_valid_fan_mode" raise ServiceValidationError( - f"The {mode_type}_mode {mode} is not a valid {mode_type}_mode:" - f" {modes_str}", translation_domain=DOMAIN, translation_key=translation_key, translation_placeholders={ @@ -657,7 +710,7 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): def set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" - raise NotImplementedError() + raise NotImplementedError async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" @@ -667,7 +720,7 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): def set_humidity(self, humidity: int) -> None: """Set new target humidity.""" - raise NotImplementedError() + raise NotImplementedError async def async_set_humidity(self, humidity: int) -> None: """Set new target humidity.""" @@ -681,7 +734,7 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): def set_fan_mode(self, fan_mode: str) -> None: """Set new target fan mode.""" - raise NotImplementedError() + raise NotImplementedError async def async_set_fan_mode(self, fan_mode: str) -> None: """Set new target fan mode.""" @@ -689,7 +742,7 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): def set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set new target hvac mode.""" - raise NotImplementedError() + raise NotImplementedError async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set new target hvac mode.""" @@ -703,7 +756,7 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): def set_swing_mode(self, swing_mode: str) -> None: """Set new target swing operation.""" - raise NotImplementedError() + raise NotImplementedError async def async_set_swing_mode(self, swing_mode: str) -> None: """Set new target swing operation.""" @@ -717,7 +770,7 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): def set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode.""" - raise NotImplementedError() + raise NotImplementedError async def async_set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode.""" @@ -725,7 +778,7 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): def turn_aux_heat_on(self) -> None: """Turn auxiliary heater on.""" - raise NotImplementedError() + raise NotImplementedError async def async_turn_aux_heat_on(self) -> None: """Turn auxiliary heater on.""" @@ -733,7 +786,7 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): def turn_aux_heat_off(self) -> None: """Turn auxiliary heater off.""" - raise NotImplementedError() + raise NotImplementedError async def async_turn_aux_heat_off(self) -> None: """Turn auxiliary heater off.""" @@ -826,12 +879,12 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): return self._attr_max_temp @cached_property - def min_humidity(self) -> int: + def min_humidity(self) -> float: """Return the minimum humidity.""" return self._attr_min_humidity @cached_property - def max_humidity(self) -> int: + def max_humidity(self) -> float: """Return the maximum humidity.""" return self._attr_max_humidity diff --git a/homeassistant/components/climate/device_action.py b/homeassistant/components/climate/device_action.py index a920884c252..84f166b752e 100644 --- a/homeassistant/components/climate/device_action.py +++ b/homeassistant/components/climate/device_action.py @@ -1,4 +1,5 @@ """Provides device automations for Climate.""" + from __future__ import annotations import voluptuous as vol diff --git a/homeassistant/components/climate/device_condition.py b/homeassistant/components/climate/device_condition.py index 78f358db32e..1becbf84915 100644 --- a/homeassistant/components/climate/device_condition.py +++ b/homeassistant/components/climate/device_condition.py @@ -1,4 +1,5 @@ """Provide the device automations for Climate.""" + from __future__ import annotations import voluptuous as vol diff --git a/homeassistant/components/climate/device_trigger.py b/homeassistant/components/climate/device_trigger.py index 0afd2485517..9702c97d0da 100644 --- a/homeassistant/components/climate/device_trigger.py +++ b/homeassistant/components/climate/device_trigger.py @@ -1,4 +1,5 @@ """Provides device automations for Climate.""" + from __future__ import annotations import voluptuous as vol @@ -139,13 +140,13 @@ async def async_attach_trigger( } if trigger_type == "current_temperature_changed": - numeric_state_config[ - numeric_state_trigger.CONF_VALUE_TEMPLATE - ] = "{{ state.attributes.current_temperature }}" + numeric_state_config[numeric_state_trigger.CONF_VALUE_TEMPLATE] = ( + "{{ state.attributes.current_temperature }}" + ) else: # trigger_type == "current_humidity_changed" - numeric_state_config[ - numeric_state_trigger.CONF_VALUE_TEMPLATE - ] = "{{ state.attributes.current_humidity }}" + numeric_state_config[numeric_state_trigger.CONF_VALUE_TEMPLATE] = ( + "{{ state.attributes.current_humidity }}" + ) if CONF_ABOVE in config: numeric_state_config[CONF_ABOVE] = config[CONF_ABOVE] diff --git a/homeassistant/components/climate/group.py b/homeassistant/components/climate/group.py index cec41f81b28..f0b7a748740 100644 --- a/homeassistant/components/climate/group.py +++ b/homeassistant/components/climate/group.py @@ -1,16 +1,19 @@ """Describe group states.""" +from typing import TYPE_CHECKING -from homeassistant.components.group import GroupIntegrationRegistry from homeassistant.const import STATE_OFF from homeassistant.core import HomeAssistant, callback from .const import HVAC_MODES, HVACMode +if TYPE_CHECKING: + from homeassistant.components.group import GroupIntegrationRegistry + @callback def async_describe_on_off_states( - hass: HomeAssistant, registry: GroupIntegrationRegistry + hass: HomeAssistant, registry: "GroupIntegrationRegistry" ) -> None: """Describe group on off states.""" registry.on_off_states( diff --git a/homeassistant/components/climate/icons.json b/homeassistant/components/climate/icons.json index 5afce637ed5..91a306f1da4 100644 --- a/homeassistant/components/climate/icons.json +++ b/homeassistant/components/climate/icons.json @@ -58,6 +58,12 @@ "set_fan_mode": "mdi:fan", "set_humidity": "mdi:water-percent", "set_swing_mode": "mdi:arrow-oscillating", - "set_temperature": "mdi:thermometer" + "set_temperature": "mdi:thermometer", + "set_aux_heat": "mdi:radiator", + "set_preset_mode": "mdi:sofa", + "set_hvac_mode": "mdi:hvac", + "turn_on": "mdi:power-on", + "turn_off": "mdi:power-off", + "toggle": "mdi:toggle-switch" } } diff --git a/homeassistant/components/climate/intent.py b/homeassistant/components/climate/intent.py index db263451f0b..3073d3e3c26 100644 --- a/homeassistant/components/climate/intent.py +++ b/homeassistant/components/climate/intent.py @@ -58,6 +58,7 @@ class GetTemperatureIntent(intent.IntentHandler): raise intent.NoStatesMatchedError( name=entity_text or entity_name, area=area_name or area_id, + floor=None, domains={DOMAIN}, device_classes=None, ) @@ -75,6 +76,7 @@ class GetTemperatureIntent(intent.IntentHandler): raise intent.NoStatesMatchedError( name=entity_name, area=None, + floor=None, domains={DOMAIN}, device_classes=None, ) diff --git a/homeassistant/components/climate/reproduce_state.py b/homeassistant/components/climate/reproduce_state.py index e5fb5d6004b..110a8579ece 100644 --- a/homeassistant/components/climate/reproduce_state.py +++ b/homeassistant/components/climate/reproduce_state.py @@ -1,4 +1,5 @@ """Module that groups code required to handle state restore for component.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/climate/significant_change.py b/homeassistant/components/climate/significant_change.py index 7198153f9af..0c4cdd4ac6a 100644 --- a/homeassistant/components/climate/significant_change.py +++ b/homeassistant/components/climate/significant_change.py @@ -1,4 +1,5 @@ """Helper to test significant Climate state changes.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/climate/strings.json b/homeassistant/components/climate/strings.json index eb9285b0c4f..c31d22ccbeb 100644 --- a/homeassistant/components/climate/strings.json +++ b/homeassistant/components/climate/strings.json @@ -238,6 +238,16 @@ } } }, + "issues": { + "deprecated_climate_aux_url_custom": { + "title": "The {platform} custom integration is using deprecated climate auxiliary heater", + "description": "The custom integration `{platform}` implements the `is_aux_heat` property or uses the auxiliary heater methods in a subclass of ClimateEntity.\n\nPlease create a bug report at {issue_tracker}.\n\nOnce an updated version of `{platform}` is available, install it and restart Home Assistant to fix this issue." + }, + "deprecated_climate_aux_no_url": { + "title": "[%key:component::climate::issues::deprecated_climate_aux_url_custom::title%]", + "description": "The custom integration `{platform}` implements the `is_aux_heat` property or uses the auxiliary heater methods in a subclass of ClimateEntity.\n\nPlease report it to the author of the {platform} integration.\n\nOnce an updated version of `{platform}` is available, install it and restart Home Assistant to fix this issue." + } + }, "exceptions": { "not_valid_preset_mode": { "message": "Preset mode {mode} is not valid. Valid preset modes are: {modes}." diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py index 888e99e3a34..aefab869955 100644 --- a/homeassistant/components/cloud/__init__.py +++ b/homeassistant/components/cloud/__init__.py @@ -1,4 +1,5 @@ """Component to integrate the Home Assistant cloud.""" + from __future__ import annotations import asyncio @@ -11,7 +12,7 @@ from hass_nabucasa import Cloud import voluptuous as vol from homeassistant.components import alexa, google_assistant -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import SOURCE_SYSTEM, ConfigEntry from homeassistant.const import ( CONF_DESCRIPTION, CONF_MODE, @@ -304,7 +305,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return loaded = True - await hass.config_entries.flow.async_init(DOMAIN, context={"source": "system"}) + await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_SYSTEM} + ) async def _on_connect() -> None: """Handle cloud connect.""" diff --git a/homeassistant/components/cloud/account_link.py b/homeassistant/components/cloud/account_link.py index f1e5d1a6903..df2789663c0 100644 --- a/homeassistant/components/cloud/account_link.py +++ b/homeassistant/components/cloud/account_link.py @@ -1,4 +1,5 @@ """Account linking via the cloud.""" + from __future__ import annotations from datetime import datetime diff --git a/homeassistant/components/cloud/alexa_config.py b/homeassistant/components/cloud/alexa_config.py index 415f2415095..12f2b04d856 100644 --- a/homeassistant/components/cloud/alexa_config.py +++ b/homeassistant/components/cloud/alexa_config.py @@ -1,4 +1,5 @@ """Alexa configuration for Home Assistant Cloud.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/cloud/assist_pipeline.py b/homeassistant/components/cloud/assist_pipeline.py index 2c381dd0ac0..e9d66bdcc1f 100644 --- a/homeassistant/components/cloud/assist_pipeline.py +++ b/homeassistant/components/cloud/assist_pipeline.py @@ -1,4 +1,5 @@ """Handle Cloud assist pipelines.""" + import asyncio from homeassistant.components.assist_pipeline import ( diff --git a/homeassistant/components/cloud/binary_sensor.py b/homeassistant/components/cloud/binary_sensor.py index d56896dd7b1..0693a8285ce 100644 --- a/homeassistant/components/cloud/binary_sensor.py +++ b/homeassistant/components/cloud/binary_sensor.py @@ -1,4 +1,5 @@ """Support for Home Assistant Cloud binary sensors.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/cloud/client.py b/homeassistant/components/cloud/client.py index e569602f944..01c8de77156 100644 --- a/homeassistant/components/cloud/client.py +++ b/homeassistant/components/cloud/client.py @@ -1,4 +1,5 @@ """Interface implementation for cloud client.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/cloud/config_flow.py b/homeassistant/components/cloud/config_flow.py index a9554d97294..932291c2bfa 100644 --- a/homeassistant/components/cloud/config_flow.py +++ b/homeassistant/components/cloud/config_flow.py @@ -1,10 +1,10 @@ """Config flow for the Cloud integration.""" + from __future__ import annotations from typing import Any -from homeassistant.config_entries import ConfigFlow -from homeassistant.data_entry_flow import FlowResult +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from .const import DOMAIN @@ -16,7 +16,7 @@ class CloudConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_system( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the system step.""" if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") diff --git a/homeassistant/components/cloud/const.py b/homeassistant/components/cloud/const.py index f704fb61f69..1ee7392eccf 100644 --- a/homeassistant/components/cloud/const.py +++ b/homeassistant/components/cloud/const.py @@ -1,4 +1,5 @@ """Constants for the cloud component.""" + from __future__ import annotations from typing import Any @@ -32,7 +33,7 @@ PREF_GOOGLE_SETTINGS_VERSION = "google_settings_version" PREF_TTS_DEFAULT_VOICE = "tts_default_voice" PREF_GOOGLE_CONNECTED = "google_connected" PREF_REMOTE_ALLOW_REMOTE_ENABLE = "remote_allow_remote_enable" -DEFAULT_TTS_DEFAULT_VOICE = ("en-US", "female") +DEFAULT_TTS_DEFAULT_VOICE = ("en-US", "JennyNeural") DEFAULT_DISABLE_2FA = False DEFAULT_ALEXA_REPORT_STATE = True DEFAULT_GOOGLE_REPORT_STATE = True diff --git a/homeassistant/components/cloud/google_config.py b/homeassistant/components/cloud/google_config.py index bda2412b476..1ba2fab717f 100644 --- a/homeassistant/components/cloud/google_config.py +++ b/homeassistant/components/cloud/google_config.py @@ -1,4 +1,5 @@ """Google config for Cloud.""" + from __future__ import annotations import asyncio @@ -176,7 +177,7 @@ class CloudGoogleConfig(AbstractConfig): def get_local_user_id(self, webhook_id: Any) -> str: """Map webhook ID to a Home Assistant user ID. - Any action inititated by Google Assistant via the local SDK will be attributed + Any action initiated by Google Assistant via the local SDK will be attributed to the returned user ID. """ return self._user @@ -399,7 +400,11 @@ class CloudGoogleConfig(AbstractConfig): @callback def async_get_agent_users(self) -> tuple: """Return known agent users.""" - if not self._prefs.google_connected or not self._cloud.username: + if ( + not self._cloud.is_logged_in # Can't call Cloud.username if not logged in + or not self._prefs.google_connected + or not self._cloud.username + ): return () return (self._cloud.username,) diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index 4fd9d5c0301..1a8fd7dbea9 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -1,4 +1,5 @@ """The HTTP api to control the cloud integration.""" + from __future__ import annotations import asyncio @@ -15,7 +16,7 @@ from aiohttp import web import attr from hass_nabucasa import Cloud, auth, thingtalk from hass_nabucasa.const import STATE_DISCONNECTED -from hass_nabucasa.voice import MAP_VOICE +from hass_nabucasa.voice import TTS_VOICES import voluptuous as vol from homeassistant.components import websocket_api @@ -25,7 +26,7 @@ from homeassistant.components.alexa import ( ) from homeassistant.components.google_assistant import helpers as google_helpers from homeassistant.components.homeassistant import exposed_entities -from homeassistant.components.http import HomeAssistantView, require_admin +from homeassistant.components.http import KEY_HASS, HomeAssistantView, require_admin from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES from homeassistant.core import HomeAssistant, callback @@ -70,6 +71,7 @@ _CLOUD_ERRORS: dict[type[Exception], tuple[HTTPStatus, str]] = { @callback def async_setup(hass: HomeAssistant) -> None: """Initialize the HTTP API.""" + websocket_api.async_register_command(hass, websocket_cloud_remove_data) websocket_api.async_register_command(hass, websocket_cloud_status) websocket_api.async_register_command(hass, websocket_subscription) websocket_api.async_register_command(hass, websocket_update_prefs) @@ -197,7 +199,7 @@ class GoogleActionsSyncView(HomeAssistantView): @_handle_cloud_errors async def post(self, request: web.Request) -> web.Response: """Trigger a Google Actions sync.""" - hass = request.app["hass"] + hass = request.app[KEY_HASS] cloud: Cloud[CloudClient] = hass.data[DOMAIN] gconf = await cloud.client.get_google_config() status = await gconf.async_sync_entities(gconf.agent_user_id) @@ -217,7 +219,7 @@ class CloudLoginView(HomeAssistantView): ) async def post(self, request: web.Request, data: dict[str, Any]) -> web.Response: """Handle login request.""" - hass: HomeAssistant = request.app["hass"] + hass = request.app[KEY_HASS] cloud: Cloud[CloudClient] = hass.data[DOMAIN] await cloud.login(data["email"], data["password"]) @@ -235,7 +237,7 @@ class CloudLogoutView(HomeAssistantView): @_handle_cloud_errors async def post(self, request: web.Request) -> web.Response: """Handle logout request.""" - hass = request.app["hass"] + hass = request.app[KEY_HASS] cloud: Cloud[CloudClient] = hass.data[DOMAIN] async with asyncio.timeout(REQUEST_TIMEOUT): @@ -262,7 +264,7 @@ class CloudRegisterView(HomeAssistantView): ) async def post(self, request: web.Request, data: dict[str, Any]) -> web.Response: """Handle registration request.""" - hass = request.app["hass"] + hass = request.app[KEY_HASS] cloud: Cloud[CloudClient] = hass.data[DOMAIN] client_metadata = None @@ -299,7 +301,7 @@ class CloudResendConfirmView(HomeAssistantView): @RequestDataValidator(vol.Schema({vol.Required("email"): str})) async def post(self, request: web.Request, data: dict[str, Any]) -> web.Response: """Handle resending confirm email code request.""" - hass = request.app["hass"] + hass = request.app[KEY_HASS] cloud: Cloud[CloudClient] = hass.data[DOMAIN] async with asyncio.timeout(REQUEST_TIMEOUT): @@ -319,7 +321,7 @@ class CloudForgotPasswordView(HomeAssistantView): @RequestDataValidator(vol.Schema({vol.Required("email"): str})) async def post(self, request: web.Request, data: dict[str, Any]) -> web.Response: """Handle forgot password request.""" - hass = request.app["hass"] + hass = request.app[KEY_HASS] cloud: Cloud[CloudClient] = hass.data[DOMAIN] async with asyncio.timeout(REQUEST_TIMEOUT): @@ -328,6 +330,33 @@ class CloudForgotPasswordView(HomeAssistantView): return self.json_message("ok") +@websocket_api.require_admin +@websocket_api.websocket_command({vol.Required("type"): "cloud/remove_data"}) +@websocket_api.async_response +async def websocket_cloud_remove_data( + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: dict[str, Any], +) -> None: + """Handle request for account info. + + Async friendly. + """ + cloud: Cloud[CloudClient] = hass.data[DOMAIN] + if cloud.is_logged_in: + connection.send_message( + websocket_api.error_message( + msg["id"], "logged_in", "Can't remove data when logged in." + ) + ) + return + + await cloud.remove_data() + await cloud.client.prefs.async_erase_config() + + connection.send_message(websocket_api.result_message(msg["id"])) + + @websocket_api.websocket_command({vol.Required("type"): "cloud/status"}) @websocket_api.async_response async def websocket_cloud_status( @@ -397,6 +426,16 @@ async def websocket_subscription( async_manage_legacy_subscription_issue(hass, data) +def validate_language_voice(value: tuple[str, str]) -> tuple[str, str]: + """Validate language and voice.""" + language, voice = value + if language not in TTS_VOICES: + raise vol.Invalid(f"Invalid language {language}") + if voice not in TTS_VOICES[language]: + raise vol.Invalid(f"Invalid voice {voice} for language {language}") + return value + + @_require_cloud_login @websocket_api.websocket_command( { @@ -407,7 +446,7 @@ async def websocket_subscription( vol.Optional(PREF_GOOGLE_REPORT_STATE): bool, vol.Optional(PREF_GOOGLE_SECURE_DEVICES_PIN): vol.Any(None, str), vol.Optional(PREF_TTS_DEFAULT_VOICE): vol.All( - vol.Coerce(tuple), vol.In(MAP_VOICE) + vol.Coerce(tuple), validate_language_voice ), vol.Optional(PREF_REMOTE_ALLOW_REMOTE_ENABLE): bool, } @@ -648,16 +687,14 @@ async def google_assistant_list( gconf = await cloud.client.get_google_config() entities = google_helpers.async_get_entities(hass, gconf) - result = [] - - for entity in entities: - result.append( - { - "entity_id": entity.entity_id, - "traits": [trait.name for trait in entity.traits()], - "might_2fa": entity.might_2fa_traits(), - } - ) + result = [ + { + "entity_id": entity.entity_id, + "traits": [trait.name for trait in entity.traits()], + "might_2fa": entity.might_2fa_traits(), + } + for entity in entities + ] connection.send_result(msg["id"], result) @@ -742,16 +779,14 @@ async def alexa_list( alexa_config = await cloud.client.get_alexa_config() entities = alexa_entities.async_get_entities(hass, alexa_config) - result = [] - - for entity in entities: - result.append( - { - "entity_id": entity.entity_id, - "display_categories": entity.default_display_categories(), - "interfaces": [ifc.name() for ifc in entity.interfaces()], - } - ) + result = [ + { + "entity_id": entity.entity_id, + "display_categories": entity.default_display_categories(), + "interfaces": [ifc.name() for ifc in entity.interfaces()], + } + for entity in entities + ] connection.send_result(msg["id"], result) @@ -815,5 +850,12 @@ def tts_info( ) -> None: """Fetch available tts info.""" connection.send_result( - msg["id"], {"languages": [(lang, gender.value) for lang, gender in MAP_VOICE]} + msg["id"], + { + "languages": [ + (language, voice) + for language, voices in TTS_VOICES.items() + for voice in voices + ] + }, ) diff --git a/homeassistant/components/cloud/manifest.json b/homeassistant/components/cloud/manifest.json index ef2d32fcb0c..eed2bda421b 100644 --- a/homeassistant/components/cloud/manifest.json +++ b/homeassistant/components/cloud/manifest.json @@ -3,11 +3,10 @@ "name": "Home Assistant Cloud", "after_dependencies": ["assist_pipeline", "google_assistant", "alexa"], "codeowners": ["@home-assistant/cloud"], - "dependencies": ["http", "webhook"], + "dependencies": ["http", "repairs", "webhook"], "documentation": "https://www.home-assistant.io/integrations/cloud", - "import_executor": true, "integration_type": "system", "iot_class": "cloud_push", "loggers": ["hass_nabucasa"], - "requirements": ["hass-nabucasa==0.78.0"] + "requirements": ["hass-nabucasa==0.79.0"] } diff --git a/homeassistant/components/cloud/prefs.py b/homeassistant/components/cloud/prefs.py index 010a9697f26..af4e68194d6 100644 --- a/homeassistant/components/cloud/prefs.py +++ b/homeassistant/components/cloud/prefs.py @@ -1,10 +1,13 @@ """Preference management for cloud.""" + from __future__ import annotations from collections.abc import Callable, Coroutine from typing import Any import uuid +from hass_nabucasa.voice import MAP_VOICE + from homeassistant.auth.const import GROUP_ID_ADMIN from homeassistant.auth.models import User from homeassistant.components import webhook @@ -47,7 +50,7 @@ from .const import ( STORAGE_KEY = DOMAIN STORAGE_VERSION = 1 -STORAGE_VERSION_MINOR = 3 +STORAGE_VERSION_MINOR = 4 ALEXA_SETTINGS_VERSION = 3 GOOGLE_SETTINGS_VERSION = 3 @@ -81,6 +84,24 @@ class CloudPreferencesStore(Store): # In HA Core 2024.9, remove the import and also remove the Google # assistant store if it's not been migrated by manual Google assistant old_data.setdefault(PREF_GOOGLE_CONNECTED, await google_connected()) + if old_minor_version < 4: + # Update the default TTS voice to the new default. + # The default tts voice is a tuple. + # The first item is the language, the second item used to be gender. + # The new second item is the voice name. + default_tts_voice = old_data.get(PREF_TTS_DEFAULT_VOICE) + if default_tts_voice and (voice_item_two := default_tts_voice[1]) in ( + "female", + "male", + ): + language: str = default_tts_voice[0] + if voice := MAP_VOICE.get((language, voice_item_two)): + old_data[PREF_TTS_DEFAULT_VOICE] = ( + language, + voice, + ) + else: + old_data[PREF_TTS_DEFAULT_VOICE] = DEFAULT_TTS_DEFAULT_VOICE return old_data @@ -203,6 +224,10 @@ class CloudPreferences: return True + async def async_erase_config(self) -> None: + """Erase the configuration.""" + await self._save_prefs(self._empty_config("")) + def as_dict(self) -> dict[str, Any]: """Return dictionary version.""" return { @@ -327,7 +352,10 @@ class CloudPreferences: @property def tts_default_voice(self) -> tuple[str, str]: - """Return the default TTS voice.""" + """Return the default TTS voice. + + The return value is a tuple of language and voice. + """ return self._prefs.get(PREF_TTS_DEFAULT_VOICE, DEFAULT_TTS_DEFAULT_VOICE) # type: ignore[no-any-return] async def get_cloud_user(self) -> str: diff --git a/homeassistant/components/cloud/repairs.py b/homeassistant/components/cloud/repairs.py index f7368731d92..1c5a8f1f86d 100644 --- a/homeassistant/components/cloud/repairs.py +++ b/homeassistant/components/cloud/repairs.py @@ -1,4 +1,5 @@ """Repairs implementation for the cloud integration.""" + from __future__ import annotations import asyncio @@ -7,7 +8,11 @@ from typing import Any from hass_nabucasa import Cloud import voluptuous as vol -from homeassistant.components.repairs import RepairsFlow, repairs_flow_manager +from homeassistant.components.repairs import ( + ConfirmRepairFlow, + RepairsFlow, + repairs_flow_manager, +) from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import issue_registry as ir @@ -119,4 +124,6 @@ async def async_create_fix_flow( data: dict[str, str | int | float | None] | None, ) -> RepairsFlow: """Create flow.""" - return LegacySubscriptionRepairFlow() + if issue_id == "legacy_subscription": + return LegacySubscriptionRepairFlow() + return ConfirmRepairFlow() diff --git a/homeassistant/components/cloud/strings.json b/homeassistant/components/cloud/strings.json index 4bef2ac9ba3..16a82a27c1a 100644 --- a/homeassistant/components/cloud/strings.json +++ b/homeassistant/components/cloud/strings.json @@ -24,6 +24,17 @@ } }, "issues": { + "deprecated_gender": { + "title": "The `{deprecated_option}` text-to-speech option is deprecated", + "fix_flow": { + "step": { + "confirm": { + "title": "[%key:component::cloud::issues::deprecated_voice::title%]", + "description": "The `{deprecated_option}` option for text-to-speech in the {integration_name} integration is deprecated and will be removed.\nPlease update your automations and scripts to replace the `{deprecated_option}` option with an option for a supported `{replacement_option}` instead." + } + } + } + }, "deprecated_tts_platform_config": { "title": "The Cloud text-to-speech platform configuration is deprecated", "description": "The whole `platform: cloud` entry under the `tts:` section in configuration.yaml is deprecated and should be removed. You can use the UI to change settings for the Cloud text-to-speech platform. Please adjust your configuration.yaml and restart Home Assistant to fix this issue." @@ -34,7 +45,7 @@ "step": { "confirm": { "title": "[%key:component::cloud::issues::deprecated_voice::title%]", - "description": "The '{deprecated_voice}' voice is deprecated and will be removed.\nPlease update your automations and scripts to replace the '{deprecated_voice}' with another voice like eg. '{replacement_voice}'." + "description": "The `{deprecated_voice}`voice is deprecated and will be removed.\nPlease update your automations and scripts to replace the `{deprecated_voice}` with another voice like eg. `{replacement_voice}`." } } } diff --git a/homeassistant/components/cloud/stt.py b/homeassistant/components/cloud/stt.py index 3368f25f94a..d718cc5201e 100644 --- a/homeassistant/components/cloud/stt.py +++ b/homeassistant/components/cloud/stt.py @@ -1,4 +1,5 @@ """Support for the cloud for speech to text service.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/cloud/subscription.py b/homeassistant/components/cloud/subscription.py index 63b57d2fa3d..dc6679a6e40 100644 --- a/homeassistant/components/cloud/subscription.py +++ b/homeassistant/components/cloud/subscription.py @@ -1,4 +1,5 @@ """Subscription information.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/cloud/system_health.py b/homeassistant/components/cloud/system_health.py index d149e13c996..866626f4c79 100644 --- a/homeassistant/components/cloud/system_health.py +++ b/homeassistant/components/cloud/system_health.py @@ -1,4 +1,5 @@ """Provide info to system health.""" + from typing import Any from hass_nabucasa import Cloud diff --git a/homeassistant/components/cloud/tts.py b/homeassistant/components/cloud/tts.py index 59ae5b22214..42e4b94a189 100644 --- a/homeassistant/components/cloud/tts.py +++ b/homeassistant/components/cloud/tts.py @@ -1,4 +1,5 @@ """Support for the cloud for text-to-speech service.""" + from __future__ import annotations import asyncio @@ -6,7 +7,7 @@ import logging from typing import Any from hass_nabucasa import Cloud -from hass_nabucasa.voice import MAP_VOICE, TTS_VOICES, AudioOutput, VoiceError +from hass_nabucasa.voice import MAP_VOICE, TTS_VOICES, AudioOutput, Gender, VoiceError import voluptuous as vol from homeassistant.components.tts import ( @@ -96,17 +97,7 @@ async def async_get_engine( ) -> CloudProvider: """Set up Cloud speech component.""" cloud: Cloud[CloudClient] = hass.data[DOMAIN] - - language: str | None - gender: str | None - if discovery_info is not None: - language = None - gender = None - else: - language = config[CONF_LANG] - gender = config[ATTR_GENDER] - - cloud_provider = CloudProvider(cloud, language, gender) + cloud_provider = CloudProvider(cloud) if discovery_info is not None: discovery_info["platform_loaded"].set() return cloud_provider @@ -133,11 +124,11 @@ class CloudTTSEntity(TextToSpeechEntity): def __init__(self, cloud: Cloud[CloudClient]) -> None: """Initialize cloud text-to-speech entity.""" self.cloud = cloud - self._language, self._gender = cloud.client.prefs.tts_default_voice + self._language, self._voice = cloud.client.prefs.tts_default_voice async def _sync_prefs(self, prefs: CloudPreferences) -> None: """Sync preferences.""" - self._language, self._gender = prefs.tts_default_voice + self._language, self._voice = prefs.tts_default_voice @property def default_language(self) -> str: @@ -148,7 +139,6 @@ class CloudTTSEntity(TextToSpeechEntity): def default_options(self) -> dict[str, Any]: """Return a dict include default options.""" return { - ATTR_GENDER: self._gender, ATTR_AUDIO_OUTPUT: AudioOutput.MP3, } @@ -160,6 +150,7 @@ class CloudTTSEntity(TextToSpeechEntity): @property def supported_options(self) -> list[str]: """Return list of supported options like voice, emotion.""" + # The gender option is deprecated and will be removed in 2024.10.0. return [ATTR_GENDER, ATTR_VOICE, ATTR_AUDIO_OUTPUT] async def async_added_to_hass(self) -> None: @@ -183,14 +174,27 @@ class CloudTTSEntity(TextToSpeechEntity): self, message: str, language: str, options: dict[str, Any] ) -> TtsAudioType: """Load TTS from Home Assistant Cloud.""" + gender: Gender | str | None = options.get(ATTR_GENDER) + gender = handle_deprecated_gender(self.hass, gender) original_voice: str | None = options.get(ATTR_VOICE) + if original_voice is None and language == self._language: + original_voice = self._voice voice = handle_deprecated_voice(self.hass, original_voice) + if voice not in TTS_VOICES[language]: + default_voice = TTS_VOICES[language][0] + _LOGGER.debug( + "Unsupported voice %s detected, falling back to default %s for %s", + voice, + default_voice, + language, + ) + voice = default_voice # Process TTS try: data = await self.cloud.voice.process_tts( text=message, language=language, - gender=options.get(ATTR_GENDER), + gender=gender, voice=voice, output=options[ATTR_AUDIO_OUTPUT], ) @@ -204,24 +208,16 @@ class CloudTTSEntity(TextToSpeechEntity): class CloudProvider(Provider): """Home Assistant Cloud speech API provider.""" - def __init__( - self, cloud: Cloud[CloudClient], language: str | None, gender: str | None - ) -> None: + def __init__(self, cloud: Cloud[CloudClient]) -> None: """Initialize cloud provider.""" self.cloud = cloud self.name = "Cloud" - self._language = language - self._gender = gender - - if self._language is not None: - return - - self._language, self._gender = cloud.client.prefs.tts_default_voice + self._language, self._voice = cloud.client.prefs.tts_default_voice cloud.client.prefs.async_listen_updates(self._sync_prefs) async def _sync_prefs(self, prefs: CloudPreferences) -> None: """Sync preferences.""" - self._language, self._gender = prefs.tts_default_voice + self._language, self._voice = prefs.tts_default_voice @property def default_language(self) -> str | None: @@ -236,6 +232,7 @@ class CloudProvider(Provider): @property def supported_options(self) -> list[str]: """Return list of supported options like voice, emotion.""" + # The gender option is deprecated and will be removed in 2024.10.0. return [ATTR_GENDER, ATTR_VOICE, ATTR_AUDIO_OUTPUT] @callback @@ -249,7 +246,6 @@ class CloudProvider(Provider): def default_options(self) -> dict[str, Any]: """Return a dict include default options.""" return { - ATTR_GENDER: self._gender, ATTR_AUDIO_OUTPUT: AudioOutput.MP3, } @@ -257,15 +253,28 @@ class CloudProvider(Provider): self, message: str, language: str, options: dict[str, Any] ) -> TtsAudioType: """Load TTS from Home Assistant Cloud.""" - original_voice: str | None = options.get(ATTR_VOICE) assert self.hass is not None + gender: Gender | str | None = options.get(ATTR_GENDER) + gender = handle_deprecated_gender(self.hass, gender) + original_voice: str | None = options.get(ATTR_VOICE) + if original_voice is None and language == self._language: + original_voice = self._voice voice = handle_deprecated_voice(self.hass, original_voice) + if voice not in TTS_VOICES[language]: + default_voice = TTS_VOICES[language][0] + _LOGGER.debug( + "Unsupported voice %s detected, falling back to default %s for %s", + voice, + default_voice, + language, + ) + voice = default_voice # Process TTS try: data = await self.cloud.voice.process_tts( text=message, language=language, - gender=options.get(ATTR_GENDER), + gender=gender, voice=voice, output=options[ATTR_AUDIO_OUTPUT], ) @@ -276,6 +285,32 @@ class CloudProvider(Provider): return (str(options[ATTR_AUDIO_OUTPUT].value), data) +@callback +def handle_deprecated_gender( + hass: HomeAssistant, + gender: Gender | str | None, +) -> Gender | None: + """Handle deprecated gender.""" + if gender is None: + return None + async_create_issue( + hass, + DOMAIN, + "deprecated_gender", + is_fixable=True, + is_persistent=True, + severity=IssueSeverity.WARNING, + breaks_in_ha_version="2024.10.0", + translation_key="deprecated_gender", + translation_placeholders={ + "integration_name": "Home Assistant Cloud", + "deprecated_option": "gender", + "replacement_option": "voice", + }, + ) + return Gender(gender) + + @callback def handle_deprecated_voice( hass: HomeAssistant, diff --git a/homeassistant/components/cloudflare/__init__.py b/homeassistant/components/cloudflare/__init__.py index d4c6775c6b9..5934e43f8a2 100644 --- a/homeassistant/components/cloudflare/__init__.py +++ b/homeassistant/components/cloudflare/__init__.py @@ -1,4 +1,5 @@ """Update the IP addresses of your Cloudflare DNS records.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/cloudflare/config_flow.py b/homeassistant/components/cloudflare/config_flow.py index 99f6109be4a..f4becf12067 100644 --- a/homeassistant/components/cloudflare/config_flow.py +++ b/homeassistant/components/cloudflare/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Cloudflare integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -9,10 +10,9 @@ import pycfdns import voluptuous as vol from homeassistant.components import persistent_notification -from homeassistant.config_entries import ConfigEntry, ConfigFlow +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_API_TOKEN, CONF_ZONE from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -85,14 +85,16 @@ class CloudflareConfigFlow(ConfigFlow, domain=DOMAIN): self.zones: list[pycfdns.ZoneModel] | None = None self.records: list[pycfdns.RecordModel] | None = None - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Handle initiation of re-authentication with Cloudflare.""" self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) return await self.async_step_reauth_confirm() async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle re-authentication with Cloudflare.""" errors: dict[str, str] = {} @@ -122,7 +124,7 @@ class CloudflareConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initiated by the user.""" if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") @@ -145,7 +147,7 @@ class CloudflareConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_zone( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the picking the zone.""" errors: dict[str, str] = {} @@ -167,7 +169,7 @@ class CloudflareConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_records( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the picking the zone records.""" if user_input is not None: diff --git a/homeassistant/components/cloudflare/helpers.py b/homeassistant/components/cloudflare/helpers.py index 0542bce0980..937f6036703 100644 --- a/homeassistant/components/cloudflare/helpers.py +++ b/homeassistant/components/cloudflare/helpers.py @@ -1,4 +1,5 @@ """Helpers for the CloudFlare integration.""" + import pycfdns diff --git a/homeassistant/components/cmus/media_player.py b/homeassistant/components/cmus/media_player.py index 65bfef3a0cb..ca9ad8f8489 100644 --- a/homeassistant/components/cmus/media_player.py +++ b/homeassistant/components/cmus/media_player.py @@ -1,4 +1,5 @@ """Support for interacting with and controlling the cmus music player.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/co2signal/__init__.py b/homeassistant/components/co2signal/__init__.py index 028d37a73c5..087b3148ea7 100644 --- a/homeassistant/components/co2signal/__init__.py +++ b/homeassistant/components/co2signal/__init__.py @@ -1,4 +1,5 @@ """The CO2 Signal integration.""" + from __future__ import annotations from aioelectricitymaps import ElectricityMaps diff --git a/homeassistant/components/co2signal/config_flow.py b/homeassistant/components/co2signal/config_flow.py index a952f016671..bf5d645638f 100644 --- a/homeassistant/components/co2signal/config_flow.py +++ b/homeassistant/components/co2signal/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Co2signal integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -12,15 +13,13 @@ from aioelectricitymaps import ( ) import voluptuous as vol -from homeassistant import config_entries -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.const import ( CONF_API_KEY, CONF_COUNTRY_CODE, CONF_LATITUDE, CONF_LONGITUDE, ) -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.selector import ( @@ -38,7 +37,7 @@ TYPE_SPECIFY_COORDINATES = "specify_coordinates" TYPE_SPECIFY_COUNTRY = "specify_country_code" -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class ElectricityMapsConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Co2signal.""" VERSION = 1 @@ -47,7 +46,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" data_schema = vol.Schema( { @@ -86,7 +85,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_coordinates( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Validate coordinates.""" data_schema = vol.Schema( { @@ -109,7 +108,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_country( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Validate country.""" data_schema = vol.Schema( { @@ -125,7 +124,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): "country", data_schema, {**self._data, **user_input} ) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Handle the reauth step.""" self._reauth_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] @@ -140,7 +141,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def _validate_and_create( self, step_id: str, data_schema: vol.Schema, data: Mapping[str, Any] - ) -> FlowResult: + ) -> ConfigFlowResult: """Validate data and show form if it is invalid.""" errors: dict[str, str] = {} diff --git a/homeassistant/components/co2signal/const.py b/homeassistant/components/co2signal/const.py index b025c655ce6..34ddbdc05e5 100644 --- a/homeassistant/components/co2signal/const.py +++ b/homeassistant/components/co2signal/const.py @@ -1,5 +1,4 @@ """Constants for the Co2signal integration.""" - DOMAIN = "co2signal" ATTRIBUTION = "Data provided by Electricity Maps" diff --git a/homeassistant/components/co2signal/coordinator.py b/homeassistant/components/co2signal/coordinator.py index b06bee38bc4..475ebd1225d 100644 --- a/homeassistant/components/co2signal/coordinator.py +++ b/homeassistant/components/co2signal/coordinator.py @@ -1,4 +1,5 @@ """DataUpdateCoordinator for the co2signal integration.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/co2signal/diagnostics.py b/homeassistant/components/co2signal/diagnostics.py index 1c53f7c5b08..4e553f0c7da 100644 --- a/homeassistant/components/co2signal/diagnostics.py +++ b/homeassistant/components/co2signal/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for CO2Signal.""" + from __future__ import annotations from dataclasses import asdict diff --git a/homeassistant/components/co2signal/helpers.py b/homeassistant/components/co2signal/helpers.py index f61fadaf88c..3feabef2fdd 100644 --- a/homeassistant/components/co2signal/helpers.py +++ b/homeassistant/components/co2signal/helpers.py @@ -1,4 +1,5 @@ """Helper functions for the CO2 Signal integration.""" + from __future__ import annotations from collections.abc import Mapping diff --git a/homeassistant/components/co2signal/sensor.py b/homeassistant/components/co2signal/sensor.py index bff17becede..5b11fd85827 100644 --- a/homeassistant/components/co2signal/sensor.py +++ b/homeassistant/components/co2signal/sensor.py @@ -1,4 +1,5 @@ """Support for the CO2signal platform.""" + from __future__ import annotations from collections.abc import Callable @@ -28,9 +29,9 @@ class CO2SensorEntityDescription(SensorEntityDescription): # For backwards compat, allow description to override unique ID key to use unique_id: str | None = None - unit_of_measurement_fn: Callable[ - [CarbonIntensityResponse], str | None - ] | None = None + unit_of_measurement_fn: Callable[[CarbonIntensityResponse], str | None] | None = ( + None + ) value_fn: Callable[[CarbonIntensityResponse], float | None] diff --git a/homeassistant/components/co2signal/util.py b/homeassistant/components/co2signal/util.py index b588e0abef9..5ec1f79c466 100644 --- a/homeassistant/components/co2signal/util.py +++ b/homeassistant/components/co2signal/util.py @@ -1,4 +1,5 @@ """Utils for CO2 signal.""" + from __future__ import annotations from collections.abc import Mapping diff --git a/homeassistant/components/coinbase/__init__.py b/homeassistant/components/coinbase/__init__.py index 40c8ca0c65a..0a34168b4ee 100644 --- a/homeassistant/components/coinbase/__init__.py +++ b/homeassistant/components/coinbase/__init__.py @@ -1,4 +1,5 @@ """The Coinbase integration.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/coinbase/config_flow.py b/homeassistant/components/coinbase/config_flow.py index 38053295411..dafa50bafcb 100644 --- a/homeassistant/components/coinbase/config_flow.py +++ b/homeassistant/components/coinbase/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Coinbase integration.""" + from __future__ import annotations import logging @@ -8,10 +9,15 @@ from coinbase.wallet.client import Client from coinbase.wallet.error import AuthenticationError import voluptuous as vol -from homeassistant import config_entries, core, exceptions +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.const import CONF_API_KEY, CONF_API_TOKEN -from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv from . import get_accounts @@ -48,7 +54,7 @@ def get_user_from_client(api_key, api_token): return user -async def validate_api(hass: core.HomeAssistant, data): +async def validate_api(hass: HomeAssistant, data): """Validate the credentials.""" try: @@ -72,9 +78,7 @@ async def validate_api(hass: core.HomeAssistant, data): return {"title": user["name"]} -async def validate_options( - hass: core.HomeAssistant, config_entry: config_entries.ConfigEntry, options -): +async def validate_options(hass: HomeAssistant, config_entry: ConfigEntry, options): """Validate the requested resources are provided by API.""" client = hass.data[DOMAIN][config_entry.entry_id].client @@ -100,14 +104,14 @@ async def validate_options( return True -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class CoinbaseConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Coinbase.""" VERSION = 1 async def async_step_user( self, user_input: dict[str, str] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors: dict[str, str] = {} if user_input is None: @@ -139,22 +143,22 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback def async_get_options_flow( - config_entry: config_entries.ConfigEntry, + config_entry: ConfigEntry, ) -> OptionsFlowHandler: """Get the options flow for this handler.""" return OptionsFlowHandler(config_entry) -class OptionsFlowHandler(config_entries.OptionsFlow): +class OptionsFlowHandler(OptionsFlow): """Handle a option flow for Coinbase.""" - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + 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: + ) -> ConfigFlowResult: """Manage the options.""" errors = {} @@ -216,29 +220,29 @@ class OptionsFlowHandler(config_entries.OptionsFlow): ) -class CannotConnect(exceptions.HomeAssistantError): +class CannotConnect(HomeAssistantError): """Error to indicate we cannot connect.""" -class InvalidAuth(exceptions.HomeAssistantError): +class InvalidAuth(HomeAssistantError): """Error to indicate there is invalid auth.""" -class InvalidSecret(exceptions.HomeAssistantError): +class InvalidSecret(HomeAssistantError): """Error to indicate auth failed due to invalid secret.""" -class InvalidKey(exceptions.HomeAssistantError): +class InvalidKey(HomeAssistantError): """Error to indicate auth failed due to invalid key.""" -class AlreadyConfigured(exceptions.HomeAssistantError): +class AlreadyConfigured(HomeAssistantError): """Error to indicate Coinbase API Key is already configured.""" -class CurrencyUnavailable(exceptions.HomeAssistantError): +class CurrencyUnavailable(HomeAssistantError): """Error to indicate the requested currency resource is not provided by the API.""" -class ExchangeRateUnavailable(exceptions.HomeAssistantError): +class ExchangeRateUnavailable(HomeAssistantError): """Error to indicate the requested exchange rate resource is not provided by the API.""" diff --git a/homeassistant/components/coinbase/manifest.json b/homeassistant/components/coinbase/manifest.json index dbb40b24fcc..515fe9f9abb 100644 --- a/homeassistant/components/coinbase/manifest.json +++ b/homeassistant/components/coinbase/manifest.json @@ -4,7 +4,6 @@ "codeowners": ["@tombrien"], "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/coinbase", - "import_executor": true, "iot_class": "cloud_polling", "loggers": ["coinbase"], "requirements": ["coinbase==2.1.0"] diff --git a/homeassistant/components/coinbase/sensor.py b/homeassistant/components/coinbase/sensor.py index 1442a626f74..83c63fa55fb 100644 --- a/homeassistant/components/coinbase/sensor.py +++ b/homeassistant/components/coinbase/sensor.py @@ -1,4 +1,5 @@ """Support for Coinbase sensors.""" + from __future__ import annotations import logging @@ -84,13 +85,12 @@ async def async_setup_entry( entities.append(AccountSensor(instance, currency)) if CONF_EXCHANGE_RATES in config_entry.options: - rate: str - for rate in config_entry.options[CONF_EXCHANGE_RATES]: - entities.append( - ExchangeRateSensor( - instance, rate, exchange_base_currency, exchange_precision - ) + entities.extend( + ExchangeRateSensor( + instance, rate, exchange_base_currency, exchange_precision ) + for rate in config_entry.options[CONF_EXCHANGE_RATES] + ) async_add_entities(entities) diff --git a/homeassistant/components/color_extractor/__init__.py b/homeassistant/components/color_extractor/__init__.py index e6095c9f925..56270ce0f75 100644 --- a/homeassistant/components/color_extractor/__init__.py +++ b/homeassistant/components/color_extractor/__init__.py @@ -1,4 +1,5 @@ """Module for color_extractor (RGB extraction from images) component.""" + import asyncio import io import logging diff --git a/homeassistant/components/color_extractor/config_flow.py b/homeassistant/components/color_extractor/config_flow.py index 32b803d14f9..de1f9cb35be 100644 --- a/homeassistant/components/color_extractor/config_flow.py +++ b/homeassistant/components/color_extractor/config_flow.py @@ -1,11 +1,12 @@ """Config flow to configure the Color extractor integration.""" + from __future__ import annotations from typing import Any -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN -from homeassistant.data_entry_flow import FlowResult, FlowResultType +from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from .const import DEFAULT_NAME, DOMAIN @@ -18,7 +19,7 @@ class ColorExtractorConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") @@ -28,7 +29,7 @@ class ColorExtractorConfigFlow(ConfigFlow, domain=DOMAIN): return self.async_show_form(step_id="user") - async def async_step_import(self, user_input: dict[str, Any]) -> FlowResult: + async def async_step_import(self, user_input: dict[str, Any]) -> ConfigFlowResult: """Handle import from configuration.yaml.""" result = await self.async_step_user(user_input) if result["type"] == FlowResultType.CREATE_ENTRY: diff --git a/homeassistant/components/color_extractor/const.py b/homeassistant/components/color_extractor/const.py index e783dcb533d..25c15ed9dc0 100644 --- a/homeassistant/components/color_extractor/const.py +++ b/homeassistant/components/color_extractor/const.py @@ -1,4 +1,5 @@ """Constants for the color_extractor component.""" + ATTR_PATH = "color_extract_path" ATTR_URL = "color_extract_url" diff --git a/homeassistant/components/comed_hourly_pricing/sensor.py b/homeassistant/components/comed_hourly_pricing/sensor.py index 195bfa97b7d..5d30387a9cb 100644 --- a/homeassistant/components/comed_hourly_pricing/sensor.py +++ b/homeassistant/components/comed_hourly_pricing/sensor.py @@ -1,4 +1,5 @@ """Support for ComEd Hourly Pricing data.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/comelit/__init__.py b/homeassistant/components/comelit/__init__.py index 2cf7a145eee..478be85c1d4 100644 --- a/homeassistant/components/comelit/__init__.py +++ b/homeassistant/components/comelit/__init__.py @@ -1,6 +1,5 @@ """Comelit integration.""" - from aiocomelit.const import BRIDGE from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/comelit/alarm_control_panel.py b/homeassistant/components/comelit/alarm_control_panel.py index 33107dd3e82..b325de25e97 100644 --- a/homeassistant/components/comelit/alarm_control_panel.py +++ b/homeassistant/components/comelit/alarm_control_panel.py @@ -1,4 +1,5 @@ """Support for Comelit VEDO system.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/comelit/climate.py b/homeassistant/components/comelit/climate.py index 5a879bc2d24..0b88367c0fa 100644 --- a/homeassistant/components/comelit/climate.py +++ b/homeassistant/components/comelit/climate.py @@ -1,4 +1,5 @@ """Support for climates.""" + from __future__ import annotations from enum import StrEnum @@ -24,7 +25,7 @@ from .const import DOMAIN from .coordinator import ComelitSerialBridge -class ClimaMode(StrEnum): +class ClimaComelitMode(StrEnum): """Serial Bridge clima modes.""" AUTO = "A" @@ -33,8 +34,8 @@ class ClimaMode(StrEnum): UPPER = "U" -class ClimaAction(StrEnum): - """Serial Bridge clima actions.""" +class ClimaComelitCommand(StrEnum): + """Serial Bridge clima commands.""" OFF = "off" ON = "on" @@ -44,28 +45,28 @@ class ClimaAction(StrEnum): API_STATUS: dict[str, dict[str, Any]] = { - ClimaMode.OFF: { + ClimaComelitMode.OFF: { "action": "off", "hvac_mode": HVACMode.OFF, "hvac_action": HVACAction.OFF, }, - ClimaMode.LOWER: { + ClimaComelitMode.LOWER: { "action": "lower", "hvac_mode": HVACMode.COOL, "hvac_action": HVACAction.COOLING, }, - ClimaMode.UPPER: { + ClimaComelitMode.UPPER: { "action": "upper", "hvac_mode": HVACMode.HEAT, "hvac_action": HVACAction.HEATING, }, } -MODE_TO_ACTION: dict[HVACMode, ClimaAction] = { - HVACMode.OFF: ClimaAction.OFF, - HVACMode.AUTO: ClimaAction.AUTO, - HVACMode.COOL: ClimaAction.MANUAL, - HVACMode.HEAT: ClimaAction.MANUAL, +MODE_TO_ACTION: dict[HVACMode, ClimaComelitCommand] = { + HVACMode.OFF: ClimaComelitCommand.OFF, + HVACMode.AUTO: ClimaComelitCommand.AUTO, + HVACMode.COOL: ClimaComelitCommand.MANUAL, + HVACMode.HEAT: ClimaComelitCommand.MANUAL, } @@ -138,7 +139,7 @@ class ComelitClimateEntity(CoordinatorEntity[ComelitSerialBridge], ClimateEntity @property def _api_automatic(self) -> bool: """Return device in automatic/manual mode.""" - return self._clima[3] == ClimaMode.AUTO + return self._clima[3] == ClimaComelitMode.AUTO @property def target_temperature(self) -> float: @@ -154,7 +155,7 @@ class ComelitClimateEntity(CoordinatorEntity[ComelitSerialBridge], ClimateEntity def hvac_mode(self) -> HVACMode | None: """HVAC current mode.""" - if self._api_mode == ClimaMode.OFF: + if self._api_mode == ClimaComelitMode.OFF: return HVACMode.OFF if self._api_automatic: @@ -169,7 +170,7 @@ class ComelitClimateEntity(CoordinatorEntity[ComelitSerialBridge], ClimateEntity def hvac_action(self) -> HVACAction | None: """HVAC current action.""" - if self._api_mode == ClimaMode.OFF: + if self._api_mode == ClimaComelitMode.OFF: return HVACAction.OFF if not self._api_active: @@ -188,10 +189,10 @@ class ComelitClimateEntity(CoordinatorEntity[ComelitSerialBridge], ClimateEntity return await self.coordinator.api.set_clima_status( - self._device.index, ClimaAction.MANUAL + self._device.index, ClimaComelitCommand.MANUAL ) await self.coordinator.api.set_clima_status( - self._device.index, ClimaAction.SET, target_temp + self._device.index, ClimaComelitCommand.SET, target_temp ) async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: @@ -199,7 +200,7 @@ class ComelitClimateEntity(CoordinatorEntity[ComelitSerialBridge], ClimateEntity if hvac_mode != HVACMode.OFF: await self.coordinator.api.set_clima_status( - self._device.index, ClimaAction.ON + self._device.index, ClimaComelitCommand.ON ) await self.coordinator.api.set_clima_status( self._device.index, MODE_TO_ACTION[hvac_mode] diff --git a/homeassistant/components/comelit/config_flow.py b/homeassistant/components/comelit/config_flow.py index bbb671a29a7..53d08e0097c 100644 --- a/homeassistant/components/comelit/config_flow.py +++ b/homeassistant/components/comelit/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Comelit integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -13,10 +14,10 @@ from aiocomelit.api import ComelitCommonApi from aiocomelit.const import BRIDGE import voluptuous as vol -from homeassistant import core, exceptions -from homeassistant.config_entries import ConfigEntry, ConfigFlow +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_PIN, CONF_PORT, CONF_TYPE -from homeassistant.data_entry_flow import FlowResult +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv from .const import _LOGGER, DEFAULT_PORT, DEVICE_TYPE_LIST, DOMAIN @@ -41,9 +42,7 @@ def user_form_schema(user_input: dict[str, Any] | None) -> vol.Schema: STEP_REAUTH_DATA_SCHEMA = vol.Schema({vol.Required(CONF_PIN): cv.positive_int}) -async def validate_input( - hass: core.HomeAssistant, data: dict[str, Any] -) -> dict[str, str]: +async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, str]: """Validate the user input allows us to connect.""" api: ComelitCommonApi @@ -76,7 +75,7 @@ class ComelitConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" if user_input is None: return self.async_show_form( @@ -103,7 +102,9 @@ class ComelitConfigFlow(ConfigFlow, domain=DOMAIN): step_id="user", data_schema=user_form_schema(user_input), errors=errors ) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Handle reauth flow.""" self._reauth_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] @@ -117,7 +118,7 @@ class ComelitConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle reauth confirm.""" assert self._reauth_entry errors = {} @@ -163,9 +164,9 @@ class ComelitConfigFlow(ConfigFlow, domain=DOMAIN): ) -class CannotConnect(exceptions.HomeAssistantError): +class CannotConnect(HomeAssistantError): """Error to indicate we cannot connect.""" -class InvalidAuth(exceptions.HomeAssistantError): +class InvalidAuth(HomeAssistantError): """Error to indicate there is invalid auth.""" diff --git a/homeassistant/components/comelit/const.py b/homeassistant/components/comelit/const.py index ca10e0b0a74..84d8fbd6315 100644 --- a/homeassistant/components/comelit/const.py +++ b/homeassistant/components/comelit/const.py @@ -1,4 +1,5 @@ """Comelit constants.""" + import logging from aiocomelit.const import BRIDGE, VEDO diff --git a/homeassistant/components/comelit/coordinator.py b/homeassistant/components/comelit/coordinator.py index fe23cb1f5d3..807f389a6d3 100644 --- a/homeassistant/components/comelit/coordinator.py +++ b/homeassistant/components/comelit/coordinator.py @@ -1,4 +1,5 @@ """Support for Comelit.""" + from abc import abstractmethod from datetime import timedelta from typing import Any diff --git a/homeassistant/components/comelit/cover.py b/homeassistant/components/comelit/cover.py index d35180c761b..011ed81b5cb 100644 --- a/homeassistant/components/comelit/cover.py +++ b/homeassistant/components/comelit/cover.py @@ -1,4 +1,5 @@ """Support for covers.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/comelit/humidifier.py b/homeassistant/components/comelit/humidifier.py index 8ec2e9fd28b..e7857535c78 100644 --- a/homeassistant/components/comelit/humidifier.py +++ b/homeassistant/components/comelit/humidifier.py @@ -1,4 +1,5 @@ """Support for humidifiers.""" + from __future__ import annotations from enum import StrEnum @@ -147,12 +148,12 @@ class ComelitHumidifierEntity(CoordinatorEntity[ComelitSerialBridge], Humidifier return self._humidifier[3] == HumidifierComelitMode.AUTO @property - def target_humidity(self) -> int: + def target_humidity(self) -> float: """Set target humidity.""" return self._humidifier[4] / 10 @property - def current_humidity(self) -> int: + def current_humidity(self) -> float: """Return current humidity.""" return self._humidifier[0] / 10 diff --git a/homeassistant/components/comelit/light.py b/homeassistant/components/comelit/light.py index a1743bff12d..bb5eb5fa160 100644 --- a/homeassistant/components/comelit/light.py +++ b/homeassistant/components/comelit/light.py @@ -1,4 +1,5 @@ """Support for lights.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/comelit/sensor.py b/homeassistant/components/comelit/sensor.py index 7cdb0535f8c..a86d49d73e9 100644 --- a/homeassistant/components/comelit/sensor.py +++ b/homeassistant/components/comelit/sensor.py @@ -1,4 +1,5 @@ """Support for sensors.""" + from __future__ import annotations from typing import Final diff --git a/homeassistant/components/comelit/switch.py b/homeassistant/components/comelit/switch.py index ce08c64fa78..68ba934adb6 100644 --- a/homeassistant/components/comelit/switch.py +++ b/homeassistant/components/comelit/switch.py @@ -1,4 +1,5 @@ """Support for switches.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/comfoconnect/__init__.py b/homeassistant/components/comfoconnect/__init__.py index 5ff34526cc0..118b59d6cae 100644 --- a/homeassistant/components/comfoconnect/__init__.py +++ b/homeassistant/components/comfoconnect/__init__.py @@ -1,4 +1,5 @@ """Support to control a Zehnder ComfoAir Q350/450/600 ventilation unit.""" + import logging from pycomfoconnect import Bridge, ComfoConnect diff --git a/homeassistant/components/comfoconnect/fan.py b/homeassistant/components/comfoconnect/fan.py index f76ed5939f5..f0d261ab968 100644 --- a/homeassistant/components/comfoconnect/fan.py +++ b/homeassistant/components/comfoconnect/fan.py @@ -1,4 +1,5 @@ """Platform to control a Zehnder ComfoAir Q350/450/600 ventilation unit.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/comfoconnect/sensor.py b/homeassistant/components/comfoconnect/sensor.py index 421643f5ced..97cb7fc61eb 100644 --- a/homeassistant/components/comfoconnect/sensor.py +++ b/homeassistant/components/comfoconnect/sensor.py @@ -1,4 +1,5 @@ """Platform to control a Zehnder ComfoAir Q350/450/600 ventilation unit.""" + from __future__ import annotations from dataclasses import dataclass @@ -79,19 +80,11 @@ ATTR_SUPPLY_TEMPERATURE = "supply_temperature" _LOGGER = logging.getLogger(__name__) -@dataclass(frozen=True) -class ComfoconnectRequiredKeysMixin: - """Mixin for required keys.""" - - sensor_id: int - - -@dataclass(frozen=True) -class ComfoconnectSensorEntityDescription( - SensorEntityDescription, ComfoconnectRequiredKeysMixin -): +@dataclass(frozen=True, kw_only=True) +class ComfoconnectSensorEntityDescription(SensorEntityDescription): """Describes Comfoconnect sensor entity.""" + sensor_id: int multiplier: float = 1 diff --git a/homeassistant/components/command_line/__init__.py b/homeassistant/components/command_line/__init__.py index 701391ab389..27b69e59ca4 100644 --- a/homeassistant/components/command_line/__init__.py +++ b/homeassistant/components/command_line/__init__.py @@ -1,4 +1,5 @@ """The command_line component.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/command_line/binary_sensor.py b/homeassistant/components/command_line/binary_sensor.py index 20b538fc4d7..a31f8205a28 100644 --- a/homeassistant/components/command_line/binary_sensor.py +++ b/homeassistant/components/command_line/binary_sensor.py @@ -1,4 +1,5 @@ """Support for custom shell commands to retrieve values.""" + from __future__ import annotations import asyncio @@ -148,7 +149,7 @@ class CommandBinarySensor(ManualTriggerEntity, BinarySensorEntity): async def _async_update(self) -> None: """Get the latest data and updates the state.""" - await self.hass.async_add_executor_job(self.data.update) + await self.data.async_update() value = self.data.value if self._value_template is not None: diff --git a/homeassistant/components/command_line/cover.py b/homeassistant/components/command_line/cover.py index 845de352d73..c27cd97b39a 100644 --- a/homeassistant/components/command_line/cover.py +++ b/homeassistant/components/command_line/cover.py @@ -1,4 +1,5 @@ """Support for command line covers.""" + from __future__ import annotations import asyncio @@ -28,7 +29,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import dt as dt_util, slugify from .const import CONF_COMMAND_TIMEOUT, LOGGER -from .utils import call_shell_with_timeout, check_output_or_log +from .utils import async_call_shell_with_timeout, async_check_output_or_log SCAN_INTERVAL = timedelta(seconds=15) @@ -114,11 +115,11 @@ class CommandCover(ManualTriggerEntity, CoverEntity): ), ) - def _move_cover(self, command: str) -> bool: + async def _async_move_cover(self, command: str) -> bool: """Execute the actual commands.""" LOGGER.info("Running command: %s", command) - returncode = call_shell_with_timeout(command, self._timeout) + returncode = await async_call_shell_with_timeout(command, self._timeout) success = returncode == 0 if not success: @@ -143,11 +144,11 @@ class CommandCover(ManualTriggerEntity, CoverEntity): """ return self._state - def _query_state(self) -> str | None: + async def _async_query_state(self) -> str | None: """Query for the state.""" if self._command_state: LOGGER.info("Running state value command: %s", self._command_state) - return check_output_or_log(self._command_state, self._timeout) + return await async_check_output_or_log(self._command_state, self._timeout) if TYPE_CHECKING: return None @@ -169,7 +170,7 @@ class CommandCover(ManualTriggerEntity, CoverEntity): async def _async_update(self) -> None: """Update device state.""" if self._command_state: - payload = str(await self.hass.async_add_executor_job(self._query_state)) + payload = str(await self._async_query_state()) if self._value_template: payload = self._value_template.async_render_with_possible_json_value( payload, None @@ -189,15 +190,15 @@ class CommandCover(ManualTriggerEntity, CoverEntity): async def async_open_cover(self, **kwargs: Any) -> None: """Open the cover.""" - await self.hass.async_add_executor_job(self._move_cover, self._command_open) + await self._async_move_cover(self._command_open) await self._update_entity_state() async def async_close_cover(self, **kwargs: Any) -> None: """Close the cover.""" - await self.hass.async_add_executor_job(self._move_cover, self._command_close) + await self._async_move_cover(self._command_close) await self._update_entity_state() async def async_stop_cover(self, **kwargs: Any) -> None: """Stop the cover.""" - await self.hass.async_add_executor_job(self._move_cover, self._command_stop) + await self._async_move_cover(self._command_stop) await self._update_entity_state() diff --git a/homeassistant/components/command_line/icons.json b/homeassistant/components/command_line/icons.json new file mode 100644 index 00000000000..a03163179cb --- /dev/null +++ b/homeassistant/components/command_line/icons.json @@ -0,0 +1,5 @@ +{ + "services": { + "reload": "mdi:reload" + } +} diff --git a/homeassistant/components/command_line/notify.py b/homeassistant/components/command_line/notify.py index f61e9959af9..1d025726583 100644 --- a/homeassistant/components/command_line/notify.py +++ b/homeassistant/components/command_line/notify.py @@ -1,4 +1,5 @@ """Support for command line notification services.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/command_line/sensor.py b/homeassistant/components/command_line/sensor.py index c1d60b9d2fd..4cfd9af0811 100644 --- a/homeassistant/components/command_line/sensor.py +++ b/homeassistant/components/command_line/sensor.py @@ -1,4 +1,5 @@ """Allows to configure custom shell commands to turn a value for a sensor.""" + from __future__ import annotations import asyncio @@ -33,7 +34,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import dt as dt_util from .const import CONF_COMMAND_TIMEOUT, LOGGER -from .utils import check_output_or_log +from .utils import async_check_output_or_log CONF_JSON_ATTRIBUTES = "json_attributes" @@ -138,6 +139,7 @@ class CommandSensor(ManualTriggerSensorEntity): """Update the state of the entity.""" if self._process_updates is None: self._process_updates = asyncio.Lock() + if self._process_updates.locked(): LOGGER.warning( "Updating Command Line Sensor %s took longer than the scheduled update interval %s", @@ -151,7 +153,7 @@ class CommandSensor(ManualTriggerSensorEntity): async def _async_update(self) -> None: """Get the latest data and updates the state.""" - await self.hass.async_add_executor_job(self.data.update) + await self.data.async_update() value = self.data.value if self._json_attributes: @@ -216,7 +218,7 @@ class CommandSensorData: self.command = command self.timeout = command_timeout - def update(self) -> None: + async def async_update(self) -> None: """Get the latest data with a shell command.""" command = self.command @@ -231,7 +233,7 @@ class CommandSensorData: if args_compiled: try: args_to_render = {"arguments": args} - rendered_args = args_compiled.render(args_to_render) + rendered_args = args_compiled.async_render(args_to_render) except TemplateError as ex: LOGGER.exception("Error rendering command template: %s", ex) return @@ -246,4 +248,4 @@ class CommandSensorData: command = f"{prog} {rendered_args}" LOGGER.debug("Running command: %s", command) - self.value = check_output_or_log(command, self.timeout) + self.value = await async_check_output_or_log(command, self.timeout) diff --git a/homeassistant/components/command_line/switch.py b/homeassistant/components/command_line/switch.py index efeded194ce..f84c55d0320 100644 --- a/homeassistant/components/command_line/switch.py +++ b/homeassistant/components/command_line/switch.py @@ -1,4 +1,5 @@ """Support for custom shell commands to turn a switch on/off.""" + from __future__ import annotations import asyncio @@ -28,7 +29,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import dt as dt_util, slugify from .const import CONF_COMMAND_TIMEOUT, LOGGER -from .utils import call_shell_with_timeout, check_output_or_log +from .utils import async_call_shell_with_timeout, async_check_output_or_log SCAN_INTERVAL = timedelta(seconds=30) @@ -121,28 +122,26 @@ class CommandSwitch(ManualTriggerEntity, SwitchEntity): """Execute the actual commands.""" LOGGER.info("Running command: %s", command) - success = ( - await self.hass.async_add_executor_job( - call_shell_with_timeout, command, self._timeout - ) - == 0 - ) + success = await async_call_shell_with_timeout(command, self._timeout) == 0 if not success: LOGGER.error("Command failed: %s", command) return success - def _query_state_value(self, command: str) -> str | None: + async def _async_query_state_value(self, command: str) -> str | None: """Execute state command for return value.""" LOGGER.info("Running state value command: %s", command) - return check_output_or_log(command, self._timeout) + return await async_check_output_or_log(command, self._timeout) - def _query_state_code(self, command: str) -> bool: + async def _async_query_state_code(self, command: str) -> bool: """Execute state command for return code.""" LOGGER.info("Running state code command: %s", command) return ( - call_shell_with_timeout(command, self._timeout, log_return_code=False) == 0 + await async_call_shell_with_timeout( + command, self._timeout, log_return_code=False + ) + == 0 ) @property @@ -150,12 +149,12 @@ class CommandSwitch(ManualTriggerEntity, SwitchEntity): """Return true if we do optimistic updates.""" return self._command_state is None - def _query_state(self) -> str | int | None: + async def _async_query_state(self) -> str | int | None: """Query for state.""" if self._command_state: if self._value_template: - return self._query_state_value(self._command_state) - return self._query_state_code(self._command_state) + return await self._async_query_state_value(self._command_state) + return await self._async_query_state_code(self._command_state) if TYPE_CHECKING: return None @@ -177,7 +176,7 @@ class CommandSwitch(ManualTriggerEntity, SwitchEntity): async def _async_update(self) -> None: """Update device state.""" if self._command_state: - payload = str(await self.hass.async_add_executor_job(self._query_state)) + payload = str(await self._async_query_state()) value = None if self._value_template: value = self._value_template.async_render_with_possible_json_value( diff --git a/homeassistant/components/command_line/utils.py b/homeassistant/components/command_line/utils.py index 66faa3a0bf8..067efc08e97 100644 --- a/homeassistant/components/command_line/utils.py +++ b/homeassistant/components/command_line/utils.py @@ -1,13 +1,15 @@ """The command_line component utils.""" + from __future__ import annotations +import asyncio import logging -import subprocess _LOGGER = logging.getLogger(__name__) +_EXEC_FAILED_CODE = 127 -def call_shell_with_timeout( +async def async_call_shell_with_timeout( command: str, timeout: int, *, log_return_code: bool = True ) -> int: """Run a shell command with a timeout. @@ -17,46 +19,45 @@ def call_shell_with_timeout( """ try: _LOGGER.debug("Running command: %s", command) - subprocess.check_output( + proc = await asyncio.create_subprocess_shell( # shell by design command, - shell=True, # noqa: S602 # shell by design - timeout=timeout, close_fds=False, # required for posix_spawn ) - return 0 - except subprocess.CalledProcessError as proc_exception: - if log_return_code: + async with asyncio.timeout(timeout): + await proc.communicate() + return_code = proc.returncode + if return_code == _EXEC_FAILED_CODE: + _LOGGER.error("Error trying to exec command: %s", command) + elif log_return_code and return_code != 0: _LOGGER.error( "Command failed (with return code %s): %s", - proc_exception.returncode, + proc.returncode, command, ) - return proc_exception.returncode - except subprocess.TimeoutExpired: + return return_code or 0 + except TimeoutError: _LOGGER.error("Timeout for command: %s", command) return -1 - except subprocess.SubprocessError: - _LOGGER.error("Error trying to exec command: %s", command) - return -1 -def check_output_or_log(command: str, timeout: int) -> str | None: +async def async_check_output_or_log(command: str, timeout: int) -> str | None: """Run a shell command with a timeout and return the output.""" try: - return_value = subprocess.check_output( + proc = await asyncio.create_subprocess_shell( # shell by design command, - shell=True, # noqa: S602 # shell by design - timeout=timeout, close_fds=False, # required for posix_spawn + stdout=asyncio.subprocess.PIPE, ) - return return_value.strip().decode("utf-8") - except subprocess.CalledProcessError as err: - _LOGGER.error( - "Command failed (with return code %s): %s", err.returncode, command - ) - except subprocess.TimeoutExpired: + async with asyncio.timeout(timeout): + stdout, _ = await proc.communicate() + + if proc.returncode != 0: + _LOGGER.error( + "Command failed (with return code %s): %s", proc.returncode, command + ) + else: + return stdout.strip().decode("utf-8") + except TimeoutError: _LOGGER.error("Timeout for command: %s", command) - except subprocess.SubprocessError: - _LOGGER.error("Error trying to exec command: %s", command) return None diff --git a/homeassistant/components/compensation/__init__.py b/homeassistant/components/compensation/__init__.py index 01003020108..dc1f903e8f6 100644 --- a/homeassistant/components/compensation/__init__.py +++ b/homeassistant/components/compensation/__init__.py @@ -1,4 +1,5 @@ """The Compensation integration.""" + import logging from operator import itemgetter diff --git a/homeassistant/components/compensation/const.py b/homeassistant/components/compensation/const.py index d49a6982166..ce959469700 100644 --- a/homeassistant/components/compensation/const.py +++ b/homeassistant/components/compensation/const.py @@ -1,4 +1,5 @@ """Compensation constants.""" + DOMAIN = "compensation" SENSOR = "compensation" diff --git a/homeassistant/components/compensation/sensor.py b/homeassistant/components/compensation/sensor.py index 6abc5d3d5d0..11d838e2467 100644 --- a/homeassistant/components/compensation/sensor.py +++ b/homeassistant/components/compensation/sensor.py @@ -1,4 +1,5 @@ """Support for compensation sensor.""" + from __future__ import annotations import logging @@ -17,13 +18,13 @@ from homeassistant.const import ( CONF_UNIT_OF_MEASUREMENT, STATE_UNKNOWN, ) -from homeassistant.core import HomeAssistant, State, callback +from homeassistant.core import Event, HomeAssistant, State, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import ( EventStateChangedData, async_track_state_change_event, ) -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, EventType +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import ( CONF_COMPENSATION, @@ -128,7 +129,7 @@ class CompensationSensor(SensorEntity): @callback def _async_compensation_sensor_state_listener( - self, event: EventType[EventStateChangedData] + self, event: Event[EventStateChangedData] ) -> None: """Handle sensor state changes.""" new_state: State | None diff --git a/homeassistant/components/concord232/alarm_control_panel.py b/homeassistant/components/concord232/alarm_control_panel.py index de5d4495a85..12123a81a38 100644 --- a/homeassistant/components/concord232/alarm_control_panel.py +++ b/homeassistant/components/concord232/alarm_control_panel.py @@ -1,4 +1,5 @@ """Support for Concord232 alarm control panels.""" + from __future__ import annotations import datetime diff --git a/homeassistant/components/concord232/binary_sensor.py b/homeassistant/components/concord232/binary_sensor.py index 305822222ac..79cf0c758e1 100644 --- a/homeassistant/components/concord232/binary_sensor.py +++ b/homeassistant/components/concord232/binary_sensor.py @@ -1,4 +1,5 @@ """Support for exposing Concord232 elements as sensors.""" + from __future__ import annotations import datetime diff --git a/homeassistant/components/config/__init__.py b/homeassistant/components/config/__init__.py index fbeb5904a1a..d71a00ce3bd 100644 --- a/homeassistant/components/config/__init__.py +++ b/homeassistant/components/config/__init__.py @@ -1,48 +1,47 @@ """Component to configure Home Assistant via an API.""" + from __future__ import annotations -import asyncio -from collections.abc import Callable, Coroutine -from http import HTTPStatus -import importlib -import os -from typing import Any, Generic, TypeVar, cast - -from aiohttp import web -import voluptuous as vol - from homeassistant.components import frontend -from homeassistant.components.http import HomeAssistantView, require_admin -from homeassistant.const import CONF_ID, EVENT_COMPONENT_LOADED +from homeassistant.const import EVENT_COMPONENT_LOADED from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv from homeassistant.helpers.typing import ConfigType from homeassistant.setup import ATTR_COMPONENT -from homeassistant.util.file import write_utf8_file_atomic -from homeassistant.util.yaml import dump, load_yaml -from homeassistant.util.yaml.loader import JSON_TYPE -_DataT = TypeVar("_DataT", dict[str, dict[str, Any]], list[dict[str, Any]]) - -DOMAIN = "config" +from . import ( + area_registry, + auth, + auth_provider_homeassistant, + automation, + category_registry, + config_entries, + core, + device_registry, + entity_registry, + floor_registry, + label_registry, + scene, + script, +) +from .const import DOMAIN SECTIONS = ( - "area_registry", - "auth", - "auth_provider_homeassistant", - "automation", - "config_entries", - "core", - "device_registry", - "entity_registry", - "floor_registry", - "label_registry", - "script", - "scene", + area_registry, + auth, + auth_provider_homeassistant, + automation, + category_registry, + config_entries, + core, + device_registry, + entity_registry, + floor_registry, + label_registry, + script, + scene, ) -ACTION_CREATE_UPDATE = "create_update" -ACTION_DELETE = "delete" + CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN) @@ -53,231 +52,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: hass, "config", "config", "hass:cog", require_admin=True ) - for panel_name in SECTIONS: - panel = importlib.import_module(f".{panel_name}", __name__) - + for panel in SECTIONS: if panel.async_setup(hass): - key = f"{DOMAIN}.{panel_name}" + name = panel.__name__.split(".")[-1] + key = f"{DOMAIN}.{name}" hass.bus.async_fire(EVENT_COMPONENT_LOADED, {ATTR_COMPONENT: key}) return True - - -class BaseEditConfigView(HomeAssistantView, Generic[_DataT]): - """Configure a Group endpoint.""" - - def __init__( - self, - component: str, - config_type: str, - path: str, - key_schema: Callable[[Any], str], - data_schema: Callable[[dict[str, Any]], Any], - *, - post_write_hook: Callable[[str, str], Coroutine[Any, Any, None]] | None = None, - data_validator: Callable[ - [HomeAssistant, str, dict[str, Any]], - Coroutine[Any, Any, dict[str, Any] | None], - ] - | None = None, - ) -> None: - """Initialize a config view.""" - self.url = f"/api/config/{component}/{config_type}/{{config_key}}" - self.name = f"api:config:{component}:{config_type}" - self.path = path - self.key_schema = key_schema - self.data_schema = data_schema - self.post_write_hook = post_write_hook - self.data_validator = data_validator - self.mutation_lock = asyncio.Lock() - - def _empty_config(self) -> _DataT: - """Empty config if file not found.""" - raise NotImplementedError - - def _get_value( - self, hass: HomeAssistant, data: _DataT, config_key: str - ) -> dict[str, Any] | None: - """Get value.""" - raise NotImplementedError - - def _write_value( - self, - hass: HomeAssistant, - data: _DataT, - config_key: str, - new_value: dict[str, Any], - ) -> None: - """Set value.""" - raise NotImplementedError - - def _delete_value( - self, hass: HomeAssistant, data: _DataT, config_key: str - ) -> dict[str, Any] | None: - """Delete value.""" - raise NotImplementedError - - @require_admin - async def get(self, request: web.Request, config_key: str) -> web.Response: - """Fetch device specific config.""" - hass: HomeAssistant = request.app["hass"] - async with self.mutation_lock: - current = await self.read_config(hass) - value = self._get_value(hass, current, config_key) - - if value is None: - return self.json_message("Resource not found", HTTPStatus.NOT_FOUND) - - return self.json(value) - - @require_admin - async def post(self, request: web.Request, config_key: str) -> web.Response: - """Validate config and return results.""" - try: - data = await request.json() - except ValueError: - return self.json_message("Invalid JSON specified", HTTPStatus.BAD_REQUEST) - - try: - self.key_schema(config_key) - except vol.Invalid as err: - return self.json_message(f"Key malformed: {err}", HTTPStatus.BAD_REQUEST) - - hass: HomeAssistant = request.app["hass"] - - try: - # We just validate, we don't store that data because - # we don't want to store the defaults. - if self.data_validator: - await self.data_validator(hass, config_key, data) - else: - self.data_schema(data) - except (vol.Invalid, HomeAssistantError) as err: - return self.json_message( - f"Message malformed: {err}", HTTPStatus.BAD_REQUEST - ) - - path = hass.config.path(self.path) - - async with self.mutation_lock: - current = await self.read_config(hass) - self._write_value(hass, current, config_key, data) - - await hass.async_add_executor_job(_write, path, current) - - if self.post_write_hook is not None: - hass.async_create_task( - self.post_write_hook(ACTION_CREATE_UPDATE, config_key) - ) - - return self.json({"result": "ok"}) - - @require_admin - async def delete(self, request: web.Request, config_key: str) -> web.Response: - """Remove an entry.""" - hass: HomeAssistant = request.app["hass"] - async with self.mutation_lock: - current = await self.read_config(hass) - value = self._get_value(hass, current, config_key) - path = hass.config.path(self.path) - - if value is None: - return self.json_message("Resource not found", HTTPStatus.BAD_REQUEST) - - self._delete_value(hass, current, config_key) - await hass.async_add_executor_job(_write, path, current) - - if self.post_write_hook is not None: - hass.async_create_task(self.post_write_hook(ACTION_DELETE, config_key)) - - return self.json({"result": "ok"}) - - async def read_config(self, hass: HomeAssistant) -> _DataT: - """Read the config.""" - current = await hass.async_add_executor_job(_read, hass.config.path(self.path)) - if not current: - current = self._empty_config() - return cast(_DataT, current) - - -class EditKeyBasedConfigView(BaseEditConfigView[dict[str, dict[str, Any]]]): - """Configure a list of entries.""" - - def _empty_config(self) -> dict[str, Any]: - """Return an empty config.""" - return {} - - def _get_value( - self, hass: HomeAssistant, data: dict[str, dict[str, Any]], config_key: str - ) -> dict[str, Any] | None: - """Get value.""" - return data.get(config_key) - - def _write_value( - self, - hass: HomeAssistant, - data: dict[str, dict[str, Any]], - config_key: str, - new_value: dict[str, Any], - ) -> None: - """Set value.""" - data.setdefault(config_key, {}).update(new_value) - - def _delete_value( - self, hass: HomeAssistant, data: dict[str, dict[str, Any]], config_key: str - ) -> dict[str, Any]: - """Delete value.""" - return data.pop(config_key) - - -class EditIdBasedConfigView(BaseEditConfigView[list[dict[str, Any]]]): - """Configure key based config entries.""" - - def _empty_config(self) -> list[Any]: - """Return an empty config.""" - return [] - - def _get_value( - self, hass: HomeAssistant, data: list[dict[str, Any]], config_key: str - ) -> dict[str, Any] | None: - """Get value.""" - return next((val for val in data if val.get(CONF_ID) == config_key), None) - - def _write_value( - self, - hass: HomeAssistant, - data: list[dict[str, Any]], - config_key: str, - new_value: dict[str, Any], - ) -> None: - """Set value.""" - if (value := self._get_value(hass, data, config_key)) is None: - value = {CONF_ID: config_key} - data.append(value) - - value.update(new_value) - - def _delete_value( - self, hass: HomeAssistant, data: list[dict[str, Any]], config_key: str - ) -> None: - """Delete value.""" - index = next( - idx for idx, val in enumerate(data) if val.get(CONF_ID) == config_key - ) - data.pop(index) - - -def _read(path: str) -> JSON_TYPE | None: - """Read YAML helper.""" - if not os.path.isfile(path): - return None - - return load_yaml(path) - - -def _write(path: str, data: dict | list) -> None: - """Write YAML helper.""" - # Do it before opening file. If dump causes error it will now not - # truncate the file. - contents = dump(data) - write_utf8_file_atomic(path, contents) diff --git a/homeassistant/components/config/area_registry.py b/homeassistant/components/config/area_registry.py index 31841717109..a499ab84784 100644 --- a/homeassistant/components/config/area_registry.py +++ b/homeassistant/components/config/area_registry.py @@ -1,4 +1,5 @@ """HTTP views to interact with the area registry.""" + from __future__ import annotations from typing import Any @@ -39,7 +40,9 @@ def websocket_list_areas( { vol.Required("type"): "config/area_registry/create", vol.Optional("aliases"): list, + vol.Optional("floor_id"): str, vol.Optional("icon"): str, + vol.Optional("labels"): [str], vol.Required("name"): str, vol.Optional("picture"): vol.Any(str, None), } @@ -62,6 +65,10 @@ def websocket_create_area( # Convert aliases to a set data["aliases"] = set(data["aliases"]) + if "labels" in data: + # Convert labels to a set + data["labels"] = set(data["labels"]) + try: entry = registry.async_create(**data) except ValueError as err: @@ -99,7 +106,9 @@ def websocket_delete_area( vol.Required("type"): "config/area_registry/update", vol.Optional("aliases"): list, vol.Required("area_id"): str, + vol.Optional("floor_id"): vol.Any(str, None), vol.Optional("icon"): vol.Any(str, None), + vol.Optional("labels"): [str], vol.Optional("name"): str, vol.Optional("picture"): vol.Any(str, None), } @@ -122,6 +131,10 @@ def websocket_update_area( # Convert aliases to a set data["aliases"] = set(data["aliases"]) + if "labels" in data: + # Convert labels to a set + data["labels"] = set(data["labels"]) + try: entry = registry.async_update(**data) except ValueError as err: @@ -136,7 +149,9 @@ def _entry_dict(entry: AreaEntry) -> dict[str, Any]: return { "aliases": list(entry.aliases), "area_id": entry.id, + "floor_id": entry.floor_id, "icon": entry.icon, + "labels": list(entry.labels), "name": entry.name, "picture": entry.picture, } diff --git a/homeassistant/components/config/auth.py b/homeassistant/components/config/auth.py index 0409bf0f0f4..266c06d6ee8 100644 --- a/homeassistant/components/config/auth.py +++ b/homeassistant/components/config/auth.py @@ -1,4 +1,5 @@ """Offer API to configure Home Assistant auth.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/config/auth_provider_homeassistant.py b/homeassistant/components/config/auth_provider_homeassistant.py index 0c58cad536e..94c179e1a5f 100644 --- a/homeassistant/components/config/auth_provider_homeassistant.py +++ b/homeassistant/components/config/auth_provider_homeassistant.py @@ -1,4 +1,5 @@ """Offer API to configure the Home Assistant auth provider.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/config/automation.py b/homeassistant/components/config/automation.py index cf637b0aa23..a5a010c00a6 100644 --- a/homeassistant/components/config/automation.py +++ b/homeassistant/components/config/automation.py @@ -1,4 +1,5 @@ """Provide configuration end points for Automations.""" + from __future__ import annotations from typing import Any @@ -14,7 +15,8 @@ from homeassistant.const import CONF_ID, SERVICE_RELOAD from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv, entity_registry as er -from . import ACTION_DELETE, EditIdBasedConfigView +from .const import ACTION_DELETE +from .view import EditIdBasedConfigView @callback diff --git a/homeassistant/components/config/category_registry.py b/homeassistant/components/config/category_registry.py new file mode 100644 index 00000000000..5fc705a5844 --- /dev/null +++ b/homeassistant/components/config/category_registry.py @@ -0,0 +1,135 @@ +"""Websocket API to interact with the category registry.""" + +from typing import Any + +import voluptuous as vol + +from homeassistant.components import websocket_api +from homeassistant.components.websocket_api.connection import ActiveConnection +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import category_registry as cr, config_validation as cv + + +@callback +def async_setup(hass: HomeAssistant) -> bool: + """Register the category registry WS commands.""" + websocket_api.async_register_command(hass, websocket_list_categories) + websocket_api.async_register_command(hass, websocket_create_category) + websocket_api.async_register_command(hass, websocket_delete_category) + websocket_api.async_register_command(hass, websocket_update_category) + return True + + +@websocket_api.websocket_command( + { + vol.Required("type"): "config/category_registry/list", + vol.Required("scope"): str, + } +) +@callback +def websocket_list_categories( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: + """Handle list categories command.""" + category_registry = cr.async_get(hass) + connection.send_result( + msg["id"], + [ + _entry_dict(entry) + for entry in category_registry.async_list_categories(scope=msg["scope"]) + ], + ) + + +@websocket_api.websocket_command( + { + vol.Required("type"): "config/category_registry/create", + vol.Required("scope"): str, + vol.Required("name"): str, + vol.Optional("icon"): vol.Any(cv.icon, None), + } +) +@websocket_api.require_admin +@callback +def websocket_create_category( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: + """Create category command.""" + category_registry = cr.async_get(hass) + + data = dict(msg) + data.pop("type") + data.pop("id") + + try: + entry = category_registry.async_create(**data) + except ValueError as err: + connection.send_error(msg["id"], "invalid_info", str(err)) + else: + connection.send_result(msg["id"], _entry_dict(entry)) + + +@websocket_api.websocket_command( + { + vol.Required("type"): "config/category_registry/delete", + vol.Required("scope"): str, + vol.Required("category_id"): str, + } +) +@websocket_api.require_admin +@callback +def websocket_delete_category( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: + """Delete category command.""" + category_registry = cr.async_get(hass) + + try: + category_registry.async_delete( + scope=msg["scope"], category_id=msg["category_id"] + ) + except KeyError: + connection.send_error(msg["id"], "invalid_info", "Category ID doesn't exist") + else: + connection.send_result(msg["id"]) + + +@websocket_api.websocket_command( + { + vol.Required("type"): "config/category_registry/update", + vol.Required("scope"): str, + vol.Required("category_id"): str, + vol.Optional("name"): str, + vol.Optional("icon"): vol.Any(cv.icon, None), + } +) +@websocket_api.require_admin +@callback +def websocket_update_category( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: + """Handle update category websocket command.""" + category_registry = cr.async_get(hass) + + data = dict(msg) + data.pop("type") + data.pop("id") + + try: + entry = category_registry.async_update(**data) + except ValueError as err: + connection.send_error(msg["id"], "invalid_info", str(err)) + except KeyError: + connection.send_error(msg["id"], "invalid_info", "Category ID doesn't exist") + else: + connection.send_result(msg["id"], _entry_dict(entry)) + + +@callback +def _entry_dict(entry: cr.CategoryEntry) -> dict[str, Any]: + """Convert entry to API format.""" + return { + "category_id": entry.category_id, + "icon": entry.icon, + "name": entry.name, + } diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index 52904cb8d35..8eb4eb22fb5 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -1,4 +1,5 @@ """Http views to control the config manager.""" + from __future__ import annotations from collections.abc import Callable @@ -12,7 +13,8 @@ import voluptuous as vol from homeassistant import config_entries, data_entry_flow from homeassistant.auth.permissions.const import CAT_CONFIG_ENTRIES, POLICY_EDIT from homeassistant.components import websocket_api -from homeassistant.components.http import HomeAssistantView, require_admin +from homeassistant.components.http import KEY_HASS, HomeAssistantView, require_admin +from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import DependencyError, Unauthorized import homeassistant.helpers.config_validation as cv @@ -63,7 +65,7 @@ class ConfigManagerEntryIndexView(HomeAssistantView): async def get(self, request: web.Request) -> web.Response: """List available config entries.""" - hass: HomeAssistant = request.app["hass"] + hass = request.app[KEY_HASS] domain = None if "domain" in request.query: domain = request.query["domain"] @@ -87,7 +89,7 @@ class ConfigManagerEntryResourceView(HomeAssistantView): if not request["hass_user"].is_admin: raise Unauthorized(config_entry_id=entry_id, permission="remove") - hass: HomeAssistant = request.app["hass"] + hass = request.app[KEY_HASS] try: result = await hass.config_entries.async_remove(entry_id) @@ -108,7 +110,7 @@ class ConfigManagerEntryResourceReloadView(HomeAssistantView): if not request["hass_user"].is_admin: raise Unauthorized(config_entry_id=entry_id, permission="remove") - hass: HomeAssistant = request.app["hass"] + hass = request.app[KEY_HASS] entry = hass.config_entries.async_get_entry(entry_id) if not entry: return self.json_message("Invalid entry specified", HTTPStatus.NOT_FOUND) @@ -140,7 +142,9 @@ def _prepare_config_flow_result_json( return data -class ConfigManagerFlowIndexView(FlowManagerIndexView): +class ConfigManagerFlowIndexView( + FlowManagerIndexView[config_entries.ConfigEntriesFlowManager] +): """View to create config flows.""" url = "/api/config/config_entries/flow" @@ -153,10 +157,26 @@ class ConfigManagerFlowIndexView(FlowManagerIndexView): @require_admin( error=Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission="add") ) - async def post(self, request: web.Request) -> web.Response: - """Handle a POST request.""" + @RequestDataValidator( + vol.Schema( + { + vol.Required("handler"): vol.Any(str, list), + vol.Optional("show_advanced_options", default=False): cv.boolean, + vol.Optional("entry_id"): cv.string, + }, + extra=vol.ALLOW_EXTRA, + ) + ) + async def post(self, request: web.Request, data: dict[str, Any]) -> web.Response: + """Initialize a POST request for a config entry flow.""" + return await self._post_impl(request, data) + + async def _post_impl( + self, request: web.Request, data: dict[str, Any] + ) -> web.Response: + """Handle a POST request for a config entry flow.""" try: - return await super().post(request) + return await super()._post_impl(request, data) except DependencyError as exc: return web.Response( text=f"Failed dependencies {', '.join(exc.failed_dependencies)}", @@ -167,6 +187,9 @@ class ConfigManagerFlowIndexView(FlowManagerIndexView): """Return context.""" context = super().get_context(data) context["source"] = config_entries.SOURCE_USER + if entry_id := data.get("entry_id"): + context["source"] = config_entries.SOURCE_RECONFIGURE + context["entry_id"] = entry_id return context def _prepare_result_json( @@ -176,7 +199,9 @@ class ConfigManagerFlowIndexView(FlowManagerIndexView): return _prepare_config_flow_result_json(result, super()._prepare_result_json) -class ConfigManagerFlowResourceView(FlowManagerResourceView): +class ConfigManagerFlowResourceView( + FlowManagerResourceView[config_entries.ConfigEntriesFlowManager] +): """View to interact with the flow manager.""" url = "/api/config/config_entries/flow/{flow_id}" @@ -211,14 +236,16 @@ class ConfigManagerAvailableFlowView(HomeAssistantView): async def get(self, request: web.Request) -> web.Response: """List available flow handlers.""" - hass: HomeAssistant = request.app["hass"] + hass = request.app[KEY_HASS] kwargs: dict[str, Any] = {} if "type" in request.query: kwargs["type_filter"] = request.query["type"] return self.json(await async_get_config_flows(hass, **kwargs)) -class OptionManagerFlowIndexView(FlowManagerIndexView): +class OptionManagerFlowIndexView( + FlowManagerIndexView[config_entries.OptionsFlowManager] +): """View to create option flows.""" url = "/api/config/config_entries/options/flow" @@ -235,7 +262,9 @@ class OptionManagerFlowIndexView(FlowManagerIndexView): return await super().post(request) -class OptionManagerFlowResourceView(FlowManagerResourceView): +class OptionManagerFlowResourceView( + FlowManagerResourceView[config_entries.OptionsFlowManager] +): """View to interact with the option flow manager.""" url = "/api/config/config_entries/options/flow/{flow_id}" diff --git a/homeassistant/components/config/const.py b/homeassistant/components/config/const.py new file mode 100644 index 00000000000..f48d00a7ca9 --- /dev/null +++ b/homeassistant/components/config/const.py @@ -0,0 +1,5 @@ +"""Constants for config.""" + +ACTION_CREATE_UPDATE = "create_update" +ACTION_DELETE = "delete" +DOMAIN = "config" diff --git a/homeassistant/components/config/core.py b/homeassistant/components/config/core.py index c3e070a3751..5c3e4cfe09b 100644 --- a/homeassistant/components/config/core.py +++ b/homeassistant/components/config/core.py @@ -1,4 +1,5 @@ """Component to interact with Hassbian tools.""" + from __future__ import annotations from typing import Any @@ -7,7 +8,7 @@ from aiohttp import web import voluptuous as vol from homeassistant.components import websocket_api -from homeassistant.components.http import HomeAssistantView, require_admin +from homeassistant.components.http import KEY_HASS, HomeAssistantView, require_admin from homeassistant.components.sensor import async_update_suggested_units from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import check_config, config_validation as cv @@ -34,7 +35,7 @@ class CheckConfigView(HomeAssistantView): async def post(self, request: web.Request) -> web.Response: """Validate configuration and return results.""" - res = await check_config.async_check_ha_config_file(request.app["hass"]) + res = await check_config.async_check_ha_config_file(request.app[KEY_HASS]) state = "invalid" if res.errors else "valid" diff --git a/homeassistant/components/config/device_registry.py b/homeassistant/components/config/device_registry.py index 7bd76310929..f2b0035d060 100644 --- a/homeassistant/components/config/device_registry.py +++ b/homeassistant/components/config/device_registry.py @@ -1,4 +1,5 @@ """HTTP views to interact with the device registry.""" + from __future__ import annotations from typing import Any, cast @@ -68,6 +69,7 @@ def websocket_list_devices( # We only allow setting disabled_by user via API. # No Enum support like this in voluptuous, use .value vol.Optional("disabled_by"): vol.Any(DeviceEntryDisabler.USER.value, None), + vol.Optional("labels"): [str], vol.Optional("name_by_user"): vol.Any(str, None), } ) @@ -86,6 +88,10 @@ def websocket_update_device( if msg.get("disabled_by") is not None: msg["disabled_by"] = DeviceEntryDisabler(msg["disabled_by"]) + if "labels" in msg: + # Convert labels to a set + msg["labels"] = set(msg["labels"]) + entry = cast(DeviceEntry, registry.async_update_device(**msg)) connection.send_message(websocket_api.result_message(msg_id, entry.dict_repr)) @@ -124,7 +130,7 @@ async def websocket_remove_config_entry_from_device( try: integration = await loader.async_get_integration(hass, config_entry.domain) - component = integration.get_component() + component = await integration.async_get_component() except (ImportError, loader.IntegrationNotFound) as exc: raise HomeAssistantError("Integration not found") from exc diff --git a/homeassistant/components/config/entity_registry.py b/homeassistant/components/config/entity_registry.py index 66a1ceeba69..7cdec324340 100644 --- a/homeassistant/components/config/entity_registry.py +++ b/homeassistant/components/config/entity_registry.py @@ -1,4 +1,5 @@ """HTTP views to interact with the entity registry.""" + from __future__ import annotations from typing import Any @@ -152,8 +153,19 @@ def websocket_get_entities( # If passed in, we update value. Passing None will remove old value. vol.Optional("aliases"): list, vol.Optional("area_id"): vol.Any(str, None), + # Categories is a mapping of key/value (scope/category_id) pairs. + # If passed in, we update/adjust only the provided scope(s). + # Other category scopes in the entity, are left as is. + # + # Categorized items such as entities + # can only be in 1 category ID per scope at a time. + # Therefore, passing in a category ID will either add or move + # the entity to that specific category. Passing in None will + # remove the entity from the category. + vol.Optional("categories"): cv.schema_with_slug_keys(vol.Any(str, None)), vol.Optional("device_class"): vol.Any(str, None), vol.Optional("icon"): vol.Any(str, None), + vol.Optional("labels"): [str], vol.Optional("name"): vol.Any(str, None), vol.Optional("new_entity_id"): str, # We only allow setting disabled_by user via API. @@ -213,6 +225,10 @@ def websocket_update_entity( # Convert aliases to a set changes["aliases"] = set(msg["aliases"]) + if "labels" in msg: + # Convert labels to a set + changes["labels"] = set(msg["labels"]) + if "disabled_by" in msg and msg["disabled_by"] is None: # Don't allow enabling an entity of a disabled device if entity_entry.device_id: @@ -226,6 +242,18 @@ def websocket_update_entity( ) return + # Update the categories if provided + if "categories" in msg: + categories = entity_entry.categories.copy() + for scope, category_id in msg["categories"].items(): + if scope in categories and category_id is None: + # Remove the category from the scope as it was unset + del categories[scope] + elif category_id is not None: + # Add or update the category for the given scope + categories[scope] = category_id + changes["categories"] = categories + try: if changes: entity_entry = registry.async_update_entity(entity_id, **changes) diff --git a/homeassistant/components/config/floor_registry.py b/homeassistant/components/config/floor_registry.py index 4b3ffbd4575..986f772ac53 100644 --- a/homeassistant/components/config/floor_registry.py +++ b/homeassistant/components/config/floor_registry.py @@ -1,4 +1,5 @@ """Websocket API to interact with the floor registry.""" + from typing import Any import voluptuous as vol @@ -40,8 +41,9 @@ def websocket_list_floors( { vol.Required("type"): "config/floor_registry/create", vol.Required("name"): str, + vol.Optional("aliases"): list, vol.Optional("icon"): vol.Any(str, None), - vol.Optional("level"): int, + vol.Optional("level"): vol.Any(int, None), } ) @websocket_api.require_admin @@ -56,6 +58,10 @@ def websocket_create_floor( data.pop("type") data.pop("id") + if "aliases" in data: + # Convert aliases to a set + data["aliases"] = set(data["aliases"]) + try: entry = registry.async_create(**data) except ValueError as err: @@ -90,8 +96,9 @@ def websocket_delete_floor( { vol.Required("type"): "config/floor_registry/update", vol.Required("floor_id"): str, + vol.Optional("aliases"): list, vol.Optional("icon"): vol.Any(str, None), - vol.Optional("level"): int, + vol.Optional("level"): vol.Any(int, None), vol.Optional("name"): str, } ) @@ -107,6 +114,10 @@ def websocket_update_floor( data.pop("type") data.pop("id") + if "aliases" in data: + # Convert aliases to a set + data["aliases"] = set(data["aliases"]) + try: entry = registry.async_update(**data) except ValueError as err: @@ -119,6 +130,7 @@ def websocket_update_floor( def _entry_dict(entry: FloorEntry) -> dict[str, Any]: """Convert entry to API format.""" return { + "aliases": list(entry.aliases), "floor_id": entry.floor_id, "icon": entry.icon, "level": entry.level, diff --git a/homeassistant/components/config/label_registry.py b/homeassistant/components/config/label_registry.py index 7ea80231e82..1d5d526016d 100644 --- a/homeassistant/components/config/label_registry.py +++ b/homeassistant/components/config/label_registry.py @@ -1,4 +1,5 @@ """Websocket API to interact with the label registry.""" + from typing import Any import voluptuous as vol @@ -9,6 +10,35 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.label_registry import LabelEntry, async_get +SUPPORTED_LABEL_THEME_COLORS = { + "primary", + "accent", + "disabled", + "red", + "pink", + "purple", + "deep-purple", + "indigo", + "blue", + "light-blue", + "cyan", + "teal", + "green", + "light-green", + "lime", + "yellow", + "amber", + "orange", + "deep-orange", + "brown", + "light-grey", + "grey", + "dark-grey", + "blue-grey", + "black", + "white", +} + @callback def async_setup(hass: HomeAssistant) -> bool: @@ -41,7 +71,9 @@ def websocket_list_labels( { vol.Required("type"): "config/label_registry/create", vol.Required("name"): str, - vol.Optional("color"): vol.Any(cv.color_hex, None), + vol.Optional("color"): vol.Any( + cv.color_hex, vol.In(SUPPORTED_LABEL_THEME_COLORS), None + ), vol.Optional("description"): vol.Any(str, None), vol.Optional("icon"): vol.Any(cv.icon, None), } @@ -92,7 +124,9 @@ def websocket_delete_label( { vol.Required("type"): "config/label_registry/update", vol.Required("label_id"): str, - vol.Optional("color"): vol.Any(cv.color_hex, None), + vol.Optional("color"): vol.Any( + cv.color_hex, vol.In(SUPPORTED_LABEL_THEME_COLORS), None + ), vol.Optional("description"): vol.Any(str, None), vol.Optional("icon"): vol.Any(cv.icon, None), vol.Optional("name"): str, diff --git a/homeassistant/components/config/scene.py b/homeassistant/components/config/scene.py index 01bdce0c8bc..a2e2693036a 100644 --- a/homeassistant/components/config/scene.py +++ b/homeassistant/components/config/scene.py @@ -1,4 +1,5 @@ """Provide configuration end points for Scenes.""" + from __future__ import annotations from typing import Any @@ -10,7 +11,8 @@ from homeassistant.const import CONF_ID, SERVICE_RELOAD from homeassistant.core import DOMAIN as HA_DOMAIN, HomeAssistant, callback from homeassistant.helpers import config_validation as cv, entity_registry as er -from . import ACTION_DELETE, EditIdBasedConfigView +from .const import ACTION_DELETE +from .view import EditIdBasedConfigView @callback diff --git a/homeassistant/components/config/script.py b/homeassistant/components/config/script.py index d181ad94286..c39aad4fcdb 100644 --- a/homeassistant/components/config/script.py +++ b/homeassistant/components/config/script.py @@ -1,4 +1,5 @@ """Provide configuration end points for scripts.""" + from __future__ import annotations from typing import Any @@ -13,7 +14,8 @@ from homeassistant.const import SERVICE_RELOAD from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv, entity_registry as er -from . import ACTION_DELETE, EditKeyBasedConfigView +from .const import ACTION_DELETE +from .view import EditKeyBasedConfigView @callback diff --git a/homeassistant/components/config/view.py b/homeassistant/components/config/view.py new file mode 100644 index 00000000000..62459a83a7d --- /dev/null +++ b/homeassistant/components/config/view.py @@ -0,0 +1,244 @@ +"""Component to configure Home Assistant via an API.""" + +from __future__ import annotations + +import asyncio +from collections.abc import Callable, Coroutine +from http import HTTPStatus +import os +from typing import Any, Generic, TypeVar, cast + +from aiohttp import web +import voluptuous as vol + +from homeassistant.components.http import KEY_HASS, HomeAssistantView, require_admin +from homeassistant.const import CONF_ID +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.util.file import write_utf8_file_atomic +from homeassistant.util.yaml import dump, load_yaml +from homeassistant.util.yaml.loader import JSON_TYPE + +from .const import ACTION_CREATE_UPDATE, ACTION_DELETE + +_DataT = TypeVar("_DataT", dict[str, dict[str, Any]], list[dict[str, Any]]) + + +class BaseEditConfigView(HomeAssistantView, Generic[_DataT]): + """Configure a Group endpoint.""" + + def __init__( + self, + component: str, + config_type: str, + path: str, + key_schema: Callable[[Any], str], + data_schema: Callable[[dict[str, Any]], Any], + *, + post_write_hook: Callable[[str, str], Coroutine[Any, Any, None]] | None = None, + data_validator: Callable[ + [HomeAssistant, str, dict[str, Any]], + Coroutine[Any, Any, dict[str, Any] | None], + ] + | None = None, + ) -> None: + """Initialize a config view.""" + self.url = f"/api/config/{component}/{config_type}/{{config_key}}" + self.name = f"api:config:{component}:{config_type}" + self.path = path + self.key_schema = key_schema + self.data_schema = data_schema + self.post_write_hook = post_write_hook + self.data_validator = data_validator + self.mutation_lock = asyncio.Lock() + + def _empty_config(self) -> _DataT: + """Empty config if file not found.""" + raise NotImplementedError + + def _get_value( + self, hass: HomeAssistant, data: _DataT, config_key: str + ) -> dict[str, Any] | None: + """Get value.""" + raise NotImplementedError + + def _write_value( + self, + hass: HomeAssistant, + data: _DataT, + config_key: str, + new_value: dict[str, Any], + ) -> None: + """Set value.""" + raise NotImplementedError + + def _delete_value( + self, hass: HomeAssistant, data: _DataT, config_key: str + ) -> dict[str, Any] | None: + """Delete value.""" + raise NotImplementedError + + @require_admin + async def get(self, request: web.Request, config_key: str) -> web.Response: + """Fetch device specific config.""" + hass = request.app[KEY_HASS] + async with self.mutation_lock: + current = await self.read_config(hass) + value = self._get_value(hass, current, config_key) + + if value is None: + return self.json_message("Resource not found", HTTPStatus.NOT_FOUND) + + return self.json(value) + + @require_admin + async def post(self, request: web.Request, config_key: str) -> web.Response: + """Validate config and return results.""" + try: + data = await request.json() + except ValueError: + return self.json_message("Invalid JSON specified", HTTPStatus.BAD_REQUEST) + + try: + self.key_schema(config_key) + except vol.Invalid as err: + return self.json_message(f"Key malformed: {err}", HTTPStatus.BAD_REQUEST) + + hass = request.app[KEY_HASS] + + try: + # We just validate, we don't store that data because + # we don't want to store the defaults. + if self.data_validator: + await self.data_validator(hass, config_key, data) + else: + self.data_schema(data) + except (vol.Invalid, HomeAssistantError) as err: + return self.json_message( + f"Message malformed: {err}", HTTPStatus.BAD_REQUEST + ) + + path = hass.config.path(self.path) + + async with self.mutation_lock: + current = await self.read_config(hass) + self._write_value(hass, current, config_key, data) + + await hass.async_add_executor_job(_write, path, current) + + if self.post_write_hook is not None: + hass.async_create_task( + self.post_write_hook(ACTION_CREATE_UPDATE, config_key) + ) + + return self.json({"result": "ok"}) + + @require_admin + async def delete(self, request: web.Request, config_key: str) -> web.Response: + """Remove an entry.""" + hass = request.app[KEY_HASS] + async with self.mutation_lock: + current = await self.read_config(hass) + value = self._get_value(hass, current, config_key) + path = hass.config.path(self.path) + + if value is None: + return self.json_message("Resource not found", HTTPStatus.BAD_REQUEST) + + self._delete_value(hass, current, config_key) + await hass.async_add_executor_job(_write, path, current) + + if self.post_write_hook is not None: + hass.async_create_task(self.post_write_hook(ACTION_DELETE, config_key)) + + return self.json({"result": "ok"}) + + async def read_config(self, hass: HomeAssistant) -> _DataT: + """Read the config.""" + current = await hass.async_add_executor_job(_read, hass.config.path(self.path)) + if not current: + current = self._empty_config() + return cast(_DataT, current) + + +class EditKeyBasedConfigView(BaseEditConfigView[dict[str, dict[str, Any]]]): + """Configure a list of entries.""" + + def _empty_config(self) -> dict[str, Any]: + """Return an empty config.""" + return {} + + def _get_value( + self, hass: HomeAssistant, data: dict[str, dict[str, Any]], config_key: str + ) -> dict[str, Any] | None: + """Get value.""" + return data.get(config_key) + + def _write_value( + self, + hass: HomeAssistant, + data: dict[str, dict[str, Any]], + config_key: str, + new_value: dict[str, Any], + ) -> None: + """Set value.""" + data.setdefault(config_key, {}).update(new_value) + + def _delete_value( + self, hass: HomeAssistant, data: dict[str, dict[str, Any]], config_key: str + ) -> dict[str, Any]: + """Delete value.""" + return data.pop(config_key) + + +class EditIdBasedConfigView(BaseEditConfigView[list[dict[str, Any]]]): + """Configure key based config entries.""" + + def _empty_config(self) -> list[Any]: + """Return an empty config.""" + return [] + + def _get_value( + self, hass: HomeAssistant, data: list[dict[str, Any]], config_key: str + ) -> dict[str, Any] | None: + """Get value.""" + return next((val for val in data if val.get(CONF_ID) == config_key), None) + + def _write_value( + self, + hass: HomeAssistant, + data: list[dict[str, Any]], + config_key: str, + new_value: dict[str, Any], + ) -> None: + """Set value.""" + if (value := self._get_value(hass, data, config_key)) is None: + value = {CONF_ID: config_key} + data.append(value) + + value.update(new_value) + + def _delete_value( + self, hass: HomeAssistant, data: list[dict[str, Any]], config_key: str + ) -> None: + """Delete value.""" + index = next( + idx for idx, val in enumerate(data) if val.get(CONF_ID) == config_key + ) + data.pop(index) + + +def _read(path: str) -> JSON_TYPE | None: + """Read YAML helper.""" + if not os.path.isfile(path): + return None + + return load_yaml(path) + + +def _write(path: str, data: dict | list) -> None: + """Write YAML helper.""" + # Do it before opening file. If dump causes error it will now not + # truncate the file. + contents = dump(data) + write_utf8_file_atomic(path, contents) diff --git a/homeassistant/components/configurator/__init__.py b/homeassistant/components/configurator/__init__.py index 6fd3917cc9c..0579df90dc9 100644 --- a/homeassistant/components/configurator/__init__.py +++ b/homeassistant/components/configurator/__init__.py @@ -5,6 +5,7 @@ This will return a request id that has to be used for future calls. A callback has to be provided to `request_config` which will be called when the user has submitted configuration information. """ + from __future__ import annotations from collections.abc import Callable @@ -14,7 +15,12 @@ import functools as ft from typing import Any from homeassistant.const import ATTR_ENTITY_PICTURE, ATTR_FRIENDLY_NAME -from homeassistant.core import HomeAssistant, ServiceCall, callback as async_callback +from homeassistant.core import ( + HassJob, + HomeAssistant, + ServiceCall, + callback as async_callback, +) from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.event import async_call_later @@ -244,7 +250,9 @@ class Configurator: # field validation goes here? if callback and ( - job := self.hass.async_add_job(callback, call.data.get(ATTR_FIELDS, {})) + job := self.hass.async_add_hass_job( + HassJob(callback), call.data.get(ATTR_FIELDS, {}) + ) ): await job diff --git a/homeassistant/components/control4/__init__.py b/homeassistant/components/control4/__init__.py index 63cbd9351c7..b8d195fcb05 100644 --- a/homeassistant/components/control4/__init__.py +++ b/homeassistant/components/control4/__init__.py @@ -1,8 +1,10 @@ """The Control4 integration.""" + from __future__ import annotations import json import logging +from typing import Any from aiohttp import client_exceptions from pyControl4.account import C4Account @@ -35,13 +37,14 @@ from .const import ( CONF_DIRECTOR_ALL_ITEMS, CONF_DIRECTOR_MODEL, CONF_DIRECTOR_SW_VERSION, + CONF_UI_CONFIGURATION, DEFAULT_SCAN_INTERVAL, DOMAIN, ) _LOGGER = logging.getLogger(__name__) -PLATFORMS = [Platform.LIGHT] +PLATFORMS = [Platform.LIGHT, Platform.MEDIA_PLAYER] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -104,6 +107,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: director_all_items = json.loads(director_all_items) entry_data[CONF_DIRECTOR_ALL_ITEMS] = director_all_items + entry_data[CONF_UI_CONFIGURATION] = json.loads(await director.getUiConfiguration()) + # Load options from config entry entry_data[CONF_SCAN_INTERVAL] = entry.options.get( CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL @@ -137,21 +142,21 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def get_items_of_category(hass: HomeAssistant, entry: ConfigEntry, category: str): """Return a list of all Control4 items with the specified category.""" director_all_items = hass.data[DOMAIN][entry.entry_id][CONF_DIRECTOR_ALL_ITEMS] - return_list = [] - for item in director_all_items: - if "categories" in item and category in item["categories"]: - return_list.append(item) - return return_list + return [ + item + for item in director_all_items + if "categories" in item and category in item["categories"] + ] -class Control4Entity(CoordinatorEntity): +class Control4Entity(CoordinatorEntity[Any]): """Base entity for Control4.""" def __init__( self, entry_data: dict, - coordinator: DataUpdateCoordinator, - name: str, + coordinator: DataUpdateCoordinator[Any], + name: str | None, idx: int, device_name: str | None, device_manufacturer: str | None, diff --git a/homeassistant/components/control4/config_flow.py b/homeassistant/components/control4/config_flow.py index b93e586b7ca..2d7c6ade255 100644 --- a/homeassistant/components/control4/config_flow.py +++ b/homeassistant/components/control4/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Control4 integration.""" + from __future__ import annotations import logging @@ -9,7 +10,7 @@ from pyControl4.director import C4Director from pyControl4.error_handling import NotFound, Unauthorized import voluptuous as vol -from homeassistant import config_entries, exceptions +from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, @@ -17,6 +18,7 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.helpers.device_registry import format_mac @@ -86,7 +88,7 @@ class Control4Validator: return False -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class Control4ConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Control4.""" VERSION = 1 @@ -137,16 +139,16 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback def async_get_options_flow( - config_entry: config_entries.ConfigEntry, + config_entry: ConfigEntry, ) -> OptionsFlowHandler: """Get the options flow for this handler.""" return OptionsFlowHandler(config_entry) -class OptionsFlowHandler(config_entries.OptionsFlow): +class OptionsFlowHandler(OptionsFlow): """Handle a option flow for Control4.""" - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + def __init__(self, config_entry: ConfigEntry) -> None: """Initialize options flow.""" self.config_entry = config_entry @@ -168,9 +170,9 @@ class OptionsFlowHandler(config_entries.OptionsFlow): return self.async_show_form(step_id="init", data_schema=data_schema) -class CannotConnect(exceptions.HomeAssistantError): +class CannotConnect(HomeAssistantError): """Error to indicate we cannot connect.""" -class InvalidAuth(exceptions.HomeAssistantError): +class InvalidAuth(HomeAssistantError): """Error to indicate there is invalid auth.""" diff --git a/homeassistant/components/control4/const.py b/homeassistant/components/control4/const.py index 677610a1618..f8d939e1ac5 100644 --- a/homeassistant/components/control4/const.py +++ b/homeassistant/components/control4/const.py @@ -10,6 +10,7 @@ CONF_DIRECTOR = "director" CONF_DIRECTOR_SW_VERSION = "director_sw_version" CONF_DIRECTOR_MODEL = "director_model" CONF_DIRECTOR_ALL_ITEMS = "director_all_items" +CONF_UI_CONFIGURATION = "ui_configuration" CONF_CONTROLLER_UNIQUE_ID = "controller_unique_id" CONF_CONFIG_LISTENER = "config_listener" diff --git a/homeassistant/components/control4/director_utils.py b/homeassistant/components/control4/director_utils.py index 3d360e36438..2ce03c2e635 100644 --- a/homeassistant/components/control4/director_utils.py +++ b/homeassistant/components/control4/director_utils.py @@ -1,4 +1,5 @@ """Provides data updates from the Control4 controller for platforms.""" + from collections import defaultdict from collections.abc import Set import logging diff --git a/homeassistant/components/control4/light.py b/homeassistant/components/control4/light.py index a2d1308be98..d7cfd44dc43 100644 --- a/homeassistant/components/control4/light.py +++ b/homeassistant/components/control4/light.py @@ -1,4 +1,5 @@ """Platform for Control4 Lights.""" + from __future__ import annotations import asyncio @@ -44,7 +45,7 @@ async def async_setup_entry( scan_interval, ) - async def async_update_data_non_dimmer(): + async def async_update_data_non_dimmer() -> dict[int, dict[str, Any]]: """Fetch data from Control4 director for non-dimmer lights.""" try: return await update_variables_for_config_entry( @@ -53,7 +54,7 @@ async def async_setup_entry( except C4Exception as err: raise UpdateFailed(f"Error communicating with API: {err}") from err - async def async_update_data_dimmer(): + async def async_update_data_dimmer() -> dict[int, dict[str, Any]]: """Fetch data from Control4 director for dimmer lights.""" try: return await update_variables_for_config_entry( @@ -62,14 +63,14 @@ async def async_setup_entry( except C4Exception as err: raise UpdateFailed(f"Error communicating with API: {err}") from err - non_dimmer_coordinator = DataUpdateCoordinator( + non_dimmer_coordinator = DataUpdateCoordinator[dict[int, dict[str, Any]]]( hass, _LOGGER, name="light", update_method=async_update_data_non_dimmer, update_interval=timedelta(seconds=scan_interval), ) - dimmer_coordinator = DataUpdateCoordinator( + dimmer_coordinator = DataUpdateCoordinator[dict[int, dict[str, Any]]]( hass, _LOGGER, name="light", @@ -148,10 +149,12 @@ async def async_setup_entry( class Control4Light(Control4Entity, LightEntity): """Control4 light entity.""" + _attr_has_entity_name = True + def __init__( self, entry_data: dict, - coordinator: DataUpdateCoordinator, + coordinator: DataUpdateCoordinator[dict[int, dict[str, Any]]], name: str, idx: int, device_name: str | None, diff --git a/homeassistant/components/control4/media_player.py b/homeassistant/components/control4/media_player.py new file mode 100644 index 00000000000..99d8c27face --- /dev/null +++ b/homeassistant/components/control4/media_player.py @@ -0,0 +1,392 @@ +"""Platform for Control4 Rooms Media Players.""" + +from __future__ import annotations + +from dataclasses import dataclass +from datetime import timedelta +import enum +import logging +from typing import Any + +from pyControl4.error_handling import C4Exception +from pyControl4.room import C4Room + +from homeassistant.components.media_player import ( + MediaPlayerDeviceClass, + MediaPlayerEntity, + MediaPlayerEntityFeature, + MediaPlayerState, + MediaType, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_SCAN_INTERVAL +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from . import Control4Entity +from .const import CONF_DIRECTOR, CONF_DIRECTOR_ALL_ITEMS, CONF_UI_CONFIGURATION, DOMAIN +from .director_utils import update_variables_for_config_entry + +_LOGGER = logging.getLogger(__name__) + +CONTROL4_POWER_STATE = "POWER_STATE" +CONTROL4_VOLUME_STATE = "CURRENT_VOLUME" +CONTROL4_MUTED_STATE = "IS_MUTED" +CONTROL4_CURRENT_VIDEO_DEVICE = "CURRENT_VIDEO_DEVICE" +CONTROL4_PLAYING = "PLAYING" +CONTROL4_PAUSED = "PAUSED" +CONTROL4_STOPPED = "STOPPED" +CONTROL4_MEDIA_INFO = "CURRENT MEDIA INFO" + +CONTROL4_PARENT_ID = "parentId" + +VARIABLES_OF_INTEREST = { + CONTROL4_POWER_STATE, + CONTROL4_VOLUME_STATE, + CONTROL4_MUTED_STATE, + CONTROL4_CURRENT_VIDEO_DEVICE, + CONTROL4_MEDIA_INFO, + CONTROL4_PLAYING, + CONTROL4_PAUSED, + CONTROL4_STOPPED, +} + + +class _SourceType(enum.Enum): + AUDIO = 1 + VIDEO = 2 + + +@dataclass +class _RoomSource: + """Class for Room Source.""" + + source_type: set[_SourceType] + idx: int + name: str + + +async def get_rooms(hass: HomeAssistant, entry: ConfigEntry): + """Return a list of all Control4 rooms.""" + director_all_items = hass.data[DOMAIN][entry.entry_id][CONF_DIRECTOR_ALL_ITEMS] + return [ + item + for item in director_all_items + if "typeName" in item and item["typeName"] == "room" + ] + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up Control4 rooms from a config entry.""" + all_rooms = await get_rooms(hass, entry) + if not all_rooms: + return + + entry_data = hass.data[DOMAIN][entry.entry_id] + scan_interval = entry_data[CONF_SCAN_INTERVAL] + _LOGGER.debug("Scan interval = %s", scan_interval) + + async def async_update_data() -> dict[int, dict[str, Any]]: + """Fetch data from Control4 director.""" + try: + return await update_variables_for_config_entry( + hass, entry, VARIABLES_OF_INTEREST + ) + except C4Exception as err: + raise UpdateFailed(f"Error communicating with API: {err}") from err + + coordinator = DataUpdateCoordinator[dict[int, dict[str, Any]]]( + hass, + _LOGGER, + name="room", + update_method=async_update_data, + update_interval=timedelta(seconds=scan_interval), + ) + + # Fetch initial data so we have data when entities subscribe + await coordinator.async_refresh() + + items_by_id = { + item["id"]: item + for item in hass.data[DOMAIN][entry.entry_id][CONF_DIRECTOR_ALL_ITEMS] + } + item_to_parent_map = { + k: item["parentId"] + for k, item in items_by_id.items() + if "parentId" in item and k > 1 + } + + ui_config = entry_data[CONF_UI_CONFIGURATION] + + entity_list = [] + for room in all_rooms: + room_id = room["id"] + + sources: dict[int, _RoomSource] = {} + for exp in ui_config["experiences"]: + if room_id == exp["room_id"]: + exp_type = exp["type"] + if exp_type not in ("listen", "watch"): + continue + + dev_type = ( + _SourceType.AUDIO if exp_type == "listen" else _SourceType.VIDEO + ) + for source in exp["sources"]["source"]: + dev_id = source["id"] + name = items_by_id.get(dev_id, {}).get( + "name", f"Unknown Device - {dev_id}" + ) + if dev_id in sources: + sources[dev_id].source_type.add(dev_type) + else: + sources[dev_id] = _RoomSource( + source_type={dev_type}, idx=dev_id, name=name + ) + + try: + hidden = room["roomHidden"] + entity_list.append( + Control4Room( + entry_data, + coordinator, + room["name"], + room_id, + item_to_parent_map, + sources, + hidden, + ) + ) + except KeyError: + _LOGGER.exception( + "Unknown device properties received from Control4: %s", + room, + ) + continue + + async_add_entities(entity_list, True) + + +class Control4Room(Control4Entity, MediaPlayerEntity): + """Control4 Room entity.""" + + _attr_has_entity_name = True + + def __init__( + self, + entry_data: dict, + coordinator: DataUpdateCoordinator[dict[int, dict[str, Any]]], + name: str, + room_id: int, + id_to_parent: dict[int, int], + sources: dict[int, _RoomSource], + room_hidden: bool, + ) -> None: + """Initialize Control4 room entity.""" + super().__init__( + entry_data, + coordinator, + None, + room_id, + device_name=name, + device_manufacturer=None, + device_model=None, + device_id=room_id, + ) + self._attr_entity_registry_enabled_default = not room_hidden + self._id_to_parent = id_to_parent + self._sources = sources + self._attr_supported_features = ( + MediaPlayerEntityFeature.PLAY + | MediaPlayerEntityFeature.PAUSE + | MediaPlayerEntityFeature.STOP + | MediaPlayerEntityFeature.VOLUME_MUTE + | MediaPlayerEntityFeature.VOLUME_SET + | MediaPlayerEntityFeature.VOLUME_STEP + | MediaPlayerEntityFeature.TURN_OFF + | MediaPlayerEntityFeature.SELECT_SOURCE + ) + + def _create_api_object(self): + """Create a pyControl4 device object. + + This exists so the director token used is always the latest one, without needing to re-init the entire entity. + """ + return C4Room(self.entry_data[CONF_DIRECTOR], self._idx) + + def _get_device_from_variable(self, var: str) -> int | None: + current_device = self.coordinator.data[self._idx][var] + if current_device == 0: + return None + + return current_device + + def _get_current_video_device_id(self) -> int | None: + return self._get_device_from_variable(CONTROL4_CURRENT_VIDEO_DEVICE) + + def _get_current_playing_device_id(self) -> int | None: + media_info = self._get_media_info() + if media_info: + if "medSrcDev" in media_info: + return media_info["medSrcDev"] + if "deviceid" in media_info: + return media_info["deviceid"] + return 0 + + def _get_media_info(self) -> dict | None: + """Get the Media Info Dictionary if populated.""" + media_info = self.coordinator.data[self._idx][CONTROL4_MEDIA_INFO] + if "mediainfo" in media_info: + return media_info["mediainfo"] + return None + + def _get_current_source_state(self) -> str | None: + current_source = self._get_current_playing_device_id() + while current_source: + current_data = self.coordinator.data.get(current_source, None) + if current_data: + if current_data.get(CONTROL4_PLAYING, None): + return MediaPlayerState.PLAYING + if current_data.get(CONTROL4_PAUSED, None): + return MediaPlayerState.PAUSED + if current_data.get(CONTROL4_STOPPED, None): + return MediaPlayerState.ON + current_source = self._id_to_parent.get(current_source, None) + return None + + @property + def device_class(self) -> MediaPlayerDeviceClass | None: + """Return the class of this entity.""" + for avail_source in self._sources.values(): + if _SourceType.VIDEO in avail_source.source_type: + return MediaPlayerDeviceClass.TV + return MediaPlayerDeviceClass.SPEAKER + + @property + def state(self): + """Return whether this room is on or idle.""" + + if source_state := self._get_current_source_state(): + return source_state + + if self.coordinator.data[self._idx][CONTROL4_POWER_STATE]: + return MediaPlayerState.ON + + return MediaPlayerState.IDLE + + @property + def source(self): + """Get the current source.""" + current_source = self._get_current_playing_device_id() + if not current_source or current_source not in self._sources: + return None + return self._sources[current_source].name + + @property + def media_title(self) -> str | None: + """Get the Media Title.""" + media_info = self._get_media_info() + if not media_info: + return None + if "title" in media_info: + return media_info["title"] + current_source = self._get_current_playing_device_id() + if not current_source or current_source not in self._sources: + return None + return self._sources[current_source].name + + @property + def media_content_type(self): + """Get current content type if available.""" + current_source = self._get_current_playing_device_id() + if not current_source: + return None + if current_source == self._get_current_video_device_id(): + return MediaType.VIDEO + return MediaType.MUSIC + + async def async_media_play_pause(self): + """If possible, toggle the current play/pause state. + + Not every source supports play/pause. + Unfortunately MediaPlayer capabilities are not dynamic, + so we must determine if play/pause is supported here + """ + if self._get_current_source_state(): + await super().async_media_play_pause() + + @property + def source_list(self) -> list[str]: + """Get the available source.""" + return [x.name for x in self._sources.values()] + + @property + def volume_level(self): + """Get the volume level.""" + return self.coordinator.data[self._idx][CONTROL4_VOLUME_STATE] / 100 + + @property + def is_volume_muted(self): + """Check if the volume is muted.""" + return bool(self.coordinator.data[self._idx][CONTROL4_MUTED_STATE]) + + async def async_select_source(self, source): + """Select a new source.""" + for avail_source in self._sources.values(): + if avail_source.name == source: + audio_only = _SourceType.VIDEO not in avail_source.source_type + if audio_only: + await self._create_api_object().setAudioSource(avail_source.idx) + else: + await self._create_api_object().setVideoAndAudioSource( + avail_source.idx + ) + break + + await self.coordinator.async_request_refresh() + + async def async_turn_off(self): + """Turn off the room.""" + await self._create_api_object().setRoomOff() + await self.coordinator.async_request_refresh() + + async def async_mute_volume(self, mute): + """Mute the room.""" + if mute: + await self._create_api_object().setMuteOn() + else: + await self._create_api_object().setMuteOff() + await self.coordinator.async_request_refresh() + + async def async_set_volume_level(self, volume): + """Set room volume, 0-1 scale.""" + await self._create_api_object().setVolume(int(volume * 100)) + await self.coordinator.async_request_refresh() + + async def async_volume_up(self): + """Increase the volume by 1.""" + await self._create_api_object().setIncrementVolume() + await self.coordinator.async_request_refresh() + + async def async_volume_down(self): + """Decrease the volume by 1.""" + await self._create_api_object().setDecrementVolume() + await self.coordinator.async_request_refresh() + + async def async_media_pause(self): + """Issue a pause command.""" + await self._create_api_object().setPause() + await self.coordinator.async_request_refresh() + + async def async_media_play(self): + """Issue a play command.""" + await self._create_api_object().setPlay() + await self.coordinator.async_request_refresh() + + async def async_media_stop(self): + """Issue a stop command.""" + await self._create_api_object().setStop() + await self.coordinator.async_request_refresh() diff --git a/homeassistant/components/conversation/__init__.py b/homeassistant/components/conversation/__init__.py index 09b0e8e2310..dd8fb967824 100644 --- a/homeassistant/components/conversation/__init__.py +++ b/homeassistant/components/conversation/__init__.py @@ -1,4 +1,5 @@ """Support for functionality to have conversations with Home Assistant.""" + from __future__ import annotations import asyncio @@ -484,7 +485,7 @@ class ConversationProcessView(http.HomeAssistantView): ) async def post(self, request: web.Request, data: dict[str, str]) -> web.Response: """Send a request for processing.""" - hass = request.app["hass"] + hass = request.app[http.KEY_HASS] result = await async_converse( hass, diff --git a/homeassistant/components/conversation/agent.py b/homeassistant/components/conversation/agent.py index 2eae3631187..22b3437907c 100644 --- a/homeassistant/components/conversation/agent.py +++ b/homeassistant/components/conversation/agent.py @@ -1,4 +1,5 @@ """Agent foundation for conversation integration.""" + from __future__ import annotations from abc import ABC, abstractmethod diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py index cd371ff0630..c0307c68908 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -34,6 +34,7 @@ from homeassistant.helpers import ( area_registry as ar, device_registry as dr, entity_registry as er, + floor_registry as fr, intent, start, template, @@ -43,7 +44,6 @@ from homeassistant.helpers.event import ( EventStateChangedData, async_track_state_added_domain, ) -from homeassistant.helpers.typing import EventType from homeassistant.util.json import JsonObjectType, json_loads_object from .agent import AbstractConversationAgent, ConversationInput, ConversationResult @@ -54,7 +54,9 @@ _DEFAULT_ERROR_TEXT = "Sorry, I couldn't understand that" _ENTITY_REGISTRY_UPDATE_FIELDS = ["aliases", "name", "original_name"] REGEX_TYPE = type(re.compile("")) -TRIGGER_CALLBACK_TYPE = Callable[[str, RecognizeResult], Awaitable[str | None]] +TRIGGER_CALLBACK_TYPE = Callable[ + [str, RecognizeResult, str | None], Awaitable[str | None] +] METADATA_CUSTOM_SENTENCE = "hass_custom_sentence" METADATA_CUSTOM_FILE = "hass_custom_file" @@ -115,7 +117,7 @@ def async_setup(hass: core.HomeAssistant) -> None: async_should_expose(hass, DOMAIN, entity_id) @core.callback - def async_entity_state_listener(event: EventType[EventStateChangedData]) -> None: + def async_entity_state_listener(event: core.Event[EventStateChangedData]) -> None: """Set expose flag on new entities.""" async_should_expose(hass, DOMAIN, event.data["entity_id"]) @@ -162,17 +164,22 @@ class DefaultAgent(AbstractConversationAgent): self.hass.bus.async_listen( ar.EVENT_AREA_REGISTRY_UPDATED, - self._async_handle_area_registry_changed, # type: ignore[arg-type] + self._async_handle_area_floor_registry_changed, + run_immediately=True, + ) + self.hass.bus.async_listen( + fr.EVENT_FLOOR_REGISTRY_UPDATED, + self._async_handle_area_floor_registry_changed, run_immediately=True, ) self.hass.bus.async_listen( er.EVENT_ENTITY_REGISTRY_UPDATED, - self._async_handle_entity_registry_changed, # type: ignore[arg-type] + self._async_handle_entity_registry_changed, run_immediately=True, ) self.hass.bus.async_listen( EVENT_STATE_CHANGED, - self._async_handle_state_changed, # type: ignore[arg-type] + self._async_handle_state_changed, run_immediately=True, ) async_listen_entity_updates( @@ -225,7 +232,7 @@ class DefaultAgent(AbstractConversationAgent): # Gather callback responses in parallel trigger_callbacks = [ self._trigger_sentences[trigger_id].callback( - result.sentence, trigger_result + result.sentence, trigger_result, user_input.device_id ) for trigger_id, trigger_result in result.matched_triggers.items() ] @@ -695,15 +702,18 @@ class DefaultAgent(AbstractConversationAgent): return lang_intents @core.callback - def _async_handle_area_registry_changed( - self, event: EventType[ar.EventAreaRegistryUpdatedData] + def _async_handle_area_floor_registry_changed( + self, + event: core.Event[ + ar.EventAreaRegistryUpdatedData | fr.EventFloorRegistryUpdatedData + ], ) -> None: - """Clear area area cache when the area registry has changed.""" + """Clear area/floor list cache when the area registry has changed.""" self._slot_lists = None @core.callback def _async_handle_entity_registry_changed( - self, event: EventType[er.EventEntityRegistryUpdatedData] + self, event: core.Event[er.EventEntityRegistryUpdatedData] ) -> None: """Clear names list cache when an entity registry entry has changed.""" if event.data["action"] != "update" or not any( @@ -714,7 +724,7 @@ class DefaultAgent(AbstractConversationAgent): @core.callback def _async_handle_state_changed( - self, event: EventType[EventStateChangedData] + self, event: core.Event[EventStateChangedData] ) -> None: """Clear names list cache when a state is added or removed from the state machine.""" if event.data["old_state"] and event.data["new_state"]: @@ -772,6 +782,8 @@ class DefaultAgent(AbstractConversationAgent): # Default name entity_names.append((state.name, state.name, context)) + _LOGGER.debug("Exposed entities: %s", entity_names) + # Expose all areas. # # We pass in area id here with the expectation that no two areas will @@ -787,11 +799,25 @@ class DefaultAgent(AbstractConversationAgent): area_names.append((alias, area.id)) - _LOGGER.debug("Exposed entities: %s", entity_names) + # Expose all floors. + # + # We pass in floor id here with the expectation that no two floors will + # share the same name or alias. + floors = fr.async_get(self.hass) + floor_names = [] + for floor in floors.async_list_floors(): + floor_names.append((floor.name, floor.floor_id)) + if floor.aliases: + for alias in floor.aliases: + if not alias.strip(): + continue + + floor_names.append((alias, floor.floor_id)) self._slot_lists = { "area": TextSlotList.from_tuples(area_names, allow_template=False), "name": TextSlotList.from_tuples(entity_names, allow_template=False), + "floor": TextSlotList.from_tuples(floor_names, allow_template=False), } return self._slot_lists @@ -952,6 +978,10 @@ def _get_unmatched_response(result: RecognizeResult) -> tuple[ErrorKey, dict[str # area only return ErrorKey.NO_AREA, {"area": unmatched_area} + if unmatched_floor := unmatched_text.get("floor"): + # floor only + return ErrorKey.NO_FLOOR, {"floor": unmatched_floor} + # Area may still have matched matched_area: str | None = None if matched_area_entity := result.entities.get("area"): @@ -999,6 +1029,13 @@ def _get_no_states_matched_response( "area": no_states_error.area, } + if no_states_error.floor: + # domain in floor + return ErrorKey.NO_DOMAIN_IN_FLOOR, { + "domain": domain, + "floor": no_states_error.floor, + } + # domain only return ErrorKey.NO_DOMAIN, {"domain": domain} diff --git a/homeassistant/components/conversation/icons.json b/homeassistant/components/conversation/icons.json new file mode 100644 index 00000000000..b39a1603b15 --- /dev/null +++ b/homeassistant/components/conversation/icons.json @@ -0,0 +1,6 @@ +{ + "services": { + "process": "mdi:message-processing", + "reload": "mdi:reload" + } +} diff --git a/homeassistant/components/conversation/manifest.json b/homeassistant/components/conversation/manifest.json index 00f645ea0f3..612e9b25c06 100644 --- a/homeassistant/components/conversation/manifest.json +++ b/homeassistant/components/conversation/manifest.json @@ -7,5 +7,5 @@ "integration_type": "system", "iot_class": "local_push", "quality_scale": "internal", - "requirements": ["hassil==1.6.1", "home-assistant-intents==2024.3.12"] + "requirements": ["hassil==1.6.1", "home-assistant-intents==2024.4.3"] } diff --git a/homeassistant/components/conversation/strings.json b/homeassistant/components/conversation/strings.json index 255e6cec430..3150623ba65 100644 --- a/homeassistant/components/conversation/strings.json +++ b/homeassistant/components/conversation/strings.json @@ -10,7 +10,7 @@ "description": "Transcribed text input." }, "language": { - "name": "Language", + "name": "[%key:common::config_flow::data::language%]", "description": "Language of text. Defaults to server language." }, "agent_id": { @@ -28,7 +28,7 @@ "description": "Reloads the intent configuration.", "fields": { "language": { - "name": "[%key:component::conversation::services::process::fields::language::name%]", + "name": "[%key:common::config_flow::data::language%]", "description": "Language to clear cached intents for. Defaults to server language." }, "agent_id": { diff --git a/homeassistant/components/conversation/trigger.py b/homeassistant/components/conversation/trigger.py index 4600135c1e5..0fadc458352 100644 --- a/homeassistant/components/conversation/trigger.py +++ b/homeassistant/components/conversation/trigger.py @@ -1,4 +1,5 @@ """Offer sentence based automation rules.""" + from __future__ import annotations from typing import Any @@ -61,7 +62,9 @@ async def async_attach_trigger( job = HassJob(action) - async def call_action(sentence: str, result: RecognizeResult) -> str | None: + async def call_action( + sentence: str, result: RecognizeResult, device_id: str | None + ) -> str | None: """Call action with right context.""" # Add slot values as extra trigger data @@ -69,9 +72,11 @@ async def async_attach_trigger( entity_name: { "name": entity_name, "text": entity.text.strip(), # remove whitespace - "value": entity.value.strip() - if isinstance(entity.value, str) - else entity.value, + "value": ( + entity.value.strip() + if isinstance(entity.value, str) + else entity.value + ), } for entity_name, entity in result.entities.items() } @@ -84,6 +89,7 @@ async def async_attach_trigger( "slots": { # direct access to values entity_name: entity["value"] for entity_name, entity in details.items() }, + "device_id": device_id, } # Wait for the automation to complete diff --git a/homeassistant/components/conversation/util.py b/homeassistant/components/conversation/util.py index 78fb4bd0ef5..b4ff2511ca1 100644 --- a/homeassistant/components/conversation/util.py +++ b/homeassistant/components/conversation/util.py @@ -1,4 +1,5 @@ """Util for Conversation.""" + from __future__ import annotations import re diff --git a/homeassistant/components/coolmaster/__init__.py b/homeassistant/components/coolmaster/__init__.py index d01310a6266..1f3f5a66380 100644 --- a/homeassistant/components/coolmaster/__init__.py +++ b/homeassistant/components/coolmaster/__init__.py @@ -1,4 +1,5 @@ """The Coolmaster integration.""" + from pycoolmasternet_async import CoolMasterNet from homeassistant.config_entries import ConfigEntry @@ -36,7 +37,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if not info: raise ConfigEntryNotReady except OSError as error: - raise ConfigEntryNotReady() from error + raise ConfigEntryNotReady from error coordinator = CoolmasterDataUpdateCoordinator(hass, coolmaster) hass.data.setdefault(DOMAIN, {}) await coordinator.async_config_entry_first_refresh() diff --git a/homeassistant/components/coolmaster/binary_sensor.py b/homeassistant/components/coolmaster/binary_sensor.py index 884d38c77dc..ba54a073f0a 100644 --- a/homeassistant/components/coolmaster/binary_sensor.py +++ b/homeassistant/components/coolmaster/binary_sensor.py @@ -1,4 +1,5 @@ """Binary Sensor platform for CoolMasterNet integration.""" + from __future__ import annotations from homeassistant.components.binary_sensor import ( diff --git a/homeassistant/components/coolmaster/button.py b/homeassistant/components/coolmaster/button.py index db9dd55ea0b..d958346614c 100644 --- a/homeassistant/components/coolmaster/button.py +++ b/homeassistant/components/coolmaster/button.py @@ -1,4 +1,5 @@ """Button platform for CoolMasterNet integration.""" + from __future__ import annotations from homeassistant.components.button import ButtonEntity, ButtonEntityDescription diff --git a/homeassistant/components/coolmaster/climate.py b/homeassistant/components/coolmaster/climate.py index ecb604a14cc..d3cb7122109 100644 --- a/homeassistant/components/coolmaster/climate.py +++ b/homeassistant/components/coolmaster/climate.py @@ -1,4 +1,5 @@ """CoolMasterNet platform to control of CoolMasterNet Climate Devices.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/coolmaster/config_flow.py b/homeassistant/components/coolmaster/config_flow.py index ad3817b77ce..19832eaef0a 100644 --- a/homeassistant/components/coolmaster/config_flow.py +++ b/homeassistant/components/coolmaster/config_flow.py @@ -1,4 +1,5 @@ """Config flow to configure Coolmaster.""" + from __future__ import annotations from typing import Any @@ -7,10 +8,9 @@ from pycoolmasternet_async import CoolMasterNet import voluptuous as vol from homeassistant.components.climate import HVACMode -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult from .const import CONF_SUPPORTED_MODES, CONF_SWING_SUPPORT, DEFAULT_PORT, DOMAIN @@ -46,7 +46,7 @@ class CoolmasterConfigFlow(ConfigFlow, domain=DOMAIN): VERSION = 1 @callback - def _async_get_entry(self, data: dict[str, Any]) -> FlowResult: + def _async_get_entry(self, data: dict[str, Any]) -> ConfigFlowResult: supported_modes = [ key for (key, value) in data.items() if key in AVAILABLE_MODES and value ] @@ -62,7 +62,7 @@ class CoolmasterConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" if user_input is None: return self.async_show_form(step_id="user", data_schema=DATA_SCHEMA) diff --git a/homeassistant/components/coolmaster/coordinator.py b/homeassistant/components/coolmaster/coordinator.py index 241f287e297..54d69b1c540 100644 --- a/homeassistant/components/coolmaster/coordinator.py +++ b/homeassistant/components/coolmaster/coordinator.py @@ -1,4 +1,5 @@ """DataUpdateCoordinator for coolmaster integration.""" + import logging from homeassistant.components.climate import SCAN_INTERVAL diff --git a/homeassistant/components/coolmaster/entity.py b/homeassistant/components/coolmaster/entity.py index 66572a56254..73bd1e13a26 100644 --- a/homeassistant/components/coolmaster/entity.py +++ b/homeassistant/components/coolmaster/entity.py @@ -1,4 +1,5 @@ """Base entity for Coolmaster integration.""" + from pycoolmasternet_async.coolmasternet import CoolMasterNetUnit from homeassistant.core import callback diff --git a/homeassistant/components/coolmaster/sensor.py b/homeassistant/components/coolmaster/sensor.py index 30b22f4f658..4c2a09b1ce5 100644 --- a/homeassistant/components/coolmaster/sensor.py +++ b/homeassistant/components/coolmaster/sensor.py @@ -1,4 +1,5 @@ """Sensor platform for CoolMasterNet integration.""" + from __future__ import annotations from homeassistant.components.sensor import SensorEntity, SensorEntityDescription diff --git a/homeassistant/components/counter/__init__.py b/homeassistant/components/counter/__init__.py index 7d69025fb97..a607a7bdebe 100644 --- a/homeassistant/components/counter/__init__.py +++ b/homeassistant/components/counter/__init__.py @@ -1,4 +1,5 @@ """Component to count within automations.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/counter/reproduce_state.py b/homeassistant/components/counter/reproduce_state.py index 2308e0fb07a..42c68d1f344 100644 --- a/homeassistant/components/counter/reproduce_state.py +++ b/homeassistant/components/counter/reproduce_state.py @@ -1,4 +1,5 @@ """Reproduce an Counter state.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/cover/__init__.py b/homeassistant/components/cover/__init__.py index 89f79ca9d7a..1eac6844703 100644 --- a/homeassistant/components/cover/__init__.py +++ b/homeassistant/components/cover/__init__.py @@ -1,4 +1,5 @@ """Support for Cover devices.""" + from __future__ import annotations from collections.abc import Callable @@ -43,6 +44,8 @@ from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass +from . import group as group_pre_import # noqa: F401 + if TYPE_CHECKING: from functools import cached_property else: @@ -378,7 +381,7 @@ class CoverEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): def open_cover(self, **kwargs: Any) -> None: """Open the cover.""" - raise NotImplementedError() + raise NotImplementedError async def async_open_cover(self, **kwargs: Any) -> None: """Open the cover.""" @@ -386,7 +389,7 @@ class CoverEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): def close_cover(self, **kwargs: Any) -> None: """Close cover.""" - raise NotImplementedError() + raise NotImplementedError async def async_close_cover(self, **kwargs: Any) -> None: """Close cover.""" diff --git a/homeassistant/components/cover/device_action.py b/homeassistant/components/cover/device_action.py index 2224e5bab1c..acef2cde4d8 100644 --- a/homeassistant/components/cover/device_action.py +++ b/homeassistant/components/cover/device_action.py @@ -1,4 +1,5 @@ """Provides device automations for Cover.""" + from __future__ import annotations import voluptuous as vol diff --git a/homeassistant/components/cover/device_condition.py b/homeassistant/components/cover/device_condition.py index 23ec7d75650..9c746284fe5 100644 --- a/homeassistant/components/cover/device_condition.py +++ b/homeassistant/components/cover/device_condition.py @@ -1,4 +1,5 @@ """Provides device automations for Cover.""" + from __future__ import annotations import voluptuous as vol diff --git a/homeassistant/components/cover/device_trigger.py b/homeassistant/components/cover/device_trigger.py index 8225348619d..302b1d4340a 100644 --- a/homeassistant/components/cover/device_trigger.py +++ b/homeassistant/components/cover/device_trigger.py @@ -1,4 +1,5 @@ """Provides device automations for Cover.""" + from __future__ import annotations import voluptuous as vol diff --git a/homeassistant/components/cover/group.py b/homeassistant/components/cover/group.py index 28a1dc530fe..a4b682b84ff 100644 --- a/homeassistant/components/cover/group.py +++ b/homeassistant/components/cover/group.py @@ -1,14 +1,17 @@ """Describe group states.""" +from typing import TYPE_CHECKING -from homeassistant.components.group import GroupIntegrationRegistry from homeassistant.const import STATE_CLOSED, STATE_OPEN from homeassistant.core import HomeAssistant, callback +if TYPE_CHECKING: + from homeassistant.components.group import GroupIntegrationRegistry + @callback def async_describe_on_off_states( - hass: HomeAssistant, registry: GroupIntegrationRegistry + hass: HomeAssistant, registry: "GroupIntegrationRegistry" ) -> None: """Describe group on off states.""" # On means open, Off means closed diff --git a/homeassistant/components/cover/intent.py b/homeassistant/components/cover/intent.py index dc8f722c7ed..a77bfbcbd16 100644 --- a/homeassistant/components/cover/intent.py +++ b/homeassistant/components/cover/intent.py @@ -1,6 +1,5 @@ """Intents for the cover integration.""" - from homeassistant.const import SERVICE_CLOSE_COVER, SERVICE_OPEN_COVER from homeassistant.core import HomeAssistant from homeassistant.helpers import intent diff --git a/homeassistant/components/cover/reproduce_state.py b/homeassistant/components/cover/reproduce_state.py index 59846627890..59f3df61795 100644 --- a/homeassistant/components/cover/reproduce_state.py +++ b/homeassistant/components/cover/reproduce_state.py @@ -1,4 +1,5 @@ """Reproduce an Cover state.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/cover/significant_change.py b/homeassistant/components/cover/significant_change.py index ca822c5e9e1..32f62057b93 100644 --- a/homeassistant/components/cover/significant_change.py +++ b/homeassistant/components/cover/significant_change.py @@ -1,4 +1,5 @@ """Helper to test significant Cover state changes.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/cppm_tracker/device_tracker.py b/homeassistant/components/cppm_tracker/device_tracker.py index 0c7acd33f23..8978028641d 100644 --- a/homeassistant/components/cppm_tracker/device_tracker.py +++ b/homeassistant/components/cppm_tracker/device_tracker.py @@ -1,4 +1,5 @@ """Support for ClearPass Policy Manager.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/cpuspeed/__init__.py b/homeassistant/components/cpuspeed/__init__.py index da1e0129117..cb967b950c0 100644 --- a/homeassistant/components/cpuspeed/__init__.py +++ b/homeassistant/components/cpuspeed/__init__.py @@ -1,4 +1,5 @@ """The CPU Speed integration.""" + from cpuinfo import cpuinfo from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/cpuspeed/config_flow.py b/homeassistant/components/cpuspeed/config_flow.py index 3c7d529364c..ac35cc0fc4f 100644 --- a/homeassistant/components/cpuspeed/config_flow.py +++ b/homeassistant/components/cpuspeed/config_flow.py @@ -1,12 +1,12 @@ """Config flow to configure the CPU Speed integration.""" + from __future__ import annotations from typing import Any from cpuinfo import cpuinfo -from homeassistant.config_entries import ConfigFlow -from homeassistant.data_entry_flow import FlowResult +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from .const import DOMAIN @@ -20,7 +20,7 @@ class CPUSpeedFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" await self.async_set_unique_id(DOMAIN) self._abort_if_unique_id_configured() diff --git a/homeassistant/components/cpuspeed/const.py b/homeassistant/components/cpuspeed/const.py index 8fdb8d3c986..73c637a8e26 100644 --- a/homeassistant/components/cpuspeed/const.py +++ b/homeassistant/components/cpuspeed/const.py @@ -1,4 +1,5 @@ """Constants for the CPU Speed integration.""" + import logging from typing import Final diff --git a/homeassistant/components/cpuspeed/diagnostics.py b/homeassistant/components/cpuspeed/diagnostics.py index a93c71430ef..64fe7f86fa2 100644 --- a/homeassistant/components/cpuspeed/diagnostics.py +++ b/homeassistant/components/cpuspeed/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for CPU Speed.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/cpuspeed/sensor.py b/homeassistant/components/cpuspeed/sensor.py index 0df83b24d70..6a14f7ad13f 100644 --- a/homeassistant/components/cpuspeed/sensor.py +++ b/homeassistant/components/cpuspeed/sensor.py @@ -1,4 +1,5 @@ """Support for displaying the current CPU speed.""" + from __future__ import annotations from cpuinfo import cpuinfo diff --git a/homeassistant/components/crownstone/__init__.py b/homeassistant/components/crownstone/__init__.py index 92b2f4de5ca..e1443eb9516 100644 --- a/homeassistant/components/crownstone/__init__.py +++ b/homeassistant/components/crownstone/__init__.py @@ -1,4 +1,5 @@ """Integration for Crownstone.""" + from __future__ import annotations from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/crownstone/config_flow.py b/homeassistant/components/crownstone/config_flow.py index 7c0ea4fd27d..0e707c0805a 100644 --- a/homeassistant/components/crownstone/config_flow.py +++ b/homeassistant/components/crownstone/config_flow.py @@ -1,4 +1,5 @@ """Flow handler for Crownstone.""" + from __future__ import annotations from collections.abc import Callable @@ -14,10 +15,15 @@ from serial.tools.list_ports_common import ListPortInfo import voluptuous as vol from homeassistant.components import usb -from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow +from homeassistant.config_entries import ( + ConfigEntry, + ConfigEntryBaseFlow, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.const import CONF_EMAIL, CONF_PASSWORD from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowHandler, FlowResult from homeassistant.helpers import aiohttp_client from .const import ( @@ -37,13 +43,13 @@ CONFIG_FLOW = "config_flow" OPTIONS_FLOW = "options_flow" -class BaseCrownstoneFlowHandler(FlowHandler): +class BaseCrownstoneFlowHandler(ConfigEntryBaseFlow): """Represent the base flow for Crownstone.""" cloud: CrownstoneCloud def __init__( - self, flow_type: str, create_entry_cb: Callable[..., FlowResult] + self, flow_type: str, create_entry_cb: Callable[..., ConfigFlowResult] ) -> None: """Set up flow instance.""" self.flow_type = flow_type @@ -53,7 +59,7 @@ class BaseCrownstoneFlowHandler(FlowHandler): async def async_step_usb_config( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Set up a Crownstone USB dongle.""" list_of_ports = await self.hass.async_add_executor_job( serial.tools.list_ports.comports @@ -91,7 +97,7 @@ class BaseCrownstoneFlowHandler(FlowHandler): async def async_step_usb_manual_config( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Manually enter Crownstone USB dongle path.""" if user_input is None: return self.async_show_form( @@ -104,7 +110,7 @@ class BaseCrownstoneFlowHandler(FlowHandler): async def async_step_usb_sphere_config( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Select a Crownstone sphere that the USB operates in.""" spheres = {sphere.name: sphere.cloud_id for sphere in self.cloud.cloud_data} # no need to select if there's only 1 option @@ -146,7 +152,7 @@ class CrownstoneConfigFlowHandler(BaseCrownstoneFlowHandler, ConfigFlow, domain= async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors: dict[str, str] = {} if user_input is None: @@ -189,7 +195,7 @@ class CrownstoneConfigFlowHandler(BaseCrownstoneFlowHandler, ConfigFlow, domain= self.login_info = user_input return await self.async_step_usb_config() - def async_create_new_entry(self) -> FlowResult: + def async_create_new_entry(self) -> ConfigFlowResult: """Create a new entry.""" return super().async_create_entry( title=f"Account: {self.login_info[CONF_EMAIL]}", @@ -212,7 +218,7 @@ class CrownstoneOptionsFlowHandler(BaseCrownstoneFlowHandler, OptionsFlow): async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Manage Crownstone options.""" self.cloud: CrownstoneCloud = self.hass.data[DOMAIN][self.entry.entry_id].cloud @@ -250,7 +256,7 @@ class CrownstoneOptionsFlowHandler(BaseCrownstoneFlowHandler, OptionsFlow): return self.async_show_form(step_id="init", data_schema=options_schema) - def async_create_new_entry(self) -> FlowResult: + def async_create_new_entry(self) -> ConfigFlowResult: """Create a new entry.""" # these attributes will only change when a usb was configured if self.usb_path is not None and self.usb_sphere_id is not None: diff --git a/homeassistant/components/crownstone/const.py b/homeassistant/components/crownstone/const.py index 9b3624a4575..5325a476266 100644 --- a/homeassistant/components/crownstone/const.py +++ b/homeassistant/components/crownstone/const.py @@ -1,4 +1,5 @@ """Constants for the crownstone integration.""" + from __future__ import annotations from typing import Final diff --git a/homeassistant/components/crownstone/devices.py b/homeassistant/components/crownstone/devices.py index 5645d3edd1f..4995702701d 100644 --- a/homeassistant/components/crownstone/devices.py +++ b/homeassistant/components/crownstone/devices.py @@ -1,4 +1,5 @@ """Base classes for Crownstone devices.""" + from __future__ import annotations from crownstone_cloud.cloud_models.crownstones import Crownstone diff --git a/homeassistant/components/crownstone/entry_manager.py b/homeassistant/components/crownstone/entry_manager.py index bb6b00942e5..efee05a19c8 100644 --- a/homeassistant/components/crownstone/entry_manager.py +++ b/homeassistant/components/crownstone/entry_manager.py @@ -1,4 +1,5 @@ """Manager to set up IO with Crownstone devices for a config entry.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/crownstone/helpers.py b/homeassistant/components/crownstone/helpers.py index 94df9b9c5c3..0dc86ea5f36 100644 --- a/homeassistant/components/crownstone/helpers.py +++ b/homeassistant/components/crownstone/helpers.py @@ -1,4 +1,5 @@ """Helper functions for the Crownstone integration.""" + from __future__ import annotations import os @@ -23,17 +24,18 @@ def list_ports_as_str( if no_usb_option: ports_as_string.append(DONT_USE_USB) - for port in serial_ports: - ports_as_string.append( - usb.human_readable_device_name( - port.device, - port.serial_number, - port.manufacturer, - port.description, - f"{hex(port.vid)[2:]:0>4}".upper() if port.vid else None, - f"{hex(port.pid)[2:]:0>4}".upper() if port.pid else None, - ) + ports_as_string.extend( + usb.human_readable_device_name( + port.device, + port.serial_number, + port.manufacturer, + port.description, + f"{hex(port.vid)[2:]:0>4}".upper() if port.vid else None, + f"{hex(port.pid)[2:]:0>4}".upper() if port.pid else None, ) + for port in serial_ports + ) + ports_as_string.append(MANUAL_PATH) ports_as_string.append(REFRESH_LIST) diff --git a/homeassistant/components/crownstone/light.py b/homeassistant/components/crownstone/light.py index a95238bcdbe..37904408606 100644 --- a/homeassistant/components/crownstone/light.py +++ b/homeassistant/components/crownstone/light.py @@ -1,4 +1,5 @@ """Support for Crownstone devices.""" + from __future__ import annotations from functools import partial diff --git a/homeassistant/components/crownstone/listeners.py b/homeassistant/components/crownstone/listeners.py index 6c611e27083..2642e1501ef 100644 --- a/homeassistant/components/crownstone/listeners.py +++ b/homeassistant/components/crownstone/listeners.py @@ -3,6 +3,7 @@ For data updates, Cloud Push is used in form of an SSE server that sends out events. For fast device switching Local Push is used in form of a USB dongle that hooks into a BLE mesh. """ + from __future__ import annotations from functools import partial diff --git a/homeassistant/components/cups/sensor.py b/homeassistant/components/cups/sensor.py index dd5366dee6a..647deee79a6 100644 --- a/homeassistant/components/cups/sensor.py +++ b/homeassistant/components/cups/sensor.py @@ -1,4 +1,5 @@ """Details about printers which are connected to CUPS.""" + from __future__ import annotations from datetime import timedelta @@ -73,7 +74,7 @@ def setup_platform( data.update() if data.available is False: _LOGGER.error("Unable to connect to CUPS server: %s:%s", host, port) - raise PlatformNotReady() + raise PlatformNotReady assert data.printers is not None dev: list[SensorEntity] = [] @@ -84,8 +85,10 @@ def setup_platform( dev.append(CupsSensor(data, printer)) if "marker-names" in data.attributes[printer]: - for marker in data.attributes[printer]["marker-names"]: - dev.append(MarkerSensor(data, printer, marker, True)) + dev.extend( + MarkerSensor(data, printer, marker, True) + for marker in data.attributes[printer]["marker-names"] + ) add_entities(dev, True) return @@ -94,7 +97,7 @@ def setup_platform( data.update() if data.available is False: _LOGGER.error("Unable to connect to IPP printer: %s:%s", host, port) - raise PlatformNotReady() + raise PlatformNotReady dev = [] for printer in printers: diff --git a/homeassistant/components/currencylayer/sensor.py b/homeassistant/components/currencylayer/sensor.py index b4a33392894..2fdf521ad9f 100644 --- a/homeassistant/components/currencylayer/sensor.py +++ b/homeassistant/components/currencylayer/sensor.py @@ -1,4 +1,5 @@ """Support for currencylayer.com exchange rates service.""" + from __future__ import annotations from datetime import timedelta @@ -47,12 +48,12 @@ def setup_platform( rest = CurrencylayerData(_RESOURCE, parameters) response = requests.get(_RESOURCE, params=parameters, timeout=10) - sensors = [] - for variable in config[CONF_QUOTE]: - sensors.append(CurrencylayerSensor(rest, base, variable)) if "error" in response.json(): return - add_entities(sensors, True) + add_entities( + (CurrencylayerSensor(rest, base, variable) for variable in config[CONF_QUOTE]), + True, + ) class CurrencylayerSensor(SensorEntity): diff --git a/homeassistant/components/daikin/__init__.py b/homeassistant/components/daikin/__init__.py index b8e87d2b200..f0b62e95b1f 100644 --- a/homeassistant/components/daikin/__init__.py +++ b/homeassistant/components/daikin/__init__.py @@ -1,4 +1,5 @@ """Platform for the Daikin AC.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/daikin/climate.py b/homeassistant/components/daikin/climate.py index c6bab19aa8a..34ae8701d5d 100644 --- a/homeassistant/components/daikin/climate.py +++ b/homeassistant/components/daikin/climate.py @@ -1,4 +1,5 @@ """Support for the Daikin HVAC.""" + from __future__ import annotations import logging @@ -177,9 +178,9 @@ class DaikinClimate(ClimateEntity): # temperature elif attr == ATTR_TEMPERATURE: try: - values[ - HA_ATTR_TO_DAIKIN[ATTR_TARGET_TEMPERATURE] - ] = format_target_temperature(value) + values[HA_ATTR_TO_DAIKIN[ATTR_TARGET_TEMPERATURE]] = ( + format_target_temperature(value) + ) except ValueError: _LOGGER.error("Invalid temperature %s", value) diff --git a/homeassistant/components/daikin/config_flow.py b/homeassistant/components/daikin/config_flow.py index abd2d78c7fb..2acbe42264d 100644 --- a/homeassistant/components/daikin/config_flow.py +++ b/homeassistant/components/daikin/config_flow.py @@ -1,4 +1,5 @@ """Config flow for the Daikin platform.""" + from __future__ import annotations import asyncio @@ -11,10 +12,9 @@ from pydaikin.daikin_base import Appliance, DaikinException from pydaikin.discovery import Discovery import voluptuous as vol -from homeassistant import config_entries from homeassistant.components import zeroconf +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PASSWORD, CONF_UUID -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN, KEY_MAC, TIMEOUT @@ -22,7 +22,7 @@ from .const import DOMAIN, KEY_MAC, TIMEOUT _LOGGER = logging.getLogger(__name__) -class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class FlowHandler(ConfigFlow, domain=DOMAIN): """Handle a config flow.""" VERSION = 1 @@ -49,7 +49,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): key: str | None = None, uuid: str | None = None, password: str | None = None, - ) -> FlowResult: + ) -> ConfigFlowResult: """Register new entry.""" if not self.unique_id: await self.async_set_unique_id(mac) @@ -68,7 +68,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def _create_device( self, host: str, key: str | None = None, password: str | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Create device.""" # BRP07Cxx devices needs uuid together with key if key: @@ -122,7 +122,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """User initiated config flow.""" if user_input is None: return self.async_show_form(step_id="user", data_schema=self.schema) @@ -141,7 +141,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_zeroconf( self, discovery_info: zeroconf.ZeroconfServiceInfo - ) -> FlowResult: + ) -> ConfigFlowResult: """Prepare configuration for a discovered Daikin device.""" _LOGGER.debug("Zeroconf user_input: %s", discovery_info) devices = Discovery().poll(ip=discovery_info.host) diff --git a/homeassistant/components/daikin/const.py b/homeassistant/components/daikin/const.py index 67d014ecdb3..690267e5c83 100644 --- a/homeassistant/components/daikin/const.py +++ b/homeassistant/components/daikin/const.py @@ -1,4 +1,5 @@ """Constants for Daikin.""" + DOMAIN = "daikin" ATTR_TARGET_TEMPERATURE = "target_temperature" diff --git a/homeassistant/components/daikin/sensor.py b/homeassistant/components/daikin/sensor.py index b890ad823f7..a17a80f2065 100644 --- a/homeassistant/components/daikin/sensor.py +++ b/homeassistant/components/daikin/sensor.py @@ -1,4 +1,5 @@ """Support for Daikin AC sensors.""" + from __future__ import annotations from collections.abc import Callable @@ -39,18 +40,13 @@ from .const import ( ) -@dataclass(frozen=True) -class DaikinRequiredKeysMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class DaikinSensorEntityDescription(SensorEntityDescription): + """Describes Daikin sensor entity.""" value_func: Callable[[Appliance], float | None] -@dataclass(frozen=True) -class DaikinSensorEntityDescription(SensorEntityDescription, DaikinRequiredKeysMixin): - """Describes Daikin sensor entity.""" - - SENSOR_TYPES: tuple[DaikinSensorEntityDescription, ...] = ( DaikinSensorEntityDescription( key=ATTR_INSIDE_TEMPERATURE, diff --git a/homeassistant/components/daikin/switch.py b/homeassistant/components/daikin/switch.py index dd157774d6e..af94e98a337 100644 --- a/homeassistant/components/daikin/switch.py +++ b/homeassistant/components/daikin/switch.py @@ -1,4 +1,5 @@ """Support for Daikin AirBase zones.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/danfoss_air/__init__.py b/homeassistant/components/danfoss_air/__init__.py index 5069a62bcdf..5e4880705d5 100644 --- a/homeassistant/components/danfoss_air/__init__.py +++ b/homeassistant/components/danfoss_air/__init__.py @@ -1,4 +1,5 @@ """Support for Danfoss Air HRV.""" + from datetime import timedelta import logging diff --git a/homeassistant/components/danfoss_air/binary_sensor.py b/homeassistant/components/danfoss_air/binary_sensor.py index 3764345a7b8..358d6ca07ab 100644 --- a/homeassistant/components/danfoss_air/binary_sensor.py +++ b/homeassistant/components/danfoss_air/binary_sensor.py @@ -1,4 +1,5 @@ """Support for the for Danfoss Air HRV binary sensors.""" + from __future__ import annotations from pydanfossair.commands import ReadCommand @@ -32,12 +33,13 @@ def setup_platform( ["Danfoss Air Away Mode Active", ReadCommand.away_mode, None], ] - dev = [] - - for sensor in sensors: - dev.append(DanfossAirBinarySensor(data, sensor[0], sensor[1], sensor[2])) - - add_entities(dev, True) + add_entities( + ( + DanfossAirBinarySensor(data, sensor[0], sensor[1], sensor[2]) + for sensor in sensors + ), + True, + ) class DanfossAirBinarySensor(BinarySensorEntity): diff --git a/homeassistant/components/danfoss_air/sensor.py b/homeassistant/components/danfoss_air/sensor.py index 024bb50ba34..85b4e89d434 100644 --- a/homeassistant/components/danfoss_air/sensor.py +++ b/homeassistant/components/danfoss_air/sensor.py @@ -1,4 +1,5 @@ """Support for the for Danfoss Air HRV sensors.""" + from __future__ import annotations import logging @@ -96,14 +97,13 @@ def setup_platform( ], ] - dev = [] - - for sensor in sensors: - dev.append( + add_entities( + ( DanfossAir(data, sensor[0], sensor[1], sensor[2], sensor[3], sensor[4]) - ) - - add_entities(dev, True) + for sensor in sensors + ), + True, + ) class DanfossAir(SensorEntity): diff --git a/homeassistant/components/danfoss_air/switch.py b/homeassistant/components/danfoss_air/switch.py index b1ee7dce44a..dc3277078b0 100644 --- a/homeassistant/components/danfoss_air/switch.py +++ b/homeassistant/components/danfoss_air/switch.py @@ -1,4 +1,5 @@ """Support for the for Danfoss Air HRV sswitches.""" + from __future__ import annotations import logging @@ -46,12 +47,10 @@ def setup_platform( ], ] - dev = [] - - for switch in switches: - dev.append(DanfossAir(data, switch[0], switch[1], switch[2], switch[3])) - - add_entities(dev) + add_entities( + DanfossAir(data, switch[0], switch[1], switch[2], switch[3]) + for switch in switches + ) class DanfossAir(SwitchEntity): diff --git a/homeassistant/components/datadog/__init__.py b/homeassistant/components/datadog/__init__.py index 47a2d34ec8e..2d550e48e2f 100644 --- a/homeassistant/components/datadog/__init__.py +++ b/homeassistant/components/datadog/__init__.py @@ -1,4 +1,5 @@ """Support for sending data to Datadog.""" + import logging from datadog import initialize, statsd diff --git a/homeassistant/components/date/__init__.py b/homeassistant/components/date/__init__.py index 00ec09043c9..3cb6ad3a77d 100644 --- a/homeassistant/components/date/__init__.py +++ b/homeassistant/components/date/__init__.py @@ -1,4 +1,5 @@ """Component to allow setting date as platforms.""" + from __future__ import annotations from datetime import date, timedelta @@ -109,7 +110,7 @@ class DateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): def set_value(self, value: date) -> None: """Change the date.""" - raise NotImplementedError() + raise NotImplementedError async def async_set_value(self, value: date) -> None: """Change the date.""" diff --git a/homeassistant/components/datetime/__init__.py b/homeassistant/components/datetime/__init__.py index 9a509aadc70..420cf27b5aa 100644 --- a/homeassistant/components/datetime/__init__.py +++ b/homeassistant/components/datetime/__init__.py @@ -1,4 +1,5 @@ """Component to allow setting date/time as platforms.""" + from __future__ import annotations from datetime import UTC, datetime, timedelta @@ -125,7 +126,7 @@ class DateTimeEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): def set_value(self, value: datetime) -> None: """Change the date/time.""" - raise NotImplementedError() + raise NotImplementedError async def async_set_value(self, value: datetime) -> None: """Change the date/time.""" diff --git a/homeassistant/components/ddwrt/device_tracker.py b/homeassistant/components/ddwrt/device_tracker.py index 7874786adba..21786a292f4 100644 --- a/homeassistant/components/ddwrt/device_tracker.py +++ b/homeassistant/components/ddwrt/device_tracker.py @@ -1,4 +1,5 @@ """Support for DD-WRT routers.""" + from __future__ import annotations from http import HTTPStatus @@ -97,7 +98,7 @@ class DdWrtDeviceScanner(DeviceScanner): elements = cleaned_str.split(",") num_clients = int(len(elements) / 5) self.mac2name = {} - for idx in range(0, num_clients): + for idx in range(num_clients): # The data is a single array # every 5 elements represents one host, the MAC # is the third element and the name is the first. diff --git a/homeassistant/components/debugpy/__init__.py b/homeassistant/components/debugpy/__init__.py index 1dc0f525c4d..5caf517a483 100644 --- a/homeassistant/components/debugpy/__init__.py +++ b/homeassistant/components/debugpy/__init__.py @@ -1,11 +1,12 @@ """The Remote Python Debugger integration.""" + from __future__ import annotations from asyncio import Event, get_running_loop import logging from threading import Thread -import debugpy +import debugpy # noqa: T100 import voluptuous as vol from homeassistant.const import CONF_HOST, CONF_PORT @@ -59,7 +60,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: ready = Event() def waitfor(): - debugpy.wait_for_client() + debugpy.wait_for_client() # noqa: T100 hass.loop.call_soon_threadsafe(ready.set) Thread(target=waitfor).start() diff --git a/homeassistant/components/debugpy/icons.json b/homeassistant/components/debugpy/icons.json new file mode 100644 index 00000000000..b3bb4dde23a --- /dev/null +++ b/homeassistant/components/debugpy/icons.json @@ -0,0 +1,5 @@ +{ + "services": { + "start": "mdi:play" + } +} diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index 4750c40fab2..4952cb3dafc 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -2,24 +2,16 @@ from __future__ import annotations -from typing import cast - from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - CONF_API_KEY, - CONF_HOST, - CONF_PORT, - EVENT_HOMEASSISTANT_STOP, -) -from homeassistant.core import HomeAssistant, callback +from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady -import homeassistant.helpers.entity_registry as er -from .config_flow import get_master_gateway -from .const import CONF_GROUP_ID_BASE, CONF_MASTER_GATEWAY, DOMAIN, PLATFORMS +from .config_flow import get_master_hub +from .const import CONF_MASTER_GATEWAY, DOMAIN, PLATFORMS from .deconz_event import async_setup_events, async_unload_events from .errors import AuthenticationRequired, CannotConnect -from .gateway import DeconzGateway, get_deconz_session +from .hub import DeconzHub, get_deconz_api from .services import async_setup_services, async_unload_services @@ -31,13 +23,11 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b """ hass.data.setdefault(DOMAIN, {}) - await async_update_group_unique_id(hass, config_entry) - if not config_entry.options: - await async_update_master_gateway(hass, config_entry) + await async_update_master_hub(hass, config_entry) try: - api = await get_deconz_session(hass, config_entry.data) + api = await get_deconz_api(hass, config_entry) except CannotConnect as err: raise ConfigEntryNotReady from err except AuthenticationRequired as err: @@ -46,20 +36,18 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b if not hass.data[DOMAIN]: async_setup_services(hass) - gateway = hass.data[DOMAIN][config_entry.entry_id] = DeconzGateway( - hass, config_entry, api - ) - await gateway.async_update_device_registry() + hub = hass.data[DOMAIN][config_entry.entry_id] = DeconzHub(hass, config_entry, api) + await hub.async_update_device_registry() - config_entry.add_update_listener(gateway.async_config_entry_updated) + config_entry.add_update_listener(hub.async_config_entry_updated) - await async_setup_events(gateway) + await async_setup_events(hub) await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) api.start() config_entry.async_on_unload( - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, gateway.shutdown) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, hub.shutdown) ) return True @@ -67,64 +55,34 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Unload deCONZ config entry.""" - gateway: DeconzGateway = hass.data[DOMAIN].pop(config_entry.entry_id) - async_unload_events(gateway) + hub: DeconzHub = hass.data[DOMAIN].pop(config_entry.entry_id) + async_unload_events(hub) if not hass.data[DOMAIN]: async_unload_services(hass) - elif gateway.master: - await async_update_master_gateway(hass, config_entry) - new_master_gateway = next(iter(hass.data[DOMAIN].values())) - await async_update_master_gateway(hass, new_master_gateway.config_entry) + elif hub.master: + await async_update_master_hub(hass, config_entry) + new_master_hub = next(iter(hass.data[DOMAIN].values())) + await async_update_master_hub(hass, new_master_hub.config_entry) - return await gateway.async_reset() + return await hub.async_reset() -async def async_update_master_gateway( +async def async_update_master_hub( hass: HomeAssistant, config_entry: ConfigEntry ) -> None: - """Update master gateway boolean. + """Update master hub boolean. Called by setup_entry and unload_entry. Makes sure there is always one master available. """ try: - master_gateway = get_master_gateway(hass) - master = master_gateway.config_entry == config_entry + master_hub = get_master_hub(hass) + master = master_hub.config_entry == config_entry except ValueError: master = True options = {**config_entry.options, CONF_MASTER_GATEWAY: master} hass.config_entries.async_update_entry(config_entry, options=options) - - -async def async_update_group_unique_id( - hass: HomeAssistant, config_entry: ConfigEntry -) -> None: - """Update unique ID entities based on deCONZ groups.""" - if not (group_id_base := config_entry.data.get(CONF_GROUP_ID_BASE)): - return - - old_unique_id = cast(str, group_id_base) - new_unique_id = cast(str, config_entry.unique_id) - - @callback - def update_unique_id(entity_entry: er.RegistryEntry) -> dict[str, str] | None: - """Update unique ID of entity entry.""" - if f"{old_unique_id}-" not in entity_entry.unique_id: - return None - return { - "new_unique_id": entity_entry.unique_id.replace( - old_unique_id, new_unique_id - ) - } - - await er.async_migrate_entries(hass, config_entry.entry_id, update_unique_id) - data = { - CONF_API_KEY: config_entry.data[CONF_API_KEY], - CONF_HOST: config_entry.data[CONF_HOST], - CONF_PORT: config_entry.data[CONF_PORT], - } - hass.config_entries.async_update_entry(config_entry, data=data) diff --git a/homeassistant/components/deconz/alarm_control_panel.py b/homeassistant/components/deconz/alarm_control_panel.py index 179fa2320df..ae230c783f9 100644 --- a/homeassistant/components/deconz/alarm_control_panel.py +++ b/homeassistant/components/deconz/alarm_control_panel.py @@ -1,4 +1,5 @@ """Support for deCONZ alarm control panel devices.""" + from __future__ import annotations from pydeconz.models.alarm_system import AlarmSystemArmAction @@ -28,7 +29,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from .deconz_device import DeconzDevice -from .gateway import DeconzGateway, get_gateway_from_config_entry +from .hub import DeconzHub DECONZ_TO_ALARM_STATE = { AncillaryControlPanel.ARMED_AWAY: STATE_ALARM_ARMED_AWAY, @@ -44,11 +45,9 @@ DECONZ_TO_ALARM_STATE = { } -def get_alarm_system_id_for_unique_id( - gateway: DeconzGateway, unique_id: str -) -> str | None: +def get_alarm_system_id_for_unique_id(hub: DeconzHub, unique_id: str) -> str | None: """Retrieve alarm system ID the unique ID is registered to.""" - for alarm_system in gateway.api.alarm_systems.values(): + for alarm_system in hub.api.alarm_systems.values(): if unique_id in alarm_system.devices: return alarm_system.resource_id return None @@ -60,23 +59,19 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the deCONZ alarm control panel devices.""" - gateway = get_gateway_from_config_entry(hass, config_entry) - gateway.entities[DOMAIN] = set() + hub = DeconzHub.get_hub(hass, config_entry) + hub.entities[DOMAIN] = set() @callback def async_add_sensor(_: EventType, sensor_id: str) -> None: """Add alarm control panel devices from deCONZ.""" - sensor = gateway.api.sensors.ancillary_control[sensor_id] - if alarm_system_id := get_alarm_system_id_for_unique_id( - gateway, sensor.unique_id - ): - async_add_entities( - [DeconzAlarmControlPanel(sensor, gateway, alarm_system_id)] - ) + sensor = hub.api.sensors.ancillary_control[sensor_id] + if alarm_system_id := get_alarm_system_id_for_unique_id(hub, sensor.unique_id): + async_add_entities([DeconzAlarmControlPanel(sensor, hub, alarm_system_id)]) - gateway.register_platform_add_device_callback( + hub.register_platform_add_device_callback( async_add_sensor, - gateway.api.sensors.ancillary_control, + hub.api.sensors.ancillary_control, ) @@ -96,11 +91,11 @@ class DeconzAlarmControlPanel(DeconzDevice[AncillaryControl], AlarmControlPanelE def __init__( self, device: AncillaryControl, - gateway: DeconzGateway, + hub: DeconzHub, alarm_system_id: str, ) -> None: """Set up alarm control panel device.""" - super().__init__(device, gateway) + super().__init__(device, hub) self.alarm_system_id = alarm_system_id @callback @@ -119,27 +114,27 @@ class DeconzAlarmControlPanel(DeconzDevice[AncillaryControl], AlarmControlPanelE async def async_alarm_arm_away(self, code: str | None = None) -> None: """Send arm away command.""" if code: - await self.gateway.api.alarm_systems.arm( + await self.hub.api.alarm_systems.arm( self.alarm_system_id, AlarmSystemArmAction.AWAY, code ) async def async_alarm_arm_home(self, code: str | None = None) -> None: """Send arm home command.""" if code: - await self.gateway.api.alarm_systems.arm( + await self.hub.api.alarm_systems.arm( self.alarm_system_id, AlarmSystemArmAction.STAY, code ) async def async_alarm_arm_night(self, code: str | None = None) -> None: """Send arm night command.""" if code: - await self.gateway.api.alarm_systems.arm( + await self.hub.api.alarm_systems.arm( self.alarm_system_id, AlarmSystemArmAction.NIGHT, code ) async def async_alarm_disarm(self, code: str | None = None) -> None: """Send disarm command.""" if code: - await self.gateway.api.alarm_systems.arm( + await self.hub.api.alarm_systems.arm( self.alarm_system_id, AlarmSystemArmAction.DISARM, code ) diff --git a/homeassistant/components/deconz/binary_sensor.py b/homeassistant/components/deconz/binary_sensor.py index c0a4e2585a3..eaa89c6eb9c 100644 --- a/homeassistant/components/deconz/binary_sensor.py +++ b/homeassistant/components/deconz/binary_sensor.py @@ -1,4 +1,5 @@ """Support for deCONZ binary sensors.""" + from __future__ import annotations from collections.abc import Callable @@ -27,12 +28,10 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, EntityCategory from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -import homeassistant.helpers.entity_registry as er -from .const import ATTR_DARK, ATTR_ON, DOMAIN as DECONZ_DOMAIN +from .const import ATTR_DARK, ATTR_ON from .deconz_device import DeconzDevice -from .gateway import DeconzGateway, get_gateway_from_config_entry -from .util import serial_from_unique_id +from .hub import DeconzHub _SensorDeviceT = TypeVar("_SensorDeviceT", bound=PydeconzSensorBase) @@ -163,42 +162,19 @@ ENTITY_DESCRIPTIONS: tuple[DeconzBinarySensorDescription, ...] = ( ) -@callback -def async_update_unique_id( - hass: HomeAssistant, unique_id: str, description: DeconzBinarySensorDescription -) -> None: - """Update unique ID to always have a suffix. - - Introduced with release 2022.7. - """ - ent_reg = er.async_get(hass) - - new_unique_id = f"{unique_id}-{description.key}" - if ent_reg.async_get_entity_id(DOMAIN, DECONZ_DOMAIN, new_unique_id): - return - - if description.old_unique_id_suffix: - unique_id = ( - f"{serial_from_unique_id(unique_id)}-{description.old_unique_id_suffix}" - ) - - if entity_id := ent_reg.async_get_entity_id(DOMAIN, DECONZ_DOMAIN, unique_id): - ent_reg.async_update_entity(entity_id, new_unique_id=new_unique_id) - - async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the deCONZ binary sensor.""" - gateway = get_gateway_from_config_entry(hass, config_entry) - gateway.entities[DOMAIN] = set() + hub = DeconzHub.get_hub(hass, config_entry) + hub.entities[DOMAIN] = set() @callback def async_add_sensor(_: EventType, sensor_id: str) -> None: """Add sensor from deCONZ.""" - sensor = gateway.api.sensors[sensor_id] + sensor = hub.api.sensors[sensor_id] for description in ENTITY_DESCRIPTIONS: if ( @@ -206,12 +182,11 @@ async def async_setup_entry( and not isinstance(sensor, description.instance_check) ) or description.value_fn(sensor) is None: continue - async_update_unique_id(hass, sensor.unique_id, description) - async_add_entities([DeconzBinarySensor(sensor, gateway, description)]) + async_add_entities([DeconzBinarySensor(sensor, hub, description)]) - gateway.register_platform_add_device_callback( + hub.register_platform_add_device_callback( async_add_sensor, - gateway.api.sensors, + hub.api.sensors, ) @@ -224,7 +199,7 @@ class DeconzBinarySensor(DeconzDevice[SensorResources], BinarySensorEntity): def __init__( self, device: SensorResources, - gateway: DeconzGateway, + hub: DeconzHub, description: DeconzBinarySensorDescription, ) -> None: """Initialize deCONZ binary sensor.""" @@ -233,7 +208,7 @@ class DeconzBinarySensor(DeconzDevice[SensorResources], BinarySensorEntity): self._update_key = description.update_key if description.name_suffix: self._name_suffix = description.name_suffix - super().__init__(device, gateway) + super().__init__(device, hub) if ( self.entity_description.key in PROVIDES_EXTRA_ATTRIBUTES diff --git a/homeassistant/components/deconz/button.py b/homeassistant/components/deconz/button.py index 52105c10203..a915ca56a33 100644 --- a/homeassistant/components/deconz/button.py +++ b/homeassistant/components/deconz/button.py @@ -20,7 +20,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from .deconz_device import DeconzDevice, DeconzSceneMixin -from .gateway import DeconzGateway, get_gateway_from_config_entry +from .hub import DeconzHub @dataclass(frozen=True, kw_only=True) @@ -50,33 +50,33 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the deCONZ button entity.""" - gateway = get_gateway_from_config_entry(hass, config_entry) - gateway.entities[DOMAIN] = set() + hub = DeconzHub.get_hub(hass, config_entry) + hub.entities[DOMAIN] = set() @callback def async_add_scene(_: EventType, scene_id: str) -> None: """Add scene button from deCONZ.""" - scene = gateway.api.scenes[scene_id] + scene = hub.api.scenes[scene_id] async_add_entities( - DeconzSceneButton(scene, gateway, description) + DeconzSceneButton(scene, hub, description) for description in ENTITY_DESCRIPTIONS.get(PydeconzScene, []) ) - gateway.register_platform_add_device_callback( + hub.register_platform_add_device_callback( async_add_scene, - gateway.api.scenes, + hub.api.scenes, ) @callback def async_add_presence_sensor(_: EventType, sensor_id: str) -> None: """Add presence sensor reset button from deCONZ.""" - sensor = gateway.api.sensors.presence[sensor_id] + sensor = hub.api.sensors.presence[sensor_id] if sensor.presence_event is not None: - async_add_entities([DeconzPresenceResetButton(sensor, gateway)]) + async_add_entities([DeconzPresenceResetButton(sensor, hub)]) - gateway.register_platform_add_device_callback( + hub.register_platform_add_device_callback( async_add_presence_sensor, - gateway.api.sensors.presence, + hub.api.sensors.presence, ) @@ -88,19 +88,19 @@ class DeconzSceneButton(DeconzSceneMixin, ButtonEntity): def __init__( self, device: PydeconzScene, - gateway: DeconzGateway, + hub: DeconzHub, description: DeconzButtonDescription, ) -> None: """Initialize deCONZ number entity.""" self.entity_description: DeconzButtonDescription = description - super().__init__(device, gateway) + super().__init__(device, hub) self._attr_name = f"{self._attr_name} {description.suffix}" async def async_press(self) -> None: """Store light states into scene.""" async_button_fn = getattr( - self.gateway.api.scenes, + self.hub.api.scenes, self.entity_description.button_fn, ) await async_button_fn(self._device.group_id, self._device.id) @@ -123,7 +123,7 @@ class DeconzPresenceResetButton(DeconzDevice[Presence], ButtonEntity): async def async_press(self) -> None: """Store reset presence state.""" - await self.gateway.api.sensors.presence.set_config( + await self.hub.api.sensors.presence.set_config( id=self._device.resource_id, reset_presence=True, ) diff --git a/homeassistant/components/deconz/climate.py b/homeassistant/components/deconz/climate.py index 35a0e810c9e..45a50d44e36 100644 --- a/homeassistant/components/deconz/climate.py +++ b/homeassistant/components/deconz/climate.py @@ -1,4 +1,5 @@ """Support for deCONZ climate devices.""" + from __future__ import annotations from typing import Any @@ -34,7 +35,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ATTR_LOCKED, ATTR_OFFSET, ATTR_VALVE from .deconz_device import DeconzDevice -from .gateway import DeconzGateway, get_gateway_from_config_entry +from .hub import DeconzHub DECONZ_FAN_SMART = "smart" @@ -79,18 +80,18 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the deCONZ climate devices.""" - gateway = get_gateway_from_config_entry(hass, config_entry) - gateway.entities[DOMAIN] = set() + hub = DeconzHub.get_hub(hass, config_entry) + hub.entities[DOMAIN] = set() @callback def async_add_climate(_: EventType, climate_id: str) -> None: """Add climate from deCONZ.""" - climate = gateway.api.sensors.thermostat[climate_id] - async_add_entities([DeconzThermostat(climate, gateway)]) + climate = hub.api.sensors.thermostat[climate_id] + async_add_entities([DeconzThermostat(climate, hub)]) - gateway.register_platform_add_device_callback( + hub.register_platform_add_device_callback( async_add_climate, - gateway.api.sensors.thermostat, + hub.api.sensors.thermostat, ) @@ -102,9 +103,9 @@ class DeconzThermostat(DeconzDevice[Thermostat], ClimateEntity): _attr_temperature_unit = UnitOfTemperature.CELSIUS _enable_turn_on_off_backwards_compatibility = False - def __init__(self, device: Thermostat, gateway: DeconzGateway) -> None: + def __init__(self, device: Thermostat, hub: DeconzHub) -> None: """Set up thermostat device.""" - super().__init__(device, gateway) + super().__init__(device, hub) self._attr_hvac_modes = [ HVACMode.HEAT, @@ -148,7 +149,7 @@ class DeconzThermostat(DeconzDevice[Thermostat], ClimateEntity): if fan_mode not in FAN_MODE_TO_DECONZ: raise ValueError(f"Unsupported fan mode {fan_mode}") - await self.gateway.api.sensors.thermostat.set_config( + await self.hub.api.sensors.thermostat.set_config( id=self._device.resource_id, fan_mode=FAN_MODE_TO_DECONZ[fan_mode], ) @@ -168,12 +169,12 @@ class DeconzThermostat(DeconzDevice[Thermostat], ClimateEntity): raise ValueError(f"Unsupported HVAC mode {hvac_mode}") if len(self._attr_hvac_modes) == 2: # Only allow turn on and off thermostat - await self.gateway.api.sensors.thermostat.set_config( + await self.hub.api.sensors.thermostat.set_config( id=self._device.resource_id, on=hvac_mode != HVACMode.OFF, ) else: - await self.gateway.api.sensors.thermostat.set_config( + await self.hub.api.sensors.thermostat.set_config( id=self._device.resource_id, mode=HVAC_MODE_TO_DECONZ[hvac_mode], ) @@ -207,7 +208,7 @@ class DeconzThermostat(DeconzDevice[Thermostat], ClimateEntity): if preset_mode not in PRESET_MODE_TO_DECONZ: raise ValueError(f"Unsupported preset mode {preset_mode}") - await self.gateway.api.sensors.thermostat.set_config( + await self.hub.api.sensors.thermostat.set_config( id=self._device.resource_id, preset=PRESET_MODE_TO_DECONZ[preset_mode], ) @@ -236,12 +237,12 @@ class DeconzThermostat(DeconzDevice[Thermostat], ClimateEntity): raise ValueError(f"Expected attribute {ATTR_TEMPERATURE}") if self._device.mode == ThermostatMode.COOL: - await self.gateway.api.sensors.thermostat.set_config( + await self.hub.api.sensors.thermostat.set_config( id=self._device.resource_id, cooling_setpoint=kwargs[ATTR_TEMPERATURE] * 100, ) else: - await self.gateway.api.sensors.thermostat.set_config( + await self.hub.api.sensors.thermostat.set_config( id=self._device.resource_id, heating_setpoint=kwargs[ATTR_TEMPERATURE] * 100, ) diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index 99fa6412364..d017e2c5c65 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -19,13 +19,17 @@ from pydeconz.utils import ( ) import voluptuous as vol -from homeassistant import config_entries from homeassistant.components import ssdp from homeassistant.components.hassio import HassioServiceInfo -from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow +from homeassistant.config_entries import ( + SOURCE_HASSIO, + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT from homeassistant.core import HomeAssistant, callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import aiohttp_client from .const import ( @@ -40,7 +44,7 @@ from .const import ( HASSIO_CONFIGURATION_URL, LOGGER, ) -from .gateway import DeconzGateway +from .hub import DeconzHub DECONZ_MANUFACTURERURL = "http://www.dresden-elektronik.de" CONF_SERIAL = "serial" @@ -48,11 +52,11 @@ CONF_MANUAL_INPUT = "Manually define gateway" @callback -def get_master_gateway(hass: HomeAssistant) -> DeconzGateway: +def get_master_hub(hass: HomeAssistant) -> DeconzHub: """Return the gateway which is marked as master.""" - for gateway in hass.data[DOMAIN].values(): - if gateway.master: - return cast(DeconzGateway, gateway) + for hub in hass.data[DOMAIN].values(): + if hub.master: + return cast(DeconzHub, hub) raise ValueError @@ -80,7 +84,7 @@ class DeconzFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a deCONZ config flow start. Let user choose between discovered bridges and manual configuration. @@ -110,10 +114,7 @@ class DeconzFlowHandler(ConfigFlow, domain=DOMAIN): LOGGER.debug("Discovered deCONZ gateways %s", pformat(self.bridges)) if self.bridges: - hosts = [] - - for bridge in self.bridges: - hosts.append(bridge[CONF_HOST]) + hosts = [bridge[CONF_HOST] for bridge in self.bridges] hosts.append(CONF_MANUAL_INPUT) @@ -126,7 +127,7 @@ class DeconzFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_manual_input( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Manual configuration.""" if user_input: self.host = user_input[CONF_HOST] @@ -145,7 +146,7 @@ class DeconzFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_link( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Attempt to link with the deCONZ bridge.""" errors: dict[str, str] = {} @@ -173,7 +174,7 @@ class DeconzFlowHandler(ConfigFlow, domain=DOMAIN): return self.async_show_form(step_id="link", errors=errors) - async def _create_entry(self) -> FlowResult: + async def _create_entry(self) -> ConfigFlowResult: """Create entry for gateway.""" if not self.bridge_id: session = aiohttp_client.async_get_clientsession(self.hass) @@ -205,7 +206,9 @@ class DeconzFlowHandler(ConfigFlow, domain=DOMAIN): }, ) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Trigger a reauthentication flow.""" self.context["title_placeholders"] = {CONF_HOST: entry_data[CONF_HOST]} @@ -214,7 +217,9 @@ class DeconzFlowHandler(ConfigFlow, domain=DOMAIN): return await self.async_step_link() - async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: + async def async_step_ssdp( + self, discovery_info: ssdp.SsdpServiceInfo + ) -> ConfigFlowResult: """Handle a discovered deCONZ bridge.""" if LOGGER.isEnabledFor(logging.DEBUG): LOGGER.debug("deCONZ SSDP discovery %s", pformat(discovery_info)) @@ -223,7 +228,7 @@ class DeconzFlowHandler(ConfigFlow, domain=DOMAIN): parsed_url = urlparse(discovery_info.ssdp_location) entry = await self.async_set_unique_id(self.bridge_id) - if entry and entry.source == config_entries.SOURCE_HASSIO: + if entry and entry.source == SOURCE_HASSIO: return self.async_abort(reason="already_configured") self.host = cast(str, parsed_url.hostname) @@ -245,7 +250,9 @@ class DeconzFlowHandler(ConfigFlow, domain=DOMAIN): return await self.async_step_link() - async def async_step_hassio(self, discovery_info: HassioServiceInfo) -> FlowResult: + async def async_step_hassio( + self, discovery_info: HassioServiceInfo + ) -> ConfigFlowResult: """Prepare configuration for a Hass.io deCONZ bridge. This flow is triggered by the discovery component. @@ -275,7 +282,7 @@ class DeconzFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_hassio_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm a Hass.io discovery.""" if user_input is not None: @@ -290,7 +297,7 @@ class DeconzFlowHandler(ConfigFlow, domain=DOMAIN): class DeconzOptionsFlowHandler(OptionsFlow): """Handle deCONZ options.""" - gateway: DeconzGateway + gateway: DeconzHub def __init__(self, config_entry: ConfigEntry) -> None: """Initialize deCONZ options flow.""" @@ -299,13 +306,13 @@ class DeconzOptionsFlowHandler(OptionsFlow): async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Manage the deCONZ options.""" return await self.async_step_deconz_devices() async def async_step_deconz_devices( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Manage the deconz devices options.""" if user_input is not None: self.options.update(user_input) diff --git a/homeassistant/components/deconz/const.py b/homeassistant/components/deconz/const.py index ca38edf0625..873f5cde284 100644 --- a/homeassistant/components/deconz/const.py +++ b/homeassistant/components/deconz/const.py @@ -1,4 +1,5 @@ """Constants for the deCONZ component.""" + import logging from pydeconz.models import ResourceType diff --git a/homeassistant/components/deconz/cover.py b/homeassistant/components/deconz/cover.py index 012f064dd07..b83c62c3367 100644 --- a/homeassistant/components/deconz/cover.py +++ b/homeassistant/components/deconz/cover.py @@ -1,4 +1,5 @@ """Support for deCONZ covers.""" + from __future__ import annotations from typing import Any, cast @@ -21,7 +22,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from .deconz_device import DeconzDevice -from .gateway import DeconzGateway, get_gateway_from_config_entry +from .hub import DeconzHub DECONZ_TYPE_TO_DEVICE_CLASS = { ResourceType.LEVEL_CONTROLLABLE_OUTPUT.value: CoverDeviceClass.DAMPER, @@ -36,17 +37,17 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up covers for deCONZ component.""" - gateway = get_gateway_from_config_entry(hass, config_entry) - gateway.entities[DOMAIN] = set() + hub = DeconzHub.get_hub(hass, config_entry) + hub.entities[DOMAIN] = set() @callback def async_add_cover(_: EventType, cover_id: str) -> None: """Add cover from deCONZ.""" - async_add_entities([DeconzCover(cover_id, gateway)]) + async_add_entities([DeconzCover(cover_id, hub)]) - gateway.register_platform_add_device_callback( + hub.register_platform_add_device_callback( async_add_cover, - gateway.api.lights.covers, + hub.api.lights.covers, ) @@ -55,9 +56,9 @@ class DeconzCover(DeconzDevice[Cover], CoverEntity): TYPE = DOMAIN - def __init__(self, cover_id: str, gateway: DeconzGateway) -> None: + def __init__(self, cover_id: str, hub: DeconzHub) -> None: """Set up cover device.""" - super().__init__(cover := gateway.api.lights.covers[cover_id], gateway) + super().__init__(cover := hub.api.lights.covers[cover_id], hub) self._attr_supported_features = ( CoverEntityFeature.OPEN @@ -91,7 +92,7 @@ class DeconzCover(DeconzDevice[Cover], 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.gateway.api.lights.covers.set_state( + await self.hub.api.lights.covers.set_state( id=self._device.resource_id, lift=position, legacy_mode=self.legacy_mode, @@ -99,7 +100,7 @@ class DeconzCover(DeconzDevice[Cover], CoverEntity): async def async_open_cover(self, **kwargs: Any) -> None: """Open cover.""" - await self.gateway.api.lights.covers.set_state( + await self.hub.api.lights.covers.set_state( id=self._device.resource_id, action=CoverAction.OPEN, legacy_mode=self.legacy_mode, @@ -107,7 +108,7 @@ class DeconzCover(DeconzDevice[Cover], CoverEntity): async def async_close_cover(self, **kwargs: Any) -> None: """Close cover.""" - await self.gateway.api.lights.covers.set_state( + await self.hub.api.lights.covers.set_state( id=self._device.resource_id, action=CoverAction.CLOSE, legacy_mode=self.legacy_mode, @@ -115,7 +116,7 @@ class DeconzCover(DeconzDevice[Cover], CoverEntity): async def async_stop_cover(self, **kwargs: Any) -> None: """Stop cover.""" - await self.gateway.api.lights.covers.set_state( + await self.hub.api.lights.covers.set_state( id=self._device.resource_id, action=CoverAction.STOP, legacy_mode=self.legacy_mode, @@ -131,7 +132,7 @@ class DeconzCover(DeconzDevice[Cover], 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.gateway.api.lights.covers.set_state( + await self.hub.api.lights.covers.set_state( id=self._device.resource_id, tilt=position, legacy_mode=self.legacy_mode, @@ -139,7 +140,7 @@ class DeconzCover(DeconzDevice[Cover], CoverEntity): async def async_open_cover_tilt(self, **kwargs: Any) -> None: """Open cover tilt.""" - await self.gateway.api.lights.covers.set_state( + await self.hub.api.lights.covers.set_state( id=self._device.resource_id, tilt=0, legacy_mode=self.legacy_mode, @@ -147,7 +148,7 @@ class DeconzCover(DeconzDevice[Cover], CoverEntity): async def async_close_cover_tilt(self, **kwargs: Any) -> None: """Close cover tilt.""" - await self.gateway.api.lights.covers.set_state( + await self.hub.api.lights.covers.set_state( id=self._device.resource_id, tilt=100, legacy_mode=self.legacy_mode, @@ -155,7 +156,7 @@ class DeconzCover(DeconzDevice[Cover], CoverEntity): async def async_stop_cover_tilt(self, **kwargs: Any) -> None: """Stop cover tilt.""" - await self.gateway.api.lights.covers.set_state( + await self.hub.api.lights.covers.set_state( id=self._device.resource_id, action=CoverAction.STOP, legacy_mode=self.legacy_mode, diff --git a/homeassistant/components/deconz/deconz_device.py b/homeassistant/components/deconz/deconz_device.py index 8a5ced2c678..0ddabbcfccc 100644 --- a/homeassistant/components/deconz/deconz_device.py +++ b/homeassistant/components/deconz/deconz_device.py @@ -16,7 +16,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity from .const import DOMAIN as DECONZ_DOMAIN -from .gateway import DeconzGateway +from .hub import DeconzHub from .util import serial_from_unique_id _DeviceT = TypeVar( @@ -33,11 +33,11 @@ class DeconzBase(Generic[_DeviceT]): def __init__( self, device: _DeviceT, - gateway: DeconzGateway, + hub: DeconzHub, ) -> None: """Set up device and add update callback to get data from websocket.""" self._device: _DeviceT = device - self.gateway = gateway + self.hub = hub @property def unique_id(self) -> str: @@ -67,7 +67,7 @@ class DeconzBase(Generic[_DeviceT]): model=self._device.model_id, name=self._device.name, sw_version=self._device.software_version, - via_device=(DECONZ_DOMAIN, self.gateway.api.config.bridge_id), + via_device=(DECONZ_DOMAIN, self.hub.api.config.bridge_id), ) @@ -85,11 +85,11 @@ class DeconzDevice(DeconzBase[_DeviceT], Entity): def __init__( self, device: _DeviceT, - gateway: DeconzGateway, + hub: DeconzHub, ) -> None: """Set up device and add update callback to get data from websocket.""" - super().__init__(device, gateway) - self.gateway.entities[self.TYPE].add(self.unique_id) + super().__init__(device, hub) + self.hub.entities[self.TYPE].add(self.unique_id) self._attr_name = self._device.name if self._name_suffix is not None: @@ -103,11 +103,11 @@ class DeconzDevice(DeconzBase[_DeviceT], Entity): async def async_added_to_hass(self) -> None: """Subscribe to device events.""" self._device.register_callback(self.async_update_callback) - self.gateway.deconz_ids[self.entity_id] = self._device.deconz_id + self.hub.deconz_ids[self.entity_id] = self._device.deconz_id self.async_on_remove( async_dispatcher_connect( self.hass, - self.gateway.signal_reachable, + self.hub.signal_reachable, self.async_update_connection_state, ) ) @@ -115,8 +115,8 @@ class DeconzDevice(DeconzBase[_DeviceT], Entity): async def async_will_remove_from_hass(self) -> None: """Disconnect device object when removed.""" self._device.remove_callback(self.async_update_callback) - del self.gateway.deconz_ids[self.entity_id] - self.gateway.entities[self.TYPE].remove(self.unique_id) + del self.hub.deconz_ids[self.entity_id] + self.hub.entities[self.TYPE].remove(self.unique_id) @callback def async_update_connection_state(self) -> None: @@ -126,7 +126,7 @@ class DeconzDevice(DeconzBase[_DeviceT], Entity): @callback def async_update_callback(self) -> None: """Update the device's state.""" - if self.gateway.ignore_state_updates: + if self.hub.ignore_state_updates: return if self._update_keys is not None and not self._device.changed_keys.intersection( @@ -140,8 +140,8 @@ class DeconzDevice(DeconzBase[_DeviceT], Entity): def available(self) -> bool: """Return True if device is available.""" if isinstance(self._device, PydeconzScene): - return self.gateway.available - return self.gateway.available and self._device.reachable # type: ignore[union-attr] + return self.hub.available + return self.hub.available and self._device.reachable # type: ignore[union-attr] class DeconzSceneMixin(DeconzDevice[PydeconzScene]): @@ -152,23 +152,23 @@ class DeconzSceneMixin(DeconzDevice[PydeconzScene]): def __init__( self, device: PydeconzScene, - gateway: DeconzGateway, + hub: DeconzHub, ) -> None: """Set up a scene.""" - super().__init__(device, gateway) + super().__init__(device, hub) - self.group = self.gateway.api.groups[device.group_id] + self.group = self.hub.api.groups[device.group_id] self._attr_name = device.name self._group_identifier = self.get_parent_identifier() def get_device_identifier(self) -> str: """Describe a unique identifier for this scene.""" - return f"{self.gateway.bridgeid}{self._device.deconz_id}" + return f"{self.hub.bridgeid}{self._device.deconz_id}" def get_parent_identifier(self) -> str: """Describe a unique identifier for group this scene belongs to.""" - return f"{self.gateway.bridgeid}-{self.group.deconz_id}" + return f"{self.hub.bridgeid}-{self.group.deconz_id}" @property def unique_id(self) -> str: @@ -183,5 +183,5 @@ class DeconzSceneMixin(DeconzDevice[PydeconzScene]): manufacturer="Dresden Elektronik", model="deCONZ group", name=self.group.name, - via_device=(DECONZ_DOMAIN, self.gateway.api.config.bridge_id), + via_device=(DECONZ_DOMAIN, self.hub.api.config.bridge_id), ) diff --git a/homeassistant/components/deconz/deconz_event.py b/homeassistant/components/deconz/deconz_event.py index 9bde87e5e17..56cbf47b4e3 100644 --- a/homeassistant/components/deconz/deconz_event.py +++ b/homeassistant/components/deconz/deconz_event.py @@ -26,7 +26,7 @@ from homeassistant.util import slugify from .const import ATTR_DURATION, ATTR_ROTATION, CONF_ANGLE, CONF_GESTURE, LOGGER from .deconz_device import DeconzBase -from .gateway import DeconzGateway +from .hub import DeconzHub CONF_DECONZ_EVENT = "deconz_event" CONF_DECONZ_ALARM_EVENT = "deconz_alarm_event" @@ -55,57 +55,62 @@ RELATIVE_ROTARY_DECONZ_TO_EVENT = { } -async def async_setup_events(gateway: DeconzGateway) -> None: +async def async_setup_events(hub: DeconzHub) -> None: """Set up the deCONZ events.""" @callback def async_add_sensor(_: EventType, sensor_id: str) -> None: """Create DeconzEvent.""" - new_event: DeconzAlarmEvent | DeconzEvent | DeconzPresenceEvent | DeconzRelativeRotaryEvent - sensor = gateway.api.sensors[sensor_id] + new_event: ( + DeconzAlarmEvent + | DeconzEvent + | DeconzPresenceEvent + | DeconzRelativeRotaryEvent + ) + sensor = hub.api.sensors[sensor_id] if isinstance(sensor, Switch): - new_event = DeconzEvent(sensor, gateway) + new_event = DeconzEvent(sensor, hub) elif isinstance(sensor, AncillaryControl): - new_event = DeconzAlarmEvent(sensor, gateway) + new_event = DeconzAlarmEvent(sensor, hub) elif isinstance(sensor, Presence): if sensor.presence_event is None: return - new_event = DeconzPresenceEvent(sensor, gateway) + new_event = DeconzPresenceEvent(sensor, hub) elif isinstance(sensor, RelativeRotary): - new_event = DeconzRelativeRotaryEvent(sensor, gateway) + new_event = DeconzRelativeRotaryEvent(sensor, hub) - gateway.hass.async_create_task(new_event.async_update_device_registry()) - gateway.events.append(new_event) + hub.hass.async_create_task(new_event.async_update_device_registry()) + hub.events.append(new_event) - gateway.register_platform_add_device_callback( + hub.register_platform_add_device_callback( async_add_sensor, - gateway.api.sensors.switch, + hub.api.sensors.switch, ) - gateway.register_platform_add_device_callback( + hub.register_platform_add_device_callback( async_add_sensor, - gateway.api.sensors.ancillary_control, + hub.api.sensors.ancillary_control, ) - gateway.register_platform_add_device_callback( + hub.register_platform_add_device_callback( async_add_sensor, - gateway.api.sensors.presence, + hub.api.sensors.presence, ) - gateway.register_platform_add_device_callback( + hub.register_platform_add_device_callback( async_add_sensor, - gateway.api.sensors.relative_rotary, + hub.api.sensors.relative_rotary, ) @callback -def async_unload_events(gateway: DeconzGateway) -> None: +def async_unload_events(hub: DeconzHub) -> None: """Unload all deCONZ events.""" - for event in gateway.events: + for event in hub.events: event.async_will_remove_from_hass() - gateway.events.clear() + hub.events.clear() class DeconzEventBase(DeconzBase): @@ -118,10 +123,10 @@ class DeconzEventBase(DeconzBase): def __init__( self, device: AncillaryControl | Presence | RelativeRotary | Switch, - gateway: DeconzGateway, + hub: DeconzHub, ) -> None: """Register callback that will be used for signals.""" - super().__init__(device, gateway) + super().__init__(device, hub) self._unsubscribe = device.subscribe(self.async_update_callback) @@ -145,10 +150,10 @@ class DeconzEventBase(DeconzBase): if not self.device_info: return - device_registry = dr.async_get(self.gateway.hass) + device_registry = dr.async_get(self.hub.hass) entry = device_registry.async_get_or_create( - config_entry_id=self.gateway.config_entry.entry_id, **self.device_info + config_entry_id=self.hub.config_entry.entry_id, **self.device_info ) self.device_id = entry.id @@ -165,10 +170,7 @@ class DeconzEvent(DeconzEventBase): @callback def async_update_callback(self) -> None: """Fire the event if reason is that state is updated.""" - if ( - self.gateway.ignore_state_updates - or "state" not in self._device.changed_keys - ): + if self.hub.ignore_state_updates or "state" not in self._device.changed_keys: return data: dict[str, Any] = { @@ -189,7 +191,7 @@ class DeconzEvent(DeconzEventBase): if self._device.xy is not None: data[CONF_XY] = self._device.xy - self.gateway.hass.bus.async_fire(CONF_DECONZ_EVENT, data) + self.hub.hass.bus.async_fire(CONF_DECONZ_EVENT, data) class DeconzAlarmEvent(DeconzEventBase): @@ -201,7 +203,7 @@ class DeconzAlarmEvent(DeconzEventBase): def async_update_callback(self) -> None: """Fire the event if reason is new action is updated.""" if ( - self.gateway.ignore_state_updates + self.hub.ignore_state_updates or "action" not in self._device.changed_keys or self._device.action not in SUPPORTED_DECONZ_ALARM_EVENTS ): @@ -214,7 +216,7 @@ class DeconzAlarmEvent(DeconzEventBase): CONF_EVENT: self._device.action.value, } - self.gateway.hass.bus.async_fire(CONF_DECONZ_ALARM_EVENT, data) + self.hub.hass.bus.async_fire(CONF_DECONZ_ALARM_EVENT, data) class DeconzPresenceEvent(DeconzEventBase): @@ -226,7 +228,7 @@ class DeconzPresenceEvent(DeconzEventBase): def async_update_callback(self) -> None: """Fire the event if reason is new action is updated.""" if ( - self.gateway.ignore_state_updates + self.hub.ignore_state_updates or "presenceevent" not in self._device.changed_keys or self._device.presence_event not in SUPPORTED_DECONZ_PRESENCE_EVENTS ): @@ -239,7 +241,7 @@ class DeconzPresenceEvent(DeconzEventBase): CONF_EVENT: self._device.presence_event.value, } - self.gateway.hass.bus.async_fire(CONF_DECONZ_PRESENCE_EVENT, data) + self.hub.hass.bus.async_fire(CONF_DECONZ_PRESENCE_EVENT, data) class DeconzRelativeRotaryEvent(DeconzEventBase): @@ -251,7 +253,7 @@ class DeconzRelativeRotaryEvent(DeconzEventBase): def async_update_callback(self) -> None: """Fire the event if reason is new action is updated.""" if ( - self.gateway.ignore_state_updates + self.hub.ignore_state_updates or "rotaryevent" not in self._device.changed_keys ): return @@ -265,4 +267,4 @@ class DeconzRelativeRotaryEvent(DeconzEventBase): ATTR_DURATION: self._device.expected_event_duration, } - self.gateway.hass.bus.async_fire(CONF_DECONZ_RELATIVE_ROTARY_EVENT, data) + self.hub.hass.bus.async_fire(CONF_DECONZ_RELATIVE_ROTARY_EVENT, data) diff --git a/homeassistant/components/deconz/device_trigger.py b/homeassistant/components/deconz/device_trigger.py index 70d03f808c1..5e16d85ec4d 100644 --- a/homeassistant/components/deconz/device_trigger.py +++ b/homeassistant/components/deconz/device_trigger.py @@ -1,4 +1,5 @@ """Provides device automations for deconz events.""" + from __future__ import annotations import voluptuous as vol @@ -30,7 +31,7 @@ from .deconz_event import ( DeconzPresenceEvent, DeconzRelativeRotaryEvent, ) -from .gateway import DeconzGateway +from .hub import DeconzHub CONF_SUBTYPE = "subtype" @@ -655,9 +656,9 @@ def _get_deconz_event_from_device( device: dr.DeviceEntry, ) -> DeconzAlarmEvent | DeconzEvent | DeconzPresenceEvent | DeconzRelativeRotaryEvent: """Resolve deconz event from device.""" - gateways: dict[str, DeconzGateway] = hass.data.get(DOMAIN, {}) - for gateway in gateways.values(): - for deconz_event in gateway.events: + hubs: dict[str, DeconzHub] = hass.data.get(DOMAIN, {}) + for hub in hubs.values(): + for deconz_event in hub.events: if device.id == deconz_event.device_id: return deconz_event diff --git a/homeassistant/components/deconz/diagnostics.py b/homeassistant/components/deconz/diagnostics.py index 5b7986fc4c9..fcd5dec120f 100644 --- a/homeassistant/components/deconz/diagnostics.py +++ b/homeassistant/components/deconz/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for deCONZ.""" + from __future__ import annotations from typing import Any @@ -8,7 +9,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY, CONF_UNIQUE_ID from homeassistant.core import HomeAssistant -from .gateway import get_gateway_from_config_entry +from .hub import DeconzHub REDACT_CONFIG = {CONF_API_KEY, CONF_UNIQUE_ID} REDACT_DECONZ_CONFIG = {"bridgeid", "mac", "panid"} @@ -18,29 +19,27 @@ async def async_get_config_entry_diagnostics( hass: HomeAssistant, config_entry: ConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - gateway = get_gateway_from_config_entry(hass, config_entry) + hub = DeconzHub.get_hub(hass, config_entry) diag: dict[str, Any] = {} diag["config"] = async_redact_data(config_entry.as_dict(), REDACT_CONFIG) - diag["deconz_config"] = async_redact_data( - gateway.api.config.raw, REDACT_DECONZ_CONFIG - ) + diag["deconz_config"] = async_redact_data(hub.api.config.raw, REDACT_DECONZ_CONFIG) diag["websocket_state"] = ( - gateway.api.websocket.state.value if gateway.api.websocket else "Unknown" + hub.api.websocket.state.value if hub.api.websocket else "Unknown" ) - diag["deconz_ids"] = gateway.deconz_ids - diag["entities"] = gateway.entities + diag["deconz_ids"] = hub.deconz_ids + diag["entities"] = hub.entities diag["events"] = { event.serial: { "event_id": event.event_id, "event_type": type(event).__name__, } - for event in gateway.events + for event in hub.events } - diag["alarm_systems"] = {k: v.raw for k, v in gateway.api.alarm_systems.items()} - diag["groups"] = {k: v.raw for k, v in gateway.api.groups.items()} - diag["lights"] = {k: v.raw for k, v in gateway.api.lights.items()} - diag["scenes"] = {k: v.raw for k, v in gateway.api.scenes.items()} - diag["sensors"] = {k: v.raw for k, v in gateway.api.sensors.items()} + diag["alarm_systems"] = {k: v.raw for k, v in hub.api.alarm_systems.items()} + diag["groups"] = {k: v.raw for k, v in hub.api.groups.items()} + diag["lights"] = {k: v.raw for k, v in hub.api.lights.items()} + diag["scenes"] = {k: v.raw for k, v in hub.api.scenes.items()} + diag["sensors"] = {k: v.raw for k, v in hub.api.sensors.items()} return diag diff --git a/homeassistant/components/deconz/errors.py b/homeassistant/components/deconz/errors.py index be13e579ce0..55558123034 100644 --- a/homeassistant/components/deconz/errors.py +++ b/homeassistant/components/deconz/errors.py @@ -1,4 +1,5 @@ """Errors for the deCONZ component.""" + from homeassistant.exceptions import HomeAssistantError diff --git a/homeassistant/components/deconz/fan.py b/homeassistant/components/deconz/fan.py index 278d702d63b..ee5456aab4e 100644 --- a/homeassistant/components/deconz/fan.py +++ b/homeassistant/components/deconz/fan.py @@ -1,4 +1,5 @@ """Support for deCONZ fans.""" + from __future__ import annotations from typing import Any @@ -16,7 +17,7 @@ from homeassistant.util.percentage import ( ) from .deconz_device import DeconzDevice -from .gateway import DeconzGateway, get_gateway_from_config_entry +from .hub import DeconzHub ORDERED_NAMED_FAN_SPEEDS: list[LightFanSpeed] = [ LightFanSpeed.PERCENT_25, @@ -32,20 +33,20 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up fans for deCONZ component.""" - gateway = get_gateway_from_config_entry(hass, config_entry) - gateway.entities[DOMAIN] = set() + hub = DeconzHub.get_hub(hass, config_entry) + hub.entities[DOMAIN] = set() @callback def async_add_fan(_: EventType, fan_id: str) -> None: """Add fan from deCONZ.""" - fan = gateway.api.lights.lights[fan_id] + fan = hub.api.lights.lights[fan_id] if not fan.supports_fan_speed: return - async_add_entities([DeconzFan(fan, gateway)]) + async_add_entities([DeconzFan(fan, hub)]) - gateway.register_platform_add_device_callback( + hub.register_platform_add_device_callback( async_add_fan, - gateway.api.lights.lights, + hub.api.lights.lights, ) @@ -57,9 +58,9 @@ class DeconzFan(DeconzDevice[Light], FanEntity): _attr_supported_features = FanEntityFeature.SET_SPEED - def __init__(self, device: Light, gateway: DeconzGateway) -> None: + def __init__(self, device: Light, hub: DeconzHub) -> None: """Set up fan.""" - super().__init__(device, gateway) + super().__init__(device, hub) _attr_speed_count = len(ORDERED_NAMED_FAN_SPEEDS) if device.fan_speed in ORDERED_NAMED_FAN_SPEEDS: self._default_on_speed = device.fan_speed @@ -91,7 +92,7 @@ class DeconzFan(DeconzDevice[Light], FanEntity): """Set the speed percentage of the fan.""" if percentage == 0: return await self.async_turn_off() - await self.gateway.api.lights.lights.set_state( + await self.hub.api.lights.lights.set_state( id=self._device.resource_id, fan_speed=percentage_to_ordered_list_item( ORDERED_NAMED_FAN_SPEEDS, percentage @@ -108,14 +109,14 @@ class DeconzFan(DeconzDevice[Light], FanEntity): if percentage is not None: await self.async_set_percentage(percentage) return - await self.gateway.api.lights.lights.set_state( + await self.hub.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.gateway.api.lights.lights.set_state( + await self.hub.api.lights.lights.set_state( id=self._device.resource_id, fan_speed=LightFanSpeed.OFF, ) diff --git a/homeassistant/components/deconz/hub/__init__.py b/homeassistant/components/deconz/hub/__init__.py new file mode 100644 index 00000000000..e484bd5bb59 --- /dev/null +++ b/homeassistant/components/deconz/hub/__init__.py @@ -0,0 +1,4 @@ +"""Internal functionality not part of HA infrastructure.""" + +from .api import get_deconz_api # noqa: F401 +from .hub import DeconzHub # noqa: F401 diff --git a/homeassistant/components/deconz/hub/api.py b/homeassistant/components/deconz/hub/api.py new file mode 100644 index 00000000000..71551ead6e1 --- /dev/null +++ b/homeassistant/components/deconz/hub/api.py @@ -0,0 +1,37 @@ +"""deCONZ API representation.""" + +from __future__ import annotations + +import asyncio + +from pydeconz import DeconzSession, errors + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers import aiohttp_client + +from ..const import LOGGER +from ..errors import AuthenticationRequired, CannotConnect +from .config import DeconzConfig + + +async def get_deconz_api( + hass: HomeAssistant, config_entry: ConfigEntry +) -> DeconzSession: + """Create a gateway object and verify configuration.""" + session = aiohttp_client.async_get_clientsession(hass) + + config = DeconzConfig.from_config_entry(config_entry) + api = DeconzSession(session, config.host, config.port, config.api_key) + try: + async with asyncio.timeout(10): + await api.refresh_state() + return api + + except errors.Unauthorized as err: + LOGGER.warning("Invalid key for deCONZ at %s", config.host) + raise AuthenticationRequired from err + + except (TimeoutError, errors.RequestError, errors.ResponseError) as err: + LOGGER.error("Error connecting to deCONZ gateway at %s", config.host) + raise CannotConnect from err diff --git a/homeassistant/components/deconz/hub/config.py b/homeassistant/components/deconz/hub/config.py new file mode 100644 index 00000000000..06d2dc10542 --- /dev/null +++ b/homeassistant/components/deconz/hub/config.py @@ -0,0 +1,57 @@ +"""deCONZ config entry abstraction.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Self + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT + +from ..const import ( + CONF_ALLOW_CLIP_SENSOR, + CONF_ALLOW_DECONZ_GROUPS, + CONF_ALLOW_NEW_DEVICES, + DEFAULT_ALLOW_CLIP_SENSOR, + DEFAULT_ALLOW_DECONZ_GROUPS, + DEFAULT_ALLOW_NEW_DEVICES, +) + + +@dataclass +class DeconzConfig: + """Represent a deCONZ config entry.""" + + entry: ConfigEntry + + host: str + port: int + api_key: str + + allow_clip_sensor: bool + allow_deconz_groups: bool + allow_new_devices: bool + + @classmethod + def from_config_entry(cls, config_entry: ConfigEntry) -> Self: + """Create object from config entry.""" + config = config_entry.data + options = config_entry.options + return cls( + entry=config_entry, + host=config[CONF_HOST], + port=config[CONF_PORT], + api_key=config[CONF_API_KEY], + allow_clip_sensor=options.get( + CONF_ALLOW_CLIP_SENSOR, + DEFAULT_ALLOW_CLIP_SENSOR, + ), + allow_deconz_groups=options.get( + CONF_ALLOW_DECONZ_GROUPS, + DEFAULT_ALLOW_DECONZ_GROUPS, + ), + allow_new_devices=options.get( + CONF_ALLOW_NEW_DEVICES, + DEFAULT_ALLOW_NEW_DEVICES, + ), + ) diff --git a/homeassistant/components/deconz/gateway.py b/homeassistant/components/deconz/hub/hub.py similarity index 69% rename from homeassistant/components/deconz/gateway.py rename to homeassistant/components/deconz/hub/hub.py index a9286cca112..ff958bbda50 100644 --- a/homeassistant/components/deconz/gateway.py +++ b/homeassistant/components/deconz/hub/hub.py @@ -2,45 +2,31 @@ from __future__ import annotations -import asyncio from collections.abc import Callable -from types import MappingProxyType -from typing import TYPE_CHECKING, Any, cast +from typing import TYPE_CHECKING, cast -from pydeconz import DeconzSession, errors +from pydeconz import DeconzSession 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 -from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT from homeassistant.core import Event, HomeAssistant, callback -from homeassistant.helpers import ( - aiohttp_client, - device_registry as dr, - entity_registry as er, -) +from homeassistant.helpers import 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 .const import ( - CONF_ALLOW_CLIP_SENSOR, - CONF_ALLOW_DECONZ_GROUPS, - CONF_ALLOW_NEW_DEVICES, +from ..const import ( CONF_MASTER_GATEWAY, - DEFAULT_ALLOW_CLIP_SENSOR, - DEFAULT_ALLOW_DECONZ_GROUPS, - DEFAULT_ALLOW_NEW_DEVICES, DOMAIN as DECONZ_DOMAIN, HASSIO_CONFIGURATION_URL, - LOGGER, PLATFORMS, ) -from .errors import AuthenticationRequired, CannotConnect +from .config import DeconzConfig if TYPE_CHECKING: - from .deconz_event import ( + from ..deconz_event import ( DeconzAlarmEvent, DeconzEvent, DeconzPresenceEvent, @@ -77,7 +63,7 @@ SENSORS = ( ) -class DeconzGateway: +class DeconzHub: """Manages a single deCONZ gateway.""" def __init__( @@ -85,6 +71,7 @@ class DeconzGateway: ) -> None: """Initialize the system.""" self.hass = hass + self.config = DeconzConfig.from_config_entry(config_entry) self.config_entry = config_entry self.api = api @@ -107,26 +94,17 @@ class DeconzGateway: 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 - ) - self.option_allow_new_devices = config_entry.options.get( - CONF_ALLOW_NEW_DEVICES, DEFAULT_ALLOW_NEW_DEVICES - ) + @callback + @staticmethod + def get_hub(hass: HomeAssistant, config_entry: ConfigEntry) -> DeconzHub: + """Return hub with a matching config entry ID.""" + return cast(DeconzHub, hass.data[DECONZ_DOMAIN][config_entry.entry_id]) @property def bridgeid(self) -> str: """Return the unique identifier of the gateway.""" return cast(str, self.config_entry.unique_id) - @property - def host(self) -> str: - """Return the host of the gateway.""" - return cast(str, self.config_entry.data[CONF_HOST]) - @property def master(self) -> bool: """Gateway which is used with deCONZ services without defining id.""" @@ -151,7 +129,7 @@ class DeconzGateway: """ if ( not initializing - and not self.option_allow_new_devices + and not self.config.allow_new_devices and not self.ignore_state_updates ): self.ignored_devices.add((async_add_device, device_id)) @@ -159,14 +137,14 @@ class DeconzGateway: if isinstance(deconz_device_interface, GroupHandler): self.deconz_groups.add((async_add_device, device_id)) - if not self.option_allow_deconz_groups: + if not self.config.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: + if not self.config.allow_clip_sensor: return add_device_callback(EventType.ADDED, device_id) @@ -213,7 +191,7 @@ class DeconzGateway: ) # Gateway service - configuration_url = f"http://{self.host}:{self.config_entry.data[CONF_PORT]}" + configuration_url = f"http://{self.config.host}:{self.config.port}" if self.config_entry.source == SOURCE_HASSIO: configuration_url = HASSIO_CONFIGURATION_URL device_registry.async_get_or_create( @@ -230,7 +208,7 @@ class DeconzGateway: @staticmethod async def async_config_entry_updated( - hass: HomeAssistant, entry: ConfigEntry + hass: HomeAssistant, config_entry: ConfigEntry ) -> None: """Handle signals of config entry being updated. @@ -239,32 +217,29 @@ class DeconzGateway: Causes for this is either discovery updating host address or config entry options changing. """ - if entry.entry_id not in hass.data[DECONZ_DOMAIN]: + if config_entry.entry_id not in hass.data[DECONZ_DOMAIN]: # A race condition can occur if multiple config entries are # unloaded in parallel return - gateway = get_gateway_from_config_entry(hass, entry) - - if gateway.api.host != gateway.host: - gateway.api.close() - gateway.api.host = gateway.host - gateway.api.start() + hub = DeconzHub.get_hub(hass, config_entry) + previous_config = hub.config + hub.config = DeconzConfig.from_config_entry(config_entry) + if previous_config.host != hub.config.host: + hub.api.close() + hub.api.host = hub.config.host + hub.api.start() return - await gateway.options_updated() + await hub.options_updated(previous_config) - async def options_updated(self) -> None: + async def options_updated(self, previous_config: DeconzConfig) -> None: """Manage entities affected by config entry options.""" deconz_ids = [] # Allow CLIP sensors - 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: + if self.config.allow_clip_sensor != previous_config.allow_clip_sensor: + if self.config.allow_clip_sensor: for add_device, device_id in self.clip_sensors: add_device(EventType.ADDED, device_id) else: @@ -276,12 +251,8 @@ class DeconzGateway: # Allow Groups - option_allow_deconz_groups = self.config_entry.options.get( - CONF_ALLOW_DECONZ_GROUPS, DEFAULT_ALLOW_DECONZ_GROUPS - ) - if option_allow_deconz_groups != self.option_allow_deconz_groups: - self.option_allow_deconz_groups = option_allow_deconz_groups - if option_allow_deconz_groups: + if self.config.allow_deconz_groups != previous_config.allow_deconz_groups: + if self.config.allow_deconz_groups: for add_device, device_id in self.deconz_groups: add_device(EventType.ADDED, device_id) else: @@ -289,12 +260,8 @@ class DeconzGateway: # Allow adding new devices - option_allow_new_devices = self.config_entry.options.get( - CONF_ALLOW_NEW_DEVICES, DEFAULT_ALLOW_NEW_DEVICES - ) - if option_allow_new_devices != self.option_allow_new_devices: - self.option_allow_new_devices = option_allow_new_devices - if option_allow_new_devices: + if self.config.allow_new_devices != previous_config.allow_new_devices: + if self.config.allow_new_devices: self.load_ignored_devices() # Remove entities based on above categories @@ -331,38 +298,3 @@ class DeconzGateway: self.deconz_ids = {} return True - - -@callback -def get_gateway_from_config_entry( - hass: HomeAssistant, config_entry: ConfigEntry -) -> DeconzGateway: - """Return gateway with a matching config entry ID.""" - return cast(DeconzGateway, hass.data[DECONZ_DOMAIN][config_entry.entry_id]) - - -async def get_deconz_session( - hass: HomeAssistant, - config: MappingProxyType[str, Any], -) -> DeconzSession: - """Create a gateway object and verify configuration.""" - session = aiohttp_client.async_get_clientsession(hass) - - deconz_session = DeconzSession( - session, - config[CONF_HOST], - config[CONF_PORT], - config[CONF_API_KEY], - ) - try: - async with asyncio.timeout(10): - await deconz_session.refresh_state() - return deconz_session - - except errors.Unauthorized as err: - LOGGER.warning("Invalid key for deCONZ at %s", config[CONF_HOST]) - raise AuthenticationRequired from err - - except (TimeoutError, errors.RequestError, errors.ResponseError) as err: - LOGGER.error("Error connecting to deCONZ gateway at %s", config[CONF_HOST]) - raise CannotConnect from err diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py index d618edc93f8..fc5388d2b33 100644 --- a/homeassistant/components/deconz/light.py +++ b/homeassistant/components/deconz/light.py @@ -1,4 +1,5 @@ """Support for deCONZ lights.""" + from __future__ import annotations from typing import Any, TypedDict, TypeVar @@ -35,7 +36,7 @@ from homeassistant.util.color import color_hs_to_xy from .const import DOMAIN as DECONZ_DOMAIN, POWER_PLUGS from .deconz_device import DeconzDevice -from .gateway import DeconzGateway, get_gateway_from_config_entry +from .hub import DeconzHub DECONZ_GROUP = "is_deconz_group" EFFECT_TO_DECONZ = { @@ -110,13 +111,13 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the deCONZ lights and groups from a config entry.""" - gateway = get_gateway_from_config_entry(hass, config_entry) - gateway.entities[DOMAIN] = set() + hub = DeconzHub.get_hub(hass, config_entry) + hub.entities[DOMAIN] = set() entity_registry = er.async_get(hass) # On/Off Output should be switch not light 2022.5 - for light in gateway.api.lights.lights.values(): + for light in hub.api.lights.lights.values(): if light.type == ResourceType.ON_OFF_OUTPUT.value and ( entity_id := entity_registry.async_get_entity_id( DOMAIN, DECONZ_DOMAIN, light.unique_id @@ -127,15 +128,15 @@ async def async_setup_entry( @callback def async_add_light(_: EventType, light_id: str) -> None: """Add light from deCONZ.""" - light = gateway.api.lights.lights[light_id] + light = hub.api.lights.lights[light_id] if light.type in POWER_PLUGS: return - async_add_entities([DeconzLight(light, gateway)]) + async_add_entities([DeconzLight(light, hub)]) - gateway.register_platform_add_device_callback( + hub.register_platform_add_device_callback( async_add_light, - gateway.api.lights.lights, + hub.api.lights.lights, ) @callback @@ -144,20 +145,20 @@ async def async_setup_entry( Update group states based on its sum of related lights. """ - if (group := gateway.api.groups[group_id]) and not group.lights: + if (group := hub.api.groups[group_id]) and not group.lights: return first = True for light_id in group.lights: - if (light := gateway.api.lights.lights.get(light_id)) and light.reachable: + if (light := hub.api.lights.lights.get(light_id)) and light.reachable: group.update_color_state(light, update_all_attributes=first) first = False - async_add_entities([DeconzGroup(group, gateway)]) + async_add_entities([DeconzGroup(group, hub)]) - gateway.register_platform_add_device_callback( + hub.register_platform_add_device_callback( async_add_group, - gateway.api.groups, + hub.api.groups, ) @@ -167,15 +168,15 @@ class DeconzBaseLight(DeconzDevice[_LightDeviceT], LightEntity): TYPE = DOMAIN _attr_color_mode = ColorMode.UNKNOWN - def __init__(self, device: _LightDeviceT, gateway: DeconzGateway) -> None: + def __init__(self, device: _LightDeviceT, hub: DeconzHub) -> None: """Set up light.""" - super().__init__(device, gateway) + super().__init__(device, hub) self.api: GroupHandler | LightHandler if isinstance(self._device, Light): - self.api = self.gateway.api.lights.lights + self.api = self.hub.api.lights.lights elif isinstance(self._device, Group): - self.api = self.gateway.api.groups + self.api = self.hub.api.groups self._attr_supported_color_modes: set[ColorMode] = set() @@ -323,7 +324,7 @@ class DeconzLight(DeconzBaseLight[Light]): super().async_update_callback() if self._device.reachable and "attr" not in self._device.changed_keys: - for group in self.gateway.api.groups.values(): + for group in self.hub.api.groups.values(): if self._device.resource_id in group.lights: group.update_color_state(self._device) @@ -333,10 +334,10 @@ class DeconzGroup(DeconzBaseLight[Group]): _attr_has_entity_name = True - def __init__(self, device: Group, gateway: DeconzGateway) -> None: + def __init__(self, device: Group, hub: DeconzHub) -> None: """Set up group and create an unique id.""" - self._unique_id = f"{gateway.bridgeid}-{device.deconz_id}" - super().__init__(device, gateway) + self._unique_id = f"{hub.bridgeid}-{device.deconz_id}" + super().__init__(device, hub) self._attr_name = None @@ -353,7 +354,7 @@ class DeconzGroup(DeconzBaseLight[Group]): manufacturer="Dresden Elektronik", model="deCONZ group", name=self._device.name, - via_device=(DECONZ_DOMAIN, self.gateway.api.config.bridge_id), + via_device=(DECONZ_DOMAIN, self.hub.api.config.bridge_id), ) @property diff --git a/homeassistant/components/deconz/lock.py b/homeassistant/components/deconz/lock.py index 7afde4ada11..8729d7de793 100644 --- a/homeassistant/components/deconz/lock.py +++ b/homeassistant/components/deconz/lock.py @@ -14,7 +14,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from .deconz_device import DeconzDevice -from .gateway import get_gateway_from_config_entry +from .hub import DeconzHub async def async_setup_entry( @@ -23,29 +23,29 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up locks for deCONZ component.""" - gateway = get_gateway_from_config_entry(hass, config_entry) - gateway.entities[DOMAIN] = set() + hub = DeconzHub.get_hub(hass, config_entry) + hub.entities[DOMAIN] = set() @callback def async_add_lock_from_light(_: EventType, lock_id: str) -> None: """Add lock from deCONZ.""" - lock = gateway.api.lights.locks[lock_id] - async_add_entities([DeconzLock(lock, gateway)]) + lock = hub.api.lights.locks[lock_id] + async_add_entities([DeconzLock(lock, hub)]) - gateway.register_platform_add_device_callback( + hub.register_platform_add_device_callback( async_add_lock_from_light, - gateway.api.lights.locks, + hub.api.lights.locks, ) @callback def async_add_lock_from_sensor(_: EventType, lock_id: str) -> None: """Add lock from deCONZ.""" - lock = gateway.api.sensors.door_lock[lock_id] - async_add_entities([DeconzLock(lock, gateway)]) + lock = hub.api.sensors.door_lock[lock_id] + async_add_entities([DeconzLock(lock, hub)]) - gateway.register_platform_add_device_callback( + hub.register_platform_add_device_callback( async_add_lock_from_sensor, - gateway.api.sensors.door_lock, + hub.api.sensors.door_lock, always_ignore_clip_sensors=True, ) @@ -63,12 +63,12 @@ class DeconzLock(DeconzDevice[DoorLock | Lock], LockEntity): async def async_lock(self, **kwargs: Any) -> None: """Lock the lock.""" if isinstance(self._device, DoorLock): - await self.gateway.api.sensors.door_lock.set_config( + await self.hub.api.sensors.door_lock.set_config( id=self._device.resource_id, lock=True, ) else: - await self.gateway.api.lights.locks.set_state( + await self.hub.api.lights.locks.set_state( id=self._device.resource_id, lock=True, ) @@ -76,12 +76,12 @@ class DeconzLock(DeconzDevice[DoorLock | Lock], LockEntity): async def async_unlock(self, **kwargs: Any) -> None: """Unlock the lock.""" if isinstance(self._device, DoorLock): - await self.gateway.api.sensors.door_lock.set_config( + await self.hub.api.sensors.door_lock.set_config( id=self._device.resource_id, lock=False, ) else: - await self.gateway.api.lights.locks.set_state( + await self.hub.api.lights.locks.set_state( id=self._device.resource_id, lock=False, ) diff --git a/homeassistant/components/deconz/logbook.py b/homeassistant/components/deconz/logbook.py index 39fe7e98a56..3ef14eca657 100644 --- a/homeassistant/components/deconz/logbook.py +++ b/homeassistant/components/deconz/logbook.py @@ -1,4 +1,5 @@ """Describe deCONZ logbook events.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/deconz/number.py b/homeassistant/components/deconz/number.py index e98f5d726ac..03c25668820 100644 --- a/homeassistant/components/deconz/number.py +++ b/homeassistant/components/deconz/number.py @@ -21,12 +21,9 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -import homeassistant.helpers.entity_registry as er -from .const import DOMAIN as DECONZ_DOMAIN from .deconz_device import DeconzDevice -from .gateway import DeconzGateway, get_gateway_from_config_entry -from .util import serial_from_unique_id +from .hub import DeconzHub T = TypeVar("T", Presence, PydeconzSensorBase) @@ -70,38 +67,19 @@ ENTITY_DESCRIPTIONS: tuple[DeconzNumberDescription, ...] = ( ) -@callback -def async_update_unique_id( - hass: HomeAssistant, unique_id: str, description: DeconzNumberDescription -) -> None: - """Update unique ID base to be on full unique ID rather than device serial. - - Introduced with release 2022.11. - """ - ent_reg = er.async_get(hass) - - new_unique_id = f"{unique_id}-{description.key}" - if ent_reg.async_get_entity_id(DOMAIN, DECONZ_DOMAIN, new_unique_id): - return - - unique_id = f"{serial_from_unique_id(unique_id)}-{description.key}" - if entity_id := ent_reg.async_get_entity_id(DOMAIN, DECONZ_DOMAIN, unique_id): - ent_reg.async_update_entity(entity_id, new_unique_id=new_unique_id) - - async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the deCONZ number entity.""" - gateway = get_gateway_from_config_entry(hass, config_entry) - gateway.entities[DOMAIN] = set() + hub = DeconzHub.get_hub(hass, config_entry) + hub.entities[DOMAIN] = set() @callback def async_add_sensor(_: EventType, sensor_id: str) -> None: """Add sensor from deCONZ.""" - sensor = gateway.api.sensors.presence[sensor_id] + sensor = hub.api.sensors.presence[sensor_id] for description in ENTITY_DESCRIPTIONS: if ( @@ -109,13 +87,11 @@ async def async_setup_entry( or description.value_fn(sensor) is None ): continue - if description.key == "delay": - async_update_unique_id(hass, sensor.unique_id, description) - async_add_entities([DeconzNumber(sensor, gateway, description)]) + async_add_entities([DeconzNumber(sensor, hub, description)]) - gateway.register_platform_add_device_callback( + hub.register_platform_add_device_callback( async_add_sensor, - gateway.api.sensors.presence, + hub.api.sensors.presence, always_ignore_clip_sensors=True, ) @@ -129,7 +105,7 @@ class DeconzNumber(DeconzDevice[SensorResources], NumberEntity): def __init__( self, device: SensorResources, - gateway: DeconzGateway, + hub: DeconzHub, description: DeconzNumberDescription, ) -> None: """Initialize deCONZ number entity.""" @@ -137,7 +113,7 @@ class DeconzNumber(DeconzDevice[SensorResources], NumberEntity): self.unique_id_suffix = description.key self._name_suffix = description.name_suffix self._update_key = description.update_key - super().__init__(device, gateway) + super().__init__(device, hub) @property def native_value(self) -> float | None: @@ -147,7 +123,7 @@ class DeconzNumber(DeconzDevice[SensorResources], NumberEntity): async def async_set_native_value(self, value: float) -> None: """Set sensor config.""" await self.entity_description.set_fn( - self.gateway.api, + self.hub.api, self._device.resource_id, int(value), ) diff --git a/homeassistant/components/deconz/scene.py b/homeassistant/components/deconz/scene.py index 236389cc100..f121c3107b0 100644 --- a/homeassistant/components/deconz/scene.py +++ b/homeassistant/components/deconz/scene.py @@ -12,7 +12,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from .deconz_device import DeconzSceneMixin -from .gateway import get_gateway_from_config_entry +from .hub import DeconzHub async def async_setup_entry( @@ -21,18 +21,18 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up scenes for deCONZ integration.""" - gateway = get_gateway_from_config_entry(hass, config_entry) - gateway.entities[DOMAIN] = set() + hub = DeconzHub.get_hub(hass, config_entry) + hub.entities[DOMAIN] = set() @callback def async_add_scene(_: EventType, scene_id: str) -> None: """Add scene from deCONZ.""" - scene = gateway.api.scenes[scene_id] - async_add_entities([DeconzScene(scene, gateway)]) + scene = hub.api.scenes[scene_id] + async_add_entities([DeconzScene(scene, hub)]) - gateway.register_platform_add_device_callback( + hub.register_platform_add_device_callback( async_add_scene, - gateway.api.scenes, + hub.api.scenes, ) @@ -43,7 +43,7 @@ class DeconzScene(DeconzSceneMixin, Scene): async def async_activate(self, **kwargs: Any) -> None: """Activate the scene.""" - await self.gateway.api.scenes.recall( + await self.hub.api.scenes.recall( self._device.group_id, self._device.id, ) diff --git a/homeassistant/components/deconz/select.py b/homeassistant/components/deconz/select.py index d38fee2fdc7..dad3ba9d78d 100644 --- a/homeassistant/components/deconz/select.py +++ b/homeassistant/components/deconz/select.py @@ -17,7 +17,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from .deconz_device import DeconzDevice -from .gateway import get_gateway_from_config_entry +from .hub import DeconzHub SENSITIVITY_TO_DECONZ = { "High": PresenceConfigSensitivity.HIGH.value, @@ -33,25 +33,25 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the deCONZ button entity.""" - gateway = get_gateway_from_config_entry(hass, config_entry) - gateway.entities[DOMAIN] = set() + hub = DeconzHub.get_hub(hass, config_entry) + hub.entities[DOMAIN] = set() @callback def async_add_presence_sensor(_: EventType, sensor_id: str) -> None: """Add presence select entity from deCONZ.""" - sensor = gateway.api.sensors.presence[sensor_id] + sensor = hub.api.sensors.presence[sensor_id] if sensor.presence_event is not None: async_add_entities( [ - DeconzPresenceDeviceModeSelect(sensor, gateway), - DeconzPresenceSensitivitySelect(sensor, gateway), - DeconzPresenceTriggerDistanceSelect(sensor, gateway), + DeconzPresenceDeviceModeSelect(sensor, hub), + DeconzPresenceSensitivitySelect(sensor, hub), + DeconzPresenceTriggerDistanceSelect(sensor, hub), ] ) - gateway.register_platform_add_device_callback( + hub.register_platform_add_device_callback( async_add_presence_sensor, - gateway.api.sensors.presence, + hub.api.sensors.presence, ) @@ -79,7 +79,7 @@ class DeconzPresenceDeviceModeSelect(DeconzDevice[Presence], SelectEntity): async def async_select_option(self, option: str) -> None: """Change the selected option.""" - await self.gateway.api.sensors.presence.set_config( + await self.hub.api.sensors.presence.set_config( id=self._device.resource_id, device_mode=PresenceConfigDeviceMode(option), ) @@ -106,7 +106,7 @@ class DeconzPresenceSensitivitySelect(DeconzDevice[Presence], SelectEntity): async def async_select_option(self, option: str) -> None: """Change the selected option.""" - await self.gateway.api.sensors.presence.set_config( + await self.hub.api.sensors.presence.set_config( id=self._device.resource_id, sensitivity=SENSITIVITY_TO_DECONZ[option], ) @@ -137,7 +137,7 @@ class DeconzPresenceTriggerDistanceSelect(DeconzDevice[Presence], SelectEntity): async def async_select_option(self, option: str) -> None: """Change the selected option.""" - await self.gateway.api.sensors.presence.set_config( + await self.hub.api.sensors.presence.set_config( id=self._device.resource_id, trigger_distance=PresenceConfigTriggerDistance(option), ) diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index 8366c811318..750019dc680 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -48,14 +48,12 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -import homeassistant.helpers.entity_registry as er from homeassistant.helpers.typing import StateType import homeassistant.util.dt as dt_util -from .const import ATTR_DARK, ATTR_ON, DOMAIN as DECONZ_DOMAIN +from .const import ATTR_DARK, ATTR_ON from .deconz_device import DeconzDevice -from .gateway import DeconzGateway, get_gateway_from_config_entry -from .util import serial_from_unique_id +from .hub import DeconzHub PROVIDES_EXTRA_ATTRIBUTES = ( "battery", @@ -291,37 +289,14 @@ ENTITY_DESCRIPTIONS: tuple[DeconzSensorDescription, ...] = ( ) -@callback -def async_update_unique_id( - hass: HomeAssistant, unique_id: str, description: DeconzSensorDescription -) -> None: - """Update unique ID to always have a suffix. - - Introduced with release 2022.9. - """ - ent_reg = er.async_get(hass) - - new_unique_id = f"{unique_id}-{description.key}" - if ent_reg.async_get_entity_id(DOMAIN, DECONZ_DOMAIN, new_unique_id): - return - - if description.old_unique_id_suffix: - unique_id = ( - f"{serial_from_unique_id(unique_id)}-{description.old_unique_id_suffix}" - ) - - if entity_id := ent_reg.async_get_entity_id(DOMAIN, DECONZ_DOMAIN, unique_id): - ent_reg.async_update_entity(entity_id, new_unique_id=new_unique_id) - - async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the deCONZ sensors.""" - gateway = get_gateway_from_config_entry(hass, config_entry) - gateway.entities[DOMAIN] = set() + hub = DeconzHub.get_hub(hass, config_entry) + hub.entities[DOMAIN] = set() known_device_entities: dict[str, set[str]] = { description.key: set() @@ -332,7 +307,7 @@ async def async_setup_entry( @callback def async_add_sensor(_: EventType, sensor_id: str) -> None: """Add sensor from deCONZ.""" - sensor = gateway.api.sensors[sensor_id] + sensor = hub.api.sensors[sensor_id] entities: list[DeconzSensor] = [] for description in ENTITY_DESCRIPTIONS: @@ -357,23 +332,21 @@ async def async_setup_entry( continue known_device_entities[description.key].add(unique_id) if no_sensor_data and description.key == "battery": - async_update_unique_id(hass, sensor.unique_id, description) DeconzBatteryTracker( - sensor_id, gateway, description, async_add_entities + sensor_id, hub, description, async_add_entities ) continue if no_sensor_data: continue - async_update_unique_id(hass, sensor.unique_id, description) - entities.append(DeconzSensor(sensor, gateway, description)) + entities.append(DeconzSensor(sensor, hub, description)) async_add_entities(entities) - gateway.register_platform_add_device_callback( + hub.register_platform_add_device_callback( async_add_sensor, - gateway.api.sensors, + hub.api.sensors, ) @@ -386,7 +359,7 @@ class DeconzSensor(DeconzDevice[SensorResources], SensorEntity): def __init__( self, device: SensorResources, - gateway: DeconzGateway, + hub: DeconzHub, description: DeconzSensorDescription, ) -> None: """Initialize deCONZ sensor.""" @@ -395,7 +368,7 @@ class DeconzSensor(DeconzDevice[SensorResources], SensorEntity): self._update_key = description.update_key if description.name_suffix: self._name_suffix = description.name_suffix - super().__init__(device, gateway) + super().__init__(device, hub) if ( self.entity_description.key in PROVIDES_EXTRA_ATTRIBUTES @@ -440,7 +413,7 @@ class DeconzSensor(DeconzDevice[SensorResources], SensorEntity): attr[ATTR_VOLTAGE] = self._device.voltage elif isinstance(self._device, Switch): - for event in self.gateway.events: + for event in self.hub.events: if self._device == event.device: attr[ATTR_EVENT_ID] = event.event_id @@ -453,13 +426,13 @@ class DeconzBatteryTracker: def __init__( self, sensor_id: str, - gateway: DeconzGateway, + hub: DeconzHub, description: DeconzSensorDescription, async_add_entities: AddEntitiesCallback, ) -> None: """Set up tracker.""" - self.sensor = gateway.api.sensors[sensor_id] - self.gateway = gateway + self.sensor = hub.api.sensors[sensor_id] + self.hub = hub self.description = description self.async_add_entities = async_add_entities self.unsubscribe = self.sensor.subscribe(self.async_update_callback) @@ -470,5 +443,5 @@ class DeconzBatteryTracker: if self.description.update_key in self.sensor.changed_keys: self.unsubscribe() self.async_add_entities( - [DeconzSensor(self.sensor, self.gateway, self.description)] + [DeconzSensor(self.sensor, self.hub, self.description)] ) diff --git a/homeassistant/components/deconz/services.py b/homeassistant/components/deconz/services.py index bcac6ac1e1d..91f36bb871e 100644 --- a/homeassistant/components/deconz/services.py +++ b/homeassistant/components/deconz/services.py @@ -16,9 +16,9 @@ from homeassistant.helpers.entity_registry import ( ) from homeassistant.util.read_only_dict import ReadOnlyDict -from .config_flow import get_master_gateway +from .config_flow import get_master_hub from .const import CONF_BRIDGE_ID, DOMAIN, LOGGER -from .gateway import DeconzGateway +from .hub import DeconzHub DECONZ_SERVICES = "deconz_services" @@ -66,33 +66,33 @@ def async_setup_services(hass: HomeAssistant) -> None: service_data = service_call.data if CONF_BRIDGE_ID in service_data: - found_gateway = False + found_hub = False bridge_id = normalize_bridge_id(service_data[CONF_BRIDGE_ID]) - for possible_gateway in hass.data[DOMAIN].values(): - if possible_gateway.bridgeid == bridge_id: - gateway = possible_gateway - found_gateway = True + for possible_hub in hass.data[DOMAIN].values(): + if possible_hub.bridgeid == bridge_id: + hub = possible_hub + found_hub = True break - if not found_gateway: + if not found_hub: LOGGER.error("Could not find the gateway %s", bridge_id) return else: try: - gateway = get_master_gateway(hass) + hub = get_master_hub(hass) except ValueError: LOGGER.error("No master gateway available") return if service == SERVICE_CONFIGURE_DEVICE: - await async_configure_service(gateway, service_data) + await async_configure_service(hub, service_data) elif service == SERVICE_DEVICE_REFRESH: - await async_refresh_devices_service(gateway) + await async_refresh_devices_service(hub) elif service == SERVICE_REMOVE_ORPHANED_ENTRIES: - await async_remove_orphaned_entries_service(gateway) + await async_remove_orphaned_entries_service(hub) for service in SUPPORTED_SERVICES: hass.services.async_register( @@ -110,7 +110,7 @@ def async_unload_services(hass: HomeAssistant) -> None: hass.services.async_remove(DOMAIN, service) -async def async_configure_service(gateway: DeconzGateway, data: ReadOnlyDict) -> None: +async def async_configure_service(hub: DeconzHub, data: ReadOnlyDict) -> None: """Set attribute of device in deCONZ. Entity is used to resolve to a device path (e.g. '/lights/1'). @@ -132,61 +132,61 @@ async def async_configure_service(gateway: DeconzGateway, data: ReadOnlyDict) -> if entity_id: try: - field = gateway.deconz_ids[entity_id] + field + field = hub.deconz_ids[entity_id] + field except KeyError: LOGGER.error("Could not find the entity %s", entity_id) return - await gateway.api.request("put", field, json=data) + await hub.api.request("put", field, json=data) -async def async_refresh_devices_service(gateway: DeconzGateway) -> None: +async def async_refresh_devices_service(hub: DeconzHub) -> None: """Refresh available devices from deCONZ.""" - gateway.ignore_state_updates = True - await gateway.api.refresh_state() - gateway.load_ignored_devices() - gateway.ignore_state_updates = False + hub.ignore_state_updates = True + await hub.api.refresh_state() + hub.load_ignored_devices() + hub.ignore_state_updates = False -async def async_remove_orphaned_entries_service(gateway: DeconzGateway) -> None: +async def async_remove_orphaned_entries_service(hub: DeconzHub) -> None: """Remove orphaned deCONZ entries from device and entity registries.""" - device_registry = dr.async_get(gateway.hass) - entity_registry = er.async_get(gateway.hass) + device_registry = dr.async_get(hub.hass) + entity_registry = er.async_get(hub.hass) entity_entries = async_entries_for_config_entry( - entity_registry, gateway.config_entry.entry_id + entity_registry, hub.config_entry.entry_id ) entities_to_be_removed = [] devices_to_be_removed = [ entry.id for entry in device_registry.devices.values() - if gateway.config_entry.entry_id in entry.config_entries + if hub.config_entry.entry_id in entry.config_entries ] # Don't remove the Gateway host entry - if gateway.api.config.mac: - gateway_host = device_registry.async_get_device( - connections={(CONNECTION_NETWORK_MAC, gateway.api.config.mac)}, + if hub.api.config.mac: + hub_host = device_registry.async_get_device( + connections={(CONNECTION_NETWORK_MAC, hub.api.config.mac)}, ) - if gateway_host and gateway_host.id in devices_to_be_removed: - devices_to_be_removed.remove(gateway_host.id) + if hub_host and hub_host.id in devices_to_be_removed: + devices_to_be_removed.remove(hub_host.id) # Don't remove the Gateway service entry - gateway_service = device_registry.async_get_device( - identifiers={(DOMAIN, gateway.api.config.bridge_id)} + hub_service = device_registry.async_get_device( + identifiers={(DOMAIN, hub.api.config.bridge_id)} ) - if gateway_service and gateway_service.id in devices_to_be_removed: - devices_to_be_removed.remove(gateway_service.id) + if hub_service and hub_service.id in devices_to_be_removed: + devices_to_be_removed.remove(hub_service.id) # Don't remove devices belonging to available events - for event in gateway.events: + for event in hub.events: if event.device_id in devices_to_be_removed: devices_to_be_removed.remove(event.device_id) for entry in entity_entries: # Don't remove available entities - if entry.unique_id in gateway.entities[entry.domain]: + if entry.unique_id in hub.entities[entry.domain]: # Don't remove devices with available entities if entry.device_id in devices_to_be_removed: devices_to_be_removed.remove(entry.device_id) diff --git a/homeassistant/components/deconz/siren.py b/homeassistant/components/deconz/siren.py index 45c81c9e31c..deb1c98f151 100644 --- a/homeassistant/components/deconz/siren.py +++ b/homeassistant/components/deconz/siren.py @@ -1,4 +1,5 @@ """Support for deCONZ siren.""" + from __future__ import annotations from typing import Any @@ -17,7 +18,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from .deconz_device import DeconzDevice -from .gateway import get_gateway_from_config_entry +from .hub import DeconzHub async def async_setup_entry( @@ -26,18 +27,18 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up sirens for deCONZ component.""" - gateway = get_gateway_from_config_entry(hass, config_entry) - gateway.entities[DOMAIN] = set() + hub = DeconzHub.get_hub(hass, config_entry) + hub.entities[DOMAIN] = set() @callback def async_add_siren(_: EventType, siren_id: str) -> None: """Add siren from deCONZ.""" - siren = gateway.api.lights.sirens[siren_id] - async_add_entities([DeconzSiren(siren, gateway)]) + siren = hub.api.lights.sirens[siren_id] + async_add_entities([DeconzSiren(siren, hub)]) - gateway.register_platform_add_device_callback( + hub.register_platform_add_device_callback( async_add_siren, - gateway.api.lights.sirens, + hub.api.lights.sirens, ) @@ -60,7 +61,7 @@ class DeconzSiren(DeconzDevice[Siren], SirenEntity): """Turn on siren.""" if (duration := kwargs.get(ATTR_DURATION)) is not None: duration *= 10 - await self.gateway.api.lights.sirens.set_state( + await self.hub.api.lights.sirens.set_state( id=self._device.resource_id, on=True, duration=duration, @@ -68,7 +69,7 @@ class DeconzSiren(DeconzDevice[Siren], SirenEntity): async def async_turn_off(self, **kwargs: Any) -> None: """Turn off siren.""" - await self.gateway.api.lights.sirens.set_state( + await self.hub.api.lights.sirens.set_state( id=self._device.resource_id, on=False, ) diff --git a/homeassistant/components/deconz/switch.py b/homeassistant/components/deconz/switch.py index 990de24dffc..e176d9c7710 100644 --- a/homeassistant/components/deconz/switch.py +++ b/homeassistant/components/deconz/switch.py @@ -14,7 +14,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import POWER_PLUGS from .deconz_device import DeconzDevice -from .gateway import get_gateway_from_config_entry +from .hub import DeconzHub async def async_setup_entry( @@ -26,20 +26,20 @@ async def async_setup_entry( Switches are based on the same device class as lights in deCONZ. """ - gateway = get_gateway_from_config_entry(hass, config_entry) - gateway.entities[DOMAIN] = set() + hub = DeconzHub.get_hub(hass, config_entry) + hub.entities[DOMAIN] = set() @callback def async_add_switch(_: EventType, switch_id: str) -> None: """Add switch from deCONZ.""" - switch = gateway.api.lights.lights[switch_id] + switch = hub.api.lights.lights[switch_id] if switch.type not in POWER_PLUGS: return - async_add_entities([DeconzPowerPlug(switch, gateway)]) + async_add_entities([DeconzPowerPlug(switch, hub)]) - gateway.register_platform_add_device_callback( + hub.register_platform_add_device_callback( async_add_switch, - gateway.api.lights.lights, + hub.api.lights.lights, ) @@ -55,14 +55,14 @@ class DeconzPowerPlug(DeconzDevice[Light], SwitchEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Turn on switch.""" - await self.gateway.api.lights.lights.set_state( + await self.hub.api.lights.lights.set_state( id=self._device.resource_id, on=True, ) async def async_turn_off(self, **kwargs: Any) -> None: """Turn off switch.""" - await self.gateway.api.lights.lights.set_state( + await self.hub.api.lights.lights.set_state( id=self._device.resource_id, on=False, ) diff --git a/homeassistant/components/deconz/util.py b/homeassistant/components/deconz/util.py index 4e7b1e7739f..7c44280200d 100644 --- a/homeassistant/components/deconz/util.py +++ b/homeassistant/components/deconz/util.py @@ -1,4 +1,5 @@ """Utilities for deCONZ integration.""" + from __future__ import annotations diff --git a/homeassistant/components/decora/light.py b/homeassistant/components/decora/light.py index 4a56b72ec66..237577872c9 100644 --- a/homeassistant/components/decora/light.py +++ b/homeassistant/components/decora/light.py @@ -1,4 +1,5 @@ """Support for Decora dimmers.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/decora_wifi/light.py b/homeassistant/components/decora_wifi/light.py index a9d43736743..798243b5d4b 100644 --- a/homeassistant/components/decora_wifi/light.py +++ b/homeassistant/components/decora_wifi/light.py @@ -1,4 +1,5 @@ """Interfaces with the myLeviton API for Decora Smart WiFi products.""" + from __future__ import annotations import logging @@ -62,17 +63,18 @@ def setup_platform( # Gather all the available devices... perms = session.user.get_residential_permissions() - all_switches = [] + all_switches: list = [] for permission in perms: if permission.residentialAccountId is not None: acct = ResidentialAccount(session, permission.residentialAccountId) - for residence in acct.get_residences(): - for switch in residence.get_iot_switches(): - all_switches.append(switch) + all_switches.extend( + switch + for residence in acct.get_residences() + for switch in residence.get_iot_switches() + ) elif permission.residenceId is not None: residence = Residence(session, permission.residenceId) - for switch in residence.get_iot_switches(): - all_switches.append(switch) + all_switches.extend(residence.get_iot_switches()) add_entities(DecoraWifiLight(sw) for sw in all_switches) except ValueError: diff --git a/homeassistant/components/default_config/__init__.py b/homeassistant/components/default_config/__init__.py index 2221bbbef61..e7302528b2e 100644 --- a/homeassistant/components/default_config/__init__.py +++ b/homeassistant/components/default_config/__init__.py @@ -1,4 +1,5 @@ """Component providing default configuration for new users.""" + from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv from homeassistant.helpers.typing import ConfigType diff --git a/homeassistant/components/delijn/sensor.py b/homeassistant/components/delijn/sensor.py index 2cf85b91abd..5693a00e857 100644 --- a/homeassistant/components/delijn/sensor.py +++ b/homeassistant/components/delijn/sensor.py @@ -1,4 +1,5 @@ """Support for De Lijn (Flemish public transport) information.""" + from __future__ import annotations from datetime import datetime @@ -63,9 +64,8 @@ async def async_setup_platform( session = async_get_clientsession(hass) - sensors = [] - for nextpassage in config[CONF_NEXT_DEPARTURE]: - sensors.append( + async_add_entities( + ( DeLijnPublicTransportSensor( Passages( nextpassage[CONF_STOP_ID], @@ -75,9 +75,10 @@ async def async_setup_platform( True, ) ) - ) - - async_add_entities(sensors, True) + for nextpassage in config[CONF_NEXT_DEPARTURE] + ), + True, + ) class DeLijnPublicTransportSensor(SensorEntity): diff --git a/homeassistant/components/deluge/__init__.py b/homeassistant/components/deluge/__init__.py index 40f4d772670..6a313db2669 100644 --- a/homeassistant/components/deluge/__init__.py +++ b/homeassistant/components/deluge/__init__.py @@ -1,4 +1,5 @@ """The Deluge integration.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/deluge/config_flow.py b/homeassistant/components/deluge/config_flow.py index db2598e1f67..8ebf56ceb5b 100644 --- a/homeassistant/components/deluge/config_flow.py +++ b/homeassistant/components/deluge/config_flow.py @@ -1,4 +1,5 @@ """Config flow for the Deluge integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -8,7 +9,7 @@ from typing import Any from deluge_client.client import DelugeRPCClient import voluptuous as vol -from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlow +from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlow, ConfigFlowResult from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, @@ -16,7 +17,6 @@ from homeassistant.const import ( CONF_SOURCE, CONF_USERNAME, ) -from homeassistant.data_entry_flow import FlowResult import homeassistant.helpers.config_validation as cv from .const import ( @@ -33,7 +33,7 @@ class DelugeFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initiated by the user.""" errors = {} @@ -75,7 +75,9 @@ class DelugeFlowHandler(ConfigFlow, domain=DOMAIN): ) return self.async_show_form(step_id="user", data_schema=schema, errors=errors) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Handle a reauthorization flow request.""" return await self.async_step_user() diff --git a/homeassistant/components/deluge/const.py b/homeassistant/components/deluge/const.py index 704c024c41b..91e08da3470 100644 --- a/homeassistant/components/deluge/const.py +++ b/homeassistant/components/deluge/const.py @@ -1,4 +1,5 @@ """Constants for the Deluge integration.""" + import logging from typing import Final diff --git a/homeassistant/components/deluge/coordinator.py b/homeassistant/components/deluge/coordinator.py index 7a3e840ff95..6b3c177b90d 100644 --- a/homeassistant/components/deluge/coordinator.py +++ b/homeassistant/components/deluge/coordinator.py @@ -1,4 +1,5 @@ """Data update coordinator for the Deluge integration.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/deluge/sensor.py b/homeassistant/components/deluge/sensor.py index eeb947663bf..1b96c60ec45 100644 --- a/homeassistant/components/deluge/sensor.py +++ b/homeassistant/components/deluge/sensor.py @@ -1,4 +1,5 @@ """Support for monitoring the Deluge BitTorrent client API.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/deluge/switch.py b/homeassistant/components/deluge/switch.py index 483b02844d6..866f7b4f25b 100644 --- a/homeassistant/components/deluge/switch.py +++ b/homeassistant/components/deluge/switch.py @@ -1,4 +1,5 @@ """Support for setting the Deluge BitTorrent client in Pause.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/demo/__init__.py b/homeassistant/components/demo/__init__.py index 98226d68030..6fa7e0d973b 100644 --- a/homeassistant/components/demo/__init__.py +++ b/homeassistant/components/demo/__init__.py @@ -1,4 +1,5 @@ """Set up the demo environment that mimics interaction with devices.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/demo/air_quality.py b/homeassistant/components/demo/air_quality.py index d1a56112497..551f2c8e88a 100644 --- a/homeassistant/components/demo/air_quality.py +++ b/homeassistant/components/demo/air_quality.py @@ -1,4 +1,5 @@ """Demo platform that offers fake air quality data.""" + from __future__ import annotations from homeassistant.components.air_quality import AirQualityEntity diff --git a/homeassistant/components/demo/alarm_control_panel.py b/homeassistant/components/demo/alarm_control_panel.py index 1c15e9d5b7e..0b152f87c29 100644 --- a/homeassistant/components/demo/alarm_control_panel.py +++ b/homeassistant/components/demo/alarm_control_panel.py @@ -1,4 +1,5 @@ """Demo platform that has two fake alarm control panels.""" + from __future__ import annotations import datetime diff --git a/homeassistant/components/demo/binary_sensor.py b/homeassistant/components/demo/binary_sensor.py index 21f4054b241..bc1d7b9daf2 100644 --- a/homeassistant/components/demo/binary_sensor.py +++ b/homeassistant/components/demo/binary_sensor.py @@ -1,4 +1,5 @@ """Demo platform that has two fake binary sensors.""" + from __future__ import annotations from homeassistant.components.binary_sensor import ( diff --git a/homeassistant/components/demo/button.py b/homeassistant/components/demo/button.py index 4fefd75bb8c..a3b8dd9ff0c 100644 --- a/homeassistant/components/demo/button.py +++ b/homeassistant/components/demo/button.py @@ -1,4 +1,5 @@ """Demo platform that offers a fake button entity.""" + from __future__ import annotations from homeassistant.components import persistent_notification diff --git a/homeassistant/components/demo/calendar.py b/homeassistant/components/demo/calendar.py index b4200f1be89..d513bc38250 100644 --- a/homeassistant/components/demo/calendar.py +++ b/homeassistant/components/demo/calendar.py @@ -1,4 +1,5 @@ """Demo platform that has two fake calendars.""" + from __future__ import annotations import datetime diff --git a/homeassistant/components/demo/camera.py b/homeassistant/components/demo/camera.py index 502129b5c9d..9fae6468207 100644 --- a/homeassistant/components/demo/camera.py +++ b/homeassistant/components/demo/camera.py @@ -1,4 +1,5 @@ """Demo camera platform that has a fake camera.""" + from __future__ import annotations from pathlib import Path diff --git a/homeassistant/components/demo/climate.py b/homeassistant/components/demo/climate.py index 745a2473939..ff0ed5746ca 100644 --- a/homeassistant/components/demo/climate.py +++ b/homeassistant/components/demo/climate.py @@ -1,4 +1,5 @@ """Demo platform that offers a fake climate device.""" + from __future__ import annotations from typing import Any @@ -44,7 +45,6 @@ async def async_setup_entry( swing_mode=None, hvac_mode=HVACMode.HEAT, hvac_action=HVACAction.HEATING, - aux=None, target_temp_high=None, target_temp_low=None, hvac_modes=[HVACMode.HEAT, HVACMode.OFF], @@ -57,12 +57,11 @@ async def async_setup_entry( preset=None, current_temperature=22, fan_mode="on_high", - target_humidity=67, - current_humidity=54, + target_humidity=67.4, + current_humidity=54.2, swing_mode="off", hvac_mode=HVACMode.COOL, hvac_action=HVACAction.COOLING, - aux=False, target_temp_high=None, target_temp_low=None, hvac_modes=[cls for cls in HVACMode if cls != HVACMode.HEAT_COOL], @@ -81,7 +80,6 @@ async def async_setup_entry( swing_mode="auto", hvac_mode=HVACMode.HEAT_COOL, hvac_action=None, - aux=None, target_temp_high=24, target_temp_low=21, hvac_modes=[cls for cls in HVACMode if cls != HVACMode.HEAT], @@ -108,12 +106,11 @@ class DemoClimate(ClimateEntity): preset: str | None, current_temperature: float, fan_mode: str | None, - target_humidity: int | None, - current_humidity: int | None, + target_humidity: float | None, + current_humidity: float | 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], @@ -132,8 +129,6 @@ class DemoClimate(ClimateEntity): self._attr_supported_features |= ClimateEntityFeature.TARGET_HUMIDITY if swing_mode is not None: self._attr_supported_features |= ClimateEntityFeature.SWING_MODE - if aux is not None: - self._attr_supported_features |= ClimateEntityFeature.AUX_HEAT if HVACMode.HEAT_COOL in hvac_modes or HVACMode.AUTO in hvac_modes: self._attr_supported_features |= ( ClimateEntityFeature.TARGET_TEMPERATURE_RANGE @@ -151,7 +146,6 @@ class DemoClimate(ClimateEntity): self._current_fan_mode = fan_mode self._hvac_action = hvac_action self._hvac_mode = hvac_mode - self._aux = aux self._current_swing_mode = swing_mode self._fan_modes = ["on_low", "on_high", "auto_low", "auto_high", "off"] self._hvac_modes = hvac_modes @@ -194,12 +188,12 @@ class DemoClimate(ClimateEntity): return self._target_temperature_low @property - def current_humidity(self) -> int | None: + def current_humidity(self) -> float | None: """Return the current humidity.""" return self._current_humidity @property - def target_humidity(self) -> int | None: + def target_humidity(self) -> float | None: """Return the humidity we try to reach.""" return self._target_humidity @@ -228,11 +222,6 @@ class DemoClimate(ClimateEntity): """Return preset modes.""" return self._preset_modes - @property - def is_aux_heat(self) -> bool | None: - """Return true if aux heat is on.""" - return self._aux - @property def fan_mode(self) -> str | None: """Return the fan setting.""" @@ -291,13 +280,3 @@ class DemoClimate(ClimateEntity): """Update preset_mode on.""" self._preset = preset_mode self.async_write_ha_state() - - 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) -> 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 75439e48c08..cc57ed9a460 100644 --- a/homeassistant/components/demo/config_flow.py +++ b/homeassistant/components/demo/config_flow.py @@ -1,13 +1,18 @@ """Config flow to configure demo component.""" + from __future__ import annotations from typing import Any import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult import homeassistant.helpers.config_validation as cv from . import DOMAIN @@ -19,7 +24,7 @@ CONF_SELECT = "select" CONF_MULTISELECT = "multi" -class DemoConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class DemoConfigFlow(ConfigFlow, domain=DOMAIN): """Demo configuration flow.""" VERSION = 1 @@ -27,33 +32,33 @@ class DemoConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback def async_get_options_flow( - config_entry: config_entries.ConfigEntry, + config_entry: ConfigEntry, ) -> OptionsFlowHandler: """Get the options flow for this handler.""" return OptionsFlowHandler(config_entry) - async def async_step_import(self, import_info: dict[str, Any]) -> FlowResult: + async def async_step_import(self, import_info: dict[str, Any]) -> ConfigFlowResult: """Set the config entry up from yaml.""" return self.async_create_entry(title="Demo", data=import_info) -class OptionsFlowHandler(config_entries.OptionsFlow): +class OptionsFlowHandler(OptionsFlow): """Handle options.""" - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + def __init__(self, config_entry: ConfigEntry) -> None: """Initialize options flow.""" self.config_entry = config_entry self.options = dict(config_entry.options) async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Manage the options.""" return await self.async_step_options_1() async def async_step_options_1( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Manage the options.""" if user_input is not None: self.options.update(user_input) @@ -78,7 +83,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow): async def async_step_options_2( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Manage the options 2.""" if user_input is not None: self.options.update(user_input) @@ -109,6 +114,6 @@ class OptionsFlowHandler(config_entries.OptionsFlow): ), ) - async def _update_options(self) -> FlowResult: + async def _update_options(self) -> ConfigFlowResult: """Update config entry options.""" return self.async_create_entry(title="", data=self.options) diff --git a/homeassistant/components/demo/const.py b/homeassistant/components/demo/const.py index e11b0b0731a..cd964af5f3d 100644 --- a/homeassistant/components/demo/const.py +++ b/homeassistant/components/demo/const.py @@ -1,3 +1,4 @@ """Constants for the Demo component.""" + DOMAIN = "demo" SERVICE_RANDOMIZE_DEVICE_TRACKER_DATA = "randomize_device_tracker_data" diff --git a/homeassistant/components/demo/cover.py b/homeassistant/components/demo/cover.py index 93998eb1e8b..adddb6a3a7d 100644 --- a/homeassistant/components/demo/cover.py +++ b/homeassistant/components/demo/cover.py @@ -1,4 +1,5 @@ """Demo platform for the cover component.""" + from __future__ import annotations from datetime import datetime diff --git a/homeassistant/components/demo/date.py b/homeassistant/components/demo/date.py index 34d1909bebe..b67c4248123 100644 --- a/homeassistant/components/demo/date.py +++ b/homeassistant/components/demo/date.py @@ -1,4 +1,5 @@ """Demo platform that offers a fake Date entity.""" + from __future__ import annotations from datetime import date diff --git a/homeassistant/components/demo/datetime.py b/homeassistant/components/demo/datetime.py index 63c8a5a7873..920bc14cdc5 100644 --- a/homeassistant/components/demo/datetime.py +++ b/homeassistant/components/demo/datetime.py @@ -1,4 +1,5 @@ """Demo platform that offers a fake date/time entity.""" + from __future__ import annotations from datetime import UTC, datetime diff --git a/homeassistant/components/demo/device_tracker.py b/homeassistant/components/demo/device_tracker.py index de387545368..2097f29ea28 100644 --- a/homeassistant/components/demo/device_tracker.py +++ b/homeassistant/components/demo/device_tracker.py @@ -1,4 +1,5 @@ """Demo platform for the Device tracker component.""" + from __future__ import annotations import random diff --git a/homeassistant/components/demo/event.py b/homeassistant/components/demo/event.py index 8bc720e2db7..c58b5f5fc2e 100644 --- a/homeassistant/components/demo/event.py +++ b/homeassistant/components/demo/event.py @@ -1,4 +1,5 @@ """Demo platform that offers a fake event entity.""" + from __future__ import annotations from homeassistant.components.event import EventDeviceClass, EventEntity diff --git a/homeassistant/components/demo/fan.py b/homeassistant/components/demo/fan.py index 644c4cb7860..82b256cd75f 100644 --- a/homeassistant/components/demo/fan.py +++ b/homeassistant/components/demo/fan.py @@ -1,4 +1,5 @@ """Demo fan platform that has a fake fan.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/demo/geo_location.py b/homeassistant/components/demo/geo_location.py index cd020d1bb8a..ac72a3097b0 100644 --- a/homeassistant/components/demo/geo_location.py +++ b/homeassistant/components/demo/geo_location.py @@ -1,4 +1,5 @@ """Demo platform for the geolocation component.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/demo/humidifier.py b/homeassistant/components/demo/humidifier.py index a63e3e1983f..7245d96eaf0 100644 --- a/homeassistant/components/demo/humidifier.py +++ b/homeassistant/components/demo/humidifier.py @@ -1,4 +1,5 @@ """Demo platform that offers a fake humidifier device.""" + from __future__ import annotations from typing import Any @@ -35,8 +36,8 @@ async def async_setup_entry( DemoHumidifier( name="Dehumidifier", mode=None, - target_humidity=54, - current_humidity=59, + target_humidity=54.2, + current_humidity=59.4, action=HumidifierAction.DRYING, device_class=HumidifierDeviceClass.DEHUMIDIFIER, ), @@ -59,8 +60,8 @@ class DemoHumidifier(HumidifierEntity): self, name: str, mode: str | None, - target_humidity: int, - current_humidity: int | None = None, + target_humidity: float, + current_humidity: float | None = None, available_modes: list[str] | None = None, is_on: bool = True, action: HumidifierAction | None = None, diff --git a/homeassistant/components/demo/icons.json b/homeassistant/components/demo/icons.json index 9c746c633d4..d9e1d405490 100644 --- a/homeassistant/components/demo/icons.json +++ b/homeassistant/components/demo/icons.json @@ -73,5 +73,8 @@ "default": "mdi:air-conditioner" } } + }, + "services": { + "randomize_device_tracker_data": "mdi:dice-multiple" } } diff --git a/homeassistant/components/demo/image_processing.py b/homeassistant/components/demo/image_processing.py index 71ea9d97bf6..d109f55f5a2 100644 --- a/homeassistant/components/demo/image_processing.py +++ b/homeassistant/components/demo/image_processing.py @@ -1,4 +1,5 @@ """Support for the demo image processing.""" + from __future__ import annotations from homeassistant.components.image_processing import ( diff --git a/homeassistant/components/demo/light.py b/homeassistant/components/demo/light.py index d8451bdd683..c859fef3b76 100644 --- a/homeassistant/components/demo/light.py +++ b/homeassistant/components/demo/light.py @@ -1,4 +1,5 @@ """Demo light platform that implements lights.""" + from __future__ import annotations import random diff --git a/homeassistant/components/demo/lock.py b/homeassistant/components/demo/lock.py index 3a6780ce30e..8c10877482f 100644 --- a/homeassistant/components/demo/lock.py +++ b/homeassistant/components/demo/lock.py @@ -1,4 +1,5 @@ """Demo lock platform that implements locks.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/demo/mailbox.py b/homeassistant/components/demo/mailbox.py index 8aa3e1ef384..3ec80e47118 100644 --- a/homeassistant/components/demo/mailbox.py +++ b/homeassistant/components/demo/mailbox.py @@ -1,4 +1,5 @@ """Support for a demo mailbox.""" + from __future__ import annotations from hashlib import sha1 @@ -33,7 +34,7 @@ class DemoMailbox(Mailbox): super().__init__(hass, name) self._messages: dict[str, dict[str, Any]] = {} txt = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. " - for idx in range(0, 10): + for idx in range(10): msgtime = int( dt_util.as_timestamp(dt_util.utcnow()) - 3600 * 24 * (10 - idx) ) diff --git a/homeassistant/components/demo/media_player.py b/homeassistant/components/demo/media_player.py index b0b2e1a95f5..8ce77bcd615 100644 --- a/homeassistant/components/demo/media_player.py +++ b/homeassistant/components/demo/media_player.py @@ -1,4 +1,5 @@ """Demo implementation of the media player.""" + from __future__ import annotations from datetime import datetime @@ -322,9 +323,7 @@ class DemoMusicPlayer(AbstractDemoPlayer): def join_players(self, group_members: list[str]) -> None: """Join `group_members` as a player group with the current player.""" - self._attr_group_members = [ - self.entity_id, - ] + group_members + self._attr_group_members = [self.entity_id, *group_members] self.schedule_update_ha_state() def unjoin_player(self) -> None: diff --git a/homeassistant/components/demo/notify.py b/homeassistant/components/demo/notify.py index 3d614d9abf0..c6a9483b328 100644 --- a/homeassistant/components/demo/notify.py +++ b/homeassistant/components/demo/notify.py @@ -1,4 +1,5 @@ """Demo notification service.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/demo/number.py b/homeassistant/components/demo/number.py index db065054804..8c3f5ec3477 100644 --- a/homeassistant/components/demo/number.py +++ b/homeassistant/components/demo/number.py @@ -1,4 +1,5 @@ """Demo platform that offers a fake Number entity.""" + from __future__ import annotations from homeassistant.components.number import NumberDeviceClass, NumberEntity, NumberMode diff --git a/homeassistant/components/demo/remote.py b/homeassistant/components/demo/remote.py index f4f81a52052..774f375dd27 100644 --- a/homeassistant/components/demo/remote.py +++ b/homeassistant/components/demo/remote.py @@ -1,4 +1,5 @@ """Demo platform that has two fake remotes.""" + from __future__ import annotations from collections.abc import Iterable diff --git a/homeassistant/components/demo/select.py b/homeassistant/components/demo/select.py index 58244e063f5..ff664a31d2f 100644 --- a/homeassistant/components/demo/select.py +++ b/homeassistant/components/demo/select.py @@ -1,4 +1,5 @@ """Demo platform that offers a fake select entity.""" + from __future__ import annotations from homeassistant.components.select import SelectEntity diff --git a/homeassistant/components/demo/sensor.py b/homeassistant/components/demo/sensor.py index 41057bc458f..4281ca9cc59 100644 --- a/homeassistant/components/demo/sensor.py +++ b/homeassistant/components/demo/sensor.py @@ -1,4 +1,5 @@ """Demo platform that has a couple of fake sensors.""" + from __future__ import annotations from datetime import datetime, timedelta diff --git a/homeassistant/components/demo/siren.py b/homeassistant/components/demo/siren.py index 3b3c3dfc610..235d98f5875 100644 --- a/homeassistant/components/demo/siren.py +++ b/homeassistant/components/demo/siren.py @@ -1,4 +1,5 @@ """Demo platform that offers a fake siren device.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/demo/stt.py b/homeassistant/components/demo/stt.py index 8cbc287b71d..95eebe44588 100644 --- a/homeassistant/components/demo/stt.py +++ b/homeassistant/components/demo/stt.py @@ -1,4 +1,5 @@ """Support for the demo for speech-to-text service.""" + from __future__ import annotations from collections.abc import AsyncIterable diff --git a/homeassistant/components/demo/switch.py b/homeassistant/components/demo/switch.py index ac91b069d8d..5dc05398bf1 100644 --- a/homeassistant/components/demo/switch.py +++ b/homeassistant/components/demo/switch.py @@ -1,4 +1,5 @@ """Demo platform that has two fake switches.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/demo/text.py b/homeassistant/components/demo/text.py index d7174002055..1730f414fdf 100644 --- a/homeassistant/components/demo/text.py +++ b/homeassistant/components/demo/text.py @@ -1,4 +1,5 @@ """Demo platform that offers a fake text entity.""" + from __future__ import annotations from homeassistant.components.text import TextEntity, TextMode diff --git a/homeassistant/components/demo/time.py b/homeassistant/components/demo/time.py index d0ec87386ef..f5f0322f9be 100644 --- a/homeassistant/components/demo/time.py +++ b/homeassistant/components/demo/time.py @@ -1,4 +1,5 @@ """Demo platform that offers a fake time entity.""" + from __future__ import annotations from datetime import time diff --git a/homeassistant/components/demo/tts.py b/homeassistant/components/demo/tts.py index dfc8d7d7efb..c2fa367da29 100644 --- a/homeassistant/components/demo/tts.py +++ b/homeassistant/components/demo/tts.py @@ -1,4 +1,5 @@ """Support for the demo for text-to-speech service.""" + from __future__ import annotations import os diff --git a/homeassistant/components/demo/update.py b/homeassistant/components/demo/update.py index 747b3c130d9..7e53f5ce8ca 100644 --- a/homeassistant/components/demo/update.py +++ b/homeassistant/components/demo/update.py @@ -1,4 +1,5 @@ """Demo platform that offers fake update entities.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/demo/vacuum.py b/homeassistant/components/demo/vacuum.py index 6ce67dffb90..d4c3820d29e 100644 --- a/homeassistant/components/demo/vacuum.py +++ b/homeassistant/components/demo/vacuum.py @@ -1,4 +1,5 @@ """Demo platform for the vacuum component.""" + from __future__ import annotations from datetime import datetime diff --git a/homeassistant/components/demo/water_heater.py b/homeassistant/components/demo/water_heater.py index beb46c5d8ad..f295780b190 100644 --- a/homeassistant/components/demo/water_heater.py +++ b/homeassistant/components/demo/water_heater.py @@ -1,4 +1,5 @@ """Demo platform that offers a fake water heater device.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/demo/weather.py b/homeassistant/components/demo/weather.py index a990e26c658..fbc2b660efb 100644 --- a/homeassistant/components/demo/weather.py +++ b/homeassistant/components/demo/weather.py @@ -1,4 +1,5 @@ """Demo platform that offers fake meteorological data.""" + from __future__ import annotations from datetime import datetime, timedelta diff --git a/homeassistant/components/denon/media_player.py b/homeassistant/components/denon/media_player.py index b3b9e1a98ef..1d49323f0cc 100644 --- a/homeassistant/components/denon/media_player.py +++ b/homeassistant/components/denon/media_player.py @@ -1,4 +1,5 @@ """Support for Denon Network Receivers.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/denonavr/__init__.py b/homeassistant/components/denonavr/__init__.py index d9a1300ed0e..98b77a994f6 100644 --- a/homeassistant/components/denonavr/__init__.py +++ b/homeassistant/components/denonavr/__init__.py @@ -1,4 +1,5 @@ """The denonavr component.""" + import logging from denonavr import DenonAVR diff --git a/homeassistant/components/denonavr/config_flow.py b/homeassistant/components/denonavr/config_flow.py index e93fba09a9d..9a7d2a30438 100644 --- a/homeassistant/components/denonavr/config_flow.py +++ b/homeassistant/components/denonavr/config_flow.py @@ -1,4 +1,5 @@ """Config flow to configure Denon AVR receivers using their HTTP interface.""" + from __future__ import annotations import logging @@ -9,11 +10,15 @@ import denonavr from denonavr.exceptions import AvrNetworkError, AvrTimoutError import voluptuous as vol -from homeassistant import config_entries from homeassistant.components import ssdp +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.const import CONF_HOST, CONF_MODEL, CONF_TYPE from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.httpx_client import get_async_client from .receiver import ConnectDenonAVR @@ -44,16 +49,16 @@ DEFAULT_USE_TELNET_NEW_INSTALL = True CONFIG_SCHEMA = vol.Schema({vol.Optional(CONF_HOST): str}) -class OptionsFlowHandler(config_entries.OptionsFlow): +class OptionsFlowHandler(OptionsFlow): """Options for the component.""" - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + def __init__(self, config_entry: ConfigEntry) -> None: """Init object.""" self.config_entry = config_entry async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Manage the options.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) @@ -92,7 +97,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow): return self.async_show_form(step_id="init", data_schema=settings_schema) -class DenonAvrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class DenonAvrFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a Denon AVR config flow.""" VERSION = 1 @@ -111,14 +116,14 @@ class DenonAvrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback def async_get_options_flow( - config_entry: config_entries.ConfigEntry, + config_entry: ConfigEntry, ) -> OptionsFlowHandler: """Get the options flow.""" return OptionsFlowHandler(config_entry) async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" errors = {} if user_input is not None: @@ -145,7 +150,7 @@ class DenonAvrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_select( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle multiple receivers found.""" errors: dict[str, str] = {} if user_input is not None: @@ -166,7 +171,7 @@ class DenonAvrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Allow the user to confirm adding the device.""" if user_input is not None: return await self.async_step_connect() @@ -176,7 +181,7 @@ class DenonAvrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_connect( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Connect to the receiver.""" assert self.host connect_denonavr = ConnectDenonAVR( @@ -230,7 +235,9 @@ class DenonAvrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): options={CONF_USE_TELNET: DEFAULT_USE_TELNET_NEW_INSTALL}, ) - async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: + async def async_step_ssdp( + self, discovery_info: ssdp.SsdpServiceInfo + ) -> ConfigFlowResult: """Handle a discovered Denon AVR. This flow is triggered by the SSDP component. It will check if the diff --git a/homeassistant/components/denonavr/manifest.json b/homeassistant/components/denonavr/manifest.json index d595c7616ba..9188009bde5 100644 --- a/homeassistant/components/denonavr/manifest.json +++ b/homeassistant/components/denonavr/manifest.json @@ -4,7 +4,6 @@ "codeowners": ["@ol-iver", "@starkillerOG"], "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/denonavr", - "import_executor": true, "iot_class": "local_push", "loggers": ["denonavr"], "requirements": ["denonavr==0.11.6"], diff --git a/homeassistant/components/denonavr/media_player.py b/homeassistant/components/denonavr/media_player.py index 0002b04bd62..2f9b96d9471 100644 --- a/homeassistant/components/denonavr/media_player.py +++ b/homeassistant/components/denonavr/media_player.py @@ -1,4 +1,5 @@ """Support for Denon AVR receivers using their HTTP interface.""" + from __future__ import annotations from collections.abc import Awaitable, Callable, Coroutine diff --git a/homeassistant/components/denonavr/receiver.py b/homeassistant/components/denonavr/receiver.py index c400ed0bcce..abee5ed74d2 100644 --- a/homeassistant/components/denonavr/receiver.py +++ b/homeassistant/components/denonavr/receiver.py @@ -1,4 +1,5 @@ """Code to handle a DenonAVR receiver.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/derivative/__init__.py b/homeassistant/components/derivative/__init__.py index c5b1c8e31e9..2b365e96244 100644 --- a/homeassistant/components/derivative/__init__.py +++ b/homeassistant/components/derivative/__init__.py @@ -1,4 +1,5 @@ """The Derivative integration.""" + from __future__ import annotations from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/derivative/config_flow.py b/homeassistant/components/derivative/config_flow.py index 3b0b2425aac..e15741ce9cf 100644 --- a/homeassistant/components/derivative/config_flow.py +++ b/homeassistant/components/derivative/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Derivative integration.""" + from __future__ import annotations from collections.abc import Mapping diff --git a/homeassistant/components/derivative/sensor.py b/homeassistant/components/derivative/sensor.py index cd912ceb24e..ea343288c9c 100644 --- a/homeassistant/components/derivative/sensor.py +++ b/homeassistant/components/derivative/sensor.py @@ -1,4 +1,5 @@ """Numeric derivative of data coming from a source sensor over time.""" + from __future__ import annotations from datetime import datetime, timedelta @@ -18,7 +19,7 @@ from homeassistant.const import ( STATE_UNKNOWN, UnitOfTime, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import Event, HomeAssistant, callback from homeassistant.helpers import ( config_validation as cv, device_registry as dr, @@ -30,7 +31,7 @@ from homeassistant.helpers.event import ( EventStateChangedData, async_track_state_change_event, ) -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, EventType +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import ( CONF_ROUND_DIGITS, @@ -211,7 +212,7 @@ class DerivativeSensor(RestoreSensor, SensorEntity): _LOGGER.warning("Could not restore last state: %s", err) @callback - def calc_derivative(event: EventType[EventStateChangedData]) -> None: + def calc_derivative(event: Event[EventStateChangedData]) -> None: """Handle the sensor state changes.""" if ( (old_state := event.data["old_state"]) is None diff --git a/homeassistant/components/devialet/__init__.py b/homeassistant/components/devialet/__init__.py index 034f93abb68..2eccdb2a4b6 100644 --- a/homeassistant/components/devialet/__init__.py +++ b/homeassistant/components/devialet/__init__.py @@ -1,4 +1,5 @@ """The Devialet integration.""" + from __future__ import annotations from devialet import DevialetApi diff --git a/homeassistant/components/devialet/config_flow.py b/homeassistant/components/devialet/config_flow.py index de52788de50..4c097ae6f86 100644 --- a/homeassistant/components/devialet/config_flow.py +++ b/homeassistant/components/devialet/config_flow.py @@ -1,4 +1,5 @@ """Support for Devialet Phantom speakers.""" + from __future__ import annotations import logging @@ -8,9 +9,8 @@ from devialet.devialet_api import DevialetApi import voluptuous as vol from homeassistant.components import zeroconf -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_NAME -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN @@ -31,7 +31,7 @@ class DevialetFlowHandler(ConfigFlow, domain=DOMAIN): self._serial: str | None = None self._errors: dict[str, str] = {} - async def async_validate_input(self) -> FlowResult | None: + async def async_validate_input(self) -> ConfigFlowResult | None: """Validate the input using the Devialet API.""" self._errors.clear() @@ -53,7 +53,7 @@ class DevialetFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by the user or zeroconf.""" if user_input is not None: @@ -70,7 +70,7 @@ class DevialetFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_zeroconf( self, discovery_info: zeroconf.ZeroconfServiceInfo - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by zeroconf discovery.""" LOGGER.info("Devialet device found via ZEROCONF: %s", discovery_info) @@ -87,7 +87,7 @@ class DevialetFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle user-confirmation of discovered node.""" title = f"{self._name} ({self._model})" diff --git a/homeassistant/components/devialet/const.py b/homeassistant/components/devialet/const.py index ccb4fbc7964..58482134527 100644 --- a/homeassistant/components/devialet/const.py +++ b/homeassistant/components/devialet/const.py @@ -1,4 +1,5 @@ """Constants for the Devialet integration.""" + from typing import Final DOMAIN: Final = "devialet" diff --git a/homeassistant/components/devialet/coordinator.py b/homeassistant/components/devialet/coordinator.py index 9e1eada7183..9cfeb797373 100644 --- a/homeassistant/components/devialet/coordinator.py +++ b/homeassistant/components/devialet/coordinator.py @@ -1,4 +1,5 @@ """Class representing a Devialet update coordinator.""" + from datetime import timedelta import logging diff --git a/homeassistant/components/devialet/diagnostics.py b/homeassistant/components/devialet/diagnostics.py index f9824a9cad1..ae887dd1c8c 100644 --- a/homeassistant/components/devialet/diagnostics.py +++ b/homeassistant/components/devialet/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for Devialet.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/devialet/media_player.py b/homeassistant/components/devialet/media_player.py index a79a82e6f60..d490e348b9c 100644 --- a/homeassistant/components/devialet/media_player.py +++ b/homeassistant/components/devialet/media_player.py @@ -1,4 +1,5 @@ """Support for Devialet speakers.""" + from __future__ import annotations from devialet.const import NORMAL_INPUTS diff --git a/homeassistant/components/devialet/translations/en.json b/homeassistant/components/devialet/translations/en.json deleted file mode 100644 index af0cfc4c122..00000000000 --- a/homeassistant/components/devialet/translations/en.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Service is already configured" - }, - "error": { - "cannot_connect": "Failed to connect" - }, - "flow_title": "{title}", - "step": { - "confirm": { - "description": "Do you want to set up Devialet device {device}?" - }, - "user": { - "data": { - "host": "Host" - }, - "description": "Please enter the host name or IP address of the Devialet device." - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/device_automation/__init__.py b/homeassistant/components/device_automation/__init__.py index 2bf87343c72..6d95d18214e 100644 --- a/homeassistant/components/device_automation/__init__.py +++ b/homeassistant/components/device_automation/__init__.py @@ -1,4 +1,5 @@ """Helpers for device automations.""" + from __future__ import annotations import asyncio @@ -133,8 +134,7 @@ async def async_get_device_automation_platform( hass: HomeAssistant, domain: str, automation_type: Literal[DeviceAutomationType.TRIGGER], -) -> DeviceAutomationTriggerProtocol: - ... +) -> DeviceAutomationTriggerProtocol: ... @overload @@ -142,8 +142,7 @@ async def async_get_device_automation_platform( hass: HomeAssistant, domain: str, automation_type: Literal[DeviceAutomationType.CONDITION], -) -> DeviceAutomationConditionProtocol: - ... +) -> DeviceAutomationConditionProtocol: ... @overload @@ -151,15 +150,13 @@ async def async_get_device_automation_platform( hass: HomeAssistant, domain: str, automation_type: Literal[DeviceAutomationType.ACTION], -) -> DeviceAutomationActionProtocol: - ... +) -> DeviceAutomationActionProtocol: ... @overload async def async_get_device_automation_platform( hass: HomeAssistant, domain: str, automation_type: DeviceAutomationType -) -> DeviceAutomationPlatformType: - ... +) -> DeviceAutomationPlatformType: ... async def async_get_device_automation_platform( @@ -172,7 +169,7 @@ async def async_get_device_automation_platform( platform_name = automation_type.value.section try: integration = await async_get_integration_with_requirements(hass, domain) - platform = integration.get_platform(platform_name) + platform = await integration.async_get_platform(platform_name) except IntegrationNotFound as err: raise InvalidDeviceAutomationConfig( f"Integration '{domain}' not found" diff --git a/homeassistant/components/device_automation/action.py b/homeassistant/components/device_automation/action.py index 58c124377ff..b1c63ac439b 100644 --- a/homeassistant/components/device_automation/action.py +++ b/homeassistant/components/device_automation/action.py @@ -1,4 +1,5 @@ """Device action validator.""" + from __future__ import annotations from typing import Any, Protocol diff --git a/homeassistant/components/device_automation/condition.py b/homeassistant/components/device_automation/condition.py index f819668f090..13454d416a0 100644 --- a/homeassistant/components/device_automation/condition.py +++ b/homeassistant/components/device_automation/condition.py @@ -1,4 +1,5 @@ """Validate device conditions.""" + from __future__ import annotations from typing import TYPE_CHECKING, Any, Protocol diff --git a/homeassistant/components/device_automation/const.py b/homeassistant/components/device_automation/const.py index 2970e5e79b2..5cbc0378c41 100644 --- a/homeassistant/components/device_automation/const.py +++ b/homeassistant/components/device_automation/const.py @@ -1,4 +1,5 @@ """Constants for device automations.""" + CONF_CHANGED_STATES = "changed_states" CONF_IS_OFF = "is_off" CONF_IS_ON = "is_on" diff --git a/homeassistant/components/device_automation/entity.py b/homeassistant/components/device_automation/entity.py index 87ff5a2cb52..aaa14dbf9b0 100644 --- a/homeassistant/components/device_automation/entity.py +++ b/homeassistant/components/device_automation/entity.py @@ -1,4 +1,5 @@ """Device automation helpers for entity.""" + from __future__ import annotations import voluptuous as vol diff --git a/homeassistant/components/device_automation/exceptions.py b/homeassistant/components/device_automation/exceptions.py index 0b2f2c01be7..8782f8245f3 100644 --- a/homeassistant/components/device_automation/exceptions.py +++ b/homeassistant/components/device_automation/exceptions.py @@ -1,4 +1,5 @@ """Device automation exceptions.""" + from homeassistant.exceptions import HomeAssistantError diff --git a/homeassistant/components/device_automation/helpers.py b/homeassistant/components/device_automation/helpers.py index a00455293f6..0d935444a59 100644 --- a/homeassistant/components/device_automation/helpers.py +++ b/homeassistant/components/device_automation/helpers.py @@ -1,4 +1,5 @@ """Helpers for device oriented automations.""" + from __future__ import annotations from typing import cast diff --git a/homeassistant/components/device_automation/toggle_entity.py b/homeassistant/components/device_automation/toggle_entity.py index 189fc750e50..d2220836226 100644 --- a/homeassistant/components/device_automation/toggle_entity.py +++ b/homeassistant/components/device_automation/toggle_entity.py @@ -1,4 +1,5 @@ """Device automation helpers for toggle entity.""" + from __future__ import annotations import voluptuous as vol diff --git a/homeassistant/components/device_automation/trigger.py b/homeassistant/components/device_automation/trigger.py index 6bbbd6febce..cc8c4d4d52e 100644 --- a/homeassistant/components/device_automation/trigger.py +++ b/homeassistant/components/device_automation/trigger.py @@ -1,4 +1,5 @@ """Offer device oriented automation.""" + from __future__ import annotations from typing import Any, Protocol diff --git a/homeassistant/components/device_sun_light_trigger/__init__.py b/homeassistant/components/device_sun_light_trigger/__init__.py index ea9205ebdec..861a634eda7 100644 --- a/homeassistant/components/device_sun_light_trigger/__init__.py +++ b/homeassistant/components/device_sun_light_trigger/__init__.py @@ -1,14 +1,22 @@ """Support to turn on lights based on the states.""" + from datetime import timedelta import logging import voluptuous as vol +from homeassistant.components.device_tracker import ( + DOMAIN as DOMAIN_DEVICE_TRACKER, + is_on as device_tracker_is_on, +) +from homeassistant.components.group import get_entity_ids as group_get_entity_ids from homeassistant.components.light import ( ATTR_PROFILE, ATTR_TRANSITION, DOMAIN as DOMAIN_LIGHT, + is_on as light_is_on, ) +from homeassistant.components.person import DOMAIN as DOMAIN_PERSON from homeassistant.const import ( ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_START, @@ -86,16 +94,16 @@ async def activate_automation( # noqa: C901 ): """Activate the automation.""" logger = logging.getLogger(__name__) - device_tracker = hass.components.device_tracker - group = hass.components.group - light = hass.components.light - person = hass.components.person if device_group is None: - device_entity_ids = hass.states.async_entity_ids(device_tracker.DOMAIN) + device_entity_ids = hass.states.async_entity_ids(DOMAIN_DEVICE_TRACKER) else: - device_entity_ids = group.get_entity_ids(device_group, device_tracker.DOMAIN) - device_entity_ids.extend(group.get_entity_ids(device_group, person.DOMAIN)) + device_entity_ids = group_get_entity_ids( + hass, device_group, DOMAIN_DEVICE_TRACKER + ) + device_entity_ids.extend( + group_get_entity_ids(hass, device_group, DOMAIN_PERSON) + ) if not device_entity_ids: logger.error("No devices found to track") @@ -103,9 +111,9 @@ async def activate_automation( # noqa: C901 # Get the light IDs from the specified group if light_group is None: - light_ids = hass.states.async_entity_ids(light.DOMAIN) + light_ids = hass.states.async_entity_ids(DOMAIN_LIGHT) else: - light_ids = group.get_entity_ids(light_group, light.DOMAIN) + light_ids = group_get_entity_ids(hass, light_group, DOMAIN_LIGHT) if not light_ids: logger.error("No lights found to turn on") @@ -114,12 +122,12 @@ async def activate_automation( # noqa: C901 @callback def anyone_home(): """Test if anyone is home.""" - return any(device_tracker.is_on(dt_id) for dt_id in device_entity_ids) + return any(device_tracker_is_on(hass, dt_id) for dt_id in device_entity_ids) @callback def any_light_on(): """Test if any light on.""" - return any(light.is_on(light_id) for light_id in light_ids) + return any(light_is_on(hass, light_id) for light_id in light_ids) def calc_time_for_light_when_sunset(): """Calculate the time when to start fading lights in when sun sets. @@ -135,7 +143,7 @@ async def activate_automation( # noqa: C901 async def async_turn_on_before_sunset(light_id): """Turn on lights.""" - if not anyone_home() or light.is_on(light_id): + if not anyone_home() or light_is_on(hass, light_id): return await hass.services.async_call( DOMAIN_LIGHT, diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index adcc90cccbf..ca78b1cbdc5 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -1,4 +1,5 @@ """Provide functionality to keep track of devices.""" + from __future__ import annotations from functools import partial @@ -13,6 +14,7 @@ from homeassistant.helpers.deprecation import ( from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass +from . import group as group_pre_import # noqa: F401 from .config_entry import ( # noqa: F401 ScannerEntity, TrackerEntity, @@ -67,16 +69,7 @@ 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) - + async_setup_legacy_integration(hass, config) return True diff --git a/homeassistant/components/device_tracker/config_entry.py b/homeassistant/components/device_tracker/config_entry.py index 20ac365b33b..a1c1961dc43 100644 --- a/homeassistant/components/device_tracker/config_entry.py +++ b/homeassistant/components/device_tracker/config_entry.py @@ -1,4 +1,5 @@ """Code to set up a device tracker platform using a config entry.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/device_tracker/const.py b/homeassistant/components/device_tracker/const.py index 67a90ab0f95..964b7faab9b 100644 --- a/homeassistant/components/device_tracker/const.py +++ b/homeassistant/components/device_tracker/const.py @@ -1,4 +1,5 @@ """Device tracker constants.""" + from __future__ import annotations from datetime import timedelta @@ -13,6 +14,7 @@ from homeassistant.helpers.deprecation import ( check_if_deprecated_constant, dir_with_deprecated_constants, ) +from homeassistant.util.signal_type import SignalType LOGGER: Final = logging.getLogger(__package__) @@ -67,7 +69,9 @@ ATTR_SOURCE_TYPE: Final = "source_type" ATTR_CONSIDER_HOME: Final = "consider_home" ATTR_IP: Final = "ip" -CONNECTED_DEVICE_REGISTERED: Final = "device_tracker_connected_device_registered" +CONNECTED_DEVICE_REGISTERED = SignalType[dict[str, str | None]]( + "device_tracker_connected_device_registered" +) # These can be removed if no deprecated constant are in this module anymore __getattr__ = partial(check_if_deprecated_constant, module_globals=globals()) diff --git a/homeassistant/components/device_tracker/device_condition.py b/homeassistant/components/device_tracker/device_condition.py index b5bf850b4fa..2d6d723dc49 100644 --- a/homeassistant/components/device_tracker/device_condition.py +++ b/homeassistant/components/device_tracker/device_condition.py @@ -1,4 +1,5 @@ """Provides device automations for Device tracker.""" + from __future__ import annotations import voluptuous as vol diff --git a/homeassistant/components/device_tracker/device_trigger.py b/homeassistant/components/device_tracker/device_trigger.py index 404dad0d4d1..bcd2f0f2342 100644 --- a/homeassistant/components/device_tracker/device_trigger.py +++ b/homeassistant/components/device_tracker/device_trigger.py @@ -1,4 +1,5 @@ """Provides device automations for Device Tracker.""" + from __future__ import annotations from operator import attrgetter diff --git a/homeassistant/components/device_tracker/group.py b/homeassistant/components/device_tracker/group.py index 9bd2c991678..e1b93696aa9 100644 --- a/homeassistant/components/device_tracker/group.py +++ b/homeassistant/components/device_tracker/group.py @@ -1,14 +1,17 @@ """Describe group states.""" +from typing import TYPE_CHECKING -from homeassistant.components.group import GroupIntegrationRegistry from homeassistant.const import STATE_HOME, STATE_NOT_HOME from homeassistant.core import HomeAssistant, callback +if TYPE_CHECKING: + from homeassistant.components.group import GroupIntegrationRegistry + @callback def async_describe_on_off_states( - hass: HomeAssistant, registry: GroupIntegrationRegistry + hass: HomeAssistant, registry: "GroupIntegrationRegistry" ) -> None: """Describe group on off states.""" registry.on_off_states({STATE_HOME}, STATE_NOT_HOME) diff --git a/homeassistant/components/device_tracker/legacy.py b/homeassistant/components/device_tracker/legacy.py index e1a8058d819..91cf35f43bd 100644 --- a/homeassistant/components/device_tracker/legacy.py +++ b/homeassistant/components/device_tracker/legacy.py @@ -1,4 +1,5 @@ """Legacy device tracker classes.""" + from __future__ import annotations import asyncio @@ -14,6 +15,7 @@ import voluptuous as vol from homeassistant import util from homeassistant.backports.functools import cached_property from homeassistant.components import zone +from homeassistant.components.zone import ENTITY_ID_HOME from homeassistant.config import ( async_log_schema_error, config_per_platform, @@ -48,11 +50,13 @@ from homeassistant.helpers.event import ( from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, GPSType, StateType from homeassistant.setup import ( + SetupPhases, async_notify_setup_error, async_prepare_setup_platform, async_start_setup, ) from homeassistant.util import dt as dt_util +from homeassistant.util.async_ import create_eager_task from homeassistant.util.yaml import dump from .const import ( @@ -197,19 +201,14 @@ def see( hass.services.call(DOMAIN, SERVICE_SEE, data) -async def async_setup_integration(hass: HomeAssistant, config: ConfigType) -> None: +@callback +def async_setup_integration(hass: HomeAssistant, config: ConfigType) -> None: """Set up the legacy integration.""" - tracker = await get_tracker(hass, config) - - legacy_platforms = await async_extract_config(hass, config) - - setup_tasks = [ - asyncio.create_task(legacy_platform.async_setup_legacy(hass, tracker)) - for legacy_platform in legacy_platforms - ] - - if setup_tasks: - await asyncio.wait(setup_tasks) + # The tracker is loaded in the _async_setup_integration task so + # we create a future to avoid waiting on it here so that only + # async_platform_discovered will have to wait in the rare event + # a custom component still uses the legacy device tracker discovery. + tracker_future: asyncio.Future[DeviceTracker] = hass.loop.create_future() async def async_platform_discovered( p_type: str, info: dict[str, Any] | None @@ -220,15 +219,30 @@ async def async_setup_integration(hass: HomeAssistant, config: ConfigType) -> No if platform is None or platform.type != PLATFORM_TYPE_LEGACY: return + tracker = await tracker_future await platform.async_setup_legacy(hass, tracker, info) discovery.async_listen_platform(hass, DOMAIN, async_platform_discovered) - - # Clean up stale devices - cancel_update_stale = async_track_utc_time_change( - hass, tracker.async_update_stale, second=range(0, 60, 5) + # + # Legacy and platforms load in a non-awaited tracked task + # to ensure device tracker setup can continue and config + # entry integrations are not waiting for legacy device + # tracker platforms to be set up. + # + hass.async_create_task( + _async_setup_integration(hass, config, tracker_future), eager_start=True ) + +async def _async_setup_integration( + hass: HomeAssistant, + config: ConfigType, + tracker_future: asyncio.Future[DeviceTracker], +) -> None: + """Set up the legacy integration.""" + tracker = await get_tracker(hass, config) + tracker_future.set_result(tracker) + async def async_see_service(call: ServiceCall) -> None: """Service to see a device.""" # Temp workaround for iOS, introduced in 0.65 @@ -241,6 +255,21 @@ async def async_setup_integration(hass: HomeAssistant, config: ConfigType) -> No DOMAIN, SERVICE_SEE, async_see_service, SERVICE_SEE_PAYLOAD_SCHEMA ) + legacy_platforms = await async_extract_config(hass, config) + + setup_tasks = [ + create_eager_task(legacy_platform.async_setup_legacy(hass, tracker)) + for legacy_platform in legacy_platforms + ] + + if setup_tasks: + await asyncio.wait(setup_tasks) + + # Clean up stale devices + cancel_update_stale = async_track_utc_time_change( + hass, tracker.async_update_stale, second=range(0, 60, 5) + ) + # restore await tracker.async_setup_tracked_device() @@ -252,7 +281,9 @@ async def async_setup_integration(hass: HomeAssistant, config: ConfigType) -> No """ cancel_update_stale() - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _on_hass_stop) + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, _on_hass_stop, run_immediately=True + ) @attr.s @@ -289,7 +320,12 @@ class DeviceTrackerPlatform: assert self.type == PLATFORM_TYPE_LEGACY full_name = f"{self.name}.{DOMAIN}" LOGGER.info("Setting up %s", full_name) - with async_start_setup(hass, [full_name]): + with async_start_setup( + hass, + integration=self.name, + group=str(id(self.config)), + phase=SetupPhases.PLATFORM_SETUP, + ): try: scanner = None setup: bool | None = None @@ -480,7 +516,7 @@ def async_setup_scanner_platform( }, } - zone_home = hass.states.get(hass.components.zone.ENTITY_ID_HOME) + zone_home = hass.states.get(ENTITY_ID_HOME) if zone_home is not None: kwargs["gps"] = [ zone_home.attributes[ATTR_LATITUDE], @@ -938,7 +974,7 @@ class DeviceScanner: def scan_devices(self) -> list[str]: """Scan for devices.""" - raise NotImplementedError() + raise NotImplementedError async def async_scan_devices(self) -> list[str]: """Scan for devices.""" @@ -949,7 +985,7 @@ class DeviceScanner: def get_device_name(self, device: str) -> str | None: """Get the name of a device.""" - raise NotImplementedError() + raise NotImplementedError async def async_get_device_name(self, device: str) -> str | None: """Get the name of a device.""" @@ -960,7 +996,7 @@ class DeviceScanner: def get_extra_attributes(self, device: str) -> dict: """Get the extra attributes of a device.""" - raise NotImplementedError() + raise NotImplementedError async def async_get_extra_attributes(self, device: str) -> dict: """Get the extra attributes of a device.""" diff --git a/homeassistant/components/devolo_home_control/__init__.py b/homeassistant/components/devolo_home_control/__init__.py index ec0d5a3a666..78e536209d1 100644 --- a/homeassistant/components/devolo_home_control/__init__.py +++ b/homeassistant/components/devolo_home_control/__init__.py @@ -1,4 +1,5 @@ """The devolo_home_control integration.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/devolo_home_control/binary_sensor.py b/homeassistant/components/devolo_home_control/binary_sensor.py index a0d80926bc8..43793a15368 100644 --- a/homeassistant/components/devolo_home_control/binary_sensor.py +++ b/homeassistant/components/devolo_home_control/binary_sensor.py @@ -1,4 +1,5 @@ """Platform for binary sensor integration.""" + from __future__ import annotations from devolo_home_control_api.devices.zwave import Zwave @@ -33,29 +34,27 @@ async def async_setup_entry( entities: list[BinarySensorEntity] = [] for gateway in hass.data[DOMAIN][entry.entry_id]["gateways"]: - for device in gateway.binary_sensor_devices: - for binary_sensor in device.binary_sensor_property: - entities.append( - DevoloBinaryDeviceEntity( - homecontrol=gateway, - device_instance=device, - element_uid=binary_sensor, - ) - ) - for device in gateway.devices.values(): - if hasattr(device, "remote_control_property"): - for remote in device.remote_control_property: - for index in range( - 1, device.remote_control_property[remote].key_count + 1 - ): - entities.append( - DevoloRemoteControl( - homecontrol=gateway, - device_instance=device, - element_uid=remote, - key=index, - ) - ) + entities.extend( + DevoloBinaryDeviceEntity( + homecontrol=gateway, + device_instance=device, + element_uid=binary_sensor, + ) + for device in gateway.binary_sensor_devices + for binary_sensor in device.binary_sensor_property + ) + entities.extend( + DevoloRemoteControl( + homecontrol=gateway, + device_instance=device, + element_uid=remote, + key=index, + ) + for device in gateway.devices.values() + if hasattr(device, "remote_control_property") + for remote in device.remote_control_property + for index in range(1, device.remote_control_property[remote].key_count + 1) + ) async_add_entities(entities) diff --git a/homeassistant/components/devolo_home_control/climate.py b/homeassistant/components/devolo_home_control/climate.py index 9f17a653673..f94c7dae15a 100644 --- a/homeassistant/components/devolo_home_control/climate.py +++ b/homeassistant/components/devolo_home_control/climate.py @@ -1,4 +1,5 @@ """Platform for climate integration.""" + from __future__ import annotations from typing import Any @@ -25,26 +26,24 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Get all cover devices and setup them via config entry.""" - entities = [] - for gateway in hass.data[DOMAIN][entry.entry_id]["gateways"]: - for device in gateway.multi_level_switch_devices: - for multi_level_switch in device.multi_level_switch_property: - if device.device_model_uid in ( - "devolo.model.Thermostat:Valve", - "devolo.model.Room:Thermostat", - "devolo.model.Eurotronic:Spirit:Device", - "unk.model.Danfoss:Thermostat", - ): - entities.append( - DevoloClimateDeviceEntity( - homecontrol=gateway, - device_instance=device, - element_uid=multi_level_switch, - ) - ) - - async_add_entities(entities) + async_add_entities( + DevoloClimateDeviceEntity( + homecontrol=gateway, + device_instance=device, + element_uid=multi_level_switch, + ) + for gateway in hass.data[DOMAIN][entry.entry_id]["gateways"] + for device in gateway.multi_level_switch_devices + for multi_level_switch in device.multi_level_switch_property + if device.device_model_uid + in ( + "devolo.model.Thermostat:Valve", + "devolo.model.Room:Thermostat", + "devolo.model.Eurotronic:Spirit:Device", + "unk.model.Danfoss:Thermostat", + ) + ) class DevoloClimateDeviceEntity(DevoloMultiLevelSwitchDeviceEntity, ClimateEntity): diff --git a/homeassistant/components/devolo_home_control/config_flow.py b/homeassistant/components/devolo_home_control/config_flow.py index aef9592d2e9..662ce51daaf 100644 --- a/homeassistant/components/devolo_home_control/config_flow.py +++ b/homeassistant/components/devolo_home_control/config_flow.py @@ -1,4 +1,5 @@ """Config flow to configure the devolo home control integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -6,19 +7,17 @@ from typing import Any import voluptuous as vol -from homeassistant import config_entries from homeassistant.components import zeroconf -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult from . import configure_mydevolo from .const import CONF_MYDEVOLO, DEFAULT_MYDEVOLO, DOMAIN, SUPPORTED_MODEL_TYPES from .exceptions import CredentialsInvalid, UuidChanged -class DevoloHomeControlFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class DevoloHomeControlFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a devolo HomeControl config flow.""" VERSION = 1 @@ -34,7 +33,7 @@ class DevoloHomeControlFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initiated by the user.""" if self.show_advanced_options: self.data_schema[vol.Required(CONF_MYDEVOLO, default=self._url)] = str @@ -47,7 +46,7 @@ class DevoloHomeControlFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_zeroconf( self, discovery_info: zeroconf.ZeroconfServiceInfo - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle zeroconf discovery.""" # Check if it is a gateway if discovery_info.properties.get("MT") in SUPPORTED_MODEL_TYPES: @@ -57,7 +56,7 @@ class DevoloHomeControlFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_zeroconf_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initiated by zeroconf.""" if user_input is None: return self._show_form(step_id="zeroconf_confirm") @@ -68,7 +67,9 @@ class DevoloHomeControlFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): step_id="zeroconf_confirm", errors={"base": "invalid_auth"} ) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Handle reauthentication.""" self._reauth_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] @@ -82,7 +83,7 @@ class DevoloHomeControlFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initiated by reauthentication.""" if user_input is None: return self._show_form(step_id="reauth_confirm") @@ -97,7 +98,7 @@ class DevoloHomeControlFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): step_id="reauth_confirm", errors={"base": "reauth_failed"} ) - async def _connect_mydevolo(self, user_input: dict[str, Any]) -> FlowResult: + async def _connect_mydevolo(self, user_input: dict[str, Any]) -> ConfigFlowResult: """Connect to mydevolo.""" user_input[CONF_MYDEVOLO] = user_input.get(CONF_MYDEVOLO, self._url) mydevolo = configure_mydevolo(conf=user_input) @@ -135,7 +136,7 @@ class DevoloHomeControlFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @callback def _show_form( self, step_id: str, errors: dict[str, str] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Show the form to the user.""" return self.async_show_form( step_id=step_id, diff --git a/homeassistant/components/devolo_home_control/const.py b/homeassistant/components/devolo_home_control/const.py index 6ba934f6591..eb48a6d269e 100644 --- a/homeassistant/components/devolo_home_control/const.py +++ b/homeassistant/components/devolo_home_control/const.py @@ -1,4 +1,5 @@ """Constants for the devolo_home_control integration.""" + import re from homeassistant.const import Platform diff --git a/homeassistant/components/devolo_home_control/cover.py b/homeassistant/components/devolo_home_control/cover.py index b76948bcee7..03aec622645 100644 --- a/homeassistant/components/devolo_home_control/cover.py +++ b/homeassistant/components/devolo_home_control/cover.py @@ -1,4 +1,5 @@ """Platform for cover integration.""" + from __future__ import annotations from typing import Any @@ -20,21 +21,18 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Get all cover devices and setup them via config entry.""" - entities = [] - for gateway in hass.data[DOMAIN][entry.entry_id]["gateways"]: - for device in gateway.multi_level_switch_devices: - for multi_level_switch in device.multi_level_switch_property: - if multi_level_switch.startswith("devolo.Blinds"): - entities.append( - DevoloCoverDeviceEntity( - homecontrol=gateway, - device_instance=device, - element_uid=multi_level_switch, - ) - ) - - async_add_entities(entities) + async_add_entities( + DevoloCoverDeviceEntity( + homecontrol=gateway, + device_instance=device, + element_uid=multi_level_switch, + ) + for gateway in hass.data[DOMAIN][entry.entry_id]["gateways"] + for device in gateway.multi_level_switch_devices + for multi_level_switch in device.multi_level_switch_property + if multi_level_switch.startswith("devolo.Blinds") + ) class DevoloCoverDeviceEntity(DevoloMultiLevelSwitchDeviceEntity, CoverEntity): diff --git a/homeassistant/components/devolo_home_control/devolo_device.py b/homeassistant/components/devolo_home_control/devolo_device.py index e63e711ea6f..fe8212732a5 100644 --- a/homeassistant/components/devolo_home_control/devolo_device.py +++ b/homeassistant/components/devolo_home_control/devolo_device.py @@ -1,4 +1,5 @@ """Base class for a device entity integrated in devolo Home Control.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/devolo_home_control/devolo_multi_level_switch.py b/homeassistant/components/devolo_home_control/devolo_multi_level_switch.py index d2608ed43c7..3072cb01f2e 100644 --- a/homeassistant/components/devolo_home_control/devolo_multi_level_switch.py +++ b/homeassistant/components/devolo_home_control/devolo_multi_level_switch.py @@ -1,4 +1,5 @@ """Base class for multi level switches in devolo Home Control.""" + from devolo_home_control_api.devices.zwave import Zwave from devolo_home_control_api.homecontrol import HomeControl diff --git a/homeassistant/components/devolo_home_control/diagnostics.py b/homeassistant/components/devolo_home_control/diagnostics.py index 412effcd5ed..753d04db0a3 100644 --- a/homeassistant/components/devolo_home_control/diagnostics.py +++ b/homeassistant/components/devolo_home_control/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for devolo Home Control.""" + from __future__ import annotations from typing import Any @@ -21,25 +22,24 @@ async def async_get_config_entry_diagnostics( """Return diagnostics for a config entry.""" gateways: list[HomeControl] = hass.data[DOMAIN][entry.entry_id]["gateways"] - device_info = [] - for gateway in gateways: - device_info.append( - { - "gateway": { - "local_connection": gateway.gateway.local_connection, - "firmware_version": gateway.gateway.firmware_version, - }, - "devices": [ - { - "device_id": device_id, - "device_model_uid": properties.device_model_uid, - "device_type": properties.device_type, - "name": properties.name, - } - for device_id, properties in gateway.devices.items() - ], - } - ) + device_info = [ + { + "gateway": { + "local_connection": gateway.gateway.local_connection, + "firmware_version": gateway.gateway.firmware_version, + }, + "devices": [ + { + "device_id": device_id, + "device_model_uid": properties.device_model_uid, + "device_type": properties.device_type, + "name": properties.name, + } + for device_id, properties in gateway.devices.items() + ], + } + for gateway in gateways + ] diag_data = { "entry": async_redact_data(entry.as_dict(), TO_REDACT), diff --git a/homeassistant/components/devolo_home_control/exceptions.py b/homeassistant/components/devolo_home_control/exceptions.py index a89058e6c16..1ae66d47be2 100644 --- a/homeassistant/components/devolo_home_control/exceptions.py +++ b/homeassistant/components/devolo_home_control/exceptions.py @@ -1,4 +1,5 @@ """Custom exceptions for the devolo_home_control integration.""" + from homeassistant.exceptions import HomeAssistantError diff --git a/homeassistant/components/devolo_home_control/light.py b/homeassistant/components/devolo_home_control/light.py index e91466c7ece..36c72ca7f57 100644 --- a/homeassistant/components/devolo_home_control/light.py +++ b/homeassistant/components/devolo_home_control/light.py @@ -1,4 +1,5 @@ """Platform for light integration.""" + from __future__ import annotations from typing import Any @@ -19,21 +20,18 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Get all light devices and setup them via config entry.""" - entities = [] - for gateway in hass.data[DOMAIN][entry.entry_id]["gateways"]: - for device in gateway.multi_level_switch_devices: - for multi_level_switch in device.multi_level_switch_property.values(): - if multi_level_switch.switch_type == "dimmer": - entities.append( - DevoloLightDeviceEntity( - homecontrol=gateway, - device_instance=device, - element_uid=multi_level_switch.element_uid, - ) - ) - - async_add_entities(entities) + async_add_entities( + DevoloLightDeviceEntity( + homecontrol=gateway, + device_instance=device, + element_uid=multi_level_switch.element_uid, + ) + for gateway in hass.data[DOMAIN][entry.entry_id]["gateways"] + for device in gateway.multi_level_switch_devices + for multi_level_switch in device.multi_level_switch_property.values() + if multi_level_switch.switch_type == "dimmer" + ) class DevoloLightDeviceEntity(DevoloMultiLevelSwitchDeviceEntity, LightEntity): diff --git a/homeassistant/components/devolo_home_control/sensor.py b/homeassistant/components/devolo_home_control/sensor.py index fa11424ae94..db630cf3532 100644 --- a/homeassistant/components/devolo_home_control/sensor.py +++ b/homeassistant/components/devolo_home_control/sensor.py @@ -1,4 +1,5 @@ """Platform for sensor integration.""" + from __future__ import annotations from devolo_home_control_api.devices.zwave import Zwave @@ -44,35 +45,36 @@ async def async_setup_entry( entities: list[SensorEntity] = [] for gateway in hass.data[DOMAIN][entry.entry_id]["gateways"]: - for device in gateway.multi_level_sensor_devices: - for multi_level_sensor in device.multi_level_sensor_property: - entities.append( - DevoloGenericMultiLevelDeviceEntity( - homecontrol=gateway, - device_instance=device, - element_uid=multi_level_sensor, - ) - ) - for device in gateway.devices.values(): - if hasattr(device, "consumption_property"): - for consumption in device.consumption_property: - for consumption_type in ("current", "total"): - entities.append( - DevoloConsumptionEntity( - homecontrol=gateway, - device_instance=device, - element_uid=consumption, - consumption=consumption_type, - ) - ) - if hasattr(device, "battery_level"): - entities.append( - DevoloBatteryEntity( - homecontrol=gateway, - device_instance=device, - element_uid=f"devolo.BatterySensor:{device.uid}", - ) - ) + entities.extend( + DevoloGenericMultiLevelDeviceEntity( + homecontrol=gateway, + device_instance=device, + element_uid=multi_level_sensor, + ) + for device in gateway.multi_level_sensor_devices + for multi_level_sensor in device.multi_level_sensor_property + ) + entities.extend( + DevoloConsumptionEntity( + homecontrol=gateway, + device_instance=device, + element_uid=consumption, + consumption=consumption_type, + ) + for device in gateway.devices.values() + if hasattr(device, "consumption_property") + for consumption in device.consumption_property + for consumption_type in ("current", "total") + ) + entities.extend( + DevoloBatteryEntity( + homecontrol=gateway, + device_instance=device, + element_uid=f"devolo.BatterySensor:{device.uid}", + ) + for device in gateway.devices.values() + if hasattr(device, "battery_level") + ) async_add_entities(entities) diff --git a/homeassistant/components/devolo_home_control/siren.py b/homeassistant/components/devolo_home_control/siren.py index 216ab4ab296..fd015860bbb 100644 --- a/homeassistant/components/devolo_home_control/siren.py +++ b/homeassistant/components/devolo_home_control/siren.py @@ -1,4 +1,5 @@ """Platform for siren integration.""" + from typing import Any from devolo_home_control_api.devices.zwave import Zwave @@ -17,21 +18,18 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Get all binary sensor and multi level sensor devices and setup them via config entry.""" - entities = [] - for gateway in hass.data[DOMAIN][entry.entry_id]["gateways"]: - for device in gateway.multi_level_switch_devices: - for multi_level_switch in device.multi_level_switch_property: - if multi_level_switch.startswith("devolo.SirenMultiLevelSwitch"): - entities.append( - DevoloSirenDeviceEntity( - homecontrol=gateway, - device_instance=device, - element_uid=multi_level_switch, - ) - ) - - async_add_entities(entities) + async_add_entities( + DevoloSirenDeviceEntity( + homecontrol=gateway, + device_instance=device, + element_uid=multi_level_switch, + ) + for gateway in hass.data[DOMAIN][entry.entry_id]["gateways"] + for device in gateway.multi_level_switch_devices + for multi_level_switch in device.multi_level_switch_property + if multi_level_switch.startswith("devolo.SirenMultiLevelSwitch") + ) class DevoloSirenDeviceEntity(DevoloMultiLevelSwitchDeviceEntity, SirenEntity): diff --git a/homeassistant/components/devolo_home_control/subscriber.py b/homeassistant/components/devolo_home_control/subscriber.py index 13ffabeaba2..99c21b3fd36 100644 --- a/homeassistant/components/devolo_home_control/subscriber.py +++ b/homeassistant/components/devolo_home_control/subscriber.py @@ -1,4 +1,5 @@ """Subscriber for devolo home control API publisher.""" + from collections.abc import Callable import logging diff --git a/homeassistant/components/devolo_home_control/switch.py b/homeassistant/components/devolo_home_control/switch.py index c442cc55763..f599d39d0b6 100644 --- a/homeassistant/components/devolo_home_control/switch.py +++ b/homeassistant/components/devolo_home_control/switch.py @@ -1,4 +1,5 @@ """Platform for switch integration.""" + from __future__ import annotations from typing import Any @@ -19,23 +20,20 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Get all devices and setup the switch devices via config entry.""" - entities = [] - for gateway in hass.data[DOMAIN][entry.entry_id]["gateways"]: - for device in gateway.binary_switch_devices: - for binary_switch in device.binary_switch_property: - # Exclude the binary switch which also has multi_level_switches here, - # because those are implemented as light entities now. - if not hasattr(device, "multi_level_switch_property"): - entities.append( - DevoloSwitch( - homecontrol=gateway, - device_instance=device, - element_uid=binary_switch, - ) - ) - - async_add_entities(entities) + async_add_entities( + DevoloSwitch( + homecontrol=gateway, + device_instance=device, + element_uid=binary_switch, + ) + for gateway in hass.data[DOMAIN][entry.entry_id]["gateways"] + for device in gateway.binary_switch_devices + for binary_switch in device.binary_switch_property + # Exclude the binary switch which also has multi_level_switches here, + # because those are implemented as light entities now. + if not hasattr(device, "multi_level_switch_property") + ) class DevoloSwitch(DevoloDeviceEntity, SwitchEntity): diff --git a/homeassistant/components/devolo_home_network/__init__.py b/homeassistant/components/devolo_home_network/__init__.py index 842d1bee40f..d96312be4e6 100644 --- a/homeassistant/components/devolo_home_network/__init__.py +++ b/homeassistant/components/devolo_home_network/__init__.py @@ -1,4 +1,5 @@ """The devolo Home Network integration.""" + from __future__ import annotations import logging @@ -48,9 +49,7 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -async def async_setup_entry( # noqa: C901 - hass: HomeAssistant, entry: ConfigEntry -) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up devolo Home Network from a config entry.""" hass.data.setdefault(DOMAIN, {}) zeroconf_instance = await zeroconf.async_get_async_instance(hass) @@ -68,7 +67,10 @@ async def async_setup_entry( # noqa: C901 ) except DeviceNotFound as err: raise ConfigEntryNotReady( - f"Unable to connect to {entry.data[CONF_IP_ADDRESS]}" + f"Unable to connect to {entry.data[CONF_IP_ADDRESS]}", + translation_domain=DOMAIN, + translation_key="connection_failed", + translation_placeholders={"ip_address": entry.data[CONF_IP_ADDRESS]}, ) from err hass.data[DOMAIN][entry.entry_id] = {"device": device} @@ -100,7 +102,9 @@ async def async_setup_entry( # noqa: C901 except DeviceUnavailable as err: raise UpdateFailed(err) from err except DevicePasswordProtected as err: - raise ConfigEntryAuthFailed(err) from err + raise ConfigEntryAuthFailed( + err, translation_domain=DOMAIN, translation_key="password_wrong" + ) from err async def async_update_led_status() -> bool: """Fetch data from API endpoint.""" diff --git a/homeassistant/components/devolo_home_network/binary_sensor.py b/homeassistant/components/devolo_home_network/binary_sensor.py index cf8358b69a3..6750fbc50d5 100644 --- a/homeassistant/components/devolo_home_network/binary_sensor.py +++ b/homeassistant/components/devolo_home_network/binary_sensor.py @@ -1,4 +1,5 @@ """Platform for binary sensor integration.""" + from __future__ import annotations from collections.abc import Callable @@ -32,20 +33,13 @@ def _is_connected_to_router(entity: DevoloBinarySensorEntity) -> bool: ) -@dataclass(frozen=True) -class DevoloBinarySensorRequiredKeysMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class DevoloBinarySensorEntityDescription(BinarySensorEntityDescription): + """Describes devolo sensor entity.""" value_func: Callable[[DevoloBinarySensorEntity], bool] -@dataclass(frozen=True) -class DevoloBinarySensorEntityDescription( - BinarySensorEntityDescription, DevoloBinarySensorRequiredKeysMixin -): - """Describes devolo sensor entity.""" - - SENSOR_TYPES: dict[str, DevoloBinarySensorEntityDescription] = { CONNECTED_TO_ROUTER: DevoloBinarySensorEntityDescription( key=CONNECTED_TO_ROUTER, diff --git a/homeassistant/components/devolo_home_network/button.py b/homeassistant/components/devolo_home_network/button.py index eba1ad05157..1dcdc007189 100644 --- a/homeassistant/components/devolo_home_network/button.py +++ b/homeassistant/components/devolo_home_network/button.py @@ -1,4 +1,5 @@ """Platform for button integration.""" + from __future__ import annotations from collections.abc import Awaitable, Callable @@ -22,20 +23,13 @@ from .const import DOMAIN, IDENTIFY, PAIRING, RESTART, START_WPS from .entity import DevoloEntity -@dataclass(frozen=True) -class DevoloButtonRequiredKeysMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class DevoloButtonEntityDescription(ButtonEntityDescription): + """Describes devolo button entity.""" press_func: Callable[[Device], Awaitable[bool]] -@dataclass(frozen=True) -class DevoloButtonEntityDescription( - ButtonEntityDescription, DevoloButtonRequiredKeysMixin -): - """Describes devolo button entity.""" - - BUTTON_TYPES: dict[str, DevoloButtonEntityDescription] = { IDENTIFY: DevoloButtonEntityDescription( key=IDENTIFY, @@ -123,9 +117,13 @@ class DevoloButtonEntity(DevoloEntity, ButtonEntity): except DevicePasswordProtected as ex: self.entry.async_start_reauth(self.hass) raise HomeAssistantError( - f"Device {self.entry.title} require re-authenticatication to set or change the password" + translation_domain=DOMAIN, + translation_key="password_protected", + translation_placeholders={"title": self.entry.title}, ) from ex except DeviceUnavailable as ex: raise HomeAssistantError( - f"Device {self.entry.title} did not respond" + translation_domain=DOMAIN, + translation_key="no_response", + translation_placeholders={"title": self.entry.title}, ) from ex diff --git a/homeassistant/components/devolo_home_network/config_flow.py b/homeassistant/components/devolo_home_network/config_flow.py index 08892e19e4e..a53211aa479 100644 --- a/homeassistant/components/devolo_home_network/config_flow.py +++ b/homeassistant/components/devolo_home_network/config_flow.py @@ -1,4 +1,5 @@ """Config flow for devolo Home Network integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -9,10 +10,10 @@ from devolo_plc_api.device import Device from devolo_plc_api.exceptions.device import DeviceNotFound import voluptuous as vol -from homeassistant import config_entries, core from homeassistant.components import zeroconf +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_IP_ADDRESS, CONF_NAME, CONF_PASSWORD -from homeassistant.data_entry_flow import FlowResult +from homeassistant.core import HomeAssistant from homeassistant.helpers.httpx_client import get_async_client from .const import DOMAIN, PRODUCT, SERIAL_NUMBER, TITLE @@ -23,9 +24,7 @@ STEP_USER_DATA_SCHEMA = vol.Schema({vol.Required(CONF_IP_ADDRESS): str}) STEP_REAUTH_DATA_SCHEMA = vol.Schema({vol.Optional(CONF_PASSWORD): str}) -async def validate_input( - hass: core.HomeAssistant, data: dict[str, Any] -) -> dict[str, str]: +async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, str]: """Validate the user input allows us to connect. Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user. @@ -44,14 +43,14 @@ async def validate_input( } -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class DevoloHomeNetworkConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for devolo Home Network.""" VERSION = 1 async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors: dict = {} @@ -79,7 +78,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_zeroconf( self, discovery_info: zeroconf.ZeroconfServiceInfo - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle zeroconf discovery.""" if discovery_info.properties["MT"] in ["2600", "2601"]: return self.async_abort(reason="home_control") @@ -99,7 +98,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_zeroconf_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initiated by zeroconf.""" title = self.context["title_placeholders"][CONF_NAME] if user_input is not None: @@ -113,7 +112,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): description_placeholders={"host_name": title}, ) - async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth(self, data: Mapping[str, Any]) -> ConfigFlowResult: """Handle reauthentication.""" self.context[CONF_HOST] = data[CONF_IP_ADDRESS] self.context["title_placeholders"][PRODUCT] = self.hass.data[DOMAIN][ @@ -123,7 +122,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initiated by reauthentication.""" if user_input is None: return self.async_show_form( diff --git a/homeassistant/components/devolo_home_network/device_tracker.py b/homeassistant/components/devolo_home_network/device_tracker.py index c73e08abed2..f97a4c36400 100644 --- a/homeassistant/components/devolo_home_network/device_tracker.py +++ b/homeassistant/components/devolo_home_network/device_tracker.py @@ -1,4 +1,5 @@ """Platform for device tracker integration.""" + from __future__ import annotations from devolo_plc_api.device import Device @@ -27,9 +28,9 @@ async def async_setup_entry( ) -> 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[list[ConnectedStationInfo]] - ] = hass.data[DOMAIN][entry.entry_id]["coordinators"] + coordinators: dict[str, DataUpdateCoordinator[list[ConnectedStationInfo]]] = ( + hass.data[DOMAIN][entry.entry_id]["coordinators"] + ) registry = er.async_get(hass) tracked = set() diff --git a/homeassistant/components/devolo_home_network/diagnostics.py b/homeassistant/components/devolo_home_network/diagnostics.py index bd4393d73dd..17d65fd26b2 100644 --- a/homeassistant/components/devolo_home_network/diagnostics.py +++ b/homeassistant/components/devolo_home_network/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for devolo Home Network.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/devolo_home_network/entity.py b/homeassistant/components/devolo_home_network/entity.py index d6ddf661494..a6159d7b948 100644 --- a/homeassistant/components/devolo_home_network/entity.py +++ b/homeassistant/components/devolo_home_network/entity.py @@ -1,4 +1,5 @@ """Generic platform.""" + from __future__ import annotations from typing import TypeVar diff --git a/homeassistant/components/devolo_home_network/image.py b/homeassistant/components/devolo_home_network/image.py index 72cf4f57c1d..71d27b18d0c 100644 --- a/homeassistant/components/devolo_home_network/image.py +++ b/homeassistant/components/devolo_home_network/image.py @@ -1,4 +1,5 @@ """Platform for image integration.""" + from __future__ import annotations from collections.abc import Callable @@ -21,20 +22,13 @@ from .const import DOMAIN, IMAGE_GUEST_WIFI, SWITCH_GUEST_WIFI from .entity import DevoloCoordinatorEntity -@dataclass(frozen=True) -class DevoloImageRequiredKeysMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class DevoloImageEntityDescription(ImageEntityDescription): + """Describes devolo image entity.""" image_func: Callable[[WifiGuestAccessGet], bytes] -@dataclass(frozen=True) -class DevoloImageEntityDescription( - ImageEntityDescription, DevoloImageRequiredKeysMixin -): - """Describes devolo image entity.""" - - IMAGE_TYPES: dict[str, DevoloImageEntityDescription] = { IMAGE_GUEST_WIFI: DevoloImageEntityDescription( key=IMAGE_GUEST_WIFI, diff --git a/homeassistant/components/devolo_home_network/sensor.py b/homeassistant/components/devolo_home_network/sensor.py index 750bb9ad13d..cc682d8f694 100644 --- a/homeassistant/components/devolo_home_network/sensor.py +++ b/homeassistant/components/devolo_home_network/sensor.py @@ -1,4 +1,5 @@ """Platform for sensor integration.""" + from __future__ import annotations from collections.abc import Callable @@ -49,19 +50,14 @@ class DataRateDirection(StrEnum): TX = "tx_rate" -@dataclass(frozen=True) -class DevoloSensorRequiredKeysMixin(Generic[_CoordinatorDataT]): - """Mixin for required keys.""" - - value_func: Callable[[_CoordinatorDataT], float] - - -@dataclass(frozen=True) +@dataclass(frozen=True, kw_only=True) class DevoloSensorEntityDescription( - SensorEntityDescription, DevoloSensorRequiredKeysMixin[_CoordinatorDataT] + SensorEntityDescription, Generic[_CoordinatorDataT] ): """Describes devolo sensor entity.""" + value_func: Callable[[_CoordinatorDataT], float] + SENSOR_TYPES: dict[str, DevoloSensorEntityDescription[Any]] = { CONNECTED_PLC_DEVICES: DevoloSensorEntityDescription[LogicalNetwork]( diff --git a/homeassistant/components/devolo_home_network/strings.json b/homeassistant/components/devolo_home_network/strings.json index 1362417c125..9d86b127d77 100644 --- a/homeassistant/components/devolo_home_network/strings.json +++ b/homeassistant/components/devolo_home_network/strings.json @@ -78,5 +78,19 @@ "name": "Enable LEDs" } } + }, + "exceptions": { + "connection_failed": { + "message": "Unable to connect to {ip_address}" + }, + "no_response": { + "message": "Device {title} did not respond" + }, + "password_protected": { + "message": "Device {title} requires re-authenticatication to set or change the password" + }, + "password_wrong": { + "message": "The used password is wrong" + } } } diff --git a/homeassistant/components/devolo_home_network/switch.py b/homeassistant/components/devolo_home_network/switch.py index af0569a016f..2a9775257a8 100644 --- a/homeassistant/components/devolo_home_network/switch.py +++ b/homeassistant/components/devolo_home_network/switch.py @@ -1,4 +1,5 @@ """Platform for switch integration.""" + from __future__ import annotations from collections.abc import Awaitable, Callable @@ -23,22 +24,15 @@ from .entity import DevoloCoordinatorEntity _DataT = TypeVar("_DataT", bound=WifiGuestAccessGet | bool) -@dataclass(frozen=True) -class DevoloSwitchRequiredKeysMixin(Generic[_DataT]): - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class DevoloSwitchEntityDescription(SwitchEntityDescription, Generic[_DataT]): + """Describes devolo switch entity.""" is_on_func: Callable[[_DataT], bool] turn_on_func: Callable[[Device], Awaitable[bool]] turn_off_func: Callable[[Device], Awaitable[bool]] -@dataclass(frozen=True) -class DevoloSwitchEntityDescription( - SwitchEntityDescription, DevoloSwitchRequiredKeysMixin[_DataT] -): - """Describes devolo switch entity.""" - - SWITCH_TYPES: dict[str, DevoloSwitchEntityDescription[Any]] = { SWITCH_GUEST_WIFI: DevoloSwitchEntityDescription[WifiGuestAccessGet]( key=SWITCH_GUEST_WIFI, @@ -115,7 +109,9 @@ class DevoloSwitchEntity(DevoloCoordinatorEntity[_DataT], SwitchEntity): except DevicePasswordProtected as ex: self.entry.async_start_reauth(self.hass) raise HomeAssistantError( - f"Device {self.entry.title} require re-authenticatication to set or change the password" + translation_domain=DOMAIN, + translation_key="password_protected", + translation_placeholders={"title": self.entry.title}, ) from ex except DeviceUnavailable: pass # The coordinator will handle this @@ -128,7 +124,9 @@ class DevoloSwitchEntity(DevoloCoordinatorEntity[_DataT], SwitchEntity): except DevicePasswordProtected as ex: self.entry.async_start_reauth(self.hass) raise HomeAssistantError( - f"Device {self.entry.title} require re-authenticatication to set or change the password" + translation_domain=DOMAIN, + translation_key="password_protected", + translation_placeholders={"title": self.entry.title}, ) from ex except DeviceUnavailable: pass # The coordinator will handle this diff --git a/homeassistant/components/devolo_home_network/update.py b/homeassistant/components/devolo_home_network/update.py index 03f86381307..75fc1b7b99c 100644 --- a/homeassistant/components/devolo_home_network/update.py +++ b/homeassistant/components/devolo_home_network/update.py @@ -1,4 +1,5 @@ """Platform for update integration.""" + from __future__ import annotations from collections.abc import Awaitable, Callable @@ -26,21 +27,14 @@ from .const import DOMAIN, REGULAR_FIRMWARE from .entity import DevoloCoordinatorEntity -@dataclass(frozen=True) -class DevoloUpdateRequiredKeysMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class DevoloUpdateEntityDescription(UpdateEntityDescription): + """Describes devolo update entity.""" latest_version: Callable[[UpdateFirmwareCheck], str] update_func: Callable[[Device], Awaitable[bool]] -@dataclass(frozen=True) -class DevoloUpdateEntityDescription( - UpdateEntityDescription, DevoloUpdateRequiredKeysMixin -): - """Describes devolo update entity.""" - - UPDATE_TYPES: dict[str, DevoloUpdateEntityDescription] = { REGULAR_FIRMWARE: DevoloUpdateEntityDescription( key=REGULAR_FIRMWARE, @@ -123,9 +117,13 @@ class DevoloUpdateEntity(DevoloCoordinatorEntity, UpdateEntity): except DevicePasswordProtected as ex: self.entry.async_start_reauth(self.hass) raise HomeAssistantError( - f"Device {self.entry.title} require re-authentication to set or change the password" + translation_domain=DOMAIN, + translation_key="password_protected", + translation_placeholders={"title": self.entry.title}, ) from ex except DeviceUnavailable as ex: raise HomeAssistantError( - f"Device {self.entry.title} did not respond" + translation_domain=DOMAIN, + translation_key="no_response", + translation_placeholders={"title": self.entry.title}, ) from ex diff --git a/homeassistant/components/dexcom/__init__.py b/homeassistant/components/dexcom/__init__.py index 2d7c2120758..5ff95fae47e 100644 --- a/homeassistant/components/dexcom/__init__.py +++ b/homeassistant/components/dexcom/__init__.py @@ -1,8 +1,9 @@ """The Dexcom integration.""" + from datetime import timedelta import logging -from pydexcom import AccountError, Dexcom, SessionError +from pydexcom import AccountError, Dexcom, GlucoseReading, SessionError from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_UNIT_OF_MEASUREMENT, CONF_USERNAME @@ -10,15 +11,7 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import ( - CONF_SERVER, - COORDINATOR, - DOMAIN, - MG_DL, - PLATFORMS, - SERVER_OUS, - UNDO_UPDATE_LISTENER, -) +from .const import CONF_SERVER, DOMAIN, MG_DL, PLATFORMS, SERVER_OUS _LOGGER = logging.getLogger(__name__) @@ -50,7 +43,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except SessionError as error: raise UpdateFailed(error) from error - coordinator = DataUpdateCoordinator( + coordinator = DataUpdateCoordinator[GlucoseReading]( hass, _LOGGER, name=DOMAIN, @@ -59,11 +52,9 @@ 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] = { - COORDINATOR: coordinator, - UNDO_UPDATE_LISTENER: entry.add_update_listener(update_listener), - } + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + + entry.async_on_unload(entry.add_update_listener(update_listener)) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) @@ -72,10 +63,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: 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) - hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENER]() - - if unload_ok: + 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/dexcom/config_flow.py b/homeassistant/components/dexcom/config_flow.py index 6ccb09881af..48cdcd99439 100644 --- a/homeassistant/components/dexcom/config_flow.py +++ b/homeassistant/components/dexcom/config_flow.py @@ -1,10 +1,11 @@ """Config flow for Dexcom integration.""" + from __future__ import annotations from pydexcom import AccountError, Dexcom, SessionError import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow from homeassistant.const import CONF_PASSWORD, CONF_UNIT_OF_MEASUREMENT, CONF_USERNAME from homeassistant.core import callback @@ -19,7 +20,7 @@ DATA_SCHEMA = vol.Schema( ) -class DexcomConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class DexcomConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Dexcom.""" VERSION = 1 @@ -56,16 +57,16 @@ class DexcomConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback def async_get_options_flow( - config_entry: config_entries.ConfigEntry, + config_entry: ConfigEntry, ) -> DexcomOptionsFlowHandler: """Get the options flow for this handler.""" return DexcomOptionsFlowHandler(config_entry) -class DexcomOptionsFlowHandler(config_entries.OptionsFlow): +class DexcomOptionsFlowHandler(OptionsFlow): """Handle a option flow for Dexcom.""" - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + def __init__(self, config_entry: ConfigEntry) -> None: """Initialize options flow.""" self.config_entry = config_entry diff --git a/homeassistant/components/dexcom/const.py b/homeassistant/components/dexcom/const.py index 8712eeb10ad..487a844eb2b 100644 --- a/homeassistant/components/dexcom/const.py +++ b/homeassistant/components/dexcom/const.py @@ -1,22 +1,10 @@ """Constants for the Dexcom integration.""" + from homeassistant.const import Platform DOMAIN = "dexcom" PLATFORMS = [Platform.SENSOR] -GLUCOSE_TREND_ICON = [ - "mdi:help", - "mdi:arrow-up-thick", - "mdi:arrow-up", - "mdi:arrow-top-right", - "mdi:arrow-right", - "mdi:arrow-bottom-right", - "mdi:arrow-down", - "mdi:arrow-down-thick", - "mdi:help", - "mdi:alert-circle-outline", -] - MMOL_L = "mmol/L" MG_DL = "mg/dL" @@ -24,6 +12,3 @@ CONF_SERVER = "server" SERVER_OUS = "EU" SERVER_US = "US" - -COORDINATOR = "coordinator" -UNDO_UPDATE_LISTENER = "undo_update_listener" diff --git a/homeassistant/components/dexcom/icons.json b/homeassistant/components/dexcom/icons.json index 9d0b3534e17..de8355ce861 100644 --- a/homeassistant/components/dexcom/icons.json +++ b/homeassistant/components/dexcom/icons.json @@ -3,6 +3,18 @@ "sensor": { "glucose_value": { "default": "mdi:diabetes" + }, + "glucose_trend": { + "default": "mdi:help", + "state": { + "rising_quickly": "mdi:arrow-up-thick", + "rising": "mdi:arrow-up", + "rising_slightly": "mdi:arrow-top-right", + "steady": "mdi:arrow-right", + "falling_slightly": "mdi:arrow-bottom-right", + "falling": "mdi:arrow-down", + "falling_quickly": "mdi:arrow-down-thick" + } } } } diff --git a/homeassistant/components/dexcom/sensor.py b/homeassistant/components/dexcom/sensor.py index 592419abc1b..10b30f39fcb 100644 --- a/homeassistant/components/dexcom/sensor.py +++ b/homeassistant/components/dexcom/sensor.py @@ -1,7 +1,10 @@ """Support for Dexcom sensors.""" + from __future__ import annotations -from homeassistant.components.sensor import SensorEntity +from pydexcom import GlucoseReading + +from homeassistant.components.sensor import SensorDeviceClass, SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_UNIT_OF_MEASUREMENT, CONF_USERNAME from homeassistant.core import HomeAssistant @@ -12,7 +15,17 @@ from homeassistant.helpers.update_coordinator import ( DataUpdateCoordinator, ) -from .const import COORDINATOR, DOMAIN, GLUCOSE_TREND_ICON, MG_DL +from .const import DOMAIN, MG_DL + +TRENDS = { + 1: "rising_quickly", + 2: "rising", + 3: "rising_slightly", + 4: "steady", + 5: "falling_slightly", + 6: "falling", + 7: "falling_quickly", +} async def async_setup_entry( @@ -21,7 +34,7 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Dexcom sensors.""" - coordinator = hass.data[DOMAIN][config_entry.entry_id][COORDINATOR] + coordinator = hass.data[DOMAIN][config_entry.entry_id] username = config_entry.data[CONF_USERNAME] unit_of_measurement = config_entry.options[CONF_UNIT_OF_MEASUREMENT] async_add_entities( @@ -31,17 +44,22 @@ async def async_setup_entry( coordinator, username, config_entry.entry_id, unit_of_measurement ), ], - False, ) -class DexcomSensorEntity(CoordinatorEntity, SensorEntity): +class DexcomSensorEntity( + CoordinatorEntity[DataUpdateCoordinator[GlucoseReading]], SensorEntity +): """Base Dexcom sensor entity.""" _attr_has_entity_name = True def __init__( - self, coordinator: DataUpdateCoordinator, username: str, entry_id: str, key: str + self, + coordinator: DataUpdateCoordinator[GlucoseReading], + username: str, + entry_id: str, + key: str, ) -> None: """Initialize the sensor.""" super().__init__(coordinator) @@ -81,6 +99,8 @@ class DexcomGlucoseTrendSensor(DexcomSensorEntity): """Representation of a Dexcom glucose trend sensor.""" _attr_translation_key = "glucose_trend" + _attr_device_class = SensorDeviceClass.ENUM + _attr_options = list(TRENDS.values()) def __init__( self, coordinator: DataUpdateCoordinator, username: str, entry_id: str @@ -89,15 +109,15 @@ class DexcomGlucoseTrendSensor(DexcomSensorEntity): super().__init__(coordinator, username, entry_id, "trend") @property - def icon(self): - """Return the icon for the frontend.""" - if self.coordinator.data: - return GLUCOSE_TREND_ICON[self.coordinator.data.trend] - return GLUCOSE_TREND_ICON[0] - - @property - def native_value(self): + def native_value(self) -> str | None: """Return the state of the sensor.""" if self.coordinator.data: - return self.coordinator.data.trend_description + return TRENDS.get(self.coordinator.data.trend) return None + + @property + def available(self) -> bool: + """Return if entity is available.""" + return super().available and ( + self.coordinator.data is None or self.coordinator.data.trend != 9 + ) diff --git a/homeassistant/components/dexcom/strings.json b/homeassistant/components/dexcom/strings.json index 7efc2708bcc..91b5725d918 100644 --- a/homeassistant/components/dexcom/strings.json +++ b/homeassistant/components/dexcom/strings.json @@ -35,7 +35,16 @@ "name": "Glucose value" }, "glucose_trend": { - "name": "Glucose trend" + "name": "Glucose trend", + "state": { + "rising_quickly": "Rising quickly", + "rising": "Rising", + "rising_slightly": "Rising slightly", + "steady": "Steady", + "falling_slightly": "Falling slightly", + "falling": "Falling", + "falling_quickly": "Falling quickly" + } } } } diff --git a/homeassistant/components/dhcp/__init__.py b/homeassistant/components/dhcp/__init__.py index ebd0629950e..050bc7a74b2 100644 --- a/homeassistant/components/dhcp/__init__.py +++ b/homeassistant/components/dhcp/__init__.py @@ -1,7 +1,7 @@ """The dhcp integration.""" + from __future__ import annotations -from abc import ABC, abstractmethod import asyncio from collections.abc import Callable from dataclasses import dataclass @@ -52,7 +52,7 @@ from homeassistant.helpers.event import ( async_track_state_added_domain, async_track_time_interval, ) -from homeassistant.helpers.typing import ConfigType, EventType +from homeassistant.helpers.typing import ConfigType from homeassistant.loader import DHCPMatcher, async_get_dhcp from .const import DOMAIN @@ -131,31 +131,43 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # For the passive classes we need to start listening # for state changes and connect the dispatchers before # everything else starts up or we will miss events - for passive_cls in (DeviceTrackerRegisteredWatcher, DeviceTrackerWatcher): - passive_watcher = passive_cls(hass, address_data, integration_matchers) - passive_watcher.async_start() - watchers.append(passive_watcher) + device_watcher = DeviceTrackerWatcher(hass, address_data, integration_matchers) + device_watcher.async_start() + watchers.append(device_watcher) + + device_tracker_registered_watcher = DeviceTrackerRegisteredWatcher( + hass, address_data, integration_matchers + ) + device_tracker_registered_watcher.async_start() + watchers.append(device_tracker_registered_watcher) async def _async_initialize(event: Event) -> None: await aiodhcpwatcher.async_init() - for active_cls in (DHCPWatcher, NetworkWatcher): - active_watcher = active_cls(hass, address_data, integration_matchers) - active_watcher.async_start() - watchers.append(active_watcher) + network_watcher = NetworkWatcher(hass, address_data, integration_matchers) + network_watcher.async_start() + watchers.append(network_watcher) + + dhcp_watcher = DHCPWatcher(hass, address_data, integration_matchers) + await dhcp_watcher.async_start() + watchers.append(dhcp_watcher) @callback def _async_stop(event: Event) -> None: for watcher in watchers: watcher.async_stop() - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_stop) + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, _async_stop, run_immediately=True + ) - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, _async_initialize) + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STARTED, _async_initialize, run_immediately=True + ) return True -class WatcherBase(ABC): +class WatcherBase: """Base class for dhcp and device tracker watching.""" def __init__( @@ -179,11 +191,6 @@ class WatcherBase(ABC): self._unsub() self._unsub = None - @abstractmethod - @callback - def async_start(self) -> None: - """Start the watcher.""" - @callback def async_process_client( self, ip_address: str, hostname: str, unformatted_mac_address: str @@ -316,7 +323,9 @@ class NetworkWatcher(WatcherBase): """Start a new discovery task if one is not running.""" if self._discover_task and not self._discover_task.done(): return - self._discover_task = self.hass.async_create_task(self.async_discover()) + self._discover_task = self.hass.async_create_background_task( + self.async_discover(), name="dhcp discovery", eager_start=True + ) async def async_discover(self) -> None: """Process discovery.""" @@ -342,9 +351,7 @@ class DeviceTrackerWatcher(WatcherBase): self._async_process_device_state(state) @callback - def _async_process_device_event( - self, event: EventType[EventStateChangedData] - ) -> None: + def _async_process_device_event(self, event: Event[EventStateChangedData]) -> None: """Process a device tracker state change event.""" self._async_process_device_state(event.data["new_state"]) @@ -402,10 +409,9 @@ class DHCPWatcher(WatcherBase): response.ip_address, response.hostname, response.mac_address ) - @callback - def async_start(self) -> None: + async def async_start(self) -> None: """Start watching for dhcp packets.""" - self._unsub = aiodhcpwatcher.start(self._async_process_dhcp_request) + self._unsub = await aiodhcpwatcher.async_start(self._async_process_dhcp_request) @lru_cache(maxsize=4096, typed=True) diff --git a/homeassistant/components/dhcp/manifest.json b/homeassistant/components/dhcp/manifest.json index 673a80b8d9f..0d77b997e82 100644 --- a/homeassistant/components/dhcp/manifest.json +++ b/homeassistant/components/dhcp/manifest.json @@ -3,7 +3,6 @@ "name": "DHCP Discovery", "codeowners": ["@bdraco"], "documentation": "https://www.home-assistant.io/integrations/dhcp", - "import_executor": true, "integration_type": "system", "iot_class": "local_push", "loggers": [ @@ -15,8 +14,8 @@ ], "quality_scale": "internal", "requirements": [ - "aiodhcpwatcher==0.8.2", - "aiodiscover==1.6.1", + "aiodhcpwatcher==1.0.0", + "aiodiscover==2.0.0", "cached_ipaddress==0.3.0" ] } diff --git a/homeassistant/components/diagnostics/__init__.py b/homeassistant/components/diagnostics/__init__.py index 679efd137ce..5ada7713c33 100644 --- a/homeassistant/components/diagnostics/__init__.py +++ b/homeassistant/components/diagnostics/__init__.py @@ -1,4 +1,5 @@ """The Diagnostics integration.""" + from __future__ import annotations from collections.abc import Callable, Coroutine, Mapping @@ -40,13 +41,17 @@ CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN) class DiagnosticsPlatformData: """Diagnostic platform data.""" - config_entry_diagnostics: Callable[ - [HomeAssistant, ConfigEntry], Coroutine[Any, Any, Mapping[str, Any]] - ] | None - device_diagnostics: Callable[ - [HomeAssistant, ConfigEntry, DeviceEntry], - Coroutine[Any, Any, Mapping[str, Any]], - ] | None + config_entry_diagnostics: ( + Callable[[HomeAssistant, ConfigEntry], Coroutine[Any, Any, Mapping[str, Any]]] + | None + ) + device_diagnostics: ( + Callable[ + [HomeAssistant, ConfigEntry, DeviceEntry], + Coroutine[Any, Any, Mapping[str, Any]], + ] + | None + ) @dataclass(slots=True) @@ -232,7 +237,7 @@ class DownloadDiagnosticsView(http.HomeAssistantView): device_diagnostics = sub_type is not None - hass: HomeAssistant = request.app["hass"] + hass = request.app[http.KEY_HASS] if (config_entry := hass.config_entries.async_get_entry(d_id)) is None: return web.Response(status=HTTPStatus.NOT_FOUND) diff --git a/homeassistant/components/diagnostics/const.py b/homeassistant/components/diagnostics/const.py index 20f97be1eb1..11042c3b7dc 100644 --- a/homeassistant/components/diagnostics/const.py +++ b/homeassistant/components/diagnostics/const.py @@ -1,4 +1,5 @@ """Constants for the Diagnostics integration.""" + from enum import StrEnum DOMAIN = "diagnostics" diff --git a/homeassistant/components/diagnostics/util.py b/homeassistant/components/diagnostics/util.py index 47a0eac9a0d..9b33b33f1ed 100644 --- a/homeassistant/components/diagnostics/util.py +++ b/homeassistant/components/diagnostics/util.py @@ -1,4 +1,5 @@ """Diagnostic utilities.""" + from __future__ import annotations from collections.abc import Iterable, Mapping @@ -17,8 +18,7 @@ def async_redact_data(data: Mapping, to_redact: Iterable[Any]) -> dict: # type: @overload -def async_redact_data(data: _T, to_redact: Iterable[Any]) -> _T: - ... +def async_redact_data(data: _T, to_redact: Iterable[Any]) -> _T: ... @callback diff --git a/homeassistant/components/dialogflow/__init__.py b/homeassistant/components/dialogflow/__init__.py index d34b27233be..95c8861d665 100644 --- a/homeassistant/components/dialogflow/__init__.py +++ b/homeassistant/components/dialogflow/__init__.py @@ -1,4 +1,5 @@ """Support for Dialogflow webhook.""" + import logging from aiohttp import web diff --git a/homeassistant/components/dialogflow/config_flow.py b/homeassistant/components/dialogflow/config_flow.py index 7e62869c3fa..9833e7d1c18 100644 --- a/homeassistant/components/dialogflow/config_flow.py +++ b/homeassistant/components/dialogflow/config_flow.py @@ -1,4 +1,5 @@ """Config flow for DialogFlow.""" + from homeassistant.helpers import config_entry_flow from .const import DOMAIN diff --git a/homeassistant/components/digital_ocean/__init__.py b/homeassistant/components/digital_ocean/__init__.py index ed951fa2aa4..e5b62d430b6 100644 --- a/homeassistant/components/digital_ocean/__init__.py +++ b/homeassistant/components/digital_ocean/__init__.py @@ -1,4 +1,5 @@ """Support for Digital Ocean.""" + from datetime import timedelta import logging diff --git a/homeassistant/components/digital_ocean/binary_sensor.py b/homeassistant/components/digital_ocean/binary_sensor.py index e2bd09ba15e..9218d9bde0e 100644 --- a/homeassistant/components/digital_ocean/binary_sensor.py +++ b/homeassistant/components/digital_ocean/binary_sensor.py @@ -1,4 +1,5 @@ """Support for monitoring the state of Digital Ocean droplets.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/digital_ocean/switch.py b/homeassistant/components/digital_ocean/switch.py index b226dbab0a9..a01965e3667 100644 --- a/homeassistant/components/digital_ocean/switch.py +++ b/homeassistant/components/digital_ocean/switch.py @@ -1,4 +1,5 @@ """Support for interacting with Digital Ocean droplets.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/directv/__init__.py b/homeassistant/components/directv/__init__.py index 3dfb5708b98..50eb6bc7959 100644 --- a/homeassistant/components/directv/__init__.py +++ b/homeassistant/components/directv/__init__.py @@ -1,4 +1,5 @@ """The DirecTV integration.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/directv/config_flow.py b/homeassistant/components/directv/config_flow.py index b3209638012..f1289119f2b 100644 --- a/homeassistant/components/directv/config_flow.py +++ b/homeassistant/components/directv/config_flow.py @@ -1,4 +1,5 @@ """Config flow for DirecTV.""" + from __future__ import annotations import logging @@ -9,10 +10,9 @@ from directv import DIRECTV, DIRECTVError import voluptuous as vol from homeassistant.components import ssdp -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_NAME from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import CONF_RECEIVER_ID, DOMAIN @@ -46,7 +46,7 @@ class DirecTVConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initiated by the user.""" if user_input is None: return self._show_setup_form() @@ -66,7 +66,9 @@ class DirecTVConfigFlow(ConfigFlow, domain=DOMAIN): return self.async_create_entry(title=user_input[CONF_HOST], data=user_input) - async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: + async def async_step_ssdp( + self, discovery_info: ssdp.SsdpServiceInfo + ) -> ConfigFlowResult: """Handle SSDP discovery.""" host = urlparse(discovery_info.ssdp_location).hostname receiver_id = None @@ -101,7 +103,7 @@ class DirecTVConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_ssdp_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a confirmation flow initiated by SSDP.""" if user_input is None: return self.async_show_form( @@ -115,7 +117,7 @@ class DirecTVConfigFlow(ConfigFlow, domain=DOMAIN): data=self.discovery_info, ) - def _show_setup_form(self, errors: dict | None = None) -> FlowResult: + def _show_setup_form(self, errors: dict | None = None) -> ConfigFlowResult: """Show the setup form to the user.""" return self.async_show_form( step_id="user", diff --git a/homeassistant/components/directv/const.py b/homeassistant/components/directv/const.py index e90fd6879c7..fe9aba3b4f4 100644 --- a/homeassistant/components/directv/const.py +++ b/homeassistant/components/directv/const.py @@ -1,4 +1,5 @@ """Constants for the DirecTV integration.""" + DOMAIN = "directv" # Attributes diff --git a/homeassistant/components/directv/entity.py b/homeassistant/components/directv/entity.py index 0da2cfcb9d6..45a3c59991d 100644 --- a/homeassistant/components/directv/entity.py +++ b/homeassistant/components/directv/entity.py @@ -1,4 +1,5 @@ """Base DirecTV Entity.""" + from __future__ import annotations from directv import DIRECTV diff --git a/homeassistant/components/directv/media_player.py b/homeassistant/components/directv/media_player.py index 63d086564ee..6c4a40598de 100644 --- a/homeassistant/components/directv/media_player.py +++ b/homeassistant/components/directv/media_player.py @@ -1,4 +1,5 @@ """Support for the DirecTV receivers.""" + from __future__ import annotations import logging @@ -59,18 +60,18 @@ async def async_setup_entry( ) -> None: """Set up the DirecTV config entry.""" dtv = hass.data[DOMAIN][entry.entry_id] - entities = [] - for location in dtv.device.locations: - entities.append( + async_add_entities( + ( DIRECTVMediaPlayer( dtv=dtv, name=str.title(location.name), address=location.address, ) - ) - - async_add_entities(entities, True) + for location in dtv.device.locations + ), + True, + ) class DIRECTVMediaPlayer(DIRECTVEntity, MediaPlayerEntity): @@ -277,7 +278,7 @@ class DIRECTVMediaPlayer(DIRECTVEntity, MediaPlayerEntity): async def async_turn_on(self) -> None: """Turn on the receiver.""" if self._is_client: - raise NotImplementedError() + raise NotImplementedError _LOGGER.debug("Turn on %s", self.name) await self.dtv.remote("poweron", self._address) @@ -285,7 +286,7 @@ class DIRECTVMediaPlayer(DIRECTVEntity, MediaPlayerEntity): async def async_turn_off(self) -> None: """Turn off the receiver.""" if self._is_client: - raise NotImplementedError() + raise NotImplementedError _LOGGER.debug("Turn off %s", self.name) await self.dtv.remote("poweroff", self._address) diff --git a/homeassistant/components/directv/remote.py b/homeassistant/components/directv/remote.py index d100abd3495..5a77d90bd3c 100644 --- a/homeassistant/components/directv/remote.py +++ b/homeassistant/components/directv/remote.py @@ -1,4 +1,5 @@ """Support for the DIRECTV remote.""" + from __future__ import annotations from collections.abc import Iterable @@ -28,18 +29,18 @@ async def async_setup_entry( ) -> None: """Load DirecTV remote based on a config entry.""" dtv = hass.data[DOMAIN][entry.entry_id] - entities = [] - for location in dtv.device.locations: - entities.append( + async_add_entities( + ( DIRECTVRemote( dtv=dtv, name=str.title(location.name), address=location.address, ) - ) - - async_add_entities(entities, True) + for location in dtv.device.locations + ), + True, + ) class DIRECTVRemote(DIRECTVEntity, RemoteEntity): diff --git a/homeassistant/components/discogs/sensor.py b/homeassistant/components/discogs/sensor.py index e6a46ac535f..4a732130485 100644 --- a/homeassistant/components/discogs/sensor.py +++ b/homeassistant/components/discogs/sensor.py @@ -1,4 +1,5 @@ """Show the amount of records in a user's Discogs collection.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/discord/__init__.py b/homeassistant/components/discord/__init__.py index 329709e88d2..8c1e80527f8 100644 --- a/homeassistant/components/discord/__init__.py +++ b/homeassistant/components/discord/__init__.py @@ -1,4 +1,5 @@ """The discord integration.""" + from aiohttp.client_exceptions import ClientConnectorError import nextcord diff --git a/homeassistant/components/discord/config_flow.py b/homeassistant/components/discord/config_flow.py index 14c2cc6c040..a2747c1d803 100644 --- a/homeassistant/components/discord/config_flow.py +++ b/homeassistant/components/discord/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Discord integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -9,9 +10,8 @@ from aiohttp.client_exceptions import ClientConnectorError import nextcord import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_API_TOKEN, CONF_NAME -from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN, URL_PLACEHOLDER @@ -20,16 +20,18 @@ _LOGGER = logging.getLogger(__name__) CONFIG_SCHEMA = vol.Schema({vol.Required(CONF_API_TOKEN): str}) -class DiscordFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class DiscordFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a config flow for Discord.""" - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Handle a reauthorization flow request.""" return await self.async_step_reauth_confirm() async def async_step_reauth_confirm( self, user_input: dict[str, str] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm reauth dialog.""" errors = {} @@ -52,7 +54,7 @@ class DiscordFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, str] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initiated by the user.""" errors = {} diff --git a/homeassistant/components/discord/manifest.json b/homeassistant/components/discord/manifest.json index 78d4dc203e2..5f1ba2a13ef 100644 --- a/homeassistant/components/discord/manifest.json +++ b/homeassistant/components/discord/manifest.json @@ -4,7 +4,6 @@ "codeowners": ["@tkdrob"], "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/discord", - "import_executor": true, "integration_type": "service", "iot_class": "cloud_push", "loggers": ["discord"], diff --git a/homeassistant/components/discord/notify.py b/homeassistant/components/discord/notify.py index ff83d97f8c2..5f1e494c97e 100644 --- a/homeassistant/components/discord/notify.py +++ b/homeassistant/components/discord/notify.py @@ -1,4 +1,5 @@ """Discord platform for notify component.""" + from __future__ import annotations from io import BytesIO diff --git a/homeassistant/components/discovergy/__init__.py b/homeassistant/components/discovergy/__init__.py index 786f589bf7b..0d38182da5d 100644 --- a/homeassistant/components/discovergy/__init__.py +++ b/homeassistant/components/discovergy/__init__.py @@ -1,4 +1,5 @@ """The Discovergy integration.""" + from __future__ import annotations from pydiscovergy import Discovergy diff --git a/homeassistant/components/discovergy/config_flow.py b/homeassistant/components/discovergy/config_flow.py index d0e0c272d24..e47935764a8 100644 --- a/homeassistant/components/discovergy/config_flow.py +++ b/homeassistant/components/discovergy/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Discovergy integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -10,10 +11,8 @@ from pydiscovergy.authentication import BasicAuth import pydiscovergy.error as discovergyError import voluptuous as vol -from homeassistant import config_entries -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_EMAIL, CONF_PASSWORD -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.httpx_client import get_async_client from homeassistant.helpers.selector import ( TextSelector, @@ -48,7 +47,7 @@ CONFIG_SCHEMA = vol.Schema( ) -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class DiscovergyConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Discovergy.""" VERSION = 1 @@ -57,7 +56,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" if user_input is None: return self.async_show_form( @@ -67,14 +66,16 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return await self._validate_and_save(user_input) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Handle the initial step.""" self._existing_entry = await self.async_set_unique_id(self.context["unique_id"]) return await self._validate_and_save(entry_data, step_id="reauth") async def _validate_and_save( self, user_input: Mapping[str, Any] | None = None, step_id: str = "user" - ) -> FlowResult: + ) -> ConfigFlowResult: """Validate user input and create config entry.""" errors = {} diff --git a/homeassistant/components/discovergy/const.py b/homeassistant/components/discovergy/const.py index f410eb94bcf..39ff7a7cd4b 100644 --- a/homeassistant/components/discovergy/const.py +++ b/homeassistant/components/discovergy/const.py @@ -1,4 +1,5 @@ """Constants for the Discovergy integration.""" + from __future__ import annotations DOMAIN = "discovergy" diff --git a/homeassistant/components/discovergy/coordinator.py b/homeassistant/components/discovergy/coordinator.py index e5e161a5d40..3be4c71c987 100644 --- a/homeassistant/components/discovergy/coordinator.py +++ b/homeassistant/components/discovergy/coordinator.py @@ -1,4 +1,5 @@ """DataUpdateCoordinator for the Discovergy integration.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/discovergy/diagnostics.py b/homeassistant/components/discovergy/diagnostics.py index 99d559e94bc..15676da9888 100644 --- a/homeassistant/components/discovergy/diagnostics.py +++ b/homeassistant/components/discovergy/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for discovergy.""" + from __future__ import annotations from dataclasses import asdict diff --git a/homeassistant/components/discovergy/sensor.py b/homeassistant/components/discovergy/sensor.py index 365a67fe552..0a820917821 100644 --- a/homeassistant/components/discovergy/sensor.py +++ b/homeassistant/components/discovergy/sensor.py @@ -1,4 +1,5 @@ """Discovergy sensor entity.""" + from collections.abc import Callable from dataclasses import dataclass, field from datetime import datetime @@ -42,7 +43,7 @@ class DiscovergySensorEntityDescription(SensorEntityDescription): value_fn: Callable[[Reading, str, int], datetime | float | None] = field( default=_get_and_scale ) - alternative_keys: list[str] = field(default_factory=lambda: []) + alternative_keys: list[str] = field(default_factory=list) scale: int = field(default_factory=lambda: 1000) diff --git a/homeassistant/components/discovergy/system_health.py b/homeassistant/components/discovergy/system_health.py index 61fe4099596..1d539ac7012 100644 --- a/homeassistant/components/discovergy/system_health.py +++ b/homeassistant/components/discovergy/system_health.py @@ -1,4 +1,5 @@ """Provide info to system health.""" + from typing import Any from pydiscovergy.const import API_BASE diff --git a/homeassistant/components/dlib_face_detect/image_processing.py b/homeassistant/components/dlib_face_detect/image_processing.py index 42031b28844..9f6b30dee61 100644 --- a/homeassistant/components/dlib_face_detect/image_processing.py +++ b/homeassistant/components/dlib_face_detect/image_processing.py @@ -1,4 +1,5 @@ """Component that will help set the Dlib face detect processing.""" + from __future__ import annotations import io @@ -23,13 +24,10 @@ def setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the Dlib Face detection platform.""" - entities = [] - for camera in config[CONF_SOURCE]: - entities.append( - DlibFaceDetectEntity(camera[CONF_ENTITY_ID], camera.get(CONF_NAME)) - ) - - add_entities(entities) + add_entities( + DlibFaceDetectEntity(camera[CONF_ENTITY_ID], camera.get(CONF_NAME)) + for camera in config[CONF_SOURCE] + ) class DlibFaceDetectEntity(ImageProcessingFaceEntity): diff --git a/homeassistant/components/dlib_face_identify/image_processing.py b/homeassistant/components/dlib_face_identify/image_processing.py index e6aaa6848d0..ac9e69ec9e1 100644 --- a/homeassistant/components/dlib_face_identify/image_processing.py +++ b/homeassistant/components/dlib_face_identify/image_processing.py @@ -1,4 +1,5 @@ """Component that will help set the Dlib face detect processing.""" + from __future__ import annotations import io @@ -37,18 +38,15 @@ def setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the Dlib Face detection platform.""" - entities = [] - for camera in config[CONF_SOURCE]: - entities.append( - DlibFaceIdentifyEntity( - camera[CONF_ENTITY_ID], - config[CONF_FACES], - camera.get(CONF_NAME), - config[CONF_CONFIDENCE], - ) + add_entities( + DlibFaceIdentifyEntity( + camera[CONF_ENTITY_ID], + config[CONF_FACES], + camera.get(CONF_NAME), + config[CONF_CONFIDENCE], ) - - add_entities(entities) + for camera in config[CONF_SOURCE] + ) class DlibFaceIdentifyEntity(ImageProcessingFaceEntity): diff --git a/homeassistant/components/dlink/__init__.py b/homeassistant/components/dlink/__init__.py index 40fce4acf76..80260643223 100644 --- a/homeassistant/components/dlink/__init__.py +++ b/homeassistant/components/dlink/__init__.py @@ -1,4 +1,5 @@ """The D-Link Power Plug integration.""" + from __future__ import annotations from pyW215.pyW215 import SmartPlug diff --git a/homeassistant/components/dlink/config_flow.py b/homeassistant/components/dlink/config_flow.py index 09df5571a78..52937d26b7d 100644 --- a/homeassistant/components/dlink/config_flow.py +++ b/homeassistant/components/dlink/config_flow.py @@ -1,4 +1,5 @@ """Config flow for the D-Link Power Plug integration.""" + from __future__ import annotations import logging @@ -7,24 +8,25 @@ from typing import Any from pyW215.pyW215 import SmartPlug import voluptuous as vol -from homeassistant import config_entries from homeassistant.components import dhcp +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME -from homeassistant.data_entry_flow import FlowResult from .const import CONF_USE_LEGACY_PROTOCOL, DEFAULT_NAME, DEFAULT_USERNAME, DOMAIN _LOGGER = logging.getLogger(__name__) -class DLinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class DLinkFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a config flow for D-Link Power Plug.""" def __init__(self) -> None: """Initialize a D-Link Power Plug flow.""" self.ip_address: str | None = None - async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: + async def async_step_dhcp( + self, discovery_info: dhcp.DhcpServiceInfo + ) -> ConfigFlowResult: """Handle dhcp discovery.""" await self.async_set_unique_id(discovery_info.macaddress) self._abort_if_unique_id_configured(updates={CONF_HOST: discovery_info.ip}) @@ -41,7 +43,7 @@ class DLinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_confirm_discovery( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Allow the user to confirm adding the device.""" errors = {} if user_input is not None: @@ -74,7 +76,7 @@ class DLinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initiated by the user.""" errors = {} if user_input is not None: diff --git a/homeassistant/components/dlink/data.py b/homeassistant/components/dlink/data.py index b93cd219166..35cafc18367 100644 --- a/homeassistant/components/dlink/data.py +++ b/homeassistant/components/dlink/data.py @@ -1,4 +1,5 @@ """Data for the D-Link Power Plug integration.""" + from __future__ import annotations from datetime import datetime diff --git a/homeassistant/components/dlink/entity.py b/homeassistant/components/dlink/entity.py index 238db5f5c57..2a9ac0e6c12 100644 --- a/homeassistant/components/dlink/entity.py +++ b/homeassistant/components/dlink/entity.py @@ -1,4 +1,5 @@ """Entity representing a D-Link Power Plug device.""" + from __future__ import annotations from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/dlink/switch.py b/homeassistant/components/dlink/switch.py index 0814945bc07..a37caa6700c 100644 --- a/homeassistant/components/dlink/switch.py +++ b/homeassistant/components/dlink/switch.py @@ -1,4 +1,5 @@ """Support for D-Link Power Plug Switches.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/dlna_dmr/__init__.py b/homeassistant/components/dlna_dmr/__init__.py index e9dd60c5896..d22f4eb41d4 100644 --- a/homeassistant/components/dlna_dmr/__init__.py +++ b/homeassistant/components/dlna_dmr/__init__.py @@ -1,4 +1,5 @@ """The dlna_dmr component.""" + from __future__ import annotations from homeassistant import config_entries diff --git a/homeassistant/components/dlna_dmr/config_flow.py b/homeassistant/components/dlna_dmr/config_flow.py index 1ad29c72c26..9d95ba3883e 100644 --- a/homeassistant/components/dlna_dmr/config_flow.py +++ b/homeassistant/components/dlna_dmr/config_flow.py @@ -1,4 +1,5 @@ """Config flow for DLNA DMR.""" + from __future__ import annotations from collections.abc import Callable, Mapping @@ -15,11 +16,15 @@ from async_upnp_client.profiles.profile import find_device_of_type from getmac import get_mac_address import voluptuous as vol -from homeassistant import config_entries from homeassistant.components import ssdp +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.const import CONF_DEVICE_ID, CONF_HOST, CONF_MAC, CONF_TYPE, CONF_URL from homeassistant.core import HomeAssistant, callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import IntegrationError from homeassistant.helpers import config_validation as cv, device_registry as dr @@ -42,7 +47,7 @@ class ConnectError(IntegrationError): """Error occurred when trying to connect to a device.""" -class DlnaDmrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class DlnaDmrFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a DLNA DMR config flow. The Unique Device Name (UDN) of the DMR device is used as the unique_id for @@ -65,12 +70,12 @@ class DlnaDmrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback def async_get_options_flow( - config_entry: config_entries.ConfigEntry, - ) -> config_entries.OptionsFlow: + config_entry: ConfigEntry, + ) -> OptionsFlow: """Define the config flow to handle options.""" return DlnaDmrOptionsFlowHandler(config_entry) - async def async_step_user(self, user_input: FlowInput = None) -> FlowResult: + async def async_step_user(self, user_input: FlowInput = None) -> ConfigFlowResult: """Handle a flow initialized by the user. Let user choose from a list of found and unconfigured devices or to @@ -102,7 +107,7 @@ class DlnaDmrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) return self.async_show_form(step_id="user", data_schema=data_schema) - async def async_step_manual(self, user_input: FlowInput = None) -> FlowResult: + async def async_step_manual(self, user_input: FlowInput = None) -> ConfigFlowResult: """Manual URL entry by the user.""" LOGGER.debug("async_step_manual: user_input: %s", user_input) @@ -124,7 +129,9 @@ class DlnaDmrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): step_id="manual", data_schema=data_schema, errors=errors ) - async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: + async def async_step_ssdp( + self, discovery_info: ssdp.SsdpServiceInfo + ) -> ConfigFlowResult: """Handle a flow initialized by SSDP discovery.""" if LOGGER.isEnabledFor(logging.DEBUG): LOGGER.debug("async_step_ssdp: discovery_info %s", pformat(discovery_info)) @@ -151,7 +158,9 @@ class DlnaDmrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_confirm() - async def async_step_ignore(self, user_input: Mapping[str, Any]) -> FlowResult: + async def async_step_ignore( + self, user_input: Mapping[str, Any] + ) -> ConfigFlowResult: """Ignore this config flow, and add MAC address as secondary identifier. Not all DMR devices correctly implement the spec, so their UDN may @@ -185,7 +194,9 @@ class DlnaDmrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): }, ) - async def async_step_unignore(self, user_input: Mapping[str, Any]) -> FlowResult: + async def async_step_unignore( + self, user_input: Mapping[str, Any] + ) -> ConfigFlowResult: """Rediscover previously ignored devices by their unique_id.""" LOGGER.debug("async_step_unignore: user_input: %s", user_input) self._udn = user_input["unique_id"] @@ -208,7 +219,9 @@ class DlnaDmrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_confirm() - async def async_step_confirm(self, user_input: FlowInput = None) -> FlowResult: + async def async_step_confirm( + self, user_input: FlowInput = None + ) -> ConfigFlowResult: """Allow the user to confirm adding the device.""" LOGGER.debug("async_step_confirm: %s", user_input) @@ -256,7 +269,7 @@ class DlnaDmrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if not self._mac and (host := urlparse(self._location).hostname): self._mac = await _async_get_mac_address(self.hass, host) - def _create_entry(self) -> FlowResult: + def _create_entry(self) -> ConfigFlowResult: """Create a config entry, assuming all required information is now known.""" LOGGER.debug( "_async_create_entry: location: %s, UDN: %s", self._location, self._udn @@ -333,19 +346,19 @@ class DlnaDmrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return discoveries -class DlnaDmrOptionsFlowHandler(config_entries.OptionsFlow): +class DlnaDmrOptionsFlowHandler(OptionsFlow): """Handle a DLNA DMR options flow. Configures the single instance and updates the existing config entry. """ - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + def __init__(self, config_entry: ConfigEntry) -> None: """Initialize.""" self.config_entry = config_entry async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Manage the options.""" errors: dict[str, str] = {} # Don't modify existing (read-only) options -- copy and update instead diff --git a/homeassistant/components/dlna_dmr/const.py b/homeassistant/components/dlna_dmr/const.py index 4cea664f058..df81cee08e4 100644 --- a/homeassistant/components/dlna_dmr/const.py +++ b/homeassistant/components/dlna_dmr/const.py @@ -1,4 +1,5 @@ """Constants for the DLNA DMR component.""" + from __future__ import annotations from collections.abc import Mapping diff --git a/homeassistant/components/dlna_dmr/data.py b/homeassistant/components/dlna_dmr/data.py index 1a1a28d758c..7af396f7c60 100644 --- a/homeassistant/components/dlna_dmr/data.py +++ b/homeassistant/components/dlna_dmr/data.py @@ -1,4 +1,5 @@ """Data used by this integration.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/dlna_dmr/manifest.json b/homeassistant/components/dlna_dmr/manifest.json index 128822cf289..41fa49f1a94 100644 --- a/homeassistant/components/dlna_dmr/manifest.json +++ b/homeassistant/components/dlna_dmr/manifest.json @@ -8,7 +8,7 @@ "documentation": "https://www.home-assistant.io/integrations/dlna_dmr", "iot_class": "local_push", "loggers": ["async_upnp_client"], - "requirements": ["async-upnp-client==0.38.2", "getmac==0.9.4"], + "requirements": ["async-upnp-client==0.38.3", "getmac==0.9.4"], "ssdp": [ { "deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1", diff --git a/homeassistant/components/dlna_dmr/media_player.py b/homeassistant/components/dlna_dmr/media_player.py index c8c70486854..69b9c0ffdb7 100644 --- a/homeassistant/components/dlna_dmr/media_player.py +++ b/homeassistant/components/dlna_dmr/media_player.py @@ -1,4 +1,5 @@ """Support for DLNA DMR (Device Media Renderer).""" + from __future__ import annotations import asyncio @@ -56,6 +57,17 @@ _R = TypeVar("_R") _P = ParamSpec("_P") +_TRANSPORT_STATE_TO_MEDIA_PLAYER_STATE = { + TransportState.PLAYING: MediaPlayerState.PLAYING, + TransportState.TRANSITIONING: MediaPlayerState.PLAYING, + TransportState.PAUSED_PLAYBACK: MediaPlayerState.PAUSED, + TransportState.PAUSED_RECORDING: MediaPlayerState.PAUSED, + # Unable to map this state to anything reasonable, so it's "Unknown" + TransportState.VENDOR_DEFINED: None, + None: MediaPlayerState.ON, +} + + def catch_request_errors( func: Callable[Concatenate[_DlnaDmrEntityT, _P], Awaitable[_R]], ) -> Callable[Concatenate[_DlnaDmrEntityT, _P], Coroutine[Any, Any, _R | None]]: @@ -185,6 +197,7 @@ class DlnaDmrEntity(MediaPlayerEntity): self._updated_registry: bool = False self._config_entry = config_entry self._attr_device_info = dr.DeviceInfo(connections={(dr.CONNECTION_UPNP, udn)}) + self._attr_supported_features = self._supported_features() async def async_added_to_hass(self) -> None: """Handle addition.""" @@ -344,6 +357,11 @@ class DlnaDmrEntity(MediaPlayerEntity): # Device was de/re-connected, state might have changed self.async_write_ha_state() + def async_write_ha_state(self) -> None: + """Write the state.""" + self._attr_supported_features = self._supported_features() + super().async_write_ha_state() + async def _device_connect(self, location: str) -> None: """Connect to the device now that it's available.""" _LOGGER.debug("Connecting to device at %s", location) @@ -490,6 +508,9 @@ class DlnaDmrEntity(MediaPlayerEntity): finally: self.check_available = False + # Supported features may have changed + self._attr_supported_features = self._supported_features() + def _on_event( self, service: UpnpService, state_variables: Sequence[UpnpStateVariable] ) -> None: @@ -530,28 +551,13 @@ class DlnaDmrEntity(MediaPlayerEntity): @property def state(self) -> MediaPlayerState | None: """State of the player.""" - if not self._device or not self.available: + if not self._device: return MediaPlayerState.OFF - if self._device.transport_state is None: - return MediaPlayerState.ON - if self._device.transport_state in ( - TransportState.PLAYING, - TransportState.TRANSITIONING, - ): - return MediaPlayerState.PLAYING - if self._device.transport_state in ( - TransportState.PAUSED_PLAYBACK, - TransportState.PAUSED_RECORDING, - ): - return MediaPlayerState.PAUSED - if self._device.transport_state == TransportState.VENDOR_DEFINED: - # Unable to map this state to anything reasonable, so it's "Unknown" - return None + return _TRANSPORT_STATE_TO_MEDIA_PLAYER_STATE.get( + self._device.transport_state, MediaPlayerState.IDLE + ) - return MediaPlayerState.IDLE - - @property - def supported_features(self) -> MediaPlayerEntityFeature: + def _supported_features(self) -> MediaPlayerEntityFeature: """Flag media player features that are supported at this moment. Supported features may change as the device enters different states. diff --git a/homeassistant/components/dlna_dms/__init__.py b/homeassistant/components/dlna_dms/__init__.py index 5cd6321a5df..668a2e9d965 100644 --- a/homeassistant/components/dlna_dms/__init__.py +++ b/homeassistant/components/dlna_dms/__init__.py @@ -3,6 +3,7 @@ A single config entry is used, with SSDP discovery for media servers. Each server is wrapped in a DmsEntity, and the server's USN is used as the unique_id. """ + from __future__ import annotations from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/dlna_dms/config_flow.py b/homeassistant/components/dlna_dms/config_flow.py index e147055df05..480f45ee95b 100644 --- a/homeassistant/components/dlna_dms/config_flow.py +++ b/homeassistant/components/dlna_dms/config_flow.py @@ -1,4 +1,5 @@ """Config flow for DLNA DMS.""" + from __future__ import annotations import logging @@ -9,10 +10,10 @@ from urllib.parse import urlparse from async_upnp_client.profiles.dlna import DmsDevice import voluptuous as vol -from homeassistant import config_entries from homeassistant.components import ssdp +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_DEVICE_ID, CONF_HOST, CONF_URL -from homeassistant.data_entry_flow import AbortFlow, FlowResult +from homeassistant.data_entry_flow import AbortFlow from .const import CONF_SOURCE_ID, CONFIG_VERSION, DEFAULT_NAME, DOMAIN from .util import generate_source_id @@ -20,7 +21,7 @@ from .util import generate_source_id LOGGER = logging.getLogger(__name__) -class DlnaDmsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class DlnaDmsFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a DLNA DMS config flow. The Unique Service Name (USN) of the DMS device is used as the unique_id for @@ -39,7 +40,7 @@ class DlnaDmsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by the user by listing unconfigured devices.""" LOGGER.debug("async_step_user: user_input: %s", user_input) @@ -65,7 +66,9 @@ class DlnaDmsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): data_schema = vol.Schema({vol.Optional(CONF_HOST): vol.In(discovery_choices)}) return self.async_show_form(step_id="user", data_schema=data_schema) - async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: + async def async_step_ssdp( + self, discovery_info: ssdp.SsdpServiceInfo + ) -> ConfigFlowResult: """Handle a flow initialized by SSDP discovery.""" if LOGGER.isEnabledFor(logging.DEBUG): LOGGER.debug("async_step_ssdp: discovery_info %s", pformat(discovery_info)) @@ -101,7 +104,7 @@ class DlnaDmsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Allow the user to confirm adding the device.""" if user_input is not None: return self._create_entry() @@ -109,7 +112,7 @@ class DlnaDmsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self._set_confirm_only() return self.async_show_form(step_id="confirm") - def _create_entry(self) -> FlowResult: + def _create_entry(self) -> ConfigFlowResult: """Create a config entry, assuming all required information is now known.""" LOGGER.debug( "_create_entry: name: %s, location: %s, USN: %s", diff --git a/homeassistant/components/dlna_dms/const.py b/homeassistant/components/dlna_dms/const.py index 8d4cb6352ee..686e6c63108 100644 --- a/homeassistant/components/dlna_dms/const.py +++ b/homeassistant/components/dlna_dms/const.py @@ -1,4 +1,5 @@ """Constants for the DLNA MediaServer integration.""" + from __future__ import annotations from collections.abc import Mapping diff --git a/homeassistant/components/dlna_dms/dms.py b/homeassistant/components/dlna_dms/dms.py index 54cca744360..aaa55e3ad3e 100644 --- a/homeassistant/components/dlna_dms/dms.py +++ b/homeassistant/components/dlna_dms/dms.py @@ -1,4 +1,5 @@ """Wrapper for media_source around async_upnp_client's DmsDevice .""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/dlna_dms/manifest.json b/homeassistant/components/dlna_dms/manifest.json index aaa6e1ee7de..c87e5e87779 100644 --- a/homeassistant/components/dlna_dms/manifest.json +++ b/homeassistant/components/dlna_dms/manifest.json @@ -8,7 +8,7 @@ "documentation": "https://www.home-assistant.io/integrations/dlna_dms", "iot_class": "local_polling", "quality_scale": "platinum", - "requirements": ["async-upnp-client==0.38.2"], + "requirements": ["async-upnp-client==0.38.3"], "ssdp": [ { "deviceType": "urn:schemas-upnp-org:device:MediaServer:1", diff --git a/homeassistant/components/dlna_dms/util.py b/homeassistant/components/dlna_dms/util.py index 74c5bd2e01b..78ada3c708a 100644 --- a/homeassistant/components/dlna_dms/util.py +++ b/homeassistant/components/dlna_dms/util.py @@ -1,4 +1,5 @@ """Small utility functions for the dlna_dms integration.""" + from __future__ import annotations from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/dnsip/__init__.py b/homeassistant/components/dnsip/__init__.py index 13783a1b07f..78309b5f2bf 100644 --- a/homeassistant/components/dnsip/__init__.py +++ b/homeassistant/components/dnsip/__init__.py @@ -1,4 +1,5 @@ """The dnsip component.""" + from __future__ import annotations from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/dnsip/config_flow.py b/homeassistant/components/dnsip/config_flow.py index 6a1437d5159..f07971d5db5 100644 --- a/homeassistant/components/dnsip/config_flow.py +++ b/homeassistant/components/dnsip/config_flow.py @@ -1,4 +1,5 @@ """Adds config flow for dnsip integration.""" + from __future__ import annotations import asyncio @@ -9,10 +10,14 @@ import aiodns from aiodns.error import DNSError import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlowWithConfigEntry, +) from homeassistant.const import CONF_NAME from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult import homeassistant.helpers.config_validation as cv from .const import ( @@ -72,7 +77,7 @@ async def async_validate_hostname( return result -class DnsIPConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class DnsIPConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for dnsip integration.""" VERSION = 1 @@ -80,14 +85,14 @@ class DnsIPConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback def async_get_options_flow( - config_entry: config_entries.ConfigEntry, + config_entry: ConfigEntry, ) -> DnsIPOptionsFlowHandler: """Return Option handler.""" return DnsIPOptionsFlowHandler(config_entry) async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors = {} @@ -141,47 +146,47 @@ class DnsIPConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -class DnsIPOptionsFlowHandler(config_entries.OptionsFlow): +class DnsIPOptionsFlowHandler(OptionsFlowWithConfigEntry): """Handle a option config flow for dnsip integration.""" - def __init__(self, entry: config_entries.ConfigEntry) -> None: - """Initialize options flow.""" - self.entry = entry - async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Manage the options.""" errors = {} if user_input is not None: + resolver = user_input.get(CONF_RESOLVER, DEFAULT_RESOLVER) + resolver_ipv6 = user_input.get(CONF_RESOLVER_IPV6, DEFAULT_RESOLVER_IPV6) validate = await async_validate_hostname( - self.entry.data[CONF_HOSTNAME], - user_input[CONF_RESOLVER], - user_input[CONF_RESOLVER_IPV6], + self.config_entry.data[CONF_HOSTNAME], + resolver, + resolver_ipv6, ) - if validate[CONF_IPV4] is False and self.entry.data[CONF_IPV4] is True: + if ( + validate[CONF_IPV4] is False + and self.config_entry.data[CONF_IPV4] is True + ): errors[CONF_RESOLVER] = "invalid_resolver" - elif validate[CONF_IPV6] is False and self.entry.data[CONF_IPV6] is True: + elif ( + validate[CONF_IPV6] is False + and self.config_entry.data[CONF_IPV6] is True + ): errors[CONF_RESOLVER_IPV6] = "invalid_resolver" else: - return self.async_create_entry(title=self.entry.title, data=user_input) + return self.async_create_entry( + title=self.config_entry.title, + data={CONF_RESOLVER: resolver, CONF_RESOLVER_IPV6: resolver_ipv6}, + ) - return self.async_show_form( - step_id="init", - data_schema=vol.Schema( + schema = self.add_suggested_values_to_schema( + vol.Schema( { - vol.Optional( - CONF_RESOLVER, - default=self.entry.options.get(CONF_RESOLVER, DEFAULT_RESOLVER), - ): cv.string, - vol.Optional( - CONF_RESOLVER_IPV6, - default=self.entry.options.get( - CONF_RESOLVER_IPV6, DEFAULT_RESOLVER_IPV6 - ), - ): cv.string, + vol.Optional(CONF_RESOLVER): cv.string, + vol.Optional(CONF_RESOLVER_IPV6): cv.string, } ), - errors=errors, + self.config_entry.options, ) + + return self.async_show_form(step_id="init", data_schema=schema, errors=errors) diff --git a/homeassistant/components/dnsip/const.py b/homeassistant/components/dnsip/const.py index 56215d3d9a6..41116bde61a 100644 --- a/homeassistant/components/dnsip/const.py +++ b/homeassistant/components/dnsip/const.py @@ -1,4 +1,5 @@ """Constants for dnsip integration.""" + from homeassistant.const import Platform DOMAIN = "dnsip" diff --git a/homeassistant/components/dnsip/manifest.json b/homeassistant/components/dnsip/manifest.json index e90e4eb2c72..17c0677e4d9 100644 --- a/homeassistant/components/dnsip/manifest.json +++ b/homeassistant/components/dnsip/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/dnsip", "iot_class": "cloud_polling", - "requirements": ["aiodns==3.0.0"] + "requirements": ["aiodns==3.1.1"] } diff --git a/homeassistant/components/dnsip/sensor.py b/homeassistant/components/dnsip/sensor.py index 975ec1992ae..529de6f2b1b 100644 --- a/homeassistant/components/dnsip/sensor.py +++ b/homeassistant/components/dnsip/sensor.py @@ -1,4 +1,5 @@ """Get your own public IP address or that of any host.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/dominos/__init__.py b/homeassistant/components/dominos/__init__.py index 37344ed9d4b..48404e6dbee 100644 --- a/homeassistant/components/dominos/__init__.py +++ b/homeassistant/components/dominos/__init__.py @@ -1,4 +1,5 @@ """Support for Dominos Pizza ordering.""" + from datetime import timedelta import logging diff --git a/homeassistant/components/dominos/icons.json b/homeassistant/components/dominos/icons.json new file mode 100644 index 00000000000..d88bfb2542f --- /dev/null +++ b/homeassistant/components/dominos/icons.json @@ -0,0 +1,5 @@ +{ + "services": { + "order": "mdi:pizza" + } +} diff --git a/homeassistant/components/doods/image_processing.py b/homeassistant/components/doods/image_processing.py index ba97dbe38ec..11985ef4889 100644 --- a/homeassistant/components/doods/image_processing.py +++ b/homeassistant/components/doods/image_processing.py @@ -1,4 +1,5 @@ """Support for the DOODS service.""" + from __future__ import annotations import io @@ -111,19 +112,17 @@ def setup_platform( ) return - entities = [] - for camera in config[CONF_SOURCE]: - entities.append( - Doods( - hass, - camera[CONF_ENTITY_ID], - camera.get(CONF_NAME), - doods, - detector, - config, - ) + add_entities( + Doods( + hass, + camera[CONF_ENTITY_ID], + camera.get(CONF_NAME), + doods, + detector, + config, ) - add_entities(entities) + for camera in config[CONF_SOURCE] + ) class Doods(ImageProcessingEntity): diff --git a/homeassistant/components/doorbird/__init__.py b/homeassistant/components/doorbird/__init__.py index d7800a26fc8..71786eac779 100644 --- a/homeassistant/components/doorbird/__init__.py +++ b/homeassistant/components/doorbird/__init__.py @@ -1,4 +1,5 @@ """Support for DoorBird devices.""" + from __future__ import annotations from http import HTTPStatus diff --git a/homeassistant/components/doorbird/button.py b/homeassistant/components/doorbird/button.py index d77ac7a378e..48709ceb9c1 100644 --- a/homeassistant/components/doorbird/button.py +++ b/homeassistant/components/doorbird/button.py @@ -17,20 +17,13 @@ from .models import DoorBirdData IR_RELAY = "__ir_light__" -@dataclass(frozen=True) -class DoorbirdButtonEntityDescriptionMixin: - """Mixin to describe a Doorbird Button entity.""" +@dataclass(frozen=True, kw_only=True) +class DoorbirdButtonEntityDescription(ButtonEntityDescription): + """Class to describe a Doorbird Button entity.""" press_action: Callable[[DoorBird, str], None] -@dataclass(frozen=True) -class DoorbirdButtonEntityDescription( - ButtonEntityDescription, DoorbirdButtonEntityDescriptionMixin -): - """Class to describe a Doorbird Button entity.""" - - RELAY_ENTITY_DESCRIPTION = DoorbirdButtonEntityDescription( key="relay", translation_key="relay", diff --git a/homeassistant/components/doorbird/camera.py b/homeassistant/components/doorbird/camera.py index 3da47eb572a..ddc8ae0bdc5 100644 --- a/homeassistant/components/doorbird/camera.py +++ b/homeassistant/components/doorbird/camera.py @@ -1,4 +1,5 @@ """Support for viewing the camera feed from a DoorBird video doorbell.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/doorbird/config_flow.py b/homeassistant/components/doorbird/config_flow.py index 983e56e64da..8bb069bab88 100644 --- a/homeassistant/components/doorbird/config_flow.py +++ b/homeassistant/components/doorbird/config_flow.py @@ -1,4 +1,5 @@ """Config flow for DoorBird integration.""" + from __future__ import annotations from http import HTTPStatus @@ -9,11 +10,16 @@ from doorbirdpy import DoorBird import requests import voluptuous as vol -from homeassistant import config_entries, core, exceptions from homeassistant.components import zeroconf +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant, callback -from homeassistant.data_entry_flow import FlowResult +from homeassistant.exceptions import HomeAssistantError from .const import CONF_EVENTS, DOMAIN, DOORBIRD_OUI from .util import get_mac_address_from_door_station_info @@ -39,9 +45,7 @@ def _check_device(device: DoorBird) -> tuple[tuple[bool, int], dict[str, Any]]: return device.ready(), device.info() -async def validate_input( - hass: core.HomeAssistant, data: dict[str, Any] -) -> dict[str, str]: +async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, str]: """Validate the user input allows us to connect.""" device = DoorBird(data[CONF_HOST], data[CONF_USERNAME], data[CONF_PASSWORD]) try: @@ -75,7 +79,7 @@ async def async_verify_supported_device(hass: HomeAssistant, host: str) -> bool: return False -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class DoorBirdConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for DoorBird.""" VERSION = 1 @@ -86,7 +90,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors: dict[str, str] = {} if user_input is not None: @@ -101,7 +105,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_zeroconf( self, discovery_info: zeroconf.ZeroconfServiceInfo - ) -> FlowResult: + ) -> ConfigFlowResult: """Prepare configuration for a discovered doorbird device.""" macaddress = discovery_info.properties["macaddress"] @@ -152,22 +156,22 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback def async_get_options_flow( - config_entry: config_entries.ConfigEntry, + config_entry: ConfigEntry, ) -> OptionsFlowHandler: """Get the options flow for this handler.""" return OptionsFlowHandler(config_entry) -class OptionsFlowHandler(config_entries.OptionsFlow): +class OptionsFlowHandler(OptionsFlow): """Handle a option flow for doorbird.""" - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + 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: + ) -> ConfigFlowResult: """Handle options flow.""" if user_input is not None: events = [event.strip() for event in user_input[CONF_EVENTS].split(",")] @@ -184,9 +188,9 @@ class OptionsFlowHandler(config_entries.OptionsFlow): return self.async_show_form(step_id="init", data_schema=options_schema) -class CannotConnect(exceptions.HomeAssistantError): +class CannotConnect(HomeAssistantError): """Error to indicate we cannot connect.""" -class InvalidAuth(exceptions.HomeAssistantError): +class InvalidAuth(HomeAssistantError): """Error to indicate there is invalid auth.""" diff --git a/homeassistant/components/doorbird/const.py b/homeassistant/components/doorbird/const.py index 416603a312c..1bd13496e3a 100644 --- a/homeassistant/components/doorbird/const.py +++ b/homeassistant/components/doorbird/const.py @@ -1,4 +1,5 @@ """The DoorBird integration constants.""" + from homeassistant.const import Platform DOMAIN = "doorbird" diff --git a/homeassistant/components/doorbird/device.py b/homeassistant/components/doorbird/device.py index 767a80a7857..e0fb02fcb8d 100644 --- a/homeassistant/components/doorbird/device.py +++ b/homeassistant/components/doorbird/device.py @@ -1,4 +1,5 @@ """Support for DoorBird devices.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/doorbird/entity.py b/homeassistant/components/doorbird/entity.py index 4360a8ff490..f09ef54d5ac 100644 --- a/homeassistant/components/doorbird/entity.py +++ b/homeassistant/components/doorbird/entity.py @@ -1,6 +1,5 @@ """The DoorBird integration base entity.""" - from homeassistant.helpers import device_registry as dr from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity import Entity diff --git a/homeassistant/components/doorbird/logbook.py b/homeassistant/components/doorbird/logbook.py index 84497a312ae..71a63557a2c 100644 --- a/homeassistant/components/doorbird/logbook.py +++ b/homeassistant/components/doorbird/logbook.py @@ -1,4 +1,5 @@ """Describe logbook events.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/doorbird/models.py b/homeassistant/components/doorbird/models.py index f8fb8687e59..a8ecbf19d5d 100644 --- a/homeassistant/components/doorbird/models.py +++ b/homeassistant/components/doorbird/models.py @@ -1,4 +1,5 @@ """The doorbird integration models.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/homeassistant/components/doorbird/view.py b/homeassistant/components/doorbird/view.py index 396db79bf4c..e1fa8e7cfbb 100644 --- a/homeassistant/components/doorbird/view.py +++ b/homeassistant/components/doorbird/view.py @@ -1,12 +1,12 @@ """Support for DoorBird devices.""" + from __future__ import annotations from http import HTTPStatus from aiohttp import web -from homeassistant.components.http import HomeAssistantView -from homeassistant.core import HomeAssistant +from homeassistant.components.http import KEY_HASS, HomeAssistantView from .const import API_URL, DOMAIN from .device import async_reset_device_favorites @@ -23,7 +23,7 @@ class DoorBirdRequestView(HomeAssistantView): async def get(self, request: web.Request, event: str) -> web.Response: """Respond to requests from the device.""" - hass: HomeAssistant = request.app["hass"] + hass = request.app[KEY_HASS] token: str | None = request.query.get("token") if ( token is None diff --git a/homeassistant/components/dormakaba_dkey/__init__.py b/homeassistant/components/dormakaba_dkey/__init__.py index 4903e46b8dc..a8868e8563c 100644 --- a/homeassistant/components/dormakaba_dkey/__init__.py +++ b/homeassistant/components/dormakaba_dkey/__init__.py @@ -1,4 +1,5 @@ """The Dormakaba dKey integration.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/dormakaba_dkey/binary_sensor.py b/homeassistant/components/dormakaba_dkey/binary_sensor.py index 2ec2b0a1c91..a8574443e35 100644 --- a/homeassistant/components/dormakaba_dkey/binary_sensor.py +++ b/homeassistant/components/dormakaba_dkey/binary_sensor.py @@ -1,4 +1,5 @@ """Dormakaba dKey integration binary sensor platform.""" + from __future__ import annotations from collections.abc import Callable @@ -22,20 +23,13 @@ from .entity import DormakabaDkeyEntity from .models import DormakabaDkeyData -@dataclass(frozen=True) -class DormakabaDkeyBinarySensorDescriptionMixin: - """Class for keys required by Dormakaba dKey binary sensor entity.""" +@dataclass(frozen=True, kw_only=True) +class DormakabaDkeyBinarySensorDescription(BinarySensorEntityDescription): + """Describes Dormakaba dKey binary sensor entity.""" is_on: Callable[[Notifications], bool] -@dataclass(frozen=True) -class DormakabaDkeyBinarySensorDescription( - BinarySensorEntityDescription, DormakabaDkeyBinarySensorDescriptionMixin -): - """Describes Dormakaba dKey binary sensor entity.""" - - BINARY_SENSOR_DESCRIPTIONS = ( DormakabaDkeyBinarySensorDescription( key="door_position", diff --git a/homeassistant/components/dormakaba_dkey/config_flow.py b/homeassistant/components/dormakaba_dkey/config_flow.py index f03861d015e..d4cd19644c1 100644 --- a/homeassistant/components/dormakaba_dkey/config_flow.py +++ b/homeassistant/components/dormakaba_dkey/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Dormakaba dKey integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -9,14 +10,13 @@ from bleak import BleakError from py_dormakaba_dkey import DKEYLock, device_filter, errors as dkey_errors import voluptuous as vol -from homeassistant import config_entries from homeassistant.components.bluetooth import ( BluetoothServiceInfoBleak, async_discovered_service_info, async_last_service_info, ) +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_ADDRESS -from homeassistant.data_entry_flow import FlowResult from .const import CONF_ASSOCIATION_DATA, DOMAIN @@ -29,12 +29,12 @@ STEP_ASSOCIATE_SCHEMA = vol.Schema( ) -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class DormkabaConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Dormakaba dKey.""" VERSION = 1 - _reauth_entry: config_entries.ConfigEntry | None = None + _reauth_entry: ConfigEntry | None = None def __init__(self) -> None: """Initialize the config flow.""" @@ -46,7 +46,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the user step to pick discovered device.""" errors: dict[str, str] = {} @@ -92,7 +92,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_bluetooth( self, discovery_info: BluetoothServiceInfoBleak - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the Bluetooth discovery step.""" await self.async_set_unique_id(discovery_info.address) self._abort_if_unique_id_configured() @@ -103,7 +103,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_bluetooth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle bluetooth confirm step.""" # mypy is not aware that we can't get here without having these set already assert self._discovery_info is not None @@ -117,7 +117,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_associate() - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Handle reauthorization request.""" self._reauth_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] @@ -126,7 +128,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle reauthorization flow.""" errors = {} reauth_entry = self._reauth_entry @@ -149,7 +151,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_associate( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle associate step.""" # mypy is not aware that we can't get here without having these set already assert self._discovery_info is not None diff --git a/homeassistant/components/dormakaba_dkey/entity.py b/homeassistant/components/dormakaba_dkey/entity.py index 26a06deed0e..756edccf02f 100644 --- a/homeassistant/components/dormakaba_dkey/entity.py +++ b/homeassistant/components/dormakaba_dkey/entity.py @@ -1,4 +1,5 @@ """Dormakaba dKey integration base entity.""" + from __future__ import annotations import abc diff --git a/homeassistant/components/dormakaba_dkey/lock.py b/homeassistant/components/dormakaba_dkey/lock.py index e238c4e143b..5f475d37152 100644 --- a/homeassistant/components/dormakaba_dkey/lock.py +++ b/homeassistant/components/dormakaba_dkey/lock.py @@ -1,4 +1,5 @@ """Dormakaba dKey integration lock platform.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/dormakaba_dkey/models.py b/homeassistant/components/dormakaba_dkey/models.py index cd260c15e81..23687e82334 100644 --- a/homeassistant/components/dormakaba_dkey/models.py +++ b/homeassistant/components/dormakaba_dkey/models.py @@ -1,4 +1,5 @@ """The Dormakaba dKey integration models.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/homeassistant/components/dormakaba_dkey/sensor.py b/homeassistant/components/dormakaba_dkey/sensor.py index 39915563b03..e461ba1e44f 100644 --- a/homeassistant/components/dormakaba_dkey/sensor.py +++ b/homeassistant/components/dormakaba_dkey/sensor.py @@ -1,4 +1,5 @@ """Dormakaba dKey integration sensor platform.""" + from __future__ import annotations from py_dormakaba_dkey import DKEYLock diff --git a/homeassistant/components/dovado/__init__.py b/homeassistant/components/dovado/__init__.py index 40069a769a1..60e8351cc24 100644 --- a/homeassistant/components/dovado/__init__.py +++ b/homeassistant/components/dovado/__init__.py @@ -1,4 +1,5 @@ """Support for Dovado router.""" + from datetime import timedelta import logging diff --git a/homeassistant/components/dovado/notify.py b/homeassistant/components/dovado/notify.py index ba7dcf6d486..556848bf89f 100644 --- a/homeassistant/components/dovado/notify.py +++ b/homeassistant/components/dovado/notify.py @@ -1,4 +1,5 @@ """Support for SMS notifications from the Dovado router.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/dovado/sensor.py b/homeassistant/components/dovado/sensor.py index 4de20bf86e8..bd53fb22ad2 100644 --- a/homeassistant/components/dovado/sensor.py +++ b/homeassistant/components/dovado/sensor.py @@ -1,4 +1,5 @@ """Support for sensors from the Dovado router.""" + from __future__ import annotations from dataclasses import dataclass @@ -30,18 +31,13 @@ SENSOR_NETWORK = "network" SENSOR_SMS_UNREAD = "sms" -@dataclass(frozen=True) -class DovadoRequiredKeysMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class DovadoSensorEntityDescription(SensorEntityDescription): + """Describes Dovado sensor entity.""" identifier: str -@dataclass(frozen=True) -class DovadoSensorEntityDescription(SensorEntityDescription, DovadoRequiredKeysMixin): - """Describes Dovado sensor entity.""" - - SENSOR_TYPES: tuple[DovadoSensorEntityDescription, ...] = ( DovadoSensorEntityDescription( identifier=SENSOR_NETWORK, diff --git a/homeassistant/components/downloader/__init__.py b/homeassistant/components/downloader/__init__.py index 1922fde3102..94d243e2cf2 100644 --- a/homeassistant/components/downloader/__init__.py +++ b/homeassistant/components/downloader/__init__.py @@ -1,8 +1,8 @@ """Support for functionality to download files.""" + from __future__ import annotations from http import HTTPStatus -import logging import os import re import threading @@ -10,33 +10,26 @@ import threading import requests import voluptuous as vol +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.data_entry_flow import FlowResultType import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue +from homeassistant.helpers.service import async_register_admin_service from homeassistant.helpers.typing import ConfigType from homeassistant.util import raise_if_invalid_filename, raise_if_invalid_path -_LOGGER = logging.getLogger(__name__) - -ATTR_FILENAME = "filename" -ATTR_SUBDIR = "subdir" -ATTR_URL = "url" -ATTR_OVERWRITE = "overwrite" - -CONF_DOWNLOAD_DIR = "download_dir" - -DOMAIN = "downloader" -DOWNLOAD_FAILED_EVENT = "download_failed" -DOWNLOAD_COMPLETED_EVENT = "download_completed" - -SERVICE_DOWNLOAD_FILE = "download_file" - -SERVICE_DOWNLOAD_FILE_SCHEMA = vol.Schema( - { - vol.Required(ATTR_URL): cv.url, - vol.Optional(ATTR_SUBDIR): cv.string, - vol.Optional(ATTR_FILENAME): cv.string, - vol.Optional(ATTR_OVERWRITE, default=False): cv.boolean, - } +from .const import ( + _LOGGER, + ATTR_FILENAME, + ATTR_OVERWRITE, + ATTR_SUBDIR, + ATTR_URL, + CONF_DOWNLOAD_DIR, + DOMAIN, + DOWNLOAD_COMPLETED_EVENT, + DOWNLOAD_FAILED_EVENT, + SERVICE_DOWNLOAD_FILE, ) CONFIG_SCHEMA = vol.Schema( @@ -45,9 +38,46 @@ CONFIG_SCHEMA = vol.Schema( ) -def setup(hass: HomeAssistant, config: ConfigType) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the Downloader component, via the YAML file.""" + if DOMAIN not in config: + return True + + import_result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={ + CONF_DOWNLOAD_DIR: config[DOMAIN][CONF_DOWNLOAD_DIR], + }, + ) + + translation_key = "deprecated_yaml" + if ( + import_result["type"] == FlowResultType.ABORT + and import_result["reason"] == "import_failed" + ): + translation_key = "import_failed" + + async_create_issue( + hass, + DOMAIN, + f"deprecated_yaml_{DOMAIN}", + breaks_in_ha_version="2024.9.0", + is_fixable=False, + issue_domain=DOMAIN, + severity=IssueSeverity.WARNING, + translation_key=translation_key, + translation_placeholders={ + "domain": DOMAIN, + "integration_title": "Downloader", + }, + ) + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Listen for download events to download files.""" - download_path = config[DOMAIN][CONF_DOWNLOAD_DIR] + download_path = entry.data[CONF_DOWNLOAD_DIR] # If path is relative, we assume relative to Home Assistant config dir if not os.path.isabs(download_path): @@ -57,7 +87,6 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: _LOGGER.error( "Download path %s does not exist. File Downloader not active", download_path ) - return False def download_file(service: ServiceCall) -> None: @@ -168,11 +197,19 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: threading.Thread(target=do_download).start() - hass.services.register( + async_register_admin_service( + hass, DOMAIN, SERVICE_DOWNLOAD_FILE, download_file, - schema=SERVICE_DOWNLOAD_FILE_SCHEMA, + schema=vol.Schema( + { + vol.Optional(ATTR_FILENAME): cv.string, + vol.Optional(ATTR_SUBDIR): cv.string, + vol.Required(ATTR_URL): cv.url, + vol.Optional(ATTR_OVERWRITE, default=False): cv.boolean, + } + ), ) return True diff --git a/homeassistant/components/downloader/config_flow.py b/homeassistant/components/downloader/config_flow.py new file mode 100644 index 00000000000..15af8b56163 --- /dev/null +++ b/homeassistant/components/downloader/config_flow.py @@ -0,0 +1,71 @@ +"""Config flow for Downloader integration.""" + +from __future__ import annotations + +import os +from typing import Any + +import voluptuous as vol + +from homeassistant import exceptions +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult +from homeassistant.helpers import config_validation as cv + +from .const import _LOGGER, CONF_DOWNLOAD_DIR, DEFAULT_NAME, DOMAIN + + +class DownloaderConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for Downloader.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle the initial step.""" + errors: dict[str, str] = {} + + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + + if user_input is not None: + try: + await self._validate_input(user_input) + except DirectoryDoesNotExist: + errors["base"] = "cannot_connect" + else: + return self.async_create_entry(title=DEFAULT_NAME, data=user_input) + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_DOWNLOAD_DIR): cv.string, + } + ), + errors=errors, + ) + + async def async_step_import( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle a flow initiated by configuration file.""" + + return await self.async_step_user(user_input) + + async def _validate_input(self, user_input: dict[str, Any]) -> None: + """Validate the user input if the directory exists.""" + download_path = user_input[CONF_DOWNLOAD_DIR] + if not os.path.isabs(download_path): + download_path = self.hass.config.path(download_path) + + if not await self.hass.async_add_executor_job(os.path.isdir, download_path): + _LOGGER.error( + "Download path %s does not exist. File Downloader not active", + download_path, + ) + raise DirectoryDoesNotExist + + +class DirectoryDoesNotExist(exceptions.HomeAssistantError): + """Error to indicate the specified download directory does not exist.""" diff --git a/homeassistant/components/downloader/const.py b/homeassistant/components/downloader/const.py new file mode 100644 index 00000000000..14160e4cd5d --- /dev/null +++ b/homeassistant/components/downloader/const.py @@ -0,0 +1,20 @@ +"""Constants for the Downloader component.""" + +import logging + +_LOGGER = logging.getLogger(__package__) + +DOMAIN = "downloader" +DEFAULT_NAME = "Downloader" +CONF_DOWNLOAD_DIR = "download_dir" +ATTR_FILENAME = "filename" +ATTR_SUBDIR = "subdir" +ATTR_URL = "url" +ATTR_OVERWRITE = "overwrite" + +CONF_DOWNLOAD_DIR = "download_dir" + +DOWNLOAD_FAILED_EVENT = "download_failed" +DOWNLOAD_COMPLETED_EVENT = "download_completed" + +SERVICE_DOWNLOAD_FILE = "download_file" diff --git a/homeassistant/components/downloader/icons.json b/homeassistant/components/downloader/icons.json new file mode 100644 index 00000000000..2a78df93ca7 --- /dev/null +++ b/homeassistant/components/downloader/icons.json @@ -0,0 +1,5 @@ +{ + "services": { + "download_file": "mdi:download" + } +} diff --git a/homeassistant/components/downloader/manifest.json b/homeassistant/components/downloader/manifest.json index 5e4f0f5fde9..876404be889 100644 --- a/homeassistant/components/downloader/manifest.json +++ b/homeassistant/components/downloader/manifest.json @@ -1,7 +1,8 @@ { "domain": "downloader", "name": "Downloader", - "codeowners": [], + "codeowners": ["@erwindouna"], + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/downloader", "quality_scale": "internal" } diff --git a/homeassistant/components/downloader/strings.json b/homeassistant/components/downloader/strings.json index c81b9f0ea39..77dd0abd9d3 100644 --- a/homeassistant/components/downloader/strings.json +++ b/homeassistant/components/downloader/strings.json @@ -1,4 +1,17 @@ { + "config": { + "step": { + "user": { + "description": "Select a location to get to store downloads. The setup will check if the directory exists." + } + }, + "error": { + "cannot_connect": "The directory could not be reached. Please check your settings." + }, + "abort": { + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" + } + }, "services": { "download_file": { "name": "Download file", @@ -22,5 +35,15 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "title": "The {integration_title} YAML configuration is being removed", + "description": "Configuring {integration_title} using YAML is being removed.\n\nYour configuration is already imported.\n\nRemove the `{domain}` configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + }, + "import_failed": { + "title": "The {integration_title} failed to import", + "description": "The {integration_title} integration failed to import.\n\nPlease check the logs for more details." + } } } diff --git a/homeassistant/components/dremel_3d_printer/__init__.py b/homeassistant/components/dremel_3d_printer/__init__.py index 5f9f10dc9c1..76cd63a3a1d 100644 --- a/homeassistant/components/dremel_3d_printer/__init__.py +++ b/homeassistant/components/dremel_3d_printer/__init__.py @@ -1,4 +1,5 @@ """The Dremel 3D Printer (3D20, 3D40, 3D45) integration.""" + from __future__ import annotations from dremel3dpy import Dremel3DPrinter diff --git a/homeassistant/components/dremel_3d_printer/binary_sensor.py b/homeassistant/components/dremel_3d_printer/binary_sensor.py index 22c2a1a9557..e6df0ebcf6e 100644 --- a/homeassistant/components/dremel_3d_printer/binary_sensor.py +++ b/homeassistant/components/dremel_3d_printer/binary_sensor.py @@ -1,4 +1,5 @@ """Support for monitoring Dremel 3D Printer binary sensors.""" + from __future__ import annotations from collections.abc import Callable @@ -19,20 +20,13 @@ from .const import DOMAIN from .entity import Dremel3DPrinterEntity -@dataclass(frozen=True) -class Dremel3DPrinterBinarySensorEntityMixin: - """Mixin for Dremel 3D Printer binary sensor.""" +@dataclass(frozen=True, kw_only=True) +class Dremel3DPrinterBinarySensorEntityDescription(BinarySensorEntityDescription): + """Describes a Dremel 3D Printer binary sensor.""" value_fn: Callable[[Dremel3DPrinter], bool] -@dataclass(frozen=True) -class Dremel3DPrinterBinarySensorEntityDescription( - BinarySensorEntityDescription, Dremel3DPrinterBinarySensorEntityMixin -): - """Describes a Dremel 3D Printer binary sensor.""" - - BINARY_SENSOR_TYPES: tuple[Dremel3DPrinterBinarySensorEntityDescription, ...] = ( Dremel3DPrinterBinarySensorEntityDescription( key="door", diff --git a/homeassistant/components/dremel_3d_printer/button.py b/homeassistant/components/dremel_3d_printer/button.py index b2ea103f78b..d92263b6a15 100644 --- a/homeassistant/components/dremel_3d_printer/button.py +++ b/homeassistant/components/dremel_3d_printer/button.py @@ -1,4 +1,5 @@ """Support for Dremel 3D Printer buttons.""" + from __future__ import annotations from collections.abc import Callable @@ -16,20 +17,13 @@ from .const import DOMAIN from .entity import Dremel3DPrinterEntity -@dataclass(frozen=True) -class Dremel3DPrinterButtonEntityMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class Dremel3DPrinterButtonEntityDescription(ButtonEntityDescription): + """Describes a Dremel 3D Printer button entity.""" press_fn: Callable[[Dremel3DPrinter], None] -@dataclass(frozen=True) -class Dremel3DPrinterButtonEntityDescription( - ButtonEntityDescription, Dremel3DPrinterButtonEntityMixin -): - """Describes a Dremel 3D Printer button entity.""" - - BUTTON_TYPES: tuple[Dremel3DPrinterButtonEntityDescription, ...] = ( Dremel3DPrinterButtonEntityDescription( key="cancel_job", diff --git a/homeassistant/components/dremel_3d_printer/camera.py b/homeassistant/components/dremel_3d_printer/camera.py index 7468400ec35..dc663844c9c 100644 --- a/homeassistant/components/dremel_3d_printer/camera.py +++ b/homeassistant/components/dremel_3d_printer/camera.py @@ -1,4 +1,5 @@ """Support for Dremel 3D45 Camera.""" + from __future__ import annotations from homeassistant.components.camera import CameraEntityDescription diff --git a/homeassistant/components/dremel_3d_printer/config_flow.py b/homeassistant/components/dremel_3d_printer/config_flow.py index 6fa4d2e0a5b..aa4cdb045e7 100644 --- a/homeassistant/components/dremel_3d_printer/config_flow.py +++ b/homeassistant/components/dremel_3d_printer/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Dremel 3D Printer (3D20, 3D40, 3D45).""" + from __future__ import annotations from json.decoder import JSONDecodeError @@ -8,9 +9,8 @@ from dremel3dpy import Dremel3DPrinter from requests.exceptions import ConnectTimeout, HTTPError import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST -from homeassistant.data_entry_flow import FlowResult import homeassistant.helpers.config_validation as cv from .const import DOMAIN, LOGGER @@ -20,14 +20,14 @@ def _schema_with_defaults(host: str = "") -> vol.Schema: return vol.Schema({vol.Required(CONF_HOST, default=host): cv.string}) -class Dremel3DPrinterConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class Dremel3DPrinterConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Dremel 3D Printer.""" VERSION = 1 async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors = {} diff --git a/homeassistant/components/dremel_3d_printer/const.py b/homeassistant/components/dremel_3d_printer/const.py index cccdeb937cb..f060daf0d57 100644 --- a/homeassistant/components/dremel_3d_printer/const.py +++ b/homeassistant/components/dremel_3d_printer/const.py @@ -1,4 +1,5 @@ """Constants for the Dremel 3D Printer (3D20, 3D40, 3D45) integration.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/dremel_3d_printer/sensor.py b/homeassistant/components/dremel_3d_printer/sensor.py index 98e4cd0e85d..bda2bb537fd 100644 --- a/homeassistant/components/dremel_3d_printer/sensor.py +++ b/homeassistant/components/dremel_3d_printer/sensor.py @@ -1,4 +1,5 @@ """Support for monitoring Dremel 3D Printer sensors.""" + from __future__ import annotations from collections.abc import Callable @@ -31,19 +32,11 @@ from .const import ATTR_EXTRUDER, ATTR_PLATFORM, DOMAIN from .entity import Dremel3DPrinterEntity -@dataclass(frozen=True) -class Dremel3DPrinterSensorEntityMixin: - """Mixin for Dremel 3D Printer sensor.""" - - value_fn: Callable[[Dremel3DPrinter, str], StateType | datetime] - - -@dataclass(frozen=True) -class Dremel3DPrinterSensorEntityDescription( - SensorEntityDescription, Dremel3DPrinterSensorEntityMixin -): +@dataclass(frozen=True, kw_only=True) +class Dremel3DPrinterSensorEntityDescription(SensorEntityDescription): """Describes a Dremel 3D Printer sensor.""" + value_fn: Callable[[Dremel3DPrinter, str], StateType | datetime] available_fn: Callable[[Dremel3DPrinter, str], bool] = lambda api, _: True diff --git a/homeassistant/components/drop_connect/__init__.py b/homeassistant/components/drop_connect/__init__.py index 7bfab762f99..bc700456398 100644 --- a/homeassistant/components/drop_connect/__init__.py +++ b/homeassistant/components/drop_connect/__init__.py @@ -1,4 +1,5 @@ """The drop_connect integration.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/drop_connect/config_flow.py b/homeassistant/components/drop_connect/config_flow.py index a2b93ad1da1..476b244f345 100644 --- a/homeassistant/components/drop_connect/config_flow.py +++ b/homeassistant/components/drop_connect/config_flow.py @@ -1,4 +1,5 @@ """Config flow for drop_connect integration.""" + from __future__ import annotations import logging @@ -6,8 +7,7 @@ from typing import TYPE_CHECKING, Any from dropmqttapi.discovery import DropDiscovery -from homeassistant import config_entries -from homeassistant.data_entry_flow import FlowResult +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.helpers.service_info.mqtt import MqttServiceInfo from .const import ( @@ -26,14 +26,16 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class FlowHandler(ConfigFlow, domain=DOMAIN): """Handle DROP config flow.""" VERSION = 1 _drop_discovery: DropDiscovery | None = None - async def async_step_mqtt(self, discovery_info: MqttServiceInfo) -> FlowResult: + async def async_step_mqtt( + self, discovery_info: MqttServiceInfo + ) -> ConfigFlowResult: """Handle a flow initialized by MQTT discovery.""" # Abort if the topic does not match our discovery topic or the payload is empty. @@ -64,7 +66,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm the setup.""" if TYPE_CHECKING: assert self._drop_discovery is not None @@ -93,6 +95,6 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" return self.async_abort(reason="not_supported") diff --git a/homeassistant/components/drop_connect/coordinator.py b/homeassistant/components/drop_connect/coordinator.py index e4937ed5f65..0861e091153 100644 --- a/homeassistant/components/drop_connect/coordinator.py +++ b/homeassistant/components/drop_connect/coordinator.py @@ -1,4 +1,5 @@ """DROP device data update coordinator object.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/drop_connect/entity.py b/homeassistant/components/drop_connect/entity.py index 85c506b19a3..459552e8511 100644 --- a/homeassistant/components/drop_connect/entity.py +++ b/homeassistant/components/drop_connect/entity.py @@ -1,4 +1,5 @@ """Base entity class for DROP entities.""" + from __future__ import annotations from typing import TYPE_CHECKING diff --git a/homeassistant/components/drop_connect/sensor.py b/homeassistant/components/drop_connect/sensor.py index c9450440473..0806737254e 100644 --- a/homeassistant/components/drop_connect/sensor.py +++ b/homeassistant/components/drop_connect/sensor.py @@ -1,4 +1,5 @@ """Support for DROP sensors.""" + from __future__ import annotations from collections.abc import Callable @@ -19,6 +20,7 @@ from homeassistant.const import ( UnitOfPressure, UnitOfTemperature, UnitOfVolume, + UnitOfVolumeFlowRate, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -69,7 +71,8 @@ SENSORS: list[DROPSensorEntityDescription] = [ DROPSensorEntityDescription( key=CURRENT_FLOW_RATE, translation_key=CURRENT_FLOW_RATE, - native_unit_of_measurement="gpm", + device_class=SensorDeviceClass.VOLUME_FLOW_RATE, + native_unit_of_measurement=UnitOfVolumeFlowRate.GALLONS_PER_MINUTE, suggested_display_precision=1, value_fn=lambda device: device.drop_api.current_flow_rate(), state_class=SensorStateClass.MEASUREMENT, @@ -77,7 +80,8 @@ SENSORS: list[DROPSensorEntityDescription] = [ DROPSensorEntityDescription( key=PEAK_FLOW_RATE, translation_key=PEAK_FLOW_RATE, - native_unit_of_measurement="gpm", + device_class=SensorDeviceClass.VOLUME_FLOW_RATE, + native_unit_of_measurement=UnitOfVolumeFlowRate.GALLONS_PER_MINUTE, suggested_display_precision=1, value_fn=lambda device: device.drop_api.peak_flow_rate(), state_class=SensorStateClass.MEASUREMENT, diff --git a/homeassistant/components/dsmr/__init__.py b/homeassistant/components/dsmr/__init__.py index f3546fc0a00..00252b98517 100644 --- a/homeassistant/components/dsmr/__init__.py +++ b/homeassistant/components/dsmr/__init__.py @@ -1,4 +1,5 @@ """The dsmr component.""" + from __future__ import annotations from asyncio import CancelledError diff --git a/homeassistant/components/dsmr/config_flow.py b/homeassistant/components/dsmr/config_flow.py index a38326c1346..49e1818edcc 100644 --- a/homeassistant/components/dsmr/config_flow.py +++ b/homeassistant/components/dsmr/config_flow.py @@ -1,4 +1,5 @@ """Config flow for DSMR integration.""" + from __future__ import annotations import asyncio @@ -17,11 +18,15 @@ import serial import serial.tools.list_ports import voluptuous as vol -from homeassistant import config_entries, core, exceptions -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.const import CONF_HOST, CONF_PORT, CONF_PROTOCOL, CONF_TYPE -from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError from .const import ( CONF_DSMR_VERSION, @@ -75,7 +80,7 @@ class DSMRConnection: return identifier return None - async def validate_connect(self, hass: core.HomeAssistant) -> bool: + async def validate_connect(self, hass: HomeAssistant) -> bool: """Test if we can validate connection with the device.""" def update_telegram(telegram: dict[str, DSMRObject]) -> None: @@ -131,7 +136,7 @@ class DSMRConnection: async def _validate_dsmr_connection( - hass: core.HomeAssistant, data: dict[str, Any], protocol: str + hass: HomeAssistant, data: dict[str, Any], protocol: str ) -> dict[str, str | None]: """Validate the user input allows us to connect.""" conn = DSMRConnection( @@ -157,7 +162,7 @@ async def _validate_dsmr_connection( } -class DSMRFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class DSMRFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a config flow for DSMR.""" VERSION = 1 @@ -172,7 +177,7 @@ class DSMRFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Step when user initializes a integration.""" if user_input is not None: user_selection = user_input[CONF_TYPE] @@ -188,7 +193,7 @@ class DSMRFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_setup_network( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Step when setting up network configuration.""" errors: dict[str, str] = {} if user_input is not None: @@ -213,7 +218,7 @@ class DSMRFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_setup_serial( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Step when setting up serial configuration.""" errors: dict[str, str] = {} if user_input is not None: @@ -257,7 +262,7 @@ class DSMRFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_setup_serial_manual_path( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Select path manually.""" if user_input is not None: validate_data = { @@ -303,7 +308,7 @@ class DSMRFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return data -class DSMROptionFlowHandler(config_entries.OptionsFlow): +class DSMROptionFlowHandler(OptionsFlow): """Handle options.""" def __init__(self, entry: ConfigEntry) -> None: @@ -312,7 +317,7 @@ class DSMROptionFlowHandler(config_entries.OptionsFlow): async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Manage the options.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) @@ -344,9 +349,9 @@ def get_serial_by_id(dev_path: str) -> str: return dev_path -class CannotConnect(exceptions.HomeAssistantError): +class CannotConnect(HomeAssistantError): """Error to indicate we cannot connect.""" -class CannotCommunicate(exceptions.HomeAssistantError): +class CannotCommunicate(HomeAssistantError): """Error to indicate we cannot connect.""" diff --git a/homeassistant/components/dsmr/const.py b/homeassistant/components/dsmr/const.py index 9504929c5a9..d40581bcdee 100644 --- a/homeassistant/components/dsmr/const.py +++ b/homeassistant/components/dsmr/const.py @@ -1,4 +1,5 @@ """Constants for the DSMR integration.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index ad1c4e64c55..7b2e916529a 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -1,4 +1,5 @@ """Support for Dutch Smart Meter (also known as Smartmeter or P1 port).""" + from __future__ import annotations import asyncio @@ -530,9 +531,7 @@ async def async_setup_entry( add_entities_handler = None if dsmr_version == "5B": - mbus_entities = create_mbus_entities(hass, telegram, entry) - for mbus_entity in mbus_entities: - entities.append(mbus_entity) + entities.extend(create_mbus_entities(hass, telegram, entry)) entities.extend( [ diff --git a/homeassistant/components/dsmr_reader/config_flow.py b/homeassistant/components/dsmr_reader/config_flow.py index 44ff6663654..4f2485ec647 100644 --- a/homeassistant/components/dsmr_reader/config_flow.py +++ b/homeassistant/components/dsmr_reader/config_flow.py @@ -1,11 +1,12 @@ """Config flow to configure DSMR Reader.""" + from __future__ import annotations from collections.abc import Awaitable from typing import Any +from homeassistant.config_entries import ConfigFlowResult from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.config_entry_flow import DiscoveryFlowHandler from .const import DOMAIN @@ -27,7 +28,7 @@ class DsmrReaderFlowHandler(DiscoveryFlowHandler[Awaitable[bool]], domain=DOMAIN async def async_step_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm setup.""" if user_input is None: return self.async_show_form( diff --git a/homeassistant/components/dsmr_reader/definitions.py b/homeassistant/components/dsmr_reader/definitions.py index 2b5b995eabd..901dfc047f5 100644 --- a/homeassistant/components/dsmr_reader/definitions.py +++ b/homeassistant/components/dsmr_reader/definitions.py @@ -1,4 +1,5 @@ """Definitions for DSMR Reader sensors added to MQTT.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/dsmr_reader/manifest.json b/homeassistant/components/dsmr_reader/manifest.json index cec0dd941fb..35dc21384bd 100644 --- a/homeassistant/components/dsmr_reader/manifest.json +++ b/homeassistant/components/dsmr_reader/manifest.json @@ -1,7 +1,7 @@ { "domain": "dsmr_reader", "name": "DSMR Reader", - "codeowners": ["@depl0y", "@glodenox"], + "codeowners": ["@sorted-bits", "@glodenox"], "config_flow": true, "dependencies": ["mqtt"], "documentation": "https://www.home-assistant.io/integrations/dsmr_reader", diff --git a/homeassistant/components/dsmr_reader/sensor.py b/homeassistant/components/dsmr_reader/sensor.py index c618995ed45..3c07ad65de6 100644 --- a/homeassistant/components/dsmr_reader/sensor.py +++ b/homeassistant/components/dsmr_reader/sensor.py @@ -1,4 +1,5 @@ """Support for DSMR Reader through MQTT.""" + from __future__ import annotations from homeassistant.components import mqtt diff --git a/homeassistant/components/dte_energy_bridge/sensor.py b/homeassistant/components/dte_energy_bridge/sensor.py index 34c78ed824b..c33bb37e468 100644 --- a/homeassistant/components/dte_energy_bridge/sensor.py +++ b/homeassistant/components/dte_energy_bridge/sensor.py @@ -1,4 +1,5 @@ """Support for monitoring energy usage using the DTE energy bridge.""" + from __future__ import annotations from http import HTTPStatus diff --git a/homeassistant/components/dublin_bus_transport/sensor.py b/homeassistant/components/dublin_bus_transport/sensor.py index b50bd604763..3f9c57456f8 100644 --- a/homeassistant/components/dublin_bus_transport/sensor.py +++ b/homeassistant/components/dublin_bus_transport/sensor.py @@ -3,6 +3,7 @@ For more info on the API see : https://data.gov.ie/dataset/real-time-passenger-information-rtpi-for-dublin-bus-bus-eireann-luas-and-irish-rail/resource/4b9f2c4f-6bf5-4958-a43a-f12dab04cf61 """ + from __future__ import annotations from contextlib import suppress diff --git a/homeassistant/components/duckdns/__init__.py b/homeassistant/components/duckdns/__init__.py index c0c3b14566c..557178de571 100644 --- a/homeassistant/components/duckdns/__init__.py +++ b/homeassistant/components/duckdns/__init__.py @@ -1,4 +1,5 @@ """Integrate with DuckDNS.""" + from __future__ import annotations from collections.abc import Callable, Coroutine, Sequence @@ -141,7 +142,7 @@ def async_track_time_interval_backoff( ) interval_listener_job = HassJob(interval_listener, cancel_on_shutdown=True) - hass.async_run_job(interval_listener, dt_util.utcnow()) + hass.async_run_hass_job(interval_listener_job, dt_util.utcnow()) def remove_listener() -> None: """Remove interval listener.""" diff --git a/homeassistant/components/duckdns/icons.json b/homeassistant/components/duckdns/icons.json new file mode 100644 index 00000000000..79ec18d13ff --- /dev/null +++ b/homeassistant/components/duckdns/icons.json @@ -0,0 +1,5 @@ +{ + "services": { + "set_txt": "mdi:text-box-edit-outline" + } +} diff --git a/homeassistant/components/dunehd/__init__.py b/homeassistant/components/dunehd/__init__.py index c97e27f4017..27e9e749472 100644 --- a/homeassistant/components/dunehd/__init__.py +++ b/homeassistant/components/dunehd/__init__.py @@ -1,4 +1,5 @@ """The Dune HD component.""" + from __future__ import annotations from typing import Final diff --git a/homeassistant/components/dunehd/config_flow.py b/homeassistant/components/dunehd/config_flow.py index fac2e245633..8a0f3eec4a0 100644 --- a/homeassistant/components/dunehd/config_flow.py +++ b/homeassistant/components/dunehd/config_flow.py @@ -1,4 +1,5 @@ """Adds config flow for Dune HD integration.""" + from __future__ import annotations from typing import Any @@ -6,15 +7,15 @@ from typing import Any from pdunehd import DuneHDPlayer import voluptuous as vol -from homeassistant import config_entries, exceptions +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST -from homeassistant.data_entry_flow import FlowResult +from homeassistant.exceptions import HomeAssistantError from homeassistant.util.network import is_host_valid from .const import DOMAIN -class DuneHDConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class DuneHDConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Dune HD integration.""" VERSION = 1 @@ -24,11 +25,11 @@ class DuneHDConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): player = DuneHDPlayer(host) state = await self.hass.async_add_executor_job(player.update_state) if not state: - raise CannotConnect() + raise CannotConnect async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors = {} @@ -38,7 +39,7 @@ class DuneHDConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): try: if self.host_already_configured(host): - raise AlreadyConfigured() + raise AlreadyConfigured await self.init_device(host) except CannotConnect: errors[CONF_HOST] = "cannot_connect" @@ -63,9 +64,9 @@ class DuneHDConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return host in existing_hosts -class CannotConnect(exceptions.HomeAssistantError): +class CannotConnect(HomeAssistantError): """Error to indicate we cannot connect.""" -class AlreadyConfigured(exceptions.HomeAssistantError): +class AlreadyConfigured(HomeAssistantError): """Error to indicate device is already configured.""" diff --git a/homeassistant/components/dunehd/const.py b/homeassistant/components/dunehd/const.py index 1cc89cf2028..b4aa34ee72c 100644 --- a/homeassistant/components/dunehd/const.py +++ b/homeassistant/components/dunehd/const.py @@ -1,4 +1,5 @@ """Constants for Dune HD integration.""" + from __future__ import annotations from typing import Final diff --git a/homeassistant/components/dunehd/media_player.py b/homeassistant/components/dunehd/media_player.py index ff7f78d537b..ded23ea4669 100644 --- a/homeassistant/components/dunehd/media_player.py +++ b/homeassistant/components/dunehd/media_player.py @@ -1,4 +1,5 @@ """Dune HD implementation of the media player.""" + from __future__ import annotations from typing import Any, Final diff --git a/homeassistant/components/duotecno/__init__.py b/homeassistant/components/duotecno/__init__.py index 288210c7280..1873db45226 100644 --- a/homeassistant/components/duotecno/__init__.py +++ b/homeassistant/components/duotecno/__init__.py @@ -1,4 +1,5 @@ """The duotecno integration.""" + from __future__ import annotations from duotecno.controller import PyDuotecno diff --git a/homeassistant/components/duotecno/binary_sensor.py b/homeassistant/components/duotecno/binary_sensor.py index 60578adf6a7..10c807a8023 100644 --- a/homeassistant/components/duotecno/binary_sensor.py +++ b/homeassistant/components/duotecno/binary_sensor.py @@ -1,4 +1,5 @@ """Support for Duotecno binary sensors.""" + from __future__ import annotations from duotecno.controller import PyDuotecno diff --git a/homeassistant/components/duotecno/climate.py b/homeassistant/components/duotecno/climate.py index 3df80721af4..77b602c8716 100644 --- a/homeassistant/components/duotecno/climate.py +++ b/homeassistant/components/duotecno/climate.py @@ -1,4 +1,5 @@ """Support for Duotecno climate devices.""" + from __future__ import annotations from typing import Any, Final diff --git a/homeassistant/components/duotecno/config_flow.py b/homeassistant/components/duotecno/config_flow.py index 6f08b025835..44675d6bbde 100644 --- a/homeassistant/components/duotecno/config_flow.py +++ b/homeassistant/components/duotecno/config_flow.py @@ -1,4 +1,5 @@ """Config flow for duotecno integration.""" + from __future__ import annotations import logging @@ -8,9 +9,8 @@ from duotecno.controller import PyDuotecno from duotecno.exceptions import InvalidPassword import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT -from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN @@ -25,14 +25,14 @@ STEP_USER_DATA_SCHEMA = vol.Schema( ) -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class DuoTecnoConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for duotecno.""" VERSION = 1 async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") diff --git a/homeassistant/components/duotecno/const.py b/homeassistant/components/duotecno/const.py index 6bffe2358e1..964e0e91d1b 100644 --- a/homeassistant/components/duotecno/const.py +++ b/homeassistant/components/duotecno/const.py @@ -1,4 +1,5 @@ """Constants for the duotecno integration.""" + from typing import Final DOMAIN: Final = "duotecno" diff --git a/homeassistant/components/duotecno/cover.py b/homeassistant/components/duotecno/cover.py index b8802c77304..1c4f7d70fc5 100644 --- a/homeassistant/components/duotecno/cover.py +++ b/homeassistant/components/duotecno/cover.py @@ -1,4 +1,5 @@ """Support for Velbus covers.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/duotecno/entity.py b/homeassistant/components/duotecno/entity.py index 85566b3ebad..86f61c8a73c 100644 --- a/homeassistant/components/duotecno/entity.py +++ b/homeassistant/components/duotecno/entity.py @@ -1,4 +1,5 @@ """Support for Velbus devices.""" + from __future__ import annotations from collections.abc import Awaitable, Callable, Coroutine diff --git a/homeassistant/components/duotecno/light.py b/homeassistant/components/duotecno/light.py index 851dd64bfb2..57635ac2bc2 100644 --- a/homeassistant/components/duotecno/light.py +++ b/homeassistant/components/duotecno/light.py @@ -1,4 +1,5 @@ """Support for Duotecno lights.""" + from typing import Any from duotecno.controller import PyDuotecno diff --git a/homeassistant/components/duotecno/manifest.json b/homeassistant/components/duotecno/manifest.json index 7b33784a612..0c8eab8f0a0 100644 --- a/homeassistant/components/duotecno/manifest.json +++ b/homeassistant/components/duotecno/manifest.json @@ -7,5 +7,5 @@ "iot_class": "local_push", "loggers": ["pyduotecno", "pyduotecno-node", "pyduotecno-unit"], "quality_scale": "silver", - "requirements": ["pyDuotecno==2024.1.2"] + "requirements": ["pyDuotecno==2024.3.2"] } diff --git a/homeassistant/components/duotecno/switch.py b/homeassistant/components/duotecno/switch.py index d43f82fc657..b3a87786d4e 100644 --- a/homeassistant/components/duotecno/switch.py +++ b/homeassistant/components/duotecno/switch.py @@ -1,4 +1,5 @@ """Support for Duotecno switches.""" + from typing import Any from duotecno.controller import PyDuotecno diff --git a/homeassistant/components/dwd_weather_warnings/config_flow.py b/homeassistant/components/dwd_weather_warnings/config_flow.py index 1e0bb797c7a..5076dbae187 100644 --- a/homeassistant/components/dwd_weather_warnings/config_flow.py +++ b/homeassistant/components/dwd_weather_warnings/config_flow.py @@ -7,8 +7,7 @@ from typing import Any from dwdwfsapi import DwdWeatherWarningsAPI import voluptuous as vol -from homeassistant.config_entries import ConfigFlow -from homeassistant.data_entry_flow import FlowResult +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult import homeassistant.helpers.config_validation as cv from .const import CONF_REGION_IDENTIFIER, DOMAIN @@ -21,7 +20,7 @@ class DwdWeatherWarningsConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors: dict = {} diff --git a/homeassistant/components/dweet/__init__.py b/homeassistant/components/dweet/__init__.py index eb809ff47df..c1232bab2cf 100644 --- a/homeassistant/components/dweet/__init__.py +++ b/homeassistant/components/dweet/__init__.py @@ -1,4 +1,5 @@ """Support for sending data to Dweet.io.""" + from datetime import timedelta import logging diff --git a/homeassistant/components/dweet/sensor.py b/homeassistant/components/dweet/sensor.py index 8a1b5a1bc6c..79e25bec0c1 100644 --- a/homeassistant/components/dweet/sensor.py +++ b/homeassistant/components/dweet/sensor.py @@ -1,4 +1,5 @@ """Support for showing values from Dweet.io.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/dynalite/__init__.py b/homeassistant/components/dynalite/__init__.py index 77880fd74cb..46fcfb267d0 100644 --- a/homeassistant/components/dynalite/__init__.py +++ b/homeassistant/components/dynalite/__init__.py @@ -1,4 +1,5 @@ """Support for the Dynalite networks.""" + from __future__ import annotations import voluptuous as vol @@ -64,10 +65,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def dynalite_service(service_call: ServiceCall) -> None: data = service_call.data host = data.get(ATTR_HOST, "") - bridges = [] - for cur_bridge in hass.data[DOMAIN].values(): - if not host or cur_bridge.host == host: - bridges.append(cur_bridge) + bridges = [ + bridge + for bridge in hass.data[DOMAIN].values() + if not host or bridge.host == host + ] LOGGER.debug("Selected bridged for service call: %s", bridges) if service_call.service == SERVICE_REQUEST_AREA_PRESET: bridge_attr = "request_area_preset" diff --git a/homeassistant/components/dynalite/bridge.py b/homeassistant/components/dynalite/bridge.py index 82666f20a40..2245364b0b7 100644 --- a/homeassistant/components/dynalite/bridge.py +++ b/homeassistant/components/dynalite/bridge.py @@ -1,4 +1,5 @@ """Code to handle a Dynalite bridge.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/dynalite/config_flow.py b/homeassistant/components/dynalite/config_flow.py index 7cced80c97e..3ae4828b668 100644 --- a/homeassistant/components/dynalite/config_flow.py +++ b/homeassistant/components/dynalite/config_flow.py @@ -1,14 +1,14 @@ """Config flow to configure Dynalite hub.""" + from __future__ import annotations from typing import Any import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_validation as cv from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue @@ -17,7 +17,7 @@ from .const import DEFAULT_PORT, DOMAIN, LOGGER from .convert_config import convert_config -class DynaliteFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class DynaliteFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a Dynalite config flow.""" VERSION = 1 @@ -26,7 +26,7 @@ class DynaliteFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Initialize the Dynalite flow.""" self.host = None - async def async_step_import(self, import_info: dict[str, Any]) -> FlowResult: + async def async_step_import(self, import_info: dict[str, Any]) -> ConfigFlowResult: """Import a new bridge as a config entry.""" LOGGER.debug("Starting async_step_import (deprecated) - %s", import_info) # Raise an issue that this is deprecated and has been imported @@ -60,7 +60,7 @@ class DynaliteFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Step when user initializes a integration.""" if user_input is not None: return await self._try_create(user_input) @@ -73,7 +73,7 @@ class DynaliteFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) return self.async_show_form(step_id="user", data_schema=schema) - async def _try_create(self, info: dict[str, Any]) -> FlowResult: + async def _try_create(self, info: dict[str, Any]) -> ConfigFlowResult: """Try to connect and if successful, create entry.""" host = info[CONF_HOST] configured_hosts = [ diff --git a/homeassistant/components/dynalite/const.py b/homeassistant/components/dynalite/const.py index f46719febb1..c1cb1a0fb1b 100644 --- a/homeassistant/components/dynalite/const.py +++ b/homeassistant/components/dynalite/const.py @@ -1,4 +1,5 @@ """Constants for the Dynalite component.""" + import logging from homeassistant.const import CONF_ROOM, Platform diff --git a/homeassistant/components/dynalite/convert_config.py b/homeassistant/components/dynalite/convert_config.py index 25d18dd92e8..00edc26f1ab 100644 --- a/homeassistant/components/dynalite/convert_config.py +++ b/homeassistant/components/dynalite/convert_config.py @@ -1,4 +1,5 @@ """Convert the HA config to the dynalite config.""" + from __future__ import annotations from types import MappingProxyType diff --git a/homeassistant/components/dynalite/dynalitebase.py b/homeassistant/components/dynalite/dynalitebase.py index baf4c12a4c5..bfc62609101 100644 --- a/homeassistant/components/dynalite/dynalitebase.py +++ b/homeassistant/components/dynalite/dynalitebase.py @@ -1,4 +1,5 @@ """Support for the Dynalite devices as entities.""" + from __future__ import annotations from abc import ABC, abstractmethod @@ -30,9 +31,7 @@ def async_setup_entry_base( @callback def async_add_entities_platform(devices): # assumes it is called with a single platform - added_entities = [] - for device in devices: - added_entities.append(entity_from_device(device, bridge)) + added_entities = [entity_from_device(device, bridge) for device in devices] async_add_entities(added_entities) bridge.register_add_devices(platform, async_add_entities_platform) diff --git a/homeassistant/components/eafm/__init__.py b/homeassistant/components/eafm/__init__.py index d18553b6ee6..1f95437484f 100644 --- a/homeassistant/components/eafm/__init__.py +++ b/homeassistant/components/eafm/__init__.py @@ -1,4 +1,5 @@ """UK Environment Agency Flood Monitoring Integration.""" + import asyncio from datetime import timedelta import logging diff --git a/homeassistant/components/eafm/config_flow.py b/homeassistant/components/eafm/config_flow.py index 75e5a62a2a0..0345d2acf94 100644 --- a/homeassistant/components/eafm/config_flow.py +++ b/homeassistant/components/eafm/config_flow.py @@ -1,14 +1,15 @@ """Config flow to configure flood monitoring gauges.""" + from aioeafm import get_stations import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN -class UKFloodsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class UKFloodsFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a UK Environment Agency flood monitoring config flow.""" VERSION = 1 diff --git a/homeassistant/components/easyenergy/__init__.py b/homeassistant/components/easyenergy/__init__.py index e941c78b1fb..e520631158a 100644 --- a/homeassistant/components/easyenergy/__init__.py +++ b/homeassistant/components/easyenergy/__init__.py @@ -1,4 +1,5 @@ """The easyEnergy integration.""" + from __future__ import annotations from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/easyenergy/config_flow.py b/homeassistant/components/easyenergy/config_flow.py index a8c23f2c6e2..07e94060b74 100644 --- a/homeassistant/components/easyenergy/config_flow.py +++ b/homeassistant/components/easyenergy/config_flow.py @@ -1,10 +1,10 @@ """Config flow for easyEnergy integration.""" + from __future__ import annotations from typing import Any -from homeassistant.config_entries import ConfigFlow -from homeassistant.data_entry_flow import FlowResult +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from .const import DOMAIN @@ -16,7 +16,7 @@ class EasyEnergyFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" await self.async_set_unique_id(DOMAIN) diff --git a/homeassistant/components/easyenergy/const.py b/homeassistant/components/easyenergy/const.py index 1de7ac0bd58..4670e9c4edd 100644 --- a/homeassistant/components/easyenergy/const.py +++ b/homeassistant/components/easyenergy/const.py @@ -1,4 +1,5 @@ """Constants for the easyEnergy integration.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/easyenergy/coordinator.py b/homeassistant/components/easyenergy/coordinator.py index 3996fd4d16a..8c1c593af93 100644 --- a/homeassistant/components/easyenergy/coordinator.py +++ b/homeassistant/components/easyenergy/coordinator.py @@ -1,4 +1,5 @@ """The Coordinator for easyEnergy.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/easyenergy/diagnostics.py b/homeassistant/components/easyenergy/diagnostics.py index 0c885174872..d6912e1c926 100644 --- a/homeassistant/components/easyenergy/diagnostics.py +++ b/homeassistant/components/easyenergy/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for easyEnergy.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/easyenergy/sensor.py b/homeassistant/components/easyenergy/sensor.py index d719eac17af..65fe2558d46 100644 --- a/homeassistant/components/easyenergy/sensor.py +++ b/homeassistant/components/easyenergy/sensor.py @@ -1,4 +1,5 @@ """Support for easyEnergy sensors.""" + from __future__ import annotations from collections.abc import Callable @@ -29,21 +30,14 @@ from .const import DOMAIN, SERVICE_TYPE_DEVICE_NAMES from .coordinator import EasyEnergyData, EasyEnergyDataUpdateCoordinator -@dataclass(frozen=True) -class EasyEnergySensorEntityDescriptionMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class EasyEnergySensorEntityDescription(SensorEntityDescription): + """Describes easyEnergy sensor entity.""" value_fn: Callable[[EasyEnergyData], float | datetime | None] service_type: str -@dataclass(frozen=True) -class EasyEnergySensorEntityDescription( - SensorEntityDescription, EasyEnergySensorEntityDescriptionMixin -): - """Describes easyEnergy sensor entity.""" - - SENSORS: tuple[EasyEnergySensorEntityDescription, ...] = ( EasyEnergySensorEntityDescription( key="current_hour_price", diff --git a/homeassistant/components/easyenergy/services.py b/homeassistant/components/easyenergy/services.py index 95763e5db25..5b80cfafd08 100644 --- a/homeassistant/components/easyenergy/services.py +++ b/homeassistant/components/easyenergy/services.py @@ -1,4 +1,5 @@ """Services for easyEnergy integration.""" + from __future__ import annotations from datetime import date, datetime @@ -94,7 +95,6 @@ def __get_coordinator( if not entry: raise ServiceValidationError( - f"Invalid config entry: {entry_id}", translation_domain=DOMAIN, translation_key="invalid_config_entry", translation_placeholders={ @@ -103,7 +103,6 @@ def __get_coordinator( ) if entry.state != ConfigEntryState.LOADED: raise ServiceValidationError( - f"{entry.title} is not loaded", translation_domain=DOMAIN, translation_key="unloaded_config_entry", translation_placeholders={ diff --git a/homeassistant/components/ebox/sensor.py b/homeassistant/components/ebox/sensor.py index aaa8c5ceb0c..aff154cca02 100644 --- a/homeassistant/components/ebox/sensor.py +++ b/homeassistant/components/ebox/sensor.py @@ -2,6 +2,7 @@ Get data from 'My Usage Page' page: https://client.ebox.ca/myusage """ + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/ebusd/__init__.py b/homeassistant/components/ebusd/__init__.py index b1eb03989ea..debfc335496 100644 --- a/homeassistant/components/ebusd/__init__.py +++ b/homeassistant/components/ebusd/__init__.py @@ -1,4 +1,5 @@ """Support for Ebusd daemon for communication with eBUS heating systems.""" + import logging import ebusdpy diff --git a/homeassistant/components/ebusd/const.py b/homeassistant/components/ebusd/const.py index 9bc489f40f2..4fb3032e19b 100644 --- a/homeassistant/components/ebusd/const.py +++ b/homeassistant/components/ebusd/const.py @@ -1,4 +1,5 @@ """Constants for ebus component.""" + from homeassistant.components.sensor import SensorDeviceClass from homeassistant.const import ( PERCENTAGE, diff --git a/homeassistant/components/ebusd/icons.json b/homeassistant/components/ebusd/icons.json new file mode 100644 index 00000000000..642be37a43b --- /dev/null +++ b/homeassistant/components/ebusd/icons.json @@ -0,0 +1,5 @@ +{ + "services": { + "write": "mdi:pencil" + } +} diff --git a/homeassistant/components/ebusd/sensor.py b/homeassistant/components/ebusd/sensor.py index 923f94f705d..2eaaddf7e2f 100644 --- a/homeassistant/components/ebusd/sensor.py +++ b/homeassistant/components/ebusd/sensor.py @@ -1,4 +1,5 @@ """Support for Ebusd sensors.""" + from __future__ import annotations import datetime @@ -37,13 +38,13 @@ def setup_platform( monitored_conditions = discovery_info["monitored_conditions"] name = discovery_info["client_name"] - dev = [] - for condition in monitored_conditions: - dev.append( + add_entities( + ( EbusdSensor(ebusd_api, discovery_info["sensor_types"][condition], name) - ) - - add_entities(dev, True) + for condition in monitored_conditions + ), + True, + ) class EbusdSensor(SensorEntity): diff --git a/homeassistant/components/ecoal_boiler/__init__.py b/homeassistant/components/ecoal_boiler/__init__.py index 4db8a3ab6cc..e9b519c7095 100644 --- a/homeassistant/components/ecoal_boiler/__init__.py +++ b/homeassistant/components/ecoal_boiler/__init__.py @@ -1,4 +1,5 @@ """Support to control ecoal/esterownik.pl coal/wood boiler controller.""" + import logging from ecoaliface.simple import ECoalController diff --git a/homeassistant/components/ecoal_boiler/sensor.py b/homeassistant/components/ecoal_boiler/sensor.py index 06dfec9ff01..4ce52d283fc 100644 --- a/homeassistant/components/ecoal_boiler/sensor.py +++ b/homeassistant/components/ecoal_boiler/sensor.py @@ -1,4 +1,5 @@ """Allows reading temperatures from ecoal/esterownik.pl controller.""" + from __future__ import annotations from homeassistant.components.sensor import SensorDeviceClass, SensorEntity diff --git a/homeassistant/components/ecoal_boiler/switch.py b/homeassistant/components/ecoal_boiler/switch.py index b2b46beb26b..7fede88bc2b 100644 --- a/homeassistant/components/ecoal_boiler/switch.py +++ b/homeassistant/components/ecoal_boiler/switch.py @@ -1,4 +1,5 @@ """Allows to configuration ecoal (esterownik.pl) pumps as switches.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/ecobee/__init__.py b/homeassistant/components/ecobee/__init__.py index 962eebc2a33..8083d0efcb4 100644 --- a/homeassistant/components/ecobee/__init__.py +++ b/homeassistant/components/ecobee/__init__.py @@ -1,4 +1,5 @@ """Support for ecobee.""" + from datetime import timedelta from pyecobee import ECOBEE_API_KEY, ECOBEE_REFRESH_TOKEN, Ecobee, ExpiredTokenError diff --git a/homeassistant/components/ecobee/binary_sensor.py b/homeassistant/components/ecobee/binary_sensor.py index 4ad0190e01a..18e09178581 100644 --- a/homeassistant/components/ecobee/binary_sensor.py +++ b/homeassistant/components/ecobee/binary_sensor.py @@ -1,4 +1,5 @@ """Support for Ecobee binary sensors.""" + from __future__ import annotations from homeassistant.components.binary_sensor import ( diff --git a/homeassistant/components/ecobee/climate.py b/homeassistant/components/ecobee/climate.py index 58a3cb09997..0df8a42c566 100644 --- a/homeassistant/components/ecobee/climate.py +++ b/homeassistant/components/ecobee/climate.py @@ -1,4 +1,5 @@ """Support for Ecobee Thermostats.""" + from __future__ import annotations import collections @@ -11,7 +12,6 @@ from homeassistant.components.climate import ( ATTR_TARGET_TEMP_LOW, FAN_AUTO, FAN_ON, - PRESET_AWAY, PRESET_NONE, ClimateEntity, ClimateEntityFeature, @@ -37,7 +37,7 @@ from homeassistant.util.unit_conversion import TemperatureConverter from . import EcobeeData from .const import _LOGGER, DOMAIN, ECOBEE_MODEL_TO_NAME, MANUFACTURER -from .util import ecobee_date, ecobee_time +from .util import ecobee_date, ecobee_time, is_indefinite_hold ATTR_COOL_TEMP = "cool_temp" ATTR_END_DATE = "end_date" @@ -55,6 +55,7 @@ ATTR_AUTO_AWAY = "auto_away" ATTR_FOLLOW_ME = "follow_me" DEFAULT_RESUME_ALL = False +PRESET_AWAY_INDEFINITELY = "away_indefinitely" PRESET_TEMPERATURE = "temp" PRESET_VACATION = "vacation" PRESET_HOLD_NEXT_TRANSITION = "next_transition" @@ -324,6 +325,7 @@ class Thermostat(ClimateEntity): _attr_name = None _attr_has_entity_name = True _enable_turn_on_off_backwards_compatibility = False + _attr_translation_key = "ecobee" def __init__( self, data: EcobeeData, thermostat_index: int, thermostat: dict @@ -480,6 +482,11 @@ class Thermostat(ClimateEntity): continue if event["type"] == "hold": + if event["holdClimateRef"] == "away" and is_indefinite_hold( + event["startDate"], event["endDate"] + ): + return PRESET_AWAY_INDEFINITELY + if event["holdClimateRef"] in self._preset_modes: return self._preset_modes[event["holdClimateRef"]] @@ -576,7 +583,7 @@ class Thermostat(ClimateEntity): if self.preset_mode == PRESET_VACATION: self.data.ecobee.delete_vacation(self.thermostat_index, self.vacation) - if preset_mode == PRESET_AWAY: + if preset_mode == PRESET_AWAY_INDEFINITELY: self.data.ecobee.set_climate_hold( self.thermostat_index, "away", "indefinite", self.hold_hours() ) @@ -624,7 +631,9 @@ class Thermostat(ClimateEntity): @property def preset_modes(self): """Return available preset modes.""" - return list(self._preset_modes.values()) + # Return presets provided by the ecobee API, and an indefinite away + # preset which we handle separately in set_preset_mode(). + return [*self._preset_modes.values(), PRESET_AWAY_INDEFINITELY] def set_auto_temp_hold(self, heat_temp, cool_temp): """Set temperature hold in auto mode.""" @@ -708,7 +717,7 @@ class Thermostat(ClimateEntity): def set_humidity(self, humidity: int) -> None: """Set the humidity level.""" - if humidity not in range(0, 101): + if not (0 <= humidity <= 100): raise ValueError( f"Invalid set_humidity value (must be in range 0-100): {humidity}" ) diff --git a/homeassistant/components/ecobee/config_flow.py b/homeassistant/components/ecobee/config_flow.py index 0bd7306e54e..dd5c2c62c85 100644 --- a/homeassistant/components/ecobee/config_flow.py +++ b/homeassistant/components/ecobee/config_flow.py @@ -1,4 +1,5 @@ """Config flow to configure ecobee.""" + from pyecobee import ( ECOBEE_API_KEY, ECOBEE_CONFIG_FILENAME, @@ -7,7 +8,7 @@ from pyecobee import ( ) import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_API_KEY from homeassistant.exceptions import HomeAssistantError from homeassistant.util.json import load_json_object @@ -15,7 +16,7 @@ from homeassistant.util.json import load_json_object from .const import _LOGGER, CONF_REFRESH_TOKEN, DATA_ECOBEE_CONFIG, DOMAIN -class EcobeeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class EcobeeFlowHandler(ConfigFlow, domain=DOMAIN): """Handle an ecobee config flow.""" VERSION = 1 diff --git a/homeassistant/components/ecobee/const.py b/homeassistant/components/ecobee/const.py index 23fe544d3c9..e20acb5cfca 100644 --- a/homeassistant/components/ecobee/const.py +++ b/homeassistant/components/ecobee/const.py @@ -1,4 +1,5 @@ """Constants for the ecobee integration.""" + import logging from homeassistant.components.weather import ( diff --git a/homeassistant/components/ecobee/entity.py b/homeassistant/components/ecobee/entity.py index 24fe11d17da..08ec1968999 100644 --- a/homeassistant/components/ecobee/entity.py +++ b/homeassistant/components/ecobee/entity.py @@ -1,4 +1,5 @@ """Base classes shared among Ecobee entities.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/ecobee/humidifier.py b/homeassistant/components/ecobee/humidifier.py index d8ebd3d77d8..0de7de2e803 100644 --- a/homeassistant/components/ecobee/humidifier.py +++ b/homeassistant/components/ecobee/humidifier.py @@ -1,4 +1,5 @@ """Support for using humidifier with ecobee thermostats.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/ecobee/notify.py b/homeassistant/components/ecobee/notify.py index 1372cc9f64d..b2f6ccb05c8 100644 --- a/homeassistant/components/ecobee/notify.py +++ b/homeassistant/components/ecobee/notify.py @@ -1,4 +1,5 @@ """Support for Ecobee Send Message service.""" + from __future__ import annotations from homeassistant.components.notify import ATTR_TARGET, BaseNotificationService diff --git a/homeassistant/components/ecobee/number.py b/homeassistant/components/ecobee/number.py index 345ca7b705f..4c3dd801c41 100644 --- a/homeassistant/components/ecobee/number.py +++ b/homeassistant/components/ecobee/number.py @@ -1,4 +1,5 @@ """Support for using number with ecobee thermostats.""" + from __future__ import annotations from collections.abc import Awaitable, Callable @@ -18,21 +19,14 @@ from .entity import EcobeeBaseEntity _LOGGER = logging.getLogger(__name__) -@dataclass(frozen=True) -class EcobeeNumberEntityDescriptionBase: - """Required values when describing Ecobee number entities.""" +@dataclass(frozen=True, kw_only=True) +class EcobeeNumberEntityDescription(NumberEntityDescription): + """Class describing Ecobee number entities.""" ecobee_setting_key: str set_fn: Callable[[EcobeeData, int, int], Awaitable] -@dataclass(frozen=True) -class EcobeeNumberEntityDescription( - NumberEntityDescription, EcobeeNumberEntityDescriptionBase -): - """Class describing Ecobee number entities.""" - - VENTILATOR_NUMBERS = ( EcobeeNumberEntityDescription( key="home", @@ -60,16 +54,17 @@ async def async_setup_entry( ) -> None: """Set up the ecobee thermostat number entity.""" data: EcobeeData = hass.data[DOMAIN] - entities = [] _LOGGER.debug("Adding min time ventilators numbers (if present)") - for index, thermostat in enumerate(data.ecobee.thermostats): - if thermostat["settings"]["ventilatorType"] == "none": - continue - _LOGGER.debug("Adding %s's ventilator min times number", thermostat["name"]) - for numbers in VENTILATOR_NUMBERS: - entities.append(EcobeeVentilatorMinTime(data, index, numbers)) - async_add_entities(entities, True) + async_add_entities( + ( + EcobeeVentilatorMinTime(data, index, numbers) + for index, thermostat in enumerate(data.ecobee.thermostats) + if thermostat["settings"]["ventilatorType"] != "none" + for numbers in VENTILATOR_NUMBERS + ), + True, + ) class EcobeeVentilatorMinTime(EcobeeBaseEntity, NumberEntity): diff --git a/homeassistant/components/ecobee/sensor.py b/homeassistant/components/ecobee/sensor.py index 7f0e7b808a8..3e2e984cccb 100644 --- a/homeassistant/components/ecobee/sensor.py +++ b/homeassistant/components/ecobee/sensor.py @@ -1,4 +1,5 @@ """Support for Ecobee sensors.""" + from __future__ import annotations from dataclasses import dataclass @@ -25,20 +26,13 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN, ECOBEE_MODEL_TO_NAME, MANUFACTURER -@dataclass(frozen=True) -class EcobeeSensorEntityDescriptionMixin: - """Represent the required ecobee entity description attributes.""" +@dataclass(frozen=True, kw_only=True) +class EcobeeSensorEntityDescription(SensorEntityDescription): + """Represent the ecobee sensor entity description.""" runtime_key: str | None -@dataclass(frozen=True) -class EcobeeSensorEntityDescription( - SensorEntityDescription, EcobeeSensorEntityDescriptionMixin -): - """Represent the ecobee sensor entity description.""" - - SENSOR_TYPES: tuple[EcobeeSensorEntityDescription, ...] = ( EcobeeSensorEntityDescription( key="temperature", diff --git a/homeassistant/components/ecobee/strings.json b/homeassistant/components/ecobee/strings.json index 484d5bf1e1e..b1d1df65417 100644 --- a/homeassistant/components/ecobee/strings.json +++ b/homeassistant/components/ecobee/strings.json @@ -20,6 +20,17 @@ } }, "entity": { + "climate": { + "ecobee": { + "state_attributes": { + "preset_mode": { + "state": { + "away_indefinitely": "Away Indefinitely" + } + } + } + } + }, "number": { "ventilator_min_type_home": { "name": "Ventilator min time home" diff --git a/homeassistant/components/ecobee/util.py b/homeassistant/components/ecobee/util.py index ac30b3bb660..e2e607c84d0 100644 --- a/homeassistant/components/ecobee/util.py +++ b/homeassistant/components/ecobee/util.py @@ -1,5 +1,6 @@ """Validation utility functions for ecobee services.""" -from datetime import datetime + +from datetime import date, datetime, timedelta import voluptuous as vol @@ -22,3 +23,13 @@ def ecobee_time(time_string): "Time does not match ecobee 24-hour time format HH:MM:SS" ) from err return time_string + + +def is_indefinite_hold(start_date_string: str, end_date_string: str) -> bool: + """Determine if the given start and end dates from the ecobee API represent an indefinite hold. + + This is not documented in the API, so a rough heuristic is used where a hold over 1 year is considered indefinite. + """ + return date.fromisoformat(end_date_string) - date.fromisoformat( + start_date_string + ) > timedelta(days=365) diff --git a/homeassistant/components/ecobee/weather.py b/homeassistant/components/ecobee/weather.py index 3e71b05af1d..b7961f956eb 100644 --- a/homeassistant/components/ecobee/weather.py +++ b/homeassistant/components/ecobee/weather.py @@ -1,4 +1,5 @@ """Support for displaying weather info from Ecobee API.""" + from __future__ import annotations from datetime import timedelta @@ -171,7 +172,7 @@ class EcobeeWeather(WeatherEntity): forecasts: list[Forecast] = [] date = dt_util.utcnow() - for day in range(0, 5): + for day in range(5): forecast = _process_forecast(self.weather["forecasts"][day]) if forecast is None: continue @@ -183,11 +184,6 @@ class EcobeeWeather(WeatherEntity): return forecasts return None - @property - def forecast(self) -> list[Forecast] | None: - """Return the forecast array.""" - return self._forecast() - async def async_forecast_daily(self) -> list[Forecast] | None: """Return the daily forecast in native units.""" return self._forecast() diff --git a/homeassistant/components/ecoforest/__init__.py b/homeassistant/components/ecoforest/__init__.py index 7b4dd08610a..4d5aaa40576 100644 --- a/homeassistant/components/ecoforest/__init__.py +++ b/homeassistant/components/ecoforest/__init__.py @@ -1,4 +1,5 @@ """The Ecoforest integration.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/ecoforest/config_flow.py b/homeassistant/components/ecoforest/config_flow.py index 0afc46c2370..91260f0811e 100644 --- a/homeassistant/components/ecoforest/config_flow.py +++ b/homeassistant/components/ecoforest/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Ecoforest integration.""" + from __future__ import annotations import logging @@ -9,9 +10,8 @@ from pyecoforest.api import EcoforestApi from pyecoforest.exceptions import EcoforestAuthenticationRequired import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME -from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN, MANUFACTURER @@ -26,14 +26,14 @@ STEP_USER_DATA_SCHEMA = vol.Schema( ) -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class EcoForestConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Ecoforest.""" VERSION = 1 async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors: dict[str, str] = {} diff --git a/homeassistant/components/ecoforest/coordinator.py b/homeassistant/components/ecoforest/coordinator.py index b44ccc850ce..ae0b353a1df 100644 --- a/homeassistant/components/ecoforest/coordinator.py +++ b/homeassistant/components/ecoforest/coordinator.py @@ -1,6 +1,5 @@ """The ecoforest coordinator.""" - import logging from pyecoforest.api import EcoforestApi diff --git a/homeassistant/components/ecoforest/entity.py b/homeassistant/components/ecoforest/entity.py index 901ed1bf4bf..539b0e55e19 100644 --- a/homeassistant/components/ecoforest/entity.py +++ b/homeassistant/components/ecoforest/entity.py @@ -1,4 +1,5 @@ """Base Entity for Ecoforest.""" + from __future__ import annotations from pyecoforest.models.device import Device diff --git a/homeassistant/components/ecoforest/number.py b/homeassistant/components/ecoforest/number.py index 79d62b6a2d2..db3275c1fcc 100644 --- a/homeassistant/components/ecoforest/number.py +++ b/homeassistant/components/ecoforest/number.py @@ -1,4 +1,5 @@ """Support for Ecoforest number platform.""" + from __future__ import annotations from collections.abc import Callable @@ -16,20 +17,13 @@ from .coordinator import EcoforestCoordinator from .entity import EcoforestEntity -@dataclass(frozen=True) -class EcoforestRequiredKeysMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class EcoforestNumberEntityDescription(NumberEntityDescription): + """Describes an ecoforest number entity.""" value_fn: Callable[[Device], float | None] -@dataclass(frozen=True) -class EcoforestNumberEntityDescription( - NumberEntityDescription, EcoforestRequiredKeysMixin -): - """Describes an ecoforest number entity.""" - - NUMBER_ENTITIES = ( EcoforestNumberEntityDescription( key="power_level", diff --git a/homeassistant/components/ecoforest/sensor.py b/homeassistant/components/ecoforest/sensor.py index 90904d274ac..997b02436cc 100644 --- a/homeassistant/components/ecoforest/sensor.py +++ b/homeassistant/components/ecoforest/sensor.py @@ -1,4 +1,5 @@ """Support for Ecoforest sensors.""" + from __future__ import annotations from collections.abc import Callable @@ -33,20 +34,13 @@ STATUS_TYPE = [s.value for s in State] ALARM_TYPE = [a.value for a in Alarm] + ["none"] -@dataclass(frozen=True) -class EcoforestRequiredKeysMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class EcoforestSensorEntityDescription(SensorEntityDescription): + """Describes Ecoforest sensor entity.""" value_fn: Callable[[Device], StateType] -@dataclass(frozen=True) -class EcoforestSensorEntityDescription( - SensorEntityDescription, EcoforestRequiredKeysMixin -): - """Describes Ecoforest sensor entity.""" - - SENSOR_TYPES: tuple[EcoforestSensorEntityDescription, ...] = ( EcoforestSensorEntityDescription( key="temperature", diff --git a/homeassistant/components/ecoforest/switch.py b/homeassistant/components/ecoforest/switch.py index 1e70068cde8..f59970aa751 100644 --- a/homeassistant/components/ecoforest/switch.py +++ b/homeassistant/components/ecoforest/switch.py @@ -1,4 +1,5 @@ """Switch platform for Ecoforest.""" + from __future__ import annotations from collections.abc import Awaitable, Callable @@ -17,21 +18,14 @@ from .coordinator import EcoforestCoordinator from .entity import EcoforestEntity -@dataclass(frozen=True) -class EcoforestSwitchRequiredKeysMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class EcoforestSwitchEntityDescription(SwitchEntityDescription): + """Describes an Ecoforest switch entity.""" value_fn: Callable[[Device], bool] switch_fn: Callable[[EcoforestApi, bool], Awaitable[Device]] -@dataclass(frozen=True) -class EcoforestSwitchEntityDescription( - SwitchEntityDescription, EcoforestSwitchRequiredKeysMixin -): - """Describes an Ecoforest switch entity.""" - - SWITCH_TYPES: tuple[EcoforestSwitchEntityDescription, ...] = ( EcoforestSwitchEntityDescription( key="status", diff --git a/homeassistant/components/econet/__init__.py b/homeassistant/components/econet/__init__.py index 5728c87938b..84e636e660b 100644 --- a/homeassistant/components/econet/__init__.py +++ b/homeassistant/components/econet/__init__.py @@ -1,4 +1,5 @@ """Support for EcoNet products.""" + import asyncio from datetime import timedelta import logging diff --git a/homeassistant/components/econet/binary_sensor.py b/homeassistant/components/econet/binary_sensor.py index 2a54a45eba2..3f8e17a5fbe 100644 --- a/homeassistant/components/econet/binary_sensor.py +++ b/homeassistant/components/econet/binary_sensor.py @@ -1,4 +1,5 @@ """Support for Rheem EcoNet water heaters.""" + from __future__ import annotations from pyeconet.equipment import EquipmentType diff --git a/homeassistant/components/econet/climate.py b/homeassistant/components/econet/climate.py index ac812a07566..f6bd52c9702 100644 --- a/homeassistant/components/econet/climate.py +++ b/homeassistant/components/econet/climate.py @@ -1,4 +1,5 @@ """Support for Rheem EcoNet thermostats.""" + from typing import Any from pyeconet.equipment import EquipmentType @@ -184,17 +185,17 @@ class EcoNetThermostat(EcoNetEntity, ClimateEntity): @property def fan_modes(self): """Return the fan modes.""" - econet_fan_modes = self._econet.fan_modes - fan_list = [] - for mode in econet_fan_modes: + return [ + ECONET_FAN_STATE_TO_HA[mode] + for mode in self._econet.fan_modes # Remove the MEDLO MEDHI once we figure out how to handle it - if mode not in [ + if mode + not in [ ThermostatFanMode.UNKNOWN, ThermostatFanMode.MEDLO, ThermostatFanMode.MEDHI, - ]: - fan_list.append(ECONET_FAN_STATE_TO_HA[mode]) - return fan_list + ] + ] def set_fan_mode(self, fan_mode: str) -> None: """Set the fan mode.""" diff --git a/homeassistant/components/econet/config_flow.py b/homeassistant/components/econet/config_flow.py index d25ee7973fd..81a5fdf75f0 100644 --- a/homeassistant/components/econet/config_flow.py +++ b/homeassistant/components/econet/config_flow.py @@ -1,15 +1,16 @@ """Config flow to configure the EcoNet component.""" + from pyeconet import EcoNetApiInterface from pyeconet.errors import InvalidCredentialsError, PyeconetError import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_EMAIL, CONF_PASSWORD from .const import DOMAIN -class EcoNetFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class EcoNetFlowHandler(ConfigFlow, domain=DOMAIN): """Handle an EcoNet config flow.""" VERSION = 1 diff --git a/homeassistant/components/econet/sensor.py b/homeassistant/components/econet/sensor.py index cfcab158277..f2d4ab304a5 100644 --- a/homeassistant/components/econet/sensor.py +++ b/homeassistant/components/econet/sensor.py @@ -1,4 +1,5 @@ """Support for Rheem EcoNet water heaters.""" + from __future__ import annotations from pyeconet.equipment import Equipment, EquipmentType diff --git a/homeassistant/components/econet/water_heater.py b/homeassistant/components/econet/water_heater.py index a99ab087729..5db339b4411 100644 --- a/homeassistant/components/econet/water_heater.py +++ b/homeassistant/components/econet/water_heater.py @@ -1,4 +1,5 @@ """Support for Rheem EcoNet water heaters.""" + from datetime import timedelta import logging from typing import Any diff --git a/homeassistant/components/ecovacs/__init__.py b/homeassistant/components/ecovacs/__init__.py index 945f999cf79..ca4579a31b2 100644 --- a/homeassistant/components/ecovacs/__init__.py +++ b/homeassistant/components/ecovacs/__init__.py @@ -1,4 +1,5 @@ """Support for Ecovacs Deebot vacuums.""" + import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry @@ -27,6 +28,7 @@ CONFIG_SCHEMA = vol.Schema( PLATFORMS = [ Platform.BINARY_SENSOR, Platform.BUTTON, + Platform.EVENT, Platform.IMAGE, Platform.LAWN_MOWER, Platform.NUMBER, diff --git a/homeassistant/components/ecovacs/binary_sensor.py b/homeassistant/components/ecovacs/binary_sensor.py index f04f2110003..cc401cc3ca0 100644 --- a/homeassistant/components/ecovacs/binary_sensor.py +++ b/homeassistant/components/ecovacs/binary_sensor.py @@ -1,4 +1,5 @@ """Binary sensor module.""" + from collections.abc import Callable from dataclasses import dataclass from typing import Generic diff --git a/homeassistant/components/ecovacs/button.py b/homeassistant/components/ecovacs/button.py index 0e011726010..27f729a1ae0 100644 --- a/homeassistant/components/ecovacs/button.py +++ b/homeassistant/components/ecovacs/button.py @@ -1,4 +1,5 @@ """Ecovacs button module.""" + from dataclasses import dataclass from deebot_client.capabilities import ( @@ -73,18 +74,15 @@ async def async_setup_entry( entities: list[EcovacsEntity] = get_supported_entitites( controller, EcovacsButtonEntity, ENTITY_DESCRIPTIONS ) - for device in controller.devices(Capabilities): - lifespan_capability = device.capabilities.life_span - for description in LIFESPAN_ENTITY_DESCRIPTIONS: - if description.component in lifespan_capability.types: - entities.append( - EcovacsResetLifespanButtonEntity( - device, lifespan_capability, description - ) - ) - - if entities: - async_add_entities(entities) + entities.extend( + EcovacsResetLifespanButtonEntity( + device, device.capabilities.life_span, description + ) + for device in controller.devices(Capabilities) + for description in LIFESPAN_ENTITY_DESCRIPTIONS + if description.component in device.capabilities.life_span.types + ) + async_add_entities(entities) class EcovacsButtonEntity( diff --git a/homeassistant/components/ecovacs/config_flow.py b/homeassistant/components/ecovacs/config_flow.py index db3c60fa9e7..8cf82f6237c 100644 --- a/homeassistant/components/ecovacs/config_flow.py +++ b/homeassistant/components/ecovacs/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Ecovacs mqtt integration.""" + from __future__ import annotations import logging @@ -15,10 +16,10 @@ from deebot_client.util import md5 from deebot_client.util.continents import COUNTRIES_TO_CONTINENTS, get_continent import voluptuous as vol -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_COUNTRY, CONF_MODE, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant -from homeassistant.data_entry_flow import AbortFlow, FlowResult +from homeassistant.data_entry_flow import AbortFlow from homeassistant.helpers import aiohttp_client, selector from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.loader import async_get_issue_tracker @@ -136,7 +137,7 @@ class EcovacsConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" if not self.show_advanced_options: @@ -166,7 +167,7 @@ class EcovacsConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_auth( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the auth step.""" errors = {} @@ -217,7 +218,7 @@ class EcovacsConfigFlow(ConfigFlow, domain=DOMAIN): last_step=True, ) - async def async_step_import(self, user_input: dict[str, Any]) -> FlowResult: + async def async_step_import(self, user_input: dict[str, Any]) -> ConfigFlowResult: """Import configuration from yaml.""" def create_repair( diff --git a/homeassistant/components/ecovacs/const.py b/homeassistant/components/ecovacs/const.py index dc055cee519..e5ef0760182 100644 --- a/homeassistant/components/ecovacs/const.py +++ b/homeassistant/components/ecovacs/const.py @@ -1,4 +1,5 @@ """Ecovacs constants.""" + from enum import StrEnum from deebot_client.events import LifeSpan diff --git a/homeassistant/components/ecovacs/controller.py b/homeassistant/components/ecovacs/controller.py index 6ba5dcdba6c..5defcdf861f 100644 --- a/homeassistant/components/ecovacs/controller.py +++ b/homeassistant/components/ecovacs/controller.py @@ -1,4 +1,5 @@ """Controller module.""" + from __future__ import annotations from collections.abc import Generator, Mapping diff --git a/homeassistant/components/ecovacs/diagnostics.py b/homeassistant/components/ecovacs/diagnostics.py index 6493dce2712..9340841223e 100644 --- a/homeassistant/components/ecovacs/diagnostics.py +++ b/homeassistant/components/ecovacs/diagnostics.py @@ -1,4 +1,5 @@ """Ecovacs diagnostics.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/ecovacs/entity.py b/homeassistant/components/ecovacs/entity.py index 817172016bc..4497f82d964 100644 --- a/homeassistant/components/ecovacs/entity.py +++ b/homeassistant/components/ecovacs/entity.py @@ -1,4 +1,5 @@ """Ecovacs mqtt entity module.""" + from __future__ import annotations from collections.abc import Callable, Coroutine diff --git a/homeassistant/components/ecovacs/event.py b/homeassistant/components/ecovacs/event.py new file mode 100644 index 00000000000..daac4a626ae --- /dev/null +++ b/homeassistant/components/ecovacs/event.py @@ -0,0 +1,64 @@ +"""Event module.""" + +from deebot_client.capabilities import Capabilities, CapabilityEvent +from deebot_client.device import Device +from deebot_client.events import CleanJobStatus, ReportStatsEvent + +from homeassistant.components.event import EventEntity, EventEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import EntityCategory +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .controller import EcovacsController +from .entity import EcovacsEntity + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Add entities for passed config_entry in HA.""" + controller: EcovacsController = hass.data[DOMAIN][config_entry.entry_id] + async_add_entities( + EcovacsLastJobEventEntity(device) for device in controller.devices(Capabilities) + ) + + +class EcovacsLastJobEventEntity( + EcovacsEntity[Capabilities, CapabilityEvent[ReportStatsEvent]], + EventEntity, +): + """Ecovacs last job event entity.""" + + entity_description = EventEntityDescription( + key="stats_report", + translation_key="last_job", + entity_category=EntityCategory.DIAGNOSTIC, + event_types=["finished", "finished_with_warnings", "manually_stopped"], + ) + + def __init__(self, device: Device[Capabilities]) -> None: + """Initialize entity.""" + super().__init__(device, device.capabilities.stats.report) + + async def async_added_to_hass(self) -> None: + """Set up the event listeners now that hass is ready.""" + await super().async_added_to_hass() + + async def on_event(event: ReportStatsEvent) -> None: + """Handle event.""" + if event.status in (CleanJobStatus.NO_STATUS, CleanJobStatus.CLEANING): + # we trigger only on job done + return + + event_type = event.status.name.lower() + if event.status == CleanJobStatus.MANUAL_STOPPED: + event_type = "manually_stopped" + + self._trigger_event(event_type) + self.async_write_ha_state() + + self._subscribe(self._capability.event, on_event) diff --git a/homeassistant/components/ecovacs/icons.json b/homeassistant/components/ecovacs/icons.json index 7a57259ca5a..2e2d897c455 100644 --- a/homeassistant/components/ecovacs/icons.json +++ b/homeassistant/components/ecovacs/icons.json @@ -22,6 +22,11 @@ "default": "mdi:broom" } }, + "event": { + "last_job": { + "default": "mdi:history" + } + }, "number": { "clean_count": { "default": "mdi:counter" diff --git a/homeassistant/components/ecovacs/lawn_mower.py b/homeassistant/components/ecovacs/lawn_mower.py index e33e87bc5fb..1b13d50cc0c 100644 --- a/homeassistant/components/ecovacs/lawn_mower.py +++ b/homeassistant/components/ecovacs/lawn_mower.py @@ -42,10 +42,10 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Ecovacs mowers.""" - mowers: list[EcovacsMower] = [] controller: EcovacsController = hass.data[DOMAIN][config_entry.entry_id] - for device in controller.devices(MowerCapabilities): - mowers.append(EcovacsMower(device)) + mowers: list[EcovacsMower] = [ + EcovacsMower(device) for device in controller.devices(MowerCapabilities) + ] _LOGGER.debug("Adding Ecovacs Mowers to Home Assistant: %s", mowers) async_add_entities(mowers) @@ -62,9 +62,7 @@ class EcovacsMower( | LawnMowerEntityFeature.START_MOWING ) - entity_description = LawnMowerEntityEntityDescription( - key="mower", translation_key="mower", name=None - ) + entity_description = LawnMowerEntityEntityDescription(key="mower", name=None) def __init__(self, device: Device[MowerCapabilities]) -> None: """Initialize the mower.""" diff --git a/homeassistant/components/ecovacs/number.py b/homeassistant/components/ecovacs/number.py index 0dc379c68f0..e53f7e6aae0 100644 --- a/homeassistant/components/ecovacs/number.py +++ b/homeassistant/components/ecovacs/number.py @@ -1,4 +1,5 @@ """Ecovacs number module.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/ecovacs/select.py b/homeassistant/components/ecovacs/select.py index 00e7134266b..8a3def54e28 100644 --- a/homeassistant/components/ecovacs/select.py +++ b/homeassistant/components/ecovacs/select.py @@ -1,4 +1,5 @@ """Ecovacs select entity module.""" + from collections.abc import Callable from dataclasses import dataclass from typing import Any, Generic diff --git a/homeassistant/components/ecovacs/sensor.py b/homeassistant/components/ecovacs/sensor.py index 6efc9ec0385..92d1b10a614 100644 --- a/homeassistant/components/ecovacs/sensor.py +++ b/homeassistant/components/ecovacs/sensor.py @@ -1,4 +1,5 @@ """Ecovacs sensor module.""" + from __future__ import annotations from collections.abc import Callable @@ -179,16 +180,17 @@ async def async_setup_entry( entities: list[EcovacsEntity] = get_supported_entitites( controller, EcovacsSensor, ENTITY_DESCRIPTIONS ) - for device in controller.devices(Capabilities): - lifespan_capability = device.capabilities.life_span - for description in LIFESPAN_ENTITY_DESCRIPTIONS: - if description.component in lifespan_capability.types: - entities.append( - EcovacsLifespanSensor(device, lifespan_capability, description) - ) - - if capability := device.capabilities.error: - entities.append(EcovacsErrorSensor(device, capability)) + entities.extend( + EcovacsLifespanSensor(device, device.capabilities.life_span, description) + for device in controller.devices(Capabilities) + for description in LIFESPAN_ENTITY_DESCRIPTIONS + if description.component in device.capabilities.life_span.types + ) + entities.extend( + EcovacsErrorSensor(device, capability) + for device in controller.devices(Capabilities) + if (capability := device.capabilities.error) + ) async_add_entities(entities) diff --git a/homeassistant/components/ecovacs/strings.json b/homeassistant/components/ecovacs/strings.json index 1f43b830778..a21f57a7a24 100644 --- a/homeassistant/components/ecovacs/strings.json +++ b/homeassistant/components/ecovacs/strings.json @@ -56,6 +56,20 @@ "name": "Reset side brushes lifespan" } }, + "event": { + "last_job": { + "name": "Last job", + "state_attributes": { + "event_type": { + "state": { + "finished": "Finished", + "finished_with_warnings": "Finished with warnings", + "manually_stopped": "Manually stopped" + } + } + } + } + }, "image": { "map": { "name": "Map" diff --git a/homeassistant/components/ecovacs/switch.py b/homeassistant/components/ecovacs/switch.py index 316ed5427ba..0d2f8f2024f 100644 --- a/homeassistant/components/ecovacs/switch.py +++ b/homeassistant/components/ecovacs/switch.py @@ -1,4 +1,5 @@ """Ecovacs switch module.""" + from dataclasses import dataclass from typing import Any diff --git a/homeassistant/components/ecovacs/util.py b/homeassistant/components/ecovacs/util.py index b3e0d4d96be..14e69cd4b61 100644 --- a/homeassistant/components/ecovacs/util.py +++ b/homeassistant/components/ecovacs/util.py @@ -1,4 +1,5 @@ """Ecovacs util functions.""" + from __future__ import annotations import random @@ -30,13 +31,10 @@ def get_supported_entitites( descriptions: tuple[EcovacsCapabilityEntityDescription, ...], ) -> list[EcovacsEntity]: """Return all supported entities for all devices.""" - entities: list[EcovacsEntity] = [] - - for device in controller.devices(Capabilities): - for description in descriptions: - if isinstance(device.capabilities, description.device_capabilities) and ( - capability := description.capability_fn(device.capabilities) - ): - entities.append(entity_class(device, capability, description)) - - return entities + return [ + entity_class(device, capability, description) + for device in controller.devices(Capabilities) + for description in descriptions + if isinstance(device.capabilities, description.device_capabilities) + if (capability := description.capability_fn(device.capabilities)) + ] diff --git a/homeassistant/components/ecovacs/vacuum.py b/homeassistant/components/ecovacs/vacuum.py index 0d65d58d84c..d5016ab683d 100644 --- a/homeassistant/components/ecovacs/vacuum.py +++ b/homeassistant/components/ecovacs/vacuum.py @@ -1,4 +1,5 @@ """Support for Ecovacs Ecovacs Vacuums.""" + from __future__ import annotations from collections.abc import Mapping @@ -45,13 +46,13 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Ecovacs vacuums.""" - vacuums: list[EcovacsVacuum | EcovacsLegacyVacuum] = [] controller: EcovacsController = hass.data[DOMAIN][config_entry.entry_id] + vacuums: list[EcovacsVacuum | EcovacsLegacyVacuum] = [ + EcovacsVacuum(device) for device in controller.devices(VacuumCapabilities) + ] for device in controller.legacy_devices: await hass.async_add_executor_job(device.connect_and_wait_until_ready) vacuums.append(EcovacsLegacyVacuum(device)) - for device in controller.devices(VacuumCapabilities): - vacuums.append(EcovacsVacuum(device)) _LOGGER.debug("Adding Ecovacs Vacuums to Home Assistant: %s", vacuums) async_add_entities(vacuums) @@ -336,7 +337,6 @@ class EcovacsVacuum( params = {} elif isinstance(params, list): raise ServiceValidationError( - "Params must be a dict!", translation_domain=DOMAIN, translation_key="vacuum_send_command_params_dict", ) @@ -344,7 +344,6 @@ class EcovacsVacuum( if command in ["spot_area", "custom_area"]: if params is None: raise ServiceValidationError( - f"Params are required for {command}!", translation_domain=DOMAIN, translation_key="vacuum_send_command_params_required", translation_placeholders={"command": command}, @@ -353,7 +352,6 @@ class EcovacsVacuum( info = self._device.device_info name = info.get("nick", info["name"]) raise ServiceValidationError( - f"Vacuum {name} does not support area capability!", translation_domain=DOMAIN, translation_key="vacuum_send_command_area_not_supported", translation_placeholders={"name": name}, diff --git a/homeassistant/components/ecowitt/__init__.py b/homeassistant/components/ecowitt/__init__.py index eaf2441ffac..0c330bc3f33 100644 --- a/homeassistant/components/ecowitt/__init__.py +++ b/homeassistant/components/ecowitt/__init__.py @@ -1,4 +1,5 @@ """The Ecowitt Weather Station Component.""" + from __future__ import annotations from aioecowitt import EcoWittListener diff --git a/homeassistant/components/ecowitt/binary_sensor.py b/homeassistant/components/ecowitt/binary_sensor.py index fbe2e017339..f73467288a2 100644 --- a/homeassistant/components/ecowitt/binary_sensor.py +++ b/homeassistant/components/ecowitt/binary_sensor.py @@ -1,4 +1,5 @@ """Support for Ecowitt Weather Stations.""" + import dataclasses from typing import Final diff --git a/homeassistant/components/ecowitt/config_flow.py b/homeassistant/components/ecowitt/config_flow.py index 2b6744790bf..b131cbea6ae 100644 --- a/homeassistant/components/ecowitt/config_flow.py +++ b/homeassistant/components/ecowitt/config_flow.py @@ -1,4 +1,5 @@ """Config flow for ecowitt.""" + from __future__ import annotations import secrets @@ -6,17 +7,16 @@ from typing import Any from yarl import URL -from homeassistant import config_entries from homeassistant.components import webhook +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_WEBHOOK_ID -from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.network import get_url from .const import DOMAIN -class EcowittConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class EcowittConfigFlow(ConfigFlow, domain=DOMAIN): """Config flow for the Ecowitt.""" VERSION = 1 @@ -24,7 +24,7 @@ class EcowittConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" if user_input is None: self._webhook_id = secrets.token_hex(16) diff --git a/homeassistant/components/ecowitt/diagnostics.py b/homeassistant/components/ecowitt/diagnostics.py index 96fa020667b..e4aecc1c07b 100644 --- a/homeassistant/components/ecowitt/diagnostics.py +++ b/homeassistant/components/ecowitt/diagnostics.py @@ -1,4 +1,5 @@ """Provides diagnostics for EcoWitt.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/ecowitt/entity.py b/homeassistant/components/ecowitt/entity.py index cf62cfb2d94..d6e268c3578 100644 --- a/homeassistant/components/ecowitt/entity.py +++ b/homeassistant/components/ecowitt/entity.py @@ -1,4 +1,5 @@ """The Ecowitt Weather Station Entity.""" + from __future__ import annotations import time diff --git a/homeassistant/components/ecowitt/sensor.py b/homeassistant/components/ecowitt/sensor.py index 4bcdd2461cd..5f2f08f2519 100644 --- a/homeassistant/components/ecowitt/sensor.py +++ b/homeassistant/components/ecowitt/sensor.py @@ -1,4 +1,5 @@ """Support for Ecowitt Weather Stations.""" + from __future__ import annotations import dataclasses diff --git a/homeassistant/components/eddystone_temperature/sensor.py b/homeassistant/components/eddystone_temperature/sensor.py index 347ee1b242f..b136b193686 100644 --- a/homeassistant/components/eddystone_temperature/sensor.py +++ b/homeassistant/components/eddystone_temperature/sensor.py @@ -3,6 +3,7 @@ Your beacons must be configured to transmit UID (for identification) and TLM (for temperature) frames. """ + from __future__ import annotations import logging diff --git a/homeassistant/components/edimax/switch.py b/homeassistant/components/edimax/switch.py index 34f6a500917..61f3e6f4538 100644 --- a/homeassistant/components/edimax/switch.py +++ b/homeassistant/components/edimax/switch.py @@ -1,4 +1,5 @@ """Support for Edimax switches.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/edl21/config_flow.py b/homeassistant/components/edl21/config_flow.py index 0bedcc515ef..5a5db507bff 100644 --- a/homeassistant/components/edl21/config_flow.py +++ b/homeassistant/components/edl21/config_flow.py @@ -2,8 +2,7 @@ import voluptuous as vol -from homeassistant import config_entries -from homeassistant.data_entry_flow import FlowResult +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from .const import CONF_SERIAL_PORT, DEFAULT_TITLE, DOMAIN @@ -14,14 +13,14 @@ DATA_SCHEMA = vol.Schema( ) -class EDL21ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class EDL21ConfigFlow(ConfigFlow, domain=DOMAIN): """EDL21 config flow.""" VERSION = 1 async def async_step_user( self, user_input: dict[str, str] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the user setup step.""" if user_input is not None: self._async_abort_entries_match( diff --git a/homeassistant/components/edl21/const.py b/homeassistant/components/edl21/const.py index 2bde0ff379a..37b8c140f55 100644 --- a/homeassistant/components/edl21/const.py +++ b/homeassistant/components/edl21/const.py @@ -1,4 +1,5 @@ """Constants for the EDL21 component.""" + import logging LOGGER = logging.getLogger(__package__) diff --git a/homeassistant/components/edl21/sensor.py b/homeassistant/components/edl21/sensor.py index 0126c87b8cd..4474893d9b6 100644 --- a/homeassistant/components/edl21/sensor.py +++ b/homeassistant/components/edl21/sensor.py @@ -1,4 +1,5 @@ """Support for EDL21 Smart Meters.""" + from __future__ import annotations from collections.abc import Mapping diff --git a/homeassistant/components/efergy/__init__.py b/homeassistant/components/efergy/__init__.py index 0ca5bf1d8f7..3bfd37392ad 100644 --- a/homeassistant/components/efergy/__init__.py +++ b/homeassistant/components/efergy/__init__.py @@ -1,4 +1,5 @@ """The Efergy integration.""" + from __future__ import annotations from pyefergy import Efergy, exceptions diff --git a/homeassistant/components/efergy/config_flow.py b/homeassistant/components/efergy/config_flow.py index b2f2a368a9e..8e23925d193 100644 --- a/homeassistant/components/efergy/config_flow.py +++ b/homeassistant/components/efergy/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Efergy integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -7,22 +8,21 @@ from typing import Any from pyefergy import Efergy, exceptions import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult 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 DEFAULT_NAME, DOMAIN, LOGGER -class EfergyFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class EfergyFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a config flow for Efergy.""" VERSION = 1 async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initiated by the user.""" errors = {} if user_input is not None: @@ -53,7 +53,9 @@ class EfergyFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Handle a reauthorization flow request.""" return await self.async_step_user() diff --git a/homeassistant/components/efergy/const.py b/homeassistant/components/efergy/const.py index 5a0ca11693b..f6d26a3430d 100644 --- a/homeassistant/components/efergy/const.py +++ b/homeassistant/components/efergy/const.py @@ -1,4 +1,5 @@ """Constants for the Efergy integration.""" + from datetime import timedelta import logging from typing import Final diff --git a/homeassistant/components/efergy/sensor.py b/homeassistant/components/efergy/sensor.py index dd8752dde7f..59b2799d37b 100644 --- a/homeassistant/components/efergy/sensor.py +++ b/homeassistant/components/efergy/sensor.py @@ -1,4 +1,5 @@ """Support for Efergy sensors.""" + from __future__ import annotations import dataclasses @@ -126,15 +127,15 @@ async def async_setup_entry( description, entity_registry_enabled_default=len(api.sids) > 1, ) - for sid in api.sids: - sensors.append( - EfergySensor( - api, - description, - entry.entry_id, - sid=sid, - ) + sensors.extend( + EfergySensor( + api, + description, + entry.entry_id, + sid=sid, ) + for sid in api.sids + ) async_add_entities(sensors, True) diff --git a/homeassistant/components/egardia/__init__.py b/homeassistant/components/egardia/__init__.py index 12091cf7a8a..9ff4b9af94f 100644 --- a/homeassistant/components/egardia/__init__.py +++ b/homeassistant/components/egardia/__init__.py @@ -1,4 +1,5 @@ """Interfaces with Egardia/Woonveilig alarm control panel.""" + import logging from pythonegardia import egardiadevice, egardiaserver diff --git a/homeassistant/components/egardia/alarm_control_panel.py b/homeassistant/components/egardia/alarm_control_panel.py index 60b80fffd23..c58396ae947 100644 --- a/homeassistant/components/egardia/alarm_control_panel.py +++ b/homeassistant/components/egardia/alarm_control_panel.py @@ -1,4 +1,5 @@ """Interfaces with Egardia/Woonveilig alarm control panel.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/egardia/binary_sensor.py b/homeassistant/components/egardia/binary_sensor.py index 021111e53b3..53505f58d3b 100644 --- a/homeassistant/components/egardia/binary_sensor.py +++ b/homeassistant/components/egardia/binary_sensor.py @@ -1,4 +1,5 @@ """Interfaces with Egardia/Woonveilig alarm control panel.""" + from __future__ import annotations from homeassistant.components.binary_sensor import ( diff --git a/homeassistant/components/eight_sleep/__init__.py b/homeassistant/components/eight_sleep/__init__.py index ab5eff3b60f..9df39bbe314 100644 --- a/homeassistant/components/eight_sleep/__init__.py +++ b/homeassistant/components/eight_sleep/__init__.py @@ -1,4 +1,5 @@ """The Eight Sleep integration.""" + from __future__ import annotations from homeassistant.config_entries import ConfigEntry, ConfigEntryState diff --git a/homeassistant/components/electrasmart/__init__.py b/homeassistant/components/electrasmart/__init__.py index 6fb9c35757f..b8e5eb1bdd8 100644 --- a/homeassistant/components/electrasmart/__init__.py +++ b/homeassistant/components/electrasmart/__init__.py @@ -1,4 +1,5 @@ """The Electra Air Conditioner integration.""" + from __future__ import annotations from typing import cast diff --git a/homeassistant/components/electrasmart/config_flow.py b/homeassistant/components/electrasmart/config_flow.py index 946a9f2854d..a2e6889c346 100644 --- a/homeassistant/components/electrasmart/config_flow.py +++ b/homeassistant/components/electrasmart/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Electra Air Conditioner integration.""" + from __future__ import annotations import logging @@ -8,9 +9,8 @@ from electrasmart.api import STATUS_SUCCESS, Attributes, ElectraAPI, ElectraApiE from electrasmart.api.utils import generate_imei import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_TOKEN -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import CONF_IMEI, CONF_OTP, CONF_PHONE_NUMBER, DOMAIN @@ -18,7 +18,7 @@ from .const import CONF_IMEI, CONF_OTP, CONF_PHONE_NUMBER, DOMAIN _LOGGER = logging.getLogger(__name__) -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class ElectraSmartConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Electra Air Conditioner.""" VERSION = 1 @@ -34,7 +34,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" if not self._api: @@ -52,7 +52,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): user_input: dict[str, str] | None = None, errors: dict[str, str] | None = None, step_id: str = "user", - ) -> FlowResult: + ) -> ConfigFlowResult: """Show the setup form to the user.""" if user_input is None: user_input = {} @@ -73,7 +73,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): description_placeholders=self._description_placeholders, ) - async def _validate_phone_number(self, user_input: dict[str, str]) -> FlowResult: + async def _validate_phone_number( + self, user_input: dict[str, str] + ) -> ConfigFlowResult: """Check if config is valid and create entry if so.""" self._phone_number = user_input[CONF_PHONE_NUMBER] @@ -102,7 +104,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def _validate_one_time_password( self, user_input: dict[str, str] - ) -> FlowResult: + ) -> ConfigFlowResult: self._otp = user_input[CONF_OTP] assert isinstance(self._api, ElectraAPI) @@ -135,7 +137,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None, errors: dict[str, str] | None = None, - ) -> FlowResult: + ) -> ConfigFlowResult: """Ask the verification code to the user.""" if errors is None: errors = {} @@ -148,7 +150,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def _show_otp_form( self, errors: dict[str, str] | None = None, - ) -> FlowResult: + ) -> ConfigFlowResult: """Show the verification_code form to the user.""" return self.async_show_form( diff --git a/homeassistant/components/electric_kiwi/__init__.py b/homeassistant/components/electric_kiwi/__init__.py index 00ff6749364..8c9a0b3950e 100644 --- a/homeassistant/components/electric_kiwi/__init__.py +++ b/homeassistant/components/electric_kiwi/__init__.py @@ -1,4 +1,5 @@ """The Electric Kiwi integration.""" + from __future__ import annotations import aiohttp diff --git a/homeassistant/components/electric_kiwi/config_flow.py b/homeassistant/components/electric_kiwi/config_flow.py index c2c80aaa402..5be3edeaa66 100644 --- a/homeassistant/components/electric_kiwi/config_flow.py +++ b/homeassistant/components/electric_kiwi/config_flow.py @@ -1,12 +1,12 @@ """Config flow for Electric Kiwi.""" + from __future__ import annotations from collections.abc import Mapping import logging from typing import Any -from homeassistant.config_entries import ConfigEntry -from homeassistant.data_entry_flow import FlowResult +from homeassistant.config_entries import ConfigEntry, ConfigFlowResult from homeassistant.helpers import config_entry_oauth2_flow from .const import DOMAIN, SCOPE_VALUES @@ -34,7 +34,9 @@ class ElectricKiwiOauth2FlowHandler( """Extra data that needs to be appended to the authorize url.""" return {"scope": SCOPE_VALUES} - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Perform reauth upon an API authentication error.""" self._reauth_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] @@ -43,13 +45,13 @@ class ElectricKiwiOauth2FlowHandler( async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """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) -> FlowResult: + async def async_oauth_create_entry(self, data: dict) -> ConfigFlowResult: """Create an entry for Electric Kiwi.""" existing_entry = await self.async_set_unique_id(DOMAIN) if existing_entry: diff --git a/homeassistant/components/electric_kiwi/coordinator.py b/homeassistant/components/electric_kiwi/coordinator.py index c3f49d1aba9..a10be5eafdd 100644 --- a/homeassistant/components/electric_kiwi/coordinator.py +++ b/homeassistant/components/electric_kiwi/coordinator.py @@ -1,4 +1,5 @@ """Electric Kiwi coordinators.""" + import asyncio from collections import OrderedDict from datetime import timedelta diff --git a/homeassistant/components/electric_kiwi/oauth2.py b/homeassistant/components/electric_kiwi/oauth2.py index ce3e473159a..864550991f5 100644 --- a/homeassistant/components/electric_kiwi/oauth2.py +++ b/homeassistant/components/electric_kiwi/oauth2.py @@ -1,4 +1,5 @@ """OAuth2 implementations for Toon.""" + from __future__ import annotations import base64 diff --git a/homeassistant/components/electric_kiwi/select.py b/homeassistant/components/electric_kiwi/select.py index 5905efc1604..90b31aa7511 100644 --- a/homeassistant/components/electric_kiwi/select.py +++ b/homeassistant/components/electric_kiwi/select.py @@ -1,4 +1,5 @@ """Support for Electric Kiwi hour of free power.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/electric_kiwi/sensor.py b/homeassistant/components/electric_kiwi/sensor.py index 83431dfd925..308201a9458 100644 --- a/homeassistant/components/electric_kiwi/sensor.py +++ b/homeassistant/components/electric_kiwi/sensor.py @@ -1,4 +1,5 @@ """Support for Electric Kiwi sensors.""" + from __future__ import annotations from collections.abc import Callable @@ -34,20 +35,13 @@ ATTR_NEXT_BILLING_DATE = "next_billing_date" ATTR_HOP_PERCENTAGE = "hop_percentage" -@dataclass(frozen=True) -class ElectricKiwiAccountRequiredKeysMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class ElectricKiwiAccountSensorEntityDescription(SensorEntityDescription): + """Describes Electric Kiwi sensor entity.""" value_func: Callable[[AccountBalance], float | datetime] -@dataclass(frozen=True) -class ElectricKiwiAccountSensorEntityDescription( - SensorEntityDescription, ElectricKiwiAccountRequiredKeysMixin -): - """Describes Electric Kiwi sensor entity.""" - - ACCOUNT_SENSOR_TYPES: tuple[ElectricKiwiAccountSensorEntityDescription, ...] = ( ElectricKiwiAccountSensorEntityDescription( key=ATTR_TOTAL_RUNNING_BALANCE, @@ -85,21 +79,13 @@ ACCOUNT_SENSOR_TYPES: tuple[ElectricKiwiAccountSensorEntityDescription, ...] = ( ) -@dataclass(frozen=True) -class ElectricKiwiHOPRequiredKeysMixin: - """Mixin for required HOP keys.""" +@dataclass(frozen=True, kw_only=True) +class ElectricKiwiHOPSensorEntityDescription(SensorEntityDescription): + """Describes Electric Kiwi HOP sensor entity.""" value_func: Callable[[Hop], datetime] -@dataclass(frozen=True) -class ElectricKiwiHOPSensorEntityDescription( - SensorEntityDescription, - ElectricKiwiHOPRequiredKeysMixin, -): - """Describes Electric Kiwi HOP sensor entity.""" - - def _check_and_move_time(hop: Hop, time: str) -> datetime: """Return the time a day forward if HOP end_time is in the past.""" date_time = datetime.combine( diff --git a/homeassistant/components/elgato/__init__.py b/homeassistant/components/elgato/__init__.py index 7584695240b..8d6af325213 100644 --- a/homeassistant/components/elgato/__init__.py +++ b/homeassistant/components/elgato/__init__.py @@ -1,4 +1,5 @@ """Support for Elgato Lights.""" + from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/elgato/button.py b/homeassistant/components/elgato/button.py index 9747496c126..47e24ca245a 100644 --- a/homeassistant/components/elgato/button.py +++ b/homeassistant/components/elgato/button.py @@ -1,4 +1,5 @@ """Support for Elgato button.""" + from __future__ import annotations from collections.abc import Awaitable, Callable diff --git a/homeassistant/components/elgato/config_flow.py b/homeassistant/components/elgato/config_flow.py index 9e63df0a503..5329fcee90a 100644 --- a/homeassistant/components/elgato/config_flow.py +++ b/homeassistant/components/elgato/config_flow.py @@ -1,4 +1,5 @@ """Config flow to configure the Elgato Light integration.""" + from __future__ import annotations from typing import Any @@ -7,10 +8,9 @@ from elgato import Elgato, ElgatoError import voluptuous as vol from homeassistant.components import onboarding, zeroconf -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PORT from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN @@ -28,7 +28,7 @@ class ElgatoFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initiated by the user.""" if user_input is None: return self._async_show_setup_form() @@ -45,7 +45,7 @@ class ElgatoFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_zeroconf( self, discovery_info: zeroconf.ZeroconfServiceInfo - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle zeroconf discovery.""" self.host = discovery_info.host self.mac = discovery_info.properties.get("id") @@ -67,14 +67,14 @@ class ElgatoFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_zeroconf_confirm( self, _: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initiated by zeroconf.""" return self._async_create_entry() @callback def _async_show_setup_form( self, errors: dict[str, str] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Show the setup form to the user.""" return self.async_show_form( step_id="user", @@ -88,7 +88,7 @@ class ElgatoFlowHandler(ConfigFlow, domain=DOMAIN): ) @callback - def _async_create_entry(self) -> FlowResult: + def _async_create_entry(self) -> ConfigFlowResult: return self.async_create_entry( title=self.serial_number, data={ diff --git a/homeassistant/components/elgato/const.py b/homeassistant/components/elgato/const.py index e9a93387e63..114bb01583a 100644 --- a/homeassistant/components/elgato/const.py +++ b/homeassistant/components/elgato/const.py @@ -1,4 +1,5 @@ """Constants for the Elgato Light integration.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/elgato/coordinator.py b/homeassistant/components/elgato/coordinator.py index 0dda6bad292..c2bc79491a1 100644 --- a/homeassistant/components/elgato/coordinator.py +++ b/homeassistant/components/elgato/coordinator.py @@ -1,4 +1,5 @@ """DataUpdateCoordinator for Elgato.""" + from dataclasses import dataclass from elgato import BatteryInfo, Elgato, ElgatoConnectionError, Info, Settings, State diff --git a/homeassistant/components/elgato/diagnostics.py b/homeassistant/components/elgato/diagnostics.py index 46730b8f005..91f5c9a8319 100644 --- a/homeassistant/components/elgato/diagnostics.py +++ b/homeassistant/components/elgato/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for Elgato.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/elgato/entity.py b/homeassistant/components/elgato/entity.py index 3f46b51d7b7..42920c3d28e 100644 --- a/homeassistant/components/elgato/entity.py +++ b/homeassistant/components/elgato/entity.py @@ -1,4 +1,5 @@ """Base entity for the Elgato integration.""" + from __future__ import annotations from homeassistant.const import ATTR_CONNECTIONS, CONF_MAC diff --git a/homeassistant/components/elgato/icons.json b/homeassistant/components/elgato/icons.json new file mode 100644 index 00000000000..1b5eaf3763a --- /dev/null +++ b/homeassistant/components/elgato/icons.json @@ -0,0 +1,15 @@ +{ + "entity": { + "switch": { + "bypass": { + "default": "mdi:battery-off-outline" + }, + "energy_saving": { + "default": "mdi:leaf" + } + } + }, + "services": { + "identify": "mdi:crosshairs-question" + } +} diff --git a/homeassistant/components/elgato/light.py b/homeassistant/components/elgato/light.py index f74ec04476f..100a04fb6fb 100644 --- a/homeassistant/components/elgato/light.py +++ b/homeassistant/components/elgato/light.py @@ -1,4 +1,5 @@ """Support for Elgato lights.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/elgato/sensor.py b/homeassistant/components/elgato/sensor.py index b683b80f5fa..76d88df3fb9 100644 --- a/homeassistant/components/elgato/sensor.py +++ b/homeassistant/components/elgato/sensor.py @@ -1,4 +1,5 @@ """Support for Elgato sensors.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/elgato/switch.py b/homeassistant/components/elgato/switch.py index d1f370547a4..0d20ae95e03 100644 --- a/homeassistant/components/elgato/switch.py +++ b/homeassistant/components/elgato/switch.py @@ -1,4 +1,5 @@ """Support for Elgato switches.""" + from __future__ import annotations from collections.abc import Awaitable, Callable @@ -32,7 +33,6 @@ SWITCHES = [ ElgatoSwitchEntityDescription( key="bypass", translation_key="bypass", - icon="mdi:battery-off-outline", entity_category=EntityCategory.CONFIG, has_fn=lambda x: x.battery is not None, is_on_fn=lambda x: x.settings.battery.bypass if x.settings.battery else None, @@ -41,7 +41,6 @@ SWITCHES = [ ElgatoSwitchEntityDescription( key="energy_saving", translation_key="energy_saving", - icon="mdi:leaf", entity_category=EntityCategory.CONFIG, has_fn=lambda x: x.battery is not None, is_on_fn=lambda x: ( diff --git a/homeassistant/components/eliqonline/sensor.py b/homeassistant/components/eliqonline/sensor.py index 2a929db4b0a..2aa0ab15746 100644 --- a/homeassistant/components/eliqonline/sensor.py +++ b/homeassistant/components/eliqonline/sensor.py @@ -1,4 +1,5 @@ """Monitors home energy use for the ELIQ Online service.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/elkm1/__init__.py b/homeassistant/components/elkm1/__init__.py index 03f1f80b4f9..3b0c5f02f97 100644 --- a/homeassistant/components/elkm1/__init__.py +++ b/homeassistant/components/elkm1/__init__.py @@ -1,4 +1,5 @@ """Support the ElkM1 Gold and ElkM1 EZ8 alarm/integration panels.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/elkm1/alarm_control_panel.py b/homeassistant/components/elkm1/alarm_control_panel.py index bfac466caeb..5752bf82436 100644 --- a/homeassistant/components/elkm1/alarm_control_panel.py +++ b/homeassistant/components/elkm1/alarm_control_panel.py @@ -1,4 +1,5 @@ """Each ElkM1 area will be created as a separate alarm_control_panel.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/elkm1/binary_sensor.py b/homeassistant/components/elkm1/binary_sensor.py index 95f9162468e..c04a9d17830 100644 --- a/homeassistant/components/elkm1/binary_sensor.py +++ b/homeassistant/components/elkm1/binary_sensor.py @@ -1,4 +1,5 @@ """Support for control of ElkM1 binary sensors.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/elkm1/climate.py b/homeassistant/components/elkm1/climate.py index 97b16b14954..76ede0bbdf1 100644 --- a/homeassistant/components/elkm1/climate.py +++ b/homeassistant/components/elkm1/climate.py @@ -1,4 +1,5 @@ """Support for control of Elk-M1 connected thermostats.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/elkm1/config_flow.py b/homeassistant/components/elkm1/config_flow.py index e8d3f8cb0e4..5991c502ef6 100644 --- a/homeassistant/components/elkm1/config_flow.py +++ b/homeassistant/components/elkm1/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Elk-M1 Control integration.""" + from __future__ import annotations import logging @@ -8,8 +9,8 @@ from elkm1_lib.discovery import ElkSystem from elkm1_lib.elk import Elk import voluptuous as vol -from homeassistant import config_entries, exceptions from homeassistant.components import dhcp +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import ( CONF_ADDRESS, CONF_HOST, @@ -18,7 +19,7 @@ from homeassistant.const import ( CONF_PROTOCOL, CONF_USERNAME, ) -from homeassistant.data_entry_flow import FlowResult +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr from homeassistant.helpers.typing import DiscoveryInfoType from homeassistant.util import slugify @@ -77,7 +78,7 @@ async def validate_input(data: dict[str, str], mac: str | None) -> dict[str, str prefix = data[CONF_PREFIX] url = _make_url_from_data(data) - requires_password = url.startswith("elks://") or url.startswith("elksv1_2") + requires_password = url.startswith(("elks://", "elksv1_2")) if requires_password and (not userid or not password): raise InvalidAuth @@ -126,7 +127,7 @@ def _placeholders_from_device(device: ElkSystem) -> dict[str, str]: } -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class Elkm1ConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Elk-M1 Control.""" VERSION = 1 @@ -136,7 +137,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._discovered_device: ElkSystem | None = None self._discovered_devices: dict[str, ElkSystem] = {} - async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: + async def async_step_dhcp( + self, discovery_info: dhcp.DhcpServiceInfo + ) -> ConfigFlowResult: """Handle discovery via dhcp.""" self._discovered_device = ElkSystem( discovery_info.macaddress, discovery_info.ip, 0 @@ -146,7 +149,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_integration_discovery( self, discovery_info: DiscoveryInfoType - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle integration discovery.""" self._discovered_device = ElkSystem( discovery_info["mac_address"], @@ -158,7 +161,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) return await self._async_handle_discovery() - async def _async_handle_discovery(self) -> FlowResult: + async def _async_handle_discovery(self) -> ConfigFlowResult: """Handle any discovery.""" device = self._discovered_device assert device is not None @@ -191,7 +194,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_discovery_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm discovery.""" assert self._discovered_device is not None self.context["title_placeholders"] = _placeholders_from_device( @@ -201,7 +204,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" if user_input is not None: if mac := user_input[CONF_DEVICE]: @@ -236,7 +239,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def _async_create_or_error( self, user_input: dict[str, Any], importing: bool - ) -> tuple[dict[str, str] | None, FlowResult | None]: + ) -> tuple[dict[str, str] | None, ConfigFlowResult | None]: """Try to connect and create the entry or error.""" if self._url_already_configured(_make_url_from_data(user_input)): return None, self.async_abort(reason="address_already_configured") @@ -267,7 +270,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_discovered_connection( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle connecting the device when we have a discovery.""" errors: dict[str, str] | None = {} device = self._discovered_device @@ -299,7 +302,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_manual_connection( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle connecting the device when we need manual entry.""" errors: dict[str, str] | None = {} if user_input is not None: @@ -334,7 +337,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_import(self, user_input: dict[str, Any]) -> FlowResult: + async def async_step_import(self, user_input: dict[str, Any]) -> ConfigFlowResult: """Handle import.""" _LOGGER.debug("Elk is importing from yaml") url = _make_url_from_data(user_input) @@ -371,5 +374,5 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return hostname_from_url(url) in existing_hosts -class InvalidAuth(exceptions.HomeAssistantError): +class InvalidAuth(HomeAssistantError): """Error to indicate there is invalid auth.""" diff --git a/homeassistant/components/elkm1/discovery.py b/homeassistant/components/elkm1/discovery.py index 83b2d3f113b..8a68b6524b7 100644 --- a/homeassistant/components/elkm1/discovery.py +++ b/homeassistant/components/elkm1/discovery.py @@ -1,4 +1,5 @@ """The elkm1 integration discovery.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/elkm1/icons.json b/homeassistant/components/elkm1/icons.json new file mode 100644 index 00000000000..3bb9ea8c87d --- /dev/null +++ b/homeassistant/components/elkm1/icons.json @@ -0,0 +1,27 @@ +{ + "entity": { + "sensor": { + "panel": { + "default": "mdi:home" + }, + "setting": { + "default": "mdi:numeric" + } + } + }, + "services": { + "alarm_bypass": "mdi:shield-off", + "alarm_clear_bypass": "mdi:shield", + "alarm_arm_home_instant": "mdi:shield-lock", + "alarm_arm_night_instant": "mdi:shield-moon", + "alarm_arm_vacation": "mdi:beach", + "alarm_display_message": "mdi:message-alert", + "set_time": "mdi:clock-edit", + "speak_phrase": "mdi:message-processing", + "speak_word": "mdi:message-minus", + "sensor_counter_refresh": "mdi:refresh", + "sensor_counter_set": "mdi:counter", + "sensor_zone_bypass": "mdi:shield-off", + "sensor_zone_trigger": "mdi:shield" + } +} diff --git a/homeassistant/components/elkm1/light.py b/homeassistant/components/elkm1/light.py index 844e4f3dd15..432d6683de4 100644 --- a/homeassistant/components/elkm1/light.py +++ b/homeassistant/components/elkm1/light.py @@ -1,4 +1,5 @@ """Support for control of ElkM1 lighting (X10, UPB, etc).""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/elkm1/logbook.py b/homeassistant/components/elkm1/logbook.py index e86e58d23fd..b31c537d93f 100644 --- a/homeassistant/components/elkm1/logbook.py +++ b/homeassistant/components/elkm1/logbook.py @@ -1,4 +1,5 @@ """Describe elkm1 logbook events.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/elkm1/models.py b/homeassistant/components/elkm1/models.py index 9f784951c11..7dd3313782e 100644 --- a/homeassistant/components/elkm1/models.py +++ b/homeassistant/components/elkm1/models.py @@ -1,4 +1,5 @@ """The elkm1 integration models.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/homeassistant/components/elkm1/scene.py b/homeassistant/components/elkm1/scene.py index 9cb0c62ff77..9658052f3e5 100644 --- a/homeassistant/components/elkm1/scene.py +++ b/homeassistant/components/elkm1/scene.py @@ -1,4 +1,5 @@ """Support for control of ElkM1 tasks ("macros").""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/elkm1/sensor.py b/homeassistant/components/elkm1/sensor.py index 9bd78f61673..27a6c1596eb 100644 --- a/homeassistant/components/elkm1/sensor.py +++ b/homeassistant/components/elkm1/sensor.py @@ -1,4 +1,5 @@ """Support for control of ElkM1 sensors.""" + from __future__ import annotations from typing import Any @@ -161,7 +162,7 @@ class ElkKeypad(ElkSensor): class ElkPanel(ElkSensor): """Representation of an Elk-M1 Panel.""" - _attr_icon = "mdi:home" + _attr_translation_key = "panel" _attr_entity_category = EntityCategory.DIAGNOSTIC _element: Panel @@ -184,7 +185,7 @@ class ElkPanel(ElkSensor): class ElkSetting(ElkSensor): """Representation of an Elk-M1 Setting.""" - _attr_icon = "mdi:numeric" + _attr_translation_key = "setting" _element: Setting def _element_changed(self, _: Element, changeset: Any) -> None: diff --git a/homeassistant/components/elkm1/switch.py b/homeassistant/components/elkm1/switch.py index b4080adc698..3224f9affcf 100644 --- a/homeassistant/components/elkm1/switch.py +++ b/homeassistant/components/elkm1/switch.py @@ -1,4 +1,5 @@ """Support for control of ElkM1 outputs (relays).""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/elmax/__init__.py b/homeassistant/components/elmax/__init__.py index 0c0a80b4958..518bf1e932b 100644 --- a/homeassistant/components/elmax/__init__.py +++ b/homeassistant/components/elmax/__init__.py @@ -1,14 +1,32 @@ """The elmax-cloud integration.""" + from __future__ import annotations from datetime import timedelta import logging +from elmax_api.exceptions import ElmaxBadLoginError +from elmax_api.http import Elmax, ElmaxLocal, GenericElmax +from elmax_api.model.panel import PanelEntry + from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed -from .common import ElmaxCoordinator +from .common import ( + DirectPanel, + ElmaxCoordinator, + build_direct_ssl_context, + get_direct_api_url, +) from .const import ( + CONF_ELMAX_MODE, + CONF_ELMAX_MODE_CLOUD, + CONF_ELMAX_MODE_DIRECT, + CONF_ELMAX_MODE_DIRECT_HOST, + CONF_ELMAX_MODE_DIRECT_PORT, + CONF_ELMAX_MODE_DIRECT_SSL, + CONF_ELMAX_MODE_DIRECT_SSL_CERT, CONF_ELMAX_PANEL_ID, CONF_ELMAX_PANEL_PIN, CONF_ELMAX_PASSWORD, @@ -21,17 +39,71 @@ from .const import ( _LOGGER = logging.getLogger(__name__) +async def _load_elmax_panel_client( + entry: ConfigEntry, +) -> tuple[GenericElmax, PanelEntry]: + # Connection mode was not present in initial version, default to cloud if not set + mode = entry.data.get(CONF_ELMAX_MODE, CONF_ELMAX_MODE_CLOUD) + if mode == CONF_ELMAX_MODE_DIRECT: + client_api_url = get_direct_api_url( + host=entry.data[CONF_ELMAX_MODE_DIRECT_HOST], + port=entry.data[CONF_ELMAX_MODE_DIRECT_PORT], + use_ssl=entry.data[CONF_ELMAX_MODE_DIRECT_SSL], + ) + custom_ssl_context = None + custom_ssl_cert = entry.data.get(CONF_ELMAX_MODE_DIRECT_SSL_CERT) + if custom_ssl_cert: + custom_ssl_context = build_direct_ssl_context(cadata=custom_ssl_cert) + + client = ElmaxLocal( + panel_api_url=client_api_url, + panel_code=entry.data[CONF_ELMAX_PANEL_PIN], + ssl_context=custom_ssl_context, + ) + panel = DirectPanel(panel_uri=client_api_url) + else: + client = Elmax( + username=entry.data[CONF_ELMAX_USERNAME], + password=entry.data[CONF_ELMAX_PASSWORD], + ) + client.set_current_panel( + entry.data[CONF_ELMAX_PANEL_ID], entry.data[CONF_ELMAX_PANEL_PIN] + ) + # Make sure the panel is online and assigned to the current user + panel = await _check_cloud_panel_status(client, entry.data[CONF_ELMAX_PANEL_ID]) + + return client, panel + + +async def _check_cloud_panel_status(client: Elmax, panel_id: str) -> PanelEntry: + """Perform integrity checks against the cloud for panel-user association.""" + # Retrieve the panel online status first + panels = await client.list_control_panels() + panel = next((panel for panel in panels if panel.hash == panel_id), None) + + # If the panel is no longer available within the ones associated to that client, raise + # a config error as the user must reconfigure it in order to make it work again + if not panel: + raise ConfigEntryAuthFailed( + f"Panel ID {panel_id} is no longer linked to this user account" + ) + return panel + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up elmax-cloud from a config entry.""" + try: + client, panel = await _load_elmax_panel_client(entry) + except ElmaxBadLoginError as err: + raise ConfigEntryAuthFailed from err + # Create the API client object and attempt a login, so that we immediately know # if there is something wrong with user credentials coordinator = ElmaxCoordinator( hass=hass, logger=_LOGGER, - username=entry.data[CONF_ELMAX_USERNAME], - password=entry.data[CONF_ELMAX_PASSWORD], - panel_id=entry.data[CONF_ELMAX_PANEL_ID], - panel_pin=entry.data[CONF_ELMAX_PANEL_PIN], + elmax_api_client=client, + panel=panel, name=f"Elmax Cloud {entry.entry_id}", update_interval=timedelta(seconds=POLLING_SECONDS), ) @@ -42,11 +114,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Store a global reference to the coordinator for later use hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + entry.async_on_unload(entry.add_update_listener(async_reload_entry)) + # Perform platform initialization. await hass.config_entries.async_forward_entry_setups(entry, ELMAX_PLATFORMS) return True +async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Handle an 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.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, ELMAX_PLATFORMS) diff --git a/homeassistant/components/elmax/alarm_control_panel.py b/homeassistant/components/elmax/alarm_control_panel.py index 40c84efc60e..b9a895f6967 100644 --- a/homeassistant/components/elmax/alarm_control_panel.py +++ b/homeassistant/components/elmax/alarm_control_panel.py @@ -1,4 +1,5 @@ """Elmax sensor platform.""" + from __future__ import annotations from elmax_api.model.alarm_status import AlarmArmStatus, AlarmStatus @@ -39,7 +40,6 @@ async def async_setup_entry( # Otherwise, add all the entities we found entities = [ ElmaxArea( - panel=coordinator.panel_entry, elmax_device=area, panel_version=panel_status.release, coordinator=coordinator, diff --git a/homeassistant/components/elmax/binary_sensor.py b/homeassistant/components/elmax/binary_sensor.py index 0defbe464f9..b3bdc174246 100644 --- a/homeassistant/components/elmax/binary_sensor.py +++ b/homeassistant/components/elmax/binary_sensor.py @@ -1,4 +1,5 @@ """Elmax sensor platform.""" + from __future__ import annotations from elmax_api.model.panel import PanelStatus @@ -38,7 +39,6 @@ async def async_setup_entry( if zone.endpoint_id in known_devices: continue entity = ElmaxSensor( - panel=coordinator.panel_entry, elmax_device=zone, panel_version=panel_status.release, coordinator=coordinator, diff --git a/homeassistant/components/elmax/common.py b/homeassistant/components/elmax/common.py index 7cbc6f63596..39b6797fc58 100644 --- a/homeassistant/components/elmax/common.py +++ b/homeassistant/components/elmax/common.py @@ -1,10 +1,12 @@ """Elmax integration common classes and utilities.""" + from __future__ import annotations -import asyncio +from asyncio import timeout from datetime import timedelta import logging from logging import Logger +import ssl from elmax_api.exceptions import ( ElmaxApiError, @@ -13,12 +15,14 @@ from elmax_api.exceptions import ( ElmaxNetworkError, ElmaxPanelBusyError, ) -from elmax_api.http import Elmax +from elmax_api.http import Elmax, GenericElmax from elmax_api.model.actuator import Actuator from elmax_api.model.area import Area from elmax_api.model.cover import Cover from elmax_api.model.endpoint import DeviceEndpoint from elmax_api.model.panel import PanelEntry, PanelStatus +from httpx import ConnectError, ConnectTimeout +from packaging import version from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, HomeAssistantError @@ -29,11 +33,50 @@ from homeassistant.helpers.update_coordinator import ( UpdateFailed, ) -from .const import DEFAULT_TIMEOUT, DOMAIN +from .const import ( + DEFAULT_TIMEOUT, + DOMAIN, + ELMAX_LOCAL_API_PATH, + MIN_APIV2_SUPPORTED_VERSION, +) _LOGGER = logging.getLogger(__name__) +def get_direct_api_url(host: str, port: int, use_ssl: bool) -> str: + """Return the direct API url given the base URI.""" + schema = "https" if use_ssl else "http" + return f"{schema}://{host}:{port}/{ELMAX_LOCAL_API_PATH}" + + +def build_direct_ssl_context(cadata: str) -> ssl.SSLContext: + """Create a custom SSL context for direct-api verification.""" + context = ssl.SSLContext(protocol=ssl.PROTOCOL_TLS_CLIENT) + context.check_hostname = False + context.verify_mode = ssl.CERT_REQUIRED + context.load_verify_locations(cadata=cadata) + return context + + +def check_local_version_supported(api_version: str | None) -> bool: + """Check whether the given API version is supported.""" + if api_version is None: + return False + return version.parse(api_version) >= version.parse(MIN_APIV2_SUPPORTED_VERSION) + + +class DirectPanel(PanelEntry): + """Helper class for wrapping a directly accessed Elmax Panel.""" + + def __init__(self, panel_uri): + """Construct the object.""" + super().__init__(panel_uri, True, {}) + + def get_name_by_user(self, username: str) -> str: + """Return the panel name.""" + return f"Direct Panel {self.hash}" + + class ElmaxCoordinator(DataUpdateCoordinator[PanelStatus]): # pylint: disable=hass-enforce-coordinator-module """Coordinator helper to handle Elmax API polling.""" @@ -41,25 +84,21 @@ class ElmaxCoordinator(DataUpdateCoordinator[PanelStatus]): # pylint: disable=h self, hass: HomeAssistant, logger: Logger, - username: str, - password: str, - panel_id: str, - panel_pin: str, + elmax_api_client: GenericElmax, + panel: PanelEntry, name: str, update_interval: timedelta, ) -> None: """Instantiate the object.""" - self._client = Elmax(username=username, password=password) - self._panel_id = panel_id - self._panel_pin = panel_pin - self._panel_entry = None + self._client = elmax_api_client + self._panel_entry = panel self._state_by_endpoint = None super().__init__( hass=hass, logger=logger, name=name, update_interval=update_interval ) @property - def panel_entry(self) -> PanelEntry | None: + def panel_entry(self) -> PanelEntry: """Return the panel entry.""" return self._panel_entry @@ -92,54 +131,46 @@ class ElmaxCoordinator(DataUpdateCoordinator[PanelStatus]): # pylint: disable=h """Return the current http client being used by this instance.""" return self._client + @http_client.setter + def http_client(self, client: GenericElmax): + """Set the client library instance for Elmax API.""" + self._client = client + async def _async_update_data(self): try: - async with asyncio.timeout(DEFAULT_TIMEOUT): - # Retrieve the panel online status first - panels = await self._client.list_control_panels() - panel = next( - (panel for panel in panels if panel.hash == self._panel_id), None - ) + async with timeout(DEFAULT_TIMEOUT): + # The following command might fail in case of the panel is offline. + # We handle this case in the following exception blocks. + status = await self._client.get_current_panel_status() - # If the panel is no more available within the given. Raise config error as the user must - # reconfigure it in order to make it work again - if not panel: - raise ConfigEntryAuthFailed( - f"Panel ID {self._panel_id} is no more linked to this user" - " account" - ) - - self._panel_entry = panel - - # If the panel is online, proceed with fetching its state - # and return it right away - if panel.online: - status = await self._client.get_panel_status( - control_panel_id=panel.hash, pin=self._panel_pin - ) # type: PanelStatus - - # Store a dictionary for fast endpoint state access - self._state_by_endpoint = { - k.endpoint_id: k for k in status.all_endpoints - } - return status - - # Otherwise, return None. Listeners will know that this means the device is offline - return None + # Store a dictionary for fast endpoint state access + self._state_by_endpoint = { + k.endpoint_id: k for k in status.all_endpoints + } + return status except ElmaxBadPinError as err: raise ConfigEntryAuthFailed("Control panel pin was refused") from err except ElmaxBadLoginError as err: - raise ConfigEntryAuthFailed("Refused username/password") from err + raise ConfigEntryAuthFailed("Refused username/password/pin") from err except ElmaxApiError as err: raise UpdateFailed(f"Error communicating with ELMAX API: {err}") from err except ElmaxPanelBusyError as err: raise UpdateFailed( "Communication with the panel failed, as it is currently busy" ) from err - except ElmaxNetworkError as err: + except (ConnectError, ConnectTimeout, ElmaxNetworkError) as err: + if isinstance(self._client, Elmax): + raise UpdateFailed( + "A communication error has occurred. " + "Make sure HA can reach the internet and that " + "your firewall allows communication with the Meross Cloud." + ) from err + raise UpdateFailed( - "A network error occurred while communicating with Elmax cloud." + "A communication error has occurred. " + "Make sure the panel is online and that " + "your firewall allows communication with it." ) from err @@ -148,20 +179,18 @@ class ElmaxEntity(CoordinatorEntity[ElmaxCoordinator]): def __init__( self, - panel: PanelEntry, elmax_device: DeviceEndpoint, panel_version: str, coordinator: ElmaxCoordinator, ) -> None: """Construct the object.""" super().__init__(coordinator=coordinator) - self._panel = panel self._device = elmax_device self._attr_unique_id = elmax_device.endpoint_id self._attr_name = elmax_device.name self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, panel.hash)}, - name=panel.get_name_by_user( + identifiers={(DOMAIN, coordinator.panel_entry.hash)}, + name=coordinator.panel_entry.get_name_by_user( coordinator.http_client.get_authenticated_username() ), manufacturer="Elmax", @@ -172,4 +201,4 @@ class ElmaxEntity(CoordinatorEntity[ElmaxCoordinator]): @property def available(self) -> bool: """Return if entity is available.""" - return super().available and self._panel.online + return super().available and self.coordinator.panel_entry.online diff --git a/homeassistant/components/elmax/config_flow.py b/homeassistant/components/elmax/config_flow.py index 5b9bb3b1085..666f4e75fcd 100644 --- a/homeassistant/components/elmax/config_flow.py +++ b/homeassistant/components/elmax/config_flow.py @@ -1,4 +1,5 @@ """Config flow for elmax-cloud integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -6,21 +7,36 @@ import logging from typing import Any from elmax_api.exceptions import ElmaxBadLoginError, ElmaxBadPinError, ElmaxNetworkError -from elmax_api.http import Elmax -from elmax_api.model.panel import PanelEntry +from elmax_api.http import Elmax, ElmaxLocal, GenericElmax +from elmax_api.model.panel import PanelEntry, PanelStatus +import httpx import voluptuous as vol -from homeassistant import config_entries -from homeassistant.data_entry_flow import FlowResult +from homeassistant.components.zeroconf import ZeroconfServiceInfo +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.exceptions import HomeAssistantError +from .common import ( + build_direct_ssl_context, + check_local_version_supported, + get_direct_api_url, +) from .const import ( + CONF_ELMAX_MODE, + CONF_ELMAX_MODE_CLOUD, + CONF_ELMAX_MODE_DIRECT, + CONF_ELMAX_MODE_DIRECT_HOST, + CONF_ELMAX_MODE_DIRECT_PORT, + CONF_ELMAX_MODE_DIRECT_SSL, + CONF_ELMAX_MODE_DIRECT_SSL_CERT, CONF_ELMAX_PANEL_ID, CONF_ELMAX_PANEL_NAME, CONF_ELMAX_PANEL_PIN, CONF_ELMAX_PASSWORD, CONF_ELMAX_USERNAME, DOMAIN, + ELMAX_MODE_DIRECT_DEFAULT_HTTP_PORT, + ELMAX_MODE_DIRECT_DEFAULT_HTTPS_PORT, ) _LOGGER = logging.getLogger(__name__) @@ -40,6 +56,22 @@ REAUTH_FORM_SCHEMA = vol.Schema( } ) +DIRECT_SETUP_SCHEMA = vol.Schema( + { + vol.Required(CONF_ELMAX_MODE_DIRECT_HOST): str, + vol.Required(CONF_ELMAX_MODE_DIRECT_PORT, default=443): int, + vol.Required(CONF_ELMAX_MODE_DIRECT_SSL, default=True): bool, + vol.Required(CONF_ELMAX_PANEL_PIN): str, + } +) + +ZEROCONF_SETUP_SCHEMA = vol.Schema( + { + vol.Required(CONF_ELMAX_PANEL_PIN): str, + vol.Required(CONF_ELMAX_MODE_DIRECT_SSL, default=True): bool, + } +) + def _store_panel_by_name( panel: PanelEntry, username: str, panel_names: dict[str, str] @@ -55,43 +87,223 @@ def _store_panel_by_name( panel_names[panel_name] = panel_id -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class ElmaxConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for elmax-cloud.""" VERSION = 1 _client: Elmax - _username: str - _password: str + _selected_mode: str + _panel_pin: str + _panel_id: str + + # Direct API variables + _panel_direct_use_ssl: bool + _panel_direct_hostname: str + _panel_direct_port: int + _panel_direct_follow_mdns: bool + _panel_direct_ssl_cert: str | None + _panel_direct_http_port: int + _panel_direct_https_port: int + + # Cloud API variables + _cloud_username: str + _cloud_password: str + _reauth_cloud_username: str | None + _reauth_cloud_panelid: str | None + + # Panel selection variables _panels_schema: vol.Schema _panel_names: dict - _entry: config_entries.ConfigEntry | None + _entry: ConfigEntry | None async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: - """Handle a flow initialized by the user.""" + ) -> ConfigFlowResult: + """Handle the flow initiated by the user.""" + return await self.async_step_choose_mode(user_input=user_input) + + async def async_step_choose_mode( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle local vs cloud mode selection step.""" + return self.async_show_menu( + step_id="choose_mode", + menu_options={ + CONF_ELMAX_MODE_CLOUD: "Connect to Elmax Panel via Elmax Cloud APIs", + CONF_ELMAX_MODE_DIRECT: "Connect to Elmax Panel via local/direct IP", + }, + ) + + async def _handle_direct_and_create_entry( + self, fallback_step_id: str, schema: vol.Schema + ) -> ConfigFlowResult: + return await self._test_direct_and_create_entry() + + async def _test_direct_and_create_entry(self): + """Test the direct connection to the Elmax panel and create and entry if successful.""" + ssl_context = None + self._panel_direct_ssl_cert = None + if self._panel_direct_use_ssl: + # Fetch the remote certificate. + # Local API is exposed via a self-signed SSL that we must add to our trust store. + self._panel_direct_ssl_cert = ( + await GenericElmax.retrieve_server_certificate( + hostname=self._panel_direct_hostname, + port=self._panel_direct_port, + ) + ) + ssl_context = build_direct_ssl_context(cadata=self._panel_direct_ssl_cert) + + # Attempt the connection to make sure the pin works. Also, take the chance to retrieve the panel ID via APIs. + client_api_url = get_direct_api_url( + host=self._panel_direct_hostname, + port=self._panel_direct_port, + use_ssl=self._panel_direct_use_ssl, + ) + client = ElmaxLocal( + panel_api_url=client_api_url, + panel_code=self._panel_pin, + ssl_context=ssl_context, + ) + try: + await client.login() + except (ElmaxNetworkError, httpx.ConnectError, httpx.ConnectTimeout): + return self.async_show_form( + step_id=CONF_ELMAX_MODE_DIRECT, + data_schema=DIRECT_SETUP_SCHEMA, + errors={"base": "network_error"}, + ) + except ElmaxBadLoginError: + return self.async_show_form( + step_id=CONF_ELMAX_MODE_DIRECT, + data_schema=DIRECT_SETUP_SCHEMA, + errors={"base": "invalid_auth"}, + ) + + # Retrieve the current panel status. If this succeeds, it means the + # setup did complete successfully. + panel_status: PanelStatus = await client.get_current_panel_status() + + # Make sure this is the only Elmax integration for this specific panel id. + await self.async_set_unique_id(panel_status.panel_id) + self._abort_if_unique_id_configured() + + return await self._check_unique_and_create_entry( + unique_id=panel_status.panel_id, + title=f"Elmax Direct {panel_status.panel_id}", + data={ + CONF_ELMAX_MODE: self._selected_mode, + CONF_ELMAX_MODE_DIRECT_HOST: self._panel_direct_hostname, + CONF_ELMAX_MODE_DIRECT_PORT: self._panel_direct_port, + CONF_ELMAX_MODE_DIRECT_SSL: self._panel_direct_use_ssl, + CONF_ELMAX_PANEL_PIN: self._panel_pin, + CONF_ELMAX_PANEL_ID: panel_status.panel_id, + CONF_ELMAX_MODE_DIRECT_SSL_CERT: self._panel_direct_ssl_cert, + }, + ) + + async def async_step_direct(self, user_input: dict[str, Any]) -> ConfigFlowResult: + """Handle the direct setup step.""" + self._selected_mode = CONF_ELMAX_MODE_CLOUD + if user_input is None: + return self.async_show_form( + step_id=CONF_ELMAX_MODE_DIRECT, + data_schema=DIRECT_SETUP_SCHEMA, + errors=None, + ) + + self._panel_direct_hostname = user_input[CONF_ELMAX_MODE_DIRECT_HOST] + self._panel_direct_port = user_input[CONF_ELMAX_MODE_DIRECT_PORT] + self._panel_direct_use_ssl = user_input[CONF_ELMAX_MODE_DIRECT_SSL] + self._panel_pin = user_input[CONF_ELMAX_PANEL_PIN] + self._panel_direct_follow_mdns = True + + tmp_schema = vol.Schema( + { + vol.Required( + CONF_ELMAX_MODE_DIRECT_HOST, default=self._panel_direct_hostname + ): str, + vol.Required( + CONF_ELMAX_MODE_DIRECT_PORT, default=self._panel_direct_port + ): int, + vol.Required( + CONF_ELMAX_MODE_DIRECT_SSL, default=self._panel_direct_use_ssl + ): bool, + vol.Required(CONF_ELMAX_PANEL_PIN, default=self._panel_pin): str, + } + ) + return await self._handle_direct_and_create_entry( + fallback_step_id=CONF_ELMAX_MODE_DIRECT, schema=tmp_schema + ) + + async def async_step_zeroconf_setup( + self, user_input: dict[str, Any] + ) -> ConfigFlowResult: + """Handle the direct setup step triggered via zeroconf.""" + if user_input is None: + return self.async_show_form( + step_id="zeroconf_setup", + data_schema=ZEROCONF_SETUP_SCHEMA, + errors=None, + ) + self._panel_direct_use_ssl = user_input[CONF_ELMAX_MODE_DIRECT_SSL] + self._panel_direct_port = ( + self._panel_direct_https_port + if self._panel_direct_use_ssl + else self._panel_direct_http_port + ) + self._panel_pin = user_input[CONF_ELMAX_PANEL_PIN] + tmp_schema = vol.Schema( + { + vol.Required(CONF_ELMAX_PANEL_PIN, default=self._panel_pin): str, + vol.Required( + CONF_ELMAX_MODE_DIRECT_SSL, default=self._panel_direct_use_ssl + ): bool, + } + ) + return await self._handle_direct_and_create_entry( + fallback_step_id="zeroconf_setup", schema=tmp_schema + ) + + async def _check_unique_and_create_entry( + self, unique_id: str, title: str, data: Mapping[str, Any] + ) -> ConfigFlowResult: + # Make sure this is the only Elmax integration for this specific panel id. + await self.async_set_unique_id(unique_id) + self._abort_if_unique_id_configured() + + return self.async_create_entry( + title=title, + data=data, + ) + + async def async_step_cloud(self, user_input: dict[str, Any]) -> ConfigFlowResult: + """Handle the cloud setup flow.""" + self._selected_mode = CONF_ELMAX_MODE_CLOUD + # When invokes without parameters, show the login form. if user_input is None: - return self.async_show_form(step_id="user", data_schema=LOGIN_FORM_SCHEMA) - - username = user_input[CONF_ELMAX_USERNAME] - password = user_input[CONF_ELMAX_PASSWORD] + return self.async_show_form( + step_id=CONF_ELMAX_MODE_CLOUD, data_schema=LOGIN_FORM_SCHEMA, errors={} + ) # Otherwise, it means we are handling now the "submission" of the user form. # In this case, let's try to log in to the Elmax cloud and retrieve the available panels. + username = user_input[CONF_ELMAX_USERNAME] + password = user_input[CONF_ELMAX_PASSWORD] try: client = await self._async_login(username=username, password=password) except ElmaxBadLoginError: return self.async_show_form( - step_id="user", + step_id=CONF_ELMAX_MODE_CLOUD, data_schema=LOGIN_FORM_SCHEMA, errors={"base": "invalid_auth"}, ) except ElmaxNetworkError: _LOGGER.exception("A network error occurred") return self.async_show_form( - step_id="user", + step_id=CONF_ELMAX_MODE_CLOUD, data_schema=LOGIN_FORM_SCHEMA, errors={"base": "network_error"}, ) @@ -102,7 +314,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): # If no online panel was found, we display an error in the next UI. if not online_panels: return self.async_show_form( - step_id="user", + step_id=CONF_ELMAX_MODE_CLOUD, data_schema=LOGIN_FORM_SCHEMA, errors={"base": "no_panel_online"}, ) @@ -126,14 +338,14 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): } ) self._panels_schema = schema - self._username = username - self._password = password + self._cloud_username = username + self._cloud_password = password # If everything went OK, proceed to panel selection. return await self.async_step_panels(user_input=None) async def async_step_panels( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle Panel selection step.""" errors: dict[str, Any] = {} if user_input is None: @@ -156,39 +368,48 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): await self._client.get_panel_status( control_panel_id=panel_id, pin=panel_pin ) - return self.async_create_entry( - title=f"Elmax {panel_name}", - data={ - CONF_ELMAX_PANEL_ID: panel_id, - CONF_ELMAX_PANEL_PIN: panel_pin, - CONF_ELMAX_USERNAME: self._username, - CONF_ELMAX_PASSWORD: self._password, - }, - ) except ElmaxBadPinError: errors["base"] = "invalid_pin" except Exception: # pylint: disable=broad-except _LOGGER.exception("Error occurred") errors["base"] = "unknown" - return self.async_show_form( - step_id="panels", data_schema=self._panels_schema, errors=errors + if errors: + return self.async_show_form( + step_id="panels", data_schema=self._panels_schema, errors=errors + ) + + return await self._check_unique_and_create_entry( + unique_id=panel_id, + title=f"Elmax cloud {panel_name}", + data={ + CONF_ELMAX_MODE: CONF_ELMAX_MODE_CLOUD, + CONF_ELMAX_PANEL_ID: panel_id, + CONF_ELMAX_PANEL_PIN: panel_pin, + CONF_ELMAX_USERNAME: self._cloud_username, + CONF_ELMAX_PASSWORD: self._cloud_password, + }, ) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Perform reauth upon an API authentication error.""" self._entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) + self._reauth_cloud_username = entry_data.get(CONF_ELMAX_USERNAME) + self._reauth_cloud_panelid = entry_data.get(CONF_ELMAX_PANEL_ID) return await self.async_step_reauth_confirm() async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle reauthorization flow.""" errors = {} if user_input is not None: username = user_input[CONF_ELMAX_USERNAME] password = user_input[CONF_ELMAX_PASSWORD] panel_pin = user_input[CONF_ELMAX_PANEL_PIN] + await self.async_set_unique_id(self._reauth_cloud_panelid) # Handle authentication, make sure the panel we are re-authenticating against is listed among results # and verify its pin is correct. @@ -203,7 +424,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if p.hash == self._entry.data[CONF_ELMAX_PANEL_ID] ] if len(panels) < 1: - raise NoOnlinePanelsError() + raise NoOnlinePanelsError # Verify the pin is still valid. await client.get_panel_status( @@ -237,6 +458,89 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="reauth_confirm", data_schema=REAUTH_FORM_SCHEMA, errors=errors ) + async def _async_handle_entry_match( + self, + local_id: str, + remote_id: str | None, + host: str, + https_port: int, + http_port: int, + ) -> ConfigFlowResult | None: + # Look for another entry with the same PANEL_ID (local or remote). + # If there already is a matching panel, take the change to notify the Coordinator + # so that it uses the newly discovered IP address. This mitigates the issues + # arising with DHCP and IP changes of the panels. + for entry in self._async_current_entries(include_ignore=False): + if entry.data[CONF_ELMAX_PANEL_ID] in (local_id, remote_id): + # If the discovery finds another entry with the same ID, skip the notification. + # However, if the discovery finds a new host for a panel that was already registered + # for a given host (leave PORT comparison aside as we don't want to get notified twice + # for HTTP and HTTPS), update the entry so that the integration "follows" the DHCP IP. + if ( + entry.data.get(CONF_ELMAX_MODE, CONF_ELMAX_MODE_CLOUD) + == CONF_ELMAX_MODE_DIRECT + and entry.data[CONF_ELMAX_MODE_DIRECT_HOST] != host + ): + new_data: dict[str, Any] = {} + new_data.update(entry.data) + new_data[CONF_ELMAX_MODE_DIRECT_HOST] = host + new_data[CONF_ELMAX_MODE_DIRECT_PORT] = ( + https_port + if entry.data[CONF_ELMAX_MODE_DIRECT_SSL] + else http_port + ) + self.hass.config_entries.async_update_entry( + entry, unique_id=entry.unique_id, data=new_data + ) + # Abort the configuration, as there already is an entry for this PANEL-ID. + return self.async_abort(reason="already_configured") + return None + + async def async_step_zeroconf( + self, discovery_info: ZeroconfServiceInfo + ) -> ConfigFlowResult: + """Handle device found via zeroconf.""" + host = discovery_info.host + https_port = ( + int(discovery_info.port) + if discovery_info.port is not None + else ELMAX_MODE_DIRECT_DEFAULT_HTTPS_PORT + ) + plain_http_port = discovery_info.properties.get( + "http_port", ELMAX_MODE_DIRECT_DEFAULT_HTTP_PORT + ) + plain_http_port = int(plain_http_port) + local_id = discovery_info.properties.get("idl") + remote_id = discovery_info.properties.get("idr") + v2api_version = discovery_info.properties.get("v2") + + # Only deal with panels exposing v2 version + if not check_local_version_supported(v2api_version): + return self.async_abort(reason="not_supported") + + # Handle the discovered panel info. This is useful especially if the panel + # changes its IP address while remaining perfectly configured. + if ( + local_id is not None + and ( + abort_result := await self._async_handle_entry_match( + local_id, remote_id, host, https_port, plain_http_port + ) + ) + is not None + ): + return abort_result + + self._selected_mode = CONF_ELMAX_MODE_DIRECT + self._panel_direct_hostname = host + self._panel_direct_https_port = https_port + self._panel_direct_http_port = plain_http_port + self._panel_direct_follow_mdns = True + + return self.async_show_form( + step_id="zeroconf_setup", data_schema=ZEROCONF_SETUP_SCHEMA + ) + @staticmethod async def _async_login(username: str, password: str) -> Elmax: """Log in to the Elmax cloud and return the http client.""" diff --git a/homeassistant/components/elmax/const.py b/homeassistant/components/elmax/const.py index cd2c73002a4..d87ccbd014e 100644 --- a/homeassistant/components/elmax/const.py +++ b/homeassistant/components/elmax/const.py @@ -1,13 +1,26 @@ """Constants for the elmax-cloud integration.""" + from homeassistant.const import Platform DOMAIN = "elmax" CONF_ELMAX_USERNAME = "username" CONF_ELMAX_PASSWORD = "password" CONF_ELMAX_PANEL_ID = "panel_id" +CONF_ELMAX_PANEL_LOCAL_ID = "panel_local_id" +CONF_ELMAX_PANEL_REMOTE_ID = "panel_remote_id" CONF_ELMAX_PANEL_PIN = "panel_pin" CONF_ELMAX_PANEL_NAME = "panel_name" +CONF_ELMAX_MODE = "mode" +CONF_ELMAX_MODE_CLOUD = "cloud" +CONF_ELMAX_MODE_DIRECT = "direct" +CONF_ELMAX_MODE_DIRECT_HOST = "panel_api_host" +CONF_ELMAX_MODE_DIRECT_PORT = "panel_api_port" +CONF_ELMAX_MODE_DIRECT_SSL = "use_ssl" +CONF_ELMAX_MODE_DIRECT_SSL_CERT = "ssl_cert" + +ELMAX_LOCAL_API_PATH = "api/v2" + CONF_CONFIG_ENTRY_ID = "config_entry_id" CONF_ENDPOINT_ID = "endpoint_id" @@ -18,5 +31,8 @@ ELMAX_PLATFORMS = [ Platform.COVER, ] +ELMAX_MODE_DIRECT_DEFAULT_HTTPS_PORT = 443 +ELMAX_MODE_DIRECT_DEFAULT_HTTP_PORT = 80 POLLING_SECONDS = 30 DEFAULT_TIMEOUT = 10.0 +MIN_APIV2_SUPPORTED_VERSION = "4.9.13" diff --git a/homeassistant/components/elmax/cover.py b/homeassistant/components/elmax/cover.py index e05b17b9171..6113ccd7997 100644 --- a/homeassistant/components/elmax/cover.py +++ b/homeassistant/components/elmax/cover.py @@ -1,4 +1,5 @@ """Elmax cover platform.""" + from __future__ import annotations import logging @@ -49,7 +50,6 @@ async def async_setup_entry( if cover.endpoint_id in known_devices: continue entity = ElmaxCover( - panel=coordinator.panel_entry, elmax_device=cover, panel_version=panel_status.release, coordinator=coordinator, diff --git a/homeassistant/components/elmax/manifest.json b/homeassistant/components/elmax/manifest.json index dfb90763c83..181b1c8a882 100644 --- a/homeassistant/components/elmax/manifest.json +++ b/homeassistant/components/elmax/manifest.json @@ -6,5 +6,10 @@ "documentation": "https://www.home-assistant.io/integrations/elmax", "iot_class": "cloud_polling", "loggers": ["elmax_api"], - "requirements": ["elmax-api==0.0.4"] + "requirements": ["elmax-api==0.0.4"], + "zeroconf": [ + { + "type": "_elmax-ssl._tcp.local." + } + ] } diff --git a/homeassistant/components/elmax/strings.json b/homeassistant/components/elmax/strings.json index 4bc705adfbe..17cdaac0bb8 100644 --- a/homeassistant/components/elmax/strings.json +++ b/homeassistant/components/elmax/strings.json @@ -1,13 +1,42 @@ { "config": { "step": { - "user": { + "choose_mode": { + "description": "Please choose the connection mode to Elmax panels.", + "menu_options": { + "cloud": "Connect to Elmax Panel via Elmax Cloud APIs", + "direct": "Connect to Elmax Panel via local/direct IP" + } + }, + "cloud": { "description": "Please login to the Elmax cloud using your credentials", "data": { "password": "[%key:common::config_flow::data::password%]", "username": "[%key:common::config_flow::data::username%]" } }, + "zeroconf_setup": { + "description": "Configure discovered local Elmax panel", + "data": { + "panel_pin": "Panel PIN code", + "use_ssl": "Use SSL" + }, + "data_description": { + "use_ssl": "Whether or not using strict SSL checks. Disable if the panel does not expose a valid SSL certificate or if SSL communication is unsupported by the panel you are connecting to." + } + }, + "direct": { + "description": "Specify the Elmax panel connection parameters below.", + "data": { + "panel_api_host": "Panel API Hostname or IP", + "panel_api_port": "Panel API port", + "use_ssl": "Use SSL", + "panel_pin": "Panel PIN code" + }, + "data_description": { + "use_ssl": "Whether or not using strict SSL checks. Disable if the panel does not expose a valid SSL certificate or if SSL communication is unsupported by the panel you are connecting to." + } + }, "panels": { "description": "Select which panel you would like to control with this integration. Please note that the panel must be ON in order to be configured.", "data": { @@ -30,6 +59,7 @@ "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", "network_error": "A network error occurred", "invalid_pin": "The provided pin is invalid", + "invalid_mode": "Invalid or unsupported mode", "reauth_panel_disappeared": "The given panel is no longer associated to this user. Please log in using an account associated to this panel.", "unknown": "[%key:common::config_flow::error::unknown%]" }, diff --git a/homeassistant/components/elmax/switch.py b/homeassistant/components/elmax/switch.py index 877330892e5..911ad864b50 100644 --- a/homeassistant/components/elmax/switch.py +++ b/homeassistant/components/elmax/switch.py @@ -1,4 +1,5 @@ """Elmax switch platform.""" + import asyncio import logging from typing import Any @@ -40,7 +41,6 @@ async def async_setup_entry( if actuator.endpoint_id in known_devices: continue entity = ElmaxSwitch( - panel=coordinator.panel_entry, elmax_device=actuator, panel_version=panel_status.release, coordinator=coordinator, diff --git a/homeassistant/components/elv/__init__.py b/homeassistant/components/elv/__init__.py index 7e5b8a322a5..208d19a0f8e 100644 --- a/homeassistant/components/elv/__init__.py +++ b/homeassistant/components/elv/__init__.py @@ -1,4 +1,5 @@ """The Elv integration.""" + import voluptuous as vol from homeassistant.const import CONF_DEVICE, Platform diff --git a/homeassistant/components/elv/switch.py b/homeassistant/components/elv/switch.py index b998e2dd737..e790873e368 100644 --- a/homeassistant/components/elv/switch.py +++ b/homeassistant/components/elv/switch.py @@ -1,4 +1,5 @@ """Support for PCA 301 smart switch.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/elvia/__init__.py b/homeassistant/components/elvia/__init__.py index 1f85fe720a7..f1eafe64079 100644 --- a/homeassistant/components/elvia/__init__.py +++ b/homeassistant/components/elvia/__init__.py @@ -1,4 +1,5 @@ """The Elvia integration.""" + from __future__ import annotations from datetime import datetime, timedelta diff --git a/homeassistant/components/elvia/config_flow.py b/homeassistant/components/elvia/config_flow.py index fb50842e39b..4cf311e780e 100644 --- a/homeassistant/components/elvia/config_flow.py +++ b/homeassistant/components/elvia/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Elvia integration.""" + from __future__ import annotations from datetime import timedelta @@ -13,9 +14,6 @@ from homeassistant.util import dt as dt_util from .const import CONF_METERING_POINT_ID, DOMAIN, LOGGER -if TYPE_CHECKING: - from homeassistant.data_entry_flow import FlowResult - class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Elvia.""" @@ -28,7 +26,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None, - ) -> FlowResult: + ) -> config_entries.ConfigFlowResult: """Handle the initial step.""" errors: dict[str, str] = {} if user_input is not None: @@ -77,7 +75,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_select_meter( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> config_entries.ConfigFlowResult: """Handle selecting a metering point ID.""" if TYPE_CHECKING: assert self._metering_point_ids is not None @@ -105,7 +103,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self, api_token: str, metering_point_id: str, - ) -> FlowResult: + ) -> config_entries.ConfigFlowResult: """Store metering point ID and API token.""" if (await self.async_set_unique_id(metering_point_id)) is not None: return self.async_abort( diff --git a/homeassistant/components/elvia/const.py b/homeassistant/components/elvia/const.py index c4b8e40e73f..1342bd75d6d 100644 --- a/homeassistant/components/elvia/const.py +++ b/homeassistant/components/elvia/const.py @@ -1,4 +1,5 @@ """Constants for the Elvia integration.""" + from logging import getLogger DOMAIN = "elvia" diff --git a/homeassistant/components/elvia/importer.py b/homeassistant/components/elvia/importer.py index 097db51cab8..4e8b7f716ef 100644 --- a/homeassistant/components/elvia/importer.py +++ b/homeassistant/components/elvia/importer.py @@ -1,4 +1,5 @@ """Importer for the Elvia integration.""" + from __future__ import annotations from datetime import datetime, timedelta diff --git a/homeassistant/components/emby/media_player.py b/homeassistant/components/emby/media_player.py index 73593ef09a1..22d7939a14e 100644 --- a/homeassistant/components/emby/media_player.py +++ b/homeassistant/components/emby/media_player.py @@ -1,4 +1,5 @@ """Support to interface with the Emby API.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/emoncms/sensor.py b/homeassistant/components/emoncms/sensor.py index f26ed72f44c..746877c4e5f 100644 --- a/homeassistant/components/emoncms/sensor.py +++ b/homeassistant/components/emoncms/sensor.py @@ -1,4 +1,5 @@ """Support for monitoring emoncms feeds.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/emoncms_history/__init__.py b/homeassistant/components/emoncms_history/__init__.py index b4420ec6863..ab3f2671b99 100644 --- a/homeassistant/components/emoncms_history/__init__.py +++ b/homeassistant/components/emoncms_history/__init__.py @@ -1,4 +1,5 @@ """Support for sending data to Emoncms.""" + from datetime import timedelta from http import HTTPStatus import logging diff --git a/homeassistant/components/emonitor/__init__.py b/homeassistant/components/emonitor/__init__.py index 3bc5c7862cb..74d08432f72 100644 --- a/homeassistant/components/emonitor/__init__.py +++ b/homeassistant/components/emonitor/__init__.py @@ -1,4 +1,5 @@ """The SiteSage Emonitor integration.""" + from datetime import timedelta import logging diff --git a/homeassistant/components/emonitor/config_flow.py b/homeassistant/components/emonitor/config_flow.py index 6ffd4f1059b..70bd58e4cc0 100644 --- a/homeassistant/components/emonitor/config_flow.py +++ b/homeassistant/components/emonitor/config_flow.py @@ -1,14 +1,15 @@ """Config flow for SiteSage Emonitor integration.""" + import logging from aioemonitor import Emonitor import aiohttp import voluptuous as vol -from homeassistant import config_entries, core from homeassistant.components import dhcp +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_NAME -from homeassistant.data_entry_flow import FlowResult +from homeassistant.core import HomeAssistant from homeassistant.helpers import aiohttp_client from homeassistant.helpers.device_registry import format_mac @@ -18,7 +19,7 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -async def fetch_mac_and_title(hass: core.HomeAssistant, host): +async def fetch_mac_and_title(hass: HomeAssistant, host): """Validate the user input allows us to connect.""" session = aiohttp_client.async_get_clientsession(hass) emonitor = Emonitor(host, session) @@ -27,7 +28,7 @@ async def fetch_mac_and_title(hass: core.HomeAssistant, host): return {"title": name_short_mac(mac_address[-6:]), "mac_address": mac_address} -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class EmonitorConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for SiteSage Emonitor.""" VERSION = 1 @@ -63,7 +64,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: + async def async_step_dhcp( + self, discovery_info: dhcp.DhcpServiceInfo + ) -> ConfigFlowResult: """Handle dhcp discovery.""" self.discovered_ip = discovery_info.ip await self.async_set_unique_id(format_mac(discovery_info.macaddress)) diff --git a/homeassistant/components/emonitor/sensor.py b/homeassistant/components/emonitor/sensor.py index 1c3011ee28d..551e47a91a4 100644 --- a/homeassistant/components/emonitor/sensor.py +++ b/homeassistant/components/emonitor/sensor.py @@ -1,4 +1,5 @@ """Support for a Emonitor channel sensor.""" + from __future__ import annotations from aioemonitor.monitor import EmonitorChannel, EmonitorStatus diff --git a/homeassistant/components/emulated_hue/__init__.py b/homeassistant/components/emulated_hue/__init__.py index 1ba93da716c..9a7ce8369aa 100644 --- a/homeassistant/components/emulated_hue/__init__.py +++ b/homeassistant/components/emulated_hue/__init__.py @@ -1,4 +1,5 @@ """Support for local control of entities by emulating a Philips Hue bridge.""" + from __future__ import annotations import logging @@ -6,11 +7,12 @@ import logging from aiohttp import web import voluptuous as vol +from homeassistant.components.http import KEY_HASS from homeassistant.components.network import async_get_source_ip from homeassistant.const import ( CONF_ENTITIES, CONF_TYPE, - EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP, ) from homeassistant.core import Event, HomeAssistant @@ -130,7 +132,7 @@ async def async_setup(hass: HomeAssistant, yaml_config: ConfigType) -> bool: await config.async_setup() app = web.Application() - app["hass"] = hass + app[KEY_HASS] = hass # We misunderstood the startup signal. You're not allowed to change # anything during startup. Temp workaround. @@ -153,6 +155,6 @@ async def async_setup(hass: HomeAssistant, yaml_config: ConfigType) -> bool: """Start the bridge.""" await start_emulated_hue_bridge(hass, config, app) - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, _start) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, _start) return True diff --git a/homeassistant/components/emulated_hue/config.py b/homeassistant/components/emulated_hue/config.py index 2be89e7214c..b4208c1f3f6 100644 --- a/homeassistant/components/emulated_hue/config.py +++ b/homeassistant/components/emulated_hue/config.py @@ -1,4 +1,5 @@ """Support for local control of entities by emulating a Philips Hue bridge.""" + from __future__ import annotations from functools import cache @@ -15,14 +16,14 @@ from homeassistant.components import ( script, ) from homeassistant.const import CONF_ENTITIES, CONF_TYPE -from homeassistant.core import HomeAssistant, State, callback, split_entity_id +from homeassistant.core import Event, HomeAssistant, State, callback, split_entity_id from homeassistant.helpers import storage from homeassistant.helpers.event import ( EventStateChangedData, async_track_state_added_domain, async_track_state_removed_domain, ) -from homeassistant.helpers.typing import ConfigType, EventType +from homeassistant.helpers.typing import ConfigType SUPPORTED_DOMAINS = { climate.DOMAIN, @@ -223,7 +224,7 @@ class Config: ] @callback - def _clear_exposed_cache(self, event: EventType[EventStateChangedData]) -> None: + def _clear_exposed_cache(self, event: Event[EventStateChangedData]) -> None: """Clear the cache of exposed entity ids.""" self.get_exposed_entity_ids.cache_clear() diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py index 873f446aad8..91c4440d875 100644 --- a/homeassistant/components/emulated_hue/hue_api.py +++ b/homeassistant/components/emulated_hue/hue_api.py @@ -1,4 +1,5 @@ """Support for a Hue API to control Home Assistant.""" + from __future__ import annotations import asyncio @@ -34,7 +35,7 @@ from homeassistant.components.cover import ( CoverEntityFeature, ) from homeassistant.components.fan import ATTR_PERCENTAGE, FanEntityFeature -from homeassistant.components.http import HomeAssistantView +from homeassistant.components.http import KEY_HASS, HomeAssistantView from homeassistant.components.humidifier import ATTR_HUMIDITY, SERVICE_SET_HUMIDITY from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -64,12 +65,11 @@ from homeassistant.const import ( STATE_ON, STATE_UNAVAILABLE, ) -from homeassistant.core import State +from homeassistant.core import Event, State from homeassistant.helpers.event import ( EventStateChangedData, async_track_state_change_event, ) -from homeassistant.helpers.typing import EventType from homeassistant.util.json import json_loads from homeassistant.util.network import is_local @@ -319,7 +319,7 @@ class HueOneLightStateView(HomeAssistantView): if not _remote_is_allowed(request.remote): return self.json_message("Only local IPs allowed", HTTPStatus.UNAUTHORIZED) - hass: core.HomeAssistant = request.app["hass"] + hass = request.app[KEY_HASS] hass_entity_id = self.config.number_to_entity_id(entity_id) if hass_entity_id is None: @@ -362,7 +362,7 @@ class HueOneLightChangeView(HomeAssistantView): return self.json_message("Only local IPs allowed", HTTPStatus.UNAUTHORIZED) config = self.config - hass: core.HomeAssistant = request.app["hass"] + hass = request.app[KEY_HASS] entity_id = config.number_to_entity_id(entity_number) if entity_id is None: @@ -885,7 +885,7 @@ def create_config_model(config: Config, request: web.Request) -> dict[str, Any]: def create_list_of_entities(config: Config, request: web.Request) -> dict[str, Any]: """Create a list of all entities.""" - hass: core.HomeAssistant = request.app["hass"] + hass = request.app[KEY_HASS] return { config.entity_id_to_number(entity_id): state_to_json(config, state) for entity_id in config.get_exposed_entity_ids() @@ -915,7 +915,7 @@ async def wait_for_state_change_or_timeout( ev = asyncio.Event() @core.callback - def _async_event_changed(event: EventType[EventStateChangedData]) -> None: + def _async_event_changed(event: Event[EventStateChangedData]) -> None: ev.set() unsub = async_track_state_change_event(hass, [entity_id], _async_event_changed) diff --git a/homeassistant/components/emulated_hue/upnp.py b/homeassistant/components/emulated_hue/upnp.py index 9f5ca312343..4fb0be81814 100644 --- a/homeassistant/components/emulated_hue/upnp.py +++ b/homeassistant/components/emulated_hue/upnp.py @@ -1,4 +1,5 @@ """Support UPNP discovery method that mimics Hue hubs.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/emulated_kasa/__init__.py b/homeassistant/components/emulated_kasa/__init__.py index 41198f922cf..d5fc8af1aa4 100644 --- a/homeassistant/components/emulated_kasa/__init__.py +++ b/homeassistant/components/emulated_kasa/__init__.py @@ -1,4 +1,5 @@ """Support for local power state reporting of entities by emulating TP-Link Kasa smart plugs.""" + import logging from sense_energy import PlugInstance, SenseLink diff --git a/homeassistant/components/emulated_roku/__init__.py b/homeassistant/components/emulated_roku/__init__.py index 59929eab118..4ebd31730bf 100644 --- a/homeassistant/components/emulated_roku/__init__.py +++ b/homeassistant/components/emulated_roku/__init__.py @@ -1,4 +1,5 @@ """Support for Roku API emulation.""" + import voluptuous as vol from homeassistant import config_entries diff --git a/homeassistant/components/emulated_roku/binding.py b/homeassistant/components/emulated_roku/binding.py index 3559c0da99b..a84db4bd77b 100644 --- a/homeassistant/components/emulated_roku/binding.py +++ b/homeassistant/components/emulated_roku/binding.py @@ -1,4 +1,5 @@ """Bridge between emulated_roku and Home Assistant.""" + import logging from emulated_roku import EmulatedRokuCommandHandler, EmulatedRokuServer diff --git a/homeassistant/components/emulated_roku/config_flow.py b/homeassistant/components/emulated_roku/config_flow.py index dd7cd87c96a..1a3b2c0e2af 100644 --- a/homeassistant/components/emulated_roku/config_flow.py +++ b/homeassistant/components/emulated_roku/config_flow.py @@ -1,7 +1,8 @@ """Config flow to configure emulated_roku component.""" + import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_NAME from homeassistant.core import callback @@ -16,7 +17,7 @@ def configured_servers(hass): } -class EmulatedRokuFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class EmulatedRokuFlowHandler(ConfigFlow, domain=DOMAIN): """Handle an emulated_roku config flow.""" VERSION = 1 diff --git a/homeassistant/components/emulated_roku/const.py b/homeassistant/components/emulated_roku/const.py index 6c780367188..b8a70596048 100644 --- a/homeassistant/components/emulated_roku/const.py +++ b/homeassistant/components/emulated_roku/const.py @@ -1,4 +1,5 @@ """Constants for the emulated_roku component.""" + DOMAIN = "emulated_roku" CONF_SERVERS = "servers" diff --git a/homeassistant/components/energy/__init__.py b/homeassistant/components/energy/__init__.py index 7f86b2458cb..fe2d3b0da14 100644 --- a/homeassistant/components/energy/__init__.py +++ b/homeassistant/components/energy/__init__.py @@ -1,4 +1,5 @@ """The Energy integration.""" + from __future__ import annotations from homeassistant.components import frontend @@ -28,7 +29,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: frontend.async_register_built_in_panel(hass, DOMAIN, DOMAIN, "mdi:lightning-bolt") hass.async_create_task( - discovery.async_load_platform(hass, Platform.SENSOR, DOMAIN, {}, config) + discovery.async_load_platform(hass, Platform.SENSOR, DOMAIN, {}, config), + eager_start=True, ) hass.data[DOMAIN] = { "cost_sensors": {}, diff --git a/homeassistant/components/energy/data.py b/homeassistant/components/energy/data.py index 6f6b481b044..d4533b2fcc8 100644 --- a/homeassistant/components/energy/data.py +++ b/homeassistant/components/energy/data.py @@ -1,4 +1,5 @@ """Energy data.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/energy/sensor.py b/homeassistant/components/energy/sensor.py index b684ad5ab8f..37930e31af0 100644 --- a/homeassistant/components/energy/sensor.py +++ b/homeassistant/components/energy/sensor.py @@ -1,4 +1,5 @@ """Helper sensor for calculating utility costs.""" + from __future__ import annotations import asyncio @@ -148,7 +149,7 @@ class SensorManager: async def finish() -> None: if to_add: self.async_add_entities(to_add) - await asyncio.gather(*(ent.add_finished.wait() for ent in to_add)) + await asyncio.wait(ent.add_finished for ent in to_add) for key, entity in to_remove.items(): self.current_entities.pop(key) @@ -218,6 +219,12 @@ class SensorManager: to_add.append(self.current_entities[key]) +def _set_result_unless_done(future: asyncio.Future[None]) -> None: + """Set the result of a future unless it is done.""" + if not future.done(): + future.set_result(None) + + class EnergyCostSensor(SensorEntity): """Calculate costs incurred by consuming energy. @@ -247,7 +254,9 @@ class EnergyCostSensor(SensorEntity): self._last_energy_sensor_state: State | None = None # add_finished is set when either of async_added_to_hass or add_to_platform_abort # is called - self.add_finished = asyncio.Event() + self.add_finished: asyncio.Future[None] = ( + asyncio.get_running_loop().create_future() + ) def _reset(self, energy_state: State) -> None: """Reset the cost sensor.""" @@ -418,25 +427,25 @@ class EnergyCostSensor(SensorEntity): self._config[self._adapter.stat_energy_key] ] = self.entity_id - @callback - def async_state_changed_listener(*_: Any) -> None: - """Handle child updates.""" - self._update_cost() - self.async_write_ha_state() - self.async_on_remove( async_track_state_change_event( self.hass, cast(str, self._config[self._adapter.stat_energy_key]), - async_state_changed_listener, + self._async_state_changed_listener, ) ) - self.add_finished.set() + _set_result_unless_done(self.add_finished) + + @callback + def _async_state_changed_listener(self, *_: Any) -> None: + """Handle child updates.""" + self._update_cost() + self.async_write_ha_state() @callback def add_to_platform_abort(self) -> None: """Abort adding an entity to a platform.""" - self.add_finished.set() + _set_result_unless_done(self.add_finished) super().add_to_platform_abort() async def async_will_remove_from_hass(self) -> None: diff --git a/homeassistant/components/energy/types.py b/homeassistant/components/energy/types.py index 819ed6ac5a8..d52a15a60c8 100644 --- a/homeassistant/components/energy/types.py +++ b/homeassistant/components/energy/types.py @@ -1,4 +1,5 @@ """Types for the energy platform.""" + from __future__ import annotations from collections.abc import Awaitable, Callable diff --git a/homeassistant/components/energy/validate.py b/homeassistant/components/energy/validate.py index f1eb7591e83..2d34f606653 100644 --- a/homeassistant/components/energy/validate.py +++ b/homeassistant/components/energy/validate.py @@ -1,4 +1,5 @@ """Validate the energy preferences provide valid data.""" + from __future__ import annotations from collections.abc import Mapping, Sequence diff --git a/homeassistant/components/energy/websocket_api.py b/homeassistant/components/energy/websocket_api.py index 5d9cd81013d..2dd45a8be4d 100644 --- a/homeassistant/components/energy/websocket_api.py +++ b/homeassistant/components/energy/websocket_api.py @@ -1,4 +1,5 @@ """The Energy websocket API.""" + from __future__ import annotations import asyncio @@ -71,7 +72,9 @@ async def async_get_energy_platforms( platforms[domain] = cast(EnergyPlatform, platform).async_get_solar_forecast - await async_process_integration_platforms(hass, DOMAIN, _process_energy_platform) + await async_process_integration_platforms( + hass, DOMAIN, _process_energy_platform, wait_for_platforms=True + ) return platforms diff --git a/homeassistant/components/energyzero/__init__.py b/homeassistant/components/energyzero/__init__.py index 8878a99e562..3e1bb830cce 100644 --- a/homeassistant/components/energyzero/__init__.py +++ b/homeassistant/components/energyzero/__init__.py @@ -1,4 +1,5 @@ """The EnergyZero integration.""" + from __future__ import annotations from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/energyzero/config_flow.py b/homeassistant/components/energyzero/config_flow.py index 55fffbdec91..72a1e376dcf 100644 --- a/homeassistant/components/energyzero/config_flow.py +++ b/homeassistant/components/energyzero/config_flow.py @@ -1,10 +1,10 @@ """Config flow for EnergyZero integration.""" + from __future__ import annotations from typing import Any -from homeassistant.config_entries import ConfigFlow -from homeassistant.data_entry_flow import FlowResult +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from .const import DOMAIN @@ -16,7 +16,7 @@ class EnergyZeroFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" await self.async_set_unique_id(DOMAIN) diff --git a/homeassistant/components/energyzero/const.py b/homeassistant/components/energyzero/const.py index 03d94facf3b..7079b720f4d 100644 --- a/homeassistant/components/energyzero/const.py +++ b/homeassistant/components/energyzero/const.py @@ -1,4 +1,5 @@ """Constants for the EnergyZero integration.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/energyzero/coordinator.py b/homeassistant/components/energyzero/coordinator.py index a30509a3840..65955b2ebe6 100644 --- a/homeassistant/components/energyzero/coordinator.py +++ b/homeassistant/components/energyzero/coordinator.py @@ -1,4 +1,5 @@ """The Coordinator for EnergyZero.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/energyzero/diagnostics.py b/homeassistant/components/energyzero/diagnostics.py index b4018a32d3d..35d20fee929 100644 --- a/homeassistant/components/energyzero/diagnostics.py +++ b/homeassistant/components/energyzero/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for EnergyZero.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/energyzero/sensor.py b/homeassistant/components/energyzero/sensor.py index 005abb62e91..f65f7bd559c 100644 --- a/homeassistant/components/energyzero/sensor.py +++ b/homeassistant/components/energyzero/sensor.py @@ -1,4 +1,5 @@ """Support for EnergyZero sensors.""" + from __future__ import annotations from collections.abc import Callable @@ -29,21 +30,14 @@ from .const import DOMAIN, SERVICE_TYPE_DEVICE_NAMES from .coordinator import EnergyZeroData, EnergyZeroDataUpdateCoordinator -@dataclass(frozen=True) -class EnergyZeroSensorEntityDescriptionMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class EnergyZeroSensorEntityDescription(SensorEntityDescription): + """Describes an EnergyZero sensor entity.""" value_fn: Callable[[EnergyZeroData], float | datetime | None] service_type: str -@dataclass(frozen=True) -class EnergyZeroSensorEntityDescription( - SensorEntityDescription, EnergyZeroSensorEntityDescriptionMixin -): - """Describes a Pure Energie sensor entity.""" - - SENSORS: tuple[EnergyZeroSensorEntityDescription, ...] = ( EnergyZeroSensorEntityDescription( key="current_hour_price", diff --git a/homeassistant/components/energyzero/services.py b/homeassistant/components/energyzero/services.py index 325c443375e..d98699c5c08 100644 --- a/homeassistant/components/energyzero/services.py +++ b/homeassistant/components/energyzero/services.py @@ -1,4 +1,5 @@ """The EnergyZero services.""" + from __future__ import annotations from datetime import date, datetime @@ -61,7 +62,6 @@ def __get_date(date_input: str | None) -> date | datetime: return value raise ServiceValidationError( - "Invalid datetime provided.", translation_domain=DOMAIN, translation_key="invalid_date", translation_placeholders={ @@ -92,7 +92,6 @@ def __get_coordinator( if not entry: raise ServiceValidationError( - f"Invalid config entry: {entry_id}", translation_domain=DOMAIN, translation_key="invalid_config_entry", translation_placeholders={ @@ -101,7 +100,6 @@ def __get_coordinator( ) if entry.state != ConfigEntryState.LOADED: raise ServiceValidationError( - f"{entry.title} is not loaded", translation_domain=DOMAIN, translation_key="unloaded_config_entry", translation_placeholders={ diff --git a/homeassistant/components/enigma2/const.py b/homeassistant/components/enigma2/const.py index 0511a794172..277efad50eb 100644 --- a/homeassistant/components/enigma2/const.py +++ b/homeassistant/components/enigma2/const.py @@ -1,4 +1,5 @@ """Constants for the Enigma2 platform.""" + DOMAIN = "enigma2" CONF_USE_CHANNEL_ICON = "use_channel_icon" diff --git a/homeassistant/components/enigma2/media_player.py b/homeassistant/components/enigma2/media_player.py index b73f8b51c4d..afe8a426c72 100644 --- a/homeassistant/components/enigma2/media_player.py +++ b/homeassistant/components/enigma2/media_player.py @@ -1,4 +1,5 @@ """Support for Enigma2 media players.""" + from __future__ import annotations import contextlib diff --git a/homeassistant/components/enocean/__init__.py b/homeassistant/components/enocean/__init__.py index 18c3b888d19..6dcec5ec218 100644 --- a/homeassistant/components/enocean/__init__.py +++ b/homeassistant/components/enocean/__init__.py @@ -1,4 +1,5 @@ """Support for EnOcean devices.""" + import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry diff --git a/homeassistant/components/enocean/binary_sensor.py b/homeassistant/components/enocean/binary_sensor.py index 25fc8c4f50a..9ebedc52c00 100644 --- a/homeassistant/components/enocean/binary_sensor.py +++ b/homeassistant/components/enocean/binary_sensor.py @@ -1,4 +1,5 @@ """Support for EnOcean binary sensors.""" + from __future__ import annotations from enocean.utils import combine_hex diff --git a/homeassistant/components/enocean/config_flow.py b/homeassistant/components/enocean/config_flow.py index e47cb4c0589..1137eb23256 100644 --- a/homeassistant/components/enocean/config_flow.py +++ b/homeassistant/components/enocean/config_flow.py @@ -2,14 +2,14 @@ import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_DEVICE from . import dongle from .const import DOMAIN, ERROR_INVALID_DONGLE_PATH, LOGGER -class EnOceanFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class EnOceanFlowHandler(ConfigFlow, domain=DOMAIN): """Handle the enOcean config flows.""" VERSION = 1 diff --git a/homeassistant/components/enocean/const.py b/homeassistant/components/enocean/const.py index f9c522393d7..3624493b42e 100644 --- a/homeassistant/components/enocean/const.py +++ b/homeassistant/components/enocean/const.py @@ -1,4 +1,5 @@ """Constants for the ENOcean integration.""" + import logging from homeassistant.const import Platform diff --git a/homeassistant/components/enocean/device.py b/homeassistant/components/enocean/device.py index 220f940f37f..5c12fc12a68 100644 --- a/homeassistant/components/enocean/device.py +++ b/homeassistant/components/enocean/device.py @@ -1,4 +1,5 @@ """Representation of an EnOcean device.""" + from enocean.protocol.packet import Packet from enocean.utils import combine_hex diff --git a/homeassistant/components/enocean/dongle.py b/homeassistant/components/enocean/dongle.py index 9ccaeba504c..6402b4c3a28 100644 --- a/homeassistant/components/enocean/dongle.py +++ b/homeassistant/components/enocean/dongle.py @@ -1,4 +1,5 @@ """Representation of an EnOcean dongle.""" + import glob import logging from os.path import basename, normpath diff --git a/homeassistant/components/enocean/icons.json b/homeassistant/components/enocean/icons.json new file mode 100644 index 00000000000..81cd49ac670 --- /dev/null +++ b/homeassistant/components/enocean/icons.json @@ -0,0 +1,9 @@ +{ + "entity": { + "sensor": { + "window_handle": { + "default": "mdi:window-open-variant" + } + } + } +} diff --git a/homeassistant/components/enocean/light.py b/homeassistant/components/enocean/light.py index 2500ad7ce94..937930c4a31 100644 --- a/homeassistant/components/enocean/light.py +++ b/homeassistant/components/enocean/light.py @@ -1,4 +1,5 @@ """Support for EnOcean light sources.""" + from __future__ import annotations import math diff --git a/homeassistant/components/enocean/sensor.py b/homeassistant/components/enocean/sensor.py index 83c801d598e..c22a7d95760 100644 --- a/homeassistant/components/enocean/sensor.py +++ b/homeassistant/components/enocean/sensor.py @@ -1,4 +1,5 @@ """Support for EnOcean sensors.""" + from __future__ import annotations from collections.abc import Callable @@ -44,25 +45,17 @@ SENSOR_TYPE_TEMPERATURE = "temperature" SENSOR_TYPE_WINDOWHANDLE = "windowhandle" -@dataclass(frozen=True) -class EnOceanSensorEntityDescriptionMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class EnOceanSensorEntityDescription(SensorEntityDescription): + """Describes EnOcean sensor entity.""" unique_id: Callable[[list[int]], str | None] -@dataclass(frozen=True) -class EnOceanSensorEntityDescription( - SensorEntityDescription, EnOceanSensorEntityDescriptionMixin -): - """Describes EnOcean sensor entity.""" - - SENSOR_DESC_TEMPERATURE = EnOceanSensorEntityDescription( key=SENSOR_TYPE_TEMPERATURE, name="Temperature", native_unit_of_measurement=UnitOfTemperature.CELSIUS, - icon="mdi:thermometer", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, unique_id=lambda dev_id: f"{combine_hex(dev_id)}-{SENSOR_TYPE_TEMPERATURE}", @@ -72,7 +65,6 @@ SENSOR_DESC_HUMIDITY = EnOceanSensorEntityDescription( key=SENSOR_TYPE_HUMIDITY, name="Humidity", native_unit_of_measurement=PERCENTAGE, - icon="mdi:water-percent", device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, unique_id=lambda dev_id: f"{combine_hex(dev_id)}-{SENSOR_TYPE_HUMIDITY}", @@ -82,7 +74,6 @@ SENSOR_DESC_POWER = EnOceanSensorEntityDescription( key=SENSOR_TYPE_POWER, name="Power", native_unit_of_measurement=UnitOfPower.WATT, - icon="mdi:power-plug", device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, unique_id=lambda dev_id: f"{combine_hex(dev_id)}-{SENSOR_TYPE_POWER}", @@ -91,7 +82,7 @@ SENSOR_DESC_POWER = EnOceanSensorEntityDescription( SENSOR_DESC_WINDOWHANDLE = EnOceanSensorEntityDescription( key=SENSOR_TYPE_WINDOWHANDLE, name="WindowHandle", - icon="mdi:window-open-variant", + translation_key="window_handle", unique_id=lambda dev_id: f"{combine_hex(dev_id)}-{SENSOR_TYPE_WINDOWHANDLE}", ) diff --git a/homeassistant/components/enocean/switch.py b/homeassistant/components/enocean/switch.py index 13920f08e85..4fa75ff9712 100644 --- a/homeassistant/components/enocean/switch.py +++ b/homeassistant/components/enocean/switch.py @@ -1,4 +1,5 @@ """Support for EnOcean switches.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/enphase_envoy/__init__.py b/homeassistant/components/enphase_envoy/__init__.py index 2473c2d9b2f..2407f807eb7 100644 --- a/homeassistant/components/enphase_envoy/__init__.py +++ b/homeassistant/components/enphase_envoy/__init__.py @@ -1,4 +1,5 @@ """The Enphase Envoy integration.""" + from __future__ import annotations from pyenphase import Envoy diff --git a/homeassistant/components/enphase_envoy/binary_sensor.py b/homeassistant/components/enphase_envoy/binary_sensor.py index 5eb2e621e47..dfa619f07d8 100644 --- a/homeassistant/components/enphase_envoy/binary_sensor.py +++ b/homeassistant/components/enphase_envoy/binary_sensor.py @@ -1,4 +1,5 @@ """Support for Enphase Envoy solar energy monitor.""" + from __future__ import annotations from collections.abc import Callable @@ -22,20 +23,13 @@ from .coordinator import EnphaseUpdateCoordinator from .entity import EnvoyBaseEntity -@dataclass(frozen=True) -class EnvoyEnchargeRequiredKeysMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class EnvoyEnchargeBinarySensorEntityDescription(BinarySensorEntityDescription): + """Describes an Envoy Encharge binary sensor entity.""" value_fn: Callable[[EnvoyEncharge], bool] -@dataclass(frozen=True) -class EnvoyEnchargeBinarySensorEntityDescription( - BinarySensorEntityDescription, EnvoyEnchargeRequiredKeysMixin -): - """Describes an Envoy Encharge binary sensor entity.""" - - ENCHARGE_SENSORS = ( EnvoyEnchargeBinarySensorEntityDescription( key="communicating", @@ -53,20 +47,13 @@ ENCHARGE_SENSORS = ( ) -@dataclass(frozen=True) -class EnvoyEnpowerRequiredKeysMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class EnvoyEnpowerBinarySensorEntityDescription(BinarySensorEntityDescription): + """Describes an Envoy Enpower binary sensor entity.""" value_fn: Callable[[EnvoyEnpower], bool] -@dataclass(frozen=True) -class EnvoyEnpowerBinarySensorEntityDescription( - BinarySensorEntityDescription, EnvoyEnpowerRequiredKeysMixin -): - """Describes an Envoy Enpower binary sensor entity.""" - - ENPOWER_SENSORS = ( EnvoyEnpowerBinarySensorEntityDescription( key="communicating", diff --git a/homeassistant/components/enphase_envoy/config_flow.py b/homeassistant/components/enphase_envoy/config_flow.py index ee1966d5e51..13894d423d6 100644 --- a/homeassistant/components/enphase_envoy/config_flow.py +++ b/homeassistant/components/enphase_envoy/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Enphase Envoy integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -9,11 +10,10 @@ from awesomeversion import AwesomeVersion from pyenphase import AUTH_TOKEN_MIN_VERSION, Envoy, EnvoyError import voluptuous as vol -from homeassistant import config_entries from homeassistant.components import zeroconf +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant, callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.httpx_client import get_async_client from .const import DOMAIN, INVALID_AUTH_ERRORS @@ -37,7 +37,7 @@ async def validate_input( return envoy -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class EnphaseConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Enphase Envoy.""" VERSION = 1 @@ -47,7 +47,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.ip_address: str | None = None self.username = None self.protovers: str | None = None - self._reauth_entry: config_entries.ConfigEntry | None = None + self._reauth_entry: ConfigEntry | None = None @callback def _async_generate_schema(self) -> vol.Schema: @@ -87,7 +87,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_zeroconf( self, discovery_info: zeroconf.ZeroconfServiceInfo - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by zeroconf discovery.""" if discovery_info.ip_address.version != 4: return self.async_abort(reason="not_ipv4_address") @@ -109,7 +109,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_user() - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Handle configuration by re-auth.""" self._reauth_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] @@ -125,7 +127,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors: dict[str, str] = {} description_placeholders: dict[str, str] = {} diff --git a/homeassistant/components/enphase_envoy/const.py b/homeassistant/components/enphase_envoy/const.py index c5656a65b6f..fe8e7e9ec1f 100644 --- a/homeassistant/components/enphase_envoy/const.py +++ b/homeassistant/components/enphase_envoy/const.py @@ -1,4 +1,5 @@ """The enphase_envoy component.""" + from pyenphase import EnvoyAuthenticationError, EnvoyAuthenticationRequired from homeassistant.const import Platform diff --git a/homeassistant/components/enphase_envoy/coordinator.py b/homeassistant/components/enphase_envoy/coordinator.py index 02a9d2f2491..c8152d44726 100644 --- a/homeassistant/components/enphase_envoy/coordinator.py +++ b/homeassistant/components/enphase_envoy/coordinator.py @@ -1,4 +1,5 @@ """The enphase_envoy component.""" + from __future__ import annotations import contextlib diff --git a/homeassistant/components/enphase_envoy/diagnostics.py b/homeassistant/components/enphase_envoy/diagnostics.py index 7b8a3e03270..28d9690ae70 100644 --- a/homeassistant/components/enphase_envoy/diagnostics.py +++ b/homeassistant/components/enphase_envoy/diagnostics.py @@ -1,7 +1,11 @@ """Diagnostics support for Enphase Envoy.""" + from __future__ import annotations -from typing import Any +import copy +from typing import TYPE_CHECKING, Any + +from attr import asdict from homeassistant.components.diagnostics import async_redact_data from homeassistant.config_entries import ConfigEntry @@ -13,11 +17,15 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.json import json_dumps +from homeassistant.util.json import json_loads from .const import DOMAIN from .coordinator import EnphaseUpdateCoordinator CONF_TITLE = "title" +CLEAN_TEXT = "<>" TO_REDACT = { CONF_NAME, @@ -36,11 +44,81 @@ async def async_get_config_entry_diagnostics( """Return diagnostics for a config entry.""" coordinator: EnphaseUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - return async_redact_data( - { - "entry": entry.as_dict(), - "envoy_firmware": coordinator.envoy.firmware, - "data": coordinator.data, - }, - TO_REDACT, + if TYPE_CHECKING: + assert coordinator.envoy.data + envoy_data = coordinator.envoy.data + envoy = coordinator.envoy + + device_registry = dr.async_get(hass) + entity_registry = er.async_get(hass) + + device_entities = [] + # for each device associated with the envoy get entity and state information + for device in dr.async_entries_for_config_entry(device_registry, entry.entry_id): + entities = [] + for entity in er.async_entries_for_device( + entity_registry, device_id=device.id, include_disabled_entities=True + ): + state_dict = None + if state := hass.states.get(entity.entity_id): + state_dict = dict(state.as_dict()) + state_dict.pop("context", None) + entities.append({"entity": asdict(entity), "state": state_dict}) + device_entities.append({"device": asdict(device), "entities": entities}) + + # remove envoy serial + old_serial = coordinator.envoy_serial_number + + coordinator_data = copy.deepcopy(coordinator.data) + coordinator_data_cleaned = json_dumps(coordinator_data).replace( + old_serial, CLEAN_TEXT ) + + device_entities_cleaned = json_dumps(device_entities).replace( + old_serial, CLEAN_TEXT + ) + + envoy_model: dict[str, Any] = { + "encharge_inventory": envoy_data.encharge_inventory, + "encharge_power": envoy_data.encharge_power, + "encharge_aggregate": envoy_data.encharge_aggregate, + "enpower": envoy_data.enpower, + "system_consumption": envoy_data.system_consumption, + "system_production": envoy_data.system_production, + "system_consumption_phases": envoy_data.system_consumption_phases, + "system_production_phases": envoy_data.system_production_phases, + "ctmeter_production": envoy_data.ctmeter_production, + "ctmeter_consumption": envoy_data.ctmeter_consumption, + "ctmeter_storage": envoy_data.ctmeter_storage, + "ctmeter_production_phases": envoy_data.ctmeter_production_phases, + "ctmeter_consumption_phases": envoy_data.ctmeter_consumption_phases, + "ctmeter_storage_phases": envoy_data.ctmeter_storage_phases, + "dry_contact_status": envoy_data.dry_contact_status, + "dry_contact_settings": envoy_data.dry_contact_settings, + "inverters": envoy_data.inverters, + "tariff": envoy_data.tariff, + } + + envoy_properties: dict[str, Any] = { + "envoy_firmware": envoy.firmware, + "part_number": envoy.part_number, + "envoy_model": envoy.envoy_model, + "supported_features": [feature.name for feature in envoy.supported_features], + "phase_mode": envoy.phase_mode, + "phase_count": envoy.phase_count, + "active_phasecount": envoy.active_phase_count, + "ct_count": envoy.ct_meter_count, + "ct_consumption_meter": envoy.consumption_meter_type, + "ct_production_meter": envoy.production_meter_type, + "ct_storage_meter": envoy.storage_meter_type, + } + + diagnostic_data: dict[str, Any] = { + "config_entry": async_redact_data(entry.as_dict(), TO_REDACT), + "envoy_properties": envoy_properties, + "raw_data": json_loads(coordinator_data_cleaned), + "envoy_model_data": envoy_model, + "envoy_entities_by_device": json_loads(device_entities_cleaned), + } + + return diagnostic_data diff --git a/homeassistant/components/enphase_envoy/entity.py b/homeassistant/components/enphase_envoy/entity.py index 16669bcd098..491951625ee 100644 --- a/homeassistant/components/enphase_envoy/entity.py +++ b/homeassistant/components/enphase_envoy/entity.py @@ -1,4 +1,5 @@ """Support for Enphase Envoy solar energy monitor.""" + from __future__ import annotations from pyenphase import EnvoyData diff --git a/homeassistant/components/enphase_envoy/manifest.json b/homeassistant/components/enphase_envoy/manifest.json index 9f437ee9945..597d326968d 100644 --- a/homeassistant/components/enphase_envoy/manifest.json +++ b/homeassistant/components/enphase_envoy/manifest.json @@ -6,7 +6,7 @@ "documentation": "https://www.home-assistant.io/integrations/enphase_envoy", "iot_class": "local_polling", "loggers": ["pyenphase"], - "requirements": ["pyenphase==1.19.2"], + "requirements": ["pyenphase==1.20.1"], "zeroconf": [ { "type": "_enphase-envoy._tcp.local." diff --git a/homeassistant/components/enphase_envoy/number.py b/homeassistant/components/enphase_envoy/number.py index bf54c91f45b..61d9aabb469 100644 --- a/homeassistant/components/enphase_envoy/number.py +++ b/homeassistant/components/enphase_envoy/number.py @@ -1,4 +1,5 @@ """Number platform for Enphase Envoy solar energy monitor.""" + from __future__ import annotations from collections.abc import Awaitable, Callable @@ -25,35 +26,21 @@ from .coordinator import EnphaseUpdateCoordinator from .entity import EnvoyBaseEntity -@dataclass(frozen=True) -class EnvoyRelayRequiredKeysMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class EnvoyRelayNumberEntityDescription(NumberEntityDescription): + """Describes an Envoy Dry Contact Relay number entity.""" value_fn: Callable[[EnvoyDryContactSettings], float] -@dataclass(frozen=True) -class EnvoyRelayNumberEntityDescription( - NumberEntityDescription, EnvoyRelayRequiredKeysMixin -): - """Describes an Envoy Dry Contact Relay number entity.""" - - -@dataclass(frozen=True) -class EnvoyStorageSettingsRequiredKeysMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class EnvoyStorageSettingsNumberEntityDescription(NumberEntityDescription): + """Describes an Envoy storage mode number entity.""" value_fn: Callable[[EnvoyStorageSettings], float] update_fn: Callable[[Envoy, float], Awaitable[dict[str, Any]]] -@dataclass(frozen=True) -class EnvoyStorageSettingsNumberEntityDescription( - NumberEntityDescription, EnvoyStorageSettingsRequiredKeysMixin -): - """Describes an Envoy storage mode number entity.""" - - RELAY_ENTITIES = ( EnvoyRelayNumberEntityDescription( key="soc_low", diff --git a/homeassistant/components/enphase_envoy/select.py b/homeassistant/components/enphase_envoy/select.py index 5d2edf91d9a..98374d16394 100644 --- a/homeassistant/components/enphase_envoy/select.py +++ b/homeassistant/components/enphase_envoy/select.py @@ -1,4 +1,5 @@ """Select platform for Enphase Envoy solar energy monitor.""" + from __future__ import annotations from collections.abc import Awaitable, Callable, Coroutine @@ -21,9 +22,9 @@ from .coordinator import EnphaseUpdateCoordinator from .entity import EnvoyBaseEntity -@dataclass(frozen=True) -class EnvoyRelayRequiredKeysMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class EnvoyRelaySelectEntityDescription(SelectEntityDescription): + """Describes an Envoy Dry Contact Relay select entity.""" value_fn: Callable[[EnvoyDryContactSettings], str] update_fn: Callable[ @@ -31,28 +32,14 @@ class EnvoyRelayRequiredKeysMixin: ] -@dataclass(frozen=True) -class EnvoyRelaySelectEntityDescription( - SelectEntityDescription, EnvoyRelayRequiredKeysMixin -): - """Describes an Envoy Dry Contact Relay select entity.""" - - -@dataclass(frozen=True) -class EnvoyStorageSettingsRequiredKeysMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class EnvoyStorageSettingsSelectEntityDescription(SelectEntityDescription): + """Describes an Envoy storage settings select entity.""" value_fn: Callable[[EnvoyStorageSettings], str] update_fn: Callable[[Envoy, str], Awaitable[dict[str, Any]]] -@dataclass(frozen=True) -class EnvoyStorageSettingsSelectEntityDescription( - SelectEntityDescription, EnvoyStorageSettingsRequiredKeysMixin -): - """Describes an Envoy storage settings select entity.""" - - RELAY_MODE_MAP = { DryContactMode.MANUAL: "standard", DryContactMode.STATE_OF_CHARGE: "battery", diff --git a/homeassistant/components/enphase_envoy/sensor.py b/homeassistant/components/enphase_envoy/sensor.py index c0d1c66deae..329dc67e9e1 100644 --- a/homeassistant/components/enphase_envoy/sensor.py +++ b/homeassistant/components/enphase_envoy/sensor.py @@ -1,4 +1,5 @@ """Support for Enphase Envoy solar energy monitor.""" + from __future__ import annotations from collections.abc import Callable @@ -58,20 +59,13 @@ INVERTERS_KEY = "inverters" LAST_REPORTED_KEY = "last_reported" -@dataclass(frozen=True) -class EnvoyInverterRequiredKeysMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class EnvoyInverterSensorEntityDescription(SensorEntityDescription): + """Describes an Envoy inverter sensor entity.""" value_fn: Callable[[EnvoyInverter], datetime.datetime | float] -@dataclass(frozen=True) -class EnvoyInverterSensorEntityDescription( - SensorEntityDescription, EnvoyInverterRequiredKeysMixin -): - """Describes an Envoy inverter sensor entity.""" - - INVERTER_SENSORS = ( EnvoyInverterSensorEntityDescription( key=INVERTERS_KEY, @@ -91,21 +85,14 @@ INVERTER_SENSORS = ( ) -@dataclass(frozen=True) -class EnvoyProductionRequiredKeysMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class EnvoyProductionSensorEntityDescription(SensorEntityDescription): + """Describes an Envoy production sensor entity.""" value_fn: Callable[[EnvoySystemProduction], int] on_phase: str | None -@dataclass(frozen=True) -class EnvoyProductionSensorEntityDescription( - SensorEntityDescription, EnvoyProductionRequiredKeysMixin -): - """Describes an Envoy production sensor entity.""" - - PRODUCTION_SENSORS = ( EnvoyProductionSensorEntityDescription( key="production", @@ -164,25 +151,18 @@ PRODUCTION_PHASE_SENSORS = { ) for sensor in list(PRODUCTION_SENSORS) ] - for phase in range(0, 3) + for phase in range(3) } -@dataclass(frozen=True) -class EnvoyConsumptionRequiredKeysMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class EnvoyConsumptionSensorEntityDescription(SensorEntityDescription): + """Describes an Envoy consumption sensor entity.""" value_fn: Callable[[EnvoySystemConsumption], int] on_phase: str | None -@dataclass(frozen=True) -class EnvoyConsumptionSensorEntityDescription( - SensorEntityDescription, EnvoyConsumptionRequiredKeysMixin -): - """Describes an Envoy consumption sensor entity.""" - - CONSUMPTION_SENSORS = ( EnvoyConsumptionSensorEntityDescription( key="consumption", @@ -241,13 +221,13 @@ CONSUMPTION_PHASE_SENSORS = { ) for sensor in list(CONSUMPTION_SENSORS) ] - for phase in range(0, 3) + for phase in range(3) } -@dataclass(frozen=True) -class EnvoyCTRequiredKeysMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class EnvoyCTSensorEntityDescription(SensorEntityDescription): + """Describes an Envoy CT sensor entity.""" value_fn: Callable[ [EnvoyMeterData], @@ -256,11 +236,6 @@ class EnvoyCTRequiredKeysMixin: on_phase: str | None -@dataclass(frozen=True) -class EnvoyCTSensorEntityDescription(SensorEntityDescription, EnvoyCTRequiredKeysMixin): - """Describes an Envoy CT sensor entity.""" - - CT_NET_CONSUMPTION_SENSORS = ( EnvoyCTSensorEntityDescription( key="lifetime_net_consumption", @@ -351,7 +326,7 @@ CT_NET_CONSUMPTION_PHASE_SENSORS = { ) for sensor in list(CT_NET_CONSUMPTION_SENSORS) ] - for phase in range(0, 3) + for phase in range(3) } CT_PRODUCTION_SENSORS = ( @@ -386,37 +361,109 @@ CT_PRODUCTION_PHASE_SENSORS = { ) for sensor in list(CT_PRODUCTION_SENSORS) ] - for phase in range(0, 3) + for phase in range(3) +} + +CT_STORAGE_SENSORS = ( + EnvoyCTSensorEntityDescription( + key="lifetime_battery_discharged", + translation_key="lifetime_battery_discharged", + native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, + state_class=SensorStateClass.TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + suggested_unit_of_measurement=UnitOfEnergy.MEGA_WATT_HOUR, + suggested_display_precision=3, + value_fn=lambda ct: ct.energy_delivered, + on_phase=None, + ), + EnvoyCTSensorEntityDescription( + key="lifetime_battery_charged", + translation_key="lifetime_battery_charged", + native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, + state_class=SensorStateClass.TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + suggested_unit_of_measurement=UnitOfEnergy.MEGA_WATT_HOUR, + suggested_display_precision=3, + value_fn=lambda ct: ct.energy_received, + on_phase=None, + ), + EnvoyCTSensorEntityDescription( + key="battery_discharge", + translation_key="battery_discharge", + native_unit_of_measurement=UnitOfPower.WATT, + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.POWER, + suggested_unit_of_measurement=UnitOfPower.KILO_WATT, + suggested_display_precision=3, + value_fn=lambda ct: ct.active_power, + on_phase=None, + ), + EnvoyCTSensorEntityDescription( + key="storage_voltage", + translation_key="storage_ct_voltage", + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.VOLTAGE, + suggested_unit_of_measurement=UnitOfElectricPotential.VOLT, + suggested_display_precision=1, + entity_registry_enabled_default=False, + value_fn=lambda ct: ct.voltage, + on_phase=None, + ), + EnvoyCTSensorEntityDescription( + key="storage_ct_metering_status", + translation_key="storage_ct_metering_status", + device_class=SensorDeviceClass.ENUM, + options=list(CtMeterStatus), + entity_registry_enabled_default=False, + value_fn=lambda ct: ct.metering_status, + on_phase=None, + ), + EnvoyCTSensorEntityDescription( + key="storage_ct_status_flags", + translation_key="storage_ct_status_flags", + state_class=None, + entity_registry_enabled_default=False, + value_fn=lambda ct: 0 if ct.status_flags is None else len(ct.status_flags), + on_phase=None, + ), +) + + +CT_STORAGE_PHASE_SENSORS = { + (on_phase := PHASENAMES[phase]): [ + replace( + sensor, + key=f"{sensor.key}_l{phase + 1}", + translation_key=f"{sensor.translation_key}_phase", + entity_registry_enabled_default=False, + on_phase=on_phase, + translation_placeholders={"phase_name": f"l{phase + 1}"}, + ) + for sensor in list(CT_STORAGE_SENSORS) + ] + for phase in range(3) } -@dataclass(frozen=True) -class EnvoyEnchargeRequiredKeysMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class EnvoyEnchargeSensorEntityDescription(SensorEntityDescription): + """Describes an Envoy Encharge sensor entity.""" value_fn: Callable[[EnvoyEncharge], datetime.datetime | int | float] -@dataclass(frozen=True) -class EnvoyEnchargeSensorEntityDescription( - SensorEntityDescription, EnvoyEnchargeRequiredKeysMixin -): - """Describes an Envoy Encharge sensor entity.""" - - @dataclass(frozen=True) class EnvoyEnchargePowerRequiredKeysMixin: """Mixin for required keys.""" - value_fn: Callable[[EnvoyEnchargePower], int | float] - -@dataclass(frozen=True) -class EnvoyEnchargePowerSensorEntityDescription( - SensorEntityDescription, EnvoyEnchargePowerRequiredKeysMixin -): +@dataclass(frozen=True, kw_only=True) +class EnvoyEnchargePowerSensorEntityDescription(SensorEntityDescription): """Describes an Envoy Encharge sensor entity.""" + value_fn: Callable[[EnvoyEnchargePower], int | float] + ENCHARGE_INVENTORY_SENSORS = ( EnvoyEnchargeSensorEntityDescription( @@ -455,20 +502,13 @@ ENCHARGE_POWER_SENSORS = ( ) -@dataclass(frozen=True) -class EnvoyEnpowerRequiredKeysMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class EnvoyEnpowerSensorEntityDescription(SensorEntityDescription): + """Describes an Envoy Encharge sensor entity.""" value_fn: Callable[[EnvoyEnpower], datetime.datetime | int | float] -@dataclass(frozen=True) -class EnvoyEnpowerSensorEntityDescription( - SensorEntityDescription, EnvoyEnpowerRequiredKeysMixin -): - """Describes an Envoy Encharge sensor entity.""" - - ENPOWER_SENSORS = ( EnvoyEnpowerSensorEntityDescription( key="temperature", @@ -489,15 +529,13 @@ ENPOWER_SENSORS = ( class EnvoyEnchargeAggregateRequiredKeysMixin: """Mixin for required keys.""" - value_fn: Callable[[EnvoyEnchargeAggregate], int] - -@dataclass(frozen=True) -class EnvoyEnchargeAggregateSensorEntityDescription( - SensorEntityDescription, EnvoyEnchargeAggregateRequiredKeysMixin -): +@dataclass(frozen=True, kw_only=True) +class EnvoyEnchargeAggregateSensorEntityDescription(SensorEntityDescription): """Describes an Envoy Encharge sensor entity.""" + value_fn: Callable[[EnvoyEnchargeAggregate], int] + ENCHARGE_AGGREGATE_SENSORS = ( EnvoyEnchargeAggregateSensorEntityDescription( @@ -603,6 +641,21 @@ async def async_setup_entry( for description in CT_PRODUCTION_PHASE_SENSORS[use_phase] if phase.measurement_type == CtType.PRODUCTION ) + # Add storage CT entities + if ctmeter := envoy_data.ctmeter_storage: + entities.extend( + EnvoyStorageCTEntity(coordinator, description) + for description in CT_STORAGE_SENSORS + if ctmeter.measurement_type == CtType.STORAGE + ) + # For each storage ct phase reported add storage ct entities + if phase_data := envoy_data.ctmeter_storage_phases: + entities.extend( + EnvoyStorageCTPhaseEntity(coordinator, description) + for use_phase, phase in phase_data.items() + for description in CT_STORAGE_PHASE_SENSORS[use_phase] + if phase.measurement_type == CtType.STORAGE + ) if envoy_data.inverters: entities.extend( @@ -801,6 +854,40 @@ class EnvoyProductionCTPhaseEntity(EnvoySystemSensorEntity): ) +class EnvoyStorageCTEntity(EnvoySystemSensorEntity): + """Envoy net storage CT entity.""" + + entity_description: EnvoyCTSensorEntityDescription + + @property + def native_value( + self, + ) -> int | float | str | CtType | CtMeterStatus | CtStatusFlags | None: + """Return the state of the CT sensor.""" + if (ctmeter := self.data.ctmeter_storage) is None: + return None + return self.entity_description.value_fn(ctmeter) + + +class EnvoyStorageCTPhaseEntity(EnvoySystemSensorEntity): + """Envoy net storage CT phase entity.""" + + entity_description: EnvoyCTSensorEntityDescription + + @property + def native_value( + self, + ) -> int | float | str | CtType | CtMeterStatus | CtStatusFlags | None: + """Return the state of the CT phase sensor.""" + if TYPE_CHECKING: + assert self.entity_description.on_phase + if (ctmeter := self.data.ctmeter_storage_phases) is None: + return None + return self.entity_description.value_fn( + ctmeter[self.entity_description.on_phase] + ) + + class EnvoyInverterEntity(EnvoySensorBaseEntity): """Envoy inverter entity.""" diff --git a/homeassistant/components/enphase_envoy/strings.json b/homeassistant/components/enphase_envoy/strings.json index b0854f64f24..22112228a37 100644 --- a/homeassistant/components/enphase_envoy/strings.json +++ b/homeassistant/components/enphase_envoy/strings.json @@ -170,6 +170,24 @@ "production_ct_status_flags": { "name": "Meter status flags active production CT" }, + "lifetime_battery_discharged": { + "name": "Lifetime battery energy discharged" + }, + "lifetime_battery_charged": { + "name": "Lifetime battery energy charged" + }, + "battery_discharge": { + "name": "Current battery discharge" + }, + "storage_ct_voltage": { + "name": "Voltage storage CT" + }, + "storage_ct_metering_status": { + "name": "Metering status storage CT" + }, + "storage_ct_status_flags": { + "name": "Meter status flags active storage CT" + }, "lifetime_net_consumption_phase": { "name": "Lifetime net energy consumption {phase_name}" }, @@ -197,6 +215,24 @@ "production_ct_status_flags_phase": { "name": "Meter status flags active production CT {phase_name}" }, + "lifetime_battery_discharged_phase": { + "name": "Lifetime battery energy discharged {phase_name}" + }, + "lifetime_battery_charged_phase": { + "name": "Lifetime battery energy charged {phase_name}" + }, + "battery_discharge_phase": { + "name": "Current battery discharge {phase_name}" + }, + "storage_ct_voltage_phase": { + "name": "Voltage storage CT {phase_name}" + }, + "storage_ct_metering_status_phase": { + "name": "Metering status storage CT {phase_name}" + }, + "storage_ct_status_flags_phase": { + "name": "Meter status flags active storage CT {phase_name}" + }, "reserve_soc": { "name": "Reserve battery level" }, diff --git a/homeassistant/components/enphase_envoy/switch.py b/homeassistant/components/enphase_envoy/switch.py index 921c5601dac..dbe14ee94ea 100644 --- a/homeassistant/components/enphase_envoy/switch.py +++ b/homeassistant/components/enphase_envoy/switch.py @@ -1,4 +1,5 @@ """Switch platform for Enphase Envoy solar energy monitor.""" + from __future__ import annotations from collections.abc import Awaitable, Callable, Coroutine @@ -24,54 +25,33 @@ from .entity import EnvoyBaseEntity _LOGGER = logging.getLogger(__name__) -@dataclass(frozen=True) -class EnvoyEnpowerRequiredKeysMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class EnvoyEnpowerSwitchEntityDescription(SwitchEntityDescription): + """Describes an Envoy Enpower switch entity.""" value_fn: Callable[[EnvoyEnpower], bool] turn_on_fn: Callable[[Envoy], Coroutine[Any, Any, dict[str, Any]]] turn_off_fn: Callable[[Envoy], Coroutine[Any, Any, dict[str, Any]]] -@dataclass(frozen=True) -class EnvoyEnpowerSwitchEntityDescription( - SwitchEntityDescription, EnvoyEnpowerRequiredKeysMixin -): - """Describes an Envoy Enpower switch entity.""" - - -@dataclass(frozen=True) -class EnvoyDryContactRequiredKeysMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class EnvoyDryContactSwitchEntityDescription(SwitchEntityDescription): + """Describes an Envoy Enpower dry contact switch entity.""" value_fn: Callable[[EnvoyDryContactStatus], bool] turn_on_fn: Callable[[Envoy, str], Coroutine[Any, Any, dict[str, Any]]] turn_off_fn: Callable[[Envoy, str], Coroutine[Any, Any, dict[str, Any]]] -@dataclass(frozen=True) -class EnvoyDryContactSwitchEntityDescription( - SwitchEntityDescription, EnvoyDryContactRequiredKeysMixin -): - """Describes an Envoy Enpower dry contact switch entity.""" - - -@dataclass(frozen=True) -class EnvoyStorageSettingsRequiredKeysMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class EnvoyStorageSettingsSwitchEntityDescription(SwitchEntityDescription): + """Describes an Envoy storage settings switch entity.""" value_fn: Callable[[EnvoyStorageSettings], bool] turn_on_fn: Callable[[Envoy], Awaitable[dict[str, Any]]] turn_off_fn: Callable[[Envoy], Awaitable[dict[str, Any]]] -@dataclass(frozen=True) -class EnvoyStorageSettingsSwitchEntityDescription( - SwitchEntityDescription, EnvoyStorageSettingsRequiredKeysMixin -): - """Describes an Envoy storage settings switch entity.""" - - ENPOWER_GRID_SWITCH = EnvoyEnpowerSwitchEntityDescription( key="mains_admin_state", translation_key="grid_enabled", diff --git a/homeassistant/components/entur_public_transport/sensor.py b/homeassistant/components/entur_public_transport/sensor.py index e109c25d340..70b86d0271f 100644 --- a/homeassistant/components/entur_public_transport/sensor.py +++ b/homeassistant/components/entur_public_transport/sensor.py @@ -1,4 +1,5 @@ """Real-time information about public transport departures in Norway.""" + from __future__ import annotations from datetime import datetime, timedelta @@ -237,9 +238,9 @@ class EnturPublicTransportSensor(SensorEntity): self._attributes[ATTR_NEXT_UP_AT] = calls[1].expected_departure_time.strftime( "%H:%M" ) - self._attributes[ - ATTR_NEXT_UP_IN - ] = f"{due_in_minutes(calls[1].expected_departure_time)} min" + self._attributes[ATTR_NEXT_UP_IN] = ( + f"{due_in_minutes(calls[1].expected_departure_time)} min" + ) self._attributes[ATTR_NEXT_UP_REALTIME] = calls[1].is_realtime self._attributes[ATTR_NEXT_UP_DELAY] = calls[1].delay_in_min diff --git a/homeassistant/components/environment_canada/__init__.py b/homeassistant/components/environment_canada/__init__.py index 925bc42a930..6f47d057e81 100644 --- a/homeassistant/components/environment_canada/__init__.py +++ b/homeassistant/components/environment_canada/__init__.py @@ -1,4 +1,5 @@ """The Environment Canada (EC) component.""" + from datetime import timedelta import logging import xml.etree.ElementTree as et diff --git a/homeassistant/components/environment_canada/camera.py b/homeassistant/components/environment_canada/camera.py index 385f973a25a..73032f59ac2 100644 --- a/homeassistant/components/environment_canada/camera.py +++ b/homeassistant/components/environment_canada/camera.py @@ -1,4 +1,5 @@ """Support for the Environment Canada radar imagery.""" + from __future__ import annotations import voluptuous as vol diff --git a/homeassistant/components/environment_canada/config_flow.py b/homeassistant/components/environment_canada/config_flow.py index f4b9ee792c3..369a419f2a6 100644 --- a/homeassistant/components/environment_canada/config_flow.py +++ b/homeassistant/components/environment_canada/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Environment Canada integration.""" + import logging import xml.etree.ElementTree as et @@ -6,7 +7,7 @@ import aiohttp from env_canada import ECWeather, ec_exc import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_LANGUAGE, CONF_LATITUDE, CONF_LONGITUDE from homeassistant.helpers import config_validation as cv @@ -40,7 +41,7 @@ async def validate_input(data): } -class EnvironmentCanadaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class EnvironmentCanadaConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Environment Canada weather.""" VERSION = 1 diff --git a/homeassistant/components/environment_canada/diagnostics.py b/homeassistant/components/environment_canada/diagnostics.py index 297f4664fb0..63f8bb72189 100644 --- a/homeassistant/components/environment_canada/diagnostics.py +++ b/homeassistant/components/environment_canada/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for Environment Canada.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/environment_canada/sensor.py b/homeassistant/components/environment_canada/sensor.py index 143090cc227..8a734f74dd6 100644 --- a/homeassistant/components/environment_canada/sensor.py +++ b/homeassistant/components/environment_canada/sensor.py @@ -1,4 +1,5 @@ """Support for the Environment Canada weather service.""" + from __future__ import annotations from collections.abc import Callable @@ -33,19 +34,11 @@ from .const import ATTR_STATION, DOMAIN ATTR_TIME = "alert time" -@dataclass(frozen=True) -class ECSensorEntityDescriptionMixin: - """Mixin for required keys.""" - - value_fn: Callable[[Any], Any] - - -@dataclass(frozen=True) -class ECSensorEntityDescription( - SensorEntityDescription, ECSensorEntityDescriptionMixin -): +@dataclass(frozen=True, kw_only=True) +class ECSensorEntityDescription(SensorEntityDescription): """Describes Environment Canada sensor entity.""" + value_fn: Callable[[Any], Any] transform: Callable[[Any], Any] | None = None diff --git a/homeassistant/components/environment_canada/strings.json b/homeassistant/components/environment_canada/strings.json index eb9ec24dad0..fc03550b64e 100644 --- a/homeassistant/components/environment_canada/strings.json +++ b/homeassistant/components/environment_canada/strings.json @@ -110,9 +110,6 @@ } }, "weather": { - "hourly_forecast": { - "name": "Hourly forecast" - }, "forecast": { "name": "Forecast" } diff --git a/homeassistant/components/environment_canada/weather.py b/homeassistant/components/environment_canada/weather.py index b4b5d27f45f..643e7951c23 100644 --- a/homeassistant/components/environment_canada/weather.py +++ b/homeassistant/components/environment_canada/weather.py @@ -1,4 +1,5 @@ """Platform for retrieving meteorological data from Environment Canada.""" + from __future__ import annotations import datetime @@ -68,17 +69,15 @@ async def async_setup_entry( coordinator = hass.data[DOMAIN][config_entry.entry_id]["weather_coordinator"] entity_registry = er.async_get(hass) - entities = [ECWeather(coordinator, False)] - - # Add hourly entity to legacy config entries - if entity_registry.async_get_entity_id( + # Remove hourly entity from legacy config entries + if hourly_entity_id := entity_registry.async_get_entity_id( WEATHER_DOMAIN, DOMAIN, _calculate_unique_id(config_entry.unique_id, True), ): - entities.append(ECWeather(coordinator, True)) + entity_registry.async_remove(hourly_entity_id) - async_add_entities(entities) + async_add_entities([ECWeather(coordinator)]) def _calculate_unique_id(config_entry_unique_id: str | None, hourly: bool) -> str: @@ -98,17 +97,15 @@ class ECWeather(SingleCoordinatorWeatherEntity): WeatherEntityFeature.FORECAST_DAILY | WeatherEntityFeature.FORECAST_HOURLY ) - def __init__(self, coordinator, hourly): + def __init__(self, coordinator): """Initialize Environment Canada weather.""" super().__init__(coordinator) self.ec_data = coordinator.ec_data self._attr_attribution = self.ec_data.metadata["attribution"] - self._attr_translation_key = "hourly_forecast" if hourly else "forecast" + self._attr_translation_key = "forecast" self._attr_unique_id = _calculate_unique_id( - coordinator.config_entry.unique_id, hourly + coordinator.config_entry.unique_id, False ) - self._attr_entity_registry_enabled_default = not hourly - self._hourly = hourly self._attr_device_info = device_info(coordinator.config_entry) @property @@ -177,11 +174,6 @@ class ECWeather(SingleCoordinatorWeatherEntity): return icon_code_to_condition(int(icon_code)) return "" - @property - def forecast(self) -> list[Forecast] | None: - """Return the forecast array.""" - return get_forecast(self.ec_data, self._hourly) - @callback def _async_forecast_daily(self) -> list[Forecast] | None: """Return the daily forecast in native units.""" @@ -248,19 +240,17 @@ def get_forecast(ec_data, hourly) -> list[Forecast] | None: ) else: - for hour in ec_data.hourly_forecasts: - forecast_array.append( - { - ATTR_FORECAST_TIME: hour["period"].isoformat(), - ATTR_FORECAST_NATIVE_TEMP: int(hour["temperature"]), - ATTR_FORECAST_CONDITION: icon_code_to_condition( - int(hour["icon_code"]) - ), - ATTR_FORECAST_PRECIPITATION_PROBABILITY: int( - hour["precip_probability"] - ), - } - ) + forecast_array.extend( + { + ATTR_FORECAST_TIME: hour["period"].isoformat(), + ATTR_FORECAST_NATIVE_TEMP: int(hour["temperature"]), + ATTR_FORECAST_CONDITION: icon_code_to_condition(int(hour["icon_code"])), + ATTR_FORECAST_PRECIPITATION_PROBABILITY: int( + hour["precip_probability"] + ), + } + for hour in ec_data.hourly_forecasts + ) return forecast_array diff --git a/homeassistant/components/envisalink/__init__.py b/homeassistant/components/envisalink/__init__.py index b0a4619bbf9..65fdc1b5c63 100644 --- a/homeassistant/components/envisalink/__init__.py +++ b/homeassistant/components/envisalink/__init__.py @@ -1,4 +1,5 @@ """Support for Envisalink devices.""" + import asyncio import logging diff --git a/homeassistant/components/envisalink/alarm_control_panel.py b/homeassistant/components/envisalink/alarm_control_panel.py index 273dd4f0d0a..119608bbb2a 100644 --- a/homeassistant/components/envisalink/alarm_control_panel.py +++ b/homeassistant/components/envisalink/alarm_control_panel.py @@ -1,4 +1,5 @@ """Support for Envisalink-based alarm control panels (Honeywell/DSC).""" + from __future__ import annotations import logging diff --git a/homeassistant/components/envisalink/binary_sensor.py b/homeassistant/components/envisalink/binary_sensor.py index f08989c32be..9c0909539bb 100644 --- a/homeassistant/components/envisalink/binary_sensor.py +++ b/homeassistant/components/envisalink/binary_sensor.py @@ -1,4 +1,5 @@ """Support for Envisalink zone states- represented as binary sensors.""" + from __future__ import annotations import datetime diff --git a/homeassistant/components/envisalink/icons.json b/homeassistant/components/envisalink/icons.json new file mode 100644 index 00000000000..20696067f76 --- /dev/null +++ b/homeassistant/components/envisalink/icons.json @@ -0,0 +1,6 @@ +{ + "services": { + "alarm_keypress": "mdi:alarm-panel", + "invoke_custom_function": "mdi:console" + } +} diff --git a/homeassistant/components/envisalink/sensor.py b/homeassistant/components/envisalink/sensor.py index 72a64931070..fcafc23dd37 100644 --- a/homeassistant/components/envisalink/sensor.py +++ b/homeassistant/components/envisalink/sensor.py @@ -1,4 +1,5 @@ """Support for Envisalink sensors (shows panel info).""" + from __future__ import annotations import logging diff --git a/homeassistant/components/envisalink/switch.py b/homeassistant/components/envisalink/switch.py index 0bedc41e55e..36ad3d5bf81 100644 --- a/homeassistant/components/envisalink/switch.py +++ b/homeassistant/components/envisalink/switch.py @@ -1,4 +1,5 @@ """Support for Envisalink zone bypass switches.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/ephember/climate.py b/homeassistant/components/ephember/climate.py index 047b9234b82..89d84a2c6fd 100644 --- a/homeassistant/components/ephember/climate.py +++ b/homeassistant/components/ephember/climate.py @@ -1,4 +1,5 @@ """Support for the EPH Controls Ember themostats.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/epion/__init__.py b/homeassistant/components/epion/__init__.py index ed2f5559f32..fec975c5098 100644 --- a/homeassistant/components/epion/__init__.py +++ b/homeassistant/components/epion/__init__.py @@ -1,4 +1,5 @@ """The Epion integration.""" + from __future__ import annotations from epion import Epion diff --git a/homeassistant/components/epion/config_flow.py b/homeassistant/components/epion/config_flow.py index 7c89df94519..ce9a733ffbf 100644 --- a/homeassistant/components/epion/config_flow.py +++ b/homeassistant/components/epion/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Epion.""" + from __future__ import annotations import logging @@ -7,9 +8,8 @@ from typing import Any from epion import Epion, EpionAuthenticationError, EpionConnectionError import voluptuous as vol -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_API_KEY -from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN @@ -23,7 +23,7 @@ class EpionConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initiated by the user.""" errors = {} if user_input: diff --git a/homeassistant/components/epion/const.py b/homeassistant/components/epion/const.py index 83f82261583..9156c761346 100644 --- a/homeassistant/components/epion/const.py +++ b/homeassistant/components/epion/const.py @@ -1,4 +1,5 @@ """Constants for the Epion API.""" + from datetime import timedelta DOMAIN = "epion" diff --git a/homeassistant/components/epion/sensor.py b/homeassistant/components/epion/sensor.py index c722e73ac6c..4717c095bfe 100644 --- a/homeassistant/components/epion/sensor.py +++ b/homeassistant/components/epion/sensor.py @@ -1,4 +1,5 @@ """Support for Epion API.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/epson/__init__.py b/homeassistant/components/epson/__init__.py index 5a8544c7bf1..5171865594d 100644 --- a/homeassistant/components/epson/__init__.py +++ b/homeassistant/components/epson/__init__.py @@ -1,4 +1,5 @@ """The epson integration.""" + import logging from epson_projector import Projector diff --git a/homeassistant/components/epson/config_flow.py b/homeassistant/components/epson/config_flow.py index b1ac34b1099..4f038de9318 100644 --- a/homeassistant/components/epson/config_flow.py +++ b/homeassistant/components/epson/config_flow.py @@ -1,9 +1,10 @@ """Config flow for epson integration.""" + import logging import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT from . import validate_projector @@ -20,7 +21,7 @@ DATA_SCHEMA = vol.Schema( _LOGGER = logging.getLogger(__name__) -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class EpsonConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for epson.""" VERSION = 1 diff --git a/homeassistant/components/epson/exceptions.py b/homeassistant/components/epson/exceptions.py index 5cc65b32891..9af87391cb1 100644 --- a/homeassistant/components/epson/exceptions.py +++ b/homeassistant/components/epson/exceptions.py @@ -1,4 +1,5 @@ """The errors of Epson integration.""" + from homeassistant import exceptions diff --git a/homeassistant/components/epson/media_player.py b/homeassistant/components/epson/media_player.py index 1f401ed0a7d..a962b94b5e0 100644 --- a/homeassistant/components/epson/media_player.py +++ b/homeassistant/components/epson/media_player.py @@ -1,4 +1,5 @@ """Support for Epson projector.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/epsonworkforce/sensor.py b/homeassistant/components/epsonworkforce/sensor.py index 3b31082f333..dea611a3c3a 100644 --- a/homeassistant/components/epsonworkforce/sensor.py +++ b/homeassistant/components/epsonworkforce/sensor.py @@ -1,4 +1,5 @@ """Support for Epson Workforce Printer.""" + from __future__ import annotations from datetime import timedelta @@ -80,7 +81,7 @@ def setup_platform( api = EpsonPrinterAPI(host) if not api.available: - raise PlatformNotReady() + raise PlatformNotReady sensors = [ EpsonPrinterCartridge(api, description) diff --git a/homeassistant/components/escea/climate.py b/homeassistant/components/escea/climate.py index b204ae196e8..555da1494d7 100644 --- a/homeassistant/components/escea/climate.py +++ b/homeassistant/components/escea/climate.py @@ -1,4 +1,5 @@ """Support for the Escea Fireplace.""" + from __future__ import annotations from collections.abc import Coroutine diff --git a/homeassistant/components/escea/config_flow.py b/homeassistant/components/escea/config_flow.py index eb50e7d0fdc..957b1f6d146 100644 --- a/homeassistant/components/escea/config_flow.py +++ b/homeassistant/components/escea/config_flow.py @@ -1,4 +1,5 @@ """Config flow for escea.""" + import asyncio from contextlib import suppress import logging diff --git a/homeassistant/components/escea/discovery.py b/homeassistant/components/escea/discovery.py index 0d7f3024bfc..cbdc77536d7 100644 --- a/homeassistant/components/escea/discovery.py +++ b/homeassistant/components/escea/discovery.py @@ -1,4 +1,5 @@ """Internal discovery service for Escea Fireplace.""" + from __future__ import annotations from pescea import ( diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index bc22cc13d6f..3de5d48391f 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -1,4 +1,5 @@ """Support for esphome devices.""" + from __future__ import annotations from aioesphomeapi import APIClient diff --git a/homeassistant/components/esphome/alarm_control_panel.py b/homeassistant/components/esphome/alarm_control_panel.py index 58f63446da7..54bce4e6015 100644 --- a/homeassistant/components/esphome/alarm_control_panel.py +++ b/homeassistant/components/esphome/alarm_control_panel.py @@ -1,4 +1,5 @@ """Support for ESPHome Alarm Control Panel.""" + from __future__ import annotations from aioesphomeapi import ( @@ -31,24 +32,29 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .entity import EsphomeEntity, esphome_state_property, platform_async_setup_entry +from .entity import ( + EsphomeEntity, + convert_api_error_ha_error, + esphome_state_property, + platform_async_setup_entry, +) from .enum_mapper import EsphomeEnumMapper -_ESPHOME_ACP_STATE_TO_HASS_STATE: EsphomeEnumMapper[ - AlarmControlPanelState, str -] = EsphomeEnumMapper( - { - AlarmControlPanelState.DISARMED: STATE_ALARM_DISARMED, - AlarmControlPanelState.ARMED_HOME: STATE_ALARM_ARMED_HOME, - AlarmControlPanelState.ARMED_AWAY: STATE_ALARM_ARMED_AWAY, - AlarmControlPanelState.ARMED_NIGHT: STATE_ALARM_ARMED_NIGHT, - AlarmControlPanelState.ARMED_VACATION: STATE_ALARM_ARMED_VACATION, - AlarmControlPanelState.ARMED_CUSTOM_BYPASS: STATE_ALARM_ARMED_CUSTOM_BYPASS, - AlarmControlPanelState.PENDING: STATE_ALARM_PENDING, - AlarmControlPanelState.ARMING: STATE_ALARM_ARMING, - AlarmControlPanelState.DISARMING: STATE_ALARM_DISARMING, - AlarmControlPanelState.TRIGGERED: STATE_ALARM_TRIGGERED, - } +_ESPHOME_ACP_STATE_TO_HASS_STATE: EsphomeEnumMapper[AlarmControlPanelState, str] = ( + EsphomeEnumMapper( + { + AlarmControlPanelState.DISARMED: STATE_ALARM_DISARMED, + AlarmControlPanelState.ARMED_HOME: STATE_ALARM_ARMED_HOME, + AlarmControlPanelState.ARMED_AWAY: STATE_ALARM_ARMED_AWAY, + AlarmControlPanelState.ARMED_NIGHT: STATE_ALARM_ARMED_NIGHT, + AlarmControlPanelState.ARMED_VACATION: STATE_ALARM_ARMED_VACATION, + AlarmControlPanelState.ARMED_CUSTOM_BYPASS: STATE_ALARM_ARMED_CUSTOM_BYPASS, + AlarmControlPanelState.PENDING: STATE_ALARM_PENDING, + AlarmControlPanelState.ARMING: STATE_ALARM_ARMING, + AlarmControlPanelState.DISARMING: STATE_ALARM_DISARMING, + AlarmControlPanelState.TRIGGERED: STATE_ALARM_TRIGGERED, + } + ) ) @@ -113,42 +119,49 @@ class EsphomeAlarmControlPanel( """Return the state of the device.""" return _ESPHOME_ACP_STATE_TO_HASS_STATE.from_esphome(self._state.state) + @convert_api_error_ha_error async def async_alarm_disarm(self, code: str | None = None) -> None: """Send disarm command.""" self._client.alarm_control_panel_command( self._key, AlarmControlPanelCommand.DISARM, code ) + @convert_api_error_ha_error async def async_alarm_arm_home(self, code: str | None = None) -> None: """Send arm home command.""" self._client.alarm_control_panel_command( self._key, AlarmControlPanelCommand.ARM_HOME, code ) + @convert_api_error_ha_error async def async_alarm_arm_away(self, code: str | None = None) -> None: """Send arm away command.""" self._client.alarm_control_panel_command( self._key, AlarmControlPanelCommand.ARM_AWAY, code ) + @convert_api_error_ha_error async def async_alarm_arm_night(self, code: str | None = None) -> None: """Send arm away command.""" self._client.alarm_control_panel_command( self._key, AlarmControlPanelCommand.ARM_NIGHT, code ) + @convert_api_error_ha_error async def async_alarm_arm_custom_bypass(self, code: str | None = None) -> None: """Send arm away command.""" self._client.alarm_control_panel_command( self._key, AlarmControlPanelCommand.ARM_CUSTOM_BYPASS, code ) + @convert_api_error_ha_error async def async_alarm_arm_vacation(self, code: str | None = None) -> None: """Send arm away command.""" self._client.alarm_control_panel_command( self._key, AlarmControlPanelCommand.ARM_VACATION, code ) + @convert_api_error_ha_error async def async_alarm_trigger(self, code: str | None = None) -> None: """Send alarm trigger command.""" self._client.alarm_control_panel_command( diff --git a/homeassistant/components/esphome/binary_sensor.py b/homeassistant/components/esphome/binary_sensor.py index 4eb29f0c210..ac0676d8d1e 100644 --- a/homeassistant/components/esphome/binary_sensor.py +++ b/homeassistant/components/esphome/binary_sensor.py @@ -1,4 +1,5 @@ """Support for ESPHome binary sensors.""" + from __future__ import annotations from aioesphomeapi import BinarySensorInfo, BinarySensorState, EntityInfo diff --git a/homeassistant/components/esphome/bluetooth.py b/homeassistant/components/esphome/bluetooth.py index 37a555f3115..37ae28df0ca 100644 --- a/homeassistant/components/esphome/bluetooth.py +++ b/homeassistant/components/esphome/bluetooth.py @@ -1,4 +1,5 @@ """Bluetooth support for esphome.""" + from __future__ import annotations from functools import partial diff --git a/homeassistant/components/esphome/button.py b/homeassistant/components/esphome/button.py index d59e135d748..a825bb9b9b4 100644 --- a/homeassistant/components/esphome/button.py +++ b/homeassistant/components/esphome/button.py @@ -1,4 +1,5 @@ """Support for ESPHome buttons.""" + from __future__ import annotations from aioesphomeapi import ButtonInfo, EntityInfo, EntityState @@ -9,7 +10,11 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.enum import try_parse_enum -from .entity import EsphomeEntity, platform_async_setup_entry +from .entity import ( + EsphomeEntity, + convert_api_error_ha_error, + platform_async_setup_entry, +) async def async_setup_entry( @@ -52,6 +57,7 @@ class EsphomeButton(EsphomeEntity[ButtonInfo, EntityState], ButtonEntity): self._on_entry_data_changed() self.async_write_ha_state() + @convert_api_error_ha_error async def async_press(self) -> None: """Press the button.""" self._client.button_command(self._key) diff --git a/homeassistant/components/esphome/camera.py b/homeassistant/components/esphome/camera.py index 0b9c2995dac..83cf8d03e78 100644 --- a/homeassistant/components/esphome/camera.py +++ b/homeassistant/components/esphome/camera.py @@ -1,4 +1,5 @@ """Support for ESPHome cameras.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/esphome/climate.py b/homeassistant/components/esphome/climate.py index b9952004569..4225f60af0c 100644 --- a/homeassistant/components/esphome/climate.py +++ b/homeassistant/components/esphome/climate.py @@ -1,4 +1,5 @@ """Support for ESPHome climate devices.""" + from __future__ import annotations from typing import Any, cast @@ -55,7 +56,12 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .entity import EsphomeEntity, esphome_state_property, platform_async_setup_entry +from .entity import ( + EsphomeEntity, + convert_api_error_ha_error, + esphome_state_property, + platform_async_setup_entry, +) from .enum_mapper import EsphomeEnumMapper FAN_QUIET = "quiet" @@ -273,6 +279,7 @@ class EsphomeClimateEntity(EsphomeEntity[ClimateInfo, ClimateState], ClimateEnti """Return the humidity we try to reach.""" return round(self._state.target_humidity) + @convert_api_error_ha_error async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature (and operation mode if set).""" data: dict[str, Any] = {"key": self._key} @@ -288,16 +295,19 @@ class EsphomeClimateEntity(EsphomeEntity[ClimateInfo, ClimateState], ClimateEnti data["target_temperature_high"] = kwargs[ATTR_TARGET_TEMP_HIGH] self._client.climate_command(**data) + @convert_api_error_ha_error async def async_set_humidity(self, humidity: int) -> None: """Set new target humidity.""" self._client.climate_command(key=self._key, target_humidity=humidity) + @convert_api_error_ha_error async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set new target operation mode.""" self._client.climate_command( key=self._key, mode=_CLIMATE_MODES.from_hass(hvac_mode) ) + @convert_api_error_ha_error async def async_set_preset_mode(self, preset_mode: str) -> None: """Set preset mode.""" kwargs: dict[str, Any] = {"key": self._key} @@ -307,6 +317,7 @@ class EsphomeClimateEntity(EsphomeEntity[ClimateInfo, ClimateState], ClimateEnti kwargs["preset"] = _PRESETS.from_hass(preset_mode) self._client.climate_command(**kwargs) + @convert_api_error_ha_error async def async_set_fan_mode(self, fan_mode: str) -> None: """Set new fan mode.""" kwargs: dict[str, Any] = {"key": self._key} @@ -316,6 +327,7 @@ class EsphomeClimateEntity(EsphomeEntity[ClimateInfo, ClimateState], ClimateEnti kwargs["fan_mode"] = _FAN_MODES.from_hass(fan_mode) self._client.climate_command(**kwargs) + @convert_api_error_ha_error async def async_set_swing_mode(self, swing_mode: str) -> None: """Set new swing mode.""" self._client.climate_command( diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py index 9962b9144ea..5e166db7092 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -1,4 +1,5 @@ """Config flow to configure esphome component.""" + from __future__ import annotations from collections import OrderedDict @@ -21,10 +22,14 @@ import voluptuous as vol from homeassistant.components import dhcp, zeroconf from homeassistant.components.hassio import HassioServiceInfo -from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.device_registry import format_mac from .const import ( @@ -64,7 +69,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): async def _async_step_user_base( self, user_input: dict[str, Any] | None = None, error: str | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: if user_input is not None: self._host = user_input[CONF_HOST] self._port = user_input[CONF_PORT] @@ -87,11 +92,13 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" return await self._async_step_user_base(user_input=user_input) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """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 @@ -119,7 +126,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle reauthorization flow.""" errors = {} @@ -151,7 +158,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): self.context[CONF_NAME] = value self.context["title_placeholders"] = {"name": self._name} - async def _async_try_fetch_device_info(self) -> FlowResult: + async def _async_try_fetch_device_info(self) -> ConfigFlowResult: """Try to fetch device info and return any errors.""" response: str | None if self._noise_required: @@ -193,7 +200,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): return await self._async_step_user_base(error=response) return await self._async_authenticate_or_add() - async def _async_authenticate_or_add(self) -> FlowResult: + async def _async_authenticate_or_add(self) -> ConfigFlowResult: # Only show authentication step if device uses password assert self._device_info is not None if self._device_info.uses_password: @@ -204,7 +211,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_discovery_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle user-confirmation of discovered node.""" if user_input is not None: return await self._async_try_fetch_device_info() @@ -214,7 +221,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_zeroconf( self, discovery_info: zeroconf.ZeroconfServiceInfo - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle zeroconf discovery.""" mac_address: str | None = discovery_info.properties.get("mac") @@ -243,7 +250,9 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): return await self.async_step_discovery_confirm() - async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: + async def async_step_dhcp( + self, discovery_info: dhcp.DhcpServiceInfo + ) -> ConfigFlowResult: """Handle DHCP discovery.""" await self.async_set_unique_id(format_mac(discovery_info.macaddress)) self._abort_if_unique_id_configured(updates={CONF_HOST: discovery_info.ip}) @@ -251,7 +260,9 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): # for configured devices. return self.async_abort(reason="already_configured") - async def async_step_hassio(self, discovery_info: HassioServiceInfo) -> FlowResult: + async def async_step_hassio( + self, discovery_info: HassioServiceInfo + ) -> ConfigFlowResult: """Handle Supervisor service discovery.""" await async_set_dashboard_info( self.hass, @@ -262,7 +273,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): return self.async_abort(reason="service_received") @callback - def _async_get_entry(self) -> FlowResult: + def _async_get_entry(self) -> ConfigFlowResult: config_data = { CONF_HOST: self._host, CONF_PORT: self._port, @@ -288,7 +299,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_encryption_key( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle getting psk for transport encryption.""" errors = {} if user_input is not None: @@ -307,7 +318,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_authenticate( self, user_input: dict[str, Any] | None = None, error: str | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle getting password for authentication.""" if user_input is not None: self._password = user_input[CONF_PASSWORD] @@ -444,7 +455,7 @@ class OptionsFlowHandler(OptionsFlow): async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle options flow.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) diff --git a/homeassistant/components/esphome/const.py b/homeassistant/components/esphome/const.py index 575c57c8672..9c09591f6ea 100644 --- a/homeassistant/components/esphome/const.py +++ b/homeassistant/components/esphome/const.py @@ -1,4 +1,5 @@ """ESPHome constants.""" + from awesomeversion import AwesomeVersion DOMAIN = "esphome" diff --git a/homeassistant/components/esphome/cover.py b/homeassistant/components/esphome/cover.py index 77c3fee0afc..0b845c255a3 100644 --- a/homeassistant/components/esphome/cover.py +++ b/homeassistant/components/esphome/cover.py @@ -1,4 +1,5 @@ """Support for ESPHome covers.""" + from __future__ import annotations from typing import Any @@ -17,7 +18,12 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.enum import try_parse_enum -from .entity import EsphomeEntity, esphome_state_property, platform_async_setup_entry +from .entity import ( + EsphomeEntity, + convert_api_error_ha_error, + esphome_state_property, + platform_async_setup_entry, +) async def async_setup_entry( @@ -94,30 +100,37 @@ class EsphomeCover(EsphomeEntity[CoverInfo, CoverState], CoverEntity): return None return round(self._state.tilt * 100.0) + @convert_api_error_ha_error async def async_open_cover(self, **kwargs: Any) -> None: """Open the cover.""" self._client.cover_command(key=self._key, position=1.0) + @convert_api_error_ha_error async def async_close_cover(self, **kwargs: Any) -> None: """Close cover.""" self._client.cover_command(key=self._key, position=0.0) + @convert_api_error_ha_error async def async_stop_cover(self, **kwargs: Any) -> None: """Stop the cover.""" self._client.cover_command(key=self._key, stop=True) + @convert_api_error_ha_error async def async_set_cover_position(self, **kwargs: Any) -> None: """Move the cover to a specific position.""" self._client.cover_command(key=self._key, position=kwargs[ATTR_POSITION] / 100) + @convert_api_error_ha_error async def async_open_cover_tilt(self, **kwargs: Any) -> None: """Open the cover tilt.""" self._client.cover_command(key=self._key, tilt=1.0) + @convert_api_error_ha_error async def async_close_cover_tilt(self, **kwargs: Any) -> None: """Close the cover tilt.""" self._client.cover_command(key=self._key, tilt=0.0) + @convert_api_error_ha_error async def async_set_cover_tilt_position(self, **kwargs: Any) -> None: """Move the cover tilt to a specific position.""" tilt_position: int = kwargs[ATTR_TILT_POSITION] diff --git a/homeassistant/components/esphome/dashboard.py b/homeassistant/components/esphome/dashboard.py index 03264291d8f..b8a72ac4398 100644 --- a/homeassistant/components/esphome/dashboard.py +++ b/homeassistant/components/esphome/dashboard.py @@ -1,4 +1,5 @@ """Files to interact with a the ESPHome dashboard.""" + from __future__ import annotations import asyncio @@ -102,7 +103,7 @@ class ESPHomeDashboardManager: await dashboard.async_shutdown() self._cancel_shutdown = hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, on_hass_stop + EVENT_HOMEASSISTANT_STOP, on_hass_stop, run_immediately=True ) new_data = {"info": {"addon_slug": addon_slug, "host": host, "port": port}} diff --git a/homeassistant/components/esphome/date.py b/homeassistant/components/esphome/date.py new file mode 100644 index 00000000000..9998eea1a5d --- /dev/null +++ b/homeassistant/components/esphome/date.py @@ -0,0 +1,47 @@ +"""Support for esphome dates.""" + +from __future__ import annotations + +from datetime import date + +from aioesphomeapi import DateInfo, DateState + +from homeassistant.components.date import DateEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .entity import EsphomeEntity, esphome_state_property, platform_async_setup_entry + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up esphome dates based on a config entry.""" + await platform_async_setup_entry( + hass, + entry, + async_add_entities, + info_type=DateInfo, + entity_type=EsphomeDate, + state_type=DateState, + ) + + +class EsphomeDate(EsphomeEntity[DateInfo, DateState], DateEntity): + """A date implementation for esphome.""" + + @property + @esphome_state_property + def native_value(self) -> date | None: + """Return the state of the entity.""" + state = self._state + if state.missing_state: + return None + return date(state.year, state.month, state.day) + + async def async_set_value(self, value: date) -> None: + """Update the current date.""" + self._client.date_command(self._key, value.year, value.month, value.day) diff --git a/homeassistant/components/esphome/diagnostics.py b/homeassistant/components/esphome/diagnostics.py index f270196db50..44241f5950c 100644 --- a/homeassistant/components/esphome/diagnostics.py +++ b/homeassistant/components/esphome/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for ESPHome.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/esphome/domain_data.py b/homeassistant/components/esphome/domain_data.py index 6dae91c4c24..9ac8fe97614 100644 --- a/homeassistant/components/esphome/domain_data.py +++ b/homeassistant/components/esphome/domain_data.py @@ -1,4 +1,5 @@ """Support for esphome domain data.""" + from __future__ import annotations from dataclasses import dataclass, field diff --git a/homeassistant/components/esphome/entity.py b/homeassistant/components/esphome/entity.py index 7b06fadb33f..4f32f62ee62 100644 --- a/homeassistant/components/esphome/entity.py +++ b/homeassistant/components/esphome/entity.py @@ -1,12 +1,14 @@ """Support for esphome entities.""" + from __future__ import annotations -from collections.abc import Callable +from collections.abc import Awaitable, Callable, Coroutine import functools import math -from typing import TYPE_CHECKING, Any, Generic, TypeVar, cast +from typing import TYPE_CHECKING, Any, Concatenate, Generic, ParamSpec, TypeVar, cast from aioesphomeapi import ( + APIConnectionError, EntityCategory as EsphomeEntityCategory, EntityInfo, EntityState, @@ -17,11 +19,11 @@ import voluptuous as vol from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant, callback +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.device_registry import DeviceInfo -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -32,6 +34,7 @@ from .entry_data import RuntimeEntryData from .enum_mapper import EsphomeEnumMapper _R = TypeVar("_R") +_P = ParamSpec("_P") _InfoT = TypeVar("_InfoT", bound=EntityInfo) _EntityT = TypeVar("_EntityT", bound="EsphomeEntity[Any,Any]") _StateT = TypeVar("_StateT", bound=EntityState) @@ -140,17 +143,37 @@ def esphome_state_property( return _wrapper +def convert_api_error_ha_error( + func: Callable[Concatenate[_EntityT, _P], Awaitable[None]], +) -> Callable[Concatenate[_EntityT, _P], Coroutine[Any, Any, None]]: + """Decorate ESPHome command calls that send commands/make changes to the device. + + A decorator that wraps the passed in function, catches APIConnectionError errors, + and raises a HomeAssistant error instead. + """ + + async def handler(self: _EntityT, *args: _P.args, **kwargs: _P.kwargs) -> None: + try: + return await func(self, *args, **kwargs) + except APIConnectionError as error: + raise HomeAssistantError( + f"Error communicating with device: {error}" + ) from error + + return handler + + ICON_SCHEMA = vol.Schema(cv.icon) -ENTITY_CATEGORIES: EsphomeEnumMapper[ - EsphomeEntityCategory, EntityCategory | None -] = EsphomeEnumMapper( - { - EsphomeEntityCategory.NONE: None, - EsphomeEntityCategory.CONFIG: EntityCategory.CONFIG, - EsphomeEntityCategory.DIAGNOSTIC: EntityCategory.DIAGNOSTIC, - } +ENTITY_CATEGORIES: EsphomeEnumMapper[EsphomeEntityCategory, EntityCategory | None] = ( + EsphomeEnumMapper( + { + EsphomeEntityCategory.NONE: None, + EsphomeEntityCategory.CONFIG: EntityCategory.CONFIG, + EsphomeEntityCategory.DIAGNOSTIC: EntityCategory.DIAGNOSTIC, + } + ) ) @@ -204,25 +227,19 @@ class EsphomeEntity(Entity, Generic[_InfoT, _StateT]): async def async_added_to_hass(self) -> None: """Register callbacks.""" entry_data = self._entry_data - hass = self.hass - key = self._key - static_info = self._static_info - self.async_on_remove( - async_dispatcher_connect( - hass, - entry_data.signal_device_updated, + entry_data.async_subscribe_device_updated( self._on_device_update, ) ) self.async_on_remove( entry_data.async_subscribe_state_update( - self._state_type, key, self._on_state_update + self._state_type, self._key, self._on_state_update ) ) self.async_on_remove( entry_data.async_register_key_static_info_updated_callback( - static_info, self._on_static_info_update + self._static_info, self._on_static_info_update ) ) self._update_state_from_entry_data() diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py index a15f68fd6cc..da0dae52569 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -1,8 +1,9 @@ """Runtime entry data for ESPHome stored in hass.data.""" + from __future__ import annotations import asyncio -from collections.abc import Callable, Coroutine, Iterable +from collections.abc import Callable, Iterable from dataclasses import dataclass, field from functools import partial import logging @@ -18,6 +19,7 @@ from aioesphomeapi import ( CameraState, ClimateInfo, CoverInfo, + DateInfo, DeviceInfo, EntityInfo, EntityState, @@ -32,6 +34,7 @@ from aioesphomeapi import ( SwitchInfo, TextInfo, TextSensorInfo, + TimeInfo, UserService, build_unique_id, ) @@ -44,6 +47,7 @@ from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers import entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.storage import Store +from homeassistant.util.signal_type import SignalType from .const import DOMAIN from .dashboard import async_get_dashboard @@ -62,6 +66,7 @@ INFO_TYPE_TO_PLATFORM: dict[type[EntityInfo], Platform] = { CameraInfo: Platform.CAMERA, ClimateInfo: Platform.CLIMATE, CoverInfo: Platform.COVER, + DateInfo: Platform.DATE, FanInfo: Platform.FAN, LightInfo: Platform.LIGHT, LockInfo: Platform.LOCK, @@ -72,6 +77,7 @@ INFO_TYPE_TO_PLATFORM: dict[type[EntityInfo], Platform] = { SwitchInfo: Platform.SWITCH, TextInfo: Platform.TEXT, TextSensorInfo: Platform.SENSOR, + TimeInfo: Platform.TIME, } @@ -107,18 +113,17 @@ class RuntimeEntryData: device_info: DeviceInfo | None = None bluetooth_device: ESPHomeBluetoothDevice | None = None api_version: APIVersion = field(default_factory=APIVersion) - cleanup_callbacks: list[Callable[[], None]] = field(default_factory=list) - disconnect_callbacks: set[Callable[[], None]] = field(default_factory=set) - state_subscriptions: dict[ - tuple[type[EntityState], int], Callable[[], None] - ] = field(default_factory=dict) + cleanup_callbacks: list[CALLBACK_TYPE] = field(default_factory=list) + disconnect_callbacks: set[CALLBACK_TYPE] = field(default_factory=set) + state_subscriptions: dict[tuple[type[EntityState], int], CALLBACK_TYPE] = field( + default_factory=dict + ) + device_update_subscriptions: set[CALLBACK_TYPE] = field(default_factory=set) loaded_platforms: set[Platform] = field(default_factory=set) platform_load_lock: asyncio.Lock = field(default_factory=asyncio.Lock) _storage_contents: StoreData | None = None _pending_storage: Callable[[], StoreData] | None = None - assist_pipeline_update_callbacks: list[Callable[[], None]] = field( - default_factory=list - ) + assist_pipeline_update_callbacks: list[CALLBACK_TYPE] = field(default_factory=list) assist_pipeline_state: bool = False entity_info_callbacks: dict[ type[EntityInfo], list[Callable[[list[EntityInfo]], None]] @@ -143,14 +148,9 @@ class RuntimeEntryData: ) @property - def signal_device_updated(self) -> str: - """Return the signal to listen to for core device state update.""" - return f"esphome_{self.entry_id}_on_device_update" - - @property - def signal_static_info_updated(self) -> str: + def signal_static_info_updated(self) -> SignalType[list[EntityInfo]]: """Return the signal to listen to for updates on static info.""" - return f"esphome_{self.entry_id}_on_list" + return SignalType(f"esphome_{self.entry_id}_on_list") @callback def async_register_static_info_callback( @@ -174,15 +174,6 @@ class RuntimeEntryData: """Unsubscribe to when static info is registered.""" callbacks.remove(callback_) - @callback - def _async_unsubscribe_static_key_remove( - self, - callbacks: list[Callable[[], Coroutine[Any, Any, None]]], - callback_: Callable[[], Coroutine[Any, Any, None]], - ) -> None: - """Unsubscribe to when static info is removed.""" - callbacks.remove(callback_) - @callback def async_register_key_static_info_updated_callback( self, @@ -215,15 +206,15 @@ class RuntimeEntryData: @callback def async_subscribe_assist_pipeline_update( - self, update_callback: Callable[[], None] - ) -> Callable[[], None]: + self, update_callback: CALLBACK_TYPE + ) -> CALLBACK_TYPE: """Subscribe to assist pipeline updates.""" self.assist_pipeline_update_callbacks.append(update_callback) return partial(self._async_unsubscribe_assist_pipeline_update, update_callback) @callback def _async_unsubscribe_assist_pipeline_update( - self, update_callback: Callable[[], None] + self, update_callback: CALLBACK_TYPE ) -> None: """Unsubscribe to assist pipeline updates.""" self.assist_pipeline_update_callbacks.remove(update_callback) @@ -306,13 +297,24 @@ class RuntimeEntryData: # Then send dispatcher event async_dispatcher_send(hass, self.signal_static_info_updated, infos) + @callback + def async_subscribe_device_updated(self, callback_: CALLBACK_TYPE) -> CALLBACK_TYPE: + """Subscribe to state updates.""" + self.device_update_subscriptions.add(callback_) + return partial(self._async_unsubscribe_device_update, callback_) + + @callback + def _async_unsubscribe_device_update(self, callback_: CALLBACK_TYPE) -> None: + """Unsubscribe to device updates.""" + self.device_update_subscriptions.remove(callback_) + @callback def async_subscribe_state_update( self, state_type: type[EntityState], state_key: int, - entity_callback: Callable[[], None], - ) -> Callable[[], None]: + entity_callback: CALLBACK_TYPE, + ) -> CALLBACK_TYPE: """Subscribe to state updates.""" subscription_key = (state_type, state_key) self.state_subscriptions[subscription_key] = entity_callback @@ -339,7 +341,7 @@ class RuntimeEntryData: and subscription_key not in stale_state and state_type is not CameraState and not ( - state_type is SensorState # noqa: E721 + state_type is SensorState and (platform_info := self.info.get(SensorInfo)) and (entity_info := platform_info.get(state.key)) and (cast(SensorInfo, entity_info)).force_update @@ -358,9 +360,10 @@ class RuntimeEntryData: _LOGGER.exception("Error while calling subscription: %s", ex) @callback - def async_update_device_state(self, hass: HomeAssistant) -> None: + def async_update_device_state(self) -> None: """Distribute an update of a core device state like availability.""" - async_dispatcher_send(hass, self.signal_device_updated) + for callback_ in self.device_update_subscriptions.copy(): + callback_() async def async_load_from_store(self) -> tuple[list[EntityInfo], list[UserService]]: """Load the retained data from store and return de-serialized data.""" diff --git a/homeassistant/components/esphome/enum_mapper.py b/homeassistant/components/esphome/enum_mapper.py index fd09f9a05b6..0e59cde8a7e 100644 --- a/homeassistant/components/esphome/enum_mapper.py +++ b/homeassistant/components/esphome/enum_mapper.py @@ -21,12 +21,10 @@ class EsphomeEnumMapper(Generic[_EnumT, _ValT]): self._inverse: dict[_ValT, _EnumT] = {v: k for k, v in mapping.items()} @overload - def from_esphome(self, value: _EnumT) -> _ValT: - ... + def from_esphome(self, value: _EnumT) -> _ValT: ... @overload - def from_esphome(self, value: _EnumT | None) -> _ValT | None: - ... + def from_esphome(self, value: _EnumT | None) -> _ValT | None: ... def from_esphome(self, value: _EnumT | None) -> _ValT | None: """Convert from an esphome int representation to a hass string.""" diff --git a/homeassistant/components/esphome/fan.py b/homeassistant/components/esphome/fan.py index 90cda53dee6..082de3f7b7d 100644 --- a/homeassistant/components/esphome/fan.py +++ b/homeassistant/components/esphome/fan.py @@ -1,4 +1,5 @@ """Support for ESPHome fans.""" + from __future__ import annotations import math @@ -22,7 +23,12 @@ from homeassistant.util.percentage import ( ranged_value_to_percentage, ) -from .entity import EsphomeEntity, esphome_state_property, platform_async_setup_entry +from .entity import ( + EsphomeEntity, + convert_api_error_ha_error, + esphome_state_property, + platform_async_setup_entry, +) from .enum_mapper import EsphomeEnumMapper ORDERED_NAMED_FAN_SPEEDS = [FanSpeed.LOW, FanSpeed.MEDIUM, FanSpeed.HIGH] @@ -59,6 +65,7 @@ class EsphomeFan(EsphomeEntity[FanInfo, FanState], FanEntity): """Set the speed percentage of the fan.""" await self._async_set_percentage(percentage) + @convert_api_error_ha_error async def _async_set_percentage(self, percentage: int | None) -> None: if percentage == 0: await self.async_turn_off() @@ -88,20 +95,24 @@ class EsphomeFan(EsphomeEntity[FanInfo, FanState], FanEntity): """Turn on the fan.""" await self._async_set_percentage(percentage) + @convert_api_error_ha_error async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the fan.""" self._client.fan_command(key=self._key, state=False) + @convert_api_error_ha_error async def async_oscillate(self, oscillating: bool) -> None: """Oscillate the fan.""" self._client.fan_command(key=self._key, oscillating=oscillating) + @convert_api_error_ha_error async def async_set_direction(self, direction: str) -> None: """Set direction of the fan.""" self._client.fan_command( key=self._key, direction=_FAN_DIRECTIONS.from_hass(direction) ) + @convert_api_error_ha_error async def async_set_preset_mode(self, preset_mode: str) -> None: """Set the preset mode of the fan.""" self._client.fan_command(key=self._key, preset_mode=preset_mode) diff --git a/homeassistant/components/esphome/light.py b/homeassistant/components/esphome/light.py index 4f047bad757..bbb4021d58f 100644 --- a/homeassistant/components/esphome/light.py +++ b/homeassistant/components/esphome/light.py @@ -1,4 +1,5 @@ """Support for ESPHome lights.""" + from __future__ import annotations from functools import lru_cache @@ -32,7 +33,12 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .entity import EsphomeEntity, esphome_state_property, platform_async_setup_entry +from .entity import ( + EsphomeEntity, + convert_api_error_ha_error, + esphome_state_property, + platform_async_setup_entry, +) FLASH_LENGTHS = {FLASH_SHORT: 2, FLASH_LONG: 10} @@ -172,6 +178,7 @@ class EsphomeLight(EsphomeEntity[LightInfo, LightState], LightEntity): """Return true if the light is on.""" return self._state.state + @convert_api_error_ha_error async def async_turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" data: dict[str, Any] = {"key": self._key, "state": True} @@ -287,6 +294,7 @@ class EsphomeLight(EsphomeEntity[LightInfo, LightState], LightEntity): self._client.light_command(**data) + @convert_api_error_ha_error async def async_turn_off(self, **kwargs: Any) -> None: """Turn the entity off.""" data: dict[str, Any] = {"key": self._key, "state": False} diff --git a/homeassistant/components/esphome/lock.py b/homeassistant/components/esphome/lock.py index 55177fd9a51..98efdece92e 100644 --- a/homeassistant/components/esphome/lock.py +++ b/homeassistant/components/esphome/lock.py @@ -1,4 +1,5 @@ """Support for ESPHome locks.""" + from __future__ import annotations from typing import Any @@ -11,7 +12,12 @@ from homeassistant.const import ATTR_CODE from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .entity import EsphomeEntity, esphome_state_property, platform_async_setup_entry +from .entity import ( + EsphomeEntity, + convert_api_error_ha_error, + esphome_state_property, + platform_async_setup_entry, +) async def async_setup_entry( @@ -69,15 +75,18 @@ class EsphomeLock(EsphomeEntity[LockInfo, LockEntityState], LockEntity): """Return true if the lock is jammed (incomplete locking).""" return self._state.state == LockState.JAMMED + @convert_api_error_ha_error async def async_lock(self, **kwargs: Any) -> None: """Lock the lock.""" self._client.lock_command(self._key, LockCommand.LOCK) + @convert_api_error_ha_error async def async_unlock(self, **kwargs: Any) -> None: """Unlock the lock.""" code = kwargs.get(ATTR_CODE, None) self._client.lock_command(self._key, LockCommand.UNLOCK, code) + @convert_api_error_ha_error async def async_open(self, **kwargs: Any) -> None: """Open the door latch.""" self._client.lock_command(self._key, LockCommand.OPEN) diff --git a/homeassistant/components/esphome/manager.py b/homeassistant/components/esphome/manager.py index bd01bea8795..dc95952194e 100644 --- a/homeassistant/components/esphome/manager.py +++ b/homeassistant/components/esphome/manager.py @@ -1,4 +1,5 @@ """Manager for esphome devices.""" + from __future__ import annotations import asyncio @@ -29,7 +30,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_DEVICE_ID, CONF_MODE, - EVENT_HOMEASSISTANT_STOP, + EVENT_HOMEASSISTANT_CLOSE, EVENT_LOGGING_CHANGED, ) from homeassistant.core import Event, HomeAssistant, ServiceCall, State, callback @@ -49,7 +50,7 @@ from homeassistant.helpers.issue_registry import ( ) from homeassistant.helpers.service import async_set_service_schema from homeassistant.helpers.template import Template -from homeassistant.helpers.typing import EventType +from homeassistant.util.async_ import create_eager_task from .bluetooth import async_connect_scanner from .const import ( @@ -281,7 +282,7 @@ class ESPHomeManager: def _send_home_assistant_state_event( self, attribute: str | None, - event: EventType[EventStateChangedData], + event: Event[EventStateChangedData], ) -> None: """Forward Home Assistant states updates to ESPHome.""" event_data = event.data @@ -390,8 +391,8 @@ class ESPHomeManager: stored_device_name = entry.data.get(CONF_DEVICE_NAME) unique_id_is_mac_address = unique_id and ":" in unique_id results = await asyncio.gather( - cli.device_info(), - cli.list_entities_services(), + create_eager_task(cli.device_info()), + create_eager_task(cli.list_entities_services()), ) device_info: EsphomeDeviceInfo = results[0] @@ -454,7 +455,7 @@ class ESPHomeManager: self.device_id = _async_setup_device_registry(hass, entry, entry_data) - entry_data.async_update_device_state(hass) + entry_data.async_update_device_state() await entry_data.async_update_static_infos( hass, entry, entity_infos, device_info.mac_address ) @@ -509,7 +510,7 @@ class ESPHomeManager: # since it generates a lot of state changed events and database # writes when we already know we're shutting down and the state # will be cleared anyway. - entry_data.async_update_device_state(hass) + entry_data.async_update_device_state() async def on_connect_error(self, err: Exception) -> None: """Start reauth flow if appropriate connect error type.""" @@ -541,12 +542,19 @@ class ESPHomeManager: # the callback twice when shutting down Home Assistant. # "Unable to remove unknown listener # .onetime_listener>" + # We only close the connection at the last possible moment + # when the CLOSE event is fired so anything using a Bluetooth + # proxy has a chance to shut down properly. entry_data.cleanup_callbacks.append( - hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, self.on_stop) + hass.bus.async_listen( + EVENT_HOMEASSISTANT_CLOSE, self.on_stop, run_immediately=True + ) ) entry_data.cleanup_callbacks.append( hass.bus.async_listen( - EVENT_LOGGING_CHANGED, self._async_handle_logging_changed + EVENT_LOGGING_CHANGED, + self._async_handle_logging_changed, + run_immediately=True, ) ) @@ -772,8 +780,7 @@ def _setup_services( # New service to_register.append(service) - for service in old_services.values(): - to_unregister.append(service) + to_unregister.extend(old_services.values()) entry_data.services = {serv.key: serv for serv in services} diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index a1841306f0c..f1a5333c403 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -11,12 +11,11 @@ } ], "documentation": "https://www.home-assistant.io/integrations/esphome", - "import_executor": true, "integration_type": "device", "iot_class": "local_push", "loggers": ["aioesphomeapi", "noiseprotocol", "bleak_esphome"], "requirements": [ - "aioesphomeapi==23.0.0", + "aioesphomeapi==23.2.0", "esphome-dashboard-api==1.2.3", "bleak-esphome==1.0.0" ], diff --git a/homeassistant/components/esphome/media_player.py b/homeassistant/components/esphome/media_player.py index 208f1edebeb..c2bfdc5850d 100644 --- a/homeassistant/components/esphome/media_player.py +++ b/homeassistant/components/esphome/media_player.py @@ -1,4 +1,5 @@ """Support for ESPHome media players.""" + from __future__ import annotations from typing import Any @@ -25,7 +26,12 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .entity import EsphomeEntity, esphome_state_property, platform_async_setup_entry +from .entity import ( + EsphomeEntity, + convert_api_error_ha_error, + esphome_state_property, + platform_async_setup_entry, +) from .enum_mapper import EsphomeEnumMapper @@ -94,6 +100,7 @@ class EsphomeMediaPlayer( """Volume level of the media player (0..1).""" return self._state.volume + @convert_api_error_ha_error async def async_play_media( self, media_type: MediaType | str, media_id: str, **kwargs: Any ) -> None: @@ -123,22 +130,27 @@ class EsphomeMediaPlayer( content_filter=lambda item: item.media_content_type.startswith("audio/"), ) + @convert_api_error_ha_error async def async_set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" self._client.media_player_command(self._key, volume=volume) + @convert_api_error_ha_error async def async_media_pause(self) -> None: """Send pause command.""" self._client.media_player_command(self._key, command=MediaPlayerCommand.PAUSE) + @convert_api_error_ha_error async def async_media_play(self) -> None: """Send play command.""" self._client.media_player_command(self._key, command=MediaPlayerCommand.PLAY) + @convert_api_error_ha_error async def async_media_stop(self) -> None: """Send stop command.""" self._client.media_player_command(self._key, command=MediaPlayerCommand.STOP) + @convert_api_error_ha_error async def async_mute_volume(self, mute: bool) -> None: """Mute the volume.""" self._client.media_player_command( diff --git a/homeassistant/components/esphome/number.py b/homeassistant/components/esphome/number.py index 2619dbad045..01744dd9998 100644 --- a/homeassistant/components/esphome/number.py +++ b/homeassistant/components/esphome/number.py @@ -1,4 +1,5 @@ """Support for esphome numbers.""" + from __future__ import annotations import math @@ -16,7 +17,12 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.enum import try_parse_enum -from .entity import EsphomeEntity, esphome_state_property, platform_async_setup_entry +from .entity import ( + EsphomeEntity, + convert_api_error_ha_error, + esphome_state_property, + platform_async_setup_entry, +) from .enum_mapper import EsphomeEnumMapper @@ -77,6 +83,7 @@ class EsphomeNumber(EsphomeEntity[NumberInfo, NumberState], NumberEntity): return None return state.state + @convert_api_error_ha_error async def async_set_native_value(self, value: float) -> None: """Update the current value.""" self._client.number_command(self._key, value) diff --git a/homeassistant/components/esphome/select.py b/homeassistant/components/esphome/select.py index 43965a11df4..07a9d70e558 100644 --- a/homeassistant/components/esphome/select.py +++ b/homeassistant/components/esphome/select.py @@ -1,4 +1,5 @@ """Support for esphome selects.""" + from __future__ import annotations from aioesphomeapi import EntityInfo, SelectInfo, SelectState @@ -17,6 +18,7 @@ from .domain_data import DomainData from .entity import ( EsphomeAssistEntity, EsphomeEntity, + convert_api_error_ha_error, esphome_state_property, platform_async_setup_entry, ) @@ -65,6 +67,7 @@ class EsphomeSelect(EsphomeEntity[SelectInfo, SelectState], SelectEntity): state = self._state return None if state.missing_state else state.state + @convert_api_error_ha_error async def async_select_option(self, option: str) -> None: """Change the selected option.""" self._client.select_command(self._key, option) diff --git a/homeassistant/components/esphome/sensor.py b/homeassistant/components/esphome/sensor.py index d2be19a3fb3..4c99463505f 100644 --- a/homeassistant/components/esphome/sensor.py +++ b/homeassistant/components/esphome/sensor.py @@ -1,4 +1,5 @@ """Support for esphome sensors.""" + from __future__ import annotations from datetime import date, datetime @@ -51,15 +52,15 @@ async def async_setup_entry( ) -_STATE_CLASSES: EsphomeEnumMapper[ - EsphomeSensorStateClass, SensorStateClass | None -] = EsphomeEnumMapper( - { - EsphomeSensorStateClass.NONE: None, - EsphomeSensorStateClass.MEASUREMENT: SensorStateClass.MEASUREMENT, - EsphomeSensorStateClass.TOTAL_INCREASING: SensorStateClass.TOTAL_INCREASING, - EsphomeSensorStateClass.TOTAL: SensorStateClass.TOTAL, - } +_STATE_CLASSES: EsphomeEnumMapper[EsphomeSensorStateClass, SensorStateClass | None] = ( + EsphomeEnumMapper( + { + EsphomeSensorStateClass.NONE: None, + EsphomeSensorStateClass.MEASUREMENT: SensorStateClass.MEASUREMENT, + EsphomeSensorStateClass.TOTAL_INCREASING: SensorStateClass.TOTAL_INCREASING, + EsphomeSensorStateClass.TOTAL: SensorStateClass.TOTAL, + } + ) ) diff --git a/homeassistant/components/esphome/switch.py b/homeassistant/components/esphome/switch.py index a6ecd86f264..6fa73058bd2 100644 --- a/homeassistant/components/esphome/switch.py +++ b/homeassistant/components/esphome/switch.py @@ -1,4 +1,5 @@ """Support for ESPHome switches.""" + from __future__ import annotations from typing import Any @@ -11,7 +12,12 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.enum import try_parse_enum -from .entity import EsphomeEntity, esphome_state_property, platform_async_setup_entry +from .entity import ( + EsphomeEntity, + convert_api_error_ha_error, + esphome_state_property, + platform_async_setup_entry, +) async def async_setup_entry( @@ -47,10 +53,12 @@ class EsphomeSwitch(EsphomeEntity[SwitchInfo, SwitchState], SwitchEntity): """Return true if the switch is on.""" return self._state.state + @convert_api_error_ha_error async def async_turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" self._client.switch_command(self._key, True) + @convert_api_error_ha_error async def async_turn_off(self, **kwargs: Any) -> None: """Turn the entity off.""" self._client.switch_command(self._key, False) diff --git a/homeassistant/components/esphome/text.py b/homeassistant/components/esphome/text.py index 337cbb26fee..7d455e9ec21 100644 --- a/homeassistant/components/esphome/text.py +++ b/homeassistant/components/esphome/text.py @@ -1,4 +1,5 @@ """Support for esphome texts.""" + from __future__ import annotations from aioesphomeapi import EntityInfo, TextInfo, TextMode as EsphomeTextMode, TextState @@ -8,7 +9,12 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .entity import EsphomeEntity, esphome_state_property, platform_async_setup_entry +from .entity import ( + EsphomeEntity, + convert_api_error_ha_error, + esphome_state_property, + platform_async_setup_entry, +) from .enum_mapper import EsphomeEnumMapper @@ -58,6 +64,7 @@ class EsphomeText(EsphomeEntity[TextInfo, TextState], TextEntity): return None return state.state + @convert_api_error_ha_error async def async_set_value(self, value: str) -> None: """Update the current value.""" self._client.text_command(self._key, value) diff --git a/homeassistant/components/esphome/time.py b/homeassistant/components/esphome/time.py new file mode 100644 index 00000000000..de985a1e1d6 --- /dev/null +++ b/homeassistant/components/esphome/time.py @@ -0,0 +1,47 @@ +"""Support for esphome times.""" + +from __future__ import annotations + +from datetime import time + +from aioesphomeapi import TimeInfo, TimeState + +from homeassistant.components.time import TimeEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .entity import EsphomeEntity, esphome_state_property, platform_async_setup_entry + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up esphome times based on a config entry.""" + await platform_async_setup_entry( + hass, + entry, + async_add_entities, + info_type=TimeInfo, + entity_type=EsphomeTime, + state_type=TimeState, + ) + + +class EsphomeTime(EsphomeEntity[TimeInfo, TimeState], TimeEntity): + """A time implementation for esphome.""" + + @property + @esphome_state_property + def native_value(self) -> time | None: + """Return the state of the entity.""" + state = self._state + if state.missing_state: + return None + return time(state.hour, state.minute, state.second) + + async def async_set_value(self, value: time) -> None: + """Update the current time.""" + self._client.time_command(self._key, value.hour, value.minute, value.second) diff --git a/homeassistant/components/esphome/update.py b/homeassistant/components/esphome/update.py index a444c98b987..3e5a82bbd0b 100644 --- a/homeassistant/components/esphome/update.py +++ b/homeassistant/components/esphome/update.py @@ -1,4 +1,5 @@ """Update platform for ESPHome.""" + from __future__ import annotations import asyncio @@ -60,9 +61,7 @@ async def async_setup_entry( return unsubs = [ - async_dispatcher_connect( - hass, entry_data.signal_device_updated, _async_setup_update_entity - ), + entry_data.async_subscribe_device_updated(_async_setup_update_entity), dashboard.async_add_listener(_async_setup_update_entity), ] @@ -140,7 +139,9 @@ class ESPHomeUpdateEntity(CoordinatorEntity[ESPHomeDashboard], UpdateEntity): ) @callback - def _handle_device_update(self, static_info: EntityInfo | None = None) -> None: + def _handle_device_update( + self, static_info: list[EntityInfo] | None = None + ) -> None: """Handle updated data from the device.""" self._update_attrs() self.async_write_ha_state() @@ -158,11 +159,7 @@ class ESPHomeUpdateEntity(CoordinatorEntity[ESPHomeDashboard], UpdateEntity): ) ) self.async_on_remove( - async_dispatcher_connect( - hass, - entry_data.signal_device_updated, - self._handle_device_update, - ) + entry_data.async_subscribe_device_updated(self._handle_device_update) ) async def async_install( diff --git a/homeassistant/components/esphome/voice_assistant.py b/homeassistant/components/esphome/voice_assistant.py index 15b580a0601..f856cc27179 100644 --- a/homeassistant/components/esphome/voice_assistant.py +++ b/homeassistant/components/esphome/voice_assistant.py @@ -95,7 +95,7 @@ class VoiceAssistantUDPServer(asyncio.DatagramProtocol): @property def is_running(self) -> bool: - """True if the the UDP server is started and hasn't been asked to stop.""" + """True if the UDP server is started and hasn't been asked to stop.""" return self.started and (not self.stop_requested) async def start_server(self) -> int: diff --git a/homeassistant/components/etherscan/sensor.py b/homeassistant/components/etherscan/sensor.py index d98ddcba23c..38219bf659b 100644 --- a/homeassistant/components/etherscan/sensor.py +++ b/homeassistant/components/etherscan/sensor.py @@ -1,4 +1,5 @@ """Support for Etherscan sensors.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/eufy/__init__.py b/homeassistant/components/eufy/__init__.py index 52d6fead3eb..8ebe3e08843 100644 --- a/homeassistant/components/eufy/__init__.py +++ b/homeassistant/components/eufy/__init__.py @@ -1,4 +1,5 @@ """Support for EufyHome devices.""" + import lakeside import voluptuous as vol diff --git a/homeassistant/components/eufy/light.py b/homeassistant/components/eufy/light.py index 5185dcd8818..c1506c00cdc 100644 --- a/homeassistant/components/eufy/light.py +++ b/homeassistant/components/eufy/light.py @@ -1,4 +1,5 @@ """Support for EufyHome lights.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/eufy/switch.py b/homeassistant/components/eufy/switch.py index 324133354fb..58bcc6ceb21 100644 --- a/homeassistant/components/eufy/switch.py +++ b/homeassistant/components/eufy/switch.py @@ -1,4 +1,5 @@ """Support for EufyHome switches.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/eufylife_ble/__init__.py b/homeassistant/components/eufylife_ble/__init__.py index f407e86a289..f66cf7df30d 100644 --- a/homeassistant/components/eufylife_ble/__init__.py +++ b/homeassistant/components/eufylife_ble/__init__.py @@ -1,4 +1,5 @@ """The EufyLife integration.""" + from __future__ import annotations from eufylife_ble_client import EufyLifeBLEDevice diff --git a/homeassistant/components/eufylife_ble/config_flow.py b/homeassistant/components/eufylife_ble/config_flow.py index e3a1a301f25..072a025cf2b 100644 --- a/homeassistant/components/eufylife_ble/config_flow.py +++ b/homeassistant/components/eufylife_ble/config_flow.py @@ -1,4 +1,5 @@ """Config flow for the EufyLife integration.""" + from __future__ import annotations from typing import Any @@ -10,9 +11,8 @@ from homeassistant.components.bluetooth import ( BluetoothServiceInfoBleak, async_discovered_service_info, ) -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_ADDRESS, CONF_MODEL -from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN @@ -29,7 +29,7 @@ class EufyLifeConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_bluetooth( self, discovery_info: BluetoothServiceInfoBleak - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the bluetooth discovery step.""" await self.async_set_unique_id(discovery_info.address) self._abort_if_unique_id_configured() @@ -42,7 +42,7 @@ class EufyLifeConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_bluetooth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm discovery.""" assert self._discovery_info is not None discovery_info = self._discovery_info @@ -64,7 +64,7 @@ class EufyLifeConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the user step to pick discovered device.""" if user_input is not None: address = user_input[CONF_ADDRESS] diff --git a/homeassistant/components/eufylife_ble/models.py b/homeassistant/components/eufylife_ble/models.py index 62537f22f23..eb937fc4f3d 100644 --- a/homeassistant/components/eufylife_ble/models.py +++ b/homeassistant/components/eufylife_ble/models.py @@ -1,4 +1,5 @@ """Models for the EufyLife integration.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/homeassistant/components/eufylife_ble/sensor.py b/homeassistant/components/eufylife_ble/sensor.py index 69b88bb01f6..5e3ae64aabf 100644 --- a/homeassistant/components/eufylife_ble/sensor.py +++ b/homeassistant/components/eufylife_ble/sensor.py @@ -1,4 +1,5 @@ """Support for EufyLife sensors.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/event/__init__.py b/homeassistant/components/event/__init__.py index b05c3a6f3a5..2ad0e6d950b 100644 --- a/homeassistant/components/event/__init__.py +++ b/homeassistant/components/event/__init__.py @@ -1,4 +1,5 @@ """Component for handling incoming events as a platform.""" + from __future__ import annotations from dataclasses import asdict, dataclass @@ -9,7 +10,7 @@ from typing import TYPE_CHECKING, Any, Self, final from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.config_validation import ( # noqa: F401 +from homeassistant.helpers.config_validation import ( PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, ) @@ -146,7 +147,7 @@ class EventEntity(RestoreEntity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_) and self.entity_description.event_types is not None ): return self.entity_description.event_types - raise AttributeError() + raise AttributeError @final def _trigger_event( diff --git a/homeassistant/components/everlights/light.py b/homeassistant/components/everlights/light.py index 1a177cf8909..334e464d67e 100644 --- a/homeassistant/components/everlights/light.py +++ b/homeassistant/components/everlights/light.py @@ -1,4 +1,5 @@ """Support for EverLights lights.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/evil_genius_labs/__init__.py b/homeassistant/components/evil_genius_labs/__init__.py index 44d46d27a9d..fe91e58d839 100644 --- a/homeassistant/components/evil_genius_labs/__init__.py +++ b/homeassistant/components/evil_genius_labs/__init__.py @@ -1,4 +1,5 @@ """The Evil Genius Labs integration.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/evil_genius_labs/config_flow.py b/homeassistant/components/evil_genius_labs/config_flow.py index ab2e116b2a6..283b3d36beb 100644 --- a/homeassistant/components/evil_genius_labs/config_flow.py +++ b/homeassistant/components/evil_genius_labs/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Evil Genius Labs integration.""" + from __future__ import annotations import asyncio @@ -9,9 +10,8 @@ import aiohttp import pyevilgenius import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import aiohttp_client @@ -40,14 +40,14 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, return {"title": data["name"]["value"], "unique_id": info["wiFiChipId"]} -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class EvilGeniusLabsConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Evil Genius Labs.""" VERSION = 1 async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" if user_input is None: return self.async_show_form( diff --git a/homeassistant/components/evil_genius_labs/diagnostics.py b/homeassistant/components/evil_genius_labs/diagnostics.py index a6a15165716..2249e1269b0 100644 --- a/homeassistant/components/evil_genius_labs/diagnostics.py +++ b/homeassistant/components/evil_genius_labs/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for Evil Genius Labs.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/evil_genius_labs/light.py b/homeassistant/components/evil_genius_labs/light.py index 5612d0e8522..c64a22d28cd 100644 --- a/homeassistant/components/evil_genius_labs/light.py +++ b/homeassistant/components/evil_genius_labs/light.py @@ -1,4 +1,5 @@ """Light platform for Evil Genius Light.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/evil_genius_labs/util.py b/homeassistant/components/evil_genius_labs/util.py index eb2caf59d9d..db07cf46918 100644 --- a/homeassistant/components/evil_genius_labs/util.py +++ b/homeassistant/components/evil_genius_labs/util.py @@ -1,4 +1,5 @@ """Utilities for Evil Genius Labs.""" + from __future__ import annotations from collections.abc import Awaitable, Callable, Coroutine diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py index ddad635ddcf..3017685a307 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -2,6 +2,7 @@ Such systems include evohome, Round Thermostat, and others. """ + from __future__ import annotations from collections.abc import Awaitable diff --git a/homeassistant/components/evohome/climate.py b/homeassistant/components/evohome/climate.py index 8b74d31cc0d..2d462b5c525 100644 --- a/homeassistant/components/evohome/climate.py +++ b/homeassistant/components/evohome/climate.py @@ -1,4 +1,5 @@ """Support for Climate devices of (EMEA/EU-based) Honeywell TCC systems.""" + from __future__ import annotations from datetime import datetime, timedelta diff --git a/homeassistant/components/evohome/const.py b/homeassistant/components/evohome/const.py index 6bd3a59c225..1347c1f797c 100644 --- a/homeassistant/components/evohome/const.py +++ b/homeassistant/components/evohome/const.py @@ -1,4 +1,5 @@ """Support for (EMEA/EU-based) Honeywell TCC climate systems.""" + DOMAIN = "evohome" STORAGE_VER = 1 diff --git a/homeassistant/components/evohome/icons.json b/homeassistant/components/evohome/icons.json new file mode 100644 index 00000000000..cd0005e2546 --- /dev/null +++ b/homeassistant/components/evohome/icons.json @@ -0,0 +1,9 @@ +{ + "services": { + "set_system_mode": "mdi:pencil", + "reset_system": "mdi:refresh", + "refresh_system": "mdi:refresh", + "set_zone_override": "mdi:motion-sensor", + "clear_zone_override": "mdi:motion-sensor-off" + } +} diff --git a/homeassistant/components/evohome/water_heater.py b/homeassistant/components/evohome/water_heater.py index 26a60f9ec08..26be4b47a36 100644 --- a/homeassistant/components/evohome/water_heater.py +++ b/homeassistant/components/evohome/water_heater.py @@ -1,4 +1,5 @@ """Support for WaterHeater devices of (EMEA/EU) Honeywell TCC systems.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/ezviz/__init__.py b/homeassistant/components/ezviz/__init__.py index 12754af25e8..c453060b472 100644 --- a/homeassistant/components/ezviz/__init__.py +++ b/homeassistant/components/ezviz/__init__.py @@ -1,4 +1,5 @@ """Support for EZVIZ camera.""" + import logging from pyezviz.client import EzvizClient diff --git a/homeassistant/components/ezviz/alarm_control_panel.py b/homeassistant/components/ezviz/alarm_control_panel.py index 1cdda152685..21e9f2d0422 100644 --- a/homeassistant/components/ezviz/alarm_control_panel.py +++ b/homeassistant/components/ezviz/alarm_control_panel.py @@ -1,4 +1,5 @@ """Support for Ezviz alarm.""" + from __future__ import annotations from dataclasses import dataclass @@ -33,20 +34,13 @@ SCAN_INTERVAL = timedelta(seconds=60) PARALLEL_UPDATES = 0 -@dataclass(frozen=True) -class EzvizAlarmControlPanelEntityDescriptionMixin: - """Mixin values for EZVIZ Alarm control panel entities.""" +@dataclass(frozen=True, kw_only=True) +class EzvizAlarmControlPanelEntityDescription(AlarmControlPanelEntityDescription): + """Describe an EZVIZ Alarm control panel entity.""" ezviz_alarm_states: list -@dataclass(frozen=True) -class EzvizAlarmControlPanelEntityDescription( - AlarmControlPanelEntityDescription, EzvizAlarmControlPanelEntityDescriptionMixin -): - """Describe an EZVIZ Alarm control panel entity.""" - - ALARM_TYPE = EzvizAlarmControlPanelEntityDescription( key="ezviz_alarm", ezviz_alarm_states=[ diff --git a/homeassistant/components/ezviz/binary_sensor.py b/homeassistant/components/ezviz/binary_sensor.py index 81697e2772c..c13375cb487 100644 --- a/homeassistant/components/ezviz/binary_sensor.py +++ b/homeassistant/components/ezviz/binary_sensor.py @@ -1,4 +1,5 @@ """Support for EZVIZ binary sensors.""" + from __future__ import annotations from homeassistant.components.binary_sensor import ( diff --git a/homeassistant/components/ezviz/button.py b/homeassistant/components/ezviz/button.py index abc44419075..3c89677da09 100644 --- a/homeassistant/components/ezviz/button.py +++ b/homeassistant/components/ezviz/button.py @@ -1,4 +1,5 @@ """Support for EZVIZ button controls.""" + from __future__ import annotations from collections.abc import Callable @@ -22,26 +23,18 @@ from .entity import EzvizEntity PARALLEL_UPDATES = 1 -@dataclass(frozen=True) -class EzvizButtonEntityDescriptionMixin: - """Mixin values for EZVIZ button entities.""" +@dataclass(frozen=True, kw_only=True) +class EzvizButtonEntityDescription(ButtonEntityDescription): + """Describe a EZVIZ Button.""" method: Callable[[EzvizClient, str, str], Any] supported_ext: str -@dataclass(frozen=True) -class EzvizButtonEntityDescription( - ButtonEntityDescription, EzvizButtonEntityDescriptionMixin -): - """Describe a EZVIZ Button.""" - - BUTTON_ENTITIES = ( EzvizButtonEntityDescription( key="ptz_up", translation_key="ptz_up", - icon="mdi:pan", method=lambda pyezviz_client, serial, run: pyezviz_client.ptz_control( "UP", serial, run ), @@ -50,7 +43,6 @@ BUTTON_ENTITIES = ( EzvizButtonEntityDescription( key="ptz_down", translation_key="ptz_down", - icon="mdi:pan", method=lambda pyezviz_client, serial, run: pyezviz_client.ptz_control( "DOWN", serial, run ), @@ -59,7 +51,6 @@ BUTTON_ENTITIES = ( EzvizButtonEntityDescription( key="ptz_left", translation_key="ptz_left", - icon="mdi:pan", method=lambda pyezviz_client, serial, run: pyezviz_client.ptz_control( "LEFT", serial, run ), @@ -68,7 +59,6 @@ BUTTON_ENTITIES = ( EzvizButtonEntityDescription( key="ptz_right", translation_key="ptz_right", - icon="mdi:pan", method=lambda pyezviz_client, serial, run: pyezviz_client.ptz_control( "RIGHT", serial, run ), @@ -92,9 +82,9 @@ async def async_setup_entry( async_add_entities( EzvizButtonEntity(coordinator, camera, entity_description) for camera in coordinator.data - for capibility, value in coordinator.data[camera]["supportExt"].items() + for capability, value in coordinator.data[camera]["supportExt"].items() for entity_description in BUTTON_ENTITIES - if capibility == entity_description.supported_ext + if capability == entity_description.supported_ext if value == "1" ) diff --git a/homeassistant/components/ezviz/camera.py b/homeassistant/components/ezviz/camera.py index 6397d8a27dc..455c41b385f 100644 --- a/homeassistant/components/ezviz/camera.py +++ b/homeassistant/components/ezviz/camera.py @@ -1,4 +1,5 @@ """Support ezviz camera devices.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/ezviz/config_flow.py b/homeassistant/components/ezviz/config_flow.py index 77598ad6a1c..a453398a17a 100644 --- a/homeassistant/components/ezviz/config_flow.py +++ b/homeassistant/components/ezviz/config_flow.py @@ -1,4 +1,5 @@ """Config flow for EZVIZ.""" + from __future__ import annotations from collections.abc import Mapping @@ -16,7 +17,12 @@ from pyezviz.exceptions import ( from pyezviz.test_cam_rtsp import TestRTSPAuth import voluptuous as vol -from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.const import ( CONF_CUSTOMIZE, CONF_IP_ADDRESS, @@ -27,7 +33,6 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult from .const import ( ATTR_SERIAL, @@ -90,7 +95,7 @@ class EzvizConfigFlow(ConfigFlow, domain=DOMAIN): VERSION = 1 - async def _validate_and_create_camera_rtsp(self, data: dict) -> FlowResult: + async def _validate_and_create_camera_rtsp(self, data: dict) -> ConfigFlowResult: """Try DESCRIBE on RTSP camera with credentials.""" # Get EZVIZ cloud credentials from config entry @@ -146,7 +151,7 @@ class EzvizConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initiated by the user.""" # Check if EZVIZ cloud account is present in entry config, @@ -213,7 +218,7 @@ class EzvizConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user_custom_url( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initiated by the user for custom region url.""" errors = {} auth_data = {} @@ -262,7 +267,7 @@ class EzvizConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_integration_discovery( self, discovery_info: dict[str, Any] - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow for discovered camera without rtsp config entry.""" await self.async_set_unique_id(discovery_info[ATTR_SERIAL]) @@ -275,7 +280,7 @@ class EzvizConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm and create entry from discovery step.""" errors = {} @@ -315,14 +320,16 @@ class EzvizConfigFlow(ConfigFlow, domain=DOMAIN): }, ) - async def async_step_reauth(self, user_input: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, user_input: Mapping[str, Any] + ) -> ConfigFlowResult: """Handle a flow for reauthentication with password.""" return await self.async_step_reauth_confirm() async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a Confirm flow for reauthentication with password.""" auth_data = {} errors = {} @@ -390,7 +397,7 @@ class EzvizOptionsFlowHandler(OptionsFlow): async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Manage EZVIZ options.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) diff --git a/homeassistant/components/ezviz/coordinator.py b/homeassistant/components/ezviz/coordinator.py index 427e52f7dd0..c983371f4f8 100644 --- a/homeassistant/components/ezviz/coordinator.py +++ b/homeassistant/components/ezviz/coordinator.py @@ -1,4 +1,5 @@ """Provides the ezviz DataUpdateCoordinator.""" + import asyncio from datetime import timedelta import logging diff --git a/homeassistant/components/ezviz/entity.py b/homeassistant/components/ezviz/entity.py index c8ce3daf074..44de4a0c9c7 100644 --- a/homeassistant/components/ezviz/entity.py +++ b/homeassistant/components/ezviz/entity.py @@ -1,4 +1,5 @@ """An abstract class common to all EZVIZ entities.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/ezviz/icons.json b/homeassistant/components/ezviz/icons.json new file mode 100644 index 00000000000..89b4747ed69 --- /dev/null +++ b/homeassistant/components/ezviz/icons.json @@ -0,0 +1,32 @@ +{ + "entity": { + "button": { + "ptz_up": { + "default": "mdi:pan" + }, + "ptz_down": { + "default": "mdi:pan" + }, + "ptz_left": { + "default": "mdi:pan" + }, + "ptz_right": { + "default": "mdi:pan" + } + }, + "number": { + "detection_sensibility": { + "default": "mdi:eye" + } + }, + "select": { + "alarm_sound_mode": { + "default": "mdi:alarm" + } + } + }, + "services": { + "set_alarm_detection_sensibility": "mdi:motion-sensor", + "wake_device": "mdi:sleep-off" + } +} diff --git a/homeassistant/components/ezviz/image.py b/homeassistant/components/ezviz/image.py index aeb8eafe68f..0c362f8cbe7 100644 --- a/homeassistant/components/ezviz/image.py +++ b/homeassistant/components/ezviz/image.py @@ -1,4 +1,5 @@ """Support EZVIZ last motion image.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/ezviz/light.py b/homeassistant/components/ezviz/light.py index 558072658d3..c35b53b47b7 100644 --- a/homeassistant/components/ezviz/light.py +++ b/homeassistant/components/ezviz/light.py @@ -1,4 +1,5 @@ """Support for EZVIZ light entity.""" + from __future__ import annotations from typing import Any @@ -35,8 +36,8 @@ async def async_setup_entry( async_add_entities( EzvizLight(coordinator, camera) for camera in coordinator.data - for capibility, value in coordinator.data[camera]["supportExt"].items() - if capibility == str(SupportExt.SupportAlarmLight.value) + for capability, value in coordinator.data[camera]["supportExt"].items() + if capability == str(SupportExt.SupportAlarmLight.value) if value == "1" ) diff --git a/homeassistant/components/ezviz/number.py b/homeassistant/components/ezviz/number.py index c922173aa87..08fbd3afb34 100644 --- a/homeassistant/components/ezviz/number.py +++ b/homeassistant/components/ezviz/number.py @@ -1,4 +1,5 @@ """Support for EZVIZ number controls.""" + from __future__ import annotations from dataclasses import dataclass @@ -30,25 +31,17 @@ PARALLEL_UPDATES = 0 _LOGGER = logging.getLogger(__name__) -@dataclass(frozen=True) -class EzvizNumberEntityDescriptionMixin: - """Mixin values for EZVIZ Number entities.""" +@dataclass(frozen=True, kw_only=True) +class EzvizNumberEntityDescription(NumberEntityDescription): + """Describe a EZVIZ Number.""" supported_ext: str supported_ext_value: list -@dataclass(frozen=True) -class EzvizNumberEntityDescription( - NumberEntityDescription, EzvizNumberEntityDescriptionMixin -): - """Describe a EZVIZ Number.""" - - NUMBER_TYPE = EzvizNumberEntityDescription( key="detection_sensibility", translation_key="detection_sensibility", - icon="mdi:eye", entity_category=EntityCategory.CONFIG, native_min_value=0, native_step=1, @@ -68,8 +61,8 @@ async def async_setup_entry( async_add_entities( EzvizNumber(coordinator, camera, value, entry.entry_id) for camera in coordinator.data - for capibility, value in coordinator.data[camera]["supportExt"].items() - if capibility == NUMBER_TYPE.supported_ext + for capability, value in coordinator.data[camera]["supportExt"].items() + if capability == NUMBER_TYPE.supported_ext if value in NUMBER_TYPE.supported_ext_value ) diff --git a/homeassistant/components/ezviz/select.py b/homeassistant/components/ezviz/select.py index 8110cf61a5c..d6dc3dc8550 100644 --- a/homeassistant/components/ezviz/select.py +++ b/homeassistant/components/ezviz/select.py @@ -1,4 +1,5 @@ """Support for EZVIZ select controls.""" + from __future__ import annotations from dataclasses import dataclass @@ -20,24 +21,16 @@ from .entity import EzvizEntity PARALLEL_UPDATES = 1 -@dataclass(frozen=True) -class EzvizSelectEntityDescriptionMixin: - """Mixin values for EZVIZ Select entities.""" +@dataclass(frozen=True, kw_only=True) +class EzvizSelectEntityDescription(SelectEntityDescription): + """Describe a EZVIZ Select entity.""" supported_switch: int -@dataclass(frozen=True) -class EzvizSelectEntityDescription( - SelectEntityDescription, EzvizSelectEntityDescriptionMixin -): - """Describe a EZVIZ Select entity.""" - - SELECT_TYPE = EzvizSelectEntityDescription( key="alarm_sound_mod", translation_key="alarm_sound_mode", - icon="mdi:alarm", entity_category=EntityCategory.CONFIG, options=["soft", "intensive", "silent"], supported_switch=DeviceSwitchType.ALARM_TONE.value, diff --git a/homeassistant/components/ezviz/sensor.py b/homeassistant/components/ezviz/sensor.py index aecf25c2c78..e0750b985fc 100644 --- a/homeassistant/components/ezviz/sensor.py +++ b/homeassistant/components/ezviz/sensor.py @@ -1,4 +1,5 @@ """Support for EZVIZ sensors.""" + from __future__ import annotations from homeassistant.components.sensor import ( diff --git a/homeassistant/components/ezviz/siren.py b/homeassistant/components/ezviz/siren.py index 1f08b389236..8bacceff29f 100644 --- a/homeassistant/components/ezviz/siren.py +++ b/homeassistant/components/ezviz/siren.py @@ -1,4 +1,5 @@ """Support for EZVIZ sirens.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/ezviz/switch.py b/homeassistant/components/ezviz/switch.py index f6d19afae0c..65fb7b9f36b 100644 --- a/homeassistant/components/ezviz/switch.py +++ b/homeassistant/components/ezviz/switch.py @@ -1,4 +1,5 @@ """Support for EZVIZ Switch sensors.""" + from __future__ import annotations from dataclasses import dataclass @@ -22,20 +23,13 @@ from .coordinator import EzvizDataUpdateCoordinator from .entity import EzvizEntity -@dataclass(frozen=True) -class EzvizSwitchEntityDescriptionMixin: - """Mixin values for EZVIZ Switch entities.""" +@dataclass(frozen=True, kw_only=True) +class EzvizSwitchEntityDescription(SwitchEntityDescription): + """Describe a EZVIZ switch.""" supported_ext: str | None -@dataclass(frozen=True) -class EzvizSwitchEntityDescription( - SwitchEntityDescription, EzvizSwitchEntityDescriptionMixin -): - """Describe a EZVIZ switch.""" - - SWITCH_TYPES: dict[int, EzvizSwitchEntityDescription] = { 3: EzvizSwitchEntityDescription( key="3", diff --git a/homeassistant/components/ezviz/update.py b/homeassistant/components/ezviz/update.py index 003397d8dda..05735d152cf 100644 --- a/homeassistant/components/ezviz/update.py +++ b/homeassistant/components/ezviz/update.py @@ -1,4 +1,5 @@ """Support for EZVIZ sensors.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/faa_delays/__init__.py b/homeassistant/components/faa_delays/__init__.py index 3606da33499..750b1f4a833 100644 --- a/homeassistant/components/faa_delays/__init__.py +++ b/homeassistant/components/faa_delays/__init__.py @@ -1,4 +1,5 @@ """The FAA Delays integration.""" + from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ID, Platform from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/faa_delays/binary_sensor.py b/homeassistant/components/faa_delays/binary_sensor.py index 5cc23b5d73c..6a01bf6ebed 100644 --- a/homeassistant/components/faa_delays/binary_sensor.py +++ b/homeassistant/components/faa_delays/binary_sensor.py @@ -1,4 +1,5 @@ """Platform for FAA Delays sensor component.""" + from __future__ import annotations from collections.abc import Callable, Mapping diff --git a/homeassistant/components/faa_delays/config_flow.py b/homeassistant/components/faa_delays/config_flow.py index 2f91ce9f797..5a42c9f7602 100644 --- a/homeassistant/components/faa_delays/config_flow.py +++ b/homeassistant/components/faa_delays/config_flow.py @@ -1,4 +1,5 @@ """Config flow for FAA Delays integration.""" + import logging from typing import Any @@ -6,9 +7,8 @@ from aiohttp import ClientConnectionError import faadelays import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_ID -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import aiohttp_client from .const import DOMAIN @@ -18,14 +18,14 @@ _LOGGER = logging.getLogger(__name__) DATA_SCHEMA = vol.Schema({vol.Required(CONF_ID): str}) -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class FAADelaysConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for FAA Delays.""" VERSION = 1 async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors = {} if user_input is not None: diff --git a/homeassistant/components/faa_delays/const.py b/homeassistant/components/faa_delays/const.py index 3b9bda33bfb..b91b4536267 100644 --- a/homeassistant/components/faa_delays/const.py +++ b/homeassistant/components/faa_delays/const.py @@ -1,4 +1,5 @@ """Constants for the FAA Delays integration.""" + from __future__ import annotations DOMAIN = "faa_delays" diff --git a/homeassistant/components/faa_delays/coordinator.py b/homeassistant/components/faa_delays/coordinator.py index 2f110cf7730..9de10b2ebbb 100644 --- a/homeassistant/components/faa_delays/coordinator.py +++ b/homeassistant/components/faa_delays/coordinator.py @@ -1,4 +1,5 @@ """DataUpdateCoordinator for faa_delays integration.""" + import asyncio from datetime import timedelta import logging diff --git a/homeassistant/components/facebook/notify.py b/homeassistant/components/facebook/notify.py index 2600bbd2d9c..38ed78d125b 100644 --- a/homeassistant/components/facebook/notify.py +++ b/homeassistant/components/facebook/notify.py @@ -1,4 +1,5 @@ """Facebook platform for notify component.""" + from __future__ import annotations from http import HTTPStatus diff --git a/homeassistant/components/fail2ban/sensor.py b/homeassistant/components/fail2ban/sensor.py index 22b4bfe6ea1..53490e60c54 100644 --- a/homeassistant/components/fail2ban/sensor.py +++ b/homeassistant/components/fail2ban/sensor.py @@ -1,4 +1,5 @@ """Support for displaying IPs banned by fail2ban.""" + from __future__ import annotations from datetime import timedelta @@ -46,12 +47,9 @@ async def async_setup_platform( jails = config[CONF_JAILS] log_file = config.get(CONF_FILE_PATH, DEFAULT_LOG) - device_list = [] log_parser = BanLogParser(log_file) - for jail in jails: - device_list.append(BanSensor(name, jail, log_parser)) - async_add_entities(device_list, True) + async_add_entities((BanSensor(name, jail, log_parser) for jail in jails), True) class BanSensor(SensorEntity): diff --git a/homeassistant/components/familyhub/camera.py b/homeassistant/components/familyhub/camera.py index 324341dc8cf..da6f82cf56b 100644 --- a/homeassistant/components/familyhub/camera.py +++ b/homeassistant/components/familyhub/camera.py @@ -1,4 +1,5 @@ """Family Hub camera for Samsung Refrigerators.""" + from __future__ import annotations from pyfamilyhublocal import FamilyHubCam diff --git a/homeassistant/components/fan/__init__.py b/homeassistant/components/fan/__init__.py index c35d828e398..b62f207bde8 100644 --- a/homeassistant/components/fan/__init__.py +++ b/homeassistant/components/fan/__init__.py @@ -1,4 +1,5 @@ """Provides functionality to interact with fans.""" + from __future__ import annotations from datetime import timedelta @@ -237,7 +238,7 @@ class FanEntity(ToggleEntity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): def set_percentage(self, percentage: int) -> None: """Set the speed of the fan, as a percentage.""" - raise NotImplementedError() + raise NotImplementedError async def async_set_percentage(self, percentage: int) -> None: """Set the speed of the fan, as a percentage.""" @@ -276,7 +277,7 @@ class FanEntity(ToggleEntity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): def set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode.""" - raise NotImplementedError() + raise NotImplementedError @final async def async_handle_set_preset_mode_service(self, preset_mode: str) -> None: @@ -296,8 +297,6 @@ class FanEntity(ToggleEntity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): if not preset_modes or preset_mode not in preset_modes: preset_modes_str: str = ", ".join(preset_modes or []) raise NotValidPresetModeError( - f"The preset_mode {preset_mode} is not a valid preset_mode:" - f" {preset_modes}", translation_placeholders={ "preset_mode": preset_mode, "preset_modes": preset_modes_str, @@ -306,7 +305,7 @@ class FanEntity(ToggleEntity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): def set_direction(self, direction: str) -> None: """Set the direction of the fan.""" - raise NotImplementedError() + raise NotImplementedError async def async_set_direction(self, direction: str) -> None: """Set the direction of the fan.""" @@ -319,7 +318,7 @@ class FanEntity(ToggleEntity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): **kwargs: Any, ) -> None: """Turn on the fan.""" - raise NotImplementedError() + raise NotImplementedError @final async def async_handle_turn_on_service( @@ -351,7 +350,7 @@ class FanEntity(ToggleEntity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): def oscillate(self, oscillating: bool) -> None: """Oscillate the fan.""" - raise NotImplementedError() + raise NotImplementedError async def async_oscillate(self, oscillating: bool) -> None: """Oscillate the fan.""" diff --git a/homeassistant/components/fan/device_action.py b/homeassistant/components/fan/device_action.py index fc7f1ddce1f..b4164f1d1a6 100644 --- a/homeassistant/components/fan/device_action.py +++ b/homeassistant/components/fan/device_action.py @@ -1,4 +1,5 @@ """Provides device automations for Fan.""" + from __future__ import annotations import voluptuous as vol diff --git a/homeassistant/components/fan/device_condition.py b/homeassistant/components/fan/device_condition.py index 920f970185b..39f77b7a128 100644 --- a/homeassistant/components/fan/device_condition.py +++ b/homeassistant/components/fan/device_condition.py @@ -1,4 +1,5 @@ """Provide the device automations for Fan.""" + from __future__ import annotations import voluptuous as vol diff --git a/homeassistant/components/fan/device_trigger.py b/homeassistant/components/fan/device_trigger.py index cc10e9cbeca..8e1c518d7c7 100644 --- a/homeassistant/components/fan/device_trigger.py +++ b/homeassistant/components/fan/device_trigger.py @@ -1,4 +1,5 @@ """Provides device automations for Fan.""" + from __future__ import annotations import voluptuous as vol diff --git a/homeassistant/components/fan/group.py b/homeassistant/components/fan/group.py deleted file mode 100644 index 234883ffd5a..00000000000 --- a/homeassistant/components/fan/group.py +++ /dev/null @@ -1,14 +0,0 @@ -"""Describe group states.""" - - -from homeassistant.components.group import GroupIntegrationRegistry -from homeassistant.const import STATE_OFF, STATE_ON -from homeassistant.core import HomeAssistant, callback - - -@callback -def async_describe_on_off_states( - hass: HomeAssistant, registry: GroupIntegrationRegistry -) -> None: - """Describe group on off states.""" - registry.on_off_states({STATE_ON}, STATE_OFF) diff --git a/homeassistant/components/fan/icons.json b/homeassistant/components/fan/icons.json index f962d1e7c1a..60edbce5f01 100644 --- a/homeassistant/components/fan/icons.json +++ b/homeassistant/components/fan/icons.json @@ -23,6 +23,7 @@ "decrease_speed": "mdi:fan-minus", "increase_speed": "mdi:fan-plus", "oscillate": "mdi:arrow-oscillating", + "set_direction": "mdi:rotate-3d-variant", "set_percentage": "mdi:fan", "set_preset_mode": "mdi:fan-auto", "toggle": "mdi:fan", diff --git a/homeassistant/components/fan/reproduce_state.py b/homeassistant/components/fan/reproduce_state.py index a12b23cb16d..391059a369c 100644 --- a/homeassistant/components/fan/reproduce_state.py +++ b/homeassistant/components/fan/reproduce_state.py @@ -1,4 +1,5 @@ """Reproduce an Fan state.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/fan/significant_change.py b/homeassistant/components/fan/significant_change.py index b8038b93f79..d3d346d5f66 100644 --- a/homeassistant/components/fan/significant_change.py +++ b/homeassistant/components/fan/significant_change.py @@ -1,4 +1,5 @@ """Helper to test significant Fan state changes.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/fastdotcom/__init__.py b/homeassistant/components/fastdotcom/__init__.py index ada717a6dac..12bd355b82b 100644 --- a/homeassistant/components/fastdotcom/__init__.py +++ b/homeassistant/components/fastdotcom/__init__.py @@ -1,4 +1,5 @@ """Support for testing internet speed via Fast.com.""" + from __future__ import annotations import logging @@ -13,7 +14,7 @@ from homeassistant.helpers.start import async_at_started from homeassistant.helpers.typing import ConfigType from .const import CONF_MANUAL, DEFAULT_INTERVAL, DOMAIN, PLATFORMS -from .coordinator import FastdotcomDataUpdateCoordindator +from .coordinator import FastdotcomDataUpdateCoordinator from .services import async_setup_services _LOGGER = logging.getLogger(__name__) @@ -49,7 +50,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Fast.com from a config entry.""" - coordinator = FastdotcomDataUpdateCoordindator(hass) + coordinator = FastdotcomDataUpdateCoordinator(hass) hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator await hass.config_entries.async_forward_entry_setups( diff --git a/homeassistant/components/fastdotcom/config_flow.py b/homeassistant/components/fastdotcom/config_flow.py index 5ca35fd6802..ec62c86d787 100644 --- a/homeassistant/components/fastdotcom/config_flow.py +++ b/homeassistant/components/fastdotcom/config_flow.py @@ -1,11 +1,11 @@ """Config flow for Fast.com integration.""" + from __future__ import annotations from typing import Any -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from .const import DEFAULT_NAME, DOMAIN @@ -18,7 +18,7 @@ class FastdotcomConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") @@ -30,7 +30,7 @@ class FastdotcomConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_import( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initiated by configuration file.""" async_create_issue( self.hass, diff --git a/homeassistant/components/fastdotcom/const.py b/homeassistant/components/fastdotcom/const.py index 340be6f50ae..8cae797e052 100644 --- a/homeassistant/components/fastdotcom/const.py +++ b/homeassistant/components/fastdotcom/const.py @@ -1,4 +1,5 @@ """Constants for the Fast.com integration.""" + import logging from homeassistant.const import Platform diff --git a/homeassistant/components/fastdotcom/coordinator.py b/homeassistant/components/fastdotcom/coordinator.py index 692a85d2eda..75ac55b8314 100644 --- a/homeassistant/components/fastdotcom/coordinator.py +++ b/homeassistant/components/fastdotcom/coordinator.py @@ -1,4 +1,5 @@ """DataUpdateCoordinator for the Fast.com integration.""" + from __future__ import annotations from datetime import timedelta @@ -11,7 +12,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda from .const import DEFAULT_INTERVAL, DOMAIN, LOGGER -class FastdotcomDataUpdateCoordindator(DataUpdateCoordinator[float]): +class FastdotcomDataUpdateCoordinator(DataUpdateCoordinator[float]): """Class to manage fetching Fast.com data API.""" def __init__(self, hass: HomeAssistant) -> None: diff --git a/homeassistant/components/fastdotcom/diagnostics.py b/homeassistant/components/fastdotcom/diagnostics.py new file mode 100644 index 00000000000..d7383ef0c6a --- /dev/null +++ b/homeassistant/components/fastdotcom/diagnostics.py @@ -0,0 +1,24 @@ +"""Diagnostics support for Fast.com.""" + +from __future__ import annotations + +from typing import Any + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from .const import DOMAIN +from .coordinator import FastdotcomDataUpdateCoordinator + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, config_entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for the config entry.""" + coordinator: FastdotcomDataUpdateCoordinator = hass.data[DOMAIN][ + config_entry.entry_id + ] + + return { + "coordinator_data": coordinator.data, + } diff --git a/homeassistant/components/fastdotcom/sensor.py b/homeassistant/components/fastdotcom/sensor.py index a213898562b..721290e8c0d 100644 --- a/homeassistant/components/fastdotcom/sensor.py +++ b/homeassistant/components/fastdotcom/sensor.py @@ -1,4 +1,5 @@ """Support for Fast.com internet speed testing sensor.""" + from __future__ import annotations from homeassistant.components.sensor import ( @@ -14,7 +15,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN -from .coordinator import FastdotcomDataUpdateCoordindator +from .coordinator import FastdotcomDataUpdateCoordinator async def async_setup_entry( @@ -23,13 +24,11 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Fast.com sensor.""" - coordinator: FastdotcomDataUpdateCoordindator = hass.data[DOMAIN][entry.entry_id] + coordinator: FastdotcomDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] async_add_entities([SpeedtestSensor(entry.entry_id, coordinator)]) -class SpeedtestSensor( - CoordinatorEntity[FastdotcomDataUpdateCoordindator], SensorEntity -): +class SpeedtestSensor(CoordinatorEntity[FastdotcomDataUpdateCoordinator], SensorEntity): """Implementation of a Fast.com sensor.""" _attr_translation_key = "download" @@ -40,7 +39,7 @@ class SpeedtestSensor( _attr_has_entity_name = True def __init__( - self, entry_id: str, coordinator: FastdotcomDataUpdateCoordindator + self, entry_id: str, coordinator: FastdotcomDataUpdateCoordinator ) -> None: """Initialize the sensor.""" super().__init__(coordinator) diff --git a/homeassistant/components/fastdotcom/services.py b/homeassistant/components/fastdotcom/services.py index d1a9ee2125b..5939a667342 100644 --- a/homeassistant/components/fastdotcom/services.py +++ b/homeassistant/components/fastdotcom/services.py @@ -1,4 +1,5 @@ """Services for the Fastdotcom integration.""" + from __future__ import annotations from homeassistant.config_entries import ConfigEntryState @@ -7,14 +8,14 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import issue_registry as ir from .const import DOMAIN, SERVICE_NAME -from .coordinator import FastdotcomDataUpdateCoordindator +from .coordinator import FastdotcomDataUpdateCoordinator def async_setup_services(hass: HomeAssistant) -> None: """Set up the service for the Fastdotcom integration.""" @callback - def collect_coordinator() -> FastdotcomDataUpdateCoordindator: + def collect_coordinator() -> FastdotcomDataUpdateCoordinator: """Collect the coordinator Fastdotcom.""" config_entries = hass.config_entries.async_entries(DOMAIN) if not config_entries: @@ -23,7 +24,7 @@ def async_setup_services(hass: HomeAssistant) -> None: for config_entry in config_entries: if config_entry.state != ConfigEntryState.LOADED: raise HomeAssistantError(f"{config_entry.title} is not loaded") - coordinator: FastdotcomDataUpdateCoordindator = hass.data[DOMAIN][ + coordinator: FastdotcomDataUpdateCoordinator = hass.data[DOMAIN][ config_entry.entry_id ] break diff --git a/homeassistant/components/feedreader/__init__.py b/homeassistant/components/feedreader/__init__.py index 04511a1a986..0a16e986d0b 100644 --- a/homeassistant/components/feedreader/__init__.py +++ b/homeassistant/components/feedreader/__init__.py @@ -1,4 +1,5 @@ """Support for RSS/Atom feeds.""" + from __future__ import annotations from calendar import timegm diff --git a/homeassistant/components/ffmpeg/__init__.py b/homeassistant/components/ffmpeg/__init__.py index 4ab4ee32a09..2045b6bb06b 100644 --- a/homeassistant/components/ffmpeg/__init__.py +++ b/homeassistant/components/ffmpeg/__init__.py @@ -1,9 +1,10 @@ """Support for FFmpeg.""" + from __future__ import annotations import asyncio import re -from typing import Generic, TypeVar +from typing import TYPE_CHECKING, Generic, TypeVar from haffmpeg.core import HAFFmpeg from haffmpeg.tools import IMAGE_JPEG, FFVersion, ImageFrame @@ -23,9 +24,16 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_send, ) from homeassistant.helpers.entity import Entity +from homeassistant.helpers.system_info import is_official_image from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass +if TYPE_CHECKING: + from functools import cached_property +else: + from homeassistant.backports.functools import cached_property + + _HAFFmpegT = TypeVar("_HAFFmpegT", bound=HAFFmpeg) DOMAIN = "ffmpeg" @@ -48,6 +56,12 @@ CONF_OUTPUT = "output" DEFAULT_BINARY = "ffmpeg" +# Currently we only care if the version is < 3 +# because we use a different content-type +# It is only important to update this version if the +# content-type changes again in the future +OFFICIAL_IMAGE_VERSION = "6.0" + CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.Schema( @@ -141,26 +155,28 @@ class FFmpegManager: self._version: str | None = None self._major_version: int | None = None - @property + @cached_property def binary(self) -> str: """Return ffmpeg binary from config.""" return self._bin async def async_get_version(self) -> tuple[str | None, int | None]: """Return ffmpeg version.""" - - ffversion = FFVersion(self._bin) - self._version = await ffversion.get_version() - - self._major_version = None - if self._version is not None: - result = re.search(r"(\d+)\.", self._version) - if result is not None: - self._major_version = int(result.group(1)) + if self._version is None: + if is_official_image(): + self._version = OFFICIAL_IMAGE_VERSION + self._major_version = int(self._version.split(".")[0]) + elif ( + (version := await FFVersion(self._bin).get_version()) + and (result := re.search(r"(\d+)\.", version)) + and (major_version := int(result.group(1))) + ): + self._version = version + self._major_version = major_version return self._version, self._major_version - @property + @cached_property def ffmpeg_stream_content_type(self) -> str: """Return HTTP content type for ffmpeg stream.""" if self._major_version is not None and self._major_version > 3: @@ -213,7 +229,7 @@ class FFmpegBase(Entity, Generic[_HAFFmpegT]): This method is a coroutine. """ - raise NotImplementedError() + raise NotImplementedError async def _async_stop_ffmpeg(self, entity_ids: list[str] | None) -> None: """Stop a FFmpeg process. diff --git a/homeassistant/components/ffmpeg/camera.py b/homeassistant/components/ffmpeg/camera.py index 884629c8ae6..c0ce4ad9746 100644 --- a/homeassistant/components/ffmpeg/camera.py +++ b/homeassistant/components/ffmpeg/camera.py @@ -1,4 +1,5 @@ """Support for Cameras with FFmpeg as decoder.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/ffmpeg/icons.json b/homeassistant/components/ffmpeg/icons.json new file mode 100644 index 00000000000..a23f024599c --- /dev/null +++ b/homeassistant/components/ffmpeg/icons.json @@ -0,0 +1,7 @@ +{ + "services": { + "restart": "mdi:restart", + "start": "mdi:play", + "stop": "mdi:stop" + } +} diff --git a/homeassistant/components/ffmpeg/manifest.json b/homeassistant/components/ffmpeg/manifest.json index 65cafc195de..8cd7b1f504d 100644 --- a/homeassistant/components/ffmpeg/manifest.json +++ b/homeassistant/components/ffmpeg/manifest.json @@ -3,5 +3,5 @@ "name": "FFmpeg", "codeowners": [], "documentation": "https://www.home-assistant.io/integrations/ffmpeg", - "requirements": ["ha-ffmpeg==3.1.0"] + "requirements": ["ha-ffmpeg==3.2.0"] } diff --git a/homeassistant/components/ffmpeg_motion/binary_sensor.py b/homeassistant/components/ffmpeg_motion/binary_sensor.py index b982d944c6a..d5030d4530e 100644 --- a/homeassistant/components/ffmpeg_motion/binary_sensor.py +++ b/homeassistant/components/ffmpeg_motion/binary_sensor.py @@ -1,4 +1,5 @@ """Provides a binary sensor which is a collection of ffmpeg tools.""" + from __future__ import annotations from typing import Any, TypeVar diff --git a/homeassistant/components/ffmpeg_noise/binary_sensor.py b/homeassistant/components/ffmpeg_noise/binary_sensor.py index a802868334d..a434b4a9924 100644 --- a/homeassistant/components/ffmpeg_noise/binary_sensor.py +++ b/homeassistant/components/ffmpeg_noise/binary_sensor.py @@ -1,4 +1,5 @@ """Provides a binary sensor which is a collection of ffmpeg tools.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/fibaro/__init__.py b/homeassistant/components/fibaro/__init__.py index 159ba62bd24..2c1405130b4 100644 --- a/homeassistant/components/fibaro/__init__.py +++ b/homeassistant/components/fibaro/__init__.py @@ -1,4 +1,5 @@ """Support for the Fibaro devices.""" + from __future__ import annotations from collections import defaultdict diff --git a/homeassistant/components/fibaro/binary_sensor.py b/homeassistant/components/fibaro/binary_sensor.py index 07c0d9a779c..c0980025555 100644 --- a/homeassistant/components/fibaro/binary_sensor.py +++ b/homeassistant/components/fibaro/binary_sensor.py @@ -1,4 +1,5 @@ """Support for Fibaro binary sensors.""" + from __future__ import annotations from collections.abc import Mapping diff --git a/homeassistant/components/fibaro/climate.py b/homeassistant/components/fibaro/climate.py index cb64acdea14..cf08d52d36e 100644 --- a/homeassistant/components/fibaro/climate.py +++ b/homeassistant/components/fibaro/climate.py @@ -1,4 +1,5 @@ """Support for Fibaro thermostats.""" + from __future__ import annotations from contextlib import suppress diff --git a/homeassistant/components/fibaro/config_flow.py b/homeassistant/components/fibaro/config_flow.py index 0a06f88c5af..8c2fb502488 100644 --- a/homeassistant/components/fibaro/config_flow.py +++ b/homeassistant/components/fibaro/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Fibaro integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -8,10 +9,9 @@ from typing import Any from slugify import slugify import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResult from . import FibaroAuthFailed, FibaroConnectFailed, FibaroController from .const import CONF_IMPORT_PLUGINS, DOMAIN @@ -65,18 +65,18 @@ def _normalize_url(url: str) -> str: return url -class FibaroConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class FibaroConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Fibaro.""" VERSION = 1 def __init__(self) -> None: """Initialize.""" - self._reauth_entry: config_entries.ConfigEntry | None = None + self._reauth_entry: ConfigEntry | None = None async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors = {} @@ -97,7 +97,9 @@ class FibaroConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors ) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Handle reauthentication.""" self._reauth_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] @@ -106,7 +108,7 @@ class FibaroConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initiated by reauthentication.""" errors = {} diff --git a/homeassistant/components/fibaro/cover.py b/homeassistant/components/fibaro/cover.py index d353b352c5c..16be6e98ae1 100644 --- a/homeassistant/components/fibaro/cover.py +++ b/homeassistant/components/fibaro/cover.py @@ -1,4 +1,5 @@ """Support for Fibaro cover - curtains, rollershutters etc.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/fibaro/event.py b/homeassistant/components/fibaro/event.py index 020a478db95..c65e8f143c6 100644 --- a/homeassistant/components/fibaro/event.py +++ b/homeassistant/components/fibaro/event.py @@ -1,4 +1,5 @@ """Support for Fibaro event entities.""" + from __future__ import annotations from pyfibaro.fibaro_device import DeviceModel, SceneEvent @@ -26,13 +27,15 @@ async def async_setup_entry( """Set up the Fibaro event entities.""" controller: FibaroController = hass.data[DOMAIN][entry.entry_id] - entities = [] - for device in controller.fibaro_devices[Platform.EVENT]: - for scene_event in device.central_scene_event: - # Each scene event represents a button on a device - entities.append(FibaroEventEntity(device, scene_event)) - - async_add_entities(entities, True) + # Each scene event represents a button on a device + async_add_entities( + ( + FibaroEventEntity(device, scene_event) + for device in controller.fibaro_devices[Platform.EVENT] + for scene_event in device.central_scene_event + ), + True, + ) class FibaroEventEntity(FibaroDevice, EventEntity): diff --git a/homeassistant/components/fibaro/light.py b/homeassistant/components/fibaro/light.py index 17de9a6636a..2f2182c53cd 100644 --- a/homeassistant/components/fibaro/light.py +++ b/homeassistant/components/fibaro/light.py @@ -1,4 +1,5 @@ """Support for Fibaro lights.""" + from __future__ import annotations from contextlib import suppress diff --git a/homeassistant/components/fibaro/lock.py b/homeassistant/components/fibaro/lock.py index 715116d2843..271e3981b71 100644 --- a/homeassistant/components/fibaro/lock.py +++ b/homeassistant/components/fibaro/lock.py @@ -1,4 +1,5 @@ """Support for Fibaro locks.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/fibaro/scene.py b/homeassistant/components/fibaro/scene.py index 7ae8bff151f..a40a1ef5b57 100644 --- a/homeassistant/components/fibaro/scene.py +++ b/homeassistant/components/fibaro/scene.py @@ -1,4 +1,5 @@ """Support for Fibaro scenes.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/fibaro/sensor.py b/homeassistant/components/fibaro/sensor.py index e859a9b1afb..6e672e9cc97 100644 --- a/homeassistant/components/fibaro/sensor.py +++ b/homeassistant/components/fibaro/sensor.py @@ -1,4 +1,5 @@ """Support for Fibaro sensors.""" + from __future__ import annotations from contextlib import suppress @@ -105,29 +106,27 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Fibaro controller devices.""" - entities: list[SensorEntity] = [] controller: FibaroController = hass.data[DOMAIN][entry.entry_id] + entities: list[SensorEntity] = [ + FibaroSensor(device, MAIN_SENSOR_TYPES.get(device.type)) + for device in controller.fibaro_devices[Platform.SENSOR] + ] - for device in controller.fibaro_devices[Platform.SENSOR]: - entity_description = MAIN_SENSOR_TYPES.get(device.type) - - # main sensors are created even if the entity type is not known - entities.append(FibaroSensor(device, entity_description)) - - for platform in ( - Platform.BINARY_SENSOR, - Platform.CLIMATE, - Platform.COVER, - Platform.LIGHT, - Platform.LOCK, - Platform.SENSOR, - Platform.SWITCH, - ): - for device in controller.fibaro_devices[platform]: - for entity_description in ADDITIONAL_SENSOR_TYPES: - if entity_description.key in device.properties: - entities.append(FibaroAdditionalSensor(device, entity_description)) + entities.extend( + FibaroAdditionalSensor(device, entity_description) + for platform in ( + Platform.BINARY_SENSOR, + Platform.CLIMATE, + Platform.COVER, + Platform.LIGHT, + Platform.LOCK, + Platform.SWITCH, + ) + for device in controller.fibaro_devices[platform] + for entity_description in ADDITIONAL_SENSOR_TYPES + if entity_description.key in device.properties + ) async_add_entities(entities, True) diff --git a/homeassistant/components/fibaro/switch.py b/homeassistant/components/fibaro/switch.py index fdd473ea282..f6ceed972f7 100644 --- a/homeassistant/components/fibaro/switch.py +++ b/homeassistant/components/fibaro/switch.py @@ -1,4 +1,5 @@ """Support for Fibaro switches.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/fido/sensor.py b/homeassistant/components/fido/sensor.py index b7942056a2c..d2169ae32e8 100644 --- a/homeassistant/components/fido/sensor.py +++ b/homeassistant/components/fido/sensor.py @@ -3,6 +3,7 @@ Get data from 'Usage Summary' page: https://www.fido.ca/pages/#/my-account/wireless """ + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/file/notify.py b/homeassistant/components/file/notify.py index ca0deb89c7b..50e6cec09a8 100644 --- a/homeassistant/components/file/notify.py +++ b/homeassistant/components/file/notify.py @@ -1,4 +1,5 @@ """Support for file notification.""" + from __future__ import annotations import os diff --git a/homeassistant/components/file/sensor.py b/homeassistant/components/file/sensor.py index 82eb5880b79..f70b0bce701 100644 --- a/homeassistant/components/file/sensor.py +++ b/homeassistant/components/file/sensor.py @@ -1,4 +1,5 @@ """Support for sensor value(s) stored in local files.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/file_upload/__init__.py b/homeassistant/components/file_upload/__init__.py index 8c594f7f85c..60caf0ef7f3 100644 --- a/homeassistant/components/file_upload/__init__.py +++ b/homeassistant/components/file_upload/__init__.py @@ -1,4 +1,5 @@ """The File Upload integration.""" + from __future__ import annotations import asyncio @@ -6,14 +7,14 @@ from collections.abc import Iterator from contextlib import contextmanager from dataclasses import dataclass from pathlib import Path +from queue import SimpleQueue import shutil import tempfile from aiohttp import BodyPartReader, web -import janus import voluptuous as vol -from homeassistant.components.http import HomeAssistantView +from homeassistant.components.http import KEY_HASS, HomeAssistantView from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import Event, HomeAssistant, callback @@ -131,20 +132,21 @@ class FileUploadView(HomeAssistantView): reader = await request.multipart() file_field_reader = await reader.next() + filename: str | None if ( not isinstance(file_field_reader, BodyPartReader) or file_field_reader.name != "file" - or file_field_reader.filename is None + or (filename := file_field_reader.filename) is None ): raise vol.Invalid("Expected a file") try: - raise_if_invalid_filename(file_field_reader.filename) + raise_if_invalid_filename(filename) except ValueError as err: raise web.HTTPBadRequest from err - hass: HomeAssistant = request.app["hass"] + hass = request.app[KEY_HASS] file_id = ulid_hex() if DOMAIN not in hass.data: @@ -152,55 +154,62 @@ class FileUploadView(HomeAssistantView): file_upload_data: FileUploadData = hass.data[DOMAIN] file_dir = file_upload_data.file_dir(file_id) - queue: janus.Queue[bytes | None] = janus.Queue() + queue: SimpleQueue[tuple[bytes, asyncio.Future[None] | None] | None] = ( + SimpleQueue() + ) - def _sync_queue_consumer( - sync_q: janus.SyncQueue[bytes | None], _file_name: str - ) -> None: + def _sync_queue_consumer() -> None: file_dir.mkdir() - with (file_dir / _file_name).open("wb") as file_handle: + with (file_dir / filename).open("wb") as file_handle: while True: - _chunk = sync_q.get() - if _chunk is None: + if (_chunk_future := queue.get()) is None: break - + _chunk, _future = _chunk_future + if _future is not None: + hass.loop.call_soon_threadsafe(_future.set_result, None) file_handle.write(_chunk) - sync_q.task_done() fut: asyncio.Future[None] | None = None try: - fut = hass.async_add_executor_job( - _sync_queue_consumer, - queue.sync_q, - file_field_reader.filename, - ) - + fut = hass.async_add_executor_job(_sync_queue_consumer) + megabytes_sending = 0 while chunk := await file_field_reader.read_chunk(ONE_MEGABYTE): - queue.async_q.put_nowait(chunk) - if queue.async_q.qsize() > 5: # Allow up to 5 MB buffer size - await queue.async_q.join() - queue.async_q.put_nowait(None) # terminate queue consumer + megabytes_sending += 1 + if megabytes_sending % 5 != 0: + queue.put_nowait((chunk, None)) + continue + + chunk_future = hass.loop.create_future() + queue.put_nowait((chunk, chunk_future)) + await asyncio.wait( + (fut, chunk_future), return_when=asyncio.FIRST_COMPLETED + ) + if fut.done(): + # The executor job failed + break + + queue.put_nowait(None) # terminate queue consumer finally: if fut is not None: await fut - file_upload_data.files[file_id] = file_field_reader.filename + file_upload_data.files[file_id] = filename return self.json({"file_id": file_id}) @RequestDataValidator({vol.Required("file_id"): str}) async def delete(self, request: web.Request, data: dict[str, str]) -> web.Response: """Delete a file.""" - hass: HomeAssistant = request.app["hass"] + hass = request.app[KEY_HASS] if DOMAIN not in hass.data: - raise web.HTTPNotFound() + raise web.HTTPNotFound file_id = data["file_id"] file_upload_data: FileUploadData = hass.data[DOMAIN] if file_upload_data.files.pop(file_id, None) is None: - raise web.HTTPNotFound() + raise web.HTTPNotFound await hass.async_add_executor_job( lambda: shutil.rmtree(file_upload_data.file_dir(file_id)) diff --git a/homeassistant/components/file_upload/manifest.json b/homeassistant/components/file_upload/manifest.json index 4b4af917bd9..7e5be2db980 100644 --- a/homeassistant/components/file_upload/manifest.json +++ b/homeassistant/components/file_upload/manifest.json @@ -5,6 +5,5 @@ "dependencies": ["http"], "documentation": "https://www.home-assistant.io/integrations/file_upload", "integration_type": "system", - "quality_scale": "internal", - "requirements": ["janus==1.0.0"] + "quality_scale": "internal" } diff --git a/homeassistant/components/filesize/__init__.py b/homeassistant/components/filesize/__init__.py index 9d7cc99421f..90d2af5d52a 100644 --- a/homeassistant/components/filesize/__init__.py +++ b/homeassistant/components/filesize/__init__.py @@ -1,35 +1,18 @@ """The filesize component.""" -from __future__ import annotations -import pathlib +from __future__ import annotations from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_FILE_PATH from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady from .const import PLATFORMS from .coordinator import FileSizeCoordinator -def _get_full_path(hass: HomeAssistant, path: str) -> str: - """Check if path is valid, allowed and return full path.""" - get_path = pathlib.Path(path) - if not get_path.exists() or not get_path.is_file(): - raise ConfigEntryNotReady(f"Can not access file {path}") - - if not hass.config.is_allowed_path(path): - raise ConfigEntryNotReady(f"Filepath {path} is not valid or allowed") - - return str(get_path.absolute()) - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up from a config entry.""" - full_path = await hass.async_add_executor_job( - _get_full_path, hass, entry.data[CONF_FILE_PATH] - ) - coordinator = FileSizeCoordinator(hass, full_path) + coordinator = FileSizeCoordinator(hass, entry.data[CONF_FILE_PATH]) await coordinator.async_config_entry_first_refresh() await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/homeassistant/components/filesize/config_flow.py b/homeassistant/components/filesize/config_flow.py index 8633e6ec466..51eff46bdb3 100644 --- a/homeassistant/components/filesize/config_flow.py +++ b/homeassistant/components/filesize/config_flow.py @@ -1,4 +1,5 @@ """The filesize config flow.""" + from __future__ import annotations import logging @@ -7,10 +8,9 @@ from typing import Any import voluptuous as vol -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_FILE_PATH from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError from .const import DOMAIN @@ -43,7 +43,7 @@ class FilesizeConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" errors: dict[str, Any] = {} diff --git a/homeassistant/components/filesize/coordinator.py b/homeassistant/components/filesize/coordinator.py index 75411f84975..2e59e922801 100644 --- a/homeassistant/components/filesize/coordinator.py +++ b/homeassistant/components/filesize/coordinator.py @@ -1,9 +1,11 @@ """Coordinator for monitoring the size of a file.""" + from __future__ import annotations from datetime import datetime, timedelta import logging import os +import pathlib from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -17,7 +19,7 @@ _LOGGER = logging.getLogger(__name__) class FileSizeCoordinator(DataUpdateCoordinator[dict[str, int | float | datetime]]): """Filesize coordinator.""" - def __init__(self, hass: HomeAssistant, path: str) -> None: + def __init__(self, hass: HomeAssistant, unresolved_path: str) -> None: """Initialize filesize coordinator.""" super().__init__( hass, @@ -26,15 +28,34 @@ class FileSizeCoordinator(DataUpdateCoordinator[dict[str, int | float | datetime update_interval=timedelta(seconds=60), always_update=False, ) - self._path = path + self._unresolved_path = unresolved_path + self._path: pathlib.Path | None = None - async def _async_update_data(self) -> dict[str, float | int | datetime]: + def _get_full_path(self) -> pathlib.Path: + """Check if path is valid, allowed and return full path.""" + path = self._unresolved_path + get_path = pathlib.Path(path) + if not self.hass.config.is_allowed_path(path): + raise UpdateFailed(f"Filepath {path} is not valid or allowed") + + if not get_path.exists() or not get_path.is_file(): + raise UpdateFailed(f"Can not access file {path}") + + return get_path.absolute() + + def _update(self) -> os.stat_result: """Fetch file information.""" + if not self._path: + self._path = self._get_full_path() + try: - statinfo = await self.hass.async_add_executor_job(os.stat, self._path) + return self._path.stat() except OSError as error: raise UpdateFailed(f"Can not retrieve file statistics {error}") from error + async def _async_update_data(self) -> dict[str, float | int | datetime]: + """Fetch file information.""" + statinfo = await self.hass.async_add_executor_job(self._update) size = statinfo.st_size last_updated = dt_util.utc_from_timestamp(statinfo.st_mtime) diff --git a/homeassistant/components/filesize/sensor.py b/homeassistant/components/filesize/sensor.py index 7d41989cfca..761513b1f48 100644 --- a/homeassistant/components/filesize/sensor.py +++ b/homeassistant/components/filesize/sensor.py @@ -1,4 +1,5 @@ """Sensor for monitoring the size of a file.""" + from __future__ import annotations from datetime import datetime diff --git a/homeassistant/components/filter/icons.json b/homeassistant/components/filter/icons.json new file mode 100644 index 00000000000..a03163179cb --- /dev/null +++ b/homeassistant/components/filter/icons.json @@ -0,0 +1,5 @@ +{ + "services": { + "reload": "mdi:reload" + } +} diff --git a/homeassistant/components/filter/sensor.py b/homeassistant/components/filter/sensor.py index c240d04ec1a..5ae300f6ec4 100644 --- a/homeassistant/components/filter/sensor.py +++ b/homeassistant/components/filter/sensor.py @@ -1,4 +1,5 @@ """Allows the creation of a sensor that filters state property.""" + from __future__ import annotations from collections import Counter, deque @@ -34,7 +35,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.core import HomeAssistant, State, callback +from homeassistant.core import Event, HomeAssistant, State, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import ( @@ -43,12 +44,7 @@ from homeassistant.helpers.event import ( ) from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.start import async_at_started -from homeassistant.helpers.typing import ( - ConfigType, - DiscoveryInfoType, - EventType, - StateType, -) +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType from homeassistant.util.decorator import Registry import homeassistant.util.dt as dt_util @@ -226,7 +222,7 @@ class SensorFilter(SensorEntity): @callback def _update_filter_sensor_state_event( - self, event: EventType[EventStateChangedData] + self, event: Event[EventStateChangedData] ) -> None: """Handle device state changes.""" _LOGGER.debug("Update filter on event: %s", event) @@ -473,7 +469,7 @@ class Filter: def _filter_state(self, new_state: FilterState) -> FilterState: """Implement filter.""" - raise NotImplementedError() + raise NotImplementedError def filter_state(self, new_state: _State) -> _State: """Implement a common interface for filters.""" diff --git a/homeassistant/components/fints/sensor.py b/homeassistant/components/fints/sensor.py index c969adfe637..6d3f1f63b84 100644 --- a/homeassistant/components/fints/sensor.py +++ b/homeassistant/components/fints/sensor.py @@ -1,4 +1,5 @@ """Read the balance of your bank accounts via FinTS.""" + from __future__ import annotations from collections import namedtuple diff --git a/homeassistant/components/fireservicerota/__init__.py b/homeassistant/components/fireservicerota/__init__.py index cb7a18dfcac..c3ee594e47d 100644 --- a/homeassistant/components/fireservicerota/__init__.py +++ b/homeassistant/components/fireservicerota/__init__.py @@ -1,4 +1,5 @@ """The FireServiceRota integration.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/fireservicerota/binary_sensor.py b/homeassistant/components/fireservicerota/binary_sensor.py index d2f4e2e11f2..9938f6ab096 100644 --- a/homeassistant/components/fireservicerota/binary_sensor.py +++ b/homeassistant/components/fireservicerota/binary_sensor.py @@ -1,4 +1,5 @@ """Binary Sensor platform for FireServiceRota integration.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/fireservicerota/config_flow.py b/homeassistant/components/fireservicerota/config_flow.py index d4d2b0763d9..afaef17c5a6 100644 --- a/homeassistant/components/fireservicerota/config_flow.py +++ b/homeassistant/components/fireservicerota/config_flow.py @@ -1,4 +1,5 @@ """Config flow for FireServiceRota.""" + from __future__ import annotations from collections.abc import Mapping @@ -7,9 +8,8 @@ from typing import Any from pyfireservicerota import FireServiceRota, InvalidAuthError import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_PASSWORD, CONF_TOKEN, CONF_URL, CONF_USERNAME -from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN, URL_LIST @@ -22,7 +22,7 @@ DATA_SCHEMA = vol.Schema( ) -class FireServiceRotaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class FireServiceRotaFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a FireServiceRota config flow.""" VERSION = 1 @@ -116,7 +116,9 @@ class FireServiceRotaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): description_placeholders=self._description_placeholders, ) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Initialise re-authentication.""" await self.async_set_unique_id(entry_data[CONF_USERNAME]) self._existing_entry = {**entry_data} @@ -125,7 +127,7 @@ class FireServiceRotaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_reauth_confirm( self, user_input: dict[str, str] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Get new tokens for a config entry that can't authenticate.""" if user_input is None: return self._show_setup_form(step_id="reauth_confirm") diff --git a/homeassistant/components/fireservicerota/sensor.py b/homeassistant/components/fireservicerota/sensor.py index 797e39e99cd..864838ddaff 100644 --- a/homeassistant/components/fireservicerota/sensor.py +++ b/homeassistant/components/fireservicerota/sensor.py @@ -1,4 +1,5 @@ """Sensor platform for FireServiceRota integration.""" + import logging from typing import Any diff --git a/homeassistant/components/fireservicerota/switch.py b/homeassistant/components/fireservicerota/switch.py index 7409b2e53b4..04e1e4ef5eb 100644 --- a/homeassistant/components/fireservicerota/switch.py +++ b/homeassistant/components/fireservicerota/switch.py @@ -1,4 +1,5 @@ """Switch platform for FireServiceRota integration.""" + import logging from typing import Any diff --git a/homeassistant/components/firmata/__init__.py b/homeassistant/components/firmata/__init__.py index 78b5592a54e..283fd585d35 100644 --- a/homeassistant/components/firmata/__init__.py +++ b/homeassistant/components/firmata/__init__.py @@ -1,4 +1,5 @@ """Support for Arduino-compatible Microcontrollers through Firmata.""" + import asyncio from copy import copy import logging diff --git a/homeassistant/components/firmata/binary_sensor.py b/homeassistant/components/firmata/binary_sensor.py index 0cb38d1df8b..c25a61ddac7 100644 --- a/homeassistant/components/firmata/binary_sensor.py +++ b/homeassistant/components/firmata/binary_sensor.py @@ -1,4 +1,5 @@ """Support for Firmata binary sensor input.""" + import logging from homeassistant.components.binary_sensor import BinarySensorEntity diff --git a/homeassistant/components/firmata/board.py b/homeassistant/components/firmata/board.py index 233388d5013..9573627e130 100644 --- a/homeassistant/components/firmata/board.py +++ b/homeassistant/components/firmata/board.py @@ -1,4 +1,5 @@ """Code to handle a Firmata board.""" + from __future__ import annotations from collections.abc import Mapping diff --git a/homeassistant/components/firmata/config_flow.py b/homeassistant/components/firmata/config_flow.py index f5b7cb5af40..571df351b25 100644 --- a/homeassistant/components/firmata/config_flow.py +++ b/homeassistant/components/firmata/config_flow.py @@ -5,9 +5,8 @@ from typing import Any from pymata_express.pymata_express_serial import serial -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_NAME -from homeassistant.data_entry_flow import FlowResult from .board import get_board from .const import CONF_SERIAL_PORT, DOMAIN @@ -15,12 +14,14 @@ from .const import CONF_SERIAL_PORT, DOMAIN _LOGGER = logging.getLogger(__name__) -class FirmataFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class FirmataFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a firmata config flow.""" VERSION = 1 - async def async_step_import(self, import_config: dict[str, Any]) -> FlowResult: + async def async_step_import( + self, import_config: dict[str, Any] + ) -> ConfigFlowResult: """Import a firmata board as a config entry. This flow is triggered by `async_setup` for configured boards. diff --git a/homeassistant/components/firmata/const.py b/homeassistant/components/firmata/const.py index da722b51897..541003ec5f4 100644 --- a/homeassistant/components/firmata/const.py +++ b/homeassistant/components/firmata/const.py @@ -1,4 +1,5 @@ """Constants for the Firmata component.""" + from typing import Final from homeassistant.const import ( diff --git a/homeassistant/components/firmata/entity.py b/homeassistant/components/firmata/entity.py index 51d2ad51866..60b7c3879ff 100644 --- a/homeassistant/components/firmata/entity.py +++ b/homeassistant/components/firmata/entity.py @@ -1,4 +1,5 @@ """Entity for Firmata devices.""" + from __future__ import annotations from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/firmata/light.py b/homeassistant/components/firmata/light.py index 29504f704bf..00453762c14 100644 --- a/homeassistant/components/firmata/light.py +++ b/homeassistant/components/firmata/light.py @@ -1,4 +1,5 @@ """Support for Firmata light output.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/firmata/pin.py b/homeassistant/components/firmata/pin.py index 190889914b3..c27152a8150 100644 --- a/homeassistant/components/firmata/pin.py +++ b/homeassistant/components/firmata/pin.py @@ -1,4 +1,5 @@ """Code to handle pins on a Firmata board.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/firmata/sensor.py b/homeassistant/components/firmata/sensor.py index 3497aec9e88..32559b9197d 100644 --- a/homeassistant/components/firmata/sensor.py +++ b/homeassistant/components/firmata/sensor.py @@ -1,4 +1,5 @@ """Support for Firmata sensor input.""" + import logging from homeassistant.components.sensor import SensorEntity diff --git a/homeassistant/components/firmata/switch.py b/homeassistant/components/firmata/switch.py index f52300b6db3..4203b221d4f 100644 --- a/homeassistant/components/firmata/switch.py +++ b/homeassistant/components/firmata/switch.py @@ -1,4 +1,5 @@ """Support for Firmata switch output.""" + import logging from typing import Any diff --git a/homeassistant/components/fitbit/__init__.py b/homeassistant/components/fitbit/__init__.py index 40ea9fb1152..22d0e302d63 100644 --- a/homeassistant/components/fitbit/__init__.py +++ b/homeassistant/components/fitbit/__init__.py @@ -1,6 +1,5 @@ """The fitbit component.""" - from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/fitbit/config_flow.py b/homeassistant/components/fitbit/config_flow.py index 7ef6ecbfa28..0ae1973b5fb 100644 --- a/homeassistant/components/fitbit/config_flow.py +++ b/homeassistant/components/fitbit/config_flow.py @@ -4,9 +4,8 @@ from collections.abc import Mapping import logging from typing import Any -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import ConfigEntry, ConfigFlowResult from homeassistant.const import CONF_TOKEN -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_entry_oauth2_flow from . import api @@ -38,7 +37,9 @@ class OAuth2FlowHandler( "prompt": "consent" if not self.reauth_entry else "none", } - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Perform reauth upon an API authentication error.""" self.reauth_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] @@ -47,7 +48,7 @@ class OAuth2FlowHandler( async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm reauth dialog.""" if user_input is None: return self.async_show_form(step_id="reauth_confirm") @@ -55,7 +56,7 @@ class OAuth2FlowHandler( async def async_step_creation( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Create config entry from external data with Fitbit specific error handling.""" try: return await super().async_step_creation() @@ -68,7 +69,7 @@ class OAuth2FlowHandler( _LOGGER.error("Failed to create Fitbit credentials: %s", err) return self.async_abort(reason="cannot_connect") - async def async_oauth_create_entry(self, data: dict[str, Any]) -> FlowResult: + async def async_oauth_create_entry(self, data: dict[str, Any]) -> ConfigFlowResult: """Create an entry for the flow, or update existing entry.""" client = api.ConfigFlowFitbitApi(self.hass, data[CONF_TOKEN]) @@ -92,6 +93,6 @@ class OAuth2FlowHandler( self._abort_if_unique_id_configured() return self.async_create_entry(title=profile.display_name, data=data) - async def async_step_import(self, data: dict[str, Any]) -> FlowResult: + async def async_step_import(self, data: dict[str, Any]) -> ConfigFlowResult: """Handle import from YAML.""" return await self.async_oauth_create_entry(data) diff --git a/homeassistant/components/fitbit/const.py b/homeassistant/components/fitbit/const.py index 45b81b3919e..c20854e03cf 100644 --- a/homeassistant/components/fitbit/const.py +++ b/homeassistant/components/fitbit/const.py @@ -1,4 +1,5 @@ """Constants for the Fitbit platform.""" + from __future__ import annotations from enum import StrEnum diff --git a/homeassistant/components/fitbit/sensor.py b/homeassistant/components/fitbit/sensor.py index eb7d3b02b4d..6df4968739f 100644 --- a/homeassistant/components/fitbit/sensor.py +++ b/homeassistant/components/fitbit/sensor.py @@ -1,4 +1,5 @@ """Support for the Fitbit API.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/fivem/__init__.py b/homeassistant/components/fivem/__init__.py index 996aecef261..25d24502846 100644 --- a/homeassistant/components/fivem/__init__.py +++ b/homeassistant/components/fivem/__init__.py @@ -1,4 +1,5 @@ """The FiveM integration.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/fivem/binary_sensor.py b/homeassistant/components/fivem/binary_sensor.py index ee46067f443..de58ea52fb6 100644 --- a/homeassistant/components/fivem/binary_sensor.py +++ b/homeassistant/components/fivem/binary_sensor.py @@ -1,4 +1,5 @@ """The FiveM binary sensor platform.""" + from dataclasses import dataclass from homeassistant.components.binary_sensor import ( diff --git a/homeassistant/components/fivem/config_flow.py b/homeassistant/components/fivem/config_flow.py index e564faa81b7..7cc553a6a72 100644 --- a/homeassistant/components/fivem/config_flow.py +++ b/homeassistant/components/fivem/config_flow.py @@ -1,4 +1,5 @@ """Config flow for FiveM integration.""" + from __future__ import annotations import logging @@ -7,9 +8,8 @@ from typing import Any from fivem import FiveM, FiveMServerOfflineError import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_PORT -from homeassistant.data_entry_flow import FlowResult import homeassistant.helpers.config_validation as cv from .const import DOMAIN @@ -37,14 +37,14 @@ async def validate_input(data: dict[str, Any]) -> None: raise InvalidGameNameError -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class FiveMConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for FiveM.""" VERSION = 1 async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" if user_input is None: return self.async_show_form( diff --git a/homeassistant/components/fivem/coordinator.py b/homeassistant/components/fivem/coordinator.py index 9da641b0bd9..1fdf87fb2b7 100644 --- a/homeassistant/components/fivem/coordinator.py +++ b/homeassistant/components/fivem/coordinator.py @@ -1,4 +1,5 @@ """The FiveM update coordinator.""" + from __future__ import annotations from collections.abc import Mapping @@ -60,9 +61,7 @@ class FiveMDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): except FiveMServerOfflineError as err: raise UpdateFailed from err - players_list: list[str] = [] - for player in server.players: - players_list.append(player.name) + players_list: list[str] = [player.name for player in server.players] players_list.sort() resources_list = server.resources diff --git a/homeassistant/components/fivem/entity.py b/homeassistant/components/fivem/entity.py index 69204b559ae..a7459123fa1 100644 --- a/homeassistant/components/fivem/entity.py +++ b/homeassistant/components/fivem/entity.py @@ -1,4 +1,5 @@ """The FiveM entity.""" + from __future__ import annotations from collections.abc import Mapping diff --git a/homeassistant/components/fivem/sensor.py b/homeassistant/components/fivem/sensor.py index c39f67c5503..b63f3b9082f 100644 --- a/homeassistant/components/fivem/sensor.py +++ b/homeassistant/components/fivem/sensor.py @@ -1,4 +1,5 @@ """The FiveM sensor platform.""" + from dataclasses import dataclass from homeassistant.components.sensor import SensorEntity, SensorEntityDescription diff --git a/homeassistant/components/fixer/sensor.py b/homeassistant/components/fixer/sensor.py index 8091f8981e3..10f05ca29f8 100644 --- a/homeassistant/components/fixer/sensor.py +++ b/homeassistant/components/fixer/sensor.py @@ -1,4 +1,5 @@ """Currency exchange rate support that comes from fixer.io.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/fjaraskupan/__init__.py b/homeassistant/components/fjaraskupan/__init__.py index 48d7809b715..d95cb1d1006 100644 --- a/homeassistant/components/fjaraskupan/__init__.py +++ b/homeassistant/components/fjaraskupan/__init__.py @@ -1,4 +1,5 @@ """The Fjäråskupan integration.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/fjaraskupan/binary_sensor.py b/homeassistant/components/fjaraskupan/binary_sensor.py index 03302d490a6..93886a2ac6a 100644 --- a/homeassistant/components/fjaraskupan/binary_sensor.py +++ b/homeassistant/components/fjaraskupan/binary_sensor.py @@ -1,4 +1,5 @@ """Support for sensors.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/fjaraskupan/config_flow.py b/homeassistant/components/fjaraskupan/config_flow.py index dd1dc03d3ad..d5c287a0cff 100644 --- a/homeassistant/components/fjaraskupan/config_flow.py +++ b/homeassistant/components/fjaraskupan/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Fjäråskupan integration.""" + from __future__ import annotations from fjaraskupan import device_filter diff --git a/homeassistant/components/fjaraskupan/coordinator.py b/homeassistant/components/fjaraskupan/coordinator.py index f955c7ca024..22811ce534b 100644 --- a/homeassistant/components/fjaraskupan/coordinator.py +++ b/homeassistant/components/fjaraskupan/coordinator.py @@ -1,4 +1,5 @@ """The Fjäråskupan data update coordinator.""" + from __future__ import annotations from collections.abc import AsyncIterator diff --git a/homeassistant/components/fjaraskupan/fan.py b/homeassistant/components/fjaraskupan/fan.py index ee989bb2ee0..67514eaa411 100644 --- a/homeassistant/components/fjaraskupan/fan.py +++ b/homeassistant/components/fjaraskupan/fan.py @@ -1,4 +1,5 @@ """Support for Fjäråskupan fans.""" + from __future__ import annotations from typing import Any @@ -85,7 +86,7 @@ class Fan(CoordinatorEntity[FjaraskupanCoordinator], FanEntity): async def async_set_percentage(self, percentage: int) -> None: """Set speed.""" - # Proactively update percentage to mange successive increases + # Proactively update percentage to manage successive increases self._percentage = percentage async with self.coordinator.async_connect_and_update() as device: diff --git a/homeassistant/components/fjaraskupan/light.py b/homeassistant/components/fjaraskupan/light.py index 396f6b00e3b..b33904c805d 100644 --- a/homeassistant/components/fjaraskupan/light.py +++ b/homeassistant/components/fjaraskupan/light.py @@ -1,4 +1,5 @@ """Support for lights.""" + from __future__ import annotations from typing import Any @@ -53,13 +54,14 @@ class Light(CoordinatorEntity[FjaraskupanCoordinator], LightEntity): async with self.coordinator.async_connect_and_update() as device: if ATTR_BRIGHTNESS in kwargs: await device.send_dim(int(kwargs[ATTR_BRIGHTNESS] * (100.0 / 255.0))) - elif not self.is_on: - await device.send_command(COMMAND_LIGHT_ON_OFF) + else: + await device.send_dim(100) async def async_turn_off(self, **kwargs: Any) -> None: """Turn the entity off.""" if self.is_on: async with self.coordinator.async_connect_and_update() as device: + await device.send_dim(0) await device.send_command(COMMAND_LIGHT_ON_OFF) @property diff --git a/homeassistant/components/fjaraskupan/manifest.json b/homeassistant/components/fjaraskupan/manifest.json index f7ad701a756..91c74b68e01 100644 --- a/homeassistant/components/fjaraskupan/manifest.json +++ b/homeassistant/components/fjaraskupan/manifest.json @@ -14,5 +14,5 @@ "documentation": "https://www.home-assistant.io/integrations/fjaraskupan", "iot_class": "local_polling", "loggers": ["bleak", "fjaraskupan"], - "requirements": ["fjaraskupan==2.2.0"] + "requirements": ["fjaraskupan==2.3.0"] } diff --git a/homeassistant/components/fjaraskupan/number.py b/homeassistant/components/fjaraskupan/number.py index d57e10aa561..1828c4cdea5 100644 --- a/homeassistant/components/fjaraskupan/number.py +++ b/homeassistant/components/fjaraskupan/number.py @@ -1,4 +1,5 @@ """Support for sensors.""" + from __future__ import annotations from homeassistant.components.number import NumberEntity diff --git a/homeassistant/components/fjaraskupan/sensor.py b/homeassistant/components/fjaraskupan/sensor.py index 30527d4e29d..36db4d7ed9f 100644 --- a/homeassistant/components/fjaraskupan/sensor.py +++ b/homeassistant/components/fjaraskupan/sensor.py @@ -1,4 +1,5 @@ """Support for sensors.""" + from __future__ import annotations from fjaraskupan import Device diff --git a/homeassistant/components/fleetgo/device_tracker.py b/homeassistant/components/fleetgo/device_tracker.py index e736419ce29..3249e8035b4 100644 --- a/homeassistant/components/fleetgo/device_tracker.py +++ b/homeassistant/components/fleetgo/device_tracker.py @@ -1,4 +1,5 @@ """Support for FleetGO Platform.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/flexit/climate.py b/homeassistant/components/flexit/climate.py index 85d5e9f4eac..c15c74b4aac 100644 --- a/homeassistant/components/flexit/climate.py +++ b/homeassistant/components/flexit/climate.py @@ -1,4 +1,5 @@ """Platform for Flexit AC units with CI66 Modbus adapter.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/flexit_bacnet/__init__.py b/homeassistant/components/flexit_bacnet/__init__.py index 5732fb3822c..6b42310d181 100644 --- a/homeassistant/components/flexit_bacnet/__init__.py +++ b/homeassistant/components/flexit_bacnet/__init__.py @@ -1,4 +1,5 @@ """The Flexit Nordic (BACnet) integration.""" + from __future__ import annotations from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/flexit_bacnet/binary_sensor.py b/homeassistant/components/flexit_bacnet/binary_sensor.py index b014fbca415..901cc52de47 100644 --- a/homeassistant/components/flexit_bacnet/binary_sensor.py +++ b/homeassistant/components/flexit_bacnet/binary_sensor.py @@ -1,4 +1,5 @@ """The Flexit Nordic (BACnet) integration.""" + from collections.abc import Callable from dataclasses import dataclass diff --git a/homeassistant/components/flexit_bacnet/climate.py b/homeassistant/components/flexit_bacnet/climate.py index 84785720fb2..0526a0d6bd3 100644 --- a/homeassistant/components/flexit_bacnet/climate.py +++ b/homeassistant/components/flexit_bacnet/climate.py @@ -1,4 +1,5 @@ """The Flexit Nordic (BACnet) integration.""" + import asyncio.exceptions from typing import Any diff --git a/homeassistant/components/flexit_bacnet/config_flow.py b/homeassistant/components/flexit_bacnet/config_flow.py index 2c87dfc5b97..087f70869bb 100644 --- a/homeassistant/components/flexit_bacnet/config_flow.py +++ b/homeassistant/components/flexit_bacnet/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Flexit Nordic (BACnet) integration.""" + from __future__ import annotations import asyncio.exceptions @@ -9,9 +10,8 @@ from flexit_bacnet import FlexitBACnet from flexit_bacnet.bacnet import DecodingError import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_DEVICE_ID, CONF_IP_ADDRESS -from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN @@ -27,14 +27,14 @@ STEP_USER_DATA_SCHEMA = vol.Schema( ) -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class FlexitBacnetConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Flexit Nordic (BACnet).""" VERSION = 1 async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors: dict[str, str] = {} diff --git a/homeassistant/components/flexit_bacnet/const.py b/homeassistant/components/flexit_bacnet/const.py index ed52b45f05e..3a16e548ece 100644 --- a/homeassistant/components/flexit_bacnet/const.py +++ b/homeassistant/components/flexit_bacnet/const.py @@ -1,4 +1,5 @@ """Constants for the Flexit Nordic (BACnet) integration.""" + from flexit_bacnet import ( VENTILATION_MODE_AWAY, VENTILATION_MODE_HIGH, diff --git a/homeassistant/components/flexit_bacnet/coordinator.py b/homeassistant/components/flexit_bacnet/coordinator.py index 556264e1268..79f3b6a05ad 100644 --- a/homeassistant/components/flexit_bacnet/coordinator.py +++ b/homeassistant/components/flexit_bacnet/coordinator.py @@ -1,4 +1,5 @@ """DataUpdateCoordinator for Flexit Nordic (BACnet) integration..""" + import asyncio.exceptions from datetime import timedelta import logging diff --git a/homeassistant/components/flexit_bacnet/entity.py b/homeassistant/components/flexit_bacnet/entity.py index 3e00fae54af..bd92550db19 100644 --- a/homeassistant/components/flexit_bacnet/entity.py +++ b/homeassistant/components/flexit_bacnet/entity.py @@ -1,4 +1,5 @@ """Base entity for the Flexit Nordic (BACnet) integration.""" + from __future__ import annotations from flexit_bacnet import FlexitBACnet diff --git a/homeassistant/components/flexit_bacnet/manifest.json b/homeassistant/components/flexit_bacnet/manifest.json index d230e4ebb7a..40390162ce6 100644 --- a/homeassistant/components/flexit_bacnet/manifest.json +++ b/homeassistant/components/flexit_bacnet/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/flexit_bacnet", "integration_type": "device", "iot_class": "local_polling", - "requirements": ["flexit_bacnet==2.1.0"] + "requirements": ["flexit_bacnet==2.2.1"] } diff --git a/homeassistant/components/flexit_bacnet/number.py b/homeassistant/components/flexit_bacnet/number.py index 2731d5e8b09..6e6e2eea980 100644 --- a/homeassistant/components/flexit_bacnet/number.py +++ b/homeassistant/components/flexit_bacnet/number.py @@ -1,4 +1,5 @@ """The Flexit Nordic (BACnet) integration.""" + import asyncio.exceptions from collections.abc import Awaitable, Callable from dataclasses import dataclass diff --git a/homeassistant/components/flexit_bacnet/sensor.py b/homeassistant/components/flexit_bacnet/sensor.py index 590136ad5f7..2453acb90be 100644 --- a/homeassistant/components/flexit_bacnet/sensor.py +++ b/homeassistant/components/flexit_bacnet/sensor.py @@ -1,4 +1,5 @@ """The Flexit Nordic (BACnet) integration.""" + from collections.abc import Callable from dataclasses import dataclass diff --git a/homeassistant/components/flexit_bacnet/switch.py b/homeassistant/components/flexit_bacnet/switch.py index 0a7785eaa38..c58e35cda75 100644 --- a/homeassistant/components/flexit_bacnet/switch.py +++ b/homeassistant/components/flexit_bacnet/switch.py @@ -1,4 +1,5 @@ """The Flexit Nordic (BACnet) integration.""" + import asyncio.exceptions from collections.abc import Awaitable, Callable from dataclasses import dataclass diff --git a/homeassistant/components/flic/binary_sensor.py b/homeassistant/components/flic/binary_sensor.py index 81a23a9eeb5..b7f8bb0c854 100644 --- a/homeassistant/components/flic/binary_sensor.py +++ b/homeassistant/components/flic/binary_sensor.py @@ -1,4 +1,5 @@ """Support to use flic buttons as a binary sensor.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/flick_electric/config_flow.py b/homeassistant/components/flick_electric/config_flow.py index 842706172f1..41b58431977 100644 --- a/homeassistant/components/flick_electric/config_flow.py +++ b/homeassistant/components/flick_electric/config_flow.py @@ -1,4 +1,5 @@ """Config Flow for Flick Electric integration.""" + import asyncio import logging @@ -6,13 +7,14 @@ from pyflick.authentication import AuthException, SimpleFlickAuth from pyflick.const import DEFAULT_CLIENT_ID, DEFAULT_CLIENT_SECRET import voluptuous as vol -from homeassistant import config_entries, exceptions +from homeassistant.config_entries import ConfigFlow from homeassistant.const import ( CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_PASSWORD, CONF_USERNAME, ) +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import aiohttp_client from .const import DOMAIN @@ -29,7 +31,7 @@ DATA_SCHEMA = vol.Schema( ) -class FlickConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class FlickConfigFlow(ConfigFlow, domain=DOMAIN): """Flick config flow.""" VERSION = 1 @@ -47,9 +49,9 @@ class FlickConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async with asyncio.timeout(60): token = await auth.async_get_access_token() except TimeoutError as err: - raise CannotConnect() from err + raise CannotConnect from err except AuthException as err: - raise InvalidAuth() from err + raise InvalidAuth from err return token is not None @@ -82,9 +84,9 @@ class FlickConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -class CannotConnect(exceptions.HomeAssistantError): +class CannotConnect(HomeAssistantError): """Error to indicate we cannot connect.""" -class InvalidAuth(exceptions.HomeAssistantError): +class InvalidAuth(HomeAssistantError): """Error to indicate there is invalid auth.""" diff --git a/homeassistant/components/flick_electric/sensor.py b/homeassistant/components/flick_electric/sensor.py index 8280e7b2fe0..347109c66c0 100644 --- a/homeassistant/components/flick_electric/sensor.py +++ b/homeassistant/components/flick_electric/sensor.py @@ -1,4 +1,5 @@ """Support for Flick Electric Pricing data.""" + import asyncio from datetime import timedelta import logging diff --git a/homeassistant/components/flipr/__init__.py b/homeassistant/components/flipr/__init__.py index e6d7cb1dd17..28515dd386f 100644 --- a/homeassistant/components/flipr/__init__.py +++ b/homeassistant/components/flipr/__init__.py @@ -1,4 +1,5 @@ """The Flipr integration.""" + from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/flipr/binary_sensor.py b/homeassistant/components/flipr/binary_sensor.py index 677a282e8cb..a3c3e4dc8a1 100644 --- a/homeassistant/components/flipr/binary_sensor.py +++ b/homeassistant/components/flipr/binary_sensor.py @@ -1,4 +1,5 @@ """Support for Flipr binary sensors.""" + from __future__ import annotations from homeassistant.components.binary_sensor import ( diff --git a/homeassistant/components/flipr/config_flow.py b/homeassistant/components/flipr/config_flow.py index 5c8cc6f76fb..9d177e4c2b6 100644 --- a/homeassistant/components/flipr/config_flow.py +++ b/homeassistant/components/flipr/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Flipr integration.""" + from __future__ import annotations import logging @@ -7,16 +8,15 @@ from flipr_api import FliprAPIRestClient from requests.exceptions import HTTPError, Timeout import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_EMAIL, CONF_PASSWORD -from homeassistant.data_entry_flow import FlowResult from .const import CONF_FLIPR_ID, DOMAIN _LOGGER = logging.getLogger(__name__) -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class FliprConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Flipr.""" VERSION = 1 @@ -28,7 +28,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, str] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" if user_input is None: return self._show_setup_form() @@ -97,7 +97,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_flipr_id( self, user_input: dict[str, str] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" if not user_input: # Creation of a select with the proposal of flipr ids values found by API. diff --git a/homeassistant/components/flipr/coordinator.py b/homeassistant/components/flipr/coordinator.py index d51db645035..afc7465498f 100644 --- a/homeassistant/components/flipr/coordinator.py +++ b/homeassistant/components/flipr/coordinator.py @@ -1,4 +1,5 @@ """DataUpdateCoordinator for flipr integration.""" + from datetime import timedelta import logging diff --git a/homeassistant/components/flipr/entity.py b/homeassistant/components/flipr/entity.py index 6166d727ac7..859ffc9390b 100644 --- a/homeassistant/components/flipr/entity.py +++ b/homeassistant/components/flipr/entity.py @@ -1,4 +1,5 @@ """Base entity for the flipr entity.""" + from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.update_coordinator import ( diff --git a/homeassistant/components/flipr/icons.json b/homeassistant/components/flipr/icons.json new file mode 100644 index 00000000000..2e55e81e562 --- /dev/null +++ b/homeassistant/components/flipr/icons.json @@ -0,0 +1,12 @@ +{ + "entity": { + "sensor": { + "chlorine": { + "default": "mdi:pool" + }, + "red_ox": { + "default": "mdi:pool" + } + } + } +} diff --git a/homeassistant/components/flipr/sensor.py b/homeassistant/components/flipr/sensor.py index 66078c50c1a..7a1c64dc766 100644 --- a/homeassistant/components/flipr/sensor.py +++ b/homeassistant/components/flipr/sensor.py @@ -1,4 +1,5 @@ """Sensor platform for the Flipr's pool_sensor.""" + from __future__ import annotations from homeassistant.components.sensor import ( @@ -20,12 +21,10 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key="chlorine", translation_key="chlorine", native_unit_of_measurement=UnitOfElectricPotential.MILLIVOLT, - icon="mdi:pool", state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="ph", - icon="mdi:pool", device_class=SensorDeviceClass.PH, state_class=SensorStateClass.MEASUREMENT, ), @@ -45,7 +44,6 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key="red_ox", translation_key="red_ox", native_unit_of_measurement=UnitOfElectricPotential.MILLIVOLT, - icon="mdi:pool", state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( diff --git a/homeassistant/components/flo/__init__.py b/homeassistant/components/flo/__init__.py index b30e31de361..0d65e12a2a3 100644 --- a/homeassistant/components/flo/__init__.py +++ b/homeassistant/components/flo/__init__.py @@ -1,4 +1,5 @@ """The flo integration.""" + import asyncio import logging diff --git a/homeassistant/components/flo/binary_sensor.py b/homeassistant/components/flo/binary_sensor.py index d61f67cc623..84ce9d2bb7b 100644 --- a/homeassistant/components/flo/binary_sensor.py +++ b/homeassistant/components/flo/binary_sensor.py @@ -1,4 +1,5 @@ """Support for Flo Water Monitor binary sensors.""" + from __future__ import annotations from homeassistant.components.binary_sensor import ( diff --git a/homeassistant/components/flo/config_flow.py b/homeassistant/components/flo/config_flow.py index c34753c3295..ec92b60c740 100644 --- a/homeassistant/components/flo/config_flow.py +++ b/homeassistant/components/flo/config_flow.py @@ -1,10 +1,13 @@ """Config flow for flo integration.""" + from aioflo import async_get_api from aioflo.errors import RequestError import voluptuous as vol -from homeassistant import config_entries, core, exceptions +from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN, LOGGER @@ -14,7 +17,7 @@ DATA_SCHEMA = vol.Schema( ) -async def validate_input(hass: core.HomeAssistant, data): +async def validate_input(hass: HomeAssistant, data): """Validate the user input allows us to connect. Data has the keys from DATA_SCHEMA with values provided by the user. @@ -28,7 +31,7 @@ async def validate_input(hass: core.HomeAssistant, data): raise CannotConnect from request_error -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class FloConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for flo.""" VERSION = 1 @@ -52,5 +55,5 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -class CannotConnect(exceptions.HomeAssistantError): +class CannotConnect(HomeAssistantError): """Error to indicate we cannot connect.""" diff --git a/homeassistant/components/flo/const.py b/homeassistant/components/flo/const.py index 907561b5b9c..9eb00ebfa62 100644 --- a/homeassistant/components/flo/const.py +++ b/homeassistant/components/flo/const.py @@ -1,4 +1,5 @@ """Constants for the flo integration.""" + import logging LOGGER = logging.getLogger(__package__) diff --git a/homeassistant/components/flo/device.py b/homeassistant/components/flo/device.py index 27feb15a97e..2d99b8ac7a7 100644 --- a/homeassistant/components/flo/device.py +++ b/homeassistant/components/flo/device.py @@ -1,4 +1,5 @@ """Flo device object.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/flo/entity.py b/homeassistant/components/flo/entity.py index 2745f5f9fb7..62090d67194 100644 --- a/homeassistant/components/flo/entity.py +++ b/homeassistant/components/flo/entity.py @@ -1,4 +1,5 @@ """Base entity class for Flo entities.""" + from __future__ import annotations from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo diff --git a/homeassistant/components/flo/sensor.py b/homeassistant/components/flo/sensor.py index 476898c8ef3..9b85f3a855b 100644 --- a/homeassistant/components/flo/sensor.py +++ b/homeassistant/components/flo/sensor.py @@ -1,4 +1,5 @@ """Support for Flo Water Monitor sensors.""" + from __future__ import annotations from homeassistant.components.sensor import ( diff --git a/homeassistant/components/flo/switch.py b/homeassistant/components/flo/switch.py index 62a57c463e2..41690c28ae4 100644 --- a/homeassistant/components/flo/switch.py +++ b/homeassistant/components/flo/switch.py @@ -1,4 +1,5 @@ """Switch representing the shutoff valve for the Flo by Moen integration.""" + from __future__ import annotations from typing import Any @@ -33,11 +34,10 @@ async def async_setup_entry( devices: list[FloDeviceDataUpdateCoordinator] = hass.data[FLO_DOMAIN][ config_entry.entry_id ]["devices"] - entities = [] - for device in devices: - if device.device_type != "puck_oem": - entities.append(FloSwitch(device)) - async_add_entities(entities) + + async_add_entities( + [FloSwitch(device) for device in devices if device.device_type != "puck_oem"] + ) platform = entity_platform.async_get_current_platform() diff --git a/homeassistant/components/flock/notify.py b/homeassistant/components/flock/notify.py index c5926e3158e..61c9a29bd6c 100644 --- a/homeassistant/components/flock/notify.py +++ b/homeassistant/components/flock/notify.py @@ -1,4 +1,5 @@ """Flock platform for notify component.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/flume/__init__.py b/homeassistant/components/flume/__init__.py index 3a8718a14e0..d91c6b175cf 100644 --- a/homeassistant/components/flume/__init__.py +++ b/homeassistant/components/flume/__init__.py @@ -1,4 +1,5 @@ """The flume integration.""" + from __future__ import annotations from pyflume import FlumeAuth, FlumeDeviceList diff --git a/homeassistant/components/flume/binary_sensor.py b/homeassistant/components/flume/binary_sensor.py index a31fecf305e..28f56168d9c 100644 --- a/homeassistant/components/flume/binary_sensor.py +++ b/homeassistant/components/flume/binary_sensor.py @@ -1,4 +1,5 @@ """Flume binary sensors.""" + from __future__ import annotations from dataclasses import dataclass @@ -39,20 +40,13 @@ BINARY_SENSOR_DESCRIPTION_CONNECTED = BinarySensorEntityDescription( ) -@dataclass(frozen=True) -class FlumeBinarySensorRequiredKeysMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class FlumeBinarySensorEntityDescription(BinarySensorEntityDescription): + """Describes a binary sensor entity.""" event_rule: str -@dataclass(frozen=True) -class FlumeBinarySensorEntityDescription( - BinarySensorEntityDescription, FlumeBinarySensorRequiredKeysMixin -): - """Describes a binary sensor entity.""" - - FLUME_BINARY_NOTIFICATION_SENSORS: tuple[FlumeBinarySensorEntityDescription, ...] = ( FlumeBinarySensorEntityDescription( key="leak", diff --git a/homeassistant/components/flume/config_flow.py b/homeassistant/components/flume/config_flow.py index df2a697ed8d..cbe3f4983fa 100644 --- a/homeassistant/components/flume/config_flow.py +++ b/homeassistant/components/flume/config_flow.py @@ -1,4 +1,5 @@ """Config flow for flume integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -10,14 +11,15 @@ from pyflume import FlumeAuth, FlumeDeviceList from requests.exceptions import RequestException import voluptuous as vol -from homeassistant import config_entries, core, exceptions +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import ( CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_PASSWORD, CONF_USERNAME, ) -from homeassistant.data_entry_flow import FlowResult +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from .const import BASE_TOKEN_FILENAME, DOMAIN @@ -39,7 +41,7 @@ DATA_SCHEMA = vol.Schema( def _validate_input( - hass: core.HomeAssistant, data: dict[str, Any], clear_token_file: bool + hass: HomeAssistant, data: dict[str, Any], clear_token_file: bool ) -> FlumeDeviceList: """Validate in the executor.""" flume_token_full_path = hass.config.path( @@ -60,7 +62,7 @@ def _validate_input( async def validate_input( - hass: core.HomeAssistant, data: dict[str, Any], clear_token_file: bool = False + hass: HomeAssistant, data: dict[str, Any], clear_token_file: bool = False ) -> dict[str, Any]: """Validate the user input allows us to connect. @@ -82,7 +84,7 @@ async def validate_input( return {"title": data[CONF_USERNAME]} -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class FlumeConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for flume.""" VERSION = 1 @@ -93,7 +95,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors: dict[str, str] = {} if user_input is not None: @@ -112,14 +114,16 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="user", data_schema=DATA_SCHEMA, errors=errors ) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Handle reauth.""" self._reauth_unique_id = self.context["unique_id"] return await self.async_step_reauth_confirm() async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle reauth input.""" errors: dict[str, str] = {} existing_entry = await self.async_set_unique_id(self._reauth_unique_id) @@ -153,9 +157,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -class CannotConnect(exceptions.HomeAssistantError): +class CannotConnect(HomeAssistantError): """Error to indicate we cannot connect.""" -class InvalidAuth(exceptions.HomeAssistantError): +class InvalidAuth(HomeAssistantError): """Error to indicate there is invalid auth.""" diff --git a/homeassistant/components/flume/const.py b/homeassistant/components/flume/const.py index a4e7dba444e..1f9fc10b1b3 100644 --- a/homeassistant/components/flume/const.py +++ b/homeassistant/components/flume/const.py @@ -1,4 +1,5 @@ """The Flume component.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/flume/coordinator.py b/homeassistant/components/flume/coordinator.py index b5d37b8027f..30e7962304c 100644 --- a/homeassistant/components/flume/coordinator.py +++ b/homeassistant/components/flume/coordinator.py @@ -1,4 +1,5 @@ """The IntelliFire integration.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/flume/entity.py b/homeassistant/components/flume/entity.py index a6d13b1f291..139094e9ae3 100644 --- a/homeassistant/components/flume/entity.py +++ b/homeassistant/components/flume/entity.py @@ -1,4 +1,5 @@ """Platform for shared base classes for sensors.""" + from __future__ import annotations from typing import TypeVar diff --git a/homeassistant/components/flux/switch.py b/homeassistant/components/flux/switch.py index f71ccc87f05..63f58ff64c4 100644 --- a/homeassistant/components/flux/switch.py +++ b/homeassistant/components/flux/switch.py @@ -2,6 +2,7 @@ The idea was taken from https://github.com/KpaBap/hue-flux/ """ + from __future__ import annotations import datetime diff --git a/homeassistant/components/flux_led/__init__.py b/homeassistant/components/flux_led/__init__.py index 2d9dddd3684..b3e17a65a5c 100644 --- a/homeassistant/components/flux_led/__init__.py +++ b/homeassistant/components/flux_led/__init__.py @@ -1,4 +1,5 @@ """The Flux LED/MagicLight integration.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/flux_led/button.py b/homeassistant/components/flux_led/button.py index eb3a7341d4d..90918a55bb2 100644 --- a/homeassistant/components/flux_led/button.py +++ b/homeassistant/components/flux_led/button.py @@ -1,4 +1,5 @@ """Support for Magic home button.""" + from __future__ import annotations from flux_led.aio import AIOWifiLedBulb @@ -28,7 +29,6 @@ RESTART_BUTTON_DESCRIPTION = ButtonEntityDescription( UNPAIR_REMOTES_DESCRIPTION = ButtonEntityDescription( key=_UNPAIR_REMOTES_KEY, translation_key="unpair_remotes", - icon="mdi:remote-off", ) diff --git a/homeassistant/components/flux_led/config_flow.py b/homeassistant/components/flux_led/config_flow.py index d50e6a08b5a..469c67deb22 100644 --- a/homeassistant/components/flux_led/config_flow.py +++ b/homeassistant/components/flux_led/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Flux LED/MagicLight.""" + from __future__ import annotations import contextlib @@ -15,11 +16,18 @@ from flux_led.const import ( from flux_led.scanner import FluxLEDDiscovery import voluptuous as vol -from homeassistant import config_entries from homeassistant.components import dhcp +from homeassistant.config_entries import ( + SOURCE_IGNORE, + ConfigEntry, + ConfigEntryState, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.const import CONF_DEVICE, CONF_HOST from homeassistant.core import callback -from homeassistant.data_entry_flow import AbortFlow, FlowResult +from homeassistant.data_entry_flow import AbortFlow from homeassistant.helpers import device_registry as dr from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.typing import DiscoveryInfoType @@ -48,7 +56,7 @@ from .discovery import ( from .util import format_as_flux_mac, mac_matches_by_one -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class FluxLedConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Magic Home Integration.""" VERSION = 1 @@ -61,11 +69,13 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback - def async_get_options_flow(config_entry: config_entries.ConfigEntry) -> OptionsFlow: + def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow: """Get the options flow for the Flux LED component.""" - return OptionsFlow(config_entry) + return FluxLedOptionsFlow(config_entry) - async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: + async def async_step_dhcp( + self, discovery_info: dhcp.DhcpServiceInfo + ) -> ConfigFlowResult: """Handle discovery via dhcp.""" self._discovered_device = FluxLEDDiscovery( ipaddr=discovery_info.ip, @@ -84,7 +94,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_integration_discovery( self, discovery_info: DiscoveryInfoType - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle integration discovery.""" self._allow_update_mac = True self._discovered_device = cast(FluxLEDDiscovery, discovery_info) @@ -113,7 +123,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) ): continue - if entry.source == config_entries.SOURCE_IGNORE: + if entry.source == SOURCE_IGNORE: raise AbortFlow("already_configured") if ( async_update_entry_from_discovery( @@ -121,10 +131,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) and entry.state not in ( - config_entries.ConfigEntryState.SETUP_IN_PROGRESS, - config_entries.ConfigEntryState.NOT_LOADED, + ConfigEntryState.SETUP_IN_PROGRESS, + ConfigEntryState.NOT_LOADED, ) - ) or entry.state == config_entries.ConfigEntryState.SETUP_RETRY: + ) or entry.state == ConfigEntryState.SETUP_RETRY: self.hass.config_entries.async_schedule_reload(entry.entry_id) else: async_dispatcher_send( @@ -133,7 +143,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) raise AbortFlow("already_configured") - async def _async_handle_discovery(self) -> FlowResult: + async def _async_handle_discovery(self) -> ConfigFlowResult: """Handle any discovery.""" device = self._discovered_device assert device is not None @@ -165,7 +175,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_discovery_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm discovery.""" assert self._discovered_device is not None device = self._discovered_device @@ -186,7 +196,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) @callback - def _async_create_entry_from_device(self, device: FluxLEDDiscovery) -> FlowResult: + def _async_create_entry_from_device( + self, device: FluxLEDDiscovery + ) -> ConfigFlowResult: """Create a config entry from a device.""" self._async_abort_entries_match({CONF_HOST: device[ATTR_IPADDR]}) name = async_name_from_discovery(device) @@ -199,7 +211,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors = {} if user_input is not None: @@ -225,7 +237,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_pick_device( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the step to pick discovered device.""" if user_input is not None: mac = user_input[CONF_DEVICE] @@ -298,16 +310,16 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -class OptionsFlow(config_entries.OptionsFlow): +class FluxLedOptionsFlow(OptionsFlow): """Handle flux_led options.""" - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + def __init__(self, config_entry: ConfigEntry) -> None: """Initialize the flux_led options flow.""" self._config_entry = config_entry async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Configure the options.""" errors: dict[str, str] = {} if user_input is not None: diff --git a/homeassistant/components/flux_led/coordinator.py b/homeassistant/components/flux_led/coordinator.py index bf3f1dee94a..a473387a513 100644 --- a/homeassistant/components/flux_led/coordinator.py +++ b/homeassistant/components/flux_led/coordinator.py @@ -1,4 +1,5 @@ """The Flux LED/MagicLight integration coordinator.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/flux_led/diagnostics.py b/homeassistant/components/flux_led/diagnostics.py index f0c95ffbe56..e24c1aff9a4 100644 --- a/homeassistant/components/flux_led/diagnostics.py +++ b/homeassistant/components/flux_led/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for flux_led.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/flux_led/discovery.py b/homeassistant/components/flux_led/discovery.py index ef0c131993e..9600f773701 100644 --- a/homeassistant/components/flux_led/discovery.py +++ b/homeassistant/components/flux_led/discovery.py @@ -1,4 +1,5 @@ """The Flux LED/MagicLight integration discovery.""" + from __future__ import annotations import asyncio @@ -28,6 +29,7 @@ from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.const import CONF_HOST, CONF_MODEL, CONF_NAME from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry as dr, discovery_flow +from homeassistant.util.async_ import create_eager_task from homeassistant.util.network import is_ip_address from .const import ( @@ -184,7 +186,7 @@ async def async_discover_devices( for idx, discovered in enumerate( await asyncio.gather( *[ - scanner.async_scan(timeout=timeout, address=address) + create_eager_task(scanner.async_scan(timeout=timeout, address=address)) for address in targets ], return_exceptions=True, diff --git a/homeassistant/components/flux_led/entity.py b/homeassistant/components/flux_led/entity.py index 1adcd39e22f..bcf7bfff9ed 100644 --- a/homeassistant/components/flux_led/entity.py +++ b/homeassistant/components/flux_led/entity.py @@ -1,4 +1,5 @@ """Support for Magic Home lights.""" + from __future__ import annotations from abc import abstractmethod diff --git a/homeassistant/components/flux_led/icons.json b/homeassistant/components/flux_led/icons.json new file mode 100644 index 00000000000..873fcd7c441 --- /dev/null +++ b/homeassistant/components/flux_led/icons.json @@ -0,0 +1,61 @@ +{ + "entity": { + "button": { + "unpair_remotes": { + "default": "mdi:remote-off" + } + }, + "number": { + "effect_speed": { + "default": "mdi:speedometer" + }, + "pixels_per_segment": { + "default": "mdi:dots-grid" + }, + "segments": { + "default": "mdi:segment" + }, + "music_pixels_per_segment": { + "default": "mdi:dots-grid" + }, + "music_segments": { + "default": "mdi:segment" + } + }, + "select": { + "power_restored": { + "default": "mdi:transmission-tower-off" + }, + "ic_type": { + "default": "mdi:chip" + }, + "wiring": { + "default": "mdi:led-strip-variant" + } + }, + "sensor": { + "paired_remotes": { + "default": "mdi:remote" + } + }, + "switch": { + "remote_access": { + "default": "mdi:cloud-off-outline", + "state": { + "on": "mdi:cloud-outline" + } + }, + "music": { + "default": "mdi:microphone-off", + "state": { + "on": "mdi:microphone" + } + } + } + }, + "services": { + "set_custom_effect": "mdi:creation", + "set_zones": "mdi:texture-box", + "set_music_mode": "mdi:music" + } +} diff --git a/homeassistant/components/flux_led/light.py b/homeassistant/components/flux_led/light.py index 1232cb41031..6456eb36dbb 100644 --- a/homeassistant/components/flux_led/light.py +++ b/homeassistant/components/flux_led/light.py @@ -1,4 +1,5 @@ """Support for Magic Home lights.""" + from __future__ import annotations import ast diff --git a/homeassistant/components/flux_led/number.py b/homeassistant/components/flux_led/number.py index ac23fbe64b5..93687c0c579 100644 --- a/homeassistant/components/flux_led/number.py +++ b/homeassistant/components/flux_led/number.py @@ -1,4 +1,5 @@ """Support for LED numbers.""" + from __future__ import annotations from abc import abstractmethod @@ -89,7 +90,6 @@ class FluxSpeedNumber( _attr_native_max_value = 100 _attr_native_step = 1 _attr_mode = NumberMode.SLIDER - _attr_icon = "mdi:speedometer" _attr_translation_key = "effect_speed" @property @@ -175,7 +175,6 @@ class FluxPixelsPerSegmentNumber(FluxConfigNumber): """Defines a flux_led pixels per segment number.""" _attr_translation_key = "pixels_per_segment" - _attr_icon = "mdi:dots-grid" @property def native_max_value(self) -> int: @@ -202,7 +201,6 @@ class FluxSegmentsNumber(FluxConfigNumber): """Defines a flux_led segments number.""" _attr_translation_key = "segments" - _attr_icon = "mdi:segment" @property def native_max_value(self) -> int: @@ -237,7 +235,6 @@ class FluxMusicPixelsPerSegmentNumber(FluxMusicNumber): """Defines a flux_led music pixels per segment number.""" _attr_translation_key = "music_pixels_per_segment" - _attr_icon = "mdi:dots-grid" @property def native_max_value(self) -> int: @@ -266,7 +263,6 @@ class FluxMusicSegmentsNumber(FluxMusicNumber): """Defines a flux_led music segments number.""" _attr_translation_key = "music_segments" - _attr_icon = "mdi:segment" @property def native_max_value(self) -> int: diff --git a/homeassistant/components/flux_led/select.py b/homeassistant/components/flux_led/select.py index cca86e5a216..3809e73147a 100644 --- a/homeassistant/components/flux_led/select.py +++ b/homeassistant/components/flux_led/select.py @@ -1,4 +1,5 @@ """Support for Magic Home select.""" + from __future__ import annotations import asyncio @@ -91,7 +92,6 @@ class FluxPowerStateSelect(FluxConfigAtStartSelect, SelectEntity): """Representation of a Flux power restore state option.""" _attr_translation_key = "power_restored" - _attr_icon = "mdi:transmission-tower-off" _attr_options = list(NAME_TO_POWER_RESTORE_STATE) def __init__( @@ -125,7 +125,6 @@ class FluxPowerStateSelect(FluxConfigAtStartSelect, SelectEntity): class FluxICTypeSelect(FluxConfigSelect): """Representation of Flux ic type.""" - _attr_icon = "mdi:chip" _attr_translation_key = "ic_type" @property @@ -148,7 +147,6 @@ class FluxICTypeSelect(FluxConfigSelect): class FluxWiringsSelect(FluxConfigSelect): """Representation of Flux wirings.""" - _attr_icon = "mdi:led-strip-variant" _attr_translation_key = "wiring" @property diff --git a/homeassistant/components/flux_led/sensor.py b/homeassistant/components/flux_led/sensor.py index 9a19c629383..5a6633669ae 100644 --- a/homeassistant/components/flux_led/sensor.py +++ b/homeassistant/components/flux_led/sensor.py @@ -1,4 +1,5 @@ """Support for Magic Home sensors.""" + from __future__ import annotations from homeassistant import config_entries @@ -34,7 +35,6 @@ async def async_setup_entry( class FluxPairedRemotes(FluxEntity, SensorEntity): """Representation of a Magic Home paired remotes sensor.""" - _attr_icon = "mdi:remote" _attr_entity_category = EntityCategory.DIAGNOSTIC _attr_translation_key = "paired_remotes" diff --git a/homeassistant/components/flux_led/switch.py b/homeassistant/components/flux_led/switch.py index 58aee132216..3adcd9a9da9 100644 --- a/homeassistant/components/flux_led/switch.py +++ b/homeassistant/components/flux_led/switch.py @@ -1,4 +1,5 @@ """Support for Magic Home switches.""" + from __future__ import annotations from typing import Any @@ -103,11 +104,6 @@ class FluxRemoteAccessSwitch(FluxBaseEntity, SwitchEntity): """Return true if remote access is enabled.""" return bool(self.entry.data[CONF_REMOTE_ACCESS_ENABLED]) - @property - def icon(self) -> str: - """Return icon based on state.""" - return "mdi:cloud-outline" if self.is_on else "mdi:cloud-off-outline" - class FluxMusicSwitch(FluxEntity, SwitchEntity): """Representation of a Flux music switch.""" @@ -131,8 +127,3 @@ class FluxMusicSwitch(FluxEntity, SwitchEntity): def is_on(self) -> bool: """Return true if microphone is is on.""" return self._device.is_on and self._device.effect == MODE_MUSIC - - @property - def icon(self) -> str: - """Return icon based on state.""" - return "mdi:microphone" if self.is_on else "mdi:microphone-off" diff --git a/homeassistant/components/flux_led/util.py b/homeassistant/components/flux_led/util.py index 8db12cb6e32..2691b841952 100644 --- a/homeassistant/components/flux_led/util.py +++ b/homeassistant/components/flux_led/util.py @@ -1,4 +1,5 @@ """Utils for Magic Home.""" + from __future__ import annotations from flux_led.aio import AIOWifiLedBulb diff --git a/homeassistant/components/folder/sensor.py b/homeassistant/components/folder/sensor.py index 8c71208b745..c4454eba800 100644 --- a/homeassistant/components/folder/sensor.py +++ b/homeassistant/components/folder/sensor.py @@ -1,4 +1,5 @@ """Sensor for monitoring the contents of a folder.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/folder_watcher/__init__.py b/homeassistant/components/folder_watcher/__init__.py index 41a20360ff3..d111fe03c5c 100644 --- a/homeassistant/components/folder_watcher/__init__.py +++ b/homeassistant/components/folder_watcher/__init__.py @@ -1,4 +1,5 @@ """Component for monitoring activity on a folder.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/foobot/sensor.py b/homeassistant/components/foobot/sensor.py index 0af1206dbd3..ac8c7e3eec8 100644 --- a/homeassistant/components/foobot/sensor.py +++ b/homeassistant/components/foobot/sensor.py @@ -1,4 +1,5 @@ """Support for the Foobot indoor air quality monitor.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/forecast_solar/__init__.py b/homeassistant/components/forecast_solar/__init__.py index 1d28aad6a92..f4cb1d0a631 100644 --- a/homeassistant/components/forecast_solar/__init__.py +++ b/homeassistant/components/forecast_solar/__init__.py @@ -1,4 +1,5 @@ """The Forecast.Solar integration.""" + from __future__ import annotations from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/forecast_solar/config_flow.py b/homeassistant/components/forecast_solar/config_flow.py index 6066c85e74e..982f32eb07b 100644 --- a/homeassistant/components/forecast_solar/config_flow.py +++ b/homeassistant/components/forecast_solar/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Forecast.Solar integration.""" + from __future__ import annotations import re @@ -6,10 +7,14 @@ from typing import Any import voluptuous as vol -from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_validation as cv from .const import ( @@ -40,7 +45,7 @@ class ForecastSolarFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initiated by the user.""" if user_input is not None: return self.async_create_entry( @@ -92,7 +97,7 @@ class ForecastSolarOptionFlowHandler(OptionsFlow): async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Manage the options.""" errors = {} if user_input is not None: diff --git a/homeassistant/components/forecast_solar/const.py b/homeassistant/components/forecast_solar/const.py index 24273f32405..ac80b64b869 100644 --- a/homeassistant/components/forecast_solar/const.py +++ b/homeassistant/components/forecast_solar/const.py @@ -1,4 +1,5 @@ """Constants for the Forecast.Solar integration.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/forecast_solar/coordinator.py b/homeassistant/components/forecast_solar/coordinator.py index 2ef6912e5a2..1de5edddbef 100644 --- a/homeassistant/components/forecast_solar/coordinator.py +++ b/homeassistant/components/forecast_solar/coordinator.py @@ -1,4 +1,5 @@ """DataUpdateCoordinator for the Forecast.Solar integration.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/forecast_solar/diagnostics.py b/homeassistant/components/forecast_solar/diagnostics.py index 970747253df..a9bcebdb3cd 100644 --- a/homeassistant/components/forecast_solar/diagnostics.py +++ b/homeassistant/components/forecast_solar/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for Forecast.Solar integration.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/forecast_solar/energy.py b/homeassistant/components/forecast_solar/energy.py index b2e9b51473b..f4d03f26299 100644 --- a/homeassistant/components/forecast_solar/energy.py +++ b/homeassistant/components/forecast_solar/energy.py @@ -1,4 +1,5 @@ """Energy platform.""" + from __future__ import annotations from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/forecast_solar/sensor.py b/homeassistant/components/forecast_solar/sensor.py index 68a3fe81867..8d35b38765a 100644 --- a/homeassistant/components/forecast_solar/sensor.py +++ b/homeassistant/components/forecast_solar/sensor.py @@ -1,4 +1,5 @@ """Support for the Forecast.Solar sensor service.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/forked_daapd/__init__.py b/homeassistant/components/forked_daapd/__init__.py index 9dfb92c60c8..6ff9a030a02 100644 --- a/homeassistant/components/forked_daapd/__init__.py +++ b/homeassistant/components/forked_daapd/__init__.py @@ -1,4 +1,5 @@ """The forked_daapd component.""" + from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/forked_daapd/browse_media.py b/homeassistant/components/forked_daapd/browse_media.py index 79aa03774b7..f2c62b80234 100644 --- a/homeassistant/components/forked_daapd/browse_media.py +++ b/homeassistant/components/forked_daapd/browse_media.py @@ -1,4 +1,5 @@ """Browse media for forked-daapd.""" + from __future__ import annotations from collections.abc import Sequence diff --git a/homeassistant/components/forked_daapd/config_flow.py b/homeassistant/components/forked_daapd/config_flow.py index 84ddbbc7f0e..2440fc82943 100644 --- a/homeassistant/components/forked_daapd/config_flow.py +++ b/homeassistant/components/forked_daapd/config_flow.py @@ -1,15 +1,20 @@ """Config flow to configure forked-daapd devices.""" + from contextlib import suppress import logging from pyforked_daapd import ForkedDaapdAPI import voluptuous as vol -from homeassistant import config_entries from homeassistant.components import zeroconf +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import ( @@ -43,10 +48,10 @@ TEST_CONNECTION_ERROR_DICT = { } -class ForkedDaapdOptionsFlowHandler(config_entries.OptionsFlow): +class ForkedDaapdOptionsFlowHandler(OptionsFlow): """Handle a forked-daapd options flow.""" - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + def __init__(self, config_entry: ConfigEntry) -> None: """Initialize.""" self.config_entry = config_entry @@ -91,15 +96,15 @@ def fill_in_schema_dict(some_input): schema_dict = {} for field, _type in DATA_SCHEMA_DICT.items(): if some_input.get(str(field)): - schema_dict[ - vol.Optional(str(field), default=some_input[str(field)]) - ] = _type + schema_dict[vol.Optional(str(field), default=some_input[str(field)])] = ( + _type + ) else: schema_dict[field] = _type return schema_dict -class ForkedDaapdFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class ForkedDaapdFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a forked-daapd config flow.""" VERSION = 1 @@ -111,7 +116,7 @@ class ForkedDaapdFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback def async_get_options_flow( - config_entry: config_entries.ConfigEntry, + config_entry: ConfigEntry, ) -> ForkedDaapdOptionsFlowHandler: """Return options flow handler.""" return ForkedDaapdOptionsFlowHandler(config_entry) @@ -159,7 +164,7 @@ class ForkedDaapdFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_zeroconf( self, discovery_info: zeroconf.ZeroconfServiceInfo - ) -> FlowResult: + ) -> ConfigFlowResult: """Prepare configuration for a discovered forked-daapd device.""" version_num = 0 zeroconf_properties = discovery_info.properties diff --git a/homeassistant/components/forked_daapd/const.py b/homeassistant/components/forked_daapd/const.py index 686a9dbbde9..8d671f2fc07 100644 --- a/homeassistant/components/forked_daapd/const.py +++ b/homeassistant/components/forked_daapd/const.py @@ -1,4 +1,5 @@ """Const for forked-daapd.""" + from homeassistant.components.media_player import MediaPlayerEntityFeature, MediaType CALLBACK_TIMEOUT = 8 # max time between command and callback from forked-daapd server diff --git a/homeassistant/components/forked_daapd/media_player.py b/homeassistant/components/forked_daapd/media_player.py index df12de944ae..44596a448fc 100644 --- a/homeassistant/components/forked_daapd/media_player.py +++ b/homeassistant/components/forked_daapd/media_player.py @@ -1,4 +1,5 @@ """Support forked_daapd media player.""" + from __future__ import annotations import asyncio @@ -105,10 +106,9 @@ async def async_setup_entry( @callback def async_add_zones(api, outputs): - zone_entities = [] - for output in outputs: - zone_entities.append(ForkedDaapdZone(api, output, config_entry.entry_id)) - async_add_entities(zone_entities, False) + async_add_entities( + ForkedDaapdZone(api, output, config_entry.entry_id) for output in outputs + ) remove_add_zones_listener = async_dispatcher_connect( hass, SIGNAL_ADD_ZONES.format(config_entry.entry_id), async_add_zones @@ -127,9 +127,9 @@ async def async_setup_entry( forked_daapd_updater = ForkedDaapdUpdater( hass, forked_daapd_api, config_entry.entry_id ) - hass.data[DOMAIN][config_entry.entry_id][ - HASS_DATA_UPDATER_KEY - ] = forked_daapd_updater + hass.data[DOMAIN][config_entry.entry_id][HASS_DATA_UPDATER_KEY] = ( + forked_daapd_updater + ) await forked_daapd_updater.async_init() @@ -432,17 +432,16 @@ class ForkedDaapdMaster(MediaPlayerEntity): # restore state await self.api.set_volume(volume=self._last_volume * 100) if self._last_outputs: - futures: list[asyncio.Task[int]] = [] - for output in self._last_outputs: - futures.append( - asyncio.create_task( - self.api.change_output( - output["id"], - selected=output["selected"], - volume=output["volume"], - ) + futures: list[asyncio.Task[int]] = [ + asyncio.create_task( + self.api.change_output( + output["id"], + selected=output["selected"], + volume=output["volume"], ) ) + for output in self._last_outputs + ] await asyncio.wait(futures) else: # enable all outputs await self.api.set_enabled_outputs( @@ -650,15 +649,14 @@ class ForkedDaapdMaster(MediaPlayerEntity): self._last_outputs = self._outputs if self._outputs: await self.api.set_volume(volume=self._tts_volume * 100) - futures = [] - for output in self._outputs: - futures.append( - asyncio.create_task( - self.api.change_output( - output["id"], selected=True, volume=self._tts_volume * 100 - ) + futures = [ + asyncio.create_task( + self.api.change_output( + output["id"], selected=True, volume=self._tts_volume * 100 ) ) + for output in self._outputs + ] await asyncio.wait(futures) async def _pause_and_wait_for_callback(self): @@ -958,9 +956,9 @@ class ForkedDaapdUpdater: if not {"outputs", "volume"}.isdisjoint(update_types): # update outputs if outputs := await self._api.get_request("outputs"): outputs = outputs["outputs"] - update_events[ - "outputs" - ] = asyncio.Event() # only for master, zones should ignore + update_events["outputs"] = ( + asyncio.Event() + ) # only for master, zones should ignore async_dispatcher_send( self.hass, SIGNAL_UPDATE_OUTPUTS.format(self._entry_id), diff --git a/homeassistant/components/fortios/device_tracker.py b/homeassistant/components/fortios/device_tracker.py index d941375c8a3..3169e9a842f 100644 --- a/homeassistant/components/fortios/device_tracker.py +++ b/homeassistant/components/fortios/device_tracker.py @@ -2,6 +2,7 @@ This component is part of the device_tracker platform. """ + from __future__ import annotations import logging diff --git a/homeassistant/components/foscam/camera.py b/homeassistant/components/foscam/camera.py index 6674bff81e0..45704515422 100644 --- a/homeassistant/components/foscam/camera.py +++ b/homeassistant/components/foscam/camera.py @@ -1,4 +1,5 @@ """Component providing basic support for Foscam IP cameras.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/foscam/config_flow.py b/homeassistant/components/foscam/config_flow.py index 8d19220130d..ab9bc32c6b0 100644 --- a/homeassistant/components/foscam/config_flow.py +++ b/homeassistant/components/foscam/config_flow.py @@ -1,4 +1,5 @@ """Config flow for foscam integration.""" + from libpyfoscam import FoscamCamera from libpyfoscam.foscam import ( ERROR_FOSCAM_AUTH, @@ -7,7 +8,7 @@ from libpyfoscam.foscam import ( ) import voluptuous as vol -from homeassistant import config_entries, exceptions +from homeassistant.config_entries import ConfigFlow from homeassistant.const import ( CONF_HOST, CONF_NAME, @@ -16,6 +17,7 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.data_entry_flow import AbortFlow +from homeassistant.exceptions import HomeAssistantError from .const import CONF_RTSP_PORT, CONF_STREAM, DOMAIN, LOGGER @@ -37,7 +39,7 @@ DATA_SCHEMA = vol.Schema( ) -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class FoscamConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for foscam.""" VERSION = 2 @@ -117,13 +119,13 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -class CannotConnect(exceptions.HomeAssistantError): +class CannotConnect(HomeAssistantError): """Error to indicate we cannot connect.""" -class InvalidAuth(exceptions.HomeAssistantError): +class InvalidAuth(HomeAssistantError): """Error to indicate there is invalid auth.""" -class InvalidResponse(exceptions.HomeAssistantError): +class InvalidResponse(HomeAssistantError): """Error to indicate there is invalid response.""" diff --git a/homeassistant/components/foscam/const.py b/homeassistant/components/foscam/const.py index d5ac0f5c567..38088cf3f6f 100644 --- a/homeassistant/components/foscam/const.py +++ b/homeassistant/components/foscam/const.py @@ -1,4 +1,5 @@ """Constants for Foscam component.""" + import logging LOGGER = logging.getLogger(__package__) diff --git a/homeassistant/components/foscam/entity.py b/homeassistant/components/foscam/entity.py index ebcd9574e32..e9d1bbbe176 100644 --- a/homeassistant/components/foscam/entity.py +++ b/homeassistant/components/foscam/entity.py @@ -1,4 +1,5 @@ """Component providing basic support for Foscam IP cameras.""" + from __future__ import annotations from homeassistant.const import ATTR_HW_VERSION, ATTR_MODEL, ATTR_SW_VERSION diff --git a/homeassistant/components/foursquare/__init__.py b/homeassistant/components/foursquare/__init__.py index fcf1434c2c2..c0eac33a6a8 100644 --- a/homeassistant/components/foursquare/__init__.py +++ b/homeassistant/components/foursquare/__init__.py @@ -1,11 +1,12 @@ """Support for the Foursquare (Swarm) API.""" + from http import HTTPStatus import logging import requests import voluptuous as vol -from homeassistant.components.http import HomeAssistantView +from homeassistant.components.http import KEY_HASS, HomeAssistantView from homeassistant.const import CONF_ACCESS_TOKEN from homeassistant.core import HomeAssistant, ServiceCall import homeassistant.helpers.config_validation as cv @@ -105,4 +106,4 @@ class FoursquarePushReceiver(HomeAssistantView): ) return self.json_message("Incorrect secret", HTTPStatus.BAD_REQUEST) - request.app["hass"].bus.async_fire(EVENT_PUSH, data) + request.app[KEY_HASS].bus.async_fire(EVENT_PUSH, data) diff --git a/homeassistant/components/foursquare/icons.json b/homeassistant/components/foursquare/icons.json new file mode 100644 index 00000000000..cf60ed9f247 --- /dev/null +++ b/homeassistant/components/foursquare/icons.json @@ -0,0 +1,5 @@ +{ + "services": { + "checkin": "mdi:map-marker" + } +} diff --git a/homeassistant/components/free_mobile/notify.py b/homeassistant/components/free_mobile/notify.py index 9a1d8c99e19..d888ceadb18 100644 --- a/homeassistant/components/free_mobile/notify.py +++ b/homeassistant/components/free_mobile/notify.py @@ -1,4 +1,5 @@ """Support for Free Mobile SMS platform.""" + from __future__ import annotations from http import HTTPStatus diff --git a/homeassistant/components/freebox/__init__.py b/homeassistant/components/freebox/__init__.py index bcfbfdbec28..90ebd53048a 100644 --- a/homeassistant/components/freebox/__init__.py +++ b/homeassistant/components/freebox/__init__.py @@ -1,4 +1,5 @@ """Support for Freebox devices (Freebox v6 and Freebox mini 4K).""" + from datetime import timedelta import logging diff --git a/homeassistant/components/freebox/alarm_control_panel.py b/homeassistant/components/freebox/alarm_control_panel.py index be3d88cf5b4..4c62b928dff 100644 --- a/homeassistant/components/freebox/alarm_control_panel.py +++ b/homeassistant/components/freebox/alarm_control_panel.py @@ -1,4 +1,5 @@ """Support for Freebox alarms.""" + from typing import Any from homeassistant.components.alarm_control_panel import ( @@ -38,14 +39,14 @@ async def async_setup_entry( """Set up alarm panel.""" router: FreeboxRouter = hass.data[DOMAIN][entry.unique_id] - alarm_entities: list[AlarmControlPanelEntity] = [] - - for node in router.home_devices.values(): - if node["category"] == FreeboxHomeCategory.ALARM: - alarm_entities.append(FreeboxAlarm(hass, router, node)) - - if alarm_entities: - async_add_entities(alarm_entities, True) + async_add_entities( + ( + FreeboxAlarm(hass, router, node) + for node in router.home_devices.values() + if node["category"] == FreeboxHomeCategory.ALARM + ), + True, + ) class FreeboxAlarm(FreeboxHomeEntity, AlarmControlPanelEntity): diff --git a/homeassistant/components/freebox/binary_sensor.py b/homeassistant/components/freebox/binary_sensor.py index ef7f1ea3899..a54930753a0 100644 --- a/homeassistant/components/freebox/binary_sensor.py +++ b/homeassistant/components/freebox/binary_sensor.py @@ -1,4 +1,5 @@ """Support for Freebox devices (Freebox v6 and Freebox mini 4K).""" + from __future__ import annotations import logging @@ -52,16 +53,17 @@ async def async_setup_entry( elif node["category"] == FreeboxHomeCategory.DWS: binary_entities.append(FreeboxDwsSensor(hass, router, node)) - for endpoint in node["show_endpoints"]: + binary_entities.extend( + FreeboxCoverSensor(hass, router, node) + for endpoint in node["show_endpoints"] if ( endpoint["name"] == "cover" and endpoint["ep_type"] == "signal" and endpoint.get("value") is not None - ): - binary_entities.append(FreeboxCoverSensor(hass, router, node)) + ) + ) - if binary_entities: - async_add_entities(binary_entities, True) + async_add_entities(binary_entities, True) class FreeboxHomeBinarySensor(FreeboxHomeEntity, BinarySensorEntity): diff --git a/homeassistant/components/freebox/button.py b/homeassistant/components/freebox/button.py index d1268fb91d2..79e3c98b8b7 100644 --- a/homeassistant/components/freebox/button.py +++ b/homeassistant/components/freebox/button.py @@ -1,4 +1,5 @@ """Support for Freebox devices (Freebox v6 and Freebox mini 4K).""" + from __future__ import annotations from collections.abc import Awaitable, Callable @@ -18,20 +19,13 @@ from .const import DOMAIN from .router import FreeboxRouter -@dataclass(frozen=True) -class FreeboxButtonRequiredKeysMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class FreeboxButtonEntityDescription(ButtonEntityDescription): + """Class describing Freebox button entities.""" async_press: Callable[[FreeboxRouter], Awaitable] -@dataclass(frozen=True) -class FreeboxButtonEntityDescription( - ButtonEntityDescription, FreeboxButtonRequiredKeysMixin -): - """Class describing Freebox button entities.""" - - BUTTON_DESCRIPTIONS: tuple[FreeboxButtonEntityDescription, ...] = ( FreeboxButtonEntityDescription( key="reboot", diff --git a/homeassistant/components/freebox/camera.py b/homeassistant/components/freebox/camera.py index 96b0f63a92e..879941af040 100644 --- a/homeassistant/components/freebox/camera.py +++ b/homeassistant/components/freebox/camera.py @@ -1,4 +1,5 @@ """Support for Freebox cameras.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/freebox/config_flow.py b/homeassistant/components/freebox/config_flow.py index 7441def7d4d..b790556b8e3 100644 --- a/homeassistant/components/freebox/config_flow.py +++ b/homeassistant/components/freebox/config_flow.py @@ -1,14 +1,14 @@ """Config flow to configure the Freebox integration.""" + import logging from typing import Any from freebox_api.exceptions import AuthorizationError, HttpRequestError import voluptuous as vol -from homeassistant import config_entries from homeassistant.components import zeroconf +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_PORT -from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN from .router import get_api, get_hosts_list_if_supported @@ -16,7 +16,7 @@ from .router import get_api, get_hosts_list_if_supported _LOGGER = logging.getLogger(__name__) -class FreeboxFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class FreeboxFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a config flow.""" VERSION = 1 @@ -27,7 +27,7 @@ class FreeboxFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initiated by the user.""" if user_input is None: return self.async_show_form( @@ -51,7 +51,7 @@ class FreeboxFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_link( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Attempt to link with the Freebox router. Given a configured host, will ask the user to press the button @@ -100,7 +100,7 @@ class FreeboxFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_zeroconf( self, discovery_info: zeroconf.ZeroconfServiceInfo - ) -> FlowResult: + ) -> ConfigFlowResult: """Initialize flow from zeroconf.""" zeroconf_properties = discovery_info.properties host = zeroconf_properties["api_domain"] diff --git a/homeassistant/components/freebox/const.py b/homeassistant/components/freebox/const.py index ef5cabda1b6..13be45926b4 100644 --- a/homeassistant/components/freebox/const.py +++ b/homeassistant/components/freebox/const.py @@ -1,4 +1,5 @@ """Freebox component constants.""" + from __future__ import annotations import enum diff --git a/homeassistant/components/freebox/device_tracker.py b/homeassistant/components/freebox/device_tracker.py index 663acdc1f15..0f5b7eb4837 100644 --- a/homeassistant/components/freebox/device_tracker.py +++ b/homeassistant/components/freebox/device_tracker.py @@ -1,4 +1,5 @@ """Support for Freebox devices (Freebox v6 and Freebox mini 4K).""" + from __future__ import annotations from datetime import datetime diff --git a/homeassistant/components/freebox/home_base.py b/homeassistant/components/freebox/home_base.py index 2d75494e281..129186fd50b 100644 --- a/homeassistant/components/freebox/home_base.py +++ b/homeassistant/components/freebox/home_base.py @@ -1,4 +1,5 @@ """Support for Freebox base features.""" + from __future__ import annotations from collections.abc import Callable @@ -128,9 +129,9 @@ class FreeboxHomeEntity(Entity): if self._remove_signal_update is not None: self._remove_signal_update() - def remove_signal_update(self, dispacher: Callable[[], None]) -> None: + def remove_signal_update(self, dispatcher: Callable[[], None]) -> None: """Register state update callback.""" - self._remove_signal_update = dispacher + self._remove_signal_update = dispatcher def get_value(self, ep_type: str, name: str): """Get the value.""" diff --git a/homeassistant/components/freebox/icons.json b/homeassistant/components/freebox/icons.json new file mode 100644 index 00000000000..81361d2c990 --- /dev/null +++ b/homeassistant/components/freebox/icons.json @@ -0,0 +1,5 @@ +{ + "services": { + "reboot": "mdi:restart" + } +} diff --git a/homeassistant/components/freebox/router.py b/homeassistant/components/freebox/router.py index 3b13fad0572..26b3e37beb3 100644 --- a/homeassistant/components/freebox/router.py +++ b/homeassistant/components/freebox/router.py @@ -1,4 +1,5 @@ """Represent the Freebox router and its devices and sensors.""" + from __future__ import annotations from collections.abc import Callable, Mapping diff --git a/homeassistant/components/freebox/sensor.py b/homeassistant/components/freebox/sensor.py index 4e7c3910c54..e5a0b8223a9 100644 --- a/homeassistant/components/freebox/sensor.py +++ b/homeassistant/components/freebox/sensor.py @@ -1,4 +1,5 @@ """Support for Freebox devices (Freebox v6 and Freebox mini 4K).""" + from __future__ import annotations import logging diff --git a/homeassistant/components/freebox/switch.py b/homeassistant/components/freebox/switch.py index 5b6dd494f0b..3ffa80429e8 100644 --- a/homeassistant/components/freebox/switch.py +++ b/homeassistant/components/freebox/switch.py @@ -1,4 +1,5 @@ """Support for Freebox Delta, Revolution and Mini 4K.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/freedns/__init__.py b/homeassistant/components/freedns/__init__.py index feb1fb9fed9..5338c0d0700 100644 --- a/homeassistant/components/freedns/__init__.py +++ b/homeassistant/components/freedns/__init__.py @@ -1,4 +1,5 @@ """Integrate with FreeDNS Dynamic DNS service at freedns.afraid.org.""" + import asyncio from datetime import datetime, timedelta import logging diff --git a/homeassistant/components/freedompro/__init__.py b/homeassistant/components/freedompro/__init__.py index 78871bc99bf..c14c2f5ae36 100644 --- a/homeassistant/components/freedompro/__init__.py +++ b/homeassistant/components/freedompro/__init__.py @@ -1,4 +1,5 @@ """Support for freedompro.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/freedompro/binary_sensor.py b/homeassistant/components/freedompro/binary_sensor.py index 3bba3439341..ccea5faf41f 100644 --- a/homeassistant/components/freedompro/binary_sensor.py +++ b/homeassistant/components/freedompro/binary_sensor.py @@ -1,4 +1,5 @@ """Support for Freedompro binary_sensor.""" + from typing import Any from homeassistant.components.binary_sensor import ( diff --git a/homeassistant/components/freedompro/climate.py b/homeassistant/components/freedompro/climate.py index 3bb62cb23fb..d534db7e858 100644 --- a/homeassistant/components/freedompro/climate.py +++ b/homeassistant/components/freedompro/climate.py @@ -1,4 +1,5 @@ """Support for Freedompro climate.""" + from __future__ import annotations import json diff --git a/homeassistant/components/freedompro/config_flow.py b/homeassistant/components/freedompro/config_flow.py index c1288e61406..f1dd9dbbf14 100644 --- a/homeassistant/components/freedompro/config_flow.py +++ b/homeassistant/components/freedompro/config_flow.py @@ -1,9 +1,12 @@ """Config flow to configure Freedompro.""" + from pyfreedompro import get_list import voluptuous as vol -from homeassistant import config_entries, core, exceptions +from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_API_KEY +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import aiohttp_client from .const import DOMAIN @@ -26,7 +29,7 @@ class Hub: ) -async def validate_input(hass: core.HomeAssistant, api_key): +async def validate_input(hass: HomeAssistant, api_key): """Validate api key.""" hub = Hub(hass, api_key) result = await hub.authenticate() @@ -37,7 +40,7 @@ async def validate_input(hass: core.HomeAssistant, api_key): raise CannotConnect -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class FreedomProConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow.""" VERSION = 1 @@ -65,9 +68,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -class CannotConnect(exceptions.HomeAssistantError): +class CannotConnect(HomeAssistantError): """Error to indicate we cannot connect.""" -class InvalidAuth(exceptions.HomeAssistantError): +class InvalidAuth(HomeAssistantError): """Error to indicate there is invalid auth.""" diff --git a/homeassistant/components/freedompro/coordinator.py b/homeassistant/components/freedompro/coordinator.py index c896f5ec203..ad76a9aaa65 100644 --- a/homeassistant/components/freedompro/coordinator.py +++ b/homeassistant/components/freedompro/coordinator.py @@ -1,4 +1,5 @@ """Freedompro data update coordinator.""" + from __future__ import annotations from datetime import timedelta @@ -35,7 +36,7 @@ class FreedomproDataUpdateCoordinator(DataUpdateCoordinator[list[dict[str, Any]] if result["state"]: self._devices = result["devices"] else: - raise UpdateFailed() + raise UpdateFailed result = await get_states( aiohttp_client.async_get_clientsession(self._hass), self._api_key diff --git a/homeassistant/components/freedompro/cover.py b/homeassistant/components/freedompro/cover.py index b57acfacb4f..06ad5c80b6a 100644 --- a/homeassistant/components/freedompro/cover.py +++ b/homeassistant/components/freedompro/cover.py @@ -1,4 +1,5 @@ """Support for Freedompro cover.""" + import json from typing import Any diff --git a/homeassistant/components/freedompro/fan.py b/homeassistant/components/freedompro/fan.py index 59eb50ebe4a..fe77398ece4 100644 --- a/homeassistant/components/freedompro/fan.py +++ b/homeassistant/components/freedompro/fan.py @@ -1,4 +1,5 @@ """Support for Freedompro fan.""" + from __future__ import annotations import json diff --git a/homeassistant/components/freedompro/light.py b/homeassistant/components/freedompro/light.py index 9df3679ad70..ab8df7ec9db 100644 --- a/homeassistant/components/freedompro/light.py +++ b/homeassistant/components/freedompro/light.py @@ -1,4 +1,5 @@ """Support for Freedompro light.""" + from __future__ import annotations import json diff --git a/homeassistant/components/freedompro/lock.py b/homeassistant/components/freedompro/lock.py index b1544d9b20d..c429ef6aa99 100644 --- a/homeassistant/components/freedompro/lock.py +++ b/homeassistant/components/freedompro/lock.py @@ -1,4 +1,5 @@ """Support for Freedompro lock.""" + import json from typing import Any diff --git a/homeassistant/components/freedompro/sensor.py b/homeassistant/components/freedompro/sensor.py index dc6861a4f0a..3c5101e3634 100644 --- a/homeassistant/components/freedompro/sensor.py +++ b/homeassistant/components/freedompro/sensor.py @@ -1,4 +1,5 @@ """Support for Freedompro sensor.""" + from typing import Any from homeassistant.components.sensor import ( diff --git a/homeassistant/components/freedompro/switch.py b/homeassistant/components/freedompro/switch.py index 4de27c270b4..91e67506173 100644 --- a/homeassistant/components/freedompro/switch.py +++ b/homeassistant/components/freedompro/switch.py @@ -1,4 +1,5 @@ """Support for Freedompro switch.""" + import json from typing import Any diff --git a/homeassistant/components/fritz/__init__.py b/homeassistant/components/fritz/__init__.py index 137aaa5ba2e..ba9e2191901 100644 --- a/homeassistant/components/fritz/__init__.py +++ b/homeassistant/components/fritz/__init__.py @@ -1,4 +1,5 @@ """Support for AVM Fritz!Box functions.""" + import logging from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/fritz/binary_sensor.py b/homeassistant/components/fritz/binary_sensor.py index f703fadb4b8..adca977e179 100644 --- a/homeassistant/components/fritz/binary_sensor.py +++ b/homeassistant/components/fritz/binary_sensor.py @@ -1,4 +1,5 @@ """AVM FRITZ!Box connectivity sensor.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/fritz/button.py b/homeassistant/components/fritz/button.py index de34056b0d7..d56350dd1d0 100644 --- a/homeassistant/components/fritz/button.py +++ b/homeassistant/components/fritz/button.py @@ -1,4 +1,5 @@ """Switches for AVM Fritz!Box buttons.""" + from __future__ import annotations from collections.abc import Callable @@ -23,18 +24,13 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -@dataclass(frozen=True) -class FritzButtonDescriptionMixin: - """Mixin to describe a Button entity.""" +@dataclass(frozen=True, kw_only=True) +class FritzButtonDescription(ButtonEntityDescription): + """Class to describe a Button entity.""" press_action: Callable -@dataclass(frozen=True) -class FritzButtonDescription(ButtonEntityDescription, FritzButtonDescriptionMixin): - """Class to describe a Button entity.""" - - BUTTONS: Final = [ FritzButtonDescription( key="firmware_update", diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py index 3d287b57384..5815f9abfc1 100644 --- a/homeassistant/components/fritz/common.py +++ b/homeassistant/components/fritz/common.py @@ -1,4 +1,5 @@ """Support for AVM FRITZ!Box classes.""" + from __future__ import annotations from collections.abc import Callable, ValuesView @@ -63,10 +64,7 @@ _LOGGER = logging.getLogger(__name__) def _is_tracked(mac: str, current_devices: ValuesView) -> bool: """Check if device is already tracked.""" - for tracked in current_devices: - if mac in tracked: - return True - return False + return any(mac in tracked for tracked in current_devices) def device_filter_out_from_trackers( @@ -313,6 +311,17 @@ class FritzBoxTools( ) return unregister_entity_updates + def _entity_states_update(self) -> dict: + """Run registered entity update calls.""" + entity_states = {} + for key in list(self._entity_update_functions): + if (update_fn := self._entity_update_functions.get(key)) is not None: + _LOGGER.debug("update entity %s", key) + entity_states[key] = update_fn( + self.fritz_status, self.data["entity_states"].get(key) + ) + return entity_states + async def _async_update_data(self) -> UpdateCoordinatorDataType: """Update FritzboxTools data.""" entity_data: UpdateCoordinatorDataType = { @@ -321,15 +330,9 @@ class FritzBoxTools( } try: await self.async_scan_devices() - for key in list(self._entity_update_functions): - _LOGGER.debug("update entity %s", key) - entity_data["entity_states"][ - key - ] = await self.hass.async_add_executor_job( - self._entity_update_functions[key], - self.fritz_status, - self.data["entity_states"].get(key), - ) + entity_data["entity_states"] = await self.hass.async_add_executor_job( + self._entity_states_update + ) if self.has_call_deflections: entity_data[ "call_deflections" @@ -344,21 +347,21 @@ class FritzBoxTools( def unique_id(self) -> str: """Return unique id.""" if not self._unique_id: - raise ClassSetupMissing() + raise ClassSetupMissing return self._unique_id @property def model(self) -> str: """Return device model.""" if not self._model: - raise ClassSetupMissing() + raise ClassSetupMissing return self._model @property def current_firmware(self) -> str: """Return current SW version.""" if not self._current_firmware: - raise ClassSetupMissing() + raise ClassSetupMissing return self._current_firmware @property @@ -380,7 +383,7 @@ class FritzBoxTools( def mac(self) -> str: """Return device Mac address.""" if not self._unique_id: - raise ClassSetupMissing() + raise ClassSetupMissing return dr.format_mac(self._unique_id) @property @@ -968,7 +971,7 @@ class FritzDeviceBase(update_coordinator.CoordinatorEntity[AvmWrapper]): async def async_process_update(self) -> None: """Update device.""" - raise NotImplementedError() + raise NotImplementedError async def async_on_demand_update(self) -> None: """Update state.""" diff --git a/homeassistant/components/fritz/config_flow.py b/homeassistant/components/fritz/config_flow.py index 03bcc3b77f7..a217adf935c 100644 --- a/homeassistant/components/fritz/config_flow.py +++ b/homeassistant/components/fritz/config_flow.py @@ -1,4 +1,5 @@ """Config flow to configure the FRITZ!Box Tools integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -20,12 +21,12 @@ from homeassistant.components.device_tracker import ( from homeassistant.config_entries import ( ConfigEntry, ConfigFlow, + ConfigFlowResult, OptionsFlow, OptionsFlowWithConfigEntry, ) from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult from .const import ( CONF_OLD_DISCOVERY, @@ -110,7 +111,7 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN): return None @callback - def _async_create_entry(self) -> FlowResult: + def _async_create_entry(self) -> ConfigFlowResult: """Async create flow handler entry.""" return self.async_create_entry( title=self._name, @@ -126,7 +127,9 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN): }, ) - async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: + async def async_step_ssdp( + self, discovery_info: ssdp.SsdpServiceInfo + ) -> ConfigFlowResult: """Handle a flow initialized by discovery.""" ssdp_location: ParseResult = urlparse(discovery_info.ssdp_location or "") self._host = ssdp_location.hostname @@ -166,7 +169,7 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle user-confirmation of discovered node.""" if user_input is None: return self._show_setup_form_confirm() @@ -184,7 +187,9 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN): return self._async_create_entry() - def _show_setup_form_init(self, errors: dict[str, str] | None = None) -> FlowResult: + def _show_setup_form_init( + self, errors: dict[str, str] | None = None + ) -> ConfigFlowResult: """Show the setup form to the user.""" return self.async_show_form( step_id="user", @@ -201,7 +206,7 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN): def _show_setup_form_confirm( self, errors: dict[str, str] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Show the setup form to the user.""" return self.async_show_form( step_id="confirm", @@ -217,7 +222,7 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initiated by the user.""" if user_input is None: return self._show_setup_form_init() @@ -237,7 +242,9 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN): return self._async_create_entry() - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Handle flow upon an API authentication error.""" self._entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) self._host = entry_data[CONF_HOST] @@ -248,7 +255,7 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN): def _show_setup_form_reauth_confirm( self, user_input: dict[str, Any], errors: dict[str, str] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Show the reauth form to the user.""" default_username = user_input.get(CONF_USERNAME) return self.async_show_form( @@ -265,7 +272,7 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Dialog that informs the user that reauth is required.""" if user_input is None: return self._show_setup_form_reauth_confirm( @@ -299,7 +306,7 @@ class FritzBoxToolsOptionsFlowHandler(OptionsFlowWithConfigEntry): async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle options flow.""" if user_input is not None: diff --git a/homeassistant/components/fritz/device_tracker.py b/homeassistant/components/fritz/device_tracker.py index d4ba53aa6a2..89ba6c1cad8 100644 --- a/homeassistant/components/fritz/device_tracker.py +++ b/homeassistant/components/fritz/device_tracker.py @@ -1,4 +1,5 @@ """Support for FRITZ!Box devices.""" + from __future__ import annotations import datetime diff --git a/homeassistant/components/fritz/diagnostics.py b/homeassistant/components/fritz/diagnostics.py index 0322e55a9e0..3136f03f95b 100644 --- a/homeassistant/components/fritz/diagnostics.py +++ b/homeassistant/components/fritz/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for AVM FRITZ!Box.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/fritz/sensor.py b/homeassistant/components/fritz/sensor.py index 7fcc4944ec5..aa9c410a545 100644 --- a/homeassistant/components/fritz/sensor.py +++ b/homeassistant/components/fritz/sensor.py @@ -1,4 +1,5 @@ """AVM FRITZ!Box binary sensors.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/fritz/services.py b/homeassistant/components/fritz/services.py index e0b41d0e87e..47fb0ceb1c6 100644 --- a/homeassistant/components/fritz/services.py +++ b/homeassistant/components/fritz/services.py @@ -1,4 +1,5 @@ """Services for Fritz integration.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/fritz/switch.py b/homeassistant/components/fritz/switch.py index c3da6b5af0b..913d0165247 100644 --- a/homeassistant/components/fritz/switch.py +++ b/homeassistant/components/fritz/switch.py @@ -1,4 +1,5 @@ """Switches for AVM Fritz!Box functions.""" + from __future__ import annotations import logging @@ -288,7 +289,7 @@ class FritzBoxBaseCoordinatorSwitch(CoordinatorEntity[AvmWrapper], SwitchEntity) @property def data(self) -> dict[str, Any]: """Return entity data from coordinator data.""" - raise NotImplementedError() + raise NotImplementedError @property def available(self) -> bool: @@ -297,7 +298,7 @@ class FritzBoxBaseCoordinatorSwitch(CoordinatorEntity[AvmWrapper], SwitchEntity) async def _async_handle_turn_on_off(self, turn_on: bool) -> None: """Handle switch state change request.""" - raise NotImplementedError() + raise NotImplementedError async def async_turn_on(self, **kwargs: Any) -> None: """Turn on switch.""" diff --git a/homeassistant/components/fritz/update.py b/homeassistant/components/fritz/update.py index fafd9c37ab8..1a24a8dd152 100644 --- a/homeassistant/components/fritz/update.py +++ b/homeassistant/components/fritz/update.py @@ -1,4 +1,5 @@ """Support for AVM FRITZ!Box update platform.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/homeassistant/components/fritzbox/__init__.py b/homeassistant/components/fritzbox/__init__.py index 8cb41ebcbe1..7f4006768c4 100644 --- a/homeassistant/components/fritzbox/__init__.py +++ b/homeassistant/components/fritzbox/__init__.py @@ -1,4 +1,5 @@ """Support for AVM FRITZ!SmartHome devices.""" + from __future__ import annotations from abc import ABC, abstractmethod diff --git a/homeassistant/components/fritzbox/binary_sensor.py b/homeassistant/components/fritzbox/binary_sensor.py index c6676bb1bbf..08fddc8a0ae 100644 --- a/homeassistant/components/fritzbox/binary_sensor.py +++ b/homeassistant/components/fritzbox/binary_sensor.py @@ -1,4 +1,5 @@ """Support for Fritzbox binary sensors.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/fritzbox/button.py b/homeassistant/components/fritzbox/button.py index 6695c564331..f3ea03f91b2 100644 --- a/homeassistant/components/fritzbox/button.py +++ b/homeassistant/components/fritzbox/button.py @@ -1,4 +1,5 @@ """Support for AVM FRITZ!SmartHome templates.""" + from pyfritzhome.devicetypes import FritzhomeTemplate from homeassistant.components.button import ButtonEntity diff --git a/homeassistant/components/fritzbox/climate.py b/homeassistant/components/fritzbox/climate.py index 8dc19c199a3..17accf35819 100644 --- a/homeassistant/components/fritzbox/climate.py +++ b/homeassistant/components/fritzbox/climate.py @@ -1,4 +1,5 @@ """Support for AVM FRITZ!SmartHome thermostat devices.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/fritzbox/config_flow.py b/homeassistant/components/fritzbox/config_flow.py index b4e86b92568..e32f27969a1 100644 --- a/homeassistant/components/fritzbox/config_flow.py +++ b/homeassistant/components/fritzbox/config_flow.py @@ -1,4 +1,5 @@ """Config flow for AVM FRITZ!SmartHome.""" + from __future__ import annotations from collections.abc import Mapping @@ -11,9 +12,8 @@ from requests.exceptions import HTTPError import voluptuous as vol from homeassistant.components import ssdp -from homeassistant.config_entries import ConfigEntry, ConfigFlow +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME -from homeassistant.data_entry_flow import FlowResult from .const import DEFAULT_HOST, DEFAULT_USERNAME, DOMAIN @@ -51,7 +51,7 @@ class FritzboxConfigFlow(ConfigFlow, domain=DOMAIN): self._password: str | None = None self._username: str | None = None - def _get_entry(self, name: str) -> FlowResult: + def _get_entry(self, name: str) -> ConfigFlowResult: return self.async_create_entry( title=name, data={ @@ -92,7 +92,7 @@ class FritzboxConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" errors = {} @@ -116,7 +116,9 @@ class FritzboxConfigFlow(ConfigFlow, domain=DOMAIN): step_id="user", data_schema=DATA_SCHEMA_USER, errors=errors ) - async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: + async def async_step_ssdp( + self, discovery_info: ssdp.SsdpServiceInfo + ) -> ConfigFlowResult: """Handle a flow initialized by discovery.""" host = urlparse(discovery_info.ssdp_location).hostname assert isinstance(host, str) @@ -139,7 +141,7 @@ class FritzboxConfigFlow(ConfigFlow, domain=DOMAIN): return self.async_abort(reason="already_in_progress") # update old and user-configured config entries - for entry in self._async_current_entries(): + for entry in self._async_current_entries(include_ignore=False): if entry.data[CONF_HOST] == host: if uuid and not entry.unique_id: self.hass.config_entries.async_update_entry(entry, unique_id=uuid) @@ -153,7 +155,7 @@ class FritzboxConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle user-confirmation of discovered node.""" errors = {} @@ -176,7 +178,9 @@ class FritzboxConfigFlow(ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Trigger a reauthentication flow.""" entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) assert entry is not None @@ -189,7 +193,7 @@ class FritzboxConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle reauthorization flow.""" errors = {} diff --git a/homeassistant/components/fritzbox/const.py b/homeassistant/components/fritzbox/const.py index 791da4540a4..d664bd3a8d4 100644 --- a/homeassistant/components/fritzbox/const.py +++ b/homeassistant/components/fritzbox/const.py @@ -1,4 +1,5 @@ """Constants for the AVM FRITZ!SmartHome integration.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/fritzbox/coordinator.py b/homeassistant/components/fritzbox/coordinator.py index f6d210e367a..c58665f2b5d 100644 --- a/homeassistant/components/fritzbox/coordinator.py +++ b/homeassistant/components/fritzbox/coordinator.py @@ -1,4 +1,5 @@ """Data update coordinator for AVM FRITZ!SmartHome devices.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/homeassistant/components/fritzbox/cover.py b/homeassistant/components/fritzbox/cover.py index 4c2ba76c377..bd80b5f4af1 100644 --- a/homeassistant/components/fritzbox/cover.py +++ b/homeassistant/components/fritzbox/cover.py @@ -1,4 +1,5 @@ """Support for AVM FRITZ!SmartHome cover devices.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/fritzbox/diagnostics.py b/homeassistant/components/fritzbox/diagnostics.py index 6c50e1311df..93e560e3117 100644 --- a/homeassistant/components/fritzbox/diagnostics.py +++ b/homeassistant/components/fritzbox/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for AVM Fritz!Smarthome.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/fritzbox/light.py b/homeassistant/components/fritzbox/light.py index 6c06f2cc699..dbc09beb235 100644 --- a/homeassistant/components/fritzbox/light.py +++ b/homeassistant/components/fritzbox/light.py @@ -1,4 +1,5 @@ """Support for AVM FRITZ!SmartHome lightbulbs.""" + from __future__ import annotations from typing import Any, cast diff --git a/homeassistant/components/fritzbox/model.py b/homeassistant/components/fritzbox/model.py index 74c5bd42927..f0353bc58d6 100644 --- a/homeassistant/components/fritzbox/model.py +++ b/homeassistant/components/fritzbox/model.py @@ -1,4 +1,5 @@ """Models for the AVM FRITZ!SmartHome integration.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/fritzbox/sensor.py b/homeassistant/components/fritzbox/sensor.py index fd55369d915..29f61d6e466 100644 --- a/homeassistant/components/fritzbox/sensor.py +++ b/homeassistant/components/fritzbox/sensor.py @@ -1,4 +1,5 @@ """Support for AVM FRITZ!SmartHome temperature sensor only devices.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/fritzbox/switch.py b/homeassistant/components/fritzbox/switch.py index 4d93cddb617..b7ad08785f4 100644 --- a/homeassistant/components/fritzbox/switch.py +++ b/homeassistant/components/fritzbox/switch.py @@ -1,4 +1,5 @@ """Support for AVM FRITZ!SmartHome switch devices.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/fritzbox_callmonitor/__init__.py b/homeassistant/components/fritzbox_callmonitor/__init__.py index 534f3b0c8fc..bd6b6ab125f 100644 --- a/homeassistant/components/fritzbox_callmonitor/__init__.py +++ b/homeassistant/components/fritzbox_callmonitor/__init__.py @@ -1,4 +1,5 @@ """The fritzbox_callmonitor integration.""" + import logging from fritzconnection.core.exceptions import FritzConnectionException, FritzSecurityError diff --git a/homeassistant/components/fritzbox_callmonitor/base.py b/homeassistant/components/fritzbox_callmonitor/base.py index df19bca7b13..72d17b57abc 100644 --- a/homeassistant/components/fritzbox_callmonitor/base.py +++ b/homeassistant/components/fritzbox_callmonitor/base.py @@ -1,4 +1,5 @@ """Base class for fritzbox_callmonitor entities.""" + from __future__ import annotations from contextlib import suppress diff --git a/homeassistant/components/fritzbox_callmonitor/config_flow.py b/homeassistant/components/fritzbox_callmonitor/config_flow.py index 5065aa65b4d..ac0d3ea3337 100644 --- a/homeassistant/components/fritzbox_callmonitor/config_flow.py +++ b/homeassistant/components/fritzbox_callmonitor/config_flow.py @@ -1,4 +1,5 @@ """Config flow for fritzbox_callmonitor.""" + from __future__ import annotations from enum import StrEnum @@ -9,7 +10,13 @@ from fritzconnection.core.exceptions import FritzConnectionException, FritzSecur from requests.exceptions import ConnectionError as RequestsConnectionError import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ( + SOURCE_IMPORT, + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.const import ( CONF_HOST, CONF_NAME, @@ -18,7 +25,6 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult from .base import FritzBoxPhonebook from .const import ( @@ -54,7 +60,7 @@ class ConnectResult(StrEnum): SUCCESS = "success" -class FritzBoxCallMonitorConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class FritzBoxCallMonitorConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a fritzbox_callmonitor config flow.""" VERSION = 1 @@ -73,7 +79,7 @@ class FritzBoxCallMonitorConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Initialize flow.""" self._phonebook_names: list[str] | None = None - def _get_config_entry(self) -> FlowResult: + def _get_config_entry(self) -> ConfigFlowResult: """Create and return an config entry.""" return self.async_create_entry( title=self._phonebook_name, @@ -122,24 +128,22 @@ class FritzBoxCallMonitorConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def _get_list_of_phonebook_names(self) -> list[str]: """Return list of names for all available phonebooks.""" - phonebook_names: list[str] = [] - - for phonebook_id in self._phonebook_ids: - phonebook_names.append(await self._get_name_of_phonebook(phonebook_id)) - - return phonebook_names + return [ + await self._get_name_of_phonebook(phonebook_id) + for phonebook_id in self._phonebook_ids + ] @staticmethod @callback def async_get_options_flow( - config_entry: config_entries.ConfigEntry, + config_entry: ConfigEntry, ) -> FritzBoxCallMonitorOptionsFlowHandler: """Get the options flow for this handler.""" return FritzBoxCallMonitorOptionsFlowHandler(config_entry) async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" if user_input is None: @@ -164,7 +168,7 @@ class FritzBoxCallMonitorConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if result != ConnectResult.SUCCESS: return self.async_abort(reason=result) - if self.context["source"] == config_entries.SOURCE_IMPORT: + if self.context["source"] == SOURCE_IMPORT: self._phonebook_id = user_input[CONF_PHONEBOOK] self._phonebook_name = user_input[CONF_NAME] @@ -182,7 +186,7 @@ class FritzBoxCallMonitorConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_phonebook( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow to chose one of multiple available phonebooks.""" if self._phonebook_names is None: @@ -206,10 +210,10 @@ class FritzBoxCallMonitorConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self._get_config_entry() -class FritzBoxCallMonitorOptionsFlowHandler(config_entries.OptionsFlow): +class FritzBoxCallMonitorOptionsFlowHandler(OptionsFlow): """Handle a fritzbox_callmonitor options flow.""" - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + def __init__(self, config_entry: ConfigEntry) -> None: """Initialize.""" self.config_entry = config_entry @@ -240,7 +244,7 @@ class FritzBoxCallMonitorOptionsFlowHandler(config_entries.OptionsFlow): async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Manage the options.""" option_schema_prefixes = self._get_option_schema_prefixes() diff --git a/homeassistant/components/fritzbox_callmonitor/const.py b/homeassistant/components/fritzbox_callmonitor/const.py index a13a86574df..406a1dd6d64 100644 --- a/homeassistant/components/fritzbox_callmonitor/const.py +++ b/homeassistant/components/fritzbox_callmonitor/const.py @@ -1,4 +1,5 @@ """Constants for the AVM Fritz!Box call monitor integration.""" + from enum import StrEnum from typing import Final diff --git a/homeassistant/components/fritzbox_callmonitor/sensor.py b/homeassistant/components/fritzbox_callmonitor/sensor.py index 036c9605d0a..0a127ec36b3 100644 --- a/homeassistant/components/fritzbox_callmonitor/sensor.py +++ b/homeassistant/components/fritzbox_callmonitor/sensor.py @@ -1,4 +1,5 @@ """Sensor to monitor incoming/outgoing phone calls on a Fritz!Box router.""" + from __future__ import annotations from collections.abc import Mapping diff --git a/homeassistant/components/fronius/__init__.py b/homeassistant/components/fronius/__init__.py index d0e13aa7914..1928bb15bc2 100644 --- a/homeassistant/components/fronius/__init__.py +++ b/homeassistant/components/fronius/__init__.py @@ -1,4 +1,5 @@ """The Fronius integration.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/fronius/config_flow.py b/homeassistant/components/fronius/config_flow.py index 15b8cd7a3b8..2b46d226b7a 100644 --- a/homeassistant/components/fronius/config_flow.py +++ b/homeassistant/components/fronius/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Fronius integration.""" + from __future__ import annotations import asyncio @@ -8,11 +9,10 @@ from typing import Any, Final from pyfronius import Fronius, FroniusError import voluptuous as vol -from homeassistant import config_entries from homeassistant.components.dhcp import DhcpServiceInfo +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -73,7 +73,7 @@ async def validate_host( ) -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class FroniusConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Fronius.""" VERSION = 1 @@ -84,7 +84,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" if user_input is None: return self.async_show_form( @@ -110,10 +110,12 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors ) - async def async_step_dhcp(self, discovery_info: DhcpServiceInfo) -> FlowResult: + async def async_step_dhcp( + self, discovery_info: DhcpServiceInfo + ) -> ConfigFlowResult: """Handle a flow initiated by the DHCP client.""" for entry in self._async_current_entries(include_ignore=False): - if entry.data[CONF_HOST].lstrip("http://").rstrip("/").lower() in ( + if entry.data[CONF_HOST].removeprefix("http://").rstrip("/").lower() in ( discovery_info.ip, discovery_info.hostname, ): @@ -133,7 +135,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_confirm_discovery( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Attempt to confirm.""" title = create_title(self.info) if user_input is not None: diff --git a/homeassistant/components/fronius/const.py b/homeassistant/components/fronius/const.py index 18f35de8336..8702339ef03 100644 --- a/homeassistant/components/fronius/const.py +++ b/homeassistant/components/fronius/const.py @@ -1,4 +1,5 @@ """Constants for the Fronius integration.""" + from enum import StrEnum from typing import Final, NamedTuple, TypedDict diff --git a/homeassistant/components/fronius/coordinator.py b/homeassistant/components/fronius/coordinator.py index fcf9ce0a389..d0a20b25bee 100644 --- a/homeassistant/components/fronius/coordinator.py +++ b/homeassistant/components/fronius/coordinator.py @@ -1,4 +1,5 @@ """DataUpdateCoordinators for the Fronius integration.""" + from __future__ import annotations from abc import ABC, abstractmethod @@ -77,9 +78,9 @@ class FroniusCoordinatorBase( for solar_net_id in data: if solar_net_id not in self.unregistered_descriptors: # id seen for the first time - self.unregistered_descriptors[ - solar_net_id - ] = self.valid_descriptions.copy() + self.unregistered_descriptors[solar_net_id] = ( + self.valid_descriptions.copy() + ) return data @callback @@ -114,9 +115,9 @@ class FroniusCoordinatorBase( solar_net_id=solar_net_id, ) ) - self.unregistered_descriptors[ - solar_net_id - ] = remaining_unregistered_descriptors + self.unregistered_descriptors[solar_net_id] = ( + remaining_unregistered_descriptors + ) async_add_entities(new_entities) _add_entities_for_unregistered_descriptors() diff --git a/homeassistant/components/fronius/sensor.py b/homeassistant/components/fronius/sensor.py index 2fa4e4fd160..2d79086d8ba 100644 --- a/homeassistant/components/fronius/sensor.py +++ b/homeassistant/components/fronius/sensor.py @@ -1,4 +1,5 @@ """Support for Fronius devices.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 48d5bcb0b05..2f64a019c19 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -1,4 +1,5 @@ """Handle the frontend for Home Assistant.""" + from __future__ import annotations from collections.abc import Iterator @@ -14,7 +15,7 @@ import voluptuous as vol from yarl import URL from homeassistant.components import onboarding, websocket_api -from homeassistant.components.http.view import HomeAssistantView +from homeassistant.components.http import KEY_HASS, HomeAssistantView from homeassistant.components.websocket_api.connection import ActiveConnection from homeassistant.config import async_hass_config_yaml from homeassistant.const import ( @@ -595,7 +596,7 @@ class IndexView(web_urldispatcher.AbstractResource): async def get(self, request: web.Request) -> web.Response: """Serve the index page for panel pages.""" - hass: HomeAssistant = request.app["hass"] + hass = request.app[KEY_HASS] if not onboarding.async_is_onboarded(hass): return web.Response(status=302, headers={"location": "/onboarding.html"}) diff --git a/homeassistant/components/frontend/icons.json b/homeassistant/components/frontend/icons.json new file mode 100644 index 00000000000..9fbe4d5b9b0 --- /dev/null +++ b/homeassistant/components/frontend/icons.json @@ -0,0 +1,6 @@ +{ + "services": { + "set_theme": "mdi:palette-swatch", + "reload_themes": "mdi:reload" + } +} diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index f49a632cae2..1890572bf5a 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20240307.0"] + "requirements": ["home-assistant-frontend==20240403.1"] } diff --git a/homeassistant/components/frontend/storage.py b/homeassistant/components/frontend/storage.py index 91646dcb745..d387e14b085 100644 --- a/homeassistant/components/frontend/storage.py +++ b/homeassistant/components/frontend/storage.py @@ -1,4 +1,5 @@ """API for persistent storage for the frontend.""" + from __future__ import annotations from collections.abc import Callable, Coroutine diff --git a/homeassistant/components/frontier_silicon/__init__.py b/homeassistant/components/frontier_silicon/__init__.py index f1e0ad48d30..325af100005 100644 --- a/homeassistant/components/frontier_silicon/__init__.py +++ b/homeassistant/components/frontier_silicon/__init__.py @@ -1,4 +1,5 @@ """The Frontier Silicon integration.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/frontier_silicon/browse_media.py b/homeassistant/components/frontier_silicon/browse_media.py index cc4452b2d6b..da5169b8e7c 100644 --- a/homeassistant/components/frontier_silicon/browse_media.py +++ b/homeassistant/components/frontier_silicon/browse_media.py @@ -1,4 +1,5 @@ """Support for media browsing.""" + import logging from afsapi import AFSAPI, FSApiException, OutOfRangeException, Preset diff --git a/homeassistant/components/frontier_silicon/config_flow.py b/homeassistant/components/frontier_silicon/config_flow.py index 470be7d9b26..a9c87cd9d4a 100644 --- a/homeassistant/components/frontier_silicon/config_flow.py +++ b/homeassistant/components/frontier_silicon/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Frontier Silicon Media Player integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -14,10 +15,9 @@ from afsapi import ( ) import voluptuous as vol -from homeassistant import config_entries from homeassistant.components import ssdp +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_PIN, CONF_PORT -from homeassistant.data_entry_flow import FlowResult from .const import ( CONF_WEBFSAPI_URL, @@ -51,18 +51,18 @@ def hostname_from_url(url: str) -> str: return str(urlparse(url).hostname) -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class FrontierSiliconConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Frontier Silicon Media Player.""" VERSION = 1 _name: str _webfsapi_url: str - _reauth_entry: config_entries.ConfigEntry | None = None # Only used in reauth flows + _reauth_entry: ConfigEntry | None = None # Only used in reauth flows async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step of manual configuration.""" errors = {} @@ -87,7 +87,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="user", data_schema=data_schema, errors=errors ) - async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: + async def async_step_ssdp( + self, discovery_info: ssdp.SsdpServiceInfo + ) -> ConfigFlowResult: """Process entity discovered via SSDP.""" device_url = discovery_info.ssdp_location @@ -131,7 +133,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_confirm() - async def _async_step_device_config_if_needed(self) -> FlowResult: + async def _async_step_device_config_if_needed(self) -> ConfigFlowResult: """Most users will not have changed the default PIN on their radio. We try to use this default PIN, and only if this fails ask for it via `async_step_device_config` @@ -159,7 +161,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Allow the user to confirm adding the device. Used when the default PIN could successfully be used.""" if user_input is not None: @@ -170,7 +172,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="confirm", description_placeholders={"name": self._name} ) - async def async_step_reauth(self, config: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth(self, config: Mapping[str, Any]) -> ConfigFlowResult: """Perform reauth upon an API authentication error.""" self._webfsapi_url = config[CONF_WEBFSAPI_URL] @@ -182,7 +184,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_device_config( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle device configuration step. We ask for the PIN in this step. diff --git a/homeassistant/components/frontier_silicon/const.py b/homeassistant/components/frontier_silicon/const.py index 94f4e09a35a..ce0126aa803 100644 --- a/homeassistant/components/frontier_silicon/const.py +++ b/homeassistant/components/frontier_silicon/const.py @@ -1,4 +1,5 @@ """Constants for the Frontier Silicon Media Player integration.""" + DOMAIN = "frontier_silicon" CONF_WEBFSAPI_URL = "webfsapi_url" diff --git a/homeassistant/components/frontier_silicon/media_player.py b/homeassistant/components/frontier_silicon/media_player.py index 223abe26e55..ac72df67014 100644 --- a/homeassistant/components/frontier_silicon/media_player.py +++ b/homeassistant/components/frontier_silicon/media_player.py @@ -1,4 +1,5 @@ """Support for Frontier Silicon Devices (Medion, Hama, Auna,...).""" + from __future__ import annotations import logging diff --git a/homeassistant/components/fully_kiosk/__init__.py b/homeassistant/components/fully_kiosk/__init__.py index 8b350433858..a0ed0cb4fa0 100644 --- a/homeassistant/components/fully_kiosk/__init__.py +++ b/homeassistant/components/fully_kiosk/__init__.py @@ -1,4 +1,5 @@ """The Fully Kiosk Browser integration.""" + from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/fully_kiosk/binary_sensor.py b/homeassistant/components/fully_kiosk/binary_sensor.py index 5eebf8a77ab..3cf9adea1d5 100644 --- a/homeassistant/components/fully_kiosk/binary_sensor.py +++ b/homeassistant/components/fully_kiosk/binary_sensor.py @@ -1,4 +1,5 @@ """Fully Kiosk Browser sensor.""" + from __future__ import annotations from homeassistant.components.binary_sensor import ( diff --git a/homeassistant/components/fully_kiosk/button.py b/homeassistant/components/fully_kiosk/button.py index 0a6233937ae..94c34b50de1 100644 --- a/homeassistant/components/fully_kiosk/button.py +++ b/homeassistant/components/fully_kiosk/button.py @@ -1,4 +1,5 @@ """Fully Kiosk Browser button.""" + from __future__ import annotations from collections.abc import Callable @@ -22,20 +23,13 @@ from .coordinator import FullyKioskDataUpdateCoordinator from .entity import FullyKioskEntity -@dataclass(frozen=True) -class FullyButtonEntityDescriptionMixin: - """Mixin to describe a Fully Kiosk Browser button entity.""" +@dataclass(frozen=True, kw_only=True) +class FullyButtonEntityDescription(ButtonEntityDescription): + """Fully Kiosk Browser button description.""" press_action: Callable[[FullyKiosk], Any] -@dataclass(frozen=True) -class FullyButtonEntityDescription( - ButtonEntityDescription, FullyButtonEntityDescriptionMixin -): - """Fully Kiosk Browser button description.""" - - BUTTONS: tuple[FullyButtonEntityDescription, ...] = ( FullyButtonEntityDescription( key="restartApp", diff --git a/homeassistant/components/fully_kiosk/config_flow.py b/homeassistant/components/fully_kiosk/config_flow.py index 00eb1dd7101..8fd0d4ee4cc 100644 --- a/homeassistant/components/fully_kiosk/config_flow.py +++ b/homeassistant/components/fully_kiosk/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Fully Kiosk Browser integration.""" + from __future__ import annotations import asyncio @@ -10,8 +11,8 @@ from fullykiosk import FullyKiosk from fullykiosk.exceptions import FullyKioskError import voluptuous as vol -from homeassistant import config_entries from homeassistant.components.dhcp import DhcpServiceInfo +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import ( CONF_HOST, CONF_MAC, @@ -19,7 +20,6 @@ from homeassistant.const import ( CONF_SSL, CONF_VERIFY_SSL, ) -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.device_registry import format_mac from homeassistant.helpers.service_info.mqtt import MqttServiceInfo @@ -27,7 +27,7 @@ from homeassistant.helpers.service_info.mqtt import MqttServiceInfo from .const import DEFAULT_PORT, DOMAIN, LOGGER -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class FullyKioskConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Fully Kiosk Browser.""" VERSION = 1 @@ -42,7 +42,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): user_input: dict[str, Any], errors: dict[str, str], description_placeholders: dict[str, str] | Any = None, - ) -> FlowResult | None: + ) -> ConfigFlowResult | None: fully = FullyKiosk( async_get_clientsession(self.hass), host, @@ -85,7 +85,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors: dict[str, str] = {} placeholders: dict[str, str] = {} @@ -110,7 +110,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_dhcp(self, discovery_info: DhcpServiceInfo) -> FlowResult: + async def async_step_dhcp( + self, discovery_info: DhcpServiceInfo + ) -> ConfigFlowResult: """Handle dhcp discovery.""" mac = format_mac(discovery_info.macaddress) @@ -129,7 +131,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_discovery_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm discovery.""" errors: dict[str, str] = {} if user_input is not None: @@ -157,7 +159,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_mqtt(self, discovery_info: MqttServiceInfo) -> FlowResult: + async def async_step_mqtt( + self, discovery_info: MqttServiceInfo + ) -> ConfigFlowResult: """Handle a flow initialized by MQTT discovery.""" device_info: dict[str, Any] = json.loads(discovery_info.payload) device_id: str = device_info["deviceId"] diff --git a/homeassistant/components/fully_kiosk/const.py b/homeassistant/components/fully_kiosk/const.py index 3db33d21ef0..35fe539a552 100644 --- a/homeassistant/components/fully_kiosk/const.py +++ b/homeassistant/components/fully_kiosk/const.py @@ -1,4 +1,5 @@ """Constants for the Fully Kiosk Browser integration.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/fully_kiosk/coordinator.py b/homeassistant/components/fully_kiosk/coordinator.py index 203251351ae..405f0746437 100644 --- a/homeassistant/components/fully_kiosk/coordinator.py +++ b/homeassistant/components/fully_kiosk/coordinator.py @@ -1,4 +1,5 @@ """Provides the Fully Kiosk Browser DataUpdateCoordinator.""" + import asyncio from typing import Any, cast diff --git a/homeassistant/components/fully_kiosk/diagnostics.py b/homeassistant/components/fully_kiosk/diagnostics.py index 121621186cd..df03cb4a7bf 100644 --- a/homeassistant/components/fully_kiosk/diagnostics.py +++ b/homeassistant/components/fully_kiosk/diagnostics.py @@ -1,4 +1,5 @@ """Provides diagnostics for Fully Kiosk Browser.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/fully_kiosk/entity.py b/homeassistant/components/fully_kiosk/entity.py index b053508ae41..a1f077d7886 100644 --- a/homeassistant/components/fully_kiosk/entity.py +++ b/homeassistant/components/fully_kiosk/entity.py @@ -1,4 +1,5 @@ """Base entity for the Fully Kiosk Browser integration.""" + from __future__ import annotations import json diff --git a/homeassistant/components/fully_kiosk/media_player.py b/homeassistant/components/fully_kiosk/media_player.py index 8e6d2fad533..1e258c928e7 100644 --- a/homeassistant/components/fully_kiosk/media_player.py +++ b/homeassistant/components/fully_kiosk/media_player.py @@ -1,4 +1,5 @@ """Fully Kiosk Browser media player.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/fully_kiosk/number.py b/homeassistant/components/fully_kiosk/number.py index 4203a64074d..59c249fd1c2 100644 --- a/homeassistant/components/fully_kiosk/number.py +++ b/homeassistant/components/fully_kiosk/number.py @@ -1,4 +1,5 @@ """Fully Kiosk Browser number entity.""" + from __future__ import annotations from contextlib import suppress diff --git a/homeassistant/components/fully_kiosk/sensor.py b/homeassistant/components/fully_kiosk/sensor.py index 8e9029fda73..48fc8e51425 100644 --- a/homeassistant/components/fully_kiosk/sensor.py +++ b/homeassistant/components/fully_kiosk/sensor.py @@ -1,4 +1,5 @@ """Fully Kiosk Browser sensor.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/fully_kiosk/services.py b/homeassistant/components/fully_kiosk/services.py index 5106fd2e06e..c1e0d89f7a1 100644 --- a/homeassistant/components/fully_kiosk/services.py +++ b/homeassistant/components/fully_kiosk/services.py @@ -1,4 +1,5 @@ """Services for the Fully Kiosk Browser integration.""" + from __future__ import annotations import voluptuous as vol diff --git a/homeassistant/components/fully_kiosk/switch.py b/homeassistant/components/fully_kiosk/switch.py index d5480b784c4..9d5af87abe9 100644 --- a/homeassistant/components/fully_kiosk/switch.py +++ b/homeassistant/components/fully_kiosk/switch.py @@ -1,4 +1,5 @@ """Fully Kiosk Browser switch.""" + from __future__ import annotations from collections.abc import Callable @@ -18,9 +19,9 @@ from .coordinator import FullyKioskDataUpdateCoordinator from .entity import FullyKioskEntity -@dataclass(frozen=True) -class FullySwitchEntityDescriptionMixin: - """Fully Kiosk Browser switch entity description mixin.""" +@dataclass(frozen=True, kw_only=True) +class FullySwitchEntityDescription(SwitchEntityDescription): + """Fully Kiosk Browser switch entity description.""" on_action: Callable[[FullyKiosk], Any] off_action: Callable[[FullyKiosk], Any] @@ -29,13 +30,6 @@ class FullySwitchEntityDescriptionMixin: mqtt_off_event: str | None -@dataclass(frozen=True) -class FullySwitchEntityDescription( - SwitchEntityDescription, FullySwitchEntityDescriptionMixin -): - """Fully Kiosk Browser switch entity description.""" - - SWITCHES: tuple[FullySwitchEntityDescription, ...] = ( FullySwitchEntityDescription( key="screensaver", diff --git a/homeassistant/components/futurenow/light.py b/homeassistant/components/futurenow/light.py index d070d832052..8474c1073e9 100644 --- a/homeassistant/components/futurenow/light.py +++ b/homeassistant/components/futurenow/light.py @@ -1,4 +1,5 @@ """Support for FutureNow Ethernet unit outputs as Lights.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/fyta/__init__.py b/homeassistant/components/fyta/__init__.py new file mode 100644 index 00000000000..febd5b94469 --- /dev/null +++ b/homeassistant/components/fyta/__init__.py @@ -0,0 +1,49 @@ +"""Initialization of FYTA integration.""" + +from __future__ import annotations + +import logging + +from fyta_cli.fyta_connector import FytaConnector + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform +from homeassistant.core import HomeAssistant + +from .const import DOMAIN +from .coordinator import FytaCoordinator + +_LOGGER = logging.getLogger(__name__) + +PLATFORMS = [ + Platform.SENSOR, +] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up the Fyta integration.""" + + username = entry.data[CONF_USERNAME] + password = entry.data[CONF_PASSWORD] + + fyta = FytaConnector(username, password) + + coordinator = FytaCoordinator(hass, fyta) + + await coordinator.async_config_entry_first_refresh() + + hass.data.setdefault(DOMAIN, {})[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 Fyta entity.""" + + unload_ok = 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/fyta/config_flow.py b/homeassistant/components/fyta/config_flow.py new file mode 100644 index 00000000000..67e46f8125e --- /dev/null +++ b/homeassistant/components/fyta/config_flow.py @@ -0,0 +1,65 @@ +"""Config flow for FYTA integration.""" + +from __future__ import annotations + +import logging +from typing import Any + +from fyta_cli.fyta_connector import FytaConnector +from fyta_cli.fyta_exceptions import ( + FytaAuthentificationError, + FytaConnectionError, + FytaPasswordError, +) +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +DATA_SCHEMA = vol.Schema( + {vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str} +) + + +class FytaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Fyta.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> config_entries.ConfigFlowResult: + """Handle the initial step.""" + + errors = {} + if user_input: + self._async_abort_entries_match({CONF_USERNAME: user_input[CONF_USERNAME]}) + + fyta = FytaConnector(user_input[CONF_USERNAME], user_input[CONF_PASSWORD]) + + try: + await fyta.login() + except FytaConnectionError: + errors["base"] = "cannot_connect" + except FytaAuthentificationError: + errors["base"] = "invalid_auth" + except FytaPasswordError: + errors["base"] = "invalid_auth" + errors[CONF_PASSWORD] = "password_error" + except Exception: # pylint: disable=broad-except + errors["base"] = "unknown" + else: + return self.async_create_entry( + title=user_input[CONF_USERNAME], data=user_input + ) + finally: + await fyta.client.close() + + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA, errors=errors + ) diff --git a/homeassistant/components/fyta/const.py b/homeassistant/components/fyta/const.py new file mode 100644 index 00000000000..f99735dc6fa --- /dev/null +++ b/homeassistant/components/fyta/const.py @@ -0,0 +1,3 @@ +"""Const for fyta integration.""" + +DOMAIN = "fyta" diff --git a/homeassistant/components/fyta/coordinator.py b/homeassistant/components/fyta/coordinator.py new file mode 100644 index 00000000000..c132ee75e72 --- /dev/null +++ b/homeassistant/components/fyta/coordinator.py @@ -0,0 +1,55 @@ +"""Coordinator for FYTA integration.""" + +from datetime import datetime, timedelta +import logging +from typing import Any + +from fyta_cli.fyta_connector import FytaConnector +from fyta_cli.fyta_exceptions import ( + FytaAuthentificationError, + FytaConnectionError, + FytaPasswordError, +) + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +_LOGGER = logging.getLogger(__name__) + + +class FytaCoordinator(DataUpdateCoordinator[dict[int, dict[str, Any]]]): + """Fyta custom coordinator.""" + + config_entry: ConfigEntry + + def __init__(self, hass: HomeAssistant, fyta: FytaConnector) -> None: + """Initialize my coordinator.""" + super().__init__( + hass, + _LOGGER, + name="FYTA Coordinator", + update_interval=timedelta(seconds=60), + ) + self.fyta = fyta + + async def _async_update_data( + self, + ) -> dict[int, dict[str, Any]]: + """Fetch data from API endpoint.""" + + if self.fyta.expiration is None or self.fyta.expiration < datetime.now(): + await self.renew_authentication() + + return await self.fyta.update_all_plants() + + async def renew_authentication(self) -> None: + """Renew access token for FYTA API.""" + + try: + await self.fyta.login() + except FytaConnectionError as ex: + raise ConfigEntryNotReady from ex + except (FytaAuthentificationError, FytaPasswordError) as ex: + raise ConfigEntryError from ex diff --git a/homeassistant/components/fyta/entity.py b/homeassistant/components/fyta/entity.py new file mode 100644 index 00000000000..681a50f4cf5 --- /dev/null +++ b/homeassistant/components/fyta/entity.py @@ -0,0 +1,48 @@ +"""Entities for FYTA integration.""" + +from typing import Any + +from homeassistant.components.sensor import SensorEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN +from .coordinator import FytaCoordinator + + +class FytaPlantEntity(CoordinatorEntity[FytaCoordinator]): + """Base Fyta Plant entity.""" + + _attr_has_entity_name = True + + def __init__( + self, + coordinator: FytaCoordinator, + entry: ConfigEntry, + description: SensorEntityDescription, + plant_id: int, + ) -> None: + """Initialize the Fyta sensor.""" + super().__init__(coordinator) + + self.plant_id = plant_id + self._attr_unique_id = f"{entry.entry_id}-{plant_id}-{description.key}" + self._attr_device_info = DeviceInfo( + manufacturer="Fyta", + model="Plant", + identifiers={(DOMAIN, f"{entry.entry_id}-{plant_id}")}, + name=self.plant.get("name"), + sw_version=self.plant.get("sw_version"), + ) + self.entity_description = description + + @property + def plant(self) -> dict[str, Any]: + """Get plant data.""" + return self.coordinator.data[self.plant_id] + + @property + def available(self) -> bool: + """Test if entity is available.""" + return super().available and self.plant_id in self.coordinator.data diff --git a/homeassistant/components/fyta/icons.json b/homeassistant/components/fyta/icons.json new file mode 100644 index 00000000000..b96eeb15e62 --- /dev/null +++ b/homeassistant/components/fyta/icons.json @@ -0,0 +1,27 @@ +{ + "entity": { + "sensor": { + "status": { + "default": "mdi:flower" + }, + "temperature_status": { + "default": "mdi:thermometer-lines" + }, + "light_status": { + "default": "mdi:sun-clock-outline" + }, + "moisture_status": { + "default": "mdi:water-percent-alert" + }, + "salinity_status": { + "default": "mdi:sprout-outline" + }, + "light": { + "default": "mdi:weather-sunny" + }, + "salinity": { + "default": "mdi:sprout-outline" + } + } + } +} diff --git a/homeassistant/components/fyta/manifest.json b/homeassistant/components/fyta/manifest.json new file mode 100644 index 00000000000..a93a76a9e1d --- /dev/null +++ b/homeassistant/components/fyta/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "fyta", + "name": "FYTA", + "codeowners": ["@dontinelli"], + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/fyta", + "integration_type": "hub", + "iot_class": "cloud_polling", + "requirements": ["fyta_cli==0.3.3"] +} diff --git a/homeassistant/components/fyta/sensor.py b/homeassistant/components/fyta/sensor.py new file mode 100644 index 00000000000..0643c69981e --- /dev/null +++ b/homeassistant/components/fyta/sensor.py @@ -0,0 +1,144 @@ +"""Summary data from Fyta.""" + +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from datetime import datetime +from typing import Final + +from fyta_cli.fyta_connector import PLANT_STATUS + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfTemperature +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import FytaCoordinator +from .entity import FytaPlantEntity + + +@dataclass(frozen=True) +class FytaSensorEntityDescription(SensorEntityDescription): + """Describes Fyta sensor entity.""" + + value_fn: Callable[[str | int | float | datetime], str | int | float | datetime] = ( + lambda value: value + ) + + +PLANT_STATUS_LIST: list[str] = ["too_low", "low", "perfect", "high", "too_high"] + +SENSORS: Final[list[FytaSensorEntityDescription]] = [ + FytaSensorEntityDescription( + key="scientific_name", + translation_key="scientific_name", + ), + FytaSensorEntityDescription( + key="status", + translation_key="plant_status", + device_class=SensorDeviceClass.ENUM, + options=PLANT_STATUS_LIST, + value_fn=lambda value: PLANT_STATUS[value], + ), + FytaSensorEntityDescription( + key="temperature_status", + translation_key="temperature_status", + device_class=SensorDeviceClass.ENUM, + options=PLANT_STATUS_LIST, + value_fn=lambda value: PLANT_STATUS[value], + ), + FytaSensorEntityDescription( + key="light_status", + translation_key="light_status", + device_class=SensorDeviceClass.ENUM, + options=PLANT_STATUS_LIST, + value_fn=lambda value: PLANT_STATUS[value], + ), + FytaSensorEntityDescription( + key="moisture_status", + translation_key="moisture_status", + device_class=SensorDeviceClass.ENUM, + options=PLANT_STATUS_LIST, + value_fn=lambda value: PLANT_STATUS[value], + ), + FytaSensorEntityDescription( + key="salinity_status", + translation_key="salinity_status", + device_class=SensorDeviceClass.ENUM, + options=PLANT_STATUS_LIST, + value_fn=lambda value: PLANT_STATUS[value], + ), + FytaSensorEntityDescription( + key="temperature", + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + ), + FytaSensorEntityDescription( + key="light", + translation_key="light", + native_unit_of_measurement="mol/d", + state_class=SensorStateClass.MEASUREMENT, + ), + FytaSensorEntityDescription( + key="moisture", + native_unit_of_measurement=PERCENTAGE, + device_class=SensorDeviceClass.MOISTURE, + state_class=SensorStateClass.MEASUREMENT, + ), + FytaSensorEntityDescription( + key="salinity", + translation_key="salinity", + native_unit_of_measurement="mS/cm", + state_class=SensorStateClass.MEASUREMENT, + ), + FytaSensorEntityDescription( + key="ph", + device_class=SensorDeviceClass.PH, + state_class=SensorStateClass.MEASUREMENT, + ), + FytaSensorEntityDescription( + key="battery_level", + native_unit_of_measurement=PERCENTAGE, + device_class=SensorDeviceClass.BATTERY, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + ), +] + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up the FYTA sensors.""" + coordinator: FytaCoordinator = hass.data[DOMAIN][entry.entry_id] + + plant_entities = [ + FytaPlantSensor(coordinator, entry, sensor, plant_id) + for plant_id in coordinator.fyta.plant_list + for sensor in SENSORS + if sensor.key in coordinator.data[plant_id] + ] + + async_add_entities(plant_entities) + + +class FytaPlantSensor(FytaPlantEntity, SensorEntity): + """Represents a Fyta sensor.""" + + entity_description: FytaSensorEntityDescription + + @property + def native_value(self) -> str | int | float | datetime: + """Return the state for this sensor.""" + + val = self.plant[self.entity_description.key] + return self.entity_description.value_fn(val) diff --git a/homeassistant/components/fyta/strings.json b/homeassistant/components/fyta/strings.json new file mode 100644 index 00000000000..6d4fe68a86c --- /dev/null +++ b/homeassistant/components/fyta/strings.json @@ -0,0 +1,83 @@ +{ + "config": { + "step": { + "user": { + "title": "Credentials for FYTA API", + "description": "Provide username and password to connect to the FYTA server", + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "password_error": "Invalid password", + "unknown": "[%key:common::config_flow::error::unknown%]" + } + }, + "entity": { + "sensor": { + "scientific_name": { + "name": "Scientific name" + }, + "plant_status": { + "name": "Plant state", + "state": { + "too_low": "Too low", + "low": "Low", + "perfect": "Perfect", + "high": "High", + "too_high": "Too high" + } + }, + "temperature_status": { + "name": "Temperature state", + "state": { + "too_low": "[%key:component::fyta::entity::sensor::plant_status::state::too_low%]", + "low": "[%key:component::fyta::entity::sensor::plant_status::state::low%]", + "perfect": "[%key:component::fyta::entity::sensor::plant_status::state::perfect%]", + "high": "[%key:component::fyta::entity::sensor::plant_status::state::high%]", + "too_high": "[%key:component::fyta::entity::sensor::plant_status::state::too_high%]" + } + }, + "light_status": { + "name": "Light state", + "state": { + "too_low": "[%key:component::fyta::entity::sensor::plant_status::state::too_low%]", + "low": "[%key:component::fyta::entity::sensor::plant_status::state::low%]", + "perfect": "[%key:component::fyta::entity::sensor::plant_status::state::perfect%]", + "high": "[%key:component::fyta::entity::sensor::plant_status::state::high%]", + "too_high": "[%key:component::fyta::entity::sensor::plant_status::state::too_high%]" + } + }, + "moisture_status": { + "name": "Moisture state", + "state": { + "too_low": "[%key:component::fyta::entity::sensor::plant_status::state::too_low%]", + "low": "[%key:component::fyta::entity::sensor::plant_status::state::low%]", + "perfect": "[%key:component::fyta::entity::sensor::plant_status::state::perfect%]", + "high": "[%key:component::fyta::entity::sensor::plant_status::state::high%]", + "too_high": "[%key:component::fyta::entity::sensor::plant_status::state::too_high%]" + } + }, + "salinity_status": { + "name": "Salinity state", + "state": { + "too_low": "[%key:component::fyta::entity::sensor::plant_status::state::too_low%]", + "low": "[%key:component::fyta::entity::sensor::plant_status::state::low%]", + "perfect": "[%key:component::fyta::entity::sensor::plant_status::state::perfect%]", + "high": "[%key:component::fyta::entity::sensor::plant_status::state::high%]", + "too_high": "[%key:component::fyta::entity::sensor::plant_status::state::too_high%]" + } + }, + "light": { + "name": "Light" + }, + "salinity": { + "name": "Salinity" + } + } + } +} diff --git a/homeassistant/components/garadget/cover.py b/homeassistant/components/garadget/cover.py index 6d9705cee75..f168652b3cf 100644 --- a/homeassistant/components/garadget/cover.py +++ b/homeassistant/components/garadget/cover.py @@ -1,4 +1,5 @@ """Platform for the Garadget cover component.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/garages_amsterdam/__init__.py b/homeassistant/components/garages_amsterdam/__init__.py index 82e0c832e7b..81ec72d9fbf 100644 --- a/homeassistant/components/garages_amsterdam/__init__.py +++ b/homeassistant/components/garages_amsterdam/__init__.py @@ -1,4 +1,5 @@ """The Garages Amsterdam integration.""" + import asyncio from datetime import timedelta import logging diff --git a/homeassistant/components/garages_amsterdam/binary_sensor.py b/homeassistant/components/garages_amsterdam/binary_sensor.py index ad0630249aa..0aebe36baeb 100644 --- a/homeassistant/components/garages_amsterdam/binary_sensor.py +++ b/homeassistant/components/garages_amsterdam/binary_sensor.py @@ -1,4 +1,5 @@ """Binary Sensor platform for Garages Amsterdam.""" + from __future__ import annotations from homeassistant.components.binary_sensor import ( diff --git a/homeassistant/components/garages_amsterdam/config_flow.py b/homeassistant/components/garages_amsterdam/config_flow.py index 65a2d359747..6623ad5bd18 100644 --- a/homeassistant/components/garages_amsterdam/config_flow.py +++ b/homeassistant/components/garages_amsterdam/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Garages Amsterdam integration.""" + from __future__ import annotations import logging @@ -8,8 +9,7 @@ from aiohttp import ClientResponseError from odp_amsterdam import ODPAmsterdam, VehicleType import voluptuous as vol -from homeassistant import config_entries -from homeassistant.data_entry_flow import FlowResult +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.helpers import aiohttp_client from .const import DOMAIN @@ -17,7 +17,7 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class GaragesAmsterdamConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Garages Amsterdam.""" VERSION = 1 @@ -25,7 +25,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" if self._options is None: self._options = [] diff --git a/homeassistant/components/garages_amsterdam/entity.py b/homeassistant/components/garages_amsterdam/entity.py index 45c85a101a9..671405235d4 100644 --- a/homeassistant/components/garages_amsterdam/entity.py +++ b/homeassistant/components/garages_amsterdam/entity.py @@ -1,4 +1,5 @@ """Generic entity for Garages Amsterdam.""" + from __future__ import annotations from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo diff --git a/homeassistant/components/garages_amsterdam/sensor.py b/homeassistant/components/garages_amsterdam/sensor.py index 48a3746a762..b6fc950a843 100644 --- a/homeassistant/components/garages_amsterdam/sensor.py +++ b/homeassistant/components/garages_amsterdam/sensor.py @@ -1,4 +1,5 @@ """Sensor platform for Garages Amsterdam.""" + from __future__ import annotations from homeassistant.components.sensor import SensorEntity @@ -26,17 +27,11 @@ async def async_setup_entry( """Defer sensor setup to the shared sensor module.""" coordinator = await get_coordinator(hass) - entities: list[GaragesAmsterdamSensor] = [] - - for info_type in SENSORS: - if getattr(coordinator.data[config_entry.data["garage_name"]], info_type) != "": - entities.append( - GaragesAmsterdamSensor( - coordinator, config_entry.data["garage_name"], info_type - ) - ) - - async_add_entities(entities) + async_add_entities( + GaragesAmsterdamSensor(coordinator, config_entry.data["garage_name"], info_type) + for info_type in SENSORS + if getattr(coordinator.data[config_entry.data["garage_name"]], info_type) != "" + ) class GaragesAmsterdamSensor(GaragesAmsterdamEntity, SensorEntity): diff --git a/homeassistant/components/gardena_bluetooth/__init__.py b/homeassistant/components/gardena_bluetooth/__init__.py index 99c8fa69acf..c2b3ae6732b 100644 --- a/homeassistant/components/gardena_bluetooth/__init__.py +++ b/homeassistant/components/gardena_bluetooth/__init__.py @@ -1,4 +1,5 @@ """The Gardena Bluetooth integration.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/gardena_bluetooth/binary_sensor.py b/homeassistant/components/gardena_bluetooth/binary_sensor.py index bf905bc551d..c552beaf878 100644 --- a/homeassistant/components/gardena_bluetooth/binary_sensor.py +++ b/homeassistant/components/gardena_bluetooth/binary_sensor.py @@ -1,4 +1,5 @@ """Support for binary_sensor entities.""" + from __future__ import annotations from dataclasses import dataclass, field diff --git a/homeassistant/components/gardena_bluetooth/button.py b/homeassistant/components/gardena_bluetooth/button.py index cbdbda5f367..bdcf9094f5c 100644 --- a/homeassistant/components/gardena_bluetooth/button.py +++ b/homeassistant/components/gardena_bluetooth/button.py @@ -1,4 +1,5 @@ """Support for button entities.""" + from __future__ import annotations from dataclasses import dataclass, field diff --git a/homeassistant/components/gardena_bluetooth/config_flow.py b/homeassistant/components/gardena_bluetooth/config_flow.py index 7b34edd29af..c7631b62f47 100644 --- a/homeassistant/components/gardena_bluetooth/config_flow.py +++ b/homeassistant/components/gardena_bluetooth/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Gardena Bluetooth integration.""" + from __future__ import annotations import logging @@ -10,13 +11,13 @@ from gardena_bluetooth.exceptions import CharacteristicNotFound, CommunicationFa from gardena_bluetooth.parse import ManufacturerData, ProductType import voluptuous as vol -from homeassistant import config_entries from homeassistant.components.bluetooth import ( BluetoothServiceInfo, async_discovered_service_info, ) +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_ADDRESS -from homeassistant.data_entry_flow import AbortFlow, FlowResult +from homeassistant.data_entry_flow import AbortFlow from . import get_connection from .const import DOMAIN @@ -55,7 +56,7 @@ def _get_name(discovery_info: BluetoothServiceInfo): return PRODUCT_NAMES.get(product_type, "Gardena Device") -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class GardenaBluetoothConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Gardena Bluetooth.""" VERSION = 1 @@ -82,7 +83,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_bluetooth( self, discovery_info: BluetoothServiceInfo - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the bluetooth discovery step.""" _LOGGER.debug("Discovered device: %s", discovery_info) if not _is_supported(discovery_info): @@ -96,7 +97,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm discovery.""" assert self.address title = self.devices[self.address] @@ -117,7 +118,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" if user_input is not None: self.address = user_input[CONF_ADDRESS] diff --git a/homeassistant/components/gardena_bluetooth/coordinator.py b/homeassistant/components/gardena_bluetooth/coordinator.py index 12a212fe44b..296eff2686e 100644 --- a/homeassistant/components/gardena_bluetooth/coordinator.py +++ b/homeassistant/components/gardena_bluetooth/coordinator.py @@ -1,4 +1,5 @@ """Provides the DataUpdateCoordinator.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/gardena_bluetooth/number.py b/homeassistant/components/gardena_bluetooth/number.py index ef19a921041..cbc4866b0ff 100644 --- a/homeassistant/components/gardena_bluetooth/number.py +++ b/homeassistant/components/gardena_bluetooth/number.py @@ -1,4 +1,5 @@ """Support for number entities.""" + from __future__ import annotations from dataclasses import dataclass, field diff --git a/homeassistant/components/gardena_bluetooth/sensor.py b/homeassistant/components/gardena_bluetooth/sensor.py index ca2b1acdd8c..f2bddd3a91a 100644 --- a/homeassistant/components/gardena_bluetooth/sensor.py +++ b/homeassistant/components/gardena_bluetooth/sensor.py @@ -1,4 +1,5 @@ """Support for switch entities.""" + from __future__ import annotations from dataclasses import dataclass, field diff --git a/homeassistant/components/gardena_bluetooth/switch.py b/homeassistant/components/gardena_bluetooth/switch.py index bc83e3ed5a9..a57130c3acf 100644 --- a/homeassistant/components/gardena_bluetooth/switch.py +++ b/homeassistant/components/gardena_bluetooth/switch.py @@ -1,4 +1,5 @@ """Support for switch entities.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/gc100/__init__.py b/homeassistant/components/gc100/__init__.py index fff68f48fc0..57c8e92499f 100644 --- a/homeassistant/components/gc100/__init__.py +++ b/homeassistant/components/gc100/__init__.py @@ -1,4 +1,5 @@ """Support for controlling Global Cache gc100.""" + import gc100 import voluptuous as vol diff --git a/homeassistant/components/gc100/binary_sensor.py b/homeassistant/components/gc100/binary_sensor.py index e750f928cf7..a03eae509d9 100644 --- a/homeassistant/components/gc100/binary_sensor.py +++ b/homeassistant/components/gc100/binary_sensor.py @@ -1,4 +1,5 @@ """Support for binary sensor using GC100.""" + from __future__ import annotations import voluptuous as vol diff --git a/homeassistant/components/gc100/switch.py b/homeassistant/components/gc100/switch.py index d88b4c9fa79..ea90dde6abf 100644 --- a/homeassistant/components/gc100/switch.py +++ b/homeassistant/components/gc100/switch.py @@ -1,4 +1,5 @@ """Support for switches using GC100.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/gdacs/__init__.py b/homeassistant/components/gdacs/__init__.py index 0ec582f8d06..e96246b70bf 100644 --- a/homeassistant/components/gdacs/__init__.py +++ b/homeassistant/components/gdacs/__init__.py @@ -1,4 +1,5 @@ """The Global Disaster Alert and Coordination System (GDACS) integration.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/gdacs/config_flow.py b/homeassistant/components/gdacs/config_flow.py index acc3bbc1991..eefc9b93438 100644 --- a/homeassistant/components/gdacs/config_flow.py +++ b/homeassistant/components/gdacs/config_flow.py @@ -1,17 +1,17 @@ """Config flow to configure the GDACS integration.""" + import logging from typing import Any import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import ( CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS, CONF_SCAN_INTERVAL, ) -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_validation as cv from .const import CONF_CATEGORIES, DEFAULT_RADIUS, DEFAULT_SCAN_INTERVAL, DOMAIN @@ -23,10 +23,12 @@ DATA_SCHEMA = vol.Schema( _LOGGER = logging.getLogger(__name__) -class GdacsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class GdacsFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a GDACS config flow.""" - async def _show_form(self, errors: dict[str, str] | None = None) -> FlowResult: + async def _show_form( + self, errors: dict[str, str] | None = None + ) -> ConfigFlowResult: """Show the form to the user.""" return self.async_show_form( step_id="user", data_schema=DATA_SCHEMA, errors=errors or {} @@ -34,7 +36,7 @@ class GdacsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the start of the config flow.""" _LOGGER.debug("User input: %s", user_input) if not user_input: diff --git a/homeassistant/components/gdacs/const.py b/homeassistant/components/gdacs/const.py index 6be7e7b32fc..d1028ed2d08 100644 --- a/homeassistant/components/gdacs/const.py +++ b/homeassistant/components/gdacs/const.py @@ -1,4 +1,5 @@ """Define constants for the GDACS integration.""" + from datetime import timedelta from aio_georss_gdacs.consts import EVENT_TYPE_MAP diff --git a/homeassistant/components/gdacs/geo_location.py b/homeassistant/components/gdacs/geo_location.py index 5c4fb9d33bc..394c9086d71 100644 --- a/homeassistant/components/gdacs/geo_location.py +++ b/homeassistant/components/gdacs/geo_location.py @@ -1,4 +1,5 @@ """Geolocation support for GDACS Feed.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/gdacs/sensor.py b/homeassistant/components/gdacs/sensor.py index f660c8f73c8..90ea668ea3f 100644 --- a/homeassistant/components/gdacs/sensor.py +++ b/homeassistant/components/gdacs/sensor.py @@ -1,4 +1,5 @@ """Feed Entity Manager Sensor support for GDACS Feed.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/generic/__init__.py b/homeassistant/components/generic/__init__.py index 280b468add8..3de664dd734 100644 --- a/homeassistant/components/generic/__init__.py +++ b/homeassistant/components/generic/__init__.py @@ -1,4 +1,5 @@ """The generic component.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/generic/camera.py b/homeassistant/components/generic/camera.py index cadc855ade6..80971760b85 100644 --- a/homeassistant/components/generic/camera.py +++ b/homeassistant/components/generic/camera.py @@ -1,4 +1,5 @@ """Support for IP Cameras.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/generic/config_flow.py b/homeassistant/components/generic/config_flow.py index 4eb5c3a2a4c..8fdc0143700 100644 --- a/homeassistant/components/generic/config_flow.py +++ b/homeassistant/components/generic/config_flow.py @@ -1,4 +1,5 @@ """Config flow for generic (IP Camera).""" + from __future__ import annotations import asyncio @@ -30,7 +31,12 @@ from homeassistant.components.stream import ( SOURCE_TIMEOUT, create_stream, ) -from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.const import ( CONF_AUTHENTICATION, CONF_NAME, @@ -41,7 +47,7 @@ from homeassistant.const import ( HTTP_DIGEST_AUTHENTICATION, ) from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResult, UnknownFlow +from homeassistant.data_entry_flow import UnknownFlow from homeassistant.exceptions import TemplateError from homeassistant.helpers import config_validation as cv, template as template_helper from homeassistant.helpers.httpx_client import get_async_client @@ -177,14 +183,27 @@ async def async_test_still( except ( TimeoutError, RequestError, - HTTPStatusError, TimeoutException, ) as err: _LOGGER.error("Error getting camera image from %s: %s", url, type(err).__name__) return {CONF_STILL_IMAGE_URL: "unable_still_load"}, None + except HTTPStatusError as err: + _LOGGER.error( + "Error getting camera image from %s: %s %s", + url, + type(err).__name__, + err.response.text, + ) + if err.response.status_code in [401, 403]: + return {CONF_STILL_IMAGE_URL: "unable_still_load_auth"}, None + if err.response.status_code in [404]: + return {CONF_STILL_IMAGE_URL: "unable_still_load_not_found"}, None + if err.response.status_code in [500, 503]: + return {CONF_STILL_IMAGE_URL: "unable_still_load_server_error"}, None + return {CONF_STILL_IMAGE_URL: "unable_still_load"}, None if not image: - return {CONF_STILL_IMAGE_URL: "unable_still_load"}, None + return {CONF_STILL_IMAGE_URL: "unable_still_load_no_image"}, None fmt = get_image_type(image) _LOGGER.debug( "Still image at '%s' detected format: %s", @@ -313,7 +332,7 @@ class GenericIPCamConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the start of the config flow.""" errors = {} hass = self.hass @@ -357,7 +376,7 @@ class GenericIPCamConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user_confirm_still( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle user clicking confirm after still preview.""" if user_input: if not user_input.get(CONF_CONFIRMED_OK): @@ -389,7 +408,7 @@ class GenericOptionsFlowHandler(OptionsFlow): async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Manage Generic IP Camera options.""" errors: dict[str, str] = {} hass = self.hass @@ -430,7 +449,7 @@ class GenericOptionsFlowHandler(OptionsFlow): async def async_step_confirm_still( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle user clicking confirm after still preview.""" if user_input: if not user_input.get(CONF_CONFIRMED_OK): @@ -474,12 +493,12 @@ class CameraImagePreview(HomeAssistantView): flow = self.hass.config_entries.options.async_get(flow_id) except UnknownFlow as exc: _LOGGER.warning("Unknown flow while getting image preview") - raise web.HTTPNotFound() from exc + raise web.HTTPNotFound from exc user_input = flow["context"]["preview_cam"] camera = GenericCamera(self.hass, user_input, flow_id, "preview") if not camera.is_on: _LOGGER.debug("Camera is off") - raise web.HTTPServiceUnavailable() + raise web.HTTPServiceUnavailable image = await _async_get_image( camera, CAMERA_IMAGE_TIMEOUT, diff --git a/homeassistant/components/generic/diagnostics.py b/homeassistant/components/generic/diagnostics.py index 39d6a81ad88..e5bf4294e4a 100644 --- a/homeassistant/components/generic/diagnostics.py +++ b/homeassistant/components/generic/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for generic (IP camera).""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/generic/strings.json b/homeassistant/components/generic/strings.json index a1519fa0f48..991a36d49cc 100644 --- a/homeassistant/components/generic/strings.json +++ b/homeassistant/components/generic/strings.json @@ -5,6 +5,10 @@ "unknown": "[%key:common::config_flow::error::unknown%]", "already_exists": "A camera with these URL settings already exists.", "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.", + "unable_still_load_auth": "Unable to load valid image from still image URL: The camera may require a user name and password, or they are not correct.", + "unable_still_load_not_found": "Unable to load valid image from still image URL: The URL was not found on the server.", + "unable_still_load_server_error": "Unable to load valid image from still image URL: The camera replied with a server error.", + "unable_still_load_no_image": "Unable to load valid image from still image URL: No image was returned.", "no_still_image_or_stream_url": "You must specify at least a still image or stream URL", "invalid_still_image": "URL did not return a valid still image", "malformed_url": "Malformed URL", @@ -73,6 +77,10 @@ "unknown": "[%key:common::config_flow::error::unknown%]", "already_exists": "[%key:component::generic::config::error::already_exists%]", "unable_still_load": "[%key:component::generic::config::error::unable_still_load%]", + "unable_still_load_auth": "[%key:component::generic::config::error::unable_still_load_auth%]", + "unable_still_load_not_found": "[%key:component::generic::config::error::unable_still_load_not_found%]", + "unable_still_load_server_error": "[%key:component::generic::config::error::unable_still_load_server_error%]", + "unable_still_load_no_image": "[%key:component::generic::config::error::unable_still_load_no_image%]", "no_still_image_or_stream_url": "[%key:component::generic::config::error::no_still_image_or_stream_url%]", "invalid_still_image": "[%key:component::generic::config::error::invalid_still_image%]", "malformed_url": "[%key:component::generic::config::error::malformed_url%]", diff --git a/homeassistant/components/generic_hygrostat/__init__.py b/homeassistant/components/generic_hygrostat/__init__.py index 585d0aa1fe3..467a9f0e0c5 100644 --- a/homeassistant/components/generic_hygrostat/__init__.py +++ b/homeassistant/components/generic_hygrostat/__init__.py @@ -1,4 +1,5 @@ """The generic_hygrostat component.""" + import voluptuous as vol from homeassistant.components.humidifier import HumidifierDeviceClass @@ -35,13 +36,13 @@ HYGROSTAT_SCHEMA = vol.Schema( vol.Optional(CONF_DEVICE_CLASS): vol.In( [HumidifierDeviceClass.HUMIDIFIER, HumidifierDeviceClass.DEHUMIDIFIER] ), - vol.Optional(CONF_MAX_HUMIDITY): vol.Coerce(int), + vol.Optional(CONF_MAX_HUMIDITY): vol.Coerce(float), vol.Optional(CONF_MIN_DUR): vol.All(cv.time_period, cv.positive_timedelta), - vol.Optional(CONF_MIN_HUMIDITY): vol.Coerce(int), + vol.Optional(CONF_MIN_HUMIDITY): vol.Coerce(float), vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_DRY_TOLERANCE, default=DEFAULT_TOLERANCE): vol.Coerce(float), vol.Optional(CONF_WET_TOLERANCE, default=DEFAULT_TOLERANCE): vol.Coerce(float), - vol.Optional(CONF_TARGET_HUMIDITY): vol.Coerce(int), + vol.Optional(CONF_TARGET_HUMIDITY): vol.Coerce(float), vol.Optional(CONF_KEEP_ALIVE): vol.All(cv.time_period, cv.positive_timedelta), vol.Optional(CONF_INITIAL_STATE): cv.boolean, vol.Optional(CONF_AWAY_HUMIDITY): vol.Coerce(int), diff --git a/homeassistant/components/generic_hygrostat/humidifier.py b/homeassistant/components/generic_hygrostat/humidifier.py index d69a8a968c7..02641acccae 100644 --- a/homeassistant/components/generic_hygrostat/humidifier.py +++ b/homeassistant/components/generic_hygrostat/humidifier.py @@ -1,4 +1,5 @@ """Adds support for generic hygrostat units.""" + from __future__ import annotations import asyncio @@ -84,9 +85,9 @@ async def async_setup_platform( name: str = config[CONF_NAME] switch_entity_id: str = config[CONF_HUMIDIFIER] sensor_entity_id: str = config[CONF_SENSOR] - min_humidity: int | None = config.get(CONF_MIN_HUMIDITY) - max_humidity: int | None = config.get(CONF_MAX_HUMIDITY) - target_humidity: int | None = config.get(CONF_TARGET_HUMIDITY) + min_humidity: float | None = config.get(CONF_MIN_HUMIDITY) + max_humidity: float | None = config.get(CONF_MAX_HUMIDITY) + target_humidity: float | None = config.get(CONF_TARGET_HUMIDITY) device_class: HumidifierDeviceClass | None = config.get(CONF_DEVICE_CLASS) min_cycle_duration: timedelta | None = config.get(CONF_MIN_DUR) sensor_stale_duration: timedelta | None = config.get(CONF_STALE_DURATION) @@ -132,9 +133,9 @@ class GenericHygrostat(HumidifierEntity, RestoreEntity): name: str, switch_entity_id: str, sensor_entity_id: str, - min_humidity: int | None, - max_humidity: int | None, - target_humidity: int | None, + min_humidity: float | None, + max_humidity: float | None, + target_humidity: float | None, device_class: HumidifierDeviceClass | None, min_cycle_duration: timedelta | None, dry_tolerance: float, @@ -263,12 +264,12 @@ class GenericHygrostat(HumidifierEntity, RestoreEntity): return self._state @property - def current_humidity(self) -> int | None: + def current_humidity(self) -> float | None: """Return the measured humidity.""" - return int(self._cur_humidity) if self._cur_humidity is not None else None + return self._cur_humidity @property - def target_humidity(self) -> int | None: + def target_humidity(self) -> float | None: """Return the humidity we try to reach.""" return self._target_humidity @@ -325,7 +326,7 @@ class GenericHygrostat(HumidifierEntity, RestoreEntity): self.async_write_ha_state() @property - def min_humidity(self) -> int: + def min_humidity(self) -> float: """Return the minimum humidity.""" if self._min_humidity: return self._min_humidity @@ -334,7 +335,7 @@ class GenericHygrostat(HumidifierEntity, RestoreEntity): return super().min_humidity @property - def max_humidity(self) -> int: + def max_humidity(self) -> float: """Return the maximum humidity.""" if self._max_humidity: return self._max_humidity diff --git a/homeassistant/components/generic_thermostat/climate.py b/homeassistant/components/generic_thermostat/climate.py index 64fde0dfd26..42fd2ef6f41 100644 --- a/homeassistant/components/generic_thermostat/climate.py +++ b/homeassistant/components/generic_thermostat/climate.py @@ -1,4 +1,5 @@ """Adds support for generic thermostat units.""" + from __future__ import annotations import asyncio @@ -59,7 +60,7 @@ from homeassistant.helpers.event import ( ) from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.restore_state import RestoreEntity -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, EventType +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import DOMAIN, PLATFORMS @@ -235,7 +236,7 @@ class GenericThermostat(ClimateEntity, RestoreEntity): ) if len(presets): self._attr_supported_features |= ClimateEntityFeature.PRESET_MODE - self._attr_preset_modes = [PRESET_NONE] + list(presets.keys()) + self._attr_preset_modes = [PRESET_NONE, *presets.keys()] else: self._attr_preset_modes = [PRESET_NONE] self._presets = presets @@ -278,7 +279,9 @@ class GenericThermostat(ClimateEntity, RestoreEntity): STATE_UNAVAILABLE, STATE_UNKNOWN, ): - self.hass.create_task(self._check_switch_initial_state()) + self.hass.async_create_task( + self._check_switch_initial_state(), eager_start=True + ) if self.hass.state is CoreState.running: _async_startup() @@ -412,9 +415,7 @@ class GenericThermostat(ClimateEntity, RestoreEntity): # Get default temp from super class return super().max_temp - async def _async_sensor_changed( - self, event: EventType[EventStateChangedData] - ) -> None: + async def _async_sensor_changed(self, event: Event[EventStateChangedData]) -> None: """Handle temperature changes.""" new_state = event.data["new_state"] if new_state is None or new_state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN): @@ -437,14 +438,16 @@ class GenericThermostat(ClimateEntity, RestoreEntity): await self._async_heater_turn_off() @callback - def _async_switch_changed(self, event: EventType[EventStateChangedData]) -> None: + def _async_switch_changed(self, event: Event[EventStateChangedData]) -> None: """Handle heater switch state changes.""" new_state = event.data["new_state"] old_state = event.data["old_state"] if new_state is None: return if old_state is None: - self.hass.create_task(self._check_switch_initial_state()) + self.hass.async_create_task( + self._check_switch_initial_state(), eager_start=True + ) self.async_write_ha_state() @callback diff --git a/homeassistant/components/generic_thermostat/icons.json b/homeassistant/components/generic_thermostat/icons.json new file mode 100644 index 00000000000..a03163179cb --- /dev/null +++ b/homeassistant/components/generic_thermostat/icons.json @@ -0,0 +1,5 @@ +{ + "services": { + "reload": "mdi:reload" + } +} diff --git a/homeassistant/components/geniushub/__init__.py b/homeassistant/components/geniushub/__init__.py index 955c76fe0fc..5fc21a3e5b4 100644 --- a/homeassistant/components/geniushub/__init__.py +++ b/homeassistant/components/geniushub/__init__.py @@ -1,4 +1,5 @@ """Support for a Genius Hub system.""" + from __future__ import annotations from datetime import datetime, timedelta diff --git a/homeassistant/components/geniushub/binary_sensor.py b/homeassistant/components/geniushub/binary_sensor.py index a2f71bfd9fe..f078bb4b363 100644 --- a/homeassistant/components/geniushub/binary_sensor.py +++ b/homeassistant/components/geniushub/binary_sensor.py @@ -1,4 +1,5 @@ """Support for Genius Hub binary_sensor devices.""" + from __future__ import annotations from homeassistant.components.binary_sensor import BinarySensorEntity diff --git a/homeassistant/components/geniushub/climate.py b/homeassistant/components/geniushub/climate.py index cb817c64930..02038ced198 100644 --- a/homeassistant/components/geniushub/climate.py +++ b/homeassistant/components/geniushub/climate.py @@ -1,4 +1,5 @@ """Support for Genius Hub climate devices.""" + from __future__ import annotations from homeassistant.components.climate import ( diff --git a/homeassistant/components/geniushub/icons.json b/homeassistant/components/geniushub/icons.json new file mode 100644 index 00000000000..41697b419a8 --- /dev/null +++ b/homeassistant/components/geniushub/icons.json @@ -0,0 +1,7 @@ +{ + "services": { + "set_zone_mode": "mdi:auto-mode", + "set_zone_override": "mdi:thermometer-lines", + "set_switch_override": "mdi:toggle-switch-variant" + } +} diff --git a/homeassistant/components/geniushub/sensor.py b/homeassistant/components/geniushub/sensor.py index 22d95be079e..998bd6f1edb 100644 --- a/homeassistant/components/geniushub/sensor.py +++ b/homeassistant/components/geniushub/sensor.py @@ -1,4 +1,5 @@ """Support for Genius Hub sensor devices.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/geniushub/switch.py b/homeassistant/components/geniushub/switch.py index 7b9bf8f6112..b703df57f90 100644 --- a/homeassistant/components/geniushub/switch.py +++ b/homeassistant/components/geniushub/switch.py @@ -1,4 +1,5 @@ """Support for Genius Hub switch/outlet devices.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/geniushub/water_heater.py b/homeassistant/components/geniushub/water_heater.py index f8cf7288e57..6c3b5223ef9 100644 --- a/homeassistant/components/geniushub/water_heater.py +++ b/homeassistant/components/geniushub/water_heater.py @@ -1,4 +1,5 @@ """Support for Genius Hub water_heater devices.""" + from __future__ import annotations from homeassistant.components.water_heater import ( diff --git a/homeassistant/components/geo_json_events/__init__.py b/homeassistant/components/geo_json_events/__init__.py index 64b589f4f90..a50f7e432d9 100644 --- a/homeassistant/components/geo_json_events/__init__.py +++ b/homeassistant/components/geo_json_events/__init__.py @@ -1,4 +1,5 @@ """The GeoJSON events component.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/geo_json_events/config_flow.py b/homeassistant/components/geo_json_events/config_flow.py index ffa1c2070e9..65e5d2b1c75 100644 --- a/homeassistant/components/geo_json_events/config_flow.py +++ b/homeassistant/components/geo_json_events/config_flow.py @@ -1,4 +1,5 @@ """Config flow to configure the GeoJSON events integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -6,7 +7,7 @@ from typing import Any import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import ( CONF_LATITUDE, CONF_LOCATION, @@ -15,7 +16,6 @@ from homeassistant.const import ( CONF_URL, UnitOfLength, ) -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_validation as cv, selector from homeassistant.util.unit_conversion import DistanceConverter @@ -31,12 +31,12 @@ DATA_SCHEMA = vol.Schema( ) -class GeoJsonEventsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class GeoJsonEventsFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a GeoJSON events config flow.""" async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the start of the config flow.""" if not user_input: suggested_values: Mapping[str, Any] = { diff --git a/homeassistant/components/geo_json_events/const.py b/homeassistant/components/geo_json_events/const.py index 15f8b0a5b84..679e8f2e565 100644 --- a/homeassistant/components/geo_json_events/const.py +++ b/homeassistant/components/geo_json_events/const.py @@ -1,4 +1,5 @@ """Define constants for the GeoJSON events integration.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/geo_json_events/geo_location.py b/homeassistant/components/geo_json_events/geo_location.py index 134f6a0e943..e0067bcfdc9 100644 --- a/homeassistant/components/geo_json_events/geo_location.py +++ b/homeassistant/components/geo_json_events/geo_location.py @@ -1,4 +1,5 @@ """Support for generic GeoJSON events.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/geo_json_events/manager.py b/homeassistant/components/geo_json_events/manager.py index 93f74831ecb..deff15436a6 100644 --- a/homeassistant/components/geo_json_events/manager.py +++ b/homeassistant/components/geo_json_events/manager.py @@ -1,4 +1,5 @@ """Entity manager for generic GeoJSON events.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/geo_location/__init__.py b/homeassistant/components/geo_location/__init__.py index c5e91d32b20..d81fab65dc9 100644 --- a/homeassistant/components/geo_location/__init__.py +++ b/homeassistant/components/geo_location/__init__.py @@ -1,4 +1,5 @@ """Support for Geolocation.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/geo_location/trigger.py b/homeassistant/components/geo_location/trigger.py index f4ed94f1cf4..fb6140f707c 100644 --- a/homeassistant/components/geo_location/trigger.py +++ b/homeassistant/components/geo_location/trigger.py @@ -1,4 +1,5 @@ """Offer geolocation automation rules.""" + from __future__ import annotations import logging @@ -7,7 +8,14 @@ from typing import Final import voluptuous as vol from homeassistant.const import CONF_EVENT, CONF_PLATFORM, CONF_SOURCE, CONF_ZONE -from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, State, callback +from homeassistant.core import ( + CALLBACK_TYPE, + Event, + HassJob, + HomeAssistant, + State, + callback, +) from homeassistant.helpers import condition, config_validation as cv from homeassistant.helpers.config_validation import entity_domain from homeassistant.helpers.event import ( @@ -16,7 +24,7 @@ from homeassistant.helpers.event import ( async_track_state_change_filtered, ) from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo -from homeassistant.helpers.typing import ConfigType, EventType +from homeassistant.helpers.typing import ConfigType from . import DOMAIN @@ -57,7 +65,7 @@ async def async_attach_trigger( job = HassJob(action) @callback - def state_change_listener(event: EventType[EventStateChangedData]) -> None: + def state_change_listener(event: Event[EventStateChangedData]) -> None: """Handle specific state changes.""" # Skip if the event's source does not match the trigger's source. from_state = event.data["old_state"] diff --git a/homeassistant/components/geo_rss_events/sensor.py b/homeassistant/components/geo_rss_events/sensor.py index 3e6a0923886..8c704bcf16a 100644 --- a/homeassistant/components/geo_rss_events/sensor.py +++ b/homeassistant/components/geo_rss_events/sensor.py @@ -4,6 +4,7 @@ Retrieves current events (typically incidents or alerts) in GeoRSS format, and shows information on events filtered by distance to the HA instance's location and grouped by category. """ + from __future__ import annotations from datetime import timedelta @@ -161,9 +162,9 @@ class GeoRssServiceSensor(SensorEntity): # And now compute the attributes from the filtered events. matrix = {} for entry in feed_entries: - matrix[ - entry.title - ] = f"{entry.distance_to_home:.0f}{UnitOfLength.KILOMETERS}" + matrix[entry.title] = ( + f"{entry.distance_to_home:.0f}{UnitOfLength.KILOMETERS}" + ) self._state_attributes = matrix elif status == UPDATE_OK_NO_DATA: _LOGGER.debug("Update successful, but no data received from %s", self._feed) diff --git a/homeassistant/components/geocaching/config_flow.py b/homeassistant/components/geocaching/config_flow.py index 56fa56a1f82..05676cc346e 100644 --- a/homeassistant/components/geocaching/config_flow.py +++ b/homeassistant/components/geocaching/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Geocaching.""" + from __future__ import annotations from collections.abc import Mapping @@ -7,7 +8,7 @@ from typing import Any from geocachingapi.geocachingapi import GeocachingApi -from homeassistant.data_entry_flow import FlowResult +from homeassistant.config_entries import ConfigFlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.config_entry_oauth2_flow import AbstractOAuth2FlowHandler @@ -25,19 +26,21 @@ class GeocachingFlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN): """Return logger.""" return logging.getLogger(__name__) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Perform reauth upon an API authentication error.""" return await self.async_step_reauth_confirm() async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """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[str, Any]) -> FlowResult: + async def async_oauth_create_entry(self, data: dict[str, Any]) -> ConfigFlowResult: """Create an oauth config entry or update existing entry for reauth.""" api = GeocachingApi( environment=ENVIRONMENT, diff --git a/homeassistant/components/geocaching/const.py b/homeassistant/components/geocaching/const.py index 13b42b318c0..8c255f5452a 100644 --- a/homeassistant/components/geocaching/const.py +++ b/homeassistant/components/geocaching/const.py @@ -1,4 +1,5 @@ """Constants for the Geocaching integration.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/geocaching/coordinator.py b/homeassistant/components/geocaching/coordinator.py index f02cccf544b..8f56cd9d846 100644 --- a/homeassistant/components/geocaching/coordinator.py +++ b/homeassistant/components/geocaching/coordinator.py @@ -1,4 +1,5 @@ """Provides the Geocaching DataUpdateCoordinator.""" + from __future__ import annotations from geocachingapi.exceptions import GeocachingApiError diff --git a/homeassistant/components/geocaching/models.py b/homeassistant/components/geocaching/models.py index 60ee4e05978..3ddea00217f 100644 --- a/homeassistant/components/geocaching/models.py +++ b/homeassistant/components/geocaching/models.py @@ -1,4 +1,5 @@ """Models for the Geocaching integration.""" + from typing import TypedDict diff --git a/homeassistant/components/geocaching/oauth.py b/homeassistant/components/geocaching/oauth.py index 848c4fce66c..c872f9a7522 100644 --- a/homeassistant/components/geocaching/oauth.py +++ b/homeassistant/components/geocaching/oauth.py @@ -1,4 +1,5 @@ """oAuth2 functions and classes for Geocaching API integration.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/geocaching/sensor.py b/homeassistant/components/geocaching/sensor.py index 91f7addae44..c082e5308a1 100644 --- a/homeassistant/components/geocaching/sensor.py +++ b/homeassistant/components/geocaching/sensor.py @@ -1,4 +1,5 @@ """Platform for sensor integration.""" + from __future__ import annotations from collections.abc import Callable @@ -18,20 +19,13 @@ from .const import DOMAIN from .coordinator import GeocachingDataUpdateCoordinator -@dataclass(frozen=True) -class GeocachingRequiredKeysMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class GeocachingSensorEntityDescription(SensorEntityDescription): + """Define Sensor entity description class.""" value_fn: Callable[[GeocachingStatus], str | int | None] -@dataclass(frozen=True) -class GeocachingSensorEntityDescription( - SensorEntityDescription, GeocachingRequiredKeysMixin -): - """Define Sensor entity description class.""" - - SENSORS: tuple[GeocachingSensorEntityDescription, ...] = ( GeocachingSensorEntityDescription( key="find_count", diff --git a/homeassistant/components/geofency/__init__.py b/homeassistant/components/geofency/__init__.py index 2e6ed8429dd..547d7bdb5bc 100644 --- a/homeassistant/components/geofency/__init__.py +++ b/homeassistant/components/geofency/__init__.py @@ -1,4 +1,5 @@ """Support for Geofency.""" + from http import HTTPStatus from aiohttp import web diff --git a/homeassistant/components/geofency/config_flow.py b/homeassistant/components/geofency/config_flow.py index 2d8bce86d74..e51ef86b0cc 100644 --- a/homeassistant/components/geofency/config_flow.py +++ b/homeassistant/components/geofency/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Geofency.""" + from homeassistant.helpers import config_entry_flow from .const import DOMAIN diff --git a/homeassistant/components/geofency/device_tracker.py b/homeassistant/components/geofency/device_tracker.py index f0159915d32..178c72d2071 100644 --- a/homeassistant/components/geofency/device_tracker.py +++ b/homeassistant/components/geofency/device_tracker.py @@ -1,4 +1,5 @@ """Support for the Geofency device tracker platform.""" + from homeassistant.components.device_tracker import SourceType, TrackerEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE @@ -29,9 +30,9 @@ async def async_setup_entry( async_add_entities([GeofencyEntity(device, gps, location_name, attributes)]) - hass.data[GF_DOMAIN]["unsub_device_tracker"][ - config_entry.entry_id - ] = async_dispatcher_connect(hass, TRACKER_UPDATE, _receive_data) + hass.data[GF_DOMAIN]["unsub_device_tracker"][config_entry.entry_id] = ( + async_dispatcher_connect(hass, TRACKER_UPDATE, _receive_data) + ) # Restore previously loaded devices dev_reg = dr.async_get(hass) diff --git a/homeassistant/components/geonetnz_quakes/__init__.py b/homeassistant/components/geonetnz_quakes/__init__.py index dfe51f3119e..b9443d4aed8 100644 --- a/homeassistant/components/geonetnz_quakes/__init__.py +++ b/homeassistant/components/geonetnz_quakes/__init__.py @@ -1,4 +1,5 @@ """The GeoNet NZ Quakes integration.""" + from datetime import timedelta import logging diff --git a/homeassistant/components/geonetnz_quakes/config_flow.py b/homeassistant/components/geonetnz_quakes/config_flow.py index 64db0ac2d7b..4367f820bd3 100644 --- a/homeassistant/components/geonetnz_quakes/config_flow.py +++ b/homeassistant/components/geonetnz_quakes/config_flow.py @@ -1,9 +1,10 @@ """Config flow to configure the GeoNet NZ Quakes integration.""" + import logging import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow from homeassistant.const import ( CONF_LATITUDE, CONF_LONGITUDE, @@ -34,7 +35,7 @@ DATA_SCHEMA = vol.Schema( _LOGGER = logging.getLogger(__name__) -class GeonetnzQuakesFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class GeonetnzQuakesFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a GeoNet NZ Quakes config flow.""" async def _show_form(self, errors=None): diff --git a/homeassistant/components/geonetnz_quakes/const.py b/homeassistant/components/geonetnz_quakes/const.py index 6ec2199f9e4..db529a17fbe 100644 --- a/homeassistant/components/geonetnz_quakes/const.py +++ b/homeassistant/components/geonetnz_quakes/const.py @@ -1,4 +1,5 @@ """Define constants for the GeoNet NZ Quakes integration.""" + from datetime import timedelta from homeassistant.const import Platform diff --git a/homeassistant/components/geonetnz_quakes/geo_location.py b/homeassistant/components/geonetnz_quakes/geo_location.py index 6fa84f590f1..f3458d96a18 100644 --- a/homeassistant/components/geonetnz_quakes/geo_location.py +++ b/homeassistant/components/geonetnz_quakes/geo_location.py @@ -1,4 +1,5 @@ """Geolocation support for GeoNet NZ Quakes Feeds.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/geonetnz_quakes/sensor.py b/homeassistant/components/geonetnz_quakes/sensor.py index e69ba6eb005..020b76a6c97 100644 --- a/homeassistant/components/geonetnz_quakes/sensor.py +++ b/homeassistant/components/geonetnz_quakes/sensor.py @@ -1,4 +1,5 @@ """Feed Entity Manager Sensor support for GeoNet NZ Quakes Feeds.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/geonetnz_volcano/__init__.py b/homeassistant/components/geonetnz_volcano/__init__.py index fb7770a5461..b08d6d62c55 100644 --- a/homeassistant/components/geonetnz_volcano/__init__.py +++ b/homeassistant/components/geonetnz_volcano/__init__.py @@ -1,4 +1,5 @@ """The GeoNet NZ Volcano integration.""" + from __future__ import annotations from datetime import datetime, timedelta diff --git a/homeassistant/components/geonetnz_volcano/config_flow.py b/homeassistant/components/geonetnz_volcano/config_flow.py index 34e16970d4f..461da61ae1a 100644 --- a/homeassistant/components/geonetnz_volcano/config_flow.py +++ b/homeassistant/components/geonetnz_volcano/config_flow.py @@ -1,7 +1,8 @@ """Config flow to configure the GeoNet NZ Volcano integration.""" + import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow from homeassistant.const import ( CONF_LATITUDE, CONF_LONGITUDE, @@ -31,7 +32,7 @@ def configured_instances(hass): } -class GeonetnzVolcanoFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class GeonetnzVolcanoFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a GeoNet NZ Volcano config flow.""" async def _show_form(self, errors=None): diff --git a/homeassistant/components/geonetnz_volcano/const.py b/homeassistant/components/geonetnz_volcano/const.py index 8c17ff42544..be04a25d27a 100644 --- a/homeassistant/components/geonetnz_volcano/const.py +++ b/homeassistant/components/geonetnz_volcano/const.py @@ -1,4 +1,5 @@ """Define constants for the GeoNet NZ Volcano integration.""" + from datetime import timedelta from homeassistant.const import Platform diff --git a/homeassistant/components/geonetnz_volcano/sensor.py b/homeassistant/components/geonetnz_volcano/sensor.py index f02e076b66c..6197577b56c 100644 --- a/homeassistant/components/geonetnz_volcano/sensor.py +++ b/homeassistant/components/geonetnz_volcano/sensor.py @@ -1,4 +1,5 @@ """Feed Entity Manager Sensor support for GeoNet NZ Volcano Feeds.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/gios/__init__.py b/homeassistant/components/gios/__init__.py index 88c505fc4ae..5810a32f80f 100644 --- a/homeassistant/components/gios/__init__.py +++ b/homeassistant/components/gios/__init__.py @@ -1,4 +1,5 @@ """The GIOS component.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/gios/config_flow.py b/homeassistant/components/gios/config_flow.py index 1595b7ad131..a089aeab820 100644 --- a/homeassistant/components/gios/config_flow.py +++ b/homeassistant/components/gios/config_flow.py @@ -1,4 +1,5 @@ """Adds config flow for GIOS.""" + from __future__ import annotations import asyncio @@ -8,22 +9,21 @@ from aiohttp.client_exceptions import ClientConnectorError from gios import ApiError, Gios, InvalidSensorsDataError, NoStationError import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_NAME -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import API_TIMEOUT, CONF_STATION_ID, DOMAIN -class GiosFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class GiosFlowHandler(ConfigFlow, domain=DOMAIN): """Config flow for GIOS.""" VERSION = 1 async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" errors = {} diff --git a/homeassistant/components/gios/const.py b/homeassistant/components/gios/const.py index 33ddfae6fe1..a8490511ab8 100644 --- a/homeassistant/components/gios/const.py +++ b/homeassistant/components/gios/const.py @@ -1,4 +1,5 @@ """Constants for GIOS integration.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/gios/diagnostics.py b/homeassistant/components/gios/diagnostics.py index 954580d5c3f..1cdd9299a1c 100644 --- a/homeassistant/components/gios/diagnostics.py +++ b/homeassistant/components/gios/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for GIOS.""" + from __future__ import annotations from dataclasses import asdict diff --git a/homeassistant/components/gios/sensor.py b/homeassistant/components/gios/sensor.py index 0437f8f6172..c2da9239453 100644 --- a/homeassistant/components/gios/sensor.py +++ b/homeassistant/components/gios/sensor.py @@ -1,4 +1,5 @@ """Support for the GIOS service.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/gios/system_health.py b/homeassistant/components/gios/system_health.py index 589dc428bcb..46fe78556e2 100644 --- a/homeassistant/components/gios/system_health.py +++ b/homeassistant/components/gios/system_health.py @@ -1,4 +1,5 @@ """Provide info to system health.""" + from __future__ import annotations from typing import Any, Final diff --git a/homeassistant/components/github/__init__.py b/homeassistant/components/github/__init__.py index 725c319dd58..20df559b819 100644 --- a/homeassistant/components/github/__init__.py +++ b/homeassistant/components/github/__init__.py @@ -1,4 +1,5 @@ """The GitHub integration.""" + from __future__ import annotations from aiogithubapi import GitHubAPI @@ -25,7 +26,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: client = GitHubAPI( token=entry.data[CONF_ACCESS_TOKEN], session=async_get_clientsession(hass), - **{"client_name": SERVER_SOFTWARE}, + client_name=SERVER_SOFTWARE, ) repositories: list[str] = entry.options[CONF_REPOSITORIES] diff --git a/homeassistant/components/github/config_flow.py b/homeassistant/components/github/config_flow.py index aa7ec7b6f86..25d8782618f 100644 --- a/homeassistant/components/github/config_flow.py +++ b/homeassistant/components/github/config_flow.py @@ -1,4 +1,5 @@ """Config flow for GitHub integration.""" + from __future__ import annotations import asyncio @@ -14,10 +15,14 @@ from aiogithubapi import ( from aiogithubapi.const import OAUTH_USER_LOGIN import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.const import CONF_ACCESS_TOKEN from homeassistant.core import HomeAssistant, callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import ( SERVER_SOFTWARE, async_get_clientsession, @@ -33,12 +38,12 @@ async def get_repositories(hass: HomeAssistant, access_token: str) -> list[str]: repositories = set() async def _get_starred_repositories() -> None: - response = await client.user.starred(**{"params": {"per_page": 100}}) + response = await client.user.starred(params={"per_page": 100}) if not response.is_last_page: results = await asyncio.gather( *( client.user.starred( - **{"params": {"per_page": 100, "page": page_number}}, + params={"per_page": 100, "page": page_number}, ) for page_number in range( response.next_page_number, response.last_page_number + 1 @@ -51,12 +56,12 @@ async def get_repositories(hass: HomeAssistant, access_token: str) -> list[str]: repositories.update(response.data) async def _get_personal_repositories() -> None: - response = await client.user.repos(**{"params": {"per_page": 100}}) + response = await client.user.repos(params={"per_page": 100}) if not response.is_last_page: results = await asyncio.gather( *( client.user.repos( - **{"params": {"per_page": 100, "page": page_number}}, + params={"per_page": 100, "page": page_number}, ) for page_number in range( response.next_page_number, response.last_page_number + 1 @@ -88,7 +93,7 @@ async def get_repositories(hass: HomeAssistant, access_token: str) -> list[str]: ) -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class GitHubConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for GitHub.""" VERSION = 1 @@ -104,7 +109,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None, - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" if self._async_current_entries(): return self.async_abort(reason="already_configured") @@ -114,7 +119,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_device( self, user_input: dict[str, Any] | None = None, - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle device steps.""" async def _wait_for_login() -> None: @@ -132,7 +137,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._device = GitHubDeviceAPI( client_id=CLIENT_ID, session=async_get_clientsession(self.hass), - **{"client_name": SERVER_SOFTWARE}, + client_name=SERVER_SOFTWARE, ) try: @@ -167,7 +172,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_repositories( self, user_input: dict[str, Any] | None = None, - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle repositories step.""" if TYPE_CHECKING: @@ -196,30 +201,30 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_could_not_register( self, user_input: dict[str, Any] | None = None, - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle issues that need transition await from progress step.""" return self.async_abort(reason="could_not_register") @staticmethod @callback def async_get_options_flow( - config_entry: config_entries.ConfigEntry, + config_entry: ConfigEntry, ) -> OptionsFlowHandler: """Get the options flow for this handler.""" return OptionsFlowHandler(config_entry) -class OptionsFlowHandler(config_entries.OptionsFlow): +class OptionsFlowHandler(OptionsFlow): """Handle a option flow for GitHub.""" - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + 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: + ) -> ConfigFlowResult: """Handle options flow.""" if not user_input: configured_repositories: list[str] = self.config_entry.options[ diff --git a/homeassistant/components/github/const.py b/homeassistant/components/github/const.py index d01656ee8ae..df44860b780 100644 --- a/homeassistant/components/github/const.py +++ b/homeassistant/components/github/const.py @@ -1,4 +1,5 @@ """Constants for the GitHub integration.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/github/coordinator.py b/homeassistant/components/github/coordinator.py index 45ab055aa9a..e73e02932e9 100644 --- a/homeassistant/components/github/coordinator.py +++ b/homeassistant/components/github/coordinator.py @@ -1,4 +1,5 @@ """Custom data update coordinator for the GitHub integration.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/github/diagnostics.py b/homeassistant/components/github/diagnostics.py index 15626497344..df1e4b4a4cf 100644 --- a/homeassistant/components/github/diagnostics.py +++ b/homeassistant/components/github/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for the GitHub integration.""" + from __future__ import annotations from typing import Any @@ -26,7 +27,7 @@ async def async_get_config_entry_diagnostics( client = GitHubAPI( token=config_entry.data[CONF_ACCESS_TOKEN], session=async_get_clientsession(hass), - **{"client_name": SERVER_SOFTWARE}, + client_name=SERVER_SOFTWARE, ) try: diff --git a/homeassistant/components/github/icons.json b/homeassistant/components/github/icons.json new file mode 100644 index 00000000000..f8a2eefe0c8 --- /dev/null +++ b/homeassistant/components/github/icons.json @@ -0,0 +1,42 @@ +{ + "entity": { + "sensor": { + "discussions_count": { + "default": "mdi:forum" + }, + "stargazers_count": { + "default": "mdi:star" + }, + "subscribers_count": { + "default": "mdi:glasses" + }, + "forks_count": { + "default": "mdi:source-fork" + }, + "issues_count": { + "default": "mdi:github" + }, + "pulls_count": { + "default": "mdi:source-pull" + }, + "latest_commit": { + "default": "mdi:source-commit" + }, + "latest_discussion": { + "default": "mdi:forum" + }, + "latest_release": { + "default": "mdi:github" + }, + "latest_issue": { + "default": "mdi:github" + }, + "latest_pull_request": { + "default": "mdi:source-pull" + }, + "latest_tag": { + "default": "mdi:tag" + } + } + } +} diff --git a/homeassistant/components/github/sensor.py b/homeassistant/components/github/sensor.py index cec0e6b763f..a082f888767 100644 --- a/homeassistant/components/github/sensor.py +++ b/homeassistant/components/github/sensor.py @@ -1,4 +1,5 @@ """Sensor platform for the GitHub integration.""" + from __future__ import annotations from collections.abc import Callable, Mapping @@ -22,27 +23,16 @@ from .const import DOMAIN from .coordinator import GitHubDataUpdateCoordinator -@dataclass(frozen=True) -class BaseEntityDescriptionMixin: - """Mixin for required GitHub base description keys.""" +@dataclass(frozen=True, kw_only=True) +class GitHubSensorEntityDescription(SensorEntityDescription): + """Describes GitHub issue sensor entity.""" value_fn: Callable[[dict[str, Any]], StateType] - -@dataclass(frozen=True) -class BaseEntityDescription(SensorEntityDescription): - """Describes GitHub sensor entity default overrides.""" - - icon: str = "mdi:github" attr_fn: Callable[[dict[str, Any]], Mapping[str, Any] | None] = lambda data: None avabl_fn: Callable[[dict[str, Any]], bool] = lambda data: True -@dataclass(frozen=True) -class GitHubSensorEntityDescription(BaseEntityDescription, BaseEntityDescriptionMixin): - """Describes GitHub issue sensor entity.""" - - SENSOR_DESCRIPTIONS: tuple[GitHubSensorEntityDescription, ...] = ( GitHubSensorEntityDescription( key="discussions_count", @@ -55,7 +45,6 @@ SENSOR_DESCRIPTIONS: tuple[GitHubSensorEntityDescription, ...] = ( GitHubSensorEntityDescription( key="stargazers_count", translation_key="stargazers_count", - icon="mdi:star", native_unit_of_measurement="Stars", entity_category=EntityCategory.DIAGNOSTIC, state_class=SensorStateClass.MEASUREMENT, @@ -64,7 +53,6 @@ SENSOR_DESCRIPTIONS: tuple[GitHubSensorEntityDescription, ...] = ( GitHubSensorEntityDescription( key="subscribers_count", translation_key="subscribers_count", - icon="mdi:glasses", native_unit_of_measurement="Watchers", entity_category=EntityCategory.DIAGNOSTIC, state_class=SensorStateClass.MEASUREMENT, @@ -73,7 +61,6 @@ SENSOR_DESCRIPTIONS: tuple[GitHubSensorEntityDescription, ...] = ( GitHubSensorEntityDescription( key="forks_count", translation_key="forks_count", - icon="mdi:source-fork", native_unit_of_measurement="Forks", entity_category=EntityCategory.DIAGNOSTIC, state_class=SensorStateClass.MEASUREMENT, diff --git a/homeassistant/components/gitlab_ci/sensor.py b/homeassistant/components/gitlab_ci/sensor.py index ed0db5416c1..d247ef5af60 100644 --- a/homeassistant/components/gitlab_ci/sensor.py +++ b/homeassistant/components/gitlab_ci/sensor.py @@ -1,4 +1,5 @@ """Sensor for retrieving latest GitLab CI job information.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/gitter/sensor.py b/homeassistant/components/gitter/sensor.py index db5b189d5ea..056c275c785 100644 --- a/homeassistant/components/gitter/sensor.py +++ b/homeassistant/components/gitter/sensor.py @@ -1,4 +1,5 @@ """Support for displaying details about a Gitter.im chat room.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/glances/__init__.py b/homeassistant/components/glances/__init__.py index 1c03f8c1dbf..b6c4f477b46 100644 --- a/homeassistant/components/glances/__init__.py +++ b/homeassistant/components/glances/__init__.py @@ -1,4 +1,5 @@ """The Glances component.""" + import logging from typing import Any diff --git a/homeassistant/components/glances/config_flow.py b/homeassistant/components/glances/config_flow.py index 81d3a118729..9208a4b0ebd 100644 --- a/homeassistant/components/glances/config_flow.py +++ b/homeassistant/components/glances/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Glances.""" + from __future__ import annotations from collections.abc import Mapping @@ -10,7 +11,7 @@ from glances_api.exceptions import ( ) import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, @@ -19,7 +20,6 @@ from homeassistant.const import ( CONF_USERNAME, CONF_VERIFY_SSL, ) -from homeassistant.data_entry_flow import FlowResult from . import ServerVersionMismatch, get_api from .const import DEFAULT_HOST, DEFAULT_PORT, DOMAIN @@ -36,13 +36,15 @@ DATA_SCHEMA = vol.Schema( ) -class GlancesFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class GlancesFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a Glances config flow.""" VERSION = 1 - _reauth_entry: config_entries.ConfigEntry | None + _reauth_entry: ConfigEntry | None - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Perform reauth upon an API authentication error.""" self._reauth_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] @@ -51,7 +53,7 @@ class GlancesFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_reauth_confirm( self, user_input: dict[str, str] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm reauth dialog.""" errors = {} assert self._reauth_entry @@ -85,7 +87,7 @@ class GlancesFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors = {} if user_input is not None: diff --git a/homeassistant/components/glances/coordinator.py b/homeassistant/components/glances/coordinator.py index 9fa9346b95f..9a1b281eec2 100644 --- a/homeassistant/components/glances/coordinator.py +++ b/homeassistant/components/glances/coordinator.py @@ -1,4 +1,5 @@ """Coordinator for Glances integration.""" + import logging from typing import Any diff --git a/homeassistant/components/glances/sensor.py b/homeassistant/components/glances/sensor.py index f3718dc4c0e..7db06a08496 100644 --- a/homeassistant/components/glances/sensor.py +++ b/homeassistant/components/glances/sensor.py @@ -1,4 +1,5 @@ """Support gathering system information of hosts which are running glances.""" + from __future__ import annotations from dataclasses import dataclass @@ -27,20 +28,13 @@ from . import GlancesDataUpdateCoordinator from .const import CPU_ICON, DOMAIN -@dataclass(frozen=True) -class GlancesSensorEntityDescriptionMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class GlancesSensorEntityDescription(SensorEntityDescription): + """Describe Glances sensor entity.""" type: str -@dataclass(frozen=True) -class GlancesSensorEntityDescription( - SensorEntityDescription, GlancesSensorEntityDescriptionMixin -): - """Describe Glances sensor entity.""" - - SENSOR_TYPES = { ("fs", "disk_use_percent"): GlancesSensorEntityDescription( key="disk_use_percent", @@ -229,29 +223,29 @@ async def async_setup_entry( """Set up the Glances sensors.""" coordinator: GlancesDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] - entities = [] + entities: list[GlancesSensor] = [] for sensor_type, sensors in coordinator.data.items(): if sensor_type in ["fs", "sensors", "raid"]: - for sensor_label, params in sensors.items(): - for param in params: - if sensor_description := SENSOR_TYPES.get((sensor_type, param)): - entities.append( - GlancesSensor( - coordinator, - sensor_description, - sensor_label, - ) - ) + entities.extend( + GlancesSensor( + coordinator, + sensor_description, + sensor_label, + ) + for sensor_label, params in sensors.items() + for param in params + if (sensor_description := SENSOR_TYPES.get((sensor_type, param))) + ) else: - for sensor in sensors: - if sensor_description := SENSOR_TYPES.get((sensor_type, sensor)): - entities.append( - GlancesSensor( - coordinator, - sensor_description, - ) - ) + entities.extend( + GlancesSensor( + coordinator, + sensor_description, + ) + for sensor in sensors + if (sensor_description := SENSOR_TYPES.get((sensor_type, sensor))) + ) async_add_entities(entities) diff --git a/homeassistant/components/goalzero/__init__.py b/homeassistant/components/goalzero/__init__.py index 7a7000ba780..60b0338c258 100644 --- a/homeassistant/components/goalzero/__init__.py +++ b/homeassistant/components/goalzero/__init__.py @@ -1,4 +1,5 @@ """The Goal Zero Yeti integration.""" + from __future__ import annotations from typing import TYPE_CHECKING diff --git a/homeassistant/components/goalzero/binary_sensor.py b/homeassistant/components/goalzero/binary_sensor.py index 9464067d426..eec8773db30 100644 --- a/homeassistant/components/goalzero/binary_sensor.py +++ b/homeassistant/components/goalzero/binary_sensor.py @@ -1,4 +1,5 @@ """Support for Goal Zero Yeti Sensors.""" + from __future__ import annotations from typing import cast diff --git a/homeassistant/components/goalzero/config_flow.py b/homeassistant/components/goalzero/config_flow.py index 2312b6bd183..c276db135fa 100644 --- a/homeassistant/components/goalzero/config_flow.py +++ b/homeassistant/components/goalzero/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Goal Zero Yeti integration.""" + from __future__ import annotations import logging @@ -7,10 +8,9 @@ from typing import Any from goalzero import Yeti, exceptions import voluptuous as vol -from homeassistant import config_entries from homeassistant.components import dhcp +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_NAME -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.device_registry import format_mac @@ -19,7 +19,7 @@ from .const import DEFAULT_NAME, DOMAIN, MANUFACTURER _LOGGER = logging.getLogger(__name__) -class GoalZeroFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class GoalZeroFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a config flow for Goal Zero Yeti.""" VERSION = 1 @@ -28,7 +28,9 @@ class GoalZeroFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Initialize a Goal Zero Yeti flow.""" self.ip_address: str | None = None - async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: + async def async_step_dhcp( + self, discovery_info: dhcp.DhcpServiceInfo + ) -> ConfigFlowResult: """Handle dhcp discovery.""" self.ip_address = discovery_info.ip @@ -43,7 +45,7 @@ class GoalZeroFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_confirm_discovery( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Allow the user to confirm adding the device.""" if user_input is not None: return self.async_create_entry( @@ -65,7 +67,7 @@ class GoalZeroFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initiated by the user.""" errors = {} if user_input is not None: diff --git a/homeassistant/components/goalzero/const.py b/homeassistant/components/goalzero/const.py index 280a70abbf1..0096cffc2aa 100644 --- a/homeassistant/components/goalzero/const.py +++ b/homeassistant/components/goalzero/const.py @@ -1,4 +1,5 @@ """Constants for the Goal Zero Yeti integration.""" + import logging from typing import Final diff --git a/homeassistant/components/goalzero/sensor.py b/homeassistant/components/goalzero/sensor.py index b9a83453d7f..86f8bc9455b 100644 --- a/homeassistant/components/goalzero/sensor.py +++ b/homeassistant/components/goalzero/sensor.py @@ -1,4 +1,5 @@ """Support for Goal Zero Yeti Sensors.""" + from __future__ import annotations from typing import cast diff --git a/homeassistant/components/goalzero/switch.py b/homeassistant/components/goalzero/switch.py index 30680c6ff72..9c0aee03b83 100644 --- a/homeassistant/components/goalzero/switch.py +++ b/homeassistant/components/goalzero/switch.py @@ -1,4 +1,5 @@ """Support for Goal Zero Yeti Switches.""" + from __future__ import annotations from typing import Any, cast diff --git a/homeassistant/components/gogogate2/__init__.py b/homeassistant/components/gogogate2/__init__.py index ece2f6bbbc8..ceb07c99849 100644 --- a/homeassistant/components/gogogate2/__init__.py +++ b/homeassistant/components/gogogate2/__init__.py @@ -18,7 +18,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if CONF_DEVICE not in entry.data: config_updates = { **entry.data, - **{CONF_DEVICE: DEVICE_TYPE_GOGOGATE2}, + CONF_DEVICE: DEVICE_TYPE_GOGOGATE2, } if config_updates: diff --git a/homeassistant/components/gogogate2/common.py b/homeassistant/components/gogogate2/common.py index 093c93699ff..01834187c70 100644 --- a/homeassistant/components/gogogate2/common.py +++ b/homeassistant/components/gogogate2/common.py @@ -1,4 +1,5 @@ """Common code for GogoGate2 component.""" + from __future__ import annotations from collections.abc import Awaitable, Callable, Mapping diff --git a/homeassistant/components/gogogate2/config_flow.py b/homeassistant/components/gogogate2/config_flow.py index 344e8473984..96ab97f5ba5 100644 --- a/homeassistant/components/gogogate2/config_flow.py +++ b/homeassistant/components/gogogate2/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Gogogate2.""" + from __future__ import annotations import dataclasses @@ -10,14 +11,14 @@ from ismartgate.const import GogoGate2ApiErrorCode, ISmartGateApiErrorCode import voluptuous as vol from homeassistant.components import dhcp, zeroconf -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import ( CONF_DEVICE, CONF_IP_ADDRESS, CONF_PASSWORD, CONF_USERNAME, ) -from homeassistant.data_entry_flow import AbortFlow, FlowResult +from homeassistant.data_entry_flow import AbortFlow from .common import get_api from .const import DEVICE_TYPE_GOGOGATE2, DEVICE_TYPE_ISMARTGATE, DOMAIN @@ -40,19 +41,21 @@ class Gogogate2FlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_homekit( self, discovery_info: zeroconf.ZeroconfServiceInfo - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle homekit discovery.""" await self.async_set_unique_id( discovery_info.properties[zeroconf.ATTR_PROPERTIES_ID] ) return await self._async_discovery_handler(discovery_info.host) - async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: + async def async_step_dhcp( + self, discovery_info: dhcp.DhcpServiceInfo + ) -> ConfigFlowResult: """Handle dhcp discovery.""" await self.async_set_unique_id(discovery_info.macaddress) return await self._async_discovery_handler(discovery_info.ip) - async def _async_discovery_handler(self, ip_address: str) -> FlowResult: + async def _async_discovery_handler(self, ip_address: str) -> ConfigFlowResult: """Start the user flow from any discovery.""" self.context[CONF_IP_ADDRESS] = ip_address self._abort_if_unique_id_configured({CONF_IP_ADDRESS: ip_address}) @@ -69,7 +72,7 @@ class Gogogate2FlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle user initiated flow.""" user_input = user_input or {} errors = {} diff --git a/homeassistant/components/gogogate2/cover.py b/homeassistant/components/gogogate2/cover.py index f0039d85295..17cfebe4a70 100644 --- a/homeassistant/components/gogogate2/cover.py +++ b/homeassistant/components/gogogate2/cover.py @@ -1,4 +1,5 @@ """Support for Gogogate2 garage Doors.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/gogogate2/sensor.py b/homeassistant/components/gogogate2/sensor.py index 9100137843a..c67b7f371e2 100644 --- a/homeassistant/components/gogogate2/sensor.py +++ b/homeassistant/components/gogogate2/sensor.py @@ -1,4 +1,5 @@ """Support for Gogogate2 garage Doors.""" + from __future__ import annotations from itertools import chain diff --git a/homeassistant/components/goodwe/button.py b/homeassistant/components/goodwe/button.py index f551e09fc6a..d3d96a19a76 100644 --- a/homeassistant/components/goodwe/button.py +++ b/homeassistant/components/goodwe/button.py @@ -1,4 +1,5 @@ """GoodWe PV inverter selection settings entities.""" + from collections.abc import Awaitable, Callable from dataclasses import dataclass from datetime import datetime @@ -18,20 +19,13 @@ from .const import DOMAIN, KEY_DEVICE_INFO, KEY_INVERTER _LOGGER = logging.getLogger(__name__) -@dataclass(frozen=True) -class GoodweButtonEntityDescriptionRequired: - """Required attributes of GoodweButtonEntityDescription.""" +@dataclass(frozen=True, kw_only=True) +class GoodweButtonEntityDescription(ButtonEntityDescription): + """Class describing Goodwe button entities.""" action: Callable[[Inverter], Awaitable[None]] -@dataclass(frozen=True) -class GoodweButtonEntityDescription( - ButtonEntityDescription, GoodweButtonEntityDescriptionRequired -): - """Class describing Goodwe button entities.""" - - SYNCHRONIZE_CLOCK = GoodweButtonEntityDescription( key="synchronize_clock", translation_key="synchronize_clock", diff --git a/homeassistant/components/goodwe/config_flow.py b/homeassistant/components/goodwe/config_flow.py index ab82d4c453f..d6a3be7e56a 100644 --- a/homeassistant/components/goodwe/config_flow.py +++ b/homeassistant/components/goodwe/config_flow.py @@ -1,4 +1,5 @@ """Config flow to configure Goodwe inverters using their local API.""" + from __future__ import annotations import logging @@ -6,9 +7,8 @@ import logging from goodwe import InverterError, connect import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST -from homeassistant.data_entry_flow import FlowResult from .const import CONF_MODEL_FAMILY, DEFAULT_NAME, DOMAIN @@ -21,12 +21,12 @@ CONFIG_SCHEMA = vol.Schema( _LOGGER = logging.getLogger(__name__) -class GoodweFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class GoodweFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a Goodwe config flow.""" VERSION = 1 - async def async_step_user(self, user_input: dict | None = None) -> FlowResult: + async def async_step_user(self, user_input: dict | None = None) -> ConfigFlowResult: """Handle a flow initialized by the user.""" errors = {} if user_input is not None: diff --git a/homeassistant/components/goodwe/const.py b/homeassistant/components/goodwe/const.py index c5dbaaa49e5..730433c4a66 100644 --- a/homeassistant/components/goodwe/const.py +++ b/homeassistant/components/goodwe/const.py @@ -1,4 +1,5 @@ """Constants for the Goodwe component.""" + from datetime import timedelta from homeassistant.const import Platform diff --git a/homeassistant/components/goodwe/coordinator.py b/homeassistant/components/goodwe/coordinator.py index ac91fba787d..a8ee7df6337 100644 --- a/homeassistant/components/goodwe/coordinator.py +++ b/homeassistant/components/goodwe/coordinator.py @@ -1,4 +1,5 @@ """Update coordinator for Goodwe.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/goodwe/diagnostics.py b/homeassistant/components/goodwe/diagnostics.py index 285036c0254..600f02b9b7e 100644 --- a/homeassistant/components/goodwe/diagnostics.py +++ b/homeassistant/components/goodwe/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for Goodwe.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/goodwe/number.py b/homeassistant/components/goodwe/number.py index 09e056da607..fc8b3864ae9 100644 --- a/homeassistant/components/goodwe/number.py +++ b/homeassistant/components/goodwe/number.py @@ -1,4 +1,5 @@ """GoodWe PV inverter numeric settings entities.""" + from __future__ import annotations from collections.abc import Awaitable, Callable @@ -23,22 +24,15 @@ from .const import DOMAIN, KEY_DEVICE_INFO, KEY_INVERTER _LOGGER = logging.getLogger(__name__) -@dataclass(frozen=True) -class GoodweNumberEntityDescriptionBase: - """Required values when describing Goodwe number entities.""" +@dataclass(frozen=True, kw_only=True) +class GoodweNumberEntityDescription(NumberEntityDescription): + """Class describing Goodwe number entities.""" getter: Callable[[Inverter], Awaitable[int]] setter: Callable[[Inverter, int], Awaitable[None]] filter: Callable[[Inverter], bool] -@dataclass(frozen=True) -class GoodweNumberEntityDescription( - NumberEntityDescription, GoodweNumberEntityDescriptionBase -): - """Class describing Goodwe number entities.""" - - def _get_setting_unit(inverter: Inverter, setting: str) -> str: """Return the unit of an inverter setting.""" return next((s.unit for s in inverter.settings() if s.id_ == setting), "") diff --git a/homeassistant/components/goodwe/select.py b/homeassistant/components/goodwe/select.py index 6d033eab242..f42f50c93fc 100644 --- a/homeassistant/components/goodwe/select.py +++ b/homeassistant/components/goodwe/select.py @@ -1,4 +1,5 @@ """GoodWe PV inverter selection settings entities.""" + import logging from goodwe import Inverter, InverterError, OperationMode diff --git a/homeassistant/components/goodwe/sensor.py b/homeassistant/components/goodwe/sensor.py index a43ff971a9a..795b26a0c9f 100644 --- a/homeassistant/components/goodwe/sensor.py +++ b/homeassistant/components/goodwe/sensor.py @@ -1,4 +1,5 @@ """Support for GoodWe inverter via UDP.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/google/__init__.py b/homeassistant/components/google/__init__.py index e05a6f6fb97..9bb6dbd059f 100644 --- a/homeassistant/components/google/__init__.py +++ b/homeassistant/components/google/__init__.py @@ -1,4 +1,5 @@ """Support for Google - Calendar Event Devices.""" + from __future__ import annotations from collections.abc import Mapping diff --git a/homeassistant/components/google/config_flow.py b/homeassistant/components/google/config_flow.py index ab38e67479f..6207303c8a6 100644 --- a/homeassistant/components/google/config_flow.py +++ b/homeassistant/components/google/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Google integration.""" + from __future__ import annotations import asyncio @@ -10,9 +11,8 @@ from gcal_sync.api import GoogleCalendarService from gcal_sync.exceptions import ApiException, ApiForbiddenException import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigEntry, ConfigFlowResult, OptionsFlow from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -74,7 +74,7 @@ class OAuth2FlowHandler( def __init__(self) -> None: """Set up instance.""" super().__init__() - self._reauth_config_entry: config_entries.ConfigEntry | None = None + self._reauth_config_entry: ConfigEntry | None = None self._device_flow: DeviceFlow | None = None # First attempt is device auth, then fallback to web auth self._web_auth = False @@ -94,7 +94,7 @@ class OAuth2FlowHandler( "prompt": "consent", } - async def async_step_import(self, info: dict[str, Any]) -> FlowResult: + async def async_step_import(self, info: dict[str, Any]) -> ConfigFlowResult: """Import existing auth into a new config entry.""" if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") @@ -108,7 +108,7 @@ class OAuth2FlowHandler( async def async_step_auth( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Create an entry for auth.""" # The default behavior from the parent class is to redirect the # user with an external step. When using the device flow, we instead @@ -179,13 +179,13 @@ class OAuth2FlowHandler( async def async_step_creation( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle external yaml configuration.""" if not self._web_auth and self.external_data.get(DEVICE_AUTH_CREDS) is None: return self.async_abort(reason="code_expired") return await super().async_step_creation(user_input) - async def async_oauth_create_entry(self, data: dict) -> FlowResult: + async def async_oauth_create_entry(self, data: dict) -> ConfigFlowResult: """Create an entry for the flow, or update existing entry.""" data[CONF_CREDENTIAL_TYPE] = ( CredentialType.WEB_AUTH if self._web_auth else CredentialType.DEVICE_AUTH @@ -230,7 +230,9 @@ class OAuth2FlowHandler( }, ) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Perform reauth upon an API authentication error.""" self._reauth_config_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] @@ -240,7 +242,7 @@ class OAuth2FlowHandler( async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm reauth dialog.""" if user_input is None: return self.async_show_form(step_id="reauth_confirm") @@ -249,22 +251,22 @@ class OAuth2FlowHandler( @staticmethod @callback def async_get_options_flow( - config_entry: config_entries.ConfigEntry, - ) -> config_entries.OptionsFlow: + config_entry: ConfigEntry, + ) -> OptionsFlow: """Create an options flow.""" return OptionsFlowHandler(config_entry) -class OptionsFlowHandler(config_entries.OptionsFlow): +class OptionsFlowHandler(OptionsFlow): """Google Calendar options flow.""" - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + 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: + ) -> ConfigFlowResult: """Manage the options.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) diff --git a/homeassistant/components/google/const.py b/homeassistant/components/google/const.py index 6f497543b2d..1e0b2fc910b 100644 --- a/homeassistant/components/google/const.py +++ b/homeassistant/components/google/const.py @@ -1,4 +1,5 @@ """Constants for google integration.""" + from __future__ import annotations from enum import Enum, StrEnum diff --git a/homeassistant/components/google/manifest.json b/homeassistant/components/google/manifest.json index ec9fb7018d6..00561cb5fd6 100644 --- a/homeassistant/components/google/manifest.json +++ b/homeassistant/components/google/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/calendar.google", "iot_class": "cloud_polling", "loggers": ["googleapiclient"], - "requirements": ["gcal-sync==6.0.3", "oauth2client==4.1.3", "ical==7.0.3"] + "requirements": ["gcal-sync==6.0.4", "oauth2client==4.1.3", "ical==7.0.3"] } diff --git a/homeassistant/components/google_assistant/__init__.py b/homeassistant/components/google_assistant/__init__.py index 37f3a2c3edc..273e46040b7 100644 --- a/homeassistant/components/google_assistant/__init__.py +++ b/homeassistant/components/google_assistant/__init__.py @@ -1,4 +1,5 @@ """Support for Actions on Google Assistant Smart Home Control.""" + from __future__ import annotations import logging @@ -28,7 +29,7 @@ from .const import ( # noqa: F401 DEFAULT_EXPOSE_BY_DEFAULT, DEFAULT_EXPOSED_DOMAINS, DOMAIN, - EVENT_QUERY_RECEIVED, # noqa: F401 + EVENT_QUERY_RECEIVED, SERVICE_REQUEST_SYNC, SOURCE_CLOUD, ) diff --git a/homeassistant/components/google_assistant/button.py b/homeassistant/components/google_assistant/button.py index 139e3032f14..cf3a42e251b 100644 --- a/homeassistant/components/google_assistant/button.py +++ b/homeassistant/components/google_assistant/button.py @@ -1,4 +1,5 @@ """Support for buttons.""" + from __future__ import annotations from homeassistant import config_entries diff --git a/homeassistant/components/google_assistant/config_flow.py b/homeassistant/components/google_assistant/config_flow.py index e8e0d9962f9..9504c623138 100644 --- a/homeassistant/components/google_assistant/config_flow.py +++ b/homeassistant/components/google_assistant/config_flow.py @@ -1,11 +1,11 @@ """Config flow for google assistant component.""" -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow from .const import CONF_PROJECT_ID, DOMAIN -class GoogleAssistantHandler(config_entries.ConfigFlow, domain=DOMAIN): +class GoogleAssistantHandler(ConfigFlow, domain=DOMAIN): """Handle a config flow.""" VERSION = 1 diff --git a/homeassistant/components/google_assistant/const.py b/homeassistant/components/google_assistant/const.py index 431433e2bba..e97d8108965 100644 --- a/homeassistant/components/google_assistant/const.py +++ b/homeassistant/components/google_assistant/const.py @@ -1,4 +1,5 @@ """Constants for Google Assistant.""" + from homeassistant.components import ( alarm_control_panel, binary_sensor, diff --git a/homeassistant/components/google_assistant/data_redaction.py b/homeassistant/components/google_assistant/data_redaction.py index 6a187113bb9..50bd6dabf4c 100644 --- a/homeassistant/components/google_assistant/data_redaction.py +++ b/homeassistant/components/google_assistant/data_redaction.py @@ -1,4 +1,5 @@ """Helpers to redact Google Assistant data when logging.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/google_assistant/diagnostics.py b/homeassistant/components/google_assistant/diagnostics.py index fd4347ddd2c..48902147b05 100644 --- a/homeassistant/components/google_assistant/diagnostics.py +++ b/homeassistant/components/google_assistant/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for Hue.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/google_assistant/error.py b/homeassistant/components/google_assistant/error.py index 82c256067eb..24d6b497103 100644 --- a/homeassistant/components/google_assistant/error.py +++ b/homeassistant/components/google_assistant/error.py @@ -1,4 +1,5 @@ """Errors for Google Assistant.""" + from .const import ERR_CHALLENGE_NEEDED diff --git a/homeassistant/components/google_assistant/helpers.py b/homeassistant/components/google_assistant/helpers.py index 28479fd1e97..7f8f7a68ffa 100644 --- a/homeassistant/components/google_assistant/helpers.py +++ b/homeassistant/components/google_assistant/helpers.py @@ -1,4 +1,5 @@ """Helper classes for Google Assistant integration.""" + from __future__ import annotations from abc import ABC, abstractmethod @@ -164,7 +165,7 @@ class AbstractConfig(ABC): def get_local_user_id(self, webhook_id): """Map webhook ID to a Home Assistant user ID. - Any action inititated by Google Assistant via the local SDK will be attributed + Any action initiated by Google Assistant via the local SDK will be attributed to the returned user ID. Return None if no user id is found for the webhook_id. @@ -624,7 +625,7 @@ class GoogleEntity: if (config_aliases := entity_config.get(CONF_ALIASES, [])) or ( entity_entry and entity_entry.aliases ): - device["name"]["nicknames"] = [name] + config_aliases + device["name"]["nicknames"] = [name, *config_aliases] if entity_entry: device["name"]["nicknames"].extend(entity_entry.aliases) diff --git a/homeassistant/components/google_assistant/http.py b/homeassistant/components/google_assistant/http.py index 0d75a1bede7..95c5bafc2cc 100644 --- a/homeassistant/components/google_assistant/http.py +++ b/homeassistant/components/google_assistant/http.py @@ -1,4 +1,5 @@ """Support for Google Actions Smart Home Control.""" + from __future__ import annotations from datetime import timedelta @@ -12,7 +13,7 @@ from aiohttp.web import Request, Response import jwt from homeassistant.components import webhook -from homeassistant.components.http import HomeAssistantView +from homeassistant.components.http import KEY_HASS, HomeAssistantView from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError @@ -123,7 +124,7 @@ class GoogleConfig(AbstractConfig): def get_local_user_id(self, webhook_id): """Map webhook ID to a Home Assistant user ID. - Any action inititated by Google Assistant via the local SDK will be attributed + Any action initiated by Google Assistant via the local SDK will be attributed to the returned user ID. Return None if no user id is found for the webhook_id. @@ -380,7 +381,7 @@ class GoogleAssistantView(HomeAssistantView): """Handle Google Assistant requests.""" message: dict = await request.json() result = await async_handle_message( - request.app["hass"], + request.app[KEY_HASS], self.config, request["hass_user"].id, request["hass_user"].id, diff --git a/homeassistant/components/google_assistant/logbook.py b/homeassistant/components/google_assistant/logbook.py index 9559188cbd2..686b11b63b5 100644 --- a/homeassistant/components/google_assistant/logbook.py +++ b/homeassistant/components/google_assistant/logbook.py @@ -1,4 +1,5 @@ """Describe logbook events.""" + from homeassistant.components.logbook import LOGBOOK_ENTRY_MESSAGE, LOGBOOK_ENTRY_NAME from homeassistant.core import callback diff --git a/homeassistant/components/google_assistant/report_state.py b/homeassistant/components/google_assistant/report_state.py index aec50011200..26a341bd7b6 100644 --- a/homeassistant/components/google_assistant/report_state.py +++ b/homeassistant/components/google_assistant/report_state.py @@ -1,4 +1,5 @@ """Google Report State implementation.""" + from __future__ import annotations from collections import deque diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py index 8172d0ca92d..df55fc0d7c8 100644 --- a/homeassistant/components/google_assistant/smart_home.py +++ b/homeassistant/components/google_assistant/smart_home.py @@ -1,4 +1,5 @@ """Support for Google Assistant Smart Home API.""" + import asyncio from collections.abc import Callable, Coroutine from itertools import product diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index aba6e461778..dd1e0cb3409 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -1,4 +1,5 @@ """Implement the Google Smart Home traits.""" + from __future__ import annotations from abc import ABC, abstractmethod diff --git a/homeassistant/components/google_assistant_sdk/__init__.py b/homeassistant/components/google_assistant_sdk/__init__.py index f77931b8d89..7d8653b509d 100644 --- a/homeassistant/components/google_assistant_sdk/__init__.py +++ b/homeassistant/components/google_assistant_sdk/__init__.py @@ -1,4 +1,5 @@ """Support for Google Assistant SDK.""" + from __future__ import annotations import dataclasses diff --git a/homeassistant/components/google_assistant_sdk/application_credentials.py b/homeassistant/components/google_assistant_sdk/application_credentials.py index c8a7922bc7a..8fa99157479 100644 --- a/homeassistant/components/google_assistant_sdk/application_credentials.py +++ b/homeassistant/components/google_assistant_sdk/application_credentials.py @@ -1,4 +1,5 @@ """application_credentials platform for Google Assistant SDK.""" + from homeassistant.components.application_credentials import AuthorizationServer from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/google_assistant_sdk/config_flow.py b/homeassistant/components/google_assistant_sdk/config_flow.py index b4f617ca029..85dfd974b22 100644 --- a/homeassistant/components/google_assistant_sdk/config_flow.py +++ b/homeassistant/components/google_assistant_sdk/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Google Assistant SDK integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -7,10 +8,8 @@ from typing import Any import voluptuous as vol -from homeassistant import config_entries -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import ConfigEntry, ConfigFlowResult, OptionsFlow from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_entry_oauth2_flow from .const import CONF_LANGUAGE_CODE, DEFAULT_NAME, DOMAIN, SUPPORTED_LANGUAGE_CODES @@ -43,7 +42,9 @@ class OAuth2FlowHandler( "prompt": "consent", } - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Perform reauth upon an API authentication error.""" self.reauth_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] @@ -52,13 +53,13 @@ class OAuth2FlowHandler( async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm reauth dialog.""" 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[str, Any]) -> FlowResult: + async def async_oauth_create_entry(self, data: dict[str, Any]) -> ConfigFlowResult: """Create an entry for the flow, or update existing entry.""" if self.reauth_entry: self.hass.config_entries.async_update_entry(self.reauth_entry, data=data) @@ -80,22 +81,22 @@ class OAuth2FlowHandler( @staticmethod @callback def async_get_options_flow( - config_entry: config_entries.ConfigEntry, - ) -> config_entries.OptionsFlow: + config_entry: ConfigEntry, + ) -> OptionsFlow: """Create the options flow.""" return OptionsFlowHandler(config_entry) -class OptionsFlowHandler(config_entries.OptionsFlow): +class OptionsFlowHandler(OptionsFlow): """Google Assistant SDK options flow.""" - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + 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: + ) -> ConfigFlowResult: """Manage the options.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) diff --git a/homeassistant/components/google_assistant_sdk/const.py b/homeassistant/components/google_assistant_sdk/const.py index d63aec0ebd5..4059f006d4b 100644 --- a/homeassistant/components/google_assistant_sdk/const.py +++ b/homeassistant/components/google_assistant_sdk/const.py @@ -1,4 +1,5 @@ """Constants for Google Assistant SDK integration.""" + from typing import Final DOMAIN: Final = "google_assistant_sdk" diff --git a/homeassistant/components/google_assistant_sdk/helpers.py b/homeassistant/components/google_assistant_sdk/helpers.py index a55ff92afe6..0c4d081961f 100644 --- a/homeassistant/components/google_assistant_sdk/helpers.py +++ b/homeassistant/components/google_assistant_sdk/helpers.py @@ -1,4 +1,5 @@ """Helper classes for Google Assistant SDK integration.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/homeassistant/components/google_assistant_sdk/manifest.json b/homeassistant/components/google_assistant_sdk/manifest.json index d52b7c18c41..b6281e2a4f0 100644 --- a/homeassistant/components/google_assistant_sdk/manifest.json +++ b/homeassistant/components/google_assistant_sdk/manifest.json @@ -8,5 +8,5 @@ "integration_type": "service", "iot_class": "cloud_polling", "quality_scale": "platinum", - "requirements": ["gassist-text==0.0.10"] + "requirements": ["gassist-text==0.0.11"] } diff --git a/homeassistant/components/google_assistant_sdk/notify.py b/homeassistant/components/google_assistant_sdk/notify.py index adcd07a0cda..3f01cef2ebc 100644 --- a/homeassistant/components/google_assistant_sdk/notify.py +++ b/homeassistant/components/google_assistant_sdk/notify.py @@ -1,4 +1,5 @@ """Support for Google Assistant SDK broadcast notifications.""" + from __future__ import annotations from typing import Any @@ -65,8 +66,8 @@ class BroadcastNotificationService(BaseNotificationService): if not targets: commands.append(broadcast_commands(language_code)[0].format(message)) else: - for target in targets: - commands.append( - broadcast_commands(language_code)[1].format(message, target) - ) + commands.extend( + broadcast_commands(language_code)[1].format(message, target) + for target in targets + ) await async_send_text_commands(self.hass, commands) diff --git a/homeassistant/components/google_cloud/tts.py b/homeassistant/components/google_cloud/tts.py index 8f30448ad61..6f4751850aa 100644 --- a/homeassistant/components/google_cloud/tts.py +++ b/homeassistant/components/google_cloud/tts.py @@ -1,4 +1,5 @@ """Support for the Google Cloud TTS service.""" + import asyncio import logging import os diff --git a/homeassistant/components/google_domains/__init__.py b/homeassistant/components/google_domains/__init__.py index 1d420cb1497..a4dcef62964 100644 --- a/homeassistant/components/google_domains/__init__.py +++ b/homeassistant/components/google_domains/__init__.py @@ -1,4 +1,5 @@ """Support for Google Domains.""" + import asyncio from datetime import timedelta import logging @@ -72,7 +73,7 @@ async def _update_google_domains(hass, session, domain, user, password, timeout) resp = await session.get(url, params=params) body = await resp.text() - if body.startswith("good") or body.startswith("nochg"): + if body.startswith(("good", "nochg")): return True _LOGGER.warning("Updating Google Domains failed: %s => %s", domain, body) diff --git a/homeassistant/components/google_generative_ai_conversation/__init__.py b/homeassistant/components/google_generative_ai_conversation/__init__.py index 73450e9f5b9..e956c288b53 100644 --- a/homeassistant/components/google_generative_ai_conversation/__init__.py +++ b/homeassistant/components/google_generative_ai_conversation/__init__.py @@ -1,4 +1,5 @@ """The Google Generative AI Conversation integration.""" + from __future__ import annotations from functools import partial diff --git a/homeassistant/components/google_generative_ai_conversation/config_flow.py b/homeassistant/components/google_generative_ai_conversation/config_flow.py index 74ba3c478df..dde82db91cc 100644 --- a/homeassistant/components/google_generative_ai_conversation/config_flow.py +++ b/homeassistant/components/google_generative_ai_conversation/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Google Generative AI Conversation integration.""" + from __future__ import annotations from functools import partial @@ -11,10 +12,14 @@ from google.api_core.exceptions import ClientError import google.generativeai as genai import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.selector import ( NumberSelector, NumberSelectorConfig, @@ -66,14 +71,14 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> None: await hass.async_add_executor_job(partial(genai.list_models)) -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class GoogleGenerativeAIConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Google Generative AI Conversation.""" VERSION = 1 async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" if user_input is None: return self.async_show_form( @@ -103,22 +108,22 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod def async_get_options_flow( - config_entry: config_entries.ConfigEntry, - ) -> config_entries.OptionsFlow: + config_entry: ConfigEntry, + ) -> OptionsFlow: """Create the options flow.""" - return OptionsFlow(config_entry) + return GoogleGenerativeAIOptionsFlow(config_entry) -class OptionsFlow(config_entries.OptionsFlow): +class GoogleGenerativeAIOptionsFlow(OptionsFlow): """Google Generative AI config flow options handler.""" - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + 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: + ) -> ConfigFlowResult: """Manage the options.""" if user_input is not None: return self.async_create_entry( diff --git a/homeassistant/components/google_mail/__init__.py b/homeassistant/components/google_mail/__init__.py index 311af064fcd..1ac963b430a 100644 --- a/homeassistant/components/google_mail/__init__.py +++ b/homeassistant/components/google_mail/__init__.py @@ -1,4 +1,5 @@ """Support for Google Mail.""" + from __future__ import annotations from homeassistant.config_entries import ConfigEntry, ConfigEntryState diff --git a/homeassistant/components/google_mail/api.py b/homeassistant/components/google_mail/api.py index 10b2fec7467..e824e4b3ddd 100644 --- a/homeassistant/components/google_mail/api.py +++ b/homeassistant/components/google_mail/api.py @@ -1,4 +1,5 @@ """API for Google Mail bound to Home Assistant OAuth.""" + from aiohttp.client_exceptions import ClientError, ClientResponseError from google.auth.exceptions import RefreshError from google.oauth2.credentials import Credentials diff --git a/homeassistant/components/google_mail/application_credentials.py b/homeassistant/components/google_mail/application_credentials.py index 0b3b1dfd056..a1d3ddd6dc7 100644 --- a/homeassistant/components/google_mail/application_credentials.py +++ b/homeassistant/components/google_mail/application_credentials.py @@ -1,4 +1,5 @@ """application_credentials platform for Google Mail.""" + from homeassistant.components.application_credentials import AuthorizationServer from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/google_mail/config_flow.py b/homeassistant/components/google_mail/config_flow.py index b57947302cc..5b5c760628b 100644 --- a/homeassistant/components/google_mail/config_flow.py +++ b/homeassistant/components/google_mail/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Google Mail integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -8,9 +9,8 @@ from typing import Any, cast from google.oauth2.credentials import Credentials from googleapiclient.discovery import build -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import ConfigEntry, ConfigFlowResult from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_entry_oauth2_flow from .const import DEFAULT_ACCESS, DOMAIN @@ -40,7 +40,9 @@ class OAuth2FlowHandler( "prompt": "consent", } - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Perform reauth upon an API authentication error.""" self.reauth_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] @@ -49,13 +51,13 @@ class OAuth2FlowHandler( async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm reauth dialog.""" 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[str, Any]) -> FlowResult: + async def async_oauth_create_entry(self, data: dict[str, Any]) -> ConfigFlowResult: """Create an entry for the flow, or update existing entry.""" def _get_profile() -> str: diff --git a/homeassistant/components/google_mail/const.py b/homeassistant/components/google_mail/const.py index 6e70ea9838c..523182df072 100644 --- a/homeassistant/components/google_mail/const.py +++ b/homeassistant/components/google_mail/const.py @@ -1,4 +1,5 @@ """Constants for Google Mail integration.""" + from __future__ import annotations ATTR_BCC = "bcc" diff --git a/homeassistant/components/google_mail/entity.py b/homeassistant/components/google_mail/entity.py index fed8ff481f0..d83b18b9a50 100644 --- a/homeassistant/components/google_mail/entity.py +++ b/homeassistant/components/google_mail/entity.py @@ -1,4 +1,5 @@ """Entity representing a Google Mail account.""" + from __future__ import annotations from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo diff --git a/homeassistant/components/google_mail/notify.py b/homeassistant/components/google_mail/notify.py index 974b2e4e4bf..73c99d54ff3 100644 --- a/homeassistant/components/google_mail/notify.py +++ b/homeassistant/components/google_mail/notify.py @@ -1,4 +1,5 @@ """Notification service for Google Mail integration.""" + from __future__ import annotations import base64 diff --git a/homeassistant/components/google_mail/sensor.py b/homeassistant/components/google_mail/sensor.py index 78b0e3c9a91..1de72632de1 100644 --- a/homeassistant/components/google_mail/sensor.py +++ b/homeassistant/components/google_mail/sensor.py @@ -1,4 +1,5 @@ """Support for Google Mail Sensors.""" + from __future__ import annotations from datetime import UTC, datetime, timedelta diff --git a/homeassistant/components/google_mail/services.py b/homeassistant/components/google_mail/services.py index 1450a5d31b8..e07e2be2101 100644 --- a/homeassistant/components/google_mail/services.py +++ b/homeassistant/components/google_mail/services.py @@ -1,4 +1,5 @@ """Services for Google Mail integration.""" + from __future__ import annotations from datetime import datetime, timedelta diff --git a/homeassistant/components/google_maps/device_tracker.py b/homeassistant/components/google_maps/device_tracker.py index 93810d0f21d..b3b0430271a 100644 --- a/homeassistant/components/google_maps/device_tracker.py +++ b/homeassistant/components/google_maps/device_tracker.py @@ -1,4 +1,5 @@ """Support for Google Maps location sharing.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/google_pubsub/__init__.py b/homeassistant/components/google_pubsub/__init__.py index 22b7b358dbc..74e2a297ff4 100644 --- a/homeassistant/components/google_pubsub/__init__.py +++ b/homeassistant/components/google_pubsub/__init__.py @@ -1,4 +1,5 @@ """Support for Google Cloud Pub/Sub.""" + from __future__ import annotations import datetime diff --git a/homeassistant/components/google_sheets/__init__.py b/homeassistant/components/google_sheets/__init__.py index ba2a0884e22..bf7cf7c40df 100644 --- a/homeassistant/components/google_sheets/__init__.py +++ b/homeassistant/components/google_sheets/__init__.py @@ -1,4 +1,5 @@ """Support for Google Sheets.""" + from __future__ import annotations from datetime import datetime diff --git a/homeassistant/components/google_sheets/application_credentials.py b/homeassistant/components/google_sheets/application_credentials.py index f10f6891125..8e1f96bfa6e 100644 --- a/homeassistant/components/google_sheets/application_credentials.py +++ b/homeassistant/components/google_sheets/application_credentials.py @@ -1,4 +1,5 @@ """application_credentials platform for Google Sheets.""" + from homeassistant.components.application_credentials import AuthorizationServer from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/google_sheets/config_flow.py b/homeassistant/components/google_sheets/config_flow.py index 3805ee9d38b..a0a99742249 100644 --- a/homeassistant/components/google_sheets/config_flow.py +++ b/homeassistant/components/google_sheets/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Google Sheets integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -8,9 +9,8 @@ from typing import Any from google.oauth2.credentials import Credentials from gspread import Client, GSpreadException -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import ConfigEntry, ConfigFlowResult from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_entry_oauth2_flow from .const import DEFAULT_ACCESS, DEFAULT_NAME, DOMAIN @@ -42,7 +42,9 @@ class OAuth2FlowHandler( "prompt": "consent", } - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Perform reauth upon an API authentication error.""" self.reauth_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] @@ -51,13 +53,13 @@ class OAuth2FlowHandler( async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm reauth dialog.""" 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[str, Any]) -> FlowResult: + async def async_oauth_create_entry(self, data: dict[str, Any]) -> ConfigFlowResult: """Create an entry for the flow, or update existing entry.""" service = Client(Credentials(data[CONF_TOKEN][CONF_ACCESS_TOKEN])) diff --git a/homeassistant/components/google_sheets/const.py b/homeassistant/components/google_sheets/const.py index 71ef8f4a4f4..b93916db0a6 100644 --- a/homeassistant/components/google_sheets/const.py +++ b/homeassistant/components/google_sheets/const.py @@ -1,4 +1,5 @@ """Constants for Google Sheets integration.""" + from __future__ import annotations DOMAIN = "google_sheets" diff --git a/homeassistant/components/google_tasks/__init__.py b/homeassistant/components/google_tasks/__init__.py index da6fc85b287..b62bd0fe5a2 100644 --- a/homeassistant/components/google_tasks/__init__.py +++ b/homeassistant/components/google_tasks/__init__.py @@ -1,4 +1,5 @@ """The Google Tasks integration.""" + from __future__ import annotations from aiohttp import ClientError diff --git a/homeassistant/components/google_tasks/config_flow.py b/homeassistant/components/google_tasks/config_flow.py index b8e5e26f42c..14cd89fcec7 100644 --- a/homeassistant/components/google_tasks/config_flow.py +++ b/homeassistant/components/google_tasks/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Google Tasks.""" + import logging from typing import Any @@ -7,8 +8,8 @@ from googleapiclient.discovery import build from googleapiclient.errors import HttpError from googleapiclient.http import HttpRequest +from homeassistant.config_entries import ConfigFlowResult from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_entry_oauth2_flow from .const import DOMAIN, OAUTH2_SCOPES @@ -36,7 +37,7 @@ class OAuth2FlowHandler( "prompt": "consent", } - async def async_oauth_create_entry(self, data: dict[str, Any]) -> FlowResult: + async def async_oauth_create_entry(self, data: dict[str, Any]) -> ConfigFlowResult: """Create an entry for the flow.""" try: resource = build( diff --git a/homeassistant/components/google_tasks/todo.py b/homeassistant/components/google_tasks/todo.py index e83b0d39a30..95c5f1c3a16 100644 --- a/homeassistant/components/google_tasks/todo.py +++ b/homeassistant/components/google_tasks/todo.py @@ -1,4 +1,5 @@ """Google Tasks todo platform.""" + from __future__ import annotations from datetime import date, datetime, timedelta diff --git a/homeassistant/components/google_translate/__init__.py b/homeassistant/components/google_translate/__init__.py index ac6b07bd4b3..17400bbd0e2 100644 --- a/homeassistant/components/google_translate/__init__.py +++ b/homeassistant/components/google_translate/__init__.py @@ -1,4 +1,5 @@ """The Google Translate text-to-speech integration.""" + from __future__ import annotations from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/google_translate/config_flow.py b/homeassistant/components/google_translate/config_flow.py index 3996d41df35..5c140167b81 100644 --- a/homeassistant/components/google_translate/config_flow.py +++ b/homeassistant/components/google_translate/config_flow.py @@ -1,13 +1,13 @@ """Config flow for Google Translate text-to-speech integration.""" + from __future__ import annotations from typing import Any import voluptuous as vol -from homeassistant import config_entries from homeassistant.components.tts import CONF_LANG -from homeassistant.data_entry_flow import FlowResult +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from .const import ( CONF_TLD, @@ -26,14 +26,14 @@ STEP_USER_DATA_SCHEMA = vol.Schema( ) -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class GoogleTranslateConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Google Translate text-to-speech.""" VERSION = 1 async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" if user_input is not None: self._async_abort_entries_match( @@ -50,7 +50,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_onboarding( self, data: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by onboarding.""" return self.async_create_entry( title="Google Translate text-to-speech", diff --git a/homeassistant/components/google_translate/const.py b/homeassistant/components/google_translate/const.py index 0bb8663119b..76827606816 100644 --- a/homeassistant/components/google_translate/const.py +++ b/homeassistant/components/google_translate/const.py @@ -1,4 +1,5 @@ """Constants for the Google Translate text-to-speech integration.""" + from dataclasses import dataclass CONF_TLD = "tld" diff --git a/homeassistant/components/google_translate/strings.json b/homeassistant/components/google_translate/strings.json index a83e61f01f9..1ff177e4b45 100644 --- a/homeassistant/components/google_translate/strings.json +++ b/homeassistant/components/google_translate/strings.json @@ -3,7 +3,7 @@ "step": { "user": { "data": { - "language": "Language", + "language": "[%key:common::config_flow::data::language%]", "tld": "TLD" } } diff --git a/homeassistant/components/google_translate/tts.py b/homeassistant/components/google_translate/tts.py index 7774d9fd6c8..df7130e09e0 100644 --- a/homeassistant/components/google_translate/tts.py +++ b/homeassistant/components/google_translate/tts.py @@ -1,4 +1,5 @@ """Support for the Google speech service.""" + from __future__ import annotations from io import BytesIO diff --git a/homeassistant/components/google_travel_time/__init__.py b/homeassistant/components/google_travel_time/__init__.py index b2778a34c10..4ee9d53cf3b 100644 --- a/homeassistant/components/google_travel_time/__init__.py +++ b/homeassistant/components/google_travel_time/__init__.py @@ -1,4 +1,5 @@ """The google_travel_time component.""" + from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/google_travel_time/config_flow.py b/homeassistant/components/google_travel_time/config_flow.py index 73a4bf87b7e..424ad56b9d4 100644 --- a/homeassistant/components/google_travel_time/config_flow.py +++ b/homeassistant/components/google_travel_time/config_flow.py @@ -1,12 +1,17 @@ """Config flow for Google Maps Travel Time integration.""" + from __future__ import annotations import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.const import CONF_API_KEY, CONF_LANGUAGE, CONF_MODE, CONF_NAME from homeassistant.core import HomeAssistant, callback -from homeassistant.data_entry_flow import FlowResult import homeassistant.helpers.config_validation as cv from homeassistant.helpers.selector import ( SelectSelector, @@ -124,14 +129,14 @@ def default_options(hass: HomeAssistant) -> dict[str, str]: } -class GoogleOptionsFlow(config_entries.OptionsFlow): +class GoogleOptionsFlow(OptionsFlow): """Handle an options flow for Google Travel Time.""" - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + def __init__(self, config_entry: ConfigEntry) -> None: """Initialize google options flow.""" self.config_entry = config_entry - async def async_step_init(self, user_input=None) -> FlowResult: + async def async_step_init(self, user_input=None) -> ConfigFlowResult: """Handle the initial step.""" if user_input is not None: time_type = user_input.pop(CONF_TIME_TYPE) @@ -159,7 +164,7 @@ class GoogleOptionsFlow(config_entries.OptionsFlow): ) -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class GoogleTravelTimeConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Google Maps Travel Time.""" VERSION = 1 @@ -167,12 +172,12 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback def async_get_options_flow( - config_entry: config_entries.ConfigEntry, + config_entry: ConfigEntry, ) -> GoogleOptionsFlow: """Get the options flow for this handler.""" return GoogleOptionsFlow(config_entry) - async def async_step_user(self, user_input=None) -> FlowResult: + async def async_step_user(self, user_input=None) -> ConfigFlowResult: """Handle the initial step.""" errors = {} user_input = user_input or {} diff --git a/homeassistant/components/google_travel_time/const.py b/homeassistant/components/google_travel_time/const.py index 041858d948f..7e086640e2b 100644 --- a/homeassistant/components/google_travel_time/const.py +++ b/homeassistant/components/google_travel_time/const.py @@ -1,4 +1,5 @@ """Constants for Google Travel Time.""" + DOMAIN = "google_travel_time" ATTRIBUTION = "Powered by Google" diff --git a/homeassistant/components/google_travel_time/helpers.py b/homeassistant/components/google_travel_time/helpers.py index 9c25d02b8a5..baceffecc73 100644 --- a/homeassistant/components/google_travel_time/helpers.py +++ b/homeassistant/components/google_travel_time/helpers.py @@ -1,4 +1,5 @@ """Helpers for Google Time Travel integration.""" + import logging from googlemaps import Client @@ -29,13 +30,13 @@ def validate_config_entry( _LOGGER.error("Request denied: %s", api_error.message) raise InvalidApiKeyException from api_error _LOGGER.error("Unknown error: %s", api_error.message) - raise UnknownException() from api_error + raise UnknownException from api_error except TransportError as transport_error: _LOGGER.error("Unknown error: %s", transport_error) - raise UnknownException() from transport_error + raise UnknownException from transport_error except Timeout as timeout_error: _LOGGER.error("Timeout error") - raise TimeoutError() from timeout_error + raise TimeoutError from timeout_error class InvalidApiKeyException(Exception): diff --git a/homeassistant/components/google_travel_time/sensor.py b/homeassistant/components/google_travel_time/sensor.py index 95eb965a4ff..6c45033eeb7 100644 --- a/homeassistant/components/google_travel_time/sensor.py +++ b/homeassistant/components/google_travel_time/sensor.py @@ -1,4 +1,5 @@ """Support for Google travel time sensors.""" + from __future__ import annotations from datetime import datetime, timedelta diff --git a/homeassistant/components/google_travel_time/strings.json b/homeassistant/components/google_travel_time/strings.json index 3cfcd3cedb3..2c7840b23d8 100644 --- a/homeassistant/components/google_travel_time/strings.json +++ b/homeassistant/components/google_travel_time/strings.json @@ -27,7 +27,7 @@ "description": "You can optionally specify either a Departure Time or Arrival Time. If specifying a departure time, you can enter `now`, a Unix timestamp, or a 24 hour time string like `08:00:00`. If specifying an arrival time, you can use a Unix timestamp or a 24 hour time string like `08:00:00`", "data": { "mode": "Travel Mode", - "language": "Language", + "language": "[%key:common::config_flow::data::language%]", "time_type": "Time Type", "time": "Time", "avoid": "Avoid", diff --git a/homeassistant/components/google_wifi/sensor.py b/homeassistant/components/google_wifi/sensor.py index f90cc028fdf..776fb44a51b 100644 --- a/homeassistant/components/google_wifi/sensor.py +++ b/homeassistant/components/google_wifi/sensor.py @@ -1,4 +1,5 @@ """Support for retrieving status info from Google Wifi/OnHub routers.""" + from __future__ import annotations from dataclasses import dataclass @@ -42,21 +43,14 @@ ENDPOINT = "/api/v1/status" MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=1) -@dataclass(frozen=True) -class GoogleWifiRequiredKeysMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class GoogleWifiSensorEntityDescription(SensorEntityDescription): + """Describes GoogleWifi sensor entity.""" primary_key: str sensor_key: str -@dataclass(frozen=True) -class GoogleWifiSensorEntityDescription( - SensorEntityDescription, GoogleWifiRequiredKeysMixin -): - """Describes GoogleWifi sensor entity.""" - - SENSOR_TYPES: tuple[GoogleWifiSensorEntityDescription, ...] = ( GoogleWifiSensorEntityDescription( key=ATTR_CURRENT_VERSION, diff --git a/homeassistant/components/govee_ble/__init__.py b/homeassistant/components/govee_ble/__init__.py index a2dbecf85a2..8d074b6f997 100644 --- a/homeassistant/components/govee_ble/__init__.py +++ b/homeassistant/components/govee_ble/__init__.py @@ -1,4 +1,5 @@ """The Govee Bluetooth BLE integration.""" + from __future__ import annotations import logging @@ -25,14 +26,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: address = entry.unique_id assert address is not None data = GoveeBluetoothDeviceData() - coordinator = hass.data.setdefault(DOMAIN, {})[ - entry.entry_id - ] = PassiveBluetoothProcessorCoordinator( - hass, - _LOGGER, - address=address, - mode=BluetoothScanningMode.ACTIVE, - update_method=data.update, + coordinator = hass.data.setdefault(DOMAIN, {})[entry.entry_id] = ( + PassiveBluetoothProcessorCoordinator( + hass, + _LOGGER, + address=address, + mode=BluetoothScanningMode.ACTIVE, + update_method=data.update, + ) ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload( diff --git a/homeassistant/components/govee_ble/config_flow.py b/homeassistant/components/govee_ble/config_flow.py index fc6fe7b310d..f580fca68d8 100644 --- a/homeassistant/components/govee_ble/config_flow.py +++ b/homeassistant/components/govee_ble/config_flow.py @@ -1,4 +1,5 @@ """Config flow for govee ble integration.""" + from __future__ import annotations from typing import Any @@ -10,9 +11,8 @@ from homeassistant.components.bluetooth import ( BluetoothServiceInfoBleak, async_discovered_service_info, ) -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_ADDRESS -from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN @@ -30,7 +30,7 @@ class GoveeConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_bluetooth( self, discovery_info: BluetoothServiceInfoBleak - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the bluetooth discovery step.""" await self.async_set_unique_id(discovery_info.address) self._abort_if_unique_id_configured() @@ -43,7 +43,7 @@ class GoveeConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_bluetooth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm discovery.""" assert self._discovered_device is not None device = self._discovered_device @@ -62,7 +62,7 @@ class GoveeConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the user step to pick discovered device.""" if user_input is not None: address = user_input[CONF_ADDRESS] diff --git a/homeassistant/components/govee_ble/sensor.py b/homeassistant/components/govee_ble/sensor.py index 3809a2390f3..33f4761d02a 100644 --- a/homeassistant/components/govee_ble/sensor.py +++ b/homeassistant/components/govee_ble/sensor.py @@ -1,4 +1,5 @@ """Support for govee ble sensors.""" + from __future__ import annotations from govee_ble import DeviceClass, DeviceKey, SensorUpdate, Units diff --git a/homeassistant/components/govee_light_local/__init__.py b/homeassistant/components/govee_light_local/__init__.py index 2d4594755c4..d2537fb5c9b 100644 --- a/homeassistant/components/govee_light_local/__init__.py +++ b/homeassistant/components/govee_light_local/__init__.py @@ -1,4 +1,5 @@ """The Govee Light local integration.""" + from __future__ import annotations import asyncio @@ -8,7 +9,7 @@ from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady -from .const import DOMAIN +from .const import DISCOVERY_TIMEOUT, DOMAIN from .coordinator import GoveeLocalApiCoordinator PLATFORMS: list[Platform] = [Platform.LIGHT] @@ -25,7 +26,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() try: - async with asyncio.timeout(delay=5): + async with asyncio.timeout(delay=DISCOVERY_TIMEOUT): while not coordinator.devices: await asyncio.sleep(delay=1) except TimeoutError as ex: diff --git a/homeassistant/components/govee_light_local/config_flow.py b/homeassistant/components/govee_light_local/config_flow.py index 8058668f0ca..d31bfed0579 100644 --- a/homeassistant/components/govee_light_local/config_flow.py +++ b/homeassistant/components/govee_light_local/config_flow.py @@ -15,6 +15,7 @@ from .const import ( CONF_LISTENING_PORT_DEFAULT, CONF_MULTICAST_ADDRESS_DEFAULT, CONF_TARGET_PORT_DEFAULT, + DISCOVERY_TIMEOUT, DOMAIN, ) @@ -41,7 +42,7 @@ async def _async_has_devices(hass: HomeAssistant) -> bool: await controller.start() try: - async with asyncio.timeout(delay=5): + async with asyncio.timeout(delay=DISCOVERY_TIMEOUT): while not controller.devices: await asyncio.sleep(delay=1) except TimeoutError: diff --git a/homeassistant/components/govee_light_local/const.py b/homeassistant/components/govee_light_local/const.py index d9410c9c05e..a90a1ff1ff1 100644 --- a/homeassistant/components/govee_light_local/const.py +++ b/homeassistant/components/govee_light_local/const.py @@ -11,3 +11,4 @@ CONF_LISTENING_PORT_DEFAULT = 4002 CONF_DISCOVERY_INTERVAL_DEFAULT = 60 SCAN_INTERVAL = timedelta(seconds=30) +DISCOVERY_TIMEOUT = 5 diff --git a/homeassistant/components/gpsd/__init__.py b/homeassistant/components/gpsd/__init__.py index bdd5ddb13b0..a0e3db2e404 100644 --- a/homeassistant/components/gpsd/__init__.py +++ b/homeassistant/components/gpsd/__init__.py @@ -1,4 +1,5 @@ """The GPSD integration.""" + from __future__ import annotations from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/gpsd/config_flow.py b/homeassistant/components/gpsd/config_flow.py index db1f9c5b0c1..10fb8a3a252 100644 --- a/homeassistant/components/gpsd/config_flow.py +++ b/homeassistant/components/gpsd/config_flow.py @@ -1,4 +1,5 @@ """Config flow for GPSD integration.""" + from __future__ import annotations import socket @@ -7,9 +8,8 @@ from typing import Any from gps3.agps3threaded import GPSD_PORT as DEFAULT_PORT, HOST as DEFAULT_HOST import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_validation as cv from .const import DOMAIN @@ -22,18 +22,18 @@ STEP_USER_DATA_SCHEMA = vol.Schema( ) -class GPSDConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class GPSDConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for GPSD.""" VERSION = 1 - async def async_step_import(self, import_data: dict[str, Any]) -> FlowResult: + async def async_step_import(self, import_data: dict[str, Any]) -> ConfigFlowResult: """Import a config entry from configuration.yaml.""" return await self.async_step_user(import_data) async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" if user_input is not None: self._async_abort_entries_match(user_input) diff --git a/homeassistant/components/gpsd/sensor.py b/homeassistant/components/gpsd/sensor.py index 135d9c6c28f..bc08b7b6203 100644 --- a/homeassistant/components/gpsd/sensor.py +++ b/homeassistant/components/gpsd/sensor.py @@ -1,4 +1,5 @@ """Sensor platform for GPSD integration.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/gpslogger/__init__.py b/homeassistant/components/gpslogger/__init__.py index 9f00e2cb52d..5d35314adf8 100644 --- a/homeassistant/components/gpslogger/__init__.py +++ b/homeassistant/components/gpslogger/__init__.py @@ -1,4 +1,5 @@ """Support for GPSLogger.""" + from http import HTTPStatus from aiohttp import web diff --git a/homeassistant/components/gpslogger/config_flow.py b/homeassistant/components/gpslogger/config_flow.py index ef90a8d1607..b0a114a4330 100644 --- a/homeassistant/components/gpslogger/config_flow.py +++ b/homeassistant/components/gpslogger/config_flow.py @@ -1,4 +1,5 @@ """Config flow for GPSLogger.""" + from homeassistant.helpers import config_entry_flow from .const import DOMAIN diff --git a/homeassistant/components/gpslogger/device_tracker.py b/homeassistant/components/gpslogger/device_tracker.py index 278d6571cb7..4a28606662f 100644 --- a/homeassistant/components/gpslogger/device_tracker.py +++ b/homeassistant/components/gpslogger/device_tracker.py @@ -1,4 +1,5 @@ """Support for the GPSLogger device tracking.""" + from homeassistant.components.device_tracker import SourceType, TrackerEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -39,9 +40,9 @@ async def async_setup_entry( async_add_entities([GPSLoggerEntity(device, gps, battery, accuracy, attrs)]) - hass.data[GPL_DOMAIN]["unsub_device_tracker"][ - entry.entry_id - ] = async_dispatcher_connect(hass, TRACKER_UPDATE, _receive_data) + hass.data[GPL_DOMAIN]["unsub_device_tracker"][entry.entry_id] = ( + async_dispatcher_connect(hass, TRACKER_UPDATE, _receive_data) + ) # Restore previously loaded devices dev_reg = dr.async_get(hass) diff --git a/homeassistant/components/graphite/__init__.py b/homeassistant/components/graphite/__init__.py index 8e9b015cdad..17dd140aef7 100644 --- a/homeassistant/components/graphite/__init__.py +++ b/homeassistant/components/graphite/__init__.py @@ -1,4 +1,5 @@ """Support for sending data to a Graphite installation.""" + from contextlib import suppress import logging import queue diff --git a/homeassistant/components/gree/__init__.py b/homeassistant/components/gree/__init__.py index 13e93d780b2..5b2e95b15e2 100644 --- a/homeassistant/components/gree/__init__.py +++ b/homeassistant/components/gree/__init__.py @@ -1,4 +1,5 @@ """The Gree Climate integration.""" + from datetime import timedelta import logging diff --git a/homeassistant/components/gree/bridge.py b/homeassistant/components/gree/bridge.py index ebd5e78a820..867f742e821 100644 --- a/homeassistant/components/gree/bridge.py +++ b/homeassistant/components/gree/bridge.py @@ -1,4 +1,5 @@ """Helper and wrapper classes for Gree module.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/gree/climate.py b/homeassistant/components/gree/climate.py index 1d061c06901..66b025d52b5 100644 --- a/homeassistant/components/gree/climate.py +++ b/homeassistant/components/gree/climate.py @@ -1,4 +1,5 @@ """Support for interface with a Gree climate systems.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/gree/config_flow.py b/homeassistant/components/gree/config_flow.py index 58f83cd4486..5e118088916 100644 --- a/homeassistant/components/gree/config_flow.py +++ b/homeassistant/components/gree/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Gree.""" + from greeclimate.discovery import Discovery from homeassistant.components.network import async_get_ipv4_broadcast_addresses diff --git a/homeassistant/components/gree/entity.py b/homeassistant/components/gree/entity.py index c965ad45721..4eb4a0cbaeb 100644 --- a/homeassistant/components/gree/entity.py +++ b/homeassistant/components/gree/entity.py @@ -1,4 +1,5 @@ """Entity object for shared properties of Gree entities.""" + from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/gree/switch.py b/homeassistant/components/gree/switch.py index e18cf28e174..c1612ce99de 100644 --- a/homeassistant/components/gree/switch.py +++ b/homeassistant/components/gree/switch.py @@ -1,4 +1,5 @@ """Support for interface with a Gree climate systems.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/greeneye_monitor/__init__.py b/homeassistant/components/greeneye_monitor/__init__.py index e4090787e65..083d431e338 100644 --- a/homeassistant/components/greeneye_monitor/__init__.py +++ b/homeassistant/components/greeneye_monitor/__init__.py @@ -1,4 +1,5 @@ """Support for monitoring a GreenEye Monitor energy monitor.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/greeneye_monitor/sensor.py b/homeassistant/components/greeneye_monitor/sensor.py index c11a4168045..1290fc9459a 100644 --- a/homeassistant/components/greeneye_monitor/sensor.py +++ b/homeassistant/components/greeneye_monitor/sensor.py @@ -1,4 +1,5 @@ """Support for the sensors in a GreenEye Monitor.""" + from __future__ import annotations from typing import Any @@ -61,48 +62,46 @@ async def async_setup_platform( None, ) if monitor_config: - entities: list[GEMSensor] = [] - channel_configs = monitor_config[CONF_CHANNELS] - for sensor in channel_configs: - entities.append( - CurrentSensor( - monitor, - sensor[CONF_NUMBER], - sensor[CONF_NAME], - sensor[CONF_NET_METERING], - ) + entities: list[GEMSensor] = [ + CurrentSensor( + monitor, + sensor[CONF_NUMBER], + sensor[CONF_NAME], + sensor[CONF_NET_METERING], ) + for sensor in channel_configs + ] pulse_counter_configs = monitor_config[CONF_PULSE_COUNTERS] - for sensor in pulse_counter_configs: - entities.append( - PulseCounter( - monitor, - sensor[CONF_NUMBER], - sensor[CONF_NAME], - sensor[CONF_COUNTED_QUANTITY], - sensor[CONF_TIME_UNIT], - sensor[CONF_COUNTED_QUANTITY_PER_PULSE], - ) + entities.extend( + PulseCounter( + monitor, + sensor[CONF_NUMBER], + sensor[CONF_NAME], + sensor[CONF_COUNTED_QUANTITY], + sensor[CONF_TIME_UNIT], + sensor[CONF_COUNTED_QUANTITY_PER_PULSE], ) + for sensor in pulse_counter_configs + ) temperature_sensor_configs = monitor_config[CONF_TEMPERATURE_SENSORS] - for sensor in temperature_sensor_configs[CONF_SENSORS]: - entities.append( - TemperatureSensor( - monitor, - sensor[CONF_NUMBER], - sensor[CONF_NAME], - temperature_sensor_configs[CONF_TEMPERATURE_UNIT], - ) + entities.extend( + TemperatureSensor( + monitor, + sensor[CONF_NUMBER], + sensor[CONF_NAME], + temperature_sensor_configs[CONF_TEMPERATURE_UNIT], ) + for sensor in temperature_sensor_configs[CONF_SENSORS] + ) voltage_sensor_configs = monitor_config[CONF_VOLTAGE_SENSORS] - for sensor in voltage_sensor_configs: - entities.append( - VoltageSensor(monitor, sensor[CONF_NUMBER], sensor[CONF_NAME]) - ) + entities.extend( + VoltageSensor(monitor, sensor[CONF_NUMBER], sensor[CONF_NAME]) + for sensor in voltage_sensor_configs + ) async_add_entities(entities) monitor_configs.remove(monitor_config) diff --git a/homeassistant/components/greenwave/light.py b/homeassistant/components/greenwave/light.py index 9f904018796..aa592727220 100644 --- a/homeassistant/components/greenwave/light.py +++ b/homeassistant/components/greenwave/light.py @@ -1,4 +1,5 @@ """Support for Greenwave Reality (TCP Connected) lights.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index 9ee81191bf8..7657201da4d 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -1,71 +1,63 @@ """Provide the functionality to group entities.""" + from __future__ import annotations -from abc import abstractmethod import asyncio -from collections.abc import Callable, Collection, Mapping -from contextvars import ContextVar +from collections.abc import Collection import logging -from typing import Any, Protocol +from typing import Any import voluptuous as vol from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - ATTR_ASSUMED_STATE, - ATTR_ENTITY_ID, + ATTR_ENTITY_ID, # noqa: F401 ATTR_ICON, ATTR_NAME, CONF_ENTITIES, CONF_ICON, CONF_NAME, SERVICE_RELOAD, - STATE_OFF, - STATE_ON, Platform, ) -from homeassistant.core import ( - CALLBACK_TYPE, - HomeAssistant, - ServiceCall, - State, - callback, - split_entity_id, -) -from homeassistant.helpers import config_validation as cv, entity_registry as er, start -from homeassistant.helpers.entity import Entity, async_generate_entity_id +from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.event import ( - EventStateChangedData, - async_track_state_change_event, -) from homeassistant.helpers.group import ( expand_entity_ids as _expand_entity_ids, get_entity_ids as _get_entity_ids, ) -from homeassistant.helpers.integration_platform import ( - async_process_integration_platforms, -) from homeassistant.helpers.reload import async_reload_integration_platforms -from homeassistant.helpers.typing import ConfigType, EventType +from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass -from .const import CONF_HIDE_MEMBERS - -DOMAIN = "group" -GROUP_ORDER = "group_order" - -ENTITY_ID_FORMAT = DOMAIN + ".{}" +# +# Below we ensure the config_flow is imported so it does not need the import +# executor later. +# +# Since group is pre-imported, the loader will not get a chance to pre-import +# the config flow as there is no run time import of the group component in the +# executor. +# +from . import config_flow as config_flow_pre_import # noqa: F401 +from .const import ( # noqa: F401 + ATTR_ADD_ENTITIES, + ATTR_ALL, + ATTR_AUTO, + ATTR_ENTITIES, + ATTR_OBJECT_ID, + ATTR_ORDER, + ATTR_REMOVE_ENTITIES, + CONF_HIDE_MEMBERS, + DOMAIN, + GROUP_ORDER, + REG_KEY, +) +from .entity import Group, async_get_component +from .registry import GroupIntegrationRegistry, async_setup as async_setup_registry CONF_ALL = "all" -ATTR_ADD_ENTITIES = "add_entities" -ATTR_REMOVE_ENTITIES = "remove_entities" -ATTR_AUTO = "auto" -ATTR_ENTITIES = "entities" -ATTR_OBJECT_ID = "object_id" -ATTR_ORDER = "order" -ATTR_ALL = "all" SERVICE_SET = "set" SERVICE_REMOVE = "remove" @@ -82,23 +74,8 @@ PLATFORMS = [ Platform.SWITCH, ] -REG_KEY = f"{DOMAIN}_registry" - -ENTITY_PREFIX = f"{DOMAIN}." - _LOGGER = logging.getLogger(__name__) -current_domain: ContextVar[str] = ContextVar("current_domain") - - -class GroupProtocol(Protocol): - """Define the format of group platforms.""" - - def async_describe_on_off_states( - self, hass: HomeAssistant, registry: GroupIntegrationRegistry - ) -> None: - """Describe group on off states.""" - def _conf_preprocess(value: Any) -> dict[str, Any]: """Preprocess alternative configuration formats.""" @@ -125,36 +102,6 @@ CONFIG_SCHEMA = vol.Schema( ) -def _async_get_component(hass: HomeAssistant) -> EntityComponent[Group]: - if (component := hass.data.get(DOMAIN)) is None: - component = hass.data[DOMAIN] = EntityComponent[Group](_LOGGER, DOMAIN, hass) - return component - - -class GroupIntegrationRegistry: - """Class to hold a registry of integrations.""" - - on_off_mapping: dict[str, str] = {STATE_ON: STATE_OFF} - off_on_mapping: dict[str, str] = {STATE_OFF: STATE_ON} - on_states_by_domain: dict[str, set] = {} - exclude_domains: set = set() - - def exclude_domain(self) -> None: - """Exclude the current domain.""" - self.exclude_domains.add(current_domain.get()) - - def on_off_states(self, on_states: set, off_state: str) -> None: - """Register on and off states for the current domain.""" - for on_state in on_states: - if on_state not in self.on_off_mapping: - self.on_off_mapping[on_state] = off_state - - if len(on_states) == 1 and off_state not in self.off_on_mapping: - self.off_on_mapping[off_state] = list(on_states)[0] - - self.on_states_by_domain[current_domain.get()] = set(on_states) - - @bind_hass def is_on(hass: HomeAssistant, entity_id: str) -> bool: """Test if the group state is in its ON-state.""" @@ -183,13 +130,11 @@ def groups_with_entity(hass: HomeAssistant, entity_id: str) -> list[str]: if DOMAIN not in hass.data: return [] - groups = [] - - for group in hass.data[DOMAIN].entities: - if entity_id in group.tracking: - groups.append(group.entity_id) - - return groups + return [ + group.entity_id + for group in hass.data[DOMAIN].entities + if entity_id in group.tracking + ] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -239,9 +184,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: component: EntityComponent[Group] = hass.data[DOMAIN] - hass.data[REG_KEY] = GroupIntegrationRegistry() - - await async_process_integration_platforms(hass, DOMAIN, _process_group_platform) + await async_setup_registry(hass) await _async_process_config(hass, config) @@ -383,16 +326,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True -@callback -def _process_group_platform( - hass: HomeAssistant, domain: str, platform: GroupProtocol -) -> None: - """Process a group platform.""" - current_domain.set(domain) - registry: GroupIntegrationRegistry = hass.data[REG_KEY] - platform.async_describe_on_off_states(hass, registry) - - async def _async_process_config(hass: HomeAssistant, config: ConfigType) -> None: """Process group configuration.""" hass.data.setdefault(GROUP_ORDER, 0) @@ -430,431 +363,4 @@ async def _async_process_config(hass: HomeAssistant, config: ConfigType) -> None hass.data[GROUP_ORDER] += 1 # If called before the platform async_setup is called (test cases) - await _async_get_component(hass).async_add_entities(entities) - - -class GroupEntity(Entity): - """Representation of a Group of entities.""" - - _unrecorded_attributes = frozenset({ATTR_ENTITY_ID}) - - _attr_should_poll = False - _entity_ids: list[str] - - @callback - def async_start_preview( - self, - preview_callback: Callable[[str, Mapping[str, Any]], None], - ) -> CALLBACK_TYPE: - """Render a preview.""" - - for entity_id in self._entity_ids: - if (state := self.hass.states.get(entity_id)) is None: - continue - self.async_update_supported_features(entity_id, state) - - @callback - def async_state_changed_listener( - event: EventType[EventStateChangedData] | None, - ) -> None: - """Handle child updates.""" - self.async_update_group_state() - if event: - self.async_update_supported_features( - event.data["entity_id"], event.data["new_state"] - ) - calculated_state = self._async_calculate_state() - preview_callback(calculated_state.state, calculated_state.attributes) - - async_state_changed_listener(None) - return async_track_state_change_event( - self.hass, self._entity_ids, async_state_changed_listener - ) - - async def async_added_to_hass(self) -> None: - """Register listeners.""" - for entity_id in self._entity_ids: - if (state := self.hass.states.get(entity_id)) is None: - continue - self.async_update_supported_features(entity_id, state) - - @callback - def async_state_changed_listener( - event: EventType[EventStateChangedData], - ) -> None: - """Handle child updates.""" - self.async_set_context(event.context) - self.async_update_supported_features( - event.data["entity_id"], event.data["new_state"] - ) - self.async_defer_or_update_ha_state() - - self.async_on_remove( - async_track_state_change_event( - self.hass, self._entity_ids, async_state_changed_listener - ) - ) - self.async_on_remove(start.async_at_start(self.hass, self._update_at_start)) - - @callback - def _update_at_start(self, _: HomeAssistant) -> None: - """Update the group state at start.""" - self.async_update_group_state() - self.async_write_ha_state() - - @callback - def async_defer_or_update_ha_state(self) -> None: - """Only update once at start.""" - if not self.hass.is_running: - return - - self.async_update_group_state() - self.async_write_ha_state() - - @abstractmethod - @callback - def async_update_group_state(self) -> None: - """Abstract method to update the entity.""" - - @callback - def async_update_supported_features( - self, - entity_id: str, - new_state: State | None, - ) -> None: - """Update dictionaries with supported features.""" - - -class Group(Entity): - """Track a group of entity ids.""" - - _unrecorded_attributes = frozenset({ATTR_ENTITY_ID, ATTR_ORDER, ATTR_AUTO}) - - _attr_should_poll = False - tracking: tuple[str, ...] - trackable: tuple[str, ...] - - def __init__( - self, - hass: HomeAssistant, - name: str, - *, - created_by_service: bool, - entity_ids: Collection[str] | None, - icon: str | None, - mode: bool | None, - order: int | None, - ) -> None: - """Initialize a group. - - This Object has factory function for creation. - """ - self.hass = hass - self._name = name - self._state: str | None = None - self._icon = icon - self._set_tracked(entity_ids) - self._on_off: dict[str, bool] = {} - self._assumed: dict[str, bool] = {} - self._on_states: set[str] = set() - self.created_by_service = created_by_service - self.mode = any - if mode: - self.mode = all - self._order = order - self._assumed_state = False - self._async_unsub_state_changed: CALLBACK_TYPE | None = None - - @staticmethod - @callback - def async_create_group_entity( - hass: HomeAssistant, - name: str, - *, - created_by_service: bool, - entity_ids: Collection[str] | None, - icon: str | None, - mode: bool | None, - object_id: str | None, - order: int | None, - ) -> Group: - """Create a group entity.""" - if order is None: - hass.data.setdefault(GROUP_ORDER, 0) - order = hass.data[GROUP_ORDER] - # Keep track of the group order without iterating - # every state in the state machine every time - # we setup a new group - hass.data[GROUP_ORDER] += 1 - - group = Group( - hass, - name, - created_by_service=created_by_service, - entity_ids=entity_ids, - icon=icon, - mode=mode, - order=order, - ) - - group.entity_id = async_generate_entity_id( - ENTITY_ID_FORMAT, object_id or name, hass=hass - ) - - return group - - @staticmethod - async def async_create_group( - hass: HomeAssistant, - name: str, - *, - created_by_service: bool, - entity_ids: Collection[str] | None, - icon: str | None, - mode: bool | None, - object_id: str | None, - order: int | None, - ) -> Group: - """Initialize a group. - - This method must be run in the event loop. - """ - group = Group.async_create_group_entity( - hass, - name, - created_by_service=created_by_service, - entity_ids=entity_ids, - icon=icon, - mode=mode, - object_id=object_id, - order=order, - ) - - # If called before the platform async_setup is called (test cases) - await _async_get_component(hass).async_add_entities([group]) - return group - - @property - def name(self) -> str: - """Return the name of the group.""" - return self._name - - @name.setter - def name(self, value: str) -> None: - """Set Group name.""" - self._name = value - - @property - def state(self) -> str | None: - """Return the state of the group.""" - return self._state - - @property - def icon(self) -> str | None: - """Return the icon of the group.""" - return self._icon - - @icon.setter - def icon(self, value: str | None) -> None: - """Set Icon for group.""" - self._icon = value - - @property - def extra_state_attributes(self) -> dict[str, Any]: - """Return the state attributes for the group.""" - data = {ATTR_ENTITY_ID: self.tracking, ATTR_ORDER: self._order} - if self.created_by_service: - data[ATTR_AUTO] = True - - return data - - @property - def assumed_state(self) -> bool: - """Test if any member has an assumed state.""" - return self._assumed_state - - def update_tracked_entity_ids(self, entity_ids: Collection[str] | None) -> None: - """Update the member entity IDs.""" - asyncio.run_coroutine_threadsafe( - self.async_update_tracked_entity_ids(entity_ids), self.hass.loop - ).result() - - async def async_update_tracked_entity_ids( - self, entity_ids: Collection[str] | None - ) -> None: - """Update the member entity IDs. - - This method must be run in the event loop. - """ - self._async_stop() - self._set_tracked(entity_ids) - self._reset_tracked_state() - self._async_start() - - def _set_tracked(self, entity_ids: Collection[str] | None) -> None: - """Tuple of entities to be tracked.""" - # tracking are the entities we want to track - # trackable are the entities we actually watch - - if not entity_ids: - self.tracking = () - self.trackable = () - return - - registry: GroupIntegrationRegistry = self.hass.data[REG_KEY] - excluded_domains = registry.exclude_domains - - tracking: list[str] = [] - trackable: list[str] = [] - for ent_id in entity_ids: - ent_id_lower = ent_id.lower() - domain = split_entity_id(ent_id_lower)[0] - tracking.append(ent_id_lower) - if domain not in excluded_domains: - trackable.append(ent_id_lower) - - self.trackable = tuple(trackable) - self.tracking = tuple(tracking) - - @callback - def _async_start(self, _: HomeAssistant | None = None) -> None: - """Start tracking members and write state.""" - self._reset_tracked_state() - self._async_start_tracking() - self.async_write_ha_state() - - @callback - def _async_start_tracking(self) -> None: - """Start tracking members. - - This method must be run in the event loop. - """ - if self.trackable and self._async_unsub_state_changed is None: - self._async_unsub_state_changed = async_track_state_change_event( - self.hass, self.trackable, self._async_state_changed_listener - ) - - self._async_update_group_state() - - @callback - def _async_stop(self) -> None: - """Unregister the group from Home Assistant. - - This method must be run in the event loop. - """ - if self._async_unsub_state_changed: - self._async_unsub_state_changed() - self._async_unsub_state_changed = None - - @callback - def async_update_group_state(self) -> None: - """Query all members and determine current group state.""" - self._state = None - self._async_update_group_state() - - async def async_added_to_hass(self) -> None: - """Handle addition to Home Assistant.""" - self.async_on_remove(start.async_at_start(self.hass, self._async_start)) - - async def async_will_remove_from_hass(self) -> None: - """Handle removal from Home Assistant.""" - self._async_stop() - - async def _async_state_changed_listener( - self, event: EventType[EventStateChangedData] - ) -> None: - """Respond to a member state changing. - - This method must be run in the event loop. - """ - # removed - if self._async_unsub_state_changed is None: - return - - self.async_set_context(event.context) - - if (new_state := event.data["new_state"]) is None: - # The state was removed from the state machine - self._reset_tracked_state() - - self._async_update_group_state(new_state) - self.async_write_ha_state() - - def _reset_tracked_state(self) -> None: - """Reset tracked state.""" - self._on_off = {} - self._assumed = {} - self._on_states = set() - - for entity_id in self.trackable: - if (state := self.hass.states.get(entity_id)) is not None: - self._see_state(state) - - def _see_state(self, new_state: State) -> None: - """Keep track of the state.""" - entity_id = new_state.entity_id - domain = new_state.domain - state = new_state.state - registry: GroupIntegrationRegistry = self.hass.data[REG_KEY] - self._assumed[entity_id] = bool(new_state.attributes.get(ATTR_ASSUMED_STATE)) - - if domain not in registry.on_states_by_domain: - # Handle the group of a group case - if state in registry.on_off_mapping: - self._on_states.add(state) - elif state in registry.off_on_mapping: - self._on_states.add(registry.off_on_mapping[state]) - self._on_off[entity_id] = state in registry.on_off_mapping - else: - entity_on_state = registry.on_states_by_domain[domain] - if domain in registry.on_states_by_domain: - self._on_states.update(entity_on_state) - self._on_off[entity_id] = state in entity_on_state - - @callback - def _async_update_group_state(self, tr_state: State | None = None) -> None: - """Update group state. - - Optionally you can provide the only state changed since last update - allowing this method to take shortcuts. - - This method must be run in the event loop. - """ - # To store current states of group entities. Might not be needed. - if tr_state: - self._see_state(tr_state) - - if not self._on_off: - return - - if ( - tr_state is None - or self._assumed_state - and not tr_state.attributes.get(ATTR_ASSUMED_STATE) - ): - self._assumed_state = self.mode(self._assumed.values()) - - elif tr_state.attributes.get(ATTR_ASSUMED_STATE): - self._assumed_state = True - - num_on_states = len(self._on_states) - # If all the entity domains we are tracking - # have the same on state we use this state - # and its hass.data[REG_KEY].on_off_mapping to off - if num_on_states == 1: - on_state = list(self._on_states)[0] - # If we do not have an on state for any domains - # we use None (which will be STATE_UNKNOWN) - elif num_on_states == 0: - self._state = None - return - # If the entity domains have more than one - # on state, we use STATE_ON/STATE_OFF - else: - on_state = STATE_ON - group_is_on = self.mode(self._on_off.values()) - if group_is_on: - self._state = on_state - else: - registry: GroupIntegrationRegistry = self.hass.data[REG_KEY] - self._state = registry.on_off_mapping[on_state] + await async_get_component(hass).async_add_entities(entities) diff --git a/homeassistant/components/group/binary_sensor.py b/homeassistant/components/group/binary_sensor.py index d63dcb5e8f2..3fbadfb156c 100644 --- a/homeassistant/components/group/binary_sensor.py +++ b/homeassistant/components/group/binary_sensor.py @@ -1,4 +1,5 @@ """Platform allowing several binary sensor to be grouped into one binary sensor.""" + from __future__ import annotations from typing import Any @@ -28,7 +29,7 @@ from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import GroupEntity +from .entity import GroupEntity DEFAULT_NAME = "Binary Sensor Group" diff --git a/homeassistant/components/group/config_flow.py b/homeassistant/components/group/config_flow.py index 488f5e131f3..f3e2405d86a 100644 --- a/homeassistant/components/group/config_flow.py +++ b/homeassistant/components/group/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Group integration.""" + from __future__ import annotations from collections.abc import Callable, Coroutine, Mapping @@ -21,10 +22,10 @@ from homeassistant.helpers.schema_config_entry_flow import ( entity_selector_without_own_entities, ) -from . import DOMAIN, GroupEntity from .binary_sensor import CONF_ALL, async_create_preview_binary_sensor -from .const import CONF_HIDE_MEMBERS, CONF_IGNORE_NON_NUMERIC +from .const import CONF_HIDE_MEMBERS, CONF_IGNORE_NON_NUMERIC, DOMAIN from .cover import async_create_preview_cover +from .entity import GroupEntity from .event import async_create_preview_event from .fan import async_create_preview_fan from .light import async_create_preview_light diff --git a/homeassistant/components/group/const.py b/homeassistant/components/group/const.py index 3ef280b2770..0fdd429269f 100644 --- a/homeassistant/components/group/const.py +++ b/homeassistant/components/group/const.py @@ -2,3 +2,18 @@ CONF_HIDE_MEMBERS = "hide_members" CONF_IGNORE_NON_NUMERIC = "ignore_non_numeric" + +DOMAIN = "group" + +REG_KEY = f"{DOMAIN}_registry" + +GROUP_ORDER = "group_order" + + +ATTR_ADD_ENTITIES = "add_entities" +ATTR_REMOVE_ENTITIES = "remove_entities" +ATTR_AUTO = "auto" +ATTR_ENTITIES = "entities" +ATTR_OBJECT_ID = "object_id" +ATTR_ORDER = "order" +ATTR_ALL = "all" diff --git a/homeassistant/components/group/cover.py b/homeassistant/components/group/cover.py index 78d29378076..02e5ebbc7cd 100644 --- a/homeassistant/components/group/cover.py +++ b/homeassistant/components/group/cover.py @@ -1,4 +1,5 @@ """Platform allowing several cover to be grouped into one cover.""" + from __future__ import annotations from typing import Any @@ -42,7 +43,7 @@ from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import GroupEntity +from .entity import GroupEntity from .util import reduce_attribute KEY_OPEN_CLOSE = "open_close" diff --git a/homeassistant/components/group/entity.py b/homeassistant/components/group/entity.py new file mode 100644 index 00000000000..24c10fd2e7b --- /dev/null +++ b/homeassistant/components/group/entity.py @@ -0,0 +1,471 @@ +"""Provide entity classes for group entities.""" + +from __future__ import annotations + +from abc import abstractmethod +import asyncio +from collections.abc import Callable, Collection, Mapping +import logging +from typing import Any + +from homeassistant.const import ATTR_ASSUMED_STATE, ATTR_ENTITY_ID, STATE_ON +from homeassistant.core import ( + CALLBACK_TYPE, + Event, + HomeAssistant, + State, + callback, + split_entity_id, +) +from homeassistant.helpers import start +from homeassistant.helpers.entity import Entity, async_generate_entity_id +from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.helpers.event import ( + EventStateChangedData, + async_track_state_change_event, +) + +from .const import ATTR_AUTO, ATTR_ORDER, DOMAIN, GROUP_ORDER, REG_KEY +from .registry import GroupIntegrationRegistry + +ENTITY_ID_FORMAT = DOMAIN + ".{}" + +_PACKAGE_LOGGER = logging.getLogger(__package__) + +_LOGGER = logging.getLogger(__name__) + + +class GroupEntity(Entity): + """Representation of a Group of entities.""" + + _unrecorded_attributes = frozenset({ATTR_ENTITY_ID}) + + _attr_should_poll = False + _entity_ids: list[str] + + @callback + def async_start_preview( + self, + preview_callback: Callable[[str, Mapping[str, Any]], None], + ) -> CALLBACK_TYPE: + """Render a preview.""" + + for entity_id in self._entity_ids: + if (state := self.hass.states.get(entity_id)) is None: + continue + self.async_update_supported_features(entity_id, state) + + @callback + def async_state_changed_listener( + event: Event[EventStateChangedData] | None, + ) -> None: + """Handle child updates.""" + self.async_update_group_state() + if event: + self.async_update_supported_features( + event.data["entity_id"], event.data["new_state"] + ) + calculated_state = self._async_calculate_state() + preview_callback(calculated_state.state, calculated_state.attributes) + + async_state_changed_listener(None) + return async_track_state_change_event( + self.hass, self._entity_ids, async_state_changed_listener + ) + + async def async_added_to_hass(self) -> None: + """Register listeners.""" + for entity_id in self._entity_ids: + if (state := self.hass.states.get(entity_id)) is None: + continue + self.async_update_supported_features(entity_id, state) + + @callback + def async_state_changed_listener( + event: Event[EventStateChangedData], + ) -> None: + """Handle child updates.""" + self.async_set_context(event.context) + self.async_update_supported_features( + event.data["entity_id"], event.data["new_state"] + ) + self.async_defer_or_update_ha_state() + + self.async_on_remove( + async_track_state_change_event( + self.hass, self._entity_ids, async_state_changed_listener + ) + ) + self.async_on_remove(start.async_at_start(self.hass, self._update_at_start)) + + @callback + def _update_at_start(self, _: HomeAssistant) -> None: + """Update the group state at start.""" + self.async_update_group_state() + self.async_write_ha_state() + + @callback + def async_defer_or_update_ha_state(self) -> None: + """Only update once at start.""" + if not self.hass.is_running: + return + + self.async_update_group_state() + self.async_write_ha_state() + + @abstractmethod + @callback + def async_update_group_state(self) -> None: + """Abstract method to update the entity.""" + + @callback + def async_update_supported_features( + self, + entity_id: str, + new_state: State | None, + ) -> None: + """Update dictionaries with supported features.""" + + +class Group(Entity): + """Track a group of entity ids.""" + + _unrecorded_attributes = frozenset({ATTR_ENTITY_ID, ATTR_ORDER, ATTR_AUTO}) + + _attr_should_poll = False + tracking: tuple[str, ...] + trackable: tuple[str, ...] + + def __init__( + self, + hass: HomeAssistant, + name: str, + *, + created_by_service: bool, + entity_ids: Collection[str] | None, + icon: str | None, + mode: bool | None, + order: int | None, + ) -> None: + """Initialize a group. + + This Object has factory function for creation. + """ + self.hass = hass + self._name = name + self._state: str | None = None + self._icon = icon + self._set_tracked(entity_ids) + self._on_off: dict[str, bool] = {} + self._assumed: dict[str, bool] = {} + self._on_states: set[str] = set() + self.created_by_service = created_by_service + self.mode = any + if mode: + self.mode = all + self._order = order + self._assumed_state = False + self._async_unsub_state_changed: CALLBACK_TYPE | None = None + + @staticmethod + @callback + def async_create_group_entity( + hass: HomeAssistant, + name: str, + *, + created_by_service: bool, + entity_ids: Collection[str] | None, + icon: str | None, + mode: bool | None, + object_id: str | None, + order: int | None, + ) -> Group: + """Create a group entity.""" + if order is None: + hass.data.setdefault(GROUP_ORDER, 0) + order = hass.data[GROUP_ORDER] + # Keep track of the group order without iterating + # every state in the state machine every time + # we setup a new group + hass.data[GROUP_ORDER] += 1 + + group = Group( + hass, + name, + created_by_service=created_by_service, + entity_ids=entity_ids, + icon=icon, + mode=mode, + order=order, + ) + + group.entity_id = async_generate_entity_id( + ENTITY_ID_FORMAT, object_id or name, hass=hass + ) + + return group + + @staticmethod + async def async_create_group( + hass: HomeAssistant, + name: str, + *, + created_by_service: bool, + entity_ids: Collection[str] | None, + icon: str | None, + mode: bool | None, + object_id: str | None, + order: int | None, + ) -> Group: + """Initialize a group. + + This method must be run in the event loop. + """ + group = Group.async_create_group_entity( + hass, + name, + created_by_service=created_by_service, + entity_ids=entity_ids, + icon=icon, + mode=mode, + object_id=object_id, + order=order, + ) + + # If called before the platform async_setup is called (test cases) + await async_get_component(hass).async_add_entities([group]) + return group + + @property + def name(self) -> str: + """Return the name of the group.""" + return self._name + + @name.setter + def name(self, value: str) -> None: + """Set Group name.""" + self._name = value + + @property + def state(self) -> str | None: + """Return the state of the group.""" + return self._state + + @property + def icon(self) -> str | None: + """Return the icon of the group.""" + return self._icon + + @icon.setter + def icon(self, value: str | None) -> None: + """Set Icon for group.""" + self._icon = value + + @property + def extra_state_attributes(self) -> dict[str, Any]: + """Return the state attributes for the group.""" + data = {ATTR_ENTITY_ID: self.tracking, ATTR_ORDER: self._order} + if self.created_by_service: + data[ATTR_AUTO] = True + + return data + + @property + def assumed_state(self) -> bool: + """Test if any member has an assumed state.""" + return self._assumed_state + + def update_tracked_entity_ids(self, entity_ids: Collection[str] | None) -> None: + """Update the member entity IDs.""" + asyncio.run_coroutine_threadsafe( + self.async_update_tracked_entity_ids(entity_ids), self.hass.loop + ).result() + + async def async_update_tracked_entity_ids( + self, entity_ids: Collection[str] | None + ) -> None: + """Update the member entity IDs. + + This method must be run in the event loop. + """ + self._async_stop() + self._set_tracked(entity_ids) + self._reset_tracked_state() + self._async_start() + + def _set_tracked(self, entity_ids: Collection[str] | None) -> None: + """Tuple of entities to be tracked.""" + # tracking are the entities we want to track + # trackable are the entities we actually watch + + if not entity_ids: + self.tracking = () + self.trackable = () + return + + registry: GroupIntegrationRegistry = self.hass.data[REG_KEY] + excluded_domains = registry.exclude_domains + + tracking: list[str] = [] + trackable: list[str] = [] + for ent_id in entity_ids: + ent_id_lower = ent_id.lower() + domain = split_entity_id(ent_id_lower)[0] + tracking.append(ent_id_lower) + if domain not in excluded_domains: + trackable.append(ent_id_lower) + + self.trackable = tuple(trackable) + self.tracking = tuple(tracking) + + @callback + def _async_start(self, _: HomeAssistant | None = None) -> None: + """Start tracking members and write state.""" + self._reset_tracked_state() + self._async_start_tracking() + self.async_write_ha_state() + + @callback + def _async_start_tracking(self) -> None: + """Start tracking members. + + This method must be run in the event loop. + """ + if self.trackable and self._async_unsub_state_changed is None: + self._async_unsub_state_changed = async_track_state_change_event( + self.hass, self.trackable, self._async_state_changed_listener + ) + + self._async_update_group_state() + + @callback + def _async_stop(self) -> None: + """Unregister the group from Home Assistant. + + This method must be run in the event loop. + """ + if self._async_unsub_state_changed: + self._async_unsub_state_changed() + self._async_unsub_state_changed = None + + @callback + def async_update_group_state(self) -> None: + """Query all members and determine current group state.""" + self._state = None + self._async_update_group_state() + + async def async_added_to_hass(self) -> None: + """Handle addition to Home Assistant.""" + self.async_on_remove(start.async_at_start(self.hass, self._async_start)) + + async def async_will_remove_from_hass(self) -> None: + """Handle removal from Home Assistant.""" + self._async_stop() + + async def _async_state_changed_listener( + self, event: Event[EventStateChangedData] + ) -> None: + """Respond to a member state changing. + + This method must be run in the event loop. + """ + # removed + if self._async_unsub_state_changed is None: + return + + self.async_set_context(event.context) + + if (new_state := event.data["new_state"]) is None: + # The state was removed from the state machine + self._reset_tracked_state() + + self._async_update_group_state(new_state) + self.async_write_ha_state() + + def _reset_tracked_state(self) -> None: + """Reset tracked state.""" + self._on_off = {} + self._assumed = {} + self._on_states = set() + + for entity_id in self.trackable: + if (state := self.hass.states.get(entity_id)) is not None: + self._see_state(state) + + def _see_state(self, new_state: State) -> None: + """Keep track of the state.""" + entity_id = new_state.entity_id + domain = new_state.domain + state = new_state.state + registry: GroupIntegrationRegistry = self.hass.data[REG_KEY] + self._assumed[entity_id] = bool(new_state.attributes.get(ATTR_ASSUMED_STATE)) + + if domain not in registry.on_states_by_domain: + # Handle the group of a group case + if state in registry.on_off_mapping: + self._on_states.add(state) + elif state in registry.off_on_mapping: + self._on_states.add(registry.off_on_mapping[state]) + self._on_off[entity_id] = state in registry.on_off_mapping + else: + entity_on_state = registry.on_states_by_domain[domain] + if domain in registry.on_states_by_domain: + self._on_states.update(entity_on_state) + self._on_off[entity_id] = state in entity_on_state + + @callback + def _async_update_group_state(self, tr_state: State | None = None) -> None: + """Update group state. + + Optionally you can provide the only state changed since last update + allowing this method to take shortcuts. + + This method must be run in the event loop. + """ + # To store current states of group entities. Might not be needed. + if tr_state: + self._see_state(tr_state) + + if not self._on_off: + return + + if ( + tr_state is None + or self._assumed_state + and not tr_state.attributes.get(ATTR_ASSUMED_STATE) + ): + self._assumed_state = self.mode(self._assumed.values()) + + elif tr_state.attributes.get(ATTR_ASSUMED_STATE): + self._assumed_state = True + + num_on_states = len(self._on_states) + # If all the entity domains we are tracking + # have the same on state we use this state + # and its hass.data[REG_KEY].on_off_mapping to off + if num_on_states == 1: + on_state = list(self._on_states)[0] + # If we do not have an on state for any domains + # we use None (which will be STATE_UNKNOWN) + elif num_on_states == 0: + self._state = None + return + # If the entity domains have more than one + # on state, we use STATE_ON/STATE_OFF + else: + on_state = STATE_ON + group_is_on = self.mode(self._on_off.values()) + if group_is_on: + self._state = on_state + else: + registry: GroupIntegrationRegistry = self.hass.data[REG_KEY] + self._state = registry.on_off_mapping[on_state] + + +def async_get_component(hass: HomeAssistant) -> EntityComponent[Group]: + """Get the group entity component.""" + if (component := hass.data.get(DOMAIN)) is None: + component = hass.data[DOMAIN] = EntityComponent[Group]( + _PACKAGE_LOGGER, DOMAIN, hass + ) + return component diff --git a/homeassistant/components/group/event.py b/homeassistant/components/group/event.py index b98991e13fc..61ddb3e0645 100644 --- a/homeassistant/components/group/event.py +++ b/homeassistant/components/group/event.py @@ -1,4 +1,5 @@ """Platform allowing several event entities to be grouped into one event.""" + from __future__ import annotations import itertools @@ -24,16 +25,16 @@ from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import Event, HomeAssistant, callback from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import ( EventStateChangedData, async_track_state_change_event, ) -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, EventType +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import GroupEntity +from .entity import GroupEntity DEFAULT_NAME = "Event group" @@ -124,7 +125,7 @@ class EventGroup(GroupEntity, EventEntity): @callback def async_state_changed_listener( - event: EventType[EventStateChangedData], + event: Event[EventStateChangedData], ) -> None: """Handle child updates.""" if not self.hass.is_running: diff --git a/homeassistant/components/group/fan.py b/homeassistant/components/group/fan.py index afd240c5767..b70a4ff1531 100644 --- a/homeassistant/components/group/fan.py +++ b/homeassistant/components/group/fan.py @@ -1,4 +1,5 @@ """Platform allowing several fans to be grouped into one fan.""" + from __future__ import annotations from functools import reduce @@ -39,7 +40,7 @@ from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import GroupEntity +from .entity import GroupEntity from .util import attribute_equal, most_frequent_attribute, reduce_attribute SUPPORTED_FLAGS = { diff --git a/homeassistant/components/group/icons.json b/homeassistant/components/group/icons.json new file mode 100644 index 00000000000..8cca94e08e1 --- /dev/null +++ b/homeassistant/components/group/icons.json @@ -0,0 +1,7 @@ +{ + "services": { + "reload": "mdi:reload", + "set": "mdi:home-group-plus", + "remove": "mdi:home-group-remove" + } +} diff --git a/homeassistant/components/group/light.py b/homeassistant/components/group/light.py index c8689cdaa1c..9adced828c7 100644 --- a/homeassistant/components/group/light.py +++ b/homeassistant/components/group/light.py @@ -1,4 +1,5 @@ """Platform allowing several lights to be grouped into one light.""" + from __future__ import annotations from collections import Counter @@ -50,7 +51,7 @@ from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import GroupEntity +from .entity import GroupEntity from .util import find_state_attributes, mean_tuple, reduce_attribute DEFAULT_NAME = "Light Group" diff --git a/homeassistant/components/group/lock.py b/homeassistant/components/group/lock.py index 4a6fdc3e2ed..b0cf36bd6b1 100644 --- a/homeassistant/components/group/lock.py +++ b/homeassistant/components/group/lock.py @@ -1,4 +1,5 @@ """Platform allowing several locks to be grouped into one lock.""" + from __future__ import annotations import logging @@ -33,7 +34,7 @@ from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import GroupEntity +from .entity import GroupEntity DEFAULT_NAME = "Lock Group" diff --git a/homeassistant/components/group/media_player.py b/homeassistant/components/group/media_player.py index aa38f364d93..ccb7154f7c1 100644 --- a/homeassistant/components/group/media_player.py +++ b/homeassistant/components/group/media_player.py @@ -1,4 +1,5 @@ """Platform allowing several media players to be grouped into one media player.""" + from __future__ import annotations from collections.abc import Callable, Mapping @@ -44,14 +45,14 @@ from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.core import CALLBACK_TYPE, HomeAssistant, State, callback +from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, State, callback from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import ( EventStateChangedData, async_track_state_change_event, ) -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, EventType +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType KEY_ANNOUNCE = "announce" KEY_CLEAR_PLAYLIST = "clear_playlist" @@ -147,7 +148,7 @@ class MediaPlayerGroup(MediaPlayerEntity): } @callback - def async_on_state_change(self, event: EventType[EventStateChangedData]) -> None: + def async_on_state_change(self, event: Event[EventStateChangedData]) -> None: """Update supported features and state when a new state is received.""" self.async_set_context(event.context) self.async_update_supported_features( @@ -232,7 +233,7 @@ class MediaPlayerGroup(MediaPlayerEntity): @callback def async_state_changed_listener( - event: EventType[EventStateChangedData] | None, + event: Event[EventStateChangedData] | None, ) -> None: """Handle child updates.""" self.async_update_group_state() diff --git a/homeassistant/components/group/notify.py b/homeassistant/components/group/notify.py index 2747ba55ee1..bad3d7944d3 100644 --- a/homeassistant/components/group/notify.py +++ b/homeassistant/components/group/notify.py @@ -1,4 +1,5 @@ """Group platform for notify component.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/group/registry.py b/homeassistant/components/group/registry.py new file mode 100644 index 00000000000..1441d39d331 --- /dev/null +++ b/homeassistant/components/group/registry.py @@ -0,0 +1,68 @@ +"""Provide the functionality to group entities.""" + +from __future__ import annotations + +from contextvars import ContextVar +from typing import Protocol + +from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.integration_platform import ( + async_process_integration_platforms, +) + +from .const import DOMAIN, REG_KEY + +current_domain: ContextVar[str] = ContextVar("current_domain") + + +async def async_setup(hass: HomeAssistant) -> None: + """Set up the Group integration registry of integration platforms.""" + hass.data[REG_KEY] = GroupIntegrationRegistry() + + await async_process_integration_platforms( + hass, DOMAIN, _process_group_platform, wait_for_platforms=True + ) + + +class GroupProtocol(Protocol): + """Define the format of group platforms.""" + + def async_describe_on_off_states( + self, hass: HomeAssistant, registry: GroupIntegrationRegistry + ) -> None: + """Describe group on off states.""" + + +@callback +def _process_group_platform( + hass: HomeAssistant, domain: str, platform: GroupProtocol +) -> None: + """Process a group platform.""" + current_domain.set(domain) + registry: GroupIntegrationRegistry = hass.data[REG_KEY] + platform.async_describe_on_off_states(hass, registry) + + +class GroupIntegrationRegistry: + """Class to hold a registry of integrations.""" + + on_off_mapping: dict[str, str] = {STATE_ON: STATE_OFF} + off_on_mapping: dict[str, str] = {STATE_OFF: STATE_ON} + on_states_by_domain: dict[str, set] = {} + exclude_domains: set = set() + + def exclude_domain(self) -> None: + """Exclude the current domain.""" + self.exclude_domains.add(current_domain.get()) + + def on_off_states(self, on_states: set, off_state: str) -> None: + """Register on and off states for the current domain.""" + for on_state in on_states: + if on_state not in self.on_off_mapping: + self.on_off_mapping[on_state] = off_state + + if len(on_states) == 1 and off_state not in self.off_on_mapping: + self.off_on_mapping[off_state] = list(on_states)[0] + + self.on_states_by_domain[current_domain.get()] = set(on_states) diff --git a/homeassistant/components/group/reproduce_state.py b/homeassistant/components/group/reproduce_state.py index c99f098b222..06d4f95dee3 100644 --- a/homeassistant/components/group/reproduce_state.py +++ b/homeassistant/components/group/reproduce_state.py @@ -1,4 +1,5 @@ """Module that groups code required to handle state restore for component.""" + from __future__ import annotations from collections.abc import Iterable @@ -18,20 +19,19 @@ async def async_reproduce_states( reproduce_options: dict[str, Any] | None = None, ) -> None: """Reproduce component states.""" - states_copy = [] - for state in states: - members = get_entity_ids(hass, state.entity_id) - for member in members: - states_copy.append( - State( - member, - state.state, - state.attributes, - last_changed=state.last_changed, - last_updated=state.last_updated, - context=state.context, - ) - ) + states_copy = [ + State( + member, + state.state, + state.attributes, + last_changed=state.last_changed, + last_reported=state.last_reported, + last_updated=state.last_updated, + context=state.context, + ) + for state in states + for member in get_entity_ids(hass, state.entity_id) + ] await async_reproduce_state( hass, states_copy, context=context, reproduce_options=reproduce_options ) diff --git a/homeassistant/components/group/sensor.py b/homeassistant/components/group/sensor.py index 7334831211d..5de668c7bb0 100644 --- a/homeassistant/components/group/sensor.py +++ b/homeassistant/components/group/sensor.py @@ -52,8 +52,8 @@ from homeassistant.helpers.issue_registry import ( ) from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType -from . import DOMAIN as GROUP_DOMAIN, GroupEntity -from .const import CONF_IGNORE_NON_NUMERIC +from .const import CONF_IGNORE_NON_NUMERIC, DOMAIN as GROUP_DOMAIN +from .entity import GroupEntity DEFAULT_NAME = "Sensor Group" diff --git a/homeassistant/components/group/switch.py b/homeassistant/components/group/switch.py index 3f68d7125aa..7be6b188e72 100644 --- a/homeassistant/components/group/switch.py +++ b/homeassistant/components/group/switch.py @@ -1,4 +1,5 @@ """Platform allowing several switches to be grouped into one switch.""" + from __future__ import annotations import logging @@ -24,7 +25,7 @@ from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import GroupEntity +from .entity import GroupEntity DEFAULT_NAME = "Switch Group" CONF_ALL = "all" diff --git a/homeassistant/components/group/util.py b/homeassistant/components/group/util.py index da67e071f27..14f5064290f 100644 --- a/homeassistant/components/group/util.py +++ b/homeassistant/components/group/util.py @@ -1,4 +1,5 @@ """Utility functions to combine state attributes from multiple entities.""" + from __future__ import annotations from collections.abc import Callable, Iterator diff --git a/homeassistant/components/growatt_server/__init__.py b/homeassistant/components/growatt_server/__init__.py index 177d0957883..66df76bc6cb 100644 --- a/homeassistant/components/growatt_server/__init__.py +++ b/homeassistant/components/growatt_server/__init__.py @@ -1,4 +1,5 @@ """The Growatt server PV inverter sensor integration.""" + from homeassistant import config_entries from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/growatt_server/config_flow.py b/homeassistant/components/growatt_server/config_flow.py index a4dcd25173f..95002a70a95 100644 --- a/homeassistant/components/growatt_server/config_flow.py +++ b/homeassistant/components/growatt_server/config_flow.py @@ -1,8 +1,9 @@ """Config flow for growatt server integration.""" + import growattServer import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_URL, CONF_USERNAME from homeassistant.core import callback @@ -15,7 +16,7 @@ from .const import ( ) -class GrowattServerConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class GrowattServerConfigFlow(ConfigFlow, domain=DOMAIN): """Config flow class.""" VERSION = 1 diff --git a/homeassistant/components/growatt_server/const.py b/homeassistant/components/growatt_server/const.py index e4e7c638fa3..fe8622bea7f 100644 --- a/homeassistant/components/growatt_server/const.py +++ b/homeassistant/components/growatt_server/const.py @@ -1,4 +1,5 @@ """Define constants for the Growatt Server component.""" + from homeassistant.const import Platform CONF_PLANT_ID = "plant_id" diff --git a/homeassistant/components/growatt_server/sensor.py b/homeassistant/components/growatt_server/sensor.py index 06d06ed26ce..3cf1fa30c99 100644 --- a/homeassistant/components/growatt_server/sensor.py +++ b/homeassistant/components/growatt_server/sensor.py @@ -1,4 +1,5 @@ """Read status of growatt inverters.""" + from __future__ import annotations import datetime diff --git a/homeassistant/components/growatt_server/sensor_types/inverter.py b/homeassistant/components/growatt_server/sensor_types/inverter.py index cfacadce528..6f78e019184 100644 --- a/homeassistant/components/growatt_server/sensor_types/inverter.py +++ b/homeassistant/components/growatt_server/sensor_types/inverter.py @@ -1,4 +1,5 @@ """Growatt Sensor definitions for the Inverter type.""" + from __future__ import annotations from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass diff --git a/homeassistant/components/growatt_server/sensor_types/mix.py b/homeassistant/components/growatt_server/sensor_types/mix.py index e9722abda11..b741a589b8f 100644 --- a/homeassistant/components/growatt_server/sensor_types/mix.py +++ b/homeassistant/components/growatt_server/sensor_types/mix.py @@ -1,4 +1,5 @@ """Growatt Sensor definitions for the Mix type.""" + from __future__ import annotations from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass diff --git a/homeassistant/components/growatt_server/sensor_types/sensor_entity_description.py b/homeassistant/components/growatt_server/sensor_types/sensor_entity_description.py index cfeb98a382e..e1ee4c30326 100644 --- a/homeassistant/components/growatt_server/sensor_types/sensor_entity_description.py +++ b/homeassistant/components/growatt_server/sensor_types/sensor_entity_description.py @@ -1,4 +1,5 @@ """Sensor Entity Description for the Growatt integration.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/homeassistant/components/growatt_server/sensor_types/storage.py b/homeassistant/components/growatt_server/sensor_types/storage.py index 4b60a73c979..e8895755c65 100644 --- a/homeassistant/components/growatt_server/sensor_types/storage.py +++ b/homeassistant/components/growatt_server/sensor_types/storage.py @@ -1,4 +1,5 @@ """Growatt Sensor definitions for the Storage type.""" + from __future__ import annotations from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass diff --git a/homeassistant/components/growatt_server/sensor_types/tlx.py b/homeassistant/components/growatt_server/sensor_types/tlx.py index 645b32db9d0..d8f158f2421 100644 --- a/homeassistant/components/growatt_server/sensor_types/tlx.py +++ b/homeassistant/components/growatt_server/sensor_types/tlx.py @@ -2,6 +2,7 @@ TLX Type is also shown on the UI as: "MIN/MIC/MOD/NEO" """ + from __future__ import annotations from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass diff --git a/homeassistant/components/growatt_server/sensor_types/total.py b/homeassistant/components/growatt_server/sensor_types/total.py index 5945ad20e40..8111728d1e9 100644 --- a/homeassistant/components/growatt_server/sensor_types/total.py +++ b/homeassistant/components/growatt_server/sensor_types/total.py @@ -1,4 +1,5 @@ """Growatt Sensor definitions for Totals.""" + from __future__ import annotations from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass diff --git a/homeassistant/components/gstreamer/media_player.py b/homeassistant/components/gstreamer/media_player.py index cb221d49417..054b31c2fbe 100644 --- a/homeassistant/components/gstreamer/media_player.py +++ b/homeassistant/components/gstreamer/media_player.py @@ -1,4 +1,5 @@ """Play media via gstreamer.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/gtfs/sensor.py b/homeassistant/components/gtfs/sensor.py index 87d2b55aa24..a0a0f0ebc0e 100644 --- a/homeassistant/components/gtfs/sensor.py +++ b/homeassistant/components/gtfs/sensor.py @@ -1,4 +1,5 @@ """Support for GTFS (Google/General Transport Format Schema).""" + from __future__ import annotations import datetime @@ -736,10 +737,10 @@ class GTFSDepartureSensor(SensorEntity): self._attributes[ATTR_LOCATION_DESTINATION] = LOCATION_TYPE_OPTIONS.get( self._destination.location_type, LOCATION_TYPE_DEFAULT ) - self._attributes[ - ATTR_WHEELCHAIR_DESTINATION - ] = WHEELCHAIR_BOARDING_OPTIONS.get( - self._destination.wheelchair_boarding, WHEELCHAIR_BOARDING_DEFAULT + self._attributes[ATTR_WHEELCHAIR_DESTINATION] = ( + WHEELCHAIR_BOARDING_OPTIONS.get( + self._destination.wheelchair_boarding, WHEELCHAIR_BOARDING_DEFAULT + ) ) # Manage Route metadata diff --git a/homeassistant/components/guardian/__init__.py b/homeassistant/components/guardian/__init__.py index 8a3ac265618..812c54d76a6 100644 --- a/homeassistant/components/guardian/__init__.py +++ b/homeassistant/components/guardian/__init__.py @@ -1,4 +1,5 @@ """The Elexa Guardian integration.""" + from __future__ import annotations import asyncio @@ -138,16 +139,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: (API_VALVE_STATUS, client.valve.status), (API_WIFI_STATUS, client.wifi.status), ): - 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], + 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(async_init_coordinator(coordinator)) diff --git a/homeassistant/components/guardian/binary_sensor.py b/homeassistant/components/guardian/binary_sensor.py index c7094cf624c..c3621ea2d79 100644 --- a/homeassistant/components/guardian/binary_sensor.py +++ b/homeassistant/components/guardian/binary_sensor.py @@ -1,4 +1,5 @@ """Binary sensors for the Elexa Guardian integration.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/guardian/button.py b/homeassistant/components/guardian/button.py index cb9c6f0121c..8313ad23007 100644 --- a/homeassistant/components/guardian/button.py +++ b/homeassistant/components/guardian/button.py @@ -1,4 +1,5 @@ """Buttons for the Elexa Guardian integration.""" + from __future__ import annotations from collections.abc import Awaitable, Callable diff --git a/homeassistant/components/guardian/config_flow.py b/homeassistant/components/guardian/config_flow.py index c027fe8bc20..e73e6c586ce 100644 --- a/homeassistant/components/guardian/config_flow.py +++ b/homeassistant/components/guardian/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Elexa Guardian integration.""" + from __future__ import annotations from typing import Any @@ -7,11 +8,10 @@ from aioguardian import Client from aioguardian.errors import GuardianError import voluptuous as vol -from homeassistant import config_entries from homeassistant.components import dhcp, zeroconf +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT from homeassistant.core import HomeAssistant, callback -from homeassistant.data_entry_flow import FlowResult from .const import CONF_UID, DOMAIN, LOGGER @@ -52,7 +52,7 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, } -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class GuardianConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Elexa Guardian.""" VERSION = 1 @@ -76,7 +76,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle configuration via the UI.""" if user_input is None: return self.async_show_form( @@ -100,7 +100,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): title=info[CONF_UID], data={CONF_UID: info["uid"], **user_input} ) - async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: + async def async_step_dhcp( + self, discovery_info: dhcp.DhcpServiceInfo + ) -> ConfigFlowResult: """Handle the configuration via dhcp.""" self.discovery_info = { CONF_IP_ADDRESS: discovery_info.ip, @@ -113,7 +115,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_zeroconf( self, discovery_info: zeroconf.ZeroconfServiceInfo - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the configuration via zeroconf.""" self.discovery_info = { CONF_IP_ADDRESS: discovery_info.host, @@ -123,7 +125,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): await self._async_set_unique_id(pin) return await self._async_handle_discovery() - async def _async_handle_discovery(self) -> FlowResult: + async def _async_handle_discovery(self) -> ConfigFlowResult: """Handle any discovery.""" self.context[CONF_IP_ADDRESS] = self.discovery_info[CONF_IP_ADDRESS] if any( @@ -136,7 +138,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_discovery_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Finish the configuration via any discovery.""" if user_input is None: self._set_confirm_only() diff --git a/homeassistant/components/guardian/const.py b/homeassistant/components/guardian/const.py index c7d025ba712..593cba65264 100644 --- a/homeassistant/components/guardian/const.py +++ b/homeassistant/components/guardian/const.py @@ -1,4 +1,5 @@ """Constants for the Elexa Guardian integration.""" + import logging DOMAIN = "guardian" diff --git a/homeassistant/components/guardian/coordinator.py b/homeassistant/components/guardian/coordinator.py index dda0a20be69..819fda8bdc7 100644 --- a/homeassistant/components/guardian/coordinator.py +++ b/homeassistant/components/guardian/coordinator.py @@ -1,4 +1,5 @@ """Define Guardian-specific utilities.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/guardian/diagnostics.py b/homeassistant/components/guardian/diagnostics.py index b0317167f79..2f4287bea29 100644 --- a/homeassistant/components/guardian/diagnostics.py +++ b/homeassistant/components/guardian/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for Guardian.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/guardian/sensor.py b/homeassistant/components/guardian/sensor.py index 1941dc54248..448a7231df1 100644 --- a/homeassistant/components/guardian/sensor.py +++ b/homeassistant/components/guardian/sensor.py @@ -1,4 +1,5 @@ """Sensors for the Elexa Guardian integration.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/guardian/switch.py b/homeassistant/components/guardian/switch.py index ebe8e5549ce..25bc8115208 100644 --- a/homeassistant/components/guardian/switch.py +++ b/homeassistant/components/guardian/switch.py @@ -1,4 +1,5 @@ """Switches for the Elexa Guardian integration.""" + from __future__ import annotations from collections.abc import Awaitable, Callable, Mapping diff --git a/homeassistant/components/guardian/util.py b/homeassistant/components/guardian/util.py index a5e91dce813..6d407f9c7cc 100644 --- a/homeassistant/components/guardian/util.py +++ b/homeassistant/components/guardian/util.py @@ -1,4 +1,5 @@ """Define Guardian-specific utilities.""" + from __future__ import annotations from collections.abc import Callable, Coroutine, Iterable diff --git a/homeassistant/components/guardian/valve.py b/homeassistant/components/guardian/valve.py index a2b6b5b6ab7..fcedc71f188 100644 --- a/homeassistant/components/guardian/valve.py +++ b/homeassistant/components/guardian/valve.py @@ -1,4 +1,5 @@ """Valves for the Elexa Guardian integration.""" + from __future__ import annotations from collections.abc import Callable, Coroutine diff --git a/homeassistant/components/habitica/__init__.py b/homeassistant/components/habitica/__init__.py index 25738893689..f05bc9c1713 100644 --- a/homeassistant/components/habitica/__init__.py +++ b/homeassistant/components/habitica/__init__.py @@ -1,4 +1,5 @@ """The habitica integration.""" + import logging from habitipy.aio import HabitipyAsync diff --git a/homeassistant/components/habitica/config_flow.py b/homeassistant/components/habitica/config_flow.py index 5e016dfe605..9ee2aef40ba 100644 --- a/homeassistant/components/habitica/config_flow.py +++ b/homeassistant/components/habitica/config_flow.py @@ -1,4 +1,5 @@ """Config flow for habitica integration.""" + from __future__ import annotations import logging @@ -7,8 +8,10 @@ from aiohttp import ClientResponseError from habitipy.aio import HabitipyAsync import voluptuous as vol -from homeassistant import config_entries, core, exceptions +from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_API_KEY, CONF_NAME, CONF_URL +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import CONF_API_USER, DEFAULT_URL, DOMAIN @@ -25,9 +28,7 @@ DATA_SCHEMA = vol.Schema( _LOGGER = logging.getLogger(__name__) -async def validate_input( - hass: core.HomeAssistant, data: dict[str, str] -) -> dict[str, str]: +async def validate_input(hass: HomeAssistant, data: dict[str, str]) -> dict[str, str]: """Validate the user input allows us to connect.""" websession = async_get_clientsession(hass) @@ -45,10 +46,10 @@ async def validate_input( CONF_API_USER: data[CONF_API_USER], } except ClientResponseError as ex: - raise InvalidAuth() from ex + raise InvalidAuth from ex -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class HabiticaConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for habitica.""" VERSION = 1 @@ -81,5 +82,5 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_user(import_data) -class InvalidAuth(exceptions.HomeAssistantError): +class InvalidAuth(HomeAssistantError): """Error to indicate there is invalid auth.""" diff --git a/homeassistant/components/habitica/icons.json b/homeassistant/components/habitica/icons.json new file mode 100644 index 00000000000..4e5831c4e82 --- /dev/null +++ b/homeassistant/components/habitica/icons.json @@ -0,0 +1,5 @@ +{ + "services": { + "api_call": "mdi:console" + } +} diff --git a/homeassistant/components/habitica/sensor.py b/homeassistant/components/habitica/sensor.py index d9e0fb227c0..4d48ec199ec 100644 --- a/homeassistant/components/habitica/sensor.py +++ b/homeassistant/components/habitica/sensor.py @@ -1,4 +1,5 @@ """Support for Habitica sensors.""" + from __future__ import annotations from collections import namedtuple @@ -86,14 +87,16 @@ async def async_setup_entry( ) -> None: """Set up the habitica sensors.""" - entities: list[SensorEntity] = [] name = config_entry.data[CONF_NAME] sensor_data = HabitipyData(hass.data[DOMAIN][config_entry.entry_id]) await sensor_data.update() - for sensor_type in SENSORS_TYPES: - entities.append(HabitipySensor(name, sensor_type, sensor_data)) - for task_type in TASKS_TYPES: - entities.append(HabitipyTaskSensor(name, task_type, sensor_data)) + + entities: list[SensorEntity] = [ + HabitipySensor(name, sensor_type, sensor_data) for sensor_type in SENSORS_TYPES + ] + entities.extend( + HabitipyTaskSensor(name, task_type, sensor_data) for task_type in TASKS_TYPES + ) async_add_entities(entities, True) diff --git a/homeassistant/components/hardkernel/__init__.py b/homeassistant/components/hardkernel/__init__.py index 41e65ff8b5e..5d70f6cbfe0 100644 --- a/homeassistant/components/hardkernel/__init__.py +++ b/homeassistant/components/hardkernel/__init__.py @@ -1,4 +1,5 @@ """The Hardkernel integration.""" + from __future__ import annotations from homeassistant.components.hassio import get_os_info, is_hassio diff --git a/homeassistant/components/hardkernel/config_flow.py b/homeassistant/components/hardkernel/config_flow.py index b0445fae231..cf70adae55a 100644 --- a/homeassistant/components/hardkernel/config_flow.py +++ b/homeassistant/components/hardkernel/config_flow.py @@ -1,10 +1,10 @@ """Config flow for the Hardkernel integration.""" + from __future__ import annotations from typing import Any -from homeassistant.config_entries import ConfigFlow -from homeassistant.data_entry_flow import FlowResult +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from .const import DOMAIN @@ -14,7 +14,9 @@ class HardkernelConfigFlow(ConfigFlow, domain=DOMAIN): VERSION = 1 - async def async_step_system(self, data: dict[str, Any] | None = None) -> FlowResult: + async def async_step_system( + self, data: dict[str, Any] | None = None + ) -> ConfigFlowResult: """Handle the initial step.""" if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") diff --git a/homeassistant/components/hardkernel/hardware.py b/homeassistant/components/hardkernel/hardware.py index c94de0db68d..86dcf073680 100644 --- a/homeassistant/components/hardkernel/hardware.py +++ b/homeassistant/components/hardkernel/hardware.py @@ -1,4 +1,5 @@ """The Hardkernel hardware platform.""" + from __future__ import annotations from homeassistant.components.hardware.models import BoardInfo, HardwareInfo diff --git a/homeassistant/components/hardware/__init__.py b/homeassistant/components/hardware/__init__.py index 2e00771199c..9de281b1e50 100644 --- a/homeassistant/components/hardware/__init__.py +++ b/homeassistant/components/hardware/__init__.py @@ -1,4 +1,5 @@ """The Hardware integration.""" + from __future__ import annotations from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/hardware/hardware.py b/homeassistant/components/hardware/hardware.py index d44a232c232..f2de9182b57 100644 --- a/homeassistant/components/hardware/hardware.py +++ b/homeassistant/components/hardware/hardware.py @@ -1,4 +1,5 @@ """The Hardware integration.""" + from __future__ import annotations from homeassistant.core import HomeAssistant, callback @@ -15,7 +16,9 @@ async def async_process_hardware_platforms(hass: HomeAssistant) -> None: """Start processing hardware platforms.""" hass.data[DOMAIN]["hardware_platform"] = {} - await async_process_integration_platforms(hass, DOMAIN, _register_hardware_platform) + await async_process_integration_platforms( + hass, DOMAIN, _register_hardware_platform, wait_for_platforms=True + ) @callback diff --git a/homeassistant/components/hardware/manifest.json b/homeassistant/components/hardware/manifest.json index 8056a4cca4f..f2772e609db 100644 --- a/homeassistant/components/hardware/manifest.json +++ b/homeassistant/components/hardware/manifest.json @@ -4,7 +4,6 @@ "codeowners": ["@home-assistant/core"], "config_flow": false, "documentation": "https://www.home-assistant.io/integrations/hardware", - "import_executor": true, "integration_type": "system", "quality_scale": "internal", "requirements": ["psutil-home-assistant==0.0.1"] diff --git a/homeassistant/components/hardware/models.py b/homeassistant/components/hardware/models.py index 6b852291323..6f25d6669cf 100644 --- a/homeassistant/components/hardware/models.py +++ b/homeassistant/components/hardware/models.py @@ -1,4 +1,5 @@ """Models for Hardware.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/homeassistant/components/hardware/websocket_api.py b/homeassistant/components/hardware/websocket_api.py index d4e4f2fed5c..694c6e782e1 100644 --- a/homeassistant/components/hardware/websocket_api.py +++ b/homeassistant/components/hardware/websocket_api.py @@ -1,4 +1,5 @@ """The Hardware websocket API.""" + from __future__ import annotations import contextlib diff --git a/homeassistant/components/harman_kardon_avr/media_player.py b/homeassistant/components/harman_kardon_avr/media_player.py index 17b3d0c5717..815a8f52b42 100644 --- a/homeassistant/components/harman_kardon_avr/media_player.py +++ b/homeassistant/components/harman_kardon_avr/media_player.py @@ -1,4 +1,5 @@ """Support for interface with an Harman/Kardon or JBL AVR.""" + from __future__ import annotations import hkavr diff --git a/homeassistant/components/harmony/__init__.py b/homeassistant/components/harmony/__init__.py index 327dbad343b..a1c513a4654 100644 --- a/homeassistant/components/harmony/__init__.py +++ b/homeassistant/components/harmony/__init__.py @@ -1,4 +1,5 @@ """The Logitech Harmony Hub integration.""" + import logging from homeassistant.components.remote import ATTR_ACTIVITY, ATTR_DELAY_SECS diff --git a/homeassistant/components/harmony/config_flow.py b/homeassistant/components/harmony/config_flow.py index ad041e75f1a..b579e7659f4 100644 --- a/homeassistant/components/harmony/config_flow.py +++ b/homeassistant/components/harmony/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Logitech Harmony Hub integration.""" + from __future__ import annotations import asyncio @@ -10,16 +11,21 @@ from aioharmony.hubconnector_websocket import HubConnector import aiohttp import voluptuous as vol -from homeassistant import config_entries, exceptions from homeassistant.components import ssdp from homeassistant.components.remote import ( ATTR_ACTIVITY, ATTR_DELAY_SECS, DEFAULT_DELAY_SECS, ) +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.const import CONF_HOST, CONF_NAME from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult +from homeassistant.exceptions import HomeAssistantError from .const import DOMAIN, HARMONY_DATA, PREVIOUS_ACTIVE_ACTIVITY, UNIQUE_ID from .util import ( @@ -51,7 +57,7 @@ async def validate_input(data: dict[str, Any]) -> dict[str, Any]: } -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class HarmonyConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Logitech Harmony Hub.""" VERSION = 1 @@ -62,7 +68,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors: dict[str, str] = {} if user_input is not None: @@ -86,7 +92,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="user", data_schema=DATA_SCHEMA, errors=errors ) - async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: + async def async_step_ssdp( + self, discovery_info: ssdp.SsdpServiceInfo + ) -> ConfigFlowResult: """Handle a discovered Harmony device.""" _LOGGER.debug("SSDP discovery_info: %s", discovery_info) @@ -120,7 +128,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_link( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Attempt to link with the Harmony.""" errors: dict[str, str] = {} @@ -144,14 +152,14 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback def async_get_options_flow( - config_entry: config_entries.ConfigEntry, + config_entry: ConfigEntry, ) -> OptionsFlowHandler: """Get the options flow for this handler.""" return OptionsFlowHandler(config_entry) async def _async_create_entry_from_valid_input( self, validated: dict[str, Any], user_input: dict[str, Any] - ) -> FlowResult: + ) -> ConfigFlowResult: """Single path to create the config entry from validated input.""" data = { @@ -174,16 +182,16 @@ def _options_from_user_input(user_input: dict[str, Any]) -> dict[str, Any]: return options -class OptionsFlowHandler(config_entries.OptionsFlow): +class OptionsFlowHandler(OptionsFlow): """Handle a option flow for Harmony.""" - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + 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: + ) -> ConfigFlowResult: """Handle options flow.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) @@ -209,5 +217,5 @@ class OptionsFlowHandler(config_entries.OptionsFlow): return self.async_show_form(step_id="init", data_schema=data_schema) -class CannotConnect(exceptions.HomeAssistantError): +class CannotConnect(HomeAssistantError): """Error to indicate we cannot connect.""" diff --git a/homeassistant/components/harmony/const.py b/homeassistant/components/harmony/const.py index 8df5b3d578c..69ef2cb66c9 100644 --- a/homeassistant/components/harmony/const.py +++ b/homeassistant/components/harmony/const.py @@ -1,4 +1,5 @@ """Constants for the Harmony component.""" + from homeassistant.const import Platform DOMAIN = "harmony" diff --git a/homeassistant/components/harmony/data.py b/homeassistant/components/harmony/data.py index f7eb96d6a8f..992eaf52326 100644 --- a/homeassistant/components/harmony/data.py +++ b/homeassistant/components/harmony/data.py @@ -1,4 +1,5 @@ """Harmony data object which contains the Harmony Client.""" + from __future__ import annotations from collections.abc import Iterable @@ -28,7 +29,7 @@ class HarmonyData(HarmonySubscriberMixin): ) -> None: """Initialize a data object.""" super().__init__(hass) - self._name = name + self.name = name self._unique_id = unique_id self._available = False self._address = address @@ -59,11 +60,6 @@ class HarmonyData(HarmonySubscriberMixin): return devices - @property - def name(self) -> str: - """Return the Harmony device's name.""" - return self._name - @property def unique_id(self): """Return the Harmony device's unique_id.""" @@ -104,7 +100,7 @@ class HarmonyData(HarmonySubscriberMixin): async def connect(self) -> None: """Connect to the Harmony Hub.""" - _LOGGER.debug("%s: Connecting", self._name) + _LOGGER.debug("%s: Connecting", self.name) callbacks = { "config_updated": self._config_updated, @@ -123,27 +119,27 @@ class HarmonyData(HarmonySubscriberMixin): except (TimeoutError, aioexc.TimeOut) as err: await self._client.close() raise ConfigEntryNotReady( - f"{self._name}: Connection timed-out to {self._address}:8088" + f"{self.name}: Connection timed-out to {self._address}:8088" ) from err except (ValueError, AttributeError) as err: await self._client.close() raise ConfigEntryNotReady( - f"{self._name}: Error {err} while connected HUB at:" + f"{self.name}: Error {err} while connected HUB at:" f" {self._address}:8088" ) from err if not connected: await self._client.close() raise ConfigEntryNotReady( - f"{self._name}: Unable to connect to HUB at: {self._address}:8088" + f"{self.name}: Unable to connect to HUB at: {self._address}:8088" ) async def shutdown(self) -> None: """Close connection on shutdown.""" - _LOGGER.debug("%s: Closing Harmony Hub", self._name) + _LOGGER.debug("%s: Closing Harmony Hub", self.name) try: await self._client.close() except aioexc.TimeOut: - _LOGGER.warning("%s: Disconnect timed-out", self._name) + _LOGGER.warning("%s: Disconnect timed-out", self.name) async def async_start_activity(self, activity: str) -> None: """Start an activity from the Harmony device.""" diff --git a/homeassistant/components/harmony/entity.py b/homeassistant/components/harmony/entity.py index b1b1599a16c..99b5744e0ed 100644 --- a/homeassistant/components/harmony/entity.py +++ b/homeassistant/components/harmony/entity.py @@ -1,4 +1,5 @@ """Base class Harmony entities.""" + from __future__ import annotations from collections.abc import Callable @@ -18,11 +19,12 @@ TIME_MARK_DISCONNECTED = 10 class HarmonyEntity(Entity): """Base entity for Harmony with connection state handling.""" + _attr_has_entity_name = True + def __init__(self, data: HarmonyData) -> None: """Initialize the Harmony base entity.""" super().__init__() self._unsub_mark_disconnected: Callable[[], None] | None = None - self._name = data.name self._data = data self._attr_should_poll = False @@ -33,14 +35,14 @@ class HarmonyEntity(Entity): async def async_got_connected(self, _: str | None = None) -> None: """Notification that we're connected to the HUB.""" - _LOGGER.debug("%s: connected to the HUB", self._name) + _LOGGER.debug("%s: connected to the HUB", self._data.name) self.async_write_ha_state() self._clear_disconnection_delay() async def async_got_disconnected(self, _: str | None = None) -> None: """Notification that we're disconnected from the HUB.""" - _LOGGER.debug("%s: disconnected from the HUB", self._name) + _LOGGER.debug("%s: disconnected from the HUB", self._data.name) # We're going to wait for 10 seconds before announcing we're # unavailable, this to allow a reconnection to happen. self._unsub_mark_disconnected = async_call_later( diff --git a/homeassistant/components/harmony/remote.py b/homeassistant/components/harmony/remote.py index 863c3fe5c56..c6b2e9be718 100644 --- a/homeassistant/components/harmony/remote.py +++ b/homeassistant/components/harmony/remote.py @@ -1,4 +1,5 @@ """Support for Harmony Hub devices.""" + from __future__ import annotations from collections.abc import Iterable @@ -19,7 +20,7 @@ from homeassistant.components.remote import ( RemoteEntityFeature, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HassJob, HomeAssistant, callback from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -86,6 +87,7 @@ class HarmonyRemote(HarmonyEntity, RemoteEntity, RestoreEntity): """Remote representation used to control a Harmony device.""" _attr_supported_features = RemoteEntityFeature.ACTIVITY + _attr_name = None def __init__( self, data: HarmonyData, activity: str | None, delay_secs: float, out_path: str @@ -102,7 +104,6 @@ class HarmonyRemote(HarmonyEntity, RemoteEntity, RestoreEntity): self._config_path = out_path self._attr_unique_id = data.unique_id self._attr_device_info = self._data.device_info(DOMAIN) - self._attr_name = data.name async def _async_update_options(self, data: dict[str, Any]) -> None: """Change options when the options flow does.""" @@ -116,11 +117,11 @@ class HarmonyRemote(HarmonyEntity, RemoteEntity, RestoreEntity): self.async_on_remove( self._data.async_subscribe( HarmonyCallback( - connected=self.async_got_connected, - disconnected=self.async_got_disconnected, - config_updated=self.async_new_config, - activity_starting=self.async_new_activity, - activity_started=self.async_new_activity_finished, + connected=HassJob(self.async_got_connected), + disconnected=HassJob(self.async_got_disconnected), + config_updated=HassJob(self.async_new_config), + activity_starting=HassJob(self.async_new_activity), + activity_started=HassJob(self.async_new_activity_finished), ) ) ) @@ -135,7 +136,7 @@ class HarmonyRemote(HarmonyEntity, RemoteEntity, RestoreEntity): """Complete the initialization.""" await super().async_added_to_hass() - _LOGGER.debug("%s: Harmony Hub added", self.name) + _LOGGER.debug("%s: Harmony Hub added", self._data.name) self.async_on_remove(self._clear_disconnection_delay) self._setup_callbacks() @@ -192,7 +193,7 @@ class HarmonyRemote(HarmonyEntity, RemoteEntity, RestoreEntity): def async_new_activity(self, activity_info: tuple) -> None: """Call for updating the current activity.""" activity_id, activity_name = activity_info - _LOGGER.debug("%s: activity reported as: %s", self.name, activity_name) + _LOGGER.debug("%s: activity reported as: %s", self._data.name, activity_name) self._current_activity = activity_name if self._is_initial_update: self._is_initial_update = False @@ -208,13 +209,13 @@ class HarmonyRemote(HarmonyEntity, RemoteEntity, RestoreEntity): async def async_new_config(self, _: dict | None = None) -> None: """Call for updating the current activity.""" - _LOGGER.debug("%s: configuration has been updated", self.name) + _LOGGER.debug("%s: configuration has been updated", self._data.name) self.async_new_activity(self._data.current_activity) await self.hass.async_add_executor_job(self.write_config_file) async def async_turn_on(self, **kwargs: Any) -> None: """Start an activity from the Harmony device.""" - _LOGGER.debug("%s: Turn On", self.name) + _LOGGER.debug("%s: Turn On", self._data.name) activity = kwargs.get(ATTR_ACTIVITY, self.default_activity) @@ -227,7 +228,9 @@ class HarmonyRemote(HarmonyEntity, RemoteEntity, RestoreEntity): if activity: await self._data.async_start_activity(activity) else: - _LOGGER.error("%s: No activity specified with turn_on service", self.name) + _LOGGER.error( + "%s: No activity specified with turn_on service", self._data.name + ) async def async_turn_off(self, **kwargs: Any) -> None: """Start the PowerOff activity.""" @@ -235,9 +238,9 @@ class HarmonyRemote(HarmonyEntity, RemoteEntity, RestoreEntity): async def async_send_command(self, command: Iterable[str], **kwargs: Any) -> None: """Send a list of commands to one device.""" - _LOGGER.debug("%s: Send Command", self.name) + _LOGGER.debug("%s: Send Command", self._data.name) if (device := kwargs.get(ATTR_DEVICE)) is None: - _LOGGER.error("%s: Missing required argument: device", self.name) + _LOGGER.error("%s: Missing required argument: device", self._data.name) return num_repeats = kwargs[ATTR_NUM_REPEATS] @@ -262,10 +265,12 @@ class HarmonyRemote(HarmonyEntity, RemoteEntity, RestoreEntity): This is a handy way for users to figure out the available commands for automations. """ _LOGGER.debug( - "%s: Writing hub configuration to file: %s", self.name, self._config_path + "%s: Writing hub configuration to file: %s", + self._data.name, + self._config_path, ) if (json_config := self._data.json_config) is None: - _LOGGER.warning("%s: No configuration received from hub", self.name) + _LOGGER.warning("%s: No configuration received from hub", self._data.name) return try: @@ -274,7 +279,7 @@ class HarmonyRemote(HarmonyEntity, RemoteEntity, RestoreEntity): except OSError as exc: _LOGGER.error( "%s: Unable to write HUB configuration to %s: %s", - self.name, + self._data.name, self._config_path, exc, ) diff --git a/homeassistant/components/harmony/select.py b/homeassistant/components/harmony/select.py index f08030c0152..0bb8f462419 100644 --- a/homeassistant/components/harmony/select.py +++ b/homeassistant/components/harmony/select.py @@ -1,12 +1,12 @@ """Support for Harmony Hub select activities.""" + from __future__ import annotations import logging from homeassistant.components.select import SelectEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_NAME -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HassJob, HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ACTIVITY_POWER_OFF, DOMAIN, HARMONY_DATA @@ -24,10 +24,7 @@ async def async_setup_entry( ) -> None: """Set up harmony activities select.""" data: HarmonyData = hass.data[DOMAIN][entry.entry_id][HARMONY_DATA] - _LOGGER.debug("creating select for %s hub activities", entry.data[CONF_NAME]) - async_add_entities( - [HarmonyActivitySelect(f"{entry.data[CONF_NAME]} Activities", data)] - ) + async_add_entities([HarmonyActivitySelect(data)]) class HarmonyActivitySelect(HarmonyEntity, SelectEntity): @@ -35,18 +32,17 @@ class HarmonyActivitySelect(HarmonyEntity, SelectEntity): _attr_translation_key = "activities" - def __init__(self, name: str, data: HarmonyData) -> None: + def __init__(self, data: HarmonyData) -> None: """Initialize HarmonyActivitySelect class.""" super().__init__(data=data) self._data = data self._attr_unique_id = self._data.unique_id self._attr_device_info = self._data.device_info(DOMAIN) - self._attr_name = name @property def options(self) -> list[str]: """Return a set of selectable options.""" - return [TRANSLATABLE_POWER_OFF] + sorted(self._data.activity_names) + return [TRANSLATABLE_POWER_OFF, *sorted(self._data.activity_names)] @property def current_option(self) -> str | None: @@ -65,13 +61,14 @@ class HarmonyActivitySelect(HarmonyEntity, SelectEntity): async def async_added_to_hass(self) -> None: """Call when entity is added to hass.""" + activity_update_job = HassJob(self._async_activity_update) self.async_on_remove( self._data.async_subscribe( HarmonyCallback( - connected=self.async_got_connected, - disconnected=self.async_got_disconnected, - activity_starting=self._async_activity_update, - activity_started=self._async_activity_update, + connected=HassJob(self.async_got_connected), + disconnected=HassJob(self.async_got_disconnected), + activity_starting=activity_update_job, + activity_started=activity_update_job, config_updated=None, ) ) diff --git a/homeassistant/components/harmony/strings.json b/homeassistant/components/harmony/strings.json index f6862ca3c83..444097395c9 100644 --- a/homeassistant/components/harmony/strings.json +++ b/homeassistant/components/harmony/strings.json @@ -39,6 +39,7 @@ "entity": { "select": { "activities": { + "name": "Activities", "state": { "power_off": "Power Off" } diff --git a/homeassistant/components/harmony/subscriber.py b/homeassistant/components/harmony/subscriber.py index 8a47e437e17..e923df82843 100644 --- a/homeassistant/components/harmony/subscriber.py +++ b/homeassistant/components/harmony/subscriber.py @@ -1,17 +1,17 @@ """Mixin class for handling harmony callback subscriptions.""" + from __future__ import annotations import asyncio -from collections.abc import Callable import logging from typing import Any, NamedTuple -from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback +from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback _LOGGER = logging.getLogger(__name__) -NoParamCallback = Callable[[], Any] | None -ActivityCallback = Callable[[tuple], Any] | None +NoParamCallback = HassJob[[], Any] | None +ActivityCallback = HassJob[[tuple], Any] | None class HarmonyCallback(NamedTuple): @@ -88,9 +88,8 @@ class HarmonySubscriberMixin: self, callback_func_name: str, argument: tuple | None = None ) -> None: for subscription in self._subscriptions: - current_callback = getattr(subscription, callback_func_name) - if current_callback: + if current_callback_job := getattr(subscription, callback_func_name): if argument: - self._hass.async_run_job(current_callback, argument) + self._hass.async_run_hass_job(current_callback_job, argument) else: - self._hass.async_run_job(current_callback) + self._hass.async_run_hass_job(current_callback_job) diff --git a/homeassistant/components/harmony/switch.py b/homeassistant/components/harmony/switch.py index c5bba39eb95..0cb07e5cb1e 100644 --- a/homeassistant/components/harmony/switch.py +++ b/homeassistant/components/harmony/switch.py @@ -1,4 +1,5 @@ """Support for Harmony Hub activities.""" + import logging from typing import Any, cast @@ -6,8 +7,7 @@ from homeassistant.components.automation import automations_with_entity from homeassistant.components.script import scripts_with_entity from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN, SwitchEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_NAME -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HassJob, HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue @@ -24,28 +24,22 @@ async def async_setup_entry( ) -> None: """Set up harmony activity switches.""" data: HarmonyData = hass.data[DOMAIN][entry.entry_id][HARMONY_DATA] - activities = data.activities - switches = [] - for activity in activities: - _LOGGER.debug("creating switch for activity: %s", activity) - name = f"{entry.data[CONF_NAME]} {activity['label']}" - switches.append(HarmonyActivitySwitch(name, activity, data)) - - async_add_entities(switches, True) + async_add_entities( + (HarmonyActivitySwitch(activity, data) for activity in data.activities), True + ) class HarmonyActivitySwitch(HarmonyEntity, SwitchEntity): """Switch representation of a Harmony activity.""" - def __init__(self, name: str, activity: dict, data: HarmonyData) -> None: + def __init__(self, activity: dict, data: HarmonyData) -> None: """Initialize HarmonyActivitySwitch class.""" super().__init__(data=data) - self._activity_name = activity["label"] + self._activity_name = self._attr_name = activity["label"] self._activity_id = activity["id"] self._attr_entity_registry_enabled_default = False self._attr_unique_id = f"activity_{self._activity_id}" - self._attr_name = name self._attr_device_info = self._data.device_info(DOMAIN) @property @@ -82,13 +76,14 @@ class HarmonyActivitySwitch(HarmonyEntity, SwitchEntity): async def async_added_to_hass(self) -> None: """Call when entity is added to hass.""" + activity_update_job = HassJob(self._async_activity_update) self.async_on_remove( self._data.async_subscribe( HarmonyCallback( - connected=self.async_got_connected, - disconnected=self.async_got_disconnected, - activity_starting=self._async_activity_update, - activity_started=self._async_activity_update, + connected=HassJob(self.async_got_connected), + disconnected=HassJob(self.async_got_disconnected), + activity_starting=activity_update_job, + activity_started=activity_update_job, config_updated=None, ) ) diff --git a/homeassistant/components/harmony/util.py b/homeassistant/components/harmony/util.py index 0bfee32b414..13ca67c9e76 100644 --- a/homeassistant/components/harmony/util.py +++ b/homeassistant/components/harmony/util.py @@ -1,4 +1,5 @@ """The Logitech Harmony Hub integration utils.""" + import aioharmony.exceptions as harmony_exceptions from aioharmony.harmonyapi import HarmonyAPI diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index e367a935ace..90b155aff15 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -1,10 +1,10 @@ """Support for Hass.io.""" + from __future__ import annotations import asyncio -from collections import defaultdict from contextlib import suppress -from datetime import datetime, timedelta +from datetime import datetime import logging import os import re @@ -15,16 +15,14 @@ import voluptuous as vol from homeassistant.auth.const import GROUP_ID_ADMIN from homeassistant.components import panel_custom from homeassistant.components.homeassistant import async_set_stop_handler -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import SOURCE_SYSTEM, ConfigEntry from homeassistant.const import ( - ATTR_MANUFACTURER, ATTR_NAME, EVENT_CORE_CONFIG_UPDATE, HASSIO_USER_NAME, Platform, ) from homeassistant.core import ( - CALLBACK_TYPE, Event, HassJob, HomeAssistant, @@ -33,26 +31,37 @@ from homeassistant.core import ( callback, ) from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import config_validation as cv, device_registry as dr +from homeassistant.helpers import ( + config_validation as cv, + device_registry as dr, + discovery_flow, +) from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.debounce import Debouncer -from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.event import async_call_later from homeassistant.helpers.storage import Store from homeassistant.helpers.typing import ConfigType -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.loader import bind_hass from homeassistant.util.async_ import create_eager_task from homeassistant.util.dt import now +# config_flow, diagnostics, system_health, and entity platforms are imported to +# ensure other dependencies that wait for hassio are not waiting +# for hassio to import its platforms +from . import ( # noqa: F401 + binary_sensor, + config_flow, + diagnostics, + sensor, + system_health, + update, +) from .addon_manager import AddonError, AddonInfo, AddonManager, AddonState # noqa: F401 from .addon_panel import async_setup_addon_panel from .auth import async_setup_auth_view from .const import ( + ADDONS_COORDINATOR, ATTR_ADDON, ATTR_ADDONS, - ATTR_AUTO_UPDATE, - ATTR_CHANGELOG, ATTR_COMPRESSED, ATTR_FOLDERS, ATTR_HOMEASSISTANT, @@ -60,26 +69,31 @@ from .const import ( ATTR_INPUT, ATTR_LOCATION, ATTR_PASSWORD, - ATTR_REPOSITORY, ATTR_SLUG, - ATTR_STARTED, - ATTR_STATE, - ATTR_URL, - ATTR_VERSION, - CONTAINER_CHANGELOG, - CONTAINER_INFO, - CONTAINER_STATS, - CORE_CONTAINER, - DATA_KEY_ADDONS, - DATA_KEY_CORE, - DATA_KEY_HOST, - DATA_KEY_OS, - DATA_KEY_SUPERVISOR, + DATA_CORE_INFO, + DATA_HOST_INFO, + DATA_INFO, DATA_KEY_SUPERVISOR_ISSUES, + DATA_OS_INFO, + DATA_STORE, + DATA_SUPERVISOR_INFO, DOMAIN, - REQUEST_REFRESH_DELAY, - SUPERVISOR_CONTAINER, - SupervisorEntityModel, + HASSIO_UPDATE_INTERVAL, +) +from .data import ( + HassioDataUpdateCoordinator, + get_addons_changelogs, # noqa: F401 + get_addons_info, + get_addons_stats, # noqa: F401 + get_core_info, # noqa: F401 + get_core_stats, # noqa: F401 + get_host_info, # noqa: F401 + get_info, # noqa: F401 + get_issues_info, # noqa: F401 + get_os_info, + get_store, # noqa: F401 + get_supervisor_info, # noqa: F401 + get_supervisor_stats, # noqa: F401 ) from .discovery import HassioServiceInfo, async_setup_discovery_view # noqa: F401 from .handler import ( # noqa: F401 @@ -116,6 +130,9 @@ _LOGGER = logging.getLogger(__name__) STORAGE_KEY = DOMAIN STORAGE_VERSION = 1 +# If new platforms are added, be sure to import them above +# so we do not make other components that depend on hassio +# wait for the import of the platforms PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR, Platform.UPDATE] CONF_FRONTEND_REPO = "development_repo" @@ -125,22 +142,6 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) - -DATA_CORE_INFO = "hassio_core_info" -DATA_CORE_STATS = "hassio_core_stats" -DATA_HOST_INFO = "hassio_host_info" -DATA_STORE = "hassio_store" -DATA_INFO = "hassio_info" -DATA_OS_INFO = "hassio_os_info" -DATA_SUPERVISOR_INFO = "hassio_supervisor_info" -DATA_SUPERVISOR_STATS = "hassio_supervisor_stats" -DATA_ADDONS_CHANGELOGS = "hassio_addons_changelogs" -DATA_ADDONS_INFO = "hassio_addons_info" -DATA_ADDONS_STATS = "hassio_addons_stats" -HASSIO_UPDATE_INTERVAL = timedelta(minutes=5) - -ADDONS_COORDINATOR = "hassio_addons_coordinator" - SERVICE_ADDON_START = "addon_start" SERVICE_ADDON_STOP = "addon_stop" SERVICE_ADDON_RESTART = "addon_restart" @@ -283,126 +284,6 @@ def hostname_from_addon_slug(addon_slug: str) -> str: return addon_slug.replace("_", "-") -@callback -@bind_hass -def get_info(hass: HomeAssistant) -> dict[str, Any] | None: - """Return generic information from Supervisor. - - Async friendly. - """ - return hass.data.get(DATA_INFO) - - -@callback -@bind_hass -def get_host_info(hass: HomeAssistant) -> dict[str, Any] | None: - """Return generic host information. - - Async friendly. - """ - return hass.data.get(DATA_HOST_INFO) - - -@callback -@bind_hass -def get_store(hass: HomeAssistant) -> dict[str, Any] | None: - """Return store information. - - Async friendly. - """ - return hass.data.get(DATA_STORE) - - -@callback -@bind_hass -def get_supervisor_info(hass: HomeAssistant) -> dict[str, Any] | None: - """Return Supervisor information. - - Async friendly. - """ - return hass.data.get(DATA_SUPERVISOR_INFO) - - -@callback -@bind_hass -def get_addons_info(hass: HomeAssistant) -> dict[str, dict[str, Any]] | None: - """Return Addons info. - - Async friendly. - """ - return hass.data.get(DATA_ADDONS_INFO) - - -@callback -@bind_hass -def get_addons_stats(hass: HomeAssistant) -> dict[str, Any]: - """Return Addons stats. - - Async friendly. - """ - return hass.data.get(DATA_ADDONS_STATS) or {} - - -@callback -@bind_hass -def get_core_stats(hass: HomeAssistant) -> dict[str, Any]: - """Return core stats. - - Async friendly. - """ - return hass.data.get(DATA_CORE_STATS) or {} - - -@callback -@bind_hass -def get_supervisor_stats(hass: HomeAssistant) -> dict[str, Any]: - """Return supervisor stats. - - Async friendly. - """ - return hass.data.get(DATA_SUPERVISOR_STATS) or {} - - -@callback -@bind_hass -def get_addons_changelogs(hass: HomeAssistant): - """Return Addons changelogs. - - Async friendly. - """ - return hass.data.get(DATA_ADDONS_CHANGELOGS) - - -@callback -@bind_hass -def get_os_info(hass: HomeAssistant) -> dict[str, Any] | None: - """Return OS information. - - Async friendly. - """ - return hass.data.get(DATA_OS_INFO) - - -@callback -@bind_hass -def get_core_info(hass: HomeAssistant) -> dict[str, Any] | None: - """Return Home Assistant Core information from Supervisor. - - Async friendly. - """ - return hass.data.get(DATA_CORE_INFO) - - -@callback -@bind_hass -def get_issues_info(hass: HomeAssistant) -> SupervisorIssues | None: - """Return Supervisor issues info. - - Async friendly. - """ - return hass.data.get(DATA_KEY_SUPERVISOR_ISSUES) - - @callback @bind_hass def is_hassio(hass: HomeAssistant) -> bool: @@ -503,9 +384,12 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: last_timezone = new_timezone await hassio.update_hass_timezone(new_timezone) - hass.bus.async_listen(EVENT_CORE_CONFIG_UPDATE, push_config) + hass.bus.async_listen(EVENT_CORE_CONFIG_UPDATE, push_config, run_immediately=True) push_config_task = hass.async_create_task(push_config(None), eager_start=True) + # Start listening for problems with supervisor and making issues + hass.data[DATA_KEY_SUPERVISOR_ISSUES] = issues = SupervisorIssues(hass, hassio) + issues_task = hass.async_create_task(issues.setup(), eager_start=True) async def async_service_handler(service: ServiceCall) -> None: """Handle service calls for Hass.io.""" @@ -566,8 +450,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: ) # Fetch data - await update_info_data() - await push_config_task + update_info_task = hass.async_create_task(update_info_data(), eager_start=True) async def _async_stop(hass: HomeAssistant, restart: bool) -> None: """Stop or restart home assistant.""" @@ -590,7 +473,18 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: async_setup_ingress_view(hass, host) # Init add-on ingress panels - await async_setup_addon_panel(hass, hassio) + panels_task = hass.async_create_task( + async_setup_addon_panel(hass, hassio), eager_start=True + ) + + # Make sure to await the update_info task before + # _async_setup_hardware_integration is called + # so the hardware integration can be set up + # and does not fallback to calling later + await panels_task + await update_info_task + await push_config_task + await issues_task # Setup hardware integration for the detected board type @callback @@ -608,10 +502,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: return if (hw_integration := HARDWARE_INTEGRATIONS.get(board)) is None: return - hass.async_create_task( - hass.config_entries.flow.async_init( - hw_integration, context={"source": "system"} - ) + discovery_flow.async_create_flow( + hass, hw_integration, context={"source": SOURCE_SYSTEM}, data={} ) async_setup_hardware_integration_job = HassJob( @@ -619,16 +511,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: ) _async_setup_hardware_integration() - - hass.async_create_task( - hass.config_entries.flow.async_init(DOMAIN, context={"source": "system"}), - eager_start=True, + discovery_flow.async_create_flow( + hass, DOMAIN, context={"source": SOURCE_SYSTEM}, data={} ) - - # Start listening for problems with supervisor and making issues - hass.data[DATA_KEY_SUPERVISOR_ISSUES] = issues = SupervisorIssues(hass, hassio) - await issues.setup() - return True @@ -652,372 +537,3 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.pop(ADDONS_COORDINATOR, None) return unload_ok - - -@callback -def async_register_addons_in_dev_reg( - entry_id: str, dev_reg: dr.DeviceRegistry, addons: list[dict[str, Any]] -) -> None: - """Register addons in the device registry.""" - for addon in addons: - params = DeviceInfo( - identifiers={(DOMAIN, addon[ATTR_SLUG])}, - model=SupervisorEntityModel.ADDON, - sw_version=addon[ATTR_VERSION], - name=addon[ATTR_NAME], - entry_type=dr.DeviceEntryType.SERVICE, - configuration_url=f"homeassistant://hassio/addon/{addon[ATTR_SLUG]}", - ) - if manufacturer := addon.get(ATTR_REPOSITORY) or addon.get(ATTR_URL): - params[ATTR_MANUFACTURER] = manufacturer - dev_reg.async_get_or_create(config_entry_id=entry_id, **params) - - -@callback -def async_register_os_in_dev_reg( - entry_id: str, dev_reg: dr.DeviceRegistry, os_dict: dict[str, Any] -) -> None: - """Register OS in the device registry.""" - params = DeviceInfo( - identifiers={(DOMAIN, "OS")}, - manufacturer="Home Assistant", - model=SupervisorEntityModel.OS, - sw_version=os_dict[ATTR_VERSION], - name="Home Assistant Operating System", - entry_type=dr.DeviceEntryType.SERVICE, - ) - dev_reg.async_get_or_create(config_entry_id=entry_id, **params) - - -@callback -def async_register_host_in_dev_reg( - entry_id: str, - dev_reg: dr.DeviceRegistry, -) -> None: - """Register host in the device registry.""" - params = DeviceInfo( - identifiers={(DOMAIN, "host")}, - manufacturer="Home Assistant", - model=SupervisorEntityModel.HOST, - name="Home Assistant Host", - entry_type=dr.DeviceEntryType.SERVICE, - ) - dev_reg.async_get_or_create(config_entry_id=entry_id, **params) - - -@callback -def async_register_core_in_dev_reg( - entry_id: str, - dev_reg: dr.DeviceRegistry, - core_dict: dict[str, Any], -) -> None: - """Register OS in the device registry.""" - params = DeviceInfo( - identifiers={(DOMAIN, "core")}, - manufacturer="Home Assistant", - model=SupervisorEntityModel.CORE, - sw_version=core_dict[ATTR_VERSION], - name="Home Assistant Core", - entry_type=dr.DeviceEntryType.SERVICE, - ) - dev_reg.async_get_or_create(config_entry_id=entry_id, **params) - - -@callback -def async_register_supervisor_in_dev_reg( - entry_id: str, - dev_reg: dr.DeviceRegistry, - supervisor_dict: dict[str, Any], -) -> None: - """Register OS in the device registry.""" - params = DeviceInfo( - identifiers={(DOMAIN, "supervisor")}, - manufacturer="Home Assistant", - model=SupervisorEntityModel.SUPERVIOSR, - sw_version=supervisor_dict[ATTR_VERSION], - name="Home Assistant Supervisor", - entry_type=dr.DeviceEntryType.SERVICE, - ) - dev_reg.async_get_or_create(config_entry_id=entry_id, **params) - - -@callback -def async_remove_addons_from_dev_reg( - dev_reg: dr.DeviceRegistry, addons: set[str] -) -> None: - """Remove addons from the device registry.""" - for addon_slug in addons: - if dev := dev_reg.async_get_device(identifiers={(DOMAIN, addon_slug)}): - dev_reg.async_remove_device(dev.id) - - -class HassioDataUpdateCoordinator(DataUpdateCoordinator): # pylint: disable=hass-enforce-coordinator-module - """Class to retrieve Hass.io status.""" - - def __init__( - self, hass: HomeAssistant, config_entry: ConfigEntry, dev_reg: dr.DeviceRegistry - ) -> None: - """Initialize coordinator.""" - super().__init__( - hass, - _LOGGER, - name=DOMAIN, - update_interval=HASSIO_UPDATE_INTERVAL, - # We don't want an immediate refresh since we want to avoid - # fetching the container stats right away and avoid hammering - # the Supervisor API on startup - request_refresh_debouncer=Debouncer( - hass, _LOGGER, cooldown=REQUEST_REFRESH_DELAY, immediate=False - ), - ) - self.hassio: HassIO = hass.data[DOMAIN] - self.data = {} - self.entry_id = config_entry.entry_id - self.dev_reg = dev_reg - self.is_hass_os = (get_info(self.hass) or {}).get("hassos") is not None - self._container_updates: defaultdict[str, dict[str, set[str]]] = defaultdict( - lambda: defaultdict(set) - ) - - async def _async_update_data(self) -> dict[str, Any]: - """Update data via library.""" - is_first_update = not self.data - - try: - await self.force_data_refresh(is_first_update) - except HassioAPIError as err: - raise UpdateFailed(f"Error on Supervisor API: {err}") from err - - new_data: dict[str, Any] = {} - supervisor_info = get_supervisor_info(self.hass) or {} - addons_info = get_addons_info(self.hass) or {} - addons_stats = get_addons_stats(self.hass) - addons_changelogs = get_addons_changelogs(self.hass) - store_data = get_store(self.hass) or {} - - repositories = { - repo[ATTR_SLUG]: repo[ATTR_NAME] - for repo in store_data.get("repositories", []) - } - - new_data[DATA_KEY_ADDONS] = { - addon[ATTR_SLUG]: { - **addon, - **((addons_stats or {}).get(addon[ATTR_SLUG]) or {}), - ATTR_AUTO_UPDATE: (addons_info.get(addon[ATTR_SLUG]) or {}).get( - ATTR_AUTO_UPDATE, False - ), - ATTR_CHANGELOG: (addons_changelogs or {}).get(addon[ATTR_SLUG]), - ATTR_REPOSITORY: repositories.get( - addon.get(ATTR_REPOSITORY), addon.get(ATTR_REPOSITORY, "") - ), - } - for addon in supervisor_info.get("addons", []) - } - if self.is_hass_os: - new_data[DATA_KEY_OS] = get_os_info(self.hass) - - new_data[DATA_KEY_CORE] = { - **(get_core_info(self.hass) or {}), - **get_core_stats(self.hass), - } - new_data[DATA_KEY_SUPERVISOR] = { - **supervisor_info, - **get_supervisor_stats(self.hass), - } - new_data[DATA_KEY_HOST] = get_host_info(self.hass) or {} - - # If this is the initial refresh, register all addons and return the dict - if is_first_update: - async_register_addons_in_dev_reg( - self.entry_id, self.dev_reg, new_data[DATA_KEY_ADDONS].values() - ) - async_register_core_in_dev_reg( - self.entry_id, self.dev_reg, new_data[DATA_KEY_CORE] - ) - async_register_supervisor_in_dev_reg( - self.entry_id, self.dev_reg, new_data[DATA_KEY_SUPERVISOR] - ) - async_register_host_in_dev_reg(self.entry_id, self.dev_reg) - if self.is_hass_os: - async_register_os_in_dev_reg( - self.entry_id, self.dev_reg, new_data[DATA_KEY_OS] - ) - - # Remove add-ons that are no longer installed from device registry - supervisor_addon_devices = { - list(device.identifiers)[0][1] - for device in self.dev_reg.devices.values() - if self.entry_id in device.config_entries - and device.model == SupervisorEntityModel.ADDON - } - if stale_addons := supervisor_addon_devices - set(new_data[DATA_KEY_ADDONS]): - async_remove_addons_from_dev_reg(self.dev_reg, stale_addons) - - if not self.is_hass_os and ( - dev := self.dev_reg.async_get_device(identifiers={(DOMAIN, "OS")}) - ): - # Remove the OS device if it exists and the installation is not hassos - self.dev_reg.async_remove_device(dev.id) - - # If there are new add-ons, we should reload the config entry so we can - # create new devices and entities. We can return an empty dict because - # coordinator will be recreated. - if self.data and set(new_data[DATA_KEY_ADDONS]) - set( - self.data[DATA_KEY_ADDONS] - ): - self.hass.async_create_task( - self.hass.config_entries.async_reload(self.entry_id) - ) - return {} - - return new_data - - async def force_info_update_supervisor(self) -> None: - """Force update of the supervisor info.""" - self.hass.data[DATA_SUPERVISOR_INFO] = await self.hassio.get_supervisor_info() - await self.async_refresh() - - async def force_data_refresh(self, first_update: bool) -> None: - """Force update of the addon info.""" - container_updates = self._container_updates - - data = self.hass.data - hassio = self.hassio - updates = { - DATA_INFO: hassio.get_info(), - DATA_CORE_INFO: hassio.get_core_info(), - DATA_SUPERVISOR_INFO: hassio.get_supervisor_info(), - DATA_OS_INFO: hassio.get_os_info(), - } - if CONTAINER_STATS in container_updates[CORE_CONTAINER]: - updates[DATA_CORE_STATS] = hassio.get_core_stats() - if CONTAINER_STATS in container_updates[SUPERVISOR_CONTAINER]: - updates[DATA_SUPERVISOR_STATS] = hassio.get_supervisor_stats() - - results = await asyncio.gather(*updates.values()) - for key, result in zip(updates, results): - data[key] = result - - _addon_data = data[DATA_SUPERVISOR_INFO].get("addons", []) - all_addons: list[str] = [] - started_addons: list[str] = [] - for addon in _addon_data: - slug = addon[ATTR_SLUG] - all_addons.append(slug) - if addon[ATTR_STATE] == ATTR_STARTED: - started_addons.append(slug) - # - # Update add-on info if its the first update or - # there is at least one entity that needs the data. - # - # When entities are added they call async_enable_container_updates - # to enable updates for the endpoints they need via - # async_added_to_hass. This ensures that we only update - # the data for the endpoints that are needed to avoid unnecessary - # API calls since otherwise we would fetch stats for all containers - # and throw them away. - # - for data_key, update_func, enabled_key, wanted_addons, needs_first_update in ( - ( - DATA_ADDONS_STATS, - self._update_addon_stats, - CONTAINER_STATS, - started_addons, - False, - ), - ( - DATA_ADDONS_CHANGELOGS, - self._update_addon_changelog, - CONTAINER_CHANGELOG, - all_addons, - True, - ), - ( - DATA_ADDONS_INFO, - self._update_addon_info, - CONTAINER_INFO, - all_addons, - True, - ), - ): - container_data: dict[str, Any] = data.setdefault(data_key, {}) - container_data.update( - dict( - await asyncio.gather( - *[ - update_func(slug) - for slug in wanted_addons - if (first_update and needs_first_update) - or enabled_key in container_updates[slug] - ] - ) - ) - ) - - async def _update_addon_stats(self, slug: str) -> tuple[str, dict[str, Any] | None]: - """Update single addon stats.""" - try: - stats = await self.hassio.get_addon_stats(slug) - return (slug, stats) - except HassioAPIError as err: - _LOGGER.warning("Could not fetch stats for %s: %s", slug, err) - return (slug, None) - - async def _update_addon_changelog(self, slug: str) -> tuple[str, str | None]: - """Return the changelog for an add-on.""" - try: - changelog = await self.hassio.get_addon_changelog(slug) - return (slug, changelog) - except HassioAPIError as err: - _LOGGER.warning("Could not fetch changelog for %s: %s", slug, err) - return (slug, None) - - async def _update_addon_info(self, slug: str) -> tuple[str, dict[str, Any] | None]: - """Return the info for an add-on.""" - try: - info = await self.hassio.get_addon_info(slug) - return (slug, info) - except HassioAPIError as err: - _LOGGER.warning("Could not fetch info for %s: %s", slug, err) - return (slug, None) - - @callback - def async_enable_container_updates( - self, slug: str, entity_id: str, types: set[str] - ) -> CALLBACK_TYPE: - """Enable updates for an add-on.""" - enabled_updates = self._container_updates[slug] - for key in types: - enabled_updates[key].add(entity_id) - - @callback - def _remove() -> None: - for key in types: - enabled_updates[key].remove(entity_id) - - return _remove - - async def _async_refresh( - self, - log_failures: bool = True, - raise_on_auth_failed: bool = False, - scheduled: bool = False, - raise_on_entry_error: bool = False, - ) -> None: - """Refresh data.""" - if not scheduled and not raise_on_auth_failed: - # Force refreshing updates for non-scheduled updates - # If `raise_on_auth_failed` is set, it means this is - # the first refresh and we do not want to delay - # startup or cause a timeout so we only refresh the - # updates if this is not a scheduled refresh and - # we are not doing the first refresh. - try: - await self.hassio.refresh_updates() - except HassioAPIError as err: - _LOGGER.warning("Error on Supervisor API: %s", err) - - await super()._async_refresh( - log_failures, raise_on_auth_failed, scheduled, raise_on_entry_error - ) diff --git a/homeassistant/components/hassio/addon_manager.py b/homeassistant/components/hassio/addon_manager.py index 7f9299fa2b1..fcdcd38f776 100644 --- a/homeassistant/components/hassio/addon_manager.py +++ b/homeassistant/components/hassio/addon_manager.py @@ -1,4 +1,5 @@ """Provide add-on management.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/hassio/addon_panel.py b/homeassistant/components/hassio/addon_panel.py index db53b2f90fc..f0ccecb22f1 100644 --- a/homeassistant/components/hassio/addon_panel.py +++ b/homeassistant/components/hassio/addon_panel.py @@ -1,4 +1,5 @@ """Implement the Ingress Panel feature for Hass.io Add-ons.""" + from http import HTTPStatus import logging from typing import Any diff --git a/homeassistant/components/hassio/auth.py b/homeassistant/components/hassio/auth.py index 1e20b3da8e5..6ca89ee24be 100644 --- a/homeassistant/components/hassio/auth.py +++ b/homeassistant/components/hassio/auth.py @@ -1,4 +1,5 @@ """Implement the auth feature from Hass.io for Add-ons.""" + from http import HTTPStatus from ipaddress import ip_address import logging @@ -10,7 +11,7 @@ import voluptuous as vol from homeassistant.auth.models import User from homeassistant.auth.providers import homeassistant as auth_ha -from homeassistant.components.http import KEY_HASS_USER, HomeAssistantView +from homeassistant.components.http import KEY_HASS, KEY_HASS_USER, HomeAssistantView from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv @@ -47,12 +48,12 @@ class HassIOBaseAuth(HomeAssistantView): hassio_ip ): _LOGGER.error("Invalid auth request from %s", request.remote) - raise HTTPUnauthorized() + raise HTTPUnauthorized # Check caller token if request[KEY_HASS_USER].id != self.user.id: _LOGGER.error("Invalid auth request from %s", request[KEY_HASS_USER].name) - raise HTTPUnauthorized() + raise HTTPUnauthorized class HassIOAuth(HassIOBaseAuth): @@ -74,14 +75,14 @@ class HassIOAuth(HassIOBaseAuth): async def post(self, request: web.Request, data: dict[str, str]) -> web.Response: """Handle auth requests.""" self._check_access(request) - provider = auth_ha.async_get_provider(request.app["hass"]) + provider = auth_ha.async_get_provider(request.app[KEY_HASS]) try: await provider.async_validate_login( data[ATTR_USERNAME], data[ATTR_PASSWORD] ) except auth_ha.InvalidAuth: - raise HTTPNotFound() from None + raise HTTPNotFound from None return web.Response(status=HTTPStatus.OK) @@ -104,13 +105,13 @@ class HassIOPasswordReset(HassIOBaseAuth): async def post(self, request: web.Request, data: dict[str, str]) -> web.Response: """Handle password reset requests.""" self._check_access(request) - provider = auth_ha.async_get_provider(request.app["hass"]) + provider = auth_ha.async_get_provider(request.app[KEY_HASS]) try: await provider.async_change_password( data[ATTR_USERNAME], data[ATTR_PASSWORD] ) except auth_ha.InvalidUser as err: - raise HTTPNotFound() from err + raise HTTPNotFound from err return web.Response(status=HTTPStatus.OK) diff --git a/homeassistant/components/hassio/binary_sensor.py b/homeassistant/components/hassio/binary_sensor.py index f57cfa472c4..9d6e2ba19da 100644 --- a/homeassistant/components/hassio/binary_sensor.py +++ b/homeassistant/components/hassio/binary_sensor.py @@ -1,4 +1,5 @@ """Binary sensor platform for Hass.io addons.""" + from __future__ import annotations from dataclasses import dataclass @@ -12,8 +13,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import ADDONS_COORDINATOR -from .const import ATTR_STARTED, ATTR_STATE, DATA_KEY_ADDONS +from .const import ADDONS_COORDINATOR, ATTR_STARTED, ATTR_STATE, DATA_KEY_ADDONS from .entity import HassioAddonEntity diff --git a/homeassistant/components/hassio/config_flow.py b/homeassistant/components/hassio/config_flow.py index ef09f07b4de..57be400acc7 100644 --- a/homeassistant/components/hassio/config_flow.py +++ b/homeassistant/components/hassio/config_flow.py @@ -1,25 +1,22 @@ """Config flow for Home Assistant Supervisor integration.""" + from __future__ import annotations -import logging from typing import Any -from homeassistant import config_entries -from homeassistant.data_entry_flow import FlowResult +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult -from . import DOMAIN - -_LOGGER = logging.getLogger(__name__) +from .const import DOMAIN -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class HassIoConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Home Assistant Supervisor.""" VERSION = 1 async def async_step_system( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" # We only need one Hass.io config entry await self.async_set_unique_id(DOMAIN) diff --git a/homeassistant/components/hassio/const.py b/homeassistant/components/hassio/const.py index b31c0f1cf15..0845a98f832 100644 --- a/homeassistant/components/hassio/const.py +++ b/homeassistant/components/hassio/const.py @@ -1,4 +1,6 @@ """Hass.io const variables.""" + +from datetime import timedelta from enum import StrEnum DOMAIN = "hassio" @@ -59,6 +61,22 @@ EVENT_ISSUE_REMOVED = "issue_removed" UPDATE_KEY_SUPERVISOR = "supervisor" +ADDONS_COORDINATOR = "hassio_addons_coordinator" + + +DATA_CORE_INFO = "hassio_core_info" +DATA_CORE_STATS = "hassio_core_stats" +DATA_HOST_INFO = "hassio_host_info" +DATA_STORE = "hassio_store" +DATA_INFO = "hassio_info" +DATA_OS_INFO = "hassio_os_info" +DATA_SUPERVISOR_INFO = "hassio_supervisor_info" +DATA_SUPERVISOR_STATS = "hassio_supervisor_stats" +DATA_ADDONS_CHANGELOGS = "hassio_addons_changelogs" +DATA_ADDONS_INFO = "hassio_addons_info" +DATA_ADDONS_STATS = "hassio_addons_stats" +HASSIO_UPDATE_INTERVAL = timedelta(minutes=5) + ATTR_AUTO_UPDATE = "auto_update" ATTR_VERSION = "version" ATTR_VERSION_LATEST = "version_latest" diff --git a/homeassistant/components/hassio/data.py b/homeassistant/components/hassio/data.py new file mode 100644 index 00000000000..eaa7c2431fe --- /dev/null +++ b/homeassistant/components/hassio/data.py @@ -0,0 +1,547 @@ +"""Data for Hass.io.""" + +from __future__ import annotations + +import asyncio +from collections import defaultdict +import logging +from typing import Any + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_MANUFACTURER, ATTR_NAME +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.debounce import Debouncer +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.loader import bind_hass + +from .const import ( + ATTR_AUTO_UPDATE, + ATTR_CHANGELOG, + ATTR_REPOSITORY, + ATTR_SLUG, + ATTR_STARTED, + ATTR_STATE, + ATTR_URL, + ATTR_VERSION, + CONTAINER_CHANGELOG, + CONTAINER_INFO, + CONTAINER_STATS, + CORE_CONTAINER, + DATA_ADDONS_CHANGELOGS, + DATA_ADDONS_INFO, + DATA_ADDONS_STATS, + DATA_CORE_INFO, + DATA_CORE_STATS, + DATA_HOST_INFO, + DATA_INFO, + DATA_KEY_ADDONS, + DATA_KEY_CORE, + DATA_KEY_HOST, + DATA_KEY_OS, + DATA_KEY_SUPERVISOR, + DATA_KEY_SUPERVISOR_ISSUES, + DATA_OS_INFO, + DATA_STORE, + DATA_SUPERVISOR_INFO, + DATA_SUPERVISOR_STATS, + DOMAIN, + HASSIO_UPDATE_INTERVAL, + REQUEST_REFRESH_DELAY, + SUPERVISOR_CONTAINER, + SupervisorEntityModel, +) +from .handler import HassIO, HassioAPIError +from .issues import SupervisorIssues + +_LOGGER = logging.getLogger(__name__) + + +@callback +@bind_hass +def get_info(hass: HomeAssistant) -> dict[str, Any] | None: + """Return generic information from Supervisor. + + Async friendly. + """ + return hass.data.get(DATA_INFO) + + +@callback +@bind_hass +def get_host_info(hass: HomeAssistant) -> dict[str, Any] | None: + """Return generic host information. + + Async friendly. + """ + return hass.data.get(DATA_HOST_INFO) + + +@callback +@bind_hass +def get_store(hass: HomeAssistant) -> dict[str, Any] | None: + """Return store information. + + Async friendly. + """ + return hass.data.get(DATA_STORE) + + +@callback +@bind_hass +def get_supervisor_info(hass: HomeAssistant) -> dict[str, Any] | None: + """Return Supervisor information. + + Async friendly. + """ + return hass.data.get(DATA_SUPERVISOR_INFO) + + +@callback +@bind_hass +def get_addons_info(hass: HomeAssistant) -> dict[str, dict[str, Any]] | None: + """Return Addons info. + + Async friendly. + """ + return hass.data.get(DATA_ADDONS_INFO) + + +@callback +@bind_hass +def get_addons_stats(hass: HomeAssistant) -> dict[str, Any]: + """Return Addons stats. + + Async friendly. + """ + return hass.data.get(DATA_ADDONS_STATS) or {} + + +@callback +@bind_hass +def get_core_stats(hass: HomeAssistant) -> dict[str, Any]: + """Return core stats. + + Async friendly. + """ + return hass.data.get(DATA_CORE_STATS) or {} + + +@callback +@bind_hass +def get_supervisor_stats(hass: HomeAssistant) -> dict[str, Any]: + """Return supervisor stats. + + Async friendly. + """ + return hass.data.get(DATA_SUPERVISOR_STATS) or {} + + +@callback +@bind_hass +def get_addons_changelogs(hass: HomeAssistant): + """Return Addons changelogs. + + Async friendly. + """ + return hass.data.get(DATA_ADDONS_CHANGELOGS) + + +@callback +@bind_hass +def get_os_info(hass: HomeAssistant) -> dict[str, Any] | None: + """Return OS information. + + Async friendly. + """ + return hass.data.get(DATA_OS_INFO) + + +@callback +@bind_hass +def get_core_info(hass: HomeAssistant) -> dict[str, Any] | None: + """Return Home Assistant Core information from Supervisor. + + Async friendly. + """ + return hass.data.get(DATA_CORE_INFO) + + +@callback +@bind_hass +def get_issues_info(hass: HomeAssistant) -> SupervisorIssues | None: + """Return Supervisor issues info. + + Async friendly. + """ + return hass.data.get(DATA_KEY_SUPERVISOR_ISSUES) + + +@callback +def async_register_addons_in_dev_reg( + entry_id: str, dev_reg: dr.DeviceRegistry, addons: list[dict[str, Any]] +) -> None: + """Register addons in the device registry.""" + for addon in addons: + params = DeviceInfo( + identifiers={(DOMAIN, addon[ATTR_SLUG])}, + model=SupervisorEntityModel.ADDON, + sw_version=addon[ATTR_VERSION], + name=addon[ATTR_NAME], + entry_type=dr.DeviceEntryType.SERVICE, + configuration_url=f"homeassistant://hassio/addon/{addon[ATTR_SLUG]}", + ) + if manufacturer := addon.get(ATTR_REPOSITORY) or addon.get(ATTR_URL): + params[ATTR_MANUFACTURER] = manufacturer + dev_reg.async_get_or_create(config_entry_id=entry_id, **params) + + +@callback +def async_register_os_in_dev_reg( + entry_id: str, dev_reg: dr.DeviceRegistry, os_dict: dict[str, Any] +) -> None: + """Register OS in the device registry.""" + params = DeviceInfo( + identifiers={(DOMAIN, "OS")}, + manufacturer="Home Assistant", + model=SupervisorEntityModel.OS, + sw_version=os_dict[ATTR_VERSION], + name="Home Assistant Operating System", + entry_type=dr.DeviceEntryType.SERVICE, + ) + dev_reg.async_get_or_create(config_entry_id=entry_id, **params) + + +@callback +def async_register_host_in_dev_reg( + entry_id: str, + dev_reg: dr.DeviceRegistry, +) -> None: + """Register host in the device registry.""" + params = DeviceInfo( + identifiers={(DOMAIN, "host")}, + manufacturer="Home Assistant", + model=SupervisorEntityModel.HOST, + name="Home Assistant Host", + entry_type=dr.DeviceEntryType.SERVICE, + ) + dev_reg.async_get_or_create(config_entry_id=entry_id, **params) + + +@callback +def async_register_core_in_dev_reg( + entry_id: str, + dev_reg: dr.DeviceRegistry, + core_dict: dict[str, Any], +) -> None: + """Register OS in the device registry.""" + params = DeviceInfo( + identifiers={(DOMAIN, "core")}, + manufacturer="Home Assistant", + model=SupervisorEntityModel.CORE, + sw_version=core_dict[ATTR_VERSION], + name="Home Assistant Core", + entry_type=dr.DeviceEntryType.SERVICE, + ) + dev_reg.async_get_or_create(config_entry_id=entry_id, **params) + + +@callback +def async_register_supervisor_in_dev_reg( + entry_id: str, + dev_reg: dr.DeviceRegistry, + supervisor_dict: dict[str, Any], +) -> None: + """Register OS in the device registry.""" + params = DeviceInfo( + identifiers={(DOMAIN, "supervisor")}, + manufacturer="Home Assistant", + model=SupervisorEntityModel.SUPERVIOSR, + sw_version=supervisor_dict[ATTR_VERSION], + name="Home Assistant Supervisor", + entry_type=dr.DeviceEntryType.SERVICE, + ) + dev_reg.async_get_or_create(config_entry_id=entry_id, **params) + + +@callback +def async_remove_addons_from_dev_reg( + dev_reg: dr.DeviceRegistry, addons: set[str] +) -> None: + """Remove addons from the device registry.""" + for addon_slug in addons: + if dev := dev_reg.async_get_device(identifiers={(DOMAIN, addon_slug)}): + dev_reg.async_remove_device(dev.id) + + +class HassioDataUpdateCoordinator(DataUpdateCoordinator): # pylint: disable=hass-enforce-coordinator-module + """Class to retrieve Hass.io status.""" + + def __init__( + self, hass: HomeAssistant, config_entry: ConfigEntry, dev_reg: dr.DeviceRegistry + ) -> None: + """Initialize coordinator.""" + super().__init__( + hass, + _LOGGER, + name=DOMAIN, + update_interval=HASSIO_UPDATE_INTERVAL, + # We don't want an immediate refresh since we want to avoid + # fetching the container stats right away and avoid hammering + # the Supervisor API on startup + request_refresh_debouncer=Debouncer( + hass, _LOGGER, cooldown=REQUEST_REFRESH_DELAY, immediate=False + ), + ) + self.hassio: HassIO = hass.data[DOMAIN] + self.data = {} + self.entry_id = config_entry.entry_id + self.dev_reg = dev_reg + self.is_hass_os = (get_info(self.hass) or {}).get("hassos") is not None + self._container_updates: defaultdict[str, dict[str, set[str]]] = defaultdict( + lambda: defaultdict(set) + ) + + async def _async_update_data(self) -> dict[str, Any]: + """Update data via library.""" + is_first_update = not self.data + + try: + await self.force_data_refresh(is_first_update) + except HassioAPIError as err: + raise UpdateFailed(f"Error on Supervisor API: {err}") from err + + new_data: dict[str, Any] = {} + supervisor_info = get_supervisor_info(self.hass) or {} + addons_info = get_addons_info(self.hass) or {} + addons_stats = get_addons_stats(self.hass) + addons_changelogs = get_addons_changelogs(self.hass) + store_data = get_store(self.hass) or {} + + repositories = { + repo[ATTR_SLUG]: repo[ATTR_NAME] + for repo in store_data.get("repositories", []) + } + + new_data[DATA_KEY_ADDONS] = { + addon[ATTR_SLUG]: { + **addon, + **((addons_stats or {}).get(addon[ATTR_SLUG]) or {}), + ATTR_AUTO_UPDATE: (addons_info.get(addon[ATTR_SLUG]) or {}).get( + ATTR_AUTO_UPDATE, False + ), + ATTR_CHANGELOG: (addons_changelogs or {}).get(addon[ATTR_SLUG]), + ATTR_REPOSITORY: repositories.get( + addon.get(ATTR_REPOSITORY), addon.get(ATTR_REPOSITORY, "") + ), + } + for addon in supervisor_info.get("addons", []) + } + if self.is_hass_os: + new_data[DATA_KEY_OS] = get_os_info(self.hass) + + new_data[DATA_KEY_CORE] = { + **(get_core_info(self.hass) or {}), + **get_core_stats(self.hass), + } + new_data[DATA_KEY_SUPERVISOR] = { + **supervisor_info, + **get_supervisor_stats(self.hass), + } + new_data[DATA_KEY_HOST] = get_host_info(self.hass) or {} + + # If this is the initial refresh, register all addons and return the dict + if is_first_update: + async_register_addons_in_dev_reg( + self.entry_id, self.dev_reg, new_data[DATA_KEY_ADDONS].values() + ) + async_register_core_in_dev_reg( + self.entry_id, self.dev_reg, new_data[DATA_KEY_CORE] + ) + async_register_supervisor_in_dev_reg( + self.entry_id, self.dev_reg, new_data[DATA_KEY_SUPERVISOR] + ) + async_register_host_in_dev_reg(self.entry_id, self.dev_reg) + if self.is_hass_os: + async_register_os_in_dev_reg( + self.entry_id, self.dev_reg, new_data[DATA_KEY_OS] + ) + + # Remove add-ons that are no longer installed from device registry + supervisor_addon_devices = { + list(device.identifiers)[0][1] + for device in self.dev_reg.devices.values() + if self.entry_id in device.config_entries + and device.model == SupervisorEntityModel.ADDON + } + if stale_addons := supervisor_addon_devices - set(new_data[DATA_KEY_ADDONS]): + async_remove_addons_from_dev_reg(self.dev_reg, stale_addons) + + if not self.is_hass_os and ( + dev := self.dev_reg.async_get_device(identifiers={(DOMAIN, "OS")}) + ): + # Remove the OS device if it exists and the installation is not hassos + self.dev_reg.async_remove_device(dev.id) + + # If there are new add-ons, we should reload the config entry so we can + # create new devices and entities. We can return an empty dict because + # coordinator will be recreated. + if self.data and set(new_data[DATA_KEY_ADDONS]) - set( + self.data[DATA_KEY_ADDONS] + ): + self.hass.async_create_task( + self.hass.config_entries.async_reload(self.entry_id) + ) + return {} + + return new_data + + async def force_info_update_supervisor(self) -> None: + """Force update of the supervisor info.""" + self.hass.data[DATA_SUPERVISOR_INFO] = await self.hassio.get_supervisor_info() + await self.async_refresh() + + async def force_data_refresh(self, first_update: bool) -> None: + """Force update of the addon info.""" + container_updates = self._container_updates + + data = self.hass.data + hassio = self.hassio + updates = { + DATA_INFO: hassio.get_info(), + DATA_CORE_INFO: hassio.get_core_info(), + DATA_SUPERVISOR_INFO: hassio.get_supervisor_info(), + DATA_OS_INFO: hassio.get_os_info(), + } + if CONTAINER_STATS in container_updates[CORE_CONTAINER]: + updates[DATA_CORE_STATS] = hassio.get_core_stats() + if CONTAINER_STATS in container_updates[SUPERVISOR_CONTAINER]: + updates[DATA_SUPERVISOR_STATS] = hassio.get_supervisor_stats() + + results = await asyncio.gather(*updates.values()) + for key, result in zip(updates, results): + data[key] = result + + _addon_data = data[DATA_SUPERVISOR_INFO].get("addons", []) + all_addons: list[str] = [] + started_addons: list[str] = [] + for addon in _addon_data: + slug = addon[ATTR_SLUG] + all_addons.append(slug) + if addon[ATTR_STATE] == ATTR_STARTED: + started_addons.append(slug) + # + # Update add-on info if its the first update or + # there is at least one entity that needs the data. + # + # When entities are added they call async_enable_container_updates + # to enable updates for the endpoints they need via + # async_added_to_hass. This ensures that we only update + # the data for the endpoints that are needed to avoid unnecessary + # API calls since otherwise we would fetch stats for all containers + # and throw them away. + # + for data_key, update_func, enabled_key, wanted_addons, needs_first_update in ( + ( + DATA_ADDONS_STATS, + self._update_addon_stats, + CONTAINER_STATS, + started_addons, + False, + ), + ( + DATA_ADDONS_CHANGELOGS, + self._update_addon_changelog, + CONTAINER_CHANGELOG, + all_addons, + True, + ), + ( + DATA_ADDONS_INFO, + self._update_addon_info, + CONTAINER_INFO, + all_addons, + True, + ), + ): + container_data: dict[str, Any] = data.setdefault(data_key, {}) + container_data.update( + dict( + await asyncio.gather( + *[ + update_func(slug) + for slug in wanted_addons + if (first_update and needs_first_update) + or enabled_key in container_updates[slug] + ] + ) + ) + ) + + async def _update_addon_stats(self, slug: str) -> tuple[str, dict[str, Any] | None]: + """Update single addon stats.""" + try: + stats = await self.hassio.get_addon_stats(slug) + return (slug, stats) + except HassioAPIError as err: + _LOGGER.warning("Could not fetch stats for %s: %s", slug, err) + return (slug, None) + + async def _update_addon_changelog(self, slug: str) -> tuple[str, str | None]: + """Return the changelog for an add-on.""" + try: + changelog = await self.hassio.get_addon_changelog(slug) + return (slug, changelog) + except HassioAPIError as err: + _LOGGER.warning("Could not fetch changelog for %s: %s", slug, err) + return (slug, None) + + async def _update_addon_info(self, slug: str) -> tuple[str, dict[str, Any] | None]: + """Return the info for an add-on.""" + try: + info = await self.hassio.get_addon_info(slug) + return (slug, info) + except HassioAPIError as err: + _LOGGER.warning("Could not fetch info for %s: %s", slug, err) + return (slug, None) + + @callback + def async_enable_container_updates( + self, slug: str, entity_id: str, types: set[str] + ) -> CALLBACK_TYPE: + """Enable updates for an add-on.""" + enabled_updates = self._container_updates[slug] + for key in types: + enabled_updates[key].add(entity_id) + + @callback + def _remove() -> None: + for key in types: + enabled_updates[key].remove(entity_id) + + return _remove + + async def _async_refresh( + self, + log_failures: bool = True, + raise_on_auth_failed: bool = False, + scheduled: bool = False, + raise_on_entry_error: bool = False, + ) -> None: + """Refresh data.""" + if not scheduled and not raise_on_auth_failed: + # Force refreshing updates for non-scheduled updates + # If `raise_on_auth_failed` is set, it means this is + # the first refresh and we do not want to delay + # startup or cause a timeout so we only refresh the + # updates if this is not a scheduled refresh and + # we are not doing the first refresh. + try: + await self.hassio.refresh_updates() + except HassioAPIError as err: + _LOGGER.warning("Error on Supervisor API: %s", err) + + await super()._async_refresh( + log_failures, raise_on_auth_failed, scheduled, raise_on_entry_error + ) diff --git a/homeassistant/components/hassio/diagnostics.py b/homeassistant/components/hassio/diagnostics.py index 41b3fc1c5c3..ae8b8b3b740 100644 --- a/homeassistant/components/hassio/diagnostics.py +++ b/homeassistant/components/hassio/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for Supervisor.""" + from __future__ import annotations from typing import Any @@ -9,7 +10,8 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er -from . import ADDONS_COORDINATOR, HassioDataUpdateCoordinator +from .const import ADDONS_COORDINATOR +from .data import HassioDataUpdateCoordinator async def async_get_config_entry_diagnostics( diff --git a/homeassistant/components/hassio/discovery.py b/homeassistant/components/hassio/discovery.py index 1810e3ed2c5..66be8267d53 100644 --- a/homeassistant/components/hassio/discovery.py +++ b/homeassistant/components/hassio/discovery.py @@ -1,4 +1,5 @@ """Implement the services discovery feature from Hass.io for Add-ons.""" + from __future__ import annotations import asyncio @@ -77,7 +78,7 @@ class HassIODiscovery(HomeAssistantView): data = await self.hassio.get_discovery_message(uuid) except HassioAPIError as err: _LOGGER.error("Can't read discovery data: %s", err) - raise HTTPServiceUnavailable() from None + raise HTTPServiceUnavailable from None await self.async_process_new(data) return web.Response() diff --git a/homeassistant/components/hassio/entity.py b/homeassistant/components/hassio/entity.py index 63e0314dd05..11259c65d24 100644 --- a/homeassistant/components/hassio/entity.py +++ b/homeassistant/components/hassio/entity.py @@ -1,4 +1,5 @@ """Base for Hass.io entities.""" + from __future__ import annotations from typing import Any @@ -7,7 +8,6 @@ from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import DOMAIN, HassioDataUpdateCoordinator from .const import ( ATTR_SLUG, CONTAINER_STATS, @@ -17,9 +17,11 @@ from .const import ( DATA_KEY_HOST, DATA_KEY_OS, DATA_KEY_SUPERVISOR, + DOMAIN, KEY_TO_UPDATE_TYPES, SUPERVISOR_CONTAINER, ) +from .data import HassioDataUpdateCoordinator class HassioAddonEntity(CoordinatorEntity[HassioDataUpdateCoordinator]): diff --git a/homeassistant/components/hassio/handler.py b/homeassistant/components/hassio/handler.py index 82a2db3c234..ff34aa06cf3 100644 --- a/homeassistant/components/hassio/handler.py +++ b/homeassistant/components/hassio/handler.py @@ -1,4 +1,5 @@ """Handler for Hass.io.""" + from __future__ import annotations import asyncio @@ -570,7 +571,7 @@ class HassIO: # such as ../../../../etc/passwd if url != str(joined_url): _LOGGER.error("Invalid request %s", command) - raise HassioAPIError() + raise HassioAPIError try: response = await self.websession.request( @@ -597,7 +598,7 @@ class HassIO: method, response.status, ) - raise HassioAPIError() + raise HassioAPIError if return_text: return await response.text(encoding="utf-8") @@ -610,4 +611,4 @@ class HassIO: except aiohttp.ClientError as err: _LOGGER.error("Client error on %s request %s", command, err) - raise HassioAPIError() + raise HassioAPIError diff --git a/homeassistant/components/hassio/http.py b/homeassistant/components/hassio/http.py index d86f1b7dc5c..ffb67730fa5 100644 --- a/homeassistant/components/hassio/http.py +++ b/homeassistant/components/hassio/http.py @@ -1,4 +1,5 @@ """HTTP Support for Hass.io.""" + from __future__ import annotations from http import HTTPStatus @@ -23,11 +24,11 @@ from aiohttp.web_exceptions import HTTPBadGateway from homeassistant.components.http import ( KEY_AUTHENTICATED, + KEY_HASS, KEY_HASS_USER, HomeAssistantView, ) from homeassistant.components.onboarding import async_is_onboarded -from homeassistant.core import HomeAssistant from .const import X_HASS_SOURCE @@ -116,7 +117,7 @@ class HassIOView(HomeAssistantView): if path != unquote(path): return web.Response(status=HTTPStatus.BAD_REQUEST) - hass: HomeAssistant = request.app["hass"] + hass = request.app[KEY_HASS] is_admin = request[KEY_AUTHENTICATED] and request[KEY_HASS_USER].is_admin authorized = is_admin @@ -147,9 +148,9 @@ class HassIOView(HomeAssistantView): return web.Response(status=HTTPStatus.UNAUTHORIZED) if authorized: - headers[ - AUTHORIZATION - ] = f"Bearer {os.environ.get('SUPERVISOR_TOKEN', '')}" + headers[AUTHORIZATION] = ( + f"Bearer {os.environ.get('SUPERVISOR_TOKEN', '')}" + ) if request.method == "POST": headers[CONTENT_TYPE] = request.content_type @@ -195,7 +196,7 @@ class HassIOView(HomeAssistantView): except TimeoutError: _LOGGER.error("Client timeout error on API request %s", path) - raise HTTPBadGateway() + raise HTTPBadGateway get = _handle post = _handle diff --git a/homeassistant/components/hassio/ingress.py b/homeassistant/components/hassio/ingress.py index f9ff1dd7770..6d6faa6fe75 100644 --- a/homeassistant/components/hassio/ingress.py +++ b/homeassistant/components/hassio/ingress.py @@ -1,4 +1,5 @@ """Hass.io Add-on ingress service.""" + from __future__ import annotations import asyncio @@ -76,10 +77,10 @@ class HassIOIngress(HomeAssistantView): try: target_url = URL(url) except ValueError as err: - raise HTTPBadRequest() from err + raise HTTPBadRequest from err if not target_url.path.startswith(base_path): - raise HTTPBadRequest() + raise HTTPBadRequest return target_url @@ -98,7 +99,7 @@ class HassIOIngress(HomeAssistantView): except aiohttp.ClientError as err: _LOGGER.debug("Ingress error with %s / %s: %s", token, path, err) - raise HTTPBadGateway() from None + raise HTTPBadGateway from None get = _handle post = _handle @@ -246,7 +247,7 @@ def _init_header(request: web.Request, token: str) -> CIMultiDict | dict[str, st 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() + raise HTTPBadRequest headers[hdrs.X_FORWARDED_FOR] = _forwarded_for_header(forward_for, peername[0]) diff --git a/homeassistant/components/hassio/issues.py b/homeassistant/components/hassio/issues.py index 925c2d70afb..0bb28a3ceef 100644 --- a/homeassistant/components/hassio/issues.py +++ b/homeassistant/components/hassio/issues.py @@ -1,4 +1,5 @@ """Supervisor events monitor.""" + from __future__ import annotations import asyncio @@ -299,13 +300,13 @@ class SupervisorIssues: async def setup(self) -> None: """Create supervisor events listener.""" - await self.update() + await self._update() async_dispatcher_connect( self._hass, EVENT_SUPERVISOR_EVENT, self._supervisor_events_to_issues ) - async def update(self, _: datetime | None = None) -> None: + async def _update(self, _: datetime | None = None) -> None: """Update issues from Supervisor resolution center.""" try: data = await self._client.get_resolution_info() @@ -314,7 +315,7 @@ class SupervisorIssues: async_call_later( self._hass, REQUEST_REFRESH_DELAY, - HassJob(self.update, cancel_on_shutdown=True), + HassJob(self._update, cancel_on_shutdown=True), ) return self.unhealthy_reasons = set(data[ATTR_UNHEALTHY]) @@ -341,7 +342,7 @@ class SupervisorIssues: event[ATTR_WS_EVENT] == EVENT_SUPERVISOR_UPDATE and event.get(ATTR_UPDATE_KEY) == UPDATE_KEY_SUPERVISOR ): - self._hass.async_create_task(self.update()) + self._hass.async_create_task(self._update()) elif event[ATTR_WS_EVENT] == EVENT_HEALTH_CHANGED: self.unhealthy_reasons = ( diff --git a/homeassistant/components/hassio/repairs.py b/homeassistant/components/hassio/repairs.py index 4538d9e1b33..8458d7eaac2 100644 --- a/homeassistant/components/hassio/repairs.py +++ b/homeassistant/components/hassio/repairs.py @@ -1,4 +1,5 @@ """Repairs implementation for supervisor integration.""" + from __future__ import annotations from collections.abc import Callable, Coroutine diff --git a/homeassistant/components/hassio/sensor.py b/homeassistant/components/hassio/sensor.py index 0214f28011d..039bf483682 100644 --- a/homeassistant/components/hassio/sensor.py +++ b/homeassistant/components/hassio/sensor.py @@ -1,4 +1,5 @@ """Sensor platform for Hass.io addons.""" + from __future__ import annotations from homeassistant.components.sensor import ( @@ -12,8 +13,8 @@ from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfInformation from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import ADDONS_COORDINATOR from .const import ( + ADDONS_COORDINATOR, ATTR_CPU_PERCENT, ATTR_MEMORY_PERCENT, ATTR_VERSION, @@ -117,50 +118,48 @@ async def async_setup_entry( entities: list[ HassioOSSensor | HassioAddonSensor | CoreSensor | SupervisorSensor | HostSensor - ] = [] - - for addon in coordinator.data[DATA_KEY_ADDONS].values(): - for entity_description in ADDON_ENTITY_DESCRIPTIONS: - entities.append( - HassioAddonSensor( - addon=addon, - coordinator=coordinator, - entity_description=entity_description, - ) - ) - - for entity_description in CORE_ENTITY_DESCRIPTIONS: - entities.append( - CoreSensor( - coordinator=coordinator, - entity_description=entity_description, - ) + ] = [ + HassioAddonSensor( + addon=addon, + coordinator=coordinator, + entity_description=entity_description, ) + for addon in coordinator.data[DATA_KEY_ADDONS].values() + for entity_description in ADDON_ENTITY_DESCRIPTIONS + ] - for entity_description in SUPERVISOR_ENTITY_DESCRIPTIONS: - entities.append( - SupervisorSensor( - coordinator=coordinator, - entity_description=entity_description, - ) + entities.extend( + CoreSensor( + coordinator=coordinator, + entity_description=entity_description, ) + for entity_description in CORE_ENTITY_DESCRIPTIONS + ) - for entity_description in HOST_ENTITY_DESCRIPTIONS: - entities.append( - HostSensor( - coordinator=coordinator, - entity_description=entity_description, - ) + entities.extend( + SupervisorSensor( + coordinator=coordinator, + entity_description=entity_description, ) + for entity_description in SUPERVISOR_ENTITY_DESCRIPTIONS + ) + + entities.extend( + HostSensor( + coordinator=coordinator, + entity_description=entity_description, + ) + for entity_description in HOST_ENTITY_DESCRIPTIONS + ) if coordinator.is_hass_os: - for entity_description in OS_ENTITY_DESCRIPTIONS: - entities.append( - HassioOSSensor( - coordinator=coordinator, - entity_description=entity_description, - ) + entities.extend( + HassioOSSensor( + coordinator=coordinator, + entity_description=entity_description, ) + for entity_description in OS_ENTITY_DESCRIPTIONS + ) async_add_entities(entities) diff --git a/homeassistant/components/hassio/system_health.py b/homeassistant/components/hassio/system_health.py index d89224a2476..b77187718bb 100644 --- a/homeassistant/components/hassio/system_health.py +++ b/homeassistant/components/hassio/system_health.py @@ -1,4 +1,5 @@ """Provide info to system health.""" + from __future__ import annotations import os @@ -7,10 +8,10 @@ from typing import Any from homeassistant.components import system_health from homeassistant.core import HomeAssistant, callback -from . import get_host_info, get_info, get_os_info, get_supervisor_info +from .data import get_host_info, get_info, get_os_info, get_supervisor_info -SUPERVISOR_PING = f"http://{os.environ['SUPERVISOR']}/supervisor/ping" -OBSERVER_URL = f"http://{os.environ['SUPERVISOR']}:4357" +SUPERVISOR_PING = "http://{ip_address}/supervisor/ping" +OBSERVER_URL = "http://{ip_address}:4357" @callback @@ -23,6 +24,7 @@ def async_register( async def system_health_info(hass: HomeAssistant) -> dict[str, Any]: """Get info for the info page.""" + ip_address = os.environ["SUPERVISOR"] info = get_info(hass) or {} host_info = get_host_info(hass) or {} supervisor_info = get_supervisor_info(hass) @@ -62,7 +64,9 @@ async def system_health_info(hass: HomeAssistant) -> dict[str, Any]: information["board"] = os_info.get("board") information["supervisor_api"] = system_health.async_check_can_reach_url( - hass, SUPERVISOR_PING, OBSERVER_URL + hass, + SUPERVISOR_PING.format(ip_address=ip_address), + OBSERVER_URL.format(ip_address=ip_address), ) information["version_api"] = system_health.async_check_can_reach_url( hass, diff --git a/homeassistant/components/hassio/update.py b/homeassistant/components/hassio/update.py index 8a3199a1121..8e7650a9225 100644 --- a/homeassistant/components/hassio/update.py +++ b/homeassistant/components/hassio/update.py @@ -1,4 +1,5 @@ """Update platform for Supervisor.""" + from __future__ import annotations from typing import Any @@ -16,14 +17,8 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import ( - ADDONS_COORDINATOR, - async_update_addon, - async_update_core, - async_update_os, - async_update_supervisor, -) from .const import ( + ADDONS_COORDINATOR, ATTR_AUTO_UPDATE, ATTR_CHANGELOG, ATTR_VERSION, @@ -39,7 +34,13 @@ from .entity import ( HassioOSEntity, HassioSupervisorEntity, ) -from .handler import HassioAPIError +from .handler import ( + HassioAPIError, + async_update_addon, + async_update_core, + async_update_os, + async_update_supervisor, +) ENTITY_DESCRIPTION = UpdateEntityDescription( name="Update", @@ -66,14 +67,14 @@ async def async_setup_entry( ), ] - for addon in coordinator.data[DATA_KEY_ADDONS].values(): - entities.append( - SupervisorAddonUpdateEntity( - addon=addon, - coordinator=coordinator, - entity_description=ENTITY_DESCRIPTION, - ) + entities.extend( + SupervisorAddonUpdateEntity( + addon=addon, + coordinator=coordinator, + entity_description=ENTITY_DESCRIPTION, ) + for addon in coordinator.data[DATA_KEY_ADDONS].values() + ) if coordinator.is_hass_os: entities.append( diff --git a/homeassistant/components/hassio/websocket_api.py b/homeassistant/components/hassio/websocket_api.py index 13c258dd68c..03ca424035c 100644 --- a/homeassistant/components/hassio/websocket_api.py +++ b/homeassistant/components/hassio/websocket_api.py @@ -1,4 +1,5 @@ """Websocekt API handlers for the hassio integration.""" + import logging from numbers import Number import re @@ -45,7 +46,7 @@ WS_NO_ADMIN_ENDPOINTS = re.compile( r"^(?:" r"|/ingress/(session|validate_session)" r"|/addons/[^/]+/info" - r")$" # noqa: ISC001 + r")$" ) # fmt: on @@ -111,7 +112,7 @@ async def websocket_supervisor_api( if not connection.user.is_admin and not WS_NO_ADMIN_ENDPOINTS.match( msg[ATTR_ENDPOINT] ): - raise Unauthorized() + raise Unauthorized supervisor: HassIO = hass.data[DOMAIN] command = msg[ATTR_ENDPOINT] diff --git a/homeassistant/components/haveibeenpwned/sensor.py b/homeassistant/components/haveibeenpwned/sensor.py index 7caf9690bd8..9933ba11945 100644 --- a/homeassistant/components/haveibeenpwned/sensor.py +++ b/homeassistant/components/haveibeenpwned/sensor.py @@ -1,4 +1,5 @@ """Support for haveibeenpwned (email breaches) sensor.""" + from __future__ import annotations from datetime import timedelta @@ -48,11 +49,7 @@ def setup_platform( api_key = config[CONF_API_KEY] data = HaveIBeenPwnedData(emails, api_key) - devices = [] - for email in emails: - devices.append(HaveIBeenPwnedSensor(data, email)) - - add_entities(devices) + add_entities(HaveIBeenPwnedSensor(data, email) for email in emails) class HaveIBeenPwnedSensor(SensorEntity): diff --git a/homeassistant/components/hddtemp/sensor.py b/homeassistant/components/hddtemp/sensor.py index 77c2a28190b..3dda9f44004 100644 --- a/homeassistant/components/hddtemp/sensor.py +++ b/homeassistant/components/hddtemp/sensor.py @@ -1,4 +1,5 @@ """Support for getting the disk temperature of a host.""" + from __future__ import annotations from datetime import timedelta @@ -65,11 +66,7 @@ def setup_platform( if not disks: disks = [next(iter(hddtemp.data)).split("|")[0]] - dev = [] - for disk in disks: - dev.append(HddTempSensor(name, disk, hddtemp)) - - add_entities(dev, True) + add_entities((HddTempSensor(name, disk, hddtemp) for disk in disks), True) class HddTempSensor(SensorEntity): diff --git a/homeassistant/components/hdmi_cec/__init__.py b/homeassistant/components/hdmi_cec/__init__.py index 54ea2f3e5bd..43a649ba01a 100644 --- a/homeassistant/components/hdmi_cec/__init__.py +++ b/homeassistant/components/hdmi_cec/__init__.py @@ -1,4 +1,5 @@ """Support for HDMI CEC.""" + from __future__ import annotations from functools import reduce @@ -171,7 +172,7 @@ def parse_mapping(mapping, parents=None): if isinstance(addr, (str,)) and isinstance(val, (str,)): yield (addr, PhysicalAddress(val)) else: - cur = parents + [addr] + cur = [*parents, addr] if isinstance(val, dict): yield from parse_mapping(val, cur) elif isinstance(val, str): @@ -252,7 +253,7 @@ def setup(hass: HomeAssistant, base_config: ConfigType) -> bool: # noqa: C901 hdmi_network.send_command(KeyReleaseCommand(dst=ADDR_AUDIOSYSTEM)) else: att = 1 if att == "" else int(att) - for _ in range(0, att): + for _ in range(att): hdmi_network.send_command(KeyPressCommand(cmd, dst=ADDR_AUDIOSYSTEM)) hdmi_network.send_command(KeyReleaseCommand(dst=ADDR_AUDIOSYSTEM)) diff --git a/homeassistant/components/hdmi_cec/icons.json b/homeassistant/components/hdmi_cec/icons.json new file mode 100644 index 00000000000..0bfcb98eea2 --- /dev/null +++ b/homeassistant/components/hdmi_cec/icons.json @@ -0,0 +1,10 @@ +{ + "services": { + "power_on": "mdi:power", + "select_device": "mdi:television", + "send_command": "mdi:console", + "standby": "mdi:power-standby", + "update": "mdi:update", + "volume": "mdi:volume-high" + } +} diff --git a/homeassistant/components/hdmi_cec/media_player.py b/homeassistant/components/hdmi_cec/media_player.py index df7df830fdb..e86a1f5be70 100644 --- a/homeassistant/components/hdmi_cec/media_player.py +++ b/homeassistant/components/hdmi_cec/media_player.py @@ -1,4 +1,5 @@ """Support for HDMI CEC devices as media players.""" + from __future__ import annotations import logging @@ -94,7 +95,7 @@ class CecPlayerEntity(CecEntity, MediaPlayerEntity): def clear_playlist(self) -> None: """Clear players playlist.""" - raise NotImplementedError() + raise NotImplementedError def turn_off(self) -> None: """Turn device off.""" @@ -110,7 +111,7 @@ class CecPlayerEntity(CecEntity, MediaPlayerEntity): self, media_type: MediaType | str, media_id: str, **kwargs: Any ) -> None: """Not supported.""" - raise NotImplementedError() + raise NotImplementedError def media_next_track(self) -> None: """Skip to next track.""" @@ -118,11 +119,11 @@ class CecPlayerEntity(CecEntity, MediaPlayerEntity): def media_seek(self, position: float) -> None: """Not supported.""" - raise NotImplementedError() + raise NotImplementedError def set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" - raise NotImplementedError() + raise NotImplementedError def media_pause(self) -> None: """Pause playback.""" @@ -131,7 +132,7 @@ class CecPlayerEntity(CecEntity, MediaPlayerEntity): def select_source(self, source: str) -> None: """Not supported.""" - raise NotImplementedError() + raise NotImplementedError def media_play(self) -> None: """Start playback.""" diff --git a/homeassistant/components/hdmi_cec/switch.py b/homeassistant/components/hdmi_cec/switch.py index a554594a219..280ea20413b 100644 --- a/homeassistant/components/hdmi_cec/switch.py +++ b/homeassistant/components/hdmi_cec/switch.py @@ -1,4 +1,5 @@ """Support for HDMI CEC devices as switches.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/heatmiser/climate.py b/homeassistant/components/heatmiser/climate.py index 566a4696a73..8639d1f953e 100644 --- a/homeassistant/components/heatmiser/climate.py +++ b/homeassistant/components/heatmiser/climate.py @@ -1,4 +1,5 @@ """Support for the PRT Heatmiser themostats using the V3 protocol.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/heos/__init__.py b/homeassistant/components/heos/__init__.py index c50b70245e3..ed7c768e161 100644 --- a/homeassistant/components/heos/__init__.py +++ b/homeassistant/components/heos/__init__.py @@ -1,4 +1,5 @@ """Denon HEOS Media Player.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/heos/config_flow.py b/homeassistant/components/heos/config_flow.py index 63a020c41d9..b68d7d16717 100644 --- a/homeassistant/components/heos/config_flow.py +++ b/homeassistant/components/heos/config_flow.py @@ -1,14 +1,14 @@ """Config flow to configure Heos.""" + from typing import TYPE_CHECKING from urllib.parse import urlparse from pyheos import Heos, HeosError import voluptuous as vol -from homeassistant import config_entries from homeassistant.components import ssdp +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST -from homeassistant.data_entry_flow import FlowResult from .const import DATA_DISCOVERED_HOSTS, DOMAIN @@ -18,12 +18,14 @@ def format_title(host: str) -> str: return f"Controller ({host})" -class HeosFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class HeosFlowHandler(ConfigFlow, domain=DOMAIN): """Define a flow for HEOS.""" VERSION = 1 - async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: + async def async_step_ssdp( + self, discovery_info: ssdp.SsdpServiceInfo + ) -> ConfigFlowResult: """Handle a discovered Heos device.""" # Store discovered host if TYPE_CHECKING: diff --git a/homeassistant/components/heos/media_player.py b/homeassistant/components/heos/media_player.py index 8502dec28fa..564b764bc2e 100644 --- a/homeassistant/components/heos/media_player.py +++ b/homeassistant/components/heos/media_player.py @@ -1,4 +1,5 @@ """Denon HEOS Media Player.""" + from __future__ import annotations from collections.abc import Awaitable, Callable, Coroutine @@ -160,9 +161,9 @@ class HeosMediaPlayer(MediaPlayerEntity): async_dispatcher_connect(self.hass, SIGNAL_HEOS_UPDATED, self._heos_updated) ) # Register this player's entity_id so it can be resolved by the group manager - self.hass.data[HEOS_DOMAIN][DATA_ENTITY_ID_MAP][ - self._player.player_id - ] = self.entity_id + self.hass.data[HEOS_DOMAIN][DATA_ENTITY_ID_MAP][self._player.player_id] = ( + self.entity_id + ) async_dispatcher_send(self.hass, SIGNAL_HEOS_PLAYER_ADDED) @log_command_error("clear playlist") diff --git a/homeassistant/components/heos/services.py b/homeassistant/components/heos/services.py index 9331f786f9d..2ef80b6efd9 100644 --- a/homeassistant/components/heos/services.py +++ b/homeassistant/components/heos/services.py @@ -1,4 +1,5 @@ """Services for the HEOS integration.""" + import functools import logging diff --git a/homeassistant/components/here_travel_time/__init__.py b/homeassistant/components/here_travel_time/__init__.py index 1c728bcc12c..9da1ce491f0 100644 --- a/homeassistant/components/here_travel_time/__init__.py +++ b/homeassistant/components/here_travel_time/__init__.py @@ -1,9 +1,11 @@ """The HERE Travel Time integration.""" + from __future__ import annotations from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY, CONF_MODE, Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers.start import async_at_started from homeassistant.util import dt as dt_util from .const import ( @@ -48,22 +50,19 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b departure=departure, ) + cls: type[HERETransitDataUpdateCoordinator] | type[HERERoutingDataUpdateCoordinator] if config_entry.data[CONF_MODE] in {TRAVEL_MODE_PUBLIC, "publicTransportTimeTable"}: - hass.data.setdefault(DOMAIN, {})[ - config_entry.entry_id - ] = HERETransitDataUpdateCoordinator( - hass, - api_key, - here_travel_time_config, - ) + cls = HERETransitDataUpdateCoordinator else: - hass.data.setdefault(DOMAIN, {})[ - config_entry.entry_id - ] = HERERoutingDataUpdateCoordinator( - hass, - api_key, - here_travel_time_config, - ) + cls = HERERoutingDataUpdateCoordinator + + data_coordinator = cls(hass, api_key, here_travel_time_config) + hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = data_coordinator + + async def _async_update_at_start(_: HomeAssistant) -> None: + await data_coordinator.async_refresh() + + config_entry.async_on_unload(async_at_started(hass, _async_update_at_start)) await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) return True diff --git a/homeassistant/components/here_travel_time/config_flow.py b/homeassistant/components/here_travel_time/config_flow.py index 3db4a841d53..d27ea577c29 100644 --- a/homeassistant/components/here_travel_time/config_flow.py +++ b/homeassistant/components/here_travel_time/config_flow.py @@ -1,4 +1,5 @@ """Config flow for HERE Travel Time integration.""" + from __future__ import annotations import logging @@ -14,7 +15,12 @@ from here_routing import ( from here_transit import HERETransitError import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.const import ( CONF_API_KEY, CONF_LATITUDE, @@ -23,7 +29,6 @@ from homeassistant.const import ( CONF_NAME, ) from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult import homeassistant.helpers.config_validation as cv from homeassistant.helpers.selector import ( EntitySelector, @@ -91,7 +96,7 @@ def get_user_step_schema(data: dict[str, Any]) -> vol.Schema: ) -class HERETravelTimeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class HERETravelTimeConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for HERE Travel Time.""" VERSION = 1 @@ -103,14 +108,14 @@ class HERETravelTimeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback def async_get_options_flow( - config_entry: config_entries.ConfigEntry, + config_entry: ConfigEntry, ) -> HERETravelTimeOptionsFlow: """Get the options flow.""" return HERETravelTimeOptionsFlow(config_entry) async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors = {} user_input = user_input or {} @@ -129,7 +134,7 @@ class HERETravelTimeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="user", data_schema=get_user_step_schema(user_input), errors=errors ) - async def async_step_origin_menu(self, _: None = None) -> FlowResult: + async def async_step_origin_menu(self, _: None = None) -> ConfigFlowResult: """Show the origin menu.""" return self.async_show_menu( step_id="origin_menu", @@ -138,7 +143,7 @@ class HERETravelTimeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_origin_coordinates( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Configure origin by using gps coordinates.""" if user_input is not None: self._config[CONF_ORIGIN_LATITUDE] = user_input[CONF_ORIGIN][CONF_LATITUDE] @@ -159,7 +164,7 @@ class HERETravelTimeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) return self.async_show_form(step_id="origin_coordinates", data_schema=schema) - async def async_step_destination_menu(self, _: None = None) -> FlowResult: + async def async_step_destination_menu(self, _: None = None) -> ConfigFlowResult: """Show the destination menu.""" return self.async_show_menu( step_id="destination_menu", @@ -168,7 +173,7 @@ class HERETravelTimeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_origin_entity( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Configure origin by using an entity.""" if user_input is not None: self._config[CONF_ORIGIN_ENTITY_ID] = user_input[CONF_ORIGIN_ENTITY_ID] @@ -179,7 +184,7 @@ class HERETravelTimeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_destination_coordinates( self, user_input: dict[str, Any] | None = None, - ) -> FlowResult: + ) -> ConfigFlowResult: """Configure destination by using gps coordinates.""" if user_input is not None: self._config[CONF_DESTINATION_LATITUDE] = user_input[CONF_DESTINATION][ @@ -211,7 +216,7 @@ class HERETravelTimeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_destination_entity( self, user_input: dict[str, Any] | None = None, - ) -> FlowResult: + ) -> ConfigFlowResult: """Configure destination by using an entity.""" if user_input is not None: self._config[CONF_DESTINATION_ENTITY_ID] = user_input[ @@ -228,17 +233,17 @@ class HERETravelTimeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form(step_id="destination_entity", data_schema=schema) -class HERETravelTimeOptionsFlow(config_entries.OptionsFlow): +class HERETravelTimeOptionsFlow(OptionsFlow): """Handle HERE Travel Time options.""" - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + def __init__(self, config_entry: ConfigEntry) -> None: """Initialize HERE Travel Time options flow.""" self.config_entry = config_entry self._config: dict[str, Any] = {} async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Manage the HERE Travel Time options.""" if user_input is not None: self._config = user_input @@ -257,7 +262,7 @@ class HERETravelTimeOptionsFlow(config_entries.OptionsFlow): return self.async_show_form(step_id="init", data_schema=schema) - async def async_step_time_menu(self, _: None = None) -> FlowResult: + async def async_step_time_menu(self, _: None = None) -> ConfigFlowResult: """Show the time menu.""" return self.async_show_menu( step_id="time_menu", @@ -266,13 +271,13 @@ class HERETravelTimeOptionsFlow(config_entries.OptionsFlow): async def async_step_no_time( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Create Options Entry.""" return self.async_create_entry(title="", data=self._config) async def async_step_arrival_time( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Configure arrival time.""" if user_input is not None: self._config[CONF_ARRIVAL_TIME] = user_input[CONF_ARRIVAL_TIME] @@ -286,7 +291,7 @@ class HERETravelTimeOptionsFlow(config_entries.OptionsFlow): async def async_step_departure_time( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Configure departure time.""" if user_input is not None: self._config[CONF_DEPARTURE_TIME] = user_input[CONF_DEPARTURE_TIME] diff --git a/homeassistant/components/here_travel_time/const.py b/homeassistant/components/here_travel_time/const.py index a2a14a445d7..785070cd3b1 100644 --- a/homeassistant/components/here_travel_time/const.py +++ b/homeassistant/components/here_travel_time/const.py @@ -1,4 +1,5 @@ """Constants for the HERE Travel Time integration.""" + from typing import Final DOMAIN = "here_travel_time" diff --git a/homeassistant/components/here_travel_time/coordinator.py b/homeassistant/components/here_travel_time/coordinator.py index dbb17b58336..6591f4cb5cc 100644 --- a/homeassistant/components/here_travel_time/coordinator.py +++ b/homeassistant/components/here_travel_time/coordinator.py @@ -1,4 +1,5 @@ """The HERE Travel Time integration.""" + from __future__ import annotations from datetime import datetime, time, timedelta diff --git a/homeassistant/components/here_travel_time/model.py b/homeassistant/components/here_travel_time/model.py index 433653cbf56..178c0d8c805 100644 --- a/homeassistant/components/here_travel_time/model.py +++ b/homeassistant/components/here_travel_time/model.py @@ -1,4 +1,5 @@ """Model Classes for here_travel_time.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/homeassistant/components/here_travel_time/sensor.py b/homeassistant/components/here_travel_time/sensor.py index 193a86a3d37..4d7566ef2e2 100644 --- a/homeassistant/components/here_travel_time/sensor.py +++ b/homeassistant/components/here_travel_time/sensor.py @@ -1,4 +1,5 @@ """Support for HERE travel time sensors.""" + from __future__ import annotations from collections.abc import Mapping @@ -24,7 +25,6 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.start import async_at_started from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( @@ -86,16 +86,15 @@ async def async_setup_entry( name = config_entry.data[CONF_NAME] coordinator = hass.data[DOMAIN][entry_id] - sensors: list[HERETravelTimeSensor] = [] - for sensor_description in sensor_descriptions(config_entry.data[CONF_MODE]): - sensors.append( - HERETravelTimeSensor( - entry_id, - name, - sensor_description, - coordinator, - ) + sensors: list[HERETravelTimeSensor] = [ + HERETravelTimeSensor( + entry_id, + name, + sensor_description, + coordinator, ) + for sensor_description in sensor_descriptions(config_entry.data[CONF_MODE]) + ] sensors.append(OriginSensor(entry_id, name, coordinator)) sensors.append(DestinationSensor(entry_id, name, coordinator)) async_add_entities(sensors) @@ -140,11 +139,6 @@ class HERETravelTimeSensor( await self._async_restore_state() await super().async_added_to_hass() - async def _update_at_start(_: HomeAssistant) -> None: - await self.async_update() - - self.async_on_remove(async_at_started(self.hass, _update_at_start)) - @callback def _handle_coordinator_update(self) -> None: """Handle updated data from the coordinator.""" diff --git a/homeassistant/components/hikvision/binary_sensor.py b/homeassistant/components/hikvision/binary_sensor.py index ef8a18ec3ae..2e4af361b38 100644 --- a/homeassistant/components/hikvision/binary_sensor.py +++ b/homeassistant/components/hikvision/binary_sensor.py @@ -1,4 +1,5 @@ """Support for Hikvision event stream events represented as binary sensors.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/hikvisioncam/switch.py b/homeassistant/components/hikvisioncam/switch.py index ea521ec3a6b..c455fcb5bbc 100644 --- a/homeassistant/components/hikvisioncam/switch.py +++ b/homeassistant/components/hikvisioncam/switch.py @@ -1,4 +1,5 @@ """Support turning on/off motion detection on Hikvision cameras.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/hisense_aehw4a1/__init__.py b/homeassistant/components/hisense_aehw4a1/__init__.py index cc599aa31fc..d20f6d13217 100644 --- a/homeassistant/components/hisense_aehw4a1/__init__.py +++ b/homeassistant/components/hisense_aehw4a1/__init__.py @@ -1,4 +1,5 @@ """The Hisense AEH-W4A1 integration.""" + import ipaddress import logging diff --git a/homeassistant/components/hisense_aehw4a1/climate.py b/homeassistant/components/hisense_aehw4a1/climate.py index 0e3fa9981c1..656ba6c68c0 100644 --- a/homeassistant/components/hisense_aehw4a1/climate.py +++ b/homeassistant/components/hisense_aehw4a1/climate.py @@ -1,4 +1,5 @@ """Pyaehw4a1 platform to control of Hisense AEH-W4A1 Climate Devices.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/hisense_aehw4a1/config_flow.py b/homeassistant/components/hisense_aehw4a1/config_flow.py index 8fd651d1782..3d4c83d6f53 100644 --- a/homeassistant/components/hisense_aehw4a1/config_flow.py +++ b/homeassistant/components/hisense_aehw4a1/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Hisense AEH-W4A1 integration.""" + from pyaehw4a1.aehw4a1 import AehW4a1 from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/history/__init__.py b/homeassistant/components/history/__init__.py index 9eab92dce5c..365be06fd2d 100644 --- a/homeassistant/components/history/__init__.py +++ b/homeassistant/components/history/__init__.py @@ -1,4 +1,5 @@ """Provide pre-made queries on top of the recorder component.""" + from __future__ import annotations from datetime import datetime as dt, timedelta @@ -9,7 +10,7 @@ from aiohttp import web import voluptuous as vol from homeassistant.components import frontend -from homeassistant.components.http import HomeAssistantView +from homeassistant.components.http import KEY_HASS, HomeAssistantView from homeassistant.components.recorder import get_instance, history from homeassistant.components.recorder.util import session_scope from homeassistant.const import CONF_EXCLUDE, CONF_INCLUDE @@ -74,7 +75,7 @@ class HistoryPeriodView(HomeAssistantView): "filter_entity_id is missing", HTTPStatus.BAD_REQUEST ) - hass = request.app["hass"] + hass = request.app[KEY_HASS] for entity_id in entity_ids: if not hass.states.get(entity_id) and not valid_entity_id(entity_id): diff --git a/homeassistant/components/history/helpers.py b/homeassistant/components/history/helpers.py index 7e28e69e5f9..bd477e7e4ed 100644 --- a/homeassistant/components/history/helpers.py +++ b/homeassistant/components/history/helpers.py @@ -1,4 +1,5 @@ """Helpers for the history integration.""" + from __future__ import annotations from collections.abc import Iterable diff --git a/homeassistant/components/history/websocket_api.py b/homeassistant/components/history/websocket_api.py index f903a9904a9..462d8464229 100644 --- a/homeassistant/components/history/websocket_api.py +++ b/homeassistant/components/history/websocket_api.py @@ -1,10 +1,11 @@ """Websocket API for the history integration.""" + from __future__ import annotations import asyncio from collections.abc import Callable, Iterable, MutableMapping from dataclasses import dataclass -from datetime import datetime as dt +from datetime import datetime as dt, timedelta import logging from typing import Any, cast @@ -35,7 +36,7 @@ from homeassistant.helpers.event import ( async_track_state_change_event, ) from homeassistant.helpers.json import json_bytes -from homeassistant.helpers.typing import EventType +from homeassistant.util.async_ import create_eager_task import homeassistant.util.dt as dt_util from .const import EVENT_COALESCE_TIME, MAX_PENDING_HISTORY_STATES @@ -330,11 +331,14 @@ async def _async_events_consumer( no_attributes: bool, ) -> None: """Stream events from the queue.""" + subscriptions_setup_complete_timestamp = ( + subscriptions_setup_complete_time.timestamp() + ) while True: events: list[Event] = [await stream_queue.get()] # If the event is older than the last db # event we already sent it so we skip it. - if events[0].time_fired <= subscriptions_setup_complete_time: + if events[0].time_fired_timestamp <= subscriptions_setup_complete_timestamp: continue # We sleep for the EVENT_COALESCE_TIME so # we can group events together to minimize @@ -359,7 +363,7 @@ async def _async_events_consumer( def _async_subscribe_events( hass: HomeAssistant, subscriptions: list[CALLBACK_TYPE], - target: Callable[[Event], None], + target: Callable[[Event[Any]], None], entity_ids: list[str], significant_changes_only: bool, minimal_response: bool, @@ -372,7 +376,7 @@ def _async_subscribe_events( assert is_callback(target), "target must be a callback" @callback - def _forward_state_events_filtered(event: EventType[EventStateChangedData]) -> None: + def _forward_state_events_filtered(event: Event[EventStateChangedData]) -> None: """Filter state events and forward them.""" if (new_state := event.data["new_state"]) is None or ( old_state := event.data["old_state"] @@ -536,7 +540,7 @@ async def ws_stream( # Unsubscribe happened while sending historical states return - live_stream.task = asyncio.create_task( + live_stream.task = create_eager_task( _async_events_consumer( subscriptions_setup_complete_time, connection, @@ -546,7 +550,7 @@ async def ws_stream( ) ) - live_stream.wait_sync_task = asyncio.create_task( + live_stream.wait_sync_task = create_eager_task( get_instance(hass).async_block_till_done() ) await live_stream.wait_sync_task @@ -563,7 +567,10 @@ async def ws_stream( hass, connection, msg_id, - last_event_time or start_time, + # Add one microsecond so we are outside the window of + # the last event we got from the database since otherwise + # we could fetch the same event twice + (last_event_time or start_time) + timedelta(microseconds=1), subscriptions_setup_complete_time, entity_ids, False, # We don't want the start time state again diff --git a/homeassistant/components/history_stats/coordinator.py b/homeassistant/components/history_stats/coordinator.py index 6d4d6e55fa9..2127f1d3dc5 100644 --- a/homeassistant/components/history_stats/coordinator.py +++ b/homeassistant/components/history_stats/coordinator.py @@ -1,18 +1,18 @@ """History stats data coordinator.""" + from __future__ import annotations from datetime import timedelta import logging from typing import Any -from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback +from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback from homeassistant.exceptions import TemplateError from homeassistant.helpers.event import ( EventStateChangedData, async_track_state_change_event, ) from homeassistant.helpers.start import async_at_start -from homeassistant.helpers.typing import EventType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .data import HistoryStats, HistoryStatsState @@ -87,7 +87,7 @@ class HistoryStatsUpdateCoordinator(DataUpdateCoordinator[HistoryStatsState]): ) async def _async_update_from_event( - self, event: EventType[EventStateChangedData] + self, event: Event[EventStateChangedData] ) -> None: """Process an update from an event.""" self.async_set_updated_data(await self._history_stats.async_update(event)) diff --git a/homeassistant/components/history_stats/data.py b/homeassistant/components/history_stats/data.py index 69e56ba0333..62ab28dc4f1 100644 --- a/homeassistant/components/history_stats/data.py +++ b/homeassistant/components/history_stats/data.py @@ -1,14 +1,14 @@ """Manage the history_stats data.""" + from __future__ import annotations from dataclasses import dataclass import datetime from homeassistant.components.recorder import get_instance, history -from homeassistant.core import HomeAssistant, State +from homeassistant.core import Event, HomeAssistant, State from homeassistant.helpers.event import EventStateChangedData from homeassistant.helpers.template import Template -from homeassistant.helpers.typing import EventType import homeassistant.util.dt as dt_util from .helpers import async_calculate_period, floored_timestamp @@ -58,7 +58,7 @@ class HistoryStats: self._end = end async def async_update( - self, event: EventType[EventStateChangedData] | None + self, event: Event[EventStateChangedData] | None ) -> HistoryStatsState: """Update the stats at a given time.""" # Get previous values of start and end diff --git a/homeassistant/components/history_stats/helpers.py b/homeassistant/components/history_stats/helpers.py index 0c914e1fd41..33a45d10735 100644 --- a/homeassistant/components/history_stats/helpers.py +++ b/homeassistant/components/history_stats/helpers.py @@ -1,4 +1,5 @@ """Helpers to make instant statistics about your history.""" + from __future__ import annotations import datetime diff --git a/homeassistant/components/history_stats/icons.json b/homeassistant/components/history_stats/icons.json new file mode 100644 index 00000000000..a03163179cb --- /dev/null +++ b/homeassistant/components/history_stats/icons.json @@ -0,0 +1,5 @@ +{ + "services": { + "reload": "mdi:reload" + } +} diff --git a/homeassistant/components/history_stats/sensor.py b/homeassistant/components/history_stats/sensor.py index 7f318b03e06..0134f4682a5 100644 --- a/homeassistant/components/history_stats/sensor.py +++ b/homeassistant/components/history_stats/sensor.py @@ -1,4 +1,5 @@ """Component to make instant statistics about your history.""" + from __future__ import annotations from abc import abstractmethod diff --git a/homeassistant/components/hitron_coda/device_tracker.py b/homeassistant/components/hitron_coda/device_tracker.py index df1189f9e76..ba672e38106 100644 --- a/homeassistant/components/hitron_coda/device_tracker.py +++ b/homeassistant/components/hitron_coda/device_tracker.py @@ -1,4 +1,5 @@ """Support for the Hitron CODA-4582U, provided by Rogers.""" + from __future__ import annotations from collections import namedtuple diff --git a/homeassistant/components/hive/__init__.py b/homeassistant/components/hive/__init__.py index 1a386d3b271..fb2733223eb 100644 --- a/homeassistant/components/hive/__init__.py +++ b/homeassistant/components/hive/__init__.py @@ -1,4 +1,5 @@ """Support for the Hive devices and services.""" + from __future__ import annotations from collections.abc import Awaitable, Callable, Coroutine @@ -89,7 +90,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: devices = await hive.session.startSession(hive_config) except HTTPException as error: _LOGGER.error("Could not connect to the internet: %s", error) - raise ConfigEntryNotReady() from error + raise ConfigEntryNotReady from error except HiveReauthRequired as err: raise ConfigEntryAuthFailed from err diff --git a/homeassistant/components/hive/alarm_control_panel.py b/homeassistant/components/hive/alarm_control_panel.py index 46cc37751a0..78e8606a43c 100644 --- a/homeassistant/components/hive/alarm_control_panel.py +++ b/homeassistant/components/hive/alarm_control_panel.py @@ -1,4 +1,5 @@ """Support for the Hive alarm.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/hive/binary_sensor.py b/homeassistant/components/hive/binary_sensor.py index 6306b48f733..af1df7d4d62 100644 --- a/homeassistant/components/hive/binary_sensor.py +++ b/homeassistant/components/hive/binary_sensor.py @@ -1,4 +1,5 @@ """Support for the Hive binary sensors.""" + from datetime import timedelta from homeassistant.components.binary_sensor import ( @@ -51,13 +52,17 @@ async def async_setup_entry( hive = hass.data[DOMAIN][entry.entry_id] devices = hive.session.deviceList.get("binary_sensor") - entities = [] - if devices: - for description in BINARY_SENSOR_TYPES: - for dev in devices: - if dev["hiveType"] == description.key: - entities.append(HiveBinarySensorEntity(hive, dev, description)) - async_add_entities(entities, True) + if not devices: + return + async_add_entities( + ( + HiveBinarySensorEntity(hive, dev, description) + for dev in devices + for description in BINARY_SENSOR_TYPES + if dev["hiveType"] == description.key + ), + True, + ) class HiveBinarySensorEntity(HiveEntity, BinarySensorEntity): diff --git a/homeassistant/components/hive/climate.py b/homeassistant/components/hive/climate.py index 8085719d8c5..cb1cc15a5bf 100644 --- a/homeassistant/components/hive/climate.py +++ b/homeassistant/components/hive/climate.py @@ -1,4 +1,5 @@ """Support for the Hive climate devices.""" + from datetime import timedelta import logging from typing import Any @@ -58,11 +59,8 @@ async def async_setup_entry( hive = hass.data[DOMAIN][entry.entry_id] devices = hive.session.deviceList.get("climate") - entities = [] if devices: - for dev in devices: - entities.append(HiveClimateEntity(hive, dev)) - async_add_entities(entities, True) + async_add_entities((HiveClimateEntity(hive, dev) for dev in devices), True) platform = entity_platform.async_get_current_platform() diff --git a/homeassistant/components/hive/config_flow.py b/homeassistant/components/hive/config_flow.py index ec1c4f78e87..3b11e7a8246 100644 --- a/homeassistant/components/hive/config_flow.py +++ b/homeassistant/components/hive/config_flow.py @@ -1,4 +1,5 @@ """Config Flow for Hive.""" + from __future__ import annotations from collections.abc import Mapping @@ -13,15 +14,20 @@ from apyhiveapi.helper.hive_exceptions import ( ) import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ( + SOURCE_REAUTH, + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.const import CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult from .const import CONF_CODE, CONF_DEVICE_NAME, CONFIG_ENTRY_VERSION, DOMAIN -class HiveFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class HiveFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a Hive config flow.""" VERSION = CONFIG_ENTRY_VERSION @@ -47,7 +53,7 @@ class HiveFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): # Get user from existing entry and abort if already setup self.entry = await self.async_set_unique_id(self.data[CONF_USERNAME]) - if self.context["source"] != config_entries.SOURCE_REAUTH: + if self.context["source"] != SOURCE_REAUTH: self._abort_if_unique_id_configured() # Login to the Hive. @@ -94,7 +100,7 @@ class HiveFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors["base"] = "no_internet_available" if not errors: - if self.context["source"] == config_entries.SOURCE_REAUTH: + if self.context["source"] == SOURCE_REAUTH: return await self.async_setup_hive_entry() self.device_registration = True return await self.async_step_configuration() @@ -132,7 +138,7 @@ class HiveFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): # Setup the config entry self.data["tokens"] = self.tokens - if self.context["source"] == config_entries.SOURCE_REAUTH: + if self.context["source"] == SOURCE_REAUTH: self.hass.config_entries.async_update_entry( self.entry, title=self.data["username"], data=self.data ) @@ -140,7 +146,9 @@ class HiveFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_abort(reason="reauth_successful") return self.async_create_entry(title=self.data["username"], data=self.data) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Re Authenticate a user.""" data = { CONF_USERNAME: entry_data[CONF_USERNAME], @@ -155,16 +163,16 @@ class HiveFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback def async_get_options_flow( - config_entry: config_entries.ConfigEntry, + config_entry: ConfigEntry, ) -> HiveOptionsFlowHandler: """Hive options callback.""" return HiveOptionsFlowHandler(config_entry) -class HiveOptionsFlowHandler(config_entries.OptionsFlow): +class HiveOptionsFlowHandler(OptionsFlow): """Config flow options for Hive.""" - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + def __init__(self, config_entry: ConfigEntry) -> None: """Initialize Hive options flow.""" self.hive = None self.config_entry = config_entry diff --git a/homeassistant/components/hive/const.py b/homeassistant/components/hive/const.py index b7a2be6910f..2c07469185a 100644 --- a/homeassistant/components/hive/const.py +++ b/homeassistant/components/hive/const.py @@ -1,4 +1,5 @@ """Constants for Hive.""" + from homeassistant.const import Platform ATTR_MODE = "mode" diff --git a/homeassistant/components/hive/light.py b/homeassistant/components/hive/light.py index d173751c6c8..1ce49599262 100644 --- a/homeassistant/components/hive/light.py +++ b/homeassistant/components/hive/light.py @@ -1,4 +1,5 @@ """Support for Hive light devices.""" + from __future__ import annotations from datetime import timedelta @@ -33,11 +34,9 @@ async def async_setup_entry( hive: Hive = hass.data[DOMAIN][entry.entry_id] devices = hive.session.deviceList.get("light") - entities = [] - if devices: - for dev in devices: - entities.append(HiveDeviceLight(hive, dev)) - async_add_entities(entities, True) + if not devices: + return + async_add_entities((HiveDeviceLight(hive, dev) for dev in devices), True) class HiveDeviceLight(HiveEntity, LightEntity): diff --git a/homeassistant/components/hive/sensor.py b/homeassistant/components/hive/sensor.py index 0849cf39782..5f750642385 100644 --- a/homeassistant/components/hive/sensor.py +++ b/homeassistant/components/hive/sensor.py @@ -1,4 +1,5 @@ """Support for the Hive sensors.""" + from datetime import timedelta from homeassistant.components.sensor import ( @@ -53,13 +54,17 @@ async def async_setup_entry( """Set up Hive thermostat based on a config entry.""" hive = hass.data[DOMAIN][entry.entry_id] devices = hive.session.deviceList.get("sensor") - entities = [] - if devices: - for description in SENSOR_TYPES: - for dev in devices: - if dev["hiveType"] == description.key: - entities.append(HiveSensorEntity(hive, dev, description)) - async_add_entities(entities, True) + if not devices: + return + async_add_entities( + ( + HiveSensorEntity(hive, dev, description) + for dev in devices + for description in SENSOR_TYPES + if dev["hiveType"] == description.key + ), + True, + ) class HiveSensorEntity(HiveEntity, SensorEntity): diff --git a/homeassistant/components/hive/switch.py b/homeassistant/components/hive/switch.py index 80e9d485f5b..6bcbfc6345c 100644 --- a/homeassistant/components/hive/switch.py +++ b/homeassistant/components/hive/switch.py @@ -1,4 +1,5 @@ """Support for the Hive switches.""" + from __future__ import annotations from datetime import timedelta @@ -35,13 +36,17 @@ async def async_setup_entry( hive = hass.data[DOMAIN][entry.entry_id] devices = hive.session.deviceList.get("switch") - entities = [] - if devices: - for description in SWITCH_TYPES: - for dev in devices: - if dev["hiveType"] == description.key: - entities.append(HiveSwitch(hive, dev, description)) - async_add_entities(entities, True) + if not devices: + return + async_add_entities( + ( + HiveSwitch(hive, dev, description) + for dev in devices + for description in SWITCH_TYPES + if dev["hiveType"] == description.key + ), + True, + ) class HiveSwitch(HiveEntity, SwitchEntity): diff --git a/homeassistant/components/hive/water_heater.py b/homeassistant/components/hive/water_heater.py index 2dbb6cb0230..127fb80ef18 100644 --- a/homeassistant/components/hive/water_heater.py +++ b/homeassistant/components/hive/water_heater.py @@ -1,4 +1,5 @@ """Support for hive water heaters.""" + from datetime import timedelta import voluptuous as vol @@ -48,11 +49,8 @@ async def async_setup_entry( hive = hass.data[DOMAIN][entry.entry_id] devices = hive.session.deviceList.get("water_heater") - entities = [] if devices: - for dev in devices: - entities.append(HiveWaterHeater(hive, dev)) - async_add_entities(entities, True) + async_add_entities((HiveWaterHeater(hive, dev) for dev in devices), True) platform = entity_platform.async_get_current_platform() diff --git a/homeassistant/components/hko/__init__.py b/homeassistant/components/hko/__init__.py index a83c1dd2d89..5b06644580e 100644 --- a/homeassistant/components/hko/__init__.py +++ b/homeassistant/components/hko/__init__.py @@ -1,4 +1,5 @@ """The Hong Kong Observatory integration.""" + from __future__ import annotations from hko import LOCATIONS diff --git a/homeassistant/components/hko/config_flow.py b/homeassistant/components/hko/config_flow.py index 21697d2dd53..aeee7d4aff8 100644 --- a/homeassistant/components/hko/config_flow.py +++ b/homeassistant/components/hko/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Hong Kong Observatory integration.""" + from __future__ import annotations from asyncio import timeout @@ -7,9 +8,8 @@ from typing import Any from hko import HKO, LOCATIONS, HKOError import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_LOCATION -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.selector import SelectSelector, SelectSelectorConfig @@ -30,14 +30,14 @@ STEP_USER_DATA_SCHEMA = vol.Schema( ) -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class HKOConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Hong Kong Observatory.""" VERSION = 1 async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" if user_input is None: return self.async_show_form( diff --git a/homeassistant/components/hko/const.py b/homeassistant/components/hko/const.py index a9a554850b0..288fdf56682 100644 --- a/homeassistant/components/hko/const.py +++ b/homeassistant/components/hko/const.py @@ -1,4 +1,5 @@ """Constants for the Hong Kong Observatory integration.""" + from hko import LOCATIONS from homeassistant.components.weather import ( diff --git a/homeassistant/components/hko/coordinator.py b/homeassistant/components/hko/coordinator.py index 05280c4a3bd..c7d80ae299e 100644 --- a/homeassistant/components/hko/coordinator.py +++ b/homeassistant/components/hko/coordinator.py @@ -1,4 +1,5 @@ """Weather data coordinator for the HKO API.""" + from asyncio import timeout from datetime import timedelta import logging diff --git a/homeassistant/components/hko/weather.py b/homeassistant/components/hko/weather.py index f4a784c5308..6d3d12d8ab4 100644 --- a/homeassistant/components/hko/weather.py +++ b/homeassistant/components/hko/weather.py @@ -1,4 +1,5 @@ """Support for the HKO service.""" + from homeassistant.components.weather import ( Forecast, WeatherEntity, diff --git a/homeassistant/components/hlk_sw16/__init__.py b/homeassistant/components/hlk_sw16/__init__.py index 9be0b5203fd..3e6a9f6b0d6 100644 --- a/homeassistant/components/hlk_sw16/__init__.py +++ b/homeassistant/components/hlk_sw16/__init__.py @@ -1,4 +1,5 @@ """Support for HLK-SW16 relay switches.""" + import logging from hlk_sw16 import create_hlk_sw16_connection diff --git a/homeassistant/components/hlk_sw16/config_flow.py b/homeassistant/components/hlk_sw16/config_flow.py index 6ea5f9d43db..b315d0daa78 100644 --- a/homeassistant/components/hlk_sw16/config_flow.py +++ b/homeassistant/components/hlk_sw16/config_flow.py @@ -1,10 +1,11 @@ """Config flow for HLK-SW16.""" + import asyncio from hlk_sw16 import create_hlk_sw16_connection import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.core import HomeAssistant @@ -63,7 +64,7 @@ async def validate_input(hass: HomeAssistant, user_input): client.stop() -class SW16FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class SW16FlowHandler(ConfigFlow, domain=DOMAIN): """Handle a HLK-SW16 config flow.""" VERSION = 1 diff --git a/homeassistant/components/hlk_sw16/errors.py b/homeassistant/components/hlk_sw16/errors.py index 87453737531..01ad3deb4eb 100644 --- a/homeassistant/components/hlk_sw16/errors.py +++ b/homeassistant/components/hlk_sw16/errors.py @@ -1,4 +1,5 @@ """Errors for the HLK-SW16 component.""" + from homeassistant.exceptions import HomeAssistantError diff --git a/homeassistant/components/hlk_sw16/switch.py b/homeassistant/components/hlk_sw16/switch.py index 2189a285ad8..590ab9c4497 100644 --- a/homeassistant/components/hlk_sw16/switch.py +++ b/homeassistant/components/hlk_sw16/switch.py @@ -1,4 +1,5 @@ """Support for HLK-SW16 switches.""" + from typing import Any from homeassistant.components.switch import SwitchEntity diff --git a/homeassistant/components/holiday/__init__.py b/homeassistant/components/holiday/__init__.py index 224b1b01294..4f2c593d38e 100644 --- a/homeassistant/components/holiday/__init__.py +++ b/homeassistant/components/holiday/__init__.py @@ -1,4 +1,5 @@ """The Holiday integration.""" + from __future__ import annotations from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/holiday/calendar.py b/homeassistant/components/holiday/calendar.py index e48cc11d677..57503b340d9 100644 --- a/homeassistant/components/holiday/calendar.py +++ b/homeassistant/components/holiday/calendar.py @@ -1,4 +1,5 @@ """Holiday Calendar.""" + from __future__ import annotations from datetime import datetime diff --git a/homeassistant/components/holiday/config_flow.py b/homeassistant/components/holiday/config_flow.py index 07da19167d7..a9b2f3e9772 100644 --- a/homeassistant/components/holiday/config_flow.py +++ b/homeassistant/components/holiday/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Holiday integration.""" + from __future__ import annotations from typing import Any @@ -7,9 +8,8 @@ from babel import Locale, UnknownLocaleError from holidays import list_supported_countries import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_COUNTRY -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.selector import ( CountrySelector, CountrySelectorConfig, @@ -23,10 +23,11 @@ from .const import CONF_PROVINCE, DOMAIN SUPPORTED_COUNTRIES = list_supported_countries(include_aliases=False) -class HolidayConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class HolidayConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Holiday.""" VERSION = 1 + config_entry: ConfigEntry | None def __init__(self) -> None: """Initialize the config flow.""" @@ -34,7 +35,7 @@ class HolidayConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" if user_input is not None: self.data = user_input @@ -71,7 +72,7 @@ class HolidayConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_province( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the province step.""" if user_input is not None: combined_input: dict[str, Any] = {**self.data, **user_input} @@ -109,3 +110,64 @@ class HolidayConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) return self.async_show_form(step_id="province", data_schema=province_schema) + + async def async_step_reconfigure( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle the re-configuration of a province.""" + self.config_entry = self.hass.config_entries.async_get_entry( + self.context["entry_id"] + ) + return await self.async_step_reconfigure_confirm() + + async def async_step_reconfigure_confirm( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle the re-configuration of a province.""" + assert self.config_entry + + if user_input is not None: + combined_input: dict[str, Any] = {**self.config_entry.data, **user_input} + + country = combined_input[CONF_COUNTRY] + province = combined_input.get(CONF_PROVINCE) + + self._async_abort_entries_match( + { + CONF_COUNTRY: country, + CONF_PROVINCE: province, + } + ) + + try: + locale = Locale.parse(self.hass.config.language, sep="-") + except UnknownLocaleError: + # Default to (US) English if language not recognized by babel + # Mainly an issue with English flavors such as "en-GB" + locale = Locale("en") + province_str = f", {province}" if province else "" + name = f"{locale.territories[country]}{province_str}" + + return self.async_update_reload_and_abort( + self.config_entry, + title=name, + data=combined_input, + reason="reconfigure_successful", + ) + + province_schema = vol.Schema( + { + vol.Optional(CONF_PROVINCE): SelectSelector( + SelectSelectorConfig( + options=SUPPORTED_COUNTRIES[ + self.config_entry.data[CONF_COUNTRY] + ], + mode=SelectSelectorMode.DROPDOWN, + ) + ) + } + ) + + return self.async_show_form( + step_id="reconfigure_confirm", data_schema=province_schema + ) diff --git a/homeassistant/components/holiday/const.py b/homeassistant/components/holiday/const.py index 5d2a567a488..ed283f82412 100644 --- a/homeassistant/components/holiday/const.py +++ b/homeassistant/components/holiday/const.py @@ -1,4 +1,5 @@ """Constants for the Holiday integration.""" + from typing import Final DOMAIN: Final = "holiday" diff --git a/homeassistant/components/holiday/manifest.json b/homeassistant/components/holiday/manifest.json index 5f78d961810..5a1edcd3c3f 100644 --- a/homeassistant/components/holiday/manifest.json +++ b/homeassistant/components/holiday/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/holiday", "iot_class": "local_polling", - "requirements": ["holidays==0.44", "babel==2.13.1"] + "requirements": ["holidays==0.46", "babel==2.13.1"] } diff --git a/homeassistant/components/holiday/strings.json b/homeassistant/components/holiday/strings.json index 53d403e790e..de013f44d60 100644 --- a/homeassistant/components/holiday/strings.json +++ b/homeassistant/components/holiday/strings.json @@ -2,7 +2,8 @@ "title": "Holiday", "config": { "abort": { - "already_configured": "Already configured. Only a single configuration for country/province combination possible." + "already_configured": "Already configured. Only a single configuration for country/province combination possible.", + "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]" }, "step": { "user": { @@ -14,6 +15,11 @@ "data": { "province": "Province" } + }, + "reconfigure_confirm": { + "data": { + "province": "[%key:component::holiday::config::step::province::data::province%]" + } } } } diff --git a/homeassistant/components/home_connect/__init__.py b/homeassistant/components/home_connect/__init__.py index 79303725249..ebfd6f91c76 100644 --- a/homeassistant/components/home_connect/__init__.py +++ b/homeassistant/components/home_connect/__init__.py @@ -1,4 +1,5 @@ """Support for BSH Home Connect appliances.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/home_connect/binary_sensor.py b/homeassistant/components/home_connect/binary_sensor.py index 93b90cbfbd3..84b02be1cc4 100644 --- a/homeassistant/components/home_connect/binary_sensor.py +++ b/homeassistant/components/home_connect/binary_sensor.py @@ -1,4 +1,5 @@ """Provides a binary sensor for Home Connect.""" + import logging from homeassistant.components.binary_sensor import BinarySensorEntity diff --git a/homeassistant/components/home_connect/config_flow.py b/homeassistant/components/home_connect/config_flow.py index 6239f1e3f60..f6616bf98ca 100644 --- a/homeassistant/components/home_connect/config_flow.py +++ b/homeassistant/components/home_connect/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Home Connect.""" + import logging from homeassistant.helpers import config_entry_oauth2_flow diff --git a/homeassistant/components/home_connect/light.py b/homeassistant/components/home_connect/light.py index 7e65fed034d..3b062fac66c 100644 --- a/homeassistant/components/home_connect/light.py +++ b/homeassistant/components/home_connect/light.py @@ -1,4 +1,5 @@ """Provides a light for Home Connect.""" + import logging from math import ceil from typing import Any diff --git a/homeassistant/components/home_connect/sensor.py b/homeassistant/components/home_connect/sensor.py index a01cae5862a..9bd48617fb3 100644 --- a/homeassistant/components/home_connect/sensor.py +++ b/homeassistant/components/home_connect/sensor.py @@ -1,4 +1,5 @@ """Provides a sensor for Home Connect.""" + from datetime import datetime, timedelta import logging from typing import cast diff --git a/homeassistant/components/home_connect/switch.py b/homeassistant/components/home_connect/switch.py index dbcbfde9dc2..1239395af2b 100644 --- a/homeassistant/components/home_connect/switch.py +++ b/homeassistant/components/home_connect/switch.py @@ -1,4 +1,5 @@ """Provides a switch for Home Connect.""" + import logging from typing import Any @@ -91,7 +92,7 @@ class HomeConnectPowerSwitch(HomeConnectEntity, SwitchEntity): """Power switch class for Home Connect.""" def __init__(self, device): - """Inititialize the entity.""" + """Initialize the entity.""" super().__init__(device, "Power") async def async_turn_on(self, **kwargs: Any) -> None: diff --git a/homeassistant/components/homeassistant/__init__.py b/homeassistant/components/homeassistant/__init__.py index 02a86150ff0..6d32f175a8a 100644 --- a/homeassistant/components/homeassistant/__init__.py +++ b/homeassistant/components/homeassistant/__init__.py @@ -1,4 +1,5 @@ """Integration providing core pieces of infrastructure.""" + import asyncio from collections.abc import Callable, Coroutine import itertools as it @@ -34,6 +35,11 @@ from homeassistant.helpers.service import ( from homeassistant.helpers.template import async_load_custom_templates from homeassistant.helpers.typing import ConfigType +# The scene integration will do a late import of scene +# so we want to make sure its loaded with the component +# so its already in memory when its imported so the import +# does not do blocking I/O in the event loop. +from . import scene as scene_pre_import # noqa: F401 from .const import ( DATA_EXPOSED_ENTITIES, DATA_STOP_HANDLER, diff --git a/homeassistant/components/homeassistant/const.py b/homeassistant/components/homeassistant/const.py index 871ea5a0371..d56ab4397d9 100644 --- a/homeassistant/components/homeassistant/const.py +++ b/homeassistant/components/homeassistant/const.py @@ -1,4 +1,5 @@ """Constants for the Homeassistant integration.""" + from typing import Final import homeassistant.core as ha diff --git a/homeassistant/components/homeassistant/exposed_entities.py b/homeassistant/components/homeassistant/exposed_entities.py index 38c7f8e8128..135b2847520 100644 --- a/homeassistant/components/homeassistant/exposed_entities.py +++ b/homeassistant/components/homeassistant/exposed_entities.py @@ -1,4 +1,5 @@ """Control which entities are exposed to voice assistants.""" + from __future__ import annotations from collections.abc import Callable, Mapping diff --git a/homeassistant/components/homeassistant/icons.json b/homeassistant/components/homeassistant/icons.json new file mode 100644 index 00000000000..ec4d5729918 --- /dev/null +++ b/homeassistant/components/homeassistant/icons.json @@ -0,0 +1,17 @@ +{ + "services": { + "check_config": "mdi:receipt-text-check", + "reload_core_config": "mdi:receipt-text-send", + "restart": "mdi:restart", + "set_location": "mdi:map-marker", + "stop": "mdi:stop", + "toggle": "mdi:toggle-switch", + "turn_on": "mdi:power-on", + "turn_off": "mdi:power-off", + "update_entity": "mdi:update", + "reload_custom_templates": "mdi:palette-swatch", + "reload_config_entry": "mdi:reload", + "save_persistent_states": "mdi:content-save", + "reload_all": "mdi:reload" + } +} diff --git a/homeassistant/components/homeassistant/logbook.py b/homeassistant/components/homeassistant/logbook.py index 60e8794799d..12d6c66b69c 100644 --- a/homeassistant/components/homeassistant/logbook.py +++ b/homeassistant/components/homeassistant/logbook.py @@ -1,4 +1,5 @@ """Describe homeassistant logbook events.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/homeassistant/scene.py b/homeassistant/components/homeassistant/scene.py index f8fd901a18a..1c4fee23198 100644 --- a/homeassistant/components/homeassistant/scene.py +++ b/homeassistant/components/homeassistant/scene.py @@ -1,4 +1,5 @@ """Allow users to set and activate scenes.""" + from __future__ import annotations from collections.abc import Mapping, ValuesView @@ -281,7 +282,6 @@ async def async_setup_platform( scene = platform.entities.get(entity_id) if scene is None: raise ServiceValidationError( - f"{entity_id} is not a valid scene entity_id", translation_domain=SCENE_DOMAIN, translation_key="entity_not_scene", translation_placeholders={ @@ -291,7 +291,6 @@ async def async_setup_platform( assert isinstance(scene, HomeAssistantScene) if not scene.from_service: raise ServiceValidationError( - f"The scene {entity_id} is not created with service `scene.create`", translation_domain=SCENE_DOMAIN, translation_key="entity_not_dynamically_created", translation_placeholders={ diff --git a/homeassistant/components/homeassistant/strings.json b/homeassistant/components/homeassistant/strings.json index 0b20f8698c2..37604c0e18e 100644 --- a/homeassistant/components/homeassistant/strings.json +++ b/homeassistant/components/homeassistant/strings.json @@ -163,28 +163,31 @@ "message": "Error importing config platform {domain}: {error}" }, "config_validation_err": { - "message": "Invalid config for integration {domain} at {config_file}, line {line}: {error}. Check the logs for more information." + "message": "Invalid config for integration {domain} at {config_file}, line {line}: {error}." }, "config_validator_unknown_err": { - "message": "Unknown error calling {domain} config validator. Check the logs for more information." + "message": "Unknown error calling {domain} config validator - {error}." }, "config_schema_unknown_err": { - "message": "Unknown error calling {domain} CONFIG_SCHEMA. Check the logs for more information." + "message": "Unknown error calling {domain} CONFIG_SCHEMA - {error}." }, - "integration_config_error": { + "multiple_integration_config_errors": { "message": "Failed to process config for integration {domain} due to multiple ({errors}) errors. Check the logs for more information." }, + "max_length_exceeded": { + "message": "Value {value} for property {property_name} has a maximum length of {max_length} characters." + }, "platform_component_load_err": { - "message": "Platform error: {domain} - {error}. Check the logs for more information." + "message": "Platform error: {domain} - {error}." }, "platform_component_load_exc": { - "message": "Platform error: {domain} - {error}. Check the logs for more information." + "message": "[%key:component::homeassistant::exceptions::platform_component_load_err::message%]" }, "platform_config_validation_err": { "message": "Invalid config for {domain} from integration {p_name} at file {config_file}, line {line}: {error}. Check the logs for more information." }, "platform_schema_validator_err": { - "message": "Unknown error when validating config for {domain} from integration {p_name}" + "message": "Unknown error when validating config for {domain} from integration {p_name} - {error}." }, "service_not_found": { "message": "Service {domain}.{service} not found." diff --git a/homeassistant/components/homeassistant/system_health.py b/homeassistant/components/homeassistant/system_health.py index 488328b6e4e..8a51b9cd418 100644 --- a/homeassistant/components/homeassistant/system_health.py +++ b/homeassistant/components/homeassistant/system_health.py @@ -1,4 +1,5 @@ """Provide info to system health.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/homeassistant/trigger.py b/homeassistant/components/homeassistant/trigger.py index 401da9d01e7..495cd07502a 100644 --- a/homeassistant/components/homeassistant/trigger.py +++ b/homeassistant/components/homeassistant/trigger.py @@ -1,8 +1,10 @@ """Home Assistant trigger dispatcher.""" -import importlib + +from typing import cast from homeassistant.const import CONF_PLATFORM from homeassistant.core import CALLBACK_TYPE, HomeAssistant +from homeassistant.helpers.importlib import async_import_module from homeassistant.helpers.trigger import ( TriggerActionType, TriggerInfo, @@ -11,15 +13,21 @@ from homeassistant.helpers.trigger import ( from homeassistant.helpers.typing import ConfigType -def _get_trigger_platform(config: ConfigType) -> TriggerProtocol: - return importlib.import_module(f"..triggers.{config[CONF_PLATFORM]}", __name__) +async def _async_get_trigger_platform( + hass: HomeAssistant, platform_name: str +) -> TriggerProtocol: + """Get trigger platform from cache or import it.""" + platform = await async_import_module( + hass, f"homeassistant.components.homeassistant.triggers.{platform_name}" + ) + return cast(TriggerProtocol, platform) async def async_validate_trigger_config( hass: HomeAssistant, config: ConfigType ) -> ConfigType: """Validate config.""" - platform = _get_trigger_platform(config) + platform = await _async_get_trigger_platform(hass, config[CONF_PLATFORM]) if hasattr(platform, "async_validate_trigger_config"): return await platform.async_validate_trigger_config(hass, config) @@ -33,5 +41,5 @@ async def async_attach_trigger( trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Attach trigger of specified platform.""" - platform = _get_trigger_platform(config) + platform = await _async_get_trigger_platform(hass, config[CONF_PLATFORM]) return await platform.async_attach_trigger(hass, config, action, trigger_info) diff --git a/homeassistant/components/homeassistant/triggers/event.py b/homeassistant/components/homeassistant/triggers/event.py index 37a91d06d1a..85bd2708d5e 100644 --- a/homeassistant/components/homeassistant/triggers/event.py +++ b/homeassistant/components/homeassistant/triggers/event.py @@ -1,13 +1,15 @@ """Offer event listening automation rules.""" + from __future__ import annotations -from collections.abc import ItemsView +from collections.abc import ItemsView, Mapping from typing import Any import voluptuous as vol -from homeassistant.const import CONF_EVENT_DATA, CONF_PLATFORM +from homeassistant.const import CONF_EVENT_DATA, CONF_PLATFORM, EVENT_STATE_REPORTED from homeassistant.core import CALLBACK_TYPE, Event, HassJob, HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv, template from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType @@ -15,10 +17,24 @@ from homeassistant.helpers.typing import ConfigType CONF_EVENT_TYPE = "event_type" CONF_EVENT_CONTEXT = "context" + +def _validate_event_types(value: Any) -> Any: + """Validate the event types. + + If the event types are templated, we check when attaching the trigger. + """ + templates: list[template.Template] = value + if any(tpl.is_static and tpl.template == EVENT_STATE_REPORTED for tpl in templates): + raise vol.Invalid(f"Can't listen to {EVENT_STATE_REPORTED} in event trigger") + return value + + TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend( { vol.Required(CONF_PLATFORM): "event", - vol.Required(CONF_EVENT_TYPE): vol.All(cv.ensure_list, [cv.template]), + vol.Required(CONF_EVENT_TYPE): vol.All( + cv.ensure_list, [cv.template], _validate_event_types + ), vol.Optional(CONF_EVENT_DATA): vol.All(dict, cv.template_complex), vol.Optional(CONF_EVENT_CONTEXT): vol.All(dict, cv.template_complex), } @@ -48,6 +64,10 @@ async def async_attach_trigger( event_types = template.render_complex( config[CONF_EVENT_TYPE], variables, limited=True ) + if EVENT_STATE_REPORTED in event_types: + raise HomeAssistantError( + f"Can't listen to {EVENT_STATE_REPORTED} in event trigger" + ) event_data_schema: vol.Schema | None = None event_data_items: ItemsView | None = None if CONF_EVENT_DATA in config: @@ -100,30 +120,18 @@ async def async_attach_trigger( job = HassJob(action, f"event trigger {trigger_info}") @callback - def filter_event(event: Event) -> bool: + def filter_event(event_data: Mapping[str, Any]) -> bool: """Filter events.""" try: # Check that the event data and context match the configured # schema if one was provided if event_data_items: # Fast path for simple items comparison - if not (event.data.items() >= event_data_items): + if not (event_data.items() >= event_data_items): return False elif event_data_schema: # Slow path for schema validation - event_data_schema(event.data) - - if event_context_items: - # Fast path for simple items comparison - # This is safe because we do not mutate the event context - # pylint: disable-next=protected-access - if not (event.context._as_dict.items() >= event_context_items): - return False - elif event_context_schema: - # Slow path for schema validation - # This is safe because we make a copy of the event context - # pylint: disable-next=protected-access - event_context_schema(dict(event.context._as_dict)) + event_data_schema(event_data) except vol.Invalid: # If event doesn't match, skip event return False @@ -132,6 +140,22 @@ async def async_attach_trigger( @callback def handle_event(event: Event) -> None: """Listen for events and calls the action when data matches.""" + if event_context_items: + # Fast path for simple items comparison + # This is safe because we do not mutate the event context + # pylint: disable-next=protected-access + if not (event.context._as_dict.items() >= event_context_items): + return + elif event_context_schema: + try: + # Slow path for schema validation + # This is safe because we make a copy of the event context + # pylint: disable-next=protected-access + event_context_schema(dict(event.context._as_dict)) + except vol.Invalid: + # If event doesn't match, skip event + return + hass.async_run_hass_job( job, { @@ -145,9 +169,10 @@ async def async_attach_trigger( event.context, ) + event_filter = filter_event if event_data_items or event_data_schema else None removes = [ hass.bus.async_listen( - event_type, handle_event, event_filter=filter_event, run_immediately=True + event_type, handle_event, event_filter=event_filter, run_immediately=True ) for event_type in event_types ] diff --git a/homeassistant/components/homeassistant/triggers/homeassistant.py b/homeassistant/components/homeassistant/triggers/homeassistant.py index 84aafb44808..025ca661ac2 100644 --- a/homeassistant/components/homeassistant/triggers/homeassistant.py +++ b/homeassistant/components/homeassistant/triggers/homeassistant.py @@ -1,4 +1,5 @@ """Offer Home Assistant core automation rules.""" + import voluptuous as vol from homeassistant.const import CONF_EVENT, CONF_PLATFORM @@ -7,12 +8,14 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType +from ..const import DOMAIN + EVENT_START = "start" EVENT_SHUTDOWN = "shutdown" TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend( { - vol.Required(CONF_PLATFORM): "homeassistant", + vol.Required(CONF_PLATFORM): DOMAIN, vol.Required(CONF_EVENT): vol.Any(EVENT_START, EVENT_SHUTDOWN), } ) @@ -35,7 +38,7 @@ async def async_attach_trigger( { "trigger": { **trigger_data, - "platform": "homeassistant", + "platform": DOMAIN, "event": event, "description": "Home Assistant stopping", } @@ -50,7 +53,7 @@ async def async_attach_trigger( { "trigger": { **trigger_data, - "platform": "homeassistant", + "platform": DOMAIN, "event": event, "description": "Home Assistant starting", } diff --git a/homeassistant/components/homeassistant/triggers/numeric_state.py b/homeassistant/components/homeassistant/triggers/numeric_state.py index dad57bbcdb3..2575af41401 100644 --- a/homeassistant/components/homeassistant/triggers/numeric_state.py +++ b/homeassistant/components/homeassistant/triggers/numeric_state.py @@ -1,4 +1,5 @@ """Offer numeric state listening automation rules.""" + from __future__ import annotations from collections.abc import Callable @@ -18,7 +19,14 @@ from homeassistant.const import ( CONF_PLATFORM, CONF_VALUE_TEMPLATE, ) -from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, State, callback +from homeassistant.core import ( + CALLBACK_TYPE, + Event, + HassJob, + HomeAssistant, + State, + callback, +) from homeassistant.helpers import ( condition, config_validation as cv, @@ -31,7 +39,7 @@ from homeassistant.helpers.event import ( async_track_state_change_event, ) from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo -from homeassistant.helpers.typing import ConfigType, EventType +from homeassistant.helpers.typing import ConfigType _T = TypeVar("_T", bound=dict[str, Any]) @@ -151,7 +159,7 @@ async def async_attach_trigger( ) @callback - def state_automation_listener(event: EventType[EventStateChangedData]) -> None: + def state_automation_listener(event: Event[EventStateChangedData]) -> None: """Listen for state changes and calls action.""" entity_id = event.data["entity_id"] from_s = event.data["old_state"] diff --git a/homeassistant/components/homeassistant/triggers/state.py b/homeassistant/components/homeassistant/triggers/state.py index 061c2468c30..6f3183e2b40 100644 --- a/homeassistant/components/homeassistant/triggers/state.py +++ b/homeassistant/components/homeassistant/triggers/state.py @@ -1,4 +1,5 @@ """Offer state listening automation rules.""" + from __future__ import annotations from collections.abc import Callable @@ -9,7 +10,14 @@ import voluptuous as vol from homeassistant import exceptions from homeassistant.const import CONF_ATTRIBUTE, CONF_FOR, CONF_PLATFORM, MATCH_ALL -from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, State, callback +from homeassistant.core import ( + CALLBACK_TYPE, + Event, + HassJob, + HomeAssistant, + State, + callback, +) from homeassistant.helpers import ( config_validation as cv, entity_registry as er, @@ -22,7 +30,7 @@ from homeassistant.helpers.event import ( process_state_match, ) from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo -from homeassistant.helpers.typing import ConfigType, EventType +from homeassistant.helpers.typing import ConfigType _LOGGER = logging.getLogger(__name__) @@ -124,7 +132,7 @@ async def async_attach_trigger( _variables = trigger_info["variables"] or {} @callback - def state_automation_listener(event: EventType[EventStateChangedData]) -> None: + def state_automation_listener(event: Event[EventStateChangedData]) -> None: """Listen for state changes and calls action.""" entity = event.data["entity_id"] from_s = event.data["old_state"] diff --git a/homeassistant/components/homeassistant/triggers/time.py b/homeassistant/components/homeassistant/triggers/time.py index 3cb8809a7ad..b1d19d54795 100644 --- a/homeassistant/components/homeassistant/triggers/time.py +++ b/homeassistant/components/homeassistant/triggers/time.py @@ -1,4 +1,5 @@ """Offer time listening automation rules.""" + from datetime import datetime from functools import partial @@ -12,7 +13,14 @@ from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, State, callback +from homeassistant.core import ( + CALLBACK_TYPE, + Event, + HassJob, + HomeAssistant, + State, + callback, +) from homeassistant.helpers import config_validation as cv from homeassistant.helpers.event import ( EventStateChangedData, @@ -21,7 +29,7 @@ from homeassistant.helpers.event import ( async_track_time_change, ) from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo -from homeassistant.helpers.typing import ConfigType, EventType +from homeassistant.helpers.typing import ConfigType import homeassistant.util.dt as dt_util _TIME_TRIGGER_SCHEMA = vol.Any( @@ -71,7 +79,7 @@ async def async_attach_trigger( ) @callback - def update_entity_trigger_event(event: EventType[EventStateChangedData]) -> None: + def update_entity_trigger_event(event: Event[EventStateChangedData]) -> None: """update_entity_trigger from the event.""" return update_entity_trigger(event.data["entity_id"], event.data["new_state"]) diff --git a/homeassistant/components/homeassistant/triggers/time_pattern.py b/homeassistant/components/homeassistant/triggers/time_pattern.py index d8ac55eb04f..df49a79bcb6 100644 --- a/homeassistant/components/homeassistant/triggers/time_pattern.py +++ b/homeassistant/components/homeassistant/triggers/time_pattern.py @@ -1,4 +1,5 @@ """Offer time listening automation rules.""" + from __future__ import annotations from datetime import datetime diff --git a/homeassistant/components/homeassistant_alerts/__init__.py b/homeassistant/components/homeassistant_alerts/__init__.py index f391b990761..338d8679b19 100644 --- a/homeassistant/components/homeassistant_alerts/__init__.py +++ b/homeassistant/components/homeassistant_alerts/__init__.py @@ -1,4 +1,5 @@ """The Home Assistant alerts integration.""" + from __future__ import annotations import dataclasses @@ -97,11 +98,14 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: function=coordinator.async_refresh, ) - async def _component_loaded(_: Event) -> None: - await refresh_debouncer.async_call() + @callback + def _component_loaded(_: Event) -> None: + refresh_debouncer.async_schedule_call() await coordinator.async_refresh() - hass.bus.async_listen(EVENT_COMPONENT_LOADED, _component_loaded) + hass.bus.async_listen( + EVENT_COMPONENT_LOADED, _component_loaded, run_immediately=True + ) async_at_start(hass, initial_refresh) diff --git a/homeassistant/components/homeassistant_green/__init__.py b/homeassistant/components/homeassistant_green/__init__.py index ed86723ab94..2d35b5bbed3 100644 --- a/homeassistant/components/homeassistant_green/__init__.py +++ b/homeassistant/components/homeassistant_green/__init__.py @@ -1,4 +1,5 @@ """The Home Assistant Green integration.""" + from __future__ import annotations from homeassistant.components.hassio import get_os_info, is_hassio diff --git a/homeassistant/components/homeassistant_green/config_flow.py b/homeassistant/components/homeassistant_green/config_flow.py index c3491de430e..4b71c7f1056 100644 --- a/homeassistant/components/homeassistant_green/config_flow.py +++ b/homeassistant/components/homeassistant_green/config_flow.py @@ -1,4 +1,5 @@ """Config flow for the Home Assistant Green integration.""" + from __future__ import annotations import asyncio @@ -14,9 +15,13 @@ from homeassistant.components.hassio import ( async_set_green_settings, is_hassio, ) -from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import selector from .const import DOMAIN @@ -46,7 +51,9 @@ class HomeAssistantGreenConfigFlow(ConfigFlow, domain=DOMAIN): """Return the options flow.""" return HomeAssistantGreenOptionsFlow() - async def async_step_system(self, data: dict[str, Any] | None = None) -> FlowResult: + async def async_step_system( + self, data: dict[str, Any] | None = None + ) -> ConfigFlowResult: """Handle the initial step.""" if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") @@ -61,7 +68,7 @@ class HomeAssistantGreenOptionsFlow(OptionsFlow): async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Manage the options.""" if not is_hassio(self.hass): return self.async_abort(reason="not_hassio") @@ -70,7 +77,7 @@ class HomeAssistantGreenOptionsFlow(OptionsFlow): async def async_step_hardware_settings( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle hardware settings.""" if user_input is not None: diff --git a/homeassistant/components/homeassistant_green/hardware.py b/homeassistant/components/homeassistant_green/hardware.py index c7b1641c09c..0537d17620b 100644 --- a/homeassistant/components/homeassistant_green/hardware.py +++ b/homeassistant/components/homeassistant_green/hardware.py @@ -1,4 +1,5 @@ """The Home Assistant Green hardware platform.""" + from __future__ import annotations from homeassistant.components.hardware.models import BoardInfo, HardwareInfo diff --git a/homeassistant/components/homeassistant_hardware/__init__.py b/homeassistant/components/homeassistant_hardware/__init__.py index 057cdd3b0db..c33dabe1ec8 100644 --- a/homeassistant/components/homeassistant_hardware/__init__.py +++ b/homeassistant/components/homeassistant_hardware/__init__.py @@ -1,4 +1,5 @@ """The Home Assistant Hardware integration.""" + from __future__ import annotations from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/homeassistant_hardware/silabs_multiprotocol_addon.py b/homeassistant/components/homeassistant_hardware/silabs_multiprotocol_addon.py index ef953213fc8..535d1706737 100644 --- a/homeassistant/components/homeassistant_hardware/silabs_multiprotocol_addon.py +++ b/homeassistant/components/homeassistant_hardware/silabs_multiprotocol_addon.py @@ -1,4 +1,5 @@ """Manage the Silicon Labs Multiprotocol add-on.""" + from __future__ import annotations from abc import ABC, abstractmethod @@ -10,7 +11,6 @@ from typing import Any, Protocol import voluptuous as vol import yarl -from homeassistant import config_entries from homeassistant.components.hassio import ( AddonError, AddonInfo, @@ -19,8 +19,14 @@ from homeassistant.components.hassio import ( hostname_from_addon_slug, is_hassio, ) +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlowResult, + OptionsFlow, + OptionsFlowManager, +) from homeassistant.core import HomeAssistant, callback -from homeassistant.data_entry_flow import AbortFlow, FlowResult +from homeassistant.data_entry_flow import AbortFlow from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.integration_platform import ( async_process_integration_platforms, @@ -138,7 +144,10 @@ class MultiprotocolAddonManager(WaitingAddonManager): async def async_setup(self) -> None: """Set up the manager.""" await async_process_integration_platforms( - self._hass, "silabs_multiprotocol", self._register_multipan_platform + self._hass, + "silabs_multiprotocol", + self._register_multipan_platform, + wait_for_platforms=True, ) await self.async_load() @@ -295,10 +304,10 @@ def is_multiprotocol_url(url: str) -> bool: return parsed.host == hostname -class OptionsFlowHandler(config_entries.OptionsFlow, ABC): +class OptionsFlowHandler(OptionsFlow, ABC): """Handle an options flow for the Silicon Labs Multiprotocol add-on.""" - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + def __init__(self, config_entry: ConfigEntry) -> None: """Set up the options flow.""" # pylint: disable-next=import-outside-toplevel from homeassistant.components.zha.radio_manager import ( @@ -334,7 +343,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow, ABC): """Return the ZHA name.""" @property - def flow_manager(self) -> config_entries.OptionsFlowManager: + def flow_manager(self) -> OptionsFlowManager: """Return the correct flow manager.""" return self.hass.config_entries.options @@ -363,7 +372,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow, ABC): async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Manage the options.""" if not is_hassio(self.hass): return self.async_abort(reason="not_hassio") @@ -372,7 +381,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow, ABC): async def async_step_on_supervisor( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle logic when on Supervisor host.""" multipan_manager = await get_multiprotocol_addon_manager(self.hass) addon_info = await self._async_get_addon_info(multipan_manager) @@ -383,7 +392,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow, ABC): async def async_step_addon_not_installed( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle logic when the addon is not yet installed.""" if user_input is None: return self.async_show_form( @@ -400,7 +409,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow, ABC): async def async_step_install_addon( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Install Silicon Labs Multiprotocol add-on.""" multipan_manager = await get_multiprotocol_addon_manager(self.hass) @@ -430,7 +439,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow, ABC): async def async_step_install_failed( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Add-on installation failed.""" multipan_manager = await get_multiprotocol_addon_manager(self.hass) return self.async_abort( @@ -440,7 +449,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow, ABC): async def async_step_configure_addon( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Configure the Silicon Labs Multiprotocol add-on.""" # pylint: disable-next=import-outside-toplevel from homeassistant.components.zha import DOMAIN as ZHA_DOMAIN @@ -509,7 +518,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow, ABC): async def async_step_start_addon( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Start Silicon Labs Multiprotocol add-on.""" multipan_manager = await get_multiprotocol_addon_manager(self.hass) @@ -538,7 +547,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow, ABC): async def async_step_start_failed( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Add-on start failed.""" multipan_manager = await get_multiprotocol_addon_manager(self.hass) return self.async_abort( @@ -548,7 +557,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow, ABC): async def async_step_finish_addon_setup( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Prepare info needed to complete the config entry update.""" # Always reload entry after installing the addon. self.hass.async_create_task( @@ -567,7 +576,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow, ABC): async def async_step_addon_installed_other_device( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Show dialog explaining the addon is in use by another device.""" if user_input is None: return self.async_show_form(step_id="addon_installed_other_device") @@ -575,7 +584,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow, ABC): async def async_step_addon_installed( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle logic when the addon is already installed.""" multipan_manager = await get_multiprotocol_addon_manager(self.hass) addon_info = await self._async_get_addon_info(multipan_manager) @@ -587,7 +596,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow, ABC): async def async_step_addon_menu( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Show menu options for the addon.""" return self.async_show_menu( step_id="addon_menu", @@ -599,7 +608,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow, ABC): async def async_step_reconfigure_addon( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Reconfigure the addon.""" multipan_manager = await get_multiprotocol_addon_manager(self.hass) active_platforms = await multipan_manager.async_active_platforms() @@ -609,7 +618,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow, ABC): async def async_step_notify_unknown_multipan_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Notify that there may be unknown multipan platforms.""" if user_input is None: return self.async_show_form( @@ -619,7 +628,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow, ABC): async def async_step_change_channel( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Change the channel.""" multipan_manager = await get_multiprotocol_addon_manager(self.hass) if user_input is None: @@ -651,7 +660,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow, ABC): async def async_step_notify_channel_change( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Notify that the channel change will take about five minutes.""" if user_input is None: return self.async_show_form( @@ -664,7 +673,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow, ABC): async def async_step_uninstall_addon( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Uninstall the addon and revert the firmware.""" if user_input is None: return self.async_show_form( @@ -681,7 +690,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow, ABC): async def async_step_firmware_revert( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Install the flasher addon, if necessary.""" flasher_manager = get_flasher_addon_manager(self.hass) @@ -701,7 +710,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow, ABC): async def async_step_install_flasher_addon( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Show progress dialog for installing flasher addon.""" flasher_manager = get_flasher_addon_manager(self.hass) addon_info = await self._async_get_addon_info(flasher_manager) @@ -734,7 +743,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow, ABC): async def async_step_configure_flasher_addon( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Perform initial backup and reconfigure ZHA.""" # pylint: disable-next=import-outside-toplevel from homeassistant.components.zha import DOMAIN as ZHA_DOMAIN @@ -794,7 +803,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow, ABC): async def async_step_uninstall_multiprotocol_addon( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Uninstall Silicon Labs Multiprotocol add-on.""" multipan_manager = await get_multiprotocol_addon_manager(self.hass) @@ -821,7 +830,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow, ABC): async def async_step_start_flasher_addon( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Start Silicon Labs Flasher add-on.""" flasher_manager = get_flasher_addon_manager(self.hass) @@ -856,7 +865,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow, ABC): async def async_step_flasher_failed( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Flasher add-on start failed.""" flasher_manager = get_flasher_addon_manager(self.hass) return self.async_abort( @@ -866,7 +875,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow, ABC): async def async_step_flashing_complete( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Finish flashing and update the config entry.""" flasher_manager = get_flasher_addon_manager(self.hass) await flasher_manager.async_uninstall_addon_waiting() diff --git a/homeassistant/components/homeassistant_sky_connect/__init__.py b/homeassistant/components/homeassistant_sky_connect/__init__.py index 4880d2e375f..1ee4710769b 100644 --- a/homeassistant/components/homeassistant_sky_connect/__init__.py +++ b/homeassistant/components/homeassistant_sky_connect/__init__.py @@ -1,4 +1,5 @@ """The Home Assistant SkyConnect integration.""" + from __future__ import annotations from homeassistant.components import usb @@ -13,7 +14,7 @@ from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError from homeassistant.helpers import discovery_flow from .const import DOMAIN -from .util import get_usb_service_info +from .util import get_hardware_variant, get_usb_service_info async def _async_usb_scan_done(hass: HomeAssistant, entry: ConfigEntry) -> None: @@ -45,8 +46,9 @@ async def _async_usb_scan_done(hass: HomeAssistant, entry: ConfigEntry) -> None: ) return + hw_variant = get_hardware_variant(entry) hw_discovery_data = { - "name": "SkyConnect Multiprotocol", + "name": f"{hw_variant.short_name} Multiprotocol", "port": { "path": get_zigbee_socket(), }, diff --git a/homeassistant/components/homeassistant_sky_connect/config_flow.py b/homeassistant/components/homeassistant_sky_connect/config_flow.py index fce731777b1..3a3d32c2888 100644 --- a/homeassistant/components/homeassistant_sky_connect/config_flow.py +++ b/homeassistant/components/homeassistant_sky_connect/config_flow.py @@ -1,16 +1,16 @@ """Config flow for the Home Assistant SkyConnect integration.""" + from __future__ import annotations from typing import Any from homeassistant.components import usb from homeassistant.components.homeassistant_hardware import silabs_multiprotocol_addon -from homeassistant.config_entries import ConfigEntry, ConfigFlow +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult -from .const import DOMAIN -from .util import get_usb_service_info +from .const import DOMAIN, HardwareVariant +from .util import get_hardware_variant, get_usb_service_info class HomeAssistantSkyConnectConfigFlow(ConfigFlow, domain=DOMAIN): @@ -26,7 +26,9 @@ class HomeAssistantSkyConnectConfigFlow(ConfigFlow, domain=DOMAIN): """Return the options flow.""" return HomeAssistantSkyConnectOptionsFlow(config_entry) - async def async_step_usb(self, discovery_info: usb.UsbServiceInfo) -> FlowResult: + async def async_step_usb( + self, discovery_info: usb.UsbServiceInfo + ) -> ConfigFlowResult: """Handle usb discovery.""" device = discovery_info.device vid = discovery_info.vid @@ -37,8 +39,12 @@ class HomeAssistantSkyConnectConfigFlow(ConfigFlow, domain=DOMAIN): unique_id = f"{vid}:{pid}_{serial_number}_{manufacturer}_{description}" if await self.async_set_unique_id(unique_id): self._abort_if_unique_id_configured(updates={"device": device}) + + assert description is not None + hw_variant = HardwareVariant.from_usb_product_name(description) + return self.async_create_entry( - title="Home Assistant SkyConnect", + title=hw_variant.full_name, data={ "device": device, "vid": vid, @@ -74,10 +80,15 @@ class HomeAssistantSkyConnectOptionsFlow(silabs_multiprotocol_addon.OptionsFlowH """ return {"usb": get_usb_service_info(self.config_entry)} + @property + def _hw_variant(self) -> HardwareVariant: + """Return the hardware variant.""" + return get_hardware_variant(self.config_entry) + def _zha_name(self) -> str: """Return the ZHA name.""" - return "SkyConnect Multiprotocol" + return f"{self._hw_variant.short_name} Multiprotocol" def _hardware_name(self) -> str: """Return the name of the hardware.""" - return "Home Assistant SkyConnect" + return self._hw_variant.full_name diff --git a/homeassistant/components/homeassistant_sky_connect/const.py b/homeassistant/components/homeassistant_sky_connect/const.py index c504cead9cb..1dd1471c470 100644 --- a/homeassistant/components/homeassistant_sky_connect/const.py +++ b/homeassistant/components/homeassistant_sky_connect/const.py @@ -1,3 +1,41 @@ """Constants for the Home Assistant SkyConnect integration.""" +import dataclasses +import enum +from typing import Self + DOMAIN = "homeassistant_sky_connect" + + +@dataclasses.dataclass(frozen=True) +class VariantInfo: + """Hardware variant information.""" + + usb_product_name: str + short_name: str + full_name: str + + +class HardwareVariant(VariantInfo, enum.Enum): + """Hardware variants.""" + + SKYCONNECT = ( + "SkyConnect v1.0", + "SkyConnect", + "Home Assistant SkyConnect", + ) + + CONNECT_ZBT1 = ( + "Home Assistant Connect ZBT-1", + "Connect ZBT-1", + "Home Assistant Connect ZBT-1", + ) + + @classmethod + def from_usb_product_name(cls, usb_product_name: str) -> Self: + """Get the hardware variant from the USB product name.""" + for variant in cls: + if variant.value.usb_product_name == usb_product_name: + return variant + + raise ValueError(f"Unknown SkyConnect product name: {usb_product_name}") diff --git a/homeassistant/components/homeassistant_sky_connect/hardware.py b/homeassistant/components/homeassistant_sky_connect/hardware.py index bd752278397..a9abeb27737 100644 --- a/homeassistant/components/homeassistant_sky_connect/hardware.py +++ b/homeassistant/components/homeassistant_sky_connect/hardware.py @@ -1,13 +1,14 @@ """The Home Assistant SkyConnect hardware platform.""" + from __future__ import annotations from homeassistant.components.hardware.models import HardwareInfo, USBInfo from homeassistant.core import HomeAssistant, callback from .const import DOMAIN +from .util import get_hardware_variant DOCUMENTATION_URL = "https://skyconnect.home-assistant.io/documentation/" -DONGLE_NAME = "Home Assistant SkyConnect" @callback @@ -26,7 +27,7 @@ def async_info(hass: HomeAssistant) -> list[HardwareInfo]: manufacturer=entry.data["manufacturer"], description=entry.data["description"], ), - name=DONGLE_NAME, + name=get_hardware_variant(entry).full_name, url=DOCUMENTATION_URL, ) for entry in entries diff --git a/homeassistant/components/homeassistant_sky_connect/manifest.json b/homeassistant/components/homeassistant_sky_connect/manifest.json index 31685183a61..f56fd24de61 100644 --- a/homeassistant/components/homeassistant_sky_connect/manifest.json +++ b/homeassistant/components/homeassistant_sky_connect/manifest.json @@ -12,6 +12,12 @@ "pid": "EA60", "description": "*skyconnect v1.0*", "known_devices": ["SkyConnect v1.0"] + }, + { + "vid": "10C4", + "pid": "EA60", + "description": "*home assistant connect zbt-1*", + "known_devices": ["Home Assistant Connect ZBT-1"] } ] } diff --git a/homeassistant/components/homeassistant_sky_connect/util.py b/homeassistant/components/homeassistant_sky_connect/util.py index 7a87964f5c4..e1de1d3b442 100644 --- a/homeassistant/components/homeassistant_sky_connect/util.py +++ b/homeassistant/components/homeassistant_sky_connect/util.py @@ -1,9 +1,12 @@ """Utility functions for Home Assistant SkyConnect integration.""" + from __future__ import annotations from homeassistant.components import usb from homeassistant.config_entries import ConfigEntry +from .const import HardwareVariant + def get_usb_service_info(config_entry: ConfigEntry) -> usb.UsbServiceInfo: """Return UsbServiceInfo.""" @@ -15,3 +18,8 @@ def get_usb_service_info(config_entry: ConfigEntry) -> usb.UsbServiceInfo: manufacturer=config_entry.data["manufacturer"], description=config_entry.data["description"], ) + + +def get_hardware_variant(config_entry: ConfigEntry) -> HardwareVariant: + """Get the hardware variant from the config entry.""" + return HardwareVariant.from_usb_product_name(config_entry.data["description"]) diff --git a/homeassistant/components/homeassistant_yellow/__init__.py b/homeassistant/components/homeassistant_yellow/__init__.py index 84ad464e779..14c2de2c9a1 100644 --- a/homeassistant/components/homeassistant_yellow/__init__.py +++ b/homeassistant/components/homeassistant_yellow/__init__.py @@ -1,4 +1,5 @@ """The Home Assistant Yellow integration.""" + from __future__ import annotations from homeassistant.components.hassio import get_os_info, is_hassio diff --git a/homeassistant/components/homeassistant_yellow/config_flow.py b/homeassistant/components/homeassistant_yellow/config_flow.py index 7681d6d3847..d2212a968db 100644 --- a/homeassistant/components/homeassistant_yellow/config_flow.py +++ b/homeassistant/components/homeassistant_yellow/config_flow.py @@ -1,4 +1,5 @@ """Config flow for the Home Assistant Yellow integration.""" + from __future__ import annotations import asyncio @@ -15,9 +16,8 @@ from homeassistant.components.hassio import ( async_set_yellow_settings, ) from homeassistant.components.homeassistant_hardware import silabs_multiprotocol_addon -from homeassistant.config_entries import ConfigEntry, ConfigFlow +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import selector from .const import DOMAIN, ZHA_HW_DISCOVERY_DATA @@ -46,7 +46,9 @@ class HomeAssistantYellowConfigFlow(ConfigFlow, domain=DOMAIN): """Return the options flow.""" return HomeAssistantYellowOptionsFlow(config_entry) - async def async_step_system(self, data: dict[str, Any] | None = None) -> FlowResult: + async def async_step_system( + self, data: dict[str, Any] | None = None + ) -> ConfigFlowResult: """Handle the initial step.""" if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") @@ -61,11 +63,11 @@ class HomeAssistantYellowOptionsFlow(silabs_multiprotocol_addon.OptionsFlowHandl async def async_step_on_supervisor( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle logic when on Supervisor host.""" return await self.async_step_main_menu() - async def async_step_main_menu(self, _: None = None) -> FlowResult: + async def async_step_main_menu(self, _: None = None) -> ConfigFlowResult: """Show the main menu.""" return self.async_show_menu( step_id="main_menu", @@ -77,7 +79,7 @@ class HomeAssistantYellowOptionsFlow(silabs_multiprotocol_addon.OptionsFlowHandl async def async_step_hardware_settings( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle hardware settings.""" if user_input is not None: @@ -108,7 +110,7 @@ class HomeAssistantYellowOptionsFlow(silabs_multiprotocol_addon.OptionsFlowHandl async def async_step_reboot_menu( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm reboot host.""" return self.async_show_menu( step_id="reboot_menu", @@ -120,20 +122,20 @@ class HomeAssistantYellowOptionsFlow(silabs_multiprotocol_addon.OptionsFlowHandl async def async_step_reboot_now( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Reboot now.""" await async_reboot_host(self.hass) return self.async_create_entry(data={}) async def async_step_reboot_later( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Reboot later.""" return self.async_create_entry(data={}) async def async_step_multipan_settings( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle multipan settings.""" return await super().async_step_on_supervisor(user_input) diff --git a/homeassistant/components/homeassistant_yellow/hardware.py b/homeassistant/components/homeassistant_yellow/hardware.py index 0749ca8edc6..2b9ee0673db 100644 --- a/homeassistant/components/homeassistant_yellow/hardware.py +++ b/homeassistant/components/homeassistant_yellow/hardware.py @@ -1,4 +1,5 @@ """The Home Assistant Yellow hardware platform.""" + from __future__ import annotations from homeassistant.components.hardware.models import BoardInfo, HardwareInfo diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index a60f55e8bb0..294dc7f33a6 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -1,4 +1,5 @@ """Support for Apple HomeKit.""" + from __future__ import annotations import asyncio @@ -7,9 +8,11 @@ from copy import deepcopy import ipaddress import logging import os +import socket from typing import Any, cast from aiohttp import web +from pyhap import util as pyhap_util from pyhap.characteristic import Characteristic from pyhap.const import STANDALONE_AID from pyhap.loader import get_loader @@ -26,7 +29,7 @@ from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN from homeassistant.components.device_automation.trigger import ( async_validate_trigger_config, ) -from homeassistant.components.http import HomeAssistantView +from homeassistant.components.http import KEY_HASS, HomeAssistantView from homeassistant.components.humidifier import DOMAIN as HUMIDIFIER_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorDeviceClass from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry @@ -43,13 +46,11 @@ from homeassistant.const import ( CONF_IP_ADDRESS, CONF_NAME, CONF_PORT, - EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP, SERVICE_RELOAD, ) from homeassistant.core import ( CALLBACK_TYPE, - CoreState, HomeAssistant, ServiceCall, State, @@ -73,6 +74,7 @@ from homeassistant.helpers.service import ( async_extract_referenced_entity_ids, async_register_admin_service, ) +from homeassistant.helpers.start import async_at_started from homeassistant.helpers.typing import ConfigType from homeassistant.loader import IntegrationNotFound, async_get_integration @@ -119,6 +121,7 @@ from .const import ( SERVICE_HOMEKIT_RESET_ACCESSORY, SERVICE_HOMEKIT_UNPAIR, SHUTDOWN_TIMEOUT, + SIGNAL_RELOAD_ENTITIES, ) from .iidmanager import AccessoryIIDStorage from .models import HomeKitEntryData @@ -149,6 +152,8 @@ PORT_CLEANUP_CHECK_INTERVAL_SECS = 1 _HOMEKIT_CONFIG_UPDATE_TIME = ( 10 # number of seconds to wait for homekit to see the c# change ) +_HAS_IPV6 = hasattr(socket, "AF_INET6") +_DEFAULT_BIND = ["0.0.0.0", "::"] if _HAS_IPV6 else ["0.0.0.0"] def _has_all_unique_names_and_ports( @@ -308,7 +313,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: _LOGGER.debug("Begin setup HomeKit for %s", name) # ip_address and advertise_ip are yaml only - ip_address = conf.get(CONF_IP_ADDRESS, [None]) + ip_address = conf.get(CONF_IP_ADDRESS, _DEFAULT_BIND) advertise_ips: list[str] = conf.get( CONF_ADVERTISE_IP ) or await network.async_get_announce_addresses(hass) @@ -346,7 +351,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.async_on_unload(entry.add_update_listener(_async_update_listener)) entry.async_on_unload( - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, homekit.async_stop) + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, homekit.async_stop, run_immediately=True + ) ) entry_data = HomeKitEntryData( @@ -354,10 +361,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) hass.data[DOMAIN][entry.entry_id] = entry_data - if hass.state is CoreState.running: + async def _async_start_homekit(hass: HomeAssistant) -> None: await homekit.async_start() - else: - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, homekit.async_start) + + entry.async_on_unload(async_at_started(hass, _async_start_homekit)) return True @@ -379,7 +386,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await homekit.async_stop() logged_shutdown_wait = False - for _ in range(0, SHUTDOWN_TIMEOUT): + for _ in range(SHUTDOWN_TIMEOUT): if async_port_is_available(entry.data[CONF_PORT]): break @@ -545,11 +552,15 @@ class HomeKit: self._reset_lock = asyncio.Lock() self._cancel_reload_dispatcher: CALLBACK_TYPE | None = None - def setup(self, async_zeroconf_instance: AsyncZeroconf, uuid: str) -> None: - """Set up bridge and accessory driver.""" + def setup(self, async_zeroconf_instance: AsyncZeroconf, uuid: str) -> bool: + """Set up bridge and accessory driver. + + Returns True if data was loaded from disk + + Returns False if the persistent data was not loaded + """ assert self.iid_storage is not None persist_file = get_persist_fullpath_for_entry_id(self.hass, self._entry_id) - self.driver = HomeDriver( self.hass, self._entry_id, @@ -565,11 +576,15 @@ class HomeKit: loader=get_loader(), iid_storage=self.iid_storage, ) - # If we do not load the mac address will be wrong # as pyhap uses a random one until state is restored if os.path.exists(persist_file): self.driver.load() + return True + + # If there is no persist file, we need to generate a mac + self.driver.state.mac = pyhap_util.generate_mac() + return False async def async_reset_accessories(self, entity_ids: Iterable[str]) -> None: """Reset the accessory to load the latest configuration.""" @@ -789,18 +804,10 @@ class HomeKit: """Configure accessories for the included states.""" dev_reg = dr.async_get(self.hass) ent_reg = er.async_get(self.hass) - device_lookup = ent_reg.async_get_device_class_lookup( - { - (BINARY_SENSOR_DOMAIN, BinarySensorDeviceClass.BATTERY_CHARGING), - (BINARY_SENSOR_DOMAIN, BinarySensorDeviceClass.MOTION), - (BINARY_SENSOR_DOMAIN, BinarySensorDeviceClass.OCCUPANCY), - (SENSOR_DOMAIN, SensorDeviceClass.BATTERY), - (SENSOR_DOMAIN, SensorDeviceClass.HUMIDITY), - } - ) - + device_lookup: dict[str, dict[tuple[str, str | None], str]] = {} entity_states: list[State] = [] entity_filter = self._filter.get_filter() + entries = ent_reg.entities for state in self.hass.states.async_all(): entity_id = state.entity_id if not entity_filter(entity_id): @@ -816,7 +823,18 @@ class HomeKit: await self._async_set_device_info_attributes( ent_reg_ent, dev_reg, entity_id ) - self._async_configure_linked_sensors(ent_reg_ent, device_lookup, state) + if device_id := ent_reg_ent.device_id: + if device_id not in device_lookup: + device_lookup[device_id] = { + ( + entry.domain, + entry.device_class or entry.original_device_class, + ): entry.entity_id + for entry in entries.get_entries_for_device_id(device_id) + } + self._async_configure_linked_sensors( + ent_reg_ent, device_lookup[device_id], state + ) entity_states.append(state) @@ -829,7 +847,7 @@ class HomeKit: self.status = STATUS_WAIT self._cancel_reload_dispatcher = async_dispatcher_connect( self.hass, - f"homekit_reload_entities_{self._entry_id}", + SIGNAL_RELOAD_ENTITIES.format(self._entry_id), self.async_reload_accessories, ) async_zc_instance = await zeroconf.async_get_async_instance(self.hass) @@ -839,7 +857,9 @@ class HomeKit: # Avoid gather here since it will be I/O bound anyways await self.aid_storage.async_initialize() await self.iid_storage.async_initialize() - await self.hass.async_add_executor_job(self.setup, async_zc_instance, uuid) + loaded_from_disk = await self.hass.async_add_executor_job( + self.setup, async_zc_instance, uuid + ) assert self.driver is not None if not await self._async_create_accessories(): @@ -847,8 +867,12 @@ class HomeKit: self._async_register_bridge() _LOGGER.debug("Driver start for %s", self._name) await self.driver.async_start() - async with self.hass.data[PERSIST_LOCK_DATA]: - await self.hass.async_add_executor_job(self.driver.persist) + if not loaded_from_disk: + # If the state was not loaded from disk, it means this is the + # first time the bridge is ever starting up. In this case, we + # need to make sure its persisted to disk. + async with self.hass.data[PERSIST_LOCK_DATA]: + await self.hass.async_add_executor_job(self.driver.persist) self.status = STATUS_RUNNING if self.driver.state.paired: @@ -927,13 +951,15 @@ class HomeKit: connection: tuple[str, str], ) -> None: """Purge bridges that exist from failed pairing or manual resets.""" - devices_to_purge = [] - for entry in dev_reg.devices.values(): - if self._entry_id in entry.config_entries and ( + devices_to_purge = [ + entry.id + for entry in dev_reg.devices.values() + if self._entry_id in entry.config_entries + and ( identifier not in entry.identifiers # type: ignore[comparison-overlap] or connection not in entry.connections - ): - devices_to_purge.append(entry.id) + ) + ] for device_id in devices_to_purge: dev_reg.async_remove_device(device_id) @@ -1051,64 +1077,59 @@ class HomeKit: def _async_configure_linked_sensors( self, ent_reg_ent: er.RegistryEntry, - device_lookup: dict[str, dict[tuple[str, str | None], str]], + device_lookup: dict[tuple[str, str | None], str], state: State, ) -> None: - if ( - ent_reg_ent is None - or ent_reg_ent.device_id is None - or ent_reg_ent.device_id not in device_lookup - or (ent_reg_ent.device_class or ent_reg_ent.original_device_class) - in (BinarySensorDeviceClass.BATTERY_CHARGING, SensorDeviceClass.BATTERY) + if (ent_reg_ent.device_class or ent_reg_ent.original_device_class) in ( + BinarySensorDeviceClass.BATTERY_CHARGING, + SensorDeviceClass.BATTERY, ): return - if ATTR_BATTERY_CHARGING not in state.attributes: - battery_charging_binary_sensor_entity_id = device_lookup[ - ent_reg_ent.device_id - ].get((BINARY_SENSOR_DOMAIN, BinarySensorDeviceClass.BATTERY_CHARGING)) - if battery_charging_binary_sensor_entity_id: - self._config.setdefault(state.entity_id, {}).setdefault( - CONF_LINKED_BATTERY_CHARGING_SENSOR, - battery_charging_binary_sensor_entity_id, - ) + domain = state.domain + attributes = state.attributes - if ATTR_BATTERY_LEVEL not in state.attributes: - battery_sensor_entity_id = device_lookup[ent_reg_ent.device_id].get( + if ATTR_BATTERY_CHARGING not in attributes and ( + battery_charging_binary_sensor_entity_id := device_lookup.get( + (BINARY_SENSOR_DOMAIN, BinarySensorDeviceClass.BATTERY_CHARGING) + ) + ): + self._config.setdefault(state.entity_id, {}).setdefault( + CONF_LINKED_BATTERY_CHARGING_SENSOR, + battery_charging_binary_sensor_entity_id, + ) + + if ATTR_BATTERY_LEVEL not in attributes and ( + battery_sensor_entity_id := device_lookup.get( (SENSOR_DOMAIN, SensorDeviceClass.BATTERY) ) - if battery_sensor_entity_id: - self._config.setdefault(state.entity_id, {}).setdefault( - CONF_LINKED_BATTERY_SENSOR, battery_sensor_entity_id - ) + ): + self._config.setdefault(state.entity_id, {}).setdefault( + CONF_LINKED_BATTERY_SENSOR, battery_sensor_entity_id + ) - if state.entity_id.startswith(f"{CAMERA_DOMAIN}."): - motion_binary_sensor_entity_id = device_lookup[ent_reg_ent.device_id].get( + if domain == CAMERA_DOMAIN: + if motion_binary_sensor_entity_id := device_lookup.get( (BINARY_SENSOR_DOMAIN, BinarySensorDeviceClass.MOTION) - ) - if motion_binary_sensor_entity_id: + ): self._config.setdefault(state.entity_id, {}).setdefault( - CONF_LINKED_MOTION_SENSOR, - motion_binary_sensor_entity_id, + CONF_LINKED_MOTION_SENSOR, motion_binary_sensor_entity_id ) - doorbell_binary_sensor_entity_id = device_lookup[ent_reg_ent.device_id].get( + if doorbell_binary_sensor_entity_id := device_lookup.get( (BINARY_SENSOR_DOMAIN, BinarySensorDeviceClass.OCCUPANCY) - ) - if doorbell_binary_sensor_entity_id: + ): self._config.setdefault(state.entity_id, {}).setdefault( - CONF_LINKED_DOORBELL_SENSOR, - doorbell_binary_sensor_entity_id, + CONF_LINKED_DOORBELL_SENSOR, doorbell_binary_sensor_entity_id ) - if state.entity_id.startswith(f"{HUMIDIFIER_DOMAIN}."): - current_humidity_sensor_entity_id = device_lookup[ - ent_reg_ent.device_id - ].get((SENSOR_DOMAIN, SensorDeviceClass.HUMIDITY)) - if current_humidity_sensor_entity_id: - self._config.setdefault(state.entity_id, {}).setdefault( - CONF_LINKED_HUMIDITY_SENSOR, - current_humidity_sensor_entity_id, - ) + if domain == HUMIDIFIER_DOMAIN and ( + current_humidity_sensor_entity_id := device_lookup.get( + (SENSOR_DOMAIN, SensorDeviceClass.HUMIDITY) + ) + ): + self._config.setdefault(state.entity_id, {}).setdefault( + CONF_LINKED_HUMIDITY_SENSOR, current_humidity_sensor_entity_id + ) async def _async_set_device_info_attributes( self, @@ -1158,9 +1179,9 @@ class HomeKitPairingQRView(HomeAssistantView): async def get(self, request: web.Request) -> web.Response: """Retrieve the pairing QRCode image.""" if not request.query_string: - raise Unauthorized() + raise Unauthorized entry_id, secret = request.query_string.split("-") - hass: HomeAssistant = request.app["hass"] + hass = request.app[KEY_HASS] domain_data: dict[str, HomeKitEntryData] = hass.data[DOMAIN] if ( not (entry_data := domain_data.get(entry_id)) @@ -1168,7 +1189,7 @@ class HomeKitPairingQRView(HomeAssistantView): or not entry_data.pairing_qr_secret or secret != entry_data.pairing_qr_secret ): - raise Unauthorized() + raise Unauthorized return web.Response( body=entry_data.pairing_qr, content_type="image/svg+xml", diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index 25b1c143f54..39fa62e3445 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -1,4 +1,5 @@ """Extend the basic Accessory and Bridge functions.""" + from __future__ import annotations import logging @@ -43,6 +44,7 @@ from homeassistant.const import ( from homeassistant.core import ( CALLBACK_TYPE, Context, + Event, HomeAssistant, State, callback as ha_callback, @@ -53,7 +55,6 @@ from homeassistant.helpers.event import ( EventStateChangedData, async_track_state_change_event, ) -from homeassistant.helpers.typing import EventType from homeassistant.util.decorator import Registry from .const import ( @@ -71,6 +72,7 @@ from .const import ( CONF_LINKED_BATTERY_SENSOR, CONF_LOW_BATTERY_THRESHOLD, DEFAULT_LOW_BATTERY_THRESHOLD, + EMPTY_MAC, EVENT_HOMEKIT_CHANGED, HK_CHARGING, HK_NOT_CHARGABLE, @@ -82,6 +84,7 @@ from .const import ( MAX_VERSION_LENGTH, SERV_ACCESSORY_INFO, SERV_BATTERY_SERVICE, + SIGNAL_RELOAD_ENTITIES, TYPE_FAUCET, TYPE_OUTLET, TYPE_SHOWER, @@ -477,7 +480,7 @@ class HomeAccessory(Accessory): # type: ignore[misc] @ha_callback def async_update_event_state_callback( - self, event: EventType[EventStateChangedData] + self, event: Event[EventStateChangedData] ) -> None: """Handle state change event listener callback.""" new_state = event.data["new_state"] @@ -529,7 +532,7 @@ class HomeAccessory(Accessory): # type: ignore[misc] @ha_callback def async_update_linked_battery_callback( - self, event: EventType[EventStateChangedData] + self, event: Event[EventStateChangedData] ) -> None: """Handle linked battery sensor state change listener callback.""" if (new_state := event.data["new_state"]) is None: @@ -542,7 +545,7 @@ class HomeAccessory(Accessory): # type: ignore[misc] @ha_callback def async_update_linked_battery_charging_callback( - self, event: EventType[EventStateChangedData] + self, event: Event[EventStateChangedData] ) -> None: """Handle linked battery charging sensor state change listener callback.""" if (new_state := event.data["new_state"]) is None: @@ -587,7 +590,7 @@ class HomeAccessory(Accessory): # type: ignore[misc] Overridden by accessory types. """ - raise NotImplementedError() + raise NotImplementedError @ha_callback def async_call_service( @@ -619,7 +622,7 @@ class HomeAccessory(Accessory): # type: ignore[misc] """Reload and recreate an accessory and update the c# value in the mDNS record.""" async_dispatcher_send( self.hass, - f"homekit_reload_entities_{self.driver.entry_id}", + SIGNAL_RELOAD_ENTITIES.format(self.driver.entry_id), (self.entity_id,), ) @@ -682,7 +685,9 @@ class HomeDriver(AccessoryDriver): # type: ignore[misc] **kwargs: Any, ) -> None: """Initialize a AccessoryDriver object.""" - super().__init__(**kwargs) + # Always set an empty mac of pyhap will incur + # the cost of generating a new one for every driver + super().__init__(**kwargs, mac=EMPTY_MAC) self.hass = hass self.entry_id = entry_id self._bridge_name = bridge_name diff --git a/homeassistant/components/homekit/aidmanager.py b/homeassistant/components/homekit/aidmanager.py index 43beaaa8dc6..36df47e8a93 100644 --- a/homeassistant/components/homekit/aidmanager.py +++ b/homeassistant/components/homekit/aidmanager.py @@ -8,6 +8,7 @@ can't change the hash without causing breakages for HA users. This module generates and stores them in a HA storage. """ + from __future__ import annotations from collections.abc import Generator diff --git a/homeassistant/components/homekit/config_flow.py b/homeassistant/components/homekit/config_flow.py index d7c8ea65e2d..30fb80cbdfc 100644 --- a/homeassistant/components/homekit/config_flow.py +++ b/homeassistant/components/homekit/config_flow.py @@ -1,4 +1,5 @@ """Config flow for HomeKit integration.""" + from __future__ import annotations from collections.abc import Iterable @@ -11,13 +12,18 @@ from typing import Any, Final, TypedDict import voluptuous as vol -from homeassistant import config_entries from homeassistant.components import device_automation from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN from homeassistant.components.media_player import DOMAIN as MEDIA_PLAYER_DOMAIN from homeassistant.components.remote import DOMAIN as REMOTE_DOMAIN -from homeassistant.config_entries import SOURCE_IMPORT +from homeassistant.config_entries import ( + SOURCE_IMPORT, + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.const import ( ATTR_FRIENDLY_NAME, CONF_DEVICES, @@ -28,7 +34,6 @@ from homeassistant.const import ( CONF_PORT, ) from homeassistant.core import HomeAssistant, callback, split_entity_id -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import ( config_validation as cv, device_registry as dr, @@ -191,7 +196,7 @@ async def _async_name_to_type_map(hass: HomeAssistant) -> dict[str, str]: } -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class HomeKitConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for HomeKit.""" VERSION = 1 @@ -202,7 +207,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Choose specific domains in bridge mode.""" if user_input is not None: self.hk_data[CONF_FILTER] = _make_entity_filter( @@ -228,7 +233,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_pairing( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Pairing instructions.""" hk_data = self.hk_data @@ -278,7 +283,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) ) - async def async_step_accessory(self, accessory_input: dict[str, Any]) -> FlowResult: + async def async_step_accessory( + self, accessory_input: dict[str, Any] + ) -> ConfigFlowResult: """Handle creation a single accessory in accessory mode.""" entity_id = accessory_input[CONF_ENTITY_ID] port = accessory_input[CONF_PORT] @@ -302,7 +309,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): title=f"{name}:{entry_data[CONF_PORT]}", data=entry_data ) - async def async_step_import(self, user_input: dict[str, Any]) -> FlowResult: + async def async_step_import(self, user_input: dict[str, Any]) -> ConfigFlowResult: """Handle import from yaml.""" if not self._async_is_unique_name_port(user_input): return self.async_abort(reason="port_name_in_use") @@ -349,16 +356,16 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback def async_get_options_flow( - config_entry: config_entries.ConfigEntry, + config_entry: ConfigEntry, ) -> OptionsFlowHandler: """Get the options flow for this handler.""" return OptionsFlowHandler(config_entry) -class OptionsFlowHandler(config_entries.OptionsFlow): +class OptionsFlowHandler(OptionsFlow): """Handle a option flow for homekit.""" - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + def __init__(self, config_entry: ConfigEntry) -> None: """Initialize options flow.""" self.config_entry = config_entry self.hk_options: dict[str, Any] = {} @@ -366,7 +373,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow): async def async_step_yaml( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """No options for yaml managed entries.""" if user_input is not None: # Apparently not possible to abort an options flow @@ -377,7 +384,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow): async def async_step_advanced( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Choose advanced options.""" hk_options = self.hk_options show_advanced_options = self.show_advanced_options @@ -414,7 +421,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow): async def async_step_cameras( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Choose camera config.""" hk_options = self.hk_options all_entity_config: dict[str, dict[str, Any]] @@ -465,7 +472,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow): async def async_step_accessory( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Choose entity for the accessory.""" hk_options = self.hk_options domains = hk_options[CONF_DOMAINS] @@ -508,7 +515,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow): async def async_step_include( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Choose entities to include from the domain on the bridge.""" hk_options = self.hk_options domains = hk_options[CONF_DOMAINS] @@ -546,7 +553,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow): async def async_step_exclude( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Choose entities to exclude from the domain on the bridge.""" hk_options = self.hk_options domains = hk_options[CONF_DOMAINS] @@ -598,7 +605,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow): async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle options flow.""" if self.config_entry.source == SOURCE_IMPORT: return await self.async_step_yaml(user_input) diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index 5a7ee1d9576..9f44e2ab616 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -1,6 +1,9 @@ """Constants used be the HomeKit component.""" +from __future__ import annotations + from homeassistant.const import CONF_DEVICES +from homeassistant.util.signal_type import SignalTypeFormat # #### Misc #### DEBOUNCE_TIMEOUT = 0.5 @@ -10,6 +13,10 @@ PERSIST_LOCK_DATA = f"{DOMAIN}_persist_lock" HOMEKIT_FILE = ".homekit.state" SHUTDOWN_TIMEOUT = 30 CONF_ENTRY_INDEX = "index" +EMPTY_MAC = "00:00:00:00:00:00" +SIGNAL_RELOAD_ENTITIES: SignalTypeFormat[tuple[str, ...]] = SignalTypeFormat( + "homekit_reload_entities_{}" +) # ### Codecs #### VIDEO_CODEC_COPY = "copy" diff --git a/homeassistant/components/homekit/diagnostics.py b/homeassistant/components/homekit/diagnostics.py index 347a3df0dd4..f31dd268b26 100644 --- a/homeassistant/components/homekit/diagnostics.py +++ b/homeassistant/components/homekit/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for HomeKit.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/homekit/iidmanager.py b/homeassistant/components/homekit/iidmanager.py index f44d76d3ee7..d6daeb49f82 100644 --- a/homeassistant/components/homekit/iidmanager.py +++ b/homeassistant/components/homekit/iidmanager.py @@ -5,6 +5,7 @@ be stable between reboots and upgrades. This module generates and stores them in a HA storage. """ + from __future__ import annotations from uuid import UUID diff --git a/homeassistant/components/homekit/logbook.py b/homeassistant/components/homekit/logbook.py index e71695883a8..c1609350d68 100644 --- a/homeassistant/components/homekit/logbook.py +++ b/homeassistant/components/homekit/logbook.py @@ -1,4 +1,5 @@ """Describe logbook events.""" + from collections.abc import Callable from typing import Any diff --git a/homeassistant/components/homekit/models.py b/homeassistant/components/homekit/models.py index e96af00fead..fee081c9e51 100644 --- a/homeassistant/components/homekit/models.py +++ b/homeassistant/components/homekit/models.py @@ -1,4 +1,5 @@ """Models for the HomeKit component.""" + from dataclasses import dataclass from typing import TYPE_CHECKING diff --git a/homeassistant/components/homekit/type_cameras.py b/homeassistant/components/homekit/type_cameras.py index 078ab8818ac..d47d9775ed2 100644 --- a/homeassistant/components/homekit/type_cameras.py +++ b/homeassistant/components/homekit/type_cameras.py @@ -1,4 +1,5 @@ """Class to hold all camera accessories.""" + import asyncio from datetime import timedelta import logging @@ -16,13 +17,12 @@ from pyhap.util import callback as pyhap_callback from homeassistant.components import camera from homeassistant.components.ffmpeg import get_ffmpeg_manager from homeassistant.const import STATE_ON -from homeassistant.core import HomeAssistant, State, callback +from homeassistant.core import Event, HomeAssistant, State, callback from homeassistant.helpers.event import ( EventStateChangedData, async_track_state_change_event, async_track_time_interval, ) -from homeassistant.helpers.typing import EventType from .accessories import TYPES, HomeAccessory, HomeDriver from .const import ( @@ -283,7 +283,7 @@ class Camera(HomeAccessory, PyhapCamera): # type: ignore[misc] @callback def _async_update_motion_state_event( - self, event: EventType[EventStateChangedData] + self, event: Event[EventStateChangedData] ) -> None: """Handle state change event listener callback.""" if not state_changed_event_is_same_state(event): @@ -310,7 +310,7 @@ class Camera(HomeAccessory, PyhapCamera): # type: ignore[misc] @callback def _async_update_doorbell_state_event( - self, event: EventType[EventStateChangedData] + self, event: Event[EventStateChangedData] ) -> None: """Handle state change event listener callback.""" if not state_changed_event_is_same_state(event): diff --git a/homeassistant/components/homekit/type_covers.py b/homeassistant/components/homekit/type_covers.py index 47660e486f2..2452fd65026 100644 --- a/homeassistant/components/homekit/type_covers.py +++ b/homeassistant/components/homekit/type_covers.py @@ -1,4 +1,5 @@ """Class to hold all cover accessories.""" + import logging from typing import Any @@ -33,12 +34,11 @@ from homeassistant.const import ( STATE_OPEN, STATE_OPENING, ) -from homeassistant.core import State, callback +from homeassistant.core import Event, State, callback from homeassistant.helpers.event import ( EventStateChangedData, async_track_state_change_event, ) -from homeassistant.helpers.typing import EventType from .accessories import TYPES, HomeAccessory from .const import ( @@ -146,7 +146,7 @@ class GarageDoorOpener(HomeAccessory): @callback def _async_update_obstruction_event( - self, event: EventType[EventStateChangedData] + self, event: Event[EventStateChangedData] ) -> None: """Handle state change event listener callback.""" self._async_update_obstruction_state(event.data["new_state"]) diff --git a/homeassistant/components/homekit/type_fans.py b/homeassistant/components/homekit/type_fans.py index d371998aaf8..0dee5fa2b71 100644 --- a/homeassistant/components/homekit/type_fans.py +++ b/homeassistant/components/homekit/type_fans.py @@ -1,4 +1,5 @@ """Class to hold all fan accessories.""" + import logging from typing import Any diff --git a/homeassistant/components/homekit/type_humidifiers.py b/homeassistant/components/homekit/type_humidifiers.py index 0b2c965c7f3..2b4de072b6a 100644 --- a/homeassistant/components/homekit/type_humidifiers.py +++ b/homeassistant/components/homekit/type_humidifiers.py @@ -1,4 +1,5 @@ """Class to hold all thermostat accessories.""" + import logging from typing import Any @@ -24,12 +25,11 @@ from homeassistant.const import ( SERVICE_TURN_ON, STATE_ON, ) -from homeassistant.core import State, callback +from homeassistant.core import Event, State, callback from homeassistant.helpers.event import ( EventStateChangedData, async_track_state_change_event, ) -from homeassistant.helpers.typing import EventType from .accessories import TYPES, HomeAccessory from .const import ( @@ -194,7 +194,7 @@ class HumidifierDehumidifier(HomeAccessory): @callback def async_update_current_humidity_event( - self, event: EventType[EventStateChangedData] + self, event: Event[EventStateChangedData] ) -> None: """Handle state change event listener callback.""" self._async_update_current_humidity(event.data["new_state"]) diff --git a/homeassistant/components/homekit/type_lights.py b/homeassistant/components/homekit/type_lights.py index b45e9e1c17b..cb446ea551c 100644 --- a/homeassistant/components/homekit/type_lights.py +++ b/homeassistant/components/homekit/type_lights.py @@ -1,4 +1,5 @@ """Class to hold all light accessories.""" + from __future__ import annotations from datetime import datetime diff --git a/homeassistant/components/homekit/type_locks.py b/homeassistant/components/homekit/type_locks.py index 18dfe48b2bd..e5b0ad22396 100644 --- a/homeassistant/components/homekit/type_locks.py +++ b/homeassistant/components/homekit/type_locks.py @@ -1,4 +1,5 @@ """Class to hold all lock accessories.""" + import logging from typing import Any diff --git a/homeassistant/components/homekit/type_media_players.py b/homeassistant/components/homekit/type_media_players.py index 23fbd5b454d..4cdb471b4ff 100644 --- a/homeassistant/components/homekit/type_media_players.py +++ b/homeassistant/components/homekit/type_media_players.py @@ -1,4 +1,5 @@ """Class to hold all media player accessories.""" + import logging from typing import Any diff --git a/homeassistant/components/homekit/type_remotes.py b/homeassistant/components/homekit/type_remotes.py index e03b14f943a..75b3a5200b8 100644 --- a/homeassistant/components/homekit/type_remotes.py +++ b/homeassistant/components/homekit/type_remotes.py @@ -1,4 +1,5 @@ """Class to hold remote accessories.""" + from abc import ABC, abstractmethod import logging from typing import Any diff --git a/homeassistant/components/homekit/type_security_systems.py b/homeassistant/components/homekit/type_security_systems.py index de2c463bfb2..27c479de6ba 100644 --- a/homeassistant/components/homekit/type_security_systems.py +++ b/homeassistant/components/homekit/type_security_systems.py @@ -1,4 +1,5 @@ """Class to hold all alarm control panel accessories.""" + import logging from typing import Any diff --git a/homeassistant/components/homekit/type_sensors.py b/homeassistant/components/homekit/type_sensors.py index dbf2808a55a..bfa97756bb4 100644 --- a/homeassistant/components/homekit/type_sensors.py +++ b/homeassistant/components/homekit/type_sensors.py @@ -1,4 +1,5 @@ """Class to hold all sensor accessories.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/homekit/type_switches.py b/homeassistant/components/homekit/type_switches.py index 5c0c2c74f0a..86861417bdb 100644 --- a/homeassistant/components/homekit/type_switches.py +++ b/homeassistant/components/homekit/type_switches.py @@ -1,4 +1,5 @@ """Class to hold all switch accessories.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index c638da55764..ba0f9790efb 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -1,4 +1,5 @@ """Class to hold all thermostat accessories.""" + import logging from typing import Any diff --git a/homeassistant/components/homekit/type_triggers.py b/homeassistant/components/homekit/type_triggers.py index 625ed0a4a44..b958817bbac 100644 --- a/homeassistant/components/homekit/type_triggers.py +++ b/homeassistant/components/homekit/type_triggers.py @@ -1,4 +1,5 @@ """Class to hold all sensor accessories.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index 8a51f35564e..031cdbbc9bd 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -1,4 +1,5 @@ """Collection of useful functions for the HomeKit component.""" + from __future__ import annotations import io @@ -37,11 +38,10 @@ from homeassistant.const import ( CONF_TYPE, UnitOfTemperature, ) -from homeassistant.core import HomeAssistant, State, callback, split_entity_id +from homeassistant.core import Event, HomeAssistant, State, callback, split_entity_id import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import EventStateChangedData from homeassistant.helpers.storage import STORAGE_DIR -from homeassistant.helpers.typing import EventType from homeassistant.util.unit_conversion import TemperatureConverter from .const import ( @@ -326,10 +326,7 @@ def validate_media_player_features(state: State, feature_list: str) -> bool: # Auto detected return True - error_list = [] - for feature in feature_list: - if feature not in supported_modes: - error_list.append(feature) + error_list = [feature for feature in feature_list if feature not in supported_modes] if error_list: _LOGGER.error( @@ -623,7 +620,7 @@ def state_needs_accessory_mode(state: State) -> bool: ) -def state_changed_event_is_same_state(event: EventType[EventStateChangedData]) -> bool: +def state_changed_event_is_same_state(event: Event[EventStateChangedData]) -> bool: """Check if a state changed event is the same state.""" event_data = event.data old_state = event_data["old_state"] diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index e3ff4d47fcf..218094ddaf5 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -1,4 +1,5 @@ """Support for Homekit device discovery.""" + from __future__ import annotations import asyncio @@ -23,6 +24,7 @@ from homeassistant.core import Event, HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv, device_registry as dr from homeassistant.helpers.typing import ConfigType +from homeassistant.util.async_ import create_eager_task from .config_flow import normalize_hkid from .connection import HKDevice @@ -79,12 +81,14 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def _async_stop_homekit_controller(event: Event) -> None: await asyncio.gather( *( - connection.async_unload() + create_eager_task(connection.async_unload()) for connection in hass.data[KNOWN_DEVICES].values() ) ) - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_stop_homekit_controller) + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, _async_stop_homekit_controller, run_immediately=True + ) return True diff --git a/homeassistant/components/homekit_controller/alarm_control_panel.py b/homeassistant/components/homekit_controller/alarm_control_panel.py index f1c2440ce9e..1cb94926e8b 100644 --- a/homeassistant/components/homekit_controller/alarm_control_panel.py +++ b/homeassistant/components/homekit_controller/alarm_control_panel.py @@ -1,4 +1,5 @@ """Support for Homekit Alarm Control Panel.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/homekit_controller/binary_sensor.py b/homeassistant/components/homekit_controller/binary_sensor.py index 93bf20ef493..26e19c8944a 100644 --- a/homeassistant/components/homekit_controller/binary_sensor.py +++ b/homeassistant/components/homekit_controller/binary_sensor.py @@ -1,4 +1,5 @@ """Support for Homekit motion sensors.""" + from __future__ import annotations from aiohomekit.model.characteristics import CharacteristicsTypes diff --git a/homeassistant/components/homekit_controller/button.py b/homeassistant/components/homekit_controller/button.py index a0c61578e66..abd00f02aa0 100644 --- a/homeassistant/components/homekit_controller/button.py +++ b/homeassistant/components/homekit_controller/button.py @@ -3,6 +3,7 @@ These are mostly used where a HomeKit accessory exposes additional non-standard characteristics that don't map to a Home Assistant feature. """ + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/homekit_controller/camera.py b/homeassistant/components/homekit_controller/camera.py index 35a4b089641..4332032867a 100644 --- a/homeassistant/components/homekit_controller/camera.py +++ b/homeassistant/components/homekit_controller/camera.py @@ -1,4 +1,5 @@ """Support for Homekit cameras.""" + from __future__ import annotations from aiohomekit.model import Accessory diff --git a/homeassistant/components/homekit_controller/climate.py b/homeassistant/components/homekit_controller/climate.py index 0ca85da3fa2..49ae3bb4a42 100644 --- a/homeassistant/components/homekit_controller/climate.py +++ b/homeassistant/components/homekit_controller/climate.py @@ -1,4 +1,5 @@ """Support for Homekit climate devices.""" + from __future__ import annotations import logging @@ -200,7 +201,8 @@ class HomeKitHeaterCoolerEntity(HomeKitBaseClimateEntity): def get_characteristic_types(self) -> list[str]: """Define the homekit characteristics the entity cares about.""" - return super().get_characteristic_types() + [ + return [ + *super().get_characteristic_types(), CharacteristicsTypes.ACTIVE, CharacteristicsTypes.CURRENT_HEATER_COOLER_STATE, CharacteristicsTypes.TARGET_HEATER_COOLER_STATE, @@ -478,7 +480,8 @@ class HomeKitClimateEntity(HomeKitBaseClimateEntity): def get_characteristic_types(self) -> list[str]: """Define the homekit characteristics the entity cares about.""" - return super().get_characteristic_types() + [ + return [ + *super().get_characteristic_types(), CharacteristicsTypes.HEATING_COOLING_CURRENT, CharacteristicsTypes.HEATING_COOLING_TARGET, CharacteristicsTypes.TEMPERATURE_COOLING_THRESHOLD, @@ -634,7 +637,7 @@ class HomeKitClimateEntity(HomeKitBaseClimateEntity): return self.service.value(CharacteristicsTypes.RELATIVE_HUMIDITY_TARGET) @property - def min_humidity(self) -> int: + def min_humidity(self) -> float: """Return the minimum humidity.""" min_humidity = self.service[ CharacteristicsTypes.RELATIVE_HUMIDITY_TARGET @@ -644,7 +647,7 @@ class HomeKitClimateEntity(HomeKitBaseClimateEntity): return super().min_humidity @property - def max_humidity(self) -> int: + def max_humidity(self) -> float: """Return the maximum humidity.""" max_humidity = self.service[ CharacteristicsTypes.RELATIVE_HUMIDITY_TARGET diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index 7f0f288400d..e48cb069dfe 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -1,4 +1,5 @@ """Config flow to configure homekit_controller.""" + from __future__ import annotations import logging @@ -18,10 +19,10 @@ from aiohomekit.model.status_flags import StatusFlags from aiohomekit.utils import domain_supported, domain_to_name, serialize_broadcast_key import voluptuous as vol -from homeassistant import config_entries from homeassistant.components import zeroconf +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.core import callback -from homeassistant.data_entry_flow import AbortFlow, FlowResult +from homeassistant.data_entry_flow import AbortFlow from homeassistant.helpers import device_registry as dr from .const import DOMAIN, KNOWN_DEVICES @@ -95,7 +96,7 @@ def ensure_pin_format(pin: str, allow_insecure_setup_codes: Any = None) -> str: return "-".join(match.groups()) -class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class HomekitControllerFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a HomeKit config flow.""" VERSION = 1 @@ -116,7 +117,7 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow start.""" errors: dict[str, str] = {} @@ -166,7 +167,7 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ), ) - async def async_step_unignore(self, user_input: dict[str, Any]) -> FlowResult: + async def async_step_unignore(self, user_input: dict[str, Any]) -> ConfigFlowResult: """Rediscover a previously ignored discover.""" unique_id = user_input["unique_id"] await self.async_set_unique_id(unique_id) @@ -208,7 +209,7 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_zeroconf( self, discovery_info: zeroconf.ZeroconfServiceInfo - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a discovered HomeKit accessory. This flow is triggered by the discovery component. @@ -361,7 +362,7 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_bluetooth( self, discovery_info: bluetooth.BluetoothServiceInfoBleak - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the bluetooth discovery step.""" if not aiohomekit_const.BLE_TRANSPORT_SUPPORTED: return self.async_abort(reason="ignored_model") @@ -409,7 +410,7 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_pair( self, pair_info: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Pair with a new HomeKit accessory.""" # If async_step_pair is called with no pairing code then we do the M1 # phase of pairing. If this is successful the device enters pairing @@ -516,7 +517,7 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_busy_error( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Retry pairing after the accessory is busy.""" if user_input is not None: return await self.async_step_pair() @@ -525,7 +526,7 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_max_tries_error( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Retry pairing after the accessory has reached max tries.""" if user_input is not None: return await self.async_step_pair() @@ -534,7 +535,7 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_protocol_error( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Retry pairing after the accessory has a protocol error.""" if user_input is not None: return await self.async_step_pair() @@ -546,7 +547,7 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self, errors: dict[str, str] | None = None, description_placeholders: dict[str, str] | None = None, - ) -> FlowResult: + ) -> ConfigFlowResult: assert self.category placeholders = self.context["title_placeholders"] = { @@ -565,7 +566,7 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): data_schema=vol.Schema(schema), ) - async def _entry_from_accessory(self, pairing: AbstractPairing) -> FlowResult: + async def _entry_from_accessory(self, pairing: AbstractPairing) -> ConfigFlowResult: """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 diff --git a/homeassistant/components/homekit_controller/connection.py b/homeassistant/components/homekit_controller/connection.py index 0dabc814a7e..883ec4f1a44 100644 --- a/homeassistant/components/homekit_controller/connection.py +++ b/homeassistant/components/homekit_controller/connection.py @@ -1,4 +1,5 @@ """Helpers for managing a pairing with a HomeKit accessory or bridge.""" + from __future__ import annotations import asyncio @@ -30,7 +31,6 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.event import async_call_later, async_track_time_interval -from homeassistant.util.async_ import create_eager_task from .config_flow import normalize_hkid from .const import ( @@ -145,6 +145,7 @@ class HKDevice: cooldown=DEBOUNCE_COOLDOWN, immediate=False, function=self.async_update, + background=True, ) self._availability_callbacks: set[CALLBACK_TYPE] = set() @@ -197,13 +198,19 @@ class HKDevice: self._subscribe_timer() self._subscribe_timer = None - async def _async_subscribe(self, _now: datetime) -> None: + @callback + def _async_subscribe(self, _now: datetime) -> None: """Subscribe to characteristics.""" self._subscribe_timer = None if self._pending_subscribes: subscribes = self._pending_subscribes.copy() self._pending_subscribes.clear() - await self.pairing.subscribe(subscribes) + self.config_entry.async_create_task( + self.hass, + self.pairing.subscribe(subscribes), + name=f"hkc subscriptions {self.unique_id}", + eager_start=True, + ) def remove_watchable_characteristics( self, characteristics: list[tuple[int, int]] @@ -279,6 +286,7 @@ class HKDevice: self.hass.bus.async_listen( EVENT_HOMEASSISTANT_STARTED, self._async_populate_ble_accessory_state, + run_immediately=True, ) ) else: @@ -320,7 +328,7 @@ class HKDevice: ) # BLE devices always get an RSSI sensor as well if "sensor" not in self.platforms: - await self.async_load_platform("sensor") + await self._async_load_platforms({"sensor"}) @callback def _async_start_polling(self) -> None: @@ -340,8 +348,11 @@ class HKDevice: @callback def _async_schedule_update(self, now: datetime) -> None: """Schedule an update.""" - self.hass.async_create_task( - self._debounced_update.async_call(), eager_start=True + self.config_entry.async_create_background_task( + self.hass, + self._debounced_update.async_call(), + name=f"hkc {self.unique_id} alive poll", + eager_start=True, ) async def async_add_new_entities(self) -> None: @@ -693,8 +704,8 @@ class HKDevice: 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(), eager_start=True + self.config_entry.async_create_task( + self.hass, self.async_update_new_accessories_state(), eager_start=True ) async def async_update_new_accessories_state(self) -> None: @@ -791,19 +802,14 @@ class HKDevice: self.entities.add(entity_key) break - async def async_load_platform(self, platform: str) -> None: - """Load a single platform idempotently.""" - if platform in self.platforms: + async def _async_load_platforms(self, platforms: set[str]) -> None: + """Load a group of platforms.""" + if not (to_load := platforms - self.platforms): return - - self.platforms.add(platform) - try: - await self.hass.config_entries.async_forward_entry_setup( - self.config_entry, platform - ) - except Exception: - self.platforms.remove(platform) - raise + self.platforms.update(to_load) + await self.hass.config_entries.async_forward_entry_setups( + self.config_entry, platforms + ) async def async_load_platforms(self) -> None: """Load any platforms needed by this HomeKit device.""" @@ -822,12 +828,7 @@ class HKDevice: to_load.add(platform) if to_load: - await asyncio.gather( - *( - create_eager_task(self.async_load_platform(platform)) - for platform in to_load - ) - ) + await self._async_load_platforms(to_load) @callback def async_update_available_state(self, *_: Any) -> None: diff --git a/homeassistant/components/homekit_controller/cover.py b/homeassistant/components/homekit_controller/cover.py index f99563843c7..b15ce645a29 100644 --- a/homeassistant/components/homekit_controller/cover.py +++ b/homeassistant/components/homekit_controller/cover.py @@ -1,4 +1,5 @@ """Support for Homekit covers.""" + from __future__ import annotations from typing import TYPE_CHECKING, Any diff --git a/homeassistant/components/homekit_controller/device_trigger.py b/homeassistant/components/homekit_controller/device_trigger.py index 6dc97bf6821..a68241d7fc0 100644 --- a/homeassistant/components/homekit_controller/device_trigger.py +++ b/homeassistant/components/homekit_controller/device_trigger.py @@ -1,4 +1,5 @@ """Provides device automations for homekit devices.""" + from __future__ import annotations from collections.abc import Callable, Generator @@ -159,7 +160,7 @@ def enumerate_stateless_switch_group(service: Service) -> list[dict[str, Any]]: ) ) - results = [] + results: list[dict[str, Any]] = [] for idx, switch in enumerate(switches): char = switch[CharacteristicsTypes.INPUT_EVENT] @@ -167,15 +168,15 @@ def enumerate_stateless_switch_group(service: Service) -> list[dict[str, Any]]: # manufacturer might not - clamp options to what they say. all_values = clamp_enum_to_char(InputEventValues, char) - for event_type in all_values: - results.append( - { - "characteristic": char.iid, - "value": event_type, - "type": f"button{idx + 1}", - "subtype": HK_TO_HA_INPUT_EVENT_VALUES[event_type], - } - ) + results.extend( + { + "characteristic": char.iid, + "value": event_type, + "type": f"button{idx + 1}", + "subtype": HK_TO_HA_INPUT_EVENT_VALUES[event_type], + } + for event_type in all_values + ) return results @@ -187,17 +188,15 @@ def enumerate_doorbell(service: Service) -> list[dict[str, Any]]: # manufacturer might not - clamp options to what they say. all_values = clamp_enum_to_char(InputEventValues, input_event) - results = [] - for event_type in all_values: - results.append( - { - "characteristic": input_event.iid, - "value": event_type, - "type": "doorbell", - "subtype": HK_TO_HA_INPUT_EVENT_VALUES[event_type], - } - ) - return results + return [ + { + "characteristic": input_event.iid, + "value": event_type, + "type": "doorbell", + "subtype": HK_TO_HA_INPUT_EVENT_VALUES[event_type], + } + for event_type in all_values + ] TRIGGER_FINDERS = { diff --git a/homeassistant/components/homekit_controller/diagnostics.py b/homeassistant/components/homekit_controller/diagnostics.py index 9b17c0c2fe7..bfd034807c9 100644 --- a/homeassistant/components/homekit_controller/diagnostics.py +++ b/homeassistant/components/homekit_controller/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for HomeKit Controller.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/homekit_controller/entity.py b/homeassistant/components/homekit_controller/entity.py index 496866299d6..136c063f280 100644 --- a/homeassistant/components/homekit_controller/entity.py +++ b/homeassistant/components/homekit_controller/entity.py @@ -1,4 +1,5 @@ """Homekit Controller entities.""" + from __future__ import annotations import contextlib diff --git a/homeassistant/components/homekit_controller/event.py b/homeassistant/components/homekit_controller/event.py index 8f3d71682f1..890c12c9bab 100644 --- a/homeassistant/components/homekit_controller/event.py +++ b/homeassistant/components/homekit_controller/event.py @@ -1,4 +1,5 @@ """Support for Homekit motion sensors.""" + from __future__ import annotations from aiohomekit.model.characteristics import CharacteristicsTypes @@ -117,21 +118,21 @@ async def async_setup_entry( ) ) - for switch in switches: - # The Apple docs say that if we number the buttons ourselves - # We do it in service label index order. `switches` is already in - # that order. - entities.append( - HomeKitEventEntity( - conn, - switch, - EventEntityDescription( - key=f"{service.accessory.aid}_{service.iid}", - device_class=EventDeviceClass.BUTTON, - translation_key="button", - ), - ) + # The Apple docs say that if we number the buttons ourselves + # We do it in service label index order. `switches` is already in + # that order. + entities.extend( + HomeKitEventEntity( + conn, + switch, + EventEntityDescription( + key=f"{service.accessory.aid}_{service.iid}", + device_class=EventDeviceClass.BUTTON, + translation_key="button", + ), ) + for switch in switches + ) elif service.type == ServicesTypes.STATELESS_PROGRAMMABLE_SWITCH: # A stateless switch that has a SERVICE_LABEL_INDEX is part of a group diff --git a/homeassistant/components/homekit_controller/fan.py b/homeassistant/components/homekit_controller/fan.py index 1b2d572f2b6..39df2b7ce51 100644 --- a/homeassistant/components/homekit_controller/fan.py +++ b/homeassistant/components/homekit_controller/fan.py @@ -1,4 +1,5 @@ """Support for Homekit fans.""" + from __future__ import annotations from typing import TYPE_CHECKING, Any diff --git a/homeassistant/components/homekit_controller/humidifier.py b/homeassistant/components/homekit_controller/humidifier.py index b5e67e7f1a4..fecba147a71 100644 --- a/homeassistant/components/homekit_controller/humidifier.py +++ b/homeassistant/components/homekit_controller/humidifier.py @@ -1,4 +1,5 @@ """Support for HomeKit Controller humidifier.""" + from __future__ import annotations from typing import TYPE_CHECKING, Any @@ -154,8 +155,9 @@ class HomeKitDehumidifier(HomeKitBaseHumidifier): def get_characteristic_types(self) -> list[str]: """Define the homekit characteristics the entity cares about.""" - return super().get_characteristic_types() + [ - CharacteristicsTypes.RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD + return [ + *super().get_characteristic_types(), + CharacteristicsTypes.RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD, ] @property diff --git a/homeassistant/components/homekit_controller/light.py b/homeassistant/components/homekit_controller/light.py index fd3bf4f800b..b314ffe85de 100644 --- a/homeassistant/components/homekit_controller/light.py +++ b/homeassistant/components/homekit_controller/light.py @@ -1,4 +1,5 @@ """Support for Homekit lights.""" + from __future__ import annotations from typing import TYPE_CHECKING, Any diff --git a/homeassistant/components/homekit_controller/lock.py b/homeassistant/components/homekit_controller/lock.py index df03a1fef34..8e1bcd424d4 100644 --- a/homeassistant/components/homekit_controller/lock.py +++ b/homeassistant/components/homekit_controller/lock.py @@ -1,4 +1,5 @@ """Support for HomeKit Controller locks.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 22d78123e0a..72c1a0478a3 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -12,7 +12,6 @@ "config_flow": true, "dependencies": ["bluetooth_adapters", "zeroconf"], "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "import_executor": true, "iot_class": "local_push", "loggers": ["aiohomekit", "commentjson"], "requirements": ["aiohomekit==3.1.5"], diff --git a/homeassistant/components/homekit_controller/media_player.py b/homeassistant/components/homekit_controller/media_player.py index 90d1ba754f2..4232d1b7649 100644 --- a/homeassistant/components/homekit_controller/media_player.py +++ b/homeassistant/components/homekit_controller/media_player.py @@ -1,4 +1,5 @@ """Support for HomeKit Controller Televisions.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/homekit_controller/number.py b/homeassistant/components/homekit_controller/number.py index e2d856126da..340d31c91ae 100644 --- a/homeassistant/components/homekit_controller/number.py +++ b/homeassistant/components/homekit_controller/number.py @@ -3,6 +3,7 @@ These are mostly used where a HomeKit accessory exposes additional non-standard characteristics that don't map to a Home Assistant feature. """ + from __future__ import annotations from aiohomekit.model.characteristics import Characteristic, CharacteristicsTypes diff --git a/homeassistant/components/homekit_controller/select.py b/homeassistant/components/homekit_controller/select.py index be1a7313301..f672f293122 100644 --- a/homeassistant/components/homekit_controller/select.py +++ b/homeassistant/components/homekit_controller/select.py @@ -1,4 +1,5 @@ """Support for Homekit select entities.""" + from __future__ import annotations from dataclasses import dataclass @@ -22,19 +23,11 @@ from .connection import HKDevice from .entity import CharacteristicEntity -@dataclass(frozen=True) -class HomeKitSelectEntityDescriptionRequired: - """Required fields for HomeKitSelectEntityDescription.""" - - choices: dict[str, IntEnum] - - -@dataclass(frozen=True) -class HomeKitSelectEntityDescription( - SelectEntityDescription, HomeKitSelectEntityDescriptionRequired -): +@dataclass(frozen=True, kw_only=True) +class HomeKitSelectEntityDescription(SelectEntityDescription): """A generic description of a select entity backed by a single characteristic.""" + choices: dict[str, IntEnum] name: str | None = None diff --git a/homeassistant/components/homekit_controller/sensor.py b/homeassistant/components/homekit_controller/sensor.py index 28bb0cd309c..059be5bad99 100644 --- a/homeassistant/components/homekit_controller/sensor.py +++ b/homeassistant/components/homekit_controller/sensor.py @@ -1,4 +1,5 @@ """Support for Homekit sensors.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/homekit_controller/switch.py b/homeassistant/components/homekit_controller/switch.py index b7e1b27ef7f..65f98ed8f5e 100644 --- a/homeassistant/components/homekit_controller/switch.py +++ b/homeassistant/components/homekit_controller/switch.py @@ -1,4 +1,5 @@ """Support for Homekit switches.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/homeassistant/components/homekit_controller/utils.py b/homeassistant/components/homekit_controller/utils.py index 489dee5584c..40879a533f4 100644 --- a/homeassistant/components/homekit_controller/utils.py +++ b/homeassistant/components/homekit_controller/utils.py @@ -1,4 +1,5 @@ """Helper functions for the homekit_controller component.""" + from functools import lru_cache from typing import cast @@ -76,7 +77,9 @@ async def async_get_controller(hass: HomeAssistant) -> Controller: # Right now _async_stop_homekit_controller is only called on HA exiting # So we don't have to worry about leaking a callback here. - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_stop_homekit_controller) + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, _async_stop_homekit_controller, run_immediately=True + ) await controller.async_start() diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py index 364bfd11210..80345866b1f 100644 --- a/homeassistant/components/homematic/__init__.py +++ b/homeassistant/components/homematic/__init__.py @@ -1,4 +1,5 @@ """Support for HomeMatic devices.""" + from datetime import datetime from functools import partial import logging @@ -258,9 +259,7 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, hass.data[DATA_HOMEMATIC].stop) # Init homematic hubs - entity_hubs = [] - for hub_name in conf[CONF_HOSTS]: - entity_hubs.append(HMHub(hass, homematic, hub_name)) + entity_hubs = [HMHub(hass, homematic, hub_name) for hub_name in conf[CONF_HOSTS]] def _hm_service_virtualkey(service: ServiceCall) -> None: """Service to handle virtualkey servicecalls.""" diff --git a/homeassistant/components/homematic/binary_sensor.py b/homeassistant/components/homematic/binary_sensor.py index ba6d9781293..0d94c2bb78b 100644 --- a/homeassistant/components/homematic/binary_sensor.py +++ b/homeassistant/components/homematic/binary_sensor.py @@ -1,4 +1,5 @@ """Support for HomeMatic binary sensors.""" + from __future__ import annotations from homeassistant.components.binary_sensor import ( diff --git a/homeassistant/components/homematic/climate.py b/homeassistant/components/homematic/climate.py index 76d9dff4d46..efdb9324f76 100644 --- a/homeassistant/components/homematic/climate.py +++ b/homeassistant/components/homematic/climate.py @@ -1,4 +1,5 @@ """Support for Homematic thermostats.""" + from __future__ import annotations from typing import Any @@ -112,11 +113,7 @@ class HMThermostat(HMDevice, ClimateEntity): @property def preset_modes(self): """Return a list of available preset modes.""" - preset_modes = [] - for mode in self._hmdevice.ACTIONNODE: - if mode in HM_PRESET_MAP: - preset_modes.append(HM_PRESET_MAP[mode]) - return preset_modes + return [HM_PRESET_MAP[mode] for mode in self._hmdevice.ACTIONNODE] @property def current_humidity(self): diff --git a/homeassistant/components/homematic/cover.py b/homeassistant/components/homematic/cover.py index d9fb44219f6..b9f4a4fa96a 100644 --- a/homeassistant/components/homematic/cover.py +++ b/homeassistant/components/homematic/cover.py @@ -1,4 +1,5 @@ """Support for HomeMatic covers.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/homematic/entity.py b/homeassistant/components/homematic/entity.py index 700ef5cdc94..b728e85f959 100644 --- a/homeassistant/components/homematic/entity.py +++ b/homeassistant/components/homematic/entity.py @@ -1,4 +1,5 @@ """Homematic base entity.""" + from __future__ import annotations from abc import abstractmethod diff --git a/homeassistant/components/homematic/icons.json b/homeassistant/components/homematic/icons.json new file mode 100644 index 00000000000..998c9a385ba --- /dev/null +++ b/homeassistant/components/homematic/icons.json @@ -0,0 +1,10 @@ +{ + "services": { + "virtualkey": "mdi:keyboard", + "set_variable_value": "mdi:console", + "set_device_value": "mdi:television", + "reconnect": "mdi:wifi-refresh", + "set_install_mode": "mdi:cog", + "put_paramset": "mdi:cog" + } +} diff --git a/homeassistant/components/homematic/light.py b/homeassistant/components/homematic/light.py index 39e6df9d0ec..b05cc6a46d6 100644 --- a/homeassistant/components/homematic/light.py +++ b/homeassistant/components/homematic/light.py @@ -1,4 +1,5 @@ """Support for Homematic lights.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/homematic/lock.py b/homeassistant/components/homematic/lock.py index abca46ddf58..b79f28f2bc7 100644 --- a/homeassistant/components/homematic/lock.py +++ b/homeassistant/components/homematic/lock.py @@ -1,4 +1,5 @@ """Support for Homematic locks.""" + from __future__ import annotations from typing import Any @@ -22,11 +23,7 @@ def setup_platform( if discovery_info is None: return - devices = [] - for conf in discovery_info[ATTR_DISCOVER_DEVICES]: - devices.append(HMLock(conf)) - - add_entities(devices, True) + add_entities((HMLock(conf) for conf in discovery_info[ATTR_DISCOVER_DEVICES]), True) class HMLock(HMDevice, LockEntity): diff --git a/homeassistant/components/homematic/manifest.json b/homeassistant/components/homematic/manifest.json index 063f48d74b4..9c67a5da0b2 100644 --- a/homeassistant/components/homematic/manifest.json +++ b/homeassistant/components/homematic/manifest.json @@ -1,7 +1,7 @@ { "domain": "homematic", "name": "Homematic", - "codeowners": ["@pvizeli", "@danielperna84"], + "codeowners": ["@pvizeli"], "documentation": "https://www.home-assistant.io/integrations/homematic", "iot_class": "local_push", "loggers": ["pyhomematic"], diff --git a/homeassistant/components/homematic/notify.py b/homeassistant/components/homematic/notify.py index 33a06336104..6b7e71bb7a9 100644 --- a/homeassistant/components/homematic/notify.py +++ b/homeassistant/components/homematic/notify.py @@ -1,4 +1,5 @@ """Notification support for Homematic.""" + from __future__ import annotations import voluptuous as vol diff --git a/homeassistant/components/homematic/sensor.py b/homeassistant/components/homematic/sensor.py index 8b5d7b6bff1..eebcad95446 100644 --- a/homeassistant/components/homematic/sensor.py +++ b/homeassistant/components/homematic/sensor.py @@ -1,4 +1,5 @@ """Support for HomeMatic sensors.""" + from __future__ import annotations from copy import copy diff --git a/homeassistant/components/homematic/switch.py b/homeassistant/components/homematic/switch.py index 7accb011ebf..5f7c1f93dc8 100644 --- a/homeassistant/components/homematic/switch.py +++ b/homeassistant/components/homematic/switch.py @@ -1,4 +1,5 @@ """Support for HomeMatic switches.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/homematicip_cloud/__init__.py b/homeassistant/components/homematicip_cloud/__init__.py index 2f7d8d86012..2b2ddb64700 100644 --- a/homeassistant/components/homematicip_cloud/__init__.py +++ b/homeassistant/components/homematicip_cloud/__init__.py @@ -1,4 +1,5 @@ """Support for HomematicIP Cloud devices.""" + import voluptuous as vol from homeassistant import config_entries diff --git a/homeassistant/components/homematicip_cloud/alarm_control_panel.py b/homeassistant/components/homematicip_cloud/alarm_control_panel.py index 1a2f2293c1c..2913896d511 100644 --- a/homeassistant/components/homematicip_cloud/alarm_control_panel.py +++ b/homeassistant/components/homematicip_cloud/alarm_control_panel.py @@ -1,4 +1,5 @@ """Support for HomematicIP Cloud alarm control panel.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/homematicip_cloud/binary_sensor.py b/homeassistant/components/homematicip_cloud/binary_sensor.py index 2afe803e1eb..29d8576f060 100644 --- a/homeassistant/components/homematicip_cloud/binary_sensor.py +++ b/homeassistant/components/homematicip_cloud/binary_sensor.py @@ -1,4 +1,5 @@ """Support for HomematicIP Cloud binary sensor.""" + from __future__ import annotations from typing import Any @@ -85,15 +86,15 @@ async def async_setup_entry( if isinstance(device, AsyncTiltVibrationSensor): entities.append(HomematicipTiltVibrationSensor(hap, device)) if isinstance(device, AsyncWiredInput32): - for channel in range(1, 33): - entities.append( - HomematicipMultiContactInterface(hap, device, channel=channel) - ) + entities.extend( + HomematicipMultiContactInterface(hap, device, channel=channel) + for channel in range(1, 33) + ) elif isinstance(device, AsyncFullFlushContactInterface6): - for channel in range(1, 7): - entities.append( - HomematicipMultiContactInterface(hap, device, channel=channel) - ) + entities.extend( + HomematicipMultiContactInterface(hap, device, channel=channel) + for channel in range(1, 7) + ) elif isinstance( device, (AsyncContactInterface, AsyncFullFlushContactInterface) ): diff --git a/homeassistant/components/homematicip_cloud/button.py b/homeassistant/components/homematicip_cloud/button.py index 3fb8ebe20bd..c2707f68a89 100644 --- a/homeassistant/components/homematicip_cloud/button.py +++ b/homeassistant/components/homematicip_cloud/button.py @@ -1,4 +1,5 @@ """Support for HomematicIP Cloud button devices.""" + from __future__ import annotations from homematicip.aio.device import AsyncWallMountedGarageDoorController @@ -19,13 +20,12 @@ async def async_setup_entry( ) -> None: """Set up the HomematicIP button from a config entry.""" hap = hass.data[HMIPC_DOMAIN][config_entry.unique_id] - entities: list[HomematicipGenericEntity] = [] - for device in hap.home.devices: - if isinstance(device, AsyncWallMountedGarageDoorController): - entities.append(HomematicipGarageDoorControllerButton(hap, device)) - if entities: - async_add_entities(entities) + async_add_entities( + HomematicipGarageDoorControllerButton(hap, device) + for device in hap.home.devices + if isinstance(device, AsyncWallMountedGarageDoorController) + ) class HomematicipGarageDoorControllerButton(HomematicipGenericEntity, ButtonEntity): diff --git a/homeassistant/components/homematicip_cloud/climate.py b/homeassistant/components/homematicip_cloud/climate.py index 63b78e91a2f..b0eb2a9edfa 100644 --- a/homeassistant/components/homematicip_cloud/climate.py +++ b/homeassistant/components/homematicip_cloud/climate.py @@ -1,4 +1,5 @@ """Support for HomematicIP Cloud climate devices.""" + from __future__ import annotations from typing import Any @@ -50,12 +51,12 @@ async def async_setup_entry( ) -> None: """Set up the HomematicIP climate from a config entry.""" hap = hass.data[HMIPC_DOMAIN][config_entry.unique_id] - entities = [] - for device in hap.home.groups: - if isinstance(device, AsyncHeatingGroup): - entities.append(HomematicipHeatingGroup(hap, device)) - async_add_entities(entities) + async_add_entities( + HomematicipHeatingGroup(hap, device) + for device in hap.home.groups + if isinstance(device, AsyncHeatingGroup) + ) class HomematicipHeatingGroup(HomematicipGenericEntity, ClimateEntity): @@ -304,11 +305,7 @@ class HomematicipHeatingGroup(HomematicipGenericEntity, ClimateEntity): @property def _has_switch(self) -> bool: """Return, if a switch is in the hmip heating group.""" - for device in self._device.devices: - if isinstance(device, Switch): - return True - - return False + return any(isinstance(device, Switch) for device in self._device.devices) @property def _has_radiator_thermostat(self) -> bool: diff --git a/homeassistant/components/homematicip_cloud/config_flow.py b/homeassistant/components/homematicip_cloud/config_flow.py index 8581367d4ee..c2277e16c79 100644 --- a/homeassistant/components/homematicip_cloud/config_flow.py +++ b/homeassistant/components/homematicip_cloud/config_flow.py @@ -1,18 +1,18 @@ """Config flow to configure the HomematicIP Cloud component.""" + 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 homeassistant.config_entries import ConfigFlow, ConfigFlowResult from .const import _LOGGER, DOMAIN, HMIPC_AUTHTOKEN, HMIPC_HAPID, HMIPC_NAME, HMIPC_PIN from .hap import HomematicipAuth -class HomematicipCloudFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class HomematicipCloudFlowHandler(ConfigFlow, domain=DOMAIN): """Config flow for the HomematicIP Cloud component.""" VERSION = 1 @@ -24,13 +24,13 @@ class HomematicipCloudFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" return await self.async_step_init(user_input) async def async_step_init( self, user_input: dict[str, str] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow start.""" errors = {} @@ -61,7 +61,7 @@ class HomematicipCloudFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_link(self, user_input: None = None) -> FlowResult: + async def async_step_link(self, user_input: None = None) -> ConfigFlowResult: """Attempt to link with the HomematicIP Cloud access point.""" errors = {} @@ -83,7 +83,7 @@ class HomematicipCloudFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form(step_id="link", errors=errors) - async def async_step_import(self, import_info: dict[str, str]) -> FlowResult: + async def async_step_import(self, import_info: dict[str, str]) -> ConfigFlowResult: """Import a new access point as a config entry.""" hapid = import_info[HMIPC_HAPID].replace("-", "").upper() authtoken = import_info[HMIPC_AUTHTOKEN] diff --git a/homeassistant/components/homematicip_cloud/const.py b/homeassistant/components/homematicip_cloud/const.py index 4ea1a2fc7e0..bba67e10d4c 100644 --- a/homeassistant/components/homematicip_cloud/const.py +++ b/homeassistant/components/homematicip_cloud/const.py @@ -1,4 +1,5 @@ """Constants for the HomematicIP Cloud component.""" + import logging from homeassistant.const import Platform diff --git a/homeassistant/components/homematicip_cloud/cover.py b/homeassistant/components/homematicip_cloud/cover.py index f5a9919579c..b0cff8b6a10 100644 --- a/homeassistant/components/homematicip_cloud/cover.py +++ b/homeassistant/components/homematicip_cloud/cover.py @@ -1,4 +1,5 @@ """Support for HomematicIP Cloud cover devices.""" + from __future__ import annotations from typing import Any @@ -40,15 +41,19 @@ async def async_setup_entry( ) -> None: """Set up the HomematicIP cover from a config entry.""" hap = hass.data[HMIPC_DOMAIN][config_entry.unique_id] - entities: list[HomematicipGenericEntity] = [] + entities: list[HomematicipGenericEntity] = [ + HomematicipCoverShutterGroup(hap, group) + for group in hap.home.groups + if isinstance(group, AsyncExtendedLinkedShutterGroup) + ] for device in hap.home.devices: if isinstance(device, AsyncBlindModule): entities.append(HomematicipBlindModule(hap, device)) elif isinstance(device, AsyncDinRailBlind4): - for channel in range(1, 5): - entities.append( - HomematicipMultiCoverSlats(hap, device, channel=channel) - ) + entities.extend( + HomematicipMultiCoverSlats(hap, device, channel=channel) + for channel in range(1, 5) + ) elif isinstance(device, AsyncFullFlushBlind): entities.append(HomematicipCoverSlats(hap, device)) elif isinstance(device, AsyncFullFlushShutter): @@ -58,10 +63,6 @@ async def async_setup_entry( ): entities.append(HomematicipGarageDoorModule(hap, device)) - for group in hap.home.groups: - if isinstance(group, AsyncExtendedLinkedShutterGroup): - entities.append(HomematicipCoverShutterGroup(hap, group)) - async_add_entities(entities) diff --git a/homeassistant/components/homematicip_cloud/errors.py b/homeassistant/components/homematicip_cloud/errors.py index 1102cde6fbe..bbee58f7a41 100644 --- a/homeassistant/components/homematicip_cloud/errors.py +++ b/homeassistant/components/homematicip_cloud/errors.py @@ -1,4 +1,5 @@ """Errors for the HomematicIP Cloud component.""" + from homeassistant.exceptions import HomeAssistantError diff --git a/homeassistant/components/homematicip_cloud/generic_entity.py b/homeassistant/components/homematicip_cloud/generic_entity.py index 46d036c777b..c75649e6886 100644 --- a/homeassistant/components/homematicip_cloud/generic_entity.py +++ b/homeassistant/components/homematicip_cloud/generic_entity.py @@ -1,4 +1,5 @@ """Generic entity for the HomematicIP Cloud component.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/homematicip_cloud/hap.py b/homeassistant/components/homematicip_cloud/hap.py index b40b6ec56f6..058b7ec6c00 100644 --- a/homeassistant/components/homematicip_cloud/hap.py +++ b/homeassistant/components/homematicip_cloud/hap.py @@ -1,4 +1,5 @@ """Access point for the HomematicIP Cloud component.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/homematicip_cloud/helpers.py b/homeassistant/components/homematicip_cloud/helpers.py index 4647e553382..43edca4774a 100644 --- a/homeassistant/components/homematicip_cloud/helpers.py +++ b/homeassistant/components/homematicip_cloud/helpers.py @@ -1,4 +1,5 @@ """Helper functions for Homematicip Cloud Integration.""" + from __future__ import annotations from collections.abc import Callable, Coroutine diff --git a/homeassistant/components/homematicip_cloud/light.py b/homeassistant/components/homematicip_cloud/light.py index 3000024db43..17daafc5896 100644 --- a/homeassistant/components/homematicip_cloud/light.py +++ b/homeassistant/components/homematicip_cloud/light.py @@ -1,4 +1,5 @@ """Support for HomematicIP Cloud lights.""" + from __future__ import annotations from typing import Any @@ -55,8 +56,10 @@ async def async_setup_entry( ) ) elif isinstance(device, (AsyncWiredDimmer3, AsyncDinRailDimmer3)): - for channel in range(1, 4): - entities.append(HomematicipMultiDimmer(hap, device, channel=channel)) + entities.extend( + HomematicipMultiDimmer(hap, device, channel=channel) + for channel in range(1, 4) + ) elif isinstance( device, (AsyncDimmer, AsyncPluggableDimmer, AsyncBrandDimmer, AsyncFullFlushDimmer), diff --git a/homeassistant/components/homematicip_cloud/lock.py b/homeassistant/components/homematicip_cloud/lock.py index 563f0103060..cf98828598f 100644 --- a/homeassistant/components/homematicip_cloud/lock.py +++ b/homeassistant/components/homematicip_cloud/lock.py @@ -1,4 +1,5 @@ """Support for HomematicIP Cloud lock devices.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/homematicip_cloud/sensor.py b/homeassistant/components/homematicip_cloud/sensor.py index 04fba2cfd92..d344639bbc9 100644 --- a/homeassistant/components/homematicip_cloud/sensor.py +++ b/homeassistant/components/homematicip_cloud/sensor.py @@ -1,4 +1,5 @@ """Support for HomematicIP Cloud sensors.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/homematicip_cloud/services.py b/homeassistant/components/homematicip_cloud/services.py index 38ce6de7caf..37cda9e7683 100644 --- a/homeassistant/components/homematicip_cloud/services.py +++ b/homeassistant/components/homematicip_cloud/services.py @@ -1,4 +1,5 @@ """Support for HomematicIP Cloud devices.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/homematicip_cloud/switch.py b/homeassistant/components/homematicip_cloud/switch.py index 2b5f2f01cd3..9aa60d45d93 100644 --- a/homeassistant/components/homematicip_cloud/switch.py +++ b/homeassistant/components/homematicip_cloud/switch.py @@ -1,4 +1,5 @@ """Support for HomematicIP Cloud switches.""" + from __future__ import annotations from typing import Any @@ -38,7 +39,11 @@ async def async_setup_entry( ) -> None: """Set up the HomematicIP switch from a config entry.""" hap = hass.data[HMIPC_DOMAIN][config_entry.unique_id] - entities: list[HomematicipGenericEntity] = [] + entities: list[HomematicipGenericEntity] = [ + HomematicipGroupSwitch(hap, group) + for group in hap.home.groups + if isinstance(group, (AsyncExtendedLinkedSwitchingGroup, AsyncSwitchingGroup)) + ] for device in hap.home.devices: if isinstance(device, AsyncBrandSwitchMeasuring): # BrandSwitchMeasuring inherits PlugableSwitchMeasuring @@ -50,13 +55,17 @@ async def async_setup_entry( ): entities.append(HomematicipSwitchMeasuring(hap, device)) elif isinstance(device, AsyncWiredSwitch8): - for channel in range(1, 9): - entities.append(HomematicipMultiSwitch(hap, device, channel=channel)) + entities.extend( + HomematicipMultiSwitch(hap, device, channel=channel) + for channel in range(1, 9) + ) elif isinstance(device, AsyncDinRailSwitch): entities.append(HomematicipMultiSwitch(hap, device, channel=1)) elif isinstance(device, AsyncDinRailSwitch4): - for channel in range(1, 5): - entities.append(HomematicipMultiSwitch(hap, device, channel=channel)) + entities.extend( + HomematicipMultiSwitch(hap, device, channel=channel) + for channel in range(1, 5) + ) elif isinstance( device, ( @@ -67,8 +76,10 @@ async def async_setup_entry( ): entities.append(HomematicipSwitch(hap, device)) elif isinstance(device, AsyncOpenCollector8Module): - for channel in range(1, 9): - entities.append(HomematicipMultiSwitch(hap, device, channel=channel)) + entities.extend( + HomematicipMultiSwitch(hap, device, channel=channel) + for channel in range(1, 9) + ) elif isinstance( device, ( @@ -78,12 +89,10 @@ async def async_setup_entry( AsyncMultiIOBox, ), ): - for channel in range(1, 3): - entities.append(HomematicipMultiSwitch(hap, device, channel=channel)) - - for group in hap.home.groups: - if isinstance(group, (AsyncExtendedLinkedSwitchingGroup, AsyncSwitchingGroup)): - entities.append(HomematicipGroupSwitch(hap, group)) + entities.extend( + HomematicipMultiSwitch(hap, device, channel=channel) + for channel in range(1, 3) + ) async_add_entities(entities) diff --git a/homeassistant/components/homematicip_cloud/weather.py b/homeassistant/components/homematicip_cloud/weather.py index 573f291d557..34e3f58d6ef 100644 --- a/homeassistant/components/homematicip_cloud/weather.py +++ b/homeassistant/components/homematicip_cloud/weather.py @@ -1,4 +1,5 @@ """Support for HomematicIP Cloud weather devices.""" + from __future__ import annotations from homematicip.aio.device import ( diff --git a/homeassistant/components/homewizard/__init__.py b/homeassistant/components/homewizard/__init__.py index 35b303a62e3..6efcd7be75b 100644 --- a/homeassistant/components/homewizard/__init__.py +++ b/homeassistant/components/homewizard/__init__.py @@ -1,4 +1,5 @@ """The Homewizard integration.""" + from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady diff --git a/homeassistant/components/homewizard/config_flow.py b/homeassistant/components/homewizard/config_flow.py index bf425fe5c41..70ef47a4f03 100644 --- a/homeassistant/components/homewizard/config_flow.py +++ b/homeassistant/components/homewizard/config_flow.py @@ -1,4 +1,5 @@ """Config flow for HomeWizard.""" + from __future__ import annotations from collections.abc import Mapping @@ -11,9 +12,9 @@ from homewizard_energy.models import Device from voluptuous import Required, Schema from homeassistant.components import onboarding, zeroconf -from homeassistant.config_entries import ConfigEntry, ConfigFlow +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_IP_ADDRESS, CONF_PATH -from homeassistant.data_entry_flow import AbortFlow, FlowResult +from homeassistant.data_entry_flow import AbortFlow from homeassistant.exceptions import HomeAssistantError from .const import ( @@ -46,7 +47,7 @@ class HomeWizardConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initiated by the user.""" errors: dict[str, str] | None = None if user_input is not None: @@ -65,11 +66,14 @@ class HomeWizardConfigFlow(ConfigFlow, domain=DOMAIN): data=user_input, ) + user_input = user_input or {} return self.async_show_form( step_id="user", data_schema=Schema( { - Required(CONF_IP_ADDRESS): str, + Required( + CONF_IP_ADDRESS, default=user_input.get(CONF_IP_ADDRESS) + ): str, } ), errors=errors, @@ -77,7 +81,7 @@ class HomeWizardConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_zeroconf( self, discovery_info: zeroconf.ZeroconfServiceInfo - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle zeroconf discovery.""" if ( CONF_API_ENABLED not in discovery_info.properties @@ -109,7 +113,7 @@ class HomeWizardConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_discovery_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm discovery.""" errors: dict[str, str] | None = None if user_input is not None or not onboarding.async_is_onboarded(self.hass): @@ -143,14 +147,16 @@ class HomeWizardConfigFlow(ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Handle re-auth if API was disabled.""" self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) return await self.async_step_reauth_confirm() async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm reauth dialog.""" errors: dict[str, str] | None = None if user_input is not None: diff --git a/homeassistant/components/homewizard/const.py b/homeassistant/components/homewizard/const.py index f1a1bee2568..8cee8350268 100644 --- a/homeassistant/components/homewizard/const.py +++ b/homeassistant/components/homewizard/const.py @@ -1,4 +1,5 @@ """Constants for the Homewizard integration.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/homeassistant/components/homewizard/coordinator.py b/homeassistant/components/homewizard/coordinator.py index e38b1d54471..db41d1dd128 100644 --- a/homeassistant/components/homewizard/coordinator.py +++ b/homeassistant/components/homewizard/coordinator.py @@ -1,10 +1,11 @@ """Update coordinator for HomeWizard.""" + from __future__ import annotations import logging from homewizard_energy import HomeWizardEnergy -from homewizard_energy.const import SUPPORTS_IDENTIFY, SUPPORTS_STATE, SUPPORTS_SYSTEM +from homewizard_energy.const import SUPPORTS_IDENTIFY, SUPPORTS_STATE from homewizard_energy.errors import DisabledError, RequestError, UnsupportedError from homewizard_energy.models import Device @@ -52,8 +53,7 @@ class HWEnergyDeviceUpdateCoordinator(DataUpdateCoordinator[DeviceResponseEntry] if self.supports_state(data.device): data.state = await self.api.state() - if self.supports_system(data.device): - data.system = await self.api.system() + data.system = await self.api.system() except UnsupportedError as ex: # Old firmware, ignore @@ -93,13 +93,6 @@ class HWEnergyDeviceUpdateCoordinator(DataUpdateCoordinator[DeviceResponseEntry] return device.product_type in SUPPORTS_STATE - def supports_system(self, device: Device | None = None) -> bool: - """Return True if the device supports system.""" - if device is None: - device = self.data.device - - return device.product_type in SUPPORTS_SYSTEM - def supports_identify(self, device: Device | None = None) -> bool: """Return True if the device supports identify.""" if device is None: diff --git a/homeassistant/components/homewizard/diagnostics.py b/homeassistant/components/homewizard/diagnostics.py index b8103f7a4cb..82d9b7d72d1 100644 --- a/homeassistant/components/homewizard/diagnostics.py +++ b/homeassistant/components/homewizard/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for P1 Monitor.""" + from __future__ import annotations from dataclasses import asdict diff --git a/homeassistant/components/homewizard/entity.py b/homeassistant/components/homewizard/entity.py index 2090cc363ba..9ba54c8218c 100644 --- a/homeassistant/components/homewizard/entity.py +++ b/homeassistant/components/homewizard/entity.py @@ -1,4 +1,5 @@ """Base entity for the HomeWizard integration.""" + from __future__ import annotations from homeassistant.const import ATTR_CONNECTIONS, ATTR_IDENTIFIERS diff --git a/homeassistant/components/homewizard/helpers.py b/homeassistant/components/homewizard/helpers.py index 4c3ae76a327..a3eda4ad565 100644 --- a/homeassistant/components/homewizard/helpers.py +++ b/homeassistant/components/homewizard/helpers.py @@ -1,4 +1,5 @@ """Helpers for HomeWizard.""" + from __future__ import annotations from collections.abc import Callable, Coroutine @@ -32,7 +33,6 @@ def homewizard_exception_handler( await func(self, *args, **kwargs) except RequestError as ex: raise HomeAssistantError( - "An error occurred while communicating with HomeWizard device", translation_domain=DOMAIN, translation_key="communication_error", ) from ex @@ -41,7 +41,6 @@ def homewizard_exception_handler( self.coordinator.config_entry.entry_id ) raise HomeAssistantError( - "The local API of the HomeWizard device is disabled", translation_domain=DOMAIN, translation_key="api_disabled", ) from ex diff --git a/homeassistant/components/homewizard/manifest.json b/homeassistant/components/homewizard/manifest.json index 149d5b891f4..7355d9405df 100644 --- a/homeassistant/components/homewizard/manifest.json +++ b/homeassistant/components/homewizard/manifest.json @@ -7,6 +7,6 @@ "iot_class": "local_polling", "loggers": ["homewizard_energy"], "quality_scale": "platinum", - "requirements": ["python-homewizard-energy==4.3.1"], + "requirements": ["python-homewizard-energy==v5.0.0"], "zeroconf": ["_hwenergy._tcp.local."] } diff --git a/homeassistant/components/homewizard/number.py b/homeassistant/components/homewizard/number.py index 6145db125a1..c6261c62a9b 100644 --- a/homeassistant/components/homewizard/number.py +++ b/homeassistant/components/homewizard/number.py @@ -1,4 +1,5 @@ """Creates HomeWizard Number entities.""" + from __future__ import annotations from homeassistant.components.number import NumberEntity diff --git a/homeassistant/components/homewizard/sensor.py b/homeassistant/components/homewizard/sensor.py index e544ee601c0..19102e5b985 100644 --- a/homeassistant/components/homewizard/sensor.py +++ b/homeassistant/components/homewizard/sensor.py @@ -1,4 +1,5 @@ """Creates HomeWizard sensor entities.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/homewizard/switch.py b/homeassistant/components/homewizard/switch.py index 72e0f43a2cf..299eb9e806b 100644 --- a/homeassistant/components/homewizard/switch.py +++ b/homeassistant/components/homewizard/switch.py @@ -1,4 +1,5 @@ """Creates HomeWizard Energy switch entities.""" + from __future__ import annotations from collections.abc import Awaitable, Callable @@ -56,7 +57,7 @@ SWITCHES = [ key="cloud_connection", translation_key="cloud_connection", entity_category=EntityCategory.CONFIG, - create_fn=lambda coordinator: coordinator.supports_system(), + create_fn=lambda _: True, available_fn=lambda data: data.system is not None, is_on_fn=lambda data: data.system.cloud_enabled if data.system else None, set_fn=lambda api, active: api.system_set(cloud_enabled=active), diff --git a/homeassistant/components/homeworks/__init__.py b/homeassistant/components/homeworks/__init__.py index 3d9023d16cb..83ae12dffba 100644 --- a/homeassistant/components/homeworks/__init__.py +++ b/homeassistant/components/homeworks/__init__.py @@ -1,9 +1,15 @@ """Support for Lutron Homeworks Series 4 and 8 systems.""" + +from __future__ import annotations + +from dataclasses import dataclass import logging +from typing import Any from pyhomeworks.pyhomeworks import HW_BUTTON_PRESSED, HW_BUTTON_RELEASED, Homeworks import voluptuous as vol +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( CONF_HOST, CONF_ID, @@ -12,28 +18,34 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, Platform, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.discovery import load_platform +from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import ConfigType from homeassistant.util import slugify +from .const import ( + CONF_ADDR, + CONF_CONTROLLER_ID, + CONF_DIMMERS, + CONF_KEYPADS, + CONF_RATE, + DOMAIN, +) + _LOGGER = logging.getLogger(__name__) -DOMAIN = "homeworks" +PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.BUTTON, Platform.LIGHT] -HOMEWORKS_CONTROLLER = "homeworks" EVENT_BUTTON_PRESS = "homeworks_button_press" EVENT_BUTTON_RELEASE = "homeworks_button_release" -CONF_DIMMERS = "dimmers" -CONF_KEYPADS = "keypads" -CONF_ADDR = "addr" -CONF_RATE = "rate" +DEFAULT_FADE_RATE = 1.0 -FADE_RATE = 1.0 +KEYPAD_LEDSTATE_POLL_COOLDOWN = 1.0 CV_FADE_RATE = vol.All(vol.Coerce(float), vol.Range(min=0, max=20)) @@ -41,7 +53,7 @@ DIMMER_SCHEMA = vol.Schema( { vol.Required(CONF_ADDR): cv.string, vol.Required(CONF_NAME): cv.string, - vol.Optional(CONF_RATE, default=FADE_RATE): CV_FADE_RATE, + vol.Optional(CONF_RATE, default=DEFAULT_FADE_RATE): CV_FADE_RATE, } ) @@ -66,67 +78,156 @@ CONFIG_SCHEMA = vol.Schema( ) -def setup(hass: HomeAssistant, base_config: ConfigType) -> bool: +@dataclass +class HomeworksData: + """Container for config entry data.""" + + controller: Homeworks + controller_id: str + keypads: dict[str, HomeworksKeypad] + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Start Homeworks controller.""" - def hw_callback(msg_type, values): - """Dispatch state changes.""" - _LOGGER.debug("callback: %s, %s", msg_type, values) - addr = values[0] - signal = f"homeworks_entity_{addr}" - dispatcher_send(hass, signal, msg_type, values) - - config = base_config[DOMAIN] - controller = Homeworks(config[CONF_HOST], config[CONF_PORT], hw_callback) - hass.data[HOMEWORKS_CONTROLLER] = controller - - def cleanup(event): - controller.close() - - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, cleanup) - - dimmers = config[CONF_DIMMERS] - load_platform(hass, Platform.LIGHT, DOMAIN, {CONF_DIMMERS: dimmers}, base_config) - - for key_config in config[CONF_KEYPADS]: - addr = key_config[CONF_ADDR] - name = key_config[CONF_NAME] - HomeworksKeypadEvent(hass, addr, name) + if DOMAIN in config: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=config[DOMAIN] + ) + ) return True -class HomeworksDevice(Entity): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Homeworks from a config entry.""" + + hass.data.setdefault(DOMAIN, {}) + controller_id = entry.options[CONF_CONTROLLER_ID] + + def hw_callback(msg_type: Any, values: Any) -> None: + """Dispatch state changes.""" + _LOGGER.debug("callback: %s, %s", msg_type, values) + addr = values[0] + signal = f"homeworks_entity_{controller_id}_{addr}" + dispatcher_send(hass, signal, msg_type, values) + + config = entry.options + try: + controller = await hass.async_add_executor_job( + Homeworks, config[CONF_HOST], config[CONF_PORT], hw_callback + ) + except (ConnectionError, OSError) as err: + raise ConfigEntryNotReady from err + + def cleanup(event: Event) -> None: + controller.close() + + entry.async_on_unload(hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, cleanup)) + + keypads: dict[str, HomeworksKeypad] = {} + for key_config in config.get(CONF_KEYPADS, []): + addr = key_config[CONF_ADDR] + name = key_config[CONF_NAME] + keypads[addr] = HomeworksKeypad(hass, controller, controller_id, addr, name) + + hass.data[DOMAIN][entry.entry_id] = HomeworksData( + controller, controller_id, keypads + ) + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + entry.async_on_unload(entry.add_update_listener(update_listener)) + + 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 + + data: HomeworksData = hass.data[DOMAIN].pop(entry.entry_id) + for keypad in data.keypads.values(): + keypad.unsubscribe() + + await hass.async_add_executor_job(data.controller.close) + + return True + + +async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Handle options update.""" + await hass.config_entries.async_reload(entry.entry_id) + + +def calculate_unique_id(controller_id: str, addr: str, idx: int) -> str: + """Calculate entity unique id.""" + return f"homeworks.{controller_id}.{addr}.{idx}" + + +class HomeworksEntity(Entity): """Base class of a Homeworks device.""" + _attr_has_entity_name = True _attr_should_poll = False - def __init__(self, controller, addr, name): + def __init__( + self, + controller: Homeworks, + controller_id: str, + addr: str, + idx: int, + name: str | None, + ) -> None: """Initialize Homeworks device.""" self._addr = addr + self._idx = idx + self._controller_id = controller_id self._attr_name = name - self._attr_unique_id = f"homeworks.{self._addr}" + self._attr_unique_id = calculate_unique_id( + self._controller_id, self._addr, self._idx + ) self._controller = controller + self._attr_extra_state_attributes = {"homeworks_address": self._addr} -class HomeworksKeypadEvent: +class HomeworksKeypad: """When you want signals instead of entities. Stateless sensors such as keypads are expected to generate an event instead of a sensor entity in hass. """ - def __init__(self, hass, addr, name): + def __init__( + self, + hass: HomeAssistant, + controller: Homeworks, + controller_id: str, + addr: str, + name: str, + ) -> None: """Register callback that will be used for signals.""" - self._hass = hass self._addr = addr + self._controller = controller + self._debouncer = Debouncer( + hass, + _LOGGER, + cooldown=KEYPAD_LEDSTATE_POLL_COOLDOWN, + immediate=False, + function=self._request_keypad_led_states, + ) + self._hass = hass self._name = name self._id = slugify(self._name) - signal = f"homeworks_entity_{self._addr}" - async_dispatcher_connect(self._hass, signal, self._update_callback) + signal = f"homeworks_entity_{controller_id}_{self._addr}" + _LOGGER.debug("connecting %s", signal) + self.unsubscribe = async_dispatcher_connect( + self._hass, signal, self._update_callback + ) @callback - def _update_callback(self, msg_type, values): + def _update_callback(self, msg_type: str, values: list[Any]) -> None: """Fire events if button is pressed or released.""" if msg_type == HW_BUTTON_PRESSED: @@ -137,3 +238,15 @@ class HomeworksKeypadEvent: return data = {CONF_ID: self._id, CONF_NAME: self._name, "button": values[1]} self._hass.bus.async_fire(event, data) + + def _request_keypad_led_states(self) -> None: + """Query keypad led state.""" + # pylint: disable-next=protected-access + self._controller._send(f"RKLS, {self._addr}") + + async def request_keypad_led_states(self) -> None: + """Query keypad led state. + + Debounced to not storm the controller during setup. + """ + await self._debouncer.async_call() diff --git a/homeassistant/components/homeworks/binary_sensor.py b/homeassistant/components/homeworks/binary_sensor.py new file mode 100644 index 00000000000..9773411d26d --- /dev/null +++ b/homeassistant/components/homeworks/binary_sensor.py @@ -0,0 +1,94 @@ +"""Support for Lutron Homeworks binary sensors.""" + +from __future__ import annotations + +import logging +from typing import Any + +from pyhomeworks.pyhomeworks import HW_KEYPAD_LED_CHANGED, Homeworks + +from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_NAME +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import HomeworksData, HomeworksEntity, HomeworksKeypad +from .const import ( + CONF_ADDR, + CONF_BUTTONS, + CONF_CONTROLLER_ID, + CONF_KEYPADS, + CONF_LED, + CONF_NUMBER, + DOMAIN, +) + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up Homeworks binary sensors.""" + data: HomeworksData = hass.data[DOMAIN][entry.entry_id] + controller = data.controller + controller_id = entry.options[CONF_CONTROLLER_ID] + devs = [] + for keypad in entry.options.get(CONF_KEYPADS, []): + for button in keypad[CONF_BUTTONS]: + if not button[CONF_LED]: + continue + dev = HomeworksBinarySensor( + controller, + data.keypads[keypad[CONF_ADDR]], + controller_id, + keypad[CONF_ADDR], + keypad[CONF_NAME], + button[CONF_NAME], + button[CONF_NUMBER], + ) + devs.append(dev) + async_add_entities(devs, True) + + +class HomeworksBinarySensor(HomeworksEntity, BinarySensorEntity): + """Homeworks Binary Sensor.""" + + _attr_has_entity_name = True + + def __init__( + self, + controller: Homeworks, + keypad: HomeworksKeypad, + controller_id: str, + addr: str, + keypad_name: str, + button_name: str, + led_number: int, + ) -> None: + """Create device with Addr, name, and rate.""" + super().__init__(controller, controller_id, addr, led_number, button_name) + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, f"{controller_id}.{addr}")}, name=keypad_name + ) + self._keypad = keypad + + async def async_added_to_hass(self) -> None: + """Call when entity is added to hass.""" + signal = f"homeworks_entity_{self._controller_id}_{self._addr}" + _LOGGER.debug("connecting %s", signal) + self.async_on_remove( + async_dispatcher_connect(self.hass, signal, self._update_callback) + ) + await self._keypad.request_keypad_led_states() + + @callback + def _update_callback(self, msg_type: str, values: list[Any]) -> None: + """Process device specific messages.""" + if msg_type != HW_KEYPAD_LED_CHANGED or len(values[1]) < self._idx: + return + self._attr_is_on = bool(values[1][self._idx - 1]) + self.async_write_ha_state() diff --git a/homeassistant/components/homeworks/button.py b/homeassistant/components/homeworks/button.py new file mode 100644 index 00000000000..c8cb616d95b --- /dev/null +++ b/homeassistant/components/homeworks/button.py @@ -0,0 +1,79 @@ +"""Support for Lutron Homeworks buttons.""" + +from __future__ import annotations + +from time import sleep + +from pyhomeworks.pyhomeworks import Homeworks + +from homeassistant.components.button import ButtonEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_NAME +from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import HomeworksData, HomeworksEntity +from .const import ( + CONF_ADDR, + CONF_BUTTONS, + CONF_CONTROLLER_ID, + CONF_KEYPADS, + CONF_NUMBER, + CONF_RELEASE_DELAY, + DOMAIN, +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up Homeworks buttons.""" + data: HomeworksData = hass.data[DOMAIN][entry.entry_id] + controller = data.controller + controller_id = entry.options[CONF_CONTROLLER_ID] + devs = [] + for keypad in entry.options.get(CONF_KEYPADS, []): + for button in keypad[CONF_BUTTONS]: + dev = HomeworksButton( + controller, + controller_id, + keypad[CONF_ADDR], + keypad[CONF_NAME], + button[CONF_NAME], + button[CONF_NUMBER], + button[CONF_RELEASE_DELAY], + ) + devs.append(dev) + async_add_entities(devs, True) + + +class HomeworksButton(HomeworksEntity, ButtonEntity): + """Homeworks Button.""" + + def __init__( + self, + controller: Homeworks, + controller_id: str, + addr: str, + keypad_name: str, + button_name: str, + button_number: int, + release_delay: float, + ) -> None: + """Create device with Addr, name, and rate.""" + super().__init__(controller, controller_id, addr, button_number, button_name) + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, f"{controller_id}.{addr}")}, name=keypad_name + ) + self._release_delay = release_delay + + def press(self) -> None: + """Press the button.""" + # pylint: disable-next=protected-access + self._controller._send(f"KBP, {self._addr}, {self._idx}") + if not self._release_delay: + return + sleep(self._release_delay) + # pylint: disable-next=protected-access + self._controller._send(f"KBR, {self._addr}, {self._idx}") diff --git a/homeassistant/components/homeworks/config_flow.py b/homeassistant/components/homeworks/config_flow.py new file mode 100644 index 00000000000..b2fe4e0e022 --- /dev/null +++ b/homeassistant/components/homeworks/config_flow.py @@ -0,0 +1,740 @@ +"""Lutron Homeworks Series 4 and 8 config flow.""" + +from __future__ import annotations + +from functools import partial +import logging +from typing import Any + +from pyhomeworks.pyhomeworks import Homeworks +import voluptuous as vol + +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN +from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT +from homeassistant.core import ( + DOMAIN as HOMEASSISTANT_DOMAIN, + HomeAssistant, + async_get_hass, + callback, +) +from homeassistant.data_entry_flow import AbortFlow +from homeassistant.helpers import ( + config_validation as cv, + entity_registry as er, + issue_registry as ir, + selector, +) +from homeassistant.helpers.schema_config_entry_flow import ( + SchemaCommonFlowHandler, + SchemaFlowError, + SchemaFlowFormStep, + SchemaFlowMenuStep, + SchemaOptionsFlowHandler, +) +from homeassistant.helpers.selector import TextSelector +from homeassistant.util import slugify + +from . import DEFAULT_FADE_RATE, calculate_unique_id +from .const import ( + CONF_ADDR, + CONF_BUTTONS, + CONF_CONTROLLER_ID, + CONF_DIMMERS, + CONF_INDEX, + CONF_KEYPADS, + CONF_LED, + CONF_NUMBER, + CONF_RATE, + CONF_RELEASE_DELAY, + DEFAULT_BUTTON_NAME, + DEFAULT_KEYPAD_NAME, + DEFAULT_LIGHT_NAME, + DOMAIN, +) + +_LOGGER = logging.getLogger(__name__) + +CONTROLLER_EDIT = { + vol.Required(CONF_HOST): selector.TextSelector(), + vol.Required(CONF_PORT): selector.NumberSelector( + selector.NumberSelectorConfig( + min=1, + max=65535, + mode=selector.NumberSelectorMode.BOX, + ) + ), +} + +LIGHT_EDIT = { + vol.Optional(CONF_RATE, default=DEFAULT_FADE_RATE): selector.NumberSelector( + selector.NumberSelectorConfig( + min=0, + max=20, + mode=selector.NumberSelectorMode.SLIDER, + step=0.1, + ) + ), +} + +BUTTON_EDIT = { + vol.Optional(CONF_LED, default=False): selector.BooleanSelector(), + vol.Optional(CONF_RELEASE_DELAY, default=0): selector.NumberSelector( + selector.NumberSelectorConfig( + min=0, + max=5, + step=0.01, + mode=selector.NumberSelectorMode.BOX, + unit_of_measurement="s", + ), + ), +} + + +validate_addr = cv.matches_regex(r"\[\d\d:\d\d:\d\d:\d\d\]") + + +async def validate_add_controller( + handler: ConfigFlow | SchemaOptionsFlowHandler, user_input: dict[str, Any] +) -> dict[str, Any]: + """Validate controller setup.""" + user_input[CONF_CONTROLLER_ID] = slugify(user_input[CONF_NAME]) + user_input[CONF_PORT] = int(user_input[CONF_PORT]) + try: + handler._async_abort_entries_match( # pylint: disable=protected-access + {CONF_HOST: user_input[CONF_HOST], CONF_PORT: user_input[CONF_PORT]} + ) + except AbortFlow as err: + raise SchemaFlowError("duplicated_host_port") from err + + try: + handler._async_abort_entries_match( # pylint: disable=protected-access + {CONF_CONTROLLER_ID: user_input[CONF_CONTROLLER_ID]} + ) + except AbortFlow as err: + raise SchemaFlowError("duplicated_controller_id") from err + + await _try_connection(user_input) + + return user_input + + +async def _try_connection(user_input: dict[str, Any]) -> None: + """Try connecting to the controller.""" + + def _try_connect(host: str, port: int) -> None: + """Try connecting to the controller. + + Raises ConnectionError if the connection fails. + """ + _LOGGER.debug( + "Trying to connect to %s:%s", user_input[CONF_HOST], user_input[CONF_PORT] + ) + controller = Homeworks(host, port, lambda msg_types, values: None) + controller.close() + controller.join() + + hass = async_get_hass() + try: + await hass.async_add_executor_job( + _try_connect, user_input[CONF_HOST], user_input[CONF_PORT] + ) + except ConnectionError as err: + raise SchemaFlowError("connection_error") from err + except Exception as err: + _LOGGER.exception("Caught unexpected exception") + raise SchemaFlowError("unknown_error") from err + + +def _create_import_issue(hass: HomeAssistant) -> None: + """Create a repair issue asking the user to remove YAML.""" + ir.async_create_issue( + hass, + HOMEASSISTANT_DOMAIN, + f"deprecated_yaml_{DOMAIN}", + breaks_in_ha_version="2024.6.0", + is_fixable=False, + issue_domain=DOMAIN, + severity=ir.IssueSeverity.WARNING, + translation_key="deprecated_yaml", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": "Lutron Homeworks", + }, + ) + + +def _validate_address(handler: SchemaCommonFlowHandler, addr: str) -> None: + """Validate address.""" + try: + validate_addr(addr) + except vol.Invalid as err: + raise SchemaFlowError("invalid_addr") from err + + for _key in (CONF_DIMMERS, CONF_KEYPADS): + items: list[dict[str, Any]] = handler.options[_key] + + for item in items: + if item[CONF_ADDR] == addr: + raise SchemaFlowError("duplicated_addr") + + +def _validate_button_number(handler: SchemaCommonFlowHandler, number: int) -> None: + """Validate button number.""" + keypad = handler.flow_state["_idx"] + buttons: list[dict[str, Any]] = handler.options[CONF_KEYPADS][keypad][CONF_BUTTONS] + + for button in buttons: + if button[CONF_NUMBER] == number: + raise SchemaFlowError("duplicated_number") + + +async def validate_add_button( + handler: SchemaCommonFlowHandler, user_input: dict[str, Any] +) -> dict[str, Any]: + """Validate button input.""" + user_input[CONF_NUMBER] = int(user_input[CONF_NUMBER]) + _validate_button_number(handler, user_input[CONF_NUMBER]) + + # Standard behavior is to merge the result with the options. + # In this case, we want to add a sub-item so we update the options directly. + keypad = handler.flow_state["_idx"] + buttons: list[dict[str, Any]] = handler.options[CONF_KEYPADS][keypad][CONF_BUTTONS] + buttons.append(user_input) + return {} + + +async def validate_add_keypad( + handler: SchemaCommonFlowHandler, user_input: dict[str, Any] +) -> dict[str, Any]: + """Validate keypad or light input.""" + _validate_address(handler, user_input[CONF_ADDR]) + + # Standard behavior is to merge the result with the options. + # In this case, we want to add a sub-item so we update the options directly. + items = handler.options[CONF_KEYPADS] + items.append(user_input | {CONF_BUTTONS: []}) + return {} + + +async def validate_add_light( + handler: SchemaCommonFlowHandler, user_input: dict[str, Any] +) -> dict[str, Any]: + """Validate light input.""" + _validate_address(handler, user_input[CONF_ADDR]) + + # Standard behavior is to merge the result with the options. + # In this case, we want to add a sub-item so we update the options directly. + items = handler.options[CONF_DIMMERS] + items.append(user_input) + return {} + + +async def get_select_button_schema(handler: SchemaCommonFlowHandler) -> vol.Schema: + """Return schema for selecting a button.""" + keypad = handler.flow_state["_idx"] + buttons: list[dict[str, Any]] = handler.options[CONF_KEYPADS][keypad][CONF_BUTTONS] + + return vol.Schema( + { + vol.Required(CONF_INDEX): vol.In( + { + str(index): f"{config[CONF_NAME]} ({config[CONF_NUMBER]})" + for index, config in enumerate(buttons) + }, + ) + } + ) + + +async def get_select_keypad_schema(handler: SchemaCommonFlowHandler) -> vol.Schema: + """Return schema for selecting a keypad.""" + return vol.Schema( + { + vol.Required(CONF_INDEX): vol.In( + { + str(index): f"{config[CONF_NAME]} ({config[CONF_ADDR]})" + for index, config in enumerate(handler.options[CONF_KEYPADS]) + }, + ) + } + ) + + +async def get_select_light_schema(handler: SchemaCommonFlowHandler) -> vol.Schema: + """Return schema for selecting a light.""" + return vol.Schema( + { + vol.Required(CONF_INDEX): vol.In( + { + str(index): f"{config[CONF_NAME]} ({config[CONF_ADDR]})" + for index, config in enumerate(handler.options[CONF_DIMMERS]) + }, + ) + } + ) + + +async def validate_select_button( + handler: SchemaCommonFlowHandler, user_input: dict[str, Any] +) -> dict[str, Any]: + """Store button index in flow state.""" + handler.flow_state["_button_idx"] = int(user_input[CONF_INDEX]) + return {} + + +async def validate_select_keypad_light( + handler: SchemaCommonFlowHandler, user_input: dict[str, Any] +) -> dict[str, Any]: + """Store keypad or light index in flow state.""" + handler.flow_state["_idx"] = int(user_input[CONF_INDEX]) + return {} + + +async def get_edit_button_suggested_values( + handler: SchemaCommonFlowHandler, +) -> dict[str, Any]: + """Return suggested values for button editing.""" + keypad_idx: int = handler.flow_state["_idx"] + button_idx: int = handler.flow_state["_button_idx"] + return dict(handler.options[CONF_KEYPADS][keypad_idx][CONF_BUTTONS][button_idx]) + + +async def get_edit_light_suggested_values( + handler: SchemaCommonFlowHandler, +) -> dict[str, Any]: + """Return suggested values for light editing.""" + idx: int = handler.flow_state["_idx"] + return dict(handler.options[CONF_DIMMERS][idx]) + + +async def validate_button_edit( + handler: SchemaCommonFlowHandler, user_input: dict[str, Any] +) -> dict[str, Any]: + """Update edited keypad or light.""" + # Standard behavior is to merge the result with the options. + # In this case, we want to add a sub-item so we update the options directly. + keypad_idx: int = handler.flow_state["_idx"] + button_idx: int = handler.flow_state["_button_idx"] + buttons: list[dict] = handler.options[CONF_KEYPADS][keypad_idx][CONF_BUTTONS] + buttons[button_idx].update(user_input) + return {} + + +async def validate_light_edit( + handler: SchemaCommonFlowHandler, user_input: dict[str, Any] +) -> dict[str, Any]: + """Update edited keypad or light.""" + # Standard behavior is to merge the result with the options. + # In this case, we want to add a sub-item so we update the options directly. + idx: int = handler.flow_state["_idx"] + handler.options[CONF_DIMMERS][idx].update(user_input) + return {} + + +async def get_remove_button_schema(handler: SchemaCommonFlowHandler) -> vol.Schema: + """Return schema for button removal.""" + keypad_idx: int = handler.flow_state["_idx"] + buttons: list[dict] = handler.options[CONF_KEYPADS][keypad_idx][CONF_BUTTONS] + return vol.Schema( + { + vol.Required(CONF_INDEX): cv.multi_select( + { + str(index): f"{config[CONF_NAME]} ({config[CONF_NUMBER]})" + for index, config in enumerate(buttons) + }, + ) + } + ) + + +async def get_remove_keypad_light_schema( + handler: SchemaCommonFlowHandler, *, key: str +) -> vol.Schema: + """Return schema for keypad or light removal.""" + return vol.Schema( + { + vol.Required(CONF_INDEX): cv.multi_select( + { + str(index): f"{config[CONF_NAME]} ({config[CONF_ADDR]})" + for index, config in enumerate(handler.options[key]) + }, + ) + } + ) + + +async def validate_remove_button( + handler: SchemaCommonFlowHandler, user_input: dict[str, Any] +) -> dict[str, Any]: + """Validate remove keypad or light.""" + removed_indexes: set[str] = set(user_input[CONF_INDEX]) + + # Standard behavior is to merge the result with the options. + # In this case, we want to remove sub-items so we update the options directly. + entity_registry = er.async_get(handler.parent_handler.hass) + keypad_idx: int = handler.flow_state["_idx"] + keypad: dict = handler.options[CONF_KEYPADS][keypad_idx] + items: list[dict[str, Any]] = [] + item: dict[str, Any] + for index, item in enumerate(keypad[CONF_BUTTONS]): + if str(index) not in removed_indexes: + items.append(item) + button_number = keypad[CONF_BUTTONS][index][CONF_NUMBER] + for domain in (BINARY_SENSOR_DOMAIN, BUTTON_DOMAIN): + if entity_id := entity_registry.async_get_entity_id( + domain, + DOMAIN, + calculate_unique_id( + handler.options[CONF_CONTROLLER_ID], + keypad[CONF_ADDR], + button_number, + ), + ): + entity_registry.async_remove(entity_id) + keypad[CONF_BUTTONS] = items + return {} + + +async def validate_remove_keypad_light( + handler: SchemaCommonFlowHandler, user_input: dict[str, Any], *, key: str +) -> dict[str, Any]: + """Validate remove keypad or light.""" + removed_indexes: set[str] = set(user_input[CONF_INDEX]) + + # Standard behavior is to merge the result with the options. + # In this case, we want to remove sub-items so we update the options directly. + entity_registry = er.async_get(handler.parent_handler.hass) + items: list[dict[str, Any]] = [] + item: dict[str, Any] + for index, item in enumerate(handler.options[key]): + if str(index) not in removed_indexes: + items.append(item) + elif key != CONF_DIMMERS: + continue + if entity_id := entity_registry.async_get_entity_id( + LIGHT_DOMAIN, + DOMAIN, + calculate_unique_id( + handler.options[CONF_CONTROLLER_ID], item[CONF_ADDR], 0 + ), + ): + entity_registry.async_remove(entity_id) + handler.options[key] = items + return {} + + +DATA_SCHEMA_ADD_CONTROLLER = vol.Schema( + { + vol.Required( + CONF_NAME, description={"suggested_value": "Lutron Homeworks"} + ): selector.TextSelector(), + **CONTROLLER_EDIT, + } +) +DATA_SCHEMA_EDIT_CONTROLLER = vol.Schema(CONTROLLER_EDIT) +DATA_SCHEMA_ADD_LIGHT = vol.Schema( + { + vol.Optional(CONF_NAME, default=DEFAULT_LIGHT_NAME): TextSelector(), + vol.Required(CONF_ADDR): TextSelector(), + **LIGHT_EDIT, + } +) +DATA_SCHEMA_ADD_KEYPAD = vol.Schema( + { + vol.Optional(CONF_NAME, default=DEFAULT_KEYPAD_NAME): TextSelector(), + vol.Required(CONF_ADDR): TextSelector(), + } +) +DATA_SCHEMA_ADD_BUTTON = vol.Schema( + { + vol.Optional(CONF_NAME, default=DEFAULT_BUTTON_NAME): TextSelector(), + vol.Required(CONF_NUMBER): selector.NumberSelector( + selector.NumberSelectorConfig( + min=1, + max=24, + step=1, + mode=selector.NumberSelectorMode.BOX, + ), + ), + **BUTTON_EDIT, + } +) +DATA_SCHEMA_EDIT_BUTTON = vol.Schema(BUTTON_EDIT) +DATA_SCHEMA_EDIT_LIGHT = vol.Schema(LIGHT_EDIT) + +OPTIONS_FLOW = { + "init": SchemaFlowMenuStep( + [ + "add_keypad", + "select_edit_keypad", + "remove_keypad", + "add_light", + "select_edit_light", + "remove_light", + ] + ), + "add_keypad": SchemaFlowFormStep( + DATA_SCHEMA_ADD_KEYPAD, + suggested_values=None, + validate_user_input=validate_add_keypad, + ), + "select_edit_keypad": SchemaFlowFormStep( + get_select_keypad_schema, + suggested_values=None, + validate_user_input=validate_select_keypad_light, + next_step="edit_keypad", + ), + "edit_keypad": SchemaFlowMenuStep( + [ + "add_button", + "select_edit_button", + "remove_button", + ] + ), + "add_button": SchemaFlowFormStep( + DATA_SCHEMA_ADD_BUTTON, + suggested_values=None, + validate_user_input=validate_add_button, + ), + "select_edit_button": SchemaFlowFormStep( + get_select_button_schema, + suggested_values=None, + validate_user_input=validate_select_button, + next_step="edit_button", + ), + "edit_button": SchemaFlowFormStep( + DATA_SCHEMA_EDIT_BUTTON, + suggested_values=get_edit_button_suggested_values, + validate_user_input=validate_button_edit, + ), + "remove_button": SchemaFlowFormStep( + get_remove_button_schema, + suggested_values=None, + validate_user_input=validate_remove_button, + ), + "remove_keypad": SchemaFlowFormStep( + partial(get_remove_keypad_light_schema, key=CONF_KEYPADS), + suggested_values=None, + validate_user_input=partial(validate_remove_keypad_light, key=CONF_KEYPADS), + ), + "add_light": SchemaFlowFormStep( + DATA_SCHEMA_ADD_LIGHT, + suggested_values=None, + validate_user_input=validate_add_light, + ), + "select_edit_light": SchemaFlowFormStep( + get_select_light_schema, + suggested_values=None, + validate_user_input=validate_select_keypad_light, + next_step="edit_light", + ), + "edit_light": SchemaFlowFormStep( + DATA_SCHEMA_EDIT_LIGHT, + suggested_values=get_edit_light_suggested_values, + validate_user_input=validate_light_edit, + ), + "remove_light": SchemaFlowFormStep( + partial(get_remove_keypad_light_schema, key=CONF_DIMMERS), + suggested_values=None, + validate_user_input=partial(validate_remove_keypad_light, key=CONF_DIMMERS), + ), +} + + +class HomeworksConfigFlowHandler(ConfigFlow, domain=DOMAIN): + """Config flow for Lutron Homeworks.""" + + import_config: dict[str, Any] + + async def async_step_import(self, config: dict[str, Any]) -> ConfigFlowResult: + """Start importing configuration from yaml.""" + self.import_config = { + CONF_HOST: config[CONF_HOST], + CONF_PORT: config[CONF_PORT], + CONF_DIMMERS: [ + { + CONF_ADDR: light[CONF_ADDR], + CONF_NAME: light[CONF_NAME], + CONF_RATE: light[CONF_RATE], + } + for light in config[CONF_DIMMERS] + ], + CONF_KEYPADS: [ + { + CONF_ADDR: keypad[CONF_ADDR], + CONF_BUTTONS: [ + { + CONF_LED: button[CONF_LED], + CONF_NAME: button[CONF_NAME], + CONF_NUMBER: button[CONF_NUMBER], + CONF_RELEASE_DELAY: button[CONF_RELEASE_DELAY], + } + for button in keypad[CONF_BUTTONS] + ], + CONF_NAME: keypad[CONF_NAME], + } + for keypad in config[CONF_KEYPADS] + ], + } + return await self.async_step_import_controller_name() + + async def async_step_import_controller_name( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Ask user to set a name of the controller.""" + errors = {} + try: + self._async_abort_entries_match( + { + CONF_HOST: self.import_config[CONF_HOST], + CONF_PORT: self.import_config[CONF_PORT], + } + ) + except AbortFlow: + _create_import_issue(self.hass) + raise + + if user_input: + try: + user_input[CONF_CONTROLLER_ID] = slugify(user_input[CONF_NAME]) + self._async_abort_entries_match( + {CONF_CONTROLLER_ID: user_input[CONF_CONTROLLER_ID]} + ) + except AbortFlow: + errors["base"] = "duplicated_controller_id" + else: + self.import_config |= user_input + return await self.async_step_import_finish() + + return self.async_show_form( + step_id="import_controller_name", + data_schema=vol.Schema( + { + vol.Required( + CONF_NAME, description={"suggested_value": "Lutron Homeworks"} + ): selector.TextSelector(), + } + ), + errors=errors, + ) + + async def async_step_import_finish( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Ask user to remove YAML configuration.""" + + if user_input is not None: + entity_registry = er.async_get(self.hass) + config = self.import_config + for light in config[CONF_DIMMERS]: + addr = light[CONF_ADDR] + if entity_id := entity_registry.async_get_entity_id( + LIGHT_DOMAIN, DOMAIN, f"homeworks.{addr}" + ): + entity_registry.async_update_entity( + entity_id, + new_unique_id=calculate_unique_id( + config[CONF_CONTROLLER_ID], addr, 0 + ), + ) + name = config.pop(CONF_NAME) + return self.async_create_entry( + title=name, + data={}, + options=config, + ) + + return self.async_show_form(step_id="import_finish", data_schema=vol.Schema({})) + + async def _validate_edit_controller( + self, user_input: dict[str, Any] + ) -> dict[str, Any]: + """Validate controller setup.""" + user_input[CONF_PORT] = int(user_input[CONF_PORT]) + + our_entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) + assert our_entry + other_entries = self._async_current_entries() + for entry in other_entries: + if entry.entry_id == our_entry.entry_id: + continue + if ( + user_input[CONF_HOST] == entry.options[CONF_HOST] + and user_input[CONF_PORT] == entry.options[CONF_PORT] + ): + raise SchemaFlowError("duplicated_host_port") + + await _try_connection(user_input) + return user_input + + async def async_step_reconfigure( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle a reconfigure flow.""" + entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) + assert entry + + errors = {} + suggested_values = { + CONF_HOST: entry.options[CONF_HOST], + CONF_PORT: entry.options[CONF_PORT], + } + + if user_input: + suggested_values = { + CONF_HOST: user_input[CONF_HOST], + CONF_PORT: user_input[CONF_PORT], + } + try: + await self._validate_edit_controller(user_input) + except SchemaFlowError as err: + errors["base"] = str(err) + else: + new_options = entry.options | { + CONF_HOST: user_input[CONF_HOST], + CONF_PORT: user_input[CONF_PORT], + } + return self.async_update_reload_and_abort( + entry, options=new_options, reason="reconfigure_successful" + ) + + return self.async_show_form( + step_id="reconfigure", + data_schema=self.add_suggested_values_to_schema( + DATA_SCHEMA_EDIT_CONTROLLER, suggested_values + ), + errors=errors, + ) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle a flow initialized by the user.""" + errors = {} + if user_input: + try: + await validate_add_controller(self, user_input) + except SchemaFlowError as err: + errors["base"] = str(err) + else: + self._async_abort_entries_match( + {CONF_HOST: user_input[CONF_HOST], CONF_PORT: user_input[CONF_PORT]} + ) + name = user_input.pop(CONF_NAME) + user_input |= {CONF_DIMMERS: [], CONF_KEYPADS: []} + return self.async_create_entry(title=name, data={}, options=user_input) + + return self.async_show_form( + step_id="user", + data_schema=DATA_SCHEMA_ADD_CONTROLLER, + errors=errors, + ) + + @staticmethod + @callback + def async_get_options_flow(config_entry: ConfigEntry) -> SchemaOptionsFlowHandler: + """Options flow handler for Lutron Homeworks.""" + return SchemaOptionsFlowHandler(config_entry, OPTIONS_FLOW) diff --git a/homeassistant/components/homeworks/const.py b/homeassistant/components/homeworks/const.py new file mode 100644 index 00000000000..8baf1b6299d --- /dev/null +++ b/homeassistant/components/homeworks/const.py @@ -0,0 +1,20 @@ +"""Constants for the Lutron Homeworks integration.""" + +from __future__ import annotations + +DOMAIN = "homeworks" + +CONF_ADDR = "addr" +CONF_BUTTONS = "buttons" +CONF_CONTROLLER_ID = "controller_id" +CONF_DIMMERS = "dimmers" +CONF_INDEX = "index" +CONF_KEYPADS = "keypads" +CONF_LED = "led" +CONF_NUMBER = "number" +CONF_RATE = "rate" +CONF_RELEASE_DELAY = "release_delay" + +DEFAULT_BUTTON_NAME = "Homeworks button" +DEFAULT_KEYPAD_NAME = "Homeworks keypad" +DEFAULT_LIGHT_NAME = "Homeworks light" diff --git a/homeassistant/components/homeworks/light.py b/homeassistant/components/homeworks/light.py index 35a9e5665d1..3e3c199c75c 100644 --- a/homeassistant/components/homeworks/light.py +++ b/homeassistant/components/homeworks/light.py @@ -1,59 +1,72 @@ """Support for Lutron Homeworks lights.""" + from __future__ import annotations import logging from typing import Any -from pyhomeworks.pyhomeworks import HW_LIGHT_CHANGED +from pyhomeworks.pyhomeworks import HW_LIGHT_CHANGED, Homeworks from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import CONF_ADDR, CONF_DIMMERS, CONF_RATE, HOMEWORKS_CONTROLLER, HomeworksDevice +from . import HomeworksData, HomeworksEntity +from .const import CONF_ADDR, CONF_CONTROLLER_ID, CONF_DIMMERS, CONF_RATE, DOMAIN _LOGGER = logging.getLogger(__name__) -def setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discover_info: DiscoveryInfoType | None = None, +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up Homeworks lights.""" - if discover_info is None: - return - - controller = hass.data[HOMEWORKS_CONTROLLER] + data: HomeworksData = hass.data[DOMAIN][entry.entry_id] + controller = data.controller + controller_id = entry.options[CONF_CONTROLLER_ID] devs = [] - for dimmer in discover_info[CONF_DIMMERS]: + for dimmer in entry.options.get(CONF_DIMMERS, []): dev = HomeworksLight( - controller, dimmer[CONF_ADDR], dimmer[CONF_NAME], dimmer[CONF_RATE] + controller, + controller_id, + dimmer[CONF_ADDR], + dimmer[CONF_NAME], + dimmer[CONF_RATE], ) devs.append(dev) - add_entities(devs, True) + async_add_entities(devs, True) -class HomeworksLight(HomeworksDevice, LightEntity): +class HomeworksLight(HomeworksEntity, LightEntity): """Homeworks Light.""" _attr_color_mode = ColorMode.BRIGHTNESS _attr_supported_color_modes = {ColorMode.BRIGHTNESS} - def __init__(self, controller, addr, name, rate): + def __init__( + self, + controller: Homeworks, + controller_id: str, + addr: str, + name: str, + rate: float, + ) -> None: """Create device with Addr, name, and rate.""" - super().__init__(controller, addr, name) + super().__init__(controller, controller_id, addr, 0, None) + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, f"{controller_id}.{addr}")}, name=name + ) self._rate = rate self._level = 0 self._prev_level = 0 async def async_added_to_hass(self) -> None: """Call when entity is added to hass.""" - signal = f"homeworks_entity_{self._addr}" + signal = f"homeworks_entity_{self._controller_id}_{self._addr}" _LOGGER.debug("connecting %s", signal) self.async_on_remove( async_dispatcher_connect(self.hass, signal, self._update_callback) @@ -75,28 +88,23 @@ class HomeworksLight(HomeworksDevice, LightEntity): self._set_brightness(0) @property - def brightness(self): + def brightness(self) -> int: """Control the brightness.""" return self._level - def _set_brightness(self, level): + def _set_brightness(self, level: int) -> None: """Send the brightness level to the device.""" self._controller.fade_dim( float((level * 100.0) / 255.0), self._rate, 0, self._addr ) @property - def extra_state_attributes(self): - """Supported attributes.""" - return {"homeworks_address": self._addr} - - @property - def is_on(self): + def is_on(self) -> bool: """Is the light on/off.""" return self._level != 0 @callback - def _update_callback(self, msg_type, values): + def _update_callback(self, msg_type: str, values: list[Any]) -> None: """Process device specific messages.""" if msg_type == HW_LIGHT_CHANGED: diff --git a/homeassistant/components/homeworks/manifest.json b/homeassistant/components/homeworks/manifest.json index 4a3f132e14d..c2520b910d9 100644 --- a/homeassistant/components/homeworks/manifest.json +++ b/homeassistant/components/homeworks/manifest.json @@ -2,6 +2,7 @@ "domain": "homeworks", "name": "Lutron Homeworks", "codeowners": [], + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homeworks", "iot_class": "local_push", "loggers": ["pyhomeworks"], diff --git a/homeassistant/components/homeworks/strings.json b/homeassistant/components/homeworks/strings.json new file mode 100644 index 00000000000..03c09e12888 --- /dev/null +++ b/homeassistant/components/homeworks/strings.json @@ -0,0 +1,146 @@ +{ + "config": { + "error": { + "connection_error": "Could not connect to the controller.", + "duplicated_controller_id": "The controller name is already in use.", + "duplicated_host_port": "The specified host and port is already configured.", + "unknown_error": "[%key:common::config_flow::error::unknown%]" + }, + "step": { + "import_finish": { + "description": "The existing YAML configuration has succesfully been imported.\n\nYou can now remove the `homeworks` configuration from your configuration.yaml file." + }, + "import_controller_name": { + "description": "Lutron Homeworks is no longer configured through configuration.yaml.\n\nPlease fill in the form to import the existing configuration to the UI.", + "data": { + "name": "[%key:component::homeworks::config::step::user::data::name%]" + }, + "data_description": { + "name": "[%key:component::homeworks::config::step::user::data_description::name%]" + } + }, + "reconfigure": { + "data": { + "host": "[%key:common::config_flow::data::host%]", + "port": "[%key:common::config_flow::data::port%]" + }, + "description": "Modify a Lutron Homeworks controller connection settings" + }, + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]", + "name": "Controller name", + "port": "[%key:common::config_flow::data::port%]" + }, + "data_description": { + "name": "A unique name identifying the Lutron Homeworks controller" + }, + "description": "Add a Lutron Homeworks controller" + } + } + }, + "options": { + "error": { + "duplicated_addr": "The specified address is already in use", + "duplicated_number": "The specified number is already in use", + "invalid_addr": "Invalid address" + }, + "step": { + "init": { + "menu_options": { + "add_keypad": "Add keypad", + "add_light": "Add light", + "remove_keypad": "Remove keypad", + "remove_light": "Remove light", + "select_edit_keypad": "Configure keypad", + "select_edit_light": "Configure light" + } + }, + "add_button": { + "data": { + "name": "[%key:common::config_flow::data::name%]", + "number": "Number", + "led": "LED", + "release_delay": "Release delay" + }, + "data_description": { + "number": "Button number in the range 1 to 24", + "led": "Enable if the button has a scene select indicator", + "release_delay": "Time between press and release, set to 0 to only press" + }, + "title": "[%key:component::homeworks::options::step::init::menu_options::add_keypad%]" + }, + "add_keypad": { + "data": { + "name": "[%key:common::config_flow::data::name%]", + "addr": "Address" + }, + "data_description": { + "addr": "Keypad address, must be formatted as `[##:##:##:##]`" + }, + "title": "[%key:component::homeworks::options::step::init::menu_options::add_keypad%]" + }, + "add_light": { + "data": { + "name": "[%key:common::config_flow::data::name%]", + "addr": "[%key:component::homeworks::options::step::add_keypad::data::addr%]", + "rate": "Fade rate" + }, + "data_description": { + "addr": "Keypad address, must be formatted as `[##:##:##:##]`", + "rate": "Time in seconds for the light to transition to a new brightness level" + }, + "title": "[%key:component::homeworks::options::step::init::menu_options::add_light%]" + }, + "edit_button": { + "data": { + "led": "[%key:component::homeworks::options::step::add_button::data::led%]", + "release_delay": "[%key:component::homeworks::options::step::add_button::data::release_delay%]" + }, + "data_description": { + "led": "[%key:component::homeworks::options::step::add_button::data_description::led%]", + "release_delay": "[%key:component::homeworks::options::step::add_button::data_description::release_delay%]" + }, + "title": "[%key:component::homeworks::options::step::edit_keypad::menu_options::select_edit_button%]" + }, + "edit_keypad": { + "menu_options": { + "add_button": "Add button", + "remove_button": "Remove button", + "select_edit_button": "Configure button" + } + }, + "edit_light": { + "data": { + "rate": "[%key:component::homeworks::options::step::add_light::data::rate%]" + }, + "data_description": { + "rate": "[%key:component::homeworks::options::step::add_light::data_description::rate%]" + }, + "description": "Select a light to configure", + "title": "[%key:component::homeworks::options::step::init::menu_options::select_edit_light%]" + }, + "remove_button": { + "description": "Select buttons to remove", + "title": "[%key:component::homeworks::options::step::edit_keypad::menu_options::remove_button%]" + }, + "remove_keypad": { + "description": "Select keypads to remove", + "title": "[%key:component::homeworks::options::step::init::menu_options::remove_keypad%]" + }, + "remove_light": { + "description": "Select lights to remove", + "title": "[%key:component::homeworks::options::step::init::menu_options::remove_light%]" + }, + "select_edit_button": { + "title": "[%key:component::homeworks::options::step::edit_keypad::menu_options::select_edit_button%]" + }, + "select_edit_keypad": { + "title": "[%key:component::homeworks::options::step::init::menu_options::select_edit_keypad%]" + }, + "select_edit_light": { + "title": "[%key:component::homeworks::options::step::init::menu_options::select_edit_light%]" + } + } + } +} diff --git a/homeassistant/components/honeywell/__init__.py b/homeassistant/components/honeywell/__init__.py index f58db72a07e..c1c46e2b7af 100644 --- a/homeassistant/components/honeywell/__init__.py +++ b/homeassistant/components/honeywell/__init__.py @@ -1,4 +1,5 @@ """Support for Honeywell (US) Total Connect Comfort climate systems.""" + from dataclasses import dataclass import aiosomecomfort diff --git a/homeassistant/components/honeywell/climate.py b/homeassistant/components/honeywell/climate.py index fb8537ce36b..5ac5e8a2472 100644 --- a/homeassistant/components/honeywell/climate.py +++ b/homeassistant/components/honeywell/climate.py @@ -1,4 +1,5 @@ """Support for Honeywell (US) Total Connect Comfort climate systems.""" + from __future__ import annotations import datetime diff --git a/homeassistant/components/honeywell/config_flow.py b/homeassistant/components/honeywell/config_flow.py index aeb72899e11..85877046bc0 100644 --- a/homeassistant/components/honeywell/config_flow.py +++ b/homeassistant/components/honeywell/config_flow.py @@ -1,4 +1,5 @@ """Config flow to configure the honeywell integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -7,10 +8,14 @@ from typing import Any import aiosomecomfort import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import ( @@ -29,13 +34,15 @@ REAUTH_SCHEMA = vol.Schema( ) -class HoneywellConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class HoneywellConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a honeywell config flow.""" VERSION = 1 - entry: config_entries.ConfigEntry | None + entry: ConfigEntry | None - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Handle re-authentication with Honeywell.""" self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) @@ -43,7 +50,7 @@ class HoneywellConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm re-authentication with Honeywell.""" errors: dict[str, str] = {} assert self.entry is not None @@ -81,7 +88,7 @@ class HoneywellConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_user(self, user_input=None) -> FlowResult: + async def async_step_user(self, user_input=None) -> ConfigFlowResult: """Create config entry. Show the setup form to the user.""" errors = {} if user_input is not None: @@ -124,20 +131,20 @@ class HoneywellConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback def async_get_options_flow( - config_entry: config_entries.ConfigEntry, + config_entry: ConfigEntry, ) -> HoneywellOptionsFlowHandler: """Options callback for Honeywell.""" return HoneywellOptionsFlowHandler(config_entry) -class HoneywellOptionsFlowHandler(config_entries.OptionsFlow): +class HoneywellOptionsFlowHandler(OptionsFlow): """Config flow options for Honeywell.""" - def __init__(self, entry: config_entries.ConfigEntry) -> None: + def __init__(self, entry: ConfigEntry) -> None: """Initialize Honeywell options flow.""" self.config_entry = entry - async def async_step_init(self, user_input=None) -> FlowResult: + async def async_step_init(self, user_input=None) -> ConfigFlowResult: """Manage the options.""" if user_input is not None: return self.async_create_entry(title=DOMAIN, data=user_input) diff --git a/homeassistant/components/honeywell/const.py b/homeassistant/components/honeywell/const.py index 28868812e24..9f0034a0623 100644 --- a/homeassistant/components/honeywell/const.py +++ b/homeassistant/components/honeywell/const.py @@ -1,4 +1,5 @@ """Support for Honeywell (US) Total Connect Comfort climate systems.""" + import logging DOMAIN = "honeywell" diff --git a/homeassistant/components/honeywell/diagnostics.py b/homeassistant/components/honeywell/diagnostics.py index 4aebfc4c905..b489eb4a596 100644 --- a/homeassistant/components/honeywell/diagnostics.py +++ b/homeassistant/components/honeywell/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for Honeywell.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/honeywell/sensor.py b/homeassistant/components/honeywell/sensor.py index 0841b7df1cc..31ed8d646c5 100644 --- a/homeassistant/components/honeywell/sensor.py +++ b/homeassistant/components/honeywell/sensor.py @@ -1,4 +1,5 @@ """Support for Honeywell (US) Total Connect Comfort sensors.""" + from __future__ import annotations from collections.abc import Callable @@ -36,21 +37,14 @@ def _get_temperature_sensor_unit(device: Device) -> str: return UnitOfTemperature.FAHRENHEIT -@dataclass(frozen=True) -class HoneywellSensorEntityDescriptionMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class HoneywellSensorEntityDescription(SensorEntityDescription): + """Describes a Honeywell sensor entity.""" value_fn: Callable[[Device], Any] unit_fn: Callable[[Device], Any] -@dataclass(frozen=True) -class HoneywellSensorEntityDescription( - SensorEntityDescription, HoneywellSensorEntityDescriptionMixin -): - """Describes a Honeywell sensor entity.""" - - SENSOR_TYPES: tuple[HoneywellSensorEntityDescription, ...] = ( HoneywellSensorEntityDescription( key=OUTDOOR_TEMPERATURE_STATUS_KEY, @@ -92,14 +86,13 @@ async def async_setup_entry( ) -> None: """Set up the Honeywell thermostat.""" data: HoneywellData = hass.data[DOMAIN][config_entry.entry_id] - sensors = [] - for device in data.devices.values(): - for description in SENSOR_TYPES: - if getattr(device, description.key) is not None: - sensors.append(HoneywellSensor(device, description)) - - async_add_entities(sensors) + async_add_entities( + HoneywellSensor(device, description) + for device in data.devices.values() + for description in SENSOR_TYPES + if getattr(device, description.key) is not None + ) class HoneywellSensor(SensorEntity): diff --git a/homeassistant/components/horizon/media_player.py b/homeassistant/components/horizon/media_player.py index d91fe7019d6..c03bcc73f41 100644 --- a/homeassistant/components/horizon/media_player.py +++ b/homeassistant/components/horizon/media_player.py @@ -1,4 +1,5 @@ """Support for the Unitymedia Horizon HD Recorder.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/hp_ilo/sensor.py b/homeassistant/components/hp_ilo/sensor.py index fcb8788c646..8d29b20381d 100644 --- a/homeassistant/components/hp_ilo/sensor.py +++ b/homeassistant/components/hp_ilo/sensor.py @@ -1,4 +1,5 @@ """Support for information from HP iLO sensors.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/html5/const.py b/homeassistant/components/html5/const.py index 1d0689511b2..bf7eaca7e24 100644 --- a/homeassistant/components/html5/const.py +++ b/homeassistant/components/html5/const.py @@ -1,3 +1,4 @@ """Constants for the HTML5 component.""" + DOMAIN = "html5" SERVICE_DISMISS = "dismiss" diff --git a/homeassistant/components/html5/icons.json b/homeassistant/components/html5/icons.json new file mode 100644 index 00000000000..c3d6e27efda --- /dev/null +++ b/homeassistant/components/html5/icons.json @@ -0,0 +1,5 @@ +{ + "services": { + "dismiss": "mdi:bell-off" + } +} diff --git a/homeassistant/components/html5/notify.py b/homeassistant/components/html5/notify.py index d65a4c42488..782340dffa6 100644 --- a/homeassistant/components/html5/notify.py +++ b/homeassistant/components/html5/notify.py @@ -1,4 +1,5 @@ """HTML5 Push Messaging notification service.""" + from __future__ import annotations from contextlib import suppress @@ -19,7 +20,7 @@ import voluptuous as vol from voluptuous.humanize import humanize_error from homeassistant.components import websocket_api -from homeassistant.components.http import HomeAssistantView +from homeassistant.components.http import KEY_HASS, HomeAssistantView from homeassistant.components.notify import ( ATTR_DATA, ATTR_TARGET, @@ -231,7 +232,7 @@ class HTML5PushRegistrationView(HomeAssistantView): self.registrations[name] = data try: - hass = request.app["hass"] + hass = request.app[KEY_HASS] await hass.async_add_executor_job( save_json, self.json_path, self.registrations @@ -279,7 +280,7 @@ class HTML5PushRegistrationView(HomeAssistantView): reg = self.registrations.pop(found) try: - hass = request.app["hass"] + hass = request.app[KEY_HASS] await hass.async_add_executor_job( save_json, self.json_path, self.registrations @@ -388,7 +389,7 @@ class HTML5PushCallbackView(HomeAssistantView): ) event_name = f"{NOTIFY_CALLBACK_EVENT}.{event_payload[ATTR_TYPE]}" - request.app["hass"].bus.fire(event_name, event_payload) + request.app[KEY_HASS].bus.fire(event_name, event_payload) return self.json({"status": "ok", "event": event_payload[ATTR_TYPE]}) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index ab228e32a52..c9f8c21e0a3 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -1,4 +1,5 @@ """Support to serve the Home Assistant API as WSGI application.""" + from __future__ import annotations import asyncio @@ -33,20 +34,27 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import storage import homeassistant.helpers.config_validation as cv from homeassistant.helpers.http import ( + KEY_ALLOW_CONFIGRED_CORS, KEY_AUTHENTICATED, # noqa: F401 + KEY_HASS, HomeAssistantView, current_request, ) from homeassistant.helpers.network import NoURLAvailableError, get_url from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass -from homeassistant.setup import async_start_setup, async_when_setup_or_start +from homeassistant.setup import ( + SetupPhases, + async_start_setup, + async_when_setup_or_start, +) from homeassistant.util import dt as dt_util, ssl as ssl_util +from homeassistant.util.async_ import create_eager_task from homeassistant.util.json import json_loads from .auth import async_setup_auth from .ban import setup_bans -from .const import KEY_HASS, KEY_HASS_REFRESH_TOKEN_ID, KEY_HASS_USER # noqa: F401 +from .const import KEY_HASS_REFRESH_TOKEN_ID, KEY_HASS_USER # noqa: F401 from .cors import setup_cors from .decorators import require_admin # noqa: F401 from .forwarded import async_setup_forwarded @@ -188,6 +196,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: login_threshold = conf[CONF_LOGIN_ATTEMPTS_THRESHOLD] ssl_profile = conf[CONF_SSL_PROFILE] + source_ip_task = create_eager_task(async_get_source_ip(hass)) + server = HomeAssistantHTTP( hass, server_host=server_host, @@ -212,7 +222,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def start_server(*_: Any) -> None: """Start the server.""" - with async_start_setup(hass, ["http"]): + with async_start_setup(hass, integration="http", phase=SetupPhases.SETUP): hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_server) # We already checked it's not None. assert conf is not None @@ -222,7 +232,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: hass.http = server - local_ip = await async_get_source_ip(hass) + local_ip = await source_ip_task host = local_ip if server_host is not None: @@ -320,6 +330,7 @@ class HomeAssistantHTTP: ) -> None: """Initialize the server.""" self.app[KEY_HASS] = self.hass + self.app["hass"] = self.hass # For backwards compatibility # Order matters, security filters middleware needs to go first, # forwarded middleware needs to go second. @@ -384,7 +395,7 @@ class HomeAssistantHTTP: # Should be instance of aiohttp.web_exceptions._HTTPMove. raise redirect_exc(redirect_to) # type: ignore[arg-type,misc] - self.app["allow_configured_cors"]( + self.app[KEY_ALLOW_CONFIGRED_CORS]( self.app.router.add_route("GET", url, redirect) ) @@ -400,7 +411,7 @@ class HomeAssistantHTTP: else: resource = web.StaticResource(url_path, path) self.app.router.register_resource(resource) - self.app["allow_configured_cors"](resource) + self.app[KEY_ALLOW_CONFIGRED_CORS](resource) return async def serve_file(request: web.Request) -> web.FileResponse: @@ -409,7 +420,7 @@ class HomeAssistantHTTP: return web.FileResponse(path, headers=CACHE_HEADERS) return web.FileResponse(path) - self.app["allow_configured_cors"]( + self.app[KEY_ALLOW_CONFIGRED_CORS]( self.app.router.add_route("GET", url_path, serve_file) ) diff --git a/homeassistant/components/http/auth.py b/homeassistant/components/http/auth.py index 640d899924e..2073c998384 100644 --- a/homeassistant/components/http/auth.py +++ b/homeassistant/components/http/auth.py @@ -1,4 +1,5 @@ """Authentication for HTTP component.""" + from __future__ import annotations from collections.abc import Awaitable, Callable diff --git a/homeassistant/components/http/ban.py b/homeassistant/components/http/ban.py index 0b720b078b9..b4e949514b8 100644 --- a/homeassistant/components/http/ban.py +++ b/homeassistant/components/http/ban.py @@ -1,4 +1,5 @@ """Ban logic for HTTP component.""" + from __future__ import annotations from collections import defaultdict @@ -11,7 +12,14 @@ import logging from socket import gethostbyaddr, herror from typing import Any, Concatenate, Final, ParamSpec, TypeVar -from aiohttp.web import Application, Request, Response, StreamResponse, middleware +from aiohttp.web import ( + AppKey, + Application, + Request, + Response, + StreamResponse, + middleware, +) from aiohttp.web_exceptions import HTTPForbidden, HTTPUnauthorized import voluptuous as vol @@ -21,6 +29,7 @@ from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv from homeassistant.util import dt as dt_util, yaml +from .const import KEY_HASS from .view import HomeAssistantView _HassViewT = TypeVar("_HassViewT", bound=HomeAssistantView) @@ -28,9 +37,11 @@ _P = ParamSpec("_P") _LOGGER: Final = logging.getLogger(__name__) -KEY_BAN_MANAGER: Final = "ha_banned_ips_manager" -KEY_FAILED_LOGIN_ATTEMPTS: Final = "ha_failed_login_attempts" -KEY_LOGIN_THRESHOLD: Final = "ha_login_threshold" +KEY_BAN_MANAGER = AppKey["IpBanManager"]("ha_banned_ips_manager") +KEY_FAILED_LOGIN_ATTEMPTS = AppKey[defaultdict[IPv4Address | IPv6Address, int]]( + "ha_failed_login_attempts" +) +KEY_LOGIN_THRESHOLD = AppKey[int]("ban_manager.ip_bans_lookup") NOTIFICATION_ID_BAN: Final = "ip-ban" NOTIFICATION_ID_LOGIN: Final = "http-login" @@ -47,7 +58,7 @@ SCHEMA_IP_BAN_ENTRY: Final = vol.Schema( def setup_bans(hass: HomeAssistant, app: Application, login_threshold: int) -> None: """Create IP Ban middleware for the app.""" app.middlewares.append(ban_middleware) - app[KEY_FAILED_LOGIN_ATTEMPTS] = defaultdict(int) + app[KEY_FAILED_LOGIN_ATTEMPTS] = defaultdict[IPv4Address | IPv6Address, int](int) app[KEY_LOGIN_THRESHOLD] = login_threshold app[KEY_BAN_MANAGER] = IpBanManager(hass) @@ -63,17 +74,15 @@ async def ban_middleware( request: Request, handler: Callable[[Request], Awaitable[StreamResponse]] ) -> StreamResponse: """IP Ban middleware.""" - ban_manager: IpBanManager | None = request.app.get(KEY_BAN_MANAGER) - if ban_manager is None: + if (ban_manager := request.app.get(KEY_BAN_MANAGER)) is None: _LOGGER.error("IP Ban middleware loaded but banned IPs not loaded") return await handler(request) - ip_bans_lookup = ban_manager.ip_bans_lookup - if ip_bans_lookup: + if ip_bans_lookup := ban_manager.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() + raise HTTPForbidden try: return await handler(request) @@ -105,9 +114,10 @@ async def process_wrong_login(request: Request) -> None: Increase failed login attempts counter for remote IP address. Add ip ban entry if failed login attempts exceeds threshold. """ - hass = request.app["hass"] + hass = request.app[KEY_HASS] - remote_addr = ip_address(request.remote) # type: ignore[arg-type] + assert request.remote + remote_addr = ip_address(request.remote) remote_host = request.remote with suppress(herror): remote_host, _, _ = await hass.async_add_executor_job( @@ -153,7 +163,7 @@ async def process_wrong_login(request: Request) -> None: request.app[KEY_FAILED_LOGIN_ATTEMPTS][remote_addr] >= request.app[KEY_LOGIN_THRESHOLD] ): - ban_manager: IpBanManager = request.app[KEY_BAN_MANAGER] + ban_manager = 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) @@ -179,9 +189,7 @@ def process_success_login(request: Request) -> None: return remote_addr = ip_address(request.remote) # type: ignore[arg-type] - login_attempt_history: defaultdict[IPv4Address | IPv6Address, int] = app[ - KEY_FAILED_LOGIN_ATTEMPTS - ] + login_attempt_history = app[KEY_FAILED_LOGIN_ATTEMPTS] if remote_addr in login_attempt_history and login_attempt_history[remote_addr] > 0: _LOGGER.debug( "Login success, reset failed login attempts counter from %s", remote_addr diff --git a/homeassistant/components/http/const.py b/homeassistant/components/http/const.py index 090e5234aeb..1254744f258 100644 --- a/homeassistant/components/http/const.py +++ b/homeassistant/components/http/const.py @@ -1,8 +1,8 @@ """HTTP specific constants.""" + from typing import Final -from homeassistant.helpers.http import KEY_AUTHENTICATED # noqa: F401 +from homeassistant.helpers.http import KEY_AUTHENTICATED, KEY_HASS # noqa: F401 -KEY_HASS: Final = "hass" KEY_HASS_USER: Final = "hass_user" KEY_HASS_REFRESH_TOKEN_ID: Final = "hass_refresh_token_id" diff --git a/homeassistant/components/http/cors.py b/homeassistant/components/http/cors.py index 7eb1a5f84fe..ebae2480589 100644 --- a/homeassistant/components/http/cors.py +++ b/homeassistant/components/http/cors.py @@ -1,7 +1,8 @@ """Provide CORS support for the HTTP component.""" + from __future__ import annotations -from typing import Final +from typing import Final, cast from aiohttp.hdrs import ACCEPT, AUTHORIZATION, CONTENT_TYPE, ORIGIN from aiohttp.web import Application @@ -15,6 +16,11 @@ from aiohttp.web_urldispatcher import ( from homeassistant.const import HTTP_HEADER_X_REQUESTED_WITH from homeassistant.core import callback +from homeassistant.helpers.http import ( + KEY_ALLOW_ALL_CORS, + KEY_ALLOW_CONFIGRED_CORS, + AllowCorsType, +) ALLOWED_CORS_HEADERS: Final[list[str]] = [ ORIGIN, @@ -70,7 +76,7 @@ def setup_cors(app: Application, origins: list[str]) -> None: cors.add(route, config) cors_added.add(path_str) - app["allow_all_cors"] = lambda route: _allow_cors( + app[KEY_ALLOW_ALL_CORS] = lambda route: _allow_cors( route, { "*": aiohttp_cors.ResourceOptions( @@ -80,6 +86,6 @@ def setup_cors(app: Application, origins: list[str]) -> None: ) if origins: - app["allow_configured_cors"] = _allow_cors + app[KEY_ALLOW_CONFIGRED_CORS] = cast(AllowCorsType, _allow_cors) else: - app["allow_configured_cors"] = lambda _: None + app[KEY_ALLOW_CONFIGRED_CORS] = lambda _: None diff --git a/homeassistant/components/http/data_validator.py b/homeassistant/components/http/data_validator.py index 2868bee9432..749c4f63a2f 100644 --- a/homeassistant/components/http/data_validator.py +++ b/homeassistant/components/http/data_validator.py @@ -1,4 +1,5 @@ """Decorator for view methods to help with data validation.""" + from __future__ import annotations from collections.abc import Awaitable, Callable, Coroutine diff --git a/homeassistant/components/http/decorators.py b/homeassistant/components/http/decorators.py index b2e8e535fd2..d2e6121b08e 100644 --- a/homeassistant/components/http/decorators.py +++ b/homeassistant/components/http/decorators.py @@ -1,4 +1,5 @@ """Decorators for the Home Assistant API.""" + from __future__ import annotations from collections.abc import Callable, Coroutine @@ -28,15 +29,13 @@ def require_admin( ) -> Callable[ [_FuncType[_HomeAssistantViewT, _P, _ResponseT]], _FuncType[_HomeAssistantViewT, _P, _ResponseT], -]: - ... +]: ... @overload def require_admin( _func: _FuncType[_HomeAssistantViewT, _P, _ResponseT], -) -> _FuncType[_HomeAssistantViewT, _P, _ResponseT]: - ... +) -> _FuncType[_HomeAssistantViewT, _P, _ResponseT]: ... def require_admin( diff --git a/homeassistant/components/http/forwarded.py b/homeassistant/components/http/forwarded.py index 5d122436af2..134a31d1625 100644 --- a/homeassistant/components/http/forwarded.py +++ b/homeassistant/components/http/forwarded.py @@ -1,14 +1,14 @@ """Middleware to handle forwarded data by a reverse proxy.""" + from __future__ import annotations from collections.abc import Awaitable, Callable from ipaddress import IPv4Network, IPv6Network, ip_address import logging -from types import ModuleType -from typing import Literal from aiohttp.hdrs import X_FORWARDED_FOR, X_FORWARDED_HOST, X_FORWARDED_PROTO from aiohttp.web import Application, HTTPBadRequest, Request, StreamResponse, middleware +from hass_nabucasa import remote from homeassistant.core import callback @@ -67,30 +67,13 @@ def async_setup_forwarded( an HTTP 400 status code is thrown. """ - remote: Literal[False] | None | ModuleType = None - @middleware async def forwarded_middleware( request: Request, handler: Callable[[Request], Awaitable[StreamResponse]] ) -> StreamResponse: """Process forwarded data by a reverse proxy.""" - nonlocal remote - - if remote is None: - # Initialize remote method - try: - from hass_nabucasa import ( # pylint: disable=import-outside-toplevel - remote, - ) - - # venv users might have an old version installed if they don't have cloud around anymore - if not hasattr(remote, "is_cloud_request"): - remote = False - except ImportError: - remote = False - # Skip requests from Remote UI - if remote and remote.is_cloud_request.get(): + if remote.is_cloud_request.get(): return await handler(request) # Handle X-Forwarded-For diff --git a/homeassistant/components/http/headers.py b/homeassistant/components/http/headers.py index 20c0a58967b..bd05401ebce 100644 --- a/homeassistant/components/http/headers.py +++ b/homeassistant/components/http/headers.py @@ -1,4 +1,5 @@ """Middleware that helps with the control of headers in our responses.""" + from __future__ import annotations from collections.abc import Awaitable, Callable diff --git a/homeassistant/components/http/request_context.py b/homeassistant/components/http/request_context.py index b516b63dc5c..c5fcdfb18f3 100644 --- a/homeassistant/components/http/request_context.py +++ b/homeassistant/components/http/request_context.py @@ -1,4 +1,5 @@ """Middleware to set the request context.""" + from __future__ import annotations from collections.abc import Awaitable, Callable diff --git a/homeassistant/components/http/security_filter.py b/homeassistant/components/http/security_filter.py index 4d71334f1cf..524d125b857 100644 --- a/homeassistant/components/http/security_filter.py +++ b/homeassistant/components/http/security_filter.py @@ -1,4 +1,5 @@ """Middleware to add some basic security filtering to requests.""" + from __future__ import annotations from collections.abc import Awaitable, Callable diff --git a/homeassistant/components/http/static.py b/homeassistant/components/http/static.py index e6e773d4c0c..fd6cd742ce4 100644 --- a/homeassistant/components/http/static.py +++ b/homeassistant/components/http/static.py @@ -1,4 +1,5 @@ """Static file handling for HTTP component.""" + from __future__ import annotations from collections.abc import Mapping @@ -12,8 +13,6 @@ from aiohttp.web_exceptions import HTTPForbidden, HTTPNotFound from aiohttp.web_urldispatcher import StaticResource from lru import LRU -from homeassistant.core import HomeAssistant - from .const import KEY_HASS CACHE_TIME: Final = 31 * 86400 # = 1 month @@ -48,19 +47,19 @@ class CachingStaticResource(StaticResource): rel_url = request.match_info["filename"] key = (rel_url, self._directory) if (filepath_content_type := PATH_CACHE.get(key)) is None: - hass: HomeAssistant = request.app[KEY_HASS] + hass = request.app[KEY_HASS] try: filepath = await hass.async_add_executor_job(_get_file_path, *key) except (ValueError, FileNotFoundError) as error: # relatively safe - raise HTTPNotFound() from error + raise HTTPNotFound from error except HTTPForbidden: # forbidden raise except Exception as error: # perm error or other kind! request.app.logger.exception(error) - raise HTTPNotFound() from error + raise HTTPNotFound from error content_type: str | None = None if filepath is not None: diff --git a/homeassistant/components/http/view.py b/homeassistant/components/http/view.py index ce02879dbb3..712b4e9894f 100644 --- a/homeassistant/components/http/view.py +++ b/homeassistant/components/http/view.py @@ -1,4 +1,5 @@ """Support for views.""" + from __future__ import annotations from homeassistant.helpers.http import ( # noqa: F401 diff --git a/homeassistant/components/http/web_runner.py b/homeassistant/components/http/web_runner.py index 5c0931b97ca..fcdfbc661a7 100644 --- a/homeassistant/components/http/web_runner.py +++ b/homeassistant/components/http/web_runner.py @@ -1,4 +1,5 @@ """HomeAssistant specific aiohttp Site.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index 81be4e462d1..7d28d6c187f 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -1,4 +1,5 @@ """Support for Huawei LTE routers.""" + from __future__ import annotations from collections import defaultdict diff --git a/homeassistant/components/huawei_lte/binary_sensor.py b/homeassistant/components/huawei_lte/binary_sensor.py index d4fa0b6db6f..c90a7854a91 100644 --- a/homeassistant/components/huawei_lte/binary_sensor.py +++ b/homeassistant/components/huawei_lte/binary_sensor.py @@ -1,4 +1,5 @@ """Support for Huawei LTE binary sensors.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/huawei_lte/config_flow.py b/homeassistant/components/huawei_lte/config_flow.py index c97c8d6367b..84cf88786a9 100644 --- a/homeassistant/components/huawei_lte/config_flow.py +++ b/homeassistant/components/huawei_lte/config_flow.py @@ -1,4 +1,5 @@ """Config flow for the Huawei LTE platform.""" + from __future__ import annotations from collections.abc import Mapping @@ -20,8 +21,13 @@ from requests.exceptions import SSLError, Timeout from url_normalize import url_normalize import voluptuous as vol -from homeassistant import config_entries from homeassistant.components import ssdp +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.const import ( CONF_MAC, CONF_NAME, @@ -32,7 +38,6 @@ from homeassistant.const import ( CONF_VERIFY_SSL, ) from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult from .const import ( CONF_MANUFACTURER, @@ -50,7 +55,7 @@ from .utils import get_device_macs, non_verifying_requests_session _LOGGER = logging.getLogger(__name__) -class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class ConfigFlowHandler(ConfigFlow, domain=DOMAIN): """Handle Huawei LTE config flow.""" VERSION = 3 @@ -58,7 +63,7 @@ class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback def async_get_options_flow( - config_entry: config_entries.ConfigEntry, + config_entry: ConfigEntry, ) -> OptionsFlowHandler: """Get options flow.""" return OptionsFlowHandler(config_entry) @@ -67,7 +72,7 @@ class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None, errors: dict[str, str] | None = None, - ) -> FlowResult: + ) -> ConfigFlowResult: if user_input is None: user_input = {} return self.async_show_form( @@ -103,7 +108,7 @@ class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any], errors: dict[str, str] | None = None, - ) -> FlowResult: + ) -> ConfigFlowResult: return self.async_show_form( step_id="reauth_confirm", data_schema=vol.Schema( @@ -181,7 +186,7 @@ class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle user initiated config flow.""" if user_input is None: return await self._async_show_user_form() @@ -256,7 +261,9 @@ class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_create_entry(title=title, data=user_input) - async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: + async def async_step_ssdp( + self, discovery_info: ssdp.SsdpServiceInfo + ) -> ConfigFlowResult: """Handle SSDP initiated config flow.""" if TYPE_CHECKING: @@ -302,13 +309,15 @@ class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) return await self._async_show_user_form() - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Perform reauth upon an API authentication error.""" return await self.async_step_reauth_confirm() async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Dialog that informs the user that reauth is required.""" entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) assert entry @@ -335,16 +344,16 @@ class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_abort(reason="reauth_successful") -class OptionsFlowHandler(config_entries.OptionsFlow): +class OptionsFlowHandler(OptionsFlow): """Huawei LTE options flow.""" - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + 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: + ) -> ConfigFlowResult: """Handle options flow.""" # Recipients are persisted as a list, but handled as comma separated string in UI diff --git a/homeassistant/components/huawei_lte/device_tracker.py b/homeassistant/components/huawei_lte/device_tracker.py index 1bb5077a2b4..1f9905f4e9c 100644 --- a/homeassistant/components/huawei_lte/device_tracker.py +++ b/homeassistant/components/huawei_lte/device_tracker.py @@ -1,4 +1,5 @@ """Support for device tracking of Huawei LTE routers.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/huawei_lte/notify.py b/homeassistant/components/huawei_lte/notify.py index 3b72e2216a6..fc154de3811 100644 --- a/homeassistant/components/huawei_lte/notify.py +++ b/homeassistant/components/huawei_lte/notify.py @@ -1,4 +1,5 @@ """Support for Huawei LTE router notifications.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/huawei_lte/select.py b/homeassistant/components/huawei_lte/select.py index 6fef2d745cb..bf8f65a8ba5 100644 --- a/homeassistant/components/huawei_lte/select.py +++ b/homeassistant/components/huawei_lte/select.py @@ -1,4 +1,5 @@ """Support for Huawei LTE selects.""" + from __future__ import annotations from collections.abc import Callable @@ -26,18 +27,13 @@ from .const import DOMAIN, KEY_NET_NET_MODE _LOGGER = logging.getLogger(__name__) -@dataclass(frozen=True) -class HuaweiSelectEntityMixin: - """Mixin for Huawei LTE select entities, to ensure required fields are set.""" +@dataclass(frozen=True, kw_only=True) +class HuaweiSelectEntityDescription(SelectEntityDescription): + """Class describing Huawei LTE select entities.""" setter_fn: Callable[[str], None] -@dataclass(frozen=True) -class HuaweiSelectEntityDescription(SelectEntityDescription, HuaweiSelectEntityMixin): - """Class describing Huawei LTE select entities.""" - - async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, diff --git a/homeassistant/components/huawei_lte/sensor.py b/homeassistant/components/huawei_lte/sensor.py index d7fb5565969..e75fef42ef3 100644 --- a/homeassistant/components/huawei_lte/sensor.py +++ b/homeassistant/components/huawei_lte/sensor.py @@ -1,4 +1,5 @@ """Support for Huawei LTE sensors.""" + from __future__ import annotations from bisect import bisect @@ -673,17 +674,17 @@ async def async_setup_entry( items = filter(key_meta.include.search, items) if key_meta.exclude: items = [x for x in items if not key_meta.exclude.search(x)] - for item in items: - sensors.append( - HuaweiLteSensor( - router, - key, - item, - SENSOR_META[key].descriptions.get( - item, HuaweiSensorEntityDescription(key=item) - ), - ) + sensors.extend( + HuaweiLteSensor( + router, + key, + item, + SENSOR_META[key].descriptions.get( + item, HuaweiSensorEntityDescription(key=item) + ), ) + for item in items + ) async_add_entities(sensors, True) diff --git a/homeassistant/components/huawei_lte/switch.py b/homeassistant/components/huawei_lte/switch.py index 3743716390e..3a499851f9a 100644 --- a/homeassistant/components/huawei_lte/switch.py +++ b/homeassistant/components/huawei_lte/switch.py @@ -1,4 +1,5 @@ """Support for Huawei LTE switches.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/huawei_lte/utils.py b/homeassistant/components/huawei_lte/utils.py index df212a1c25d..2225fb13ffc 100644 --- a/homeassistant/components/huawei_lte/utils.py +++ b/homeassistant/components/huawei_lte/utils.py @@ -1,4 +1,5 @@ """Utilities for the Huawei LTE integration.""" + from __future__ import annotations from contextlib import suppress diff --git a/homeassistant/components/hue/__init__.py b/homeassistant/components/hue/__init__.py index 21e9bfab6be..d4c2959771b 100644 --- a/homeassistant/components/hue/__init__.py +++ b/homeassistant/components/hue/__init__.py @@ -1,4 +1,5 @@ """Support for the Philips Hue system.""" + from aiohue.util import normalize_bridge_id from homeassistant.components import persistent_notification diff --git a/homeassistant/components/hue/binary_sensor.py b/homeassistant/components/hue/binary_sensor.py index b66b85a4844..2cb8e8b5d90 100644 --- a/homeassistant/components/hue/binary_sensor.py +++ b/homeassistant/components/hue/binary_sensor.py @@ -1,4 +1,5 @@ """Support for Hue binary sensors.""" + from __future__ import annotations from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/hue/bridge.py b/homeassistant/components/hue/bridge.py index abf91cf4577..f167897d77b 100644 --- a/homeassistant/components/hue/bridge.py +++ b/homeassistant/components/hue/bridge.py @@ -1,4 +1,5 @@ """Code to handle a Hue bridge.""" + from __future__ import annotations import asyncio @@ -71,10 +72,11 @@ class HueBridge: async def async_initialize_bridge(self) -> bool: """Initialize Connection with the Hue API.""" + setup_ok = False try: async with asyncio.timeout(10): await self.api.initialize() - + setup_ok = True except (LinkButtonNotPressed, Unauthorized): # Usernames can become invalid if hub is reset or user removed. # We are going to fail the config entry setup and initiate a new @@ -95,6 +97,9 @@ class HueBridge: except Exception: # pylint: disable=broad-except self.logger.exception("Unknown error connecting to Hue bridge") return False + finally: + if not setup_ok: + await self.api.close() # v1 specific initialization/setup code here if self.api_version == 1: diff --git a/homeassistant/components/hue/config_flow.py b/homeassistant/components/hue/config_flow.py index a1345cf3bba..de2d9363ac7 100644 --- a/homeassistant/components/hue/config_flow.py +++ b/homeassistant/components/hue/config_flow.py @@ -1,4 +1,5 @@ """Config flow to configure Philips Hue.""" + from __future__ import annotations import asyncio @@ -12,11 +13,15 @@ from aiohue.util import normalize_bridge_id import slugify as unicode_slug import voluptuous as vol -from homeassistant import config_entries from homeassistant.components import zeroconf +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.const import CONF_API_KEY, CONF_API_VERSION, CONF_HOST from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import ( aiohttp_client, config_validation as cv, @@ -40,7 +45,7 @@ HUE_IGNORED_BRIDGE_NAMES = ["Home Assistant Bridge", "Espalexa"] HUE_MANUAL_BRIDGE_ID = "manual" -class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class HueFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a Hue config flow.""" VERSION = 1 @@ -48,7 +53,7 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback def async_get_options_flow( - config_entry: config_entries.ConfigEntry, + config_entry: ConfigEntry, ) -> HueV1OptionsFlowHandler | HueV2OptionsFlowHandler: """Get the options flow for this handler.""" if config_entry.data.get(CONF_API_VERSION, 1) == 1: @@ -62,7 +67,7 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" # This is for backwards compatibility. return await self.async_step_init(user_input) @@ -90,7 +95,7 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow start.""" # Check if user chooses manual entry if user_input is not None and user_input["id"] == HUE_MANUAL_BRIDGE_ID: @@ -112,7 +117,7 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): websession=aiohttp_client.async_get_clientsession(self.hass) ) except TimeoutError: - return self.async_abort(reason="discover_timeout") + bridges = [] if bridges: # Find already configured hosts @@ -141,7 +146,7 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_manual( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle manual bridge setup.""" if user_input is None: return self.async_show_form( @@ -157,7 +162,7 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_link( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Attempt to link with the Hue bridge. Given a configured host, will ask the user to press the link button @@ -210,7 +215,7 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_zeroconf( self, discovery_info: zeroconf.ZeroconfServiceInfo - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a discovered Hue bridge. This flow is triggered by the Zeroconf component. It will check if the @@ -239,7 +244,7 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_homekit( self, discovery_info: zeroconf.ZeroconfServiceInfo - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a discovered Hue bridge on HomeKit. The bridge ID communicated over HomeKit differs, so we cannot use that @@ -253,7 +258,7 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): await self._async_handle_discovery_without_unique_id() return await self.async_step_link() - async def async_step_import(self, import_info: dict[str, Any]) -> FlowResult: + async def async_step_import(self, import_info: dict[str, Any]) -> ConfigFlowResult: """Import a new bridge as a config entry. This flow is triggered by `async_setup` for both configured and @@ -272,16 +277,16 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_link() -class HueV1OptionsFlowHandler(config_entries.OptionsFlow): +class HueV1OptionsFlowHandler(OptionsFlow): """Handle Hue options for V1 implementation.""" - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + def __init__(self, config_entry: ConfigEntry) -> None: """Initialize Hue options flow.""" self.config_entry = config_entry async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Manage Hue options.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) @@ -307,16 +312,16 @@ class HueV1OptionsFlowHandler(config_entries.OptionsFlow): ) -class HueV2OptionsFlowHandler(config_entries.OptionsFlow): +class HueV2OptionsFlowHandler(OptionsFlow): """Handle Hue options for V2 implementation.""" - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + def __init__(self, config_entry: ConfigEntry) -> None: """Initialize Hue options flow.""" self.config_entry = config_entry async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Manage Hue options.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) diff --git a/homeassistant/components/hue/const.py b/homeassistant/components/hue/const.py index 5033aaa427a..62700478b53 100644 --- a/homeassistant/components/hue/const.py +++ b/homeassistant/components/hue/const.py @@ -1,4 +1,5 @@ """Constants for the Hue component.""" + from aiohue.v2.models.button import ButtonEvent from aiohue.v2.models.relative_rotary import ( RelativeRotaryAction, diff --git a/homeassistant/components/hue/device_trigger.py b/homeassistant/components/hue/device_trigger.py index 069f0d42d8d..4104c667d74 100644 --- a/homeassistant/components/hue/device_trigger.py +++ b/homeassistant/components/hue/device_trigger.py @@ -1,4 +1,5 @@ """Provides device automations for Philips Hue events.""" + from __future__ import annotations from typing import TYPE_CHECKING, Any diff --git a/homeassistant/components/hue/diagnostics.py b/homeassistant/components/hue/diagnostics.py index 17f00a50bbe..6bb23d832cd 100644 --- a/homeassistant/components/hue/diagnostics.py +++ b/homeassistant/components/hue/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for Hue.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/hue/errors.py b/homeassistant/components/hue/errors.py index dd217c3bc26..670d1282c96 100644 --- a/homeassistant/components/hue/errors.py +++ b/homeassistant/components/hue/errors.py @@ -1,4 +1,5 @@ """Errors for the Hue component.""" + from homeassistant.exceptions import HomeAssistantError diff --git a/homeassistant/components/hue/event.py b/homeassistant/components/hue/event.py index 183d2bfb3ae..1ba974fa167 100644 --- a/homeassistant/components/hue/event.py +++ b/homeassistant/components/hue/event.py @@ -1,4 +1,5 @@ """Hue event entities from Button resources.""" + from __future__ import annotations from typing import Any @@ -80,12 +81,12 @@ class HueButtonEventEntity(HueBaseEntity, EventEntity): # fill the event types based on the features the switch supports hue_dev_id = self.controller.get_device(self.resource.id).id model_id = self.bridge.api.devices[hue_dev_id].product_data.product_name - event_types: list[str] = [] - for event_type in DEVICE_SPECIFIC_EVENT_TYPES.get( - model_id, DEFAULT_BUTTON_EVENT_TYPES - ): - event_types.append(event_type.value) - self._attr_event_types = event_types + self._attr_event_types: list[str] = [ + event_type.value + for event_type in DEVICE_SPECIFIC_EVENT_TYPES.get( + model_id, DEFAULT_BUTTON_EVENT_TYPES + ) + ] self._attr_translation_placeholders = { "button_id": self.resource.metadata.control_id } diff --git a/homeassistant/components/hue/icons.json b/homeassistant/components/hue/icons.json new file mode 100644 index 00000000000..9371ae5843e --- /dev/null +++ b/homeassistant/components/hue/icons.json @@ -0,0 +1,6 @@ +{ + "services": { + "hue_activate_scene": "mdi:palette", + "activate_scene": "mdi:palette" + } +} diff --git a/homeassistant/components/hue/light.py b/homeassistant/components/hue/light.py index 2bd9652f9b0..c3168b5c8c1 100644 --- a/homeassistant/components/hue/light.py +++ b/homeassistant/components/hue/light.py @@ -1,4 +1,5 @@ """Support for Hue lights.""" + from __future__ import annotations from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/hue/scene.py b/homeassistant/components/hue/scene.py index 17f7a81b2a5..67148eb8be8 100644 --- a/homeassistant/components/hue/scene.py +++ b/homeassistant/components/hue/scene.py @@ -1,4 +1,5 @@ """Support for scene platform for Hue scenes (V2 only).""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/hue/sensor.py b/homeassistant/components/hue/sensor.py index 7218831abe2..45cff053aef 100644 --- a/homeassistant/components/hue/sensor.py +++ b/homeassistant/components/hue/sensor.py @@ -1,4 +1,5 @@ """Support for Hue sensors.""" + from __future__ import annotations from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/hue/services.py b/homeassistant/components/hue/services.py index 5b0b252e8d4..30555339f19 100644 --- a/homeassistant/components/hue/services.py +++ b/homeassistant/components/hue/services.py @@ -1,4 +1,5 @@ """Handle Hue Service calls.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/hue/switch.py b/homeassistant/components/hue/switch.py index c0689a85f21..b4bc57acf2d 100644 --- a/homeassistant/components/hue/switch.py +++ b/homeassistant/components/hue/switch.py @@ -1,4 +1,5 @@ """Support for switch platform for Hue resources (V2 only).""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/hue/v1/binary_sensor.py b/homeassistant/components/hue/v1/binary_sensor.py index 78cedc4437f..01524b48b79 100644 --- a/homeassistant/components/hue/v1/binary_sensor.py +++ b/homeassistant/components/hue/v1/binary_sensor.py @@ -1,4 +1,5 @@ """Hue binary sensor entities.""" + from aiohue.v1.sensors import TYPE_ZLL_PRESENCE from homeassistant.components.binary_sensor import ( diff --git a/homeassistant/components/hue/v1/device_trigger.py b/homeassistant/components/hue/v1/device_trigger.py index 930c85c0414..554926cdc70 100644 --- a/homeassistant/components/hue/v1/device_trigger.py +++ b/homeassistant/components/hue/v1/device_trigger.py @@ -1,4 +1,5 @@ """Provides device automations for Philips Hue events in V1 bridge/api.""" + from __future__ import annotations from typing import TYPE_CHECKING diff --git a/homeassistant/components/hue/v1/hue_event.py b/homeassistant/components/hue/v1/hue_event.py index b3faf88c2d9..a7fe447f3f4 100644 --- a/homeassistant/components/hue/v1/hue_event.py +++ b/homeassistant/components/hue/v1/hue_event.py @@ -1,4 +1,5 @@ """Representation of a Hue remote firing events for button presses.""" + import logging from aiohue.v1.sensors import ( diff --git a/homeassistant/components/hue/v1/light.py b/homeassistant/components/hue/v1/light.py index 18440f68239..68e05932e7a 100644 --- a/homeassistant/components/hue/v1/light.py +++ b/homeassistant/components/hue/v1/light.py @@ -1,4 +1,5 @@ """Support for the Philips Hue lights.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/hue/v1/sensor.py b/homeassistant/components/hue/v1/sensor.py index 20ea4330e30..9a85f83f3e8 100644 --- a/homeassistant/components/hue/v1/sensor.py +++ b/homeassistant/components/hue/v1/sensor.py @@ -1,4 +1,5 @@ """Hue sensor entities.""" + from aiohue.v1.sensors import ( TYPE_ZLL_LIGHTLEVEL, TYPE_ZLL_ROTARY, diff --git a/homeassistant/components/hue/v1/sensor_base.py b/homeassistant/components/hue/v1/sensor_base.py index 723ecfff451..bac02c45209 100644 --- a/homeassistant/components/hue/v1/sensor_base.py +++ b/homeassistant/components/hue/v1/sensor_base.py @@ -1,4 +1,5 @@ """Support for the Philips Hue sensors as a platform.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/hue/v1/sensor_device.py b/homeassistant/components/hue/v1/sensor_device.py index 9ffc1518cba..1ff97af2e62 100644 --- a/homeassistant/components/hue/v1/sensor_device.py +++ b/homeassistant/components/hue/v1/sensor_device.py @@ -1,4 +1,5 @@ """Support for the Philips Hue sensor devices.""" + from homeassistant.helpers import entity from homeassistant.helpers.device_registry import DeviceInfo diff --git a/homeassistant/components/hue/v2/binary_sensor.py b/homeassistant/components/hue/v2/binary_sensor.py index 4707302d288..bc650569a63 100644 --- a/homeassistant/components/hue/v2/binary_sensor.py +++ b/homeassistant/components/hue/v2/binary_sensor.py @@ -1,4 +1,5 @@ """Support for Hue binary sensors.""" + from __future__ import annotations from functools import partial diff --git a/homeassistant/components/hue/v2/device.py b/homeassistant/components/hue/v2/device.py index 75f474cc0ea..38c5724d4a8 100644 --- a/homeassistant/components/hue/v2/device.py +++ b/homeassistant/components/hue/v2/device.py @@ -1,4 +1,5 @@ """Handles Hue resource of type `device` mapping to Home Assistant device.""" + from typing import TYPE_CHECKING from aiohue.v2 import HueBridgeV2 diff --git a/homeassistant/components/hue/v2/device_trigger.py b/homeassistant/components/hue/v2/device_trigger.py index a3027736661..c35093a9f9c 100644 --- a/homeassistant/components/hue/v2/device_trigger.py +++ b/homeassistant/components/hue/v2/device_trigger.py @@ -1,4 +1,5 @@ """Provides device automations for Philips Hue events.""" + from __future__ import annotations from typing import TYPE_CHECKING, Any @@ -89,39 +90,39 @@ def async_get_triggers( # Get Hue device id from device identifier hue_dev_id = get_hue_device_id(device_entry) # extract triggers from all button resources of this Hue device - triggers = [] + triggers: list[dict[str, Any]] = [] model_id = api.devices[hue_dev_id].product_data.product_name for resource in api.devices.get_sensors(hue_dev_id): # button triggers if resource.type == ResourceTypes.BUTTON: - for event_type in DEVICE_SPECIFIC_EVENT_TYPES.get( - model_id, DEFAULT_BUTTON_EVENT_TYPES - ): - triggers.append( - { - CONF_DEVICE_ID: device_entry.id, - CONF_DOMAIN: DOMAIN, - CONF_PLATFORM: "device", - CONF_TYPE: event_type.value, - CONF_SUBTYPE: resource.metadata.control_id, - CONF_UNIQUE_ID: resource.id, - } + triggers.extend( + { + CONF_DEVICE_ID: device_entry.id, + CONF_DOMAIN: DOMAIN, + CONF_PLATFORM: "device", + CONF_TYPE: event_type.value, + CONF_SUBTYPE: resource.metadata.control_id, + CONF_UNIQUE_ID: resource.id, + } + for event_type in DEVICE_SPECIFIC_EVENT_TYPES.get( + model_id, DEFAULT_BUTTON_EVENT_TYPES ) + ) # relative_rotary triggers elif resource.type == ResourceTypes.RELATIVE_ROTARY: - for event_type in DEFAULT_ROTARY_EVENT_TYPES: - for sub_type in DEFAULT_ROTARY_EVENT_SUBTYPES: - triggers.append( - { - CONF_DEVICE_ID: device_entry.id, - CONF_DOMAIN: DOMAIN, - CONF_PLATFORM: "device", - CONF_TYPE: event_type.value, - CONF_SUBTYPE: sub_type.value, - CONF_UNIQUE_ID: resource.id, - } - ) + triggers.extend( + { + CONF_DEVICE_ID: device_entry.id, + CONF_DOMAIN: DOMAIN, + CONF_PLATFORM: "device", + CONF_TYPE: event_type.value, + CONF_SUBTYPE: sub_type.value, + CONF_UNIQUE_ID: resource.id, + } + for event_type in DEFAULT_ROTARY_EVENT_TYPES + for sub_type in DEFAULT_ROTARY_EVENT_SUBTYPES + ) return triggers diff --git a/homeassistant/components/hue/v2/entity.py b/homeassistant/components/hue/v2/entity.py index 75e4bb1edd4..8aeac4d8180 100644 --- a/homeassistant/components/hue/v2/entity.py +++ b/homeassistant/components/hue/v2/entity.py @@ -1,4 +1,5 @@ """Generic Hue Entity Model.""" + from __future__ import annotations from typing import TYPE_CHECKING, TypeAlias diff --git a/homeassistant/components/hue/v2/group.py b/homeassistant/components/hue/v2/group.py index 8f9def48a27..db30800a333 100644 --- a/homeassistant/components/hue/v2/group.py +++ b/homeassistant/components/hue/v2/group.py @@ -1,4 +1,5 @@ """Support for Hue groups (room/zone).""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/hue/v2/helpers.py b/homeassistant/components/hue/v2/helpers.py index 97fdbe6160a..480296760e7 100644 --- a/homeassistant/components/hue/v2/helpers.py +++ b/homeassistant/components/hue/v2/helpers.py @@ -1,4 +1,5 @@ """Helper functions for Philips Hue v2.""" + from __future__ import annotations diff --git a/homeassistant/components/hue/v2/hue_event.py b/homeassistant/components/hue/v2/hue_event.py index b8521a80af7..6aee6c67bf3 100644 --- a/homeassistant/components/hue/v2/hue_event.py +++ b/homeassistant/components/hue/v2/hue_event.py @@ -1,4 +1,5 @@ """Handle forward of events transmitted by Hue devices to HASS.""" + import logging from typing import TYPE_CHECKING diff --git a/homeassistant/components/hue/v2/light.py b/homeassistant/components/hue/v2/light.py index bbf5dc9c19f..b908ec83877 100644 --- a/homeassistant/components/hue/v2/light.py +++ b/homeassistant/components/hue/v2/light.py @@ -1,4 +1,5 @@ """Support for Hue lights.""" + from __future__ import annotations from functools import partial @@ -234,6 +235,9 @@ class HueLight(HueBaseEntity, LightEntity): if transition is None: # a transition is required for timed effect, default to 10 minutes transition = 600000 + # we need to clear color values if an effect is applied + color_temp = None + xy_color = None if flash is not None: await self.async_set_flash(flash) diff --git a/homeassistant/components/hue/v2/sensor.py b/homeassistant/components/hue/v2/sensor.py index 59dc8de2975..e46ca561964 100644 --- a/homeassistant/components/hue/v2/sensor.py +++ b/homeassistant/components/hue/v2/sensor.py @@ -1,4 +1,5 @@ """Support for Hue sensors.""" + from __future__ import annotations from functools import partial diff --git a/homeassistant/components/huisbaasje/__init__.py b/homeassistant/components/huisbaasje/__init__.py index 9ea4b547596..b02d0bf577c 100644 --- a/homeassistant/components/huisbaasje/__init__.py +++ b/homeassistant/components/huisbaasje/__init__.py @@ -1,4 +1,5 @@ """The Huisbaasje integration.""" + import asyncio from datetime import timedelta import logging diff --git a/homeassistant/components/huisbaasje/config_flow.py b/homeassistant/components/huisbaasje/config_flow.py index fc3a1c06a15..3697c1fcb86 100644 --- a/homeassistant/components/huisbaasje/config_flow.py +++ b/homeassistant/components/huisbaasje/config_flow.py @@ -1,10 +1,11 @@ """Config flow for Huisbaasje integration.""" + import logging from energyflip import EnergyFlip, EnergyFlipConnectionException, EnergyFlipException import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_ID, CONF_PASSWORD, CONF_USERNAME from homeassistant.data_entry_flow import AbortFlow @@ -17,7 +18,7 @@ DATA_SCHEMA = vol.Schema( ) -class HuisbaasjeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class HuisbaasjeConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Huisbaasje.""" VERSION = 1 diff --git a/homeassistant/components/huisbaasje/const.py b/homeassistant/components/huisbaasje/const.py index f9084831263..108e3fffa1e 100644 --- a/homeassistant/components/huisbaasje/const.py +++ b/homeassistant/components/huisbaasje/const.py @@ -1,4 +1,5 @@ """Constants for the Huisbaasje integration.""" + from energyflip.const import ( SOURCE_TYPE_ELECTRICITY, SOURCE_TYPE_ELECTRICITY_IN, diff --git a/homeassistant/components/huisbaasje/sensor.py b/homeassistant/components/huisbaasje/sensor.py index f07711268d5..d09b559516b 100644 --- a/homeassistant/components/huisbaasje/sensor.py +++ b/homeassistant/components/huisbaasje/sensor.py @@ -1,4 +1,5 @@ """Platform for sensor integration.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/homeassistant/components/humidifier/__init__.py b/homeassistant/components/humidifier/__init__.py index 37f9d49f0dd..1af294d8640 100644 --- a/homeassistant/components/humidifier/__init__.py +++ b/homeassistant/components/humidifier/__init__.py @@ -1,4 +1,5 @@ """Provides functionality to interact with humidifier devices.""" + from __future__ import annotations from datetime import timedelta @@ -164,18 +165,18 @@ class HumidifierEntity(ToggleEntity, cached_properties=CACHED_PROPERTIES_WITH_AT entity_description: HumidifierEntityDescription _attr_action: HumidifierAction | None = None _attr_available_modes: list[str] | None - _attr_current_humidity: int | None = None + _attr_current_humidity: float | None = None _attr_device_class: HumidifierDeviceClass | None - _attr_max_humidity: int = DEFAULT_MAX_HUMIDITY - _attr_min_humidity: int = DEFAULT_MIN_HUMIDITY + _attr_max_humidity: float = DEFAULT_MAX_HUMIDITY + _attr_min_humidity: float = DEFAULT_MIN_HUMIDITY _attr_mode: str | None _attr_supported_features: HumidifierEntityFeature = HumidifierEntityFeature(0) - _attr_target_humidity: int | None = None + _attr_target_humidity: float | None = None @property def capability_attributes(self) -> dict[str, Any]: """Return capability attributes.""" - data: dict[str, int | list[str] | None] = { + data: dict[str, Any] = { ATTR_MIN_HUMIDITY: self.min_humidity, ATTR_MAX_HUMIDITY: self.max_humidity, } @@ -198,7 +199,7 @@ class HumidifierEntity(ToggleEntity, cached_properties=CACHED_PROPERTIES_WITH_AT @property def state_attributes(self) -> dict[str, Any]: """Return the optional state attributes.""" - data: dict[str, int | str | None] = {} + data: dict[str, Any] = {} if self.action is not None: data[ATTR_ACTION] = self.action if self.is_on else HumidifierAction.OFF @@ -220,12 +221,12 @@ class HumidifierEntity(ToggleEntity, cached_properties=CACHED_PROPERTIES_WITH_AT return self._attr_action @cached_property - def current_humidity(self) -> int | None: + def current_humidity(self) -> float | None: """Return the current humidity.""" return self._attr_current_humidity @cached_property - def target_humidity(self) -> int | None: + def target_humidity(self) -> float | None: """Return the humidity we try to reach.""" return self._attr_target_humidity @@ -247,7 +248,7 @@ class HumidifierEntity(ToggleEntity, cached_properties=CACHED_PROPERTIES_WITH_AT def set_humidity(self, humidity: int) -> None: """Set new target humidity.""" - raise NotImplementedError() + raise NotImplementedError async def async_set_humidity(self, humidity: int) -> None: """Set new target humidity.""" @@ -255,19 +256,19 @@ class HumidifierEntity(ToggleEntity, cached_properties=CACHED_PROPERTIES_WITH_AT def set_mode(self, mode: str) -> None: """Set new mode.""" - raise NotImplementedError() + raise NotImplementedError async def async_set_mode(self, mode: str) -> None: """Set new mode.""" await self.hass.async_add_executor_job(self.set_mode, mode) @cached_property - def min_humidity(self) -> int: + def min_humidity(self) -> float: """Return the minimum humidity.""" return self._attr_min_humidity @cached_property - def max_humidity(self) -> int: + def max_humidity(self) -> float: """Return the maximum humidity.""" return self._attr_max_humidity diff --git a/homeassistant/components/humidifier/const.py b/homeassistant/components/humidifier/const.py index 66ac0fcf18d..fc6b0fc14d4 100644 --- a/homeassistant/components/humidifier/const.py +++ b/homeassistant/components/humidifier/const.py @@ -1,4 +1,5 @@ """Provides the constants needed for component.""" + from enum import IntFlag, StrEnum from functools import partial diff --git a/homeassistant/components/humidifier/device_action.py b/homeassistant/components/humidifier/device_action.py index f1f25101e93..74ef73443d6 100644 --- a/homeassistant/components/humidifier/device_action.py +++ b/homeassistant/components/humidifier/device_action.py @@ -1,4 +1,5 @@ """Provides device actions for Humidifier.""" + from __future__ import annotations import voluptuous as vol diff --git a/homeassistant/components/humidifier/device_condition.py b/homeassistant/components/humidifier/device_condition.py index c2c0378a746..7ea9899bba7 100644 --- a/homeassistant/components/humidifier/device_condition.py +++ b/homeassistant/components/humidifier/device_condition.py @@ -1,4 +1,5 @@ """Provide the device automations for Humidifier.""" + from __future__ import annotations import voluptuous as vol diff --git a/homeassistant/components/humidifier/device_trigger.py b/homeassistant/components/humidifier/device_trigger.py index 0d689a35318..80e0ef8df58 100644 --- a/homeassistant/components/humidifier/device_trigger.py +++ b/homeassistant/components/humidifier/device_trigger.py @@ -1,4 +1,5 @@ """Provides device automations for Climate.""" + from __future__ import annotations import voluptuous as vol @@ -125,13 +126,13 @@ async def async_attach_trigger( ), } if trigger_type == "target_humidity_changed": - numeric_state_config[ - numeric_state_trigger.CONF_VALUE_TEMPLATE - ] = "{{ state.attributes.humidity }}" + numeric_state_config[numeric_state_trigger.CONF_VALUE_TEMPLATE] = ( + "{{ state.attributes.humidity }}" + ) else: # trigger_type == "current_humidity_changed" - numeric_state_config[ - numeric_state_trigger.CONF_VALUE_TEMPLATE - ] = "{{ state.attributes.current_humidity }}" + numeric_state_config[numeric_state_trigger.CONF_VALUE_TEMPLATE] = ( + "{{ state.attributes.current_humidity }}" + ) if CONF_ABOVE in config: numeric_state_config[CONF_ABOVE] = config[CONF_ABOVE] diff --git a/homeassistant/components/humidifier/group.py b/homeassistant/components/humidifier/group.py deleted file mode 100644 index 234883ffd5a..00000000000 --- a/homeassistant/components/humidifier/group.py +++ /dev/null @@ -1,14 +0,0 @@ -"""Describe group states.""" - - -from homeassistant.components.group import GroupIntegrationRegistry -from homeassistant.const import STATE_OFF, STATE_ON -from homeassistant.core import HomeAssistant, callback - - -@callback -def async_describe_on_off_states( - hass: HomeAssistant, registry: GroupIntegrationRegistry -) -> None: - """Describe group on off states.""" - registry.on_off_states({STATE_ON}, STATE_OFF) diff --git a/homeassistant/components/humidifier/intent.py b/homeassistant/components/humidifier/intent.py index 103521aeb04..361de8e36db 100644 --- a/homeassistant/components/humidifier/intent.py +++ b/homeassistant/components/humidifier/intent.py @@ -1,4 +1,5 @@ """Intents for the humidifier integration.""" + from __future__ import annotations import voluptuous as vol diff --git a/homeassistant/components/humidifier/reproduce_state.py b/homeassistant/components/humidifier/reproduce_state.py index be4f1afbeb9..7caff04acdb 100644 --- a/homeassistant/components/humidifier/reproduce_state.py +++ b/homeassistant/components/humidifier/reproduce_state.py @@ -1,4 +1,5 @@ """Module that groups code required to handle state restore for component.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/humidifier/significant_change.py b/homeassistant/components/humidifier/significant_change.py index cc279a9fa41..dcf89f2eba9 100644 --- a/homeassistant/components/humidifier/significant_change.py +++ b/homeassistant/components/humidifier/significant_change.py @@ -1,4 +1,5 @@ """Helper to test significant Humidifier state changes.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/hunterdouglas_powerview/__init__.py b/homeassistant/components/hunterdouglas_powerview/__init__.py index 4156dcdafae..106a61e75cc 100644 --- a/homeassistant/components/hunterdouglas_powerview/__init__.py +++ b/homeassistant/components/hunterdouglas_powerview/__init__.py @@ -1,5 +1,5 @@ """The Hunter Douglas PowerView integration.""" -import asyncio + import logging from aiopvapi.helpers.aiorequest import AioRequest @@ -51,11 +51,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hub_address, loop=hass.loop, websession=websession, api_version=api_version ) + # default 15 second timeout for each call in upstream try: - async with asyncio.timeout(10): - hub = Hub(pv_request) - await hub.query_firmware() - device_info = await async_get_device_info(hub) + hub = Hub(pv_request) + await hub.query_firmware() + device_info = await async_get_device_info(hub) except HUB_EXCEPTIONS as err: raise ConfigEntryNotReady( f"Connection error to PowerView hub {hub_address}: {err}" @@ -74,15 +74,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return False try: - async with asyncio.timeout(10): - rooms = Rooms(pv_request) - room_data: PowerviewData = await rooms.get_rooms() - async with asyncio.timeout(10): - scenes = Scenes(pv_request) - scene_data: PowerviewData = await scenes.get_scenes() - async with asyncio.timeout(10): - shades = Shades(pv_request) - shade_data: PowerviewData = await shades.get_shades() + rooms = Rooms(pv_request) + room_data: PowerviewData = await rooms.get_rooms() + + scenes = Scenes(pv_request) + scene_data: PowerviewData = await scenes.get_scenes() + + shades = Shades(pv_request) + shade_data: PowerviewData = await shades.get_shades() except HUB_EXCEPTIONS as err: raise ConfigEntryNotReady( f"Connection error to PowerView hub {hub_address}: {err}" diff --git a/homeassistant/components/hunterdouglas_powerview/button.py b/homeassistant/components/hunterdouglas_powerview/button.py index c37741fcb09..ecb71f9653a 100644 --- a/homeassistant/components/hunterdouglas_powerview/button.py +++ b/homeassistant/components/hunterdouglas_powerview/button.py @@ -1,4 +1,5 @@ """Buttons for Hunter Douglas Powerview advanced features.""" + from __future__ import annotations from collections.abc import Callable @@ -84,19 +85,18 @@ async def async_setup_entry( entities: list[ButtonEntity] = [] for shade in pv_entry.shade_data.values(): room_name = getattr(pv_entry.room_data.get(shade.room_id), ATTR_NAME, "") - for description in BUTTONS_SHADE: - if description.create_entity_fn(shade): - entities.append( - PowerviewShadeButton( - pv_entry.coordinator, - pv_entry.device_info, - room_name, - shade, - shade.name, - description, - ) - ) - + entities.extend( + PowerviewShadeButton( + pv_entry.coordinator, + pv_entry.device_info, + room_name, + shade, + shade.name, + description, + ) + for description in BUTTONS_SHADE + if description.create_entity_fn(shade) + ) async_add_entities(entities) @@ -119,4 +119,5 @@ class PowerviewShadeButton(ShadeEntity, ButtonEntity): async def async_press(self) -> None: """Handle the button press.""" - await self.entity_description.press_action(self._shade) + async with self.coordinator.radio_operation_lock: + await self.entity_description.press_action(self._shade) diff --git a/homeassistant/components/hunterdouglas_powerview/config_flow.py b/homeassistant/components/hunterdouglas_powerview/config_flow.py index 97e04b7d522..7753f4ba94b 100644 --- a/homeassistant/components/hunterdouglas_powerview/config_flow.py +++ b/homeassistant/components/hunterdouglas_powerview/config_flow.py @@ -1,7 +1,7 @@ """Config flow for Hunter Douglas PowerView integration.""" + from __future__ import annotations -import asyncio import logging from typing import TYPE_CHECKING, Any @@ -9,10 +9,11 @@ from aiopvapi.helpers.aiorequest import AioRequest from aiopvapi.hub import Hub import voluptuous as vol -from homeassistant import config_entries, core, exceptions from homeassistant.components import dhcp, zeroconf +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_API_VERSION, CONF_HOST, CONF_NAME -from homeassistant.data_entry_flow import FlowResult +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession from . import async_get_device_info @@ -25,7 +26,7 @@ POWERVIEW_G2_SUFFIX = "._powerview._tcp.local." POWERVIEW_G3_SUFFIX = "._powerview-g3._tcp.local." -async def validate_input(hass: core.HomeAssistant, hub_address: str) -> dict[str, str]: +async def validate_input(hass: HomeAssistant, hub_address: str) -> dict[str, str]: """Validate the user input allows us to connect. Data has the keys from DATA_SCHEMA with values provided by the user. @@ -36,10 +37,9 @@ async def validate_input(hass: core.HomeAssistant, hub_address: str) -> dict[str pv_request = AioRequest(hub_address, loop=hass.loop, websession=websession) try: - async with asyncio.timeout(10): - hub = Hub(pv_request) - await hub.query_firmware() - device_info = await async_get_device_info(hub) + hub = Hub(pv_request) + await hub.query_firmware() + device_info = await async_get_device_info(hub) except HUB_EXCEPTIONS as err: raise CannotConnect from err @@ -59,7 +59,7 @@ async def validate_input(hass: core.HomeAssistant, hub_address: str) -> dict[str } -class PowerviewConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class PowerviewConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Hunter Douglas PowerView.""" VERSION = 1 @@ -73,7 +73,7 @@ class PowerviewConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors: dict[str, str] = {} @@ -120,7 +120,9 @@ class PowerviewConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return info, None - async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: + async def async_step_dhcp( + self, discovery_info: dhcp.DhcpServiceInfo + ) -> ConfigFlowResult: """Handle DHCP discovery.""" self.discovered_ip = discovery_info.ip self.discovered_name = discovery_info.hostname @@ -128,7 +130,7 @@ class PowerviewConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_zeroconf( self, discovery_info: zeroconf.ZeroconfServiceInfo - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle zeroconf discovery.""" self.discovered_ip = discovery_info.host name = discovery_info.name.removesuffix(POWERVIEW_G2_SUFFIX) @@ -138,14 +140,14 @@ class PowerviewConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_homekit( self, discovery_info: zeroconf.ZeroconfServiceInfo - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle HomeKit discovery.""" self.discovered_ip = discovery_info.host name = discovery_info.name.removesuffix(HAP_SUFFIX) self.discovered_name = name return await self.async_step_discovery_confirm() - async def async_step_discovery_confirm(self) -> FlowResult: + async def async_step_discovery_confirm(self) -> ConfigFlowResult: """Confirm dhcp or homekit discovery.""" # If we already have the host configured do # not open connections to it if we can avoid it. @@ -177,7 +179,7 @@ class PowerviewConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_link( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Attempt to link with Powerview.""" if user_input is not None: return self.async_create_entry( @@ -195,9 +197,9 @@ class PowerviewConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -class CannotConnect(exceptions.HomeAssistantError): +class CannotConnect(HomeAssistantError): """Error to indicate we cannot connect.""" -class UnsupportedDevice(exceptions.HomeAssistantError): +class UnsupportedDevice(HomeAssistantError): """Error to indicate the device is not supported.""" diff --git a/homeassistant/components/hunterdouglas_powerview/const.py b/homeassistant/components/hunterdouglas_powerview/const.py index a2d18c6f512..ec55d413416 100644 --- a/homeassistant/components/hunterdouglas_powerview/const.py +++ b/homeassistant/components/hunterdouglas_powerview/const.py @@ -1,6 +1,5 @@ """Constants for Hunter Douglas Powerview hub.""" - from aiohttp.client_exceptions import ServerDisconnectedError from aiopvapi.helpers.aiorequest import ( PvApiConnectionError, diff --git a/homeassistant/components/hunterdouglas_powerview/coordinator.py b/homeassistant/components/hunterdouglas_powerview/coordinator.py index db4079f2b58..f074b06b2bc 100644 --- a/homeassistant/components/hunterdouglas_powerview/coordinator.py +++ b/homeassistant/components/hunterdouglas_powerview/coordinator.py @@ -1,4 +1,5 @@ """Coordinate data for powerview devices.""" + from __future__ import annotations import asyncio @@ -25,6 +26,10 @@ class PowerviewShadeUpdateCoordinator(DataUpdateCoordinator[PowerviewShadeData]) """Initialize DataUpdateCoordinator to gather data for specific Powerview Hub.""" self.shades = shades self.hub = hub + # The hub tends to crash if there are multiple radio operations at the same time + # but it seems to handle all other requests that do not use RF without issue + # so we have a lock to prevent multiple radio operations at the same time + self.radio_operation_lock = asyncio.Lock() super().__init__( hass, _LOGGER, @@ -35,16 +40,15 @@ class PowerviewShadeUpdateCoordinator(DataUpdateCoordinator[PowerviewShadeData]) async def _async_update_data(self) -> PowerviewShadeData: """Fetch data from shade endpoint.""" - async with asyncio.timeout(10): - try: - shade_entries = await self.shades.get_shades() - except PvApiMaintenance as error: - # hub is undergoing maintenance, pause polling - raise UpdateFailed(error) from error - except HUB_EXCEPTIONS as error: - raise UpdateFailed( - f"Powerview Hub {self.hub.hub_address} did not return any data: {error}" - ) from error + try: + shade_entries = await self.shades.get_shades() + except PvApiMaintenance as error: + # hub is undergoing maintenance, pause polling + raise UpdateFailed(error) from error + except HUB_EXCEPTIONS as error: + raise UpdateFailed( + f"Powerview Hub {self.hub.hub_address} did not return any data: {error}" + ) from error if not shade_entries: raise UpdateFailed("No new shade data was returned") diff --git a/homeassistant/components/hunterdouglas_powerview/cover.py b/homeassistant/components/hunterdouglas_powerview/cover.py index 5b998b697a4..57409f37ac9 100644 --- a/homeassistant/components/hunterdouglas_powerview/cover.py +++ b/homeassistant/components/hunterdouglas_powerview/cover.py @@ -1,9 +1,8 @@ """Support for hunter douglas shades.""" + from __future__ import annotations -import asyncio from collections.abc import Callable, Iterable -from contextlib import suppress from dataclasses import replace from datetime import datetime, timedelta import logging @@ -67,15 +66,12 @@ async def async_setup_entry( """ for shade in pv_entry.shade_data.values(): - with suppress(TimeoutError): - # hold off to avoid spamming the hub - async with asyncio.timeout(10): - _LOGGER.debug("Initial refresh of shade: %s", shade.name) - await shade.refresh() + _LOGGER.debug("Initial refresh of shade: %s", shade.name) + async with coordinator.radio_operation_lock: + await shade.refresh(suppress_timeout=True) # default 15 second timeout entities: list[ShadeEntity] = [] for shade in pv_entry.shade_data.values(): - coordinator.data.update_shade_position(shade.id, shade.current_position) room_name = getattr(pv_entry.room_data.get(shade.room_id), ATTR_NAME, "") entities.extend( create_powerview_shade_entity( @@ -212,7 +208,8 @@ class PowerViewShadeBase(ShadeEntity, CoverEntity): async def _async_execute_move(self, move: ShadePosition) -> None: """Execute a move that can affect multiple positions.""" _LOGGER.debug("Move request %s: %s", self.name, move) - response = await self._shade.move(move) + async with self.coordinator.radio_operation_lock: + response = await self._shade.move(move) _LOGGER.debug("Move response %s: %s", self.name, response) # Process the response from the hub (including new positions) @@ -323,9 +320,10 @@ class PowerViewShadeBase(ShadeEntity, CoverEntity): # error if are already have one in flight return # suppress timeouts caused by hub nightly reboot - with suppress(TimeoutError): - async with asyncio.timeout(10): - await self._shade.refresh() + async with self.coordinator.radio_operation_lock: + await self._shade.refresh( + suppress_timeout=True + ) # default 15 second timeout _LOGGER.debug("Process update %s: %s", self.name, self._shade.current_position) self._async_update_shade_data(self._shade.current_position) @@ -519,6 +517,22 @@ class PowerViewShadeTiltOnly(PowerViewShadeWithTiltBase): self._attr_supported_features |= CoverEntityFeature.STOP_TILT self._max_tilt = self._shade.shade_limits.tilt_max + @property + def current_cover_position(self) -> int: + """Return the current position of cover.""" + # allows using parent class with no other alterations + return CLOSED_POSITION + + @property + def transition_steps(self) -> int: + """Return the steps to make a move.""" + return self.positions.tilt + + @property + def is_closed(self) -> bool: + """Return if the cover is closed.""" + return self.positions.tilt <= CLOSED_POSITION + class PowerViewShadeTopDown(PowerViewShadeBase): """Representation of a shade that lowers from the roof to the floor. @@ -983,6 +997,11 @@ TYPE_TO_CLASSES = { PowerViewShadeDualOverlappedFront, PowerViewShadeDualOverlappedRear, ), + 11: ( + PowerViewShadeDualOverlappedCombined, + PowerViewShadeDualOverlappedFront, + PowerViewShadeDualOverlappedRear, + ), } diff --git a/homeassistant/components/hunterdouglas_powerview/diagnostics.py b/homeassistant/components/hunterdouglas_powerview/diagnostics.py index 12f424ea501..1fbf721d2bd 100644 --- a/homeassistant/components/hunterdouglas_powerview/diagnostics.py +++ b/homeassistant/components/hunterdouglas_powerview/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for Powerview Hunter Douglas.""" + from __future__ import annotations from dataclasses import asdict diff --git a/homeassistant/components/hunterdouglas_powerview/manifest.json b/homeassistant/components/hunterdouglas_powerview/manifest.json index 276b10f5e8d..4120c55a7a7 100644 --- a/homeassistant/components/hunterdouglas_powerview/manifest.json +++ b/homeassistant/components/hunterdouglas_powerview/manifest.json @@ -18,6 +18,6 @@ }, "iot_class": "local_polling", "loggers": ["aiopvapi"], - "requirements": ["aiopvapi==3.0.2"], + "requirements": ["aiopvapi==3.1.1"], "zeroconf": ["_powerview._tcp.local.", "_powerview-g3._tcp.local."] } diff --git a/homeassistant/components/hunterdouglas_powerview/model.py b/homeassistant/components/hunterdouglas_powerview/model.py index e2311eb4e4c..7cf259ced18 100644 --- a/homeassistant/components/hunterdouglas_powerview/model.py +++ b/homeassistant/components/hunterdouglas_powerview/model.py @@ -1,4 +1,5 @@ """Define Hunter Douglas data models.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/homeassistant/components/hunterdouglas_powerview/number.py b/homeassistant/components/hunterdouglas_powerview/number.py index 6b18f663c71..b37331c08df 100644 --- a/homeassistant/components/hunterdouglas_powerview/number.py +++ b/homeassistant/components/hunterdouglas_powerview/number.py @@ -66,19 +66,18 @@ async def async_setup_entry( entities: list[PowerViewNumber] = [] for shade in pv_entry.shade_data.values(): room_name = getattr(pv_entry.room_data.get(shade.room_id), ATTR_NAME, "") - for description in NUMBERS: - if description.create_entity_fn(shade): - entities.append( - PowerViewNumber( - pv_entry.coordinator, - pv_entry.device_info, - room_name, - shade, - shade.name, - description, - ) - ) - + entities.extend( + PowerViewNumber( + pv_entry.coordinator, + pv_entry.device_info, + room_name, + shade, + shade.name, + description, + ) + for description in NUMBERS + if description.create_entity_fn(shade) + ) async_add_entities(entities) diff --git a/homeassistant/components/hunterdouglas_powerview/scene.py b/homeassistant/components/hunterdouglas_powerview/scene.py index 0ba9b13d03b..af5b86960c4 100644 --- a/homeassistant/components/hunterdouglas_powerview/scene.py +++ b/homeassistant/components/hunterdouglas_powerview/scene.py @@ -1,4 +1,5 @@ """Support for Powerview scenes from a Powerview hub.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/hunterdouglas_powerview/select.py b/homeassistant/components/hunterdouglas_powerview/select.py index bbe4614afd1..f1e9c491659 100644 --- a/homeassistant/components/hunterdouglas_powerview/select.py +++ b/homeassistant/components/hunterdouglas_powerview/select.py @@ -1,4 +1,5 @@ """Support for hunterdouglass_powerview settings.""" + from __future__ import annotations from collections.abc import Callable, Coroutine @@ -67,19 +68,18 @@ async def async_setup_entry( if not shade.has_battery_info(): continue room_name = getattr(pv_entry.room_data.get(shade.room_id), ATTR_NAME, "") - for description in DROPDOWNS: - if description.create_entity_fn(shade): - entities.append( - PowerViewSelect( - pv_entry.coordinator, - pv_entry.device_info, - room_name, - shade, - shade.name, - description, - ) - ) - + entities.extend( + PowerViewSelect( + pv_entry.coordinator, + pv_entry.device_info, + room_name, + shade, + shade.name, + description, + ) + for description in DROPDOWNS + if description.create_entity_fn(shade) + ) async_add_entities(entities) @@ -114,5 +114,6 @@ class PowerViewSelect(ShadeEntity, SelectEntity): """Change the selected option.""" await self.entity_description.select_fn(self._shade, option) # force update data to ensure new info is in coordinator - await self._shade.refresh() + async with self.coordinator.radio_operation_lock: + await self._shade.refresh(suppress_timeout=True) self.async_write_ha_state() diff --git a/homeassistant/components/hunterdouglas_powerview/sensor.py b/homeassistant/components/hunterdouglas_powerview/sensor.py index 02b4ae7c557..b24193ac438 100644 --- a/homeassistant/components/hunterdouglas_powerview/sensor.py +++ b/homeassistant/components/hunterdouglas_powerview/sensor.py @@ -62,7 +62,7 @@ SENSORS: Final = [ native_unit_fn=lambda shade: PERCENTAGE, native_value_fn=lambda shade: shade.get_battery_strength(), create_entity_fn=lambda shade: shade.is_battery_powered(), - update_fn=lambda shade: shade.refresh_battery(), + update_fn=lambda shade: shade.refresh_battery(suppress_timeout=True), ), PowerviewSensorDescription( key="signal", @@ -72,7 +72,7 @@ SENSORS: Final = [ native_unit_fn=get_signal_native_unit, native_value_fn=lambda shade: shade.get_signal_strength(), create_entity_fn=lambda shade: shade.has_signal_strength(), - update_fn=lambda shade: shade.refresh(), + update_fn=lambda shade: shade.refresh(suppress_timeout=True), entity_registry_enabled_default=False, ), ] @@ -88,19 +88,18 @@ async def async_setup_entry( entities: list[PowerViewSensor] = [] for shade in pv_entry.shade_data.values(): room_name = getattr(pv_entry.room_data.get(shade.room_id), ATTR_NAME, "") - for description in SENSORS: - if description.create_entity_fn(shade): - entities.append( - PowerViewSensor( - pv_entry.coordinator, - pv_entry.device_info, - room_name, - shade, - shade.name, - description, - ) - ) - + entities.extend( + PowerViewSensor( + pv_entry.coordinator, + pv_entry.device_info, + room_name, + shade, + shade.name, + description, + ) + for description in SENSORS + if description.create_entity_fn(shade) + ) async_add_entities(entities) @@ -154,5 +153,6 @@ class PowerViewSensor(ShadeEntity, SensorEntity): async def async_update(self) -> None: """Refresh sensor entity.""" - await self.entity_description.update_fn(self._shade) + async with self.coordinator.radio_operation_lock: + await self.entity_description.update_fn(self._shade) self.async_write_ha_state() diff --git a/homeassistant/components/hunterdouglas_powerview/shade_data.py b/homeassistant/components/hunterdouglas_powerview/shade_data.py index 86f232c3b66..fd2f0466467 100644 --- a/homeassistant/components/hunterdouglas_powerview/shade_data.py +++ b/homeassistant/components/hunterdouglas_powerview/shade_data.py @@ -1,6 +1,8 @@ """Shade data for the Hunter Douglas PowerView integration.""" + from __future__ import annotations +from dataclasses import fields import logging from typing import Any @@ -11,74 +13,71 @@ from .util import async_map_data_by_id _LOGGER = logging.getLogger(__name__) +POSITION_FIELDS = [field for field in fields(ShadePosition) if field.name != "velocity"] + + +def copy_position_data(source: ShadePosition, target: ShadePosition) -> ShadePosition: + """Copy position data from source to target for None values only.""" + for field in POSITION_FIELDS: + if (value := getattr(source, field.name)) is not None: + setattr(target, field.name, value) + class PowerviewShadeData: """Coordinate shade data between multiple api calls.""" def __init__(self) -> None: """Init the shade data.""" - self._group_data_by_id: dict[int, dict[str | int, Any]] = {} - self._shade_data_by_id: dict[int, BaseShade] = {} + self._raw_data_by_id: dict[int, dict[str | int, Any]] = {} + self._shade_group_data_by_id: dict[int, BaseShade] = {} self.positions: dict[int, ShadePosition] = {} def get_raw_data(self, shade_id: int) -> dict[str | int, Any]: """Get data for the shade.""" - return self._group_data_by_id[shade_id] + return self._raw_data_by_id[shade_id] def get_all_raw_data(self) -> dict[int, dict[str | int, Any]]: """Get data for all shades.""" - return self._group_data_by_id + return self._raw_data_by_id def get_shade(self, shade_id: int) -> BaseShade: """Get specific shade from the coordinator.""" - return self._shade_data_by_id[shade_id] + return self._shade_group_data_by_id[shade_id] def get_shade_position(self, shade_id: int) -> ShadePosition: """Get positions for a shade.""" if shade_id not in self.positions: - self.positions[shade_id] = ShadePosition() + shade_position = ShadePosition() + # If we have the group data, use it to populate the initial position + if shade := self._shade_group_data_by_id.get(shade_id): + copy_position_data(shade.current_position, shade_position) + self.positions[shade_id] = shade_position return self.positions[shade_id] def update_from_group_data(self, shade_id: int) -> None: """Process an update from the group data.""" - self.update_shade_positions(self._shade_data_by_id[shade_id]) + data = self._shade_group_data_by_id[shade_id] + copy_position_data(data.current_position, self.get_shade_position(data.id)) def store_group_data(self, shade_data: PowerviewData) -> None: """Store data from the all shades endpoint. - This does not update the shades or positions + This does not update the shades or positions (self.positions) as the data may be stale. update_from_group_data with a shade_id will update a specific shade from the group data. """ - self._shade_data_by_id = shade_data.processed - self._group_data_by_id = async_map_data_by_id(shade_data.raw) + self._shade_group_data_by_id = shade_data.processed + self._raw_data_by_id = async_map_data_by_id(shade_data.raw) - def update_shade_position(self, shade_id: int, shade_data: ShadePosition) -> None: + def update_shade_position(self, shade_id: int, new_position: ShadePosition) -> None: """Update a single shades position.""" - if shade_id not in self.positions: - self.positions[shade_id] = ShadePosition() - - # ShadePosition will return None if the value is not set - if shade_data.primary is not None: - self.positions[shade_id].primary = shade_data.primary - if shade_data.secondary is not None: - self.positions[shade_id].secondary = shade_data.secondary - if shade_data.tilt is not None: - self.positions[shade_id].tilt = shade_data.tilt + copy_position_data(new_position, self.get_shade_position(shade_id)) def update_shade_velocity(self, shade_id: int, shade_data: ShadePosition) -> None: """Update a single shades velocity.""" - if shade_id not in self.positions: - self.positions[shade_id] = ShadePosition() - # the hub will always return a velocity of 0 on initial connect, # separate definition to store consistent value in HA # this value is purely driven from HA if shade_data.velocity is not None: - self.positions[shade_id].velocity = shade_data.velocity - - def update_shade_positions(self, data: BaseShade) -> None: - """Update a shades from data dict.""" - _LOGGER.debug("Raw data update: %s", data.raw_data) - self.update_shade_position(data.id, data.current_position) + self.get_shade_position(shade_id).velocity = shade_data.velocity diff --git a/homeassistant/components/hunterdouglas_powerview/util.py b/homeassistant/components/hunterdouglas_powerview/util.py index 15330f30bdb..1d670f46429 100644 --- a/homeassistant/components/hunterdouglas_powerview/util.py +++ b/homeassistant/components/hunterdouglas_powerview/util.py @@ -1,4 +1,5 @@ """Coordinate data for powerview devices.""" + from __future__ import annotations from collections.abc import Iterable diff --git a/homeassistant/components/husqvarna_automower/__init__.py b/homeassistant/components/husqvarna_automower/__init__.py index 20218229385..03ab02429bb 100644 --- a/homeassistant/components/husqvarna_automower/__init__.py +++ b/homeassistant/components/husqvarna_automower/__init__.py @@ -3,12 +3,12 @@ import logging from aioautomower.session import AutomowerSession -from aiohttp import ClientError +from aiohttp import ClientResponseError from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import aiohttp_client, config_entry_oauth2_flow from . import api @@ -17,7 +17,14 @@ from .coordinator import AutomowerDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) -PLATFORMS: list[Platform] = [Platform.LAWN_MOWER, Platform.SENSOR, Platform.SWITCH] +PLATFORMS: list[Platform] = [ + Platform.BINARY_SENSOR, + Platform.DEVICE_TRACKER, + Platform.LAWN_MOWER, + Platform.SELECT, + Platform.SENSOR, + Platform.SWITCH, +] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -35,7 +42,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: automower_api = AutomowerSession(api_api) try: await api_api.async_get_access_token() - except ClientError as err: + except ClientResponseError as err: + if 400 <= err.status < 500: + raise ConfigEntryAuthFailed from err raise ConfigEntryNotReady from err coordinator = AutomowerDataUpdateCoordinator(hass, automower_api, entry) await coordinator.async_config_entry_first_refresh() diff --git a/homeassistant/components/husqvarna_automower/binary_sensor.py b/homeassistant/components/husqvarna_automower/binary_sensor.py new file mode 100644 index 00000000000..e8e64e7ffc7 --- /dev/null +++ b/homeassistant/components/husqvarna_automower/binary_sensor.py @@ -0,0 +1,82 @@ +"""Creates the binary sensor entities for the mower.""" + +from collections.abc import Callable +from dataclasses import dataclass +import logging + +from aioautomower.model import MowerActivities, MowerAttributes + +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import AutomowerDataUpdateCoordinator +from .entity import AutomowerBaseEntity + +_LOGGER = logging.getLogger(__name__) + + +@dataclass(frozen=True, kw_only=True) +class AutomowerBinarySensorEntityDescription(BinarySensorEntityDescription): + """Describes Automower binary sensor entity.""" + + value_fn: Callable[[MowerAttributes], bool] + + +BINARY_SENSOR_TYPES: tuple[AutomowerBinarySensorEntityDescription, ...] = ( + AutomowerBinarySensorEntityDescription( + key="battery_charging", + value_fn=lambda data: data.mower.activity == MowerActivities.CHARGING, + device_class=BinarySensorDeviceClass.BATTERY_CHARGING, + ), + AutomowerBinarySensorEntityDescription( + key="leaving_dock", + translation_key="leaving_dock", + value_fn=lambda data: data.mower.activity == MowerActivities.LEAVING, + ), + AutomowerBinarySensorEntityDescription( + key="returning_to_dock", + translation_key="returning_to_dock", + value_fn=lambda data: data.mower.activity == MowerActivities.GOING_HOME, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up binary sensor platform.""" + coordinator: AutomowerDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + AutomowerBinarySensorEntity(mower_id, coordinator, description) + for mower_id in coordinator.data + for description in BINARY_SENSOR_TYPES + ) + + +class AutomowerBinarySensorEntity(AutomowerBaseEntity, BinarySensorEntity): + """Defining the Automower Sensors with AutomowerBinarySensorEntityDescription.""" + + entity_description: AutomowerBinarySensorEntityDescription + + def __init__( + self, + mower_id: str, + coordinator: AutomowerDataUpdateCoordinator, + description: AutomowerBinarySensorEntityDescription, + ) -> None: + """Set up AutomowerSensors.""" + super().__init__(mower_id, coordinator) + self.entity_description = description + self._attr_unique_id = f"{mower_id}_{description.key}" + + @property + def is_on(self) -> bool: + """Return the state of the binary sensor.""" + return self.entity_description.value_fn(self.mower_attributes) diff --git a/homeassistant/components/husqvarna_automower/config_flow.py b/homeassistant/components/husqvarna_automower/config_flow.py index e306be7137c..b25a185c75f 100644 --- a/homeassistant/components/husqvarna_automower/config_flow.py +++ b/homeassistant/components/husqvarna_automower/config_flow.py @@ -1,11 +1,13 @@ """Config flow to add the integration via the UI.""" + +from collections.abc import Mapping import logging from typing import Any from aioautomower.utils import structure_token +from homeassistant.config_entries import ConfigEntry, ConfigFlowResult from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_entry_oauth2_flow from .const import DOMAIN, NAME @@ -22,11 +24,16 @@ class HusqvarnaConfigFlowHandler( VERSION = 1 DOMAIN = DOMAIN + reauth_entry: ConfigEntry | None = None - async def async_oauth_create_entry(self, data: dict[str, Any]) -> FlowResult: + async def async_oauth_create_entry(self, data: dict[str, Any]) -> ConfigFlowResult: """Create an entry for the flow.""" token = data[CONF_TOKEN] user_id = token[CONF_USER_ID] + if self.reauth_entry: + if self.reauth_entry.unique_id != user_id: + return self.async_abort(reason="wrong_account") + return self.async_update_reload_and_abort(self.reauth_entry, data=data) structured_token = structure_token(token[CONF_ACCESS_TOKEN]) first_name = structured_token.user.first_name last_name = structured_token.user.last_name @@ -41,3 +48,20 @@ class HusqvarnaConfigFlowHandler( def logger(self) -> logging.Logger: """Return logger.""" return logging.getLogger(__name__) + + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: + """Perform reauth upon an API authentication error.""" + self.reauth_entry = self.hass.config_entries.async_get_entry( + self.context["entry_id"] + ) + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Confirm reauth dialog.""" + if user_input is None: + return self.async_show_form(step_id="reauth_confirm") + return await self.async_step_user() diff --git a/homeassistant/components/husqvarna_automower/const.py b/homeassistant/components/husqvarna_automower/const.py index ab30bae45f2..5e38b354957 100644 --- a/homeassistant/components/husqvarna_automower/const.py +++ b/homeassistant/components/husqvarna_automower/const.py @@ -2,6 +2,5 @@ DOMAIN = "husqvarna_automower" NAME = "Husqvarna Automower" -HUSQVARNA_URL = "https://developer.husqvarnagroup.cloud/login" OAUTH2_AUTHORIZE = "https://api.authentication.husqvarnagroup.dev/v1/oauth2/authorize" OAUTH2_TOKEN = "https://api.authentication.husqvarnagroup.dev/v1/oauth2/token" diff --git a/homeassistant/components/husqvarna_automower/coordinator.py b/homeassistant/components/husqvarna_automower/coordinator.py index 2840823415a..2188725ed76 100644 --- a/homeassistant/components/husqvarna_automower/coordinator.py +++ b/homeassistant/components/husqvarna_automower/coordinator.py @@ -1,4 +1,5 @@ """Data UpdateCoordinator for the Husqvarna Automower integration.""" + import asyncio from datetime import timedelta import logging diff --git a/homeassistant/components/husqvarna_automower/device_tracker.py b/homeassistant/components/husqvarna_automower/device_tracker.py new file mode 100644 index 00000000000..a32fd8758bd --- /dev/null +++ b/homeassistant/components/husqvarna_automower/device_tracker.py @@ -0,0 +1,52 @@ +"""Creates the device tracker entity for the mower.""" + +from homeassistant.components.device_tracker import SourceType, TrackerEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import AutomowerDataUpdateCoordinator +from .entity import AutomowerBaseEntity + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up device tracker platform.""" + coordinator: AutomowerDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + AutomowerDeviceTrackerEntity(mower_id, coordinator) + for mower_id in coordinator.data + if coordinator.data[mower_id].capabilities.position + ) + + +class AutomowerDeviceTrackerEntity(AutomowerBaseEntity, TrackerEntity): + """Defining the AutomowerDeviceTrackerEntity.""" + + _attr_name = None + + def __init__( + self, + mower_id: str, + coordinator: AutomowerDataUpdateCoordinator, + ) -> None: + """Initialize AutomowerDeviceTracker.""" + super().__init__(mower_id, coordinator) + self._attr_unique_id = mower_id + + @property + def source_type(self) -> SourceType: + """Return the source type of the device.""" + return SourceType.GPS + + @property + def latitude(self) -> float: + """Return latitude value of the device.""" + return self.mower_attributes.positions[0].latitude + + @property + def longitude(self) -> float: + """Return longitude value of the device.""" + return self.mower_attributes.positions[0].longitude diff --git a/homeassistant/components/husqvarna_automower/diagnostics.py b/homeassistant/components/husqvarna_automower/diagnostics.py new file mode 100644 index 00000000000..f5677d4cb4b --- /dev/null +++ b/homeassistant/components/husqvarna_automower/diagnostics.py @@ -0,0 +1,47 @@ +"""Diagnostics support for Husqvarna Automower.""" + +from __future__ import annotations + +import logging +from typing import Any + +from homeassistant.components.diagnostics import async_redact_data +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_ACCESS_TOKEN +from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceEntry + +from .const import DOMAIN +from .coordinator import AutomowerDataUpdateCoordinator + +CONF_REFRESH_TOKEN = "refresh_token" +POSITIONS = "positions" + +TO_REDACT = { + CONF_ACCESS_TOKEN, + CONF_REFRESH_TOKEN, + POSITIONS, +} +_LOGGER = logging.getLogger(__name__) + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + return async_redact_data(entry.as_dict(), TO_REDACT) + + +async def async_get_device_diagnostics( + hass: HomeAssistant, entry: ConfigEntry, device: DeviceEntry +) -> dict[str, Any]: + """Return diagnostics for a device entry.""" + coordinator: AutomowerDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + for identifier in device.identifiers: + if identifier[0] == DOMAIN: + if ( + coordinator.data[identifier[1]].system.serial_number + == device.serial_number + ): + mower_id = identifier[1] + return async_redact_data(coordinator.data[mower_id].to_dict(), TO_REDACT) diff --git a/homeassistant/components/husqvarna_automower/entity.py b/homeassistant/components/husqvarna_automower/entity.py index 2edce942f0c..4d20d2d677b 100644 --- a/homeassistant/components/husqvarna_automower/entity.py +++ b/homeassistant/components/husqvarna_automower/entity.py @@ -28,9 +28,10 @@ class AutomowerBaseEntity(CoordinatorEntity[AutomowerDataUpdateCoordinator]): self.mower_id = mower_id self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, mower_id)}, - name=self.mower_attributes.system.name, manufacturer="Husqvarna", model=self.mower_attributes.system.model, + name=self.mower_attributes.system.name, + serial_number=self.mower_attributes.system.serial_number, suggested_area="Garden", ) diff --git a/homeassistant/components/husqvarna_automower/icons.json b/homeassistant/components/husqvarna_automower/icons.json new file mode 100644 index 00000000000..65cc85bd09b --- /dev/null +++ b/homeassistant/components/husqvarna_automower/icons.json @@ -0,0 +1,25 @@ +{ + "entity": { + "binary_sensor": { + "leaving_dock": { + "default": "mdi:debug-step-out" + }, + "returning_to_dock": { + "default": "mdi:debug-step-into" + } + }, + "select": { + "headlight_mode": { + "default": "mdi:car-light-high" + } + }, + "sensor": { + "number_of_charging_cycles": { + "default": "mdi:battery-sync-outline" + }, + "number_of_collisions": { + "default": "mdi:counter" + } + } + } +} diff --git a/homeassistant/components/husqvarna_automower/lawn_mower.py b/homeassistant/components/husqvarna_automower/lawn_mower.py index abf27af02f0..e9ed9187530 100644 --- a/homeassistant/components/husqvarna_automower/lawn_mower.py +++ b/homeassistant/components/husqvarna_automower/lawn_mower.py @@ -1,4 +1,5 @@ """Husqvarna Automower lawn mower entity.""" + import logging from aioautomower.exceptions import ApiException diff --git a/homeassistant/components/husqvarna_automower/manifest.json b/homeassistant/components/husqvarna_automower/manifest.json index ed013f2e0c2..e4536ee594d 100644 --- a/homeassistant/components/husqvarna_automower/manifest.json +++ b/homeassistant/components/husqvarna_automower/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/husqvarna_automower", "iot_class": "cloud_push", "loggers": ["aioautomower"], - "requirements": ["aioautomower==2024.3.3"] + "requirements": ["aioautomower==2024.3.4"] } diff --git a/homeassistant/components/husqvarna_automower/select.py b/homeassistant/components/husqvarna_automower/select.py new file mode 100644 index 00000000000..e4376a1bca5 --- /dev/null +++ b/homeassistant/components/husqvarna_automower/select.py @@ -0,0 +1,70 @@ +"""Creates a select entity for the headlight of the mower.""" + +import logging + +from aioautomower.exceptions import ApiException +from aioautomower.model import HeadlightModes + +from homeassistant.components.select import SelectEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import EntityCategory +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import AutomowerDataUpdateCoordinator +from .entity import AutomowerControlEntity + +_LOGGER = logging.getLogger(__name__) + + +HEADLIGHT_MODES: list = [ + HeadlightModes.ALWAYS_OFF.lower(), + HeadlightModes.ALWAYS_ON.lower(), + HeadlightModes.EVENING_AND_NIGHT.lower(), + HeadlightModes.EVENING_ONLY.lower(), +] + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up select platform.""" + coordinator: AutomowerDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + AutomowerSelectEntity(mower_id, coordinator) + for mower_id in coordinator.data + if coordinator.data[mower_id].capabilities.headlights + ) + + +class AutomowerSelectEntity(AutomowerControlEntity, SelectEntity): + """Defining the headlight mode entity.""" + + _attr_options = HEADLIGHT_MODES + _attr_entity_category = EntityCategory.CONFIG + _attr_translation_key = "headlight_mode" + + def __init__( + self, + mower_id: str, + coordinator: AutomowerDataUpdateCoordinator, + ) -> None: + """Set up select platform.""" + super().__init__(mower_id, coordinator) + self._attr_unique_id = f"{mower_id}_headlight_mode" + + @property + def current_option(self) -> str: + """Return the current option for the entity.""" + return self.mower_attributes.headlight.mode.lower() + + async def async_select_option(self, option: str) -> None: + """Change the selected option.""" + try: + await self.coordinator.api.set_headlight_mode(self.mower_id, option.upper()) + except ApiException as exception: + raise HomeAssistantError( + f"Command couldn't be sent to the command queue: {exception}" + ) from exception diff --git a/homeassistant/components/husqvarna_automower/sensor.py b/homeassistant/components/husqvarna_automower/sensor.py index 31eebde9c81..e054d02e3ba 100644 --- a/homeassistant/components/husqvarna_automower/sensor.py +++ b/homeassistant/components/husqvarna_automower/sensor.py @@ -1,7 +1,8 @@ """Creates a the sensor entities for the mower.""" + from collections.abc import Callable from dataclasses import dataclass -import datetime +from datetime import datetime import logging from aioautomower.model import MowerAttributes, MowerModes @@ -16,6 +17,8 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfLength, UnitOfTime from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType +from homeassistant.util import dt as dt_util from .const import DOMAIN from .coordinator import AutomowerDataUpdateCoordinator @@ -29,7 +32,7 @@ class AutomowerSensorEntityDescription(SensorEntityDescription): """Describes Automower sensor entity.""" exists_fn: Callable[[MowerAttributes], bool] = lambda _: True - value_fn: Callable[[MowerAttributes], str] + value_fn: Callable[[MowerAttributes], StateType | datetime] SENSOR_TYPES: tuple[AutomowerSensorEntityDescription, ...] = ( @@ -108,7 +111,6 @@ SENSOR_TYPES: tuple[AutomowerSensorEntityDescription, ...] = ( AutomowerSensorEntityDescription( key="number_of_charging_cycles", translation_key="number_of_charging_cycles", - icon="mdi:battery-sync-outline", entity_category=EntityCategory.DIAGNOSTIC, state_class=SensorStateClass.TOTAL, exists_fn=lambda data: data.statistics.number_of_charging_cycles is not None, @@ -117,7 +119,6 @@ SENSOR_TYPES: tuple[AutomowerSensorEntityDescription, ...] = ( AutomowerSensorEntityDescription( key="number_of_collisions", translation_key="number_of_collisions", - icon="mdi:counter", entity_category=EntityCategory.DIAGNOSTIC, state_class=SensorStateClass.TOTAL, exists_fn=lambda data: data.statistics.number_of_collisions is not None, @@ -138,7 +139,7 @@ SENSOR_TYPES: tuple[AutomowerSensorEntityDescription, ...] = ( key="next_start_timestamp", translation_key="next_start_timestamp", device_class=SensorDeviceClass.TIMESTAMP, - value_fn=lambda data: data.planner.next_start_dateteime, + value_fn=lambda data: dt_util.as_local(data.planner.next_start_datetime), ), ) @@ -173,6 +174,6 @@ class AutomowerSensorEntity(AutomowerBaseEntity, SensorEntity): self._attr_unique_id = f"{mower_id}_{description.key}" @property - def native_value(self) -> str | int | datetime.datetime | None: + def native_value(self) -> StateType | datetime: """Return the state of the sensor.""" return self.entity_description.value_fn(self.mower_attributes) diff --git a/homeassistant/components/husqvarna_automower/strings.json b/homeassistant/components/husqvarna_automower/strings.json index d6017de2bd7..8032c670404 100644 --- a/homeassistant/components/husqvarna_automower/strings.json +++ b/homeassistant/components/husqvarna_automower/strings.json @@ -1,6 +1,10 @@ { "config": { "step": { + "reauth_confirm": { + "title": "[%key:common::config_flow::title::reauth%]", + "description": "The Husqvarna Automower integration needs to re-authenticate your account" + }, "pick_implementation": { "title": "[%key:common::config_flow::title::oauth2_pick_implementation%]" } @@ -17,16 +21,31 @@ "user_rejected_authorize": "[%key:common::config_flow::abort::oauth2_user_rejected_authorize%]", "oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized%]", "oauth_failed": "[%key:common::config_flow::abort::oauth2_failed%]", - "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", + "wrong_account": "You can only reauthenticate this entry with the same Husqvarna account." }, "create_entry": { "default": "[%key:common::config_flow::create_entry::authenticated%]" } }, "entity": { - "switch": { - "enable_schedule": { - "name": "Enable schedule" + "binary_sensor": { + "leaving_dock": { + "name": "Leaving dock" + }, + "returning_to_dock": { + "name": "Returning to dock" + } + }, + "select": { + "headlight_mode": { + "name": "Headlight mode", + "state": { + "always_on": "Always on", + "always_off": "Always off", + "evening_only": "Evening only", + "evening_and_night": "Evening and night" + } } }, "sensor": { @@ -66,6 +85,11 @@ "demo": "Demo" } } + }, + "switch": { + "enable_schedule": { + "name": "Enable schedule" + } } } } diff --git a/homeassistant/components/husqvarna_automower/switch.py b/homeassistant/components/husqvarna_automower/switch.py index 9ba760a90e9..b178fc05c50 100644 --- a/homeassistant/components/husqvarna_automower/switch.py +++ b/homeassistant/components/husqvarna_automower/switch.py @@ -1,4 +1,5 @@ """Creates a switch entity for the mower.""" + import logging from typing import Any diff --git a/homeassistant/components/huum/__init__.py b/homeassistant/components/huum/__init__.py index a5daf471a2d..75faf1923df 100644 --- a/homeassistant/components/huum/__init__.py +++ b/homeassistant/components/huum/__init__.py @@ -1,4 +1,5 @@ """The Huum integration.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/huum/climate.py b/homeassistant/components/huum/climate.py index 2bc3c626deb..df740aea3d1 100644 --- a/homeassistant/components/huum/climate.py +++ b/homeassistant/components/huum/climate.py @@ -1,4 +1,5 @@ """Support for Huum wifi-enabled sauna.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/huum/config_flow.py b/homeassistant/components/huum/config_flow.py index 31f4c9a137c..e2ea2a7dbe1 100644 --- a/homeassistant/components/huum/config_flow.py +++ b/homeassistant/components/huum/config_flow.py @@ -1,4 +1,5 @@ """Config flow for huum integration.""" + from __future__ import annotations import logging @@ -10,7 +11,6 @@ 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 homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN @@ -32,7 +32,7 @@ class HuumConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> config_entries.ConfigFlowResult: """Handle the initial step.""" errors = {} if user_input is not None: diff --git a/homeassistant/components/hvv_departures/binary_sensor.py b/homeassistant/components/hvv_departures/binary_sensor.py index 8337921acf6..913c61f91b4 100644 --- a/homeassistant/components/hvv_departures/binary_sensor.py +++ b/homeassistant/components/hvv_departures/binary_sensor.py @@ -1,4 +1,5 @@ """Binary sensor platform for hvv_departures.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/hvv_departures/config_flow.py b/homeassistant/components/hvv_departures/config_flow.py index 24fb9c32a7d..0c909e2d8c1 100644 --- a/homeassistant/components/hvv_departures/config_flow.py +++ b/homeassistant/components/hvv_departures/config_flow.py @@ -1,4 +1,5 @@ """Config flow for HVV integration.""" + from __future__ import annotations import logging @@ -8,7 +9,7 @@ from pygti.auth import GTI_DEFAULT_HOST from pygti.exceptions import CannotConnect, InvalidAuth import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow from homeassistant.const import CONF_HOST, CONF_OFFSET, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import callback from homeassistant.helpers import aiohttp_client @@ -38,7 +39,7 @@ SCHEMA_STEP_OPTIONS = vol.Schema( ) -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class HVVDeparturesConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for HVV.""" VERSION = 1 @@ -125,16 +126,16 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback def async_get_options_flow( - config_entry: config_entries.ConfigEntry, + config_entry: ConfigEntry, ) -> OptionsFlowHandler: """Get options flow.""" return OptionsFlowHandler(config_entry) -class OptionsFlowHandler(config_entries.OptionsFlow): +class OptionsFlowHandler(OptionsFlow): """Options flow handler.""" - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + def __init__(self, config_entry: ConfigEntry) -> None: """Initialize HVV Departures options flow.""" self.config_entry = config_entry self.options = dict(config_entry.options) diff --git a/homeassistant/components/hvv_departures/sensor.py b/homeassistant/components/hvv_departures/sensor.py index 2267522e21b..5998a3dd826 100644 --- a/homeassistant/components/hvv_departures/sensor.py +++ b/homeassistant/components/hvv_departures/sensor.py @@ -1,4 +1,5 @@ """Sensor platform for hvv.""" + from datetime import timedelta import logging from typing import Any diff --git a/homeassistant/components/hydrawise/__init__.py b/homeassistant/components/hydrawise/__init__.py index 9f44d47ecf6..541d4211e49 100644 --- a/homeassistant/components/hydrawise/__init__.py +++ b/homeassistant/components/hydrawise/__init__.py @@ -1,6 +1,5 @@ """Support for Hydrawise cloud.""" - from pydrawise import legacy import voluptuous as vol diff --git a/homeassistant/components/hydrawise/binary_sensor.py b/homeassistant/components/hydrawise/binary_sensor.py index 0b12fcb3ddb..e75cf56ac75 100644 --- a/homeassistant/components/hydrawise/binary_sensor.py +++ b/homeassistant/components/hydrawise/binary_sensor.py @@ -1,4 +1,5 @@ """Support for Hydrawise sprinkler binary sensors.""" + from __future__ import annotations from pydrawise.schema import Zone @@ -74,11 +75,11 @@ async def async_setup_entry( entities.append( HydrawiseBinarySensor(coordinator, BINARY_SENSOR_STATUS, controller) ) - for zone in controller.zones: - for description in BINARY_SENSOR_TYPES: - entities.append( - HydrawiseBinarySensor(coordinator, description, controller, zone) - ) + entities.extend( + HydrawiseBinarySensor(coordinator, description, controller, zone) + for zone in controller.zones + for description in BINARY_SENSOR_TYPES + ) async_add_entities(entities) diff --git a/homeassistant/components/hydrawise/config_flow.py b/homeassistant/components/hydrawise/config_flow.py index ea3b9e58926..cfaaefcd03a 100644 --- a/homeassistant/components/hydrawise/config_flow.py +++ b/homeassistant/components/hydrawise/config_flow.py @@ -9,23 +9,23 @@ from aiohttp import ClientError from pydrawise import legacy import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_API_KEY from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN -from homeassistant.data_entry_flow import AbortFlow, FlowResult, FlowResultType +from homeassistant.data_entry_flow import AbortFlow, FlowResultType from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from .const import DOMAIN, LOGGER -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class HydrawiseConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Hydrawise.""" VERSION = 1 async def _create_entry( - self, api_key: str, *, on_failure: Callable[[str], FlowResult] - ) -> FlowResult: + self, api_key: str, *, on_failure: Callable[[str], ConfigFlowResult] + ) -> ConfigFlowResult: """Create the config entry.""" api = legacy.LegacyHydrawiseAsync(api_key) try: @@ -42,7 +42,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_create_entry(title="Hydrawise", data={CONF_API_KEY: api_key}) - def _import_issue(self, error_type: str) -> FlowResult: + def _import_issue(self, error_type: str) -> ConfigFlowResult: """Create an issue about a YAML import failure.""" async_create_issue( self.hass, @@ -78,14 +78,14 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial setup.""" if user_input is not None: api_key = user_input[CONF_API_KEY] return await self._create_entry(api_key, on_failure=self._show_form) return self._show_form() - def _show_form(self, error_type: str | None = None) -> FlowResult: + def _show_form(self, error_type: str | None = None) -> ConfigFlowResult: errors = {} if error_type is not None: errors["base"] = error_type @@ -95,7 +95,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_import(self, import_data: dict[str, Any]) -> FlowResult: + async def async_step_import(self, import_data: dict[str, Any]) -> ConfigFlowResult: """Import data from YAML.""" try: result = await self._create_entry( diff --git a/homeassistant/components/hydrawise/entity.py b/homeassistant/components/hydrawise/entity.py index 887de6ba648..2ae893887e6 100644 --- a/homeassistant/components/hydrawise/entity.py +++ b/homeassistant/components/hydrawise/entity.py @@ -1,4 +1,5 @@ """Base classes for Hydrawise entities.""" + from __future__ import annotations from pydrawise.schema import Controller, Zone diff --git a/homeassistant/components/hydrawise/icons.json b/homeassistant/components/hydrawise/icons.json new file mode 100644 index 00000000000..717b5c48357 --- /dev/null +++ b/homeassistant/components/hydrawise/icons.json @@ -0,0 +1,9 @@ +{ + "entity": { + "sensor": { + "watering_time": { + "default": "mdi:water-pump" + } + } + } +} diff --git a/homeassistant/components/hydrawise/sensor.py b/homeassistant/components/hydrawise/sensor.py index f8490ad00e1..eedeb4a07bc 100644 --- a/homeassistant/components/hydrawise/sensor.py +++ b/homeassistant/components/hydrawise/sensor.py @@ -1,4 +1,5 @@ """Support for Hydrawise sprinkler sensors.""" + from __future__ import annotations from datetime import datetime @@ -33,7 +34,6 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="watering_time", translation_key="watering_time", - icon="mdi:water-pump", native_unit_of_measurement=UnitOfTime.MINUTES, ), ) diff --git a/homeassistant/components/hydrawise/switch.py b/homeassistant/components/hydrawise/switch.py index 8a92a56975a..49106a5938a 100644 --- a/homeassistant/components/hydrawise/switch.py +++ b/homeassistant/components/hydrawise/switch.py @@ -1,4 +1,5 @@ """Support for Hydrawise cloud switches.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/hyperion/__init__.py b/homeassistant/components/hyperion/__init__.py index 42d9770656b..94137b5dd3f 100644 --- a/homeassistant/components/hyperion/__init__.py +++ b/homeassistant/components/hyperion/__init__.py @@ -1,4 +1,5 @@ """The Hyperion component.""" + from __future__ import annotations import asyncio @@ -32,7 +33,7 @@ from .const import ( SIGNAL_INSTANCE_REMOVE, ) -PLATFORMS = [Platform.CAMERA, Platform.LIGHT, Platform.SWITCH] +PLATFORMS = [Platform.CAMERA, Platform.LIGHT, Platform.SENSOR, Platform.SWITCH] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/hyperion/config_flow.py b/homeassistant/components/hyperion/config_flow.py index 52c4647f11c..e29caa27ef7 100644 --- a/homeassistant/components/hyperion/config_flow.py +++ b/homeassistant/components/hyperion/config_flow.py @@ -1,4 +1,5 @@ """Hyperion config flow.""" + from __future__ import annotations import asyncio @@ -16,6 +17,7 @@ from homeassistant.config_entries import ( SOURCE_REAUTH, ConfigEntry, ConfigFlow, + ConfigFlowResult, OptionsFlow, ) from homeassistant.const import ( @@ -27,7 +29,6 @@ from homeassistant.const import ( CONF_TOKEN, ) from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult import homeassistant.helpers.config_validation as cv from . import create_hyperion_client @@ -129,7 +130,7 @@ class HyperionConfigFlow(ConfigFlow, domain=DOMAIN): async def _advance_to_auth_step_if_necessary( self, hyperion_client: client.HyperionClient - ) -> FlowResult: + ) -> ConfigFlowResult: """Determine if auth is required.""" auth_resp = await hyperion_client.async_is_auth_required() @@ -141,7 +142,9 @@ class HyperionConfigFlow(ConfigFlow, domain=DOMAIN): return await self.async_step_auth() return await self.async_step_confirm() - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Handle a reauthentication flow.""" self._data = dict(entry_data) async with self._create_client(raw_connection=True) as hyperion_client: @@ -149,7 +152,9 @@ class HyperionConfigFlow(ConfigFlow, domain=DOMAIN): return self.async_abort(reason="cannot_connect") return await self._advance_to_auth_step_if_necessary(hyperion_client) - async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: + async def async_step_ssdp( + self, discovery_info: ssdp.SsdpServiceInfo + ) -> ConfigFlowResult: """Handle a flow initiated by SSDP.""" # Sample data provided by SSDP: { # 'ssdp_location': 'http://192.168.0.1:8090/description.xml', @@ -221,7 +226,7 @@ class HyperionConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None, - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initiated by the user.""" errors = {} if user_input: @@ -292,7 +297,7 @@ class HyperionConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_auth( self, user_input: dict[str, Any] | None = None, - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the auth step of a flow.""" errors = {} if user_input: @@ -321,7 +326,7 @@ class HyperionConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_create_token( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Send a request for a new token.""" if user_input is None: self._auth_id = client.generate_random_auth_id() @@ -347,7 +352,7 @@ class HyperionConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_create_token_external( self, auth_resp: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle completion of the request for a new token.""" if auth_resp is not None and client.ResponseOK(auth_resp): token = auth_resp.get(const.KEY_INFO, {}).get(const.KEY_TOKEN) @@ -360,7 +365,7 @@ class HyperionConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_create_token_success( self, _: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Create an entry after successful token creation.""" # Clean-up the request task. await self._cancel_request_token_task() @@ -376,7 +381,7 @@ class HyperionConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_create_token_fail( self, _: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Show an error on the auth form.""" # Clean-up the request task. await self._cancel_request_token_task() @@ -384,7 +389,7 @@ class HyperionConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Get final confirmation before entry creation.""" if user_input is None and self._require_confirm: return self.async_show_form( @@ -444,7 +449,7 @@ class HyperionOptionsFlow(OptionsFlow): async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Manage the options.""" effects = {} diff --git a/homeassistant/components/hyperion/const.py b/homeassistant/components/hyperion/const.py index 77e16df4d72..3d44dd35e08 100644 --- a/homeassistant/components/hyperion/const.py +++ b/homeassistant/components/hyperion/const.py @@ -28,3 +28,6 @@ SIGNAL_ENTITY_REMOVE = f"{DOMAIN}_entity_remove_signal.{{}}" TYPE_HYPERION_CAMERA = "hyperion_camera" TYPE_HYPERION_LIGHT = "hyperion_light" TYPE_HYPERION_COMPONENT_SWITCH_BASE = "hyperion_component_switch" + +TYPE_HYPERION_SENSOR_BASE = "hyperion_sensor" +TYPE_HYPERION_SENSOR_VISIBLE_PRIORITY = "visible_priority" diff --git a/homeassistant/components/hyperion/light.py b/homeassistant/components/hyperion/light.py index 824d83591ef..5fa129ce7ad 100644 --- a/homeassistant/components/hyperion/light.py +++ b/homeassistant/components/hyperion/light.py @@ -1,4 +1,5 @@ """Support for Hyperion-NG remotes.""" + from __future__ import annotations from collections.abc import Callable, Mapping, Sequence diff --git a/homeassistant/components/hyperion/sensor.py b/homeassistant/components/hyperion/sensor.py new file mode 100644 index 00000000000..f537c282686 --- /dev/null +++ b/homeassistant/components/hyperion/sensor.py @@ -0,0 +1,211 @@ +"""Sensor platform for Hyperion.""" + +from __future__ import annotations + +import functools +from typing import Any + +from hyperion import client +from hyperion.const import ( + KEY_COMPONENTID, + KEY_ORIGIN, + KEY_OWNER, + KEY_PRIORITIES, + KEY_PRIORITY, + KEY_RGB, + KEY_UPDATE, + KEY_VALUE, + KEY_VISIBLE, +) + +from homeassistant.components.sensor import SensorEntity, SensorEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import ( + get_hyperion_device_id, + get_hyperion_unique_id, + listen_for_instance_updates, +) +from .const import ( + CONF_INSTANCE_CLIENTS, + DOMAIN, + HYPERION_MANUFACTURER_NAME, + HYPERION_MODEL_NAME, + SIGNAL_ENTITY_REMOVE, + TYPE_HYPERION_SENSOR_BASE, + TYPE_HYPERION_SENSOR_VISIBLE_PRIORITY, +) + +SENSORS = [TYPE_HYPERION_SENSOR_VISIBLE_PRIORITY] +PRIORITY_SENSOR_DESCRIPTION = SensorEntityDescription( + key="visible_priority", + translation_key="visible_priority", + icon="mdi:lava-lamp", +) + + +def _sensor_unique_id(server_id: str, instance_num: int, suffix: str) -> str: + """Calculate a sensor's unique_id.""" + return get_hyperion_unique_id( + server_id, + instance_num, + f"{TYPE_HYPERION_SENSOR_BASE}_{suffix}", + ) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up a Hyperion platform from config entry.""" + entry_data = hass.data[DOMAIN][config_entry.entry_id] + server_id = config_entry.unique_id + + @callback + def instance_add(instance_num: int, instance_name: str) -> None: + """Add entities for a new Hyperion instance.""" + assert server_id + sensors = [ + HyperionVisiblePrioritySensor( + server_id, + instance_num, + instance_name, + entry_data[CONF_INSTANCE_CLIENTS][instance_num], + PRIORITY_SENSOR_DESCRIPTION, + ) + ] + + async_add_entities(sensors) + + @callback + def instance_remove(instance_num: int) -> None: + """Remove entities for an old Hyperion instance.""" + assert server_id + + for sensor in SENSORS: + async_dispatcher_send( + hass, + SIGNAL_ENTITY_REMOVE.format( + _sensor_unique_id(server_id, instance_num, sensor), + ), + ) + + listen_for_instance_updates(hass, config_entry, instance_add, instance_remove) + + +class HyperionSensor(SensorEntity): + """Sensor class.""" + + _attr_has_entity_name = True + _attr_should_poll = False + + def __init__( + self, + server_id: str, + instance_num: int, + instance_name: str, + hyperion_client: client.HyperionClient, + entity_description: SensorEntityDescription, + ) -> None: + """Initialize the sensor.""" + self.entity_description = entity_description + self._client = hyperion_client + self._attr_native_value = None + self._client_callbacks: dict[str, Any] = {} + + device_id = get_hyperion_device_id(server_id, instance_num) + + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, device_id)}, + manufacturer=HYPERION_MANUFACTURER_NAME, + model=HYPERION_MODEL_NAME, + name=instance_name, + configuration_url=self._client.remote_url, + ) + + @property + def available(self) -> bool: + """Return server availability.""" + return bool(self._client.has_loaded_state) + + async def async_added_to_hass(self) -> None: + """Register callbacks when entity added to hass.""" + self.async_on_remove( + async_dispatcher_connect( + self.hass, + SIGNAL_ENTITY_REMOVE.format(self._attr_unique_id), + functools.partial(self.async_remove, force_remove=True), + ) + ) + + self._client.add_callbacks(self._client_callbacks) + + async def async_will_remove_from_hass(self) -> None: + """Cleanup prior to hass removal.""" + self._client.remove_callbacks(self._client_callbacks) + + +class HyperionVisiblePrioritySensor(HyperionSensor): + """Class that displays the visible priority of a Hyperion instance.""" + + def __init__( + self, + server_id: str, + instance_num: int, + instance_name: str, + hyperion_client: client.HyperionClient, + entity_description: SensorEntityDescription, + ) -> None: + """Initialize the sensor.""" + + super().__init__( + server_id, instance_num, instance_name, hyperion_client, entity_description + ) + + self._attr_unique_id = _sensor_unique_id( + server_id, instance_num, TYPE_HYPERION_SENSOR_VISIBLE_PRIORITY + ) + + self._client_callbacks = { + f"{KEY_PRIORITIES}-{KEY_UPDATE}": self._update_priorities + } + + @callback + def _update_priorities(self, _: dict[str, Any] | None = None) -> None: + """Update Hyperion priorities.""" + state_value = None + attrs = {} + + for priority in self._client.priorities or []: + if not (KEY_VISIBLE in priority and priority[KEY_VISIBLE] is True): + continue + + if priority[KEY_COMPONENTID] == "COLOR": + state_value = priority[KEY_VALUE][KEY_RGB] + else: + state_value = priority[KEY_OWNER] + + attrs = { + "component_id": priority[KEY_COMPONENTID], + "origin": priority[KEY_ORIGIN], + "priority": priority[KEY_PRIORITY], + "owner": priority[KEY_OWNER], + } + + if priority[KEY_COMPONENTID] == "COLOR": + attrs["color"] = priority[KEY_VALUE] + else: + attrs["color"] = None + + self._attr_native_value = state_value + self._attr_extra_state_attributes = attrs + + self.async_write_ha_state() diff --git a/homeassistant/components/hyperion/strings.json b/homeassistant/components/hyperion/strings.json index 8d7e3751c4c..79c226b71eb 100644 --- a/homeassistant/components/hyperion/strings.json +++ b/homeassistant/components/hyperion/strings.json @@ -80,6 +80,11 @@ "usb_capture": { "name": "Component USB capture" } + }, + "sensor": { + "visible_priority": { + "name": "Visible priority" + } } } } diff --git a/homeassistant/components/hyperion/switch.py b/homeassistant/components/hyperion/switch.py index eb7b260a370..94cbf2aba29 100644 --- a/homeassistant/components/hyperion/switch.py +++ b/homeassistant/components/hyperion/switch.py @@ -1,4 +1,5 @@ """Switch platform for Hyperion.""" + from __future__ import annotations import functools @@ -99,18 +100,16 @@ async def async_setup_entry( def instance_add(instance_num: int, instance_name: str) -> None: """Add entities for a new Hyperion instance.""" assert server_id - switches = [] - for component in COMPONENT_SWITCHES: - switches.append( - HyperionComponentSwitch( - server_id, - instance_num, - instance_name, - component, - entry_data[CONF_INSTANCE_CLIENTS][instance_num], - ), + async_add_entities( + HyperionComponentSwitch( + server_id, + instance_num, + instance_name, + component, + entry_data[CONF_INSTANCE_CLIENTS][instance_num], ) - async_add_entities(switches) + for component in COMPONENT_SWITCHES + ) @callback def instance_remove(instance_num: int) -> None: diff --git a/homeassistant/components/ialarm/__init__.py b/homeassistant/components/ialarm/__init__.py index ff54c02a2d4..6ebd219f6ec 100644 --- a/homeassistant/components/ialarm/__init__.py +++ b/homeassistant/components/ialarm/__init__.py @@ -1,4 +1,5 @@ """iAlarm integration.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/ialarm/alarm_control_panel.py b/homeassistant/components/ialarm/alarm_control_panel.py index b09e31f5312..44e676fc32e 100644 --- a/homeassistant/components/ialarm/alarm_control_panel.py +++ b/homeassistant/components/ialarm/alarm_control_panel.py @@ -1,4 +1,5 @@ """Interfaces with iAlarm control panels.""" + from __future__ import annotations from homeassistant.components.alarm_control_panel import ( diff --git a/homeassistant/components/ialarm/config_flow.py b/homeassistant/components/ialarm/config_flow.py index a894a6f4e11..6aef66922b4 100644 --- a/homeassistant/components/ialarm/config_flow.py +++ b/homeassistant/components/ialarm/config_flow.py @@ -1,11 +1,13 @@ """Config flow for Antifurto365 iAlarm integration.""" + import logging from pyialarm import IAlarm import voluptuous as vol -from homeassistant import config_entries, core +from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.core import HomeAssistant from .const import DEFAULT_PORT, DOMAIN @@ -19,12 +21,12 @@ DATA_SCHEMA = vol.Schema( ) -async def _get_device_mac(hass: core.HomeAssistant, host, port): +async def _get_device_mac(hass: HomeAssistant, host, port): ialarm = IAlarm(host, port) return await hass.async_add_executor_job(ialarm.get_mac) -class IAlarmConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class IAlarmConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Antifurto365 iAlarm.""" VERSION = 1 diff --git a/homeassistant/components/ialarm/const.py b/homeassistant/components/ialarm/const.py index c6eaf0ec979..d1561cc86d5 100644 --- a/homeassistant/components/ialarm/const.py +++ b/homeassistant/components/ialarm/const.py @@ -1,4 +1,5 @@ """Constants for the iAlarm integration.""" + from pyialarm import IAlarm from homeassistant.const import ( diff --git a/homeassistant/components/iammeter/const.py b/homeassistant/components/iammeter/const.py index c2d122c9e32..0336007ef3e 100644 --- a/homeassistant/components/iammeter/const.py +++ b/homeassistant/components/iammeter/const.py @@ -1,4 +1,5 @@ """Constants for the Iammeter integration.""" + from __future__ import annotations DOMAIN = "iammeter" diff --git a/homeassistant/components/iammeter/sensor.py b/homeassistant/components/iammeter/sensor.py index 3537737f122..a3922b06980 100644 --- a/homeassistant/components/iammeter/sensor.py +++ b/homeassistant/components/iammeter/sensor.py @@ -1,4 +1,5 @@ """Support for iammeter via local API.""" + from __future__ import annotations from asyncio import timeout @@ -74,8 +75,8 @@ def _migrate_to_new_unique_id( phase_list = ["A", "B", "C", "NET"] id_phase_range = 1 if model == DEVICE_3080 else 4 id_name_range = 5 if model == DEVICE_3080 else 7 - for row in range(0, id_phase_range): - for idx in range(0, id_name_range): + for row in range(id_phase_range): + for idx in range(id_name_range): old_unique_id = f"{serial_number}-{row}-{idx}" new_unique_id = ( f"{serial_number}_{name_list[idx]}" diff --git a/homeassistant/components/iaqualink/__init__.py b/homeassistant/components/iaqualink/__init__.py index 49eaa2b24a5..33697dfb2cc 100644 --- a/homeassistant/components/iaqualink/__init__.py +++ b/homeassistant/components/iaqualink/__init__.py @@ -1,4 +1,5 @@ """Component to embed Aqualink devices.""" + from __future__ import annotations from collections.abc import Awaitable, Callable, Coroutine @@ -55,9 +56,7 @@ PLATFORMS = [ ] -async def async_setup_entry( # noqa: C901 - hass: HomeAssistant, entry: ConfigEntry -) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Aqualink from a config entry.""" username = entry.data[CONF_USERNAME] password = entry.data[CONF_PASSWORD] diff --git a/homeassistant/components/iaqualink/binary_sensor.py b/homeassistant/components/iaqualink/binary_sensor.py index 149261f97fc..06dbcf18e4a 100644 --- a/homeassistant/components/iaqualink/binary_sensor.py +++ b/homeassistant/components/iaqualink/binary_sensor.py @@ -1,4 +1,5 @@ """Support for Aqualink temperature sensors.""" + from __future__ import annotations from iaqualink.device import AqualinkBinarySensor @@ -24,10 +25,10 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up discovered binary sensors.""" - devs = [] - for dev in hass.data[AQUALINK_DOMAIN][DOMAIN]: - devs.append(HassAqualinkBinarySensor(dev)) - async_add_entities(devs, True) + async_add_entities( + (HassAqualinkBinarySensor(dev) for dev in hass.data[AQUALINK_DOMAIN][DOMAIN]), + True, + ) class HassAqualinkBinarySensor(AqualinkEntity, BinarySensorEntity): diff --git a/homeassistant/components/iaqualink/climate.py b/homeassistant/components/iaqualink/climate.py index 5a81ad3d681..29576e9fc10 100644 --- a/homeassistant/components/iaqualink/climate.py +++ b/homeassistant/components/iaqualink/climate.py @@ -1,4 +1,5 @@ """Support for Aqualink Thermostats.""" + from __future__ import annotations import logging @@ -32,10 +33,13 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up discovered switches.""" - devs = [] - for dev in hass.data[AQUALINK_DOMAIN][CLIMATE_DOMAIN]: - devs.append(HassAqualinkThermostat(dev)) - async_add_entities(devs, True) + async_add_entities( + ( + HassAqualinkThermostat(dev) + for dev in hass.data[AQUALINK_DOMAIN][CLIMATE_DOMAIN] + ), + True, + ) class HassAqualinkThermostat(AqualinkEntity, ClimateEntity): diff --git a/homeassistant/components/iaqualink/config_flow.py b/homeassistant/components/iaqualink/config_flow.py index 0dfc60f2fee..3605c328903 100644 --- a/homeassistant/components/iaqualink/config_flow.py +++ b/homeassistant/components/iaqualink/config_flow.py @@ -1,4 +1,5 @@ """Config flow to configure zone component.""" + from __future__ import annotations from typing import Any @@ -11,21 +12,20 @@ from iaqualink.exception import ( ) import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN -class AqualinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class AqualinkFlowHandler(ConfigFlow, domain=DOMAIN): """Aqualink config flow.""" VERSION = 1 async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow start.""" # Supporting a single account. entries = self._async_current_entries() diff --git a/homeassistant/components/iaqualink/const.py b/homeassistant/components/iaqualink/const.py index ef939e3fc52..1db4b5a6f16 100644 --- a/homeassistant/components/iaqualink/const.py +++ b/homeassistant/components/iaqualink/const.py @@ -1,4 +1,5 @@ """Constants for the iaqualink component.""" + from datetime import timedelta DOMAIN = "iaqualink" diff --git a/homeassistant/components/iaqualink/light.py b/homeassistant/components/iaqualink/light.py index 3a166ba593d..bce4f2c9855 100644 --- a/homeassistant/components/iaqualink/light.py +++ b/homeassistant/components/iaqualink/light.py @@ -1,4 +1,5 @@ """Support for Aqualink pool lights.""" + from __future__ import annotations from typing import Any @@ -30,10 +31,9 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up discovered lights.""" - devs = [] - for dev in hass.data[AQUALINK_DOMAIN][DOMAIN]: - devs.append(HassAqualinkLight(dev)) - async_add_entities(devs, True) + async_add_entities( + (HassAqualinkLight(dev) for dev in hass.data[AQUALINK_DOMAIN][DOMAIN]), True + ) class HassAqualinkLight(AqualinkEntity, LightEntity): diff --git a/homeassistant/components/iaqualink/sensor.py b/homeassistant/components/iaqualink/sensor.py index 15e8fc5836d..8e3983e9c91 100644 --- a/homeassistant/components/iaqualink/sensor.py +++ b/homeassistant/components/iaqualink/sensor.py @@ -1,4 +1,5 @@ """Support for Aqualink temperature sensors.""" + from __future__ import annotations from iaqualink.device import AqualinkSensor @@ -21,10 +22,9 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up discovered sensors.""" - devs = [] - for dev in hass.data[AQUALINK_DOMAIN][DOMAIN]: - devs.append(HassAqualinkSensor(dev)) - async_add_entities(devs, True) + async_add_entities( + (HassAqualinkSensor(dev) for dev in hass.data[AQUALINK_DOMAIN][DOMAIN]), True + ) class HassAqualinkSensor(AqualinkEntity, SensorEntity): diff --git a/homeassistant/components/iaqualink/switch.py b/homeassistant/components/iaqualink/switch.py index 590fcd61419..e681879855b 100644 --- a/homeassistant/components/iaqualink/switch.py +++ b/homeassistant/components/iaqualink/switch.py @@ -1,4 +1,5 @@ """Support for Aqualink pool feature switches.""" + from __future__ import annotations from typing import Any @@ -23,10 +24,9 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up discovered switches.""" - devs = [] - for dev in hass.data[AQUALINK_DOMAIN][DOMAIN]: - devs.append(HassAqualinkSwitch(dev)) - async_add_entities(devs, True) + async_add_entities( + (HassAqualinkSwitch(dev) for dev in hass.data[AQUALINK_DOMAIN][DOMAIN]), True + ) class HassAqualinkSwitch(AqualinkEntity, SwitchEntity): @@ -40,7 +40,7 @@ class HassAqualinkSwitch(AqualinkEntity, SwitchEntity): self._attr_icon = "mdi:robot-vacuum" elif name == "Waterfall" or name.endswith("Dscnt"): self._attr_icon = "mdi:fountain" - elif name.endswith("Pump") or name.endswith("Blower"): + elif name.endswith(("Pump", "Blower")): self._attr_icon = "mdi:fan" if name.endswith("Heater"): self._attr_icon = "mdi:radiator" diff --git a/homeassistant/components/iaqualink/utils.py b/homeassistant/components/iaqualink/utils.py index 87bc863a7f8..62d2d4d2e93 100644 --- a/homeassistant/components/iaqualink/utils.py +++ b/homeassistant/components/iaqualink/utils.py @@ -1,4 +1,5 @@ """Utility functions for Aqualink devices.""" + from __future__ import annotations from collections.abc import Awaitable diff --git a/homeassistant/components/ibeacon/__init__.py b/homeassistant/components/ibeacon/__init__.py index 02a19ef6332..0e89ee3bbcd 100644 --- a/homeassistant/components/ibeacon/__init__.py +++ b/homeassistant/components/ibeacon/__init__.py @@ -1,4 +1,5 @@ """The iBeacon tracker integration.""" + from __future__ import annotations from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/ibeacon/config_flow.py b/homeassistant/components/ibeacon/config_flow.py index c7d6c358a29..ccedaa675b6 100644 --- a/homeassistant/components/ibeacon/config_flow.py +++ b/homeassistant/components/ibeacon/config_flow.py @@ -1,4 +1,5 @@ """Config flow for iBeacon Tracker integration.""" + from __future__ import annotations from typing import Any @@ -6,23 +7,27 @@ from uuid import UUID import voluptuous as vol -from homeassistant import config_entries from homeassistant.components import bluetooth +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult import homeassistant.helpers.config_validation as cv from .const import CONF_ALLOW_NAMELESS_UUIDS, DOMAIN -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class IBeaconConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for iBeacon Tracker.""" VERSION = 1 async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") @@ -38,20 +43,20 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback def async_get_options_flow( - config_entry: config_entries.ConfigEntry, + config_entry: ConfigEntry, ) -> OptionsFlow: """Get the options flow for this handler.""" - return OptionsFlow(config_entry) + return IBeaconOptionsFlow(config_entry) -class OptionsFlow(config_entries.OptionsFlow): +class IBeaconOptionsFlow(OptionsFlow): """Handle options.""" - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + def __init__(self, config_entry: ConfigEntry) -> None: """Initialize options flow.""" self.config_entry = config_entry - async def async_step_init(self, user_input: dict | None = None) -> FlowResult: + async def async_step_init(self, user_input: dict | None = None) -> ConfigFlowResult: """Manage the options.""" errors = {} diff --git a/homeassistant/components/ibeacon/coordinator.py b/homeassistant/components/ibeacon/coordinator.py index b23ea77e013..27181e80ed8 100644 --- a/homeassistant/components/ibeacon/coordinator.py +++ b/homeassistant/components/ibeacon/coordinator.py @@ -1,4 +1,5 @@ """Tracking for iBeacon devices.""" + from __future__ import annotations from datetime import datetime diff --git a/homeassistant/components/ibeacon/device_tracker.py b/homeassistant/components/ibeacon/device_tracker.py index 8e194ac27b1..8d24d7f0aa9 100644 --- a/homeassistant/components/ibeacon/device_tracker.py +++ b/homeassistant/components/ibeacon/device_tracker.py @@ -1,4 +1,5 @@ """Support for tracking iBeacon devices.""" + from __future__ import annotations from ibeacon_ble import iBeaconAdvertisement @@ -49,6 +50,7 @@ class IBeaconTrackerEntity(IBeaconEntity, BaseTrackerEntity): """An iBeacon Tracker entity.""" _attr_name = None + _attr_translation_key = "device_tracker" def __init__( self, @@ -74,11 +76,6 @@ class IBeaconTrackerEntity(IBeaconEntity, BaseTrackerEntity): """Return tracker source type.""" return SourceType.BLUETOOTH_LE - @property - def icon(self) -> str: - """Return device icon.""" - return "mdi:bluetooth-connect" if self._active else "mdi:bluetooth-off" - @callback def _async_seen( self, diff --git a/homeassistant/components/ibeacon/entity.py b/homeassistant/components/ibeacon/entity.py index b25c82037e1..d4f969ff94a 100644 --- a/homeassistant/components/ibeacon/entity.py +++ b/homeassistant/components/ibeacon/entity.py @@ -1,4 +1,5 @@ """Support for iBeacon device sensors.""" + from __future__ import annotations from abc import abstractmethod diff --git a/homeassistant/components/ibeacon/icons.json b/homeassistant/components/ibeacon/icons.json new file mode 100644 index 00000000000..5f9b89e6568 --- /dev/null +++ b/homeassistant/components/ibeacon/icons.json @@ -0,0 +1,17 @@ +{ + "entity": { + "device_tracker": { + "device_tracker": { + "default": "mdi:bluetooth-off", + "state": { + "home": "mdi:bluetooth-connect" + } + } + }, + "sensor": { + "estimated_distance": { + "default": "mdi:signal-distance-variant" + } + } + } +} diff --git a/homeassistant/components/ibeacon/sensor.py b/homeassistant/components/ibeacon/sensor.py index 3ce145fc3b9..3b7ba3d5dbf 100644 --- a/homeassistant/components/ibeacon/sensor.py +++ b/homeassistant/components/ibeacon/sensor.py @@ -1,4 +1,5 @@ """Support for iBeacon device sensors.""" + from __future__ import annotations from collections.abc import Callable @@ -23,18 +24,13 @@ from .coordinator import IBeaconCoordinator from .entity import IBeaconEntity -@dataclass(frozen=True) -class IBeaconRequiredKeysMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class IBeaconSensorEntityDescription(SensorEntityDescription): + """Describes iBeacon sensor entity.""" value_fn: Callable[[iBeaconAdvertisement], str | int | None] -@dataclass(frozen=True) -class IBeaconSensorEntityDescription(SensorEntityDescription, IBeaconRequiredKeysMixin): - """Describes iBeacon sensor entity.""" - - SENSOR_DESCRIPTIONS = ( IBeaconSensorEntityDescription( key="rssi", @@ -56,7 +52,6 @@ SENSOR_DESCRIPTIONS = ( IBeaconSensorEntityDescription( key="estimated_distance", translation_key="estimated_distance", - icon="mdi:signal-distance-variant", native_unit_of_measurement=UnitOfLength.METERS, value_fn=lambda ibeacon_advertisement: ibeacon_advertisement.distance, state_class=SensorStateClass.MEASUREMENT, diff --git a/homeassistant/components/icloud/__init__.py b/homeassistant/components/icloud/__init__.py index 010de4ec4ba..431a1abd2e1 100644 --- a/homeassistant/components/icloud/__init__.py +++ b/homeassistant/components/icloud/__init__.py @@ -1,4 +1,5 @@ """The iCloud component.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/icloud/account.py b/homeassistant/components/icloud/account.py index 257bf536006..015726fbf73 100644 --- a/homeassistant/components/icloud/account.py +++ b/homeassistant/components/icloud/account.py @@ -1,4 +1,5 @@ """iCloud account.""" + from __future__ import annotations from datetime import timedelta @@ -149,9 +150,9 @@ class IcloudAccount: self._family_members_fullname = {} if user_info.get("membersInfo") is not None: for prs_id, member in user_info["membersInfo"].items(): - self._family_members_fullname[ - prs_id - ] = f"{member['firstName']} {member['lastName']}" + self._family_members_fullname[prs_id] = ( + f"{member['firstName']} {member['lastName']}" + ) self._devices = {} self.update_devices() @@ -318,11 +319,12 @@ class IcloudAccount: def get_devices_with_name(self, name: str) -> list[Any]: """Get devices by name.""" - result = [] name_slug = slugify(name.replace(" ", "", 99)) - for device in self.devices.values(): - if slugify(device.name.replace(" ", "", 99)) == name_slug: - result.append(device) + result = [ + device + for device in self.devices.values() + if slugify(device.name.replace(" ", "", 99)) == name_slug + ] if not result: raise ValueError(f"No device with name {name}") return result diff --git a/homeassistant/components/icloud/config_flow.py b/homeassistant/components/icloud/config_flow.py index 4be5487f755..36fe880ec79 100644 --- a/homeassistant/components/icloud/config_flow.py +++ b/homeassistant/components/icloud/config_flow.py @@ -1,4 +1,5 @@ """Config flow to configure the iCloud integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -15,9 +16,8 @@ from pyicloud.exceptions import ( ) import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.storage import Store from .const import ( @@ -38,7 +38,7 @@ CONF_VERIFICATION_CODE = "verification_code" _LOGGER = logging.getLogger(__name__) -class IcloudFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class IcloudFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a iCloud config flow.""" VERSION = 1 @@ -141,7 +141,7 @@ class IcloudFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): getattr, self.api, "devices" ) if not devices: - raise PyiCloudNoDevicesException() + raise PyiCloudNoDevicesException except (PyiCloudServiceNotActivatedException, PyiCloudNoDevicesException): _LOGGER.error("No device found in the iCloud account: %s", self._username) self.api = None @@ -178,7 +178,9 @@ class IcloudFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return await self._validate_and_create_entry(user_input, "user") - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Initialise re-authentication.""" # Store existing entry data so it can be used later and set unique ID # so existing config entry can be updated @@ -189,7 +191,7 @@ class IcloudFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Update password for a config entry that can't authenticate.""" if user_input is None: return self._show_setup_form(step_id="reauth_confirm") diff --git a/homeassistant/components/icloud/const.py b/homeassistant/components/icloud/const.py index 231f2cc1d0a..b7ea2691ca4 100644 --- a/homeassistant/components/icloud/const.py +++ b/homeassistant/components/icloud/const.py @@ -1,4 +1,5 @@ """iCloud component constants.""" + from homeassistant.const import Platform DOMAIN = "icloud" diff --git a/homeassistant/components/icloud/device_tracker.py b/homeassistant/components/icloud/device_tracker.py index 8513b47be2a..48070a7f153 100644 --- a/homeassistant/components/icloud/device_tracker.py +++ b/homeassistant/components/icloud/device_tracker.py @@ -1,4 +1,5 @@ """Support for tracking for iCloud devices.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/icloud/icons.json b/homeassistant/components/icloud/icons.json new file mode 100644 index 00000000000..4ed856aabc1 --- /dev/null +++ b/homeassistant/components/icloud/icons.json @@ -0,0 +1,8 @@ +{ + "services": { + "update": "mdi:update", + "play_sound": "mdi:speaker-wireless", + "display_message": "mdi:message-alert", + "lost_device": "mdi:devices" + } +} diff --git a/homeassistant/components/icloud/sensor.py b/homeassistant/components/icloud/sensor.py index 320c3f9f240..53c9765f6cc 100644 --- a/homeassistant/components/icloud/sensor.py +++ b/homeassistant/components/icloud/sensor.py @@ -1,4 +1,5 @@ """Support for iCloud sensors.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/idasen_desk/__init__.py b/homeassistant/components/idasen_desk/__init__.py index c3e5f3de429..75ef70fcb50 100644 --- a/homeassistant/components/idasen_desk/__init__.py +++ b/homeassistant/components/idasen_desk/__init__.py @@ -1,4 +1,5 @@ """The IKEA Idasen Desk integration.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/idasen_desk/button.py b/homeassistant/components/idasen_desk/button.py index d11738c6bcd..0de3125576d 100644 --- a/homeassistant/components/idasen_desk/button.py +++ b/homeassistant/components/idasen_desk/button.py @@ -1,14 +1,11 @@ """Representation of Idasen Desk buttons.""" + from collections.abc import Callable, Coroutine from dataclasses import dataclass import logging from typing import Any, Final -from homeassistant.components.button import ( - ButtonDeviceClass, - ButtonEntity, - ButtonEntityDescription, -) +from homeassistant.components.button import ButtonEntity, ButtonEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant @@ -21,36 +18,25 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -@dataclass(frozen=True) -class IdasenDeskButtonDescriptionMixin: - """Mixin to describe a IdasenDesk button entity.""" +@dataclass(frozen=True, kw_only=True) +class IdasenDeskButtonDescription(ButtonEntityDescription): + """Class to describe a IdasenDesk button entity.""" press_action: Callable[ [IdasenDeskCoordinator], Callable[[], Coroutine[Any, Any, Any]] ] -@dataclass(frozen=True) -class IdasenDeskButtonDescription( - ButtonEntityDescription, IdasenDeskButtonDescriptionMixin -): - """Class to describe a IdasenDesk button entity.""" - - BUTTONS: Final = [ IdasenDeskButtonDescription( key="connect", - name="Connect", - icon="mdi:bluetooth-connect", - device_class=ButtonDeviceClass.RESTART, + translation_key="connect", entity_category=EntityCategory.CONFIG, press_action=lambda coordinator: coordinator.async_connect, ), IdasenDeskButtonDescription( key="disconnect", - name="Disconnect", - icon="mdi:bluetooth-off", - device_class=ButtonDeviceClass.RESTART, + translation_key="disconnect", entity_category=EntityCategory.CONFIG, press_action=lambda coordinator: coordinator.async_disconnect, ), @@ -86,7 +72,7 @@ class IdasenDeskButton(ButtonEntity): """Initialize the IdasenDesk button entity.""" self.entity_description = description - self._attr_unique_id = f"{self.entity_description.key}-{address}" + self._attr_unique_id = f"{description.key}-{address}" self._attr_device_info = device_info self._address = address self._coordinator = coordinator diff --git a/homeassistant/components/idasen_desk/config_flow.py b/homeassistant/components/idasen_desk/config_flow.py index 80282ce0271..8d6af14f043 100644 --- a/homeassistant/components/idasen_desk/config_flow.py +++ b/homeassistant/components/idasen_desk/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Idasen Desk integration.""" + from __future__ import annotations import logging @@ -10,20 +11,19 @@ from idasen_ha import Desk from idasen_ha.errors import AuthFailedError import voluptuous as vol -from homeassistant import config_entries from homeassistant.components.bluetooth import ( BluetoothServiceInfoBleak, async_discovered_service_info, ) +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_ADDRESS -from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN, EXPECTED_SERVICE_UUID _LOGGER = logging.getLogger(__name__) -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class IdasenDeskConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Idasen Desk integration.""" VERSION = 1 @@ -35,7 +35,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_bluetooth( self, discovery_info: BluetoothServiceInfoBleak - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the bluetooth discovery step.""" await self.async_set_unique_id(discovery_info.address) self._abort_if_unique_id_configured() @@ -49,7 +49,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the user step to pick discovered device.""" errors: dict[str, str] = {} diff --git a/homeassistant/components/idasen_desk/const.py b/homeassistant/components/idasen_desk/const.py index 0d37d77307b..26d673dde37 100644 --- a/homeassistant/components/idasen_desk/const.py +++ b/homeassistant/components/idasen_desk/const.py @@ -1,6 +1,5 @@ """Constants for the Idasen Desk integration.""" - DOMAIN = "idasen_desk" EXPECTED_SERVICE_UUID = "99fa0001-338a-1024-8a49-009c0215f78a" diff --git a/homeassistant/components/idasen_desk/cover.py b/homeassistant/components/idasen_desk/cover.py index 1daebe52420..f5591eff0d8 100644 --- a/homeassistant/components/idasen_desk/cover.py +++ b/homeassistant/components/idasen_desk/cover.py @@ -1,4 +1,5 @@ """Idasen Desk integration cover platform.""" + from __future__ import annotations from typing import Any @@ -12,7 +13,6 @@ from homeassistant.components.cover import ( CoverEntityFeature, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_NAME from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.device_registry import DeviceInfo @@ -39,13 +39,15 @@ class IdasenDeskCover(CoordinatorEntity[IdasenDeskCoordinator], CoverEntity): """Representation of Idasen Desk device.""" _attr_device_class = CoverDeviceClass.DAMPER - _attr_icon = "mdi:desk" _attr_supported_features = ( CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE | CoverEntityFeature.STOP | CoverEntityFeature.SET_POSITION ) + _attr_has_entity_name = True + _attr_name = None + _attr_translation_key = "desk" def __init__( self, @@ -56,7 +58,6 @@ class IdasenDeskCover(CoordinatorEntity[IdasenDeskCoordinator], CoverEntity): """Initialize an Idasen Desk cover.""" super().__init__(coordinator) self._desk = coordinator.desk - self._attr_name = device_info[ATTR_NAME] self._attr_unique_id = address self._attr_device_info = device_info diff --git a/homeassistant/components/idasen_desk/icons.json b/homeassistant/components/idasen_desk/icons.json new file mode 100644 index 00000000000..8feca3de25d --- /dev/null +++ b/homeassistant/components/idasen_desk/icons.json @@ -0,0 +1,22 @@ +{ + "entity": { + "button": { + "connect": { + "default": "mdi:bluetooth-connect" + }, + "disconnect": { + "default": "mdi:bluetooth-off" + } + }, + "cover": { + "desk": { + "default": "mdi:desk" + } + }, + "sensor": { + "height": { + "default": "mdi:arrow-up-down" + } + } + } +} diff --git a/homeassistant/components/idasen_desk/sensor.py b/homeassistant/components/idasen_desk/sensor.py index 0fb3523a461..12a3b2ed4d9 100644 --- a/homeassistant/components/idasen_desk/sensor.py +++ b/homeassistant/components/idasen_desk/sensor.py @@ -1,4 +1,5 @@ """Representation of Idasen Desk sensors.""" + from __future__ import annotations from collections.abc import Callable @@ -22,26 +23,17 @@ from . import DeskData, IdasenDeskCoordinator from .const import DOMAIN -@dataclass(frozen=True) -class IdasenDeskSensorDescriptionMixin: - """Required values for IdasenDesk sensors.""" +@dataclass(frozen=True, kw_only=True) +class IdasenDeskSensorDescription(SensorEntityDescription): + """Class describing IdasenDesk sensor entities.""" value_fn: Callable[[IdasenDeskCoordinator], float | None] -@dataclass(frozen=True) -class IdasenDeskSensorDescription( - SensorEntityDescription, - IdasenDeskSensorDescriptionMixin, -): - """Class describing IdasenDesk sensor entities.""" - - SENSORS = ( IdasenDeskSensorDescription( key="height", translation_key="height", - icon="mdi:arrow-up-down", native_unit_of_measurement=UnitOfLength.METERS, device_class=SensorDeviceClass.DISTANCE, state_class=SensorStateClass.MEASUREMENT, diff --git a/homeassistant/components/idasen_desk/strings.json b/homeassistant/components/idasen_desk/strings.json index 446ef93e542..70e08976925 100644 --- a/homeassistant/components/idasen_desk/strings.json +++ b/homeassistant/components/idasen_desk/strings.json @@ -21,6 +21,14 @@ } }, "entity": { + "button": { + "connect": { + "name": "Connect" + }, + "disconnect": { + "name": "Disconnect" + } + }, "sensor": { "height": { "name": "Height" diff --git a/homeassistant/components/idteck_prox/__init__.py b/homeassistant/components/idteck_prox/__init__.py index 4de6af70995..7b92499a197 100644 --- a/homeassistant/components/idteck_prox/__init__.py +++ b/homeassistant/components/idteck_prox/__init__.py @@ -1,4 +1,5 @@ """Component for interfacing RFK101 proximity card readers.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/ifttt/__init__.py b/homeassistant/components/ifttt/__init__.py index 736efcb03a7..e3db68e2302 100644 --- a/homeassistant/components/ifttt/__init__.py +++ b/homeassistant/components/ifttt/__init__.py @@ -1,4 +1,5 @@ """Support to trigger Maker IFTTT recipes.""" + from __future__ import annotations from http import HTTPStatus diff --git a/homeassistant/components/ifttt/alarm_control_panel.py b/homeassistant/components/ifttt/alarm_control_panel.py index b568693303a..81ed9320bcb 100644 --- a/homeassistant/components/ifttt/alarm_control_panel.py +++ b/homeassistant/components/ifttt/alarm_control_panel.py @@ -1,4 +1,5 @@ """Support for alarm control panels that can be controlled through IFTTT.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/ifttt/config_flow.py b/homeassistant/components/ifttt/config_flow.py index dc28f6bbaa2..8c0de38a7f8 100644 --- a/homeassistant/components/ifttt/config_flow.py +++ b/homeassistant/components/ifttt/config_flow.py @@ -1,4 +1,5 @@ """Config flow for IFTTT.""" + from homeassistant.helpers import config_entry_flow from .const import DOMAIN diff --git a/homeassistant/components/ifttt/icons.json b/homeassistant/components/ifttt/icons.json new file mode 100644 index 00000000000..b943478a70b --- /dev/null +++ b/homeassistant/components/ifttt/icons.json @@ -0,0 +1,6 @@ +{ + "services": { + "push_alarm_state": "mdi:security", + "trigger": "mdi:play" + } +} diff --git a/homeassistant/components/iglo/light.py b/homeassistant/components/iglo/light.py index 5de24625ea3..1cd303b8856 100644 --- a/homeassistant/components/iglo/light.py +++ b/homeassistant/components/iglo/light.py @@ -1,4 +1,5 @@ """Support for lights under the iGlo brand.""" + from __future__ import annotations import math diff --git a/homeassistant/components/ign_sismologia/geo_location.py b/homeassistant/components/ign_sismologia/geo_location.py index 794da41ea12..af7fab5b79b 100644 --- a/homeassistant/components/ign_sismologia/geo_location.py +++ b/homeassistant/components/ign_sismologia/geo_location.py @@ -1,4 +1,5 @@ """Support for IGN Sismologia (Earthquakes) Feeds.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/ihc/__init__.py b/homeassistant/components/ihc/__init__.py index 34b65c5b791..d443ac335db 100644 --- a/homeassistant/components/ihc/__init__.py +++ b/homeassistant/components/ihc/__init__.py @@ -1,4 +1,5 @@ """Support for IHC devices.""" + import logging from ihcsdk.ihccontroller import IHCController diff --git a/homeassistant/components/ihc/auto_setup.py b/homeassistant/components/ihc/auto_setup.py index d23c3e65a41..2d6e59131cd 100644 --- a/homeassistant/components/ihc/auto_setup.py +++ b/homeassistant/components/ihc/auto_setup.py @@ -1,4 +1,5 @@ """Handle auto setup of IHC products from the ihc project file.""" + import logging import os.path diff --git a/homeassistant/components/ihc/binary_sensor.py b/homeassistant/components/ihc/binary_sensor.py index badf0f4e92f..ed273878cb4 100644 --- a/homeassistant/components/ihc/binary_sensor.py +++ b/homeassistant/components/ihc/binary_sensor.py @@ -1,4 +1,5 @@ """Support for IHC binary sensors.""" + from __future__ import annotations from ihcsdk.ihccontroller import IHCController diff --git a/homeassistant/components/ihc/const.py b/homeassistant/components/ihc/const.py index c86e77870c8..8a07bd4fec4 100644 --- a/homeassistant/components/ihc/const.py +++ b/homeassistant/components/ihc/const.py @@ -1,4 +1,5 @@ """IHC component constants.""" + from homeassistant.const import Platform ATTR_IHC_ID = "ihc_id" diff --git a/homeassistant/components/ihc/icons.json b/homeassistant/components/ihc/icons.json new file mode 100644 index 00000000000..73aab5f80d8 --- /dev/null +++ b/homeassistant/components/ihc/icons.json @@ -0,0 +1,8 @@ +{ + "services": { + "set_runtime_value_bool": "mdi:toggle-switch", + "set_runtime_value_int": "mdi:numeric", + "set_runtime_value_float": "mdi:numeric", + "pulse": "mdi:pulse" + } +} diff --git a/homeassistant/components/ihc/ihcdevice.py b/homeassistant/components/ihc/ihcdevice.py index 30c84da40f8..07ff71b812a 100644 --- a/homeassistant/components/ihc/ihcdevice.py +++ b/homeassistant/components/ihc/ihcdevice.py @@ -1,4 +1,5 @@ """Implementation of a base class for all IHC devices.""" + import logging from ihcsdk.ihccontroller import IHCController diff --git a/homeassistant/components/ihc/light.py b/homeassistant/components/ihc/light.py index b469cb54aee..98e373daff4 100644 --- a/homeassistant/components/ihc/light.py +++ b/homeassistant/components/ihc/light.py @@ -1,4 +1,5 @@ """Support for IHC lights.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/ihc/manual_setup.py b/homeassistant/components/ihc/manual_setup.py index b4775f9193b..c453494e263 100644 --- a/homeassistant/components/ihc/manual_setup.py +++ b/homeassistant/components/ihc/manual_setup.py @@ -1,4 +1,5 @@ """Handle manual setup of ihc resources as entities in Home Assistant.""" + import logging import voluptuous as vol diff --git a/homeassistant/components/ihc/sensor.py b/homeassistant/components/ihc/sensor.py index c1210a358d6..1ca41ed2666 100644 --- a/homeassistant/components/ihc/sensor.py +++ b/homeassistant/components/ihc/sensor.py @@ -1,4 +1,5 @@ """Support for IHC sensors.""" + from __future__ import annotations from ihcsdk.ihccontroller import IHCController diff --git a/homeassistant/components/ihc/service_functions.py b/homeassistant/components/ihc/service_functions.py index 3d7008ee38b..cfd91f0960c 100644 --- a/homeassistant/components/ihc/service_functions.py +++ b/homeassistant/components/ihc/service_functions.py @@ -1,4 +1,5 @@ """Support for IHC devices.""" + import voluptuous as vol from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/ihc/switch.py b/homeassistant/components/ihc/switch.py index d4593dad570..f41f17bc998 100644 --- a/homeassistant/components/ihc/switch.py +++ b/homeassistant/components/ihc/switch.py @@ -1,4 +1,5 @@ """Support for IHC switches.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/image/__init__.py b/homeassistant/components/image/__init__.py index 164b7048da8..47767a004cb 100644 --- a/homeassistant/components/image/__init__.py +++ b/homeassistant/components/image/__init__.py @@ -1,4 +1,5 @@ """The image integration.""" + from __future__ import annotations import asyncio @@ -13,7 +14,7 @@ from typing import TYPE_CHECKING, Final, final from aiohttp import hdrs, web import httpx -from homeassistant.components.http import KEY_AUTHENTICATED, HomeAssistantView +from homeassistant.components.http import KEY_AUTHENTICATED, KEY_HASS, HomeAssistantView from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONTENT_TYPE_MULTIPART, EVENT_HOMEASSISTANT_STOP from homeassistant.core import Event, HomeAssistant, callback @@ -30,9 +31,9 @@ from homeassistant.helpers.event import ( async_track_time_interval, ) from homeassistant.helpers.httpx_client import get_async_client -from homeassistant.helpers.typing import UNDEFINED, ConfigType, EventType, UndefinedType +from homeassistant.helpers.typing import UNDEFINED, ConfigType, UndefinedType -from .const import DOMAIN, IMAGE_TIMEOUT # noqa: F401 +from .const import DOMAIN, IMAGE_TIMEOUT if TYPE_CHECKING: from functools import cached_property @@ -189,7 +190,7 @@ class ImageEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): def image(self) -> bytes | None: """Return bytes of image.""" - raise NotImplementedError() + raise NotImplementedError async def _fetch_url(self, url: str) -> httpx.Response | None: """Fetch a URL.""" @@ -277,7 +278,7 @@ class ImageView(HomeAssistantView): async def get(self, request: web.Request, entity_id: str) -> web.StreamResponse: """Start a GET request.""" if (image_entity := self.component.get_entity(entity_id)) is None: - raise web.HTTPNotFound() + raise web.HTTPNotFound authenticated = ( request[KEY_AUTHENTICATED] @@ -288,9 +289,9 @@ class ImageView(HomeAssistantView): # Attempt with invalid bearer token, raise unauthorized # so ban middleware can handle it. if hdrs.AUTHORIZATION in request.headers: - raise web.HTTPUnauthorized() + raise web.HTTPUnauthorized # Invalid sigAuth or image entity access token - raise web.HTTPForbidden() + raise web.HTTPForbidden return await self.handle(request, image_entity) @@ -301,7 +302,7 @@ class ImageView(HomeAssistantView): try: image = await _async_get_image(image_entity, IMAGE_TIMEOUT) except (HomeAssistantError, ValueError) as ex: - raise web.HTTPInternalServerError() from ex + raise web.HTTPInternalServerError from ex return web.Response(body=image.content, content_type=image.content_type) @@ -335,29 +336,46 @@ async def async_get_still_stream( # given the low frequency of image updates, it is acceptable. frame.extend(frame) await response.write(frame) - # Drain to ensure that the latest frame is available to the client - await response.drain() return True event = asyncio.Event() + timed_out = False - async def image_state_update(_event: EventType[EventStateChangedData]) -> None: + @callback + def _async_image_state_update(_event: Event[EventStateChangedData]) -> None: """Write image to stream.""" event.set() - hass: HomeAssistant = request.app["hass"] + @callback + def _async_timeout_reached() -> None: + """Handle timeout.""" + nonlocal timed_out + timed_out = True + event.set() + + hass = request.app[KEY_HASS] + loop = hass.loop remove = async_track_state_change_event( hass, image_entity.entity_id, - image_state_update, + _async_image_state_update, ) + timeout_handle = None try: while True: if not await _write_frame(): return response + # Ensure that an image is sent at least every 55 seconds + # Otherwise some devices go blank + timeout_handle = loop.call_later(55, _async_timeout_reached) await event.wait() event.clear() + if not timed_out: + timeout_handle.cancel() + timed_out = False finally: + if timeout_handle: + timeout_handle.cancel() remove() diff --git a/homeassistant/components/image/const.py b/homeassistant/components/image/const.py index d262bb460f7..d96f13b4951 100644 --- a/homeassistant/components/image/const.py +++ b/homeassistant/components/image/const.py @@ -1,4 +1,5 @@ """Constants for the image integration.""" + from typing import Final DOMAIN: Final = "image" diff --git a/homeassistant/components/image/media_source.py b/homeassistant/components/image/media_source.py index 39f00b587c0..e7f240aef5c 100644 --- a/homeassistant/components/image/media_source.py +++ b/homeassistant/components/image/media_source.py @@ -1,4 +1,5 @@ -"""Expose iamges as media sources.""" +"""Expose images as media sources.""" + from __future__ import annotations from typing import cast diff --git a/homeassistant/components/image_processing/__init__.py b/homeassistant/components/image_processing/__init__.py index 178d40d1139..2c1d0f9304c 100644 --- a/homeassistant/components/image_processing/__init__.py +++ b/homeassistant/components/image_processing/__init__.py @@ -1,4 +1,5 @@ """Provides functionality to interact with image processing services.""" + from __future__ import annotations import asyncio @@ -9,7 +10,7 @@ from typing import Any, Final, TypedDict, final import voluptuous as vol -from homeassistant.components.camera import Image +from homeassistant.components.camera import async_get_image from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_NAME, @@ -164,7 +165,7 @@ class ImageProcessingEntity(Entity): def process_image(self, image: bytes) -> None: """Process image.""" - raise NotImplementedError() + raise NotImplementedError async def async_process_image(self, image: bytes) -> None: """Process image.""" @@ -175,13 +176,16 @@ class ImageProcessingEntity(Entity): This method is a coroutine. """ - camera = self.hass.components.camera + if self.camera_entity is None: + _LOGGER.error( + "No camera entity id was set by the image processing entity", + ) + return try: - image: Image = await camera.async_get_image( - self.camera_entity, timeout=self.timeout + image = await async_get_image( + self.hass, self.camera_entity, timeout=self.timeout ) - except HomeAssistantError as err: _LOGGER.error("Error on receive image from entity: %s", err) return diff --git a/homeassistant/components/image_processing/icons.json b/homeassistant/components/image_processing/icons.json new file mode 100644 index 00000000000..b19d29c186d --- /dev/null +++ b/homeassistant/components/image_processing/icons.json @@ -0,0 +1,5 @@ +{ + "services": { + "scan": "mdi:qrcode-scan" + } +} diff --git a/homeassistant/components/image_upload/__init__.py b/homeassistant/components/image_upload/__init__.py index 6faa690b4cb..19763e65fa5 100644 --- a/homeassistant/components/image_upload/__init__.py +++ b/homeassistant/components/image_upload/__init__.py @@ -1,4 +1,5 @@ """The Image Upload integration.""" + from __future__ import annotations import asyncio @@ -13,8 +14,8 @@ from aiohttp.web_request import FileField from PIL import Image, ImageOps, UnidentifiedImageError import voluptuous as vol +from homeassistant.components.http import KEY_HASS, HomeAssistantView from homeassistant.components.http.static import CACHE_HEADERS -from homeassistant.components.http.view import HomeAssistantView from homeassistant.const import CONF_ID from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import collection, config_validation as cv @@ -162,7 +163,7 @@ class ImageUploadView(HomeAssistantView): request._client_max_size = MAX_SIZE # pylint: disable=protected-access data = await request.post() - item = await request.app["hass"].data[DOMAIN].async_create_item(data) + item = await request.app[KEY_HASS].data[DOMAIN].async_create_item(data) return self.json(item) @@ -198,9 +199,9 @@ class ImageServeView(HomeAssistantView): image_info = self.image_collection.data.get(image_id) if image_info is None: - raise web.HTTPNotFound() + raise web.HTTPNotFound - hass = request.app["hass"] + hass = request.app[KEY_HASS] target_file = self.image_folder / image_id / f"{width}x{height}" if not target_file.is_file(): diff --git a/homeassistant/components/imap/__init__.py b/homeassistant/components/imap/__init__.py index 924408c30b9..7504446f3fb 100644 --- a/homeassistant/components/imap/__init__.py +++ b/homeassistant/components/imap/__init__.py @@ -1,4 +1,5 @@ """The imap integration.""" + from __future__ import annotations from aioimaplib import IMAP4_SSL, AioImapException @@ -62,8 +63,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: 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): - coordinator: ImapPushDataUpdateCoordinator | ImapPollingDataUpdateCoordinator = hass.data[ - DOMAIN - ].pop(entry.entry_id) + coordinator: ( + ImapPushDataUpdateCoordinator | ImapPollingDataUpdateCoordinator + ) = hass.data[DOMAIN].pop(entry.entry_id) await coordinator.shutdown() return unload_ok diff --git a/homeassistant/components/imap/config_flow.py b/homeassistant/components/imap/config_flow.py index 15b52ce6333..414d5830bae 100644 --- a/homeassistant/components/imap/config_flow.py +++ b/homeassistant/components/imap/config_flow.py @@ -1,4 +1,5 @@ """Config flow for imap integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -8,10 +9,15 @@ from typing import Any from aioimaplib import AioImapException import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlowWithConfigEntry, +) from homeassistant.const import CONF_PASSWORD, CONF_PORT, CONF_USERNAME, CONF_VERIFY_SSL from homeassistant.core import HomeAssistant, callback -from homeassistant.data_entry_flow import AbortFlow, FlowResult +from homeassistant.data_entry_flow import AbortFlow from homeassistant.helpers import config_validation as cv from homeassistant.helpers.selector import ( BooleanSelector, @@ -119,15 +125,15 @@ async def validate_input( return errors -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class IMAPConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for imap.""" VERSION = 1 - _reauth_entry: config_entries.ConfigEntry | None + _reauth_entry: ConfigEntry | None async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" schema = CONFIG_SCHEMA @@ -152,7 +158,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): schema = self.add_suggested_values_to_schema(schema, user_input) return self.async_show_form(step_id="user", data_schema=schema, errors=errors) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Perform reauth upon an API authentication error.""" self._reauth_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] @@ -161,7 +169,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_reauth_confirm( self, user_input: dict[str, str] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm reauth dialog.""" errors = {} assert self._reauth_entry @@ -188,18 +196,18 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback def async_get_options_flow( - config_entry: config_entries.ConfigEntry, + config_entry: ConfigEntry, ) -> OptionsFlow: """Get the options flow for this handler.""" return OptionsFlow(config_entry) -class OptionsFlow(config_entries.OptionsFlowWithConfigEntry): +class OptionsFlow(OptionsFlowWithConfigEntry): """Option flow handler.""" async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Manage the options.""" errors: dict[str, str] | None = None entry_data: dict[str, Any] = dict(self._config_entry.data) diff --git a/homeassistant/components/imap/coordinator.py b/homeassistant/components/imap/coordinator.py index f0c9099863a..78b52e06db3 100644 --- a/homeassistant/components/imap/coordinator.py +++ b/homeassistant/components/imap/coordinator.py @@ -1,4 +1,5 @@ """Coordinator for imag integration.""" + from __future__ import annotations import asyncio @@ -405,7 +406,7 @@ class ImapPollingDataUpdateCoordinator(ImapDataUpdateCoordinator): ) as ex: await self._cleanup() self.async_set_update_error(ex) - raise UpdateFailed() from ex + raise UpdateFailed from ex except InvalidFolder as ex: _LOGGER.warning("Selected mailbox folder is invalid") await self._cleanup() @@ -422,7 +423,7 @@ class ImapPollingDataUpdateCoordinator(ImapDataUpdateCoordinator): ) self.config_entry.async_start_reauth(self.hass) self.async_set_update_error(ex) - raise ConfigEntryAuthFailed() from ex + raise ConfigEntryAuthFailed from ex class ImapPushDataUpdateCoordinator(ImapDataUpdateCoordinator): diff --git a/homeassistant/components/imap/diagnostics.py b/homeassistant/components/imap/diagnostics.py index c7d5151ba49..467f19d6338 100644 --- a/homeassistant/components/imap/diagnostics.py +++ b/homeassistant/components/imap/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for IMAP.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/imap/sensor.py b/homeassistant/components/imap/sensor.py index 07e77b31470..0a9070d7a5e 100644 --- a/homeassistant/components/imap/sensor.py +++ b/homeassistant/components/imap/sensor.py @@ -1,4 +1,5 @@ """IMAP sensor support.""" + from __future__ import annotations from homeassistant.components.sensor import ( diff --git a/homeassistant/components/improv_ble/config_flow.py b/homeassistant/components/improv_ble/config_flow.py index 6f940f91946..a1a2d6b1b65 100644 --- a/homeassistant/components/improv_ble/config_flow.py +++ b/homeassistant/components/improv_ble/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Improv via BLE integration.""" + from __future__ import annotations import asyncio @@ -19,11 +20,11 @@ from improv_ble_client import ( ) import voluptuous as vol -from homeassistant import config_entries from homeassistant.components import bluetooth +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_ADDRESS from homeassistant.core import callback -from homeassistant.data_entry_flow import AbortFlow, FlowResult +from homeassistant.data_entry_flow import AbortFlow from .const import DOMAIN @@ -47,7 +48,7 @@ class Credentials: ssid: str -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class ImprovBLEConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Improv via BLE.""" VERSION = 1 @@ -55,9 +56,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): _authorize_task: asyncio.Task | None = None _can_identify: bool | None = None _credentials: Credentials | None = None - _provision_result: FlowResult | None = None + _provision_result: ConfigFlowResult | None = None _provision_task: asyncio.Task | None = None - _reauth_entry: config_entries.ConfigEntry | None = None + _reauth_entry: ConfigEntry | None = None _remove_bluetooth_callback: Callable[[], None] | None = None _unsub: Callable[[], None] | None = None @@ -71,7 +72,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the user step to pick discovered device.""" errors: dict[str, str] = {} @@ -158,7 +159,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_bluetooth( self, discovery_info: bluetooth.BluetoothServiceInfoBleak - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the Bluetooth discovery step.""" self._discovery_info = discovery_info @@ -181,7 +182,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_bluetooth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle bluetooth confirm step.""" # mypy is not aware that we can't get here without having these set already assert self._discovery_info is not None @@ -198,7 +199,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_start_improv( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Start improv flow. If the device supports identification, show a menu, if it does not, @@ -220,7 +221,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_main_menu() return await self.async_step_provision() - async def async_step_main_menu(self, _: None = None) -> FlowResult: + async def async_step_main_menu(self, _: None = None) -> ConfigFlowResult: """Show the main menu.""" return self.async_show_menu( step_id="main_menu", @@ -232,7 +233,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_identify( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle identify step.""" # mypy is not aware that we can't get here without having these set already assert self._device is not None @@ -247,7 +248,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_provision( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle provision step.""" # mypy is not aware that we can't get here without having these set already assert self._device is not None @@ -272,7 +273,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_do_provision( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Execute provisioning.""" async def _do_provision() -> None: @@ -339,7 +340,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_provision_done( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Show the result of the provision step.""" # mypy is not aware that we can't get here without having these set already assert self._provision_result is not None @@ -350,7 +351,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_authorize( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle authorize step.""" # mypy is not aware that we can't get here without having these set already assert self._device is not None diff --git a/homeassistant/components/incomfort/__init__.py b/homeassistant/components/incomfort/__init__.py index 7245aff3c35..3311bda23ee 100644 --- a/homeassistant/components/incomfort/__init__.py +++ b/homeassistant/components/incomfort/__init__.py @@ -1,4 +1,5 @@ """Support for an Intergas boiler via an InComfort/Intouch Lan2RF gateway.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/incomfort/binary_sensor.py b/homeassistant/components/incomfort/binary_sensor.py index 52edfc6ef02..59096038d6c 100644 --- a/homeassistant/components/incomfort/binary_sensor.py +++ b/homeassistant/components/incomfort/binary_sensor.py @@ -1,4 +1,5 @@ """Support for an Intergas heater via an InComfort/InTouch Lan2RF gateway.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/incomfort/climate.py b/homeassistant/components/incomfort/climate.py index 0dba00ff416..cc61e179aa4 100644 --- a/homeassistant/components/incomfort/climate.py +++ b/homeassistant/components/incomfort/climate.py @@ -1,4 +1,5 @@ """Support for an Intergas boiler via an InComfort/InTouch Lan2RF gateway.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/incomfort/sensor.py b/homeassistant/components/incomfort/sensor.py index 535d8b61653..9106afacb26 100644 --- a/homeassistant/components/incomfort/sensor.py +++ b/homeassistant/components/incomfort/sensor.py @@ -1,4 +1,5 @@ """Support for an Intergas heater via an InComfort/InTouch Lan2RF gateway.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/homeassistant/components/incomfort/water_heater.py b/homeassistant/components/incomfort/water_heater.py index 367af73810b..2cd7c84a666 100644 --- a/homeassistant/components/incomfort/water_heater.py +++ b/homeassistant/components/incomfort/water_heater.py @@ -1,4 +1,5 @@ """Support for an Intergas boiler via an InComfort/Intouch Lan2RF gateway.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/influxdb/__init__.py b/homeassistant/components/influxdb/__init__.py index 24c80dc1d54..b1c0cc53d61 100644 --- a/homeassistant/components/influxdb/__init__.py +++ b/homeassistant/components/influxdb/__init__.py @@ -1,4 +1,5 @@ """Support for sending data to an Influx database.""" + from __future__ import annotations from collections.abc import Callable @@ -512,7 +513,9 @@ class InfluxThread(threading.Thread): def __init__(self, hass, influx, event_to_json, max_tries): """Initialize the listener.""" threading.Thread.__init__(self, name=DOMAIN) - self.queue = queue.Queue() + self.queue: queue.SimpleQueue[threading.Event | tuple[float, Event] | None] = ( + queue.SimpleQueue() + ) self.influx = influx self.event_to_json = event_to_json self.max_tries = max_tries @@ -548,16 +551,17 @@ class InfluxThread(threading.Thread): if item is None: self.shutdown = True - else: + elif type(item) is tuple: timestamp, event = item age = time.monotonic() - timestamp if age < queue_seconds: - event_json = self.event_to_json(event) - if event_json: + if event_json := self.event_to_json(event): json.append(event_json) else: dropped += 1 + elif isinstance(item, threading.Event): + item.set() if dropped: _LOGGER.warning(CATCHING_UP_MESSAGE, dropped) @@ -590,12 +594,15 @@ class InfluxThread(threading.Thread): def run(self): """Process incoming events.""" while not self.shutdown: - count, json = self.get_events_json() + _, json = self.get_events_json() if json: self.write_to_influxdb(json) - for _ in range(count): - self.queue.task_done() def block_till_done(self): - """Block till all events processed.""" - self.queue.join() + """Block till all events processed. + + Currently only used for testing. + """ + event = threading.Event() + self.queue.put(event) + event.wait() diff --git a/homeassistant/components/influxdb/const.py b/homeassistant/components/influxdb/const.py index 5ffd70fe992..cab9d1e4c41 100644 --- a/homeassistant/components/influxdb/const.py +++ b/homeassistant/components/influxdb/const.py @@ -1,4 +1,5 @@ """Constants for InfluxDB integration.""" + from datetime import timedelta import re diff --git a/homeassistant/components/influxdb/manifest.json b/homeassistant/components/influxdb/manifest.json index 46cd5ecb6ca..ad3f282eff7 100644 --- a/homeassistant/components/influxdb/manifest.json +++ b/homeassistant/components/influxdb/manifest.json @@ -3,7 +3,6 @@ "name": "InfluxDB", "codeowners": ["@mdegat01"], "documentation": "https://www.home-assistant.io/integrations/influxdb", - "import_executor": true, "iot_class": "local_push", "loggers": ["influxdb", "influxdb_client"], "requirements": ["influxdb==5.3.1", "influxdb-client==1.24.0"] diff --git a/homeassistant/components/influxdb/sensor.py b/homeassistant/components/influxdb/sensor.py index a46ec581207..03b6acb204c 100644 --- a/homeassistant/components/influxdb/sensor.py +++ b/homeassistant/components/influxdb/sensor.py @@ -1,4 +1,5 @@ """InfluxDB component which allows you to get data from an Influx database.""" + from __future__ import annotations import datetime @@ -165,7 +166,7 @@ def setup_platform( influx = get_influx_connection(config, test_read=True) except ConnectionError as exc: _LOGGER.error(exc) - raise PlatformNotReady() from exc + raise PlatformNotReady from exc entities = [] if CONF_QUERIES_FLUX in config: diff --git a/homeassistant/components/inkbird/__init__.py b/homeassistant/components/inkbird/__init__.py index 5ed0d6fb367..c715c64599a 100644 --- a/homeassistant/components/inkbird/__init__.py +++ b/homeassistant/components/inkbird/__init__.py @@ -1,4 +1,5 @@ """The INKBIRD Bluetooth integration.""" + from __future__ import annotations import logging @@ -25,14 +26,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: address = entry.unique_id assert address is not None data = INKBIRDBluetoothDeviceData() - coordinator = hass.data.setdefault(DOMAIN, {})[ - entry.entry_id - ] = PassiveBluetoothProcessorCoordinator( - hass, - _LOGGER, - address=address, - mode=BluetoothScanningMode.ACTIVE, - update_method=data.update, + coordinator = hass.data.setdefault(DOMAIN, {})[entry.entry_id] = ( + PassiveBluetoothProcessorCoordinator( + hass, + _LOGGER, + address=address, + mode=BluetoothScanningMode.ACTIVE, + update_method=data.update, + ) ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload( diff --git a/homeassistant/components/inkbird/config_flow.py b/homeassistant/components/inkbird/config_flow.py index c63ad7e09d8..0d4e404c9b5 100644 --- a/homeassistant/components/inkbird/config_flow.py +++ b/homeassistant/components/inkbird/config_flow.py @@ -1,4 +1,5 @@ """Config flow for inkbird ble integration.""" + from __future__ import annotations from typing import Any @@ -10,9 +11,8 @@ from homeassistant.components.bluetooth import ( BluetoothServiceInfoBleak, async_discovered_service_info, ) -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_ADDRESS -from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN @@ -30,7 +30,7 @@ class INKBIRDConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_bluetooth( self, discovery_info: BluetoothServiceInfoBleak - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the bluetooth discovery step.""" await self.async_set_unique_id(discovery_info.address) self._abort_if_unique_id_configured() @@ -43,7 +43,7 @@ class INKBIRDConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_bluetooth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm discovery.""" assert self._discovered_device is not None device = self._discovered_device @@ -62,7 +62,7 @@ class INKBIRDConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the user step to pick discovered device.""" if user_input is not None: address = user_input[CONF_ADDRESS] diff --git a/homeassistant/components/inkbird/sensor.py b/homeassistant/components/inkbird/sensor.py index f93f2024289..a7bd71005ab 100644 --- a/homeassistant/components/inkbird/sensor.py +++ b/homeassistant/components/inkbird/sensor.py @@ -1,4 +1,5 @@ """Support for inkbird ble sensors.""" + from __future__ import annotations from inkbird_ble import DeviceClass, DeviceKey, SensorUpdate, Units diff --git a/homeassistant/components/input_boolean/__init__.py b/homeassistant/components/input_boolean/__init__.py index 613e8829aa1..91c7de96fe0 100644 --- a/homeassistant/components/input_boolean/__init__.py +++ b/homeassistant/components/input_boolean/__init__.py @@ -1,4 +1,5 @@ """Support to keep track of user controlled booleans for within automation.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/input_boolean/reproduce_state.py b/homeassistant/components/input_boolean/reproduce_state.py index 6c68489e4cb..7af28f8a92a 100644 --- a/homeassistant/components/input_boolean/reproduce_state.py +++ b/homeassistant/components/input_boolean/reproduce_state.py @@ -1,4 +1,5 @@ """Reproduce an input boolean state.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/input_button/__init__.py b/homeassistant/components/input_button/__init__.py index 3318354392c..d6c3644487b 100644 --- a/homeassistant/components/input_button/__init__.py +++ b/homeassistant/components/input_button/__init__.py @@ -1,4 +1,5 @@ """Support to keep track of user controlled buttons which can be used in automations.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/input_button/icons.json b/homeassistant/components/input_button/icons.json new file mode 100644 index 00000000000..226b8ede110 --- /dev/null +++ b/homeassistant/components/input_button/icons.json @@ -0,0 +1,6 @@ +{ + "services": { + "press": "mdi:gesture-tap-button", + "reload": "mdi:reload" + } +} diff --git a/homeassistant/components/input_datetime/__init__.py b/homeassistant/components/input_datetime/__init__.py index 73a4df12d03..c64ef506670 100644 --- a/homeassistant/components/input_datetime/__init__.py +++ b/homeassistant/components/input_datetime/__init__.py @@ -1,4 +1,5 @@ """Support to select a date and/or a time.""" + from __future__ import annotations import datetime as py_datetime diff --git a/homeassistant/components/input_datetime/icons.json b/homeassistant/components/input_datetime/icons.json new file mode 100644 index 00000000000..de899023cf2 --- /dev/null +++ b/homeassistant/components/input_datetime/icons.json @@ -0,0 +1,6 @@ +{ + "services": { + "set_datetime": "mdi:calendar-clock", + "reload": "mdi:reload" + } +} diff --git a/homeassistant/components/input_datetime/reproduce_state.py b/homeassistant/components/input_datetime/reproduce_state.py index 6f36be0850a..ccadbccd8d4 100644 --- a/homeassistant/components/input_datetime/reproduce_state.py +++ b/homeassistant/components/input_datetime/reproduce_state.py @@ -1,4 +1,5 @@ """Reproduce an Input datetime state.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/input_number/__init__.py b/homeassistant/components/input_number/__init__.py index 4a74201be15..e37f530b8af 100644 --- a/homeassistant/components/input_number/__init__.py +++ b/homeassistant/components/input_number/__init__.py @@ -1,4 +1,5 @@ """Support to set a numeric value from a slider or text box.""" + from __future__ import annotations from contextlib import suppress diff --git a/homeassistant/components/input_number/icons.json b/homeassistant/components/input_number/icons.json new file mode 100644 index 00000000000..d1423838491 --- /dev/null +++ b/homeassistant/components/input_number/icons.json @@ -0,0 +1,8 @@ +{ + "services": { + "decrement": "mdi:minus", + "increment": "mdi:plus", + "set_value": "mdi:numeric", + "reload": "mdi:reload" + } +} diff --git a/homeassistant/components/input_number/reproduce_state.py b/homeassistant/components/input_number/reproduce_state.py index 368f68b5178..c2f9cfc4702 100644 --- a/homeassistant/components/input_number/reproduce_state.py +++ b/homeassistant/components/input_number/reproduce_state.py @@ -1,4 +1,5 @@ """Reproduce an Input number state.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/input_select/__init__.py b/homeassistant/components/input_select/__init__.py index 4a384e0c17a..dcb75a92d20 100644 --- a/homeassistant/components/input_select/__init__.py +++ b/homeassistant/components/input_select/__init__.py @@ -1,4 +1,5 @@ """Support to select an option from a list.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/input_select/icons.json b/homeassistant/components/input_select/icons.json new file mode 100644 index 00000000000..03b477ddb36 --- /dev/null +++ b/homeassistant/components/input_select/icons.json @@ -0,0 +1,11 @@ +{ + "services": { + "select_next": "mdi:skip-next", + "select_option": "mdi:check", + "select_previous": "mdi:skip-previous", + "select_first": "mdi:skip-backward", + "select_last": "mdi:skip-forward", + "set_options": "mdi:cog", + "reload": "mdi:reload" + } +} diff --git a/homeassistant/components/input_select/reproduce_state.py b/homeassistant/components/input_select/reproduce_state.py index 8ba16391d7e..b451f8c3f09 100644 --- a/homeassistant/components/input_select/reproduce_state.py +++ b/homeassistant/components/input_select/reproduce_state.py @@ -1,4 +1,5 @@ """Reproduce an Input select state.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/input_text/__init__.py b/homeassistant/components/input_text/__init__.py index 01bd76d1241..52788066ba2 100644 --- a/homeassistant/components/input_text/__init__.py +++ b/homeassistant/components/input_text/__init__.py @@ -1,4 +1,5 @@ """Support to enter a value into a text box.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/input_text/icons.json b/homeassistant/components/input_text/icons.json new file mode 100644 index 00000000000..0190e4ffba2 --- /dev/null +++ b/homeassistant/components/input_text/icons.json @@ -0,0 +1,6 @@ +{ + "services": { + "set_value": "mdi:form-textbox", + "reload": "mdi:reload" + } +} diff --git a/homeassistant/components/input_text/reproduce_state.py b/homeassistant/components/input_text/reproduce_state.py index ef82579f4b7..78e81dba95a 100644 --- a/homeassistant/components/input_text/reproduce_state.py +++ b/homeassistant/components/input_text/reproduce_state.py @@ -1,4 +1,5 @@ """Reproduce an Input text state.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/insteon/__init__.py b/homeassistant/components/insteon/__init__.py index a074ad4600b..529ac20df52 100644 --- a/homeassistant/components/insteon/__init__.py +++ b/homeassistant/components/insteon/__init__.py @@ -1,4 +1,5 @@ """Support for INSTEON Modems (PLM and Hub).""" + from contextlib import suppress import logging diff --git a/homeassistant/components/insteon/api/properties.py b/homeassistant/components/insteon/api/properties.py index 80a76e482e5..7fac5439f56 100644 --- a/homeassistant/components/insteon/api/properties.py +++ b/homeassistant/components/insteon/api/properties.py @@ -74,7 +74,7 @@ def _read_only_schema(name, value): def get_schema(prop, name, groups): - """Return the correct shema type.""" + """Return the correct schema type.""" if prop.is_read_only: return _read_only_schema(name, prop.value) if name == RAMP_RATE_IN_SEC: diff --git a/homeassistant/components/insteon/binary_sensor.py b/homeassistant/components/insteon/binary_sensor.py index 02af89dba01..fb19d2287cc 100644 --- a/homeassistant/components/insteon/binary_sensor.py +++ b/homeassistant/components/insteon/binary_sensor.py @@ -1,4 +1,5 @@ """Support for INSTEON dimmers via PowerLinc Modem.""" + from pyinsteon.groups import ( CO_SENSOR, DOOR_SENSOR, diff --git a/homeassistant/components/insteon/climate.py b/homeassistant/components/insteon/climate.py index 22bd776e1c8..ffdd17f3ac0 100644 --- a/homeassistant/components/insteon/climate.py +++ b/homeassistant/components/insteon/climate.py @@ -1,4 +1,5 @@ """Support for Insteon thermostat.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/insteon/config_flow.py b/homeassistant/components/insteon/config_flow.py index 36e977f6db0..7eac51c600e 100644 --- a/homeassistant/components/insteon/config_flow.py +++ b/homeassistant/components/insteon/config_flow.py @@ -1,12 +1,19 @@ """Test config flow for Insteon.""" + from __future__ import annotations import logging from pyinsteon import async_close, async_connect, devices -from homeassistant import config_entries from homeassistant.components import dhcp, usb +from homeassistant.config_entries import ( + DEFAULT_DISCOVERY_UNIQUE_ID, + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.const import ( CONF_ADDRESS, CONF_DEVICE, @@ -17,7 +24,6 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.device_registry import format_mac from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -78,10 +84,11 @@ def _remove_override(address, options): new_options = {} if options.get(CONF_X10): new_options[CONF_X10] = options.get(CONF_X10) - new_overrides = [] - for override in options[CONF_OVERRIDE]: - if override[CONF_ADDRESS] != address: - new_overrides.append(override) + new_overrides = [ + override + for override in options[CONF_OVERRIDE] + if override[CONF_ADDRESS] != address + ] if new_overrides: new_options[CONF_OVERRIDE] = new_overrides return new_options @@ -94,19 +101,20 @@ def _remove_x10(device, options): new_options = {} if options.get(CONF_OVERRIDE): new_options[CONF_OVERRIDE] = options.get(CONF_OVERRIDE) - new_x10 = [] - for existing_device in options[CONF_X10]: + new_x10 = [ + existing_device + for existing_device in options[CONF_X10] if ( existing_device[CONF_HOUSECODE].lower() != housecode or existing_device[CONF_UNITCODE] != unitcode - ): - new_x10.append(existing_device) + ) + ] if new_x10: new_options[CONF_X10] = new_x10 return new_options, housecode, unitcode -class InsteonFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class InsteonFlowHandler(ConfigFlow, domain=DOMAIN): """Insteon config flow handler.""" _device_path: str | None = None @@ -116,7 +124,7 @@ class InsteonFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback def async_get_options_flow( - config_entry: config_entries.ConfigEntry, + config_entry: ConfigEntry, ) -> InsteonOptionsFlowHandler: """Define the config flow to handle options.""" return InsteonOptionsFlowHandler(config_entry) @@ -185,7 +193,9 @@ class InsteonFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): step_id=step_id, data_schema=data_schema, errors=errors ) - async def async_step_usb(self, discovery_info: usb.UsbServiceInfo) -> FlowResult: + async def async_step_usb( + self, discovery_info: usb.UsbServiceInfo + ) -> ConfigFlowResult: """Handle USB discovery.""" if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") @@ -203,10 +213,10 @@ class InsteonFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self.context["title_placeholders"] = { CONF_NAME: f"Insteon PLM {self._device_name}" } - await self.async_set_unique_id(config_entries.DEFAULT_DISCOVERY_UNIQUE_ID) + await self.async_set_unique_id(DEFAULT_DISCOVERY_UNIQUE_ID) return await self.async_step_confirm_usb() - async def async_step_confirm_usb(self, user_input=None) -> FlowResult: + async def async_step_confirm_usb(self, user_input=None) -> ConfigFlowResult: """Confirm a USB discovery.""" if user_input is not None: return await self.async_step_plm({CONF_DEVICE: self._device_path}) @@ -216,7 +226,9 @@ class InsteonFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): description_placeholders={CONF_NAME: self._device_name}, ) - async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: + async def async_step_dhcp( + self, discovery_info: dhcp.DhcpServiceInfo + ) -> ConfigFlowResult: """Handle a DHCP discovery.""" self.discovered_conf = {CONF_HOST: discovery_info.ip} self.context["title_placeholders"] = { @@ -226,14 +238,14 @@ class InsteonFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_user() -class InsteonOptionsFlowHandler(config_entries.OptionsFlow): +class InsteonOptionsFlowHandler(OptionsFlow): """Handle an Insteon options flow.""" - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + def __init__(self, config_entry: ConfigEntry) -> None: """Init the InsteonOptionsFlowHandler class.""" self.config_entry = config_entry - async def async_step_init(self, user_input=None) -> FlowResult: + async def async_step_init(self, user_input=None) -> ConfigFlowResult: """Init the options config flow.""" menu_options = [STEP_ADD_OVERRIDE, STEP_ADD_X10] @@ -250,7 +262,7 @@ class InsteonOptionsFlowHandler(config_entries.OptionsFlow): return self.async_show_menu(step_id="init", menu_options=menu_options) - async def async_step_change_hub_config(self, user_input=None) -> FlowResult: + async def async_step_change_hub_config(self, user_input=None) -> ConfigFlowResult: """Change the Hub configuration.""" errors = {} if user_input is not None: @@ -276,7 +288,7 @@ class InsteonOptionsFlowHandler(config_entries.OptionsFlow): step_id=STEP_CHANGE_HUB_CONFIG, data_schema=data_schema, errors=errors ) - async def async_step_change_plm_config(self, user_input=None) -> FlowResult: + async def async_step_change_plm_config(self, user_input=None) -> ConfigFlowResult: """Change the PLM configuration.""" errors = {} if user_input is not None: @@ -299,7 +311,7 @@ class InsteonOptionsFlowHandler(config_entries.OptionsFlow): step_id=STEP_CHANGE_PLM_CONFIG, data_schema=data_schema, errors=errors ) - async def async_step_add_override(self, user_input=None) -> FlowResult: + async def async_step_add_override(self, user_input=None) -> ConfigFlowResult: """Add a device override.""" errors = {} if user_input is not None: @@ -315,7 +327,7 @@ class InsteonOptionsFlowHandler(config_entries.OptionsFlow): step_id=STEP_ADD_OVERRIDE, data_schema=data_schema, errors=errors ) - async def async_step_add_x10(self, user_input=None) -> FlowResult: + async def async_step_add_x10(self, user_input=None) -> ConfigFlowResult: """Add an X10 device.""" errors: dict[str, str] = {} if user_input is not None: @@ -328,7 +340,7 @@ class InsteonOptionsFlowHandler(config_entries.OptionsFlow): step_id=STEP_ADD_X10, data_schema=data_schema, errors=errors ) - async def async_step_remove_override(self, user_input=None) -> FlowResult: + async def async_step_remove_override(self, user_input=None) -> ConfigFlowResult: """Remove a device override.""" errors: dict[str, str] = {} options = self.config_entry.options @@ -346,7 +358,7 @@ class InsteonOptionsFlowHandler(config_entries.OptionsFlow): step_id=STEP_REMOVE_OVERRIDE, data_schema=data_schema, errors=errors ) - async def async_step_remove_x10(self, user_input=None) -> FlowResult: + async def async_step_remove_x10(self, user_input=None) -> ConfigFlowResult: """Remove an X10 device.""" errors: dict[str, str] = {} options = self.config_entry.options diff --git a/homeassistant/components/insteon/const.py b/homeassistant/components/insteon/const.py index 69040199589..b7e6e6055e1 100644 --- a/homeassistant/components/insteon/const.py +++ b/homeassistant/components/insteon/const.py @@ -1,4 +1,5 @@ """Constants used by insteon component.""" + import re from pyinsteon.groups import ( diff --git a/homeassistant/components/insteon/cover.py b/homeassistant/components/insteon/cover.py index 0756e603579..60c4593f3c5 100644 --- a/homeassistant/components/insteon/cover.py +++ b/homeassistant/components/insteon/cover.py @@ -1,4 +1,5 @@ """Support for Insteon covers via PowerLinc Modem.""" + import math from typing import Any diff --git a/homeassistant/components/insteon/fan.py b/homeassistant/components/insteon/fan.py index da9e3de6422..cbdae434df6 100644 --- a/homeassistant/components/insteon/fan.py +++ b/homeassistant/components/insteon/fan.py @@ -1,4 +1,5 @@ """Support for INSTEON fans via PowerLinc Modem.""" + from __future__ import annotations import math diff --git a/homeassistant/components/insteon/icons.json b/homeassistant/components/insteon/icons.json new file mode 100644 index 00000000000..4d015e13b0d --- /dev/null +++ b/homeassistant/components/insteon/icons.json @@ -0,0 +1,15 @@ +{ + "services": { + "add_all_link": "mdi:link-variant", + "delete_all_link": "mdi:link-variant-remove", + "load_all_link_database": "mdi:database", + "print_all_link_database": "mdi:database-export", + "print_im_all_link_database": "mdi:database-export", + "x10_all_units_off": "mdi:power-off", + "x10_all_lights_on": "mdi:lightbulb-on", + "x10_all_lights_off": "mdi:lightbulb-off", + "scene_on": "mdi:palette", + "scene_off": "mdi:palette-outline", + "add_default_links": "mdi:link-variant-plus" + } +} diff --git a/homeassistant/components/insteon/insteon_entity.py b/homeassistant/components/insteon/insteon_entity.py index d1762fa8d35..f81298dfe48 100644 --- a/homeassistant/components/insteon/insteon_entity.py +++ b/homeassistant/components/insteon/insteon_entity.py @@ -1,4 +1,5 @@ """Insteon base entity.""" + import functools import logging diff --git a/homeassistant/components/insteon/ipdb.py b/homeassistant/components/insteon/ipdb.py index 9e9f987d611..5f56c0d6976 100644 --- a/homeassistant/components/insteon/ipdb.py +++ b/homeassistant/components/insteon/ipdb.py @@ -1,4 +1,5 @@ """Utility methods for the Insteon platform.""" + from collections.abc import Iterable from pyinsteon.device_types.device_base import Device diff --git a/homeassistant/components/insteon/light.py b/homeassistant/components/insteon/light.py index 121d8d62c66..f6752db3cf1 100644 --- a/homeassistant/components/insteon/light.py +++ b/homeassistant/components/insteon/light.py @@ -1,4 +1,5 @@ """Support for Insteon lights via PowerLinc Modem.""" + from typing import Any from pyinsteon.config import ON_LEVEL diff --git a/homeassistant/components/insteon/schemas.py b/homeassistant/components/insteon/schemas.py index 497af743195..e277281c240 100644 --- a/homeassistant/components/insteon/schemas.py +++ b/homeassistant/components/insteon/schemas.py @@ -1,4 +1,5 @@ """Schemas used by insteon component.""" + from __future__ import annotations from binascii import Error as HexError, unhexlify @@ -78,7 +79,7 @@ ADD_DEFAULT_LINKS_SCHEMA = vol.Schema({vol.Required(CONF_ENTITY_ID): cv.entity_i def normalize_byte_entry_to_int(entry: int | bytes | str): """Format a hex entry value.""" if isinstance(entry, int): - if entry in range(0, 256): + if entry in range(256): return entry raise ValueError("Must be single byte") if isinstance(entry, str): @@ -102,17 +103,18 @@ def add_device_override(config_data, new_override): except ValueError as err: raise ValueError("Incorrect values") from err - overrides = [] - - for override in config_data.get(CONF_OVERRIDE, []): - if override[CONF_ADDRESS] != address: - overrides.append(override) - - curr_override = {} - curr_override[CONF_ADDRESS] = address - curr_override[CONF_CAT] = cat - curr_override[CONF_SUBCAT] = subcat - overrides.append(curr_override) + overrides = [ + override + for override in config_data.get(CONF_OVERRIDE, []) + if override[CONF_ADDRESS] != address + ] + overrides.append( + { + CONF_ADDRESS: address, + CONF_CAT: cat, + CONF_SUBCAT: subcat, + } + ) new_config = {} if config_data.get(CONF_X10): @@ -123,21 +125,20 @@ def add_device_override(config_data, new_override): def add_x10_device(config_data, new_x10): """Add a new X10 device to X10 device list.""" - x10_devices = [] - for x10_device in config_data.get(CONF_X10, []): - if ( - x10_device[CONF_HOUSECODE] != new_x10[CONF_HOUSECODE] - or x10_device[CONF_UNITCODE] != new_x10[CONF_UNITCODE] - ): - x10_devices.append(x10_device) - - curr_device = {} - curr_device[CONF_HOUSECODE] = new_x10[CONF_HOUSECODE] - curr_device[CONF_UNITCODE] = new_x10[CONF_UNITCODE] - curr_device[CONF_PLATFORM] = new_x10[CONF_PLATFORM] - curr_device[CONF_DIM_STEPS] = new_x10[CONF_DIM_STEPS] - x10_devices.append(curr_device) - + x10_devices = [ + x10_device + for x10_device in config_data.get(CONF_X10, []) + if x10_device[CONF_HOUSECODE] != new_x10[CONF_HOUSECODE] + or x10_device[CONF_UNITCODE] != new_x10[CONF_UNITCODE] + ] + x10_devices.append( + { + CONF_HOUSECODE: new_x10[CONF_HOUSECODE], + CONF_UNITCODE: new_x10[CONF_UNITCODE], + CONF_PLATFORM: new_x10[CONF_PLATFORM], + CONF_DIM_STEPS: new_x10[CONF_DIM_STEPS], + } + ) new_config = {} if config_data.get(CONF_OVERRIDE): new_config[CONF_OVERRIDE] = config_data[CONF_OVERRIDE] @@ -222,17 +223,14 @@ def build_hub_schema( def build_remove_override_schema(data): """Build the schema to remove device overrides in config flow options.""" - selection = [] - for override in data: - selection.append(override[CONF_ADDRESS]) + selection = [override[CONF_ADDRESS] for override in data] return vol.Schema({vol.Required(CONF_ADDRESS): vol.In(selection)}) def build_remove_x10_schema(data): """Build the schema to remove an X10 device in config flow options.""" - selection = [] - for device in data: - housecode = device[CONF_HOUSECODE].upper() - unitcode = device[CONF_UNITCODE] - selection.append(f"Housecode: {housecode}, Unitcode: {unitcode}") + selection = [ + f"Housecode: {device[CONF_HOUSECODE].upper()}, Unitcode: {device[CONF_UNITCODE]}" + for device in data + ] return vol.Schema({vol.Required(CONF_DEVICE): vol.In(selection)}) diff --git a/homeassistant/components/insteon/switch.py b/homeassistant/components/insteon/switch.py index 8acde0429cd..b60729232f2 100644 --- a/homeassistant/components/insteon/switch.py +++ b/homeassistant/components/insteon/switch.py @@ -1,4 +1,5 @@ """Support for INSTEON dimmers via PowerLinc Modem.""" + from typing import Any from homeassistant.components.switch import SwitchEntity diff --git a/homeassistant/components/insteon/utils.py b/homeassistant/components/insteon/utils.py index d7cbe676eee..272018ea507 100644 --- a/homeassistant/components/insteon/utils.py +++ b/homeassistant/components/insteon/utils.py @@ -1,4 +1,5 @@ """Utilities used by insteon component.""" + from __future__ import annotations import asyncio @@ -145,7 +146,7 @@ def add_insteon_events(hass: HomeAssistant, device: Device) -> None: for name_or_group, event in device.events.items(): if isinstance(name_or_group, int): - for _, event in device.events[name_or_group].items(): + for event in device.events[name_or_group].values(): _register_event(event, async_fire_insteon_event) else: _register_event(event, async_fire_insteon_event) diff --git a/homeassistant/components/integration/__init__.py b/homeassistant/components/integration/__init__.py index f482f4e41e8..4a8d4baa3f2 100644 --- a/homeassistant/components/integration/__init__.py +++ b/homeassistant/components/integration/__init__.py @@ -1,4 +1,5 @@ """The Integration integration.""" + from __future__ import annotations from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/integration/config_flow.py b/homeassistant/components/integration/config_flow.py index 3a9e1d15ffe..318f1355aae 100644 --- a/homeassistant/components/integration/config_flow.py +++ b/homeassistant/components/integration/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Integration - Riemann sum integral integration.""" + from __future__ import annotations from collections.abc import Mapping diff --git a/homeassistant/components/integration/sensor.py b/homeassistant/components/integration/sensor.py index d7d5c84f17a..62a0dbdec78 100644 --- a/homeassistant/components/integration/sensor.py +++ b/homeassistant/components/integration/sensor.py @@ -1,6 +1,8 @@ """Numeric integration of data coming from a source sensor over time.""" + from __future__ import annotations +from abc import ABC, abstractmethod from dataclasses import dataclass from decimal import Decimal, DecimalException, InvalidOperation import logging @@ -26,8 +28,9 @@ from homeassistant.const import ( STATE_UNKNOWN, UnitOfTime, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import Event, HomeAssistant, State, callback from homeassistant.helpers import ( + condition, config_validation as cv, device_registry as dr, entity_registry as er, @@ -38,7 +41,7 @@ from homeassistant.helpers.event import ( EventStateChangedData, async_track_state_change_event, ) -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, EventType +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import ( CONF_ROUND_DIGITS, @@ -88,6 +91,72 @@ PLATFORM_SCHEMA = vol.All( ) +class _IntegrationMethod(ABC): + @staticmethod + def from_name(method_name: str) -> _IntegrationMethod: + return _NAME_TO_INTEGRATION_METHOD[method_name]() + + @abstractmethod + def validate_states(self, left: State, right: State) -> bool: + """Check state requirements for integration.""" + + @abstractmethod + def calculate_area_with_two_states( + self, elapsed_time: float, left: State, right: State + ) -> Decimal: + """Calculate area given two states.""" + + def calculate_area_with_one_state( + self, elapsed_time: float, constant_state: State + ) -> Decimal: + return Decimal(constant_state.state) * Decimal(elapsed_time) + + +class _Trapezoidal(_IntegrationMethod): + def calculate_area_with_two_states( + self, elapsed_time: float, left: State, right: State + ) -> Decimal: + return Decimal(elapsed_time) * (Decimal(left.state) + Decimal(right.state)) / 2 + + def validate_states(self, left: State, right: State) -> bool: + return _is_numeric_state(left) and _is_numeric_state(right) + + +class _Left(_IntegrationMethod): + def calculate_area_with_two_states( + self, elapsed_time: float, left: State, right: State + ) -> Decimal: + return self.calculate_area_with_one_state(elapsed_time, left) + + def validate_states(self, left: State, right: State) -> bool: + return _is_numeric_state(left) + + +class _Right(_IntegrationMethod): + def calculate_area_with_two_states( + self, elapsed_time: float, left: State, right: State + ) -> Decimal: + return self.calculate_area_with_one_state(elapsed_time, right) + + def validate_states(self, left: State, right: State) -> bool: + return _is_numeric_state(right) + + +def _is_numeric_state(state: State) -> bool: + try: + float(state.state) + return True + except (ValueError, TypeError): + return False + + +_NAME_TO_INTEGRATION_METHOD: dict[str, type[_IntegrationMethod]] = { + METHOD_LEFT: _Left, + METHOD_RIGHT: _Right, + METHOD_TRAPEZOIDAL: _Trapezoidal, +} + + @dataclass class IntegrationSensorExtraStoredData(SensorExtraStoredData): """Object to hold extra stored data.""" @@ -230,10 +299,10 @@ class IntegrationSensor(RestoreSensor): self._sensor_source_id = source_entity self._round_digits = round_digits self._state: Decimal | None = None - self._method = integration_method + self._method = _IntegrationMethod.from_name(integration_method) self._attr_name = name if name is not None else f"{source_entity} integral" - self._unit_template = f"{'' if unit_prefix is None else unit_prefix}{{}}" + self._unit_prefix_string = "" if unit_prefix is None else unit_prefix self._unit_of_measurement: str | None = None self._unit_prefix = UNIT_PREFIXES[unit_prefix] self._unit_time = UNIT_TIME[unit_time] @@ -243,15 +312,52 @@ class IntegrationSensor(RestoreSensor): self._last_valid_state: Decimal | None = None self._attr_device_info = device_info - def _unit(self, source_unit: str) -> str: - """Derive unit from the source sensor, SI prefix and time unit.""" + def _calculate_unit(self, source_unit: str) -> str: + """Multiply source_unit with time unit of the integral. + + Possibly cancelling out a time unit in the denominator of the source_unit. + Note that this is a heuristic string manipulation method and might not + transform all source units in a sensible way. + + Examples: + - Speed to distance: 'km/h' and 'h' will be transformed to 'km' + - Power to energy: 'W' and 'h' will be transformed to 'Wh' + + """ unit_time = self._unit_time_str if source_unit.endswith(f"/{unit_time}"): integral_unit = source_unit[0 : (-(1 + len(unit_time)))] else: integral_unit = f"{source_unit}{unit_time}" - return self._unit_template.format(integral_unit) + return f"{self._unit_prefix_string}{integral_unit}" + + def _derive_and_set_attributes_from_state(self, source_state: State) -> None: + source_unit = source_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + if source_unit is not None: + self._unit_of_measurement = self._calculate_unit(source_unit) + else: + # If the source has no defined unit we cannot derive a unit for the integral + self._unit_of_measurement = None + + if ( + self.device_class is None + and source_state.attributes.get(ATTR_DEVICE_CLASS) + == SensorDeviceClass.POWER + ): + self._attr_device_class = SensorDeviceClass.ENERGY + self._attr_icon = None # Remove this sensors icon default and allow to fallback to the ENERGY default + + def _update_integral(self, area: Decimal) -> None: + area_scaled = area / (self._unit_prefix * self._unit_time) + if isinstance(self._state, Decimal): + self._state += area_scaled + else: + self._state = area_scaled + _LOGGER.debug( + "area = %s, area_scaled = %s new state = %s", area, area_scaled, self._state + ) + self._last_valid_state = self._state async def async_added_to_hass(self) -> None: """Handle entity which will be added.""" @@ -291,109 +397,45 @@ class IntegrationSensor(RestoreSensor): self._attr_device_class = state.attributes.get(ATTR_DEVICE_CLASS) self._unit_of_measurement = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - @callback - def calc_integration(event: EventType[EventStateChangedData]) -> None: - """Handle the sensor state changes.""" - old_state = event.data["old_state"] - new_state = event.data["new_state"] - - if ( - source_state := self.hass.states.get(self._sensor_source_id) - ) is None or source_state.state == STATE_UNAVAILABLE: - self._attr_available = False - self.async_write_ha_state() - return - - self._attr_available = True - - if old_state is None or new_state is None: - # we can't calculate the elapsed time, so we can't calculate the integral - return - - unit = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - if unit is not None: - self._unit_of_measurement = self._unit(unit) - - if ( - self.device_class is None - and new_state.attributes.get(ATTR_DEVICE_CLASS) - == SensorDeviceClass.POWER - ): - self._attr_device_class = SensorDeviceClass.ENERGY - self._attr_icon = None - - self.async_write_ha_state() - - try: - # integration as the Riemann integral of previous measures. - elapsed_time = ( - new_state.last_updated - old_state.last_updated - ).total_seconds() - - if ( - self._method == METHOD_TRAPEZOIDAL - and new_state.state - not in ( - STATE_UNKNOWN, - STATE_UNAVAILABLE, - ) - and old_state.state - not in ( - STATE_UNKNOWN, - STATE_UNAVAILABLE, - ) - ): - area = ( - (Decimal(new_state.state) + Decimal(old_state.state)) - * Decimal(elapsed_time) - / 2 - ) - elif self._method == METHOD_LEFT and old_state.state not in ( - STATE_UNKNOWN, - STATE_UNAVAILABLE, - ): - area = Decimal(old_state.state) * Decimal(elapsed_time) - elif self._method == METHOD_RIGHT and new_state.state not in ( - STATE_UNKNOWN, - STATE_UNAVAILABLE, - ): - area = Decimal(new_state.state) * Decimal(elapsed_time) - else: - _LOGGER.debug( - "Could not apply method %s to %s -> %s", - self._method, - old_state.state, - new_state.state, - ) - return - - integral = area / (self._unit_prefix * self._unit_time) - _LOGGER.debug( - "area = %s, integral = %s state = %s", area, integral, self._state - ) - assert isinstance(integral, Decimal) - except ValueError as err: - _LOGGER.warning("While calculating integration: %s", err) - except DecimalException as err: - _LOGGER.warning( - "Invalid state (%s > %s): %s", old_state.state, new_state.state, err - ) - except AssertionError as err: - _LOGGER.error("Could not calculate integral: %s", err) - else: - if isinstance(self._state, Decimal): - self._state += integral - else: - self._state = integral - self._last_valid_state = self._state - self.async_write_ha_state() - self.async_on_remove( async_track_state_change_event( - self.hass, [self._sensor_source_id], calc_integration + self.hass, + [self._sensor_source_id], + self._handle_state_change, ) ) + @callback + def _handle_state_change(self, event: Event[EventStateChangedData]) -> None: + old_state = event.data["old_state"] + new_state = event.data["new_state"] + + if old_state is None or new_state is None: + return + + if condition.state(self.hass, new_state, [STATE_UNAVAILABLE]): + self._attr_available = False + self.async_write_ha_state() + return + + self._attr_available = True + self._derive_and_set_attributes_from_state(new_state) + + if not self._method.validate_states(old_state, new_state): + self.async_write_ha_state() + return + + elapsed_seconds = ( + new_state.last_updated - old_state.last_updated + ).total_seconds() + + area = self._method.calculate_area_with_two_states( + elapsed_seconds, old_state, new_state + ) + + self._update_integral(area) + self.async_write_ha_state() + @property def native_value(self) -> Decimal | None: """Return the state of the sensor.""" diff --git a/homeassistant/components/intellifire/__init__.py b/homeassistant/components/intellifire/__init__.py index 81ef383dfab..7af472c8745 100644 --- a/homeassistant/components/intellifire/__init__.py +++ b/homeassistant/components/intellifire/__init__.py @@ -1,4 +1,5 @@ """The IntelliFire integration.""" + from __future__ import annotations from aiohttp import ClientConnectionError diff --git a/homeassistant/components/intellifire/binary_sensor.py b/homeassistant/components/intellifire/binary_sensor.py index 503b97f183d..a1b8865c876 100644 --- a/homeassistant/components/intellifire/binary_sensor.py +++ b/homeassistant/components/intellifire/binary_sensor.py @@ -1,4 +1,5 @@ """Support for IntelliFire Binary Sensors.""" + from __future__ import annotations from collections.abc import Callable @@ -39,25 +40,21 @@ INTELLIFIRE_BINARY_SENSORS: tuple[IntellifireBinarySensorEntityDescription, ...] IntellifireBinarySensorEntityDescription( key="on_off", # This is the sensor name translation_key="flame", # This is the translation key - icon="mdi:fire", value_fn=lambda data: data.is_on, ), IntellifireBinarySensorEntityDescription( key="timer_on", translation_key="timer_on", - icon="mdi:camera-timer", value_fn=lambda data: data.timer_on, ), IntellifireBinarySensorEntityDescription( key="pilot_light_on", translation_key="pilot_light_on", - icon="mdi:fire-alert", value_fn=lambda data: data.pilot_on, ), IntellifireBinarySensorEntityDescription( key="thermostat_on", translation_key="thermostat_on", - icon="mdi:home-thermometer-outline", value_fn=lambda data: data.thermostat_on, ), IntellifireBinarySensorEntityDescription( @@ -77,7 +74,6 @@ INTELLIFIRE_BINARY_SENSORS: tuple[IntellifireBinarySensorEntityDescription, ...] IntellifireBinarySensorEntityDescription( key="error_fan_delay", translation_key="fan_delay_error", - icon="mdi:fan-alert", entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda data: data.error_fan_delay, device_class=BinarySensorDeviceClass.PROBLEM, @@ -99,7 +95,6 @@ INTELLIFIRE_BINARY_SENSORS: tuple[IntellifireBinarySensorEntityDescription, ...] IntellifireBinarySensorEntityDescription( key="error_fan", translation_key="fan_error", - icon="mdi:fan-alert", entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda data: data.error_fan, device_class=BinarySensorDeviceClass.PROBLEM, diff --git a/homeassistant/components/intellifire/climate.py b/homeassistant/components/intellifire/climate.py index 9fed9c08bb6..ed4facffc67 100644 --- a/homeassistant/components/intellifire/climate.py +++ b/homeassistant/components/intellifire/climate.py @@ -1,4 +1,5 @@ """Intellifire Climate Entities.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/intellifire/config_flow.py b/homeassistant/components/intellifire/config_flow.py index 6a061b40bcc..268fc6623d3 100644 --- a/homeassistant/components/intellifire/config_flow.py +++ b/homeassistant/components/intellifire/config_flow.py @@ -1,4 +1,5 @@ """Config flow for IntelliFire integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -11,10 +12,9 @@ from intellifire4py.exceptions import LoginException from intellifire4py.intellifire import IntellifireAPICloud, IntellifireAPILocal import voluptuous as vol -from homeassistant import config_entries from homeassistant.components.dhcp import DhcpServiceInfo +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PASSWORD, CONF_USERNAME -from homeassistant.data_entry_flow import FlowResult from .const import CONF_USER_ID, DOMAIN, LOGGER @@ -46,7 +46,7 @@ async def validate_host_input(host: str, dhcp_mode: bool = False) -> str: return serial -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class IntelliFireConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for IntelliFire.""" VERSION = 1 @@ -107,7 +107,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_api_config( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Configure API access.""" errors = {} @@ -151,7 +151,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="api_config", errors=errors, data_schema=control_schema ) - async def _async_validate_ip_and_continue(self, host: str) -> FlowResult: + async def _async_validate_ip_and_continue(self, host: str) -> ConfigFlowResult: """Validate local config and continue.""" self._async_abort_entries_match({CONF_HOST: host}) self._serial = await validate_host_input(host) @@ -181,7 +181,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_pick_device( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Pick which device to configure.""" errors = {} LOGGER.debug("STEP: pick_device") @@ -210,7 +210,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Start the user flow.""" # Launch fireplaces discovery @@ -222,7 +222,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): LOGGER.debug("Running Step: manual_device_entry") return await self.async_step_manual_device_entry() - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Perform reauth upon an API authentication error.""" LOGGER.debug("STEP: reauth") entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) @@ -237,7 +239,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.context["title_placeholders"] = placeholders return await self.async_step_api_config() - async def async_step_dhcp(self, discovery_info: DhcpServiceInfo) -> FlowResult: + async def async_step_dhcp( + self, discovery_info: DhcpServiceInfo + ) -> ConfigFlowResult: """Handle DHCP Discovery.""" # Run validation logic on ip diff --git a/homeassistant/components/intellifire/const.py b/homeassistant/components/intellifire/const.py index cae25ea11ae..5c8af1eefe9 100644 --- a/homeassistant/components/intellifire/const.py +++ b/homeassistant/components/intellifire/const.py @@ -1,4 +1,5 @@ """Constants for the IntelliFire integration.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/intellifire/coordinator.py b/homeassistant/components/intellifire/coordinator.py index 4045c19217b..0a46ff61435 100644 --- a/homeassistant/components/intellifire/coordinator.py +++ b/homeassistant/components/intellifire/coordinator.py @@ -1,4 +1,5 @@ """The IntelliFire integration.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/intellifire/entity.py b/homeassistant/components/intellifire/entity.py index 6ef63f5347c..3b35c9dabd8 100644 --- a/homeassistant/components/intellifire/entity.py +++ b/homeassistant/components/intellifire/entity.py @@ -1,4 +1,5 @@ """Platform for shared base classes for sensors.""" + from __future__ import annotations from homeassistant.helpers.entity import EntityDescription diff --git a/homeassistant/components/intellifire/fan.py b/homeassistant/components/intellifire/fan.py index 7c376eeec4c..387b6d059c6 100644 --- a/homeassistant/components/intellifire/fan.py +++ b/homeassistant/components/intellifire/fan.py @@ -1,4 +1,5 @@ """Fan definition for Intellifire.""" + from __future__ import annotations from collections.abc import Awaitable, Callable diff --git a/homeassistant/components/intellifire/icons.json b/homeassistant/components/intellifire/icons.json new file mode 100644 index 00000000000..6dca69484b6 --- /dev/null +++ b/homeassistant/components/intellifire/icons.json @@ -0,0 +1,45 @@ +{ + "entity": { + "binary_sensor": { + "flame": { + "default": "mdi:fire" + }, + "timer_on": { + "default": "mdi:camera-timer" + }, + "pilot_light_on": { + "default": "mdi:fire-alert" + }, + "thermostat_on": { + "default": "mdi:home-thermometer-outline" + }, + "fan_delay_error": { + "default": "mdi:fan-alert" + }, + "fan_error": { + "default": "mdi:fan-alert" + } + }, + "number": { + "flame_control": { + "default": "mdi:arrow-expand-vertical" + } + }, + "sensor": { + "flame_height": { + "default": "mdi:fire-circle" + }, + "fan_speed": { + "default": "mdi:fan" + }, + "timer_end_timestamp": { + "default": "mdi:timer-sand" + } + }, + "switch": { + "pilot_light": { + "default": "mdi:fire-alert" + } + } + } +} diff --git a/homeassistant/components/intellifire/light.py b/homeassistant/components/intellifire/light.py index a807735ed79..a7f2befaf33 100644 --- a/homeassistant/components/intellifire/light.py +++ b/homeassistant/components/intellifire/light.py @@ -1,4 +1,5 @@ """The IntelliFire Light.""" + from __future__ import annotations from collections.abc import Awaitable, Callable diff --git a/homeassistant/components/intellifire/number.py b/homeassistant/components/intellifire/number.py index efcafd2acd8..dca7a74c78e 100644 --- a/homeassistant/components/intellifire/number.py +++ b/homeassistant/components/intellifire/number.py @@ -1,4 +1,5 @@ """Flame height number sensors.""" + from __future__ import annotations from dataclasses import dataclass @@ -28,7 +29,6 @@ async def async_setup_entry( description = NumberEntityDescription( key="flame_control", translation_key="flame_control", - icon="mdi:arrow-expand-vertical", ) async_add_entities( diff --git a/homeassistant/components/intellifire/sensor.py b/homeassistant/components/intellifire/sensor.py index c974378fb71..dd3eef9c9b4 100644 --- a/homeassistant/components/intellifire/sensor.py +++ b/homeassistant/components/intellifire/sensor.py @@ -1,4 +1,5 @@ """Platform for sensor integration.""" + from __future__ import annotations from collections.abc import Callable @@ -57,7 +58,6 @@ INTELLIFIRE_SENSORS: tuple[IntellifireSensorEntityDescription, ...] = ( IntellifireSensorEntityDescription( key="flame_height", translation_key="flame_height", - icon="mdi:fire-circle", state_class=SensorStateClass.MEASUREMENT, # UI uses 1-5 for flame height, backing lib uses 0-4 value_fn=lambda data: (data.flameheight + 1), @@ -80,14 +80,12 @@ INTELLIFIRE_SENSORS: tuple[IntellifireSensorEntityDescription, ...] = ( IntellifireSensorEntityDescription( key="fan_speed", translation_key="fan_speed", - icon="mdi:fan", state_class=SensorStateClass.MEASUREMENT, value_fn=lambda data: data.fanspeed, ), IntellifireSensorEntityDescription( key="timer_end_timestamp", translation_key="timer_end_timestamp", - icon="mdi:timer-sand", state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.TIMESTAMP, value_fn=_time_remaining_to_timestamp, diff --git a/homeassistant/components/intellifire/switch.py b/homeassistant/components/intellifire/switch.py index 03e3a2be0a2..00de6d74a9c 100644 --- a/homeassistant/components/intellifire/switch.py +++ b/homeassistant/components/intellifire/switch.py @@ -1,4 +1,5 @@ """Define switch func.""" + from __future__ import annotations from collections.abc import Awaitable, Callable @@ -45,7 +46,6 @@ INTELLIFIRE_SWITCHES: tuple[IntellifireSwitchEntityDescription, ...] = ( IntellifireSwitchEntityDescription( key="pilot", translation_key="pilot_light", - icon="mdi:fire-alert", on_fn=lambda control_api: control_api.pilot_on(), off_fn=lambda control_api: control_api.pilot_off(), value_fn=lambda data: data.pilot_on, diff --git a/homeassistant/components/intent/__init__.py b/homeassistant/components/intent/__init__.py index f307208e537..7fd9fd4b712 100644 --- a/homeassistant/components/intent/__init__.py +++ b/homeassistant/components/intent/__init__.py @@ -348,7 +348,7 @@ class IntentHandleView(http.HomeAssistantView): ) async def post(self, request: web.Request, data: dict[str, Any]) -> web.Response: """Handle intent with name/data.""" - hass: HomeAssistant = request.app["hass"] + hass = request.app[http.KEY_HASS] language = hass.config.language try: diff --git a/homeassistant/components/intent_script/__init__.py b/homeassistant/components/intent_script/__init__.py index d184dad47c9..63b37c08950 100644 --- a/homeassistant/components/intent_script/__init__.py +++ b/homeassistant/components/intent_script/__init__.py @@ -1,4 +1,5 @@ """Handle intents with scripts.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/intent_script/icons.json b/homeassistant/components/intent_script/icons.json new file mode 100644 index 00000000000..a03163179cb --- /dev/null +++ b/homeassistant/components/intent_script/icons.json @@ -0,0 +1,5 @@ +{ + "services": { + "reload": "mdi:reload" + } +} diff --git a/homeassistant/components/intesishome/climate.py b/homeassistant/components/intesishome/climate.py index 64f52fae0a6..7a504d7aced 100644 --- a/homeassistant/components/intesishome/climate.py +++ b/homeassistant/components/intesishome/climate.py @@ -1,4 +1,5 @@ """Support for IntesisHome and airconwithme Smart AC Controllers.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/ios/__init__.py b/homeassistant/components/ios/__init__.py index 291f08425fa..4b2b92a482d 100644 --- a/homeassistant/components/ios/__init__.py +++ b/homeassistant/components/ios/__init__.py @@ -1,4 +1,5 @@ """Native Home Assistant iOS app component.""" + import datetime from http import HTTPStatus from typing import Any @@ -7,7 +8,7 @@ from aiohttp import web import voluptuous as vol from homeassistant import config_entries -from homeassistant.components.http import HomeAssistantView +from homeassistant.components.http import KEY_HASS, HomeAssistantView from homeassistant.const import Platform from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError @@ -348,7 +349,7 @@ class iOSIdentifyDeviceView(HomeAssistantView): except ValueError: return self.json_message("Invalid JSON", HTTPStatus.BAD_REQUEST) - hass: HomeAssistant = request.app["hass"] + hass = request.app[KEY_HASS] data[ATTR_LAST_SEEN_AT] = datetime.datetime.now().isoformat() diff --git a/homeassistant/components/ios/config_flow.py b/homeassistant/components/ios/config_flow.py index 47959e34074..6b00589d048 100644 --- a/homeassistant/components/ios/config_flow.py +++ b/homeassistant/components/ios/config_flow.py @@ -1,4 +1,5 @@ """Config flow for iOS.""" + from homeassistant.helpers import config_entry_flow from .const import DOMAIN diff --git a/homeassistant/components/ios/notify.py b/homeassistant/components/ios/notify.py index a8d1b2514cd..92a706b3a38 100644 --- a/homeassistant/components/ios/notify.py +++ b/homeassistant/components/ios/notify.py @@ -1,4 +1,5 @@ """Support for iOS push notifications.""" + from __future__ import annotations from http import HTTPStatus diff --git a/homeassistant/components/ios/sensor.py b/homeassistant/components/ios/sensor.py index 59a9d499d93..4171b8ecd46 100644 --- a/homeassistant/components/ios/sensor.py +++ b/homeassistant/components/ios/sensor.py @@ -1,4 +1,5 @@ """Support for Home Assistant iOS app sensors.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/iotawatt/__init__.py b/homeassistant/components/iotawatt/__init__.py index 9f51382f98e..8f35d4e0796 100644 --- a/homeassistant/components/iotawatt/__init__.py +++ b/homeassistant/components/iotawatt/__init__.py @@ -1,4 +1,5 @@ """The iotawatt integration.""" + from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/iotawatt/config_flow.py b/homeassistant/components/iotawatt/config_flow.py index 9ec860ea76a..b9310b8a2b9 100644 --- a/homeassistant/components/iotawatt/config_flow.py +++ b/homeassistant/components/iotawatt/config_flow.py @@ -1,4 +1,5 @@ """Config flow for iotawatt integration.""" + from __future__ import annotations import logging @@ -6,8 +7,10 @@ import logging from iotawattpy.iotawatt import Iotawatt import voluptuous as vol -from homeassistant import config_entries, core, exceptions +from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import httpx_client from .const import CONNECTION_ERRORS, DOMAIN @@ -15,9 +18,7 @@ from .const import CONNECTION_ERRORS, DOMAIN _LOGGER = logging.getLogger(__name__) -async def validate_input( - hass: core.HomeAssistant, data: dict[str, str] -) -> dict[str, str]: +async def validate_input(hass: HomeAssistant, data: dict[str, str]) -> dict[str, str]: """Validate the user input allows us to connect.""" iotawatt = Iotawatt( "", @@ -40,7 +41,7 @@ async def validate_input( return {} -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class IOTaWattConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for iotawatt.""" VERSION = 1 @@ -99,9 +100,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_create_entry(title=data[CONF_HOST], data=data) -class CannotConnect(exceptions.HomeAssistantError): +class CannotConnect(HomeAssistantError): """Error to indicate we cannot connect.""" -class InvalidAuth(exceptions.HomeAssistantError): +class InvalidAuth(HomeAssistantError): """Error to indicate there is invalid auth.""" diff --git a/homeassistant/components/iotawatt/const.py b/homeassistant/components/iotawatt/const.py index db847f3dfe8..de008388f62 100644 --- a/homeassistant/components/iotawatt/const.py +++ b/homeassistant/components/iotawatt/const.py @@ -1,4 +1,5 @@ """Constants for the IoTaWatt integration.""" + from __future__ import annotations import json diff --git a/homeassistant/components/iotawatt/coordinator.py b/homeassistant/components/iotawatt/coordinator.py index 6c97fc99169..e741c7a5a27 100644 --- a/homeassistant/components/iotawatt/coordinator.py +++ b/homeassistant/components/iotawatt/coordinator.py @@ -1,4 +1,5 @@ """IoTaWatt DataUpdateCoordinator.""" + from __future__ import annotations from datetime import datetime, timedelta diff --git a/homeassistant/components/iotawatt/sensor.py b/homeassistant/components/iotawatt/sensor.py index 4faac347c40..c9af588c160 100644 --- a/homeassistant/components/iotawatt/sensor.py +++ b/homeassistant/components/iotawatt/sensor.py @@ -1,4 +1,5 @@ """Support for IoTaWatt Energy monitor.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/iperf3/__init__.py b/homeassistant/components/iperf3/__init__.py index 0448c3e48b2..a621f1fb27e 100644 --- a/homeassistant/components/iperf3/__init__.py +++ b/homeassistant/components/iperf3/__init__.py @@ -1,4 +1,5 @@ """Support for Iperf3 network measurement tool.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/iperf3/icons.json b/homeassistant/components/iperf3/icons.json new file mode 100644 index 00000000000..3ef7e301ed6 --- /dev/null +++ b/homeassistant/components/iperf3/icons.json @@ -0,0 +1,5 @@ +{ + "services": { + "speedtest": "mdi:speedometer" + } +} diff --git a/homeassistant/components/iperf3/sensor.py b/homeassistant/components/iperf3/sensor.py index d3db0e76631..27b3eac26b5 100644 --- a/homeassistant/components/iperf3/sensor.py +++ b/homeassistant/components/iperf3/sensor.py @@ -1,4 +1,5 @@ """Support for Iperf3 sensors.""" + from __future__ import annotations from homeassistant.components.sensor import SensorEntity, SensorEntityDescription diff --git a/homeassistant/components/ipma/__init__.py b/homeassistant/components/ipma/__init__.py index 7668802c9e0..68289d13289 100644 --- a/homeassistant/components/ipma/__init__.py +++ b/homeassistant/components/ipma/__init__.py @@ -1,4 +1,5 @@ """Component for the Portuguese weather service - IPMA.""" + import asyncio import logging diff --git a/homeassistant/components/ipma/config_flow.py b/homeassistant/components/ipma/config_flow.py index cdea88bdbc0..36e70243c93 100644 --- a/homeassistant/components/ipma/config_flow.py +++ b/homeassistant/components/ipma/config_flow.py @@ -1,4 +1,5 @@ """Config flow to configure IPMA component.""" + import logging from typing import Any @@ -7,9 +8,8 @@ from pyipma.api import IPMA_API from pyipma.location import Location import voluptuous as vol -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -25,7 +25,7 @@ class IpmaFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" errors = {} diff --git a/homeassistant/components/ipma/const.py b/homeassistant/components/ipma/const.py index 26fdee779b6..dd6f1fba64a 100644 --- a/homeassistant/components/ipma/const.py +++ b/homeassistant/components/ipma/const.py @@ -1,4 +1,5 @@ """Constants for IPMA component.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/ipma/diagnostics.py b/homeassistant/components/ipma/diagnostics.py new file mode 100644 index 00000000000..948b69ee3e5 --- /dev/null +++ b/homeassistant/components/ipma/diagnostics.py @@ -0,0 +1,33 @@ +"""Diagnostics support for IPMA.""" + +from __future__ import annotations + +from typing import Any + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE +from homeassistant.core import HomeAssistant + +from .const import DATA_API, DATA_LOCATION, DOMAIN + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + + location = hass.data[DOMAIN][entry.entry_id][DATA_LOCATION] + api = hass.data[DOMAIN][entry.entry_id][DATA_API] + + return { + "location_information": { + "latitude": round(float(entry.data[CONF_LATITUDE]), 3), + "longitude": round(float(entry.data[CONF_LONGITUDE]), 3), + "global_id_local": location.global_id_local, + "id_station": location.id_station, + "name": location.name, + "station": location.station, + }, + "current_weather": await location.observation(api), + "weather_forecast": await location.forecast(api, 1), + } diff --git a/homeassistant/components/ipma/entity.py b/homeassistant/components/ipma/entity.py index 7eb8e2fe1a7..ef9401fcb07 100644 --- a/homeassistant/components/ipma/entity.py +++ b/homeassistant/components/ipma/entity.py @@ -1,4 +1,5 @@ """Base Entity for IPMA.""" + from __future__ import annotations from pyipma.api import IPMA_API diff --git a/homeassistant/components/ipma/sensor.py b/homeassistant/components/ipma/sensor.py index 99e994069a5..5f2cb98646b 100644 --- a/homeassistant/components/ipma/sensor.py +++ b/homeassistant/components/ipma/sensor.py @@ -1,4 +1,5 @@ """Support for IPMA sensors.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/ipma/system_health.py b/homeassistant/components/ipma/system_health.py index cd783490c35..7b6a5c517c7 100644 --- a/homeassistant/components/ipma/system_health.py +++ b/homeassistant/components/ipma/system_health.py @@ -1,4 +1,5 @@ """Provide info to system health.""" + from homeassistant.components import system_health from homeassistant.core import HomeAssistant, callback diff --git a/homeassistant/components/ipma/weather.py b/homeassistant/components/ipma/weather.py index 866f44f0617..ff6d8c3e86c 100644 --- a/homeassistant/components/ipma/weather.py +++ b/homeassistant/components/ipma/weather.py @@ -1,4 +1,5 @@ """Support for IPMA weather service.""" + from __future__ import annotations import asyncio @@ -204,13 +205,6 @@ class IPMAWeather(WeatherEntity, IPMADevice): for data_in in forecast ] - @property - def forecast(self) -> list[Forecast]: - """Return the forecast array.""" - return self._forecast( - self._hourly_forecast if self._period == 1 else self._daily_forecast - ) - async def _try_update_forecast( self, forecast_type: Literal["daily", "hourly"], diff --git a/homeassistant/components/ipp/__init__.py b/homeassistant/components/ipp/__init__.py index 98870c44f5a..10f24a1499d 100644 --- a/homeassistant/components/ipp/__init__.py +++ b/homeassistant/components/ipp/__init__.py @@ -1,4 +1,5 @@ """The Internet Printing Protocol (IPP) integration.""" + from __future__ import annotations from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/ipp/config_flow.py b/homeassistant/components/ipp/config_flow.py index dfe6c0b2127..ecd4d1af9f6 100644 --- a/homeassistant/components/ipp/config_flow.py +++ b/homeassistant/components/ipp/config_flow.py @@ -1,4 +1,5 @@ """Config flow to configure the IPP integration.""" + from __future__ import annotations import logging @@ -16,7 +17,7 @@ from pyipp import ( import voluptuous as vol from homeassistant.components import zeroconf -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import ( CONF_HOST, CONF_NAME, @@ -26,7 +27,6 @@ from homeassistant.const import ( CONF_VERIFY_SSL, ) from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import CONF_BASE_PATH, CONF_SERIAL, DOMAIN @@ -65,7 +65,7 @@ class IPPFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initiated by the user.""" if user_input is None: return self._show_setup_form() @@ -104,7 +104,7 @@ class IPPFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_zeroconf( self, discovery_info: zeroconf.ZeroconfServiceInfo - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle zeroconf discovery.""" host = discovery_info.host @@ -190,7 +190,7 @@ class IPPFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_zeroconf_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a confirmation flow initiated by zeroconf.""" if user_input is None: return self.async_show_form( @@ -204,7 +204,7 @@ class IPPFlowHandler(ConfigFlow, domain=DOMAIN): data=self.discovery_info, ) - def _show_setup_form(self, errors: dict | None = None) -> FlowResult: + def _show_setup_form(self, errors: dict | None = None) -> ConfigFlowResult: """Show the setup form to the user.""" return self.async_show_form( step_id="user", diff --git a/homeassistant/components/ipp/coordinator.py b/homeassistant/components/ipp/coordinator.py index 8eb8c972fab..535b18bcaf0 100644 --- a/homeassistant/components/ipp/coordinator.py +++ b/homeassistant/components/ipp/coordinator.py @@ -1,4 +1,5 @@ """Coordinator for The Internet Printing Protocol (IPP) integration.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/ipp/entity.py b/homeassistant/components/ipp/entity.py index 05adf711fd9..fdaa4cf035e 100644 --- a/homeassistant/components/ipp/entity.py +++ b/homeassistant/components/ipp/entity.py @@ -1,4 +1,5 @@ """Entities for The Internet Printing Protocol (IPP) integration.""" + from __future__ import annotations from homeassistant.helpers.device_registry import DeviceInfo diff --git a/homeassistant/components/ipp/icons.json b/homeassistant/components/ipp/icons.json new file mode 100644 index 00000000000..08abebf674d --- /dev/null +++ b/homeassistant/components/ipp/icons.json @@ -0,0 +1,15 @@ +{ + "entity": { + "sensor": { + "printer": { + "default": "mdi:printer" + }, + "uptime": { + "default": "mdi:clock-outline" + }, + "marker": { + "default": "mdi:water" + } + } + } +} diff --git a/homeassistant/components/ipp/sensor.py b/homeassistant/components/ipp/sensor.py index d1acbe9bd96..1aad6ae6b21 100644 --- a/homeassistant/components/ipp/sensor.py +++ b/homeassistant/components/ipp/sensor.py @@ -1,4 +1,5 @@ """Support for IPP sensors.""" + from __future__ import annotations from collections.abc import Callable @@ -37,19 +38,11 @@ from .coordinator import IPPDataUpdateCoordinator from .entity import IPPEntity -@dataclass(frozen=True) -class IPPSensorEntityDescriptionMixin: - """Mixin for required keys.""" - - value_fn: Callable[[Printer], StateType | datetime] - - -@dataclass(frozen=True) -class IPPSensorEntityDescription( - SensorEntityDescription, IPPSensorEntityDescriptionMixin -): +@dataclass(frozen=True, kw_only=True) +class IPPSensorEntityDescription(SensorEntityDescription): """Describes IPP sensor entity.""" + value_fn: Callable[[Printer], StateType | datetime] attributes_fn: Callable[[Printer], dict[Any, StateType]] = lambda _: {} @@ -70,7 +63,6 @@ PRINTER_SENSORS: tuple[IPPSensorEntityDescription, ...] = ( key="printer", name=None, translation_key="printer", - icon="mdi:printer", device_class=SensorDeviceClass.ENUM, options=["idle", "printing", "stopped"], attributes_fn=lambda printer: { @@ -87,7 +79,6 @@ PRINTER_SENSORS: tuple[IPPSensorEntityDescription, ...] = ( IPPSensorEntityDescription( key="uptime", translation_key="uptime", - icon="mdi:clock-outline", device_class=SensorDeviceClass.TIMESTAMP, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -118,7 +109,7 @@ async def async_setup_entry( IPPSensorEntityDescription( key=f"marker_{index}", name=marker.name, - icon="mdi:water", + translation_key="marker", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, attributes_fn=_get_marker_attributes_fn( diff --git a/homeassistant/components/iqvia/__init__.py b/homeassistant/components/iqvia/__init__.py index aa5528cc06a..eef7f929cab 100644 --- a/homeassistant/components/iqvia/__init__.py +++ b/homeassistant/components/iqvia/__init__.py @@ -1,4 +1,5 @@ """Support for IQVIA.""" + from __future__ import annotations import asyncio @@ -46,7 +47,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if not entry.unique_id: # If the config entry doesn't already have a unique ID, set one: hass.config_entries.async_update_entry( - entry, **{"unique_id": entry.data[CONF_ZIP_CODE]} + entry, unique_id=entry.data[CONF_ZIP_CODE] ) websession = aiohttp_client.async_get_clientsession(hass) @@ -91,7 +92,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # The IQVIA API can be selectively flaky, meaning that any number of the setup # API calls could fail. We only retry integration setup if *all* of the initial # API calls fail: - raise ConfigEntryNotReady() + raise ConfigEntryNotReady # Once we've successfully authenticated, we re-enable client request retries: client.enable_request_retries() diff --git a/homeassistant/components/iqvia/config_flow.py b/homeassistant/components/iqvia/config_flow.py index 32ce64014d7..444d86a7fb8 100644 --- a/homeassistant/components/iqvia/config_flow.py +++ b/homeassistant/components/iqvia/config_flow.py @@ -1,4 +1,5 @@ """Config flow to configure the IQVIA component.""" + from __future__ import annotations from typing import Any @@ -7,14 +8,13 @@ from pyiqvia import Client from pyiqvia.errors import InvalidZipError import voluptuous as vol -from homeassistant import config_entries -from homeassistant.data_entry_flow import FlowResult +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.helpers import aiohttp_client from .const import CONF_ZIP_CODE, DOMAIN -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class IqviaConfigFlow(ConfigFlow, domain=DOMAIN): """Handle an IQVIA config flow.""" VERSION = 1 @@ -25,7 +25,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the start of the config flow.""" if not user_input: return self.async_show_form(step_id="user", data_schema=self.data_schema) diff --git a/homeassistant/components/iqvia/const.py b/homeassistant/components/iqvia/const.py index 3ed961f2e74..4c4ad5c06ba 100644 --- a/homeassistant/components/iqvia/const.py +++ b/homeassistant/components/iqvia/const.py @@ -1,4 +1,5 @@ """Define IQVIA constants.""" + import logging LOGGER = logging.getLogger(__package__) diff --git a/homeassistant/components/iqvia/diagnostics.py b/homeassistant/components/iqvia/diagnostics.py index 6f2df6bd7d3..64827f183ff 100644 --- a/homeassistant/components/iqvia/diagnostics.py +++ b/homeassistant/components/iqvia/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for IQVIA.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/iqvia/sensor.py b/homeassistant/components/iqvia/sensor.py index d17a278a106..ba3c288b702 100644 --- a/homeassistant/components/iqvia/sensor.py +++ b/homeassistant/components/iqvia/sensor.py @@ -1,4 +1,5 @@ """Support for IQVIA sensors.""" + from __future__ import annotations from statistics import mean @@ -211,12 +212,12 @@ class ForecastSensor(IQVIAEntity, SensorEntity): if not outlook_coordinator.last_update_success: return - self._attr_extra_state_attributes[ - ATTR_OUTLOOK - ] = outlook_coordinator.data.get("Outlook") - self._attr_extra_state_attributes[ - ATTR_SEASON - ] = outlook_coordinator.data.get("Season") + self._attr_extra_state_attributes[ATTR_OUTLOOK] = ( + outlook_coordinator.data.get("Outlook") + ) + self._attr_extra_state_attributes[ATTR_SEASON] = ( + outlook_coordinator.data.get("Season") + ) class IndexSensor(IQVIAEntity, SensorEntity): @@ -282,8 +283,8 @@ class IndexSensor(IQVIAEntity, SensorEntity): ) elif self.entity_description.key == TYPE_DISEASE_TODAY: for attrs in period["Triggers"]: - self._attr_extra_state_attributes[ - f"{attrs['Name'].lower()}_index" - ] = attrs["Index"] + self._attr_extra_state_attributes[f"{attrs['Name'].lower()}_index"] = ( + attrs["Index"] + ) self._attr_native_value = period["Index"] diff --git a/homeassistant/components/irish_rail_transport/sensor.py b/homeassistant/components/irish_rail_transport/sensor.py index 70b53b80d9c..b0ad9372f86 100644 --- a/homeassistant/components/irish_rail_transport/sensor.py +++ b/homeassistant/components/irish_rail_transport/sensor.py @@ -1,4 +1,5 @@ """Support for Irish Rail RTPI information.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/islamic_prayer_times/__init__.py b/homeassistant/components/islamic_prayer_times/__init__.py index 55e5618d9d4..15e165d2f48 100644 --- a/homeassistant/components/islamic_prayer_times/__init__.py +++ b/homeassistant/components/islamic_prayer_times/__init__.py @@ -1,4 +1,5 @@ """The islamic_prayer_times component.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/islamic_prayer_times/config_flow.py b/homeassistant/components/islamic_prayer_times/config_flow.py index 73696572593..12730c9be08 100644 --- a/homeassistant/components/islamic_prayer_times/config_flow.py +++ b/homeassistant/components/islamic_prayer_times/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Islamic Prayer Times integration.""" + from __future__ import annotations from typing import Any @@ -7,10 +8,14 @@ from prayer_times_calculator import InvalidResponseError, PrayerTimesCalculator from requests.exceptions import ConnectionError as ConnError import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.const import CONF_LATITUDE, CONF_LOCATION, CONF_LONGITUDE, CONF_NAME from homeassistant.core import HomeAssistant, callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.selector import ( LocationSelector, SelectSelector, @@ -58,7 +63,7 @@ async def async_validate_location( return errors -class IslamicPrayerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class IslamicPrayerFlowHandler(ConfigFlow, domain=DOMAIN): """Handle the Islamic Prayer config flow.""" VERSION = 1 @@ -67,14 +72,14 @@ class IslamicPrayerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback def async_get_options_flow( - config_entry: config_entries.ConfigEntry, + config_entry: ConfigEntry, ) -> IslamicPrayerOptionsFlowHandler: """Get the options flow for this handler.""" return IslamicPrayerOptionsFlowHandler(config_entry) async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" errors = {} @@ -111,16 +116,16 @@ class IslamicPrayerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) -class IslamicPrayerOptionsFlowHandler(config_entries.OptionsFlow): +class IslamicPrayerOptionsFlowHandler(OptionsFlow): """Handle Islamic Prayer client options.""" - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + 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: + ) -> ConfigFlowResult: """Manage options.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) diff --git a/homeassistant/components/islamic_prayer_times/const.py b/homeassistant/components/islamic_prayer_times/const.py index 926651738a2..dc4237e5efa 100644 --- a/homeassistant/components/islamic_prayer_times/const.py +++ b/homeassistant/components/islamic_prayer_times/const.py @@ -1,4 +1,5 @@ """Constants for the Islamic Prayer component.""" + from typing import Final DOMAIN: Final = "islamic_prayer_times" diff --git a/homeassistant/components/islamic_prayer_times/coordinator.py b/homeassistant/components/islamic_prayer_times/coordinator.py index be138e7b45b..d70d0e2f4fe 100644 --- a/homeassistant/components/islamic_prayer_times/coordinator.py +++ b/homeassistant/components/islamic_prayer_times/coordinator.py @@ -1,4 +1,5 @@ """Coordinator for the Islamic prayer times integration.""" + from __future__ import annotations from datetime import datetime, timedelta diff --git a/homeassistant/components/islamic_prayer_times/manifest.json b/homeassistant/components/islamic_prayer_times/manifest.json index 7d2dd178788..5f7e52dd3db 100644 --- a/homeassistant/components/islamic_prayer_times/manifest.json +++ b/homeassistant/components/islamic_prayer_times/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/islamic_prayer_times", "iot_class": "cloud_polling", "loggers": ["prayer_times_calculator"], - "requirements": ["prayer-times-calculator==0.0.10"] + "requirements": ["prayer-times-calculator==0.0.12"] } diff --git a/homeassistant/components/islamic_prayer_times/sensor.py b/homeassistant/components/islamic_prayer_times/sensor.py index 70b2c9d9cc6..eb042d83c49 100644 --- a/homeassistant/components/islamic_prayer_times/sensor.py +++ b/homeassistant/components/islamic_prayer_times/sensor.py @@ -1,4 +1,5 @@ """Platform to retrieve Islamic prayer times information for Home Assistant.""" + from datetime import datetime from homeassistant.components.sensor import ( diff --git a/homeassistant/components/iss/__init__.py b/homeassistant/components/iss/__init__.py index 640e9d5d1da..606263ce769 100644 --- a/homeassistant/components/iss/__init__.py +++ b/homeassistant/components/iss/__init__.py @@ -1,4 +1,5 @@ """The iss component.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/homeassistant/components/iss/config_flow.py b/homeassistant/components/iss/config_flow.py index f8ebd9db723..80644698239 100644 --- a/homeassistant/components/iss/config_flow.py +++ b/homeassistant/components/iss/config_flow.py @@ -2,15 +2,19 @@ import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.const import CONF_SHOW_ON_MAP from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult from .const import DEFAULT_NAME, DOMAIN -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class ISSConfigFlow(ConfigFlow, domain=DOMAIN): """Config flow for iss component.""" VERSION = 1 @@ -18,12 +22,12 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback def async_get_options_flow( - config_entry: config_entries.ConfigEntry, - ) -> config_entries.OptionsFlow: + config_entry: ConfigEntry, + ) -> OptionsFlow: """Get the options flow for this handler.""" return OptionsFlowHandler(config_entry) - async def async_step_user(self, user_input=None) -> FlowResult: + async def async_step_user(self, user_input=None) -> ConfigFlowResult: """Handle a flow initialized by the user.""" # Check if already configured if self._async_current_entries(): @@ -39,15 +43,15 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form(step_id="user") -class OptionsFlowHandler(config_entries.OptionsFlow): +class OptionsFlowHandler(OptionsFlow): """Config flow options handler for iss.""" - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + def __init__(self, config_entry: ConfigEntry) -> None: """Initialize options flow.""" self.config_entry = config_entry self.options = dict(config_entry.options) - async def async_step_init(self, user_input=None) -> FlowResult: + async def async_step_init(self, user_input=None) -> ConfigFlowResult: """Manage the options.""" if user_input is not None: self.options.update(user_input) diff --git a/homeassistant/components/iss/sensor.py b/homeassistant/components/iss/sensor.py index d7b7083cdef..f4f91f0099e 100644 --- a/homeassistant/components/iss/sensor.py +++ b/homeassistant/components/iss/sensor.py @@ -1,4 +1,5 @@ """Support for iss sensor.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/isy994/__init__.py b/homeassistant/components/isy994/__init__.py index 0c5ea27a0b9..db72cc45a30 100644 --- a/homeassistant/components/isy994/__init__.py +++ b/homeassistant/components/isy994/__init__.py @@ -1,4 +1,5 @@ """Support the Universal Devices ISY/IoX controllers.""" + from __future__ import annotations import asyncio @@ -165,7 +166,9 @@ async def async_setup_entry( entry.async_on_unload(entry.add_update_listener(_async_update_listener)) entry.async_on_unload( - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_stop_auto_update) + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, _async_stop_auto_update, run_immediately=True + ) ) # Register Integration-wide Services: diff --git a/homeassistant/components/isy994/binary_sensor.py b/homeassistant/components/isy994/binary_sensor.py index 7be3b87a0d3..c130ba32746 100644 --- a/homeassistant/components/isy994/binary_sensor.py +++ b/homeassistant/components/isy994/binary_sensor.py @@ -1,4 +1,5 @@ """Support for ISY binary sensors.""" + from __future__ import annotations from datetime import datetime, timedelta diff --git a/homeassistant/components/isy994/button.py b/homeassistant/components/isy994/button.py index 6e00e1934f2..b3b6aa40503 100644 --- a/homeassistant/components/isy994/button.py +++ b/homeassistant/components/isy994/button.py @@ -1,4 +1,5 @@ """Representation of ISY/IoX buttons.""" + from __future__ import annotations from pyisy import ISY @@ -37,7 +38,15 @@ async def async_setup_entry( ISYNodeQueryButtonEntity | ISYNodeBeepButtonEntity | ISYNetworkResourceButtonEntity - ] = [] + ] = [ + ISYNetworkResourceButtonEntity( + node=node, + name=node.name, + unique_id=isy_data.uid_base(node), + device_info=device_info[CONF_NETWORK], + ) + for node in isy_data.net_resources + ] for node in isy_data.root_nodes[Platform.BUTTON]: entities.append( @@ -60,16 +69,6 @@ async def async_setup_entry( ) ) - for node in isy_data.net_resources: - entities.append( - ISYNetworkResourceButtonEntity( - node=node, - name=node.name, - unique_id=isy_data.uid_base(node), - device_info=device_info[CONF_NETWORK], - ) - ) - # Add entity to query full system entities.append( ISYNodeQueryButtonEntity( diff --git a/homeassistant/components/isy994/climate.py b/homeassistant/components/isy994/climate.py index 06b73978456..d4376b5a3b4 100644 --- a/homeassistant/components/isy994/climate.py +++ b/homeassistant/components/isy994/climate.py @@ -1,4 +1,5 @@ """Support for Insteon Thermostats via ISY Platform.""" + from __future__ import annotations from typing import Any @@ -63,14 +64,14 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the ISY thermostat platform.""" - entities = [] isy_data: IsyData = hass.data[DOMAIN][entry.entry_id] devices: dict[str, DeviceInfo] = isy_data.devices - for node in isy_data.nodes[Platform.CLIMATE]: - entities.append(ISYThermostatEntity(node, devices.get(node.primary_node))) - async_add_entities(entities) + async_add_entities( + ISYThermostatEntity(node, devices.get(node.primary_node)) + for node in isy_data.nodes[Platform.CLIMATE] + ) class ISYThermostatEntity(ISYNodeEntity, ClimateEntity): diff --git a/homeassistant/components/isy994/config_flow.py b/homeassistant/components/isy994/config_flow.py index 2cdfd1df16d..639e591746d 100644 --- a/homeassistant/components/isy994/config_flow.py +++ b/homeassistant/components/isy994/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Universal Devices ISY/IoX integration.""" + from __future__ import annotations import asyncio @@ -13,11 +14,18 @@ from pyisy.configuration import Configuration from pyisy.connection import Connection import voluptuous as vol -from homeassistant import config_entries, core, exceptions from homeassistant.components import dhcp, ssdp +from homeassistant.config_entries import ( + SOURCE_IGNORE, + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME -from homeassistant.core import callback -from homeassistant.data_entry_flow import AbortFlow, FlowResult +from homeassistant.core import HomeAssistant, callback +from homeassistant.data_entry_flow import AbortFlow +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import aiohttp_client from .const import ( @@ -58,9 +66,7 @@ def _data_schema(schema_input: dict[str, str]) -> vol.Schema: ) -async def validate_input( - hass: core.HomeAssistant, data: dict[str, Any] -) -> dict[str, str]: +async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, str]: """Validate the user input allows us to connect. Data has the keys from DATA_SCHEMA with values provided by the user. @@ -118,7 +124,7 @@ async def validate_input( } -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class Isy994ConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Universal Devices ISY/IoX.""" VERSION = 1 @@ -126,19 +132,19 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize the ISY/IoX config flow.""" self.discovered_conf: dict[str, str] = {} - self._existing_entry: config_entries.ConfigEntry | None = None + self._existing_entry: ConfigEntry | None = None @staticmethod @callback def async_get_options_flow( - config_entry: config_entries.ConfigEntry, - ) -> config_entries.OptionsFlow: + config_entry: ConfigEntry, + ) -> OptionsFlow: """Get the options flow for this handler.""" return OptionsFlowHandler(config_entry) async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors = {} info: dict[str, str] = {} @@ -175,7 +181,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): existing_entry = await self.async_set_unique_id(isy_mac) if not existing_entry: return - if existing_entry.source == config_entries.SOURCE_IGNORE: + if existing_entry.source == SOURCE_IGNORE: raise AbortFlow("already_configured") parsed_url = urlparse(existing_entry.data[CONF_HOST]) if parsed_url.hostname != ip_address: @@ -202,10 +208,12 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) raise AbortFlow("already_configured") - async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: + async def async_step_dhcp( + self, discovery_info: dhcp.DhcpServiceInfo + ) -> ConfigFlowResult: """Handle a discovered ISY/IoX device via dhcp.""" friendly_name = discovery_info.hostname - if friendly_name.startswith("polisy") or friendly_name.startswith("eisy"): + if friendly_name.startswith(("polisy", "eisy")): url = f"http://{discovery_info.ip}:8080" else: url = f"http://{discovery_info.ip}" @@ -223,7 +231,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.context["title_placeholders"] = self.discovered_conf return await self.async_step_user() - async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: + async def async_step_ssdp( + self, discovery_info: ssdp.SsdpServiceInfo + ) -> ConfigFlowResult: """Handle a discovered ISY/IoX Device.""" friendly_name = discovery_info.upnp[ssdp.ATTR_UPNP_FRIENDLY_NAME] url = discovery_info.ssdp_location @@ -250,14 +260,16 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.context["title_placeholders"] = self.discovered_conf return await self.async_step_user() - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Handle reauth.""" self._existing_entry = await self.async_set_unique_id(self.context["unique_id"]) return await self.async_step_reauth_confirm() async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle reauth input.""" errors = {} assert self._existing_entry is not None @@ -299,16 +311,16 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -class OptionsFlowHandler(config_entries.OptionsFlow): +class OptionsFlowHandler(OptionsFlow): """Handle a option flow for ISY/IoX.""" - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + 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: + ) -> ConfigFlowResult: """Handle options flow.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) @@ -337,13 +349,13 @@ class OptionsFlowHandler(config_entries.OptionsFlow): return self.async_show_form(step_id="init", data_schema=options_schema) -class InvalidHost(exceptions.HomeAssistantError): +class InvalidHost(HomeAssistantError): """Error to indicate the host value is invalid.""" -class CannotConnect(exceptions.HomeAssistantError): +class CannotConnect(HomeAssistantError): """Error to indicate we cannot connect.""" -class InvalidAuth(exceptions.HomeAssistantError): +class InvalidAuth(HomeAssistantError): """Error to indicate there is invalid auth.""" diff --git a/homeassistant/components/isy994/const.py b/homeassistant/components/isy994/const.py index 70f0f49d7a1..85ecafd6490 100644 --- a/homeassistant/components/isy994/const.py +++ b/homeassistant/components/isy994/const.py @@ -1,4 +1,5 @@ """Constants for the ISY Platform.""" + import logging from pyisy.constants import PROP_ON_LEVEL, PROP_RAMP_RATE @@ -213,19 +214,21 @@ NODE_FILTERS: dict[Platform, dict[str, list[str]]] = { "7.13.", TYPE_CATEGORY_SAFETY, ], # Does a startswith() match; include the dot - FILTER_ZWAVE_CAT: (["104", "112", "138"] + list(map(str, range(148, 180)))), + FILTER_ZWAVE_CAT: (["104", "112", "138", *map(str, range(148, 180))]), }, Platform.SENSOR: { # This is just a more-readable way of including MOST uoms between 1-100 # (Remember that range() is non-inclusive of the stop value) FILTER_UOM: ( - ["1"] - + list(map(str, range(3, 11))) - + list(map(str, range(12, 51))) - + list(map(str, range(52, 66))) - + list(map(str, range(69, 78))) - + ["79"] - + list(map(str, range(82, 97))) + [ + "1", + *map(str, range(3, 11)), + *map(str, range(12, 51)), + *map(str, range(52, 66)), + *map(str, range(69, 78)), + "79", + *map(str, range(82, 97)), + ] ), FILTER_STATES: [], FILTER_NODE_DEF_ID: [ @@ -237,7 +240,7 @@ NODE_FILTERS: dict[Platform, dict[str, list[str]]] = { "RemoteLinc2_ADV", ], FILTER_INSTEON_TYPE: ["0.16.", "0.17.", "0.18.", "9.0.", "9.7."], - FILTER_ZWAVE_CAT: (["118", "143"] + list(map(str, range(180, 186)))), + FILTER_ZWAVE_CAT: (["118", "143", *map(str, range(180, 186))]), }, Platform.LOCK: { FILTER_UOM: ["11"], @@ -593,14 +596,12 @@ UOM_TO_STATES = { 4: "highly polluted", }, UOM_BARRIER: { # Barrier Status - **{ - 0: STATE_CLOSED, - 100: STATE_OPEN, - 101: STATE_UNKNOWN, - 102: "stopped", - 103: STATE_CLOSING, - 104: STATE_OPENING, - }, + 0: STATE_CLOSED, + 100: STATE_OPEN, + 101: STATE_UNKNOWN, + 102: "stopped", + 103: STATE_CLOSING, + 104: STATE_OPENING, **{ b: f"{b} %" for a, b in enumerate(list(range(1, 100))) }, # 1-99 are percentage open diff --git a/homeassistant/components/isy994/cover.py b/homeassistant/components/isy994/cover.py index 2ada6339295..b9d7ec44d27 100644 --- a/homeassistant/components/isy994/cover.py +++ b/homeassistant/components/isy994/cover.py @@ -1,4 +1,5 @@ """Support for ISY covers.""" + from __future__ import annotations from typing import Any, cast @@ -26,13 +27,16 @@ async def async_setup_entry( ) -> None: """Set up the ISY cover platform.""" isy_data: IsyData = hass.data[DOMAIN][entry.entry_id] - entities: list[ISYCoverEntity | ISYCoverProgramEntity] = [] devices: dict[str, DeviceInfo] = isy_data.devices - for node in isy_data.nodes[Platform.COVER]: - entities.append(ISYCoverEntity(node, devices.get(node.primary_node))) + entities: list[ISYCoverEntity | ISYCoverProgramEntity] = [ + ISYCoverEntity(node, devices.get(node.primary_node)) + for node in isy_data.nodes[Platform.COVER] + ] - for name, status, actions in isy_data.programs[Platform.COVER]: - entities.append(ISYCoverProgramEntity(name, status, actions)) + entities.extend( + ISYCoverProgramEntity(name, status, actions) + for name, status, actions in isy_data.programs[Platform.COVER] + ) async_add_entities(entities) diff --git a/homeassistant/components/isy994/entity.py b/homeassistant/components/isy994/entity.py index a93f2d91d31..893b33644fe 100644 --- a/homeassistant/components/isy994/entity.py +++ b/homeassistant/components/isy994/entity.py @@ -1,4 +1,5 @@ """Representation of ISYEntity Types.""" + from __future__ import annotations from typing import Any, cast diff --git a/homeassistant/components/isy994/fan.py b/homeassistant/components/isy994/fan.py index ebdef4146e0..da920540476 100644 --- a/homeassistant/components/isy994/fan.py +++ b/homeassistant/components/isy994/fan.py @@ -1,4 +1,5 @@ """Support for ISY fans.""" + from __future__ import annotations import math @@ -31,13 +32,15 @@ async def async_setup_entry( """Set up the ISY fan platform.""" isy_data: IsyData = hass.data[DOMAIN][entry.entry_id] devices: dict[str, DeviceInfo] = isy_data.devices - entities: list[ISYFanEntity | ISYFanProgramEntity] = [] + entities: list[ISYFanEntity | ISYFanProgramEntity] = [ + ISYFanEntity(node, devices.get(node.primary_node)) + for node in isy_data.nodes[Platform.FAN] + ] - for node in isy_data.nodes[Platform.FAN]: - entities.append(ISYFanEntity(node, devices.get(node.primary_node))) - - for name, status, actions in isy_data.programs[Platform.FAN]: - entities.append(ISYFanProgramEntity(name, status, actions)) + entities.extend( + ISYFanProgramEntity(name, status, actions) + for name, status, actions in isy_data.programs[Platform.FAN] + ) async_add_entities(entities) diff --git a/homeassistant/components/isy994/helpers.py b/homeassistant/components/isy994/helpers.py index 5e0ff592ea9..8b6a4249931 100644 --- a/homeassistant/components/isy994/helpers.py +++ b/homeassistant/components/isy994/helpers.py @@ -1,4 +1,5 @@ """Sorting helpers for ISY device classifications.""" + from __future__ import annotations from typing import cast @@ -316,9 +317,9 @@ def _generate_device_info(node: Node) -> DeviceInfo: and node.zwave_props and node.zwave_props.mfr_id != "0" ): - device_info[ - ATTR_MANUFACTURER - ] = f"Z-Wave MfrID:{int(node.zwave_props.mfr_id):#0{6}x}" + device_info[ATTR_MANUFACTURER] = ( + f"Z-Wave MfrID:{int(node.zwave_props.mfr_id):#0{6}x}" + ) model += ( f"Type:{int(node.zwave_props.prod_type_id):#0{6}x} " f"Product:{int(node.zwave_props.product_id):#0{6}x}" diff --git a/homeassistant/components/isy994/icons.json b/homeassistant/components/isy994/icons.json new file mode 100644 index 00000000000..27b2ea6954e --- /dev/null +++ b/homeassistant/components/isy994/icons.json @@ -0,0 +1,12 @@ +{ + "services": { + "send_raw_node_command": "mdi:console-line", + "send_node_command": "mdi:console", + "get_zwave_parameter": "mdi:download", + "set_zwave_parameter": "mdi:upload", + "set_zwave_lock_user_code": "mdi:upload-lock", + "delete_zwave_lock_user_code": "mdi:lock-remove", + "rename_node": "mdi:pencil", + "send_program_command": "mdi:console" + } +} diff --git a/homeassistant/components/isy994/light.py b/homeassistant/components/isy994/light.py index b16b4ca5a83..69701534840 100644 --- a/homeassistant/components/isy994/light.py +++ b/homeassistant/components/isy994/light.py @@ -1,4 +1,5 @@ """Support for ISY lights.""" + from __future__ import annotations from typing import Any, cast @@ -31,13 +32,10 @@ async def async_setup_entry( isy_options = entry.options restore_light_state = isy_options.get(CONF_RESTORE_LIGHT_STATE, False) - entities = [] - for node in isy_data.nodes[Platform.LIGHT]: - entities.append( - ISYLightEntity(node, restore_light_state, devices.get(node.primary_node)) - ) - - async_add_entities(entities) + async_add_entities( + ISYLightEntity(node, restore_light_state, devices.get(node.primary_node)) + for node in isy_data.nodes[Platform.LIGHT] + ) class ISYLightEntity(ISYNodeEntity, LightEntity, RestoreEntity): diff --git a/homeassistant/components/isy994/lock.py b/homeassistant/components/isy994/lock.py index 67c2587a238..dc2da2a6ee2 100644 --- a/homeassistant/components/isy994/lock.py +++ b/homeassistant/components/isy994/lock.py @@ -1,4 +1,5 @@ """Support for ISY locks.""" + from __future__ import annotations from typing import Any @@ -52,12 +53,15 @@ async def async_setup_entry( """Set up the ISY lock platform.""" isy_data: IsyData = hass.data[DOMAIN][entry.entry_id] devices: dict[str, DeviceInfo] = isy_data.devices - entities: list[ISYLockEntity | ISYLockProgramEntity] = [] - for node in isy_data.nodes[Platform.LOCK]: - entities.append(ISYLockEntity(node, devices.get(node.primary_node))) + entities: list[ISYLockEntity | ISYLockProgramEntity] = [ + ISYLockEntity(node, devices.get(node.primary_node)) + for node in isy_data.nodes[Platform.LOCK] + ] - for name, status, actions in isy_data.programs[Platform.LOCK]: - entities.append(ISYLockProgramEntity(name, status, actions)) + entities.extend( + ISYLockProgramEntity(name, status, actions) + for name, status, actions in isy_data.programs[Platform.LOCK] + ) async_add_entities(entities) async_setup_lock_services(hass) diff --git a/homeassistant/components/isy994/manifest.json b/homeassistant/components/isy994/manifest.json index 8c9815cd425..3aa81027b4f 100644 --- a/homeassistant/components/isy994/manifest.json +++ b/homeassistant/components/isy994/manifest.json @@ -21,7 +21,6 @@ } ], "documentation": "https://www.home-assistant.io/integrations/isy994", - "import_executor": true, "integration_type": "hub", "iot_class": "local_push", "loggers": ["pyisy"], diff --git a/homeassistant/components/isy994/models.py b/homeassistant/components/isy994/models.py index c8a7e1dbefe..5b599df9458 100644 --- a/homeassistant/components/isy994/models.py +++ b/homeassistant/components/isy994/models.py @@ -1,4 +1,5 @@ """The ISY/IoX integration data models.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/homeassistant/components/isy994/number.py b/homeassistant/components/isy994/number.py index baadf3b2dc7..c8feba1bf8d 100644 --- a/homeassistant/components/isy994/number.py +++ b/homeassistant/components/isy994/number.py @@ -1,4 +1,5 @@ """Support for ISY number entities.""" + from __future__ import annotations from dataclasses import replace diff --git a/homeassistant/components/isy994/select.py b/homeassistant/components/isy994/select.py index 3c55e5cbda9..41e5899504d 100644 --- a/homeassistant/components/isy994/select.py +++ b/homeassistant/components/isy994/select.py @@ -1,4 +1,5 @@ """Support for ISY select entities.""" + from __future__ import annotations from typing import cast diff --git a/homeassistant/components/isy994/sensor.py b/homeassistant/components/isy994/sensor.py index 9e39f5d04e4..94c333346af 100644 --- a/homeassistant/components/isy994/sensor.py +++ b/homeassistant/components/isy994/sensor.py @@ -1,4 +1,5 @@ """Support for ISY sensors.""" + from __future__ import annotations from typing import Any, cast diff --git a/homeassistant/components/isy994/services.py b/homeassistant/components/isy994/services.py index a6adfcfb917..fedf7f8e902 100644 --- a/homeassistant/components/isy994/services.py +++ b/homeassistant/components/isy994/services.py @@ -1,4 +1,5 @@ """ISY Services and Commands.""" + from __future__ import annotations from typing import Any @@ -130,7 +131,7 @@ def async_get_entities(hass: HomeAssistant) -> dict[str, Entity]: @callback -def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901 +def async_setup_services(hass: HomeAssistant) -> None: """Create and register services for the ISY integration.""" existing_services = hass.services.async_services_for_domain(DOMAIN) if existing_services and SERVICE_SEND_PROGRAM_COMMAND in existing_services: diff --git a/homeassistant/components/isy994/switch.py b/homeassistant/components/isy994/switch.py index da208dcc79c..391ad18e02f 100644 --- a/homeassistant/components/isy994/switch.py +++ b/homeassistant/components/isy994/switch.py @@ -1,4 +1,5 @@ """Support for ISY switches.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/homeassistant/components/isy994/system_health.py b/homeassistant/components/isy994/system_health.py index 44286111a62..dfc45c267dd 100644 --- a/homeassistant/components/isy994/system_health.py +++ b/homeassistant/components/isy994/system_health.py @@ -1,4 +1,5 @@ """Provide info to system health.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/isy994/util.py b/homeassistant/components/isy994/util.py index f4846b61aed..ed1a5abca8b 100644 --- a/homeassistant/components/isy994/util.py +++ b/homeassistant/components/isy994/util.py @@ -1,4 +1,5 @@ """ISY utils.""" + from __future__ import annotations from homeassistant.core import HomeAssistant, callback diff --git a/homeassistant/components/itach/remote.py b/homeassistant/components/itach/remote.py index c0dddfa080e..606ca4fd021 100644 --- a/homeassistant/components/itach/remote.py +++ b/homeassistant/components/itach/remote.py @@ -1,4 +1,5 @@ """Support for iTach IR devices.""" + from __future__ import annotations from collections.abc import Iterable diff --git a/homeassistant/components/itunes/media_player.py b/homeassistant/components/itunes/media_player.py index d73086f9ab1..13ad66f1417 100644 --- a/homeassistant/components/itunes/media_player.py +++ b/homeassistant/components/itunes/media_player.py @@ -1,4 +1,5 @@ """Support for interfacing to iTunes API.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/izone/__init__.py b/homeassistant/components/izone/__init__.py index fd8d27ac422..c00f2d1f83f 100644 --- a/homeassistant/components/izone/__init__.py +++ b/homeassistant/components/izone/__init__.py @@ -1,4 +1,5 @@ """Platform for the iZone AC.""" + import voluptuous as vol from homeassistant import config_entries diff --git a/homeassistant/components/izone/climate.py b/homeassistant/components/izone/climate.py index e85b7ef4d56..1786ef23522 100644 --- a/homeassistant/components/izone/climate.py +++ b/homeassistant/components/izone/climate.py @@ -1,4 +1,5 @@ """Support for the iZone HVAC.""" + from __future__ import annotations from collections.abc import Callable, Mapping diff --git a/homeassistant/components/izone/discovery.py b/homeassistant/components/izone/discovery.py index a170ed30a74..327c5c0dc85 100644 --- a/homeassistant/components/izone/discovery.py +++ b/homeassistant/components/izone/discovery.py @@ -1,4 +1,5 @@ """Internal discovery service for iZone AC.""" + import logging import pizone diff --git a/homeassistant/components/izone/icons.json b/homeassistant/components/izone/icons.json new file mode 100644 index 00000000000..e02cd57c141 --- /dev/null +++ b/homeassistant/components/izone/icons.json @@ -0,0 +1,6 @@ +{ + "services": { + "airflow_min": "mdi:fan-minus", + "airflow_max": "mdi:fan-plus" + } +} diff --git a/homeassistant/components/jellyfin/__init__.py b/homeassistant/components/jellyfin/__init__.py index 2e9e6bb71f7..c24f06d7b19 100644 --- a/homeassistant/components/jellyfin/__init__.py +++ b/homeassistant/components/jellyfin/__init__.py @@ -1,4 +1,5 @@ """The Jellyfin integration.""" + from typing import Any from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/jellyfin/browse_media.py b/homeassistant/components/jellyfin/browse_media.py index ac47bcf732f..2af2bac4875 100644 --- a/homeassistant/components/jellyfin/browse_media.py +++ b/homeassistant/components/jellyfin/browse_media.py @@ -1,4 +1,5 @@ """Support for media browsing.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/jellyfin/client_wrapper.py b/homeassistant/components/jellyfin/client_wrapper.py index ab771d405ea..ab5d5e7d7f8 100644 --- a/homeassistant/components/jellyfin/client_wrapper.py +++ b/homeassistant/components/jellyfin/client_wrapper.py @@ -1,4 +1,5 @@ """Utility methods for initializing a Jellyfin client.""" + from __future__ import annotations import socket diff --git a/homeassistant/components/jellyfin/config_flow.py b/homeassistant/components/jellyfin/config_flow.py index 84360ed053e..c6e447d18e8 100644 --- a/homeassistant/components/jellyfin/config_flow.py +++ b/homeassistant/components/jellyfin/config_flow.py @@ -1,4 +1,5 @@ """Config flow for the Jellyfin integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -7,9 +8,8 @@ from typing import Any import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME -from homeassistant.data_entry_flow import FlowResult from homeassistant.util.uuid import random_uuid_hex from .client_wrapper import CannotConnect, InvalidAuth, create_client, validate_input @@ -37,7 +37,7 @@ def _generate_client_device_id() -> str: return random_uuid_hex() -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class JellyfinConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Jellyfin.""" VERSION = 1 @@ -45,15 +45,12 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize the Jellyfin config flow.""" self.client_device_id: str | None = None - self.entry: config_entries.ConfigEntry | None = None + self.entry: ConfigEntry | None = None async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a user defined configuration.""" - if self._async_current_entries(): - return self.async_abort(reason="single_instance_allowed") - errors: dict[str, str] = {} if user_input is not None: @@ -92,14 +89,16 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors ) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Perform reauth upon an API authentication error.""" self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) return await self.async_step_reauth_confirm() async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Dialog that informs the user that reauth is required.""" errors: dict[str, str] = {} diff --git a/homeassistant/components/jellyfin/const.py b/homeassistant/components/jellyfin/const.py index fb8b4f15d82..764356e2ea6 100644 --- a/homeassistant/components/jellyfin/const.py +++ b/homeassistant/components/jellyfin/const.py @@ -1,4 +1,5 @@ """Constants for the Jellyfin integration.""" + import logging from typing import Final diff --git a/homeassistant/components/jellyfin/coordinator.py b/homeassistant/components/jellyfin/coordinator.py index f4ab98ca268..4d907ac1531 100644 --- a/homeassistant/components/jellyfin/coordinator.py +++ b/homeassistant/components/jellyfin/coordinator.py @@ -1,4 +1,5 @@ """Data update coordinator for the Jellyfin integration.""" + from __future__ import annotations from abc import ABC, abstractmethod diff --git a/homeassistant/components/jellyfin/diagnostics.py b/homeassistant/components/jellyfin/diagnostics.py index 36b2882fbeb..ecc66868bd0 100644 --- a/homeassistant/components/jellyfin/diagnostics.py +++ b/homeassistant/components/jellyfin/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for Jellyfin.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/jellyfin/entity.py b/homeassistant/components/jellyfin/entity.py index e45557fa4b6..2204a36dc61 100644 --- a/homeassistant/components/jellyfin/entity.py +++ b/homeassistant/components/jellyfin/entity.py @@ -1,4 +1,5 @@ """Base Entity for Jellyfin.""" + from __future__ import annotations from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo diff --git a/homeassistant/components/jellyfin/manifest.json b/homeassistant/components/jellyfin/manifest.json index 990449364a7..19358cff17c 100644 --- a/homeassistant/components/jellyfin/manifest.json +++ b/homeassistant/components/jellyfin/manifest.json @@ -7,5 +7,6 @@ "integration_type": "service", "iot_class": "local_polling", "loggers": ["jellyfin_apiclient_python"], - "requirements": ["jellyfin-apiclient-python==1.9.2"] + "requirements": ["jellyfin-apiclient-python==1.9.2"], + "single_config_entry": true } diff --git a/homeassistant/components/jellyfin/media_player.py b/homeassistant/components/jellyfin/media_player.py index 0f4b58b17e8..954ac7af69e 100644 --- a/homeassistant/components/jellyfin/media_player.py +++ b/homeassistant/components/jellyfin/media_player.py @@ -1,4 +1,5 @@ """Support for the Jellyfin media player.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/jellyfin/media_source.py b/homeassistant/components/jellyfin/media_source.py index 3bbe3e0b184..add04d1a1ec 100644 --- a/homeassistant/components/jellyfin/media_source.py +++ b/homeassistant/components/jellyfin/media_source.py @@ -1,4 +1,5 @@ """The Media Source implementation for the Jellyfin integration.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/jellyfin/models.py b/homeassistant/components/jellyfin/models.py index b6365042127..bfa639a7567 100644 --- a/homeassistant/components/jellyfin/models.py +++ b/homeassistant/components/jellyfin/models.py @@ -1,4 +1,5 @@ """Models for the Jellyfin integration.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/homeassistant/components/jellyfin/sensor.py b/homeassistant/components/jellyfin/sensor.py index df503d14378..85c7e9e9ee1 100644 --- a/homeassistant/components/jellyfin/sensor.py +++ b/homeassistant/components/jellyfin/sensor.py @@ -1,4 +1,5 @@ """Support for Jellyfin sensors.""" + from __future__ import annotations from collections.abc import Callable @@ -16,20 +17,13 @@ from .entity import JellyfinEntity from .models import JellyfinData -@dataclass(frozen=True) -class JellyfinSensorEntityDescriptionMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class JellyfinSensorEntityDescription(SensorEntityDescription): + """Describes Jellyfin sensor entity.""" value_fn: Callable[[JellyfinDataT], StateType] -@dataclass(frozen=True) -class JellyfinSensorEntityDescription( - SensorEntityDescription, JellyfinSensorEntityDescriptionMixin -): - """Describes Jellyfin sensor entity.""" - - def _count_now_playing(data: JellyfinDataT) -> int: """Count the number of now playing.""" session_ids = [ diff --git a/homeassistant/components/jellyfin/strings.json b/homeassistant/components/jellyfin/strings.json index 3e8965da785..3e4c8066b77 100644 --- a/homeassistant/components/jellyfin/strings.json +++ b/homeassistant/components/jellyfin/strings.json @@ -23,7 +23,6 @@ }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } } diff --git a/homeassistant/components/jewish_calendar/__init__.py b/homeassistant/components/jewish_calendar/__init__.py index 550ca2d9e5d..1ce5386d2c2 100644 --- a/homeassistant/components/jewish_calendar/__init__.py +++ b/homeassistant/components/jewish_calendar/__init__.py @@ -1,4 +1,5 @@ """The jewish_calendar component.""" + from __future__ import annotations from hdate import Location diff --git a/homeassistant/components/jewish_calendar/binary_sensor.py b/homeassistant/components/jewish_calendar/binary_sensor.py index 638d54d6159..73ddca27cc1 100644 --- a/homeassistant/components/jewish_calendar/binary_sensor.py +++ b/homeassistant/components/jewish_calendar/binary_sensor.py @@ -1,4 +1,5 @@ """Support for Jewish Calendar binary sensors.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/jewish_calendar/sensor.py b/homeassistant/components/jewish_calendar/sensor.py index b20364f7e64..2a16ecb9c14 100644 --- a/homeassistant/components/jewish_calendar/sensor.py +++ b/homeassistant/components/jewish_calendar/sensor.py @@ -1,4 +1,5 @@ """Platform to retrieve Jewish calendar information for Home Assistant.""" + from __future__ import annotations from datetime import date as Date diff --git a/homeassistant/components/joaoapps_join/__init__.py b/homeassistant/components/joaoapps_join/__init__.py index b53855c79fb..f537866054f 100644 --- a/homeassistant/components/joaoapps_join/__init__.py +++ b/homeassistant/components/joaoapps_join/__init__.py @@ -1,4 +1,5 @@ """Support for Joaoapps Join services.""" + import logging from pyjoin import ( diff --git a/homeassistant/components/joaoapps_join/notify.py b/homeassistant/components/joaoapps_join/notify.py index 33eede4dc21..6e9efc4da21 100644 --- a/homeassistant/components/joaoapps_join/notify.py +++ b/homeassistant/components/joaoapps_join/notify.py @@ -1,4 +1,5 @@ """Support for Join notifications.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/juicenet/__init__.py b/homeassistant/components/juicenet/__init__.py index bcefe763e15..5c32caab36f 100644 --- a/homeassistant/components/juicenet/__init__.py +++ b/homeassistant/components/juicenet/__init__.py @@ -1,4 +1,5 @@ """The JuiceNet integration.""" + from datetime import timedelta import logging diff --git a/homeassistant/components/juicenet/config_flow.py b/homeassistant/components/juicenet/config_flow.py index 35c1853b974..237c89922b2 100644 --- a/homeassistant/components/juicenet/config_flow.py +++ b/homeassistant/components/juicenet/config_flow.py @@ -1,11 +1,13 @@ """Config flow for JuiceNet integration.""" + import logging import aiohttp from pyjuicenet import Api, TokenError import voluptuous as vol -from homeassistant import config_entries, core, exceptions +from homeassistant import core, exceptions +from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_ACCESS_TOKEN from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -37,7 +39,7 @@ async def validate_input(hass: core.HomeAssistant, data): return {"title": "JuiceNet"} -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class JuiceNetConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for JuiceNet.""" VERSION = 1 diff --git a/homeassistant/components/juicenet/device.py b/homeassistant/components/juicenet/device.py index 86e1c92e4da..daec88c2a94 100644 --- a/homeassistant/components/juicenet/device.py +++ b/homeassistant/components/juicenet/device.py @@ -10,7 +10,7 @@ class JuiceNetApi: self._devices = [] async def setup(self): - """JuiceNet device setup.""" # noqa: D403 + """JuiceNet device setup.""" self._devices = await self.api.get_devices() @property diff --git a/homeassistant/components/juicenet/number.py b/homeassistant/components/juicenet/number.py index fd2535c5bf3..383d0d590c4 100644 --- a/homeassistant/components/juicenet/number.py +++ b/homeassistant/components/juicenet/number.py @@ -1,4 +1,5 @@ """Support for controlling juicenet/juicepoint/juicebox based EVSE numbers.""" + from __future__ import annotations from dataclasses import dataclass @@ -19,19 +20,11 @@ from .const import DOMAIN, JUICENET_API, JUICENET_COORDINATOR from .entity import JuiceNetDevice -@dataclass(frozen=True) -class JuiceNetNumberEntityDescriptionMixin: - """Mixin for required keys.""" - - setter_key: str - - -@dataclass(frozen=True) -class JuiceNetNumberEntityDescription( - NumberEntityDescription, JuiceNetNumberEntityDescriptionMixin -): +@dataclass(frozen=True, kw_only=True) +class JuiceNetNumberEntityDescription(NumberEntityDescription): """An entity description for a JuiceNetNumber.""" + setter_key: str native_max_value_key: str | None = None diff --git a/homeassistant/components/juicenet/sensor.py b/homeassistant/components/juicenet/sensor.py index 5f71e066b9c..1f0b815cd97 100644 --- a/homeassistant/components/juicenet/sensor.py +++ b/homeassistant/components/juicenet/sensor.py @@ -1,4 +1,5 @@ """Support for monitoring juicenet/juicepoint/juicebox based EVSE sensors.""" + from __future__ import annotations from homeassistant.components.sensor import ( diff --git a/homeassistant/components/juicenet/switch.py b/homeassistant/components/juicenet/switch.py index 7c373eeeb24..d800ac58c2c 100644 --- a/homeassistant/components/juicenet/switch.py +++ b/homeassistant/components/juicenet/switch.py @@ -1,4 +1,5 @@ """Support for monitoring juicenet/juicepoint/juicebox based EVSE switches.""" + from typing import Any from homeassistant.components.switch import SwitchEntity @@ -16,14 +17,13 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the JuiceNet switches.""" - entities = [] juicenet_data = hass.data[DOMAIN][config_entry.entry_id] api = juicenet_data[JUICENET_API] coordinator = juicenet_data[JUICENET_COORDINATOR] - for device in api.devices: - entities.append(JuiceNetChargeNowSwitch(device, coordinator)) - async_add_entities(entities) + async_add_entities( + JuiceNetChargeNowSwitch(device, coordinator) for device in api.devices + ) class JuiceNetChargeNowSwitch(JuiceNetDevice, SwitchEntity): diff --git a/homeassistant/components/justnimbus/__init__.py b/homeassistant/components/justnimbus/__init__.py index c30e213814e..101a2086962 100644 --- a/homeassistant/components/justnimbus/__init__.py +++ b/homeassistant/components/justnimbus/__init__.py @@ -1,4 +1,5 @@ """The JustNimbus integration.""" + from __future__ import annotations from homeassistant.config_entries import ConfigEntry @@ -14,7 +15,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if "zip_code" in entry.data: coordinator = JustNimbusCoordinator(hass=hass, entry=entry) else: - raise ConfigEntryAuthFailed() + raise ConfigEntryAuthFailed await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator diff --git a/homeassistant/components/justnimbus/config_flow.py b/homeassistant/components/justnimbus/config_flow.py index 536943ef607..2a286c41b5f 100644 --- a/homeassistant/components/justnimbus/config_flow.py +++ b/homeassistant/components/justnimbus/config_flow.py @@ -1,4 +1,5 @@ """Config flow for JustNimbus integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -8,9 +9,8 @@ from typing import Any import justnimbus import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_CLIENT_ID -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_validation as cv from .const import CONF_ZIP_CODE, DOMAIN @@ -25,15 +25,15 @@ STEP_USER_DATA_SCHEMA = vol.Schema( ) -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class JustNimbusConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for JustNimbus.""" VERSION = 1 - reauth_entry: config_entries.ConfigEntry | None = None + reauth_entry: ConfigEntry | None = None async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" if user_input is None: return self.async_show_form( @@ -76,7 +76,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors ) - async def async_step_reauth(self, user_input: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, user_input: Mapping[str, Any] + ) -> ConfigFlowResult: """Perform reauth upon an API authentication error.""" self.reauth_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] diff --git a/homeassistant/components/justnimbus/const.py b/homeassistant/components/justnimbus/const.py index 11a4ae487c4..cf2d099becd 100644 --- a/homeassistant/components/justnimbus/const.py +++ b/homeassistant/components/justnimbus/const.py @@ -1,6 +1,5 @@ """Constants for the JustNimbus integration.""" - from typing import Final from homeassistant.const import Platform diff --git a/homeassistant/components/justnimbus/coordinator.py b/homeassistant/components/justnimbus/coordinator.py index 9dc7dcbc743..4031ad86fdf 100644 --- a/homeassistant/components/justnimbus/coordinator.py +++ b/homeassistant/components/justnimbus/coordinator.py @@ -1,4 +1,5 @@ """JustNimbus coordinator.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/justnimbus/entity.py b/homeassistant/components/justnimbus/entity.py index 7303d4ec2c7..f85c3f33f93 100644 --- a/homeassistant/components/justnimbus/entity.py +++ b/homeassistant/components/justnimbus/entity.py @@ -1,4 +1,5 @@ """Base Entity for JustNimbus sensors.""" + from __future__ import annotations from homeassistant.helpers.device_registry import DeviceInfo diff --git a/homeassistant/components/justnimbus/icons.json b/homeassistant/components/justnimbus/icons.json new file mode 100644 index 00000000000..ed2ea39d08b --- /dev/null +++ b/homeassistant/components/justnimbus/icons.json @@ -0,0 +1,27 @@ +{ + "entity": { + "sensor": { + "pump_pressure": { + "default": "mdi:water-pump" + }, + "reservoir_temperature": { + "default": "mdi:coolant-temperature" + }, + "reservoir_content": { + "default": "mdi:car-coolant-level" + }, + "water_saved": { + "default": "mdi:water-opacity" + }, + "water_used": { + "default": "mdi:chart-donut" + }, + "reservoir_capacity": { + "default": "mdi:waves" + }, + "pump_type": { + "default": "mdi:pump" + } + } + } +} diff --git a/homeassistant/components/justnimbus/sensor.py b/homeassistant/components/justnimbus/sensor.py index 14b89b6c2c1..c2c22307371 100644 --- a/homeassistant/components/justnimbus/sensor.py +++ b/homeassistant/components/justnimbus/sensor.py @@ -1,4 +1,5 @@ """Support for the JustNimbus platform.""" + from __future__ import annotations from collections.abc import Callable @@ -28,25 +29,17 @@ from .const import DOMAIN from .entity import JustNimbusEntity -@dataclass(frozen=True) -class JustNimbusEntityDescriptionMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class JustNimbusEntityDescription(SensorEntityDescription): + """Describes JustNimbus sensor entity.""" value_fn: Callable[[JustNimbusCoordinator], Any] -@dataclass(frozen=True) -class JustNimbusEntityDescription( - SensorEntityDescription, JustNimbusEntityDescriptionMixin -): - """Describes JustNimbus sensor entity.""" - - SENSOR_TYPES = ( JustNimbusEntityDescription( key="pump_pressure", translation_key="pump_pressure", - icon="mdi:water-pump", native_unit_of_measurement=UnitOfPressure.BAR, device_class=SensorDeviceClass.PRESSURE, state_class=SensorStateClass.MEASUREMENT, @@ -56,7 +49,6 @@ SENSOR_TYPES = ( JustNimbusEntityDescription( key="reservoir_temp", translation_key="reservoir_temperature", - icon="mdi:coolant-temperature", native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, @@ -66,7 +58,6 @@ SENSOR_TYPES = ( JustNimbusEntityDescription( key="reservoir_content", translation_key="reservoir_content", - icon="mdi:car-coolant-level", native_unit_of_measurement=UnitOfVolume.LITERS, device_class=SensorDeviceClass.VOLUME, state_class=SensorStateClass.TOTAL, @@ -76,7 +67,6 @@ SENSOR_TYPES = ( JustNimbusEntityDescription( key="water_saved", translation_key="water_saved", - icon="mdi:water-opacity", native_unit_of_measurement=UnitOfVolume.LITERS, device_class=SensorDeviceClass.VOLUME, state_class=SensorStateClass.TOTAL_INCREASING, @@ -86,7 +76,6 @@ SENSOR_TYPES = ( JustNimbusEntityDescription( key="water_used", translation_key="water_used", - icon="mdi:chart-donut", native_unit_of_measurement=UnitOfVolume.LITERS, device_class=SensorDeviceClass.VOLUME, state_class=SensorStateClass.TOTAL_INCREASING, @@ -96,7 +85,6 @@ SENSOR_TYPES = ( JustNimbusEntityDescription( key="reservoir_capacity", translation_key="reservoir_capacity", - icon="mdi:waves", native_unit_of_measurement=UnitOfVolume.LITERS, device_class=SensorDeviceClass.VOLUME, state_class=SensorStateClass.TOTAL, @@ -106,7 +94,6 @@ SENSOR_TYPES = ( JustNimbusEntityDescription( key="pump_type", translation_key="pump_type", - icon="mdi:pump", entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda coordinator: coordinator.data.pump_type, ), diff --git a/homeassistant/components/jvc_projector/__init__.py b/homeassistant/components/jvc_projector/__init__.py index 33af1d315f7..28e4cc995bb 100644 --- a/homeassistant/components/jvc_projector/__init__.py +++ b/homeassistant/components/jvc_projector/__init__.py @@ -18,7 +18,7 @@ from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from .const import DOMAIN from .coordinator import JvcProjectorDataUpdateCoordinator -PLATFORMS = [Platform.BINARY_SENSOR, Platform.REMOTE] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.REMOTE, Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/jvc_projector/config_flow.py b/homeassistant/components/jvc_projector/config_flow.py index 181d11e1f56..7564d571d3b 100644 --- a/homeassistant/components/jvc_projector/config_flow.py +++ b/homeassistant/components/jvc_projector/config_flow.py @@ -9,9 +9,8 @@ from jvcprojector import JvcProjector, JvcProjectorAuthError, JvcProjectorConnec from jvcprojector.projector import DEFAULT_PORT import voluptuous as vol -from homeassistant.config_entries import ConfigEntry, ConfigFlow +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.device_registry import format_mac from homeassistant.util.network import is_host_valid @@ -27,7 +26,7 @@ class JvcProjectorConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle user initiated device additions.""" errors = {} @@ -74,7 +73,9 @@ class JvcProjectorConfigFlow(ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_reauth(self, user_input: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, user_input: Mapping[str, Any] + ) -> ConfigFlowResult: """Perform reauth on password authentication error.""" self._reauth_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] @@ -83,7 +84,7 @@ class JvcProjectorConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_reauth_confirm( self, user_input: Mapping[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Dialog that informs the user that reauth is required.""" assert self._reauth_entry diff --git a/homeassistant/components/jvc_projector/coordinator.py b/homeassistant/components/jvc_projector/coordinator.py index a63d68781b3..874253b3324 100644 --- a/homeassistant/components/jvc_projector/coordinator.py +++ b/homeassistant/components/jvc_projector/coordinator.py @@ -21,8 +21,8 @@ from .const import NAME _LOGGER = logging.getLogger(__name__) -INTERVAL_SLOW = timedelta(seconds=60) -INTERVAL_FAST = timedelta(seconds=6) +INTERVAL_SLOW = timedelta(seconds=10) +INTERVAL_FAST = timedelta(seconds=5) class JvcProjectorDataUpdateCoordinator(DataUpdateCoordinator[dict[str, str]]): diff --git a/homeassistant/components/jvc_projector/icons.json b/homeassistant/components/jvc_projector/icons.json index 94e2ec41cf6..c70ded78cb4 100644 --- a/homeassistant/components/jvc_projector/icons.json +++ b/homeassistant/components/jvc_projector/icons.json @@ -7,6 +7,17 @@ "on": "mdi:projector" } } + }, + "sensor": { + "jvc_power_status": { + "default": "mdi:power-plug-off", + "state": { + "on": "mdi:power-plug", + "warming": "mdi:heat-wave", + "cooling": "mdi:snowflake", + "error": "mdi:alert-circle" + } + } } } } diff --git a/homeassistant/components/jvc_projector/sensor.py b/homeassistant/components/jvc_projector/sensor.py new file mode 100644 index 00000000000..9be04b367e6 --- /dev/null +++ b/homeassistant/components/jvc_projector/sensor.py @@ -0,0 +1,65 @@ +"""Sensor platform for JVC Projector integration.""" + +from __future__ import annotations + +from jvcprojector import const + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import EntityCategory +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import JvcProjectorDataUpdateCoordinator +from .const import DOMAIN +from .entity import JvcProjectorEntity + +JVC_SENSORS = ( + SensorEntityDescription( + key="power", + translation_key="jvc_power_status", + device_class=SensorDeviceClass.ENUM, + entity_category=EntityCategory.DIAGNOSTIC, + options=[ + const.STANDBY, + const.ON, + const.WARMING, + const.COOLING, + const.ERROR, + ], + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up the JVC Projector platform from a config entry.""" + coordinator: JvcProjectorDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + + async_add_entities( + JvcSensor(coordinator, description) for description in JVC_SENSORS + ) + + +class JvcSensor(JvcProjectorEntity, SensorEntity): + """The entity class for JVC Projector integration.""" + + def __init__( + self, + coordinator: JvcProjectorDataUpdateCoordinator, + description: SensorEntityDescription, + ) -> None: + """Initialize the JVC Projector sensor.""" + super().__init__(coordinator) + self.entity_description = description + self._attr_unique_id = f"{coordinator.unique_id}_{description.key}" + + @property + def native_value(self) -> str | None: + """Return the native value.""" + return self.coordinator.data[self.entity_description.key] diff --git a/homeassistant/components/jvc_projector/strings.json b/homeassistant/components/jvc_projector/strings.json index 06efdc8f9aa..9991fa1cf67 100644 --- a/homeassistant/components/jvc_projector/strings.json +++ b/homeassistant/components/jvc_projector/strings.json @@ -37,6 +37,18 @@ "jvc_power": { "name": "[%key:component::sensor::entity_component::power::name%]" } + }, + "sensor": { + "jvc_power_status": { + "name": "Power status", + "state": { + "standby": "[%key:common::state::standby%]", + "on": "[%key:common::state::on%]", + "warming": "Warming", + "cooling": "Cooling", + "error": "Error" + } + } } } } diff --git a/homeassistant/components/kaiterra/__init__.py b/homeassistant/components/kaiterra/__init__.py index 6cf866d2d63..67fcfbe482c 100644 --- a/homeassistant/components/kaiterra/__init__.py +++ b/homeassistant/components/kaiterra/__init__.py @@ -1,4 +1,5 @@ """Support for Kaiterra devices.""" + import voluptuous as vol from homeassistant.const import ( diff --git a/homeassistant/components/kaiterra/air_quality.py b/homeassistant/components/kaiterra/air_quality.py index edbddb361c9..4d0d83a38eb 100644 --- a/homeassistant/components/kaiterra/air_quality.py +++ b/homeassistant/components/kaiterra/air_quality.py @@ -1,4 +1,5 @@ """Support for Kaiterra Air Quality Sensors.""" + from __future__ import annotations from homeassistant.components.air_quality import AirQualityEntity diff --git a/homeassistant/components/kaiterra/api_data.py b/homeassistant/components/kaiterra/api_data.py index 6ee73b8ace7..945cc6e9b86 100644 --- a/homeassistant/components/kaiterra/api_data.py +++ b/homeassistant/components/kaiterra/api_data.py @@ -1,4 +1,5 @@ """Data for all Kaiterra devices.""" + import asyncio from logging import getLogger diff --git a/homeassistant/components/kaiterra/sensor.py b/homeassistant/components/kaiterra/sensor.py index bb780aab619..22401f9027a 100644 --- a/homeassistant/components/kaiterra/sensor.py +++ b/homeassistant/components/kaiterra/sensor.py @@ -1,4 +1,5 @@ """Support for Kaiterra Temperature ahn Humidity Sensors.""" + from __future__ import annotations from dataclasses import dataclass @@ -17,20 +18,13 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import DISPATCHER_KAITERRA, DOMAIN -@dataclass(frozen=True) -class KaiterraSensorRequiredKeysMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class KaiterraSensorEntityDescription(SensorEntityDescription): + """Class describing Renault sensor entities.""" suffix: str -@dataclass(frozen=True) -class KaiterraSensorEntityDescription( - SensorEntityDescription, KaiterraSensorRequiredKeysMixin -): - """Class describing Renault sensor entities.""" - - SENSORS = [ KaiterraSensorEntityDescription( suffix="Temperature", diff --git a/homeassistant/components/kaleidescape/config_flow.py b/homeassistant/components/kaleidescape/config_flow.py index 5454f29f5cb..bb9f47ec1e8 100644 --- a/homeassistant/components/kaleidescape/config_flow.py +++ b/homeassistant/components/kaleidescape/config_flow.py @@ -2,21 +2,18 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, cast +from typing import Any, cast from urllib.parse import urlparse import voluptuous as vol from homeassistant.components import ssdp -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST from . import KaleidescapeDeviceInfo, UnsupportedError, validate_host from .const import DEFAULT_HOST, DOMAIN, NAME as KALEIDESCAPE_NAME -if TYPE_CHECKING: - from homeassistant.data_entry_flow import FlowResult - ERROR_CANNOT_CONNECT = "cannot_connect" ERROR_UNSUPPORTED = "unsupported" @@ -30,7 +27,7 @@ class KaleidescapeConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle user initiated device additions.""" errors = {} host = DEFAULT_HOST @@ -63,7 +60,9 @@ class KaleidescapeConfigFlow(ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: + async def async_step_ssdp( + self, discovery_info: ssdp.SsdpServiceInfo + ) -> ConfigFlowResult: """Handle discovered device.""" host = cast(str, urlparse(discovery_info.ssdp_location).hostname) serial_number = discovery_info.upnp[ssdp.ATTR_UPNP_SERIAL] @@ -93,7 +92,7 @@ class KaleidescapeConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_discovery_confirm( self, user_input: dict | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle addition of discovered device.""" if user_input is None: return self.async_show_form( diff --git a/homeassistant/components/kaleidescape/icons.json b/homeassistant/components/kaleidescape/icons.json new file mode 100644 index 00000000000..2a9408b6843 --- /dev/null +++ b/homeassistant/components/kaleidescape/icons.json @@ -0,0 +1,54 @@ +{ + "entity": { + "sensor": { + "media_location": { + "default": "mdi:monitor" + }, + "play_status": { + "default": "mdi:monitor" + }, + "play_speed": { + "default": "mdi:monitor" + }, + "video_mode": { + "default": "mdi:monitor-screenshot" + }, + "video_color_eotf": { + "default": "mdi:monitor-eye" + }, + "video_color_space": { + "default": "mdi:monitor-eye" + }, + "video_color_depth": { + "default": "mdi:monitor-eye" + }, + "video_color_sampling": { + "default": "mdi:monitor-eye" + }, + "screen_mask_ratio": { + "default": "mdi:monitor-screenshot" + }, + "screen_mask_top_trim_rel": { + "default": "mdi:monitor-screenshot" + }, + "screen_mask_bottom_trim_rel": { + "default": "mdi:monitor-screenshot" + }, + "screen_mask_conservative_ratio": { + "default": "mdi:monitor-screenshot" + }, + "screen_mask_top_mask_abs": { + "default": "mdi:monitor-screenshot" + }, + "screen_mask_bottom_mask_abs": { + "default": "mdi:monitor-screenshot" + }, + "cinemascape_mask": { + "default": "mdi:monitor-star" + }, + "cinemascape_mode": { + "default": "mdi:monitor-star" + } + } + } +} diff --git a/homeassistant/components/kaleidescape/media_player.py b/homeassistant/components/kaleidescape/media_player.py index 7751f6b6a29..33acb899728 100644 --- a/homeassistant/components/kaleidescape/media_player.py +++ b/homeassistant/components/kaleidescape/media_player.py @@ -1,4 +1,5 @@ """Kaleidescape Media Player.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/kaleidescape/sensor.py b/homeassistant/components/kaleidescape/sensor.py index ba9eaca1e95..5520943e683 100644 --- a/homeassistant/components/kaleidescape/sensor.py +++ b/homeassistant/components/kaleidescape/sensor.py @@ -22,85 +22,68 @@ if TYPE_CHECKING: from homeassistant.helpers.typing import StateType -@dataclass(frozen=True) -class BaseEntityDescriptionMixin: - """Mixin for required descriptor keys.""" +@dataclass(frozen=True, kw_only=True) +class KaleidescapeSensorEntityDescription(SensorEntityDescription): + """Describes Kaleidescape sensor entity.""" value_fn: Callable[[KaleidescapeDevice], StateType] -@dataclass(frozen=True) -class KaleidescapeSensorEntityDescription( - SensorEntityDescription, BaseEntityDescriptionMixin -): - """Describes Kaleidescape sensor entity.""" - - SENSOR_TYPES: tuple[KaleidescapeSensorEntityDescription, ...] = ( KaleidescapeSensorEntityDescription( key="media_location", translation_key="media_location", - icon="mdi:monitor", value_fn=lambda device: device.automation.movie_location, ), KaleidescapeSensorEntityDescription( key="play_status", translation_key="play_status", - icon="mdi:monitor", value_fn=lambda device: device.movie.play_status, ), KaleidescapeSensorEntityDescription( key="play_speed", translation_key="play_speed", - icon="mdi:monitor", value_fn=lambda device: device.movie.play_speed, ), KaleidescapeSensorEntityDescription( key="video_mode", translation_key="video_mode", - icon="mdi:monitor-screenshot", entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda device: device.automation.video_mode, ), KaleidescapeSensorEntityDescription( key="video_color_eotf", translation_key="video_color_eotf", - icon="mdi:monitor-eye", entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda device: device.automation.video_color_eotf, ), KaleidescapeSensorEntityDescription( key="video_color_space", translation_key="video_color_space", - icon="mdi:monitor-eye", entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda device: device.automation.video_color_space, ), KaleidescapeSensorEntityDescription( key="video_color_depth", translation_key="video_color_depth", - icon="mdi:monitor-eye", entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda device: device.automation.video_color_depth, ), KaleidescapeSensorEntityDescription( key="video_color_sampling", translation_key="video_color_sampling", - icon="mdi:monitor-eye", entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda device: device.automation.video_color_sampling, ), KaleidescapeSensorEntityDescription( key="screen_mask_ratio", translation_key="screen_mask_ratio", - icon="mdi:monitor-screenshot", entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda device: device.automation.screen_mask_ratio, ), KaleidescapeSensorEntityDescription( key="screen_mask_top_trim_rel", translation_key="screen_mask_top_trim_rel", - icon="mdi:monitor-screenshot", entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=PERCENTAGE, value_fn=lambda device: device.automation.screen_mask_top_trim_rel / 10.0, @@ -108,7 +91,6 @@ SENSOR_TYPES: tuple[KaleidescapeSensorEntityDescription, ...] = ( KaleidescapeSensorEntityDescription( key="screen_mask_bottom_trim_rel", translation_key="screen_mask_bottom_trim_rel", - icon="mdi:monitor-screenshot", entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=PERCENTAGE, value_fn=lambda device: device.automation.screen_mask_bottom_trim_rel / 10.0, @@ -116,14 +98,12 @@ SENSOR_TYPES: tuple[KaleidescapeSensorEntityDescription, ...] = ( KaleidescapeSensorEntityDescription( key="screen_mask_conservative_ratio", translation_key="screen_mask_conservative_ratio", - icon="mdi:monitor-screenshot", entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda device: device.automation.screen_mask_conservative_ratio, ), KaleidescapeSensorEntityDescription( key="screen_mask_top_mask_abs", translation_key="screen_mask_top_mask_abs", - icon="mdi:monitor-screenshot", entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=PERCENTAGE, value_fn=lambda device: device.automation.screen_mask_top_mask_abs / 10.0, @@ -131,7 +111,6 @@ SENSOR_TYPES: tuple[KaleidescapeSensorEntityDescription, ...] = ( KaleidescapeSensorEntityDescription( key="screen_mask_bottom_mask_abs", translation_key="screen_mask_bottom_mask_abs", - icon="mdi:monitor-screenshot", entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=PERCENTAGE, value_fn=lambda device: device.automation.screen_mask_bottom_mask_abs / 10.0, @@ -139,14 +118,12 @@ SENSOR_TYPES: tuple[KaleidescapeSensorEntityDescription, ...] = ( KaleidescapeSensorEntityDescription( key="cinemascape_mask", translation_key="cinemascape_mask", - icon="mdi:monitor-star", entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda device: device.automation.cinemascape_mask, ), KaleidescapeSensorEntityDescription( key="cinemascape_mode", translation_key="cinemascape_mode", - icon="mdi:monitor-star", entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda device: device.automation.cinemascape_mode, ), diff --git a/homeassistant/components/kankun/switch.py b/homeassistant/components/kankun/switch.py index f64b11706a1..f650494b3b1 100644 --- a/homeassistant/components/kankun/switch.py +++ b/homeassistant/components/kankun/switch.py @@ -1,4 +1,5 @@ """Support for customised Kankun SP3 Wifi switch.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/keba/__init__.py b/homeassistant/components/keba/__init__.py index 1c99a6500c5..34eb7c99166 100644 --- a/homeassistant/components/keba/__init__.py +++ b/homeassistant/components/keba/__init__.py @@ -1,4 +1,5 @@ """Support for KEBA charging stations.""" + import asyncio import logging diff --git a/homeassistant/components/keba/binary_sensor.py b/homeassistant/components/keba/binary_sensor.py index 7997130c90a..9f8e0ac3f3e 100644 --- a/homeassistant/components/keba/binary_sensor.py +++ b/homeassistant/components/keba/binary_sensor.py @@ -1,4 +1,5 @@ """Support for KEBA charging station binary sensors.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/keba/icons.json b/homeassistant/components/keba/icons.json new file mode 100644 index 00000000000..7f64bf7fb34 --- /dev/null +++ b/homeassistant/components/keba/icons.json @@ -0,0 +1,12 @@ +{ + "services": { + "request_data": "mdi:database-arrow-down", + "authorize": "mdi:lock", + "deauthorize": "mdi:lock-open", + "set_energy": "mdi:flash", + "set_current": "mdi:flash", + "enable": "mdi:flash", + "disable": "mdi:fash-off", + "set_failsafe": "mdi:message-alert" + } +} diff --git a/homeassistant/components/keba/lock.py b/homeassistant/components/keba/lock.py index de8d28d7739..be005b92874 100644 --- a/homeassistant/components/keba/lock.py +++ b/homeassistant/components/keba/lock.py @@ -1,4 +1,5 @@ """Support for KEBA charging station switch.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/keba/notify.py b/homeassistant/components/keba/notify.py index 78b3976f656..5358ba32ff9 100644 --- a/homeassistant/components/keba/notify.py +++ b/homeassistant/components/keba/notify.py @@ -1,4 +1,5 @@ """Support for Keba notifications.""" + from __future__ import annotations from homeassistant.components.notify import ATTR_DATA, BaseNotificationService diff --git a/homeassistant/components/keba/sensor.py b/homeassistant/components/keba/sensor.py index 635683419b2..74c08933cbe 100644 --- a/homeassistant/components/keba/sensor.py +++ b/homeassistant/components/keba/sensor.py @@ -1,4 +1,5 @@ """Support for KEBA charging station sensors.""" + from __future__ import annotations from homeassistant.components.sensor import ( diff --git a/homeassistant/components/keenetic_ndms2/__init__.py b/homeassistant/components/keenetic_ndms2/__init__.py index 6f33b11742a..e2ca17ebce8 100644 --- a/homeassistant/components/keenetic_ndms2/__init__.py +++ b/homeassistant/components/keenetic_ndms2/__init__.py @@ -1,4 +1,5 @@ """The keenetic_ndms2 component.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/keenetic_ndms2/binary_sensor.py b/homeassistant/components/keenetic_ndms2/binary_sensor.py index ab0b3370197..cb7d83b9238 100644 --- a/homeassistant/components/keenetic_ndms2/binary_sensor.py +++ b/homeassistant/components/keenetic_ndms2/binary_sensor.py @@ -1,4 +1,5 @@ """The Keenetic Client class.""" + from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, BinarySensorEntity, diff --git a/homeassistant/components/keenetic_ndms2/config_flow.py b/homeassistant/components/keenetic_ndms2/config_flow.py index 535c83c927e..f00bbe22939 100644 --- a/homeassistant/components/keenetic_ndms2/config_flow.py +++ b/homeassistant/components/keenetic_ndms2/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Keenetic NDMS2.""" + from __future__ import annotations from typing import Any @@ -7,9 +8,13 @@ from urllib.parse import urlparse from ndms2_client import Client, ConnectionException, InterfaceInfo, TelnetConnection import voluptuous as vol -from homeassistant import config_entries from homeassistant.components import ssdp -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, @@ -18,7 +23,6 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_validation as cv from .const import ( @@ -37,7 +41,7 @@ from .const import ( from .router import KeeneticRouter -class KeeneticFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class KeeneticFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a config flow.""" VERSION = 1 @@ -52,7 +56,7 @@ class KeeneticFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" errors = {} if user_input is not None: @@ -97,7 +101,9 @@ class KeeneticFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: + async def async_step_ssdp( + self, discovery_info: ssdp.SsdpServiceInfo + ) -> ConfigFlowResult: """Handle a discovered device.""" friendly_name = discovery_info.upnp.get(ssdp.ATTR_UPNP_FRIENDLY_NAME, "") @@ -124,7 +130,7 @@ class KeeneticFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_user() -class KeeneticOptionsFlowHandler(config_entries.OptionsFlow): +class KeeneticOptionsFlowHandler(OptionsFlow): """Handle options.""" def __init__(self, config_entry: ConfigEntry) -> None: @@ -134,7 +140,7 @@ class KeeneticOptionsFlowHandler(config_entries.OptionsFlow): async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Manage the options.""" router: KeeneticRouter = self.hass.data[DOMAIN][self.config_entry.entry_id][ ROUTER @@ -153,7 +159,7 @@ class KeeneticOptionsFlowHandler(config_entries.OptionsFlow): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """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/keenetic_ndms2/device_tracker.py b/homeassistant/components/keenetic_ndms2/device_tracker.py index c9e81071ad7..e15c96d8353 100644 --- a/homeassistant/components/keenetic_ndms2/device_tracker.py +++ b/homeassistant/components/keenetic_ndms2/device_tracker.py @@ -1,4 +1,5 @@ """Support for Keenetic routers as device tracker.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/keenetic_ndms2/router.py b/homeassistant/components/keenetic_ndms2/router.py index 77101dcbf3e..5a4f32a05cd 100644 --- a/homeassistant/components/keenetic_ndms2/router.py +++ b/homeassistant/components/keenetic_ndms2/router.py @@ -1,4 +1,5 @@ """The Keenetic Client class.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/kef/icons.json b/homeassistant/components/kef/icons.json new file mode 100644 index 00000000000..eeb6dd099ce --- /dev/null +++ b/homeassistant/components/kef/icons.json @@ -0,0 +1,12 @@ +{ + "services": { + "update_dsp": "mdi:update", + "set_mode": "mdi:cog", + "set_desk_db": "mdi:volume-high", + "set_wall_db": "mdi:volume-high", + "set_treble_db": "mdi:volume-high", + "set_high_hz": "mdi:sine-wave", + "set_low_hz": "mdi:cosine-wave", + "set_sub_db": "mdi:volume-high" + } +} diff --git a/homeassistant/components/kef/media_player.py b/homeassistant/components/kef/media_player.py index b8407fd8bde..d7d33dabd44 100644 --- a/homeassistant/components/kef/media_player.py +++ b/homeassistant/components/kef/media_player.py @@ -1,4 +1,5 @@ """Platform for the KEF Wireless Speakers.""" + from __future__ import annotations from datetime import timedelta @@ -269,7 +270,7 @@ class KefMediaPlayer(MediaPlayerEntity): async def async_turn_on(self) -> None: """Turn the media player on.""" if not self._supports_on: - raise NotImplementedError() + raise NotImplementedError await self._speaker.turn_on() async def async_volume_up(self) -> None: diff --git a/homeassistant/components/kegtron/__init__.py b/homeassistant/components/kegtron/__init__.py index 7a1669bdcd4..d7485be0840 100644 --- a/homeassistant/components/kegtron/__init__.py +++ b/homeassistant/components/kegtron/__init__.py @@ -1,4 +1,5 @@ """The Kegtron integration.""" + from __future__ import annotations import logging @@ -25,14 +26,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: address = entry.unique_id assert address is not None data = KegtronBluetoothDeviceData() - coordinator = hass.data.setdefault(DOMAIN, {})[ - entry.entry_id - ] = PassiveBluetoothProcessorCoordinator( - hass, - _LOGGER, - address=address, - mode=BluetoothScanningMode.PASSIVE, - update_method=data.update, + coordinator = hass.data.setdefault(DOMAIN, {})[entry.entry_id] = ( + PassiveBluetoothProcessorCoordinator( + hass, + _LOGGER, + address=address, + mode=BluetoothScanningMode.PASSIVE, + update_method=data.update, + ) ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload( diff --git a/homeassistant/components/kegtron/config_flow.py b/homeassistant/components/kegtron/config_flow.py index cc0457af87b..8dfb28f0caa 100644 --- a/homeassistant/components/kegtron/config_flow.py +++ b/homeassistant/components/kegtron/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Kegtron ble integration.""" + from __future__ import annotations from typing import Any @@ -10,9 +11,8 @@ from homeassistant.components.bluetooth import ( BluetoothServiceInfoBleak, async_discovered_service_info, ) -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_ADDRESS -from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN @@ -30,7 +30,7 @@ class KegtronConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_bluetooth( self, discovery_info: BluetoothServiceInfoBleak - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the bluetooth discovery step.""" await self.async_set_unique_id(discovery_info.address) self._abort_if_unique_id_configured() @@ -43,7 +43,7 @@ class KegtronConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_bluetooth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm discovery.""" assert self._discovered_device is not None device = self._discovered_device @@ -62,7 +62,7 @@ class KegtronConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the user step to pick discovered device.""" if user_input is not None: address = user_input[CONF_ADDRESS] diff --git a/homeassistant/components/kegtron/device.py b/homeassistant/components/kegtron/device.py index 85516a3aea3..033094e41d7 100644 --- a/homeassistant/components/kegtron/device.py +++ b/homeassistant/components/kegtron/device.py @@ -1,4 +1,5 @@ """Support for Kegtron devices.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/kegtron/sensor.py b/homeassistant/components/kegtron/sensor.py index 8e1ed539385..4fc4ac9242f 100644 --- a/homeassistant/components/kegtron/sensor.py +++ b/homeassistant/components/kegtron/sensor.py @@ -1,4 +1,5 @@ """Support for Kegtron sensors.""" + from __future__ import annotations from kegtron_ble import ( diff --git a/homeassistant/components/keyboard/__init__.py b/homeassistant/components/keyboard/__init__.py index d129505515d..bf935f119d0 100644 --- a/homeassistant/components/keyboard/__init__.py +++ b/homeassistant/components/keyboard/__init__.py @@ -1,4 +1,5 @@ """Support to emulate keyboard presses on host machine.""" + from pykeyboard import PyKeyboard import voluptuous as vol diff --git a/homeassistant/components/keyboard/icons.json b/homeassistant/components/keyboard/icons.json new file mode 100644 index 00000000000..8186b2684dd --- /dev/null +++ b/homeassistant/components/keyboard/icons.json @@ -0,0 +1,10 @@ +{ + "services": { + "volume_up": "mdi:volume-high", + "volume_down": "mdi:volume-low", + "volume_mute": "mdi:volume-off", + "media_play_pause": "mdi:play-pause", + "media_next_track": "mdi:skip-next", + "media_prev_track": "mdi:skip-previous" + } +} diff --git a/homeassistant/components/keyboard_remote/__init__.py b/homeassistant/components/keyboard_remote/__init__.py index eecde05d1f4..5831a770466 100644 --- a/homeassistant/components/keyboard_remote/__init__.py +++ b/homeassistant/components/keyboard_remote/__init__.py @@ -1,4 +1,5 @@ """Receive signals from a keyboard and use it as a remote control.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/keymitt_ble/__init__.py b/homeassistant/components/keymitt_ble/__init__.py index 1a7df4fe0a9..7fea46d7a02 100644 --- a/homeassistant/components/keymitt_ble/__init__.py +++ b/homeassistant/components/keymitt_ble/__init__.py @@ -1,4 +1,5 @@ """Integration to integrate Keymitt BLE devices with Home Assistant.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/keymitt_ble/config_flow.py b/homeassistant/components/keymitt_ble/config_flow.py index 5665dc27d17..589798a281a 100644 --- a/homeassistant/components/keymitt_ble/config_flow.py +++ b/homeassistant/components/keymitt_ble/config_flow.py @@ -1,4 +1,5 @@ """Adds config flow for MicroBot.""" + from __future__ import annotations import logging @@ -17,9 +18,8 @@ from homeassistant.components.bluetooth import ( BluetoothServiceInfoBleak, async_discovered_service_info, ) -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_ACCESS_TOKEN, CONF_ADDRESS -from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN @@ -54,7 +54,7 @@ class MicroBotConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_bluetooth( self, discovery_info: BluetoothServiceInfoBleak - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the bluetooth discovery step.""" _LOGGER.debug("Discovered bluetooth device: %s", discovery_info) await self.async_set_unique_id(discovery_info.address) @@ -71,14 +71,14 @@ class MicroBotConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" # This is for backwards compatibility. return await self.async_step_init(user_input) async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Check if paired.""" errors: dict[str, str] = {} @@ -125,7 +125,7 @@ class MicroBotConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_link( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Given a configured host, will ask the user to press the button to pair.""" errors: dict[str, str] = {} token = randomid(32) diff --git a/homeassistant/components/keymitt_ble/const.py b/homeassistant/components/keymitt_ble/const.py index a10e7124226..9d4be4fa278 100644 --- a/homeassistant/components/keymitt_ble/const.py +++ b/homeassistant/components/keymitt_ble/const.py @@ -1,4 +1,5 @@ """Constants for Keymitt BLE.""" + # Base component constants DOMAIN = "keymitt_ble" MANUFACTURER = "Naran/Keymitt" diff --git a/homeassistant/components/keymitt_ble/coordinator.py b/homeassistant/components/keymitt_ble/coordinator.py index e3a995e3813..3e72826ac5d 100644 --- a/homeassistant/components/keymitt_ble/coordinator.py +++ b/homeassistant/components/keymitt_ble/coordinator.py @@ -1,4 +1,5 @@ """Integration to integrate Keymitt BLE devices with Home Assistant.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/keymitt_ble/entity.py b/homeassistant/components/keymitt_ble/entity.py index a9294bce239..b5229e6917e 100644 --- a/homeassistant/components/keymitt_ble/entity.py +++ b/homeassistant/components/keymitt_ble/entity.py @@ -1,4 +1,5 @@ """MicroBot class.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/keymitt_ble/icons.json b/homeassistant/components/keymitt_ble/icons.json new file mode 100644 index 00000000000..77450fbf026 --- /dev/null +++ b/homeassistant/components/keymitt_ble/icons.json @@ -0,0 +1,5 @@ +{ + "services": { + "calibrate": "mdi:wrench" + } +} diff --git a/homeassistant/components/keymitt_ble/switch.py b/homeassistant/components/keymitt_ble/switch.py index 4c9f0c335a7..2c255ecdf28 100644 --- a/homeassistant/components/keymitt_ble/switch.py +++ b/homeassistant/components/keymitt_ble/switch.py @@ -1,4 +1,5 @@ """Switch platform for MicroBot.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/kira/__init__.py b/homeassistant/components/kira/__init__.py index c54b8a91208..b0305bc0643 100644 --- a/homeassistant/components/kira/__init__.py +++ b/homeassistant/components/kira/__init__.py @@ -1,4 +1,5 @@ """KIRA interface to receive UDP packets from an IR-IP bridge.""" + import logging import os diff --git a/homeassistant/components/kira/remote.py b/homeassistant/components/kira/remote.py index 4c06216a210..f6ee4af75ef 100644 --- a/homeassistant/components/kira/remote.py +++ b/homeassistant/components/kira/remote.py @@ -1,4 +1,5 @@ """Support for Keene Electronics IR-IP devices.""" + from __future__ import annotations from collections.abc import Iterable diff --git a/homeassistant/components/kira/sensor.py b/homeassistant/components/kira/sensor.py index e1a4f08dd14..5779ed4df35 100644 --- a/homeassistant/components/kira/sensor.py +++ b/homeassistant/components/kira/sensor.py @@ -1,4 +1,5 @@ """KIRA interface to receive UDP packets from an IR-IP bridge.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/kitchen_sink/__init__.py b/homeassistant/components/kitchen_sink/__init__.py index 228803097d6..6b6694c920d 100644 --- a/homeassistant/components/kitchen_sink/__init__.py +++ b/homeassistant/components/kitchen_sink/__init__.py @@ -3,6 +3,7 @@ This sets up a demo environment of features which are obscure or which represent incorrect behavior, and are thus not wanted in the demo integration. """ + from __future__ import annotations import datetime diff --git a/homeassistant/components/kitchen_sink/button.py b/homeassistant/components/kitchen_sink/button.py index cdc0cebb348..5c62c4b32d1 100644 --- a/homeassistant/components/kitchen_sink/button.py +++ b/homeassistant/components/kitchen_sink/button.py @@ -1,4 +1,5 @@ """Demo platform that offers a fake button entity.""" + from __future__ import annotations from homeassistant.components import persistent_notification diff --git a/homeassistant/components/kitchen_sink/config_flow.py b/homeassistant/components/kitchen_sink/config_flow.py index 54104784c50..93c8a292ba9 100644 --- a/homeassistant/components/kitchen_sink/config_flow.py +++ b/homeassistant/components/kitchen_sink/config_flow.py @@ -1,20 +1,20 @@ """Config flow to configure the Kitchen Sink component.""" + from __future__ import annotations from typing import Any -from homeassistant import config_entries -from homeassistant.data_entry_flow import FlowResult +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from . import DOMAIN -class KitchenSinkConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class KitchenSinkConfigFlow(ConfigFlow, domain=DOMAIN): """Kitchen Sink configuration flow.""" VERSION = 1 - async def async_step_import(self, import_info: dict[str, Any]) -> FlowResult: + async def async_step_import(self, import_info: dict[str, Any]) -> ConfigFlowResult: """Set the config entry up from yaml.""" if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") diff --git a/homeassistant/components/kitchen_sink/image.py b/homeassistant/components/kitchen_sink/image.py index 4fe20f08de9..504b36464f5 100644 --- a/homeassistant/components/kitchen_sink/image.py +++ b/homeassistant/components/kitchen_sink/image.py @@ -1,4 +1,5 @@ """Demo image platform.""" + from __future__ import annotations from pathlib import Path diff --git a/homeassistant/components/kitchen_sink/lawn_mower.py b/homeassistant/components/kitchen_sink/lawn_mower.py index 119b37b7569..50ec70f6759 100644 --- a/homeassistant/components/kitchen_sink/lawn_mower.py +++ b/homeassistant/components/kitchen_sink/lawn_mower.py @@ -1,4 +1,5 @@ """Demo platform that has a couple fake lawn mowers.""" + from __future__ import annotations from homeassistant.components.lawn_mower import ( diff --git a/homeassistant/components/kitchen_sink/lock.py b/homeassistant/components/kitchen_sink/lock.py index b25941cf1a3..228e383e94d 100644 --- a/homeassistant/components/kitchen_sink/lock.py +++ b/homeassistant/components/kitchen_sink/lock.py @@ -1,4 +1,5 @@ """Demo platform that has a couple of fake locks.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/kitchen_sink/sensor.py b/homeassistant/components/kitchen_sink/sensor.py index 4800104d17d..95e56c276e4 100644 --- a/homeassistant/components/kitchen_sink/sensor.py +++ b/homeassistant/components/kitchen_sink/sensor.py @@ -1,4 +1,5 @@ """Demo platform that has a couple of fake sensors.""" + from __future__ import annotations from homeassistant.components.sensor import ( diff --git a/homeassistant/components/kitchen_sink/switch.py b/homeassistant/components/kitchen_sink/switch.py index e60de2f09c8..68a8312b496 100644 --- a/homeassistant/components/kitchen_sink/switch.py +++ b/homeassistant/components/kitchen_sink/switch.py @@ -1,4 +1,5 @@ """Demo platform that has some fake switches.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/kitchen_sink/weather.py b/homeassistant/components/kitchen_sink/weather.py index 8449b68b460..8a12cb4bdb9 100644 --- a/homeassistant/components/kitchen_sink/weather.py +++ b/homeassistant/components/kitchen_sink/weather.py @@ -1,4 +1,5 @@ """Demo platform that offers fake meteorological data.""" + from __future__ import annotations from datetime import timedelta @@ -60,29 +61,6 @@ async def async_setup_entry( """Set up the Demo config entry.""" async_add_entities( [ - DemoWeather( - "Legacy weather", - "Sunshine", - 21.6414, - 92, - 1099, - 0.5, - UnitOfTemperature.CELSIUS, - UnitOfPressure.HPA, - UnitOfSpeed.METERS_PER_SECOND, - [ - [ATTR_CONDITION_RAINY, 1, 22, 15, 60], - [ATTR_CONDITION_RAINY, 5, 19, 8, 30], - [ATTR_CONDITION_CLOUDY, 0, 15, 9, 10], - [ATTR_CONDITION_SUNNY, 0, 12, 6, 0], - [ATTR_CONDITION_PARTLYCLOUDY, 2, 14, 7, 20], - [ATTR_CONDITION_RAINY, 15, 18, 7, 0], - [ATTR_CONDITION_FOG, 0.2, 21, 12, 100], - ], - None, - None, - None, - ), DemoWeather( "Legacy + daily weather", "Sunshine", @@ -102,15 +80,6 @@ async def async_setup_entry( [ATTR_CONDITION_RAINY, 15, 18, 7, 0], [ATTR_CONDITION_FOG, 0.2, 21, 12, 100], ], - [ - [ATTR_CONDITION_RAINY, 1, 22, 15, 60], - [ATTR_CONDITION_RAINY, 5, 19, 8, 30], - [ATTR_CONDITION_CLOUDY, 0, 15, 9, 10], - [ATTR_CONDITION_SUNNY, 0, 12, 6, 0], - [ATTR_CONDITION_PARTLYCLOUDY, 2, 14, 7, 20], - [ATTR_CONDITION_RAINY, 15, 18, 7, 0], - [ATTR_CONDITION_FOG, 0.2, 21, 12, 100], - ], None, None, ), @@ -124,7 +93,6 @@ async def async_setup_entry( UnitOfTemperature.FAHRENHEIT, UnitOfPressure.INHG, UnitOfSpeed.MILES_PER_HOUR, - None, [ [ATTR_CONDITION_SNOWY, 2, -10, -15, 60], [ATTR_CONDITION_PARTLYCLOUDY, 1, -13, -14, 25], @@ -155,7 +123,6 @@ async def async_setup_entry( UnitOfTemperature.CELSIUS, UnitOfPressure.HPA, UnitOfSpeed.METERS_PER_SECOND, - None, [ [ATTR_CONDITION_RAINY, 1, 22, 15, 60], [ATTR_CONDITION_RAINY, 5, 19, 8, 30], @@ -195,7 +162,6 @@ async def async_setup_entry( UnitOfPressure.HPA, UnitOfSpeed.METERS_PER_SECOND, None, - None, [ [ATTR_CONDITION_CLOUDY, 1, 22, 15, 60], [ATTR_CONDITION_CLOUDY, 5, 19, 8, 30], @@ -225,7 +191,6 @@ async def async_setup_entry( UnitOfTemperature.CELSIUS, UnitOfPressure.HPA, UnitOfSpeed.METERS_PER_SECOND, - None, [ [ATTR_CONDITION_RAINY, 1, 22, 15, 60], [ATTR_CONDITION_RAINY, 5, 19, 8, 30], @@ -267,7 +232,6 @@ class DemoWeather(WeatherEntity): temperature_unit: str, pressure_unit: str, wind_speed_unit: str, - forecast: list[list] | None, forecast_daily: list[list] | None, forecast_hourly: list[list] | None, forecast_twice_daily: list[list] | None, @@ -283,7 +247,6 @@ class DemoWeather(WeatherEntity): self._native_pressure_unit = pressure_unit self._native_wind_speed = wind_speed self._native_wind_speed_unit = wind_speed_unit - self._forecast = forecast self._forecast_daily = forecast_daily self._forecast_hourly = forecast_hourly self._forecast_twice_daily = forecast_twice_daily @@ -359,28 +322,6 @@ class DemoWeather(WeatherEntity): """Return the weather condition.""" return CONDITION_MAP[self._condition.lower()] - @property - def forecast(self) -> list[Forecast]: - """Return legacy forecast.""" - if self._forecast is None: - return [] - reftime = dt_util.now().replace(hour=16, minute=00) - - forecast_data = [] - for entry in self._forecast: - 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=24) - forecast_data.append(data_dict) - - return forecast_data - async def async_forecast_daily(self) -> list[Forecast]: """Return the daily forecast.""" if self._forecast_daily is None: diff --git a/homeassistant/components/kiwi/lock.py b/homeassistant/components/kiwi/lock.py index 44dc2bb2521..770b842091c 100644 --- a/homeassistant/components/kiwi/lock.py +++ b/homeassistant/components/kiwi/lock.py @@ -1,4 +1,5 @@ """Support for the KIWI.KI lock platform.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/kmtronic/__init__.py b/homeassistant/components/kmtronic/__init__.py index 638884dff26..5f93de3c60e 100644 --- a/homeassistant/components/kmtronic/__init__.py +++ b/homeassistant/components/kmtronic/__init__.py @@ -1,4 +1,5 @@ """The kmtronic integration.""" + import asyncio from datetime import timedelta import logging diff --git a/homeassistant/components/kmtronic/config_flow.py b/homeassistant/components/kmtronic/config_flow.py index 8a00a03e673..dd0a7652418 100644 --- a/homeassistant/components/kmtronic/config_flow.py +++ b/homeassistant/components/kmtronic/config_flow.py @@ -1,4 +1,5 @@ """Config flow for kmtronic integration.""" + from __future__ import annotations import logging @@ -8,9 +9,10 @@ from pykmtronic.auth import Auth from pykmtronic.hub import KMTronicHubAPI import voluptuous as vol -from homeassistant import config_entries, core, exceptions +from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import aiohttp_client from .const import CONF_REVERSE, DOMAIN @@ -26,7 +28,7 @@ DATA_SCHEMA = vol.Schema( ) -async def validate_input(hass: core.HomeAssistant, data): +async def validate_input(hass: HomeAssistant, data): """Validate the user input allows us to connect.""" session = aiohttp_client.async_get_clientsession(hass) auth = Auth( @@ -47,7 +49,7 @@ async def validate_input(hass: core.HomeAssistant, data): return data -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class KmtronicConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for kmtronic.""" VERSION = 1 @@ -55,7 +57,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback def async_get_options_flow( - config_entry: config_entries.ConfigEntry, + config_entry: ConfigEntry, ) -> KMTronicOptionsFlow: """Get the options flow for this handler.""" return KMTronicOptionsFlow(config_entry) @@ -81,18 +83,18 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -class CannotConnect(exceptions.HomeAssistantError): +class CannotConnect(HomeAssistantError): """Error to indicate we cannot connect.""" -class InvalidAuth(exceptions.HomeAssistantError): +class InvalidAuth(HomeAssistantError): """Error to indicate there is invalid auth.""" -class KMTronicOptionsFlow(config_entries.OptionsFlow): +class KMTronicOptionsFlow(OptionsFlow): """Handle options.""" - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + def __init__(self, config_entry: ConfigEntry) -> None: """Initialize options flow.""" self.config_entry = config_entry diff --git a/homeassistant/components/kmtronic/switch.py b/homeassistant/components/kmtronic/switch.py index 144c05e927e..f00ecf8623c 100644 --- a/homeassistant/components/kmtronic/switch.py +++ b/homeassistant/components/kmtronic/switch.py @@ -1,4 +1,5 @@ """KMtronic Switch integration.""" + from typing import Any import urllib.parse diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index c6869f34eeb..c84d53d6039 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -1,4 +1,5 @@ """Support KNX devices.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/knx/binary_sensor.py b/homeassistant/components/knx/binary_sensor.py index 9005ca707b9..dee56608421 100644 --- a/homeassistant/components/knx/binary_sensor.py +++ b/homeassistant/components/knx/binary_sensor.py @@ -1,4 +1,5 @@ """Support for KNX/IP binary sensors.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/knx/button.py b/homeassistant/components/knx/button.py index 94b5b51e401..a38d8ad1b6c 100644 --- a/homeassistant/components/knx/button.py +++ b/homeassistant/components/knx/button.py @@ -1,4 +1,5 @@ """Support for KNX/IP buttons.""" + from __future__ import annotations from xknx import XKNX diff --git a/homeassistant/components/knx/climate.py b/homeassistant/components/knx/climate.py index 1038cdde80f..ce1e4f018b9 100644 --- a/homeassistant/components/knx/climate.py +++ b/homeassistant/components/knx/climate.py @@ -1,4 +1,5 @@ """Support for KNX/IP climate devices.""" + from __future__ import annotations from typing import Any @@ -203,10 +204,10 @@ class KNXClimate(KnxEntity, ClimateEntity): """Return the list of available operation/controller modes.""" ha_controller_modes: list[HVACMode | None] = [] if self._device.mode is not None: - for knx_controller_mode in self._device.mode.controller_modes: - ha_controller_modes.append( - CONTROLLER_MODES.get(knx_controller_mode.value) - ) + ha_controller_modes.extend( + CONTROLLER_MODES.get(knx_controller_mode.value) + for knx_controller_mode in self._device.mode.controller_modes + ) if self._device.supports_on_off: if not ha_controller_modes: diff --git a/homeassistant/components/knx/config_flow.py b/homeassistant/components/knx/config_flow.py index 5338a5fddca..af1eee89af7 100644 --- a/homeassistant/components/knx/config_flow.py +++ b/homeassistant/components/knx/config_flow.py @@ -1,4 +1,5 @@ """Config flow for KNX.""" + from __future__ import annotations from abc import ABC, abstractmethod @@ -18,10 +19,15 @@ from xknx.io.self_description import request_description from xknx.io.util import validate_ip as xknx_validate_ip from xknx.secure.keyring import Keyring, XMLInterface -from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow +from homeassistant.config_entries import ( + ConfigEntry, + ConfigEntryBaseFlow, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowHandler, FlowResult from homeassistant.helpers import selector from homeassistant.helpers.typing import UNDEFINED @@ -96,7 +102,7 @@ _PORT_SELECTOR = vol.All( ) -class KNXCommonFlow(ABC, FlowHandler): +class KNXCommonFlow(ABC, ConfigEntryBaseFlow): """Base class for KNX flows.""" def __init__(self, initial_data: KNXConfigEntryData) -> None: @@ -115,7 +121,7 @@ class KNXCommonFlow(ABC, FlowHandler): self._async_scan_gen: AsyncGenerator[GatewayDescriptor, None] | None = None @abstractmethod - def finish_flow(self) -> FlowResult: + def finish_flow(self) -> ConfigFlowResult: """Finish the flow.""" @property @@ -136,7 +142,7 @@ class KNXCommonFlow(ABC, FlowHandler): async def async_step_connection_type( self, user_input: dict | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle connection type configuration.""" if user_input is not None: if self._async_scan_gen: @@ -202,7 +208,9 @@ class KNXCommonFlow(ABC, FlowHandler): step_id="connection_type", data_schema=vol.Schema(fields) ) - async def async_step_tunnel(self, user_input: dict | None = None) -> FlowResult: + async def async_step_tunnel( + self, user_input: dict | None = None + ) -> ConfigFlowResult: """Select a tunnel from a list. Will be skipped if the gateway scan was unsuccessful @@ -258,7 +266,7 @@ class KNXCommonFlow(ABC, FlowHandler): async def async_step_manual_tunnel( self, user_input: dict | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Manually configure tunnel connection parameters. Fields default to preselected gateway if one was found.""" errors: dict = {} @@ -380,7 +388,7 @@ class KNXCommonFlow(ABC, FlowHandler): async def async_step_secure_tunnel_manual( self, user_input: dict | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Configure ip secure tunnelling manually.""" errors: dict = {} @@ -428,7 +436,7 @@ class KNXCommonFlow(ABC, FlowHandler): async def async_step_secure_routing_manual( self, user_input: dict | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Configure ip secure routing manually.""" errors: dict = {} @@ -481,7 +489,7 @@ class KNXCommonFlow(ABC, FlowHandler): async def async_step_secure_knxkeys( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Manage upload of new KNX Keyring file.""" errors: dict[str, str] = {} @@ -533,7 +541,7 @@ class KNXCommonFlow(ABC, FlowHandler): async def async_step_knxkeys_tunnel_select( self, user_input: dict | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Select if a specific tunnel should be used from knxkeys file.""" errors = {} description_placeholders = {} @@ -589,17 +597,17 @@ class KNXCommonFlow(ABC, FlowHandler): value=CONF_KNX_AUTOMATIC, label=CONF_KNX_AUTOMATIC.capitalize() ) ] - for endpoint in self._tunnel_endpoints: - tunnel_endpoint_options.append( - selector.SelectOptionDict( - value=str(endpoint.individual_address), - label=( - f"{endpoint.individual_address} " - f"{'🔐 ' if endpoint.user_id else ''}" - f"(Data Secure GAs: {len(endpoint.group_addresses)})" - ), - ) + tunnel_endpoint_options.extend( + selector.SelectOptionDict( + value=str(endpoint.individual_address), + label=( + f"{endpoint.individual_address} " + f"{'🔐 ' if endpoint.user_id else ''}" + f"(Data Secure GAs: {len(endpoint.group_addresses)})" + ), ) + for endpoint in self._tunnel_endpoints + ) return self.async_show_form( step_id="knxkeys_tunnel_select", data_schema=vol.Schema( @@ -618,7 +626,9 @@ class KNXCommonFlow(ABC, FlowHandler): description_placeholders=description_placeholders, ) - async def async_step_routing(self, user_input: dict | None = None) -> FlowResult: + async def async_step_routing( + self, user_input: dict | None = None + ) -> ConfigFlowResult: """Routing setup.""" errors: dict = {} _individual_address = ( @@ -703,7 +713,7 @@ class KNXCommonFlow(ABC, FlowHandler): async def async_step_secure_key_source_menu_tunnel( self, user_input: dict | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Show the key source menu.""" return self.async_show_menu( step_id="secure_key_source_menu_tunnel", @@ -712,7 +722,7 @@ class KNXCommonFlow(ABC, FlowHandler): async def async_step_secure_key_source_menu_routing( self, user_input: dict | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Show the key source menu.""" return self.async_show_menu( step_id="secure_key_source_menu_routing", @@ -736,7 +746,7 @@ class KNXConfigFlow(KNXCommonFlow, ConfigFlow, domain=DOMAIN): return KNXOptionsFlow(config_entry) @callback - def finish_flow(self) -> FlowResult: + def finish_flow(self) -> ConfigFlowResult: """Create the ConfigEntry.""" title = self.new_title or f"KNX {self.new_entry_data[CONF_KNX_CONNECTION_TYPE]}" return self.async_create_entry( @@ -744,10 +754,8 @@ class KNXConfigFlow(KNXCommonFlow, ConfigFlow, domain=DOMAIN): data=DEFAULT_ENTRY_DATA | self.new_entry_data, ) - async def async_step_user(self, user_input: dict | None = None) -> FlowResult: + async def async_step_user(self, user_input: dict | None = None) -> ConfigFlowResult: """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_connection_type() @@ -762,7 +770,7 @@ class KNXOptionsFlow(KNXCommonFlow, OptionsFlow): super().__init__(initial_data=config_entry.data) # type: ignore[arg-type] @callback - def finish_flow(self) -> FlowResult: + def finish_flow(self) -> ConfigFlowResult: """Update the ConfigEntry and finish the flow.""" new_data = DEFAULT_ENTRY_DATA | self.initial_data | self.new_entry_data self.hass.config_entries.async_update_entry( @@ -774,7 +782,7 @@ class KNXOptionsFlow(KNXCommonFlow, OptionsFlow): async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Manage KNX options.""" return self.async_show_menu( step_id="init", @@ -787,7 +795,7 @@ class KNXOptionsFlow(KNXCommonFlow, OptionsFlow): async def async_step_communication_settings( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Manage KNX communication settings.""" if user_input is not None: self.new_entry_data = KNXConfigEntryData( diff --git a/homeassistant/components/knx/const.py b/homeassistant/components/knx/const.py index 8cb1986c540..9c0d5e1125a 100644 --- a/homeassistant/components/knx/const.py +++ b/homeassistant/components/knx/const.py @@ -1,4 +1,5 @@ """Constants for the KNX integration.""" + from __future__ import annotations from collections.abc import Awaitable, Callable diff --git a/homeassistant/components/knx/cover.py b/homeassistant/components/knx/cover.py index 9e86fc8b36e..9d86d6ac272 100644 --- a/homeassistant/components/knx/cover.py +++ b/homeassistant/components/knx/cover.py @@ -1,4 +1,5 @@ """Support for KNX/IP covers.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/knx/date.py b/homeassistant/components/knx/date.py index 1f286d59ecb..fa20a8d04c5 100644 --- a/homeassistant/components/knx/date.py +++ b/homeassistant/components/knx/date.py @@ -1,4 +1,5 @@ """Support for KNX/IP date.""" + from __future__ import annotations from datetime import date as dt_date diff --git a/homeassistant/components/knx/datetime.py b/homeassistant/components/knx/datetime.py index fc63df04233..47d9b9f55b2 100644 --- a/homeassistant/components/knx/datetime.py +++ b/homeassistant/components/knx/datetime.py @@ -1,4 +1,5 @@ """Support for KNX/IP datetime.""" + from __future__ import annotations from datetime import datetime diff --git a/homeassistant/components/knx/device.py b/homeassistant/components/knx/device.py index 583ca2f768b..fd5abc6a072 100644 --- a/homeassistant/components/knx/device.py +++ b/homeassistant/components/knx/device.py @@ -1,4 +1,5 @@ """Handle KNX Devices.""" + from __future__ import annotations from xknx import XKNX diff --git a/homeassistant/components/knx/device_trigger.py b/homeassistant/components/knx/device_trigger.py index 867a7c075b0..93e1623f88c 100644 --- a/homeassistant/components/knx/device_trigger.py +++ b/homeassistant/components/knx/device_trigger.py @@ -1,4 +1,5 @@ """Provides device triggers for KNX.""" + from __future__ import annotations from typing import Any, Final diff --git a/homeassistant/components/knx/diagnostics.py b/homeassistant/components/knx/diagnostics.py index 2fada718d31..1907539fc61 100644 --- a/homeassistant/components/knx/diagnostics.py +++ b/homeassistant/components/knx/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for KNX.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/knx/expose.py b/homeassistant/components/knx/expose.py index d5c871d59ba..6e4a3b80f6e 100644 --- a/homeassistant/components/knx/expose.py +++ b/homeassistant/components/knx/expose.py @@ -1,4 +1,5 @@ """Exposures to KNX bus.""" + from __future__ import annotations from collections.abc import Callable @@ -17,12 +18,12 @@ from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.core import HomeAssistant, State, callback +from homeassistant.core import Event, HomeAssistant, State, callback from homeassistant.helpers.event import ( EventStateChangedData, async_track_state_change_event, ) -from homeassistant.helpers.typing import ConfigType, EventType, StateType +from homeassistant.helpers.typing import ConfigType, StateType from .const import CONF_RESPOND_TO_READ, KNX_ADDRESS from .schema import ExposeSchema @@ -148,9 +149,7 @@ class KNXExposeSensor: return str(value)[:14] return value - async def _async_entity_changed( - self, event: EventType[EventStateChangedData] - ) -> None: + async def _async_entity_changed(self, event: Event[EventStateChangedData]) -> None: """Handle entity change.""" new_state = event.data["new_state"] if (new_value := self._get_expose_value(new_state)) is None: diff --git a/homeassistant/components/knx/fan.py b/homeassistant/components/knx/fan.py index a22a16a6e69..e94609bdc86 100644 --- a/homeassistant/components/knx/fan.py +++ b/homeassistant/components/knx/fan.py @@ -1,4 +1,5 @@ """Support for KNX/IP fans.""" + from __future__ import annotations import math diff --git a/homeassistant/components/knx/helpers/keyring.py b/homeassistant/components/knx/helpers/keyring.py index 5d1dfea6383..9e9cfda2b80 100644 --- a/homeassistant/components/knx/helpers/keyring.py +++ b/homeassistant/components/knx/helpers/keyring.py @@ -1,4 +1,5 @@ """KNX Keyring handler.""" + import logging from pathlib import Path import shutil diff --git a/homeassistant/components/knx/icons.json b/homeassistant/components/knx/icons.json new file mode 100644 index 00000000000..736923375ee --- /dev/null +++ b/homeassistant/components/knx/icons.json @@ -0,0 +1,31 @@ +{ + "entity": { + "sensor": { + "individual_address": { + "default": "mdi:router-network" + }, + "telegrams_incoming": { + "default": "mdi:upload-network" + }, + "telegrams_incoming_error": { + "default": "mdi:help-network" + }, + "telegrams_outgoing": { + "default": "mdi:download-network" + }, + "telegrams_outgoing_error": { + "default": "mdi:close-network" + }, + "telegram_count": { + "default": "mdi:plus-network" + } + } + }, + "services": { + "send": "mdi:email-arrow-right", + "read": "mdi:email-search", + "event_register": "mdi:home-import-outline", + "exposure_register": "mdi:home-export-outline", + "reload": "mdi:reload" + } +} diff --git a/homeassistant/components/knx/knx_entity.py b/homeassistant/components/knx/knx_entity.py index 9545510e635..b03c59486e5 100644 --- a/homeassistant/components/knx/knx_entity.py +++ b/homeassistant/components/knx/knx_entity.py @@ -1,4 +1,5 @@ """Base class for KNX devices.""" + from __future__ import annotations from typing import cast diff --git a/homeassistant/components/knx/light.py b/homeassistant/components/knx/light.py index f25e78a4d70..b1c1681a817 100644 --- a/homeassistant/components/knx/light.py +++ b/homeassistant/components/knx/light.py @@ -1,4 +1,5 @@ """Support for KNX/IP lights.""" + from __future__ import annotations from typing import Any, cast diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index 290b560dad5..99c150a8346 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -14,5 +14,6 @@ "xknx==2.12.2", "xknxproject==3.7.0", "knx-frontend==2024.1.20.105944" - ] + ], + "single_config_entry": true } diff --git a/homeassistant/components/knx/notify.py b/homeassistant/components/knx/notify.py index 9d190ac78b0..74ae86dc5d0 100644 --- a/homeassistant/components/knx/notify.py +++ b/homeassistant/components/knx/notify.py @@ -1,4 +1,5 @@ """Support for KNX/IP notification services.""" + from __future__ import annotations from typing import Any @@ -27,21 +28,16 @@ async def async_get_service( if platform_config := hass.data[DATA_KNX_CONFIG].get(NotifySchema.PLATFORM): xknx: XKNX = hass.data[DOMAIN].xknx - notification_devices = [] - for device_config in platform_config: - notification_devices.append( - XknxNotification( - xknx, - name=device_config[CONF_NAME], - group_address=device_config[KNX_ADDRESS], - value_type=device_config[CONF_TYPE], - ) + notification_devices = [ + XknxNotification( + xknx, + name=device_config[CONF_NAME], + group_address=device_config[KNX_ADDRESS], + value_type=device_config[CONF_TYPE], ) - return ( - KNXNotificationService(notification_devices) - if notification_devices - else None - ) + for device_config in platform_config + ] + return KNXNotificationService(notification_devices) return None diff --git a/homeassistant/components/knx/number.py b/homeassistant/components/knx/number.py index fbf4db3f5b2..8a9f1dea87c 100644 --- a/homeassistant/components/knx/number.py +++ b/homeassistant/components/knx/number.py @@ -1,4 +1,5 @@ """Support for KNX/IP numeric values.""" + from __future__ import annotations from typing import cast diff --git a/homeassistant/components/knx/project.py b/homeassistant/components/knx/project.py index d47241b174b..13e71dbbe38 100644 --- a/homeassistant/components/knx/project.py +++ b/homeassistant/components/knx/project.py @@ -1,4 +1,5 @@ """Handle KNX project data.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/homeassistant/components/knx/scene.py b/homeassistant/components/knx/scene.py index a028cebc8f7..342d0f9eb83 100644 --- a/homeassistant/components/knx/scene.py +++ b/homeassistant/components/knx/scene.py @@ -1,4 +1,5 @@ """Support for KNX scenes.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/knx/schema.py b/homeassistant/components/knx/schema.py index d559cd2005a..39670b4f92b 100644 --- a/homeassistant/components/knx/schema.py +++ b/homeassistant/components/knx/schema.py @@ -1,4 +1,5 @@ """Voluptuous schemas for the KNX integration.""" + from __future__ import annotations from abc import ABC diff --git a/homeassistant/components/knx/select.py b/homeassistant/components/knx/select.py index 2852917e021..5d7532e0e5d 100644 --- a/homeassistant/components/knx/select.py +++ b/homeassistant/components/knx/select.py @@ -1,4 +1,5 @@ """Support for KNX/IP select entities.""" + from __future__ import annotations from xknx import XKNX diff --git a/homeassistant/components/knx/sensor.py b/homeassistant/components/knx/sensor.py index 2f09f7e8ed6..173979f78dc 100644 --- a/homeassistant/components/knx/sensor.py +++ b/homeassistant/components/knx/sensor.py @@ -1,4 +1,5 @@ """Support for KNX/IP sensors.""" + from __future__ import annotations from collections.abc import Callable @@ -55,7 +56,6 @@ SYSTEM_ENTITY_DESCRIPTIONS = ( KNXSystemEntityDescription( key="individual_address", always_available=False, - icon="mdi:router-network", should_poll=False, value_fn=lambda knx: str(knx.xknx.current_address), ), @@ -76,7 +76,6 @@ SYSTEM_ENTITY_DESCRIPTIONS = ( ), KNXSystemEntityDescription( key="telegrams_incoming", - icon="mdi:upload-network", entity_registry_enabled_default=False, force_update=True, state_class=SensorStateClass.TOTAL_INCREASING, @@ -84,13 +83,11 @@ SYSTEM_ENTITY_DESCRIPTIONS = ( ), KNXSystemEntityDescription( key="telegrams_incoming_error", - icon="mdi:help-network", state_class=SensorStateClass.TOTAL_INCREASING, value_fn=lambda knx: knx.xknx.connection_manager.cemi_count_incoming_error, ), KNXSystemEntityDescription( key="telegrams_outgoing", - icon="mdi:download-network", entity_registry_enabled_default=False, force_update=True, state_class=SensorStateClass.TOTAL_INCREASING, @@ -98,13 +95,11 @@ SYSTEM_ENTITY_DESCRIPTIONS = ( ), KNXSystemEntityDescription( key="telegrams_outgoing_error", - icon="mdi:close-network", state_class=SensorStateClass.TOTAL_INCREASING, value_fn=lambda knx: knx.xknx.connection_manager.cemi_count_outgoing_error, ), KNXSystemEntityDescription( key="telegram_count", - icon="mdi:plus-network", force_update=True, state_class=SensorStateClass.TOTAL_INCREASING, value_fn=lambda knx: knx.xknx.connection_manager.cemi_count_outgoing diff --git a/homeassistant/components/knx/services.py b/homeassistant/components/knx/services.py index 99c44a5eee6..24b9452cf60 100644 --- a/homeassistant/components/knx/services.py +++ b/homeassistant/components/knx/services.py @@ -1,4 +1,5 @@ """KNX integration services.""" + from __future__ import annotations from functools import partial diff --git a/homeassistant/components/knx/strings.json b/homeassistant/components/knx/strings.json index 5f5a2263eac..39b96dddf8f 100644 --- a/homeassistant/components/knx/strings.json +++ b/homeassistant/components/knx/strings.json @@ -108,10 +108,6 @@ } } }, - "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_service%]", - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" - }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "invalid_backbone_key": "Invalid backbone key. 32 hexadecimal numbers expected.", diff --git a/homeassistant/components/knx/switch.py b/homeassistant/components/knx/switch.py index 81f8de815c9..096ce235e2c 100644 --- a/homeassistant/components/knx/switch.py +++ b/homeassistant/components/knx/switch.py @@ -1,4 +1,5 @@ """Support for KNX/IP switches.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/knx/telegrams.py b/homeassistant/components/knx/telegrams.py index 95250d99f85..7c3ea28c4df 100644 --- a/homeassistant/components/knx/telegrams.py +++ b/homeassistant/components/knx/telegrams.py @@ -1,4 +1,5 @@ """KNX Telegram handler.""" + from __future__ import annotations from collections import deque diff --git a/homeassistant/components/knx/text.py b/homeassistant/components/knx/text.py index abd3f44ae6b..22d008cd5ce 100644 --- a/homeassistant/components/knx/text.py +++ b/homeassistant/components/knx/text.py @@ -1,4 +1,5 @@ """Support for KNX/IP text.""" + from __future__ import annotations from xknx import XKNX diff --git a/homeassistant/components/knx/time.py b/homeassistant/components/knx/time.py index af8ee48b806..c11b40d13dc 100644 --- a/homeassistant/components/knx/time.py +++ b/homeassistant/components/knx/time.py @@ -1,4 +1,5 @@ """Support for KNX/IP time.""" + from __future__ import annotations from datetime import time as dt_time diff --git a/homeassistant/components/knx/validation.py b/homeassistant/components/knx/validation.py index c0ac93d19eb..9fe87a2c3f6 100644 --- a/homeassistant/components/knx/validation.py +++ b/homeassistant/components/knx/validation.py @@ -1,4 +1,5 @@ """Validation helpers for KNX config schemas.""" + from collections.abc import Callable import ipaddress from typing import Any diff --git a/homeassistant/components/knx/weather.py b/homeassistant/components/knx/weather.py index 92034be95ff..90796f26f1a 100644 --- a/homeassistant/components/knx/weather.py +++ b/homeassistant/components/knx/weather.py @@ -1,4 +1,5 @@ """Support for KNX/IP weather station.""" + from __future__ import annotations from xknx import XKNX diff --git a/homeassistant/components/knx/websocket.py b/homeassistant/components/knx/websocket.py index e3eb5de8530..f6869902793 100644 --- a/homeassistant/components/knx/websocket.py +++ b/homeassistant/components/knx/websocket.py @@ -1,4 +1,5 @@ """KNX Websocket API.""" + from __future__ import annotations from typing import TYPE_CHECKING, Final diff --git a/homeassistant/components/kodi/browse_media.py b/homeassistant/components/kodi/browse_media.py index 5df714e27da..60e99d98cb1 100644 --- a/homeassistant/components/kodi/browse_media.py +++ b/homeassistant/components/kodi/browse_media.py @@ -1,4 +1,5 @@ """Support for media browsing.""" + import asyncio import contextlib import logging diff --git a/homeassistant/components/kodi/config_flow.py b/homeassistant/components/kodi/config_flow.py index bd6d23e0f8e..1d9d1ca4f7c 100644 --- a/homeassistant/components/kodi/config_flow.py +++ b/homeassistant/components/kodi/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Kodi integration.""" + from __future__ import annotations import logging @@ -6,8 +7,8 @@ import logging from pykodi import CannotConnectError, InvalidAuthError, Kodi, get_kodi_connection import voluptuous as vol -from homeassistant import config_entries, core, exceptions from homeassistant.components import zeroconf +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import ( CONF_HOST, CONF_NAME, @@ -17,8 +18,8 @@ from homeassistant.const import ( CONF_TIMEOUT, CONF_USERNAME, ) -from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import ( @@ -33,7 +34,7 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -async def validate_http(hass: core.HomeAssistant, data): +async def validate_http(hass: HomeAssistant, data): """Validate the user input allows us to connect over HTTP.""" host = data[CONF_HOST] @@ -56,7 +57,7 @@ async def validate_http(hass: core.HomeAssistant, data): raise InvalidAuth from error -async def validate_ws(hass: core.HomeAssistant, data): +async def validate_ws(hass: HomeAssistant, data): """Validate the user input allows us to connect over WS.""" if not (ws_port := data.get(CONF_WS_PORT)): return @@ -77,14 +78,14 @@ async def validate_ws(hass: core.HomeAssistant, data): await kwc.connect() if not kwc.connected: _LOGGER.warning("Cannot connect to %s:%s over WebSocket", host, ws_port) - raise WSCannotConnect() + raise WSCannotConnect kodi = Kodi(kwc) await kodi.ping() except CannotConnectError as error: raise WSCannotConnect from error -class KodiConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class KodiConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Kodi.""" VERSION = 1 @@ -102,7 +103,7 @@ class KodiConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_zeroconf( self, discovery_info: zeroconf.ZeroconfServiceInfo - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle zeroconf discovery.""" self._host = discovery_info.host self._port = discovery_info.port or DEFAULT_PORT @@ -313,13 +314,13 @@ class KodiConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return data -class CannotConnect(exceptions.HomeAssistantError): +class CannotConnect(HomeAssistantError): """Error to indicate we cannot connect.""" -class InvalidAuth(exceptions.HomeAssistantError): +class InvalidAuth(HomeAssistantError): """Error to indicate there is invalid auth.""" -class WSCannotConnect(exceptions.HomeAssistantError): +class WSCannotConnect(HomeAssistantError): """Error to indicate we cannot connect to websocket.""" diff --git a/homeassistant/components/kodi/const.py b/homeassistant/components/kodi/const.py index 8f0ae5de737..479b02e0fb5 100644 --- a/homeassistant/components/kodi/const.py +++ b/homeassistant/components/kodi/const.py @@ -1,4 +1,5 @@ """Constants for the Kodi platform.""" + DOMAIN = "kodi" CONF_WS_PORT = "ws_port" diff --git a/homeassistant/components/kodi/device_trigger.py b/homeassistant/components/kodi/device_trigger.py index 3f931d1e264..8659872f8c1 100644 --- a/homeassistant/components/kodi/device_trigger.py +++ b/homeassistant/components/kodi/device_trigger.py @@ -1,4 +1,5 @@ """Provides device automations for Kodi.""" + from __future__ import annotations import voluptuous as vol diff --git a/homeassistant/components/kodi/icons.json b/homeassistant/components/kodi/icons.json new file mode 100644 index 00000000000..07bd246e92d --- /dev/null +++ b/homeassistant/components/kodi/icons.json @@ -0,0 +1,6 @@ +{ + "services": { + "add_to_playlist": "mdi:playlist-plus", + "call_method": "mdi:console" + } +} diff --git a/homeassistant/components/kodi/media_player.py b/homeassistant/components/kodi/media_player.py index bca1c7f6f0e..74140ca873c 100644 --- a/homeassistant/components/kodi/media_player.py +++ b/homeassistant/components/kodi/media_player.py @@ -1,4 +1,5 @@ """Support for interfacing with the XBMC/Kodi JSON-RPC API.""" + from __future__ import annotations from collections.abc import Awaitable, Callable, Coroutine diff --git a/homeassistant/components/kodi/notify.py b/homeassistant/components/kodi/notify.py index f3459e891b7..05b5ff56be4 100644 --- a/homeassistant/components/kodi/notify.py +++ b/homeassistant/components/kodi/notify.py @@ -1,4 +1,5 @@ """Kodi notification service.""" + from __future__ import annotations import logging @@ -59,7 +60,7 @@ async def async_get_service( port: int = config[CONF_PORT] encryption = config.get(CONF_PROXY_SSL) - if host.startswith("http://") or host.startswith("https://"): + if host.startswith(("http://", "https://")): host = host[host.index("://") + 3 :] _LOGGER.warning( "Kodi host name should no longer contain http:// See updated " diff --git a/homeassistant/components/konnected/__init__.py b/homeassistant/components/konnected/__init__.py index fa8a35d7a64..25c731ac7f4 100644 --- a/homeassistant/components/konnected/__init__.py +++ b/homeassistant/components/konnected/__init__.py @@ -1,4 +1,5 @@ """Support for Konnected devices.""" + import copy import hmac from http import HTTPStatus @@ -11,7 +12,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components.binary_sensor import DEVICE_CLASSES_SCHEMA -from homeassistant.components.http import HomeAssistantView +from homeassistant.components.http import KEY_HASS, HomeAssistantView from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ENTITY_ID, @@ -304,7 +305,7 @@ class KonnectedView(HomeAssistantView): async def update_sensor(self, request: Request, device_id) -> Response: """Process a put or post.""" - hass = request.app["hass"] + hass = request.app[KEY_HASS] data = hass.data[DOMAIN] auth = request.headers.get(AUTHORIZATION) @@ -376,7 +377,7 @@ class KonnectedView(HomeAssistantView): async def get(self, request: Request, device_id) -> Response: """Return the current binary state of a switch.""" - hass = request.app["hass"] + hass = request.app[KEY_HASS] data = hass.data[DOMAIN] if not (device := data[CONF_DEVICES].get(device_id)): @@ -424,7 +425,8 @@ class KonnectedView(HomeAssistantView): # Make sure entity is setup if zone_entity_id := zone.get(ATTR_ENTITY_ID): resp["state"] = self.binary_value( - hass.states.get(zone_entity_id).state, zone[CONF_ACTIVATION] + hass.states.get(zone_entity_id).state, # type: ignore[union-attr] + zone[CONF_ACTIVATION], ) return self.json(resp) diff --git a/homeassistant/components/konnected/binary_sensor.py b/homeassistant/components/konnected/binary_sensor.py index d7c41337342..75c381c53f2 100644 --- a/homeassistant/components/konnected/binary_sensor.py +++ b/homeassistant/components/konnected/binary_sensor.py @@ -1,4 +1,5 @@ """Support for wired binary sensors attached to a Konnected device.""" + from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( diff --git a/homeassistant/components/konnected/config_flow.py b/homeassistant/components/konnected/config_flow.py index c9889dd6464..29f4fbe2a49 100644 --- a/homeassistant/components/konnected/config_flow.py +++ b/homeassistant/components/konnected/config_flow.py @@ -1,4 +1,5 @@ """Config flow for konnected.io integration.""" + from __future__ import annotations import asyncio @@ -11,12 +12,17 @@ from urllib.parse import urlparse import voluptuous as vol -from homeassistant import config_entries from homeassistant.components import ssdp from homeassistant.components.binary_sensor import ( DEVICE_CLASSES_SCHEMA, BinarySensorDeviceClass, ) +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.const import ( CONF_ACCESS_TOKEN, CONF_BINARY_SENSORS, @@ -33,7 +39,6 @@ from homeassistant.const import ( CONF_ZONE, ) from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_validation as cv from .const import ( @@ -166,7 +171,7 @@ CONFIG_ENTRY_SCHEMA = vol.Schema( ) -class KonnectedFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class KonnectedFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a config flow for Konnected Panels.""" VERSION = 1 @@ -244,7 +249,9 @@ class KonnectedFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) return await self.async_step_user() - async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: + async def async_step_ssdp( + self, discovery_info: ssdp.SsdpServiceInfo + ) -> ConfigFlowResult: """Handle a discovered konnected panel. This flow is triggered by the SSDP component. It will check if the @@ -376,16 +383,16 @@ class KonnectedFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback def async_get_options_flow( - config_entry: config_entries.ConfigEntry, + config_entry: ConfigEntry, ) -> OptionsFlowHandler: """Return the Options Flow.""" return OptionsFlowHandler(config_entry) -class OptionsFlowHandler(config_entries.OptionsFlow): +class OptionsFlowHandler(OptionsFlow): """Handle a option flow for a Konnected Panel.""" - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + def __init__(self, config_entry: ConfigEntry) -> None: """Initialize options flow.""" self.entry = config_entry self.model = self.entry.data[CONF_MODEL] @@ -563,9 +570,10 @@ class OptionsFlowHandler(config_entries.OptionsFlow): if user_input is not None: zone = {"zone": self.active_cfg} zone.update(user_input) - self.new_opt[CONF_BINARY_SENSORS] = self.new_opt.get( - CONF_BINARY_SENSORS, [] - ) + [zone] + self.new_opt[CONF_BINARY_SENSORS] = [ + *self.new_opt.get(CONF_BINARY_SENSORS, []), + zone, + ] self.io_cfg.pop(self.active_cfg) self.active_cfg = None @@ -638,7 +646,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow): if user_input is not None: zone = {"zone": self.active_cfg} zone.update(user_input) - self.new_opt[CONF_SENSORS] = self.new_opt.get(CONF_SENSORS, []) + [zone] + self.new_opt[CONF_SENSORS] = [*self.new_opt.get(CONF_SENSORS, []), zone] self.io_cfg.pop(self.active_cfg) self.active_cfg = None @@ -707,7 +715,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow): zone = {"zone": self.active_cfg} zone.update(user_input) del zone[CONF_MORE_STATES] - self.new_opt[CONF_SWITCHES] = self.new_opt.get(CONF_SWITCHES, []) + [zone] + self.new_opt[CONF_SWITCHES] = [*self.new_opt.get(CONF_SWITCHES, []), zone] # iterate through multiple switch states if self.current_states: diff --git a/homeassistant/components/konnected/errors.py b/homeassistant/components/konnected/errors.py index 5a0207f3f8d..a377942a02f 100644 --- a/homeassistant/components/konnected/errors.py +++ b/homeassistant/components/konnected/errors.py @@ -1,4 +1,5 @@ """Errors for the Konnected component.""" + from homeassistant.exceptions import HomeAssistantError diff --git a/homeassistant/components/konnected/handlers.py b/homeassistant/components/konnected/handlers.py index af784750627..55fdc57bc46 100644 --- a/homeassistant/components/konnected/handlers.py +++ b/homeassistant/components/konnected/handlers.py @@ -1,4 +1,5 @@ """Handle Konnected messages.""" + import logging from homeassistant.components.sensor import SensorDeviceClass diff --git a/homeassistant/components/konnected/panel.py b/homeassistant/components/konnected/panel.py index 61c77f5a7de..605b27f7547 100644 --- a/homeassistant/components/konnected/panel.py +++ b/homeassistant/components/konnected/panel.py @@ -1,4 +1,5 @@ """Support for Konnected devices.""" + import asyncio import logging diff --git a/homeassistant/components/konnected/sensor.py b/homeassistant/components/konnected/sensor.py index 3f203d5f3e8..6191f98f179 100644 --- a/homeassistant/components/konnected/sensor.py +++ b/homeassistant/components/konnected/sensor.py @@ -1,4 +1,5 @@ """Support for DHT and DS18B20 sensors attached to a Konnected device.""" + from __future__ import annotations from homeassistant.components.sensor import ( diff --git a/homeassistant/components/konnected/switch.py b/homeassistant/components/konnected/switch.py index 18132a913ad..424a2d9164d 100644 --- a/homeassistant/components/konnected/switch.py +++ b/homeassistant/components/konnected/switch.py @@ -1,4 +1,5 @@ """Support for wired switches attached to a Konnected device.""" + import logging from typing import Any diff --git a/homeassistant/components/kostal_plenticore/__init__.py b/homeassistant/components/kostal_plenticore/__init__.py index b7e4c86f772..d3fb65ad77b 100644 --- a/homeassistant/components/kostal_plenticore/__init__.py +++ b/homeassistant/components/kostal_plenticore/__init__.py @@ -1,4 +1,5 @@ """The Kostal Plenticore Solar Inverter integration.""" + import logging from pykoplenti import ApiException diff --git a/homeassistant/components/kostal_plenticore/config_flow.py b/homeassistant/components/kostal_plenticore/config_flow.py index 8dd3a823570..c1c8ac249e0 100644 --- a/homeassistant/components/kostal_plenticore/config_flow.py +++ b/homeassistant/components/kostal_plenticore/config_flow.py @@ -1,11 +1,12 @@ """Config flow for Kostal Plenticore Solar Inverter integration.""" + import logging from aiohttp.client_exceptions import ClientError from pykoplenti import ApiClient, AuthenticationException import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_BASE, CONF_HOST, CONF_PASSWORD from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -38,7 +39,7 @@ async def test_connection(hass: HomeAssistant, data) -> str: return values["scb:network"][hostname_id] -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class KostalPlenticoreConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Kostal Plenticore Solar Inverter.""" VERSION = 1 diff --git a/homeassistant/components/kostal_plenticore/const.py b/homeassistant/components/kostal_plenticore/const.py index c0e897e6131..668b10e6971 100644 --- a/homeassistant/components/kostal_plenticore/const.py +++ b/homeassistant/components/kostal_plenticore/const.py @@ -1,2 +1,3 @@ """Constants for the Kostal Plenticore Solar Inverter integration.""" + DOMAIN = "kostal_plenticore" diff --git a/homeassistant/components/kostal_plenticore/diagnostics.py b/homeassistant/components/kostal_plenticore/diagnostics.py index eef9f05537f..9b78265971c 100644 --- a/homeassistant/components/kostal_plenticore/diagnostics.py +++ b/homeassistant/components/kostal_plenticore/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for Kostal Plenticore.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/kostal_plenticore/helper.py b/homeassistant/components/kostal_plenticore/helper.py index a04415a4f31..4a4e6539f03 100644 --- a/homeassistant/components/kostal_plenticore/helper.py +++ b/homeassistant/components/kostal_plenticore/helper.py @@ -1,4 +1,5 @@ """Code to handle the Plenticore API.""" + from __future__ import annotations from collections import defaultdict @@ -100,8 +101,10 @@ class Plenticore: manufacturer="Kostal", model=f"{prod1} {prod2}", name=settings["scb:network"][hostname_id], - sw_version=f'IOC: {device_local["Properties:VersionIOC"]}' - + f' MC: {device_local["Properties:VersionMC"]}', + sw_version=( + f'IOC: {device_local["Properties:VersionIOC"]}' + f' MC: {device_local["Properties:VersionMC"]}' + ), ) return True diff --git a/homeassistant/components/kostal_plenticore/number.py b/homeassistant/components/kostal_plenticore/number.py index 36e1fc95eb8..2e544a16fec 100644 --- a/homeassistant/components/kostal_plenticore/number.py +++ b/homeassistant/components/kostal_plenticore/number.py @@ -1,4 +1,5 @@ """Platform for Kostal Plenticore numbers.""" + from __future__ import annotations from dataclasses import dataclass @@ -26,9 +27,9 @@ from .helper import PlenticoreDataFormatter, SettingDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) -@dataclass(frozen=True) -class PlenticoreNumberEntityDescriptionMixin: - """Define an entity description mixin for number entities.""" +@dataclass(frozen=True, kw_only=True) +class PlenticoreNumberEntityDescription(NumberEntityDescription): + """Describes a Plenticore number entity.""" module_id: str data_id: str @@ -36,13 +37,6 @@ class PlenticoreNumberEntityDescriptionMixin: fmt_to: str -@dataclass(frozen=True) -class PlenticoreNumberEntityDescription( - NumberEntityDescription, PlenticoreNumberEntityDescriptionMixin -): - """Describes a Plenticore number entity.""" - - NUMBER_SETTINGS_DATA = [ PlenticoreNumberEntityDescription( key="battery_min_soc", diff --git a/homeassistant/components/kostal_plenticore/select.py b/homeassistant/components/kostal_plenticore/select.py index 321bc4e5d70..555bb89641b 100644 --- a/homeassistant/components/kostal_plenticore/select.py +++ b/homeassistant/components/kostal_plenticore/select.py @@ -1,4 +1,5 @@ """Platform for Kostal Plenticore select widgets.""" + from __future__ import annotations from dataclasses import dataclass @@ -19,20 +20,13 @@ from .helper import Plenticore, SelectDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) -@dataclass(frozen=True) -class PlenticoreRequiredKeysMixin: - """A class that describes required properties for plenticore select entities.""" +@dataclass(frozen=True, kw_only=True) +class PlenticoreSelectEntityDescription(SelectEntityDescription): + """A class that describes plenticore select entities.""" module_id: str -@dataclass(frozen=True) -class PlenticoreSelectEntityDescription( - SelectEntityDescription, PlenticoreRequiredKeysMixin -): - """A class that describes plenticore select entities.""" - - SELECT_SETTINGS_DATA = [ PlenticoreSelectEntityDescription( module_id="devices:local", diff --git a/homeassistant/components/kostal_plenticore/sensor.py b/homeassistant/components/kostal_plenticore/sensor.py index 237a50f85b7..d6e13ecb5b7 100644 --- a/homeassistant/components/kostal_plenticore/sensor.py +++ b/homeassistant/components/kostal_plenticore/sensor.py @@ -1,4 +1,5 @@ """Platform for Kostal Plenticore sensors.""" + from __future__ import annotations from collections.abc import Callable @@ -33,21 +34,14 @@ from .helper import PlenticoreDataFormatter, ProcessDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) -@dataclass(frozen=True) -class PlenticoreRequiredKeysMixin: - """A class that describes required properties for plenticore sensor entities.""" +@dataclass(frozen=True, kw_only=True) +class PlenticoreSensorEntityDescription(SensorEntityDescription): + """A class that describes plenticore sensor entities.""" module_id: str formatter: str -@dataclass(frozen=True) -class PlenticoreSensorEntityDescription( - SensorEntityDescription, PlenticoreRequiredKeysMixin -): - """A class that describes plenticore sensor entities.""" - - SENSOR_PROCESS_DATA = [ PlenticoreSensorEntityDescription( module_id="devices:local", diff --git a/homeassistant/components/kostal_plenticore/switch.py b/homeassistant/components/kostal_plenticore/switch.py index 509a3610884..f2ea1a5ef7c 100644 --- a/homeassistant/components/kostal_plenticore/switch.py +++ b/homeassistant/components/kostal_plenticore/switch.py @@ -1,4 +1,5 @@ """Platform for Kostal Plenticore switches.""" + from __future__ import annotations from dataclasses import dataclass @@ -20,9 +21,9 @@ from .helper import SettingDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) -@dataclass(frozen=True) -class PlenticoreRequiredKeysMixin: - """A class that describes required properties for plenticore switch entities.""" +@dataclass(frozen=True, kw_only=True) +class PlenticoreSwitchEntityDescription(SwitchEntityDescription): + """A class that describes plenticore switch entities.""" module_id: str is_on: str @@ -32,13 +33,6 @@ class PlenticoreRequiredKeysMixin: off_label: str -@dataclass(frozen=True) -class PlenticoreSwitchEntityDescription( - SwitchEntityDescription, PlenticoreRequiredKeysMixin -): - """A class that describes plenticore switch entities.""" - - SWITCH_SETTINGS_DATA = [ PlenticoreSwitchEntityDescription( module_id="devices:local", diff --git a/homeassistant/components/kraken/__init__.py b/homeassistant/components/kraken/__init__.py index 395de951bbd..692f602460b 100644 --- a/homeassistant/components/kraken/__init__.py +++ b/homeassistant/components/kraken/__init__.py @@ -1,4 +1,5 @@ """The kraken integration.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/kraken/config_flow.py b/homeassistant/components/kraken/config_flow.py index ff4c882504c..3375746f25d 100644 --- a/homeassistant/components/kraken/config_flow.py +++ b/homeassistant/components/kraken/config_flow.py @@ -1,4 +1,5 @@ """Config flow for kraken integration.""" + from __future__ import annotations from typing import Any @@ -7,17 +8,21 @@ import krakenex from pykrakenapi.pykrakenapi import KrakenAPI import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.const import CONF_SCAN_INTERVAL from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_validation as cv from .const import CONF_TRACKED_ASSET_PAIRS, DEFAULT_SCAN_INTERVAL, DOMAIN from .utils import get_tradable_asset_pairs -class KrakenConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class KrakenConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for kraken.""" VERSION = 1 @@ -25,14 +30,14 @@ class KrakenConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback def async_get_options_flow( - config_entry: config_entries.ConfigEntry, + config_entry: ConfigEntry, ) -> KrakenOptionsFlowHandler: """Get the options flow for this handler.""" return KrakenOptionsFlowHandler(config_entry) async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" if self._async_current_entries(): return self.async_abort(reason="already_configured") @@ -45,16 +50,16 @@ class KrakenConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -class KrakenOptionsFlowHandler(config_entries.OptionsFlow): +class KrakenOptionsFlowHandler(OptionsFlow): """Handle Kraken client options.""" - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + def __init__(self, config_entry: ConfigEntry) -> None: """Initialize Kraken options flow.""" self.config_entry = config_entry async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Manage the Kraken options.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) diff --git a/homeassistant/components/kraken/const.py b/homeassistant/components/kraken/const.py index 8a5f7fa828f..3b1bc29c7cd 100644 --- a/homeassistant/components/kraken/const.py +++ b/homeassistant/components/kraken/const.py @@ -1,4 +1,5 @@ """Constants for the kraken integration.""" + from __future__ import annotations from typing import TypedDict diff --git a/homeassistant/components/kraken/sensor.py b/homeassistant/components/kraken/sensor.py index 7e55da2b189..37fee795783 100644 --- a/homeassistant/components/kraken/sensor.py +++ b/homeassistant/components/kraken/sensor.py @@ -1,4 +1,5 @@ """The kraken integration.""" + from __future__ import annotations from collections.abc import Callable @@ -32,18 +33,13 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -@dataclass(frozen=True) -class KrakenRequiredKeysMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class KrakenSensorEntityDescription(SensorEntityDescription): + """Describes Kraken sensor entity.""" value_fn: Callable[[DataUpdateCoordinator[KrakenResponse], str], float | int] -@dataclass(frozen=True) -class KrakenSensorEntityDescription(SensorEntityDescription, KrakenRequiredKeysMixin): - """Describes Kraken sensor entity.""" - - SENSOR_TYPES: tuple[KrakenSensorEntityDescription, ...] = ( KrakenSensorEntityDescription( key="ask", diff --git a/homeassistant/components/kraken/utils.py b/homeassistant/components/kraken/utils.py index 3c3d80dafb6..210756a7792 100644 --- a/homeassistant/components/kraken/utils.py +++ b/homeassistant/components/kraken/utils.py @@ -1,4 +1,5 @@ """Utility functions for the kraken integration.""" + from __future__ import annotations from pykrakenapi.pykrakenapi import KrakenAPI diff --git a/homeassistant/components/kulersky/config_flow.py b/homeassistant/components/kulersky/config_flow.py index 1f9c67b9aa1..fca214dd9a3 100644 --- a/homeassistant/components/kulersky/config_flow.py +++ b/homeassistant/components/kulersky/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Kuler Sky.""" + import logging import pykulersky diff --git a/homeassistant/components/kulersky/const.py b/homeassistant/components/kulersky/const.py index 8b314d7bde9..8d0b4380bb3 100644 --- a/homeassistant/components/kulersky/const.py +++ b/homeassistant/components/kulersky/const.py @@ -1,4 +1,5 @@ """Constants for the Kuler Sky integration.""" + DOMAIN = "kulersky" DATA_ADDRESSES = "addresses" diff --git a/homeassistant/components/kulersky/light.py b/homeassistant/components/kulersky/light.py index 6636bfdba9f..cb98e52250f 100644 --- a/homeassistant/components/kulersky/light.py +++ b/homeassistant/components/kulersky/light.py @@ -1,4 +1,5 @@ """Kuler Sky light platform.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/kwb/sensor.py b/homeassistant/components/kwb/sensor.py index c15cff72655..e55b90cf89f 100644 --- a/homeassistant/components/kwb/sensor.py +++ b/homeassistant/components/kwb/sensor.py @@ -1,4 +1,5 @@ """Support for KWB Easyfire.""" + from __future__ import annotations from pykwb import kwb @@ -71,16 +72,14 @@ def setup_platform( easyfire.run_thread() - sensors = [] - for sensor in easyfire.get_sensors(): - if (sensor.sensor_type != kwb.PROP_SENSOR_RAW) or ( - sensor.sensor_type == kwb.PROP_SENSOR_RAW and raw - ): - sensors.append(KWBSensor(easyfire, sensor, client_name)) - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, lambda event: easyfire.stop_thread()) - add_entities(sensors) + add_entities( + KWBSensor(easyfire, sensor, client_name) + for sensor in easyfire.get_sensors() + if (sensor.sensor_type != kwb.PROP_SENSOR_RAW) + or (sensor.sensor_type == kwb.PROP_SENSOR_RAW and raw) + ) class KWBSensor(SensorEntity): diff --git a/homeassistant/components/lacrosse/sensor.py b/homeassistant/components/lacrosse/sensor.py index 40d38da55eb..c059248b422 100644 --- a/homeassistant/components/lacrosse/sensor.py +++ b/homeassistant/components/lacrosse/sensor.py @@ -1,4 +1,5 @@ """Support for LaCrosse sensor components.""" + from __future__ import annotations from datetime import datetime, timedelta diff --git a/homeassistant/components/lacrosse_view/__init__.py b/homeassistant/components/lacrosse_view/__init__.py index 86793a94a4b..d977af418a2 100644 --- a/homeassistant/components/lacrosse_view/__init__.py +++ b/homeassistant/components/lacrosse_view/__init__.py @@ -1,4 +1,5 @@ """The LaCrosse View integration.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/lacrosse_view/config_flow.py b/homeassistant/components/lacrosse_view/config_flow.py index 67d294de179..805afc40d2b 100644 --- a/homeassistant/components/lacrosse_view/config_flow.py +++ b/homeassistant/components/lacrosse_view/config_flow.py @@ -1,4 +1,5 @@ """Config flow for LaCrosse View integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -8,9 +9,8 @@ from typing import Any from lacrosse_view import LaCrosse, Location, LoginError import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -45,7 +45,7 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> list[Loca return locations -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class LaCrosseViewConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for LaCrosse View.""" VERSION = 1 @@ -54,11 +54,11 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Initialize the config flow.""" self.data: dict[str, str] = {} self.locations: list[Location] = [] - self._reauth_entry: config_entries.ConfigEntry | None = None + self._reauth_entry: ConfigEntry | None = None async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" if user_input is None: _LOGGER.debug("Showing initial form") @@ -100,7 +100,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_location( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the location step.""" if not user_input: @@ -135,7 +135,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): }, ) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Reauth in case of a password change or other error.""" self._reauth_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] diff --git a/homeassistant/components/lacrosse_view/coordinator.py b/homeassistant/components/lacrosse_view/coordinator.py index b45fe3ae1b4..5ec02a86709 100644 --- a/homeassistant/components/lacrosse_view/coordinator.py +++ b/homeassistant/components/lacrosse_view/coordinator.py @@ -1,4 +1,5 @@ """DataUpdateCoordinator for LaCrosse View.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/lacrosse_view/diagnostics.py b/homeassistant/components/lacrosse_view/diagnostics.py index 754cc39d38e..eaf3ded6a4a 100644 --- a/homeassistant/components/lacrosse_view/diagnostics.py +++ b/homeassistant/components/lacrosse_view/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for LaCrosse View.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/lacrosse_view/sensor.py b/homeassistant/components/lacrosse_view/sensor.py index 960ab0ff325..b2ad9672504 100644 --- a/homeassistant/components/lacrosse_view/sensor.py +++ b/homeassistant/components/lacrosse_view/sensor.py @@ -1,4 +1,5 @@ """Sensor component for LaCrosse View.""" + from __future__ import annotations from collections.abc import Callable @@ -35,20 +36,13 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -@dataclass(frozen=True) -class LaCrosseSensorEntityDescriptionMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class LaCrosseSensorEntityDescription(SensorEntityDescription): + """Description for LaCrosse View sensor.""" value_fn: Callable[[Sensor, str], float | int | str | None] -@dataclass(frozen=True) -class LaCrosseSensorEntityDescription( - SensorEntityDescription, LaCrosseSensorEntityDescriptionMixin -): - """Description for LaCrosse View sensor.""" - - def get_value(sensor: Sensor, field: str) -> float | int | str | None: """Get the value of a sensor field.""" field_data = sensor.data.get(field) @@ -171,11 +165,13 @@ async def async_setup_entry( message = ( f"Unsupported sensor field: {field}\nPlease create an issue on " "GitHub." - " https://github.com/home-assistant/core/issues/new?assignees=&la" - "bels=&template=bug_report.yml&integration_name=LaCrosse%20View&integrat" - "ion_link=https://www.home-assistant.io/integrations/lacrosse_view/&addi" - f"tional_information=Field:%20{field}%0ASensor%20Model:%20{sensor.model}&" - f"title=LaCrosse%20View%20Unsupported%20sensor%20field:%20{field}" + " https://github.com/home-assistant/core/issues/new?assignees=" + "&labels=&template=bug_report.yml&integration_name=LaCrosse%20View" + "&integration_link=" + "https://www.home-assistant.io/integrations/lacrosse_view/" + "&additional_information=" + f"Field:%20{field}%0ASensor%20Model:%20{sensor.model}" + f"&title=LaCrosse%20View%20Unsupported%20sensor%20field:%20{field}" ) _LOGGER.warning(message) diff --git a/homeassistant/components/lamarzocco/__init__.py b/homeassistant/components/lamarzocco/__init__.py index 0cdacc8d2e4..d2a7bbb6216 100644 --- a/homeassistant/components/lamarzocco/__init__.py +++ b/homeassistant/components/lamarzocco/__init__.py @@ -29,6 +29,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: + await hass.config_entries.async_reload(entry.entry_id) + + entry.async_on_unload(entry.add_update_listener(update_listener)) + return True diff --git a/homeassistant/components/lamarzocco/config_flow.py b/homeassistant/components/lamarzocco/config_flow.py index 7c63532104f..3cacdae1749 100644 --- a/homeassistant/components/lamarzocco/config_flow.py +++ b/homeassistant/components/lamarzocco/config_flow.py @@ -1,4 +1,5 @@ """Config flow for La Marzocco integration.""" + from collections.abc import Mapping import logging from typing import Any @@ -7,9 +8,22 @@ from lmcloud import LMCloud as LaMarzoccoClient from lmcloud.exceptions import AuthFail, RequestNotSuccessful import voluptuous as vol -from homeassistant.config_entries import ConfigEntry, ConfigFlow -from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME -from homeassistant.data_entry_flow import FlowResult +from homeassistant.components.bluetooth import BluetoothServiceInfo +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, + OptionsFlowWithConfigEntry, +) +from homeassistant.const import ( + CONF_HOST, + CONF_MAC, + CONF_NAME, + CONF_PASSWORD, + CONF_USERNAME, +) +from homeassistant.core import callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.selector import ( SelectOptionDict, @@ -18,7 +32,7 @@ from homeassistant.helpers.selector import ( SelectSelectorMode, ) -from .const import CONF_MACHINE, DOMAIN +from .const import CONF_MACHINE, CONF_USE_BLUETOOTH, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -32,10 +46,11 @@ class LmConfigFlow(ConfigFlow, domain=DOMAIN): self.reauth_entry: ConfigEntry | None = None self._config: dict[str, Any] = {} self._machines: list[tuple[str, str]] = [] + self._discovered: dict[str, str] = {} async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors = {} @@ -47,6 +62,7 @@ class LmConfigFlow(ConfigFlow, domain=DOMAIN): data = { **data, **user_input, + **self._discovered, } lm = LaMarzoccoClient() @@ -71,6 +87,18 @@ class LmConfigFlow(ConfigFlow, domain=DOMAIN): self.reauth_entry.entry_id ) return self.async_abort(reason="reauth_successful") + if self._discovered: + serials = [machine[0] for machine in self._machines] + if self._discovered[CONF_MACHINE] not in serials: + errors["base"] = "machine_not_found" + else: + self._config = data + return self.async_show_form( + step_id="machine_selection", + data_schema=vol.Schema( + {vol.Optional(CONF_HOST): cv.string} + ), + ) if not errors: self._config = data @@ -89,13 +117,16 @@ class LmConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_machine_selection( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Let user select machine to connect to.""" errors: dict[str, str] = {} if user_input: - serial_number = user_input[CONF_MACHINE] - await self.async_set_unique_id(serial_number) - self._abort_if_unique_id_configured() + if not self._discovered: + serial_number = user_input[CONF_MACHINE] + await self.async_set_unique_id(serial_number) + self._abort_if_unique_id_configured() + else: + serial_number = self._discovered[CONF_MACHINE] # validate local connection if host is provided if user_input.get(CONF_HOST): @@ -141,7 +172,33 @@ class LmConfigFlow(ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_bluetooth( + self, discovery_info: BluetoothServiceInfo + ) -> ConfigFlowResult: + """Handle a flow initialized by discovery over Bluetooth.""" + address = discovery_info.address + name = discovery_info.name + + _LOGGER.debug( + "Discovered La Marzocco machine %s through Bluetooth at address %s", + name, + address, + ) + + self._discovered[CONF_NAME] = name + self._discovered[CONF_MAC] = address + + serial = name.split("_")[1] + self._discovered[CONF_MACHINE] = serial + + await self.async_set_unique_id(serial) + self._abort_if_unique_id_configured() + + return await self.async_step_user() + + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Perform reauth upon an API authentication error.""" self.reauth_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] @@ -150,7 +207,7 @@ class LmConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Dialog that informs the user that reauth is required.""" if not user_input: return self.async_show_form( @@ -163,3 +220,36 @@ class LmConfigFlow(ConfigFlow, domain=DOMAIN): ) return await self.async_step_user(user_input) + + @staticmethod + @callback + def async_get_options_flow( + config_entry: ConfigEntry, + ) -> OptionsFlow: + """Create the options flow.""" + return LmOptionsFlowHandler(config_entry) + + +class LmOptionsFlowHandler(OptionsFlowWithConfigEntry): + """Handles options flow for the component.""" + + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Manage the options for the custom component.""" + if user_input: + return self.async_create_entry(title="", data=user_input) + + options_schema = vol.Schema( + { + vol.Optional( + CONF_USE_BLUETOOTH, + default=self.options.get(CONF_USE_BLUETOOTH, True), + ): cv.boolean, + } + ) + + return self.async_show_form( + step_id="init", + data_schema=options_schema, + ) diff --git a/homeassistant/components/lamarzocco/const.py b/homeassistant/components/lamarzocco/const.py index 2afd1c4cf48..87878ea5089 100644 --- a/homeassistant/components/lamarzocco/const.py +++ b/homeassistant/components/lamarzocco/const.py @@ -5,3 +5,5 @@ from typing import Final DOMAIN: Final = "lamarzocco" CONF_MACHINE: Final = "machine" + +CONF_USE_BLUETOOTH = "use_bluetooth" diff --git a/homeassistant/components/lamarzocco/coordinator.py b/homeassistant/components/lamarzocco/coordinator.py index 438c4e42634..7901b0bb3fa 100644 --- a/homeassistant/components/lamarzocco/coordinator.py +++ b/homeassistant/components/lamarzocco/coordinator.py @@ -1,26 +1,36 @@ """Coordinator for La Marzocco API.""" + from collections.abc import Callable, Coroutine from datetime import timedelta import logging from typing import Any +from bleak.backends.device import BLEDevice from lmcloud import LMCloud as LaMarzoccoClient +from lmcloud.const import BT_MODEL_NAMES from lmcloud.exceptions import AuthFail, RequestNotSuccessful +from homeassistant.components.bluetooth import ( + async_ble_device_from_address, + async_discovered_service_info, +) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST +from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.httpx_client import get_async_client from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import CONF_MACHINE, DOMAIN +from .const import CONF_MACHINE, CONF_USE_BLUETOOTH, DOMAIN SCAN_INTERVAL = timedelta(seconds=30) _LOGGER = logging.getLogger(__name__) +NAME_PREFIXES = tuple(BT_MODEL_NAMES) + + class LaMarzoccoUpdateCoordinator(DataUpdateCoordinator[None]): """Class to handle fetching data from the La Marzocco API centrally.""" @@ -35,6 +45,7 @@ class LaMarzoccoUpdateCoordinator(DataUpdateCoordinator[None]): self.local_connection_configured = ( self.config_entry.data.get(CONF_HOST) is not None ) + self._use_bluetooth = False async def _async_update_data(self) -> None: """Fetch data from API endpoint.""" @@ -79,6 +90,46 @@ class LaMarzoccoUpdateCoordinator(DataUpdateCoordinator[None]): name="lm_websocket_task", ) + # initialize Bluetooth + if self.config_entry.options.get(CONF_USE_BLUETOOTH, True): + + def bluetooth_configured() -> bool: + return self.config_entry.data.get( + CONF_MAC, "" + ) and self.config_entry.data.get(CONF_NAME, "") + + if not bluetooth_configured(): + machine = self.config_entry.data[CONF_MACHINE] + for discovery_info in async_discovered_service_info(self.hass): + if ( + (name := discovery_info.name) + and name.startswith(NAME_PREFIXES) + and name.split("_")[1] == machine + ): + _LOGGER.debug( + "Found Bluetooth device, configuring with Bluetooth" + ) + # found a device, add MAC address to config entry + self.hass.config_entries.async_update_entry( + self.config_entry, + data={ + **self.config_entry.data, + CONF_MAC: discovery_info.address, + CONF_NAME: discovery_info.name, + }, + ) + break + + if bluetooth_configured(): + # config entry contains BT config + _LOGGER.debug("Initializing with known Bluetooth device") + await self.lm.init_bluetooth_with_known_device( + self.config_entry.data[CONF_USERNAME], + self.config_entry.data.get(CONF_MAC, ""), + self.config_entry.data.get(CONF_NAME, ""), + ) + self._use_bluetooth = True + self.lm.initialized = True async def _async_handle_request( @@ -97,3 +148,15 @@ class LaMarzoccoUpdateCoordinator(DataUpdateCoordinator[None]): except RequestNotSuccessful as ex: _LOGGER.debug(ex, exc_info=True) raise UpdateFailed("Querying API failed. Error: %s" % ex) from ex + + def async_get_ble_device(self) -> BLEDevice | None: + """Get a Bleak Client for the machine.""" + # according to HA best practices, we should not reuse the same client + # get a new BLE device from hass and init a new Bleak Client with it + if not self._use_bluetooth: + return None + + return async_ble_device_from_address( + self.hass, + self.lm.lm_bluetooth.address, + ) diff --git a/homeassistant/components/lamarzocco/diagnostics.py b/homeassistant/components/lamarzocco/diagnostics.py index 6e75152bd60..648d1357a35 100644 --- a/homeassistant/components/lamarzocco/diagnostics.py +++ b/homeassistant/components/lamarzocco/diagnostics.py @@ -1,6 +1,5 @@ """Diagnostics support for La Marzocco.""" - from __future__ import annotations from typing import Any diff --git a/homeassistant/components/lamarzocco/manifest.json b/homeassistant/components/lamarzocco/manifest.json index 8dd8e1294b0..ec6068e1988 100644 --- a/homeassistant/components/lamarzocco/manifest.json +++ b/homeassistant/components/lamarzocco/manifest.json @@ -1,8 +1,23 @@ { "domain": "lamarzocco", "name": "La Marzocco", + "bluetooth": [ + { + "local_name": "MICRA_*" + }, + { + "local_name": "MINI_*" + }, + { + "local_name": "GS3_*" + }, + { + "local_name": "GS3AV_*" + } + ], "codeowners": ["@zweckj"], "config_flow": true, + "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/lamarzocco", "integration_type": "device", "iot_class": "cloud_polling", diff --git a/homeassistant/components/lamarzocco/number.py b/homeassistant/components/lamarzocco/number.py index 05f937f48f6..af5256bc77b 100644 --- a/homeassistant/components/lamarzocco/number.py +++ b/homeassistant/components/lamarzocco/number.py @@ -63,7 +63,9 @@ ENTITIES: tuple[LaMarzoccoNumberEntityDescription, ...] = ( native_step=PRECISION_TENTHS, native_min_value=85, native_max_value=104, - set_value_fn=lambda coordinator, temp: coordinator.lm.set_coffee_temp(temp), + set_value_fn=lambda coordinator, temp: coordinator.lm.set_coffee_temp( + temp, coordinator.async_get_ble_device() + ), native_value_fn=lambda lm: lm.current_status["coffee_set_temp"], ), LaMarzoccoNumberEntityDescription( @@ -74,7 +76,9 @@ ENTITIES: tuple[LaMarzoccoNumberEntityDescription, ...] = ( native_step=PRECISION_WHOLE, native_min_value=126, native_max_value=131, - set_value_fn=lambda coordinator, temp: coordinator.lm.set_steam_temp(int(temp)), + set_value_fn=lambda coordinator, temp: coordinator.lm.set_steam_temp( + int(temp), coordinator.async_get_ble_device() + ), native_value_fn=lambda lm: lm.current_status["steam_set_temp"], supported_fn=lambda coordinator: coordinator.lm.model_name in ( @@ -208,20 +212,19 @@ async def async_setup_entry( """Set up number entities.""" coordinator = hass.data[DOMAIN][config_entry.entry_id] - async_add_entities( + entities: list[NumberEntity] = [ LaMarzoccoNumberEntity(coordinator, description) for description in ENTITIES if description.supported_fn(coordinator) - ) + ] - entities: list[LaMarzoccoKeyNumberEntity] = [] for description in KEY_ENTITIES: if description.supported_fn(coordinator): num_keys = KEYS_PER_MODEL[coordinator.lm.model_name] - for key in range(min(num_keys, 1), num_keys + 1): - entities.append( - LaMarzoccoKeyNumberEntity(coordinator, description, key) - ) + entities.extend( + LaMarzoccoKeyNumberEntity(coordinator, description, key) + for key in range(min(num_keys, 1), num_keys + 1) + ) async_add_entities(entities) diff --git a/homeassistant/components/lamarzocco/select.py b/homeassistant/components/lamarzocco/select.py index 1e70000a479..f063f8e6336 100644 --- a/homeassistant/components/lamarzocco/select.py +++ b/homeassistant/components/lamarzocco/select.py @@ -36,7 +36,7 @@ ENTITIES: tuple[LaMarzoccoSelectEntityDescription, ...] = ( translation_key="steam_temp_select", options=["1", "2", "3"], select_option_fn=lambda coordinator, option: coordinator.lm.set_steam_level( - int(option) + int(option), coordinator.async_get_ble_device() ), current_option_fn=lambda lm: lm.current_status["steam_level_set"], supported_fn=lambda coordinator: coordinator.lm.model_name diff --git a/homeassistant/components/lamarzocco/strings.json b/homeassistant/components/lamarzocco/strings.json index 57421dfee83..03ce2eb93e8 100644 --- a/homeassistant/components/lamarzocco/strings.json +++ b/homeassistant/components/lamarzocco/strings.json @@ -42,6 +42,16 @@ } } }, + "options": { + "step": { + "init": { + "data": { + "title": "Update Configuration", + "use_bluetooth": "Use Bluetooth" + } + } + } + }, "entity": { "binary_sensor": { "brew_active": { diff --git a/homeassistant/components/lamarzocco/switch.py b/homeassistant/components/lamarzocco/switch.py index 0d4d8d7dc8e..dd647bf4582 100644 --- a/homeassistant/components/lamarzocco/switch.py +++ b/homeassistant/components/lamarzocco/switch.py @@ -1,4 +1,5 @@ """Switch platform for La Marzocco espresso machines.""" + from collections.abc import Callable, Coroutine from dataclasses import dataclass from typing import Any @@ -30,7 +31,9 @@ ENTITIES: tuple[LaMarzoccoSwitchEntityDescription, ...] = ( key="main", translation_key="main", name=None, - control_fn=lambda coordinator, state: coordinator.lm.set_power(state), + control_fn=lambda coordinator, state: coordinator.lm.set_power( + state, coordinator.async_get_ble_device() + ), is_on_fn=lambda coordinator: coordinator.lm.current_status["power"], ), LaMarzoccoSwitchEntityDescription( @@ -46,7 +49,9 @@ ENTITIES: tuple[LaMarzoccoSwitchEntityDescription, ...] = ( LaMarzoccoSwitchEntityDescription( key="steam_boiler_enable", translation_key="steam_boiler", - control_fn=lambda coordinator, state: coordinator.lm.set_steam(state), + control_fn=lambda coordinator, state: coordinator.lm.set_steam( + state, coordinator.async_get_ble_device() + ), is_on_fn=lambda coordinator: coordinator.lm.current_status[ "steam_boiler_enable" ], diff --git a/homeassistant/components/lametric/__init__.py b/homeassistant/components/lametric/__init__.py index 867b80cf408..10fdee0ddc7 100644 --- a/homeassistant/components/lametric/__init__.py +++ b/homeassistant/components/lametric/__init__.py @@ -1,4 +1,5 @@ """Support for LaMetric time.""" + from homeassistant.components import notify as hass_notify from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME, Platform diff --git a/homeassistant/components/lametric/application_credentials.py b/homeassistant/components/lametric/application_credentials.py index ab763c8f6fb..9c478be67f0 100644 --- a/homeassistant/components/lametric/application_credentials.py +++ b/homeassistant/components/lametric/application_credentials.py @@ -1,4 +1,5 @@ """Application credentials platform for LaMetric.""" + from homeassistant.components.application_credentials import AuthorizationServer from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/lametric/button.py b/homeassistant/components/lametric/button.py index dacbf8d2445..f0a452f2d02 100644 --- a/homeassistant/components/lametric/button.py +++ b/homeassistant/components/lametric/button.py @@ -1,4 +1,5 @@ """Support for LaMetric buttons.""" + from __future__ import annotations from collections.abc import Awaitable, Callable @@ -30,28 +31,24 @@ BUTTONS = [ LaMetricButtonEntityDescription( key="app_next", translation_key="app_next", - icon="mdi:arrow-right-bold", entity_category=EntityCategory.CONFIG, press_fn=lambda api: api.app_next(), ), LaMetricButtonEntityDescription( key="app_previous", translation_key="app_previous", - icon="mdi:arrow-left-bold", entity_category=EntityCategory.CONFIG, press_fn=lambda api: api.app_previous(), ), LaMetricButtonEntityDescription( key="dismiss_current", translation_key="dismiss_current", - icon="mdi:bell-cancel", entity_category=EntityCategory.CONFIG, press_fn=lambda api: api.dismiss_current_notification(), ), LaMetricButtonEntityDescription( key="dismiss_all", translation_key="dismiss_all", - icon="mdi:bell-cancel", entity_category=EntityCategory.CONFIG, press_fn=lambda api: api.dismiss_all_notifications(), ), diff --git a/homeassistant/components/lametric/config_flow.py b/homeassistant/components/lametric/config_flow.py index 1dad190d706..ed1477e1149 100644 --- a/homeassistant/components/lametric/config_flow.py +++ b/homeassistant/components/lametric/config_flow.py @@ -1,4 +1,5 @@ """Config flow to configure the LaMetric integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -28,9 +29,9 @@ from homeassistant.components.ssdp import ( ATTR_UPNP_SERIAL, SsdpServiceInfo, ) -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import ConfigEntry, ConfigFlowResult from homeassistant.const import CONF_API_KEY, CONF_DEVICE, CONF_HOST, CONF_MAC -from homeassistant.data_entry_flow import AbortFlow, FlowResult +from homeassistant.data_entry_flow import AbortFlow from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.config_entry_oauth2_flow import AbstractOAuth2FlowHandler from homeassistant.helpers.device_registry import format_mac @@ -72,11 +73,13 @@ class LaMetricFlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initiated by the user.""" return await self.async_step_choice_enter_manual_or_fetch_cloud() - async def async_step_ssdp(self, discovery_info: SsdpServiceInfo) -> FlowResult: + async def async_step_ssdp( + self, discovery_info: SsdpServiceInfo + ) -> ConfigFlowResult: """Handle a flow initiated by SSDP discovery.""" url = URL(discovery_info.ssdp_location or "") if url.host is None or not ( @@ -106,7 +109,9 @@ class LaMetricFlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN): self.discovered_serial = serial return await self.async_step_choice_enter_manual_or_fetch_cloud() - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Handle initiation of re-authentication with LaMetric.""" self.reauth_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] @@ -115,7 +120,7 @@ class LaMetricFlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN): async def async_step_choice_enter_manual_or_fetch_cloud( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the user's choice. Either enter the manual credentials or fetch the cloud credentials. @@ -127,7 +132,7 @@ class LaMetricFlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN): async def async_step_manual_entry( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the user's choice of entering the device manually.""" errors: dict[str, str] = {} if user_input is not None: @@ -166,7 +171,9 @@ class LaMetricFlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN): errors=errors, ) - async def async_step_cloud_fetch_devices(self, data: dict[str, Any]) -> FlowResult: + async def async_step_cloud_fetch_devices( + self, data: dict[str, Any] + ) -> ConfigFlowResult: """Fetch information about devices from the cloud.""" lametric = LaMetricCloud( token=data["token"]["access_token"], @@ -184,7 +191,7 @@ class LaMetricFlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN): async def async_step_cloud_select_device( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle device selection from devices offered by the cloud.""" if self.discovered: user_input = {CONF_DEVICE: self.discovered_serial} @@ -232,7 +239,9 @@ class LaMetricFlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN): errors=errors, ) - async def _async_step_create_entry(self, host: str, api_key: str) -> FlowResult: + async def _async_step_create_entry( + self, host: str, api_key: str + ) -> ConfigFlowResult: """Create entry.""" lametric = LaMetricDevice( host=host, @@ -287,7 +296,9 @@ class LaMetricFlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN): }, ) - async def async_step_dhcp(self, discovery_info: DhcpServiceInfo) -> FlowResult: + async def async_step_dhcp( + self, discovery_info: DhcpServiceInfo + ) -> ConfigFlowResult: """Handle dhcp discovery to update existing entries.""" mac = format_mac(discovery_info.macaddress) for entry in self._async_current_entries(): diff --git a/homeassistant/components/lametric/coordinator.py b/homeassistant/components/lametric/coordinator.py index 88f34adf45c..6655b035740 100644 --- a/homeassistant/components/lametric/coordinator.py +++ b/homeassistant/components/lametric/coordinator.py @@ -1,4 +1,5 @@ """DataUpdateCoordinator for the LaMatric integration.""" + from __future__ import annotations from demetriek import Device, LaMetricAuthenticationError, LaMetricDevice, LaMetricError diff --git a/homeassistant/components/lametric/diagnostics.py b/homeassistant/components/lametric/diagnostics.py index 256f5f06e91..69c681e911a 100644 --- a/homeassistant/components/lametric/diagnostics.py +++ b/homeassistant/components/lametric/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for LaMetric.""" + from __future__ import annotations import json diff --git a/homeassistant/components/lametric/entity.py b/homeassistant/components/lametric/entity.py index 54626a3838d..43539f13185 100644 --- a/homeassistant/components/lametric/entity.py +++ b/homeassistant/components/lametric/entity.py @@ -1,4 +1,5 @@ """Base entity for the LaMetric integration.""" + from __future__ import annotations from homeassistant.helpers.device_registry import ( diff --git a/homeassistant/components/lametric/helpers.py b/homeassistant/components/lametric/helpers.py index 3a3014a369e..24c028da78c 100644 --- a/homeassistant/components/lametric/helpers.py +++ b/homeassistant/components/lametric/helpers.py @@ -1,4 +1,5 @@ """Helpers for LaMetric.""" + from __future__ import annotations from collections.abc import Callable, Coroutine diff --git a/homeassistant/components/lametric/icons.json b/homeassistant/components/lametric/icons.json new file mode 100644 index 00000000000..7e1841272cf --- /dev/null +++ b/homeassistant/components/lametric/icons.json @@ -0,0 +1,45 @@ +{ + "entity": { + "button": { + "app_next": { + "default": "mdi:arrow-right-bold" + }, + "app_previous": { + "default": "mdi:arrow-left-bold" + }, + "dismiss_current": { + "default": "mdi:bell-cancel" + }, + "dismiss_all": { + "default": "mdi:bell-cancel" + } + }, + "number": { + "brightness": { + "default": "mdi:brightness-6" + }, + "volume": { + "default": "mdi:volume-high" + } + }, + "select": { + "brightness_mode": { + "default": "mdi:brightness-auto" + } + }, + "sensor": { + "rssi": { + "default": "mdi:wifi" + } + }, + "switch": { + "bluetooth": { + "default": "mdi:bluetooth" + } + } + }, + "services": { + "chart": "mdi:chart-areaspline-variant", + "message": "mdi:message" + } +} diff --git a/homeassistant/components/lametric/notify.py b/homeassistant/components/lametric/notify.py index d8b9627238c..7362f0ca402 100644 --- a/homeassistant/components/lametric/notify.py +++ b/homeassistant/components/lametric/notify.py @@ -1,4 +1,5 @@ """Support for LaMetric notifications.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/lametric/number.py b/homeassistant/components/lametric/number.py index 9acdc6f1411..cea9debb04b 100644 --- a/homeassistant/components/lametric/number.py +++ b/homeassistant/components/lametric/number.py @@ -1,4 +1,5 @@ """Support for LaMetric numbers.""" + from __future__ import annotations from collections.abc import Awaitable, Callable @@ -30,8 +31,8 @@ class LaMetricNumberEntityDescription(NumberEntityDescription): NUMBERS = [ LaMetricNumberEntityDescription( key="brightness", + translation_key="brightness", name="Brightness", - icon="mdi:brightness-6", entity_category=EntityCategory.CONFIG, native_step=1, native_min_value=0, @@ -42,8 +43,8 @@ NUMBERS = [ ), LaMetricNumberEntityDescription( key="volume", + translation_key="volume", name="Volume", - icon="mdi:volume-high", entity_category=EntityCategory.CONFIG, native_step=1, native_min_value=0, diff --git a/homeassistant/components/lametric/select.py b/homeassistant/components/lametric/select.py index c7a3f55125b..bf9872f2791 100644 --- a/homeassistant/components/lametric/select.py +++ b/homeassistant/components/lametric/select.py @@ -1,4 +1,5 @@ """Support for LaMetric selects.""" + from __future__ import annotations from collections.abc import Awaitable, Callable @@ -31,7 +32,6 @@ SELECTS = [ LaMetricSelectEntityDescription( key="brightness_mode", translation_key="brightness_mode", - icon="mdi:brightness-auto", entity_category=EntityCategory.CONFIG, options=["auto", "manual"], current_fn=lambda device: device.display.brightness_mode.value, diff --git a/homeassistant/components/lametric/sensor.py b/homeassistant/components/lametric/sensor.py index 5ef3608d33b..f202a77b530 100644 --- a/homeassistant/components/lametric/sensor.py +++ b/homeassistant/components/lametric/sensor.py @@ -1,4 +1,5 @@ """Support for LaMetric sensors.""" + from __future__ import annotations from collections.abc import Callable @@ -32,7 +33,6 @@ SENSORS = [ LaMetricSensorEntityDescription( key="rssi", translation_key="rssi", - icon="mdi:wifi", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, native_unit_of_measurement=PERCENTAGE, diff --git a/homeassistant/components/lametric/services.py b/homeassistant/components/lametric/services.py index a20680267d9..d5191e0a434 100644 --- a/homeassistant/components/lametric/services.py +++ b/homeassistant/components/lametric/services.py @@ -1,4 +1,5 @@ """Support for LaMetric time services.""" + from __future__ import annotations from demetriek import ( diff --git a/homeassistant/components/lametric/switch.py b/homeassistant/components/lametric/switch.py index 7fda3a22b8f..9689bb7b802 100644 --- a/homeassistant/components/lametric/switch.py +++ b/homeassistant/components/lametric/switch.py @@ -1,4 +1,5 @@ """Support for LaMetric switches.""" + from __future__ import annotations from collections.abc import Awaitable, Callable @@ -32,7 +33,6 @@ SWITCHES = [ LaMetricSwitchEntityDescription( key="bluetooth", translation_key="bluetooth", - icon="mdi:bluetooth", entity_category=EntityCategory.CONFIG, available_fn=lambda device: device.bluetooth.available, is_on_fn=lambda device: device.bluetooth.active, diff --git a/homeassistant/components/landisgyr_heat_meter/__init__.py b/homeassistant/components/landisgyr_heat_meter/__init__.py index 101216cd0d4..4e52e246d81 100644 --- a/homeassistant/components/landisgyr_heat_meter/__init__.py +++ b/homeassistant/components/landisgyr_heat_meter/__init__.py @@ -1,4 +1,5 @@ """The Landis+Gyr Heat Meter integration.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/landisgyr_heat_meter/config_flow.py b/homeassistant/components/landisgyr_heat_meter/config_flow.py index 479e7107025..f7288b8a0cd 100644 --- a/homeassistant/components/landisgyr_heat_meter/config_flow.py +++ b/homeassistant/components/landisgyr_heat_meter/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Landis+Gyr Heat Meter integration.""" + from __future__ import annotations import asyncio @@ -10,11 +11,10 @@ from serial.tools import list_ports import ultraheat_api import voluptuous as vol -from homeassistant import config_entries from homeassistant.components import usb +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_DEVICE from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError from .const import DOMAIN, ULTRAHEAT_TIMEOUT @@ -30,14 +30,14 @@ STEP_USER_DATA_SCHEMA = vol.Schema( ) -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class LandisgyrConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Ultraheat Heat Meter.""" VERSION = 2 async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Step when setting up serial configuration.""" errors = {} @@ -63,7 +63,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_setup_serial_manual_path( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Set path manually.""" errors = {} diff --git a/homeassistant/components/landisgyr_heat_meter/sensor.py b/homeassistant/components/landisgyr_heat_meter/sensor.py index 075aeb67b50..dd76d3e53cc 100644 --- a/homeassistant/components/landisgyr_heat_meter/sensor.py +++ b/homeassistant/components/landisgyr_heat_meter/sensor.py @@ -1,4 +1,5 @@ """Platform for sensor integration.""" + from __future__ import annotations from collections.abc import Callable @@ -39,20 +40,13 @@ from . import DOMAIN _LOGGER = logging.getLogger(__name__) -@dataclass(frozen=True) -class HeatMeterSensorEntityDescriptionMixin: - """Mixin for additional Heat Meter sensor description attributes .""" +@dataclass(frozen=True, kw_only=True) +class HeatMeterSensorEntityDescription(SensorEntityDescription): + """Heat Meter sensor description.""" value_fn: Callable[[HeatMeterResponse], StateType | datetime] -@dataclass(frozen=True) -class HeatMeterSensorEntityDescription( - SensorEntityDescription, HeatMeterSensorEntityDescriptionMixin -): - """Heat Meter sensor description.""" - - HEAT_METER_SENSOR_TYPES = ( HeatMeterSensorEntityDescription( key="heat_usage_mwh", @@ -292,11 +286,10 @@ async def async_setup_entry( name="Landis+Gyr Heat Meter", ) - sensors = [] - for description in HEAT_METER_SENSOR_TYPES: - sensors.append(HeatMeterSensor(coordinator, description, device)) - - async_add_entities(sensors) + async_add_entities( + HeatMeterSensor(coordinator, description, device) + for description in HEAT_METER_SENSOR_TYPES + ) class HeatMeterSensor( diff --git a/homeassistant/components/lannouncer/notify.py b/homeassistant/components/lannouncer/notify.py index eb89ac416f8..525372710af 100644 --- a/homeassistant/components/lannouncer/notify.py +++ b/homeassistant/components/lannouncer/notify.py @@ -1,4 +1,5 @@ """Lannouncer platform for notify component.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/lastfm/__init__.py b/homeassistant/components/lastfm/__init__.py index 72dcf08a2d0..ebcc929c39c 100644 --- a/homeassistant/components/lastfm/__init__.py +++ b/homeassistant/components/lastfm/__init__.py @@ -1,4 +1,5 @@ """The lastfm component.""" + from __future__ import annotations from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/lastfm/config_flow.py b/homeassistant/components/lastfm/config_flow.py index 4ff809b56d0..154409ac66d 100644 --- a/homeassistant/components/lastfm/config_flow.py +++ b/homeassistant/components/lastfm/config_flow.py @@ -1,4 +1,5 @@ """Config flow for LastFm.""" + from __future__ import annotations from typing import Any @@ -9,11 +10,11 @@ import voluptuous as vol from homeassistant.config_entries import ( ConfigEntry, ConfigFlow, + ConfigFlowResult, OptionsFlowWithConfigEntry, ) from homeassistant.const import CONF_API_KEY from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.selector import ( SelectOptionDict, SelectSelector, @@ -83,7 +84,7 @@ class LastFmConfigFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Initialize user input.""" errors: dict[str, str] = {} if user_input is not None: @@ -102,7 +103,7 @@ class LastFmConfigFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_friends( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Form to select other users and friends.""" errors: dict[str, str] = {} if user_input is not None: @@ -159,7 +160,7 @@ class LastFmOptionsFlowHandler(OptionsFlowWithConfigEntry): async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Initialize form.""" errors: dict[str, str] = {} if user_input is not None: diff --git a/homeassistant/components/lastfm/const.py b/homeassistant/components/lastfm/const.py index f895876c3c3..03cad974a57 100644 --- a/homeassistant/components/lastfm/const.py +++ b/homeassistant/components/lastfm/const.py @@ -1,4 +1,5 @@ """Constants for LastFM.""" + import logging from typing import Final diff --git a/homeassistant/components/lastfm/coordinator.py b/homeassistant/components/lastfm/coordinator.py index 6e62fe2c84e..18473745450 100644 --- a/homeassistant/components/lastfm/coordinator.py +++ b/homeassistant/components/lastfm/coordinator.py @@ -1,4 +1,5 @@ """DataUpdateCoordinator for the LastFM integration.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/homeassistant/components/lastfm/icons.json b/homeassistant/components/lastfm/icons.json new file mode 100644 index 00000000000..fe453dc53c2 --- /dev/null +++ b/homeassistant/components/lastfm/icons.json @@ -0,0 +1,9 @@ +{ + "entity": { + "sensor": { + "lastfm": { + "default": "mdi:radio-fm" + } + } + } +} diff --git a/homeassistant/components/lastfm/sensor.py b/homeassistant/components/lastfm/sensor.py index 2b022a00107..48770113a80 100644 --- a/homeassistant/components/lastfm/sensor.py +++ b/homeassistant/components/lastfm/sensor.py @@ -1,4 +1,5 @@ """Sensor for Last.fm account status.""" + from __future__ import annotations import hashlib @@ -43,7 +44,7 @@ class LastFmSensor(CoordinatorEntity[LastFMDataUpdateCoordinator], SensorEntity) """A class for the Last.fm account.""" _attr_attribution = "Data provided by Last.fm" - _attr_icon = "mdi:radio-fm" + _attr_translation_key = "lastfm" _attr_has_entity_name = True _attr_name = None diff --git a/homeassistant/components/launch_library/__init__.py b/homeassistant/components/launch_library/__init__.py index e85c9c81566..23bf159ac61 100644 --- a/homeassistant/components/launch_library/__init__.py +++ b/homeassistant/components/launch_library/__init__.py @@ -1,4 +1,5 @@ """The launch_library component.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/launch_library/config_flow.py b/homeassistant/components/launch_library/config_flow.py index d57bc3b7d01..3cdff3650b3 100644 --- a/homeassistant/components/launch_library/config_flow.py +++ b/homeassistant/components/launch_library/config_flow.py @@ -1,22 +1,22 @@ """Config flow to configure launch library component.""" + from __future__ import annotations from typing import Any -from homeassistant import config_entries -from homeassistant.data_entry_flow import FlowResult +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from .const import DOMAIN -class LaunchLibraryFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class LaunchLibraryFlowHandler(ConfigFlow, domain=DOMAIN): """Config flow for Launch Library component.""" VERSION = 1 async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" # Check if already configured if self._async_current_entries(): diff --git a/homeassistant/components/launch_library/diagnostics.py b/homeassistant/components/launch_library/diagnostics.py index 35665e1b39e..35d0a699ab5 100644 --- a/homeassistant/components/launch_library/diagnostics.py +++ b/homeassistant/components/launch_library/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for Launch Library.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/launch_library/sensor.py b/homeassistant/components/launch_library/sensor.py index 2c1934f0c16..66b1d95ba2a 100644 --- a/homeassistant/components/launch_library/sensor.py +++ b/homeassistant/components/launch_library/sensor.py @@ -1,4 +1,5 @@ """Support for Launch Library sensors.""" + from __future__ import annotations from collections.abc import Callable @@ -31,21 +32,14 @@ from .const import DOMAIN DEFAULT_NEXT_LAUNCH_NAME = "Next launch" -@dataclass(frozen=True) -class LaunchLibrarySensorEntityDescriptionMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class LaunchLibrarySensorEntityDescription(SensorEntityDescription): + """Describes a Next Launch sensor entity.""" value_fn: Callable[[Launch | Event], datetime | int | str | None] attributes_fn: Callable[[Launch | Event], dict[str, Any] | None] -@dataclass(frozen=True) -class LaunchLibrarySensorEntityDescription( - SensorEntityDescription, LaunchLibrarySensorEntityDescriptionMixin -): - """Describes a Next Launch sensor entity.""" - - SENSOR_DESCRIPTIONS: tuple[LaunchLibrarySensorEntityDescription, ...] = ( LaunchLibrarySensorEntityDescription( key="next_launch", @@ -74,7 +68,7 @@ SENSOR_DESCRIPTIONS: tuple[LaunchLibrarySensorEntityDescription, ...] = ( LaunchLibrarySensorEntityDescription( key="launch_probability", icon="mdi:dice-multiple", - translation_key="next_launch", + translation_key="launch_probability", native_unit_of_measurement=PERCENTAGE, value_fn=lambda nl: None if nl.probability == -1 else nl.probability, attributes_fn=lambda nl: None, @@ -82,7 +76,7 @@ SENSOR_DESCRIPTIONS: tuple[LaunchLibrarySensorEntityDescription, ...] = ( LaunchLibrarySensorEntityDescription( key="launch_status", icon="mdi:rocket-launch", - translation_key="next_launch", + translation_key="launch_status", value_fn=lambda nl: nl.status.name, attributes_fn=lambda nl: {"reason": nl.holdreason} if nl.inhold else None, ), diff --git a/homeassistant/components/laundrify/__init__.py b/homeassistant/components/laundrify/__init__.py index 4bfb84082d5..9eb15625319 100644 --- a/homeassistant/components/laundrify/__init__.py +++ b/homeassistant/components/laundrify/__init__.py @@ -1,4 +1,5 @@ """The laundrify integration.""" + from __future__ import annotations from laundrify_aio import LaundrifyAPI diff --git a/homeassistant/components/laundrify/binary_sensor.py b/homeassistant/components/laundrify/binary_sensor.py index 5cca6870b6c..80732bdc470 100644 --- a/homeassistant/components/laundrify/binary_sensor.py +++ b/homeassistant/components/laundrify/binary_sensor.py @@ -1,4 +1,5 @@ """Platform for binary sensor integration.""" + from __future__ import annotations import logging @@ -40,10 +41,10 @@ class LaundrifyPowerPlug( """Representation of a laundrify Power Plug.""" _attr_device_class = BinarySensorDeviceClass.RUNNING - _attr_icon = "mdi:washing-machine" _attr_unique_id: str _attr_has_entity_name = True _attr_name = None + _attr_translation_key = "laundrify_power_plug" def __init__( self, coordinator: LaundrifyUpdateCoordinator, device: LaundrifyDevice diff --git a/homeassistant/components/laundrify/config_flow.py b/homeassistant/components/laundrify/config_flow.py index 55a29fec2e7..c131befd7d4 100644 --- a/homeassistant/components/laundrify/config_flow.py +++ b/homeassistant/components/laundrify/config_flow.py @@ -1,4 +1,5 @@ """Config flow for laundrify integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -13,9 +14,8 @@ from laundrify_aio.exceptions import ( ) from voluptuous import Required, Schema -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_ACCESS_TOKEN, CONF_CODE -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN @@ -32,13 +32,13 @@ class LaundrifyConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" return await self.async_step_init(user_input) async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" if user_input is None: return self.async_show_form(step_id="init", data_schema=CONFIG_SCHEMA) @@ -77,13 +77,15 @@ class LaundrifyConfigFlow(ConfigFlow, domain=DOMAIN): step_id="init", data_schema=CONFIG_SCHEMA, errors=errors ) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Perform reauth upon an API authentication error.""" return await self.async_step_reauth_confirm() async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Dialog that informs the user that reauth is required.""" if user_input is None: return self.async_show_form( diff --git a/homeassistant/components/laundrify/coordinator.py b/homeassistant/components/laundrify/coordinator.py index d67410c6aa3..c3fdc265174 100644 --- a/homeassistant/components/laundrify/coordinator.py +++ b/homeassistant/components/laundrify/coordinator.py @@ -1,4 +1,5 @@ """Custom DataUpdateCoordinator for the laundrify integration.""" + import asyncio from datetime import timedelta import logging diff --git a/homeassistant/components/laundrify/icons.json b/homeassistant/components/laundrify/icons.json new file mode 100644 index 00000000000..370adb1f953 --- /dev/null +++ b/homeassistant/components/laundrify/icons.json @@ -0,0 +1,9 @@ +{ + "entity": { + "binary_sensor": { + "laundrify_power_plug": { + "default": "mdi:washing-machine" + } + } + } +} diff --git a/homeassistant/components/laundrify/model.py b/homeassistant/components/laundrify/model.py index aa6bf77509f..862824c3154 100644 --- a/homeassistant/components/laundrify/model.py +++ b/homeassistant/components/laundrify/model.py @@ -1,4 +1,5 @@ """Models for laundrify platform.""" + from __future__ import annotations from typing import TypedDict diff --git a/homeassistant/components/lawn_mower/__init__.py b/homeassistant/components/lawn_mower/__init__.py index b1eac0a6609..f28bd1308b0 100644 --- a/homeassistant/components/lawn_mower/__init__.py +++ b/homeassistant/components/lawn_mower/__init__.py @@ -1,4 +1,5 @@ """The lawn mower integration.""" + from __future__ import annotations from datetime import timedelta @@ -107,7 +108,7 @@ class LawnMowerEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): def start_mowing(self) -> None: """Start or resume mowing.""" - raise NotImplementedError() + raise NotImplementedError async def async_start_mowing(self) -> None: """Start or resume mowing.""" @@ -115,7 +116,7 @@ class LawnMowerEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): def dock(self) -> None: """Dock the mower.""" - raise NotImplementedError() + raise NotImplementedError async def async_dock(self) -> None: """Dock the mower.""" @@ -123,7 +124,7 @@ class LawnMowerEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): def pause(self) -> None: """Pause the lawn mower.""" - raise NotImplementedError() + raise NotImplementedError async def async_pause(self) -> None: """Pause the lawn mower.""" diff --git a/homeassistant/components/lawn_mower/const.py b/homeassistant/components/lawn_mower/const.py index 706c9616450..e060abe6423 100644 --- a/homeassistant/components/lawn_mower/const.py +++ b/homeassistant/components/lawn_mower/const.py @@ -1,4 +1,5 @@ """Constants for the lawn mower integration.""" + from enum import IntFlag, StrEnum diff --git a/homeassistant/components/lcn/__init__.py b/homeassistant/components/lcn/__init__.py index 527c3de7c9e..6866a10d55e 100644 --- a/homeassistant/components/lcn/__init__.py +++ b/homeassistant/components/lcn/__init__.py @@ -1,4 +1,5 @@ """Support for LCN devices.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/lcn/binary_sensor.py b/homeassistant/components/lcn/binary_sensor.py index ceeeecf50c4..35836e4653e 100644 --- a/homeassistant/components/lcn/binary_sensor.py +++ b/homeassistant/components/lcn/binary_sensor.py @@ -1,4 +1,5 @@ """Support for LCN binary sensors.""" + from __future__ import annotations import pypck @@ -42,15 +43,12 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up LCN switch entities from a config entry.""" - entities = [] - for entity_config in config_entry.data[CONF_ENTITIES]: - if entity_config[CONF_DOMAIN] == DOMAIN_BINARY_SENSOR: - entities.append( - create_lcn_binary_sensor_entity(hass, entity_config, config_entry) - ) - - async_add_entities(entities) + async_add_entities( + create_lcn_binary_sensor_entity(hass, entity_config, config_entry) + for entity_config in config_entry.data[CONF_ENTITIES] + if entity_config[CONF_DOMAIN] == DOMAIN_BINARY_SENSOR + ) class LcnRegulatorLockSensor(LcnEntity, BinarySensorEntity): diff --git a/homeassistant/components/lcn/climate.py b/homeassistant/components/lcn/climate.py index d1e92d54fb1..c03061618f7 100644 --- a/homeassistant/components/lcn/climate.py +++ b/homeassistant/components/lcn/climate.py @@ -1,4 +1,5 @@ """Support for LCN climate control.""" + from __future__ import annotations from typing import Any, cast @@ -55,15 +56,12 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up LCN switch entities from a config entry.""" - entities = [] - for entity_config in config_entry.data[CONF_ENTITIES]: - if entity_config[CONF_DOMAIN] == DOMAIN_CLIMATE: - entities.append( - create_lcn_climate_entity(hass, entity_config, config_entry) - ) - - async_add_entities(entities) + async_add_entities( + create_lcn_climate_entity(hass, entity_config, config_entry) + for entity_config in config_entry.data[CONF_ENTITIES] + if entity_config[CONF_DOMAIN] == DOMAIN_CLIMATE + ) class LcnClimate(LcnEntity, ClimateEntity): diff --git a/homeassistant/components/lcn/config_flow.py b/homeassistant/components/lcn/config_flow.py index 09f588b6953..d05eb896f27 100644 --- a/homeassistant/components/lcn/config_flow.py +++ b/homeassistant/components/lcn/config_flow.py @@ -1,11 +1,17 @@ """Config flow to configure the LCN integration.""" + from __future__ import annotations import logging import pypck -from homeassistant import config_entries +from homeassistant.config_entries import ( + SOURCE_IMPORT, + ConfigEntry, + ConfigFlow, + ConfigFlowResult, +) from homeassistant.const import ( CONF_HOST, CONF_IP_ADDRESS, @@ -14,7 +20,6 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.typing import ConfigType from .const import CONF_DIM_MODE, CONF_SK_NUM_TRIES, DOMAIN @@ -23,9 +28,7 @@ from .helpers import purge_device_registry, purge_entity_registry _LOGGER = logging.getLogger(__name__) -def get_config_entry( - hass: HomeAssistant, data: ConfigType -) -> config_entries.ConfigEntry | None: +def get_config_entry(hass: HomeAssistant, data: ConfigType) -> ConfigEntry | None: """Check config entries for already configured entries based on the ip address/port.""" return next( ( @@ -65,12 +68,12 @@ async def validate_connection(host_name: str, data: ConfigType) -> ConfigType: return data -class LcnFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class LcnFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a LCN config flow.""" VERSION = 1 - async def async_step_import(self, data: ConfigType) -> FlowResult: + async def async_step_import(self, data: ConfigType) -> ConfigFlowResult: """Import existing configuration from LCN.""" host_name = data[CONF_HOST] # validate the imported connection parameters @@ -94,7 +97,7 @@ class LcnFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): # check if we already have a host with the same address configured if entry := get_config_entry(self.hass, data): - entry.source = config_entries.SOURCE_IMPORT + entry.source = SOURCE_IMPORT # Cleanup entity and device registry, if we imported from configuration.yaml to # remove orphans when entities were removed from configuration purge_entity_registry(self.hass, entry.entry_id, data) diff --git a/homeassistant/components/lcn/const.py b/homeassistant/components/lcn/const.py index e8da5b39073..bcf9ecdf295 100644 --- a/homeassistant/components/lcn/const.py +++ b/homeassistant/components/lcn/const.py @@ -1,4 +1,5 @@ """Constants for the LCN component.""" + from itertools import product from homeassistant.const import Platform diff --git a/homeassistant/components/lcn/cover.py b/homeassistant/components/lcn/cover.py index 31b2dbface0..edc60a202a1 100644 --- a/homeassistant/components/lcn/cover.py +++ b/homeassistant/components/lcn/cover.py @@ -1,4 +1,5 @@ """Support for LCN covers.""" + from __future__ import annotations from typing import Any @@ -39,13 +40,12 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up LCN cover entities from a config entry.""" - entities = [] - for entity_config in config_entry.data[CONF_ENTITIES]: - if entity_config[CONF_DOMAIN] == DOMAIN_COVER: - entities.append(create_lcn_cover_entity(hass, entity_config, config_entry)) - - async_add_entities(entities) + async_add_entities( + create_lcn_cover_entity(hass, entity_config, config_entry) + for entity_config in config_entry.data[CONF_ENTITIES] + if entity_config[CONF_DOMAIN] == DOMAIN_COVER + ) class LcnOutputsCover(LcnEntity, CoverEntity): diff --git a/homeassistant/components/lcn/device_trigger.py b/homeassistant/components/lcn/device_trigger.py index 46a94929d0b..42b5506110f 100644 --- a/homeassistant/components/lcn/device_trigger.py +++ b/homeassistant/components/lcn/device_trigger.py @@ -1,4 +1,5 @@ """Provides device triggers for LCN.""" + from __future__ import annotations import voluptuous as vol diff --git a/homeassistant/components/lcn/helpers.py b/homeassistant/components/lcn/helpers.py index 8cb0201033e..b0b1a2f1c04 100644 --- a/homeassistant/components/lcn/helpers.py +++ b/homeassistant/components/lcn/helpers.py @@ -1,4 +1,5 @@ """Helpers for LCN component.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/lcn/icons.json b/homeassistant/components/lcn/icons.json new file mode 100644 index 00000000000..c8b451a79ea --- /dev/null +++ b/homeassistant/components/lcn/icons.json @@ -0,0 +1,17 @@ +{ + "services": { + "output_abs": "mdi:brightness-auto", + "output_rel": "mdi:brightness-7", + "output_toggle": "mdi:toggle-switch", + "relays": "mdi:light-switch-off", + "led": "mdi:led-on", + "var_abs": "mdi:wrench", + "var_reset": "mdi:reload", + "var_rel": "mdi:wrench", + "lock_regulator": "mdi:lock", + "send_keys": "mdi:alarm-panel", + "lock_keys": "mdi:lock", + "dyn_text": "mdi:form-textbox", + "pck": "mdi:package-variant-closed" + } +} diff --git a/homeassistant/components/lcn/light.py b/homeassistant/components/lcn/light.py index 65c1344edf0..584161a0829 100644 --- a/homeassistant/components/lcn/light.py +++ b/homeassistant/components/lcn/light.py @@ -1,4 +1,5 @@ """Support for LCN lights.""" + from __future__ import annotations from typing import Any @@ -52,13 +53,12 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up LCN light entities from a config entry.""" - entities = [] - for entity_config in config_entry.data[CONF_ENTITIES]: - if entity_config[CONF_DOMAIN] == DOMAIN_LIGHT: - entities.append(create_lcn_light_entity(hass, entity_config, config_entry)) - - async_add_entities(entities) + async_add_entities( + create_lcn_light_entity(hass, entity_config, config_entry) + for entity_config in config_entry.data[CONF_ENTITIES] + if entity_config[CONF_DOMAIN] == DOMAIN_LIGHT + ) class LcnOutputLight(LcnEntity, LightEntity): diff --git a/homeassistant/components/lcn/scene.py b/homeassistant/components/lcn/scene.py index f1980b6475d..7e476987c53 100644 --- a/homeassistant/components/lcn/scene.py +++ b/homeassistant/components/lcn/scene.py @@ -1,4 +1,5 @@ """Support for LCN scenes.""" + from __future__ import annotations from typing import Any @@ -42,13 +43,12 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up LCN switch entities from a config entry.""" - entities = [] - for entity_config in config_entry.data[CONF_ENTITIES]: - if entity_config[CONF_DOMAIN] == DOMAIN_SCENE: - entities.append(create_lcn_scene_entity(hass, entity_config, config_entry)) - - async_add_entities(entities) + async_add_entities( + create_lcn_scene_entity(hass, entity_config, config_entry) + for entity_config in config_entry.data[CONF_ENTITIES] + if entity_config[CONF_DOMAIN] == DOMAIN_SCENE + ) class LcnScene(LcnEntity, Scene): diff --git a/homeassistant/components/lcn/schemas.py b/homeassistant/components/lcn/schemas.py index bd02c80da66..b907525747d 100644 --- a/homeassistant/components/lcn/schemas.py +++ b/homeassistant/components/lcn/schemas.py @@ -1,4 +1,5 @@ """Schema definitions for LCN configuration and websockets api.""" + import voluptuous as vol from homeassistant.components.climate import DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP diff --git a/homeassistant/components/lcn/sensor.py b/homeassistant/components/lcn/sensor.py index 1428019b59f..32b97ab8317 100644 --- a/homeassistant/components/lcn/sensor.py +++ b/homeassistant/components/lcn/sensor.py @@ -1,4 +1,5 @@ """Support for LCN sensors.""" + from __future__ import annotations from itertools import chain @@ -55,13 +56,12 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up LCN switch entities from a config entry.""" - entities = [] - for entity_config in config_entry.data[CONF_ENTITIES]: - if entity_config[CONF_DOMAIN] == DOMAIN_SENSOR: - entities.append(create_lcn_sensor_entity(hass, entity_config, config_entry)) - - async_add_entities(entities) + async_add_entities( + create_lcn_sensor_entity(hass, entity_config, config_entry) + for entity_config in config_entry.data[CONF_ENTITIES] + if entity_config[CONF_DOMAIN] == DOMAIN_SENSOR + ) class LcnVariableSensor(LcnEntity, SensorEntity): diff --git a/homeassistant/components/lcn/switch.py b/homeassistant/components/lcn/switch.py index 8374ff85ab7..b82394ced0d 100644 --- a/homeassistant/components/lcn/switch.py +++ b/homeassistant/components/lcn/switch.py @@ -1,4 +1,5 @@ """Support for LCN switches.""" + from __future__ import annotations from typing import Any @@ -40,13 +41,11 @@ async def async_setup_entry( ) -> None: """Set up LCN switch entities from a config entry.""" - entities = [] - - for entity_config in config_entry.data[CONF_ENTITIES]: - if entity_config[CONF_DOMAIN] == DOMAIN_SWITCH: - entities.append(create_lcn_switch_entity(hass, entity_config, config_entry)) - - async_add_entities(entities) + async_add_entities( + create_lcn_switch_entity(hass, entity_config, config_entry) + for entity_config in config_entry.data[CONF_ENTITIES] + if entity_config[CONF_DOMAIN] == DOMAIN_SWITCH + ) class LcnOutputSwitch(LcnEntity, SwitchEntity): diff --git a/homeassistant/components/ld2410_ble/binary_sensor.py b/homeassistant/components/ld2410_ble/binary_sensor.py index cca87de7a60..c52bc34b699 100644 --- a/homeassistant/components/ld2410_ble/binary_sensor.py +++ b/homeassistant/components/ld2410_ble/binary_sensor.py @@ -1,6 +1,5 @@ """LD2410 BLE integration binary sensor platform.""" - from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, BinarySensorEntity, diff --git a/homeassistant/components/ld2410_ble/config_flow.py b/homeassistant/components/ld2410_ble/config_flow.py index d56610f87a8..10d282cb8c7 100644 --- a/homeassistant/components/ld2410_ble/config_flow.py +++ b/homeassistant/components/ld2410_ble/config_flow.py @@ -1,4 +1,5 @@ """Config flow for LD2410BLE integration.""" + from __future__ import annotations import logging @@ -8,20 +9,19 @@ from bluetooth_data_tools import human_readable_name from ld2410_ble import BLEAK_EXCEPTIONS, LD2410BLE import voluptuous as vol -from homeassistant import config_entries from homeassistant.components.bluetooth import ( BluetoothServiceInfoBleak, async_discovered_service_info, ) +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_ADDRESS -from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN, LOCAL_NAMES _LOGGER = logging.getLogger(__name__) -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class Ld2410BleConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for LD2410 BLE.""" VERSION = 1 @@ -33,7 +33,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_bluetooth( self, discovery_info: BluetoothServiceInfoBleak - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the bluetooth discovery step.""" await self.async_set_unique_id(discovery_info.address) self._abort_if_unique_id_configured() @@ -47,7 +47,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the user step to pick discovered device.""" errors: dict[str, str] = {} diff --git a/homeassistant/components/ld2410_ble/models.py b/homeassistant/components/ld2410_ble/models.py index e2666277495..a7f5f4f2e3e 100644 --- a/homeassistant/components/ld2410_ble/models.py +++ b/homeassistant/components/ld2410_ble/models.py @@ -1,4 +1,5 @@ """The ld2410 ble integration models.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/homeassistant/components/ld2410_ble/sensor.py b/homeassistant/components/ld2410_ble/sensor.py index 5bd4a0d4d2d..6daa1397161 100644 --- a/homeassistant/components/ld2410_ble/sensor.py +++ b/homeassistant/components/ld2410_ble/sensor.py @@ -1,6 +1,5 @@ """LD2410 BLE integration sensor platform.""" - from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, @@ -93,7 +92,7 @@ MOTION_ENERGY_GATES = [ entity_registry_enabled_default=False, native_unit_of_measurement="Target Energy", ) - for i in range(0, 9) + for i in range(9) ] STATIC_ENERGY_GATES = [ @@ -104,22 +103,20 @@ STATIC_ENERGY_GATES = [ entity_registry_enabled_default=False, native_unit_of_measurement="Target Energy", ) - for i in range(0, 9) + for i in range(9) ] -SENSOR_DESCRIPTIONS = ( - [ - MOVING_TARGET_DISTANCE_DESCRIPTION, - STATIC_TARGET_DISTANCE_DESCRIPTION, - MOVING_TARGET_ENERGY_DESCRIPTION, - STATIC_TARGET_ENERGY_DESCRIPTION, - DETECTION_DISTANCE_DESCRIPTION, - MAX_MOTION_GATES_DESCRIPTION, - MAX_STATIC_GATES_DESCRIPTION, - ] - + MOTION_ENERGY_GATES - + STATIC_ENERGY_GATES -) +SENSOR_DESCRIPTIONS = [ + MOVING_TARGET_DISTANCE_DESCRIPTION, + STATIC_TARGET_DISTANCE_DESCRIPTION, + MOVING_TARGET_ENERGY_DESCRIPTION, + STATIC_TARGET_ENERGY_DESCRIPTION, + DETECTION_DISTANCE_DESCRIPTION, + MAX_MOTION_GATES_DESCRIPTION, + MAX_STATIC_GATES_DESCRIPTION, + *MOTION_ENERGY_GATES, + *STATIC_ENERGY_GATES, +] async def async_setup_entry( diff --git a/homeassistant/components/leaone/__init__.py b/homeassistant/components/leaone/__init__.py index 9f8bac34d55..74119cfaa4c 100644 --- a/homeassistant/components/leaone/__init__.py +++ b/homeassistant/components/leaone/__init__.py @@ -1,4 +1,5 @@ """The Leaone integration.""" + from __future__ import annotations import logging @@ -25,14 +26,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: address = entry.unique_id assert address is not None data = LeaoneBluetoothDeviceData() - coordinator = hass.data.setdefault(DOMAIN, {})[ - entry.entry_id - ] = PassiveBluetoothProcessorCoordinator( - hass, - _LOGGER, - address=address, - mode=BluetoothScanningMode.PASSIVE, - update_method=data.update, + coordinator = hass.data.setdefault(DOMAIN, {})[entry.entry_id] = ( + PassiveBluetoothProcessorCoordinator( + hass, + _LOGGER, + address=address, + mode=BluetoothScanningMode.PASSIVE, + update_method=data.update, + ) ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload( diff --git a/homeassistant/components/leaone/config_flow.py b/homeassistant/components/leaone/config_flow.py index 5bbf2917332..8878f9af065 100644 --- a/homeassistant/components/leaone/config_flow.py +++ b/homeassistant/components/leaone/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Leaone integration.""" + from __future__ import annotations from typing import Any @@ -7,9 +8,8 @@ from leaone_ble import LeaoneBluetoothDeviceData as DeviceData import voluptuous as vol from homeassistant.components.bluetooth import async_discovered_service_info -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_ADDRESS -from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN @@ -25,7 +25,7 @@ class LeaoneConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the user step to pick discovered device.""" if user_input is not None: address = user_input[CONF_ADDRESS] diff --git a/homeassistant/components/leaone/device.py b/homeassistant/components/leaone/device.py index a745873b693..0b95783dfd7 100644 --- a/homeassistant/components/leaone/device.py +++ b/homeassistant/components/leaone/device.py @@ -1,4 +1,5 @@ """Support for Leaone devices.""" + from __future__ import annotations from leaone_ble import DeviceKey diff --git a/homeassistant/components/leaone/sensor.py b/homeassistant/components/leaone/sensor.py index a614e63231a..c57f6678897 100644 --- a/homeassistant/components/leaone/sensor.py +++ b/homeassistant/components/leaone/sensor.py @@ -1,4 +1,5 @@ """Support for Leaone sensors.""" + from __future__ import annotations from leaone_ble import DeviceClass as LeaoneSensorDeviceClass, SensorUpdate, Units diff --git a/homeassistant/components/led_ble/__init__.py b/homeassistant/components/led_ble/__init__.py index 27a273ed7b0..d09f88b145a 100644 --- a/homeassistant/components/led_ble/__init__.py +++ b/homeassistant/components/led_ble/__init__.py @@ -1,4 +1,5 @@ """The LED BLE integration.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/led_ble/config_flow.py b/homeassistant/components/led_ble/config_flow.py index 44436180ea8..a5afbcc6c0d 100644 --- a/homeassistant/components/led_ble/config_flow.py +++ b/homeassistant/components/led_ble/config_flow.py @@ -1,4 +1,5 @@ """Config flow for LEDBLE integration.""" + from __future__ import annotations import logging @@ -8,20 +9,19 @@ from bluetooth_data_tools import human_readable_name from led_ble import BLEAK_EXCEPTIONS, LEDBLE import voluptuous as vol -from homeassistant import config_entries from homeassistant.components.bluetooth import ( BluetoothServiceInfoBleak, async_discovered_service_info, ) +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_ADDRESS -from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN, LOCAL_NAMES, UNSUPPORTED_SUB_MODEL _LOGGER = logging.getLogger(__name__) -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class LedBleConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Yale Access Bluetooth.""" VERSION = 1 @@ -33,7 +33,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_bluetooth( self, discovery_info: BluetoothServiceInfoBleak - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the bluetooth discovery step.""" if discovery_info.name.startswith(UNSUPPORTED_SUB_MODEL): # These versions speak a different protocol @@ -51,7 +51,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the user step to pick discovered device.""" errors: dict[str, str] = {} diff --git a/homeassistant/components/led_ble/light.py b/homeassistant/components/led_ble/light.py index a1da82dfe6d..3bca7269eba 100644 --- a/homeassistant/components/led_ble/light.py +++ b/homeassistant/components/led_ble/light.py @@ -1,4 +1,5 @@ """LED BLE integration light platform.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/led_ble/models.py b/homeassistant/components/led_ble/models.py index 0eda9439f11..a8dd3443dce 100644 --- a/homeassistant/components/led_ble/models.py +++ b/homeassistant/components/led_ble/models.py @@ -1,4 +1,5 @@ """The led ble integration models.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/homeassistant/components/lg_netcast/const.py b/homeassistant/components/lg_netcast/const.py index 6cb44a5a3f8..0344ad6f177 100644 --- a/homeassistant/components/lg_netcast/const.py +++ b/homeassistant/components/lg_netcast/const.py @@ -1,2 +1,3 @@ """Constants for the lg_netcast component.""" + DOMAIN = "lg_netcast" diff --git a/homeassistant/components/lg_netcast/media_player.py b/homeassistant/components/lg_netcast/media_player.py index 2b59e628705..81927710299 100644 --- a/homeassistant/components/lg_netcast/media_player.py +++ b/homeassistant/components/lg_netcast/media_player.py @@ -1,4 +1,5 @@ """Support for LG TV running on NetCast 3 or 4.""" + from __future__ import annotations from datetime import datetime diff --git a/homeassistant/components/lg_soundbar/__init__.py b/homeassistant/components/lg_soundbar/__init__.py index 21d7fa4e773..cd1ce1c8139 100644 --- a/homeassistant/components/lg_soundbar/__init__.py +++ b/homeassistant/components/lg_soundbar/__init__.py @@ -1,4 +1,5 @@ """The lg_soundbar component.""" + import logging from homeassistant import config_entries, core diff --git a/homeassistant/components/lg_soundbar/config_flow.py b/homeassistant/components/lg_soundbar/config_flow.py index fde5c20ebd7..cc8e696b065 100644 --- a/homeassistant/components/lg_soundbar/config_flow.py +++ b/homeassistant/components/lg_soundbar/config_flow.py @@ -1,13 +1,13 @@ """Config flow to configure the LG Soundbar integration.""" + import logging from queue import Empty, Full, Queue import temescal import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_PORT -from homeassistant.data_entry_flow import FlowResult from .const import DEFAULT_PORT, DOMAIN @@ -67,12 +67,12 @@ def test_connect(host, port): return details -class LGSoundbarConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class LGSoundbarConfigFlow(ConfigFlow, domain=DOMAIN): """LG Soundbar config flow.""" VERSION = 1 - async def async_step_user(self, user_input=None) -> FlowResult: + async def async_step_user(self, user_input=None) -> ConfigFlowResult: """Handle a flow initiated by the user.""" if user_input is None: return self._show_form() diff --git a/homeassistant/components/lg_soundbar/const.py b/homeassistant/components/lg_soundbar/const.py index c71e43c0d60..2ac0602568b 100644 --- a/homeassistant/components/lg_soundbar/const.py +++ b/homeassistant/components/lg_soundbar/const.py @@ -1,4 +1,5 @@ """Constants for the LG Soundbar integration.""" + DOMAIN = "lg_soundbar" DEFAULT_PORT = 9741 diff --git a/homeassistant/components/lg_soundbar/media_player.py b/homeassistant/components/lg_soundbar/media_player.py index cfd0ebbd7a7..61baed1198b 100644 --- a/homeassistant/components/lg_soundbar/media_player.py +++ b/homeassistant/components/lg_soundbar/media_player.py @@ -1,4 +1,5 @@ """Support for LG soundbars.""" + from __future__ import annotations import temescal @@ -165,11 +166,11 @@ class LGDevice(MediaPlayerEntity): @property def sound_mode_list(self): """Return the available sound modes.""" - modes = [] - for equaliser in self._equalisers: - if equaliser < len(temescal.equalisers): - modes.append(temescal.equalisers[equaliser]) - return sorted(modes) + return sorted( + temescal.equalisers[equaliser] + for equaliser in self._equalisers + if equaliser < len(temescal.equalisers) + ) @property def source(self): @@ -181,11 +182,11 @@ class LGDevice(MediaPlayerEntity): @property def source_list(self): """List of available input sources.""" - sources = [] - for function in self._functions: - if function < len(temescal.functions): - sources.append(temescal.functions[function]) - return sorted(sources) + return sorted( + temescal.functions[function] + for function in self._functions + if function < len(temescal.functions) + ) def set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" diff --git a/homeassistant/components/lidarr/__init__.py b/homeassistant/components/lidarr/__init__.py index dd63920b209..acfb8f30f30 100644 --- a/homeassistant/components/lidarr/__init__.py +++ b/homeassistant/components/lidarr/__init__.py @@ -1,4 +1,5 @@ """The Lidarr component.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/lidarr/config_flow.py b/homeassistant/components/lidarr/config_flow.py index a0b73950766..379a01375b6 100644 --- a/homeassistant/components/lidarr/config_flow.py +++ b/homeassistant/components/lidarr/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Lidarr.""" + from __future__ import annotations from collections.abc import Mapping @@ -9,10 +10,9 @@ from aiopyarr import exceptions from aiopyarr.lidarr_client import LidarrClient import voluptuous as vol -from homeassistant.config_entries import ConfigEntry, ConfigFlow +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_API_KEY, CONF_URL, CONF_VERIFY_SSL from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DEFAULT_NAME, DOMAIN @@ -27,7 +27,9 @@ class LidarrConfigFlow(ConfigFlow, domain=DOMAIN): """Initialize the flow.""" self.entry: ConfigEntry | None = None - async def async_step_reauth(self, user_input: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, user_input: Mapping[str, Any] + ) -> ConfigFlowResult: """Handle configuration by re-auth.""" self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) @@ -35,7 +37,7 @@ class LidarrConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_reauth_confirm( self, user_input: dict[str, str] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm reauth dialog.""" if user_input is not None: return await self.async_step_user() @@ -45,7 +47,7 @@ class LidarrConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initiated by the user.""" errors = {} diff --git a/homeassistant/components/lidarr/const.py b/homeassistant/components/lidarr/const.py index ccf56db802e..1ef32a7e68f 100644 --- a/homeassistant/components/lidarr/const.py +++ b/homeassistant/components/lidarr/const.py @@ -1,4 +1,5 @@ """Constants for Lidarr.""" + import logging from typing import Final diff --git a/homeassistant/components/lidarr/coordinator.py b/homeassistant/components/lidarr/coordinator.py index 9cc606a12b3..8b3116055d4 100644 --- a/homeassistant/components/lidarr/coordinator.py +++ b/homeassistant/components/lidarr/coordinator.py @@ -1,4 +1,5 @@ """Data update coordinator for the Lidarr integration.""" + from __future__ import annotations from abc import ABC, abstractmethod diff --git a/homeassistant/components/lidarr/icons.json b/homeassistant/components/lidarr/icons.json new file mode 100644 index 00000000000..fcda789a8a3 --- /dev/null +++ b/homeassistant/components/lidarr/icons.json @@ -0,0 +1,15 @@ +{ + "entity": { + "sensor": { + "disk_space": { + "default": "mdi:harddisk" + }, + "queue": { + "default": "mdi:download" + }, + "wanted": { + "default": "mdi:music" + } + } + } +} diff --git a/homeassistant/components/lidarr/sensor.py b/homeassistant/components/lidarr/sensor.py index 027779f93fe..c876aec4623 100644 --- a/homeassistant/components/lidarr/sensor.py +++ b/homeassistant/components/lidarr/sensor.py @@ -1,4 +1,5 @@ """Support for Lidarr.""" + from __future__ import annotations from collections.abc import Callable @@ -62,10 +63,13 @@ class LidarrSensorEntityDescription( """Class to describe a Lidarr sensor.""" attributes_fn: Callable[[T], dict[str, str] | None] = lambda _: None - description_fn: Callable[ - [LidarrSensorEntityDescription[T], LidarrRootFolder], - tuple[LidarrSensorEntityDescription[T], str] | None, - ] | None = None + description_fn: ( + Callable[ + [LidarrSensorEntityDescription[T], LidarrRootFolder], + tuple[LidarrSensorEntityDescription[T], str] | None, + ] + | None + ) = None SENSOR_TYPES: dict[str, LidarrSensorEntityDescription[Any]] = { @@ -74,7 +78,6 @@ SENSOR_TYPES: dict[str, LidarrSensorEntityDescription[Any]] = { translation_key="disk_space", native_unit_of_measurement=UnitOfInformation.GIGABYTES, device_class=SensorDeviceClass.DATA_SIZE, - icon="mdi:harddisk", value_fn=get_space, state_class=SensorStateClass.TOTAL, description_fn=get_modified_description, @@ -83,7 +86,6 @@ SENSOR_TYPES: dict[str, LidarrSensorEntityDescription[Any]] = { key="queue", translation_key="queue", native_unit_of_measurement="Albums", - icon="mdi:download", value_fn=lambda data, _: data.totalRecords, state_class=SensorStateClass.TOTAL, attributes_fn=lambda data: {i.title: queue_str(i) for i in data.records}, @@ -92,7 +94,6 @@ SENSOR_TYPES: dict[str, LidarrSensorEntityDescription[Any]] = { key="wanted", translation_key="wanted", native_unit_of_measurement="Albums", - icon="mdi:music", value_fn=lambda data, _: data.totalRecords, state_class=SensorStateClass.TOTAL, entity_registry_enabled_default=False, diff --git a/homeassistant/components/lifx/__init__.py b/homeassistant/components/lifx/__init__.py index 76d4b7e36c5..47f00959bcd 100644 --- a/homeassistant/components/lifx/__init__.py +++ b/homeassistant/components/lifx/__init__.py @@ -1,4 +1,5 @@ """Support for LIFX.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/lifx/binary_sensor.py b/homeassistant/components/lifx/binary_sensor.py index 5719c881d1f..454561a6f4e 100644 --- a/homeassistant/components/lifx/binary_sensor.py +++ b/homeassistant/components/lifx/binary_sensor.py @@ -1,4 +1,5 @@ """Binary sensor entities for LIFX integration.""" + from __future__ import annotations from homeassistant.components.binary_sensor import ( diff --git a/homeassistant/components/lifx/button.py b/homeassistant/components/lifx/button.py index 86e3bc569b1..694c91b4c27 100644 --- a/homeassistant/components/lifx/button.py +++ b/homeassistant/components/lifx/button.py @@ -1,4 +1,5 @@ """Button entity for LIFX devices..""" + from __future__ import annotations from homeassistant.components.button import ( diff --git a/homeassistant/components/lifx/config_flow.py b/homeassistant/components/lifx/config_flow.py index b6fd67c0356..e4db80bec73 100644 --- a/homeassistant/components/lifx/config_flow.py +++ b/homeassistant/components/lifx/config_flow.py @@ -1,4 +1,5 @@ """Config flow flow LIFX.""" + from __future__ import annotations import socket @@ -8,12 +9,11 @@ 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.config_entries import ConfigFlow, ConfigFlowResult 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 @@ -36,7 +36,7 @@ from .util import ( ) -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class LifXConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for LIFX.""" VERSION = 1 @@ -46,7 +46,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._discovered_devices: dict[str, Light] = {} self._discovered_device: Light | None = None - async def async_step_dhcp(self, discovery_info: DhcpServiceInfo) -> FlowResult: + async def async_step_dhcp( + self, discovery_info: DhcpServiceInfo + ) -> ConfigFlowResult: """Handle discovery via DHCP.""" mac = discovery_info.macaddress host = discovery_info.ip @@ -69,13 +71,13 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_homekit( self, discovery_info: zeroconf.ZeroconfServiceInfo - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle HomeKit discovery.""" return await self._async_handle_discovery(host=discovery_info.host) async def async_step_integration_discovery( self, discovery_info: DiscoveryInfoType - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle LIFX UDP broadcast discovery.""" serial = discovery_info[CONF_SERIAL] host = discovery_info[CONF_HOST] @@ -85,7 +87,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def _async_handle_discovery( self, host: str, serial: str | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle any discovery.""" self._async_abort_entries_match({CONF_HOST: host}) self.context[CONF_HOST] = host @@ -120,7 +122,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_discovery_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm discovery.""" assert self._discovered_device is not None discovered = self._discovered_device @@ -146,7 +148,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors = {} if user_input is not None: @@ -168,7 +170,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_pick_device( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the step to pick discovered device.""" if user_input is not None: serial = user_input[CONF_DEVICE] @@ -207,7 +209,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) @callback - def _async_create_entry_from_device(self, device: Light) -> FlowResult: + def _async_create_entry_from_device(self, device: Light) -> ConfigFlowResult: """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( diff --git a/homeassistant/components/lifx/coordinator.py b/homeassistant/components/lifx/coordinator.py index 18a8a24cb94..63912cbb820 100644 --- a/homeassistant/components/lifx/coordinator.py +++ b/homeassistant/components/lifx/coordinator.py @@ -1,4 +1,5 @@ """Coordinator for lifx.""" + from __future__ import annotations import asyncio @@ -380,8 +381,7 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator[None]): # pad the color list with blanks if necessary if len(colors) < 82: - for _ in range(82 - len(colors)): - colors.append((0, 0, 0, 0)) + colors.extend([(0, 0, 0, 0) for _ in range(82 - len(colors))]) await async_execute_lifx( partial( diff --git a/homeassistant/components/lifx/diagnostics.py b/homeassistant/components/lifx/diagnostics.py index abe13cd1a50..b9ef1af4dc6 100644 --- a/homeassistant/components/lifx/diagnostics.py +++ b/homeassistant/components/lifx/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for LIFX.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/lifx/discovery.py b/homeassistant/components/lifx/discovery.py index a4072ee23ef..81c2d44de87 100644 --- a/homeassistant/components/lifx/discovery.py +++ b/homeassistant/components/lifx/discovery.py @@ -1,4 +1,5 @@ """The lifx integration discovery.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/lifx/entity.py b/homeassistant/components/lifx/entity.py index 4bc6b87393d..279bcb86594 100644 --- a/homeassistant/components/lifx/entity.py +++ b/homeassistant/components/lifx/entity.py @@ -1,4 +1,5 @@ """Support for LIFX lights.""" + from __future__ import annotations from aiolifx import products diff --git a/homeassistant/components/lifx/icons.json b/homeassistant/components/lifx/icons.json new file mode 100644 index 00000000000..bf9e5e732d5 --- /dev/null +++ b/homeassistant/components/lifx/icons.json @@ -0,0 +1,12 @@ +{ + "services": { + "set_hev_cycle_state": "mdi:led-on", + "set_state": "mdi:led-on", + "effect_pulse": "mdi:pulse", + "effect_colorloop": "mdi:looks", + "effect_move": "mdi:cube-send", + "effect_flame": "mdi:fire", + "effect_morph": "mdi:shape-outline", + "effect_stop": "mdi:stop" + } +} diff --git a/homeassistant/components/lifx/light.py b/homeassistant/components/lifx/light.py index 74ed209742c..90632f82d9e 100644 --- a/homeassistant/components/lifx/light.py +++ b/homeassistant/components/lifx/light.py @@ -1,4 +1,5 @@ """Support for LIFX lights.""" + from __future__ import annotations import asyncio @@ -417,7 +418,7 @@ class LIFXMultiZone(LIFXColor): await super().set_color(hsbk, kwargs, duration) return - zones = list(range(0, num_zones)) + zones = list(range(num_zones)) else: zones = [x for x in set(zones) if x < num_zones] diff --git a/homeassistant/components/lifx/manager.py b/homeassistant/components/lifx/manager.py index 9e72ded620e..038fdceab26 100644 --- a/homeassistant/components/lifx/manager.py +++ b/homeassistant/components/lifx/manager.py @@ -1,4 +1,5 @@ """Support for LIFX lights.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/lifx/migration.py b/homeassistant/components/lifx/migration.py index 359480a4507..9f8365cbceb 100644 --- a/homeassistant/components/lifx/migration.py +++ b/homeassistant/components/lifx/migration.py @@ -1,4 +1,5 @@ """Migrate lifx devices to their own config entry.""" + from __future__ import annotations from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/lifx/select.py b/homeassistant/components/lifx/select.py index 183e31dec1f..ef2967d1776 100644 --- a/homeassistant/components/lifx/select.py +++ b/homeassistant/components/lifx/select.py @@ -1,4 +1,5 @@ """Select sensor entities for LIFX integration.""" + from __future__ import annotations from aiolifx_themes.themes import ThemeLibrary diff --git a/homeassistant/components/lifx/sensor.py b/homeassistant/components/lifx/sensor.py index e10f9579bc3..2f54317f9bd 100644 --- a/homeassistant/components/lifx/sensor.py +++ b/homeassistant/components/lifx/sensor.py @@ -1,4 +1,5 @@ """Sensors for LIFX lights.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/lifx/util.py b/homeassistant/components/lifx/util.py index 5d41839f61d..bb77c7595d3 100644 --- a/homeassistant/components/lifx/util.py +++ b/homeassistant/components/lifx/util.py @@ -62,7 +62,7 @@ def infrared_brightness_value_to_option(value: int) -> str | None: def infrared_brightness_option_to_value(option: str) -> int | None: """Convert infrared brightness option to value.""" option_values = {v: k for k, v in INFRARED_BRIGHTNESS_VALUES_MAP.items()} - return option_values.get(option, None) + return option_values.get(option) def convert_8_to_16(value: int) -> int: diff --git a/homeassistant/components/lifx_cloud/scene.py b/homeassistant/components/lifx_cloud/scene.py index 61656741f82..8e7ab0c2d46 100644 --- a/homeassistant/components/lifx_cloud/scene.py +++ b/homeassistant/components/lifx_cloud/scene.py @@ -1,4 +1,5 @@ """Support for LIFX Cloud scenes.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 795975b5c3e..53c7328ece4 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -1,4 +1,5 @@ """Provides functionality to interact with lights.""" + from __future__ import annotations from collections.abc import Iterable @@ -402,7 +403,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: await component.async_setup(config) profiles = hass.data[DATA_PROFILES] = Profiles(hass) - await profiles.async_initialize() + # Profiles are loaded in a separate task to avoid delaying the setup + # of the light base platform. + hass.async_create_task(profiles.async_initialize(), eager_start=True) def preprocess_data(data: dict[str, Any]) -> dict[str | vol.Optional, Any]: """Preprocess the service data.""" @@ -1215,9 +1218,9 @@ class LightEntity(ToggleEntity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): color_temp_kelvin = self.color_temp_kelvin data[ATTR_COLOR_TEMP_KELVIN] = color_temp_kelvin if color_temp_kelvin: - data[ - ATTR_COLOR_TEMP - ] = color_util.color_temperature_kelvin_to_mired(color_temp_kelvin) + data[ATTR_COLOR_TEMP] = ( + color_util.color_temperature_kelvin_to_mired(color_temp_kelvin) + ) else: data[ATTR_COLOR_TEMP] = None else: @@ -1230,9 +1233,9 @@ class LightEntity(ToggleEntity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): color_temp_kelvin = self.color_temp_kelvin data[ATTR_COLOR_TEMP_KELVIN] = color_temp_kelvin if color_temp_kelvin: - data[ - ATTR_COLOR_TEMP - ] = color_util.color_temperature_kelvin_to_mired(color_temp_kelvin) + data[ATTR_COLOR_TEMP] = ( + color_util.color_temperature_kelvin_to_mired(color_temp_kelvin) + ) else: data[ATTR_COLOR_TEMP] = None else: @@ -1336,5 +1339,5 @@ class LightEntity(ToggleEntity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): """Return if light color mode issues should be reported.""" if not self.platform: return True - # philips_js and tuya have known issues, we don't need users to open issues - return self.platform.platform_name not in {"philips_js", "tuya"} + # philips_js has known issues, we don't need users to open issues + return self.platform.platform_name not in {"philips_js"} diff --git a/homeassistant/components/light/device_action.py b/homeassistant/components/light/device_action.py index 2b49c963438..dbdf7200a7b 100644 --- a/homeassistant/components/light/device_action.py +++ b/homeassistant/components/light/device_action.py @@ -1,4 +1,5 @@ """Provides device actions for lights.""" + from __future__ import annotations import voluptuous as vol @@ -46,8 +47,12 @@ _ACTION_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend( vol.Required(ATTR_ENTITY_ID): cv.entity_id_or_uuid, vol.Required(CONF_DOMAIN): DOMAIN, vol.Required(CONF_TYPE): vol.In( - toggle_entity.DEVICE_ACTION_TYPES - + [TYPE_BRIGHTNESS_INCREASE, TYPE_BRIGHTNESS_DECREASE, TYPE_FLASH] + [ + *toggle_entity.DEVICE_ACTION_TYPES, + TYPE_BRIGHTNESS_INCREASE, + TYPE_BRIGHTNESS_DECREASE, + TYPE_FLASH, + ] ), vol.Optional(ATTR_BRIGHTNESS_PCT): VALID_BRIGHTNESS_PCT, vol.Optional(ATTR_FLASH): VALID_FLASH, diff --git a/homeassistant/components/light/device_condition.py b/homeassistant/components/light/device_condition.py index 12e86c1e23d..f9bb7c30bd7 100644 --- a/homeassistant/components/light/device_condition.py +++ b/homeassistant/components/light/device_condition.py @@ -1,4 +1,5 @@ """Provides device conditions for lights.""" + from __future__ import annotations import voluptuous as vol diff --git a/homeassistant/components/light/device_trigger.py b/homeassistant/components/light/device_trigger.py index 5ae5b12bf61..033ea75357e 100644 --- a/homeassistant/components/light/device_trigger.py +++ b/homeassistant/components/light/device_trigger.py @@ -1,4 +1,5 @@ """Provides device trigger for lights.""" + from __future__ import annotations import voluptuous as vol diff --git a/homeassistant/components/light/group.py b/homeassistant/components/light/group.py deleted file mode 100644 index 234883ffd5a..00000000000 --- a/homeassistant/components/light/group.py +++ /dev/null @@ -1,14 +0,0 @@ -"""Describe group states.""" - - -from homeassistant.components.group import GroupIntegrationRegistry -from homeassistant.const import STATE_OFF, STATE_ON -from homeassistant.core import HomeAssistant, callback - - -@callback -def async_describe_on_off_states( - hass: HomeAssistant, registry: GroupIntegrationRegistry -) -> None: - """Describe group on off states.""" - registry.on_off_states({STATE_ON}, STATE_OFF) diff --git a/homeassistant/components/light/intent.py b/homeassistant/components/light/intent.py index 605434af916..53127babee9 100644 --- a/homeassistant/components/light/intent.py +++ b/homeassistant/components/light/intent.py @@ -1,4 +1,5 @@ """Intents for the light integration.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/light/reproduce_state.py b/homeassistant/components/light/reproduce_state.py index 54fcd01843c..4024f2f84ba 100644 --- a/homeassistant/components/light/reproduce_state.py +++ b/homeassistant/components/light/reproduce_state.py @@ -1,4 +1,5 @@ """Reproduce an Light state.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/light/significant_change.py b/homeassistant/components/light/significant_change.py index dc8f711b579..1877c925622 100644 --- a/homeassistant/components/light/significant_change.py +++ b/homeassistant/components/light/significant_change.py @@ -1,4 +1,5 @@ """Helper to test significant Light state changes.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/lightwave/__init__.py b/homeassistant/components/lightwave/__init__.py index 9feefd6e24d..6c462b040d4 100644 --- a/homeassistant/components/lightwave/__init__.py +++ b/homeassistant/components/lightwave/__init__.py @@ -1,4 +1,5 @@ """Support for device connected via Lightwave WiFi-link hub.""" + import logging from lightwave.lightwave import LWLink diff --git a/homeassistant/components/lightwave/climate.py b/homeassistant/components/lightwave/climate.py index 5e89e4f8145..1016e8ce80d 100644 --- a/homeassistant/components/lightwave/climate.py +++ b/homeassistant/components/lightwave/climate.py @@ -1,4 +1,5 @@ """Support for LightwaveRF TRVs.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/lightwave/light.py b/homeassistant/components/lightwave/light.py index f89dbb6bf5f..fb007b321ab 100644 --- a/homeassistant/components/lightwave/light.py +++ b/homeassistant/components/lightwave/light.py @@ -1,4 +1,5 @@ """Support for LightwaveRF lights.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/lightwave/sensor.py b/homeassistant/components/lightwave/sensor.py index dac591aea34..721c508dd99 100644 --- a/homeassistant/components/lightwave/sensor.py +++ b/homeassistant/components/lightwave/sensor.py @@ -1,4 +1,5 @@ """Support for LightwaveRF TRV - Associated Battery.""" + from __future__ import annotations from homeassistant.components.sensor import ( diff --git a/homeassistant/components/lightwave/switch.py b/homeassistant/components/lightwave/switch.py index 67b69d0e5c4..ca146ca881c 100644 --- a/homeassistant/components/lightwave/switch.py +++ b/homeassistant/components/lightwave/switch.py @@ -1,4 +1,5 @@ """Support for LightwaveRF switches.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/limitlessled/light.py b/homeassistant/components/limitlessled/light.py index 926e0a8a6d6..0b666b59faa 100644 --- a/homeassistant/components/limitlessled/light.py +++ b/homeassistant/components/limitlessled/light.py @@ -1,4 +1,5 @@ """Support for LimitlessLED bulbs.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/linear_garage_door/__init__.py b/homeassistant/components/linear_garage_door/__init__.py index d168da511e0..e21d8eaba58 100644 --- a/homeassistant/components/linear_garage_door/__init__.py +++ b/homeassistant/components/linear_garage_door/__init__.py @@ -1,4 +1,5 @@ """The Linear Garage Door integration.""" + from __future__ import annotations from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/linear_garage_door/config_flow.py b/homeassistant/components/linear_garage_door/config_flow.py index 6bca49adb4c..bfb6f825030 100644 --- a/homeassistant/components/linear_garage_door/config_flow.py +++ b/homeassistant/components/linear_garage_door/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Linear Garage Door integration.""" + from __future__ import annotations from collections.abc import Collection, Mapping, Sequence @@ -10,10 +11,9 @@ from linear_garage_door import Linear from linear_garage_door.errors import InvalidLoginError import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_EMAIL, CONF_PASSWORD from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -63,7 +63,7 @@ async def validate_input( return info -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class LinearGarageDoorConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Linear Garage Door.""" VERSION = 1 @@ -71,11 +71,11 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize the config flow.""" self.data: dict[str, Sequence[Collection[str]]] = {} - self._reauth_entry: config_entries.ConfigEntry | None = None + self._reauth_entry: ConfigEntry | None = None async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" data_schema = STEP_USER_DATA_SCHEMA @@ -115,7 +115,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_site( self, user_input: dict[str, Any] | None = None, - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the site step.""" if isinstance(self.data["sites"], list): @@ -150,7 +150,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): }, ) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Reauth in case of a password change or other error.""" self._reauth_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] diff --git a/homeassistant/components/linear_garage_door/coordinator.py b/homeassistant/components/linear_garage_door/coordinator.py index e9234327429..b771b552b62 100644 --- a/homeassistant/components/linear_garage_door/coordinator.py +++ b/homeassistant/components/linear_garage_door/coordinator.py @@ -1,4 +1,5 @@ """DataUpdateCoordinator for Linear.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/linear_garage_door/diagnostics.py b/homeassistant/components/linear_garage_door/diagnostics.py index fffcdd7de87..fc4906daa77 100644 --- a/homeassistant/components/linear_garage_door/diagnostics.py +++ b/homeassistant/components/linear_garage_door/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for Linear Garage Door.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/linksys_smart/device_tracker.py b/homeassistant/components/linksys_smart/device_tracker.py index d0440c832c8..a33f0070c70 100644 --- a/homeassistant/components/linksys_smart/device_tracker.py +++ b/homeassistant/components/linksys_smart/device_tracker.py @@ -1,4 +1,5 @@ """Support for Linksys Smart Wifi routers.""" + from __future__ import annotations from http import HTTPStatus diff --git a/homeassistant/components/linode/__init__.py b/homeassistant/components/linode/__init__.py index bd61519edc4..2ed3cf244d0 100644 --- a/homeassistant/components/linode/__init__.py +++ b/homeassistant/components/linode/__init__.py @@ -1,4 +1,5 @@ """Support for Linode.""" + from datetime import timedelta import logging diff --git a/homeassistant/components/linode/binary_sensor.py b/homeassistant/components/linode/binary_sensor.py index 17a68e9be9c..02c7a1ef383 100644 --- a/homeassistant/components/linode/binary_sensor.py +++ b/homeassistant/components/linode/binary_sensor.py @@ -1,4 +1,5 @@ """Support for monitoring the state of Linode Nodes.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/linode/switch.py b/homeassistant/components/linode/switch.py index b59e8f901e5..f2665671c0b 100644 --- a/homeassistant/components/linode/switch.py +++ b/homeassistant/components/linode/switch.py @@ -1,4 +1,5 @@ """Support for interacting with Linode nodes.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/linux_battery/sensor.py b/homeassistant/components/linux_battery/sensor.py index 08b2dc33bae..9dc0e8c675d 100644 --- a/homeassistant/components/linux_battery/sensor.py +++ b/homeassistant/components/linux_battery/sensor.py @@ -1,4 +1,5 @@ """Details about the built-in battery.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/lirc/__init__.py b/homeassistant/components/lirc/__init__.py index 1b9688906ff..b847a160f51 100644 --- a/homeassistant/components/lirc/__init__.py +++ b/homeassistant/components/lirc/__init__.py @@ -1,4 +1,5 @@ """Support for LIRC devices.""" + import logging import threading import time diff --git a/homeassistant/components/litejet/__init__.py b/homeassistant/components/litejet/__init__.py index da24aee9ab8..e9d1cca74cb 100644 --- a/homeassistant/components/litejet/__init__.py +++ b/homeassistant/components/litejet/__init__.py @@ -1,4 +1,5 @@ """Support for the LiteJet lighting system.""" + import logging import pylitejet diff --git a/homeassistant/components/litejet/config_flow.py b/homeassistant/components/litejet/config_flow.py index a7b5a6f000e..19ddf0122c4 100644 --- a/homeassistant/components/litejet/config_flow.py +++ b/homeassistant/components/litejet/config_flow.py @@ -1,4 +1,5 @@ """Config flow for the LiteJet lighting system.""" + from __future__ import annotations from typing import Any @@ -7,25 +8,29 @@ import pylitejet from serial import SerialException import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.const import CONF_PORT from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult import homeassistant.helpers.config_validation as cv from .const import CONF_DEFAULT_TRANSITION, DOMAIN -class LiteJetOptionsFlow(config_entries.OptionsFlow): +class LiteJetOptionsFlow(OptionsFlow): """Handle LiteJet options.""" - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + def __init__(self, config_entry: ConfigEntry) -> None: """Initialize LiteJet options flow.""" self.config_entry = config_entry async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Manage LiteJet options.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) @@ -45,12 +50,12 @@ class LiteJetOptionsFlow(config_entries.OptionsFlow): ) -class LiteJetConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class LiteJetConfigFlow(ConfigFlow, domain=DOMAIN): """LiteJet config flow.""" async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Create a LiteJet config entry based upon user input.""" if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") @@ -79,7 +84,7 @@ class LiteJetConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback def async_get_options_flow( - config_entry: config_entries.ConfigEntry, + config_entry: ConfigEntry, ) -> LiteJetOptionsFlow: """Get the options flow for this handler.""" return LiteJetOptionsFlow(config_entry) diff --git a/homeassistant/components/litejet/const.py b/homeassistant/components/litejet/const.py index baeadd7f4a9..69492efd0c7 100644 --- a/homeassistant/components/litejet/const.py +++ b/homeassistant/components/litejet/const.py @@ -1,4 +1,5 @@ """LiteJet constants.""" + from homeassistant.const import Platform DOMAIN = "litejet" diff --git a/homeassistant/components/litejet/diagnostics.py b/homeassistant/components/litejet/diagnostics.py index 48f38542dfd..7a10f4d6754 100644 --- a/homeassistant/components/litejet/diagnostics.py +++ b/homeassistant/components/litejet/diagnostics.py @@ -1,4 +1,5 @@ """Support for LiteJet diagnostics.""" + from typing import Any from pylitejet import LiteJet diff --git a/homeassistant/components/litejet/light.py b/homeassistant/components/litejet/light.py index 167f7a62a00..f2b9af9adb4 100644 --- a/homeassistant/components/litejet/light.py +++ b/homeassistant/components/litejet/light.py @@ -1,4 +1,5 @@ """Support for LiteJet lights.""" + from __future__ import annotations from typing import Any @@ -96,7 +97,7 @@ class LiteJetLight(LightEntity): try: await self._lj.activate_load(self._index) except LiteJetError as exc: - raise HomeAssistantError() from exc + raise HomeAssistantError from exc return # If either attribute is specified then Home Assistant must @@ -108,7 +109,7 @@ class LiteJetLight(LightEntity): try: await self._lj.activate_load_at(self._index, brightness, int(transition)) except LiteJetError as exc: - raise HomeAssistantError() from exc + raise HomeAssistantError from exc async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the light.""" @@ -116,7 +117,7 @@ class LiteJetLight(LightEntity): try: await self._lj.activate_load_at(self._index, 0, kwargs[ATTR_TRANSITION]) except LiteJetError as exc: - raise HomeAssistantError() from exc + raise HomeAssistantError from exc return # If transition attribute is not specified then the simple @@ -125,7 +126,7 @@ class LiteJetLight(LightEntity): try: await self._lj.deactivate_load(self._index) except LiteJetError as exc: - raise HomeAssistantError() from exc + raise HomeAssistantError from exc async def async_update(self) -> None: """Retrieve the light's brightness from the LiteJet system.""" diff --git a/homeassistant/components/litejet/scene.py b/homeassistant/components/litejet/scene.py index ec8e4d697fe..712e223aa3e 100644 --- a/homeassistant/components/litejet/scene.py +++ b/homeassistant/components/litejet/scene.py @@ -1,4 +1,5 @@ """Support for LiteJet scenes.""" + import logging from typing import Any @@ -76,4 +77,4 @@ class LiteJetScene(Scene): try: await self._lj.activate_scene(self._index) except LiteJetError as exc: - raise HomeAssistantError() from exc + raise HomeAssistantError from exc diff --git a/homeassistant/components/litejet/switch.py b/homeassistant/components/litejet/switch.py index 5089b9ec0f9..28f751f3ec1 100644 --- a/homeassistant/components/litejet/switch.py +++ b/homeassistant/components/litejet/switch.py @@ -1,4 +1,5 @@ """Support for LiteJet switch.""" + from typing import Any from pylitejet import LiteJet, LiteJetError @@ -91,11 +92,11 @@ class LiteJetSwitch(SwitchEntity): try: await self._lj.press_switch(self._index) except LiteJetError as exc: - raise HomeAssistantError() from exc + raise HomeAssistantError from exc async def async_turn_off(self, **kwargs: Any) -> None: """Release the switch.""" try: await self._lj.release_switch(self._index) except LiteJetError as exc: - raise HomeAssistantError() from exc + raise HomeAssistantError from exc diff --git a/homeassistant/components/litejet/trigger.py b/homeassistant/components/litejet/trigger.py index df5ffac9b99..2786cc8b76a 100644 --- a/homeassistant/components/litejet/trigger.py +++ b/homeassistant/components/litejet/trigger.py @@ -1,4 +1,5 @@ """Trigger an automation when a LiteJet switch is released.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/litterrobot/__init__.py b/homeassistant/components/litterrobot/__init__.py index daf71fe8a6e..ec9849bbb89 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 import FeederRobot, LitterRobot, LitterRobot3, LitterRobot4, Robot diff --git a/homeassistant/components/litterrobot/binary_sensor.py b/homeassistant/components/litterrobot/binary_sensor.py index 6a588c36d6c..2f44f44ed53 100644 --- a/homeassistant/components/litterrobot/binary_sensor.py +++ b/homeassistant/components/litterrobot/binary_sensor.py @@ -1,4 +1,5 @@ """Support for Litter-Robot binary sensors.""" + from __future__ import annotations from collections.abc import Callable @@ -52,7 +53,6 @@ BINARY_SENSOR_MAP: dict[type[Robot], tuple[RobotBinarySensorEntityDescription, . RobotBinarySensorEntityDescription[LitterRobot]( key="sleeping", translation_key="sleeping", - icon="mdi:sleep", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, is_on_fn=lambda robot: robot.is_sleeping, @@ -60,7 +60,6 @@ BINARY_SENSOR_MAP: dict[type[Robot], tuple[RobotBinarySensorEntityDescription, . RobotBinarySensorEntityDescription[LitterRobot]( key="sleep_mode", translation_key="sleep_mode", - icon="mdi:sleep", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, is_on_fn=lambda robot: robot.sleep_mode_enabled, diff --git a/homeassistant/components/litterrobot/button.py b/homeassistant/components/litterrobot/button.py index de93ead5190..02477e7fa03 100644 --- a/homeassistant/components/litterrobot/button.py +++ b/homeassistant/components/litterrobot/button.py @@ -1,4 +1,5 @@ """Support for Litter-Robot button.""" + from __future__ import annotations from collections.abc import Callable, Coroutine @@ -61,14 +62,12 @@ class RobotButtonEntityDescription(ButtonEntityDescription, RequiredKeysMixin[_R LITTER_ROBOT_BUTTON = RobotButtonEntityDescription[LitterRobot3]( key="reset_waste_drawer", translation_key="reset_waste_drawer", - icon="mdi:delete-variant", entity_category=EntityCategory.CONFIG, press_fn=lambda robot: robot.reset_waste_drawer(), ) FEEDER_ROBOT_BUTTON = RobotButtonEntityDescription[FeederRobot]( key="give_snack", translation_key="give_snack", - icon="mdi:candy-outline", press_fn=lambda robot: robot.give_snack(), ) diff --git a/homeassistant/components/litterrobot/config_flow.py b/homeassistant/components/litterrobot/config_flow.py index 558945ca1db..39a1646a8b7 100644 --- a/homeassistant/components/litterrobot/config_flow.py +++ b/homeassistant/components/litterrobot/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Litter-Robot integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -9,9 +10,8 @@ from pylitterbot import Account from pylitterbot.exceptions import LitterRobotException, LitterRobotLoginException import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN @@ -23,21 +23,23 @@ STEP_USER_DATA_SCHEMA = vol.Schema( ) -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class LitterRobotConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Litter-Robot.""" VERSION = 1 username: str - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Handle a reauthorization flow request.""" self.username = entry_data[CONF_USERNAME] return await self.async_step_reauth_confirm() async def async_step_reauth_confirm( self, user_input: dict[str, str] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle user's reauth credentials.""" errors = {} if user_input: @@ -62,7 +64,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: Mapping[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors = {} diff --git a/homeassistant/components/litterrobot/const.py b/homeassistant/components/litterrobot/const.py index 5ac889d9b73..632465b902d 100644 --- a/homeassistant/components/litterrobot/const.py +++ b/homeassistant/components/litterrobot/const.py @@ -1,2 +1,3 @@ """Constants for the Litter-Robot integration.""" + DOMAIN = "litterrobot" diff --git a/homeassistant/components/litterrobot/entity.py b/homeassistant/components/litterrobot/entity.py index fb1fbe58a7b..4639404b92b 100644 --- a/homeassistant/components/litterrobot/entity.py +++ b/homeassistant/components/litterrobot/entity.py @@ -1,4 +1,5 @@ """Litter-Robot entities for common data and methods.""" + from __future__ import annotations from typing import Generic, TypeVar diff --git a/homeassistant/components/litterrobot/hub.py b/homeassistant/components/litterrobot/hub.py index 5dc8098a8df..4af004bddf5 100644 --- a/homeassistant/components/litterrobot/hub.py +++ b/homeassistant/components/litterrobot/hub.py @@ -1,4 +1,5 @@ """A wrapper 'hub' for the Litter-Robot API.""" + from __future__ import annotations from collections.abc import Generator, Mapping diff --git a/homeassistant/components/litterrobot/icons.json b/homeassistant/components/litterrobot/icons.json new file mode 100644 index 00000000000..333f309e9e8 --- /dev/null +++ b/homeassistant/components/litterrobot/icons.json @@ -0,0 +1,45 @@ +{ + "entity": { + "binary_sensor": { + "sleeping": { + "default": "mdi:sleep" + }, + "sleep_mode": { + "default": "mdi:sleep" + } + }, + "button": { + "reset_waste_drawer": { + "default": "mdi:delete-variant" + }, + "give_snack": { + "default": "mdi:candy-outline" + } + }, + "select": { + "cycle_delay": { + "default": "mdi:timer-outline" + }, + "meal_insert_size": { + "default": "mdi:scale" + } + }, + "switch": { + "night_light_mode": { + "default": "mdi:lightbulb-off", + "state": { + "on": "mdi:lightbulb-on" + } + }, + "panel_lockout": { + "default": "mdi:lock-open", + "state": { + "on": "mdi:lock" + } + } + } + }, + "services": { + "set_sleep_mode": "mdi:sleep" + } +} diff --git a/homeassistant/components/litterrobot/select.py b/homeassistant/components/litterrobot/select.py index 726cfaebaeb..e7ecbada10d 100644 --- a/homeassistant/components/litterrobot/select.py +++ b/homeassistant/components/litterrobot/select.py @@ -1,4 +1,5 @@ """Support for Litter-Robot selects.""" + from __future__ import annotations from collections.abc import Callable, Coroutine @@ -51,7 +52,6 @@ ROBOT_SELECT_MAP: dict[type[Robot], RobotSelectEntityDescription] = { LitterRobot: RobotSelectEntityDescription[LitterRobot, int]( # type: ignore[type-abstract] # only used for isinstance check key="cycle_delay", translation_key="cycle_delay", - icon="mdi:timer-outline", unit_of_measurement=UnitOfTime.MINUTES, current_fn=lambda robot: robot.clean_cycle_wait_time_minutes, options_fn=lambda robot: robot.VALID_WAIT_TIMES, @@ -72,7 +72,6 @@ ROBOT_SELECT_MAP: dict[type[Robot], RobotSelectEntityDescription] = { FeederRobot: RobotSelectEntityDescription[FeederRobot, float]( key="meal_insert_size", translation_key="meal_insert_size", - icon="mdi:scale", unit_of_measurement="cups", current_fn=lambda robot: robot.meal_insert_size, options_fn=lambda robot: robot.VALID_MEAL_INSERT_SIZES, diff --git a/homeassistant/components/litterrobot/sensor.py b/homeassistant/components/litterrobot/sensor.py index a25921e440c..1b4b7f78fdc 100644 --- a/homeassistant/components/litterrobot/sensor.py +++ b/homeassistant/components/litterrobot/sensor.py @@ -1,4 +1,5 @@ """Support for Litter-Robot sensors.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/litterrobot/switch.py b/homeassistant/components/litterrobot/switch.py index 84e6fa2be67..60ca9b4d6c7 100644 --- a/homeassistant/components/litterrobot/switch.py +++ b/homeassistant/components/litterrobot/switch.py @@ -1,4 +1,5 @@ """Support for Litter-Robot switches.""" + from __future__ import annotations from collections.abc import Callable, Coroutine @@ -22,7 +23,6 @@ from .hub import LitterRobotHub class RequiredKeysMixin(Generic[_RobotT]): """A class that describes robot switch entity required keys.""" - icons: tuple[str, str] set_fn: Callable[[_RobotT, bool], Coroutine[Any, Any, bool]] @@ -37,13 +37,11 @@ ROBOT_SWITCHES = [ RobotSwitchEntityDescription[LitterRobot | FeederRobot]( key="night_light_mode_enabled", translation_key="night_light_mode", - icons=("mdi:lightbulb-on", "mdi:lightbulb-off"), set_fn=lambda robot, value: robot.set_night_light(value), ), RobotSwitchEntityDescription[LitterRobot | FeederRobot]( key="panel_lock_enabled", translation_key="panel_lockout", - icons=("mdi:lock", "mdi:lock-open"), set_fn=lambda robot, value: robot.set_panel_lockout(value), ), ] @@ -59,12 +57,6 @@ class RobotSwitchEntity(LitterRobotEntity[_RobotT], SwitchEntity): """Return true if switch is on.""" return bool(getattr(self.robot, self.entity_description.key)) - @property - def icon(self) -> str: - """Return the icon.""" - icon_on, icon_off = self.entity_description.icons - return icon_on if self.is_on else icon_off - async def async_turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" await self.entity_description.set_fn(self.robot, True) diff --git a/homeassistant/components/litterrobot/time.py b/homeassistant/components/litterrobot/time.py index bb840e17a8f..4e5e80a8ca6 100644 --- a/homeassistant/components/litterrobot/time.py +++ b/homeassistant/components/litterrobot/time.py @@ -1,4 +1,5 @@ """Support for Litter-Robot time.""" + from __future__ import annotations from collections.abc import Callable, Coroutine diff --git a/homeassistant/components/litterrobot/update.py b/homeassistant/components/litterrobot/update.py index 584a6af77c2..c4d1ada6080 100644 --- a/homeassistant/components/litterrobot/update.py +++ b/homeassistant/components/litterrobot/update.py @@ -1,4 +1,5 @@ """Support for Litter-Robot updates.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/litterrobot/vacuum.py b/homeassistant/components/litterrobot/vacuum.py index 681af81481d..4f9efa2dff7 100644 --- a/homeassistant/components/litterrobot/vacuum.py +++ b/homeassistant/components/litterrobot/vacuum.py @@ -1,4 +1,5 @@ """Support for Litter-Robot "Vacuum".""" + from __future__ import annotations from datetime import time diff --git a/homeassistant/components/livisi/__init__.py b/homeassistant/components/livisi/__init__.py index e638c84a917..26e36e68efa 100644 --- a/homeassistant/components/livisi/__init__.py +++ b/homeassistant/components/livisi/__init__.py @@ -1,4 +1,5 @@ """The Livisi Smart Home integration.""" + from __future__ import annotations from typing import Final diff --git a/homeassistant/components/livisi/binary_sensor.py b/homeassistant/components/livisi/binary_sensor.py index 42170bbeb4c..d4edd59f2d7 100644 --- a/homeassistant/components/livisi/binary_sensor.py +++ b/homeassistant/components/livisi/binary_sensor.py @@ -1,4 +1,5 @@ """Code to handle a Livisi Binary Sensor.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/livisi/climate.py b/homeassistant/components/livisi/climate.py index 6990dabff1d..56fe63d351f 100644 --- a/homeassistant/components/livisi/climate.py +++ b/homeassistant/components/livisi/climate.py @@ -1,4 +1,5 @@ """Code to handle a Livisi Virtual Climate Control.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/livisi/config_flow.py b/homeassistant/components/livisi/config_flow.py index c8685eb2390..7317aec0abc 100644 --- a/homeassistant/components/livisi/config_flow.py +++ b/homeassistant/components/livisi/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Livisi Home Assistant.""" + from __future__ import annotations from contextlib import suppress @@ -8,15 +9,14 @@ from aiohttp import ClientConnectorError from aiolivisi import AioLivisi, errors as livisi_errors import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_PASSWORD -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import aiohttp_client from .const import DOMAIN, LOGGER -class LivisiFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class LivisiFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a Livisi Smart Home config flow.""" def __init__(self) -> None: @@ -31,7 +31,7 @@ class LivisiFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, str] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" if user_input is None: return self.async_show_form(step_id="user", data_schema=self.data_schema) @@ -70,7 +70,7 @@ class LivisiFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def create_entity( self, user_input: dict[str, str], controller_info: dict[str, Any] - ) -> FlowResult: + ) -> ConfigFlowResult: """Create LIVISI entity.""" if (controller_data := controller_info.get("gateway")) is None: controller_data = controller_info diff --git a/homeassistant/components/livisi/const.py b/homeassistant/components/livisi/const.py index 2769e6030ee..49e7ef8cfda 100644 --- a/homeassistant/components/livisi/const.py +++ b/homeassistant/components/livisi/const.py @@ -1,4 +1,5 @@ """Constants for the Livisi Smart Home integration.""" + import logging from typing import Final diff --git a/homeassistant/components/livisi/coordinator.py b/homeassistant/components/livisi/coordinator.py index 17a3b1828d0..7cb5757310f 100644 --- a/homeassistant/components/livisi/coordinator.py +++ b/homeassistant/components/livisi/coordinator.py @@ -1,4 +1,5 @@ """Code to manage fetching LIVISI data API.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/livisi/entity.py b/homeassistant/components/livisi/entity.py index f76901ddb05..3160b8f288a 100644 --- a/homeassistant/components/livisi/entity.py +++ b/homeassistant/components/livisi/entity.py @@ -1,4 +1,5 @@ """Code to handle a Livisi switches.""" + from __future__ import annotations from collections.abc import Mapping diff --git a/homeassistant/components/livisi/switch.py b/homeassistant/components/livisi/switch.py index 2c5a2b5137b..fa604c5fc87 100644 --- a/homeassistant/components/livisi/switch.py +++ b/homeassistant/components/livisi/switch.py @@ -1,4 +1,5 @@ """Code to handle a Livisi switches.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/llamalab_automate/notify.py b/homeassistant/components/llamalab_automate/notify.py index 8361d65725c..6ce00db71c3 100644 --- a/homeassistant/components/llamalab_automate/notify.py +++ b/homeassistant/components/llamalab_automate/notify.py @@ -1,4 +1,5 @@ """LlamaLab Automate notification service.""" + from __future__ import annotations from http import HTTPStatus diff --git a/homeassistant/components/local_calendar/__init__.py b/homeassistant/components/local_calendar/__init__.py index 3b302742ab6..2be5133a21c 100644 --- a/homeassistant/components/local_calendar/__init__.py +++ b/homeassistant/components/local_calendar/__init__.py @@ -1,4 +1,5 @@ """The Local Calendar integration.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/local_calendar/config_flow.py b/homeassistant/components/local_calendar/config_flow.py index a5a75fee58b..8caa3a5d528 100644 --- a/homeassistant/components/local_calendar/config_flow.py +++ b/homeassistant/components/local_calendar/config_flow.py @@ -1,12 +1,12 @@ """Config flow for Local Calendar 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 homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.util import slugify from .const import CONF_CALENDAR_NAME, CONF_STORAGE_KEY, DOMAIN @@ -18,14 +18,14 @@ STEP_USER_DATA_SCHEMA = vol.Schema( ) -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class LocalCalendarConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Local Calendar.""" VERSION = 1 async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" if user_input is None: return self.async_show_form( diff --git a/homeassistant/components/local_file/camera.py b/homeassistant/components/local_file/camera.py index 5b29516e03d..72fe1a88b86 100644 --- a/homeassistant/components/local_file/camera.py +++ b/homeassistant/components/local_file/camera.py @@ -1,4 +1,5 @@ """Camera that loads a picture from a local file.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/local_file/const.py b/homeassistant/components/local_file/const.py index 3ea98f89c0e..03f6cf641f1 100644 --- a/homeassistant/components/local_file/const.py +++ b/homeassistant/components/local_file/const.py @@ -1,4 +1,5 @@ """Constants for the Local File Camera component.""" + DOMAIN = "local_file" SERVICE_UPDATE_FILE_PATH = "update_file_path" DATA_LOCAL_FILE = "local_file_cameras" diff --git a/homeassistant/components/local_file/icons.json b/homeassistant/components/local_file/icons.json new file mode 100644 index 00000000000..c9c92fa86c8 --- /dev/null +++ b/homeassistant/components/local_file/icons.json @@ -0,0 +1,5 @@ +{ + "services": { + "update_file_path": "mdi:cog" + } +} diff --git a/homeassistant/components/local_ip/__init__.py b/homeassistant/components/local_ip/__init__.py index b5ed762ef5d..45ddbed7150 100644 --- a/homeassistant/components/local_ip/__init__.py +++ b/homeassistant/components/local_ip/__init__.py @@ -1,4 +1,5 @@ """Get the local IP address of the Home Assistant instance.""" + from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/local_ip/config_flow.py b/homeassistant/components/local_ip/config_flow.py index be708f5d8b9..3a4612d84aa 100644 --- a/homeassistant/components/local_ip/config_flow.py +++ b/homeassistant/components/local_ip/config_flow.py @@ -1,10 +1,10 @@ """Config flow for local_ip.""" + from __future__ import annotations from typing import Any -from homeassistant.config_entries import ConfigFlow -from homeassistant.data_entry_flow import FlowResult +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from .const import DOMAIN @@ -14,7 +14,7 @@ class SimpleConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") diff --git a/homeassistant/components/local_ip/const.py b/homeassistant/components/local_ip/const.py index b079ec87663..38390cbfdc1 100644 --- a/homeassistant/components/local_ip/const.py +++ b/homeassistant/components/local_ip/const.py @@ -1,4 +1,5 @@ """Local IP constants.""" + from homeassistant.const import Platform DOMAIN = "local_ip" diff --git a/homeassistant/components/local_ip/icons.json b/homeassistant/components/local_ip/icons.json new file mode 100644 index 00000000000..34b329aae63 --- /dev/null +++ b/homeassistant/components/local_ip/icons.json @@ -0,0 +1,9 @@ +{ + "entity": { + "sensor": { + "local_ip": { + "default": "mdi:ip" + } + } + } +} diff --git a/homeassistant/components/local_ip/sensor.py b/homeassistant/components/local_ip/sensor.py index 4c502895b3f..7f855220563 100644 --- a/homeassistant/components/local_ip/sensor.py +++ b/homeassistant/components/local_ip/sensor.py @@ -24,7 +24,7 @@ class IPSensor(SensorEntity): """A simple sensor.""" _attr_unique_id = SENSOR - _attr_icon = "mdi:ip" + _attr_translation_key = "local_ip" def __init__(self, name: str) -> None: """Initialize the sensor.""" diff --git a/homeassistant/components/local_todo/__init__.py b/homeassistant/components/local_todo/__init__.py index f8403251ba0..8245822bd9f 100644 --- a/homeassistant/components/local_todo/__init__.py +++ b/homeassistant/components/local_todo/__init__.py @@ -1,4 +1,5 @@ """The Local To-do integration.""" + from __future__ import annotations from pathlib import Path diff --git a/homeassistant/components/local_todo/config_flow.py b/homeassistant/components/local_todo/config_flow.py index 73328358a3c..a79a62c647b 100644 --- a/homeassistant/components/local_todo/config_flow.py +++ b/homeassistant/components/local_todo/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Local To-do integration.""" + from __future__ import annotations import logging @@ -6,8 +7,7 @@ from typing import Any import voluptuous as vol -from homeassistant import config_entries -from homeassistant.data_entry_flow import FlowResult +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.util import slugify from .const import CONF_STORAGE_KEY, CONF_TODO_LIST_NAME, DOMAIN @@ -21,14 +21,14 @@ STEP_USER_DATA_SCHEMA = vol.Schema( ) -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class LocalTodoConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Local To-do.""" VERSION = 1 async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors: dict[str, str] = {} if user_input is not None: diff --git a/homeassistant/components/locative/__init__.py b/homeassistant/components/locative/__init__.py index cca322f3baa..c27b6b4153b 100644 --- a/homeassistant/components/locative/__init__.py +++ b/homeassistant/components/locative/__init__.py @@ -1,4 +1,5 @@ """Support for Locative.""" + from __future__ import annotations from http import HTTPStatus diff --git a/homeassistant/components/locative/config_flow.py b/homeassistant/components/locative/config_flow.py index a1ac8263416..2aaa2e65f1c 100644 --- a/homeassistant/components/locative/config_flow.py +++ b/homeassistant/components/locative/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Locative.""" + from homeassistant.helpers import config_entry_flow from .const import DOMAIN diff --git a/homeassistant/components/locative/device_tracker.py b/homeassistant/components/locative/device_tracker.py index 2ec1e7437de..0b5cb32c22b 100644 --- a/homeassistant/components/locative/device_tracker.py +++ b/homeassistant/components/locative/device_tracker.py @@ -1,4 +1,5 @@ """Support for the Locative platform.""" + from homeassistant.components.device_tracker import SourceType, TrackerEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback @@ -23,9 +24,9 @@ async def async_setup_entry( async_add_entities([LocativeEntity(device, location, location_name)]) - hass.data[LT_DOMAIN]["unsub_device_tracker"][ - entry.entry_id - ] = async_dispatcher_connect(hass, TRACKER_UPDATE, _receive_data) + hass.data[LT_DOMAIN]["unsub_device_tracker"][entry.entry_id] = ( + async_dispatcher_connect(hass, TRACKER_UPDATE, _receive_data) + ) class LocativeEntity(TrackerEntity): diff --git a/homeassistant/components/lock/__init__.py b/homeassistant/components/lock/__init__.py index a4e7c4b7d1a..b2cd28324cb 100644 --- a/homeassistant/components/lock/__init__.py +++ b/homeassistant/components/lock/__init__.py @@ -1,4 +1,5 @@ """Component to interface with locks that can be controlled remotely.""" + from __future__ import annotations from datetime import timedelta @@ -41,6 +42,8 @@ from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.typing import ConfigType, StateType +from . import group as group_pre_import # noqa: F401 + if TYPE_CHECKING: from functools import cached_property else: @@ -153,7 +156,6 @@ class LockEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): if TYPE_CHECKING: assert self.code_format raise ServiceValidationError( - f"The code for {self.entity_id} doesn't match pattern {self.code_format}", translation_domain=DOMAIN, translation_key="add_default_code", translation_placeholders={ @@ -216,7 +218,7 @@ class LockEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): def lock(self, **kwargs: Any) -> None: """Lock the lock.""" - raise NotImplementedError() + raise NotImplementedError async def async_lock(self, **kwargs: Any) -> None: """Lock the lock.""" @@ -229,7 +231,7 @@ class LockEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): def unlock(self, **kwargs: Any) -> None: """Unlock the lock.""" - raise NotImplementedError() + raise NotImplementedError async def async_unlock(self, **kwargs: Any) -> None: """Unlock the lock.""" @@ -242,7 +244,7 @@ class LockEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): def open(self, **kwargs: Any) -> None: """Open the door latch.""" - raise NotImplementedError() + raise NotImplementedError async def async_open(self, **kwargs: Any) -> None: """Open the door latch.""" diff --git a/homeassistant/components/lock/device_action.py b/homeassistant/components/lock/device_action.py index fba95a932de..a75966414f8 100644 --- a/homeassistant/components/lock/device_action.py +++ b/homeassistant/components/lock/device_action.py @@ -1,4 +1,5 @@ """Provides device automations for Lock.""" + from __future__ import annotations import voluptuous as vol diff --git a/homeassistant/components/lock/device_condition.py b/homeassistant/components/lock/device_condition.py index 5ba93554aec..327bde2c0e3 100644 --- a/homeassistant/components/lock/device_condition.py +++ b/homeassistant/components/lock/device_condition.py @@ -1,4 +1,5 @@ """Provides device automations for Lock.""" + from __future__ import annotations import voluptuous as vol diff --git a/homeassistant/components/lock/device_trigger.py b/homeassistant/components/lock/device_trigger.py index c6b86eaca4a..57a83c7dc7a 100644 --- a/homeassistant/components/lock/device_trigger.py +++ b/homeassistant/components/lock/device_trigger.py @@ -1,4 +1,5 @@ """Provides device automations for Lock.""" + from __future__ import annotations import voluptuous as vol diff --git a/homeassistant/components/lock/group.py b/homeassistant/components/lock/group.py index 9cf460dc019..99109e852f6 100644 --- a/homeassistant/components/lock/group.py +++ b/homeassistant/components/lock/group.py @@ -1,14 +1,17 @@ """Describe group states.""" +from typing import TYPE_CHECKING -from homeassistant.components.group import GroupIntegrationRegistry from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED from homeassistant.core import HomeAssistant, callback +if TYPE_CHECKING: + from homeassistant.components.group import GroupIntegrationRegistry + @callback def async_describe_on_off_states( - hass: HomeAssistant, registry: GroupIntegrationRegistry + hass: HomeAssistant, registry: "GroupIntegrationRegistry" ) -> None: """Describe group on off states.""" registry.on_off_states({STATE_UNLOCKED}, STATE_LOCKED) diff --git a/homeassistant/components/lock/reproduce_state.py b/homeassistant/components/lock/reproduce_state.py index b6eefcfac63..36afcf5f310 100644 --- a/homeassistant/components/lock/reproduce_state.py +++ b/homeassistant/components/lock/reproduce_state.py @@ -1,4 +1,5 @@ """Reproduce an Lock state.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/lock/significant_change.py b/homeassistant/components/lock/significant_change.py index 172bf2559c5..138f2393257 100644 --- a/homeassistant/components/lock/significant_change.py +++ b/homeassistant/components/lock/significant_change.py @@ -1,4 +1,5 @@ """Helper to test significant Lock state changes.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/logbook/__init__.py b/homeassistant/components/logbook/__init__.py index 891d1fb3fb0..f19e64aa6f0 100644 --- a/homeassistant/components/logbook/__init__.py +++ b/homeassistant/components/logbook/__init__.py @@ -1,4 +1,5 @@ """Event parser and human readable log generator.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/logbook/const.py b/homeassistant/components/logbook/const.py index 2d9911117f9..282580bdc95 100644 --- a/homeassistant/components/logbook/const.py +++ b/homeassistant/components/logbook/const.py @@ -1,15 +1,19 @@ """Event parser and human readable log generator.""" + from __future__ import annotations from homeassistant.components.automation import EVENT_AUTOMATION_TRIGGERED -from homeassistant.components.counter import DOMAIN as COUNTER_DOMAIN -from homeassistant.components.proximity import DOMAIN as PROXIMITY_DOMAIN from homeassistant.components.script import EVENT_SCRIPT_STARTED from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import EVENT_CALL_SERVICE, EVENT_LOGBOOK_ENTRY +# # Domains that are always continuous -ALWAYS_CONTINUOUS_DOMAINS = {COUNTER_DOMAIN, PROXIMITY_DOMAIN} +# +# These are hard coded here to avoid importing +# the entire counter and proximity integrations +# to get the name of the domain. +ALWAYS_CONTINUOUS_DOMAINS = {"counter", "proximity"} # Domains that are continuous if there is a UOM set on the entity CONDITIONALLY_CONTINUOUS_DOMAINS = {SENSOR_DOMAIN} diff --git a/homeassistant/components/logbook/helpers.py b/homeassistant/components/logbook/helpers.py index b7293087e7e..1731fcaddd9 100644 --- a/homeassistant/components/logbook/helpers.py +++ b/homeassistant/components/logbook/helpers.py @@ -1,4 +1,5 @@ """Event parser and human readable log generator.""" + from __future__ import annotations from collections.abc import Callable, Mapping @@ -27,7 +28,6 @@ from homeassistant.helpers.event import ( EventStateChangedData, async_track_state_change_event, ) -from homeassistant.helpers.typing import EventType from .const import ALWAYS_CONTINUOUS_DOMAINS, AUTOMATION_EVENTS, BUILT_IN_EVENTS, DOMAIN from .models import LogbookConfig @@ -161,7 +161,7 @@ def event_forwarder_filtered( def async_subscribe_events( hass: HomeAssistant, subscriptions: list[CALLBACK_TYPE], - target: Callable[[Event], None], + target: Callable[[Event[Any]], None], event_types: tuple[str, ...], entities_filter: Callable[[str], bool] | None, entity_ids: list[str] | None, @@ -176,10 +176,10 @@ def async_subscribe_events( event_forwarder = event_forwarder_filtered( target, entities_filter, entity_ids, device_ids ) - for event_type in event_types: - subscriptions.append( - hass.bus.async_listen(event_type, event_forwarder, run_immediately=True) - ) + subscriptions.extend( + hass.bus.async_listen(event_type, event_forwarder, run_immediately=True) + for event_type in event_types + ) if device_ids and not entity_ids: # No entities to subscribe to but we are filtering @@ -188,7 +188,7 @@ def async_subscribe_events( return @callback - def _forward_state_events_filtered(event: EventType[EventStateChangedData]) -> None: + def _forward_state_events_filtered(event: Event[EventStateChangedData]) -> None: if (old_state := event.data["old_state"]) is None or ( new_state := event.data["new_state"] ) is None: @@ -211,7 +211,7 @@ def async_subscribe_events( subscriptions.append( hass.bus.async_listen( EVENT_STATE_CHANGED, - _forward_state_events_filtered, # type: ignore[arg-type] + _forward_state_events_filtered, run_immediately=True, ) ) diff --git a/homeassistant/components/logbook/icons.json b/homeassistant/components/logbook/icons.json new file mode 100644 index 00000000000..cd2cde8600c --- /dev/null +++ b/homeassistant/components/logbook/icons.json @@ -0,0 +1,5 @@ +{ + "services": { + "log": "mdi:file-document" + } +} diff --git a/homeassistant/components/logbook/models.py b/homeassistant/components/logbook/models.py index 22420f243c6..1073c6b0d3a 100644 --- a/homeassistant/components/logbook/models.py +++ b/homeassistant/components/logbook/models.py @@ -1,4 +1,5 @@ """Event parser and human readable log generator.""" + from __future__ import annotations from collections.abc import Callable, Mapping @@ -50,7 +51,7 @@ class LazyEventPartialState: self._event_data_cache = event_data_cache # We need to explicitly check for the row is EventAsRow as the unhappy path # to fetch row.data for Row is very expensive - if type(row) is EventAsRow: # noqa: E721 + if type(row) is EventAsRow: # If its an EventAsRow we can avoid the whole # json decode process as we already have the data self.data = row.data diff --git a/homeassistant/components/logbook/processor.py b/homeassistant/components/logbook/processor.py index 02a6dae3ce6..2180a63b74f 100644 --- a/homeassistant/components/logbook/processor.py +++ b/homeassistant/components/logbook/processor.py @@ -1,4 +1,5 @@ """Event parser and human readable log generator.""" + from __future__ import annotations from collections.abc import Callable, Generator, Sequence @@ -427,7 +428,7 @@ class EventCache: def get(self, row: EventAsRow | Row) -> LazyEventPartialState: """Get the event from the row.""" - if type(row) is EventAsRow: # noqa: E721 - this is never subclassed + if type(row) is EventAsRow: # - this is never subclassed return LazyEventPartialState(row, self._event_data_cache) if event := self.event_cache.get(row): return event diff --git a/homeassistant/components/logbook/queries/__init__.py b/homeassistant/components/logbook/queries/__init__.py index af41374ec9b..c27da37742b 100644 --- a/homeassistant/components/logbook/queries/__init__.py +++ b/homeassistant/components/logbook/queries/__init__.py @@ -1,4 +1,5 @@ """Queries for logbook.""" + from __future__ import annotations from collections.abc import Collection diff --git a/homeassistant/components/logbook/queries/all.py b/homeassistant/components/logbook/queries/all.py index 21f88135a1d..cd596414583 100644 --- a/homeassistant/components/logbook/queries/all.py +++ b/homeassistant/components/logbook/queries/all.py @@ -1,4 +1,5 @@ """All queries for logbook.""" + from __future__ import annotations from sqlalchemy import lambda_stmt diff --git a/homeassistant/components/logbook/queries/common.py b/homeassistant/components/logbook/queries/common.py index cbbe8724ece..8f9ab8a80cd 100644 --- a/homeassistant/components/logbook/queries/common.py +++ b/homeassistant/components/logbook/queries/common.py @@ -1,4 +1,5 @@ """Queries for logbook.""" + from __future__ import annotations from typing import Final diff --git a/homeassistant/components/logbook/queries/devices.py b/homeassistant/components/logbook/queries/devices.py index 75604de6104..f4b1c06c40c 100644 --- a/homeassistant/components/logbook/queries/devices.py +++ b/homeassistant/components/logbook/queries/devices.py @@ -1,4 +1,5 @@ """Devices queries for logbook.""" + from __future__ import annotations from collections.abc import Iterable diff --git a/homeassistant/components/logbook/queries/entities.py b/homeassistant/components/logbook/queries/entities.py index 95c1d565263..494c2965215 100644 --- a/homeassistant/components/logbook/queries/entities.py +++ b/homeassistant/components/logbook/queries/entities.py @@ -1,4 +1,5 @@ """Entities queries for logbook.""" + from __future__ import annotations from collections.abc import Collection, Iterable diff --git a/homeassistant/components/logbook/queries/entities_and_devices.py b/homeassistant/components/logbook/queries/entities_and_devices.py index c465a343d61..383bb71e223 100644 --- a/homeassistant/components/logbook/queries/entities_and_devices.py +++ b/homeassistant/components/logbook/queries/entities_and_devices.py @@ -1,4 +1,5 @@ """Entities and Devices queries for logbook.""" + from __future__ import annotations from collections.abc import Collection, Iterable diff --git a/homeassistant/components/logbook/rest_api.py b/homeassistant/components/logbook/rest_api.py index 57d0a6695c7..5f1918ebccf 100644 --- a/homeassistant/components/logbook/rest_api.py +++ b/homeassistant/components/logbook/rest_api.py @@ -1,4 +1,5 @@ """Event parser and human readable log generator.""" + from __future__ import annotations from collections.abc import Callable @@ -9,7 +10,7 @@ from typing import Any, cast from aiohttp import web import voluptuous as vol -from homeassistant.components.http import HomeAssistantView +from homeassistant.components.http import KEY_HASS, HomeAssistantView from homeassistant.components.recorder import get_instance from homeassistant.components.recorder.filters import Filters from homeassistant.core import HomeAssistant, callback @@ -86,7 +87,7 @@ class LogbookView(HomeAssistantView): return self.json_message("Invalid end_time", HTTPStatus.BAD_REQUEST) end_day = end_day_dt - hass = request.app["hass"] + hass = request.app[KEY_HASS] context_id = request.query.get("context_id") diff --git a/homeassistant/components/logbook/websocket_api.py b/homeassistant/components/logbook/websocket_api.py index 0b1b34ca375..cac58971cde 100644 --- a/homeassistant/components/logbook/websocket_api.py +++ b/homeassistant/components/logbook/websocket_api.py @@ -1,4 +1,5 @@ """Event parser and human readable log generator.""" + from __future__ import annotations import asyncio @@ -17,6 +18,7 @@ from homeassistant.components.websocket_api.connection import ActiveConnection from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.json import json_bytes +from homeassistant.util.async_ import create_eager_task import homeassistant.util.dt as dt_util from .const import DOMAIN @@ -220,11 +222,14 @@ async def _async_events_consumer( event_processor: EventProcessor, ) -> None: """Stream events from the queue.""" + subscriptions_setup_complete_timestamp = ( + subscriptions_setup_complete_time.timestamp() + ) while True: events: list[Event] = [await stream_queue.get()] # If the event is older than the last db # event we already sent it so we skip it. - if events[0].time_fired <= subscriptions_setup_complete_time: + if events[0].time_fired_timestamp <= subscriptions_setup_complete_timestamp: continue # We sleep for the EVENT_COALESCE_TIME so # we can group events together to minimize @@ -394,7 +399,7 @@ async def ws_event_stream( # Unsubscribe happened while sending historical events return - live_stream.task = asyncio.create_task( + live_stream.task = create_eager_task( _async_events_consumer( subscriptions_setup_complete_time, connection, @@ -404,7 +409,7 @@ async def ws_event_stream( ) ) - live_stream.wait_sync_task = asyncio.create_task( + live_stream.wait_sync_task = create_eager_task( get_instance(hass).async_block_till_done() ) await live_stream.wait_sync_task @@ -421,7 +426,10 @@ async def ws_event_stream( hass, connection, msg_id, - last_event_time or start_time, + # Add one microsecond so we are outside the window of + # the last event we got from the database since otherwise + # we could fetch the same event twice + (last_event_time or start_time) + timedelta(microseconds=1), subscriptions_setup_complete_time, messages.event_message, event_processor, diff --git a/homeassistant/components/logentries/__init__.py b/homeassistant/components/logentries/__init__.py index fc7a30de762..6357510a07e 100644 --- a/homeassistant/components/logentries/__init__.py +++ b/homeassistant/components/logentries/__init__.py @@ -1,4 +1,5 @@ """Support for sending data to Logentries webhook endpoint.""" + import json import logging diff --git a/homeassistant/components/logger/__init__.py b/homeassistant/components/logger/__init__.py index 32b864047a6..be6e8c1b24e 100644 --- a/homeassistant/components/logger/__init__.py +++ b/homeassistant/components/logger/__init__.py @@ -1,4 +1,5 @@ """Support for setting the level of logging for components.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/logger/const.py b/homeassistant/components/logger/const.py index 4a7edfacead..bc026d196b7 100644 --- a/homeassistant/components/logger/const.py +++ b/homeassistant/components/logger/const.py @@ -1,4 +1,5 @@ """Constants for the Logger integration.""" + import logging DOMAIN = "logger" @@ -20,7 +21,7 @@ LOGSEVERITY = { LOGSEVERITY_FATAL: logging.FATAL, LOGSEVERITY_ERROR: logging.ERROR, LOGSEVERITY_WARNING: logging.WARNING, - LOGSEVERITY_WARN: logging.WARN, + LOGSEVERITY_WARN: logging.WARNING, LOGSEVERITY_INFO: logging.INFO, LOGSEVERITY_DEBUG: logging.DEBUG, LOGSEVERITY_NOTSET: logging.NOTSET, diff --git a/homeassistant/components/logger/helpers.py b/homeassistant/components/logger/helpers.py index bf37ab3625b..a527a081fca 100644 --- a/homeassistant/components/logger/helpers.py +++ b/homeassistant/components/logger/helpers.py @@ -1,4 +1,5 @@ """Helpers for the logger integration.""" + from __future__ import annotations from collections import defaultdict diff --git a/homeassistant/components/logger/icons.json b/homeassistant/components/logger/icons.json new file mode 100644 index 00000000000..305dd3ece91 --- /dev/null +++ b/homeassistant/components/logger/icons.json @@ -0,0 +1,6 @@ +{ + "services": { + "set_default_level": "mdi:cog-outline", + "set_level": "mdi:cog-outline" + } +} diff --git a/homeassistant/components/logger/websocket_api.py b/homeassistant/components/logger/websocket_api.py index 240db3144af..fafc2d3eedb 100644 --- a/homeassistant/components/logger/websocket_api.py +++ b/homeassistant/components/logger/websocket_api.py @@ -1,4 +1,5 @@ """Websocket API handlers for the logger integration.""" + from typing import Any import voluptuous as vol diff --git a/homeassistant/components/logi_circle/__init__.py b/homeassistant/components/logi_circle/__init__.py index fa358d05fcd..0713bcc438e 100644 --- a/homeassistant/components/logi_circle/__init__.py +++ b/homeassistant/components/logi_circle/__init__.py @@ -1,4 +1,5 @@ """Support for Logi Circle devices.""" + import asyncio from aiohttp.client_exceptions import ClientResponseError diff --git a/homeassistant/components/logi_circle/camera.py b/homeassistant/components/logi_circle/camera.py index d1ea01e864c..ad31713d734 100644 --- a/homeassistant/components/logi_circle/camera.py +++ b/homeassistant/components/logi_circle/camera.py @@ -1,4 +1,5 @@ """Support to the Logi Circle cameras.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/logi_circle/config_flow.py b/homeassistant/components/logi_circle/config_flow.py index be22a9a5d30..6c1a549aa04 100644 --- a/homeassistant/components/logi_circle/config_flow.py +++ b/homeassistant/components/logi_circle/config_flow.py @@ -1,4 +1,5 @@ """Config flow to configure Logi Circle component.""" + import asyncio from collections import OrderedDict from http import HTTPStatus @@ -7,8 +8,8 @@ from logi_circle import LogiCircle from logi_circle.exception import AuthorizationFailed import voluptuous as vol -from homeassistant import config_entries -from homeassistant.components.http import HomeAssistantView +from homeassistant.components.http import KEY_HASS, HomeAssistantView +from homeassistant.config_entries import ConfigFlow from homeassistant.const import ( CONF_API_KEY, CONF_CLIENT_ID, @@ -53,7 +54,7 @@ def register_flow_implementation( } -class LogiCircleFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class LogiCircleFlowHandler(ConfigFlow, domain=DOMAIN): """Config flow for Logi Circle component.""" VERSION = 1 @@ -191,7 +192,7 @@ class LogiCircleAuthCallbackView(HomeAssistantView): async def get(self, request): """Receive authorization code.""" - hass = request.app["hass"] + hass = request.app[KEY_HASS] if "code" in request.query: hass.async_create_task( hass.config_entries.flow.async_init( diff --git a/homeassistant/components/logi_circle/const.py b/homeassistant/components/logi_circle/const.py index 3e74611f767..e144f47ce4e 100644 --- a/homeassistant/components/logi_circle/const.py +++ b/homeassistant/components/logi_circle/const.py @@ -1,4 +1,5 @@ """Constants in Logi Circle component.""" + from __future__ import annotations DOMAIN = "logi_circle" diff --git a/homeassistant/components/logi_circle/icons.json b/homeassistant/components/logi_circle/icons.json new file mode 100644 index 00000000000..9289746d375 --- /dev/null +++ b/homeassistant/components/logi_circle/icons.json @@ -0,0 +1,7 @@ +{ + "services": { + "set_config": "mdi:cog", + "livestream_snapshot": "mdi:camera", + "livestream_record": "mdi:record-rec" + } +} diff --git a/homeassistant/components/logi_circle/sensor.py b/homeassistant/components/logi_circle/sensor.py index d06569a19ca..121cb8848ae 100644 --- a/homeassistant/components/logi_circle/sensor.py +++ b/homeassistant/components/logi_circle/sensor.py @@ -1,4 +1,5 @@ """Support for Logi Circle sensors.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/london_air/sensor.py b/homeassistant/components/london_air/sensor.py index 98cc4c4b4e8..0895e507a85 100644 --- a/homeassistant/components/london_air/sensor.py +++ b/homeassistant/components/london_air/sensor.py @@ -1,4 +1,5 @@ """Sensor for checking the status of London air.""" + from __future__ import annotations from datetime import timedelta @@ -71,11 +72,8 @@ def setup_platform( """Set up the London Air sensor.""" data = APIData() data.update() - sensors = [] - for name in config[CONF_LOCATIONS]: - sensors.append(AirSensor(name, data)) - add_entities(sensors, True) + add_entities((AirSensor(name, data) for name in config[CONF_LOCATIONS]), True) class APIData: @@ -140,14 +138,16 @@ class AirSensor(SensorEntity): def update(self) -> None: """Update the sensor.""" - sites_status = [] + sites_status: list = [] self._api_data.update() if self._api_data.data: self._site_data = self._api_data.data[self._name] self._updated = self._site_data[0]["updated"] - for site in self._site_data: - if site["pollutants_status"] != "no_species_data": - sites_status.append(site["pollutants_status"]) + sites_status.extend( + site["pollutants_status"] + for site in self._site_data + if site["pollutants_status"] != "no_species_data" + ) if sites_status: self._state = max(set(sites_status), key=sites_status.count) @@ -166,9 +166,9 @@ def parse_species(species_data): species_dict["code"] = species["@SpeciesCode"] species_dict["quality"] = species["@AirQualityBand"] species_dict["index"] = species["@AirQualityIndex"] - species_dict[ - "summary" - ] = f"{species_dict['code']} is {species_dict['quality']}" + species_dict["summary"] = ( + f"{species_dict['code']} is {species_dict['quality']}" + ) parsed_species_data.append(species_dict) quality_list.append(species_dict["quality"]) return parsed_species_data, quality_list diff --git a/homeassistant/components/london_underground/const.py b/homeassistant/components/london_underground/const.py index 4928d3bb164..532f4333ba9 100644 --- a/homeassistant/components/london_underground/const.py +++ b/homeassistant/components/london_underground/const.py @@ -1,4 +1,5 @@ """Constants for the London underground integration.""" + from datetime import timedelta DOMAIN = "london_underground" diff --git a/homeassistant/components/london_underground/coordinator.py b/homeassistant/components/london_underground/coordinator.py index 2d3fd6b970f..cf14ad14b43 100644 --- a/homeassistant/components/london_underground/coordinator.py +++ b/homeassistant/components/london_underground/coordinator.py @@ -1,4 +1,5 @@ """DataUpdateCoordinator for London underground integration.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/london_underground/sensor.py b/homeassistant/components/london_underground/sensor.py index 3f5ec42521e..e5735aa7fba 100644 --- a/homeassistant/components/london_underground/sensor.py +++ b/homeassistant/components/london_underground/sensor.py @@ -1,4 +1,5 @@ """Sensor for checking the status of London Underground tube lines.""" + from __future__ import annotations import logging @@ -44,11 +45,9 @@ async def async_setup_platform( if not coordinator.last_update_success: raise PlatformNotReady - sensors = [] - for line in config[CONF_LINE]: - sensors.append(LondonTubeSensor(coordinator, line)) - - async_add_entities(sensors) + async_add_entities( + LondonTubeSensor(coordinator, line) for line in config[CONF_LINE] + ) class LondonTubeSensor(CoordinatorEntity[LondonTubeCoordinator], SensorEntity): diff --git a/homeassistant/components/lookin/__init__.py b/homeassistant/components/lookin/__init__.py index 358ccc5ae37..a0e529bc189 100644 --- a/homeassistant/components/lookin/__init__.py +++ b/homeassistant/components/lookin/__init__.py @@ -1,4 +1,5 @@ """The lookin integration.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/lookin/climate.py b/homeassistant/components/lookin/climate.py index 1bee2d14295..fadeb6d16fa 100644 --- a/homeassistant/components/lookin/climate.py +++ b/homeassistant/components/lookin/climate.py @@ -1,4 +1,5 @@ """The lookin integration climate platform.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/lookin/config_flow.py b/homeassistant/components/lookin/config_flow.py index 895d071ab4e..61dfd9a2c20 100644 --- a/homeassistant/components/lookin/config_flow.py +++ b/homeassistant/components/lookin/config_flow.py @@ -1,4 +1,5 @@ """The lookin integration config_flow.""" + from __future__ import annotations import logging @@ -8,10 +9,9 @@ import aiohttp from aiolookin import Device, LookInHttpProtocol, NoUsableService import voluptuous as vol -from homeassistant import config_entries from homeassistant.components import zeroconf +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN @@ -19,7 +19,7 @@ from .const import DOMAIN LOGGER = logging.getLogger(__name__) -class LookinFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class LookinFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a config flow for lookin.""" def __init__(self) -> None: @@ -29,7 +29,7 @@ class LookinFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_zeroconf( self, discovery_info: zeroconf.ZeroconfServiceInfo - ) -> FlowResult: + ) -> ConfigFlowResult: """Start a discovery flow from zeroconf.""" uid: str = discovery_info.hostname.removesuffix(".local.") host: str = discovery_info.host @@ -52,7 +52,7 @@ class LookinFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """User initiated discover flow.""" errors: dict[str, str] = {} @@ -88,7 +88,7 @@ class LookinFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_discovery_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm the discover flow.""" assert self._host is not None if user_input is None: diff --git a/homeassistant/components/lookin/const.py b/homeassistant/components/lookin/const.py index 8eb96dcefd8..d4624932ad9 100644 --- a/homeassistant/components/lookin/const.py +++ b/homeassistant/components/lookin/const.py @@ -1,4 +1,5 @@ """The lookin integration constants.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/lookin/coordinator.py b/homeassistant/components/lookin/coordinator.py index d556899a914..925a7416731 100644 --- a/homeassistant/components/lookin/coordinator.py +++ b/homeassistant/components/lookin/coordinator.py @@ -1,4 +1,5 @@ """Coordinator for lookin devices.""" + from __future__ import annotations from collections.abc import Awaitable, Callable diff --git a/homeassistant/components/lookin/entity.py b/homeassistant/components/lookin/entity.py index 0e518ffc1e5..fd36301ddb6 100644 --- a/homeassistant/components/lookin/entity.py +++ b/homeassistant/components/lookin/entity.py @@ -1,4 +1,5 @@ """The lookin integration entity.""" + from __future__ import annotations from abc import abstractmethod diff --git a/homeassistant/components/lookin/light.py b/homeassistant/components/lookin/light.py index c4b263cbad1..804d0ebef01 100644 --- a/homeassistant/components/lookin/light.py +++ b/homeassistant/components/lookin/light.py @@ -1,4 +1,5 @@ """The lookin integration light platform.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/lookin/media_player.py b/homeassistant/components/lookin/media_player.py index f7ae457cbff..b3dda9c9e0c 100644 --- a/homeassistant/components/lookin/media_player.py +++ b/homeassistant/components/lookin/media_player.py @@ -1,4 +1,5 @@ """The lookin integration light platform.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/lookin/models.py b/homeassistant/components/lookin/models.py index 2de3a7ee761..3bf6ae9d862 100644 --- a/homeassistant/components/lookin/models.py +++ b/homeassistant/components/lookin/models.py @@ -1,4 +1,5 @@ """The lookin integration models.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/homeassistant/components/lookin/sensor.py b/homeassistant/components/lookin/sensor.py index 822c28fda30..cae4f7782a8 100644 --- a/homeassistant/components/lookin/sensor.py +++ b/homeassistant/components/lookin/sensor.py @@ -1,4 +1,5 @@ """The lookin integration sensor platform.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/loqed/__init__.py b/homeassistant/components/loqed/__init__.py index e22987ba426..b6408880c96 100644 --- a/homeassistant/components/loqed/__init__.py +++ b/homeassistant/components/loqed/__init__.py @@ -1,4 +1,5 @@ """The loqed integration.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/loqed/config_flow.py b/homeassistant/components/loqed/config_flow.py index 1c76f480529..8c82a7a6964 100644 --- a/homeassistant/components/loqed/config_flow.py +++ b/homeassistant/components/loqed/config_flow.py @@ -1,4 +1,5 @@ """Config flow for loqed integration.""" + from __future__ import annotations import logging @@ -9,12 +10,11 @@ import aiohttp from loqedAPI import cloud_loqed, loqed import voluptuous as vol -from homeassistant import config_entries from homeassistant.components import webhook from homeassistant.components.zeroconf import ZeroconfServiceInfo +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_API_TOKEN, CONF_NAME, CONF_WEBHOOK_ID from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -23,7 +23,7 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class LoqedConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Loqed.""" VERSION = 1 @@ -83,7 +83,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_zeroconf( self, discovery_info: ZeroconfServiceInfo - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle zeroconf discovery.""" host = discovery_info.host self._host = host @@ -101,7 +101,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Show userform to user.""" user_data_schema = ( vol.Schema( diff --git a/homeassistant/components/loqed/const.py b/homeassistant/components/loqed/const.py index 59011f26566..262009e20ae 100644 --- a/homeassistant/components/loqed/const.py +++ b/homeassistant/components/loqed/const.py @@ -1,5 +1,4 @@ """Constants for the loqed integration.""" - DOMAIN = "loqed" CONF_CLOUDHOOK_URL = "cloudhook_url" diff --git a/homeassistant/components/loqed/coordinator.py b/homeassistant/components/loqed/coordinator.py index d33cd8772b2..1447934103e 100644 --- a/homeassistant/components/loqed/coordinator.py +++ b/homeassistant/components/loqed/coordinator.py @@ -1,4 +1,5 @@ """Provides the coordinator for a LOQED lock.""" + import asyncio import logging from typing import TypedDict diff --git a/homeassistant/components/loqed/entity.py b/homeassistant/components/loqed/entity.py index aec50ec8f92..9a443e23924 100644 --- a/homeassistant/components/loqed/entity.py +++ b/homeassistant/components/loqed/entity.py @@ -1,4 +1,5 @@ """Base entity for the LOQED integration.""" + from __future__ import annotations from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo diff --git a/homeassistant/components/loqed/lock.py b/homeassistant/components/loqed/lock.py index d34df19e2d1..be6b39176d6 100644 --- a/homeassistant/components/loqed/lock.py +++ b/homeassistant/components/loqed/lock.py @@ -1,4 +1,5 @@ """LOQED lock integration for Home Assistant.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/loqed/sensor.py b/homeassistant/components/loqed/sensor.py index ee4fa7ecd74..1d4595db8e9 100644 --- a/homeassistant/components/loqed/sensor.py +++ b/homeassistant/components/loqed/sensor.py @@ -1,4 +1,5 @@ """Creates LOQED sensors.""" + from typing import Final from homeassistant.components.sensor import ( diff --git a/homeassistant/components/lovelace/__init__.py b/homeassistant/components/lovelace/__init__.py index daa44bf60be..60d03717be0 100644 --- a/homeassistant/components/lovelace/__init__.py +++ b/homeassistant/components/lovelace/__init__.py @@ -1,9 +1,10 @@ """Support for the Lovelace UI.""" + import logging import voluptuous as vol -from homeassistant.components import frontend, websocket_api +from homeassistant.components import frontend, onboarding, websocket_api from homeassistant.config import ( async_hass_config_yaml, async_process_component_and_handle_errors, @@ -13,11 +14,13 @@ from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import collection, config_validation as cv from homeassistant.helpers.service import async_register_admin_service +from homeassistant.helpers.translation import async_get_translations from homeassistant.helpers.typing import ConfigType from homeassistant.loader import async_get_integration from . import dashboard, resources, websocket from .const import ( # noqa: F401 + CONF_ALLOW_SINGLE_WORD, CONF_ICON, CONF_REQUIRE_ADMIN, CONF_SHOW_IN_SIDEBAR, @@ -200,6 +203,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # Process storage dashboards dashboards_collection = dashboard.DashboardsCollection(hass) + # This can be removed when the map integration is removed + hass.data[DOMAIN]["dashboards_collection"] = dashboards_collection + dashboards_collection.async_add_listener(storage_dashboard_changed) await dashboards_collection.async_load() @@ -211,6 +217,12 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: STORAGE_DASHBOARD_UPDATE_FIELDS, ).async_setup(hass, create_list=False) + def create_map_dashboard(): + hass.async_create_task(_create_map_dashboard(hass)) + + if not onboarding.async_is_onboarded(hass): + onboarding.async_add_listener(hass, create_map_dashboard) + return True @@ -248,3 +260,25 @@ def _register_panel(hass, url_path, mode, config, update): kwargs["sidebar_icon"] = config.get(CONF_ICON, DEFAULT_ICON) frontend.async_register_built_in_panel(hass, DOMAIN, **kwargs) + + +async def _create_map_dashboard(hass: HomeAssistant): + translations = await async_get_translations( + hass, hass.config.language, "dashboard", {onboarding.DOMAIN} + ) + title = translations["component.onboarding.dashboard.map.title"] + + dashboards_collection: dashboard.DashboardsCollection = hass.data[DOMAIN][ + "dashboards_collection" + ] + await dashboards_collection.async_create_item( + { + CONF_ALLOW_SINGLE_WORD: True, + CONF_ICON: "mdi:map", + CONF_TITLE: title, + CONF_URL_PATH: "map", + } + ) + + map_store: dashboard.LovelaceStorage = hass.data[DOMAIN]["dashboards"]["map"] + await map_store.async_save({"strategy": {"type": "map"}}) diff --git a/homeassistant/components/lovelace/const.py b/homeassistant/components/lovelace/const.py index 01110bb8a7c..538bd49d72c 100644 --- a/homeassistant/components/lovelace/const.py +++ b/homeassistant/components/lovelace/const.py @@ -1,4 +1,5 @@ """Constants for Lovelace.""" + from typing import Any import voluptuous as vol @@ -23,6 +24,7 @@ MODE_STORAGE = "storage" MODE_AUTO = "auto-gen" LOVELACE_CONFIG_FILE = "ui-lovelace.yaml" +CONF_ALLOW_SINGLE_WORD = "allow_single_word" CONF_URL_PATH = "url_path" CONF_RESOURCE_TYPE_WS = "res_type" @@ -74,6 +76,8 @@ STORAGE_DASHBOARD_CREATE_FIELDS = { # For now we write "storage" as all modes. # In future we can adjust this to be other modes. vol.Optional(CONF_MODE, default=MODE_STORAGE): MODE_STORAGE, + # Set to allow adding dashboard without hyphen + vol.Optional(CONF_ALLOW_SINGLE_WORD): bool, } STORAGE_DASHBOARD_UPDATE_FIELDS = DASHBOARD_BASE_UPDATE_FIELDS diff --git a/homeassistant/components/lovelace/dashboard.py b/homeassistant/components/lovelace/dashboard.py index d935ad9bff5..17116a011a4 100644 --- a/homeassistant/components/lovelace/dashboard.py +++ b/homeassistant/components/lovelace/dashboard.py @@ -1,4 +1,5 @@ """Lovelace dashboard support.""" + from __future__ import annotations from abc import ABC, abstractmethod @@ -17,6 +18,7 @@ from homeassistant.helpers import collection, storage from homeassistant.util.yaml import Secrets, load_yaml_dict from .const import ( + CONF_ALLOW_SINGLE_WORD, CONF_ICON, CONF_URL_PATH, DOMAIN, @@ -231,29 +233,16 @@ class DashboardsCollection(collection.DictStorageCollection): storage.Store(hass, DASHBOARDS_STORAGE_VERSION, DASHBOARDS_STORAGE_KEY), ) - async def _async_load_data(self) -> collection.SerializedStorageCollection | None: - """Load the data.""" - if (data := await self.store.async_load()) is None: - return data - - updated = False - - for item in data["items"] or []: - if "-" not in item[CONF_URL_PATH]: - updated = True - item[CONF_URL_PATH] = f"lovelace-{item[CONF_URL_PATH]}" - - if updated: - await self.store.async_save(data) - - return data - async def _process_create_data(self, data: dict) -> dict: """Validate the config is valid.""" - if "-" not in data[CONF_URL_PATH]: + url_path = data[CONF_URL_PATH] + + allow_single_word = data.pop(CONF_ALLOW_SINGLE_WORD, False) + + if not allow_single_word and "-" not in url_path: raise vol.Invalid("Url path needs to contain a hyphen (-)") - if data[CONF_URL_PATH] in self.hass.data[DATA_PANELS]: + if url_path in self.hass.data[DATA_PANELS]: raise vol.Invalid("Panel url path needs to be unique") return self.CREATE_SCHEMA(data) diff --git a/homeassistant/components/lovelace/icons.json b/homeassistant/components/lovelace/icons.json new file mode 100644 index 00000000000..fe0a0e114ae --- /dev/null +++ b/homeassistant/components/lovelace/icons.json @@ -0,0 +1,5 @@ +{ + "services": { + "reload_resources": "mdi:reload" + } +} diff --git a/homeassistant/components/lovelace/manifest.json b/homeassistant/components/lovelace/manifest.json index 9dcffdb3b6c..ed55142ee77 100644 --- a/homeassistant/components/lovelace/manifest.json +++ b/homeassistant/components/lovelace/manifest.json @@ -2,6 +2,7 @@ "domain": "lovelace", "name": "Dashboards", "codeowners": ["@home-assistant/frontend"], + "dependencies": ["onboarding"], "documentation": "https://www.home-assistant.io/integrations/lovelace", "integration_type": "system", "quality_scale": "internal" diff --git a/homeassistant/components/lovelace/resources.py b/homeassistant/components/lovelace/resources.py index b6d0c939fec..2dbbbacabea 100644 --- a/homeassistant/components/lovelace/resources.py +++ b/homeassistant/components/lovelace/resources.py @@ -1,4 +1,5 @@ """Lovelace resources support.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/lovelace/system_health.py b/homeassistant/components/lovelace/system_health.py index 96ae2f47540..1e703768ae6 100644 --- a/homeassistant/components/lovelace/system_health.py +++ b/homeassistant/components/lovelace/system_health.py @@ -1,4 +1,5 @@ """Provide info to system health.""" + import asyncio from homeassistant.components import system_health diff --git a/homeassistant/components/lovelace/websocket.py b/homeassistant/components/lovelace/websocket.py index b756c2765e1..e4eaa42073f 100644 --- a/homeassistant/components/lovelace/websocket.py +++ b/homeassistant/components/lovelace/websocket.py @@ -1,4 +1,5 @@ """Websocket API for Lovelace.""" + from __future__ import annotations from functools import wraps diff --git a/homeassistant/components/luci/device_tracker.py b/homeassistant/components/luci/device_tracker.py index f4ebe4376f3..d62c1b07b5c 100644 --- a/homeassistant/components/luci/device_tracker.py +++ b/homeassistant/components/luci/device_tracker.py @@ -1,4 +1,5 @@ """Support for OpenWRT (luci) routers.""" + from __future__ import annotations import logging @@ -95,14 +96,11 @@ class LuciDeviceScanner(DeviceScanner): _LOGGER.debug("Luci get_all_connected_devices returned: %s", result) - last_results = [] - for device in result: - if ( - not hasattr(self.router.router.owrt_version, "release") - or not self.router.router.owrt_version.release - or self.router.router.owrt_version.release[0] < 19 - or device.reachable - ): - last_results.append(device) - - self.last_results = last_results + self.last_results = [ + device + for device in result + if not hasattr(self.router.router.owrt_version, "release") + or not self.router.router.owrt_version.release + or self.router.router.owrt_version.release[0] < 19 + or device.reachable + ] diff --git a/homeassistant/components/luftdaten/__init__.py b/homeassistant/components/luftdaten/__init__.py index d842fdc1a89..2ef7864566f 100644 --- a/homeassistant/components/luftdaten/__init__.py +++ b/homeassistant/components/luftdaten/__init__.py @@ -3,6 +3,7 @@ Sensor.Community was previously called Luftdaten, hence the domain differs from the integration name. """ + from __future__ import annotations import logging diff --git a/homeassistant/components/luftdaten/config_flow.py b/homeassistant/components/luftdaten/config_flow.py index 0fa800f1d8b..ba14afeb092 100644 --- a/homeassistant/components/luftdaten/config_flow.py +++ b/homeassistant/components/luftdaten/config_flow.py @@ -1,4 +1,5 @@ """Config flow to configure the Sensor.Community integration.""" + from __future__ import annotations from typing import Any @@ -7,22 +8,21 @@ from luftdaten import Luftdaten from luftdaten.exceptions import LuftdatenConnectionError import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_SHOW_ON_MAP from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult import homeassistant.helpers.config_validation as cv from .const import CONF_SENSOR_ID, DOMAIN -class SensorCommunityFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class SensorCommunityFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a Sensor.Community config flow.""" VERSION = 1 @callback - def _show_form(self, errors: dict[str, str] | None = None) -> FlowResult: + def _show_form(self, errors: dict[str, str] | None = None) -> ConfigFlowResult: """Show the form to the user.""" return self.async_show_form( step_id="user", @@ -37,7 +37,7 @@ class SensorCommunityFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the start of the config flow.""" if user_input is None: return self._show_form() diff --git a/homeassistant/components/luftdaten/const.py b/homeassistant/components/luftdaten/const.py index b64079933cd..bc6970455c8 100644 --- a/homeassistant/components/luftdaten/const.py +++ b/homeassistant/components/luftdaten/const.py @@ -1,4 +1,5 @@ """Define constants for the Sensor.Community integration.""" + from datetime import timedelta ATTR_SENSOR_ID = "sensor_id" diff --git a/homeassistant/components/luftdaten/diagnostics.py b/homeassistant/components/luftdaten/diagnostics.py index 8146d8eb7d6..a1bbcbcadd7 100644 --- a/homeassistant/components/luftdaten/diagnostics.py +++ b/homeassistant/components/luftdaten/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for Sensor.Community.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/luftdaten/sensor.py b/homeassistant/components/luftdaten/sensor.py index 58fa5788bda..8b9def63fda 100644 --- a/homeassistant/components/luftdaten/sensor.py +++ b/homeassistant/components/luftdaten/sensor.py @@ -1,4 +1,5 @@ """Support for Sensor.Community sensors.""" + from __future__ import annotations from typing import cast diff --git a/homeassistant/components/lupusec/__init__.py b/homeassistant/components/lupusec/__init__.py index f937c7edd10..51bba44aef0 100644 --- a/homeassistant/components/lupusec/__init__.py +++ b/homeassistant/components/lupusec/__init__.py @@ -1,4 +1,5 @@ """Support for Lupusec Home Security system.""" + from json import JSONDecodeError import logging @@ -78,12 +79,12 @@ async def handle_async_init_result(hass: HomeAssistant, domain: str, conf: dict) async_create_issue( hass, DOMAIN, - f"deprecated_yaml_import_issue_${result['reason']}", + f"deprecated_yaml_import_issue_{result['reason']}", breaks_in_ha_version="2024.8.0", is_fixable=False, issue_domain=DOMAIN, severity=IssueSeverity.WARNING, - translation_key=f"deprecated_yaml_import_issue_${result['reason']}", + translation_key=f"deprecated_yaml_import_issue_{result['reason']}", translation_placeholders=ISSUE_PLACEHOLDER, ) diff --git a/homeassistant/components/lupusec/alarm_control_panel.py b/homeassistant/components/lupusec/alarm_control_panel.py index cd4e433bd5d..090d9ab3ced 100644 --- a/homeassistant/components/lupusec/alarm_control_panel.py +++ b/homeassistant/components/lupusec/alarm_control_panel.py @@ -1,4 +1,5 @@ """Support for Lupusec System alarm control panels.""" + from __future__ import annotations from datetime import timedelta @@ -43,7 +44,6 @@ class LupusecAlarm(LupusecDevice, AlarmControlPanelEntity): """An alarm_control_panel implementation for Lupusec.""" _attr_name = None - _attr_icon = "mdi:security" _attr_supported_features = ( AlarmControlPanelEntityFeature.ARM_HOME | AlarmControlPanelEntityFeature.ARM_AWAY diff --git a/homeassistant/components/lupusec/binary_sensor.py b/homeassistant/components/lupusec/binary_sensor.py index 5cf63579984..b2413e2b462 100644 --- a/homeassistant/components/lupusec/binary_sensor.py +++ b/homeassistant/components/lupusec/binary_sensor.py @@ -1,4 +1,5 @@ """Support for Lupusec Security System binary sensors.""" + from __future__ import annotations from datetime import timedelta @@ -34,13 +35,12 @@ async def async_setup_entry( device_types = CONST.TYPE_OPENING + CONST.TYPE_SENSOR - sensors = [] partial_func = partial(data.get_devices, generic_type=device_types) devices = await hass.async_add_executor_job(partial_func) - for device in devices: - sensors.append(LupusecBinarySensor(device, config_entry.entry_id)) - async_add_entities(sensors) + async_add_entities( + LupusecBinarySensor(device, config_entry.entry_id) for device in devices + ) class LupusecBinarySensor(LupusecBaseSensor, BinarySensorEntity): diff --git a/homeassistant/components/lupusec/config_flow.py b/homeassistant/components/lupusec/config_flow.py index 1fae687cbdb..c3fe7295266 100644 --- a/homeassistant/components/lupusec/config_flow.py +++ b/homeassistant/components/lupusec/config_flow.py @@ -1,4 +1,4 @@ -""""Config flow for Lupusec integration.""" +"""Config flow for Lupusec integration.""" from json import JSONDecodeError import logging @@ -16,7 +16,6 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError from .const import DOMAIN @@ -37,7 +36,7 @@ class LupusecConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> config_entries.ConfigFlowResult: """Handle a flow initiated by the user.""" errors = {} @@ -67,7 +66,9 @@ class LupusecConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): step_id="user", data_schema=DATA_SCHEMA, errors=errors ) - async def async_step_import(self, user_input: dict[str, Any]) -> FlowResult: + async def async_step_import( + self, user_input: dict[str, Any] + ) -> config_entries.ConfigFlowResult: """Import the yaml config.""" self._async_abort_entries_match( { diff --git a/homeassistant/components/lupusec/entity.py b/homeassistant/components/lupusec/entity.py index 6237e5dd16b..dc0dac89dc8 100644 --- a/homeassistant/components/lupusec/entity.py +++ b/homeassistant/components/lupusec/entity.py @@ -1,4 +1,5 @@ """Provides the Lupusec entity for Home Assistant.""" + import lupupy from homeassistant.helpers.device_registry import DeviceInfo diff --git a/homeassistant/components/lupusec/switch.py b/homeassistant/components/lupusec/switch.py index e07c974f033..23f3c927880 100644 --- a/homeassistant/components/lupusec/switch.py +++ b/homeassistant/components/lupusec/switch.py @@ -1,4 +1,5 @@ """Support for Lupusec Security System switches.""" + from __future__ import annotations from datetime import timedelta @@ -29,13 +30,12 @@ async def async_setup_entry( device_types = CONST.TYPE_SWITCH - switches = [] partial_func = partial(data.get_devices, generic_type=device_types) devices = await hass.async_add_executor_job(partial_func) - for device in devices: - switches.append(LupusecSwitch(device, config_entry.entry_id)) - async_add_entities(switches) + async_add_entities( + LupusecSwitch(device, config_entry.entry_id) for device in devices + ) class LupusecSwitch(LupusecBaseSensor, SwitchEntity): diff --git a/homeassistant/components/lutron/__init__.py b/homeassistant/components/lutron/__init__.py index ad69a14c0f5..517eb4c8350 100644 --- a/homeassistant/components/lutron/__init__.py +++ b/homeassistant/components/lutron/__init__.py @@ -1,4 +1,5 @@ """Component for interacting with a Lutron RadioRA 2 system.""" + from dataclasses import dataclass import logging diff --git a/homeassistant/components/lutron/binary_sensor.py b/homeassistant/components/lutron/binary_sensor.py index 8cae9c9714a..c33b545413d 100644 --- a/homeassistant/components/lutron/binary_sensor.py +++ b/homeassistant/components/lutron/binary_sensor.py @@ -1,4 +1,5 @@ """Support for Lutron Powr Savr occupancy sensors.""" + from __future__ import annotations from collections.abc import Mapping diff --git a/homeassistant/components/lutron/config_flow.py b/homeassistant/components/lutron/config_flow.py index 04628849230..8fd11484a72 100644 --- a/homeassistant/components/lutron/config_flow.py +++ b/homeassistant/components/lutron/config_flow.py @@ -1,4 +1,5 @@ """Config flow to configure the Lutron integration.""" + from __future__ import annotations import logging @@ -8,9 +9,8 @@ from urllib.error import HTTPError from pylutron import Lutron import voluptuous as vol -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME -from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN @@ -24,7 +24,7 @@ class LutronConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """First step in the config flow.""" # Check if a configuration entry already exists @@ -74,7 +74,9 @@ class LutronConfigFlow(ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_import(self, import_config: dict[str, Any]) -> FlowResult: + async def async_step_import( + self, import_config: dict[str, Any] + ) -> ConfigFlowResult: """Attempt to import the existing configuration.""" if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") diff --git a/homeassistant/components/lutron/cover.py b/homeassistant/components/lutron/cover.py index cdcdf93ccbd..2f80798aee4 100644 --- a/homeassistant/components/lutron/cover.py +++ b/homeassistant/components/lutron/cover.py @@ -1,4 +1,5 @@ """Support for Lutron shades.""" + from __future__ import annotations from collections.abc import Mapping @@ -69,7 +70,7 @@ class LutronCover(LutronDevice, CoverEntity): def _request_state(self) -> None: """Request the state from the device.""" - self._lutron_device.level # pylint: disable=pointless-statement + _ = self._lutron_device.level def _update_attrs(self) -> None: """Update the state attributes.""" diff --git a/homeassistant/components/lutron/fan.py b/homeassistant/components/lutron/fan.py index 4aac95759aa..c350e70b222 100644 --- a/homeassistant/components/lutron/fan.py +++ b/homeassistant/components/lutron/fan.py @@ -1,4 +1,5 @@ """Lutron fan platform.""" + from __future__ import annotations import logging @@ -78,7 +79,7 @@ class LutronFan(LutronDevice, FanEntity): def _request_state(self) -> None: """Request the state from the device.""" - self._lutron_device.level # pylint: disable=pointless-statement + _ = self._lutron_device.level def _update_attrs(self) -> None: """Update the state attributes.""" diff --git a/homeassistant/components/lutron/light.py b/homeassistant/components/lutron/light.py index 0bd00177cc1..18b5edd1039 100644 --- a/homeassistant/components/lutron/light.py +++ b/homeassistant/components/lutron/light.py @@ -1,4 +1,5 @@ """Support for Lutron lights.""" + from __future__ import annotations from collections.abc import Mapping @@ -8,7 +9,14 @@ from typing import Any from pylutron import Output from homeassistant.components.automation import automations_with_entity -from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_FLASH, + ATTR_TRANSITION, + ColorMode, + LightEntity, + LightEntityFeature, +) from homeassistant.components.script import scripts_with_entity from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform @@ -100,6 +108,7 @@ class LutronLight(LutronDevice, LightEntity): _attr_color_mode = ColorMode.BRIGHTNESS _attr_supported_color_modes = {ColorMode.BRIGHTNESS} + _attr_supported_features = LightEntityFeature.TRANSITION | LightEntityFeature.FLASH _lutron_device: Output _prev_brightness: int | None = None _attr_name = None @@ -122,14 +131,20 @@ class LutronLight(LutronDevice, LightEntity): severity=IssueSeverity.WARNING, translation_key="deprecated_light_fan_on", ) - if ATTR_BRIGHTNESS in kwargs and self._lutron_device.is_dimmable: - brightness = kwargs[ATTR_BRIGHTNESS] - elif self._prev_brightness == 0: - brightness = 255 / 2 + if flash := kwargs.get(ATTR_FLASH): + self._lutron_device.flash(0.5 if flash == "short" else 1.5) else: - brightness = self._prev_brightness - self._prev_brightness = brightness - self._lutron_device.level = to_lutron_level(brightness) + if ATTR_BRIGHTNESS in kwargs and self._lutron_device.is_dimmable: + brightness = kwargs[ATTR_BRIGHTNESS] + elif self._prev_brightness == 0: + brightness = 255 / 2 + else: + brightness = self._prev_brightness + self._prev_brightness = brightness + args = {"new_level": brightness} + if ATTR_TRANSITION in kwargs: + args["fade_time_seconds"] = kwargs[ATTR_TRANSITION] + self._lutron_device.set_level(**args) def turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" @@ -144,7 +159,10 @@ class LutronLight(LutronDevice, LightEntity): severity=IssueSeverity.WARNING, translation_key="deprecated_light_fan_off", ) - self._lutron_device.level = 0 + args = {"new_level": 0} + if ATTR_TRANSITION in kwargs: + args["fade_time_seconds"] = kwargs[ATTR_TRANSITION] + self._lutron_device.set_level(**args) @property def extra_state_attributes(self) -> Mapping[str, Any] | None: @@ -153,7 +171,7 @@ class LutronLight(LutronDevice, LightEntity): def _request_state(self) -> None: """Request the state from the device.""" - self._lutron_device.level # pylint: disable=pointless-statement + _ = self._lutron_device.level def _update_attrs(self) -> None: """Update the state attributes.""" diff --git a/homeassistant/components/lutron/scene.py b/homeassistant/components/lutron/scene.py index 9485eddf78b..b66ca08a587 100644 --- a/homeassistant/components/lutron/scene.py +++ b/homeassistant/components/lutron/scene.py @@ -1,4 +1,5 @@ """Support for Lutron scenes.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/lutron/switch.py b/homeassistant/components/lutron/switch.py index 37aa26f6313..c8b93dd7398 100644 --- a/homeassistant/components/lutron/switch.py +++ b/homeassistant/components/lutron/switch.py @@ -1,4 +1,5 @@ """Support for Lutron switches.""" + from __future__ import annotations from collections.abc import Mapping @@ -60,7 +61,7 @@ class LutronSwitch(LutronDevice, SwitchEntity): def _request_state(self) -> None: """Request the state from the device.""" - self._lutron_device.level # pylint: disable=pointless-statement + _ = self._lutron_device.level def _update_attrs(self) -> None: """Update the state attributes.""" @@ -104,7 +105,7 @@ class LutronLed(LutronKeypad, SwitchEntity): def _request_state(self) -> None: """Request the state from the device.""" - self._lutron_device.state # pylint: disable=pointless-statement + _ = self._lutron_device.state def _update_attrs(self) -> None: """Update the state attributes.""" diff --git a/homeassistant/components/lutron_caseta/__init__.py b/homeassistant/components/lutron_caseta/__init__.py index 0dceada821e..f6fed0688c4 100644 --- a/homeassistant/components/lutron_caseta/__init__.py +++ b/homeassistant/components/lutron_caseta/__init__.py @@ -1,4 +1,5 @@ """Component for interacting with a Lutron Caseta system.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/lutron_caseta/binary_sensor.py b/homeassistant/components/lutron_caseta/binary_sensor.py index da7d6106796..73d468a88f2 100644 --- a/homeassistant/components/lutron_caseta/binary_sensor.py +++ b/homeassistant/components/lutron_caseta/binary_sensor.py @@ -1,4 +1,5 @@ """Support for Lutron Caseta Occupancy/Vacancy Sensors.""" + from pylutron_caseta import OCCUPANCY_GROUP_OCCUPIED from homeassistant.components.binary_sensor import ( diff --git a/homeassistant/components/lutron_caseta/button.py b/homeassistant/components/lutron_caseta/button.py index d31e4579675..a1ed43a8b03 100644 --- a/homeassistant/components/lutron_caseta/button.py +++ b/homeassistant/components/lutron_caseta/button.py @@ -1,4 +1,5 @@ """Support for pico and keypad buttons.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/lutron_caseta/config_flow.py b/homeassistant/components/lutron_caseta/config_flow.py index 21f7cbd9683..d7b47aebc7e 100644 --- a/homeassistant/components/lutron_caseta/config_flow.py +++ b/homeassistant/components/lutron_caseta/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Lutron Caseta.""" + from __future__ import annotations import asyncio @@ -10,11 +11,10 @@ from pylutron_caseta.pairing import PAIR_CA, PAIR_CERT, PAIR_KEY, async_pair from pylutron_caseta.smartbridge import Smartbridge import voluptuous as vol -from homeassistant import config_entries from homeassistant.components import zeroconf +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_NAME from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult from .const import ( ABORT_REASON_CANNOT_CONNECT, @@ -45,7 +45,7 @@ DATA_SCHEMA_USER = vol.Schema({vol.Required(CONF_HOST): str}) TLS_ASSET_TEMPLATE = "lutron_caseta-{}-{}.pem" -class LutronCasetaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class LutronCasetaFlowHandler(ConfigFlow, domain=DOMAIN): """Handle Lutron Caseta config flow.""" VERSION = 1 @@ -67,7 +67,7 @@ class LutronCasetaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_zeroconf( self, discovery_info: zeroconf.ZeroconfServiceInfo - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by zeroconf discovery.""" hostname = discovery_info.hostname if hostname is None or not hostname.lower().startswith("lutron-"): @@ -88,7 +88,7 @@ class LutronCasetaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_homekit( self, discovery_info: zeroconf.ZeroconfServiceInfo - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by homekit discovery.""" return await self.async_step_zeroconf(discovery_info) diff --git a/homeassistant/components/lutron_caseta/cover.py b/homeassistant/components/lutron_caseta/cover.py index cca04e0a298..aa5c2f4e0b9 100644 --- a/homeassistant/components/lutron_caseta/cover.py +++ b/homeassistant/components/lutron_caseta/cover.py @@ -4,6 +4,7 @@ from typing import Any from homeassistant.components.cover import ( ATTR_POSITION, + ATTR_TILT_POSITION, DOMAIN, CoverDeviceClass, CoverEntity, @@ -18,26 +19,8 @@ from .const import DOMAIN as CASETA_DOMAIN from .models import LutronCasetaData -async def async_setup_entry( - hass: HomeAssistant, - config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up the Lutron Caseta cover platform. - - Adds shades from the Caseta bridge associated with the config_entry as - cover entities. - """ - data: LutronCasetaData = hass.data[CASETA_DOMAIN][config_entry.entry_id] - bridge = data.bridge - cover_devices = bridge.get_devices_by_domain(DOMAIN) - async_add_entities( - LutronCasetaCover(cover_device, data) for cover_device in cover_devices - ) - - -class LutronCasetaCover(LutronCasetaDeviceUpdatableEntity, CoverEntity): - """Representation of a Lutron shade.""" +class LutronCasetaShade(LutronCasetaDeviceUpdatableEntity, CoverEntity): + """Representation of a Lutron shade with open/close functionality.""" _attr_supported_features = ( CoverEntityFeature.OPEN @@ -57,16 +40,16 @@ class LutronCasetaCover(LutronCasetaDeviceUpdatableEntity, CoverEntity): """Return the current position of cover.""" return self._device["current_state"] - async def async_stop_cover(self, **kwargs: Any) -> None: - """Top the cover.""" - await self._smartbridge.stop_cover(self.device_id) - async def async_close_cover(self, **kwargs: Any) -> None: """Close the cover.""" await self._smartbridge.lower_cover(self.device_id) await self.async_update() self.async_write_ha_state() + async def async_stop_cover(self, **kwargs: Any) -> None: + """Stop the cover.""" + await self._smartbridge.stop_cover(self.device_id) + async def async_open_cover(self, **kwargs: Any) -> None: """Open the cover.""" await self._smartbridge.raise_cover(self.device_id) @@ -75,6 +58,77 @@ class LutronCasetaCover(LutronCasetaDeviceUpdatableEntity, CoverEntity): async def async_set_cover_position(self, **kwargs: Any) -> None: """Move the shade to a specific position.""" - if ATTR_POSITION in kwargs: - position = kwargs[ATTR_POSITION] - await self._smartbridge.set_value(self.device_id, position) + await self._smartbridge.set_value(self.device_id, kwargs[ATTR_POSITION]) + + +class LutronCasetaTiltOnlyBlind(LutronCasetaDeviceUpdatableEntity, CoverEntity): + """Representation of a Lutron tilt only blind.""" + + _attr_supported_features = ( + CoverEntityFeature.OPEN_TILT + | CoverEntityFeature.CLOSE_TILT + | CoverEntityFeature.SET_TILT_POSITION + | CoverEntityFeature.OPEN_TILT + ) + _attr_device_class = CoverDeviceClass.BLIND + + @property + def is_closed(self) -> bool: + """Return if the blind is closed, either at position 0 or 100.""" + return self._device["tilt"] == 0 or self._device["tilt"] == 100 + + @property + def current_cover_tilt_position(self) -> int: + """Return the current tilt position of blind.""" + return self._device["tilt"] + + async def async_close_cover_tilt(self, **kwargs: Any) -> None: + """Close the blind.""" + await self._smartbridge.set_tilt(self.device_id, 0) + await self.async_update() + self.async_write_ha_state() + + async def async_open_cover_tilt(self, **kwargs: Any) -> None: + """Open the blind.""" + await self._smartbridge.set_tilt(self.device_id, 50) + await self.async_update() + self.async_write_ha_state() + + async def async_set_cover_tilt_position(self, **kwargs: Any) -> None: + """Move the blind to a specific tilt.""" + self._smartbridge.set_tilt(self.device_id, kwargs[ATTR_TILT_POSITION]) + + +PYLUTRON_TYPE_TO_CLASSES = { + "SerenaTiltOnlyWoodBlind": LutronCasetaTiltOnlyBlind, + "SerenaHoneycombShade": LutronCasetaShade, + "SerenaRollerShade": LutronCasetaShade, + "TriathlonHoneycombShade": LutronCasetaShade, + "TriathlonRollerShade": LutronCasetaShade, + "QsWirelessShade": LutronCasetaShade, + "QsWirelessHorizontalSheerBlind": LutronCasetaShade, + "Shade": LutronCasetaShade, + "PalladiomWireFreeShade": LutronCasetaShade, +} + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Lutron Caseta cover platform. + + Adds shades from the Caseta bridge associated with the config_entry as + cover entities. + """ + data: LutronCasetaData = hass.data[CASETA_DOMAIN][config_entry.entry_id] + bridge = data.bridge + cover_devices = bridge.get_devices_by_domain(DOMAIN) + async_add_entities( + # default to standard LutronCasetaCover type if the pylutron type is not yet mapped + PYLUTRON_TYPE_TO_CLASSES.get(cover_device["type"], LutronCasetaShade)( + cover_device, data + ) + for cover_device in cover_devices + ) diff --git a/homeassistant/components/lutron_caseta/device_trigger.py b/homeassistant/components/lutron_caseta/device_trigger.py index 7e178698afe..86b82e64127 100644 --- a/homeassistant/components/lutron_caseta/device_trigger.py +++ b/homeassistant/components/lutron_caseta/device_trigger.py @@ -1,4 +1,5 @@ """Provides device triggers for lutron caseta.""" + from __future__ import annotations import logging @@ -378,8 +379,6 @@ async def async_get_triggers( hass: HomeAssistant, device_id: str ) -> list[dict[str, str]]: """List device triggers for lutron caseta devices.""" - triggers = [] - # Check if device is a valid keypad. Return empty if not. if not (data := get_lutron_data_by_dr_id(hass, device_id)) or not ( keypad := data.keypad_data.dr_device_id_to_keypad.get(device_id) @@ -394,19 +393,17 @@ async def async_get_triggers( keypad_button_names_to_leap[keypad["lutron_device_id"]], ) - for trigger in SUPPORTED_INPUTS_EVENTS_TYPES: - for subtype in valid_buttons: - triggers.append( - { - CONF_PLATFORM: "device", - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_TYPE: trigger, - CONF_SUBTYPE: subtype, - } - ) - - return triggers + return [ + { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_TYPE: trigger, + CONF_SUBTYPE: subtype, + } + for trigger in SUPPORTED_INPUTS_EVENTS_TYPES + for subtype in valid_buttons + ] async def async_attach_trigger( diff --git a/homeassistant/components/lutron_caseta/diagnostics.py b/homeassistant/components/lutron_caseta/diagnostics.py index 07bd0a9e8ce..61a24d21b4e 100644 --- a/homeassistant/components/lutron_caseta/diagnostics.py +++ b/homeassistant/components/lutron_caseta/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for lutron_caseta.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/lutron_caseta/fan.py b/homeassistant/components/lutron_caseta/fan.py index ba69f17d880..1577cf52727 100644 --- a/homeassistant/components/lutron_caseta/fan.py +++ b/homeassistant/components/lutron_caseta/fan.py @@ -1,4 +1,5 @@ """Support for Lutron Caseta fans.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/lutron_caseta/light.py b/homeassistant/components/lutron_caseta/light.py index eb3e38b2e39..44c4c63e094 100644 --- a/homeassistant/components/lutron_caseta/light.py +++ b/homeassistant/components/lutron_caseta/light.py @@ -1,4 +1,5 @@ """Support for Lutron Caseta lights.""" + from datetime import timedelta from typing import Any diff --git a/homeassistant/components/lutron_caseta/logbook.py b/homeassistant/components/lutron_caseta/logbook.py index ec612ded375..5b5d2c0f9f1 100644 --- a/homeassistant/components/lutron_caseta/logbook.py +++ b/homeassistant/components/lutron_caseta/logbook.py @@ -1,4 +1,5 @@ """Describe lutron_caseta logbook events.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/lutron_caseta/models.py b/homeassistant/components/lutron_caseta/models.py index 91b042106cb..d5ccbecbd61 100644 --- a/homeassistant/components/lutron_caseta/models.py +++ b/homeassistant/components/lutron_caseta/models.py @@ -1,4 +1,5 @@ """The lutron_caseta integration models.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/homeassistant/components/lutron_caseta/scene.py b/homeassistant/components/lutron_caseta/scene.py index 520dcd965f2..f4aebdafe9b 100644 --- a/homeassistant/components/lutron_caseta/scene.py +++ b/homeassistant/components/lutron_caseta/scene.py @@ -1,4 +1,5 @@ """Support for Lutron Caseta scenes.""" + from typing import Any from pylutron_caseta.smartbridge import Smartbridge diff --git a/homeassistant/components/lutron_caseta/util.py b/homeassistant/components/lutron_caseta/util.py index dfcf7a32228..07b5b502fd0 100644 --- a/homeassistant/components/lutron_caseta/util.py +++ b/homeassistant/components/lutron_caseta/util.py @@ -1,4 +1,5 @@ """Support for Lutron Caseta.""" + from __future__ import annotations diff --git a/homeassistant/components/lw12wifi/light.py b/homeassistant/components/lw12wifi/light.py index 9f520a79ae1..272fcd4a8a1 100644 --- a/homeassistant/components/lw12wifi/light.py +++ b/homeassistant/components/lw12wifi/light.py @@ -1,4 +1,5 @@ """Support for Lagute LW-12 WiFi LED Controller.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/lyric/__init__.py b/homeassistant/components/lyric/__init__.py index d048b31d0b0..e2c85c1400b 100644 --- a/homeassistant/components/lyric/__init__.py +++ b/homeassistant/components/lyric/__init__.py @@ -1,4 +1,5 @@ """The Honeywell Lyric integration.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/lyric/api.py b/homeassistant/components/lyric/api.py index 171e010137a..c9a424bf8ab 100644 --- a/homeassistant/components/lyric/api.py +++ b/homeassistant/components/lyric/api.py @@ -1,4 +1,5 @@ """API for Honeywell Lyric bound to Home Assistant OAuth.""" + from typing import cast from aiohttp import BasicAuth, ClientSession diff --git a/homeassistant/components/lyric/climate.py b/homeassistant/components/lyric/climate.py index ecf9b50474d..f8ae978c2fd 100644 --- a/homeassistant/components/lyric/climate.py +++ b/homeassistant/components/lyric/climate.py @@ -1,4 +1,5 @@ """Support for Honeywell Lyric climate platform.""" + from __future__ import annotations import asyncio @@ -125,23 +126,22 @@ async def async_setup_entry( """Set up the Honeywell Lyric climate platform based on a config entry.""" coordinator: DataUpdateCoordinator[Lyric] = hass.data[DOMAIN][entry.entry_id] - entities = [] - - for location in coordinator.data.locations: - for device in location.devices: - entities.append( - LyricClimate( - coordinator, - ClimateEntityDescription( - key=f"{device.macID}_thermostat", - name=device.name, - ), - location, - device, - ) + async_add_entities( + ( + LyricClimate( + coordinator, + ClimateEntityDescription( + key=f"{device.macID}_thermostat", + name=device.name, + ), + location, + device, ) - - async_add_entities(entities, True) + for location in coordinator.data.locations + for device in location.devices + ), + True, + ) platform = entity_platform.async_get_current_platform() diff --git a/homeassistant/components/lyric/config_flow.py b/homeassistant/components/lyric/config_flow.py index de6808dd0de..db4647145fe 100644 --- a/homeassistant/components/lyric/config_flow.py +++ b/homeassistant/components/lyric/config_flow.py @@ -1,11 +1,12 @@ """Config flow for Honeywell Lyric.""" + from __future__ import annotations from collections.abc import Mapping import logging from typing import Any -from homeassistant.data_entry_flow import FlowResult +from homeassistant.config_entries import ConfigFlowResult from homeassistant.helpers import config_entry_oauth2_flow from .const import DOMAIN @@ -23,19 +24,21 @@ class OAuth2FlowHandler( """Return logger.""" return logging.getLogger(__name__) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Perform reauth upon an API authentication error.""" return await self.async_step_reauth_confirm() async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """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[str, Any]) -> FlowResult: + async def async_oauth_create_entry(self, data: dict[str, Any]) -> ConfigFlowResult: """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/const.py b/homeassistant/components/lyric/const.py index 4f2f72b937b..6d973b56181 100644 --- a/homeassistant/components/lyric/const.py +++ b/homeassistant/components/lyric/const.py @@ -1,4 +1,5 @@ """Constants for the Honeywell Lyric integration.""" + from aiohttp.client_exceptions import ClientResponseError from aiolyric.exceptions import LyricAuthenticationException, LyricException diff --git a/homeassistant/components/lyric/icons.json b/homeassistant/components/lyric/icons.json new file mode 100644 index 00000000000..555215f8685 --- /dev/null +++ b/homeassistant/components/lyric/icons.json @@ -0,0 +1,12 @@ +{ + "entity": { + "sensor": { + "setpoint_status": { + "default": "mdi:thermostat" + } + } + }, + "services": { + "set_hold_time": "mdi:timer-pause" + } +} diff --git a/homeassistant/components/lyric/sensor.py b/homeassistant/components/lyric/sensor.py index 1b9af351e71..276336e02cc 100644 --- a/homeassistant/components/lyric/sensor.py +++ b/homeassistant/components/lyric/sensor.py @@ -1,4 +1,5 @@ """Support for Honeywell Lyric sensor platform.""" + from __future__ import annotations from collections.abc import Callable @@ -41,21 +42,14 @@ LYRIC_SETPOINT_STATUS_NAMES = { } -@dataclass(frozen=True) -class LyricSensorEntityDescriptionMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class LyricSensorEntityDescription(SensorEntityDescription): + """Class describing Honeywell Lyric sensor entities.""" value_fn: Callable[[LyricDevice], StateType | datetime] suitable_fn: Callable[[LyricDevice], bool] -@dataclass(frozen=True) -class LyricSensorEntityDescription( - SensorEntityDescription, LyricSensorEntityDescriptionMixin -): - """Class describing Honeywell Lyric sensor entities.""" - - DEVICE_SENSORS: list[LyricSensorEntityDescription] = [ LyricSensorEntityDescription( key="indoor_temperature", @@ -105,7 +99,6 @@ DEVICE_SENSORS: list[LyricSensorEntityDescription] = [ LyricSensorEntityDescription( key="setpoint_status", translation_key="setpoint_status", - icon="mdi:thermostat", value_fn=lambda device: get_setpoint_status( device.changeableValues.thermostatSetpointStatus, device.changeableValues.nextPeriodTime, @@ -141,22 +134,18 @@ async def async_setup_entry( """Set up the Honeywell Lyric sensor platform based on a config entry.""" coordinator: DataUpdateCoordinator[Lyric] = hass.data[DOMAIN][entry.entry_id] - entities = [] - - for location in coordinator.data.locations: - for device in location.devices: - for device_sensor in DEVICE_SENSORS: - if device_sensor.suitable_fn(device): - entities.append( - LyricSensor( - coordinator, - device_sensor, - location, - device, - ) - ) - - async_add_entities(entities) + async_add_entities( + LyricSensor( + coordinator, + device_sensor, + location, + device, + ) + for location in coordinator.data.locations + for device in location.devices + for device_sensor in DEVICE_SENSORS + if device_sensor.suitable_fn(device) + ) class LyricSensor(LyricDeviceEntity, SensorEntity): diff --git a/homeassistant/components/mailbox/__init__.py b/homeassistant/components/mailbox/__init__.py index 1becc15624e..b000f1eadcb 100644 --- a/homeassistant/components/mailbox/__init__.py +++ b/homeassistant/components/mailbox/__init__.py @@ -1,4 +1,5 @@ """Support for Voice mailboxes.""" + from __future__ import annotations import asyncio @@ -179,7 +180,7 @@ class Mailbox: @property def media_type(self) -> str: """Return the supported media type.""" - raise NotImplementedError() + raise NotImplementedError @property def can_delete(self) -> bool: @@ -193,15 +194,15 @@ class Mailbox: async def async_get_media(self, msgid: str) -> bytes: """Return the media blob for the msgid.""" - raise NotImplementedError() + raise NotImplementedError async def async_get_messages(self) -> list[dict[str, Any]]: """Return a list of the current messages.""" - raise NotImplementedError() + raise NotImplementedError async def async_delete(self, msgid: str) -> bool: """Delete the specified messages.""" - raise NotImplementedError() + raise NotImplementedError class StreamError(Exception): @@ -231,16 +232,16 @@ class MailboxPlatformsView(MailboxView): async def get(self, request: web.Request) -> web.Response: """Retrieve list of platforms.""" - platforms: list[dict[str, Any]] = [] - for mailbox in self.mailboxes: - platforms.append( + return self.json( + [ { "name": mailbox.name, "has_media": mailbox.has_media, "can_delete": mailbox.can_delete, } - ) - return self.json(platforms) + for mailbox in self.mailboxes + ] + ) class MailboxMessageView(MailboxView): diff --git a/homeassistant/components/mailgun/__init__.py b/homeassistant/components/mailgun/__init__.py index fc2cfcb4cef..25a4bf494ee 100644 --- a/homeassistant/components/mailgun/__init__.py +++ b/homeassistant/components/mailgun/__init__.py @@ -1,4 +1,5 @@ """Support for Mailgun.""" + import hashlib import hmac import json diff --git a/homeassistant/components/mailgun/config_flow.py b/homeassistant/components/mailgun/config_flow.py index bfeaed5ae5b..3af12460e5e 100644 --- a/homeassistant/components/mailgun/config_flow.py +++ b/homeassistant/components/mailgun/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Mailgun.""" + from homeassistant.helpers import config_entry_flow from .const import DOMAIN diff --git a/homeassistant/components/mailgun/notify.py b/homeassistant/components/mailgun/notify.py index b7104d4a0f1..ed5b3a69135 100644 --- a/homeassistant/components/mailgun/notify.py +++ b/homeassistant/components/mailgun/notify.py @@ -1,4 +1,5 @@ """Support for the Mailgun mail notifications.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/manual/alarm_control_panel.py b/homeassistant/components/manual/alarm_control_panel.py index 099275a98a1..6f4a3306c29 100644 --- a/homeassistant/components/manual/alarm_control_panel.py +++ b/homeassistant/components/manual/alarm_control_panel.py @@ -1,4 +1,5 @@ """Support for manual alarms.""" + from __future__ import annotations import datetime diff --git a/homeassistant/components/manual_mqtt/alarm_control_panel.py b/homeassistant/components/manual_mqtt/alarm_control_panel.py index d1442a4e9ed..0cd92b552c6 100644 --- a/homeassistant/components/manual_mqtt/alarm_control_panel.py +++ b/homeassistant/components/manual_mqtt/alarm_control_panel.py @@ -1,4 +1,5 @@ """Support for manual alarms controllable via MQTT.""" + from __future__ import annotations import datetime @@ -27,7 +28,7 @@ from homeassistant.const import ( STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import Event, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -36,7 +37,7 @@ from homeassistant.helpers.event import ( async_track_point_in_time, async_track_state_change_event, ) -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, EventType +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -482,7 +483,7 @@ class ManualMQTTAlarm(alarm.AlarmControlPanelEntity): ) async def _async_state_changed_listener( - self, event: EventType[EventStateChangedData] + self, event: Event[EventStateChangedData] ) -> None: """Publish state change to MQTT.""" if (new_state := event.data["new_state"]) is None: diff --git a/homeassistant/components/map/__init__.py b/homeassistant/components/map/__init__.py index a3ba65be7db..25095e92b93 100644 --- a/homeassistant/components/map/__init__.py +++ b/homeassistant/components/map/__init__.py @@ -1,15 +1,53 @@ """Support for showing device locations.""" -from homeassistant.components import frontend -from homeassistant.core import HomeAssistant + +from homeassistant.components import onboarding +from homeassistant.components.lovelace import _create_map_dashboard +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue +from homeassistant.helpers.storage import Store from homeassistant.helpers.typing import ConfigType DOMAIN = "map" CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN) +STORAGE_KEY = DOMAIN +STORAGE_VERSION_MAJOR = 1 + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Register the built-in map panel.""" - frontend.async_register_built_in_panel(hass, "map", "map", "hass:tooltip-account") + """Create a map panel.""" + + if DOMAIN in config: + async_create_issue( + hass, + HOMEASSISTANT_DOMAIN, + f"deprecated_yaml_{DOMAIN}", + breaks_in_ha_version="2024.10.0", + is_fixable=False, + is_persistent=False, + issue_domain=DOMAIN, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": "map", + }, + ) + + store: Store[dict[str, bool]] = Store( + hass, + STORAGE_VERSION_MAJOR, + STORAGE_KEY, + ) + data = await store.async_load() + if data: + return True + + if onboarding.async_is_onboarded(hass): + await _create_map_dashboard(hass) + + await store.async_save({"migrated": True}) + return True diff --git a/homeassistant/components/map/manifest.json b/homeassistant/components/map/manifest.json index b617aa3e5fa..6a0333c862a 100644 --- a/homeassistant/components/map/manifest.json +++ b/homeassistant/components/map/manifest.json @@ -2,7 +2,7 @@ "domain": "map", "name": "Map", "codeowners": [], - "dependencies": ["frontend"], + "dependencies": ["frontend", "lovelace"], "documentation": "https://www.home-assistant.io/integrations/map", "integration_type": "system", "quality_scale": "internal" diff --git a/homeassistant/components/marytts/tts.py b/homeassistant/components/marytts/tts.py index 56f31d81d97..168d735a987 100644 --- a/homeassistant/components/marytts/tts.py +++ b/homeassistant/components/marytts/tts.py @@ -1,4 +1,5 @@ """Support for the MaryTTS service.""" + from __future__ import annotations from speak2mary import MaryTTS diff --git a/homeassistant/components/mastodon/manifest.json b/homeassistant/components/mastodon/manifest.json index 2505dcc5dd0..673a60166c0 100644 --- a/homeassistant/components/mastodon/manifest.json +++ b/homeassistant/components/mastodon/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/mastodon", "iot_class": "cloud_push", "loggers": ["mastodon"], - "requirements": ["Mastodon.py==1.5.1"] + "requirements": ["Mastodon.py==1.8.1"] } diff --git a/homeassistant/components/mastodon/notify.py b/homeassistant/components/mastodon/notify.py index 8f259bc3433..97ab2145486 100644 --- a/homeassistant/components/mastodon/notify.py +++ b/homeassistant/components/mastodon/notify.py @@ -1,4 +1,5 @@ """Mastodon platform for notify component.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/matrix/__init__.py b/homeassistant/components/matrix/__init__.py index e91ee4d270c..b8f1ec08fe0 100644 --- a/homeassistant/components/matrix/__init__.py +++ b/homeassistant/components/matrix/__init__.py @@ -1,4 +1,5 @@ """The Matrix bot component.""" + from __future__ import annotations import asyncio @@ -431,18 +432,16 @@ class MatrixBot: self, target_rooms: Sequence[RoomAnyID], message_type: str, content: dict ) -> None: """Wrap _handle_room_send for multiple target_rooms.""" - _tasks = [] - for target_room in target_rooms: - _tasks.append( - self.hass.async_create_task( - self._handle_room_send( - target_room=target_room, - message_type=message_type, - content=content, - ) + await asyncio.wait( + self.hass.async_create_task( + self._handle_room_send( + target_room=target_room, + message_type=message_type, + content=content, ) ) - await asyncio.wait(_tasks) + for target_room in target_rooms + ) async def _send_image( self, image_path: str, target_rooms: Sequence[RoomAnyID] diff --git a/homeassistant/components/matrix/const.py b/homeassistant/components/matrix/const.py index b7e0c22e2ac..bae53f05727 100644 --- a/homeassistant/components/matrix/const.py +++ b/homeassistant/components/matrix/const.py @@ -1,4 +1,5 @@ """Constants for the Matrix integration.""" + DOMAIN = "matrix" SERVICE_SEND_MESSAGE = "send_message" diff --git a/homeassistant/components/matrix/icons.json b/homeassistant/components/matrix/icons.json new file mode 100644 index 00000000000..4fc56ebe0ff --- /dev/null +++ b/homeassistant/components/matrix/icons.json @@ -0,0 +1,5 @@ +{ + "services": { + "send_message": "mdi:matrix" + } +} diff --git a/homeassistant/components/matrix/notify.py b/homeassistant/components/matrix/notify.py index c71f91eb582..0c8430afacd 100644 --- a/homeassistant/components/matrix/notify.py +++ b/homeassistant/components/matrix/notify.py @@ -1,4 +1,5 @@ """Support for Matrix notifications.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/matter/adapter.py b/homeassistant/components/matter/adapter.py index 6d7d437a206..a3536435ded 100644 --- a/homeassistant/components/matter/adapter.py +++ b/homeassistant/components/matter/adapter.py @@ -1,4 +1,5 @@ """Matter to Home Assistant adapter.""" + from __future__ import annotations from typing import TYPE_CHECKING, cast @@ -184,10 +185,14 @@ class MatterAdapter: endpoint, ) identifiers = {(DOMAIN, f"{ID_TYPE_DEVICE_ID}_{node_device_id}")} + serial_number: str | None = None # if available, we also add the serialnumber as identifier - if basic_info.serialNumber and "test" not in basic_info.serialNumber.lower(): + if ( + basic_info_serial_number := basic_info.serialNumber + ) and "test" not in basic_info_serial_number.lower(): # prefix identifier with 'serial_' to be able to filter it - identifiers.add((DOMAIN, f"{ID_TYPE_SERIAL}_{basic_info.serialNumber}")) + identifiers.add((DOMAIN, f"{ID_TYPE_SERIAL}_{basic_info_serial_number}")) + serial_number = basic_info_serial_number model = ( get_clean_name(basic_info.productName) or device_type.__name__ @@ -202,6 +207,7 @@ class MatterAdapter: sw_version=basic_info.softwareVersionString, manufacturer=basic_info.vendorName or endpoint.node.device_info.vendorName, model=model, + serial_number=serial_number, via_device=(DOMAIN, bridge_device_id) if bridge_device_id else None, ) diff --git a/homeassistant/components/matter/addon.py b/homeassistant/components/matter/addon.py index 84f430a58d8..a463685a073 100644 --- a/homeassistant/components/matter/addon.py +++ b/homeassistant/components/matter/addon.py @@ -1,4 +1,5 @@ """Provide add-on management.""" + from __future__ import annotations from homeassistant.components.hassio import AddonManager diff --git a/homeassistant/components/matter/api.py b/homeassistant/components/matter/api.py index 21445e469aa..e6a2a6c54d5 100644 --- a/homeassistant/components/matter/api.py +++ b/homeassistant/components/matter/api.py @@ -1,4 +1,5 @@ """Handle websocket api for Matter.""" + from __future__ import annotations from collections.abc import Callable, Coroutine @@ -165,7 +166,7 @@ async def websocket_commission_on_network( ) -> None: """Commission a device already on the network.""" await matter.matter_client.commission_on_network( - msg["pin"], ip_addr=msg.get("ip_addr", None) + msg["pin"], ip_addr=msg.get("ip_addr") ) connection.send_result(msg[ID]) diff --git a/homeassistant/components/matter/binary_sensor.py b/homeassistant/components/matter/binary_sensor.py index ea87fabf3f5..23ac2195355 100644 --- a/homeassistant/components/matter/binary_sensor.py +++ b/homeassistant/components/matter/binary_sensor.py @@ -1,4 +1,5 @@ """Matter binary sensors.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/homeassistant/components/matter/climate.py b/homeassistant/components/matter/climate.py index 8769fc430d8..1b949d3ebfb 100644 --- a/homeassistant/components/matter/climate.py +++ b/homeassistant/components/matter/climate.py @@ -1,4 +1,5 @@ """Matter climate platform.""" + from __future__ import annotations from enum import IntEnum @@ -312,6 +313,6 @@ DISCOVERY_SCHEMAS = [ clusters.Thermostat.Attributes.UnoccupiedCoolingSetpoint, clusters.Thermostat.Attributes.UnoccupiedHeatingSetpoint, ), - device_type=(device_types.Thermostat,), + device_type=(device_types.Thermostat, device_types.RoomAirConditioner), ), ] diff --git a/homeassistant/components/matter/config_flow.py b/homeassistant/components/matter/config_flow.py index 1636790c4cb..7dc06807a98 100644 --- a/homeassistant/components/matter/config_flow.py +++ b/homeassistant/components/matter/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Matter integration.""" + from __future__ import annotations import asyncio @@ -8,7 +9,6 @@ from matter_server.client import MatterClient from matter_server.client.exceptions import CannotConnect, InvalidServerVersion import voluptuous as vol -from homeassistant import config_entries from homeassistant.components.hassio import ( AddonError, AddonInfo, @@ -17,9 +17,10 @@ from homeassistant.components.hassio import ( HassioServiceInfo, is_hassio, ) +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_URL from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import AbortFlow, FlowResult +from homeassistant.data_entry_flow import AbortFlow from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import aiohttp_client @@ -56,7 +57,7 @@ def build_ws_address(host: str, port: int) -> str: return f"ws://{host}:{port}/ws" -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class MatterConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Matter.""" VERSION = 1 @@ -72,7 +73,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_install_addon( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Install Matter Server add-on.""" if not self.install_task: self.install_task = self.hass.async_create_task(self._async_install_addon()) @@ -98,7 +99,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_install_failed( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Add-on installation failed.""" return self.async_abort(reason="addon_install_failed") @@ -120,7 +121,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_start_addon( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Start Matter Server add-on.""" if not self.start_task: self.start_task = self.hass.async_create_task(self._async_start_addon()) @@ -143,7 +144,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_start_failed( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Add-on start failed.""" return self.async_abort(reason="addon_start_failed") @@ -186,7 +187,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" if is_hassio(self.hass): return await self.async_step_on_supervisor() @@ -195,7 +196,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_manual( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a manual configuration.""" if user_input is None: return self.async_show_form( @@ -222,7 +223,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="manual", data_schema=get_manual_schema(user_input), errors=errors ) - async def async_step_hassio(self, discovery_info: HassioServiceInfo) -> FlowResult: + async def async_step_hassio( + self, discovery_info: HassioServiceInfo + ) -> ConfigFlowResult: """Receive configuration from add-on discovery info. This flow is triggered by the Matter Server add-on. @@ -240,7 +243,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_hassio_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm the add-on discovery.""" if user_input is not None: return await self.async_step_on_supervisor( @@ -251,7 +254,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_on_supervisor( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle logic when on Supervisor host.""" if user_input is None: return self.async_show_form( @@ -274,7 +277,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_finish_addon_setup( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Prepare info needed to complete the config entry.""" if not self.ws_address: discovery_info = await self._async_get_addon_discovery_info() @@ -289,7 +292,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return await self._async_create_entry_or_abort() - async def _async_create_entry_or_abort(self) -> FlowResult: + async def _async_create_entry_or_abort(self) -> ConfigFlowResult: """Return a config entry for the flow or abort if already configured.""" assert self.ws_address is not None diff --git a/homeassistant/components/matter/const.py b/homeassistant/components/matter/const.py index e7f96bd2448..a0e160a6c01 100644 --- a/homeassistant/components/matter/const.py +++ b/homeassistant/components/matter/const.py @@ -1,4 +1,5 @@ """Constants for the Matter integration.""" + import logging ADDON_SLUG = "core_matter_server" diff --git a/homeassistant/components/matter/cover.py b/homeassistant/components/matter/cover.py index 590f325cf22..ea5250c9bd3 100644 --- a/homeassistant/components/matter/cover.py +++ b/homeassistant/components/matter/cover.py @@ -1,4 +1,5 @@ """Matter cover.""" + from __future__ import annotations from enum import IntEnum diff --git a/homeassistant/components/matter/diagnostics.py b/homeassistant/components/matter/diagnostics.py index 8846a75b42a..23b6854c791 100644 --- a/homeassistant/components/matter/diagnostics.py +++ b/homeassistant/components/matter/diagnostics.py @@ -1,4 +1,5 @@ """Provide diagnostics for Matter.""" + from __future__ import annotations from copy import deepcopy @@ -21,7 +22,7 @@ def redact_matter_attributes(node_data: dict[str, Any]) -> dict[str, Any]: """Redact Matter cluster attribute.""" redacted = deepcopy(node_data) for attribute_to_redact in ATTRIBUTES_TO_REDACT: - for attribute_path, _value in redacted["attributes"].items(): + for attribute_path in redacted["attributes"]: _, cluster_id, attribute_id = parse_attribute_path(attribute_path) if cluster_id != attribute_to_redact.cluster_id: continue diff --git a/homeassistant/components/matter/discovery.py b/homeassistant/components/matter/discovery.py index e1d004a15c8..985ac1c996e 100644 --- a/homeassistant/components/matter/discovery.py +++ b/homeassistant/components/matter/discovery.py @@ -1,4 +1,5 @@ """Map Matter Nodes and Attributes to Home Assistant entities.""" + from __future__ import annotations from collections.abc import Generator diff --git a/homeassistant/components/matter/entity.py b/homeassistant/components/matter/entity.py index 5c3f65d903c..dcb3586934b 100644 --- a/homeassistant/components/matter/entity.py +++ b/homeassistant/components/matter/entity.py @@ -1,4 +1,5 @@ """Matter entity base class.""" + from __future__ import annotations from abc import abstractmethod diff --git a/homeassistant/components/matter/event.py b/homeassistant/components/matter/event.py index e84fcec32d8..ea48beef782 100644 --- a/homeassistant/components/matter/event.py +++ b/homeassistant/components/matter/event.py @@ -1,4 +1,5 @@ """Matter event entities from Node events.""" + from __future__ import annotations from typing import Any @@ -104,7 +105,7 @@ class MatterEventEntity(MatterEntity, EventEntity): """Call when Node attribute(s) changed.""" @callback - def _on_matter_node_event( # noqa: F821 + def _on_matter_node_event( self, event: EventType, data: MatterNodeEvent, diff --git a/homeassistant/components/matter/helpers.py b/homeassistant/components/matter/helpers.py index 8f7f3d81883..9aa58879214 100644 --- a/homeassistant/components/matter/helpers.py +++ b/homeassistant/components/matter/helpers.py @@ -1,4 +1,5 @@ """Provide integration helpers that are aware of the matter integration.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/matter/light.py b/homeassistant/components/matter/light.py index aa93cef9916..fce780896a4 100644 --- a/homeassistant/components/matter/light.py +++ b/homeassistant/components/matter/light.py @@ -1,4 +1,5 @@ """Matter light.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/matter/lock.py b/homeassistant/components/matter/lock.py index dd29638f765..e5067efd482 100644 --- a/homeassistant/components/matter/lock.py +++ b/homeassistant/components/matter/lock.py @@ -1,4 +1,5 @@ """Matter lock.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/matter/manifest.json b/homeassistant/components/matter/manifest.json index 0e1ed4e80b6..716e296ec15 100644 --- a/homeassistant/components/matter/manifest.json +++ b/homeassistant/components/matter/manifest.json @@ -5,7 +5,6 @@ "config_flow": true, "dependencies": ["websocket_api"], "documentation": "https://www.home-assistant.io/integrations/matter", - "import_executor": true, "iot_class": "local_push", "requirements": ["python-matter-server==5.7.0"] } diff --git a/homeassistant/components/matter/models.py b/homeassistant/components/matter/models.py index 5f47f73b139..18e503523ae 100644 --- a/homeassistant/components/matter/models.py +++ b/homeassistant/components/matter/models.py @@ -1,4 +1,5 @@ """Models used for the Matter integration.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/homeassistant/components/matter/sensor.py b/homeassistant/components/matter/sensor.py index 90a9cb3fcc8..6f1bd1d142b 100644 --- a/homeassistant/components/matter/sensor.py +++ b/homeassistant/components/matter/sensor.py @@ -1,4 +1,5 @@ """Matter sensors.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/homeassistant/components/matter/switch.py b/homeassistant/components/matter/switch.py index 61922e8e8c9..9bc858d40c0 100644 --- a/homeassistant/components/matter/switch.py +++ b/homeassistant/components/matter/switch.py @@ -1,4 +1,5 @@ """Matter switches.""" + from __future__ import annotations from typing import Any @@ -85,6 +86,7 @@ DISCOVERY_SCHEMAS = [ device_types.ColorDimmerSwitch, device_types.DimmerSwitch, device_types.Thermostat, + device_types.RoomAirConditioner, ), ), ] diff --git a/homeassistant/components/matter/util.py b/homeassistant/components/matter/util.py index 834c71a0307..0df2230ab96 100644 --- a/homeassistant/components/matter/util.py +++ b/homeassistant/components/matter/util.py @@ -1,4 +1,5 @@ """Provide integration utilities.""" + from __future__ import annotations XY_COLOR_FACTOR = 65536 diff --git a/homeassistant/components/maxcube/__init__.py b/homeassistant/components/maxcube/__init__.py index 41aed4be15c..82cdc56e5d9 100644 --- a/homeassistant/components/maxcube/__init__.py +++ b/homeassistant/components/maxcube/__init__.py @@ -1,4 +1,5 @@ """Support for the MAX! Cube LAN Gateway.""" + import logging from threading import Lock import time diff --git a/homeassistant/components/maxcube/binary_sensor.py b/homeassistant/components/maxcube/binary_sensor.py index 6dcfcf8dca4..208b93eb19a 100644 --- a/homeassistant/components/maxcube/binary_sensor.py +++ b/homeassistant/components/maxcube/binary_sensor.py @@ -1,4 +1,5 @@ """Support for MAX! binary sensors via MAX! Cube.""" + from __future__ import annotations from homeassistant.components.binary_sensor import ( diff --git a/homeassistant/components/maxcube/climate.py b/homeassistant/components/maxcube/climate.py index 42abed48724..b14efbbe073 100644 --- a/homeassistant/components/maxcube/climate.py +++ b/homeassistant/components/maxcube/climate.py @@ -1,4 +1,5 @@ """Support for MAX! Thermostats via MAX! Cube.""" + from __future__ import annotations import logging @@ -53,13 +54,13 @@ def setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Iterate through all MAX! Devices and add thermostats.""" - devices = [] - for handler in hass.data[DATA_KEY].values(): - for device in handler.cube.devices: - if device.is_thermostat() or device.is_wallthermostat(): - devices.append(MaxCubeClimate(handler, device)) - add_entities(devices) + add_entities( + MaxCubeClimate(handler, device) + for handler in hass.data[DATA_KEY].values() + for device in handler.cube.devices + if device.is_thermostat() or device.is_wallthermostat() + ) class MaxCubeClimate(ClimateEntity): diff --git a/homeassistant/components/mazda/__init__.py b/homeassistant/components/mazda/__init__.py index 75e7baf7413..fd323060ac0 100644 --- a/homeassistant/components/mazda/__init__.py +++ b/homeassistant/components/mazda/__init__.py @@ -1,4 +1,5 @@ """The Mazda Connected Services integration.""" + from __future__ import annotations from homeassistant.config_entries import ConfigEntry, ConfigEntryState diff --git a/homeassistant/components/meater/__init__.py b/homeassistant/components/meater/__init__.py index ee2307fbc84..08ca32029cb 100644 --- a/homeassistant/components/meater/__init__.py +++ b/homeassistant/components/meater/__init__.py @@ -1,4 +1,5 @@ """The Meater Temperature Probe integration.""" + import asyncio from datetime import timedelta import logging diff --git a/homeassistant/components/meater/config_flow.py b/homeassistant/components/meater/config_flow.py index 91a927a5fb2..0f2bb35755f 100644 --- a/homeassistant/components/meater/config_flow.py +++ b/homeassistant/components/meater/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Meater.""" + from __future__ import annotations from collections.abc import Mapping @@ -7,9 +8,8 @@ from typing import Any from meater import AuthenticationError, MeaterApi, ServiceUnavailableError import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import aiohttp_client from .const import DOMAIN @@ -20,7 +20,7 @@ USER_SCHEMA = vol.Schema( ) -class MeaterConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class MeaterConfigFlow(ConfigFlow, domain=DOMAIN): """Meater Config Flow.""" _data_schema = USER_SCHEMA @@ -28,7 +28,7 @@ class MeaterConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, str] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Define the login user step.""" if user_input is None: return self.async_show_form( @@ -45,7 +45,9 @@ class MeaterConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return await self._try_connect_meater("user", None, username, password) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Handle configuration by re-auth.""" self._data_schema = REAUTH_SCHEMA self._username = entry_data[CONF_USERNAME] @@ -53,7 +55,7 @@ class MeaterConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_reauth_confirm( self, user_input: dict[str, str] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle re-auth completion.""" placeholders = {"username": self._username} if not user_input: @@ -70,7 +72,7 @@ class MeaterConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def _try_connect_meater( self, step_id, placeholders: dict[str, str] | None, username: str, password: str - ) -> FlowResult: + ) -> ConfigFlowResult: session = aiohttp_client.async_get_clientsession(self.hass) api = MeaterApi(session) diff --git a/homeassistant/components/meater/sensor.py b/homeassistant/components/meater/sensor.py index a7e03ae7c22..f719cb0f0e3 100644 --- a/homeassistant/components/meater/sensor.py +++ b/homeassistant/components/meater/sensor.py @@ -1,4 +1,5 @@ """The Meater Temperature Probe integration.""" + from __future__ import annotations from collections.abc import Callable @@ -27,21 +28,14 @@ from homeassistant.util import dt as dt_util from .const import DOMAIN -@dataclass(frozen=True) -class MeaterSensorEntityDescriptionMixin: - """Mixin for MeaterSensorEntityDescription.""" +@dataclass(frozen=True, kw_only=True) +class MeaterSensorEntityDescription(SensorEntityDescription): + """Describes meater sensor entity.""" available: Callable[[MeaterProbe | None], bool] value: Callable[[MeaterProbe], datetime | float | str | None] -@dataclass(frozen=True) -class MeaterSensorEntityDescription( - SensorEntityDescription, MeaterSensorEntityDescriptionMixin -): - """Describes meater sensor entity.""" - - def _elapsed_time_to_timestamp(probe: MeaterProbe) -> datetime | None: """Convert elapsed time to timestamp.""" if not probe.cook or not hasattr(probe.cook, "time_elapsed"): diff --git a/homeassistant/components/medcom_ble/__init__.py b/homeassistant/components/medcom_ble/__init__.py index a129c4fc7f9..36357746b95 100644 --- a/homeassistant/components/medcom_ble/__init__.py +++ b/homeassistant/components/medcom_ble/__init__.py @@ -1,4 +1,5 @@ """The Medcom BLE integration.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/medcom_ble/config_flow.py b/homeassistant/components/medcom_ble/config_flow.py index 30a87afbb72..faf482ca1f9 100644 --- a/homeassistant/components/medcom_ble/config_flow.py +++ b/homeassistant/components/medcom_ble/config_flow.py @@ -16,9 +16,9 @@ from homeassistant.components.bluetooth import ( BluetoothServiceInfo, async_discovered_service_info, ) -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_ADDRESS -from homeassistant.data_entry_flow import AbortFlow, FlowResult +from homeassistant.data_entry_flow import AbortFlow from .const import DOMAIN @@ -51,7 +51,7 @@ class InspectorBLEConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_bluetooth( self, discovery_info: BluetoothServiceInfo - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the bluetooth discovery step.""" _LOGGER.debug("Discovered BLE device: %s", discovery_info.name) await self.async_set_unique_id(discovery_info.address) @@ -67,7 +67,7 @@ class InspectorBLEConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_bluetooth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm discovery.""" # We always will have self._discovery_info be a BluetoothServiceInfo at this point # and this helps mypy not complain @@ -84,7 +84,7 @@ class InspectorBLEConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the user step to pick discovered device.""" if user_input is not None: address = user_input[CONF_ADDRESS] @@ -123,7 +123,7 @@ class InspectorBLEConfigFlow(ConfigFlow, domain=DOMAIN): ), ) - async def async_step_check_connection(self) -> FlowResult: + async def async_step_check_connection(self) -> ConfigFlowResult: """Check we can connect to the device before considering the configuration is successful.""" # We always will have self._discovery_info be a BluetoothServiceInfo at this point # and this helps mypy not complain diff --git a/homeassistant/components/medcom_ble/icons.json b/homeassistant/components/medcom_ble/icons.json new file mode 100644 index 00000000000..682d028fa02 --- /dev/null +++ b/homeassistant/components/medcom_ble/icons.json @@ -0,0 +1,9 @@ +{ + "entity": { + "sensor": { + "cpm": { + "default": "mdi:radioactive" + } + } + } +} diff --git a/homeassistant/components/medcom_ble/sensor.py b/homeassistant/components/medcom_ble/sensor.py index 4c7488ddc12..b5cb29be845 100644 --- a/homeassistant/components/medcom_ble/sensor.py +++ b/homeassistant/components/medcom_ble/sensor.py @@ -1,4 +1,5 @@ """Support for Medcom BLE radiation monitor sensors.""" + from __future__ import annotations import logging @@ -29,7 +30,6 @@ SENSORS_MAPPING_TEMPLATE: dict[str, SensorEntityDescription] = { translation_key="cpm", native_unit_of_measurement=UNIT_CPM, state_class=SensorStateClass.MEASUREMENT, - icon="mdi:radioactive", ), } diff --git a/homeassistant/components/media_extractor/__init__.py b/homeassistant/components/media_extractor/__init__.py index b657caceaff..888265e8d3c 100644 --- a/homeassistant/components/media_extractor/__init__.py +++ b/homeassistant/components/media_extractor/__init__.py @@ -1,4 +1,5 @@ """Decorator service for the media_player.play_media service.""" + from collections.abc import Callable import logging from pathlib import Path @@ -126,7 +127,7 @@ class MediaExtractor: all_media = ydl.extract_info(self.get_media_url(), process=False) except DownloadError as err: # This exception will be logged by youtube-dl itself - raise MEDownloadException() from err + raise MEDownloadException from err if "entries" in all_media: _LOGGER.warning("Playlists are not supported, looking for the first video") @@ -135,7 +136,7 @@ class MediaExtractor: selected_media = entries[0] else: _LOGGER.error("Playlist is empty") - raise MEDownloadException() + raise MEDownloadException else: selected_media = all_media @@ -146,7 +147,7 @@ class MediaExtractor: requested_stream = ydl.process_ie_result(selected_media, download=False) except (ExtractorError, DownloadError) as err: _LOGGER.error("Could not extract stream for the query: %s", query) - raise MEQueryException() from err + raise MEQueryException from err if "formats" in requested_stream: if requested_stream["extractor"] == "youtube": diff --git a/homeassistant/components/media_extractor/icons.json b/homeassistant/components/media_extractor/icons.json new file mode 100644 index 00000000000..71b65e7c4a6 --- /dev/null +++ b/homeassistant/components/media_extractor/icons.json @@ -0,0 +1,5 @@ +{ + "services": { + "play_media": "mdi:play" + } +} diff --git a/homeassistant/components/media_extractor/manifest.json b/homeassistant/components/media_extractor/manifest.json index 111509c1f31..c86099a9ea4 100644 --- a/homeassistant/components/media_extractor/manifest.json +++ b/homeassistant/components/media_extractor/manifest.json @@ -7,5 +7,5 @@ "iot_class": "calculated", "loggers": ["yt_dlp"], "quality_scale": "internal", - "requirements": ["yt-dlp==2023.11.16"] + "requirements": ["yt-dlp==2024.03.10"] } diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index ffb1d6d4a32..6535aea3e52 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -1,4 +1,5 @@ """Component to interface with various media players.""" + from __future__ import annotations import asyncio @@ -62,6 +63,7 @@ from homeassistant.helpers.network import get_url from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass +from . import group as group_pre_import # noqa: F401 from .browse_media import BrowseMedia, async_process_play_media_url # noqa: F401 from .const import ( # noqa: F401 ATTR_APP_ID, @@ -781,7 +783,7 @@ class MediaPlayerEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): def turn_on(self) -> None: """Turn the media player on.""" - raise NotImplementedError() + raise NotImplementedError async def async_turn_on(self) -> None: """Turn the media player on.""" @@ -789,7 +791,7 @@ class MediaPlayerEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): def turn_off(self) -> None: """Turn the media player off.""" - raise NotImplementedError() + raise NotImplementedError async def async_turn_off(self) -> None: """Turn the media player off.""" @@ -797,7 +799,7 @@ class MediaPlayerEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): def mute_volume(self, mute: bool) -> None: """Mute the volume.""" - raise NotImplementedError() + raise NotImplementedError async def async_mute_volume(self, mute: bool) -> None: """Mute the volume.""" @@ -805,7 +807,7 @@ class MediaPlayerEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): def set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" - raise NotImplementedError() + raise NotImplementedError async def async_set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" @@ -813,7 +815,7 @@ class MediaPlayerEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): def media_play(self) -> None: """Send play command.""" - raise NotImplementedError() + raise NotImplementedError async def async_media_play(self) -> None: """Send play command.""" @@ -821,7 +823,7 @@ class MediaPlayerEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): def media_pause(self) -> None: """Send pause command.""" - raise NotImplementedError() + raise NotImplementedError async def async_media_pause(self) -> None: """Send pause command.""" @@ -829,7 +831,7 @@ class MediaPlayerEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): def media_stop(self) -> None: """Send stop command.""" - raise NotImplementedError() + raise NotImplementedError async def async_media_stop(self) -> None: """Send stop command.""" @@ -837,7 +839,7 @@ class MediaPlayerEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): def media_previous_track(self) -> None: """Send previous track command.""" - raise NotImplementedError() + raise NotImplementedError async def async_media_previous_track(self) -> None: """Send previous track command.""" @@ -845,7 +847,7 @@ class MediaPlayerEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): def media_next_track(self) -> None: """Send next track command.""" - raise NotImplementedError() + raise NotImplementedError async def async_media_next_track(self) -> None: """Send next track command.""" @@ -853,7 +855,7 @@ class MediaPlayerEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): def media_seek(self, position: float) -> None: """Send seek command.""" - raise NotImplementedError() + raise NotImplementedError async def async_media_seek(self, position: float) -> None: """Send seek command.""" @@ -863,7 +865,7 @@ class MediaPlayerEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): self, media_type: MediaType | str, media_id: str, **kwargs: Any ) -> None: """Play a piece of media.""" - raise NotImplementedError() + raise NotImplementedError async def async_play_media( self, media_type: MediaType | str, media_id: str, **kwargs: Any @@ -875,7 +877,7 @@ class MediaPlayerEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): def select_source(self, source: str) -> None: """Select input source.""" - raise NotImplementedError() + raise NotImplementedError async def async_select_source(self, source: str) -> None: """Select input source.""" @@ -883,7 +885,7 @@ class MediaPlayerEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): def select_sound_mode(self, sound_mode: str) -> None: """Select sound mode.""" - raise NotImplementedError() + raise NotImplementedError async def async_select_sound_mode(self, sound_mode: str) -> None: """Select sound mode.""" @@ -891,7 +893,7 @@ class MediaPlayerEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): def clear_playlist(self) -> None: """Clear players playlist.""" - raise NotImplementedError() + raise NotImplementedError async def async_clear_playlist(self) -> None: """Clear players playlist.""" @@ -899,7 +901,7 @@ class MediaPlayerEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): def set_shuffle(self, shuffle: bool) -> None: """Enable/disable shuffle mode.""" - raise NotImplementedError() + raise NotImplementedError async def async_set_shuffle(self, shuffle: bool) -> None: """Enable/disable shuffle mode.""" @@ -907,7 +909,7 @@ class MediaPlayerEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): def set_repeat(self, repeat: RepeatMode) -> None: """Set repeat mode.""" - raise NotImplementedError() + raise NotImplementedError async def async_set_repeat(self, repeat: RepeatMode) -> None: """Set repeat mode.""" @@ -1133,11 +1135,11 @@ class MediaPlayerEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): The BrowseMedia instance will be used by the "media_player/browse_media" websocket command. """ - raise NotImplementedError() + raise NotImplementedError def join_players(self, group_members: list[str]) -> None: """Join `group_members` as a player group with the current player.""" - raise NotImplementedError() + raise NotImplementedError async def async_join_players(self, group_members: list[str]) -> None: """Join `group_members` as a player group with the current player.""" @@ -1145,7 +1147,7 @@ class MediaPlayerEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): def unjoin_player(self) -> None: """Remove this player from any group.""" - raise NotImplementedError() + raise NotImplementedError async def async_unjoin_player(self) -> None: """Remove this player from any group.""" diff --git a/homeassistant/components/media_player/browse_media.py b/homeassistant/components/media_player/browse_media.py index 1e9be742c53..351d4e9140f 100644 --- a/homeassistant/components/media_player/browse_media.py +++ b/homeassistant/components/media_player/browse_media.py @@ -1,4 +1,5 @@ """Browse media features for media player.""" + from __future__ import annotations from collections.abc import Sequence diff --git a/homeassistant/components/media_player/const.py b/homeassistant/components/media_player/const.py index 2c609750153..9b69ee62846 100644 --- a/homeassistant/components/media_player/const.py +++ b/homeassistant/components/media_player/const.py @@ -1,4 +1,5 @@ """Provides the constants needed for component.""" + from enum import IntFlag, StrEnum # How long our auth signature on the content should be valid for diff --git a/homeassistant/components/media_player/device_condition.py b/homeassistant/components/media_player/device_condition.py index 5efee0c0b49..660f53bc8d5 100644 --- a/homeassistant/components/media_player/device_condition.py +++ b/homeassistant/components/media_player/device_condition.py @@ -1,4 +1,5 @@ """Provides device automations for Media player.""" + from __future__ import annotations import voluptuous as vol diff --git a/homeassistant/components/media_player/device_trigger.py b/homeassistant/components/media_player/device_trigger.py index e626059841c..9d1a3fab37e 100644 --- a/homeassistant/components/media_player/device_trigger.py +++ b/homeassistant/components/media_player/device_trigger.py @@ -1,4 +1,5 @@ """Provides device automations for Media player.""" + from __future__ import annotations import voluptuous as vol diff --git a/homeassistant/components/media_player/errors.py b/homeassistant/components/media_player/errors.py index 2e8443c2794..5888ba6b5b0 100644 --- a/homeassistant/components/media_player/errors.py +++ b/homeassistant/components/media_player/errors.py @@ -1,4 +1,5 @@ """Errors for the Media Player component.""" + from homeassistant.exceptions import HomeAssistantError diff --git a/homeassistant/components/media_player/group.py b/homeassistant/components/media_player/group.py index b7b2efb55c8..f4d465922af 100644 --- a/homeassistant/components/media_player/group.py +++ b/homeassistant/components/media_player/group.py @@ -1,7 +1,7 @@ """Describe group states.""" +from typing import TYPE_CHECKING -from homeassistant.components.group import GroupIntegrationRegistry from homeassistant.const import ( STATE_IDLE, STATE_OFF, @@ -11,10 +11,13 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback +if TYPE_CHECKING: + from homeassistant.components.group import GroupIntegrationRegistry + @callback def async_describe_on_off_states( - hass: HomeAssistant, registry: GroupIntegrationRegistry + hass: HomeAssistant, registry: "GroupIntegrationRegistry" ) -> None: """Describe group on off states.""" registry.on_off_states( diff --git a/homeassistant/components/media_player/icons.json b/homeassistant/components/media_player/icons.json index e2769085833..847ce5989d6 100644 --- a/homeassistant/components/media_player/icons.json +++ b/homeassistant/components/media_player/icons.json @@ -52,7 +52,7 @@ "unjoin": "mdi:ungroup", "volume_down": "mdi:volume-minus", "volume_mute": "mdi:volume-mute", - "volume_set": "mdi:volume", + "volume_set": "mdi:volume-medium", "volume_up": "mdi:volume-plus" } } diff --git a/homeassistant/components/media_player/reproduce_state.py b/homeassistant/components/media_player/reproduce_state.py index 387792a1b60..a40575a9dba 100644 --- a/homeassistant/components/media_player/reproduce_state.py +++ b/homeassistant/components/media_player/reproduce_state.py @@ -1,4 +1,5 @@ """Module that groups code required to handle state restore for component.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/media_player/significant_change.py b/homeassistant/components/media_player/significant_change.py index 43a253d9220..ea5cf9d1b27 100644 --- a/homeassistant/components/media_player/significant_change.py +++ b/homeassistant/components/media_player/significant_change.py @@ -1,4 +1,5 @@ """Helper to test significant Media Player state changes.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/media_source/__init__.py b/homeassistant/components/media_source/__init__.py index fdb7fa5f1f2..2f996523fdc 100644 --- a/homeassistant/components/media_source/__init__.py +++ b/homeassistant/components/media_source/__init__.py @@ -1,4 +1,5 @@ """The media_source integration.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/media_source/const.py b/homeassistant/components/media_source/const.py index 73599efb6c3..809e0d8a1fd 100644 --- a/homeassistant/components/media_source/const.py +++ b/homeassistant/components/media_source/const.py @@ -1,4 +1,5 @@ """Constants for the media_source integration.""" + import re from homeassistant.components.media_player import MediaClass diff --git a/homeassistant/components/media_source/error.py b/homeassistant/components/media_source/error.py index 00f3ced5d8d..120e7583e23 100644 --- a/homeassistant/components/media_source/error.py +++ b/homeassistant/components/media_source/error.py @@ -1,4 +1,5 @@ """Errors for media source.""" + from homeassistant.exceptions import HomeAssistantError diff --git a/homeassistant/components/media_source/local_source.py b/homeassistant/components/media_source/local_source.py index ac6623a3af8..8a67ae4a5b4 100644 --- a/homeassistant/components/media_source/local_source.py +++ b/homeassistant/components/media_source/local_source.py @@ -1,4 +1,5 @@ """Local Media Source Implementation.""" + from __future__ import annotations import logging @@ -218,21 +219,21 @@ class LocalMediaView(http.HomeAssistantView): try: raise_if_invalid_path(location) except ValueError as err: - raise web.HTTPBadRequest() from err + raise web.HTTPBadRequest from err if source_dir_id not in self.hass.config.media_dirs: - raise web.HTTPNotFound() + raise web.HTTPNotFound media_path = self.source.async_full_path(source_dir_id, location) # Check that the file exists if not media_path.is_file(): - raise web.HTTPNotFound() + raise web.HTTPNotFound # Check that it's a media file mime_type, _ = mimetypes.guess_type(str(media_path)) if not mime_type or mime_type.split("/")[0] not in MEDIA_MIME_TYPES: - raise web.HTTPNotFound() + raise web.HTTPNotFound return web.FileResponse(media_path) @@ -264,19 +265,19 @@ class UploadMediaView(http.HomeAssistantView): data = self.schema(dict(await request.post())) except vol.Invalid as err: LOGGER.error("Received invalid upload data: %s", err) - raise web.HTTPBadRequest() from err + raise web.HTTPBadRequest from err try: item = MediaSourceItem.from_uri(self.hass, data["media_content_id"], None) except ValueError as err: LOGGER.error("Received invalid upload data: %s", err) - raise web.HTTPBadRequest() from err + raise web.HTTPBadRequest from err try: source_dir_id, location = self.source.async_parse_identifier(item) except Unresolvable as err: LOGGER.error("Invalid local source ID") - raise web.HTTPBadRequest() from err + raise web.HTTPBadRequest from err uploaded_file: FileField = data["file"] @@ -288,7 +289,7 @@ class UploadMediaView(http.HomeAssistantView): raise_if_invalid_filename(uploaded_file.filename) except ValueError as err: LOGGER.error("Invalid filename") - raise web.HTTPBadRequest() from err + raise web.HTTPBadRequest from err try: await self.hass.async_add_executor_job( @@ -298,7 +299,7 @@ class UploadMediaView(http.HomeAssistantView): ) except ValueError as err: LOGGER.error("Moving upload failed: %s", err) - raise web.HTTPBadRequest() from err + raise web.HTTPBadRequest from err return self.json( {"media_content_id": f"{data['media_content_id']}/{uploaded_file.filename}"} diff --git a/homeassistant/components/media_source/models.py b/homeassistant/components/media_source/models.py index cbe71447a3f..482ed0e855f 100644 --- a/homeassistant/components/media_source/models.py +++ b/homeassistant/components/media_source/models.py @@ -1,4 +1,5 @@ """Media Source models.""" + from __future__ import annotations from abc import ABC diff --git a/homeassistant/components/mediaroom/media_player.py b/homeassistant/components/mediaroom/media_player.py index 4dc6d677abb..22417adcf51 100644 --- a/homeassistant/components/mediaroom/media_player.py +++ b/homeassistant/components/mediaroom/media_player.py @@ -1,4 +1,5 @@ """Support for the Mediaroom Set-up-box.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/melcloud/__init__.py b/homeassistant/components/melcloud/__init__.py index 2db3e79dfe9..30645661ff1 100644 --- a/homeassistant/components/melcloud/__init__.py +++ b/homeassistant/components/melcloud/__init__.py @@ -1,4 +1,5 @@ """The MELCloud Climate integration.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/melcloud/climate.py b/homeassistant/components/melcloud/climate.py index ed37ff76b76..4bf12650b82 100644 --- a/homeassistant/components/melcloud/climate.py +++ b/homeassistant/components/melcloud/climate.py @@ -1,4 +1,5 @@ """Platform for climate integration.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/melcloud/config_flow.py b/homeassistant/components/melcloud/config_flow.py index 88f658a0615..f071b64988d 100644 --- a/homeassistant/components/melcloud/config_flow.py +++ b/homeassistant/components/melcloud/config_flow.py @@ -1,4 +1,5 @@ """Config flow for the MELCloud platform.""" + from __future__ import annotations import asyncio @@ -11,9 +12,8 @@ from aiohttp import ClientError, ClientResponseError import pymelcloud import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN @@ -21,14 +21,14 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class FlowHandler(ConfigFlow, domain=DOMAIN): """Handle a config flow.""" VERSION = 1 - entry: config_entries.ConfigEntry | None = None + entry: ConfigEntry | None = None - async def _create_entry(self, username: str, token: str) -> FlowResult: + async def _create_entry(self, username: str, token: str) -> ConfigFlowResult: """Register new entry.""" await self.async_set_unique_id(username) self._abort_if_unique_id_configured({CONF_TOKEN: token}) @@ -42,7 +42,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): *, password: str | None = None, token: str | None = None, - ) -> FlowResult: + ) -> ConfigFlowResult: """Create client.""" try: async with asyncio.timeout(10): @@ -67,7 +67,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """User initiated config flow.""" if user_input is None: return self.async_show_form( @@ -79,14 +79,16 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): username = user_input[CONF_USERNAME] return await self._create_client(username, password=user_input[CONF_PASSWORD]) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Handle initiation of re-authentication with MELCloud.""" self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) return await self.async_step_reauth_confirm() async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle re-authentication with MELCloud.""" errors: dict[str, str] = {} diff --git a/homeassistant/components/melcloud/icons.json b/homeassistant/components/melcloud/icons.json new file mode 100644 index 00000000000..de3eb3c0ba2 --- /dev/null +++ b/homeassistant/components/melcloud/icons.json @@ -0,0 +1,13 @@ +{ + "entity": { + "sensor": { + "energy_consumed": { + "default": "mdi:factory" + } + } + }, + "services": { + "set_vane_horizontal": "mdi:arrow-left-right", + "set_vane_vertical": "mdi:arrow-up-down" + } +} diff --git a/homeassistant/components/melcloud/sensor.py b/homeassistant/components/melcloud/sensor.py index d3d1f4976f6..84585c556ca 100644 --- a/homeassistant/components/melcloud/sensor.py +++ b/homeassistant/components/melcloud/sensor.py @@ -1,4 +1,5 @@ """Support for MelCloud device sensors.""" + from __future__ import annotations from collections.abc import Callable @@ -23,26 +24,18 @@ from . import MelCloudDevice from .const import DOMAIN -@dataclasses.dataclass(frozen=True) -class MelcloudRequiredKeysMixin: - """Mixin for required keys.""" +@dataclasses.dataclass(frozen=True, kw_only=True) +class MelcloudSensorEntityDescription(SensorEntityDescription): + """Describes Melcloud sensor entity.""" value_fn: Callable[[Any], float] enabled: Callable[[Any], bool] -@dataclasses.dataclass(frozen=True) -class MelcloudSensorEntityDescription( - SensorEntityDescription, MelcloudRequiredKeysMixin -): - """Describes Melcloud sensor entity.""" - - ATA_SENSORS: tuple[MelcloudSensorEntityDescription, ...] = ( MelcloudSensorEntityDescription( key="room_temperature", translation_key="room_temperature", - icon="mdi:thermometer", native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, @@ -51,7 +44,7 @@ ATA_SENSORS: tuple[MelcloudSensorEntityDescription, ...] = ( ), MelcloudSensorEntityDescription( key="energy", - icon="mdi:factory", + translation_key="energy_consumed", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, @@ -63,7 +56,6 @@ ATW_SENSORS: tuple[MelcloudSensorEntityDescription, ...] = ( MelcloudSensorEntityDescription( key="outside_temperature", translation_key="outside_temperature", - icon="mdi:thermometer", native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, @@ -73,7 +65,6 @@ ATW_SENSORS: tuple[MelcloudSensorEntityDescription, ...] = ( MelcloudSensorEntityDescription( key="tank_temperature", translation_key="tank_temperature", - icon="mdi:thermometer", native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, @@ -85,7 +76,6 @@ ATW_ZONE_SENSORS: tuple[MelcloudSensorEntityDescription, ...] = ( MelcloudSensorEntityDescription( key="room_temperature", translation_key="room_temperature", - icon="mdi:thermometer", native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, @@ -95,7 +85,6 @@ ATW_ZONE_SENSORS: tuple[MelcloudSensorEntityDescription, ...] = ( MelcloudSensorEntityDescription( key="flow_temperature", translation_key="flow_temperature", - icon="mdi:thermometer", native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, @@ -105,7 +94,6 @@ ATW_ZONE_SENSORS: tuple[MelcloudSensorEntityDescription, ...] = ( MelcloudSensorEntityDescription( key="return_temperature", translation_key="return_temperature", - icon="mdi:thermometer", native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, diff --git a/homeassistant/components/melcloud/water_heater.py b/homeassistant/components/melcloud/water_heater.py index 210b8bd51e2..7d170430b04 100644 --- a/homeassistant/components/melcloud/water_heater.py +++ b/homeassistant/components/melcloud/water_heater.py @@ -1,4 +1,5 @@ """Platform for water_heater integration.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/melissa/__init__.py b/homeassistant/components/melissa/__init__.py index 10027999dda..9a9268e73d1 100644 --- a/homeassistant/components/melissa/__init__.py +++ b/homeassistant/components/melissa/__init__.py @@ -1,5 +1,6 @@ """Support for Melissa climate.""" -import melissa + +from melissa import AsyncMelissa import voluptuous as vol from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform @@ -30,7 +31,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: conf = config[DOMAIN] username = conf.get(CONF_USERNAME) password = conf.get(CONF_PASSWORD) - api = melissa.AsyncMelissa(username=username, password=password) + api = AsyncMelissa(username=username, password=password) await api.async_connect() hass.data[DATA_MELISSA] = api diff --git a/homeassistant/components/melissa/climate.py b/homeassistant/components/melissa/climate.py index f94c3af6d9a..fcb0820a6f0 100644 --- a/homeassistant/components/melissa/climate.py +++ b/homeassistant/components/melissa/climate.py @@ -1,4 +1,5 @@ """Support for Melissa Climate A/C.""" + from __future__ import annotations import logging @@ -43,13 +44,14 @@ async def async_setup_platform( api = hass.data[DATA_MELISSA] devices = (await api.async_fetch_devices()).values() - all_devices = [] - - for device in devices: - if device["type"] == "melissa": - all_devices.append(MelissaClimate(api, device["serial_number"], device)) - - async_add_entities(all_devices) + async_add_entities( + ( + MelissaClimate(api, device["serial_number"], device) + for device in devices + if device["type"] == "melissa" + ), + True, + ) class MelissaClimate(ClimateEntity): diff --git a/homeassistant/components/melnor/config_flow.py b/homeassistant/components/melnor/config_flow.py index 7aad59acf7b..223b30c7bb3 100644 --- a/homeassistant/components/melnor/config_flow.py +++ b/homeassistant/components/melnor/config_flow.py @@ -6,16 +6,15 @@ from typing import Any import voluptuous as vol -from homeassistant import config_entries from homeassistant.components.bluetooth import async_discovered_service_info from homeassistant.components.bluetooth.models import BluetoothServiceInfoBleak +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_ADDRESS -from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN, MANUFACTURER_DATA_START, MANUFACTURER_ID -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class MelnorConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for melnor.""" VERSION = 1 @@ -25,7 +24,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._discovered_address: str self._discovered_addresses: list[str] = [] - def _create_entry(self, address: str) -> FlowResult: + def _create_entry(self, address: str) -> ConfigFlowResult: """Create an entry for a discovered device.""" return self.async_create_entry( @@ -37,7 +36,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_bluetooth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle user-confirmation of discovered device.""" if user_input is not None: @@ -50,7 +49,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_bluetooth( self, discovery_info: BluetoothServiceInfoBleak - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by Bluetooth discovery.""" address = discovery_info.address @@ -65,7 +64,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_pick_device( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the step to pick discovered device.""" if user_input is not None: @@ -108,7 +107,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" return await self.async_step_pick_device() diff --git a/homeassistant/components/melnor/const.py b/homeassistant/components/melnor/const.py index cadf9c0a618..be6931799af 100644 --- a/homeassistant/components/melnor/const.py +++ b/homeassistant/components/melnor/const.py @@ -1,6 +1,5 @@ """Constants for the melnor integration.""" - DOMAIN = "melnor" DEFAULT_NAME = "Melnor Bluetooth" diff --git a/homeassistant/components/melnor/icons.json b/homeassistant/components/melnor/icons.json new file mode 100644 index 00000000000..72e479a7d5a --- /dev/null +++ b/homeassistant/components/melnor/icons.json @@ -0,0 +1,23 @@ +{ + "entity": { + "number": { + "manual_minutes": { + "default": "mdi:timer-cog-outline" + }, + "frequency_interval_hours": { + "default": "mdi:calendar-refresh-outline" + }, + "frequency_duration_minutes": { + "default": "mdi:timer-outline" + } + }, + "switch": { + "manual": { + "default": "mdi:sprinkler" + }, + "frequency": { + "default": "mdi:calendar-sync-outline" + } + } + } +} diff --git a/homeassistant/components/melnor/models.py b/homeassistant/components/melnor/models.py index beb8b42a4a3..ffcccccb789 100644 --- a/homeassistant/components/melnor/models.py +++ b/homeassistant/components/melnor/models.py @@ -117,14 +117,15 @@ def get_entities_for_valves( ], ) -> list[CoordinatorEntity[MelnorDataUpdateCoordinator]]: """Get descriptions for valves.""" - entities = [] + entities: list[CoordinatorEntity[MelnorDataUpdateCoordinator]] = [] # This device may not have 4 valves total, but the library will only expose the right number of valves for i in range(1, 5): valve = coordinator.data[f"zone{i}"] if valve is not None: - for description in descriptions: - entities.append(function(valve, description)) + entities.extend( + function(valve, description) for description in descriptions + ) return entities diff --git a/homeassistant/components/melnor/number.py b/homeassistant/components/melnor/number.py index caf2d499851..33d9fa443b1 100644 --- a/homeassistant/components/melnor/number.py +++ b/homeassistant/components/melnor/number.py @@ -26,27 +26,19 @@ from .models import ( ) -@dataclass(frozen=True) -class MelnorZoneNumberEntityDescriptionMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class MelnorZoneNumberEntityDescription(NumberEntityDescription): + """Describes Melnor number entity.""" set_num_fn: Callable[[Valve, int], Coroutine[Any, Any, None]] state_fn: Callable[[Valve], Any] -@dataclass(frozen=True) -class MelnorZoneNumberEntityDescription( - NumberEntityDescription, MelnorZoneNumberEntityDescriptionMixin -): - """Describes Melnor number entity.""" - - ZONE_ENTITY_DESCRIPTIONS: list[MelnorZoneNumberEntityDescription] = [ MelnorZoneNumberEntityDescription( entity_category=EntityCategory.CONFIG, native_max_value=360, native_min_value=1, - icon="mdi:timer-cog-outline", key="manual_minutes", translation_key="manual_minutes", native_unit_of_measurement=UnitOfTime.MINUTES, @@ -57,7 +49,6 @@ ZONE_ENTITY_DESCRIPTIONS: list[MelnorZoneNumberEntityDescription] = [ entity_category=EntityCategory.CONFIG, native_max_value=168, native_min_value=1, - icon="mdi:calendar-refresh-outline", key="frequency_interval_hours", translation_key="frequency_interval_hours", native_unit_of_measurement=UnitOfTime.HOURS, @@ -68,7 +59,6 @@ ZONE_ENTITY_DESCRIPTIONS: list[MelnorZoneNumberEntityDescription] = [ entity_category=EntityCategory.CONFIG, native_max_value=360, native_min_value=1, - icon="mdi:timer-outline", key="frequency_duration_minutes", translation_key="frequency_duration_minutes", native_unit_of_measurement=UnitOfTime.MINUTES, diff --git a/homeassistant/components/melnor/sensor.py b/homeassistant/components/melnor/sensor.py index 255c3c9747d..6528773d9d8 100644 --- a/homeassistant/components/melnor/sensor.py +++ b/homeassistant/components/melnor/sensor.py @@ -1,4 +1,5 @@ """Sensor support for Melnor Bluetooth water timer.""" + from __future__ import annotations from collections.abc import Callable @@ -54,32 +55,18 @@ def next_cycle(valve: Valve) -> datetime | None: return None -@dataclass(frozen=True) -class MelnorSensorEntityDescriptionMixin: - """Mixin for required keys.""" - - state_fn: Callable[[Device], Any] - - -@dataclass(frozen=True) -class MelnorZoneSensorEntityDescriptionMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class MelnorZoneSensorEntityDescription(SensorEntityDescription): + """Describes Melnor sensor entity.""" state_fn: Callable[[Valve], Any] -@dataclass(frozen=True) -class MelnorZoneSensorEntityDescription( - SensorEntityDescription, MelnorZoneSensorEntityDescriptionMixin -): +@dataclass(frozen=True, kw_only=True) +class MelnorSensorEntityDescription(SensorEntityDescription): """Describes Melnor sensor entity.""" - -@dataclass(frozen=True) -class MelnorSensorEntityDescription( - SensorEntityDescription, MelnorSensorEntityDescriptionMixin -): - """Describes Melnor sensor entity.""" + state_fn: Callable[[Device], Any] DEVICE_ENTITY_DESCRIPTIONS: list[MelnorSensorEntityDescription] = [ diff --git a/homeassistant/components/melnor/switch.py b/homeassistant/components/melnor/switch.py index e3c0e0afa15..f912db1e981 100644 --- a/homeassistant/components/melnor/switch.py +++ b/homeassistant/components/melnor/switch.py @@ -25,33 +25,25 @@ from .models import ( ) -@dataclass(frozen=True) -class MelnorSwitchEntityDescriptionMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class MelnorSwitchEntityDescription(SwitchEntityDescription): + """Describes Melnor switch entity.""" on_off_fn: Callable[[Valve, bool], Coroutine[Any, Any, None]] state_fn: Callable[[Valve], Any] -@dataclass(frozen=True) -class MelnorSwitchEntityDescription( - SwitchEntityDescription, MelnorSwitchEntityDescriptionMixin -): - """Describes Melnor switch entity.""" - - ZONE_ENTITY_DESCRIPTIONS = [ MelnorSwitchEntityDescription( device_class=SwitchDeviceClass.SWITCH, - icon="mdi:sprinkler", key="manual", + translation_key="manual", name=None, on_off_fn=lambda valve, bool: valve.set_is_watering(bool), state_fn=lambda valve: valve.is_watering, ), MelnorSwitchEntityDescription( device_class=SwitchDeviceClass.SWITCH, - icon="mdi:calendar-sync-outline", key="frequency", translation_key="frequency", on_off_fn=lambda valve, bool: valve.set_frequency_enabled(bool), diff --git a/homeassistant/components/melnor/time.py b/homeassistant/components/melnor/time.py index 36afe2d976d..d2d05f6517f 100644 --- a/homeassistant/components/melnor/time.py +++ b/homeassistant/components/melnor/time.py @@ -23,21 +23,14 @@ from .models import ( ) -@dataclass(frozen=True) -class MelnorZoneTimeEntityDescriptionMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class MelnorZoneTimeEntityDescription(TimeEntityDescription): + """Describes Melnor number entity.""" set_time_fn: Callable[[Valve, time], Coroutine[Any, Any, None]] state_fn: Callable[[Valve], Any] -@dataclass(frozen=True) -class MelnorZoneTimeEntityDescription( - TimeEntityDescription, MelnorZoneTimeEntityDescriptionMixin -): - """Describes Melnor number entity.""" - - ZONE_ENTITY_DESCRIPTIONS: list[MelnorZoneTimeEntityDescription] = [ MelnorZoneTimeEntityDescription( entity_category=EntityCategory.CONFIG, diff --git a/homeassistant/components/meraki/device_tracker.py b/homeassistant/components/meraki/device_tracker.py index dae98734ae7..58da08d984c 100644 --- a/homeassistant/components/meraki/device_tracker.py +++ b/homeassistant/components/meraki/device_tracker.py @@ -1,4 +1,5 @@ """Support for the Meraki CMX location service.""" + from __future__ import annotations from http import HTTPStatus @@ -12,7 +13,7 @@ from homeassistant.components.device_tracker import ( AsyncSeeCallback, SourceType, ) -from homeassistant.components.http import HomeAssistantView +from homeassistant.components.http import KEY_HASS, HomeAssistantView from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -86,7 +87,7 @@ class MerakiView(HomeAssistantView): if not data["data"]["observations"]: _LOGGER.debug("No observations found") return - self._handle(request.app["hass"], data) + self._handle(request.app[KEY_HASS], data) @callback def _handle(self, hass, data): diff --git a/homeassistant/components/message_bird/notify.py b/homeassistant/components/message_bird/notify.py index 241646918c6..b1b7b373e6a 100644 --- a/homeassistant/components/message_bird/notify.py +++ b/homeassistant/components/message_bird/notify.py @@ -1,4 +1,5 @@ """MessageBird platform for notify component.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/met/__init__.py b/homeassistant/components/met/__init__.py index a10a07b5374..ec402a16489 100644 --- a/homeassistant/components/met/__init__.py +++ b/homeassistant/components/met/__init__.py @@ -1,4 +1,5 @@ """The met component.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/met/config_flow.py b/homeassistant/components/met/config_flow.py index ac614e4691b..84a44682413 100644 --- a/homeassistant/components/met/config_flow.py +++ b/homeassistant/components/met/config_flow.py @@ -1,12 +1,18 @@ """Config flow to configure Met component.""" + from __future__ import annotations from typing import Any import voluptuous as vol -from homeassistant import config_entries -from homeassistant.config_entries import OptionsFlowWithConfigEntry +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, + OptionsFlowWithConfigEntry, +) from homeassistant.const import ( CONF_ELEVATION, CONF_LATITUDE, @@ -15,7 +21,6 @@ from homeassistant.const import ( UnitOfLength, ) from homeassistant.core import HomeAssistant, callback -from homeassistant.data_entry_flow import FlowResult import homeassistant.helpers.config_validation as cv from homeassistant.helpers.selector import ( NumberSelector, @@ -47,7 +52,7 @@ def configured_instances(hass: HomeAssistant) -> set[str]: def _get_data_schema( - hass: HomeAssistant, config_entry: config_entries.ConfigEntry | None = None + hass: HomeAssistant, config_entry: ConfigEntry | None = None ) -> vol.Schema: """Get a schema with default values.""" # If tracking home or no config entry is passed in, default value come from Home location @@ -91,14 +96,14 @@ def _get_data_schema( ) -class MetConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class MetConfigFlowHandler(ConfigFlow, domain=DOMAIN): """Config flow for Met component.""" VERSION = 1 async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" errors = {} @@ -120,7 +125,7 @@ class MetConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_onboarding( self, data: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by onboarding.""" # Don't create entry if latitude or longitude isn't set. # Also, filters out our onboarding default location. @@ -137,8 +142,8 @@ class MetConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback def async_get_options_flow( - config_entry: config_entries.ConfigEntry, - ) -> config_entries.OptionsFlow: + config_entry: ConfigEntry, + ) -> OptionsFlow: """Get the options flow for Met.""" return MetOptionsFlowHandler(config_entry) @@ -148,7 +153,7 @@ class MetOptionsFlowHandler(OptionsFlowWithConfigEntry): async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Configure options for Met.""" if user_input is not None: diff --git a/homeassistant/components/met/const.py b/homeassistant/components/met/const.py index b690f1b6723..c513e98504e 100644 --- a/homeassistant/components/met/const.py +++ b/homeassistant/components/met/const.py @@ -1,4 +1,5 @@ """Constants for Met component.""" + from homeassistant.components.weather import ( ATTR_CONDITION_CLEAR_NIGHT, ATTR_CONDITION_CLOUDY, diff --git a/homeassistant/components/met/coordinator.py b/homeassistant/components/met/coordinator.py index 6354e286cee..ef73e1b52ab 100644 --- a/homeassistant/components/met/coordinator.py +++ b/homeassistant/components/met/coordinator.py @@ -1,4 +1,5 @@ """DataUpdateCoordinator for Met.no integration.""" + from __future__ import annotations from collections.abc import Callable @@ -77,7 +78,7 @@ class MetWeatherData: """Fetch data from API - (current weather and forecast).""" resp = await self._weather_data.fetching_data() if not resp: - raise CannotConnect() + raise CannotConnect self.current_weather_data = self._weather_data.get_current_weather() time_zone = dt_util.DEFAULT_TIME_ZONE self.daily_forecast = self._weather_data.get_forecast(time_zone, False, 0) diff --git a/homeassistant/components/met/manifest.json b/homeassistant/components/met/manifest.json index a3190109cac..e900c5a012a 100644 --- a/homeassistant/components/met/manifest.json +++ b/homeassistant/components/met/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/met", "iot_class": "cloud_polling", "loggers": ["metno"], - "requirements": ["PyMetno==0.11.0"] + "requirements": ["PyMetno==0.12.0"] } diff --git a/homeassistant/components/met/weather.py b/homeassistant/components/met/weather.py index e4a63e326a6..d0ee4f275ea 100644 --- a/homeassistant/components/met/weather.py +++ b/homeassistant/components/met/weather.py @@ -1,4 +1,5 @@ """Support for Met.no weather service.""" + from __future__ import annotations from types import MappingProxyType @@ -230,11 +231,6 @@ class MetWeather(SingleCoordinatorWeatherEntity[MetDataUpdateCoordinator]): ha_forecast.append(ha_item) # type: ignore[arg-type] return ha_forecast - @property - def forecast(self) -> list[Forecast] | None: - """Return the forecast array.""" - return self._forecast(False) - @callback def _async_forecast_daily(self) -> list[Forecast] | None: """Return the daily forecast in native units.""" diff --git a/homeassistant/components/met_eireann/__init__.py b/homeassistant/components/met_eireann/__init__.py index 5edecbbac0b..92f2ffcfac6 100644 --- a/homeassistant/components/met_eireann/__init__.py +++ b/homeassistant/components/met_eireann/__init__.py @@ -1,4 +1,5 @@ """The met_eireann component.""" + from datetime import timedelta import logging from types import MappingProxyType diff --git a/homeassistant/components/met_eireann/config_flow.py b/homeassistant/components/met_eireann/config_flow.py index b4c0102b97e..422b46827da 100644 --- a/homeassistant/components/met_eireann/config_flow.py +++ b/homeassistant/components/met_eireann/config_flow.py @@ -1,24 +1,24 @@ """Config flow to configure Met Éireann component.""" + from typing import Any import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME -from homeassistant.data_entry_flow import FlowResult import homeassistant.helpers.config_validation as cv from .const import DOMAIN, HOME_LOCATION_NAME -class MetEireannFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class MetEireannFlowHandler(ConfigFlow, domain=DOMAIN): """Config flow for Met Eireann component.""" VERSION = 1 async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" if user_input is not None: # Check if an identical entity is already configured diff --git a/homeassistant/components/met_eireann/const.py b/homeassistant/components/met_eireann/const.py index 9316aad1b17..3a4c3dda507 100644 --- a/homeassistant/components/met_eireann/const.py +++ b/homeassistant/components/met_eireann/const.py @@ -1,4 +1,5 @@ """Constants for Met Éireann component.""" + from homeassistant.components.weather import ( ATTR_CONDITION_CLEAR_NIGHT, ATTR_CONDITION_CLOUDY, diff --git a/homeassistant/components/met_eireann/weather.py b/homeassistant/components/met_eireann/weather.py index 84fc20cead7..404ef5d8393 100644 --- a/homeassistant/components/met_eireann/weather.py +++ b/homeassistant/components/met_eireann/weather.py @@ -1,4 +1,5 @@ """Support for Met Éireann weather service.""" + import logging from types import MappingProxyType from typing import Any, cast @@ -52,17 +53,15 @@ async def async_setup_entry( coordinator = hass.data[DOMAIN][config_entry.entry_id] entity_registry = er.async_get(hass) - entities = [MetEireannWeather(coordinator, config_entry.data, False)] - - # Add hourly entity to legacy config entries - if entity_registry.async_get_entity_id( + # Remove hourly entity from legacy config entries + if entity_id := entity_registry.async_get_entity_id( WEATHER_DOMAIN, DOMAIN, _calculate_unique_id(config_entry.data, True), ): - entities.append(MetEireannWeather(coordinator, config_entry.data, True)) + entity_registry.async_remove(entity_id) - async_add_entities(entities) + async_add_entities([MetEireannWeather(coordinator, config_entry.data)]) def _calculate_unique_id(config: MappingProxyType[str, Any], hourly: bool) -> str: @@ -92,19 +91,15 @@ class MetEireannWeather( self, coordinator: DataUpdateCoordinator[MetEireannWeatherData], config: MappingProxyType[str, Any], - hourly: bool, ) -> None: """Initialise the platform with a data instance and site.""" super().__init__(coordinator) - self._attr_unique_id = _calculate_unique_id(config, hourly) + self._attr_unique_id = _calculate_unique_id(config, False) self._config = config - self._hourly = hourly - name_appendix = " Hourly" if hourly else "" if (name := self._config.get(CONF_NAME)) is not None: - self._attr_name = f"{name}{name_appendix}" + self._attr_name = name else: - self._attr_name = f"{DEFAULT_NAME}{name_appendix}" - self._attr_entity_registry_enabled_default = not hourly + self._attr_name = DEFAULT_NAME self._attr_device_info = DeviceInfo( name="Forecast", entry_type=DeviceEntryType.SERVICE, @@ -178,11 +173,6 @@ class MetEireannWeather( ha_forecast.append(ha_item) return ha_forecast - @property - def forecast(self) -> list[Forecast]: - """Return the forecast array.""" - return self._forecast(self._hourly) - @callback def _async_forecast_daily(self) -> list[Forecast]: """Return the daily forecast in native units.""" diff --git a/homeassistant/components/meteo_france/__init__.py b/homeassistant/components/meteo_france/__init__.py index 3f1cd2a5e34..ddba982934c 100644 --- a/homeassistant/components/meteo_france/__init__.py +++ b/homeassistant/components/meteo_france/__init__.py @@ -1,4 +1,5 @@ """Support for Meteo-France weather data.""" + from datetime import timedelta import logging @@ -53,6 +54,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def _async_update_data_alert() -> CurrentPhenomenons: """Fetch data from API endpoint.""" + assert isinstance(department, str) return await hass.async_add_executor_job( client.get_warning_current_phenomenoms, department, 0, True ) diff --git a/homeassistant/components/meteo_france/config_flow.py b/homeassistant/components/meteo_france/config_flow.py index a3001ee25c0..37995534fb1 100644 --- a/homeassistant/components/meteo_france/config_flow.py +++ b/homeassistant/components/meteo_france/config_flow.py @@ -1,4 +1,5 @@ """Config flow to configure the Meteo-France integration.""" + from __future__ import annotations import logging @@ -8,18 +9,16 @@ from meteofrance_api.client import MeteoFranceClient from meteofrance_api.model import Place import voluptuous as vol -from homeassistant import config_entries -from homeassistant.config_entries import SOURCE_IMPORT +from homeassistant.config_entries import SOURCE_IMPORT, ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult from .const import CONF_CITY, DOMAIN _LOGGER = logging.getLogger(__name__) -class MeteoFranceFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class MeteoFranceFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a Meteo-France config flow.""" VERSION = 1 @@ -33,7 +32,7 @@ class MeteoFranceFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None, errors: dict[str, str] | None = None, - ) -> FlowResult: + ) -> ConfigFlowResult: """Show the setup form to the user.""" if user_input is None: @@ -49,7 +48,7 @@ class MeteoFranceFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initiated by the user.""" errors: dict[str, str] = {} @@ -83,7 +82,7 @@ class MeteoFranceFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_cities( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Step where the user choose the city from the API search results.""" if not user_input: if len(self.places) > 1 and self.source != SOURCE_IMPORT: diff --git a/homeassistant/components/meteo_france/const.py b/homeassistant/components/meteo_france/const.py index e950dfe1fa8..2230f43b754 100644 --- a/homeassistant/components/meteo_france/const.py +++ b/homeassistant/components/meteo_france/const.py @@ -1,4 +1,5 @@ """Meteo-France component constants.""" + from __future__ import annotations from homeassistant.components.weather import ( diff --git a/homeassistant/components/meteo_france/sensor.py b/homeassistant/components/meteo_france/sensor.py index c5ff38f2a87..23ea6bb1500 100644 --- a/homeassistant/components/meteo_france/sensor.py +++ b/homeassistant/components/meteo_france/sensor.py @@ -1,4 +1,5 @@ """Support for Meteo-France raining forecast sensor.""" + from __future__ import annotations from dataclasses import dataclass @@ -51,20 +52,13 @@ from .const import ( _DataT = TypeVar("_DataT", bound=Rain | Forecast | CurrentPhenomenons) -@dataclass(frozen=True) -class MeteoFranceRequiredKeysMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class MeteoFranceSensorEntityDescription(SensorEntityDescription): + """Describes Meteo-France sensor entity.""" data_path: str -@dataclass(frozen=True) -class MeteoFranceSensorEntityDescription( - SensorEntityDescription, MeteoFranceRequiredKeysMixin -): - """Describes Meteo-France sensor entity.""" - - SENSOR_TYPES: tuple[MeteoFranceSensorEntityDescription, ...] = ( MeteoFranceSensorEntityDescription( key="pressure", diff --git a/homeassistant/components/meteo_france/weather.py b/homeassistant/components/meteo_france/weather.py index 79e35b6219f..9edc557aafc 100644 --- a/homeassistant/components/meteo_france/weather.py +++ b/homeassistant/components/meteo_france/weather.py @@ -1,4 +1,5 @@ """Support for Meteo-France weather service.""" + import logging import time @@ -215,11 +216,6 @@ class MeteoFranceWeather( ) return forecast_data - @property - def forecast(self) -> list[Forecast]: - """Return the forecast array.""" - return self._forecast(self._mode) - async def async_forecast_daily(self) -> list[Forecast]: """Return the daily forecast in native units.""" return self._forecast(FORECAST_MODE_DAILY) diff --git a/homeassistant/components/meteoalarm/binary_sensor.py b/homeassistant/components/meteoalarm/binary_sensor.py index 246074ce585..8b38ac6dbb3 100644 --- a/homeassistant/components/meteoalarm/binary_sensor.py +++ b/homeassistant/components/meteoalarm/binary_sensor.py @@ -1,4 +1,5 @@ """Binary Sensor for MeteoAlarm.eu.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/meteoclimatic/__init__.py b/homeassistant/components/meteoclimatic/__init__.py index 7510c4bec4c..1e729258218 100644 --- a/homeassistant/components/meteoclimatic/__init__.py +++ b/homeassistant/components/meteoclimatic/__init__.py @@ -1,4 +1,5 @@ """Support for Meteoclimatic weather data.""" + import logging from meteoclimatic import MeteoclimaticClient diff --git a/homeassistant/components/meteoclimatic/config_flow.py b/homeassistant/components/meteoclimatic/config_flow.py index 49be3889ead..d772a6c9d62 100644 --- a/homeassistant/components/meteoclimatic/config_flow.py +++ b/homeassistant/components/meteoclimatic/config_flow.py @@ -1,18 +1,19 @@ """Config flow to configure the Meteoclimatic integration.""" + import logging from meteoclimatic import MeteoclimaticClient from meteoclimatic.exceptions import MeteoclimaticError, StationNotFound import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow from .const import CONF_STATION_CODE, DOMAIN _LOGGER = logging.getLogger(__name__) -class MeteoclimaticFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class MeteoclimaticFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a Meteoclimatic config flow.""" VERSION = 1 diff --git a/homeassistant/components/meteoclimatic/const.py b/homeassistant/components/meteoclimatic/const.py index 4a7276d4e42..3d8f93d014d 100644 --- a/homeassistant/components/meteoclimatic/const.py +++ b/homeassistant/components/meteoclimatic/const.py @@ -1,4 +1,5 @@ """Meteoclimatic component constants.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/meteoclimatic/sensor.py b/homeassistant/components/meteoclimatic/sensor.py index 9a54e766945..2194f82e43e 100644 --- a/homeassistant/components/meteoclimatic/sensor.py +++ b/homeassistant/components/meteoclimatic/sensor.py @@ -1,4 +1,5 @@ """Support for Meteoclimatic sensor.""" + from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, diff --git a/homeassistant/components/meteoclimatic/weather.py b/homeassistant/components/meteoclimatic/weather.py index f9b341cf114..75a93689efa 100644 --- a/homeassistant/components/meteoclimatic/weather.py +++ b/homeassistant/components/meteoclimatic/weather.py @@ -1,4 +1,5 @@ """Support for Meteoclimatic weather service.""" + from meteoclimatic import Condition from homeassistant.components.weather import WeatherEntity diff --git a/homeassistant/components/metoffice/__init__.py b/homeassistant/components/metoffice/__init__.py index a658de9a024..18fc121d5d3 100644 --- a/homeassistant/components/metoffice/__init__.py +++ b/homeassistant/components/metoffice/__init__.py @@ -1,4 +1,5 @@ """The Met Office integration.""" + from __future__ import annotations import asyncio @@ -93,7 +94,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: fetch_site, connection, latitude, longitude ) if site is None: - raise ConfigEntryNotReady() + raise ConfigEntryNotReady async def async_update_3hourly() -> MetOfficeData: return await hass.async_add_executor_job( diff --git a/homeassistant/components/metoffice/config_flow.py b/homeassistant/components/metoffice/config_flow.py index 3cf3b0fcda0..8b3c10cd460 100644 --- a/homeassistant/components/metoffice/config_flow.py +++ b/homeassistant/components/metoffice/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Met Office integration.""" + from __future__ import annotations import logging @@ -7,9 +8,10 @@ from typing import Any import datapoint import voluptuous as vol -from homeassistant import config_entries, core, exceptions +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME -from homeassistant.data_entry_flow import FlowResult +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv from .const import DOMAIN @@ -18,9 +20,7 @@ from .helpers import fetch_site _LOGGER = logging.getLogger(__name__) -async def validate_input( - hass: core.HomeAssistant, data: dict[str, Any] -) -> dict[str, str]: +async def validate_input(hass: 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. @@ -36,19 +36,19 @@ async def validate_input( ) if site is None: - raise CannotConnect() + raise CannotConnect return {"site_name": site.name} -class MetOfficeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class MetOfficeConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Met Office weather integration.""" VERSION = 1 async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors = {} if user_input is not None: @@ -87,5 +87,5 @@ class MetOfficeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -class CannotConnect(exceptions.HomeAssistantError): +class CannotConnect(HomeAssistantError): """Error to indicate we cannot connect.""" diff --git a/homeassistant/components/metoffice/const.py b/homeassistant/components/metoffice/const.py index 8b86784b70b..966aec7d381 100644 --- a/homeassistant/components/metoffice/const.py +++ b/homeassistant/components/metoffice/const.py @@ -1,4 +1,5 @@ """Constants for Met Office Integration.""" + from datetime import timedelta from homeassistant.components.weather import ( diff --git a/homeassistant/components/metoffice/data.py b/homeassistant/components/metoffice/data.py index c6bb2b4c01b..651e56c3adc 100644 --- a/homeassistant/components/metoffice/data.py +++ b/homeassistant/components/metoffice/data.py @@ -1,4 +1,5 @@ """Common Met Office Data class used by both sensor and entity.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/homeassistant/components/metoffice/helpers.py b/homeassistant/components/metoffice/helpers.py index 5b698bf19da..56d4d8f971b 100644 --- a/homeassistant/components/metoffice/helpers.py +++ b/homeassistant/components/metoffice/helpers.py @@ -1,4 +1,5 @@ """Helpers used for Met Office integration.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/metoffice/sensor.py b/homeassistant/components/metoffice/sensor.py index 84a51a0d584..61f825abdc3 100644 --- a/homeassistant/components/metoffice/sensor.py +++ b/homeassistant/components/metoffice/sensor.py @@ -1,4 +1,5 @@ """Support for UK Met Office weather service.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/metoffice/weather.py b/homeassistant/components/metoffice/weather.py index b6e35168276..33fec874611 100644 --- a/homeassistant/components/metoffice/weather.py +++ b/homeassistant/components/metoffice/weather.py @@ -1,4 +1,5 @@ """Support for UK Met Office weather service.""" + from __future__ import annotations from typing import Any, cast @@ -44,31 +45,24 @@ async def async_setup_entry( entity_registry = er.async_get(hass) hass_data = hass.data[DOMAIN][entry.entry_id] - entities = [ - MetOfficeWeather( - hass_data[METOFFICE_DAILY_COORDINATOR], - hass_data[METOFFICE_HOURLY_COORDINATOR], - hass_data, - False, - ) - ] - - # Add hourly entity to legacy config entries - if entity_registry.async_get_entity_id( + # Remove hourly entity from legacy config entries + if entity_id := entity_registry.async_get_entity_id( WEATHER_DOMAIN, DOMAIN, _calculate_unique_id(hass_data[METOFFICE_COORDINATES], True), ): - entities.append( + entity_registry.async_remove(entity_id) + + async_add_entities( + [ MetOfficeWeather( hass_data[METOFFICE_DAILY_COORDINATOR], hass_data[METOFFICE_HOURLY_COORDINATOR], hass_data, - True, ) - ) - - async_add_entities(entities, False) + ], + False, + ) def _build_forecast_data(timestep: Timestep) -> Forecast: @@ -118,14 +112,9 @@ class MetOfficeWeather( coordinator_daily: TimestampDataUpdateCoordinator[MetOfficeData], coordinator_hourly: TimestampDataUpdateCoordinator[MetOfficeData], hass_data: dict[str, Any], - use_3hourly: bool, ) -> None: """Initialise the platform with a data instance.""" - self._hourly = use_3hourly - if use_3hourly: - observation_coordinator = coordinator_hourly - else: - observation_coordinator = coordinator_daily + observation_coordinator = coordinator_daily super().__init__( observation_coordinator, daily_coordinator=coordinator_daily, @@ -135,9 +124,9 @@ class MetOfficeWeather( self._attr_device_info = get_device_info( coordinates=hass_data[METOFFICE_COORDINATES], name=hass_data[METOFFICE_NAME] ) - self._attr_name = "3-Hourly" if use_3hourly else "Daily" + self._attr_name = "Daily" self._attr_unique_id = _calculate_unique_id( - hass_data[METOFFICE_COORDINATES], use_3hourly + hass_data[METOFFICE_COORDINATES], False ) @property @@ -192,14 +181,6 @@ class MetOfficeWeather( return str(value) if value is not None else None return None - @property - def forecast(self) -> list[Forecast] | None: - """Return the forecast array.""" - return [ - _build_forecast_data(timestep) - for timestep in self.coordinator.data.forecast - ] - @callback def _async_forecast_daily(self) -> list[Forecast] | None: """Return the twice daily forecast in native units.""" diff --git a/homeassistant/components/mfi/sensor.py b/homeassistant/components/mfi/sensor.py index 84f4abdaead..afa5e00bf02 100644 --- a/homeassistant/components/mfi/sensor.py +++ b/homeassistant/components/mfi/sensor.py @@ -1,4 +1,5 @@ """Support for Ubiquiti mFi sensors.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/mfi/switch.py b/homeassistant/components/mfi/switch.py index a25d0dfc87b..fe0aeb902ee 100644 --- a/homeassistant/components/mfi/switch.py +++ b/homeassistant/components/mfi/switch.py @@ -1,4 +1,5 @@ """Support for Ubiquiti mFi switches.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/microbees/binary_sensor.py b/homeassistant/components/microbees/binary_sensor.py new file mode 100644 index 00000000000..551f68ba354 --- /dev/null +++ b/homeassistant/components/microbees/binary_sensor.py @@ -0,0 +1,83 @@ +"""BinarySensor integration microBees.""" + +from microBeesPy import Sensor + +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import MicroBeesUpdateCoordinator +from .entity import MicroBeesEntity + +BINARYSENSOR_TYPES = { + 12: BinarySensorEntityDescription( + device_class=BinarySensorDeviceClass.MOTION, + key="motion_sensor", + ), + 13: BinarySensorEntityDescription( + device_class=BinarySensorDeviceClass.DOOR, + key="door_sensor", + ), + 19: BinarySensorEntityDescription( + device_class=BinarySensorDeviceClass.MOISTURE, + key="moisture_sensor", + ), + 20: BinarySensorEntityDescription( + device_class=BinarySensorDeviceClass.SMOKE, + key="smoke_sensor", + ), +} + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up the microBees binary sensor platform.""" + coordinator: MicroBeesUpdateCoordinator = hass.data[DOMAIN][ + entry.entry_id + ].coordinator + async_add_entities( + MBBinarySensor(coordinator, entity_description, bee_id, binary_sensor.id) + for bee_id, bee in coordinator.data.bees.items() + for binary_sensor in bee.sensors + if (entity_description := BINARYSENSOR_TYPES.get(binary_sensor.device_type)) + is not None + ) + + +class MBBinarySensor(MicroBeesEntity, BinarySensorEntity): + """Representation of a microBees BinarySensor.""" + + def __init__( + self, + coordinator: MicroBeesUpdateCoordinator, + entity_description: BinarySensorEntityDescription, + bee_id: int, + sensor_id: int, + ) -> None: + """Initialize the microBees BinarySensor.""" + super().__init__(coordinator, bee_id) + self._attr_unique_id = f"{bee_id}_{sensor_id}" + self.sensor_id = sensor_id + self.entity_description = entity_description + + @property + def name(self) -> str: + """Name of the BinarySensor.""" + return self.sensor.name + + @property + def is_on(self) -> bool: + """Return the state of the BinarySensor.""" + return self.sensor.value + + @property + def sensor(self) -> Sensor: + """Return the BinarySensor.""" + return self.coordinator.data.sensors[self.sensor_id] diff --git a/homeassistant/components/microbees/button.py b/homeassistant/components/microbees/button.py index cf82a60bfa4..f449fa9afee 100644 --- a/homeassistant/components/microbees/button.py +++ b/homeassistant/components/microbees/button.py @@ -1,4 +1,5 @@ """Button integration microBees.""" + from typing import Any from homeassistant.components.button import ButtonEntity diff --git a/homeassistant/components/microbees/config_flow.py b/homeassistant/components/microbees/config_flow.py index fb0b5faa020..dedb6c1f374 100644 --- a/homeassistant/components/microbees/config_flow.py +++ b/homeassistant/components/microbees/config_flow.py @@ -1,4 +1,5 @@ """Config flow for microBees integration.""" + from collections.abc import Mapping import logging from typing import Any @@ -7,7 +8,6 @@ from microBeesPy import MicroBees, MicroBeesException from homeassistant import config_entries from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import aiohttp_client, config_entry_oauth2_flow from .const import DOMAIN @@ -32,7 +32,9 @@ class OAuth2FlowHandler( scopes = ["read", "write"] return {"scope": " ".join(scopes)} - async def async_oauth_create_entry(self, data: dict[str, Any]) -> FlowResult: + async def async_oauth_create_entry( + self, data: dict[str, Any] + ) -> config_entries.ConfigFlowResult: """Create an oauth config entry or update existing entry for reauth.""" microbees = MicroBees( @@ -61,7 +63,9 @@ class OAuth2FlowHandler( return self.async_abort(reason="reauth_successful") return self.async_abort(reason="wrong_account") - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> config_entries.ConfigFlowResult: """Perform reauth upon an API authentication error.""" self.reauth_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] @@ -70,7 +74,7 @@ class OAuth2FlowHandler( async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> config_entries.ConfigFlowResult: """Confirm reauth dialog.""" if user_input is None: return self.async_show_form(step_id="reauth_confirm") diff --git a/homeassistant/components/microbees/const.py b/homeassistant/components/microbees/const.py index cf7644c8dfa..ab8637f0f75 100644 --- a/homeassistant/components/microbees/const.py +++ b/homeassistant/components/microbees/const.py @@ -1,11 +1,14 @@ """Constants for the microBees integration.""" + from homeassistant.const import Platform DOMAIN = "microbees" OAUTH2_AUTHORIZE = "https://dev.microbees.com/oauth/authorize" OAUTH2_TOKEN = "https://dev.microbees.com/oauth/token" PLATFORMS = [ + Platform.BINARY_SENSOR, Platform.BUTTON, + Platform.COVER, Platform.LIGHT, Platform.SENSOR, Platform.SWITCH, diff --git a/homeassistant/components/microbees/cover.py b/homeassistant/components/microbees/cover.py new file mode 100644 index 00000000000..bdf6e815af1 --- /dev/null +++ b/homeassistant/components/microbees/cover.py @@ -0,0 +1,117 @@ +"""Cover integration microBees.""" + +from typing import Any + +from microBeesPy import Actuator + +from homeassistant.components.cover import ( + CoverDeviceClass, + CoverEntity, + CoverEntityFeature, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.event import async_call_later + +from .const import DOMAIN +from .coordinator import MicroBeesUpdateCoordinator +from .entity import MicroBeesEntity + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up the microBees cover platform.""" + coordinator: MicroBeesUpdateCoordinator = hass.data[DOMAIN][ + entry.entry_id + ].coordinator + + async_add_entities( + MBCover( + coordinator, + bee_id, + next(filter(lambda x: x.deviceID == 551, bee.actuators)).id, + next(filter(lambda x: x.deviceID == 552, bee.actuators)).id, + ) + for bee_id, bee in coordinator.data.bees.items() + if bee.productID == 47 + ) + + +class MBCover(MicroBeesEntity, CoverEntity): + """Representation of a microBees cover.""" + + _attr_device_class = CoverDeviceClass.SHUTTER + _attr_supported_features = ( + CoverEntityFeature.OPEN | CoverEntityFeature.STOP | CoverEntityFeature.CLOSE + ) + + def __init__(self, coordinator, bee_id, actuator_up_id, actuator_down_id) -> None: + """Initialize the microBees cover.""" + super().__init__(coordinator, bee_id) + self.actuator_up_id = actuator_up_id + self.actuator_down_id = actuator_down_id + self._attr_is_closed = None + + @property + def name(self) -> str: + """Name of the cover.""" + return self.bee.name + + @property + def actuator_up(self) -> Actuator: + """Return the rolling up actuator.""" + return self.coordinator.data.actuators[self.actuator_up_id] + + @property + def actuator_down(self) -> Actuator: + """Return the rolling down actuator.""" + return self.coordinator.data.actuators[self.actuator_down_id] + + def _reset_open_close(self, *_: Any) -> None: + """Reset the opening and closing state.""" + self._attr_is_opening = False + self._attr_is_closing = False + self.async_write_ha_state() + + async def async_open_cover(self, **kwargs: Any) -> None: + """Open the cover.""" + sendCommand = await self.coordinator.microbees.sendCommand( + self.actuator_up_id, + self.actuator_up.configuration.actuator_timing * 1000, + ) + + if not sendCommand: + raise HomeAssistantError(f"Failed to open {self.name}") + + self._attr_is_opening = True + async_call_later( + self.hass, + self.actuator_down.configuration.actuator_timing, + self._reset_open_close, + ) + + async def async_close_cover(self, **kwargs: Any) -> None: + """Close the cover.""" + sendCommand = await self.coordinator.microbees.sendCommand( + self.actuator_down_id, + self.actuator_down.configuration.actuator_timing * 1000, + ) + if not sendCommand: + raise HomeAssistantError(f"Failed to close {self.name}") + + self._attr_is_closing = True + async_call_later( + self.hass, + self.actuator_down.configuration.actuator_timing, + self._reset_open_close, + ) + + async def async_stop_cover(self, **kwargs: Any) -> None: + """Stop the cover.""" + if self.is_opening: + await self.coordinator.microbees.sendCommand(self.actuator_up_id, 0) + if self.is_closing: + await self.coordinator.microbees.sendCommand(self.actuator_down_id, 0) diff --git a/homeassistant/components/microbees/light.py b/homeassistant/components/microbees/light.py index 7616cba41b0..411eab22324 100644 --- a/homeassistant/components/microbees/light.py +++ b/homeassistant/components/microbees/light.py @@ -1,4 +1,5 @@ """Light integration microBees.""" + from typing import Any from homeassistant.components.light import ATTR_RGBW_COLOR, ColorMode, LightEntity diff --git a/homeassistant/components/microbees/sensor.py b/homeassistant/components/microbees/sensor.py index 56db4c00ee3..360422de735 100644 --- a/homeassistant/components/microbees/sensor.py +++ b/homeassistant/components/microbees/sensor.py @@ -1,4 +1,5 @@ """sensor integration microBees.""" + from microBeesPy import Sensor from homeassistant.components.sensor import ( diff --git a/homeassistant/components/microbees/switch.py b/homeassistant/components/microbees/switch.py index 4a52d95620b..8e3c03e9ba4 100644 --- a/homeassistant/components/microbees/switch.py +++ b/homeassistant/components/microbees/switch.py @@ -1,4 +1,5 @@ """Switch integration microBees.""" + from typing import Any from homeassistant.components.switch import SwitchEntity diff --git a/homeassistant/components/microsoft/tts.py b/homeassistant/components/microsoft/tts.py index 9bcd7f533f8..ea95771429f 100644 --- a/homeassistant/components/microsoft/tts.py +++ b/homeassistant/components/microsoft/tts.py @@ -1,4 +1,5 @@ """Support for the Microsoft Cognitive Services text-to-speech service.""" + import logging from pycsspeechtts import pycsspeechtts diff --git a/homeassistant/components/microsoft_face/__init__.py b/homeassistant/components/microsoft_face/__init__.py index e3f722ae2be..23535911e5c 100644 --- a/homeassistant/components/microsoft_face/__init__.py +++ b/homeassistant/components/microsoft_face/__init__.py @@ -1,4 +1,5 @@ """Support for Microsoft face recognition.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/microsoft_face/icons.json b/homeassistant/components/microsoft_face/icons.json new file mode 100644 index 00000000000..826e390197a --- /dev/null +++ b/homeassistant/components/microsoft_face/icons.json @@ -0,0 +1,10 @@ +{ + "services": { + "create_group": "mdi:account-multiple-plus", + "create_person": "mdi:account-plus", + "delete_group": "mdi:account-multiple-remove", + "delete_person": "mdi:account-remove", + "face_person": "mdi:face-man", + "train_group": "mdi:account-multiple-check" + } +} diff --git a/homeassistant/components/microsoft_face_detect/image_processing.py b/homeassistant/components/microsoft_face_detect/image_processing.py index 8b69e21428d..ef8a4f5df4b 100644 --- a/homeassistant/components/microsoft_face_detect/image_processing.py +++ b/homeassistant/components/microsoft_face_detect/image_processing.py @@ -1,4 +1,5 @@ """Component that will help set the Microsoft face detect processing.""" + from __future__ import annotations import logging @@ -55,15 +56,12 @@ async def async_setup_platform( api = hass.data[DATA_MICROSOFT_FACE] attributes = config[CONF_ATTRIBUTES] - entities = [] - for camera in config[CONF_SOURCE]: - entities.append( - MicrosoftFaceDetectEntity( - camera[CONF_ENTITY_ID], api, attributes, camera.get(CONF_NAME) - ) + async_add_entities( + MicrosoftFaceDetectEntity( + camera[CONF_ENTITY_ID], api, attributes, camera.get(CONF_NAME) ) - - async_add_entities(entities) + for camera in config[CONF_SOURCE] + ) class MicrosoftFaceDetectEntity(ImageProcessingFaceEntity): diff --git a/homeassistant/components/microsoft_face_identify/image_processing.py b/homeassistant/components/microsoft_face_identify/image_processing.py index 4c76d29ebc6..d1af1d4a827 100644 --- a/homeassistant/components/microsoft_face_identify/image_processing.py +++ b/homeassistant/components/microsoft_face_identify/image_processing.py @@ -1,4 +1,5 @@ """Component that will help set the Microsoft face for verify processing.""" + from __future__ import annotations import logging @@ -37,19 +38,16 @@ async def async_setup_platform( face_group = config[CONF_GROUP] confidence = config[CONF_CONFIDENCE] - entities = [] - for camera in config[CONF_SOURCE]: - entities.append( - MicrosoftFaceIdentifyEntity( - camera[CONF_ENTITY_ID], - api, - face_group, - confidence, - camera.get(CONF_NAME), - ) + async_add_entities( + MicrosoftFaceIdentifyEntity( + camera[CONF_ENTITY_ID], + api, + face_group, + confidence, + camera.get(CONF_NAME), ) - - async_add_entities(entities) + for camera in config[CONF_SOURCE] + ) class MicrosoftFaceIdentifyEntity(ImageProcessingFaceEntity): diff --git a/homeassistant/components/mikrotik/__init__.py b/homeassistant/components/mikrotik/__init__.py index 6c98c389984..76d9a57c7ef 100644 --- a/homeassistant/components/mikrotik/__init__.py +++ b/homeassistant/components/mikrotik/__init__.py @@ -1,4 +1,5 @@ """The Mikrotik component.""" + from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/mikrotik/config_flow.py b/homeassistant/components/mikrotik/config_flow.py index 84b334c5f8f..8e5ff50e590 100644 --- a/homeassistant/components/mikrotik/config_flow.py +++ b/homeassistant/components/mikrotik/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Mikrotik.""" + from __future__ import annotations from collections.abc import Mapping @@ -6,7 +7,12 @@ from typing import Any import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, @@ -15,7 +21,6 @@ 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, @@ -30,23 +35,23 @@ from .errors import CannotConnect, LoginError from .hub import get_api -class MikrotikFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class MikrotikFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a Mikrotik config flow.""" VERSION = 1 - _reauth_entry: config_entries.ConfigEntry | None + _reauth_entry: ConfigEntry | None @staticmethod @callback def async_get_options_flow( - config_entry: config_entries.ConfigEntry, + config_entry: ConfigEntry, ) -> MikrotikOptionsFlowHandler: """Get the options flow for this handler.""" return MikrotikOptionsFlowHandler(config_entry) async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" errors = {} if user_input is not None: @@ -78,7 +83,7 @@ class MikrotikFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth(self, data: Mapping[str, Any]) -> ConfigFlowResult: """Perform reauth upon an API authentication error.""" self._reauth_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] @@ -87,7 +92,7 @@ class MikrotikFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_reauth_confirm( self, user_input: dict[str, str] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm reauth dialog.""" errors = {} assert self._reauth_entry @@ -122,22 +127,22 @@ class MikrotikFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) -class MikrotikOptionsFlowHandler(config_entries.OptionsFlow): +class MikrotikOptionsFlowHandler(OptionsFlow): """Handle Mikrotik options.""" - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + def __init__(self, config_entry: ConfigEntry) -> None: """Initialize Mikrotik options flow.""" self.config_entry = config_entry async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Manage the Mikrotik options.""" return await self.async_step_device_tracker() async def async_step_device_tracker( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """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 8407dd14a6e..772c956b54b 100644 --- a/homeassistant/components/mikrotik/const.py +++ b/homeassistant/components/mikrotik/const.py @@ -1,4 +1,5 @@ """Constants used in the Mikrotik components.""" + from typing import Final DOMAIN: Final = "mikrotik" diff --git a/homeassistant/components/mikrotik/device_tracker.py b/homeassistant/components/mikrotik/device_tracker.py index 8136334514f..866eba0b8bb 100644 --- a/homeassistant/components/mikrotik/device_tracker.py +++ b/homeassistant/components/mikrotik/device_tracker.py @@ -1,4 +1,5 @@ """Support for Mikrotik routers as device tracker.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/mikrotik/errors.py b/homeassistant/components/mikrotik/errors.py index 22cd63d7468..d3049c7a0b8 100644 --- a/homeassistant/components/mikrotik/errors.py +++ b/homeassistant/components/mikrotik/errors.py @@ -1,4 +1,5 @@ """Errors for the Mikrotik component.""" + from homeassistant.exceptions import HomeAssistantError diff --git a/homeassistant/components/mikrotik/hub.py b/homeassistant/components/mikrotik/hub.py index 044a45fb9b5..6b94a621683 100644 --- a/homeassistant/components/mikrotik/hub.py +++ b/homeassistant/components/mikrotik/hub.py @@ -1,4 +1,5 @@ """The Mikrotik router class.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/mill/__init__.py b/homeassistant/components/mill/__init__.py index 136c4a2940f..b2f06597563 100644 --- a/homeassistant/components/mill/__init__.py +++ b/homeassistant/components/mill/__init__.py @@ -1,4 +1,5 @@ """The mill component.""" + from __future__ import annotations from datetime import timedelta @@ -65,8 +66,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: key = entry.data[CONF_USERNAME] conn_type = CLOUD - if not await mill_data_connection.connect(): - raise ConfigEntryNotReady + try: + if not await mill_data_connection.connect(): + raise ConfigEntryNotReady + except TimeoutError as error: + raise ConfigEntryNotReady from error data_coordinator = MillDataUpdateCoordinator( hass, mill_data_connection=mill_data_connection, diff --git a/homeassistant/components/mill/config_flow.py b/homeassistant/components/mill/config_flow.py index 9f7dd5d5cdb..58660d6358e 100644 --- a/homeassistant/components/mill/config_flow.py +++ b/homeassistant/components/mill/config_flow.py @@ -1,16 +1,17 @@ """Adds config flow for Mill integration.""" + from mill import Mill from mill_local import Mill as MillLocal import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD, CONF_USERNAME from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import CLOUD, CONNECTION_TYPE, DOMAIN, LOCAL -class MillConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class MillConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Mill integration.""" VERSION = 1 diff --git a/homeassistant/components/mill/icons.json b/homeassistant/components/mill/icons.json new file mode 100644 index 00000000000..13d6bb650c1 --- /dev/null +++ b/homeassistant/components/mill/icons.json @@ -0,0 +1,5 @@ +{ + "services": { + "set_room_temperature": "mdi:thermometer" + } +} diff --git a/homeassistant/components/mill/sensor.py b/homeassistant/components/mill/sensor.py index 8c7c418e8ff..64b9008a82b 100644 --- a/homeassistant/components/mill/sensor.py +++ b/homeassistant/components/mill/sensor.py @@ -1,4 +1,5 @@ """Support for mill wifi-enabled home heaters.""" + from __future__ import annotations import mill diff --git a/homeassistant/components/min_max/config_flow.py b/homeassistant/components/min_max/config_flow.py index f449c98819e..36133f7394d 100644 --- a/homeassistant/components/min_max/config_flow.py +++ b/homeassistant/components/min_max/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Min/Max integration.""" + from __future__ import annotations from collections.abc import Mapping diff --git a/homeassistant/components/min_max/icons.json b/homeassistant/components/min_max/icons.json new file mode 100644 index 00000000000..a03163179cb --- /dev/null +++ b/homeassistant/components/min_max/icons.json @@ -0,0 +1,5 @@ +{ + "services": { + "reload": "mdi:reload" + } +} diff --git a/homeassistant/components/min_max/sensor.py b/homeassistant/components/min_max/sensor.py index cc26a684a8d..4ea63f5a472 100644 --- a/homeassistant/components/min_max/sensor.py +++ b/homeassistant/components/min_max/sensor.py @@ -1,4 +1,5 @@ """Support for displaying minimal, maximal, mean or median values.""" + from __future__ import annotations from datetime import datetime @@ -22,7 +23,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import Event, HomeAssistant, callback from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import ( @@ -30,12 +31,7 @@ from homeassistant.helpers.event import ( async_track_state_change_event, ) from homeassistant.helpers.reload import async_setup_reload_service -from homeassistant.helpers.typing import ( - ConfigType, - DiscoveryInfoType, - EventType, - StateType, -) +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType from . import PLATFORMS from .const import CONF_ENTITY_IDS, CONF_ROUND_DIGITS, DOMAIN @@ -257,7 +253,7 @@ class MinMaxSensor(SensorEntity): # Replay current state of source entities for entity_id in self._entity_ids: state = self.hass.states.get(entity_id) - state_event: EventType[EventStateChangedData] = EventType( + state_event: Event[EventStateChangedData] = Event( "", {"entity_id": entity_id, "new_state": state, "old_state": None} ) self._async_min_max_sensor_state_listener(state_event, update_state=False) @@ -292,7 +288,7 @@ class MinMaxSensor(SensorEntity): @callback def _async_min_max_sensor_state_listener( - self, event: EventType[EventStateChangedData], update_state: bool = True + self, event: Event[EventStateChangedData], update_state: bool = True ) -> None: """Handle the sensor state changes.""" new_state = event.data["new_state"] diff --git a/homeassistant/components/minecraft_server/__init__.py b/homeassistant/components/minecraft_server/__init__.py index 2cd6c51546a..5ec737c3f73 100644 --- a/homeassistant/components/minecraft_server/__init__.py +++ b/homeassistant/components/minecraft_server/__init__.py @@ -1,4 +1,5 @@ """The Minecraft Server integration.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/minecraft_server/api.py b/homeassistant/components/minecraft_server/api.py index d86f8453413..3155d83a736 100644 --- a/homeassistant/components/minecraft_server/api.py +++ b/homeassistant/components/minecraft_server/api.py @@ -1,6 +1,5 @@ """API for the Minecraft Server integration.""" - from dataclasses import dataclass from enum import StrEnum import logging @@ -141,8 +140,7 @@ class MinecraftServer: players_list: list[str] = [] if players := status_response.players.sample: - for player in players: - players_list.append(player.name) + players_list.extend(player.name for player in players) players_list.sort() return MinecraftServerData( diff --git a/homeassistant/components/minecraft_server/binary_sensor.py b/homeassistant/components/minecraft_server/binary_sensor.py index 6c0a2a248f3..60f2e00da0e 100644 --- a/homeassistant/components/minecraft_server/binary_sensor.py +++ b/homeassistant/components/minecraft_server/binary_sensor.py @@ -1,5 +1,4 @@ """The Minecraft Server binary sensor platform.""" -from dataclasses import dataclass from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, @@ -14,22 +13,14 @@ from .const import DOMAIN from .coordinator import MinecraftServerCoordinator from .entity import MinecraftServerEntity -ICON_STATUS = "mdi:lan" - KEY_STATUS = "status" -@dataclass(frozen=True) -class MinecraftServerBinarySensorEntityDescription(BinarySensorEntityDescription): - """Class describing Minecraft Server binary sensor entities.""" - - BINARY_SENSOR_DESCRIPTIONS = [ - MinecraftServerBinarySensorEntityDescription( + BinarySensorEntityDescription( key=KEY_STATUS, translation_key=KEY_STATUS, device_class=BinarySensorDeviceClass.CONNECTIVITY, - icon=ICON_STATUS, ), ] @@ -54,12 +45,10 @@ async def async_setup_entry( class MinecraftServerBinarySensorEntity(MinecraftServerEntity, BinarySensorEntity): """Representation of a Minecraft Server binary sensor base entity.""" - entity_description: MinecraftServerBinarySensorEntityDescription - def __init__( self, coordinator: MinecraftServerCoordinator, - description: MinecraftServerBinarySensorEntityDescription, + description: BinarySensorEntityDescription, config_entry: ConfigEntry, ) -> None: """Initialize binary sensor base entity.""" diff --git a/homeassistant/components/minecraft_server/config_flow.py b/homeassistant/components/minecraft_server/config_flow.py index 022b7ed3991..654d903068f 100644 --- a/homeassistant/components/minecraft_server/config_flow.py +++ b/homeassistant/components/minecraft_server/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Minecraft Server integration.""" + from __future__ import annotations import logging @@ -6,9 +7,8 @@ from typing import Any import voluptuous as vol -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_ADDRESS, CONF_NAME, CONF_TYPE -from homeassistant.data_entry_flow import FlowResult from .api import MinecraftServer, MinecraftServerAddressError, MinecraftServerType from .const import DEFAULT_NAME, DOMAIN @@ -25,7 +25,7 @@ class MinecraftServerConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors: dict[str, str] = {} @@ -66,7 +66,7 @@ class MinecraftServerConfigFlow(ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None, errors: dict[str, str] | None = None, - ) -> FlowResult: + ) -> ConfigFlowResult: """Show the setup form to the user.""" if user_input is None: user_input = {} diff --git a/homeassistant/components/minecraft_server/coordinator.py b/homeassistant/components/minecraft_server/coordinator.py index e498375cafc..37eeb9f2ac2 100644 --- a/homeassistant/components/minecraft_server/coordinator.py +++ b/homeassistant/components/minecraft_server/coordinator.py @@ -1,4 +1,5 @@ """The Minecraft Server integration.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/minecraft_server/diagnostics.py b/homeassistant/components/minecraft_server/diagnostics.py index 62e507ef09f..1cae535dc43 100644 --- a/homeassistant/components/minecraft_server/diagnostics.py +++ b/homeassistant/components/minecraft_server/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics for the Minecraft Server integration.""" + from collections.abc import Iterable from dataclasses import asdict from typing import Any diff --git a/homeassistant/components/minecraft_server/icons.json b/homeassistant/components/minecraft_server/icons.json new file mode 100644 index 00000000000..75719b39753 --- /dev/null +++ b/homeassistant/components/minecraft_server/icons.json @@ -0,0 +1,42 @@ +{ + "entity": { + "binary_sensor": { + "status": { + "default": "mdi:lan", + "state": { + "on": "mdi:lan-connect", + "off": "mdi:lan-disconnect" + } + } + }, + "sensor": { + "edition": { + "default": "mdi:minecraft" + }, + "game_mode": { + "default": "mdi:cog" + }, + "latency": { + "default": "mdi:signal" + }, + "players_max": { + "default": "mdi:account-multiple" + }, + "players_online": { + "default": "mdi:account-multiple" + }, + "protocol_version": { + "default": "mdi:numeric" + }, + "version": { + "default": "mdi:numeric" + }, + "motd": { + "default": "mdi:minecraft" + }, + "map_name": { + "default": "mdi:map" + } + } + } +} diff --git a/homeassistant/components/minecraft_server/sensor.py b/homeassistant/components/minecraft_server/sensor.py index 606d6085fda..4b862f54715 100644 --- a/homeassistant/components/minecraft_server/sensor.py +++ b/homeassistant/components/minecraft_server/sensor.py @@ -1,4 +1,5 @@ """The Minecraft Server sensor platform.""" + from __future__ import annotations from collections.abc import Callable, MutableMapping @@ -19,16 +20,6 @@ from .entity import MinecraftServerEntity ATTR_PLAYERS_LIST = "players_list" -ICON_EDITION = "mdi:minecraft" -ICON_GAME_MODE = "mdi:cog" -ICON_MAP_NAME = "mdi:map" -ICON_LATENCY = "mdi:signal" -ICON_PLAYERS_MAX = "mdi:account-multiple" -ICON_PLAYERS_ONLINE = "mdi:account-multiple" -ICON_PROTOCOL_VERSION = "mdi:numeric" -ICON_VERSION = "mdi:numeric" -ICON_MOTD = "mdi:minecraft" - KEY_EDITION = "edition" KEY_GAME_MODE = "game_mode" KEY_MAP_NAME = "map_name" @@ -41,22 +32,15 @@ UNIT_PLAYERS_MAX = "players" UNIT_PLAYERS_ONLINE = "players" -@dataclass(frozen=True) -class MinecraftServerEntityDescriptionMixin: - """Mixin values for Minecraft Server entities.""" +@dataclass(frozen=True, kw_only=True) +class MinecraftServerSensorEntityDescription(SensorEntityDescription): + """Class describing Minecraft Server sensor entities.""" value_fn: Callable[[MinecraftServerData], StateType] attributes_fn: Callable[[MinecraftServerData], MutableMapping[str, Any]] | None supported_server_types: set[MinecraftServerType] -@dataclass(frozen=True) -class MinecraftServerSensorEntityDescription( - SensorEntityDescription, MinecraftServerEntityDescriptionMixin -): - """Class describing Minecraft Server sensor entities.""" - - def get_extra_state_attributes_players_list( data: MinecraftServerData, ) -> dict[str, list[str]]: @@ -74,7 +58,6 @@ SENSOR_DESCRIPTIONS = [ MinecraftServerSensorEntityDescription( key=KEY_VERSION, translation_key=KEY_VERSION, - icon=ICON_VERSION, value_fn=lambda data: data.version, attributes_fn=None, supported_server_types={ @@ -86,7 +69,6 @@ SENSOR_DESCRIPTIONS = [ MinecraftServerSensorEntityDescription( key=KEY_PROTOCOL_VERSION, translation_key=KEY_PROTOCOL_VERSION, - icon=ICON_PROTOCOL_VERSION, value_fn=lambda data: data.protocol_version, attributes_fn=None, supported_server_types={ @@ -100,7 +82,6 @@ SENSOR_DESCRIPTIONS = [ key=KEY_PLAYERS_MAX, translation_key=KEY_PLAYERS_MAX, native_unit_of_measurement=UNIT_PLAYERS_MAX, - icon=ICON_PLAYERS_MAX, value_fn=lambda data: data.players_max, attributes_fn=None, supported_server_types={ @@ -114,7 +95,6 @@ SENSOR_DESCRIPTIONS = [ translation_key=KEY_LATENCY, native_unit_of_measurement=UnitOfTime.MILLISECONDS, suggested_display_precision=0, - icon=ICON_LATENCY, value_fn=lambda data: data.latency, attributes_fn=None, supported_server_types={ @@ -126,7 +106,6 @@ SENSOR_DESCRIPTIONS = [ MinecraftServerSensorEntityDescription( key=KEY_MOTD, translation_key=KEY_MOTD, - icon=ICON_MOTD, value_fn=lambda data: data.motd, attributes_fn=None, supported_server_types={ @@ -138,7 +117,6 @@ SENSOR_DESCRIPTIONS = [ key=KEY_PLAYERS_ONLINE, translation_key=KEY_PLAYERS_ONLINE, native_unit_of_measurement=UNIT_PLAYERS_ONLINE, - icon=ICON_PLAYERS_ONLINE, value_fn=lambda data: data.players_online, attributes_fn=get_extra_state_attributes_players_list, supported_server_types={ @@ -149,7 +127,6 @@ SENSOR_DESCRIPTIONS = [ MinecraftServerSensorEntityDescription( key=KEY_EDITION, translation_key=KEY_EDITION, - icon=ICON_EDITION, value_fn=lambda data: data.edition, attributes_fn=None, supported_server_types={ @@ -161,7 +138,6 @@ SENSOR_DESCRIPTIONS = [ MinecraftServerSensorEntityDescription( key=KEY_GAME_MODE, translation_key=KEY_GAME_MODE, - icon=ICON_GAME_MODE, value_fn=lambda data: data.game_mode, attributes_fn=None, supported_server_types={ @@ -171,7 +147,6 @@ SENSOR_DESCRIPTIONS = [ MinecraftServerSensorEntityDescription( key=KEY_MAP_NAME, translation_key=KEY_MAP_NAME, - icon=ICON_MAP_NAME, value_fn=lambda data: data.map_name, attributes_fn=None, supported_server_types={ diff --git a/homeassistant/components/minio/__init__.py b/homeassistant/components/minio/__init__.py index 1f325f3866d..e2cbcdf9ed1 100644 --- a/homeassistant/components/minio/__init__.py +++ b/homeassistant/components/minio/__init__.py @@ -1,4 +1,5 @@ """Minio component.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/minio/icons.json b/homeassistant/components/minio/icons.json new file mode 100644 index 00000000000..16deb1a168d --- /dev/null +++ b/homeassistant/components/minio/icons.json @@ -0,0 +1,7 @@ +{ + "services": { + "get": "mdi:cloud-download", + "put": "mdi:cloud-upload", + "remove": "mdi:delete" + } +} diff --git a/homeassistant/components/minio/minio_helper.py b/homeassistant/components/minio/minio_helper.py index 7edb11797eb..979de40ece7 100644 --- a/homeassistant/components/minio/minio_helper.py +++ b/homeassistant/components/minio/minio_helper.py @@ -1,4 +1,5 @@ """Minio helper methods.""" + from __future__ import annotations from collections.abc import Iterable diff --git a/homeassistant/components/mjpeg/camera.py b/homeassistant/components/mjpeg/camera.py index 6a5c4e3203b..ab8c67f2ca9 100644 --- a/homeassistant/components/mjpeg/camera.py +++ b/homeassistant/components/mjpeg/camera.py @@ -1,4 +1,5 @@ """Support for IP Cameras.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/mjpeg/config_flow.py b/homeassistant/components/mjpeg/config_flow.py index 024766f4c63..84267936788 100644 --- a/homeassistant/components/mjpeg/config_flow.py +++ b/homeassistant/components/mjpeg/config_flow.py @@ -1,4 +1,5 @@ """Config flow to configure the MJPEG IP Camera integration.""" + from __future__ import annotations from http import HTTPStatus @@ -10,7 +11,12 @@ from requests.auth import HTTPBasicAuth, HTTPDigestAuth from requests.exceptions import HTTPError, Timeout import voluptuous as vol -from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.const import ( CONF_AUTHENTICATION, CONF_NAME, @@ -21,7 +27,6 @@ from homeassistant.const import ( HTTP_DIGEST_AUTHENTICATION, ) from homeassistant.core import HomeAssistant, callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError from .const import CONF_MJPEG_URL, CONF_STILL_IMAGE_URL, DOMAIN, LOGGER @@ -140,7 +145,7 @@ class MJPEGFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" errors: dict[str, str] = {} @@ -184,7 +189,7 @@ class MJPEGOptionsFlowHandler(OptionsFlow): async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Manage MJPEG IP Camera options.""" errors: dict[str, str] = {} diff --git a/homeassistant/components/moat/__init__.py b/homeassistant/components/moat/__init__.py index ed360f53b65..8ee2e294552 100644 --- a/homeassistant/components/moat/__init__.py +++ b/homeassistant/components/moat/__init__.py @@ -1,4 +1,5 @@ """The Moat Bluetooth BLE integration.""" + from __future__ import annotations import logging @@ -25,14 +26,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: address = entry.unique_id assert address is not None data = MoatBluetoothDeviceData() - coordinator = hass.data.setdefault(DOMAIN, {})[ - entry.entry_id - ] = PassiveBluetoothProcessorCoordinator( - hass, - _LOGGER, - address=address, - mode=BluetoothScanningMode.PASSIVE, - update_method=data.update, + coordinator = hass.data.setdefault(DOMAIN, {})[entry.entry_id] = ( + PassiveBluetoothProcessorCoordinator( + hass, + _LOGGER, + address=address, + mode=BluetoothScanningMode.PASSIVE, + update_method=data.update, + ) ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload( diff --git a/homeassistant/components/moat/config_flow.py b/homeassistant/components/moat/config_flow.py index 4e522a81c73..e7a1d55f05c 100644 --- a/homeassistant/components/moat/config_flow.py +++ b/homeassistant/components/moat/config_flow.py @@ -1,4 +1,5 @@ """Config flow for moat ble integration.""" + from __future__ import annotations from typing import Any @@ -10,9 +11,8 @@ from homeassistant.components.bluetooth import ( BluetoothServiceInfoBleak, async_discovered_service_info, ) -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_ADDRESS -from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN @@ -30,7 +30,7 @@ class MoatConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_bluetooth( self, discovery_info: BluetoothServiceInfoBleak - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the bluetooth discovery step.""" await self.async_set_unique_id(discovery_info.address) self._abort_if_unique_id_configured() @@ -43,7 +43,7 @@ class MoatConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_bluetooth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm discovery.""" assert self._discovered_device is not None device = self._discovered_device @@ -62,7 +62,7 @@ class MoatConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the user step to pick discovered device.""" if user_input is not None: address = user_input[CONF_ADDRESS] diff --git a/homeassistant/components/moat/sensor.py b/homeassistant/components/moat/sensor.py index f75717fad40..3118c539d3a 100644 --- a/homeassistant/components/moat/sensor.py +++ b/homeassistant/components/moat/sensor.py @@ -1,4 +1,5 @@ """Support for moat ble sensors.""" + from __future__ import annotations from moat_ble import DeviceClass, DeviceKey, SensorUpdate, Units diff --git a/homeassistant/components/mobile_app/__init__.py b/homeassistant/components/mobile_app/__init__.py index 19aba56f260..20a5448f2be 100644 --- a/homeassistant/components/mobile_app/__init__.py +++ b/homeassistant/components/mobile_app/__init__.py @@ -1,4 +1,5 @@ """Integrates Native Apps to Home Assistant.""" + from contextlib import suppress from typing import Any @@ -18,7 +19,16 @@ from homeassistant.helpers import ( from homeassistant.helpers.storage import Store from homeassistant.helpers.typing import ConfigType -from . import websocket_api +# Pre-import the platforms so they get loaded when the integration +# is imported as they are almost always going to be loaded and its +# cheaper to import them all at once. +from . import ( # noqa: F401 + binary_sensor as binary_sensor_pre_import, + device_tracker as device_tracker_pre_import, + notify as notify_pre_import, + sensor as sensor_pre_import, + websocket_api, +) from .const import ( ATTR_DEVICE_NAME, ATTR_MANUFACTURER, diff --git a/homeassistant/components/mobile_app/binary_sensor.py b/homeassistant/components/mobile_app/binary_sensor.py index 2be71965371..58683ef378c 100644 --- a/homeassistant/components/mobile_app/binary_sensor.py +++ b/homeassistant/components/mobile_app/binary_sensor.py @@ -1,4 +1,5 @@ """Binary sensor platform for mobile_app.""" + from typing import Any from homeassistant.components.binary_sensor import BinarySensorEntity diff --git a/homeassistant/components/mobile_app/config_flow.py b/homeassistant/components/mobile_app/config_flow.py index 246c433672b..66035733c33 100644 --- a/homeassistant/components/mobile_app/config_flow.py +++ b/homeassistant/components/mobile_app/config_flow.py @@ -1,15 +1,16 @@ """Config flow for Mobile App.""" + import uuid -from homeassistant import config_entries from homeassistant.components import person +from homeassistant.config_entries import ConfigFlow from homeassistant.const import ATTR_DEVICE_ID from homeassistant.helpers import entity_registry as er from .const import ATTR_APP_ID, ATTR_DEVICE_NAME, CONF_USER_ID, DOMAIN -class MobileAppFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class MobileAppFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a Mobile App config flow.""" VERSION = 1 diff --git a/homeassistant/components/mobile_app/const.py b/homeassistant/components/mobile_app/const.py index efc105a80ea..25c35b3e87e 100644 --- a/homeassistant/components/mobile_app/const.py +++ b/homeassistant/components/mobile_app/const.py @@ -1,4 +1,5 @@ """Constants for mobile_app.""" + import voluptuous as vol from homeassistant.helpers import config_validation as cv diff --git a/homeassistant/components/mobile_app/device_action.py b/homeassistant/components/mobile_app/device_action.py index f6e870fc7d6..bebdef0e917 100644 --- a/homeassistant/components/mobile_app/device_action.py +++ b/homeassistant/components/mobile_app/device_action.py @@ -1,4 +1,5 @@ """Provides device actions for Mobile App.""" + from __future__ import annotations import voluptuous as vol diff --git a/homeassistant/components/mobile_app/device_tracker.py b/homeassistant/components/mobile_app/device_tracker.py index fb555db49cb..2c7a4147811 100644 --- a/homeassistant/components/mobile_app/device_tracker.py +++ b/homeassistant/components/mobile_app/device_tracker.py @@ -1,4 +1,5 @@ """Device tracker for Mobile app.""" + from homeassistant.components.device_tracker import ( ATTR_BATTERY, ATTR_GPS, diff --git a/homeassistant/components/mobile_app/entity.py b/homeassistant/components/mobile_app/entity.py index 76cf22cef54..1cac62ce964 100644 --- a/homeassistant/components/mobile_app/entity.py +++ b/homeassistant/components/mobile_app/entity.py @@ -1,4 +1,5 @@ """A entity class for mobile_app.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/mobile_app/helpers.py b/homeassistant/components/mobile_app/helpers.py index 9265b72d0d0..13d50b7984f 100644 --- a/homeassistant/components/mobile_app/helpers.py +++ b/homeassistant/components/mobile_app/helpers.py @@ -1,4 +1,5 @@ """Helpers for mobile_app.""" + from __future__ import annotations from collections.abc import Callable, Mapping @@ -142,16 +143,6 @@ def error_response( ) -def supports_encryption() -> bool: - """Test if we support encryption.""" - try: - import nacl # noqa: F401 pylint: disable=import-outside-toplevel - - return True - except OSError: - return False - - def safe_registration(registration: dict) -> dict: """Return a registration without sensitive values.""" # Sensitive values: webhook_id, secret, cloudhook_url diff --git a/homeassistant/components/mobile_app/http_api.py b/homeassistant/components/mobile_app/http_api.py index 92bb473d51a..f4786b2914c 100644 --- a/homeassistant/components/mobile_app/http_api.py +++ b/homeassistant/components/mobile_app/http_api.py @@ -1,4 +1,5 @@ """Provides an HTTP API for mobile_app.""" + from __future__ import annotations from contextlib import suppress @@ -10,7 +11,7 @@ from nacl.secret import SecretBox import voluptuous as vol from homeassistant.components import cloud -from homeassistant.components.http import HomeAssistantView +from homeassistant.components.http import KEY_HASS, HomeAssistantView from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.const import ATTR_DEVICE_ID, CONF_WEBHOOK_ID from homeassistant.helpers import config_validation as cv @@ -34,7 +35,6 @@ from .const import ( DOMAIN, SCHEMA_APP_DATA, ) -from .helpers import supports_encryption from .util import async_create_cloud_hook @@ -65,7 +65,7 @@ class RegistrationsView(HomeAssistantView): ) async def post(self, request: Request, data: dict) -> Response: """Handle the POST request for registration.""" - hass = request.app["hass"] + hass = request.app[KEY_HASS] webhook_id = secrets.token_hex() @@ -76,7 +76,7 @@ class RegistrationsView(HomeAssistantView): data[CONF_WEBHOOK_ID] = webhook_id - if data[ATTR_SUPPORTS_ENCRYPTION] and supports_encryption(): + if data[ATTR_SUPPORTS_ENCRYPTION]: data[CONF_SECRET] = secrets.token_hex(SecretBox.KEY_SIZE) data[CONF_USER_ID] = request["hass_user"].id @@ -93,7 +93,7 @@ class RegistrationsView(HomeAssistantView): remote_ui_url = None if cloud.async_active_subscription(hass): - with suppress(hass.components.cloud.CloudNotAvailable): + with suppress(cloud.CloudNotAvailable): remote_ui_url = cloud.async_remote_ui_url(hass) return self.json( diff --git a/homeassistant/components/mobile_app/logbook.py b/homeassistant/components/mobile_app/logbook.py index 083db294a1c..6a863e6a75b 100644 --- a/homeassistant/components/mobile_app/logbook.py +++ b/homeassistant/components/mobile_app/logbook.py @@ -1,4 +1,5 @@ """Describe mobile_app logbook events.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/mobile_app/manifest.json b/homeassistant/components/mobile_app/manifest.json index c3d15be3468..aeab576a7cd 100644 --- a/homeassistant/components/mobile_app/manifest.json +++ b/homeassistant/components/mobile_app/manifest.json @@ -6,7 +6,6 @@ "config_flow": true, "dependencies": ["http", "webhook", "person", "tag", "websocket_api"], "documentation": "https://www.home-assistant.io/integrations/mobile_app", - "import_executor": true, "iot_class": "local_push", "loggers": ["nacl"], "quality_scale": "internal", diff --git a/homeassistant/components/mobile_app/notify.py b/homeassistant/components/mobile_app/notify.py index e6f7126b0b8..c0efd302c47 100644 --- a/homeassistant/components/mobile_app/notify.py +++ b/homeassistant/components/mobile_app/notify.py @@ -1,4 +1,5 @@ """Support for mobile_app push notifications.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/mobile_app/push_notification.py b/homeassistant/components/mobile_app/push_notification.py index 0ef9739c8bd..d295a844878 100644 --- a/homeassistant/components/mobile_app/push_notification.py +++ b/homeassistant/components/mobile_app/push_notification.py @@ -1,4 +1,5 @@ """Push notification handling.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/mobile_app/sensor.py b/homeassistant/components/mobile_app/sensor.py index fd712faf121..6f049d6f2d5 100644 --- a/homeassistant/components/mobile_app/sensor.py +++ b/homeassistant/components/mobile_app/sensor.py @@ -1,4 +1,5 @@ """Sensor platform for mobile_app.""" + from __future__ import annotations from datetime import date, datetime diff --git a/homeassistant/components/mobile_app/util.py b/homeassistant/components/mobile_app/util.py index a7871d935ed..f139a203c34 100644 --- a/homeassistant/components/mobile_app/util.py +++ b/homeassistant/components/mobile_app/util.py @@ -1,4 +1,5 @@ """Mobile app utility functions.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/mobile_app/webhook.py b/homeassistant/components/mobile_app/webhook.py index 1a56b13ddc5..e7cccd0f151 100644 --- a/homeassistant/components/mobile_app/webhook.py +++ b/homeassistant/components/mobile_app/webhook.py @@ -1,4 +1,5 @@ """Webhook handlers for mobile_app.""" + from __future__ import annotations import asyncio @@ -99,7 +100,6 @@ from .const import ( DATA_DEVICES, DOMAIN, ERR_ENCRYPTION_ALREADY_ENABLED, - ERR_ENCRYPTION_NOT_AVAILABLE, ERR_ENCRYPTION_REQUIRED, ERR_INVALID_FORMAT, ERR_SENSOR_NOT_REGISTERED, @@ -114,7 +114,6 @@ from .helpers import ( error_response, registration_context, safe_registration, - supports_encryption, webhook_response, ) @@ -297,7 +296,7 @@ async def webhook_call_service( config_entry.data[ATTR_DEVICE_NAME], ex, ) - raise HTTPBadRequest() from ex + raise HTTPBadRequest from ex return empty_okay_response() @@ -483,13 +482,6 @@ async def webhook_enable_encryption( ERR_ENCRYPTION_ALREADY_ENABLED, "Encryption already enabled" ) - if not supports_encryption(): - _LOGGER.warning( - "Unable to enable encryption for %s because libsodium is unavailable!", - config_entry.data[ATTR_DEVICE_NAME], - ) - return error_response(ERR_ENCRYPTION_NOT_AVAILABLE, "Encryption is unavailable") - secret = secrets.token_hex(SecretBox.KEY_SIZE) update_data = { @@ -610,9 +602,9 @@ async def webhook_register_sensor( async_dispatcher_send(hass, f"{SIGNAL_SENSOR_UPDATE}-{unique_store_key}", data) else: data[CONF_UNIQUE_ID] = unique_store_key - data[ - CONF_NAME - ] = f"{config_entry.data[ATTR_DEVICE_NAME]} {data[ATTR_SENSOR_NAME]}" + data[CONF_NAME] = ( + f"{config_entry.data[ATTR_DEVICE_NAME]} {data[ATTR_SENSOR_NAME]}" + ) register_signal = f"{DOMAIN}_{data[ATTR_SENSOR_TYPE]}_register" async_dispatcher_send(hass, register_signal, data) @@ -743,7 +735,7 @@ async def webhook_get_config( resp[CONF_CLOUDHOOK_URL] = config_entry.data[CONF_CLOUDHOOK_URL] if cloud.async_active_subscription(hass): - with suppress(hass.components.cloud.CloudNotAvailable): + with suppress(cloud.CloudNotAvailable): resp[CONF_REMOTE_UI_URL] = cloud.async_remote_ui_url(hass) webhook_id = config_entry.data[CONF_WEBHOOK_ID] diff --git a/homeassistant/components/mobile_app/websocket_api.py b/homeassistant/components/mobile_app/websocket_api.py index c900aa8f93b..83c3cb29cea 100644 --- a/homeassistant/components/mobile_app/websocket_api.py +++ b/homeassistant/components/mobile_app/websocket_api.py @@ -1,4 +1,5 @@ """Mobile app websocket API.""" + from __future__ import annotations from functools import wraps diff --git a/homeassistant/components/mochad/__init__.py b/homeassistant/components/mochad/__init__.py index 114ade17740..36ebb74edc3 100644 --- a/homeassistant/components/mochad/__init__.py +++ b/homeassistant/components/mochad/__init__.py @@ -1,4 +1,5 @@ """Support for CM15A/CM19A X10 Controller using mochad daemon.""" + import logging import threading diff --git a/homeassistant/components/mochad/light.py b/homeassistant/components/mochad/light.py index b2dba04b205..4740823d85a 100644 --- a/homeassistant/components/mochad/light.py +++ b/homeassistant/components/mochad/light.py @@ -1,4 +1,5 @@ """Support for X10 dimmer over Mochad.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/mochad/switch.py b/homeassistant/components/mochad/switch.py index fe90d94e40a..beb12d9d409 100644 --- a/homeassistant/components/mochad/switch.py +++ b/homeassistant/components/mochad/switch.py @@ -1,4 +1,5 @@ """Support for X10 switch over Mochad.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index e5bb9e8bf38..94a84d3440d 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -1,4 +1,5 @@ """Support for Modbus.""" + from __future__ import annotations import logging @@ -50,7 +51,7 @@ from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType -from .const import ( # noqa: F401 +from .const import ( CALL_TYPE_COIL, CALL_TYPE_DISCRETE, CALL_TYPE_REGISTER_HOLDING, @@ -60,7 +61,6 @@ from .const import ( # noqa: F401 CONF_BAUDRATE, CONF_BYTESIZE, CONF_CLIMATES, - CONF_CLOSE_COMM_ON_ERROR, CONF_DATA_TYPE, CONF_DEVICE_ADDRESS, CONF_FAN_MODE_AUTO, @@ -97,7 +97,6 @@ from .const import ( # noqa: F401 CONF_PARITY, CONF_PRECISION, CONF_RETRIES, - CONF_RETRY_ON_EMPTY, CONF_SCALE, CONF_SLAVE_COUNT, CONF_STATE_CLOSED, @@ -133,7 +132,6 @@ from .const import ( # noqa: F401 ) from .modbus import ModbusHub, async_modbus_setup from .validators import ( - check_config, check_hvac_target_temp_registers, duplicate_fan_mode_validator, hvac_fixedsize_reglist_validator, @@ -373,10 +371,8 @@ MODBUS_SCHEMA = vol.Schema( { vol.Optional(CONF_NAME, default=DEFAULT_HUB): cv.string, vol.Optional(CONF_TIMEOUT, default=3): cv.socket_timeout, - vol.Optional(CONF_CLOSE_COMM_ON_ERROR): cv.boolean, vol.Optional(CONF_DELAY, default=0): cv.positive_int, vol.Optional(CONF_RETRIES): cv.positive_int, - vol.Optional(CONF_RETRY_ON_EMPTY): cv.boolean, vol.Optional(CONF_MSG_WAIT): cv.positive_int, vol.Optional(CONF_BINARY_SENSORS): vol.All( cv.ensure_list, [BINARY_SENSOR_SCHEMA] @@ -421,7 +417,6 @@ CONFIG_SCHEMA = vol.Schema( [ vol.Any(SERIAL_SCHEMA, ETHERNET_SCHEMA), ], - check_config, ), }, extra=vol.ALLOW_EXTRA, diff --git a/homeassistant/components/modbus/base_platform.py b/homeassistant/components/modbus/base_platform.py index ac11bab303d..5c8816dd74e 100644 --- a/homeassistant/components/modbus/base_platform.py +++ b/homeassistant/components/modbus/base_platform.py @@ -1,4 +1,5 @@ """Base implementation for all modbus platforms.""" + from __future__ import annotations from abc import abstractmethod @@ -28,7 +29,6 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity, ToggleEntity from homeassistant.helpers.event import async_call_later, async_track_time_interval -from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.restore_state import RestoreEntity from .const import ( @@ -45,7 +45,6 @@ from .const import ( CONF_DATA_TYPE, CONF_DEVICE_ADDRESS, CONF_INPUT_TYPE, - CONF_LAZY_ERROR, CONF_MAX_VALUE, CONF_MIN_VALUE, CONF_NAN_VALUE, @@ -62,14 +61,12 @@ from .const import ( CONF_VIRTUAL_COUNT, CONF_WRITE_TYPE, CONF_ZERO_SUPPRESS, - MODBUS_DOMAIN, SIGNAL_START_ENTITY, SIGNAL_STOP_ENTITY, DataType, ) from .modbus import ModbusHub -PARALLEL_UPDATES = 1 _LOGGER = logging.getLogger(__name__) @@ -81,27 +78,8 @@ class BasePlatform(Entity): ) -> None: """Initialize the Modbus binary sensor.""" - if CONF_LAZY_ERROR in entry: - async_create_issue( - hass, - MODBUS_DOMAIN, - "removed_lazy_error_count", - breaks_in_ha_version="2024.7.0", - is_fixable=False, - severity=IssueSeverity.WARNING, - translation_key="removed_lazy_error_count", - translation_placeholders={ - "config_key": "lazy_error_count", - "integration": MODBUS_DOMAIN, - "url": "https://www.home-assistant.io/integrations/modbus", - }, - ) - _LOGGER.warning( - "`lazy_error_count`: is deprecated and will be removed in version 2024.7" - ) - self._hub = hub - self._slave = entry.get(CONF_SLAVE, None) or entry.get(CONF_DEVICE_ADDRESS, 0) + self._slave = entry.get(CONF_SLAVE) or entry.get(CONF_DEVICE_ADDRESS, 0) self._address = int(entry[CONF_ADDRESS]) self._input_type = entry[CONF_INPUT_TYPE] self._value: str | None = None @@ -127,7 +105,7 @@ class BasePlatform(Entity): self._min_value = get_optional_numeric_config(CONF_MIN_VALUE) self._max_value = get_optional_numeric_config(CONF_MAX_VALUE) - self._nan_value = entry.get(CONF_NAN_VALUE, None) + self._nan_value = entry.get(CONF_NAN_VALUE) self._zero_suppress = get_optional_numeric_config(CONF_ZERO_SUPPRESS) @abstractmethod @@ -183,7 +161,7 @@ class BaseStructPlatform(BasePlatform, RestoreEntity): self._structure: str = config[CONF_STRUCTURE] self._scale = config[CONF_SCALE] self._offset = config[CONF_OFFSET] - self._slave_count = config.get(CONF_SLAVE_COUNT, None) or config.get( + self._slave_count = config.get(CONF_SLAVE_COUNT) or config.get( CONF_VIRTUAL_COUNT, 0 ) self._slave_size = self._count = config[CONF_COUNT] @@ -206,7 +184,7 @@ class BaseStructPlatform(BasePlatform, RestoreEntity): """Do swap as needed.""" if slave_count: swapped = [] - for i in range(0, self._slave_count + 1): + for i in range(self._slave_count + 1): inx = i * self._slave_size inx2 = inx + self._slave_size swapped.extend(self._swap_registers(registers[inx:inx2], 0)) diff --git a/homeassistant/components/modbus/binary_sensor.py b/homeassistant/components/modbus/binary_sensor.py index 6c0f6422df2..314877b7927 100644 --- a/homeassistant/components/modbus/binary_sensor.py +++ b/homeassistant/components/modbus/binary_sensor.py @@ -1,4 +1,5 @@ """Support for Modbus Coil and Discrete Input sensors.""" + from __future__ import annotations from datetime import datetime @@ -92,10 +93,9 @@ class ModbusBinarySensor(BasePlatform, RestoreEntity, BinarySensorEntity): name=name, ) - slaves: list[SlaveSensor] = [] - for idx in range(0, slave_count): - slaves.append(SlaveSensor(self._coordinator, idx, entry)) - return slaves + return [ + SlaveSensor(self._coordinator, idx, entry) for idx in range(slave_count) + ] async def async_added_to_hass(self) -> None: """Handle entity which will be added.""" diff --git a/homeassistant/components/modbus/climate.py b/homeassistant/components/modbus/climate.py index a57fe53ada7..07dd12d3c94 100644 --- a/homeassistant/components/modbus/climate.py +++ b/homeassistant/components/modbus/climate.py @@ -1,4 +1,5 @@ """Support for Generic Modbus Thermostats.""" + from __future__ import annotations from datetime import datetime diff --git a/homeassistant/components/modbus/const.py b/homeassistant/components/modbus/const.py index e536a31c4f6..425bd744a1e 100644 --- a/homeassistant/components/modbus/const.py +++ b/homeassistant/components/modbus/const.py @@ -1,4 +1,5 @@ """Constants used in modbus integration.""" + from enum import Enum from homeassistant.const import ( @@ -15,7 +16,6 @@ from homeassistant.const import ( CONF_BAUDRATE = "baudrate" CONF_BYTESIZE = "bytesize" CONF_CLIMATES = "climates" -CONF_CLOSE_COMM_ON_ERROR = "close_comm_on_error" CONF_DATA_TYPE = "data_type" CONF_DEVICE_ADDRESS = "device_address" CONF_FANS = "fans" @@ -29,7 +29,6 @@ CONF_MSG_WAIT = "message_wait_milliseconds" CONF_NAN_VALUE = "nan_value" CONF_PARITY = "parity" CONF_RETRIES = "retries" -CONF_RETRY_ON_EMPTY = "retry_on_empty" CONF_PRECISION = "precision" CONF_SCALE = "scale" CONF_SLAVE_COUNT = "slave_count" diff --git a/homeassistant/components/modbus/cover.py b/homeassistant/components/modbus/cover.py index 072f1bb3d93..1221a05a5ac 100644 --- a/homeassistant/components/modbus/cover.py +++ b/homeassistant/components/modbus/cover.py @@ -1,4 +1,5 @@ """Support for Modbus covers.""" + from __future__ import annotations from datetime import datetime diff --git a/homeassistant/components/modbus/fan.py b/homeassistant/components/modbus/fan.py index e5006b66f81..93e3fdded1a 100644 --- a/homeassistant/components/modbus/fan.py +++ b/homeassistant/components/modbus/fan.py @@ -1,4 +1,5 @@ """Support for Modbus fans.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/modbus/icons.json b/homeassistant/components/modbus/icons.json new file mode 100644 index 00000000000..eeaeff6403b --- /dev/null +++ b/homeassistant/components/modbus/icons.json @@ -0,0 +1,9 @@ +{ + "services": { + "reload": "mdi:reload", + "write_coil": "mdi:pencil", + "write_register": "mdi:database-edit", + "stop": "mdi:stop", + "restart": "mdi:restart" + } +} diff --git a/homeassistant/components/modbus/light.py b/homeassistant/components/modbus/light.py index acc01f39b46..16714219bc2 100644 --- a/homeassistant/components/modbus/light.py +++ b/homeassistant/components/modbus/light.py @@ -1,4 +1,5 @@ """Support for Modbus lights.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/modbus/manifest.json b/homeassistant/components/modbus/manifest.json index 14faad789fe..956961c7e67 100644 --- a/homeassistant/components/modbus/manifest.json +++ b/homeassistant/components/modbus/manifest.json @@ -5,6 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/modbus", "iot_class": "local_polling", "loggers": ["pymodbus"], - "quality_scale": "gold", + "quality_scale": "platinum", "requirements": ["pymodbus==3.6.6"] } diff --git a/homeassistant/components/modbus/modbus.py b/homeassistant/components/modbus/modbus.py index c8e7fc3765e..0d1848e0d8e 100644 --- a/homeassistant/components/modbus/modbus.py +++ b/homeassistant/components/modbus/modbus.py @@ -1,4 +1,5 @@ """Support for Modbus.""" + from __future__ import annotations import asyncio @@ -7,8 +8,11 @@ from collections.abc import Callable import logging from typing import Any -from pymodbus.client import ModbusSerialClient, ModbusTcpClient, ModbusUdpClient -from pymodbus.client.base import ModbusBaseClient +from pymodbus.client import ( + AsyncModbusSerialClient, + AsyncModbusTcpClient, + AsyncModbusUdpClient, +) from pymodbus.exceptions import ModbusException from pymodbus.pdu import ModbusResponse from pymodbus.transaction import ModbusAsciiFramer, ModbusRtuFramer, ModbusSocketFramer @@ -30,7 +34,6 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_call_later -from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType @@ -50,11 +53,8 @@ from .const import ( CALL_TYPE_WRITE_REGISTERS, CONF_BAUDRATE, CONF_BYTESIZE, - CONF_CLOSE_COMM_ON_ERROR, CONF_MSG_WAIT, CONF_PARITY, - CONF_RETRIES, - CONF_RETRY_ON_EMPTY, CONF_STOPBITS, DEFAULT_HUB, MODBUS_DOMAIN as DOMAIN, @@ -70,6 +70,7 @@ from .const import ( TCP, UDP, ) +from .validators import check_config _LOGGER = logging.getLogger(__name__) @@ -128,6 +129,10 @@ async def async_modbus_setup( await async_setup_reload_service(hass, DOMAIN, [DOMAIN]) + if config[DOMAIN]: + config[DOMAIN] = check_config(hass, config[DOMAIN]) + if not config[DOMAIN]: + return False if DOMAIN in hass.data and config[DOMAIN] == []: hubs = hass.data[DOMAIN] for name in hubs: @@ -252,64 +257,10 @@ class ModbusHub: def __init__(self, hass: HomeAssistant, client_config: dict[str, Any]) -> None: """Initialize the Modbus hub.""" - if CONF_RETRIES in client_config: - async_create_issue( - hass, - DOMAIN, - "deprecated_retries", - breaks_in_ha_version="2024.7.0", - is_fixable=False, - severity=IssueSeverity.WARNING, - translation_key="deprecated_retries", - translation_placeholders={ - "config_key": "retries", - "integration": DOMAIN, - "url": "https://www.home-assistant.io/integrations/modbus", - }, - ) - _LOGGER.warning( - "`retries`: is deprecated and will be removed in version 2024.7" - ) - else: - client_config[CONF_RETRIES] = 3 - if CONF_CLOSE_COMM_ON_ERROR in client_config: - async_create_issue( - hass, - DOMAIN, - "deprecated_close_comm_config", - breaks_in_ha_version="2024.4.0", - is_fixable=False, - severity=IssueSeverity.WARNING, - translation_key="deprecated_close_comm_config", - translation_placeholders={ - "config_key": "close_comm_on_error", - "integration": DOMAIN, - "url": "https://www.home-assistant.io/integrations/modbus", - }, - ) - _LOGGER.warning( - "`close_comm_on_error`: is deprecated and will be removed in version 2024.4" - ) - if CONF_RETRY_ON_EMPTY in client_config: - async_create_issue( - hass, - DOMAIN, - "deprecated_retry_on_empty", - breaks_in_ha_version="2024.4.0", - is_fixable=False, - severity=IssueSeverity.WARNING, - translation_key="deprecated_retry_on_empty", - translation_placeholders={ - "config_key": "retry_on_empty", - "integration": DOMAIN, - "url": "https://www.home-assistant.io/integrations/modbus", - }, - ) - _LOGGER.warning( - "`retry_on_empty`: is deprecated and will be removed in version 2024.4" - ) # generic configuration - self._client: ModbusBaseClient | None = None + self._client: ( + AsyncModbusSerialClient | AsyncModbusTcpClient | AsyncModbusUdpClient | None + ) = None self._async_cancel_listener: Callable[[], None] | None = None self._in_error = False self._lock = asyncio.Lock() @@ -319,10 +270,10 @@ class ModbusHub: self._config_delay = client_config[CONF_DELAY] self._pb_request: dict[str, RunEntry] = {} self._pb_class = { - SERIAL: ModbusSerialClient, - TCP: ModbusTcpClient, - UDP: ModbusUdpClient, - RTUOVERTCP: ModbusTcpClient, + SERIAL: AsyncModbusSerialClient, + TCP: AsyncModbusTcpClient, + UDP: AsyncModbusUdpClient, + RTUOVERTCP: AsyncModbusTcpClient, } self._pb_params = { "port": client_config[CONF_PORT], @@ -370,9 +321,14 @@ class ModbusHub: async def async_pb_connect(self) -> None: """Connect to device, async.""" async with self._lock: - if not await self.hass.async_add_executor_job(self.pb_connect): - err = f"{self.name} connect failed, retry in pymodbus" + try: + await self._client.connect() # type: ignore[union-attr] + except ModbusException as exception_error: + err = f"{self.name} connect failed, retry in pymodbus ({str(exception_error)})" self._log_error(err, error_state=False) + return + message = f"modbus {self.name} communication open" + _LOGGER.info(message) async def async_setup(self) -> bool: """Set up pymodbus client.""" @@ -426,26 +382,14 @@ class ModbusHub: message = f"modbus {self.name} communication closed" _LOGGER.warning(message) - def pb_connect(self) -> bool: - """Connect client.""" - try: - self._client.connect() # type: ignore[union-attr] - except ModbusException as exception_error: - self._log_error(str(exception_error), error_state=False) - return False - - message = f"modbus {self.name} communication open" - _LOGGER.info(message) - return True - - def pb_call( + async def low_level_pb_call( self, slave: int | None, address: int, value: int | list[int], use_call: str ) -> ModbusResponse | None: """Call sync. pymodbus.""" kwargs = {"slave": slave} if slave else {} entry = self._pb_request[use_call] try: - result: ModbusResponse = entry.func(address, value, **kwargs) + result: ModbusResponse = await entry.func(address, value, **kwargs) except ModbusException as exception_error: error = ( f"Error: device: {slave} address: {address} -> {str(exception_error)}" @@ -482,9 +426,7 @@ class ModbusHub: async with self._lock: if not self._client: return None - result = await self.hass.async_add_executor_job( - self.pb_call, unit, address, value, use_call - ) + result = await self.low_level_pb_call(unit, address, value, use_call) if self._msg_wait: # small delay until next request/response await asyncio.sleep(self._msg_wait) diff --git a/homeassistant/components/modbus/sensor.py b/homeassistant/components/modbus/sensor.py index 4f2a1d6dc76..6c6e1ef1830 100644 --- a/homeassistant/components/modbus/sensor.py +++ b/homeassistant/components/modbus/sensor.py @@ -1,4 +1,5 @@ """Support for Modbus Register sensors.""" + from __future__ import annotations from datetime import datetime @@ -93,10 +94,9 @@ class ModbusRegisterSensor(BaseStructPlatform, RestoreSensor, SensorEntity): name=name, ) - slaves: list[SlaveSensor] = [] - for idx in range(0, slave_count): - slaves.append(SlaveSensor(self._coordinator, idx, entry)) - return slaves + return [ + SlaveSensor(self._coordinator, idx, entry) for idx in range(slave_count) + ] async def async_added_to_hass(self) -> None: """Handle entity which will be added.""" diff --git a/homeassistant/components/modbus/strings.json b/homeassistant/components/modbus/strings.json index 12e66f5d2ca..fd93185b891 100644 --- a/homeassistant/components/modbus/strings.json +++ b/homeassistant/components/modbus/strings.json @@ -78,13 +78,25 @@ "title": "`{config_key}` configuration key is being removed", "description": "Please remove the `{config_key}` key from the {integration} entry in your configuration.yaml file and restart Home Assistant to fix this issue.\n\nThe maximum number of retries is now fixed to 3." }, - "deprecated_close_comm_config": { - "title": "`{config_key}` configuration key is being removed", - "description": "Please remove the `{config_key}` key from the {integration} entry in your `configuration.yaml` file and restart Home Assistant to fix this issue. All errors will be reported, as `lazy_error_count` is accepted but ignored." + "missing_modbus_name": { + "title": "Modbus entry with host `{sub_2}` missing name", + "description": "Please add `{sub_1}` key to the {integration} entry with host `{sub_2}` in your configuration.yaml file and restart Home Assistant to fix this issue\n\n. `{sub_1}: {sub_3}` have been added." }, - "deprecated_retry_on_empty": { - "title": "`{config_key}` configuration key is being removed", - "description": "Please remove the `{config_key}` key from the {integration} entry in your configuration.yaml file and restart Home Assistant to fix this issue.\n\nRetry on empty is automatically applied, see [the documentation]({url}) for other error handling parameters." + "duplicate_modbus_entry": { + "title": "Modbus {sub_2} host/port {sub_1} is duplicate, second entry not loaded.", + "description": "Please update {sub_2} and/or {sub_1} for the entry in your configuration.yaml file and restart Home Assistant to fix this issue." + }, + "duplicate_entity_entry": { + "title": "Modbus {sub_1} address {sub_2} is duplicate, second entry not loaded.", + "description": "An address can only be associated with on entity, Please correct the entry in your configuration.yaml file and restart Home Assistant to fix this issue." + }, + "duplicate_entity_name": { + "title": "Modbus {sub_1} is duplicate, second entry not loaded.", + "description": "A entity name must be unique, Please correct the entry in your configuration.yaml file and restart Home Assistant to fix this issue." + }, + "no_entities": { + "title": "Modbus {sub_1} contain no entities, entry not loaded.", + "description": "Please add at least one entity to Modbus {sub_1} in your configuration.yaml file and restart Home Assistant to fix this issue." } } } diff --git a/homeassistant/components/modbus/switch.py b/homeassistant/components/modbus/switch.py index 0c955ea409d..ff02e4a7a7e 100644 --- a/homeassistant/components/modbus/switch.py +++ b/homeassistant/components/modbus/switch.py @@ -1,4 +1,5 @@ """Support for Modbus switches.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/modbus/validators.py b/homeassistant/components/modbus/validators.py index 650083dc7e4..7de2ecbe604 100644 --- a/homeassistant/components/modbus/validators.py +++ b/homeassistant/components/modbus/validators.py @@ -1,4 +1,5 @@ """Validate Modbus configuration.""" + from __future__ import annotations from collections import namedtuple @@ -23,6 +24,8 @@ from homeassistant.const import ( CONF_TIMEOUT, CONF_TYPE, ) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from .const import ( CONF_DATA_TYPE, @@ -32,6 +35,8 @@ from .const import ( CONF_HVAC_MODE_REGISTER, CONF_HVAC_ONOFF_REGISTER, CONF_INPUT_TYPE, + CONF_LAZY_ERROR, + CONF_RETRIES, CONF_SLAVE_COUNT, CONF_SWAP, CONF_SWAP_BYTE, @@ -42,6 +47,7 @@ from .const import ( CONF_WRITE_TYPE, DEFAULT_HUB, DEFAULT_SCAN_INTERVAL, + MODBUS_DOMAIN as DOMAIN, PLATFORMS, SERIAL, DataType, @@ -110,6 +116,29 @@ DEFAULT_STRUCT_FORMAT = { } +def modbus_create_issue( + hass: HomeAssistant, key: str, subs: list[str], err: str +) -> None: + """Create issue modbus style.""" + async_create_issue( + hass, + DOMAIN, + key, + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key=key, + translation_placeholders={ + "sub_1": subs[0], + "sub_2": subs[1], + "sub_3": subs[2], + "integration": DOMAIN, + }, + issue_domain=DOMAIN, + learn_more_url="https://www.home-assistant.io/integrations/modbus", + ) + _LOGGER.warning(err) + + def struct_validator(config: dict[str, Any]) -> dict[str, Any]: """Sensor schema validator.""" @@ -117,8 +146,8 @@ def struct_validator(config: dict[str, Any]) -> dict[str, Any]: data_type = config[CONF_DATA_TYPE] if data_type == "int": data_type = config[CONF_DATA_TYPE] = DataType.INT16 - count = config.get(CONF_COUNT, None) - structure = config.get(CONF_STRUCTURE, None) + count = config.get(CONF_COUNT) + structure = config.get(CONF_STRUCTURE) slave_count = config.get(CONF_SLAVE_COUNT, config.get(CONF_VIRTUAL_COUNT)) validator = DEFAULT_STRUCT_FORMAT[data_type].validate_parm swap_type = config.get(CONF_SWAP) @@ -147,6 +176,8 @@ def struct_validator(config: dict[str, Any]) -> dict[str, Any]: raise vol.Invalid(error) if config[CONF_DATA_TYPE] == DataType.CUSTOM: + assert isinstance(structure, str) + assert isinstance(count, int) try: size = struct.calcsize(structure) except struct.error as err: @@ -267,7 +298,162 @@ def register_int_list_validator(value: Any) -> Any: ) -def check_config(config: dict) -> dict: +def validate_modbus( + hass: HomeAssistant, + hosts: set[str], + hub_names: set[str], + hub: dict, + hub_name_inx: int, +) -> bool: + """Validate modbus entries.""" + if CONF_RETRIES in hub: + async_create_issue( + hass, + DOMAIN, + "deprecated_retries", + breaks_in_ha_version="2024.7.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="deprecated_retries", + translation_placeholders={ + "config_key": "retries", + "integration": DOMAIN, + "url": "https://www.home-assistant.io/integrations/modbus", + }, + ) + _LOGGER.warning( + "`retries`: is deprecated and will be removed in version 2024.7" + ) + else: + hub[CONF_RETRIES] = 3 + + host: str = ( + hub[CONF_PORT] + if hub[CONF_TYPE] == SERIAL + else f"{hub[CONF_HOST]}_{hub[CONF_PORT]}" + ) + if CONF_NAME not in hub: + hub[CONF_NAME] = ( + DEFAULT_HUB if not hub_name_inx else f"{DEFAULT_HUB}_{hub_name_inx}" + ) + hub_name_inx += 1 + modbus_create_issue( + hass, + "missing_modbus_name", + [ + "name", + host, + hub[CONF_NAME], + ], + f"Modbus host/port {host} is missing name, added {hub[CONF_NAME]}!", + ) + name = hub[CONF_NAME] + if host in hosts or name in hub_names: + modbus_create_issue( + hass, + "duplicate_modbus_entry", + [ + host, + hub[CONF_NAME], + "", + ], + f"Modbus {name} host/port {host} is duplicate, not loaded!", + ) + return False + hosts.add(host) + hub_names.add(name) + return True + + +def validate_entity( + hass: HomeAssistant, + hub_name: str, + component: str, + entity: dict, + minimum_scan_interval: int, + ent_names: set[str], + ent_addr: set[str], +) -> bool: + """Validate entity.""" + if CONF_LAZY_ERROR in entity: + async_create_issue( + hass, + DOMAIN, + "removed_lazy_error_count", + breaks_in_ha_version="2024.7.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="removed_lazy_error_count", + translation_placeholders={ + "config_key": "lazy_error_count", + "integration": DOMAIN, + "url": "https://www.home-assistant.io/integrations/modbus", + }, + ) + _LOGGER.warning( + "`lazy_error_count`: is deprecated and will be removed in version 2024.7" + ) + name = f"{component}.{entity[CONF_NAME]}" + addr = f"{hub_name}{entity[CONF_ADDRESS]}" + scan_interval = entity.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) + if 0 < scan_interval < 5: + err = ( + f"{hub_name} {name} scan_interval is lower than 5 seconds, " + "which may cause Home Assistant stability issues" + ) + _LOGGER.warning(err) + entity[CONF_SCAN_INTERVAL] = scan_interval + minimum_scan_interval = min(scan_interval, minimum_scan_interval) + for conf_type in ( + CONF_INPUT_TYPE, + CONF_WRITE_TYPE, + CONF_COMMAND_ON, + CONF_COMMAND_OFF, + ): + if conf_type in entity: + addr += f"_{entity[conf_type]}" + inx = entity.get(CONF_SLAVE) or entity.get(CONF_DEVICE_ADDRESS, 0) + addr += f"_{inx}" + loc_addr: set[str] = {addr} + if CONF_TARGET_TEMP in entity: + loc_addr.add(f"{hub_name}{entity[CONF_TARGET_TEMP]}_{inx}") + if CONF_HVAC_MODE_REGISTER in entity: + loc_addr.add(f"{hub_name}{entity[CONF_HVAC_MODE_REGISTER][CONF_ADDRESS]}_{inx}") + if CONF_FAN_MODE_REGISTER in entity: + loc_addr.add(f"{hub_name}{entity[CONF_FAN_MODE_REGISTER][CONF_ADDRESS]}_{inx}") + + dup_addrs = ent_addr.intersection(loc_addr) + if len(dup_addrs) > 0: + for addr in dup_addrs: + modbus_create_issue( + hass, + "duplicate_entity_entry", + [ + f"{hub_name}/{name}", + addr, + "", + ], + f"Modbus {hub_name}/{name} address {addr} is duplicate, second entry not loaded!", + ) + return False + if name in ent_names: + modbus_create_issue( + hass, + "duplicate_entity_name", + [ + f"{hub_name}/{name}", + "", + "", + ], + f"Modbus {hub_name}/{name} is duplicate, second entry not loaded!", + ) + return False + ent_names.add(name) + ent_addr.update(loc_addr) + return True + + +def check_config(hass: HomeAssistant, config: dict) -> dict: """Do final config check.""" hosts: set[str] = set() hub_names: set[str] = set() @@ -276,97 +462,10 @@ def check_config(config: dict) -> dict: ent_names: set[str] = set() ent_addr: set[str] = set() - def validate_modbus(hub: dict, hub_name_inx: int) -> bool: - """Validate modbus entries.""" - host: str = ( - hub[CONF_PORT] - if hub[CONF_TYPE] == SERIAL - else f"{hub[CONF_HOST]}_{hub[CONF_PORT]}" - ) - if CONF_NAME not in hub: - hub[CONF_NAME] = ( - DEFAULT_HUB if not hub_name_inx else f"{DEFAULT_HUB}_{hub_name_inx}" - ) - hub_name_inx += 1 - err = f"Modbus host/port {host} is missing name, added {hub[CONF_NAME]}!" - _LOGGER.warning(err) - name = hub[CONF_NAME] - if host in hosts or name in hub_names: - err = f"Modbus {name} host/port {host} is duplicate, not loaded!" - _LOGGER.warning(err) - return False - hosts.add(host) - hub_names.add(name) - return True - - def validate_entity( - hub_name: str, - component: str, - entity: dict, - minimum_scan_interval: int, - ent_names: set, - ent_addr: set, - ) -> bool: - """Validate entity.""" - name = f"{component}.{entity[CONF_NAME]}" - addr = f"{hub_name}{entity[CONF_ADDRESS]}" - scan_interval = entity.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) - if 0 < scan_interval < 5: - _LOGGER.warning( - ( - "%s %s scan_interval(%d) is lower than 5 seconds, " - "which may cause Home Assistant stability issues" - ), - hub_name, - name, - scan_interval, - ) - entity[CONF_SCAN_INTERVAL] = scan_interval - minimum_scan_interval = min(scan_interval, minimum_scan_interval) - for conf_type in ( - CONF_INPUT_TYPE, - CONF_WRITE_TYPE, - CONF_COMMAND_ON, - CONF_COMMAND_OFF, - ): - if conf_type in entity: - addr += f"_{entity[conf_type]}" - inx = entity.get(CONF_SLAVE, None) or entity.get(CONF_DEVICE_ADDRESS, 0) - addr += f"_{inx}" - loc_addr: set[str] = {addr} - - if CONF_TARGET_TEMP in entity: - loc_addr.add(f"{hub_name}{entity[CONF_TARGET_TEMP]}_{inx}") - if CONF_HVAC_MODE_REGISTER in entity: - loc_addr.add( - f"{hub_name}{entity[CONF_HVAC_MODE_REGISTER][CONF_ADDRESS]}_{inx}" - ) - if CONF_FAN_MODE_REGISTER in entity: - loc_addr.add( - f"{hub_name}{entity[CONF_FAN_MODE_REGISTER][CONF_ADDRESS]}_{inx}" - ) - - dup_addrs = ent_addr.intersection(loc_addr) - if len(dup_addrs) > 0: - for addr in dup_addrs: - err = ( - f"Modbus {hub_name}/{name} address {addr} is duplicate, second" - " entry not loaded!" - ) - _LOGGER.warning(err) - return False - if name in ent_names: - err = f"Modbus {hub_name}/{name} is duplicate, second entry not loaded!" - _LOGGER.warning(err) - return False - ent_names.add(name) - ent_addr.update(loc_addr) - return True - hub_inx = 0 while hub_inx < len(config): hub = config[hub_inx] - if not validate_modbus(hub, hub_name_inx): + if not validate_modbus(hass, hosts, hub_names, hub, hub_name_inx): del config[hub_inx] continue minimum_scan_interval = 9999 @@ -379,6 +478,7 @@ def check_config(config: dict) -> dict: entities = hub[conf_key] while entity_inx < len(entities): if not validate_entity( + hass, hub[CONF_NAME], component, entities[entity_inx], @@ -390,10 +490,18 @@ def check_config(config: dict) -> dict: else: entity_inx += 1 if no_entities: - err = f"Modbus {hub[CONF_NAME]} contain no entities, this will cause instability, please add at least one entity!" - _LOGGER.warning(err) - # Ensure timeout is not started/handled. - hub[CONF_TIMEOUT] = -1 + modbus_create_issue( + hass, + "no_entities", + [ + hub[CONF_NAME], + "", + "", + ], + f"Modbus {hub[CONF_NAME]} contain no entities, causing instability, entry not loaded", + ) + del config[hub_inx] + continue if hub[CONF_TIMEOUT] >= minimum_scan_interval: hub[CONF_TIMEOUT] = minimum_scan_interval - 1 _LOGGER.warning( diff --git a/homeassistant/components/modem_callerid/__init__.py b/homeassistant/components/modem_callerid/__init__.py index bbdd1b05383..886e33b714b 100644 --- a/homeassistant/components/modem_callerid/__init__.py +++ b/homeassistant/components/modem_callerid/__init__.py @@ -1,4 +1,5 @@ """The Modem Caller ID integration.""" + from phone_modem import PhoneModem from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/modem_callerid/button.py b/homeassistant/components/modem_callerid/button.py index 5f9e4cf489c..3cad9062be9 100644 --- a/homeassistant/components/modem_callerid/button.py +++ b/homeassistant/components/modem_callerid/button.py @@ -1,4 +1,5 @@ """Support for Phone Modem button.""" + from __future__ import annotations from phone_modem import PhoneModem @@ -32,7 +33,6 @@ async def async_setup_entry( class PhoneModemButton(ButtonEntity): """Implementation of USB modem caller ID button.""" - _attr_icon = "mdi:phone-hangup" _attr_translation_key = "phone_modem_reject" _attr_has_entity_name = True diff --git a/homeassistant/components/modem_callerid/config_flow.py b/homeassistant/components/modem_callerid/config_flow.py index fac20073fe9..98e6708a34c 100644 --- a/homeassistant/components/modem_callerid/config_flow.py +++ b/homeassistant/components/modem_callerid/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Modem Caller ID integration.""" + from __future__ import annotations from typing import Any @@ -8,10 +9,9 @@ import serial.tools.list_ports from serial.tools.list_ports_common import ListPortInfo import voluptuous as vol -from homeassistant import config_entries from homeassistant.components import usb +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_DEVICE, CONF_NAME -from homeassistant.data_entry_flow import FlowResult from .const import DEFAULT_NAME, DOMAIN, EXCEPTIONS @@ -23,14 +23,16 @@ def _generate_unique_id(port: ListPortInfo) -> str: return f"{port.vid}:{port.pid}_{port.serial_number}_{port.manufacturer}_{port.description}" -class PhoneModemFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class PhoneModemFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a config flow for Phone Modem.""" def __init__(self) -> None: """Set up flow instance.""" self._device: str | None = None - async def async_step_usb(self, discovery_info: usb.UsbServiceInfo) -> FlowResult: + async def async_step_usb( + self, discovery_info: usb.UsbServiceInfo + ) -> ConfigFlowResult: """Handle USB Discovery.""" dev_path = discovery_info.device unique_id = f"{discovery_info.vid}:{discovery_info.pid}_{discovery_info.serial_number}_{discovery_info.manufacturer}_{discovery_info.description}" @@ -44,7 +46,7 @@ class PhoneModemFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_usb_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle USB Discovery confirmation.""" if user_input is not None: return self.async_create_entry( @@ -56,7 +58,7 @@ class PhoneModemFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initiated by the user.""" errors: dict[str, str] | None = {} if self._async_in_progress(): diff --git a/homeassistant/components/modem_callerid/const.py b/homeassistant/components/modem_callerid/const.py index d144e2afd5c..d86d2648a10 100644 --- a/homeassistant/components/modem_callerid/const.py +++ b/homeassistant/components/modem_callerid/const.py @@ -1,4 +1,5 @@ """Constants for the Modem Caller ID integration.""" + from typing import Final from phone_modem import exceptions @@ -7,7 +8,6 @@ from serial import SerialException DATA_KEY_API = "api" DEFAULT_NAME = "Phone Modem" DOMAIN = "modem_callerid" -ICON = "mdi:phone-classic" EXCEPTIONS: Final = ( FileNotFoundError, diff --git a/homeassistant/components/modem_callerid/icons.json b/homeassistant/components/modem_callerid/icons.json new file mode 100644 index 00000000000..956d941f1db --- /dev/null +++ b/homeassistant/components/modem_callerid/icons.json @@ -0,0 +1,14 @@ +{ + "entity": { + "button": { + "phone_modem_reject": { + "default": "mdi:phone-hangup" + } + }, + "sensor": { + "incoming_call": { + "default": "mdi:phone-classic" + } + } + } +} diff --git a/homeassistant/components/modem_callerid/sensor.py b/homeassistant/components/modem_callerid/sensor.py index c7c4403300a..00c821f3511 100644 --- a/homeassistant/components/modem_callerid/sensor.py +++ b/homeassistant/components/modem_callerid/sensor.py @@ -1,4 +1,5 @@ """A sensor for incoming calls using a USB modem that supports caller ID.""" + from __future__ import annotations from phone_modem import PhoneModem @@ -10,7 +11,7 @@ from homeassistant.core import Event, HomeAssistant, callback from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import CID, DATA_KEY_API, DOMAIN, ICON +from .const import CID, DATA_KEY_API, DOMAIN async def async_setup_entry( @@ -40,10 +41,10 @@ async def async_setup_entry( class ModemCalleridSensor(SensorEntity): """Implementation of USB modem caller ID sensor.""" - _attr_icon = ICON _attr_should_poll = False _attr_has_entity_name = True _attr_name = None + _attr_translation_key = "incoming_call" def __init__(self, api: PhoneModem, server_unique_id: str) -> None: """Initialize the sensor.""" diff --git a/homeassistant/components/modern_forms/__init__.py b/homeassistant/components/modern_forms/__init__.py index 5997b2aa846..5b33a85578c 100644 --- a/homeassistant/components/modern_forms/__init__.py +++ b/homeassistant/components/modern_forms/__init__.py @@ -1,4 +1,5 @@ """The Modern Forms integration.""" + from __future__ import annotations from collections.abc import Callable, Coroutine @@ -139,14 +140,12 @@ class ModernFormsDeviceEntity(CoordinatorEntity[ModernFormsDataUpdateCoordinator *, entry_id: str, coordinator: ModernFormsDataUpdateCoordinator, - icon: str | None = None, enabled_default: bool = True, ) -> None: """Initialize the Modern Forms entity.""" super().__init__(coordinator) self._attr_enabled_default = enabled_default self._entry_id = entry_id - self._attr_icon = icon @property def device_info(self) -> DeviceInfo: diff --git a/homeassistant/components/modern_forms/binary_sensor.py b/homeassistant/components/modern_forms/binary_sensor.py index b3361c3f143..0322c5e39d7 100644 --- a/homeassistant/components/modern_forms/binary_sensor.py +++ b/homeassistant/components/modern_forms/binary_sensor.py @@ -1,4 +1,5 @@ """Support for Modern Forms Binary Sensors.""" + from __future__ import annotations from homeassistant.components.binary_sensor import BinarySensorEntity @@ -40,11 +41,10 @@ class ModernFormsBinarySensor(ModernFormsDeviceEntity, BinarySensorEntity): *, entry_id: str, coordinator: ModernFormsDataUpdateCoordinator, - icon: str, key: str, ) -> None: """Initialize Modern Forms switch.""" - super().__init__(entry_id=entry_id, coordinator=coordinator, icon=icon) + super().__init__(entry_id=entry_id, coordinator=coordinator) self._attr_unique_id = f"{coordinator.data.info.mac_address}_{key}" @@ -62,7 +62,6 @@ class ModernFormsLightSleepTimerActive(ModernFormsBinarySensor): super().__init__( coordinator=coordinator, entry_id=entry_id, - icon="mdi:av-timer", key="light_sleep_timer_active", ) @@ -94,7 +93,6 @@ class ModernFormsFanSleepTimerActive(ModernFormsBinarySensor): super().__init__( coordinator=coordinator, entry_id=entry_id, - icon="mdi:av-timer", key="fan_sleep_timer_active", ) diff --git a/homeassistant/components/modern_forms/config_flow.py b/homeassistant/components/modern_forms/config_flow.py index f8f3e2f1dc1..c2b88d65a1b 100644 --- a/homeassistant/components/modern_forms/config_flow.py +++ b/homeassistant/components/modern_forms/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Modern Forms.""" + from __future__ import annotations from typing import Any @@ -7,9 +8,8 @@ from aiomodernforms import ModernFormsConnectionError, ModernFormsDevice import voluptuous as vol from homeassistant.components import zeroconf -from homeassistant.config_entries import SOURCE_ZEROCONF, ConfigFlow +from homeassistant.config_entries import SOURCE_ZEROCONF, ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN @@ -22,13 +22,13 @@ class ModernFormsFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle setup by user for Modern Forms integration.""" return await self._handle_config_flow(user_input) async def async_step_zeroconf( self, discovery_info: zeroconf.ZeroconfServiceInfo - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle zeroconf discovery.""" host = discovery_info.hostname.rstrip(".") name, _ = host.rsplit(".") @@ -47,13 +47,13 @@ class ModernFormsFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_zeroconf_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initiated by zeroconf.""" return await self._handle_config_flow(user_input) async def _handle_config_flow( self, user_input: dict[str, Any] | None = None, prepare: bool = False - ) -> FlowResult: + ) -> ConfigFlowResult: """Config flow handler for ModernForms.""" source = self.context.get("source") @@ -97,7 +97,7 @@ class ModernFormsFlowHandler(ConfigFlow, domain=DOMAIN): data={CONF_HOST: user_input[CONF_HOST], CONF_MAC: user_input[CONF_MAC]}, ) - def _show_setup_form(self, errors: dict | None = None) -> FlowResult: + def _show_setup_form(self, errors: dict | None = None) -> ConfigFlowResult: """Show the setup form to the user.""" return self.async_show_form( step_id="user", @@ -105,7 +105,7 @@ class ModernFormsFlowHandler(ConfigFlow, domain=DOMAIN): errors=errors or {}, ) - def _show_confirm_dialog(self, errors: dict | None = None) -> FlowResult: + def _show_confirm_dialog(self, errors: dict | None = None) -> ConfigFlowResult: """Show the confirm dialog to the user.""" name = self.context.get(CONF_NAME) return self.async_show_form( diff --git a/homeassistant/components/modern_forms/fan.py b/homeassistant/components/modern_forms/fan.py index e6bcff715b8..b714cf04879 100644 --- a/homeassistant/components/modern_forms/fan.py +++ b/homeassistant/components/modern_forms/fan.py @@ -1,4 +1,5 @@ """Support for Modern Forms Fan Fans.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/modern_forms/icons.json b/homeassistant/components/modern_forms/icons.json new file mode 100644 index 00000000000..e5df55dc15e --- /dev/null +++ b/homeassistant/components/modern_forms/icons.json @@ -0,0 +1,34 @@ +{ + "entity": { + "binary_sensor": { + "light_sleep_timer_active": { + "default": "mdi:av-timer" + }, + "fan_sleep_timer_active": { + "default": "mdi:av-timer" + } + }, + "sensor": { + "light_timer_remaining_time": { + "default": "mdi:timer-outline" + }, + "fan_timer_remaining_time": { + "default": "mdi:timer-outline" + } + }, + "switch": { + "away_mode": { + "default": "mdi:airplane-takeoff" + }, + "adaptive_learning": { + "default": "mdi:school-outline" + } + } + }, + "services": { + "set_light_sleep_timer": "mdi:timer", + "clear_light_sleep_timer": "mdi:timer-cancel", + "set_fan_sleep_timer": "mdi:timer", + "clear_fan_sleep_timer": "mdi:timer-cancel" + } +} diff --git a/homeassistant/components/modern_forms/light.py b/homeassistant/components/modern_forms/light.py index 013d6a17d6d..3284b96d31f 100644 --- a/homeassistant/components/modern_forms/light.py +++ b/homeassistant/components/modern_forms/light.py @@ -1,4 +1,5 @@ """Support for Modern Forms Fan lights.""" + from __future__ import annotations from typing import Any @@ -90,7 +91,6 @@ class ModernFormsLightEntity(ModernFormsDeviceEntity, LightEntity): super().__init__( entry_id=entry_id, coordinator=coordinator, - icon=None, ) self._attr_unique_id = f"{self.coordinator.data.info.mac_address}" diff --git a/homeassistant/components/modern_forms/sensor.py b/homeassistant/components/modern_forms/sensor.py index efd659f3ae0..6a92f0fcac2 100644 --- a/homeassistant/components/modern_forms/sensor.py +++ b/homeassistant/components/modern_forms/sensor.py @@ -1,4 +1,5 @@ """Support for Modern Forms switches.""" + from __future__ import annotations from datetime import datetime @@ -43,12 +44,11 @@ class ModernFormsSensor(ModernFormsDeviceEntity, SensorEntity): *, entry_id: str, coordinator: ModernFormsDataUpdateCoordinator, - icon: str, key: str, ) -> None: """Initialize Modern Forms switch.""" self._key = key - super().__init__(entry_id=entry_id, coordinator=coordinator, icon=icon) + super().__init__(entry_id=entry_id, coordinator=coordinator) self._attr_unique_id = f"{self.coordinator.data.info.mac_address}_{self._key}" @@ -64,7 +64,6 @@ class ModernFormsLightTimerRemainingTimeSensor(ModernFormsSensor): super().__init__( coordinator=coordinator, entry_id=entry_id, - icon="mdi:timer-outline", key="light_timer_remaining_time", ) self._attr_device_class = SensorDeviceClass.TIMESTAMP @@ -95,7 +94,6 @@ class ModernFormsFanTimerRemainingTimeSensor(ModernFormsSensor): super().__init__( coordinator=coordinator, entry_id=entry_id, - icon="mdi:timer-outline", key="fan_timer_remaining_time", ) self._attr_device_class = SensorDeviceClass.TIMESTAMP diff --git a/homeassistant/components/modern_forms/switch.py b/homeassistant/components/modern_forms/switch.py index 18d8caccbd6..d8c76d733fc 100644 --- a/homeassistant/components/modern_forms/switch.py +++ b/homeassistant/components/modern_forms/switch.py @@ -1,4 +1,5 @@ """Support for Modern Forms switches.""" + from __future__ import annotations from typing import Any @@ -39,12 +40,11 @@ class ModernFormsSwitch(ModernFormsDeviceEntity, SwitchEntity): *, entry_id: str, coordinator: ModernFormsDataUpdateCoordinator, - icon: str, key: str, ) -> None: """Initialize Modern Forms switch.""" self._key = key - super().__init__(entry_id=entry_id, coordinator=coordinator, icon=icon) + super().__init__(entry_id=entry_id, coordinator=coordinator) self._attr_unique_id = f"{self.coordinator.data.info.mac_address}_{self._key}" @@ -60,7 +60,6 @@ class ModernFormsAwaySwitch(ModernFormsSwitch): super().__init__( coordinator=coordinator, entry_id=entry_id, - icon="mdi:airplane-takeoff", key="away_mode", ) @@ -92,7 +91,6 @@ class ModernFormsAdaptiveLearningSwitch(ModernFormsSwitch): super().__init__( coordinator=coordinator, entry_id=entry_id, - icon="mdi:school-outline", key="adaptive_learning", ) diff --git a/homeassistant/components/moehlenhoff_alpha2/__init__.py b/homeassistant/components/moehlenhoff_alpha2/__init__.py index 4a4c57b676e..1611d8ac4bc 100644 --- a/homeassistant/components/moehlenhoff_alpha2/__init__.py +++ b/homeassistant/components/moehlenhoff_alpha2/__init__.py @@ -1,4 +1,5 @@ """Support for the Moehlenhoff Alpha2.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/moehlenhoff_alpha2/climate.py b/homeassistant/components/moehlenhoff_alpha2/climate.py index 063628d6d32..147e4bda2fa 100644 --- a/homeassistant/components/moehlenhoff_alpha2/climate.py +++ b/homeassistant/components/moehlenhoff_alpha2/climate.py @@ -1,4 +1,5 @@ """Support for Alpha2 room control unit via Alpha2 base.""" + import logging from typing import Any diff --git a/homeassistant/components/moehlenhoff_alpha2/config_flow.py b/homeassistant/components/moehlenhoff_alpha2/config_flow.py index a4bdfd71cce..a2a43c7bc5d 100644 --- a/homeassistant/components/moehlenhoff_alpha2/config_flow.py +++ b/homeassistant/components/moehlenhoff_alpha2/config_flow.py @@ -1,4 +1,5 @@ """Alpha2 config flow.""" + import logging from typing import Any @@ -6,9 +7,8 @@ import aiohttp from moehlenhoff_alpha2 import Alpha2Base import voluptuous as vol -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST -from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN @@ -43,7 +43,7 @@ class Alpha2BaseConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" errors = {} if user_input is not None: diff --git a/homeassistant/components/mold_indicator/sensor.py b/homeassistant/components/mold_indicator/sensor.py index ce3844475c5..6839e57c838 100644 --- a/homeassistant/components/mold_indicator/sensor.py +++ b/homeassistant/components/mold_indicator/sensor.py @@ -1,4 +1,5 @@ """Calculates mold growth indication from temperature and humidity.""" + from __future__ import annotations import logging @@ -16,14 +17,14 @@ from homeassistant.const import ( STATE_UNKNOWN, UnitOfTemperature, ) -from homeassistant.core import HomeAssistant, State, callback +from homeassistant.core import Event, HomeAssistant, State, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import ( EventStateChangedData, async_track_state_change_event, ) -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, EventType +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util.unit_conversion import TemperatureConverter from homeassistant.util.unit_system import METRIC_SYSTEM @@ -121,7 +122,7 @@ class MoldIndicator(SensorEntity): @callback def mold_indicator_sensors_state_listener( - event: EventType[EventStateChangedData], + event: Event[EventStateChangedData], ) -> None: """Handle for state changes for dependent sensors.""" new_state = event.data["new_state"] diff --git a/homeassistant/components/monoprice/__init__.py b/homeassistant/components/monoprice/__init__.py index 1c9a2fa7868..57282fb6545 100644 --- a/homeassistant/components/monoprice/__init__.py +++ b/homeassistant/components/monoprice/__init__.py @@ -1,4 +1,5 @@ """The Monoprice 6-Zone Amplifier integration.""" + import logging from pymonoprice import get_monoprice diff --git a/homeassistant/components/monoprice/config_flow.py b/homeassistant/components/monoprice/config_flow.py index 9c659d4f733..7b9113821d1 100644 --- a/homeassistant/components/monoprice/config_flow.py +++ b/homeassistant/components/monoprice/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Monoprice 6-Zone Amplifier integration.""" + from __future__ import annotations import logging @@ -7,8 +8,10 @@ from pymonoprice import get_monoprice from serial import SerialException import voluptuous as vol -from homeassistant import config_entries, core, exceptions +from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow from homeassistant.const import CONF_PORT +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError from .const import ( CONF_SOURCE_1, @@ -37,7 +40,7 @@ OPTIONS_FOR_DATA = {vol.Optional(source): str for source in SOURCES} DATA_SCHEMA = vol.Schema({vol.Required(CONF_PORT): str, **OPTIONS_FOR_DATA}) -@core.callback +@callback def _sources_from_config(data): sources_config = { str(idx + 1): data.get(source) for idx, source in enumerate(SOURCES) @@ -50,7 +53,7 @@ def _sources_from_config(data): } -async def validate_input(hass: core.HomeAssistant, data): +async def validate_input(hass: HomeAssistant, data): """Validate the user input allows us to connect. Data has the keys from DATA_SCHEMA with values provided by the user. @@ -67,7 +70,7 @@ async def validate_input(hass: core.HomeAssistant, data): return {CONF_PORT: data[CONF_PORT], CONF_SOURCES: sources} -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class MonoPriceConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Monoprice 6-Zone Amplifier.""" VERSION = 1 @@ -91,15 +94,15 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) @staticmethod - @core.callback + @callback def async_get_options_flow( - config_entry: config_entries.ConfigEntry, + config_entry: ConfigEntry, ) -> MonopriceOptionsFlowHandler: """Define the config flow to handle options.""" return MonopriceOptionsFlowHandler(config_entry) -@core.callback +@callback def _key_for_source(index, source, previous_sources): if str(index) in previous_sources: key = vol.Optional( @@ -111,14 +114,14 @@ def _key_for_source(index, source, previous_sources): return key -class MonopriceOptionsFlowHandler(config_entries.OptionsFlow): +class MonopriceOptionsFlowHandler(OptionsFlow): """Handle a Monoprice options flow.""" - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + def __init__(self, config_entry: ConfigEntry) -> None: """Initialize.""" self.config_entry = config_entry - @core.callback + @callback def _previous_sources(self): if CONF_SOURCES in self.config_entry.options: previous = self.config_entry.options[CONF_SOURCES] @@ -147,5 +150,5 @@ class MonopriceOptionsFlowHandler(config_entries.OptionsFlow): ) -class CannotConnect(exceptions.HomeAssistantError): +class CannotConnect(HomeAssistantError): """Error to indicate we cannot connect.""" diff --git a/homeassistant/components/monoprice/icons.json b/homeassistant/components/monoprice/icons.json new file mode 100644 index 00000000000..22610cc2a47 --- /dev/null +++ b/homeassistant/components/monoprice/icons.json @@ -0,0 +1,6 @@ +{ + "services": { + "snapshot": "mdi:content-copy", + "restore": "mdi:content-paste" + } +} diff --git a/homeassistant/components/monoprice/media_player.py b/homeassistant/components/monoprice/media_player.py index 92b98abf374..daf13b4d7b8 100644 --- a/homeassistant/components/monoprice/media_player.py +++ b/homeassistant/components/monoprice/media_player.py @@ -1,4 +1,5 @@ """Support for interfacing with Monoprice 6 zone home audio controller.""" + import logging from serial import SerialException diff --git a/homeassistant/components/moon/__init__.py b/homeassistant/components/moon/__init__.py index e2eaaf89948..72305548fde 100644 --- a/homeassistant/components/moon/__init__.py +++ b/homeassistant/components/moon/__init__.py @@ -1,4 +1,5 @@ """The Moon integration.""" + from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/moon/config_flow.py b/homeassistant/components/moon/config_flow.py index 08b2a4995f1..1c424c866e4 100644 --- a/homeassistant/components/moon/config_flow.py +++ b/homeassistant/components/moon/config_flow.py @@ -1,10 +1,10 @@ """Config flow to configure the Moon integration.""" + from __future__ import annotations from typing import Any -from homeassistant.config_entries import ConfigFlow -from homeassistant.data_entry_flow import FlowResult +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from .const import DEFAULT_NAME, DOMAIN @@ -16,7 +16,7 @@ class MoonConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") diff --git a/homeassistant/components/moon/const.py b/homeassistant/components/moon/const.py index 87c525758b9..3e926b4ff3e 100644 --- a/homeassistant/components/moon/const.py +++ b/homeassistant/components/moon/const.py @@ -1,4 +1,5 @@ """Constants for the Moon integration.""" + from typing import Final from homeassistant.const import Platform diff --git a/homeassistant/components/moon/icons.json b/homeassistant/components/moon/icons.json new file mode 100644 index 00000000000..77c578c8f0d --- /dev/null +++ b/homeassistant/components/moon/icons.json @@ -0,0 +1,19 @@ +{ + "entity": { + "sensor": { + "phase": { + "default": "mdi:weather-night", + "state": { + "first_quarter": "mdi:moon-first-quarter", + "full_moon": "mdi:moon-full", + "last_quarter": "mdi:moon-last-quarter", + "new_moon": "mdi:moon-new", + "waning_crescent": "mdi:moon-waning-crescent", + "waning_gibbous": "mdi:moon-waning-gibbous", + "waxing_crescent": "mdi:moon-waxing-crescent", + "waxing_gibbous": "mdi:moon-waxing-gibbous" + } + } + } + } +} diff --git a/homeassistant/components/moon/sensor.py b/homeassistant/components/moon/sensor.py index b7c5b8d7726..1e2674a24bf 100644 --- a/homeassistant/components/moon/sensor.py +++ b/homeassistant/components/moon/sensor.py @@ -1,4 +1,5 @@ """Support for tracking the moon phases.""" + from __future__ import annotations from astral import moon @@ -21,17 +22,6 @@ STATE_WANING_GIBBOUS = "waning_gibbous" STATE_WAXING_CRESCENT = "waxing_crescent" STATE_WAXING_GIBBOUS = "waxing_gibbous" -MOON_ICONS = { - STATE_FIRST_QUARTER: "mdi:moon-first-quarter", - STATE_FULL_MOON: "mdi:moon-full", - STATE_LAST_QUARTER: "mdi:moon-last-quarter", - STATE_NEW_MOON: "mdi:moon-new", - STATE_WANING_CRESCENT: "mdi:moon-waning-crescent", - STATE_WANING_GIBBOUS: "mdi:moon-waning-gibbous", - STATE_WAXING_CRESCENT: "mdi:moon-waxing-crescent", - STATE_WAXING_GIBBOUS: "mdi:moon-waxing-gibbous", -} - async def async_setup_entry( hass: HomeAssistant, @@ -89,5 +79,3 @@ class MoonSensorEntity(SensorEntity): self._attr_native_value = STATE_LAST_QUARTER else: self._attr_native_value = STATE_WANING_CRESCENT - - self._attr_icon = MOON_ICONS.get(self._attr_native_value) diff --git a/homeassistant/components/mopeka/__init__.py b/homeassistant/components/mopeka/__init__.py index a000ef0cc88..da3ee156683 100644 --- a/homeassistant/components/mopeka/__init__.py +++ b/homeassistant/components/mopeka/__init__.py @@ -1,4 +1,5 @@ """The Mopeka integration.""" + from __future__ import annotations import logging @@ -25,14 +26,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: address = entry.unique_id assert address is not None data = MopekaIOTBluetoothDeviceData() - coordinator = hass.data.setdefault(DOMAIN, {})[ - entry.entry_id - ] = PassiveBluetoothProcessorCoordinator( - hass, - _LOGGER, - address=address, - mode=BluetoothScanningMode.PASSIVE, - update_method=data.update, + coordinator = hass.data.setdefault(DOMAIN, {})[entry.entry_id] = ( + PassiveBluetoothProcessorCoordinator( + hass, + _LOGGER, + address=address, + mode=BluetoothScanningMode.PASSIVE, + update_method=data.update, + ) ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload( diff --git a/homeassistant/components/mopeka/config_flow.py b/homeassistant/components/mopeka/config_flow.py index 54a2e7bcaf3..1732157ce49 100644 --- a/homeassistant/components/mopeka/config_flow.py +++ b/homeassistant/components/mopeka/config_flow.py @@ -1,4 +1,5 @@ """Config flow for mopeka integration.""" + from __future__ import annotations from typing import Any @@ -10,9 +11,8 @@ from homeassistant.components.bluetooth import ( BluetoothServiceInfoBleak, async_discovered_service_info, ) -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_ADDRESS -from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN @@ -30,7 +30,7 @@ class MopekaConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_bluetooth( self, discovery_info: BluetoothServiceInfoBleak - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the bluetooth discovery step.""" await self.async_set_unique_id(discovery_info.address) self._abort_if_unique_id_configured() @@ -43,7 +43,7 @@ class MopekaConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_bluetooth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm discovery.""" assert self._discovered_device is not None device = self._discovered_device @@ -62,7 +62,7 @@ class MopekaConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the user step to pick discovered device.""" if user_input is not None: address = user_input[CONF_ADDRESS] diff --git a/homeassistant/components/mopeka/device.py b/homeassistant/components/mopeka/device.py index 74bc389d3ae..b1b01c07957 100644 --- a/homeassistant/components/mopeka/device.py +++ b/homeassistant/components/mopeka/device.py @@ -1,4 +1,5 @@ """Support for Mopeka devices.""" + from __future__ import annotations from mopeka_iot_ble import DeviceKey diff --git a/homeassistant/components/mopeka/sensor.py b/homeassistant/components/mopeka/sensor.py index 1cddf42c730..b4b02bb083f 100644 --- a/homeassistant/components/mopeka/sensor.py +++ b/homeassistant/components/mopeka/sensor.py @@ -1,4 +1,5 @@ """Support for Mopeka sensors.""" + from __future__ import annotations from mopeka_iot_ble import SensorUpdate diff --git a/homeassistant/components/motion_blinds/__init__.py b/homeassistant/components/motion_blinds/__init__.py index a4868c0a210..182ea310029 100644 --- a/homeassistant/components/motion_blinds/__init__.py +++ b/homeassistant/components/motion_blinds/__init__.py @@ -1,4 +1,5 @@ """The motion_blinds component.""" + import asyncio from datetime import timedelta import logging diff --git a/homeassistant/components/motion_blinds/config_flow.py b/homeassistant/components/motion_blinds/config_flow.py index 588d470bb6c..3ba215a3f4c 100644 --- a/homeassistant/components/motion_blinds/config_flow.py +++ b/homeassistant/components/motion_blinds/config_flow.py @@ -1,4 +1,5 @@ """Config flow to configure Motionblinds using their WLAN API.""" + from __future__ import annotations from typing import Any @@ -6,11 +7,15 @@ from typing import Any from motionblinds import MotionDiscovery, MotionGateway import voluptuous as vol -from homeassistant import config_entries from homeassistant.components import dhcp +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.const import CONF_API_KEY, CONF_HOST from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.device_registry import format_mac from .const import ( @@ -30,16 +35,16 @@ CONFIG_SCHEMA = vol.Schema( ) -class OptionsFlowHandler(config_entries.OptionsFlow): +class OptionsFlowHandler(OptionsFlow): """Options for the component.""" - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + def __init__(self, config_entry: ConfigEntry) -> None: """Init object.""" self.config_entry = config_entry async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Manage the options.""" errors: dict[str, str] = {} if user_input is not None: @@ -61,7 +66,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow): ) -class MotionBlindsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class MotionBlindsFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a Motionblinds config flow.""" VERSION = 1 @@ -75,12 +80,14 @@ class MotionBlindsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback def async_get_options_flow( - config_entry: config_entries.ConfigEntry, + config_entry: ConfigEntry, ) -> OptionsFlowHandler: """Get the options flow.""" return OptionsFlowHandler(config_entry) - async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: + async def async_step_dhcp( + self, discovery_info: dhcp.DhcpServiceInfo + ) -> ConfigFlowResult: """Handle discovery via dhcp.""" mac_address = format_mac(discovery_info.macaddress).replace(":", "") await self.async_set_unique_id(mac_address) @@ -107,7 +114,7 @@ class MotionBlindsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" errors = {} if user_input is not None: @@ -136,7 +143,7 @@ class MotionBlindsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_select( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle multiple motion gateways found.""" if user_input is not None: self._host = user_input["select_ip"] @@ -148,7 +155,7 @@ class MotionBlindsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_connect( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Connect to the Motion Gateway.""" errors: dict[str, str] = {} if user_input is not None: diff --git a/homeassistant/components/motion_blinds/const.py b/homeassistant/components/motion_blinds/const.py index 4d9f8a7934d..e089fd17943 100644 --- a/homeassistant/components/motion_blinds/const.py +++ b/homeassistant/components/motion_blinds/const.py @@ -1,4 +1,5 @@ """Constants for the Motionblinds component.""" + from homeassistant.const import Platform DOMAIN = "motion_blinds" diff --git a/homeassistant/components/motion_blinds/coordinator.py b/homeassistant/components/motion_blinds/coordinator.py index f0cb67a6261..b2abd205ce5 100644 --- a/homeassistant/components/motion_blinds/coordinator.py +++ b/homeassistant/components/motion_blinds/coordinator.py @@ -1,4 +1,5 @@ """DataUpdateCoordinator for Motionblinds integration.""" + import asyncio from datetime import timedelta import logging diff --git a/homeassistant/components/motion_blinds/cover.py b/homeassistant/components/motion_blinds/cover.py index 60d8aae2ff8..eb40a1d66ca 100644 --- a/homeassistant/components/motion_blinds/cover.py +++ b/homeassistant/components/motion_blinds/cover.py @@ -1,4 +1,5 @@ """Support for Motionblinds using their WLAN API.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/motion_blinds/entity.py b/homeassistant/components/motion_blinds/entity.py index 36c45c3afc2..b1495dd8ecf 100644 --- a/homeassistant/components/motion_blinds/entity.py +++ b/homeassistant/components/motion_blinds/entity.py @@ -1,4 +1,5 @@ """Support for Motionblinds using their WLAN API.""" + from __future__ import annotations from motionblinds import DEVICE_TYPES_GATEWAY, DEVICE_TYPES_WIFI, MotionGateway diff --git a/homeassistant/components/motion_blinds/gateway.py b/homeassistant/components/motion_blinds/gateway.py index ff37b640127..44f7caa74b2 100644 --- a/homeassistant/components/motion_blinds/gateway.py +++ b/homeassistant/components/motion_blinds/gateway.py @@ -1,4 +1,5 @@ """Code to handle a Motion Gateway.""" + import contextlib import logging import socket diff --git a/homeassistant/components/motion_blinds/icons.json b/homeassistant/components/motion_blinds/icons.json new file mode 100644 index 00000000000..a61c36e3f00 --- /dev/null +++ b/homeassistant/components/motion_blinds/icons.json @@ -0,0 +1,5 @@ +{ + "services": { + "set_absolute_position": "mdi:set-square" + } +} diff --git a/homeassistant/components/motion_blinds/sensor.py b/homeassistant/components/motion_blinds/sensor.py index b746b39bdf0..6418cebda0c 100644 --- a/homeassistant/components/motion_blinds/sensor.py +++ b/homeassistant/components/motion_blinds/sensor.py @@ -1,4 +1,5 @@ """Support for Motionblinds sensors.""" + from motionblinds import DEVICE_TYPES_WIFI from motionblinds.motion_blinds import DEVICE_TYPE_TDBU diff --git a/homeassistant/components/motionblinds_ble/__init__.py b/homeassistant/components/motionblinds_ble/__init__.py new file mode 100644 index 00000000000..3c6df12e878 --- /dev/null +++ b/homeassistant/components/motionblinds_ble/__init__.py @@ -0,0 +1,102 @@ +"""Motionblinds Bluetooth integration.""" + +from __future__ import annotations + +from functools import partial +import logging + +from motionblindsble.const import MotionBlindType +from motionblindsble.crypt import MotionCrypt +from motionblindsble.device import MotionDevice + +from homeassistant.components.bluetooth import ( + BluetoothCallbackMatcher, + BluetoothChange, + BluetoothScanningMode, + BluetoothServiceInfoBleak, + async_ble_device_from_address, + async_register_callback, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_ADDRESS, Platform +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.event import async_call_later +from homeassistant.helpers.typing import ConfigType + +from .const import CONF_BLIND_TYPE, CONF_MAC_CODE, DOMAIN + +_LOGGER = logging.getLogger(__name__) + +PLATFORMS: list[Platform] = [Platform.BUTTON, Platform.COVER, Platform.SELECT] + +CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN) + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up Motionblinds Bluetooth integration.""" + + _LOGGER.debug("Setting up Motionblinds Bluetooth integration") + + # The correct time is needed for encryption + _LOGGER.debug("Setting timezone for encryption: %s", hass.config.time_zone) + MotionCrypt.set_timezone(hass.config.time_zone) + + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Motionblinds Bluetooth device from a config entry.""" + + _LOGGER.debug("(%s) Setting up device", entry.data[CONF_MAC_CODE]) + + ble_device = async_ble_device_from_address(hass, entry.data[CONF_ADDRESS]) + device = MotionDevice( + ble_device if ble_device is not None else entry.data[CONF_ADDRESS], + blind_type=MotionBlindType[entry.data[CONF_BLIND_TYPE].upper()], + ) + + # Register Home Assistant functions to use in the library + device.set_create_task_factory( + partial( + entry.async_create_background_task, + hass=hass, + name=device.ble_device.address, + ) + ) + device.set_call_later_factory(partial(async_call_later, hass=hass)) + + # Register a callback that updates the BLEDevice in the library + @callback + def async_update_ble_device( + service_info: BluetoothServiceInfoBleak, change: BluetoothChange + ) -> None: + """Update the BLEDevice.""" + _LOGGER.debug("(%s) New BLE device found", service_info.address) + device.set_ble_device(service_info.device, rssi=service_info.advertisement.rssi) + + entry.async_on_unload( + async_register_callback( + hass, + async_update_ble_device, + BluetoothCallbackMatcher(address=entry.data[CONF_ADDRESS]), + BluetoothScanningMode.ACTIVE, + ) + ) + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = device + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + _LOGGER.debug("(%s) Finished setting up device", entry.data[CONF_MAC_CODE]) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload Motionblinds Bluetooth device from 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/motionblinds_ble/button.py b/homeassistant/components/motionblinds_ble/button.py new file mode 100644 index 00000000000..a099276cd85 --- /dev/null +++ b/homeassistant/components/motionblinds_ble/button.py @@ -0,0 +1,88 @@ +"""Button entities for the Motionblinds Bluetooth integration.""" + +from __future__ import annotations + +from collections.abc import Callable, Coroutine +from dataclasses import dataclass +import logging +from typing import Any + +from motionblindsble.device import MotionDevice + +from homeassistant.components.button import ButtonEntity, ButtonEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import EntityCategory +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import ATTR_CONNECT, ATTR_DISCONNECT, ATTR_FAVORITE, CONF_MAC_CODE, DOMAIN +from .entity import MotionblindsBLEEntity + +_LOGGER = logging.getLogger(__name__) + +PARALLEL_UPDATES = 0 + + +@dataclass(frozen=True, kw_only=True) +class MotionblindsBLEButtonEntityDescription(ButtonEntityDescription): + """Entity description of a button entity with command attribute.""" + + command: Callable[[MotionDevice], Coroutine[Any, Any, None]] + + +BUTTON_TYPES: list[MotionblindsBLEButtonEntityDescription] = [ + MotionblindsBLEButtonEntityDescription( + key=ATTR_CONNECT, + translation_key=ATTR_CONNECT, + entity_category=EntityCategory.CONFIG, + command=lambda device: device.connect(), + ), + MotionblindsBLEButtonEntityDescription( + key=ATTR_DISCONNECT, + translation_key=ATTR_DISCONNECT, + entity_category=EntityCategory.CONFIG, + command=lambda device: device.disconnect(), + ), + MotionblindsBLEButtonEntityDescription( + key=ATTR_FAVORITE, + translation_key=ATTR_FAVORITE, + entity_category=EntityCategory.CONFIG, + command=lambda device: device.favorite(), + ), +] + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up button entities based on a config entry.""" + + device: MotionDevice = hass.data[DOMAIN][entry.entry_id] + + async_add_entities( + MotionblindsBLEButtonEntity( + device, + entry, + entity_description, + unique_id_suffix=entity_description.key, + ) + for entity_description in BUTTON_TYPES + ) + + +class MotionblindsBLEButtonEntity(MotionblindsBLEEntity, ButtonEntity): + """Representation of a button entity.""" + + entity_description: MotionblindsBLEButtonEntityDescription + + async def async_added_to_hass(self) -> None: + """Log button entity information.""" + _LOGGER.debug( + "(%s) Setting up %s button entity", + self.entry.data[CONF_MAC_CODE], + self.entity_description.key, + ) + + async def async_press(self) -> None: + """Handle the button press.""" + await self.entity_description.command(self.device) diff --git a/homeassistant/components/motionblinds_ble/config_flow.py b/homeassistant/components/motionblinds_ble/config_flow.py new file mode 100644 index 00000000000..23302ae9624 --- /dev/null +++ b/homeassistant/components/motionblinds_ble/config_flow.py @@ -0,0 +1,214 @@ +"""Config flow for Motionblinds Bluetooth integration.""" + +from __future__ import annotations + +import logging +import re +from typing import TYPE_CHECKING, Any + +from bleak.backends.device import BLEDevice +from motionblindsble.const import DISPLAY_NAME, MotionBlindType +import voluptuous as vol + +from homeassistant.components import bluetooth +from homeassistant.components.bluetooth import BluetoothServiceInfoBleak +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult +from homeassistant.const import CONF_ADDRESS +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.selector import ( + SelectSelector, + SelectSelectorConfig, + SelectSelectorMode, +) + +from .const import ( + CONF_BLIND_TYPE, + CONF_LOCAL_NAME, + CONF_MAC_CODE, + DOMAIN, + ERROR_COULD_NOT_FIND_MOTOR, + ERROR_INVALID_MAC_CODE, + ERROR_NO_BLUETOOTH_ADAPTER, + ERROR_NO_DEVICES_FOUND, +) + +_LOGGER = logging.getLogger(__name__) + +CONFIG_SCHEMA = vol.Schema({vol.Required(CONF_MAC_CODE): str}) + + +class FlowHandler(ConfigFlow, domain=DOMAIN): + """Handle a config flow for Motionblinds Bluetooth.""" + + def __init__(self) -> None: + """Initialize a ConfigFlow.""" + self._discovery_info: BluetoothServiceInfoBleak | BLEDevice | None = None + self._mac_code: str | None = None + self._display_name: str | None = None + self._blind_type: MotionBlindType | None = None + + async def async_step_bluetooth( + self, discovery_info: BluetoothServiceInfoBleak + ) -> ConfigFlowResult: + """Handle the bluetooth discovery step.""" + _LOGGER.debug( + "Discovered Motionblinds bluetooth device: %s", discovery_info.as_dict() + ) + await self.async_set_unique_id(discovery_info.address) + self._abort_if_unique_id_configured() + + self._discovery_info = discovery_info + self._mac_code = get_mac_from_local_name(discovery_info.name) + self._display_name = DISPLAY_NAME.format(mac_code=self._mac_code) + self.context["local_name"] = discovery_info.name + self.context["title_placeholders"] = {"name": self._display_name} + + return await self.async_step_confirm() + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle a flow initialized by the user.""" + errors: dict[str, str] = {} + if user_input is not None: + mac_code = user_input[CONF_MAC_CODE] + # Discover with BLE + try: + await self.async_discover_motionblind(mac_code) + except NoBluetoothAdapter: + return self.async_abort(reason=EXCEPTION_MAP[NoBluetoothAdapter]) + except NoDevicesFound: + return self.async_abort(reason=EXCEPTION_MAP[NoDevicesFound]) + except tuple(EXCEPTION_MAP.keys()) as e: + errors = {"base": EXCEPTION_MAP.get(type(e), str(type(e)))} + return self.async_show_form( + step_id="user", data_schema=CONFIG_SCHEMA, errors=errors + ) + return await self.async_step_confirm() + + scanner_count = bluetooth.async_scanner_count(self.hass, connectable=True) + if not scanner_count: + _LOGGER.error("No bluetooth adapter found") + return self.async_abort(reason=EXCEPTION_MAP[NoBluetoothAdapter]) + + return self.async_show_form( + step_id="user", data_schema=CONFIG_SCHEMA, errors=errors + ) + + async def async_step_confirm( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Confirm a single device.""" + if user_input is not None: + self._blind_type = user_input[CONF_BLIND_TYPE] + + if TYPE_CHECKING: + assert self._discovery_info is not None + + return self.async_create_entry( + title=str(self._display_name), + data={ + CONF_ADDRESS: self._discovery_info.address, + CONF_LOCAL_NAME: self._discovery_info.name, + CONF_MAC_CODE: self._mac_code, + CONF_BLIND_TYPE: self._blind_type, + }, + ) + + return self.async_show_form( + step_id="confirm", + data_schema=vol.Schema( + { + vol.Required(CONF_BLIND_TYPE): SelectSelector( + SelectSelectorConfig( + options=[ + blind_type.name.lower() + for blind_type in MotionBlindType + ], + translation_key=CONF_BLIND_TYPE, + mode=SelectSelectorMode.DROPDOWN, + ) + ) + } + ), + description_placeholders={"display_name": self._display_name}, + ) + + async def async_discover_motionblind(self, mac_code: str) -> None: + """Discover Motionblinds initialized by the user.""" + if not is_valid_mac(mac_code): + _LOGGER.error("Invalid MAC code: %s", mac_code.upper()) + raise InvalidMACCode + + scanner_count = bluetooth.async_scanner_count(self.hass, connectable=True) + if not scanner_count: + _LOGGER.error("No bluetooth adapter found") + raise NoBluetoothAdapter + + bleak_scanner = bluetooth.async_get_scanner(self.hass) + devices = await bleak_scanner.discover() + + if len(devices) == 0: + _LOGGER.error("Could not find any bluetooth devices") + raise NoDevicesFound + + motion_device: BLEDevice | None = next( + ( + device + for device in devices + if device + and device.name + and f"MOTION_{mac_code.upper()}" in device.name + ), + None, + ) + + if motion_device is None: + _LOGGER.error("Could not find a motor with MAC code: %s", mac_code.upper()) + raise CouldNotFindMotor + + await self.async_set_unique_id(motion_device.address, raise_on_progress=False) + self._abort_if_unique_id_configured() + + self._discovery_info = motion_device + self._mac_code = mac_code.upper() + self._display_name = DISPLAY_NAME.format(mac_code=self._mac_code) + + +def is_valid_mac(data: str) -> bool: + """Validate the provided MAC address.""" + + mac_regex = r"^[0-9A-Fa-f]{4}$" + return bool(re.match(mac_regex, data)) + + +def get_mac_from_local_name(data: str) -> str | None: + """Get the MAC address from the bluetooth local name.""" + + mac_regex = r"^MOTION_([0-9A-Fa-f]{4})$" + match = re.search(mac_regex, data) + return str(match.group(1)) if match else None + + +class CouldNotFindMotor(HomeAssistantError): + """Error to indicate no motor with that MAC code could be found.""" + + +class InvalidMACCode(HomeAssistantError): + """Error to indicate the MAC code is invalid.""" + + +class NoBluetoothAdapter(HomeAssistantError): + """Error to indicate no bluetooth adapter could be found.""" + + +class NoDevicesFound(HomeAssistantError): + """Error to indicate no bluetooth devices could be found.""" + + +EXCEPTION_MAP = { + NoBluetoothAdapter: ERROR_NO_BLUETOOTH_ADAPTER, + NoDevicesFound: ERROR_NO_DEVICES_FOUND, + CouldNotFindMotor: ERROR_COULD_NOT_FIND_MOTOR, + InvalidMACCode: ERROR_INVALID_MAC_CODE, +} diff --git a/homeassistant/components/motionblinds_ble/const.py b/homeassistant/components/motionblinds_ble/const.py new file mode 100644 index 00000000000..bd88927559e --- /dev/null +++ b/homeassistant/components/motionblinds_ble/const.py @@ -0,0 +1,21 @@ +"""Constants for the Motionblinds Bluetooth integration.""" + +ATTR_CONNECT = "connect" +ATTR_DISCONNECT = "disconnect" +ATTR_FAVORITE = "favorite" +ATTR_SPEED = "speed" + +CONF_LOCAL_NAME = "local_name" +CONF_MAC_CODE = "mac_code" +CONF_BLIND_TYPE = "blind_type" + +DOMAIN = "motionblinds_ble" + +ERROR_COULD_NOT_FIND_MOTOR = "could_not_find_motor" +ERROR_INVALID_MAC_CODE = "invalid_mac_code" +ERROR_NO_BLUETOOTH_ADAPTER = "no_bluetooth_adapter" +ERROR_NO_DEVICES_FOUND = "no_devices_found" + +ICON_VERTICAL_BLIND = "mdi:blinds-vertical-closed" + +MANUFACTURER = "Motionblinds" diff --git a/homeassistant/components/motionblinds_ble/cover.py b/homeassistant/components/motionblinds_ble/cover.py new file mode 100644 index 00000000000..afeeb5b0d70 --- /dev/null +++ b/homeassistant/components/motionblinds_ble/cover.py @@ -0,0 +1,230 @@ +"""Cover entities for the Motionblinds Bluetooth integration.""" + +from __future__ import annotations + +from dataclasses import dataclass +import logging +from typing import Any + +from motionblindsble.const import MotionBlindType, MotionRunningType +from motionblindsble.device import MotionDevice + +from homeassistant.components.cover import ( + ATTR_POSITION, + ATTR_TILT_POSITION, + CoverDeviceClass, + CoverEntity, + CoverEntityDescription, + CoverEntityFeature, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import CONF_BLIND_TYPE, CONF_MAC_CODE, DOMAIN, ICON_VERTICAL_BLIND +from .entity import MotionblindsBLEEntity + +_LOGGER = logging.getLogger(__name__) + + +@dataclass(frozen=True, kw_only=True) +class MotionblindsBLECoverEntityDescription(CoverEntityDescription): + """Entity description of a cover entity with default values.""" + + key: str = CoverDeviceClass.BLIND.value + translation_key: str = CoverDeviceClass.BLIND.value + + +SHADE_ENTITY_DESCRIPTION = MotionblindsBLECoverEntityDescription( + device_class=CoverDeviceClass.SHADE +) +BLIND_ENTITY_DESCRIPTION = MotionblindsBLECoverEntityDescription( + device_class=CoverDeviceClass.BLIND +) +CURTAIN_ENTITY_DESCRIPTION = MotionblindsBLECoverEntityDescription( + device_class=CoverDeviceClass.CURTAIN +) +VERTICAL_ENTITY_DESCRIPTION = MotionblindsBLECoverEntityDescription( + device_class=CoverDeviceClass.CURTAIN, icon=ICON_VERTICAL_BLIND +) + +BLIND_TYPE_TO_ENTITY_DESCRIPTION: dict[str, MotionblindsBLECoverEntityDescription] = { + MotionBlindType.HONEYCOMB.name: SHADE_ENTITY_DESCRIPTION, + MotionBlindType.ROMAN.name: SHADE_ENTITY_DESCRIPTION, + MotionBlindType.ROLLER.name: SHADE_ENTITY_DESCRIPTION, + MotionBlindType.DOUBLE_ROLLER.name: SHADE_ENTITY_DESCRIPTION, + MotionBlindType.VENETIAN.name: BLIND_ENTITY_DESCRIPTION, + MotionBlindType.VENETIAN_TILT_ONLY.name: BLIND_ENTITY_DESCRIPTION, + MotionBlindType.CURTAIN.name: CURTAIN_ENTITY_DESCRIPTION, + MotionBlindType.VERTICAL.name: VERTICAL_ENTITY_DESCRIPTION, +} + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up cover entity based on a config entry.""" + + cover_class: type[MotionblindsBLECoverEntity] = BLIND_TYPE_TO_CLASS[ + entry.data[CONF_BLIND_TYPE].upper() + ] + device: MotionDevice = hass.data[DOMAIN][entry.entry_id] + entity_description: MotionblindsBLECoverEntityDescription = ( + BLIND_TYPE_TO_ENTITY_DESCRIPTION[entry.data[CONF_BLIND_TYPE].upper()] + ) + entity: MotionblindsBLECoverEntity = cover_class(device, entry, entity_description) + + async_add_entities([entity]) + + +class MotionblindsBLECoverEntity(MotionblindsBLEEntity, CoverEntity): + """Representation of a cover entity.""" + + _attr_is_closed: bool | None = None + _attr_name = None + + async def async_added_to_hass(self) -> None: + """Register device callbacks.""" + _LOGGER.debug( + "(%s) Added %s cover entity (%s)", + self.entry.data[CONF_MAC_CODE], + MotionBlindType[self.entry.data[CONF_BLIND_TYPE].upper()].value.lower(), + BLIND_TYPE_TO_CLASS[self.entry.data[CONF_BLIND_TYPE].upper()].__name__, + ) + self.device.register_running_callback(self.async_update_running) + self.device.register_position_callback(self.async_update_position) + + async def async_stop_cover(self, **kwargs: Any) -> None: + """Stop moving the cover entity.""" + _LOGGER.debug("(%s) Stopping", self.entry.data[CONF_MAC_CODE]) + await self.device.stop() + + @callback + def async_update_running( + self, running_type: MotionRunningType | None, write_state: bool = True + ) -> None: + """Update the running type (e.g. opening/closing) of the cover entity.""" + if running_type in {None, MotionRunningType.STILL, MotionRunningType.UNKNOWN}: + self._attr_is_opening = False + self._attr_is_closing = False + else: + self._attr_is_opening = running_type is MotionRunningType.OPENING + self._attr_is_closing = running_type is not MotionRunningType.OPENING + if running_type is not MotionRunningType.STILL: + self._attr_is_closed = None + if write_state: + self.async_write_ha_state() + + @callback + def async_update_position( + self, + position: int | None, + tilt: int | None, + ) -> None: + """Update the position of the cover entity.""" + if position is None: + self._attr_current_cover_position = None + self._attr_is_closed = None + else: + self._attr_current_cover_position = 100 - position + self._attr_is_closed = self._attr_current_cover_position == 0 + if tilt is None: + self._attr_current_cover_tilt_position = None + else: + self._attr_current_cover_tilt_position = 100 - round(100 * tilt / 180) + self.async_write_ha_state() + + +class PositionCover(MotionblindsBLECoverEntity): + """Representation of a cover entity with position capability.""" + + _attr_supported_features = ( + CoverEntityFeature.OPEN + | CoverEntityFeature.CLOSE + | CoverEntityFeature.STOP + | CoverEntityFeature.SET_POSITION + ) + + async def async_open_cover(self, **kwargs: Any) -> None: + """Open the cover entity.""" + _LOGGER.debug("(%s) Opening", self.entry.data[CONF_MAC_CODE]) + await self.device.open() + + async def async_close_cover(self, **kwargs: Any) -> None: + """Close the cover entity.""" + _LOGGER.debug("(%s) Closing", self.entry.data[CONF_MAC_CODE]) + await self.device.close() + + async def async_set_cover_position(self, **kwargs: Any) -> None: + """Move the cover entity to a specific position.""" + new_position: int = 100 - int(kwargs[ATTR_POSITION]) + + _LOGGER.debug( + "(%s) Setting position to %i", + self.entry.data[CONF_MAC_CODE], + new_position, + ) + await self.device.position(new_position) + + +class TiltCover(MotionblindsBLECoverEntity): + """Representation of a cover entity with tilt capability.""" + + _attr_supported_features = ( + CoverEntityFeature.OPEN_TILT + | CoverEntityFeature.CLOSE_TILT + | CoverEntityFeature.STOP_TILT + | CoverEntityFeature.SET_TILT_POSITION + ) + + async def async_open_cover_tilt(self, **kwargs: Any) -> None: + """Tilt the cover entity open.""" + _LOGGER.debug("(%s) Tilt opening", self.entry.data[CONF_MAC_CODE]) + await self.device.open_tilt() + + async def async_close_cover_tilt(self, **kwargs: Any) -> None: + """Tilt the cover entity closed.""" + _LOGGER.debug("(%s) Tilt closing", self.entry.data[CONF_MAC_CODE]) + await self.device.close_tilt() + + async def async_stop_cover_tilt(self, **kwargs: Any) -> None: + """Stop tilting the cover entity.""" + await self.async_stop_cover(**kwargs) + + async def async_set_cover_tilt_position(self, **kwargs: Any) -> None: + """Tilt the cover entity to a specific position.""" + new_tilt: int = 100 - int(kwargs[ATTR_TILT_POSITION]) + + _LOGGER.debug( + "(%s) Setting tilt position to %i", + self.entry.data[CONF_MAC_CODE], + new_tilt, + ) + await self.device.tilt(round(180 * new_tilt / 100)) + + +class PositionTiltCover(PositionCover, TiltCover): + """Representation of a cover entity with position & tilt capabilities.""" + + _attr_supported_features = ( + CoverEntityFeature.OPEN + | CoverEntityFeature.CLOSE + | CoverEntityFeature.STOP + | CoverEntityFeature.SET_POSITION + | CoverEntityFeature.OPEN_TILT + | CoverEntityFeature.CLOSE_TILT + | CoverEntityFeature.STOP_TILT + | CoverEntityFeature.SET_TILT_POSITION + ) + + +BLIND_TYPE_TO_CLASS: dict[str, type[MotionblindsBLECoverEntity]] = { + MotionBlindType.ROLLER.name: PositionCover, + MotionBlindType.HONEYCOMB.name: PositionCover, + MotionBlindType.ROMAN.name: PositionCover, + MotionBlindType.VENETIAN.name: PositionTiltCover, + MotionBlindType.VENETIAN_TILT_ONLY.name: TiltCover, + MotionBlindType.DOUBLE_ROLLER.name: PositionTiltCover, + MotionBlindType.CURTAIN.name: PositionCover, + MotionBlindType.VERTICAL.name: PositionTiltCover, +} diff --git a/homeassistant/components/motionblinds_ble/entity.py b/homeassistant/components/motionblinds_ble/entity.py new file mode 100644 index 00000000000..0b8171e7acd --- /dev/null +++ b/homeassistant/components/motionblinds_ble/entity.py @@ -0,0 +1,52 @@ +"""Base entities for the Motionblinds Bluetooth integration.""" + +import logging + +from motionblindsble.const import MotionBlindType +from motionblindsble.device import MotionDevice + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_ADDRESS +from homeassistant.helpers.device_registry import CONNECTION_BLUETOOTH, DeviceInfo +from homeassistant.helpers.entity import Entity, EntityDescription + +from .const import CONF_BLIND_TYPE, CONF_MAC_CODE, MANUFACTURER + +_LOGGER = logging.getLogger(__name__) + + +class MotionblindsBLEEntity(Entity): + """Base class for Motionblinds Bluetooth entities.""" + + _attr_has_entity_name = True + _attr_should_poll = False + + device: MotionDevice + entry: ConfigEntry + + def __init__( + self, + device: MotionDevice, + entry: ConfigEntry, + entity_description: EntityDescription, + unique_id_suffix: str | None = None, + ) -> None: + """Initialize the entity.""" + if unique_id_suffix is None: + self._attr_unique_id = entry.data[CONF_ADDRESS] + else: + self._attr_unique_id = f"{entry.data[CONF_ADDRESS]}_{unique_id_suffix}" + self.device = device + self.entry = entry + self.entity_description = entity_description + self._attr_device_info = DeviceInfo( + connections={(CONNECTION_BLUETOOTH, entry.data[CONF_ADDRESS])}, + manufacturer=MANUFACTURER, + model=MotionBlindType[entry.data[CONF_BLIND_TYPE].upper()].value, + name=device.display_name, + ) + + async def async_update(self) -> None: + """Update state, called by HA if there is a poll interval and by the service homeassistant.update_entity.""" + _LOGGER.debug("(%s) Updating entity", self.entry.data[CONF_MAC_CODE]) + await self.device.status_query() diff --git a/homeassistant/components/motionblinds_ble/icons.json b/homeassistant/components/motionblinds_ble/icons.json new file mode 100644 index 00000000000..c8d2b085d75 --- /dev/null +++ b/homeassistant/components/motionblinds_ble/icons.json @@ -0,0 +1,20 @@ +{ + "entity": { + "button": { + "connect": { + "default": "mdi:bluetooth" + }, + "disconnect": { + "default": "mdi:bluetooth-off" + }, + "favorite": { + "default": "mdi:star" + } + }, + "select": { + "speed": { + "default": "mdi:run-fast" + } + } + } +} diff --git a/homeassistant/components/motionblinds_ble/manifest.json b/homeassistant/components/motionblinds_ble/manifest.json new file mode 100644 index 00000000000..aa727be13f8 --- /dev/null +++ b/homeassistant/components/motionblinds_ble/manifest.json @@ -0,0 +1,18 @@ +{ + "domain": "motionblinds_ble", + "name": "Motionblinds Bluetooth", + "bluetooth": [ + { + "local_name": "MOTION_*", + "connectable": true + } + ], + "codeowners": ["@LennP", "@jerrybboy"], + "config_flow": true, + "dependencies": ["bluetooth_adapters"], + "documentation": "https://www.home-assistant.io/integrations/motionblinds_ble", + "integration_type": "device", + "iot_class": "assumed_state", + "loggers": ["motionblindsble"], + "requirements": ["motionblindsble==0.0.9"] +} diff --git a/homeassistant/components/motionblinds_ble/select.py b/homeassistant/components/motionblinds_ble/select.py new file mode 100644 index 00000000000..c297c887910 --- /dev/null +++ b/homeassistant/components/motionblinds_ble/select.py @@ -0,0 +1,79 @@ +"""Select entities for the Motionblinds Bluetooth integration.""" + +from __future__ import annotations + +import logging + +from motionblindsble.const import MotionBlindType, MotionSpeedLevel +from motionblindsble.device import MotionDevice + +from homeassistant.components.select import SelectEntity, SelectEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import EntityCategory +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import ATTR_SPEED, CONF_MAC_CODE, DOMAIN +from .entity import MotionblindsBLEEntity + +_LOGGER = logging.getLogger(__name__) + +PARALLEL_UPDATES = 0 + + +SELECT_TYPES: dict[str, SelectEntityDescription] = { + ATTR_SPEED: SelectEntityDescription( + key=ATTR_SPEED, + translation_key=ATTR_SPEED, + entity_category=EntityCategory.CONFIG, + options=["1", "2", "3"], + ) +} + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up select entities based on a config entry.""" + + device: MotionDevice = hass.data[DOMAIN][entry.entry_id] + + if device.blind_type not in {MotionBlindType.CURTAIN, MotionBlindType.VERTICAL}: + async_add_entities([SpeedSelect(device, entry, SELECT_TYPES[ATTR_SPEED])]) + + +class SpeedSelect(MotionblindsBLEEntity, SelectEntity): + """Representation of a speed select entity.""" + + def __init__( + self, + device: MotionDevice, + entry: ConfigEntry, + entity_description: SelectEntityDescription, + ) -> None: + """Initialize the speed select entity.""" + super().__init__( + device, entry, entity_description, unique_id_suffix=entity_description.key + ) + self._attr_current_option = None + + async def async_added_to_hass(self) -> None: + """Register device callbacks.""" + _LOGGER.debug( + "(%s) Setting up speed select entity", + self.entry.data[CONF_MAC_CODE], + ) + self.device.register_speed_callback(self.async_update_speed) + + @callback + def async_update_speed(self, speed_level: MotionSpeedLevel | None) -> None: + """Update the speed sensor value.""" + self._attr_current_option = str(speed_level.value) if speed_level else None + self.async_write_ha_state() + + async def async_select_option(self, option: str) -> None: + """Change the selected speed sensor value.""" + speed_level = MotionSpeedLevel(int(option)) + await self.device.speed(speed_level) + self._attr_current_option = str(speed_level.value) if speed_level else None + self.async_write_ha_state() diff --git a/homeassistant/components/motionblinds_ble/strings.json b/homeassistant/components/motionblinds_ble/strings.json new file mode 100644 index 00000000000..0bc9ad4c012 --- /dev/null +++ b/homeassistant/components/motionblinds_ble/strings.json @@ -0,0 +1,60 @@ +{ + "config": { + "abort": { + "no_bluetooth_adapter": "No bluetooth adapter found", + "no_devices_found": "Could not find any bluetooth devices" + }, + "error": { + "could_not_find_motor": "Could not find a motor with that MAC code", + "invalid_mac_code": "Invalid MAC code" + }, + "step": { + "user": { + "description": "Fill in the 4-character MAC code of your motor, for example F3ED or E3A6", + "data": { + "mac_code": "MAC code" + } + }, + "confirm": { + "description": "What kind of blind is {display_name}?" + } + } + }, + "selector": { + "blind_type": { + "options": { + "roller": "Roller blind", + "honeycomb": "Honeycomb blind", + "roman": "Roman blind", + "venetian": "Venetian blind", + "venetian_tilt_only": "Venetian blind (tilt-only)", + "double_roller": "Double roller blind", + "curtain": "Curtain blind", + "vertical": "Vertical blind" + } + } + }, + "entity": { + "button": { + "connect": { + "name": "[%key:common::action::connect%]" + }, + "disconnect": { + "name": "[%key:common::action::disconnect%]" + }, + "favorite": { + "name": "Favorite" + } + }, + "select": { + "speed": { + "name": "Speed", + "state": { + "1": "Low", + "2": "Medium", + "3": "High" + } + } + } + } +} diff --git a/homeassistant/components/motioneye/__init__.py b/homeassistant/components/motioneye/__init__.py index 37519a236ab..43869ef51de 100644 --- a/homeassistant/components/motioneye/__init__.py +++ b/homeassistant/components/motioneye/__init__.py @@ -1,4 +1,5 @@ """The motionEye integration.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/motioneye/camera.py b/homeassistant/components/motioneye/camera.py index fd3f0ec86c0..da5eb36d494 100644 --- a/homeassistant/components/motioneye/camera.py +++ b/homeassistant/components/motioneye/camera.py @@ -1,4 +1,5 @@ """The motionEye integration.""" + from __future__ import annotations from contextlib import suppress diff --git a/homeassistant/components/motioneye/config_flow.py b/homeassistant/components/motioneye/config_flow.py index 4ab4761fe0b..bbbd2bc7fba 100644 --- a/homeassistant/components/motioneye/config_flow.py +++ b/homeassistant/components/motioneye/config_flow.py @@ -1,4 +1,5 @@ """Config flow for motionEye integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -16,11 +17,11 @@ from homeassistant.config_entries import ( SOURCE_REAUTH, ConfigEntry, ConfigFlow, + ConfigFlowResult, OptionsFlow, ) from homeassistant.const import CONF_SOURCE, CONF_URL, CONF_WEBHOOK_ID from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -47,12 +48,12 @@ class MotionEyeConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" def _get_form( user_input: dict[str, Any], errors: dict[str, str] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Show the form to the user.""" url_schema: dict[vol.Required, type[str]] = {} if not self._hassio_discovery: @@ -157,11 +158,15 @@ class MotionEyeConfigFlow(ConfigFlow, domain=DOMAIN): data=user_input, ) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Handle a reauthentication flow.""" return await self.async_step_user() - async def async_step_hassio(self, discovery_info: HassioServiceInfo) -> FlowResult: + async def async_step_hassio( + self, discovery_info: HassioServiceInfo + ) -> ConfigFlowResult: """Handle Supervisor discovery.""" self._hassio_discovery = discovery_info.config await self._async_handle_discovery_without_unique_id() @@ -170,7 +175,7 @@ class MotionEyeConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_hassio_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm Supervisor discovery.""" if user_input is None and self._hassio_discovery is not None: return self.async_show_form( @@ -196,7 +201,7 @@ class MotionEyeOptionsFlow(OptionsFlow): async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Manage the options.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) diff --git a/homeassistant/components/motioneye/const.py b/homeassistant/components/motioneye/const.py index ebe4e24d6cf..15a856035e1 100644 --- a/homeassistant/components/motioneye/const.py +++ b/homeassistant/components/motioneye/const.py @@ -1,4 +1,5 @@ """Constants for the motionEye integration.""" + from datetime import timedelta from typing import Final diff --git a/homeassistant/components/motioneye/icons.json b/homeassistant/components/motioneye/icons.json new file mode 100644 index 00000000000..b0a4ea8dcb1 --- /dev/null +++ b/homeassistant/components/motioneye/icons.json @@ -0,0 +1,7 @@ +{ + "services": { + "set_text_overlay": "mdi:text-box-outline", + "action": "mdi:gesture-tap-button", + "snapshot": "mdi:camera" + } +} diff --git a/homeassistant/components/motioneye/media_source.py b/homeassistant/components/motioneye/media_source.py index 46300e3d3db..7c12b84f255 100644 --- a/homeassistant/components/motioneye/media_source.py +++ b/homeassistant/components/motioneye/media_source.py @@ -1,4 +1,5 @@ """motionEye Media Source Implementation.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/motioneye/sensor.py b/homeassistant/components/motioneye/sensor.py index 4d0abb84d46..dac4d77cdb4 100644 --- a/homeassistant/components/motioneye/sensor.py +++ b/homeassistant/components/motioneye/sensor.py @@ -1,4 +1,5 @@ """Sensor platform for motionEye.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/motioneye/switch.py b/homeassistant/components/motioneye/switch.py index 069c5edaad7..81a01587aa0 100644 --- a/homeassistant/components/motioneye/switch.py +++ b/homeassistant/components/motioneye/switch.py @@ -1,4 +1,5 @@ """Switch platform for motionEye.""" + from __future__ import annotations from types import MappingProxyType diff --git a/homeassistant/components/motionmount/__init__.py b/homeassistant/components/motionmount/__init__.py index 6f62a0731b6..28963d83d89 100644 --- a/homeassistant/components/motionmount/__init__.py +++ b/homeassistant/components/motionmount/__init__.py @@ -1,4 +1,5 @@ """The Vogel's MotionMount integration.""" + from __future__ import annotations import socket diff --git a/homeassistant/components/motionmount/binary_sensor.py b/homeassistant/components/motionmount/binary_sensor.py index 6bbed2e90c5..45b6e821440 100644 --- a/homeassistant/components/motionmount/binary_sensor.py +++ b/homeassistant/components/motionmount/binary_sensor.py @@ -1,4 +1,5 @@ """Support for MotionMount binary sensors.""" + import motionmount from homeassistant.components.binary_sensor import ( diff --git a/homeassistant/components/motionmount/config_flow.py b/homeassistant/components/motionmount/config_flow.py index a593b30201e..19d3557d36b 100644 --- a/homeassistant/components/motionmount/config_flow.py +++ b/homeassistant/components/motionmount/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Vogel's MotionMount.""" + import logging import socket from typing import Any @@ -6,10 +7,13 @@ from typing import Any import motionmount import voluptuous as vol -from homeassistant import config_entries from homeassistant.components import zeroconf +from homeassistant.config_entries import ( + DEFAULT_DISCOVERY_UNIQUE_ID, + ConfigFlow, + ConfigFlowResult, +) from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, CONF_UUID -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.device_registry import format_mac from .const import DOMAIN, EMPTY_MAC @@ -23,7 +27,7 @@ _LOGGER = logging.getLogger(__name__) # 3. New CE but old Pro FW -> It doesn't supply the mac using DNS-SD but we can read it (returning the EMPTY_MAC) # 4. New CE and new Pro FW -> Both DNS-SD and a read gives us the mac # If we can't get the mac, we use DEFAULT_DISCOVERY_UNIQUE_ID as an ID, so we can always configure a single MotionMount. Most households will only have a single MotionMount -class MotionMountFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class MotionMountFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a Vogel's MotionMount config flow.""" VERSION = 1 @@ -34,7 +38,7 @@ class MotionMountFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initiated by the user.""" if user_input is None: return self._show_setup_form() @@ -55,13 +59,13 @@ class MotionMountFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_abort(reason="already_configured") # Otherwise we try to continue with the generic uid - info[CONF_UUID] = config_entries.DEFAULT_DISCOVERY_UNIQUE_ID + info[CONF_UUID] = DEFAULT_DISCOVERY_UNIQUE_ID # If the device mac is valid we use it, otherwise we use the default id if info.get(CONF_UUID, EMPTY_MAC) != EMPTY_MAC: unique_id = info[CONF_UUID] else: - unique_id = config_entries.DEFAULT_DISCOVERY_UNIQUE_ID + unique_id = DEFAULT_DISCOVERY_UNIQUE_ID name = info.get(CONF_NAME, user_input[CONF_HOST]) @@ -77,7 +81,7 @@ class MotionMountFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_zeroconf( self, discovery_info: zeroconf.ZeroconfServiceInfo - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle zeroconf discovery.""" # Extract information from discovery @@ -137,7 +141,7 @@ class MotionMountFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_zeroconf_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a confirmation flow initiated by zeroconf.""" if user_input is None: return self.async_show_form( @@ -162,7 +166,9 @@ class MotionMountFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return {CONF_UUID: format_mac(mm.mac.hex()), CONF_NAME: mm.name} - def _show_setup_form(self, errors: dict[str, str] | None = None) -> FlowResult: + def _show_setup_form( + self, errors: dict[str, str] | None = None + ) -> ConfigFlowResult: """Show the setup form to the user.""" return self.async_show_form( step_id="user", diff --git a/homeassistant/components/motionmount/number.py b/homeassistant/components/motionmount/number.py index 476e14c3a82..3217a4558e1 100644 --- a/homeassistant/components/motionmount/number.py +++ b/homeassistant/components/motionmount/number.py @@ -1,4 +1,5 @@ """Support for MotionMount numeric control.""" + import motionmount from homeassistant.components.number import NumberEntity diff --git a/homeassistant/components/motionmount/select.py b/homeassistant/components/motionmount/select.py index ef0b1e918ae..7d8a6ccdbc4 100644 --- a/homeassistant/components/motionmount/select.py +++ b/homeassistant/components/motionmount/select.py @@ -1,4 +1,5 @@ """Support for MotionMount numeric control.""" + import motionmount from homeassistant.components.select import SelectEntity diff --git a/homeassistant/components/motionmount/sensor.py b/homeassistant/components/motionmount/sensor.py index ed3cbd7d38b..933b637b0c2 100644 --- a/homeassistant/components/motionmount/sensor.py +++ b/homeassistant/components/motionmount/sensor.py @@ -1,4 +1,5 @@ """Support for MotionMount sensors.""" + import motionmount from homeassistant.components.sensor import SensorDeviceClass, SensorEntity diff --git a/homeassistant/components/mpd/media_player.py b/homeassistant/components/mpd/media_player.py index 0721afa9d3a..7f69b7bf914 100644 --- a/homeassistant/components/mpd/media_player.py +++ b/homeassistant/components/mpd/media_player.py @@ -1,4 +1,5 @@ """Support to interact with a Music Player Daemon.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 1412ad63e68..8e866776a41 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -1,4 +1,5 @@ """Support for MQTT message handling.""" + from __future__ import annotations import asyncio @@ -83,7 +84,6 @@ from .const import ( # noqa: F401 DOMAIN, MQTT_CONNECTED, MQTT_DISCONNECTED, - PLATFORMS, RELOADABLE_PLATFORMS, TEMPLATE_ERRORS, ) @@ -98,9 +98,11 @@ from .models import ( # noqa: F401 ) from .util import ( # noqa: F401 async_create_certificate_temp_files, + async_forward_entry_setup_and_setup_discovery, async_wait_for_mqtt_client, get_mqtt_data, mqtt_config_entry_enabled, + platforms_from_config, valid_publish_topic, valid_qos_schema, valid_subscribe_topic, @@ -329,8 +331,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except vol.Invalid as err: err_str = str(err) raise ServiceValidationError( - f"Unable to publish: topic template '{msg_topic_template}' produced an " - f"invalid topic '{rendered_topic}' after rendering ({err_str})", translation_domain=DOMAIN, translation_key="invalid_publish_topic", translation_placeholders={ @@ -403,19 +403,24 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) except ConfigValidationError as ex: raise ServiceValidationError( - str(ex), translation_domain=ex.translation_domain, translation_key=ex.translation_key, translation_placeholders=ex.translation_placeholders, ) from ex + new_config: list[ConfigType] = config_yaml.get(DOMAIN, []) + platforms_used = platforms_from_config(new_config) + new_platforms = platforms_used - mqtt_data.platforms_loaded + await async_forward_entry_setup_and_setup_discovery( + hass, entry, new_platforms + ) # Check the schema before continuing reload await async_check_config_schema(hass, config_yaml) # Remove repair issues _async_remove_mqtt_issues(hass, mqtt_data) - mqtt_data.config = config_yaml.get(DOMAIN, {}) + mqtt_data.config = new_config # Reload the modern yaml platforms mqtt_platforms = async_get_platforms(hass, DOMAIN) @@ -429,7 +434,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ] await asyncio.gather(*tasks) - for _, component in mqtt_data.reload_handlers.items(): + for component in mqtt_data.reload_handlers.values(): component() # Fire event @@ -437,37 +442,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async_register_admin_service(hass, DOMAIN, SERVICE_RELOAD, _reload_config) - async def async_forward_entry_setup_and_setup_discovery( - config_entry: ConfigEntry, - conf: ConfigType, - ) -> None: - """Forward the config entry setup to the platforms and set up discovery.""" - # Local import to avoid circular dependencies - # pylint: disable-next=import-outside-toplevel - from . import device_automation, tag - - # Forward the entry setup to the MQTT platforms - await asyncio.gather( - *( - [ - device_automation.async_setup_entry(hass, config_entry), - tag.async_setup_entry(hass, config_entry), - ] - + [ - hass.config_entries.async_forward_entry_setup(entry, component) - for component in PLATFORMS - ] - ) + platforms_used = platforms_from_config(mqtt_data.config) + await async_forward_entry_setup_and_setup_discovery(hass, entry, platforms_used) + # Setup reload service after all platforms have loaded + await async_setup_reload_service() + # Setup discovery + if conf.get(CONF_DISCOVERY, DEFAULT_DISCOVERY): + await discovery.async_start( + hass, conf.get(CONF_DISCOVERY_PREFIX, DEFAULT_PREFIX), entry ) - # Setup discovery - if conf.get(CONF_DISCOVERY, DEFAULT_DISCOVERY): - await discovery.async_start( - hass, conf.get(CONF_DISCOVERY_PREFIX, DEFAULT_PREFIX), entry - ) - # Setup reload service after all platforms have loaded - await async_setup_reload_service() - - await async_forward_entry_setup_and_setup_discovery(entry, conf) return True @@ -604,9 +587,10 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await asyncio.gather( *( hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS + for component in mqtt_data.platforms_loaded ) ) + mqtt_data.platforms_loaded = set() await asyncio.sleep(0) # Unsubscribe reload dispatchers while reload_dispatchers := mqtt_data.reload_dispatchers: diff --git a/homeassistant/components/mqtt/alarm_control_panel.py b/homeassistant/components/mqtt/alarm_control_panel.py index 68aca18f249..e4614817790 100644 --- a/homeassistant/components/mqtt/alarm_control_panel.py +++ b/homeassistant/components/mqtt/alarm_control_panel.py @@ -1,4 +1,5 @@ """Control a MQTT alarm.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/mqtt/binary_sensor.py b/homeassistant/components/mqtt/binary_sensor.py index 7ab2e9ebf90..80ab11925d4 100644 --- a/homeassistant/components/mqtt/binary_sensor.py +++ b/homeassistant/components/mqtt/binary_sensor.py @@ -1,4 +1,5 @@ """Support for MQTT binary sensors.""" + from __future__ import annotations from datetime import datetime, timedelta diff --git a/homeassistant/components/mqtt/button.py b/homeassistant/components/mqtt/button.py index f0d8037b60d..f6374aaa3cd 100644 --- a/homeassistant/components/mqtt/button.py +++ b/homeassistant/components/mqtt/button.py @@ -1,4 +1,5 @@ """Support for MQTT buttons.""" + from __future__ import annotations import voluptuous as vol diff --git a/homeassistant/components/mqtt/camera.py b/homeassistant/components/mqtt/camera.py index 954cddd20f7..605d37834ec 100644 --- a/homeassistant/components/mqtt/camera.py +++ b/homeassistant/components/mqtt/camera.py @@ -1,4 +1,5 @@ """Camera that loads a picture from an MQTT topic.""" + from __future__ import annotations from base64 import b64decode diff --git a/homeassistant/components/mqtt/client.py b/homeassistant/components/mqtt/client.py index ace3cf9fd64..b2fab355c41 100644 --- a/homeassistant/components/mqtt/client.py +++ b/homeassistant/components/mqtt/client.py @@ -1,4 +1,5 @@ """Support for MQTT message handling.""" + from __future__ import annotations import asyncio @@ -811,9 +812,11 @@ class MQTT: subscriptions: list[Subscription] = [] if topic in self._simple_subscriptions: subscriptions.extend(self._simple_subscriptions[topic]) - for subscription in self._wildcard_subscriptions: - if subscription.matcher(topic): - subscriptions.append(subscription) + subscriptions.extend( + subscription + for subscription in self._wildcard_subscriptions + if subscription.matcher(topic) + ) return subscriptions @callback diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index 4e85163767c..972bf02ecea 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -1,4 +1,5 @@ """Support for MQTT climate devices.""" + from __future__ import annotations from abc import ABC, abstractmethod @@ -271,12 +272,12 @@ _PLATFORM_SCHEMA_BASE = MQTT_BASE_SCHEMA.extend( vol.Optional(CONF_FAN_MODE_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_HUMIDITY_COMMAND_TEMPLATE): cv.template, vol.Optional(CONF_HUMIDITY_COMMAND_TOPIC): valid_publish_topic, - vol.Optional(CONF_HUMIDITY_MIN, default=DEFAULT_MIN_HUMIDITY): vol.Coerce( - float - ), - vol.Optional(CONF_HUMIDITY_MAX, default=DEFAULT_MAX_HUMIDITY): vol.Coerce( - float - ), + vol.Optional( + CONF_HUMIDITY_MIN, default=DEFAULT_MIN_HUMIDITY + ): cv.positive_float, + vol.Optional( + CONF_HUMIDITY_MAX, default=DEFAULT_MAX_HUMIDITY + ): cv.positive_float, vol.Optional(CONF_HUMIDITY_STATE_TEMPLATE): cv.template, vol.Optional(CONF_HUMIDITY_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_MODE_COMMAND_TEMPLATE): cv.template, @@ -455,7 +456,7 @@ class MqttTemperatureControlEntity(MqttEntity, ABC): except ValueError: _LOGGER.error("Could not parse %s from %s", template_name, payload) - def prepare_subscribe_topics( # noqa: C901 + def prepare_subscribe_topics( self, topics: dict[str, dict[str, Any]], ) -> None: @@ -660,15 +661,12 @@ class MqttClimate(MqttTemperatureControlEntity, ClimateEntity): self._optimistic or CONF_PRESET_MODE_STATE_TOPIC not in config ) - value_templates: dict[str, Template | None] = {} - for key in VALUE_TEMPLATE_KEYS: - value_templates[key] = None - if CONF_VALUE_TEMPLATE in config: - value_templates = { - key: config.get(CONF_VALUE_TEMPLATE) for key in VALUE_TEMPLATE_KEYS - } - for key in VALUE_TEMPLATE_KEYS & config.keys(): - value_templates[key] = config[key] + value_templates: dict[str, Template | None] = { + key: config.get(CONF_VALUE_TEMPLATE) for key in VALUE_TEMPLATE_KEYS + } + value_templates.update( + {key: config[key] for key in VALUE_TEMPLATE_KEYS & config.keys()} + ) self._value_templates = { key: MqttValueTemplate( template, @@ -677,11 +675,10 @@ class MqttClimate(MqttTemperatureControlEntity, ClimateEntity): for key, template in value_templates.items() } - self._command_templates = {} - for key in COMMAND_TEMPLATE_KEYS: - self._command_templates[key] = MqttCommandTemplate( - config.get(key), entity=self - ).async_render + self._command_templates = { + key: MqttCommandTemplate(config.get(key), entity=self).async_render + for key in COMMAND_TEMPLATE_KEYS + } support = ClimateEntityFeature.TURN_ON | ClimateEntityFeature.TURN_OFF if (self._topic[CONF_TEMP_STATE_TOPIC] is not None) or ( @@ -717,7 +714,7 @@ class MqttClimate(MqttTemperatureControlEntity, ClimateEntity): self._attr_supported_features = support - def _prepare_subscribe_topics(self) -> None: # noqa: C901 + def _prepare_subscribe_topics(self) -> None: """(Re)Subscribe to topics.""" topics: dict[str, dict[str, Any]] = {} @@ -868,7 +865,7 @@ class MqttClimate(MqttTemperatureControlEntity, ClimateEntity): await self.async_set_hvac_mode(operation_mode) await super().async_set_temperature(**kwargs) - async def async_set_humidity(self, humidity: int) -> None: + async def async_set_humidity(self, humidity: float) -> None: """Set new target humidity.""" await self._set_climate_attribute( diff --git a/homeassistant/components/mqtt/config.py b/homeassistant/components/mqtt/config.py index 895a8e3b581..ed8f58218c6 100644 --- a/homeassistant/components/mqtt/config.py +++ b/homeassistant/components/mqtt/config.py @@ -1,4 +1,5 @@ """Support for MQTT message handling.""" + from __future__ import annotations import voluptuous as vol diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index 4f46dffec11..5bf0c9c1879 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -1,4 +1,5 @@ """Config flow for MQTT.""" + from __future__ import annotations from collections import OrderedDict @@ -14,7 +15,12 @@ import voluptuous as vol from homeassistant.components.file_upload import process_uploaded_file from homeassistant.components.hassio import HassioServiceInfo -from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.const import ( CONF_CLIENT_ID, CONF_DISCOVERY, @@ -26,7 +32,6 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_validation as cv from homeassistant.helpers.json import json_dumps from homeassistant.helpers.selector import ( @@ -171,7 +176,7 @@ class FlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") @@ -180,7 +185,7 @@ class FlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_broker( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm the setup.""" errors: dict[str, str] = {} fields: OrderedDict[Any, Any] = OrderedDict() @@ -211,7 +216,9 @@ class FlowHandler(ConfigFlow, domain=DOMAIN): step_id="broker", data_schema=vol.Schema(fields), errors=errors ) - async def async_step_hassio(self, discovery_info: HassioServiceInfo) -> FlowResult: + async def async_step_hassio( + self, discovery_info: HassioServiceInfo + ) -> ConfigFlowResult: """Receive a Hass.io discovery.""" await self._async_handle_discovery_without_unique_id() @@ -221,7 +228,7 @@ class FlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_hassio_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm a Hass.io discovery.""" errors: dict[str, str] = {} if TYPE_CHECKING: @@ -265,13 +272,13 @@ class MQTTOptionsFlowHandler(OptionsFlow): self.broker_config: dict[str, str | int] = {} self.options = config_entry.options - async def async_step_init(self, user_input: None = None) -> FlowResult: + async def async_step_init(self, user_input: None = None) -> ConfigFlowResult: """Manage the MQTT options.""" return await self.async_step_broker() async def async_step_broker( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Manage the MQTT broker configuration.""" errors: dict[str, str] = {} fields: OrderedDict[Any, Any] = OrderedDict() @@ -304,7 +311,7 @@ class MQTTOptionsFlowHandler(OptionsFlow): async def async_step_options( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Manage the MQTT options.""" errors = {} current_config = self.config_entry.data @@ -391,9 +398,9 @@ class MQTTOptionsFlowHandler(OptionsFlow): # build form fields: OrderedDict[vol.Marker, Any] = OrderedDict() fields[vol.Optional(CONF_DISCOVERY, default=discovery)] = BOOLEAN_SELECTOR - fields[ - vol.Optional(CONF_DISCOVERY_PREFIX, default=discovery_prefix) - ] = PUBLISH_TOPIC_SELECTOR + fields[vol.Optional(CONF_DISCOVERY_PREFIX, default=discovery_prefix)] = ( + PUBLISH_TOPIC_SELECTOR + ) # Birth message is disabled if CONF_BIRTH_MESSAGE = {} fields[ @@ -414,9 +421,9 @@ class MQTTOptionsFlowHandler(OptionsFlow): ) ] = TEXT_SELECTOR fields[vol.Optional("birth_qos", default=birth[ATTR_QOS])] = QOS_SELECTOR - fields[ - vol.Optional("birth_retain", default=birth[ATTR_RETAIN]) - ] = BOOLEAN_SELECTOR + fields[vol.Optional("birth_retain", default=birth[ATTR_RETAIN])] = ( + BOOLEAN_SELECTOR + ) # Will message is disabled if CONF_WILL_MESSAGE = {} fields[ @@ -437,9 +444,9 @@ class MQTTOptionsFlowHandler(OptionsFlow): ) ] = TEXT_SELECTOR fields[vol.Optional("will_qos", default=will[ATTR_QOS])] = QOS_SELECTOR - fields[ - vol.Optional("will_retain", default=will[ATTR_RETAIN]) - ] = BOOLEAN_SELECTOR + fields[vol.Optional("will_retain", default=will[ATTR_RETAIN])] = ( + BOOLEAN_SELECTOR + ) return self.async_show_form( step_id="options", @@ -565,7 +572,7 @@ async def async_get_broker_settings( ) schema = vol.Schema({cv.string: cv.template}) schema(validated_user_input[CONF_WS_HEADERS]) - except JSON_DECODE_EXCEPTIONS + (vol.MultipleInvalid,): + except (*JSON_DECODE_EXCEPTIONS, vol.MultipleInvalid): errors["base"] = "bad_ws_headers" return False return True diff --git a/homeassistant/components/mqtt/config_integration.py b/homeassistant/components/mqtt/config_integration.py index 0f2d617930d..2500923ca9b 100644 --- a/homeassistant/components/mqtt/config_integration.py +++ b/homeassistant/components/mqtt/config_integration.py @@ -1,4 +1,5 @@ """Support for MQTT platform config setup.""" + from __future__ import annotations import voluptuous as vol diff --git a/homeassistant/components/mqtt/const.py b/homeassistant/components/mqtt/const.py index 7f97910961d..82320cd2f11 100644 --- a/homeassistant/components/mqtt/const.py +++ b/homeassistant/components/mqtt/const.py @@ -142,34 +142,6 @@ MQTT_DISCONNECTED = "mqtt_disconnected" PAYLOAD_EMPTY_JSON = "{}" PAYLOAD_NONE = "None" -PLATFORMS = [ - Platform.ALARM_CONTROL_PANEL, - Platform.BINARY_SENSOR, - Platform.BUTTON, - Platform.CAMERA, - Platform.CLIMATE, - Platform.COVER, - Platform.DEVICE_TRACKER, - Platform.EVENT, - Platform.FAN, - Platform.HUMIDIFIER, - Platform.IMAGE, - Platform.LAWN_MOWER, - Platform.LIGHT, - Platform.LOCK, - Platform.NUMBER, - Platform.SCENE, - Platform.SELECT, - Platform.SENSOR, - Platform.SIREN, - Platform.SWITCH, - Platform.TEXT, - Platform.UPDATE, - Platform.VACUUM, - Platform.VALVE, - Platform.WATER_HEATER, -] - RELOADABLE_PLATFORMS = [ Platform.ALARM_CONTROL_PANEL, Platform.BINARY_SENSOR, diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index dce82774205..a659b1bb0c1 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -1,4 +1,5 @@ """Support for MQTT cover devices.""" + from __future__ import annotations from contextlib import suppress diff --git a/homeassistant/components/mqtt/debug_info.py b/homeassistant/components/mqtt/debug_info.py index 41614a62f30..7ff93a6bd06 100644 --- a/homeassistant/components/mqtt/debug_info.py +++ b/homeassistant/components/mqtt/debug_info.py @@ -1,4 +1,5 @@ """Helper to handle a set of topics to subscribe to.""" + from __future__ import annotations from collections import deque @@ -238,11 +239,14 @@ def info_for_config_entry(hass: HomeAssistant) -> dict[str, list[Any]]: mqtt_data = get_mqtt_data(hass) mqtt_info: dict[str, list[Any]] = {"entities": [], "triggers": []} - for entity_id in mqtt_data.debug_info_entities: - mqtt_info["entities"].append(_info_for_entity(hass, entity_id)) + mqtt_info["entities"].extend( + _info_for_entity(hass, entity_id) for entity_id in mqtt_data.debug_info_entities + ) - for trigger_key in mqtt_data.debug_info_triggers: - mqtt_info["triggers"].append(_info_for_trigger(hass, trigger_key)) + mqtt_info["triggers"].extend( + _info_for_trigger(hass, trigger_key) + for trigger_key in mqtt_data.debug_info_triggers + ) return mqtt_info @@ -258,16 +262,16 @@ def info_for_device(hass: HomeAssistant, device_id: str) -> dict[str, list[Any]] entries = er.async_entries_for_device( entity_registry, device_id, include_disabled_entities=True ) - for entry in entries: - if entry.entity_id not in mqtt_data.debug_info_entities: - continue + mqtt_info["entities"].extend( + _info_for_entity(hass, entry.entity_id) + for entry in entries + if entry.entity_id in mqtt_data.debug_info_entities + ) - mqtt_info["entities"].append(_info_for_entity(hass, entry.entity_id)) - - for trigger_key, trigger in mqtt_data.debug_info_triggers.items(): - if trigger["device_id"] != device_id: - continue - - mqtt_info["triggers"].append(_info_for_trigger(hass, trigger_key)) + mqtt_info["triggers"].extend( + _info_for_trigger(hass, trigger_key) + for trigger_key, trigger in mqtt_data.debug_info_triggers.items() + if trigger["device_id"] == device_id + ) return mqtt_info diff --git a/homeassistant/components/mqtt/device_automation.py b/homeassistant/components/mqtt/device_automation.py index c0e6f5750fb..25fb510a07e 100644 --- a/homeassistant/components/mqtt/device_automation.py +++ b/homeassistant/components/mqtt/device_automation.py @@ -1,4 +1,5 @@ """Provides device automations for MQTT.""" + from __future__ import annotations import functools diff --git a/homeassistant/components/mqtt/device_tracker.py b/homeassistant/components/mqtt/device_tracker.py index 6e5aeb8f228..417a636434f 100644 --- a/homeassistant/components/mqtt/device_tracker.py +++ b/homeassistant/components/mqtt/device_tracker.py @@ -1,4 +1,5 @@ """Support for tracking MQTT enabled devices identified.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/mqtt/device_trigger.py b/homeassistant/components/mqtt/device_trigger.py index b6d505d7c98..db94305f9d7 100644 --- a/homeassistant/components/mqtt/device_trigger.py +++ b/homeassistant/components/mqtt/device_trigger.py @@ -1,4 +1,5 @@ """Provides device automations for MQTT.""" + from __future__ import annotations from collections.abc import Callable @@ -352,24 +353,20 @@ async def async_get_triggers( ) -> list[dict[str, str]]: """List device triggers for MQTT devices.""" mqtt_data = get_mqtt_data(hass) - triggers: list[dict[str, str]] = [] if not mqtt_data.device_triggers: - return triggers + return [] - for trig in mqtt_data.device_triggers.values(): - if trig.device_id != device_id or trig.topic is None: - continue - - trigger = { + return [ + { **MQTT_TRIGGER_BASE, "device_id": device_id, "type": trig.type, "subtype": trig.subtype, } - triggers.append(trigger) - - return triggers + for trig in mqtt_data.device_triggers.values() + if trig.device_id == device_id and trig.topic is not None + ] async def async_attach_trigger( diff --git a/homeassistant/components/mqtt/diagnostics.py b/homeassistant/components/mqtt/diagnostics.py index 82bae04d2c9..9c0f59fe8c3 100644 --- a/homeassistant/components/mqtt/diagnostics.py +++ b/homeassistant/components/mqtt/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for MQTT.""" + from __future__ import annotations from typing import TYPE_CHECKING, Any @@ -94,36 +95,40 @@ def _async_device_as_dict(hass: HomeAssistant, device: DeviceEntry) -> dict[str, include_disabled_entities=True, ) - for entity_entry in entities: + def _state_dict(entity_entry: er.RegistryEntry) -> dict[str, Any] | None: state = hass.states.get(entity_entry.entity_id) - state_dict = None - if state: - state_dict = dict(state.as_dict()) + if not state: + return None - # The context doesn't provide useful information in this case. - state_dict.pop("context", None) + state_dict = dict(state.as_dict()) - entity_domain = split_entity_id(state.entity_id)[0] + # The context doesn't provide useful information in this case. + state_dict.pop("context", None) - # Retract some sensitive state attributes - if entity_domain == device_tracker.DOMAIN: - state_dict["attributes"] = async_redact_data( - state_dict["attributes"], REDACT_STATE_DEVICE_TRACKER - ) + entity_domain = split_entity_id(state.entity_id)[0] - data["entities"].append( - { - "device_class": entity_entry.device_class, - "disabled_by": entity_entry.disabled_by, - "disabled": entity_entry.disabled, - "entity_category": entity_entry.entity_category, - "entity_id": entity_entry.entity_id, - "icon": entity_entry.icon, - "original_device_class": entity_entry.original_device_class, - "original_icon": entity_entry.original_icon, - "state": state_dict, - "unit_of_measurement": entity_entry.unit_of_measurement, - } - ) + # Retract some sensitive state attributes + if entity_domain == device_tracker.DOMAIN: + state_dict["attributes"] = async_redact_data( + state_dict["attributes"], REDACT_STATE_DEVICE_TRACKER + ) + return state_dict + + data["entities"].extend( + { + "device_class": entity_entry.device_class, + "disabled_by": entity_entry.disabled_by, + "disabled": entity_entry.disabled, + "entity_category": entity_entry.entity_category, + "entity_id": entity_entry.entity_id, + "icon": entity_entry.icon, + "original_device_class": entity_entry.original_device_class, + "original_icon": entity_entry.original_icon, + "state": state_dict, + "unit_of_measurement": entity_entry.unit_of_measurement, + } + for entity_entry in entities + if (state_dict := _state_dict(entity_entry)) is not None + ) return data diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index 84163e217df..13c56a9b48e 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -1,4 +1,5 @@ """Support for MQTT discovery.""" + from __future__ import annotations import asyncio @@ -24,6 +25,7 @@ from homeassistant.helpers.service_info.mqtt import MqttServiceInfo from homeassistant.helpers.typing import DiscoveryInfoType from homeassistant.loader import async_get_mqtt from homeassistant.util.json import json_loads_object +from homeassistant.util.signal_type import SignalTypeFormat from .. import mqtt from .abbreviations import ABBREVIATIONS, DEVICE_ABBREVIATIONS, ORIGIN_ABBREVIATIONS @@ -39,7 +41,7 @@ from .const import ( DOMAIN, ) from .models import MqttOriginInfo, ReceiveMessage -from .util import get_mqtt_data +from .util import async_forward_entry_setup_and_setup_discovery, get_mqtt_data _LOGGER = logging.getLogger(__name__) @@ -78,9 +80,14 @@ SUPPORTED_COMPONENTS = { "water_heater", } -MQTT_DISCOVERY_UPDATED = "mqtt_discovery_updated_{}" -MQTT_DISCOVERY_NEW = "mqtt_discovery_new_{}_{}" -MQTT_DISCOVERY_DONE = "mqtt_discovery_done_{}" +MQTT_DISCOVERY_UPDATED: SignalTypeFormat[MQTTDiscoveryPayload] = SignalTypeFormat( + "mqtt_discovery_updated_{}" +) +MQTT_DISCOVERY_NEW: SignalTypeFormat[MQTTDiscoveryPayload] = SignalTypeFormat( + "mqtt_discovery_new_{}_{}" +) +MQTT_DISCOVERY_NEW_COMPONENT = "mqtt_discovery_new_component" +MQTT_DISCOVERY_DONE: SignalTypeFormat[Any] = SignalTypeFormat("mqtt_discovery_done_{}") TOPIC_BASE = "~" @@ -140,7 +147,31 @@ async def async_start( # noqa: C901 ) -> None: """Start MQTT Discovery.""" mqtt_data = get_mqtt_data(hass) - mqtt_integrations = {} + platform_setup_lock: dict[str, asyncio.Lock] = {} + + async def _async_component_setup(discovery_payload: MQTTDiscoveryPayload) -> None: + """Perform component set up.""" + discovery_hash = discovery_payload.discovery_data[ATTR_DISCOVERY_HASH] + component, discovery_id = discovery_hash + platform_setup_lock.setdefault(component, asyncio.Lock()) + async with platform_setup_lock[component]: + if component not in mqtt_data.platforms_loaded: + await async_forward_entry_setup_and_setup_discovery( + hass, config_entry, {component} + ) + # Add component + message = f"Found new component: {component} {discovery_id}" + async_log_discovery_origin_info(message, discovery_payload) + mqtt_data.discovery_already_discovered.add(discovery_hash) + async_dispatcher_send( + hass, MQTT_DISCOVERY_NEW.format(component, "mqtt"), discovery_payload + ) + + mqtt_data.reload_dispatchers.append( + async_dispatcher_connect( + hass, MQTT_DISCOVERY_NEW_COMPONENT, _async_component_setup + ) + ) @callback def async_discovery_message_received(msg: ReceiveMessage) -> None: # noqa: C901 @@ -303,7 +334,10 @@ async def async_start( # noqa: C901 "pending": deque([]), } - if already_discovered: + if component not in mqtt_data.platforms_loaded and payload: + # Load component first + async_dispatcher_send(hass, MQTT_DISCOVERY_NEW_COMPONENT, payload) + elif already_discovered: # Dispatch update message = f"Component has already been discovered: {component} {discovery_id}, sending update" async_log_discovery_origin_info(message, payload) @@ -374,14 +408,17 @@ async def async_start( # noqa: C901 ): mqtt_data.integration_unsubscribe.pop(key)() - for topic in topics: - key = f"{integration}_{topic}" - mqtt_data.integration_unsubscribe[key] = await mqtt.async_subscribe( - hass, - topic, - functools.partial(async_integration_message_received, integration), - 0, - ) + mqtt_data.integration_unsubscribe.update( + { + f"{integration}_{topic}": await mqtt.async_subscribe( + hass, + topic, + functools.partial(async_integration_message_received, integration), + 0, + ) + for topic in topics + } + ) async def async_stop(hass: HomeAssistant) -> None: diff --git a/homeassistant/components/mqtt/event.py b/homeassistant/components/mqtt/event.py index c245b66fdb1..c72791f3284 100644 --- a/homeassistant/components/mqtt/event.py +++ b/homeassistant/components/mqtt/event.py @@ -1,4 +1,5 @@ """Support for MQTT events.""" + from __future__ import annotations from collections.abc import Callable @@ -29,7 +30,6 @@ from .const import ( CONF_STATE_TOPIC, PAYLOAD_EMPTY_JSON, PAYLOAD_NONE, - TEMPLATE_ERRORS, ) from .debug_info import log_messages from .mixins import ( @@ -39,6 +39,7 @@ from .mixins import ( ) from .models import ( MqttValueTemplate, + MqttValueTemplateException, PayloadSentinel, ReceiveMessage, ReceivePayloadType, @@ -134,13 +135,13 @@ class MqttEvent(MqttEntity, EventEntity): event_type: str try: payload = self._template(msg.payload, PayloadSentinel.DEFAULT) - except TEMPLATE_ERRORS: + except MqttValueTemplateException as exc: + _LOGGER.warning(exc) return if ( not payload or payload is PayloadSentinel.DEFAULT - or payload == PAYLOAD_NONE - or payload == PAYLOAD_EMPTY_JSON + or payload in (PAYLOAD_NONE, PAYLOAD_EMPTY_JSON) ): _LOGGER.debug( "Ignoring empty payload '%s' after rendering for topic %s", diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index 24783e171c8..0fed4ab666e 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -1,4 +1,5 @@ """Support for MQTT fans.""" + from __future__ import annotations from collections.abc import Callable @@ -318,13 +319,11 @@ class MqttFan(MqttEntity, FanEntity): ATTR_PRESET_MODE: config.get(CONF_PRESET_MODE_COMMAND_TEMPLATE), ATTR_OSCILLATING: config.get(CONF_OSCILLATION_COMMAND_TEMPLATE), } - self._command_templates = {} - for key, tpl in command_templates.items(): - self._command_templates[key] = MqttCommandTemplate( - tpl, entity=self - ).async_render + self._command_templates = { + key: MqttCommandTemplate(tpl, entity=self).async_render + for key, tpl in command_templates.items() + } - self._value_templates = {} value_templates: dict[str, Template | None] = { CONF_STATE: config.get(CONF_STATE_VALUE_TEMPLATE), ATTR_DIRECTION: config.get(CONF_DIRECTION_VALUE_TEMPLATE), @@ -332,11 +331,12 @@ class MqttFan(MqttEntity, FanEntity): ATTR_PRESET_MODE: config.get(CONF_PRESET_MODE_VALUE_TEMPLATE), ATTR_OSCILLATING: config.get(CONF_OSCILLATION_VALUE_TEMPLATE), } - for key, tpl in value_templates.items(): - self._value_templates[key] = MqttValueTemplate( - tpl, - entity=self, + self._value_templates = { + key: MqttValueTemplate( + tpl, entity=self ).async_render_with_possible_json_value + for key, tpl in value_templates.items() + } def _prepare_subscribe_topics(self) -> None: """(Re)Subscribe to topics.""" diff --git a/homeassistant/components/mqtt/humidifier.py b/homeassistant/components/mqtt/humidifier.py index 75a74a0dcaa..7c9ba26389c 100644 --- a/homeassistant/components/mqtt/humidifier.py +++ b/homeassistant/components/mqtt/humidifier.py @@ -1,4 +1,5 @@ """Support for MQTT humidifiers.""" + from __future__ import annotations from collections.abc import Callable @@ -148,10 +149,10 @@ _PLATFORM_SCHEMA_BASE = MQTT_RW_SCHEMA.extend( vol.Optional(CONF_TARGET_HUMIDITY_COMMAND_TEMPLATE): cv.template, vol.Optional( CONF_TARGET_HUMIDITY_MAX, default=DEFAULT_MAX_HUMIDITY - ): cv.positive_int, + ): cv.positive_float, vol.Optional( CONF_TARGET_HUMIDITY_MIN, default=DEFAULT_MIN_HUMIDITY - ): cv.positive_int, + ): cv.positive_float, vol.Optional(CONF_TARGET_HUMIDITY_STATE_TEMPLATE): cv.template, vol.Optional(CONF_TARGET_HUMIDITY_STATE_TOPIC): valid_subscribe_topic, vol.Optional( @@ -253,18 +254,16 @@ class MqttHumidifier(MqttEntity, HumidifierEntity): ) self._optimistic_mode = optimistic or self._topic[CONF_MODE_STATE_TOPIC] is None - self._command_templates = {} command_templates: dict[str, Template | None] = { CONF_STATE: config.get(CONF_COMMAND_TEMPLATE), ATTR_HUMIDITY: config.get(CONF_TARGET_HUMIDITY_COMMAND_TEMPLATE), ATTR_MODE: config.get(CONF_MODE_COMMAND_TEMPLATE), } - for key, tpl in command_templates.items(): - self._command_templates[key] = MqttCommandTemplate( - tpl, entity=self - ).async_render + self._command_templates = { + key: MqttCommandTemplate(tpl, entity=self).async_render + for key, tpl in command_templates.items() + } - self._value_templates = {} value_templates: dict[str, Template | None] = { ATTR_ACTION: config.get(CONF_ACTION_TEMPLATE), ATTR_CURRENT_HUMIDITY: config.get(CONF_CURRENT_HUMIDITY_TEMPLATE), @@ -272,11 +271,13 @@ class MqttHumidifier(MqttEntity, HumidifierEntity): ATTR_HUMIDITY: config.get(CONF_TARGET_HUMIDITY_STATE_TEMPLATE), ATTR_MODE: config.get(CONF_MODE_STATE_TEMPLATE), } - for key, tpl in value_templates.items(): - self._value_templates[key] = MqttValueTemplate( + self._value_templates = { + key: MqttValueTemplate( tpl, entity=self, ).async_render_with_possible_json_value + for key, tpl in value_templates.items() + } def add_subscription( self, @@ -484,7 +485,7 @@ class MqttHumidifier(MqttEntity, HumidifierEntity): self._attr_is_on = False self.async_write_ha_state() - async def async_set_humidity(self, humidity: int) -> None: + async def async_set_humidity(self, humidity: float) -> None: """Set the target humidity of the humidifier. This method is a coroutine. diff --git a/homeassistant/components/mqtt/icons.json b/homeassistant/components/mqtt/icons.json new file mode 100644 index 00000000000..1979359c5a1 --- /dev/null +++ b/homeassistant/components/mqtt/icons.json @@ -0,0 +1,7 @@ +{ + "services": { + "publish": "mdi:publish", + "dump": "mdi:database-export", + "reload": "mdi:reload" + } +} diff --git a/homeassistant/components/mqtt/image.py b/homeassistant/components/mqtt/image.py index e91a8c5c259..be3956cc972 100644 --- a/homeassistant/components/mqtt/image.py +++ b/homeassistant/components/mqtt/image.py @@ -1,4 +1,5 @@ """Support for MQTT images.""" + from __future__ import annotations from base64 import b64decode @@ -24,14 +25,19 @@ from homeassistant.util import dt as dt_util from . import subscription from .config import MQTT_BASE_SCHEMA -from .const import CONF_ENCODING, CONF_QOS, TEMPLATE_ERRORS +from .const import CONF_ENCODING, CONF_QOS from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entity_entry_helper, ) -from .models import MessageCallbackType, MqttValueTemplate, ReceiveMessage +from .models import ( + MessageCallbackType, + MqttValueTemplate, + MqttValueTemplateException, + ReceiveMessage, +) from .util import get_mqtt_data, valid_subscribe_topic _LOGGER = logging.getLogger(__name__) @@ -191,7 +197,8 @@ class MqttImage(MqttEntity, ImageEntity): try: url = cv.url(self._url_template(msg.payload)) self._attr_image_url = url - except TEMPLATE_ERRORS: + except MqttValueTemplateException as exc: + _LOGGER.warning(exc) return except vol.Invalid: _LOGGER.error( diff --git a/homeassistant/components/mqtt/lawn_mower.py b/homeassistant/components/mqtt/lawn_mower.py index 924d34bf5c7..e6dc9125583 100644 --- a/homeassistant/components/mqtt/lawn_mower.py +++ b/homeassistant/components/mqtt/lawn_mower.py @@ -1,4 +1,5 @@ """Support for MQTT lawn mowers.""" + from __future__ import annotations from collections.abc import Callable @@ -173,7 +174,7 @@ class MqttLawnMower(MqttEntity, LawnMowerEntity, RestoreEntity): self._attr_activity = LawnMowerActivity(payload) except ValueError: _LOGGER.error( - "Invalid activity for %s: '%s' (valid activies: %s)", + "Invalid activity for %s: '%s' (valid activities: %s)", self.entity_id, payload, [option.value for option in LawnMowerActivity], diff --git a/homeassistant/components/mqtt/light/__init__.py b/homeassistant/components/mqtt/light/__init__.py index a5f3f0aca84..29c5cc20d91 100644 --- a/homeassistant/components/mqtt/light/__init__.py +++ b/homeassistant/components/mqtt/light/__init__.py @@ -1,4 +1,5 @@ """Support for MQTT lights.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/mqtt/light/schema.py b/homeassistant/components/mqtt/light/schema.py index 6e2ac60b28d..baec7dd40e5 100644 --- a/homeassistant/components/mqtt/light/schema.py +++ b/homeassistant/components/mqtt/light/schema.py @@ -1,4 +1,5 @@ """Shared schema code.""" + import voluptuous as vol from ..const import CONF_SCHEMA diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py index 2ca0a7e7e47..052fa394248 100644 --- a/homeassistant/components/mqtt/light/schema_basic.py +++ b/homeassistant/components/mqtt/light/schema_basic.py @@ -1,4 +1,5 @@ """Support for MQTT lights.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/mqtt/light/schema_json.py b/homeassistant/components/mqtt/light/schema_json.py index b1e5c1c18d4..6d3cd6328b8 100644 --- a/homeassistant/components/mqtt/light/schema_json.py +++ b/homeassistant/components/mqtt/light/schema_json.py @@ -1,6 +1,8 @@ """Support for MQTT JSON lights.""" + from __future__ import annotations +from collections.abc import Callable from contextlib import suppress import logging from typing import TYPE_CHECKING, Any, cast @@ -20,6 +22,7 @@ from homeassistant.components.light import ( ATTR_TRANSITION, ATTR_WHITE, ATTR_XY_COLOR, + DOMAIN as LIGHT_DOMAIN, ENTITY_ID_FORMAT, FLASH_LONG, FLASH_SHORT, @@ -43,13 +46,15 @@ from homeassistant.const import ( CONF_XY, STATE_ON, ) -from homeassistant.core import callback +from homeassistant.core import async_get_hass, callback import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.json import json_dumps from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType import homeassistant.util.color as color_util from homeassistant.util.json import json_loads_object +from homeassistant.util.yaml import dump as yaml_dump from .. import subscription from ..config import DEFAULT_QOS, DEFAULT_RETAIN, MQTT_RW_SCHEMA @@ -59,6 +64,7 @@ from ..const import ( CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, + DOMAIN as MQTT_DOMAIN, ) from ..debug_info import log_messages from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, write_state_on_attr_change @@ -100,12 +106,87 @@ CONF_MAX_MIREDS = "max_mireds" CONF_MIN_MIREDS = "min_mireds" -def valid_color_configuration(config: ConfigType) -> ConfigType: +def valid_color_configuration( + setup_from_yaml: bool, +) -> Callable[[dict[str, Any]], dict[str, Any]]: """Test color_mode is not combined with deprecated config.""" - deprecated = {CONF_COLOR_TEMP, CONF_HS, CONF_RGB, CONF_XY} - if config[CONF_COLOR_MODE] and any(config.get(key) for key in deprecated): - raise vol.Invalid(f"color_mode must not be combined with any of {deprecated}") - return config + + def _valid_color_configuration(config: ConfigType) -> ConfigType: + deprecated = {CONF_COLOR_TEMP, CONF_HS, CONF_RGB, CONF_XY} + deprecated_flags_used = any(config.get(key) for key in deprecated) + if config.get(CONF_SUPPORTED_COLOR_MODES): + if deprecated_flags_used: + raise vol.Invalid( + "supported_color_modes must not " + f"be combined with any of {deprecated}" + ) + elif deprecated_flags_used: + deprecated_flags = ", ".join(key for key in deprecated if key in config) + _LOGGER.warning( + "Deprecated flags [%s] used in MQTT JSON light config " + "for handling color mode, please use `supported_color_modes` instead. " + "Got: %s. This will stop working in Home Assistant Core 2025.3", + deprecated_flags, + config, + ) + if not setup_from_yaml: + return config + issue_id = hex(hash(frozenset(config))) + yaml_config_str = yaml_dump(config) + learn_more_url = ( + "https://www.home-assistant.io/integrations/" + f"{LIGHT_DOMAIN}.mqtt/#json-schema" + ) + hass = async_get_hass() + async_create_issue( + hass, + MQTT_DOMAIN, + issue_id, + issue_domain=LIGHT_DOMAIN, + is_fixable=False, + severity=IssueSeverity.WARNING, + learn_more_url=learn_more_url, + translation_placeholders={ + "deprecated_flags": deprecated_flags, + "config": yaml_config_str, + }, + translation_key="deprecated_color_handling", + ) + + if CONF_COLOR_MODE in config: + _LOGGER.warning( + "Deprecated flag `color_mode` used in MQTT JSON light config " + ", the `color_mode` flag is not used anymore and should be removed. " + "Got: %s. This will stop working in Home Assistant Core 2025.3", + config, + ) + if not setup_from_yaml: + return config + issue_id = hex(hash(frozenset(config))) + yaml_config_str = yaml_dump(config) + learn_more_url = ( + "https://www.home-assistant.io/integrations/" + f"{LIGHT_DOMAIN}.mqtt/#json-schema" + ) + hass = async_get_hass() + async_create_issue( + hass, + MQTT_DOMAIN, + issue_id, + breaks_in_ha_version="2025.3.0", + issue_domain=LIGHT_DOMAIN, + is_fixable=False, + severity=IssueSeverity.WARNING, + learn_more_url=learn_more_url, + translation_placeholders={ + "config": yaml_config_str, + }, + translation_key="deprecated_color_mode_flag", + ) + + return config + + return _valid_color_configuration _PLATFORM_SCHEMA_BASE = ( @@ -115,9 +196,11 @@ _PLATFORM_SCHEMA_BASE = ( vol.Optional( CONF_BRIGHTNESS_SCALE, default=DEFAULT_BRIGHTNESS_SCALE ): vol.All(vol.Coerce(int), vol.Range(min=1)), - vol.Inclusive( - CONF_COLOR_MODE, "color_mode", default=DEFAULT_COLOR_MODE - ): cv.boolean, + # CONF_COLOR_MODE was deprecated with HA Core 2024.4 and will be + # removed with HA Core 2025.3 + vol.Optional(CONF_COLOR_MODE): cv.boolean, + # CONF_COLOR_TEMP was deprecated with HA Core 2024.4 and will be + # removed with HA Core 2025.3 vol.Optional(CONF_COLOR_TEMP, default=DEFAULT_COLOR_TEMP): cv.boolean, vol.Optional(CONF_EFFECT, default=DEFAULT_EFFECT): cv.boolean, vol.Optional(CONF_EFFECT_LIST): vol.All(cv.ensure_list, [cv.string]), @@ -127,6 +210,8 @@ _PLATFORM_SCHEMA_BASE = ( vol.Optional( CONF_FLASH_TIME_SHORT, default=DEFAULT_FLASH_TIME_SHORT ): cv.positive_int, + # CONF_HS was deprecated with HA Core 2024.4 and will be + # removed with HA Core 2025.3 vol.Optional(CONF_HS, default=DEFAULT_HS): cv.boolean, vol.Optional(CONF_MAX_MIREDS): cv.positive_int, vol.Optional(CONF_MIN_MIREDS): cv.positive_int, @@ -135,9 +220,11 @@ _PLATFORM_SCHEMA_BASE = ( vol.Coerce(int), vol.In([0, 1, 2]) ), vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, + # CONF_RGB was deprecated with HA Core 2024.4 and will be + # removed with HA Core 2025.3 vol.Optional(CONF_RGB, default=DEFAULT_RGB): cv.boolean, vol.Optional(CONF_STATE_TOPIC): valid_subscribe_topic, - vol.Inclusive(CONF_SUPPORTED_COLOR_MODES, "color_mode"): vol.All( + vol.Optional(CONF_SUPPORTED_COLOR_MODES): vol.All( cv.ensure_list, [vol.In(VALID_COLOR_MODES)], vol.Unique(), @@ -146,6 +233,8 @@ _PLATFORM_SCHEMA_BASE = ( vol.Optional(CONF_WHITE_SCALE, default=DEFAULT_WHITE_SCALE): vol.All( vol.Coerce(int), vol.Range(min=1) ), + # CONF_XY was deprecated with HA Core 2024.4 and will be + # removed with HA Core 2025.3 vol.Optional(CONF_XY, default=DEFAULT_XY): cv.boolean, }, ) @@ -154,13 +243,13 @@ _PLATFORM_SCHEMA_BASE = ( ) DISCOVERY_SCHEMA_JSON = vol.All( + valid_color_configuration(False), _PLATFORM_SCHEMA_BASE.extend({}, extra=vol.REMOVE_EXTRA), - valid_color_configuration, ) PLATFORM_SCHEMA_MODERN_JSON = vol.All( + valid_color_configuration(True), _PLATFORM_SCHEMA_BASE, - valid_color_configuration, ) @@ -176,6 +265,8 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): _topic: dict[str, str | None] _optimistic: bool + _deprecated_color_handling: bool = False + @staticmethod def config_schema() -> vol.Schema: """Return the config schema.""" @@ -205,7 +296,14 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): self._attr_supported_features |= ( config[CONF_EFFECT] and LightEntityFeature.EFFECT ) - if not self._config[CONF_COLOR_MODE]: + if supported_color_modes := self._config.get(CONF_SUPPORTED_COLOR_MODES): + self._attr_supported_color_modes = supported_color_modes + if self.supported_color_modes and len(self.supported_color_modes) == 1: + self._attr_color_mode = next(iter(self.supported_color_modes)) + else: + self._attr_color_mode = ColorMode.UNKNOWN + else: + self._deprecated_color_handling = True color_modes = {ColorMode.ONOFF} if config[CONF_BRIGHTNESS]: color_modes.add(ColorMode.BRIGHTNESS) @@ -216,15 +314,9 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): self._attr_supported_color_modes = filter_supported_color_modes(color_modes) if self.supported_color_modes and len(self.supported_color_modes) == 1: self._fixed_color_mode = next(iter(self.supported_color_modes)) - else: - self._attr_supported_color_modes = self._config[CONF_SUPPORTED_COLOR_MODES] - if self.supported_color_modes and len(self.supported_color_modes) == 1: - self._attr_color_mode = next(iter(self.supported_color_modes)) - else: - self._attr_color_mode = ColorMode.UNKNOWN def _update_color(self, values: dict[str, Any]) -> None: - if not self._config[CONF_COLOR_MODE]: + if self._deprecated_color_handling: # Deprecated color handling try: red = int(values["color"]["r"]) @@ -353,7 +445,7 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): self._attr_is_on = None if ( - not self._config[CONF_COLOR_MODE] + self._deprecated_color_handling and color_supported(self.supported_color_modes) and "color" in values ): @@ -363,7 +455,7 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): else: self._update_color(values) - if self._config[CONF_COLOR_MODE] and "color_mode" in values: + if not self._deprecated_color_handling and "color_mode" in values: self._update_color(values) if brightness_supported(self.supported_color_modes): @@ -390,9 +482,9 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): ) if ( - self.supported_color_modes + self._deprecated_color_handling + and self.supported_color_modes and ColorMode.COLOR_TEMP in self.supported_color_modes - and not self._config[CONF_COLOR_MODE] ): # Deprecated color handling try: @@ -461,7 +553,7 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): @property def color_mode(self) -> ColorMode | str | None: """Return current color mode.""" - if self._config[CONF_COLOR_MODE]: + if not self._deprecated_color_handling: return self._attr_color_mode if self._fixed_color_mode: # Legacy light with support for a single color mode @@ -484,19 +576,20 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): message["flash"] = self._flash_times[CONF_FLASH_TIME_SHORT] def _scale_rgbxx(self, rgbxx: tuple[int, ...], kwargs: Any) -> tuple[int, ...]: - # If there's a brightness topic set, we don't want to scale the - # RGBxx values given using the brightness. + # If brightness is supported, we don't want to scale the + # RGBxx values given using the brightness and + # we pop the brightness, to omit it from the payload brightness: int if self._config[CONF_BRIGHTNESS]: brightness = 255 else: - brightness = kwargs.get(ATTR_BRIGHTNESS, 255) + brightness = kwargs.pop(ATTR_BRIGHTNESS, 255) return tuple(round(i / 255 * brightness) for i in rgbxx) def _supports_color_mode(self, color_mode: ColorMode | str) -> bool: """Return True if the light natively supports a color mode.""" return ( - self._config[CONF_COLOR_MODE] + not self._deprecated_color_handling and self.supported_color_modes is not None and color_mode in self.supported_color_modes ) @@ -522,12 +615,13 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): hs_color = kwargs[ATTR_HS_COLOR] message["color"] = {} if self._config[CONF_RGB]: - # If there's a brightness topic set, we don't want to scale the + # If brightness is supported, we don't want to scale the # RGB values given using the brightness. if self._config[CONF_BRIGHTNESS]: brightness = 255 else: - brightness = kwargs.get(ATTR_BRIGHTNESS, 255) + # We pop the brightness, to omit it from the payload + brightness = kwargs.pop(ATTR_BRIGHTNESS, 255) rgb = color_util.color_hsv_to_RGB( hs_color[0], hs_color[1], brightness / 255 * 100 ) @@ -595,7 +689,9 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): self._set_flash_and_transition(message, **kwargs) - if ATTR_BRIGHTNESS in kwargs and self._config[CONF_BRIGHTNESS]: + if ATTR_BRIGHTNESS in kwargs and brightness_supported( + self.supported_color_modes + ): device_brightness = color_util.brightness_to_value( (1, self._config[CONF_BRIGHTNESS_SCALE]), kwargs[ATTR_BRIGHTNESS], diff --git a/homeassistant/components/mqtt/light/schema_template.py b/homeassistant/components/mqtt/light/schema_template.py index e4900053fb3..95f97f0a736 100644 --- a/homeassistant/components/mqtt/light/schema_template.py +++ b/homeassistant/components/mqtt/light/schema_template.py @@ -1,4 +1,5 @@ """Support for MQTT Template lights.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/mqtt/lock.py b/homeassistant/components/mqtt/lock.py index 26b6009426c..79e02be9d4f 100644 --- a/homeassistant/components/mqtt/lock.py +++ b/homeassistant/components/mqtt/lock.py @@ -1,4 +1,5 @@ """Support for MQTT locks.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/mqtt/manifest.json b/homeassistant/components/mqtt/manifest.json index 2fc77fb1d4a..3a284c6719c 100644 --- a/homeassistant/components/mqtt/manifest.json +++ b/homeassistant/components/mqtt/manifest.json @@ -5,7 +5,6 @@ "config_flow": true, "dependencies": ["file_upload", "http"], "documentation": "https://www.home-assistant.io/integrations/mqtt", - "import_executor": true, "iot_class": "local_push", "quality_scale": "gold", "requirements": ["paho-mqtt==1.6.1"] diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index 5736f821f69..42ad807d2f1 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -1,4 +1,5 @@ """MQTT component mixins and helpers.""" + from __future__ import annotations from abc import ABC, abstractmethod @@ -29,7 +30,7 @@ from homeassistant.const import ( CONF_UNIQUE_ID, CONF_VALUE_TEMPLATE, ) -from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback +from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback from homeassistant.helpers import ( config_validation as cv, device_registry as dr, @@ -59,7 +60,6 @@ from homeassistant.helpers.typing import ( UNDEFINED, ConfigType, DiscoveryInfoType, - EventType, UndefinedType, ) from homeassistant.util.json import json_loads @@ -94,7 +94,6 @@ from .const import ( DOMAIN, MQTT_CONNECTED, MQTT_DISCONNECTED, - TEMPLATE_ERRORS, ) from .debug_info import log_message, log_messages from .discovery import ( @@ -109,6 +108,7 @@ from .discovery import ( from .models import ( MessageCallbackType, MqttValueTemplate, + MqttValueTemplateException, PublishPayloadType, ReceiveMessage, ) @@ -482,7 +482,8 @@ def write_state_on_attr_change( } try: msg_callback(msg) - except TEMPLATE_ERRORS: + except MqttValueTemplateException as exc: + _LOGGER.warning(exc) return if not _attrs_have_changed(tracked_attrs): return @@ -767,7 +768,7 @@ async def async_remove_discovery_payload( async def async_clear_discovery_topic_if_entity_removed( hass: HomeAssistant, discovery_data: DiscoveryInfoType, - event: EventType[er.EventEntityRegistryUpdatedData], + event: Event[er.EventEntityRegistryUpdatedData], ) -> None: """Clear the discovery topic if the entity is removed.""" if event.data["action"] == "remove": @@ -871,7 +872,7 @@ class MqttDiscoveryDeviceUpdate(ABC): return async def _async_device_removed( - self, event: EventType[EventDeviceRegistryUpdatedData] + self, event: Event[EventDeviceRegistryUpdatedData] ) -> None: """Handle the manual removal of a device.""" if self._skip_device_removal or not async_removed_from_device( @@ -1054,16 +1055,16 @@ class MqttDiscoveryUpdate(Entity): if self._discovery_data is not None: discovery_hash: tuple[str, str] = self._discovery_data[ATTR_DISCOVERY_HASH] if self.registry_entry is not None: - self._registry_hooks[ - discovery_hash - ] = async_track_entity_registry_updated_event( - self.hass, - self.entity_id, - partial( - async_clear_discovery_topic_if_entity_removed, + self._registry_hooks[discovery_hash] = ( + async_track_entity_registry_updated_event( self.hass, - self._discovery_data, - ), + self.entity_id, + partial( + async_clear_discovery_topic_if_entity_removed, + self.hass, + self._discovery_data, + ), + ) ) stop_discovery_updates(self.hass, self._discovery_data) send_discovery_done(self.hass, self._discovery_data) @@ -1342,7 +1343,7 @@ def update_device( @callback def async_removed_from_device( hass: HomeAssistant, - event: EventType[EventDeviceRegistryUpdatedData], + event: Event[EventDeviceRegistryUpdatedData], mqtt_device_id: str, config_entry_id: str, ) -> bool: diff --git a/homeassistant/components/mqtt/models.py b/homeassistant/components/mqtt/models.py index 1295bfb8ff3..6e6ae784eec 100644 --- a/homeassistant/components/mqtt/models.py +++ b/homeassistant/components/mqtt/models.py @@ -1,4 +1,5 @@ """Models used by multiple MQTT modules.""" + from __future__ import annotations from ast import literal_eval @@ -13,7 +14,7 @@ from typing import TYPE_CHECKING, Any, TypedDict import voluptuous as vol -from homeassistant.const import ATTR_ENTITY_ID, ATTR_NAME +from homeassistant.const import ATTR_ENTITY_ID, ATTR_NAME, Platform from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.exceptions import ServiceValidationError, TemplateError from homeassistant.helpers import template @@ -115,6 +116,8 @@ class MqttOriginInfo(TypedDict, total=False): class MqttCommandTemplateException(ServiceValidationError): """Handle MqttCommandTemplate exceptions.""" + _message: str + def __init__( self, *args: object, @@ -223,6 +226,38 @@ class MqttCommandTemplate: ) from exc +class MqttValueTemplateException(TemplateError): + """Handle MqttValueTemplate exceptions.""" + + _message: str + + def __init__( + self, + *args: object, + base_exception: Exception, + value_template: str, + default: ReceivePayloadType | PayloadSentinel, + payload: ReceivePayloadType, + entity_id: str | None = None, + ) -> None: + """Initialize exception.""" + super().__init__(base_exception, *args) + entity_id_log = "" if entity_id is None else f" for entity '{entity_id}'" + default_log = str(default) + default_payload_log = ( + "" if default is PayloadSentinel.NONE else f", default value: {default_log}" + ) + payload_log = str(payload) + self._message = ( + f"{type(base_exception).__name__}: {base_exception} rendering template{entity_id_log}" + f", template: '{value_template}'{default_payload_log} and payload: {payload_log}" + ) + + def __str__(self) -> str: + """Return exception message string.""" + return self._message + + class MqttValueTemplate: """Class for rendering MQTT value template with possible json values.""" @@ -291,14 +326,13 @@ class MqttValueTemplate: ) ) except TEMPLATE_ERRORS as exc: - _LOGGER.error( - "%s: %s rendering template for entity '%s', template: '%s'", - type(exc).__name__, - exc, - self._entity.entity_id if self._entity else "n/a", - self._value_template.template, - ) - raise + raise MqttValueTemplateException( + base_exception=exc, + value_template=self._value_template.template, + default=default, + payload=payload, + entity_id=self._entity.entity_id if self._entity else None, + ) from exc return rendered_payload _LOGGER.debug( @@ -318,17 +352,13 @@ class MqttValueTemplate: ) ) except TEMPLATE_ERRORS as exc: - _LOGGER.error( - "%s: %s rendering template for entity '%s', template: " - "'%s', default value: %s and payload: %s", - type(exc).__name__, - exc, - self._entity.entity_id if self._entity else "n/a", - self._value_template.template, - default, - payload, - ) - raise + raise MqttValueTemplateException( + base_exception=exc, + value_template=self._value_template.template, + default=default, + payload=payload, + entity_id=self._entity.entity_id if self._entity else None, + ) from exc return rendered_payload @@ -383,6 +413,7 @@ class MqttData: discovery_unsubscribe: list[CALLBACK_TYPE] = field(default_factory=list) integration_unsubscribe: dict[str, CALLBACK_TYPE] = field(default_factory=dict) last_discovery: float = 0.0 + platforms_loaded: set[Platform | str] = field(default_factory=set) reload_dispatchers: list[CALLBACK_TYPE] = field(default_factory=list) reload_handlers: dict[str, CALLBACK_TYPE] = field(default_factory=dict) reload_schema: dict[str, vol.Schema] = field(default_factory=dict) diff --git a/homeassistant/components/mqtt/number.py b/homeassistant/components/mqtt/number.py index 83eb047519f..88730d6e7a2 100644 --- a/homeassistant/components/mqtt/number.py +++ b/homeassistant/components/mqtt/number.py @@ -1,4 +1,5 @@ """Configure number in a device through MQTT topic.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/mqtt/scene.py b/homeassistant/components/mqtt/scene.py index de75f470228..a5ba2700e80 100644 --- a/homeassistant/components/mqtt/scene.py +++ b/homeassistant/components/mqtt/scene.py @@ -1,4 +1,5 @@ """Support for MQTT scenes.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/mqtt/select.py b/homeassistant/components/mqtt/select.py index 5d9bc989c25..af09f5c0202 100644 --- a/homeassistant/components/mqtt/select.py +++ b/homeassistant/components/mqtt/select.py @@ -1,4 +1,5 @@ """Configure select in a device through MQTT topic.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index 9d1ed964be3..9ba6308e07c 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -1,4 +1,5 @@ """Support for MQTT sensors.""" + from __future__ import annotations from collections.abc import Callable @@ -17,6 +18,7 @@ from homeassistant.components.sensor import ( RestoreSensor, SensorDeviceClass, SensorExtraStoredData, + SensorStateClass, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -83,11 +85,27 @@ _PLATFORM_SCHEMA_BASE = MQTT_RO_SCHEMA.extend( } ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) + +def validate_sensor_state_class_config(config: ConfigType) -> ConfigType: + """Validate the sensor state class config.""" + if ( + CONF_LAST_RESET_VALUE_TEMPLATE in config + and (state_class := config.get(CONF_STATE_CLASS)) != SensorStateClass.TOTAL + ): + raise vol.Invalid( + f"The option `{CONF_LAST_RESET_VALUE_TEMPLATE}` cannot be used " + f"together with state class `{state_class}`" + ) + + return config + + PLATFORM_SCHEMA_MODERN = vol.All( # Deprecated in HA Core 2021.11.0 https://github.com/home-assistant/core/pull/54840 # Removed in HA Core 2023.6.0 cv.removed(CONF_LAST_RESET_TOPIC), _PLATFORM_SCHEMA_BASE, + validate_sensor_state_class_config, ) DISCOVERY_SCHEMA = vol.All( @@ -95,6 +113,7 @@ DISCOVERY_SCHEMA = vol.All( # Removed in HA Core 2023.6.0 cv.removed(CONF_LAST_RESET_TOPIC), _PLATFORM_SCHEMA_BASE.extend({}, extra=vol.REMOVE_EXTRA), + validate_sensor_state_class_config, ) diff --git a/homeassistant/components/mqtt/siren.py b/homeassistant/components/mqtt/siren.py index cb2ecbafa55..e360416db7c 100644 --- a/homeassistant/components/mqtt/siren.py +++ b/homeassistant/components/mqtt/siren.py @@ -1,4 +1,5 @@ """Support for MQTT sirens.""" + from __future__ import annotations from collections.abc import Callable @@ -300,10 +301,7 @@ class MqttSiren(MqttEntity, SirenEntity): else {} ) if extra_attributes: - return ( - dict({*self._extra_attributes.items(), *extra_attributes.items()}) - or None - ) + return dict({*self._extra_attributes.items(), *extra_attributes.items()}) return self._extra_attributes or None async def _async_publish( @@ -366,9 +364,13 @@ class MqttSiren(MqttEntity, SirenEntity): def _update(self, data: SirenTurnOnServiceParameters) -> None: """Update the extra siren state attributes.""" - for attribute, support in SUPPORTED_ATTRIBUTES.items(): - if self._attr_supported_features & support and attribute in data: - data_attr = data[attribute] # type: ignore[literal-required] - if self._extra_attributes.get(attribute) == data_attr: - continue - self._extra_attributes[attribute] = data_attr + self._extra_attributes.update( + { + attribute: data_attr + for attribute, support in SUPPORTED_ATTRIBUTES.items() + if self._attr_supported_features & support + and attribute in data + and (data_attr := data[attribute]) # type: ignore[literal-required] + != self._extra_attributes.get(attribute) + } + ) diff --git a/homeassistant/components/mqtt/strings.json b/homeassistant/components/mqtt/strings.json index 4c37de8204c..87fe0bd033a 100644 --- a/homeassistant/components/mqtt/strings.json +++ b/homeassistant/components/mqtt/strings.json @@ -4,6 +4,14 @@ "title": "MQTT vacuum entities with deprecated `schema` config settings found in your configuration.yaml", "description": "The `schema` setting for MQTT vacuum entities is deprecated and should be removed. Please adjust your configuration.yaml and restart Home Assistant to fix this issue." }, + "deprecated_color_handling": { + "title": "Deprecated color handling used for MQTT light", + "description": "An MQTT light config (with `json` schema) found in `configuration.yaml` uses deprecated color handling flags.\n\nConfiguration found:\n```yaml\n{config}\n```\nDeprecated flags: **{deprecated_flags}**.\n\nUse the `supported_color_modes` option instead and [reload](/developer-tools/yaml) the manually configured MQTT items or restart Home Assistant to fix this issue." + }, + "deprecated_color_mode_flag": { + "title": "Deprecated color_mode option flag used for MQTT light", + "description": "An MQTT light config (with `json` schema) found in `configuration.yaml` uses a deprecated `color_mode` flag.\n\nConfiguration found:\n```yaml\n{config}\n```\n\nRemove the option from your config and [reload](/developer-tools/yaml) the manually configured MQTT items or restart Home Assistant to fix this issue." + }, "invalid_platform_config": { "title": "Invalid config found for mqtt {domain} item", "description": "Home Assistant detected an invalid config for a manually configured item.\n\nPlatform domain: **{domain}**\nConfiguration file: **{config_file}**\nNear line: **{line}**\nConfiguration found:\n```yaml\n{config}\n```\nError: **{error}**.\n\nMake sure the configuration is valid and [reload](/developer-tools/yaml) the manually configured MQTT items or restart Home Assistant to fix this issue." diff --git a/homeassistant/components/mqtt/subscription.py b/homeassistant/components/mqtt/subscription.py index 3f8f0f4ee3e..14f2999fa9c 100644 --- a/homeassistant/components/mqtt/subscription.py +++ b/homeassistant/components/mqtt/subscription.py @@ -1,4 +1,5 @@ """Helper to handle a set of topics to subscribe to.""" + from __future__ import annotations from collections.abc import Callable, Coroutine diff --git a/homeassistant/components/mqtt/switch.py b/homeassistant/components/mqtt/switch.py index c45e6dd77ab..8be42a9ed19 100644 --- a/homeassistant/components/mqtt/switch.py +++ b/homeassistant/components/mqtt/switch.py @@ -1,4 +1,5 @@ """Support for MQTT switches.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/mqtt/tag.py b/homeassistant/components/mqtt/tag.py index 0eda584e95a..42f6915fc91 100644 --- a/homeassistant/components/mqtt/tag.py +++ b/homeassistant/components/mqtt/tag.py @@ -1,8 +1,10 @@ """Provides tag scanning for MQTT.""" + from __future__ import annotations from collections.abc import Callable import functools +import logging import voluptuous as vol @@ -15,7 +17,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import subscription from .config import MQTT_BASE_SCHEMA -from .const import ATTR_DISCOVERY_HASH, CONF_QOS, CONF_TOPIC, TEMPLATE_ERRORS +from .const import ATTR_DISCOVERY_HASH, CONF_QOS, CONF_TOPIC from .discovery import MQTTDiscoveryPayload from .mixins import ( MQTT_ENTITY_DEVICE_INFO_SCHEMA, @@ -25,10 +27,17 @@ from .mixins import ( send_discovery_done, update_device, ) -from .models import MqttValueTemplate, ReceiveMessage, ReceivePayloadType +from .models import ( + MqttValueTemplate, + MqttValueTemplateException, + ReceiveMessage, + ReceivePayloadType, +) from .subscription import EntitySubscription from .util import get_mqtt_data, valid_subscribe_topic +_LOGGER = logging.getLogger(__name__) + LOG_NAME = "Tag" TAG = "tag" @@ -138,7 +147,8 @@ class MQTTTagScanner(MqttDiscoveryDeviceUpdate): async def tag_scanned(msg: ReceiveMessage) -> None: try: tag_id = str(self._value_template(msg.payload, "")).strip() - except TEMPLATE_ERRORS: + except MqttValueTemplateException as exc: + _LOGGER.warning(exc) return if not tag_id: # No output from template, ignore return diff --git a/homeassistant/components/mqtt/text.py b/homeassistant/components/mqtt/text.py index fb121c25a9c..e5786dbe94d 100644 --- a/homeassistant/components/mqtt/text.py +++ b/homeassistant/components/mqtt/text.py @@ -1,4 +1,5 @@ """Support for MQTT text platform.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/mqtt/trigger.py b/homeassistant/components/mqtt/trigger.py index 623c6c53373..d7086885b24 100644 --- a/homeassistant/components/mqtt/trigger.py +++ b/homeassistant/components/mqtt/trigger.py @@ -1,4 +1,5 @@ """Offer MQTT listening automation rules.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/mqtt/update.py b/homeassistant/components/mqtt/update.py index 45424995224..0171e8eee2d 100644 --- a/homeassistant/components/mqtt/update.py +++ b/homeassistant/components/mqtt/update.py @@ -1,4 +1,5 @@ """Configure update platform in a device through MQTT topic.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/mqtt/util.py b/homeassistant/components/mqtt/util.py index fb47bbfc667..a4635d1e4cc 100644 --- a/homeassistant/components/mqtt/util.py +++ b/homeassistant/components/mqtt/util.py @@ -10,10 +10,12 @@ from typing import Any import voluptuous as vol -from homeassistant.config_entries import ConfigEntryState +from homeassistant.config_entries import ConfigEntry, ConfigEntryState +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, template from homeassistant.helpers.typing import ConfigType +from homeassistant.util.async_ import create_eager_task from .const import ( ATTR_PAYLOAD, @@ -39,6 +41,47 @@ TEMP_DIR_NAME = f"home-assistant-{DOMAIN}" _VALID_QOS_SCHEMA = vol.All(vol.Coerce(int), vol.In([0, 1, 2])) +def platforms_from_config(config: list[ConfigType]) -> set[Platform | str]: + """Return the platforms to be set up.""" + return {key for platform in config for key in platform} + + +async def async_forward_entry_setup_and_setup_discovery( + hass: HomeAssistant, config_entry: ConfigEntry, platforms: set[Platform | str] +) -> None: + """Forward the config entry setup to the platforms and set up discovery.""" + mqtt_data = get_mqtt_data(hass) + platforms_loaded = mqtt_data.platforms_loaded + new_platforms: set[Platform | str] = platforms - platforms_loaded + tasks: list[asyncio.Task] = [] + if "device_automation" in new_platforms: + # Local import to avoid circular dependencies + # pylint: disable-next=import-outside-toplevel + from . import device_automation + + tasks.append( + create_eager_task(device_automation.async_setup_entry(hass, config_entry)) + ) + if "tag" in new_platforms: + # Local import to avoid circular dependencies + # pylint: disable-next=import-outside-toplevel + from . import tag + + tasks.append(create_eager_task(tag.async_setup_entry(hass, config_entry))) + if new_entity_platforms := (new_platforms - {"tag", "device_automation"}): + tasks.append( + create_eager_task( + hass.config_entries.async_forward_entry_setups( + config_entry, new_entity_platforms + ) + ) + ) + if not tasks: + return + await asyncio.gather(*tasks) + platforms_loaded.update(new_platforms) + + 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)): @@ -93,9 +136,9 @@ def valid_topic(topic: Any) -> str: ) if "\0" in validated_topic: raise vol.Invalid("MQTT topic name/filter must not contain null character.") - if any(char <= "\u001F" for char in validated_topic): + if any(char <= "\u001f" for char in validated_topic): raise vol.Invalid("MQTT topic name/filter must not contain control characters.") - if any("\u007f" <= char <= "\u009F" for char in validated_topic): + if any("\u007f" <= char <= "\u009f" for char in validated_topic): raise vol.Invalid("MQTT topic name/filter must not contain control characters.") if any("\ufdd0" <= char <= "\ufdef" for char in validated_topic): raise vol.Invalid("MQTT topic name/filter must not contain non-characters.") diff --git a/homeassistant/components/mqtt/valve.py b/homeassistant/components/mqtt/valve.py index 9d167f42d12..241d6748280 100644 --- a/homeassistant/components/mqtt/valve.py +++ b/homeassistant/components/mqtt/valve.py @@ -1,4 +1,5 @@ """Support for MQTT valve devices.""" + from __future__ import annotations from contextlib import suppress diff --git a/homeassistant/components/mqtt/water_heater.py b/homeassistant/components/mqtt/water_heater.py index a2cf2e511a0..09db5fc33e7 100644 --- a/homeassistant/components/mqtt/water_heater.py +++ b/homeassistant/components/mqtt/water_heater.py @@ -1,4 +1,5 @@ """Support for MQTT water heater devices.""" + from __future__ import annotations import logging @@ -225,28 +226,23 @@ class MqttWaterHeater(MqttTemperatureControlEntity, WaterHeaterEntity): if self._topic[CONF_MODE_STATE_TOPIC] is None or self._optimistic: self._attr_current_operation = STATE_OFF - value_templates: dict[str, Template | None] = {} - for key in VALUE_TEMPLATE_KEYS: - value_templates[key] = None - if CONF_VALUE_TEMPLATE in config: - value_templates = { - key: config.get(CONF_VALUE_TEMPLATE) for key in VALUE_TEMPLATE_KEYS - } - for key in VALUE_TEMPLATE_KEYS & config.keys(): - value_templates[key] = config[key] + value_templates: dict[str, Template | None] = { + key: config.get(CONF_VALUE_TEMPLATE) for key in VALUE_TEMPLATE_KEYS + } + value_templates.update( + {key: config[key] for key in VALUE_TEMPLATE_KEYS & config.keys()} + ) self._value_templates = { key: MqttValueTemplate( - template, - entity=self, + template, entity=self ).async_render_with_possible_json_value for key, template in value_templates.items() } - self._command_templates = {} - for key in COMMAND_TEMPLATE_KEYS: - self._command_templates[key] = MqttCommandTemplate( - config.get(key), entity=self - ).async_render + self._command_templates = { + key: MqttCommandTemplate(config.get(key), entity=self).async_render + for key in COMMAND_TEMPLATE_KEYS + } support = WaterHeaterEntityFeature(0) if (self._topic[CONF_TEMP_STATE_TOPIC] is not None) or ( diff --git a/homeassistant/components/mqtt_eventstream/__init__.py b/homeassistant/components/mqtt_eventstream/__init__.py index af370fe82f3..5e677d13cfe 100644 --- a/homeassistant/components/mqtt_eventstream/__init__.py +++ b/homeassistant/components/mqtt_eventstream/__init__.py @@ -1,4 +1,5 @@ """Connect two Home Assistant instances via MQTT.""" + import json import logging diff --git a/homeassistant/components/mqtt_json/device_tracker.py b/homeassistant/components/mqtt_json/device_tracker.py index 2b355eb68e6..68f42479930 100644 --- a/homeassistant/components/mqtt_json/device_tracker.py +++ b/homeassistant/components/mqtt_json/device_tracker.py @@ -1,4 +1,5 @@ """Support for GPS tracking MQTT enabled devices.""" + from __future__ import annotations import json diff --git a/homeassistant/components/mqtt_room/sensor.py b/homeassistant/components/mqtt_room/sensor.py index cb0e840604e..df0be7b4968 100644 --- a/homeassistant/components/mqtt_room/sensor.py +++ b/homeassistant/components/mqtt_room/sensor.py @@ -1,4 +1,5 @@ """Support for MQTT room presence detection.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/mqtt_statestream/__init__.py b/homeassistant/components/mqtt_statestream/__init__.py index 32836825876..a9b86c4bf8f 100644 --- a/homeassistant/components/mqtt_statestream/__init__.py +++ b/homeassistant/components/mqtt_statestream/__init__.py @@ -1,6 +1,9 @@ """Publish simple item state changes via MQTT.""" + +from collections.abc import Mapping import json import logging +from typing import Any import voluptuous as vol @@ -89,9 +92,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: @callback def _ha_started(hass: HomeAssistant) -> None: @callback - def _event_filter(evt: Event) -> bool: - entity_id: str = evt.data["entity_id"] - new_state: State | None = evt.data["new_state"] + def _event_filter(event_data: Mapping[str, Any]) -> bool: + entity_id: str = event_data["entity_id"] + new_state: State | None = event_data["new_state"] if new_state is None: return False if not publish_filter(entity_id): diff --git a/homeassistant/components/msteams/notify.py b/homeassistant/components/msteams/notify.py index 7a729897e76..d6ebdf3e711 100644 --- a/homeassistant/components/msteams/notify.py +++ b/homeassistant/components/msteams/notify.py @@ -1,4 +1,5 @@ """Microsoft Teams platform for notify component.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/mullvad/__init__.py b/homeassistant/components/mullvad/__init__.py index cd692f00537..b79b9b4aa6a 100644 --- a/homeassistant/components/mullvad/__init__.py +++ b/homeassistant/components/mullvad/__init__.py @@ -1,4 +1,5 @@ """The Mullvad VPN integration.""" + import asyncio from datetime import timedelta import logging diff --git a/homeassistant/components/mullvad/binary_sensor.py b/homeassistant/components/mullvad/binary_sensor.py index 264bbe15520..2e649d9a586 100644 --- a/homeassistant/components/mullvad/binary_sensor.py +++ b/homeassistant/components/mullvad/binary_sensor.py @@ -1,4 +1,5 @@ """Setup Mullvad VPN Binary Sensors.""" + from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, BinarySensorEntity, diff --git a/homeassistant/components/mullvad/config_flow.py b/homeassistant/components/mullvad/config_flow.py index ad045dbb54c..55957f160a3 100644 --- a/homeassistant/components/mullvad/config_flow.py +++ b/homeassistant/components/mullvad/config_flow.py @@ -1,18 +1,18 @@ """Config flow for Mullvad VPN integration.""" + from mullvad_api import MullvadAPI, MullvadAPIError -from homeassistant import config_entries -from homeassistant.data_entry_flow import FlowResult +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from .const import DOMAIN -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class MullvadConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Mullvad VPN.""" VERSION = 1 - async def async_step_user(self, user_input=None) -> FlowResult: + async def async_step_user(self, user_input=None) -> ConfigFlowResult: """Handle the initial step.""" self._async_abort_entries_match() diff --git a/homeassistant/components/mutesync/__init__.py b/homeassistant/components/mutesync/__init__.py index cbbbbaa6a11..75eefaf6784 100644 --- a/homeassistant/components/mutesync/__init__.py +++ b/homeassistant/components/mutesync/__init__.py @@ -1,4 +1,5 @@ """The mütesync integration.""" + from __future__ import annotations import asyncio @@ -40,14 +41,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return state - coordinator = hass.data.setdefault(DOMAIN, {})[ - entry.entry_id - ] = update_coordinator.DataUpdateCoordinator( - hass, - logging.getLogger(__name__), - name=DOMAIN, - update_interval=UPDATE_INTERVAL_NOT_IN_MEETING, - update_method=update_data, + coordinator = hass.data.setdefault(DOMAIN, {})[entry.entry_id] = ( + update_coordinator.DataUpdateCoordinator( + hass, + logging.getLogger(__name__), + name=DOMAIN, + update_interval=UPDATE_INTERVAL_NOT_IN_MEETING, + update_method=update_data, + ) ) await coordinator.async_config_entry_first_refresh() diff --git a/homeassistant/components/mutesync/binary_sensor.py b/homeassistant/components/mutesync/binary_sensor.py index 910f91fc4c6..87bf246f4e0 100644 --- a/homeassistant/components/mutesync/binary_sensor.py +++ b/homeassistant/components/mutesync/binary_sensor.py @@ -1,4 +1,5 @@ """mütesync binary sensor entities.""" + from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/mutesync/config_flow.py b/homeassistant/components/mutesync/config_flow.py index 21bbcfe69bb..2399cdc063e 100644 --- a/homeassistant/components/mutesync/config_flow.py +++ b/homeassistant/components/mutesync/config_flow.py @@ -1,4 +1,5 @@ """Config flow for mütesync integration.""" + from __future__ import annotations import asyncio @@ -8,9 +9,8 @@ import aiohttp import mutesync import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -38,14 +38,14 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, return token -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class MuteSyncConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for mütesync.""" VERSION = 1 async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" if user_input is None: return self.async_show_form( diff --git a/homeassistant/components/mutesync/const.py b/homeassistant/components/mutesync/const.py index 027a48f46ca..9fd04d1e0c5 100644 --- a/homeassistant/components/mutesync/const.py +++ b/homeassistant/components/mutesync/const.py @@ -1,4 +1,5 @@ """Constants for the mütesync integration.""" + from datetime import timedelta from typing import Final diff --git a/homeassistant/components/mvglive/sensor.py b/homeassistant/components/mvglive/sensor.py index 2f8467a0333..6aefa83d4bb 100644 --- a/homeassistant/components/mvglive/sensor.py +++ b/homeassistant/components/mvglive/sensor.py @@ -1,4 +1,5 @@ """Support for departure information for public transport in Munich.""" + from __future__ import annotations from copy import deepcopy @@ -70,9 +71,8 @@ def setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the MVGLive sensor.""" - sensors = [] - for nextdeparture in config[CONF_NEXT_DEPARTURE]: - sensors.append( + add_entities( + ( MVGLiveSensor( nextdeparture.get(CONF_STATION), nextdeparture.get(CONF_DESTINATIONS), @@ -83,8 +83,10 @@ def setup_platform( nextdeparture.get(CONF_NUMBER), nextdeparture.get(CONF_NAME), ) - ) - add_entities(sensors, True) + for nextdeparture in config[CONF_NEXT_DEPARTURE] + ), + True, + ) class MVGLiveSensor(SensorEntity): diff --git a/homeassistant/components/my/__init__.py b/homeassistant/components/my/__init__.py index d699e42e105..d18589f66ce 100644 --- a/homeassistant/components/my/__init__.py +++ b/homeassistant/components/my/__init__.py @@ -1,4 +1,5 @@ """Support for my.home-assistant.io redirect service.""" + from homeassistant.components import frontend from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv diff --git a/homeassistant/components/mycroft/__init__.py b/homeassistant/components/mycroft/__init__.py index 7c4b75aaad1..557eca972e6 100644 --- a/homeassistant/components/mycroft/__init__.py +++ b/homeassistant/components/mycroft/__init__.py @@ -1,4 +1,5 @@ """Support for Mycroft AI.""" + import voluptuous as vol from homeassistant.const import CONF_HOST, Platform diff --git a/homeassistant/components/mycroft/notify.py b/homeassistant/components/mycroft/notify.py index a9dd82caef1..67203ae0564 100644 --- a/homeassistant/components/mycroft/notify.py +++ b/homeassistant/components/mycroft/notify.py @@ -1,4 +1,5 @@ """Mycroft AI notification platform.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/myq/__init__.py b/homeassistant/components/myq/__init__.py index 86a158c09fa..41b36a34c20 100644 --- a/homeassistant/components/myq/__init__.py +++ b/homeassistant/components/myq/__init__.py @@ -1,4 +1,5 @@ """The MyQ integration.""" + from __future__ import annotations from homeassistant.config_entries import ConfigEntry, ConfigEntryState diff --git a/homeassistant/components/myq/config_flow.py b/homeassistant/components/myq/config_flow.py index 27bb1c4b9e5..46b3d7afcc5 100644 --- a/homeassistant/components/myq/config_flow.py +++ b/homeassistant/components/myq/config_flow.py @@ -1,11 +1,11 @@ """Config flow for MyQ integration.""" -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow from . import DOMAIN -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class MyQConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for MyQ.""" VERSION = 1 diff --git a/homeassistant/components/mysensors/__init__.py b/homeassistant/components/mysensors/__init__.py index a3f52cd28ab..699190a087c 100644 --- a/homeassistant/components/mysensors/__init__.py +++ b/homeassistant/components/mysensors/__init__.py @@ -1,4 +1,5 @@ """Connect to a MySensors gateway via pymysensors API.""" + from __future__ import annotations from collections.abc import Callable, Mapping diff --git a/homeassistant/components/mysensors/binary_sensor.py b/homeassistant/components/mysensors/binary_sensor.py index b70a7fc8d55..a0a1c92c682 100644 --- a/homeassistant/components/mysensors/binary_sensor.py +++ b/homeassistant/components/mysensors/binary_sensor.py @@ -1,4 +1,5 @@ """Support for MySensors binary sensors.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/mysensors/climate.py b/homeassistant/components/mysensors/climate.py index 0058fca021e..0008297f299 100644 --- a/homeassistant/components/mysensors/climate.py +++ b/homeassistant/components/mysensors/climate.py @@ -1,4 +1,5 @@ """MySensors platform that offers a Climate (MySensors-HVAC) component.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/mysensors/config_flow.py b/homeassistant/components/mysensors/config_flow.py index fdf056c6c06..b4347a39e12 100644 --- a/homeassistant/components/mysensors/config_flow.py +++ b/homeassistant/components/mysensors/config_flow.py @@ -1,4 +1,5 @@ """Config flow for MySensors.""" + from __future__ import annotations import os @@ -11,16 +12,14 @@ from awesomeversion import ( ) import voluptuous as vol -from homeassistant import config_entries from homeassistant.components.mqtt import ( DOMAIN as MQTT_DOMAIN, valid_publish_topic, valid_subscribe_topic, ) -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_DEVICE from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import selector import homeassistant.helpers.config_validation as cv @@ -120,7 +119,7 @@ def _is_same_device( return True -class MySensorsConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class MySensorsConfigFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a config flow.""" def __init__(self) -> None: @@ -129,13 +128,13 @@ class MySensorsConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, str] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Create a config entry from frontend user input.""" return await self.async_step_select_gateway_type() async def async_step_select_gateway_type( self, user_input: dict[str, str] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Show the select gateway type menu.""" return self.async_show_menu( step_id="select_gateway_type", @@ -144,7 +143,7 @@ class MySensorsConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_gw_serial( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Create config entry for a serial gateway.""" gw_type = self._gw_type = CONF_GATEWAY_TYPE_SERIAL errors: dict[str, str] = {} @@ -173,7 +172,7 @@ class MySensorsConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_gw_tcp( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Create a config entry for a tcp gateway.""" gw_type = self._gw_type = CONF_GATEWAY_TYPE_TCP errors: dict[str, str] = {} @@ -207,7 +206,7 @@ class MySensorsConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_gw_mqtt( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Create a config entry for a mqtt gateway.""" # Naive check that doesn't consider config entry state. if MQTT_DOMAIN not in self.hass.config.components: @@ -262,7 +261,7 @@ class MySensorsConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) @callback - def _async_create_entry(self, user_input: dict[str, Any]) -> FlowResult: + def _async_create_entry(self, user_input: dict[str, Any]) -> ConfigFlowResult: """Create the config entry.""" return self.async_create_entry( title=f"{user_input[CONF_DEVICE]}", @@ -303,9 +302,9 @@ class MySensorsConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): except vol.Invalid: errors[CONF_PERSISTENCE_FILE] = "invalid_persistence_file" else: - real_persistence_path = user_input[ - CONF_PERSISTENCE_FILE - ] = self._normalize_persistence_file(user_input[CONF_PERSISTENCE_FILE]) + real_persistence_path = user_input[CONF_PERSISTENCE_FILE] = ( + self._normalize_persistence_file(user_input[CONF_PERSISTENCE_FILE]) + ) for other_entry in self._async_current_entries(): if CONF_PERSISTENCE_FILE not in other_entry.data: continue diff --git a/homeassistant/components/mysensors/const.py b/homeassistant/components/mysensors/const.py index 0a4b4c090ef..3885a2d7a0e 100644 --- a/homeassistant/components/mysensors/const.py +++ b/homeassistant/components/mysensors/const.py @@ -1,4 +1,5 @@ """MySensors constants.""" + from __future__ import annotations from collections import defaultdict diff --git a/homeassistant/components/mysensors/cover.py b/homeassistant/components/mysensors/cover.py index 8be5f1f8620..acd5643965f 100644 --- a/homeassistant/components/mysensors/cover.py +++ b/homeassistant/components/mysensors/cover.py @@ -1,4 +1,5 @@ """Support for MySensors covers.""" + from __future__ import annotations from enum import Enum, unique diff --git a/homeassistant/components/mysensors/device.py b/homeassistant/components/mysensors/device.py index c70ef1f89ed..5caa42c282c 100644 --- a/homeassistant/components/mysensors/device.py +++ b/homeassistant/components/mysensors/device.py @@ -1,4 +1,5 @@ """Handle MySensors devices.""" + from __future__ import annotations from abc import abstractmethod diff --git a/homeassistant/components/mysensors/device_tracker.py b/homeassistant/components/mysensors/device_tracker.py index d56e9874560..968ee94b60e 100644 --- a/homeassistant/components/mysensors/device_tracker.py +++ b/homeassistant/components/mysensors/device_tracker.py @@ -1,4 +1,5 @@ """Support for tracking MySensors devices.""" + from __future__ import annotations from homeassistant.components.device_tracker import SourceType, TrackerEntity diff --git a/homeassistant/components/mysensors/gateway.py b/homeassistant/components/mysensors/gateway.py index 28cacbe7762..b932a33d0fa 100644 --- a/homeassistant/components/mysensors/gateway.py +++ b/homeassistant/components/mysensors/gateway.py @@ -1,4 +1,5 @@ """Handle MySensors gateways.""" + from __future__ import annotations import asyncio @@ -16,6 +17,8 @@ from homeassistant.components.mqtt import ( DOMAIN as MQTT_DOMAIN, ReceiveMessage as MQTTReceiveMessage, ReceivePayloadType, + async_publish, + async_subscribe, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_DEVICE, EVENT_HOMEASSISTANT_STOP @@ -170,13 +173,10 @@ async def _get_gateway( # Naive check that doesn't consider config entry state. if MQTT_DOMAIN not in hass.config.components: return None - mqtt = hass.components.mqtt def pub_callback(topic: str, payload: str, qos: int, retain: bool) -> None: """Call MQTT publish function.""" - hass.async_create_task( - mqtt.async_publish(hass, topic, payload, qos, retain) - ) + hass.async_create_task(async_publish(hass, topic, payload, qos, retain)) def sub_callback( topic: str, sub_cb: Callable[[str, ReceivePayloadType, int], None], qos: int @@ -188,7 +188,7 @@ async def _get_gateway( """Call callback.""" sub_cb(msg.topic, msg.payload, msg.qos) - hass.async_create_task(mqtt.async_subscribe(topic, internal_callback, qos)) + hass.async_create_task(async_subscribe(hass, topic, internal_callback, qos)) gateway = mysensors.AsyncMQTTGateway( pub_callback, @@ -279,10 +279,8 @@ async def _gw_start( gateway.on_conn_made = gateway_connected # Don't use hass.async_create_task to avoid holding up setup indefinitely. - hass.data[DOMAIN][ - MYSENSORS_GATEWAY_START_TASK.format(entry.entry_id) - ] = asyncio.create_task( - gateway.start() + hass.data[DOMAIN][MYSENSORS_GATEWAY_START_TASK.format(entry.entry_id)] = ( + asyncio.create_task(gateway.start()) ) # store the connect task so it can be cancelled in gw_stop async def stop_this_gw(_: Event) -> None: diff --git a/homeassistant/components/mysensors/handler.py b/homeassistant/components/mysensors/handler.py index aa8a235c7cb..20e0ddd0e5a 100644 --- a/homeassistant/components/mysensors/handler.py +++ b/homeassistant/components/mysensors/handler.py @@ -1,4 +1,5 @@ """Handle MySensors messages.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/mysensors/helpers.py b/homeassistant/components/mysensors/helpers.py index 9985929eecd..cb075b8f485 100644 --- a/homeassistant/components/mysensors/helpers.py +++ b/homeassistant/components/mysensors/helpers.py @@ -1,4 +1,5 @@ """Helper functions for mysensors package.""" + from __future__ import annotations from collections import defaultdict diff --git a/homeassistant/components/mysensors/light.py b/homeassistant/components/mysensors/light.py index 7aea1e906a6..c3691a40140 100644 --- a/homeassistant/components/mysensors/light.py +++ b/homeassistant/components/mysensors/light.py @@ -1,4 +1,5 @@ """Support for MySensors lights.""" + from __future__ import annotations from typing import Any, cast diff --git a/homeassistant/components/mysensors/remote.py b/homeassistant/components/mysensors/remote.py index 8521e407ae1..e9404bb3197 100644 --- a/homeassistant/components/mysensors/remote.py +++ b/homeassistant/components/mysensors/remote.py @@ -1,4 +1,5 @@ """Support MySensors IR transceivers.""" + from __future__ import annotations from collections.abc import Iterable diff --git a/homeassistant/components/mysensors/sensor.py b/homeassistant/components/mysensors/sensor.py index 84ae1ed031f..537bf575af0 100644 --- a/homeassistant/components/mysensors/sensor.py +++ b/homeassistant/components/mysensors/sensor.py @@ -1,4 +1,5 @@ """Support for MySensors sensors.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/mysensors/switch.py b/homeassistant/components/mysensors/switch.py index b1ec1a420d2..400ef2c5896 100644 --- a/homeassistant/components/mysensors/switch.py +++ b/homeassistant/components/mysensors/switch.py @@ -1,4 +1,5 @@ """Support for MySensors switches.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/mysensors/text.py b/homeassistant/components/mysensors/text.py index 68fa2a434d5..021324d7a67 100644 --- a/homeassistant/components/mysensors/text.py +++ b/homeassistant/components/mysensors/text.py @@ -1,4 +1,5 @@ """Provide a text platform for MySensors.""" + from __future__ import annotations from homeassistant.components.text import TextEntity diff --git a/homeassistant/components/mystrom/__init__.py b/homeassistant/components/mystrom/__init__.py index 3b033e3338c..09cd7b42da0 100644 --- a/homeassistant/components/mystrom/__init__.py +++ b/homeassistant/components/mystrom/__init__.py @@ -1,4 +1,5 @@ """The myStrom integration.""" + from __future__ import annotations import logging @@ -29,7 +30,7 @@ async def _async_get_device_state( await device.get_state() except MyStromConnectionError as err: _LOGGER.error("No route to myStrom plug: %s", ip_address) - raise ConfigEntryNotReady() from err + raise ConfigEntryNotReady from err def _get_mystrom_bulb(host: str, mac: str) -> MyStromBulb: @@ -47,7 +48,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: info = await pymystrom.get_device_info(host) except MyStromConnectionError as err: _LOGGER.error("No route to myStrom plug: %s", host) - raise ConfigEntryNotReady() from err + raise ConfigEntryNotReady from err info.setdefault("type", 101) diff --git a/homeassistant/components/mystrom/binary_sensor.py b/homeassistant/components/mystrom/binary_sensor.py index 91a7814df2d..2201eb778d6 100644 --- a/homeassistant/components/mystrom/binary_sensor.py +++ b/homeassistant/components/mystrom/binary_sensor.py @@ -1,11 +1,12 @@ """Support for the myStrom buttons.""" + from __future__ import annotations from http import HTTPStatus import logging from homeassistant.components.binary_sensor import DOMAIN, BinarySensorEntity -from homeassistant.components.http import HomeAssistantView +from homeassistant.components.http import KEY_HASS, HomeAssistantView from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -37,7 +38,7 @@ class MyStromView(HomeAssistantView): async def get(self, request): """Handle the GET request received from a myStrom button.""" - res = await self._handle(request.app["hass"], request.query) + res = await self._handle(request.app[KEY_HASS], request.query) return res async def _handle(self, hass, data): diff --git a/homeassistant/components/mystrom/config_flow.py b/homeassistant/components/mystrom/config_flow.py index 6b2fe85bfe8..38b292e9f97 100644 --- a/homeassistant/components/mystrom/config_flow.py +++ b/homeassistant/components/mystrom/config_flow.py @@ -1,4 +1,5 @@ """Config flow for myStrom integration.""" + from __future__ import annotations import logging @@ -8,9 +9,8 @@ import pymystrom from pymystrom.exceptions import MyStromConnectionError import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_NAME -from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN @@ -26,14 +26,14 @@ STEP_USER_DATA_SCHEMA = vol.Schema( ) -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class MyStromConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for myStrom.""" VERSION = 1 async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors: dict[str, str] = {} diff --git a/homeassistant/components/mystrom/const.py b/homeassistant/components/mystrom/const.py index 5641463abf1..0e576d47438 100644 --- a/homeassistant/components/mystrom/const.py +++ b/homeassistant/components/mystrom/const.py @@ -1,4 +1,5 @@ """Constants for the myStrom integration.""" + DOMAIN = "mystrom" DEFAULT_NAME = "myStrom" MANUFACTURER = "myStrom" diff --git a/homeassistant/components/mystrom/light.py b/homeassistant/components/mystrom/light.py index ce9357d23f7..5dabb609437 100644 --- a/homeassistant/components/mystrom/light.py +++ b/homeassistant/components/mystrom/light.py @@ -1,4 +1,5 @@ """Support for myStrom Wifi bulbs.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/mystrom/models.py b/homeassistant/components/mystrom/models.py index 96cc40996ef..694a2f43df6 100644 --- a/homeassistant/components/mystrom/models.py +++ b/homeassistant/components/mystrom/models.py @@ -1,4 +1,5 @@ """Models for the mystrom integration.""" + from dataclasses import dataclass from typing import Any diff --git a/homeassistant/components/mystrom/sensor.py b/homeassistant/components/mystrom/sensor.py index 4551c9ebbec..2c35d35dad6 100644 --- a/homeassistant/components/mystrom/sensor.py +++ b/homeassistant/components/mystrom/sensor.py @@ -1,4 +1,5 @@ """Support for myStrom sensors of switches/plugs.""" + from __future__ import annotations from collections.abc import Callable @@ -29,6 +30,13 @@ class MyStromSwitchSensorEntityDescription(SensorEntityDescription): SENSOR_TYPES: tuple[MyStromSwitchSensorEntityDescription, ...] = ( + MyStromSwitchSensorEntityDescription( + key="avg_consumption", + translation_key="avg_consumption", + device_class=SensorDeviceClass.POWER, + native_unit_of_measurement=UnitOfPower.WATT, + value_fn=lambda device: device.consumedWs, + ), MyStromSwitchSensorEntityDescription( key="consumption", device_class=SensorDeviceClass.POWER, @@ -51,13 +59,12 @@ async def async_setup_entry( ) -> None: """Set up the myStrom entities.""" device: MyStromSwitch = hass.data[DOMAIN][entry.entry_id].device - sensors = [] - for description in SENSOR_TYPES: - if description.value_fn(device) is not None: - sensors.append(MyStromSwitchSensor(device, entry.title, description)) - - async_add_entities(sensors) + async_add_entities( + MyStromSwitchSensor(device, entry.title, description) + for description in SENSOR_TYPES + if description.value_fn(device) is not None + ) class MyStromSwitchSensor(SensorEntity): diff --git a/homeassistant/components/mystrom/strings.json b/homeassistant/components/mystrom/strings.json index 9ebd1c36df0..80d0866f6f4 100644 --- a/homeassistant/components/mystrom/strings.json +++ b/homeassistant/components/mystrom/strings.json @@ -17,5 +17,12 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } + }, + "entity": { + "sensor": { + "avg_consumption": { + "name": "Average consumption" + } + } } } diff --git a/homeassistant/components/mystrom/switch.py b/homeassistant/components/mystrom/switch.py index 8f459e6801e..9958fcf7f01 100644 --- a/homeassistant/components/mystrom/switch.py +++ b/homeassistant/components/mystrom/switch.py @@ -1,4 +1,5 @@ """Support for myStrom switches/plugs.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/mythicbeastsdns/__init__.py b/homeassistant/components/mythicbeastsdns/__init__.py index 2319bc8f812..f4de18aa0ef 100644 --- a/homeassistant/components/mythicbeastsdns/__init__.py +++ b/homeassistant/components/mythicbeastsdns/__init__.py @@ -1,4 +1,5 @@ """Support for Mythic Beasts Dynamic DNS service.""" + from datetime import timedelta import mbddns diff --git a/homeassistant/components/myuplink/__init__.py b/homeassistant/components/myuplink/__init__.py index fcfffc54b31..5dee46b24cf 100644 --- a/homeassistant/components/myuplink/__init__.py +++ b/homeassistant/components/myuplink/__init__.py @@ -1,10 +1,11 @@ """The myUplink integration.""" + from __future__ import annotations from http import HTTPStatus from aiohttp import ClientError, ClientResponseError -from myuplink import MyUplinkAPI +from myuplink import MyUplinkAPI, get_manufacturer, get_system_name from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform @@ -82,12 +83,16 @@ def create_devices( """Update all devices.""" device_registry = dr.async_get(hass) - for device_id, device in coordinator.data.devices.items(): - device_registry.async_get_or_create( - config_entry_id=config_entry.entry_id, - identifiers={(DOMAIN, device_id)}, - name=device.productName, - manufacturer=device.productName.split(" ")[0], - model=device.productName, - sw_version=device.firmwareCurrent, - ) + for system in coordinator.data.systems: + devices_in_system = [x.id for x in system.devices] + for device_id, device in coordinator.data.devices.items(): + if device_id in devices_in_system: + device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + identifiers={(DOMAIN, device_id)}, + name=get_system_name(system), + manufacturer=get_manufacturer(device), + model=device.productName, + sw_version=device.firmwareCurrent, + serial_number=device.product_serial_number, + ) diff --git a/homeassistant/components/myuplink/api.py b/homeassistant/components/myuplink/api.py index 1b74d41bc97..89a5d0c19b0 100644 --- a/homeassistant/components/myuplink/api.py +++ b/homeassistant/components/myuplink/api.py @@ -1,4 +1,5 @@ """API for myUplink bound to Home Assistant OAuth.""" + from __future__ import annotations from typing import cast diff --git a/homeassistant/components/myuplink/binary_sensor.py b/homeassistant/components/myuplink/binary_sensor.py index b5ade88a002..38b8c9c5fd3 100644 --- a/homeassistant/components/myuplink/binary_sensor.py +++ b/homeassistant/components/myuplink/binary_sensor.py @@ -20,7 +20,7 @@ CATEGORY_BASED_DESCRIPTIONS: dict[str, dict[str, BinarySensorEntityDescription]] "NIBEF": { "43161": BinarySensorEntityDescription( key="elect_add", - icon="mdi:electric-switch", + translation_key="elect_add", ), }, } diff --git a/homeassistant/components/myuplink/config_flow.py b/homeassistant/components/myuplink/config_flow.py index c108aa00ebe..fe31dcc6183 100644 --- a/homeassistant/components/myuplink/config_flow.py +++ b/homeassistant/components/myuplink/config_flow.py @@ -1,10 +1,10 @@ """Config flow for myUplink.""" + from collections.abc import Mapping import logging from typing import Any -from homeassistant.config_entries import ConfigEntry -from homeassistant.data_entry_flow import FlowResult +from homeassistant.config_entries import ConfigEntry, ConfigFlowResult from homeassistant.helpers import config_entry_oauth2_flow from .const import DOMAIN, OAUTH2_SCOPES @@ -29,7 +29,9 @@ class OAuth2FlowHandler( """Extra data that needs to be appended to the authorize url.""" return {"scope": " ".join(OAUTH2_SCOPES)} - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Perform reauth upon an API authentication error.""" self.config_entry_reauth = self.hass.config_entries.async_get_entry( self.context["entry_id"] @@ -38,7 +40,7 @@ class OAuth2FlowHandler( async def async_step_reauth_confirm( self, user_input: Mapping[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Dialog that informs the user that reauth is required.""" if user_input is None: return self.async_show_form( @@ -47,7 +49,7 @@ class OAuth2FlowHandler( return await self.async_step_user() - async def async_oauth_create_entry(self, data: dict) -> FlowResult: + async def async_oauth_create_entry(self, data: dict) -> ConfigFlowResult: """Create or update the config entry.""" if self.config_entry_reauth: return self.async_update_reload_and_abort( diff --git a/homeassistant/components/myuplink/coordinator.py b/homeassistant/components/myuplink/coordinator.py index 03a902fc4bb..211fd894ac5 100644 --- a/homeassistant/components/myuplink/coordinator.py +++ b/homeassistant/components/myuplink/coordinator.py @@ -1,4 +1,5 @@ """Coordinator for myUplink.""" + import asyncio.timeouts from dataclasses import dataclass from datetime import datetime, timedelta diff --git a/homeassistant/components/myuplink/diagnostics.py b/homeassistant/components/myuplink/diagnostics.py index 55cbb07c0d0..d108db595a1 100644 --- a/homeassistant/components/myuplink/diagnostics.py +++ b/homeassistant/components/myuplink/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for myUplink.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/myuplink/entity.py b/homeassistant/components/myuplink/entity.py index e3d6184c368..351ba6bfc92 100644 --- a/homeassistant/components/myuplink/entity.py +++ b/homeassistant/components/myuplink/entity.py @@ -1,4 +1,5 @@ """Provide a common entity class for myUplink entities.""" + from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/myuplink/helpers.py b/homeassistant/components/myuplink/helpers.py index 8b16dacfd34..abe039605d3 100644 --- a/homeassistant/components/myuplink/helpers.py +++ b/homeassistant/components/myuplink/helpers.py @@ -31,3 +31,19 @@ def find_matching_platform( return Platform.SENSOR return Platform.SENSOR + + +def skip_entity(model: str, device_point: DevicePoint) -> bool: + """Check if entity should be skipped for this device model.""" + if model == "SMO 20": + if len(device_point.smart_home_categories) > 0 or device_point.parameter_id in ( + "40940", + "47011", + "47015", + "47028", + "47032", + "50004", + ): + return False + return True + return False diff --git a/homeassistant/components/myuplink/icons.json b/homeassistant/components/myuplink/icons.json new file mode 100644 index 00000000000..580b83b1b15 --- /dev/null +++ b/homeassistant/components/myuplink/icons.json @@ -0,0 +1,45 @@ +{ + "entity": { + "binary_sensor": { + "elect_add": { + "default": "mdi:electric-switch", + "state": { + "on": "mdi:electric-switch-closed" + } + } + }, + "number": { + "degree_minutes": { + "default": "mdi:thermometer-lines" + } + }, + "sensor": { + "airflow": { + "default": "mdi:weather-windy" + }, + "elect_add": { + "default": "mdi:heat-wave" + }, + "fan_mode": { + "default": "mdi:fan" + }, + "priority": { + "default": "mdi:priority-high" + }, + "status_compressor": { + "default": "mdi:heat-pump-outline" + } + }, + "switch": { + "boost_ventilation": { + "default": "mdi:fan-plus" + }, + "temporary_lux": { + "default": "mdi:water-alert-outline", + "state": { + "on": "mdi:water-alert" + } + } + } + } +} diff --git a/homeassistant/components/myuplink/number.py b/homeassistant/components/myuplink/number.py index ddfcdb109d4..89d6658d368 100644 --- a/homeassistant/components/myuplink/number.py +++ b/homeassistant/components/myuplink/number.py @@ -1,6 +1,5 @@ """Number entity for myUplink.""" - from aiohttp import ClientError from myuplink import DevicePoint @@ -14,12 +13,12 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import MyUplinkDataCoordinator from .const import DOMAIN from .entity import MyUplinkEntity -from .helpers import find_matching_platform +from .helpers import find_matching_platform, skip_entity DEVICE_POINT_UNIT_DESCRIPTIONS: dict[str, NumberEntityDescription] = { "DM": NumberEntityDescription( key="degree_minutes", - icon="mdi:thermometer-lines", + translation_key="degree_minutes", native_unit_of_measurement="DM", ), } @@ -28,7 +27,7 @@ CATEGORY_BASED_DESCRIPTIONS: dict[str, dict[str, NumberEntityDescription]] = { "NIBEF": { "40940": NumberEntityDescription( key="degree_minutes", - icon="mdi:thermometer-lines", + translation_key="degree_minutes", native_unit_of_measurement="DM", ), }, @@ -66,6 +65,8 @@ async def async_setup_entry( # Setup device point number entities for device_id, point_data in coordinator.data.points.items(): for point_id, device_point in point_data.items(): + if skip_entity(device_point.category, device_point): + continue description = get_description(device_point) if find_matching_platform(device_point, description) == Platform.NUMBER: entities.append( diff --git a/homeassistant/components/myuplink/sensor.py b/homeassistant/components/myuplink/sensor.py index 1e4bfed1a20..6cde6b6b071 100644 --- a/homeassistant/components/myuplink/sensor.py +++ b/homeassistant/components/myuplink/sensor.py @@ -27,7 +27,7 @@ from homeassistant.helpers.typing import StateType from . import MyUplinkDataCoordinator from .const import DOMAIN from .entity import MyUplinkEntity -from .helpers import find_matching_platform +from .helpers import find_matching_platform, skip_entity DEVICE_POINT_UNIT_DESCRIPTIONS: dict[str, SensorEntityDescription] = { "°C": SensorEntityDescription( @@ -81,10 +81,10 @@ DEVICE_POINT_UNIT_DESCRIPTIONS: dict[str, SensorEntityDescription] = { ), "m3/h": SensorEntityDescription( key="airflow", + translation_key="airflow", device_class=SensorDeviceClass.VOLUME_FLOW_RATE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR, - icon="mdi:weather-windy", ), "s": SensorEntityDescription( key="seconds", @@ -101,22 +101,22 @@ CATEGORY_BASED_DESCRIPTIONS: dict[str, dict[str, SensorEntityDescription]] = { "NIBEF": { "43108": SensorEntityDescription( key="fan_mode", - icon="mdi:fan", + translation_key="fan_mode", ), "43427": SensorEntityDescription( key="status_compressor", + translation_key="status_compressor", device_class=SensorDeviceClass.ENUM, - icon="mdi:heat-pump-outline", ), "49993": SensorEntityDescription( key="elect_add", + translation_key="elect_add", device_class=SensorDeviceClass.ENUM, - icon="mdi:heat-wave", ), "49994": SensorEntityDescription( key="priority", + translation_key="priority", device_class=SensorDeviceClass.ENUM, - icon="mdi:priority-high", ), }, "NIBE": {}, @@ -155,6 +155,8 @@ async def async_setup_entry( # Setup device point sensors for device_id, point_data in coordinator.data.points.items(): for point_id, device_point in point_data.items(): + if skip_entity(device_point.category, device_point): + continue if find_matching_platform(device_point) == Platform.SENSOR: description = get_description(device_point) entity_class = MyUplinkDevicePointSensor diff --git a/homeassistant/components/myuplink/switch.py b/homeassistant/components/myuplink/switch.py index 310c6417133..d26695f4cbe 100644 --- a/homeassistant/components/myuplink/switch.py +++ b/homeassistant/components/myuplink/switch.py @@ -15,13 +15,17 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import MyUplinkDataCoordinator from .const import DOMAIN from .entity import MyUplinkEntity -from .helpers import find_matching_platform +from .helpers import find_matching_platform, skip_entity CATEGORY_BASED_DESCRIPTIONS: dict[str, dict[str, SwitchEntityDescription]] = { "NIBEF": { "50004": SwitchEntityDescription( key="temporary_lux", - icon="mdi:water-alert-outline", + translation_key="temporary_lux", + ), + "50005": SwitchEntityDescription( + key="boost_ventilation", + translation_key="boost_ventilation", ), }, } @@ -54,6 +58,8 @@ async def async_setup_entry( # Setup device point switches for device_id, point_data in coordinator.data.points.items(): for point_id, device_point in point_data.items(): + if skip_entity(device_point.category, device_point): + continue if find_matching_platform(device_point) == Platform.SWITCH: description = get_description(device_point) diff --git a/homeassistant/components/myuplink/update.py b/homeassistant/components/myuplink/update.py index 2b779e83386..6a38741a562 100644 --- a/homeassistant/components/myuplink/update.py +++ b/homeassistant/components/myuplink/update.py @@ -25,21 +25,17 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up update entity.""" - entities: list[UpdateEntity] = [] coordinator: MyUplinkDataCoordinator = hass.data[DOMAIN][config_entry.entry_id] - # Setup update entities - for device_id in coordinator.data.devices: - entities.append( - MyUplinkDeviceUpdate( - coordinator=coordinator, - device_id=device_id, - entity_description=UPDATE_DESCRIPTION, - unique_id_suffix="upd", - ) + async_add_entities( + MyUplinkDeviceUpdate( + coordinator=coordinator, + device_id=device_id, + entity_description=UPDATE_DESCRIPTION, + unique_id_suffix="upd", ) - - async_add_entities(entities) + for device_id in coordinator.data.devices + ) class MyUplinkDeviceUpdate(MyUplinkEntity, UpdateEntity): diff --git a/homeassistant/components/nad/media_player.py b/homeassistant/components/nad/media_player.py index fd84668d651..fa9ce4dd08e 100644 --- a/homeassistant/components/nad/media_player.py +++ b/homeassistant/components/nad/media_player.py @@ -1,4 +1,5 @@ """Support for interfacing with NAD receivers through RS-232.""" + from __future__ import annotations from nad_receiver import NADReceiver, NADReceiverTCP, NADReceiverTelnet diff --git a/homeassistant/components/nam/__init__.py b/homeassistant/components/nam/__init__.py index 9df1b93a4d7..63fd6af9295 100644 --- a/homeassistant/components/nam/__init__.py +++ b/homeassistant/components/nam/__init__.py @@ -1,4 +1,5 @@ """The Nettigo Air Monitor component.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/nam/button.py b/homeassistant/components/nam/button.py index a280369e7c8..b414e5c5525 100644 --- a/homeassistant/components/nam/button.py +++ b/homeassistant/components/nam/button.py @@ -1,4 +1,5 @@ """Support for the Nettigo Air Monitor service.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/nam/config_flow.py b/homeassistant/components/nam/config_flow.py index 8f44c28df3a..efdc8f2514b 100644 --- a/homeassistant/components/nam/config_flow.py +++ b/homeassistant/components/nam/config_flow.py @@ -1,4 +1,5 @@ """Adds config flow for Nettigo Air Monitor.""" + from __future__ import annotations import asyncio @@ -17,11 +18,10 @@ from nettigo_air_monitor import ( ) import voluptuous as vol -from homeassistant import config_entries from homeassistant.components import zeroconf +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.device_registry import format_mac @@ -70,7 +70,7 @@ async def async_check_credentials( await nam.async_check_credentials() -class NAMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class NAMFlowHandler(ConfigFlow, domain=DOMAIN): """Config flow for Nettigo Air Monitor.""" VERSION = 1 @@ -78,12 +78,12 @@ class NAMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize flow.""" self.host: str - self.entry: config_entries.ConfigEntry + self.entry: ConfigEntry self._config: NamConfig async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" errors: dict[str, str] = {} @@ -119,7 +119,7 @@ class NAMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_credentials( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the credentials step.""" errors: dict[str, str] = {} @@ -145,7 +145,7 @@ class NAMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_zeroconf( self, discovery_info: zeroconf.ZeroconfServiceInfo - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle zeroconf discovery.""" self.host = discovery_info.host self.context["title_placeholders"] = {"host": self.host} @@ -167,7 +167,7 @@ class NAMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_confirm_discovery( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle discovery confirm.""" errors: dict[str, str] = {} @@ -188,7 +188,9 @@ class NAMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Handle configuration by re-auth.""" if entry := self.hass.config_entries.async_get_entry(self.context["entry_id"]): self.entry = entry @@ -198,7 +200,7 @@ class NAMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Dialog that informs the user that reauth is required.""" errors: dict[str, str] = {} diff --git a/homeassistant/components/nam/const.py b/homeassistant/components/nam/const.py index 5e18b94745c..66718b01c3f 100644 --- a/homeassistant/components/nam/const.py +++ b/homeassistant/components/nam/const.py @@ -1,4 +1,5 @@ """Constants for Nettigo Air Monitor integration.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/nam/diagnostics.py b/homeassistant/components/nam/diagnostics.py index f3e4a20cc76..8ce885f0297 100644 --- a/homeassistant/components/nam/diagnostics.py +++ b/homeassistant/components/nam/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for NAM.""" + from __future__ import annotations from dataclasses import asdict diff --git a/homeassistant/components/nam/sensor.py b/homeassistant/components/nam/sensor.py index cd1543affa2..a098f48e434 100644 --- a/homeassistant/components/nam/sensor.py +++ b/homeassistant/components/nam/sensor.py @@ -1,4 +1,5 @@ """Support for the Nettigo Air Monitor service.""" + from __future__ import annotations from collections.abc import Callable @@ -74,18 +75,13 @@ PARALLEL_UPDATES = 1 _LOGGER = logging.getLogger(__name__) -@dataclass(frozen=True) -class NAMSensorRequiredKeysMixin: - """Class for NAM entity required keys.""" +@dataclass(frozen=True, kw_only=True) +class NAMSensorEntityDescription(SensorEntityDescription): + """NAM sensor entity description.""" value: Callable[[NAMSensors], StateType | datetime] -@dataclass(frozen=True) -class NAMSensorEntityDescription(SensorEntityDescription, NAMSensorRequiredKeysMixin): - """NAM sensor entity description.""" - - SENSORS: tuple[NAMSensorEntityDescription, ...] = ( NAMSensorEntityDescription( key=ATTR_BME280_HUMIDITY, @@ -371,12 +367,11 @@ async def async_setup_entry( ) ent_reg.async_update_entity(entity_id, new_unique_id=new_unique_id) - sensors: list[NAMSensor] = [] - for description in SENSORS: - if getattr(coordinator.data, description.key) is not None: - sensors.append(NAMSensor(coordinator, description)) - - async_add_entities(sensors, False) + async_add_entities( + NAMSensor(coordinator, description) + for description in SENSORS + if getattr(coordinator.data, description.key) is not None + ) class NAMSensor(CoordinatorEntity[NAMDataUpdateCoordinator], SensorEntity): diff --git a/homeassistant/components/namecheapdns/__init__.py b/homeassistant/components/namecheapdns/__init__.py index 84f995c947a..43310c5e922 100644 --- a/homeassistant/components/namecheapdns/__init__.py +++ b/homeassistant/components/namecheapdns/__init__.py @@ -1,4 +1,5 @@ """Support for namecheap DNS services.""" + from datetime import timedelta import logging diff --git a/homeassistant/components/nanoleaf/__init__.py b/homeassistant/components/nanoleaf/__init__.py index 4c3b0880170..9e368353774 100644 --- a/homeassistant/components/nanoleaf/__init__.py +++ b/homeassistant/components/nanoleaf/__init__.py @@ -1,4 +1,5 @@ """The Nanoleaf integration.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/nanoleaf/config_flow.py b/homeassistant/components/nanoleaf/config_flow.py index 87239f5fd80..ff25a25caf4 100644 --- a/homeassistant/components/nanoleaf/config_flow.py +++ b/homeassistant/components/nanoleaf/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Nanoleaf integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -9,10 +10,9 @@ from typing import Any, Final, cast from aionanoleaf import InvalidToken, Nanoleaf, Unauthorized, Unavailable import voluptuous as vol -from homeassistant import config_entries from homeassistant.components import ssdp, zeroconf +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_TOKEN -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.json import save_json from homeassistant.util.json import JsonObjectType, JsonValueType, load_json_object @@ -31,10 +31,10 @@ USER_SCHEMA: Final = vol.Schema( ) -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class NanoleafConfigFlow(ConfigFlow, domain=DOMAIN): """Nanoleaf config flow.""" - reauth_entry: config_entries.ConfigEntry | None = None + reauth_entry: ConfigEntry | None = None nanoleaf: Nanoleaf @@ -46,7 +46,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle Nanoleaf flow initiated by the user.""" if user_input is None: return self.async_show_form( @@ -77,10 +77,12 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) return await self.async_step_link() - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Handle Nanoleaf reauth flow if token is invalid.""" self.reauth_entry = cast( - config_entries.ConfigEntry, + ConfigEntry, self.hass.config_entries.async_get_entry(self.context["entry_id"]), ) self.nanoleaf = Nanoleaf( @@ -91,21 +93,21 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_zeroconf( self, discovery_info: zeroconf.ZeroconfServiceInfo - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle Nanoleaf Zeroconf discovery.""" _LOGGER.debug("Zeroconf discovered: %s", discovery_info) return await self._async_homekit_zeroconf_discovery_handler(discovery_info) async def async_step_homekit( self, discovery_info: zeroconf.ZeroconfServiceInfo - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle Nanoleaf Homekit discovery.""" _LOGGER.debug("Homekit discovered: %s", discovery_info) return await self._async_homekit_zeroconf_discovery_handler(discovery_info) async def _async_homekit_zeroconf_discovery_handler( self, discovery_info: zeroconf.ZeroconfServiceInfo - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle Nanoleaf Homekit and Zeroconf discovery.""" return await self._async_discovery_handler( discovery_info.host, @@ -113,7 +115,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): discovery_info.properties[zeroconf.ATTR_PROPERTIES_ID], ) - async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: + async def async_step_ssdp( + self, discovery_info: ssdp.SsdpServiceInfo + ) -> ConfigFlowResult: """Handle Nanoleaf SSDP discovery.""" _LOGGER.debug("SSDP discovered: %s", discovery_info) return await self._async_discovery_handler( @@ -124,7 +128,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def _async_discovery_handler( self, host: str, name: str, device_id: str - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle Nanoleaf discovery.""" # The name is unique and printed on the device and cannot be changed. await self.async_set_unique_id(name) @@ -156,7 +160,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_link( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle Nanoleaf link step.""" if user_input is None: return self.async_show_form(step_id="link") @@ -188,7 +192,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_setup_finish( self, discovery_integration_import: bool = False - ) -> FlowResult: + ) -> ConfigFlowResult: """Finish Nanoleaf config flow.""" try: await self.nanoleaf.get_info() diff --git a/homeassistant/components/nanoleaf/device_trigger.py b/homeassistant/components/nanoleaf/device_trigger.py index 5de093a4a17..15b14e9719e 100644 --- a/homeassistant/components/nanoleaf/device_trigger.py +++ b/homeassistant/components/nanoleaf/device_trigger.py @@ -1,4 +1,5 @@ """Provides device triggers for Nanoleaf.""" + from __future__ import annotations import voluptuous as vol diff --git a/homeassistant/components/nanoleaf/diagnostics.py b/homeassistant/components/nanoleaf/diagnostics.py index 9783f7854f3..57f385e5039 100644 --- a/homeassistant/components/nanoleaf/diagnostics.py +++ b/homeassistant/components/nanoleaf/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for Nanoleaf.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/nanoleaf/icons.json b/homeassistant/components/nanoleaf/icons.json new file mode 100644 index 00000000000..3f4ebf9ed9f --- /dev/null +++ b/homeassistant/components/nanoleaf/icons.json @@ -0,0 +1,9 @@ +{ + "entity": { + "light": { + "light": { + "default": "mdi:triangle-outline" + } + } + } +} diff --git a/homeassistant/components/nanoleaf/light.py b/homeassistant/components/nanoleaf/light.py index dc251ac1e5d..b80048307bb 100644 --- a/homeassistant/components/nanoleaf/light.py +++ b/homeassistant/components/nanoleaf/light.py @@ -1,4 +1,5 @@ """Support for Nanoleaf Lights.""" + from __future__ import annotations import math @@ -47,7 +48,7 @@ class NanoleafLight(NanoleafEntity, LightEntity): _attr_supported_color_modes = {ColorMode.COLOR_TEMP, ColorMode.HS} _attr_supported_features = LightEntityFeature.EFFECT | LightEntityFeature.TRANSITION _attr_name = None - _attr_icon = "mdi:triangle-outline" + _attr_translation_key = "light" def __init__( self, nanoleaf: Nanoleaf, coordinator: DataUpdateCoordinator[None] diff --git a/homeassistant/components/neato/__init__.py b/homeassistant/components/neato/__init__.py index b172d84533c..e0a3f4bc37a 100644 --- a/homeassistant/components/neato/__init__.py +++ b/homeassistant/components/neato/__init__.py @@ -1,4 +1,5 @@ """Support for Neato botvac connected vacuum cleaners.""" + import logging import aiohttp diff --git a/homeassistant/components/neato/api.py b/homeassistant/components/neato/api.py index d4596f1658d..75a3d6724de 100644 --- a/homeassistant/components/neato/api.py +++ b/homeassistant/components/neato/api.py @@ -1,4 +1,5 @@ """API for Neato Botvac bound to Home Assistant OAuth.""" + from __future__ import annotations from asyncio import run_coroutine_threadsafe diff --git a/homeassistant/components/neato/button.py b/homeassistant/components/neato/button.py index 8b23bbe4681..29114ce5188 100644 --- a/homeassistant/components/neato/button.py +++ b/homeassistant/components/neato/button.py @@ -1,4 +1,5 @@ """Support for Neato buttons.""" + from __future__ import annotations from pybotvac import Robot diff --git a/homeassistant/components/neato/camera.py b/homeassistant/components/neato/camera.py index 9ce66a53622..e4d5f81f33a 100644 --- a/homeassistant/components/neato/camera.py +++ b/homeassistant/components/neato/camera.py @@ -1,4 +1,5 @@ """Support for loading picture from Neato.""" + from __future__ import annotations from datetime import timedelta @@ -28,12 +29,13 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up Neato camera with config entry.""" - dev = [] neato: NeatoHub = hass.data[NEATO_LOGIN] mapdata: dict[str, Any] | None = hass.data.get(NEATO_MAP_DATA) - for robot in hass.data[NEATO_ROBOTS]: - if "maps" in robot.traits: - dev.append(NeatoCleaningMap(neato, robot, mapdata)) + dev = [ + NeatoCleaningMap(neato, robot, mapdata) + for robot in hass.data[NEATO_ROBOTS] + if "maps" in robot.traits + ] if not dev: return diff --git a/homeassistant/components/neato/config_flow.py b/homeassistant/components/neato/config_flow.py index 6b31cf9c05d..642fea11081 100644 --- a/homeassistant/components/neato/config_flow.py +++ b/homeassistant/components/neato/config_flow.py @@ -1,12 +1,12 @@ """Config flow for Neato Botvac.""" + from __future__ import annotations from collections.abc import Mapping import logging from typing import Any -from homeassistant.config_entries import SOURCE_REAUTH -from homeassistant.data_entry_flow import FlowResult +from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlowResult from homeassistant.helpers import config_entry_oauth2_flow from .const import NEATO_DOMAIN @@ -26,7 +26,7 @@ class OAuth2FlowHandler( async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Create an entry for the flow.""" current_entries = self._async_current_entries() if self.source != SOURCE_REAUTH and current_entries: @@ -35,19 +35,21 @@ class OAuth2FlowHandler( return await super().async_step_user(user_input=user_input) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Perform reauth upon migration of old entries.""" return await self.async_step_reauth_confirm() async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm reauth upon migration of old entries.""" 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[str, Any]) -> FlowResult: + async def async_oauth_create_entry(self, data: dict[str, Any]) -> ConfigFlowResult: """Create an entry for the flow. Update an entry if one already exist.""" current_entries = self._async_current_entries() if self.source == SOURCE_REAUTH and current_entries: diff --git a/homeassistant/components/neato/entity.py b/homeassistant/components/neato/entity.py index 46ad358c638..e4486b20ec4 100644 --- a/homeassistant/components/neato/entity.py +++ b/homeassistant/components/neato/entity.py @@ -1,4 +1,5 @@ """Base entity for Neato.""" + from __future__ import annotations from pybotvac import Robot diff --git a/homeassistant/components/neato/hub.py b/homeassistant/components/neato/hub.py index 6ee00b2a8b4..fd5f045c30f 100644 --- a/homeassistant/components/neato/hub.py +++ b/homeassistant/components/neato/hub.py @@ -1,4 +1,5 @@ """Support for Neato botvac connected vacuum cleaners.""" + from datetime import timedelta import logging diff --git a/homeassistant/components/neato/icons.json b/homeassistant/components/neato/icons.json new file mode 100644 index 00000000000..ca50d5a9bc7 --- /dev/null +++ b/homeassistant/components/neato/icons.json @@ -0,0 +1,5 @@ +{ + "services": { + "custom_cleaning": "mdi:broom" + } +} diff --git a/homeassistant/components/neato/manifest.json b/homeassistant/components/neato/manifest.json index 5222ec938c8..1d5edb7ca44 100644 --- a/homeassistant/components/neato/manifest.json +++ b/homeassistant/components/neato/manifest.json @@ -1,7 +1,7 @@ { "domain": "neato", "name": "Neato Botvac", - "codeowners": ["@dshokouhi", "@Santobert"], + "codeowners": ["@Santobert"], "config_flow": true, "dependencies": ["application_credentials"], "documentation": "https://www.home-assistant.io/integrations/neato", diff --git a/homeassistant/components/neato/sensor.py b/homeassistant/components/neato/sensor.py index 3b68ddcf3df..c247cc48493 100644 --- a/homeassistant/components/neato/sensor.py +++ b/homeassistant/components/neato/sensor.py @@ -1,4 +1,5 @@ """Support for Neato sensors.""" + from __future__ import annotations from datetime import timedelta @@ -29,10 +30,8 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the Neato sensor using config entry.""" - dev = [] neato: NeatoHub = hass.data[NEATO_LOGIN] - for robot in hass.data[NEATO_ROBOTS]: - dev.append(NeatoSensor(neato, robot)) + dev = [NeatoSensor(neato, robot) for robot in hass.data[NEATO_ROBOTS]] if not dev: return diff --git a/homeassistant/components/neato/switch.py b/homeassistant/components/neato/switch.py index ae90a8230b2..25da1c41df1 100644 --- a/homeassistant/components/neato/switch.py +++ b/homeassistant/components/neato/switch.py @@ -1,4 +1,5 @@ """Support for Neato Connected Vacuums switches.""" + from __future__ import annotations from datetime import timedelta @@ -31,12 +32,12 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up Neato switch with config entry.""" - dev = [] neato: NeatoHub = hass.data[NEATO_LOGIN] - - for robot in hass.data[NEATO_ROBOTS]: - for type_name in SWITCH_TYPES: - dev.append(NeatoConnectedSwitch(neato, robot, type_name)) + dev = [ + NeatoConnectedSwitch(neato, robot, type_name) + for robot in hass.data[NEATO_ROBOTS] + for type_name in SWITCH_TYPES + ] if not dev: return diff --git a/homeassistant/components/neato/vacuum.py b/homeassistant/components/neato/vacuum.py index 891b090d5d3..b750b121f58 100644 --- a/homeassistant/components/neato/vacuum.py +++ b/homeassistant/components/neato/vacuum.py @@ -1,4 +1,5 @@ """Support for Neato Connected Vacuums.""" + from __future__ import annotations from datetime import timedelta @@ -63,12 +64,13 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up Neato vacuum with config entry.""" - dev = [] neato: NeatoHub = hass.data[NEATO_LOGIN] mapdata: dict[str, Any] | None = hass.data.get(NEATO_MAP_DATA) persistent_maps: dict[str, Any] | None = hass.data.get(NEATO_PERSISTENT_MAPS) - for robot in hass.data[NEATO_ROBOTS]: - dev.append(NeatoConnectedVacuum(neato, robot, mapdata, persistent_maps)) + dev = [ + NeatoConnectedVacuum(neato, robot, mapdata, persistent_maps) + for robot in hass.data[NEATO_ROBOTS] + ] if not dev: return @@ -94,7 +96,6 @@ async def async_setup_entry( class NeatoConnectedVacuum(NeatoEntity, StateVacuumEntity): """Representation of a Neato Connected Vacuum.""" - _attr_icon = "mdi:robot-vacuum-variant" _attr_supported_features = ( VacuumEntityFeature.BATTERY | VacuumEntityFeature.PAUSE diff --git a/homeassistant/components/nederlandse_spoorwegen/sensor.py b/homeassistant/components/nederlandse_spoorwegen/sensor.py index f0c782bc1b5..55727289181 100644 --- a/homeassistant/components/nederlandse_spoorwegen/sensor.py +++ b/homeassistant/components/nederlandse_spoorwegen/sensor.py @@ -1,4 +1,5 @@ """Support for Nederlandse Spoorwegen public transport.""" + from __future__ import annotations from datetime import datetime, timedelta @@ -63,7 +64,7 @@ def setup_platform( requests.exceptions.HTTPError, ) as error: _LOGGER.error("Could not connect to the internet: %s", error) - raise PlatformNotReady() from error + raise PlatformNotReady from error except RequestParametersError as error: _LOGGER.error("Could not fetch stations, please check configuration: %s", error) return @@ -134,8 +135,7 @@ class NSDepartureSensor(SensorEntity): if self._trips[0].trip_parts: route = [self._trips[0].departure] - for k in self._trips[0].trip_parts: - route.append(k.destination) + route.extend(k.destination for k in self._trips[0].trip_parts) # Static attributes attributes = { diff --git a/homeassistant/components/ness_alarm/__init__.py b/homeassistant/components/ness_alarm/__init__.py index b5d30219550..a8202434ce5 100644 --- a/homeassistant/components/ness_alarm/__init__.py +++ b/homeassistant/components/ness_alarm/__init__.py @@ -1,4 +1,5 @@ """Support for Ness D8X/D16X devices.""" + from collections import namedtuple import datetime import logging diff --git a/homeassistant/components/ness_alarm/alarm_control_panel.py b/homeassistant/components/ness_alarm/alarm_control_panel.py index a4f77f59e25..2835dee9056 100644 --- a/homeassistant/components/ness_alarm/alarm_control_panel.py +++ b/homeassistant/components/ness_alarm/alarm_control_panel.py @@ -1,4 +1,5 @@ """Support for Ness D8X/D16X alarm panel.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/ness_alarm/binary_sensor.py b/homeassistant/components/ness_alarm/binary_sensor.py index 117d65b0940..bb0fa38ef72 100644 --- a/homeassistant/components/ness_alarm/binary_sensor.py +++ b/homeassistant/components/ness_alarm/binary_sensor.py @@ -1,4 +1,5 @@ """Support for Ness D8X/D16X zone states - represented as binary sensors.""" + from __future__ import annotations from homeassistant.components.binary_sensor import BinarySensorEntity diff --git a/homeassistant/components/ness_alarm/icons.json b/homeassistant/components/ness_alarm/icons.json new file mode 100644 index 00000000000..ea17fd2b299 --- /dev/null +++ b/homeassistant/components/ness_alarm/icons.json @@ -0,0 +1,6 @@ +{ + "services": { + "aux": "mdi:audio-input-stereo-minijack", + "panic": "mdi:fire" + } +} diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index 8d1d58f9117..383521452d0 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -1,4 +1,5 @@ """Support for Nest devices.""" + from __future__ import annotations from abc import ABC, abstractmethod diff --git a/homeassistant/components/nest/camera.py b/homeassistant/components/nest/camera.py index c943ea922e9..e87c9ccbbe7 100644 --- a/homeassistant/components/nest/camera.py +++ b/homeassistant/components/nest/camera.py @@ -1,4 +1,5 @@ """Support for Google Nest SDM Cameras.""" + from __future__ import annotations import asyncio @@ -47,14 +48,12 @@ async def async_setup_entry( device_manager: DeviceManager = hass.data[DOMAIN][entry.entry_id][ DATA_DEVICE_MANAGER ] - entities = [] - for device in device_manager.devices.values(): - if ( - CameraImageTrait.NAME in device.traits - or CameraLiveStreamTrait.NAME in device.traits - ): - entities.append(NestCamera(device)) - async_add_entities(entities) + async_add_entities( + NestCamera(device) + for device in device_manager.devices.values() + if CameraImageTrait.NAME in device.traits + or CameraLiveStreamTrait.NAME in device.traits + ) class NestCamera(Camera): diff --git a/homeassistant/components/nest/climate.py b/homeassistant/components/nest/climate.py index 2d0186b2bfd..411389f9fb2 100644 --- a/homeassistant/components/nest/climate.py +++ b/homeassistant/components/nest/climate.py @@ -1,4 +1,5 @@ """Support for Google Nest SDM climate devices.""" + from __future__ import annotations from typing import Any, cast @@ -85,11 +86,12 @@ async def async_setup_entry( device_manager: DeviceManager = hass.data[DOMAIN][entry.entry_id][ DATA_DEVICE_MANAGER ] - entities = [] - for device in device_manager.devices.values(): - if ThermostatHvacTrait.NAME in device.traits: - entities.append(ThermostatEntity(device)) - async_add_entities(entities) + + async_add_entities( + ThermostatEntity(device) + for device in device_manager.devices.values() + if ThermostatHvacTrait.NAME in device.traits + ) class ThermostatEntity(ClimateEntity): @@ -216,13 +218,13 @@ class ThermostatEntity(ClimateEntity): @property def preset_modes(self) -> list[str]: """Return the available presets.""" - modes = [] - if ThermostatEcoTrait.NAME in self._device.traits: - trait = self._device.traits[ThermostatEcoTrait.NAME] - for mode in trait.available_modes: - if mode in PRESET_MODE_MAP: - modes.append(PRESET_MODE_MAP[mode]) - return modes + if ThermostatEcoTrait.NAME not in self._device.traits: + return [] + return [ + PRESET_MODE_MAP[mode] + for mode in self._device.traits[ThermostatEcoTrait.NAME].available_modes + if mode in PRESET_MODE_MAP + ] @property def fan_mode(self) -> str: diff --git a/homeassistant/components/nest/config_flow.py b/homeassistant/components/nest/config_flow.py index 381cc36449d..7b5f5d2c5fb 100644 --- a/homeassistant/components/nest/config_flow.py +++ b/homeassistant/components/nest/config_flow.py @@ -7,6 +7,7 @@ This configuration flow supports the following: NestFlowHandler is an implementation of AbstractOAuth2FlowHandler with some overrides to custom steps inserted in the middle of the flow. """ + from __future__ import annotations from collections.abc import Iterable, Mapping @@ -22,8 +23,7 @@ from google_nest_sdm.exceptions import ( from google_nest_sdm.structure import InfoTrait, Structure import voluptuous as vol -from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry -from homeassistant.data_entry_flow import FlowResult +from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry, ConfigFlowResult from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.util import get_random_string @@ -71,10 +71,11 @@ def _generate_subscription_id(cloud_project_id: str) -> str: def generate_config_title(structures: Iterable[Structure]) -> str | None: """Pick a user friendly config title based on the Google Home name(s).""" - names: list[str] = [] - for structure in structures: - if (trait := structure.traits.get(InfoTrait.NAME)) and trait.custom_name: - names.append(trait.custom_name) + names: list[str] = [ + trait.custom_name + for structure in structures + if (trait := structure.traits.get(InfoTrait.NAME)) and trait.custom_name + ] if not names: return None return ", ".join(names) @@ -133,7 +134,7 @@ class NestFlowHandler( authorize_url = OAUTH2_AUTHORIZE.format(project_id=project_id) return f"{authorize_url}{query}" - async def async_oauth_create_entry(self, data: dict[str, Any]) -> FlowResult: + async def async_oauth_create_entry(self, data: dict[str, Any]) -> ConfigFlowResult: """Complete OAuth setup and finish pubsub or finish.""" _LOGGER.debug("Finishing post-oauth configuration") self._data.update(data) @@ -142,7 +143,9 @@ class NestFlowHandler( return await self.async_step_finish() return await self.async_step_pubsub() - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Perform reauth upon an API authentication error.""" self._data.update(entry_data) @@ -150,7 +153,7 @@ class NestFlowHandler( async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm reauth dialog.""" if user_input is None: return self.async_show_form(step_id="reauth_confirm") @@ -158,7 +161,7 @@ class NestFlowHandler( async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" self._data[DATA_SDM] = {} if self.source == SOURCE_REAUTH: @@ -169,8 +172,8 @@ class NestFlowHandler( async def async_step_create_cloud_project( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: - """Handle initial step in app credentails flow.""" + ) -> ConfigFlowResult: + """Handle initial step in app credentials flow.""" implementations = await config_entry_oauth2_flow.async_get_implementations( self.hass, self.DOMAIN ) @@ -196,7 +199,7 @@ class NestFlowHandler( async def async_step_cloud_project( self, user_input: dict | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle cloud project in user input.""" if user_input is not None: self._data.update(user_input) @@ -216,7 +219,7 @@ class NestFlowHandler( async def async_step_device_project( self, user_input: dict | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Collect device access project from user input.""" errors = {} if user_input is not None: @@ -249,7 +252,7 @@ class NestFlowHandler( async def async_step_pubsub( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Configure and create Pub/Sub subscriber.""" data = { **self._data, @@ -313,7 +316,9 @@ class NestFlowHandler( errors=errors, ) - async def async_step_finish(self, data: dict[str, Any] | None = None) -> FlowResult: + async def async_step_finish( + self, data: dict[str, Any] | None = None + ) -> ConfigFlowResult: """Create an entry for the SDM flow.""" _LOGGER.debug("Creating/updating configuration entry") # Update existing config entry when in the reauth flow. diff --git a/homeassistant/components/nest/device_trigger.py b/homeassistant/components/nest/device_trigger.py index f7369489e22..52c756d6a18 100644 --- a/homeassistant/components/nest/device_trigger.py +++ b/homeassistant/components/nest/device_trigger.py @@ -1,4 +1,5 @@ """Provides device automations for Nest.""" + from __future__ import annotations import voluptuous as vol diff --git a/homeassistant/components/nest/media_source.py b/homeassistant/components/nest/media_source.py index ba2faaeaae5..d48006c449d 100644 --- a/homeassistant/components/nest/media_source.py +++ b/homeassistant/components/nest/media_source.py @@ -490,9 +490,10 @@ def _browse_clip_preview( event_id: MediaId, device: Device, event: ClipPreviewSession ) -> BrowseMediaSource: """Build a BrowseMediaSource for a specific clip preview event.""" - types = [] - for event_type in event.event_types: - types.append(MEDIA_SOURCE_EVENT_TITLE_MAP.get(event_type, "Event")) + types = [ + MEDIA_SOURCE_EVENT_TITLE_MAP.get(event_type, "Event") + for event_type in event.event_types + ] return BrowseMediaSource( domain=DOMAIN, identifier=event_id.identifier, diff --git a/homeassistant/components/nest/sensor.py b/homeassistant/components/nest/sensor.py index aa170710eb6..edd359619fd 100644 --- a/homeassistant/components/nest/sensor.py +++ b/homeassistant/components/nest/sensor.py @@ -1,4 +1,5 @@ """Support for Google Nest SDM sensors.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/netatmo/__init__.py b/homeassistant/components/netatmo/__init__.py index c514e7b890d..322af8cf3ac 100644 --- a/homeassistant/components/netatmo/__init__.py +++ b/homeassistant/components/netatmo/__init__.py @@ -1,4 +1,5 @@ """The Netatmo integration.""" + from __future__ import annotations from http import HTTPStatus diff --git a/homeassistant/components/netatmo/api.py b/homeassistant/components/netatmo/api.py index 7605689b3f5..f5fe591bfbf 100644 --- a/homeassistant/components/netatmo/api.py +++ b/homeassistant/components/netatmo/api.py @@ -1,4 +1,5 @@ """API for Netatmo bound to HASS OAuth.""" + from collections.abc import Iterable from typing import cast diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py index dc566afd233..bd12e757359 100644 --- a/homeassistant/components/netatmo/camera.py +++ b/homeassistant/components/netatmo/camera.py @@ -1,4 +1,5 @@ """Support for the Netatmo cameras.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index db12efb2f01..15bf3291618 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -1,4 +1,5 @@ """Support for Netatmo Smart thermostats.""" + from __future__ import annotations import logging @@ -266,9 +267,9 @@ class NetatmoThermostat(NetatmoBaseEntity, ClimateEntity): "name", None, ) - self._attr_extra_state_attributes[ - ATTR_SELECTED_SCHEDULE - ] = self._selected_schedule + self._attr_extra_state_attributes[ATTR_SELECTED_SCHEDULE] = ( + self._selected_schedule + ) self.async_write_ha_state() self.data_handler.async_force_update(self._signal_name) return @@ -429,14 +430,14 @@ class NetatmoThermostat(NetatmoBaseEntity, ClimateEntity): self._selected_schedule = getattr( self._room.home.get_selected_schedule(), "name", None ) - self._attr_extra_state_attributes[ - ATTR_SELECTED_SCHEDULE - ] = self._selected_schedule + self._attr_extra_state_attributes[ATTR_SELECTED_SCHEDULE] = ( + self._selected_schedule + ) if self._model == NA_VALVE: - self._attr_extra_state_attributes[ - ATTR_HEATING_POWER_REQUEST - ] = self._room.heating_power_request + self._attr_extra_state_attributes[ATTR_HEATING_POWER_REQUEST] = ( + self._room.heating_power_request + ) else: for module in self._room.modules.values(): if hasattr(module, "boiler_status"): diff --git a/homeassistant/components/netatmo/config_flow.py b/homeassistant/components/netatmo/config_flow.py index bae81a7762f..b5eae967d56 100644 --- a/homeassistant/components/netatmo/config_flow.py +++ b/homeassistant/components/netatmo/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Netatmo.""" + from __future__ import annotations from collections.abc import Mapping @@ -8,10 +9,14 @@ import uuid import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ( + SOURCE_REAUTH, + ConfigEntry, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.const import CONF_SHOW_ON_MAP, CONF_UUID from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_entry_oauth2_flow, config_validation as cv from .api import get_api_scopes @@ -40,8 +45,8 @@ class NetatmoFlowHandler( @staticmethod @callback def async_get_options_flow( - config_entry: config_entries.ConfigEntry, - ) -> config_entries.OptionsFlow: + config_entry: ConfigEntry, + ) -> OptionsFlow: """Get the options flow for this handler.""" return NetatmoOptionsFlowHandler(config_entry) @@ -56,32 +61,31 @@ class NetatmoFlowHandler( scopes = get_api_scopes(self.flow_impl.domain) return {"scope": " ".join(scopes)} - async def async_step_user(self, user_input: dict | None = None) -> FlowResult: + async def async_step_user(self, user_input: dict | None = None) -> ConfigFlowResult: """Handle a flow start.""" await self.async_set_unique_id(DOMAIN) - if ( - self.source != config_entries.SOURCE_REAUTH - and self._async_current_entries() - ): + if self.source != SOURCE_REAUTH and self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") return await super().async_step_user(user_input) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Perform reauth upon an API authentication error.""" return await self.async_step_reauth_confirm() async def async_step_reauth_confirm( self, user_input: dict | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """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) -> FlowResult: + async def async_oauth_create_entry(self, data: dict) -> ConfigFlowResult: """Create an oauth config entry or update existing entry for reauth.""" existing_entry = await self.async_set_unique_id(DOMAIN) if existing_entry: @@ -92,22 +96,22 @@ class NetatmoFlowHandler( return await super().async_oauth_create_entry(data) -class NetatmoOptionsFlowHandler(config_entries.OptionsFlow): +class NetatmoOptionsFlowHandler(OptionsFlow): """Handle Netatmo options.""" - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + def __init__(self, config_entry: ConfigEntry) -> None: """Initialize Netatmo options flow.""" self.config_entry = config_entry self.options = dict(config_entry.options) self.options.setdefault(CONF_WEATHER_AREAS, {}) - async def async_step_init(self, user_input: dict | None = None) -> FlowResult: + async def async_step_init(self, user_input: dict | None = None) -> ConfigFlowResult: """Manage the Netatmo options.""" return await self.async_step_public_weather_areas() async def async_step_public_weather_areas( self, user_input: dict | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Manage configuration of Netatmo public weather areas.""" errors: dict = {} @@ -142,16 +146,16 @@ class NetatmoOptionsFlowHandler(config_entries.OptionsFlow): errors=errors, ) - async def async_step_public_weather(self, user_input: dict) -> FlowResult: + async def async_step_public_weather(self, user_input: dict) -> ConfigFlowResult: """Manage configuration of Netatmo public weather sensors.""" if user_input is not None and CONF_NEW_AREA not in user_input: - self.options[CONF_WEATHER_AREAS][ - user_input[CONF_AREA_NAME] - ] = fix_coordinates(user_input) + self.options[CONF_WEATHER_AREAS][user_input[CONF_AREA_NAME]] = ( + fix_coordinates(user_input) + ) - self.options[CONF_WEATHER_AREAS][user_input[CONF_AREA_NAME]][ - CONF_UUID - ] = str(uuid.uuid4()) + self.options[CONF_WEATHER_AREAS][user_input[CONF_AREA_NAME]][CONF_UUID] = ( + str(uuid.uuid4()) + ) return await self.async_step_public_weather_areas() @@ -203,7 +207,7 @@ class NetatmoOptionsFlowHandler(config_entries.OptionsFlow): return self.async_show_form(step_id="public_weather", data_schema=data_schema) - def _create_options_entry(self) -> FlowResult: + def _create_options_entry(self) -> ConfigFlowResult: """Update config entry options.""" return self.async_create_entry( title="Netatmo Public Weather", data=self.options diff --git a/homeassistant/components/netatmo/const.py b/homeassistant/components/netatmo/const.py index 416c5668eae..34a5c42038e 100644 --- a/homeassistant/components/netatmo/const.py +++ b/homeassistant/components/netatmo/const.py @@ -1,4 +1,5 @@ """Constants used by the Netatmo component.""" + from homeassistant.const import Platform API = "api" diff --git a/homeassistant/components/netatmo/cover.py b/homeassistant/components/netatmo/cover.py index b9537fee179..f2b5c801eec 100644 --- a/homeassistant/components/netatmo/cover.py +++ b/homeassistant/components/netatmo/cover.py @@ -1,4 +1,5 @@ """Support for Netatmo/Bubendorff covers.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/netatmo/data_handler.py b/homeassistant/components/netatmo/data_handler.py index 42d4ced6792..a4c4dbfa21d 100644 --- a/homeassistant/components/netatmo/data_handler.py +++ b/homeassistant/components/netatmo/data_handler.py @@ -1,4 +1,5 @@ """The Netatmo data handler.""" + from __future__ import annotations from collections import deque diff --git a/homeassistant/components/netatmo/device_trigger.py b/homeassistant/components/netatmo/device_trigger.py index f4719badcfa..686df2ef2cb 100644 --- a/homeassistant/components/netatmo/device_trigger.py +++ b/homeassistant/components/netatmo/device_trigger.py @@ -1,4 +1,5 @@ """Provides device automations for Netatmo.""" + from __future__ import annotations import voluptuous as vol @@ -95,7 +96,7 @@ async def async_get_triggers( """List device triggers for Netatmo devices.""" registry = er.async_get(hass) device_registry = dr.async_get(hass) - triggers = [] + triggers: list[dict[str, str]] = [] for entry in er.async_entries_for_device(registry, device_id): if ( @@ -105,17 +106,17 @@ async def async_get_triggers( for trigger in DEVICES.get(device.model, []): if trigger in SUBTYPES: - for subtype in SUBTYPES[trigger]: - triggers.append( - { - CONF_PLATFORM: "device", - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.id, - CONF_TYPE: trigger, - CONF_SUBTYPE: subtype, - } - ) + triggers.extend( + { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.id, + CONF_TYPE: trigger, + CONF_SUBTYPE: subtype, + } + for subtype in SUBTYPES[trigger] + ) else: triggers.append( { diff --git a/homeassistant/components/netatmo/diagnostics.py b/homeassistant/components/netatmo/diagnostics.py index 3a30dcd8588..4901ef6bd55 100644 --- a/homeassistant/components/netatmo/diagnostics.py +++ b/homeassistant/components/netatmo/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for Netatmo.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/netatmo/entity.py b/homeassistant/components/netatmo/entity.py index e6829604d48..579d2177824 100644 --- a/homeassistant/components/netatmo/entity.py +++ b/homeassistant/components/netatmo/entity.py @@ -1,4 +1,5 @@ """Base class for Netatmo entities.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/netatmo/fan.py b/homeassistant/components/netatmo/fan.py index 8f22861a249..1b2798dd118 100644 --- a/homeassistant/components/netatmo/fan.py +++ b/homeassistant/components/netatmo/fan.py @@ -1,4 +1,5 @@ """Support for Netatmo/Bubendorff fans.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/netatmo/helper.py b/homeassistant/components/netatmo/helper.py index ea30e059d3a..026f3f916f5 100644 --- a/homeassistant/components/netatmo/helper.py +++ b/homeassistant/components/netatmo/helper.py @@ -1,4 +1,5 @@ """Helper for Netatmo integration.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/homeassistant/components/netatmo/icons.json b/homeassistant/components/netatmo/icons.json new file mode 100644 index 00000000000..c585a9c7587 --- /dev/null +++ b/homeassistant/components/netatmo/icons.json @@ -0,0 +1,14 @@ +{ + "services": { + "set_camera_light": "mdi:led-on", + "set_schedule": "mdi:calendar-clock", + "set_preset_mode_with_end_datetime": "mdi:calendar-clock", + "set_temperature_with_end_datetime": "mdi:thermometer", + "set_temperature_with_time_period": "mdi:thermometer", + "clear_temperature_setting": "mdi:thermometer", + "set_persons_home": "mdi:home", + "set_person_away": "mdi:walk", + "register_webhook": "mdi:link-variant", + "unregister_webhook": "mdi:link-variant-off" + } +} diff --git a/homeassistant/components/netatmo/light.py b/homeassistant/components/netatmo/light.py index c38aec41564..9ccab51f792 100644 --- a/homeassistant/components/netatmo/light.py +++ b/homeassistant/components/netatmo/light.py @@ -1,4 +1,5 @@ """Support for the Netatmo camera lights.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/netatmo/media_source.py b/homeassistant/components/netatmo/media_source.py index 58bf2f93c96..7ad4acf5316 100644 --- a/homeassistant/components/netatmo/media_source.py +++ b/homeassistant/components/netatmo/media_source.py @@ -1,4 +1,5 @@ """Netatmo Media Source Implementation.""" + from __future__ import annotations import datetime as dt diff --git a/homeassistant/components/netatmo/select.py b/homeassistant/components/netatmo/select.py index 2dd88782ac3..6680242f579 100644 --- a/homeassistant/components/netatmo/select.py +++ b/homeassistant/components/netatmo/select.py @@ -1,4 +1,5 @@ """Support for the Netatmo climate schedule selector.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index 430de8e318c..481b0ba86aa 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -1,4 +1,5 @@ """Support for the Netatmo sensors.""" + from __future__ import annotations from dataclasses import dataclass @@ -70,18 +71,13 @@ SUPPORTED_PUBLIC_SENSOR_TYPES: tuple[str, ...] = ( ) -@dataclass(frozen=True) -class NetatmoRequiredKeysMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class NetatmoSensorEntityDescription(SensorEntityDescription): + """Describes Netatmo sensor entity.""" netatmo_name: str -@dataclass(frozen=True) -class NetatmoSensorEntityDescription(SensorEntityDescription, NetatmoRequiredKeysMixin): - """Describes Netatmo sensor entity.""" - - SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( NetatmoSensorEntityDescription( key="temperature", diff --git a/homeassistant/components/netatmo/switch.py b/homeassistant/components/netatmo/switch.py index 730f41afeeb..6677adec4b0 100644 --- a/homeassistant/components/netatmo/switch.py +++ b/homeassistant/components/netatmo/switch.py @@ -1,4 +1,5 @@ """Support for Netatmo/BTicino/Legrande switches.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/netatmo/webhook.py b/homeassistant/components/netatmo/webhook.py index 9761c8298c7..e5e17133690 100644 --- a/homeassistant/components/netatmo/webhook.py +++ b/homeassistant/components/netatmo/webhook.py @@ -1,4 +1,5 @@ """The Netatmo integration.""" + import logging from aiohttp.web import Request diff --git a/homeassistant/components/netdata/sensor.py b/homeassistant/components/netdata/sensor.py index 0e33cd9c952..abbb3bbb6c9 100644 --- a/homeassistant/components/netdata/sensor.py +++ b/homeassistant/components/netdata/sensor.py @@ -1,4 +1,5 @@ """Support gathering system information of hosts which are running netdata.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/netgear/__init__.py b/homeassistant/components/netgear/__init__.py index b21286ff05b..445453ad2aa 100644 --- a/homeassistant/components/netgear/__init__.py +++ b/homeassistant/components/netgear/__init__.py @@ -1,4 +1,5 @@ """Support for Netgear routers.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/netgear/button.py b/homeassistant/components/netgear/button.py index 6ec988edbe1..e5b9ec209c7 100644 --- a/homeassistant/components/netgear/button.py +++ b/homeassistant/components/netgear/button.py @@ -1,4 +1,5 @@ """Support for Netgear Button.""" + from collections.abc import Callable, Coroutine from dataclasses import dataclass from typing import Any @@ -19,20 +20,13 @@ from .entity import NetgearRouterCoordinatorEntity from .router import NetgearRouter -@dataclass(frozen=True) -class NetgearButtonEntityDescriptionRequired: - """Required attributes of NetgearButtonEntityDescription.""" +@dataclass(frozen=True, kw_only=True) +class NetgearButtonEntityDescription(ButtonEntityDescription): + """Class describing Netgear button entities.""" action: Callable[[NetgearRouter], Callable[[], Coroutine[Any, Any, None]]] -@dataclass(frozen=True) -class NetgearButtonEntityDescription( - ButtonEntityDescription, NetgearButtonEntityDescriptionRequired -): - """Class describing Netgear button entities.""" - - BUTTONS = [ NetgearButtonEntityDescription( key="reboot", diff --git a/homeassistant/components/netgear/config_flow.py b/homeassistant/components/netgear/config_flow.py index 7b74880d011..a872e9fb4ac 100644 --- a/homeassistant/components/netgear/config_flow.py +++ b/homeassistant/components/netgear/config_flow.py @@ -1,4 +1,5 @@ """Config flow to configure the Netgear integration.""" + from __future__ import annotations import logging @@ -8,8 +9,13 @@ from urllib.parse import urlparse from pynetgear import DEFAULT_HOST, DEFAULT_PORT, DEFAULT_USER import voluptuous as vol -from homeassistant import config_entries from homeassistant.components import ssdp +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, @@ -18,7 +24,6 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.util.network import is_ipv4_address from .const import ( @@ -55,10 +60,10 @@ def _ordered_shared_schema(schema_input): } -class OptionsFlowHandler(config_entries.OptionsFlow): +class OptionsFlowHandler(OptionsFlow): """Options for the component.""" - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + def __init__(self, config_entry: ConfigEntry) -> None: """Init object.""" self.config_entry = config_entry @@ -81,7 +86,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow): return self.async_show_form(step_id="init", data_schema=settings_schema) -class NetgearFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class NetgearFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a config flow.""" VERSION = 1 @@ -99,7 +104,7 @@ class NetgearFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback def async_get_options_flow( - config_entry: config_entries.ConfigEntry, + config_entry: ConfigEntry, ) -> OptionsFlowHandler: """Get the options flow.""" return OptionsFlowHandler(config_entry) @@ -121,7 +126,9 @@ class NetgearFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): description_placeholders=self.placeholders, ) - async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: + async def async_step_ssdp( + self, discovery_info: ssdp.SsdpServiceInfo + ) -> ConfigFlowResult: """Initialize flow from ssdp.""" updated_data: dict[str, str | int | bool] = {} diff --git a/homeassistant/components/netgear/const.py b/homeassistant/components/netgear/const.py index cf6cc827519..40394677362 100644 --- a/homeassistant/components/netgear/const.py +++ b/homeassistant/components/netgear/const.py @@ -1,4 +1,5 @@ """Netgear component constants.""" + from datetime import timedelta from homeassistant.const import Platform diff --git a/homeassistant/components/netgear/device_tracker.py b/homeassistant/components/netgear/device_tracker.py index 38ad024a2c4..ee3d010e443 100644 --- a/homeassistant/components/netgear/device_tracker.py +++ b/homeassistant/components/netgear/device_tracker.py @@ -1,4 +1,5 @@ """Support for Netgear routers.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/netgear/entity.py b/homeassistant/components/netgear/entity.py index 45418681db0..2610b4c7132 100644 --- a/homeassistant/components/netgear/entity.py +++ b/homeassistant/components/netgear/entity.py @@ -1,4 +1,5 @@ """Represent the Netgear router and its devices.""" + from __future__ import annotations from abc import abstractmethod diff --git a/homeassistant/components/netgear/errors.py b/homeassistant/components/netgear/errors.py index 2ac1ed18224..36e5fcb1e63 100644 --- a/homeassistant/components/netgear/errors.py +++ b/homeassistant/components/netgear/errors.py @@ -1,4 +1,5 @@ """Errors for the Netgear component.""" + from homeassistant.exceptions import HomeAssistantError diff --git a/homeassistant/components/netgear/icons.json b/homeassistant/components/netgear/icons.json new file mode 100644 index 00000000000..c1688b56692 --- /dev/null +++ b/homeassistant/components/netgear/icons.json @@ -0,0 +1,113 @@ +{ + "entity": { + "sensor": { + "link_type": { + "default": "mdi:lan" + }, + "link_rate": { + "default": "mdi:speedometer" + }, + "signal_strength": { + "default": "mdi:wifi" + }, + "ssid": { + "default": "mdi:wifi-marker" + }, + "access_point_mac": { + "default": "mdi:router-network" + }, + "upload_today": { + "default": "mdi:upload" + }, + "download_today": { + "default": "mdi:download" + }, + "upload_yesterday": { + "default": "mdi:upload" + }, + "download_yesterday": { + "default": "mdi:download" + }, + "upload_week": { + "default": "mdi:upload" + }, + "upload_week_average": { + "default": "mdi:upload" + }, + "download_week": { + "default": "mdi:download" + }, + "download_week_average": { + "default": "mdi:download" + }, + "upload_month": { + "default": "mdi:upload" + }, + "upload_month_average": { + "default": "mdi:upload" + }, + "download_month": { + "default": "mdi:download" + }, + "download_month_average": { + "default": "mdi:download" + }, + "upload_last_month": { + "default": "mdi:upload" + }, + "upload_last_month_average": { + "default": "mdi:upload" + }, + "download_last_month": { + "default": "mdi:download" + }, + "download_last_month_average": { + "default": "mdi:download" + }, + "uplink_bandwidth": { + "default": "mdi:upload" + }, + "downlink_bandwidth": { + "default": "mdi:download" + }, + "average_ping": { + "default": "mdi:wan" + }, + "cpu_utilization": { + "default": "mdi:cpu-64-bit" + }, + "memory_utilization": { + "default": "mdi:memory" + }, + "ethernet_link_status": { + "default": "mdi:ethernet" + } + }, + "switch": { + "allowed_on_network": { + "default": "mdi:block-helper" + }, + "access_control": { + "default": "mdi:block-helper" + }, + "traffic_meter": { + "default": "mdi:wifi-arrow-up-down" + }, + "parental_control": { + "default": "mdi:account-child-outline" + }, + "quality_of_service": { + "default": "mdi:wifi-star" + }, + "2g_guest_wifi": { + "default": "mdi:wifi" + }, + "5g_guest_wifi": { + "default": "mdi:wifi" + }, + "smart_connect": { + "default": "mdi:wifi" + } + } + } +} diff --git a/homeassistant/components/netgear/router.py b/homeassistant/components/netgear/router.py index 3c3be7fe9fb..1e4bf2480e9 100644 --- a/homeassistant/components/netgear/router.py +++ b/homeassistant/components/netgear/router.py @@ -1,4 +1,5 @@ """Represent the Netgear router and its devices.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/netgear/sensor.py b/homeassistant/components/netgear/sensor.py index 897fe9da30c..72087dd28db 100644 --- a/homeassistant/components/netgear/sensor.py +++ b/homeassistant/components/netgear/sensor.py @@ -1,4 +1,5 @@ """Support for Netgear routers.""" + from __future__ import annotations from collections.abc import Callable @@ -46,33 +47,28 @@ SENSOR_TYPES = { key="type", translation_key="link_type", entity_category=EntityCategory.DIAGNOSTIC, - icon="mdi:lan", ), "link_rate": SensorEntityDescription( key="link_rate", translation_key="link_rate", native_unit_of_measurement="Mbps", entity_category=EntityCategory.DIAGNOSTIC, - icon="mdi:speedometer", ), "signal": SensorEntityDescription( key="signal", translation_key="signal_strength", native_unit_of_measurement=PERCENTAGE, entity_category=EntityCategory.DIAGNOSTIC, - icon="mdi:wifi", ), "ssid": SensorEntityDescription( key="ssid", translation_key="ssid", entity_category=EntityCategory.DIAGNOSTIC, - icon="mdi:wifi-marker", ), "conn_ap_mac": SensorEntityDescription( key="conn_ap_mac", translation_key="access_point_mac", entity_category=EntityCategory.DIAGNOSTIC, - icon="mdi:router-network", ), } @@ -92,7 +88,6 @@ SENSOR_TRAFFIC_TYPES = [ entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=UnitOfInformation.MEGABYTES, device_class=SensorDeviceClass.DATA_SIZE, - icon="mdi:upload", ), NetgearSensorEntityDescription( key="NewTodayDownload", @@ -100,7 +95,6 @@ SENSOR_TRAFFIC_TYPES = [ entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=UnitOfInformation.MEGABYTES, device_class=SensorDeviceClass.DATA_SIZE, - icon="mdi:download", ), NetgearSensorEntityDescription( key="NewYesterdayUpload", @@ -108,7 +102,6 @@ SENSOR_TRAFFIC_TYPES = [ entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=UnitOfInformation.MEGABYTES, device_class=SensorDeviceClass.DATA_SIZE, - icon="mdi:upload", ), NetgearSensorEntityDescription( key="NewYesterdayDownload", @@ -116,7 +109,6 @@ SENSOR_TRAFFIC_TYPES = [ entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=UnitOfInformation.MEGABYTES, device_class=SensorDeviceClass.DATA_SIZE, - icon="mdi:download", ), NetgearSensorEntityDescription( key="NewWeekUpload", @@ -124,7 +116,6 @@ SENSOR_TRAFFIC_TYPES = [ entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=UnitOfInformation.MEGABYTES, device_class=SensorDeviceClass.DATA_SIZE, - icon="mdi:upload", index=0, value=lambda data: data[0], ), @@ -134,7 +125,6 @@ SENSOR_TRAFFIC_TYPES = [ entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=UnitOfInformation.MEGABYTES, device_class=SensorDeviceClass.DATA_SIZE, - icon="mdi:upload", index=1, value=lambda data: data[1], ), @@ -144,7 +134,6 @@ SENSOR_TRAFFIC_TYPES = [ entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=UnitOfInformation.MEGABYTES, device_class=SensorDeviceClass.DATA_SIZE, - icon="mdi:download", index=0, value=lambda data: data[0], ), @@ -154,7 +143,6 @@ SENSOR_TRAFFIC_TYPES = [ entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=UnitOfInformation.MEGABYTES, device_class=SensorDeviceClass.DATA_SIZE, - icon="mdi:download", index=1, value=lambda data: data[1], ), @@ -164,7 +152,6 @@ SENSOR_TRAFFIC_TYPES = [ entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=UnitOfInformation.MEGABYTES, device_class=SensorDeviceClass.DATA_SIZE, - icon="mdi:upload", index=0, value=lambda data: data[0], ), @@ -174,7 +161,6 @@ SENSOR_TRAFFIC_TYPES = [ entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=UnitOfInformation.MEGABYTES, device_class=SensorDeviceClass.DATA_SIZE, - icon="mdi:upload", index=1, value=lambda data: data[1], ), @@ -184,7 +170,6 @@ SENSOR_TRAFFIC_TYPES = [ entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=UnitOfInformation.MEGABYTES, device_class=SensorDeviceClass.DATA_SIZE, - icon="mdi:download", index=0, value=lambda data: data[0], ), @@ -194,7 +179,6 @@ SENSOR_TRAFFIC_TYPES = [ entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=UnitOfInformation.MEGABYTES, device_class=SensorDeviceClass.DATA_SIZE, - icon="mdi:download", index=1, value=lambda data: data[1], ), @@ -204,7 +188,6 @@ SENSOR_TRAFFIC_TYPES = [ entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=UnitOfInformation.MEGABYTES, device_class=SensorDeviceClass.DATA_SIZE, - icon="mdi:upload", index=0, value=lambda data: data[0], ), @@ -214,7 +197,6 @@ SENSOR_TRAFFIC_TYPES = [ entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=UnitOfInformation.MEGABYTES, device_class=SensorDeviceClass.DATA_SIZE, - icon="mdi:upload", index=1, value=lambda data: data[1], ), @@ -224,7 +206,6 @@ SENSOR_TRAFFIC_TYPES = [ entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=UnitOfInformation.MEGABYTES, device_class=SensorDeviceClass.DATA_SIZE, - icon="mdi:download", index=0, value=lambda data: data[0], ), @@ -234,7 +215,6 @@ SENSOR_TRAFFIC_TYPES = [ entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=UnitOfInformation.MEGABYTES, device_class=SensorDeviceClass.DATA_SIZE, - icon="mdi:download", index=1, value=lambda data: data[1], ), @@ -247,7 +227,6 @@ SENSOR_SPEED_TYPES = [ entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=UnitOfDataRate.MEGABITS_PER_SECOND, device_class=SensorDeviceClass.DATA_RATE, - icon="mdi:upload", ), NetgearSensorEntityDescription( key="NewOOKLADownlinkBandwidth", @@ -255,14 +234,12 @@ SENSOR_SPEED_TYPES = [ entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=UnitOfDataRate.MEGABITS_PER_SECOND, device_class=SensorDeviceClass.DATA_RATE, - icon="mdi:download", ), NetgearSensorEntityDescription( key="AveragePing", translation_key="average_ping", entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=UnitOfTime.MILLISECONDS, - icon="mdi:wan", ), ] @@ -272,7 +249,6 @@ SENSOR_UTILIZATION = [ translation_key="cpu_utilization", entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=PERCENTAGE, - icon="mdi:cpu-64-bit", state_class=SensorStateClass.MEASUREMENT, ), NetgearSensorEntityDescription( @@ -280,7 +256,6 @@ SENSOR_UTILIZATION = [ translation_key="memory_utilization", entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=PERCENTAGE, - icon="mdi:memory", state_class=SensorStateClass.MEASUREMENT, ), ] @@ -290,7 +265,6 @@ SENSOR_LINK_TYPES = [ key="NewEthernetLinkStatus", translation_key="ethernet_link_status", entity_category=EntityCategory.DIAGNOSTIC, - icon="mdi:ethernet", ), ] @@ -306,30 +280,16 @@ async def async_setup_entry( coordinator_utilization = hass.data[DOMAIN][entry.entry_id][KEY_COORDINATOR_UTIL] coordinator_link = hass.data[DOMAIN][entry.entry_id][KEY_COORDINATOR_LINK] - # Router entities - router_entities = [] - - for description in SENSOR_TRAFFIC_TYPES: - router_entities.append( - NetgearRouterSensorEntity(coordinator_traffic, router, description) + async_add_entities( + NetgearRouterSensorEntity(coordinator, router, description) + for (coordinator, descriptions) in ( + (coordinator_traffic, SENSOR_TRAFFIC_TYPES), + (coordinator_speed, SENSOR_SPEED_TYPES), + (coordinator_utilization, SENSOR_UTILIZATION), + (coordinator_link, SENSOR_LINK_TYPES), ) - - for description in SENSOR_SPEED_TYPES: - router_entities.append( - NetgearRouterSensorEntity(coordinator_speed, router, description) - ) - - for description in SENSOR_UTILIZATION: - router_entities.append( - NetgearRouterSensorEntity(coordinator_utilization, router, description) - ) - - for description in SENSOR_LINK_TYPES: - router_entities.append( - NetgearRouterSensorEntity(coordinator_link, router, description) - ) - - async_add_entities(router_entities) + for description in descriptions + ) # Entities per network device tracked = set() @@ -343,17 +303,15 @@ async def async_setup_entry( if not coordinator.data: return - new_entities = [] + new_entities: list[NetgearSensorEntity] = [] for mac, device in router.devices.items(): if mac in tracked: continue new_entities.extend( - [ - NetgearSensorEntity(coordinator, router, device, attribute) - for attribute in sensors - ] + NetgearSensorEntity(coordinator, router, device, attribute) + for attribute in sensors ) tracked.add(mac) diff --git a/homeassistant/components/netgear/switch.py b/homeassistant/components/netgear/switch.py index 4be13a0f32c..85f214d784a 100644 --- a/homeassistant/components/netgear/switch.py +++ b/homeassistant/components/netgear/switch.py @@ -1,4 +1,5 @@ """Support for Netgear switches.""" + from collections.abc import Callable from dataclasses import dataclass from datetime import timedelta @@ -26,7 +27,6 @@ SWITCH_TYPES = [ SwitchEntityDescription( key="allow_or_block", translation_key="allowed_on_network", - icon="mdi:block-helper", entity_category=EntityCategory.CONFIG, ) ] @@ -36,22 +36,19 @@ SWITCH_TYPES = [ class NetgearSwitchEntityDescriptionRequired: """Required attributes of NetgearSwitchEntityDescription.""" + +@dataclass(frozen=True, kw_only=True) +class NetgearSwitchEntityDescription(SwitchEntityDescription): + """Class describing Netgear Switch entities.""" + update: Callable[[NetgearRouter], bool] action: Callable[[NetgearRouter], bool] -@dataclass(frozen=True) -class NetgearSwitchEntityDescription( - SwitchEntityDescription, NetgearSwitchEntityDescriptionRequired -): - """Class describing Netgear Switch entities.""" - - ROUTER_SWITCH_TYPES = [ NetgearSwitchEntityDescription( key="access_control", translation_key="access_control", - icon="mdi:block-helper", entity_category=EntityCategory.CONFIG, update=lambda router: router.api.get_block_device_enable_status, action=lambda router: router.api.set_block_device_enable, @@ -59,7 +56,6 @@ ROUTER_SWITCH_TYPES = [ NetgearSwitchEntityDescription( key="traffic_meter", translation_key="traffic_meter", - icon="mdi:wifi-arrow-up-down", entity_category=EntityCategory.CONFIG, update=lambda router: router.api.get_traffic_meter_enabled, action=lambda router: router.api.enable_traffic_meter, @@ -67,7 +63,6 @@ ROUTER_SWITCH_TYPES = [ NetgearSwitchEntityDescription( key="parental_control", translation_key="parental_control", - icon="mdi:account-child-outline", entity_category=EntityCategory.CONFIG, update=lambda router: router.api.get_parental_control_enable_status, action=lambda router: router.api.enable_parental_control, @@ -75,7 +70,6 @@ ROUTER_SWITCH_TYPES = [ NetgearSwitchEntityDescription( key="qos", translation_key="quality_of_service", - icon="mdi:wifi-star", entity_category=EntityCategory.CONFIG, update=lambda router: router.api.get_qos_enable_status, action=lambda router: router.api.set_qos_enable_status, @@ -83,7 +77,6 @@ ROUTER_SWITCH_TYPES = [ NetgearSwitchEntityDescription( key="2g_guest_wifi", translation_key="2g_guest_wifi", - icon="mdi:wifi", entity_category=EntityCategory.CONFIG, update=lambda router: router.api.get_2g_guest_access_enabled, action=lambda router: router.api.set_2g_guest_access_enabled, @@ -91,7 +84,6 @@ ROUTER_SWITCH_TYPES = [ NetgearSwitchEntityDescription( key="5g_guest_wifi", translation_key="5g_guest_wifi", - icon="mdi:wifi", entity_category=EntityCategory.CONFIG, update=lambda router: router.api.get_5g_guest_access_enabled, action=lambda router: router.api.set_5g_guest_access_enabled, @@ -99,7 +91,6 @@ ROUTER_SWITCH_TYPES = [ NetgearSwitchEntityDescription( key="smart_connect", translation_key="smart_connect", - icon="mdi:wifi", entity_category=EntityCategory.CONFIG, update=lambda router: router.api.get_smart_connect_enabled, action=lambda router: router.api.set_smart_connect_enabled, @@ -113,13 +104,10 @@ async def async_setup_entry( """Set up switches for Netgear component.""" router = hass.data[DOMAIN][entry.entry_id][KEY_ROUTER] - # Router entities - router_entities = [] - - for description in ROUTER_SWITCH_TYPES: - router_entities.append(NetgearRouterSwitchEntity(router, description)) - - async_add_entities(router_entities) + async_add_entities( + NetgearRouterSwitchEntity(router, description) + for description in ROUTER_SWITCH_TYPES + ) # Entities per network device coordinator = hass.data[DOMAIN][entry.entry_id][KEY_COORDINATOR] diff --git a/homeassistant/components/netgear/update.py b/homeassistant/components/netgear/update.py index 78e11e7c174..1fbfee3d892 100644 --- a/homeassistant/components/netgear/update.py +++ b/homeassistant/components/netgear/update.py @@ -1,4 +1,5 @@ """Update entities for Netgear devices.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/netgear_lte/__init__.py b/homeassistant/components/netgear_lte/__init__.py index 491ee0efe59..8c54cb96b3d 100644 --- a/homeassistant/components/netgear_lte/__init__.py +++ b/homeassistant/components/netgear_lte/__init__.py @@ -1,4 +1,5 @@ """Support for Netgear LTE modems.""" + from datetime import timedelta from aiohttp.cookiejar import CookieJar diff --git a/homeassistant/components/netgear_lte/binary_sensor.py b/homeassistant/components/netgear_lte/binary_sensor.py index 2830c551b80..43a9c1bd260 100644 --- a/homeassistant/components/netgear_lte/binary_sensor.py +++ b/homeassistant/components/netgear_lte/binary_sensor.py @@ -1,4 +1,5 @@ """Support for Netgear LTE binary sensors.""" + from __future__ import annotations from homeassistant.components.binary_sensor import ( diff --git a/homeassistant/components/netgear_lte/config_flow.py b/homeassistant/components/netgear_lte/config_flow.py index a3a56bab03b..fe411f79699 100644 --- a/homeassistant/components/netgear_lte/config_flow.py +++ b/homeassistant/components/netgear_lte/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Netgear LTE integration.""" + from __future__ import annotations from typing import Any @@ -8,18 +9,18 @@ from eternalegypt import Error, Modem from eternalegypt.eternalegypt import Information import voluptuous as vol -from homeassistant import config_entries, exceptions +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_PASSWORD -from homeassistant.data_entry_flow import FlowResult +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_create_clientsession from .const import DEFAULT_HOST, DOMAIN, LOGGER, MANUFACTURER -class NetgearLTEFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class NetgearLTEFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a config flow for Netgear LTE.""" - async def async_step_import(self, config: dict[str, Any]) -> FlowResult: + async def async_step_import(self, config: dict[str, Any]) -> ConfigFlowResult: """Import a configuration from config.yaml.""" host = config[CONF_HOST] password = config[CONF_PASSWORD] @@ -37,7 +38,7 @@ class NetgearLTEFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initiated by the user.""" errors = {} @@ -94,7 +95,7 @@ class NetgearLTEFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return info -class InputValidationError(exceptions.HomeAssistantError): +class InputValidationError(HomeAssistantError): """Error to indicate we cannot proceed due to invalid input.""" def __init__(self, base: str) -> None: diff --git a/homeassistant/components/netgear_lte/const.py b/homeassistant/components/netgear_lte/const.py index b47218bf4e1..69a96c289e8 100644 --- a/homeassistant/components/netgear_lte/const.py +++ b/homeassistant/components/netgear_lte/const.py @@ -1,4 +1,5 @@ """Constants for the Netgear LTE integration.""" + import logging from typing import Final diff --git a/homeassistant/components/netgear_lte/icons.json b/homeassistant/components/netgear_lte/icons.json new file mode 100644 index 00000000000..543d9bf4690 --- /dev/null +++ b/homeassistant/components/netgear_lte/icons.json @@ -0,0 +1,39 @@ +{ + "entity": { + "sensor": { + "sms": { + "default": "mdi:message-processing" + }, + "sms_total": { + "default": "mdi:message-processing" + }, + "upstream": { + "default": "mdi:ip-network" + }, + "connection_text": { + "default": "mdi:radio-tower" + }, + "connection_type": { + "default": "mdi:ip" + }, + "service_type": { + "default": "mdi:radio-tower" + }, + "register_network_display": { + "default": "mdi:web" + }, + "band": { + "default": "mdi:radio-tower" + }, + "cell_id": { + "default": "mdi:radio-tower" + } + } + }, + "services": { + "delete_sms": "mdi:delete", + "set_option": "mdi:cog", + "connect_lte": "mdi:wifi", + "disconnect_lte": "mdi:wifi-off" + } +} diff --git a/homeassistant/components/netgear_lte/notify.py b/homeassistant/components/netgear_lte/notify.py index ddc5e93677c..97ba402dc35 100644 --- a/homeassistant/components/netgear_lte/notify.py +++ b/homeassistant/components/netgear_lte/notify.py @@ -1,4 +1,5 @@ """Support for Netgear LTE notifications.""" + from __future__ import annotations import attr diff --git a/homeassistant/components/netgear_lte/sensor.py b/homeassistant/components/netgear_lte/sensor.py index 4e978a2f964..62b4796f068 100644 --- a/homeassistant/components/netgear_lte/sensor.py +++ b/homeassistant/components/netgear_lte/sensor.py @@ -1,4 +1,5 @@ """Support for Netgear LTE sensors.""" + from __future__ import annotations from collections.abc import Callable @@ -36,14 +37,12 @@ SENSORS: tuple[NetgearLTESensorEntityDescription, ...] = ( NetgearLTESensorEntityDescription( key="sms", translation_key="sms", - icon="mdi:message-processing", native_unit_of_measurement="unread", value_fn=lambda modem_data: sum(1 for x in modem_data.data.sms if x.unread), ), NetgearLTESensorEntityDescription( key="sms_total", translation_key="sms_total", - icon="mdi:message-processing", native_unit_of_measurement="messages", value_fn=lambda modem_data: len(modem_data.data.sms), ), @@ -84,49 +83,42 @@ SENSORS: tuple[NetgearLTESensorEntityDescription, ...] = ( key="upstream", translation_key="upstream", entity_registry_enabled_default=False, - icon="mdi:ip-network", entity_category=EntityCategory.DIAGNOSTIC, ), NetgearLTESensorEntityDescription( key="connection_text", translation_key="connection_text", entity_registry_enabled_default=False, - icon="mdi:radio-tower", entity_category=EntityCategory.DIAGNOSTIC, ), NetgearLTESensorEntityDescription( key="connection_type", translation_key="connection_type", entity_registry_enabled_default=False, - icon="mdi:ip", entity_category=EntityCategory.DIAGNOSTIC, ), NetgearLTESensorEntityDescription( key="current_ps_service_type", translation_key="service_type", entity_registry_enabled_default=False, - icon="mdi:radio-tower", entity_category=EntityCategory.DIAGNOSTIC, ), NetgearLTESensorEntityDescription( key="register_network_display", translation_key="register_network_display", entity_registry_enabled_default=False, - icon="mdi:web", entity_category=EntityCategory.DIAGNOSTIC, ), NetgearLTESensorEntityDescription( key="current_band", translation_key="band", entity_registry_enabled_default=False, - icon="mdi:radio-tower", entity_category=EntityCategory.DIAGNOSTIC, ), NetgearLTESensorEntityDescription( key="cell_id", translation_key="cell_id", entity_registry_enabled_default=False, - icon="mdi:radio-tower", entity_category=EntityCategory.DIAGNOSTIC, ), ) diff --git a/homeassistant/components/netgear_lte/services.py b/homeassistant/components/netgear_lte/services.py index 2ea98896791..02000820119 100644 --- a/homeassistant/components/netgear_lte/services.py +++ b/homeassistant/components/netgear_lte/services.py @@ -1,4 +1,5 @@ """Services for the Netgear LTE integration.""" + from typing import TYPE_CHECKING import voluptuous as vol diff --git a/homeassistant/components/netio/switch.py b/homeassistant/components/netio/switch.py index 546aa07e22d..0f0c85c1720 100644 --- a/homeassistant/components/netio/switch.py +++ b/homeassistant/components/netio/switch.py @@ -1,4 +1,5 @@ """The Netio switch component.""" + from __future__ import annotations from collections import namedtuple diff --git a/homeassistant/components/network/__init__.py b/homeassistant/components/network/__init__.py index 32bb9a574cd..10046f75127 100644 --- a/homeassistant/components/network/__init__.py +++ b/homeassistant/components/network/__init__.py @@ -1,4 +1,5 @@ """The Network Configuration integration.""" + from __future__ import annotations from ipaddress import IPv4Address, IPv6Address, ip_interface @@ -130,10 +131,8 @@ async def async_get_announce_addresses(hass: HomeAssistant) -> list[str]: for adapter in adapters: if not adapter["enabled"]: continue - for ips in adapter["ipv4"]: - addresses.append(str(IPv4Address(ips["address"]))) - for ips in adapter["ipv6"]: - addresses.append(str(IPv6Address(ips["address"]))) + addresses.extend(str(IPv4Address(ips["address"])) for ips in adapter["ipv4"]) + addresses.extend(str(IPv6Address(ips["address"])) for ips in adapter["ipv6"]) # Puts the default IPv4 address first in the list to preserve compatibility, # because some mDNS implementations ignores anything but the first announced @@ -141,7 +140,7 @@ async def async_get_announce_addresses(hass: HomeAssistant) -> list[str]: if default_ip := await async_get_source_ip(hass, target_ip=MDNS_TARGET_IP): if default_ip in addresses: addresses.remove(default_ip) - return [default_ip] + list(addresses) + return [default_ip, *addresses] return list(addresses) diff --git a/homeassistant/components/network/const.py b/homeassistant/components/network/const.py index 3a166189b85..6c5b6f80eda 100644 --- a/homeassistant/components/network/const.py +++ b/homeassistant/components/network/const.py @@ -1,4 +1,5 @@ """Constants for the network integration.""" + from __future__ import annotations from typing import Final diff --git a/homeassistant/components/network/models.py b/homeassistant/components/network/models.py index 4428578f8f9..93d34e92302 100644 --- a/homeassistant/components/network/models.py +++ b/homeassistant/components/network/models.py @@ -1,4 +1,5 @@ """Models helper class for the network integration.""" + from __future__ import annotations from typing import TypedDict diff --git a/homeassistant/components/network/network.py b/homeassistant/components/network/network.py index 0b90023bfd4..4158307bb1a 100644 --- a/homeassistant/components/network/network.py +++ b/homeassistant/components/network/network.py @@ -1,4 +1,5 @@ """Network helper class for the network integration.""" + from __future__ import annotations import logging @@ -7,6 +8,7 @@ from typing import Any from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.singleton import singleton from homeassistant.helpers.storage import Store +from homeassistant.util.async_ import create_eager_task from .const import ( ATTR_CONFIGURED_ADAPTERS, @@ -50,8 +52,9 @@ class Network: async def async_setup(self) -> None: """Set up the network config.""" - await self.async_load() + storage_load_task = create_eager_task(self.async_load()) self.adapters = await async_load_adapters() + await storage_load_task @callback def async_configure(self) -> None: diff --git a/homeassistant/components/network/util.py b/homeassistant/components/network/util.py index 2fb0690684c..55c3c2f5ead 100644 --- a/homeassistant/components/network/util.py +++ b/homeassistant/components/network/util.py @@ -1,4 +1,5 @@ """Network helper class for the network integration.""" + from __future__ import annotations from ipaddress import IPv4Address, IPv6Address, ip_address diff --git a/homeassistant/components/network/websocket.py b/homeassistant/components/network/websocket.py index 4c55585e112..78626b893e4 100644 --- a/homeassistant/components/network/websocket.py +++ b/homeassistant/components/network/websocket.py @@ -1,4 +1,5 @@ """The Network Configuration integration websocket commands.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/neurio_energy/sensor.py b/homeassistant/components/neurio_energy/sensor.py index a9023ffca2b..4a7ce43a0d7 100644 --- a/homeassistant/components/neurio_energy/sensor.py +++ b/homeassistant/components/neurio_energy/sensor.py @@ -1,4 +1,5 @@ """Support for monitoring a Neurio energy sensor.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/nexia/__init__.py b/homeassistant/components/nexia/__init__.py index f1954eb50b8..9d9299b1ce9 100644 --- a/homeassistant/components/nexia/__init__.py +++ b/homeassistant/components/nexia/__init__.py @@ -1,4 +1,5 @@ """Support for Nexia / Trane XL Thermostats.""" + import logging import aiohttp diff --git a/homeassistant/components/nexia/binary_sensor.py b/homeassistant/components/nexia/binary_sensor.py index 02c3fef1162..9b3d9cab986 100644 --- a/homeassistant/components/nexia/binary_sensor.py +++ b/homeassistant/components/nexia/binary_sensor.py @@ -1,4 +1,5 @@ """Support for Nexia / Trane XL Thermostats.""" + from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -40,7 +41,7 @@ async def async_setup_entry( class NexiaBinarySensor(NexiaThermostatEntity, BinarySensorEntity): - """Provices Nexia BinarySensor support.""" + """Provides Nexia BinarySensor support.""" def __init__(self, coordinator, thermostat, sensor_call, translation_key): """Initialize the nexia sensor.""" diff --git a/homeassistant/components/nexia/climate.py b/homeassistant/components/nexia/climate.py index 63caeb445b7..78c0bc88ef7 100644 --- a/homeassistant/components/nexia/climate.py +++ b/homeassistant/components/nexia/climate.py @@ -1,4 +1,5 @@ """Support for Nexia / Trane XL thermostats.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/nexia/config_flow.py b/homeassistant/components/nexia/config_flow.py index 46dc1454a2a..5af4ff52fbb 100644 --- a/homeassistant/components/nexia/config_flow.py +++ b/homeassistant/components/nexia/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Nexia integration.""" + import logging import aiohttp @@ -6,8 +7,10 @@ from nexia.const import BRAND_ASAIR, BRAND_NEXIA, BRAND_TRANE from nexia.home import NexiaHome import voluptuous as vol -from homeassistant import config_entries, core, exceptions +from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import ( @@ -36,7 +39,7 @@ DATA_SCHEMA = vol.Schema( ) -async def validate_input(hass: core.HomeAssistant, data): +async def validate_input(hass: HomeAssistant, data): """Validate the user input allows us to connect. Data has the keys from DATA_SCHEMA with values provided by the user. @@ -73,7 +76,7 @@ async def validate_input(hass: core.HomeAssistant, data): return info -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class NexiaConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Nexia.""" VERSION = 1 @@ -102,9 +105,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -class CannotConnect(exceptions.HomeAssistantError): +class CannotConnect(HomeAssistantError): """Error to indicate we cannot connect.""" -class InvalidAuth(exceptions.HomeAssistantError): +class InvalidAuth(HomeAssistantError): """Error to indicate there is invalid auth.""" diff --git a/homeassistant/components/nexia/const.py b/homeassistant/components/nexia/const.py index fe2d6527ea0..b46037eab7b 100644 --- a/homeassistant/components/nexia/const.py +++ b/homeassistant/components/nexia/const.py @@ -1,4 +1,5 @@ """Nexia constants.""" + from homeassistant.const import Platform PLATFORMS = [ diff --git a/homeassistant/components/nexia/coordinator.py b/homeassistant/components/nexia/coordinator.py index cd515e44b14..894c491c45b 100644 --- a/homeassistant/components/nexia/coordinator.py +++ b/homeassistant/components/nexia/coordinator.py @@ -1,4 +1,5 @@ """Component to embed nexia devices.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/nexia/diagnostics.py b/homeassistant/components/nexia/diagnostics.py index 9b3f518217b..e03cf23b83b 100644 --- a/homeassistant/components/nexia/diagnostics.py +++ b/homeassistant/components/nexia/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for nexia.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/nexia/icons.json b/homeassistant/components/nexia/icons.json new file mode 100644 index 00000000000..620d1a42c03 --- /dev/null +++ b/homeassistant/components/nexia/icons.json @@ -0,0 +1,27 @@ +{ + "entity": { + "number": { + "fan_speed": { + "default": "mdi:fan" + } + }, + "scene": { + "automation": { + "default": "mdi:script-text-outline" + } + }, + "switch": { + "hold": { + "default": "mdi:timer", + "state": { + "on": "mdi:timer-off" + } + } + } + }, + "services": { + "set_aircleaner_mode": "mdi:air-filter", + "set_humidify_setpoint": "mdi:water-percent", + "set_hvac_run_mode": "mdi:hvac" + } +} diff --git a/homeassistant/components/nexia/manifest.json b/homeassistant/components/nexia/manifest.json index 1384226eac1..0013cd63de1 100644 --- a/homeassistant/components/nexia/manifest.json +++ b/homeassistant/components/nexia/manifest.json @@ -10,7 +10,6 @@ } ], "documentation": "https://www.home-assistant.io/integrations/nexia", - "import_executor": true, "iot_class": "cloud_polling", "loggers": ["nexia"], "requirements": ["nexia==2.0.8"] diff --git a/homeassistant/components/nexia/number.py b/homeassistant/components/nexia/number.py index b44c6a4c48f..a4117584720 100644 --- a/homeassistant/components/nexia/number.py +++ b/homeassistant/components/nexia/number.py @@ -1,4 +1,5 @@ """Support for Nexia / Trane XL Thermostats.""" + from __future__ import annotations from nexia.home import NexiaHome @@ -41,7 +42,6 @@ class NexiaFanSpeedEntity(NexiaThermostatEntity, NumberEntity): """Provides Nexia Fan Speed support.""" _attr_native_unit_of_measurement = PERCENTAGE - _attr_icon = "mdi:fan" _attr_translation_key = "fan_speed" def __init__( diff --git a/homeassistant/components/nexia/scene.py b/homeassistant/components/nexia/scene.py index 3a21c61badd..337068e44e9 100644 --- a/homeassistant/components/nexia/scene.py +++ b/homeassistant/components/nexia/scene.py @@ -1,4 +1,5 @@ """Support for Nexia Automations.""" + from typing import Any from nexia.automation import NexiaAutomation @@ -35,7 +36,7 @@ async def async_setup_entry( class NexiaAutomationScene(NexiaEntity, Scene): """Provides Nexia automation support.""" - _attr_icon = "mdi:script-text-outline" + _attr_translation_key = "automation" def __init__( self, coordinator: NexiaDataUpdateCoordinator, automation: NexiaAutomation diff --git a/homeassistant/components/nexia/sensor.py b/homeassistant/components/nexia/sensor.py index 79e07bc71b4..a77920630f8 100644 --- a/homeassistant/components/nexia/sensor.py +++ b/homeassistant/components/nexia/sensor.py @@ -1,4 +1,5 @@ """Support for Nexia / Trane XL Thermostats.""" + from __future__ import annotations from nexia.const import UNIT_CELSIUS diff --git a/homeassistant/components/nexia/switch.py b/homeassistant/components/nexia/switch.py index 7f191d39c73..5e19136d55e 100644 --- a/homeassistant/components/nexia/switch.py +++ b/homeassistant/components/nexia/switch.py @@ -1,4 +1,5 @@ """Support for Nexia switches.""" + from __future__ import annotations from typing import Any @@ -53,11 +54,6 @@ class NexiaHoldSwitch(NexiaThermostatZoneEntity, SwitchEntity): """Return if the zone is in hold mode.""" return self._zone.is_in_permanent_hold() - @property - def icon(self) -> str: - """Return the icon for the switch.""" - return "mdi:timer-off" if self._zone.is_in_permanent_hold() else "mdi:timer" - async def async_turn_on(self, **kwargs: Any) -> None: """Enable permanent hold.""" if self._zone.get_current_mode() == OPERATION_MODE_OFF: diff --git a/homeassistant/components/nextbus/config_flow.py b/homeassistant/components/nextbus/config_flow.py index a4045ada372..1d2b7f0b00f 100644 --- a/homeassistant/components/nextbus/config_flow.py +++ b/homeassistant/components/nextbus/config_flow.py @@ -1,13 +1,13 @@ """Config flow to configure the Nextbus integration.""" + from collections import Counter import logging from py_nextbus import NextBusClient import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_NAME, CONF_STOP -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.selector import ( SelectOptionDict, SelectSelector, @@ -89,7 +89,7 @@ def _unique_id_from_data(data: dict[str, str]) -> str: return f"{data[CONF_AGENCY]}_{data[CONF_ROUTE]}_{data[CONF_STOP]}" -class NextBusFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class NextBusFlowHandler(ConfigFlow, domain=DOMAIN): """Handle Nextbus configuration.""" VERSION = 1 @@ -104,7 +104,7 @@ class NextBusFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self._client = NextBusClient(output_format="json") _LOGGER.info("Init new config flow") - async def async_step_import(self, config_input: dict[str, str]) -> FlowResult: + async def async_step_import(self, config_input: dict[str, str]) -> ConfigFlowResult: """Handle import of config.""" agency_tag = config_input[CONF_AGENCY] route_tag = config_input[CONF_ROUTE] @@ -141,14 +141,14 @@ class NextBusFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, str] | None = None, - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initiated by the user.""" return await self.async_step_agency(user_input) async def async_step_agency( self, user_input: dict[str, str] | None = None, - ) -> FlowResult: + ) -> ConfigFlowResult: """Select agency.""" if user_input is not None: self.data[CONF_AGENCY] = user_input[CONF_AGENCY] @@ -173,7 +173,7 @@ class NextBusFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_route( self, user_input: dict[str, str] | None = None, - ) -> FlowResult: + ) -> ConfigFlowResult: """Select route.""" if user_input is not None: self.data[CONF_ROUTE] = user_input[CONF_ROUTE] @@ -198,7 +198,7 @@ class NextBusFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_stop( self, user_input: dict[str, str] | None = None, - ) -> FlowResult: + ) -> ConfigFlowResult: """Select stop.""" if user_input is not None: diff --git a/homeassistant/components/nextbus/const.py b/homeassistant/components/nextbus/const.py index 0a2eabf57b3..aa125193031 100644 --- a/homeassistant/components/nextbus/const.py +++ b/homeassistant/components/nextbus/const.py @@ -1,4 +1,5 @@ """NextBus Constants.""" + DOMAIN = "nextbus" CONF_AGENCY = "agency" diff --git a/homeassistant/components/nextbus/coordinator.py b/homeassistant/components/nextbus/coordinator.py index f130e40ef05..abf280bece9 100644 --- a/homeassistant/components/nextbus/coordinator.py +++ b/homeassistant/components/nextbus/coordinator.py @@ -1,4 +1,5 @@ """NextBus data update coordinator.""" + from datetime import timedelta import logging from typing import Any, cast diff --git a/homeassistant/components/nextbus/icons.json b/homeassistant/components/nextbus/icons.json new file mode 100644 index 00000000000..7176a937e83 --- /dev/null +++ b/homeassistant/components/nextbus/icons.json @@ -0,0 +1,9 @@ +{ + "entity": { + "sensor": { + "nextbus": { + "default": "mdi:bus" + } + } + } +} diff --git a/homeassistant/components/nextbus/sensor.py b/homeassistant/components/nextbus/sensor.py index f62bf07eeef..68d10726609 100644 --- a/homeassistant/components/nextbus/sensor.py +++ b/homeassistant/components/nextbus/sensor.py @@ -1,4 +1,5 @@ """NextBus sensor.""" + from __future__ import annotations from itertools import chain @@ -105,7 +106,7 @@ class NextBusDepartureSensor( """ _attr_device_class = SensorDeviceClass.TIMESTAMP - _attr_icon = "mdi:bus" + _attr_translation_key = "nextbus" def __init__( self, diff --git a/homeassistant/components/nextbus/util.py b/homeassistant/components/nextbus/util.py index 73b3b400ff4..e9a1e1fd254 100644 --- a/homeassistant/components/nextbus/util.py +++ b/homeassistant/components/nextbus/util.py @@ -1,4 +1,5 @@ """Utils for NextBus integration module.""" + from typing import Any diff --git a/homeassistant/components/nextcloud/binary_sensor.py b/homeassistant/components/nextcloud/binary_sensor.py index 313d555a3d7..6c6f6141975 100644 --- a/homeassistant/components/nextcloud/binary_sensor.py +++ b/homeassistant/components/nextcloud/binary_sensor.py @@ -1,4 +1,5 @@ """Summary binary data from Nextcoud.""" + from __future__ import annotations from typing import Final @@ -58,11 +59,9 @@ async def async_setup_entry( """Set up the Nextcloud binary sensors.""" coordinator: NextcloudDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] async_add_entities( - [ - NextcloudBinarySensor(coordinator, entry, sensor) - for sensor in BINARY_SENSORS - if sensor.key in coordinator.data - ] + NextcloudBinarySensor(coordinator, entry, sensor) + for sensor in BINARY_SENSORS + if sensor.key in coordinator.data ) diff --git a/homeassistant/components/nextcloud/config_flow.py b/homeassistant/components/nextcloud/config_flow.py index ec56307aad7..c469936ac48 100644 --- a/homeassistant/components/nextcloud/config_flow.py +++ b/homeassistant/components/nextcloud/config_flow.py @@ -1,4 +1,5 @@ """Config flow to configure the Nextcloud integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -12,9 +13,8 @@ from nextcloudmonitor import ( ) import voluptuous as vol -from homeassistant.config_entries import ConfigEntry, ConfigFlow +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME, CONF_VERIFY_SSL -from homeassistant.data_entry_flow import FlowResult from .const import DEFAULT_VERIFY_SSL, DOMAIN @@ -52,7 +52,7 @@ class NextcloudConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" errors = {} @@ -75,14 +75,16 @@ class NextcloudConfigFlow(ConfigFlow, domain=DOMAIN): step_id="user", data_schema=data_schema, errors=errors ) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Handle flow upon an API authentication error.""" self._entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) return await self.async_step_reauth_confirm() async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle reauthorization flow.""" errors = {} assert self._entry is not None diff --git a/homeassistant/components/nextcloud/const.py b/homeassistant/components/nextcloud/const.py index 248128dd538..2cc6513cd4b 100644 --- a/homeassistant/components/nextcloud/const.py +++ b/homeassistant/components/nextcloud/const.py @@ -1,4 +1,5 @@ """Constants for Nextcloud integration.""" + from datetime import timedelta DOMAIN = "nextcloud" diff --git a/homeassistant/components/nextcloud/entity.py b/homeassistant/components/nextcloud/entity.py index b9dab9179c1..19431756e43 100644 --- a/homeassistant/components/nextcloud/entity.py +++ b/homeassistant/components/nextcloud/entity.py @@ -1,4 +1,5 @@ """Base entity for the Nextcloud integration.""" + from urllib.parse import urlparse from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/nextcloud/icons.json b/homeassistant/components/nextcloud/icons.json new file mode 100644 index 00000000000..7fc2e13cd50 --- /dev/null +++ b/homeassistant/components/nextcloud/icons.json @@ -0,0 +1,75 @@ +{ + "entity": { + "sensor": { + "nextcloud_activeusers_last1hour": { + "default": "mdi:account-multiple" + }, + "nextcloud_activeusers_last24hours": { + "default": "mdi:account-multiple" + }, + "nextcloud_activeusers_last5minutes": { + "default": "mdi:account-multiple" + }, + "nextcloud_database_size": { + "default": "mdi:database" + }, + "nextcloud_database_type": { + "default": "mdi:database" + }, + "nextcloud_database_version": { + "default": "mdi:database" + }, + "nextcloud_server_php_opcache_memory_usage_current_wasted_percentage": { + "default": "mdi:language-php" + }, + "nextcloud_server_php_opcache_memory_usage_free_memory": { + "default": "mdi:language-php" + }, + "nextcloud_server_php_opcache_memory_usage_used_memory": { + "default": "mdi:language-php" + }, + "nextcloud_server_php_opcache_memory_usage_wasted_memory": { + "default": "mdi:language-php" + }, + "nextcloud_server_php_max_execution_time": { + "default": "mdi:language-php" + }, + "nextcloud_server_php_memory_limit": { + "default": "mdi:language-php" + }, + "nextcloud_server_php_upload_max_filesize": { + "default": "mdi:language-php" + }, + "nextcloud_server_php_version": { + "default": "mdi:language-php" + }, + "nextcloud_system_apps_num_updates_available": { + "default": "mdi:update" + }, + "nextcloud_system_cpuload_1": { + "default": "mdi:chip" + }, + "nextcloud_system_cpuload_5": { + "default": "mdi:chip" + }, + "nextcloud_system_cpuload_15": { + "default": "mdi:chip" + }, + "nextcloud_system_freespace": { + "default": "mdi:harddisk" + }, + "nextcloud_system_mem_free": { + "default": "mdi:memory" + }, + "nextcloud_system_mem_total": { + "default": "mdi:memory" + }, + "nextcloud_system_swap_total": { + "default": "mdi:memory" + }, + "nextcloud_system_swap_free": { + "default": "mdi:memory" + } + } + } +} diff --git a/homeassistant/components/nextcloud/sensor.py b/homeassistant/components/nextcloud/sensor.py index 851cb9f3cd3..d8a2a362ce0 100644 --- a/homeassistant/components/nextcloud/sensor.py +++ b/homeassistant/components/nextcloud/sensor.py @@ -1,4 +1,5 @@ """Summary data from Nextcoud.""" + from __future__ import annotations from collections.abc import Callable @@ -45,21 +46,18 @@ SENSORS: Final[list[NextcloudSensorEntityDescription]] = [ translation_key="nextcloud_activeusers_last1hour", state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, - icon="mdi:account-multiple", ), NextcloudSensorEntityDescription( key="activeUsers_last24hours", translation_key="nextcloud_activeusers_last24hours", state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, - icon="mdi:account-multiple", ), NextcloudSensorEntityDescription( key="activeUsers_last5minutes", translation_key="nextcloud_activeusers_last5minutes", state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, - icon="mdi:account-multiple", ), NextcloudSensorEntityDescription( key="cache_expunges", @@ -136,7 +134,6 @@ SENSORS: Final[list[NextcloudSensorEntityDescription]] = [ key="database_size", translation_key="nextcloud_database_size", device_class=SensorDeviceClass.DATA_SIZE, - icon="mdi:database", native_unit_of_measurement=UnitOfInformation.BYTES, suggested_display_precision=1, suggested_unit_of_measurement=UnitOfInformation.MEGABYTES, @@ -145,13 +142,11 @@ SENSORS: Final[list[NextcloudSensorEntityDescription]] = [ key="database_type", translation_key="nextcloud_database_type", entity_category=EntityCategory.DIAGNOSTIC, - icon="mdi:database", ), NextcloudSensorEntityDescription( key="database_version", translation_key="nextcloud_database_version", entity_category=EntityCategory.DIAGNOSTIC, - icon="mdi:database", ), NextcloudSensorEntityDescription( key="interned_strings_usage_buffer_size", @@ -328,7 +323,6 @@ SENSORS: Final[list[NextcloudSensorEntityDescription]] = [ translation_key="nextcloud_server_php_opcache_memory_usage_current_wasted_percentage", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, - icon="mdi:language-php", native_unit_of_measurement=PERCENTAGE, suggested_display_precision=1, ), @@ -338,7 +332,6 @@ SENSORS: Final[list[NextcloudSensorEntityDescription]] = [ device_class=SensorDeviceClass.DATA_SIZE, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, - icon="mdi:language-php", native_unit_of_measurement=UnitOfInformation.BYTES, suggested_display_precision=1, suggested_unit_of_measurement=UnitOfInformation.MEGABYTES, @@ -349,7 +342,6 @@ SENSORS: Final[list[NextcloudSensorEntityDescription]] = [ device_class=SensorDeviceClass.DATA_SIZE, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, - icon="mdi:language-php", native_unit_of_measurement=UnitOfInformation.BYTES, suggested_display_precision=1, suggested_unit_of_measurement=UnitOfInformation.MEGABYTES, @@ -360,7 +352,6 @@ SENSORS: Final[list[NextcloudSensorEntityDescription]] = [ device_class=SensorDeviceClass.DATA_SIZE, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, - icon="mdi:language-php", native_unit_of_measurement=UnitOfInformation.BYTES, suggested_display_precision=1, suggested_unit_of_measurement=UnitOfInformation.MEGABYTES, @@ -370,7 +361,6 @@ SENSORS: Final[list[NextcloudSensorEntityDescription]] = [ translation_key="nextcloud_server_php_max_execution_time", device_class=SensorDeviceClass.DURATION, entity_category=EntityCategory.DIAGNOSTIC, - icon="mdi:language-php", native_unit_of_measurement=UnitOfTime.SECONDS, ), NextcloudSensorEntityDescription( @@ -378,7 +368,6 @@ SENSORS: Final[list[NextcloudSensorEntityDescription]] = [ translation_key="nextcloud_server_php_memory_limit", device_class=SensorDeviceClass.DATA_SIZE, entity_category=EntityCategory.DIAGNOSTIC, - icon="mdi:language-php", native_unit_of_measurement=UnitOfInformation.BYTES, suggested_display_precision=1, suggested_unit_of_measurement=UnitOfInformation.MEGABYTES, @@ -388,7 +377,6 @@ SENSORS: Final[list[NextcloudSensorEntityDescription]] = [ translation_key="nextcloud_server_php_upload_max_filesize", device_class=SensorDeviceClass.DATA_SIZE, entity_category=EntityCategory.DIAGNOSTIC, - icon="mdi:language-php", native_unit_of_measurement=UnitOfInformation.BYTES, suggested_display_precision=1, suggested_unit_of_measurement=UnitOfInformation.MEGABYTES, @@ -397,7 +385,6 @@ SENSORS: Final[list[NextcloudSensorEntityDescription]] = [ key="server_php_version", translation_key="nextcloud_server_php_version", entity_category=EntityCategory.DIAGNOSTIC, - icon="mdi:language-php", ), NextcloudSensorEntityDescription( key="server_webserver", @@ -526,34 +513,29 @@ SENSORS: Final[list[NextcloudSensorEntityDescription]] = [ key="system_apps_num_updates_available", translation_key="nextcloud_system_apps_num_updates_available", state_class=SensorStateClass.MEASUREMENT, - icon="mdi:update", ), NextcloudSensorEntityDescription( key="system_cpuload_1", translation_key="nextcloud_system_cpuload_1", native_unit_of_measurement=UNIT_OF_LOAD, - icon="mdi:chip", suggested_display_precision=2, ), NextcloudSensorEntityDescription( key="system_cpuload_5", translation_key="nextcloud_system_cpuload_5", native_unit_of_measurement=UNIT_OF_LOAD, - icon="mdi:chip", suggested_display_precision=2, ), NextcloudSensorEntityDescription( key="system_cpuload_15", translation_key="nextcloud_system_cpuload_15", native_unit_of_measurement=UNIT_OF_LOAD, - icon="mdi:chip", suggested_display_precision=2, ), NextcloudSensorEntityDescription( key="system_freespace", translation_key="nextcloud_system_freespace", device_class=SensorDeviceClass.DATA_SIZE, - icon="mdi:harddisk", native_unit_of_measurement=UnitOfInformation.BYTES, suggested_display_precision=2, suggested_unit_of_measurement=UnitOfInformation.GIGABYTES, @@ -562,7 +544,6 @@ SENSORS: Final[list[NextcloudSensorEntityDescription]] = [ key="system_mem_free", translation_key="nextcloud_system_mem_free", device_class=SensorDeviceClass.DATA_SIZE, - icon="mdi:memory", native_unit_of_measurement=UnitOfInformation.KILOBYTES, suggested_display_precision=2, suggested_unit_of_measurement=UnitOfInformation.GIGABYTES, @@ -571,7 +552,6 @@ SENSORS: Final[list[NextcloudSensorEntityDescription]] = [ key="system_mem_total", translation_key="nextcloud_system_mem_total", device_class=SensorDeviceClass.DATA_SIZE, - icon="mdi:memory", native_unit_of_measurement=UnitOfInformation.KILOBYTES, suggested_display_precision=2, suggested_unit_of_measurement=UnitOfInformation.GIGABYTES, @@ -598,7 +578,6 @@ SENSORS: Final[list[NextcloudSensorEntityDescription]] = [ key="system_swap_total", translation_key="nextcloud_system_swap_total", device_class=SensorDeviceClass.DATA_SIZE, - icon="mdi:memory", native_unit_of_measurement=UnitOfInformation.KILOBYTES, suggested_display_precision=2, suggested_unit_of_measurement=UnitOfInformation.GIGABYTES, @@ -607,7 +586,6 @@ SENSORS: Final[list[NextcloudSensorEntityDescription]] = [ key="system_swap_free", translation_key="nextcloud_system_swap_free", device_class=SensorDeviceClass.DATA_SIZE, - icon="mdi:memory", native_unit_of_measurement=UnitOfInformation.KILOBYTES, suggested_display_precision=2, suggested_unit_of_measurement=UnitOfInformation.GIGABYTES, @@ -629,11 +607,9 @@ async def async_setup_entry( """Set up the Nextcloud sensors.""" coordinator: NextcloudDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] async_add_entities( - [ - NextcloudSensor(coordinator, entry, sensor) - for sensor in SENSORS - if sensor.key in coordinator.data - ] + NextcloudSensor(coordinator, entry, sensor) + for sensor in SENSORS + if sensor.key in coordinator.data ) diff --git a/homeassistant/components/nextcloud/update.py b/homeassistant/components/nextcloud/update.py index 5d52ac2a48f..52583d690bf 100644 --- a/homeassistant/components/nextcloud/update.py +++ b/homeassistant/components/nextcloud/update.py @@ -1,4 +1,5 @@ """Update data from Nextcoud.""" + from __future__ import annotations from homeassistant.components.update import UpdateEntity, UpdateEntityDescription diff --git a/homeassistant/components/nextdns/__init__.py b/homeassistant/components/nextdns/__init__.py index af972fb7509..389173a2694 100644 --- a/homeassistant/components/nextdns/__init__.py +++ b/homeassistant/components/nextdns/__init__.py @@ -1,4 +1,5 @@ """The NextDNS component.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/nextdns/binary_sensor.py b/homeassistant/components/nextdns/binary_sensor.py index dad29893161..f6860586808 100644 --- a/homeassistant/components/nextdns/binary_sensor.py +++ b/homeassistant/components/nextdns/binary_sensor.py @@ -1,4 +1,5 @@ """Support for the NextDNS service.""" + from __future__ import annotations from collections.abc import Callable @@ -67,11 +68,9 @@ async def async_setup_entry( ATTR_CONNECTION ] - sensors: list[NextDnsBinarySensor] = [] - for description in SENSORS: - sensors.append(NextDnsBinarySensor(coordinator, description)) - - async_add_entities(sensors) + async_add_entities( + NextDnsBinarySensor(coordinator, description) for description in SENSORS + ) class NextDnsBinarySensor( diff --git a/homeassistant/components/nextdns/button.py b/homeassistant/components/nextdns/button.py index 2eafe2b477e..d74152248a5 100644 --- a/homeassistant/components/nextdns/button.py +++ b/homeassistant/components/nextdns/button.py @@ -1,4 +1,5 @@ """Support for the NextDNS service.""" + from __future__ import annotations from homeassistant.components.button import ButtonEntity, ButtonEntityDescription diff --git a/homeassistant/components/nextdns/config_flow.py b/homeassistant/components/nextdns/config_flow.py index b0a1d936752..28fd50af2dc 100644 --- a/homeassistant/components/nextdns/config_flow.py +++ b/homeassistant/components/nextdns/config_flow.py @@ -1,4 +1,5 @@ """Adds config flow for NextDNS.""" + from __future__ import annotations import asyncio @@ -8,15 +9,14 @@ from aiohttp.client_exceptions import ClientConnectorError from nextdns import ApiError, InvalidApiKeyError, NextDns import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_API_KEY, CONF_PROFILE_NAME -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import CONF_PROFILE_ID, DOMAIN -class NextDnsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class NextDnsFlowHandler(ConfigFlow, domain=DOMAIN): """Config flow for NextDNS.""" VERSION = 1 @@ -28,7 +28,7 @@ class NextDnsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" errors: dict[str, str] = {} @@ -58,7 +58,7 @@ class NextDnsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_profiles( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the profiles step.""" errors: dict[str, str] = {} diff --git a/homeassistant/components/nextdns/const.py b/homeassistant/components/nextdns/const.py index 031dd1c5814..b8210c1939c 100644 --- a/homeassistant/components/nextdns/const.py +++ b/homeassistant/components/nextdns/const.py @@ -1,4 +1,5 @@ """Constants for NextDNS integration.""" + from datetime import timedelta ATTR_CONNECTION = "connection" diff --git a/homeassistant/components/nextdns/diagnostics.py b/homeassistant/components/nextdns/diagnostics.py index 2c0d313060f..c0a9071bb9d 100644 --- a/homeassistant/components/nextdns/diagnostics.py +++ b/homeassistant/components/nextdns/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for NextDNS.""" + from __future__ import annotations from dataclasses import asdict diff --git a/homeassistant/components/nextdns/sensor.py b/homeassistant/components/nextdns/sensor.py index b6864fea50a..4357179cbdb 100644 --- a/homeassistant/components/nextdns/sensor.py +++ b/homeassistant/components/nextdns/sensor.py @@ -1,4 +1,5 @@ """Support for the NextDNS service.""" + from __future__ import annotations from collections.abc import Callable @@ -310,15 +311,12 @@ async def async_setup_entry( 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) + async_add_entities( + NextDnsSensor(coordinators[description.coordinator_type], description) + for description in SENSORS + ) class NextDnsSensor( diff --git a/homeassistant/components/nextdns/switch.py b/homeassistant/components/nextdns/switch.py index a01b8a8c3c3..81bf8b4e8c6 100644 --- a/homeassistant/components/nextdns/switch.py +++ b/homeassistant/components/nextdns/switch.py @@ -1,4 +1,5 @@ """Support for the NextDNS service.""" + from __future__ import annotations from collections.abc import Callable @@ -537,11 +538,9 @@ async def async_setup_entry( ATTR_SETTINGS ] - switches: list[NextDnsSwitch] = [] - for description in SWITCHES: - switches.append(NextDnsSwitch(coordinator, description)) - - async_add_entities(switches) + async_add_entities( + NextDnsSwitch(coordinator, description) for description in SWITCHES + ) class NextDnsSwitch(CoordinatorEntity[NextDnsSettingsUpdateCoordinator], SwitchEntity): diff --git a/homeassistant/components/nextdns/system_health.py b/homeassistant/components/nextdns/system_health.py index a56a89914b8..09c13f0580e 100644 --- a/homeassistant/components/nextdns/system_health.py +++ b/homeassistant/components/nextdns/system_health.py @@ -1,4 +1,5 @@ """Provide info to system health.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/nfandroidtv/__init__.py b/homeassistant/components/nfandroidtv/__init__.py index 38622fc0060..42d42e26d1f 100644 --- a/homeassistant/components/nfandroidtv/__init__.py +++ b/homeassistant/components/nfandroidtv/__init__.py @@ -1,4 +1,5 @@ """The NFAndroidTV integration.""" + from notifications_android_tv.notifications import ConnectError, Notifications from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/nfandroidtv/config_flow.py b/homeassistant/components/nfandroidtv/config_flow.py index d4491cee48e..83621c63789 100644 --- a/homeassistant/components/nfandroidtv/config_flow.py +++ b/homeassistant/components/nfandroidtv/config_flow.py @@ -1,4 +1,5 @@ """Config flow for NFAndroidTV integration.""" + from __future__ import annotations import logging @@ -7,21 +8,20 @@ from typing import Any from notifications_android_tv.notifications import ConnectError, Notifications import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_NAME -from homeassistant.data_entry_flow import FlowResult from .const import DEFAULT_NAME, DOMAIN _LOGGER = logging.getLogger(__name__) -class NFAndroidTVFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class NFAndroidTVFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a config flow for NFAndroidTV.""" async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initiated by the user.""" errors = {} diff --git a/homeassistant/components/nfandroidtv/const.py b/homeassistant/components/nfandroidtv/const.py index 4d4a7c82ecb..cd4b99d0981 100644 --- a/homeassistant/components/nfandroidtv/const.py +++ b/homeassistant/components/nfandroidtv/const.py @@ -1,4 +1,5 @@ """Constants for the NFAndroidTV integration.""" + DOMAIN: str = "nfandroidtv" CONF_DURATION = "duration" CONF_FONTSIZE = "fontsize" diff --git a/homeassistant/components/nfandroidtv/notify.py b/homeassistant/components/nfandroidtv/notify.py index c70272d3835..dd42a0ab10b 100644 --- a/homeassistant/components/nfandroidtv/notify.py +++ b/homeassistant/components/nfandroidtv/notify.py @@ -1,4 +1,5 @@ """Notifications for Android TV notification service.""" + from __future__ import annotations from io import BufferedReader diff --git a/homeassistant/components/nibe_heatpump/__init__.py b/homeassistant/components/nibe_heatpump/__init__.py index 058f3ef8711..fbb49351e0e 100644 --- a/homeassistant/components/nibe_heatpump/__init__.py +++ b/homeassistant/components/nibe_heatpump/__init__.py @@ -1,4 +1,5 @@ """The Nibe Heat Pump integration.""" + from __future__ import annotations from nibe.connection import Connection diff --git a/homeassistant/components/nibe_heatpump/binary_sensor.py b/homeassistant/components/nibe_heatpump/binary_sensor.py index d1fdfa710a1..035a4a23a08 100644 --- a/homeassistant/components/nibe_heatpump/binary_sensor.py +++ b/homeassistant/components/nibe_heatpump/binary_sensor.py @@ -1,4 +1,5 @@ """The Nibe Heat Pump binary sensors.""" + from __future__ import annotations from nibe.coil import Coil, CoilData diff --git a/homeassistant/components/nibe_heatpump/button.py b/homeassistant/components/nibe_heatpump/button.py index f45b2af2909..0c3122805e1 100644 --- a/homeassistant/components/nibe_heatpump/button.py +++ b/homeassistant/components/nibe_heatpump/button.py @@ -1,4 +1,5 @@ """The Nibe Heat Pump sensors.""" + from __future__ import annotations from nibe.coil_groups import UNIT_COILGROUPS, UnitCoilGroup diff --git a/homeassistant/components/nibe_heatpump/climate.py b/homeassistant/components/nibe_heatpump/climate.py index 3a89f4f6022..746ed26687d 100644 --- a/homeassistant/components/nibe_heatpump/climate.py +++ b/homeassistant/components/nibe_heatpump/climate.py @@ -1,4 +1,5 @@ """The Nibe Heat Pump climate.""" + from __future__ import annotations from datetime import date diff --git a/homeassistant/components/nibe_heatpump/config_flow.py b/homeassistant/components/nibe_heatpump/config_flow.py index 6680ca6e325..913ebd6b00c 100644 --- a/homeassistant/components/nibe_heatpump/config_flow.py +++ b/homeassistant/components/nibe_heatpump/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Nibe Heat Pump integration.""" + from __future__ import annotations from typing import Any @@ -17,10 +18,9 @@ from nibe.heatpump import HeatPump, Model import voluptuous as vol import yarl -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_IP_ADDRESS, CONF_MODEL from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import selector from .const import ( @@ -166,20 +166,20 @@ async def validate_modbus_input( } -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class NibeHeatPumpConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Nibe Heat Pump.""" VERSION = 1 async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" return self.async_show_menu(step_id="user", menu_options=["modbus", "nibegw"]) async def async_step_modbus( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the modbus step.""" if user_input is None: return self.async_show_form( @@ -205,7 +205,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_nibegw( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the nibegw step.""" if user_input is None: return self.async_show_form( diff --git a/homeassistant/components/nibe_heatpump/const.py b/homeassistant/components/nibe_heatpump/const.py index 0f16567671c..ccdd92783de 100644 --- a/homeassistant/components/nibe_heatpump/const.py +++ b/homeassistant/components/nibe_heatpump/const.py @@ -1,4 +1,5 @@ """Constants for the Nibe Heat Pump integration.""" + import logging DOMAIN = "nibe_heatpump" diff --git a/homeassistant/components/nibe_heatpump/coordinator.py b/homeassistant/components/nibe_heatpump/coordinator.py index ce75247083b..1711c3d8f2a 100644 --- a/homeassistant/components/nibe_heatpump/coordinator.py +++ b/homeassistant/components/nibe_heatpump/coordinator.py @@ -1,4 +1,5 @@ """The Nibe Heat Pump coordinator.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/nibe_heatpump/number.py b/homeassistant/components/nibe_heatpump/number.py index 83ccc124e51..509f3364fee 100644 --- a/homeassistant/components/nibe_heatpump/number.py +++ b/homeassistant/components/nibe_heatpump/number.py @@ -1,4 +1,5 @@ """The Nibe Heat Pump numbers.""" + from __future__ import annotations from nibe.coil import Coil, CoilData diff --git a/homeassistant/components/nibe_heatpump/select.py b/homeassistant/components/nibe_heatpump/select.py index c4794cc18b7..07c958885b8 100644 --- a/homeassistant/components/nibe_heatpump/select.py +++ b/homeassistant/components/nibe_heatpump/select.py @@ -1,4 +1,5 @@ """The Nibe Heat Pump select.""" + from __future__ import annotations from nibe.coil import Coil, CoilData diff --git a/homeassistant/components/nibe_heatpump/sensor.py b/homeassistant/components/nibe_heatpump/sensor.py index 8c9439e6531..c6bac0323b9 100644 --- a/homeassistant/components/nibe_heatpump/sensor.py +++ b/homeassistant/components/nibe_heatpump/sensor.py @@ -1,4 +1,5 @@ """The Nibe Heat Pump sensors.""" + from __future__ import annotations from nibe.coil import Coil, CoilData diff --git a/homeassistant/components/nibe_heatpump/switch.py b/homeassistant/components/nibe_heatpump/switch.py index f55882d529c..594a8078b76 100644 --- a/homeassistant/components/nibe_heatpump/switch.py +++ b/homeassistant/components/nibe_heatpump/switch.py @@ -1,4 +1,5 @@ """The Nibe Heat Pump switch.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/nibe_heatpump/water_heater.py b/homeassistant/components/nibe_heatpump/water_heater.py index db688fdb69c..c60f5b6e3b2 100644 --- a/homeassistant/components/nibe_heatpump/water_heater.py +++ b/homeassistant/components/nibe_heatpump/water_heater.py @@ -1,4 +1,5 @@ """The Nibe Heat Pump sensors.""" + from __future__ import annotations from datetime import date diff --git a/homeassistant/components/nightscout/config_flow.py b/homeassistant/components/nightscout/config_flow.py index 6249979c83d..6d2a0e6c385 100644 --- a/homeassistant/components/nightscout/config_flow.py +++ b/homeassistant/components/nightscout/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Nightscout integration.""" + import logging from typing import Any @@ -6,9 +7,9 @@ from aiohttp import ClientError, ClientResponseError from py_nightscout import Api as NightscoutAPI import voluptuous as vol -from homeassistant import config_entries, exceptions +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_API_KEY, CONF_URL -from homeassistant.data_entry_flow import FlowResult +from homeassistant.exceptions import HomeAssistantError from .const import DOMAIN from .utils import hash_from_url @@ -36,14 +37,14 @@ async def _validate_input(data: dict[str, Any]) -> dict[str, str]: return {"title": status.name} -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class NightscoutConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Nightscout.""" VERSION = 1 async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors: dict[str, str] = {} @@ -66,7 +67,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -class InputValidationError(exceptions.HomeAssistantError): +class InputValidationError(HomeAssistantError): """Error to indicate we cannot proceed due to invalid input.""" def __init__(self, base: str) -> None: diff --git a/homeassistant/components/nightscout/sensor.py b/homeassistant/components/nightscout/sensor.py index bdc46e75cb8..92291bdc4f9 100644 --- a/homeassistant/components/nightscout/sensor.py +++ b/homeassistant/components/nightscout/sensor.py @@ -1,4 +1,5 @@ """Support for Nightscout sensors.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/nightscout/utils.py b/homeassistant/components/nightscout/utils.py index ac9ce1a3384..928abd1aa4f 100644 --- a/homeassistant/components/nightscout/utils.py +++ b/homeassistant/components/nightscout/utils.py @@ -1,4 +1,5 @@ """Nightscout util functions.""" + from __future__ import annotations import hashlib diff --git a/homeassistant/components/niko_home_control/light.py b/homeassistant/components/niko_home_control/light.py index b541a145a66..6554bf5eeec 100644 --- a/homeassistant/components/niko_home_control/light.py +++ b/homeassistant/components/niko_home_control/light.py @@ -1,4 +1,5 @@ """Support for Niko Home Control.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/nilu/air_quality.py b/homeassistant/components/nilu/air_quality.py index 3745c6bae6f..7b1068771d2 100644 --- a/homeassistant/components/nilu/air_quality.py +++ b/homeassistant/components/nilu/air_quality.py @@ -1,4 +1,5 @@ """Sensor for checking the air quality around Norway.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/nina/__init__.py b/homeassistant/components/nina/__init__.py index 435ea288aa7..d5b1c5ccb35 100644 --- a/homeassistant/components/nina/__init__.py +++ b/homeassistant/components/nina/__init__.py @@ -1,4 +1,5 @@ """The Nina integration.""" + from __future__ import annotations from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/nina/binary_sensor.py b/homeassistant/components/nina/binary_sensor.py index 92c7d16dc84..397ced0f5d3 100644 --- a/homeassistant/components/nina/binary_sensor.py +++ b/homeassistant/components/nina/binary_sensor.py @@ -1,4 +1,5 @@ """NINA sensor platform.""" + from __future__ import annotations from typing import Any @@ -43,15 +44,11 @@ async def async_setup_entry( regions: dict[str, str] = config_entry.data[CONF_REGIONS] message_slots: int = config_entry.data[CONF_MESSAGE_SLOTS] - entities: list[NINAMessage] = [] - - for ent in coordinator.data: - for i in range(0, message_slots): - entities.append( - NINAMessage(coordinator, ent, regions[ent], i + 1, config_entry) - ) - - async_add_entities(entities) + async_add_entities( + NINAMessage(coordinator, ent, regions[ent], i + 1, config_entry) + for ent in coordinator.data + for i in range(message_slots) + ) class NINAMessage(CoordinatorEntity[NINADataUpdateCoordinator], BinarySensorEntity): diff --git a/homeassistant/components/nina/config_flow.py b/homeassistant/components/nina/config_flow.py index 9c6de40ac6b..07c3f6fe9a1 100644 --- a/homeassistant/components/nina/config_flow.py +++ b/homeassistant/components/nina/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Nina integration.""" + from __future__ import annotations from typing import Any @@ -6,9 +7,13 @@ from typing import Any from pynina import ApiError, Nina import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_registry import ( @@ -81,7 +86,7 @@ def prepare_user_input( return user_input -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class NinaConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for NINA.""" VERSION: int = 1 @@ -96,9 +101,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.regions[name] = {} async def async_step_user( - self: ConfigFlow, + self, user_input: dict[str, Any] | None = None, - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors: dict[str, Any] = {} @@ -158,16 +163,16 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback def async_get_options_flow( - config_entry: config_entries.ConfigEntry, + config_entry: ConfigEntry, ) -> OptionsFlowHandler: """Get the options flow for this handler.""" return OptionsFlowHandler(config_entry) -class OptionsFlowHandler(config_entries.OptionsFlow): +class OptionsFlowHandler(OptionsFlow): """Handle a option flow for nut.""" - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + def __init__(self, config_entry: ConfigEntry) -> None: """Initialize options flow.""" self.config_entry = config_entry self.data = dict(self.config_entry.data) @@ -220,7 +225,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow): removed_entities_slots = [ f"{region}-{slot_id}" for region in self.data[CONF_REGIONS] - for slot_id in range(0, self.data[CONF_MESSAGE_SLOTS] + 1) + for slot_id in range(self.data[CONF_MESSAGE_SLOTS] + 1) if slot_id > user_input[CONF_MESSAGE_SLOTS] ] diff --git a/homeassistant/components/nina/const.py b/homeassistant/components/nina/const.py index 198e21c2689..1e755056079 100644 --- a/homeassistant/components/nina/const.py +++ b/homeassistant/components/nina/const.py @@ -1,4 +1,5 @@ """Constants for the Nina integration.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/nina/coordinator.py b/homeassistant/components/nina/coordinator.py index b2c97503442..c731c7a62d7 100644 --- a/homeassistant/components/nina/coordinator.py +++ b/homeassistant/components/nina/coordinator.py @@ -1,4 +1,5 @@ """DataUpdateCoordinator for the nina integration.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/nissan_leaf/__init__.py b/homeassistant/components/nissan_leaf/__init__.py index a0cb3c4f8cc..9eaca66119f 100644 --- a/homeassistant/components/nissan_leaf/__init__.py +++ b/homeassistant/components/nissan_leaf/__init__.py @@ -1,4 +1,5 @@ """Support for the Nissan Leaf Carwings/Nissan Connect API.""" + from __future__ import annotations import asyncio @@ -320,9 +321,9 @@ class LeafDataStore: self.data[DATA_RANGE_AC] = None if hasattr(server_response, "cruising_range_ac_off_km"): - self.data[ - DATA_RANGE_AC_OFF - ] = server_response.cruising_range_ac_off_km + self.data[DATA_RANGE_AC_OFF] = ( + server_response.cruising_range_ac_off_km + ) else: self.data[DATA_RANGE_AC_OFF] = None diff --git a/homeassistant/components/nissan_leaf/binary_sensor.py b/homeassistant/components/nissan_leaf/binary_sensor.py index 40880714c2a..3b15fabe382 100644 --- a/homeassistant/components/nissan_leaf/binary_sensor.py +++ b/homeassistant/components/nissan_leaf/binary_sensor.py @@ -1,4 +1,5 @@ """Plugged In Status Support for the Nissan Leaf.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/nissan_leaf/button.py b/homeassistant/components/nissan_leaf/button.py index 1f948fe9847..aa2bbbbca9b 100644 --- a/homeassistant/components/nissan_leaf/button.py +++ b/homeassistant/components/nissan_leaf/button.py @@ -1,4 +1,5 @@ """Button to start charging the Nissan Leaf.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/nissan_leaf/const.py b/homeassistant/components/nissan_leaf/const.py index f7ebed99ab8..299576b86a7 100644 --- a/homeassistant/components/nissan_leaf/const.py +++ b/homeassistant/components/nissan_leaf/const.py @@ -1,4 +1,5 @@ """Constants for the Nissan Leaf integration.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/nissan_leaf/icons.json b/homeassistant/components/nissan_leaf/icons.json new file mode 100644 index 00000000000..5da03ed5f1a --- /dev/null +++ b/homeassistant/components/nissan_leaf/icons.json @@ -0,0 +1,6 @@ +{ + "services": { + "start_charge": "mdi:flash", + "update": "mdi:update" + } +} diff --git a/homeassistant/components/nissan_leaf/sensor.py b/homeassistant/components/nissan_leaf/sensor.py index cd3524eaf87..bde1719e9b1 100644 --- a/homeassistant/components/nissan_leaf/sensor.py +++ b/homeassistant/components/nissan_leaf/sensor.py @@ -1,4 +1,5 @@ """Battery Charge and Range Support for the Nissan Leaf.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/nissan_leaf/switch.py b/homeassistant/components/nissan_leaf/switch.py index 97f02a9e0be..39f875ff95f 100644 --- a/homeassistant/components/nissan_leaf/switch.py +++ b/homeassistant/components/nissan_leaf/switch.py @@ -1,4 +1,5 @@ """Charge and Climate Control Support for the Nissan Leaf.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/nmap_tracker/__init__.py b/homeassistant/components/nmap_tracker/__init__.py index bab71df94dc..ffc4b975308 100644 --- a/homeassistant/components/nmap_tracker/__init__.py +++ b/homeassistant/components/nmap_tracker/__init__.py @@ -1,4 +1,5 @@ """The Nmap Tracker integration.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/nmap_tracker/config_flow.py b/homeassistant/components/nmap_tracker/config_flow.py index a1afa1b1bba..6128272fbbb 100644 --- a/homeassistant/components/nmap_tracker/config_flow.py +++ b/homeassistant/components/nmap_tracker/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Nmap Tracker integration.""" + from __future__ import annotations from ipaddress import ip_address, ip_network, summarize_address_range @@ -6,7 +7,6 @@ from typing import Any import voluptuous as vol -from homeassistant import config_entries from homeassistant.components import network from homeassistant.components.device_tracker import ( CONF_CONSIDER_HOME, @@ -14,10 +14,14 @@ from homeassistant.components.device_tracker import ( DEFAULT_CONSIDER_HOME, ) from homeassistant.components.network import MDNS_TARGET_IP -from homeassistant.config_entries import ConfigEntry, OptionsFlow +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.const import CONF_EXCLUDE, CONF_HOSTS from homeassistant.core import HomeAssistant, callback -from homeassistant.data_entry_flow import FlowResult import homeassistant.helpers.config_validation as cv from .const import ( @@ -133,16 +137,16 @@ async def _async_build_schema_with_user_input( return vol.Schema(schema) -class OptionsFlowHandler(config_entries.OptionsFlow): +class OptionsFlowHandler(OptionsFlow): """Handle a option flow for homekit.""" - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + def __init__(self, config_entry: ConfigEntry) -> None: """Initialize options flow.""" self.options = dict(config_entry.options) async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle options flow.""" errors = {} if user_input is not None: @@ -163,7 +167,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow): ) -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class NmapTrackerConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Nmap Tracker.""" VERSION = 1 @@ -174,7 +178,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors = {} if user_input is not None: diff --git a/homeassistant/components/nmap_tracker/const.py b/homeassistant/components/nmap_tracker/const.py index c7c1342036e..617f84e8aca 100644 --- a/homeassistant/components/nmap_tracker/const.py +++ b/homeassistant/components/nmap_tracker/const.py @@ -1,4 +1,5 @@ """The Nmap Tracker integration.""" + from typing import Final from homeassistant.const import Platform diff --git a/homeassistant/components/nmap_tracker/device_tracker.py b/homeassistant/components/nmap_tracker/device_tracker.py index bada45256a8..3f07926eaef 100644 --- a/homeassistant/components/nmap_tracker/device_tracker.py +++ b/homeassistant/components/nmap_tracker/device_tracker.py @@ -1,4 +1,5 @@ """Support for scanning a network with nmap.""" + from __future__ import annotations import logging @@ -46,6 +47,7 @@ class NmapTrackerEntity(ScannerEntity): """An Nmap Tracker entity.""" _attr_should_poll = False + _attr_translation_key = "device_tracker" def __init__( self, nmap_tracker: NmapDeviceScanner, mac_address: str, active: bool @@ -98,11 +100,6 @@ class NmapTrackerEntity(ScannerEntity): """Return tracker source type.""" return SourceType.ROUTER - @property - def icon(self) -> str: - """Return device icon.""" - return "mdi:lan-connect" if self._active else "mdi:lan-disconnect" - @callback def async_process_update(self, online: bool) -> None: """Update device.""" diff --git a/homeassistant/components/nmap_tracker/icons.json b/homeassistant/components/nmap_tracker/icons.json new file mode 100644 index 00000000000..02d1d17b92b --- /dev/null +++ b/homeassistant/components/nmap_tracker/icons.json @@ -0,0 +1,12 @@ +{ + "entity": { + "device_tracker": { + "device_tracker": { + "default": "mdi:lan-disconnect", + "state": { + "home": "mdi:lan-connect" + } + } + } + } +} diff --git a/homeassistant/components/nmbs/sensor.py b/homeassistant/components/nmbs/sensor.py index 7fe40af3b69..a684b47e245 100644 --- a/homeassistant/components/nmbs/sensor.py +++ b/homeassistant/components/nmbs/sensor.py @@ -1,4 +1,5 @@ """Get ride details and liveboard details for NMBS (Belgian railway).""" + from __future__ import annotations import logging diff --git a/homeassistant/components/no_ip/__init__.py b/homeassistant/components/no_ip/__init__.py index 8ab277c3def..9680464c9fa 100644 --- a/homeassistant/components/no_ip/__init__.py +++ b/homeassistant/components/no_ip/__init__.py @@ -1,4 +1,5 @@ """Integrate with NO-IP Dynamic DNS service.""" + import asyncio import base64 from datetime import datetime, timedelta @@ -103,7 +104,7 @@ async def _update_no_ip( resp = await session.get(url, params=params, headers=headers) body = await resp.text() - if body.startswith("good") or body.startswith("nochg"): + if body.startswith(("good", "nochg")): _LOGGER.debug("Updating NO-IP success: %s", domain) return True diff --git a/homeassistant/components/noaa_tides/sensor.py b/homeassistant/components/noaa_tides/sensor.py index a83f18fd6ca..235263345e6 100644 --- a/homeassistant/components/noaa_tides/sensor.py +++ b/homeassistant/components/noaa_tides/sensor.py @@ -1,4 +1,5 @@ """Support for the NOAA Tides and Currents API.""" + from __future__ import annotations from datetime import datetime, timedelta diff --git a/homeassistant/components/nobo_hub/__init__.py b/homeassistant/components/nobo_hub/__init__.py index 6c77f98d1b1..f9d2ce2e3da 100644 --- a/homeassistant/components/nobo_hub/__init__.py +++ b/homeassistant/components/nobo_hub/__init__.py @@ -1,4 +1,5 @@ """The Nobø Ecohub integration.""" + from __future__ import annotations from pynobo import nobo @@ -6,6 +7,7 @@ from pynobo import nobo from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_IP_ADDRESS, EVENT_HOMEASSISTANT_STOP, Platform from homeassistant.core import HomeAssistant +import homeassistant.util.dt as dt_util from .const import CONF_AUTO_DISCOVERED, CONF_SERIAL, DOMAIN @@ -18,7 +20,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: serial = entry.data[CONF_SERIAL] discover = entry.data[CONF_AUTO_DISCOVERED] ip_address = None if discover else entry.data[CONF_IP_ADDRESS] - hub = nobo(serial=serial, ip=ip_address, discover=discover, synchronous=False) + hub = nobo( + serial=serial, + ip=ip_address, + discover=discover, + synchronous=False, + timezone=dt_util.DEFAULT_TIME_ZONE, + ) await hub.connect() hass.data.setdefault(DOMAIN, {}) diff --git a/homeassistant/components/nobo_hub/climate.py b/homeassistant/components/nobo_hub/climate.py index ca8ee08885d..f1e2f4a78f0 100644 --- a/homeassistant/components/nobo_hub/climate.py +++ b/homeassistant/components/nobo_hub/climate.py @@ -1,4 +1,5 @@ """Python Control of Nobø Hub - Nobø Energy Control.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/nobo_hub/config_flow.py b/homeassistant/components/nobo_hub/config_flow.py index f1e2dd7d9d2..6fc5bba2c1b 100644 --- a/homeassistant/components/nobo_hub/config_flow.py +++ b/homeassistant/components/nobo_hub/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Nobø Ecohub integration.""" + from __future__ import annotations import socket @@ -7,10 +8,14 @@ from typing import Any from pynobo import nobo import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.const import CONF_IP_ADDRESS from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError from .const import ( @@ -26,7 +31,7 @@ DATA_NOBO_HUB_IMPL = "nobo_hub_flow_implementation" DEVICE_INPUT = "device_input" -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class NoboHubConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Nobø Ecohub.""" VERSION = 1 @@ -38,7 +43,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" if self._discovered_hubs is None: self._discovered_hubs = dict(await nobo.async_discover_hubs()) @@ -67,7 +72,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_selected( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle configuration of a selected discovered device.""" errors = {} if user_input is not None: @@ -97,7 +102,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_manual( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle configuration of an undiscovered device.""" errors = {} if user_input is not None: @@ -124,7 +129,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def _create_configuration( self, serial: str, ip_address: str, auto_discovered: bool - ) -> FlowResult: + ) -> ConfigFlowResult: await self.async_set_unique_id(serial) self._abort_if_unique_id_configured() name = await self._test_connection(serial, ip_address) @@ -164,8 +169,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback def async_get_options_flow( - config_entry: config_entries.ConfigEntry, - ) -> config_entries.OptionsFlow: + config_entry: ConfigEntry, + ) -> OptionsFlow: """Get the options flow for this handler.""" return OptionsFlowHandler(config_entry) @@ -179,14 +184,14 @@ class NoboHubConnectError(HomeAssistantError): self.msg = msg -class OptionsFlowHandler(config_entries.OptionsFlow): +class OptionsFlowHandler(OptionsFlow): """Handles options flow for the component.""" - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + def __init__(self, config_entry: ConfigEntry) -> None: """Initialize the options flow.""" self.config_entry = config_entry - async def async_step_init(self, user_input=None) -> FlowResult: + async def async_step_init(self, user_input=None) -> ConfigFlowResult: """Manage the options.""" if user_input is not None: diff --git a/homeassistant/components/nobo_hub/manifest.json b/homeassistant/components/nobo_hub/manifest.json index 9ddbed7dadc..4741eb39e29 100644 --- a/homeassistant/components/nobo_hub/manifest.json +++ b/homeassistant/components/nobo_hub/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/nobo_hub", "integration_type": "hub", "iot_class": "local_push", - "requirements": ["pynobo==1.6.0"] + "requirements": ["pynobo==1.8.0"] } diff --git a/homeassistant/components/nobo_hub/select.py b/homeassistant/components/nobo_hub/select.py index 2708dd75ffe..43f177dd7a0 100644 --- a/homeassistant/components/nobo_hub/select.py +++ b/homeassistant/components/nobo_hub/select.py @@ -1,4 +1,5 @@ """Python Control of Nobø Hub - Nobø Energy Control.""" + from __future__ import annotations from pynobo import nobo diff --git a/homeassistant/components/nobo_hub/sensor.py b/homeassistant/components/nobo_hub/sensor.py index 3446f1ea43b..1632b6ba5e7 100644 --- a/homeassistant/components/nobo_hub/sensor.py +++ b/homeassistant/components/nobo_hub/sensor.py @@ -1,4 +1,5 @@ """Python Control of Nobø Hub - Nobø Energy Control.""" + from __future__ import annotations from pynobo import nobo diff --git a/homeassistant/components/norway_air/air_quality.py b/homeassistant/components/norway_air/air_quality.py index 1a3d3661a15..c16df860751 100644 --- a/homeassistant/components/norway_air/air_quality.py +++ b/homeassistant/components/norway_air/air_quality.py @@ -1,4 +1,5 @@ """Sensor for checking the air quality forecast around Norway.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/norway_air/manifest.json b/homeassistant/components/norway_air/manifest.json index 84af1313cf5..f787f647db8 100644 --- a/homeassistant/components/norway_air/manifest.json +++ b/homeassistant/components/norway_air/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/norway_air", "iot_class": "cloud_polling", "loggers": ["metno"], - "requirements": ["PyMetno==0.11.0"] + "requirements": ["PyMetno==0.12.0"] } diff --git a/homeassistant/components/notify/__init__.py b/homeassistant/components/notify/__init__.py index e9e61527884..e7390a49676 100644 --- a/homeassistant/components/notify/__init__.py +++ b/homeassistant/components/notify/__init__.py @@ -1,7 +1,6 @@ """Provides functionality to notify people.""" -from __future__ import annotations -import asyncio +from __future__ import annotations import voluptuous as vol @@ -42,19 +41,14 @@ PLATFORM_SCHEMA = vol.Schema( async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the notify services.""" - platform_setups = async_setup_legacy(hass, config) - - # 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, but we need to do it after - # the dispatcher is connected so we don't miss integrations - # that are registered before the dispatcher is connected - hass.config.components.add(DOMAIN) - - if platform_setups: - await asyncio.wait([asyncio.create_task(setup) for setup in platform_setups]) + for setup in async_setup_legacy(hass, config): + # Tasks are created as tracked tasks to ensure startup + # waits for them to finish, but we explicitly do not + # want to wait for them to finish here because we want + # any config entries that use notify as a base platform + # to be able to start with out having to wait for the + # legacy platforms to finish setting up. + hass.async_create_task(setup, eager_start=True) async def persistent_notification(service: ServiceCall) -> None: """Send notification via the built-in persistent_notify integration.""" diff --git a/homeassistant/components/notify/const.py b/homeassistant/components/notify/const.py index 38dba680635..b653b5d1cbf 100644 --- a/homeassistant/components/notify/const.py +++ b/homeassistant/components/notify/const.py @@ -1,4 +1,5 @@ """Provide common notify constants.""" + import logging import voluptuous as vol diff --git a/homeassistant/components/notify/icons.json b/homeassistant/components/notify/icons.json new file mode 100644 index 00000000000..88577bc2356 --- /dev/null +++ b/homeassistant/components/notify/icons.json @@ -0,0 +1,6 @@ +{ + "services": { + "notify": "mdi:bell-ring", + "persistent_notification": "mdi:bell-badge" + } +} diff --git a/homeassistant/components/notify/legacy.py b/homeassistant/components/notify/legacy.py index 7c78bfc44d3..2f6984e36f1 100644 --- a/homeassistant/components/notify/legacy.py +++ b/homeassistant/components/notify/legacy.py @@ -1,4 +1,5 @@ """Handle legacy notification platforms.""" + from __future__ import annotations import asyncio @@ -15,7 +16,11 @@ from homeassistant.helpers.service import async_set_service_schema from homeassistant.helpers.template import Template from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.loader import async_get_integration, bind_hass -from homeassistant.setup import async_prepare_setup_platform, async_start_setup +from homeassistant.setup import ( + SetupPhases, + async_prepare_setup_platform, + async_start_setup, +) from homeassistant.util import slugify from homeassistant.util.yaml import load_yaml_dict @@ -83,7 +88,12 @@ def async_setup_legacy( full_name = f"{DOMAIN}.{integration_name}" LOGGER.info("Setting up %s", full_name) - with async_start_setup(hass, [full_name]): + with async_start_setup( + hass, + integration=integration_name, + group=str(id(p_config)), + phase=SetupPhases.PLATFORM_SETUP, + ): notify_service: BaseNotificationService | None = None try: if hasattr(platform, "async_get_service"): @@ -231,7 +241,7 @@ class BaseNotificationService: kwargs can contain ATTR_TITLE to specify a title. """ - raise NotImplementedError() + raise NotImplementedError async def async_send_message(self, message: str, **kwargs: Any) -> None: """Send a message. diff --git a/homeassistant/components/notify_events/__init__.py b/homeassistant/components/notify_events/__init__.py index 12efa693b19..2be97d709a9 100644 --- a/homeassistant/components/notify_events/__init__.py +++ b/homeassistant/components/notify_events/__init__.py @@ -1,4 +1,5 @@ """The notify_events component.""" + import voluptuous as vol from homeassistant.const import CONF_TOKEN, Platform diff --git a/homeassistant/components/notify_events/notify.py b/homeassistant/components/notify_events/notify.py index 3eb1bbac9c9..bfe0e4a2a57 100644 --- a/homeassistant/components/notify_events/notify.py +++ b/homeassistant/components/notify_events/notify.py @@ -1,4 +1,5 @@ """Notify.Events platform for notify component.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/notion/__init__.py b/homeassistant/components/notion/__init__.py index 6d2b6c48e1b..ca45e3a6d16 100644 --- a/homeassistant/components/notion/__init__.py +++ b/homeassistant/components/notion/__init__.py @@ -1,8 +1,7 @@ """Support for Notion.""" + from __future__ import annotations -import asyncio -from dataclasses import dataclass, field from datetime import timedelta from typing import Any from uuid import UUID @@ -10,8 +9,6 @@ from uuid import UUID from aionotion.bridge.models import Bridge from aionotion.errors import InvalidCredentialsError, NotionError from aionotion.listener.models import Listener, ListenerKind -from aionotion.sensor.models import Sensor -from aionotion.user.models import UserPreferences from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform @@ -24,11 +21,7 @@ from homeassistant.helpers import ( ) from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity import EntityDescription -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, - UpdateFailed, -) +from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( CONF_REFRESH_TOKEN, @@ -46,6 +39,7 @@ from .const import ( SENSOR_TEMPERATURE, SENSOR_WINDOW_HINGED, ) +from .coordinator import NotionDataUpdateCoordinator from .util import async_get_client_with_credentials, async_get_client_with_refresh_token PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] @@ -53,11 +47,6 @@ PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] ATTR_SYSTEM_MODE = "system_mode" ATTR_SYSTEM_NAME = "system_name" -DATA_BRIDGES = "bridges" -DATA_LISTENERS = "listeners" -DATA_SENSORS = "sensors" -DATA_USER_PREFERENCES = "user_preferences" - DEFAULT_SCAN_INTERVAL = timedelta(minutes=1) CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) @@ -87,57 +76,6 @@ def is_uuid(value: str) -> bool: return True -@dataclass -class NotionData: - """Define a manager class for Notion data.""" - - hass: HomeAssistant - entry: ConfigEntry - - # Define a dict of bridges, indexed by bridge ID (an integer): - bridges: dict[int, Bridge] = field(default_factory=dict) - - # Define a dict of listeners, indexed by listener UUID (a string): - listeners: dict[str, Listener] = field(default_factory=dict) - - # Define a dict of sensors, indexed by sensor UUID (a string): - sensors: dict[str, Sensor] = field(default_factory=dict) - - # Define a user preferences response object: - user_preferences: UserPreferences | None = field(default=None) - - def update_bridges(self, bridges: list[Bridge]) -> None: - """Update the bridges.""" - for bridge in bridges: - # If a new bridge is discovered, register it: - if bridge.id not in self.bridges: - _async_register_new_bridge(self.hass, self.entry, bridge) - self.bridges[bridge.id] = bridge - - def update_listeners(self, listeners: list[Listener]) -> None: - """Update the listeners.""" - self.listeners = {listener.id: listener for listener in listeners} - - def update_sensors(self, sensors: list[Sensor]) -> None: - """Update the sensors.""" - self.sensors = {sensor.uuid: sensor for sensor in sensors} - - def update_user_preferences(self, user_preferences: UserPreferences) -> None: - """Update the user preferences.""" - self.user_preferences = user_preferences - - def asdict(self) -> dict[str, Any]: - """Represent this dataclass (and its Pydantic contents) as a dict.""" - data: dict[str, Any] = { - DATA_BRIDGES: [item.to_dict() for item in self.bridges.values()], - DATA_LISTENERS: [item.to_dict() for item in self.listeners.values()], - DATA_SENSORS: [item.to_dict() for item in self.sensors.values()], - } - if self.user_preferences: - data[DATA_USER_PREFERENCES] = self.user_preferences.to_dict() - return data - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Notion as a config entry.""" entry_updates: dict[str, Any] = {"data": {**entry.data}} @@ -187,51 +125,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Create a callback to save the refresh token when it changes: entry.async_on_unload(client.add_refresh_token_callback(async_save_refresh_token)) - async def async_update() -> NotionData: - """Get the latest data from the Notion API.""" - data = NotionData(hass=hass, entry=entry) - - try: - async with asyncio.TaskGroup() as tg: - bridges = tg.create_task(client.bridge.async_all()) - listeners = tg.create_task(client.listener.async_all()) - sensors = tg.create_task(client.sensor.async_all()) - user_preferences = tg.create_task(client.user.async_preferences()) - except BaseExceptionGroup as err: - result = err.exceptions[0] - if isinstance(result, InvalidCredentialsError): - raise ConfigEntryAuthFailed( - "Invalid username and/or password" - ) from result - if isinstance(result, NotionError): - raise UpdateFailed( - f"There was a Notion error while updating: {result}" - ) from result - if isinstance(result, Exception): - LOGGER.debug( - "There was an unknown error while updating: %s", - result, - exc_info=result, - ) - raise UpdateFailed( - f"There was an unknown error while updating: {result}" - ) from result - if isinstance(result, BaseException): - raise result from None - - data.update_bridges(bridges.result()) - data.update_listeners(listeners.result()) - data.update_sensors(sensors.result()) - data.update_user_preferences(user_preferences.result()) - return data - - coordinator = DataUpdateCoordinator( - hass, - LOGGER, - name=entry.data[CONF_USERNAME], - update_interval=DEFAULT_SCAN_INTERVAL, - update_method=async_update, - ) + coordinator = NotionDataUpdateCoordinator(hass, entry=entry, client=client) await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {}) @@ -281,39 +175,17 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return unload_ok -@callback -def _async_register_new_bridge( - hass: HomeAssistant, entry: ConfigEntry, bridge: Bridge -) -> None: - """Register a new bridge.""" - if name := bridge.name: - bridge_name = name.capitalize() - else: - bridge_name = str(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=str(bridge.hardware_revision), - name=bridge_name, - sw_version=bridge.firmware_version.wifi, - ) - - -class NotionEntity(CoordinatorEntity[DataUpdateCoordinator[NotionData]]): +class NotionEntity(CoordinatorEntity[NotionDataUpdateCoordinator]): """Define a base Notion entity.""" _attr_has_entity_name = True def __init__( self, - coordinator: DataUpdateCoordinator[NotionData], + coordinator: NotionDataUpdateCoordinator, listener_id: str, sensor_id: str, bridge_id: int, - system_id: str, description: EntityDescription, ) -> None: """Initialize the entity.""" @@ -337,7 +209,6 @@ class NotionEntity(CoordinatorEntity[DataUpdateCoordinator[NotionData]]): self._bridge_id = bridge_id self._listener_id = listener_id self._sensor_id = sensor_id - self._system_id = system_id self.entity_description = description @property diff --git a/homeassistant/components/notion/binary_sensor.py b/homeassistant/components/notion/binary_sensor.py index dfa6dc5ec06..da50a809689 100644 --- a/homeassistant/components/notion/binary_sensor.py +++ b/homeassistant/components/notion/binary_sensor.py @@ -1,4 +1,5 @@ """Support for Notion binary sensors.""" + from __future__ import annotations from dataclasses import dataclass @@ -30,6 +31,7 @@ from .const import ( SENSOR_SMOKE_CO, SENSOR_WINDOW_HINGED, ) +from .coordinator import NotionDataUpdateCoordinator from .model import NotionEntityDescription @@ -109,7 +111,7 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up Notion sensors based on a config entry.""" - coordinator = hass.data[DOMAIN][entry.entry_id] + coordinator: NotionDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] async_add_entities( [ @@ -118,7 +120,6 @@ async def async_setup_entry( listener_id, sensor.uuid, sensor.bridge.id, - sensor.system_id, description, ) for listener_id, listener in coordinator.data.listeners.items() diff --git a/homeassistant/components/notion/config_flow.py b/homeassistant/components/notion/config_flow.py index f43c87b5085..9a65f922fd9 100644 --- a/homeassistant/components/notion/config_flow.py +++ b/homeassistant/components/notion/config_flow.py @@ -1,4 +1,5 @@ """Config flow to configure the Notion integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -8,11 +9,9 @@ from typing import Any from aionotion.errors import InvalidCredentialsError, NotionError import voluptuous as vol -from homeassistant import config_entries -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResult from .const import CONF_REFRESH_TOKEN, CONF_USER_UUID, DOMAIN, LOGGER from .util import async_get_client_with_credentials @@ -64,7 +63,7 @@ async def async_validate_credentials( ) -class NotionFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class NotionFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a Notion config flow.""" VERSION = 1 @@ -73,7 +72,9 @@ class NotionFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Initialize.""" self._reauth_entry: ConfigEntry | None = None - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Handle configuration by re-auth.""" self._reauth_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] @@ -82,7 +83,7 @@ class NotionFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle re-auth completion.""" assert self._reauth_entry @@ -121,7 +122,7 @@ class NotionFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, str] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the start of the config flow.""" if not user_input: return self.async_show_form(step_id="user", data_schema=AUTH_SCHEMA) diff --git a/homeassistant/components/notion/const.py b/homeassistant/components/notion/const.py index b1ea921a71b..590431d1a59 100644 --- a/homeassistant/components/notion/const.py +++ b/homeassistant/components/notion/const.py @@ -1,4 +1,5 @@ """Define constants for the Notion integration.""" + import logging DOMAIN = "notion" diff --git a/homeassistant/components/notion/coordinator.py b/homeassistant/components/notion/coordinator.py new file mode 100644 index 00000000000..c3fd23abc84 --- /dev/null +++ b/homeassistant/components/notion/coordinator.py @@ -0,0 +1,164 @@ +"""Define a Notion data coordinator.""" + +import asyncio +from dataclasses import dataclass, field +from datetime import timedelta +from typing import Any + +from aionotion.bridge.models import Bridge +from aionotion.client import Client +from aionotion.errors import InvalidCredentialsError, NotionError +from aionotion.listener.models import Listener +from aionotion.sensor.models import Sensor +from aionotion.user.models import UserPreferences + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_USERNAME +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import ConfigEntryAuthFailed +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DOMAIN, LOGGER + +DATA_BRIDGES = "bridges" +DATA_LISTENERS = "listeners" +DATA_SENSORS = "sensors" +DATA_USER_PREFERENCES = "user_preferences" + +DEFAULT_SCAN_INTERVAL = timedelta(minutes=1) + + +@callback +def _async_register_new_bridge( + hass: HomeAssistant, entry: ConfigEntry, bridge: Bridge +) -> None: + """Register a new bridge.""" + if name := bridge.name: + bridge_name = name.capitalize() + else: + bridge_name = str(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=str(bridge.hardware_revision), + name=bridge_name, + sw_version=bridge.firmware_version.wifi, + ) + + +@dataclass +class NotionData: + """Define a manager class for Notion data.""" + + hass: HomeAssistant + entry: ConfigEntry + + # Define a dict of bridges, indexed by bridge ID (an integer): + bridges: dict[int, Bridge] = field(default_factory=dict) + + # Define a dict of listeners, indexed by listener UUID (a string): + listeners: dict[str, Listener] = field(default_factory=dict) + + # Define a dict of sensors, indexed by sensor UUID (a string): + sensors: dict[str, Sensor] = field(default_factory=dict) + + # Define a user preferences response object: + user_preferences: UserPreferences | None = field(default=None) + + def update_bridges(self, bridges: list[Bridge]) -> None: + """Update the bridges.""" + for bridge in bridges: + # If a new bridge is discovered, register it: + if bridge.id not in self.bridges: + _async_register_new_bridge(self.hass, self.entry, bridge) + self.bridges[bridge.id] = bridge + + def update_listeners(self, listeners: list[Listener]) -> None: + """Update the listeners.""" + self.listeners = {listener.id: listener for listener in listeners} + + def update_sensors(self, sensors: list[Sensor]) -> None: + """Update the sensors.""" + self.sensors = {sensor.uuid: sensor for sensor in sensors} + + def update_user_preferences(self, user_preferences: UserPreferences) -> None: + """Update the user preferences.""" + self.user_preferences = user_preferences + + def asdict(self) -> dict[str, Any]: + """Represent this dataclass (and its Pydantic contents) as a dict.""" + data: dict[str, Any] = { + DATA_BRIDGES: [item.to_dict() for item in self.bridges.values()], + DATA_LISTENERS: [item.to_dict() for item in self.listeners.values()], + DATA_SENSORS: [item.to_dict() for item in self.sensors.values()], + } + if self.user_preferences: + data[DATA_USER_PREFERENCES] = self.user_preferences.to_dict() + return data + + +class NotionDataUpdateCoordinator(DataUpdateCoordinator[NotionData]): + """Define a Notion data coordinator.""" + + config_entry: ConfigEntry + + def __init__( + self, + hass: HomeAssistant, + *, + entry: ConfigEntry, + client: Client, + ) -> None: + """Initialize.""" + super().__init__( + hass, + LOGGER, + name=entry.data[CONF_USERNAME], + update_interval=DEFAULT_SCAN_INTERVAL, + ) + + self._client = client + self._entry = entry + + async def _async_update_data(self) -> NotionData: + """Fetch data from Notion.""" + data = NotionData(hass=self.hass, entry=self._entry) + + try: + async with asyncio.TaskGroup() as tg: + bridges = tg.create_task(self._client.bridge.async_all()) + listeners = tg.create_task(self._client.listener.async_all()) + sensors = tg.create_task(self._client.sensor.async_all()) + user_preferences = tg.create_task(self._client.user.async_preferences()) + except BaseExceptionGroup as err: + result = err.exceptions[0] + if isinstance(result, InvalidCredentialsError): + raise ConfigEntryAuthFailed( + "Invalid username and/or password" + ) from result + if isinstance(result, NotionError): + raise UpdateFailed( + f"There was a Notion error while updating: {result}" + ) from result + if isinstance(result, Exception): + LOGGER.debug( + "There was an unknown error while updating: %s", + result, + exc_info=result, + ) + raise UpdateFailed( + f"There was an unknown error while updating: {result}" + ) from result + if isinstance(result, BaseException): + raise result from None + + data.update_bridges(bridges.result()) + data.update_listeners(listeners.result()) + data.update_sensors(sensors.result()) + data.update_user_preferences(user_preferences.result()) + + return data diff --git a/homeassistant/components/notion/diagnostics.py b/homeassistant/components/notion/diagnostics.py index 5c32f235639..424e5f7d0ac 100644 --- a/homeassistant/components/notion/diagnostics.py +++ b/homeassistant/components/notion/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for Notion.""" + from __future__ import annotations from typing import Any @@ -7,10 +8,9 @@ from homeassistant.components.diagnostics import async_redact_data from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_EMAIL, CONF_UNIQUE_ID, CONF_USERNAME from homeassistant.core import HomeAssistant -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from . import NotionData from .const import CONF_REFRESH_TOKEN, CONF_USER_UUID, DOMAIN +from .coordinator import NotionDataUpdateCoordinator CONF_DEVICE_KEY = "device_key" CONF_HARDWARE_ID = "hardware_id" @@ -37,7 +37,7 @@ async def async_get_config_entry_diagnostics( hass: HomeAssistant, entry: ConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - coordinator: DataUpdateCoordinator[NotionData] = hass.data[DOMAIN][entry.entry_id] + coordinator: NotionDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] return async_redact_data( { diff --git a/homeassistant/components/notion/icons.json b/homeassistant/components/notion/icons.json new file mode 100644 index 00000000000..63ea6ad2c18 --- /dev/null +++ b/homeassistant/components/notion/icons.json @@ -0,0 +1,9 @@ +{ + "entity": { + "sensor": { + "mold_risk": { + "default": "mdi:liquid-spot" + } + } + } +} diff --git a/homeassistant/components/notion/model.py b/homeassistant/components/notion/model.py index 059ea551b09..541ca245329 100644 --- a/homeassistant/components/notion/model.py +++ b/homeassistant/components/notion/model.py @@ -1,4 +1,5 @@ """Define Notion model mixins.""" + from dataclasses import dataclass from aionotion.listener.models import ListenerKind diff --git a/homeassistant/components/notion/sensor.py b/homeassistant/components/notion/sensor.py index f5439895ac9..d12dabbbc33 100644 --- a/homeassistant/components/notion/sensor.py +++ b/homeassistant/components/notion/sensor.py @@ -1,4 +1,5 @@ """Support for Notion sensors.""" + from dataclasses import dataclass from aionotion.listener.models import ListenerKind @@ -16,6 +17,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import NotionEntity from .const import DOMAIN, SENSOR_MOLD, SENSOR_TEMPERATURE +from .coordinator import NotionDataUpdateCoordinator from .model import NotionEntityDescription @@ -28,7 +30,6 @@ SENSOR_DESCRIPTIONS = ( NotionSensorDescription( key=SENSOR_MOLD, translation_key="mold_risk", - icon="mdi:liquid-spot", listener_kind=ListenerKind.MOLD, ), NotionSensorDescription( @@ -45,7 +46,7 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up Notion sensors based on a config entry.""" - coordinator = hass.data[DOMAIN][entry.entry_id] + coordinator: NotionDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] async_add_entities( [ @@ -54,7 +55,6 @@ async def async_setup_entry( listener_id, sensor.uuid, sensor.bridge.id, - sensor.system_id, description, ) for listener_id, listener in coordinator.data.listeners.items() diff --git a/homeassistant/components/notion/util.py b/homeassistant/components/notion/util.py index 553199b7c7a..b155249268a 100644 --- a/homeassistant/components/notion/util.py +++ b/homeassistant/components/notion/util.py @@ -1,4 +1,5 @@ """Define notion utilities.""" + from aionotion import ( async_get_client_with_credentials as cwc, async_get_client_with_refresh_token as cwrt, diff --git a/homeassistant/components/nsw_fuel_station/__init__.py b/homeassistant/components/nsw_fuel_station/__init__.py index 818656779a3..76dc9d4c6ff 100644 --- a/homeassistant/components/nsw_fuel_station/__init__.py +++ b/homeassistant/components/nsw_fuel_station/__init__.py @@ -1,4 +1,5 @@ """The nsw_fuel_station component.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/homeassistant/components/nsw_fuel_station/sensor.py b/homeassistant/components/nsw_fuel_station/sensor.py index 7106b487786..7f28a9d28f2 100644 --- a/homeassistant/components/nsw_fuel_station/sensor.py +++ b/homeassistant/components/nsw_fuel_station/sensor.py @@ -1,4 +1,5 @@ """Sensor platform to display the current fuel prices at a NSW fuel station.""" + from __future__ import annotations import logging 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 28e056e29fb..24bae7f7b12 100644 --- a/homeassistant/components/nsw_rural_fire_service_feed/geo_location.py +++ b/homeassistant/components/nsw_rural_fire_service_feed/geo_location.py @@ -1,4 +1,5 @@ """Support for NSW Rural Fire Service Feeds.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/nuheat/__init__.py b/homeassistant/components/nuheat/__init__.py index 86574920cd0..c8accd6ab73 100644 --- a/homeassistant/components/nuheat/__init__.py +++ b/homeassistant/components/nuheat/__init__.py @@ -1,4 +1,5 @@ """Support for NuHeat thermostats.""" + from datetime import timedelta from http import HTTPStatus import logging diff --git a/homeassistant/components/nuheat/climate.py b/homeassistant/components/nuheat/climate.py index b2ebbfa8485..db85827fc9b 100644 --- a/homeassistant/components/nuheat/climate.py +++ b/homeassistant/components/nuheat/climate.py @@ -1,4 +1,5 @@ """Support for NuHeat thermostats.""" + import logging from typing import Any diff --git a/homeassistant/components/nuheat/config_flow.py b/homeassistant/components/nuheat/config_flow.py index 0959466244d..a75b65abccd 100644 --- a/homeassistant/components/nuheat/config_flow.py +++ b/homeassistant/components/nuheat/config_flow.py @@ -1,4 +1,5 @@ """Config flow for NuHeat integration.""" + from http import HTTPStatus import logging @@ -6,8 +7,10 @@ import nuheat import requests.exceptions import voluptuous as vol -from homeassistant import config_entries, core, exceptions +from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from .const import CONF_SERIAL_NUMBER, DOMAIN @@ -22,7 +25,7 @@ DATA_SCHEMA = vol.Schema( ) -async def validate_input(hass: core.HomeAssistant, data): +async def validate_input(hass: HomeAssistant, data): """Validate the user input allows us to connect. Data has the keys from DATA_SCHEMA with values provided by the user. @@ -56,7 +59,7 @@ async def validate_input(hass: core.HomeAssistant, data): return {"title": thermostat.room, "serial_number": thermostat.serial_number} -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class NuHeatConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for NuHeat.""" VERSION = 1 @@ -87,13 +90,13 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -class CannotConnect(exceptions.HomeAssistantError): +class CannotConnect(HomeAssistantError): """Error to indicate we cannot connect.""" -class InvalidAuth(exceptions.HomeAssistantError): +class InvalidAuth(HomeAssistantError): """Error to indicate there is invalid auth.""" -class InvalidThermostat(exceptions.HomeAssistantError): +class InvalidThermostat(HomeAssistantError): """Error to indicate there is invalid thermostat.""" diff --git a/homeassistant/components/nuheat/const.py b/homeassistant/components/nuheat/const.py index ea43c33d9b0..96e7cdc2d60 100644 --- a/homeassistant/components/nuheat/const.py +++ b/homeassistant/components/nuheat/const.py @@ -1,4 +1,5 @@ """Constants for NuHeat thermostats.""" + from homeassistant.const import Platform DOMAIN = "nuheat" diff --git a/homeassistant/components/nuki/__init__.py b/homeassistant/components/nuki/__init__.py index 0ea75590ee3..cbd7af3ecec 100644 --- a/homeassistant/components/nuki/__init__.py +++ b/homeassistant/components/nuki/__init__.py @@ -1,4 +1,5 @@ """The nuki component.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/nuki/binary_sensor.py b/homeassistant/components/nuki/binary_sensor.py index c01c1c50237..f2a98599e27 100644 --- a/homeassistant/components/nuki/binary_sensor.py +++ b/homeassistant/components/nuki/binary_sensor.py @@ -1,4 +1,5 @@ """Doorsensor Support for the Nuki Lock.""" + from __future__ import annotations from pynuki.constants import STATE_DOORSENSOR_OPENED @@ -50,6 +51,7 @@ class NukiDoorsensorEntity(NukiEntity[NukiDevice], BinarySensorEntity): """Return a unique ID.""" return f"{self._nuki_device.nuki_id}_doorsensor" + # Deprecated, can be removed in 2024.10 @property def extra_state_attributes(self): """Return the device specific state attributes.""" @@ -90,6 +92,7 @@ class NukiRingactionEntity(NukiEntity[NukiDevice], BinarySensorEntity): """Return a unique ID.""" return f"{self._nuki_device.nuki_id}_ringaction" + # Deprecated, can be removed in 2024.10 @property def extra_state_attributes(self): """Return the device specific state attributes.""" diff --git a/homeassistant/components/nuki/config_flow.py b/homeassistant/components/nuki/config_flow.py index 4acfecf492b..4a3e96f68a5 100644 --- a/homeassistant/components/nuki/config_flow.py +++ b/homeassistant/components/nuki/config_flow.py @@ -1,4 +1,5 @@ """Config flow to configure the Nuki integration.""" + from collections.abc import Mapping import logging from typing import Any @@ -8,10 +9,9 @@ from pynuki.bridge import InvalidCredentialsException from requests.exceptions import RequestException import voluptuous as vol -from homeassistant import config_entries from homeassistant.components import dhcp +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TOKEN -from homeassistant.data_entry_flow import FlowResult from .const import CONF_ENCRYPT_TOKEN, DEFAULT_PORT, DEFAULT_TIMEOUT, DOMAIN from .helpers import CannotConnect, InvalidAuth, parse_id @@ -59,7 +59,7 @@ async def validate_input(hass, data): return info -class NukiConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class NukiConfigFlow(ConfigFlow, domain=DOMAIN): """Nuki config flow.""" def __init__(self): @@ -71,7 +71,9 @@ class NukiConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a flow initiated by the user.""" return await self.async_step_validate(user_input) - async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: + async def async_step_dhcp( + self, discovery_info: dhcp.DhcpServiceInfo + ) -> ConfigFlowResult: """Prepare configuration for a DHCP discovered Nuki bridge.""" await self.async_set_unique_id(discovery_info.hostname[12:].upper()) @@ -87,7 +89,9 @@ class NukiConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_validate() - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Perform reauth upon an API authentication error.""" self._data = entry_data diff --git a/homeassistant/components/nuki/const.py b/homeassistant/components/nuki/const.py index 21a2dcf9e5b..28975f37432 100644 --- a/homeassistant/components/nuki/const.py +++ b/homeassistant/components/nuki/const.py @@ -1,4 +1,5 @@ """Constants for Nuki.""" + DOMAIN = "nuki" # Attributes diff --git a/homeassistant/components/nuki/helpers.py b/homeassistant/components/nuki/helpers.py index 1ba8e393f54..9ea4621168c 100644 --- a/homeassistant/components/nuki/helpers.py +++ b/homeassistant/components/nuki/helpers.py @@ -1,4 +1,5 @@ """nuki integration helpers.""" + from homeassistant import exceptions diff --git a/homeassistant/components/nuki/lock.py b/homeassistant/components/nuki/lock.py index f1e553e6668..d63bfaf6757 100644 --- a/homeassistant/components/nuki/lock.py +++ b/homeassistant/components/nuki/lock.py @@ -1,4 +1,5 @@ """Nuki.io lock platform.""" + from __future__ import annotations from abc import abstractmethod @@ -76,6 +77,7 @@ class NukiDeviceEntity(NukiEntity[_NukiDeviceT], LockEntity): """Return a unique ID.""" return self._nuki_device.nuki_id + # Deprecated, can be removed in 2024.10 @property def extra_state_attributes(self) -> dict[str, Any]: """Return the device specific state attributes.""" diff --git a/homeassistant/components/nuki/sensor.py b/homeassistant/components/nuki/sensor.py index 3c6775cd171..6647eff5c83 100644 --- a/homeassistant/components/nuki/sensor.py +++ b/homeassistant/components/nuki/sensor.py @@ -1,4 +1,5 @@ """Battery sensor for the Nuki Lock.""" + from __future__ import annotations from pynuki.device import NukiDevice @@ -37,6 +38,7 @@ class NukiBatterySensor(NukiEntity[NukiDevice], SensorEntity): """Return a unique ID.""" return f"{self._nuki_device.nuki_id}_battery_level" + # Deprecated, can be removed in 2024.10 @property def extra_state_attributes(self): """Return the device specific state attributes.""" diff --git a/homeassistant/components/numato/__init__.py b/homeassistant/components/numato/__init__.py index 7a66ac55d70..978264d867e 100644 --- a/homeassistant/components/numato/__init__.py +++ b/homeassistant/components/numato/__init__.py @@ -1,4 +1,5 @@ """Support for controlling GPIO pins of a Numato Labs USB GPIO expander.""" + import logging import numato_gpio as gpio diff --git a/homeassistant/components/numato/binary_sensor.py b/homeassistant/components/numato/binary_sensor.py index 98e71b9fc2d..1f664a372ba 100644 --- a/homeassistant/components/numato/binary_sensor.py +++ b/homeassistant/components/numato/binary_sensor.py @@ -1,4 +1,5 @@ """Binary sensor platform integration for Numato USB GPIO expanders.""" + from __future__ import annotations from functools import partial @@ -53,7 +54,6 @@ def setup_platform( for port, port_name in ports.items(): try: api.setup_input(device_id, port) - api.edge_detect(device_id, port, partial(read_gpio, device_id)) except NumatoGpioError as err: _LOGGER.error( @@ -67,7 +67,17 @@ def setup_platform( err, ) continue + try: + api.edge_detect(device_id, port, partial(read_gpio, device_id)) + except NumatoGpioError as err: + _LOGGER.info( + "Notification setup failed on device %s, " + "updates on binary sensor %s only in polling mode: %s", + device_id, + port_name, + err, + ) binary_sensors.append( NumatoGpioBinarySensor( port_name, diff --git a/homeassistant/components/numato/manifest.json b/homeassistant/components/numato/manifest.json index e6efcea5315..f7bcf0527c2 100644 --- a/homeassistant/components/numato/manifest.json +++ b/homeassistant/components/numato/manifest.json @@ -6,5 +6,5 @@ "integration_type": "hub", "iot_class": "local_push", "loggers": ["numato_gpio"], - "requirements": ["numato-gpio==0.12.0"] + "requirements": ["numato-gpio==0.13.0"] } diff --git a/homeassistant/components/numato/sensor.py b/homeassistant/components/numato/sensor.py index 44adb78e6a0..6efc3f6160f 100644 --- a/homeassistant/components/numato/sensor.py +++ b/homeassistant/components/numato/sensor.py @@ -1,4 +1,5 @@ """Sensor platform integration for ADC ports of Numato USB GPIO expanders.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/numato/switch.py b/homeassistant/components/numato/switch.py index 92fc7e0e2df..37d1229e0b2 100644 --- a/homeassistant/components/numato/switch.py +++ b/homeassistant/components/numato/switch.py @@ -1,4 +1,5 @@ """Switch platform integration for Numato USB GPIO expanders.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/number/__init__.py b/homeassistant/components/number/__init__.py index c95381d09c2..8c55bbc2cba 100644 --- a/homeassistant/components/number/__init__.py +++ b/homeassistant/components/number/__init__.py @@ -1,4 +1,5 @@ """Component to allow numeric input for platforms.""" + from __future__ import annotations from collections.abc import Callable @@ -393,7 +394,7 @@ class NumberEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): def set_native_value(self, value: float) -> None: """Set new value.""" - raise NotImplementedError() + raise NotImplementedError async def async_set_native_value(self, value: float) -> None: """Set new value.""" @@ -402,7 +403,7 @@ class NumberEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): @final def set_value(self, value: float) -> None: """Set new value.""" - raise NotImplementedError() + raise NotImplementedError @final async def async_set_value(self, value: float) -> None: diff --git a/homeassistant/components/number/const.py b/homeassistant/components/number/const.py index 071f480f766..89829adcc50 100644 --- a/homeassistant/components/number/const.py +++ b/homeassistant/components/number/const.py @@ -1,4 +1,5 @@ """Provides the constants needed for the component.""" + from __future__ import annotations from enum import StrEnum diff --git a/homeassistant/components/number/device_action.py b/homeassistant/components/number/device_action.py index 22a51d85ad9..8882bb22a0d 100644 --- a/homeassistant/components/number/device_action.py +++ b/homeassistant/components/number/device_action.py @@ -1,4 +1,5 @@ """Provides device actions for Number.""" + from __future__ import annotations import voluptuous as vol diff --git a/homeassistant/components/number/reproduce_state.py b/homeassistant/components/number/reproduce_state.py index caed64a3d9e..e92573fb40e 100644 --- a/homeassistant/components/number/reproduce_state.py +++ b/homeassistant/components/number/reproduce_state.py @@ -1,4 +1,5 @@ """Reproduce a Number entity state.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/number/significant_change.py b/homeassistant/components/number/significant_change.py index 11bca6457f1..ff77f25e527 100644 --- a/homeassistant/components/number/significant_change.py +++ b/homeassistant/components/number/significant_change.py @@ -1,4 +1,5 @@ """Helper to test significant Number state changes.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/number/websocket_api.py b/homeassistant/components/number/websocket_api.py index 1ca61fd158f..5c8730c9eaa 100644 --- a/homeassistant/components/number/websocket_api.py +++ b/homeassistant/components/number/websocket_api.py @@ -1,4 +1,5 @@ """The sensor websocket API.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/nut/__init__.py b/homeassistant/components/nut/__init__.py index 8b0d8fe4640..575def8bf0f 100644 --- a/homeassistant/components/nut/__init__.py +++ b/homeassistant/components/nut/__init__.py @@ -1,13 +1,13 @@ """The nut component.""" + from __future__ import annotations -import asyncio from dataclasses import dataclass from datetime import timedelta import logging -from typing import cast +from typing import TYPE_CHECKING -from pynut2.nut2 import PyNUTClient, PyNUTError +from aionut import AIONUTClient, NUTError, NUTLoginError from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -18,9 +18,10 @@ from homeassistant.const import ( CONF_RESOURCES, CONF_SCAN_INTERVAL, CONF_USERNAME, + EVENT_HOMEASSISTANT_STOP, ) -from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError +from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.exceptions import ConfigEntryAuthFailed, HomeAssistantError from homeassistant.helpers import device_registry as dr from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -63,13 +64,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: data = PyNUTData(host, port, alias, username, password) + entry.async_on_unload(data.async_shutdown) + async def async_update_data() -> dict[str, str]: """Fetch data from NUT.""" - async with asyncio.timeout(10): - await hass.async_add_executor_job(data.update) - if not data.status: - raise UpdateFailed("Error fetching UPS state") - return data.status + try: + return await data.async_update() + except NUTLoginError as err: + raise ConfigEntryAuthFailed from err + except NUTError as err: + raise UpdateFailed(f"Error fetching UPS state: {err}") from err coordinator = DataUpdateCoordinator( hass, @@ -82,6 +86,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Fetch initial data so we have data when entities subscribe await coordinator.async_config_entry_first_refresh() + + # Note that async_listen_once is not used here because the listener + # could be removed after the event is fired. + entry.async_on_unload( + hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, data.async_shutdown) + ) status = coordinator.data _LOGGER.debug("NUT Sensors Available: %s", status) @@ -94,7 +104,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if username is not None and password is not None: user_available_commands = { device_supported_command - for device_supported_command in data.list_commands() or {} + for device_supported_command in await data.async_list_commands() or {} if device_supported_command in INTEGRATION_SUPPORTED_COMMANDS } else: @@ -212,15 +222,14 @@ class PyNUTData: alias: str | None, username: str | None, password: str | None, + persistent: bool = True, ) -> None: """Initialize the data object.""" self._host = host self._alias = alias - # Establish client with persistent=False to open/close connection on - # each update call. This is more reliable with async. - self._client = PyNUTClient(self._host, port, username, password, 5, False) + self._client = AIONUTClient(self._host, port, username, password, 5, persistent) self.ups_list: dict[str, str] | None = None self._status: dict[str, str] | None = None self._device_info: NUTDeviceInfo | None = None @@ -240,18 +249,11 @@ class PyNUTData: """Return the device info for the ups.""" return self._device_info or NUTDeviceInfo() - def _get_alias(self) -> str | None: + async def _async_get_alias(self) -> str | None: """Get the ups alias from NUT.""" - try: - ups_list: dict[str, str] = self._client.list_ups() - except PyNUTError as err: - _LOGGER.error("Failure getting NUT ups alias, %s", err) - return None - - if not ups_list: + if not (ups_list := await self._client.list_ups()): _LOGGER.error("Empty list while getting NUT ups aliases") return None - self.ups_list = ups_list return list(ups_list)[0] @@ -267,42 +269,45 @@ class PyNUTData: return device_info - def _get_status(self) -> dict[str, str] | None: + async def _async_get_status(self) -> dict[str, str]: """Get the ups status from NUT.""" if self._alias is None: - self._alias = self._get_alias() + self._alias = await self._async_get_alias() + if TYPE_CHECKING: + assert self._alias is not None + return await self._client.list_vars(self._alias) - try: - status: dict[str, str] = self._client.list_vars(self._alias) - except (PyNUTError, ConnectionResetError) as err: - _LOGGER.debug("Error getting NUT vars for host %s: %s", self._host, err) - return None - - return status - - def update(self) -> None: + async def async_update(self) -> dict[str, str]: """Fetch the latest status from NUT.""" - self._status = self._get_status() + self._status = await self._async_get_status() if self._device_info is None: self._device_info = self._get_device_info() + return self._status - async def async_run_command( - self, hass: HomeAssistant, command_name: str | None - ) -> None: + async def async_run_command(self, command_name: str) -> None: """Invoke instant command in UPS.""" + if TYPE_CHECKING: + assert self._alias is not None + try: - await hass.async_add_executor_job( - self._client.run_command, self._alias, command_name - ) - except PyNUTError as err: + await self._client.run_command(self._alias, command_name) + except NUTError as err: raise HomeAssistantError( f"Error running command {command_name}, {err}" ) from err - def list_commands(self) -> dict[str, str] | None: + async def async_list_commands(self) -> set[str] | None: """Fetch the list of supported commands.""" + if TYPE_CHECKING: + assert self._alias is not None + try: - return cast(dict[str, str], self._client.list_commands(self._alias)) - except PyNUTError as err: + return await self._client.list_commands(self._alias) + except NUTError as err: _LOGGER.error("Error retrieving supported commands %s", err) return None + + @callback + def async_shutdown(self, _: Event | None = None) -> None: + """Shutdown the client connection.""" + self._client.shutdown() diff --git a/homeassistant/components/nut/config_flow.py b/homeassistant/components/nut/config_flow.py index 917f004ce32..f0126ba4894 100644 --- a/homeassistant/components/nut/config_flow.py +++ b/homeassistant/components/nut/config_flow.py @@ -1,15 +1,21 @@ """Config flow for Network UPS Tools (NUT) integration.""" + from __future__ import annotations from collections.abc import Mapping import logging from typing import Any +from aionut import NUTError, NUTLoginError import voluptuous as vol -from homeassistant import exceptions from homeassistant.components import zeroconf -from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.const import ( CONF_ALIAS, CONF_BASE, @@ -20,28 +26,23 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import HomeAssistant, callback -from homeassistant.data_entry_flow import FlowResult +from homeassistant.data_entry_flow import AbortFlow from . import PyNUTData from .const import DEFAULT_HOST, DEFAULT_PORT, DEFAULT_SCAN_INTERVAL, DOMAIN _LOGGER = logging.getLogger(__name__) +AUTH_SCHEMA = {vol.Optional(CONF_USERNAME): str, vol.Optional(CONF_PASSWORD): str} -def _base_schema(discovery_info: zeroconf.ZeroconfServiceInfo | None) -> vol.Schema: + +def _base_schema(nut_config: dict[str, Any]) -> vol.Schema: """Generate base schema.""" - base_schema = {} - if not discovery_info: - base_schema.update( - { - vol.Optional(CONF_HOST, default=DEFAULT_HOST): str, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): int, - } - ) - base_schema.update( - {vol.Optional(CONF_USERNAME): str, vol.Optional(CONF_PASSWORD): str} - ) - + base_schema = { + vol.Optional(CONF_HOST, default=nut_config.get(CONF_HOST) or DEFAULT_HOST): str, + vol.Optional(CONF_PORT, default=nut_config.get(CONF_PORT) or DEFAULT_PORT): int, + } + base_schema.update(AUTH_SCHEMA) return vol.Schema(base_schema) @@ -62,10 +63,11 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, username = data.get(CONF_USERNAME) password = data.get(CONF_PASSWORD) - nut_data = PyNUTData(host, port, alias, username, password) - await hass.async_add_executor_job(nut_data.update) - if not (status := nut_data.status): - raise CannotConnect + nut_data = PyNUTData(host, port, alias, username, password, persistent=False) + status = await nut_data.async_update() + + if not alias and not nut_data.ups_list: + raise AbortFlow("no_ups_found") return {"ups_list": nut_data.ups_list, "available_resources": status} @@ -88,71 +90,73 @@ class NutConfigFlow(ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize the nut config flow.""" self.nut_config: dict[str, Any] = {} - self.discovery_info: zeroconf.ZeroconfServiceInfo | None = None self.ups_list: dict[str, str] | None = None self.title: str | None = None + self.reauth_entry: ConfigEntry | None = None async def async_step_zeroconf( self, discovery_info: zeroconf.ZeroconfServiceInfo - ) -> FlowResult: + ) -> ConfigFlowResult: """Prepare configuration for a discovered nut device.""" - self.discovery_info = discovery_info await self._async_handle_discovery_without_unique_id() - self.context["title_placeholders"] = { + self.nut_config = { + CONF_HOST: discovery_info.host or DEFAULT_HOST, CONF_PORT: discovery_info.port or DEFAULT_PORT, - CONF_HOST: discovery_info.host, } + self.context["title_placeholders"] = self.nut_config.copy() return await self.async_step_user() async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the user input.""" errors: dict[str, str] = {} + placeholders: dict[str, str] = {} + nut_config = self.nut_config if user_input is not None: - if self.discovery_info: - user_input.update( - { - CONF_HOST: self.discovery_info.host, - CONF_PORT: self.discovery_info.port or DEFAULT_PORT, - } - ) - info, errors = await self._async_validate_or_error(user_input) + nut_config.update(user_input) + + info, errors, placeholders = await self._async_validate_or_error(nut_config) if not errors: - self.nut_config.update(user_input) if len(info["ups_list"]) > 1: self.ups_list = info["ups_list"] return await self.async_step_ups() - if self._host_port_alias_already_configured(self.nut_config): + if self._host_port_alias_already_configured(nut_config): return self.async_abort(reason="already_configured") - title = _format_host_port_alias(self.nut_config) - return self.async_create_entry(title=title, data=self.nut_config) + title = _format_host_port_alias(nut_config) + return self.async_create_entry(title=title, data=nut_config) return self.async_show_form( - step_id="user", data_schema=_base_schema(self.discovery_info), errors=errors + step_id="user", + data_schema=_base_schema(nut_config), + errors=errors, + description_placeholders=placeholders, ) async def async_step_ups( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the picking the ups.""" errors: dict[str, str] = {} + placeholders: dict[str, str] = {} + nut_config = self.nut_config if user_input is not None: self.nut_config.update(user_input) - if self._host_port_alias_already_configured(self.nut_config): + if self._host_port_alias_already_configured(nut_config): return self.async_abort(reason="already_configured") - _, errors = await self._async_validate_or_error(self.nut_config) + _, errors, placeholders = await self._async_validate_or_error(nut_config) if not errors: - title = _format_host_port_alias(self.nut_config) - return self.async_create_entry(title=title, data=self.nut_config) + title = _format_host_port_alias(nut_config) + return self.async_create_entry(title=title, data=nut_config) return self.async_show_form( step_id="ups", data_schema=_ups_schema(self.ups_list or {}), errors=errors, + description_placeholders=placeholders, ) def _host_port_alias_already_configured(self, user_input: dict[str, Any]) -> bool: @@ -166,17 +170,66 @@ class NutConfigFlow(ConfigFlow, domain=DOMAIN): async def _async_validate_or_error( self, config: dict[str, Any] - ) -> tuple[dict[str, Any], dict[str, str]]: - errors = {} - info = {} + ) -> tuple[dict[str, Any], dict[str, str], dict[str, str]]: + errors: dict[str, str] = {} + info: dict[str, Any] = {} + description_placeholders: dict[str, str] = {} try: info = await validate_input(self.hass, config) - except CannotConnect: + except NUTLoginError: + errors[CONF_PASSWORD] = "invalid_auth" + except NUTError as ex: errors[CONF_BASE] = "cannot_connect" + description_placeholders["error"] = str(ex) + except AbortFlow: + raise except Exception: # pylint: disable=broad-except _LOGGER.exception("Unexpected exception") errors[CONF_BASE] = "unknown" - return info, errors + return info, errors, description_placeholders + + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: + """Handle reauth.""" + entry_id = self.context["entry_id"] + self.reauth_entry = self.hass.config_entries.async_get_entry(entry_id) + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle reauth input.""" + errors: dict[str, str] = {} + existing_entry = self.reauth_entry + assert existing_entry + existing_data = existing_entry.data + description_placeholders: dict[str, str] = { + CONF_HOST: existing_data[CONF_HOST], + CONF_PORT: existing_data[CONF_PORT], + } + if user_input is not None: + new_config = { + **existing_data, + # Username/password are optional and some servers + # use ip based authentication and will fail if + # username/password are provided + CONF_USERNAME: user_input.get(CONF_USERNAME), + CONF_PASSWORD: user_input.get(CONF_PASSWORD), + } + _, errors, placeholders = await self._async_validate_or_error(new_config) + if not errors: + return self.async_update_reload_and_abort( + existing_entry, data=new_config + ) + description_placeholders.update(placeholders) + + return self.async_show_form( + description_placeholders=description_placeholders, + step_id="reauth_confirm", + data_schema=vol.Schema(AUTH_SCHEMA), + errors=errors, + ) @staticmethod @callback @@ -194,7 +247,7 @@ class OptionsFlowHandler(OptionsFlow): async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle options flow.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) @@ -210,7 +263,3 @@ class OptionsFlowHandler(OptionsFlow): } return self.async_show_form(step_id="init", data_schema=vol.Schema(base_schema)) - - -class CannotConnect(exceptions.HomeAssistantError): - """Error to indicate we cannot connect.""" diff --git a/homeassistant/components/nut/const.py b/homeassistant/components/nut/const.py index 13951a44d90..9be06de1f73 100644 --- a/homeassistant/components/nut/const.py +++ b/homeassistant/components/nut/const.py @@ -1,4 +1,5 @@ """The nut component.""" + from __future__ import annotations from homeassistant.const import Platform diff --git a/homeassistant/components/nut/device_action.py b/homeassistant/components/nut/device_action.py index 4898d9cc82d..0ec58e651b2 100644 --- a/homeassistant/components/nut/device_action.py +++ b/homeassistant/components/nut/device_action.py @@ -1,4 +1,5 @@ """Provides device actions for Network UPS Tools (NUT).""" + from __future__ import annotations import voluptuous as vol @@ -57,7 +58,7 @@ async def async_call_action_from_config( device_id: str = config[CONF_DEVICE_ID] entry_id = _get_entry_id_from_device_id(hass, device_id) data: PyNUTData = hass.data[DOMAIN][entry_id][PYNUT_DATA] - await data.async_run_command(hass, command_name) + await data.async_run_command(command_name) def _get_device_action_name(command_name: str) -> str: diff --git a/homeassistant/components/nut/diagnostics.py b/homeassistant/components/nut/diagnostics.py index 9ee430a655b..88a05e461c9 100644 --- a/homeassistant/components/nut/diagnostics.py +++ b/homeassistant/components/nut/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for Nut.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/nut/manifest.json b/homeassistant/components/nut/manifest.json index 0303dd70ec1..1f649a32d7f 100644 --- a/homeassistant/components/nut/manifest.json +++ b/homeassistant/components/nut/manifest.json @@ -6,7 +6,7 @@ "documentation": "https://www.home-assistant.io/integrations/nut", "integration_type": "device", "iot_class": "local_polling", - "loggers": ["pynut2"], - "requirements": ["pynut2==2.1.2"], + "loggers": ["aionut"], + "requirements": ["aionut==4.3.2"], "zeroconf": ["_nut._tcp.local."] } diff --git a/homeassistant/components/nut/sensor.py b/homeassistant/components/nut/sensor.py index e8290f5fa6d..cd5ae64901d 100644 --- a/homeassistant/components/nut/sensor.py +++ b/homeassistant/components/nut/sensor.py @@ -1,4 +1,5 @@ """Provides a sensor to track various status aspects of a UPS.""" + from __future__ import annotations from dataclasses import asdict diff --git a/homeassistant/components/nut/strings.json b/homeassistant/components/nut/strings.json index 7347744d56f..d5b9acbdaad 100644 --- a/homeassistant/components/nut/strings.json +++ b/homeassistant/components/nut/strings.json @@ -18,14 +18,24 @@ "data": { "alias": "Alias" } + }, + "reauth_confirm": { + "description": "Re-authenticate NUT server at {host}:{port}", + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } } }, "error": { - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "unknown": "[%key:common::config_flow::error::unknown%]" + "cannot_connect": "Connection error: {error}", + "unknown": "[%key:common::config_flow::error::unknown%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "no_ups_found": "There are no UPS devices available on the NUT server.", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } }, "options": { @@ -166,7 +176,9 @@ "ups_delay_reboot": { "name": "UPS reboot delay" }, "ups_delay_shutdown": { "name": "UPS shutdown delay" }, "ups_delay_start": { "name": "Load restart delay" }, - "ups_display_language": { "name": "Language" }, + "ups_display_language": { + "name": "[%key:common::config_flow::data::language%]" + }, "ups_efficiency": { "name": "Efficiency" }, "ups_id": { "name": "System identifier" }, "ups_load": { "name": "Load" }, diff --git a/homeassistant/components/nws/__init__.py b/homeassistant/components/nws/__init__.py index da54f3b119e..34157769b97 100644 --- a/homeassistant/components/nws/__init__.py +++ b/homeassistant/components/nws/__init__.py @@ -1,4 +1,5 @@ """The National Weather Service integration.""" + from __future__ import annotations from collections.abc import Awaitable, Callable diff --git a/homeassistant/components/nws/config_flow.py b/homeassistant/components/nws/config_flow.py index 10eab390917..37d5bb5bf82 100644 --- a/homeassistant/components/nws/config_flow.py +++ b/homeassistant/components/nws/config_flow.py @@ -1,4 +1,5 @@ """Config flow for National Weather Service (NWS) integration.""" + from __future__ import annotations import logging @@ -8,9 +9,10 @@ import aiohttp from pynws import SimpleNWS import voluptuous as vol -from homeassistant import config_entries, core, exceptions +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE -from homeassistant.data_entry_flow import FlowResult +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -20,9 +22,7 @@ from .const import CONF_STATION, DOMAIN _LOGGER = logging.getLogger(__name__) -async def validate_input( - hass: core.HomeAssistant, data: dict[str, Any] -) -> dict[str, str]: +async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, str]: """Validate the user input allows us to connect. Data has the keys from DATA_SCHEMA with values provided by the user. @@ -45,14 +45,14 @@ async def validate_input( return {"title": nws.station} -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class NWSConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for National Weather Service (NWS).""" VERSION = 1 async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors: dict[str, str] = {} if user_input is not None: @@ -88,5 +88,5 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -class CannotConnect(exceptions.HomeAssistantError): +class CannotConnect(HomeAssistantError): """Error to indicate we cannot connect.""" diff --git a/homeassistant/components/nws/const.py b/homeassistant/components/nws/const.py index 1e028649d89..3de874b5c10 100644 --- a/homeassistant/components/nws/const.py +++ b/homeassistant/components/nws/const.py @@ -1,4 +1,5 @@ """Constants for National Weather Service Integration.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/nws/sensor.py b/homeassistant/components/nws/sensor.py index 35fb6c0ec1f..1d8c5ab045e 100644 --- a/homeassistant/components/nws/sensor.py +++ b/homeassistant/components/nws/sensor.py @@ -1,4 +1,5 @@ """Sensors for National Weather Service (NWS).""" + from __future__ import annotations from dataclasses import dataclass diff --git a/homeassistant/components/nws/weather.py b/homeassistant/components/nws/weather.py index 9d41e54ccd0..89414f5acf1 100644 --- a/homeassistant/components/nws/weather.py +++ b/homeassistant/components/nws/weather.py @@ -1,4 +1,5 @@ """Support for NWS weather service.""" + from __future__ import annotations from types import MappingProxyType @@ -84,17 +85,15 @@ async def async_setup_entry( entity_registry = er.async_get(hass) nws_data: NWSData = hass.data[DOMAIN][entry.entry_id] - entities = [NWSWeather(entry.data, nws_data, DAYNIGHT)] - - # Add hourly entity to legacy config entries - if entity_registry.async_get_entity_id( + # Remove hourly entity from legacy config entries + if entity_id := entity_registry.async_get_entity_id( WEATHER_DOMAIN, DOMAIN, _calculate_unique_id(entry.data, HOURLY), ): - entities.append(NWSWeather(entry.data, nws_data, HOURLY)) + entity_registry.async_remove(entity_id) - async_add_entities(entities, False) + async_add_entities([NWSWeather(entry.data, nws_data)], False) if TYPE_CHECKING: @@ -129,7 +128,6 @@ class NWSWeather(CoordinatorWeatherEntity): self, entry_data: MappingProxyType[str, Any], nws_data: NWSData, - mode: str, ) -> None: """Initialise the platform with a data instance and station name.""" super().__init__( @@ -142,23 +140,17 @@ class NWSWeather(CoordinatorWeatherEntity): self.nws = nws_data.api latitude = entry_data[CONF_LATITUDE] longitude = entry_data[CONF_LONGITUDE] - if mode == DAYNIGHT: - self.coordinator_forecast_legacy = nws_data.coordinator_forecast - else: - self.coordinator_forecast_legacy = nws_data.coordinator_forecast_hourly + self.coordinator_forecast_legacy = nws_data.coordinator_forecast self.station = self.nws.station - self.mode = mode - self._attr_entity_registry_enabled_default = mode == DAYNIGHT - self.observation: dict[str, Any] | None = None self._forecast_hourly: list[dict[str, Any]] | None = None self._forecast_legacy: list[dict[str, Any]] | None = None self._forecast_twice_daily: list[dict[str, Any]] | None = None - self._attr_unique_id = _calculate_unique_id(entry_data, mode) + self._attr_unique_id = _calculate_unique_id(entry_data, DAYNIGHT) self._attr_device_info = device_info(latitude, longitude) - self._attr_name = f"{self.station} {self.mode.title()}" + self._attr_name = self.station async def async_added_to_hass(self) -> None: """Set up a listener and load data.""" @@ -193,10 +185,7 @@ class NWSWeather(CoordinatorWeatherEntity): @callback def _handle_legacy_forecast_coordinator_update(self) -> None: """Handle updated data from the legacy forecast coordinator.""" - if self.mode == DAYNIGHT: - self._forecast_legacy = self.nws.forecast - else: - self._forecast_legacy = self.nws.forecast_hourly + self._forecast_legacy = self.nws.forecast self.async_write_ha_state() @property @@ -310,11 +299,6 @@ class NWSWeather(CoordinatorWeatherEntity): forecast.append(data) return forecast - @property - def forecast(self) -> list[Forecast] | None: - """Return forecast.""" - return self._forecast(self._forecast_legacy, self.mode) - @callback def _async_forecast_hourly(self) -> list[Forecast] | None: """Return the hourly forecast in native units.""" diff --git a/homeassistant/components/nx584/alarm_control_panel.py b/homeassistant/components/nx584/alarm_control_panel.py index 3eaaf07ad1c..d29ac0388ca 100644 --- a/homeassistant/components/nx584/alarm_control_panel.py +++ b/homeassistant/components/nx584/alarm_control_panel.py @@ -1,4 +1,5 @@ """Support for NX584 alarm control panels.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/nx584/binary_sensor.py b/homeassistant/components/nx584/binary_sensor.py index ca55ea25c40..627051a4d65 100644 --- a/homeassistant/components/nx584/binary_sensor.py +++ b/homeassistant/components/nx584/binary_sensor.py @@ -1,4 +1,5 @@ """Support for exposing NX584 elements as sensors.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/nx584/icons.json b/homeassistant/components/nx584/icons.json new file mode 100644 index 00000000000..76e5ae82e09 --- /dev/null +++ b/homeassistant/components/nx584/icons.json @@ -0,0 +1,6 @@ +{ + "services": { + "bypass_zone": "mdi:wrench", + "unbypass_zone": "mdi:wrench" + } +} diff --git a/homeassistant/components/nzbget/__init__.py b/homeassistant/components/nzbget/__init__.py index 9d6fafd30c7..61b3f98739c 100644 --- a/homeassistant/components/nzbget/__init__.py +++ b/homeassistant/components/nzbget/__init__.py @@ -1,4 +1,5 @@ """The NZBGet integration.""" + import voluptuous as vol from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/nzbget/config_flow.py b/homeassistant/components/nzbget/config_flow.py index 782ec791eeb..2c549e4ed24 100644 --- a/homeassistant/components/nzbget/config_flow.py +++ b/homeassistant/components/nzbget/config_flow.py @@ -1,4 +1,5 @@ """Config flow for NZBGet.""" + from __future__ import annotations import logging @@ -6,7 +7,7 @@ from typing import Any import voluptuous as vol -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import ( CONF_HOST, CONF_NAME, @@ -16,7 +17,6 @@ from homeassistant.const import ( CONF_USERNAME, CONF_VERIFY_SSL, ) -from homeassistant.data_entry_flow import FlowResult from .const import DEFAULT_NAME, DEFAULT_PORT, DEFAULT_SSL, DEFAULT_VERIFY_SSL, DOMAIN from .coordinator import NZBGetAPI, NZBGetAPIException @@ -48,7 +48,7 @@ class NZBGetConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initiated by the user.""" if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") @@ -82,9 +82,9 @@ class NZBGetConfigFlow(ConfigFlow, domain=DOMAIN): } if self.show_advanced_options: - data_schema[ - vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL) - ] = bool + data_schema[vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL)] = ( + bool + ) return self.async_show_form( step_id="user", diff --git a/homeassistant/components/nzbget/const.py b/homeassistant/components/nzbget/const.py index 7838d64c6d7..6742567bbf2 100644 --- a/homeassistant/components/nzbget/const.py +++ b/homeassistant/components/nzbget/const.py @@ -1,4 +1,5 @@ """Constants for NZBGet.""" + DOMAIN = "nzbget" # Attributes diff --git a/homeassistant/components/nzbget/coordinator.py b/homeassistant/components/nzbget/coordinator.py index dcefe25eae9..cf9625ce4ec 100644 --- a/homeassistant/components/nzbget/coordinator.py +++ b/homeassistant/components/nzbget/coordinator.py @@ -1,4 +1,5 @@ """Provides the NZBGet DataUpdateCoordinator.""" + import asyncio from collections.abc import Mapping from datetime import timedelta diff --git a/homeassistant/components/nzbget/icons.json b/homeassistant/components/nzbget/icons.json new file mode 100644 index 00000000000..a693e9fec86 --- /dev/null +++ b/homeassistant/components/nzbget/icons.json @@ -0,0 +1,7 @@ +{ + "services": { + "pause": "mdi:pause", + "resume": "mdi:play", + "set_speed": "mdi:speedometer" + } +} diff --git a/homeassistant/components/nzbget/sensor.py b/homeassistant/components/nzbget/sensor.py index d76e004d720..394e1175c2f 100644 --- a/homeassistant/components/nzbget/sensor.py +++ b/homeassistant/components/nzbget/sensor.py @@ -1,4 +1,5 @@ """Monitor the NZBGet API.""" + from __future__ import annotations from datetime import datetime, timedelta diff --git a/homeassistant/components/nzbget/switch.py b/homeassistant/components/nzbget/switch.py index 5d72cae37cf..c6505fd522d 100644 --- a/homeassistant/components/nzbget/switch.py +++ b/homeassistant/components/nzbget/switch.py @@ -1,4 +1,5 @@ """Support for NZBGet switches.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/oasa_telematics/sensor.py b/homeassistant/components/oasa_telematics/sensor.py index b9109645943..2a68c7ce15d 100644 --- a/homeassistant/components/oasa_telematics/sensor.py +++ b/homeassistant/components/oasa_telematics/sensor.py @@ -1,4 +1,5 @@ """Support for OASA Telematics from telematics.oasa.gr.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/obihai/config_flow.py b/homeassistant/components/obihai/config_flow.py index 1790add84f0..559900db5d0 100644 --- a/homeassistant/components/obihai/config_flow.py +++ b/homeassistant/components/obihai/config_flow.py @@ -9,10 +9,9 @@ from pyobihai import PyObihai import voluptuous as vol from homeassistant.components import dhcp -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.device_registry import format_mac from .connectivity import validate_auth @@ -59,7 +58,7 @@ class ObihaiFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" errors: dict[str, str] = {} @@ -94,7 +93,9 @@ class ObihaiFlowHandler(ConfigFlow, domain=DOMAIN): data_schema=self.add_suggested_values_to_schema(data_schema, user_input), ) - async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: + async def async_step_dhcp( + self, discovery_info: dhcp.DhcpServiceInfo + ) -> ConfigFlowResult: """Prepare configuration for a DHCP discovered Obihai.""" self._dhcp_discovery_info = discovery_info @@ -102,7 +103,7 @@ class ObihaiFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_dhcp_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Attempt to confirm.""" assert self._dhcp_discovery_info await self.async_set_unique_id(format_mac(self._dhcp_discovery_info.macaddress)) diff --git a/homeassistant/components/obihai/sensor.py b/homeassistant/components/obihai/sensor.py index 53208a1e6a1..91920b4c32d 100644 --- a/homeassistant/components/obihai/sensor.py +++ b/homeassistant/components/obihai/sensor.py @@ -1,4 +1,5 @@ """Support for Obihai Sensors.""" + from __future__ import annotations import datetime @@ -23,16 +24,16 @@ async def async_setup_entry( requester: ObihaiConnection = hass.data[DOMAIN][entry.entry_id] - sensors = [] - for key in requester.services: - sensors.append(ObihaiServiceSensors(requester, key)) + sensors = [ObihaiServiceSensors(requester, key) for key in requester.services] + + sensors.extend( + ObihaiServiceSensors(requester, key) for key in requester.call_direction + ) if requester.line_services is not None: - for key in requester.line_services: - sensors.append(ObihaiServiceSensors(requester, key)) - - for key in requester.call_direction: - sensors.append(ObihaiServiceSensors(requester, key)) + sensors.extend( + ObihaiServiceSensors(requester, key) for key in requester.line_services + ) async_add_entities(sensors, update_before_add=True) diff --git a/homeassistant/components/octoprint/__init__.py b/homeassistant/components/octoprint/__init__.py index 1a96078c003..7a9f3990435 100644 --- a/homeassistant/components/octoprint/__init__.py +++ b/homeassistant/components/octoprint/__init__.py @@ -1,4 +1,5 @@ """Support for monitoring OctoPrint 3D printers.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/octoprint/binary_sensor.py b/homeassistant/components/octoprint/binary_sensor.py index 0bc13f66415..10a637e5a3b 100644 --- a/homeassistant/components/octoprint/binary_sensor.py +++ b/homeassistant/components/octoprint/binary_sensor.py @@ -1,4 +1,5 @@ """Support for monitoring OctoPrint binary sensors.""" + from __future__ import annotations from abc import abstractmethod diff --git a/homeassistant/components/octoprint/button.py b/homeassistant/components/octoprint/button.py index b2c1672b3e4..2a2e5015303 100644 --- a/homeassistant/components/octoprint/button.py +++ b/homeassistant/components/octoprint/button.py @@ -1,7 +1,8 @@ """Support for Octoprint buttons.""" + from pyoctoprintapi import OctoprintClient, OctoprintPrinterInfo -from homeassistant.components.button import ButtonEntity +from homeassistant.components.button import ButtonDeviceClass, ButtonEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError @@ -30,11 +31,16 @@ async def async_setup_entry( OctoprintResumeJobButton(coordinator, device_id, client), OctoprintPauseJobButton(coordinator, device_id, client), OctoprintStopJobButton(coordinator, device_id, client), + OctoprintShutdownSystemButton(coordinator, device_id, client), + OctoprintRebootSystemButton(coordinator, device_id, client), + OctoprintRestartOctoprintButton(coordinator, device_id, client), ] ) -class OctoprintButton(CoordinatorEntity[OctoprintDataUpdateCoordinator], ButtonEntity): +class OctoprintPrinterButton( + CoordinatorEntity[OctoprintDataUpdateCoordinator], ButtonEntity +): """Represent an OctoPrint binary sensor.""" client: OctoprintClient @@ -60,7 +66,35 @@ class OctoprintButton(CoordinatorEntity[OctoprintDataUpdateCoordinator], ButtonE return self.coordinator.last_update_success and self.coordinator.data["printer"] -class OctoprintPauseJobButton(OctoprintButton): +class OctoprintSystemButton( + CoordinatorEntity[OctoprintDataUpdateCoordinator], ButtonEntity +): + """Represent an OctoPrint binary sensor.""" + + client: OctoprintClient + + def __init__( + self, + coordinator: OctoprintDataUpdateCoordinator, + button_type: str, + device_id: str, + client: OctoprintClient, + ) -> None: + """Initialize a new OctoPrint button.""" + super().__init__(coordinator) + self.client = client + self._device_id = device_id + self._attr_name = f"OctoPrint {button_type}" + self._attr_unique_id = f"{button_type}-{device_id}" + self._attr_device_info = coordinator.device_info + + @property + def available(self) -> bool: + """Return if entity is available.""" + return self.coordinator.last_update_success + + +class OctoprintPauseJobButton(OctoprintPrinterButton): """Pause the active job.""" def __init__( @@ -82,7 +116,7 @@ class OctoprintPauseJobButton(OctoprintButton): raise InvalidPrinterState("Printer is not printing") -class OctoprintResumeJobButton(OctoprintButton): +class OctoprintResumeJobButton(OctoprintPrinterButton): """Resume the active job.""" def __init__( @@ -104,7 +138,7 @@ class OctoprintResumeJobButton(OctoprintButton): raise InvalidPrinterState("Printer is not currently paused") -class OctoprintStopJobButton(OctoprintButton): +class OctoprintStopJobButton(OctoprintPrinterButton): """Resume the active job.""" def __init__( @@ -124,5 +158,60 @@ class OctoprintStopJobButton(OctoprintButton): await self.client.cancel_job() +class OctoprintShutdownSystemButton(OctoprintSystemButton): + """Shutdown the system.""" + + def __init__( + self, + coordinator: OctoprintDataUpdateCoordinator, + device_id: str, + client: OctoprintClient, + ) -> None: + """Initialize a new OctoPrint button.""" + super().__init__(coordinator, "Shutdown System", device_id, client) + + async def async_press(self) -> None: + """Handle the button press.""" + await self.client.shutdown() + + +class OctoprintRebootSystemButton(OctoprintSystemButton): + """Reboot the system.""" + + _attr_device_class = ButtonDeviceClass.RESTART + + def __init__( + self, + coordinator: OctoprintDataUpdateCoordinator, + device_id: str, + client: OctoprintClient, + ) -> None: + """Initialize a new OctoPrint button.""" + super().__init__(coordinator, "Reboot System", device_id, client) + + async def async_press(self) -> None: + """Handle the button press.""" + await self.client.reboot_system() + + +class OctoprintRestartOctoprintButton(OctoprintSystemButton): + """Restart Octoprint.""" + + _attr_device_class = ButtonDeviceClass.RESTART + + def __init__( + self, + coordinator: OctoprintDataUpdateCoordinator, + device_id: str, + client: OctoprintClient, + ) -> None: + """Initialize a new OctoPrint button.""" + super().__init__(coordinator, "Restart Octoprint", device_id, client) + + async def async_press(self) -> None: + """Handle the button press.""" + await self.client.restart() + + class InvalidPrinterState(HomeAssistantError): """Service attempted in invalid state.""" diff --git a/homeassistant/components/octoprint/camera.py b/homeassistant/components/octoprint/camera.py index a6955706508..c5d6f9a62e1 100644 --- a/homeassistant/components/octoprint/camera.py +++ b/homeassistant/components/octoprint/camera.py @@ -1,4 +1,5 @@ """Support for OctoPrint binary camera.""" + from __future__ import annotations from pyoctoprintapi import OctoprintClient, WebcamSettings diff --git a/homeassistant/components/octoprint/config_flow.py b/homeassistant/components/octoprint/config_flow.py index 01a3e9518c0..6bd592c38bf 100644 --- a/homeassistant/components/octoprint/config_flow.py +++ b/homeassistant/components/octoprint/config_flow.py @@ -1,4 +1,5 @@ """Config flow for OctoPrint integration.""" + from __future__ import annotations import asyncio @@ -11,8 +12,8 @@ from pyoctoprintapi import ApiError, OctoprintClient, OctoprintException import voluptuous as vol from yarl import URL -from homeassistant import config_entries, data_entry_flow, exceptions from homeassistant.components import ssdp, zeroconf +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import ( CONF_API_KEY, CONF_HOST, @@ -22,7 +23,8 @@ from homeassistant.const import ( CONF_USERNAME, CONF_VERIFY_SSL, ) -from homeassistant.data_entry_flow import FlowResult +from homeassistant.data_entry_flow import AbortFlow +from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv from homeassistant.util.ssl import get_default_context, get_default_no_verify_context @@ -47,7 +49,7 @@ def _schema_with_defaults( ) -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class OctoPrintConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for OctoPrint.""" VERSION = 1 @@ -76,7 +78,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors = {} try: return await self._finish_config(user_input) - except data_entry_flow.AbortFlow as err: + except AbortFlow as err: raise err from None except CannotConnect: errors["base"] = "cannot_connect" @@ -160,7 +162,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_zeroconf( self, discovery_info: zeroconf.ZeroconfServiceInfo - ) -> data_entry_flow.FlowResult: + ) -> ConfigFlowResult: """Handle discovery flow.""" uuid = discovery_info.properties["uuid"] await self.async_set_unique_id(uuid) @@ -186,7 +188,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_ssdp( self, discovery_info: ssdp.SsdpServiceInfo - ) -> data_entry_flow.FlowResult: + ) -> ConfigFlowResult: """Handle ssdp discovery flow.""" uuid = discovery_info.upnp["UDN"][5:] await self.async_set_unique_id(uuid) @@ -209,7 +211,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_user() - async def async_step_reauth(self, config: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth(self, config: Mapping[str, Any]) -> ConfigFlowResult: """Handle reauthorization request from Octoprint.""" self._reauth_data = dict(config) @@ -223,7 +225,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle reauthorization flow.""" assert self._reauth_data is not None @@ -279,5 +281,5 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): session.detach() -class CannotConnect(exceptions.HomeAssistantError): +class CannotConnect(HomeAssistantError): """Error to indicate we cannot connect.""" diff --git a/homeassistant/components/octoprint/coordinator.py b/homeassistant/components/octoprint/coordinator.py index c6ce8fa66b7..ff00b6c3420 100644 --- a/homeassistant/components/octoprint/coordinator.py +++ b/homeassistant/components/octoprint/coordinator.py @@ -1,4 +1,5 @@ """The data update coordinator for OctoPrint.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/octoprint/icons.json b/homeassistant/components/octoprint/icons.json new file mode 100644 index 00000000000..972ecabb765 --- /dev/null +++ b/homeassistant/components/octoprint/icons.json @@ -0,0 +1,5 @@ +{ + "services": { + "printer_connect": "mdi:lan-connect" + } +} diff --git a/homeassistant/components/octoprint/sensor.py b/homeassistant/components/octoprint/sensor.py index 1ea29c2b4e8..fb5f292d669 100644 --- a/homeassistant/components/octoprint/sensor.py +++ b/homeassistant/components/octoprint/sensor.py @@ -1,4 +1,5 @@ """Support for monitoring OctoPrint sensors.""" + from __future__ import annotations from datetime import datetime, timedelta @@ -54,7 +55,7 @@ async def async_setup_entry( if not coordinator.data["printer"]: return - new_tools = [] + new_tools: list[OctoPrintTemperatureSensor] = [] for tool in [ tool for tool in coordinator.data["printer"].temperatures @@ -62,15 +63,15 @@ async def async_setup_entry( ]: assert device_id is not None known_tools.add(tool.name) - for temp_type in ("actual", "target"): - new_tools.append( - OctoPrintTemperatureSensor( - coordinator, - tool.name, - temp_type, - device_id, - ) + new_tools.extend( + OctoPrintTemperatureSensor( + coordinator, + tool.name, + temp_type, + device_id, ) + for temp_type in ("actual", "target") + ) async_add_entities(new_tools) config_entry.async_on_unload(coordinator.async_add_listener(async_add_tool_sensors)) diff --git a/homeassistant/components/oem/climate.py b/homeassistant/components/oem/climate.py index 86c770ec82d..6c4b97ca450 100644 --- a/homeassistant/components/oem/climate.py +++ b/homeassistant/components/oem/climate.py @@ -1,4 +1,5 @@ """OpenEnergyMonitor Thermostat Support.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/ohmconnect/sensor.py b/homeassistant/components/ohmconnect/sensor.py index 11606bfc6c2..2598e5fe514 100644 --- a/homeassistant/components/ohmconnect/sensor.py +++ b/homeassistant/components/ohmconnect/sensor.py @@ -1,4 +1,5 @@ """Support for OhmConnect.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/ollama/__init__.py b/homeassistant/components/ollama/__init__.py new file mode 100644 index 00000000000..8c9b00f3c9c --- /dev/null +++ b/homeassistant/components/ollama/__init__.py @@ -0,0 +1,266 @@ +"""The Ollama integration.""" + +from __future__ import annotations + +import asyncio +import logging +import time +from typing import Literal + +import httpx +import ollama + +from homeassistant.components import conversation +from homeassistant.components.homeassistant.exposed_entities import async_should_expose +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_URL, MATCH_ALL +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady, TemplateError +from homeassistant.helpers import ( + area_registry as ar, + config_validation as cv, + device_registry as dr, + entity_registry as er, + intent, + template, +) +from homeassistant.util import ulid + +from .const import ( + CONF_MAX_HISTORY, + CONF_MODEL, + CONF_PROMPT, + DEFAULT_MAX_HISTORY, + DEFAULT_PROMPT, + DEFAULT_TIMEOUT, + DOMAIN, + KEEP_ALIVE_FOREVER, + MAX_HISTORY_SECONDS, +) +from .models import ExposedEntity, MessageHistory, MessageRole + +_LOGGER = logging.getLogger(__name__) + +__all__ = [ + "CONF_URL", + "CONF_PROMPT", + "CONF_MODEL", + "CONF_MAX_HISTORY", + "MAX_HISTORY_NO_LIMIT", + "DOMAIN", +] + +CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Ollama from a config entry.""" + settings = {**entry.data, **entry.options} + client = ollama.AsyncClient(host=settings[CONF_URL]) + try: + async with asyncio.timeout(DEFAULT_TIMEOUT): + await client.list() + except (TimeoutError, httpx.ConnectError) as err: + raise ConfigEntryNotReady(err) from err + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = client + + conversation.async_set_agent(hass, entry, OllamaAgent(hass, entry)) + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload Ollama.""" + hass.data[DOMAIN].pop(entry.entry_id) + conversation.async_unset_agent(hass, entry) + return True + + +class OllamaAgent(conversation.AbstractConversationAgent): + """Ollama conversation agent.""" + + def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: + """Initialize the agent.""" + self.hass = hass + self.entry = entry + + # conversation id -> message history + self._history: dict[str, MessageHistory] = {} + + @property + def supported_languages(self) -> list[str] | Literal["*"]: + """Return a list of supported languages.""" + return MATCH_ALL + + async def async_process( + self, user_input: conversation.ConversationInput + ) -> conversation.ConversationResult: + """Process a sentence.""" + settings = {**self.entry.data, **self.entry.options} + + client = self.hass.data[DOMAIN][self.entry.entry_id] + conversation_id = user_input.conversation_id or ulid.ulid_now() + model = settings[CONF_MODEL] + + # Look up message history + message_history: MessageHistory | None = None + message_history = self._history.get(conversation_id) + if message_history is None: + # New history + # + # Render prompt and error out early if there's a problem + raw_prompt = settings.get(CONF_PROMPT, DEFAULT_PROMPT) + try: + prompt = self._generate_prompt(raw_prompt) + _LOGGER.debug("Prompt: %s", prompt) + except TemplateError as err: + _LOGGER.error("Error rendering prompt: %s", err) + intent_response = intent.IntentResponse(language=user_input.language) + intent_response.async_set_error( + intent.IntentResponseErrorCode.UNKNOWN, + f"Sorry, I had a problem generating my prompt: {err}", + ) + return conversation.ConversationResult( + response=intent_response, conversation_id=conversation_id + ) + + message_history = MessageHistory( + timestamp=time.monotonic(), + messages=[ + ollama.Message(role=MessageRole.SYSTEM.value, content=prompt) + ], + ) + self._history[conversation_id] = message_history + else: + # Bump timestamp so this conversation won't get cleaned up + message_history.timestamp = time.monotonic() + + # Clean up old histories + self._prune_old_histories() + + # Trim this message history to keep a maximum number of *user* messages + max_messages = int(settings.get(CONF_MAX_HISTORY, DEFAULT_MAX_HISTORY)) + self._trim_history(message_history, max_messages) + + # Add new user message + message_history.messages.append( + ollama.Message(role=MessageRole.USER.value, content=user_input.text) + ) + + # Get response + try: + response = await client.chat( + model=model, + # Make a copy of the messages because we mutate the list later + messages=list(message_history.messages), + stream=False, + keep_alive=KEEP_ALIVE_FOREVER, + ) + except (ollama.RequestError, ollama.ResponseError) as err: + _LOGGER.error("Unexpected error talking to Ollama server: %s", err) + intent_response = intent.IntentResponse(language=user_input.language) + intent_response.async_set_error( + intent.IntentResponseErrorCode.UNKNOWN, + f"Sorry, I had a problem talking to the Ollama server: {err}", + ) + return conversation.ConversationResult( + response=intent_response, conversation_id=conversation_id + ) + + response_message = response["message"] + message_history.messages.append( + ollama.Message( + role=response_message["role"], content=response_message["content"] + ) + ) + + # Create intent response + intent_response = intent.IntentResponse(language=user_input.language) + intent_response.async_set_speech(response_message["content"]) + return conversation.ConversationResult( + response=intent_response, conversation_id=conversation_id + ) + + def _prune_old_histories(self) -> None: + """Remove old message histories.""" + now = time.monotonic() + self._history = { + conversation_id: message_history + for conversation_id, message_history in self._history.items() + if (now - message_history.timestamp) <= MAX_HISTORY_SECONDS + } + + def _trim_history(self, message_history: MessageHistory, max_messages: int) -> None: + """Trims excess messages from a single history.""" + if max_messages < 1: + # Keep all messages + return + + if message_history.num_user_messages >= max_messages: + # Trim history but keep system prompt (first message). + # Every other message should be an assistant message, so keep 2x + # message objects. + num_keep = 2 * max_messages + drop_index = len(message_history.messages) - num_keep + message_history.messages = [ + message_history.messages[0] + ] + message_history.messages[drop_index:] + + def _generate_prompt(self, raw_prompt: str) -> str: + """Generate a prompt for the user.""" + return template.Template(raw_prompt, self.hass).async_render( + { + "ha_name": self.hass.config.location_name, + "ha_language": self.hass.config.language, + "exposed_entities": self._get_exposed_entities(), + }, + parse_result=False, + ) + + def _get_exposed_entities(self) -> list[ExposedEntity]: + """Get state list of exposed entities.""" + area_registry = ar.async_get(self.hass) + entity_registry = er.async_get(self.hass) + device_registry = dr.async_get(self.hass) + + exposed_entities = [] + exposed_states = [ + state + for state in self.hass.states.async_all() + if async_should_expose(self.hass, conversation.DOMAIN, state.entity_id) + ] + + for state in exposed_states: + entity = entity_registry.async_get(state.entity_id) + names = [state.name] + area_names = [] + + if entity is not None: + # Add aliases + names.extend(entity.aliases) + if entity.area_id and ( + area := area_registry.async_get_area(entity.area_id) + ): + # Entity is in area + area_names.append(area.name) + area_names.extend(area.aliases) + elif entity.device_id and ( + device := device_registry.async_get(entity.device_id) + ): + # Check device area + if device.area_id and ( + area := area_registry.async_get_area(device.area_id) + ): + area_names.append(area.name) + area_names.extend(area.aliases) + + exposed_entities.append( + ExposedEntity( + entity_id=state.entity_id, + state=state, + names=names, + area_names=area_names, + ) + ) + + return exposed_entities diff --git a/homeassistant/components/ollama/config_flow.py b/homeassistant/components/ollama/config_flow.py new file mode 100644 index 00000000000..50d0667803f --- /dev/null +++ b/homeassistant/components/ollama/config_flow.py @@ -0,0 +1,245 @@ +"""Config flow for Ollama integration.""" + +from __future__ import annotations + +import asyncio +import logging +import sys +from types import MappingProxyType +from typing import Any + +import httpx +import ollama +import voluptuous as vol + +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) +from homeassistant.const import CONF_URL +from homeassistant.helpers.selector import ( + NumberSelector, + NumberSelectorConfig, + NumberSelectorMode, + SelectOptionDict, + SelectSelector, + SelectSelectorConfig, + TemplateSelector, + TextSelector, + TextSelectorConfig, + TextSelectorType, +) + +from .const import ( + CONF_MAX_HISTORY, + CONF_MODEL, + CONF_PROMPT, + DEFAULT_MAX_HISTORY, + DEFAULT_MODEL, + DEFAULT_PROMPT, + DEFAULT_TIMEOUT, + DOMAIN, + MODEL_NAMES, +) + +_LOGGER = logging.getLogger(__name__) + + +STEP_USER_DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_URL): TextSelector( + TextSelectorConfig(type=TextSelectorType.URL) + ), + } +) + + +class OllamaConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for Ollama.""" + + VERSION = 1 + + def __init__(self) -> None: + """Initialize config flow.""" + self.url: str | None = None + self.model: str | None = None + self.client: ollama.AsyncClient | None = None + self.download_task: asyncio.Task | None = None + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle the initial step.""" + user_input = user_input or {} + self.url = user_input.get(CONF_URL, self.url) + self.model = user_input.get(CONF_MODEL, self.model) + + if self.url is None: + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA, last_step=False + ) + + errors = {} + + try: + self.client = ollama.AsyncClient(host=self.url) + async with asyncio.timeout(DEFAULT_TIMEOUT): + response = await self.client.list() + + downloaded_models: set[str] = { + model_info["model"] for model_info in response.get("models", []) + } + except (TimeoutError, httpx.ConnectError): + errors["base"] = "cannot_connect" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + + if errors: + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + ) + + if self.model is None: + # Show models that have been downloaded first, followed by all known + # models (only latest tags). + models_to_list = [ + SelectOptionDict(label=f"{m} (downloaded)", value=m) + for m in sorted(downloaded_models) + ] + [ + SelectOptionDict(label=m, value=f"{m}:latest") + for m in sorted(MODEL_NAMES) + if m not in downloaded_models + ] + model_step_schema = vol.Schema( + { + vol.Required( + CONF_MODEL, description={"suggested_value": DEFAULT_MODEL} + ): SelectSelector( + SelectSelectorConfig(options=models_to_list, custom_value=True) + ), + } + ) + + return self.async_show_form( + step_id="user", + data_schema=model_step_schema, + ) + + if self.model not in downloaded_models: + # Ollama server needs to download model first + return await self.async_step_download() + + return self.async_create_entry( + title=_get_title(self.model), + data={CONF_URL: self.url, CONF_MODEL: self.model}, + ) + + async def async_step_download( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Step to wait for Ollama server to download a model.""" + assert self.model is not None + assert self.client is not None + + if self.download_task is None: + # Tell Ollama server to pull the model. + # The task will block until the model and metadata are fully + # downloaded. + self.download_task = self.hass.async_create_background_task( + self.client.pull(self.model), f"Downloading {self.model}" + ) + + if self.download_task.done(): + if err := self.download_task.exception(): + _LOGGER.exception("Unexpected error while downloading model: %s", err) + return self.async_show_progress_done(next_step_id="failed") + + return self.async_show_progress_done(next_step_id="finish") + + return self.async_show_progress( + step_id="download", + progress_action="download", + progress_task=self.download_task, + ) + + async def async_step_finish( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Step after model downloading has succeeded.""" + assert self.url is not None + assert self.model is not None + + return self.async_create_entry( + title=_get_title(self.model), + data={CONF_URL: self.url, CONF_MODEL: self.model}, + ) + + async def async_step_failed( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Step after model downloading has failed.""" + return self.async_abort(reason="download_failed") + + @staticmethod + def async_get_options_flow( + config_entry: ConfigEntry, + ) -> OptionsFlow: + """Create the options flow.""" + return OllamaOptionsFlow(config_entry) + + +class OllamaOptionsFlow(OptionsFlow): + """Ollama options flow.""" + + def __init__(self, config_entry: ConfigEntry) -> None: + """Initialize options flow.""" + self.config_entry = config_entry + self.url: str = self.config_entry.data[CONF_URL] + self.model: str = self.config_entry.data[CONF_MODEL] + + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Manage the options.""" + if user_input is not None: + return self.async_create_entry( + title=_get_title(self.model), data=user_input + ) + + options = self.config_entry.options or MappingProxyType({}) + schema = ollama_config_option_schema(options) + return self.async_show_form( + step_id="init", + data_schema=vol.Schema(schema), + ) + + +def ollama_config_option_schema(options: MappingProxyType[str, Any]) -> dict: + """Ollama options schema.""" + return { + vol.Optional( + CONF_PROMPT, + description={"suggested_value": options.get(CONF_PROMPT, DEFAULT_PROMPT)}, + ): TemplateSelector(), + vol.Optional( + CONF_MAX_HISTORY, + description={ + "suggested_value": options.get(CONF_MAX_HISTORY, DEFAULT_MAX_HISTORY) + }, + ): NumberSelector( + NumberSelectorConfig( + min=0, max=sys.maxsize, step=1, mode=NumberSelectorMode.BOX + ) + ), + } + + +def _get_title(model: str) -> str: + """Get title for config entry.""" + if model.endswith(":latest"): + model = model.split(":", maxsplit=1)[0] + + return model diff --git a/homeassistant/components/ollama/const.py b/homeassistant/components/ollama/const.py new file mode 100644 index 00000000000..853370066dc --- /dev/null +++ b/homeassistant/components/ollama/const.py @@ -0,0 +1,155 @@ +"""Constants for the Ollama integration.""" + +DOMAIN = "ollama" + +CONF_MODEL = "model" +CONF_PROMPT = "prompt" +DEFAULT_PROMPT = """{%- set used_domains = set([ + "binary_sensor", + "climate", + "cover", + "fan", + "light", + "lock", + "sensor", + "switch", + "weather", +]) %} +{%- set used_attributes = set([ + "temperature", + "current_temperature", + "temperature_unit", + "brightness", + "humidity", + "unit_of_measurement", + "device_class", + "current_position", + "percentage", +]) %} + +This smart home is controlled by Home Assistant. +The current time is {{ now().strftime("%X") }}. +Today's date is {{ now().strftime("%x") }}. + +An overview of the areas and the devices in this smart home: +```yaml +{%- for entity in exposed_entities: %} +{%- if entity.domain not in used_domains: %} + {%- continue %} +{%- endif %} + +- domain: {{ entity.domain }} +{%- if entity.names | length == 1: %} + name: {{ entity.names[0] }} +{%- else: %} + names: +{%- for name in entity.names: %} + - {{ name }} +{%- endfor %} +{%- endif %} +{%- if entity.area_names | length == 1: %} + area: {{ entity.area_names[0] }} +{%- elif entity.area_names: %} + areas: +{%- for area_name in entity.area_names: %} + - {{ area_name }} +{%- endfor %} +{%- endif %} + state: {{ entity.state.state }} + {%- set attributes_key_printed = False %} +{%- for attr_name, attr_value in entity.state.attributes.items(): %} + {%- if attr_name in used_attributes: %} + {%- if not attributes_key_printed: %} + attributes: + {%- set attributes_key_printed = True %} + {%- endif %} + {{ attr_name }}: {{ attr_value }} + {%- endif %} +{%- endfor %} +{%- endfor %} +``` + +Answer the user's questions using the information about this smart home. +Keep your answers brief and do not apologize.""" + +KEEP_ALIVE_FOREVER = -1 +DEFAULT_TIMEOUT = 5.0 # seconds + +CONF_MAX_HISTORY = "max_history" +DEFAULT_MAX_HISTORY = 20 + +MAX_HISTORY_SECONDS = 60 * 60 # 1 hour + +MODEL_NAMES = [ # https://ollama.com/library + "gemma", + "llama2", + "mistral", + "mixtral", + "llava", + "neural-chat", + "codellama", + "dolphin-mixtral", + "qwen", + "llama2-uncensored", + "mistral-openorca", + "deepseek-coder", + "nous-hermes2", + "phi", + "orca-mini", + "dolphin-mistral", + "wizard-vicuna-uncensored", + "vicuna", + "tinydolphin", + "llama2-chinese", + "nomic-embed-text", + "openhermes", + "zephyr", + "tinyllama", + "openchat", + "wizardcoder", + "starcoder", + "phind-codellama", + "starcoder2", + "yi", + "orca2", + "falcon", + "wizard-math", + "dolphin-phi", + "starling-lm", + "nous-hermes", + "stable-code", + "medllama2", + "bakllava", + "codeup", + "wizardlm-uncensored", + "solar", + "everythinglm", + "sqlcoder", + "dolphincoder", + "nous-hermes2-mixtral", + "stable-beluga", + "yarn-mistral", + "stablelm2", + "samantha-mistral", + "meditron", + "stablelm-zephyr", + "magicoder", + "yarn-llama2", + "llama-pro", + "deepseek-llm", + "wizard-vicuna", + "codebooga", + "mistrallite", + "all-minilm", + "nexusraven", + "open-orca-platypus2", + "goliath", + "notux", + "megadolphin", + "alfred", + "xwinlm", + "wizardlm", + "duckdb-nsql", + "notus", +] +DEFAULT_MODEL = "llama2:latest" diff --git a/homeassistant/components/ollama/manifest.json b/homeassistant/components/ollama/manifest.json new file mode 100644 index 00000000000..6b16ae667f1 --- /dev/null +++ b/homeassistant/components/ollama/manifest.json @@ -0,0 +1,11 @@ +{ + "domain": "ollama", + "name": "Ollama", + "codeowners": ["@synesthesiam"], + "config_flow": true, + "dependencies": ["conversation"], + "documentation": "https://www.home-assistant.io/integrations/ollama", + "integration_type": "service", + "iot_class": "local_polling", + "requirements": ["ollama-hass==0.1.7"] +} diff --git a/homeassistant/components/ollama/models.py b/homeassistant/components/ollama/models.py new file mode 100644 index 00000000000..ce0f858bb8c --- /dev/null +++ b/homeassistant/components/ollama/models.py @@ -0,0 +1,47 @@ +"""Models for Ollama integration.""" + +from dataclasses import dataclass +from enum import StrEnum +from functools import cached_property + +import ollama + +from homeassistant.core import State + + +class MessageRole(StrEnum): + """Role of a chat message.""" + + SYSTEM = "system" # prompt + USER = "user" + + +@dataclass +class MessageHistory: + """Chat message history.""" + + timestamp: float + """Timestamp of last use in seconds.""" + + messages: list[ollama.Message] + """List of message history, including system prompt and assistant responses.""" + + @property + def num_user_messages(self) -> int: + """Return a count of user messages.""" + return sum(m["role"] == MessageRole.USER for m in self.messages) + + +@dataclass(frozen=True) +class ExposedEntity: + """Relevant information about an exposed entity.""" + + entity_id: str + state: State + names: list[str] + area_names: list[str] + + @cached_property + def domain(self) -> str: + """Get domain from entity id.""" + return self.entity_id.split(".", maxsplit=1)[0] diff --git a/homeassistant/components/ollama/strings.json b/homeassistant/components/ollama/strings.json new file mode 100644 index 00000000000..59f48929681 --- /dev/null +++ b/homeassistant/components/ollama/strings.json @@ -0,0 +1,33 @@ +{ + "config": { + "step": { + "user": { + "data": { + "url": "[%key:common::config_flow::data::url%]", + "model": "Model" + } + }, + "download": { + "title": "Downloading model" + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "download_failed": "Model downloading failed", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "progress": { + "download": "Please wait while the model is downloaded, which may take a very long time. Check your Ollama server logs for more details." + } + }, + "options": { + "step": { + "init": { + "data": { + "prompt": "Prompt template", + "max_history": "Max history messages" + } + } + } + } +} diff --git a/homeassistant/components/ombi/__init__.py b/homeassistant/components/ombi/__init__.py index b67b097dfbf..719efdc8ae3 100644 --- a/homeassistant/components/ombi/__init__.py +++ b/homeassistant/components/ombi/__init__.py @@ -1,4 +1,5 @@ """Support for Ombi.""" + import logging import pyombi diff --git a/homeassistant/components/ombi/const.py b/homeassistant/components/ombi/const.py index 59a57a480c2..6616cd9219d 100644 --- a/homeassistant/components/ombi/const.py +++ b/homeassistant/components/ombi/const.py @@ -1,4 +1,5 @@ """Support for Ombi.""" + from __future__ import annotations ATTR_SEASON = "season" diff --git a/homeassistant/components/ombi/icons.json b/homeassistant/components/ombi/icons.json new file mode 100644 index 00000000000..4b3e32a1e13 --- /dev/null +++ b/homeassistant/components/ombi/icons.json @@ -0,0 +1,7 @@ +{ + "services": { + "submit_movie_request": "mdi:movie-roll", + "submit_tv_request": "mdi:television-classic", + "submit_music_request": "mdi:music" + } +} diff --git a/homeassistant/components/ombi/sensor.py b/homeassistant/components/ombi/sensor.py index f534144d02c..ab9df9ad111 100644 --- a/homeassistant/components/ombi/sensor.py +++ b/homeassistant/components/ombi/sensor.py @@ -1,4 +1,5 @@ """Support for Ombi.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/omnilogic/__init__.py b/homeassistant/components/omnilogic/__init__.py index 27f145f82b6..d9966290986 100644 --- a/homeassistant/components/omnilogic/__init__.py +++ b/homeassistant/components/omnilogic/__init__.py @@ -1,4 +1,5 @@ """The Omnilogic integration.""" + import logging from omnilogic import LoginException, OmniLogic, OmniLogicException diff --git a/homeassistant/components/omnilogic/common.py b/homeassistant/components/omnilogic/common.py index 3fbd53d20f2..adc87e7be26 100644 --- a/homeassistant/components/omnilogic/common.py +++ b/homeassistant/components/omnilogic/common.py @@ -60,7 +60,7 @@ class OmniLogicUpdateCoordinator(DataUpdateCoordinator[dict[tuple, dict[str, Any if "systemId" in item: system_id = item["systemId"] - current_id = current_id + (item_kind, system_id) + current_id = (*current_id, item_kind, system_id) data[current_id] = item for kind in ALL_ITEM_KINDS: diff --git a/homeassistant/components/omnilogic/config_flow.py b/homeassistant/components/omnilogic/config_flow.py index 1635eaa7558..3f3acc3c100 100644 --- a/homeassistant/components/omnilogic/config_flow.py +++ b/homeassistant/components/omnilogic/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Omnilogic integration.""" + from __future__ import annotations import logging @@ -6,7 +7,7 @@ import logging from omnilogic import LoginException, OmniLogic, OmniLogicException import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import callback from homeassistant.helpers import aiohttp_client @@ -16,7 +17,7 @@ from .const import CONF_SCAN_INTERVAL, DEFAULT_PH_OFFSET, DEFAULT_SCAN_INTERVAL, _LOGGER = logging.getLogger(__name__) -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class OmniLogicConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Omnilogic.""" VERSION = 1 @@ -24,7 +25,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback def async_get_options_flow( - config_entry: config_entries.ConfigEntry, + config_entry: ConfigEntry, ) -> OptionsFlowHandler: """Get the options flow for this handler.""" return OptionsFlowHandler(config_entry) @@ -72,10 +73,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -class OptionsFlowHandler(config_entries.OptionsFlow): +class OptionsFlowHandler(OptionsFlow): """Handle Omnilogic client options.""" - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + def __init__(self, config_entry: ConfigEntry) -> None: """Initialize options flow.""" self.config_entry = config_entry diff --git a/homeassistant/components/omnilogic/icons.json b/homeassistant/components/omnilogic/icons.json new file mode 100644 index 00000000000..ee5b5102177 --- /dev/null +++ b/homeassistant/components/omnilogic/icons.json @@ -0,0 +1,5 @@ +{ + "services": { + "set_pump_speed": "mdi:water-pump" + } +} diff --git a/homeassistant/components/omnilogic/sensor.py b/homeassistant/components/omnilogic/sensor.py index be082584308..5eb5a5dd0c4 100644 --- a/homeassistant/components/omnilogic/sensor.py +++ b/homeassistant/components/omnilogic/sensor.py @@ -1,4 +1,5 @@ """Definition and setup of the Omnilogic Sensors for Home Assistant.""" + from typing import Any from homeassistant.components.sensor import SensorDeviceClass, SensorEntity diff --git a/homeassistant/components/omnilogic/switch.py b/homeassistant/components/omnilogic/switch.py index dfefd17bf11..9bdc59a14c8 100644 --- a/homeassistant/components/omnilogic/switch.py +++ b/homeassistant/components/omnilogic/switch.py @@ -1,4 +1,5 @@ """Platform for Omnilogic switch integration.""" + import time from typing import Any diff --git a/homeassistant/components/onboarding/__init__.py b/homeassistant/components/onboarding/__init__.py index 4243d05c085..c11bd79c377 100644 --- a/homeassistant/components/onboarding/__init__.py +++ b/homeassistant/components/onboarding/__init__.py @@ -1,7 +1,10 @@ """Support to help onboard new users.""" + from __future__ import annotations -from typing import TYPE_CHECKING +from collections.abc import Callable +from dataclasses import dataclass +from typing import TYPE_CHECKING, TypedDict from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv @@ -25,15 +28,30 @@ STORAGE_VERSION = 4 CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN) -class OnboadingStorage(Store[dict[str, list[str]]]): +@dataclass +class OnboardingData: + """Container for onboarding data.""" + + listeners: list[Callable[[], None]] + onboarded: bool + steps: OnboardingStoreData + + +class OnboardingStoreData(TypedDict): + """Onboarding store data.""" + + done: list[str] + + +class OnboardingStorage(Store[OnboardingStoreData]): """Store onboarding data.""" async def _async_migrate_func( self, old_major_version: int, old_minor_version: int, - old_data: dict[str, list[str]], - ) -> dict[str, list[str]]: + old_data: OnboardingStoreData, + ) -> OnboardingStoreData: """Migrate to the new version.""" # From version 1 -> 2, we automatically mark the integration step done if old_major_version < 2: @@ -49,21 +67,37 @@ class OnboadingStorage(Store[dict[str, list[str]]]): @callback def async_is_onboarded(hass: HomeAssistant) -> bool: """Return if Home Assistant has been onboarded.""" - data = hass.data.get(DOMAIN) - return data is None or data is True + data: OnboardingData | None = hass.data.get(DOMAIN) + return data is None or data.onboarded is True @bind_hass @callback def async_is_user_onboarded(hass: HomeAssistant) -> bool: """Return if a user has been created as part of onboarding.""" - return async_is_onboarded(hass) or STEP_USER in hass.data[DOMAIN]["done"] + return async_is_onboarded(hass) or STEP_USER in hass.data[DOMAIN].steps["done"] + + +@callback +def async_add_listener(hass: HomeAssistant, listener: Callable[[], None]) -> None: + """Add a listener to be called when onboarding is complete.""" + data: OnboardingData | None = hass.data.get(DOMAIN) + + if not data: + # Onboarding not active + return + + if data.onboarded: + listener() + return + + data.listeners.append(listener) async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the onboarding component.""" - store = OnboadingStorage(hass, STORAGE_VERSION, STORAGE_KEY, private=True) - data: dict[str, list[str]] | None + store = OnboardingStorage(hass, STORAGE_VERSION, STORAGE_KEY, private=True) + data: OnboardingStoreData | None if (data := await store.async_load()) is None: data = {"done": []} @@ -87,7 +121,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: if set(data["done"]) == set(STEPS): return True - hass.data[DOMAIN] = data + hass.data[DOMAIN] = OnboardingData([], False, data) await views.async_setup(hass, data, store) diff --git a/homeassistant/components/onboarding/const.py b/homeassistant/components/onboarding/const.py index 5a771f524ac..7ccfc13774d 100644 --- a/homeassistant/components/onboarding/const.py +++ b/homeassistant/components/onboarding/const.py @@ -1,4 +1,5 @@ """Constants for the onboarding component.""" + DOMAIN = "onboarding" STEP_USER = "user" STEP_CORE_CONFIG = "core_config" diff --git a/homeassistant/components/onboarding/manifest.json b/homeassistant/components/onboarding/manifest.json index 466292eb2e0..918d845993a 100644 --- a/homeassistant/components/onboarding/manifest.json +++ b/homeassistant/components/onboarding/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Onboarding", "after_dependencies": ["hassio"], "codeowners": ["@home-assistant/core"], - "dependencies": ["analytics", "auth", "http", "person"], + "dependencies": ["auth", "http", "person"], "documentation": "https://www.home-assistant.io/integrations/onboarding", "integration_type": "system", "quality_scale": "internal" diff --git a/homeassistant/components/onboarding/strings.json b/homeassistant/components/onboarding/strings.json index 9e3806927d2..bc8bb6e54ff 100644 --- a/homeassistant/components/onboarding/strings.json +++ b/homeassistant/components/onboarding/strings.json @@ -3,5 +3,8 @@ "living_room": "Living Room", "bedroom": "Bedroom", "kitchen": "Kitchen" + }, + "dashboard": { + "map": { "title": "Map" } } } diff --git a/homeassistant/components/onboarding/views.py b/homeassistant/components/onboarding/views.py index e1edfa82a62..1ecfc10d974 100644 --- a/homeassistant/components/onboarding/views.py +++ b/homeassistant/components/onboarding/views.py @@ -1,7 +1,9 @@ """Onboarding views.""" + from __future__ import annotations import asyncio +from collections.abc import Coroutine from http import HTTPStatus from typing import TYPE_CHECKING, Any, cast @@ -13,16 +15,18 @@ from homeassistant.auth.const import GROUP_ID_ADMIN from homeassistant.auth.providers.homeassistant import HassAuthProvider from homeassistant.components import person from homeassistant.components.auth import indieauth -from homeassistant.components.http import KEY_HASS_REFRESH_TOKEN_ID +from homeassistant.components.http import KEY_HASS, KEY_HASS_REFRESH_TOKEN_ID from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.components.http.view import HomeAssistantView from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import area_registry as ar from homeassistant.helpers.system_info import async_get_system_info from homeassistant.helpers.translation import async_get_translations +from homeassistant.setup import async_setup_component +from homeassistant.util.async_ import create_eager_task if TYPE_CHECKING: - from . import OnboadingStorage + from . import OnboardingData, OnboardingStorage, OnboardingStoreData from .const import ( DEFAULT_AREAS, @@ -36,7 +40,7 @@ from .const import ( async def async_setup( - hass: HomeAssistant, data: dict[str, list[str]], store: OnboadingStorage + hass: HomeAssistant, data: OnboardingStoreData, store: OnboardingStorage ) -> None: """Set up the onboarding view.""" hass.http.register_view(OnboardingView(data, store)) @@ -54,7 +58,7 @@ class OnboardingView(HomeAssistantView): url = "/api/onboarding" name = "api:onboarding" - def __init__(self, data: dict[str, list[str]], store: OnboadingStorage) -> None: + def __init__(self, data: OnboardingStoreData, store: OnboardingStorage) -> None: """Initialize the onboarding view.""" self._store = store self._data = data @@ -73,16 +77,16 @@ class InstallationTypeOnboardingView(HomeAssistantView): url = "/api/onboarding/installation_type" name = "api:onboarding:installation_type" - def __init__(self, data: dict[str, list[str]]) -> None: + def __init__(self, data: OnboardingStoreData) -> None: """Initialize the onboarding installation type view.""" self._data = data async def get(self, request: web.Request) -> web.Response: """Return the onboarding status.""" if self._data["done"]: - raise HTTPUnauthorized() + raise HTTPUnauthorized - hass: HomeAssistant = request.app["hass"] + hass = request.app[KEY_HASS] info = await async_get_system_info(hass) return self.json({"installation_type": info["installation_type"]}) @@ -92,7 +96,7 @@ class _BaseOnboardingView(HomeAssistantView): step: str - def __init__(self, data: dict[str, list[str]], store: OnboadingStorage) -> None: + def __init__(self, data: OnboardingStoreData, store: OnboardingStorage) -> None: """Initialize the onboarding view.""" self._store = store self._data = data @@ -109,7 +113,10 @@ class _BaseOnboardingView(HomeAssistantView): await self._store.async_save(self._data) if set(self._data["done"]) == set(STEPS): - hass.data[DOMAIN] = True + data: OnboardingData = hass.data[DOMAIN] + data.onboarded = True + for listener in data.listeners: + listener() class UserOnboardingView(_BaseOnboardingView): @@ -133,7 +140,7 @@ class UserOnboardingView(_BaseOnboardingView): ) async def post(self, request: web.Request, data: dict[str, str]) -> web.Response: """Handle user creation, area creation.""" - hass: HomeAssistant = request.app["hass"] + hass = request.app[KEY_HASS] async with self._lock: if self._async_is_done(): @@ -187,7 +194,7 @@ class CoreConfigOnboardingView(_BaseOnboardingView): async def post(self, request: web.Request) -> web.Response: """Handle finishing core config step.""" - hass: HomeAssistant = request.app["hass"] + hass = request.app[KEY_HASS] async with self._lock: if self._async_is_done(): @@ -215,15 +222,22 @@ class CoreConfigOnboardingView(_BaseOnboardingView): ): onboard_integrations.append("rpi_power") - # Set up integrations after onboarding - await asyncio.gather( - *( - hass.config_entries.flow.async_init( - domain, context={"source": "onboarding"} - ) - for domain in onboard_integrations + coros: list[Coroutine[Any, Any, Any]] = [ + hass.config_entries.flow.async_init( + domain, context={"source": "onboarding"} ) - ) + for domain in onboard_integrations + ] + + if "analytics" not in hass.config.components: + # If by some chance that analytics has not finished + # setting up, wait for it here so its ready for the + # next step. + coros.append(async_setup_component(hass, "analytics", {})) + + # Set up integrations after onboarding and ensure + # analytics is ready for the next step. + await asyncio.gather(*(create_eager_task(coro) for coro in coros)) return self.json({}) @@ -240,7 +254,7 @@ class IntegrationOnboardingView(_BaseOnboardingView): ) async def post(self, request: web.Request, data: dict[str, Any]) -> web.Response: """Handle token creation.""" - hass: HomeAssistant = request.app["hass"] + hass = request.app[KEY_HASS] refresh_token_id = request[KEY_HASS_REFRESH_TOKEN_ID] async with self._lock: @@ -253,7 +267,7 @@ class IntegrationOnboardingView(_BaseOnboardingView): # Validate client ID and redirect uri if not await indieauth.verify_redirect_uri( - request.app["hass"], data["client_id"], data["redirect_uri"] + request.app[KEY_HASS], data["client_id"], data["redirect_uri"] ): return self.json_message( "invalid client id or redirect uri", HTTPStatus.BAD_REQUEST @@ -284,7 +298,7 @@ class AnalyticsOnboardingView(_BaseOnboardingView): async def post(self, request: web.Request) -> web.Response: """Handle finishing analytics step.""" - hass: HomeAssistant = request.app["hass"] + hass = request.app[KEY_HASS] async with self._lock: if self._async_is_done(): diff --git a/homeassistant/components/oncue/__init__.py b/homeassistant/components/oncue/__init__.py index a87b2a9e02c..b3d59f50321 100644 --- a/homeassistant/components/oncue/__init__.py +++ b/homeassistant/components/oncue/__init__.py @@ -1,4 +1,5 @@ """The Oncue integration.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/oncue/binary_sensor.py b/homeassistant/components/oncue/binary_sensor.py index cfb088ebdb8..8adf422d656 100644 --- a/homeassistant/components/oncue/binary_sensor.py +++ b/homeassistant/components/oncue/binary_sensor.py @@ -1,4 +1,5 @@ """Support for Oncue binary sensors.""" + from __future__ import annotations from aiooncue import OncueDevice diff --git a/homeassistant/components/oncue/config_flow.py b/homeassistant/components/oncue/config_flow.py index cedb4feb7a4..ba672dcc588 100644 --- a/homeassistant/components/oncue/config_flow.py +++ b/homeassistant/components/oncue/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Oncue integration.""" + from __future__ import annotations import logging @@ -7,9 +8,8 @@ from typing import Any from aiooncue import LoginFailedException, Oncue import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import CONNECTION_EXCEPTIONS, DOMAIN @@ -17,14 +17,14 @@ from .const import CONNECTION_EXCEPTIONS, DOMAIN _LOGGER = logging.getLogger(__name__) -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class OncueConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Oncue.""" VERSION = 1 async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors = {} diff --git a/homeassistant/components/oncue/const.py b/homeassistant/components/oncue/const.py index 599ef5ee22b..bc14133b0d3 100644 --- a/homeassistant/components/oncue/const.py +++ b/homeassistant/components/oncue/const.py @@ -1,6 +1,5 @@ """Constants for the Oncue integration.""" - import aiohttp from aiooncue import ServiceFailedException diff --git a/homeassistant/components/oncue/entity.py b/homeassistant/components/oncue/entity.py index 6d988d4aaaf..55bd86d8912 100644 --- a/homeassistant/components/oncue/entity.py +++ b/homeassistant/components/oncue/entity.py @@ -1,4 +1,5 @@ """Support for Oncue sensors.""" + from __future__ import annotations from aiooncue import OncueDevice, OncueSensor diff --git a/homeassistant/components/oncue/manifest.json b/homeassistant/components/oncue/manifest.json index 24414e4efb8..b4c425a1645 100644 --- a/homeassistant/components/oncue/manifest.json +++ b/homeassistant/components/oncue/manifest.json @@ -12,5 +12,5 @@ "documentation": "https://www.home-assistant.io/integrations/oncue", "iot_class": "cloud_polling", "loggers": ["aiooncue"], - "requirements": ["aiooncue==0.3.5"] + "requirements": ["aiooncue==0.3.7"] } diff --git a/homeassistant/components/oncue/sensor.py b/homeassistant/components/oncue/sensor.py index d4d45264396..f79beed38b2 100644 --- a/homeassistant/components/oncue/sensor.py +++ b/homeassistant/components/oncue/sensor.py @@ -1,4 +1,5 @@ """Support for Oncue sensors.""" + from __future__ import annotations from aiooncue import OncueDevice, OncueSensor diff --git a/homeassistant/components/ondilo_ico/api.py b/homeassistant/components/ondilo_ico/api.py index e0c6f9001c4..621750c2f58 100644 --- a/homeassistant/components/ondilo_ico/api.py +++ b/homeassistant/components/ondilo_ico/api.py @@ -1,4 +1,5 @@ """API for Ondilo ICO bound to Home Assistant OAuth.""" + from asyncio import run_coroutine_threadsafe import logging from typing import Any diff --git a/homeassistant/components/ondilo_ico/config_flow.py b/homeassistant/components/ondilo_ico/config_flow.py index 503f3936303..5a0fe8c21a5 100644 --- a/homeassistant/components/ondilo_ico/config_flow.py +++ b/homeassistant/components/ondilo_ico/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Ondilo ICO.""" + import logging from homeassistant.helpers import config_entry_oauth2_flow diff --git a/homeassistant/components/ondilo_ico/icons.json b/homeassistant/components/ondilo_ico/icons.json new file mode 100644 index 00000000000..9319b747b28 --- /dev/null +++ b/homeassistant/components/ondilo_ico/icons.json @@ -0,0 +1,21 @@ +{ + "entity": { + "sensor": { + "oxydo_reduction_potential": { + "default": "mdi:pool" + }, + "ph": { + "default": "mdi:pool" + }, + "tds": { + "default": "mdi:pool" + }, + "rssi": { + "default": "mdi:wifi" + }, + "salt": { + "default": "mdi:pool" + } + } + } +} diff --git a/homeassistant/components/ondilo_ico/manifest.json b/homeassistant/components/ondilo_ico/manifest.json index 5a8515ddf2e..1d41eb04d86 100644 --- a/homeassistant/components/ondilo_ico/manifest.json +++ b/homeassistant/components/ondilo_ico/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/ondilo_ico", "iot_class": "cloud_polling", "loggers": ["ondilo"], - "requirements": ["ondilo==0.2.0"] + "requirements": ["ondilo==0.4.0"] } diff --git a/homeassistant/components/ondilo_ico/sensor.py b/homeassistant/components/ondilo_ico/sensor.py index 90c79003b8a..17569fd784f 100644 --- a/homeassistant/components/ondilo_ico/sensor.py +++ b/homeassistant/components/ondilo_ico/sensor.py @@ -1,4 +1,5 @@ """Platform for sensor integration.""" + from __future__ import annotations from datetime import timedelta @@ -43,20 +44,17 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key="orp", translation_key="oxydo_reduction_potential", native_unit_of_measurement=UnitOfElectricPotential.MILLIVOLT, - icon="mdi:pool", state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="ph", translation_key="ph", - icon="mdi:pool", state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="tds", translation_key="tds", native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, - icon="mdi:pool", state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( @@ -68,7 +66,6 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="rssi", translation_key="rssi", - icon="mdi:wifi", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, ), @@ -76,7 +73,6 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key="salt", translation_key="salt", native_unit_of_measurement="mg/L", - icon="mdi:pool", state_class=SensorStateClass.MEASUREMENT, ), ) diff --git a/homeassistant/components/onewire/__init__.py b/homeassistant/components/onewire/__init__.py index e3454a5eb5c..72119915246 100644 --- a/homeassistant/components/onewire/__init__.py +++ b/homeassistant/components/onewire/__init__.py @@ -1,4 +1,5 @@ """The 1-Wire component.""" + import logging from pyownet import protocol @@ -25,7 +26,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: CannotConnect, # Failed to connect to the server protocol.OwnetError, # Connected to server, but failed to list the devices ) as exc: - raise ConfigEntryNotReady() from exc + raise ConfigEntryNotReady from exc hass.data[DOMAIN][entry.entry_id] = onewire_hub diff --git a/homeassistant/components/onewire/binary_sensor.py b/homeassistant/components/onewire/binary_sensor.py index 5cd3fa65a60..fea78fd3760 100644 --- a/homeassistant/components/onewire/binary_sensor.py +++ b/homeassistant/components/onewire/binary_sensor.py @@ -1,4 +1,5 @@ """Support for 1-Wire binary sensors.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/homeassistant/components/onewire/config_flow.py b/homeassistant/components/onewire/config_flow.py index 4764e3b2a55..a217674e3b4 100644 --- a/homeassistant/components/onewire/config_flow.py +++ b/homeassistant/components/onewire/config_flow.py @@ -1,4 +1,5 @@ """Config flow for 1-Wire component.""" + from __future__ import annotations from typing import Any @@ -8,11 +9,11 @@ import voluptuous as vol from homeassistant.config_entries import ( ConfigEntry, ConfigFlow, + ConfigFlowResult, OptionsFlowWithConfigEntry, ) from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.core import HomeAssistant, callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_validation as cv, device_registry as dr from homeassistant.helpers.device_registry import DeviceEntry @@ -65,7 +66,7 @@ class OneWireFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle 1-Wire config flow start. Let user manually input configuration. @@ -124,7 +125,7 @@ class OnewireOptionsFlowHandler(OptionsFlowWithConfigEntry): async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Manage the options.""" device_registry = dr.async_get(self.hass) self.configurable_devices = { @@ -142,7 +143,7 @@ class OnewireOptionsFlowHandler(OptionsFlowWithConfigEntry): async def async_step_device_selection( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Select what devices to configure.""" errors = {} if user_input is not None: @@ -187,7 +188,7 @@ class OnewireOptionsFlowHandler(OptionsFlowWithConfigEntry): async def async_step_configure_device( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Config precision option for device.""" if user_input is not None: self._update_device_options(user_input) diff --git a/homeassistant/components/onewire/const.py b/homeassistant/components/onewire/const.py index ed744caee17..a4f3ebe9a78 100644 --- a/homeassistant/components/onewire/const.py +++ b/homeassistant/components/onewire/const.py @@ -1,4 +1,5 @@ """Constants for 1-Wire component.""" + from __future__ import annotations from homeassistant.const import Platform @@ -27,6 +28,7 @@ DEVICE_SUPPORT = { "3B": (), "42": (), "7E": ("EDS0066", "EDS0068"), + "A6": (), "EF": ("HB_HUB", "HB_MOISTURE_METER", "HobbyBoards_EF"), } diff --git a/homeassistant/components/onewire/diagnostics.py b/homeassistant/components/onewire/diagnostics.py index 36db7fd5360..387553849f3 100644 --- a/homeassistant/components/onewire/diagnostics.py +++ b/homeassistant/components/onewire/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for 1-Wire.""" + from __future__ import annotations from dataclasses import asdict diff --git a/homeassistant/components/onewire/model.py b/homeassistant/components/onewire/model.py index 6e134fd8466..9deaca2d121 100644 --- a/homeassistant/components/onewire/model.py +++ b/homeassistant/components/onewire/model.py @@ -1,4 +1,5 @@ """Type definitions for 1-Wire integration.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/homeassistant/components/onewire/onewire_entities.py b/homeassistant/components/onewire/onewire_entities.py index cad55234181..03ed2dd679a 100644 --- a/homeassistant/components/onewire/onewire_entities.py +++ b/homeassistant/components/onewire/onewire_entities.py @@ -1,4 +1,5 @@ """Support for 1-Wire entities.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/homeassistant/components/onewire/onewirehub.py b/homeassistant/components/onewire/onewirehub.py index d0e2a0f1706..b01cc6ba3d6 100644 --- a/homeassistant/components/onewire/onewirehub.py +++ b/homeassistant/components/onewire/onewirehub.py @@ -1,4 +1,5 @@ """Hub for communication with 1-Wire server or mount_dir.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/onewire/sensor.py b/homeassistant/components/onewire/sensor.py index a7d199c21a9..d32afce7fa9 100644 --- a/homeassistant/components/onewire/sensor.py +++ b/homeassistant/components/onewire/sensor.py @@ -1,4 +1,5 @@ """Support for 1-Wire environment sensors.""" + from __future__ import annotations from collections.abc import Callable, Mapping @@ -382,6 +383,9 @@ def get_entities( elif "7E" in family: device_sub_type = "EDS" family = device_type + elif "A6" in family: + # A6 is a secondary family code for DS2438 + family = "26" if family not in get_sensor_types(device_sub_type): continue diff --git a/homeassistant/components/onewire/switch.py b/homeassistant/components/onewire/switch.py index c63198ccf05..cdf1315394e 100644 --- a/homeassistant/components/onewire/switch.py +++ b/homeassistant/components/onewire/switch.py @@ -1,4 +1,5 @@ """Support for 1-Wire environment switches.""" + from __future__ import annotations from dataclasses import dataclass @@ -180,6 +181,9 @@ def get_entities(onewire_hub: OneWireHub) -> list[OneWireSwitch]: if "EF" in family: device_sub_type = "HobbyBoard" family = device_type + elif "A6" in family: + # A6 is a secondary family code for DS2438 + family = "26" if family not in get_sensor_types(device_sub_type): continue diff --git a/homeassistant/components/onkyo/media_player.py b/homeassistant/components/onkyo/media_player.py index 4d6d0f6965d..ef0105bd6d2 100644 --- a/homeassistant/components/onkyo/media_player.py +++ b/homeassistant/components/onkyo/media_player.py @@ -1,4 +1,5 @@ """Support for Onkyo Receivers.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/onvif/__init__.py b/homeassistant/components/onvif/__init__.py index ea6cd542fea..02e7e28ea18 100644 --- a/homeassistant/components/onvif/__init__.py +++ b/homeassistant/components/onvif/__init__.py @@ -1,4 +1,5 @@ """The ONVIF integration.""" + import asyncio from contextlib import suppress from http import HTTPStatus @@ -86,7 +87,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise ConfigEntryNotReady(f"Setup was unexpectedly canceled: {err}") from err if not device.available: - raise ConfigEntryNotReady() + raise ConfigEntryNotReady hass.data[DOMAIN][entry.unique_id] = device diff --git a/homeassistant/components/onvif/base.py b/homeassistant/components/onvif/base.py index 5f8a7d978d1..c9900106256 100644 --- a/homeassistant/components/onvif/base.py +++ b/homeassistant/components/onvif/base.py @@ -1,4 +1,5 @@ """Base classes for ONVIF entities.""" + from __future__ import annotations from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo diff --git a/homeassistant/components/onvif/binary_sensor.py b/homeassistant/components/onvif/binary_sensor.py index 3676e3b6c27..4aa4d81e055 100644 --- a/homeassistant/components/onvif/binary_sensor.py +++ b/homeassistant/components/onvif/binary_sensor.py @@ -1,4 +1,5 @@ """Support for ONVIF binary sensors.""" + from __future__ import annotations from homeassistant.components.binary_sensor import ( diff --git a/homeassistant/components/onvif/button.py b/homeassistant/components/onvif/button.py index f263821a460..1e86b73fc66 100644 --- a/homeassistant/components/onvif/button.py +++ b/homeassistant/components/onvif/button.py @@ -1,4 +1,5 @@ """ONVIF Buttons.""" + from homeassistant.components.button import ButtonDeviceClass, ButtonEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory diff --git a/homeassistant/components/onvif/camera.py b/homeassistant/components/onvif/camera.py index c6ee74c2c50..4b6dfa1a625 100644 --- a/homeassistant/components/onvif/camera.py +++ b/homeassistant/components/onvif/camera.py @@ -1,4 +1,5 @@ """Support for ONVIF Cameras with FFmpeg as decoder.""" + from __future__ import annotations import asyncio @@ -104,9 +105,9 @@ class ONVIFCameraEntity(ONVIFBaseEntity, Camera): self.stream_options[CONF_RTSP_TRANSPORT] = device.config_entry.options.get( CONF_RTSP_TRANSPORT, next(iter(RTSP_TRANSPORTS)) ) - self.stream_options[ - CONF_USE_WALLCLOCK_AS_TIMESTAMPS - ] = device.config_entry.options.get(CONF_USE_WALLCLOCK_AS_TIMESTAMPS, False) + self.stream_options[CONF_USE_WALLCLOCK_AS_TIMESTAMPS] = ( + device.config_entry.options.get(CONF_USE_WALLCLOCK_AS_TIMESTAMPS, False) + ) self._basic_auth = ( device.config_entry.data.get(CONF_SNAPSHOT_AUTH) == HTTP_BASIC_AUTHENTICATION diff --git a/homeassistant/components/onvif/config_flow.py b/homeassistant/components/onvif/config_flow.py index 9688a78bf3f..515f9cd5f68 100644 --- a/homeassistant/components/onvif/config_flow.py +++ b/homeassistant/components/onvif/config_flow.py @@ -1,4 +1,5 @@ """Config flow for ONVIF.""" + from __future__ import annotations from collections.abc import Mapping @@ -14,7 +15,6 @@ from wsdiscovery.scope import Scope from wsdiscovery.service import Service from zeep.exceptions import Fault -from homeassistant import config_entries from homeassistant.components import dhcp from homeassistant.components.ffmpeg import CONF_EXTRA_ARGUMENTS from homeassistant.components.stream import ( @@ -22,6 +22,13 @@ from homeassistant.components.stream import ( CONF_USE_WALLCLOCK_AS_TIMESTAMPS, RTSP_TRANSPORTS, ) +from homeassistant.config_entries import ( + ConfigEntry, + ConfigEntryState, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.const import ( CONF_HOST, CONF_NAME, @@ -30,7 +37,7 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import HomeAssistant, callback -from homeassistant.data_entry_flow import AbortFlow, FlowResult +from homeassistant.data_entry_flow import AbortFlow from homeassistant.helpers import device_registry as dr from .const import ( @@ -91,16 +98,16 @@ async def async_discovery(hass: HomeAssistant) -> list[dict[str, Any]]: return devices -class OnvifFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class OnvifFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a ONVIF config flow.""" VERSION = 1 - _reauth_entry: config_entries.ConfigEntry + _reauth_entry: ConfigEntry @staticmethod @callback def async_get_options_flow( - config_entry: config_entries.ConfigEntry, + config_entry: ConfigEntry, ) -> OnvifOptionsFlowHandler: """Get the options flow for this handler.""" return OnvifOptionsFlowHandler(config_entry) @@ -123,7 +130,9 @@ class OnvifFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): data_schema=vol.Schema({vol.Required("auto", default=True): bool}), ) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Handle re-authentication of an existing config entry.""" reauth_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] @@ -134,7 +143,7 @@ class OnvifFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm reauth.""" entry = self._reauth_entry errors: dict[str, str] | None = {} @@ -161,7 +170,9 @@ class OnvifFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): description_placeholders=description_placeholders, ) - async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: + async def async_step_dhcp( + self, discovery_info: dhcp.DhcpServiceInfo + ) -> ConfigFlowResult: """Handle dhcp discovery.""" hass = self.hass mac = discovery_info.macaddress @@ -176,7 +187,7 @@ class OnvifFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if ( not (entry := hass.config_entries.async_get_entry(entry_id)) or entry.domain != DOMAIN - or entry.state is config_entries.ConfigEntryState.LOADED + or entry.state is ConfigEntryState.LOADED ): continue if hass.config_entries.async_update_entry( @@ -235,7 +246,7 @@ class OnvifFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_configure( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Device configuration.""" errors: dict[str, str] = {} description_placeholders: dict[str, str] = {} @@ -374,10 +385,10 @@ class OnvifFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): await device.close() -class OnvifOptionsFlowHandler(config_entries.OptionsFlow): +class OnvifOptionsFlowHandler(OptionsFlow): """Handle ONVIF options.""" - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + def __init__(self, config_entry: ConfigEntry) -> None: """Initialize ONVIF options flow.""" self.config_entry = config_entry self.options = dict(config_entry.options) diff --git a/homeassistant/components/onvif/const.py b/homeassistant/components/onvif/const.py index 77fa098a316..d191a1710d5 100644 --- a/homeassistant/components/onvif/const.py +++ b/homeassistant/components/onvif/const.py @@ -1,4 +1,5 @@ """Constants for the onvif component.""" + import logging from httpx import RequestError diff --git a/homeassistant/components/onvif/device.py b/homeassistant/components/onvif/device.py index 358cbbf5c83..2001d95e2d4 100644 --- a/homeassistant/components/onvif/device.py +++ b/homeassistant/components/onvif/device.py @@ -1,4 +1,5 @@ """ONVIF device abstraction.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/onvif/diagnostics.py b/homeassistant/components/onvif/diagnostics.py index a802aed5e80..aa2042f3321 100644 --- a/homeassistant/components/onvif/diagnostics.py +++ b/homeassistant/components/onvif/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for ONVIF.""" + from __future__ import annotations from dataclasses import asdict diff --git a/homeassistant/components/onvif/event.py b/homeassistant/components/onvif/event.py index c5539818a1c..9dcdba628e0 100644 --- a/homeassistant/components/onvif/event.py +++ b/homeassistant/components/onvif/event.py @@ -1,4 +1,5 @@ """ONVIF event abstraction.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/onvif/icons.json b/homeassistant/components/onvif/icons.json new file mode 100644 index 00000000000..4db9a9f9e49 --- /dev/null +++ b/homeassistant/components/onvif/icons.json @@ -0,0 +1,18 @@ +{ + "entity": { + "switch": { + "autofocus": { + "default": "mdi:focus-auto" + }, + "ir_lamp": { + "default": "mdi:spotlight-beam" + }, + "wiper": { + "default": "mdi:wiper" + } + } + }, + "services": { + "ptz": "mdi:pan" + } +} diff --git a/homeassistant/components/onvif/models.py b/homeassistant/components/onvif/models.py index 64edc85f3d1..ad91a514e88 100644 --- a/homeassistant/components/onvif/models.py +++ b/homeassistant/components/onvif/models.py @@ -1,4 +1,5 @@ """ONVIF models.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/homeassistant/components/onvif/parsers.py b/homeassistant/components/onvif/parsers.py index 02899dbc1a2..690a3739b4f 100644 --- a/homeassistant/components/onvif/parsers.py +++ b/homeassistant/components/onvif/parsers.py @@ -1,4 +1,5 @@ """ONVIF event parsers.""" + from __future__ import annotations from collections.abc import Callable, Coroutine @@ -11,9 +12,9 @@ from homeassistant.util.decorator import Registry from .models import Event -PARSERS: Registry[ - str, Callable[[str, Any], Coroutine[Any, Any, Event | None]] -] = Registry() +PARSERS: Registry[str, Callable[[str, Any], Coroutine[Any, Any, Event | None]]] = ( + Registry() +) VIDEO_SOURCE_MAPPING = { "vsconf": "VideoSourceToken", diff --git a/homeassistant/components/onvif/sensor.py b/homeassistant/components/onvif/sensor.py index 67da0ed979d..5b0c72e88dd 100644 --- a/homeassistant/components/onvif/sensor.py +++ b/homeassistant/components/onvif/sensor.py @@ -1,4 +1,5 @@ """Support for ONVIF binary sensors.""" + from __future__ import annotations from datetime import date, datetime diff --git a/homeassistant/components/onvif/strings.json b/homeassistant/components/onvif/strings.json index 5a36b89688a..c3f0b89df3b 100644 --- a/homeassistant/components/onvif/strings.json +++ b/homeassistant/components/onvif/strings.json @@ -71,6 +71,19 @@ } } }, + "entity": { + "switch": { + "autofocus": { + "name": "Autofocus" + }, + "ir_lamp": { + "name": "IR lamp" + }, + "wiper": { + "name": "Wiper" + } + } + }, "services": { "ptz": { "name": "PTZ", diff --git a/homeassistant/components/onvif/switch.py b/homeassistant/components/onvif/switch.py index 673f77f558c..02b48d20bef 100644 --- a/homeassistant/components/onvif/switch.py +++ b/homeassistant/components/onvif/switch.py @@ -1,4 +1,5 @@ """ONVIF switches for controlling cameras.""" + from __future__ import annotations from collections.abc import Callable, Coroutine @@ -16,9 +17,9 @@ from .device import ONVIFDevice from .models import Profile -@dataclass(frozen=True) -class ONVIFSwitchEntityDescriptionMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class ONVIFSwitchEntityDescription(SwitchEntityDescription): + """Describes ONVIF switch entity.""" turn_on_fn: Callable[ [ONVIFDevice], Callable[[Profile, Any], Coroutine[Any, Any, None]] @@ -31,18 +32,10 @@ class ONVIFSwitchEntityDescriptionMixin: supported_fn: Callable[[ONVIFDevice], bool] -@dataclass(frozen=True) -class ONVIFSwitchEntityDescription( - SwitchEntityDescription, ONVIFSwitchEntityDescriptionMixin -): - """Describes ONVIF switch entity.""" - - SWITCHES: tuple[ONVIFSwitchEntityDescription, ...] = ( ONVIFSwitchEntityDescription( key="autofocus", - name="Autofocus", - icon="mdi:focus-auto", + translation_key="autofocus", turn_on_data={"Focus": {"AutoFocusMode": "AUTO"}}, turn_off_data={"Focus": {"AutoFocusMode": "MANUAL"}}, turn_on_fn=lambda device: device.async_set_imaging_settings, @@ -51,8 +44,7 @@ SWITCHES: tuple[ONVIFSwitchEntityDescription, ...] = ( ), ONVIFSwitchEntityDescription( key="ir_lamp", - name="IR lamp", - icon="mdi:spotlight-beam", + translation_key="ir_lamp", turn_on_data={"IrCutFilter": "OFF"}, turn_off_data={"IrCutFilter": "ON"}, turn_on_fn=lambda device: device.async_set_imaging_settings, @@ -61,8 +53,7 @@ SWITCHES: tuple[ONVIFSwitchEntityDescription, ...] = ( ), ONVIFSwitchEntityDescription( key="wiper", - name="Wiper", - icon="mdi:wiper", + translation_key="wiper", turn_on_data="tt:Wiper|On", turn_off_data="tt:Wiper|Off", turn_on_fn=lambda device: device.async_run_aux_command, diff --git a/homeassistant/components/onvif/util.py b/homeassistant/components/onvif/util.py index 5077a65e0b0..064d9cfad5f 100644 --- a/homeassistant/components/onvif/util.py +++ b/homeassistant/components/onvif/util.py @@ -1,4 +1,5 @@ """ONVIF util.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/open_meteo/__init__.py b/homeassistant/components/open_meteo/__init__.py index 4dc6a45e16c..ac09b0f61a2 100644 --- a/homeassistant/components/open_meteo/__init__.py +++ b/homeassistant/components/open_meteo/__init__.py @@ -1,4 +1,5 @@ """Support for Open-Meteo.""" + from __future__ import annotations from open_meteo import ( diff --git a/homeassistant/components/open_meteo/config_flow.py b/homeassistant/components/open_meteo/config_flow.py index 7a603f887f0..128e9f17f37 100644 --- a/homeassistant/components/open_meteo/config_flow.py +++ b/homeassistant/components/open_meteo/config_flow.py @@ -1,4 +1,5 @@ """Config flow to configure the Open-Meteo integration.""" + from __future__ import annotations from typing import Any @@ -6,9 +7,8 @@ from typing import Any import voluptuous as vol from homeassistant.components.zone import DOMAIN as ZONE_DOMAIN -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_ZONE -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.selector import EntitySelector, EntitySelectorConfig from .const import DOMAIN @@ -21,7 +21,7 @@ class OpenMeteoFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" if user_input is not None: await self.async_set_unique_id(user_input[CONF_ZONE]) diff --git a/homeassistant/components/open_meteo/const.py b/homeassistant/components/open_meteo/const.py index 94e27293bad..e83fad9d59f 100644 --- a/homeassistant/components/open_meteo/const.py +++ b/homeassistant/components/open_meteo/const.py @@ -1,4 +1,5 @@ """Constants for the Open-Meteo integration.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/open_meteo/diagnostics.py b/homeassistant/components/open_meteo/diagnostics.py index a88325066fe..0ce9f4fcf3d 100644 --- a/homeassistant/components/open_meteo/diagnostics.py +++ b/homeassistant/components/open_meteo/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for Open-Meteo.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/open_meteo/weather.py b/homeassistant/components/open_meteo/weather.py index 3d66422fd60..8ee3edd5183 100644 --- a/homeassistant/components/open_meteo/weather.py +++ b/homeassistant/components/open_meteo/weather.py @@ -1,4 +1,5 @@ """Support for Open-Meteo weather.""" + from __future__ import annotations from open_meteo import Forecast as OpenMeteoForecast @@ -87,9 +88,9 @@ class OpenMeteoWeatherEntity( return None return self.coordinator.data.current_weather.wind_direction - @property - def forecast(self) -> list[Forecast] | None: - """Return the forecast in native units.""" + @callback + def _async_forecast_daily(self) -> list[Forecast] | None: + """Return the daily forecast in native units.""" if self.coordinator.data.daily is None: return None @@ -123,8 +124,3 @@ class OpenMeteoWeatherEntity( forecasts.append(forecast) return forecasts - - @callback - def _async_forecast_daily(self) -> list[Forecast] | None: - """Return the daily forecast in native units.""" - return self.forecast diff --git a/homeassistant/components/openai_conversation/__init__.py b/homeassistant/components/openai_conversation/__init__.py index b0762979ca2..07e872a0f5d 100644 --- a/homeassistant/components/openai_conversation/__init__.py +++ b/homeassistant/components/openai_conversation/__init__.py @@ -1,4 +1,5 @@ """The OpenAI Conversation integration.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/openai_conversation/config_flow.py b/homeassistant/components/openai_conversation/config_flow.py index ef1e498d061..fdbbbc554df 100644 --- a/homeassistant/components/openai_conversation/config_flow.py +++ b/homeassistant/components/openai_conversation/config_flow.py @@ -1,4 +1,5 @@ """Config flow for OpenAI Conversation integration.""" + from __future__ import annotations import logging @@ -9,10 +10,14 @@ from typing import Any import openai import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.selector import ( NumberSelector, NumberSelectorConfig, @@ -61,14 +66,14 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> None: await hass.async_add_executor_job(client.with_options(timeout=10.0).models.list) -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class OpenAIConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for OpenAI Conversation.""" VERSION = 1 async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" if user_input is None: return self.async_show_form( @@ -95,22 +100,22 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod def async_get_options_flow( - config_entry: config_entries.ConfigEntry, - ) -> config_entries.OptionsFlow: + config_entry: ConfigEntry, + ) -> OptionsFlow: """Create the options flow.""" - return OptionsFlow(config_entry) + return OpenAIOptionsFlow(config_entry) -class OptionsFlow(config_entries.OptionsFlow): +class OpenAIOptionsFlow(OptionsFlow): """OpenAI config flow options handler.""" - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + 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: + ) -> ConfigFlowResult: """Manage the options.""" if user_input is not None: return self.async_create_entry(title="OpenAI Conversation", data=user_input) diff --git a/homeassistant/components/openai_conversation/icons.json b/homeassistant/components/openai_conversation/icons.json new file mode 100644 index 00000000000..7f736a5ff3b --- /dev/null +++ b/homeassistant/components/openai_conversation/icons.json @@ -0,0 +1,5 @@ +{ + "services": { + "generate_image": "mdi:image-sync" + } +} diff --git a/homeassistant/components/openalpr_cloud/image_processing.py b/homeassistant/components/openalpr_cloud/image_processing.py index 0dbebda6962..2a8fe328c7d 100644 --- a/homeassistant/components/openalpr_cloud/image_processing.py +++ b/homeassistant/components/openalpr_cloud/image_processing.py @@ -1,4 +1,5 @@ """Component that will help set the OpenALPR cloud for ALPR processing.""" + from __future__ import annotations import asyncio @@ -79,15 +80,12 @@ async def async_setup_platform( "country": config[CONF_REGION], } - entities = [] - for camera in config[CONF_SOURCE]: - entities.append( - OpenAlprCloudEntity( - camera[CONF_ENTITY_ID], params, confidence, camera.get(CONF_NAME) - ) + async_add_entities( + OpenAlprCloudEntity( + camera[CONF_ENTITY_ID], params, confidence, camera.get(CONF_NAME) ) - - async_add_entities(entities) + for camera in config[CONF_SOURCE] + ) class ImageProcessingAlprEntity(ImageProcessingEntity): @@ -141,8 +139,7 @@ class ImageProcessingAlprEntity(ImageProcessingEntity): # Send events for i_plate in new_plates: - self.hass.async_add_job( - self.hass.bus.async_fire, + self.hass.bus.async_fire( EVENT_FOUND_PLATE, { ATTR_PLATE: i_plate, diff --git a/homeassistant/components/opencv/__init__.py b/homeassistant/components/opencv/__init__.py deleted file mode 100644 index 0e4a755b2b9..00000000000 --- a/homeassistant/components/opencv/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The opencv component.""" diff --git a/homeassistant/components/opencv/image_processing.py b/homeassistant/components/opencv/image_processing.py deleted file mode 100644 index 89c1a16aa59..00000000000 --- a/homeassistant/components/opencv/image_processing.py +++ /dev/null @@ -1,198 +0,0 @@ -"""Support for OpenCV classification on images.""" -from __future__ import annotations - -from datetime import timedelta -import logging - -import numpy as np -import requests -import voluptuous as vol - -from homeassistant.components.image_processing import ( - PLATFORM_SCHEMA, - ImageProcessingEntity, -) -from homeassistant.const import CONF_ENTITY_ID, CONF_NAME, CONF_SOURCE -from homeassistant.core import HomeAssistant, split_entity_id -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType - -try: - # Verify that the OpenCV python package is pre-installed - import cv2 - - CV2_IMPORTED = True -except ImportError: - CV2_IMPORTED = False - - -_LOGGER = logging.getLogger(__name__) - -ATTR_MATCHES = "matches" -ATTR_TOTAL_MATCHES = "total_matches" - -CASCADE_URL = ( - "https://raw.githubusercontent.com/opencv/opencv/master/data/" - "lbpcascades/lbpcascade_frontalface.xml" -) - -CONF_CLASSIFIER = "classifier" -CONF_FILE = "file" -CONF_MIN_SIZE = "min_size" -CONF_NEIGHBORS = "neighbors" -CONF_SCALE = "scale" - -DEFAULT_CLASSIFIER_PATH = "lbp_frontalface.xml" -DEFAULT_MIN_SIZE = (30, 30) -DEFAULT_NEIGHBORS = 4 -DEFAULT_SCALE = 1.1 -DEFAULT_TIMEOUT = 10 - -SCAN_INTERVAL = timedelta(seconds=2) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_CLASSIFIER): { - cv.string: vol.Any( - cv.isfile, - vol.Schema( - { - vol.Required(CONF_FILE): cv.isfile, - vol.Optional(CONF_SCALE, DEFAULT_SCALE): float, - vol.Optional( - CONF_NEIGHBORS, DEFAULT_NEIGHBORS - ): cv.positive_int, - vol.Optional(CONF_MIN_SIZE, DEFAULT_MIN_SIZE): vol.Schema( - vol.All(vol.Coerce(tuple), vol.ExactSequence([int, int])) - ), - } - ), - ) - } - } -) - - -def _create_processor_from_config(hass, camera_entity, config): - """Create an OpenCV processor from configuration.""" - classifier_config = config.get(CONF_CLASSIFIER) - name = f"{config[CONF_NAME]} {split_entity_id(camera_entity)[1].replace('_', ' ')}" - - processor = OpenCVImageProcessor(hass, camera_entity, name, classifier_config) - - return processor - - -def _get_default_classifier(dest_path): - """Download the default OpenCV classifier.""" - _LOGGER.info("Downloading default classifier") - req = requests.get(CASCADE_URL, stream=True, timeout=10) - with open(dest_path, "wb") as fil: - for chunk in req.iter_content(chunk_size=1024): - if chunk: # filter out keep-alive new chunks - fil.write(chunk) - - -def setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the OpenCV image processing platform.""" - if not CV2_IMPORTED: - _LOGGER.error( - "No OpenCV library found! Install or compile for your system " - "following instructions here: https://opencv.org/?s=releases" - ) - return - - entities = [] - if CONF_CLASSIFIER not in config: - dest_path = hass.config.path(DEFAULT_CLASSIFIER_PATH) - _get_default_classifier(dest_path) - config[CONF_CLASSIFIER] = {"Face": dest_path} - - for camera in config[CONF_SOURCE]: - entities.append( - OpenCVImageProcessor( - hass, - camera[CONF_ENTITY_ID], - camera.get(CONF_NAME), - config[CONF_CLASSIFIER], - ) - ) - - add_entities(entities) - - -class OpenCVImageProcessor(ImageProcessingEntity): - """Representation of an OpenCV image processor.""" - - def __init__(self, hass, camera_entity, name, classifiers): - """Initialize the OpenCV entity.""" - self.hass = hass - self._camera_entity = camera_entity - if name: - self._name = name - else: - self._name = f"OpenCV {split_entity_id(camera_entity)[1]}" - self._classifiers = classifiers - self._matches = {} - self._total_matches = 0 - self._last_image = None - - @property - def camera_entity(self): - """Return camera entity id from process pictures.""" - return self._camera_entity - - @property - def name(self): - """Return the name of the image processor.""" - return self._name - - @property - def state(self): - """Return the state of the entity.""" - return self._total_matches - - @property - def extra_state_attributes(self): - """Return device specific state attributes.""" - return {ATTR_MATCHES: self._matches, ATTR_TOTAL_MATCHES: self._total_matches} - - def process_image(self, image): - """Process the image.""" - cv_image = cv2.imdecode(np.asarray(bytearray(image)), cv2.IMREAD_UNCHANGED) - - matches = {} - total_matches = 0 - - for name, classifier in self._classifiers.items(): - scale = DEFAULT_SCALE - neighbors = DEFAULT_NEIGHBORS - min_size = DEFAULT_MIN_SIZE - if isinstance(classifier, dict): - path = classifier[CONF_FILE] - scale = classifier.get(CONF_SCALE, scale) - neighbors = classifier.get(CONF_NEIGHBORS, neighbors) - min_size = classifier.get(CONF_MIN_SIZE, min_size) - else: - path = classifier - - cascade = cv2.CascadeClassifier(path) - - detections = cascade.detectMultiScale( - cv_image, scaleFactor=scale, minNeighbors=neighbors, minSize=min_size - ) - regions = [] - for x, y, w, h in detections: - regions.append((int(x), int(y), int(w), int(h))) - total_matches += 1 - - matches[name] = regions - - self._matches = matches - self._total_matches = total_matches diff --git a/homeassistant/components/opencv/manifest.json b/homeassistant/components/opencv/manifest.json deleted file mode 100644 index 3c484385934..00000000000 --- a/homeassistant/components/opencv/manifest.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "domain": "opencv", - "name": "OpenCV", - "codeowners": [], - "documentation": "https://www.home-assistant.io/integrations/opencv", - "iot_class": "local_push", - "requirements": ["numpy==1.26.0", "opencv-python-headless==4.6.0.66"] -} diff --git a/homeassistant/components/openerz/sensor.py b/homeassistant/components/openerz/sensor.py index 4bfe11ee264..7447f2eafe4 100644 --- a/homeassistant/components/openerz/sensor.py +++ b/homeassistant/components/openerz/sensor.py @@ -1,4 +1,5 @@ """Support for OpenERZ API for Zurich city waste disposal system.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/openevse/sensor.py b/homeassistant/components/openevse/sensor.py index aafda0d038b..b2360b13a6f 100644 --- a/homeassistant/components/openevse/sensor.py +++ b/homeassistant/components/openevse/sensor.py @@ -1,4 +1,5 @@ """Support for monitoring an OpenEVSE Charger.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/openexchangerates/__init__.py b/homeassistant/components/openexchangerates/__init__.py index 1b6ab4e65f1..65005235c6b 100644 --- a/homeassistant/components/openexchangerates/__init__.py +++ b/homeassistant/components/openexchangerates/__init__.py @@ -1,4 +1,5 @@ """The Open Exchange Rates integration.""" + from __future__ import annotations from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/openexchangerates/config_flow.py b/homeassistant/components/openexchangerates/config_flow.py index 0425b44d9e6..2fc0acea78d 100644 --- a/homeassistant/components/openexchangerates/config_flow.py +++ b/homeassistant/components/openexchangerates/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Open Exchange Rates integration.""" + from __future__ import annotations import asyncio @@ -12,10 +13,10 @@ from aioopenexchangerates import ( ) import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_API_KEY, CONF_BASE from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import AbortFlow, FlowResult +from homeassistant.data_entry_flow import AbortFlow from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import CLIENT_TIMEOUT, DEFAULT_BASE, DOMAIN, LOGGER @@ -45,7 +46,7 @@ async def validate_input(hass: HomeAssistant, data: dict[str, str]) -> dict[str, return {"title": data[CONF_BASE]} -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class OpenExchangeRatesConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Open Exchange Rates.""" VERSION = 1 @@ -53,11 +54,11 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize the config flow.""" self.currencies: dict[str, str] = {} - self._reauth_entry: config_entries.ConfigEntry | None = None + self._reauth_entry: ConfigEntry | None = None async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" currencies = await self.async_get_currencies() @@ -110,7 +111,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Handle reauth.""" self._reauth_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] diff --git a/homeassistant/components/openexchangerates/const.py b/homeassistant/components/openexchangerates/const.py index 146919cfe44..0dd79158ff7 100644 --- a/homeassistant/components/openexchangerates/const.py +++ b/homeassistant/components/openexchangerates/const.py @@ -1,4 +1,5 @@ """Provide common constants for Open Exchange Rates.""" + from datetime import timedelta import logging diff --git a/homeassistant/components/openexchangerates/coordinator.py b/homeassistant/components/openexchangerates/coordinator.py index beb588c7ce6..627e0d92e32 100644 --- a/homeassistant/components/openexchangerates/coordinator.py +++ b/homeassistant/components/openexchangerates/coordinator.py @@ -1,4 +1,5 @@ """Provide an OpenExchangeRates data coordinator.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/openexchangerates/sensor.py b/homeassistant/components/openexchangerates/sensor.py index 66baf54c16a..55ca7bd2fb9 100644 --- a/homeassistant/components/openexchangerates/sensor.py +++ b/homeassistant/components/openexchangerates/sensor.py @@ -1,4 +1,5 @@ """Support for openexchangerates.org exchange rates service.""" + from __future__ import annotations from homeassistant.components.sensor import SensorEntity diff --git a/homeassistant/components/opengarage/__init__.py b/homeassistant/components/opengarage/__init__.py index 46d018ec1af..adc96ee0946 100644 --- a/homeassistant/components/opengarage/__init__.py +++ b/homeassistant/components/opengarage/__init__.py @@ -1,4 +1,5 @@ """The OpenGarage integration.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/opengarage/binary_sensor.py b/homeassistant/components/opengarage/binary_sensor.py index 22f118ca804..2eca670b990 100644 --- a/homeassistant/components/opengarage/binary_sensor.py +++ b/homeassistant/components/opengarage/binary_sensor.py @@ -1,4 +1,5 @@ """Platform for the opengarage.io binary sensor component.""" + from __future__ import annotations import logging @@ -35,14 +36,12 @@ async def async_setup_entry( entry.entry_id ] async_add_entities( - [ - OpenGarageBinarySensor( - open_garage_data_coordinator, - cast(str, entry.unique_id), - description, - ) - for description in SENSOR_TYPES - ], + OpenGarageBinarySensor( + open_garage_data_coordinator, + cast(str, entry.unique_id), + description, + ) + for description in SENSOR_TYPES ) diff --git a/homeassistant/components/opengarage/button.py b/homeassistant/components/opengarage/button.py index 9f676919098..f3a31d1b050 100644 --- a/homeassistant/components/opengarage/button.py +++ b/homeassistant/components/opengarage/button.py @@ -1,4 +1,5 @@ """OpenGarage button.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/opengarage/config_flow.py b/homeassistant/components/opengarage/config_flow.py index c1b0ff7105e..0b86c563783 100644 --- a/homeassistant/components/opengarage/config_flow.py +++ b/homeassistant/components/opengarage/config_flow.py @@ -1,4 +1,5 @@ """Config flow for OpenGarage integration.""" + from __future__ import annotations import logging @@ -8,10 +9,9 @@ import aiohttp import opengarage import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_PORT, CONF_VERIFY_SSL from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.device_registry import format_mac @@ -53,14 +53,14 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, return {"title": status.get("name"), "unique_id": format_mac(status["mac"])} -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class OpenGarageConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for OpenGarage.""" VERSION = 1 async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" if user_input is None: return self.async_show_form( diff --git a/homeassistant/components/opengarage/cover.py b/homeassistant/components/opengarage/cover.py index 3f3f6b11acf..69338ad4b90 100644 --- a/homeassistant/components/opengarage/cover.py +++ b/homeassistant/components/opengarage/cover.py @@ -1,4 +1,5 @@ """Platform for the opengarage.io cover component.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/opengarage/entity.py b/homeassistant/components/opengarage/entity.py index c8380ea9244..4bf63567fe3 100644 --- a/homeassistant/components/opengarage/entity.py +++ b/homeassistant/components/opengarage/entity.py @@ -1,4 +1,5 @@ """Entity for the opengarage.io component.""" + from __future__ import annotations from homeassistant.core import callback diff --git a/homeassistant/components/opengarage/sensor.py b/homeassistant/components/opengarage/sensor.py index b1d6cb921fa..39b431157ab 100644 --- a/homeassistant/components/opengarage/sensor.py +++ b/homeassistant/components/opengarage/sensor.py @@ -1,4 +1,5 @@ """Platform for the opengarage.io sensor component.""" + from __future__ import annotations import logging @@ -65,15 +66,13 @@ async def async_setup_entry( entry.entry_id ] async_add_entities( - [ - OpenGarageSensor( - open_garage_data_coordinator, - cast(str, entry.unique_id), - description, - ) - for description in SENSOR_TYPES - if description.key in open_garage_data_coordinator.data - ], + OpenGarageSensor( + open_garage_data_coordinator, + cast(str, entry.unique_id), + description, + ) + for description in SENSOR_TYPES + if description.key in open_garage_data_coordinator.data ) diff --git a/homeassistant/components/openhardwaremonitor/sensor.py b/homeassistant/components/openhardwaremonitor/sensor.py index 4206bc72c1d..4e15ca3dd57 100644 --- a/homeassistant/components/openhardwaremonitor/sensor.py +++ b/homeassistant/components/openhardwaremonitor/sensor.py @@ -1,4 +1,5 @@ """Support for Open Hardware Monitor Sensor Platform.""" + from __future__ import annotations from datetime import timedelta @@ -169,7 +170,7 @@ class OpenHardwareMonitorData: result = devices.copy() if json[OHM_CHILDREN]: - for child_index in range(0, len(json[OHM_CHILDREN])): + for child_index in range(len(json[OHM_CHILDREN])): child_path = path.copy() child_path.append(child_index) diff --git a/homeassistant/components/openhome/config_flow.py b/homeassistant/components/openhome/config_flow.py index c8a13a3c7aa..5b26b63922b 100644 --- a/homeassistant/components/openhome/config_flow.py +++ b/homeassistant/components/openhome/config_flow.py @@ -8,9 +8,8 @@ from homeassistant.components.ssdp import ( ATTR_UPNP_UDN, SsdpServiceInfo, ) -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_NAME -from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN @@ -25,7 +24,9 @@ def _is_complete_discovery(discovery_info: SsdpServiceInfo) -> bool: class OpenhomeConfigFlow(ConfigFlow, domain=DOMAIN): """Handle an Openhome config flow.""" - async def async_step_ssdp(self, discovery_info: SsdpServiceInfo) -> FlowResult: + async def async_step_ssdp( + self, discovery_info: SsdpServiceInfo + ) -> ConfigFlowResult: """Handle a flow initialized by discovery.""" _LOGGER.debug("async_step_ssdp: started") @@ -51,7 +52,7 @@ class OpenhomeConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle user-confirmation of discovered node.""" if user_input is not None: diff --git a/homeassistant/components/openhome/const.py b/homeassistant/components/openhome/const.py index 09fcd2ef0e2..4c9925df19e 100644 --- a/homeassistant/components/openhome/const.py +++ b/homeassistant/components/openhome/const.py @@ -1,4 +1,5 @@ """Constants for the Openhome component.""" + DOMAIN = "openhome" SERVICE_INVOKE_PIN = "invoke_pin" ATTR_PIN_INDEX = "pin" diff --git a/homeassistant/components/openhome/icons.json b/homeassistant/components/openhome/icons.json new file mode 100644 index 00000000000..081e97c3489 --- /dev/null +++ b/homeassistant/components/openhome/icons.json @@ -0,0 +1,5 @@ +{ + "services": { + "invoke_pin": "mdi:alarm-panel" + } +} diff --git a/homeassistant/components/openhome/media_player.py b/homeassistant/components/openhome/media_player.py index 25052824ffe..12e5ed992c2 100644 --- a/homeassistant/components/openhome/media_player.py +++ b/homeassistant/components/openhome/media_player.py @@ -1,4 +1,5 @@ """Support for Openhome Devices.""" + from __future__ import annotations from collections.abc import Awaitable, Callable, Coroutine diff --git a/homeassistant/components/openhome/update.py b/homeassistant/components/openhome/update.py index 6d36bccec65..bbe4fdac3b3 100644 --- a/homeassistant/components/openhome/update.py +++ b/homeassistant/components/openhome/update.py @@ -1,4 +1,5 @@ """Update entities for Linn devices.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/opensensemap/air_quality.py b/homeassistant/components/opensensemap/air_quality.py index 0e918103cd2..c9b4c726a59 100644 --- a/homeassistant/components/opensensemap/air_quality.py +++ b/homeassistant/components/opensensemap/air_quality.py @@ -1,4 +1,5 @@ """Support for openSenseMap Air Quality data.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/opensky/__init__.py b/homeassistant/components/opensky/__init__.py index 6e60c2ec4f1..c95dc1283a4 100644 --- a/homeassistant/components/opensky/__init__.py +++ b/homeassistant/components/opensky/__init__.py @@ -1,4 +1,5 @@ """The opensky component.""" + from __future__ import annotations from aiohttp import BasicAuth diff --git a/homeassistant/components/opensky/config_flow.py b/homeassistant/components/opensky/config_flow.py index 87621ea3508..863b6050616 100644 --- a/homeassistant/components/opensky/config_flow.py +++ b/homeassistant/components/opensky/config_flow.py @@ -1,4 +1,5 @@ """Config flow for OpenSky integration.""" + from __future__ import annotations from typing import Any @@ -11,6 +12,7 @@ import voluptuous as vol from homeassistant.config_entries import ( ConfigEntry, ConfigFlow, + ConfigFlowResult, OptionsFlowWithConfigEntry, ) from homeassistant.const import ( @@ -22,7 +24,6 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -48,7 +49,7 @@ class OpenSkyConfigFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Initialize user input.""" if user_input is not None: return self.async_create_entry( @@ -87,7 +88,7 @@ class OpenSkyOptionsFlowHandler(OptionsFlowWithConfigEntry): async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Initialize form.""" errors: dict[str, str] = {} if user_input is not None: diff --git a/homeassistant/components/opensky/const.py b/homeassistant/components/opensky/const.py index 7fe26b424d3..46cd7f4263e 100644 --- a/homeassistant/components/opensky/const.py +++ b/homeassistant/components/opensky/const.py @@ -1,4 +1,5 @@ """OpenSky constants.""" + import logging from homeassistant.const import Platform diff --git a/homeassistant/components/opensky/coordinator.py b/homeassistant/components/opensky/coordinator.py index d85924737a1..f54e01b0006 100644 --- a/homeassistant/components/opensky/coordinator.py +++ b/homeassistant/components/opensky/coordinator.py @@ -1,4 +1,5 @@ """DataUpdateCoordinator for the OpenSky integration.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/opensky/icons.json b/homeassistant/components/opensky/icons.json new file mode 100644 index 00000000000..763e489c4e7 --- /dev/null +++ b/homeassistant/components/opensky/icons.json @@ -0,0 +1,9 @@ +{ + "entity": { + "sensor": { + "flights": { + "default": "mdi:airplane" + } + } + } +} diff --git a/homeassistant/components/opensky/sensor.py b/homeassistant/components/opensky/sensor.py index 9cae0366357..9d317ae3e0d 100644 --- a/homeassistant/components/opensky/sensor.py +++ b/homeassistant/components/opensky/sensor.py @@ -1,4 +1,5 @@ """Sensor for the Open Sky Network.""" + from __future__ import annotations from homeassistant.components.sensor import SensorEntity, SensorStateClass @@ -38,7 +39,7 @@ class OpenSkySensor(CoordinatorEntity[OpenSkyDataUpdateCoordinator], SensorEntit ) _attr_has_entity_name = True _attr_name = None - _attr_icon = "mdi:airplane" + _attr_translation_key = "flights" _attr_native_unit_of_measurement = "flights" _attr_state_class = SensorStateClass.MEASUREMENT diff --git a/homeassistant/components/opentherm_gw/__init__.py b/homeassistant/components/opentherm_gw/__init__.py index 12f4724e056..ca37b7baaef 100644 --- a/homeassistant/components/opentherm_gw/__init__.py +++ b/homeassistant/components/opentherm_gw/__init__.py @@ -1,4 +1,5 @@ """Support for OpenTherm Gateway devices.""" + import asyncio from datetime import date, datetime import logging diff --git a/homeassistant/components/opentherm_gw/binary_sensor.py b/homeassistant/components/opentherm_gw/binary_sensor.py index d6aa5a3b700..ad8d09afa89 100644 --- a/homeassistant/components/opentherm_gw/binary_sensor.py +++ b/homeassistant/components/opentherm_gw/binary_sensor.py @@ -1,4 +1,5 @@ """Support for OpenTherm Gateway binary sensors.""" + import logging from homeassistant.components.binary_sensor import ENTITY_ID_FORMAT, BinarySensorEntity @@ -27,25 +28,19 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the OpenTherm Gateway binary sensors.""" - sensors = [] gw_dev = hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][config_entry.data[CONF_ID]] - for var, info in BINARY_SENSOR_INFO.items(): - device_class = info[0] - friendly_name_format = info[1] - status_sources = info[2] - for source in status_sources: - sensors.append( - OpenThermBinarySensor( - gw_dev, - var, - source, - device_class, - friendly_name_format, - ) - ) - - async_add_entities(sensors) + async_add_entities( + OpenThermBinarySensor( + gw_dev, + var, + source, + info[0], + info[1], + ) + for var, info in BINARY_SENSOR_INFO.items() + for source in info[2] + ) class OpenThermBinarySensor(BinarySensorEntity): diff --git a/homeassistant/components/opentherm_gw/climate.py b/homeassistant/components/opentherm_gw/climate.py index 0b9cd1862be..c020a82f08f 100644 --- a/homeassistant/components/opentherm_gw/climate.py +++ b/homeassistant/components/opentherm_gw/climate.py @@ -1,4 +1,5 @@ """Support for OpenTherm Gateway climate devices.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/opentherm_gw/config_flow.py b/homeassistant/components/opentherm_gw/config_flow.py index 70bed0d1665..19906689b57 100644 --- a/homeassistant/components/opentherm_gw/config_flow.py +++ b/homeassistant/components/opentherm_gw/config_flow.py @@ -1,4 +1,5 @@ """OpenTherm Gateway config flow.""" + from __future__ import annotations import asyncio @@ -8,7 +9,7 @@ from pyotgw import vars as gw_vars from serial import SerialException import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow from homeassistant.const import ( CONF_DEVICE, CONF_ID, @@ -30,7 +31,7 @@ from .const import ( ) -class OpenThermGwConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class OpenThermGwConfigFlow(ConfigFlow, domain=DOMAIN): """OpenTherm Gateway Config Flow.""" VERSION = 1 @@ -38,7 +39,7 @@ class OpenThermGwConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback def async_get_options_flow( - config_entry: config_entries.ConfigEntry, + config_entry: ConfigEntry, ) -> OpenThermGwOptionsFlow: """Get the options flow for this handler.""" return OpenThermGwOptionsFlow(config_entry) @@ -116,10 +117,10 @@ class OpenThermGwConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -class OpenThermGwOptionsFlow(config_entries.OptionsFlow): +class OpenThermGwOptionsFlow(OptionsFlow): """Handle opentherm_gw options.""" - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + def __init__(self, config_entry: ConfigEntry) -> None: """Initialize the options flow.""" self.config_entry = config_entry diff --git a/homeassistant/components/opentherm_gw/const.py b/homeassistant/components/opentherm_gw/const.py index 82d982b2fa9..74b856b4eaf 100644 --- a/homeassistant/components/opentherm_gw/const.py +++ b/homeassistant/components/opentherm_gw/const.py @@ -1,4 +1,5 @@ """Constants for the opentherm_gw integration.""" + from __future__ import annotations import pyotgw.vars as gw_vars @@ -11,7 +12,7 @@ from homeassistant.const import ( UnitOfPressure, UnitOfTemperature, UnitOfTime, - UnitOfVolume, + UnitOfVolumeFlowRate, ) ATTR_GW_ID = "gateway_id" @@ -309,8 +310,8 @@ SENSOR_INFO: dict[str, list] = { [gw_vars.BOILER, gw_vars.THERMOSTAT], ], gw_vars.DATA_DHW_FLOW_RATE: [ - None, - f"{UnitOfVolume.LITERS}/{UnitOfTime.MINUTES}", + SensorDeviceClass.VOLUME_FLOW_RATE, + UnitOfVolumeFlowRate.LITERS_PER_MINUTE, "Hot Water Flow Rate {}", SENSOR_FLOAT_SUGGESTED_DISPLAY_PRECISION, [gw_vars.BOILER, gw_vars.THERMOSTAT], diff --git a/homeassistant/components/opentherm_gw/icons.json b/homeassistant/components/opentherm_gw/icons.json new file mode 100644 index 00000000000..9d5d903aabc --- /dev/null +++ b/homeassistant/components/opentherm_gw/icons.json @@ -0,0 +1,15 @@ +{ + "services": { + "reset_gateway": "mdi:reload", + "set_central_heating_ovrd": "mdi:heat-wave", + "set_clock": "mdi:clock", + "set_control_setpoint": "mdi:thermometer-lines", + "set_hot_water_ovrd": "mdi:thermometer-lines", + "set_hot_water_setpoint": "mdi:thermometer-lines", + "set_gpio_mode": "mdi:cable-data", + "set_led_mode": "mdi:led-on", + "set_max_modulation": "mdi:thermometer-lines", + "set_outside_temperature": "mdi:thermometer-lines", + "set_setback_temperature": "mdi:thermometer-lines" + } +} diff --git a/homeassistant/components/opentherm_gw/sensor.py b/homeassistant/components/opentherm_gw/sensor.py index 5848d50ad95..9171292c21b 100644 --- a/homeassistant/components/opentherm_gw/sensor.py +++ b/homeassistant/components/opentherm_gw/sensor.py @@ -1,4 +1,5 @@ """Support for OpenTherm Gateway sensors.""" + import logging from homeassistant.components.sensor import ENTITY_ID_FORMAT, SensorEntity @@ -22,29 +23,21 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the OpenTherm Gateway sensors.""" - sensors = [] gw_dev = hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][config_entry.data[CONF_ID]] - for var, info in SENSOR_INFO.items(): - device_class = info[0] - unit = info[1] - friendly_name_format = info[2] - suggested_display_precision = info[3] - status_sources = info[4] - for source in status_sources: - sensors.append( - OpenThermSensor( - gw_dev, - var, - source, - device_class, - unit, - friendly_name_format, - suggested_display_precision, - ) - ) - - async_add_entities(sensors) + async_add_entities( + OpenThermSensor( + gw_dev, + var, + source, + info[0], + info[1], + info[2], + info[3], + ) + for var, info in SENSOR_INFO.items() + for source in info[4] + ) class OpenThermSensor(SensorEntity): diff --git a/homeassistant/components/openuv/__init__.py b/homeassistant/components/openuv/__init__.py index a9ea1946f91..b7c13ad49f1 100644 --- a/homeassistant/components/openuv/__init__.py +++ b/homeassistant/components/openuv/__init__.py @@ -1,4 +1,5 @@ """Support for UV data from openuv.io.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/openuv/binary_sensor.py b/homeassistant/components/openuv/binary_sensor.py index 9c970f34dc3..da4dfc3f742 100644 --- a/homeassistant/components/openuv/binary_sensor.py +++ b/homeassistant/components/openuv/binary_sensor.py @@ -1,4 +1,5 @@ """Support for OpenUV binary sensors.""" + from homeassistant.components.binary_sensor import ( BinarySensorEntity, BinarySensorEntityDescription, @@ -20,7 +21,6 @@ ATTR_PROTECTION_WINDOW_STARTING_UV = "start_uv" BINARY_SENSOR_DESCRIPTION_PROTECTION_WINDOW = BinarySensorEntityDescription( key=TYPE_PROTECTION_WINDOW, translation_key="protection_window", - icon="mdi:sunglasses", ) diff --git a/homeassistant/components/openuv/config_flow.py b/homeassistant/components/openuv/config_flow.py index d78fa84c8c5..52e369fd6df 100644 --- a/homeassistant/components/openuv/config_flow.py +++ b/homeassistant/components/openuv/config_flow.py @@ -1,4 +1,5 @@ """Config flow to configure the OpenUV component.""" + from __future__ import annotations from collections.abc import Mapping @@ -9,8 +10,7 @@ from pyopenuv import Client from pyopenuv.errors import OpenUvError import voluptuous as vol -from homeassistant import config_entries -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.const import ( CONF_API_KEY, CONF_ELEVATION, @@ -18,7 +18,6 @@ from homeassistant.const import ( CONF_LONGITUDE, ) from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.helpers.schema_config_entry_flow import ( SchemaFlowFormStep, @@ -70,7 +69,7 @@ class OpenUvData: return f"{self.latitude}, {self.longitude}" -class OpenUvFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class OpenUvFlowHandler(ConfigFlow, domain=DOMAIN): """Handle an OpenUV config flow.""" VERSION = 2 @@ -99,7 +98,7 @@ class OpenUvFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def _async_verify( self, data: OpenUvData, error_step_id: str, error_schema: vol.Schema - ) -> FlowResult: + ) -> ConfigFlowResult: """Verify the credentials and create/re-auth the entry.""" websession = aiohttp_client.async_get_clientsession(self.hass) client = Client(data.api_key, 0, 0, session=websession) @@ -138,14 +137,16 @@ class OpenUvFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Define the config flow to handle options.""" return SchemaOptionsFlowHandler(config_entry, OPTIONS_FLOW) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Handle configuration by re-auth.""" self._reauth_data = entry_data return await self.async_step_reauth_confirm() async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle re-auth completion.""" if not user_input: return self.async_show_form( @@ -168,7 +169,7 @@ class OpenUvFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the start of the config flow.""" if not user_input: return self.async_show_form( diff --git a/homeassistant/components/openuv/const.py b/homeassistant/components/openuv/const.py index b03726d5749..9d711bb8901 100644 --- a/homeassistant/components/openuv/const.py +++ b/homeassistant/components/openuv/const.py @@ -1,4 +1,5 @@ """Define constants for the OpenUV component.""" + import logging DOMAIN = "openuv" diff --git a/homeassistant/components/openuv/coordinator.py b/homeassistant/components/openuv/coordinator.py index f82a85e19b0..32d502cb8ce 100644 --- a/homeassistant/components/openuv/coordinator.py +++ b/homeassistant/components/openuv/coordinator.py @@ -1,4 +1,5 @@ """Define an update coordinator for OpenUV.""" + from __future__ import annotations from collections.abc import Awaitable, Callable diff --git a/homeassistant/components/openuv/diagnostics.py b/homeassistant/components/openuv/diagnostics.py index 99c5f89d456..e16316d4148 100644 --- a/homeassistant/components/openuv/diagnostics.py +++ b/homeassistant/components/openuv/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for OpenUV.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/openuv/icons.json b/homeassistant/components/openuv/icons.json new file mode 100644 index 00000000000..49a79c69672 --- /dev/null +++ b/homeassistant/components/openuv/icons.json @@ -0,0 +1,38 @@ +{ + "entity": { + "binary_sensor": { + "protection_window": { + "default": "mdi:sunglasses" + } + }, + "sensor": { + "current_uv_index": { + "default": "mdi:weather-sunny" + }, + "current_uv_level": { + "default": "mdi:weather-sunny" + }, + "max_uv_index": { + "default": "mdi:weather-sunny" + }, + "skin_type_1_safe_exposure_time": { + "default": "mdi:timer-outline" + }, + "skin_type_2_safe_exposure_time": { + "default": "mdi:timer-outline" + }, + "skin_type_3_safe_exposure_time": { + "default": "mdi:timer-outline" + }, + "skin_type_4_safe_exposure_time": { + "default": "mdi:timer-outline" + }, + "skin_type_5_safe_exposure_time": { + "default": "mdi:timer-outline" + }, + "skin_type_6_safe_exposure_time": { + "default": "mdi:timer-outline" + } + } + } +} diff --git a/homeassistant/components/openuv/sensor.py b/homeassistant/components/openuv/sensor.py index 9e337d49ba3..a79bc410715 100644 --- a/homeassistant/components/openuv/sensor.py +++ b/homeassistant/components/openuv/sensor.py @@ -1,4 +1,5 @@ """Support for OpenUV sensors.""" + from __future__ import annotations from collections.abc import Callable, Mapping @@ -89,7 +90,6 @@ SENSOR_DESCRIPTIONS = ( OpenUvSensorEntityDescription( key=TYPE_CURRENT_UV_INDEX, translation_key="current_uv_index", - icon="mdi:weather-sunny", native_unit_of_measurement=UV_INDEX, state_class=SensorStateClass.MEASUREMENT, value_fn=lambda data: data["uv"], @@ -97,7 +97,6 @@ SENSOR_DESCRIPTIONS = ( OpenUvSensorEntityDescription( key=TYPE_CURRENT_UV_LEVEL, translation_key="current_uv_level", - icon="mdi:weather-sunny", device_class=SensorDeviceClass.ENUM, options=[label.value for label in UV_LABEL_DEFINITIONS], value_fn=lambda data: get_uv_label(data["uv"]), @@ -105,7 +104,6 @@ SENSOR_DESCRIPTIONS = ( OpenUvSensorEntityDescription( key=TYPE_MAX_UV_INDEX, translation_key="max_uv_index", - icon="mdi:weather-sunny", native_unit_of_measurement=UV_INDEX, state_class=SensorStateClass.MEASUREMENT, value_fn=lambda data: data["uv_max"], @@ -113,7 +111,6 @@ SENSOR_DESCRIPTIONS = ( OpenUvSensorEntityDescription( key=TYPE_SAFE_EXPOSURE_TIME_1, translation_key="skin_type_1_safe_exposure_time", - icon="mdi:timer-outline", native_unit_of_measurement=UnitOfTime.MINUTES, state_class=SensorStateClass.MEASUREMENT, value_fn=lambda data: data["safe_exposure_time"][ @@ -123,7 +120,6 @@ SENSOR_DESCRIPTIONS = ( OpenUvSensorEntityDescription( key=TYPE_SAFE_EXPOSURE_TIME_2, translation_key="skin_type_2_safe_exposure_time", - icon="mdi:timer-outline", native_unit_of_measurement=UnitOfTime.MINUTES, state_class=SensorStateClass.MEASUREMENT, value_fn=lambda data: data["safe_exposure_time"][ @@ -133,7 +129,6 @@ SENSOR_DESCRIPTIONS = ( OpenUvSensorEntityDescription( key=TYPE_SAFE_EXPOSURE_TIME_3, translation_key="skin_type_3_safe_exposure_time", - icon="mdi:timer-outline", native_unit_of_measurement=UnitOfTime.MINUTES, state_class=SensorStateClass.MEASUREMENT, value_fn=lambda data: data["safe_exposure_time"][ @@ -143,7 +138,6 @@ SENSOR_DESCRIPTIONS = ( OpenUvSensorEntityDescription( key=TYPE_SAFE_EXPOSURE_TIME_4, translation_key="skin_type_4_safe_exposure_time", - icon="mdi:timer-outline", native_unit_of_measurement=UnitOfTime.MINUTES, state_class=SensorStateClass.MEASUREMENT, value_fn=lambda data: data["safe_exposure_time"][ @@ -153,7 +147,6 @@ SENSOR_DESCRIPTIONS = ( OpenUvSensorEntityDescription( key=TYPE_SAFE_EXPOSURE_TIME_5, translation_key="skin_type_5_safe_exposure_time", - icon="mdi:timer-outline", native_unit_of_measurement=UnitOfTime.MINUTES, state_class=SensorStateClass.MEASUREMENT, value_fn=lambda data: data["safe_exposure_time"][ @@ -163,7 +156,6 @@ SENSOR_DESCRIPTIONS = ( OpenUvSensorEntityDescription( key=TYPE_SAFE_EXPOSURE_TIME_6, translation_key="skin_type_6_safe_exposure_time", - icon="mdi:timer-outline", native_unit_of_measurement=UnitOfTime.MINUTES, state_class=SensorStateClass.MEASUREMENT, value_fn=lambda data: data["safe_exposure_time"][ diff --git a/homeassistant/components/openweathermap/__init__.py b/homeassistant/components/openweathermap/__init__.py index 22c97d72fa5..ad99416e448 100644 --- a/homeassistant/components/openweathermap/__init__.py +++ b/homeassistant/components/openweathermap/__init__.py @@ -1,4 +1,5 @@ """The openweathermap component.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/openweathermap/config_flow.py b/homeassistant/components/openweathermap/config_flow.py index 799be35fb42..cc4c71c2bd5 100644 --- a/homeassistant/components/openweathermap/config_flow.py +++ b/homeassistant/components/openweathermap/config_flow.py @@ -1,11 +1,12 @@ """Config flow for OpenWeatherMap.""" + from __future__ import annotations from pyowm import OWM from pyowm.commons.exceptions import APIRequestError, UnauthorizedError import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow from homeassistant.const import ( CONF_API_KEY, CONF_LANGUAGE, @@ -28,7 +29,7 @@ from .const import ( ) -class OpenWeatherMapConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class OpenWeatherMapConfigFlow(ConfigFlow, domain=DOMAIN): """Config flow for OpenWeatherMap.""" VERSION = CONFIG_FLOW_VERSION @@ -36,7 +37,7 @@ class OpenWeatherMapConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback def async_get_options_flow( - config_entry: config_entries.ConfigEntry, + config_entry: ConfigEntry, ) -> OpenWeatherMapOptionsFlow: """Get the options flow for this handler.""" return OpenWeatherMapOptionsFlow(config_entry) @@ -90,10 +91,10 @@ class OpenWeatherMapConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form(step_id="user", data_schema=schema, errors=errors) -class OpenWeatherMapOptionsFlow(config_entries.OptionsFlow): +class OpenWeatherMapOptionsFlow(OptionsFlow): """Handle options.""" - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + def __init__(self, config_entry: ConfigEntry) -> None: """Initialize options flow.""" self.config_entry = config_entry diff --git a/homeassistant/components/openweathermap/const.py b/homeassistant/components/openweathermap/const.py index d7deab21743..dbd536a2556 100644 --- a/homeassistant/components/openweathermap/const.py +++ b/homeassistant/components/openweathermap/const.py @@ -1,4 +1,5 @@ """Consts for the OpenWeatherMap.""" + from __future__ import annotations from homeassistant.components.weather import ( diff --git a/homeassistant/components/openweathermap/sensor.py b/homeassistant/components/openweathermap/sensor.py index a1e0e9d2169..16d9c3064d7 100644 --- a/homeassistant/components/openweathermap/sensor.py +++ b/homeassistant/components/openweathermap/sensor.py @@ -1,4 +1,5 @@ """Support for the OpenWeatherMap (OWM) service.""" + from __future__ import annotations from datetime import datetime diff --git a/homeassistant/components/openweathermap/strings.json b/homeassistant/components/openweathermap/strings.json index a29a8952434..c53b685af91 100644 --- a/homeassistant/components/openweathermap/strings.json +++ b/homeassistant/components/openweathermap/strings.json @@ -11,7 +11,7 @@ "user": { "data": { "api_key": "[%key:common::config_flow::data::api_key%]", - "language": "Language", + "language": "[%key:common::config_flow::data::language%]", "latitude": "[%key:common::config_flow::data::latitude%]", "longitude": "[%key:common::config_flow::data::longitude%]", "mode": "[%key:common::config_flow::data::mode%]", @@ -25,7 +25,7 @@ "step": { "init": { "data": { - "language": "Language", + "language": "[%key:common::config_flow::data::language%]", "mode": "[%key:common::config_flow::data::mode%]" } } diff --git a/homeassistant/components/openweathermap/weather.py b/homeassistant/components/openweathermap/weather.py index bf1ae5ca7da..62bf18ba813 100644 --- a/homeassistant/components/openweathermap/weather.py +++ b/homeassistant/components/openweathermap/weather.py @@ -1,4 +1,5 @@ """Support for the OpenWeatherMap (OWM) service.""" + from __future__ import annotations from typing import cast @@ -184,7 +185,7 @@ class OpenWeatherMapWeather(SingleCoordinatorWeatherEntity[WeatherUpdateCoordina return self.coordinator.data[ATTR_API_WIND_BEARING] @property - def forecast(self) -> list[Forecast] | None: + def _forecast(self) -> list[Forecast] | None: """Return the forecast array.""" api_forecasts = self.coordinator.data[ATTR_API_FORECAST] forecasts = [ @@ -200,9 +201,9 @@ class OpenWeatherMapWeather(SingleCoordinatorWeatherEntity[WeatherUpdateCoordina @callback def _async_forecast_daily(self) -> list[Forecast] | None: """Return the daily forecast in native units.""" - return self.forecast + return self._forecast @callback def _async_forecast_hourly(self) -> list[Forecast] | None: """Return the hourly forecast in native units.""" - return self.forecast + return self._forecast diff --git a/homeassistant/components/openweathermap/weather_update_coordinator.py b/homeassistant/components/openweathermap/weather_update_coordinator.py index 05b24d60f79..d54a7fa899f 100644 --- a/homeassistant/components/openweathermap/weather_update_coordinator.py +++ b/homeassistant/components/openweathermap/weather_update_coordinator.py @@ -1,4 +1,5 @@ """Weather data coordinator for the OpenWeatherMap (OWM) service.""" + import asyncio from datetime import timedelta import logging diff --git a/homeassistant/components/opnsense/__init__.py b/homeassistant/components/opnsense/__init__.py index 0111379df44..d2ee2e2dfbd 100644 --- a/homeassistant/components/opnsense/__init__.py +++ b/homeassistant/components/opnsense/__init__.py @@ -1,4 +1,5 @@ """Support for OPNSense Routers.""" + import logging from pyopnsense import diagnostics diff --git a/homeassistant/components/opnsense/device_tracker.py b/homeassistant/components/opnsense/device_tracker.py index 78a8315335c..7c018e20a36 100644 --- a/homeassistant/components/opnsense/device_tracker.py +++ b/homeassistant/components/opnsense/device_tracker.py @@ -1,4 +1,5 @@ """Device tracker support for OPNSense routers.""" + from __future__ import annotations from homeassistant.components.device_tracker import DeviceScanner diff --git a/homeassistant/components/opower/__init__.py b/homeassistant/components/opower/__init__.py index f4fca22c9b4..1a34d0547aa 100644 --- a/homeassistant/components/opower/__init__.py +++ b/homeassistant/components/opower/__init__.py @@ -1,4 +1,5 @@ """The Opower integration.""" + from __future__ import annotations from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/opower/config_flow.py b/homeassistant/components/opower/config_flow.py index ab1fbbe36e3..858d14dd832 100644 --- a/homeassistant/components/opower/config_flow.py +++ b/homeassistant/components/opower/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Opower integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -15,10 +16,9 @@ from opower import ( ) import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant, callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_create_clientsession from .const import CONF_TOTP_SECRET, CONF_UTILITY, DOMAIN @@ -55,19 +55,19 @@ async def _validate_login( return errors -class OpowerConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class OpowerConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Opower.""" VERSION = 1 def __init__(self) -> None: """Initialize a new OpowerConfigFlow.""" - self.reauth_entry: config_entries.ConfigEntry | None = None + self.reauth_entry: ConfigEntry | None = None self.utility_info: dict[str, Any] | None = None async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors: dict[str, str] = {} if user_input is not None: @@ -91,7 +91,7 @@ class OpowerConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_mfa( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle MFA step.""" assert self.utility_info is not None errors: dict[str, str] = {} @@ -120,14 +120,16 @@ class OpowerConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) @callback - def _async_create_opower_entry(self, data: dict[str, Any]) -> FlowResult: + def _async_create_opower_entry(self, data: dict[str, Any]) -> ConfigFlowResult: """Create the config entry.""" return self.async_create_entry( title=f"{data[CONF_UTILITY]} ({data[CONF_USERNAME]})", data=data, ) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Handle configuration by re-auth.""" self.reauth_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] @@ -136,7 +138,7 @@ class OpowerConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Dialog that informs the user that reauth is required.""" assert self.reauth_entry errors: dict[str, str] = {} diff --git a/homeassistant/components/opower/coordinator.py b/homeassistant/components/opower/coordinator.py index 73c60068cd4..d4cce99e1cc 100644 --- a/homeassistant/components/opower/coordinator.py +++ b/homeassistant/components/opower/coordinator.py @@ -1,4 +1,5 @@ """Coordinator to handle Opower connections.""" + from datetime import datetime, timedelta import logging import socket @@ -108,7 +109,7 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]): ) last_stat = await get_instance(self.hass).async_add_executor_job( - get_last_statistics, self.hass, 1, consumption_statistic_id, True, set() + get_last_statistics, self.hass, 1, cost_statistic_id, True, set() ) if not last_stat: _LOGGER.debug("Updating statistic for the first time") @@ -118,7 +119,7 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]): last_stats_time = None else: cost_reads = await self._async_get_recent_cost_reads( - account, last_stat[consumption_statistic_id][0]["start"] + account, last_stat[cost_statistic_id][0]["start"] ) if not cost_reads: _LOGGER.debug("No recent usage/cost data. Skipping update") diff --git a/homeassistant/components/opower/manifest.json b/homeassistant/components/opower/manifest.json index 820aac5d20a..879aeb0327b 100644 --- a/homeassistant/components/opower/manifest.json +++ b/homeassistant/components/opower/manifest.json @@ -5,8 +5,7 @@ "config_flow": true, "dependencies": ["recorder"], "documentation": "https://www.home-assistant.io/integrations/opower", - "import_executor": true, "iot_class": "cloud_polling", "loggers": ["opower"], - "requirements": ["opower==0.3.1"] + "requirements": ["opower==0.4.2"] } diff --git a/homeassistant/components/opower/sensor.py b/homeassistant/components/opower/sensor.py index 9940132dac2..9f467dce1c6 100644 --- a/homeassistant/components/opower/sensor.py +++ b/homeassistant/components/opower/sensor.py @@ -1,4 +1,5 @@ """Support for Opower sensors.""" + from __future__ import annotations from collections.abc import Callable @@ -24,18 +25,13 @@ from .const import DOMAIN from .coordinator import OpowerCoordinator -@dataclass(frozen=True) -class OpowerEntityDescriptionMixin: - """Mixin values for required keys.""" +@dataclass(frozen=True, kw_only=True) +class OpowerEntityDescription(SensorEntityDescription): + """Class describing Opower sensors entities.""" value_fn: Callable[[Forecast], str | float] -@dataclass(frozen=True) -class OpowerEntityDescription(SensorEntityDescription, OpowerEntityDescriptionMixin): - """Class describing Opower sensors entities.""" - - # suggested_display_precision=0 for all sensors since # Opower provides 0 decimal points for all these. # (for the statistics in the energy dashboard Opower does provide decimal points) @@ -191,16 +187,16 @@ async def async_setup_entry( and forecast.unit_of_measure in [UnitOfMeasure.THERM, UnitOfMeasure.CCF] ): sensors = GAS_SENSORS - for sensor in sensors: - entities.append( - OpowerSensor( - coordinator, - sensor, - forecast.account.utility_account_id, - device, - device_id, - ) + entities.extend( + OpowerSensor( + coordinator, + sensor, + forecast.account.utility_account_id, + device, + device_id, ) + for sensor in sensors + ) async_add_entities(entities) diff --git a/homeassistant/components/opple/light.py b/homeassistant/components/opple/light.py index 0d42b35b83b..2fbbf6ae02a 100644 --- a/homeassistant/components/opple/light.py +++ b/homeassistant/components/opple/light.py @@ -1,4 +1,5 @@ """Support for the Opple light.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/oralb/__init__.py b/homeassistant/components/oralb/__init__.py index 23a022effef..71bcb2f2deb 100644 --- a/homeassistant/components/oralb/__init__.py +++ b/homeassistant/components/oralb/__init__.py @@ -1,4 +1,5 @@ """The OralB integration.""" + from __future__ import annotations import logging @@ -64,20 +65,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) return await data.async_poll(connectable_device) - coordinator = hass.data.setdefault(DOMAIN, {})[ - entry.entry_id - ] = ActiveBluetoothProcessorCoordinator( - hass, - _LOGGER, - address=address, - mode=BluetoothScanningMode.PASSIVE, - update_method=data.update, - needs_poll_method=_needs_poll, - poll_method=_async_poll, - # We will take advertisements from non-connectable devices - # since we will trade the BLEDevice for a connectable one - # if we need to poll it - connectable=False, + coordinator = hass.data.setdefault(DOMAIN, {})[entry.entry_id] = ( + ActiveBluetoothProcessorCoordinator( + hass, + _LOGGER, + address=address, + mode=BluetoothScanningMode.PASSIVE, + update_method=data.update, + needs_poll_method=_needs_poll, + poll_method=_async_poll, + # We will take advertisements from non-connectable devices + # since we will trade the BLEDevice for a connectable one + # if we need to poll it + connectable=False, + ) ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload( diff --git a/homeassistant/components/oralb/config_flow.py b/homeassistant/components/oralb/config_flow.py index 28e16c7f8a7..ab5d919194e 100644 --- a/homeassistant/components/oralb/config_flow.py +++ b/homeassistant/components/oralb/config_flow.py @@ -1,4 +1,5 @@ """Config flow for oralb ble integration.""" + from __future__ import annotations from typing import Any @@ -10,9 +11,8 @@ from homeassistant.components.bluetooth import ( BluetoothServiceInfoBleak, async_discovered_service_info, ) -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_ADDRESS -from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN @@ -30,7 +30,7 @@ class OralBConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_bluetooth( self, discovery_info: BluetoothServiceInfoBleak - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the bluetooth discovery step.""" await self.async_set_unique_id(discovery_info.address) self._abort_if_unique_id_configured() @@ -43,7 +43,7 @@ class OralBConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_bluetooth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm discovery.""" assert self._discovered_device is not None device = self._discovered_device @@ -62,7 +62,7 @@ class OralBConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the user step to pick discovered device.""" if user_input is not None: address = user_input[CONF_ADDRESS] diff --git a/homeassistant/components/oralb/device.py b/homeassistant/components/oralb/device.py index 3cc46fd27c6..0fb6b71981d 100644 --- a/homeassistant/components/oralb/device.py +++ b/homeassistant/components/oralb/device.py @@ -1,4 +1,5 @@ """Support for OralB devices.""" + from __future__ import annotations from oralb_ble import DeviceKey diff --git a/homeassistant/components/oralb/sensor.py b/homeassistant/components/oralb/sensor.py index f743122d0cb..b6e52c1284d 100644 --- a/homeassistant/components/oralb/sensor.py +++ b/homeassistant/components/oralb/sensor.py @@ -1,4 +1,5 @@ """Support for OralB sensors.""" + from __future__ import annotations from oralb_ble import OralBSensor, SensorUpdate diff --git a/homeassistant/components/oru/sensor.py b/homeassistant/components/oru/sensor.py index a971a8e461b..b1d814dd98a 100644 --- a/homeassistant/components/oru/sensor.py +++ b/homeassistant/components/oru/sensor.py @@ -1,4 +1,5 @@ """Platform for sensor integration.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/orvibo/switch.py b/homeassistant/components/orvibo/switch.py index 9e86d3787af..ece833b7036 100644 --- a/homeassistant/components/orvibo/switch.py +++ b/homeassistant/components/orvibo/switch.py @@ -1,4 +1,5 @@ """Support for Orvibo S20 Wifi Smart Switches.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/osoenergy/__init__.py b/homeassistant/components/osoenergy/__init__.py index f0b89eaea90..48ea01e8bb8 100644 --- a/homeassistant/components/osoenergy/__init__.py +++ b/homeassistant/components/osoenergy/__init__.py @@ -1,4 +1,5 @@ """Support for the OSO Energy devices and services.""" + from typing import Any, Generic, TypeVar from aiohttp.web_exceptions import HTTPException @@ -44,7 +45,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: devices: Any = await osoenergy.session.start_session(osoenergy_config) except HTTPException as error: - raise ConfigEntryNotReady() from error + raise ConfigEntryNotReady from error except OSOEnergyReauthRequired as err: raise ConfigEntryAuthFailed from err diff --git a/homeassistant/components/osoenergy/config_flow.py b/homeassistant/components/osoenergy/config_flow.py index 28b037f9cc5..ce0932571e5 100644 --- a/homeassistant/components/osoenergy/config_flow.py +++ b/homeassistant/components/osoenergy/config_flow.py @@ -1,4 +1,5 @@ """Config Flow for OSO Energy.""" + from collections.abc import Mapping import logging from typing import Any @@ -6,10 +7,13 @@ from typing import Any from apyosoenergyapi import OSOEnergy import voluptuous as vol -from homeassistant import config_entries -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import ( + SOURCE_REAUTH, + ConfigEntry, + ConfigFlow, + ConfigFlowResult, +) from homeassistant.const import CONF_API_KEY -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import aiohttp_client from .const import DOMAIN @@ -18,7 +22,7 @@ _LOGGER = logging.getLogger(__name__) _SCHEMA_STEP_USER = vol.Schema({vol.Required(CONF_API_KEY): str}) -class OSOEnergyFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class OSOEnergyFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a OSO Energy config flow.""" VERSION = 1 @@ -27,7 +31,7 @@ class OSOEnergyFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Initialize.""" self.entry: ConfigEntry | None = None - async def async_step_user(self, user_input=None) -> FlowResult: + async def async_step_user(self, user_input=None) -> ConfigFlowResult: """Handle a flow initialized by the user.""" errors = {} @@ -36,10 +40,7 @@ class OSOEnergyFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if user_email := await self.get_user_email(user_input[CONF_API_KEY]): await self.async_set_unique_id(user_email) - if ( - self.context["source"] == config_entries.SOURCE_REAUTH - and self.entry - ): + if self.context["source"] == SOURCE_REAUTH and self.entry: self.hass.config_entries.async_update_entry( self.entry, title=user_email, data=user_input ) @@ -67,7 +68,9 @@ class OSOEnergyFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): _LOGGER.exception("Unknown error occurred") return None - async def async_step_reauth(self, user_input: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, user_input: Mapping[str, Any] + ) -> ConfigFlowResult: """Re Authenticate a user.""" self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) data = {CONF_API_KEY: user_input[CONF_API_KEY]} diff --git a/homeassistant/components/osoenergy/water_heater.py b/homeassistant/components/osoenergy/water_heater.py index 4b2ad7c48d6..eaf54a9f9a4 100644 --- a/homeassistant/components/osoenergy/water_heater.py +++ b/homeassistant/components/osoenergy/water_heater.py @@ -1,4 +1,5 @@ """Support for OSO Energy water heaters.""" + from typing import Any from apyosoenergyapi.helper.const import OSOEnergyWaterHeaterData @@ -44,11 +45,9 @@ async def async_setup_entry( """Set up OSO Energy heater based on a config entry.""" osoenergy = hass.data[DOMAIN][entry.entry_id] devices = osoenergy.session.device_list.get("water_heater") - entities = [] - if devices: - for dev in devices: - entities.append(OSOEnergyWaterHeater(osoenergy, dev)) - async_add_entities(entities, True) + if not devices: + return + async_add_entities((OSOEnergyWaterHeater(osoenergy, dev) for dev in devices), True) class OSOEnergyWaterHeater( diff --git a/homeassistant/components/osramlightify/light.py b/homeassistant/components/osramlightify/light.py index 1c0a134831b..2f0ba1bccb4 100644 --- a/homeassistant/components/osramlightify/light.py +++ b/homeassistant/components/osramlightify/light.py @@ -1,4 +1,5 @@ """Support for Osram Lightify.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/otbr/__init__.py b/homeassistant/components/otbr/__init__.py index fe4cc8c1145..97c2f40eb99 100644 --- a/homeassistant/components/otbr/__init__.py +++ b/homeassistant/components/otbr/__init__.py @@ -1,4 +1,5 @@ """The Open Thread Border Router integration.""" + from __future__ import annotations import aiohttp diff --git a/homeassistant/components/otbr/config_flow.py b/homeassistant/components/otbr/config_flow.py index d0d3f1c1060..8342a965bd3 100644 --- a/homeassistant/components/otbr/config_flow.py +++ b/homeassistant/components/otbr/config_flow.py @@ -1,4 +1,5 @@ """Config flow for the Open Thread Border Router integration.""" + from __future__ import annotations from contextlib import suppress @@ -19,10 +20,9 @@ from homeassistant.components.hassio import ( ) from homeassistant.components.homeassistant_yellow import hardware as yellow_hardware from homeassistant.components.thread import async_get_preferred_dataset -from homeassistant.config_entries import SOURCE_HASSIO, ConfigFlow +from homeassistant.config_entries import SOURCE_HASSIO, ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_URL from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -59,6 +59,9 @@ async def _title(hass: HomeAssistant, discovery_info: HassioServiceInfo) -> str: if device and "SkyConnect" in device: return f"Home Assistant SkyConnect ({discovery_info.name})" + if device and "Connect_ZBT-1" in device: + return f"Home Assistant Connect ZBT-1 ({discovery_info.name})" + return discovery_info.name @@ -100,7 +103,7 @@ class OTBRConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, str] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Set up by user.""" if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") @@ -129,7 +132,9 @@ class OTBRConfigFlow(ConfigFlow, domain=DOMAIN): step_id="user", data_schema=data_schema, errors=errors ) - async def async_step_hassio(self, discovery_info: HassioServiceInfo) -> FlowResult: + async def async_step_hassio( + self, discovery_info: HassioServiceInfo + ) -> ConfigFlowResult: """Handle hassio discovery.""" config = discovery_info.config url = f"http://{config['host']}:{config['port']}" diff --git a/homeassistant/components/otbr/util.py b/homeassistant/components/otbr/util.py index 9c47df5eaf7..4374412b8c1 100644 --- a/homeassistant/components/otbr/util.py +++ b/homeassistant/components/otbr/util.py @@ -1,4 +1,5 @@ """Utility functions for the Open Thread Border Router integration.""" + from __future__ import annotations from collections.abc import Callable, Coroutine diff --git a/homeassistant/components/otp/sensor.py b/homeassistant/components/otp/sensor.py index 7c7c30df970..a9b4368d1e6 100644 --- a/homeassistant/components/otp/sensor.py +++ b/homeassistant/components/otp/sensor.py @@ -1,4 +1,5 @@ """Support for One-Time Password (OTP).""" + from __future__ import annotations import time diff --git a/homeassistant/components/ourgroceries/__init__.py b/homeassistant/components/ourgroceries/__init__.py index 472313aa315..5086a5cfc9b 100644 --- a/homeassistant/components/ourgroceries/__init__.py +++ b/homeassistant/components/ourgroceries/__init__.py @@ -1,4 +1,5 @@ """The OurGroceries integration.""" + from __future__ import annotations from aiohttp import ClientError diff --git a/homeassistant/components/ourgroceries/config_flow.py b/homeassistant/components/ourgroceries/config_flow.py index 65670dd7f92..98eae900db6 100644 --- a/homeassistant/components/ourgroceries/config_flow.py +++ b/homeassistant/components/ourgroceries/config_flow.py @@ -1,4 +1,5 @@ """Config flow for OurGroceries integration.""" + from __future__ import annotations import logging @@ -9,9 +10,8 @@ from ourgroceries import OurGroceries from ourgroceries.exceptions import InvalidLoginException import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN @@ -25,14 +25,14 @@ STEP_USER_DATA_SCHEMA = vol.Schema( ) -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class OurGroceriesConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for OurGroceries.""" VERSION = 1 async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors: dict[str, str] = {} if user_input is not None: diff --git a/homeassistant/components/ourgroceries/coordinator.py b/homeassistant/components/ourgroceries/coordinator.py index c583fb4d5b1..bc645b2bdb3 100644 --- a/homeassistant/components/ourgroceries/coordinator.py +++ b/homeassistant/components/ourgroceries/coordinator.py @@ -1,4 +1,5 @@ """The OurGroceries coordinator.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/overkiz/__init__.py b/homeassistant/components/overkiz/__init__.py index 03a81f67308..ce877e15261 100644 --- a/homeassistant/components/overkiz/__init__.py +++ b/homeassistant/components/overkiz/__init__.py @@ -1,4 +1,5 @@ """The Overkiz (by Somfy) integration.""" + from __future__ import annotations from collections import defaultdict diff --git a/homeassistant/components/overkiz/alarm_control_panel.py b/homeassistant/components/overkiz/alarm_control_panel.py index e2555308e34..72c99982a1b 100644 --- a/homeassistant/components/overkiz/alarm_control_panel.py +++ b/homeassistant/components/overkiz/alarm_control_panel.py @@ -1,4 +1,5 @@ """Support for Overkiz alarm control panel.""" + from __future__ import annotations from collections.abc import Callable @@ -35,20 +36,13 @@ from .coordinator import OverkizDataUpdateCoordinator from .entity import OverkizDescriptiveEntity -@dataclass(frozen=True) -class OverkizAlarmDescriptionMixin: - """Define an entity description mixin for switch entities.""" +@dataclass(frozen=True, kw_only=True) +class OverkizAlarmDescription(AlarmControlPanelEntityDescription): + """Class to describe an Overkiz alarm control panel.""" supported_features: AlarmControlPanelEntityFeature fn_state: Callable[[Callable[[str], OverkizStateType]], str] - -@dataclass(frozen=True) -class OverkizAlarmDescription( - AlarmControlPanelEntityDescription, OverkizAlarmDescriptionMixin -): - """Class to describe an Overkiz alarm control panel.""" - alarm_disarm: str | None = None alarm_disarm_args: OverkizStateType | list[OverkizStateType] = None alarm_arm_home: str | None = None @@ -227,21 +221,19 @@ async def async_setup_entry( ) -> None: """Set up the Overkiz alarm control panel from a config entry.""" data: HomeAssistantOverkizData = hass.data[DOMAIN][entry.entry_id] - entities: list[OverkizAlarmControlPanel] = [] - for device in data.platforms[Platform.ALARM_CONTROL_PANEL]: - if description := SUPPORTED_DEVICES.get(device.widget) or SUPPORTED_DEVICES.get( - device.ui_class - ): - entities.append( - OverkizAlarmControlPanel( - device.device_url, - data.coordinator, - description, - ) - ) - - async_add_entities(entities) + async_add_entities( + OverkizAlarmControlPanel( + device.device_url, + data.coordinator, + description, + ) + for device in data.platforms[Platform.ALARM_CONTROL_PANEL] + if ( + description := SUPPORTED_DEVICES.get(device.widget) + or SUPPORTED_DEVICES.get(device.ui_class) + ) + ) class OverkizAlarmControlPanel(OverkizDescriptiveEntity, AlarmControlPanelEntity): diff --git a/homeassistant/components/overkiz/binary_sensor.py b/homeassistant/components/overkiz/binary_sensor.py index 975ef4ff834..c37afc9cb0c 100644 --- a/homeassistant/components/overkiz/binary_sensor.py +++ b/homeassistant/components/overkiz/binary_sensor.py @@ -1,4 +1,5 @@ """Support for Overkiz binary sensors.""" + from __future__ import annotations from collections.abc import Callable @@ -22,20 +23,13 @@ from .const import DOMAIN, IGNORED_OVERKIZ_DEVICES from .entity import OverkizDescriptiveEntity -@dataclass(frozen=True) -class OverkizBinarySensorDescriptionMixin: - """Define an entity description mixin for binary sensor entities.""" +@dataclass(frozen=True, kw_only=True) +class OverkizBinarySensorDescription(BinarySensorEntityDescription): + """Class to describe an Overkiz binary sensor.""" value_fn: Callable[[OverkizStateType], bool] -@dataclass(frozen=True) -class OverkizBinarySensorDescription( - BinarySensorEntityDescription, OverkizBinarySensorDescriptionMixin -): - """Class to describe an Overkiz binary sensor.""" - - BINARY_SENSOR_DESCRIPTIONS: list[OverkizBinarySensorDescription] = [ # RainSensor/RainSensor OverkizBinarySensorDescription( @@ -111,6 +105,22 @@ BINARY_SENSOR_DESCRIPTIONS: list[OverkizBinarySensorDescription] = [ ) == 1, ), + OverkizBinarySensorDescription( + key=OverkizState.CORE_HEATING_STATUS, + name="Heating status", + device_class=BinarySensorDeviceClass.HEAT, + value_fn=lambda state: state == OverkizCommandParam.ON, + ), + OverkizBinarySensorDescription( + key=OverkizState.MODBUSLINK_DHW_ABSENCE_MODE, + name="Absence mode", + value_fn=lambda state: state == OverkizCommandParam.ON, + ), + OverkizBinarySensorDescription( + key=OverkizState.MODBUSLINK_DHW_BOOST_MODE, + name="Boost mode", + value_fn=lambda state: state == OverkizCommandParam.ON, + ), ] SUPPORTED_STATES = { @@ -134,15 +144,15 @@ async def async_setup_entry( ): continue - for state in device.definition.states: - if description := SUPPORTED_STATES.get(state.qualified_name): - entities.append( - OverkizBinarySensor( - device.device_url, - data.coordinator, - description, - ) - ) + entities.extend( + OverkizBinarySensor( + device.device_url, + data.coordinator, + description, + ) + for state in device.definition.states + if (description := SUPPORTED_STATES.get(state.qualified_name)) + ) async_add_entities(entities) diff --git a/homeassistant/components/overkiz/button.py b/homeassistant/components/overkiz/button.py index f8f33db7eed..5a1116aeeb5 100644 --- a/homeassistant/components/overkiz/button.py +++ b/homeassistant/components/overkiz/button.py @@ -1,4 +1,5 @@ """Support for Overkiz (virtual) buttons.""" + from __future__ import annotations from dataclasses import dataclass @@ -94,15 +95,15 @@ async def async_setup_entry( ): continue - for command in device.definition.commands: - if description := SUPPORTED_COMMANDS.get(command.command_name): - entities.append( - OverkizButton( - device.device_url, - data.coordinator, - description, - ) - ) + entities.extend( + OverkizButton( + device.device_url, + data.coordinator, + description, + ) + for command in device.definition.commands + if (description := SUPPORTED_COMMANDS.get(command.command_name)) + ) async_add_entities(entities) diff --git a/homeassistant/components/overkiz/climate.py b/homeassistant/components/overkiz/climate.py index 2c24ca4f832..b569d05d2d7 100644 --- a/homeassistant/components/overkiz/climate.py +++ b/homeassistant/components/overkiz/climate.py @@ -1,4 +1,5 @@ """Support for Overkiz climate devices.""" + from __future__ import annotations from typing import cast @@ -6,6 +7,7 @@ from typing import cast from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import HomeAssistantOverkizData @@ -26,15 +28,16 @@ async def async_setup_entry( """Set up the Overkiz climate from a config entry.""" data: HomeAssistantOverkizData = hass.data[DOMAIN][entry.entry_id] - async_add_entities( + # Match devices based on the widget. + entities_based_on_widget: list[Entity] = [ WIDGET_TO_CLIMATE_ENTITY[device.widget](device.device_url, data.coordinator) for device in data.platforms[Platform.CLIMATE] if device.widget in WIDGET_TO_CLIMATE_ENTITY - ) + ] - # Match devices based on the widget and controllableName - # This is for example used for Atlantic APC, where devices with different functionality share the same uiClass and widget. - async_add_entities( + # Match devices based on the widget and controllableName. + # ie Atlantic APC + entities_based_on_widget_and_controllable: list[Entity] = [ WIDGET_AND_CONTROLLABLE_TO_CLIMATE_ENTITY[device.widget][ cast(Controllable, device.controllable_name) ](device.device_url, data.coordinator) @@ -42,14 +45,21 @@ async def async_setup_entry( if device.widget in WIDGET_AND_CONTROLLABLE_TO_CLIMATE_ENTITY and device.controllable_name in WIDGET_AND_CONTROLLABLE_TO_CLIMATE_ENTITY[device.widget] - ) + ] - # Hitachi Air To Air Heat Pumps - async_add_entities( + # Match devices based on the widget and protocol. + # #ie Hitachi Air To Air Heat Pumps + entities_based_on_widget_and_protocol: list[Entity] = [ WIDGET_AND_PROTOCOL_TO_CLIMATE_ENTITY[device.widget][device.protocol]( device.device_url, data.coordinator ) for device in data.platforms[Platform.CLIMATE] if device.widget in WIDGET_AND_PROTOCOL_TO_CLIMATE_ENTITY and device.protocol in WIDGET_AND_PROTOCOL_TO_CLIMATE_ENTITY[device.widget] + ] + + async_add_entities( + entities_based_on_widget + + entities_based_on_widget_and_controllable + + entities_based_on_widget_and_protocol ) diff --git a/homeassistant/components/overkiz/climate_entities/__init__.py b/homeassistant/components/overkiz/climate_entities/__init__.py index 72230c99a05..df997f7a68e 100644 --- a/homeassistant/components/overkiz/climate_entities/__init__.py +++ b/homeassistant/components/overkiz/climate_entities/__init__.py @@ -1,4 +1,5 @@ """Climate entities for the Overkiz (by Somfy) integration.""" + from enum import StrEnum, unique from pyoverkiz.enums import Protocol diff --git a/homeassistant/components/overkiz/climate_entities/atlantic_electrical_heater.py b/homeassistant/components/overkiz/climate_entities/atlantic_electrical_heater.py index 2678986574d..ce9857f9d8c 100644 --- a/homeassistant/components/overkiz/climate_entities/atlantic_electrical_heater.py +++ b/homeassistant/components/overkiz/climate_entities/atlantic_electrical_heater.py @@ -1,4 +1,5 @@ """Support for Atlantic Electrical Heater.""" + from __future__ import annotations from typing import cast diff --git a/homeassistant/components/overkiz/climate_entities/atlantic_electrical_heater_with_adjustable_temperature_setpoint.py b/homeassistant/components/overkiz/climate_entities/atlantic_electrical_heater_with_adjustable_temperature_setpoint.py index 36e958fb49c..64a7dc1e645 100644 --- a/homeassistant/components/overkiz/climate_entities/atlantic_electrical_heater_with_adjustable_temperature_setpoint.py +++ b/homeassistant/components/overkiz/climate_entities/atlantic_electrical_heater_with_adjustable_temperature_setpoint.py @@ -1,4 +1,5 @@ """Support for Atlantic Electrical Heater (With Adjustable Temperature Setpoint).""" + from __future__ import annotations from typing import Any @@ -142,7 +143,9 @@ class AtlanticElectricalHeaterWithAdjustableTemperatureSetpoint( @property def current_temperature(self) -> float | None: """Return the current temperature.""" - if temperature := self.temperature_device.states[OverkizState.CORE_TEMPERATURE]: + if self.temperature_device is not None and ( + temperature := self.temperature_device.states[OverkizState.CORE_TEMPERATURE] + ): return temperature.value_as_float return None diff --git a/homeassistant/components/overkiz/climate_entities/atlantic_electrical_towel_dryer.py b/homeassistant/components/overkiz/climate_entities/atlantic_electrical_towel_dryer.py index fefaa75a114..e49fc4358e9 100644 --- a/homeassistant/components/overkiz/climate_entities/atlantic_electrical_towel_dryer.py +++ b/homeassistant/components/overkiz/climate_entities/atlantic_electrical_towel_dryer.py @@ -1,4 +1,5 @@ """Support for Atlantic Electrical Towel Dryer.""" + from __future__ import annotations from typing import Any, cast @@ -94,7 +95,9 @@ class AtlanticElectricalTowelDryer(OverkizEntity, ClimateEntity): @property def current_temperature(self) -> float | None: """Return the current temperature.""" - if temperature := self.temperature_device.states[OverkizState.CORE_TEMPERATURE]: + if self.temperature_device is not None and ( + temperature := self.temperature_device.states[OverkizState.CORE_TEMPERATURE] + ): return cast(float, temperature.value) return None diff --git a/homeassistant/components/overkiz/climate_entities/atlantic_heat_recovery_ventilation.py b/homeassistant/components/overkiz/climate_entities/atlantic_heat_recovery_ventilation.py index 5876f7df4a7..f1d96b5687b 100644 --- a/homeassistant/components/overkiz/climate_entities/atlantic_heat_recovery_ventilation.py +++ b/homeassistant/components/overkiz/climate_entities/atlantic_heat_recovery_ventilation.py @@ -1,4 +1,5 @@ """Support for AtlanticHeatRecoveryVentilation.""" + from __future__ import annotations from typing import cast @@ -68,7 +69,9 @@ class AtlanticHeatRecoveryVentilation(OverkizEntity, ClimateEntity): @property def current_temperature(self) -> float | None: """Return the current temperature.""" - if temperature := self.temperature_device.states[OverkizState.CORE_TEMPERATURE]: + if self.temperature_device is not None and ( + temperature := self.temperature_device.states[OverkizState.CORE_TEMPERATURE] + ): return cast(float, temperature.value) return None diff --git a/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_heating_zone.py b/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_heating_zone.py index 157ec72a249..3da2ccc922b 100644 --- a/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_heating_zone.py +++ b/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_heating_zone.py @@ -1,4 +1,5 @@ """Support for Atlantic Pass APC Heating Control.""" + from __future__ import annotations from typing import Any, cast @@ -107,7 +108,9 @@ class AtlanticPassAPCHeatingZone(OverkizEntity, ClimateEntity): @property def current_temperature(self) -> float | None: """Return the current temperature.""" - if temperature := self.temperature_device.states[OverkizState.CORE_TEMPERATURE]: + if self.temperature_device is not None and ( + temperature := self.temperature_device.states[OverkizState.CORE_TEMPERATURE] + ): return cast(float, temperature.value) return None @@ -156,7 +159,7 @@ class AtlanticPassAPCHeatingZone(OverkizEntity, ClimateEntity): await self.async_set_heating_mode(PRESET_MODES_TO_OVERKIZ[preset_mode]) @property - def preset_mode(self) -> str: + def preset_mode(self) -> str | None: """Return the current preset mode, e.g., home, away, temp.""" heating_mode = cast( str, self.executor.select_state(OverkizState.IO_PASS_APC_HEATING_MODE) @@ -176,7 +179,7 @@ class AtlanticPassAPCHeatingZone(OverkizEntity, ClimateEntity): return OVERKIZ_TO_PRESET_MODES[heating_mode] @property - def target_temperature(self) -> float: + def target_temperature(self) -> float | None: """Return hvac target temperature.""" current_heating_profile = self.current_heating_profile if current_heating_profile in OVERKIZ_TEMPERATURE_STATE_BY_PROFILE: diff --git a/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_zone_control.py b/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_zone_control.py index cfb92067875..7fbab821b8d 100644 --- a/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_zone_control.py +++ b/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_zone_control.py @@ -1,4 +1,5 @@ """Support for Atlantic Pass APC Zone Control.""" + from typing import cast from pyoverkiz.enums import OverkizCommand, OverkizCommandParam, OverkizState diff --git a/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_zone_control_zone.py b/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_zone_control_zone.py index a30cb93f287..f18edd0cfe6 100644 --- a/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_zone_control_zone.py +++ b/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_zone_control_zone.py @@ -1,17 +1,26 @@ """Support for Atlantic Pass APC Heating Control.""" + from __future__ import annotations from asyncio import sleep +from functools import cached_property from typing import Any, cast from pyoverkiz.enums import OverkizCommand, OverkizCommandParam, OverkizState -from homeassistant.components.climate import PRESET_NONE, HVACMode -from homeassistant.const import ATTR_TEMPERATURE +from homeassistant.components.climate import ( + ATTR_TARGET_TEMP_HIGH, + ATTR_TARGET_TEMP_LOW, + PRESET_NONE, + ClimateEntityFeature, + HVACAction, + HVACMode, +) +from homeassistant.const import ATTR_TEMPERATURE, PRECISION_HALVES from ..coordinator import OverkizDataUpdateCoordinator +from ..executor import OverkizExecutor from .atlantic_pass_apc_heating_zone import AtlanticPassAPCHeatingZone -from .atlantic_pass_apc_zone_control import OVERKIZ_TO_HVAC_MODE PRESET_SCHEDULE = "schedule" PRESET_MANUAL = "manual" @@ -23,32 +32,127 @@ OVERKIZ_MODE_TO_PRESET_MODES: dict[str, str] = { PRESET_MODES_TO_OVERKIZ = {v: k for k, v in OVERKIZ_MODE_TO_PRESET_MODES.items()} +# Maps the HVAC current ZoneControl system operating mode. +OVERKIZ_TO_HVAC_ACTION: dict[str, HVACAction] = { + OverkizCommandParam.COOLING: HVACAction.COOLING, + OverkizCommandParam.DRYING: HVACAction.DRYING, + OverkizCommandParam.HEATING: HVACAction.HEATING, + # There is no known way to differentiate OFF from Idle. + OverkizCommandParam.STOP: HVACAction.OFF, +} + +HVAC_ACTION_TO_OVERKIZ_PROFILE_STATE: dict[HVACAction, OverkizState] = { + HVACAction.COOLING: OverkizState.IO_PASS_APC_COOLING_PROFILE, + HVACAction.HEATING: OverkizState.IO_PASS_APC_HEATING_PROFILE, +} + +HVAC_ACTION_TO_OVERKIZ_MODE_STATE: dict[HVACAction, OverkizState] = { + HVACAction.COOLING: OverkizState.IO_PASS_APC_COOLING_MODE, + HVACAction.HEATING: OverkizState.IO_PASS_APC_HEATING_MODE, +} + TEMPERATURE_ZONECONTROL_DEVICE_INDEX = 1 +SUPPORTED_FEATURES: ClimateEntityFeature = ( + ClimateEntityFeature.PRESET_MODE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON +) -# Those device depends on a main probe that choose the operating mode (heating, cooling, ...) +OVERKIZ_THERMAL_CONFIGURATION_TO_HVAC_MODE: dict[ + OverkizCommandParam, tuple[HVACMode, ClimateEntityFeature] +] = { + OverkizCommandParam.COOLING: ( + HVACMode.COOL, + SUPPORTED_FEATURES | ClimateEntityFeature.TARGET_TEMPERATURE, + ), + OverkizCommandParam.HEATING: ( + HVACMode.HEAT, + SUPPORTED_FEATURES | ClimateEntityFeature.TARGET_TEMPERATURE, + ), + OverkizCommandParam.HEATING_AND_COOLING: ( + HVACMode.HEAT_COOL, + SUPPORTED_FEATURES | ClimateEntityFeature.TARGET_TEMPERATURE_RANGE, + ), +} + + +# Those device depends on a main probe that choose the operating mode (heating, cooling, ...). class AtlanticPassAPCZoneControlZone(AtlanticPassAPCHeatingZone): """Representation of Atlantic Pass APC Heating And Cooling Zone Control.""" + _attr_target_temperature_step = PRECISION_HALVES + def __init__( self, device_url: str, coordinator: OverkizDataUpdateCoordinator ) -> None: """Init method.""" super().__init__(device_url, coordinator) - # There is less supported functions, because they depend on the ZoneControl. - if not self.is_using_derogated_temperature_fallback: - # Modes are not configurable, they will follow current HVAC Mode of Zone Control. - self._attr_hvac_modes = [] + # When using derogated temperature, we fallback to legacy behavior. + if self.is_using_derogated_temperature_fallback: + return - # Those are available and tested presets on Shogun. - self._attr_preset_modes = [*PRESET_MODES_TO_OVERKIZ] + self._attr_hvac_modes = [] + self._attr_supported_features = ClimateEntityFeature(0) + + # Modes depends on device capabilities. + if (thermal_configuration := self.thermal_configuration) is not None: + ( + device_hvac_mode, + climate_entity_feature, + ) = thermal_configuration + self._attr_hvac_modes = [device_hvac_mode, HVACMode.OFF] + self._attr_supported_features = climate_entity_feature + + # Those are available and tested presets on Shogun. + self._attr_preset_modes = [*PRESET_MODES_TO_OVERKIZ] # Those APC Heating and Cooling probes depends on the zone control device (main probe). # Only the base device (#1) can be used to get/set some states. # Like to retrieve and set the current operating mode (heating, cooling, drying, off). - self.zone_control_device = self.executor.linked_device( - TEMPERATURE_ZONECONTROL_DEVICE_INDEX + + self.zone_control_executor: OverkizExecutor | None = None + + if ( + zone_control_device := self.executor.linked_device( + TEMPERATURE_ZONECONTROL_DEVICE_INDEX + ) + ) is not None: + self.zone_control_executor = OverkizExecutor( + zone_control_device.device_url, + coordinator, + ) + + @cached_property + def thermal_configuration(self) -> tuple[HVACMode, ClimateEntityFeature] | None: + """Retrieve thermal configuration for this devices.""" + + if ( + ( + state_thermal_configuration := cast( + OverkizCommandParam | None, + self.executor.select_state(OverkizState.CORE_THERMAL_CONFIGURATION), + ) + ) + is not None + and state_thermal_configuration + in OVERKIZ_THERMAL_CONFIGURATION_TO_HVAC_MODE + ): + return OVERKIZ_THERMAL_CONFIGURATION_TO_HVAC_MODE[ + state_thermal_configuration + ] + + return None + + @cached_property + def device_hvac_mode(self) -> HVACMode | None: + """ZoneControlZone device has a single possible mode.""" + + return ( + None + if self.thermal_configuration is None + else self.thermal_configuration[0] ) @property @@ -60,16 +164,37 @@ class AtlanticPassAPCZoneControlZone(AtlanticPassAPCHeatingZone): ) @property - def zone_control_hvac_mode(self) -> HVACMode: + def zone_control_hvac_action(self) -> HVACAction: """Return hvac operation ie. heat, cool, dry, off mode.""" + if self.zone_control_executor is not None and ( + ( + state := self.zone_control_executor.select_state( + OverkizState.IO_PASS_APC_OPERATING_MODE + ) + ) + is not None + ): + return OVERKIZ_TO_HVAC_ACTION[cast(str, state)] + + return HVACAction.OFF + + @property + def hvac_action(self) -> HVACAction | None: + """Return the current running hvac operation.""" + + # When ZoneControl action is heating/cooling but Zone is stopped, means the zone is idle. if ( - state := self.zone_control_device.states[ - OverkizState.IO_PASS_APC_OPERATING_MODE - ] - ) is not None and (value := state.value_as_str) is not None: - return OVERKIZ_TO_HVAC_MODE[value] - return HVACMode.OFF + hvac_action := self.zone_control_hvac_action + ) in HVAC_ACTION_TO_OVERKIZ_PROFILE_STATE and cast( + str, + self.executor.select_state( + HVAC_ACTION_TO_OVERKIZ_PROFILE_STATE[hvac_action] + ), + ) == OverkizCommandParam.STOP: + return HVACAction.IDLE + + return hvac_action @property def hvac_mode(self) -> HVACMode: @@ -78,30 +203,32 @@ class AtlanticPassAPCZoneControlZone(AtlanticPassAPCHeatingZone): if self.is_using_derogated_temperature_fallback: return super().hvac_mode - zone_control_hvac_mode = self.zone_control_hvac_mode + if (device_hvac_mode := self.device_hvac_mode) is None: + return HVACMode.OFF - # Should be same, because either thermostat or this integration change both. - on_off_state = cast( + cooling_is_off = cast( str, - self.executor.select_state( - OverkizState.CORE_COOLING_ON_OFF - if zone_control_hvac_mode == HVACMode.COOL - else OverkizState.CORE_HEATING_ON_OFF - ), - ) + self.executor.select_state(OverkizState.CORE_COOLING_ON_OFF), + ) in (OverkizCommandParam.OFF, None) + + heating_is_off = cast( + str, + self.executor.select_state(OverkizState.CORE_HEATING_ON_OFF), + ) in (OverkizCommandParam.OFF, None) # Device is Stopped, it means the air flux is flowing but its venting door is closed. - if on_off_state == OverkizCommandParam.OFF: - hvac_mode = HVACMode.OFF - else: - hvac_mode = zone_control_hvac_mode + if ( + (device_hvac_mode == HVACMode.COOL and cooling_is_off) + or (device_hvac_mode == HVACMode.HEAT and heating_is_off) + or ( + device_hvac_mode == HVACMode.HEAT_COOL + and cooling_is_off + and heating_is_off + ) + ): + return HVACMode.OFF - # It helps keep it consistent with the Zone Control, within the interface. - if self._attr_hvac_modes != [zone_control_hvac_mode, HVACMode.OFF]: - self._attr_hvac_modes = [zone_control_hvac_mode, HVACMode.OFF] - self.async_write_ha_state() - - return hvac_mode + return device_hvac_mode async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set new target hvac mode.""" @@ -112,46 +239,49 @@ class AtlanticPassAPCZoneControlZone(AtlanticPassAPCHeatingZone): # They are mainly managed by the Zone Control device # However, it make sense to map the OFF Mode to the Overkiz STOP Preset - if hvac_mode == HVACMode.OFF: - await self.executor.async_execute_command( - OverkizCommand.SET_COOLING_ON_OFF, - OverkizCommandParam.OFF, - ) - await self.executor.async_execute_command( - OverkizCommand.SET_HEATING_ON_OFF, - OverkizCommandParam.OFF, - ) - else: - await self.executor.async_execute_command( - OverkizCommand.SET_COOLING_ON_OFF, - OverkizCommandParam.ON, - ) - await self.executor.async_execute_command( - OverkizCommand.SET_HEATING_ON_OFF, - OverkizCommandParam.ON, - ) + on_off_target_command_param = ( + OverkizCommandParam.OFF + if hvac_mode == HVACMode.OFF + else OverkizCommandParam.ON + ) + + await self.executor.async_execute_command( + OverkizCommand.SET_COOLING_ON_OFF, + on_off_target_command_param, + ) + await self.executor.async_execute_command( + OverkizCommand.SET_HEATING_ON_OFF, + on_off_target_command_param, + ) await self.async_refresh_modes() @property - def preset_mode(self) -> str: + def preset_mode(self) -> str | None: """Return the current preset mode, e.g., schedule, manual.""" if self.is_using_derogated_temperature_fallback: return super().preset_mode - mode = OVERKIZ_MODE_TO_PRESET_MODES[ - cast( - str, - self.executor.select_state( - OverkizState.IO_PASS_APC_COOLING_MODE - if self.zone_control_hvac_mode == HVACMode.COOL - else OverkizState.IO_PASS_APC_HEATING_MODE - ), + if ( + self.zone_control_hvac_action in HVAC_ACTION_TO_OVERKIZ_MODE_STATE + and ( + mode_state := HVAC_ACTION_TO_OVERKIZ_MODE_STATE[ + self.zone_control_hvac_action + ] ) - ] + and ( + ( + mode := OVERKIZ_MODE_TO_PRESET_MODES[ + cast(str, self.executor.select_state(mode_state)) + ] + ) + is not None + ) + ): + return mode - return mode if mode is not None else PRESET_NONE + return PRESET_NONE async def async_set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode.""" @@ -172,13 +302,18 @@ class AtlanticPassAPCZoneControlZone(AtlanticPassAPCHeatingZone): await self.async_refresh_modes() @property - def target_temperature(self) -> float: + def target_temperature(self) -> float | None: """Return hvac target temperature.""" if self.is_using_derogated_temperature_fallback: return super().target_temperature - if self.zone_control_hvac_mode == HVACMode.COOL: + device_hvac_mode = self.device_hvac_mode + + if device_hvac_mode == HVACMode.HEAT_COOL: + return None + + if device_hvac_mode == HVACMode.COOL: return cast( float, self.executor.select_state( @@ -186,7 +321,7 @@ class AtlanticPassAPCZoneControlZone(AtlanticPassAPCHeatingZone): ), ) - if self.zone_control_hvac_mode == HVACMode.HEAT: + if device_hvac_mode == HVACMode.HEAT: return cast( float, self.executor.select_state( @@ -198,32 +333,73 @@ class AtlanticPassAPCZoneControlZone(AtlanticPassAPCHeatingZone): float, self.executor.select_state(OverkizState.CORE_TARGET_TEMPERATURE) ) + @property + def target_temperature_high(self) -> float | None: + """Return the highbound target temperature we try to reach (cooling).""" + + if self.device_hvac_mode != HVACMode.HEAT_COOL: + return None + + return cast( + float, + self.executor.select_state(OverkizState.CORE_COOLING_TARGET_TEMPERATURE), + ) + + @property + def target_temperature_low(self) -> float | None: + """Return the lowbound target temperature we try to reach (heating).""" + + if self.device_hvac_mode != HVACMode.HEAT_COOL: + return None + + return cast( + float, + self.executor.select_state(OverkizState.CORE_HEATING_TARGET_TEMPERATURE), + ) + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new temperature.""" if self.is_using_derogated_temperature_fallback: return await super().async_set_temperature(**kwargs) - temperature = kwargs[ATTR_TEMPERATURE] + target_temperature = kwargs.get(ATTR_TEMPERATURE) + target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW) + target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH) + hvac_mode = self.hvac_mode + + if hvac_mode == HVACMode.HEAT_COOL: + if target_temp_low is not None: + await self.executor.async_execute_command( + OverkizCommand.SET_HEATING_TARGET_TEMPERATURE, + target_temp_low, + ) + + if target_temp_high is not None: + await self.executor.async_execute_command( + OverkizCommand.SET_COOLING_TARGET_TEMPERATURE, + target_temp_high, + ) + + elif target_temperature is not None: + if hvac_mode == HVACMode.HEAT: + await self.executor.async_execute_command( + OverkizCommand.SET_HEATING_TARGET_TEMPERATURE, + target_temperature, + ) + + elif hvac_mode == HVACMode.COOL: + await self.executor.async_execute_command( + OverkizCommand.SET_COOLING_TARGET_TEMPERATURE, + target_temperature, + ) - # Change both (heating/cooling) temperature is a good way to have consistency - await self.executor.async_execute_command( - OverkizCommand.SET_HEATING_TARGET_TEMPERATURE, - temperature, - ) - await self.executor.async_execute_command( - OverkizCommand.SET_COOLING_TARGET_TEMPERATURE, - temperature, - ) await self.executor.async_execute_command( OverkizCommand.SET_DEROGATION_ON_OFF_STATE, - OverkizCommandParam.OFF, + OverkizCommandParam.ON, ) - # Target temperature may take up to 1 minute to get refreshed. - await self.executor.async_execute_command( - OverkizCommand.REFRESH_TARGET_TEMPERATURE - ) + await self.async_refresh_modes() async def async_refresh_modes(self) -> None: """Refresh the device modes to have new states.""" @@ -250,3 +426,51 @@ class AtlanticPassAPCZoneControlZone(AtlanticPassAPCHeatingZone): await self.executor.async_execute_command( OverkizCommand.REFRESH_TARGET_TEMPERATURE ) + + @property + def min_temp(self) -> float: + """Return Minimum Temperature for AC of this group.""" + + device_hvac_mode = self.device_hvac_mode + + if device_hvac_mode in (HVACMode.HEAT, HVACMode.HEAT_COOL): + return cast( + float, + self.executor.select_state( + OverkizState.CORE_MINIMUM_HEATING_TARGET_TEMPERATURE + ), + ) + + if device_hvac_mode == HVACMode.COOL: + return cast( + float, + self.executor.select_state( + OverkizState.CORE_MINIMUM_COOLING_TARGET_TEMPERATURE + ), + ) + + return super().min_temp + + @property + def max_temp(self) -> float: + """Return Max Temperature for AC of this group.""" + + device_hvac_mode = self.device_hvac_mode + + if device_hvac_mode == HVACMode.HEAT: + return cast( + float, + self.executor.select_state( + OverkizState.CORE_MAXIMUM_HEATING_TARGET_TEMPERATURE + ), + ) + + if device_hvac_mode in (HVACMode.COOL, HVACMode.HEAT_COOL): + return cast( + float, + self.executor.select_state( + OverkizState.CORE_MAXIMUM_COOLING_TARGET_TEMPERATURE + ), + ) + + return super().max_temp diff --git a/homeassistant/components/overkiz/climate_entities/hitachi_air_to_air_heat_pump_hlrrwifi.py b/homeassistant/components/overkiz/climate_entities/hitachi_air_to_air_heat_pump_hlrrwifi.py index 9b956acd014..efdae2165a9 100644 --- a/homeassistant/components/overkiz/climate_entities/hitachi_air_to_air_heat_pump_hlrrwifi.py +++ b/homeassistant/components/overkiz/climate_entities/hitachi_air_to_air_heat_pump_hlrrwifi.py @@ -1,4 +1,5 @@ """Support for HitachiAirToAirHeatPump.""" + from __future__ import annotations from typing import Any, cast diff --git a/homeassistant/components/overkiz/climate_entities/hitachi_air_to_air_heat_pump_ovp.py b/homeassistant/components/overkiz/climate_entities/hitachi_air_to_air_heat_pump_ovp.py index bf6bb5f95d5..b31ecf91ec0 100644 --- a/homeassistant/components/overkiz/climate_entities/hitachi_air_to_air_heat_pump_ovp.py +++ b/homeassistant/components/overkiz/climate_entities/hitachi_air_to_air_heat_pump_ovp.py @@ -1,4 +1,5 @@ """Support for HitachiAirToAirHeatPump.""" + from __future__ import annotations from typing import Any @@ -94,6 +95,7 @@ class HitachiAirToAirHeatPumpOVP(OverkizEntity, ClimateEntity): _attr_target_temperature_step = 1.0 _attr_temperature_unit = UnitOfTemperature.CELSIUS _attr_translation_key = DOMAIN + _enable_turn_on_off_backwards_compatibility = False def __init__( self, device_url: str, coordinator: OverkizDataUpdateCoordinator @@ -105,6 +107,8 @@ class HitachiAirToAirHeatPumpOVP(OverkizEntity, ClimateEntity): ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE | ClimateEntityFeature.PRESET_MODE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON ) if self.device.states.get(OverkizState.OVP_SWING): @@ -294,6 +298,11 @@ class HitachiAirToAirHeatPumpOVP(OverkizEntity, ClimateEntity): OverkizState.OVP_FAN_SPEED, OverkizCommandParam.AUTO, ) + # Sanitize fan mode: Overkiz is sometimes providing a state that + # cannot be used as a command. Convert it to HA space and back to Overkiz + if fan_mode not in FAN_MODES_TO_OVERKIZ.values(): + fan_mode = FAN_MODES_TO_OVERKIZ[OVERKIZ_TO_FAN_MODES[fan_mode]] + hvac_mode = self._control_backfill( hvac_mode, OverkizState.OVP_MODE_CHANGE, @@ -353,5 +362,5 @@ class HitachiAirToAirHeatPumpOVP(OverkizEntity, ClimateEntity): ] await self.executor.async_execute_command( - OverkizCommand.GLOBAL_CONTROL, command_data + OverkizCommand.GLOBAL_CONTROL, *command_data ) diff --git a/homeassistant/components/overkiz/climate_entities/somfy_heating_temperature_interface.py b/homeassistant/components/overkiz/climate_entities/somfy_heating_temperature_interface.py index f98865456e1..85ce7ae57e3 100644 --- a/homeassistant/components/overkiz/climate_entities/somfy_heating_temperature_interface.py +++ b/homeassistant/components/overkiz/climate_entities/somfy_heating_temperature_interface.py @@ -1,4 +1,5 @@ """Support for Somfy Heating Temperature Interface.""" + from __future__ import annotations from typing import Any @@ -165,7 +166,9 @@ class SomfyHeatingTemperatureInterface(OverkizEntity, ClimateEntity): @property def current_temperature(self) -> float | None: """Return the current temperature.""" - if temperature := self.temperature_device.states[OverkizState.CORE_TEMPERATURE]: + if self.temperature_device is not None and ( + temperature := self.temperature_device.states[OverkizState.CORE_TEMPERATURE] + ): return temperature.value_as_float return None diff --git a/homeassistant/components/overkiz/climate_entities/somfy_thermostat.py b/homeassistant/components/overkiz/climate_entities/somfy_thermostat.py index 2b6840b463d..829a3bad03b 100644 --- a/homeassistant/components/overkiz/climate_entities/somfy_thermostat.py +++ b/homeassistant/components/overkiz/climate_entities/somfy_thermostat.py @@ -1,4 +1,5 @@ """Support for Somfy Smart Thermostat.""" + from __future__ import annotations from typing import Any, cast @@ -103,7 +104,9 @@ class SomfyThermostat(OverkizEntity, ClimateEntity): @property def current_temperature(self) -> float | None: """Return the current temperature.""" - if temperature := self.temperature_device.states[OverkizState.CORE_TEMPERATURE]: + if self.temperature_device is not None and ( + temperature := self.temperature_device.states[OverkizState.CORE_TEMPERATURE] + ): return cast(float, temperature.value) return None diff --git a/homeassistant/components/overkiz/climate_entities/valve_heating_temperature_interface.py b/homeassistant/components/overkiz/climate_entities/valve_heating_temperature_interface.py index 7b7493a37bb..e2165e8b6c6 100644 --- a/homeassistant/components/overkiz/climate_entities/valve_heating_temperature_interface.py +++ b/homeassistant/components/overkiz/climate_entities/valve_heating_temperature_interface.py @@ -1,4 +1,5 @@ """Support for ValveHeatingTemperatureInterface.""" + from __future__ import annotations from typing import Any, cast @@ -90,7 +91,9 @@ class ValveHeatingTemperatureInterface(OverkizEntity, ClimateEntity): @property def current_temperature(self) -> float | None: """Return the current temperature.""" - if temperature := self.temperature_device.states[OverkizState.CORE_TEMPERATURE]: + if self.temperature_device is not None and ( + temperature := self.temperature_device.states[OverkizState.CORE_TEMPERATURE] + ): return temperature.value_as_float return None diff --git a/homeassistant/components/overkiz/config_flow.py b/homeassistant/components/overkiz/config_flow.py index 5ded6de86f3..f95f885f7ef 100644 --- a/homeassistant/components/overkiz/config_flow.py +++ b/homeassistant/components/overkiz/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Overkiz integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -22,9 +23,8 @@ from pyoverkiz.obfuscate import obfuscate_id from pyoverkiz.utils import generate_local_server, is_overkiz_gateway import voluptuous as vol -from homeassistant import config_entries from homeassistant.components import dhcp, zeroconf -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, @@ -32,7 +32,6 @@ from homeassistant.const import ( CONF_USERNAME, CONF_VERIFY_SSL, ) -from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_create_clientsession @@ -43,7 +42,7 @@ class DeveloperModeDisabled(HomeAssistantError): """Error to indicate Somfy Developer Mode is disabled.""" -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class OverkizConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Overkiz (by Somfy).""" VERSION = 1 @@ -84,7 +83,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step via config flow.""" if user_input: self._server = user_input[CONF_HUB] @@ -109,7 +108,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_local_or_cloud( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Users can choose between local API or cloud API via config flow.""" if user_input: self._api_type = user_input[CONF_API_TYPE] @@ -135,7 +134,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_cloud( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the cloud authentication step via config flow.""" errors: dict[str, str] = {} description_placeholders = {} @@ -217,7 +216,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_local( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the local authentication step via config flow.""" errors = {} description_placeholders = {} @@ -300,7 +299,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: + async def async_step_dhcp( + self, discovery_info: dhcp.DhcpServiceInfo + ) -> ConfigFlowResult: """Handle DHCP discovery.""" hostname = discovery_info.hostname gateway_id = hostname[8:22] @@ -311,7 +312,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_zeroconf( self, discovery_info: zeroconf.ZeroconfServiceInfo - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle ZeroConf discovery.""" properties = discovery_info.properties gateway_id = properties["gateway_pin"] @@ -333,7 +334,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return await self._process_discovery(gateway_id) - async def _process_discovery(self, gateway_id: str) -> FlowResult: + async def _process_discovery(self, gateway_id: str) -> ConfigFlowResult: """Handle discovery of a gateway.""" await self.async_set_unique_id(gateway_id) self._abort_if_unique_id_configured() @@ -341,7 +342,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_user() - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Handle reauth.""" self._reauth_entry = cast( ConfigEntry, diff --git a/homeassistant/components/overkiz/const.py b/homeassistant/components/overkiz/const.py index 0f30f64444b..59acc4ac232 100644 --- a/homeassistant/components/overkiz/const.py +++ b/homeassistant/components/overkiz/const.py @@ -1,4 +1,5 @@ """Constants for the Overkiz (by Somfy) integration.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/overkiz/coordinator.py b/homeassistant/components/overkiz/coordinator.py index 4630af8bbf8..17068d26b7c 100644 --- a/homeassistant/components/overkiz/coordinator.py +++ b/homeassistant/components/overkiz/coordinator.py @@ -1,4 +1,5 @@ """Helpers to help coordinate updates.""" + from __future__ import annotations from collections.abc import Callable, Coroutine diff --git a/homeassistant/components/overkiz/cover.py b/homeassistant/components/overkiz/cover.py index 4e741aa68e6..51d2c9f2334 100644 --- a/homeassistant/components/overkiz/cover.py +++ b/homeassistant/components/overkiz/cover.py @@ -1,4 +1,5 @@ """Support for Overkiz covers - shutters etc.""" + from pyoverkiz.enums import OverkizCommand, UIClass from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/overkiz/cover_entities/awning.py b/homeassistant/components/overkiz/cover_entities/awning.py index da940d59218..4b6e5b176a7 100644 --- a/homeassistant/components/overkiz/cover_entities/awning.py +++ b/homeassistant/components/overkiz/cover_entities/awning.py @@ -1,4 +1,5 @@ """Support for Overkiz awnings.""" + from __future__ import annotations from typing import Any, cast diff --git a/homeassistant/components/overkiz/cover_entities/generic_cover.py b/homeassistant/components/overkiz/cover_entities/generic_cover.py index f4a8a6a0d45..df13072524d 100644 --- a/homeassistant/components/overkiz/cover_entities/generic_cover.py +++ b/homeassistant/components/overkiz/cover_entities/generic_cover.py @@ -1,4 +1,5 @@ """Base class for Overkiz covers, shutters, awnings, etc.""" + from __future__ import annotations from typing import Any, cast diff --git a/homeassistant/components/overkiz/cover_entities/vertical_cover.py b/homeassistant/components/overkiz/cover_entities/vertical_cover.py index 2bc6f73103f..48ac2c838c5 100644 --- a/homeassistant/components/overkiz/cover_entities/vertical_cover.py +++ b/homeassistant/components/overkiz/cover_entities/vertical_cover.py @@ -1,4 +1,5 @@ """Support for Overkiz Vertical Covers.""" + from __future__ import annotations from typing import Any, cast diff --git a/homeassistant/components/overkiz/diagnostics.py b/homeassistant/components/overkiz/diagnostics.py index cb8cf6eb22f..427230b9c82 100644 --- a/homeassistant/components/overkiz/diagnostics.py +++ b/homeassistant/components/overkiz/diagnostics.py @@ -1,4 +1,5 @@ """Provides diagnostics for Overkiz.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/overkiz/entity.py b/homeassistant/components/overkiz/entity.py index 3c0170e543f..c13b2fc96ba 100644 --- a/homeassistant/components/overkiz/entity.py +++ b/homeassistant/components/overkiz/entity.py @@ -1,4 +1,5 @@ """Parent class for every Overkiz device.""" + from __future__ import annotations from typing import cast diff --git a/homeassistant/components/overkiz/executor.py b/homeassistant/components/overkiz/executor.py index af29dbaf523..94b2c1b25fa 100644 --- a/homeassistant/components/overkiz/executor.py +++ b/homeassistant/components/overkiz/executor.py @@ -1,4 +1,5 @@ """Class for helpers and communication with the OverKiz API.""" + from __future__ import annotations from typing import Any, cast @@ -40,9 +41,9 @@ class OverkizExecutor: """Return Overkiz device linked to this entity.""" return self.coordinator.data[self.device_url] - def linked_device(self, index: int) -> Device: + def linked_device(self, index: int) -> Device | None: """Return Overkiz device sharing the same base url.""" - return self.coordinator.data[f"{self.base_device_url}#{index}"] + return self.coordinator.data.get(f"{self.base_device_url}#{index}") def select_command(self, *commands: str) -> str | None: """Select first existing command in a list of commands.""" diff --git a/homeassistant/components/overkiz/light.py b/homeassistant/components/overkiz/light.py index bb06b645d24..18d724dd63a 100644 --- a/homeassistant/components/overkiz/light.py +++ b/homeassistant/components/overkiz/light.py @@ -1,4 +1,5 @@ """Support for Overkiz lights.""" + from __future__ import annotations from typing import Any, cast diff --git a/homeassistant/components/overkiz/lock.py b/homeassistant/components/overkiz/lock.py index 8a333652b75..2494903d076 100644 --- a/homeassistant/components/overkiz/lock.py +++ b/homeassistant/components/overkiz/lock.py @@ -1,4 +1,5 @@ """Support for Overkiz locks.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/overkiz/manifest.json b/homeassistant/components/overkiz/manifest.json index db24a299f2a..2ef0f0ebef4 100644 --- a/homeassistant/components/overkiz/manifest.json +++ b/homeassistant/components/overkiz/manifest.json @@ -19,7 +19,7 @@ "integration_type": "hub", "iot_class": "local_polling", "loggers": ["boto3", "botocore", "pyhumps", "pyoverkiz", "s3transfer"], - "requirements": ["pyoverkiz==1.13.8"], + "requirements": ["pyoverkiz==1.13.9"], "zeroconf": [ { "type": "_kizbox._tcp.local.", diff --git a/homeassistant/components/overkiz/number.py b/homeassistant/components/overkiz/number.py index b53dbb5db75..494d430c393 100644 --- a/homeassistant/components/overkiz/number.py +++ b/homeassistant/components/overkiz/number.py @@ -1,4 +1,5 @@ """Support for Overkiz (virtual) numbers.""" + from __future__ import annotations import asyncio @@ -27,23 +28,18 @@ BOOST_MODE_DURATION_DELAY = 1 OPERATING_MODE_DELAY = 3 -@dataclass(frozen=True) -class OverkizNumberDescriptionMixin: - """Define an entity description mixin for number entities.""" +@dataclass(frozen=True, kw_only=True) +class OverkizNumberDescription(NumberEntityDescription): + """Class to describe an Overkiz number.""" command: str - -@dataclass(frozen=True) -class OverkizNumberDescription(NumberEntityDescription, OverkizNumberDescriptionMixin): - """Class to describe an Overkiz number.""" - min_value_state_name: str | None = None max_value_state_name: str | None = None inverted: bool = False - set_native_value: Callable[ - [float, Callable[..., Awaitable[None]]], Awaitable[None] - ] | None = None + set_native_value: ( + Callable[[float, Callable[..., Awaitable[None]]], Awaitable[None]] | None + ) = None async def _async_set_native_value_boost_mode_duration( @@ -101,6 +97,28 @@ NUMBER_DESCRIPTIONS: list[OverkizNumberDescription] = [ max_value_state_name=OverkizState.CORE_MAXIMAL_SHOWER_MANUAL_MODE, entity_category=EntityCategory.CONFIG, ), + OverkizNumberDescription( + key=OverkizState.CORE_TARGET_DWH_TEMPERATURE, + name="Target temperature", + device_class=NumberDeviceClass.TEMPERATURE, + command=OverkizCommand.SET_TARGET_DHW_TEMPERATURE, + native_min_value=50, + native_max_value=65, + min_value_state_name=OverkizState.CORE_MINIMAL_TEMPERATURE_MANUAL_MODE, + max_value_state_name=OverkizState.CORE_MAXIMAL_TEMPERATURE_MANUAL_MODE, + entity_category=EntityCategory.CONFIG, + ), + OverkizNumberDescription( + key=OverkizState.CORE_WATER_TARGET_TEMPERATURE, + name="Water target temperature", + device_class=NumberDeviceClass.TEMPERATURE, + command=OverkizCommand.SET_WATER_TARGET_TEMPERATURE, + native_min_value=50, + native_max_value=65, + min_value_state_name=OverkizState.CORE_MINIMAL_TEMPERATURE_MANUAL_MODE, + max_value_state_name=OverkizState.CORE_MAXIMAL_TEMPERATURE_MANUAL_MODE, + entity_category=EntityCategory.CONFIG, + ), # SomfyHeatingTemperatureInterface OverkizNumberDescription( key=OverkizState.CORE_ECO_ROOM_TEMPERATURE, @@ -187,15 +205,15 @@ async def async_setup_entry( ): continue - for state in device.definition.states: - if description := SUPPORTED_STATES.get(state.qualified_name): - entities.append( - OverkizNumber( - device.device_url, - data.coordinator, - description, - ) - ) + entities.extend( + OverkizNumber( + device.device_url, + data.coordinator, + description, + ) + for state in device.definition.states + if (description := SUPPORTED_STATES.get(state.qualified_name)) + ) async_add_entities(entities) diff --git a/homeassistant/components/overkiz/scene.py b/homeassistant/components/overkiz/scene.py index 464b19d87e6..8cbbb9dbe5d 100644 --- a/homeassistant/components/overkiz/scene.py +++ b/homeassistant/components/overkiz/scene.py @@ -1,4 +1,5 @@ """Support for Overkiz scenes.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/overkiz/select.py b/homeassistant/components/overkiz/select.py index c225d475f63..83cdc9c4f2b 100644 --- a/homeassistant/components/overkiz/select.py +++ b/homeassistant/components/overkiz/select.py @@ -1,4 +1,5 @@ """Support for Overkiz select.""" + from __future__ import annotations from collections.abc import Awaitable, Callable @@ -17,18 +18,13 @@ from .const import DOMAIN, IGNORED_OVERKIZ_DEVICES from .entity import OverkizDescriptiveEntity -@dataclass(frozen=True) -class OverkizSelectDescriptionMixin: - """Define an entity description mixin for select entities.""" +@dataclass(frozen=True, kw_only=True) +class OverkizSelectDescription(SelectEntityDescription): + """Class to describe an Overkiz select entity.""" select_option: Callable[[str, Callable[..., Awaitable[None]]], Awaitable[None]] -@dataclass(frozen=True) -class OverkizSelectDescription(SelectEntityDescription, OverkizSelectDescriptionMixin): - """Class to describe an Overkiz select entity.""" - - def _select_option_open_closed_pedestrian( option: str, execute_command: Callable[..., Awaitable[None]] ) -> Awaitable[None]: @@ -147,15 +143,15 @@ async def async_setup_entry( ): continue - for state in device.definition.states: - if description := SUPPORTED_STATES.get(state.qualified_name): - entities.append( - OverkizSelect( - device.device_url, - data.coordinator, - description, - ) - ) + entities.extend( + OverkizSelect( + device.device_url, + data.coordinator, + description, + ) + for state in device.definition.states + if (description := SUPPORTED_STATES.get(state.qualified_name)) + ) async_add_entities(entities) diff --git a/homeassistant/components/overkiz/sensor.py b/homeassistant/components/overkiz/sensor.py index 3f1de4c381e..c62840eea97 100644 --- a/homeassistant/components/overkiz/sensor.py +++ b/homeassistant/components/overkiz/sensor.py @@ -1,4 +1,5 @@ """Support for Overkiz sensors.""" + from __future__ import annotations from collections.abc import Callable @@ -398,6 +399,20 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [ native_unit_of_measurement=UnitOfTime.SECONDS, entity_category=EntityCategory.DIAGNOSTIC, ), + OverkizSensorDescription( + key=OverkizState.CORE_BOTTOM_TANK_WATER_TEMPERATURE, + name="Bottom tank water temperature", + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + ), + OverkizSensorDescription( + key=OverkizState.CORE_CONTROL_WATER_TARGET_TEMPERATURE, + name="Control water target temperature", + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + ), # Cover OverkizSensorDescription( key=OverkizState.CORE_TARGET_CLOSURE, @@ -458,15 +473,15 @@ async def async_setup_entry( ): continue - for state in device.definition.states: - if description := SUPPORTED_STATES.get(state.qualified_name): - entities.append( - OverkizStateSensor( - device.device_url, - data.coordinator, - description, - ) - ) + entities.extend( + OverkizStateSensor( + device.device_url, + data.coordinator, + description, + ) + for state in device.definition.states + if (description := SUPPORTED_STATES.get(state.qualified_name)) + ) async_add_entities(entities) diff --git a/homeassistant/components/overkiz/siren.py b/homeassistant/components/overkiz/siren.py index f60eb04cfd3..a7ba41e2fef 100644 --- a/homeassistant/components/overkiz/siren.py +++ b/homeassistant/components/overkiz/siren.py @@ -1,4 +1,5 @@ """Support for Overkiz sirens.""" + from typing import Any from pyoverkiz.enums import OverkizState diff --git a/homeassistant/components/overkiz/switch.py b/homeassistant/components/overkiz/switch.py index 0396e385a3c..ac3ea351559 100644 --- a/homeassistant/components/overkiz/switch.py +++ b/homeassistant/components/overkiz/switch.py @@ -1,4 +1,5 @@ """Support for Overkiz switches.""" + from __future__ import annotations from collections.abc import Callable @@ -24,18 +25,12 @@ from .const import DOMAIN from .entity import OverkizDescriptiveEntity -@dataclass(frozen=True) -class OverkizSwitchDescriptionMixin: - """Define an entity description mixin for switch entities.""" +@dataclass(frozen=True, kw_only=True) +class OverkizSwitchDescription(SwitchEntityDescription): + """Class to describe an Overkiz switch.""" turn_on: str turn_off: str - - -@dataclass(frozen=True) -class OverkizSwitchDescription(SwitchEntityDescription, OverkizSwitchDescriptionMixin): - """Class to describe an Overkiz switch.""" - is_on: Callable[[Callable[[str], OverkizStateType]], bool] | None = None turn_on_args: OverkizStateType | list[OverkizStateType] | None = None turn_off_args: OverkizStateType | list[OverkizStateType] | None = None @@ -121,21 +116,19 @@ async def async_setup_entry( ) -> None: """Set up the Overkiz switch from a config entry.""" data: HomeAssistantOverkizData = hass.data[DOMAIN][entry.entry_id] - entities: list[OverkizSwitch] = [] - for device in data.platforms[Platform.SWITCH]: - if description := SUPPORTED_DEVICES.get(device.widget) or SUPPORTED_DEVICES.get( - device.ui_class - ): - entities.append( - OverkizSwitch( - device.device_url, - data.coordinator, - description, - ) - ) - - async_add_entities(entities) + async_add_entities( + OverkizSwitch( + device.device_url, + data.coordinator, + description, + ) + for device in data.platforms[Platform.SWITCH] + if ( + description := SUPPORTED_DEVICES.get(device.widget) + or SUPPORTED_DEVICES.get(device.ui_class) + ) + ) class OverkizSwitch(OverkizDescriptiveEntity, SwitchEntity): diff --git a/homeassistant/components/overkiz/water_heater.py b/homeassistant/components/overkiz/water_heater.py index e22f442c266..c76f6d5099f 100644 --- a/homeassistant/components/overkiz/water_heater.py +++ b/homeassistant/components/overkiz/water_heater.py @@ -1,4 +1,5 @@ """Support for Overkiz water heater devices.""" + from __future__ import annotations from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/overkiz/water_heater_entities/__init__.py b/homeassistant/components/overkiz/water_heater_entities/__init__.py index 71b66f6ea93..6f6539ef659 100644 --- a/homeassistant/components/overkiz/water_heater_entities/__init__.py +++ b/homeassistant/components/overkiz/water_heater_entities/__init__.py @@ -1,4 +1,5 @@ """Water heater entities for the Overkiz (by Somfy) integration.""" + from pyoverkiz.enums.ui import UIWidget from .atlantic_pass_apc_dhw import AtlanticPassAPCDHW diff --git a/homeassistant/components/overkiz/water_heater_entities/domestic_hot_water_production.py b/homeassistant/components/overkiz/water_heater_entities/domestic_hot_water_production.py index 2285e2dc3d2..abd3f40adc2 100644 --- a/homeassistant/components/overkiz/water_heater_entities/domestic_hot_water_production.py +++ b/homeassistant/components/overkiz/water_heater_entities/domestic_hot_water_production.py @@ -1,4 +1,5 @@ """Support for DomesticHotWaterProduction.""" + from __future__ import annotations from typing import Any, cast diff --git a/homeassistant/components/overkiz/water_heater_entities/hitachi_dhw.py b/homeassistant/components/overkiz/water_heater_entities/hitachi_dhw.py index 0dd5b70feb6..9f0a8798233 100644 --- a/homeassistant/components/overkiz/water_heater_entities/hitachi_dhw.py +++ b/homeassistant/components/overkiz/water_heater_entities/hitachi_dhw.py @@ -1,4 +1,5 @@ """Support for Hitachi DHW.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/ovo_energy/__init__.py b/homeassistant/components/ovo_energy/__init__.py index 3e2e868728d..6ad39ad82cb 100644 --- a/homeassistant/components/ovo_energy/__init__.py +++ b/homeassistant/components/ovo_energy/__init__.py @@ -1,4 +1,5 @@ """Support for OVO Energy.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/ovo_energy/config_flow.py b/homeassistant/components/ovo_energy/config_flow.py index 69fab07829f..41c64913764 100644 --- a/homeassistant/components/ovo_energy/config_flow.py +++ b/homeassistant/components/ovo_energy/config_flow.py @@ -1,4 +1,5 @@ """Config flow to configure the OVO Energy integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -8,9 +9,8 @@ import aiohttp from ovoenergy.ovoenergy import OVOEnergy import voluptuous as vol -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from homeassistant.data_entry_flow import FlowResult from .const import CONF_ACCOUNT, DOMAIN @@ -37,7 +37,7 @@ class OVOEnergyFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: Mapping[str, Any] | None = None, - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initiated by the user.""" errors = {} if user_input is not None: @@ -73,7 +73,7 @@ class OVOEnergyFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_reauth( self, user_input: Mapping[str, Any], - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle configuration by re-auth.""" errors = {} diff --git a/homeassistant/components/ovo_energy/const.py b/homeassistant/components/ovo_energy/const.py index 1068c5443fd..2d615e7c44a 100644 --- a/homeassistant/components/ovo_energy/const.py +++ b/homeassistant/components/ovo_energy/const.py @@ -1,4 +1,5 @@ """Constants for the OVO Energy integration.""" + DOMAIN = "ovo_energy" DATA_CLIENT = "ovo_client" diff --git a/homeassistant/components/ovo_energy/icons.json b/homeassistant/components/ovo_energy/icons.json new file mode 100644 index 00000000000..083e8ca8c2c --- /dev/null +++ b/homeassistant/components/ovo_energy/icons.json @@ -0,0 +1,12 @@ +{ + "entity": { + "sensor": { + "last_gas_reading": { + "default": "mdi:gas-cylinder" + }, + "last_gas_cost": { + "default": "mdi:cash-multiple" + } + } + } +} diff --git a/homeassistant/components/ovo_energy/sensor.py b/homeassistant/components/ovo_energy/sensor.py index 761515c9c84..76c084b368f 100644 --- a/homeassistant/components/ovo_energy/sensor.py +++ b/homeassistant/components/ovo_energy/sensor.py @@ -1,4 +1,5 @@ """Support for OVO Energy sensors.""" + from __future__ import annotations from collections.abc import Callable @@ -81,7 +82,6 @@ SENSOR_TYPES_GAS: tuple[OVOEnergySensorEntityDescription, ...] = ( device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, - icon="mdi:gas-cylinder", value=lambda usage: usage.gas[-1].consumption, ), OVOEnergySensorEntityDescription( @@ -89,7 +89,6 @@ SENSOR_TYPES_GAS: tuple[OVOEnergySensorEntityDescription, ...] = ( translation_key=KEY_LAST_GAS_COST, device_class=SensorDeviceClass.MONETARY, state_class=SensorStateClass.TOTAL_INCREASING, - icon="mdi:cash-multiple", value=lambda usage: usage.gas[-1].cost.amount if usage.gas[-1].cost is not None else None, diff --git a/homeassistant/components/owntracks/__init__.py b/homeassistant/components/owntracks/__init__.py index 1b3d67ce7b4..ffc50065c5a 100644 --- a/homeassistant/components/owntracks/__init__.py +++ b/homeassistant/components/owntracks/__init__.py @@ -1,4 +1,5 @@ """Support for OwnTracks.""" + from collections import defaultdict import json import logging @@ -186,19 +187,17 @@ async def handle_webhook(hass, webhook_id, request): async_dispatcher_send(hass, DOMAIN, hass, context, message) - response = [] - - for person in hass.states.async_all("person"): - if "latitude" in person.attributes and "longitude" in person.attributes: - response.append( - { - "_type": "location", - "lat": person.attributes["latitude"], - "lon": person.attributes["longitude"], - "tid": "".join(p[0] for p in person.name.split(" ")[:2]), - "tst": int(person.last_updated.timestamp()), - } - ) + response = [ + { + "_type": "location", + "lat": person.attributes["latitude"], + "lon": person.attributes["longitude"], + "tid": "".join(p[0] for p in person.name.split(" ")[:2]), + "tst": int(person.last_updated.timestamp()), + } + for person in hass.states.async_all("person") + if "latitude" in person.attributes and "longitude" in person.attributes + ] if message["_type"] == "encrypted" and context.secret: return json_response( diff --git a/homeassistant/components/owntracks/config_flow.py b/homeassistant/components/owntracks/config_flow.py index 13b2051ffa2..29fe4f0cf65 100644 --- a/homeassistant/components/owntracks/config_flow.py +++ b/homeassistant/components/owntracks/config_flow.py @@ -1,8 +1,9 @@ """Config flow for OwnTracks.""" + import secrets -from homeassistant import config_entries from homeassistant.components import cloud, webhook +from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_WEBHOOK_ID from .const import DOMAIN @@ -12,7 +13,7 @@ CONF_SECRET = "secret" CONF_CLOUDHOOK = "cloudhook" -class OwnTracksFlow(config_entries.ConfigFlow, domain=DOMAIN): +class OwnTracksFlow(ConfigFlow, domain=DOMAIN): """Set up OwnTracks.""" VERSION = 1 diff --git a/homeassistant/components/owntracks/device_tracker.py b/homeassistant/components/owntracks/device_tracker.py index e2053868cb9..8471f734196 100644 --- a/homeassistant/components/owntracks/device_tracker.py +++ b/homeassistant/components/owntracks/device_tracker.py @@ -1,4 +1,5 @@ """Device tracker platform that adds support for OwnTracks over MQTT.""" + from homeassistant.components.device_tracker import ( ATTR_SOURCE_TYPE, DOMAIN, diff --git a/homeassistant/components/owntracks/helper.py b/homeassistant/components/owntracks/helper.py index a9499059ba0..f88dcb03864 100644 --- a/homeassistant/components/owntracks/helper.py +++ b/homeassistant/components/owntracks/helper.py @@ -1,4 +1,5 @@ """Helper for OwnTracks.""" + try: import nacl except ImportError: diff --git a/homeassistant/components/owntracks/messages.py b/homeassistant/components/owntracks/messages.py index df61aa6e968..3e669079848 100644 --- a/homeassistant/components/owntracks/messages.py +++ b/homeassistant/components/owntracks/messages.py @@ -1,4 +1,5 @@ """OwnTracks Message handlers.""" + import json import logging diff --git a/homeassistant/components/p1_monitor/__init__.py b/homeassistant/components/p1_monitor/__init__.py index 18c58525097..201e76d4a76 100644 --- a/homeassistant/components/p1_monitor/__init__.py +++ b/homeassistant/components/p1_monitor/__init__.py @@ -1,4 +1,5 @@ """The P1 Monitor integration.""" + from __future__ import annotations from typing import TypedDict diff --git a/homeassistant/components/p1_monitor/config_flow.py b/homeassistant/components/p1_monitor/config_flow.py index 00b035aba7f..9c039d06b94 100644 --- a/homeassistant/components/p1_monitor/config_flow.py +++ b/homeassistant/components/p1_monitor/config_flow.py @@ -1,4 +1,5 @@ """Config flow for P1 Monitor integration.""" + from __future__ import annotations from typing import Any @@ -6,9 +7,8 @@ from typing import Any from p1monitor import P1Monitor, P1MonitorError import voluptuous as vol -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.selector import TextSelector @@ -22,7 +22,7 @@ class P1MonitorFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" errors = {} diff --git a/homeassistant/components/p1_monitor/const.py b/homeassistant/components/p1_monitor/const.py index 045301d38c4..297a06a9629 100644 --- a/homeassistant/components/p1_monitor/const.py +++ b/homeassistant/components/p1_monitor/const.py @@ -1,4 +1,5 @@ """Constants for the P1 Monitor integration.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/p1_monitor/diagnostics.py b/homeassistant/components/p1_monitor/diagnostics.py index b2668f060a4..b1b3bd2a506 100644 --- a/homeassistant/components/p1_monitor/diagnostics.py +++ b/homeassistant/components/p1_monitor/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for P1 Monitor.""" + from __future__ import annotations from dataclasses import asdict diff --git a/homeassistant/components/p1_monitor/icons.json b/homeassistant/components/p1_monitor/icons.json new file mode 100644 index 00000000000..d95084ca0e6 --- /dev/null +++ b/homeassistant/components/p1_monitor/icons.json @@ -0,0 +1,9 @@ +{ + "entity": { + "sensor": { + "energy_tariff_period": { + "default": "mdi:calendar-clock" + } + } + } +} diff --git a/homeassistant/components/p1_monitor/sensor.py b/homeassistant/components/p1_monitor/sensor.py index 587dc980e41..b97383bdae5 100644 --- a/homeassistant/components/p1_monitor/sensor.py +++ b/homeassistant/components/p1_monitor/sensor.py @@ -1,4 +1,5 @@ """Support for P1 Monitor sensors.""" + from __future__ import annotations from typing import Literal @@ -88,7 +89,6 @@ SENSORS_SMARTMETER: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="energy_tariff_period", translation_key="energy_tariff_period", - icon="mdi:calendar-clock", ), ) diff --git a/homeassistant/components/panasonic_bluray/media_player.py b/homeassistant/components/panasonic_bluray/media_player.py index ae017e7d72c..a121da93486 100644 --- a/homeassistant/components/panasonic_bluray/media_player.py +++ b/homeassistant/components/panasonic_bluray/media_player.py @@ -1,4 +1,5 @@ """Support for Panasonic Blu-ray players.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/panasonic_viera/__init__.py b/homeassistant/components/panasonic_viera/__init__.py index 6504aad7509..7d224c7126f 100644 --- a/homeassistant/components/panasonic_viera/__init__.py +++ b/homeassistant/components/panasonic_viera/__init__.py @@ -1,4 +1,5 @@ """The Panasonic Viera integration.""" + from functools import partial import logging from urllib.error import HTTPError, URLError diff --git a/homeassistant/components/panasonic_viera/config_flow.py b/homeassistant/components/panasonic_viera/config_flow.py index 80ab929231e..c06de119244 100644 --- a/homeassistant/components/panasonic_viera/config_flow.py +++ b/homeassistant/components/panasonic_viera/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Panasonic Viera TV integration.""" + from functools import partial import logging from urllib.error import URLError @@ -6,7 +7,7 @@ from urllib.error import URLError from panasonic_viera import TV_TYPE_ENCRYPTED, RemoteControl, SOAPError import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PIN, CONF_PORT from .const import ( @@ -25,7 +26,7 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class PanasonicVieraConfigFlow(ConfigFlow, domain=DOMAIN): """Config flow for Panasonic Viera.""" VERSION = 1 diff --git a/homeassistant/components/panasonic_viera/const.py b/homeassistant/components/panasonic_viera/const.py index a2e3fc2eece..f76c01e396b 100644 --- a/homeassistant/components/panasonic_viera/const.py +++ b/homeassistant/components/panasonic_viera/const.py @@ -1,4 +1,5 @@ """Constants for the Panasonic Viera integration.""" + DOMAIN = "panasonic_viera" DEVICE_MANUFACTURER = "Panasonic" diff --git a/homeassistant/components/panasonic_viera/media_player.py b/homeassistant/components/panasonic_viera/media_player.py index 9e7fe4168ab..44063022129 100644 --- a/homeassistant/components/panasonic_viera/media_player.py +++ b/homeassistant/components/panasonic_viera/media_player.py @@ -1,4 +1,5 @@ """Media player support for Panasonic Viera TV.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/panasonic_viera/remote.py b/homeassistant/components/panasonic_viera/remote.py index b0512ededce..c47dce36306 100644 --- a/homeassistant/components/panasonic_viera/remote.py +++ b/homeassistant/components/panasonic_viera/remote.py @@ -1,4 +1,5 @@ """Remote control support for Panasonic Viera TV.""" + from __future__ import annotations from collections.abc import Iterable diff --git a/homeassistant/components/pandora/media_player.py b/homeassistant/components/pandora/media_player.py index 7b09f40c3f1..eb6815959c2 100644 --- a/homeassistant/components/pandora/media_player.py +++ b/homeassistant/components/pandora/media_player.py @@ -1,4 +1,5 @@ """Component for controlling Pandora stations through the pianobar client.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/panel_custom/__init__.py b/homeassistant/components/panel_custom/__init__.py index 4f084d5900a..89ad6066f48 100644 --- a/homeassistant/components/panel_custom/__init__.py +++ b/homeassistant/components/panel_custom/__init__.py @@ -1,4 +1,5 @@ """Register a custom front end panel.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/panel_iframe/__init__.py b/homeassistant/components/panel_iframe/__init__.py index e33e5078288..1b6dfebd6b0 100644 --- a/homeassistant/components/panel_iframe/__init__.py +++ b/homeassistant/components/panel_iframe/__init__.py @@ -1,10 +1,14 @@ """Register an iFrame front end panel.""" + import voluptuous as vol -from homeassistant.components import frontend +from homeassistant.components import lovelace +from homeassistant.components.lovelace import dashboard from homeassistant.const import CONF_ICON, CONF_URL from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue +from homeassistant.helpers.storage import Store from homeassistant.helpers.typing import ConfigType DOMAIN = "panel_iframe" @@ -36,18 +40,59 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) +STORAGE_KEY = DOMAIN +STORAGE_VERSION_MAJOR = 1 + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the iFrame frontend panels.""" + async_create_issue( + hass, + DOMAIN, + "deprecated_yaml", + breaks_in_ha_version="2024.10.0", + is_fixable=False, + is_persistent=False, + issue_domain=DOMAIN, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": "iframe Panel", + }, + ) + + store: Store[dict[str, bool]] = Store( + hass, + STORAGE_VERSION_MAJOR, + STORAGE_KEY, + ) + data = await store.async_load() + if data: + return True + + dashboards_collection: dashboard.DashboardsCollection = hass.data[lovelace.DOMAIN][ + "dashboards_collection" + ] + for url_path, info in config[DOMAIN].items(): - frontend.async_register_built_in_panel( - hass, - "iframe", - info.get(CONF_TITLE), - info.get(CONF_ICON), - url_path, - {"url": info[CONF_URL]}, - require_admin=info[CONF_REQUIRE_ADMIN], + dashboard_create_data = { + lovelace.CONF_ALLOW_SINGLE_WORD: True, + lovelace.CONF_URL_PATH: url_path, + } + for key in (CONF_ICON, CONF_REQUIRE_ADMIN, CONF_TITLE): + if key in info: + dashboard_create_data[key] = info[key] + + await dashboards_collection.async_create_item(dashboard_create_data) + + dashboard_store: dashboard.LovelaceStorage = hass.data[lovelace.DOMAIN][ + "dashboards" + ][url_path] + await dashboard_store.async_save( + {"strategy": {"type": "iframe", "url": info[CONF_URL]}} ) + await store.async_save({"migrated": True}) + return True diff --git a/homeassistant/components/panel_iframe/manifest.json b/homeassistant/components/panel_iframe/manifest.json index 04eeb93ffa6..7a39e0ba17d 100644 --- a/homeassistant/components/panel_iframe/manifest.json +++ b/homeassistant/components/panel_iframe/manifest.json @@ -2,7 +2,7 @@ "domain": "panel_iframe", "name": "iframe Panel", "codeowners": ["@home-assistant/frontend"], - "dependencies": ["frontend"], + "dependencies": ["frontend", "lovelace"], "documentation": "https://www.home-assistant.io/integrations/panel_iframe", "quality_scale": "internal" } diff --git a/homeassistant/components/panel_iframe/strings.json b/homeassistant/components/panel_iframe/strings.json new file mode 100644 index 00000000000..595b1f04818 --- /dev/null +++ b/homeassistant/components/panel_iframe/strings.json @@ -0,0 +1,8 @@ +{ + "issues": { + "deprecated_yaml": { + "title": "The {integration_title} YAML configuration is being removed", + "description": "Configuring {integration_title} using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically as a regular dashboard.\n\nRemove the `{domain}` configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } + } +} diff --git a/homeassistant/components/peco/__init__.py b/homeassistant/components/peco/__init__.py index 52e988f0f60..168b045ff4d 100644 --- a/homeassistant/components/peco/__init__.py +++ b/homeassistant/components/peco/__init__.py @@ -1,4 +1,5 @@ """The PECO Outage Counter integration.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/homeassistant/components/peco/binary_sensor.py b/homeassistant/components/peco/binary_sensor.py index 7f0402b207f..a55f0fcc731 100644 --- a/homeassistant/components/peco/binary_sensor.py +++ b/homeassistant/components/peco/binary_sensor.py @@ -1,4 +1,5 @@ """Binary sensor for PECO outage counter.""" + from __future__ import annotations from typing import Final diff --git a/homeassistant/components/peco/config_flow.py b/homeassistant/components/peco/config_flow.py index 144495ec066..a5e8f4451fd 100644 --- a/homeassistant/components/peco/config_flow.py +++ b/homeassistant/components/peco/config_flow.py @@ -1,4 +1,5 @@ """Config flow for PECO Outage Counter integration.""" + from __future__ import annotations import logging @@ -12,8 +13,7 @@ from peco import ( ) import voluptuous as vol -from homeassistant import config_entries -from homeassistant.data_entry_flow import FlowResult +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.helpers import config_validation as cv from .const import CONF_COUNTY, CONF_PHONE_NUMBER, COUNTY_LIST, DOMAIN @@ -28,7 +28,7 @@ STEP_USER_DATA_SCHEMA = vol.Schema( _LOGGER = logging.getLogger(__name__) -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class PecoConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for PECO Outage Counter.""" VERSION = 1 @@ -54,7 +54,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" if user_input is None: return self.async_show_form( @@ -90,7 +90,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_finish_smart_meter( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the finish smart meter step.""" if "phone_number" in self.meter_error: if self.meter_error["type"] == "error": diff --git a/homeassistant/components/peco/const.py b/homeassistant/components/peco/const.py index 1df8ae41ecb..5d73057698f 100644 --- a/homeassistant/components/peco/const.py +++ b/homeassistant/components/peco/const.py @@ -1,4 +1,5 @@ """Constants for the PECO Outage Counter integration.""" + import logging from typing import Final diff --git a/homeassistant/components/peco/icons.json b/homeassistant/components/peco/icons.json new file mode 100644 index 00000000000..734c5262bbc --- /dev/null +++ b/homeassistant/components/peco/icons.json @@ -0,0 +1,21 @@ +{ + "entity": { + "sensor": { + "customers_out": { + "default": "mdi:power-plug-off" + }, + "percent_customers_out": { + "default": "mdi:power-plug-off" + }, + "outage_count": { + "default": "mdi:power-plug-off" + }, + "customers_served": { + "default": "mdi:power-plug-off" + }, + "map_alert": { + "default": "mdi:alert" + } + } + } +} diff --git a/homeassistant/components/peco/sensor.py b/homeassistant/components/peco/sensor.py index f9ad35fd251..d08947eb0ec 100644 --- a/homeassistant/components/peco/sensor.py +++ b/homeassistant/components/peco/sensor.py @@ -1,4 +1,5 @@ """Sensor component for PECO outage counter.""" + from __future__ import annotations from collections.abc import Callable @@ -24,21 +25,14 @@ from . import PECOCoordinatorData from .const import ATTR_CONTENT, CONF_COUNTY, DOMAIN -@dataclass(frozen=True) -class PECOSensorEntityDescriptionMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class PECOSensorEntityDescription(SensorEntityDescription): + """Description for PECO sensor.""" value_fn: Callable[[PECOCoordinatorData], int | str] attribute_fn: Callable[[PECOCoordinatorData], dict[str, str]] -@dataclass(frozen=True) -class PECOSensorEntityDescription( - SensorEntityDescription, PECOSensorEntityDescriptionMixin -): - """Description for PECO sensor.""" - - PARALLEL_UPDATES: Final = 0 SENSOR_LIST: tuple[PECOSensorEntityDescription, ...] = ( PECOSensorEntityDescription( @@ -46,7 +40,6 @@ SENSOR_LIST: tuple[PECOSensorEntityDescription, ...] = ( translation_key="customers_out", value_fn=lambda data: int(data.outages.customers_out), attribute_fn=lambda data: {}, - icon="mdi:power-plug-off", state_class=SensorStateClass.MEASUREMENT, ), PECOSensorEntityDescription( @@ -55,7 +48,6 @@ SENSOR_LIST: tuple[PECOSensorEntityDescription, ...] = ( native_unit_of_measurement=PERCENTAGE, value_fn=lambda data: int(data.outages.percent_customers_out), attribute_fn=lambda data: {}, - icon="mdi:power-plug-off", state_class=SensorStateClass.MEASUREMENT, ), PECOSensorEntityDescription( @@ -63,7 +55,6 @@ SENSOR_LIST: tuple[PECOSensorEntityDescription, ...] = ( translation_key="outage_count", value_fn=lambda data: int(data.outages.outage_count), attribute_fn=lambda data: {}, - icon="mdi:power-plug-off", state_class=SensorStateClass.MEASUREMENT, ), PECOSensorEntityDescription( @@ -71,7 +62,6 @@ SENSOR_LIST: tuple[PECOSensorEntityDescription, ...] = ( translation_key="customers_served", value_fn=lambda data: int(data.outages.customers_served), attribute_fn=lambda data: {}, - icon="mdi:power-plug-off", state_class=SensorStateClass.MEASUREMENT, ), PECOSensorEntityDescription( @@ -79,7 +69,6 @@ SENSOR_LIST: tuple[PECOSensorEntityDescription, ...] = ( translation_key="map_alert", value_fn=lambda data: str(data.alerts.alert_title), attribute_fn=lambda data: {ATTR_CONTENT: data.alerts.alert_content}, - icon="mdi:alert", ), ) diff --git a/homeassistant/components/pegel_online/__init__.py b/homeassistant/components/pegel_online/__init__.py index e9e0e9d6aae..38b952293e0 100644 --- a/homeassistant/components/pegel_online/__init__.py +++ b/homeassistant/components/pegel_online/__init__.py @@ -1,4 +1,5 @@ """The PEGELONLINE component.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/pegel_online/config_flow.py b/homeassistant/components/pegel_online/config_flow.py index a72e450e2e5..440d1fbddf9 100644 --- a/homeassistant/components/pegel_online/config_flow.py +++ b/homeassistant/components/pegel_online/config_flow.py @@ -1,4 +1,5 @@ """Config flow for PEGELONLINE.""" + from __future__ import annotations from typing import Any @@ -6,7 +7,7 @@ from typing import Any from aiopegelonline import CONNECT_ERRORS, PegelOnline import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import ( CONF_LATITUDE, CONF_LOCATION, @@ -14,7 +15,6 @@ from homeassistant.const import ( CONF_RADIUS, UnitOfLength, ) -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.selector import ( LocationSelector, @@ -29,7 +29,7 @@ from homeassistant.helpers.selector import ( from .const import CONF_STATION, DEFAULT_RADIUS, DOMAIN -class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class FlowHandler(ConfigFlow, domain=DOMAIN): """Handle a config flow.""" VERSION = 1 @@ -42,7 +42,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" if not user_input: return self._show_form_user() @@ -69,7 +69,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_select_station( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the step select_station of a flow initialized by the user.""" if not user_input: stations = [ @@ -101,7 +101,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None, errors: dict[str, Any] | None = None, - ) -> FlowResult: + ) -> ConfigFlowResult: if user_input is None: user_input = {} return self.async_show_form( diff --git a/homeassistant/components/pegel_online/const.py b/homeassistant/components/pegel_online/const.py index 1e6c26a057b..ae7ef1f032a 100644 --- a/homeassistant/components/pegel_online/const.py +++ b/homeassistant/components/pegel_online/const.py @@ -1,4 +1,5 @@ """Constants for PEGELONLINE.""" + from datetime import timedelta DOMAIN = "pegel_online" diff --git a/homeassistant/components/pegel_online/coordinator.py b/homeassistant/components/pegel_online/coordinator.py index 9463aa48872..1802af8e05c 100644 --- a/homeassistant/components/pegel_online/coordinator.py +++ b/homeassistant/components/pegel_online/coordinator.py @@ -1,4 +1,5 @@ """DataUpdateCoordinator for pegel_online.""" + import logging from aiopegelonline import CONNECT_ERRORS, PegelOnline, Station, StationMeasurements diff --git a/homeassistant/components/pegel_online/entity.py b/homeassistant/components/pegel_online/entity.py index e9c4ebdb909..4ad12f12913 100644 --- a/homeassistant/components/pegel_online/entity.py +++ b/homeassistant/components/pegel_online/entity.py @@ -1,4 +1,5 @@ """The PEGELONLINE base entity.""" + from __future__ import annotations from homeassistant.helpers.device_registry import DeviceInfo diff --git a/homeassistant/components/pegel_online/sensor.py b/homeassistant/components/pegel_online/sensor.py index 657baf29c9f..6471b8cbd4b 100644 --- a/homeassistant/components/pegel_online/sensor.py +++ b/homeassistant/components/pegel_online/sensor.py @@ -1,4 +1,5 @@ """PEGELONLINE sensor entities.""" + from __future__ import annotations from dataclasses import dataclass @@ -21,20 +22,13 @@ from .coordinator import PegelOnlineDataUpdateCoordinator from .entity import PegelOnlineEntity -@dataclass(frozen=True) -class PegelOnlineRequiredKeysMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class PegelOnlineSensorEntityDescription(SensorEntityDescription): + """PEGELONLINE sensor entity description.""" measurement_key: str -@dataclass(frozen=True) -class PegelOnlineSensorEntityDescription( - SensorEntityDescription, PegelOnlineRequiredKeysMixin -): - """PEGELONLINE sensor entity description.""" - - SENSORS: tuple[PegelOnlineSensorEntityDescription, ...] = ( PegelOnlineSensorEntityDescription( key="air_temperature", diff --git a/homeassistant/components/pencom/switch.py b/homeassistant/components/pencom/switch.py index 064ac43e6b8..a1ec25a58e9 100644 --- a/homeassistant/components/pencom/switch.py +++ b/homeassistant/components/pencom/switch.py @@ -1,4 +1,5 @@ """Pencom relay control.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/permobil/__init__.py b/homeassistant/components/permobil/__init__.py index 0213fb6a4b6..675a803ce91 100644 --- a/homeassistant/components/permobil/__init__.py +++ b/homeassistant/components/permobil/__init__.py @@ -1,4 +1,5 @@ """The MyPermobil integration.""" + from __future__ import annotations import logging @@ -21,7 +22,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import APPLICATION, DOMAIN from .coordinator import MyPermobilCoordinator -PLATFORMS: list[Platform] = [Platform.SENSOR] +PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/permobil/binary_sensor.py b/homeassistant/components/permobil/binary_sensor.py new file mode 100644 index 00000000000..4b768cf5af5 --- /dev/null +++ b/homeassistant/components/permobil/binary_sensor.py @@ -0,0 +1,72 @@ +"""Platform for binary sensor integration.""" + +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from typing import Any + +from mypermobil import BATTERY_CHARGING + +from homeassistant import config_entries +from homeassistant.components.binary_sensor import ( + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import MyPermobilCoordinator +from .entity import PermobilEntity + + +@dataclass(frozen=True, kw_only=True) +class PermobilBinarySensorEntityDescription(BinarySensorEntityDescription): + """Describes Permobil binary sensor entity.""" + + is_on_fn: Callable[[Any], bool] + available_fn: Callable[[Any], bool] + + +BINARY_SENSOR_DESCRIPTIONS: tuple[PermobilBinarySensorEntityDescription, ...] = ( + PermobilBinarySensorEntityDescription( + is_on_fn=lambda data: data.battery[BATTERY_CHARGING[0]], + available_fn=lambda data: BATTERY_CHARGING[0] in data.battery, + key="is_charging", + translation_key="is_charging", + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: config_entries.ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Create and setup the binary sensor.""" + + coordinator: MyPermobilCoordinator = hass.data[DOMAIN][config_entry.entry_id] + + async_add_entities( + PermobilbinarySensor(coordinator=coordinator, description=description) + for description in BINARY_SENSOR_DESCRIPTIONS + ) + + +class PermobilbinarySensor(PermobilEntity, BinarySensorEntity): + """Representation of a Binary Sensor.""" + + entity_description: PermobilBinarySensorEntityDescription + + @property + def is_on(self) -> bool: + """Return True if the wheelchair is charging.""" + return self.entity_description.is_on_fn(self.coordinator.data) + + @property + def available(self) -> bool: + """Return True if the sensor has value.""" + return super().available and self.entity_description.available_fn( + self.coordinator.data + ) diff --git a/homeassistant/components/permobil/config_flow.py b/homeassistant/components/permobil/config_flow.py index 2e3e228d512..cb47640e55f 100644 --- a/homeassistant/components/permobil/config_flow.py +++ b/homeassistant/components/permobil/config_flow.py @@ -1,4 +1,5 @@ """Config flow for MyPermobil integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -13,10 +14,9 @@ from mypermobil import ( ) import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_CODE, CONF_EMAIL, CONF_REGION, CONF_TOKEN, CONF_TTL from homeassistant.core import HomeAssistant, async_get_hass -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import selector from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -41,7 +41,7 @@ GET_EMAIL_SCHEMA = vol.Schema( GET_TOKEN_SCHEMA = vol.Schema({vol.Required(CONF_CODE): cv.string}) -class PermobilConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class PermobilConfigFlow(ConfigFlow, domain=DOMAIN): """Permobil config flow.""" VERSION = 1 @@ -56,7 +56,7 @@ class PermobilConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Invoke when a user initiates a flow via the user interface.""" errors: dict[str, str] = {} @@ -80,7 +80,7 @@ class PermobilConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_region( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Invoke when a user initiates a flow via the user interface.""" errors: dict[str, str] = {} if not user_input: @@ -130,7 +130,7 @@ class PermobilConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_email_code( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Second step in config flow to enter the email code.""" errors: dict[str, str] = {} @@ -160,7 +160,9 @@ class PermobilConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_create_entry(title=self.data[CONF_EMAIL], data=self.data) - async def async_step_reauth(self, user_input: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, user_input: Mapping[str, Any] + ) -> ConfigFlowResult: """Perform reauth upon an API authentication error.""" reauth_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] diff --git a/homeassistant/components/permobil/coordinator.py b/homeassistant/components/permobil/coordinator.py index 3695236cdf0..f505e73fa23 100644 --- a/homeassistant/components/permobil/coordinator.py +++ b/homeassistant/components/permobil/coordinator.py @@ -17,7 +17,7 @@ _LOGGER = logging.getLogger(__name__) class MyPermobilData: """MyPermobil data stored in the DataUpdateCoordinator.""" - battery: dict[str, str | float | int | list | dict] + battery: dict[str, str | float | int | bool | list | dict] daily_usage: dict[str, str | float | int | list | dict] records: dict[str, str | float | int | list | dict] diff --git a/homeassistant/components/permobil/entity.py b/homeassistant/components/permobil/entity.py new file mode 100644 index 00000000000..702781aa361 --- /dev/null +++ b/homeassistant/components/permobil/entity.py @@ -0,0 +1,29 @@ +"""PermobilEntity class.""" + +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import EntityDescription +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN +from .coordinator import MyPermobilCoordinator + + +class PermobilEntity(CoordinatorEntity[MyPermobilCoordinator]): + """Representation of a permobil Entity.""" + + _attr_has_entity_name = True + + def __init__( + self, + coordinator: MyPermobilCoordinator, + description: EntityDescription, + ) -> None: + """Initialize the entity.""" + super().__init__(coordinator) + self.entity_description = description + self._attr_unique_id = f"{coordinator.p_api.email}_{description.key}" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, coordinator.p_api.email)}, + manufacturer="Permobil", + name="Permobil Wheelchair", + ) diff --git a/homeassistant/components/permobil/icons.json b/homeassistant/components/permobil/icons.json new file mode 100644 index 00000000000..ba3c612b756 --- /dev/null +++ b/homeassistant/components/permobil/icons.json @@ -0,0 +1,36 @@ +{ + "entity": { + "sensor": { + "state_of_health": { + "default": "mdi:battery-heart-variant" + }, + "charge_time_left": { + "default": "mdi:battery-clock" + }, + "distance_left": { + "default": "mdi:map-marker-distance" + }, + "max_watt_hours": { + "default": "mdi:lightning-bolt" + }, + "watt_hours_left": { + "default": "mdi:lightning-bolt" + }, + "max_distance_left": { + "default": "mdi:map-marker-distance" + }, + "usage_distance": { + "default": "mdi:map-marker-distance" + }, + "usage_adjustments": { + "default": "mdi:seat-recline-extra" + }, + "record_adjustments": { + "default": "mdi:seat-recline-extra" + }, + "record_distance": { + "default": "mdi:map-marker-distance" + } + } + } +} diff --git a/homeassistant/components/permobil/sensor.py b/homeassistant/components/permobil/sensor.py index 8af6fcf5ab1..54d3a61c519 100644 --- a/homeassistant/components/permobil/sensor.py +++ b/homeassistant/components/permobil/sensor.py @@ -1,4 +1,5 @@ """Platform for sensor integration.""" + from __future__ import annotations from collections.abc import Callable @@ -32,29 +33,22 @@ from homeassistant.components.sensor import ( from homeassistant.const import PERCENTAGE, UnitOfEnergy, UnitOfLength, UnitOfTime from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import BATTERY_ASSUMED_VOLTAGE, DOMAIN, KM, MILES from .coordinator import MyPermobilCoordinator +from .entity import PermobilEntity _LOGGER = logging.getLogger(__name__) -@dataclass(frozen=True) -class PermobilRequiredKeysMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class PermobilSensorEntityDescription(SensorEntityDescription): + """Describes Permobil sensor entity.""" value_fn: Callable[[Any], float | int] available_fn: Callable[[Any], bool] -@dataclass(frozen=True) -class PermobilSensorEntityDescription( - SensorEntityDescription, PermobilRequiredKeysMixin -): - """Describes Permobil sensor entity.""" - - SENSOR_DESCRIPTIONS: tuple[PermobilSensorEntityDescription, ...] = ( PermobilSensorEntityDescription( # Current battery as a percentage @@ -72,7 +66,6 @@ SENSOR_DESCRIPTIONS: tuple[PermobilSensorEntityDescription, ...] = ( available_fn=lambda data: BATTERY_STATE_OF_HEALTH[0] in data.battery, key="state_of_health", translation_key="state_of_health", - icon="mdi:battery-heart-variant", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, ), @@ -82,7 +75,6 @@ SENSOR_DESCRIPTIONS: tuple[PermobilSensorEntityDescription, ...] = ( available_fn=lambda data: BATTERY_CHARGE_TIME_LEFT[0] in data.battery, key="charge_time_left", translation_key="charge_time_left", - icon="mdi:battery-clock", native_unit_of_measurement=UnitOfTime.HOURS, device_class=SensorDeviceClass.DURATION, ), @@ -92,7 +84,6 @@ SENSOR_DESCRIPTIONS: tuple[PermobilSensorEntityDescription, ...] = ( available_fn=lambda data: BATTERY_DISTANCE_LEFT[0] in data.battery, key="distance_left", translation_key="distance_left", - icon="mdi:map-marker-distance", native_unit_of_measurement=UnitOfLength.KILOMETERS, device_class=SensorDeviceClass.DISTANCE, ), @@ -112,7 +103,6 @@ SENSOR_DESCRIPTIONS: tuple[PermobilSensorEntityDescription, ...] = ( available_fn=lambda data: BATTERY_MAX_AMPERE_HOURS[0] in data.battery, key="max_watt_hours", translation_key="max_watt_hours", - icon="mdi:lightning-bolt", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, device_class=SensorDeviceClass.ENERGY_STORAGE, state_class=SensorStateClass.MEASUREMENT, @@ -124,7 +114,6 @@ SENSOR_DESCRIPTIONS: tuple[PermobilSensorEntityDescription, ...] = ( available_fn=lambda data: BATTERY_AMPERE_HOURS_LEFT[0] in data.battery, key="watt_hours_left", translation_key="watt_hours_left", - icon="mdi:lightning-bolt", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, device_class=SensorDeviceClass.ENERGY_STORAGE, state_class=SensorStateClass.MEASUREMENT, @@ -135,7 +124,6 @@ SENSOR_DESCRIPTIONS: tuple[PermobilSensorEntityDescription, ...] = ( available_fn=lambda data: BATTERY_MAX_DISTANCE_LEFT[0] in data.battery, key="max_distance_left", translation_key="max_distance_left", - icon="mdi:map-marker-distance", native_unit_of_measurement=UnitOfLength.KILOMETERS, device_class=SensorDeviceClass.DISTANCE, ), @@ -145,7 +133,6 @@ SENSOR_DESCRIPTIONS: tuple[PermobilSensorEntityDescription, ...] = ( available_fn=lambda data: USAGE_DISTANCE[0] in data.daily_usage, key="usage_distance", translation_key="usage_distance", - icon="mdi:map-marker-distance", native_unit_of_measurement=UnitOfLength.KILOMETERS, device_class=SensorDeviceClass.DISTANCE, state_class=SensorStateClass.TOTAL_INCREASING, @@ -156,7 +143,6 @@ SENSOR_DESCRIPTIONS: tuple[PermobilSensorEntityDescription, ...] = ( available_fn=lambda data: USAGE_ADJUSTMENTS[0] in data.daily_usage, key="usage_adjustments", translation_key="usage_adjustments", - icon="mdi:seat-recline-extra", native_unit_of_measurement="adjustments", state_class=SensorStateClass.TOTAL_INCREASING, ), @@ -166,7 +152,6 @@ SENSOR_DESCRIPTIONS: tuple[PermobilSensorEntityDescription, ...] = ( available_fn=lambda data: RECORDS_SEATING[0] in data.records, key="record_adjustments", translation_key="record_adjustments", - icon="mdi:seat-recline-extra", native_unit_of_measurement="adjustments", state_class=SensorStateClass.TOTAL_INCREASING, ), @@ -176,7 +161,6 @@ SENSOR_DESCRIPTIONS: tuple[PermobilSensorEntityDescription, ...] = ( available_fn=lambda data: RECORDS_DISTANCE[0] in data.records, key="record_distance", translation_key="record_distance", - icon="mdi:map-marker-distance", device_class=SensorDeviceClass.DISTANCE, state_class=SensorStateClass.TOTAL_INCREASING, ), @@ -203,28 +187,14 @@ async def async_setup_entry( ) -class PermobilSensor(CoordinatorEntity[MyPermobilCoordinator], SensorEntity): +class PermobilSensor(PermobilEntity, SensorEntity): """Representation of a Sensor. This implements the common functions of all sensors. """ - _attr_has_entity_name = True _attr_suggested_display_precision = 0 entity_description: PermobilSensorEntityDescription - _available = True - - def __init__( - self, - coordinator: MyPermobilCoordinator, - description: PermobilSensorEntityDescription, - ) -> None: - """Initialize the sensor.""" - super().__init__(coordinator=coordinator) - self.entity_description = description - self._attr_unique_id = ( - f"{coordinator.p_api.email}_{self.entity_description.key}" - ) @property def native_unit_of_measurement(self) -> str | None: diff --git a/homeassistant/components/permobil/strings.json b/homeassistant/components/permobil/strings.json index 5070c13d9e5..d3a9290854e 100644 --- a/homeassistant/components/permobil/strings.json +++ b/homeassistant/components/permobil/strings.json @@ -69,6 +69,11 @@ "record_distance": { "name": "Record distance" } + }, + "binary_sensor": { + "is_charging": { + "name": "Is charging" + } } } } diff --git a/homeassistant/components/persistent_notification/__init__.py b/homeassistant/components/persistent_notification/__init__.py index 6d6fb7bfbd6..a785d015ffb 100644 --- a/homeassistant/components/persistent_notification/__init__.py +++ b/homeassistant/components/persistent_notification/__init__.py @@ -1,4 +1,5 @@ """Support for displaying persistent notifications.""" + from __future__ import annotations from collections.abc import Callable, Mapping @@ -20,6 +21,7 @@ from homeassistant.helpers.dispatcher import ( from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass import homeassistant.util.dt as dt_util +from homeassistant.util.signal_type import SignalType from homeassistant.util.uuid import random_uuid_hex DOMAIN = "persistent_notification" @@ -49,7 +51,9 @@ class UpdateType(StrEnum): UPDATED = "updated" -SIGNAL_PERSISTENT_NOTIFICATIONS_UPDATED = "persistent_notifications_updated" +SIGNAL_PERSISTENT_NOTIFICATIONS_UPDATED = SignalType[ + UpdateType, dict[str, Notification] +]("persistent_notifications_updated") SCHEMA_SERVICE_NOTIFICATION = vol.Schema( {vol.Required(ATTR_NOTIFICATION_ID): cv.string} diff --git a/homeassistant/components/persistent_notification/icons.json b/homeassistant/components/persistent_notification/icons.json new file mode 100644 index 00000000000..9c782bd7b21 --- /dev/null +++ b/homeassistant/components/persistent_notification/icons.json @@ -0,0 +1,7 @@ +{ + "services": { + "create": "mdi:message-badge", + "dismiss": "mdi:bell-off", + "dismiss_all": "mdi:notification-clear-all" + } +} diff --git a/homeassistant/components/persistent_notification/trigger.py b/homeassistant/components/persistent_notification/trigger.py index 4c9c2bd9204..431443d9139 100644 --- a/homeassistant/components/persistent_notification/trigger.py +++ b/homeassistant/components/persistent_notification/trigger.py @@ -1,4 +1,5 @@ """Offer persistent_notifications triggered automation rules.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/person/__init__.py b/homeassistant/components/person/__init__.py index 2075c3fc713..8aa3251641b 100644 --- a/homeassistant/components/person/__init__.py +++ b/homeassistant/components/person/__init__.py @@ -1,7 +1,8 @@ """Support for tracking people.""" + from __future__ import annotations -from collections.abc import Callable +from collections.abc import Callable, Mapping import logging from typing import Any, Self @@ -53,9 +54,11 @@ from homeassistant.helpers.event import ( ) from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.storage import Store -from homeassistant.helpers.typing import ConfigType, EventType +from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass +from . import group as group_pre_import # noqa: F401 + _LOGGER = logging.getLogger(__name__) ATTR_SOURCE = "source" @@ -131,7 +134,7 @@ async def async_add_user_device_tracker( await coll.async_update_item( person[CONF_ID], - {CONF_DEVICE_TRACKERS: device_trackers + [device_tracker_entity_id]}, + {CONF_DEVICE_TRACKERS: [*device_trackers, device_tracker_entity_id]}, ) break @@ -241,14 +244,15 @@ class PersonStorageCollection(collection.DictStorageCollection): er.EVENT_ENTITY_REGISTRY_UPDATED, self._entity_registry_updated, event_filter=self._entity_registry_filter, + run_immediately=True, ) @callback - def _entity_registry_filter(self, event: Event) -> bool: + def _entity_registry_filter(self, event_data: Mapping[str, Any]) -> bool: """Filter entity registry events.""" return ( - event.data["action"] == "remove" - and split_entity_id(event.data[ATTR_ENTITY_ID])[0] == "device_tracker" + event_data["action"] == "remove" + and split_entity_id(event_data[ATTR_ENTITY_ID])[0] == "device_tracker" ) async def _entity_registry_updated(self, event: Event) -> None: @@ -517,9 +521,7 @@ class Person(collection.CollectionEntity, RestoreEntity): self._update_state() @callback - def _async_handle_tracker_update( - self, event: EventType[EventStateChangedData] - ) -> None: + def _async_handle_tracker_update(self, event: Event[EventStateChangedData]) -> None: """Handle the device tracker state changes.""" self._update_state() diff --git a/homeassistant/components/person/group.py b/homeassistant/components/person/group.py index 9bd2c991678..e1b93696aa9 100644 --- a/homeassistant/components/person/group.py +++ b/homeassistant/components/person/group.py @@ -1,14 +1,17 @@ """Describe group states.""" +from typing import TYPE_CHECKING -from homeassistant.components.group import GroupIntegrationRegistry from homeassistant.const import STATE_HOME, STATE_NOT_HOME from homeassistant.core import HomeAssistant, callback +if TYPE_CHECKING: + from homeassistant.components.group import GroupIntegrationRegistry + @callback def async_describe_on_off_states( - hass: HomeAssistant, registry: GroupIntegrationRegistry + hass: HomeAssistant, registry: "GroupIntegrationRegistry" ) -> None: """Describe group on off states.""" registry.on_off_states({STATE_HOME}, STATE_NOT_HOME) diff --git a/homeassistant/components/person/significant_change.py b/homeassistant/components/person/significant_change.py index 680b9194144..c6720bcc4ff 100644 --- a/homeassistant/components/person/significant_change.py +++ b/homeassistant/components/person/significant_change.py @@ -1,4 +1,5 @@ """Helper to test significant Person state changes.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/philips_js/__init__.py b/homeassistant/components/philips_js/__init__.py index c8540a187da..e56d1cdc651 100644 --- a/homeassistant/components/philips_js/__init__.py +++ b/homeassistant/components/philips_js/__init__.py @@ -1,4 +1,5 @@ """The Philips TV integration.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/philips_js/binary_sensor.py b/homeassistant/components/philips_js/binary_sensor.py index 74fe41bf722..a21d1416192 100644 --- a/homeassistant/components/philips_js/binary_sensor.py +++ b/homeassistant/components/philips_js/binary_sensor.py @@ -1,4 +1,5 @@ """Philips TV binary sensors.""" + from __future__ import annotations from dataclasses import dataclass @@ -29,13 +30,11 @@ DESCRIPTIONS = ( PhilipsTVBinarySensorEntityDescription( key="recording_ongoing", translation_key="recording_ongoing", - icon="mdi:record-rec", recording_value="RECORDING_ONGOING", ), PhilipsTVBinarySensorEntityDescription( key="recording_new", translation_key="recording_new", - icon="mdi:new-box", recording_value="RECORDING_NEW", ), ) @@ -65,10 +64,7 @@ def _check_for_recording_entry(api: PhilipsTV, entry: str, value: str) -> bool: """Return True if at least one specified value is available within entry of list.""" if api.recordings_list is None: return False - for rec in api.recordings_list["recordings"]: - if rec.get(entry) == value: - return True - return False + return any(rec.get(entry) == value for rec in api.recordings_list["recordings"]) class PhilipsTVBinarySensorEntityRecordingType(PhilipsJsEntity, BinarySensorEntity): diff --git a/homeassistant/components/philips_js/config_flow.py b/homeassistant/components/philips_js/config_flow.py index d1cd3e7b1a5..ed0fce05f46 100644 --- a/homeassistant/components/philips_js/config_flow.py +++ b/homeassistant/components/philips_js/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Philips TV integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -8,7 +9,7 @@ from typing import Any from haphilipsjs import ConnectionFailure, PairingFailure, PhilipsTV import voluptuous as vol -from homeassistant import config_entries, core +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.const import ( CONF_API_VERSION, CONF_HOST, @@ -16,7 +17,7 @@ from homeassistant.const import ( CONF_PIN, CONF_USERNAME, ) -from homeassistant.data_entry_flow import FlowResult +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import selector from homeassistant.helpers.schema_config_entry_flow import ( SchemaFlowFormStep, @@ -49,7 +50,7 @@ OPTIONS_FLOW = { async def _validate_input( - hass: core.HomeAssistant, host: str, api_version: int + hass: HomeAssistant, host: str, api_version: int ) -> PhilipsTV: """Validate the user input allows us to connect.""" hub = PhilipsTV(host, api_version) @@ -63,7 +64,7 @@ async def _validate_input( return hub -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class PhilipsJSConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Philips TV.""" VERSION = 1 @@ -74,9 +75,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._current: dict[str, Any] = {} self._hub: PhilipsTV | None = None self._pair_state: Any = None - self._entry: config_entries.ConfigEntry | None = None + self._entry: ConfigEntry | None = None - async def _async_create_current(self) -> FlowResult: + async def _async_create_current(self) -> ConfigFlowResult: system = self._current[CONF_SYSTEM] if self._entry: self.hass.config_entries.async_update_entry( @@ -94,7 +95,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_pair( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Attempt to pair with device.""" assert self._hub @@ -145,7 +146,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._current[CONF_PASSWORD] = password return await self._async_create_current() - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Handle configuration by re-auth.""" self._entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) self._current[CONF_HOST] = entry_data[CONF_HOST] @@ -154,7 +157,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors = {} if user_input: @@ -187,9 +190,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form(step_id="user", data_schema=schema, errors=errors) @staticmethod - @core.callback + @callback def async_get_options_flow( - config_entry: config_entries.ConfigEntry, + config_entry: ConfigEntry, ) -> SchemaOptionsFlowHandler: """Get the options flow for this handler.""" return SchemaOptionsFlowHandler(config_entry, OPTIONS_FLOW) diff --git a/homeassistant/components/philips_js/device_trigger.py b/homeassistant/components/philips_js/device_trigger.py index bdf47674bc8..4c2ec9b95db 100644 --- a/homeassistant/components/philips_js/device_trigger.py +++ b/homeassistant/components/philips_js/device_trigger.py @@ -1,4 +1,5 @@ """Provides device automations for control of device.""" + from __future__ import annotations import voluptuous as vol diff --git a/homeassistant/components/philips_js/diagnostics.py b/homeassistant/components/philips_js/diagnostics.py index 889b8e47e3f..34cc71c9b94 100644 --- a/homeassistant/components/philips_js/diagnostics.py +++ b/homeassistant/components/philips_js/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for Philips JS.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/philips_js/entity.py b/homeassistant/components/philips_js/entity.py index c2645974f49..e0d97f940d0 100644 --- a/homeassistant/components/philips_js/entity.py +++ b/homeassistant/components/philips_js/entity.py @@ -1,4 +1,5 @@ """Base Philips js entity.""" + from __future__ import annotations from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/philips_js/icons.json b/homeassistant/components/philips_js/icons.json new file mode 100644 index 00000000000..ea9cbd3114e --- /dev/null +++ b/homeassistant/components/philips_js/icons.json @@ -0,0 +1,25 @@ +{ + "entity": { + "binary_sensor": { + "recording_ongoing": { + "default": "mdi:record-rec" + }, + "recording_new": { + "default": "mdi:new-box" + } + }, + "light": { + "ambilight": { + "default": "mdi:television-ambient-light" + } + }, + "switch": { + "screen_state": { + "default": "mdi:television-shimmer" + }, + "ambilight_hue": { + "default": "mdi:television-ambient-light" + } + } + } +} diff --git a/homeassistant/components/philips_js/light.py b/homeassistant/components/philips_js/light.py index 75f43039de8..6a91b872913 100644 --- a/homeassistant/components/philips_js/light.py +++ b/homeassistant/components/philips_js/light.py @@ -1,4 +1,5 @@ """Component to integrate ambilight for TVs exposing the Joint Space API.""" + from __future__ import annotations from dataclasses import dataclass @@ -153,7 +154,6 @@ class PhilipsTVLightEntity(PhilipsJsEntity, LightEntity): self._attr_supported_color_modes = {ColorMode.HS, ColorMode.ONOFF} self._attr_supported_features = LightEntityFeature.EFFECT self._attr_unique_id = coordinator.unique_id - self._attr_icon = "mdi:television-ambient-light" self._update_from_coordinator() diff --git a/homeassistant/components/philips_js/media_player.py b/homeassistant/components/philips_js/media_player.py index 6ee70b03d54..c8b89d57854 100644 --- a/homeassistant/components/philips_js/media_player.py +++ b/homeassistant/components/philips_js/media_player.py @@ -1,4 +1,5 @@ """Media Player component to integrate TVs exposing the Joint Space API.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/philips_js/remote.py b/homeassistant/components/philips_js/remote.py index c5b24089809..5972724c54b 100644 --- a/homeassistant/components/philips_js/remote.py +++ b/homeassistant/components/philips_js/remote.py @@ -1,4 +1,5 @@ """Remote control support for Apple TV.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/philips_js/switch.py b/homeassistant/components/philips_js/switch.py index 29cfa10a230..697e7f2f060 100644 --- a/homeassistant/components/philips_js/switch.py +++ b/homeassistant/components/philips_js/switch.py @@ -1,4 +1,5 @@ """Philips TV menu switches.""" + from __future__ import annotations from typing import Any @@ -45,7 +46,6 @@ class PhilipsTVScreenSwitch(PhilipsJsEntity, SwitchEntity): super().__init__(coordinator) - self._attr_icon = "mdi:television-shimmer" self._attr_unique_id = f"{coordinator.unique_id}_screenstate" @property @@ -84,7 +84,6 @@ class PhilipsTVAmbilightHueSwitch(PhilipsJsEntity, SwitchEntity): super().__init__(coordinator) - self._attr_icon = "mdi:television-ambient-light" self._attr_unique_id = f"{coordinator.unique_id}_ambi_hue" @property diff --git a/homeassistant/components/pi_hole/__init__.py b/homeassistant/components/pi_hole/__init__.py index c57b969ce9c..f892114b26c 100644 --- a/homeassistant/components/pi_hole/__init__.py +++ b/homeassistant/components/pi_hole/__init__.py @@ -1,4 +1,5 @@ """The pi_hole component.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/pi_hole/binary_sensor.py b/homeassistant/components/pi_hole/binary_sensor.py index 2f3a5a4801c..0593d12faa7 100644 --- a/homeassistant/components/pi_hole/binary_sensor.py +++ b/homeassistant/components/pi_hole/binary_sensor.py @@ -1,4 +1,5 @@ """Support for getting status from a Pi-hole system.""" + from __future__ import annotations from collections.abc import Callable @@ -21,19 +22,11 @@ from . import PiHoleEntity from .const import DATA_KEY_API, DATA_KEY_COORDINATOR, DOMAIN as PIHOLE_DOMAIN -@dataclass(frozen=True) -class RequiredPiHoleBinaryDescription: - """Represent the required attributes of the PiHole binary description.""" - - state_value: Callable[[Hole], bool] - - -@dataclass(frozen=True) -class PiHoleBinarySensorEntityDescription( - BinarySensorEntityDescription, RequiredPiHoleBinaryDescription -): +@dataclass(frozen=True, kw_only=True) +class PiHoleBinarySensorEntityDescription(BinarySensorEntityDescription): """Describes PiHole binary sensor entity.""" + state_value: Callable[[Hole], bool] extra_value: Callable[[Hole], dict[str, Any] | None] = lambda api: None @@ -41,7 +34,6 @@ BINARY_SENSOR_TYPES: tuple[PiHoleBinarySensorEntityDescription, ...] = ( PiHoleBinarySensorEntityDescription( key="status", translation_key="status", - icon="mdi:pi-hole", state_value=lambda api: bool(api.data.get("status") == "enabled"), ), ) diff --git a/homeassistant/components/pi_hole/config_flow.py b/homeassistant/components/pi_hole/config_flow.py index 136e851429d..d6f42d57deb 100644 --- a/homeassistant/components/pi_hole/config_flow.py +++ b/homeassistant/components/pi_hole/config_flow.py @@ -1,4 +1,5 @@ """Config flow to configure the Pi-hole integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -9,7 +10,7 @@ from hole import Hole from hole.exceptions import HoleError import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import ( CONF_API_KEY, CONF_HOST, @@ -19,7 +20,6 @@ from homeassistant.const import ( CONF_SSL, CONF_VERIFY_SSL, ) -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import ( @@ -33,7 +33,7 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -class PiHoleFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class PiHoleFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a Pi-hole config flow.""" VERSION = 1 @@ -44,7 +44,7 @@ class PiHoleFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initiated by the user.""" errors = {} @@ -103,7 +103,7 @@ class PiHoleFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_api_key( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle step to setup API key.""" errors = {} if user_input is not None: @@ -120,7 +120,9 @@ class PiHoleFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Perform reauth upon an API authentication error.""" self._config = dict(entry_data) return await self.async_step_reauth_confirm() @@ -128,7 +130,7 @@ class PiHoleFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None, - ) -> FlowResult: + ) -> ConfigFlowResult: """Perform reauth confirm upon an API authentication error.""" errors = {} if user_input is not None: diff --git a/homeassistant/components/pi_hole/const.py b/homeassistant/components/pi_hole/const.py index 0114a6621b5..b6c97bc6118 100644 --- a/homeassistant/components/pi_hole/const.py +++ b/homeassistant/components/pi_hole/const.py @@ -1,4 +1,5 @@ """Constants for the pi_hole integration.""" + from datetime import timedelta DOMAIN = "pi_hole" diff --git a/homeassistant/components/pi_hole/diagnostics.py b/homeassistant/components/pi_hole/diagnostics.py index 8b3c32b0ac2..46efebaf475 100644 --- a/homeassistant/components/pi_hole/diagnostics.py +++ b/homeassistant/components/pi_hole/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for the Pi-hole integration.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/pi_hole/icons.json b/homeassistant/components/pi_hole/icons.json new file mode 100644 index 00000000000..58f20da5a2d --- /dev/null +++ b/homeassistant/components/pi_hole/icons.json @@ -0,0 +1,41 @@ +{ + "entity": { + "binary_sensor": { + "status": { + "default": "mdi:pi-hole" + } + }, + "sensor": { + "ads_blocked_today": { + "default": "mdi:close-octagon-outline" + }, + "ads_percentage_today": { + "default": "mdi:close-octagon-outline" + }, + "clients_ever_seen": { + "default": "mdi:account-outline" + }, + "dns_queries_today": { + "default": "mdi:comment-question-outline" + }, + "domains_being_blocked": { + "default": "mdi:block-helper" + }, + "queries_cached": { + "default": "mdi:comment-question-outline" + }, + "queries_forwarded": { + "default": "mdi:comment-question-outline" + }, + "unique_clients": { + "default": "mdi:account-outline" + }, + "unique_domains": { + "default": "mdi:domain" + } + } + }, + "services": { + "disable": "mdi:server-off" + } +} diff --git a/homeassistant/components/pi_hole/sensor.py b/homeassistant/components/pi_hole/sensor.py index c6a8d5da83d..a62252d10c1 100644 --- a/homeassistant/components/pi_hole/sensor.py +++ b/homeassistant/components/pi_hole/sensor.py @@ -1,4 +1,5 @@ """Support for getting statistical data from a Pi-hole system.""" + from __future__ import annotations from hole import Hole @@ -19,55 +20,46 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key="ads_blocked_today", translation_key="ads_blocked_today", native_unit_of_measurement="ads", - icon="mdi:close-octagon-outline", ), SensorEntityDescription( key="ads_percentage_today", translation_key="ads_percentage_today", native_unit_of_measurement=PERCENTAGE, - icon="mdi:close-octagon-outline", ), SensorEntityDescription( key="clients_ever_seen", translation_key="clients_ever_seen", native_unit_of_measurement="clients", - icon="mdi:account-outline", ), SensorEntityDescription( key="dns_queries_today", translation_key="dns_queries_today", native_unit_of_measurement="queries", - icon="mdi:comment-question-outline", ), SensorEntityDescription( key="domains_being_blocked", translation_key="domains_being_blocked", native_unit_of_measurement="domains", - icon="mdi:block-helper", ), SensorEntityDescription( key="queries_cached", translation_key="queries_cached", native_unit_of_measurement="queries", - icon="mdi:comment-question-outline", ), SensorEntityDescription( key="queries_forwarded", translation_key="queries_forwarded", native_unit_of_measurement="queries", - icon="mdi:comment-question-outline", ), SensorEntityDescription( key="unique_clients", translation_key="unique_clients", native_unit_of_measurement="clients", - icon="mdi:account-outline", ), SensorEntityDescription( key="unique_domains", translation_key="unique_domains", native_unit_of_measurement="domains", - icon="mdi:domain", ), ) diff --git a/homeassistant/components/pi_hole/switch.py b/homeassistant/components/pi_hole/switch.py index dc699beb26b..963ee7c9738 100644 --- a/homeassistant/components/pi_hole/switch.py +++ b/homeassistant/components/pi_hole/switch.py @@ -1,4 +1,5 @@ """Support for turning on and off Pi-hole system.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/pi_hole/update.py b/homeassistant/components/pi_hole/update.py index b559a1cf806..75d4f91f2be 100644 --- a/homeassistant/components/pi_hole/update.py +++ b/homeassistant/components/pi_hole/update.py @@ -1,4 +1,5 @@ """Support for update entities of a Pi-hole system.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/picnic/config_flow.py b/homeassistant/components/picnic/config_flow.py index b02c0a74bfc..9712286b554 100644 --- a/homeassistant/components/picnic/config_flow.py +++ b/homeassistant/components/picnic/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Picnic integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -10,15 +11,15 @@ from python_picnic_api.session import PicnicAuthError import requests import voluptuous as vol -from homeassistant import config_entries, core, exceptions -from homeassistant.config_entries import SOURCE_REAUTH +from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlow, ConfigFlowResult from homeassistant.const import ( CONF_ACCESS_TOKEN, CONF_COUNTRY_CODE, CONF_PASSWORD, CONF_USERNAME, ) -from homeassistant.data_entry_flow import FlowResult +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from .const import COUNTRY_CODES, DOMAIN @@ -45,7 +46,7 @@ class PicnicHub: return picnic.session.auth_token, picnic.get_user() -async def validate_input(hass: core.HomeAssistant, data): +async def validate_input(hass: HomeAssistant, data): """Validate the user input allows us to connect. Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user. @@ -67,7 +68,7 @@ async def validate_input(hass: core.HomeAssistant, data): # Return the validation result address = ( f'{user_data["address"]["street"]} {user_data["address"]["house_number"]}' - + f'{user_data["address"]["house_number_ext"]}' + f'{user_data["address"]["house_number_ext"]}' ) return auth_token, { "title": address, @@ -75,12 +76,14 @@ async def validate_input(hass: core.HomeAssistant, data): } -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class PicnicConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Picnic.""" VERSION = 1 - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Perform the re-auth step upon an API authentication error.""" return await self.async_step_user() @@ -128,9 +131,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -class CannotConnect(exceptions.HomeAssistantError): +class CannotConnect(HomeAssistantError): """Error to indicate we cannot connect.""" -class InvalidAuth(exceptions.HomeAssistantError): +class InvalidAuth(HomeAssistantError): """Error to indicate there is invalid auth.""" diff --git a/homeassistant/components/picnic/const.py b/homeassistant/components/picnic/const.py index 851df6f41b2..4e8eafd8912 100644 --- a/homeassistant/components/picnic/const.py +++ b/homeassistant/components/picnic/const.py @@ -1,4 +1,5 @@ """Constants for the Picnic integration.""" + from __future__ import annotations DOMAIN = "picnic" diff --git a/homeassistant/components/picnic/coordinator.py b/homeassistant/components/picnic/coordinator.py index 61af7e5cc91..7a76d3174cd 100644 --- a/homeassistant/components/picnic/coordinator.py +++ b/homeassistant/components/picnic/coordinator.py @@ -1,4 +1,5 @@ """Coordinator to fetch data from the Picnic API.""" + import asyncio from contextlib import suppress import copy diff --git a/homeassistant/components/picnic/icons.json b/homeassistant/components/picnic/icons.json new file mode 100644 index 00000000000..d8f99153f33 --- /dev/null +++ b/homeassistant/components/picnic/icons.json @@ -0,0 +1,62 @@ +{ + "entity": { + "sensor": { + "cart_items_count": { + "default": "mdi:format-list-numbered" + }, + "cart_total_price": { + "default": "mdi:currency-eur" + }, + "selected_slot_end": { + "default": "mdi:calendar-end" + }, + "selected_slot_max_order_time": { + "default": "mdi:clock-alert-outline" + }, + "selected_slot_min_order_value": { + "default": "mdi:currency-eur" + }, + "last_order_slot_start": { + "default": "mdi:calendar-start" + }, + "last_order_slot_end": { + "default": "mdi:calendar-end" + }, + "last_order_status": { + "default": "mdi:list-status" + }, + "last_order_max_order_time": { + "default": "mdi:clock-alert-outline" + }, + "last_order_delivery_time": { + "default": "mdi:timeline-clock" + }, + "last_order_total_price": { + "default": "mdi:cash-marker" + }, + "next_delivery_eta_start": { + "default": "mdi:clock-start" + }, + "next_delivery_eta_end": { + "default": "mdi:clock-end" + }, + "next_delivery_slot_start": { + "default": "mdi:calendar-start" + }, + "next_delivery_slot_end": { + "default": "mdi:calendar-end" + }, + "selected_slot_start": { + "default": "mdi:calendar-start" + } + }, + "todo": { + "shopping_cart": { + "default": "mdi:cart" + } + } + }, + "services": { + "add_product": "mdi:cart-plus" + } +} diff --git a/homeassistant/components/picnic/sensor.py b/homeassistant/components/picnic/sensor.py index 56d2d22cf29..866bd6b56c1 100644 --- a/homeassistant/components/picnic/sensor.py +++ b/homeassistant/components/picnic/sensor.py @@ -1,4 +1,5 @@ """Definition of Picnic sensors.""" + from __future__ import annotations from collections.abc import Callable @@ -44,20 +45,15 @@ from .const import ( from .coordinator import PicnicUpdateCoordinator -@dataclass(frozen=True) -class PicnicRequiredKeysMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class PicnicSensorEntityDescription(SensorEntityDescription): + """Describes Picnic sensor entity.""" data_type: Literal[ "cart_data", "slot_data", "next_delivery_data", "last_order_data" ] value_fn: Callable[[Any], StateType | datetime] - -@dataclass(frozen=True) -class PicnicSensorEntityDescription(SensorEntityDescription, PicnicRequiredKeysMixin): - """Describes Picnic sensor entity.""" - entity_registry_enabled_default: bool = False @@ -65,7 +61,6 @@ SENSOR_TYPES: tuple[PicnicSensorEntityDescription, ...] = ( PicnicSensorEntityDescription( key=SENSOR_CART_ITEMS_COUNT, translation_key=SENSOR_CART_ITEMS_COUNT, - icon="mdi:format-list-numbered", data_type="cart_data", value_fn=lambda cart: cart.get("total_count", 0), ), @@ -73,7 +68,6 @@ SENSOR_TYPES: tuple[PicnicSensorEntityDescription, ...] = ( key=SENSOR_CART_TOTAL_PRICE, translation_key=SENSOR_CART_TOTAL_PRICE, native_unit_of_measurement=CURRENCY_EURO, - icon="mdi:currency-eur", entity_registry_enabled_default=True, data_type="cart_data", value_fn=lambda cart: cart.get("total_price", 0) / 100, @@ -82,7 +76,6 @@ SENSOR_TYPES: tuple[PicnicSensorEntityDescription, ...] = ( key=SENSOR_SELECTED_SLOT_START, translation_key=SENSOR_SELECTED_SLOT_START, device_class=SensorDeviceClass.TIMESTAMP, - icon="mdi:calendar-start", entity_registry_enabled_default=True, data_type="slot_data", value_fn=lambda slot: dt_util.parse_datetime(str(slot.get("window_start"))), @@ -91,7 +84,6 @@ SENSOR_TYPES: tuple[PicnicSensorEntityDescription, ...] = ( key=SENSOR_SELECTED_SLOT_END, translation_key=SENSOR_SELECTED_SLOT_END, device_class=SensorDeviceClass.TIMESTAMP, - icon="mdi:calendar-end", entity_registry_enabled_default=True, data_type="slot_data", value_fn=lambda slot: dt_util.parse_datetime(str(slot.get("window_end"))), @@ -100,7 +92,6 @@ SENSOR_TYPES: tuple[PicnicSensorEntityDescription, ...] = ( key=SENSOR_SELECTED_SLOT_MAX_ORDER_TIME, translation_key=SENSOR_SELECTED_SLOT_MAX_ORDER_TIME, device_class=SensorDeviceClass.TIMESTAMP, - icon="mdi:clock-alert-outline", entity_registry_enabled_default=True, data_type="slot_data", value_fn=lambda slot: dt_util.parse_datetime(str(slot.get("cut_off_time"))), @@ -109,7 +100,6 @@ SENSOR_TYPES: tuple[PicnicSensorEntityDescription, ...] = ( key=SENSOR_SELECTED_SLOT_MIN_ORDER_VALUE, translation_key=SENSOR_SELECTED_SLOT_MIN_ORDER_VALUE, native_unit_of_measurement=CURRENCY_EURO, - icon="mdi:currency-eur", entity_registry_enabled_default=True, data_type="slot_data", value_fn=lambda slot: ( @@ -122,7 +112,6 @@ SENSOR_TYPES: tuple[PicnicSensorEntityDescription, ...] = ( key=SENSOR_LAST_ORDER_SLOT_START, translation_key=SENSOR_LAST_ORDER_SLOT_START, device_class=SensorDeviceClass.TIMESTAMP, - icon="mdi:calendar-start", data_type="last_order_data", value_fn=lambda last_order: dt_util.parse_datetime( str(last_order.get("slot", {}).get("window_start")) @@ -132,7 +121,6 @@ SENSOR_TYPES: tuple[PicnicSensorEntityDescription, ...] = ( key=SENSOR_LAST_ORDER_SLOT_END, translation_key=SENSOR_LAST_ORDER_SLOT_END, device_class=SensorDeviceClass.TIMESTAMP, - icon="mdi:calendar-end", data_type="last_order_data", value_fn=lambda last_order: dt_util.parse_datetime( str(last_order.get("slot", {}).get("window_end")) @@ -141,7 +129,6 @@ SENSOR_TYPES: tuple[PicnicSensorEntityDescription, ...] = ( PicnicSensorEntityDescription( key=SENSOR_LAST_ORDER_STATUS, translation_key=SENSOR_LAST_ORDER_STATUS, - icon="mdi:list-status", data_type="last_order_data", value_fn=lambda last_order: last_order.get("status"), ), @@ -149,7 +136,6 @@ SENSOR_TYPES: tuple[PicnicSensorEntityDescription, ...] = ( key=SENSOR_LAST_ORDER_MAX_ORDER_TIME, translation_key=SENSOR_LAST_ORDER_MAX_ORDER_TIME, device_class=SensorDeviceClass.TIMESTAMP, - icon="mdi:clock-alert-outline", entity_registry_enabled_default=True, data_type="last_order_data", value_fn=lambda last_order: dt_util.parse_datetime( @@ -160,7 +146,6 @@ SENSOR_TYPES: tuple[PicnicSensorEntityDescription, ...] = ( key=SENSOR_LAST_ORDER_DELIVERY_TIME, translation_key=SENSOR_LAST_ORDER_DELIVERY_TIME, device_class=SensorDeviceClass.TIMESTAMP, - icon="mdi:timeline-clock", entity_registry_enabled_default=True, data_type="last_order_data", value_fn=lambda last_order: dt_util.parse_datetime( @@ -171,7 +156,6 @@ SENSOR_TYPES: tuple[PicnicSensorEntityDescription, ...] = ( key=SENSOR_LAST_ORDER_TOTAL_PRICE, translation_key=SENSOR_LAST_ORDER_TOTAL_PRICE, native_unit_of_measurement=CURRENCY_EURO, - icon="mdi:cash-marker", data_type="last_order_data", value_fn=lambda last_order: last_order.get("total_price", 0) / 100, ), @@ -179,7 +163,6 @@ SENSOR_TYPES: tuple[PicnicSensorEntityDescription, ...] = ( key=SENSOR_NEXT_DELIVERY_ETA_START, translation_key=SENSOR_NEXT_DELIVERY_ETA_START, device_class=SensorDeviceClass.TIMESTAMP, - icon="mdi:clock-start", entity_registry_enabled_default=True, data_type="next_delivery_data", value_fn=lambda next_delivery: dt_util.parse_datetime( @@ -190,7 +173,6 @@ SENSOR_TYPES: tuple[PicnicSensorEntityDescription, ...] = ( key=SENSOR_NEXT_DELIVERY_ETA_END, translation_key=SENSOR_NEXT_DELIVERY_ETA_END, device_class=SensorDeviceClass.TIMESTAMP, - icon="mdi:clock-end", entity_registry_enabled_default=True, data_type="next_delivery_data", value_fn=lambda next_delivery: dt_util.parse_datetime( @@ -201,7 +183,6 @@ SENSOR_TYPES: tuple[PicnicSensorEntityDescription, ...] = ( key=SENSOR_NEXT_DELIVERY_SLOT_START, translation_key=SENSOR_NEXT_DELIVERY_SLOT_START, device_class=SensorDeviceClass.TIMESTAMP, - icon="mdi:calendar-start", data_type="next_delivery_data", value_fn=lambda next_delivery: dt_util.parse_datetime( str(next_delivery.get("slot", {}).get("window_start")) @@ -211,7 +192,6 @@ SENSOR_TYPES: tuple[PicnicSensorEntityDescription, ...] = ( key=SENSOR_NEXT_DELIVERY_SLOT_END, translation_key=SENSOR_NEXT_DELIVERY_SLOT_END, device_class=SensorDeviceClass.TIMESTAMP, - icon="mdi:calendar-end", data_type="next_delivery_data", value_fn=lambda next_delivery: dt_util.parse_datetime( str(next_delivery.get("slot", {}).get("window_end")) diff --git a/homeassistant/components/picnic/services.py b/homeassistant/components/picnic/services.py index 03b7576703d..f820daee54b 100644 --- a/homeassistant/components/picnic/services.py +++ b/homeassistant/components/picnic/services.py @@ -1,4 +1,5 @@ """Services for the Picnic integration.""" + from __future__ import annotations from typing import cast diff --git a/homeassistant/components/picnic/todo.py b/homeassistant/components/picnic/todo.py index fea99f7403d..7fa2bbccd3e 100644 --- a/homeassistant/components/picnic/todo.py +++ b/homeassistant/components/picnic/todo.py @@ -1,4 +1,5 @@ """Definition of Picnic shopping cart.""" + from __future__ import annotations import logging @@ -39,7 +40,6 @@ class PicnicCart(TodoListEntity, CoordinatorEntity[PicnicUpdateCoordinator]): """A Picnic Shopping Cart TodoListEntity.""" _attr_has_entity_name = True - _attr_icon = "mdi:cart" _attr_supported_features = TodoListEntityFeature.CREATE_TODO_ITEM _attr_translation_key = "shopping_cart" @@ -66,18 +66,15 @@ class PicnicCart(TodoListEntity, CoordinatorEntity[PicnicUpdateCoordinator]): _LOGGER.debug(self.coordinator.data["cart_data"]["items"]) - items = [] - for item in self.coordinator.data["cart_data"]["items"]: - for article in item["items"]: - items.append( - TodoItem( - summary=f"{article['name']} ({article['unit_quantity']})", - uid=f"{item['id']}-{article['id']}", - status=TodoItemStatus.NEEDS_ACTION, # We set 'NEEDS_ACTION' so they count as state - ) - ) - - return items + return [ + TodoItem( + summary=f"{article['name']} ({article['unit_quantity']})", + uid=f"{item['id']}-{article['id']}", + status=TodoItemStatus.NEEDS_ACTION, # We set 'NEEDS_ACTION' so they count as state + ) + for item in self.coordinator.data["cart_data"]["items"] + for article in item["items"] + ] async def async_create_todo_item(self, item: TodoItem) -> None: """Add item to shopping cart.""" diff --git a/homeassistant/components/picotts/tts.py b/homeassistant/components/picotts/tts.py index 4d9f1755145..8ba17fdac17 100644 --- a/homeassistant/components/picotts/tts.py +++ b/homeassistant/components/picotts/tts.py @@ -1,4 +1,5 @@ """Support for the Pico TTS speech service.""" + import logging import os import shutil diff --git a/homeassistant/components/pilight/__init__.py b/homeassistant/components/pilight/__init__.py index f4d2e539b6a..1f1eee0c92a 100644 --- a/homeassistant/components/pilight/__init__.py +++ b/homeassistant/components/pilight/__init__.py @@ -1,4 +1,5 @@ """Component to create an interface to a Pilight daemon.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/pilight/base_class.py b/homeassistant/components/pilight/base_class.py index cb96d89e6a2..d2d83813516 100644 --- a/homeassistant/components/pilight/base_class.py +++ b/homeassistant/components/pilight/base_class.py @@ -1,4 +1,5 @@ """Base class for pilight.""" + import voluptuous as vol from homeassistant.const import ( diff --git a/homeassistant/components/pilight/binary_sensor.py b/homeassistant/components/pilight/binary_sensor.py index 303c755a035..0ddb2de4603 100644 --- a/homeassistant/components/pilight/binary_sensor.py +++ b/homeassistant/components/pilight/binary_sensor.py @@ -1,4 +1,5 @@ """Support for Pilight binary sensors.""" + from __future__ import annotations import datetime diff --git a/homeassistant/components/pilight/icons.json b/homeassistant/components/pilight/icons.json new file mode 100644 index 00000000000..c1b8e741e45 --- /dev/null +++ b/homeassistant/components/pilight/icons.json @@ -0,0 +1,5 @@ +{ + "services": { + "send": "mdi:send" + } +} diff --git a/homeassistant/components/pilight/light.py b/homeassistant/components/pilight/light.py index bcb2197db19..60713b59475 100644 --- a/homeassistant/components/pilight/light.py +++ b/homeassistant/components/pilight/light.py @@ -1,4 +1,5 @@ """Support for switching devices via Pilight to on and off.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/pilight/sensor.py b/homeassistant/components/pilight/sensor.py index 21036a94210..003e3428bdd 100644 --- a/homeassistant/components/pilight/sensor.py +++ b/homeassistant/components/pilight/sensor.py @@ -1,4 +1,5 @@ """Support for Pilight sensors.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/pilight/switch.py b/homeassistant/components/pilight/switch.py index 75d286afad2..0d0023d9cd6 100644 --- a/homeassistant/components/pilight/switch.py +++ b/homeassistant/components/pilight/switch.py @@ -1,4 +1,5 @@ """Support for switching devices via Pilight to on and off.""" + from __future__ import annotations import voluptuous as vol diff --git a/homeassistant/components/ping/__init__.py b/homeassistant/components/ping/__init__.py index 81df1401f91..e75b36dc38d 100644 --- a/homeassistant/components/ping/__init__.py +++ b/homeassistant/components/ping/__init__.py @@ -1,4 +1,5 @@ """The ping component.""" + from __future__ import annotations from dataclasses import dataclass @@ -19,7 +20,7 @@ from .helpers import PingDataICMPLib, PingDataSubProcess _LOGGER = logging.getLogger(__name__) CONFIG_SCHEMA = cv.platform_only_config_schema(DOMAIN) -PLATFORMS = [Platform.BINARY_SENSOR, Platform.DEVICE_TRACKER] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.DEVICE_TRACKER, Platform.SENSOR] @dataclass(slots=True) diff --git a/homeassistant/components/ping/binary_sensor.py b/homeassistant/components/ping/binary_sensor.py index 97636111586..35d4e218dce 100644 --- a/homeassistant/components/ping/binary_sensor.py +++ b/homeassistant/components/ping/binary_sensor.py @@ -1,4 +1,5 @@ """Tracks the latency of a host by sending ICMP echo requests (ping).""" + from __future__ import annotations import logging @@ -18,11 +19,11 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import PingDomainData from .const import CONF_IMPORTED_BY, CONF_PING_COUNT, DEFAULT_PING_COUNT, DOMAIN from .coordinator import PingUpdateCoordinator +from .entity import PingEntity _LOGGER = logging.getLogger(__name__) @@ -84,20 +85,18 @@ async def async_setup_entry( async_add_entities([PingBinarySensor(entry, data.coordinators[entry.entry_id])]) -class PingBinarySensor(CoordinatorEntity[PingUpdateCoordinator], BinarySensorEntity): +class PingBinarySensor(PingEntity, BinarySensorEntity): """Representation of a Ping Binary sensor.""" _attr_device_class = BinarySensorDeviceClass.CONNECTIVITY _attr_available = False + _attr_name = None def __init__( self, config_entry: ConfigEntry, coordinator: PingUpdateCoordinator ) -> None: """Initialize the Ping Binary sensor.""" - super().__init__(coordinator) - - self._attr_name = config_entry.title - self._attr_unique_id = config_entry.entry_id + super().__init__(config_entry, coordinator, config_entry.entry_id) # if this was imported just enable it when it was enabled before if CONF_IMPORTED_BY in config_entry.data: @@ -113,11 +112,9 @@ class PingBinarySensor(CoordinatorEntity[PingUpdateCoordinator], BinarySensorEnt @property def extra_state_attributes(self) -> dict[str, Any] | None: """Return the state attributes of the ICMP checo request.""" - if self.coordinator.data.data is None: - return None return { - ATTR_ROUND_TRIP_TIME_AVG: self.coordinator.data.data["avg"], - ATTR_ROUND_TRIP_TIME_MAX: self.coordinator.data.data["max"], - ATTR_ROUND_TRIP_TIME_MDEV: self.coordinator.data.data["mdev"], - ATTR_ROUND_TRIP_TIME_MIN: self.coordinator.data.data["min"], + ATTR_ROUND_TRIP_TIME_AVG: self.coordinator.data.data.get("avg"), + ATTR_ROUND_TRIP_TIME_MAX: self.coordinator.data.data.get("max"), + ATTR_ROUND_TRIP_TIME_MDEV: self.coordinator.data.data.get("mdev"), + ATTR_ROUND_TRIP_TIME_MIN: self.coordinator.data.data.get("min"), } diff --git a/homeassistant/components/ping/config_flow.py b/homeassistant/components/ping/config_flow.py index 29b8a8ba2a5..52600c379c4 100644 --- a/homeassistant/components/ping/config_flow.py +++ b/homeassistant/components/ping/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Ping (ICMP) integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -7,14 +8,18 @@ from typing import Any import voluptuous as vol -from homeassistant import config_entries from homeassistant.components.device_tracker import ( CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME, ) +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.const import CONF_HOST, CONF_NAME from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import selector from homeassistant.util.network import is_ip_address @@ -23,14 +28,14 @@ from .const import CONF_IMPORTED_BY, CONF_PING_COUNT, DEFAULT_PING_COUNT, DOMAIN _LOGGER = logging.getLogger(__name__) -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class PingConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Ping.""" VERSION = 1 async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" if user_input is None: return self.async_show_form( @@ -56,7 +61,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): }, ) - async def async_step_import(self, import_info: Mapping[str, Any]) -> FlowResult: + async def async_step_import( + self, import_info: Mapping[str, Any] + ) -> ConfigFlowResult: """Import an entry.""" to_import = { @@ -78,22 +85,22 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback def async_get_options_flow( - config_entry: config_entries.ConfigEntry, - ) -> config_entries.OptionsFlow: + config_entry: ConfigEntry, + ) -> OptionsFlow: """Create the options flow.""" return OptionsFlowHandler(config_entry) -class OptionsFlowHandler(config_entries.OptionsFlow): +class OptionsFlowHandler(OptionsFlow): """Handle an options flow for Ping.""" - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + 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: + ) -> ConfigFlowResult: """Manage the options.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) diff --git a/homeassistant/components/ping/coordinator.py b/homeassistant/components/ping/coordinator.py index f6bda9693b8..38ab2e79ffc 100644 --- a/homeassistant/components/ping/coordinator.py +++ b/homeassistant/components/ping/coordinator.py @@ -1,4 +1,5 @@ """DataUpdateCoordinator for the ping integration.""" + from __future__ import annotations from dataclasses import dataclass @@ -20,7 +21,7 @@ class PingResult: ip_address: str is_alive: bool - data: dict[str, Any] | None + data: dict[str, Any] class PingUpdateCoordinator(DataUpdateCoordinator[PingResult]): @@ -49,5 +50,5 @@ class PingUpdateCoordinator(DataUpdateCoordinator[PingResult]): return PingResult( ip_address=self.ping.ip_address, is_alive=self.ping.is_alive, - data=self.ping.data, + data=self.ping.data or {}, ) diff --git a/homeassistant/components/ping/device_tracker.py b/homeassistant/components/ping/device_tracker.py index 6b904043b30..b202c1c406e 100644 --- a/homeassistant/components/ping/device_tracker.py +++ b/homeassistant/components/ping/device_tracker.py @@ -1,4 +1,5 @@ """Tracks devices by sending a ICMP echo request (ping).""" + from __future__ import annotations from datetime import datetime, timedelta diff --git a/homeassistant/components/ping/entity.py b/homeassistant/components/ping/entity.py new file mode 100644 index 00000000000..34207b284bb --- /dev/null +++ b/homeassistant/components/ping/entity.py @@ -0,0 +1,29 @@ +"""Base entity for the Ping component.""" + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import DOMAIN +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .coordinator import PingUpdateCoordinator + + +class PingEntity(CoordinatorEntity[PingUpdateCoordinator]): + """Represents a Ping base entity.""" + + _attr_has_entity_name = True + + def __init__( + self, + config_entry: ConfigEntry, + coordinator: PingUpdateCoordinator, + unique_id: str, + ) -> None: + """Initialize the sensor.""" + super().__init__(coordinator) + + self._attr_unique_id = unique_id + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, config_entry.entry_id)}, + manufacturer="Ping", + ) diff --git a/homeassistant/components/ping/helpers.py b/homeassistant/components/ping/helpers.py index e3ebaffec12..f9afcef7be9 100644 --- a/homeassistant/components/ping/helpers.py +++ b/homeassistant/components/ping/helpers.py @@ -1,4 +1,5 @@ """Ping classes shared between platforms.""" + import asyncio from contextlib import suppress import logging @@ -70,7 +71,6 @@ class PingDataICMPLib(PingData): "min": data.min_rtt, "max": data.max_rtt, "avg": data.avg_rtt, - "mdev": "", } @@ -135,7 +135,7 @@ class PingDataSubProcess(PingData): if TYPE_CHECKING: assert match is not None rtt_min, rtt_avg, rtt_max = match.groups() - return {"min": rtt_min, "avg": rtt_avg, "max": rtt_max, "mdev": ""} + return {"min": rtt_min, "avg": rtt_avg, "max": rtt_max} match = PING_MATCHER.search(str(out_data).rsplit("\n", maxsplit=1)[-1]) if TYPE_CHECKING: assert match is not None diff --git a/homeassistant/components/ping/sensor.py b/homeassistant/components/ping/sensor.py new file mode 100644 index 00000000000..135087f4b5b --- /dev/null +++ b/homeassistant/components/ping/sensor.py @@ -0,0 +1,119 @@ +"""Sensor platform that for Ping integration.""" + +from collections.abc import Callable +from dataclasses import dataclass + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import EntityCategory, UnitOfTime +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import PingDomainData +from .const import DOMAIN +from .coordinator import PingResult, PingUpdateCoordinator +from .entity import PingEntity + + +@dataclass(frozen=True, kw_only=True) +class PingSensorEntityDescription(SensorEntityDescription): + """Class to describe a Ping sensor entity.""" + + value_fn: Callable[[PingResult], float | None] + has_fn: Callable[[PingResult], bool] + + +SENSORS: tuple[PingSensorEntityDescription, ...] = ( + PingSensorEntityDescription( + key="round_trip_time_avg", + translation_key="round_trip_time_avg", + native_unit_of_measurement=UnitOfTime.MILLISECONDS, + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.DURATION, + entity_registry_enabled_default=False, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda result: result.data.get("avg"), + has_fn=lambda result: "avg" in result.data, + ), + PingSensorEntityDescription( + key="round_trip_time_max", + translation_key="round_trip_time_max", + native_unit_of_measurement=UnitOfTime.MILLISECONDS, + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.DURATION, + entity_registry_enabled_default=False, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda result: result.data.get("max"), + has_fn=lambda result: "max" in result.data, + ), + PingSensorEntityDescription( + key="round_trip_time_mdev", + translation_key="round_trip_time_mdev", + native_unit_of_measurement=UnitOfTime.MILLISECONDS, + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.DURATION, + entity_registry_enabled_default=False, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda result: result.data.get("mdev"), + has_fn=lambda result: "mdev" in result.data, + ), + PingSensorEntityDescription( + key="round_trip_time_min", + translation_key="round_trip_time_min", + native_unit_of_measurement=UnitOfTime.MILLISECONDS, + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.DURATION, + entity_registry_enabled_default=False, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda result: result.data.get("min"), + has_fn=lambda result: "min" in result.data, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up Ping sensors from config entry.""" + data: PingDomainData = hass.data[DOMAIN] + coordinator = data.coordinators[entry.entry_id] + + async_add_entities( + PingSensor(entry, description, coordinator) + for description in SENSORS + if description.has_fn(coordinator.data) + ) + + +class PingSensor(PingEntity, SensorEntity): + """Represents a Ping sensor.""" + + entity_description: PingSensorEntityDescription + + def __init__( + self, + config_entry: ConfigEntry, + description: PingSensorEntityDescription, + coordinator: PingUpdateCoordinator, + ) -> None: + """Initialize the sensor.""" + super().__init__( + config_entry, coordinator, f"{config_entry.entry_id}-{description.key}" + ) + + self.entity_description = description + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return super().available and self.coordinator.data.is_alive + + @property + def native_value(self) -> float | None: + """Return the sensor state.""" + return self.entity_description.value_fn(self.coordinator.data) diff --git a/homeassistant/components/ping/strings.json b/homeassistant/components/ping/strings.json index 421d9079c62..ef9f74b4207 100644 --- a/homeassistant/components/ping/strings.json +++ b/homeassistant/components/ping/strings.json @@ -1,4 +1,20 @@ { + "entity": { + "sensor": { + "round_trip_time_avg": { + "name": "Round Trip Time Average" + }, + "round_trip_time_max": { + "name": "Round Trip Time Maximum" + }, + "round_trip_time_mdev": { + "name": "Round Trip Time Mean Deviation" + }, + "round_trip_time_min": { + "name": "Round Trip Time Minimum" + } + } + }, "config": { "step": { "user": { diff --git a/homeassistant/components/pioneer/media_player.py b/homeassistant/components/pioneer/media_player.py index 741d2b580e4..15cd3cbf303 100644 --- a/homeassistant/components/pioneer/media_player.py +++ b/homeassistant/components/pioneer/media_player.py @@ -1,4 +1,5 @@ """Support for Pioneer Network Receivers.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/pjlink/media_player.py b/homeassistant/components/pjlink/media_player.py index 1a7ff877bb8..ff3be3266a0 100644 --- a/homeassistant/components/pjlink/media_player.py +++ b/homeassistant/components/pjlink/media_player.py @@ -1,4 +1,5 @@ """Support for controlling projector via the PJLink protocol.""" + from __future__ import annotations from pypjlink import MUTE_AUDIO, Projector diff --git a/homeassistant/components/plaato/__init__.py b/homeassistant/components/plaato/__init__.py index 69c65383138..0d8678d95ef 100644 --- a/homeassistant/components/plaato/__init__.py +++ b/homeassistant/components/plaato/__init__.py @@ -1,4 +1,5 @@ """Support for Plaato devices.""" + from datetime import timedelta import logging diff --git a/homeassistant/components/plaato/binary_sensor.py b/homeassistant/components/plaato/binary_sensor.py index e8fbaa5d6f1..42019bbec9b 100644 --- a/homeassistant/components/plaato/binary_sensor.py +++ b/homeassistant/components/plaato/binary_sensor.py @@ -1,4 +1,5 @@ """Support for Plaato Airlock sensors.""" + from __future__ import annotations from pyplaato.plaato import PlaatoKeg diff --git a/homeassistant/components/plaato/config_flow.py b/homeassistant/components/plaato/config_flow.py index 654150ffa48..1240abc5e81 100644 --- a/homeassistant/components/plaato/config_flow.py +++ b/homeassistant/components/plaato/config_flow.py @@ -1,12 +1,12 @@ """Config flow for Plaato.""" + from __future__ import annotations from pyplaato.plaato import PlaatoDeviceType import voluptuous as vol -from homeassistant import config_entries from homeassistant.components import cloud, webhook -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow from homeassistant.const import CONF_SCAN_INTERVAL, CONF_TOKEN, CONF_WEBHOOK_ID from homeassistant.core import callback import homeassistant.helpers.config_validation as cv @@ -26,7 +26,7 @@ from .const import ( ) -class PlaatoConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class PlaatoConfigFlow(ConfigFlow, domain=DOMAIN): """Handles a Plaato config flow.""" VERSION = 1 @@ -168,7 +168,7 @@ class PlaatoConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return PlaatoOptionsFlowHandler(config_entry) -class PlaatoOptionsFlowHandler(config_entries.OptionsFlow): +class PlaatoOptionsFlowHandler(OptionsFlow): """Handle Plaato options.""" def __init__(self, config_entry: ConfigEntry) -> None: diff --git a/homeassistant/components/plaato/const.py b/homeassistant/components/plaato/const.py index 6825baff906..73382765bfe 100644 --- a/homeassistant/components/plaato/const.py +++ b/homeassistant/components/plaato/const.py @@ -1,4 +1,5 @@ """Const for Plaato.""" + from datetime import timedelta from homeassistant.const import Platform diff --git a/homeassistant/components/plaato/entity.py b/homeassistant/components/plaato/entity.py index b7650567c2b..d4c4622a998 100644 --- a/homeassistant/components/plaato/entity.py +++ b/homeassistant/components/plaato/entity.py @@ -1,4 +1,5 @@ """PlaatoEntity class.""" + from pyplaato.models.device import PlaatoDevice from homeassistant.helpers import entity diff --git a/homeassistant/components/plaato/sensor.py b/homeassistant/components/plaato/sensor.py index f3d9a5c3e41..7aa30dd2fe0 100644 --- a/homeassistant/components/plaato/sensor.py +++ b/homeassistant/components/plaato/sensor.py @@ -1,4 +1,5 @@ """Support for Plaato Airlock sensors.""" + from __future__ import annotations from pyplaato.models.device import PlaatoDevice diff --git a/homeassistant/components/plant/__init__.py b/homeassistant/components/plant/__init__.py index 28cdece0b02..076f93faf7b 100644 --- a/homeassistant/components/plant/__init__.py +++ b/homeassistant/components/plant/__init__.py @@ -1,4 +1,5 @@ """Support for monitoring plants.""" + from collections import deque from contextlib import suppress from datetime import datetime, timedelta @@ -19,7 +20,7 @@ from homeassistant.const import ( STATE_UNKNOWN, UnitOfTemperature, ) -from homeassistant.core import HomeAssistant, State, callback +from homeassistant.core import Event, HomeAssistant, State, callback from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -28,9 +29,10 @@ from homeassistant.helpers.event import ( EventStateChangedData, async_track_state_change_event, ) -from homeassistant.helpers.typing import ConfigType, EventType +from homeassistant.helpers.typing import ConfigType from homeassistant.util import dt as dt_util +from . import group as group_pre_import # noqa: F401 from .const import ( ATTR_DICT_OF_UNITS_OF_MEASUREMENT, ATTR_MAX_BRIGHTNESS_HISTORY, @@ -179,7 +181,7 @@ class Plant(Entity): self._brightness_history = DailyHistory(self._conf_check_days) @callback - def _state_changed_event(self, event: EventType[EventStateChangedData]) -> None: + def _state_changed_event(self, event: Event[EventStateChangedData]) -> None: """Sensor state change event.""" self.state_changed(event.data["entity_id"], event.data["new_state"]) diff --git a/homeassistant/components/plant/const.py b/homeassistant/components/plant/const.py index 0368c55e152..656705b3d12 100644 --- a/homeassistant/components/plant/const.py +++ b/homeassistant/components/plant/const.py @@ -1,4 +1,5 @@ """Const for Plant.""" + from typing import Final DOMAIN: Final = "plant" diff --git a/homeassistant/components/plant/group.py b/homeassistant/components/plant/group.py index 90e894abb0f..96d4166fe1f 100644 --- a/homeassistant/components/plant/group.py +++ b/homeassistant/components/plant/group.py @@ -1,14 +1,17 @@ """Describe group states.""" +from typing import TYPE_CHECKING -from homeassistant.components.group import GroupIntegrationRegistry from homeassistant.const import STATE_OK, STATE_PROBLEM from homeassistant.core import HomeAssistant, callback +if TYPE_CHECKING: + from homeassistant.components.group import GroupIntegrationRegistry + @callback def async_describe_on_off_states( - hass: HomeAssistant, registry: GroupIntegrationRegistry + hass: HomeAssistant, registry: "GroupIntegrationRegistry" ) -> None: """Describe group on off states.""" registry.on_off_states({STATE_PROBLEM}, STATE_OK) diff --git a/homeassistant/components/plex/__init__.py b/homeassistant/components/plex/__init__.py index 4ce5a359dcd..eb57dc46727 100644 --- a/homeassistant/components/plex/__init__.py +++ b/homeassistant/components/plex/__init__.py @@ -1,4 +1,5 @@ """Support to embed Plex.""" + from functools import partial import logging @@ -41,7 +42,6 @@ from .const import ( DOMAIN, INVALID_TOKEN_MESSAGE, PLATFORMS, - PLATFORMS_COMPLETED, PLEX_SERVER_CONFIG, PLEX_UPDATE_LIBRARY_SIGNAL, PLEX_UPDATE_PLATFORMS_SIGNAL, @@ -93,18 +93,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: gdm.scan(scan_for_clients=True) debouncer = Debouncer[None]( - hass, - _LOGGER, - cooldown=10, - immediate=True, - function=gdm_scan, + hass, _LOGGER, cooldown=10, immediate=True, function=gdm_scan, background=True ).async_call hass_data = PlexData( servers={}, dispatchers={}, websockets={}, - platforms_completed={}, gdm_scanner=gdm, gdm_debouncer=debouncer, ) @@ -179,7 +174,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: server_id = plex_server.machine_identifier hass_data = get_plex_data(hass) hass_data[SERVERS][server_id] = plex_server - hass_data[PLATFORMS_COMPLETED][server_id] = set() entry.add_update_listener(async_options_updated) @@ -232,11 +226,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) hass_data[WEBSOCKETS][server_id] = websocket - def start_websocket_session(platform): - hass_data[PLATFORMS_COMPLETED][server_id].add(platform) - if hass_data[PLATFORMS_COMPLETED][server_id] == PLATFORMS: - hass.loop.create_task(websocket.listen()) - def close_websocket_session(_): websocket.close() @@ -247,8 +236,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - for platform in PLATFORMS: - start_websocket_session(platform) + entry.async_create_background_task( + hass, websocket.listen(), f"plex websocket listener {entry.entry_id}" + ) async_cleanup_plex_devices(hass, entry) diff --git a/homeassistant/components/plex/button.py b/homeassistant/components/plex/button.py index 24bc09bac42..8bb34be38ce 100644 --- a/homeassistant/components/plex/button.py +++ b/homeassistant/components/plex/button.py @@ -1,4 +1,5 @@ """Representation of Plex buttons.""" + from __future__ import annotations from homeassistant.components.button import ButtonEntity diff --git a/homeassistant/components/plex/cast.py b/homeassistant/components/plex/cast.py index 7dc112b72de..bf68be20292 100644 --- a/homeassistant/components/plex/cast.py +++ b/homeassistant/components/plex/cast.py @@ -1,4 +1,5 @@ """Google Cast support for the Plex component.""" + from __future__ import annotations from pychromecast import Chromecast diff --git a/homeassistant/components/plex/config_flow.py b/homeassistant/components/plex/config_flow.py index 10ae380a08a..301716e14d5 100644 --- a/homeassistant/components/plex/config_flow.py +++ b/homeassistant/components/plex/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Plex.""" + from __future__ import annotations from collections.abc import Mapping @@ -13,10 +14,17 @@ from plexauth import PlexAuth import requests.exceptions import voluptuous as vol -from homeassistant import config_entries from homeassistant.components import http -from homeassistant.components.http.view import HomeAssistantView +from homeassistant.components.http import KEY_HASS, HomeAssistantView from homeassistant.components.media_player import DOMAIN as MP_DOMAIN +from homeassistant.config_entries import ( + SOURCE_INTEGRATION_DISCOVERY, + SOURCE_REAUTH, + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.const import ( CONF_CLIENT_ID, CONF_HOST, @@ -28,7 +36,6 @@ from homeassistant.const import ( CONF_VERIFY_SSL, ) from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import discovery_flow from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -80,12 +87,12 @@ async def async_discover(hass): discovery_flow.async_create_flow( hass, DOMAIN, - context={CONF_SOURCE: config_entries.SOURCE_INTEGRATION_DISCOVERY}, + context={CONF_SOURCE: SOURCE_INTEGRATION_DISCOVERY}, data=server_data, ) -class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class PlexFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a Plex config flow.""" VERSION = 1 @@ -93,7 +100,7 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback def async_get_options_flow( - config_entry: config_entries.ConfigEntry, + config_entry: ConfigEntry, ) -> PlexOptionsFlowHandler: """Get the options flow for this handler.""" return PlexOptionsFlowHandler(config_entry) @@ -241,7 +248,7 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): } entry = await self.async_set_unique_id(server_id) - if self.context[CONF_SOURCE] == config_entries.SOURCE_REAUTH: + if self.context[CONF_SOURCE] == SOURCE_REAUTH: self.hass.config_entries.async_update_entry(entry, data=data) _LOGGER.debug("Updated config entry for %s", plex_server.friendly_name) await self.hass.config_entries.async_reload(entry.entry_id) @@ -338,7 +345,9 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): server_config = {CONF_TOKEN: self.token} return await self.async_step_server_validate(server_config) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Handle a reauthorization flow request.""" self._reauth_config = { CONF_SERVER_IDENTIFIER: entry_data[CONF_SERVER_IDENTIFIER] @@ -346,10 +355,10 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_user() -class PlexOptionsFlowHandler(config_entries.OptionsFlow): +class PlexOptionsFlowHandler(OptionsFlow): """Handle Plex options.""" - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + def __init__(self, config_entry: ConfigEntry) -> None: """Initialize Plex options flow.""" self.options = copy.deepcopy(dict(config_entry.options)) self.server_id = config_entry.data[CONF_SERVER_IDENTIFIER] @@ -435,7 +444,7 @@ class PlexAuthorizationCallbackView(HomeAssistantView): async def get(self, request): """Receive authorization confirmation.""" - hass = request.app["hass"] + hass = request.app[KEY_HASS] await hass.config_entries.flow.async_configure( flow_id=request.query["flow_id"], user_input=None ) diff --git a/homeassistant/components/plex/const.py b/homeassistant/components/plex/const.py index 30b59c73994..d5d70219471 100644 --- a/homeassistant/components/plex/const.py +++ b/homeassistant/components/plex/const.py @@ -1,4 +1,5 @@ """Constants for the Plex component.""" + from datetime import timedelta from typing import Final @@ -23,7 +24,6 @@ GDM_SCANNER: Final = "gdm_scanner" PLATFORMS = frozenset( [Platform.BUTTON, Platform.MEDIA_PLAYER, Platform.SENSOR, Platform.UPDATE] ) -PLATFORMS_COMPLETED: Final = "platforms_completed" PLAYER_SOURCE = "player_source" SERVERS: Final = "servers" WEBSOCKETS: Final = "websockets" diff --git a/homeassistant/components/plex/errors.py b/homeassistant/components/plex/errors.py index ddbc1a2ea40..c1e1a186b9e 100644 --- a/homeassistant/components/plex/errors.py +++ b/homeassistant/components/plex/errors.py @@ -1,4 +1,5 @@ """Errors for the Plex component.""" + from homeassistant.exceptions import HomeAssistantError diff --git a/homeassistant/components/plex/helpers.py b/homeassistant/components/plex/helpers.py index 6a334c5ff61..3c7ff8180c8 100644 --- a/homeassistant/components/plex/helpers.py +++ b/homeassistant/components/plex/helpers.py @@ -1,4 +1,5 @@ """Helper methods for common Plex integration operations.""" + from __future__ import annotations from collections.abc import Callable, Coroutine @@ -7,7 +8,6 @@ from typing import TYPE_CHECKING, Any, TypedDict from plexapi.gdm import GDM from plexwebsocket import PlexWebsocket -from homeassistant.const import Platform from homeassistant.core import CALLBACK_TYPE, HomeAssistant from .const import DOMAIN, SERVERS @@ -22,7 +22,6 @@ class PlexData(TypedDict): servers: dict[str, PlexServer] dispatchers: dict[str, list[CALLBACK_TYPE]] websockets: dict[str, PlexWebsocket] - platforms_completed: dict[str, set[Platform]] gdm_scanner: GDM gdm_debouncer: Callable[[], Coroutine[Any, Any, None]] diff --git a/homeassistant/components/plex/icons.json b/homeassistant/components/plex/icons.json new file mode 100644 index 00000000000..03bc835d2f6 --- /dev/null +++ b/homeassistant/components/plex/icons.json @@ -0,0 +1,13 @@ +{ + "entity": { + "sensor": { + "plex": { + "default": "mdi:plex" + } + } + }, + "services": { + "refresh_library": "mdi:refresh", + "scan_for_clients": "mdi:database-refresh" + } +} diff --git a/homeassistant/components/plex/manifest.json b/homeassistant/components/plex/manifest.json index 99dd44c1ed8..85362371715 100644 --- a/homeassistant/components/plex/manifest.json +++ b/homeassistant/components/plex/manifest.json @@ -5,11 +5,10 @@ "config_flow": true, "dependencies": ["http"], "documentation": "https://www.home-assistant.io/integrations/plex", - "import_executor": true, "iot_class": "local_push", "loggers": ["plexapi", "plexwebsocket"], "requirements": [ - "PlexAPI==4.15.10", + "PlexAPI==4.15.11", "plexauth==0.0.6", "plexwebsocket==0.0.14" ], diff --git a/homeassistant/components/plex/media_browser.py b/homeassistant/components/plex/media_browser.py index d3a0cc0fb2e..9184edeb3bd 100644 --- a/homeassistant/components/plex/media_browser.py +++ b/homeassistant/components/plex/media_browser.py @@ -1,4 +1,5 @@ """Support to interface with the Plex API.""" + from __future__ import annotations from yarl import URL @@ -292,18 +293,16 @@ def generate_plex_uri(server_id, media_id, params=None): def root_payload(hass, is_internal, platform=None): """Return root payload for Plex.""" - children = [] - - for server_id in get_plex_data(hass)[SERVERS]: - children.append( - browse_media( - hass, - is_internal, - "server", - generate_plex_uri(server_id, ""), - platform=platform, - ) + children = [ + browse_media( + hass, + is_internal, + "server", + generate_plex_uri(server_id, ""), + platform=platform, ) + for server_id in get_plex_data(hass)[SERVERS] + ] if len(children) == 1: return children[0] diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index 3e817b4ea1a..21e52171fe8 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -1,4 +1,5 @@ """Support to interface with the Plex API.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/plex/media_search.py b/homeassistant/components/plex/media_search.py index a853bac33a5..bd785a08907 100644 --- a/homeassistant/components/plex/media_search.py +++ b/homeassistant/components/plex/media_search.py @@ -1,4 +1,5 @@ """Helper methods to search for Plex media.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/plex/models.py b/homeassistant/components/plex/models.py index 9c274774d07..f2fa3f60d24 100644 --- a/homeassistant/components/plex/models.py +++ b/homeassistant/components/plex/models.py @@ -1,4 +1,5 @@ """Models to represent various Plex objects used in the integration.""" + import logging from homeassistant.components.media_player import MediaType diff --git a/homeassistant/components/plex/sensor.py b/homeassistant/components/plex/sensor.py index acc309ab14c..eb27f465a7e 100644 --- a/homeassistant/components/plex/sensor.py +++ b/homeassistant/components/plex/sensor.py @@ -1,4 +1,5 @@ """Support for Plex media server monitoring.""" + from __future__ import annotations import logging @@ -56,12 +57,14 @@ async def async_setup_entry( """Set up Plex sensor from a config entry.""" server_id = config_entry.data[CONF_SERVER_IDENTIFIER] plexserver = get_plex_server(hass, server_id) - sensors = [PlexSensor(hass, plexserver)] + sensors: list[SensorEntity] = [PlexSensor(hass, plexserver)] def create_library_sensors(): """Create Plex library sensors with sync calls.""" - for library in plexserver.library.sections(): - sensors.append(PlexLibrarySectionSensor(hass, plexserver, library)) + sensors.extend( + PlexLibrarySectionSensor(hass, plexserver, library) + for library in plexserver.library.sections() + ) await hass.async_add_executor_job(create_library_sensors) async_add_entities(sensors) @@ -72,7 +75,7 @@ class PlexSensor(SensorEntity): _attr_has_entity_name = True _attr_name = None - _attr_icon = "mdi:plex" + _attr_translation_key = "plex" _attr_should_poll = False _attr_native_unit_of_measurement = "watching" @@ -183,10 +186,10 @@ class PlexLibrarySectionSensor(SensorEntity): libtype=primary_libtype, includeCollections=False ) for libtype in LIBRARY_ATTRIBUTE_TYPES.get(self.library_type, []): - self._attr_extra_state_attributes[ - f"{libtype}s" - ] = self.library_section.totalViewSize( - libtype=libtype, includeCollections=False + self._attr_extra_state_attributes[f"{libtype}s"] = ( + self.library_section.totalViewSize( + libtype=libtype, includeCollections=False + ) ) recent_libtype = LIBRARY_RECENT_LIBTYPE.get( diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index 1c3c944c9c4..584378d51f9 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -1,4 +1,5 @@ """Shared class to maintain Plex server instances.""" + from __future__ import annotations import logging @@ -96,6 +97,7 @@ class PlexServer: cooldown=DEBOUNCE_TIMEOUT, immediate=True, function=self._async_update_platforms, + background=True, ).async_call self.thumbnail_cache = {} @@ -481,9 +483,9 @@ class PlexServer: continue process_device("session", player) - available_clients[player.machineIdentifier][ - "session" - ] = self.active_sessions[unique_id] + available_clients[player.machineIdentifier]["session"] = ( + self.active_sessions[unique_id] + ) for device in devices: process_device("PMS", device) diff --git a/homeassistant/components/plex/services.py b/homeassistant/components/plex/services.py index 39d41369a4b..e0fe79be182 100644 --- a/homeassistant/components/plex/services.py +++ b/homeassistant/components/plex/services.py @@ -1,4 +1,5 @@ """Services for the Plex integration.""" + import json import logging diff --git a/homeassistant/components/plex/update.py b/homeassistant/components/plex/update.py index e48c3a339d5..7acf4551f33 100644 --- a/homeassistant/components/plex/update.py +++ b/homeassistant/components/plex/update.py @@ -1,4 +1,5 @@ """Representation of Plex updates.""" + import logging from typing import Any @@ -52,6 +53,7 @@ class PlexUpdate(UpdateEntity): self._attr_installed_version = self.plex_server.version try: if (release := self.plex_server.checkForUpdate()) is None: + self._attr_latest_version = self.installed_version return except (requests.exceptions.RequestException, PlexApiException): _LOGGER.debug("Polling update sensor failed, will try again") diff --git a/homeassistant/components/plex/view.py b/homeassistant/components/plex/view.py index ba883883ddc..c1254a9795a 100644 --- a/homeassistant/components/plex/view.py +++ b/homeassistant/components/plex/view.py @@ -1,4 +1,5 @@ """Implement a view to provide proxied Plex thumbnails to the media browser.""" + from __future__ import annotations from http import HTTPStatus @@ -8,7 +9,7 @@ from aiohttp import web from aiohttp.hdrs import CACHE_CONTROL from aiohttp.typedefs import LooseHeaders -from homeassistant.components.http import KEY_AUTHENTICATED, HomeAssistantView +from homeassistant.components.http import KEY_AUTHENTICATED, KEY_HASS, HomeAssistantView from homeassistant.components.media_player import async_fetch_image from .const import SERVERS @@ -33,7 +34,7 @@ class PlexImageView(HomeAssistantView): if not request[KEY_AUTHENTICATED]: return web.Response(status=HTTPStatus.UNAUTHORIZED) - hass = request.app["hass"] + hass = request.app[KEY_HASS] if (server := get_plex_data(hass)[SERVERS].get(server_id)) is None: return web.Response(status=HTTPStatus.NOT_FOUND) diff --git a/homeassistant/components/plugwise/__init__.py b/homeassistant/components/plugwise/__init__.py index bfae7772b93..28389ffa357 100644 --- a/homeassistant/components/plugwise/__init__.py +++ b/homeassistant/components/plugwise/__init__.py @@ -1,4 +1,5 @@ """Plugwise platform for Home Assistant Core.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/plugwise/binary_sensor.py b/homeassistant/components/plugwise/binary_sensor.py index c362652cf47..d32ae94160f 100644 --- a/homeassistant/components/plugwise/binary_sensor.py +++ b/homeassistant/components/plugwise/binary_sensor.py @@ -1,4 +1,5 @@ """Plugwise Binary Sensor component for Home Assistant.""" + from __future__ import annotations from collections.abc import Mapping diff --git a/homeassistant/components/plugwise/climate.py b/homeassistant/components/plugwise/climate.py index 3553df02e8d..7820c86a242 100644 --- a/homeassistant/components/plugwise/climate.py +++ b/homeassistant/components/plugwise/climate.py @@ -1,4 +1,5 @@ """Plugwise Climate component for Home Assistant.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/plugwise/config_flow.py b/homeassistant/components/plugwise/config_flow.py index 89c1b6eab52..4c33e51788f 100644 --- a/homeassistant/components/plugwise/config_flow.py +++ b/homeassistant/components/plugwise/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Plugwise integration.""" + from __future__ import annotations from typing import Any @@ -15,7 +16,7 @@ from plugwise.exceptions import ( import voluptuous as vol from homeassistant.components.zeroconf import ZeroconfServiceInfo -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import ( CONF_BASE, CONF_HOST, @@ -25,7 +26,6 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import ( @@ -89,7 +89,7 @@ class PlugwiseConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_zeroconf( self, discovery_info: ZeroconfServiceInfo - ) -> FlowResult: + ) -> ConfigFlowResult: """Prepare configuration for a discovered Plugwise Smile.""" self.discovery_info = discovery_info _properties = discovery_info.properties @@ -166,7 +166,7 @@ class PlugwiseConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step when using network/gateway setups.""" errors = {} diff --git a/homeassistant/components/plugwise/const.py b/homeassistant/components/plugwise/const.py index cad891f16f2..975ddae346a 100644 --- a/homeassistant/components/plugwise/const.py +++ b/homeassistant/components/plugwise/const.py @@ -1,4 +1,5 @@ """Constants for Plugwise component.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/plugwise/coordinator.py b/homeassistant/components/plugwise/coordinator.py index 395ec4e6e63..15a0e8c4821 100644 --- a/homeassistant/components/plugwise/coordinator.py +++ b/homeassistant/components/plugwise/coordinator.py @@ -1,4 +1,5 @@ """DataUpdateCoordinator for Plugwise.""" + from datetime import timedelta from plugwise import PlugwiseData, Smile diff --git a/homeassistant/components/plugwise/diagnostics.py b/homeassistant/components/plugwise/diagnostics.py index ef54efbe96d..44c0fa9a1da 100644 --- a/homeassistant/components/plugwise/diagnostics.py +++ b/homeassistant/components/plugwise/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for Plugwise.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/plugwise/entity.py b/homeassistant/components/plugwise/entity.py index 1c9149fad72..b2562ef8f39 100644 --- a/homeassistant/components/plugwise/entity.py +++ b/homeassistant/components/plugwise/entity.py @@ -1,4 +1,5 @@ """Generic Plugwise Entity Class.""" + from __future__ import annotations from plugwise.constants import DeviceData diff --git a/homeassistant/components/plugwise/manifest.json b/homeassistant/components/plugwise/manifest.json index 9b898305899..888f813760a 100644 --- a/homeassistant/components/plugwise/manifest.json +++ b/homeassistant/components/plugwise/manifest.json @@ -6,7 +6,7 @@ "documentation": "https://www.home-assistant.io/integrations/plugwise", "integration_type": "hub", "iot_class": "local_polling", - "loggers": ["crcmod", "plugwise"], - "requirements": ["plugwise==0.36.3"], + "loggers": ["plugwise"], + "requirements": ["plugwise==0.37.1"], "zeroconf": ["_plugwise._tcp.local."] } diff --git a/homeassistant/components/plugwise/number.py b/homeassistant/components/plugwise/number.py index c71b52cf5c8..2bae113a73e 100644 --- a/homeassistant/components/plugwise/number.py +++ b/homeassistant/components/plugwise/number.py @@ -1,4 +1,5 @@ """Number platform for Plugwise integration.""" + from __future__ import annotations from collections.abc import Awaitable, Callable @@ -75,15 +76,12 @@ async def async_setup_entry( 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) + async_add_entities( + PlugwiseNumberEntity(coordinator, device_id, description) + for device_id, device in coordinator.data.devices.items() + for description in NUMBER_TYPES + if description.key in device + ) class PlugwiseNumberEntity(PlugwiseEntity, NumberEntity): diff --git a/homeassistant/components/plugwise/select.py b/homeassistant/components/plugwise/select.py index ff5eb3af4a5..10718a818ff 100644 --- a/homeassistant/components/plugwise/select.py +++ b/homeassistant/components/plugwise/select.py @@ -1,4 +1,5 @@ """Plugwise Select component for Home Assistant.""" + from __future__ import annotations from collections.abc import Awaitable, Callable @@ -67,15 +68,12 @@ async def async_setup_entry( config_entry.entry_id ] - entities: list[PlugwiseSelectEntity] = [] - for device_id, device in coordinator.data.devices.items(): - for description in SELECT_TYPES: - if description.options_key in device: - entities.append( - PlugwiseSelectEntity(coordinator, device_id, description) - ) - - async_add_entities(entities) + async_add_entities( + PlugwiseSelectEntity(coordinator, device_id, description) + for device_id, device in coordinator.data.devices.items() + for description in SELECT_TYPES + if description.options_key in device + ) class PlugwiseSelectEntity(PlugwiseEntity, SelectEntity): diff --git a/homeassistant/components/plugwise/sensor.py b/homeassistant/components/plugwise/sensor.py index 86992bb08f1..2dfe97a06c5 100644 --- a/homeassistant/components/plugwise/sensor.py +++ b/homeassistant/components/plugwise/sensor.py @@ -1,4 +1,5 @@ """Plugwise Sensor component for Home Assistant.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/homeassistant/components/plugwise/switch.py b/homeassistant/components/plugwise/switch.py index 50e0a3cc4f8..3c737e19a4a 100644 --- a/homeassistant/components/plugwise/switch.py +++ b/homeassistant/components/plugwise/switch.py @@ -1,4 +1,5 @@ """Plugwise Switch component for HomeAssistant.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/homeassistant/components/plugwise/util.py b/homeassistant/components/plugwise/util.py index 4f8d4c8d8fe..df1069cbbc3 100644 --- a/homeassistant/components/plugwise/util.py +++ b/homeassistant/components/plugwise/util.py @@ -1,4 +1,5 @@ """Utilities for Plugwise.""" + from collections.abc import Awaitable, Callable, Coroutine from typing import Any, Concatenate, ParamSpec, TypeVar diff --git a/homeassistant/components/plum_lightpad/__init__.py b/homeassistant/components/plum_lightpad/__init__.py index 78c7bf7ff6a..f1816f03d3b 100644 --- a/homeassistant/components/plum_lightpad/__init__.py +++ b/homeassistant/components/plum_lightpad/__init__.py @@ -1,4 +1,5 @@ """Support for Plum Lightpad devices.""" + import logging from aiohttp import ContentTypeError diff --git a/homeassistant/components/plum_lightpad/config_flow.py b/homeassistant/components/plum_lightpad/config_flow.py index 9f81a57d42e..2a929d14c9e 100644 --- a/homeassistant/components/plum_lightpad/config_flow.py +++ b/homeassistant/components/plum_lightpad/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Plum Lightpad.""" + from __future__ import annotations import logging @@ -8,9 +9,8 @@ from aiohttp import ContentTypeError from requests.exceptions import ConnectTimeout, HTTPError import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN from .utils import load_plum @@ -18,7 +18,7 @@ from .utils import load_plum _LOGGER = logging.getLogger(__name__) -class PlumLightpadConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class PlumLightpadConfigFlow(ConfigFlow, domain=DOMAIN): """Config flow for Plum Lightpad integration.""" VERSION = 1 @@ -37,7 +37,7 @@ class PlumLightpadConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by the user or redirected to by import.""" if not user_input: return self._show_form() diff --git a/homeassistant/components/plum_lightpad/icons.json b/homeassistant/components/plum_lightpad/icons.json new file mode 100644 index 00000000000..dd65160e474 --- /dev/null +++ b/homeassistant/components/plum_lightpad/icons.json @@ -0,0 +1,9 @@ +{ + "entity": { + "light": { + "glow_ring": { + "default": "mdi:crop-portrait" + } + } + } +} diff --git a/homeassistant/components/plum_lightpad/light.py b/homeassistant/components/plum_lightpad/light.py index 9464e66e3a9..a385565b837 100644 --- a/homeassistant/components/plum_lightpad/light.py +++ b/homeassistant/components/plum_lightpad/light.py @@ -1,4 +1,5 @@ """Support for Plum Lightpad lights.""" + from __future__ import annotations from typing import Any @@ -130,8 +131,8 @@ class GlowRing(LightEntity): _attr_color_mode = ColorMode.HS _attr_should_poll = False + _attr_translation_key = "glow_ring" _attr_supported_color_modes = {ColorMode.HS} - _attr_icon = "mdi:crop-portrait" def __init__(self, lightpad): """Initialize the light.""" diff --git a/homeassistant/components/pocketcasts/sensor.py b/homeassistant/components/pocketcasts/sensor.py index c541e2cc0f2..b2ad050fc14 100644 --- a/homeassistant/components/pocketcasts/sensor.py +++ b/homeassistant/components/pocketcasts/sensor.py @@ -1,4 +1,5 @@ """Support for Pocket Casts.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/point/__init__.py b/homeassistant/components/point/__init__.py index 9fe63bf1d55..9f0f6e6dc7c 100644 --- a/homeassistant/components/point/__init__.py +++ b/homeassistant/components/point/__init__.py @@ -1,4 +1,5 @@ """Support for Minut Point.""" + import asyncio import logging diff --git a/homeassistant/components/point/alarm_control_panel.py b/homeassistant/components/point/alarm_control_panel.py index c2a904ec2a1..b04742af06a 100644 --- a/homeassistant/components/point/alarm_control_panel.py +++ b/homeassistant/components/point/alarm_control_panel.py @@ -1,4 +1,5 @@ """Support for Minut Point.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/point/binary_sensor.py b/homeassistant/components/point/binary_sensor.py index 81101d2da79..8863ee8ed81 100644 --- a/homeassistant/components/point/binary_sensor.py +++ b/homeassistant/components/point/binary_sensor.py @@ -1,4 +1,5 @@ """Support for Minut Point binary sensors.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/point/config_flow.py b/homeassistant/components/point/config_flow.py index 718e4a831c9..acf4b3e6d34 100644 --- a/homeassistant/components/point/config_flow.py +++ b/homeassistant/components/point/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Minut Point.""" + import asyncio from collections import OrderedDict import logging @@ -6,8 +7,8 @@ import logging from pypoint import PointSession import voluptuous as vol -from homeassistant import config_entries -from homeassistant.components.http import HomeAssistantView +from homeassistant.components.http import KEY_HASS, HomeAssistantView +from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.core import callback from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -40,7 +41,7 @@ def register_flow_implementation(hass, domain, client_id, client_secret): } -class PointFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class PointFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a config flow.""" VERSION = 1 @@ -179,7 +180,7 @@ class MinutAuthCallbackView(HomeAssistantView): @staticmethod async def get(request): """Receive authorization code.""" - hass = request.app["hass"] + hass = request.app[KEY_HASS] if "code" in request.query: hass.async_create_task( hass.config_entries.flow.async_init( diff --git a/homeassistant/components/point/const.py b/homeassistant/components/point/const.py index c21971185f9..c8c8f14d019 100644 --- a/homeassistant/components/point/const.py +++ b/homeassistant/components/point/const.py @@ -1,4 +1,5 @@ """Define constants for the Point component.""" + from datetime import timedelta DOMAIN = "point" diff --git a/homeassistant/components/point/sensor.py b/homeassistant/components/point/sensor.py index 471fa72c6c5..f648bb4daf9 100644 --- a/homeassistant/components/point/sensor.py +++ b/homeassistant/components/point/sensor.py @@ -1,7 +1,7 @@ """Support for Minut Point sensors.""" + from __future__ import annotations -from dataclasses import dataclass import logging from homeassistant.components.sensor import ( @@ -23,36 +23,22 @@ from .const import DOMAIN as POINT_DOMAIN, POINT_DISCOVERY_NEW _LOGGER = logging.getLogger(__name__) -@dataclass(frozen=True) -class MinutPointRequiredKeysMixin: - """Mixin for required keys.""" - - precision: int - - -@dataclass(frozen=True) -class MinutPointSensorEntityDescription( - SensorEntityDescription, MinutPointRequiredKeysMixin -): - """Describes MinutPoint sensor entity.""" - - -SENSOR_TYPES: tuple[MinutPointSensorEntityDescription, ...] = ( - MinutPointSensorEntityDescription( +SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( + SensorEntityDescription( key="temperature", - precision=1, + suggested_display_precision=1, device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=UnitOfTemperature.CELSIUS, ), - MinutPointSensorEntityDescription( + SensorEntityDescription( key="humidity", - precision=1, + suggested_display_precision=1, device_class=SensorDeviceClass.HUMIDITY, native_unit_of_measurement=PERCENTAGE, ), - MinutPointSensorEntityDescription( + SensorEntityDescription( key="sound", - precision=1, + suggested_display_precision=1, device_class=SensorDeviceClass.SOUND_PRESSURE, native_unit_of_measurement=UnitOfSoundPressure.WEIGHTED_DECIBEL_A, ), @@ -85,10 +71,8 @@ async def async_setup_entry( class MinutPointSensor(MinutPointEntity, SensorEntity): """The platform class required by Home Assistant.""" - entity_description: MinutPointSensorEntityDescription - def __init__( - self, point_client, device_id, description: MinutPointSensorEntityDescription + self, point_client, device_id, description: SensorEntityDescription ) -> None: """Initialize the sensor.""" super().__init__(point_client, device_id, description.device_class) @@ -99,9 +83,5 @@ class MinutPointSensor(MinutPointEntity, SensorEntity): _LOGGER.debug("Update sensor value for %s", self) if self.is_updated: self._attr_native_value = await self.device.sensor(self.device_class) - if self.native_value is not None: - self._attr_native_value = round( - self.native_value, self.entity_description.precision - ) self._updated = parse_datetime(self.device.last_update) self.async_write_ha_state() diff --git a/homeassistant/components/poolsense/__init__.py b/homeassistant/components/poolsense/__init__.py index 644ecb8cf3d..808d2300798 100644 --- a/homeassistant/components/poolsense/__init__.py +++ b/homeassistant/components/poolsense/__init__.py @@ -1,4 +1,5 @@ """The PoolSense integration.""" + import logging from poolsense import PoolSense diff --git a/homeassistant/components/poolsense/binary_sensor.py b/homeassistant/components/poolsense/binary_sensor.py index 052a205a37b..69c133c8c1e 100644 --- a/homeassistant/components/poolsense/binary_sensor.py +++ b/homeassistant/components/poolsense/binary_sensor.py @@ -1,4 +1,5 @@ """Support for PoolSense binary sensors.""" + from __future__ import annotations from homeassistant.components.binary_sensor import ( diff --git a/homeassistant/components/poolsense/config_flow.py b/homeassistant/components/poolsense/config_flow.py index 64685d67035..915fa1c8d06 100644 --- a/homeassistant/components/poolsense/config_flow.py +++ b/homeassistant/components/poolsense/config_flow.py @@ -1,13 +1,13 @@ """Config flow for PoolSense integration.""" + import logging from typing import Any from poolsense import PoolSense import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_EMAIL, CONF_PASSWORD -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import aiohttp_client from .const import DOMAIN @@ -15,7 +15,7 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -class PoolSenseConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class PoolSenseConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for PoolSense.""" VERSION = 1 @@ -25,7 +25,7 @@ class PoolSenseConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors = {} diff --git a/homeassistant/components/poolsense/coordinator.py b/homeassistant/components/poolsense/coordinator.py index e5e3e6ad1bd..8b6f99ed72b 100644 --- a/homeassistant/components/poolsense/coordinator.py +++ b/homeassistant/components/poolsense/coordinator.py @@ -1,4 +1,5 @@ """DataUpdateCoordinator for poolsense integration.""" + import asyncio from datetime import timedelta import logging diff --git a/homeassistant/components/poolsense/entity.py b/homeassistant/components/poolsense/entity.py index 0eca39cc48d..eaf2c4ab540 100644 --- a/homeassistant/components/poolsense/entity.py +++ b/homeassistant/components/poolsense/entity.py @@ -1,4 +1,5 @@ """Base entity for poolsense integration.""" + from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/poolsense/icons.json b/homeassistant/components/poolsense/icons.json new file mode 100644 index 00000000000..fb36897af78 --- /dev/null +++ b/homeassistant/components/poolsense/icons.json @@ -0,0 +1,24 @@ +{ + "entity": { + "sensor": { + "chlorine": { + "default": "mdi:pool" + }, + "water_temp": { + "default": "mdi:coolant-temperature" + }, + "chlorine_high": { + "default": "mdi:pool" + }, + "chlorine_low": { + "default": "mdi:pool" + }, + "ph_high": { + "default": "mdi:pool" + }, + "ph_low": { + "default": "mdi:pool" + } + } + } +} diff --git a/homeassistant/components/poolsense/sensor.py b/homeassistant/components/poolsense/sensor.py index c61196d9931..d40ee823664 100644 --- a/homeassistant/components/poolsense/sensor.py +++ b/homeassistant/components/poolsense/sensor.py @@ -1,4 +1,5 @@ """Sensor platform for the PoolSense sensor.""" + from __future__ import annotations from homeassistant.components.sensor import ( @@ -25,11 +26,9 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key="Chlorine", translation_key="chlorine", native_unit_of_measurement=UnitOfElectricPotential.MILLIVOLT, - icon="mdi:pool", ), SensorEntityDescription( key="pH", - icon="mdi:pool", device_class=SensorDeviceClass.PH, ), SensorEntityDescription( @@ -40,36 +39,31 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="Water Temp", native_unit_of_measurement=UnitOfTemperature.CELSIUS, - icon="mdi:coolant-temperature", + translation_key="water_temp", device_class=SensorDeviceClass.TEMPERATURE, ), SensorEntityDescription( key="Last Seen", translation_key="last_seen", - icon="mdi:clock", device_class=SensorDeviceClass.TIMESTAMP, ), SensorEntityDescription( key="Chlorine High", translation_key="chlorine_high", native_unit_of_measurement=UnitOfElectricPotential.MILLIVOLT, - icon="mdi:pool", ), SensorEntityDescription( key="Chlorine Low", translation_key="chlorine_low", native_unit_of_measurement=UnitOfElectricPotential.MILLIVOLT, - icon="mdi:pool", ), SensorEntityDescription( key="pH High", translation_key="ph_high", - icon="mdi:pool", ), SensorEntityDescription( key="pH Low", translation_key="ph_low", - icon="mdi:pool", ), ) diff --git a/homeassistant/components/powerwall/__init__.py b/homeassistant/components/powerwall/__init__.py index 086e49eef94..5257e5a6299 100644 --- a/homeassistant/components/powerwall/__init__.py +++ b/homeassistant/components/powerwall/__init__.py @@ -1,4 +1,5 @@ """The Tesla Powerwall integration.""" + from __future__ import annotations from contextlib import AsyncExitStack diff --git a/homeassistant/components/powerwall/config_flow.py b/homeassistant/components/powerwall/config_flow.py index 8b347ef49c1..7629d83d9d6 100644 --- a/homeassistant/components/powerwall/config_flow.py +++ b/homeassistant/components/powerwall/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Tesla Powerwall integration.""" + from __future__ import annotations import asyncio @@ -16,10 +17,16 @@ from tesla_powerwall import ( ) import voluptuous as vol -from homeassistant import config_entries, core, exceptions from homeassistant.components import dhcp +from homeassistant.config_entries import ( + ConfigEntry, + ConfigEntryState, + ConfigFlow, + ConfigFlowResult, +) from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD -from homeassistant.data_entry_flow import FlowResult +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_create_clientsession from homeassistant.util.network import is_ip_address @@ -30,8 +37,8 @@ _LOGGER = logging.getLogger(__name__) ENTRY_FAILURE_STATES = { - config_entries.ConfigEntryState.SETUP_ERROR, - config_entries.ConfigEntryState.SETUP_RETRY, + ConfigEntryState.SETUP_ERROR, + ConfigEntryState.SETUP_RETRY, } @@ -59,9 +66,7 @@ async def _powerwall_is_reachable(ip_address: str, password: str) -> bool: return True -async def validate_input( - hass: core.HomeAssistant, data: dict[str, str] -) -> dict[str, str]: +async def validate_input(hass: HomeAssistant, data: dict[str, str]) -> dict[str, str]: """Validate the user input allows us to connect. Data has the keys from schema with values provided by the user. @@ -85,7 +90,7 @@ async def validate_input( return {"title": site_info.site_name, "unique_id": gateway_din.upper()} -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class PowerwallConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Tesla Powerwall.""" VERSION = 1 @@ -94,11 +99,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Initialize the powerwall flow.""" self.ip_address: str | None = None self.title: str | None = None - self.reauth_entry: config_entries.ConfigEntry | None = None + self.reauth_entry: ConfigEntry | None = None - async def _async_powerwall_is_offline( - self, entry: config_entries.ConfigEntry - ) -> bool: + async def _async_powerwall_is_offline(self, entry: ConfigEntry) -> bool: """Check if the power wall is offline. We define offline by the config entry @@ -113,7 +116,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): or not async_last_update_was_successful(self.hass, entry) ) and not await _powerwall_is_reachable(ip_address, password) - async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: + async def async_step_dhcp( + self, discovery_info: dhcp.DhcpServiceInfo + ) -> ConfigFlowResult: """Handle dhcp discovery.""" self.ip_address = discovery_info.ip gateway_din = discovery_info.hostname.upper() @@ -180,7 +185,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_confirm_discovery( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm a discovered powerwall.""" assert self.ip_address is not None assert self.unique_id is not None @@ -209,7 +214,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors: dict[str, str] | None = {} description_placeholders: dict[str, str] = {} @@ -243,7 +248,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle reauth confirmation.""" assert self.reauth_entry is not None errors: dict[str, str] | None = {} @@ -265,7 +270,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): description_placeholders=description_placeholders, ) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Handle configuration by re-auth.""" self.reauth_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] @@ -273,5 +280,5 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_reauth_confirm() -class WrongVersion(exceptions.HomeAssistantError): +class WrongVersion(HomeAssistantError): """Error indicating we cannot interact with the powerwall software version.""" diff --git a/homeassistant/components/powerwall/const.py b/homeassistant/components/powerwall/const.py index c20ab760f23..bb3a6c2355e 100644 --- a/homeassistant/components/powerwall/const.py +++ b/homeassistant/components/powerwall/const.py @@ -1,4 +1,5 @@ """Constants for the Tesla Powerwall integration.""" + from typing import Final DOMAIN = "powerwall" diff --git a/homeassistant/components/powerwall/manifest.json b/homeassistant/components/powerwall/manifest.json index df57396c7bf..4185e90ab7b 100644 --- a/homeassistant/components/powerwall/manifest.json +++ b/homeassistant/components/powerwall/manifest.json @@ -12,7 +12,6 @@ } ], "documentation": "https://www.home-assistant.io/integrations/powerwall", - "import_executor": true, "iot_class": "local_polling", "loggers": ["tesla_powerwall"], "requirements": ["tesla-powerwall==0.5.1"] diff --git a/homeassistant/components/powerwall/models.py b/homeassistant/components/powerwall/models.py index 3216b83a7db..1adfd754650 100644 --- a/homeassistant/components/powerwall/models.py +++ b/homeassistant/components/powerwall/models.py @@ -1,4 +1,5 @@ """The powerwall integration models.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/homeassistant/components/powerwall/sensor.py b/homeassistant/components/powerwall/sensor.py index 9e17cd32e9c..38189ecd6f3 100644 --- a/homeassistant/components/powerwall/sensor.py +++ b/homeassistant/components/powerwall/sensor.py @@ -1,4 +1,5 @@ """Support for powerwall sensors.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/private_ble_device/__init__.py b/homeassistant/components/private_ble_device/__init__.py index dcb6555bbc9..ab4de9ef04d 100644 --- a/homeassistant/components/private_ble_device/__init__.py +++ b/homeassistant/components/private_ble_device/__init__.py @@ -1,4 +1,5 @@ """Private BLE Device integration.""" + from __future__ import annotations from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/private_ble_device/config_flow.py b/homeassistant/components/private_ble_device/config_flow.py index 4fec68e507e..c7311e8691b 100644 --- a/homeassistant/components/private_ble_device/config_flow.py +++ b/homeassistant/components/private_ble_device/config_flow.py @@ -1,4 +1,5 @@ """Config flow for the BLE Tracker.""" + from __future__ import annotations import base64 @@ -8,8 +9,7 @@ import logging import voluptuous as vol from homeassistant.components import bluetooth -from homeassistant.config_entries import ConfigFlow -from homeassistant.data_entry_flow import FlowResult +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from .const import DOMAIN from .coordinator import async_last_service_info @@ -50,7 +50,7 @@ class BLEDeviceTrackerConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, str] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Set up by user.""" errors: dict[str, str] = {} diff --git a/homeassistant/components/private_ble_device/const.py b/homeassistant/components/private_ble_device/const.py index 086fd06bfd5..4eaf862ca75 100644 --- a/homeassistant/components/private_ble_device/const.py +++ b/homeassistant/components/private_ble_device/const.py @@ -1,2 +1,3 @@ """Constants for Private BLE Device.""" + DOMAIN = "private_ble_device" diff --git a/homeassistant/components/private_ble_device/coordinator.py b/homeassistant/components/private_ble_device/coordinator.py index e41c3d02e9e..69db399a454 100644 --- a/homeassistant/components/private_ble_device/coordinator.py +++ b/homeassistant/components/private_ble_device/coordinator.py @@ -1,4 +1,5 @@ """Central manager for tracking devices with random but resolvable MAC addresses.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/private_ble_device/device_tracker.py b/homeassistant/components/private_ble_device/device_tracker.py index 64e23b25ebe..fbaf0d44751 100644 --- a/homeassistant/components/private_ble_device/device_tracker.py +++ b/homeassistant/components/private_ble_device/device_tracker.py @@ -1,4 +1,5 @@ """Tracking for bluetooth low energy devices.""" + from __future__ import annotations from collections.abc import Mapping @@ -31,6 +32,7 @@ class BasePrivateDeviceTracker(BasePrivateDeviceEntity, BaseTrackerEntity): _attr_should_poll = False _attr_has_entity_name = True + _attr_translation_key = "device_tracker" _attr_name = None @property @@ -68,8 +70,3 @@ class BasePrivateDeviceTracker(BasePrivateDeviceEntity, BaseTrackerEntity): def source_type(self) -> SourceType: """Return the source type, eg gps or router, of the device.""" return SourceType.BLUETOOTH_LE - - @property - def icon(self) -> str: - """Return device icon.""" - return "mdi:bluetooth-connect" if self._last_info else "mdi:bluetooth-off" diff --git a/homeassistant/components/private_ble_device/entity.py b/homeassistant/components/private_ble_device/entity.py index 978313e9671..2c574805c53 100644 --- a/homeassistant/components/private_ble_device/entity.py +++ b/homeassistant/components/private_ble_device/entity.py @@ -1,4 +1,5 @@ """Tracking for bluetooth low energy devices.""" + from __future__ import annotations from abc import abstractmethod diff --git a/homeassistant/components/private_ble_device/icons.json b/homeassistant/components/private_ble_device/icons.json new file mode 100644 index 00000000000..0bab138182d --- /dev/null +++ b/homeassistant/components/private_ble_device/icons.json @@ -0,0 +1,20 @@ +{ + "entity": { + "device_tracker": { + "device_tracker": { + "default": "mdi:bluetooth-off", + "state": { + "home": "mdi:bluetooth-connect" + } + } + }, + "sensor": { + "estimated_distance": { + "default": "mdi:signal-distance-variant" + }, + "estimated_broadcast_interval": { + "default": "mdi:timer-sync-outline" + } + } + } +} diff --git a/homeassistant/components/private_ble_device/sensor.py b/homeassistant/components/private_ble_device/sensor.py index fb094de3d58..e2c4fb0c7da 100644 --- a/homeassistant/components/private_ble_device/sensor.py +++ b/homeassistant/components/private_ble_device/sensor.py @@ -1,4 +1,5 @@ -"""Support for iBeacon device sensors.""" +"""Support for Private BLE Device sensors.""" + from __future__ import annotations from collections.abc import Callable @@ -26,22 +27,15 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .entity import BasePrivateDeviceEntity -@dataclass(frozen=True) -class PrivateDeviceSensorEntityDescriptionRequired: - """Required domain specific fields for sensor entity.""" +@dataclass(frozen=True, kw_only=True) +class PrivateDeviceSensorEntityDescription(SensorEntityDescription): + """Describes sensor entity.""" value_fn: Callable[ [HomeAssistant, bluetooth.BluetoothServiceInfoBleak], str | int | float | None ] -@dataclass(frozen=True) -class PrivateDeviceSensorEntityDescription( - SensorEntityDescription, PrivateDeviceSensorEntityDescriptionRequired -): - """Describes sensor entity.""" - - SENSOR_DESCRIPTIONS = ( PrivateDeviceSensorEntityDescription( key="rssi", @@ -65,7 +59,6 @@ SENSOR_DESCRIPTIONS = ( PrivateDeviceSensorEntityDescription( key="estimated_distance", translation_key="estimated_distance", - icon="mdi:signal-distance-variant", native_unit_of_measurement=UnitOfLength.METERS, value_fn=lambda _, service_info: service_info.advertisement and service_info.advertisement.tx_power @@ -79,7 +72,6 @@ SENSOR_DESCRIPTIONS = ( PrivateDeviceSensorEntityDescription( key="estimated_broadcast_interval", translation_key="estimated_broadcast_interval", - icon="mdi:timer-sync-outline", native_unit_of_measurement=UnitOfTime.SECONDS, entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, diff --git a/homeassistant/components/profiler/__init__.py b/homeassistant/components/profiler/__init__.py index 89240820057..30385a1c267 100644 --- a/homeassistant/components/profiler/__init__.py +++ b/homeassistant/components/profiler/__init__.py @@ -1,4 +1,5 @@ """The profiler integration.""" + import asyncio from contextlib import suppress from datetime import timedelta @@ -35,6 +36,7 @@ SERVICE_DUMP_LOG_OBJECTS = "dump_log_objects" SERVICE_LRU_STATS = "lru_stats" SERVICE_LOG_THREAD_FRAMES = "log_thread_frames" SERVICE_LOG_EVENT_LOOP_SCHEDULED = "log_event_loop_scheduled" +SERVICE_SET_ASYNCIO_DEBUG = "set_asyncio_debug" _LRU_CACHE_WRAPPER_OBJECT = _lru_cache_wrapper.__name__ _SQLALCHEMY_LRU_OBJECT = "LRUCache" @@ -56,12 +58,14 @@ SERVICES = ( SERVICE_LRU_STATS, SERVICE_LOG_THREAD_FRAMES, SERVICE_LOG_EVENT_LOOP_SCHEDULED, + SERVICE_SET_ASYNCIO_DEBUG, ) DEFAULT_SCAN_INTERVAL = timedelta(seconds=30) DEFAULT_MAX_OBJECTS = 5 +CONF_ENABLED = "enabled" CONF_SECONDS = "seconds" CONF_MAX_OBJECTS = "max_objects" @@ -253,6 +257,19 @@ async def async_setup_entry( # noqa: C901 arepr.maxstring = original_maxstring arepr.maxother = original_maxother + async def _async_asyncio_debug(call: ServiceCall) -> None: + """Enable or disable asyncio debug.""" + enabled = call.data[CONF_ENABLED] + # Always log this at critical level so we know when + # it's been changed when reviewing logs + _LOGGER.critical("Setting asyncio debug to %s", enabled) + # Make sure the logger is set to at least INFO or + # we won't see the messages + base_logger = logging.getLogger() + if enabled and base_logger.getEffectiveLevel() > logging.INFO: + base_logger.setLevel(logging.INFO) + hass.loop.set_debug(enabled) + async_register_admin_service( hass, DOMAIN, @@ -347,6 +364,14 @@ async def async_setup_entry( # noqa: C901 _async_dump_scheduled, ) + async_register_admin_service( + hass, + DOMAIN, + SERVICE_SET_ASYNCIO_DEBUG, + _async_asyncio_debug, + schema=vol.Schema({vol.Optional(CONF_ENABLED, default=True): cv.boolean}), + ) + return True diff --git a/homeassistant/components/profiler/config_flow.py b/homeassistant/components/profiler/config_flow.py index db3241bf1d3..4acce51e25f 100644 --- a/homeassistant/components/profiler/config_flow.py +++ b/homeassistant/components/profiler/config_flow.py @@ -1,10 +1,11 @@ """Config flow for Profiler integration.""" -from homeassistant import config_entries + +from homeassistant.config_entries import ConfigFlow from .const import DEFAULT_NAME, DOMAIN -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class ProfilerConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Profiler.""" VERSION = 1 diff --git a/homeassistant/components/profiler/icons.json b/homeassistant/components/profiler/icons.json new file mode 100644 index 00000000000..9a8c0e85f0d --- /dev/null +++ b/homeassistant/components/profiler/icons.json @@ -0,0 +1,15 @@ +{ + "services": { + "start": "mdi:play", + "memory": "mdi:memory", + "start_log_objects": "mdi:invoice-text-plus", + "stop_log_objects": "mdi:invoice-text-remove", + "dump_log_objects": "mdi:invoice-export-outline", + "start_log_object_sources": "mdi:play", + "stop_log_object_sources": "mdi:stop", + "lru_stats": "mdi:chart-areaspline", + "log_thread_frames": "mdi:format-list-bulleted", + "log_event_loop_scheduled": "mdi:calendar-clock", + "set_asyncio_debug": "mdi:bug-check" + } +} diff --git a/homeassistant/components/profiler/services.yaml b/homeassistant/components/profiler/services.yaml index 311325fa404..6842b2f45f2 100644 --- a/homeassistant/components/profiler/services.yaml +++ b/homeassistant/components/profiler/services.yaml @@ -53,3 +53,9 @@ stop_log_object_sources: lru_stats: log_thread_frames: log_event_loop_scheduled: +set_asyncio_debug: + fields: + enabled: + default: true + selector: + boolean: diff --git a/homeassistant/components/profiler/strings.json b/homeassistant/components/profiler/strings.json index a14324a9082..980550a1a4a 100644 --- a/homeassistant/components/profiler/strings.json +++ b/homeassistant/components/profiler/strings.json @@ -83,6 +83,16 @@ "log_event_loop_scheduled": { "name": "Log event loop scheduled", "description": "Logs what is scheduled in the event loop." + }, + "set_asyncio_debug": { + "name": "Set asyncio debug", + "description": "Enable or disable asyncio debug.", + "fields": { + "enabled": { + "name": "Enabled", + "description": "Whether to enable or disable asyncio debug." + } + } } } } diff --git a/homeassistant/components/progettihwsw/binary_sensor.py b/homeassistant/components/progettihwsw/binary_sensor.py index ea7a7dce5c3..a89b8b3c3f1 100644 --- a/homeassistant/components/progettihwsw/binary_sensor.py +++ b/homeassistant/components/progettihwsw/binary_sensor.py @@ -1,4 +1,5 @@ """Control binary sensor instances.""" + import asyncio from datetime import timedelta import logging @@ -28,7 +29,6 @@ async def async_setup_entry( """Set up the binary sensors from a config entry.""" board_api = hass.data[DOMAIN][config_entry.entry_id] input_count = config_entry.data["input_count"] - binary_sensors = [] async def async_update_data(): """Fetch data from API endpoint of board.""" @@ -44,16 +44,14 @@ async def async_setup_entry( ) await coordinator.async_refresh() - for i in range(1, int(input_count) + 1): - binary_sensors.append( - ProgettihwswBinarySensor( - coordinator, - f"Input #{i}", - setup_input(board_api, i), - ) + async_add_entities( + ProgettihwswBinarySensor( + coordinator, + f"Input #{i}", + setup_input(board_api, i), ) - - async_add_entities(binary_sensors) + for i in range(1, int(input_count) + 1) + ) class ProgettihwswBinarySensor(CoordinatorEntity, BinarySensorEntity): diff --git a/homeassistant/components/progettihwsw/config_flow.py b/homeassistant/components/progettihwsw/config_flow.py index 9b4fc16c8c4..5a5d0de1a80 100644 --- a/homeassistant/components/progettihwsw/config_flow.py +++ b/homeassistant/components/progettihwsw/config_flow.py @@ -3,7 +3,9 @@ from ProgettiHWSW.ProgettiHWSWAPI import ProgettiHWSWAPI import voluptuous as vol -from homeassistant import config_entries, core, exceptions +from homeassistant.config_entries import ConfigFlow +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from .const import DOMAIN @@ -12,7 +14,7 @@ DATA_SCHEMA = vol.Schema( ) -async def validate_input(hass: core.HomeAssistant, data): +async def validate_input(hass: HomeAssistant, data): """Validate the user host input.""" api_instance = ProgettiHWSWAPI(f'{data["host"]}:{data["port"]}') @@ -29,7 +31,7 @@ async def validate_input(hass: core.HomeAssistant, data): } -class ProgettiHWSWConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class ProgettiHWSWConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for ProgettiHWSW Automation.""" VERSION = 1 @@ -49,13 +51,13 @@ class ProgettiHWSWConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): relay_modes_schema = {} for i in range(1, int(self.s1_in["relay_count"]) + 1): - relay_modes_schema[ - vol.Required(f"relay_{str(i)}", default="bistable") - ] = vol.In( - { - "bistable": "Bistable (ON/OFF Mode)", - "monostable": "Monostable (Timer Mode)", - } + relay_modes_schema[vol.Required(f"relay_{str(i)}", default="bistable")] = ( + vol.In( + { + "bistable": "Bistable (ON/OFF Mode)", + "monostable": "Monostable (Timer Mode)", + } + ) ) return self.async_show_form( @@ -88,13 +90,13 @@ class ProgettiHWSWConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -class CannotConnect(exceptions.HomeAssistantError): +class CannotConnect(HomeAssistantError): """Error to indicate we cannot identify host.""" -class WrongInfo(exceptions.HomeAssistantError): +class WrongInfo(HomeAssistantError): """Error to indicate we cannot validate relay modes input.""" -class ExistingEntry(exceptions.HomeAssistantError): +class ExistingEntry(HomeAssistantError): """Error to indicate we cannot validate relay modes input.""" diff --git a/homeassistant/components/progettihwsw/switch.py b/homeassistant/components/progettihwsw/switch.py index f466e11a1cc..88faa35e0a4 100644 --- a/homeassistant/components/progettihwsw/switch.py +++ b/homeassistant/components/progettihwsw/switch.py @@ -1,4 +1,5 @@ """Control switches.""" + import asyncio from datetime import timedelta import logging @@ -29,7 +30,6 @@ async def async_setup_entry( """Set up the switches from a config entry.""" board_api = hass.data[DOMAIN][config_entry.entry_id] relay_count = config_entry.data["relay_count"] - switches = [] async def async_update_data(): """Fetch data from API endpoint of board.""" @@ -45,16 +45,14 @@ async def async_setup_entry( ) await coordinator.async_refresh() - for i in range(1, int(relay_count) + 1): - switches.append( - ProgettihwswSwitch( - coordinator, - f"Relay #{i}", - setup_switch(board_api, i, config_entry.data[f"relay_{str(i)}"]), - ) + async_add_entities( + ProgettihwswSwitch( + coordinator, + f"Relay #{i}", + setup_switch(board_api, i, config_entry.data[f"relay_{str(i)}"]), ) - - async_add_entities(switches) + for i in range(1, int(relay_count) + 1) + ) class ProgettihwswSwitch(CoordinatorEntity, SwitchEntity): diff --git a/homeassistant/components/proliphix/climate.py b/homeassistant/components/proliphix/climate.py index 797fd751197..23ccc03a038 100644 --- a/homeassistant/components/proliphix/climate.py +++ b/homeassistant/components/proliphix/climate.py @@ -1,4 +1,5 @@ """Support for Proliphix NT10e Thermostats.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/prometheus/__init__.py b/homeassistant/components/prometheus/__init__.py index 86163704797..d3a307a6616 100644 --- a/homeassistant/components/prometheus/__init__.py +++ b/homeassistant/components/prometheus/__init__.py @@ -1,4 +1,5 @@ """Support for Prometheus metrics export.""" + from __future__ import annotations from collections.abc import Callable @@ -48,7 +49,7 @@ from homeassistant.const import ( STATE_UNKNOWN, UnitOfTemperature, ) -from homeassistant.core import HomeAssistant, State +from homeassistant.core import Event, HomeAssistant, State from homeassistant.helpers import entityfilter, state as state_helper import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_registry import ( @@ -57,7 +58,7 @@ from homeassistant.helpers.entity_registry import ( ) from homeassistant.helpers.entity_values import EntityValues from homeassistant.helpers.event import EventStateChangedData -from homeassistant.helpers.typing import ConfigType, EventType +from homeassistant.helpers.typing import ConfigType from homeassistant.util.dt import as_timestamp from homeassistant.util.unit_conversion import TemperatureConverter @@ -131,10 +132,10 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: default_metric, ) - hass.bus.listen(EVENT_STATE_CHANGED, metrics.handle_state_changed_event) # type: ignore[arg-type] + hass.bus.listen(EVENT_STATE_CHANGED, metrics.handle_state_changed_event) hass.bus.listen( EVENT_ENTITY_REGISTRY_UPDATED, - metrics.handle_entity_registry_updated, # type: ignore[arg-type] + metrics.handle_entity_registry_updated, ) for state in hass.states.all(): @@ -179,9 +180,7 @@ class PrometheusMetrics: self._metrics: dict[str, MetricWrapperBase] = {} self._climate_units = climate_units - def handle_state_changed_event( - self, event: EventType[EventStateChangedData] - ) -> None: + def handle_state_changed_event(self, event: Event[EventStateChangedData]) -> None: """Handle new messages from the bus.""" if (state := event.data.get("new_state")) is None: return @@ -231,7 +230,7 @@ class PrometheusMetrics: last_updated_time_seconds.labels(**labels).set(state.last_updated.timestamp()) def handle_entity_registry_updated( - self, event: EventType[EventEntityRegistryUpdatedData] + self, event: Event[EventEntityRegistryUpdatedData] ) -> None: """Listen for deleted, disabled or renamed entities and remove them from the Prometheus Registry.""" if event.data["action"] in (None, "create"): @@ -259,7 +258,7 @@ class PrometheusMetrics: self, entity_id: str, friendly_name: str | None = None ) -> None: """Remove labelsets matching the given entity id from all metrics.""" - for _, metric in self._metrics.items(): + for metric in self._metrics.values(): for sample in cast(list[prometheus_client.Metric], metric.collect())[ 0 ].samples: diff --git a/homeassistant/components/prosegur/__init__.py b/homeassistant/components/prosegur/__init__.py index fd79a091e39..bf2aad451df 100644 --- a/homeassistant/components/prosegur/__init__.py +++ b/homeassistant/components/prosegur/__init__.py @@ -1,4 +1,5 @@ """The Prosegur Alarm integration.""" + import logging from pyprosegur.auth import Auth diff --git a/homeassistant/components/prosegur/alarm_control_panel.py b/homeassistant/components/prosegur/alarm_control_panel.py index 77cdb5e11a2..61e7c73e3a5 100644 --- a/homeassistant/components/prosegur/alarm_control_panel.py +++ b/homeassistant/components/prosegur/alarm_control_panel.py @@ -1,4 +1,5 @@ """Support for Prosegur alarm control panels.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/prosegur/camera.py b/homeassistant/components/prosegur/camera.py index c711ca2eac6..fd911fa5898 100644 --- a/homeassistant/components/prosegur/camera.py +++ b/homeassistant/components/prosegur/camera.py @@ -1,4 +1,5 @@ """Support for Prosegur cameras.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/prosegur/config_flow.py b/homeassistant/components/prosegur/config_flow.py index c28245a09ff..ca8e4db35cc 100644 --- a/homeassistant/components/prosegur/config_flow.py +++ b/homeassistant/components/prosegur/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Prosegur Alarm integration.""" + from collections.abc import Mapping import logging from typing import Any, cast @@ -7,10 +8,10 @@ from pyprosegur.auth import COUNTRY, Auth from pyprosegur.installation import Installation import voluptuous as vol -from homeassistant import config_entries, core, exceptions -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_COUNTRY, CONF_PASSWORD, CONF_USERNAME -from homeassistant.data_entry_flow import FlowResult +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import aiohttp_client, selector from .const import CONF_CONTRACT, DOMAIN @@ -28,7 +29,7 @@ STEP_USER_DATA_SCHEMA = vol.Schema( ) -async def validate_input(hass: core.HomeAssistant, data): +async def validate_input(hass: HomeAssistant, data): """Validate the user input allows us to connect.""" session = aiohttp_client.async_get_clientsession(hass) auth = Auth(session, data[CONF_USERNAME], data[CONF_PASSWORD], data[CONF_COUNTRY]) @@ -41,7 +42,7 @@ async def validate_input(hass: core.HomeAssistant, data): raise CannotConnect from ConnectionError -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class ProsegurConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Prosegur Alarm.""" VERSION = 1 @@ -74,7 +75,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_choose_contract( self, user_input: Any | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Let user decide which contract is being setup.""" if user_input: @@ -103,7 +104,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ), ) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Handle initiation of re-authentication with Prosegur.""" self.entry = cast( ConfigEntry, @@ -155,9 +158,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -class CannotConnect(exceptions.HomeAssistantError): +class CannotConnect(HomeAssistantError): """Error to indicate we cannot connect.""" -class InvalidAuth(exceptions.HomeAssistantError): +class InvalidAuth(HomeAssistantError): """Error to indicate there is invalid auth.""" diff --git a/homeassistant/components/prosegur/diagnostics.py b/homeassistant/components/prosegur/diagnostics.py index 59b51f5b5d1..ec13f5511a4 100644 --- a/homeassistant/components/prosegur/diagnostics.py +++ b/homeassistant/components/prosegur/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for Prosegur.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/prosegur/icons.json b/homeassistant/components/prosegur/icons.json new file mode 100644 index 00000000000..33cddefdaea --- /dev/null +++ b/homeassistant/components/prosegur/icons.json @@ -0,0 +1,5 @@ +{ + "services": { + "request_image": "mdi:image-sync" + } +} diff --git a/homeassistant/components/prowl/notify.py b/homeassistant/components/prowl/notify.py index c365ce151ec..b5556c15c6c 100644 --- a/homeassistant/components/prowl/notify.py +++ b/homeassistant/components/prowl/notify.py @@ -1,4 +1,5 @@ """Prowl notification service.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/proximity/__init__.py b/homeassistant/components/proximity/__init__.py index 349658223f3..f6c67fc088f 100644 --- a/homeassistant/components/proximity/__init__.py +++ b/homeassistant/components/proximity/__init__.py @@ -1,4 +1,5 @@ """Support for tracking the proximity of a device.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/proximity/config_flow.py b/homeassistant/components/proximity/config_flow.py index 1a549d22f81..14f26f5d45d 100644 --- a/homeassistant/components/proximity/config_flow.py +++ b/homeassistant/components/proximity/config_flow.py @@ -1,4 +1,5 @@ """Config flow for proximity.""" + from __future__ import annotations from typing import Any, cast @@ -8,10 +9,14 @@ import voluptuous as vol from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER_DOMAIN from homeassistant.components.person import DOMAIN as PERSON_DOMAIN from homeassistant.components.zone import DOMAIN as ZONE_DOMAIN -from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.const import CONF_ZONE, UnitOfLength from homeassistant.core import State, callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.selector import ( EntitySelector, EntitySelectorConfig, @@ -87,7 +92,7 @@ class ProximityConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" if user_input is not None: self._async_abort_entries_match(user_input) @@ -113,7 +118,7 @@ class ProximityConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_import( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Import a yaml config entry.""" return await self.async_step_user(user_input) @@ -130,7 +135,7 @@ class ProximityOptionsFlow(OptionsFlow): async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle options flow.""" if user_input is not None: self.hass.config_entries.async_update_entry( diff --git a/homeassistant/components/proximity/coordinator.py b/homeassistant/components/proximity/coordinator.py index 047ab1b6b3a..ea33c1f8121 100644 --- a/homeassistant/components/proximity/coordinator.py +++ b/homeassistant/components/proximity/coordinator.py @@ -15,10 +15,10 @@ from homeassistant.const import ( CONF_ZONE, UnitOfLength, ) -from homeassistant.core import HomeAssistant, State, callback +from homeassistant.core import Event, HomeAssistant, State, callback from homeassistant.helpers import entity_registry as er from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue -from homeassistant.helpers.typing import ConfigType, EventType +from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.util.location import distance from homeassistant.util.unit_conversion import DistanceConverter @@ -107,7 +107,7 @@ class ProximityDataUpdateCoordinator(DataUpdateCoordinator[ProximityData]): await self.async_refresh() async def async_check_tracked_entity_change( - self, event: EventType[er.EventEntityRegistryUpdatedData] + self, event: Event[er.EventEntityRegistryUpdatedData] ) -> None: """Fetch and process tracked entity change event.""" data = event.data @@ -124,8 +124,10 @@ class ProximityDataUpdateCoordinator(DataUpdateCoordinator[ProximityData]): **self.config_entry.data, CONF_TRACKED_ENTITIES: [ tracked_entity - for tracked_entity in self.tracked_entities - + [new_tracked_entity_id] + for tracked_entity in ( + *self.tracked_entities, + new_tracked_entity_id, + ) if tracked_entity != old_tracked_entity_id ], }, @@ -292,15 +294,15 @@ class ProximityDataUpdateCoordinator(DataUpdateCoordinator[ProximityData]): old_lat = None old_lon = None - entities_data[state_change_data.entity_id][ - ATTR_DIR_OF_TRAVEL - ] = self._calc_direction_of_travel( - zone_state, - new_state, - old_lat, - old_lon, - new_state.attributes.get(ATTR_LATITUDE), - new_state.attributes.get(ATTR_LONGITUDE), + entities_data[state_change_data.entity_id][ATTR_DIR_OF_TRAVEL] = ( + self._calc_direction_of_travel( + zone_state, + new_state, + old_lat, + old_lon, + new_state.attributes.get(ATTR_LATITUDE), + new_state.attributes.get(ATTR_LONGITUDE), + ) ) # takeover data for legacy proximity entity @@ -335,9 +337,9 @@ class ProximityDataUpdateCoordinator(DataUpdateCoordinator[ProximityData]): if cast(int, nearest_distance_to) == int(distance_to): _LOGGER.debug("set equally close entity_data: %s", entity_data) - proximity_data[ - ATTR_NEAREST - ] = f"{proximity_data[ATTR_NEAREST]}, {str(entity_data[ATTR_NAME])}" + proximity_data[ATTR_NEAREST] = ( + f"{proximity_data[ATTR_NEAREST]}, {str(entity_data[ATTR_NAME])}" + ) return ProximityData(proximity_data, entities_data) diff --git a/homeassistant/components/proximity/diagnostics.py b/homeassistant/components/proximity/diagnostics.py index 3ccecbe1f19..d296c489e94 100644 --- a/homeassistant/components/proximity/diagnostics.py +++ b/homeassistant/components/proximity/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for Proximity.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/proximity/helpers.py b/homeassistant/components/proximity/helpers.py index 9c0787538e5..af3d6d2a3bb 100644 --- a/homeassistant/components/proximity/helpers.py +++ b/homeassistant/components/proximity/helpers.py @@ -1,4 +1,5 @@ """Helper functions for proximity.""" + from homeassistant.components.automation import automations_with_entity from homeassistant.components.script import scripts_with_entity from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/proxmoxve/__init__.py b/homeassistant/components/proxmoxve/__init__.py index 1c22ca50c23..e0b8f91088d 100644 --- a/homeassistant/components/proxmoxve/__init__.py +++ b/homeassistant/components/proxmoxve/__init__.py @@ -1,4 +1,5 @@ """Support for Proxmox VE.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/proxmoxve/binary_sensor.py b/homeassistant/components/proxmoxve/binary_sensor.py index ea02e547e98..412f40af6e8 100644 --- a/homeassistant/components/proxmoxve/binary_sensor.py +++ b/homeassistant/components/proxmoxve/binary_sensor.py @@ -1,4 +1,5 @@ """Binary sensor to read Proxmox VE data.""" + from __future__ import annotations from homeassistant.components.binary_sensor import ( diff --git a/homeassistant/components/proxy/camera.py b/homeassistant/components/proxy/camera.py index 79e9faf8d70..3a93c7a2d36 100644 --- a/homeassistant/components/proxy/camera.py +++ b/homeassistant/components/proxy/camera.py @@ -1,4 +1,5 @@ """Proxy camera platform that enables image processing of camera data.""" + from __future__ import annotations import asyncio @@ -77,16 +78,16 @@ async def async_setup_platform( def _precheck_image(image, opts): """Perform some pre-checks on the given image.""" if not opts: - raise ValueError() + raise ValueError try: img = Image.open(io.BytesIO(image)) except OSError as err: _LOGGER.warning("Failed to open image") - raise ValueError() from err + raise ValueError from err imgfmt = str(img.format) if imgfmt not in ("PNG", "JPEG"): _LOGGER.warning("Image is of unsupported type: %s", imgfmt) - raise ValueError() + raise ValueError if img.mode != "RGB": img = img.convert("RGB") return img @@ -291,7 +292,7 @@ class ProxyCamera(Camera): if not image: return None except HomeAssistantError as err: - raise asyncio.CancelledError() from err + raise asyncio.CancelledError from err if self._mode == MODE_RESIZE: job = _resize_image diff --git a/homeassistant/components/prusalink/__init__.py b/homeassistant/components/prusalink/__init__.py index 08670ef5433..2ff4601466c 100644 --- a/homeassistant/components/prusalink/__init__.py +++ b/homeassistant/components/prusalink/__init__.py @@ -1,4 +1,5 @@ """The PrusaLink integration.""" + from __future__ import annotations from abc import ABC, abstractmethod @@ -22,8 +23,8 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryError from homeassistant.helpers import issue_registry as ir -from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.httpx_client import get_async_client from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -43,7 +44,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise ConfigEntryError("Please upgrade your printer's firmware.") api = PrusaLink( - async_get_clientsession(hass), + get_async_client(hass), entry.data[CONF_HOST], entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD], @@ -80,7 +81,7 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> password = config_entry.data[CONF_API_KEY] api = PrusaLink( - async_get_clientsession(hass), + get_async_client(hass), config_entry.data[CONF_HOST], username, password, diff --git a/homeassistant/components/prusalink/button.py b/homeassistant/components/prusalink/button.py index 8f8a62794a9..d70356f04d1 100644 --- a/homeassistant/components/prusalink/button.py +++ b/homeassistant/components/prusalink/button.py @@ -1,4 +1,5 @@ """PrusaLink sensors.""" + from __future__ import annotations from collections.abc import Callable, Coroutine @@ -40,7 +41,6 @@ BUTTONS: dict[str, tuple[PrusaLinkButtonEntityDescription, ...]] = { PrusaLinkButtonEntityDescription[PrinterStatus]( key="printer.cancel_job", translation_key="cancel_job", - icon="mdi:cancel", press_fn=lambda api: api.cancel_job, available_fn=lambda data: ( data["printer"]["state"] @@ -50,7 +50,6 @@ BUTTONS: dict[str, tuple[PrusaLinkButtonEntityDescription, ...]] = { PrusaLinkButtonEntityDescription[PrinterStatus]( key="job.pause_job", translation_key="pause_job", - icon="mdi:pause", press_fn=lambda api: api.pause_job, available_fn=lambda data: cast( bool, data["printer"]["state"] == PrinterState.PRINTING.value @@ -59,7 +58,6 @@ BUTTONS: dict[str, tuple[PrusaLinkButtonEntityDescription, ...]] = { PrusaLinkButtonEntityDescription[PrinterStatus]( key="job.resume_job", translation_key="resume_job", - icon="mdi:play", press_fn=lambda api: api.resume_job, available_fn=lambda data: cast( bool, data["printer"]["state"] == PrinterState.PAUSED.value diff --git a/homeassistant/components/prusalink/camera.py b/homeassistant/components/prusalink/camera.py index 7f6fab0583b..cc625b7ef57 100644 --- a/homeassistant/components/prusalink/camera.py +++ b/homeassistant/components/prusalink/camera.py @@ -1,6 +1,9 @@ """Camera entity for PrusaLink.""" + from __future__ import annotations +from pyprusalink.types import PrinterState + from homeassistant.components.camera import Camera from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -37,6 +40,7 @@ class PrusaLinkJobPreviewEntity(PrusaLinkEntity, Camera): """Get if camera is available.""" return ( super().available + and self.coordinator.data.get("state") != PrinterState.IDLE.value and (file := self.coordinator.data.get("file")) and file.get("refs", {}).get("thumbnail") ) diff --git a/homeassistant/components/prusalink/config_flow.py b/homeassistant/components/prusalink/config_flow.py index e4e9b9d719c..b0c7cf2f756 100644 --- a/homeassistant/components/prusalink/config_flow.py +++ b/homeassistant/components/prusalink/config_flow.py @@ -1,22 +1,22 @@ """Config flow for PrusaLink integration.""" + from __future__ import annotations import asyncio import logging from typing import Any -from aiohttp import ClientError from awesomeversion import AwesomeVersion, AwesomeVersionException +from httpx import HTTPError, InvalidURL from pyprusalink import PrusaLink -from pyprusalink.types import InvalidAuth +from pyprusalink.types import InvalidAuth, VersionInfo import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.httpx_client import get_async_client from .const import DOMAIN @@ -34,13 +34,33 @@ STEP_USER_DATA_SCHEMA = vol.Schema( ) +def ensure_printer_is_supported(version: VersionInfo) -> None: + """Raise NotSupported exception if the printer is not supported.""" + + try: + if AwesomeVersion("2.0.0") <= AwesomeVersion(version["api"]): + return + + # Workaround to allow PrusaLink 0.7.2 on MK3 and MK2.5 that supports + # the 2.0.0 API, but doesn't advertise it yet + if version.get("original", "").startswith( + ("PrusaLink I3MK3", "PrusaLink I3MK2") + ) and AwesomeVersion("0.7.2") <= AwesomeVersion(version["server"]): + return + + except AwesomeVersionException as err: + raise NotSupported from err + + raise NotSupported + + async def validate_input(hass: HomeAssistant, data: dict[str, str]) -> dict[str, str]: """Validate the user input allows us to connect. Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user. """ api = PrusaLink( - async_get_clientsession(hass), + get_async_client(hass), data[CONF_HOST], data[CONF_USERNAME], data[CONF_PASSWORD], @@ -50,20 +70,16 @@ async def validate_input(hass: HomeAssistant, data: dict[str, str]) -> dict[str, async with asyncio.timeout(5): version = await api.get_version() - except (TimeoutError, ClientError) as err: + except (TimeoutError, HTTPError, InvalidURL) as err: _LOGGER.error("Could not connect to PrusaLink: %s", err) raise CannotConnect from err - try: - if AwesomeVersion(version["api"]) < AwesomeVersion("2.0.0"): - raise NotSupported - except AwesomeVersionException as err: - raise NotSupported from err + ensure_printer_is_supported(version) return {"title": version["hostname"] or version["text"]} -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class PrusaLinkConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for PrusaLink.""" VERSION = 1 @@ -71,7 +87,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" if user_input is None: return self.async_show_form( diff --git a/homeassistant/components/prusalink/icons.json b/homeassistant/components/prusalink/icons.json new file mode 100644 index 00000000000..4d97ea76ddd --- /dev/null +++ b/homeassistant/components/prusalink/icons.json @@ -0,0 +1,35 @@ +{ + "entity": { + "button": { + "cancel_job": { + "default": "mdi:cancel" + }, + "pause_job": { + "default": "mdi:pause" + }, + "resume_job": { + "default": "mdi:play" + } + }, + "sensor": { + "printer_state": { + "default": "mdi:printer-3d" + }, + "material": { + "default": "mdi:palette-swatch-variant" + }, + "progress": { + "default": "mdi:progress-clock" + }, + "filename": { + "default": "mdi:file-image-outline" + }, + "print_start": { + "default": "mdi:clock-start" + }, + "print_finish": { + "default": "mdi:clock-end" + } + } + } +} diff --git a/homeassistant/components/prusalink/manifest.json b/homeassistant/components/prusalink/manifest.json index a9d8353690e..6c64419debb 100644 --- a/homeassistant/components/prusalink/manifest.json +++ b/homeassistant/components/prusalink/manifest.json @@ -10,5 +10,5 @@ ], "documentation": "https://www.home-assistant.io/integrations/prusalink", "iot_class": "local_polling", - "requirements": ["pyprusalink==2.0.0"] + "requirements": ["pyprusalink==2.1.1"] } diff --git a/homeassistant/components/prusalink/sensor.py b/homeassistant/components/prusalink/sensor.py index 29e1d5c9757..604b029fc92 100644 --- a/homeassistant/components/prusalink/sensor.py +++ b/homeassistant/components/prusalink/sensor.py @@ -1,4 +1,5 @@ """PrusaLink sensors.""" + from __future__ import annotations from collections.abc import Callable @@ -54,7 +55,6 @@ SENSORS: dict[str, tuple[PrusaLinkSensorEntityDescription, ...]] = { PrusaLinkSensorEntityDescription[PrinterStatus]( key="printer.state", name=None, - icon="mdi:printer-3d", value_fn=lambda data: (cast(str, data["printer"]["state"].lower())), device_class=SensorDeviceClass.ENUM, options=[state.value.lower() for state in PrinterState], @@ -137,7 +137,6 @@ SENSORS: dict[str, tuple[PrusaLinkSensorEntityDescription, ...]] = { PrusaLinkSensorEntityDescription[LegacyPrinterStatus]( key="printer.telemetry.material", translation_key="material", - icon="mdi:palette-swatch-variant", value_fn=lambda data: cast(str, data["telemetry"]["material"]), ), ), @@ -145,39 +144,39 @@ SENSORS: dict[str, tuple[PrusaLinkSensorEntityDescription, ...]] = { PrusaLinkSensorEntityDescription[JobInfo]( key="job.progress", translation_key="progress", - icon="mdi:progress-clock", native_unit_of_measurement=PERCENTAGE, value_fn=lambda data: cast(float, data["progress"]), - available_fn=lambda data: data.get("progress") is not None, + available_fn=lambda data: data.get("progress") is not None + and data.get("state") != PrinterState.IDLE.value, ), PrusaLinkSensorEntityDescription[JobInfo]( key="job.filename", translation_key="filename", - icon="mdi:file-image-outline", value_fn=lambda data: cast(str, data["file"]["display_name"]), - available_fn=lambda data: data.get("file") is not None, + available_fn=lambda data: data.get("file") is not None + and data.get("state") != PrinterState.IDLE.value, ), PrusaLinkSensorEntityDescription[JobInfo]( key="job.start", translation_key="print_start", device_class=SensorDeviceClass.TIMESTAMP, - icon="mdi:clock-start", value_fn=ignore_variance( lambda data: (utcnow() - timedelta(seconds=data["time_printing"])), timedelta(minutes=2), ), - available_fn=lambda data: data.get("time_printing") is not None, + available_fn=lambda data: data.get("time_printing") is not None + and data.get("state") != PrinterState.IDLE.value, ), PrusaLinkSensorEntityDescription[JobInfo]( key="job.finish", translation_key="print_finish", - icon="mdi:clock-end", device_class=SensorDeviceClass.TIMESTAMP, value_fn=ignore_variance( lambda data: (utcnow() + timedelta(seconds=data["time_remaining"])), timedelta(minutes=2), ), - available_fn=lambda data: data.get("time_remaining") is not None, + available_fn=lambda data: data.get("time_remaining") is not None + and data.get("state") != PrinterState.IDLE.value, ), ), } diff --git a/homeassistant/components/ps4/__init__.py b/homeassistant/components/ps4/__init__.py index f68ad6ce896..3e92861b963 100644 --- a/homeassistant/components/ps4/__init__.py +++ b/homeassistant/components/ps4/__init__.py @@ -1,4 +1,5 @@ """Support for PlayStation 4 consoles.""" + import logging import os diff --git a/homeassistant/components/ps4/config_flow.py b/homeassistant/components/ps4/config_flow.py index 0865d09a460..b842c2f7cfb 100644 --- a/homeassistant/components/ps4/config_flow.py +++ b/homeassistant/components/ps4/config_flow.py @@ -1,4 +1,5 @@ """Config Flow for PlayStation 4.""" + from collections import OrderedDict from pyps4_2ndscreen.errors import CredentialTimeout @@ -6,7 +7,7 @@ from pyps4_2ndscreen.helpers import Helper from pyps4_2ndscreen.media_art import COUNTRIES import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow from homeassistant.const import ( CONF_CODE, CONF_HOST, @@ -38,7 +39,7 @@ PORT_MSG = {UDP_PORT: "port_987_bind_error", TCP_PORT: "port_997_bind_error"} PIN_LENGTH = 8 -class PlayStation4FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class PlayStation4FlowHandler(ConfigFlow, domain=DOMAIN): """Handle a PlayStation 4 config flow.""" VERSION = CONFIG_ENTRY_VERSION diff --git a/homeassistant/components/ps4/const.py b/homeassistant/components/ps4/const.py index 20b4feda5fa..bd1144c4d98 100644 --- a/homeassistant/components/ps4/const.py +++ b/homeassistant/components/ps4/const.py @@ -1,4 +1,5 @@ """Constants for PlayStation 4.""" + ATTR_MEDIA_IMAGE_URL = "media_image_url" CONFIG_ENTRY_VERSION = 3 DEFAULT_NAME = "PlayStation 4" diff --git a/homeassistant/components/ps4/icons.json b/homeassistant/components/ps4/icons.json new file mode 100644 index 00000000000..8da5909213b --- /dev/null +++ b/homeassistant/components/ps4/icons.json @@ -0,0 +1,12 @@ +{ + "entity": { + "media_player": { + "media_player": { + "default": "mdi:sony-playstation" + } + } + }, + "services": { + "send_command": "mdi:console" + } +} diff --git a/homeassistant/components/ps4/media_player.py b/homeassistant/components/ps4/media_player.py index 42a1021afe4..f01bc00ba72 100644 --- a/homeassistant/components/ps4/media_player.py +++ b/homeassistant/components/ps4/media_player.py @@ -1,4 +1,5 @@ """Support for PlayStation 4 consoles.""" + from contextlib import suppress import logging from typing import Any, cast @@ -41,8 +42,6 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -ICON = "mdi:sony-playstation" - DEFAULT_RETRIES = 2 @@ -67,7 +66,6 @@ async def async_setup_entry( class PS4Device(MediaPlayerEntity): """Representation of a PS4.""" - _attr_icon = ICON _attr_supported_features = ( MediaPlayerEntityFeature.TURN_OFF | MediaPlayerEntityFeature.TURN_ON @@ -75,6 +73,7 @@ class PS4Device(MediaPlayerEntity): | MediaPlayerEntityFeature.STOP | MediaPlayerEntityFeature.SELECT_SOURCE ) + _attr_translation_key = "media_player" def __init__( self, diff --git a/homeassistant/components/pulseaudio_loopback/switch.py b/homeassistant/components/pulseaudio_loopback/switch.py index 92dfb235b1b..553a1b4a283 100644 --- a/homeassistant/components/pulseaudio_loopback/switch.py +++ b/homeassistant/components/pulseaudio_loopback/switch.py @@ -1,4 +1,5 @@ """Switch logic for loading/unloading pulseaudio loopback modules.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/pure_energie/__init__.py b/homeassistant/components/pure_energie/__init__.py index cda73a7da0b..e018648e95e 100644 --- a/homeassistant/components/pure_energie/__init__.py +++ b/homeassistant/components/pure_energie/__init__.py @@ -1,4 +1,5 @@ """The Pure Energie integration.""" + from __future__ import annotations from typing import NamedTuple diff --git a/homeassistant/components/pure_energie/config_flow.py b/homeassistant/components/pure_energie/config_flow.py index 9e6c510c8b6..a2bbb671ff7 100644 --- a/homeassistant/components/pure_energie/config_flow.py +++ b/homeassistant/components/pure_energie/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Pure Energie integration.""" + from __future__ import annotations from typing import Any @@ -7,9 +8,8 @@ from gridnet import Device, GridNet, GridNetConnectionError import voluptuous as vol from homeassistant.components import zeroconf -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_NAME -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.selector import TextSelector @@ -25,7 +25,7 @@ class PureEnergieFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" errors = {} @@ -59,7 +59,7 @@ class PureEnergieFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_zeroconf( self, discovery_info: zeroconf.ZeroconfServiceInfo - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle zeroconf discovery.""" self.discovered_host = discovery_info.host try: @@ -83,7 +83,7 @@ class PureEnergieFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_zeroconf_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initiated by zeroconf.""" if user_input is not None: return self.async_create_entry( diff --git a/homeassistant/components/pure_energie/const.py b/homeassistant/components/pure_energie/const.py index 9c908da6068..bba7708c174 100644 --- a/homeassistant/components/pure_energie/const.py +++ b/homeassistant/components/pure_energie/const.py @@ -1,4 +1,5 @@ """Constants for the Pure Energie integration.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/pure_energie/diagnostics.py b/homeassistant/components/pure_energie/diagnostics.py index b2e071d58cd..fb93b81a4fd 100644 --- a/homeassistant/components/pure_energie/diagnostics.py +++ b/homeassistant/components/pure_energie/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for Pure Energie.""" + from __future__ import annotations from dataclasses import asdict diff --git a/homeassistant/components/pure_energie/sensor.py b/homeassistant/components/pure_energie/sensor.py index 09470609c9e..7f2c36bc4f6 100644 --- a/homeassistant/components/pure_energie/sensor.py +++ b/homeassistant/components/pure_energie/sensor.py @@ -1,4 +1,5 @@ """Support for Pure Energie sensors.""" + from __future__ import annotations from collections.abc import Callable @@ -22,20 +23,13 @@ from . import PureEnergieData, PureEnergieDataUpdateCoordinator from .const import DOMAIN -@dataclass(frozen=True) -class PureEnergieSensorEntityDescriptionMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class PureEnergieSensorEntityDescription(SensorEntityDescription): + """Describes a Pure Energie sensor entity.""" value_fn: Callable[[PureEnergieData], int | float] -@dataclass(frozen=True) -class PureEnergieSensorEntityDescription( - SensorEntityDescription, PureEnergieSensorEntityDescriptionMixin -): - """Describes a Pure Energie sensor entity.""" - - SENSORS: tuple[PureEnergieSensorEntityDescription, ...] = ( PureEnergieSensorEntityDescription( key="power_flow", diff --git a/homeassistant/components/purpleair/__init__.py b/homeassistant/components/purpleair/__init__.py index f52d0799d35..fb86612597a 100644 --- a/homeassistant/components/purpleair/__init__.py +++ b/homeassistant/components/purpleair/__init__.py @@ -1,4 +1,5 @@ """The PurpleAir integration.""" + from __future__ import annotations from collections.abc import Mapping diff --git a/homeassistant/components/purpleair/config_flow.py b/homeassistant/components/purpleair/config_flow.py index e2b43726dc4..f9a6415bb38 100644 --- a/homeassistant/components/purpleair/config_flow.py +++ b/homeassistant/components/purpleair/config_flow.py @@ -1,4 +1,5 @@ """Config flow for PurpleAir integration.""" + from __future__ import annotations import asyncio @@ -12,16 +13,19 @@ from aiopurpleair.endpoints.sensors import NearbySensorResult from aiopurpleair.errors import InvalidApiKeyError, PurpleAirError import voluptuous as vol -from homeassistant import config_entries -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.const import ( CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_SHOW_ON_MAP, ) -from homeassistant.core import HomeAssistant, callback -from homeassistant.data_entry_flow import FlowResult +from homeassistant.core import Event, HomeAssistant, callback from homeassistant.helpers import ( aiohttp_client, config_validation as cv, @@ -38,7 +42,6 @@ from homeassistant.helpers.selector import ( SelectSelectorConfig, SelectSelectorMode, ) -from homeassistant.helpers.typing import EventType from .const import CONF_SENSOR_INDICES, DOMAIN, LOGGER @@ -193,7 +196,7 @@ async def async_validate_coordinates( return ValidationResult(data=nearby_sensor_results) -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class PurpleAirConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for PurpleAir.""" VERSION = 1 @@ -213,7 +216,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_by_coordinates( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the discovery of sensors near a latitude/longitude.""" if user_input is None: return self.async_show_form( @@ -243,7 +246,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_choose_sensor( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the selection of a sensor.""" if user_input is None: options = self._flow_data.pop(CONF_NEARBY_SENSOR_OPTIONS) @@ -260,7 +263,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): options={CONF_SENSOR_INDICES: [int(user_input[CONF_SENSOR_INDEX])]}, ) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Handle configuration by re-auth.""" self._reauth_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] @@ -269,7 +274,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the re-auth step.""" if user_input is None: return self.async_show_form( @@ -298,7 +303,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" if user_input is None: return self.async_show_form(step_id="user", data_schema=API_KEY_SCHEMA) @@ -319,7 +324,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_by_coordinates() -class PurpleAirOptionsFlowHandler(config_entries.OptionsFlow): +class PurpleAirOptionsFlowHandler(OptionsFlow): """Handle a PurpleAir options flow.""" def __init__(self, config_entry: ConfigEntry) -> None: @@ -345,7 +350,7 @@ class PurpleAirOptionsFlowHandler(config_entries.OptionsFlow): async def async_step_add_sensor( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Add a sensor.""" if user_input is None: return self.async_show_form( @@ -376,7 +381,7 @@ class PurpleAirOptionsFlowHandler(config_entries.OptionsFlow): async def async_step_choose_sensor( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Choose a sensor.""" if user_input is None: options = self._flow_data.pop(CONF_NEARBY_SENSOR_OPTIONS) @@ -396,7 +401,7 @@ class PurpleAirOptionsFlowHandler(config_entries.OptionsFlow): async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Manage the options.""" return self.async_show_menu( step_id="init", @@ -405,7 +410,7 @@ class PurpleAirOptionsFlowHandler(config_entries.OptionsFlow): async def async_step_remove_sensor( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Remove a sensor.""" if user_input is None: return self.async_show_form( @@ -430,7 +435,7 @@ class PurpleAirOptionsFlowHandler(config_entries.OptionsFlow): @callback def async_device_entity_state_changed( - _: EventType[EventStateChangedData], + _: Event[EventStateChangedData], ) -> None: """Listen and respond when all device entities are removed.""" if all( @@ -467,7 +472,7 @@ class PurpleAirOptionsFlowHandler(config_entries.OptionsFlow): async def async_step_settings( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Manage settings.""" if user_input is None: return self.async_show_form( diff --git a/homeassistant/components/purpleair/const.py b/homeassistant/components/purpleair/const.py index 60f51a9e7dd..5f1ec84d469 100644 --- a/homeassistant/components/purpleair/const.py +++ b/homeassistant/components/purpleair/const.py @@ -1,4 +1,5 @@ """Constants for the PurpleAir integration.""" + import logging DOMAIN = "purpleair" diff --git a/homeassistant/components/purpleair/coordinator.py b/homeassistant/components/purpleair/coordinator.py index 9982db667f2..7bf0770c6fc 100644 --- a/homeassistant/components/purpleair/coordinator.py +++ b/homeassistant/components/purpleair/coordinator.py @@ -1,4 +1,5 @@ """Define a PurpleAir DataUpdateCoordinator.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/purpleair/diagnostics.py b/homeassistant/components/purpleair/diagnostics.py index 0c52879b7a7..a3b3af857fb 100644 --- a/homeassistant/components/purpleair/diagnostics.py +++ b/homeassistant/components/purpleair/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for PurpleAir.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/purpleair/icons.json b/homeassistant/components/purpleair/icons.json new file mode 100644 index 00000000000..683d5b31b14 --- /dev/null +++ b/homeassistant/components/purpleair/icons.json @@ -0,0 +1,24 @@ +{ + "entity": { + "sensor": { + "pm0_3_count_concentration": { + "default": "mdi:blur" + }, + "pm0_5_count_concentration": { + "default": "mdi:blur" + }, + "pm1_0_count_concentration": { + "default": "mdi:blur" + }, + "pm10_0_count_concentration": { + "default": "mdi:blur" + }, + "pm5_0_count_concentration": { + "default": "mdi:blur" + }, + "pm2_5_count_concentration": { + "default": "mdi:blur" + } + } + } +} diff --git a/homeassistant/components/purpleair/sensor.py b/homeassistant/components/purpleair/sensor.py index 50dbb47a285..d1db77c2c31 100644 --- a/homeassistant/components/purpleair/sensor.py +++ b/homeassistant/components/purpleair/sensor.py @@ -1,4 +1,5 @@ """Support for PurpleAir sensors.""" + from __future__ import annotations from collections.abc import Callable @@ -52,7 +53,6 @@ SENSOR_DESCRIPTIONS = [ key="pm0.3_count_concentration", translation_key="pm0_3_count_concentration", entity_registry_enabled_default=False, - icon="mdi:blur", native_unit_of_measurement=CONCENTRATION_PARTICLES_PER_100_MILLILITERS, state_class=SensorStateClass.MEASUREMENT, value_fn=lambda sensor: sensor.pm0_3_um_count, @@ -61,7 +61,6 @@ SENSOR_DESCRIPTIONS = [ key="pm0.5_count_concentration", translation_key="pm0_5_count_concentration", entity_registry_enabled_default=False, - icon="mdi:blur", native_unit_of_measurement=CONCENTRATION_PARTICLES_PER_100_MILLILITERS, state_class=SensorStateClass.MEASUREMENT, value_fn=lambda sensor: sensor.pm0_5_um_count, @@ -70,7 +69,6 @@ SENSOR_DESCRIPTIONS = [ key="pm1.0_count_concentration", translation_key="pm1_0_count_concentration", entity_registry_enabled_default=False, - icon="mdi:blur", native_unit_of_measurement=CONCENTRATION_PARTICLES_PER_100_MILLILITERS, state_class=SensorStateClass.MEASUREMENT, value_fn=lambda sensor: sensor.pm1_0_um_count, @@ -86,7 +84,6 @@ SENSOR_DESCRIPTIONS = [ key="pm10.0_count_concentration", translation_key="pm10_0_count_concentration", entity_registry_enabled_default=False, - icon="mdi:blur", native_unit_of_measurement=CONCENTRATION_PARTICLES_PER_100_MILLILITERS, state_class=SensorStateClass.MEASUREMENT, value_fn=lambda sensor: sensor.pm10_0_um_count, @@ -102,7 +99,6 @@ SENSOR_DESCRIPTIONS = [ key="pm2.5_count_concentration", translation_key="pm2_5_count_concentration", entity_registry_enabled_default=False, - icon="mdi:blur", native_unit_of_measurement=CONCENTRATION_PARTICLES_PER_100_MILLILITERS, state_class=SensorStateClass.MEASUREMENT, value_fn=lambda sensor: sensor.pm2_5_um_count, @@ -118,7 +114,6 @@ SENSOR_DESCRIPTIONS = [ key="pm5.0_count_concentration", translation_key="pm5_0_count_concentration", entity_registry_enabled_default=False, - icon="mdi:blur", native_unit_of_measurement=CONCENTRATION_PARTICLES_PER_100_MILLILITERS, state_class=SensorStateClass.MEASUREMENT, value_fn=lambda sensor: sensor.pm5_0_um_count, diff --git a/homeassistant/components/push/camera.py b/homeassistant/components/push/camera.py index 2cedcb8598a..8744ce8c2a1 100644 --- a/homeassistant/components/push/camera.py +++ b/homeassistant/components/push/camera.py @@ -1,4 +1,5 @@ """Camera platform that receives images through HTTP POST.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/pushbullet/__init__.py b/homeassistant/components/pushbullet/__init__.py index 14d90d4ca0b..e5892afc926 100644 --- a/homeassistant/components/pushbullet/__init__.py +++ b/homeassistant/components/pushbullet/__init__.py @@ -1,4 +1,5 @@ """The pushbullet component.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/pushbullet/api.py b/homeassistant/components/pushbullet/api.py index 691ef7413c3..72805a9aa94 100644 --- a/homeassistant/components/pushbullet/api.py +++ b/homeassistant/components/pushbullet/api.py @@ -1,4 +1,5 @@ """Pushbullet Notification provider.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/pushbullet/config_flow.py b/homeassistant/components/pushbullet/config_flow.py index 1eca2bd890b..08ade743aee 100644 --- a/homeassistant/components/pushbullet/config_flow.py +++ b/homeassistant/components/pushbullet/config_flow.py @@ -1,4 +1,5 @@ """Config flow for pushbullet integration.""" + from __future__ import annotations from typing import Any @@ -6,9 +7,8 @@ from typing import Any from pushbullet import InvalidKeyError, PushBullet, PushbulletError import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_API_KEY, CONF_NAME -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import selector from .const import DEFAULT_NAME, DOMAIN @@ -21,12 +21,12 @@ CONFIG_SCHEMA = vol.Schema( ) -class PushBulletConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class PushBulletConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for pushbullet integration.""" async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors = {} diff --git a/homeassistant/components/pushbullet/notify.py b/homeassistant/components/pushbullet/notify.py index 662240d0bf5..96f78c4a35d 100644 --- a/homeassistant/components/pushbullet/notify.py +++ b/homeassistant/components/pushbullet/notify.py @@ -1,4 +1,5 @@ """Pushbullet platform for notify component.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/pushbullet/sensor.py b/homeassistant/components/pushbullet/sensor.py index 2f2a1d066f3..4989fc91d5e 100644 --- a/homeassistant/components/pushbullet/sensor.py +++ b/homeassistant/components/pushbullet/sensor.py @@ -1,4 +1,5 @@ """Pushbullet platform for sensor component.""" + from __future__ import annotations from homeassistant.components.sensor import SensorEntity, SensorEntityDescription diff --git a/homeassistant/components/pushover/__init__.py b/homeassistant/components/pushover/__init__.py index c3b15b7c130..73c4223889e 100644 --- a/homeassistant/components/pushover/__init__.py +++ b/homeassistant/components/pushover/__init__.py @@ -1,4 +1,5 @@ """The pushover component.""" + from __future__ import annotations from pushover_complete import BadAPIRequestError, PushoverAPI diff --git a/homeassistant/components/pushover/config_flow.py b/homeassistant/components/pushover/config_flow.py index 32f91031351..fcc28b45ede 100644 --- a/homeassistant/components/pushover/config_flow.py +++ b/homeassistant/components/pushover/config_flow.py @@ -1,4 +1,5 @@ """Config flow for pushover integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -7,10 +8,9 @@ from typing import Any from pushover_complete import BadAPIRequestError, PushoverAPI import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_API_KEY, CONF_NAME from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResult from .const import CONF_USER_KEY, DEFAULT_NAME, DOMAIN @@ -39,12 +39,14 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, return errors -class PushBulletConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class PushBulletConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for pushover integration.""" - _reauth_entry: config_entries.ConfigEntry | None + _reauth_entry: ConfigEntry | None - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Perform reauth upon an API authentication error.""" self._reauth_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] @@ -53,7 +55,7 @@ class PushBulletConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_reauth_confirm( self, user_input: dict[str, str] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm reauth dialog.""" errors = {} if user_input is not None and self._reauth_entry: @@ -84,7 +86,7 @@ class PushBulletConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors = {} diff --git a/homeassistant/components/pushover/notify.py b/homeassistant/components/pushover/notify.py index 52eaed227e5..34ee1d08bdd 100644 --- a/homeassistant/components/pushover/notify.py +++ b/homeassistant/components/pushover/notify.py @@ -1,4 +1,5 @@ """Pushover platform for notify component.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/pushsafer/notify.py b/homeassistant/components/pushsafer/notify.py index 5411db05e2d..f4f5bf88a22 100644 --- a/homeassistant/components/pushsafer/notify.py +++ b/homeassistant/components/pushsafer/notify.py @@ -1,4 +1,5 @@ """Pushsafer platform for notify component.""" + from __future__ import annotations import base64 diff --git a/homeassistant/components/pvoutput/__init__.py b/homeassistant/components/pvoutput/__init__.py index bca5a23e62b..7dc02a07d1c 100644 --- a/homeassistant/components/pvoutput/__init__.py +++ b/homeassistant/components/pvoutput/__init__.py @@ -1,4 +1,5 @@ """The PVOutput integration.""" + from __future__ import annotations from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/pvoutput/config_flow.py b/homeassistant/components/pvoutput/config_flow.py index 2016f87e611..9d18952e7b4 100644 --- a/homeassistant/components/pvoutput/config_flow.py +++ b/homeassistant/components/pvoutput/config_flow.py @@ -1,4 +1,5 @@ """Config flow to configure the PVOutput integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -7,10 +8,9 @@ from typing import Any from pvo import PVOutput, PVOutputAuthenticationError, PVOutputError import voluptuous as vol -from homeassistant.config_entries import ConfigEntry, ConfigFlow +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import CONF_SYSTEM_ID, DOMAIN, LOGGER @@ -37,7 +37,7 @@ class PVOutputFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" errors = {} @@ -84,7 +84,9 @@ class PVOutputFlowHandler(ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Handle initiation of re-authentication with PVOutput.""" self.reauth_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] @@ -93,7 +95,7 @@ class PVOutputFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle re-authentication with PVOutput.""" errors = {} diff --git a/homeassistant/components/pvoutput/const.py b/homeassistant/components/pvoutput/const.py index f01d10fd13d..be63053a899 100644 --- a/homeassistant/components/pvoutput/const.py +++ b/homeassistant/components/pvoutput/const.py @@ -1,4 +1,5 @@ """Constants for the PVOutput integration.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/pvoutput/coordinator.py b/homeassistant/components/pvoutput/coordinator.py index 7b307f20274..5c38792c553 100644 --- a/homeassistant/components/pvoutput/coordinator.py +++ b/homeassistant/components/pvoutput/coordinator.py @@ -1,4 +1,5 @@ """DataUpdateCoordinator for the PVOutput integration.""" + from __future__ import annotations from pvo import PVOutput, PVOutputAuthenticationError, PVOutputNoDataError, Status diff --git a/homeassistant/components/pvoutput/diagnostics.py b/homeassistant/components/pvoutput/diagnostics.py index dfe215b7ddd..3b9007b77b4 100644 --- a/homeassistant/components/pvoutput/diagnostics.py +++ b/homeassistant/components/pvoutput/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for PVOutput.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/pvoutput/sensor.py b/homeassistant/components/pvoutput/sensor.py index c003e3cfad8..ef2bb3eb660 100644 --- a/homeassistant/components/pvoutput/sensor.py +++ b/homeassistant/components/pvoutput/sensor.py @@ -1,4 +1,5 @@ """Support for getting collected information from PVOutput.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/pvpc_hourly_pricing/__init__.py b/homeassistant/components/pvpc_hourly_pricing/__init__.py index af9154f5512..6ef16ea29b6 100644 --- a/homeassistant/components/pvpc_hourly_pricing/__init__.py +++ b/homeassistant/components/pvpc_hourly_pricing/__init__.py @@ -1,4 +1,5 @@ """The pvpc_hourly_pricing integration to collect Spain official electric prices.""" + from datetime import timedelta import logging diff --git a/homeassistant/components/pvpc_hourly_pricing/config_flow.py b/homeassistant/components/pvpc_hourly_pricing/config_flow.py index 66092cb9211..239e1bcb0e9 100644 --- a/homeassistant/components/pvpc_hourly_pricing/config_flow.py +++ b/homeassistant/components/pvpc_hourly_pricing/config_flow.py @@ -1,4 +1,5 @@ """Config flow for pvpc_hourly_pricing.""" + from __future__ import annotations from collections.abc import Mapping @@ -7,10 +8,14 @@ from typing import Any from aiopvpc import DEFAULT_POWER_KW, PVPCData import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlowWithConfigEntry, +) from homeassistant.const import CONF_API_TOKEN, CONF_NAME from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.util import dt as dt_util @@ -32,7 +37,7 @@ _MAIL_TO_LINK = ( ) -class TariffSelectorConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class TariffSelectorConfigFlow(ConfigFlow, domain=DOMAIN): """Handle config flow for `pvpc_hourly_pricing`.""" VERSION = 1 @@ -43,19 +48,19 @@ class TariffSelectorConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): _use_api_token: bool = False _api_token: str | None = None _api: PVPCData | None = None - _reauth_entry: config_entries.ConfigEntry | None = None + _reauth_entry: ConfigEntry | None = None @staticmethod @callback def async_get_options_flow( - config_entry: config_entries.ConfigEntry, + config_entry: ConfigEntry, ) -> PVPCOptionsFlowHandler: """Get the options flow for this handler.""" return PVPCOptionsFlowHandler(config_entry) async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" if user_input is not None: await self.async_set_unique_id(user_input[ATTR_TARIFF]) @@ -92,7 +97,7 @@ class TariffSelectorConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_api_token( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle optional step to define API token for extra sensors.""" if user_input is not None: self._api_token = user_input[CONF_API_TOKEN] @@ -110,7 +115,9 @@ class TariffSelectorConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): description_placeholders={"mail_to_link": _MAIL_TO_LINK}, ) - async def _async_verify(self, step_id: str, data_schema: vol.Schema) -> FlowResult: + async def _async_verify( + self, step_id: str, data_schema: vol.Schema + ) -> ConfigFlowResult: """Attempt to verify the provided configuration.""" errors: dict[str, str] = {} auth_ok = True @@ -144,7 +151,9 @@ class TariffSelectorConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): assert self._name is not None return self.async_create_entry(title=self._name, data=data) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Handle re-authentication with ESIOS Token.""" self._reauth_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] @@ -159,7 +168,7 @@ class TariffSelectorConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm reauth dialog.""" data_schema = vol.Schema( { @@ -174,7 +183,7 @@ class TariffSelectorConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form(step_id="reauth_confirm", data_schema=data_schema) -class PVPCOptionsFlowHandler(config_entries.OptionsFlowWithConfigEntry): +class PVPCOptionsFlowHandler(OptionsFlowWithConfigEntry): """Handle PVPC options.""" _power: float | None = None @@ -182,7 +191,7 @@ class PVPCOptionsFlowHandler(config_entries.OptionsFlowWithConfigEntry): async def async_step_api_token( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle optional step to define API token for extra sensors.""" if user_input is not None and user_input.get(CONF_API_TOKEN): return self.async_create_entry( @@ -208,7 +217,7 @@ class PVPCOptionsFlowHandler(config_entries.OptionsFlowWithConfigEntry): async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Manage the options.""" if user_input is not None: if user_input[CONF_USE_API_TOKEN]: diff --git a/homeassistant/components/pvpc_hourly_pricing/const.py b/homeassistant/components/pvpc_hourly_pricing/const.py index a6bfc6f3188..9aaa46233cb 100644 --- a/homeassistant/components/pvpc_hourly_pricing/const.py +++ b/homeassistant/components/pvpc_hourly_pricing/const.py @@ -1,4 +1,5 @@ """Constant values for pvpc_hourly_pricing.""" + from aiopvpc.const import TARIFFS import voluptuous as vol diff --git a/homeassistant/components/pvpc_hourly_pricing/helpers.py b/homeassistant/components/pvpc_hourly_pricing/helpers.py index 195d20aee89..e0792f76404 100644 --- a/homeassistant/components/pvpc_hourly_pricing/helpers.py +++ b/homeassistant/components/pvpc_hourly_pricing/helpers.py @@ -1,4 +1,5 @@ """Helper functions to relate sensors keys and unique ids.""" + from aiopvpc.const import ( ALL_SENSORS, KEY_INJECTION, diff --git a/homeassistant/components/pvpc_hourly_pricing/sensor.py b/homeassistant/components/pvpc_hourly_pricing/sensor.py index 9cc3ef35a4b..246a8b65892 100644 --- a/homeassistant/components/pvpc_hourly_pricing/sensor.py +++ b/homeassistant/components/pvpc_hourly_pricing/sensor.py @@ -1,4 +1,5 @@ """Sensor to collect the reference daily prices of electricity ('PVPC') in Spain.""" + from __future__ import annotations from collections.abc import Mapping @@ -153,8 +154,10 @@ async def async_setup_entry( coordinator: ElecPricesDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] sensors = [ElecPriceSensor(coordinator, SENSOR_TYPES[0], entry.unique_id)] if coordinator.api.using_private_api: - for sensor_desc in SENSOR_TYPES[1:]: - sensors.append(ElecPriceSensor(coordinator, sensor_desc, entry.unique_id)) + sensors.extend( + ElecPriceSensor(coordinator, sensor_desc, entry.unique_id) + for sensor_desc in SENSOR_TYPES[1:] + ) async_add_entities(sensors) diff --git a/homeassistant/components/pyload/sensor.py b/homeassistant/components/pyload/sensor.py index 35e32d510ca..b7d4d1f461b 100644 --- a/homeassistant/components/pyload/sensor.py +++ b/homeassistant/components/pyload/sensor.py @@ -1,4 +1,5 @@ """Support for monitoring pyLoad.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/python_script/__init__.py b/homeassistant/components/python_script/__init__.py index c10c6de5b3f..e976ae5d1b0 100644 --- a/homeassistant/components/python_script/__init__.py +++ b/homeassistant/components/python_script/__init__.py @@ -1,4 +1,5 @@ """Component to allow running Python scripts.""" + import datetime import glob import logging diff --git a/homeassistant/components/python_script/icons.json b/homeassistant/components/python_script/icons.json new file mode 100644 index 00000000000..a03163179cb --- /dev/null +++ b/homeassistant/components/python_script/icons.json @@ -0,0 +1,5 @@ +{ + "services": { + "reload": "mdi:reload" + } +} diff --git a/homeassistant/components/qbittorrent/__init__.py b/homeassistant/components/qbittorrent/__init__.py index 84315186097..7b1a38b7e31 100644 --- a/homeassistant/components/qbittorrent/__init__.py +++ b/homeassistant/components/qbittorrent/__init__.py @@ -1,4 +1,5 @@ """The qbittorrent component.""" + import logging from qbittorrent.client import LoginRequired diff --git a/homeassistant/components/qbittorrent/config_flow.py b/homeassistant/components/qbittorrent/config_flow.py index 54215fb4563..c17c842529b 100644 --- a/homeassistant/components/qbittorrent/config_flow.py +++ b/homeassistant/components/qbittorrent/config_flow.py @@ -1,4 +1,5 @@ """Config flow for qBittorrent.""" + from __future__ import annotations import logging @@ -8,9 +9,8 @@ from qbittorrent.client import LoginRequired from requests.exceptions import RequestException import voluptuous as vol -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME, CONF_VERIFY_SSL -from homeassistant.data_entry_flow import FlowResult from .const import DEFAULT_NAME, DEFAULT_URL, DOMAIN from .helpers import setup_client @@ -32,7 +32,7 @@ class QbittorrentConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a user-initiated config flow.""" errors = {} diff --git a/homeassistant/components/qbittorrent/const.py b/homeassistant/components/qbittorrent/const.py index 96c60e9b380..d8fe2c012a3 100644 --- a/homeassistant/components/qbittorrent/const.py +++ b/homeassistant/components/qbittorrent/const.py @@ -1,4 +1,5 @@ """Constants for qBittorrent.""" + from typing import Final DOMAIN: Final = "qbittorrent" diff --git a/homeassistant/components/qbittorrent/coordinator.py b/homeassistant/components/qbittorrent/coordinator.py index 11467ce62f4..32ce4cf9711 100644 --- a/homeassistant/components/qbittorrent/coordinator.py +++ b/homeassistant/components/qbittorrent/coordinator.py @@ -1,4 +1,5 @@ """The QBittorrent coordinator.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/qbittorrent/helpers.py b/homeassistant/components/qbittorrent/helpers.py index 7f7833e912a..b9c29675473 100644 --- a/homeassistant/components/qbittorrent/helpers.py +++ b/homeassistant/components/qbittorrent/helpers.py @@ -1,4 +1,5 @@ """Helper functions for qBittorrent.""" + from qbittorrent.client import Client diff --git a/homeassistant/components/qbittorrent/icons.json b/homeassistant/components/qbittorrent/icons.json new file mode 100644 index 00000000000..bb458c751e1 --- /dev/null +++ b/homeassistant/components/qbittorrent/icons.json @@ -0,0 +1,12 @@ +{ + "entity": { + "sensor": { + "download_speed": { + "default": "mdi:cloud-download" + }, + "upload_speed": { + "default": "mdi:cloud-upload" + } + } + } +} diff --git a/homeassistant/components/qbittorrent/sensor.py b/homeassistant/components/qbittorrent/sensor.py index 78e8ba59d44..84eac7d28cf 100644 --- a/homeassistant/components/qbittorrent/sensor.py +++ b/homeassistant/components/qbittorrent/sensor.py @@ -1,4 +1,5 @@ """Support for monitoring the qBittorrent API.""" + from __future__ import annotations from collections.abc import Callable @@ -64,7 +65,6 @@ SENSOR_TYPES: tuple[QBittorrentSensorEntityDescription, ...] = ( QBittorrentSensorEntityDescription( key=SENSOR_TYPE_DOWNLOAD_SPEED, translation_key="download_speed", - icon="mdi:cloud-download", device_class=SensorDeviceClass.DATA_RATE, native_unit_of_measurement=UnitOfDataRate.BYTES_PER_SECOND, suggested_display_precision=2, @@ -76,7 +76,6 @@ SENSOR_TYPES: tuple[QBittorrentSensorEntityDescription, ...] = ( QBittorrentSensorEntityDescription( key=SENSOR_TYPE_UPLOAD_SPEED, translation_key="upload_speed", - icon="mdi:cloud-upload", device_class=SensorDeviceClass.DATA_RATE, native_unit_of_measurement=UnitOfDataRate.BYTES_PER_SECOND, suggested_display_precision=2, diff --git a/homeassistant/components/qingping/__init__.py b/homeassistant/components/qingping/__init__.py index 9155c02c07c..ac91f314a28 100644 --- a/homeassistant/components/qingping/__init__.py +++ b/homeassistant/components/qingping/__init__.py @@ -1,4 +1,5 @@ """The Qingping integration.""" + from __future__ import annotations import logging @@ -25,14 +26,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: address = entry.unique_id assert address is not None data = QingpingBluetoothDeviceData() - coordinator = hass.data.setdefault(DOMAIN, {})[ - entry.entry_id - ] = PassiveBluetoothProcessorCoordinator( - hass, - _LOGGER, - address=address, - mode=BluetoothScanningMode.PASSIVE, - update_method=data.update, + coordinator = hass.data.setdefault(DOMAIN, {})[entry.entry_id] = ( + PassiveBluetoothProcessorCoordinator( + hass, + _LOGGER, + address=address, + mode=BluetoothScanningMode.PASSIVE, + update_method=data.update, + ) ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload( diff --git a/homeassistant/components/qingping/binary_sensor.py b/homeassistant/components/qingping/binary_sensor.py index 99bcf83ec1a..f4f81eac394 100644 --- a/homeassistant/components/qingping/binary_sensor.py +++ b/homeassistant/components/qingping/binary_sensor.py @@ -1,4 +1,5 @@ """Support for Qingping binary sensors.""" + from __future__ import annotations from qingping_ble import ( diff --git a/homeassistant/components/qingping/config_flow.py b/homeassistant/components/qingping/config_flow.py index a90085afb4f..c5efe03a878 100644 --- a/homeassistant/components/qingping/config_flow.py +++ b/homeassistant/components/qingping/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Qingping integration.""" + from __future__ import annotations from typing import Any @@ -12,9 +13,8 @@ from homeassistant.components.bluetooth import ( async_discovered_service_info, async_process_advertisements, ) -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_ADDRESS -from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN @@ -52,7 +52,7 @@ class QingpingConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_bluetooth( self, discovery_info: BluetoothServiceInfoBleak - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the bluetooth discovery step.""" await self.async_set_unique_id(discovery_info.address) self._abort_if_unique_id_configured() @@ -69,7 +69,7 @@ class QingpingConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_bluetooth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm discovery.""" assert self._discovered_device is not None device = self._discovered_device @@ -88,7 +88,7 @@ class QingpingConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the user step to pick discovered device.""" if user_input is not None: address = user_input[CONF_ADDRESS] diff --git a/homeassistant/components/qingping/device.py b/homeassistant/components/qingping/device.py index ec6bb23c2af..466ac43f079 100644 --- a/homeassistant/components/qingping/device.py +++ b/homeassistant/components/qingping/device.py @@ -1,4 +1,5 @@ """Support for Qingping devices.""" + from __future__ import annotations from qingping_ble import DeviceKey diff --git a/homeassistant/components/qingping/sensor.py b/homeassistant/components/qingping/sensor.py index bc99ed80ff3..e75c9b34f49 100644 --- a/homeassistant/components/qingping/sensor.py +++ b/homeassistant/components/qingping/sensor.py @@ -1,4 +1,5 @@ """Support for Qingping sensors.""" + from __future__ import annotations from qingping_ble import ( diff --git a/homeassistant/components/qld_bushfire/geo_location.py b/homeassistant/components/qld_bushfire/geo_location.py index 3606b664a3f..5d0173f8c54 100644 --- a/homeassistant/components/qld_bushfire/geo_location.py +++ b/homeassistant/components/qld_bushfire/geo_location.py @@ -1,4 +1,5 @@ """Support for Queensland Bushfire Alert Feeds.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/qnap/__init__.py b/homeassistant/components/qnap/__init__.py index 2491e69803f..82e912a60cd 100644 --- a/homeassistant/components/qnap/__init__.py +++ b/homeassistant/components/qnap/__init__.py @@ -1,4 +1,5 @@ """The qnap component.""" + from __future__ import annotations from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/qnap/config_flow.py b/homeassistant/components/qnap/config_flow.py index 04b5340fa8a..3e0c524f59e 100644 --- a/homeassistant/components/qnap/config_flow.py +++ b/homeassistant/components/qnap/config_flow.py @@ -1,4 +1,5 @@ """Config flow to configure qnap component.""" + from __future__ import annotations import logging @@ -8,7 +9,7 @@ from qnapstats import QNAPStats from requests.exceptions import ConnectTimeout import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, @@ -17,7 +18,6 @@ from homeassistant.const import ( CONF_USERNAME, CONF_VERIFY_SSL, ) -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_validation as cv from .const import ( @@ -42,7 +42,7 @@ DATA_SCHEMA = vol.Schema( _LOGGER = logging.getLogger(__name__) -class QnapConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class QnapConfigFlow(ConfigFlow, domain=DOMAIN): """Qnap configuration flow.""" VERSION = 1 @@ -50,7 +50,7 @@ class QnapConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None, - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" errors = {} if user_input is not None: diff --git a/homeassistant/components/qnap/coordinator.py b/homeassistant/components/qnap/coordinator.py index b868a931ebd..8c2bf81a47f 100644 --- a/homeassistant/components/qnap/coordinator.py +++ b/homeassistant/components/qnap/coordinator.py @@ -1,4 +1,5 @@ """Data coordinator for the qnap integration.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/qnap/sensor.py b/homeassistant/components/qnap/sensor.py index 348759012ac..2a5186ad940 100644 --- a/homeassistant/components/qnap/sensor.py +++ b/homeassistant/components/qnap/sensor.py @@ -1,4 +1,5 @@ """Support for QNAP NAS Sensors.""" + from __future__ import annotations from homeassistant import config_entries diff --git a/homeassistant/components/qnap_qsw/__init__.py b/homeassistant/components/qnap_qsw/__init__.py index b3bcc1705de..d7352435b07 100644 --- a/homeassistant/components/qnap_qsw/__init__.py +++ b/homeassistant/components/qnap_qsw/__init__.py @@ -1,4 +1,5 @@ """The QNAP QSW integration.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/qnap_qsw/binary_sensor.py b/homeassistant/components/qnap_qsw/binary_sensor.py index f655beee3d4..a9c025b86ce 100644 --- a/homeassistant/components/qnap_qsw/binary_sensor.py +++ b/homeassistant/components/qnap_qsw/binary_sensor.py @@ -1,4 +1,5 @@ """Support for the QNAP QSW binary sensors.""" + from __future__ import annotations from dataclasses import dataclass, replace @@ -82,14 +83,14 @@ async def async_setup_entry( """Add QNAP QSW binary sensors from a config_entry.""" coordinator: QswDataCoordinator = hass.data[DOMAIN][entry.entry_id][QSW_COORD_DATA] - entities: list[QswBinarySensor] = [] - - for description in BINARY_SENSOR_TYPES: + entities: list[QswBinarySensor] = [ + QswBinarySensor(coordinator, description, entry) + for description in BINARY_SENSOR_TYPES if ( description.key in coordinator.data and description.subkey in coordinator.data[description.key] - ): - entities.append(QswBinarySensor(coordinator, description, entry)) + ) + ] for description in LACP_PORT_BINARY_SENSOR_TYPES: if ( diff --git a/homeassistant/components/qnap_qsw/button.py b/homeassistant/components/qnap_qsw/button.py index c2c4f9f6043..091c6786a92 100644 --- a/homeassistant/components/qnap_qsw/button.py +++ b/homeassistant/components/qnap_qsw/button.py @@ -1,4 +1,5 @@ """Support for the QNAP QSW buttons.""" + from __future__ import annotations from collections.abc import Awaitable, Callable @@ -22,18 +23,13 @@ from .coordinator import QswDataCoordinator from .entity import QswDataEntity -@dataclass(frozen=True) -class QswButtonDescriptionMixin: - """Mixin to describe a Button entity.""" +@dataclass(frozen=True, kw_only=True) +class QswButtonDescription(ButtonEntityDescription): + """Class to describe a Button entity.""" press_action: Callable[[QnapQswApi], Awaitable[bool]] -@dataclass(frozen=True) -class QswButtonDescription(ButtonEntityDescription, QswButtonDescriptionMixin): - """Class to describe a Button entity.""" - - BUTTON_TYPES: Final[tuple[QswButtonDescription, ...]] = ( QswButtonDescription( device_class=ButtonDeviceClass.RESTART, diff --git a/homeassistant/components/qnap_qsw/config_flow.py b/homeassistant/components/qnap_qsw/config_flow.py index e0d4a1f78cd..3a10e54ac82 100644 --- a/homeassistant/components/qnap_qsw/config_flow.py +++ b/homeassistant/components/qnap_qsw/config_flow.py @@ -1,4 +1,5 @@ """Config flow for QNAP QSW.""" + from __future__ import annotations import logging @@ -8,10 +9,10 @@ from aioqsw.exceptions import LoginError, QswError from aioqsw.localapi import ConnectionOptions, QnapQswApi import voluptuous as vol -from homeassistant import config_entries from homeassistant.components import dhcp +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME -from homeassistant.data_entry_flow import AbortFlow, FlowResult +from homeassistant.data_entry_flow import AbortFlow from homeassistant.helpers import aiohttp_client from homeassistant.helpers.device_registry import format_mac @@ -20,7 +21,7 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class QNapQSWConfigFlow(ConfigFlow, domain=DOMAIN): """Handle config flow for a QNAP QSW device.""" _discovered_mac: str | None = None @@ -28,7 +29,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors = {} @@ -71,7 +72,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: + async def async_step_dhcp( + self, discovery_info: dhcp.DhcpServiceInfo + ) -> ConfigFlowResult: """Handle DHCP discovery.""" self._discovered_url = f"http://{discovery_info.ip}" self._discovered_mac = discovery_info.macaddress @@ -97,7 +100,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_discovered_connection( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm discovery.""" errors = {} assert self._discovered_url is not None diff --git a/homeassistant/components/qnap_qsw/coordinator.py b/homeassistant/components/qnap_qsw/coordinator.py index 6451b525004..c873f2a773d 100644 --- a/homeassistant/components/qnap_qsw/coordinator.py +++ b/homeassistant/components/qnap_qsw/coordinator.py @@ -1,4 +1,5 @@ """The QNAP QSW coordinator.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/qnap_qsw/diagnostics.py b/homeassistant/components/qnap_qsw/diagnostics.py index 2467e9181a3..e732c551a40 100644 --- a/homeassistant/components/qnap_qsw/diagnostics.py +++ b/homeassistant/components/qnap_qsw/diagnostics.py @@ -1,4 +1,5 @@ """Support for the QNAP QSW diagnostics.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/qnap_qsw/entity.py b/homeassistant/components/qnap_qsw/entity.py index de92afe69a2..a3038b1fc7b 100644 --- a/homeassistant/components/qnap_qsw/entity.py +++ b/homeassistant/components/qnap_qsw/entity.py @@ -1,4 +1,5 @@ """Entity classes for the QNAP QSW integration.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/homeassistant/components/qnap_qsw/icons.json b/homeassistant/components/qnap_qsw/icons.json new file mode 100644 index 00000000000..4cdae8e6718 --- /dev/null +++ b/homeassistant/components/qnap_qsw/icons.json @@ -0,0 +1,33 @@ +{ + "entity": { + "sensor": { + "fan_1_speed": { + "default": "mdi:fan-speed-1" + }, + "fan_2_speed": { + "default": "mdi:fan-speed-2" + }, + "ports": { + "default": "mdi:ethernet" + }, + "rx": { + "default": "mdi:download-network" + }, + "rx_errors": { + "default": "mdi:close-network" + }, + "rx_speed": { + "default": "mdi:download-network" + }, + "tx": { + "default": "mdi:upload-network" + }, + "tx_speed": { + "default": "mdi:upload-network" + }, + "uptime": { + "default": "mdi:timer-outline" + } + } + } +} diff --git a/homeassistant/components/qnap_qsw/sensor.py b/homeassistant/components/qnap_qsw/sensor.py index 3168e4511d2..b64c0aaad82 100644 --- a/homeassistant/components/qnap_qsw/sensor.py +++ b/homeassistant/components/qnap_qsw/sensor.py @@ -1,4 +1,5 @@ """Support for the QNAP QSW sensors.""" + from __future__ import annotations from dataclasses import dataclass, replace @@ -62,7 +63,6 @@ class QswSensorEntityDescription(SensorEntityDescription, QswEntityDescription): SENSOR_TYPES: Final[tuple[QswSensorEntityDescription, ...]] = ( QswSensorEntityDescription( translation_key="fan_1_speed", - icon="mdi:fan-speed-1", key=QSD_SYSTEM_SENSOR, native_unit_of_measurement=RPM, state_class=SensorStateClass.MEASUREMENT, @@ -70,7 +70,6 @@ SENSOR_TYPES: Final[tuple[QswSensorEntityDescription, ...]] = ( ), QswSensorEntityDescription( translation_key="fan_2_speed", - icon="mdi:fan-speed-2", key=QSD_SYSTEM_SENSOR, native_unit_of_measurement=RPM, state_class=SensorStateClass.MEASUREMENT, @@ -82,7 +81,6 @@ SENSOR_TYPES: Final[tuple[QswSensorEntityDescription, ...]] = ( ATTR_MAX: [QSD_SYSTEM_BOARD, QSD_PORT_NUM], }, entity_registry_enabled_default=False, - icon="mdi:ethernet", key=QSD_PORTS_STATUS, state_class=SensorStateClass.MEASUREMENT, subkey=QSD_LINK, @@ -91,7 +89,6 @@ SENSOR_TYPES: Final[tuple[QswSensorEntityDescription, ...]] = ( entity_registry_enabled_default=False, translation_key="rx", device_class=SensorDeviceClass.DATA_SIZE, - icon="mdi:download-network", key=QSD_PORTS_STATISTICS, native_unit_of_measurement=UnitOfInformation.BYTES, state_class=SensorStateClass.TOTAL_INCREASING, @@ -100,7 +97,6 @@ SENSOR_TYPES: Final[tuple[QswSensorEntityDescription, ...]] = ( QswSensorEntityDescription( entity_registry_enabled_default=False, translation_key="rx_errors", - icon="mdi:close-network", key=QSD_PORTS_STATISTICS, entity_category=EntityCategory.DIAGNOSTIC, state_class=SensorStateClass.TOTAL_INCREASING, @@ -110,7 +106,6 @@ SENSOR_TYPES: Final[tuple[QswSensorEntityDescription, ...]] = ( entity_registry_enabled_default=False, translation_key="rx_speed", device_class=SensorDeviceClass.DATA_RATE, - icon="mdi:download-network", key=QSD_PORTS_STATISTICS, native_unit_of_measurement=UnitOfDataRate.BYTES_PER_SECOND, state_class=SensorStateClass.MEASUREMENT, @@ -130,7 +125,6 @@ SENSOR_TYPES: Final[tuple[QswSensorEntityDescription, ...]] = ( entity_registry_enabled_default=False, translation_key="tx", device_class=SensorDeviceClass.DATA_SIZE, - icon="mdi:upload-network", key=QSD_PORTS_STATISTICS, native_unit_of_measurement=UnitOfInformation.BYTES, state_class=SensorStateClass.TOTAL_INCREASING, @@ -140,7 +134,6 @@ SENSOR_TYPES: Final[tuple[QswSensorEntityDescription, ...]] = ( entity_registry_enabled_default=False, translation_key="tx_speed", device_class=SensorDeviceClass.DATA_RATE, - icon="mdi:upload-network", key=QSD_PORTS_STATISTICS, native_unit_of_measurement=UnitOfDataRate.BYTES_PER_SECOND, state_class=SensorStateClass.MEASUREMENT, @@ -148,7 +141,6 @@ SENSOR_TYPES: Final[tuple[QswSensorEntityDescription, ...]] = ( ), QswSensorEntityDescription( translation_key="uptime", - icon="mdi:timer-outline", key=QSD_SYSTEM_TIME, entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=UnitOfTime.SECONDS, @@ -296,14 +288,14 @@ async def async_setup_entry( """Add QNAP QSW sensors from a config_entry.""" coordinator: QswDataCoordinator = hass.data[DOMAIN][entry.entry_id][QSW_COORD_DATA] - entities: list[QswSensor] = [] - - for description in SENSOR_TYPES: + entities: list[QswSensor] = [ + QswSensor(coordinator, description, entry) + for description in SENSOR_TYPES if ( description.key in coordinator.data and description.subkey in coordinator.data[description.key] - ): - entities.append(QswSensor(coordinator, description, entry)) + ) + ] for description in LACP_PORT_SENSOR_TYPES: if ( diff --git a/homeassistant/components/qnap_qsw/update.py b/homeassistant/components/qnap_qsw/update.py index 5ea6e80f4bb..ac789235271 100644 --- a/homeassistant/components/qnap_qsw/update.py +++ b/homeassistant/components/qnap_qsw/update.py @@ -1,4 +1,5 @@ """Support for the QNAP QSW update.""" + from __future__ import annotations from typing import Any, Final diff --git a/homeassistant/components/qrcode/image_processing.py b/homeassistant/components/qrcode/image_processing.py index 06fe92e5b1d..bec0cea8c2f 100644 --- a/homeassistant/components/qrcode/image_processing.py +++ b/homeassistant/components/qrcode/image_processing.py @@ -1,4 +1,5 @@ """Support for the QR code image processing.""" + from __future__ import annotations import io @@ -20,11 +21,10 @@ def setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the QR code image processing platform.""" - entities = [] - for camera in config[CONF_SOURCE]: - entities.append(QrEntity(camera[CONF_ENTITY_ID], camera.get(CONF_NAME))) - - add_entities(entities) + add_entities( + QrEntity(camera[CONF_ENTITY_ID], camera.get(CONF_NAME)) + for camera in config[CONF_SOURCE] + ) class QrEntity(ImageProcessingEntity): diff --git a/homeassistant/components/quantum_gateway/device_tracker.py b/homeassistant/components/quantum_gateway/device_tracker.py index c8e23b68416..1c43cbb14a8 100644 --- a/homeassistant/components/quantum_gateway/device_tracker.py +++ b/homeassistant/components/quantum_gateway/device_tracker.py @@ -1,4 +1,5 @@ """Support for Verizon FiOS Quantum Gateways.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/qvr_pro/__init__.py b/homeassistant/components/qvr_pro/__init__.py index d0fc8511dd3..9aad94790c6 100644 --- a/homeassistant/components/qvr_pro/__init__.py +++ b/homeassistant/components/qvr_pro/__init__.py @@ -1,4 +1,5 @@ """Support for QVR Pro NVR software by QNAP.""" + import logging from pyqvrpro import Client diff --git a/homeassistant/components/qvr_pro/camera.py b/homeassistant/components/qvr_pro/camera.py index f3caac864d9..544ef808ca7 100644 --- a/homeassistant/components/qvr_pro/camera.py +++ b/homeassistant/components/qvr_pro/camera.py @@ -1,4 +1,5 @@ """Support for QVR Pro streams.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/qvr_pro/icons.json b/homeassistant/components/qvr_pro/icons.json new file mode 100644 index 00000000000..556a8d40752 --- /dev/null +++ b/homeassistant/components/qvr_pro/icons.json @@ -0,0 +1,6 @@ +{ + "services": { + "start_record": "mdi:record-rec", + "stop_record": "mdi:stop" + } +} diff --git a/homeassistant/components/qwikswitch/__init__.py b/homeassistant/components/qwikswitch/__init__.py index d11b71a6dd4..eea110a02d7 100644 --- a/homeassistant/components/qwikswitch/__init__.py +++ b/homeassistant/components/qwikswitch/__init__.py @@ -1,4 +1,5 @@ """Support for Qwikswitch devices.""" + from __future__ import annotations import logging @@ -224,7 +225,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async_dispatcher_send(hass, qspacket[QS_ID], qspacket) # Update all ha_objects - hass.async_add_job(qsusb.update_from_devices) + hass.async_create_task(qsusb.update_from_devices()) @callback def async_start(_): diff --git a/homeassistant/components/qwikswitch/binary_sensor.py b/homeassistant/components/qwikswitch/binary_sensor.py index a4ab193228c..b35908da12c 100644 --- a/homeassistant/components/qwikswitch/binary_sensor.py +++ b/homeassistant/components/qwikswitch/binary_sensor.py @@ -1,4 +1,5 @@ """Support for Qwikswitch Binary Sensors.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/qwikswitch/light.py b/homeassistant/components/qwikswitch/light.py index 2109b8d2c3b..12c2763d3a4 100644 --- a/homeassistant/components/qwikswitch/light.py +++ b/homeassistant/components/qwikswitch/light.py @@ -1,4 +1,5 @@ """Support for Qwikswitch Relays and Dimmers.""" + from __future__ import annotations from homeassistant.components.light import ColorMode, LightEntity diff --git a/homeassistant/components/qwikswitch/sensor.py b/homeassistant/components/qwikswitch/sensor.py index 8a72ca7c811..856949d8926 100644 --- a/homeassistant/components/qwikswitch/sensor.py +++ b/homeassistant/components/qwikswitch/sensor.py @@ -1,4 +1,5 @@ """Support for Qwikswitch Sensors.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/qwikswitch/switch.py b/homeassistant/components/qwikswitch/switch.py index e36fc60d589..1623bfb3361 100644 --- a/homeassistant/components/qwikswitch/switch.py +++ b/homeassistant/components/qwikswitch/switch.py @@ -1,4 +1,5 @@ """Support for Qwikswitch relays.""" + from __future__ import annotations from homeassistant.components.switch import SwitchEntity diff --git a/homeassistant/components/rabbitair/__init__.py b/homeassistant/components/rabbitair/__init__.py index 97b37f6c03f..e4eb67a67f5 100644 --- a/homeassistant/components/rabbitair/__init__.py +++ b/homeassistant/components/rabbitair/__init__.py @@ -1,4 +1,5 @@ """The Rabbit Air integration.""" + from __future__ import annotations from rabbitair import Client, UdpClient diff --git a/homeassistant/components/rabbitair/config_flow.py b/homeassistant/components/rabbitair/config_flow.py index e265740179d..30dfac93236 100644 --- a/homeassistant/components/rabbitair/config_flow.py +++ b/homeassistant/components/rabbitair/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Rabbit Air integration.""" + from __future__ import annotations import logging @@ -11,7 +12,6 @@ from homeassistant import config_entries from homeassistant.components import zeroconf from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST, CONF_MAC from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr @@ -58,7 +58,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> config_entries.ConfigFlowResult: """Handle the initial step.""" errors = {} @@ -100,7 +100,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_zeroconf( self, discovery_info: zeroconf.ZeroconfServiceInfo - ) -> FlowResult: + ) -> config_entries.ConfigFlowResult: """Handle zeroconf discovery.""" mac = dr.format_mac(discovery_info.properties["id"]) await self.async_set_unique_id(mac) diff --git a/homeassistant/components/rabbitair/coordinator.py b/homeassistant/components/rabbitair/coordinator.py index 36c58f8700c..3c7db126c7d 100644 --- a/homeassistant/components/rabbitair/coordinator.py +++ b/homeassistant/components/rabbitair/coordinator.py @@ -1,4 +1,5 @@ """Rabbit Air Update Coordinator.""" + from collections.abc import Coroutine from datetime import timedelta import logging diff --git a/homeassistant/components/rabbitair/entity.py b/homeassistant/components/rabbitair/entity.py index 07e49aae7cb..47a1b7db3eb 100644 --- a/homeassistant/components/rabbitair/entity.py +++ b/homeassistant/components/rabbitair/entity.py @@ -1,4 +1,5 @@ """A base class for Rabbit Air entities.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/rabbitair/fan.py b/homeassistant/components/rabbitair/fan.py index 46465163839..ee4f136930d 100644 --- a/homeassistant/components/rabbitair/fan.py +++ b/homeassistant/components/rabbitair/fan.py @@ -1,4 +1,5 @@ """Support for Rabbit Air fan entity.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/rachio/__init__.py b/homeassistant/components/rachio/__init__.py index 13299b4e7dc..f91a7b4fa75 100644 --- a/homeassistant/components/rachio/__init__.py +++ b/homeassistant/components/rachio/__init__.py @@ -1,4 +1,5 @@ """Integration with the Rachio Iro sprinkler system controller.""" + import logging import secrets @@ -82,7 +83,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise ConfigEntryNotReady from error # Check for Rachio controller devices - if not person.controllers: + if not person.controllers and not person.base_stations: _LOGGER.error("No Rachio devices found in account %s", person.username) return False _LOGGER.info( @@ -90,10 +91,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: "%d Rachio device(s) found; The url %s must be accessible from the internet" " in order to receive updates" ), - len(person.controllers), + len(person.controllers) + len(person.base_stations), webhook_url, ) + for base in person.base_stations: + await base.coordinator.async_config_entry_first_refresh() + # Enable platform hass.data.setdefault(DOMAIN, {})[entry.entry_id] = person async_register_webhook(hass, entry) diff --git a/homeassistant/components/rachio/binary_sensor.py b/homeassistant/components/rachio/binary_sensor.py index 652806a2bad..eb7a84867ab 100644 --- a/homeassistant/components/rachio/binary_sensor.py +++ b/homeassistant/components/rachio/binary_sensor.py @@ -1,4 +1,5 @@ """Integration with the Rachio Iro sprinkler system controller.""" + from abc import abstractmethod import logging diff --git a/homeassistant/components/rachio/config_flow.py b/homeassistant/components/rachio/config_flow.py index 477abcb3694..d0a311db60e 100644 --- a/homeassistant/components/rachio/config_flow.py +++ b/homeassistant/components/rachio/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Rachio integration.""" + from __future__ import annotations from http import HTTPStatus @@ -8,11 +9,16 @@ from rachiopy import Rachio from requests.exceptions import ConnectTimeout import voluptuous as vol -from homeassistant import config_entries, core, exceptions from homeassistant.components import zeroconf +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.const import CONF_API_KEY -from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError from .const import ( CONF_MANUAL_RUN_MINS, @@ -28,7 +34,7 @@ _LOGGER = logging.getLogger(__name__) DATA_SCHEMA = vol.Schema({vol.Required(CONF_API_KEY): str}, extra=vol.ALLOW_EXTRA) -async def validate_input(hass: core.HomeAssistant, data): +async def validate_input(hass: HomeAssistant, data): """Validate the user input allows us to connect. Data has the keys from DATA_SCHEMA with values provided by the user. @@ -56,7 +62,7 @@ async def validate_input(hass: core.HomeAssistant, data): return {"title": username} -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class RachioConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Rachio.""" VERSION = 1 @@ -84,7 +90,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_homekit( self, discovery_info: zeroconf.ZeroconfServiceInfo - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle HomeKit discovery.""" self._async_abort_entries_match() await self.async_set_unique_id( @@ -96,16 +102,16 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback def async_get_options_flow( - config_entry: config_entries.ConfigEntry, + config_entry: ConfigEntry, ) -> OptionsFlowHandler: """Get the options flow for this handler.""" return OptionsFlowHandler(config_entry) -class OptionsFlowHandler(config_entries.OptionsFlow): +class OptionsFlowHandler(OptionsFlow): """Handle a option flow for Rachio.""" - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + def __init__(self, config_entry: ConfigEntry) -> None: """Initialize options flow.""" self.config_entry = config_entry @@ -127,9 +133,9 @@ class OptionsFlowHandler(config_entries.OptionsFlow): return self.async_show_form(step_id="init", data_schema=data_schema) -class CannotConnect(exceptions.HomeAssistantError): +class CannotConnect(HomeAssistantError): """Error to indicate we cannot connect.""" -class InvalidAuth(exceptions.HomeAssistantError): +class InvalidAuth(HomeAssistantError): """Error to indicate there is invalid auth.""" diff --git a/homeassistant/components/rachio/const.py b/homeassistant/components/rachio/const.py index dad044e5049..22c92be2b74 100644 --- a/homeassistant/components/rachio/const.py +++ b/homeassistant/components/rachio/const.py @@ -26,6 +26,7 @@ KEY_NAME = "name" KEY_MODEL = "model" KEY_ON = "on" KEY_DURATION = "totalDuration" +KEY_DURATION_MINUTES = "duration" KEY_RAIN_DELAY = "rainDelayExpirationDate" KEY_RAIN_DELAY_END = "endTime" KEY_RAIN_SENSOR_TRIPPED = "rainSensorTripped" @@ -47,6 +48,21 @@ KEY_CUSTOM_SHADE = "customShade" KEY_CUSTOM_CROP = "customCrop" KEY_CUSTOM_SLOPE = "customSlope" +# Smart Hose timer +KEY_BASE_STATIONS = "baseStations" +KEY_VALVES = "valves" +KEY_REPORTED_STATE = "reportedState" +KEY_STATE = "state" +KEY_CONNECTED = "connected" +KEY_CURRENT_STATUS = "lastWateringAction" +KEY_DETECT_FLOW = "detectFlow" +KEY_BATTERY_STATUS = "batteryStatus" +KEY_REASON = "reason" +KEY_DEFAULT_RUNTIME = "defaultRuntimeSeconds" +KEY_DURATION_SECONDS = "durationSeconds" +KEY_FLOW_DETECTED = "flowDetected" +KEY_START_TIME = "start" + STATUS_ONLINE = "ONLINE" MODEL_GENERATION_1 = "GENERATION1" @@ -56,6 +72,7 @@ SERVICE_PAUSE_WATERING = "pause_watering" SERVICE_RESUME_WATERING = "resume_watering" SERVICE_STOP_WATERING = "stop_watering" SERVICE_SET_ZONE_MOISTURE = "set_zone_moisture_percent" +SERVICE_START_WATERING = "start_watering" SERVICE_START_MULTIPLE_ZONES = "start_multiple_zone_schedule" SIGNAL_RACHIO_UPDATE = f"{DOMAIN}_update" diff --git a/homeassistant/components/rachio/coordinator.py b/homeassistant/components/rachio/coordinator.py new file mode 100644 index 00000000000..4f8cc87daef --- /dev/null +++ b/homeassistant/components/rachio/coordinator.py @@ -0,0 +1,56 @@ +"""Coordinator object for the Rachio integration.""" + +from datetime import timedelta +import logging +from typing import Any + +from rachiopy import Rachio +from requests.exceptions import Timeout + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.debounce import Debouncer +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DOMAIN, KEY_ID, KEY_VALVES + +_LOGGER = logging.getLogger(__name__) + +UPDATE_DELAY_TIME = 8 + + +class RachioUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): + """Coordinator Class for Rachio Hose Timers.""" + + def __init__( + self, + hass: HomeAssistant, + rachio: Rachio, + base_station, + base_count: int, + ) -> None: + """Initialize the Rachio Update Coordinator.""" + self.hass = hass + self.rachio = rachio + self.base_station = base_station + super().__init__( + hass, + _LOGGER, + name=f"{DOMAIN} update coordinator", + # To avoid exceeding the rate limit, increase polling interval for + # each additional base station on the account + update_interval=timedelta(minutes=(base_count + 1)), + # Debouncer used because the API takes a bit to update state changes + request_refresh_debouncer=Debouncer( + hass, _LOGGER, cooldown=UPDATE_DELAY_TIME, immediate=False + ), + ) + + async def _async_update_data(self) -> dict[str, Any]: + """Update smart hose timer data.""" + try: + data = await self.hass.async_add_executor_job( + self.rachio.valve.list_valves, self.base_station[KEY_ID] + ) + except Timeout as err: + raise UpdateFailed(f"Could not connect to the Rachio API: {err}") from err + return {valve[KEY_ID]: valve for valve in data[1][KEY_VALVES]} diff --git a/homeassistant/components/rachio/device.py b/homeassistant/components/rachio/device.py index 485a726acd0..c018d7e6f86 100644 --- a/homeassistant/components/rachio/device.py +++ b/homeassistant/components/rachio/device.py @@ -1,4 +1,5 @@ """Adapter to wrap the rachiopy api for home assistant.""" + from __future__ import annotations from http import HTTPStatus @@ -16,6 +17,7 @@ from homeassistant.helpers import config_validation as cv from .const import ( DOMAIN, + KEY_BASE_STATIONS, KEY_DEVICES, KEY_ENABLED, KEY_EXTERNAL_ID, @@ -36,6 +38,7 @@ from .const import ( SERVICE_STOP_WATERING, WEBHOOK_CONST_ID, ) +from .coordinator import RachioUpdateCoordinator _LOGGER = logging.getLogger(__name__) @@ -66,6 +69,7 @@ class RachioPerson: self.username = None self._id: str | None = None self._controllers: list[RachioIro] = [] + self._base_stations: list[RachioBaseStation] = [] async def async_setup(self, hass: HomeAssistant) -> None: """Create rachio devices and services.""" @@ -77,30 +81,34 @@ class RachioPerson: can_pause = True break - all_devices = [rachio_iro.name for rachio_iro in self._controllers] + all_controllers = [rachio_iro.name for rachio_iro in self._controllers] def pause_water(service: ServiceCall) -> None: """Service to pause watering on all or specific controllers.""" duration = service.data[ATTR_DURATION] - devices = service.data.get(ATTR_DEVICES, all_devices) + devices = service.data.get(ATTR_DEVICES, all_controllers) for iro in self._controllers: if iro.name in devices: iro.pause_watering(duration) def resume_water(service: ServiceCall) -> None: """Service to resume watering on all or specific controllers.""" - devices = service.data.get(ATTR_DEVICES, all_devices) + devices = service.data.get(ATTR_DEVICES, all_controllers) for iro in self._controllers: if iro.name in devices: iro.resume_watering() def stop_water(service: ServiceCall) -> None: """Service to stop watering on all or specific controllers.""" - devices = service.data.get(ATTR_DEVICES, all_devices) + devices = service.data.get(ATTR_DEVICES, all_controllers) for iro in self._controllers: if iro.name in devices: iro.stop_watering() + # If only hose timers on account, none of these services apply + if not all_controllers: + return + hass.services.async_register( DOMAIN, SERVICE_STOP_WATERING, @@ -144,6 +152,9 @@ class RachioPerson: raise ConfigEntryNotReady(f"API Error: {data}") self.username = data[1][KEY_USERNAME] devices: list[dict[str, Any]] = data[1][KEY_DEVICES] + base_station_data = rachio.valve.list_base_stations(self._id) + base_stations: list[dict[str, Any]] = base_station_data[1][KEY_BASE_STATIONS] + for controller in devices: webhooks = rachio.notification.get_device_webhook(controller[KEY_ID])[1] # The API does not provide a way to tell if a controller is shared @@ -173,6 +184,14 @@ class RachioPerson: rachio_iro.setup() self._controllers.append(rachio_iro) + base_count = len(base_stations) + self._base_stations.extend( + RachioBaseStation( + rachio, base, RachioUpdateCoordinator(hass, rachio, base, base_count) + ) + for base in base_stations + ) + _LOGGER.info('Using Rachio API as user "%s"', self.username) @property @@ -185,6 +204,11 @@ class RachioPerson: """Get a list of controllers managed by this account.""" return self._controllers + @property + def base_stations(self) -> list[RachioBaseStation]: + """List of smart hose timer base stations.""" + return self._base_stations + def start_multiple_zones(self, zones) -> None: """Start multiple zones.""" self.rachio.zone.start_multiple(zones) @@ -244,10 +268,11 @@ class RachioIro: _deinit_webhooks(None) # Choose which events to listen for and get their IDs - event_types = [] - for event_type in self.rachio.notification.get_webhook_event_type()[1]: - if event_type[KEY_NAME] in LISTEN_EVENT_TYPES: - event_types.append({"id": event_type[KEY_ID]}) + event_types = [ + {"id": event_type[KEY_ID]} + for event_type in self.rachio.notification.get_webhook_event_type()[1] + if event_type[KEY_NAME] in LISTEN_EVENT_TYPES + ] # Register to listen to these events from the device url = self.rachio.webhook_url @@ -319,6 +344,28 @@ class RachioIro: _LOGGER.debug("Resuming watering on %s", self) +class RachioBaseStation: + """Represent a smart hose timer base station.""" + + def __init__( + self, rachio: Rachio, data: dict[str, Any], coordinator: RachioUpdateCoordinator + ) -> None: + """Initialize a hose time base station.""" + self.rachio = rachio + self._id = data[KEY_ID] + self.serial_number = data[KEY_SERIAL_NUMBER] + self.mac_address = data[KEY_MAC_ADDRESS] + self.coordinator = coordinator + + def start_watering(self, valve_id: str, duration: int) -> None: + """Start watering on this valve.""" + self.rachio.valve.start_watering(valve_id, duration) + + def stop_watering(self, valve_id: str) -> None: + """Stop watering on this valve.""" + self.rachio.valve.stop_watering(valve_id) + + def is_invalid_auth_code(http_status_code: int) -> bool: """HTTP status codes that mean invalid auth.""" return http_status_code in (HTTPStatus.UNAUTHORIZED, HTTPStatus.FORBIDDEN) diff --git a/homeassistant/components/rachio/icons.json b/homeassistant/components/rachio/icons.json new file mode 100644 index 00000000000..dfab8788fc8 --- /dev/null +++ b/homeassistant/components/rachio/icons.json @@ -0,0 +1,20 @@ +{ + "entity": { + "switch": { + "standby": { + "default": "mdi:power" + }, + "rain_delay": { + "default": "mdi:camera-timer" + } + } + }, + "services": { + "set_zone_moisture_percent": "mdi:water-percent", + "start_multiple_zone_schedule": "mdi:play", + "pause_watering": "mdi:pause", + "resume_watering": "mdi:play", + "stop_watering": "mdi:stop", + "start_watering": "mdi:water" + } +} diff --git a/homeassistant/components/rachio/services.yaml b/homeassistant/components/rachio/services.yaml index 6a6a8bf5cf6..72582de870a 100644 --- a/homeassistant/components/rachio/services.yaml +++ b/homeassistant/components/rachio/services.yaml @@ -11,6 +11,17 @@ set_zone_moisture_percent: min: 0 max: 100 unit_of_measurement: "%" +start_watering: + target: + entity: + integration: rachio + domain: switch + fields: + duration: + example: 15 + required: false + selector: + object: start_multiple_zone_schedule: target: entity: diff --git a/homeassistant/components/rachio/strings.json b/homeassistant/components/rachio/strings.json index 560c300db17..2e4de262d21 100644 --- a/homeassistant/components/rachio/strings.json +++ b/homeassistant/components/rachio/strings.json @@ -63,6 +63,16 @@ } } }, + "start_watering": { + "name": "Start watering", + "description": "Start a single zone, a schedule or any number of smart hose timers.", + "fields": { + "duration": { + "name": "Duration", + "description": "Number of minutes to run. For sprinkler zones the maximum duration is 3 hours, or 24 hours for smart hose timers. Leave empty for schedules." + } + } + }, "pause_watering": { "name": "Pause watering", "description": "Pause any currently running zones or schedules.", diff --git a/homeassistant/components/rachio/switch.py b/homeassistant/components/rachio/switch.py index bbb08f6d46f..fe3d455df3c 100644 --- a/homeassistant/components/rachio/switch.py +++ b/homeassistant/components/rachio/switch.py @@ -1,4 +1,5 @@ """Integration with the Rachio Iro sprinkler system controller.""" + from abc import abstractmethod from contextlib import suppress from datetime import timedelta @@ -10,19 +11,28 @@ import voluptuous as vol from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ENTITY_ID, ATTR_ID -from homeassistant.core import HomeAssistant, ServiceCall, callback +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, ServiceCall, callback from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import config_validation as cv, entity_platform +from homeassistant.helpers import ( + config_validation as cv, + device_registry as dr, + entity_platform, +) +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_point_in_utc_time +from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util.dt import as_timestamp, now, parse_datetime, utc_from_timestamp from .const import ( CONF_MANUAL_RUN_MINS, DEFAULT_MANUAL_RUN_MINS, + DEFAULT_NAME, DOMAIN as DOMAIN_RACHIO, + KEY_CONNECTED, + KEY_CURRENT_STATUS, KEY_CUSTOM_CROP, KEY_CUSTOM_SHADE, KEY_CUSTOM_SLOPE, @@ -35,7 +45,9 @@ from .const import ( KEY_ON, KEY_RAIN_DELAY, KEY_RAIN_DELAY_END, + KEY_REPORTED_STATE, KEY_SCHEDULE_ID, + KEY_STATE, KEY_SUBTYPE, KEY_SUMMARY, KEY_TYPE, @@ -45,6 +57,7 @@ from .const import ( SCHEDULE_TYPE_FLEX, SERVICE_SET_ZONE_MOISTURE, SERVICE_START_MULTIPLE_ZONES, + SERVICE_START_WATERING, SIGNAL_RACHIO_CONTROLLER_UPDATE, SIGNAL_RACHIO_RAIN_DELAY_UPDATE, SIGNAL_RACHIO_SCHEDULE_UPDATE, @@ -54,6 +67,7 @@ from .const import ( SLOPE_SLIGHT, SLOPE_STEEP, ) +from .coordinator import RachioUpdateCoordinator from .device import RachioPerson from .entity import RachioDevice from .webhooks import ( @@ -79,6 +93,7 @@ ATTR_SCHEDULE_ENABLED = "Enabled" ATTR_SCHEDULE_DURATION = "Duration" ATTR_SCHEDULE_TYPE = "Type" ATTR_SORT_ORDER = "sortOrder" +ATTR_WATERING_DURATION = "Watering Duration seconds" ATTR_ZONE_NUMBER = "Zone number" ATTR_ZONE_SHADE = "Shade" ATTR_ZONE_SLOPE = "Slope" @@ -140,6 +155,19 @@ async def async_setup_entry( else: raise HomeAssistantError("No matching zones found in given entity_ids") + platform = entity_platform.async_get_current_platform() + platform.async_register_entity_service( + SERVICE_START_WATERING, + { + vol.Optional(ATTR_DURATION): cv.positive_int, + }, + "turn_on", + ) + + # If only hose timers on account, none of these services apply + if not zone_entities: + return + hass.services.async_register( DOMAIN_RACHIO, SERVICE_START_MULTIPLE_ZONES, @@ -168,10 +196,18 @@ def _create_entities(hass: HomeAssistant, config_entry: ConfigEntry) -> list[Ent schedules = controller.list_schedules() flex_schedules = controller.list_flex_schedules() current_schedule = controller.current_schedule - for zone in zones: - entities.append(RachioZone(person, controller, zone, current_schedule)) - for sched in schedules + flex_schedules: - entities.append(RachioSchedule(person, controller, sched, current_schedule)) + entities.extend( + RachioZone(person, controller, zone, current_schedule) for zone in zones + ) + entities.extend( + RachioSchedule(person, controller, schedule, current_schedule) + for schedule in schedules + flex_schedules + ) + entities.extend( + RachioValve(person, base_station, valve, base_station.coordinator) + for base_station in person.base_stations + for valve in base_station.coordinator.data.values() + ) return entities @@ -198,7 +234,6 @@ class RachioStandbySwitch(RachioSwitch): _attr_has_entity_name = True _attr_translation_key = "standby" - _attr_icon = "mdi:power" @property def unique_id(self) -> str: @@ -242,11 +277,10 @@ class RachioRainDelay(RachioSwitch): _attr_has_entity_name = True _attr_translation_key = "rain_delay" - _attr_icon = "mdi:camera-timer" - def __init__(self, controller): + def __init__(self, controller) -> None: """Set up a Rachio rain delay switch.""" - self._cancel_update = None + self._cancel_update: CALLBACK_TYPE | None = None super().__init__(controller) @property @@ -322,7 +356,7 @@ class RachioZone(RachioSwitch): _attr_icon = "mdi:water" - def __init__(self, person, controller, data, current_schedule): + def __init__(self, person, controller, data, current_schedule) -> None: """Initialize a new Rachio Zone.""" self.id = data[KEY_ID] self._attr_name = data[KEY_NAME] @@ -377,11 +411,14 @@ class RachioZone(RachioSwitch): self.turn_off() # Start this zone - manual_run_time = timedelta( - minutes=self._person.config_entry.options.get( - CONF_MANUAL_RUN_MINS, DEFAULT_MANUAL_RUN_MINS + if ATTR_DURATION in kwargs: + manual_run_time = timedelta(minutes=kwargs[ATTR_DURATION]) + else: + manual_run_time = timedelta( + minutes=self._person.config_entry.options.get( + CONF_MANUAL_RUN_MINS, DEFAULT_MANUAL_RUN_MINS + ) ) - ) # The API limit is 3 hours, and requires an int be passed self._controller.rachio.zone.start(self.zone_id, manual_run_time.seconds) _LOGGER.debug( @@ -433,7 +470,7 @@ class RachioZone(RachioSwitch): class RachioSchedule(RachioSwitch): """Representation of one fixed schedule on the Rachio Iro.""" - def __init__(self, person, controller, data, current_schedule): + def __init__(self, person, controller, data, current_schedule) -> None: """Initialize a new Rachio Schedule.""" self._schedule_id = data[KEY_ID] self._duration = data[KEY_DURATION] @@ -507,3 +544,70 @@ class RachioSchedule(RachioSwitch): self.hass, SIGNAL_RACHIO_SCHEDULE_UPDATE, self._async_handle_update ) ) + + +class RachioValve(CoordinatorEntity[RachioUpdateCoordinator], SwitchEntity): + """Representation of one smart hose timer valve.""" + + def __init__( + self, person, base, data, coordinator: RachioUpdateCoordinator + ) -> None: + """Initialize a new smart hose valve.""" + super().__init__(coordinator) + self._person = person + self._base = base + self.id = data[KEY_ID] + self._attr_name = data[KEY_NAME] + self._attr_unique_id = f"{self.id}-valve" + self._static_attrs = data[KEY_STATE][KEY_REPORTED_STATE] + self._attr_is_on = KEY_CURRENT_STATUS in self._static_attrs + self._attr_device_info = DeviceInfo( + identifiers={ + ( + DOMAIN_RACHIO, + self.id, + ) + }, + connections={(dr.CONNECTION_NETWORK_MAC, self._base.mac_address)}, + manufacturer=DEFAULT_NAME, + model="Smart Hose Timer", + name=self._attr_name, + configuration_url="https://app.rach.io", + ) + + @property + def available(self) -> bool: + """Return if the valve is available.""" + return super().available and self._static_attrs[KEY_CONNECTED] + + def turn_on(self, **kwargs: Any) -> None: + """Turn on this valve.""" + if ATTR_DURATION in kwargs: + manual_run_time = timedelta(minutes=kwargs[ATTR_DURATION]) + else: + manual_run_time = timedelta( + minutes=self._person.config_entry.options.get( + CONF_MANUAL_RUN_MINS, DEFAULT_MANUAL_RUN_MINS + ) + ) + + self._base.start_watering(self.id, manual_run_time.seconds) + self._attr_is_on = True + self.schedule_update_ha_state(force_refresh=True) + _LOGGER.debug("Starting valve %s for %s", self.name, str(manual_run_time)) + + def turn_off(self, **kwargs: Any) -> None: + """Turn off this valve.""" + self._base.stop_watering(self.id) + self._attr_is_on = False + self.schedule_update_ha_state(force_refresh=True) + _LOGGER.debug("Stopping watering on valve %s", self.name) + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated coordinator data.""" + data = self.coordinator.data[self.id] + + self._static_attrs = data[KEY_STATE][KEY_REPORTED_STATE] + self._attr_is_on = KEY_CURRENT_STATUS in self._static_attrs + super()._handle_coordinator_update() diff --git a/homeassistant/components/rachio/webhooks.py b/homeassistant/components/rachio/webhooks.py index 298b9c03701..06cd0941dcc 100644 --- a/homeassistant/components/rachio/webhooks.py +++ b/homeassistant/components/rachio/webhooks.py @@ -1,4 +1,5 @@ """Webhooks used by rachio.""" + from __future__ import annotations from aiohttp import web diff --git a/homeassistant/components/radarr/__init__.py b/homeassistant/components/radarr/__init__.py index b6b05b5b568..d3e44e6b7fc 100644 --- a/homeassistant/components/radarr/__init__.py +++ b/homeassistant/components/radarr/__init__.py @@ -1,4 +1,5 @@ """The Radarr component.""" + from __future__ import annotations from typing import Any, cast diff --git a/homeassistant/components/radarr/binary_sensor.py b/homeassistant/components/radarr/binary_sensor.py index 5d439680bc2..4962ef81614 100644 --- a/homeassistant/components/radarr/binary_sensor.py +++ b/homeassistant/components/radarr/binary_sensor.py @@ -1,4 +1,5 @@ """Support for Radarr binary sensors.""" + from __future__ import annotations from aiopyarr import Health diff --git a/homeassistant/components/radarr/calendar.py b/homeassistant/components/radarr/calendar.py index 3a5308fffd5..ad5e1b8ffd9 100644 --- a/homeassistant/components/radarr/calendar.py +++ b/homeassistant/components/radarr/calendar.py @@ -1,4 +1,5 @@ """Support for Radarr calendar items.""" + from __future__ import annotations from datetime import datetime diff --git a/homeassistant/components/radarr/config_flow.py b/homeassistant/components/radarr/config_flow.py index 3feb9a01bea..81589c5fe30 100644 --- a/homeassistant/components/radarr/config_flow.py +++ b/homeassistant/components/radarr/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Radarr.""" + from __future__ import annotations from collections.abc import Mapping @@ -10,10 +11,9 @@ from aiopyarr.models.host_configuration import PyArrHostConfiguration from aiopyarr.radarr_client import RadarrClient import voluptuous as vol -from homeassistant.config_entries import ConfigEntry, ConfigFlow +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_API_KEY, CONF_URL, CONF_VERIFY_SSL from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DEFAULT_NAME, DEFAULT_URL, DOMAIN @@ -28,7 +28,7 @@ class RadarrConfigFlow(ConfigFlow, domain=DOMAIN): """Initialize the flow.""" self.entry: ConfigEntry | None = None - async def async_step_reauth(self, _: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth(self, _: Mapping[str, Any]) -> ConfigFlowResult: """Handle configuration by re-auth.""" self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) @@ -36,7 +36,7 @@ class RadarrConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_reauth_confirm( self, user_input: dict[str, str] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm reauth dialog.""" if user_input is not None: return await self.async_step_user() @@ -46,7 +46,7 @@ class RadarrConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initiated by the user.""" errors = {} diff --git a/homeassistant/components/radarr/const.py b/homeassistant/components/radarr/const.py index 37388dd51ef..ef3b29af4e5 100644 --- a/homeassistant/components/radarr/const.py +++ b/homeassistant/components/radarr/const.py @@ -1,4 +1,5 @@ """Constants for Radarr.""" + import logging from typing import Final diff --git a/homeassistant/components/radarr/coordinator.py b/homeassistant/components/radarr/coordinator.py index 7f395169644..0580fdcc020 100644 --- a/homeassistant/components/radarr/coordinator.py +++ b/homeassistant/components/radarr/coordinator.py @@ -1,4 +1,5 @@ """Data update coordinator for the Radarr integration.""" + from __future__ import annotations from abc import ABC, abstractmethod diff --git a/homeassistant/components/radarr/icons.json b/homeassistant/components/radarr/icons.json new file mode 100644 index 00000000000..ff31d936ae5 --- /dev/null +++ b/homeassistant/components/radarr/icons.json @@ -0,0 +1,12 @@ +{ + "entity": { + "sensor": { + "movies": { + "default": "mdi:television" + }, + "queue": { + "default": "mdi:download" + } + } + } +} diff --git a/homeassistant/components/radarr/sensor.py b/homeassistant/components/radarr/sensor.py index ad9dd4e1ae0..e6700fb3637 100644 --- a/homeassistant/components/radarr/sensor.py +++ b/homeassistant/components/radarr/sensor.py @@ -1,4 +1,5 @@ """Support for Radarr.""" + from __future__ import annotations from collections.abc import Callable @@ -60,10 +61,13 @@ class RadarrSensorEntityDescription( ): """Class to describe a Radarr sensor.""" - description_fn: Callable[ - [RadarrSensorEntityDescription[T], RootFolder], - tuple[RadarrSensorEntityDescription[T], str] | None, - ] | None = None + description_fn: ( + Callable[ + [RadarrSensorEntityDescription[T], RootFolder], + tuple[RadarrSensorEntityDescription[T], str] | None, + ] + | None + ) = None SENSOR_TYPES: dict[str, RadarrSensorEntityDescription[Any]] = { @@ -80,7 +84,6 @@ SENSOR_TYPES: dict[str, RadarrSensorEntityDescription[Any]] = { key="movies", translation_key="movies", native_unit_of_measurement="Movies", - icon="mdi:television", entity_registry_enabled_default=False, value_fn=lambda data, _: data, ), @@ -88,7 +91,6 @@ SENSOR_TYPES: dict[str, RadarrSensorEntityDescription[Any]] = { key="queue", translation_key="queue", native_unit_of_measurement="Movies", - icon="mdi:download", entity_registry_enabled_default=False, state_class=SensorStateClass.TOTAL, value_fn=lambda data, _: data, diff --git a/homeassistant/components/radio_browser/__init__.py b/homeassistant/components/radio_browser/__init__.py index fdd7537e9e1..d1c2db3543a 100644 --- a/homeassistant/components/radio_browser/__init__.py +++ b/homeassistant/components/radio_browser/__init__.py @@ -1,4 +1,5 @@ """The Radio Browser integration.""" + from __future__ import annotations from aiodns.error import DNSError diff --git a/homeassistant/components/radio_browser/config_flow.py b/homeassistant/components/radio_browser/config_flow.py index 1c6964d0715..137ee7c8e87 100644 --- a/homeassistant/components/radio_browser/config_flow.py +++ b/homeassistant/components/radio_browser/config_flow.py @@ -1,10 +1,10 @@ """Config flow for Radio Browser integration.""" + from __future__ import annotations from typing import Any -from homeassistant.config_entries import ConfigFlow -from homeassistant.data_entry_flow import FlowResult +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from .const import DOMAIN @@ -16,7 +16,7 @@ class RadioBrowserConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") @@ -28,6 +28,6 @@ class RadioBrowserConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_onboarding( self, data: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by onboarding.""" return self.async_create_entry(title="Radio Browser", data={}) diff --git a/homeassistant/components/radio_browser/const.py b/homeassistant/components/radio_browser/const.py index eb456db08e8..206cc3de933 100644 --- a/homeassistant/components/radio_browser/const.py +++ b/homeassistant/components/radio_browser/const.py @@ -1,4 +1,5 @@ """Constants for the Radio Browser integration.""" + import logging from typing import Final diff --git a/homeassistant/components/radio_browser/manifest.json b/homeassistant/components/radio_browser/manifest.json index e8e03fd1828..4192805ec62 100644 --- a/homeassistant/components/radio_browser/manifest.json +++ b/homeassistant/components/radio_browser/manifest.json @@ -4,8 +4,7 @@ "codeowners": ["@frenck"], "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/radio_browser", - "import_executor": true, "integration_type": "service", "iot_class": "cloud_polling", - "requirements": ["radios==0.2.0"] + "requirements": ["radios==0.3.1"] } diff --git a/homeassistant/components/radio_browser/media_source.py b/homeassistant/components/radio_browser/media_source.py index dffbdc42dbe..5bf0b7f491b 100644 --- a/homeassistant/components/radio_browser/media_source.py +++ b/homeassistant/components/radio_browser/media_source.py @@ -1,4 +1,5 @@ """Expose Radio Browser as a media source.""" + from __future__ import annotations import mimetypes diff --git a/homeassistant/components/radiotherm/__init__.py b/homeassistant/components/radiotherm/__init__.py index 86a9fe58013..d5f1e4c076c 100644 --- a/homeassistant/components/radiotherm/__init__.py +++ b/homeassistant/components/radiotherm/__init__.py @@ -1,4 +1,5 @@ """The radiotherm component.""" + from __future__ import annotations from collections.abc import Coroutine diff --git a/homeassistant/components/radiotherm/climate.py b/homeassistant/components/radiotherm/climate.py index 4ab57fd6821..73ab3644a0b 100644 --- a/homeassistant/components/radiotherm/climate.py +++ b/homeassistant/components/radiotherm/climate.py @@ -1,4 +1,5 @@ """Support for Radio Thermostat wifi-enabled home thermostats.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/radiotherm/config_flow.py b/homeassistant/components/radiotherm/config_flow.py index c370cc86484..a8de05d9963 100644 --- a/homeassistant/components/radiotherm/config_flow.py +++ b/homeassistant/components/radiotherm/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Radio Thermostat integration.""" + from __future__ import annotations import logging @@ -8,11 +9,10 @@ from urllib.error import URLError from radiotherm.validate import RadiothermTstatError import voluptuous as vol -from homeassistant import config_entries from homeassistant.components import dhcp +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError from .const import DOMAIN @@ -33,7 +33,7 @@ async def validate_connection(hass: HomeAssistant, host: str) -> RadioThermInitD raise CannotConnect(f"Failed to connect to {host}: {ex}") from ex -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class RadioThermConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Radio Thermostat.""" VERSION = 1 @@ -43,7 +43,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.discovered_ip: str | None = None self.discovered_init_data: RadioThermInitData | None = None - async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: + async def async_step_dhcp( + self, discovery_info: dhcp.DhcpServiceInfo + ) -> ConfigFlowResult: """Discover via DHCP.""" self._async_abort_entries_match({CONF_HOST: discovery_info.ip}) try: @@ -84,7 +86,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors = {} if user_input is not None: diff --git a/homeassistant/components/radiotherm/coordinator.py b/homeassistant/components/radiotherm/coordinator.py index 5b0161d9f22..06e3554c8d7 100644 --- a/homeassistant/components/radiotherm/coordinator.py +++ b/homeassistant/components/radiotherm/coordinator.py @@ -1,4 +1,5 @@ """Coordinator for radiotherm.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/radiotherm/data.py b/homeassistant/components/radiotherm/data.py index 3aa4e6b7631..4803cacd84b 100644 --- a/homeassistant/components/radiotherm/data.py +++ b/homeassistant/components/radiotherm/data.py @@ -1,4 +1,5 @@ """The radiotherm component data.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/homeassistant/components/radiotherm/icons.json b/homeassistant/components/radiotherm/icons.json new file mode 100644 index 00000000000..be955c5dcf0 --- /dev/null +++ b/homeassistant/components/radiotherm/icons.json @@ -0,0 +1,12 @@ +{ + "entity": { + "switch": { + "hold": { + "default": "mdi:timer-off", + "state": { + "on": "mdi:timer" + } + } + } + } +} diff --git a/homeassistant/components/radiotherm/switch.py b/homeassistant/components/radiotherm/switch.py index 3b71baffec6..e7b463e3def 100644 --- a/homeassistant/components/radiotherm/switch.py +++ b/homeassistant/components/radiotherm/switch.py @@ -1,4 +1,5 @@ """Support for radiotherm switches.""" + from __future__ import annotations from typing import Any @@ -35,11 +36,6 @@ class RadioThermHoldSwitch(RadioThermostatEntity, SwitchEntity): super().__init__(coordinator) self._attr_unique_id = f"{coordinator.init_data.mac}_hold" - @property - def icon(self) -> str: - """Return the icon for the switch.""" - return "mdi:timer-off" if self.is_on else "mdi:timer" - @callback def _process_data(self) -> None: """Update and validate the data from the thermostat.""" diff --git a/homeassistant/components/radiotherm/util.py b/homeassistant/components/radiotherm/util.py index 85b927d7935..fb15531987a 100644 --- a/homeassistant/components/radiotherm/util.py +++ b/homeassistant/components/radiotherm/util.py @@ -1,4 +1,5 @@ """Utils for radiotherm.""" + from __future__ import annotations from radiotherm.thermostat import CommonThermostat diff --git a/homeassistant/components/rainbird/__init__.py b/homeassistant/components/rainbird/__init__.py index 2a660435e17..da2a0e4b475 100644 --- a/homeassistant/components/rainbird/__init__.py +++ b/homeassistant/components/rainbird/__init__.py @@ -1,13 +1,22 @@ """Support for Rain Bird Irrigation system LNK WiFi Module.""" + from __future__ import annotations import logging +from typing import Any +import aiohttp from pyrainbird.async_client import AsyncRainbirdClient, AsyncRainbirdController from pyrainbird.exceptions import RainbirdApiException from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PASSWORD, Platform +from homeassistant.const import ( + CONF_HOST, + CONF_MAC, + CONF_PASSWORD, + EVENT_HOMEASSISTANT_CLOSE, + Platform, +) from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr, entity_registry as er @@ -30,13 +39,30 @@ PLATFORMS = [ DOMAIN = "rainbird" +def _async_register_clientsession_shutdown( + hass: HomeAssistant, entry: ConfigEntry, clientsession: aiohttp.ClientSession +) -> None: + """Register cleanup hooks for the clientsession.""" + + async def _async_close_websession(*_: Any) -> None: + """Close websession.""" + await clientsession.close() + + unsub = hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_CLOSE, _async_close_websession + ) + entry.async_on_unload(unsub) + entry.async_on_unload(_async_close_websession) + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up the config entry for Rain Bird.""" hass.data.setdefault(DOMAIN, {}) clientsession = async_create_clientsession() - entry.async_on_unload(clientsession.close) + _async_register_clientsession_shutdown(hass, entry, clientsession) + controller = AsyncRainbirdController( AsyncRainbirdClient( clientsession, diff --git a/homeassistant/components/rainbird/binary_sensor.py b/homeassistant/components/rainbird/binary_sensor.py index 142e8ecc4b8..d44022b0a2d 100644 --- a/homeassistant/components/rainbird/binary_sensor.py +++ b/homeassistant/components/rainbird/binary_sensor.py @@ -1,4 +1,5 @@ """Support for Rain Bird Irrigation system LNK WiFi Module.""" + from __future__ import annotations import logging @@ -21,7 +22,6 @@ _LOGGER = logging.getLogger(__name__) RAIN_SENSOR_ENTITY_DESCRIPTION = BinarySensorEntityDescription( key="rainsensor", translation_key="rainsensor", - icon="mdi:water", ) diff --git a/homeassistant/components/rainbird/calendar.py b/homeassistant/components/rainbird/calendar.py index 2001a14ac93..85906fa3fe3 100644 --- a/homeassistant/components/rainbird/calendar.py +++ b/homeassistant/components/rainbird/calendar.py @@ -49,7 +49,7 @@ class RainBirdCalendarEntity( _attr_has_entity_name = True _attr_name: str | None = None - _attr_icon = "mdi:sprinkler" + _attr_translation_key = "calendar" def __init__( self, diff --git a/homeassistant/components/rainbird/config_flow.py b/homeassistant/components/rainbird/config_flow.py index a4fceceede9..44576db8a33 100644 --- a/homeassistant/components/rainbird/config_flow.py +++ b/homeassistant/components/rainbird/config_flow.py @@ -14,11 +14,14 @@ from pyrainbird.async_client import ( from pyrainbird.data import WifiParams import voluptuous as vol -from homeassistant import config_entries -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PASSWORD from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_validation as cv, selector from homeassistant.helpers.device_registry import format_mac @@ -53,7 +56,7 @@ class ConfigFlowError(Exception): self.error_code = error_code -class RainbirdConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class RainbirdConfigFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a config flow for Rain Bird.""" @staticmethod @@ -66,7 +69,7 @@ class RainbirdConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Configure the Rain Bird device.""" error_code: str | None = None if user_input: @@ -132,7 +135,7 @@ class RainbirdConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self, data: dict[str, Any], options: dict[str, Any], - ) -> FlowResult: + ) -> ConfigFlowResult: """Create the config entry.""" # The integration has historically used a serial number, but not all devices # historically had a valid one. Now the mac address is used as a unique id @@ -159,7 +162,7 @@ class RainbirdConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) -class RainBirdOptionsFlowHandler(config_entries.OptionsFlow): +class RainBirdOptionsFlowHandler(OptionsFlow): """Handle a RainBird options flow.""" def __init__(self, config_entry: ConfigEntry) -> None: @@ -168,7 +171,7 @@ class RainBirdOptionsFlowHandler(config_entries.OptionsFlow): async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Manage the options.""" if user_input is not None: return self.async_create_entry(data=user_input) diff --git a/homeassistant/components/rainbird/icons.json b/homeassistant/components/rainbird/icons.json new file mode 100644 index 00000000000..79d2256f184 --- /dev/null +++ b/homeassistant/components/rainbird/icons.json @@ -0,0 +1,28 @@ +{ + "entity": { + "binary_sensor": { + "rainsensor": { + "default": "mdi:water" + } + }, + "calendar": { + "calendar": { + "default": "mdi:sprinkler" + } + }, + "number": { + "rain_delay": { + "default": "mdi:water-off" + } + }, + "sensor": { + "raindelay": { + "default": "mdi:water-off" + } + } + }, + "services": { + "start_irrigation": "mdi:water", + "set_rain_delay": "mdi:water-sync" + } +} diff --git a/homeassistant/components/rainbird/number.py b/homeassistant/components/rainbird/number.py index dd9664222b2..507a31e59a4 100644 --- a/homeassistant/components/rainbird/number.py +++ b/homeassistant/components/rainbird/number.py @@ -1,4 +1,5 @@ """The number platform for rainbird.""" + from __future__ import annotations import logging @@ -41,7 +42,6 @@ class RainDelayNumber(CoordinatorEntity[RainbirdUpdateCoordinator], NumberEntity _attr_native_max_value = 14 _attr_native_step = 1 _attr_native_unit_of_measurement = UnitOfTime.DAYS - _attr_icon = "mdi:water-off" _attr_translation_key = "rain_delay" _attr_has_entity_name = True diff --git a/homeassistant/components/rainbird/sensor.py b/homeassistant/components/rainbird/sensor.py index 84bf8cadb7b..649d643a20c 100644 --- a/homeassistant/components/rainbird/sensor.py +++ b/homeassistant/components/rainbird/sensor.py @@ -1,4 +1,5 @@ """Support for Rain Bird Irrigation system LNK Wi-Fi Module.""" + from __future__ import annotations import logging @@ -19,7 +20,6 @@ _LOGGER = logging.getLogger(__name__) RAIN_DELAY_ENTITY_DESCRIPTION = SensorEntityDescription( key="raindelay", translation_key="raindelay", - icon="mdi:water-off", ) diff --git a/homeassistant/components/rainbird/switch.py b/homeassistant/components/rainbird/switch.py index 810a6fbb721..a929f5b875b 100644 --- a/homeassistant/components/rainbird/switch.py +++ b/homeassistant/components/rainbird/switch.py @@ -1,4 +1,5 @@ """Support for Rain Bird Irrigation system LNK Wi-Fi Module.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/raincloud/__init__.py b/homeassistant/components/raincloud/__init__.py index 1214fd9416f..e6f5d2ecf8d 100644 --- a/homeassistant/components/raincloud/__init__.py +++ b/homeassistant/components/raincloud/__init__.py @@ -1,4 +1,5 @@ """Support for Melnor RainCloud sprinkler water timer.""" + from datetime import timedelta import logging diff --git a/homeassistant/components/raincloud/binary_sensor.py b/homeassistant/components/raincloud/binary_sensor.py index b3766cd02cd..8530323dad1 100644 --- a/homeassistant/components/raincloud/binary_sensor.py +++ b/homeassistant/components/raincloud/binary_sensor.py @@ -1,4 +1,5 @@ """Support for Melnor RainCloud sprinkler water timer.""" + from __future__ import annotations import logging @@ -44,8 +45,10 @@ def setup_platform( else: # create a sensor for each zone managed by faucet - for zone in raincloud.controller.faucet.zones: - sensors.append(RainCloudBinarySensor(zone, sensor_type)) + sensors.extend( + RainCloudBinarySensor(zone, sensor_type) + for zone in raincloud.controller.faucet.zones + ) add_entities(sensors, True) diff --git a/homeassistant/components/raincloud/sensor.py b/homeassistant/components/raincloud/sensor.py index 4d21d36d069..34cd3f213ed 100644 --- a/homeassistant/components/raincloud/sensor.py +++ b/homeassistant/components/raincloud/sensor.py @@ -1,4 +1,5 @@ """Support for Melnor RainCloud sprinkler water timer.""" + from __future__ import annotations import logging @@ -47,8 +48,10 @@ def setup_platform( sensors.append(RainCloudSensor(raincloud.controller.faucet, sensor_type)) else: # create a sensor for each zone managed by a faucet - for zone in raincloud.controller.faucet.zones: - sensors.append(RainCloudSensor(zone, sensor_type)) + sensors.extend( + RainCloudSensor(zone, sensor_type) + for zone in raincloud.controller.faucet.zones + ) add_entities(sensors, True) diff --git a/homeassistant/components/raincloud/switch.py b/homeassistant/components/raincloud/switch.py index abcb680daa3..d901f862133 100644 --- a/homeassistant/components/raincloud/switch.py +++ b/homeassistant/components/raincloud/switch.py @@ -1,4 +1,5 @@ """Support for Melnor RainCloud sprinkler water timer.""" + from __future__ import annotations import logging @@ -46,13 +47,14 @@ def setup_platform( raincloud = hass.data[DATA_RAINCLOUD].data default_watering_timer = config[CONF_WATERING_TIME] - sensors = [] - for sensor_type in config[CONF_MONITORED_CONDITIONS]: - # create a sensor for each zone managed by faucet - for zone in raincloud.controller.faucet.zones: - sensors.append(RainCloudSwitch(default_watering_timer, zone, sensor_type)) - - add_entities(sensors, True) + add_entities( + ( + RainCloudSwitch(default_watering_timer, zone, sensor_type) + for zone in raincloud.controller.faucet.zones + for sensor_type in config[CONF_MONITORED_CONDITIONS] + ), + True, + ) class RainCloudSwitch(RainCloudEntity, SwitchEntity): diff --git a/homeassistant/components/rainforest_eagle/__init__.py b/homeassistant/components/rainforest_eagle/__init__.py index 7cf540de1e6..67baa4dbd99 100644 --- a/homeassistant/components/rainforest_eagle/__init__.py +++ b/homeassistant/components/rainforest_eagle/__init__.py @@ -1,4 +1,5 @@ """The Rainforest Eagle integration.""" + from __future__ import annotations from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/rainforest_eagle/config_flow.py b/homeassistant/components/rainforest_eagle/config_flow.py index f1e01a9d77a..b48c1329695 100644 --- a/homeassistant/components/rainforest_eagle/config_flow.py +++ b/homeassistant/components/rainforest_eagle/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Rainforest Eagle integration.""" + from __future__ import annotations import logging @@ -6,9 +7,8 @@ from typing import Any import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_TYPE -from homeassistant.data_entry_flow import FlowResult from . import data from .const import CONF_CLOUD_ID, CONF_HARDWARE_ADDRESS, CONF_INSTALL_CODE, DOMAIN @@ -31,14 +31,14 @@ def create_schema(user_input: dict[str, Any] | None) -> vol.Schema: ) -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class RainforestEagleConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Rainforest Eagle.""" VERSION = 1 async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" if user_input is None: return self.async_show_form( diff --git a/homeassistant/components/rainforest_eagle/data.py b/homeassistant/components/rainforest_eagle/data.py index 9da6372086f..879aa467d9b 100644 --- a/homeassistant/components/rainforest_eagle/data.py +++ b/homeassistant/components/rainforest_eagle/data.py @@ -1,4 +1,5 @@ """Rainforest data.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/rainforest_eagle/diagnostics.py b/homeassistant/components/rainforest_eagle/diagnostics.py index f20a20af9e2..14c980bad7d 100644 --- a/homeassistant/components/rainforest_eagle/diagnostics.py +++ b/homeassistant/components/rainforest_eagle/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for Eagle.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/rainforest_eagle/sensor.py b/homeassistant/components/rainforest_eagle/sensor.py index 987142c6390..27eae0e3e8e 100644 --- a/homeassistant/components/rainforest_eagle/sensor.py +++ b/homeassistant/components/rainforest_eagle/sensor.py @@ -1,4 +1,5 @@ """Support for the Rainforest Eagle energy monitor.""" + from __future__ import annotations from homeassistant.components.sensor import ( diff --git a/homeassistant/components/rainforest_raven/__init__.py b/homeassistant/components/rainforest_raven/__init__.py index d72b12f68c6..76f82624160 100644 --- a/homeassistant/components/rainforest_raven/__init__.py +++ b/homeassistant/components/rainforest_raven/__init__.py @@ -1,4 +1,5 @@ """Integration for Rainforest RAVEn devices.""" + from __future__ import annotations from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/rainforest_raven/config_flow.py b/homeassistant/components/rainforest_raven/config_flow.py index 2f0234efb7a..72d258dc1d3 100644 --- a/homeassistant/components/rainforest_raven/config_flow.py +++ b/homeassistant/components/rainforest_raven/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Rainforest RAVEn devices.""" + from __future__ import annotations import asyncio @@ -11,10 +12,9 @@ import serial.tools.list_ports from serial.tools.list_ports_common import ListPortInfo import voluptuous as vol -from homeassistant import config_entries from homeassistant.components import usb +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_DEVICE, CONF_MAC, CONF_NAME -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.selector import ( SelectSelector, SelectSelectorConfig, @@ -38,7 +38,7 @@ def _generate_unique_id(info: ListPortInfo | usb.UsbServiceInfo) -> str: ) -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class RainforestRavenConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Rainforest RAVEn devices.""" def __init__(self) -> None: @@ -66,7 +66,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_meters( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Connect to device and discover meters.""" errors: dict[str, str] = {} if user_input is not None: @@ -98,7 +98,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) return self.async_show_form(step_id="meters", data_schema=schema, errors=errors) - async def async_step_usb(self, discovery_info: usb.UsbServiceInfo) -> FlowResult: + async def async_step_usb( + self, discovery_info: usb.UsbServiceInfo + ) -> ConfigFlowResult: """Handle USB Discovery.""" device = discovery_info.device dev_path = await self.hass.async_add_executor_job(usb.get_serial_by_id, device) @@ -114,7 +116,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initiated by the user.""" if self._async_in_progress(): return self.async_abort(reason="already_in_progress") diff --git a/homeassistant/components/rainforest_raven/const.py b/homeassistant/components/rainforest_raven/const.py index a5269ddbc26..ca46f0303cc 100644 --- a/homeassistant/components/rainforest_raven/const.py +++ b/homeassistant/components/rainforest_raven/const.py @@ -1,3 +1,4 @@ """Constants for the Rainforest RAVEn integration.""" + DEFAULT_NAME = "Rainforest RAVEn" DOMAIN = "rainforest_raven" diff --git a/homeassistant/components/rainforest_raven/coordinator.py b/homeassistant/components/rainforest_raven/coordinator.py index dbb7203768b..37e44b12eba 100644 --- a/homeassistant/components/rainforest_raven/coordinator.py +++ b/homeassistant/components/rainforest_raven/coordinator.py @@ -1,4 +1,5 @@ """Data update coordination for Rainforest RAVEn devices.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/rainforest_raven/diagnostics.py b/homeassistant/components/rainforest_raven/diagnostics.py index 970915888ec..820c4826f00 100644 --- a/homeassistant/components/rainforest_raven/diagnostics.py +++ b/homeassistant/components/rainforest_raven/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for a Rainforest RAVEn device.""" + from __future__ import annotations from collections.abc import Mapping diff --git a/homeassistant/components/rainforest_raven/icons.json b/homeassistant/components/rainforest_raven/icons.json new file mode 100644 index 00000000000..964ecf92c74 --- /dev/null +++ b/homeassistant/components/rainforest_raven/icons.json @@ -0,0 +1,12 @@ +{ + "entity": { + "sensor": { + "signal_strength": { + "default": "mdi:wifi" + }, + "meter_price": { + "default": "mdi:cash" + } + } + } +} diff --git a/homeassistant/components/rainforest_raven/manifest.json b/homeassistant/components/rainforest_raven/manifest.json index ad161d32201..a2717f0e886 100644 --- a/homeassistant/components/rainforest_raven/manifest.json +++ b/homeassistant/components/rainforest_raven/manifest.json @@ -6,7 +6,7 @@ "dependencies": ["usb"], "documentation": "https://www.home-assistant.io/integrations/rainforest_raven", "iot_class": "local_polling", - "requirements": ["aioraven==0.5.2"], + "requirements": ["aioraven==0.5.3"], "usb": [ { "vid": "0403", diff --git a/homeassistant/components/rainforest_raven/sensor.py b/homeassistant/components/rainforest_raven/sensor.py index d1f1aebb0f3..23ca3220694 100644 --- a/homeassistant/components/rainforest_raven/sensor.py +++ b/homeassistant/components/rainforest_raven/sensor.py @@ -1,4 +1,5 @@ """Sensor entity for a Rainforest RAVEn device.""" + from __future__ import annotations from dataclasses import dataclass @@ -70,7 +71,6 @@ DIAGNOSTICS = ( key="link_strength", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, - icon="mdi:wifi", entity_category=EntityCategory.DIAGNOSTIC, attribute_keys=[ "channel", @@ -104,7 +104,6 @@ async def async_setup_entry( translation_key="meter_price", key="price", native_unit_of_measurement=f"{meter_data['PriceCluster']['currency'].value}/{UnitOfEnergy.KILO_WATT_HOUR}", - icon="mdi:cash", state_class=SensorStateClass.MEASUREMENT, attribute_keys=[ "tier", diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index 2e821fc7a7a..bcd60875c70 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -1,4 +1,5 @@ """Support for RainMachine devices.""" + from __future__ import annotations from collections.abc import Callable, Coroutine diff --git a/homeassistant/components/rainmachine/binary_sensor.py b/homeassistant/components/rainmachine/binary_sensor.py index 930139acf60..866cddbabbd 100644 --- a/homeassistant/components/rainmachine/binary_sensor.py +++ b/homeassistant/components/rainmachine/binary_sensor.py @@ -1,4 +1,5 @@ """Binary sensors for key RainMachine data.""" + from dataclasses import dataclass from homeassistant.components.binary_sensor import ( @@ -42,14 +43,12 @@ BINARY_SENSOR_DESCRIPTIONS = ( RainMachineBinarySensorDescription( key=TYPE_FLOW_SENSOR, translation_key=TYPE_FLOW_SENSOR, - icon="mdi:water-pump", api_category=DATA_PROVISION_SETTINGS, data_key="useFlowSensor", ), RainMachineBinarySensorDescription( key=TYPE_FREEZE, translation_key=TYPE_FREEZE, - icon="mdi:cancel", entity_category=EntityCategory.DIAGNOSTIC, api_category=DATA_RESTRICTIONS_CURRENT, data_key="freeze", @@ -57,7 +56,6 @@ BINARY_SENSOR_DESCRIPTIONS = ( RainMachineBinarySensorDescription( key=TYPE_HOURLY, translation_key=TYPE_HOURLY, - icon="mdi:cancel", entity_category=EntityCategory.DIAGNOSTIC, api_category=DATA_RESTRICTIONS_CURRENT, data_key="hourly", @@ -65,7 +63,6 @@ BINARY_SENSOR_DESCRIPTIONS = ( RainMachineBinarySensorDescription( key=TYPE_MONTH, translation_key=TYPE_MONTH, - icon="mdi:cancel", entity_category=EntityCategory.DIAGNOSTIC, api_category=DATA_RESTRICTIONS_CURRENT, data_key="month", @@ -73,7 +70,6 @@ BINARY_SENSOR_DESCRIPTIONS = ( RainMachineBinarySensorDescription( key=TYPE_RAINDELAY, translation_key=TYPE_RAINDELAY, - icon="mdi:cancel", entity_category=EntityCategory.DIAGNOSTIC, api_category=DATA_RESTRICTIONS_CURRENT, data_key="rainDelay", @@ -81,7 +77,6 @@ BINARY_SENSOR_DESCRIPTIONS = ( RainMachineBinarySensorDescription( key=TYPE_RAINSENSOR, translation_key=TYPE_RAINSENSOR, - icon="mdi:cancel", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, api_category=DATA_RESTRICTIONS_CURRENT, @@ -90,7 +85,6 @@ BINARY_SENSOR_DESCRIPTIONS = ( RainMachineBinarySensorDescription( key=TYPE_WEEKDAY, translation_key=TYPE_WEEKDAY, - icon="mdi:cancel", entity_category=EntityCategory.DIAGNOSTIC, api_category=DATA_RESTRICTIONS_CURRENT, data_key="weekDay", diff --git a/homeassistant/components/rainmachine/button.py b/homeassistant/components/rainmachine/button.py index 6309d9777a1..24486a34b88 100644 --- a/homeassistant/components/rainmachine/button.py +++ b/homeassistant/components/rainmachine/button.py @@ -1,4 +1,5 @@ """Buttons for the RainMachine integration.""" + from __future__ import annotations from collections.abc import Awaitable, Callable diff --git a/homeassistant/components/rainmachine/config_flow.py b/homeassistant/components/rainmachine/config_flow.py index 1d73ef3dd88..5c07f04c163 100644 --- a/homeassistant/components/rainmachine/config_flow.py +++ b/homeassistant/components/rainmachine/config_flow.py @@ -1,4 +1,5 @@ """Config flow to configure the RainMachine component.""" + from __future__ import annotations from typing import Any @@ -8,12 +9,15 @@ from regenmaschine.controller import Controller from regenmaschine.errors import RainMachineError import voluptuous as vol -from homeassistant import config_entries from homeassistant.components import zeroconf -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD, CONF_PORT, CONF_SSL from homeassistant.core import HomeAssistant, callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import aiohttp_client, config_validation as cv from .const import ( @@ -46,7 +50,7 @@ async def async_get_controller( return get_client_controller(client) -class RainMachineFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class RainMachineFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a RainMachine config flow.""" VERSION = 2 @@ -63,19 +67,19 @@ class RainMachineFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_homekit( self, discovery_info: zeroconf.ZeroconfServiceInfo - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by homekit discovery.""" return await self.async_step_homekit_zeroconf(discovery_info) async def async_step_zeroconf( self, discovery_info: zeroconf.ZeroconfServiceInfo - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle discovery via zeroconf.""" return await self.async_step_homekit_zeroconf(discovery_info) async def async_step_homekit_zeroconf( self, discovery_info: zeroconf.ZeroconfServiceInfo - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle discovery via zeroconf.""" ip_address = discovery_info.host @@ -117,7 +121,7 @@ class RainMachineFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the start of the config flow.""" errors = {} if user_input: @@ -161,7 +165,7 @@ class RainMachineFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) -class RainMachineOptionsFlowHandler(config_entries.OptionsFlow): +class RainMachineOptionsFlowHandler(OptionsFlow): """Handle a RainMachine options flow.""" def __init__(self, config_entry: ConfigEntry) -> None: @@ -170,7 +174,7 @@ class RainMachineOptionsFlowHandler(config_entries.OptionsFlow): async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Manage the options.""" if user_input is not None: return self.async_create_entry(data=user_input) diff --git a/homeassistant/components/rainmachine/const.py b/homeassistant/components/rainmachine/const.py index e28b2326b79..3bd89ab7e23 100644 --- a/homeassistant/components/rainmachine/const.py +++ b/homeassistant/components/rainmachine/const.py @@ -1,4 +1,5 @@ """Define constants for the SimpliSafe component.""" + import logging LOGGER = logging.getLogger(__package__) diff --git a/homeassistant/components/rainmachine/diagnostics.py b/homeassistant/components/rainmachine/diagnostics.py index e4835d514e6..5564ee693a4 100644 --- a/homeassistant/components/rainmachine/diagnostics.py +++ b/homeassistant/components/rainmachine/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for RainMachine.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/rainmachine/icons.json b/homeassistant/components/rainmachine/icons.json new file mode 100644 index 00000000000..32988081a18 --- /dev/null +++ b/homeassistant/components/rainmachine/icons.json @@ -0,0 +1,85 @@ +{ + "entity": { + "binary_sensor": { + "flow_sensor": { + "default": "mdi:water-pump" + }, + "freeze": { + "default": "mdi:cancel" + }, + "hourly": { + "default": "mdi:cancel" + }, + "month": { + "default": "mdi:cancel" + }, + "raindelay": { + "default": "mdi:cancel" + }, + "rainsensor": { + "default": "mdi:cancel" + }, + "weekday": { + "default": "mdi:cancel" + } + }, + "select": { + "freeze_protection_temperature": { + "default": "mdi:thermometer" + } + }, + "sensor": { + "translation_key_0": { + "default": "mdi:abc" + }, + "translation_key_1": { + "default": "mdi:abc" + }, + "translation_key_2": { + "default": "mdi:abc" + }, + "translation_key_3": { + "default": "mdi:abc" + }, + "translation_key_4": { + "default": "mdi:abc" + }, + "translation_key_5": { + "default": "mdi:abc" + }, + "translation_key_6": { + "default": "mdi:abc" + }, + "translation_key_7": { + "default": "mdi:abc" + } + }, + "switch": { + "flow_sensor_clicks_cubic_meter": { + "default": "mdi:water-pump" + }, + "flow_sensor_consumed_liters": { + "default": "mdi:water-pump" + }, + "flow_sensor_leak_clicks": { + "default": "mdi:pipe-leak" + }, + "flow_sensor_leak_volume": { + "default": "mdi:pipe-leak" + } + } + }, + "services": { + "pause_watering": "mdi:pause", + "restrict_watering": "mdi:cancel", + "start_program": "mdi:play", + "start_zone": "mdi:play", + "stop_all": "mdi:stop", + "stop_program": "mdi:stop", + "stop_zone": "mdi:stop", + "unpause_watering": "mdi:play-pause", + "push_flow_meter_data": "mdi:database-arrow-up", + "push_weather_data": "mdi:database-arrow-up", + "unrestrict_watering": "mdi:check" + } +} diff --git a/homeassistant/components/rainmachine/manifest.json b/homeassistant/components/rainmachine/manifest.json index 1c4c78564f6..0d871998eed 100644 --- a/homeassistant/components/rainmachine/manifest.json +++ b/homeassistant/components/rainmachine/manifest.json @@ -10,7 +10,7 @@ "integration_type": "device", "iot_class": "local_polling", "loggers": ["regenmaschine"], - "requirements": ["regenmaschine==2024.01.0"], + "requirements": ["regenmaschine==2024.03.0"], "zeroconf": [ { "type": "_http._tcp.local.", diff --git a/homeassistant/components/rainmachine/model.py b/homeassistant/components/rainmachine/model.py index e7f166b67dd..ee5567112cf 100644 --- a/homeassistant/components/rainmachine/model.py +++ b/homeassistant/components/rainmachine/model.py @@ -1,4 +1,5 @@ """Define RainMachine data models.""" + from dataclasses import dataclass from homeassistant.helpers.entity import EntityDescription diff --git a/homeassistant/components/rainmachine/select.py b/homeassistant/components/rainmachine/select.py index 893c1afa8da..bb622330897 100644 --- a/homeassistant/components/rainmachine/select.py +++ b/homeassistant/components/rainmachine/select.py @@ -1,4 +1,5 @@ """Support for RainMachine selects.""" + from __future__ import annotations from dataclasses import dataclass @@ -50,7 +51,6 @@ SELECT_DESCRIPTIONS = ( FreezeProtectionSelectDescription( key=TYPE_FREEZE_PROTECTION_TEMPERATURE, translation_key=TYPE_FREEZE_PROTECTION_TEMPERATURE, - icon="mdi:thermometer", entity_category=EntityCategory.CONFIG, api_category=DATA_RESTRICTIONS_UNIVERSAL, data_key="freezeProtectTemp", diff --git a/homeassistant/components/rainmachine/sensor.py b/homeassistant/components/rainmachine/sensor.py index ed9b8cc0142..15188e86963 100644 --- a/homeassistant/components/rainmachine/sensor.py +++ b/homeassistant/components/rainmachine/sensor.py @@ -1,4 +1,5 @@ """Support for sensor data from RainMachine.""" + from __future__ import annotations from dataclasses import dataclass @@ -66,7 +67,6 @@ SENSOR_DESCRIPTIONS = ( RainMachineSensorDataDescription( key=TYPE_FLOW_SENSOR_CLICK_M3, translation_key=TYPE_FLOW_SENSOR_CLICK_M3, - icon="mdi:water-pump", native_unit_of_measurement=f"clicks/{UnitOfVolume.CUBIC_METERS}", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -77,7 +77,6 @@ SENSOR_DESCRIPTIONS = ( RainMachineSensorDataDescription( key=TYPE_FLOW_SENSOR_CONSUMED_LITERS, translation_key=TYPE_FLOW_SENSOR_CONSUMED_LITERS, - icon="mdi:water-pump", device_class=SensorDeviceClass.WATER, entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=UnitOfVolume.LITERS, @@ -89,7 +88,6 @@ SENSOR_DESCRIPTIONS = ( RainMachineSensorDataDescription( key=TYPE_FLOW_SENSOR_LEAK_CLICKS, translation_key=TYPE_FLOW_SENSOR_LEAK_CLICKS, - icon="mdi:pipe-leak", entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement="clicks", entity_registry_enabled_default=False, @@ -100,7 +98,6 @@ SENSOR_DESCRIPTIONS = ( RainMachineSensorDataDescription( key=TYPE_FLOW_SENSOR_LEAK_VOLUME, translation_key=TYPE_FLOW_SENSOR_LEAK_VOLUME, - icon="mdi:pipe-leak", device_class=SensorDeviceClass.WATER, entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=UnitOfVolume.LITERS, diff --git a/homeassistant/components/rainmachine/switch.py b/homeassistant/components/rainmachine/switch.py index 8450cb7d5e6..f7be08d71d3 100644 --- a/homeassistant/components/rainmachine/switch.py +++ b/homeassistant/components/rainmachine/switch.py @@ -1,4 +1,5 @@ """Component providing support for RainMachine programs and zones.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/rainmachine/update.py b/homeassistant/components/rainmachine/update.py index 8d5690b5320..38bf74effa0 100644 --- a/homeassistant/components/rainmachine/update.py +++ b/homeassistant/components/rainmachine/update.py @@ -1,4 +1,5 @@ """Support for RainMachine updates.""" + from __future__ import annotations from enum import Enum diff --git a/homeassistant/components/rainmachine/util.py b/homeassistant/components/rainmachine/util.py index a557b701824..2848101eca1 100644 --- a/homeassistant/components/rainmachine/util.py +++ b/homeassistant/components/rainmachine/util.py @@ -1,4 +1,5 @@ """Define RainMachine utilities.""" + from __future__ import annotations from collections.abc import Awaitable, Callable, Iterable diff --git a/homeassistant/components/random/__init__.py b/homeassistant/components/random/__init__.py index 89a772529bd..bff2ce53dfb 100644 --- a/homeassistant/components/random/__init__.py +++ b/homeassistant/components/random/__init__.py @@ -1,4 +1,5 @@ """The random component.""" + from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/random/binary_sensor.py b/homeassistant/components/random/binary_sensor.py index a6d330e6151..bbcf87630c5 100644 --- a/homeassistant/components/random/binary_sensor.py +++ b/homeassistant/components/random/binary_sensor.py @@ -1,4 +1,5 @@ """Support for showing random states.""" + from __future__ import annotations from collections.abc import Mapping diff --git a/homeassistant/components/random/config_flow.py b/homeassistant/components/random/config_flow.py index 96dde9c8742..dc7d91603a5 100644 --- a/homeassistant/components/random/config_flow.py +++ b/homeassistant/components/random/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Random helper.""" + from collections.abc import Callable, Coroutine, Mapping from enum import StrEnum from typing import Any, cast diff --git a/homeassistant/components/random/const.py b/homeassistant/components/random/const.py index df6a18f8d11..a35ce4315a4 100644 --- a/homeassistant/components/random/const.py +++ b/homeassistant/components/random/const.py @@ -1,4 +1,5 @@ """Constants for random helper.""" + DOMAIN = "random" DEFAULT_MIN = 0 diff --git a/homeassistant/components/random/sensor.py b/homeassistant/components/random/sensor.py index 8cc21e34ce9..716350b2bb0 100644 --- a/homeassistant/components/random/sensor.py +++ b/homeassistant/components/random/sensor.py @@ -1,4 +1,5 @@ """Support for showing random numbers.""" + from __future__ import annotations from collections.abc import Mapping diff --git a/homeassistant/components/rapt_ble/__init__.py b/homeassistant/components/rapt_ble/__init__.py index 1b3d65ee2a4..4fd4c32a4cc 100644 --- a/homeassistant/components/rapt_ble/__init__.py +++ b/homeassistant/components/rapt_ble/__init__.py @@ -1,4 +1,5 @@ """The rapt_ble integration.""" + from __future__ import annotations import logging @@ -25,14 +26,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: address = entry.unique_id assert address is not None data = RAPTPillBluetoothDeviceData() - coordinator = hass.data.setdefault(DOMAIN, {})[ - entry.entry_id - ] = PassiveBluetoothProcessorCoordinator( - hass, - _LOGGER, - address=address, - mode=BluetoothScanningMode.ACTIVE, - update_method=data.update, + coordinator = hass.data.setdefault(DOMAIN, {})[entry.entry_id] = ( + PassiveBluetoothProcessorCoordinator( + hass, + _LOGGER, + address=address, + mode=BluetoothScanningMode.ACTIVE, + update_method=data.update, + ) ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload( diff --git a/homeassistant/components/rapt_ble/config_flow.py b/homeassistant/components/rapt_ble/config_flow.py index 9323ed4eb76..805a2cf8cbd 100644 --- a/homeassistant/components/rapt_ble/config_flow.py +++ b/homeassistant/components/rapt_ble/config_flow.py @@ -1,4 +1,5 @@ """Config flow for rapt_ble.""" + from __future__ import annotations from typing import Any @@ -10,9 +11,8 @@ from homeassistant.components.bluetooth import ( BluetoothServiceInfoBleak, async_discovered_service_info, ) -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_ADDRESS -from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN @@ -30,7 +30,7 @@ class RAPTPillConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_bluetooth( self, discovery_info: BluetoothServiceInfoBleak - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the bluetooth discovery step.""" await self.async_set_unique_id(discovery_info.address) self._abort_if_unique_id_configured() @@ -43,7 +43,7 @@ class RAPTPillConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_bluetooth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm discovery.""" assert self._discovered_device is not None device = self._discovered_device @@ -62,7 +62,7 @@ class RAPTPillConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the user step to pick discovered device.""" if user_input is not None: address = user_input[CONF_ADDRESS] diff --git a/homeassistant/components/rapt_ble/sensor.py b/homeassistant/components/rapt_ble/sensor.py index 9967a36faee..d718bbc031a 100644 --- a/homeassistant/components/rapt_ble/sensor.py +++ b/homeassistant/components/rapt_ble/sensor.py @@ -1,4 +1,5 @@ """Support for RAPT Pill hydrometers.""" + from __future__ import annotations from rapt_ble import DeviceClass, DeviceKey, SensorUpdate, Units diff --git a/homeassistant/components/raspberry_pi/__init__.py b/homeassistant/components/raspberry_pi/__init__.py index 19697d9b69d..d1dcd04922f 100644 --- a/homeassistant/components/raspberry_pi/__init__.py +++ b/homeassistant/components/raspberry_pi/__init__.py @@ -1,4 +1,5 @@ """The Raspberry Pi integration.""" + from __future__ import annotations from homeassistant.components.hassio import get_os_info, is_hassio diff --git a/homeassistant/components/raspberry_pi/config_flow.py b/homeassistant/components/raspberry_pi/config_flow.py index db0f8643e5c..d049776a6e0 100644 --- a/homeassistant/components/raspberry_pi/config_flow.py +++ b/homeassistant/components/raspberry_pi/config_flow.py @@ -1,10 +1,10 @@ """Config flow for the Raspberry Pi integration.""" + from __future__ import annotations from typing import Any -from homeassistant.config_entries import ConfigFlow -from homeassistant.data_entry_flow import FlowResult +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from .const import DOMAIN @@ -14,7 +14,9 @@ class RaspberryPiConfigFlow(ConfigFlow, domain=DOMAIN): VERSION = 1 - async def async_step_system(self, data: dict[str, Any] | None = None) -> FlowResult: + async def async_step_system( + self, data: dict[str, Any] | None = None + ) -> ConfigFlowResult: """Handle the initial step.""" if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") diff --git a/homeassistant/components/raspberry_pi/hardware.py b/homeassistant/components/raspberry_pi/hardware.py index 2141ff6034d..54d375c7b6e 100644 --- a/homeassistant/components/raspberry_pi/hardware.py +++ b/homeassistant/components/raspberry_pi/hardware.py @@ -1,4 +1,5 @@ """The Raspberry Pi hardware platform.""" + from __future__ import annotations from homeassistant.components.hardware.models import BoardInfo, HardwareInfo diff --git a/homeassistant/components/raspyrfm/switch.py b/homeassistant/components/raspyrfm/switch.py index 77360c6d224..ce69818beec 100644 --- a/homeassistant/components/raspyrfm/switch.py +++ b/homeassistant/components/raspyrfm/switch.py @@ -1,4 +1,5 @@ """Support for switches that can be controlled using the RaspyRFM rc module.""" + from __future__ import annotations from raspyrfm_client import RaspyRFMClient diff --git a/homeassistant/components/rdw/__init__.py b/homeassistant/components/rdw/__init__.py index 601d21ee889..f123db7c697 100644 --- a/homeassistant/components/rdw/__init__.py +++ b/homeassistant/components/rdw/__init__.py @@ -1,4 +1,5 @@ """Support for RDW.""" + from __future__ import annotations from vehicle import RDW, Vehicle diff --git a/homeassistant/components/rdw/binary_sensor.py b/homeassistant/components/rdw/binary_sensor.py index ce8e2908251..5360ce4a7fe 100644 --- a/homeassistant/components/rdw/binary_sensor.py +++ b/homeassistant/components/rdw/binary_sensor.py @@ -1,4 +1,5 @@ """Support for RDW binary sensors.""" + from __future__ import annotations from collections.abc import Callable @@ -34,7 +35,6 @@ BINARY_SENSORS: tuple[RDWBinarySensorEntityDescription, ...] = ( RDWBinarySensorEntityDescription( key="liability_insured", translation_key="liability_insured", - icon="mdi:shield-car", is_on_fn=lambda vehicle: vehicle.liability_insured, ), RDWBinarySensorEntityDescription( diff --git a/homeassistant/components/rdw/config_flow.py b/homeassistant/components/rdw/config_flow.py index a9fedc88dac..cf59abc650c 100644 --- a/homeassistant/components/rdw/config_flow.py +++ b/homeassistant/components/rdw/config_flow.py @@ -1,4 +1,5 @@ """Config flow to configure the RDW integration.""" + from __future__ import annotations from typing import Any @@ -6,8 +7,7 @@ from typing import Any from vehicle import RDW, RDWError, RDWUnknownLicensePlateError import voluptuous as vol -from homeassistant.config_entries import ConfigFlow -from homeassistant.data_entry_flow import FlowResult +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import CONF_LICENSE_PLATE, DOMAIN @@ -20,7 +20,7 @@ class RDWFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" errors = {} diff --git a/homeassistant/components/rdw/const.py b/homeassistant/components/rdw/const.py index e39e4048791..d9f99010dd7 100644 --- a/homeassistant/components/rdw/const.py +++ b/homeassistant/components/rdw/const.py @@ -1,4 +1,5 @@ """Constants for the RDW integration.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/rdw/diagnostics.py b/homeassistant/components/rdw/diagnostics.py index dbf3d8e21c0..f55bc33e026 100644 --- a/homeassistant/components/rdw/diagnostics.py +++ b/homeassistant/components/rdw/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for RDW.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/rdw/icons.json b/homeassistant/components/rdw/icons.json new file mode 100644 index 00000000000..5e1dffcad9c --- /dev/null +++ b/homeassistant/components/rdw/icons.json @@ -0,0 +1,9 @@ +{ + "entity": { + "binary_sensor": { + "liability_insured": { + "default": "mdi:shield-car" + } + } + } +} diff --git a/homeassistant/components/rdw/sensor.py b/homeassistant/components/rdw/sensor.py index a6ad9047852..2c9c9addcfb 100644 --- a/homeassistant/components/rdw/sensor.py +++ b/homeassistant/components/rdw/sensor.py @@ -1,4 +1,5 @@ """Support for RDW sensors.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/recollect_waste/__init__.py b/homeassistant/components/recollect_waste/__init__.py index 5f3a50e93ed..bd01aed5473 100644 --- a/homeassistant/components/recollect_waste/__init__.py +++ b/homeassistant/components/recollect_waste/__init__.py @@ -1,4 +1,5 @@ """The ReCollect Waste integration.""" + from __future__ import annotations from datetime import date, timedelta diff --git a/homeassistant/components/recollect_waste/calendar.py b/homeassistant/components/recollect_waste/calendar.py index c439f647da5..3a76451358e 100644 --- a/homeassistant/components/recollect_waste/calendar.py +++ b/homeassistant/components/recollect_waste/calendar.py @@ -1,4 +1,5 @@ """Support for ReCollect Waste calendars.""" + from __future__ import annotations import datetime @@ -47,8 +48,8 @@ async def async_setup_entry( class ReCollectWasteCalendar(ReCollectWasteEntity, CalendarEntity): """Define a ReCollect Waste calendar.""" - _attr_icon = "mdi:delete-empty" _attr_name = None + _attr_translation_key = "calendar" def __init__( self, diff --git a/homeassistant/components/recollect_waste/config_flow.py b/homeassistant/components/recollect_waste/config_flow.py index c3e770cc458..882eb6a00d2 100644 --- a/homeassistant/components/recollect_waste/config_flow.py +++ b/homeassistant/components/recollect_waste/config_flow.py @@ -1,4 +1,5 @@ """Config flow for ReCollect Waste integration.""" + from __future__ import annotations from typing import Any @@ -7,10 +8,14 @@ from aiorecollect.client import Client from aiorecollect.errors import RecollectError import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.const import CONF_FRIENDLY_NAME from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import aiohttp_client from .const import CONF_PLACE_ID, CONF_SERVICE_ID, DOMAIN, LOGGER @@ -20,7 +25,7 @@ DATA_SCHEMA = vol.Schema( ) -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class RecollectWasteConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for ReCollect Waste.""" VERSION = 2 @@ -28,14 +33,14 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback def async_get_options_flow( - config_entry: config_entries.ConfigEntry, - ) -> config_entries.OptionsFlow: + config_entry: ConfigEntry, + ) -> OptionsFlow: """Define the config flow to handle options.""" return RecollectWasteOptionsFlowHandler(config_entry) async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle configuration via the UI.""" if user_input is None: return self.async_show_form( @@ -71,16 +76,16 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -class RecollectWasteOptionsFlowHandler(config_entries.OptionsFlow): +class RecollectWasteOptionsFlowHandler(OptionsFlow): """Handle a Recollect Waste options flow.""" - def __init__(self, entry: config_entries.ConfigEntry) -> None: + def __init__(self, entry: ConfigEntry) -> None: """Initialize.""" self._entry = entry async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Manage the options.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) diff --git a/homeassistant/components/recollect_waste/const.py b/homeassistant/components/recollect_waste/const.py index 5589507d4ac..b1fa1b0e6f0 100644 --- a/homeassistant/components/recollect_waste/const.py +++ b/homeassistant/components/recollect_waste/const.py @@ -1,4 +1,5 @@ """Define ReCollect Waste constants.""" + import logging DOMAIN = "recollect_waste" diff --git a/homeassistant/components/recollect_waste/diagnostics.py b/homeassistant/components/recollect_waste/diagnostics.py index 35bc1b56896..f1dbcdb4061 100644 --- a/homeassistant/components/recollect_waste/diagnostics.py +++ b/homeassistant/components/recollect_waste/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for ReCollect Waste.""" + from __future__ import annotations import dataclasses diff --git a/homeassistant/components/recollect_waste/entity.py b/homeassistant/components/recollect_waste/entity.py index 5ccd65cc55a..a300e527fd2 100644 --- a/homeassistant/components/recollect_waste/entity.py +++ b/homeassistant/components/recollect_waste/entity.py @@ -1,4 +1,5 @@ """Define a base ReCollect Waste entity.""" + from aiorecollect.client import PickupEvent from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/recollect_waste/icons.json b/homeassistant/components/recollect_waste/icons.json new file mode 100644 index 00000000000..59a2e742a1d --- /dev/null +++ b/homeassistant/components/recollect_waste/icons.json @@ -0,0 +1,9 @@ +{ + "entity": { + "calendar": { + "calendar": { + "default": "mdi:delete-empty" + } + } + } +} diff --git a/homeassistant/components/recollect_waste/sensor.py b/homeassistant/components/recollect_waste/sensor.py index da5394a9341..36658fb5008 100644 --- a/homeassistant/components/recollect_waste/sensor.py +++ b/homeassistant/components/recollect_waste/sensor.py @@ -1,4 +1,5 @@ """Support for ReCollect Waste sensors.""" + from __future__ import annotations from datetime import date @@ -88,9 +89,9 @@ class ReCollectWasteSensor(ReCollectWasteEntity, SensorEntity): self._attr_native_value = None else: self._attr_extra_state_attributes[ATTR_AREA_NAME] = event.area_name - self._attr_extra_state_attributes[ - ATTR_PICKUP_TYPES - ] = async_get_pickup_type_names(self._entry, event.pickup_types) + self._attr_extra_state_attributes[ATTR_PICKUP_TYPES] = ( + async_get_pickup_type_names(self._entry, event.pickup_types) + ) self._attr_native_value = event.date super()._handle_coordinator_update() diff --git a/homeassistant/components/recollect_waste/util.py b/homeassistant/components/recollect_waste/util.py index 185078f297c..dcd8a1569df 100644 --- a/homeassistant/components/recollect_waste/util.py +++ b/homeassistant/components/recollect_waste/util.py @@ -1,4 +1,5 @@ """Define ReCollect Waste utilities.""" + from aiorecollect.client import PickupType from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 2217d6c7d4e..de75207389f 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -1,4 +1,5 @@ """Support for recording details.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/recorder/auto_repairs/events/schema.py b/homeassistant/components/recorder/auto_repairs/events/schema.py index 3cc2e74f95b..fb3b38c61c5 100644 --- a/homeassistant/components/recorder/auto_repairs/events/schema.py +++ b/homeassistant/components/recorder/auto_repairs/events/schema.py @@ -1,4 +1,5 @@ """Events schema repairs.""" + from __future__ import annotations from typing import TYPE_CHECKING diff --git a/homeassistant/components/recorder/auto_repairs/schema.py b/homeassistant/components/recorder/auto_repairs/schema.py index aedf917dd22..aa2fc1bb8cb 100644 --- a/homeassistant/components/recorder/auto_repairs/schema.py +++ b/homeassistant/components/recorder/auto_repairs/schema.py @@ -1,11 +1,12 @@ """Schema repairs.""" + from __future__ import annotations from collections.abc import Iterable, Mapping import logging from typing import TYPE_CHECKING -from sqlalchemy import MetaData +from sqlalchemy import MetaData, Table from sqlalchemy.exc import OperationalError from sqlalchemy.orm import DeclarativeBase from sqlalchemy.orm.attributes import InstrumentedAttribute @@ -93,9 +94,9 @@ def _validate_table_schema_has_correct_collation( with session_scope(session=instance.get_session(), read_only=True) as session: table = table_object.__tablename__ metadata_obj = MetaData() + reflected_table = Table(table, metadata_obj, autoload_with=instance.engine) connection = session.connection() - metadata_obj.reflect(bind=connection) - dialect_kwargs = metadata_obj.tables[table].dialect_kwargs + dialect_kwargs = reflected_table.dialect_kwargs # Check if the table has a collation set, if its not set than its # using the server default collation for the database diff --git a/homeassistant/components/recorder/auto_repairs/states/schema.py b/homeassistant/components/recorder/auto_repairs/states/schema.py index 3c0daef452d..3900f4fb763 100644 --- a/homeassistant/components/recorder/auto_repairs/states/schema.py +++ b/homeassistant/components/recorder/auto_repairs/states/schema.py @@ -1,4 +1,5 @@ """States schema repairs.""" + from __future__ import annotations from typing import TYPE_CHECKING diff --git a/homeassistant/components/recorder/auto_repairs/statistics/duplicates.py b/homeassistant/components/recorder/auto_repairs/statistics/duplicates.py index 5b7a141bd70..06a5c5258f1 100644 --- a/homeassistant/components/recorder/auto_repairs/statistics/duplicates.py +++ b/homeassistant/components/recorder/auto_repairs/statistics/duplicates.py @@ -1,4 +1,5 @@ """Statistics duplication repairs.""" + from __future__ import annotations import json diff --git a/homeassistant/components/recorder/auto_repairs/statistics/schema.py b/homeassistant/components/recorder/auto_repairs/statistics/schema.py index 607935bd6ff..13c172290ca 100644 --- a/homeassistant/components/recorder/auto_repairs/statistics/schema.py +++ b/homeassistant/components/recorder/auto_repairs/statistics/schema.py @@ -1,4 +1,5 @@ """Statistics schema repairs.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/recorder/backup.py b/homeassistant/components/recorder/backup.py index a1f6f4f39bc..d47cbe92bd4 100644 --- a/homeassistant/components/recorder/backup.py +++ b/homeassistant/components/recorder/backup.py @@ -1,4 +1,5 @@ """Backup platform for the Recorder integration.""" + from logging import getLogger from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/recorder/const.py b/homeassistant/components/recorder/const.py index 66d46c0c20e..1869bb32239 100644 --- a/homeassistant/components/recorder/const.py +++ b/homeassistant/components/recorder/const.py @@ -53,6 +53,7 @@ STATISTICS_ROWS_SCHEMA_VERSION = 23 CONTEXT_ID_AS_BINARY_SCHEMA_VERSION = 36 EVENT_TYPE_IDS_SCHEMA_VERSION = 37 STATES_META_SCHEMA_VERSION = 38 +LAST_REPORTED_SCHEMA_VERSION = 43 LEGACY_STATES_EVENT_ID_INDEX_SCHEMA_VERSION = 28 diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index 547c78e2a7a..0e404ce4da0 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -1,4 +1,5 @@ """Support for recording details.""" + from __future__ import annotations import asyncio @@ -14,7 +15,7 @@ import time from typing import TYPE_CHECKING, Any, TypeVar, cast import psutil_home_assistant as ha_psutil -from sqlalchemy import create_engine, event as sqlalchemy_event, exc, select +from sqlalchemy import create_engine, event as sqlalchemy_event, exc, select, update from sqlalchemy.engine import Engine from sqlalchemy.engine.interfaces import DBAPIConnection from sqlalchemy.exc import SQLAlchemyError @@ -42,12 +43,11 @@ from homeassistant.util.enum import try_parse_enum from . import migration, statistics from .const import ( - CONTEXT_ID_AS_BINARY_SCHEMA_VERSION, DB_WORKER_PREFIX, DOMAIN, ESTIMATED_QUEUE_ITEM_SIZE, - EVENT_TYPE_IDS_SCHEMA_VERSION, KEEPALIVE_TIME, + LAST_REPORTED_SCHEMA_VERSION, LEGACY_STATES_EVENT_ID_INDEX_SCHEMA_VERSION, MARIADB_PYMYSQL_URL_PREFIX, MARIADB_URL_PREFIX, @@ -57,7 +57,6 @@ from .const import ( QUEUE_PERCENTAGE_ALLOWED_AVAILABLE_MEMORY, SQLITE_MAX_BIND_VARS, SQLITE_URL_PREFIX, - STATES_META_SCHEMA_VERSION, STATISTICS_ROWS_SCHEMA_VERSION, SupportedDialect, ) @@ -77,14 +76,15 @@ from .db_schema import ( StatisticsShortTerm, ) from .executor import DBInterruptibleThreadPoolExecutor +from .migration import ( + EntityIDMigration, + EventsContextIDMigration, + EventTypeIDMigration, + StatesContextIDMigration, +) from .models import DatabaseEngine, StatisticData, StatisticMetaData, UnsupportedDialect from .pool import POOL_SIZE, MutexPool, RecorderPool -from .queries import ( - has_entity_ids_to_migrate, - has_event_type_to_migrate, - has_events_context_ids_to_migrate, - has_states_context_ids_to_migrate, -) +from .queries import get_migration_changes from .table_managers.event_data import EventDataManager from .table_managers.event_types import EventTypeManager from .table_managers.recorder_runs import RecorderRunsManager @@ -100,17 +100,13 @@ from .tasks import ( CommitTask, CompileMissingStatisticsTask, DatabaseLockTask, - EntityIDMigrationTask, EntityIDPostMigrationTask, EventIdMigrationTask, - EventsContextIDMigrationTask, - EventTypeIDMigrationTask, ImportStatisticsTask, KeepAliveTask, PerodicCleanupTask, PurgeTask, RecorderTask, - StatesContextIDMigrationTask, StatisticsTask, StopTask, SynchronizeTask, @@ -481,8 +477,12 @@ class Recorder(threading.Thread): def async_register(self) -> None: """Post connection initialize.""" bus = self.hass.bus - bus.async_listen_once(EVENT_HOMEASSISTANT_CLOSE, self._async_close) - bus.async_listen_once(EVENT_HOMEASSISTANT_FINAL_WRITE, self._async_shutdown) + bus.async_listen_once( + EVENT_HOMEASSISTANT_CLOSE, self._async_close, run_immediately=True + ) + bus.async_listen_once( + EVENT_HOMEASSISTANT_FINAL_WRITE, self._async_shutdown, run_immediately=True + ) async_at_started(self.hass, self._async_hass_started) @callback @@ -782,44 +782,35 @@ class Recorder(threading.Thread): def _activate_and_set_db_ready(self) -> None: """Activate the table managers or schedule migrations and mark the db as ready.""" - with session_scope(session=self.get_session(), read_only=True) as session: + with session_scope(session=self.get_session()) as session: # Prime the statistics meta manager as soon as possible # since we want the frontend queries to avoid a thundering # herd of queries to find the statistics meta data if # there are a lot of statistics graphs on the frontend. - if self.schema_version >= STATISTICS_ROWS_SCHEMA_VERSION: + schema_version = self.schema_version + if schema_version >= STATISTICS_ROWS_SCHEMA_VERSION: self.statistics_meta_manager.load(session) - if ( - self.schema_version < CONTEXT_ID_AS_BINARY_SCHEMA_VERSION - or execute_stmt_lambda_element( - session, has_states_context_ids_to_migrate() - ) - ): - self.queue_task(StatesContextIDMigrationTask()) + migration_changes: dict[str, int] = { + row[0]: row[1] + for row in execute_stmt_lambda_element(session, get_migration_changes()) + } - if ( - self.schema_version < CONTEXT_ID_AS_BINARY_SCHEMA_VERSION - or execute_stmt_lambda_element( - session, has_events_context_ids_to_migrate() - ) - ): - self.queue_task(EventsContextIDMigrationTask()) + for migrator_cls in (StatesContextIDMigration, EventsContextIDMigration): + migrator = migrator_cls(session, schema_version, migration_changes) + if migrator.needs_migrate(): + self.queue_task(migrator.task()) - if ( - self.schema_version < EVENT_TYPE_IDS_SCHEMA_VERSION - or execute_stmt_lambda_element(session, has_event_type_to_migrate()) - ): - self.queue_task(EventTypeIDMigrationTask()) + migrator = EventTypeIDMigration(session, schema_version, migration_changes) + if migrator.needs_migrate(): + self.queue_task(migrator.task()) else: _LOGGER.debug("Activating event_types manager as all data is migrated") self.event_type_manager.active = True - if ( - self.schema_version < STATES_META_SCHEMA_VERSION - or execute_stmt_lambda_element(session, has_entity_ids_to_migrate()) - ): - self.queue_task(EntityIDMigrationTask()) + migrator = EntityIDMigration(session, schema_version, migration_changes) + if migrator.needs_migrate(): + self.queue_task(migrator.task()) else: _LOGGER.debug("Activating states_meta manager as all data is migrated") self.states_meta_manager.active = True @@ -886,7 +877,7 @@ class Recorder(threading.Thread): for task_or_event in startup_task_or_events: # Event is never subclassed so we can # use a fast type check - if type(task_or_event) is Event: # noqa: E721 + if type(task_or_event) is Event: event_ = task_or_event if event_.event_type == EVENT_STATE_CHANGED: state_change_events.append(event_) @@ -917,7 +908,7 @@ class Recorder(threading.Thread): # is an Event so we can process it directly # and since its never subclassed, we can # use a fast type check - if type(task) is Event: # noqa: E721 + if type(task) is Event: self._process_one_event(task) return # If its not an event, commit everything @@ -1100,12 +1091,22 @@ class Recorder(threading.Thread): entity_id = event.data["entity_id"] dbstate = States.from_event(event) + old_state = event.data["old_state"] + + assert self.event_session is not None + session = self.event_session states_manager = self.states_manager - if old_state := states_manager.pop_pending(entity_id): - dbstate.old_state = old_state + if pending_state := states_manager.pop_pending(entity_id): + dbstate.old_state = pending_state + if old_state: + pending_state.last_reported_ts = old_state.last_reported_timestamp elif old_state_id := states_manager.pop_committed(entity_id): dbstate.old_state_id = old_state_id + if old_state: + states_manager.update_pending_last_reported( + old_state_id, old_state.last_reported_timestamp + ) if entity_removed: dbstate.state = None else: @@ -1119,8 +1120,6 @@ class Recorder(threading.Thread): ): return - assert self.event_session is not None - session = self.event_session # Map the entity_id to the StatesMeta table if pending_states_meta := states_meta_manager.get_pending(entity_id): dbstate.states_meta_rel = pending_states_meta @@ -1202,7 +1201,23 @@ class Recorder(threading.Thread): session = self.event_session self._commits_without_expire += 1 + if ( + pending_last_reported + := self.states_manager.get_pending_last_reported_timestamp() + ) and self.schema_version >= LAST_REPORTED_SCHEMA_VERSION: + with session.no_autoflush: + session.execute( + update(States), + [ + { + "state_id": state_id, + "last_reported_ts": last_reported_timestamp, + } + for state_id, last_reported_timestamp in pending_last_reported.items() + ], + ) session.commit() + self._event_session_has_pending_writes = False # We just committed the state attributes to the database # and we now know the attributes_ids. We can save diff --git a/homeassistant/components/recorder/db_schema.py b/homeassistant/components/recorder/db_schema.py index 7c7d9a743f3..5b24448211d 100644 --- a/homeassistant/components/recorder/db_schema.py +++ b/homeassistant/components/recorder/db_schema.py @@ -1,4 +1,5 @@ """Models for SQLAlchemy.""" + from __future__ import annotations from collections.abc import Callable @@ -67,7 +68,7 @@ class Base(DeclarativeBase): """Base class for tables.""" -SCHEMA_VERSION = 42 +SCHEMA_VERSION = 43 _LOGGER = logging.getLogger(__name__) @@ -83,6 +84,7 @@ TABLE_STATISTICS = "statistics" TABLE_STATISTICS_META = "statistics_meta" TABLE_STATISTICS_RUNS = "statistics_runs" TABLE_STATISTICS_SHORT_TERM = "statistics_short_term" +TABLE_MIGRATION_CHANGES = "migration_changes" STATISTICS_TABLES = ("statistics", "statistics_short_term") @@ -99,6 +101,7 @@ ALL_TABLES = [ TABLE_EVENT_TYPES, TABLE_RECORDER_RUNS, TABLE_SCHEMA_CHANGES, + TABLE_MIGRATION_CHANGES, TABLE_STATES_META, TABLE_STATISTICS, TABLE_STATISTICS_META, @@ -318,7 +321,7 @@ class Events(Base): EventOrigin(self.origin) if self.origin else EVENT_ORIGIN_ORDER[self.origin_idx or 0], - dt_util.utc_from_timestamp(self.time_fired_ts or 0), + self.time_fired_ts or 0, context=context, ) except JSON_DECODE_EXCEPTIONS: @@ -425,6 +428,7 @@ class States(Base): event_id: Mapped[int | None] = mapped_column(UNUSED_LEGACY_INTEGER_COLUMN) last_changed: Mapped[datetime | None] = mapped_column(UNUSED_LEGACY_DATETIME_COLUMN) last_changed_ts: Mapped[float | None] = mapped_column(TIMESTAMP_TYPE) + last_reported_ts: Mapped[float | None] = mapped_column(TIMESTAMP_TYPE) last_updated: Mapped[datetime | None] = mapped_column(UNUSED_LEGACY_DATETIME_COLUMN) last_updated_ts: Mapped[float | None] = mapped_column( TIMESTAMP_TYPE, default=time.time, index=True @@ -496,6 +500,7 @@ class States(Base): dbstate.state = "" dbstate.last_updated_ts = event.time_fired_timestamp dbstate.last_changed_ts = None + dbstate.last_reported_ts = None return dbstate dbstate.state = state.state @@ -504,6 +509,10 @@ class States(Base): dbstate.last_changed_ts = None else: dbstate.last_changed_ts = state.last_changed_timestamp + if state.last_updated == state.last_reported: + dbstate.last_reported_ts = None + else: + dbstate.last_reported_ts = state.last_reported_timestamp return dbstate @@ -520,21 +529,27 @@ class States(Base): # When json_loads fails _LOGGER.exception("Error converting row to state: %s", self) return None + last_updated = dt_util.utc_from_timestamp(self.last_updated_ts or 0) if self.last_changed_ts is None or self.last_changed_ts == self.last_updated_ts: - last_changed = last_updated = dt_util.utc_from_timestamp( - self.last_updated_ts or 0 - ) + last_changed = dt_util.utc_from_timestamp(self.last_updated_ts or 0) else: - last_updated = dt_util.utc_from_timestamp(self.last_updated_ts or 0) last_changed = dt_util.utc_from_timestamp(self.last_changed_ts or 0) + if ( + self.last_reported_ts is None + or self.last_reported_ts == self.last_updated_ts + ): + last_reported = dt_util.utc_from_timestamp(self.last_updated_ts or 0) + else: + last_reported = dt_util.utc_from_timestamp(self.last_reported_ts or 0) return State( self.entity_id or "", self.state, # type: ignore[arg-type] # Join the state_attributes table on attributes_id to get the attributes # for newer states attrs, - last_changed, - last_updated, + last_changed=last_changed, + last_reported=last_reported, + last_updated=last_updated, context=context, validate_entity_id=validate_entity_id, ) @@ -770,6 +785,15 @@ class RecorderRuns(Base): return self +class MigrationChanges(Base): + """Representation of migration changes.""" + + __tablename__ = TABLE_MIGRATION_CHANGES + + migration_id: Mapped[str] = mapped_column(String(255), primary_key=True) + version: Mapped[int] = mapped_column(SmallInteger) + + class SchemaChanges(Base): """Representation of schema version changes.""" diff --git a/homeassistant/components/recorder/entity_registry.py b/homeassistant/components/recorder/entity_registry.py index 24f33cd815e..5bf1856316a 100644 --- a/homeassistant/components/recorder/entity_registry.py +++ b/homeassistant/components/recorder/entity_registry.py @@ -1,5 +1,8 @@ """Recorder entity registry helper.""" + +from collections.abc import Mapping import logging +from typing import Any from homeassistant.core import Event, HomeAssistant, callback from homeassistant.helpers import entity_registry as er @@ -28,9 +31,9 @@ def async_setup(hass: HomeAssistant) -> None: ) @callback - def entity_registry_changed_filter(event: Event) -> bool: + def entity_registry_changed_filter(event_data: Mapping[str, Any]) -> bool: """Handle entity_id changed filter.""" - return event.data["action"] == "update" and "old_entity_id" in event.data + return event_data["action"] == "update" and "old_entity_id" in event_data @callback def _setup_entity_registry_event_handler(hass: HomeAssistant) -> None: diff --git a/homeassistant/components/recorder/executor.py b/homeassistant/components/recorder/executor.py index 3f677e72fdf..b17547499e8 100644 --- a/homeassistant/components/recorder/executor.py +++ b/homeassistant/components/recorder/executor.py @@ -1,4 +1,5 @@ """Database executor helpers.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/recorder/filters.py b/homeassistant/components/recorder/filters.py index fda8716df27..92f4c5d3902 100644 --- a/homeassistant/components/recorder/filters.py +++ b/homeassistant/components/recorder/filters.py @@ -1,4 +1,5 @@ """Provide pre-made queries on top of the recorder component.""" + from __future__ import annotations from collections.abc import Callable, Collection, Iterable diff --git a/homeassistant/components/recorder/history/__init__.py b/homeassistant/components/recorder/history/__init__.py index 7a569e70b15..05452fdac47 100644 --- a/homeassistant/components/recorder/history/__init__.py +++ b/homeassistant/components/recorder/history/__init__.py @@ -1,4 +1,5 @@ """Provide pre-made queries on top of the recorder component.""" + from __future__ import annotations from collections.abc import MutableMapping diff --git a/homeassistant/components/recorder/history/common.py b/homeassistant/components/recorder/history/common.py index 6d0150925d3..3427ee9d7ee 100644 --- a/homeassistant/components/recorder/history/common.py +++ b/homeassistant/components/recorder/history/common.py @@ -1,4 +1,5 @@ """Common functions for history.""" + from __future__ import annotations from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/recorder/history/const.py b/homeassistant/components/recorder/history/const.py index 61a615a7979..f5286a3ff3d 100644 --- a/homeassistant/components/recorder/history/const.py +++ b/homeassistant/components/recorder/history/const.py @@ -1,6 +1,5 @@ """Constants for history.""" - STATE_KEY = "state" LAST_CHANGED_KEY = "last_changed" diff --git a/homeassistant/components/recorder/history/legacy.py b/homeassistant/components/recorder/history/legacy.py index 2e1b02a8b64..ad9505e1af2 100644 --- a/homeassistant/components/recorder/history/legacy.py +++ b/homeassistant/components/recorder/history/legacy.py @@ -1,4 +1,5 @@ """Provide pre-made queries on top of the recorder component.""" + from __future__ import annotations from collections import defaultdict @@ -813,6 +814,7 @@ def _sorted_states_to_dict( } ) prev_state = state + continue for row in group: if (state := row[state_idx]) != prev_state: diff --git a/homeassistant/components/recorder/history/modern.py b/homeassistant/components/recorder/history/modern.py index da58822e266..5fd4f415e02 100644 --- a/homeassistant/components/recorder/history/modern.py +++ b/homeassistant/components/recorder/history/modern.py @@ -1,4 +1,5 @@ """Provide pre-made queries on top of the recorder component.""" + from __future__ import annotations from collections.abc import Callable, Iterable, Iterator, MutableMapping @@ -26,6 +27,7 @@ from homeassistant.core import HomeAssistant, State, split_entity_id import homeassistant.util.dt as dt_util from ... import recorder +from ..const import LAST_REPORTED_SCHEMA_VERSION from ..db_schema import SHARED_ATTR_OR_LEGACY_ATTRIBUTES, StateAttributes, States from ..filters import Filters from ..models import ( @@ -51,32 +53,43 @@ _FIELD_MAP = { def _stmt_and_join_attributes( - no_attributes: bool, include_last_changed: bool + no_attributes: bool, + include_last_changed: bool, + include_last_reported: bool, ) -> Select: """Return the statement and if StateAttributes should be joined.""" _select = select(States.metadata_id, States.state, States.last_updated_ts) if include_last_changed: _select = _select.add_columns(States.last_changed_ts) + if include_last_reported: + _select = _select.add_columns(States.last_reported_ts) if not no_attributes: _select = _select.add_columns(SHARED_ATTR_OR_LEGACY_ATTRIBUTES) return _select def _stmt_and_join_attributes_for_start_state( - no_attributes: bool, include_last_changed: bool + no_attributes: bool, + include_last_changed: bool, + include_last_reported: bool, ) -> Select: """Return the statement and if StateAttributes should be joined.""" _select = select(States.metadata_id, States.state) _select = _select.add_columns(literal(value=0).label("last_updated_ts")) if include_last_changed: _select = _select.add_columns(literal(value=0).label("last_changed_ts")) + if include_last_reported: + _select = _select.add_columns(literal(value=0).label("last_reported_ts")) if not no_attributes: _select = _select.add_columns(SHARED_ATTR_OR_LEGACY_ATTRIBUTES) return _select def _select_from_subquery( - subquery: Subquery | CompoundSelect, no_attributes: bool, include_last_changed: bool + subquery: Subquery | CompoundSelect, + no_attributes: bool, + include_last_changed: bool, + include_last_reported: bool, ) -> Select: """Return the statement to select from the union.""" base_select = select( @@ -86,6 +99,8 @@ def _select_from_subquery( ) if include_last_changed: base_select = base_select.add_columns(subquery.c.last_changed_ts) + if include_last_reported: + base_select = base_select.add_columns(subquery.c.last_reported_ts) if no_attributes: return base_select return base_select.add_columns(subquery.c.attributes) @@ -133,7 +148,7 @@ def _significant_states_stmt( ) -> Select | CompoundSelect: """Query the database for significant state changes.""" include_last_changed = not significant_changes_only - stmt = _stmt_and_join_attributes(no_attributes, include_last_changed) + stmt = _stmt_and_join_attributes(no_attributes, include_last_changed, False) if significant_changes_only: # Since we are filtering on entity_id (metadata_id) we can avoid # the join of the states_meta table since we already know which @@ -173,13 +188,17 @@ def _significant_states_stmt( ).subquery(), no_attributes, include_last_changed, + False, + ), + _select_from_subquery( + stmt.subquery(), no_attributes, include_last_changed, False ), - _select_from_subquery(stmt.subquery(), no_attributes, include_last_changed), ).subquery() return _select_from_subquery( unioned_subquery, no_attributes, include_last_changed, + False, ).order_by(unioned_subquery.c.metadata_id, unioned_subquery.c.last_updated_ts) @@ -309,9 +328,10 @@ def _state_changed_during_period_stmt( limit: int | None, include_start_time_state: bool, run_start_ts: float | None, + include_last_reported: bool, ) -> Select | CompoundSelect: stmt = ( - _stmt_and_join_attributes(no_attributes, False) + _stmt_and_join_attributes(no_attributes, False, include_last_reported) .filter( ( (States.last_changed_ts == States.last_updated_ts) @@ -343,18 +363,22 @@ def _state_changed_during_period_stmt( single_metadata_id, no_attributes, False, + include_last_reported, ).subquery(), no_attributes, False, + include_last_reported, ), _select_from_subquery( stmt.subquery(), no_attributes, False, + include_last_reported, ), ).subquery(), no_attributes, False, + include_last_reported, ) @@ -369,6 +393,9 @@ def state_changes_during_period( include_start_time_state: bool = True, ) -> MutableMapping[str, list[State]]: """Return states changes during UTC period start_time - end_time.""" + has_last_reported = ( + recorder.get_instance(hass).schema_version >= LAST_REPORTED_SCHEMA_VERSION + ) if not entity_id: raise ValueError("entity_id must be provided") entity_ids = [entity_id.lower()] @@ -401,12 +428,14 @@ def state_changes_during_period( limit, include_start_time_state, run_start_ts, + has_last_reported, ), track_on=[ bool(end_time_ts), no_attributes, bool(limit), include_start_time_state, + has_last_reported, ], ) return cast( @@ -426,7 +455,7 @@ def state_changes_during_period( def _get_last_state_changes_single_stmt(metadata_id: int) -> Select: return ( - _stmt_and_join_attributes(False, False) + _stmt_and_join_attributes(False, False, False) .join( ( lastest_state_for_metadata_id := ( @@ -453,10 +482,10 @@ def _get_last_state_changes_single_stmt(metadata_id: int) -> Select: def _get_last_state_changes_multiple_stmt( - number_of_states: int, metadata_id: int + number_of_states: int, metadata_id: int, include_last_reported: bool ) -> Select: return ( - _stmt_and_join_attributes(False, False) + _stmt_and_join_attributes(False, False, include_last_reported) .where( States.state_id == ( @@ -478,6 +507,9 @@ def get_last_state_changes( hass: HomeAssistant, number_of_states: int, entity_id: str ) -> MutableMapping[str, list[State]]: """Return the last number_of_states.""" + has_last_reported = ( + recorder.get_instance(hass).schema_version >= LAST_REPORTED_SCHEMA_VERSION + ) entity_id_lower = entity_id.lower() entity_ids = [entity_id_lower] @@ -502,8 +534,9 @@ def get_last_state_changes( else: stmt = lambda_stmt( lambda: _get_last_state_changes_multiple_stmt( - number_of_states, metadata_id + number_of_states, metadata_id, has_last_reported ), + track_on=[has_last_reported], ) states = list(execute_stmt_lambda_element(session, stmt, orm_rows=False)) return cast( @@ -529,7 +562,9 @@ def _get_start_time_state_for_entities_stmt( # We got an include-list of entities, accelerate the query by filtering already # in the inner and the outer query. stmt = ( - _stmt_and_join_attributes_for_start_state(no_attributes, include_last_changed) + _stmt_and_join_attributes_for_start_state( + no_attributes, include_last_changed, False + ) .join( ( most_recent_states_for_entities_by_date := ( @@ -597,6 +632,7 @@ def _get_start_time_state_stmt( single_metadata_id, no_attributes, include_last_changed, + False, ) # We have more than one entity to look at so we need to do a query on states # since the last recorder run started. @@ -614,11 +650,14 @@ def _get_single_entity_start_time_stmt( metadata_id: int, no_attributes: bool, include_last_changed: bool, + include_last_reported: bool, ) -> Select: # Use an entirely different (and extremely fast) query if we only # have a single entity id stmt = ( - _stmt_and_join_attributes_for_start_state(no_attributes, include_last_changed) + _stmt_and_join_attributes_for_start_state( + no_attributes, include_last_changed, include_last_reported + ) .filter( States.last_updated_ts < epoch_time, States.metadata_id == metadata_id, @@ -757,7 +796,7 @@ def _sorted_states_to_dict( _utc_from_timestamp = dt_util.utc_from_timestamp ent_results.extend( { - attr_state: (prev_state := state), # noqa: F841 + attr_state: (prev_state := state), attr_time: _utc_from_timestamp(row[last_updated_ts_idx]).isoformat(), } for row in group diff --git a/homeassistant/components/recorder/icons.json b/homeassistant/components/recorder/icons.json new file mode 100644 index 00000000000..1090401abd5 --- /dev/null +++ b/homeassistant/components/recorder/icons.json @@ -0,0 +1,8 @@ +{ + "services": { + "purge": "mdi:database-sync", + "purge_entities": "mdi:database-sync", + "disable": "mdi:database-off", + "enable": "mdi:database" + } +} diff --git a/homeassistant/components/recorder/manifest.json b/homeassistant/components/recorder/manifest.json index 98b6d15facb..e5b20cfd3b0 100644 --- a/homeassistant/components/recorder/manifest.json +++ b/homeassistant/components/recorder/manifest.json @@ -1,13 +1,14 @@ { "domain": "recorder", "name": "Recorder", + "after_dependencies": ["http"], "codeowners": ["@home-assistant/core"], "documentation": "https://www.home-assistant.io/integrations/recorder", "integration_type": "system", "iot_class": "local_push", "quality_scale": "internal", "requirements": [ - "SQLAlchemy==2.0.27", + "SQLAlchemy==2.0.29", "fnv-hash-fast==0.5.0", "psutil-home-assistant==0.0.1" ] diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index 427e3acab2d..fc2e6ec2b3f 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -1,6 +1,8 @@ """Schema migration helpers.""" + from __future__ import annotations +from abc import ABC, abstractmethod from collections.abc import Callable, Iterable import contextlib from dataclasses import dataclass, replace as dataclass_replace @@ -24,6 +26,7 @@ from sqlalchemy.exc import ( from sqlalchemy.orm.session import Session from sqlalchemy.schema import AddConstraint, DropConstraint from sqlalchemy.sql.expression import true +from sqlalchemy.sql.lambdas import StatementLambdaElement from homeassistant.core import HomeAssistant from homeassistant.util.enum import try_parse_enum @@ -45,7 +48,12 @@ from .auto_repairs.statistics.schema import ( correct_db_schema as statistics_correct_db_schema, validate_db_schema as statistics_validate_db_schema, ) -from .const import SupportedDialect +from .const import ( + CONTEXT_ID_AS_BINARY_SCHEMA_VERSION, + EVENT_TYPE_IDS_SCHEMA_VERSION, + STATES_META_SCHEMA_VERSION, + SupportedDialect, +) from .db_schema import ( CONTEXT_ID_BIN_MAX_LENGTH, DOUBLE_PRECISION_TYPE_SQL, @@ -59,6 +67,7 @@ from .db_schema import ( Base, Events, EventTypes, + MigrationChanges, SchemaChanges, States, StatesMeta, @@ -79,6 +88,10 @@ from .queries import ( find_states_context_ids_to_migrate, find_unmigrated_short_term_statistics_rows, find_unmigrated_statistics_rows, + has_entity_ids_to_migrate, + has_event_type_to_migrate, + has_events_context_ids_to_migrate, + has_states_context_ids_to_migrate, has_used_states_event_ids, migrate_single_short_term_statistics_row_to_timestamp, migrate_single_statistics_row_to_timestamp, @@ -86,11 +99,17 @@ from .queries import ( from .statistics import get_start_time from .tasks import ( CommitTask, + EntityIDMigrationTask, + EventsContextIDMigrationTask, + EventTypeIDMigrationTask, PostSchemaMigrationTask, + RecorderTask, + StatesContextIDMigrationTask, StatisticsTimestampMigrationCleanupTask, ) from .util import ( database_job_retry_wrapper, + execute_stmt_lambda_element, get_index_by_name, retryable_database_job, session_scope, @@ -162,7 +181,7 @@ def _get_schema_version(session: Session) -> int | None: def get_schema_version(session_maker: Callable[[], Session]) -> int | None: """Get the schema version.""" try: - with session_scope(session=session_maker()) as session: + with session_scope(session=session_maker(), read_only=True) as session: return _get_schema_version(session) except Exception as err: # pylint: disable=broad-except _LOGGER.exception("Error when determining DB schema version: %s", err) @@ -511,21 +530,20 @@ def _update_states_table_with_foreign_key_options( ) -> None: """Add the options to foreign key constraints.""" inspector = sqlalchemy.inspect(engine) - alters = [] - for foreign_key in inspector.get_foreign_keys(TABLE_STATES): - if foreign_key["name"] and ( + alters = [ + { + "old_fk": ForeignKeyConstraint((), (), name=foreign_key["name"]), + "columns": foreign_key["constrained_columns"], + } + for foreign_key in inspector.get_foreign_keys(TABLE_STATES) + if foreign_key["name"] + and ( # MySQL/MariaDB will have empty options not foreign_key.get("options") - or # Postgres will have ondelete set to None - foreign_key.get("options", {}).get("ondelete") is None - ): - alters.append( - { - "old_fk": ForeignKeyConstraint((), (), name=foreign_key["name"]), - "columns": foreign_key["constrained_columns"], - } - ) + or foreign_key.get("options", {}).get("ondelete") is None + ) + ] if not alters: return @@ -556,10 +574,11 @@ def _drop_foreign_key_constraints( ) -> None: """Drop foreign key constraints for a table on specific columns.""" inspector = sqlalchemy.inspect(engine) - drops = [] - for foreign_key in inspector.get_foreign_keys(table): - if foreign_key["name"] and foreign_key["constrained_columns"] == columns: - drops.append(ForeignKeyConstraint((), (), name=foreign_key["name"])) + drops = [ + ForeignKeyConstraint((), (), name=foreign_key["name"]) + for foreign_key in inspector.get_foreign_keys(table) + if foreign_key["name"] and foreign_key["constrained_columns"] == columns + ] # Bind the ForeignKeyConstraints to the table old_table = Table(table, MetaData(), *drops) # noqa: F841 @@ -859,9 +878,10 @@ def _apply_update( # noqa: C901 if engine.dialect.name == SupportedDialect.MYSQL: # Ensure the row format is dynamic or the index # unique will be too large - with contextlib.suppress(SQLAlchemyError), session_scope( - session=session_maker() - ) as session: + with ( + contextlib.suppress(SQLAlchemyError), + session_scope(session=session_maker()) as session, + ): connection = session.connection() # This is safe to run multiple times and fast # since the table is small. @@ -1061,6 +1081,12 @@ def _apply_update( # noqa: C901 _migrate_statistics_columns_to_timestamp_removing_duplicates( hass, instance, session_maker, engine ) + elif new_version == 43: + _add_columns( + session_maker, + "states", + [f"last_reported_ts {_column_types.timestamp_type}"], + ) else: raise ValueError(f"No schema migration defined for version {new_version}") @@ -1113,9 +1139,10 @@ def _correct_table_character_set_and_collation( "computers. Please be patient!", table, ) - with contextlib.suppress(SQLAlchemyError), session_scope( - session=session_maker() - ) as session: + with ( + contextlib.suppress(SQLAlchemyError), + session_scope(session=session_maker()) as session, + ): connection = session.connection() connection.execute( # Using LOCK=EXCLUSIVE to prevent the database from corrupting @@ -1477,7 +1504,8 @@ def migrate_states_context_ids(instance: Recorder) -> bool: ) # If there is more work to do return False # so that we can be called again - is_done = not states + if is_done := not states: + _mark_migration_done(session, StatesContextIDMigration) if is_done: _drop_index(session_maker, "states", "ix_states_context_id") @@ -1514,7 +1542,8 @@ def migrate_events_context_ids(instance: Recorder) -> bool: ) # If there is more work to do return False # so that we can be called again - is_done = not events + if is_done := not events: + _mark_migration_done(session, EventsContextIDMigration) if is_done: _drop_index(session_maker, "events", "ix_events_context_id") @@ -1558,9 +1587,9 @@ def migrate_event_type_ids(instance: Recorder) -> bool: assert ( db_event_type.event_type is not None ), "event_type should never be None" - event_type_to_id[ - db_event_type.event_type - ] = db_event_type.event_type_id + event_type_to_id[db_event_type.event_type] = ( + db_event_type.event_type_id + ) event_type_manager.clear_non_existent(db_event_type.event_type) session.execute( @@ -1579,7 +1608,8 @@ def migrate_event_type_ids(instance: Recorder) -> bool: # If there is more work to do return False # so that we can be called again - is_done = not events + if is_done := not events: + _mark_migration_done(session, EventTypeIDMigration) if is_done: instance.event_type_manager.active = True @@ -1630,9 +1660,9 @@ def migrate_entity_ids(instance: Recorder) -> bool: assert ( db_states_metadata.entity_id is not None ), "entity_id should never be None" - entity_id_to_metadata_id[ - db_states_metadata.entity_id - ] = db_states_metadata.metadata_id + entity_id_to_metadata_id[db_states_metadata.entity_id] = ( + db_states_metadata.metadata_id + ) session.execute( update(States), @@ -1653,7 +1683,8 @@ def migrate_entity_ids(instance: Recorder) -> bool: # If there is more work to do return False # so that we can be called again - is_done = not states + if is_done := not states: + _mark_migration_done(session, EntityIDMigration) _LOGGER.debug("Migrating entity_ids done=%s", is_done) return is_done @@ -1748,11 +1779,116 @@ def _initialize_database(session: Session) -> bool: def initialize_database(session_maker: Callable[[], Session]) -> bool: """Initialize a new database.""" try: - with session_scope(session=session_maker()) as session: + with session_scope(session=session_maker(), read_only=True) as session: if _get_schema_version(session) is not None: return True + + with session_scope(session=session_maker()) as session: return _initialize_database(session) except Exception as err: # pylint: disable=broad-except _LOGGER.exception("Error when initialise database: %s", err) return False + + +class BaseRunTimeMigration(ABC): + """Base class for run time migrations.""" + + required_schema_version = 0 + migration_version = 1 + migration_id: str + task: Callable[[], RecorderTask] + + def __init__( + self, session: Session, schema_version: int, migration_changes: dict[str, int] + ) -> None: + """Initialize a new BaseRunTimeMigration.""" + self.schema_version = schema_version + self.session = session + self.migration_changes = migration_changes + + @abstractmethod + def needs_migrate_query(self) -> StatementLambdaElement: + """Return the query to check if the migration needs to run.""" + + def needs_migrate(self) -> bool: + """Return if the migration needs to run. + + If the migration needs to run, it will return True. + + If the migration does not need to run, it will return False and + mark the migration as done in the database if its not already + marked as done. + """ + if self.schema_version < self.required_schema_version: + # Schema is too old, we must have to migrate + return True + if self.migration_changes.get(self.migration_id, -1) >= self.migration_version: + # The migration changes table indicates that the migration has been done + return False + # We do not know if the migration is done from the + # migration changes table so we must check the data + # This is the slow path + if not execute_stmt_lambda_element(self.session, self.needs_migrate_query()): + _mark_migration_done(self.session, self.__class__) + return False + return True + + +class StatesContextIDMigration(BaseRunTimeMigration): + """Migration to migrate states context_ids to binary format.""" + + required_schema_version = CONTEXT_ID_AS_BINARY_SCHEMA_VERSION + migration_id = "state_context_id_as_binary" + task = StatesContextIDMigrationTask + + def needs_migrate_query(self) -> StatementLambdaElement: + """Return the query to check if the migration needs to run.""" + return has_states_context_ids_to_migrate() + + +class EventsContextIDMigration(BaseRunTimeMigration): + """Migration to migrate events context_ids to binary format.""" + + required_schema_version = CONTEXT_ID_AS_BINARY_SCHEMA_VERSION + migration_id = "event_context_id_as_binary" + task = EventsContextIDMigrationTask + + def needs_migrate_query(self) -> StatementLambdaElement: + """Return the query to check if the migration needs to run.""" + return has_events_context_ids_to_migrate() + + +class EventTypeIDMigration(BaseRunTimeMigration): + """Migration to migrate event_type to event_type_ids.""" + + required_schema_version = EVENT_TYPE_IDS_SCHEMA_VERSION + migration_id = "event_type_id_migration" + task = EventTypeIDMigrationTask + + def needs_migrate_query(self) -> StatementLambdaElement: + """Check if the data is migrated.""" + return has_event_type_to_migrate() + + +class EntityIDMigration(BaseRunTimeMigration): + """Migration to migrate entity_ids to states_meta.""" + + required_schema_version = STATES_META_SCHEMA_VERSION + migration_id = "entity_id_migration" + task = EntityIDMigrationTask + + def needs_migrate_query(self) -> StatementLambdaElement: + """Check if the data is migrated.""" + return has_entity_ids_to_migrate() + + +def _mark_migration_done( + session: Session, migration: type[BaseRunTimeMigration] +) -> None: + """Mark a migration as done in the database.""" + session.merge( + MigrationChanges( + migration_id=migration.migration_id, version=migration.migration_version + ) + ) diff --git a/homeassistant/components/recorder/models/__init__.py b/homeassistant/components/recorder/models/__init__.py index 1a204e767e3..d43a1da161e 100644 --- a/homeassistant/components/recorder/models/__init__.py +++ b/homeassistant/components/recorder/models/__init__.py @@ -1,4 +1,5 @@ """Models for Recorder.""" + from __future__ import annotations from .context import ( diff --git a/homeassistant/components/recorder/models/context.py b/homeassistant/components/recorder/models/context.py index f25e4d4412f..c09ee366b84 100644 --- a/homeassistant/components/recorder/models/context.py +++ b/homeassistant/components/recorder/models/context.py @@ -1,4 +1,5 @@ """Models for Recorder.""" + from __future__ import annotations from contextlib import suppress diff --git a/homeassistant/components/recorder/models/database.py b/homeassistant/components/recorder/models/database.py index a8c23d20061..94c5a7cc027 100644 --- a/homeassistant/components/recorder/models/database.py +++ b/homeassistant/components/recorder/models/database.py @@ -1,4 +1,5 @@ """Models for the database in the Recorder.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/homeassistant/components/recorder/models/event.py b/homeassistant/components/recorder/models/event.py index 1d644b62f46..379a6fddb1d 100644 --- a/homeassistant/components/recorder/models/event.py +++ b/homeassistant/components/recorder/models/event.py @@ -1,4 +1,5 @@ """Models events in for Recorder.""" + from __future__ import annotations diff --git a/homeassistant/components/recorder/models/legacy.py b/homeassistant/components/recorder/models/legacy.py index 398ad773ba2..4b32ae65748 100644 --- a/homeassistant/components/recorder/models/legacy.py +++ b/homeassistant/components/recorder/models/legacy.py @@ -1,4 +1,5 @@ """Models for Recorder.""" + from __future__ import annotations from datetime import datetime @@ -47,6 +48,7 @@ class LegacyLazyStatePreSchema31(State): self.state = self._row.state or "" self._attributes: dict[str, Any] | None = None self._last_changed: datetime | None = start_time + self._last_reported: datetime | None = start_time self._last_updated: datetime | None = start_time self._context: Context | None = None self.attr_cache = attr_cache @@ -92,6 +94,18 @@ class LegacyLazyStatePreSchema31(State): """Set last changed datetime.""" self._last_changed = value + @property + def last_reported(self) -> datetime: + """Last reported datetime.""" + if self._last_reported is None: + self._last_reported = self.last_updated + return self._last_reported + + @last_reported.setter + def last_reported(self, value: datetime) -> None: + """Set last reported datetime.""" + self._last_reported = value + @property def last_updated(self) -> datetime: """Last updated datetime.""" @@ -195,6 +209,7 @@ class LegacyLazyState(State): self._last_changed_ts: float | None = ( self._row.last_changed_ts or self._last_updated_ts ) + self._last_reported_ts: float | None = self._last_updated_ts self._context: Context | None = None self.attr_cache = attr_cache @@ -235,6 +250,17 @@ class LegacyLazyState(State): """Set last changed datetime.""" self._last_changed_ts = process_timestamp(value).timestamp() + @property + def last_reported(self) -> datetime: + """Last reported datetime.""" + assert self._last_reported_ts is not None + return dt_util.utc_from_timestamp(self._last_reported_ts) + + @last_reported.setter + def last_reported(self, value: datetime) -> None: + """Set last reported datetime.""" + self._last_reported_ts = process_timestamp(value).timestamp() + @property def last_updated(self) -> datetime: """Last updated datetime.""" diff --git a/homeassistant/components/recorder/models/state.py b/homeassistant/components/recorder/models/state.py index 5f469638ec0..e1f23f32118 100644 --- a/homeassistant/components/recorder/models/state.py +++ b/homeassistant/components/recorder/models/state.py @@ -1,4 +1,5 @@ """Models states in for Recorder.""" + from __future__ import annotations from datetime import datetime @@ -80,6 +81,18 @@ class LazyState(State): self._last_changed_ts or self._last_updated_ts ) + @cached_property + def _last_reported_ts(self) -> float | None: + """Last reported timestamp.""" + return getattr(self._row, "last_reported_ts", None) + + @cached_property + def last_reported(self) -> datetime: # type: ignore[override] + """Last reported datetime.""" + return dt_util.utc_from_timestamp( + self._last_reported_ts or self._last_updated_ts + ) + @cached_property def last_updated(self) -> datetime: # type: ignore[override] """Last updated datetime.""" diff --git a/homeassistant/components/recorder/models/statistics.py b/homeassistant/components/recorder/models/statistics.py index 4cf465955c5..ad4d82067c4 100644 --- a/homeassistant/components/recorder/models/statistics.py +++ b/homeassistant/components/recorder/models/statistics.py @@ -1,4 +1,5 @@ """Models for statistics in the Recorder.""" + from __future__ import annotations from datetime import datetime, timedelta diff --git a/homeassistant/components/recorder/models/time.py b/homeassistant/components/recorder/models/time.py index 40e4afd18a7..6295060c8d3 100644 --- a/homeassistant/components/recorder/models/time.py +++ b/homeassistant/components/recorder/models/time.py @@ -1,4 +1,5 @@ """Models for Recorder.""" + from __future__ import annotations from datetime import datetime @@ -15,13 +16,11 @@ EMPTY_JSON_OBJECT = "{}" @overload -def process_timestamp(ts: None) -> None: - ... +def process_timestamp(ts: None) -> None: ... @overload -def process_timestamp(ts: datetime) -> datetime: - ... +def process_timestamp(ts: datetime) -> datetime: ... def process_timestamp(ts: datetime | None) -> datetime | None: @@ -35,13 +34,11 @@ def process_timestamp(ts: datetime | None) -> datetime | None: @overload -def process_timestamp_to_utc_isoformat(ts: None) -> None: - ... +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) -> str: ... def process_timestamp_to_utc_isoformat(ts: datetime | None) -> str | None: diff --git a/homeassistant/components/recorder/pool.py b/homeassistant/components/recorder/pool.py index 46f140305e3..27bc313b162 100644 --- a/homeassistant/components/recorder/pool.py +++ b/homeassistant/components/recorder/pool.py @@ -1,4 +1,5 @@ """A pool for sqlite connections.""" + import logging import threading import traceback diff --git a/homeassistant/components/recorder/purge.py b/homeassistant/components/recorder/purge.py index a9d8c0b2482..f42bae00abe 100644 --- a/homeassistant/components/recorder/purge.py +++ b/homeassistant/components/recorder/purge.py @@ -1,4 +1,5 @@ """Purge old data helper.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/recorder/queries.py b/homeassistant/components/recorder/queries.py index c03057b31b2..d982576620d 100644 --- a/homeassistant/components/recorder/queries.py +++ b/homeassistant/components/recorder/queries.py @@ -1,4 +1,5 @@ """Queries for the recorder.""" + from __future__ import annotations from collections.abc import Iterable @@ -12,6 +13,7 @@ from .db_schema import ( EventData, Events, EventTypes, + MigrationChanges, RecorderRuns, StateAttributes, States, @@ -811,6 +813,13 @@ def find_states_context_ids_to_migrate(max_bind_vars: int) -> StatementLambdaEle ) +def get_migration_changes() -> StatementLambdaElement: + """Query the database for previous migration changes.""" + return lambda_stmt( + lambda: select(MigrationChanges.migration_id, MigrationChanges.version) + ) + + def find_event_types_to_purge() -> StatementLambdaElement: """Find event_type_ids to purge.""" return lambda_stmt( diff --git a/homeassistant/components/recorder/repack.py b/homeassistant/components/recorder/repack.py index 53c922cf481..8c7ad137d86 100644 --- a/homeassistant/components/recorder/repack.py +++ b/homeassistant/components/recorder/repack.py @@ -1,4 +1,5 @@ """Purge repack helper.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/recorder/services.py b/homeassistant/components/recorder/services.py index fb2cd1f0bef..b4d719a9481 100644 --- a/homeassistant/components/recorder/services.py +++ b/homeassistant/components/recorder/services.py @@ -1,4 +1,5 @@ """Support for recorder services.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index ab4626c192b..f840fdbd7b6 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -1,4 +1,5 @@ """Statistics helper.""" + from __future__ import annotations from collections import defaultdict @@ -2038,7 +2039,7 @@ def _fast_build_sum_list( ] -def _sorted_statistics_to_dict( # noqa: C901 +def _sorted_statistics_to_dict( hass: HomeAssistant, session: Session, stats: Sequence[Row[Any]], @@ -2062,7 +2063,7 @@ def _sorted_statistics_to_dict( # noqa: C901 seen_statistic_ids: set[str] = set() key_func = itemgetter(metadata_id_idx) for meta_id, group in groupby(stats, key_func): - stats_list = stats_by_meta_id[meta_id] = list(group) + stats_by_meta_id[meta_id] = list(group) seen_statistic_ids.add(metadata[meta_id]["statistic_id"]) # Set all statistic IDs to empty lists in result set to maintain the order diff --git a/homeassistant/components/recorder/system_health/__init__.py b/homeassistant/components/recorder/system_health/__init__.py index a3545ec2c89..16feaa19886 100644 --- a/homeassistant/components/recorder/system_health/__init__.py +++ b/homeassistant/components/recorder/system_health/__init__.py @@ -1,4 +1,5 @@ """Provide info to system health.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/recorder/system_health/mysql.py b/homeassistant/components/recorder/system_health/mysql.py index 1ade699eaf1..21d9d952d3a 100644 --- a/homeassistant/components/recorder/system_health/mysql.py +++ b/homeassistant/components/recorder/system_health/mysql.py @@ -1,4 +1,5 @@ """Provide info to system health for mysql.""" + from __future__ import annotations from sqlalchemy import text diff --git a/homeassistant/components/recorder/system_health/postgresql.py b/homeassistant/components/recorder/system_health/postgresql.py index aa9197a8e85..b917e548ae5 100644 --- a/homeassistant/components/recorder/system_health/postgresql.py +++ b/homeassistant/components/recorder/system_health/postgresql.py @@ -1,4 +1,5 @@ """Provide info to system health for postgresql.""" + from __future__ import annotations from sqlalchemy import text diff --git a/homeassistant/components/recorder/system_health/sqlite.py b/homeassistant/components/recorder/system_health/sqlite.py index 01c601aa9e9..95123d1fd14 100644 --- a/homeassistant/components/recorder/system_health/sqlite.py +++ b/homeassistant/components/recorder/system_health/sqlite.py @@ -1,4 +1,5 @@ """Provide info to system health for sqlite.""" + from __future__ import annotations from sqlalchemy import text diff --git a/homeassistant/components/recorder/table_managers/event_data.py b/homeassistant/components/recorder/table_managers/event_data.py index 4c46b1b9faf..e8bb3f2300f 100644 --- a/homeassistant/components/recorder/table_managers/event_data.py +++ b/homeassistant/components/recorder/table_managers/event_data.py @@ -1,4 +1,5 @@ """Support managing EventData.""" + from __future__ import annotations from collections.abc import Iterable diff --git a/homeassistant/components/recorder/table_managers/event_types.py b/homeassistant/components/recorder/table_managers/event_types.py index c74684a0f77..94ceab7bf68 100644 --- a/homeassistant/components/recorder/table_managers/event_types.py +++ b/homeassistant/components/recorder/table_managers/event_types.py @@ -1,4 +1,5 @@ """Support managing EventTypes.""" + from __future__ import annotations from collections.abc import Iterable diff --git a/homeassistant/components/recorder/table_managers/recorder_runs.py b/homeassistant/components/recorder/table_managers/recorder_runs.py index 455c8375b1c..b0b9818118b 100644 --- a/homeassistant/components/recorder/table_managers/recorder_runs.py +++ b/homeassistant/components/recorder/table_managers/recorder_runs.py @@ -1,4 +1,5 @@ """Track recorder run history.""" + from __future__ import annotations import bisect diff --git a/homeassistant/components/recorder/table_managers/state_attributes.py b/homeassistant/components/recorder/table_managers/state_attributes.py index ddaf8cb4fca..e2fb9153be8 100644 --- a/homeassistant/components/recorder/table_managers/state_attributes.py +++ b/homeassistant/components/recorder/table_managers/state_attributes.py @@ -1,4 +1,5 @@ """Support managing StateAttributes.""" + from __future__ import annotations from collections.abc import Iterable diff --git a/homeassistant/components/recorder/table_managers/states.py b/homeassistant/components/recorder/table_managers/states.py index fcfdcef0891..d5cef759c54 100644 --- a/homeassistant/components/recorder/table_managers/states.py +++ b/homeassistant/components/recorder/table_managers/states.py @@ -1,4 +1,5 @@ """Support managing States.""" + from __future__ import annotations from ..db_schema import States @@ -11,6 +12,7 @@ class StatesManager: """Initialize the states manager for linking old_state_id.""" self._pending: dict[str, States] = {} self._last_committed_id: dict[str, int] = {} + self._last_reported: dict[int, float] = {} def pop_pending(self, entity_id: str) -> States | None: """Pop a pending state. @@ -43,6 +45,16 @@ class StatesManager: """ self._pending[entity_id] = state + def update_pending_last_reported( + self, state_id: int, last_reported_timestamp: float + ) -> None: + """Update the last reported timestamp for a state.""" + self._last_reported[state_id] = last_reported_timestamp + + def get_pending_last_reported_timestamp(self) -> dict[int, float]: + """Return the last reported timestamp for all pending states.""" + return self._last_reported + def post_commit_pending(self) -> None: """Call after commit to load the state_id of the new States into committed. @@ -52,6 +64,7 @@ class StatesManager: for entity_id, db_states in self._pending.items(): self._last_committed_id[entity_id] = db_states.state_id self._pending.clear() + self._last_reported.clear() def reset(self) -> None: """Reset after the database has been reset or changed. diff --git a/homeassistant/components/recorder/table_managers/states_meta.py b/homeassistant/components/recorder/table_managers/states_meta.py index 9b7aa1f7f96..ebc1dab45f3 100644 --- a/homeassistant/components/recorder/table_managers/states_meta.py +++ b/homeassistant/components/recorder/table_managers/states_meta.py @@ -1,4 +1,5 @@ """Support managing StatesMeta.""" + from __future__ import annotations from collections.abc import Iterable, Sequence diff --git a/homeassistant/components/recorder/table_managers/statistics_meta.py b/homeassistant/components/recorder/table_managers/statistics_meta.py index d6c69e2682b..32e989b0e3d 100644 --- a/homeassistant/components/recorder/table_managers/statistics_meta.py +++ b/homeassistant/components/recorder/table_managers/statistics_meta.py @@ -1,4 +1,5 @@ """Support managing StatesMeta.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/recorder/tasks.py b/homeassistant/components/recorder/tasks.py index c062eb3915f..1b81d7a983f 100644 --- a/homeassistant/components/recorder/tasks.py +++ b/homeassistant/components/recorder/tasks.py @@ -1,4 +1,5 @@ """Support for recording details.""" + from __future__ import annotations import abc diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index 8ed5c3e8cdc..770dc91353c 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -1,4 +1,5 @@ """SQLAlchemy util functions.""" + from __future__ import annotations from collections.abc import Callable, Collection, Generator, Iterable, Sequence @@ -159,7 +160,7 @@ def execute( This method also retries a few times in the case of stale connections. """ debug = _LOGGER.isEnabledFor(logging.DEBUG) - for tryno in range(0, RETRIES): + for tryno in range(RETRIES): try: if debug: timer_start = time.perf_counter() @@ -644,7 +645,7 @@ def retryable_database_job( return job(instance, *args, **kwargs) except OperationalError as err: if _is_retryable_error(instance, err): - assert isinstance(err.orig, BaseException) + assert isinstance(err.orig, BaseException) # noqa: PT017 _LOGGER.info( "%s; %s not completed, retrying", err.orig.args[1], description ) @@ -690,7 +691,7 @@ def database_job_retry_wrapper( instance, err ): raise - assert isinstance(err.orig, BaseException) + assert isinstance(err.orig, BaseException) # noqa: PT017 _LOGGER.info( "%s; %s failed, retrying", err.orig.args[1], description ) diff --git a/homeassistant/components/recorder/websocket_api.py b/homeassistant/components/recorder/websocket_api.py index f2b4df1d0cc..79104485e19 100644 --- a/homeassistant/components/recorder/websocket_api.py +++ b/homeassistant/components/recorder/websocket_api.py @@ -1,4 +1,5 @@ """The Recorder websocket API.""" + from __future__ import annotations from datetime import datetime as dt diff --git a/homeassistant/components/recovery_mode/__init__.py b/homeassistant/components/recovery_mode/__init__.py index 46a8d320663..8d02d6044c2 100644 --- a/homeassistant/components/recovery_mode/__init__.py +++ b/homeassistant/components/recovery_mode/__init__.py @@ -1,4 +1,5 @@ """The Recovery Mode integration.""" + from homeassistant.components import persistent_notification from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv diff --git a/homeassistant/components/recswitch/switch.py b/homeassistant/components/recswitch/switch.py index 43e71ef1df9..a0035d50582 100644 --- a/homeassistant/components/recswitch/switch.py +++ b/homeassistant/components/recswitch/switch.py @@ -1,4 +1,5 @@ """Support for Ankuoo RecSwitch MS6126 devices.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/reddit/sensor.py b/homeassistant/components/reddit/sensor.py index 43550658ac3..47aa2ab86f6 100644 --- a/homeassistant/components/reddit/sensor.py +++ b/homeassistant/components/reddit/sensor.py @@ -1,4 +1,5 @@ """Support for Reddit.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/refoss/__init__.py b/homeassistant/components/refoss/__init__.py index d83ca17dd6b..666a17847c9 100644 --- a/homeassistant/components/refoss/__init__.py +++ b/homeassistant/components/refoss/__init__.py @@ -1,4 +1,5 @@ """Refoss devices platform loader.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/refoss/bridge.py b/homeassistant/components/refoss/bridge.py index 888179e8a7c..f7bb526d11a 100644 --- a/homeassistant/components/refoss/bridge.py +++ b/homeassistant/components/refoss/bridge.py @@ -1,4 +1,5 @@ """Refoss integration.""" + from __future__ import annotations from refoss_ha.device import DeviceInfo diff --git a/homeassistant/components/refoss/const.py b/homeassistant/components/refoss/const.py index dd11921c75e..86e40fce43c 100644 --- a/homeassistant/components/refoss/const.py +++ b/homeassistant/components/refoss/const.py @@ -1,4 +1,5 @@ """const.""" + from __future__ import annotations from logging import Logger, getLogger diff --git a/homeassistant/components/refoss/coordinator.py b/homeassistant/components/refoss/coordinator.py index a542f0e1ae8..8b03313d6d6 100644 --- a/homeassistant/components/refoss/coordinator.py +++ b/homeassistant/components/refoss/coordinator.py @@ -1,4 +1,5 @@ """Helper and coordinator for refoss.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/refoss/entity.py b/homeassistant/components/refoss/entity.py index d3425974bb1..3032c32ed51 100644 --- a/homeassistant/components/refoss/entity.py +++ b/homeassistant/components/refoss/entity.py @@ -1,4 +1,5 @@ """Entity object for shared properties of Refoss entities.""" + from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/refoss/util.py b/homeassistant/components/refoss/util.py index cd589022d73..4c44b9537af 100644 --- a/homeassistant/components/refoss/util.py +++ b/homeassistant/components/refoss/util.py @@ -1,4 +1,5 @@ """Refoss helpers functions.""" + from __future__ import annotations from refoss_ha.discovery import Discovery diff --git a/homeassistant/components/rejseplanen/manifest.json b/homeassistant/components/rejseplanen/manifest.json index a54bc1f075b..72da7a65f45 100644 --- a/homeassistant/components/rejseplanen/manifest.json +++ b/homeassistant/components/rejseplanen/manifest.json @@ -1,7 +1,7 @@ { "domain": "rejseplanen", "name": "Rejseplanen", - "codeowners": ["@DarkFox"], + "codeowners": [], "documentation": "https://www.home-assistant.io/integrations/rejseplanen", "iot_class": "cloud_polling", "loggers": ["rjpl"], diff --git a/homeassistant/components/rejseplanen/sensor.py b/homeassistant/components/rejseplanen/sensor.py index 135205aa95d..d95b9e1b271 100644 --- a/homeassistant/components/rejseplanen/sensor.py +++ b/homeassistant/components/rejseplanen/sensor.py @@ -3,6 +3,7 @@ For more info on the API see: https://help.rejseplanen.dk/hc/en-us/articles/214174465-Rejseplanen-s-API """ + from __future__ import annotations from contextlib import suppress @@ -238,9 +239,9 @@ class PublicTransportData: } if real_time_date is not None and real_time_time is not None: - departure_data[ - ATTR_REAL_TIME_AT - ] = f"{real_time_date} {real_time_time}" + departure_data[ATTR_REAL_TIME_AT] = ( + f"{real_time_date} {real_time_time}" + ) if item.get("rtTrack") is not None: departure_data[ATTR_TRACK] = item.get("rtTrack") diff --git a/homeassistant/components/remember_the_milk/__init__.py b/homeassistant/components/remember_the_milk/__init__.py index 82025eeba5d..3d1654960a7 100644 --- a/homeassistant/components/remember_the_milk/__init__.py +++ b/homeassistant/components/remember_the_milk/__init__.py @@ -1,4 +1,5 @@ """Support to interact with Remember The Milk.""" + import json import logging import os diff --git a/homeassistant/components/remember_the_milk/icons.json b/homeassistant/components/remember_the_milk/icons.json new file mode 100644 index 00000000000..3ca17113fb8 --- /dev/null +++ b/homeassistant/components/remember_the_milk/icons.json @@ -0,0 +1,6 @@ +{ + "services": { + "create_task": "mdi:check", + "complete_task": "mdi:check-all" + } +} diff --git a/homeassistant/components/remote/__init__.py b/homeassistant/components/remote/__init__.py index c5facb9785c..2b88c51e936 100644 --- a/homeassistant/components/remote/__init__.py +++ b/homeassistant/components/remote/__init__.py @@ -1,4 +1,5 @@ """Support to interface with universal remote control devices.""" + from __future__ import annotations from collections.abc import Iterable @@ -234,7 +235,7 @@ class RemoteEntity(ToggleEntity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_) def send_command(self, command: Iterable[str], **kwargs: Any) -> None: """Send commands to a device.""" - raise NotImplementedError() + raise NotImplementedError async def async_send_command(self, command: Iterable[str], **kwargs: Any) -> None: """Send commands to a device.""" @@ -244,7 +245,7 @@ class RemoteEntity(ToggleEntity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_) def learn_command(self, **kwargs: Any) -> None: """Learn a command from a device.""" - raise NotImplementedError() + raise NotImplementedError async def async_learn_command(self, **kwargs: Any) -> None: """Learn a command from a device.""" @@ -252,7 +253,7 @@ class RemoteEntity(ToggleEntity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_) def delete_command(self, **kwargs: Any) -> None: """Delete commands from the database.""" - raise NotImplementedError() + raise NotImplementedError async def async_delete_command(self, **kwargs: Any) -> None: """Delete commands from the database.""" diff --git a/homeassistant/components/remote/device_action.py b/homeassistant/components/remote/device_action.py index 936c7aca37a..a0ae707724e 100644 --- a/homeassistant/components/remote/device_action.py +++ b/homeassistant/components/remote/device_action.py @@ -1,4 +1,5 @@ """Provides device actions for remotes.""" + from __future__ import annotations import voluptuous as vol diff --git a/homeassistant/components/remote/device_condition.py b/homeassistant/components/remote/device_condition.py index 33f680e6829..f34b7f61580 100644 --- a/homeassistant/components/remote/device_condition.py +++ b/homeassistant/components/remote/device_condition.py @@ -1,4 +1,5 @@ """Provides device conditions for remotes.""" + from __future__ import annotations import voluptuous as vol diff --git a/homeassistant/components/remote/device_trigger.py b/homeassistant/components/remote/device_trigger.py index f2d5c54e3c3..0f08cb155aa 100644 --- a/homeassistant/components/remote/device_trigger.py +++ b/homeassistant/components/remote/device_trigger.py @@ -1,4 +1,5 @@ """Provides device triggers for remotes.""" + from __future__ import annotations import voluptuous as vol diff --git a/homeassistant/components/remote/group.py b/homeassistant/components/remote/group.py deleted file mode 100644 index 234883ffd5a..00000000000 --- a/homeassistant/components/remote/group.py +++ /dev/null @@ -1,14 +0,0 @@ -"""Describe group states.""" - - -from homeassistant.components.group import GroupIntegrationRegistry -from homeassistant.const import STATE_OFF, STATE_ON -from homeassistant.core import HomeAssistant, callback - - -@callback -def async_describe_on_off_states( - hass: HomeAssistant, registry: GroupIntegrationRegistry -) -> None: - """Describe group on off states.""" - registry.on_off_states({STATE_ON}, STATE_OFF) diff --git a/homeassistant/components/remote/reproduce_state.py b/homeassistant/components/remote/reproduce_state.py index 064e4a9711a..06a04acf0ef 100644 --- a/homeassistant/components/remote/reproduce_state.py +++ b/homeassistant/components/remote/reproduce_state.py @@ -1,4 +1,5 @@ """Reproduce an Remote state.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/remote/significant_change.py b/homeassistant/components/remote/significant_change.py index 8e5a3669041..5d2dff87909 100644 --- a/homeassistant/components/remote/significant_change.py +++ b/homeassistant/components/remote/significant_change.py @@ -1,4 +1,5 @@ """Helper to test significant Remote state changes.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/remote_rpi_gpio/__init__.py b/homeassistant/components/remote_rpi_gpio/__init__.py index 1654cc0c01d..7ab7e89b4d5 100644 --- a/homeassistant/components/remote_rpi_gpio/__init__.py +++ b/homeassistant/components/remote_rpi_gpio/__init__.py @@ -1,4 +1,5 @@ """Support for controlling GPIO pins of a Raspberry Pi.""" + from gpiozero import LED, DigitalInputDevice from gpiozero.pins.pigpio import PiGPIOFactory diff --git a/homeassistant/components/remote_rpi_gpio/binary_sensor.py b/homeassistant/components/remote_rpi_gpio/binary_sensor.py index bc0e694e8eb..ad995614ed4 100644 --- a/homeassistant/components/remote_rpi_gpio/binary_sensor.py +++ b/homeassistant/components/remote_rpi_gpio/binary_sensor.py @@ -1,4 +1,5 @@ """Support for binary sensor using RPi GPIO.""" + from __future__ import annotations import requests diff --git a/homeassistant/components/remote_rpi_gpio/switch.py b/homeassistant/components/remote_rpi_gpio/switch.py index 962cf6b4f3c..756e9dcfce9 100644 --- a/homeassistant/components/remote_rpi_gpio/switch.py +++ b/homeassistant/components/remote_rpi_gpio/switch.py @@ -1,4 +1,5 @@ """Allows to configure a switch using RPi GPIO.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/renault/__init__.py b/homeassistant/components/renault/__init__.py index 6b5679088a0..62425d9c20e 100644 --- a/homeassistant/components/renault/__init__.py +++ b/homeassistant/components/renault/__init__.py @@ -1,4 +1,5 @@ """Support for Renault devices.""" + import aiohttp from renault_api.gigya.exceptions import GigyaException @@ -20,16 +21,16 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b config_entry.data[CONF_USERNAME], config_entry.data[CONF_PASSWORD] ) except (aiohttp.ClientConnectionError, GigyaException) as exc: - raise ConfigEntryNotReady() from exc + raise ConfigEntryNotReady from exc if not login_success: - raise ConfigEntryAuthFailed() + raise ConfigEntryAuthFailed hass.data.setdefault(DOMAIN, {}) try: await renault_hub.async_initialise(config_entry) except aiohttp.ClientError as exc: - raise ConfigEntryNotReady() from exc + raise ConfigEntryNotReady from exc hass.data[DOMAIN][config_entry.entry_id] = renault_hub diff --git a/homeassistant/components/renault/binary_sensor.py b/homeassistant/components/renault/binary_sensor.py index 0d66e5444e7..37e91a1e435 100644 --- a/homeassistant/components/renault/binary_sensor.py +++ b/homeassistant/components/renault/binary_sensor.py @@ -1,7 +1,7 @@ """Support for Renault binary sensors.""" + from __future__ import annotations -from collections.abc import Callable from dataclasses import dataclass from renault_api.kamereon.enums import ChargeState, PlugState @@ -22,23 +22,15 @@ from .entity import RenaultDataEntity, RenaultDataEntityDescription from .renault_hub import RenaultHub -@dataclass(frozen=True) -class RenaultBinarySensorRequiredKeysMixin: - """Mixin for required keys.""" - - on_key: str - on_value: StateType - - -@dataclass(frozen=True) +@dataclass(frozen=True, kw_only=True) class RenaultBinarySensorEntityDescription( BinarySensorEntityDescription, RenaultDataEntityDescription, - RenaultBinarySensorRequiredKeysMixin, ): """Class describing Renault binary sensor entities.""" - icon_fn: Callable[[RenaultBinarySensor], str] | None = None + on_key: str + on_value: StateType async def async_setup_entry( @@ -71,13 +63,6 @@ class RenaultBinarySensor( return None return data == self.entity_description.on_value - @property - def icon(self) -> str | None: - """Icon handling.""" - if self.entity_description.icon_fn: - return self.entity_description.icon_fn(self) - return None - BINARY_SENSOR_TYPES: tuple[RenaultBinarySensorEntityDescription, ...] = tuple( [ @@ -98,7 +83,6 @@ BINARY_SENSOR_TYPES: tuple[RenaultBinarySensorEntityDescription, ...] = tuple( RenaultBinarySensorEntityDescription( key="hvac_status", coordinator="hvac_status", - icon_fn=lambda e: "mdi:fan" if e.is_on else "mdi:fan-off", on_key="hvacStatus", on_value="on", translation_key="hvac_status", diff --git a/homeassistant/components/renault/button.py b/homeassistant/components/renault/button.py index 87883204890..9a6e1d76df6 100644 --- a/homeassistant/components/renault/button.py +++ b/homeassistant/components/renault/button.py @@ -1,4 +1,5 @@ """Support for Renault button entities.""" + from __future__ import annotations from collections.abc import Callable, Coroutine @@ -15,19 +16,11 @@ from .entity import RenaultEntity from .renault_hub import RenaultHub -@dataclass(frozen=True) -class RenaultButtonRequiredKeysMixin: - """Mixin for required keys.""" - - async_press: Callable[[RenaultButtonEntity], Coroutine[Any, Any, Any]] - - -@dataclass(frozen=True) -class RenaultButtonEntityDescription( - ButtonEntityDescription, RenaultButtonRequiredKeysMixin -): +@dataclass(frozen=True, kw_only=True) +class RenaultButtonEntityDescription(ButtonEntityDescription): """Class describing Renault button entities.""" + async_press: Callable[[RenaultButtonEntity], Coroutine[Any, Any, Any]] requires_electricity: bool = False @@ -61,20 +54,17 @@ BUTTON_TYPES: tuple[RenaultButtonEntityDescription, ...] = ( RenaultButtonEntityDescription( async_press=lambda x: x.vehicle.set_ac_start(21, None), key="start_air_conditioner", - icon="mdi:air-conditioner", translation_key="start_air_conditioner", ), RenaultButtonEntityDescription( async_press=lambda x: x.vehicle.set_charge_start(), key="start_charge", - icon="mdi:ev-station", requires_electricity=True, translation_key="start_charge", ), RenaultButtonEntityDescription( async_press=lambda x: x.vehicle.set_charge_stop(), key="stop_charge", - icon="mdi:ev-station", requires_electricity=True, translation_key="stop_charge", ), diff --git a/homeassistant/components/renault/config_flow.py b/homeassistant/components/renault/config_flow.py index 8f5b99972d1..82429dd146c 100644 --- a/homeassistant/components/renault/config_flow.py +++ b/homeassistant/components/renault/config_flow.py @@ -1,4 +1,5 @@ """Config flow to configure Renault component.""" + from __future__ import annotations from collections.abc import Mapping @@ -7,15 +8,14 @@ from typing import TYPE_CHECKING, Any from renault_api.const import AVAILABLE_LOCALES import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from homeassistant.data_entry_flow import FlowResult from .const import CONF_KAMEREON_ACCOUNT_ID, CONF_LOCALE, DOMAIN from .renault_hub import RenaultHub -class RenaultFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class RenaultFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a Renault config flow.""" VERSION = 1 @@ -28,7 +28,7 @@ class RenaultFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a Renault config flow start. Ask the user for API keys. @@ -45,7 +45,7 @@ class RenaultFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_kamereon() return self._show_user_form() - def _show_user_form(self, errors: dict[str, Any] | None = None) -> FlowResult: + def _show_user_form(self, errors: dict[str, Any] | None = None) -> ConfigFlowResult: """Show the API keys form.""" return self.async_show_form( step_id="user", @@ -61,7 +61,7 @@ class RenaultFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_kamereon( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Select Kamereon account.""" if user_input: await self.async_set_unique_id(user_input[CONF_KAMEREON_ACCOUNT_ID]) @@ -93,14 +93,16 @@ class RenaultFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ), ) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Perform reauth upon an API authentication error.""" self._original_data = entry_data return await self.async_step_reauth_confirm() async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Dialog that informs the user that reauth is required.""" if not user_input: return self._show_reauth_confirm_form() @@ -128,7 +130,7 @@ class RenaultFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): def _show_reauth_confirm_form( self, errors: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Show the API keys form.""" if TYPE_CHECKING: assert self._original_data diff --git a/homeassistant/components/renault/const.py b/homeassistant/components/renault/const.py index 8e68eac01eb..201a07c6783 100644 --- a/homeassistant/components/renault/const.py +++ b/homeassistant/components/renault/const.py @@ -1,4 +1,5 @@ """Constants for the Renault component.""" + from homeassistant.const import Platform DOMAIN = "renault" diff --git a/homeassistant/components/renault/coordinator.py b/homeassistant/components/renault/coordinator.py index f8e6a21823a..f77a38f2505 100644 --- a/homeassistant/components/renault/coordinator.py +++ b/homeassistant/components/renault/coordinator.py @@ -1,4 +1,5 @@ """Proxy to handle account communication with Renault servers.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/renault/device_tracker.py b/homeassistant/components/renault/device_tracker.py index a27c59cecfb..922173461a0 100644 --- a/homeassistant/components/renault/device_tracker.py +++ b/homeassistant/components/renault/device_tracker.py @@ -1,4 +1,5 @@ """Support for Renault device trackers.""" + from __future__ import annotations from renault_api.kamereon.models import KamereonVehicleLocationData @@ -54,7 +55,6 @@ DEVICE_TRACKER_TYPES: tuple[RenaultDataEntityDescription, ...] = ( RenaultDataEntityDescription( key="location", coordinator="location", - icon="mdi:car", translation_key="location", ), ) diff --git a/homeassistant/components/renault/diagnostics.py b/homeassistant/components/renault/diagnostics.py index 2029ef989d6..1234def019e 100644 --- a/homeassistant/components/renault/diagnostics.py +++ b/homeassistant/components/renault/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for Renault.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/renault/entity.py b/homeassistant/components/renault/entity.py index fd7f0eb3654..10de028b2d0 100644 --- a/homeassistant/components/renault/entity.py +++ b/homeassistant/components/renault/entity.py @@ -1,4 +1,5 @@ """Base classes for Renault entities.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/homeassistant/components/renault/icons.json b/homeassistant/components/renault/icons.json new file mode 100644 index 00000000000..75356fda411 --- /dev/null +++ b/homeassistant/components/renault/icons.json @@ -0,0 +1,71 @@ +{ + "entity": { + "binary_sensor": { + "hvac_status": { + "default": "mdi:fan-off", + "state": { + "on": "mdi:fan" + } + } + }, + "button": { + "start_air_conditioner": { + "default": "mdi:air-conditioner" + }, + "start_charge": { + "default": "mdi:ev-station" + }, + "stop_charge": { + "default": "mdi:ev-station" + } + }, + "device_tracker": { + "location": { + "default": "mdi:car" + } + }, + "select": { + "charge_mode": { + "default": "mdi:calendar-remove", + "state": { + "schedule_mode": "mdi:calendar-clock", + "scheduled": "mdi:calendar-clock" + } + } + }, + "sensor": { + "charge_state": { + "default": "mdi:mdi:flash-off", + "state": { + "charge_in_progress": "mdi:flash" + } + }, + "charging_remaining_time": { + "default": "mdi:timer" + }, + "plug_state": { + "default": "mdi:power-plug-off", + "state": { + "plugged": "mdi:power-plug" + } + }, + "battery_autonomy": { + "default": "mdi:ev-station" + }, + "mileage": { + "default": "mdi:sign-direction" + }, + "fuel_autonomy": { + "default": "mdi:gas-station" + }, + "fuel_quantity": { + "default": "mdi:fuel" + } + } + }, + "services": { + "ac_start": "mdi:hvac", + "ac_cancel": "mdi:hvac-off", + "charge_set_schedules": "mdi:calendar-clock" + } +} diff --git a/homeassistant/components/renault/renault_hub.py b/homeassistant/components/renault/renault_hub.py index 49819dd919f..97a9d080b86 100644 --- a/homeassistant/components/renault/renault_hub.py +++ b/homeassistant/components/renault/renault_hub.py @@ -1,4 +1,5 @@ """Proxy to handle account communication with Renault servers.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/renault/renault_vehicle.py b/homeassistant/components/renault/renault_vehicle.py index e44a50d57a1..59e1826ce1b 100644 --- a/homeassistant/components/renault/renault_vehicle.py +++ b/homeassistant/components/renault/renault_vehicle.py @@ -1,4 +1,5 @@ """Proxy to handle account communication with Renault servers.""" + from __future__ import annotations import asyncio @@ -124,16 +125,16 @@ class RenaultVehicleProxy: coordinator = self.coordinators[key] if coordinator.not_supported: # Remove endpoint as it is not supported for this vehicle. - LOGGER.info( - "Ignoring endpoint %s as it is not supported for this vehicle: %s", + LOGGER.warning( + "Ignoring endpoint %s as it is not supported: %s", coordinator.name, coordinator.last_exception, ) del self.coordinators[key] elif coordinator.access_denied: # Remove endpoint as it is denied for this vehicle. - LOGGER.info( - "Ignoring endpoint %s as it is denied for this vehicle: %s", + LOGGER.warning( + "Ignoring endpoint %s as it is denied: %s", coordinator.name, coordinator.last_exception, ) diff --git a/homeassistant/components/renault/select.py b/homeassistant/components/renault/select.py index 9dcc52abc87..f6c8f73d24b 100644 --- a/homeassistant/components/renault/select.py +++ b/homeassistant/components/renault/select.py @@ -1,7 +1,7 @@ """Support for Renault sensors.""" + from __future__ import annotations -from collections.abc import Callable from dataclasses import dataclass from typing import cast @@ -18,22 +18,14 @@ from .entity import RenaultDataEntity, RenaultDataEntityDescription from .renault_hub import RenaultHub -@dataclass(frozen=True) -class RenaultSelectRequiredKeysMixin: - """Mixin for required keys.""" - - data_key: str - icon_lambda: Callable[[RenaultSelectEntity], str] - - -@dataclass(frozen=True) +@dataclass(frozen=True, kw_only=True) class RenaultSelectEntityDescription( - SelectEntityDescription, - RenaultDataEntityDescription, - RenaultSelectRequiredKeysMixin, + SelectEntityDescription, RenaultDataEntityDescription ): """Class describing Renault select entities.""" + data_key: str + async def async_setup_entry( hass: HomeAssistant, @@ -68,30 +60,17 @@ class RenaultSelectEntity( """Return the state of this entity.""" return self._get_data_attr(self.entity_description.data_key) - @property - def icon(self) -> str | None: - """Icon handling.""" - return self.entity_description.icon_lambda(self) - async def async_select_option(self, option: str) -> None: """Change the selected option.""" await self.vehicle.set_charge_mode(option) -def _get_charge_mode_icon(entity: RenaultSelectEntity) -> str: - """Return the icon of this entity.""" - if entity.data == "schedule_mode": - return "mdi:calendar-clock" - return "mdi:calendar-remove" - - SENSOR_TYPES: tuple[RenaultSelectEntityDescription, ...] = ( RenaultSelectEntityDescription( key="charge_mode", coordinator="charge_mode", data_key="chargeMode", translation_key="charge_mode", - icon_lambda=_get_charge_mode_icon, options=["always", "always_charging", "schedule_mode"], ), ) diff --git a/homeassistant/components/renault/sensor.py b/homeassistant/components/renault/sensor.py index d30b8d01fb3..352fddb8d8b 100644 --- a/homeassistant/components/renault/sensor.py +++ b/homeassistant/components/renault/sensor.py @@ -1,4 +1,5 @@ """Support for Renault sensors.""" + from __future__ import annotations from collections.abc import Callable @@ -6,7 +7,6 @@ from dataclasses import dataclass from datetime import datetime from typing import TYPE_CHECKING, Any, Generic, cast -from renault_api.kamereon.enums import ChargeState, PlugState from renault_api.kamereon.models import ( KamereonVehicleBatteryStatusData, KamereonVehicleCockpitData, @@ -43,23 +43,14 @@ from .renault_hub import RenaultHub from .renault_vehicle import RenaultVehicleProxy -@dataclass(frozen=True) -class RenaultSensorRequiredKeysMixin(Generic[T]): - """Mixin for required keys.""" - - data_key: str - entity_class: type[RenaultSensor[T]] - - -@dataclass(frozen=True) +@dataclass(frozen=True, kw_only=True) class RenaultSensorEntityDescription( - SensorEntityDescription, - RenaultDataEntityDescription, - RenaultSensorRequiredKeysMixin[T], + SensorEntityDescription, RenaultDataEntityDescription, Generic[T] ): """Class describing Renault sensor entities.""" - icon_lambda: Callable[[RenaultSensor[T]], str] | None = None + data_key: str + entity_class: type[RenaultSensor[T]] condition_lambda: Callable[[RenaultVehicleProxy], bool] | None = None requires_fuel: bool = False value_lambda: Callable[[RenaultSensor[T]], StateType | datetime] | None = None @@ -93,13 +84,6 @@ class RenaultSensor(RenaultDataEntity[T], SensorEntity): """Return the state of this entity.""" return self._get_data_attr(self.entity_description.data_key) - @property - def icon(self) -> str | None: - """Icon handling.""" - if self.entity_description.icon_lambda is None: - return super().icon - return self.entity_description.icon_lambda(self) - @property def native_value(self) -> StateType | datetime: """Return the state of this entity.""" @@ -122,13 +106,6 @@ def _get_charge_state_formatted(entity: RenaultSensor[T]) -> str | None: return charging_status.name.lower() if charging_status else None -def _get_charge_state_icon(entity: RenaultSensor[T]) -> str: - """Return the icon of this entity.""" - if entity.data == ChargeState.CHARGE_IN_PROGRESS.value: - return "mdi:flash" - return "mdi:flash-off" - - def _get_plug_state_formatted(entity: RenaultSensor[T]) -> str | None: """Return the plug_status of this entity.""" data = cast(KamereonVehicleBatteryStatusData, entity.coordinator.data) @@ -136,15 +113,8 @@ def _get_plug_state_formatted(entity: RenaultSensor[T]) -> str | None: return plug_status.name.lower() if plug_status else None -def _get_plug_state_icon(entity: RenaultSensor[T]) -> str: - """Return the icon of this entity.""" - if entity.data == PlugState.PLUGGED.value: - return "mdi:power-plug" - return "mdi:power-plug-off" - - def _get_rounded_value(entity: RenaultSensor[T]) -> float: - """Return the icon of this entity.""" + """Return the rounded value of this entity.""" return round(cast(float, entity.data)) @@ -173,7 +143,6 @@ SENSOR_TYPES: tuple[RenaultSensorEntityDescription[Any], ...] = ( translation_key="charge_state", device_class=SensorDeviceClass.ENUM, entity_class=RenaultSensor[KamereonVehicleBatteryStatusData], - icon_lambda=_get_charge_state_icon, options=[ "not_in_charge", "waiting_for_a_planned_charge", @@ -192,7 +161,6 @@ SENSOR_TYPES: tuple[RenaultSensorEntityDescription[Any], ...] = ( data_key="chargingRemainingTime", device_class=SensorDeviceClass.DURATION, entity_class=RenaultSensor[KamereonVehicleBatteryStatusData], - icon="mdi:timer", native_unit_of_measurement=UnitOfTime.MINUTES, state_class=SensorStateClass.MEASUREMENT, translation_key="charging_remaining_time", @@ -232,7 +200,6 @@ SENSOR_TYPES: tuple[RenaultSensorEntityDescription[Any], ...] = ( translation_key="plug_state", device_class=SensorDeviceClass.ENUM, entity_class=RenaultSensor[KamereonVehicleBatteryStatusData], - icon_lambda=_get_plug_state_icon, options=["unplugged", "plugged", "plug_error", "plug_unknown"], value_lambda=_get_plug_state_formatted, ), @@ -242,7 +209,6 @@ SENSOR_TYPES: tuple[RenaultSensorEntityDescription[Any], ...] = ( data_key="batteryAutonomy", device_class=SensorDeviceClass.DISTANCE, entity_class=RenaultSensor[KamereonVehicleBatteryStatusData], - icon="mdi:ev-station", native_unit_of_measurement=UnitOfLength.KILOMETERS, state_class=SensorStateClass.MEASUREMENT, translation_key="battery_autonomy", @@ -283,7 +249,6 @@ SENSOR_TYPES: tuple[RenaultSensorEntityDescription[Any], ...] = ( data_key="totalMileage", device_class=SensorDeviceClass.DISTANCE, entity_class=RenaultSensor[KamereonVehicleCockpitData], - icon="mdi:sign-direction", native_unit_of_measurement=UnitOfLength.KILOMETERS, state_class=SensorStateClass.TOTAL_INCREASING, value_lambda=_get_rounded_value, @@ -295,7 +260,6 @@ SENSOR_TYPES: tuple[RenaultSensorEntityDescription[Any], ...] = ( data_key="fuelAutonomy", device_class=SensorDeviceClass.DISTANCE, entity_class=RenaultSensor[KamereonVehicleCockpitData], - icon="mdi:gas-station", native_unit_of_measurement=UnitOfLength.KILOMETERS, state_class=SensorStateClass.MEASUREMENT, requires_fuel=True, @@ -308,7 +272,6 @@ SENSOR_TYPES: tuple[RenaultSensorEntityDescription[Any], ...] = ( data_key="fuelQuantity", device_class=SensorDeviceClass.VOLUME, entity_class=RenaultSensor[KamereonVehicleCockpitData], - icon="mdi:fuel", native_unit_of_measurement=UnitOfVolume.LITERS, state_class=SensorStateClass.TOTAL, requires_fuel=True, diff --git a/homeassistant/components/renault/services.py b/homeassistant/components/renault/services.py index d2c7d451844..b49088ddb7d 100644 --- a/homeassistant/components/renault/services.py +++ b/homeassistant/components/renault/services.py @@ -1,4 +1,5 @@ """Support for Renault services.""" + from __future__ import annotations from collections.abc import Mapping diff --git a/homeassistant/components/renault/strings.json b/homeassistant/components/renault/strings.json index 0b0c3d87822..322f7a207d7 100644 --- a/homeassistant/components/renault/strings.json +++ b/homeassistant/components/renault/strings.json @@ -75,7 +75,8 @@ "state": { "always": "Instant", "always_charging": "[%key:component::renault::entity::select::charge_mode::state::always%]", - "schedule_mode": "Planner" + "schedule_mode": "Planner", + "scheduled": "Scheduled" } } }, diff --git a/homeassistant/components/renson/__init__.py b/homeassistant/components/renson/__init__.py index c046f93cdc0..d1eebdf0a5f 100644 --- a/homeassistant/components/renson/__init__.py +++ b/homeassistant/components/renson/__init__.py @@ -1,4 +1,5 @@ """The Renson integration.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/homeassistant/components/renson/binary_sensor.py b/homeassistant/components/renson/binary_sensor.py index 012ecee2e98..46f832ed15c 100644 --- a/homeassistant/components/renson/binary_sensor.py +++ b/homeassistant/components/renson/binary_sensor.py @@ -1,4 +1,5 @@ """Binary sensors for renson.""" + from __future__ import annotations from dataclasses import dataclass @@ -30,20 +31,13 @@ from .coordinator import RensonCoordinator from .entity import RensonEntity -@dataclass(frozen=True) -class RensonBinarySensorEntityDescriptionMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class RensonBinarySensorEntityDescription(BinarySensorEntityDescription): + """Description of binary sensor.""" field: FieldEnum -@dataclass(frozen=True) -class RensonBinarySensorEntityDescription( - BinarySensorEntityDescription, RensonBinarySensorEntityDescriptionMixin -): - """Description of binary sensor.""" - - BINARY_SENSORS: tuple[RensonBinarySensorEntityDescription, ...] = ( RensonBinarySensorEntityDescription( translation_key="frost_protection_active", diff --git a/homeassistant/components/renson/button.py b/homeassistant/components/renson/button.py index 5cdf0e4787b..02278a0d6f6 100644 --- a/homeassistant/components/renson/button.py +++ b/homeassistant/components/renson/button.py @@ -1,4 +1,5 @@ """Renson ventilation unit buttons.""" + from __future__ import annotations from collections.abc import Callable @@ -21,20 +22,13 @@ from .const import DOMAIN from .entity import RensonEntity -@dataclass(frozen=True) -class RensonButtonEntityDescriptionMixin: - """Action function called on press.""" +@dataclass(frozen=True, kw_only=True) +class RensonButtonEntityDescription(ButtonEntityDescription): + """Class describing Renson button entity.""" action_fn: Callable[[RensonVentilation], None] -@dataclass(frozen=True) -class RensonButtonEntityDescription( - ButtonEntityDescription, RensonButtonEntityDescriptionMixin -): - """Class describing Renson button entity.""" - - ENTITY_DESCRIPTIONS: tuple[RensonButtonEntityDescription, ...] = ( RensonButtonEntityDescription( key="sync_time", diff --git a/homeassistant/components/renson/config_flow.py b/homeassistant/components/renson/config_flow.py index 9883772ce02..ec380f5a513 100644 --- a/homeassistant/components/renson/config_flow.py +++ b/homeassistant/components/renson/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Renson integration.""" + from __future__ import annotations import logging @@ -7,10 +8,9 @@ from typing import Any from renson_endura_delta import renson import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError from .const import DOMAIN @@ -24,7 +24,7 @@ STEP_USER_DATA_SCHEMA = vol.Schema( ) -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class RensonConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Renson.""" VERSION = 1 @@ -42,7 +42,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" if user_input is None: return self.async_show_form( diff --git a/homeassistant/components/renson/const.py b/homeassistant/components/renson/const.py index 53bbd90c4b7..840e1ce428a 100644 --- a/homeassistant/components/renson/const.py +++ b/homeassistant/components/renson/const.py @@ -1,4 +1,3 @@ """Constants for the Renson integration.""" - DOMAIN = "renson" diff --git a/homeassistant/components/renson/coordinator.py b/homeassistant/components/renson/coordinator.py index 924a3b765f5..8613220eee1 100644 --- a/homeassistant/components/renson/coordinator.py +++ b/homeassistant/components/renson/coordinator.py @@ -1,4 +1,5 @@ """DataUpdateCoordinator for the renson integration.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/renson/entity.py b/homeassistant/components/renson/entity.py index 9bb2c27b112..cee991386ea 100644 --- a/homeassistant/components/renson/entity.py +++ b/homeassistant/components/renson/entity.py @@ -1,4 +1,5 @@ """Entity class for Renson ventilation unit.""" + from __future__ import annotations from renson_endura_delta.field_enum import ( diff --git a/homeassistant/components/renson/fan.py b/homeassistant/components/renson/fan.py index e6bd2717981..226d623af2b 100644 --- a/homeassistant/components/renson/fan.py +++ b/homeassistant/components/renson/fan.py @@ -1,4 +1,5 @@ """Platform to control a Renson ventilation unit.""" + from __future__ import annotations import logging @@ -117,9 +118,9 @@ async def async_setup_entry( class RensonFan(RensonEntity, FanEntity): """Representation of the Renson fan platform.""" - _attr_icon = "mdi:air-conditioner" _attr_has_entity_name = True _attr_name = None + _attr_translation_key = "fan" _attr_supported_features = FanEntityFeature.SET_SPEED def __init__(self, api: RensonVentilation, coordinator: RensonCoordinator) -> None: diff --git a/homeassistant/components/renson/icons.json b/homeassistant/components/renson/icons.json new file mode 100644 index 00000000000..b7b1fdfdd8c --- /dev/null +++ b/homeassistant/components/renson/icons.json @@ -0,0 +1,24 @@ +{ + "entity": { + "fan": { + "fan": { + "default": "mdi:air-conditioner" + } + }, + "number": { + "filter_change": { + "default": "mdi:filter" + } + }, + "switch": { + "breeze": { + "default": "mdi:weather-dust" + } + } + }, + "services": { + "set_timer_level": "mdi:timer", + "set_breeze": "mdi:weather-windy", + "set_pollution_settings": "mdi:air-filter" + } +} diff --git a/homeassistant/components/renson/number.py b/homeassistant/components/renson/number.py index 344fa3ff0bd..fb8ab8fc552 100644 --- a/homeassistant/components/renson/number.py +++ b/homeassistant/components/renson/number.py @@ -1,4 +1,5 @@ """Platform to control a Renson ventilation unit.""" + from __future__ import annotations import logging @@ -26,7 +27,6 @@ _LOGGER = logging.getLogger(__name__) RENSON_NUMBER_DESCRIPTION = NumberEntityDescription( key="filter_change", translation_key="filter_change", - icon="mdi:filter", native_step=1, native_min_value=0, native_max_value=360, diff --git a/homeassistant/components/renson/sensor.py b/homeassistant/components/renson/sensor.py index 367b4a47a63..1df62e12312 100644 --- a/homeassistant/components/renson/sensor.py +++ b/homeassistant/components/renson/sensor.py @@ -1,4 +1,5 @@ """Sensor data of the Renson ventilation unit.""" + from __future__ import annotations from dataclasses import dataclass @@ -50,21 +51,14 @@ from .coordinator import RensonCoordinator from .entity import RensonEntity -@dataclass(frozen=True) -class RensonSensorEntityDescriptionMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class RensonSensorEntityDescription(SensorEntityDescription): + """Description of a Renson sensor.""" field: FieldEnum raw_format: bool -@dataclass(frozen=True) -class RensonSensorEntityDescription( - SensorEntityDescription, RensonSensorEntityDescriptionMixin -): - """Description of a Renson sensor.""" - - SENSORS: tuple[RensonSensorEntityDescription, ...] = ( RensonSensorEntityDescription( key="CO2_QUALITY_FIELD", diff --git a/homeassistant/components/renson/switch.py b/homeassistant/components/renson/switch.py index a724dcc5530..2cd44d20a6a 100644 --- a/homeassistant/components/renson/switch.py +++ b/homeassistant/components/renson/switch.py @@ -1,4 +1,5 @@ """Breeze switch of the Renson ventilation unit.""" + from __future__ import annotations import logging @@ -22,7 +23,6 @@ _LOGGER = logging.getLogger(__name__) class RensonBreezeSwitch(RensonEntity, SwitchEntity): """Provide the breeze switch.""" - _attr_icon = "mdi:weather-dust" _attr_device_class = SwitchDeviceClass.SWITCH _attr_has_entity_name = True _attr_translation_key = "breeze" diff --git a/homeassistant/components/renson/time.py b/homeassistant/components/renson/time.py index 57d6869a72c..feb47fadf99 100644 --- a/homeassistant/components/renson/time.py +++ b/homeassistant/components/renson/time.py @@ -1,4 +1,5 @@ """Renson ventilation unit time.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/reolink/binary_sensor.py b/homeassistant/components/reolink/binary_sensor.py index 03b30d8195e..fe80177da12 100644 --- a/homeassistant/components/reolink/binary_sensor.py +++ b/homeassistant/components/reolink/binary_sensor.py @@ -1,4 +1,5 @@ """Component providing support for Reolink binary sensors.""" + from __future__ import annotations from collections.abc import Callable @@ -7,6 +8,7 @@ from dataclasses import dataclass from reolink_aio.api import ( DUAL_LENS_DUAL_MOTION_MODELS, FACE_DETECTION_TYPE, + PACKAGE_DETECTION_TYPE, PERSON_DETECTION_TYPE, PET_DETECTION_TYPE, VEHICLE_DETECTION_TYPE, @@ -35,8 +37,6 @@ class ReolinkBinarySensorEntityDescription( ): """A class that describes binary sensor entities.""" - icon_off: str = "mdi:motion-sensor-off" - icon: str = "mdi:motion-sensor" value: Callable[[Host, int], bool] @@ -49,7 +49,6 @@ BINARY_SENSORS = ( ReolinkBinarySensorEntityDescription( key=FACE_DETECTION_TYPE, translation_key="face", - icon="mdi:face-recognition", value=lambda api, ch: api.ai_detected(ch, FACE_DETECTION_TYPE), supported=lambda api, ch: api.ai_supported(ch, FACE_DETECTION_TYPE), ), @@ -62,16 +61,12 @@ BINARY_SENSORS = ( ReolinkBinarySensorEntityDescription( key=VEHICLE_DETECTION_TYPE, translation_key="vehicle", - icon="mdi:car", - icon_off="mdi:car-off", value=lambda api, ch: api.ai_detected(ch, VEHICLE_DETECTION_TYPE), supported=lambda api, ch: api.ai_supported(ch, VEHICLE_DETECTION_TYPE), ), ReolinkBinarySensorEntityDescription( key=PET_DETECTION_TYPE, translation_key="pet", - icon="mdi:dog-side", - icon_off="mdi:dog-side-off", value=lambda api, ch: api.ai_detected(ch, PET_DETECTION_TYPE), supported=lambda api, ch: ( api.ai_supported(ch, PET_DETECTION_TYPE) @@ -81,16 +76,18 @@ BINARY_SENSORS = ( ReolinkBinarySensorEntityDescription( key=PET_DETECTION_TYPE, translation_key="animal", - icon="mdi:paw", - icon_off="mdi:paw-off", value=lambda api, ch: api.ai_detected(ch, PET_DETECTION_TYPE), supported=lambda api, ch: api.supported(ch, "ai_animal"), ), + ReolinkBinarySensorEntityDescription( + key=PACKAGE_DETECTION_TYPE, + translation_key="package", + value=lambda api, ch: api.ai_detected(ch, PACKAGE_DETECTION_TYPE), + supported=lambda api, ch: api.ai_supported(ch, PACKAGE_DETECTION_TYPE), + ), ReolinkBinarySensorEntityDescription( key="visitor", translation_key="visitor", - icon="mdi:bell-ring-outline", - icon_off="mdi:doorbell", value=lambda api, ch: api.visitor_detected(ch), supported=lambda api, ch: api.is_doorbell(ch), ), @@ -140,13 +137,6 @@ class ReolinkBinarySensorEntity(ReolinkChannelCoordinatorEntity, BinarySensorEnt key = entity_description.key self._attr_translation_key = f"{key}_lens_{self._channel}" - @property - def icon(self) -> str | None: - """Icon of the sensor.""" - if self.is_on is False: - return self.entity_description.icon_off - return super().icon - @property def is_on(self) -> bool: """State of the sensor.""" diff --git a/homeassistant/components/reolink/button.py b/homeassistant/components/reolink/button.py index 5656f178db6..528807920d3 100644 --- a/homeassistant/components/reolink/button.py +++ b/homeassistant/components/reolink/button.py @@ -1,4 +1,5 @@ """Component providing support for Reolink button entities.""" + from __future__ import annotations from collections.abc import Callable @@ -64,7 +65,6 @@ BUTTON_ENTITIES = ( ReolinkButtonEntityDescription( key="ptz_stop", translation_key="ptz_stop", - icon="mdi:pan", enabled_default=lambda api, ch: api.supported(ch, "pan_tilt"), supported=lambda api, ch: ( api.supported(ch, "pan_tilt") or api.supported(ch, "zoom_basic") @@ -74,7 +74,6 @@ BUTTON_ENTITIES = ( ReolinkButtonEntityDescription( key="ptz_left", translation_key="ptz_left", - icon="mdi:pan", supported=lambda api, ch: api.supported(ch, "pan"), method=lambda api, ch: api.set_ptz_command(ch, command=PtzEnum.left.value), ptz_cmd=PtzEnum.left.value, @@ -82,7 +81,6 @@ BUTTON_ENTITIES = ( ReolinkButtonEntityDescription( key="ptz_right", translation_key="ptz_right", - icon="mdi:pan", supported=lambda api, ch: api.supported(ch, "pan"), method=lambda api, ch: api.set_ptz_command(ch, command=PtzEnum.right.value), ptz_cmd=PtzEnum.right.value, @@ -90,7 +88,6 @@ BUTTON_ENTITIES = ( ReolinkButtonEntityDescription( key="ptz_up", translation_key="ptz_up", - icon="mdi:pan", supported=lambda api, ch: api.supported(ch, "tilt"), method=lambda api, ch: api.set_ptz_command(ch, command=PtzEnum.up.value), ptz_cmd=PtzEnum.up.value, @@ -98,7 +95,6 @@ BUTTON_ENTITIES = ( ReolinkButtonEntityDescription( key="ptz_down", translation_key="ptz_down", - icon="mdi:pan", supported=lambda api, ch: api.supported(ch, "tilt"), method=lambda api, ch: api.set_ptz_command(ch, command=PtzEnum.down.value), ptz_cmd=PtzEnum.down.value, @@ -106,7 +102,6 @@ BUTTON_ENTITIES = ( ReolinkButtonEntityDescription( key="ptz_zoom_in", translation_key="ptz_zoom_in", - icon="mdi:magnify", entity_registry_enabled_default=False, supported=lambda api, ch: api.supported(ch, "zoom_basic"), method=lambda api, ch: api.set_ptz_command(ch, command=PtzEnum.zoomin.value), @@ -115,7 +110,6 @@ BUTTON_ENTITIES = ( ReolinkButtonEntityDescription( key="ptz_zoom_out", translation_key="ptz_zoom_out", - icon="mdi:magnify", entity_registry_enabled_default=False, supported=lambda api, ch: api.supported(ch, "zoom_basic"), method=lambda api, ch: api.set_ptz_command(ch, command=PtzEnum.zoomout.value), @@ -124,7 +118,6 @@ BUTTON_ENTITIES = ( ReolinkButtonEntityDescription( key="ptz_calibrate", translation_key="ptz_calibrate", - icon="mdi:pan", entity_category=EntityCategory.CONFIG, supported=lambda api, ch: api.supported(ch, "ptz_callibrate"), method=lambda api, ch: api.ptz_callibrate(ch), @@ -132,14 +125,12 @@ BUTTON_ENTITIES = ( ReolinkButtonEntityDescription( key="guard_go_to", translation_key="guard_go_to", - icon="mdi:crosshairs-gps", supported=lambda api, ch: api.supported(ch, "ptz_guard"), method=lambda api, ch: api.set_ptz_guard(ch, command=GuardEnum.goto.value), ), ReolinkButtonEntityDescription( key="guard_set", translation_key="guard_set", - icon="mdi:crosshairs-gps", entity_category=EntityCategory.CONFIG, supported=lambda api, ch: api.supported(ch, "ptz_guard"), method=lambda api, ch: api.set_ptz_guard(ch, command=GuardEnum.set.value), diff --git a/homeassistant/components/reolink/camera.py b/homeassistant/components/reolink/camera.py index 715588a8225..a2c396e7ef5 100644 --- a/homeassistant/components/reolink/camera.py +++ b/homeassistant/components/reolink/camera.py @@ -1,4 +1,5 @@ """Component providing support for Reolink IP cameras.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/homeassistant/components/reolink/config_flow.py b/homeassistant/components/reolink/config_flow.py index fc9b717f89b..b62a7b7f709 100644 --- a/homeassistant/components/reolink/config_flow.py +++ b/homeassistant/components/reolink/config_flow.py @@ -1,4 +1,5 @@ """Config flow for the Reolink camera component.""" + from __future__ import annotations from collections.abc import Mapping @@ -8,8 +9,13 @@ from typing import Any from reolink_aio.exceptions import ApiError, CredentialsInvalidError, ReolinkError import voluptuous as vol -from homeassistant import config_entries from homeassistant.components import dhcp +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, @@ -18,7 +24,7 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import callback -from homeassistant.data_entry_flow import AbortFlow, FlowResult +from homeassistant.data_entry_flow import AbortFlow from homeassistant.helpers import config_validation as cv from homeassistant.helpers.device_registry import format_mac @@ -33,7 +39,7 @@ DEFAULT_PROTOCOL = "rtsp" DEFAULT_OPTIONS = {CONF_PROTOCOL: DEFAULT_PROTOCOL} -class ReolinkOptionsFlowHandler(config_entries.OptionsFlow): +class ReolinkOptionsFlowHandler(OptionsFlow): """Handle Reolink options.""" def __init__(self, config_entry): @@ -42,7 +48,7 @@ class ReolinkOptionsFlowHandler(config_entries.OptionsFlow): async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Manage the Reolink options.""" if user_input is not None: return self.async_create_entry(data=user_input) @@ -60,7 +66,7 @@ class ReolinkOptionsFlowHandler(config_entries.OptionsFlow): ) -class ReolinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class ReolinkFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a config flow for Reolink device.""" VERSION = 1 @@ -75,12 +81,14 @@ class ReolinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback def async_get_options_flow( - config_entry: config_entries.ConfigEntry, + config_entry: ConfigEntry, ) -> ReolinkOptionsFlowHandler: """Options callback for Reolink.""" return ReolinkOptionsFlowHandler(config_entry) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Perform reauth upon an authentication error or no admin privileges.""" self._host = entry_data[CONF_HOST] self._username = entry_data[CONF_USERNAME] @@ -94,13 +102,15 @@ class ReolinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Dialog that informs the user that reauth is required.""" if user_input is not None: return await self.async_step_user() return self.async_show_form(step_id="reauth_confirm") - async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: + async def async_step_dhcp( + self, discovery_info: dhcp.DhcpServiceInfo + ) -> ConfigFlowResult: """Handle discovery via dhcp.""" mac_address = format_mac(discovery_info.macaddress) existing_entry = await self.async_set_unique_id(mac_address) @@ -157,7 +167,7 @@ class ReolinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors = {} placeholders = { @@ -183,9 +193,9 @@ class ReolinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors[CONF_HOST] = "api_error" except ReolinkWebhookException as err: placeholders["error"] = str(err) - placeholders[ - "more_info" - ] = "https://www.home-assistant.io/more-info/no-url-available/#configuring-the-instance-url" + placeholders["more_info"] = ( + "https://www.home-assistant.io/more-info/no-url-available/#configuring-the-instance-url" + ) errors["base"] = "webhook_exception" except (ReolinkError, ReolinkException) as err: placeholders["error"] = str(err) diff --git a/homeassistant/components/reolink/diagnostics.py b/homeassistant/components/reolink/diagnostics.py index 04b476296f8..5c13bccf58d 100644 --- a/homeassistant/components/reolink/diagnostics.py +++ b/homeassistant/components/reolink/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for Reolink.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/reolink/entity.py b/homeassistant/components/reolink/entity.py index 042e6b45717..e02fd931f66 100644 --- a/homeassistant/components/reolink/entity.py +++ b/homeassistant/components/reolink/entity.py @@ -1,4 +1,5 @@ """Reolink parent entity class.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/reolink/exceptions.py b/homeassistant/components/reolink/exceptions.py index f3e9e0158cd..d166b438f31 100644 --- a/homeassistant/components/reolink/exceptions.py +++ b/homeassistant/components/reolink/exceptions.py @@ -1,4 +1,5 @@ """Exceptions for the Reolink Camera integration.""" + from homeassistant.exceptions import HomeAssistantError diff --git a/homeassistant/components/reolink/host.py b/homeassistant/components/reolink/host.py index 77aeffd5412..44750cdeb3c 100644 --- a/homeassistant/components/reolink/host.py +++ b/homeassistant/components/reolink/host.py @@ -1,4 +1,5 @@ """Module which encapsulates the NVR/camera API and subscription.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/reolink/icons.json b/homeassistant/components/reolink/icons.json new file mode 100644 index 00000000000..fcf88fb6726 --- /dev/null +++ b/homeassistant/components/reolink/icons.json @@ -0,0 +1,266 @@ +{ + "entity": { + "binary_sensor": { + "face": { + "default": "mdi:motion-sensor-off", + "state": { + "on": "mdi:face-recognition" + } + }, + "vehicle": { + "default": "mdi:car-off", + "state": { + "on": "mdi:car" + } + }, + "pet": { + "default": "mdi:dog-side-off", + "state": { + "on": "mdi:dog-side" + } + }, + "animal": { + "default": "mdi:paw-off", + "state": { + "on": "mdi:paw" + } + }, + "package": { + "default": "mdi:gift-off-outline", + "state": { + "on": "mdi:gift-outline" + } + }, + "visitor": { + "default": "mdi:doorbell", + "state": { + "on": "mdi:bell-ring-outline" + } + }, + "person": { + "default": "mdi:motion-sensor-off", + "state": { + "on": "mdi:motion-sensor" + } + } + }, + "button": { + "ptz_stop": { + "default": "mdi:pan" + }, + "ptz_left": { + "default": "mdi:pan" + }, + "ptz_right": { + "default": "mdi:pan" + }, + "ptz_up": { + "default": "mdi:pan" + }, + "ptz_down": { + "default": "mdi:pan" + }, + "ptz_zoom_in": { + "default": "mdi:magnify" + }, + "ptz_zoom_out": { + "default": "mdi:magnify" + }, + "ptz_calibrate": { + "default": "mdi:pan" + }, + "guard_go_to": { + "default": "mdi:crosshairs-gps" + }, + "guard_set": { + "default": "mdi:crosshairs-gps" + } + }, + "light": { + "floodlight": { + "default": "mdi:spotlight-beam" + }, + "status_led": { + "default": "mdi:lightning-bolt-circle" + } + }, + "number": { + "zoom": { + "default": "mdi:magnify" + }, + "focus": { + "default": "mdi:focus-field" + }, + "floodlight_brightness": { + "default": "mdi:spotlight-beam" + }, + "volume": { + "default": "mdi:volume-high" + }, + "guard_return_time": { + "default": "mdi:crosshairs-gps" + }, + "motion_sensitivity": { + "default": "mdi:motion-sensor" + }, + "ai_face_sensitivity": { + "default": "mdi:face-recognition" + }, + "ai_person_sensitivity": { + "default": "mdi:account" + }, + "ai_vehicle_sensitivity": { + "default": "mdi:car" + }, + "ai_package_sensitivity": { + "default": "mdi:gift-outline" + }, + "ai_pet_sensitivity": { + "default": "mdi:dog-side" + }, + "ai_animal_sensitivity": { + "default": "mdi:paw" + }, + "ai_face_delay": { + "default": "mdi:face-recognition" + }, + "ai_person_delay": { + "default": "mdi:account" + }, + "ai_vehicle_delay": { + "default": "mdi:car" + }, + "ai_package_delay": { + "default": "mdi:gift-outline" + }, + "ai_pet_delay": { + "default": "mdi:dog-side" + }, + "ai_animal_delay": { + "default": "mdi:paw" + }, + "auto_quick_reply_time": { + "default": "mdi:message-reply-text-outline" + }, + "auto_track_limit_left": { + "default": "mdi:angle-acute" + }, + "auto_track_limit_right": { + "default": "mdi:angle-acute" + }, + "auto_track_disappear_time": { + "default": "mdi:target-account" + }, + "auto_track_stop_time": { + "default": "mdi:target-account" + }, + "day_night_switch_threshold": { + "default": "mdi:theme-light-dark" + }, + "image_brightness": { + "default": "mdi:image-edit" + }, + "image_contrast": { + "default": "mdi:image-edit" + }, + "image_saturation": { + "default": "mdi:image-edit" + }, + "image_sharpness": { + "default": "mdi:image-edit" + }, + "image_hue": { + "default": "mdi:image-edit" + } + }, + "select": { + "floodlight_mode": { + "default": "mdi:spotlight-beam" + }, + "day_night_mode": { + "default": "mdi:theme-light-dark" + }, + "ptz_preset": { + "default": "mdi:pan" + }, + "play_quick_reply_message": { + "default": "mdi:message-reply-text-outline" + }, + "auto_quick_reply_message": { + "default": "mdi:message-reply-text-outline" + }, + "auto_track_method": { + "default": "mdi:target-account" + }, + "status_led": { + "default": "mdi:lightning-bolt-circle" + } + }, + "sensor": { + "ptz_pan_position": { + "default": "mdi:pan" + }, + "wifi_signal": { + "default": "mdi:wifi" + }, + "hdd_storage": { + "default": "mdi:harddisk" + }, + "sd_storage": { + "default": "mdi:micro-sd" + } + }, + "siren": { + "siren": { + "default": "mdi:alarm-light" + } + }, + "switch": { + "ir_lights": { + "default": "mdi:led-off" + }, + "record_audio": { + "default": "mdi:microphone" + }, + "siren_on_event": { + "default": "mdi:alarm-light" + }, + "auto_tracking": { + "default": "mdi:target-account" + }, + "auto_focus": { + "default": "mdi:focus-field" + }, + "guard_return": { + "default": "mdi:crosshairs-gps" + }, + "ptz_patrol": { + "default": "mdi:map-marker-path" + }, + "email": { + "default": "mdi:email" + }, + "ftp_upload": { + "default": "mdi:swap-horizontal" + }, + "push_notifications": { + "default": "mdi:message-badge" + }, + "record": { + "default": "mdi:record-rec" + }, + "buzzer": { + "default": "mdi:room-service" + }, + "doorbell_button_sound": { + "default": "mdi:volume-high" + }, + "hdr": { + "default": "mdi:hdr" + } + } + }, + "services": { + "ptz_move": "mdi:pan" + } +} diff --git a/homeassistant/components/reolink/light.py b/homeassistant/components/reolink/light.py index 222ab984e3f..877bf80080b 100644 --- a/homeassistant/components/reolink/light.py +++ b/homeassistant/components/reolink/light.py @@ -1,4 +1,5 @@ """Component providing support for Reolink light entities.""" + from __future__ import annotations from collections.abc import Callable @@ -43,7 +44,6 @@ LIGHT_ENTITIES = ( key="floodlight", cmd_key="GetWhiteLed", translation_key="floodlight", - icon="mdi:spotlight-beam", supported=lambda api, ch: api.supported(ch, "floodLight"), is_on_fn=lambda api, ch: api.whiteled_state(ch), turn_on_off_fn=lambda api, ch, value: api.set_whiteled(ch, state=value), @@ -54,7 +54,6 @@ LIGHT_ENTITIES = ( key="status_led", cmd_key="GetPowerLed", translation_key="status_led", - icon="mdi:lightning-bolt-circle", entity_category=EntityCategory.CONFIG, supported=lambda api, ch: api.supported(ch, "power_led"), is_on_fn=lambda api, ch: api.status_led_enabled(ch), diff --git a/homeassistant/components/reolink/media_source.py b/homeassistant/components/reolink/media_source.py index 2a1eee9e97d..c22a0fc28e7 100644 --- a/homeassistant/components/reolink/media_source.py +++ b/homeassistant/components/reolink/media_source.py @@ -46,7 +46,6 @@ class ReolinkVODMediaSource(MediaSource): """Initialize ReolinkVODMediaSource.""" super().__init__(DOMAIN) self.hass = hass - self.data: dict[str, ReolinkData] = hass.data[DOMAIN] async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia: """Resolve media to a url.""" @@ -57,7 +56,8 @@ class ReolinkVODMediaSource(MediaSource): _, config_entry_id, channel_str, stream_res, filename = identifier channel = int(channel_str) - host = self.data[config_entry_id].host + data: dict[str, ReolinkData] = self.hass.data[DOMAIN] + host = data[config_entry_id].host vod_type = VodRequestType.RTMP if host.api.is_nvr: @@ -130,7 +130,8 @@ class ReolinkVODMediaSource(MediaSource): if config_entry.state != ConfigEntryState.LOADED: continue channels: list[str] = [] - host = self.data[config_entry.entry_id].host + data: dict[str, ReolinkData] = self.hass.data[DOMAIN] + host = data[config_entry.entry_id].host entities = er.async_entries_for_config_entry( entity_reg, config_entry.entry_id ) @@ -187,7 +188,8 @@ class ReolinkVODMediaSource(MediaSource): self, config_entry_id: str, channel: int ) -> BrowseMediaSource: """Allow the user to select the high or low playback resolution, (low loads faster).""" - host = self.data[config_entry_id].host + data: dict[str, ReolinkData] = self.hass.data[DOMAIN] + host = data[config_entry_id].host main_enc = await host.api.get_encoding(channel, "main") if main_enc == "h265": @@ -236,14 +238,14 @@ class ReolinkVODMediaSource(MediaSource): self, config_entry_id: str, channel: int, stream: str ) -> BrowseMediaSource: """Return all days on which recordings are available for a reolink camera.""" - host = self.data[config_entry_id].host + data: dict[str, ReolinkData] = self.hass.data[DOMAIN] + host = data[config_entry_id].host # We want today of the camera, not necessarily today of the server now = host.api.time() or await host.api.async_get_time() start = now - dt.timedelta(days=31) end = now - children: list[BrowseMediaSource] = [] if _LOGGER.isEnabledFor(logging.DEBUG): _LOGGER.debug( "Requesting recording days of %s from %s to %s", @@ -254,19 +256,19 @@ class ReolinkVODMediaSource(MediaSource): statuses, _ = await host.api.request_vod_files( channel, start, end, status_only=True, stream=stream ) - for status in statuses: - for day in status.days: - children.append( - BrowseMediaSource( - domain=DOMAIN, - identifier=f"DAY|{config_entry_id}|{channel}|{stream}|{status.year}|{status.month}|{day}", - media_class=MediaClass.DIRECTORY, - media_content_type=MediaType.PLAYLIST, - title=f"{status.year}/{status.month}/{day}", - can_play=False, - can_expand=True, - ) - ) + children: list[BrowseMediaSource] = [ + BrowseMediaSource( + domain=DOMAIN, + identifier=f"DAY|{config_entry_id}|{channel}|{stream}|{status.year}|{status.month}|{day}", + media_class=MediaClass.DIRECTORY, + media_content_type=MediaType.PLAYLIST, + title=f"{status.year}/{status.month}/{day}", + can_play=False, + can_expand=True, + ) + for status in statuses + for day in status.days + ] return BrowseMediaSource( domain=DOMAIN, @@ -289,7 +291,8 @@ class ReolinkVODMediaSource(MediaSource): day: int, ) -> BrowseMediaSource: """Return all recording files on a specific day of a Reolink camera.""" - host = self.data[config_entry_id].host + data: dict[str, ReolinkData] = self.hass.data[DOMAIN] + host = data[config_entry_id].host start = dt.datetime(year, month, day, hour=0, minute=0, second=0) end = dt.datetime(year, month, day, hour=23, minute=59, second=59) diff --git a/homeassistant/components/reolink/number.py b/homeassistant/components/reolink/number.py index b27976eaa0e..c4623c49c91 100644 --- a/homeassistant/components/reolink/number.py +++ b/homeassistant/components/reolink/number.py @@ -1,4 +1,5 @@ """Component providing support for Reolink number entities.""" + from __future__ import annotations from collections.abc import Callable @@ -43,7 +44,6 @@ NUMBER_ENTITIES = ( key="zoom", cmd_key="GetZoomFocus", translation_key="zoom", - icon="mdi:magnify", mode=NumberMode.SLIDER, native_step=1, get_min_value=lambda api, ch: api.zoom_range(ch)["zoom"]["pos"]["min"], @@ -56,7 +56,6 @@ NUMBER_ENTITIES = ( key="focus", cmd_key="GetZoomFocus", translation_key="focus", - icon="mdi:focus-field", mode=NumberMode.SLIDER, native_step=1, get_min_value=lambda api, ch: api.zoom_range(ch)["focus"]["pos"]["min"], @@ -72,7 +71,6 @@ NUMBER_ENTITIES = ( key="floodlight_brightness", cmd_key="GetWhiteLed", translation_key="floodlight_brightness", - icon="mdi:spotlight-beam", entity_category=EntityCategory.CONFIG, native_step=1, native_min_value=1, @@ -85,7 +83,6 @@ NUMBER_ENTITIES = ( key="volume", cmd_key="GetAudioCfg", translation_key="volume", - icon="mdi:volume-high", entity_category=EntityCategory.CONFIG, native_step=1, native_min_value=0, @@ -98,7 +95,6 @@ NUMBER_ENTITIES = ( key="guard_return_time", cmd_key="GetPtzGuard", translation_key="guard_return_time", - icon="mdi:crosshairs-gps", entity_category=EntityCategory.CONFIG, native_step=1, native_unit_of_measurement=UnitOfTime.SECONDS, @@ -112,7 +108,6 @@ NUMBER_ENTITIES = ( key="motion_sensitivity", cmd_key="GetMdAlarm", translation_key="motion_sensitivity", - icon="mdi:motion-sensor", entity_category=EntityCategory.CONFIG, native_step=1, native_min_value=1, @@ -124,8 +119,7 @@ NUMBER_ENTITIES = ( ReolinkNumberEntityDescription( key="ai_face_sensititvity", cmd_key="GetAiAlarm", - translation_key="ai_face_sensititvity", - icon="mdi:face-recognition", + translation_key="ai_face_sensitivity", entity_category=EntityCategory.CONFIG, native_step=1, native_min_value=0, @@ -139,8 +133,7 @@ NUMBER_ENTITIES = ( ReolinkNumberEntityDescription( key="ai_person_sensititvity", cmd_key="GetAiAlarm", - translation_key="ai_person_sensititvity", - icon="mdi:account", + translation_key="ai_person_sensitivity", entity_category=EntityCategory.CONFIG, native_step=1, native_min_value=0, @@ -154,8 +147,7 @@ NUMBER_ENTITIES = ( ReolinkNumberEntityDescription( key="ai_vehicle_sensititvity", cmd_key="GetAiAlarm", - translation_key="ai_vehicle_sensititvity", - icon="mdi:car", + translation_key="ai_vehicle_sensitivity", entity_category=EntityCategory.CONFIG, native_step=1, native_min_value=0, @@ -166,11 +158,24 @@ NUMBER_ENTITIES = ( value=lambda api, ch: api.ai_sensitivity(ch, "vehicle"), method=lambda api, ch, value: api.set_ai_sensitivity(ch, int(value), "vehicle"), ), + ReolinkNumberEntityDescription( + key="ai_package_sensititvity", + cmd_key="GetAiAlarm", + translation_key="ai_package_sensitivity", + entity_category=EntityCategory.CONFIG, + native_step=1, + native_min_value=0, + native_max_value=100, + supported=lambda api, ch: ( + api.supported(ch, "ai_sensitivity") and api.ai_supported(ch, "package") + ), + value=lambda api, ch: api.ai_sensitivity(ch, "package"), + method=lambda api, ch, value: api.set_ai_sensitivity(ch, int(value), "package"), + ), ReolinkNumberEntityDescription( key="ai_pet_sensititvity", cmd_key="GetAiAlarm", - translation_key="ai_pet_sensititvity", - icon="mdi:dog-side", + translation_key="ai_pet_sensitivity", entity_category=EntityCategory.CONFIG, native_step=1, native_min_value=0, @@ -186,8 +191,7 @@ NUMBER_ENTITIES = ( ReolinkNumberEntityDescription( key="ai_pet_sensititvity", cmd_key="GetAiAlarm", - translation_key="ai_animal_sensititvity", - icon="mdi:paw", + translation_key="ai_animal_sensitivity", entity_category=EntityCategory.CONFIG, native_step=1, native_min_value=0, @@ -202,7 +206,6 @@ NUMBER_ENTITIES = ( key="ai_face_delay", cmd_key="GetAiAlarm", translation_key="ai_face_delay", - icon="mdi:face-recognition", entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, native_step=1, @@ -219,7 +222,6 @@ NUMBER_ENTITIES = ( key="ai_person_delay", cmd_key="GetAiAlarm", translation_key="ai_person_delay", - icon="mdi:account", entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, native_step=1, @@ -236,7 +238,6 @@ NUMBER_ENTITIES = ( key="ai_vehicle_delay", cmd_key="GetAiAlarm", translation_key="ai_vehicle_delay", - icon="mdi:car", entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, native_step=1, @@ -249,11 +250,26 @@ NUMBER_ENTITIES = ( value=lambda api, ch: api.ai_delay(ch, "vehicle"), method=lambda api, ch, value: api.set_ai_delay(ch, int(value), "vehicle"), ), + ReolinkNumberEntityDescription( + key="ai_package_delay", + cmd_key="GetAiAlarm", + translation_key="ai_package_delay", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + native_step=1, + native_unit_of_measurement=UnitOfTime.SECONDS, + native_min_value=0, + native_max_value=8, + supported=lambda api, ch: ( + api.supported(ch, "ai_delay") and api.ai_supported(ch, "package") + ), + value=lambda api, ch: api.ai_delay(ch, "package"), + method=lambda api, ch, value: api.set_ai_delay(ch, int(value), "package"), + ), ReolinkNumberEntityDescription( key="ai_pet_delay", cmd_key="GetAiAlarm", translation_key="ai_pet_delay", - icon="mdi:dog-side", entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, native_step=1, @@ -272,7 +288,6 @@ NUMBER_ENTITIES = ( key="ai_pet_delay", cmd_key="GetAiAlarm", translation_key="ai_animal_delay", - icon="mdi:paw", entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, native_step=1, @@ -289,7 +304,6 @@ NUMBER_ENTITIES = ( key="auto_quick_reply_time", cmd_key="GetAutoReply", translation_key="auto_quick_reply_time", - icon="mdi:message-reply-text-outline", entity_category=EntityCategory.CONFIG, native_step=1, native_unit_of_measurement=UnitOfTime.SECONDS, @@ -303,7 +317,6 @@ NUMBER_ENTITIES = ( key="auto_track_limit_left", cmd_key="GetPtzTraceSection", translation_key="auto_track_limit_left", - icon="mdi:angle-acute", mode=NumberMode.SLIDER, entity_category=EntityCategory.CONFIG, native_step=1, @@ -317,7 +330,6 @@ NUMBER_ENTITIES = ( key="auto_track_limit_right", cmd_key="GetPtzTraceSection", translation_key="auto_track_limit_right", - icon="mdi:angle-acute", mode=NumberMode.SLIDER, entity_category=EntityCategory.CONFIG, native_step=1, @@ -331,7 +343,6 @@ NUMBER_ENTITIES = ( key="auto_track_disappear_time", cmd_key="GetAiCfg", translation_key="auto_track_disappear_time", - icon="mdi:target-account", entity_category=EntityCategory.CONFIG, native_step=1, native_unit_of_measurement=UnitOfTime.SECONDS, @@ -347,7 +358,6 @@ NUMBER_ENTITIES = ( key="auto_track_stop_time", cmd_key="GetAiCfg", translation_key="auto_track_stop_time", - icon="mdi:target-account", entity_category=EntityCategory.CONFIG, native_step=1, native_unit_of_measurement=UnitOfTime.SECONDS, @@ -361,7 +371,6 @@ NUMBER_ENTITIES = ( key="day_night_switch_threshold", cmd_key="GetIsp", translation_key="day_night_switch_threshold", - icon="mdi:theme-light-dark", entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, native_step=1, @@ -375,7 +384,6 @@ NUMBER_ENTITIES = ( key="image_brightness", cmd_key="GetImage", translation_key="image_brightness", - icon="mdi:image-edit", entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, native_step=1, @@ -389,7 +397,6 @@ NUMBER_ENTITIES = ( key="image_contrast", cmd_key="GetImage", translation_key="image_contrast", - icon="mdi:image-edit", entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, native_step=1, @@ -403,7 +410,6 @@ NUMBER_ENTITIES = ( key="image_saturation", cmd_key="GetImage", translation_key="image_saturation", - icon="mdi:image-edit", entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, native_step=1, @@ -417,7 +423,6 @@ NUMBER_ENTITIES = ( key="image_sharpness", cmd_key="GetImage", translation_key="image_sharpness", - icon="mdi:image-edit", entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, native_step=1, @@ -431,7 +436,6 @@ NUMBER_ENTITIES = ( key="image_hue", cmd_key="GetImage", translation_key="image_hue", - icon="mdi:image-edit", entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, native_step=1, diff --git a/homeassistant/components/reolink/select.py b/homeassistant/components/reolink/select.py index 769ccdf7e01..13757e7bb22 100644 --- a/homeassistant/components/reolink/select.py +++ b/homeassistant/components/reolink/select.py @@ -1,4 +1,5 @@ """Component providing support for Reolink select entities.""" + from __future__ import annotations from collections.abc import Callable @@ -41,12 +42,16 @@ class ReolinkSelectEntityDescription( value: Callable[[Host, int], str] | None = None +def _get_quick_reply_id(api: Host, ch: int, mess: str) -> int: + """Get the quick reply file id from the message string.""" + return [k for k, v in api.quick_reply_dict(ch).items() if v == mess][0] + + SELECT_ENTITIES = ( ReolinkSelectEntityDescription( key="floodlight_mode", cmd_key="GetWhiteLed", translation_key="floodlight_mode", - icon="mdi:spotlight-beam", entity_category=EntityCategory.CONFIG, get_options=lambda api, ch: api.whiteled_mode_list(ch), supported=lambda api, ch: api.supported(ch, "floodLight"), @@ -57,7 +62,6 @@ SELECT_ENTITIES = ( key="day_night_mode", cmd_key="GetIsp", translation_key="day_night_mode", - icon="mdi:theme-light-dark", entity_category=EntityCategory.CONFIG, get_options=[mode.name for mode in DayNightEnum], supported=lambda api, ch: api.supported(ch, "dayNight"), @@ -67,29 +71,35 @@ SELECT_ENTITIES = ( ReolinkSelectEntityDescription( key="ptz_preset", translation_key="ptz_preset", - icon="mdi:pan", get_options=lambda api, ch: list(api.ptz_presets(ch)), supported=lambda api, ch: api.supported(ch, "ptz_presets"), method=lambda api, ch, name: api.set_ptz_command(ch, preset=name), ), + ReolinkSelectEntityDescription( + key="play_quick_reply_message", + translation_key="play_quick_reply_message", + get_options=lambda api, ch: list(api.quick_reply_dict(ch).values())[1:], + supported=lambda api, ch: api.supported(ch, "play_quick_reply"), + method=lambda api, ch, mess: ( + api.play_quick_reply(ch, file_id=_get_quick_reply_id(api, ch, mess)) + ), + ), ReolinkSelectEntityDescription( key="auto_quick_reply_message", cmd_key="GetAutoReply", translation_key="auto_quick_reply_message", - icon="mdi:message-reply-text-outline", entity_category=EntityCategory.CONFIG, get_options=lambda api, ch: list(api.quick_reply_dict(ch).values()), supported=lambda api, ch: api.supported(ch, "quick_reply"), value=lambda api, ch: api.quick_reply_dict(ch)[api.quick_reply_file(ch)], - method=lambda api, ch, mess: api.set_quick_reply( - ch, file_id=[k for k, v in api.quick_reply_dict(ch).items() if v == mess][0] + method=lambda api, ch, mess: ( + api.set_quick_reply(ch, file_id=_get_quick_reply_id(api, ch, mess)) ), ), ReolinkSelectEntityDescription( key="auto_track_method", cmd_key="GetAiCfg", translation_key="auto_track_method", - icon="mdi:target-account", entity_category=EntityCategory.CONFIG, get_options=[method.name for method in TrackMethodEnum], supported=lambda api, ch: api.supported(ch, "auto_track_method"), @@ -100,7 +110,6 @@ SELECT_ENTITIES = ( key="status_led", cmd_key="GetPowerLed", translation_key="status_led", - icon="mdi:lightning-bolt-circle", entity_category=EntityCategory.CONFIG, get_options=[state.name for state in StatusLedEnum], supported=lambda api, ch: api.supported(ch, "doorbell_led"), diff --git a/homeassistant/components/reolink/sensor.py b/homeassistant/components/reolink/sensor.py index 6f4af489fe5..36363beaf80 100644 --- a/homeassistant/components/reolink/sensor.py +++ b/homeassistant/components/reolink/sensor.py @@ -1,4 +1,5 @@ """Component providing support for Reolink sensors.""" + from __future__ import annotations from collections.abc import Callable @@ -14,7 +15,7 @@ from homeassistant.components.sensor import ( SensorStateClass, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import EntityCategory +from homeassistant.const import PERCENTAGE, EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType @@ -36,7 +37,7 @@ class ReolinkSensorEntityDescription( ): """A class that describes sensor entities for a camera channel.""" - value: Callable[[Host, int], int] + value: Callable[[Host, int], int | float] @dataclass(frozen=True, kw_only=True) @@ -54,7 +55,6 @@ SENSORS = ( key="ptz_pan_position", cmd_key="GetPtzCurPos", translation_key="ptz_pan_position", - icon="mdi:pan", state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, value=lambda api, ch: api.ptz_pan_position(ch), @@ -67,7 +67,6 @@ HOST_SENSORS = ( key="wifi_signal", cmd_key="GetWifiSignal", translation_key="wifi_signal", - icon="mdi:wifi", state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -76,6 +75,19 @@ HOST_SENSORS = ( ), ) +HDD_SENSORS = ( + ReolinkSensorEntityDescription( + key="storage", + cmd_key="GetHddInfo", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + value=lambda api, idx: api.hdd_storage(idx), + supported=lambda api, idx: api.supported(None, "hdd"), + ), +) + async def async_setup_entry( hass: HomeAssistant, @@ -85,7 +97,9 @@ async def async_setup_entry( """Set up a Reolink IP Camera.""" reolink_data: ReolinkData = hass.data[DOMAIN][config_entry.entry_id] - entities: list[ReolinkSensorEntity | ReolinkHostSensorEntity] = [ + entities: list[ + ReolinkSensorEntity | ReolinkHostSensorEntity | ReolinkHddSensorEntity + ] = [ ReolinkSensorEntity(reolink_data, channel, entity_description) for entity_description in SENSORS for channel in reolink_data.host.api.channels @@ -98,6 +112,14 @@ async def async_setup_entry( if entity_description.supported(reolink_data.host.api) ] ) + entities.extend( + [ + ReolinkHddSensorEntity(reolink_data, hdd_index, entity_description) + for entity_description in HDD_SENSORS + for hdd_index in reolink_data.host.api.hdd_list + if entity_description.supported(reolink_data.host.api, hdd_index) + ] + ) async_add_entities(entities) @@ -140,3 +162,38 @@ class ReolinkHostSensorEntity(ReolinkHostCoordinatorEntity, SensorEntity): def native_value(self) -> StateType | date | datetime | Decimal: """Return the value reported by the sensor.""" return self.entity_description.value(self._host.api) + + +class ReolinkHddSensorEntity(ReolinkHostCoordinatorEntity, SensorEntity): + """Base sensor class for Reolink host sensors.""" + + entity_description: ReolinkSensorEntityDescription + + def __init__( + self, + reolink_data: ReolinkData, + hdd_index: int, + entity_description: ReolinkSensorEntityDescription, + ) -> None: + """Initialize Reolink host sensor.""" + self.entity_description = entity_description + super().__init__(reolink_data) + self._hdd_index = hdd_index + self._attr_translation_placeholders = {"hdd_index": str(hdd_index)} + self._attr_unique_id = ( + f"{self._host.unique_id}_{hdd_index}_{entity_description.key}" + ) + if self._host.api.hdd_type(hdd_index) == "HDD": + self._attr_translation_key = "hdd_storage" + else: + self._attr_translation_key = "sd_storage" + + @property + def native_value(self) -> StateType | date | datetime | Decimal: + """Return the value reported by the sensor.""" + return self.entity_description.value(self._host.api, self._hdd_index) + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self._host.api.hdd_available(self._hdd_index) and super().available diff --git a/homeassistant/components/reolink/siren.py b/homeassistant/components/reolink/siren.py index 90590acb4e4..269c0690105 100644 --- a/homeassistant/components/reolink/siren.py +++ b/homeassistant/components/reolink/siren.py @@ -1,4 +1,5 @@ """Component providing support for Reolink siren entities.""" + from __future__ import annotations from dataclasses import dataclass @@ -34,7 +35,6 @@ SIREN_ENTITIES = ( ReolinkSirenEntityDescription( key="siren", translation_key="siren", - icon="mdi:alarm-light", supported=lambda api, ch: api.supported(ch, "siren_play"), ), ) diff --git a/homeassistant/components/reolink/strings.json b/homeassistant/components/reolink/strings.json index fb4d42bb97d..2282289bdbc 100644 --- a/homeassistant/components/reolink/strings.json +++ b/homeassistant/components/reolink/strings.json @@ -103,6 +103,9 @@ "visitor": { "name": "Visitor" }, + "package": { + "name": "Package" + }, "motion_lens_0": { "name": "Motion lens 0" }, @@ -124,6 +127,9 @@ "visitor_lens_0": { "name": "Visitor lens 0" }, + "package_lens_0": { + "name": "Package lens 0" + }, "motion_lens_1": { "name": "Motion lens 1" }, @@ -144,6 +150,9 @@ }, "visitor_lens_1": { "name": "Visitor lens 1" + }, + "package_lens_1": { + "name": "Package lens 1" } }, "button": { @@ -261,19 +270,22 @@ "motion_sensitivity": { "name": "Motion sensitivity" }, - "ai_face_sensititvity": { + "ai_face_sensitivity": { "name": "AI face sensitivity" }, - "ai_person_sensititvity": { + "ai_person_sensitivity": { "name": "AI person sensitivity" }, - "ai_vehicle_sensititvity": { + "ai_vehicle_sensitivity": { "name": "AI vehicle sensitivity" }, - "ai_pet_sensititvity": { + "ai_package_sensitivity": { + "name": "AI package sensitivity" + }, + "ai_pet_sensitivity": { "name": "AI pet sensitivity" }, - "ai_animal_sensititvity": { + "ai_animal_sensitivity": { "name": "AI animal sensitivity" }, "ai_face_delay": { @@ -285,6 +297,9 @@ "ai_vehicle_delay": { "name": "AI vehicle delay" }, + "ai_package_delay": { + "name": "AI package delay" + }, "ai_pet_delay": { "name": "AI pet delay" }, @@ -348,6 +363,9 @@ "ptz_preset": { "name": "PTZ preset" }, + "play_quick_reply_message": { + "name": "Play quick reply message" + }, "auto_quick_reply_message": { "name": "Auto quick reply message", "state": { @@ -378,6 +396,12 @@ }, "ptz_pan_position": { "name": "PTZ pan position" + }, + "hdd_storage": { + "name": "HDD {hdd_index} storage" + }, + "sd_storage": { + "name": "SD {hdd_index} storage" } }, "siren": { @@ -401,9 +425,12 @@ "auto_focus": { "name": "Auto focus" }, - "gaurd_return": { + "guard_return": { "name": "Guard return" }, + "ptz_patrol": { + "name": "PTZ patrol" + }, "email": { "name": "Email on event" }, diff --git a/homeassistant/components/reolink/switch.py b/homeassistant/components/reolink/switch.py index 7f57b78df1e..adda97debb4 100644 --- a/homeassistant/components/reolink/switch.py +++ b/homeassistant/components/reolink/switch.py @@ -1,4 +1,5 @@ """Component providing support for Reolink switch entities.""" + from __future__ import annotations from collections.abc import Callable @@ -33,7 +34,7 @@ class ReolinkSwitchEntityDescription( """A class that describes switch entities.""" method: Callable[[Host, int, bool], Any] - value: Callable[[Host, int], bool] + value: Callable[[Host, int], bool | None] @dataclass(frozen=True, kw_only=True) @@ -52,7 +53,6 @@ SWITCH_ENTITIES = ( key="ir_lights", cmd_key="GetIrLights", translation_key="ir_lights", - icon="mdi:led-off", entity_category=EntityCategory.CONFIG, supported=lambda api, ch: api.supported(ch, "ir_lights"), value=lambda api, ch: api.ir_enabled(ch), @@ -62,7 +62,6 @@ SWITCH_ENTITIES = ( key="record_audio", cmd_key="GetEnc", translation_key="record_audio", - icon="mdi:microphone", entity_category=EntityCategory.CONFIG, supported=lambda api, ch: api.supported(ch, "audio"), value=lambda api, ch: api.audio_record(ch), @@ -72,7 +71,6 @@ SWITCH_ENTITIES = ( key="siren_on_event", cmd_key="GetAudioAlarm", translation_key="siren_on_event", - icon="mdi:alarm-light", entity_category=EntityCategory.CONFIG, supported=lambda api, ch: api.supported(ch, "siren"), value=lambda api, ch: api.audio_alarm_enabled(ch), @@ -82,7 +80,6 @@ SWITCH_ENTITIES = ( key="auto_tracking", cmd_key="GetAiCfg", translation_key="auto_tracking", - icon="mdi:target-account", entity_category=EntityCategory.CONFIG, supported=lambda api, ch: api.supported(ch, "auto_track"), value=lambda api, ch: api.auto_track_enabled(ch), @@ -92,7 +89,6 @@ SWITCH_ENTITIES = ( key="auto_focus", cmd_key="GetAutoFocus", translation_key="auto_focus", - icon="mdi:focus-field", entity_category=EntityCategory.CONFIG, supported=lambda api, ch: api.supported(ch, "auto_focus"), value=lambda api, ch: api.autofocus_enabled(ch), @@ -101,18 +97,23 @@ SWITCH_ENTITIES = ( ReolinkSwitchEntityDescription( key="gaurd_return", cmd_key="GetPtzGuard", - translation_key="gaurd_return", - icon="mdi:crosshairs-gps", + translation_key="guard_return", entity_category=EntityCategory.CONFIG, supported=lambda api, ch: api.supported(ch, "ptz_guard"), value=lambda api, ch: api.ptz_guard_enabled(ch), method=lambda api, ch, value: api.set_ptz_guard(ch, enable=value), ), + ReolinkSwitchEntityDescription( + key="ptz_patrol", + translation_key="ptz_patrol", + supported=lambda api, ch: api.supported(ch, "ptz_patrol"), + value=lambda api, ch: None, + method=lambda api, ch, value: api.ctrl_ptz_patrol(ch, value), + ), ReolinkSwitchEntityDescription( key="email", cmd_key="GetEmail", translation_key="email", - icon="mdi:email", entity_category=EntityCategory.CONFIG, supported=lambda api, ch: api.supported(ch, "email") and api.is_nvr, value=lambda api, ch: api.email_enabled(ch), @@ -122,7 +123,6 @@ SWITCH_ENTITIES = ( key="ftp_upload", cmd_key="GetFtp", translation_key="ftp_upload", - icon="mdi:swap-horizontal", entity_category=EntityCategory.CONFIG, supported=lambda api, ch: api.supported(ch, "ftp") and api.is_nvr, value=lambda api, ch: api.ftp_enabled(ch), @@ -132,7 +132,6 @@ SWITCH_ENTITIES = ( key="push_notifications", cmd_key="GetPush", translation_key="push_notifications", - icon="mdi:message-badge", entity_category=EntityCategory.CONFIG, supported=lambda api, ch: api.supported(ch, "push") and api.is_nvr, value=lambda api, ch: api.push_enabled(ch), @@ -142,7 +141,6 @@ SWITCH_ENTITIES = ( key="record", cmd_key="GetRec", translation_key="record", - icon="mdi:record-rec", entity_category=EntityCategory.CONFIG, supported=lambda api, ch: api.supported(ch, "recording") and api.is_nvr, value=lambda api, ch: api.recording_enabled(ch), @@ -152,7 +150,6 @@ SWITCH_ENTITIES = ( key="buzzer", cmd_key="GetBuzzerAlarmV20", translation_key="buzzer", - icon="mdi:room-service", entity_category=EntityCategory.CONFIG, supported=lambda api, ch: api.supported(ch, "buzzer") and api.is_nvr, value=lambda api, ch: api.buzzer_enabled(ch), @@ -162,7 +159,6 @@ SWITCH_ENTITIES = ( key="doorbell_button_sound", cmd_key="GetAudioCfg", translation_key="doorbell_button_sound", - icon="mdi:volume-high", entity_category=EntityCategory.CONFIG, supported=lambda api, ch: api.supported(ch, "doorbell_button_sound"), value=lambda api, ch: api.doorbell_button_sound(ch), @@ -172,7 +168,6 @@ SWITCH_ENTITIES = ( key="hdr", cmd_key="GetIsp", translation_key="hdr", - icon="mdi:hdr", entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, supported=lambda api, ch: api.supported(ch, "HDR"), @@ -186,7 +181,6 @@ NVR_SWITCH_ENTITIES = ( key="email", cmd_key="GetEmail", translation_key="email", - icon="mdi:email", entity_category=EntityCategory.CONFIG, supported=lambda api: api.supported(None, "email"), value=lambda api: api.email_enabled(), @@ -196,7 +190,6 @@ NVR_SWITCH_ENTITIES = ( key="ftp_upload", cmd_key="GetFtp", translation_key="ftp_upload", - icon="mdi:swap-horizontal", entity_category=EntityCategory.CONFIG, supported=lambda api: api.supported(None, "ftp"), value=lambda api: api.ftp_enabled(), @@ -206,7 +199,6 @@ NVR_SWITCH_ENTITIES = ( key="push_notifications", cmd_key="GetPush", translation_key="push_notifications", - icon="mdi:message-badge", entity_category=EntityCategory.CONFIG, supported=lambda api: api.supported(None, "push"), value=lambda api: api.push_enabled(), @@ -216,7 +208,6 @@ NVR_SWITCH_ENTITIES = ( key="record", cmd_key="GetRec", translation_key="record", - icon="mdi:record-rec", entity_category=EntityCategory.CONFIG, supported=lambda api: api.supported(None, "recording"), value=lambda api: api.recording_enabled(), @@ -275,7 +266,7 @@ class ReolinkSwitchEntity(ReolinkChannelCoordinatorEntity, SwitchEntity): super().__init__(reolink_data, channel) @property - def is_on(self) -> bool: + def is_on(self) -> bool | None: """Return true if switch is on.""" return self.entity_description.value(self._host.api, self._channel) diff --git a/homeassistant/components/reolink/update.py b/homeassistant/components/reolink/update.py index ffd429e92ad..41933ae2efc 100644 --- a/homeassistant/components/reolink/update.py +++ b/homeassistant/components/reolink/update.py @@ -1,4 +1,5 @@ """Update entities for Reolink devices.""" + from __future__ import annotations from datetime import datetime diff --git a/homeassistant/components/reolink/util.py b/homeassistant/components/reolink/util.py index cc9ad192bc3..cf4659224e3 100644 --- a/homeassistant/components/reolink/util.py +++ b/homeassistant/components/reolink/util.py @@ -1,4 +1,5 @@ """Utility functions for the Reolink component.""" + from __future__ import annotations from homeassistant import config_entries diff --git a/homeassistant/components/repairs/__init__.py b/homeassistant/components/repairs/__init__.py index 228972e8718..8d3fc429ce0 100644 --- a/homeassistant/components/repairs/__init__.py +++ b/homeassistant/components/repairs/__init__.py @@ -1,4 +1,5 @@ """The repairs integration.""" + from __future__ import annotations from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/repairs/issue_handler.py b/homeassistant/components/repairs/issue_handler.py index f2ce3bac84e..8a170b1de8d 100644 --- a/homeassistant/components/repairs/issue_handler.py +++ b/homeassistant/components/repairs/issue_handler.py @@ -1,4 +1,5 @@ """The repairs integration.""" + from __future__ import annotations from typing import Any @@ -102,7 +103,9 @@ 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) + await async_process_integration_platforms( + hass, DOMAIN, _register_repairs_platform, wait_for_platforms=True + ) @callback diff --git a/homeassistant/components/repairs/models.py b/homeassistant/components/repairs/models.py index 6ae175b29e9..afac8813d1e 100644 --- a/homeassistant/components/repairs/models.py +++ b/homeassistant/components/repairs/models.py @@ -1,4 +1,5 @@ """Models for Repairs.""" + from __future__ import annotations from typing import Protocol diff --git a/homeassistant/components/repairs/websocket_api.py b/homeassistant/components/repairs/websocket_api.py index 78a3c10bbe4..af5f82e49d4 100644 --- a/homeassistant/components/repairs/websocket_api.py +++ b/homeassistant/components/repairs/websocket_api.py @@ -1,4 +1,5 @@ """The repairs websocket API.""" + from __future__ import annotations from http import HTTPStatus diff --git a/homeassistant/components/repetier/__init__.py b/homeassistant/components/repetier/__init__.py index 91a16ea3fbe..2642e78e7ec 100644 --- a/homeassistant/components/repetier/__init__.py +++ b/homeassistant/components/repetier/__init__.py @@ -1,4 +1,5 @@ """Support for Repetier-Server sensors.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/homeassistant/components/repetier/sensor.py b/homeassistant/components/repetier/sensor.py index 578ca58b80f..d413c25c8d4 100644 --- a/homeassistant/components/repetier/sensor.py +++ b/homeassistant/components/repetier/sensor.py @@ -1,4 +1,5 @@ """Support for monitoring Repetier Server Sensors.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/rest/__init__.py b/homeassistant/components/rest/__init__.py index ee79c45921c..1c33b4592df 100644 --- a/homeassistant/components/rest/__init__.py +++ b/homeassistant/components/rest/__init__.py @@ -1,4 +1,5 @@ """The rest component.""" + from __future__ import annotations import asyncio @@ -40,6 +41,7 @@ from homeassistant.helpers.reload import ( ) from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from homeassistant.util.async_ import create_eager_task from .const import ( CONF_ENCODING, @@ -129,10 +131,10 @@ async def _async_process_config(hass: HomeAssistant, config: ConfigType) -> bool load_coroutines.append(load_coroutine) if refresh_coroutines: - await asyncio.gather(*refresh_coroutines) + await asyncio.gather(*(create_eager_task(coro) for coro in refresh_coroutines)) if load_coroutines: - await asyncio.gather(*load_coroutines) + await asyncio.gather(*(create_eager_task(coro) for coro in load_coroutines)) return True diff --git a/homeassistant/components/rest/binary_sensor.py b/homeassistant/components/rest/binary_sensor.py index 8c629e2240e..0568203a91c 100644 --- a/homeassistant/components/rest/binary_sensor.py +++ b/homeassistant/components/rest/binary_sensor.py @@ -1,4 +1,5 @@ """Support for RESTful binary sensors.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/rest/data.py b/homeassistant/components/rest/data.py index 61c88a14400..06be7a4f6ff 100644 --- a/homeassistant/components/rest/data.py +++ b/homeassistant/components/rest/data.py @@ -1,4 +1,5 @@ """Support for RESTful API.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/rest/entity.py b/homeassistant/components/rest/entity.py index f0cccc8b762..3695c899371 100644 --- a/homeassistant/components/rest/entity.py +++ b/homeassistant/components/rest/entity.py @@ -1,4 +1,5 @@ """The base entity for the rest component.""" + from __future__ import annotations from abc import abstractmethod diff --git a/homeassistant/components/rest/icons.json b/homeassistant/components/rest/icons.json new file mode 100644 index 00000000000..a03163179cb --- /dev/null +++ b/homeassistant/components/rest/icons.json @@ -0,0 +1,5 @@ +{ + "services": { + "reload": "mdi:reload" + } +} diff --git a/homeassistant/components/rest/notify.py b/homeassistant/components/rest/notify.py index e155fe47048..7744154c1c5 100644 --- a/homeassistant/components/rest/notify.py +++ b/homeassistant/components/rest/notify.py @@ -1,4 +1,5 @@ """RESTful platform for notify component.""" + from __future__ import annotations from http import HTTPStatus diff --git a/homeassistant/components/rest/sensor.py b/homeassistant/components/rest/sensor.py index 67f70a716b0..199ab3721c3 100644 --- a/homeassistant/components/rest/sensor.py +++ b/homeassistant/components/rest/sensor.py @@ -1,4 +1,5 @@ """Support for RESTful API sensors.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/rest/switch.py b/homeassistant/components/rest/switch.py index e021b72ff3d..f0da6366cfc 100644 --- a/homeassistant/components/rest/switch.py +++ b/homeassistant/components/rest/switch.py @@ -1,4 +1,5 @@ """Support for RESTful switches.""" + from __future__ import annotations from http import HTTPStatus diff --git a/homeassistant/components/rest_command/__init__.py b/homeassistant/components/rest_command/__init__.py index 199186cf222..c43e23cf068 100644 --- a/homeassistant/components/rest_command/__init__.py +++ b/homeassistant/components/rest_command/__init__.py @@ -1,4 +1,5 @@ """Support for exposing regular REST commands as services.""" + from __future__ import annotations from http import HTTPStatus @@ -172,33 +173,37 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: _content = await response.text() except (JSONDecodeError, AttributeError) as err: raise HomeAssistantError( - f"Response of '{request_url}' could not be decoded as JSON", translation_domain=DOMAIN, translation_key="decoding_error", - translation_placeholders={"decoding_type": "json"}, + translation_placeholders={ + "request_url": request_url, + "decoding_type": "JSON", + }, ) from err except UnicodeDecodeError as err: raise HomeAssistantError( - f"Response of '{request_url}' could not be decoded as text", translation_domain=DOMAIN, translation_key="decoding_error", - translation_placeholders={"decoding_type": "text"}, + translation_placeholders={ + "request_url": request_url, + "decoding_type": "text", + }, ) from err return {"content": _content, "status": response.status} except TimeoutError as err: raise HomeAssistantError( - f"Timeout when calling resource '{request_url}'", translation_domain=DOMAIN, translation_key="timeout", + translation_placeholders={"request_url": request_url}, ) from err except aiohttp.ClientError as err: raise HomeAssistantError( - f"Client error occurred when calling resource '{request_url}'", translation_domain=DOMAIN, translation_key="client_error", + translation_placeholders={"request_url": request_url}, ) from err # register services diff --git a/homeassistant/components/rest_command/icons.json b/homeassistant/components/rest_command/icons.json new file mode 100644 index 00000000000..a03163179cb --- /dev/null +++ b/homeassistant/components/rest_command/icons.json @@ -0,0 +1,5 @@ +{ + "services": { + "reload": "mdi:reload" + } +} diff --git a/homeassistant/components/rest_command/strings.json b/homeassistant/components/rest_command/strings.json index 8a48cddace3..2d658ad8b20 100644 --- a/homeassistant/components/rest_command/strings.json +++ b/homeassistant/components/rest_command/strings.json @@ -7,13 +7,13 @@ }, "exceptions": { "timeout": { - "message": "Timeout while waiting for response from the server" + "message": "Timeout when calling resource '{request_url}'" }, "client_error": { - "message": "An error occurred while requesting the resource" + "message": "Client error occurred when calling resource '{request_url}'" }, "decoding_error": { - "message": "The response from the server could not be decoded as {decoding_type}" + "message": "The response of '{request_url}' could not be decoded as {decoding_type}" } } } diff --git a/homeassistant/components/rflink/__init__.py b/homeassistant/components/rflink/__init__.py index 5b90e656911..74b7d4aa4c0 100644 --- a/homeassistant/components/rflink/__init__.py +++ b/homeassistant/components/rflink/__init__.py @@ -1,4 +1,5 @@ """Support for Rflink devices.""" + from __future__ import annotations import asyncio @@ -376,7 +377,7 @@ class RflinkDevice(Entity): def _handle_event(self, event): """Platform specific event handler.""" - raise NotImplementedError() + raise NotImplementedError @property def name(self): diff --git a/homeassistant/components/rflink/binary_sensor.py b/homeassistant/components/rflink/binary_sensor.py index e307d9de382..789e25c62b1 100644 --- a/homeassistant/components/rflink/binary_sensor.py +++ b/homeassistant/components/rflink/binary_sensor.py @@ -1,4 +1,5 @@ """Support for Rflink binary sensors.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/rflink/cover.py b/homeassistant/components/rflink/cover.py index 9492611f439..d440b324532 100644 --- a/homeassistant/components/rflink/cover.py +++ b/homeassistant/components/rflink/cover.py @@ -1,4 +1,5 @@ """Support for Rflink Cover devices.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/rflink/icons.json b/homeassistant/components/rflink/icons.json new file mode 100644 index 00000000000..988b048eee7 --- /dev/null +++ b/homeassistant/components/rflink/icons.json @@ -0,0 +1,5 @@ +{ + "services": { + "send_command": "mdi:send" + } +} diff --git a/homeassistant/components/rflink/light.py b/homeassistant/components/rflink/light.py index 347d2f1074f..d354e317ccb 100644 --- a/homeassistant/components/rflink/light.py +++ b/homeassistant/components/rflink/light.py @@ -1,4 +1,5 @@ """Support for Rflink lights.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/rflink/sensor.py b/homeassistant/components/rflink/sensor.py index fd6db8f0c60..b01d1f709fe 100644 --- a/homeassistant/components/rflink/sensor.py +++ b/homeassistant/components/rflink/sensor.py @@ -1,4 +1,5 @@ """Support for Rflink sensors.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/rflink/switch.py b/homeassistant/components/rflink/switch.py index fe82dd0297b..fdf8f63ab7d 100644 --- a/homeassistant/components/rflink/switch.py +++ b/homeassistant/components/rflink/switch.py @@ -1,4 +1,5 @@ """Support for Rflink switches.""" + from __future__ import annotations import voluptuous as vol diff --git a/homeassistant/components/rfxtrx/__init__.py b/homeassistant/components/rfxtrx/__init__.py index 0f3988442c7..4cacb27b49a 100644 --- a/homeassistant/components/rfxtrx/__init__.py +++ b/homeassistant/components/rfxtrx/__init__.py @@ -1,4 +1,5 @@ """Support for RFXtrx devices.""" + from __future__ import annotations import binascii @@ -79,6 +80,7 @@ SERVICE_SEND_SCHEMA = vol.Schema({ATTR_EVENT: _bytearray_string}) PLATFORMS = [ Platform.BINARY_SENSOR, Platform.COVER, + Platform.EVENT, Platform.LIGHT, Platform.SENSOR, Platform.SIREN, diff --git a/homeassistant/components/rfxtrx/binary_sensor.py b/homeassistant/components/rfxtrx/binary_sensor.py index 03cf65a49ff..03c22167358 100644 --- a/homeassistant/components/rfxtrx/binary_sensor.py +++ b/homeassistant/components/rfxtrx/binary_sensor.py @@ -1,4 +1,5 @@ """Support for RFXtrx binary sensors.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/rfxtrx/config_flow.py b/homeassistant/components/rfxtrx/config_flow.py index 8f6ff45840c..837ca554615 100644 --- a/homeassistant/components/rfxtrx/config_flow.py +++ b/homeassistant/components/rfxtrx/config_flow.py @@ -1,4 +1,5 @@ """Config flow for RFXCOM RFXtrx integration.""" + from __future__ import annotations import asyncio @@ -13,8 +14,12 @@ import serial import serial.tools.list_ports import voluptuous as vol -from homeassistant import config_entries, data_entry_flow, exceptions -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.const import ( CONF_COMMAND_OFF, CONF_COMMAND_ON, @@ -26,6 +31,7 @@ from homeassistant.const import ( CONF_TYPE, ) from homeassistant.core import State, callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import ( config_validation as cv, device_registry as dr, @@ -68,13 +74,13 @@ class DeviceData(TypedDict): def none_or_int(value: str | None, base: int) -> int | None: - """Check if strin is one otherwise convert to int.""" + """Check if string is one otherwise convert to int.""" if value is None: return None return int(value, base) -class OptionsFlow(config_entries.OptionsFlow): +class RfxtrxOptionsFlow(OptionsFlow): """Handle Rfxtrx options.""" _device_registry: dr.DeviceRegistry @@ -91,13 +97,13 @@ class OptionsFlow(config_entries.OptionsFlow): async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> data_entry_flow.FlowResult: + ) -> ConfigFlowResult: """Manage the options.""" return await self.async_step_prompt_options() async def async_step_prompt_options( self, user_input: dict[str, Any] | None = None - ) -> data_entry_flow.FlowResult: + ) -> ConfigFlowResult: """Prompt for options.""" errors = {} @@ -171,7 +177,7 @@ class OptionsFlow(config_entries.OptionsFlow): async def async_step_set_device_options( self, user_input: dict[str, Any] | None = None - ) -> data_entry_flow.FlowResult: + ) -> ConfigFlowResult: """Manage device options.""" errors = {} assert self._selected_device_object @@ -489,14 +495,14 @@ class OptionsFlow(config_entries.OptionsFlow): ) -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class RfxtrxConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for RFXCOM RFXtrx.""" VERSION = 1 async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> data_entry_flow.FlowResult: + ) -> ConfigFlowResult: """Step when user initializes a integration.""" await self.async_set_unique_id(DOMAIN) self._abort_if_unique_id_configured() @@ -515,7 +521,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_setup_network( self, user_input: dict[str, Any] | None = None - ) -> data_entry_flow.FlowResult: + ) -> ConfigFlowResult: """Step when setting up network configuration.""" errors: dict[str, str] = {} @@ -542,7 +548,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_setup_serial( self, user_input: dict[str, Any] | None = None - ) -> data_entry_flow.FlowResult: + ) -> ConfigFlowResult: """Step when setting up serial configuration.""" errors: dict[str, str] = {} @@ -581,7 +587,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_setup_serial_manual_path( self, user_input: dict[str, Any] | None = None - ) -> data_entry_flow.FlowResult: + ) -> ConfigFlowResult: """Select path manually.""" errors: dict[str, str] = {} @@ -628,7 +634,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @callback def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow: """Get the options flow for this handler.""" - return OptionsFlow(config_entry) + return RfxtrxOptionsFlow(config_entry) def _test_transport(host: str | None, port: int | None, device: str | None) -> bool: @@ -658,5 +664,5 @@ def get_serial_by_id(dev_path: str) -> str: return dev_path -class CannotConnect(exceptions.HomeAssistantError): +class CannotConnect(HomeAssistantError): """Error to indicate we cannot connect.""" diff --git a/homeassistant/components/rfxtrx/cover.py b/homeassistant/components/rfxtrx/cover.py index 532e41ac50c..9e9e5a090e4 100644 --- a/homeassistant/components/rfxtrx/cover.py +++ b/homeassistant/components/rfxtrx/cover.py @@ -1,4 +1,5 @@ """Support for RFXtrx covers.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/rfxtrx/device_action.py b/homeassistant/components/rfxtrx/device_action.py index 15595b88cd2..65cf1a11911 100644 --- a/homeassistant/components/rfxtrx/device_action.py +++ b/homeassistant/components/rfxtrx/device_action.py @@ -1,4 +1,5 @@ """Provides device automations for RFXCOM RFXtrx.""" + from __future__ import annotations from collections.abc import Callable @@ -50,21 +51,17 @@ async def async_get_actions( except ValueError: return [] - actions = [] - for action_type in ACTION_TYPES: - if hasattr(device, action_type): - data: dict[int, str] = getattr(device, ACTION_SELECTION[action_type], {}) - for value in data.values(): - actions.append( - { - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_TYPE: action_type, - CONF_SUBTYPE: value, - } - ) - - return actions + return [ + { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_TYPE: action_type, + CONF_SUBTYPE: value, + } + for action_type in ACTION_TYPES + if hasattr(device, action_type) + for value in getattr(device, ACTION_SELECTION[action_type], {}).values() + ] def _get_commands( diff --git a/homeassistant/components/rfxtrx/device_trigger.py b/homeassistant/components/rfxtrx/device_trigger.py index a2f32395572..9e42cfa3919 100644 --- a/homeassistant/components/rfxtrx/device_trigger.py +++ b/homeassistant/components/rfxtrx/device_trigger.py @@ -1,4 +1,5 @@ """Provides device automations for RFXCOM RFXtrx.""" + from __future__ import annotations import voluptuous as vol @@ -50,20 +51,17 @@ async def async_get_triggers( """List device triggers for RFXCOM RFXtrx devices.""" device = async_get_device_object(hass, device_id) - triggers = [] - for conf_type in TRIGGER_TYPES: - data: dict[int, str] = getattr(device, TRIGGER_SELECTION[conf_type], {}) - for command in data.values(): - triggers.append( - { - CONF_PLATFORM: "device", - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_TYPE: conf_type, - CONF_SUBTYPE: command, - } - ) - return triggers + return [ + { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_TYPE: conf_type, + CONF_SUBTYPE: command, + } + for conf_type in TRIGGER_TYPES + for command in getattr(device, TRIGGER_SELECTION[conf_type], {}).values() + ] async def async_validate_trigger_config( diff --git a/homeassistant/components/rfxtrx/diagnostics.py b/homeassistant/components/rfxtrx/diagnostics.py index bc2fae2452d..d8bebfca2ae 100644 --- a/homeassistant/components/rfxtrx/diagnostics.py +++ b/homeassistant/components/rfxtrx/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for RFXCOM RFXtrx.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/rfxtrx/event.py b/homeassistant/components/rfxtrx/event.py new file mode 100644 index 00000000000..7e73919aacd --- /dev/null +++ b/homeassistant/components/rfxtrx/event.py @@ -0,0 +1,95 @@ +"""Support for RFXtrx sensors.""" + +from __future__ import annotations + +import logging +from typing import Any + +from RFXtrx import ControlEvent, RFXtrxDevice, RFXtrxEvent, SensorEvent + +from homeassistant.components.event import EventEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.util import slugify + +from . import DeviceTuple, RfxtrxEntity, async_setup_platform_entry + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up config entry.""" + + def _supported(event: RFXtrxEvent) -> bool: + return isinstance(event, (ControlEvent, SensorEvent)) + + def _constructor( + event: RFXtrxEvent, + auto: RFXtrxEvent | None, + device_id: DeviceTuple, + entity_info: dict[str, Any], + ) -> list[Entity]: + entities: list[Entity] = [] + + if hasattr(event.device, "COMMANDS"): + entities.append( + RfxtrxEventEntity( + event.device, device_id, "COMMANDS", "Command", "command" + ) + ) + + if hasattr(event.device, "STATUS"): + entities.append( + RfxtrxEventEntity( + event.device, device_id, "STATUS", "Sensor Status", "status" + ) + ) + + return entities + + await async_setup_platform_entry( + hass, config_entry, async_add_entities, _supported, _constructor + ) + + +class RfxtrxEventEntity(RfxtrxEntity, EventEntity): + """Representation of a RFXtrx event.""" + + def __init__( + self, + device: RFXtrxDevice, + device_id: DeviceTuple, + device_attribute: str, + value_attribute: str, + translation_key: str, + ) -> None: + """Initialize the sensor.""" + super().__init__(device, device_id) + commands: dict[int, str] = getattr(device, device_attribute) + self._attr_name = None + self._attr_unique_id = "_".join(x for x in device_id) + self._attr_event_types = [slugify(command) for command in commands.values()] + self._attr_translation_key = translation_key + self._value_attribute = value_attribute + + @callback + def _handle_event(self, event: RFXtrxEvent, device_id: DeviceTuple) -> None: + """Check if event applies to me and update.""" + if not self._event_applies(event, device_id): + return + + assert isinstance(event, (ControlEvent, SensorEvent)) + + event_type = slugify(event.values[self._value_attribute]) + if event_type not in self._attr_event_types: + _LOGGER.warning("Event type %s is not known", event_type) + return + + self._trigger_event(event_type, event.values) + self.async_write_ha_state() diff --git a/homeassistant/components/rfxtrx/helpers.py b/homeassistant/components/rfxtrx/helpers.py index cfc16126359..184d814a6cc 100644 --- a/homeassistant/components/rfxtrx/helpers.py +++ b/homeassistant/components/rfxtrx/helpers.py @@ -1,6 +1,5 @@ """Provides helpers for RFXtrx.""" - from RFXtrx import RFXtrxDevice, get_device from homeassistant.core import HomeAssistant, callback diff --git a/homeassistant/components/rfxtrx/icons.json b/homeassistant/components/rfxtrx/icons.json new file mode 100644 index 00000000000..c1b8e741e45 --- /dev/null +++ b/homeassistant/components/rfxtrx/icons.json @@ -0,0 +1,5 @@ +{ + "services": { + "send": "mdi:send" + } +} diff --git a/homeassistant/components/rfxtrx/light.py b/homeassistant/components/rfxtrx/light.py index ad84515d41d..f9bbbc28a8d 100644 --- a/homeassistant/components/rfxtrx/light.py +++ b/homeassistant/components/rfxtrx/light.py @@ -1,4 +1,5 @@ """Support for RFXtrx lights.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/rfxtrx/sensor.py b/homeassistant/components/rfxtrx/sensor.py index 66803edffc5..f421b6da7ef 100644 --- a/homeassistant/components/rfxtrx/sensor.py +++ b/homeassistant/components/rfxtrx/sensor.py @@ -1,4 +1,5 @@ """Support for RFXtrx sensors.""" + from __future__ import annotations from collections.abc import Callable @@ -254,18 +255,15 @@ async def async_setup_entry( device_id: DeviceTuple, entity_info: dict[str, Any], ) -> list[Entity]: - entities: list[Entity] = [] - for data_type in set(event.values) & set(SENSOR_TYPES_DICT): - entities.append( - RfxtrxSensor( - event.device, - device_id, - SENSOR_TYPES_DICT[data_type], - event=event if auto else None, - ) + return [ + RfxtrxSensor( + event.device, + device_id, + SENSOR_TYPES_DICT[data_type], + event=event if auto else None, ) - - return entities + for data_type in set(event.values) & set(SENSOR_TYPES_DICT) + ] await async_setup_platform_entry( hass, config_entry, async_add_entities, _supported, _constructor diff --git a/homeassistant/components/rfxtrx/siren.py b/homeassistant/components/rfxtrx/siren.py index bfff08d5ea6..67a0c6b7dce 100644 --- a/homeassistant/components/rfxtrx/siren.py +++ b/homeassistant/components/rfxtrx/siren.py @@ -1,4 +1,5 @@ """Support for RFXtrx sirens.""" + from __future__ import annotations from datetime import datetime diff --git a/homeassistant/components/rfxtrx/strings.json b/homeassistant/components/rfxtrx/strings.json index 9b99553d3f0..aeb4b2395d3 100644 --- a/homeassistant/components/rfxtrx/strings.json +++ b/homeassistant/components/rfxtrx/strings.json @@ -83,6 +83,94 @@ } }, "entity": { + "event": { + "command": { + "state_attributes": { + "event_type": { + "state": { + "sound_0": "Sound 0", + "sound_1": "Sound 1", + "sound_2": "Sound 2", + "sound_3": "Sound 3", + "sound_4": "Sound 4", + "sound_5": "Sound 5", + "sound_6": "Sound 6", + "sound_7": "Sound 7", + "sound_8": "Sound 8", + "sound_9": "Sound 9", + "sound_10": "Sound 10", + "sound_11": "Sound 11", + "sound_12": "Sound 12", + "sound_13": "Sound 13", + "sound_14": "Sound 14", + "sound_15": "Sound 15", + "down": "Down", + "up": "Up", + "all_off": "All Off", + "all_on": "All On", + "scene": "Scene", + "off": "Off", + "on": "On", + "dim": "Dim", + "bright": "Bright", + "all_group_off": "All/group Off", + "all_group_on": "All/group On", + "chime": "Chime", + "illegal_command": "Illegal command", + "set_level": "Set level", + "group_off": "Group off", + "group_on": "Group on", + "set_group_level": "Set group level", + "level_1": "Level 1", + "level_2": "Level 2", + "level_3": "Level 3", + "level_4": "Level 4", + "level_5": "Level 5", + "level_6": "Level 6", + "level_7": "Level 7", + "level_8": "Level 8", + "level_9": "Level 9", + "program": "Program", + "stop": "Stop", + "0_5_seconds_up": "0.5 Seconds Up", + "0_5_seconds_down": "0.5 Seconds Down", + "2_seconds_up": "2 Seconds Up", + "2_seconds_down": "2 Seconds Down", + "enable_sun_automation": "Enable sun automation", + "disable_sun_automation": "Disable sun automation", + "normal": "Normal", + "normal_delayed": "Normal Delayed", + "alarm": "Alarm", + "alarm_delayed": "Alarm Delayed", + "motion": "Motion", + "no_motion": "No Motion", + "panic": "Panic", + "end_panic": "End Panic", + "ir": "IR", + "arm_away": "Arm Away", + "arm_away_delayed": "Arm Away Delayed", + "arm_home": "Arm Home", + "arm_home_delayed": "Arm Home Delayed", + "disarm": "Disarm", + "light_1_off": "Light 1 Off", + "light_1_on": "Light 1 On", + "light_2_off": "Light 2 Off", + "light_2_on": "Light 2 On", + "dark_detected": "Dark Detected", + "light_detected": "Light Detected", + "battery_low": "Battery low", + "pairing_kd101": "Pairing KD101", + "normal_tamper": "Normal Tamper", + "normal_delayed_tamper": "Normal Delayed Tamper", + "alarm_tamper": "Alarm Tamper", + "alarm_delayed_tamper": "Alarm Delayed Tamper", + "motion_tamper": "Motion Tamper", + "no_motion_tamper": "No Motion Tamper" + } + } + } + } + }, "sensor": { "current_ch_1": { "name": "Current Ch. 1" diff --git a/homeassistant/components/rfxtrx/switch.py b/homeassistant/components/rfxtrx/switch.py index edc34aeb80d..fad395f41c2 100644 --- a/homeassistant/components/rfxtrx/switch.py +++ b/homeassistant/components/rfxtrx/switch.py @@ -1,4 +1,5 @@ """Support for RFXtrx switches.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/rhasspy/__init__.py b/homeassistant/components/rhasspy/__init__.py index 669d81952d4..d673aace40b 100644 --- a/homeassistant/components/rhasspy/__init__.py +++ b/homeassistant/components/rhasspy/__init__.py @@ -1,4 +1,5 @@ """The Rhasspy integration.""" + from __future__ import annotations from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/rhasspy/config_flow.py b/homeassistant/components/rhasspy/config_flow.py index 69ed802f817..114d74d4d05 100644 --- a/homeassistant/components/rhasspy/config_flow.py +++ b/homeassistant/components/rhasspy/config_flow.py @@ -1,24 +1,24 @@ """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 homeassistant.config_entries import ConfigFlow, ConfigFlowResult from .const import DOMAIN -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class RhasspyConfigFlow(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: + ) -> ConfigFlowResult: """Handle the initial step.""" if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") diff --git a/homeassistant/components/ridwell/__init__.py b/homeassistant/components/ridwell/__init__.py index 53575e79c45..cf584207091 100644 --- a/homeassistant/components/ridwell/__init__.py +++ b/homeassistant/components/ridwell/__init__.py @@ -1,4 +1,5 @@ """The Ridwell integration.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/ridwell/calendar.py b/homeassistant/components/ridwell/calendar.py index 3ef3bbdc5ae..ecca0366754 100644 --- a/homeassistant/components/ridwell/calendar.py +++ b/homeassistant/components/ridwell/calendar.py @@ -1,4 +1,5 @@ """Support for Ridwell calendars.""" + from __future__ import annotations import datetime @@ -49,8 +50,8 @@ async def async_setup_entry( class RidwellCalendar(RidwellEntity, CalendarEntity): """Define a Ridwell calendar.""" - _attr_icon = "mdi:delete-empty" _attr_name = None + _attr_translation_key = "calendar" def __init__( self, coordinator: RidwellDataUpdateCoordinator, account: RidwellAccount diff --git a/homeassistant/components/ridwell/config_flow.py b/homeassistant/components/ridwell/config_flow.py index 722c20336d4..a54d4debe75 100644 --- a/homeassistant/components/ridwell/config_flow.py +++ b/homeassistant/components/ridwell/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Ridwell integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -8,9 +9,8 @@ from aioridwell import async_get_client from aioridwell.errors import InvalidCredentialsError, RidwellError import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import aiohttp_client, config_validation as cv from .const import DOMAIN, LOGGER @@ -29,8 +29,8 @@ STEP_USER_DATA_SCHEMA = vol.Schema( ) -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): - """Handle a config flow for WattTime.""" +class RidwellConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for Ridwell.""" VERSION = 2 @@ -41,7 +41,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def _async_validate( self, error_step_id: str, error_schema: vol.Schema - ) -> FlowResult: + ) -> ConfigFlowResult: """Validate input credentials and proceed accordingly.""" errors = {} session = aiohttp_client.async_get_clientsession(self.hass) @@ -81,14 +81,16 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): data={CONF_USERNAME: self._username, CONF_PASSWORD: self._password}, ) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Handle configuration by re-auth.""" self._username = entry_data[CONF_USERNAME] return await self.async_step_reauth_confirm() async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle re-auth completion.""" if not user_input: return self.async_show_form( @@ -105,7 +107,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" if not user_input: return self.async_show_form( diff --git a/homeassistant/components/ridwell/const.py b/homeassistant/components/ridwell/const.py index 69c5ead5277..75ceb2dbbd6 100644 --- a/homeassistant/components/ridwell/const.py +++ b/homeassistant/components/ridwell/const.py @@ -1,4 +1,5 @@ """Constants for the Ridwell integration.""" + import logging DOMAIN = "ridwell" diff --git a/homeassistant/components/ridwell/coordinator.py b/homeassistant/components/ridwell/coordinator.py index 9561cd26e4b..28190522c76 100644 --- a/homeassistant/components/ridwell/coordinator.py +++ b/homeassistant/components/ridwell/coordinator.py @@ -1,4 +1,5 @@ """Define a Ridwell coordinator.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/ridwell/diagnostics.py b/homeassistant/components/ridwell/diagnostics.py index f48861cee19..0eff7583311 100644 --- a/homeassistant/components/ridwell/diagnostics.py +++ b/homeassistant/components/ridwell/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for Ridwell.""" + from __future__ import annotations import dataclasses diff --git a/homeassistant/components/ridwell/entity.py b/homeassistant/components/ridwell/entity.py index 095ecc3c5c6..d8323f7aef6 100644 --- a/homeassistant/components/ridwell/entity.py +++ b/homeassistant/components/ridwell/entity.py @@ -1,4 +1,5 @@ """Define a base Ridwell entity.""" + from __future__ import annotations from datetime import date diff --git a/homeassistant/components/ridwell/icons.json b/homeassistant/components/ridwell/icons.json new file mode 100644 index 00000000000..dd9680ae687 --- /dev/null +++ b/homeassistant/components/ridwell/icons.json @@ -0,0 +1,14 @@ +{ + "entity": { + "calendar": { + "calendar": { + "default": "mdi:delete-empty" + } + }, + "switch": { + "opt_in": { + "default": "mdi:calendar-check" + } + } + } +} diff --git a/homeassistant/components/ridwell/sensor.py b/homeassistant/components/ridwell/sensor.py index e4626831d7d..7fc7fdb5348 100644 --- a/homeassistant/components/ridwell/sensor.py +++ b/homeassistant/components/ridwell/sensor.py @@ -1,4 +1,5 @@ """Support for Ridwell sensors.""" + from __future__ import annotations from collections.abc import Mapping diff --git a/homeassistant/components/ridwell/switch.py b/homeassistant/components/ridwell/switch.py index f47fc1ca0af..04e3e4c5ff9 100644 --- a/homeassistant/components/ridwell/switch.py +++ b/homeassistant/components/ridwell/switch.py @@ -1,4 +1,5 @@ """Support for Ridwell buttons.""" + from __future__ import annotations from typing import Any @@ -19,7 +20,6 @@ from .entity import RidwellEntity SWITCH_DESCRIPTION = SwitchEntityDescription( key="opt_in", translation_key="opt_in", - icon="mdi:calendar-check", ) diff --git a/homeassistant/components/ring/__init__.py b/homeassistant/components/ring/__init__.py index 26fdc6d0575..e3697d4fccc 100644 --- a/homeassistant/components/ring/__init__.py +++ b/homeassistant/components/ring/__init__.py @@ -1,15 +1,17 @@ """Support for Ring Doorbell/Chimes.""" + from __future__ import annotations from functools import partial import logging -import ring_doorbell +from ring_doorbell import Auth, Ring from homeassistant.config_entries import ConfigEntry from homeassistant.const import APPLICATION_NAME, CONF_TOKEN, __version__ from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from .const import ( DOMAIN, @@ -37,10 +39,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) ) - auth = ring_doorbell.Auth( + auth = Auth( f"{APPLICATION_NAME}/{__version__}", entry.data[CONF_TOKEN], token_updater ) - ring = ring_doorbell.Ring(auth) + ring = Ring(auth) devices_coordinator = RingDataCoordinator(hass, ring) notifications_coordinator = RingNotificationsCoordinator(hass, ring) @@ -61,6 +63,23 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_refresh_all(_: ServiceCall) -> None: """Refresh all ring data.""" + _LOGGER.warning( + "Detected use of service 'ring.update'. " + "This is deprecated and will stop working in Home Assistant 2024.10. " + "Use 'homeassistant.update_entity' instead which updates all ring entities", + ) + async_create_issue( + hass, + DOMAIN, + "deprecated_service_ring_update", + breaks_in_ha_version="2024.10.0", + is_fixable=True, + is_persistent=False, + issue_domain=DOMAIN, + severity=IssueSeverity.WARNING, + translation_key="deprecated_service_ring_update", + ) + for info in hass.data[DOMAIN].values(): await info[RING_DEVICES_COORDINATOR].async_refresh() await info[RING_NOTIFICATIONS_COORDINATOR].async_refresh() diff --git a/homeassistant/components/ring/binary_sensor.py b/homeassistant/components/ring/binary_sensor.py index a7e04f4cfb9..19daebf9ce1 100644 --- a/homeassistant/components/ring/binary_sensor.py +++ b/homeassistant/components/ring/binary_sensor.py @@ -1,4 +1,5 @@ """Component providing HA sensor support for Ring Door Bell/Chimes.""" + from __future__ import annotations from dataclasses import dataclass @@ -19,25 +20,18 @@ from .coordinator import RingNotificationsCoordinator from .entity import RingEntity -@dataclass(frozen=True) -class RingRequiredKeysMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class RingBinarySensorEntityDescription(BinarySensorEntityDescription): + """Describes Ring binary sensor entity.""" category: list[str] -@dataclass(frozen=True) -class RingBinarySensorEntityDescription( - BinarySensorEntityDescription, RingRequiredKeysMixin -): - """Describes Ring binary sensor entity.""" - - BINARY_SENSOR_TYPES: tuple[RingBinarySensorEntityDescription, ...] = ( RingBinarySensorEntityDescription( key="ding", translation_key="ding", - category=["doorbots", "authorized_doorbots"], + category=["doorbots", "authorized_doorbots", "other"], device_class=BinarySensorDeviceClass.OCCUPANCY, ), RingBinarySensorEntityDescription( @@ -62,7 +56,7 @@ async def async_setup_entry( entities = [ RingBinarySensor(ring, device, notifications_coordinator, description) - for device_type in ("doorbots", "authorized_doorbots", "stickup_cams") + for device_type in ("doorbots", "authorized_doorbots", "stickup_cams", "other") for description in BINARY_SENSOR_TYPES if device_type in description.category for device in devices[device_type] diff --git a/homeassistant/components/ring/button.py b/homeassistant/components/ring/button.py new file mode 100644 index 00000000000..343c0d68257 --- /dev/null +++ b/homeassistant/components/ring/button.py @@ -0,0 +1,57 @@ +"""Component providing support for Ring buttons.""" + +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_platform import AddEntitiesCallback + +from .const import DOMAIN, RING_DEVICES, RING_DEVICES_COORDINATOR +from .coordinator import RingDataCoordinator +from .entity import RingEntity, exception_wrap + +BUTTON_DESCRIPTION = ButtonEntityDescription( + key="open_door", translation_key="open_door" +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Create the buttons for the Ring devices.""" + devices = hass.data[DOMAIN][config_entry.entry_id][RING_DEVICES] + devices_coordinator: RingDataCoordinator = hass.data[DOMAIN][config_entry.entry_id][ + RING_DEVICES_COORDINATOR + ] + + async_add_entities( + RingDoorButton(device, devices_coordinator, BUTTON_DESCRIPTION) + for device in devices["other"] + if device.has_capability("open") + ) + + +class RingDoorButton(RingEntity, ButtonEntity): + """Creates a button to open the ring intercom door.""" + + def __init__( + self, + device, + coordinator, + description: ButtonEntityDescription, + ) -> None: + """Initialize the button.""" + super().__init__( + device, + coordinator, + ) + self.entity_description = description + self._attr_unique_id = f"{device.id}-{description.key}" + + @exception_wrap + def press(self) -> None: + """Open the door.""" + self._device.open_door() diff --git a/homeassistant/components/ring/camera.py b/homeassistant/components/ring/camera.py index 265d7102b91..7cbe3559ab2 100644 --- a/homeassistant/components/ring/camera.py +++ b/homeassistant/components/ring/camera.py @@ -1,4 +1,5 @@ """Component providing support to the Ring Door Bell camera.""" + from __future__ import annotations from datetime import timedelta @@ -7,7 +8,6 @@ import logging from typing import Optional from haffmpeg.camera import CameraMjpeg -import requests from homeassistant.components import ffmpeg from homeassistant.components.camera import Camera @@ -19,9 +19,10 @@ from homeassistant.util import dt as dt_util from .const import DOMAIN, RING_DEVICES, RING_DEVICES_COORDINATOR from .coordinator import RingDataCoordinator -from .entity import RingEntity +from .entity import RingEntity, exception_wrap FORCE_REFRESH_INTERVAL = timedelta(minutes=3) +MOTION_DETECTION_CAPABILITY = "motion_detection" _LOGGER = logging.getLogger(__name__) @@ -67,6 +68,8 @@ class RingCam(RingEntity, Camera): self._image = None self._expires_at = dt_util.utcnow() - FORCE_REFRESH_INTERVAL self._attr_unique_id = device.id + if device.has_capability(MOTION_DETECTION_CAPABILITY): + self._attr_motion_detection_enabled = device.motion_detection @callback def _handle_coordinator_update(self): @@ -131,6 +134,13 @@ class RingCam(RingEntity, Camera): async def async_update(self) -> None: """Update camera entity and refresh attributes.""" + if ( + self._device.has_capability(MOTION_DETECTION_CAPABILITY) + and self._attr_motion_detection_enabled != self._device.motion_detection + ): + self._attr_motion_detection_enabled = self._device.motion_detection + self.async_write_ha_state() + if self._last_event is None: return @@ -144,17 +154,31 @@ class RingCam(RingEntity, Camera): if self._last_video_id != self._last_event["id"]: self._image = None - try: - video_url = await self.hass.async_add_executor_job( - self._device.recording_url, self._last_event["id"] - ) - except requests.Timeout: - _LOGGER.warning( - "Time out fetching recording url for camera %s", self.entity_id - ) - video_url = None + self._video_url = await self.hass.async_add_executor_job(self._get_video) - if video_url: - self._last_video_id = self._last_event["id"] - self._video_url = video_url - self._expires_at = FORCE_REFRESH_INTERVAL + utcnow + self._last_video_id = self._last_event["id"] + self._expires_at = FORCE_REFRESH_INTERVAL + utcnow + + @exception_wrap + def _get_video(self) -> str: + return self._device.recording_url(self._last_event["id"]) + + @exception_wrap + def _set_motion_detection_enabled(self, new_state): + if not self._device.has_capability(MOTION_DETECTION_CAPABILITY): + _LOGGER.error( + "Entity %s does not have motion detection capability", self.entity_id + ) + return + + self._device.motion_detection = new_state + self._attr_motion_detection_enabled = new_state + self.schedule_update_ha_state(False) + + def enable_motion_detection(self) -> None: + """Enable motion detection in the camera.""" + self._set_motion_detection_enabled(True) + + def disable_motion_detection(self) -> None: + """Disable motion detection in camera.""" + self._set_motion_detection_enabled(False) diff --git a/homeassistant/components/ring/config_flow.py b/homeassistant/components/ring/config_flow.py index 5c735a3ee8c..6d4f28eb311 100644 --- a/homeassistant/components/ring/config_flow.py +++ b/homeassistant/components/ring/config_flow.py @@ -1,13 +1,13 @@ """Config flow for Ring integration.""" + from collections.abc import Mapping import logging from typing import Any -import ring_doorbell +from ring_doorbell import Auth, AuthenticationError, Requires2FAError import voluptuous as vol -from homeassistant import config_entries, core, exceptions -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.const import ( APPLICATION_NAME, CONF_PASSWORD, @@ -15,7 +15,8 @@ from homeassistant.const import ( CONF_USERNAME, __version__ as ha_version, ) -from homeassistant.data_entry_flow import FlowResult +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from .const import CONF_2FA, DOMAIN @@ -27,10 +28,10 @@ STEP_USER_DATA_SCHEMA = vol.Schema( STEP_REAUTH_DATA_SCHEMA = vol.Schema({vol.Required(CONF_PASSWORD): str}) -async def validate_input(hass: core.HomeAssistant, data): +async def validate_input(hass: HomeAssistant, data): """Validate the user input allows us to connect.""" - auth = ring_doorbell.Auth(f"{APPLICATION_NAME}/{ha_version}") + auth = Auth(f"{APPLICATION_NAME}/{ha_version}") try: token = await hass.async_add_executor_job( @@ -39,15 +40,15 @@ async def validate_input(hass: core.HomeAssistant, data): data[CONF_PASSWORD], data.get(CONF_2FA), ) - except ring_doorbell.Requires2FAError as err: + except Requires2FAError as err: raise Require2FA from err - except ring_doorbell.AuthenticationError as err: + except AuthenticationError as err: raise InvalidAuth from err return token -class RingConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class RingConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Ring.""" VERSION = 1 @@ -96,7 +97,9 @@ class RingConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): data_schema=vol.Schema({vol.Required(CONF_2FA): str}), ) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Handle reauth upon an API authentication error.""" self.reauth_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] @@ -105,7 +108,7 @@ class RingConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Dialog that informs the user that reauth is required.""" errors = {} assert self.reauth_entry is not None @@ -143,9 +146,9 @@ class RingConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -class Require2FA(exceptions.HomeAssistantError): +class Require2FA(HomeAssistantError): """Error to indicate we require 2FA.""" -class InvalidAuth(exceptions.HomeAssistantError): +class InvalidAuth(HomeAssistantError): """Error to indicate there is invalid auth.""" diff --git a/homeassistant/components/ring/const.py b/homeassistant/components/ring/const.py index f0e0c63d778..23f378a38be 100644 --- a/homeassistant/components/ring/const.py +++ b/homeassistant/components/ring/const.py @@ -1,4 +1,5 @@ """The Ring constants.""" + from __future__ import annotations from datetime import timedelta @@ -15,6 +16,7 @@ DEFAULT_ENTITY_NAMESPACE = "ring" PLATFORMS = [ Platform.BINARY_SENSOR, + Platform.BUTTON, Platform.CAMERA, Platform.LIGHT, Platform.SENSOR, diff --git a/homeassistant/components/ring/coordinator.py b/homeassistant/components/ring/coordinator.py index 943b1c628bf..fdb6fc1f296 100644 --- a/homeassistant/components/ring/coordinator.py +++ b/homeassistant/components/ring/coordinator.py @@ -1,12 +1,12 @@ """Data coordinators for the ring integration.""" + from asyncio import TaskGroup from collections.abc import Callable from dataclasses import dataclass import logging from typing import Any, Optional -import ring_doorbell -from ring_doorbell.generic import RingGeneric +from ring_doorbell import AuthenticationError, Ring, RingError, RingGeneric, RingTimeout from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed @@ -22,15 +22,15 @@ async def _call_api( ): try: return await hass.async_add_executor_job(target, *args) - except ring_doorbell.AuthenticationError as err: + except AuthenticationError as err: # Raising ConfigEntryAuthFailed will cancel future updates # and start a config flow with SOURCE_REAUTH (async_step_reauth) raise ConfigEntryAuthFailed from err - except ring_doorbell.RingTimeout as err: + except RingTimeout as err: raise UpdateFailed( f"Timeout communicating with API{msg_suffix}: {err}" ) from err - except ring_doorbell.RingError as err: + except RingError as err: raise UpdateFailed(f"Error communicating with API{msg_suffix}: {err}") from err @@ -48,7 +48,7 @@ class RingDataCoordinator(DataUpdateCoordinator[dict[int, RingDeviceData]]): def __init__( self, hass: HomeAssistant, - ring_api: ring_doorbell.Ring, + ring_api: Ring, ) -> None: """Initialize my coordinator.""" super().__init__( @@ -57,7 +57,7 @@ class RingDataCoordinator(DataUpdateCoordinator[dict[int, RingDeviceData]]): logger=_LOGGER, update_interval=SCAN_INTERVAL, ) - self.ring_api: ring_doorbell.Ring = ring_api + self.ring_api: Ring = ring_api self.first_call: bool = True async def _async_update_data(self): @@ -104,7 +104,7 @@ class RingDataCoordinator(DataUpdateCoordinator[dict[int, RingDeviceData]]): class RingNotificationsCoordinator(DataUpdateCoordinator[None]): """Global notifications coordinator.""" - def __init__(self, hass: HomeAssistant, ring_api: ring_doorbell.Ring) -> None: + def __init__(self, hass: HomeAssistant, ring_api: Ring) -> None: """Initialize my coordinator.""" super().__init__( hass, @@ -112,7 +112,7 @@ class RingNotificationsCoordinator(DataUpdateCoordinator[None]): name="active dings", update_interval=NOTIFICATIONS_SCAN_INTERVAL, ) - self.ring_api: ring_doorbell.Ring = ring_api + self.ring_api: Ring = ring_api async def _async_update_data(self): """Fetch data from API endpoint.""" diff --git a/homeassistant/components/ring/diagnostics.py b/homeassistant/components/ring/diagnostics.py index 105800f8d13..5295629979a 100644 --- a/homeassistant/components/ring/diagnostics.py +++ b/homeassistant/components/ring/diagnostics.py @@ -1,9 +1,10 @@ """Diagnostics support for Ring.""" + from __future__ import annotations from typing import Any -import ring_doorbell +from ring_doorbell import Ring from homeassistant.components.diagnostics import async_redact_data from homeassistant.config_entries import ConfigEntry @@ -32,11 +33,12 @@ async def async_get_config_entry_diagnostics( hass: HomeAssistant, entry: ConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - ring: ring_doorbell.Ring = hass.data[DOMAIN][entry.entry_id]["api"] - devices_raw = [] - for device_type in ring.devices_data: - for device_id in ring.devices_data[device_type]: - devices_raw.append(ring.devices_data[device_type][device_id]) + ring: Ring = hass.data[DOMAIN][entry.entry_id]["api"] + devices_raw = [ + ring.devices_data[device_type][device_id] + for device_type in ring.devices_data + for device_id in ring.devices_data[device_type] + ] return async_redact_data( {"device_data": devices_raw}, TO_REDACT, diff --git a/homeassistant/components/ring/entity.py b/homeassistant/components/ring/entity.py index 78f0c8e468e..fb617ecd7d1 100644 --- a/homeassistant/components/ring/entity.py +++ b/homeassistant/components/ring/entity.py @@ -1,9 +1,12 @@ """Base class for Ring entity.""" -from typing import TypeVar -from ring_doorbell.generic import RingGeneric +from collections.abc import Callable +from typing import Any, Concatenate, ParamSpec, TypeVar + +from ring_doorbell import AuthenticationError, RingError, RingGeneric, RingTimeout from homeassistant.core import callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -18,6 +21,33 @@ _RingCoordinatorT = TypeVar( "_RingCoordinatorT", bound=(RingDataCoordinator | RingNotificationsCoordinator), ) +_T = TypeVar("_T", bound="RingEntity") +_P = ParamSpec("_P") + + +def exception_wrap( + func: Callable[Concatenate[_T, _P], Any], +) -> Callable[Concatenate[_T, _P], Any]: + """Define a wrapper to catch exceptions and raise HomeAssistant errors.""" + + def _wrap(self: _T, *args: _P.args, **kwargs: _P.kwargs) -> None: + try: + return func(self, *args, **kwargs) + except AuthenticationError as err: + self.hass.loop.call_soon_threadsafe( + self.coordinator.config_entry.async_start_reauth, self.hass + ) + raise HomeAssistantError(err) from err + except RingTimeout as err: + raise HomeAssistantError( + f"Timeout communicating with API {func}: {err}" + ) from err + except RingError as err: + raise HomeAssistantError( + f"Error communicating with API{func}: {err}" + ) from err + + return _wrap class RingEntity(CoordinatorEntity[_RingCoordinatorT]): diff --git a/homeassistant/components/ring/icons.json b/homeassistant/components/ring/icons.json new file mode 100644 index 00000000000..9dd31fd0fd1 --- /dev/null +++ b/homeassistant/components/ring/icons.json @@ -0,0 +1,44 @@ +{ + "entity": { + "sensor": { + "last_activity": { + "default": "mdi:history" + }, + "last_ding": { + "default": "mdi:history" + }, + "last_motion": { + "default": "mdi:history" + }, + "volume": { + "default": "mdi:bell-ring" + }, + "doorbell_volume": { + "default": "mdi:bell-ring" + }, + "mic_volume": { + "default": "mdi:microphone" + }, + "voice_volume": { + "default": "mdi:account-voice" + }, + "open_door": { + "default": "mdi:door-closed-lock" + }, + "wifi_signal_category": { + "default": "mdi:wifi" + }, + "wifi_signal_strength": { + "default": "mdi:wifi" + } + }, + "switch": { + "siren": { + "default": "mdi:alarm-bell" + } + } + }, + "services": { + "update": "mdi:refresh" + } +} diff --git a/homeassistant/components/ring/light.py b/homeassistant/components/ring/light.py index 73ec8349384..31e22c2084c 100644 --- a/homeassistant/components/ring/light.py +++ b/homeassistant/components/ring/light.py @@ -1,11 +1,10 @@ """Component providing HA switch support for Ring Door Bell/Chimes.""" + from datetime import timedelta import logging from typing import Any -import requests -from ring_doorbell import RingStickUpCam -from ring_doorbell.generic import RingGeneric +from ring_doorbell import RingGeneric, RingStickUpCam from homeassistant.components.light import ColorMode, LightEntity from homeassistant.config_entries import ConfigEntry @@ -15,7 +14,7 @@ import homeassistant.util.dt as dt_util from .const import DOMAIN, RING_DEVICES, RING_DEVICES_COORDINATOR from .coordinator import RingDataCoordinator -from .entity import RingEntity +from .entity import RingEntity, exception_wrap _LOGGER = logging.getLogger(__name__) @@ -41,13 +40,12 @@ async def async_setup_entry( devices_coordinator: RingDataCoordinator = hass.data[DOMAIN][config_entry.entry_id][ RING_DEVICES_COORDINATOR ] - lights = [] - for device in devices["stickup_cams"]: - if device.has_capability("light"): - lights.append(RingLight(device, devices_coordinator)) - - async_add_entities(lights) + async_add_entities( + RingLight(device, devices_coordinator) + for device in devices["stickup_cams"] + if device.has_capability("light") + ) class RingLight(RingEntity, LightEntity): @@ -75,13 +73,10 @@ class RingLight(RingEntity, LightEntity): self._attr_is_on = device.lights == ON_STATE super()._handle_coordinator_update() + @exception_wrap def _set_light(self, new_state): """Update light state, and causes Home Assistant to correctly update.""" - try: - self._device.lights = new_state - except requests.Timeout: - _LOGGER.error("Time out setting %s light to %s", self.entity_id, new_state) - return + self._device.lights = new_state self._attr_is_on = new_state == ON_STATE self._no_updates_until = dt_util.utcnow() + SKIP_UPDATES_DELAY diff --git a/homeassistant/components/ring/manifest.json b/homeassistant/components/ring/manifest.json index 0390db640e5..67e2cfcdc78 100644 --- a/homeassistant/components/ring/manifest.json +++ b/homeassistant/components/ring/manifest.json @@ -13,5 +13,5 @@ "documentation": "https://www.home-assistant.io/integrations/ring", "iot_class": "cloud_polling", "loggers": ["ring_doorbell"], - "requirements": ["ring-doorbell[listen]==0.8.7"] + "requirements": ["ring-doorbell[listen]==0.8.9"] } diff --git a/homeassistant/components/ring/sensor.py b/homeassistant/components/ring/sensor.py index 32382a2f929..9ba677e7e5b 100644 --- a/homeassistant/components/ring/sensor.py +++ b/homeassistant/components/ring/sensor.py @@ -1,10 +1,11 @@ """Component providing HA sensor support for Ring Door Bell/Chimes.""" + from __future__ import annotations from dataclasses import dataclass from typing import Any -from ring_doorbell.generic import RingGeneric +from ring_doorbell import RingGeneric from homeassistant.components.sensor import ( SensorDeviceClass, @@ -39,7 +40,13 @@ async def async_setup_entry( entities = [ description.cls(device, devices_coordinator, description) - for device_type in ("chimes", "doorbots", "authorized_doorbots", "stickup_cams") + for device_type in ( + "chimes", + "doorbots", + "authorized_doorbots", + "stickup_cams", + "other", + ) for description in SENSOR_TYPES if device_type in description.category for device in devices[device_type] @@ -71,6 +78,12 @@ class RingSensor(RingEntity, SensorEntity): sensor_type = self.entity_description.key if sensor_type == "volume": return self._device.volume + if sensor_type == "doorbell_volume": + return self._device.doorbell_volume + if sensor_type == "mic_volume": + return self._device.mic_volume + if sensor_type == "voice_volume": + return self._device.voice_volume if sensor_type == "battery": return self._device.battery_life @@ -142,25 +155,20 @@ class HistoryRingSensor(RingSensor): return attrs -@dataclass(frozen=True) -class RingRequiredKeysMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class RingSensorEntityDescription(SensorEntityDescription): + """Describes Ring sensor entity.""" category: list[str] cls: type[RingSensor] - -@dataclass(frozen=True) -class RingSensorEntityDescription(SensorEntityDescription, RingRequiredKeysMixin): - """Describes Ring sensor entity.""" - kind: str | None = None SENSOR_TYPES: tuple[RingSensorEntityDescription, ...] = ( RingSensorEntityDescription( key="battery", - category=["doorbots", "authorized_doorbots", "stickup_cams"], + category=["doorbots", "authorized_doorbots", "stickup_cams", "other"], native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.BATTERY, state_class=SensorStateClass.MEASUREMENT, @@ -170,16 +178,14 @@ SENSOR_TYPES: tuple[RingSensorEntityDescription, ...] = ( RingSensorEntityDescription( key="last_activity", translation_key="last_activity", - category=["doorbots", "authorized_doorbots", "stickup_cams"], - icon="mdi:history", + category=["doorbots", "authorized_doorbots", "stickup_cams", "other"], device_class=SensorDeviceClass.TIMESTAMP, cls=HistoryRingSensor, ), RingSensorEntityDescription( key="last_ding", translation_key="last_ding", - category=["doorbots", "authorized_doorbots"], - icon="mdi:history", + category=["doorbots", "authorized_doorbots", "other"], kind="ding", device_class=SensorDeviceClass.TIMESTAMP, cls=HistoryRingSensor, @@ -188,7 +194,6 @@ SENSOR_TYPES: tuple[RingSensorEntityDescription, ...] = ( key="last_motion", translation_key="last_motion", category=["doorbots", "authorized_doorbots", "stickup_cams"], - icon="mdi:history", kind="motion", device_class=SensorDeviceClass.TIMESTAMP, cls=HistoryRingSensor, @@ -197,23 +202,38 @@ SENSOR_TYPES: tuple[RingSensorEntityDescription, ...] = ( key="volume", translation_key="volume", category=["chimes", "doorbots", "authorized_doorbots", "stickup_cams"], - icon="mdi:bell-ring", + cls=RingSensor, + ), + RingSensorEntityDescription( + key="doorbell_volume", + translation_key="doorbell_volume", + category=["other"], + cls=RingSensor, + ), + RingSensorEntityDescription( + key="mic_volume", + translation_key="mic_volume", + category=["other"], + cls=RingSensor, + ), + RingSensorEntityDescription( + key="voice_volume", + translation_key="voice_volume", + category=["other"], cls=RingSensor, ), RingSensorEntityDescription( key="wifi_signal_category", translation_key="wifi_signal_category", - category=["chimes", "doorbots", "authorized_doorbots", "stickup_cams"], - icon="mdi:wifi", + category=["chimes", "doorbots", "authorized_doorbots", "stickup_cams", "other"], entity_category=EntityCategory.DIAGNOSTIC, cls=HealthDataRingSensor, ), RingSensorEntityDescription( key="wifi_signal_strength", translation_key="wifi_signal_strength", - category=["chimes", "doorbots", "authorized_doorbots", "stickup_cams"], + category=["chimes", "doorbots", "authorized_doorbots", "stickup_cams", "other"], native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, - icon="mdi:wifi", device_class=SensorDeviceClass.SIGNAL_STRENGTH, entity_category=EntityCategory.DIAGNOSTIC, cls=HealthDataRingSensor, diff --git a/homeassistant/components/ring/siren.py b/homeassistant/components/ring/siren.py index 0844f650e57..4e53ab8a006 100644 --- a/homeassistant/components/ring/siren.py +++ b/homeassistant/components/ring/siren.py @@ -1,9 +1,10 @@ """Component providing HA Siren support for Ring Chimes.""" + import logging from typing import Any +from ring_doorbell import RingGeneric from ring_doorbell.const import CHIME_TEST_SOUND_KINDS, KIND_DING -from ring_doorbell.generic import RingGeneric from homeassistant.components.siren import ATTR_TONE, SirenEntity, SirenEntityFeature from homeassistant.config_entries import ConfigEntry @@ -12,7 +13,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN, RING_DEVICES, RING_DEVICES_COORDINATOR from .coordinator import RingDataCoordinator -from .entity import RingEntity +from .entity import RingEntity, exception_wrap _LOGGER = logging.getLogger(__name__) @@ -27,12 +28,10 @@ async def async_setup_entry( coordinator: RingDataCoordinator = hass.data[DOMAIN][config_entry.entry_id][ RING_DEVICES_COORDINATOR ] - sirens = [] - for device in devices["chimes"]: - sirens.append(RingChimeSiren(device, coordinator)) - - async_add_entities(sirens) + async_add_entities( + RingChimeSiren(device, coordinator) for device in devices["chimes"] + ) class RingChimeSiren(RingEntity, SirenEntity): @@ -48,6 +47,7 @@ class RingChimeSiren(RingEntity, SirenEntity): # Entity class attributes self._attr_unique_id = f"{self._device.id}-siren" + @exception_wrap def turn_on(self, **kwargs: Any) -> None: """Play the test sound on a Ring Chime device.""" tone = kwargs.get(ATTR_TONE) or KIND_DING diff --git a/homeassistant/components/ring/strings.json b/homeassistant/components/ring/strings.json index 688e3141beb..142c533fcfc 100644 --- a/homeassistant/components/ring/strings.json +++ b/homeassistant/components/ring/strings.json @@ -37,6 +37,11 @@ "name": "Ding" } }, + "button": { + "open_door": { + "name": "Open door" + } + }, "light": { "light": { "name": "[%key:component::light::title%]" @@ -60,6 +65,15 @@ "volume": { "name": "Volume" }, + "doorbell_volume": { + "name": "Doorbell volume" + }, + "mic_volume": { + "name": "Mic volume" + }, + "voice_volume": { + "name": "Voice volume" + }, "wifi_signal_category": { "name": "Wi-Fi signal category" }, @@ -78,5 +92,18 @@ "name": "Update", "description": "Updates the data we have for all your ring devices." } + }, + "issues": { + "deprecated_service_ring_update": { + "title": "Detected use of deprecated service `ring.update`", + "fix_flow": { + "step": { + "confirm": { + "title": "[%key:component::ring::issues::deprecated_service_ring_update::title%]", + "description": "Use `homeassistant.update_entity` instead which will update all ring entities.\n\nPlease replace calls to this service and adjust your automations and scripts and select **submit** to close this issue." + } + } + } + } } } diff --git a/homeassistant/components/ring/switch.py b/homeassistant/components/ring/switch.py index 1f06f06e32e..15aa0a787bb 100644 --- a/homeassistant/components/ring/switch.py +++ b/homeassistant/components/ring/switch.py @@ -1,11 +1,10 @@ """Component providing HA switch support for Ring Door Bell/Chimes.""" + from datetime import timedelta import logging from typing import Any -import requests -from ring_doorbell import RingStickUpCam -from ring_doorbell.generic import RingGeneric +from ring_doorbell import RingGeneric, RingStickUpCam from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry @@ -15,12 +14,10 @@ import homeassistant.util.dt as dt_util from .const import DOMAIN, RING_DEVICES, RING_DEVICES_COORDINATOR from .coordinator import RingDataCoordinator -from .entity import RingEntity +from .entity import RingEntity, exception_wrap _LOGGER = logging.getLogger(__name__) -SIREN_ICON = "mdi:alarm-bell" - # It takes a few seconds for the API to correctly return an update indicating # that the changes have been made. Once we request a change (i.e. a light @@ -40,13 +37,12 @@ async def async_setup_entry( coordinator: RingDataCoordinator = hass.data[DOMAIN][config_entry.entry_id][ RING_DEVICES_COORDINATOR ] - switches = [] - for device in devices["stickup_cams"]: - if device.has_capability("siren"): - switches.append(SirenSwitch(device, coordinator)) - - async_add_entities(switches) + async_add_entities( + SirenSwitch(device, coordinator) + for device in devices["stickup_cams"] + if device.has_capability("siren") + ) class BaseRingSwitch(RingEntity, SwitchEntity): @@ -65,7 +61,6 @@ class SirenSwitch(BaseRingSwitch): """Creates a switch to turn the ring cameras siren on and off.""" _attr_translation_key = "siren" - _attr_icon = SIREN_ICON def __init__(self, device: RingGeneric, coordinator: RingDataCoordinator) -> None: """Initialize the switch for a device with a siren.""" @@ -85,13 +80,10 @@ class SirenSwitch(BaseRingSwitch): self._attr_is_on = device.siren > 0 super()._handle_coordinator_update() + @exception_wrap def _set_switch(self, new_state): """Update switch state, and causes Home Assistant to correctly update.""" - try: - self._device.siren = new_state - except requests.Timeout: - _LOGGER.error("Time out setting %s siren to %s", self.entity_id, new_state) - return + self._device.siren = new_state self._attr_is_on = new_state > 0 self._no_updates_until = dt_util.utcnow() + SKIP_UPDATES_DELAY diff --git a/homeassistant/components/ripple/sensor.py b/homeassistant/components/ripple/sensor.py index bab765c289c..1b65ec7ae09 100644 --- a/homeassistant/components/ripple/sensor.py +++ b/homeassistant/components/ripple/sensor.py @@ -1,4 +1,5 @@ """Support for Ripple sensors.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/risco/__init__.py b/homeassistant/components/risco/__init__.py index efc17c48a06..531cd982a1e 100644 --- a/homeassistant/components/risco/__init__.py +++ b/homeassistant/components/risco/__init__.py @@ -1,4 +1,5 @@ """The Risco integration.""" + from __future__ import annotations from collections.abc import Callable @@ -16,7 +17,7 @@ from pyrisco import ( ) from pyrisco.cloud.alarm import Alarm from pyrisco.cloud.event import Event -from pyrisco.common import Partition, Zone +from pyrisco.common import Partition, System, Zone from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -41,6 +42,7 @@ from .const import ( DEFAULT_SCAN_INTERVAL, DOMAIN, EVENTS_COORDINATOR, + SYSTEM_UPDATE_SIGNAL, TYPE_LOCAL, ) @@ -88,7 +90,7 @@ async def _async_setup_local_entry(hass: HomeAssistant, entry: ConfigEntry) -> b try: await risco.connect() except CannotConnectError as error: - raise ConfigEntryNotReady() from error + raise ConfigEntryNotReady from error except UnauthorizedError: _LOGGER.exception("Failed to login to Risco cloud") return False @@ -121,6 +123,12 @@ async def _async_setup_local_entry(hass: HomeAssistant, entry: ConfigEntry) -> b entry.async_on_unload(risco.add_partition_handler(_partition)) + async def _system(system: System) -> None: + _LOGGER.debug("Risco system update") + async_dispatcher_send(hass, SYSTEM_UPDATE_SIGNAL) + + entry.async_on_unload(risco.add_system_handler(_system)) + entry.async_on_unload(entry.add_update_listener(_update_listener)) hass.data.setdefault(DOMAIN, {}) diff --git a/homeassistant/components/risco/alarm_control_panel.py b/homeassistant/components/risco/alarm_control_panel.py index 8a233d0b5fe..580842e78ad 100644 --- a/homeassistant/components/risco/alarm_control_panel.py +++ b/homeassistant/components/risco/alarm_control_panel.py @@ -1,4 +1,5 @@ """Support for Risco alarms.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/risco/binary_sensor.py b/homeassistant/components/risco/binary_sensor.py index ea7153b2aee..afb65ee226f 100644 --- a/homeassistant/components/risco/binary_sensor.py +++ b/homeassistant/components/risco/binary_sensor.py @@ -1,24 +1,73 @@ """Support for Risco alarm zones.""" + from __future__ import annotations from collections.abc import Mapping +from itertools import chain from typing import Any from pyrisco.cloud.zone import Zone as CloudZone +from pyrisco.common import System from pyrisco.local.zone import Zone as LocalZone from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, BinarySensorEntity, + BinarySensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import LocalData, RiscoDataUpdateCoordinator, is_local -from .const import DATA_COORDINATOR, DOMAIN +from .const import DATA_COORDINATOR, DOMAIN, SYSTEM_UPDATE_SIGNAL from .entity import RiscoCloudZoneEntity, RiscoLocalZoneEntity +SYSTEM_ENTITY_DESCRIPTIONS = [ + BinarySensorEntityDescription( + key="low_battery_trouble", + translation_key="low_battery_trouble", + device_class=BinarySensorDeviceClass.BATTERY, + ), + BinarySensorEntityDescription( + key="ac_trouble", + translation_key="ac_trouble", + device_class=BinarySensorDeviceClass.PROBLEM, + ), + BinarySensorEntityDescription( + key="monitoring_station_1_trouble", + translation_key="monitoring_station_1_trouble", + device_class=BinarySensorDeviceClass.PROBLEM, + ), + BinarySensorEntityDescription( + key="monitoring_station_2_trouble", + translation_key="monitoring_station_2_trouble", + device_class=BinarySensorDeviceClass.PROBLEM, + ), + BinarySensorEntityDescription( + key="monitoring_station_3_trouble", + translation_key="monitoring_station_3_trouble", + device_class=BinarySensorDeviceClass.PROBLEM, + ), + BinarySensorEntityDescription( + key="phone_line_trouble", + translation_key="phone_line_trouble", + device_class=BinarySensorDeviceClass.PROBLEM, + ), + BinarySensorEntityDescription( + key="clock_trouble", + translation_key="clock_trouble", + device_class=BinarySensorDeviceClass.PROBLEM, + ), + BinarySensorEntityDescription( + key="box_tamper", + translation_key="box_tamper", + device_class=BinarySensorDeviceClass.TAMPER, + ), +] + async def async_setup_entry( hass: HomeAssistant, @@ -28,7 +77,7 @@ async def async_setup_entry( """Set up the Risco alarm control panel.""" if is_local(config_entry): local_data: LocalData = hass.data[DOMAIN][config_entry.entry_id] - async_add_entities( + zone_entities = ( entity for zone_id, zone in local_data.system.zones.items() for entity in ( @@ -37,6 +86,15 @@ async def async_setup_entry( RiscoLocalArmedBinarySensor(local_data.system.id, zone_id, zone), ) ) + + system_entities = ( + RiscoSystemBinarySensor( + local_data.system.id, local_data.system.system, entity_description + ) + for entity_description in SYSTEM_ENTITY_DESCRIPTIONS + ) + + async_add_entities(chain(system_entities, zone_entities)) else: coordinator: RiscoDataUpdateCoordinator = hass.data[DOMAIN][ config_entry.entry_id @@ -127,3 +185,40 @@ class RiscoLocalArmedBinarySensor(RiscoLocalZoneEntity, BinarySensorEntity): def is_on(self) -> bool | None: """Return true if sensor is on.""" return self._zone.armed + + +class RiscoSystemBinarySensor(BinarySensorEntity): + """Risco local system binary sensor class.""" + + _attr_should_poll = False + _attr_has_entity_name = True + + def __init__( + self, + system_id: str, + system: System, + entity_description: BinarySensorEntityDescription, + ) -> None: + """Init the sensor.""" + self._system = system + self._property = entity_description.key + self._attr_unique_id = f"{system_id}_{self._property}" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, system_id)}, + manufacturer="Risco", + name=system.name, + ) + self.entity_description = entity_description + + async def async_added_to_hass(self) -> None: + """Subscribe to updates.""" + self.async_on_remove( + async_dispatcher_connect( + self.hass, SYSTEM_UPDATE_SIGNAL, self.async_write_ha_state + ) + ) + + @property + def is_on(self) -> bool | None: + """Return true if sensor is on.""" + return getattr(self._system, self._property) diff --git a/homeassistant/components/risco/config_flow.py b/homeassistant/components/risco/config_flow.py index 61a452a7ecb..0f13721856c 100644 --- a/homeassistant/components/risco/config_flow.py +++ b/homeassistant/components/risco/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Risco integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -8,7 +9,12 @@ from typing import Any from pyrisco import CannotConnectError, RiscoCloud, RiscoLocal, UnauthorizedError import voluptuous as vol -from homeassistant import config_entries, core +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, @@ -22,7 +28,7 @@ from homeassistant.const import ( STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, ) -from homeassistant.data_entry_flow import FlowResult +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import ( @@ -64,7 +70,7 @@ HA_STATES = [ async def validate_cloud_input( - hass: core.HomeAssistant, data: dict[str, Any] + hass: HomeAssistant, data: dict[str, Any] ) -> dict[str, str]: """Validate the user input allows us to connect to Risco Cloud. @@ -81,7 +87,7 @@ async def validate_cloud_input( async def validate_local_input( - hass: core.HomeAssistant, data: Mapping[str, str] + hass: HomeAssistant, data: Mapping[str, str] ) -> dict[str, Any]: """Validate the user input allows us to connect to a local panel. @@ -109,26 +115,26 @@ async def validate_local_input( return {"title": site_id, "comm_delay": comm_delay} -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class RiscoConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Risco.""" VERSION = 1 def __init__(self) -> None: """Init the config flow.""" - self._reauth_entry: config_entries.ConfigEntry | None = None + self._reauth_entry: ConfigEntry | None = None @staticmethod - @core.callback + @callback def async_get_options_flow( - config_entry: config_entries.ConfigEntry, + config_entry: ConfigEntry, ) -> RiscoOptionsFlowHandler: """Define the config flow to handle options.""" return RiscoOptionsFlowHandler(config_entry) async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" return self.async_show_menu( step_id="user", @@ -137,7 +143,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_cloud( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Configure a cloud based alarm.""" errors: dict[str, str] = {} if user_input is not None: @@ -169,14 +175,16 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="cloud", data_schema=CLOUD_SCHEMA, errors=errors ) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Handle configuration by re-auth.""" self._reauth_entry = await self.async_set_unique_id(entry_data[CONF_USERNAME]) return await self.async_step_cloud() async def async_step_local( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Configure a local based alarm.""" errors: dict[str, str] = {} if user_input is not None: @@ -198,8 +206,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): title=info["title"], data={ **user_input, - **{CONF_TYPE: TYPE_LOCAL}, - **{CONF_COMMUNICATION_DELAY: info["comm_delay"]}, + CONF_TYPE: TYPE_LOCAL, + CONF_COMMUNICATION_DELAY: info["comm_delay"], }, ) @@ -208,10 +216,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -class RiscoOptionsFlowHandler(config_entries.OptionsFlow): +class RiscoOptionsFlowHandler(OptionsFlow): """Handle a Risco options flow.""" - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + def __init__(self, config_entry: ConfigEntry) -> None: """Initialize.""" self.config_entry = config_entry self._data = {**DEFAULT_OPTIONS, **config_entry.options} @@ -234,7 +242,7 @@ class RiscoOptionsFlowHandler(config_entries.OptionsFlow): async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Manage the options.""" if user_input is not None: self._data = {**self._data, **user_input} @@ -244,7 +252,7 @@ class RiscoOptionsFlowHandler(config_entries.OptionsFlow): async def async_step_risco_to_ha( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Map Risco states to HA states.""" if user_input is not None: self._data[CONF_RISCO_STATES_TO_HA] = user_input @@ -264,7 +272,7 @@ class RiscoOptionsFlowHandler(config_entries.OptionsFlow): async def async_step_ha_to_risco( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Map HA states to Risco states.""" if user_input is not None: self._data[CONF_HA_STATES_TO_RISCO] = user_input diff --git a/homeassistant/components/risco/const.py b/homeassistant/components/risco/const.py index 800003d2384..a27aeae4bf0 100644 --- a/homeassistant/components/risco/const.py +++ b/homeassistant/components/risco/const.py @@ -19,6 +19,7 @@ TYPE_LOCAL = "local" MAX_COMMUNICATION_DELAY = 3 +SYSTEM_UPDATE_SIGNAL = "risco_system_update" CONF_CODE_ARM_REQUIRED = "code_arm_required" CONF_CODE_DISARM_REQUIRED = "code_disarm_required" CONF_RISCO_STATES_TO_HA = "risco_states_to_ha" diff --git a/homeassistant/components/risco/entity.py b/homeassistant/components/risco/entity.py index ac3c04cfc2e..b3a3cdd1d4d 100644 --- a/homeassistant/components/risco/entity.py +++ b/homeassistant/components/risco/entity.py @@ -1,4 +1,5 @@ """A risco entity base class.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/risco/manifest.json b/homeassistant/components/risco/manifest.json index b5d8c4442fd..4c590b95e52 100644 --- a/homeassistant/components/risco/manifest.json +++ b/homeassistant/components/risco/manifest.json @@ -7,5 +7,5 @@ "iot_class": "local_push", "loggers": ["pyrisco"], "quality_scale": "platinum", - "requirements": ["pyrisco==0.5.10"] + "requirements": ["pyrisco==0.6.0"] } diff --git a/homeassistant/components/risco/sensor.py b/homeassistant/components/risco/sensor.py index 138c08c18f6..f4d6ddaf451 100644 --- a/homeassistant/components/risco/sensor.py +++ b/homeassistant/components/risco/sensor.py @@ -1,4 +1,5 @@ """Sensor for Risco Events.""" + from __future__ import annotations from collections.abc import Collection, Mapping diff --git a/homeassistant/components/risco/strings.json b/homeassistant/components/risco/strings.json index 13dfd60b5b6..69d7e571f43 100644 --- a/homeassistant/components/risco/strings.json +++ b/homeassistant/components/risco/strings.json @@ -72,6 +72,30 @@ }, "armed": { "name": "Armed" + }, + "low_battery_trouble": { + "name": "Low battery trouble" + }, + "ac_trouble": { + "name": "A/C trouble" + }, + "monitoring_station_1_trouble": { + "name": "Monitoring station 1 trouble" + }, + "monitoring_station_2_trouble": { + "name": "Monitoring station 2 trouble" + }, + "monitoring_station_3_trouble": { + "name": "Monitoring station 3 trouble" + }, + "phone_line_trouble": { + "name": "Phone line trouble" + }, + "clock_trouble": { + "name": "Clock trouble" + }, + "box_tamper": { + "name": "Box tamper" } }, "switch": { diff --git a/homeassistant/components/risco/switch.py b/homeassistant/components/risco/switch.py index d22b2bb2192..c43b55b0233 100644 --- a/homeassistant/components/risco/switch.py +++ b/homeassistant/components/risco/switch.py @@ -1,4 +1,5 @@ """Support for bypassing Risco alarm zones.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/rituals_perfume_genie/__init__.py b/homeassistant/components/rituals_perfume_genie/__init__.py index e0fac0abfcf..792a470ca3c 100644 --- a/homeassistant/components/rituals_perfume_genie/__init__.py +++ b/homeassistant/components/rituals_perfume_genie/__init__.py @@ -1,4 +1,5 @@ """The Rituals Perfume Genie integration.""" + import asyncio import aiohttp diff --git a/homeassistant/components/rituals_perfume_genie/binary_sensor.py b/homeassistant/components/rituals_perfume_genie/binary_sensor.py index f33a687b88f..63666fc1aca 100644 --- a/homeassistant/components/rituals_perfume_genie/binary_sensor.py +++ b/homeassistant/components/rituals_perfume_genie/binary_sensor.py @@ -1,4 +1,5 @@ """Support for Rituals Perfume Genie binary sensors.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/rituals_perfume_genie/config_flow.py b/homeassistant/components/rituals_perfume_genie/config_flow.py index f9a7f1cb6b8..7bff52fb864 100644 --- a/homeassistant/components/rituals_perfume_genie/config_flow.py +++ b/homeassistant/components/rituals_perfume_genie/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Rituals Perfume Genie integration.""" + from __future__ import annotations import logging @@ -8,9 +9,8 @@ from aiohttp import ClientResponseError from pyrituals import Account, AuthenticationException import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_EMAIL, CONF_PASSWORD -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import ACCOUNT_HASH, DOMAIN @@ -25,14 +25,14 @@ DATA_SCHEMA = vol.Schema( ) -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class RitualsPerfumeGenieConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Rituals Perfume Genie.""" VERSION = 1 async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" if user_input is None: return self.async_show_form(step_id="user", data_schema=DATA_SCHEMA) diff --git a/homeassistant/components/rituals_perfume_genie/coordinator.py b/homeassistant/components/rituals_perfume_genie/coordinator.py index b63b28e4de9..4c86f110b17 100644 --- a/homeassistant/components/rituals_perfume_genie/coordinator.py +++ b/homeassistant/components/rituals_perfume_genie/coordinator.py @@ -1,4 +1,5 @@ """The Rituals Perfume Genie data update coordinator.""" + import logging from pyrituals import Diffuser diff --git a/homeassistant/components/rituals_perfume_genie/diagnostics.py b/homeassistant/components/rituals_perfume_genie/diagnostics.py index 75b622b48b1..bcc61a01ad6 100644 --- a/homeassistant/components/rituals_perfume_genie/diagnostics.py +++ b/homeassistant/components/rituals_perfume_genie/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for Rituals Perfume Genie.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/rituals_perfume_genie/entity.py b/homeassistant/components/rituals_perfume_genie/entity.py index 83564f40488..35dbf639dd0 100644 --- a/homeassistant/components/rituals_perfume_genie/entity.py +++ b/homeassistant/components/rituals_perfume_genie/entity.py @@ -1,4 +1,5 @@ """Base class for Rituals Perfume Genie diffuser entity.""" + from __future__ import annotations from homeassistant.helpers.device_registry import DeviceInfo diff --git a/homeassistant/components/rituals_perfume_genie/icons.json b/homeassistant/components/rituals_perfume_genie/icons.json new file mode 100644 index 00000000000..0d66e206356 --- /dev/null +++ b/homeassistant/components/rituals_perfume_genie/icons.json @@ -0,0 +1,30 @@ +{ + "entity": { + "number": { + "perfume_amount": { + "default": "mdi:gauge" + } + }, + "select": { + "room_size_square_meter": { + "default": "mdi:ruler-square" + } + }, + "sensor": { + "fill": { + "default": "mdi:beaker" + }, + "perfume": { + "default": "mdi:tag" + }, + "wifi_percentage": { + "default": "mdi:wifi" + } + }, + "switch": { + "fan": { + "default": "mdi:fan" + } + } + } +} diff --git a/homeassistant/components/rituals_perfume_genie/number.py b/homeassistant/components/rituals_perfume_genie/number.py index 164b6de52c9..0ac9c30f285 100644 --- a/homeassistant/components/rituals_perfume_genie/number.py +++ b/homeassistant/components/rituals_perfume_genie/number.py @@ -1,4 +1,5 @@ """Support for Rituals Perfume Genie numbers.""" + from __future__ import annotations from collections.abc import Awaitable, Callable @@ -29,7 +30,6 @@ ENTITY_DESCRIPTIONS = ( RitualsNumberEntityDescription( key="perfume_amount", translation_key="perfume_amount", - icon="mdi:gauge", native_min_value=1, native_max_value=3, value_fn=lambda diffuser: diffuser.perfume_amount, diff --git a/homeassistant/components/rituals_perfume_genie/select.py b/homeassistant/components/rituals_perfume_genie/select.py index b9f0c29b267..e93d6ae03ef 100644 --- a/homeassistant/components/rituals_perfume_genie/select.py +++ b/homeassistant/components/rituals_perfume_genie/select.py @@ -1,4 +1,5 @@ """Support for Rituals Perfume Genie numbers.""" + from __future__ import annotations from collections.abc import Awaitable, Callable @@ -29,7 +30,6 @@ ENTITY_DESCRIPTIONS = ( RitualsSelectEntityDescription( key="room_size_square_meter", translation_key="room_size_square_meter", - icon="mdi:ruler-square", unit_of_measurement=AREA_SQUARE_METERS, entity_category=EntityCategory.CONFIG, options=["15", "30", "60", "100"], diff --git a/homeassistant/components/rituals_perfume_genie/sensor.py b/homeassistant/components/rituals_perfume_genie/sensor.py index cd139c94f1c..46faa8d73e9 100644 --- a/homeassistant/components/rituals_perfume_genie/sensor.py +++ b/homeassistant/components/rituals_perfume_genie/sensor.py @@ -1,4 +1,5 @@ """Support for Rituals Perfume Genie sensors.""" + from __future__ import annotations from collections.abc import Callable @@ -40,19 +41,16 @@ ENTITY_DESCRIPTIONS = ( RitualsSensorEntityDescription( key="fill", translation_key="fill", - icon="mdi:beaker", value_fn=lambda diffuser: diffuser.fill, ), RitualsSensorEntityDescription( key="perfume", translation_key="perfume", - icon="mdi:tag", value_fn=lambda diffuser: diffuser.perfume, ), RitualsSensorEntityDescription( key="wifi_percentage", translation_key="wifi_percentage", - icon="mdi:wifi", native_unit_of_measurement=PERCENTAGE, value_fn=lambda diffuser: diffuser.wifi_percentage, ), diff --git a/homeassistant/components/rituals_perfume_genie/switch.py b/homeassistant/components/rituals_perfume_genie/switch.py index 9c9a5f73d16..b5828f5ca07 100644 --- a/homeassistant/components/rituals_perfume_genie/switch.py +++ b/homeassistant/components/rituals_perfume_genie/switch.py @@ -1,4 +1,5 @@ """Support for Rituals Perfume Genie switches.""" + from __future__ import annotations from collections.abc import Awaitable, Callable @@ -17,27 +18,20 @@ from .coordinator import RitualsDataUpdateCoordinator from .entity import DiffuserEntity -@dataclass(frozen=True) -class RitualsEntityDescriptionMixin: - """Mixin values for Rituals entities.""" +@dataclass(frozen=True, kw_only=True) +class RitualsSwitchEntityDescription(SwitchEntityDescription): + """Class describing Rituals switch entities.""" is_on_fn: Callable[[Diffuser], bool] turn_on_fn: Callable[[Diffuser], Awaitable[None]] turn_off_fn: Callable[[Diffuser], Awaitable[None]] -@dataclass(frozen=True) -class RitualsSwitchEntityDescription( - SwitchEntityDescription, RitualsEntityDescriptionMixin -): - """Class describing Rituals switch entities.""" - - ENTITY_DESCRIPTIONS = ( RitualsSwitchEntityDescription( key="is_on", name=None, - icon="mdi:fan", + translation_key="fan", is_on_fn=lambda diffuser: diffuser.is_on, turn_on_fn=lambda diffuser: diffuser.turn_on(), turn_off_fn=lambda diffuser: diffuser.turn_off(), diff --git a/homeassistant/components/rmvtransport/sensor.py b/homeassistant/components/rmvtransport/sensor.py index ebe8f34c892..e53423d3b14 100644 --- a/homeassistant/components/rmvtransport/sensor.py +++ b/homeassistant/components/rmvtransport/sensor.py @@ -1,4 +1,5 @@ """Support for departure information for Rhein-Main public transport.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/roborock/__init__.py b/homeassistant/components/roborock/__init__.py index f4293213c00..c01d1fc7c9b 100644 --- a/homeassistant/components/roborock/__init__.py +++ b/homeassistant/components/roborock/__init__.py @@ -1,4 +1,5 @@ """The Roborock component.""" + from __future__ import annotations import asyncio @@ -86,12 +87,10 @@ def build_setup_functions( product_info: dict[str, HomeDataProduct], ) -> list[Coroutine[Any, Any, RoborockDataUpdateCoordinator | None]]: """Create a list of setup functions that can later be called asynchronously.""" - setup_functions = [] - for device in device_map.values(): - setup_functions.append( - setup_device(hass, user_data, device, product_info[device.product_id]) - ) - return setup_functions + return [ + setup_device(hass, user_data, device, product_info[device.product_id]) + for device in device_map.values() + ] async def setup_device( diff --git a/homeassistant/components/roborock/binary_sensor.py b/homeassistant/components/roborock/binary_sensor.py index e4e65288832..00716207f7a 100644 --- a/homeassistant/components/roborock/binary_sensor.py +++ b/homeassistant/components/roborock/binary_sensor.py @@ -1,4 +1,5 @@ """Support for Roborock sensors.""" + from __future__ import annotations from collections.abc import Callable @@ -22,20 +23,13 @@ from .coordinator import RoborockDataUpdateCoordinator from .device import RoborockCoordinatedEntity -@dataclass(frozen=True) -class RoborockBinarySensorDescriptionMixin: - """A class that describes binary sensor entities.""" +@dataclass(frozen=True, kw_only=True) +class RoborockBinarySensorDescription(BinarySensorEntityDescription): + """A class that describes Roborock binary sensors.""" value_fn: Callable[[DeviceProp], bool | int | None] -@dataclass(frozen=True) -class RoborockBinarySensorDescription( - BinarySensorEntityDescription, RoborockBinarySensorDescriptionMixin -): - """A class that describes Roborock binary sensors.""" - - BINARY_SENSOR_DESCRIPTIONS = [ RoborockBinarySensorDescription( key="dry_status", diff --git a/homeassistant/components/roborock/button.py b/homeassistant/components/roborock/button.py index e64b39c2383..fe6dfabb56c 100644 --- a/homeassistant/components/roborock/button.py +++ b/homeassistant/components/roborock/button.py @@ -1,4 +1,5 @@ """Support for Roborock button.""" + from __future__ import annotations from dataclasses import dataclass @@ -17,21 +18,14 @@ from .coordinator import RoborockDataUpdateCoordinator from .device import RoborockEntity -@dataclass(frozen=True) -class RoborockButtonDescriptionMixin: - """Define an entity description mixin for button entities.""" +@dataclass(frozen=True, kw_only=True) +class RoborockButtonDescription(ButtonEntityDescription): + """Describes a Roborock button entity.""" command: RoborockCommand param: list | dict | None -@dataclass(frozen=True) -class RoborockButtonDescription( - ButtonEntityDescription, RoborockButtonDescriptionMixin -): - """Describes a Roborock button entity.""" - - CONSUMABLE_BUTTON_DESCRIPTIONS = [ RoborockButtonDescription( key="reset_sensor_consumable", diff --git a/homeassistant/components/roborock/config_flow.py b/homeassistant/components/roborock/config_flow.py index 82c513a1b97..ede9afc826d 100644 --- a/homeassistant/components/roborock/config_flow.py +++ b/homeassistant/components/roborock/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Roborock.""" + from __future__ import annotations from collections.abc import Mapping @@ -16,17 +17,15 @@ from roborock.exceptions import ( from roborock.web_api import RoborockApiClient import voluptuous as vol -from homeassistant import config_entries -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_USERNAME -from homeassistant.data_entry_flow import FlowResult from .const import CONF_BASE_URL, CONF_ENTRY_CODE, CONF_USER_DATA, DOMAIN _LOGGER = logging.getLogger(__name__) -class RoborockFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class RoborockFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a config flow for Roborock.""" VERSION = 1 @@ -39,7 +38,7 @@ class RoborockFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" errors: dict[str, str] = {} @@ -81,7 +80,7 @@ class RoborockFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_code( self, user_input: dict[str, Any] | None = None, - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" errors: dict[str, str] = {} assert self._client @@ -120,7 +119,9 @@ class RoborockFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Perform reauth upon an API authentication error.""" self._username = entry_data[CONF_USERNAME] assert self._username @@ -132,7 +133,7 @@ class RoborockFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm reauth dialog.""" errors: dict[str, str] = {} if user_input is not None: @@ -143,7 +144,7 @@ class RoborockFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): def _create_entry( self, client: RoborockApiClient, username: str, user_data: UserData - ) -> FlowResult: + ) -> ConfigFlowResult: """Finished config flow and create entry.""" return self.async_create_entry( title=username, diff --git a/homeassistant/components/roborock/const.py b/homeassistant/components/roborock/const.py index f163c9620d1..77f0be3363e 100644 --- a/homeassistant/components/roborock/const.py +++ b/homeassistant/components/roborock/const.py @@ -1,4 +1,5 @@ """Constants for Roborock.""" + from vacuum_map_parser_base.config.drawable import Drawable from homeassistant.const import Platform diff --git a/homeassistant/components/roborock/coordinator.py b/homeassistant/components/roborock/coordinator.py index 7154a36f7b8..e682b119069 100644 --- a/homeassistant/components/roborock/coordinator.py +++ b/homeassistant/components/roborock/coordinator.py @@ -1,4 +1,5 @@ """Roborock Coordinator.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/roborock/device.py b/homeassistant/components/roborock/device.py index 2921a372e00..7affaa396e6 100644 --- a/homeassistant/components/roborock/device.py +++ b/homeassistant/components/roborock/device.py @@ -1,4 +1,5 @@ """Support for Roborock device base class.""" + from typing import Any from roborock.api import AttributeCache, RoborockClient @@ -57,7 +58,6 @@ class RoborockEntity(Entity): else: command_name = command raise HomeAssistantError( - f"Error while calling {command}", translation_domain=DOMAIN, translation_key="command_failed", translation_placeholders={ diff --git a/homeassistant/components/roborock/diagnostics.py b/homeassistant/components/roborock/diagnostics.py index e5fcc834267..79a9f0bafed 100644 --- a/homeassistant/components/roborock/diagnostics.py +++ b/homeassistant/components/roborock/diagnostics.py @@ -1,4 +1,5 @@ """Support for the Airzone diagnostics.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/roborock/image.py b/homeassistant/components/roborock/image.py index 66957232679..3367f1b3017 100644 --- a/homeassistant/components/roborock/image.py +++ b/homeassistant/components/roborock/image.py @@ -1,4 +1,5 @@ """Support for Roborock image.""" + import asyncio import io from itertools import chain @@ -104,7 +105,6 @@ class RoborockMap(RoborockCoordinatedEntity, ImageEntity): parsed_map = self.parser.parse(map_bytes) if parsed_map.image is None: raise HomeAssistantError( - "Something went wrong creating the map", translation_domain=DOMAIN, translation_key="map_failure", ) diff --git a/homeassistant/components/roborock/models.py b/homeassistant/components/roborock/models.py index c1d32df2d6d..45b98fddbc5 100644 --- a/homeassistant/components/roborock/models.py +++ b/homeassistant/components/roborock/models.py @@ -1,4 +1,5 @@ """Roborock Models.""" + from dataclasses import dataclass from typing import Any diff --git a/homeassistant/components/roborock/number.py b/homeassistant/components/roborock/number.py index 2218e5ec2ce..09030ef8500 100644 --- a/homeassistant/components/roborock/number.py +++ b/homeassistant/components/roborock/number.py @@ -1,4 +1,5 @@ """Support for Roborock number.""" + import asyncio from collections.abc import Callable, Coroutine from dataclasses import dataclass @@ -23,9 +24,9 @@ from .device import RoborockEntity _LOGGER = logging.getLogger(__name__) -@dataclass(frozen=True) -class RoborockNumberDescriptionMixin: - """Define an entity description mixin for button entities.""" +@dataclass(frozen=True, kw_only=True) +class RoborockNumberDescription(NumberEntityDescription): + """Class to describe a Roborock number entity.""" # Gets the status of the switch cache_key: CacheableAttribute @@ -33,13 +34,6 @@ class RoborockNumberDescriptionMixin: update_value: Callable[[AttributeCache, float], Coroutine[Any, Any, dict]] -@dataclass(frozen=True) -class RoborockNumberDescription( - NumberEntityDescription, RoborockNumberDescriptionMixin -): - """Class to describe an Roborock number entity.""" - - NUMBER_DESCRIPTIONS: list[RoborockNumberDescription] = [ RoborockNumberDescription( key="volume", diff --git a/homeassistant/components/roborock/select.py b/homeassistant/components/roborock/select.py index 3fdd10c97d5..fa7f4250804 100644 --- a/homeassistant/components/roborock/select.py +++ b/homeassistant/components/roborock/select.py @@ -1,4 +1,5 @@ """Support for Roborock select.""" + from collections.abc import Callable from dataclasses import dataclass @@ -18,9 +19,9 @@ from .coordinator import RoborockDataUpdateCoordinator from .device import RoborockCoordinatedEntity -@dataclass(frozen=True) -class RoborockSelectDescriptionMixin: - """Define an entity description mixin for select entities.""" +@dataclass(frozen=True, kw_only=True) +class RoborockSelectDescription(SelectEntityDescription): + """Class to describe a Roborock select entity.""" # The command that the select entity will send to the api. api_command: RoborockCommand @@ -28,16 +29,9 @@ class RoborockSelectDescriptionMixin: value_fn: Callable[[Status], str | None] # Gets all options of the select entity. options_lambda: Callable[[Status], list[str] | None] - # Takes the value from the select entiy and converts it for the api. + # Takes the value from the select entity and converts it for the api. parameter_lambda: Callable[[str, Status], list[int]] - -@dataclass(frozen=True) -class RoborockSelectDescription( - SelectEntityDescription, RoborockSelectDescriptionMixin -): - """Class to describe an Roborock select entity.""" - protocol_listener: RoborockDataProtocol | None = None diff --git a/homeassistant/components/roborock/sensor.py b/homeassistant/components/roborock/sensor.py index 8d723ec57cd..acee1688cc7 100644 --- a/homeassistant/components/roborock/sensor.py +++ b/homeassistant/components/roborock/sensor.py @@ -1,4 +1,5 @@ """Support for Roborock sensors.""" + from __future__ import annotations from collections.abc import Callable @@ -36,19 +37,12 @@ from .coordinator import RoborockDataUpdateCoordinator from .device import RoborockCoordinatedEntity -@dataclass(frozen=True) -class RoborockSensorDescriptionMixin: - """A class that describes sensor entities.""" +@dataclass(frozen=True, kw_only=True) +class RoborockSensorDescription(SensorEntityDescription): + """A class that describes Roborock sensors.""" value_fn: Callable[[DeviceProp], StateType | datetime.datetime] - -@dataclass(frozen=True) -class RoborockSensorDescription( - SensorEntityDescription, RoborockSensorDescriptionMixin -): - """A class that describes Roborock sensors.""" - protocol_listener: RoborockDataProtocol | None = None diff --git a/homeassistant/components/roborock/switch.py b/homeassistant/components/roborock/switch.py index acd3e2613af..9c7ca3cdcae 100644 --- a/homeassistant/components/roborock/switch.py +++ b/homeassistant/components/roborock/switch.py @@ -1,4 +1,5 @@ """Support for Roborock switch.""" + from __future__ import annotations import asyncio @@ -24,9 +25,9 @@ from .device import RoborockEntity _LOGGER = logging.getLogger(__name__) -@dataclass(frozen=True) -class RoborockSwitchDescriptionMixin: - """Define an entity description mixin for switch entities.""" +@dataclass(frozen=True, kw_only=True) +class RoborockSwitchDescription(SwitchEntityDescription): + """Class to describe a Roborock switch entity.""" # Gets the status of the switch cache_key: CacheableAttribute @@ -36,13 +37,6 @@ class RoborockSwitchDescriptionMixin: attribute: str -@dataclass(frozen=True) -class RoborockSwitchDescription( - SwitchEntityDescription, RoborockSwitchDescriptionMixin -): - """Class to describe an Roborock switch entity.""" - - SWITCH_DESCRIPTIONS: list[RoborockSwitchDescription] = [ RoborockSwitchDescription( cache_key=CacheableAttribute.child_lock_status, diff --git a/homeassistant/components/roborock/time.py b/homeassistant/components/roborock/time.py index 71dee773fa4..9a3cac86425 100644 --- a/homeassistant/components/roborock/time.py +++ b/homeassistant/components/roborock/time.py @@ -1,4 +1,5 @@ """Support for Roborock time.""" + import asyncio from collections.abc import Callable, Coroutine from dataclasses import dataclass @@ -25,9 +26,9 @@ from .device import RoborockEntity _LOGGER = logging.getLogger(__name__) -@dataclass(frozen=True) -class RoborockTimeDescriptionMixin: - """Define an entity description mixin for time entities.""" +@dataclass(frozen=True, kw_only=True) +class RoborockTimeDescription(TimeEntityDescription): + """Class to describe a Roborock time entity.""" # Gets the status of the switch cache_key: CacheableAttribute @@ -37,11 +38,6 @@ class RoborockTimeDescriptionMixin: get_value: Callable[[AttributeCache], datetime.time] -@dataclass(frozen=True) -class RoborockTimeDescription(TimeEntityDescription, RoborockTimeDescriptionMixin): - """Class to describe an Roborock time entity.""" - - TIME_DESCRIPTIONS: list[RoborockTimeDescription] = [ RoborockTimeDescription( key="dnd_start_time", diff --git a/homeassistant/components/roborock/vacuum.py b/homeassistant/components/roborock/vacuum.py index dafbb731bd2..22d9353e2a2 100644 --- a/homeassistant/components/roborock/vacuum.py +++ b/homeassistant/components/roborock/vacuum.py @@ -1,4 +1,5 @@ """Support for Roborock vacuum class.""" + from typing import Any from roborock.code_mappings import RoborockStateCode @@ -121,7 +122,12 @@ class RoborockVacuum(RoborockCoordinatedEntity, StateVacuumEntity): async def async_start(self) -> None: """Start the vacuum.""" - await self.send(RoborockCommand.APP_START) + if self._device_status.in_cleaning == 2: + await self.send(RoborockCommand.RESUME_ZONED_CLEAN) + elif self._device_status.in_cleaning == 3: + await self.send(RoborockCommand.RESUME_SEGMENT_CLEAN) + else: + await self.send(RoborockCommand.APP_START) async def async_pause(self) -> None: """Pause the vacuum.""" diff --git a/homeassistant/components/rocketchat/notify.py b/homeassistant/components/rocketchat/notify.py index 30bcc6c4515..9b7b40873ce 100644 --- a/homeassistant/components/rocketchat/notify.py +++ b/homeassistant/components/rocketchat/notify.py @@ -1,4 +1,5 @@ """Rocket.Chat notification service.""" + from __future__ import annotations from http import HTTPStatus diff --git a/homeassistant/components/roku/__init__.py b/homeassistant/components/roku/__init__.py index f31a07feb29..0620207a8ee 100644 --- a/homeassistant/components/roku/__init__.py +++ b/homeassistant/components/roku/__init__.py @@ -1,4 +1,5 @@ """Support for Roku.""" + from __future__ import annotations from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/roku/binary_sensor.py b/homeassistant/components/roku/binary_sensor.py index 144fded24b9..0f5f29f63f6 100644 --- a/homeassistant/components/roku/binary_sensor.py +++ b/homeassistant/components/roku/binary_sensor.py @@ -1,4 +1,5 @@ """Support for Roku binary sensors.""" + from __future__ import annotations from collections.abc import Callable @@ -19,45 +20,34 @@ from .const import DOMAIN from .entity import RokuEntity -@dataclass(frozen=True) -class RokuBinarySensorEntityDescriptionMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class RokuBinarySensorEntityDescription(BinarySensorEntityDescription): + """Describes a Roku binary sensor entity.""" value_fn: Callable[[RokuDevice], bool | None] -@dataclass(frozen=True) -class RokuBinarySensorEntityDescription( - BinarySensorEntityDescription, RokuBinarySensorEntityDescriptionMixin -): - """Describes a Roku binary sensor entity.""" - - BINARY_SENSORS: tuple[RokuBinarySensorEntityDescription, ...] = ( RokuBinarySensorEntityDescription( key="headphones_connected", translation_key="headphones_connected", - icon="mdi:headphones", value_fn=lambda device: device.info.headphones_connected, ), RokuBinarySensorEntityDescription( key="supports_airplay", translation_key="supports_airplay", - icon="mdi:cast-variant", entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda device: device.info.supports_airplay, ), RokuBinarySensorEntityDescription( key="supports_ethernet", translation_key="supports_ethernet", - icon="mdi:ethernet", entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda device: device.info.ethernet_support, ), RokuBinarySensorEntityDescription( key="supports_find_remote", translation_key="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/browse_media.py b/homeassistant/components/roku/browse_media.py index acaf2e5adbc..1ac37f10eb9 100644 --- a/homeassistant/components/roku/browse_media.py +++ b/homeassistant/components/roku/browse_media.py @@ -1,4 +1,5 @@ """Support for media browsing.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/roku/config_flow.py b/homeassistant/components/roku/config_flow.py index e3b8b97aa8f..07c1afae9e2 100644 --- a/homeassistant/components/roku/config_flow.py +++ b/homeassistant/components/roku/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Roku.""" + from __future__ import annotations import logging @@ -9,10 +10,9 @@ from rokuecp import Roku, RokuError import voluptuous as vol from homeassistant.components import ssdp, zeroconf -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_NAME from homeassistant.core import HomeAssistant, callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN @@ -52,7 +52,7 @@ class RokuConfigFlow(ConfigFlow, domain=DOMAIN): self.discovery_info = {} @callback - def _show_form(self, errors: dict[str, Any] | None = None) -> FlowResult: + def _show_form(self, errors: dict[str, Any] | None = None) -> ConfigFlowResult: """Show the form to the user.""" return self.async_show_form( step_id="user", @@ -62,7 +62,7 @@ class RokuConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" if not user_input: return self._show_form() @@ -86,7 +86,7 @@ class RokuConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_homekit( self, discovery_info: zeroconf.ZeroconfServiceInfo - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by homekit discovery.""" # If we already have the host configured do @@ -114,7 +114,9 @@ class RokuConfigFlow(ConfigFlow, domain=DOMAIN): return await self.async_step_discovery_confirm() - async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: + async def async_step_ssdp( + self, discovery_info: ssdp.SsdpServiceInfo + ) -> ConfigFlowResult: """Handle a flow initialized by discovery.""" host = urlparse(discovery_info.ssdp_location).hostname name = discovery_info.upnp[ssdp.ATTR_UPNP_FRIENDLY_NAME] @@ -140,7 +142,7 @@ class RokuConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_discovery_confirm( self, user_input: dict | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle user-confirmation of discovered device.""" if user_input is None: return self.async_show_form( diff --git a/homeassistant/components/roku/const.py b/homeassistant/components/roku/const.py index f098483e0c6..ab633a4044c 100644 --- a/homeassistant/components/roku/const.py +++ b/homeassistant/components/roku/const.py @@ -1,4 +1,5 @@ """Constants for the Roku integration.""" + DOMAIN = "roku" # Attributes diff --git a/homeassistant/components/roku/coordinator.py b/homeassistant/components/roku/coordinator.py index a0bd9df238c..baef00b2596 100644 --- a/homeassistant/components/roku/coordinator.py +++ b/homeassistant/components/roku/coordinator.py @@ -1,4 +1,5 @@ """Coordinator for Roku.""" + from __future__ import annotations from datetime import datetime, timedelta diff --git a/homeassistant/components/roku/diagnostics.py b/homeassistant/components/roku/diagnostics.py index bdd32f251ea..6c6809ee33a 100644 --- a/homeassistant/components/roku/diagnostics.py +++ b/homeassistant/components/roku/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for Roku.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/roku/entity.py b/homeassistant/components/roku/entity.py index b783831d4ec..259cb092cb8 100644 --- a/homeassistant/components/roku/entity.py +++ b/homeassistant/components/roku/entity.py @@ -1,4 +1,5 @@ """Base Entity for Roku.""" + from __future__ import annotations from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo diff --git a/homeassistant/components/roku/helpers.py b/homeassistant/components/roku/helpers.py index 60a3cbeec30..fc68e82c2d8 100644 --- a/homeassistant/components/roku/helpers.py +++ b/homeassistant/components/roku/helpers.py @@ -1,4 +1,5 @@ """Helpers for Roku.""" + from __future__ import annotations from collections.abc import Awaitable, Callable, Coroutine diff --git a/homeassistant/components/roku/icons.json b/homeassistant/components/roku/icons.json new file mode 100644 index 00000000000..02e5d1e5698 --- /dev/null +++ b/homeassistant/components/roku/icons.json @@ -0,0 +1,37 @@ +{ + "entity": { + "binary_sensor": { + "headphones_connected": { + "default": "mdi:headphones" + }, + "supports_airplay": { + "default": "mdi:cast-variant" + }, + "supports_ethernet": { + "default": "mdi:ethernet" + }, + "supports_find_remote": { + "default": "mdi:remote" + } + }, + "select": { + "application": { + "default": "mdi:application" + }, + "channel": { + "default": "mdi:television" + } + }, + "sensor": { + "active_app": { + "default": "mdi:application" + }, + "active_app_id": { + "default": "mdi:application-cog" + } + } + }, + "services": { + "search": "mdi:magnify" + } +} diff --git a/homeassistant/components/roku/media_player.py b/homeassistant/components/roku/media_player.py index 62a1a181459..92361909219 100644 --- a/homeassistant/components/roku/media_player.py +++ b/homeassistant/components/roku/media_player.py @@ -1,4 +1,5 @@ """Support for the Roku media player.""" + from __future__ import annotations import datetime as dt @@ -254,9 +255,12 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): @property def source_list(self) -> list[str]: """List of available input sources.""" - return ["Home"] + sorted( - app.name for app in self.coordinator.data.apps if app.name is not None - ) + return [ + "Home", + *sorted( + app.name for app in self.coordinator.data.apps if app.name is not None + ), + ] @roku_exception_handler() async def search(self, keyword: str) -> None: diff --git a/homeassistant/components/roku/remote.py b/homeassistant/components/roku/remote.py index ef5350eb741..fa351e021e8 100644 --- a/homeassistant/components/roku/remote.py +++ b/homeassistant/components/roku/remote.py @@ -1,4 +1,5 @@ """Support for the Roku remote.""" + from __future__ import annotations from collections.abc import Iterable diff --git a/homeassistant/components/roku/select.py b/homeassistant/components/roku/select.py index ef0f198f586..5f3b9d4049b 100644 --- a/homeassistant/components/roku/select.py +++ b/homeassistant/components/roku/select.py @@ -1,4 +1,5 @@ """Support for Roku selects.""" + from __future__ import annotations from collections.abc import Awaitable, Callable @@ -18,15 +19,6 @@ from .entity import RokuEntity from .helpers import format_channel_name, roku_exception_handler -@dataclass(frozen=True) -class RokuSelectEntityDescriptionMixin: - """Mixin for required keys.""" - - options_fn: Callable[[RokuDevice], list[str]] - value_fn: Callable[[RokuDevice], str | None] - set_fn: Callable[[RokuDevice, Roku, str], Awaitable[None]] - - def _get_application_name(device: RokuDevice) -> str | None: if device.app is None or device.app.name is None: return None @@ -38,7 +30,7 @@ def _get_application_name(device: RokuDevice) -> str | None: def _get_applications(device: RokuDevice) -> list[str]: - return ["Home"] + sorted(app.name for app in device.apps if app.name is not None) + return ["Home", *sorted(app.name for app in device.apps if app.name is not None)] def _get_channel_name(device: RokuDevice) -> str | None: @@ -85,18 +77,19 @@ async def _tune_channel(device: RokuDevice, roku: Roku, value: str) -> None: await roku.tune(_channel.number) -@dataclass(frozen=True) -class RokuSelectEntityDescription( - SelectEntityDescription, RokuSelectEntityDescriptionMixin -): +@dataclass(frozen=True, kw_only=True) +class RokuSelectEntityDescription(SelectEntityDescription): """Describes Roku select entity.""" + options_fn: Callable[[RokuDevice], list[str]] + value_fn: Callable[[RokuDevice], str | None] + set_fn: Callable[[RokuDevice, Roku, str], Awaitable[None]] + ENTITIES: tuple[RokuSelectEntityDescription, ...] = ( RokuSelectEntityDescription( key="application", translation_key="application", - icon="mdi:application", set_fn=_launch_application, value_fn=_get_application_name, options_fn=_get_applications, @@ -107,7 +100,6 @@ ENTITIES: tuple[RokuSelectEntityDescription, ...] = ( CHANNEL_ENTITY = RokuSelectEntityDescription( key="channel", translation_key="channel", - icon="mdi:television", set_fn=_tune_channel, value_fn=_get_channel_name, options_fn=_get_channels, @@ -123,15 +115,13 @@ async def async_setup_entry( coordinator: RokuDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] device: RokuDevice = coordinator.data - entities: list[RokuSelectEntity] = [] - - for description in ENTITIES: - entities.append( - RokuSelectEntity( - coordinator=coordinator, - description=description, - ) + entities: list[RokuSelectEntity] = [ + RokuSelectEntity( + coordinator=coordinator, + description=description, ) + for description in ENTITIES + ] if len(device.channels) > 0: entities.append( diff --git a/homeassistant/components/roku/sensor.py b/homeassistant/components/roku/sensor.py index b462b8c531b..ed134cc4c2a 100644 --- a/homeassistant/components/roku/sensor.py +++ b/homeassistant/components/roku/sensor.py @@ -1,4 +1,5 @@ """Support for Roku sensors.""" + from __future__ import annotations from collections.abc import Callable @@ -17,33 +18,24 @@ from .coordinator import RokuDataUpdateCoordinator from .entity import RokuEntity -@dataclass(frozen=True) -class RokuSensorEntityDescriptionMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class RokuSensorEntityDescription(SensorEntityDescription): + """Describes Roku sensor entity.""" value_fn: Callable[[RokuDevice], str | None] -@dataclass(frozen=True) -class RokuSensorEntityDescription( - SensorEntityDescription, RokuSensorEntityDescriptionMixin -): - """Describes Roku sensor entity.""" - - SENSORS: tuple[RokuSensorEntityDescription, ...] = ( RokuSensorEntityDescription( key="active_app", translation_key="active_app", entity_category=EntityCategory.DIAGNOSTIC, - icon="mdi:application", value_fn=lambda device: device.app.name if device.app else None, ), RokuSensorEntityDescription( key="active_app_id", translation_key="active_app_id", entity_category=EntityCategory.DIAGNOSTIC, - icon="mdi:application-cog", value_fn=lambda device: device.app.app_id if device.app else None, ), ) diff --git a/homeassistant/components/romy/config_flow.py b/homeassistant/components/romy/config_flow.py index 6bc96c9878c..bccae667695 100644 --- a/homeassistant/components/romy/config_flow.py +++ b/homeassistant/components/romy/config_flow.py @@ -1,4 +1,5 @@ """Config flow for ROMY integration.""" + from __future__ import annotations import romy @@ -7,7 +8,6 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components import zeroconf from homeassistant.const import CONF_HOST, CONF_PASSWORD -from homeassistant.data_entry_flow import FlowResult import homeassistant.helpers.config_validation as cv from .const import DOMAIN, LOGGER @@ -26,7 +26,7 @@ class RomyConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, str] | None = None - ) -> FlowResult: + ) -> config_entries.ConfigFlowResult: """Handle the user step.""" errors: dict[str, str] = {} @@ -59,7 +59,7 @@ class RomyConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_password( self, user_input: dict[str, str] | None = None - ) -> FlowResult: + ) -> config_entries.ConfigFlowResult: """Unlock the robots local http interface with password.""" errors: dict[str, str] = {} @@ -85,7 +85,7 @@ class RomyConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_zeroconf( self, discovery_info: zeroconf.ZeroconfServiceInfo - ) -> FlowResult: + ) -> config_entries.ConfigFlowResult: """Handle zeroconf discovery.""" LOGGER.debug("Zeroconf discovery_info: %s", discovery_info) @@ -125,7 +125,7 @@ class RomyConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_zeroconf_confirm( self, user_input: dict[str, str] | None = None - ) -> FlowResult: + ) -> config_entries.ConfigFlowResult: """Handle a confirmation flow initiated by zeroconf.""" if user_input is None: return self.async_show_form( @@ -137,7 +137,7 @@ class RomyConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) return await self._async_step_finish_config() - async def _async_step_finish_config(self) -> FlowResult: + async def _async_step_finish_config(self) -> config_entries.ConfigFlowResult: """Finish the configuration setup.""" return self.async_create_entry( title=self.robot_name_given_by_user, diff --git a/homeassistant/components/romy/entity.py b/homeassistant/components/romy/entity.py new file mode 100644 index 00000000000..ee4e209f158 --- /dev/null +++ b/homeassistant/components/romy/entity.py @@ -0,0 +1,24 @@ +"""Base entity for ROMY.""" + +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN +from .coordinator import RomyVacuumCoordinator + + +class RomyEntity(CoordinatorEntity[RomyVacuumCoordinator]): + """Base ROMY entity.""" + + _attr_has_entity_name = True + + def __init__(self, coordinator: RomyVacuumCoordinator) -> None: + """Initialize ROMY entity.""" + super().__init__(coordinator) + self.romy = coordinator.romy + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, self.romy.unique_id)}, + manufacturer="ROMY", + name=self.romy.name, + model=self.romy.model, + ) diff --git a/homeassistant/components/romy/vacuum.py b/homeassistant/components/romy/vacuum.py index 0670c2a49f6..de74d371f0e 100644 --- a/homeassistant/components/romy/vacuum.py +++ b/homeassistant/components/romy/vacuum.py @@ -4,22 +4,16 @@ For more details about this platform, please refer to the documentation https://home-assistant.io/components/vacuum.romy/. """ - from typing import Any -from romy import RomyRobot - from homeassistant.components.vacuum import StateVacuumEntity, VacuumEntityFeature from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN, LOGGER from .coordinator import RomyVacuumCoordinator - -ICON = "mdi:robot-vacuum" +from .entity import RomyEntity FAN_SPEED_NONE = "default" FAN_SPEED_NORMAL = "normal" @@ -58,33 +52,23 @@ async def async_setup_entry( """Set up ROMY vacuum cleaner.""" coordinator: RomyVacuumCoordinator = hass.data[DOMAIN][config_entry.entry_id] - async_add_entities([RomyVacuumEntity(coordinator, coordinator.romy)], True) + async_add_entities([RomyVacuumEntity(coordinator)]) -class RomyVacuumEntity(CoordinatorEntity[RomyVacuumCoordinator], StateVacuumEntity): +class RomyVacuumEntity(RomyEntity, StateVacuumEntity): """Representation of a ROMY vacuum cleaner robot.""" - _attr_has_entity_name = True - _attr_name = None _attr_supported_features = SUPPORT_ROMY_ROBOT _attr_fan_speed_list = FAN_SPEEDS - _attr_icon = ICON + _attr_name = None def __init__( self, coordinator: RomyVacuumCoordinator, - romy: RomyRobot, ) -> None: """Initialize the ROMY Robot.""" super().__init__(coordinator) - self.romy = romy self._attr_unique_id = self.romy.unique_id - self._device_info = DeviceInfo( - identifiers={(DOMAIN, romy.unique_id)}, - manufacturer="ROMY", - name=romy.name, - model=romy.model, - ) @callback def _handle_coordinator_update(self) -> None: diff --git a/homeassistant/components/roomba/__init__.py b/homeassistant/components/roomba/__init__.py index bd302e16a90..d00010aa3e9 100644 --- a/homeassistant/components/roomba/__init__.py +++ b/homeassistant/components/roomba/__init__.py @@ -1,4 +1,5 @@ """The roomba component.""" + import asyncio import contextlib from functools import partial diff --git a/homeassistant/components/roomba/binary_sensor.py b/homeassistant/components/roomba/binary_sensor.py index 007d803fbf4..40a5535d5af 100644 --- a/homeassistant/components/roomba/binary_sensor.py +++ b/homeassistant/components/roomba/binary_sensor.py @@ -1,4 +1,5 @@ """Roomba binary sensor entities.""" + from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -28,7 +29,6 @@ async def async_setup_entry( class RoombaBinStatus(IRobotEntity, BinarySensorEntity): """Class to hold Roomba Sensor basic info.""" - _attr_icon = "mdi:delete-variant" _attr_translation_key = "bin_full" @property diff --git a/homeassistant/components/roomba/braava.py b/homeassistant/components/roomba/braava.py index db517a065ea..37411680d0b 100644 --- a/homeassistant/components/roomba/braava.py +++ b/homeassistant/components/roomba/braava.py @@ -1,4 +1,5 @@ """Class for Braava devices.""" + import logging from homeassistant.components.vacuum import VacuumEntityFeature @@ -36,11 +37,11 @@ class BraavaJet(IRobotVacuum): super().__init__(roomba, blid) # Initialize fan speed list - speed_list = [] - for behavior in BRAAVA_MOP_BEHAVIORS: - for spray in BRAAVA_SPRAY_AMOUNT: - speed_list.append(f"{behavior}-{spray}") - self._attr_fan_speed_list = speed_list + self._attr_fan_speed_list = [ + f"{behavior}-{spray}" + for behavior in BRAAVA_MOP_BEHAVIORS + for spray in BRAAVA_SPRAY_AMOUNT + ] @property def fan_speed(self): diff --git a/homeassistant/components/roomba/config_flow.py b/homeassistant/components/roomba/config_flow.py index e4fb45865a2..7b834421135 100644 --- a/homeassistant/components/roomba/config_flow.py +++ b/homeassistant/components/roomba/config_flow.py @@ -1,4 +1,5 @@ """Config flow to configure roomba component.""" + from __future__ import annotations import asyncio @@ -9,11 +10,15 @@ from roombapy.discovery import RoombaDiscovery from roombapy.getpassword import RoombaPassword import voluptuous as vol -from homeassistant import config_entries, core from homeassistant.components import dhcp, zeroconf +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.const import CONF_DELAY, CONF_HOST, CONF_NAME, CONF_PASSWORD -from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult +from homeassistant.core import HomeAssistant, callback from . import CannotConnect, async_connect_or_timeout, async_disconnect_or_timeout from .const import ( @@ -38,7 +43,7 @@ AUTH_HELP_URL_KEY = "auth_help_url" AUTH_HELP_URL_VALUE = "https://www.home-assistant.io/integrations/roomba/#manually-retrieving-your-credentials" -async def validate_input(hass: core.HomeAssistant, data): +async def validate_input(hass: HomeAssistant, data): """Validate the user input allows us to connect. Data has the keys from DATA_SCHEMA with values provided by the user. @@ -65,7 +70,7 @@ async def validate_input(hass: core.HomeAssistant, data): } -class RoombaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class RoombaConfigFlow(ConfigFlow, domain=DOMAIN): """Roomba configuration flow.""" VERSION = 1 @@ -80,26 +85,30 @@ class RoombaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback def async_get_options_flow( - config_entry: config_entries.ConfigEntry, + config_entry: ConfigEntry, ) -> OptionsFlowHandler: """Get the options flow for this handler.""" return OptionsFlowHandler(config_entry) async def async_step_zeroconf( self, discovery_info: zeroconf.ZeroconfServiceInfo - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle zeroconf discovery.""" return await self._async_step_discovery( - discovery_info.host, discovery_info.hostname.lower().rstrip(".local.") + discovery_info.host, discovery_info.hostname.lower().removesuffix(".local.") ) - async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: + async def async_step_dhcp( + self, discovery_info: dhcp.DhcpServiceInfo + ) -> ConfigFlowResult: """Handle dhcp discovery.""" return await self._async_step_discovery( discovery_info.ip, discovery_info.hostname ) - async def _async_step_discovery(self, ip_address: str, hostname: str) -> FlowResult: + async def _async_step_discovery( + self, ip_address: str, hostname: str + ) -> ConfigFlowResult: """Handle any discovery.""" self._async_abort_entries_match({CONF_HOST: ip_address}) @@ -281,10 +290,10 @@ class RoombaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -class OptionsFlowHandler(config_entries.OptionsFlow): +class OptionsFlowHandler(OptionsFlow): """Handle options.""" - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + def __init__(self, config_entry: ConfigEntry) -> None: """Initialize options flow.""" self.config_entry = config_entry diff --git a/homeassistant/components/roomba/const.py b/homeassistant/components/roomba/const.py index 71db96f8c21..331c0900682 100644 --- a/homeassistant/components/roomba/const.py +++ b/homeassistant/components/roomba/const.py @@ -1,4 +1,5 @@ """The roomba constants.""" + from homeassistant.const import Platform DOMAIN = "roomba" diff --git a/homeassistant/components/roomba/icons.json b/homeassistant/components/roomba/icons.json new file mode 100644 index 00000000000..cdb36ef97e5 --- /dev/null +++ b/homeassistant/components/roomba/icons.json @@ -0,0 +1,38 @@ +{ + "entity": { + "binary_sensor": { + "bin_full": { + "default": "mdi:delete-variant" + } + }, + "sensor": { + "battery_cycles": { + "default": "mdi:counter" + }, + "total_cleaning_time": { + "default": "mdi:clock" + }, + "average_mission_time": { + "default": "mdi:clock" + }, + "total_missions": { + "default": "mdi:counter" + }, + "successful_missions": { + "default": "mdi:counter" + }, + "canceled_missions": { + "default": "mdi:counter" + }, + "failed_missions": { + "default": "mdi:counter" + }, + "scrubs_count": { + "default": "mdi:counter" + }, + "total_cleaned_area": { + "default": "mdi:texture-box" + } + } + } +} diff --git a/homeassistant/components/roomba/irobot_base.py b/homeassistant/components/roomba/irobot_base.py index 38de3a7fb2b..4850dc0b7e9 100644 --- a/homeassistant/components/roomba/irobot_base.py +++ b/homeassistant/components/roomba/irobot_base.py @@ -1,4 +1,5 @@ """Base class for iRobot devices.""" + from __future__ import annotations import asyncio @@ -250,7 +251,7 @@ class IRobotVacuum(IRobotEntity, StateVacuumEntity): """Set the vacuum cleaner to return to the dock.""" if self.state == STATE_CLEANING: await self.async_pause() - for _ in range(0, 10): + for _ in range(10): if self.state == STATE_PAUSED: break await asyncio.sleep(1) diff --git a/homeassistant/components/roomba/manifest.json b/homeassistant/components/roomba/manifest.json index 530ba8e8137..a697680b379 100644 --- a/homeassistant/components/roomba/manifest.json +++ b/homeassistant/components/roomba/manifest.json @@ -22,10 +22,9 @@ } ], "documentation": "https://www.home-assistant.io/integrations/roomba", - "import_executor": true, "iot_class": "local_push", "loggers": ["paho_mqtt", "roombapy"], - "requirements": ["roombapy==1.6.13"], + "requirements": ["roombapy==1.8.1"], "zeroconf": [ { "type": "_amzn-alexa._tcp.local.", diff --git a/homeassistant/components/roomba/models.py b/homeassistant/components/roomba/models.py index 87610bed1ae..350495cae7b 100644 --- a/homeassistant/components/roomba/models.py +++ b/homeassistant/components/roomba/models.py @@ -1,4 +1,5 @@ """The roomba integration models.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/homeassistant/components/roomba/roomba.py b/homeassistant/components/roomba/roomba.py index 2c50508a637..5d774120634 100644 --- a/homeassistant/components/roomba/roomba.py +++ b/homeassistant/components/roomba/roomba.py @@ -1,4 +1,5 @@ """Class for Roomba devices.""" + import logging from homeassistant.components.vacuum import VacuumEntityFeature diff --git a/homeassistant/components/roomba/sensor.py b/homeassistant/components/roomba/sensor.py index ad2894ebb11..6e043d237f3 100644 --- a/homeassistant/components/roomba/sensor.py +++ b/homeassistant/components/roomba/sensor.py @@ -1,4 +1,5 @@ """Sensor platform for Roomba.""" + from collections.abc import Callable from dataclasses import dataclass @@ -26,20 +27,13 @@ from .irobot_base import IRobotEntity from .models import RoombaData -@dataclass(frozen=True) -class RoombaSensorEntityDescriptionMixin: - """Mixin for describing Roomba data.""" +@dataclass(frozen=True, kw_only=True) +class RoombaSensorEntityDescription(SensorEntityDescription): + """Immutable class for describing Roomba data.""" value_fn: Callable[[IRobotEntity], StateType] -@dataclass(frozen=True) -class RoombaSensorEntityDescription( - SensorEntityDescription, RoombaSensorEntityDescriptionMixin -): - """Immutable class for describing Roomba data.""" - - SENSORS: list[RoombaSensorEntityDescription] = [ RoombaSensorEntityDescription( key="battery", @@ -52,7 +46,6 @@ SENSORS: list[RoombaSensorEntityDescription] = [ key="battery_cycles", translation_key="battery_cycles", state_class=SensorStateClass.MEASUREMENT, - icon="mdi:counter", entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda self: self.battery_stats.get("nLithChrg") or self.battery_stats.get("nNimhChrg"), @@ -60,7 +53,6 @@ SENSORS: list[RoombaSensorEntityDescription] = [ RoombaSensorEntityDescription( key="total_cleaning_time", translation_key="total_cleaning_time", - icon="mdi:clock", native_unit_of_measurement=UnitOfTime.HOURS, entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda self: self.run_stats.get("hr"), @@ -68,7 +60,6 @@ SENSORS: list[RoombaSensorEntityDescription] = [ RoombaSensorEntityDescription( key="average_mission_time", translation_key="average_mission_time", - icon="mdi:clock", native_unit_of_measurement=UnitOfTime.MINUTES, entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda self: self.mission_stats.get("aMssnM"), @@ -76,7 +67,6 @@ SENSORS: list[RoombaSensorEntityDescription] = [ RoombaSensorEntityDescription( key="total_missions", translation_key="total_missions", - icon="mdi:counter", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement="Missions", entity_category=EntityCategory.DIAGNOSTIC, @@ -85,7 +75,6 @@ SENSORS: list[RoombaSensorEntityDescription] = [ RoombaSensorEntityDescription( key="successful_missions", translation_key="successful_missions", - icon="mdi:counter", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement="Missions", entity_category=EntityCategory.DIAGNOSTIC, @@ -94,7 +83,6 @@ SENSORS: list[RoombaSensorEntityDescription] = [ RoombaSensorEntityDescription( key="canceled_missions", translation_key="canceled_missions", - icon="mdi:counter", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement="Missions", entity_category=EntityCategory.DIAGNOSTIC, @@ -103,7 +91,6 @@ SENSORS: list[RoombaSensorEntityDescription] = [ RoombaSensorEntityDescription( key="failed_missions", translation_key="failed_missions", - icon="mdi:counter", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement="Missions", entity_category=EntityCategory.DIAGNOSTIC, @@ -112,7 +99,6 @@ SENSORS: list[RoombaSensorEntityDescription] = [ RoombaSensorEntityDescription( key="scrubs_count", translation_key="scrubs_count", - icon="mdi:counter", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement="Scrubs", entity_category=EntityCategory.DIAGNOSTIC, @@ -122,7 +108,6 @@ SENSORS: list[RoombaSensorEntityDescription] = [ RoombaSensorEntityDescription( key="total_cleaned_area", translation_key="total_cleaned_area", - icon="mdi:texture-box", native_unit_of_measurement=AREA_SQUARE_METERS, entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda self: ( diff --git a/homeassistant/components/roomba/vacuum.py b/homeassistant/components/roomba/vacuum.py index b6c0e893b1c..e4a83375ccc 100644 --- a/homeassistant/components/roomba/vacuum.py +++ b/homeassistant/components/roomba/vacuum.py @@ -1,4 +1,5 @@ """Support for Wi-Fi enabled iRobot Roombas.""" + from __future__ import annotations from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/roon/__init__.py b/homeassistant/components/roon/__init__.py index f721f0bac40..272ad6f011b 100644 --- a/homeassistant/components/roon/__init__.py +++ b/homeassistant/components/roon/__init__.py @@ -1,4 +1,5 @@ """Roon (www.roonlabs.com) component.""" + from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, Platform from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/roon/config_flow.py b/homeassistant/components/roon/config_flow.py index 6ccf97155c4..d24cdb0c98d 100644 --- a/homeassistant/components/roon/config_flow.py +++ b/homeassistant/components/roon/config_flow.py @@ -1,12 +1,15 @@ """Config flow for roon integration.""" + import asyncio import logging from roonapi import RoonApi, RoonDiscovery import voluptuous as vol -from homeassistant import config_entries, core, exceptions +from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv from .const import ( @@ -99,7 +102,7 @@ async def discover(hass): return servers -async def authenticate(hass: core.HomeAssistant, host, port, servers): +async def authenticate(hass: HomeAssistant, host, port, servers): """Connect and authenticate home assistant.""" hub = RoonHub(hass) @@ -116,7 +119,7 @@ async def authenticate(hass: core.HomeAssistant, host, port, servers): } -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class RoonConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for roon.""" VERSION = 1 @@ -152,7 +155,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) async def async_step_link(self, user_input=None): - """Handle linking and authenticting with the roon server.""" + """Handle linking and authenticating with the roon server.""" errors = {} if user_input is not None: # Do not authenticate if the host is already configured @@ -174,5 +177,5 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form(step_id="link", errors=errors) -class InvalidAuth(exceptions.HomeAssistantError): +class InvalidAuth(HomeAssistantError): """Error to indicate there is invalid auth.""" diff --git a/homeassistant/components/roon/event.py b/homeassistant/components/roon/event.py index fc1bb339cd7..ea5014c8755 100644 --- a/homeassistant/components/roon/event.py +++ b/homeassistant/components/roon/event.py @@ -1,4 +1,5 @@ """Roon event entities.""" + import logging from typing import cast diff --git a/homeassistant/components/roon/icons.json b/homeassistant/components/roon/icons.json new file mode 100644 index 00000000000..571ca3f45a2 --- /dev/null +++ b/homeassistant/components/roon/icons.json @@ -0,0 +1,5 @@ +{ + "services": { + "transfer": "mdi:monitor-multiple" + } +} diff --git a/homeassistant/components/roon/media_browser.py b/homeassistant/components/roon/media_browser.py index dd7a2a1faa3..806375bc902 100644 --- a/homeassistant/components/roon/media_browser.py +++ b/homeassistant/components/roon/media_browser.py @@ -1,4 +1,5 @@ """Support to interface with the Roon API.""" + import logging from homeassistant.components.media_player import BrowseMedia, MediaClass diff --git a/homeassistant/components/roon/media_player.py b/homeassistant/components/roon/media_player.py index 5fce298a56b..3b1735cd2fc 100644 --- a/homeassistant/components/roon/media_player.py +++ b/homeassistant/components/roon/media_player.py @@ -1,4 +1,5 @@ """MediaPlayer platform for Roon integration.""" + from __future__ import annotations import logging @@ -232,12 +233,13 @@ class RoonDevice(MediaPlayerEntity): } now_playing_data = None + media_position = convert(player_data.get("seek_position"), int, 0) + try: now_playing_data = player_data["now_playing"] media_title = now_playing_data["three_line"]["line1"] media_artist = now_playing_data["three_line"]["line2"] media_album_name = now_playing_data["three_line"]["line3"] - media_position = convert(now_playing_data["seek_position"], int, 0) media_duration = convert(now_playing_data.get("length"), int, 0) image_id = now_playing_data.get("image_key") except KeyError: diff --git a/homeassistant/components/roon/server.py b/homeassistant/components/roon/server.py index 488fe18aae4..3f2e541b125 100644 --- a/homeassistant/components/roon/server.py +++ b/homeassistant/components/roon/server.py @@ -1,4 +1,5 @@ """Code to handle the api connection to a Roon server.""" + import asyncio import logging diff --git a/homeassistant/components/route53/__init__.py b/homeassistant/components/route53/__init__.py index 4f17b507560..92094b0b608 100644 --- a/homeassistant/components/route53/__init__.py +++ b/homeassistant/components/route53/__init__.py @@ -1,4 +1,5 @@ """Update the IP addresses of your Route53 DNS records.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/route53/icons.json b/homeassistant/components/route53/icons.json new file mode 100644 index 00000000000..30a854991f0 --- /dev/null +++ b/homeassistant/components/route53/icons.json @@ -0,0 +1,5 @@ +{ + "services": { + "update_records": "mdi:database-refresh" + } +} diff --git a/homeassistant/components/rova/__init__.py b/homeassistant/components/rova/__init__.py index 411ea6c7239..64f0e787a4b 100644 --- a/homeassistant/components/rova/__init__.py +++ b/homeassistant/components/rova/__init__.py @@ -1 +1,64 @@ """The rova component.""" + +from __future__ import annotations + +from requests.exceptions import ConnectTimeout, HTTPError +from rova.rova import Rova + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue + +from .const import CONF_HOUSE_NUMBER, CONF_HOUSE_NUMBER_SUFFIX, CONF_ZIP_CODE, DOMAIN +from .coordinator import RovaCoordinator + +PLATFORMS: list[Platform] = [Platform.SENSOR] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up ROVA from a config entry.""" + + api = Rova( + entry.data[CONF_ZIP_CODE], + entry.data[CONF_HOUSE_NUMBER], + entry.data[CONF_HOUSE_NUMBER_SUFFIX], + ) + + try: + rova_area = await hass.async_add_executor_job(api.is_rova_area) + except (ConnectTimeout, HTTPError) as ex: + raise ConfigEntryNotReady from ex + + if not rova_area: + async_create_issue( + hass, + DOMAIN, + f"no_rova_area_{entry.data[CONF_ZIP_CODE]}", + is_fixable=False, + issue_domain=DOMAIN, + severity=IssueSeverity.ERROR, + translation_key="no_rova_area", + translation_placeholders={ + CONF_ZIP_CODE: entry.data[CONF_ZIP_CODE], + }, + ) + raise ConfigEntryError("Rova does not collect garbage in this area") + + coordinator = RovaCoordinator(hass, api) + + await coordinator.async_config_entry_first_refresh() + + hass.data.setdefault(DOMAIN, {})[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 ROVA 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/rova/config_flow.py b/homeassistant/components/rova/config_flow.py new file mode 100644 index 00000000000..d618681783e --- /dev/null +++ b/homeassistant/components/rova/config_flow.py @@ -0,0 +1,91 @@ +"""Config flow for the Rova platform.""" + +from typing import Any + +from requests.exceptions import ConnectTimeout, HTTPError +from rova.rova import Rova +import voluptuous as vol + +from homeassistant import config_entries + +from .const import CONF_HOUSE_NUMBER, CONF_HOUSE_NUMBER_SUFFIX, CONF_ZIP_CODE, DOMAIN + + +class RovaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle Rova config flow.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> config_entries.ConfigFlowResult: + """Step when user initializes a integration.""" + errors: dict[str, str] = {} + + if user_input is not None: + # generate unique name for rova integration + zip_code = user_input[CONF_ZIP_CODE] + number = user_input[CONF_HOUSE_NUMBER] + suffix = user_input[CONF_HOUSE_NUMBER_SUFFIX] + + await self.async_set_unique_id(f"{zip_code}{number}{suffix}".strip()) + self._abort_if_unique_id_configured() + + api = Rova(zip_code, number, suffix) + + try: + if not await self.hass.async_add_executor_job(api.is_rova_area): + errors = {"base": "invalid_rova_area"} + except (ConnectTimeout, HTTPError): + errors = {"base": "cannot_connect"} + + if not errors: + return self.async_create_entry( + title=f"{zip_code} {number} {suffix}".strip(), + data=user_input, + ) + + return self.async_show_form( + step_id="user", + data_schema=self.add_suggested_values_to_schema( + vol.Schema( + { + vol.Required(CONF_ZIP_CODE): str, + vol.Required(CONF_HOUSE_NUMBER): str, + vol.Optional(CONF_HOUSE_NUMBER_SUFFIX, default=""): str, + } + ), + user_input, + ), + errors=errors, + ) + + async def async_step_import( + self, user_input: dict[str, Any] + ) -> config_entries.ConfigFlowResult: + """Import the yaml config.""" + zip_code = user_input[CONF_ZIP_CODE] + number = user_input[CONF_HOUSE_NUMBER] + suffix = user_input[CONF_HOUSE_NUMBER_SUFFIX] + + await self.async_set_unique_id(f"{zip_code}{number}{suffix}".strip()) + self._abort_if_unique_id_configured() + + api = Rova(zip_code, number, suffix) + + try: + result = await self.hass.async_add_executor_job(api.is_rova_area) + + if result: + return self.async_create_entry( + title=f"{zip_code} {number} {suffix}".strip(), + data={ + CONF_ZIP_CODE: zip_code, + CONF_HOUSE_NUMBER: number, + CONF_HOUSE_NUMBER_SUFFIX: suffix, + }, + ) + return self.async_abort(reason="invalid_rova_area") + + except (ConnectTimeout, HTTPError): + return self.async_abort(reason="cannot_connect") diff --git a/homeassistant/components/rova/const.py b/homeassistant/components/rova/const.py index 71d39d3703b..fa815b922ea 100644 --- a/homeassistant/components/rova/const.py +++ b/homeassistant/components/rova/const.py @@ -1,8 +1,13 @@ """Const file for Rova.""" + import logging LOGGER = logging.getLogger(__package__) +DEFAULT_NAME = "Rova" + CONF_ZIP_CODE = "zip_code" CONF_HOUSE_NUMBER = "house_number" CONF_HOUSE_NUMBER_SUFFIX = "house_number_suffix" + +DOMAIN = "rova" diff --git a/homeassistant/components/rova/coordinator.py b/homeassistant/components/rova/coordinator.py new file mode 100644 index 00000000000..ef411be19e8 --- /dev/null +++ b/homeassistant/components/rova/coordinator.py @@ -0,0 +1,42 @@ +"""Coordinator for Rova.""" + +from datetime import datetime, timedelta + +from rova.rova import Rova + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from homeassistant.util.dt import get_time_zone + +from .const import DOMAIN, LOGGER + + +class RovaCoordinator(DataUpdateCoordinator[dict[str, datetime]]): + """Class to manage fetching Rova data.""" + + def __init__(self, hass: HomeAssistant, api: Rova) -> None: + """Initialize.""" + super().__init__( + hass, + LOGGER, + name=DOMAIN, + update_interval=timedelta(hours=12), + ) + self.api = api + + async def _async_update_data(self) -> dict[str, datetime]: + """Fetch data from Rova API.""" + + items = await self.hass.async_add_executor_job(self.api.get_calendar_items) + + data = {} + + for item in items: + date = datetime.strptime(item["Date"], "%Y-%m-%dT%H:%M:%S").replace( + tzinfo=get_time_zone("Europe/Amsterdam") + ) + code = item["GarbageTypeCode"].lower() + if code not in data: + data[code] = date + + return data diff --git a/homeassistant/components/rova/icons.json b/homeassistant/components/rova/icons.json new file mode 100644 index 00000000000..dc3e85ca8c1 --- /dev/null +++ b/homeassistant/components/rova/icons.json @@ -0,0 +1,18 @@ +{ + "entity": { + "sensor": { + "bio": { + "default": "mdi:leaf" + }, + "paper": { + "default": "mdi:file" + }, + "plastic": { + "default": "mdi:recycle" + }, + "residual": { + "default": "mdi:delete" + } + } + } +} diff --git a/homeassistant/components/rova/manifest.json b/homeassistant/components/rova/manifest.json index a87ec224122..b867cac8e7a 100644 --- a/homeassistant/components/rova/manifest.json +++ b/homeassistant/components/rova/manifest.json @@ -2,6 +2,7 @@ "domain": "rova", "name": "ROVA", "codeowners": [], + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/rova", "iot_class": "cloud_polling", "loggers": ["rova"], diff --git a/homeassistant/components/rova/sensor.py b/homeassistant/components/rova/sensor.py index 3565b3baf0d..e510bcf0caf 100644 --- a/homeassistant/components/rova/sensor.py +++ b/homeassistant/components/rova/sensor.py @@ -1,10 +1,9 @@ """Support for Rova garbage calendar.""" + from __future__ import annotations -from datetime import datetime, timedelta +from datetime import datetime -from requests.exceptions import ConnectTimeout, HTTPError -from rova.rova import Rova import voluptuous as vol from homeassistant.components.sensor import ( @@ -13,42 +12,40 @@ from homeassistant.components.sensor import ( SensorEntity, SensorEntityDescription, ) +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_MONITORED_CONDITIONS, CONF_NAME -from homeassistant.core import HomeAssistant +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant +from homeassistant.data_entry_flow import FlowResultType import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from homeassistant.util import Throttle -from homeassistant.util.dt import get_time_zone +from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import CONF_HOUSE_NUMBER, CONF_HOUSE_NUMBER_SUFFIX, CONF_ZIP_CODE, LOGGER +from .const import CONF_HOUSE_NUMBER, CONF_HOUSE_NUMBER_SUFFIX, CONF_ZIP_CODE, DOMAIN +from .coordinator import RovaCoordinator -UPDATE_DELAY = timedelta(hours=12) -SCAN_INTERVAL = timedelta(hours=12) +ISSUE_PLACEHOLDER = {"url": "/config/integrations/dashboard/add?domain=rova"} - -SENSOR_TYPES: dict[str, SensorEntityDescription] = { - "bio": SensorEntityDescription( +SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( + SensorEntityDescription( key="gft", - name="bio", - icon="mdi:recycle", + translation_key="bio", ), - "paper": SensorEntityDescription( + SensorEntityDescription( key="papier", - name="paper", - icon="mdi:recycle", + translation_key="paper", ), - "plastic": SensorEntityDescription( + SensorEntityDescription( key="pmd", - name="plastic", - icon="mdi:recycle", + translation_key="plastic", ), - "residual": SensorEntityDescription( + SensorEntityDescription( key="restafval", - name="residual", - icon="mdi:recycle", + translation_key="residual", ), -} +) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -63,88 +60,88 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -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: - """Create the Rova data service and sensors.""" - - zip_code = config[CONF_ZIP_CODE] - house_number = config[CONF_HOUSE_NUMBER] - house_number_suffix = config[CONF_HOUSE_NUMBER_SUFFIX] - platform_name = config[CONF_NAME] - - # Create new Rova object to retrieve data - api = Rova(zip_code, house_number, house_number_suffix) - - try: - if not api.is_rova_area(): - LOGGER.error("ROVA does not collect garbage in this area") - return - except (ConnectTimeout, HTTPError): - LOGGER.error("Could not retrieve details from ROVA API") - return - - # Create rova data service which will retrieve and update the data. - data_service = RovaData(api) - - # Create a new sensor for each garbage type. - entities = [ - RovaSensor(platform_name, SENSOR_TYPES[sensor_key], data_service) - for sensor_key in config[CONF_MONITORED_CONDITIONS] - ] - add_entities(entities, True) + """Set up the rova sensor platform through yaml configuration.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=config, + ) + if ( + result["type"] == FlowResultType.CREATE_ENTRY + or result["reason"] == "already_configured" + ): + async_create_issue( + hass, + HOMEASSISTANT_DOMAIN, + f"deprecated_yaml_{DOMAIN}", + breaks_in_ha_version="2024.10.0", + is_fixable=False, + issue_domain=DOMAIN, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": "Rova", + }, + ) + else: + async_create_issue( + hass, + DOMAIN, + f"deprecated_yaml_import_issue_{result['reason']}", + breaks_in_ha_version="2024.10.0", + is_fixable=False, + issue_domain=DOMAIN, + severity=IssueSeverity.WARNING, + translation_key=f"deprecated_yaml_import_issue_{result['reason']}", + translation_placeholders=ISSUE_PLACEHOLDER, + ) -class RovaSensor(SensorEntity): +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Add Rova entry.""" + coordinator: RovaCoordinator = hass.data[DOMAIN][entry.entry_id] + + assert entry.unique_id + unique_id = entry.unique_id + + async_add_entities( + RovaSensor(unique_id, description, coordinator) for description in SENSOR_TYPES + ) + + +class RovaSensor(CoordinatorEntity[RovaCoordinator], SensorEntity): """Representation of a Rova sensor.""" + _attr_device_class = SensorDeviceClass.TIMESTAMP + _attr_has_entity_name = True + def __init__( - self, platform_name, description: SensorEntityDescription, data_service + self, + unique_id: str, + description: SensorEntityDescription, + coordinator: RovaCoordinator, ) -> None: """Initialize the sensor.""" + super().__init__(coordinator) self.entity_description = description - self.data_service = data_service + self._attr_unique_id = f"{unique_id}_{description.key}" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, unique_id)}, + entry_type=DeviceEntryType.SERVICE, + ) - self._attr_name = f"{platform_name}_{description.name}" - self._attr_device_class = SensorDeviceClass.TIMESTAMP - - def update(self) -> None: - """Get the latest data from the sensor and update the state.""" - self.data_service.update() - pickup_date = self.data_service.data.get(self.entity_description.key) - if pickup_date is not None: - self._attr_native_value = pickup_date - - -class RovaData: - """Get and update the latest data from the Rova API.""" - - def __init__(self, api): - """Initialize the data object.""" - self.api = api - self.data = {} - - @Throttle(UPDATE_DELAY) - def update(self): - """Update the data from the Rova API.""" - - try: - items = self.api.get_calendar_items() - except (ConnectTimeout, HTTPError): - LOGGER.error("Could not retrieve data, retry again later") - return - - self.data = {} - - for item in items: - date = datetime.strptime(item["Date"], "%Y-%m-%dT%H:%M:%S").replace( - tzinfo=get_time_zone("Europe/Amsterdam") - ) - code = item["GarbageTypeCode"].lower() - if code not in self.data: - self.data[code] = date - - LOGGER.debug("Updated Rova calendar: %s", self.data) + @property + def native_value(self) -> datetime | None: + """Return the state of the sensor.""" + return self.coordinator.data.get(self.entity_description.key) diff --git a/homeassistant/components/rova/strings.json b/homeassistant/components/rova/strings.json new file mode 100644 index 00000000000..709e5450411 --- /dev/null +++ b/homeassistant/components/rova/strings.json @@ -0,0 +1,53 @@ +{ + "config": { + "step": { + "user": { + "title": "Provide your address details", + "data": { + "zip_code": "Your zip code", + "house_number": "Your house number", + "house_number_suffix": "A suffix for your house number" + } + } + }, + "error": { + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]", + "invalid_rova_area": "Rova does not collect at this address" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]", + "cannot_connect": "Could not connect to the Rova API", + "invalid_rova_area": "Rova does not collect at this address" + } + }, + "issues": { + "deprecated_yaml_import_issue_cannot_connect": { + "title": "The Rova YAML configuration import failed", + "description": "Configuring Rova using YAML is being removed but there was a connection error importing your YAML configuration.\n\nEnsure connection to Rova works and restart Home Assistant to try again or remove the Rova YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually." + }, + "deprecated_yaml_import_issue_invalid_rova_area": { + "title": "The Rova YAML configuration import failed", + "description": "There was an error when trying to import your Rova YAML configuration.\n\nRova does not collect at this address.\n\nEnsure the imported configuration is correct and remove the Rova YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually." + }, + "no_rova_area": { + "title": "Rova does not collect at this address anymore", + "description": "Rova does not collect at {zip_code} anymore.\n\nPlease remove the integration." + } + }, + "entity": { + "sensor": { + "bio": { + "name": "Bio" + }, + "paper": { + "name": "Paper" + }, + "plastic": { + "name": "Plastic" + }, + "residual": { + "name": "Residual" + } + } + } +} diff --git a/homeassistant/components/rpi_camera/__init__.py b/homeassistant/components/rpi_camera/__init__.py index 15c59476ab2..31b667d2b4e 100644 --- a/homeassistant/components/rpi_camera/__init__.py +++ b/homeassistant/components/rpi_camera/__init__.py @@ -1,4 +1,5 @@ """The rpi_camera component.""" + import voluptuous as vol from homeassistant.const import CONF_FILE_PATH, CONF_NAME, Platform diff --git a/homeassistant/components/rpi_camera/camera.py b/homeassistant/components/rpi_camera/camera.py index 3f9b5fd5860..a8ebaaaca6f 100644 --- a/homeassistant/components/rpi_camera/camera.py +++ b/homeassistant/components/rpi_camera/camera.py @@ -1,4 +1,5 @@ """Camera platform that has a Raspberry Pi camera.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/rpi_power/__init__.py b/homeassistant/components/rpi_power/__init__.py index 79504f17d65..31925784629 100644 --- a/homeassistant/components/rpi_power/__init__.py +++ b/homeassistant/components/rpi_power/__init__.py @@ -1,4 +1,5 @@ """The Raspberry Pi Power Supply Checker integration.""" + from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/rpi_power/binary_sensor.py b/homeassistant/components/rpi_power/binary_sensor.py index b5337923d6c..a7306899bde 100644 --- a/homeassistant/components/rpi_power/binary_sensor.py +++ b/homeassistant/components/rpi_power/binary_sensor.py @@ -2,6 +2,7 @@ Minimal Kernel needed is 4.14+ """ + import logging from rpi_bad_power import UnderVoltage, new_under_voltage diff --git a/homeassistant/components/rpi_power/config_flow.py b/homeassistant/components/rpi_power/config_flow.py index 97814c2a866..c44bb65d79a 100644 --- a/homeassistant/components/rpi_power/config_flow.py +++ b/homeassistant/components/rpi_power/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Raspberry Pi Power Supply Checker.""" + from __future__ import annotations from collections.abc import Awaitable @@ -6,8 +7,8 @@ from typing import Any from rpi_bad_power import new_under_voltage +from homeassistant.config_entries import ConfigFlowResult from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.config_entry_flow import DiscoveryFlowHandler from .const import DOMAIN @@ -34,7 +35,7 @@ class RPiPowerFlow(DiscoveryFlowHandler[Awaitable[bool]], domain=DOMAIN): async def async_step_onboarding( self, data: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by onboarding.""" if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") diff --git a/homeassistant/components/rss_feed_template/__init__.py b/homeassistant/components/rss_feed_template/__init__.py index 7b7cbb7e94f..8d2e47315ef 100644 --- a/homeassistant/components/rss_feed_template/__init__.py +++ b/homeassistant/components/rss_feed_template/__init__.py @@ -1,4 +1,5 @@ """Support to export sensor values via RSS feed.""" + from __future__ import annotations from html import escape diff --git a/homeassistant/components/rtorrent/sensor.py b/homeassistant/components/rtorrent/sensor.py index 2ef6786153a..099927f1893 100644 --- a/homeassistant/components/rtorrent/sensor.py +++ b/homeassistant/components/rtorrent/sensor.py @@ -1,4 +1,5 @@ """Support for monitoring the rtorrent BitTorrent client API.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/rtsp_to_webrtc/config_flow.py b/homeassistant/components/rtsp_to_webrtc/config_flow.py index f66a0a30d8c..adab1a456d0 100644 --- a/homeassistant/components/rtsp_to_webrtc/config_flow.py +++ b/homeassistant/components/rtsp_to_webrtc/config_flow.py @@ -1,4 +1,5 @@ """Config flow for RTSPtoWebRTC.""" + from __future__ import annotations import logging @@ -8,11 +9,15 @@ from urllib.parse import urlparse import rtsp_to_webrtc import voluptuous as vol -from homeassistant import config_entries from homeassistant.components.hassio import HassioServiceInfo +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from . import CONF_STUN_SERVER, DATA_SERVER_URL, DOMAIN @@ -22,14 +27,14 @@ _LOGGER = logging.getLogger(__name__) DATA_SCHEMA = vol.Schema({vol.Required(DATA_SERVER_URL): str}) -class RTSPToWebRTCConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class RTSPToWebRTCConfigFlow(ConfigFlow, domain=DOMAIN): """RTSPtoWebRTC config flow.""" _hassio_discovery: dict[str, Any] async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Configure the RTSPtoWebRTC server url.""" if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") @@ -72,7 +77,9 @@ class RTSPToWebRTCConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return "server_unreachable" return None - async def async_step_hassio(self, discovery_info: HassioServiceInfo) -> FlowResult: + async def async_step_hassio( + self, discovery_info: HassioServiceInfo + ) -> ConfigFlowResult: """Prepare configuration for the RTSPtoWebRTC server add-on discovery.""" if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") @@ -82,7 +89,7 @@ class RTSPToWebRTCConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_hassio_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm Add-on discovery.""" errors = None if user_input is not None: @@ -109,22 +116,22 @@ class RTSPToWebRTCConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback def async_get_options_flow( - config_entry: config_entries.ConfigEntry, - ) -> config_entries.OptionsFlow: + config_entry: ConfigEntry, + ) -> OptionsFlow: """Create an options flow.""" return OptionsFlowHandler(config_entry) -class OptionsFlowHandler(config_entries.OptionsFlow): +class OptionsFlowHandler(OptionsFlow): """RTSPtoWeb Options flow.""" - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + 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: + ) -> ConfigFlowResult: """Manage the options.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) diff --git a/homeassistant/components/ruckus_unleashed/__init__.py b/homeassistant/components/ruckus_unleashed/__init__.py index 63521a622cd..c2c46fcc125 100644 --- a/homeassistant/components/ruckus_unleashed/__init__.py +++ b/homeassistant/components/ruckus_unleashed/__init__.py @@ -1,4 +1,5 @@ """The Ruckus Unleashed integration.""" + import logging from aioruckus import AjaxSession diff --git a/homeassistant/components/ruckus_unleashed/config_flow.py b/homeassistant/components/ruckus_unleashed/config_flow.py index c11e9cbe89f..1a75b8ae139 100644 --- a/homeassistant/components/ruckus_unleashed/config_flow.py +++ b/homeassistant/components/ruckus_unleashed/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Ruckus Unleashed integration.""" + from collections.abc import Mapping import logging from typing import Any @@ -7,9 +8,10 @@ from aioruckus import AjaxSession, SystemStat from aioruckus.exceptions import AuthenticationError, SchemaError import voluptuous as vol -from homeassistant import config_entries, core, exceptions +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME -from homeassistant.data_entry_flow import FlowResult +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from .const import ( API_MESH_NAME, @@ -31,7 +33,7 @@ DATA_SCHEMA = vol.Schema( ) -async def validate_input(hass: core.HomeAssistant, data): +async def validate_input(hass: HomeAssistant, data): """Validate the user input allows us to connect. Data has the keys from DATA_SCHEMA with values provided by the user. @@ -57,16 +59,16 @@ async def validate_input(hass: core.HomeAssistant, data): } -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class RuckusUnleashedConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Ruckus Unleashed.""" VERSION = 1 - _reauth_entry: config_entries.ConfigEntry | None = None + _reauth_entry: ConfigEntry | None = None async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors = {} if user_input is not None: @@ -105,7 +107,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="user", data_schema=data_schema, errors=errors ) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Perform reauth upon an API authentication error.""" self._reauth_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] @@ -113,9 +117,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_user() -class CannotConnect(exceptions.HomeAssistantError): +class CannotConnect(HomeAssistantError): """Error to indicate we cannot connect.""" -class InvalidAuth(exceptions.HomeAssistantError): +class InvalidAuth(HomeAssistantError): """Error to indicate there is invalid auth.""" diff --git a/homeassistant/components/ruckus_unleashed/const.py b/homeassistant/components/ruckus_unleashed/const.py index 089981348b6..9076437b8c7 100644 --- a/homeassistant/components/ruckus_unleashed/const.py +++ b/homeassistant/components/ruckus_unleashed/const.py @@ -1,4 +1,5 @@ """Constants for the Ruckus Unleashed integration.""" + from homeassistant.const import Platform DOMAIN = "ruckus_unleashed" diff --git a/homeassistant/components/ruckus_unleashed/coordinator.py b/homeassistant/components/ruckus_unleashed/coordinator.py index 7c11aac7f68..989748af86e 100644 --- a/homeassistant/components/ruckus_unleashed/coordinator.py +++ b/homeassistant/components/ruckus_unleashed/coordinator.py @@ -1,4 +1,5 @@ """Ruckus Unleashed DataUpdateCoordinator.""" + from datetime import timedelta import logging diff --git a/homeassistant/components/ruckus_unleashed/device_tracker.py b/homeassistant/components/ruckus_unleashed/device_tracker.py index 89cc22ef766..233e5cd4945 100644 --- a/homeassistant/components/ruckus_unleashed/device_tracker.py +++ b/homeassistant/components/ruckus_unleashed/device_tracker.py @@ -1,4 +1,5 @@ """Support for Ruckus Unleashed devices.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/russound_rio/media_player.py b/homeassistant/components/russound_rio/media_player.py index fc0dca341a8..74339153f69 100644 --- a/homeassistant/components/russound_rio/media_player.py +++ b/homeassistant/components/russound_rio/media_player.py @@ -1,4 +1,5 @@ """Support for Russound multizone controllers using RIO Protocol.""" + from __future__ import annotations from russound_rio import Russound diff --git a/homeassistant/components/russound_rnet/media_player.py b/homeassistant/components/russound_rnet/media_player.py index b19f4b9dfee..3b061d5a503 100644 --- a/homeassistant/components/russound_rnet/media_player.py +++ b/homeassistant/components/russound_rnet/media_player.py @@ -1,4 +1,5 @@ """Support for interfacing with Russound via RNET Protocol.""" + from __future__ import annotations import logging @@ -57,9 +58,7 @@ def setup_platform( russ = russound.Russound(host, port) russ.connect() - sources = [] - for source in config[CONF_SOURCES]: - sources.append(source["name"]) + sources = [source["name"] for source in config[CONF_SOURCES]] if russ.is_connected(): for zone_id, extra in config[CONF_ZONES].items(): diff --git a/homeassistant/components/ruuvi_gateway/__init__.py b/homeassistant/components/ruuvi_gateway/__init__.py index 59d37abbf7b..77b3e9b57de 100644 --- a/homeassistant/components/ruuvi_gateway/__init__.py +++ b/homeassistant/components/ruuvi_gateway/__init__.py @@ -1,4 +1,5 @@ """The Ruuvi Gateway integration.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/ruuvi_gateway/bluetooth.py b/homeassistant/components/ruuvi_gateway/bluetooth.py index c4fbe474776..bdd1e21d491 100644 --- a/homeassistant/components/ruuvi_gateway/bluetooth.py +++ b/homeassistant/components/ruuvi_gateway/bluetooth.py @@ -1,4 +1,5 @@ """Bluetooth support for Ruuvi Gateway.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/ruuvi_gateway/config_flow.py b/homeassistant/components/ruuvi_gateway/config_flow.py index 178c55a53e4..825f57b2cf2 100644 --- a/homeassistant/components/ruuvi_gateway/config_flow.py +++ b/homeassistant/components/ruuvi_gateway/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Ruuvi Gateway integration.""" + from __future__ import annotations import logging @@ -7,10 +8,9 @@ from typing import Any import aioruuvigateway.api as gw_api from aioruuvigateway.excs import CannotConnect, InvalidAuth -from homeassistant import config_entries from homeassistant.components import dhcp +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_TOKEN -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.device_registry import format_mac from homeassistant.helpers.httpx_client import get_async_client @@ -20,7 +20,7 @@ from .schemata import CONFIG_SCHEMA, get_config_schema_with_default_host _LOGGER = logging.getLogger(__name__) -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class RuuviConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Ruuvi Gateway.""" VERSION = 1 @@ -33,7 +33,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def _async_validate( self, user_input: dict[str, Any], - ) -> tuple[FlowResult | None, dict[str, str]]: + ) -> tuple[ConfigFlowResult | None, dict[str, str]]: """Validate configuration (either discovered or user input).""" errors: dict[str, str] = {} @@ -67,7 +67,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None, - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle requesting or validating user input.""" if user_input is not None: result, errors = await self._async_validate(user_input) @@ -81,7 +81,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors=(errors or None), ) - async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: + async def async_step_dhcp( + self, discovery_info: dhcp.DhcpServiceInfo + ) -> ConfigFlowResult: """Prepare configuration for a DHCP discovered Ruuvi Gateway.""" await self.async_set_unique_id(format_mac(discovery_info.macaddress)) self._abort_if_unique_id_configured(updates={CONF_HOST: discovery_info.ip}) diff --git a/homeassistant/components/ruuvi_gateway/const.py b/homeassistant/components/ruuvi_gateway/const.py index 80bebda105b..a323b911d1f 100644 --- a/homeassistant/components/ruuvi_gateway/const.py +++ b/homeassistant/components/ruuvi_gateway/const.py @@ -1,4 +1,5 @@ """Constants for the Ruuvi Gateway integration.""" + from datetime import timedelta DOMAIN = "ruuvi_gateway" diff --git a/homeassistant/components/ruuvi_gateway/coordinator.py b/homeassistant/components/ruuvi_gateway/coordinator.py index 38bc3b0e201..ba72dfe4cbc 100644 --- a/homeassistant/components/ruuvi_gateway/coordinator.py +++ b/homeassistant/components/ruuvi_gateway/coordinator.py @@ -1,4 +1,5 @@ """Update coordinator for Ruuvi Gateway.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/ruuvi_gateway/models.py b/homeassistant/components/ruuvi_gateway/models.py index adb405f0bf8..3717ffdb25a 100644 --- a/homeassistant/components/ruuvi_gateway/models.py +++ b/homeassistant/components/ruuvi_gateway/models.py @@ -1,4 +1,5 @@ """Models for Ruuvi Gateway integration.""" + from __future__ import annotations import dataclasses diff --git a/homeassistant/components/ruuvi_gateway/schemata.py b/homeassistant/components/ruuvi_gateway/schemata.py index eec86cd129f..4662e07acbf 100644 --- a/homeassistant/components/ruuvi_gateway/schemata.py +++ b/homeassistant/components/ruuvi_gateway/schemata.py @@ -1,4 +1,5 @@ """Schemata for ruuvi_gateway.""" + from __future__ import annotations import voluptuous as vol diff --git a/homeassistant/components/ruuvitag_ble/__init__.py b/homeassistant/components/ruuvitag_ble/__init__.py index 5e30820f837..86a0b2cd40a 100644 --- a/homeassistant/components/ruuvitag_ble/__init__.py +++ b/homeassistant/components/ruuvitag_ble/__init__.py @@ -1,4 +1,5 @@ """The ruuvitag_ble integration.""" + from __future__ import annotations import logging @@ -25,14 +26,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: address = entry.unique_id assert address is not None data = RuuvitagBluetoothDeviceData() - coordinator = hass.data.setdefault(DOMAIN, {})[ - entry.entry_id - ] = PassiveBluetoothProcessorCoordinator( - hass, - _LOGGER, - address=address, - mode=BluetoothScanningMode.ACTIVE, - update_method=data.update, + coordinator = hass.data.setdefault(DOMAIN, {})[entry.entry_id] = ( + PassiveBluetoothProcessorCoordinator( + hass, + _LOGGER, + address=address, + mode=BluetoothScanningMode.ACTIVE, + update_method=data.update, + ) ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload( diff --git a/homeassistant/components/ruuvitag_ble/config_flow.py b/homeassistant/components/ruuvitag_ble/config_flow.py index 620b901f4fe..564f3c9d0e2 100644 --- a/homeassistant/components/ruuvitag_ble/config_flow.py +++ b/homeassistant/components/ruuvitag_ble/config_flow.py @@ -1,4 +1,5 @@ """Config flow for ruuvitag_ble.""" + from __future__ import annotations from typing import Any @@ -10,9 +11,8 @@ from homeassistant.components.bluetooth import ( BluetoothServiceInfoBleak, async_discovered_service_info, ) -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_ADDRESS -from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN @@ -30,7 +30,7 @@ class RuuvitagConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_bluetooth( self, discovery_info: BluetoothServiceInfoBleak - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the bluetooth discovery step.""" await self.async_set_unique_id(discovery_info.address) self._abort_if_unique_id_configured() @@ -43,7 +43,7 @@ class RuuvitagConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_bluetooth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm discovery.""" assert self._discovered_device is not None device = self._discovered_device @@ -62,7 +62,7 @@ class RuuvitagConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the user step to pick discovered device.""" if user_input is not None: address = user_input[CONF_ADDRESS] diff --git a/homeassistant/components/ruuvitag_ble/sensor.py b/homeassistant/components/ruuvitag_ble/sensor.py index 128edd42b19..a098c263c5d 100644 --- a/homeassistant/components/ruuvitag_ble/sensor.py +++ b/homeassistant/components/ruuvitag_ble/sensor.py @@ -1,4 +1,5 @@ """Support for RuuviTag sensors.""" + from __future__ import annotations from sensor_state_data import ( diff --git a/homeassistant/components/rympro/__init__.py b/homeassistant/components/rympro/__init__.py index 7ac6de2fa0b..f24735f4ed0 100644 --- a/homeassistant/components/rympro/__init__.py +++ b/homeassistant/components/rympro/__init__.py @@ -1,4 +1,5 @@ """The Read Your Meter Pro integration.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/rympro/config_flow.py b/homeassistant/components/rympro/config_flow.py index b954bb10c57..f30e47f09a1 100644 --- a/homeassistant/components/rympro/config_flow.py +++ b/homeassistant/components/rympro/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Read Your Meter Pro integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -8,10 +9,9 @@ from typing import Any from pyrympro import CannotConnectError, RymPro, UnauthorizedError import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_TOKEN, CONF_UNIQUE_ID from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN @@ -41,18 +41,18 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, return {CONF_TOKEN: token, CONF_UNIQUE_ID: info["accountNumber"]} -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class RymproConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Read Your Meter Pro.""" VERSION = 1 def __init__(self) -> None: """Init the config flow.""" - self._reauth_entry: config_entries.ConfigEntry | None = None + self._reauth_entry: ConfigEntry | None = None async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" if user_input is None: return self.async_show_form( @@ -92,7 +92,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors ) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Handle configuration by re-auth.""" self._reauth_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] diff --git a/homeassistant/components/rympro/coordinator.py b/homeassistant/components/rympro/coordinator.py index 592c82adc68..f2e5162a0f0 100644 --- a/homeassistant/components/rympro/coordinator.py +++ b/homeassistant/components/rympro/coordinator.py @@ -1,4 +1,5 @@ """The Read Your Meter Pro integration.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/rympro/sensor.py b/homeassistant/components/rympro/sensor.py index a6b5b8df93d..8bb0af6e9ff 100644 --- a/homeassistant/components/rympro/sensor.py +++ b/homeassistant/components/rympro/sensor.py @@ -1,4 +1,5 @@ """Sensor for RymPro meters.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/homeassistant/components/sabnzbd/__init__.py b/homeassistant/components/sabnzbd/__init__.py index 7d0437da033..6a68f98203b 100644 --- a/homeassistant/components/sabnzbd/__init__.py +++ b/homeassistant/components/sabnzbd/__init__.py @@ -1,4 +1,5 @@ """Support for monitoring an SABnzbd NZB client.""" + from __future__ import annotations from collections.abc import Callable, Coroutine diff --git a/homeassistant/components/sabnzbd/config_flow.py b/homeassistant/components/sabnzbd/config_flow.py index d433b562183..944c3f2936c 100644 --- a/homeassistant/components/sabnzbd/config_flow.py +++ b/homeassistant/components/sabnzbd/config_flow.py @@ -1,4 +1,5 @@ """Adds config flow for SabNzbd.""" + from __future__ import annotations import logging @@ -6,7 +7,7 @@ from typing import Any import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import ( CONF_API_KEY, CONF_HOST, @@ -15,7 +16,6 @@ from homeassistant.const import ( CONF_SSL, CONF_URL, ) -from homeassistant.data_entry_flow import FlowResult from .const import DEFAULT_NAME, DOMAIN from .sab import get_client @@ -31,7 +31,7 @@ USER_SCHEMA = vol.Schema( ) -class SABnzbdConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class SABnzbdConfigFlow(ConfigFlow, domain=DOMAIN): """Sabnzbd config flow.""" VERSION = 1 @@ -47,7 +47,7 @@ class SABnzbdConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" errors = {} @@ -68,7 +68,7 @@ class SABnzbdConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_import(self, import_data): """Import sabnzbd config from configuration.yaml.""" protocol = "https://" if import_data[CONF_SSL] else "http://" - import_data[ - CONF_URL - ] = f"{protocol}{import_data[CONF_HOST]}:{import_data[CONF_PORT]}" + import_data[CONF_URL] = ( + f"{protocol}{import_data[CONF_HOST]}:{import_data[CONF_PORT]}" + ) return await self.async_step_user(import_data) diff --git a/homeassistant/components/sabnzbd/const.py b/homeassistant/components/sabnzbd/const.py index 8add1f61493..a9cd80898f7 100644 --- a/homeassistant/components/sabnzbd/const.py +++ b/homeassistant/components/sabnzbd/const.py @@ -1,4 +1,5 @@ """Constants for the Sabnzbd component.""" + from datetime import timedelta DOMAIN = "sabnzbd" diff --git a/homeassistant/components/sabnzbd/icons.json b/homeassistant/components/sabnzbd/icons.json new file mode 100644 index 00000000000..a693e9fec86 --- /dev/null +++ b/homeassistant/components/sabnzbd/icons.json @@ -0,0 +1,7 @@ +{ + "services": { + "pause": "mdi:pause", + "resume": "mdi:play", + "set_speed": "mdi:speedometer" + } +} diff --git a/homeassistant/components/sabnzbd/sab.py b/homeassistant/components/sabnzbd/sab.py index af70e4b8afc..0114e14e516 100644 --- a/homeassistant/components/sabnzbd/sab.py +++ b/homeassistant/components/sabnzbd/sab.py @@ -1,4 +1,5 @@ """Support for the Sabnzbd service.""" + from pysabnzbd import SabnzbdApi, SabnzbdApiException from homeassistant.const import CONF_API_KEY, CONF_URL diff --git a/homeassistant/components/sabnzbd/sensor.py b/homeassistant/components/sabnzbd/sensor.py index ff33c084ffa..d5f19b5e718 100644 --- a/homeassistant/components/sabnzbd/sensor.py +++ b/homeassistant/components/sabnzbd/sensor.py @@ -1,4 +1,5 @@ """Support for monitoring an SABnzbd NZB client.""" + from __future__ import annotations from dataclasses import dataclass @@ -20,18 +21,13 @@ from . import DOMAIN, SIGNAL_SABNZBD_UPDATED from .const import DEFAULT_NAME, KEY_API_DATA -@dataclass(frozen=True) -class SabnzbdRequiredKeysMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class SabnzbdSensorEntityDescription(SensorEntityDescription): + """Describes Sabnzbd sensor entity.""" key: str -@dataclass(frozen=True) -class SabnzbdSensorEntityDescription(SensorEntityDescription, SabnzbdRequiredKeysMixin): - """Describes Sabnzbd sensor entity.""" - - SPEED_KEY = "kbpersec" SENSOR_TYPES: tuple[SabnzbdSensorEntityDescription, ...] = ( diff --git a/homeassistant/components/saj/sensor.py b/homeassistant/components/saj/sensor.py index 866279af973..75b56c98ac3 100644 --- a/homeassistant/components/saj/sensor.py +++ b/homeassistant/components/saj/sensor.py @@ -1,4 +1,5 @@ """SAJ solar inverter interface.""" + from __future__ import annotations from collections.abc import Callable, Coroutine @@ -78,7 +79,7 @@ async def async_setup_platform( sensor_def = pysaj.Sensors(wifi) # Use all sensors by default - hass_sensors = [] + hass_sensors: list[SAJsensor] = [] kwargs = {} if wifi: @@ -102,11 +103,11 @@ async def async_setup_platform( if not done: raise PlatformNotReady - for sensor in sensor_def: - if sensor.enabled: - hass_sensors.append( - SAJsensor(saj.serialnumber, sensor, inverter_name=config.get(CONF_NAME)) - ) + hass_sensors.extend( + SAJsensor(saj.serialnumber, sensor, inverter_name=config.get(CONF_NAME)) + for sensor in sensor_def + if sensor.enabled + ) async_add_entities(hass_sensors) diff --git a/homeassistant/components/samsungtv/__init__.py b/homeassistant/components/samsungtv/__init__.py index 56fd230fd6f..68a58710c19 100644 --- a/homeassistant/components/samsungtv/__init__.py +++ b/homeassistant/components/samsungtv/__init__.py @@ -1,4 +1,5 @@ """The Samsung TV integration.""" + from __future__ import annotations from collections.abc import Coroutine, Mapping @@ -148,7 +149,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await bridge.async_close_remote() entry.async_on_unload( - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_bridge) + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, stop_bridge, run_immediately=True + ) ) await _async_update_ssdp_locations(hass, entry) diff --git a/homeassistant/components/samsungtv/bridge.py b/homeassistant/components/samsungtv/bridge.py index f2767ce693e..817437ef4d6 100644 --- a/homeassistant/components/samsungtv/bridge.py +++ b/homeassistant/components/samsungtv/bridge.py @@ -1,4 +1,5 @@ """samsungctl and samsungtvws bridge classes.""" + from __future__ import annotations from abc import ABC, abstractmethod diff --git a/homeassistant/components/samsungtv/config_flow.py b/homeassistant/components/samsungtv/config_flow.py index e7f71210dfe..4845fb4fb74 100644 --- a/homeassistant/components/samsungtv/config_flow.py +++ b/homeassistant/components/samsungtv/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Samsung TV.""" + from __future__ import annotations from collections.abc import Mapping @@ -11,8 +12,13 @@ import getmac from samsungtvws.encrypted.authenticator import SamsungTVEncryptedWSAsyncAuthenticator import voluptuous as vol -from homeassistant import config_entries from homeassistant.components import dhcp, ssdp, zeroconf +from homeassistant.config_entries import ( + ConfigEntry, + ConfigEntryState, + ConfigFlow, + ConfigFlowResult, +) from homeassistant.const import ( CONF_HOST, CONF_MAC, @@ -23,7 +29,7 @@ from homeassistant.const import ( CONF_TOKEN, ) from homeassistant.core import callback -from homeassistant.data_entry_flow import AbortFlow, FlowResult +from homeassistant.data_entry_flow import AbortFlow from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.device_registry import format_mac @@ -57,7 +63,7 @@ def _strip_uuid(udn: str) -> str: def _entry_is_complete( - entry: config_entries.ConfigEntry, + entry: ConfigEntry, ssdp_rendering_control_location: str | None, ssdp_main_tv_agent_location: str | None, ) -> bool: @@ -91,14 +97,14 @@ def _mac_is_same_with_incorrect_formatting( ) -class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class SamsungTVConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a Samsung TV config flow.""" VERSION = 2 def __init__(self) -> None: """Initialize flow.""" - self._reauth_entry: config_entries.ConfigEntry | None = None + self._reauth_entry: ConfigEntry | None = None self._host: str = "" self._mac: str | None = None self._udn: str | None = None @@ -131,7 +137,7 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): CONF_SSDP_MAIN_TV_AGENT_LOCATION: self._ssdp_main_tv_agent_location, } - def _get_entry_from_bridge(self) -> FlowResult: + def _get_entry_from_bridge(self) -> ConfigFlowResult: """Get device entry.""" assert self._bridge data = self._base_config_entry() @@ -178,13 +184,13 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if self._model: updates[CONF_MODEL] = self._model if self._ssdp_rendering_control_location: - updates[ - CONF_SSDP_RENDERING_CONTROL_LOCATION - ] = self._ssdp_rendering_control_location + updates[CONF_SSDP_RENDERING_CONTROL_LOCATION] = ( + self._ssdp_rendering_control_location + ) if self._ssdp_main_tv_agent_location: - updates[ - CONF_SSDP_MAIN_TV_AGENT_LOCATION - ] = self._ssdp_main_tv_agent_location + updates[CONF_SSDP_MAIN_TV_AGENT_LOCATION] = ( + self._ssdp_main_tv_agent_location + ) self._abort_if_unique_id_configured(updates=updates, reload_on_update=False) async def _async_create_bridge(self) -> None: @@ -252,7 +258,7 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" if user_input is not None: await self._async_set_name_host_from_input(user_input) @@ -270,7 +276,7 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_pairing( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a pairing by accepting the message on the TV.""" assert self._bridge is not None errors: dict[str, str] = {} @@ -292,7 +298,7 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_encrypted_pairing( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a encrypted pairing.""" assert self._host is not None await self._async_start_encrypted_pairing(self._host) @@ -326,9 +332,9 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @callback def _async_get_existing_matching_entry( self, - ) -> tuple[config_entries.ConfigEntry | None, bool]: + ) -> tuple[ConfigEntry | None, bool]: """Get first existing matching entry (prefer unique id).""" - matching_host_entry: config_entries.ConfigEntry | None = None + matching_host_entry: ConfigEntry | None = None for entry in self._async_current_entries(include_ignore=False): if (self._mac and self._mac == entry.data.get(CONF_MAC)) or ( self._upnp_udn and self._upnp_udn == entry.unique_id @@ -345,7 +351,7 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @callback def _async_update_existing_matching_entry( self, - ) -> config_entries.ConfigEntry | None: + ) -> ConfigEntry | None: """Check existing entries and update them. Returns the existing entry if it was updated. @@ -382,13 +388,13 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): or update_model ): if update_ssdp_rendering_control_location: - data[ - CONF_SSDP_RENDERING_CONTROL_LOCATION - ] = self._ssdp_rendering_control_location + data[CONF_SSDP_RENDERING_CONTROL_LOCATION] = ( + self._ssdp_rendering_control_location + ) if update_ssdp_main_tv_agent_location: - data[ - CONF_SSDP_MAIN_TV_AGENT_LOCATION - ] = self._ssdp_main_tv_agent_location + data[CONF_SSDP_MAIN_TV_AGENT_LOCATION] = ( + self._ssdp_main_tv_agent_location + ) if update_mac: data[CONF_MAC] = self._mac if update_model: @@ -398,7 +404,7 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return None LOGGER.debug("Updating existing config entry with %s", entry_kw_args) self.hass.config_entries.async_update_entry(entry, **entry_kw_args) - if entry.state != config_entries.ConfigEntryState.LOADED: + if entry.state != ConfigEntryState.LOADED: # If its loaded it already has a reload listener in place # and we do not want to trigger multiple reloads self.hass.async_create_task( @@ -430,7 +436,9 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ): raise AbortFlow(RESULT_NOT_SUPPORTED) - async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: + async def async_step_ssdp( + self, discovery_info: ssdp.SsdpServiceInfo + ) -> ConfigFlowResult: """Handle a flow initialized by ssdp discovery.""" LOGGER.debug("Samsung device found via SSDP: %s", discovery_info) model_name: str = discovery_info.upnp.get(ssdp.ATTR_UPNP_MODEL_NAME) or "" @@ -475,7 +483,9 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.context["title_placeholders"] = {"device": self._title} return await self.async_step_confirm() - async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: + async def async_step_dhcp( + self, discovery_info: dhcp.DhcpServiceInfo + ) -> ConfigFlowResult: """Handle a flow initialized by dhcp discovery.""" LOGGER.debug("Samsung device found via DHCP: %s", discovery_info) self._mac = format_mac(discovery_info.macaddress) @@ -487,7 +497,7 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_zeroconf( self, discovery_info: zeroconf.ZeroconfServiceInfo - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by zeroconf discovery.""" LOGGER.debug("Samsung device found via ZEROCONF: %s", discovery_info) self._mac = format_mac(discovery_info.properties["deviceid"]) @@ -499,7 +509,7 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle user-confirmation of discovered node.""" if user_input is not None: await self._async_create_bridge() @@ -512,7 +522,9 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="confirm", description_placeholders={"device": self._title} ) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Handle configuration by re-auth.""" self._reauth_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] @@ -525,7 +537,7 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm reauth.""" errors = {} assert self._reauth_entry @@ -569,7 +581,7 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_reauth_confirm_encrypted( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm reauth (encrypted method).""" errors = {} assert self._reauth_entry diff --git a/homeassistant/components/samsungtv/const.py b/homeassistant/components/samsungtv/const.py index 6c657145d7a..9fe8fb58cbd 100644 --- a/homeassistant/components/samsungtv/const.py +++ b/homeassistant/components/samsungtv/const.py @@ -1,4 +1,5 @@ """Constants for the Samsung TV integration.""" + import logging LOGGER = logging.getLogger(__package__) diff --git a/homeassistant/components/samsungtv/device_trigger.py b/homeassistant/components/samsungtv/device_trigger.py index f3a69e637e6..e47cde785eb 100644 --- a/homeassistant/components/samsungtv/device_trigger.py +++ b/homeassistant/components/samsungtv/device_trigger.py @@ -1,4 +1,5 @@ """Provides device automations for control of Samsung TV.""" + from __future__ import annotations import voluptuous as vol diff --git a/homeassistant/components/samsungtv/diagnostics.py b/homeassistant/components/samsungtv/diagnostics.py index 319e08827cf..5ce0c0393ca 100644 --- a/homeassistant/components/samsungtv/diagnostics.py +++ b/homeassistant/components/samsungtv/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for SamsungTV.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/samsungtv/entity.py b/homeassistant/components/samsungtv/entity.py index 384a7a21528..ee2f50716eb 100644 --- a/homeassistant/components/samsungtv/entity.py +++ b/homeassistant/components/samsungtv/entity.py @@ -1,4 +1,5 @@ """Base SamsungTV Entity.""" + from __future__ import annotations from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/samsungtv/helpers.py b/homeassistant/components/samsungtv/helpers.py index 06a3c3e70e1..b334c60442b 100644 --- a/homeassistant/components/samsungtv/helpers.py +++ b/homeassistant/components/samsungtv/helpers.py @@ -1,4 +1,5 @@ """Helper functions for Samsung TV.""" + from __future__ import annotations from homeassistant.core import HomeAssistant, callback diff --git a/homeassistant/components/samsungtv/manifest.json b/homeassistant/components/samsungtv/manifest.json index 63a78925a6e..460e191828e 100644 --- a/homeassistant/components/samsungtv/manifest.json +++ b/homeassistant/components/samsungtv/manifest.json @@ -31,7 +31,6 @@ } ], "documentation": "https://www.home-assistant.io/integrations/samsungtv", - "import_executor": true, "integration_type": "device", "iot_class": "local_push", "loggers": ["samsungctl", "samsungtvws"], @@ -40,7 +39,7 @@ "samsungctl[websocket]==0.7.1", "samsungtvws[async,encrypted]==2.6.0", "wakeonlan==2.1.0", - "async-upnp-client==0.38.2" + "async-upnp-client==0.38.3" ], "ssdp": [ { diff --git a/homeassistant/components/samsungtv/media_player.py b/homeassistant/components/samsungtv/media_player.py index 44fce7f953f..36715c44a9b 100644 --- a/homeassistant/components/samsungtv/media_player.py +++ b/homeassistant/components/samsungtv/media_player.py @@ -1,8 +1,9 @@ """Support for interface with an Samsung TV.""" + from __future__ import annotations import asyncio -from collections.abc import Coroutine, Sequence +from collections.abc import Sequence from typing import Any from async_upnp_client.aiohttp import AiohttpNotifyServer, AiohttpSessionRequester @@ -35,6 +36,7 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.trigger import PluggableAction +from homeassistant.util.async_ import create_eager_task from .bridge import SamsungTVBridge, SamsungTVWSBridge from .const import CONF_SSDP_RENDERING_CONTROL_LOCATION, DOMAIN, LOGGER @@ -171,15 +173,15 @@ class SamsungTVDevice(SamsungTVEntity, MediaPlayerEntity): await self._dmr_device.async_unsubscribe_services() return - startup_tasks: list[Coroutine[Any, Any, Any]] = [] + startup_tasks: list[asyncio.Task[Any]] = [] if not self._app_list_event.is_set(): - startup_tasks.append(self._async_startup_app_list()) + startup_tasks.append(create_eager_task(self._async_startup_app_list())) if self._dmr_device and not self._dmr_device.is_subscribed: - startup_tasks.append(self._async_resubscribe_dmr()) + startup_tasks.append(create_eager_task(self._async_resubscribe_dmr())) if not self._dmr_device and self._ssdp_rendering_control_location: - startup_tasks.append(self._async_startup_dmr()) + startup_tasks.append(create_eager_task(self._async_startup_dmr())) if startup_tasks: await asyncio.gather(*startup_tasks) diff --git a/homeassistant/components/samsungtv/remote.py b/homeassistant/components/samsungtv/remote.py index bbe65d2ac82..752c5e2f950 100644 --- a/homeassistant/components/samsungtv/remote.py +++ b/homeassistant/components/samsungtv/remote.py @@ -1,4 +1,5 @@ """Support for the SamsungTV remote.""" + from __future__ import annotations from collections.abc import Iterable diff --git a/homeassistant/components/samsungtv/trigger.py b/homeassistant/components/samsungtv/trigger.py index cd78ff18be7..dc32617b583 100644 --- a/homeassistant/components/samsungtv/trigger.py +++ b/homeassistant/components/samsungtv/trigger.py @@ -1,4 +1,5 @@ """Samsung TV trigger dispatcher.""" + from __future__ import annotations from typing import cast diff --git a/homeassistant/components/samsungtv/triggers/turn_on.py b/homeassistant/components/samsungtv/triggers/turn_on.py index de0036234ad..6bcb9365b67 100644 --- a/homeassistant/components/samsungtv/triggers/turn_on.py +++ b/homeassistant/components/samsungtv/triggers/turn_on.py @@ -1,4 +1,5 @@ """Samsung TV device turn on trigger.""" + from __future__ import annotations import voluptuous as vol diff --git a/homeassistant/components/satel_integra/__init__.py b/homeassistant/components/satel_integra/__init__.py index e9edd1ff52c..466faf27b12 100644 --- a/homeassistant/components/satel_integra/__init__.py +++ b/homeassistant/components/satel_integra/__init__.py @@ -1,4 +1,5 @@ """Support for Satel Integra devices.""" + import collections import logging diff --git a/homeassistant/components/satel_integra/alarm_control_panel.py b/homeassistant/components/satel_integra/alarm_control_panel.py index 79ef4c048b3..bce2c2c6a5d 100644 --- a/homeassistant/components/satel_integra/alarm_control_panel.py +++ b/homeassistant/components/satel_integra/alarm_control_panel.py @@ -1,4 +1,5 @@ """Support for Satel Integra alarm, using ETHM module.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/satel_integra/binary_sensor.py b/homeassistant/components/satel_integra/binary_sensor.py index 5d2ce2c193c..b668ced326c 100644 --- a/homeassistant/components/satel_integra/binary_sensor.py +++ b/homeassistant/components/satel_integra/binary_sensor.py @@ -1,4 +1,5 @@ """Support for Satel Integra zone states- represented as binary sensors.""" + from __future__ import annotations from homeassistant.components.binary_sensor import ( diff --git a/homeassistant/components/satel_integra/switch.py b/homeassistant/components/satel_integra/switch.py index 469b2280290..6ce82908de7 100644 --- a/homeassistant/components/satel_integra/switch.py +++ b/homeassistant/components/satel_integra/switch.py @@ -1,4 +1,5 @@ """Support for Satel Integra modifiable outputs represented as switches.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/scene/__init__.py b/homeassistant/components/scene/__init__.py index 3c8adbd0502..5a7df164e1f 100644 --- a/homeassistant/components/scene/__init__.py +++ b/homeassistant/components/scene/__init__.py @@ -1,4 +1,5 @@ """Allow users to set and activate scenes.""" + from __future__ import annotations import functools as ft @@ -31,15 +32,13 @@ def _hass_domain_validator(config: dict[str, Any]) -> dict[str, Any]: def _platform_validator(config: dict[str, Any]) -> dict[str, Any]: """Validate it is a valid platform.""" + platform_name = config[CONF_PLATFORM] try: - platform = importlib.import_module(f".{config[CONF_PLATFORM]}", __name__) + platform = importlib.import_module( + f"homeassistant.components.{platform_name}.scene" + ) except ImportError: - try: - platform = importlib.import_module( - f"homeassistant.components.{config[CONF_PLATFORM]}.scene" - ) - except ImportError: - raise vol.Invalid("Invalid platform specified") from None + raise vol.Invalid("Invalid platform specified") from None if not hasattr(platform, "PLATFORM_SCHEMA"): return config @@ -67,7 +66,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: await component.async_setup(config) # Ensure Home Assistant platform always loaded. - await component.async_setup_platform(HA_DOMAIN, {"platform": HA_DOMAIN, STATES: []}) + hass.async_create_task( + component.async_setup_platform(HA_DOMAIN, {"platform": HA_DOMAIN, STATES: []}), + eager_start=True, + ) component.async_register_entity_service( SERVICE_TURN_ON, {ATTR_TRANSITION: vol.All(vol.Coerce(float), vol.Clamp(min=0, max=6553))}, @@ -126,10 +128,10 @@ class Scene(RestoreEntity): def activate(self, **kwargs: Any) -> None: """Activate scene. Try to get entities into requested state.""" - raise NotImplementedError() + raise NotImplementedError async def async_activate(self, **kwargs: Any) -> None: """Activate scene. Try to get entities into requested state.""" - task = self.hass.async_add_job(ft.partial(self.activate, **kwargs)) + task = self.hass.async_add_executor_job(ft.partial(self.activate, **kwargs)) if task: await task diff --git a/homeassistant/components/scene/icons.json b/homeassistant/components/scene/icons.json index 3ab7264b357..563c0f31ddc 100644 --- a/homeassistant/components/scene/icons.json +++ b/homeassistant/components/scene/icons.json @@ -3,5 +3,12 @@ "_": { "default": "mdi:palette" } + }, + "services": { + "turn_on": "mdi:power", + "reload": "mdi:reload", + "apply": "mdi:check", + "create": "mdi:plus", + "delete": "mdi:delete" } } diff --git a/homeassistant/components/schedule/__init__.py b/homeassistant/components/schedule/__init__.py index 2f7831fedd4..2dc2ff2d035 100644 --- a/homeassistant/components/schedule/__init__.py +++ b/homeassistant/components/schedule/__init__.py @@ -1,4 +1,5 @@ """Support for schedules in Home Assistant.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/schedule/const.py b/homeassistant/components/schedule/const.py index e044a614e4d..5ec57aae78d 100644 --- a/homeassistant/components/schedule/const.py +++ b/homeassistant/components/schedule/const.py @@ -1,4 +1,5 @@ """Constants for the schedule integration.""" + import logging from typing import Final diff --git a/homeassistant/components/schedule/icons.json b/homeassistant/components/schedule/icons.json new file mode 100644 index 00000000000..a03163179cb --- /dev/null +++ b/homeassistant/components/schedule/icons.json @@ -0,0 +1,5 @@ +{ + "services": { + "reload": "mdi:reload" + } +} diff --git a/homeassistant/components/schlage/__init__.py b/homeassistant/components/schlage/__init__.py index 96ff32d3e85..1c3ad547f3d 100644 --- a/homeassistant/components/schlage/__init__.py +++ b/homeassistant/components/schlage/__init__.py @@ -1,4 +1,5 @@ """The Schlage integration.""" + from __future__ import annotations from pycognito.exceptions import WarrantException diff --git a/homeassistant/components/schlage/binary_sensor.py b/homeassistant/components/schlage/binary_sensor.py index 5c97a903c72..a141403bdf4 100644 --- a/homeassistant/components/schlage/binary_sensor.py +++ b/homeassistant/components/schlage/binary_sensor.py @@ -20,25 +20,13 @@ from .coordinator import LockData, SchlageDataUpdateCoordinator from .entity import SchlageEntity -@dataclass(frozen=True) -class SchlageBinarySensorEntityDescriptionMixin: - """Mixin for required keys.""" - - # NOTE: This has to be a mixin because these are required keys. - # BinarySensorEntityDescription has attributes with default values, - # which means we can't inherit from it because you haven't have - # non-default arguments follow default arguments in an initializer. +@dataclass(frozen=True, kw_only=True) +class SchlageBinarySensorEntityDescription(BinarySensorEntityDescription): + """Entity description for a Schlage binary_sensor.""" value_fn: Callable[[LockData], bool] -@dataclass(frozen=True) -class SchlageBinarySensorEntityDescription( - BinarySensorEntityDescription, SchlageBinarySensorEntityDescriptionMixin -): - """Entity description for a Schlage binary_sensor.""" - - _DESCRIPTIONS: tuple[SchlageBinarySensorEntityDescription] = ( SchlageBinarySensorEntityDescription( key="keypad_disabled", @@ -57,17 +45,15 @@ async def async_setup_entry( ) -> None: """Set up binary_sensors based on a config entry.""" coordinator: SchlageDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] - entities = [] - for device_id in coordinator.data.locks: - for description in _DESCRIPTIONS: - entities.append( - SchlageBinarySensor( - coordinator=coordinator, - description=description, - device_id=device_id, - ) - ) - async_add_entities(entities) + async_add_entities( + SchlageBinarySensor( + coordinator=coordinator, + description=description, + device_id=device_id, + ) + for device_id in coordinator.data.locks + for description in _DESCRIPTIONS + ) class SchlageBinarySensor(SchlageEntity, BinarySensorEntity): diff --git a/homeassistant/components/schlage/config_flow.py b/homeassistant/components/schlage/config_flow.py index 84bc3ef8ef6..217cacedc41 100644 --- a/homeassistant/components/schlage/config_flow.py +++ b/homeassistant/components/schlage/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Schlage integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -8,10 +9,8 @@ import pyschlage from pyschlage.exceptions import NotAuthorizedError import voluptuous as vol -from homeassistant import config_entries -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN, LOGGER @@ -21,7 +20,7 @@ STEP_USER_DATA_SCHEMA = vol.Schema( STEP_REAUTH_DATA_SCHEMA = vol.Schema({vol.Required(CONF_PASSWORD): str}) -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class SchlageConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Schlage.""" VERSION = 1 @@ -30,7 +29,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" if user_input is None: return self._show_user_form({}) @@ -45,13 +44,15 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(user_id) return self.async_create_entry(title=username, data=user_input) - def _show_user_form(self, errors: dict[str, str]) -> FlowResult: + def _show_user_form(self, errors: dict[str, str]) -> ConfigFlowResult: """Show the user form.""" return self.async_show_form( step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors ) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Handle reauth upon an API authentication error.""" self.reauth_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] @@ -60,7 +61,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Dialog that informs the user that reauth is required.""" assert self.reauth_entry is not None if user_input is None: @@ -85,7 +86,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): await self.hass.config_entries.async_reload(self.reauth_entry.entry_id) return self.async_abort(reason="reauth_successful") - def _show_reauth_form(self, errors: dict[str, str]) -> FlowResult: + def _show_reauth_form(self, errors: dict[str, str]) -> ConfigFlowResult: """Show the reauth form.""" return self.async_show_form( step_id="reauth_confirm", diff --git a/homeassistant/components/schlage/coordinator.py b/homeassistant/components/schlage/coordinator.py index 3d736306d91..959d1e215f8 100644 --- a/homeassistant/components/schlage/coordinator.py +++ b/homeassistant/components/schlage/coordinator.py @@ -1,4 +1,5 @@ """DataUpdateCoordinator for the Schlage integration.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/schlage/lock.py b/homeassistant/components/schlage/lock.py index 0b5e35492de..7e6f60211b0 100644 --- a/homeassistant/components/schlage/lock.py +++ b/homeassistant/components/schlage/lock.py @@ -1,4 +1,5 @@ """Platform for Schlage lock integration.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/schlage/switch.py b/homeassistant/components/schlage/switch.py index 36c8fa74244..53771768ccd 100644 --- a/homeassistant/components/schlage/switch.py +++ b/homeassistant/components/schlage/switch.py @@ -24,27 +24,15 @@ from .coordinator import SchlageDataUpdateCoordinator from .entity import SchlageEntity -@dataclass(frozen=True) -class SchlageSwitchEntityDescriptionMixin: - """Mixin for required keys.""" - - # NOTE: This has to be a mixin because these are required keys. - # SwitchEntityDescription has attributes with default values, - # which means we can't inherit from it because you haven't have - # non-default arguments follow default arguments in an initializer. +@dataclass(frozen=True, kw_only=True) +class SchlageSwitchEntityDescription(SwitchEntityDescription): + """Entity description for a Schlage switch.""" on_fn: Callable[[Lock], None] off_fn: Callable[[Lock], None] value_fn: Callable[[Lock], bool] -@dataclass(frozen=True) -class SchlageSwitchEntityDescription( - SwitchEntityDescription, SchlageSwitchEntityDescriptionMixin -): - """Entity description for a Schlage switch.""" - - SWITCHES: tuple[SchlageSwitchEntityDescription, ...] = ( SchlageSwitchEntityDescription( key="beeper", @@ -74,17 +62,15 @@ async def async_setup_entry( ) -> None: """Set up switches based on a config entry.""" coordinator: SchlageDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] - entities = [] - for device_id in coordinator.data.locks: - for description in SWITCHES: - entities.append( - SchlageSwitch( - coordinator=coordinator, - description=description, - device_id=device_id, - ) - ) - async_add_entities(entities) + async_add_entities( + SchlageSwitch( + coordinator=coordinator, + description=description, + device_id=device_id, + ) + for device_id in coordinator.data.locks + for description in SWITCHES + ) class SchlageSwitch(SchlageEntity, SwitchEntity): diff --git a/homeassistant/components/schluter/__init__.py b/homeassistant/components/schluter/__init__.py index 5abb6b2a112..907841a2e5e 100644 --- a/homeassistant/components/schluter/__init__.py +++ b/homeassistant/components/schluter/__init__.py @@ -1,4 +1,5 @@ """The Schluter DITRA-HEAT integration.""" + import logging from requests import RequestException, Session diff --git a/homeassistant/components/schluter/climate.py b/homeassistant/components/schluter/climate.py index 5d747c8f345..74e2d9a0194 100644 --- a/homeassistant/components/schluter/climate.py +++ b/homeassistant/components/schluter/climate.py @@ -1,4 +1,5 @@ """Support for Schluter thermostats.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/scrape/__init__.py b/homeassistant/components/scrape/__init__.py index e96260139da..3906f5cf306 100644 --- a/homeassistant/components/scrape/__init__.py +++ b/homeassistant/components/scrape/__init__.py @@ -1,4 +1,5 @@ """The scrape component.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/scrape/config_flow.py b/homeassistant/components/scrape/config_flow.py index b4305b3948e..017b3c707a9 100644 --- a/homeassistant/components/scrape/config_flow.py +++ b/homeassistant/components/scrape/config_flow.py @@ -1,4 +1,5 @@ """Adds config flow for Scrape integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -59,6 +60,7 @@ from homeassistant.helpers.selector import ( TextSelectorConfig, TextSelectorType, ) +from homeassistant.helpers.trigger_template_entity import CONF_AVAILABILITY from . import COMBINED_SCHEMA from .const import ( @@ -104,6 +106,7 @@ SENSOR_SETUP = { ), vol.Optional(CONF_ATTRIBUTE): TextSelector(), vol.Optional(CONF_VALUE_TEMPLATE): TemplateSelector(), + vol.Optional(CONF_AVAILABILITY): TemplateSelector(), vol.Optional(CONF_DEVICE_CLASS): SelectSelector( SelectSelectorConfig( options=[ diff --git a/homeassistant/components/scrape/const.py b/homeassistant/components/scrape/const.py index cd64199fa23..292f0d0b247 100644 --- a/homeassistant/components/scrape/const.py +++ b/homeassistant/components/scrape/const.py @@ -1,4 +1,5 @@ """Constants for Scrape integration.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/scrape/coordinator.py b/homeassistant/components/scrape/coordinator.py index 9fc66db3481..74fd510ac94 100644 --- a/homeassistant/components/scrape/coordinator.py +++ b/homeassistant/components/scrape/coordinator.py @@ -1,4 +1,5 @@ """Coordinator for the scrape component.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/scrape/sensor.py b/homeassistant/components/scrape/sensor.py index bb8c233983d..61d58ea7bc5 100644 --- a/homeassistant/components/scrape/sensor.py +++ b/homeassistant/components/scrape/sensor.py @@ -1,4 +1,5 @@ """Support for getting data from websites with scraping.""" + from __future__ import annotations import logging @@ -112,10 +113,13 @@ async def async_setup_entry( Template(value_string, hass) if value_string is not None else None ) - trigger_entity_config = {CONF_NAME: name} + trigger_entity_config: dict[str, str | Template | None] = {CONF_NAME: name} for key in TRIGGER_ENTITY_OPTIONS: if key not in sensor_config: continue + if key == CONF_AVAILABILITY: + trigger_entity_config[key] = Template(sensor_config[key], hass) + continue trigger_entity_config[key] = sensor_config[key] entities.append( diff --git a/homeassistant/components/screenlogic/__init__.py b/homeassistant/components/screenlogic/__init__.py index 56c686df6b4..c7dbaabd565 100644 --- a/homeassistant/components/screenlogic/__init__.py +++ b/homeassistant/components/screenlogic/__init__.py @@ -1,4 +1,5 @@ """The Screenlogic integration.""" + import logging from typing import Any diff --git a/homeassistant/components/screenlogic/binary_sensor.py b/homeassistant/components/screenlogic/binary_sensor.py index 096c2c22918..1277ea7e1d4 100644 --- a/homeassistant/components/screenlogic/binary_sensor.py +++ b/homeassistant/components/screenlogic/binary_sensor.py @@ -1,4 +1,5 @@ """Support for a ScreenLogic Binary Sensor.""" + from copy import copy import dataclasses import logging @@ -32,14 +33,14 @@ from .util import cleanup_excluded_entity _LOGGER = logging.getLogger(__name__) -@dataclasses.dataclass(frozen=True) +@dataclasses.dataclass(frozen=True, kw_only=True) class ScreenLogicBinarySensorDescription( BinarySensorEntityDescription, ScreenLogicEntityDescription ): """A class that describes ScreenLogic binary sensor eneites.""" -@dataclasses.dataclass(frozen=True) +@dataclasses.dataclass(frozen=True, kw_only=True) class ScreenLogicPushBinarySensorDescription( ScreenLogicBinarySensorDescription, ScreenLogicPushEntityDescription ): @@ -174,32 +175,31 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up entry.""" - entities: list[ScreenLogicBinarySensor] = [] coordinator: ScreenlogicDataUpdateCoordinator = hass.data[SL_DOMAIN][ config_entry.entry_id ] gateway = coordinator.gateway - for core_sensor_description in SUPPORTED_CORE_SENSORS: + entities: list[ScreenLogicBinarySensor] = [ + ScreenLogicPushBinarySensor(coordinator, core_sensor_description) + for core_sensor_description in SUPPORTED_CORE_SENSORS if ( gateway.get_data( *core_sensor_description.data_root, core_sensor_description.key ) is not None - ): - entities.append( - ScreenLogicPushBinarySensor(coordinator, core_sensor_description) - ) + ) + ] for p_index, p_data in gateway.get_data(DEVICE.PUMP).items(): if not p_data or not p_data.get(VALUE.DATA): continue - for proto_pump_sensor_description in SUPPORTED_PUMP_SENSORS: - entities.append( - ScreenLogicPumpBinarySensor( - coordinator, copy(proto_pump_sensor_description), p_index - ) + entities.extend( + ScreenLogicPumpBinarySensor( + coordinator, copy(proto_pump_sensor_description), p_index ) + for proto_pump_sensor_description in SUPPORTED_PUMP_SENSORS + ) chem_sensor_description: ScreenLogicPushBinarySensorDescription for chem_sensor_description in SUPPORTED_INTELLICHEM_SENSORS: diff --git a/homeassistant/components/screenlogic/climate.py b/homeassistant/components/screenlogic/climate.py index 6d95f06a49c..8e89cb2eb03 100644 --- a/homeassistant/components/screenlogic/climate.py +++ b/homeassistant/components/screenlogic/climate.py @@ -1,4 +1,5 @@ """Support for a ScreenLogic heating device.""" + from dataclasses import dataclass import logging from typing import Any @@ -46,29 +47,26 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up entry.""" - entities = [] coordinator: ScreenlogicDataUpdateCoordinator = hass.data[SL_DOMAIN][ config_entry.entry_id ] gateway = coordinator.gateway - for body_index in gateway.get_data(DEVICE.BODY): - entities.append( - ScreenLogicClimate( - coordinator, - ScreenLogicClimateDescription( - subscription_code=CODE.STATUS_CHANGED, - data_root=(DEVICE.BODY,), - key=body_index, - ), - ) + async_add_entities( + ScreenLogicClimate( + coordinator, + ScreenLogicClimateDescription( + subscription_code=CODE.STATUS_CHANGED, + data_root=(DEVICE.BODY,), + key=body_index, + ), ) - - async_add_entities(entities) + for body_index in gateway.get_data(DEVICE.BODY) + ) -@dataclass(frozen=True) +@dataclass(frozen=True, kw_only=True) class ScreenLogicClimateDescription( ClimateEntityDescription, ScreenLogicPushEntityDescription ): diff --git a/homeassistant/components/screenlogic/config_flow.py b/homeassistant/components/screenlogic/config_flow.py index 25d00e3a2ce..74a01fdeaa2 100644 --- a/homeassistant/components/screenlogic/config_flow.py +++ b/homeassistant/components/screenlogic/config_flow.py @@ -1,4 +1,5 @@ """Config flow for ScreenLogic.""" + from __future__ import annotations import logging @@ -9,11 +10,15 @@ from screenlogicpy.const.common import SL_GATEWAY_IP, SL_GATEWAY_NAME, SL_GATEWA from screenlogicpy.requests import login import voluptuous as vol -from homeassistant import config_entries from homeassistant.components import dhcp +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT, CONF_SCAN_INTERVAL from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import format_mac @@ -60,7 +65,7 @@ def name_for_mac(mac): return f"Pentair: {short_mac(mac)}" -class ScreenlogicConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class ScreenlogicConfigFlow(ConfigFlow, domain=DOMAIN): """Config flow to setup screen logic devices.""" VERSION = 1 @@ -73,17 +78,19 @@ class ScreenlogicConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback def async_get_options_flow( - config_entry: config_entries.ConfigEntry, + config_entry: ConfigEntry, ) -> ScreenLogicOptionsFlowHandler: """Get the options flow for ScreenLogic.""" return ScreenLogicOptionsFlowHandler(config_entry) - async def async_step_user(self, user_input=None) -> FlowResult: + async def async_step_user(self, user_input=None) -> ConfigFlowResult: """Handle the start of the config flow.""" self.discovered_gateways = await async_discover_gateways_by_unique_id(self.hass) return await self.async_step_gateway_select() - async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: + async def async_step_dhcp( + self, discovery_info: dhcp.DhcpServiceInfo + ) -> ConfigFlowResult: """Handle dhcp discovery.""" mac = format_mac(discovery_info.macaddress) await self.async_set_unique_id(mac) @@ -94,7 +101,7 @@ class ScreenlogicConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.context["title_placeholders"] = {"name": discovery_info.hostname} return await self.async_step_gateway_entry() - async def async_step_gateway_select(self, user_input=None) -> FlowResult: + async def async_step_gateway_select(self, user_input=None) -> ConfigFlowResult: """Handle the selection of a discovered ScreenLogic gateway.""" existing = self._async_current_ids() unconfigured_gateways = { @@ -141,7 +148,7 @@ class ScreenlogicConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): description_placeholders={}, ) - async def async_step_gateway_entry(self, user_input=None) -> FlowResult: + async def async_step_gateway_entry(self, user_input=None) -> ConfigFlowResult: """Handle the manual entry of a ScreenLogic gateway.""" errors: dict[str, str] = {} ip_address = self.discovered_ip @@ -180,14 +187,14 @@ class ScreenlogicConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -class ScreenLogicOptionsFlowHandler(config_entries.OptionsFlow): +class ScreenLogicOptionsFlowHandler(OptionsFlow): """Handles the options for the ScreenLogic integration.""" - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + def __init__(self, config_entry: ConfigEntry) -> None: """Init the screen logic options flow.""" self.config_entry = config_entry - async def async_step_init(self, user_input=None) -> FlowResult: + async def async_step_init(self, user_input=None) -> ConfigFlowResult: """Manage the options.""" if user_input is not None: return self.async_create_entry( diff --git a/homeassistant/components/screenlogic/const.py b/homeassistant/components/screenlogic/const.py index 104736f300b..31e8468240f 100644 --- a/homeassistant/components/screenlogic/const.py +++ b/homeassistant/components/screenlogic/const.py @@ -1,4 +1,5 @@ """Constants for the ScreenLogic integration.""" + from screenlogicpy.const.common import UNIT from screenlogicpy.device_const.circuit import FUNCTION from screenlogicpy.device_const.system import COLOR_MODE diff --git a/homeassistant/components/screenlogic/coordinator.py b/homeassistant/components/screenlogic/coordinator.py index f16f2b9ff34..281bac86e01 100644 --- a/homeassistant/components/screenlogic/coordinator.py +++ b/homeassistant/components/screenlogic/coordinator.py @@ -1,4 +1,5 @@ """ScreenlogicDataUpdateCoordinator definition.""" + from datetime import timedelta import logging diff --git a/homeassistant/components/screenlogic/data.py b/homeassistant/components/screenlogic/data.py index cda1bc83f81..2df09ab142b 100644 --- a/homeassistant/components/screenlogic/data.py +++ b/homeassistant/components/screenlogic/data.py @@ -1,4 +1,5 @@ """Support for configurable supported data values for the ScreenLogic integration.""" + from screenlogicpy.const.data import DEVICE, VALUE ENTITY_MIGRATIONS = { diff --git a/homeassistant/components/screenlogic/entity.py b/homeassistant/components/screenlogic/entity.py index fc2c855d682..0f7530b7289 100644 --- a/homeassistant/components/screenlogic/entity.py +++ b/homeassistant/components/screenlogic/entity.py @@ -1,4 +1,5 @@ """Base ScreenLogicEntity definitions.""" + from collections.abc import Callable from dataclasses import dataclass from datetime import datetime @@ -28,19 +29,11 @@ from .util import generate_unique_id _LOGGER = logging.getLogger(__name__) -@dataclass(frozen=True) -class ScreenLogicEntityRequiredKeyMixin: - """Mixin for required ScreenLogic entity data_path.""" - - data_root: ScreenLogicDataPath - - -@dataclass(frozen=True) -class ScreenLogicEntityDescription( - EntityDescription, ScreenLogicEntityRequiredKeyMixin -): +@dataclass(frozen=True, kw_only=True) +class ScreenLogicEntityDescription(EntityDescription): """Base class for a ScreenLogic entity description.""" + data_root: ScreenLogicDataPath enabled_lambda: Callable[..., bool] | None = None @@ -103,21 +96,13 @@ class ScreenLogicEntity(CoordinatorEntity[ScreenlogicDataUpdateCoordinator]): raise HomeAssistantError(f"Data not found: {self._data_path}") from ke -@dataclass(frozen=True) -class ScreenLogicPushEntityRequiredKeyMixin: - """Mixin for required key for ScreenLogic push entities.""" +@dataclass(frozen=True, kw_only=True) +class ScreenLogicPushEntityDescription(ScreenLogicEntityDescription): + """Base class for a ScreenLogic push entity description.""" subscription_code: CODE -@dataclass(frozen=True) -class ScreenLogicPushEntityDescription( - ScreenLogicEntityDescription, - ScreenLogicPushEntityRequiredKeyMixin, -): - """Base class for a ScreenLogic push entity description.""" - - class ScreenLogicPushEntity(ScreenLogicEntity): """Base class for all ScreenLogic push entities.""" @@ -174,7 +159,7 @@ class ScreenLogicSwitchingEntity(ScreenLogicEntity): await self._async_set_state(ON_OFF.OFF) async def _async_set_state(self, state: ON_OFF) -> None: - raise NotImplementedError() + raise NotImplementedError class ScreenLogicCircuitEntity(ScreenLogicSwitchingEntity, ScreenLogicPushEntity): diff --git a/homeassistant/components/screenlogic/icons.json b/homeassistant/components/screenlogic/icons.json new file mode 100644 index 00000000000..d8d021c20e6 --- /dev/null +++ b/homeassistant/components/screenlogic/icons.json @@ -0,0 +1,7 @@ +{ + "services": { + "set_color_mode": "mdi:palette", + "start_super_chlorination": "mdi:pool", + "stop_super_chlorination": "mdi:pool" + } +} diff --git a/homeassistant/components/screenlogic/light.py b/homeassistant/components/screenlogic/light.py index 60cf7d52a48..4def432d97c 100644 --- a/homeassistant/components/screenlogic/light.py +++ b/homeassistant/components/screenlogic/light.py @@ -1,4 +1,5 @@ """Support for a ScreenLogic light 'circuit' switch.""" + from dataclasses import dataclass import logging @@ -60,7 +61,7 @@ async def async_setup_entry( async_add_entities(entities) -@dataclass(frozen=True) +@dataclass(frozen=True, kw_only=True) class ScreenLogicLightDescription( LightEntityDescription, ScreenLogicPushEntityDescription ): diff --git a/homeassistant/components/screenlogic/number.py b/homeassistant/components/screenlogic/number.py index 1ff611b2c9f..76640339040 100644 --- a/homeassistant/components/screenlogic/number.py +++ b/homeassistant/components/screenlogic/number.py @@ -1,4 +1,5 @@ """Support for a ScreenLogic number entity.""" + from dataclasses import dataclass import logging @@ -27,7 +28,7 @@ _LOGGER = logging.getLogger(__name__) PARALLEL_UPDATES = 1 -@dataclass(frozen=True) +@dataclass(frozen=True, kw_only=True) class ScreenLogicNumberDescription( NumberEntityDescription, ScreenLogicEntityDescription, @@ -111,7 +112,7 @@ class ScreenLogicNumber(ScreenLogicEntity, NumberEntity): async def async_set_native_value(self, value: float) -> None: """Update the current value.""" - raise NotImplementedError() + raise NotImplementedError class ScreenLogicSCGNumber(ScreenLogicNumber): diff --git a/homeassistant/components/screenlogic/sensor.py b/homeassistant/components/screenlogic/sensor.py index c73ce8be42c..e4fc86a6b5f 100644 --- a/homeassistant/components/screenlogic/sensor.py +++ b/homeassistant/components/screenlogic/sensor.py @@ -1,4 +1,5 @@ """Support for a ScreenLogic Sensor.""" + from collections.abc import Callable from copy import copy import dataclasses @@ -35,21 +36,16 @@ from .util import cleanup_excluded_entity, get_ha_unit _LOGGER = logging.getLogger(__name__) -@dataclasses.dataclass(frozen=True) -class ScreenLogicSensorMixin: - """Mixin for SecreenLogic sensor entity.""" +@dataclasses.dataclass(frozen=True, kw_only=True) +class ScreenLogicSensorDescription( + SensorEntityDescription, ScreenLogicEntityDescription +): + """Describes a ScreenLogic sensor.""" value_mod: Callable[[int | str], int | str] | None = None -@dataclasses.dataclass(frozen=True) -class ScreenLogicSensorDescription( - ScreenLogicSensorMixin, SensorEntityDescription, ScreenLogicEntityDescription -): - """Describes a ScreenLogic sensor.""" - - -@dataclasses.dataclass(frozen=True) +@dataclasses.dataclass(frozen=True, kw_only=True) class ScreenLogicPushSensorDescription( ScreenLogicSensorDescription, ScreenLogicPushEntityDescription ): @@ -231,20 +227,21 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up entry.""" - entities: list[ScreenLogicSensor] = [] coordinator: ScreenlogicDataUpdateCoordinator = hass.data[SL_DOMAIN][ config_entry.entry_id ] gateway = coordinator.gateway - for core_sensor_description in SUPPORTED_CORE_SENSORS: + entities: list[ScreenLogicSensor] = [ + ScreenLogicPushSensor(coordinator, core_sensor_description) + for core_sensor_description in SUPPORTED_CORE_SENSORS if ( gateway.get_data( *core_sensor_description.data_root, core_sensor_description.key ) is not None - ): - entities.append(ScreenLogicPushSensor(coordinator, core_sensor_description)) + ) + ] for pump_index, pump_data in gateway.get_data(DEVICE.PUMP).items(): if not pump_data or not pump_data.get(VALUE.DATA): diff --git a/homeassistant/components/screenlogic/switch.py b/homeassistant/components/screenlogic/switch.py index 43f749db913..fe697567bab 100644 --- a/homeassistant/components/screenlogic/switch.py +++ b/homeassistant/components/screenlogic/switch.py @@ -1,4 +1,5 @@ """Support for a ScreenLogic 'circuit' switch.""" + from dataclasses import dataclass import logging @@ -22,7 +23,7 @@ from .entity import ( _LOGGER = logging.getLogger(__name__) -@dataclass(frozen=True) +@dataclass(frozen=True, kw_only=True) class ScreenLogicCircuitSwitchDescription( SwitchEntityDescription, ScreenLogicPushEntityDescription ): diff --git a/homeassistant/components/screenlogic/util.py b/homeassistant/components/screenlogic/util.py index 928effc73fc..781d0fcab24 100644 --- a/homeassistant/components/screenlogic/util.py +++ b/homeassistant/components/screenlogic/util.py @@ -1,4 +1,5 @@ """Utility functions for the ScreenLogic integration.""" + import logging from screenlogicpy.const.data import SHARED_VALUES diff --git a/homeassistant/components/script/__init__.py b/homeassistant/components/script/__init__.py index f1a86687255..82752ed15bc 100644 --- a/homeassistant/components/script/__init__.py +++ b/homeassistant/components/script/__init__.py @@ -1,4 +1,5 @@ """Support for scripts.""" + from __future__ import annotations from abc import ABC, abstractmethod @@ -55,6 +56,7 @@ from homeassistant.helpers.service import async_set_service_schema from homeassistant.helpers.trace import trace_get, trace_path from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass +from homeassistant.util.async_ import create_eager_task from homeassistant.util.dt import parse_datetime from .config import ScriptConfig @@ -156,6 +158,30 @@ def areas_in_script(hass: HomeAssistant, entity_id: str) -> list[str]: return _x_in_script(hass, entity_id, "referenced_areas") +@callback +def scripts_with_floor(hass: HomeAssistant, floor_id: str) -> list[str]: + """Return all scripts that reference the floor.""" + return _scripts_with_x(hass, floor_id, "referenced_floors") + + +@callback +def floors_in_script(hass: HomeAssistant, entity_id: str) -> list[str]: + """Return all floors in a script.""" + return _x_in_script(hass, entity_id, "referenced_floors") + + +@callback +def scripts_with_label(hass: HomeAssistant, label_id: str) -> list[str]: + """Return all scripts that reference the label.""" + return _scripts_with_x(hass, label_id, "referenced_labels") + + +@callback +def labels_in_script(hass: HomeAssistant, entity_id: str) -> list[str]: + """Return all labels in a script.""" + return _x_in_script(hass, entity_id, "referenced_labels") + + @callback def scripts_with_blueprint(hass: HomeAssistant, blueprint_path: str) -> list[str]: """Return all scripts that reference the blueprint.""" @@ -197,8 +223,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: await _async_process_config(hass, config, component) # Add some default blueprints to blueprints/script, does nothing - # if blueprints/script already exists - await async_get_blueprints(hass).async_populate() + # if blueprints/script already exists but still has to create + # an executor job to check if the folder exists so we run it in a + # separate task to avoid waiting for it to finish setting up + # since a tracked task will be waited at the end of startup + hass.async_create_task( + async_get_blueprints(hass).async_populate(), eager_start=True + ) async def reload_service(service: ServiceCall) -> None: """Call a service to reload scripts.""" @@ -226,7 +257,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: await asyncio.wait( [ - asyncio.create_task(script_entity.async_turn_off()) + create_eager_task(script_entity.async_turn_off()) for script_entity in script_entities ] ) @@ -387,6 +418,16 @@ class BaseScriptEntity(ToggleEntity, ABC): raw_config: ConfigType | None + @cached_property + @abstractmethod + def referenced_labels(self) -> set[str]: + """Return a set of referenced labels.""" + + @cached_property + @abstractmethod + def referenced_floors(self) -> set[str]: + """Return a set of referenced floors.""" + @cached_property @abstractmethod def referenced_areas(self) -> set[str]: @@ -411,7 +452,7 @@ class BaseScriptEntity(ToggleEntity, ABC): class UnavailableScriptEntity(BaseScriptEntity): """A non-functional script entity with its state set to unavailable. - This class is instatiated when an script fails to validate. + This class is instantiated when an script fails to validate. """ _attr_should_poll = False @@ -432,6 +473,16 @@ class UnavailableScriptEntity(BaseScriptEntity): """Return the name of the entity.""" return self._name + @cached_property + def referenced_labels(self) -> set[str]: + """Return a set of referenced labels.""" + return set() + + @cached_property + def referenced_floors(self) -> set[str]: + """Return a set of referenced floors.""" + return set() + @cached_property def referenced_areas(self) -> set[str]: """Return a set of referenced areas.""" @@ -515,6 +566,16 @@ class ScriptEntity(BaseScriptEntity, RestoreEntity): """Return true if script is on.""" return self.script.is_running + @cached_property + def referenced_labels(self) -> set[str]: + """Return a set of referenced labels.""" + return self.script.referenced_labels + + @cached_property + def referenced_floors(self) -> set[str]: + """Return a set of referenced floors.""" + return self.script.referenced_floors + @cached_property def referenced_areas(self) -> set[str]: """Return a set of referenced areas.""" @@ -575,7 +636,7 @@ class ScriptEntity(BaseScriptEntity, RestoreEntity): script_stack_cv.set([]) self._changed.clear() - self.hass.async_create_task(coro) + self.hass.async_create_task(coro, eager_start=True) # Wait for first state change so we can guarantee that # it is written to the State Machine before we return. await self._changed.wait() diff --git a/homeassistant/components/script/config.py b/homeassistant/components/script/config.py index 1cbab23d843..fc1b49b1823 100644 --- a/homeassistant/components/script/config.py +++ b/homeassistant/components/script/config.py @@ -1,4 +1,5 @@ """Config validation helper for the script integration.""" + from __future__ import annotations from collections.abc import Mapping diff --git a/homeassistant/components/script/const.py b/homeassistant/components/script/const.py index 7b4b0f5fe9c..e6e5fdca13f 100644 --- a/homeassistant/components/script/const.py +++ b/homeassistant/components/script/const.py @@ -1,4 +1,5 @@ """Constants for the script integration.""" + import logging DOMAIN = "script" diff --git a/homeassistant/components/script/helpers.py b/homeassistant/components/script/helpers.py index 4504869e270..b070a4d60ce 100644 --- a/homeassistant/components/script/helpers.py +++ b/homeassistant/components/script/helpers.py @@ -1,4 +1,5 @@ """Helpers for automation integration.""" + from homeassistant.components.blueprint import DomainBlueprints from homeassistant.const import SERVICE_RELOAD from homeassistant.core import HomeAssistant, callback diff --git a/homeassistant/components/script/logbook.py b/homeassistant/components/script/logbook.py index ce23f083ee0..ec7d62d7949 100644 --- a/homeassistant/components/script/logbook.py +++ b/homeassistant/components/script/logbook.py @@ -1,4 +1,5 @@ """Describe logbook events.""" + from homeassistant.components.logbook import ( LOGBOOK_ENTRY_CONTEXT_ID, LOGBOOK_ENTRY_ENTITY_ID, diff --git a/homeassistant/components/script/trace.py b/homeassistant/components/script/trace.py index c63d50b1041..a50cda752d0 100644 --- a/homeassistant/components/script/trace.py +++ b/homeassistant/components/script/trace.py @@ -1,4 +1,5 @@ """Trace support for script.""" + from __future__ import annotations from collections.abc import Iterator diff --git a/homeassistant/components/scsgate/__init__.py b/homeassistant/components/scsgate/__init__.py index af5921e2c3b..7f00f8abe84 100644 --- a/homeassistant/components/scsgate/__init__.py +++ b/homeassistant/components/scsgate/__init__.py @@ -1,4 +1,5 @@ """Support for SCSGate components.""" + import logging from threading import Lock diff --git a/homeassistant/components/scsgate/cover.py b/homeassistant/components/scsgate/cover.py index f68b089e2d7..8f17ca170a0 100644 --- a/homeassistant/components/scsgate/cover.py +++ b/homeassistant/components/scsgate/cover.py @@ -1,4 +1,5 @@ """Support for SCSGate covers.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/scsgate/light.py b/homeassistant/components/scsgate/light.py index df63bad49d4..a4bb78fcd1c 100644 --- a/homeassistant/components/scsgate/light.py +++ b/homeassistant/components/scsgate/light.py @@ -1,4 +1,5 @@ """Support for SCSGate lights.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/scsgate/switch.py b/homeassistant/components/scsgate/switch.py index 3f215130048..8ad31106cf7 100644 --- a/homeassistant/components/scsgate/switch.py +++ b/homeassistant/components/scsgate/switch.py @@ -1,4 +1,5 @@ """Support for SCSGate switches.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/search/__init__.py b/homeassistant/components/search/__init__.py index 7dd7d952e95..a85a21e8102 100644 --- a/homeassistant/components/search/__init__.py +++ b/homeassistant/components/search/__init__.py @@ -1,7 +1,10 @@ """The Search integration.""" + from __future__ import annotations -from collections import defaultdict, deque +from collections import defaultdict +from collections.abc import Iterable +from enum import StrEnum import logging from typing import Any @@ -11,6 +14,7 @@ from homeassistant.components import automation, group, person, script, websocke from homeassistant.components.homeassistant import scene from homeassistant.core import HomeAssistant, callback, split_entity_id from homeassistant.helpers import ( + area_registry as ar, config_validation as cv, device_registry as dr, entity_registry as er, @@ -27,6 +31,25 @@ _LOGGER = logging.getLogger(__name__) CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN) +# enum of item types +class ItemType(StrEnum): + """Item types.""" + + AREA = "area" + AUTOMATION = "automation" + AUTOMATION_BLUEPRINT = "automation_blueprint" + CONFIG_ENTRY = "config_entry" + DEVICE = "device" + ENTITY = "entity" + FLOOR = "floor" + GROUP = "group" + LABEL = "label" + PERSON = "person" + SCENE = "scene" + SCRIPT = "script" + SCRIPT_BLUEPRINT = "script_blueprint" + + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Search component.""" websocket_api.async_register_command(hass, websocket_search_related) @@ -36,21 +59,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: @websocket_api.websocket_command( { vol.Required("type"): "search/related", - vol.Required("item_type"): vol.In( - ( - "area", - "automation", - "automation_blueprint", - "config_entry", - "device", - "entity", - "group", - "person", - "scene", - "script", - "script_blueprint", - ) - ), + vol.Required("item_type"): vol.Coerce(ItemType), vol.Required("item_id"): str, } ) @@ -61,271 +70,523 @@ def websocket_search_related( msg: dict[str, Any], ) -> None: """Handle search.""" - searcher = Searcher( - hass, - dr.async_get(hass), - er.async_get(hass), - get_entity_sources(hass), - ) + searcher = Searcher(hass, get_entity_sources(hass)) connection.send_result( msg["id"], searcher.async_search(msg["item_type"], msg["item_id"]) ) class Searcher: - """Find related things. + """Find related things.""" - Few rules: - Scenes, scripts, automations and config entries will only be expanded if they are - the entry point. They won't be expanded if we process them. This is because they - turn the results into garbage. - """ - - # These types won't be further explored. Config entries + Output types. - DONT_RESOLVE = { - "area", - "automation", - "automation_blueprint", - "config_entry", - "group", - "scene", - "script", - "script_blueprint", - } - # These types exist as an entity and so need cleanup in results EXIST_AS_ENTITY = {"automation", "group", "person", "scene", "script"} def __init__( self, hass: HomeAssistant, - device_reg: dr.DeviceRegistry, - entity_reg: er.EntityRegistry, entity_sources: dict[str, EntityInfo], ) -> None: """Search results.""" self.hass = hass - self._device_reg = device_reg - self._entity_reg = entity_reg - self._sources = entity_sources - self.results: defaultdict[str, set[str]] = defaultdict(set) - self._to_resolve: deque[tuple[str, str]] = deque() + self._area_registry = ar.async_get(hass) + self._device_registry = dr.async_get(hass) + self._entity_registry = er.async_get(hass) + self._entity_sources = entity_sources + self.results: defaultdict[ItemType, set[str]] = defaultdict(set) @callback - def async_search(self, item_type: str, item_id: str) -> dict[str, set[str]]: + def async_search(self, item_type: ItemType, item_id: str) -> dict[str, set[str]]: """Find results.""" _LOGGER.debug("Searching for %s/%s", item_type, item_id) - self.results[item_type].add(item_id) - self._to_resolve.append((item_type, item_id)) + getattr(self, f"_async_search_{item_type}")(item_id) - while self._to_resolve: - search_type, search_id = self._to_resolve.popleft() - getattr(self, f"_resolve_{search_type}")(search_id) - - # Clean up entity_id items, from the general "entity" type result, - # that are also found in the specific entity domain type. - for result_type in self.EXIST_AS_ENTITY: - self.results["entity"] -= self.results[result_type] - - # Remove entry into graph from search results. - to_remove_item_type = item_type - if item_type == "entity": - domain = split_entity_id(item_id)[0] - - if domain in self.EXIST_AS_ENTITY: - to_remove_item_type = domain - - self.results[to_remove_item_type].remove(item_id) + # Remove the original requested item from the results (if present) + if item_type in self.results and item_id in self.results[item_type]: + self.results[item_type].remove(item_id) # Filter out empty sets. return {key: val for key, val in self.results.items() if val} @callback - def _add_or_resolve(self, item_type: str, item_id: str) -> None: - """Add an item to explore.""" - if item_id in self.results[item_type]: + def _add(self, item_type: ItemType, item_id: str | Iterable[str] | None) -> None: + """Add an item (or items) to the results.""" + if item_id is None: return - self.results[item_type].add(item_id) - - if item_type not in self.DONT_RESOLVE: - self._to_resolve.append((item_type, item_id)) + if isinstance(item_id, str): + self.results[item_type].add(item_id) + else: + self.results[item_type].update(item_id) @callback - def _resolve_area(self, area_id: str) -> None: - """Resolve an area.""" - for device in dr.async_entries_for_area(self._device_reg, area_id): - self._add_or_resolve("device", device.id) + def _async_search_area(self, area_id: str, *, entry_point: bool = True) -> None: + """Find results for an area.""" + if not (area_entry := self._async_resolve_up_area(area_id)): + return - for entity_entry in er.async_entries_for_area(self._entity_reg, area_id): - self._add_or_resolve("entity", entity_entry.entity_id) + if entry_point: + # Add labels of this area + self._add(ItemType.LABEL, area_entry.labels) - for entity_id in script.scripts_with_area(self.hass, area_id): - self._add_or_resolve("entity", entity_id) + # Automations referencing this area + self._add( + ItemType.AUTOMATION, automation.automations_with_area(self.hass, area_id) + ) - for entity_id in automation.automations_with_area(self.hass, area_id): - self._add_or_resolve("entity", entity_id) + # Scripts referencing this area + self._add(ItemType.SCRIPT, script.scripts_with_area(self.hass, area_id)) + + # Entity in this area, will extend this with the entities of the devices in this area + entity_entries = er.async_entries_for_area(self._entity_registry, area_id) + + # Devices in this area + for device in dr.async_entries_for_area(self._device_registry, area_id): + self._add(ItemType.DEVICE, device.id) + + # Config entries for devices in this area + if device_entry := self._device_registry.async_get(device.id): + self._add(ItemType.CONFIG_ENTRY, device_entry.config_entries) + + # Automations referencing this device + self._add( + ItemType.AUTOMATION, + automation.automations_with_device(self.hass, device.id), + ) + + # Scripts referencing this device + self._add(ItemType.SCRIPT, script.scripts_with_device(self.hass, device.id)) + + # Entities of this device + for entity_entry in er.async_entries_for_device( + self._entity_registry, device.id + ): + # Skip the entity if it's in a different area + if entity_entry.area_id is not None: + continue + entity_entries.append(entity_entry) + + # Process entities in this area + for entity_entry in entity_entries: + self._add(ItemType.ENTITY, entity_entry.entity_id) + + # If this entity also exists as a resource, we add it. + if entity_entry.domain in self.EXIST_AS_ENTITY: + self._add(ItemType(entity_entry.domain), entity_entry.entity_id) + + # Automations referencing this entity + self._add( + ItemType.AUTOMATION, + automation.automations_with_entity(self.hass, entity_entry.entity_id), + ) + + # Scripts referencing this entity + self._add( + ItemType.SCRIPT, + script.scripts_with_entity(self.hass, entity_entry.entity_id), + ) + + # Groups that have this entity as a member + self._add( + ItemType.GROUP, + group.groups_with_entity(self.hass, entity_entry.entity_id), + ) + + # Persons that use this entity + self._add( + ItemType.PERSON, + person.persons_with_entity(self.hass, entity_entry.entity_id), + ) + + # Scenes that reference this entity + self._add( + ItemType.SCENE, + scene.scenes_with_entity(self.hass, entity_entry.entity_id), + ) + + # Config entries for entities in this area + self._add(ItemType.CONFIG_ENTRY, entity_entry.config_entry_id) @callback - def _resolve_automation(self, automation_entity_id: str) -> None: - """Resolve an automation. + def _async_search_automation(self, automation_entity_id: str) -> None: + """Find results for an automation.""" + # Up resolve the automation entity itself + if entity_entry := self._async_resolve_up_entity(automation_entity_id): + # Add labels of this automation entity + self._add(ItemType.LABEL, entity_entry.labels) - Will only be called if automation is an entry point. - """ - for entity in automation.entities_in_automation( - self.hass, automation_entity_id - ): - self._add_or_resolve("entity", entity) + # Find the blueprint used in this automation + self._add( + ItemType.AUTOMATION_BLUEPRINT, + automation.blueprint_in_automation(self.hass, automation_entity_id), + ) - for device in automation.devices_in_automation(self.hass, automation_entity_id): - self._add_or_resolve("device", device) + # Floors referenced in this automation + self._add( + ItemType.FLOOR, + automation.floors_in_automation(self.hass, automation_entity_id), + ) + # Areas referenced in this automation for area in automation.areas_in_automation(self.hass, automation_entity_id): - self._add_or_resolve("area", area) + self._add(ItemType.AREA, area) + self._async_resolve_up_area(area) - if blueprint := automation.blueprint_in_automation( + # Devices referenced in this automation + for device in automation.devices_in_automation(self.hass, automation_entity_id): + self._add(ItemType.DEVICE, device) + self._async_resolve_up_device(device) + + # Entities referenced in this automation + for entity_id in automation.entities_in_automation( self.hass, automation_entity_id ): - self._add_or_resolve("automation_blueprint", blueprint) + self._add(ItemType.ENTITY, entity_id) + self._async_resolve_up_entity(entity_id) + + # If this entity also exists as a resource, we add it. + domain = split_entity_id(entity_id)[0] + if domain in self.EXIST_AS_ENTITY: + self._add(ItemType(domain), entity_id) + + # For an automation, we want to unwrap the groups, to ensure we + # relate this automation to all those members as well. + if domain == "group": + for group_entity_id in group.get_entity_ids(self.hass, entity_id): + self._add(ItemType.ENTITY, group_entity_id) + self._async_resolve_up_entity(group_entity_id) + + # For an automation, we want to unwrap the scenes, to ensure we + # relate this automation to all referenced entities as well. + if domain == "scene": + for scene_entity_id in scene.entities_in_scene(self.hass, entity_id): + self._add(ItemType.ENTITY, scene_entity_id) + self._async_resolve_up_entity(scene_entity_id) + + # Fully search the script if it is part of an automation. + # This makes the automation return all results of the embedded script. + if domain == "script": + self._async_search_script(entity_id, entry_point=False) @callback - def _resolve_automation_blueprint(self, blueprint_path: str) -> None: - """Resolve an automation blueprint. - - Will only be called if blueprint is an entry point. - """ - for entity_id in automation.automations_with_blueprint( - self.hass, blueprint_path - ): - self._add_or_resolve("automation", entity_id) + def _async_search_automation_blueprint(self, blueprint_path: str) -> None: + """Find results for an automation blueprint.""" + self._add( + ItemType.AUTOMATION, + automation.automations_with_blueprint(self.hass, blueprint_path), + ) @callback - def _resolve_config_entry(self, config_entry_id: str) -> None: - """Resolve a config entry. - - Will only be called if config entry is an entry point. - """ + def _async_search_config_entry(self, config_entry_id: str) -> None: + """Find results for a config entry.""" for device_entry in dr.async_entries_for_config_entry( - self._device_reg, config_entry_id + self._device_registry, config_entry_id ): - self._add_or_resolve("device", device_entry.id) + self._add(ItemType.DEVICE, device_entry.id) + self._async_search_device(device_entry.id, entry_point=False) for entity_entry in er.async_entries_for_config_entry( - self._entity_reg, config_entry_id + self._entity_registry, config_entry_id ): - self._add_or_resolve("entity", entity_entry.entity_id) + self._add(ItemType.ENTITY, entity_entry.entity_id) + self._async_search_entity(entity_entry.entity_id, entry_point=False) @callback - def _resolve_device(self, device_id: str) -> None: - """Resolve a device.""" - device_entry = self._device_reg.async_get(device_id) - # Unlikely entry doesn't exist, but let's guard for bad data. - if device_entry is not None: - if device_entry.area_id: - self._add_or_resolve("area", device_entry.area_id) + def _async_search_device(self, device_id: str, *, entry_point: bool = True) -> None: + """Find results for a device.""" + if not (device_entry := self._async_resolve_up_device(device_id)): + return - for config_entry_id in device_entry.config_entries: - self._add_or_resolve("config_entry", config_entry_id) + if entry_point: + # Add labels of this device + self._add(ItemType.LABEL, device_entry.labels) - # We do not resolve device_entry.via_device_id because that - # device is not related data-wise inside HA. + # Automations referencing this device + self._add( + ItemType.AUTOMATION, + automation.automations_with_device(self.hass, device_id), + ) - for entity_entry in er.async_entries_for_device(self._entity_reg, device_id): - self._add_or_resolve("entity", entity_entry.entity_id) + # Scripts referencing this device + self._add(ItemType.SCRIPT, script.scripts_with_device(self.hass, device_id)) - for entity_id in script.scripts_with_device(self.hass, device_id): - self._add_or_resolve("entity", entity_id) - - for entity_id in automation.automations_with_device(self.hass, device_id): - self._add_or_resolve("entity", entity_id) + # Entities of this device + for entity_entry in er.async_entries_for_device( + self._entity_registry, device_id + ): + self._add(ItemType.ENTITY, entity_entry.entity_id) + # Add all entity information as well + self._async_search_entity(entity_entry.entity_id, entry_point=False) @callback - def _resolve_entity(self, entity_id: str) -> None: - """Resolve an entity.""" - # Extra: Find automations and scripts that reference this entity. + def _async_search_entity(self, entity_id: str, *, entry_point: bool = True) -> None: + """Find results for an entity.""" + # Resolve up the entity itself + entity_entry = self._async_resolve_up_entity(entity_id) - for entity in scene.scenes_with_entity(self.hass, entity_id): - self._add_or_resolve("entity", entity) + if entity_entry and entry_point: + # Add labels of this entity + self._add(ItemType.LABEL, entity_entry.labels) - for entity in group.groups_with_entity(self.hass, entity_id): - self._add_or_resolve("entity", entity) + # Automations referencing this entity + self._add( + ItemType.AUTOMATION, + automation.automations_with_entity(self.hass, entity_id), + ) - for entity in automation.automations_with_entity(self.hass, entity_id): - self._add_or_resolve("entity", entity) + # Scripts referencing this entity + self._add(ItemType.SCRIPT, script.scripts_with_entity(self.hass, entity_id)) - for entity in script.scripts_with_entity(self.hass, entity_id): - self._add_or_resolve("entity", entity) + # Groups that have this entity as a member + self._add(ItemType.GROUP, group.groups_with_entity(self.hass, entity_id)) - for entity in person.persons_with_entity(self.hass, entity_id): - self._add_or_resolve("entity", entity) + # Persons referencing this entity + self._add(ItemType.PERSON, person.persons_with_entity(self.hass, entity_id)) - # Find devices - entity_entry = self._entity_reg.async_get(entity_id) - if entity_entry is not None: - if entity_entry.device_id: - self._add_or_resolve("device", entity_entry.device_id) - - if entity_entry.config_entry_id is not None: - self._add_or_resolve("config_entry", entity_entry.config_entry_id) - else: - source = self._sources.get(entity_id) - if source is not None and "config_entry" in source: - self._add_or_resolve("config_entry", source["config_entry"]) - - domain = split_entity_id(entity_id)[0] - - if domain in self.EXIST_AS_ENTITY: - self._add_or_resolve(domain, entity_id) + # Scenes referencing this entity + self._add(ItemType.SCENE, scene.scenes_with_entity(self.hass, entity_id)) @callback - def _resolve_group(self, group_entity_id: str) -> None: - """Resolve a group. + def _async_search_floor(self, floor_id: str) -> None: + """Find results for a floor.""" + # Automations referencing this floor + self._add( + ItemType.AUTOMATION, + automation.automations_with_floor(self.hass, floor_id), + ) - Will only be called if group is an entry point. + # Scripts referencing this floor + self._add(ItemType.SCRIPT, script.scripts_with_floor(self.hass, floor_id)) + + for area_entry in ar.async_entries_for_floor(self._area_registry, floor_id): + self._add(ItemType.AREA, area_entry.id) + self._async_search_area(area_entry.id, entry_point=False) + + @callback + def _async_search_group(self, group_entity_id: str) -> None: + """Find results for a group. + + Note: We currently only support the classic groups, thus + we don't look up the area/floor for a group entity. """ + # Automations referencing this group + self._add( + ItemType.AUTOMATION, + automation.automations_with_entity(self.hass, group_entity_id), + ) + + # Scripts referencing this group + self._add( + ItemType.SCRIPT, script.scripts_with_entity(self.hass, group_entity_id) + ) + + # Scenes that reference this group + self._add(ItemType.SCENE, scene.scenes_with_entity(self.hass, group_entity_id)) + + # Entities in this group for entity_id in group.get_entity_ids(self.hass, group_entity_id): - self._add_or_resolve("entity", entity_id) + self._add(ItemType.ENTITY, entity_id) + self._async_resolve_up_entity(entity_id) @callback - def _resolve_person(self, person_entity_id: str) -> None: - """Resolve a person. + def _async_search_label(self, label_id: str) -> None: + """Find results for a label.""" - Will only be called if person is an entry point. - """ - for entity in person.entities_in_person(self.hass, person_entity_id): - self._add_or_resolve("entity", entity) + # Areas with this label + for area_entry in ar.async_entries_for_label(self._area_registry, label_id): + self._add(ItemType.AREA, area_entry.id) + + # Devices with this label + for device in dr.async_entries_for_label(self._device_registry, label_id): + self._add(ItemType.DEVICE, device.id) + + # Entities with this label + for entity_entry in er.async_entries_for_label(self._entity_registry, label_id): + self._add(ItemType.ENTITY, entity_entry.entity_id) + + # If this entity also exists as a resource, we add it. + domain = split_entity_id(entity_entry.entity_id)[0] + if domain in self.EXIST_AS_ENTITY: + self._add(ItemType(domain), entity_entry.entity_id) + + # Automations referencing this label + self._add( + ItemType.AUTOMATION, + automation.automations_with_label(self.hass, label_id), + ) + + # Scripts referencing this label + self._add(ItemType.SCRIPT, script.scripts_with_label(self.hass, label_id)) @callback - def _resolve_scene(self, scene_entity_id: str) -> None: - """Resolve a scene. + def _async_search_person(self, person_entity_id: str) -> None: + """Find results for a person.""" + # Up resolve the scene entity itself + if entity_entry := self._async_resolve_up_entity(person_entity_id): + # Add labels of this person entity + self._add(ItemType.LABEL, entity_entry.labels) - Will only be called if scene is an entry point. - """ + # Automations referencing this person + self._add( + ItemType.AUTOMATION, + automation.automations_with_entity(self.hass, person_entity_id), + ) + + # Scripts referencing this person + self._add( + ItemType.SCRIPT, script.scripts_with_entity(self.hass, person_entity_id) + ) + + # Add all member entities of this person + self._add( + ItemType.ENTITY, person.entities_in_person(self.hass, person_entity_id) + ) + + @callback + def _async_search_scene(self, scene_entity_id: str) -> None: + """Find results for a scene.""" + # Up resolve the scene entity itself + if entity_entry := self._async_resolve_up_entity(scene_entity_id): + # Add labels of this scene entity + self._add(ItemType.LABEL, entity_entry.labels) + + # Automations referencing this scene + self._add( + ItemType.AUTOMATION, + automation.automations_with_entity(self.hass, scene_entity_id), + ) + + # Scripts referencing this scene + self._add( + ItemType.SCRIPT, script.scripts_with_entity(self.hass, scene_entity_id) + ) + + # Add all entities in this scene for entity in scene.entities_in_scene(self.hass, scene_entity_id): - self._add_or_resolve("entity", entity) + self._add(ItemType.ENTITY, entity) + self._async_resolve_up_entity(entity) @callback - def _resolve_script(self, script_entity_id: str) -> None: - """Resolve a script. + def _async_search_script( + self, script_entity_id: str, *, entry_point: bool = True + ) -> None: + """Find results for a script.""" + # Up resolve the script entity itself + entity_entry = self._async_resolve_up_entity(script_entity_id) - Will only be called if script is an entry point. - """ - for entity in script.entities_in_script(self.hass, script_entity_id): - self._add_or_resolve("entity", entity) + if entity_entry and entry_point: + # Add labels of this script entity + self._add(ItemType.LABEL, entity_entry.labels) - for device in script.devices_in_script(self.hass, script_entity_id): - self._add_or_resolve("device", device) + # Find the blueprint used in this script + self._add( + ItemType.SCRIPT_BLUEPRINT, + script.blueprint_in_script(self.hass, script_entity_id), + ) + # Floors referenced in this script + self._add(ItemType.FLOOR, script.floors_in_script(self.hass, script_entity_id)) + + # Areas referenced in this script for area in script.areas_in_script(self.hass, script_entity_id): - self._add_or_resolve("area", area) + self._add(ItemType.AREA, area) + self._async_resolve_up_area(area) - if blueprint := script.blueprint_in_script(self.hass, script_entity_id): - self._add_or_resolve("script_blueprint", blueprint) + # Devices referenced in this script + for device in script.devices_in_script(self.hass, script_entity_id): + self._add(ItemType.DEVICE, device) + self._async_resolve_up_device(device) + + # Entities referenced in this script + for entity_id in script.entities_in_script(self.hass, script_entity_id): + self._add(ItemType.ENTITY, entity_id) + self._async_resolve_up_entity(entity_id) + + # If this entity also exists as a resource, we add it. + domain = split_entity_id(entity_id)[0] + if domain in self.EXIST_AS_ENTITY: + self._add(ItemType(domain), entity_id) + + # For an script, we want to unwrap the groups, to ensure we + # relate this script to all those members as well. + if domain == "group": + for group_entity_id in group.get_entity_ids(self.hass, entity_id): + self._add(ItemType.ENTITY, group_entity_id) + self._async_resolve_up_entity(group_entity_id) + + # For an script, we want to unwrap the scenes, to ensure we + # relate this script to all referenced entities as well. + if domain == "scene": + for scene_entity_id in scene.entities_in_scene(self.hass, entity_id): + self._add(ItemType.ENTITY, scene_entity_id) + self._async_resolve_up_entity(scene_entity_id) + + # Fully search the script if it is nested. + # This makes the script return all results of the embedded script. + if domain == "script": + self._async_search_script(entity_id, entry_point=False) @callback - def _resolve_script_blueprint(self, blueprint_path: str) -> None: - """Resolve a script blueprint. + def _async_search_script_blueprint(self, blueprint_path: str) -> None: + """Find results for a script blueprint.""" + self._add( + ItemType.SCRIPT, script.scripts_with_blueprint(self.hass, blueprint_path) + ) - Will only be called if blueprint is an entry point. + @callback + def _async_resolve_up_device(self, device_id: str) -> dr.DeviceEntry | None: + """Resolve up from a device. + + Above a device is an area or floor. + Above a device is also the config entry. """ - for entity_id in script.scripts_with_blueprint(self.hass, blueprint_path): - self._add_or_resolve("script", entity_id) + if device_entry := self._device_registry.async_get(device_id): + if device_entry.area_id: + self._add(ItemType.AREA, device_entry.area_id) + self._async_resolve_up_area(device_entry.area_id) + + self._add(ItemType.CONFIG_ENTRY, device_entry.config_entries) + + return device_entry + + @callback + def _async_resolve_up_entity(self, entity_id: str) -> er.RegistryEntry | None: + """Resolve up from an entity. + + Above an entity is a device, area or floor. + Above an entity is also the config entry. + """ + if entity_entry := self._entity_registry.async_get(entity_id): + # Entity has an overridden area + if entity_entry.area_id: + self._add(ItemType.AREA, entity_entry.area_id) + self._async_resolve_up_area(entity_entry.area_id) + + # Inherit area from device + elif entity_entry.device_id and ( + device_entry := self._device_registry.async_get(entity_entry.device_id) + ): + if device_entry.area_id: + self._add(ItemType.AREA, device_entry.area_id) + self._async_resolve_up_area(device_entry.area_id) + + # Add device that provided this entity + self._add(ItemType.DEVICE, entity_entry.device_id) + + # Add config entry that provided this entity + self._add(ItemType.CONFIG_ENTRY, entity_entry.config_entry_id) + elif source := self._entity_sources.get(entity_id): + # Add config entry that provided this entity + self._add(ItemType.CONFIG_ENTRY, source.get("config_entry")) + + return entity_entry + + @callback + def _async_resolve_up_area(self, area_id: str) -> ar.AreaEntry | None: + """Resolve up from an area. + + Above an area can be a floor. + """ + if area_entry := self._area_registry.async_get_area(area_id): + self._add(ItemType.FLOOR, area_entry.floor_id) + + return area_entry diff --git a/homeassistant/components/season/__init__.py b/homeassistant/components/season/__init__.py index f67abee3bea..42af9c4459b 100644 --- a/homeassistant/components/season/__init__.py +++ b/homeassistant/components/season/__init__.py @@ -1,4 +1,5 @@ """The Season integration.""" + from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/season/config_flow.py b/homeassistant/components/season/config_flow.py index 069037e53a0..77c408f4e3f 100644 --- a/homeassistant/components/season/config_flow.py +++ b/homeassistant/components/season/config_flow.py @@ -1,13 +1,13 @@ """Config flow to configure the Season integration.""" + from __future__ import annotations from typing import Any import voluptuous as vol -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_TYPE -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.selector import ( SelectSelector, SelectSelectorConfig, @@ -24,7 +24,7 @@ class SeasonConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" if user_input is not None: await self.async_set_unique_id(user_input[CONF_TYPE]) diff --git a/homeassistant/components/season/const.py b/homeassistant/components/season/const.py index c27d4f5c40e..d3b1827ac9a 100644 --- a/homeassistant/components/season/const.py +++ b/homeassistant/components/season/const.py @@ -1,4 +1,5 @@ """Constants for the Season integration.""" + from typing import Final from homeassistant.const import Platform diff --git a/homeassistant/components/season/icons.json b/homeassistant/components/season/icons.json new file mode 100644 index 00000000000..160ab257338 --- /dev/null +++ b/homeassistant/components/season/icons.json @@ -0,0 +1,15 @@ +{ + "entity": { + "sensor": { + "season": { + "default": "mdi:cloud", + "state": { + "spring": "mdi:flower", + "summer": "mdi:sunglasses", + "autumn": "mdi:leaf", + "winter": "mdi:snowflake" + } + } + } + } +} diff --git a/homeassistant/components/season/sensor.py b/homeassistant/components/season/sensor.py index cfca3c1f9ea..96744db1d02 100644 --- a/homeassistant/components/season/sensor.py +++ b/homeassistant/components/season/sensor.py @@ -1,4 +1,5 @@ """Support for Season sensors.""" + from __future__ import annotations from datetime import date, datetime @@ -32,13 +33,6 @@ HEMISPHERE_SEASON_SWAP = { STATE_SUMMER: STATE_WINTER, } -SEASON_ICONS = { - STATE_SPRING: "mdi:flower", - STATE_SUMMER: "mdi:sunglasses", - STATE_AUTUMN: "mdi:leaf", - STATE_WINTER: "mdi:snowflake", -} - async def async_setup_entry( hass: HomeAssistant, @@ -113,7 +107,3 @@ class SeasonSensorEntity(SensorEntity): self._attr_native_value = get_season( utcnow().replace(tzinfo=None), self.hemisphere, self.type ) - - self._attr_icon = "mdi:cloud" - if self._attr_native_value: - self._attr_icon = SEASON_ICONS[self._attr_native_value] diff --git a/homeassistant/components/select/__init__.py b/homeassistant/components/select/__init__.py index 8ec08f4606f..0c54dfc0aac 100644 --- a/homeassistant/components/select/__init__.py +++ b/homeassistant/components/select/__init__.py @@ -1,4 +1,5 @@ """Component to allow selecting an option from a list as platforms.""" + from __future__ import annotations from datetime import timedelta @@ -163,7 +164,7 @@ class SelectEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): and self.entity_description.options is not None ): return self.entity_description.options - raise AttributeError() + raise AttributeError @cached_property def current_option(self) -> str | None: @@ -178,7 +179,6 @@ class SelectEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): if not options or option not in options: friendly_options: str = ", ".join(options or []) raise ServiceValidationError( - f"Option {option} is not valid for {self.entity_id}", translation_domain=DOMAIN, translation_key="not_valid_option", translation_placeholders={ @@ -196,7 +196,7 @@ class SelectEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): def select_option(self, option: str) -> None: """Change the selected option.""" - raise NotImplementedError() + raise NotImplementedError async def async_select_option(self, option: str) -> None: """Change the selected option.""" diff --git a/homeassistant/components/select/device_action.py b/homeassistant/components/select/device_action.py index a7d47d8c833..a3827a23d41 100644 --- a/homeassistant/components/select/device_action.py +++ b/homeassistant/components/select/device_action.py @@ -1,4 +1,5 @@ """Provides device actions for Select.""" + from __future__ import annotations from contextlib import suppress diff --git a/homeassistant/components/select/device_condition.py b/homeassistant/components/select/device_condition.py index 712e7bf78b6..cd99009dd90 100644 --- a/homeassistant/components/select/device_condition.py +++ b/homeassistant/components/select/device_condition.py @@ -1,4 +1,5 @@ """Provide the device conditions for Select.""" + from __future__ import annotations import voluptuous as vol diff --git a/homeassistant/components/select/device_trigger.py b/homeassistant/components/select/device_trigger.py index 2cd2da0e1a6..b09a25ba082 100644 --- a/homeassistant/components/select/device_trigger.py +++ b/homeassistant/components/select/device_trigger.py @@ -1,4 +1,5 @@ """Provides device triggers for Select.""" + from __future__ import annotations import voluptuous as vol diff --git a/homeassistant/components/select/reproduce_state.py b/homeassistant/components/select/reproduce_state.py index 0b68ae12fdc..88ccda6f07d 100644 --- a/homeassistant/components/select/reproduce_state.py +++ b/homeassistant/components/select/reproduce_state.py @@ -1,4 +1,5 @@ """Reproduce a Select entity state.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/select/significant_change.py b/homeassistant/components/select/significant_change.py index 835db314a38..c9cd6b735d6 100644 --- a/homeassistant/components/select/significant_change.py +++ b/homeassistant/components/select/significant_change.py @@ -1,4 +1,5 @@ """Helper to test significant Select state changes.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/sendgrid/notify.py b/homeassistant/components/sendgrid/notify.py index 25d00fdd3b8..01ceccf781a 100644 --- a/homeassistant/components/sendgrid/notify.py +++ b/homeassistant/components/sendgrid/notify.py @@ -1,4 +1,5 @@ """SendGrid notification service.""" + from __future__ import annotations from http import HTTPStatus diff --git a/homeassistant/components/sense/__init__.py b/homeassistant/components/sense/__init__.py index 392dd33a031..9d909730f5a 100644 --- a/homeassistant/components/sense/__init__.py +++ b/homeassistant/components/sense/__init__.py @@ -1,4 +1,5 @@ """Support for monitoring a Sense energy sensor.""" + from datetime import timedelta import logging diff --git a/homeassistant/components/sense/binary_sensor.py b/homeassistant/components/sense/binary_sensor.py index 094ecbdfcf7..7dde4c029b1 100644 --- a/homeassistant/components/sense/binary_sensor.py +++ b/homeassistant/components/sense/binary_sensor.py @@ -1,4 +1,5 @@ """Support for monitoring a Sense energy sensor device.""" + import logging from homeassistant.components.binary_sensor import ( diff --git a/homeassistant/components/sense/config_flow.py b/homeassistant/components/sense/config_flow.py index 86b68db1e32..e5880675d2b 100644 --- a/homeassistant/components/sense/config_flow.py +++ b/homeassistant/components/sense/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Sense integration.""" + from collections.abc import Mapping import logging from typing import Any @@ -10,9 +11,8 @@ from sense_energy import ( ) import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_CODE, CONF_EMAIL, CONF_PASSWORD, CONF_TIMEOUT -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import ACTIVE_UPDATE_RATE, DEFAULT_TIMEOUT, DOMAIN, SENSE_CONNECT_EXCEPTIONS @@ -28,7 +28,7 @@ DATA_SCHEMA = vol.Schema( ) -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class SenseConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Sense.""" VERSION = 1 @@ -121,7 +121,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="user", data_schema=DATA_SCHEMA, errors=errors ) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Handle configuration by re-auth.""" self._auth_data = dict(entry_data) return await self.async_step_reauth_validate(entry_data) diff --git a/homeassistant/components/sense/sensor.py b/homeassistant/components/sense/sensor.py index 1adfe3ecbd3..199bae43701 100644 --- a/homeassistant/components/sense/sensor.py +++ b/homeassistant/components/sense/sensor.py @@ -70,7 +70,8 @@ TRENDS_SENSOR_TYPES = { SENSOR_VARIANTS = [(PRODUCTION_ID, PRODUCTION_NAME), (CONSUMPTION_ID, CONSUMPTION_NAME)] # Trend production/consumption variants -TREND_SENSOR_VARIANTS = SENSOR_VARIANTS + [ +TREND_SENSOR_VARIANTS = [ + *SENSOR_VARIANTS, (PRODUCTION_PCT_ID, PRODUCTION_PCT_NAME), (NET_PRODUCTION_ID, NET_PRODUCTION_NAME), (FROM_GRID_ID, FROM_GRID_NAME), @@ -127,8 +128,10 @@ async def async_setup_entry( ) ) - for i in range(len(data.active_voltage)): - entities.append(SenseVoltageSensor(data, i, sense_monitor_id)) + entities.extend( + SenseVoltageSensor(data, i, sense_monitor_id) + for i in range(len(data.active_voltage)) + ) for type_id, typ in TRENDS_SENSOR_TYPES.items(): for variant_id, variant_name in TREND_SENSOR_VARIANTS: diff --git a/homeassistant/components/sensibo/__init__.py b/homeassistant/components/sensibo/__init__.py index 9a278d0c4df..b14d06c5811 100644 --- a/homeassistant/components/sensibo/__init__.py +++ b/homeassistant/components/sensibo/__init__.py @@ -1,4 +1,5 @@ """The Sensibo component.""" + from __future__ import annotations from pysensibo.exceptions import AuthenticationError diff --git a/homeassistant/components/sensibo/binary_sensor.py b/homeassistant/components/sensibo/binary_sensor.py index 5cd71a2b0e4..a34c7884ac7 100644 --- a/homeassistant/components/sensibo/binary_sensor.py +++ b/homeassistant/components/sensibo/binary_sensor.py @@ -1,4 +1,5 @@ """Binary Sensor platform for Sensibo integration.""" + from __future__ import annotations from collections.abc import Callable @@ -24,34 +25,20 @@ from .entity import SensiboDeviceBaseEntity, SensiboMotionBaseEntity PARALLEL_UPDATES = 0 -@dataclass(frozen=True) -class MotionBaseEntityDescriptionMixin: - """Mixin for required Sensibo base description keys.""" +@dataclass(frozen=True, kw_only=True) +class SensiboMotionBinarySensorEntityDescription(BinarySensorEntityDescription): + """Describes Sensibo Motion sensor entity.""" value_fn: Callable[[MotionSensor], bool | None] -@dataclass(frozen=True) -class DeviceBaseEntityDescriptionMixin: - """Mixin for required Sensibo base description keys.""" +@dataclass(frozen=True, kw_only=True) +class SensiboDeviceBinarySensorEntityDescription(BinarySensorEntityDescription): + """Describes Sensibo Motion sensor entity.""" value_fn: Callable[[SensiboDevice], bool | None] -@dataclass(frozen=True) -class SensiboMotionBinarySensorEntityDescription( - BinarySensorEntityDescription, MotionBaseEntityDescriptionMixin -): - """Describes Sensibo Motion sensor entity.""" - - -@dataclass(frozen=True) -class SensiboDeviceBinarySensorEntityDescription( - BinarySensorEntityDescription, DeviceBaseEntityDescriptionMixin -): - """Describes Sensibo Motion sensor entity.""" - - FILTER_CLEAN_REQUIRED_DESCRIPTION = SensiboDeviceBinarySensorEntityDescription( key="filter_clean", translation_key="filter_clean", @@ -70,13 +57,11 @@ MOTION_SENSOR_TYPES: tuple[SensiboMotionBinarySensorEntityDescription, ...] = ( key="is_main_sensor", translation_key="is_main_sensor", entity_category=EntityCategory.DIAGNOSTIC, - icon="mdi:connection", value_fn=lambda data: data.is_main_sensor, ), SensiboMotionBinarySensorEntityDescription( key="motion", device_class=BinarySensorDeviceClass.MOTION, - icon="mdi:motion-sensor", value_fn=lambda data: data.motion, ), ) @@ -86,7 +71,6 @@ MOTION_DEVICE_SENSOR_TYPES: tuple[SensiboDeviceBinarySensorEntityDescription, .. key="room_occupied", translation_key="room_occupied", device_class=BinarySensorDeviceClass.MOTION, - icon="mdi:motion-sensor", value_fn=lambda data: data.room_occupied, ), ) diff --git a/homeassistant/components/sensibo/button.py b/homeassistant/components/sensibo/button.py index 942f7eaeb00..fbfabaa97fb 100644 --- a/homeassistant/components/sensibo/button.py +++ b/homeassistant/components/sensibo/button.py @@ -1,4 +1,5 @@ """Button platform for Sensibo integration.""" + from __future__ import annotations from dataclasses import dataclass @@ -17,24 +18,16 @@ from .entity import SensiboDeviceBaseEntity, async_handle_api_call PARALLEL_UPDATES = 0 -@dataclass(frozen=True) -class SensiboEntityDescriptionMixin: - """Mixin values for Sensibo entities.""" +@dataclass(frozen=True, kw_only=True) +class SensiboButtonEntityDescription(ButtonEntityDescription): + """Class describing Sensibo Button entities.""" data_key: str -@dataclass(frozen=True) -class SensiboButtonEntityDescription( - ButtonEntityDescription, SensiboEntityDescriptionMixin -): - """Class describing Sensibo Button entities.""" - - DEVICE_BUTTON_TYPES = SensiboButtonEntityDescription( key="reset_filter", translation_key="reset_filter", - icon="mdi:air-filter", entity_category=EntityCategory.CONFIG, data_key="filter_clean", ) diff --git a/homeassistant/components/sensibo/climate.py b/homeassistant/components/sensibo/climate.py index 0ad2a0a714f..f7661a3ee80 100644 --- a/homeassistant/components/sensibo/climate.py +++ b/homeassistant/components/sensibo/climate.py @@ -1,4 +1,5 @@ """Support for Sensibo wifi-enabled home thermostats.""" + from __future__ import annotations from bisect import bisect_left @@ -229,11 +230,9 @@ class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity): @property def hvac_modes(self) -> list[HVACMode]: """Return the list of available hvac operation modes.""" - hvac_modes = [] if TYPE_CHECKING: assert self.device_data.hvac_modes - for mode in self.device_data.hvac_modes: - hvac_modes.append(SENSIBO_TO_HA[mode]) + hvac_modes = [SENSIBO_TO_HA[mode] for mode in self.device_data.hvac_modes] return hvac_modes if hvac_modes else [HVACMode.OFF] @property @@ -315,14 +314,12 @@ class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity): """Set new target temperature.""" if "targetTemperature" not in self.device_data.active_features: raise HomeAssistantError( - "Current mode doesn't support setting Target Temperature", translation_domain=DOMAIN, translation_key="no_target_temperature_in_features", ) if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: raise ServiceValidationError( - "No target temperature provided", translation_domain=DOMAIN, translation_key="no_target_temperature", ) @@ -342,13 +339,11 @@ class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity): """Set new target fan mode.""" if "fanLevel" not in self.device_data.active_features: raise HomeAssistantError( - "Current mode doesn't support setting Fanlevel", translation_domain=DOMAIN, translation_key="no_fan_level_in_features", ) if fan_mode not in AVAILABLE_FAN_MODES: raise HomeAssistantError( - f"Climate fan mode {fan_mode} is not supported by the integration, please open an issue", translation_domain=DOMAIN, translation_key="fan_mode_not_supported", translation_placeholders={"fan_mode": fan_mode}, @@ -394,13 +389,11 @@ class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity): """Set new target swing operation.""" if "swing" not in self.device_data.active_features: raise HomeAssistantError( - "Current mode doesn't support setting Swing", translation_domain=DOMAIN, translation_key="no_swing_in_features", ) if swing_mode not in AVAILABLE_SWING_MODES: raise HomeAssistantError( - f"Climate swing mode {swing_mode} is not supported by the integration, please open an issue", translation_domain=DOMAIN, translation_key="swing_not_supported", translation_placeholders={"swing_mode": swing_mode}, diff --git a/homeassistant/components/sensibo/config_flow.py b/homeassistant/components/sensibo/config_flow.py index d826e854fa0..667f96fe1c2 100644 --- a/homeassistant/components/sensibo/config_flow.py +++ b/homeassistant/components/sensibo/config_flow.py @@ -1,4 +1,5 @@ """Adds config flow for Sensibo integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -7,9 +8,8 @@ from typing import Any from pysensibo.exceptions import AuthenticationError import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_API_KEY -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.selector import TextSelector from .const import DEFAULT_NAME, DOMAIN @@ -22,14 +22,16 @@ DATA_SCHEMA = vol.Schema( ) -class SensiboConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class SensiboConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Sensibo integration.""" VERSION = 2 - entry: config_entries.ConfigEntry | None + entry: ConfigEntry | None - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Handle re-authentication with Sensibo.""" self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) @@ -37,7 +39,7 @@ class SensiboConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm re-authentication with Sensibo.""" errors: dict[str, str] = {} @@ -74,7 +76,7 @@ class SensiboConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors: dict[str, str] = {} diff --git a/homeassistant/components/sensibo/coordinator.py b/homeassistant/components/sensibo/coordinator.py index 1cdcbd79932..4f4f76aba10 100644 --- a/homeassistant/components/sensibo/coordinator.py +++ b/homeassistant/components/sensibo/coordinator.py @@ -1,4 +1,5 @@ """DataUpdateCoordinator for the Sensibo integration.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/sensibo/diagnostics.py b/homeassistant/components/sensibo/diagnostics.py index 32ad07871a3..d00da7e1223 100644 --- a/homeassistant/components/sensibo/diagnostics.py +++ b/homeassistant/components/sensibo/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for Sensibo.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/sensibo/entity.py b/homeassistant/components/sensibo/entity.py index 5a755a7730c..97ef4dffca7 100644 --- a/homeassistant/components/sensibo/entity.py +++ b/homeassistant/components/sensibo/entity.py @@ -1,4 +1,5 @@ """Base entity for Sensibo integration.""" + from __future__ import annotations import asyncio @@ -33,7 +34,6 @@ def async_handle_api_call( res = await function(entity, *args, **kwargs) except SENSIBO_ERRORS as err: raise HomeAssistantError( - str(err), translation_domain=DOMAIN, translation_key="service_raised", translation_placeholders={"error": str(err), "name": entity.name}, @@ -42,7 +42,6 @@ def async_handle_api_call( LOGGER.debug("Result %s for entity %s with arguments %s", res, entity, kwargs) if res is not True: raise HomeAssistantError( - f"Could not execute service for {entity.name}", translation_domain=DOMAIN, translation_key="service_result_not_true", translation_placeholders={"name": entity.name}, diff --git a/homeassistant/components/sensibo/icons.json b/homeassistant/components/sensibo/icons.json new file mode 100644 index 00000000000..e26840e48eb --- /dev/null +++ b/homeassistant/components/sensibo/icons.json @@ -0,0 +1,54 @@ +{ + "entity": { + "binary_sensor": { + "is_main_sensor": { + "default": "mdi:connection" + } + }, + "button": { + "reset_filter": { + "default": "mdi:air-filter" + } + }, + "select": { + "horizontalswing": { + "default": "mdi:air-conditioner" + }, + "light": { + "default": "mdi:flashlight" + } + }, + "sensor": { + "filter_last_reset": { + "default": "mdi:timer" + }, + "battery_voltage": { + "default": "mdi:battery" + }, + "sensitivity": { + "default": "mdi:air-filter" + }, + "timer_time": { + "default": "mdi:timer" + }, + "airq_tvoc": { + "default": "mdi:air-filter" + } + }, + "switch": { + "timer_on_switch": { + "default": "mdi:timer" + }, + "climate_react_switch": { + "default": "mdi:wizard-hat" + } + } + }, + "services": { + "assume_state": "mdi:shape-outline", + "enable_timer": "mdi:timer-play", + "enable_pure_boost": "mdi:air-filter", + "full_state": "mdi:shape", + "enable_climate_react": "mdi:wizard-hat" + } +} diff --git a/homeassistant/components/sensibo/number.py b/homeassistant/components/sensibo/number.py index ac76277fb20..9c7b97ff79f 100644 --- a/homeassistant/components/sensibo/number.py +++ b/homeassistant/components/sensibo/number.py @@ -1,4 +1,5 @@ """Number platform for Sensibo integration.""" + from __future__ import annotations from collections.abc import Callable @@ -24,21 +25,14 @@ from .entity import SensiboDeviceBaseEntity, async_handle_api_call PARALLEL_UPDATES = 0 -@dataclass(frozen=True) -class SensiboEntityDescriptionMixin: - """Mixin values for Sensibo entities.""" +@dataclass(frozen=True, kw_only=True) +class SensiboNumberEntityDescription(NumberEntityDescription): + """Class describing Sensibo Number entities.""" remote_key: str value_fn: Callable[[SensiboDevice], float | None] -@dataclass(frozen=True) -class SensiboNumberEntityDescription( - NumberEntityDescription, SensiboEntityDescriptionMixin -): - """Class describing Sensibo Number entities.""" - - DEVICE_NUMBER_TYPES = ( SensiboNumberEntityDescription( key="calibration_temp", diff --git a/homeassistant/components/sensibo/select.py b/homeassistant/components/sensibo/select.py index bbac3fbdbd0..798d4735b16 100644 --- a/homeassistant/components/sensibo/select.py +++ b/homeassistant/components/sensibo/select.py @@ -1,4 +1,5 @@ """Select platform for Sensibo integration.""" + from __future__ import annotations from collections.abc import Callable @@ -20,9 +21,9 @@ from .entity import SensiboDeviceBaseEntity, async_handle_api_call PARALLEL_UPDATES = 0 -@dataclass(frozen=True) -class SensiboSelectDescriptionMixin: - """Mixin values for Sensibo entities.""" +@dataclass(frozen=True, kw_only=True) +class SensiboSelectEntityDescription(SelectEntityDescription): + """Class describing Sensibo Select entities.""" data_key: str value_fn: Callable[[SensiboDevice], str | None] @@ -30,18 +31,10 @@ class SensiboSelectDescriptionMixin: transformation: Callable[[SensiboDevice], dict | None] -@dataclass(frozen=True) -class SensiboSelectEntityDescription( - SelectEntityDescription, SensiboSelectDescriptionMixin -): - """Class describing Sensibo Select entities.""" - - DEVICE_SELECT_TYPES = ( SensiboSelectEntityDescription( key="horizontalSwing", data_key="horizontal_swing_mode", - icon="mdi:air-conditioner", value_fn=lambda data: data.horizontal_swing_mode, options_fn=lambda data: data.horizontal_swing_modes, translation_key="horizontalswing", @@ -50,7 +43,6 @@ DEVICE_SELECT_TYPES = ( SensiboSelectEntityDescription( key="light", data_key="light_mode", - icon="mdi:flashlight", value_fn=lambda data: data.light_mode, options_fn=lambda data: data.light_modes, translation_key="light", @@ -108,8 +100,6 @@ class SensiboSelect(SensiboDeviceBaseEntity, SelectEntity): if self.entity_description.key not in self.device_data.active_features: hvac_mode = self.device_data.hvac_mode if self.device_data.hvac_mode else "" raise HomeAssistantError( - f"Current mode {self.device_data.hvac_mode} doesn't support setting" - f" {self.entity_description.name}", translation_domain=DOMAIN, translation_key="select_option_not_available", translation_placeholders={ diff --git a/homeassistant/components/sensibo/sensor.py b/homeassistant/components/sensibo/sensor.py index 805b888204b..0a2b23b2cd9 100644 --- a/homeassistant/components/sensibo/sensor.py +++ b/homeassistant/components/sensibo/sensor.py @@ -1,4 +1,5 @@ """Sensor platform for Sensibo integration.""" + from __future__ import annotations from collections.abc import Callable, Mapping @@ -36,40 +37,25 @@ from .entity import SensiboDeviceBaseEntity, SensiboMotionBaseEntity PARALLEL_UPDATES = 0 -@dataclass(frozen=True) -class MotionBaseEntityDescriptionMixin: - """Mixin for required Sensibo base description keys.""" +@dataclass(frozen=True, kw_only=True) +class SensiboMotionSensorEntityDescription(SensorEntityDescription): + """Describes Sensibo Motion sensor entity.""" value_fn: Callable[[MotionSensor], StateType] -@dataclass(frozen=True) -class DeviceBaseEntityDescriptionMixin: - """Mixin for required Sensibo base description keys.""" +@dataclass(frozen=True, kw_only=True) +class SensiboDeviceSensorEntityDescription(SensorEntityDescription): + """Describes Sensibo Device sensor entity.""" value_fn: Callable[[SensiboDevice], StateType | datetime] extra_fn: Callable[[SensiboDevice], dict[str, str | bool | None] | None] | None -@dataclass(frozen=True) -class SensiboMotionSensorEntityDescription( - SensorEntityDescription, MotionBaseEntityDescriptionMixin -): - """Describes Sensibo Motion sensor entity.""" - - -@dataclass(frozen=True) -class SensiboDeviceSensorEntityDescription( - SensorEntityDescription, DeviceBaseEntityDescriptionMixin -): - """Describes Sensibo Device sensor entity.""" - - FILTER_LAST_RESET_DESCRIPTION = SensiboDeviceSensorEntityDescription( key="filter_last_reset", translation_key="filter_last_reset", device_class=SensorDeviceClass.TIMESTAMP, - icon="mdi:timer", value_fn=lambda data: data.filter_last_reset, extra_fn=None, ) @@ -82,7 +68,6 @@ MOTION_SENSOR_TYPES: tuple[SensiboMotionSensorEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, state_class=SensorStateClass.MEASUREMENT, - icon="mdi:wifi", value_fn=lambda data: data.rssi, entity_registry_enabled_default=False, ), @@ -93,7 +78,6 @@ MOTION_SENSOR_TYPES: tuple[SensiboMotionSensorEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=UnitOfElectricPotential.VOLT, state_class=SensorStateClass.MEASUREMENT, - icon="mdi:battery", value_fn=lambda data: data.battery_voltage, ), SensiboMotionSensorEntityDescription( @@ -101,7 +85,6 @@ MOTION_SENSOR_TYPES: tuple[SensiboMotionSensorEntityDescription, ...] = ( device_class=SensorDeviceClass.HUMIDITY, native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, - icon="mdi:water", value_fn=lambda data: data.humidity, ), SensiboMotionSensorEntityDescription( @@ -109,7 +92,6 @@ MOTION_SENSOR_TYPES: tuple[SensiboMotionSensorEntityDescription, ...] = ( device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=UnitOfTemperature.CELSIUS, state_class=SensorStateClass.MEASUREMENT, - icon="mdi:thermometer", value_fn=lambda data: data.temperature, ), ) @@ -119,14 +101,12 @@ PURE_SENSOR_TYPES: tuple[SensiboDeviceSensorEntityDescription, ...] = ( device_class=SensorDeviceClass.PM25, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, state_class=SensorStateClass.MEASUREMENT, - icon="mdi:air-filter", value_fn=lambda data: data.pm25, extra_fn=None, ), SensiboDeviceSensorEntityDescription( key="pure_sensitivity", translation_key="sensitivity", - icon="mdi:air-filter", value_fn=lambda data: data.pure_sensitivity, extra_fn=None, ), @@ -138,7 +118,6 @@ DEVICE_SENSOR_TYPES: tuple[SensiboDeviceSensorEntityDescription, ...] = ( key="timer_time", translation_key="timer_time", device_class=SensorDeviceClass.TIMESTAMP, - icon="mdi:timer", value_fn=lambda data: data.timer_time, extra_fn=lambda data: {"id": data.timer_id, "turn_on": data.timer_state_on}, ), @@ -188,7 +167,6 @@ AIRQ_SENSOR_TYPES: tuple[SensiboDeviceSensorEntityDescription, ...] = ( translation_key="airq_tvoc", native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION, state_class=SensorStateClass.MEASUREMENT, - icon="mdi:air-filter", value_fn=lambda data: data.tvoc, extra_fn=None, ), diff --git a/homeassistant/components/sensibo/switch.py b/homeassistant/components/sensibo/switch.py index 0911985ed7d..a8ebd63fa43 100644 --- a/homeassistant/components/sensibo/switch.py +++ b/homeassistant/components/sensibo/switch.py @@ -1,4 +1,5 @@ """Switch platform for Sensibo integration.""" + from __future__ import annotations from collections.abc import Callable, Mapping @@ -24,9 +25,9 @@ from .entity import SensiboDeviceBaseEntity, async_handle_api_call PARALLEL_UPDATES = 0 -@dataclass(frozen=True) -class DeviceBaseEntityDescriptionMixin: - """Mixin for required Sensibo Device description keys.""" +@dataclass(frozen=True, kw_only=True) +class SensiboDeviceSwitchEntityDescription(SwitchEntityDescription): + """Describes Sensibo Switch entity.""" value_fn: Callable[[SensiboDevice], bool | None] extra_fn: Callable[[SensiboDevice], dict[str, str | bool | None]] | None @@ -35,19 +36,11 @@ class DeviceBaseEntityDescriptionMixin: data_key: str -@dataclass(frozen=True) -class SensiboDeviceSwitchEntityDescription( - SwitchEntityDescription, DeviceBaseEntityDescriptionMixin -): - """Describes Sensibo Switch entity.""" - - DEVICE_SWITCH_TYPES: tuple[SensiboDeviceSwitchEntityDescription, ...] = ( SensiboDeviceSwitchEntityDescription( key="timer_on_switch", translation_key="timer_on_switch", device_class=SwitchDeviceClass.SWITCH, - icon="mdi:timer", value_fn=lambda data: data.timer_on, extra_fn=lambda data: {"id": data.timer_id, "turn_on": data.timer_state_on}, command_on="async_turn_on_timer", @@ -58,7 +51,6 @@ DEVICE_SWITCH_TYPES: tuple[SensiboDeviceSwitchEntityDescription, ...] = ( key="climate_react_switch", translation_key="climate_react_switch", device_class=SwitchDeviceClass.SWITCH, - icon="mdi:wizard-hat", value_fn=lambda data: data.smart_on, extra_fn=lambda data: {"type": data.smart_type}, command_on="async_turn_on_off_smart", @@ -183,8 +175,6 @@ class SensiboDeviceSwitch(SensiboDeviceBaseEntity, SwitchEntity): """Make service call to api for setting Climate React.""" if self.device_data.smart_type is None: raise HomeAssistantError( - "Use Sensibo Enable Climate React Service once to enable switch or the" - " Sensibo app", translation_domain=DOMAIN, translation_key="climate_react_not_available", ) diff --git a/homeassistant/components/sensibo/update.py b/homeassistant/components/sensibo/update.py index c51d57dd9d1..9376cd1eb38 100644 --- a/homeassistant/components/sensibo/update.py +++ b/homeassistant/components/sensibo/update.py @@ -1,4 +1,5 @@ """Update platform for Sensibo integration.""" + from __future__ import annotations from collections.abc import Callable @@ -23,27 +24,19 @@ from .entity import SensiboDeviceBaseEntity PARALLEL_UPDATES = 0 -@dataclass(frozen=True) -class DeviceBaseEntityDescriptionMixin: - """Mixin for required Sensibo base description keys.""" +@dataclass(frozen=True, kw_only=True) +class SensiboDeviceUpdateEntityDescription(UpdateEntityDescription): + """Describes Sensibo Update entity.""" value_version: Callable[[SensiboDevice], str | None] value_available: Callable[[SensiboDevice], str | None] -@dataclass(frozen=True) -class SensiboDeviceUpdateEntityDescription( - UpdateEntityDescription, DeviceBaseEntityDescriptionMixin -): - """Describes Sensibo Update entity.""" - - DEVICE_SENSOR_TYPES: tuple[SensiboDeviceUpdateEntityDescription, ...] = ( SensiboDeviceUpdateEntityDescription( key="fw_ver_available", device_class=UpdateDeviceClass.FIRMWARE, entity_category=EntityCategory.DIAGNOSTIC, - icon="mdi:rocket-launch", value_version=lambda data: data.fw_ver, value_available=lambda data: data.fw_ver_available, ), diff --git a/homeassistant/components/sensibo/util.py b/homeassistant/components/sensibo/util.py index 98b843a9dfc..3c750b2f017 100644 --- a/homeassistant/components/sensibo/util.py +++ b/homeassistant/components/sensibo/util.py @@ -1,4 +1,5 @@ """Utils for Sensibo integration.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/sensirion_ble/__init__.py b/homeassistant/components/sensirion_ble/__init__.py index 66e6f7c250b..5ea5593004e 100644 --- a/homeassistant/components/sensirion_ble/__init__.py +++ b/homeassistant/components/sensirion_ble/__init__.py @@ -1,4 +1,5 @@ """The sensirion_ble integration.""" + from __future__ import annotations import logging @@ -25,14 +26,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: address = entry.unique_id assert address is not None data = SensirionBluetoothDeviceData() - coordinator = hass.data.setdefault(DOMAIN, {})[ - entry.entry_id - ] = PassiveBluetoothProcessorCoordinator( - hass, - _LOGGER, - address=address, - mode=BluetoothScanningMode.ACTIVE, - update_method=data.update, + coordinator = hass.data.setdefault(DOMAIN, {})[entry.entry_id] = ( + PassiveBluetoothProcessorCoordinator( + hass, + _LOGGER, + address=address, + mode=BluetoothScanningMode.ACTIVE, + update_method=data.update, + ) ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload( diff --git a/homeassistant/components/sensirion_ble/config_flow.py b/homeassistant/components/sensirion_ble/config_flow.py index 0442b6d16c3..066a4a69f9a 100644 --- a/homeassistant/components/sensirion_ble/config_flow.py +++ b/homeassistant/components/sensirion_ble/config_flow.py @@ -1,4 +1,5 @@ """Config flow for sensirion_ble.""" + from __future__ import annotations from typing import Any @@ -10,9 +11,8 @@ from homeassistant.components.bluetooth import ( BluetoothServiceInfoBleak, async_discovered_service_info, ) -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_ADDRESS -from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN @@ -30,7 +30,7 @@ class SensirionConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_bluetooth( self, discovery_info: BluetoothServiceInfoBleak - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the bluetooth discovery step.""" await self.async_set_unique_id(discovery_info.address) self._abort_if_unique_id_configured() @@ -43,7 +43,7 @@ class SensirionConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_bluetooth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm discovery.""" assert self._discovered_device is not None device = self._discovered_device @@ -62,7 +62,7 @@ class SensirionConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the user step to pick discovered device.""" if user_input is not None: address = user_input[CONF_ADDRESS] diff --git a/homeassistant/components/sensirion_ble/sensor.py b/homeassistant/components/sensirion_ble/sensor.py index 3d288f92d12..2ca5a524c8f 100644 --- a/homeassistant/components/sensirion_ble/sensor.py +++ b/homeassistant/components/sensirion_ble/sensor.py @@ -1,4 +1,5 @@ """Support for Sensirion sensors.""" + from __future__ import annotations from sensor_state_data import ( diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 9f525c3d498..92499a05af4 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -1,4 +1,5 @@ """Component to interface with various sensors that can be monitored.""" + from __future__ import annotations import asyncio @@ -10,9 +11,7 @@ from decimal import Decimal, InvalidOperation as DecimalInvalidOperation from functools import partial import logging from math import ceil, floor, isfinite, log10 -from typing import TYPE_CHECKING, Any, Final, Self, cast, final - -from typing_extensions import override +from typing import TYPE_CHECKING, Any, Final, Self, cast, final, override from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( # noqa: F401 @@ -69,6 +68,7 @@ from homeassistant.helpers.typing import UNDEFINED, ConfigType, StateType, Undef from homeassistant.util import dt as dt_util from homeassistant.util.enum import try_parse_enum +from . import group as group_pre_import # noqa: F401 from .const import ( # noqa: F401 _DEPRECATED_STATE_CLASS_MEASUREMENT, _DEPRECATED_STATE_CLASS_TOTAL, @@ -870,9 +870,9 @@ class SensorExtraStoredData(ExtraStoredData): def as_dict(self) -> dict[str, Any]: """Return a dict representation of the sensor data.""" - native_value: StateType | date | datetime | Decimal | dict[ - str, str - ] = self.native_value + native_value: StateType | date | datetime | Decimal | dict[str, str] = ( + self.native_value + ) if isinstance(native_value, (date, datetime)): native_value = { "__type": str(type(native_value)), diff --git a/homeassistant/components/sensor/const.py b/homeassistant/components/sensor/const.py index 3dc8f878791..cc89908f00d 100644 --- a/homeassistant/components/sensor/const.py +++ b/homeassistant/components/sensor/const.py @@ -1,4 +1,5 @@ """Constants for sensor.""" + from __future__ import annotations from enum import StrEnum @@ -344,6 +345,7 @@ class SensorDeviceClass(StrEnum): - SI /metric: `mm/d`, `mm/h`, `m/s`, `km/h` - USCS / imperial: `in/d`, `in/h`, `ft/s`, `mph` - Nautical: `kn` + - Beaufort: `Beaufort` """ SULPHUR_DIOXIDE = "sulphur_dioxide" @@ -431,6 +433,7 @@ class SensorDeviceClass(StrEnum): - SI /metric: `m/s`, `km/h` - USCS / imperial: `ft/s`, `mph` - Nautical: `kn` + - Beaufort: `Beaufort` """ diff --git a/homeassistant/components/sensor/device_condition.py b/homeassistant/components/sensor/device_condition.py index b7cf533d3da..fb605d9419c 100644 --- a/homeassistant/components/sensor/device_condition.py +++ b/homeassistant/components/sensor/device_condition.py @@ -1,4 +1,5 @@ """Provides device conditions for sensors.""" + from __future__ import annotations import voluptuous as vol diff --git a/homeassistant/components/sensor/device_trigger.py b/homeassistant/components/sensor/device_trigger.py index c5c19a19d0b..b46f6260285 100644 --- a/homeassistant/components/sensor/device_trigger.py +++ b/homeassistant/components/sensor/device_trigger.py @@ -1,4 +1,5 @@ """Provides device triggers for sensors.""" + import voluptuous as vol from homeassistant.components.device_automation import ( diff --git a/homeassistant/components/sensor/group.py b/homeassistant/components/sensor/group.py index 2ac081496cd..13a70cc4b6b 100644 --- a/homeassistant/components/sensor/group.py +++ b/homeassistant/components/sensor/group.py @@ -1,13 +1,16 @@ """Describe group states.""" +from typing import TYPE_CHECKING -from homeassistant.components.group import GroupIntegrationRegistry from homeassistant.core import HomeAssistant, callback +if TYPE_CHECKING: + from homeassistant.components.group import GroupIntegrationRegistry + @callback def async_describe_on_off_states( - hass: HomeAssistant, registry: GroupIntegrationRegistry + hass: HomeAssistant, registry: "GroupIntegrationRegistry" ) -> None: """Describe group on off states.""" registry.exclude_domain() diff --git a/homeassistant/components/sensor/helpers.py b/homeassistant/components/sensor/helpers.py index a3f5e3827bf..12a5dcefdf8 100644 --- a/homeassistant/components/sensor/helpers.py +++ b/homeassistant/components/sensor/helpers.py @@ -1,4 +1,5 @@ """Helpers for sensor entities.""" + from __future__ import annotations from datetime import date, datetime diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index a53ae906718..97ad49fb937 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -1,4 +1,5 @@ """Statistics helper for sensor.""" + from __future__ import annotations from collections import defaultdict diff --git a/homeassistant/components/sensor/significant_change.py b/homeassistant/components/sensor/significant_change.py index f426674c32d..f320a7efcdf 100644 --- a/homeassistant/components/sensor/significant_change.py +++ b/homeassistant/components/sensor/significant_change.py @@ -1,4 +1,5 @@ """Helper to test significant sensor state changes.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/sensor/websocket_api.py b/homeassistant/components/sensor/websocket_api.py index a98c4b25392..2110ccc7253 100644 --- a/homeassistant/components/sensor/websocket_api.py +++ b/homeassistant/components/sensor/websocket_api.py @@ -1,4 +1,5 @@ """The sensor websocket API.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/sensorpro/__init__.py b/homeassistant/components/sensorpro/__init__.py index 43c87ad32ee..be15b65e0f9 100644 --- a/homeassistant/components/sensorpro/__init__.py +++ b/homeassistant/components/sensorpro/__init__.py @@ -1,4 +1,5 @@ """The SensorPro integration.""" + from __future__ import annotations import logging @@ -25,14 +26,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: address = entry.unique_id assert address is not None data = SensorProBluetoothDeviceData() - coordinator = hass.data.setdefault(DOMAIN, {})[ - entry.entry_id - ] = PassiveBluetoothProcessorCoordinator( - hass, - _LOGGER, - address=address, - mode=BluetoothScanningMode.PASSIVE, - update_method=data.update, + coordinator = hass.data.setdefault(DOMAIN, {})[entry.entry_id] = ( + PassiveBluetoothProcessorCoordinator( + hass, + _LOGGER, + address=address, + mode=BluetoothScanningMode.PASSIVE, + update_method=data.update, + ) ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload( diff --git a/homeassistant/components/sensorpro/config_flow.py b/homeassistant/components/sensorpro/config_flow.py index 182a35880ab..ce26d70a659 100644 --- a/homeassistant/components/sensorpro/config_flow.py +++ b/homeassistant/components/sensorpro/config_flow.py @@ -1,4 +1,5 @@ """Config flow for sensorpro ble integration.""" + from __future__ import annotations from typing import Any @@ -10,9 +11,8 @@ from homeassistant.components.bluetooth import ( BluetoothServiceInfoBleak, async_discovered_service_info, ) -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_ADDRESS -from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN @@ -30,7 +30,7 @@ class SensorProConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_bluetooth( self, discovery_info: BluetoothServiceInfoBleak - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the bluetooth discovery step.""" await self.async_set_unique_id(discovery_info.address) self._abort_if_unique_id_configured() @@ -43,7 +43,7 @@ class SensorProConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_bluetooth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm discovery.""" assert self._discovered_device is not None device = self._discovered_device @@ -62,7 +62,7 @@ class SensorProConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the user step to pick discovered device.""" if user_input is not None: address = user_input[CONF_ADDRESS] diff --git a/homeassistant/components/sensorpro/device.py b/homeassistant/components/sensorpro/device.py index 326eb8b8bbd..38b94a19452 100644 --- a/homeassistant/components/sensorpro/device.py +++ b/homeassistant/components/sensorpro/device.py @@ -1,4 +1,5 @@ """Support for SensorPro devices.""" + from __future__ import annotations from sensorpro_ble import DeviceKey diff --git a/homeassistant/components/sensorpro/sensor.py b/homeassistant/components/sensorpro/sensor.py index 9ac4b10b99e..536a3c6b775 100644 --- a/homeassistant/components/sensorpro/sensor.py +++ b/homeassistant/components/sensorpro/sensor.py @@ -1,4 +1,5 @@ """Support for SensorPro sensors.""" + from __future__ import annotations from sensorpro_ble import ( diff --git a/homeassistant/components/sensorpush/__init__.py b/homeassistant/components/sensorpush/__init__.py index 7828a581d07..1a479caacf2 100644 --- a/homeassistant/components/sensorpush/__init__.py +++ b/homeassistant/components/sensorpush/__init__.py @@ -1,4 +1,5 @@ """The SensorPush Bluetooth integration.""" + from __future__ import annotations import logging @@ -25,14 +26,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: address = entry.unique_id assert address is not None data = SensorPushBluetoothDeviceData() - coordinator = hass.data.setdefault(DOMAIN, {})[ - entry.entry_id - ] = PassiveBluetoothProcessorCoordinator( - hass, - _LOGGER, - address=address, - mode=BluetoothScanningMode.PASSIVE, - update_method=data.update, + coordinator = hass.data.setdefault(DOMAIN, {})[entry.entry_id] = ( + PassiveBluetoothProcessorCoordinator( + hass, + _LOGGER, + address=address, + mode=BluetoothScanningMode.PASSIVE, + update_method=data.update, + ) ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload( diff --git a/homeassistant/components/sensorpush/config_flow.py b/homeassistant/components/sensorpush/config_flow.py index 9913b7f7b09..d826029276b 100644 --- a/homeassistant/components/sensorpush/config_flow.py +++ b/homeassistant/components/sensorpush/config_flow.py @@ -1,4 +1,5 @@ """Config flow for sensorpush integration.""" + from __future__ import annotations from typing import Any @@ -10,9 +11,8 @@ from homeassistant.components.bluetooth import ( BluetoothServiceInfoBleak, async_discovered_service_info, ) -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_ADDRESS -from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN @@ -30,7 +30,7 @@ class SensorPushConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_bluetooth( self, discovery_info: BluetoothServiceInfoBleak - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the bluetooth discovery step.""" await self.async_set_unique_id(discovery_info.address) self._abort_if_unique_id_configured() @@ -43,7 +43,7 @@ class SensorPushConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_bluetooth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm discovery.""" assert self._discovered_device is not None device = self._discovered_device @@ -62,7 +62,7 @@ class SensorPushConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the user step to pick discovered device.""" if user_input is not None: address = user_input[CONF_ADDRESS] diff --git a/homeassistant/components/sensorpush/sensor.py b/homeassistant/components/sensorpush/sensor.py index e12bf0e48c6..20d97a32415 100644 --- a/homeassistant/components/sensorpush/sensor.py +++ b/homeassistant/components/sensorpush/sensor.py @@ -1,4 +1,5 @@ """Support for sensorpush ble sensors.""" + from __future__ import annotations from sensorpush_ble import DeviceClass, DeviceKey, SensorUpdate, Units diff --git a/homeassistant/components/sentry/__init__.py b/homeassistant/components/sentry/__init__.py index 5e4fb80688d..dcbcc59a749 100644 --- a/homeassistant/components/sentry/__init__.py +++ b/homeassistant/components/sentry/__init__.py @@ -1,4 +1,5 @@ """The sentry integration.""" + from __future__ import annotations import re diff --git a/homeassistant/components/sentry/config_flow.py b/homeassistant/components/sentry/config_flow.py index 4fe2b0cc503..b10409caf38 100644 --- a/homeassistant/components/sentry/config_flow.py +++ b/homeassistant/components/sentry/config_flow.py @@ -1,4 +1,5 @@ """Config flow for sentry integration.""" + from __future__ import annotations import logging @@ -7,9 +8,13 @@ from typing import Any from sentry_sdk.utils import BadDsn, Dsn import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult from .const import ( CONF_DSN, @@ -33,7 +38,7 @@ _LOGGER = logging.getLogger(__name__) DATA_SCHEMA = vol.Schema({vol.Required(CONF_DSN): str}) -class SentryConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class SentryConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a Sentry config flow.""" VERSION = 1 @@ -41,14 +46,14 @@ class SentryConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback def async_get_options_flow( - config_entry: config_entries.ConfigEntry, + config_entry: ConfigEntry, ) -> SentryOptionsFlow: """Get the options flow for this handler.""" return SentryOptionsFlow(config_entry) async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a user config flow.""" if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") @@ -70,16 +75,16 @@ class SentryConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -class SentryOptionsFlow(config_entries.OptionsFlow): +class SentryOptionsFlow(OptionsFlow): """Handle Sentry options.""" - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + def __init__(self, config_entry: ConfigEntry) -> None: """Initialize Sentry options flow.""" self.config_entry = config_entry async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Manage Sentry options.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) diff --git a/homeassistant/components/senz/__init__.py b/homeassistant/components/senz/__init__.py index 559760ec52d..d40b485bf89 100644 --- a/homeassistant/components/senz/__init__.py +++ b/homeassistant/components/senz/__init__.py @@ -1,4 +1,5 @@ """The nVent RAYCHEM SENZ integration.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/senz/api.py b/homeassistant/components/senz/api.py index 1f0ccee3c7c..fa139ac9c64 100644 --- a/homeassistant/components/senz/api.py +++ b/homeassistant/components/senz/api.py @@ -1,4 +1,5 @@ """API for nVent RAYCHEM SENZ bound to Home Assistant OAuth.""" + from typing import cast from aiosenz import AbstractSENZAuth diff --git a/homeassistant/components/senz/climate.py b/homeassistant/components/senz/climate.py index c921e1ac1da..3b834654ca6 100644 --- a/homeassistant/components/senz/climate.py +++ b/homeassistant/components/senz/climate.py @@ -1,4 +1,5 @@ """nVent RAYCHEM SENZ climate platform.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/senz/config_flow.py b/homeassistant/components/senz/config_flow.py index 1bc38321370..457c4f10dd8 100644 --- a/homeassistant/components/senz/config_flow.py +++ b/homeassistant/components/senz/config_flow.py @@ -1,4 +1,5 @@ """Config flow for nVent RAYCHEM SENZ.""" + import logging from homeassistant.helpers import config_entry_oauth2_flow diff --git a/homeassistant/components/serial/sensor.py b/homeassistant/components/serial/sensor.py index 63253375cc7..7f40279df85 100644 --- a/homeassistant/components/serial/sensor.py +++ b/homeassistant/components/serial/sensor.py @@ -1,4 +1,5 @@ """Support for reading data from a serial port.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/serial_pm/sensor.py b/homeassistant/components/serial_pm/sensor.py index 2c94b9bcf01..00ac4fe8731 100644 --- a/homeassistant/components/serial_pm/sensor.py +++ b/homeassistant/components/serial_pm/sensor.py @@ -1,4 +1,5 @@ """Support for particulate matter sensors connected to a serial port.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/sesame/lock.py b/homeassistant/components/sesame/lock.py index c539e7507eb..050a5978acc 100644 --- a/homeassistant/components/sesame/lock.py +++ b/homeassistant/components/sesame/lock.py @@ -1,4 +1,5 @@ """Support for Sesame, by CANDY HOUSE.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/seven_segments/image_processing.py b/homeassistant/components/seven_segments/image_processing.py index 58d532f58f8..622ceb761a0 100644 --- a/homeassistant/components/seven_segments/image_processing.py +++ b/homeassistant/components/seven_segments/image_processing.py @@ -1,4 +1,5 @@ """Optical character recognition processing of seven segments displays.""" + from __future__ import annotations import io @@ -56,15 +57,12 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the Seven segments OCR platform.""" - entities = [] - for camera in config[CONF_SOURCE]: - entities.append( - ImageProcessingSsocr( - hass, camera[CONF_ENTITY_ID], config, camera.get(CONF_NAME) - ) + async_add_entities( + ImageProcessingSsocr( + hass, camera[CONF_ENTITY_ID], config, camera.get(CONF_NAME) ) - - async_add_entities(entities) + for camera in config[CONF_SOURCE] + ) class ImageProcessingSsocr(ImageProcessingEntity): @@ -98,14 +96,14 @@ class ImageProcessingSsocr(ImageProcessingEntity): threshold = ["-t", str(config[CONF_THRESHOLD])] extra_arguments = config[CONF_EXTRA_ARGUMENTS].split(" ") - self._command = ( - [config[CONF_SSOCR_BIN]] - + crop - + digits - + threshold - + rotate - + extra_arguments - ) + self._command = [ + config[CONF_SSOCR_BIN], + *crop, + *digits, + *threshold, + *rotate, + *extra_arguments, + ] self._command.append(self.filepath) @property diff --git a/homeassistant/components/seventeentrack/__init__.py b/homeassistant/components/seventeentrack/__init__.py index 43eebf346f0..183d1bd4068 100644 --- a/homeassistant/components/seventeentrack/__init__.py +++ b/homeassistant/components/seventeentrack/__init__.py @@ -1 +1,32 @@ """The seventeentrack component.""" + +from py17track import Client as SeventeenTrackClient +from py17track.errors import SeventeenTrackError + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import DOMAIN + +PLATFORMS = [Platform.SENSOR] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up 17Track from a config entry.""" + + session = async_get_clientsession(hass) + client = SeventeenTrackClient(session=session) + + try: + await client.profile.login(entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD]) + except SeventeenTrackError as err: + raise ConfigEntryNotReady from err + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = client + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + return True diff --git a/homeassistant/components/seventeentrack/config_flow.py b/homeassistant/components/seventeentrack/config_flow.py new file mode 100644 index 00000000000..ae31e1962d7 --- /dev/null +++ b/homeassistant/components/seventeentrack/config_flow.py @@ -0,0 +1,136 @@ +"""Adds config flow for 17track.net.""" + +from __future__ import annotations + +import logging +from typing import Any + +from py17track import Client as SeventeenTrackClient +from py17track.errors import SeventeenTrackError +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlowResult +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import callback +from homeassistant.helpers import aiohttp_client +from homeassistant.helpers.schema_config_entry_flow import ( + SchemaFlowFormStep, + SchemaOptionsFlowHandler, +) + +from .const import ( + CONF_SHOW_ARCHIVED, + CONF_SHOW_DELIVERED, + DEFAULT_SHOW_ARCHIVED, + DEFAULT_SHOW_DELIVERED, + DOMAIN, +) + +CONF_SHOW = { + vol.Optional(CONF_SHOW_ARCHIVED, default=DEFAULT_SHOW_ARCHIVED): bool, + vol.Optional(CONF_SHOW_DELIVERED, default=DEFAULT_SHOW_DELIVERED): bool, +} + +_LOGGER = logging.getLogger(__name__) + +OPTIONS_SCHEMA = vol.Schema(CONF_SHOW) +OPTIONS_FLOW = { + "init": SchemaFlowFormStep(OPTIONS_SCHEMA), +} + +USER_SCHEMA = vol.Schema( + { + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + } +) + + +class SeventeenTrackConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """17track config flow.""" + + VERSION = 1 + + @staticmethod + @callback + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> SchemaOptionsFlowHandler: + """Get options flow for this handler.""" + return SchemaOptionsFlowHandler(config_entry, OPTIONS_FLOW) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle a flow initialized by the user.""" + + errors = {} + if user_input: + client = self._get_client() + + try: + if not await client.profile.login( + user_input[CONF_USERNAME], user_input[CONF_PASSWORD] + ): + errors["base"] = "invalid_auth" + except SeventeenTrackError as err: + _LOGGER.error("There was an error while logging in: %s", err) + errors["base"] = "cannot_connect" + + if not errors: + account_id = client.profile.account_id + await self.async_set_unique_id(account_id) + self._abort_if_unique_id_configured() + + return self.async_create_entry( + title=user_input[CONF_USERNAME], + data=user_input, + options={ + CONF_SHOW_ARCHIVED: DEFAULT_SHOW_ARCHIVED, + CONF_SHOW_DELIVERED: DEFAULT_SHOW_DELIVERED, + }, + ) + + return self.async_show_form( + step_id="user", + data_schema=USER_SCHEMA, + errors=errors, + ) + + async def async_step_import(self, import_data: dict[str, Any]) -> ConfigFlowResult: + """Import 17Track config from configuration.yaml.""" + + client = self._get_client() + + try: + login_result = await client.profile.login( + import_data[CONF_USERNAME], import_data[CONF_PASSWORD] + ) + except SeventeenTrackError: + return self.async_abort(reason="cannot_connect") + + if not login_result: + return self.async_abort(reason="invalid_auth") + + account_id = client.profile.account_id + + await self.async_set_unique_id(account_id) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=import_data[CONF_USERNAME], + data=import_data, + options={ + CONF_SHOW_ARCHIVED: import_data.get( + CONF_SHOW_ARCHIVED, DEFAULT_SHOW_ARCHIVED + ), + CONF_SHOW_DELIVERED: import_data.get( + CONF_SHOW_DELIVERED, DEFAULT_SHOW_DELIVERED + ), + }, + ) + + @callback + def _get_client(self): + session = aiohttp_client.async_get_clientsession(self.hass) + return SeventeenTrackClient(session=session) diff --git a/homeassistant/components/seventeentrack/const.py b/homeassistant/components/seventeentrack/const.py new file mode 100644 index 00000000000..6f8ae1b221c --- /dev/null +++ b/homeassistant/components/seventeentrack/const.py @@ -0,0 +1,39 @@ +"""Constants for the 17track.net component.""" + +from datetime import timedelta + +ATTR_DESTINATION_COUNTRY = "destination_country" +ATTR_INFO_TEXT = "info_text" +ATTR_TIMESTAMP = "timestamp" +ATTR_ORIGIN_COUNTRY = "origin_country" +ATTR_PACKAGES = "packages" +ATTR_PACKAGE_TYPE = "package_type" +ATTR_STATUS = "status" +ATTR_TRACKING_INFO_LANGUAGE = "tracking_info_language" +ATTR_TRACKING_NUMBER = "tracking_number" + +CONF_SHOW_ARCHIVED = "show_archived" +CONF_SHOW_DELIVERED = "show_delivered" + +DEFAULT_SHOW_ARCHIVED = False +DEFAULT_SHOW_DELIVERED = False + +DOMAIN = "seventeentrack" + +DATA_PACKAGES = "package_data" +DATA_SUMMARY = "summary_data" + +ATTRIBUTION = "Data provided by 17track.net" +DEFAULT_SCAN_INTERVAL = timedelta(minutes=10) + +UNIQUE_ID_TEMPLATE = "package_{0}_{1}" +ENTITY_ID_TEMPLATE = "sensor.seventeentrack_package_{0}" + +NOTIFICATION_DELIVERED_ID = "package_delivered_{0}" +NOTIFICATION_DELIVERED_TITLE = "Package {0} delivered" +NOTIFICATION_DELIVERED_MESSAGE = ( + "Package Delivered: {0}
Visit 17.track for more information: " + "https://t.17track.net/track#nums={1}" +) + +VALUE_DELIVERED = "Delivered" diff --git a/homeassistant/components/seventeentrack/manifest.json b/homeassistant/components/seventeentrack/manifest.json index f752f95ff2d..30bdeaa900f 100644 --- a/homeassistant/components/seventeentrack/manifest.json +++ b/homeassistant/components/seventeentrack/manifest.json @@ -1,8 +1,10 @@ { "domain": "seventeentrack", "name": "17TRACK", - "codeowners": [], + "codeowners": ["@shaiu"], + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/seventeentrack", + "integration_type": "service", "iot_class": "cloud_polling", "loggers": ["py17track"], "requirements": ["py17track==2021.12.2"] diff --git a/homeassistant/components/seventeentrack/sensor.py b/homeassistant/components/seventeentrack/sensor.py index dd61e1627b4..1de627fab39 100644 --- a/homeassistant/components/seventeentrack/sensor.py +++ b/homeassistant/components/seventeentrack/sensor.py @@ -1,66 +1,54 @@ """Support for package tracking sensors from 17track.net.""" + from __future__ import annotations -from datetime import timedelta import logging -from py17track import Client as SeventeenTrackClient from py17track.errors import SeventeenTrackError +from py17track.package import Package import voluptuous as vol from homeassistant.components import persistent_notification from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( ATTR_FRIENDLY_NAME, ATTR_LOCATION, CONF_PASSWORD, - CONF_SCAN_INTERVAL, CONF_USERNAME, ) -from homeassistant.core import HomeAssistant -from homeassistant.helpers import ( - aiohttp_client, - config_validation as cv, - entity, - entity_registry as er, -) +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant +from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers import config_validation as cv, entity, entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_call_later -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType from homeassistant.util import Throttle, slugify -_LOGGER = logging.getLogger(__name__) - -ATTR_DESTINATION_COUNTRY = "destination_country" -ATTR_INFO_TEXT = "info_text" -ATTR_TIMESTAMP = "timestamp" -ATTR_ORIGIN_COUNTRY = "origin_country" -ATTR_PACKAGES = "packages" -ATTR_PACKAGE_TYPE = "package_type" -ATTR_STATUS = "status" -ATTR_TRACKING_INFO_LANGUAGE = "tracking_info_language" -ATTR_TRACKING_NUMBER = "tracking_number" - -CONF_SHOW_ARCHIVED = "show_archived" -CONF_SHOW_DELIVERED = "show_delivered" - -DATA_PACKAGES = "package_data" -DATA_SUMMARY = "summary_data" - -ATTRIBUTION = "Data provided by 17track.net" -DEFAULT_SCAN_INTERVAL = timedelta(minutes=10) - -UNIQUE_ID_TEMPLATE = "package_{0}_{1}" -ENTITY_ID_TEMPLATE = "sensor.seventeentrack_package_{0}" - -NOTIFICATION_DELIVERED_ID = "package_delivered_{0}" -NOTIFICATION_DELIVERED_TITLE = "Package {0} delivered" -NOTIFICATION_DELIVERED_MESSAGE = ( - "Package Delivered: {0}
Visit 17.track for more information: " - "https://t.17track.net/track#nums={1}" +from .const import ( + ATTR_DESTINATION_COUNTRY, + ATTR_INFO_TEXT, + ATTR_ORIGIN_COUNTRY, + ATTR_PACKAGE_TYPE, + ATTR_PACKAGES, + ATTR_STATUS, + ATTR_TIMESTAMP, + ATTR_TRACKING_INFO_LANGUAGE, + ATTR_TRACKING_NUMBER, + ATTRIBUTION, + CONF_SHOW_ARCHIVED, + CONF_SHOW_DELIVERED, + DEFAULT_SCAN_INTERVAL, + DOMAIN, + ENTITY_ID_TEMPLATE, + NOTIFICATION_DELIVERED_MESSAGE, + NOTIFICATION_DELIVERED_TITLE, + UNIQUE_ID_TEMPLATE, + VALUE_DELIVERED, ) -VALUE_DELIVERED = "Delivered" +_LOGGER = logging.getLogger(__name__) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -71,6 +59,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( } ) +ISSUE_PLACEHOLDER = {"url": "/config/integrations/dashboard/add?domain=seventeentrack"} + async def async_setup_platform( hass: HomeAssistant, @@ -78,32 +68,57 @@ async def async_setup_platform( async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Configure the platform and add the sensors.""" + """Initialize 17Track import from config.""" - session = aiohttp_client.async_get_clientsession(hass) - - client = SeventeenTrackClient(session=session) - - try: - login_result = await client.profile.login( - config[CONF_USERNAME], config[CONF_PASSWORD] + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=config + ) + if ( + result["type"] == FlowResultType.CREATE_ENTRY + or result["reason"] == "already_configured" + ): + async_create_issue( + hass, + HOMEASSISTANT_DOMAIN, + f"deprecated_yaml_{DOMAIN}", + is_fixable=False, + breaks_in_ha_version="2024.10.0", + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": "17Track", + }, + ) + else: + async_create_issue( + hass, + DOMAIN, + f"deprecated_yaml_import_issue_{result['reason']}", + breaks_in_ha_version="2024.10.0", + is_fixable=False, + issue_domain=DOMAIN, + severity=IssueSeverity.WARNING, + translation_key=f"deprecated_yaml_import_issue_{result['reason']}", + translation_placeholders=ISSUE_PLACEHOLDER, ) - if not login_result: - _LOGGER.error("Invalid username and password provided") - return - except SeventeenTrackError as err: - _LOGGER.error("There was an error while logging in: %s", err) - return - scan_interval = config.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up a 17Track sensor entry.""" + + client = hass.data[DOMAIN][config_entry.entry_id] data = SeventeenTrackData( client, async_add_entities, - scan_interval, - config[CONF_SHOW_ARCHIVED], - config[CONF_SHOW_DELIVERED], + DEFAULT_SCAN_INTERVAL, + config_entry.options[CONF_SHOW_ARCHIVED], + config_entry.options[CONF_SHOW_DELIVERED], str(hass.config.time_zone), ) await data.async_update() @@ -116,7 +131,7 @@ class SeventeenTrackSummarySensor(SensorEntity): _attr_icon = "mdi:package" _attr_native_unit_of_measurement = "packages" - def __init__(self, data, status, initial_state): + def __init__(self, data, status, initial_state) -> None: """Initialize.""" self._attr_extra_state_attributes = {} self._data = data @@ -126,12 +141,12 @@ class SeventeenTrackSummarySensor(SensorEntity): self._attr_unique_id = f"summary_{data.account_id}_{slugify(status)}" @property - def available(self): + def available(self) -> bool: """Return whether the entity is available.""" return self._state is not None @property - def native_value(self): + def native_value(self) -> StateType: """Return the state.""" return self._state @@ -168,7 +183,7 @@ class SeventeenTrackPackageSensor(SensorEntity): _attr_attribution = ATTRIBUTION _attr_icon = "mdi:package" - def __init__(self, data, package): + def __init__(self, data, package) -> None: """Initialize.""" self._attr_extra_state_attributes = { ATTR_DESTINATION_COUNTRY: package.destination_country, @@ -195,14 +210,14 @@ class SeventeenTrackPackageSensor(SensorEntity): return self._data.packages.get(self._tracking_number) is not None @property - def name(self): + def name(self) -> str: """Return the name.""" if not (name := self._friendly_name): name = self._tracking_number return f"Seventeentrack Package: {name}" @property - def native_value(self): + def native_value(self) -> StateType: """Return the state.""" return self._state @@ -277,18 +292,17 @@ class SeventeenTrackData: show_archived, show_delivered, timezone, - ): + ) -> None: """Initialize.""" self._async_add_entities = async_add_entities self._client = client self._scan_interval = scan_interval self._show_archived = show_archived self.account_id = client.profile.account_id - self.packages = {} + self.packages: dict[str, Package] = {} self.show_delivered = show_delivered self.timezone = timezone - self.summary = {} - + self.summary: dict[str, int] = {} self.async_update = Throttle(self._scan_interval)(self._async_update) self.first_update = True diff --git a/homeassistant/components/seventeentrack/strings.json b/homeassistant/components/seventeentrack/strings.json new file mode 100644 index 00000000000..39ddb5ef8ef --- /dev/null +++ b/homeassistant/components/seventeentrack/strings.json @@ -0,0 +1,42 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } + } + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" + } + }, + "options": { + "step": { + "init": { + "description": "Configure general settings", + "data": { + "show_archived": "Whether sensors should be created for archived packages", + "show_delivered": "Whether sensors should be created for delivered packages" + } + } + } + }, + "issues": { + "deprecated_yaml_import_issue_cannot_connect": { + "title": "The 17Track YAML configuration import cannot connect to server", + "description": "Configuring 17Track using YAML is being removed but there was a connection error importing your YAML configuration.\n\nThings you can try:\nMake sure your home assistant can reach the web.\n\nThen restart Home Assistant to try importing this integration again.\n\nAlternatively, you may remove the 17Track configuration from your YAML configuration entirely, restart Home Assistant, and add the 17Track integration manually." + }, + "deprecated_yaml_import_issue_invalid_auth": { + "title": "The 17Track YAML configuration import request failed due to invalid authentication", + "description": "Configuring 17Track using YAML is being removed but there were invalid credentials provided while importing your existing configuration.\nSetup will not proceed.\n\nVerify that your 17Track credentials are correct and restart Home Assistant to attempt the import again.\n\nAlternatively, you may remove the 17Track configuration from your YAML configuration entirely, restart Home Assistant, and add the 17Track integration manually." + } + } +} diff --git a/homeassistant/components/sfr_box/__init__.py b/homeassistant/components/sfr_box/__init__.py index 564f1970b64..dade1af0e52 100644 --- a/homeassistant/components/sfr_box/__init__.py +++ b/homeassistant/components/sfr_box/__init__.py @@ -1,4 +1,5 @@ """SFR Box.""" + from __future__ import annotations import asyncio @@ -28,9 +29,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: await box.authenticate(username=username, password=password) except SFRBoxAuthenticationError as err: - raise ConfigEntryAuthFailed() from err + raise ConfigEntryAuthFailed from err except SFRBoxError as err: - raise ConfigEntryNotReady() from err + raise ConfigEntryNotReady from err platforms = PLATFORMS_WITH_AUTH data = DomainData( diff --git a/homeassistant/components/sfr_box/binary_sensor.py b/homeassistant/components/sfr_box/binary_sensor.py index 9bf053a3897..7ddcb16c9f8 100644 --- a/homeassistant/components/sfr_box/binary_sensor.py +++ b/homeassistant/components/sfr_box/binary_sensor.py @@ -1,4 +1,5 @@ """SFR Box sensor platform.""" + from __future__ import annotations from collections.abc import Callable @@ -26,20 +27,13 @@ from .models import DomainData _T = TypeVar("_T") -@dataclass(frozen=True) -class SFRBoxBinarySensorMixin(Generic[_T]): - """Mixin for SFR Box sensors.""" +@dataclass(frozen=True, kw_only=True) +class SFRBoxBinarySensorEntityDescription(BinarySensorEntityDescription, Generic[_T]): + """Description for SFR Box binary sensors.""" value_fn: Callable[[_T], bool | None] -@dataclass(frozen=True) -class SFRBoxBinarySensorEntityDescription( - BinarySensorEntityDescription, SFRBoxBinarySensorMixin[_T] -): - """Description for SFR Box binary sensors.""" - - DSL_SENSOR_TYPES: tuple[SFRBoxBinarySensorEntityDescription[DslInfo], ...] = ( SFRBoxBinarySensorEntityDescription[DslInfo]( key="status", diff --git a/homeassistant/components/sfr_box/button.py b/homeassistant/components/sfr_box/button.py index 56c5335e908..6dc91149d86 100644 --- a/homeassistant/components/sfr_box/button.py +++ b/homeassistant/components/sfr_box/button.py @@ -1,4 +1,5 @@ """SFR Box button platform.""" + from __future__ import annotations from collections.abc import Awaitable, Callable, Coroutine @@ -49,18 +50,13 @@ def with_error_wrapping( return wrapper -@dataclass(frozen=True) -class SFRBoxButtonMixin: - """Mixin for SFR Box buttons.""" +@dataclass(frozen=True, kw_only=True) +class SFRBoxButtonEntityDescription(ButtonEntityDescription): + """Description for SFR Box buttons.""" async_press: Callable[[SFRBox], Coroutine[None, None, None]] -@dataclass(frozen=True) -class SFRBoxButtonEntityDescription(ButtonEntityDescription, SFRBoxButtonMixin): - """Description for SFR Box buttons.""" - - BUTTON_TYPES: tuple[SFRBoxButtonEntityDescription, ...] = ( SFRBoxButtonEntityDescription( async_press=lambda x: x.system_reboot(), diff --git a/homeassistant/components/sfr_box/config_flow.py b/homeassistant/components/sfr_box/config_flow.py index 836ed708743..f7d72c01ccd 100644 --- a/homeassistant/components/sfr_box/config_flow.py +++ b/homeassistant/components/sfr_box/config_flow.py @@ -1,4 +1,5 @@ """SFR Box config flow.""" + from __future__ import annotations from collections.abc import Mapping @@ -8,9 +9,8 @@ from sfrbox_api.bridge import SFRBox from sfrbox_api.exceptions import SFRBoxAuthenticationError, SFRBoxError import voluptuous as vol -from homeassistant.config_entries import ConfigEntry, ConfigFlow +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import selector from homeassistant.helpers.httpx_client import get_async_client @@ -41,7 +41,7 @@ class SFRBoxFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, str] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" errors = {} if user_input is not None: @@ -65,7 +65,7 @@ class SFRBoxFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_choose_auth( self, user_input: dict[str, str] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" return self.async_show_menu( step_id="choose_auth", @@ -74,7 +74,7 @@ class SFRBoxFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_auth( self, user_input: dict[str, str] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Check authentication.""" errors = {} if user_input is not None: @@ -107,11 +107,13 @@ class SFRBoxFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_skip_auth( self, user_input: dict[str, str] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Skip authentication.""" return self.async_create_entry(title="SFR Box", data=self._config) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Handle failed credentials.""" self._reauth_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] diff --git a/homeassistant/components/sfr_box/const.py b/homeassistant/components/sfr_box/const.py index 3700890b957..acc4e8e4941 100644 --- a/homeassistant/components/sfr_box/const.py +++ b/homeassistant/components/sfr_box/const.py @@ -1,4 +1,5 @@ """SFR Box constants.""" + from homeassistant.const import Platform DEFAULT_HOST = "192.168.0.1" diff --git a/homeassistant/components/sfr_box/coordinator.py b/homeassistant/components/sfr_box/coordinator.py index 739fc2a770b..08698edd74a 100644 --- a/homeassistant/components/sfr_box/coordinator.py +++ b/homeassistant/components/sfr_box/coordinator.py @@ -1,4 +1,5 @@ """SFR Box coordinator.""" + from collections.abc import Callable, Coroutine from datetime import timedelta import logging @@ -36,4 +37,4 @@ class SFRDataUpdateCoordinator(DataUpdateCoordinator[_T]): try: return await self._method(self.box) except SFRBoxError as err: - raise UpdateFailed() from err + raise UpdateFailed from err diff --git a/homeassistant/components/sfr_box/diagnostics.py b/homeassistant/components/sfr_box/diagnostics.py index e0e84a7ec1a..c0c964cd153 100644 --- a/homeassistant/components/sfr_box/diagnostics.py +++ b/homeassistant/components/sfr_box/diagnostics.py @@ -1,4 +1,5 @@ """SFR Box diagnostics platform.""" + from __future__ import annotations import dataclasses diff --git a/homeassistant/components/sfr_box/models.py b/homeassistant/components/sfr_box/models.py index ff723c2c6ef..aa776a6bf60 100644 --- a/homeassistant/components/sfr_box/models.py +++ b/homeassistant/components/sfr_box/models.py @@ -1,4 +1,5 @@ """SFR Box models.""" + from dataclasses import dataclass from sfrbox_api.bridge import SFRBox diff --git a/homeassistant/components/sfr_box/sensor.py b/homeassistant/components/sfr_box/sensor.py index 6f77ca8d285..403ec762768 100644 --- a/homeassistant/components/sfr_box/sensor.py +++ b/homeassistant/components/sfr_box/sensor.py @@ -1,4 +1,5 @@ """SFR Box sensor platform.""" + from collections.abc import Callable from dataclasses import dataclass from typing import Generic, TypeVar @@ -32,18 +33,13 @@ from .models import DomainData _T = TypeVar("_T") -@dataclass(frozen=True) -class SFRBoxSensorMixin(Generic[_T]): - """Mixin for SFR Box sensors.""" +@dataclass(frozen=True, kw_only=True) +class SFRBoxSensorEntityDescription(SensorEntityDescription, Generic[_T]): + """Description for SFR Box sensors.""" value_fn: Callable[[_T], StateType] -@dataclass(frozen=True) -class SFRBoxSensorEntityDescription(SensorEntityDescription, SFRBoxSensorMixin[_T]): - """Description for SFR Box sensors.""" - - DSL_SENSOR_TYPES: tuple[SFRBoxSensorEntityDescription[DslInfo], ...] = ( SFRBoxSensorEntityDescription[DslInfo]( key="linemode", diff --git a/homeassistant/components/sharkiq/__init__.py b/homeassistant/components/sharkiq/__init__.py index 53a8c4cba3d..a29a2b2e773 100644 --- a/homeassistant/components/sharkiq/__init__.py +++ b/homeassistant/components/sharkiq/__init__.py @@ -1,4 +1,5 @@ """Shark IQ Integration.""" + import asyncio from contextlib import suppress diff --git a/homeassistant/components/sharkiq/config_flow.py b/homeassistant/components/sharkiq/config_flow.py index c0ca5e1b9e5..492b8f2a365 100644 --- a/homeassistant/components/sharkiq/config_flow.py +++ b/homeassistant/components/sharkiq/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Shark IQ integration.""" + from __future__ import annotations import asyncio @@ -9,9 +10,10 @@ import aiohttp from sharkiq import SharkIqAuthError, get_ayla_api import voluptuous as vol -from homeassistant import config_entries, core, exceptions +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_PASSWORD, CONF_REGION, CONF_USERNAME -from homeassistant.data_entry_flow import FlowResult +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import selector from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -39,7 +41,7 @@ SHARKIQ_SCHEMA = vol.Schema( async def _validate_input( - hass: core.HomeAssistant, data: Mapping[str, Any] + hass: HomeAssistant, data: Mapping[str, Any] ) -> dict[str, str]: """Validate the user input allows us to connect.""" ayla_api = get_ayla_api( @@ -74,7 +76,7 @@ async def _validate_input( return {"title": data[CONF_USERNAME]} -class SharkIqConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class SharkIqConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Shark IQ.""" VERSION = 1 @@ -99,7 +101,7 @@ class SharkIqConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, str] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors: dict[str, str] = {} if user_input is not None: @@ -113,7 +115,9 @@ class SharkIqConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="user", data_schema=SHARKIQ_SCHEMA, errors=errors ) - async def async_step_reauth(self, user_input: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, user_input: Mapping[str, Any] + ) -> ConfigFlowResult: """Handle re-auth if login is invalid.""" errors: dict[str, str] = {} @@ -136,13 +140,13 @@ class SharkIqConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -class CannotConnect(exceptions.HomeAssistantError): +class CannotConnect(HomeAssistantError): """Error to indicate we cannot connect.""" -class InvalidAuth(exceptions.HomeAssistantError): +class InvalidAuth(HomeAssistantError): """Error to indicate there is invalid auth.""" -class UnknownAuth(exceptions.HomeAssistantError): +class UnknownAuth(HomeAssistantError): """Error to indicate there is an uncaught auth error.""" diff --git a/homeassistant/components/sharkiq/const.py b/homeassistant/components/sharkiq/const.py index b12a86dc240..8d5d4708e0e 100644 --- a/homeassistant/components/sharkiq/const.py +++ b/homeassistant/components/sharkiq/const.py @@ -1,4 +1,5 @@ """Shark IQ Constants.""" + from datetime import timedelta import logging diff --git a/homeassistant/components/sharkiq/update_coordinator.py b/homeassistant/components/sharkiq/update_coordinator.py index c378797f56e..01550024e9e 100644 --- a/homeassistant/components/sharkiq/update_coordinator.py +++ b/homeassistant/components/sharkiq/update_coordinator.py @@ -1,4 +1,5 @@ """Data update coordinator for shark iq vacuums.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/sharkiq/vacuum.py b/homeassistant/components/sharkiq/vacuum.py index 9510b7d3f66..658d446b9cb 100644 --- a/homeassistant/components/sharkiq/vacuum.py +++ b/homeassistant/components/sharkiq/vacuum.py @@ -1,4 +1,5 @@ """Shark IQ Wrapper.""" + from __future__ import annotations from collections.abc import Iterable @@ -98,7 +99,7 @@ class SharkVacuumEntity(CoordinatorEntity[SharkIqUpdateCoordinator], StateVacuum def clean_spot(self, **kwargs: Any) -> None: """Clean a spot. Not yet implemented.""" - raise NotImplementedError() + raise NotImplementedError def send_command( self, @@ -107,7 +108,7 @@ class SharkVacuumEntity(CoordinatorEntity[SharkIqUpdateCoordinator], StateVacuum **kwargs: Any, ) -> None: """Send a command to the vacuum. Not yet implemented.""" - raise NotImplementedError() + raise NotImplementedError @property def is_online(self) -> bool: diff --git a/homeassistant/components/shell_command/__init__.py b/homeassistant/components/shell_command/__init__.py index 5aa8dadee19..91cb48e9988 100644 --- a/homeassistant/components/shell_command/__init__.py +++ b/homeassistant/components/shell_command/__init__.py @@ -1,4 +1,5 @@ """Expose regular shell commands as services.""" + from __future__ import annotations import asyncio @@ -76,7 +77,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: else: # Template used. Break into list and use create_subprocess_exec # (which uses shell=False) for security - shlexed_cmd = [prog] + shlex.split(rendered_args) + shlexed_cmd = [prog, *shlex.split(rendered_args)] create_process = asyncio.create_subprocess_exec( *shlexed_cmd, diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index 4895e2a1a2b..7d23a1cd57d 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -1,4 +1,5 @@ """The Shelly integration.""" + from __future__ import annotations import contextlib @@ -6,7 +7,7 @@ from typing import Any, Final from aioshelly.block_device import BlockDevice, BlockUpdateType from aioshelly.common import ConnectionOptions -from aioshelly.const import RPC_GENERATIONS +from aioshelly.const import DEFAULT_COAP_PORT, RPC_GENERATIONS from aioshelly.exceptions import ( DeviceConnectionError, FirmwareUnsupported, @@ -36,7 +37,6 @@ from .const import ( CONF_COAP_PORT, CONF_SLEEP_PERIOD, DATA_CONFIG_ENTRY, - DEFAULT_COAP_PORT, DOMAIN, FIRMWARE_UNSUPPORTED_ISSUE_ID, LOGGER, @@ -53,9 +53,11 @@ from .coordinator import ( ) from .utils import ( async_create_issue_unsupported_firmware, + async_shutdown_device, get_block_device_sleep_period, get_coap_context, get_device_entry_gen, + get_http_port, get_rpc_device_wakeup_period, get_ws_context, ) @@ -249,6 +251,7 @@ async def _async_setup_rpc_entry(hass: HomeAssistant, entry: ConfigEntry) -> boo entry.data.get(CONF_USERNAME), entry.data.get(CONF_PASSWORD), device_mac=entry.unique_id, + port=get_http_port(entry.data), ) ws_context = await get_ws_context(hass) @@ -337,12 +340,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: shelly_entry_data = get_entry_data(hass)[entry.entry_id] # If device is present, block/rpc coordinator is not setup yet - device = shelly_entry_data.device - if isinstance(device, RpcDevice): - await device.shutdown() - return True - if isinstance(device, BlockDevice): - device.shutdown() + if (device := shelly_entry_data.device) is not None: + await async_shutdown_device(device) return True platforms = RPC_SLEEPING_PLATFORMS diff --git a/homeassistant/components/shelly/binary_sensor.py b/homeassistant/components/shelly/binary_sensor.py index e9c8e909e87..04df9fb1adc 100644 --- a/homeassistant/components/shelly/binary_sensor.py +++ b/homeassistant/components/shelly/binary_sensor.py @@ -1,4 +1,5 @@ """Binary sensor for Shelly.""" + from __future__ import annotations from dataclasses import dataclass @@ -38,19 +39,19 @@ from .utils import ( ) -@dataclass(frozen=True) +@dataclass(frozen=True, kw_only=True) class BlockBinarySensorDescription( BlockEntityDescription, BinarySensorEntityDescription ): """Class to describe a BLOCK binary sensor.""" -@dataclass(frozen=True) +@dataclass(frozen=True, kw_only=True) class RpcBinarySensorDescription(RpcEntityDescription, BinarySensorEntityDescription): """Class to describe a RPC binary sensor.""" -@dataclass(frozen=True) +@dataclass(frozen=True, kw_only=True) class RestBinarySensorDescription(RestEntityDescription, BinarySensorEntityDescription): """Class to describe a REST binary sensor.""" @@ -166,7 +167,7 @@ RPC_SENSORS: Final = { entity_category=EntityCategory.DIAGNOSTIC, ), "external_power": RpcBinarySensorDescription( - key="devicepower:0", + key="devicepower", sub_key="external", name="External power", value=lambda status, _: status["present"], @@ -206,6 +207,14 @@ RPC_SENSORS: Final = { name="Smoke", device_class=BinarySensorDeviceClass.SMOKE, ), + "restart": RpcBinarySensorDescription( + key="sys", + sub_key="restart_required", + name="Restart required", + device_class=BinarySensorDeviceClass.PROBLEM, + entity_registry_enabled_default=False, + entity_category=EntityCategory.DIAGNOSTIC, + ), } diff --git a/homeassistant/components/shelly/bluetooth/__init__.py b/homeassistant/components/shelly/bluetooth/__init__.py index 5432ceb3a12..fad7ddf4424 100644 --- a/homeassistant/components/shelly/bluetooth/__init__.py +++ b/homeassistant/components/shelly/bluetooth/__init__.py @@ -1,4 +1,5 @@ """Bluetooth support for shelly.""" + from __future__ import annotations from typing import TYPE_CHECKING diff --git a/homeassistant/components/shelly/button.py b/homeassistant/components/shelly/button.py index f4294dee9ee..12c347908fb 100644 --- a/homeassistant/components/shelly/button.py +++ b/homeassistant/components/shelly/button.py @@ -1,4 +1,5 @@ """Button for Shelly.""" + from __future__ import annotations from collections.abc import Callable, Coroutine @@ -31,19 +32,12 @@ _ShellyCoordinatorT = TypeVar( ) -@dataclass(frozen=True) -class ShellyButtonDescriptionMixin(Generic[_ShellyCoordinatorT]): - """Mixin to describe a Button entity.""" +@dataclass(frozen=True, kw_only=True) +class ShellyButtonDescription(ButtonEntityDescription, Generic[_ShellyCoordinatorT]): + """Class to describe a Button entity.""" press_action: Callable[[_ShellyCoordinatorT], Coroutine[Any, Any, None]] - -@dataclass(frozen=True) -class ShellyButtonDescription( - ButtonEntityDescription, ShellyButtonDescriptionMixin[_ShellyCoordinatorT] -): - """Class to describe a Button entity.""" - supported: Callable[[_ShellyCoordinatorT], bool] = lambda _: True diff --git a/homeassistant/components/shelly/climate.py b/homeassistant/components/shelly/climate.py index 3ceb38c84c3..b368b38820e 100644 --- a/homeassistant/components/shelly/climate.py +++ b/homeassistant/components/shelly/climate.py @@ -1,4 +1,5 @@ """Climate support for Shelly.""" + from __future__ import annotations from collections.abc import Mapping @@ -318,7 +319,7 @@ class BlockSleepingClimate( f" {repr(err)}" ) from err except InvalidAuthError: - self.coordinator.entry.async_start_reauth(self.hass) + await self.coordinator.async_shutdown_device_and_start_reauth() async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" @@ -435,7 +436,10 @@ class BlockSleepingClimate( ]["schedule_profile_names"], ] except InvalidAuthError: - self.coordinator.entry.async_start_reauth(self.hass) + self.hass.async_create_task( + self.coordinator.async_shutdown_device_and_start_reauth(), + eager_start=True, + ) else: self.async_write_ha_state() diff --git a/homeassistant/components/shelly/config_flow.py b/homeassistant/components/shelly/config_flow.py index 2ae5a74bb42..24b66e15893 100644 --- a/homeassistant/components/shelly/config_flow.py +++ b/homeassistant/components/shelly/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Shelly integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -6,8 +7,9 @@ from typing import Any, Final from aioshelly.block_device import BlockDevice from aioshelly.common import ConnectionOptions, get_info -from aioshelly.const import BLOCK_GENERATIONS, RPC_GENERATIONS +from aioshelly.const import BLOCK_GENERATIONS, DEFAULT_HTTP_PORT, RPC_GENERATIONS from aioshelly.exceptions import ( + CustomPortNotSupported, DeviceConnectionError, FirmwareUnsupported, InvalidAuthError, @@ -16,10 +18,20 @@ from aioshelly.rpc_device import RpcDevice import voluptuous as vol from homeassistant.components.zeroconf import ZeroconfServiceInfo -from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow -from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) +from homeassistant.const import ( + CONF_HOST, + CONF_MAC, + CONF_PASSWORD, + CONF_PORT, + CONF_USERNAME, +) from homeassistant.core import HomeAssistant, callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.selector import SelectSelector, SelectSelectorConfig @@ -37,6 +49,7 @@ from .utils import ( get_block_device_sleep_period, get_coap_context, get_device_entry_gen, + get_http_port, get_info_auth, get_info_gen, get_model_name, @@ -45,7 +58,12 @@ from .utils import ( mac_address_from_name, ) -HOST_SCHEMA: Final = vol.Schema({vol.Required(CONF_HOST): str}) +CONFIG_SCHEMA: Final = vol.Schema( + { + vol.Required(CONF_HOST): str, + vol.Required(CONF_PORT, default=DEFAULT_HTTP_PORT): vol.Coerce(int), + } +) BLE_SCANNER_OPTIONS = [ @@ -60,14 +78,21 @@ INTERNAL_WIFI_AP_IP = "192.168.33.1" async def validate_input( hass: HomeAssistant, host: str, + port: int, info: dict[str, Any], data: dict[str, Any], ) -> dict[str, Any]: """Validate the user input allows us to connect. - Data has the keys from HOST_SCHEMA with values provided by the user. + Data has the keys from CONFIG_SCHEMA with values provided by the user. """ - options = ConnectionOptions(host, data.get(CONF_USERNAME), data.get(CONF_PASSWORD)) + options = ConnectionOptions( + ip_address=host, + username=data.get(CONF_USERNAME), + password=data.get(CONF_PASSWORD), + device_mac=info[CONF_MAC], + port=port, + ) gen = get_info_gen(info) @@ -109,21 +134,24 @@ class ShellyConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Shelly.""" VERSION = 1 + MINOR_VERSION = 2 host: str = "" + port: int = DEFAULT_HTTP_PORT info: dict[str, Any] = {} device_info: dict[str, Any] = {} entry: ConfigEntry | None = None async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors: dict[str, str] = {} if user_input is not None: - host: str = user_input[CONF_HOST] + host = user_input[CONF_HOST] + port = user_input[CONF_PORT] try: - self.info = await self._async_get_info(host) + self.info = await self._async_get_info(host, port) except DeviceConnectionError: errors["base"] = "cannot_connect" except FirmwareUnsupported: @@ -132,18 +160,21 @@ class ShellyConfigFlow(ConfigFlow, domain=DOMAIN): LOGGER.exception("Unexpected exception") errors["base"] = "unknown" else: - await self.async_set_unique_id(self.info["mac"]) + await self.async_set_unique_id(self.info[CONF_MAC]) self._abort_if_unique_id_configured({CONF_HOST: host}) self.host = host + self.port = port if get_info_auth(self.info): return await self.async_step_credentials() try: device_info = await validate_input( - self.hass, self.host, self.info, {} + self.hass, host, port, self.info, {} ) except DeviceConnectionError: errors["base"] = "cannot_connect" + except CustomPortNotSupported: + errors["base"] = "custom_port_not_supported" except Exception: # pylint: disable=broad-except LOGGER.exception("Unexpected exception") errors["base"] = "unknown" @@ -152,7 +183,8 @@ class ShellyConfigFlow(ConfigFlow, domain=DOMAIN): return self.async_create_entry( title=device_info["title"], data={ - **user_input, + CONF_HOST: user_input[CONF_HOST], + CONF_PORT: user_input[CONF_PORT], CONF_SLEEP_PERIOD: device_info[CONF_SLEEP_PERIOD], "model": device_info["model"], CONF_GEN: device_info[CONF_GEN], @@ -161,12 +193,12 @@ class ShellyConfigFlow(ConfigFlow, domain=DOMAIN): errors["base"] = "firmware_not_fully_provisioned" return self.async_show_form( - step_id="user", data_schema=HOST_SCHEMA, errors=errors + step_id="user", data_schema=CONFIG_SCHEMA, errors=errors ) async def async_step_credentials( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the credentials step.""" errors: dict[str, str] = {} if user_input is not None: @@ -174,7 +206,7 @@ class ShellyConfigFlow(ConfigFlow, domain=DOMAIN): user_input[CONF_USERNAME] = "admin" try: device_info = await validate_input( - self.hass, self.host, self.info, user_input + self.hass, self.host, self.port, self.info, user_input ) except InvalidAuthError: errors["base"] = "invalid_auth" @@ -190,6 +222,7 @@ class ShellyConfigFlow(ConfigFlow, domain=DOMAIN): data={ **user_input, CONF_HOST: self.host, + CONF_PORT: self.port, CONF_SLEEP_PERIOD: device_info[CONF_SLEEP_PERIOD], "model": device_info["model"], CONF_GEN: device_info[CONF_GEN], @@ -239,7 +272,7 @@ class ShellyConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_zeroconf( self, discovery_info: ZeroconfServiceInfo - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle zeroconf discovery.""" host = discovery_info.host # First try to get the mac address from the name @@ -249,7 +282,9 @@ class ShellyConfigFlow(ConfigFlow, domain=DOMAIN): await self._async_discovered_mac(mac, host) try: - self.info = await self._async_get_info(host) + # Devices behind range extender doesn't generate zeroconf packets + # so port is always the default one + self.info = await self._async_get_info(host, DEFAULT_HTTP_PORT) except DeviceConnectionError: return self.async_abort(reason="cannot_connect") except FirmwareUnsupported: @@ -258,7 +293,7 @@ class ShellyConfigFlow(ConfigFlow, domain=DOMAIN): if not mac: # We could not get the mac address from the name # so need to check here since we just got the info - await self._async_discovered_mac(self.info["mac"], host) + await self._async_discovered_mac(self.info[CONF_MAC], host) self.host = host self.context.update( @@ -272,7 +307,9 @@ class ShellyConfigFlow(ConfigFlow, domain=DOMAIN): return await self.async_step_credentials() try: - self.device_info = await validate_input(self.hass, self.host, self.info, {}) + self.device_info = await validate_input( + self.hass, self.host, self.port, self.info, {} + ) except DeviceConnectionError: return self.async_abort(reason="cannot_connect") @@ -280,7 +317,7 @@ class ShellyConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_confirm_discovery( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle discovery confirm.""" errors: dict[str, str] = {} @@ -310,29 +347,32 @@ class ShellyConfigFlow(ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Handle configuration by re-auth.""" self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) return await self.async_step_reauth_confirm() async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Dialog that informs the user that reauth is required.""" errors: dict[str, str] = {} assert self.entry is not None host = self.entry.data[CONF_HOST] + port = get_http_port(self.entry.data) if user_input is not None: try: - info = await self._async_get_info(host) + info = await self._async_get_info(host, port) except (DeviceConnectionError, InvalidAuthError, FirmwareUnsupported): return self.async_abort(reason="reauth_unsuccessful") if get_device_entry_gen(self.entry) != 1: user_input[CONF_USERNAME] = "admin" try: - await validate_input(self.hass, host, info, user_input) + await validate_input(self.hass, host, port, info, user_input) except (DeviceConnectionError, InvalidAuthError, FirmwareUnsupported): return self.async_abort(reason="reauth_unsuccessful") @@ -354,9 +394,9 @@ class ShellyConfigFlow(ConfigFlow, domain=DOMAIN): errors=errors, ) - async def _async_get_info(self, host: str) -> dict[str, Any]: + async def _async_get_info(self, host: str, port: int) -> dict[str, Any]: """Get info from shelly device.""" - return await get_info(async_get_clientsession(self.hass), host) + return await get_info(async_get_clientsession(self.hass), host, port=port) @staticmethod @callback @@ -384,7 +424,7 @@ class OptionsFlowHandler(OptionsFlow): async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle options flow.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) diff --git a/homeassistant/components/shelly/const.py b/homeassistant/components/shelly/const.py index 827a6c00a30..2ac0416bb6c 100644 --- a/homeassistant/components/shelly/const.py +++ b/homeassistant/components/shelly/const.py @@ -1,4 +1,5 @@ """Constants for the Shelly integration.""" + from __future__ import annotations from enum import StrEnum @@ -32,11 +33,13 @@ LOGGER: Logger = getLogger(__package__) DATA_CONFIG_ENTRY: Final = "config_entry" CONF_COAP_PORT: Final = "coap_port" -DEFAULT_COAP_PORT: Final = 5683 FIRMWARE_PATTERN: Final = re.compile(r"^(\d{8})") -# max light transition time in milliseconds -MAX_TRANSITION_TIME: Final = 5000 +# max BLOCK light transition time in milliseconds (min=0) +BLOCK_MAX_TRANSITION_TIME_MS: Final = 5000 + +# min RPC light transition time in seconds (max=10800, limited by light entity to 6553) +RPC_MIN_TRANSITION_TIME_SEC = 0.5 RGBW_MODELS: Final = ( MODEL_BULB, @@ -231,3 +234,5 @@ DEVICES_WITHOUT_FIRMWARE_CHANGELOG = ( ) CONF_GEN = "gen" + +SHELLY_PLUS_RGBW_CHANNELS = 4 diff --git a/homeassistant/components/shelly/coordinator.py b/homeassistant/components/shelly/coordinator.py index d41282b1f0b..c52585c3363 100644 --- a/homeassistant/components/shelly/coordinator.py +++ b/homeassistant/components/shelly/coordinator.py @@ -1,4 +1,5 @@ """Coordinators for the Shelly integration.""" + from __future__ import annotations import asyncio @@ -57,7 +58,9 @@ from .const import ( BLEScannerMode, ) from .utils import ( + async_shutdown_device, get_device_entry_gen, + get_http_port, get_rpc_device_wakeup_period, update_device_fw_info, ) @@ -139,7 +142,7 @@ class ShellyCoordinatorBase(DataUpdateCoordinator[None], Generic[_DeviceT]): model=MODEL_NAMES.get(self.model, self.model), sw_version=self.sw_version, hw_version=f"gen{get_device_entry_gen(self.entry)} ({self.model})", - configuration_url=f"http://{self.entry.data[CONF_HOST]}", + configuration_url=f"http://{self.entry.data[CONF_HOST]}:{get_http_port(self.entry.data)}", ) self.device_id = device_entry.id @@ -149,6 +152,14 @@ class ShellyCoordinatorBase(DataUpdateCoordinator[None], Generic[_DeviceT]): LOGGER.debug("Reloading entry %s", self.name) await self.hass.config_entries.async_reload(self.entry.entry_id) + async def async_shutdown_device_and_start_reauth(self) -> None: + """Shutdown Shelly device and start reauth flow.""" + # not running disconnect events since we have auth error + # and won't be able to send commands to the device + self.last_update_success = False + await async_shutdown_device(self.device) + self.entry.async_start_reauth(self.hass) + class ShellyBlockCoordinator(ShellyCoordinatorBase[BlockDevice]): """Coordinator for a Shelly block based device.""" @@ -178,7 +189,9 @@ class ShellyBlockCoordinator(ShellyCoordinatorBase[BlockDevice]): self.async_add_listener(self._async_device_updates_handler) ) entry.async_on_unload( - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self._handle_ha_stop) + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, self._handle_ha_stop, run_immediately=True + ) ) @callback @@ -296,7 +309,7 @@ class ShellyBlockCoordinator(ShellyCoordinatorBase[BlockDevice]): except DeviceConnectionError as err: raise UpdateFailed(f"Error fetching data: {repr(err)}") from err except InvalidAuthError: - self.entry.async_start_reauth(self.hass) + await self.async_shutdown_device_and_start_reauth() @callback def _async_handle_update( @@ -380,7 +393,7 @@ class ShellyRestCoordinator(ShellyCoordinatorBase[BlockDevice]): except DeviceConnectionError as err: raise UpdateFailed(f"Error fetching data: {repr(err)}") from err except InvalidAuthError: - self.entry.async_start_reauth(self.hass) + await self.async_shutdown_device_and_start_reauth() else: update_device_fw_info(self.hass, self.device, self.entry) @@ -407,7 +420,9 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]): self._input_event_listeners: list[Callable[[dict[str, Any]], None]] = [] entry.async_on_unload( - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self._handle_ha_stop) + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, self._handle_ha_stop, run_immediately=True + ) ) entry.async_on_unload(entry.add_update_listener(self._async_update_listener)) @@ -534,7 +549,7 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]): except DeviceConnectionError as err: raise UpdateFailed(f"Device disconnected: {repr(err)}") from err except InvalidAuthError: - self.entry.async_start_reauth(self.hass) + await self.async_shutdown_device_and_start_reauth() async def _async_disconnected(self) -> None: """Handle device disconnected.""" @@ -627,7 +642,8 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]): try: await async_stop_scanner(self.device) except InvalidAuthError: - self.entry.async_start_reauth(self.hass) + await self.async_shutdown_device_and_start_reauth() + return await self.device.shutdown() await self._async_disconnected() @@ -657,7 +673,7 @@ class ShellyRpcPollingCoordinator(ShellyCoordinatorBase[RpcDevice]): except (DeviceConnectionError, RpcCallError) as err: raise UpdateFailed(f"Device disconnected: {repr(err)}") from err except InvalidAuthError: - self.entry.async_start_reauth(self.hass) + await self.async_shutdown_device_and_start_reauth() def get_block_coordinator_by_device_id( diff --git a/homeassistant/components/shelly/cover.py b/homeassistant/components/shelly/cover.py index caff64d7707..2327c5b4779 100644 --- a/homeassistant/components/shelly/cover.py +++ b/homeassistant/components/shelly/cover.py @@ -1,4 +1,5 @@ """Cover for Shelly.""" + from __future__ import annotations from typing import Any, cast diff --git a/homeassistant/components/shelly/device_trigger.py b/homeassistant/components/shelly/device_trigger.py index 1f41483efc0..9aa57fa1d15 100644 --- a/homeassistant/components/shelly/device_trigger.py +++ b/homeassistant/components/shelly/device_trigger.py @@ -1,4 +1,5 @@ """Provides device triggers for Shelly.""" + from __future__ import annotations from typing import Final diff --git a/homeassistant/components/shelly/diagnostics.py b/homeassistant/components/shelly/diagnostics.py index 0a1fa0e21fc..473bef21835 100644 --- a/homeassistant/components/shelly/diagnostics.py +++ b/homeassistant/components/shelly/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for Shelly.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py index 513e2c88998..accca5f1a64 100644 --- a/homeassistant/components/shelly/entity.py +++ b/homeassistant/components/shelly/entity.py @@ -1,4 +1,5 @@ """Shelly entity helper.""" + from __future__ import annotations from collections.abc import Callable, Mapping @@ -278,21 +279,16 @@ class BlockEntityDescription(EntityDescription): extra_state_attributes: Callable[[Block], dict | None] | None = None -@dataclass(frozen=True) -class RpcEntityRequiredKeysMixin: - """Class for RPC entity required keys.""" - - sub_key: str - - -@dataclass(frozen=True) -class RpcEntityDescription(EntityDescription, RpcEntityRequiredKeysMixin): +@dataclass(frozen=True, kw_only=True) +class RpcEntityDescription(EntityDescription): """Class to describe a RPC entity.""" # BlockEntity does not support UNDEFINED or None, # restrict the type to str. name: str = "" + sub_key: str + value: Callable[[Any, Any], Any] | None = None available: Callable[[dict], bool] | None = None removal_condition: Callable[[dict, dict, str], bool] | None = None @@ -348,7 +344,7 @@ class ShellyBlockEntity(CoordinatorEntity[ShellyBlockCoordinator]): f" {repr(err)}" ) from err except InvalidAuthError: - self.coordinator.entry.async_start_reauth(self.hass) + await self.coordinator.async_shutdown_device_and_start_reauth() class ShellyRpcEntity(CoordinatorEntity[ShellyRpcCoordinator]): @@ -401,7 +397,7 @@ class ShellyRpcEntity(CoordinatorEntity[ShellyRpcCoordinator]): f" {params}, error: {repr(err)}" ) from err except InvalidAuthError: - self.coordinator.entry.async_start_reauth(self.hass) + await self.coordinator.async_shutdown_device_and_start_reauth() class ShellyBlockAttributeEntity(ShellyBlockEntity, Entity): @@ -633,9 +629,9 @@ class ShellySleepingRpcAttributeEntity(ShellyRpcAttributeEntity): self._attr_device_info = DeviceInfo( connections={(CONNECTION_NETWORK_MAC, coordinator.mac)} ) - self._attr_unique_id = ( - self._attr_unique_id - ) = f"{coordinator.mac}-{key}-{attribute}" + self._attr_unique_id = self._attr_unique_id = ( + f"{coordinator.mac}-{key}-{attribute}" + ) self._last_value = None if coordinator.device.initialized: diff --git a/homeassistant/components/shelly/event.py b/homeassistant/components/shelly/event.py index 5425f71366f..0b6b81461ac 100644 --- a/homeassistant/components/shelly/event.py +++ b/homeassistant/components/shelly/event.py @@ -1,4 +1,5 @@ """Event for Shelly.""" + from __future__ import annotations from collections.abc import Callable @@ -37,14 +38,14 @@ from .utils import ( ) -@dataclass(frozen=True) +@dataclass(frozen=True, kw_only=True) class ShellyBlockEventDescription(EventEntityDescription): """Class to describe Shelly event.""" removal_condition: Callable[[dict, Block], bool] | None = None -@dataclass(frozen=True) +@dataclass(frozen=True, kw_only=True) class ShellyRpcEventDescription(EventEntityDescription): """Class to describe Shelly event.""" diff --git a/homeassistant/components/shelly/light.py b/homeassistant/components/shelly/light.py index 234f376e85f..d0590fc7c20 100644 --- a/homeassistant/components/shelly/light.py +++ b/homeassistant/components/shelly/light.py @@ -1,4 +1,5 @@ """Light for Shelly.""" + from __future__ import annotations from typing import Any, cast @@ -13,6 +14,7 @@ from homeassistant.components.light import ( ATTR_RGB_COLOR, ATTR_RGBW_COLOR, ATTR_TRANSITION, + DOMAIN as LIGHT_DOMAIN, ColorMode, LightEntity, LightEntityFeature, @@ -23,21 +25,24 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( + BLOCK_MAX_TRANSITION_TIME_MS, DUAL_MODE_LIGHT_MODELS, KELVIN_MAX_VALUE, KELVIN_MIN_VALUE_COLOR, KELVIN_MIN_VALUE_WHITE, LOGGER, - MAX_TRANSITION_TIME, MODELS_SUPPORTING_LIGHT_TRANSITION, RGBW_MODELS, + RPC_MIN_TRANSITION_TIME_SEC, SHBLB_1_RGB_EFFECTS, + SHELLY_PLUS_RGBW_CHANNELS, STANDARD_RGB_EFFECTS, ) from .coordinator import ShellyBlockCoordinator, ShellyRpcCoordinator, get_entry_data from .entity import ShellyBlockEntity, ShellyRpcEntity from .utils import ( async_remove_shelly_entity, + async_remove_shelly_rpc_entities, brightness_to_percentage, get_device_entry_gen, get_rpc_key_ids, @@ -115,9 +120,30 @@ def async_setup_rpc_entry( ) return - light_key_ids = get_rpc_key_ids(coordinator.device.status, "light") - if light_key_ids: + if light_key_ids := get_rpc_key_ids(coordinator.device.status, "light"): + # Light mode remove RGB & RGBW entities, add light entities + async_remove_shelly_rpc_entities( + hass, LIGHT_DOMAIN, coordinator.mac, ["rgb:0", "rgbw:0"] + ) async_add_entities(RpcShellyLight(coordinator, id_) for id_ in light_key_ids) + return + + light_keys = [f"light:{i}" for i in range(SHELLY_PLUS_RGBW_CHANNELS)] + + if rgb_key_ids := get_rpc_key_ids(coordinator.device.status, "rgb"): + # RGB mode remove light & RGBW entities, add RGB entity + async_remove_shelly_rpc_entities( + hass, LIGHT_DOMAIN, coordinator.mac, [*light_keys, "rgbw:0"] + ) + async_add_entities(RpcShellyRgbLight(coordinator, id_) for id_ in rgb_key_ids) + return + + if rgbw_key_ids := get_rpc_key_ids(coordinator.device.status, "rgbw"): + # RGBW mode remove light & RGB entities, add RGBW entity + async_remove_shelly_rpc_entities( + hass, LIGHT_DOMAIN, coordinator.mac, [*light_keys, "rgb:0"] + ) + async_add_entities(RpcShellyRgbwLight(coordinator, id_) for id_ in rgbw_key_ids) class BlockShellyLight(ShellyBlockEntity, LightEntity): @@ -279,7 +305,7 @@ class BlockShellyLight(ShellyBlockEntity, LightEntity): if ATTR_TRANSITION in kwargs: params["transition"] = min( - int(kwargs[ATTR_TRANSITION] * 1000), MAX_TRANSITION_TIME + int(kwargs[ATTR_TRANSITION] * 1000), BLOCK_MAX_TRANSITION_TIME_MS ) if ATTR_BRIGHTNESS in kwargs and brightness_supported(supported_color_modes): @@ -351,7 +377,7 @@ class BlockShellyLight(ShellyBlockEntity, LightEntity): if ATTR_TRANSITION in kwargs: params["transition"] = min( - int(kwargs[ATTR_TRANSITION] * 1000), MAX_TRANSITION_TIME + int(kwargs[ATTR_TRANSITION] * 1000), BLOCK_MAX_TRANSITION_TIME_MS ) self.control_result = await self.set_state(**params) @@ -365,40 +391,14 @@ class BlockShellyLight(ShellyBlockEntity, LightEntity): super()._update_callback() -class RpcShellySwitchAsLight(ShellyRpcEntity, LightEntity): - """Entity that controls a relay as light on RPC based Shelly devices.""" +class RpcShellyLightBase(ShellyRpcEntity, LightEntity): + """Base Entity for RPC based Shelly devices.""" - _attr_color_mode = ColorMode.ONOFF - _attr_supported_color_modes = {ColorMode.ONOFF} + _component: str = "Light" def __init__(self, coordinator: ShellyRpcCoordinator, id_: int) -> None: """Initialize light.""" - super().__init__(coordinator, f"switch:{id_}") - self._id = id_ - - @property - def is_on(self) -> bool: - """If light is on.""" - return bool(self.status["output"]) - - async def async_turn_on(self, **kwargs: Any) -> None: - """Turn on light.""" - await self.call_rpc("Switch.Set", {"id": self._id, "on": True}) - - async def async_turn_off(self, **kwargs: Any) -> None: - """Turn off light.""" - await self.call_rpc("Switch.Set", {"id": self._id, "on": False}) - - -class RpcShellyLight(ShellyRpcEntity, LightEntity): - """Entity that controls a light on RPC based Shelly devices.""" - - _attr_color_mode = ColorMode.BRIGHTNESS - _attr_supported_color_modes = {ColorMode.BRIGHTNESS} - - def __init__(self, coordinator: ShellyRpcCoordinator, id_: int) -> None: - """Initialize light.""" - super().__init__(coordinator, f"light:{id_}") + super().__init__(coordinator, f"{self._component.lower()}:{id_}") self._id = id_ @property @@ -411,6 +411,16 @@ class RpcShellyLight(ShellyRpcEntity, LightEntity): """Return the brightness of this light between 0..255.""" return percentage_to_brightness(self.status["brightness"]) + @property + def rgb_color(self) -> tuple[int, int, int]: + """Return the rgb color value [int, int, int].""" + return cast(tuple, self.status["rgb"]) + + @property + def rgbw_color(self) -> tuple[int, int, int, int]: + """Return the rgbw color value [int, int, int, int].""" + return (*self.status["rgb"], self.status["white"]) + async def async_turn_on(self, **kwargs: Any) -> None: """Turn on light.""" params: dict[str, Any] = {"id": self._id, "on": True} @@ -418,8 +428,66 @@ class RpcShellyLight(ShellyRpcEntity, LightEntity): if ATTR_BRIGHTNESS in kwargs: params["brightness"] = brightness_to_percentage(kwargs[ATTR_BRIGHTNESS]) - await self.call_rpc("Light.Set", params) + if ATTR_TRANSITION in kwargs: + params["transition_duration"] = max( + kwargs[ATTR_TRANSITION], RPC_MIN_TRANSITION_TIME_SEC + ) + + if ATTR_RGB_COLOR in kwargs: + params["rgb"] = list(kwargs[ATTR_RGB_COLOR]) + + if ATTR_RGBW_COLOR in kwargs: + params["rgb"] = list(kwargs[ATTR_RGBW_COLOR][:-1]) + params["white"] = kwargs[ATTR_RGBW_COLOR][-1] + + await self.call_rpc(f"{self._component}.Set", params) async def async_turn_off(self, **kwargs: Any) -> None: """Turn off light.""" - await self.call_rpc("Light.Set", {"id": self._id, "on": False}) + params: dict[str, Any] = {"id": self._id, "on": False} + + if ATTR_TRANSITION in kwargs: + params["transition_duration"] = max( + kwargs[ATTR_TRANSITION], RPC_MIN_TRANSITION_TIME_SEC + ) + + await self.call_rpc(f"{self._component}.Set", params) + + +class RpcShellySwitchAsLight(RpcShellyLightBase): + """Entity that controls a relay as light on RPC based Shelly devices.""" + + _component = "Switch" + + _attr_color_mode = ColorMode.ONOFF + _attr_supported_color_modes = {ColorMode.ONOFF} + + +class RpcShellyLight(RpcShellyLightBase): + """Entity that controls a light on RPC based Shelly devices.""" + + _component = "Light" + + _attr_color_mode = ColorMode.BRIGHTNESS + _attr_supported_color_modes = {ColorMode.BRIGHTNESS} + _attr_supported_features = LightEntityFeature.TRANSITION + + +class RpcShellyRgbLight(RpcShellyLightBase): + """Entity that controls a RGB light on RPC based Shelly devices.""" + + _component = "RGB" + + _attr_color_mode = ColorMode.RGB + _attr_supported_color_modes = {ColorMode.RGB} + _attr_supported_features = LightEntityFeature.TRANSITION + + +class RpcShellyRgbwLight(RpcShellyLightBase): + """Entity that controls a RGBW light on RPC based Shelly devices.""" + + _component = "RGBW" + + _attr_color_mode = ColorMode.RGBW + _attr_supported_color_modes = {ColorMode.RGBW} + _attr_supported_features = LightEntityFeature.TRANSITION diff --git a/homeassistant/components/shelly/logbook.py b/homeassistant/components/shelly/logbook.py index d55ffe0fd28..fbf72e6ebe8 100644 --- a/homeassistant/components/shelly/logbook.py +++ b/homeassistant/components/shelly/logbook.py @@ -1,4 +1,5 @@ """Describe Shelly logbook events.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/shelly/manifest.json b/homeassistant/components/shelly/manifest.json index 0e0f9d7d796..06159cb543b 100644 --- a/homeassistant/components/shelly/manifest.json +++ b/homeassistant/components/shelly/manifest.json @@ -3,14 +3,13 @@ "name": "Shelly", "codeowners": ["@balloob", "@bieniu", "@thecode", "@chemelli74", "@bdraco"], "config_flow": true, - "dependencies": ["bluetooth", "http"], + "dependencies": ["bluetooth", "http", "network"], "documentation": "https://www.home-assistant.io/integrations/shelly", - "import_executor": true, "integration_type": "device", "iot_class": "local_push", "loggers": ["aioshelly"], "quality_scale": "platinum", - "requirements": ["aioshelly==8.1.1"], + "requirements": ["aioshelly==8.2.0"], "zeroconf": [ { "type": "_http._tcp.local.", diff --git a/homeassistant/components/shelly/number.py b/homeassistant/components/shelly/number.py index ef3963c53c3..6fdf05fa9cb 100644 --- a/homeassistant/components/shelly/number.py +++ b/homeassistant/components/shelly/number.py @@ -1,4 +1,5 @@ """Number for Shelly.""" + from __future__ import annotations from dataclasses import dataclass @@ -29,7 +30,7 @@ from .entity import ( ) -@dataclass(frozen=True) +@dataclass(frozen=True, kw_only=True) class BlockNumberDescription(BlockEntityDescription, NumberEntityDescription): """Class to describe a BLOCK sensor.""" @@ -125,4 +126,4 @@ class BlockSleepingNumber(ShellySleepingBlockAttributeEntity, RestoreNumber): f" {repr(err)}" ) from err except InvalidAuthError: - self.coordinator.entry.async_start_reauth(self.hass) + await self.coordinator.async_shutdown_device_and_start_reauth() diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index 82fc4fe6d78..6cdeea9f842 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -1,4 +1,5 @@ """Sensor for Shelly.""" + from __future__ import annotations from dataclasses import dataclass @@ -58,17 +59,17 @@ from .utils import ( ) -@dataclass(frozen=True) +@dataclass(frozen=True, kw_only=True) class BlockSensorDescription(BlockEntityDescription, SensorEntityDescription): """Class to describe a BLOCK sensor.""" -@dataclass(frozen=True) +@dataclass(frozen=True, kw_only=True) class RpcSensorDescription(RpcEntityDescription, SensorEntityDescription): """Class to describe a RPC sensor.""" -@dataclass(frozen=True) +@dataclass(frozen=True, kw_only=True) class RestSensorDescription(RestEntityDescription, SensorEntityDescription): """Class to describe a REST sensor.""" @@ -933,7 +934,7 @@ RPC_SENSORS: Final = { state_class=SensorStateClass.MEASUREMENT, ), "battery": RpcSensorDescription( - key="devicepower:0", + key="devicepower", sub_key="battery", name="Battery", native_unit_of_measurement=PERCENTAGE, diff --git a/homeassistant/components/shelly/strings.json b/homeassistant/components/shelly/strings.json index 9676c24f883..d4a8b117f4c 100644 --- a/homeassistant/components/shelly/strings.json +++ b/homeassistant/components/shelly/strings.json @@ -5,10 +5,12 @@ "user": { "description": "Before setup, battery-powered devices must be woken up, you can now wake the device up using a button on it.", "data": { - "host": "[%key:common::config_flow::data::host%]" + "host": "[%key:common::config_flow::data::host%]", + "port": "[%key:common::config_flow::data::port%]" }, "data_description": { - "host": "The hostname or IP address of the Shelly device to connect to." + "host": "The hostname or IP address of the Shelly device to connect to.", + "port": "The TCP port of the Shelly device to connect to (Gen2+)." } }, "credentials": { @@ -31,7 +33,8 @@ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", "unknown": "[%key:common::config_flow::error::unknown%]", - "firmware_not_fully_provisioned": "Device not fully provisioned. Please contact Shelly support" + "firmware_not_fully_provisioned": "Device not fully provisioned. Please contact Shelly support", + "custom_port_not_supported": "Gen1 device does not support custom port." }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", diff --git a/homeassistant/components/shelly/switch.py b/homeassistant/components/shelly/switch.py index a45fd9295f2..48ff337d22a 100644 --- a/homeassistant/components/shelly/switch.py +++ b/homeassistant/components/shelly/switch.py @@ -1,4 +1,5 @@ """Switch for Shelly.""" + from __future__ import annotations from dataclasses import dataclass @@ -45,7 +46,7 @@ from .utils import ( ) -@dataclass(frozen=True) +@dataclass(frozen=True, kw_only=True) class BlockSwitchDescription(BlockEntityDescription, SwitchEntityDescription): """Class to describe a BLOCK switch.""" diff --git a/homeassistant/components/shelly/update.py b/homeassistant/components/shelly/update.py index 9e8b1505afe..56ad1f2ef67 100644 --- a/homeassistant/components/shelly/update.py +++ b/homeassistant/components/shelly/update.py @@ -1,4 +1,5 @@ """Update entities for Shelly devices.""" + from __future__ import annotations from collections.abc import Callable @@ -40,35 +41,21 @@ from .utils import get_device_entry_gen, get_release_url LOGGER = logging.getLogger(__name__) -@dataclass(frozen=True) -class RpcUpdateRequiredKeysMixin: - """Class for RPC update required keys.""" - - latest_version: Callable[[dict], Any] - beta: bool - - -@dataclass(frozen=True) -class RestUpdateRequiredKeysMixin: - """Class for REST update required keys.""" - - latest_version: Callable[[dict], Any] - beta: bool - - -@dataclass(frozen=True) -class RpcUpdateDescription( - RpcEntityDescription, UpdateEntityDescription, RpcUpdateRequiredKeysMixin -): +@dataclass(frozen=True, kw_only=True) +class RpcUpdateDescription(RpcEntityDescription, UpdateEntityDescription): """Class to describe a RPC update.""" + latest_version: Callable[[dict], Any] + beta: bool -@dataclass(frozen=True) -class RestUpdateDescription( - RestEntityDescription, UpdateEntityDescription, RestUpdateRequiredKeysMixin -): + +@dataclass(frozen=True, kw_only=True) +class RestUpdateDescription(RestEntityDescription, UpdateEntityDescription): """Class to describe a REST update.""" + latest_version: Callable[[dict], Any] + beta: bool + REST_UPDATES: Final = { "fwupdate": RestUpdateDescription( @@ -213,7 +200,7 @@ class RestUpdateEntity(ShellyRestAttributeEntity, UpdateEntity): except DeviceConnectionError as err: raise HomeAssistantError(f"Error starting OTA update: {repr(err)}") from err except InvalidAuthError: - self.coordinator.entry.async_start_reauth(self.hass) + await self.coordinator.async_shutdown_device_and_start_reauth() else: LOGGER.debug("Result of OTA update call: %s", result) @@ -235,7 +222,7 @@ class RpcUpdateEntity(ShellyRpcAttributeEntity, UpdateEntity): ) -> None: """Initialize update entity.""" super().__init__(coordinator, key, attribute, description) - self._ota_in_progress: bool = False + self._ota_in_progress: bool | int = False self._attr_release_url = get_release_url( coordinator.device.gen, coordinator.model, description.beta ) @@ -250,14 +237,13 @@ class RpcUpdateEntity(ShellyRpcAttributeEntity, UpdateEntity): @callback def _ota_progress_callback(self, event: dict[str, Any]) -> None: """Handle device OTA progress.""" - if self._ota_in_progress: + if self.in_progress is not False: event_type = event["event"] if event_type == OTA_BEGIN: - self._attr_in_progress = 0 + self._ota_in_progress = 0 elif event_type == OTA_PROGRESS: - self._attr_in_progress = event["progress_percent"] + self._ota_in_progress = event["progress_percent"] elif event_type in (OTA_ERROR, OTA_SUCCESS): - self._attr_in_progress = False self._ota_in_progress = False self.async_write_ha_state() @@ -275,6 +261,11 @@ class RpcUpdateEntity(ShellyRpcAttributeEntity, UpdateEntity): return self.installed_version + @property + def in_progress(self) -> bool | int: + """Update installation in progress.""" + return self._ota_in_progress + async def async_install( self, version: str | None, backup: bool, **kwargs: Any ) -> None: @@ -302,10 +293,10 @@ class RpcUpdateEntity(ShellyRpcAttributeEntity, UpdateEntity): except RpcCallError as err: raise HomeAssistantError(f"OTA update request error: {repr(err)}") from err except InvalidAuthError: - self.coordinator.entry.async_start_reauth(self.hass) + await self.coordinator.async_shutdown_device_and_start_reauth() else: self._ota_in_progress = True - LOGGER.debug("OTA update call successful") + LOGGER.info("OTA update call for %s successful", self.coordinator.name) class RpcSleepingUpdateEntity( diff --git a/homeassistant/components/shelly/utils.py b/homeassistant/components/shelly/utils.py index 9389f4e1507..ce98e0d5c12 100644 --- a/homeassistant/components/shelly/utils.py +++ b/homeassistant/components/shelly/utils.py @@ -1,13 +1,18 @@ """Shelly helpers functions.""" + from __future__ import annotations from datetime import datetime, timedelta +from ipaddress import IPv4Address +from types import MappingProxyType from typing import Any, cast from aiohttp.web import Request, WebSocketResponse from aioshelly.block_device import COAP, Block, BlockDevice from aioshelly.const import ( BLOCK_GENERATIONS, + DEFAULT_COAP_PORT, + DEFAULT_HTTP_PORT, MODEL_1L, MODEL_DIMMER, MODEL_DIMMER_2, @@ -18,9 +23,10 @@ from aioshelly.const import ( ) from aioshelly.rpc_device import RpcDevice, WsServer +from homeassistant.components import network from homeassistant.components.http import HomeAssistantView from homeassistant.config_entries import ConfigEntry -from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.const import CONF_PORT, EVENT_HOMEASSISTANT_STOP from homeassistant.core import Event, HomeAssistant, callback from homeassistant.helpers import issue_registry as ir, singleton from homeassistant.helpers.device_registry import ( @@ -35,7 +41,6 @@ from .const import ( BASIC_INPUTS_EVENTS_TYPES, CONF_COAP_PORT, CONF_GEN, - DEFAULT_COAP_PORT, DEVICES_WITHOUT_FIRMWARE_CHANGELOG, DOMAIN, FIRMWARE_UNSUPPORTED_ISSUE_ID, @@ -185,8 +190,6 @@ def get_block_input_triggers( if not is_block_momentary_input(device.settings, block, True): return [] - triggers = [] - if block.type == "device" or get_number_of_channels(device, block) == 1: subtype = "button" else: @@ -200,32 +203,42 @@ def get_block_input_triggers( else: trigger_types = BASIC_INPUTS_EVENTS_TYPES - for trigger_type in trigger_types: - triggers.append((trigger_type, subtype)) - - return triggers + return [(trigger_type, subtype) for trigger_type in trigger_types] def get_shbtn_input_triggers() -> list[tuple[str, str]]: """Return list of input triggers for SHBTN models.""" - triggers = [] - - for trigger_type in SHBTN_INPUTS_EVENTS_TYPES: - triggers.append((trigger_type, "button")) - - return triggers + return [(trigger_type, "button") for trigger_type in SHBTN_INPUTS_EVENTS_TYPES] @singleton.singleton("shelly_coap") async def get_coap_context(hass: HomeAssistant) -> COAP: """Get CoAP context to be used in all Shelly Gen1 devices.""" context = COAP() + + adapters = await network.async_get_adapters(hass) + LOGGER.debug("Network adapters: %s", adapters) + + ipv4: list[IPv4Address] = [] + if not network.async_only_default_interface_enabled(adapters): + ipv4.extend( + address + for address in await network.async_get_enabled_source_ips(hass) + if address.version == 4 + and not ( + address.is_link_local + or address.is_loopback + or address.is_multicast + or address.is_unspecified + ) + ) + LOGGER.debug("Network IPv4 addresses: %s", ipv4) if DOMAIN in hass.data: port = hass.data[DOMAIN].get(CONF_COAP_PORT, DEFAULT_COAP_PORT) else: port = DEFAULT_COAP_PORT LOGGER.info("Starting CoAP context with UDP port %s", port) - await context.initialize(port) + await context.initialize(port, ipv4) @callback def shutdown_listener(ev: Event) -> None: @@ -462,3 +475,28 @@ def is_rpc_wifi_stations_disabled( return False return True + + +def get_http_port(data: MappingProxyType[str, Any]) -> int: + """Get port from config entry data.""" + return cast(int, data.get(CONF_PORT, DEFAULT_HTTP_PORT)) + + +async def async_shutdown_device(device: BlockDevice | RpcDevice) -> None: + """Shutdown a Shelly device.""" + if isinstance(device, RpcDevice): + await device.shutdown() + if isinstance(device, BlockDevice): + device.shutdown() + + +@callback +def async_remove_shelly_rpc_entities( + hass: HomeAssistant, domain: str, mac: str, keys: list[str] +) -> None: + """Remove RPC based Shelly entity.""" + entity_reg = er_async_get(hass) + for key in keys: + if entity_id := entity_reg.async_get_entity_id(domain, DOMAIN, f"{mac}-{key}"): + LOGGER.debug("Removing entity: %s", entity_id) + entity_reg.async_remove(entity_id) diff --git a/homeassistant/components/shelly/valve.py b/homeassistant/components/shelly/valve.py index 7bc4a9a5329..a17738e3575 100644 --- a/homeassistant/components/shelly/valve.py +++ b/homeassistant/components/shelly/valve.py @@ -1,4 +1,5 @@ """Valve for Shelly.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/homeassistant/components/shodan/sensor.py b/homeassistant/components/shodan/sensor.py index bdef681fdd2..fd608cbcb45 100644 --- a/homeassistant/components/shodan/sensor.py +++ b/homeassistant/components/shodan/sensor.py @@ -1,4 +1,5 @@ """Sensor for displaying the number of result on Shodan.io.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/shopping_list/__init__.py b/homeassistant/components/shopping_list/__init__.py index e030f15d26e..1176192bdcd 100644 --- a/homeassistant/components/shopping_list/__init__.py +++ b/homeassistant/components/shopping_list/__init__.py @@ -1,4 +1,5 @@ """Support to manage a shopping list.""" + from __future__ import annotations from collections.abc import Callable @@ -414,7 +415,7 @@ class ShoppingListView(http.HomeAssistantView): @callback def get(self, request: web.Request) -> web.Response: """Retrieve shopping list items.""" - return self.json(request.app["hass"].data[DOMAIN].items) + return self.json(request.app[http.KEY_HASS].data[DOMAIN].items) class UpdateShoppingListItemView(http.HomeAssistantView): @@ -426,7 +427,7 @@ class UpdateShoppingListItemView(http.HomeAssistantView): async def post(self, request: web.Request, item_id: str) -> web.Response: """Update a shopping list item.""" data = await request.json() - hass: HomeAssistant = request.app["hass"] + hass = request.app[http.KEY_HASS] try: item = await hass.data[DOMAIN].async_update(item_id, data) @@ -446,7 +447,7 @@ class CreateShoppingListItemView(http.HomeAssistantView): @RequestDataValidator(vol.Schema({vol.Required("name"): str})) async def post(self, request: web.Request, data: dict[str, str]) -> web.Response: """Create a new shopping list item.""" - hass: HomeAssistant = request.app["hass"] + hass = request.app[http.KEY_HASS] item = await hass.data[DOMAIN].async_add(data["name"]) return self.json(item) @@ -459,7 +460,7 @@ class ClearCompletedItemsView(http.HomeAssistantView): async def post(self, request: web.Request) -> web.Response: """Retrieve if API is running.""" - hass: HomeAssistant = request.app["hass"] + hass = request.app[http.KEY_HASS] await hass.data[DOMAIN].async_clear_completed() return self.json_message("Cleared completed items.") diff --git a/homeassistant/components/shopping_list/config_flow.py b/homeassistant/components/shopping_list/config_flow.py index 0637dcea390..ffc8a3be21a 100644 --- a/homeassistant/components/shopping_list/config_flow.py +++ b/homeassistant/components/shopping_list/config_flow.py @@ -1,10 +1,10 @@ """Config flow to configure the shopping list integration.""" + from __future__ import annotations from typing import Any -from homeassistant.config_entries import ConfigFlow -from homeassistant.data_entry_flow import FlowResult +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from .const import DOMAIN @@ -16,7 +16,7 @@ class ShoppingListFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" # Check if already configured await self.async_set_unique_id(DOMAIN) @@ -31,6 +31,6 @@ class ShoppingListFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_onboarding( self, _: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by onboarding.""" return await self.async_step_user(user_input={}) diff --git a/homeassistant/components/shopping_list/icons.json b/homeassistant/components/shopping_list/icons.json new file mode 100644 index 00000000000..7de3eb1b948 --- /dev/null +++ b/homeassistant/components/shopping_list/icons.json @@ -0,0 +1,19 @@ +{ + "entity": { + "todo": { + "shopping_list": { + "default": "mdi:cart" + } + } + }, + "services": { + "add_item": "mdi:cart-plus", + "remove_item": "mdi:cart-remove", + "complete_item": "mdi:cart-check", + "incomplete_item": "mdi:cart-off", + "complete_all": "mdi:cart-check", + "incomplete_all": "mdi:cart-off", + "clear_completed_items": "mdi:cart-remove", + "sort": "mdi:sort" + } +} diff --git a/homeassistant/components/shopping_list/intent.py b/homeassistant/components/shopping_list/intent.py index 180007c2dfb..70a70467cbd 100644 --- a/homeassistant/components/shopping_list/intent.py +++ b/homeassistant/components/shopping_list/intent.py @@ -1,4 +1,5 @@ """Intents for the Shopping List integration.""" + from __future__ import annotations from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/shopping_list/todo.py b/homeassistant/components/shopping_list/todo.py index 2d959858067..82b6cbfc7f5 100644 --- a/homeassistant/components/shopping_list/todo.py +++ b/homeassistant/components/shopping_list/todo.py @@ -32,7 +32,6 @@ class ShoppingTodoListEntity(TodoListEntity): """A To-do List representation of the Shopping List.""" _attr_has_entity_name = True - _attr_icon = "mdi:cart" _attr_translation_key = "shopping_list" _attr_should_poll = False _attr_supported_features = ( diff --git a/homeassistant/components/sia/__init__.py b/homeassistant/components/sia/__init__.py index a59d1f1cdad..d1bc3fa9968 100644 --- a/homeassistant/components/sia/__init__.py +++ b/homeassistant/components/sia/__init__.py @@ -1,4 +1,5 @@ """The sia integration.""" + from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PORT from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/sia/alarm_control_panel.py b/homeassistant/components/sia/alarm_control_panel.py index e7850a5f9d2..8c995da542a 100644 --- a/homeassistant/components/sia/alarm_control_panel.py +++ b/homeassistant/components/sia/alarm_control_panel.py @@ -1,4 +1,5 @@ """Module for SIA Alarm Control Panels.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/homeassistant/components/sia/binary_sensor.py b/homeassistant/components/sia/binary_sensor.py index f6e2533be93..307b5073e90 100644 --- a/homeassistant/components/sia/binary_sensor.py +++ b/homeassistant/components/sia/binary_sensor.py @@ -1,4 +1,5 @@ """Module for SIA Binary Sensors.""" + from __future__ import annotations from collections.abc import Iterable diff --git a/homeassistant/components/sia/config_flow.py b/homeassistant/components/sia/config_flow.py index cbcb60a4565..f7a7a1f06e4 100644 --- a/homeassistant/components/sia/config_flow.py +++ b/homeassistant/components/sia/config_flow.py @@ -1,4 +1,5 @@ """Config flow for sia integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -15,10 +16,14 @@ from pysiaalarm import ( ) import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.const import CONF_PORT, CONF_PROTOCOL from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult from .const import ( CONF_ACCOUNT, @@ -87,7 +92,7 @@ def validate_zones(data: dict[str, Any]) -> dict[str, str] | None: return None -class SIAConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class SIAConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for sia.""" VERSION: int = 1 @@ -95,7 +100,7 @@ class SIAConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback def async_get_options_flow( - config_entry: config_entries.ConfigEntry, + config_entry: ConfigEntry, ) -> SIAOptionsFlowHandler: """Get the options flow for this handler.""" return SIAOptionsFlowHandler(config_entry) @@ -107,7 +112,7 @@ class SIAConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial user step.""" errors: dict[str, str] | None = None if user_input is not None: @@ -120,7 +125,7 @@ class SIAConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_add_account( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the additional accounts steps.""" errors: dict[str, str] | None = None if user_input is not None: @@ -133,7 +138,7 @@ class SIAConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_handle_data_and_route( self, user_input: dict[str, Any] - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the user_input, check if configured and route to the right next step or create entry.""" self._update_data(user_input) @@ -171,10 +176,10 @@ class SIAConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._options[CONF_ACCOUNTS][account][CONF_ZONES] = user_input[CONF_ZONES] -class SIAOptionsFlowHandler(config_entries.OptionsFlow): +class SIAOptionsFlowHandler(OptionsFlow): """Handle SIA options.""" - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + def __init__(self, config_entry: ConfigEntry) -> None: """Initialize SIA options flow.""" self.config_entry = config_entry self.options = deepcopy(dict(config_entry.options)) @@ -183,7 +188,7 @@ class SIAOptionsFlowHandler(config_entries.OptionsFlow): async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Manage the SIA options.""" self.hub = self.hass.data[DOMAIN][self.config_entry.entry_id] assert self.hub is not None @@ -193,7 +198,7 @@ class SIAOptionsFlowHandler(config_entries.OptionsFlow): async def async_step_options( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Create the options step for a account.""" errors: dict[str, str] | None = None if user_input is not None: diff --git a/homeassistant/components/sia/const.py b/homeassistant/components/sia/const.py index 82783611e07..20a0afa9edf 100644 --- a/homeassistant/components/sia/const.py +++ b/homeassistant/components/sia/const.py @@ -1,4 +1,5 @@ """Constants for the sia integration.""" + from __future__ import annotations from typing import Final diff --git a/homeassistant/components/sia/hub.py b/homeassistant/components/sia/hub.py index 9ba7a19a9be..591e4aadad7 100644 --- a/homeassistant/components/sia/hub.py +++ b/homeassistant/components/sia/hub.py @@ -1,4 +1,5 @@ """The sia hub.""" + from __future__ import annotations from copy import deepcopy diff --git a/homeassistant/components/sia/sia_entity_base.py b/homeassistant/components/sia/sia_entity_base.py index f6895cc48a9..aecac2b540b 100644 --- a/homeassistant/components/sia/sia_entity_base.py +++ b/homeassistant/components/sia/sia_entity_base.py @@ -1,4 +1,5 @@ """Module for SIA Base Entity.""" + from __future__ import annotations from abc import abstractmethod diff --git a/homeassistant/components/sia/utils.py b/homeassistant/components/sia/utils.py index e9db69041d6..2fab4bf39ce 100644 --- a/homeassistant/components/sia/utils.py +++ b/homeassistant/components/sia/utils.py @@ -1,4 +1,5 @@ """Helper functions for the SIA integration.""" + from __future__ import annotations from datetime import datetime, timedelta diff --git a/homeassistant/components/sigfox/sensor.py b/homeassistant/components/sigfox/sensor.py index 1a1d7bb74b0..fbda6fece21 100644 --- a/homeassistant/components/sigfox/sensor.py +++ b/homeassistant/components/sigfox/sensor.py @@ -1,4 +1,5 @@ """Sensor for SigFox devices.""" + from __future__ import annotations import datetime @@ -51,10 +52,7 @@ def setup_platform( auth = sigfox.auth devices = sigfox.devices - sensors = [] - for device in devices: - sensors.append(SigfoxDevice(device, auth, name)) - add_entities(sensors, True) + add_entities((SigfoxDevice(device, auth, name) for device in devices), True) def epoch_to_datetime(epoch_time): @@ -91,10 +89,7 @@ class SigfoxAPI: """Get a list of device types.""" url = urljoin(API_URL, "devicetypes") response = requests.get(url, auth=self._auth, timeout=10) - device_types = [] - for device in json.loads(response.text)["data"]: - device_types.append(device["id"]) - return device_types + return [device["id"] for device in json.loads(response.text)["data"]] def get_devices(self, device_types): """Get the device_id of each device registered.""" @@ -104,8 +99,7 @@ class SigfoxAPI: url = urljoin(API_URL, location_url) response = requests.get(url, auth=self._auth, timeout=10) devices_data = json.loads(response.text)["data"] - for device in devices_data: - devices.append(device["id"]) + devices.extend(device["id"] for device in devices_data) return devices @property diff --git a/homeassistant/components/sighthound/image_processing.py b/homeassistant/components/sighthound/image_processing.py index 69776f4e2ac..bcfa4bca3c2 100644 --- a/homeassistant/components/sighthound/image_processing.py +++ b/homeassistant/components/sighthound/image_processing.py @@ -1,4 +1,5 @@ """Person detection using Sighthound cloud service.""" + from __future__ import annotations import io diff --git a/homeassistant/components/signal_messenger/notify.py b/homeassistant/components/signal_messenger/notify.py index 25dae1617c3..58cd85fb26e 100644 --- a/homeassistant/components/signal_messenger/notify.py +++ b/homeassistant/components/signal_messenger/notify.py @@ -1,4 +1,5 @@ """Signal Messenger for notify component.""" + from __future__ import annotations import logging @@ -164,8 +165,8 @@ class SignalNotificationService(BaseNotificationService): size += len(chunk) if size > attachment_size_limit: raise ValueError( - "Attachment too large (Stream reports {}). Max size: {}" - " bytes".format(size, CONF_MAX_ALLOWED_DOWNLOAD_SIZE_BYTES) + f"Attachment too large (Stream reports {size}). " + f"Max size: {CONF_MAX_ALLOWED_DOWNLOAD_SIZE_BYTES} bytes" ) chunks.extend(chunk) diff --git a/homeassistant/components/simplepush/config_flow.py b/homeassistant/components/simplepush/config_flow.py index d87f6fa1913..4e954e89938 100644 --- a/homeassistant/components/simplepush/config_flow.py +++ b/homeassistant/components/simplepush/config_flow.py @@ -1,4 +1,5 @@ """Config flow for simplepush integration.""" + from __future__ import annotations from typing import Any @@ -6,9 +7,8 @@ from typing import Any from simplepush import UnknownError, send import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_NAME, CONF_PASSWORD -from homeassistant.data_entry_flow import FlowResult from .const import ATTR_ENCRYPTED, CONF_DEVICE_KEY, CONF_SALT, DEFAULT_NAME, DOMAIN @@ -36,12 +36,12 @@ def validate_input(entry: dict[str, str]) -> dict[str, str] | None: return None -class SimplePushFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class SimplePushFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a config flow for simplepush.""" async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initiated by the user.""" errors: dict[str, str] | None = None if user_input is not None: diff --git a/homeassistant/components/simplepush/notify.py b/homeassistant/components/simplepush/notify.py index cc6c61ced03..e21a62a6a12 100644 --- a/homeassistant/components/simplepush/notify.py +++ b/homeassistant/components/simplepush/notify.py @@ -1,4 +1,5 @@ """Simplepush notification service.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index 1e558356ea3..cdeb6910aa5 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -1,4 +1,5 @@ """Support for SimpliSafe alarm systems.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/simplisafe/alarm_control_panel.py b/homeassistant/components/simplisafe/alarm_control_panel.py index 71f250b0e02..731400e67d5 100644 --- a/homeassistant/components/simplisafe/alarm_control_panel.py +++ b/homeassistant/components/simplisafe/alarm_control_panel.py @@ -1,4 +1,5 @@ """Support for SimpliSafe alarm control panels.""" + from __future__ import annotations from simplipy.errors import SimplipyError diff --git a/homeassistant/components/simplisafe/binary_sensor.py b/homeassistant/components/simplisafe/binary_sensor.py index d9384b948ed..3f56149a9f8 100644 --- a/homeassistant/components/simplisafe/binary_sensor.py +++ b/homeassistant/components/simplisafe/binary_sensor.py @@ -1,4 +1,5 @@ """Support for SimpliSafe binary sensors.""" + from __future__ import annotations from simplipy.device import DeviceTypes, DeviceV3 @@ -78,8 +79,10 @@ async def async_setup_entry( if sensor.type in SUPPORTED_BATTERY_SENSOR_TYPES: sensors.append(BatteryBinarySensor(simplisafe, system, sensor)) - for lock in system.locks.values(): - sensors.append(BatteryBinarySensor(simplisafe, system, lock)) + sensors.extend( + BatteryBinarySensor(simplisafe, system, lock) + for lock in system.locks.values() + ) async_add_entities(sensors) diff --git a/homeassistant/components/simplisafe/button.py b/homeassistant/components/simplisafe/button.py index 220ca89d170..40bf857da2a 100644 --- a/homeassistant/components/simplisafe/button.py +++ b/homeassistant/components/simplisafe/button.py @@ -1,4 +1,5 @@ """Buttons for the SimpliSafe integration.""" + from __future__ import annotations from collections.abc import Awaitable, Callable diff --git a/homeassistant/components/simplisafe/config_flow.py b/homeassistant/components/simplisafe/config_flow.py index dcfcd6cd9d3..c0d98c5644f 100644 --- a/homeassistant/components/simplisafe/config_flow.py +++ b/homeassistant/components/simplisafe/config_flow.py @@ -1,4 +1,5 @@ """Config flow to configure the SimpliSafe component.""" + from __future__ import annotations from collections.abc import Mapping @@ -13,11 +14,14 @@ from simplipy.util.auth import ( ) import voluptuous as vol -from homeassistant import config_entries -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.const import CONF_CODE, CONF_TOKEN, CONF_URL, CONF_USERNAME from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import aiohttp_client, config_validation as cv from .const import DOMAIN, LOGGER @@ -47,7 +51,7 @@ def async_get_simplisafe_oauth_values() -> SimpliSafeOAuthValues: return SimpliSafeOAuthValues(auth_url, code_verifier) -class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class SimpliSafeFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a SimpliSafe config flow.""" VERSION = 1 @@ -65,14 +69,14 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Define the config flow to handle options.""" return SimpliSafeOptionsFlowHandler(config_entry) - async def async_step_reauth(self, config: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth(self, config: Mapping[str, Any]) -> ConfigFlowResult: """Handle configuration by re-auth.""" self._reauth = True return await self.async_step_user() async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the start of the config flow.""" if user_input is None: return self.async_show_form( @@ -144,7 +148,7 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_create_entry(title=simplisafe_user_id, data=data) -class SimpliSafeOptionsFlowHandler(config_entries.OptionsFlow): +class SimpliSafeOptionsFlowHandler(OptionsFlow): """Handle a SimpliSafe options flow.""" def __init__(self, config_entry: ConfigEntry) -> None: @@ -153,7 +157,7 @@ class SimpliSafeOptionsFlowHandler(config_entries.OptionsFlow): async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Manage the options.""" if user_input is not None: return self.async_create_entry(data=user_input) diff --git a/homeassistant/components/simplisafe/const.py b/homeassistant/components/simplisafe/const.py index 1405f60b400..1ed77bcd685 100644 --- a/homeassistant/components/simplisafe/const.py +++ b/homeassistant/components/simplisafe/const.py @@ -1,4 +1,5 @@ """Define constants for the SimpliSafe component.""" + import logging LOGGER = logging.getLogger(__package__) diff --git a/homeassistant/components/simplisafe/diagnostics.py b/homeassistant/components/simplisafe/diagnostics.py index cb983f74202..e63e1551740 100644 --- a/homeassistant/components/simplisafe/diagnostics.py +++ b/homeassistant/components/simplisafe/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for SimpliSafe.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/simplisafe/icons.json b/homeassistant/components/simplisafe/icons.json new file mode 100644 index 00000000000..60ddb7f0982 --- /dev/null +++ b/homeassistant/components/simplisafe/icons.json @@ -0,0 +1,7 @@ +{ + "services": { + "remove_pin": "mdi:alarm-panel-outline", + "set_pin": "mdi:alarm-panel", + "set_system_properties": "mdi:cog" + } +} diff --git a/homeassistant/components/simplisafe/lock.py b/homeassistant/components/simplisafe/lock.py index 9ce59eb3b56..680fc0f4c0f 100644 --- a/homeassistant/components/simplisafe/lock.py +++ b/homeassistant/components/simplisafe/lock.py @@ -1,4 +1,5 @@ """Support for SimpliSafe locks.""" + from __future__ import annotations from typing import Any @@ -33,15 +34,16 @@ async def async_setup_entry( ) -> None: """Set up SimpliSafe locks based on a config entry.""" simplisafe = hass.data[DOMAIN][entry.entry_id] - locks = [] + locks: list[SimpliSafeLock] = [] for system in simplisafe.systems.values(): if system.version == 2: LOGGER.info("Skipping lock setup for V2 system: %s", system.system_id) continue - for lock in system.locks.values(): - locks.append(SimpliSafeLock(simplisafe, system, lock)) + locks.extend( + SimpliSafeLock(simplisafe, system, lock) for lock in system.locks.values() + ) async_add_entities(locks) diff --git a/homeassistant/components/simplisafe/sensor.py b/homeassistant/components/simplisafe/sensor.py index 949d8890398..fbccfc4b2f9 100644 --- a/homeassistant/components/simplisafe/sensor.py +++ b/homeassistant/components/simplisafe/sensor.py @@ -1,4 +1,5 @@ """Support for SimpliSafe freeze sensor.""" + from __future__ import annotations from simplipy.device import DeviceTypes @@ -24,16 +25,18 @@ async def async_setup_entry( ) -> None: """Set up SimpliSafe freeze sensors based on a config entry.""" simplisafe = hass.data[DOMAIN][entry.entry_id] - sensors = [] + sensors: list[SimplisafeFreezeSensor] = [] for system in simplisafe.systems.values(): if system.version == 2: LOGGER.info("Skipping sensor setup for V2 system: %s", system.system_id) continue - for sensor in system.sensors.values(): - if sensor.type == DeviceTypes.TEMPERATURE: - sensors.append(SimplisafeFreezeSensor(simplisafe, system, sensor)) + sensors.extend( + SimplisafeFreezeSensor(simplisafe, system, sensor) + for sensor in system.sensors.values() + if sensor.type == DeviceTypes.TEMPERATURE + ) async_add_entities(sensors) diff --git a/homeassistant/components/simplisafe/typing.py b/homeassistant/components/simplisafe/typing.py index d49d356036a..5651a3072b9 100644 --- a/homeassistant/components/simplisafe/typing.py +++ b/homeassistant/components/simplisafe/typing.py @@ -1,4 +1,5 @@ """Define typing helpers for SimpliSafe.""" + from simplipy.system.v2 import SystemV2 from simplipy.system.v3 import SystemV3 diff --git a/homeassistant/components/simulated/sensor.py b/homeassistant/components/simulated/sensor.py index 0f9db48e78c..51ec19ac80b 100644 --- a/homeassistant/components/simulated/sensor.py +++ b/homeassistant/components/simulated/sensor.py @@ -1,4 +1,5 @@ """Adds a simulated sensor.""" + from __future__ import annotations from datetime import datetime diff --git a/homeassistant/components/sinch/notify.py b/homeassistant/components/sinch/notify.py index cd37f8bf627..77443dd1a84 100644 --- a/homeassistant/components/sinch/notify.py +++ b/homeassistant/components/sinch/notify.py @@ -1,4 +1,5 @@ """Support for Sinch notifications.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/siren/__init__.py b/homeassistant/components/siren/__init__.py index fb41d5f7b48..a083aa9d702 100644 --- a/homeassistant/components/siren/__init__.py +++ b/homeassistant/components/siren/__init__.py @@ -1,4 +1,5 @@ """Component to interface with various sirens/chimes.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/sisyphus/__init__.py b/homeassistant/components/sisyphus/__init__.py index 721c51ca964..da8d670d412 100644 --- a/homeassistant/components/sisyphus/__init__.py +++ b/homeassistant/components/sisyphus/__init__.py @@ -1,4 +1,5 @@ """Support for controlling Sisyphus Kinetic Art Tables.""" + import asyncio import logging diff --git a/homeassistant/components/sisyphus/light.py b/homeassistant/components/sisyphus/light.py index d0cd7597e58..eff0fb378a3 100644 --- a/homeassistant/components/sisyphus/light.py +++ b/homeassistant/components/sisyphus/light.py @@ -1,4 +1,5 @@ """Support for the light on the Sisyphus Kinetic Art Table.""" + from __future__ import annotations import logging @@ -32,7 +33,7 @@ async def async_setup_platform( table_holder = hass.data[DATA_SISYPHUS][host] table = await table_holder.get_table() except aiohttp.ClientError as err: - raise PlatformNotReady() from err + raise PlatformNotReady from err add_entities([SisyphusLight(table_holder.name, table)], update_before_add=True) diff --git a/homeassistant/components/sisyphus/media_player.py b/homeassistant/components/sisyphus/media_player.py index e13924a51e9..3884a83928a 100644 --- a/homeassistant/components/sisyphus/media_player.py +++ b/homeassistant/components/sisyphus/media_player.py @@ -1,4 +1,5 @@ """Support for track controls on the Sisyphus Kinetic Art Table.""" + from __future__ import annotations import aiohttp @@ -34,7 +35,7 @@ async def async_setup_platform( table_holder = hass.data[DATA_SISYPHUS][host] table = await table_holder.get_table() except aiohttp.ClientError as err: - raise PlatformNotReady() from err + raise PlatformNotReady from err add_entities([SisyphusPlayer(table_holder.name, host, table)], True) diff --git a/homeassistant/components/sky_hub/device_tracker.py b/homeassistant/components/sky_hub/device_tracker.py index 8741b2ed560..52c56993be0 100644 --- a/homeassistant/components/sky_hub/device_tracker.py +++ b/homeassistant/components/sky_hub/device_tracker.py @@ -1,4 +1,5 @@ """Support for Sky Hub.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/skybeacon/sensor.py b/homeassistant/components/skybeacon/sensor.py index 17bf8a3ab7f..94a3e270cb3 100644 --- a/homeassistant/components/skybeacon/sensor.py +++ b/homeassistant/components/skybeacon/sensor.py @@ -1,4 +1,5 @@ """Support for Skybeacon temperature/humidity Bluetooth LE sensors.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/skybell/__init__.py b/homeassistant/components/skybell/__init__.py index ac948408a3f..0282ad40254 100644 --- a/homeassistant/components/skybell/__init__.py +++ b/homeassistant/components/skybell/__init__.py @@ -1,4 +1,5 @@ """Support for the Skybell HD Doorbell.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/skybell/binary_sensor.py b/homeassistant/components/skybell/binary_sensor.py index fa55b352f61..3c2d90b2630 100644 --- a/homeassistant/components/skybell/binary_sensor.py +++ b/homeassistant/components/skybell/binary_sensor.py @@ -1,4 +1,5 @@ """Binary sensor support for the Skybell HD Doorbell.""" + from __future__ import annotations from aioskybell.helpers import const as CONST diff --git a/homeassistant/components/skybell/camera.py b/homeassistant/components/skybell/camera.py index 1e510687a02..683b840debe 100644 --- a/homeassistant/components/skybell/camera.py +++ b/homeassistant/components/skybell/camera.py @@ -1,4 +1,5 @@ """Camera support for the Skybell HD Doorbell.""" + from __future__ import annotations from aiohttp import web diff --git a/homeassistant/components/skybell/config_flow.py b/homeassistant/components/skybell/config_flow.py index 5e63ae4f929..26602e81882 100644 --- a/homeassistant/components/skybell/config_flow.py +++ b/homeassistant/components/skybell/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Skybell integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -7,27 +8,28 @@ from typing import Any from aioskybell import Skybell, exceptions import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_EMAIL, CONF_PASSWORD -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN -class SkybellFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class SkybellFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a config flow for Skybell.""" reauth_email: str - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Handle a reauthorization flow request.""" self.reauth_email = entry_data[CONF_EMAIL] return await self.async_step_reauth_confirm() async def async_step_reauth_confirm( self, user_input: dict[str, str] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle user's reauth credentials.""" errors = {} if user_input: @@ -53,7 +55,7 @@ class SkybellFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initiated by the user.""" errors = {} diff --git a/homeassistant/components/skybell/const.py b/homeassistant/components/skybell/const.py index 1d46e45dad1..38c52ea23aa 100644 --- a/homeassistant/components/skybell/const.py +++ b/homeassistant/components/skybell/const.py @@ -1,4 +1,5 @@ """Constants for the Skybell HD Doorbell.""" + import logging from typing import Final diff --git a/homeassistant/components/skybell/entity.py b/homeassistant/components/skybell/entity.py index 2d596ec8aac..f3b0c077212 100644 --- a/homeassistant/components/skybell/entity.py +++ b/homeassistant/components/skybell/entity.py @@ -1,4 +1,5 @@ """Entity representing a Skybell HD Doorbell.""" + from __future__ import annotations from aioskybell import SkybellDevice diff --git a/homeassistant/components/skybell/icons.json b/homeassistant/components/skybell/icons.json new file mode 100644 index 00000000000..a2084cd2971 --- /dev/null +++ b/homeassistant/components/skybell/icons.json @@ -0,0 +1,27 @@ +{ + "entity": { + "sensor": { + "chime_level": { + "default": "mdi:bell-ring" + }, + "last_button_event": { + "default": "mdi:clock" + }, + "last_motion_event": { + "default": "mdi:clock" + }, + "last_check_in": { + "default": "mdi:clock" + }, + "motion_threshold": { + "default": "mdi:walk" + }, + "wifi_ssid": { + "default": "mdi:wifi-settings" + }, + "wifi_status": { + "default": "mdi:wifi-strength-3" + } + } + } +} diff --git a/homeassistant/components/skybell/light.py b/homeassistant/components/skybell/light.py index 70fe01fdb5e..cba9e70c848 100644 --- a/homeassistant/components/skybell/light.py +++ b/homeassistant/components/skybell/light.py @@ -1,4 +1,5 @@ """Light/LED support for the Skybell HD Doorbell.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/skybell/sensor.py b/homeassistant/components/skybell/sensor.py index 7093c5cad20..5f0df77ecfa 100644 --- a/homeassistant/components/skybell/sensor.py +++ b/homeassistant/components/skybell/sensor.py @@ -1,4 +1,5 @@ """Sensor support for Skybell Doorbells.""" + from __future__ import annotations from collections.abc import Callable @@ -22,45 +23,34 @@ from homeassistant.helpers.typing import StateType from .entity import DOMAIN, SkybellEntity -@dataclass(frozen=True) -class SkybellSensorEntityDescriptionMixIn: - """Mixin for Skybell sensor.""" +@dataclass(frozen=True, kw_only=True) +class SkybellSensorEntityDescription(SensorEntityDescription): + """Class to describe a Skybell sensor.""" value_fn: Callable[[SkybellDevice], StateType | datetime] -@dataclass(frozen=True) -class SkybellSensorEntityDescription( - SensorEntityDescription, SkybellSensorEntityDescriptionMixIn -): - """Class to describe a Skybell sensor.""" - - SENSOR_TYPES: tuple[SkybellSensorEntityDescription, ...] = ( SkybellSensorEntityDescription( key="chime_level", translation_key="chime_level", - icon="mdi:bell-ring", value_fn=lambda device: device.outdoor_chime_level, ), SkybellSensorEntityDescription( key="last_button_event", translation_key="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", translation_key="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, translation_key="last_check_in", - icon="mdi:clock", entity_registry_enabled_default=False, device_class=SensorDeviceClass.TIMESTAMP, entity_category=EntityCategory.DIAGNOSTIC, @@ -69,7 +59,6 @@ SENSOR_TYPES: tuple[SkybellSensorEntityDescription, ...] = ( SkybellSensorEntityDescription( key="motion_threshold", translation_key="motion_threshold", - icon="mdi:walk", entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda device: device.motion_threshold, @@ -84,7 +73,6 @@ SENSOR_TYPES: tuple[SkybellSensorEntityDescription, ...] = ( SkybellSensorEntityDescription( key=CONST.ATTR_WIFI_SSID, translation_key="wifi_ssid", - icon="mdi:wifi-settings", entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda device: device.wifi_ssid, @@ -92,7 +80,6 @@ SENSOR_TYPES: tuple[SkybellSensorEntityDescription, ...] = ( SkybellSensorEntityDescription( key=CONST.ATTR_WIFI_STATUS, translation_key="wifi_status", - icon="mdi:wifi-strength-3", entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda device: device.wifi_status, diff --git a/homeassistant/components/skybell/switch.py b/homeassistant/components/skybell/switch.py index f67cca41ac9..fa4f723573f 100644 --- a/homeassistant/components/skybell/switch.py +++ b/homeassistant/components/skybell/switch.py @@ -1,4 +1,5 @@ """Switch support for the Skybell HD Doorbell.""" + from __future__ import annotations from typing import Any, cast diff --git a/homeassistant/components/slack/__init__.py b/homeassistant/components/slack/__init__.py index 47ee07a7004..e5f6a50122e 100644 --- a/homeassistant/components/slack/__init__.py +++ b/homeassistant/components/slack/__init__.py @@ -1,4 +1,5 @@ """The slack integration.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/slack/config_flow.py b/homeassistant/components/slack/config_flow.py index 187cef057a0..b23dc60da60 100644 --- a/homeassistant/components/slack/config_flow.py +++ b/homeassistant/components/slack/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Slack integration.""" + from __future__ import annotations import logging @@ -7,9 +8,8 @@ from slack import WebClient from slack.errors import SlackApiError import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_API_KEY, CONF_ICON, CONF_NAME, CONF_USERNAME -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import aiohttp_client from .const import CONF_DEFAULT_CHANNEL, DOMAIN @@ -26,12 +26,12 @@ CONFIG_SCHEMA = vol.Schema( ) -class SlackFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class SlackFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a config flow for Slack.""" async def async_step_user( self, user_input: dict[str, str] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initiated by the user.""" errors = {} diff --git a/homeassistant/components/slack/const.py b/homeassistant/components/slack/const.py index ccc1fbb6643..0c26b63c72d 100644 --- a/homeassistant/components/slack/const.py +++ b/homeassistant/components/slack/const.py @@ -1,4 +1,5 @@ """Constants for the Slack integration.""" + from typing import Final ATTR_BLOCKS = "blocks" diff --git a/homeassistant/components/slack/icons.json b/homeassistant/components/slack/icons.json new file mode 100644 index 00000000000..5f09ed12fcc --- /dev/null +++ b/homeassistant/components/slack/icons.json @@ -0,0 +1,9 @@ +{ + "entity": { + "sensor": { + "do_not_disturb_until": { + "default": "mdi:clock" + } + } + } +} diff --git a/homeassistant/components/slack/notify.py b/homeassistant/components/slack/notify.py index aae2846503d..06fc76e217a 100644 --- a/homeassistant/components/slack/notify.py +++ b/homeassistant/components/slack/notify.py @@ -1,4 +1,5 @@ """Slack platform for notify component.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/slack/sensor.py b/homeassistant/components/slack/sensor.py index 4e65fdfc26d..b4d7fd28bd7 100644 --- a/homeassistant/components/slack/sensor.py +++ b/homeassistant/components/slack/sensor.py @@ -1,4 +1,5 @@ """Slack platform for sensor component.""" + from __future__ import annotations from slack import WebClient @@ -30,7 +31,6 @@ async def async_setup_entry( SensorEntityDescription( key="do_not_disturb_until", translation_key="do_not_disturb_until", - icon="mdi:clock", device_class=SensorDeviceClass.TIMESTAMP, ), entry, diff --git a/homeassistant/components/sleepiq/__init__.py b/homeassistant/components/sleepiq/__init__.py index f70bed1333e..6506be06e72 100644 --- a/homeassistant/components/sleepiq/__init__.py +++ b/homeassistant/components/sleepiq/__init__.py @@ -1,4 +1,5 @@ """Support for SleepIQ from SleepNumber.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/sleepiq/binary_sensor.py b/homeassistant/components/sleepiq/binary_sensor.py index e137edb29ce..cb56a516b9b 100644 --- a/homeassistant/components/sleepiq/binary_sensor.py +++ b/homeassistant/components/sleepiq/binary_sensor.py @@ -1,4 +1,5 @@ """Support for SleepIQ sensors.""" + from asyncsleepiq import SleepIQBed, SleepIQSleeper from homeassistant.components.binary_sensor import ( diff --git a/homeassistant/components/sleepiq/button.py b/homeassistant/components/sleepiq/button.py index 0d9a118d3c9..94b010066c9 100644 --- a/homeassistant/components/sleepiq/button.py +++ b/homeassistant/components/sleepiq/button.py @@ -1,4 +1,5 @@ """Support for SleepIQ buttons.""" + from __future__ import annotations from collections.abc import Callable @@ -17,20 +18,13 @@ from .coordinator import SleepIQData from .entity import SleepIQEntity -@dataclass(frozen=True) -class SleepIQButtonEntityDescriptionMixin: - """Describes a SleepIQ Button entity.""" +@dataclass(frozen=True, kw_only=True) +class SleepIQButtonEntityDescription(ButtonEntityDescription): + """Class to describe a Button entity.""" press_action: Callable[[SleepIQBed], Any] -@dataclass(frozen=True) -class SleepIQButtonEntityDescription( - ButtonEntityDescription, SleepIQButtonEntityDescriptionMixin -): - """Class to describe a Button entity.""" - - ENTITY_DESCRIPTIONS = [ SleepIQButtonEntityDescription( key="calibrate", diff --git a/homeassistant/components/sleepiq/config_flow.py b/homeassistant/components/sleepiq/config_flow.py index 77806a1f977..4a4813192c3 100644 --- a/homeassistant/components/sleepiq/config_flow.py +++ b/homeassistant/components/sleepiq/config_flow.py @@ -1,4 +1,5 @@ """Config flow to configure SleepIQ component.""" + from __future__ import annotations from collections.abc import Mapping @@ -8,10 +9,9 @@ from typing import Any from asyncsleepiq import AsyncSleepIQ, SleepIQLoginException, SleepIQTimeoutException import voluptuous as vol -from homeassistant.config_entries import ConfigEntry, ConfigFlow +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN @@ -28,7 +28,9 @@ class SleepIQFlowHandler(ConfigFlow, domain=DOMAIN): """Initialize the config flow.""" self._reauth_entry: ConfigEntry | None = None - async def async_step_import(self, import_config: dict[str, Any]) -> FlowResult: + async def async_step_import( + self, import_config: dict[str, Any] + ) -> ConfigFlowResult: """Import a SleepIQ account as a config entry. This flow is triggered by 'async_setup' for configured accounts. @@ -46,7 +48,7 @@ class SleepIQFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" errors = {} @@ -80,7 +82,9 @@ class SleepIQFlowHandler(ConfigFlow, domain=DOMAIN): last_step=True, ) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Perform reauth upon an API authentication error.""" self._reauth_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] @@ -89,7 +93,7 @@ class SleepIQFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm reauth.""" errors: dict[str, str] = {} assert self._reauth_entry is not None diff --git a/homeassistant/components/sleepiq/coordinator.py b/homeassistant/components/sleepiq/coordinator.py index 8d51da5f47a..7fe4f964b36 100644 --- a/homeassistant/components/sleepiq/coordinator.py +++ b/homeassistant/components/sleepiq/coordinator.py @@ -1,4 +1,5 @@ """Coordinator for SleepIQ.""" + import asyncio from dataclasses import dataclass from datetime import timedelta diff --git a/homeassistant/components/sleepiq/entity.py b/homeassistant/components/sleepiq/entity.py index 9a0342aa7ac..3ffd736ccda 100644 --- a/homeassistant/components/sleepiq/entity.py +++ b/homeassistant/components/sleepiq/entity.py @@ -1,4 +1,5 @@ """Entity for the SleepIQ integration.""" + from abc import abstractmethod from typing import TypeVar diff --git a/homeassistant/components/sleepiq/light.py b/homeassistant/components/sleepiq/light.py index e684d383b40..781bd8e600a 100644 --- a/homeassistant/components/sleepiq/light.py +++ b/homeassistant/components/sleepiq/light.py @@ -1,4 +1,5 @@ """Support for SleepIQ outlet lights.""" + import logging from typing import Any diff --git a/homeassistant/components/sleepiq/number.py b/homeassistant/components/sleepiq/number.py index 4f90ef7dbdc..905ceab18bd 100644 --- a/homeassistant/components/sleepiq/number.py +++ b/homeassistant/components/sleepiq/number.py @@ -1,4 +1,5 @@ """Support for SleepIQ SleepNumber firmness number entities.""" + from __future__ import annotations from collections.abc import Callable, Coroutine @@ -30,9 +31,9 @@ from .coordinator import SleepIQData, SleepIQDataUpdateCoordinator from .entity import SleepIQBedEntity, sleeper_for_side -@dataclass(frozen=True) -class SleepIQNumberEntityDescriptionMixin: - """Mixin to describe a SleepIQ number entity.""" +@dataclass(frozen=True, kw_only=True) +class SleepIQNumberEntityDescription(NumberEntityDescription): + """Class to describe a SleepIQ number entity.""" value_fn: Callable[[Any], float] set_value_fn: Callable[[Any, int], Coroutine[None, None, None]] @@ -40,13 +41,6 @@ class SleepIQNumberEntityDescriptionMixin: get_unique_id_fn: Callable[[SleepIQBed, Any], str] -@dataclass(frozen=True) -class SleepIQNumberEntityDescription( - NumberEntityDescription, SleepIQNumberEntityDescriptionMixin -): - """Class to describe a SleepIQ number entity.""" - - async def _async_set_firmness(sleeper: SleepIQSleeper, firmness: int) -> None: await sleeper.set_sleepnumber(firmness) @@ -149,35 +143,35 @@ async def async_setup_entry( """Set up the SleepIQ bed sensors.""" data: SleepIQData = hass.data[DOMAIN][entry.entry_id] - entities = [] + entities: list[SleepIQNumberEntity] = [] for bed in data.client.beds.values(): - for sleeper in bed.sleepers: - entities.append( - SleepIQNumberEntity( - data.data_coordinator, - bed, - sleeper, - NUMBER_DESCRIPTIONS[FIRMNESS], - ) + entities.extend( + SleepIQNumberEntity( + data.data_coordinator, + bed, + sleeper, + NUMBER_DESCRIPTIONS[FIRMNESS], ) - for actuator in bed.foundation.actuators: - entities.append( - SleepIQNumberEntity( - data.data_coordinator, - bed, - actuator, - NUMBER_DESCRIPTIONS[ACTUATOR], - ) + for sleeper in bed.sleepers + ) + entities.extend( + SleepIQNumberEntity( + data.data_coordinator, + bed, + actuator, + NUMBER_DESCRIPTIONS[ACTUATOR], ) - for foot_warmer in bed.foundation.foot_warmers: - entities.append( - SleepIQNumberEntity( - data.data_coordinator, - bed, - foot_warmer, - NUMBER_DESCRIPTIONS[FOOT_WARMING_TIMER], - ) + for actuator in bed.foundation.actuators + ) + entities.extend( + SleepIQNumberEntity( + data.data_coordinator, + bed, + foot_warmer, + NUMBER_DESCRIPTIONS[FOOT_WARMING_TIMER], ) + for foot_warmer in bed.foundation.foot_warmers + ) async_add_entities(entities) diff --git a/homeassistant/components/sleepiq/select.py b/homeassistant/components/sleepiq/select.py index df8d854c9da..0a09aa4d657 100644 --- a/homeassistant/components/sleepiq/select.py +++ b/homeassistant/components/sleepiq/select.py @@ -1,4 +1,5 @@ """Support for SleepIQ foundation preset selection.""" + from __future__ import annotations from asyncsleepiq import ( @@ -28,14 +29,14 @@ async def async_setup_entry( data: SleepIQData = hass.data[DOMAIN][entry.entry_id] entities: list[SleepIQBedEntity] = [] for bed in data.client.beds.values(): - for preset in bed.foundation.presets: - entities.append(SleepIQSelectEntity(data.data_coordinator, bed, preset)) - for foot_warmer in bed.foundation.foot_warmers: - entities.append( - SleepIQFootWarmingTempSelectEntity( - data.data_coordinator, bed, foot_warmer - ) - ) + entities.extend( + SleepIQSelectEntity(data.data_coordinator, bed, preset) + for preset in bed.foundation.presets + ) + entities.extend( + SleepIQFootWarmingTempSelectEntity(data.data_coordinator, bed, foot_warmer) + for foot_warmer in bed.foundation.foot_warmers + ) async_add_entities(entities) diff --git a/homeassistant/components/sleepiq/sensor.py b/homeassistant/components/sleepiq/sensor.py index c463c80224e..413e8e4d856 100644 --- a/homeassistant/components/sleepiq/sensor.py +++ b/homeassistant/components/sleepiq/sensor.py @@ -1,4 +1,5 @@ """Support for SleepIQ Sensor.""" + from __future__ import annotations from asyncsleepiq import SleepIQBed, SleepIQSleeper diff --git a/homeassistant/components/sleepiq/switch.py b/homeassistant/components/sleepiq/switch.py index 62ad72d9db4..9fc8ca9d20e 100644 --- a/homeassistant/components/sleepiq/switch.py +++ b/homeassistant/components/sleepiq/switch.py @@ -1,4 +1,5 @@ """Support for SleepIQ switches.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/slide/__init__.py b/homeassistant/components/slide/__init__.py index 6ab3528a927..868a0e82f89 100644 --- a/homeassistant/components/slide/__init__.py +++ b/homeassistant/components/slide/__init__.py @@ -1,4 +1,5 @@ """Component for the Slide API.""" + from datetime import timedelta import logging diff --git a/homeassistant/components/slide/cover.py b/homeassistant/components/slide/cover.py index 866d3d40307..5186b3d0fea 100644 --- a/homeassistant/components/slide/cover.py +++ b/homeassistant/components/slide/cover.py @@ -1,4 +1,5 @@ """Support for Slide slides.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/slimproto/__init__.py b/homeassistant/components/slimproto/__init__.py index c22349bb2f2..a5ab10ac32b 100644 --- a/homeassistant/components/slimproto/__init__.py +++ b/homeassistant/components/slimproto/__init__.py @@ -1,4 +1,5 @@ """SlimProto Player integration.""" + from __future__ import annotations from aioslimproto import SlimServer diff --git a/homeassistant/components/slimproto/config_flow.py b/homeassistant/components/slimproto/config_flow.py index 7e2e96f74dc..24457493f9b 100644 --- a/homeassistant/components/slimproto/config_flow.py +++ b/homeassistant/components/slimproto/config_flow.py @@ -4,8 +4,7 @@ from __future__ import annotations from typing import Any -from homeassistant.config_entries import ConfigFlow -from homeassistant.data_entry_flow import FlowResult +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from .const import DEFAULT_NAME, DOMAIN @@ -17,7 +16,7 @@ class SlimProtoConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") diff --git a/homeassistant/components/slimproto/const.py b/homeassistant/components/slimproto/const.py index 3b85de5d794..e8ef4fbd148 100644 --- a/homeassistant/components/slimproto/const.py +++ b/homeassistant/components/slimproto/const.py @@ -1,6 +1,5 @@ """Constants for SlimProto Player integration.""" - DOMAIN = "slimproto" DEFAULT_NAME = "SlimProto Player" diff --git a/homeassistant/components/slimproto/manifest.json b/homeassistant/components/slimproto/manifest.json index b221db96262..f270e020740 100644 --- a/homeassistant/components/slimproto/manifest.json +++ b/homeassistant/components/slimproto/manifest.json @@ -6,5 +6,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/slimproto", "iot_class": "local_push", - "requirements": ["aioslimproto==2.3.3"] + "requirements": ["aioslimproto==3.0.0"] } diff --git a/homeassistant/components/slimproto/media_player.py b/homeassistant/components/slimproto/media_player.py index 1bf3c57fee2..42c50d21e75 100644 --- a/homeassistant/components/slimproto/media_player.py +++ b/homeassistant/components/slimproto/media_player.py @@ -1,11 +1,12 @@ """MediaPlayer platform for SlimProto Player integration.""" + from __future__ import annotations import asyncio from typing import Any from aioslimproto.client import PlayerState, SlimClient -from aioslimproto.const import EventType, SlimEvent +from aioslimproto.models import EventType, SlimEvent from aioslimproto.server import SlimServer from homeassistant.components import media_source @@ -106,9 +107,9 @@ class SlimProtoPlayer(MediaPlayerEntity): ) # PiCore + SqueezeESP32 player has web interface if "-pCP" in self.player.firmware or self.player.device_model == "SqueezeESP32": - self._attr_device_info[ - "configuration_url" - ] = f"http://{self.player.device_address}" + self._attr_device_info["configuration_url"] = ( + f"http://{self.player.device_address}" + ) self.update_attributes() async def async_added_to_hass(self) -> None: @@ -144,9 +145,23 @@ class SlimProtoPlayer(MediaPlayerEntity): def update_attributes(self) -> None: """Handle player updates.""" self._attr_volume_level = self.player.volume_level / 100 + self._attr_is_volume_muted = self.player.muted self._attr_media_position = self.player.elapsed_seconds self._attr_media_position_updated_at = utcnow() - self._attr_media_content_id = self.player.current_url + if (current_media := self.player.current_media) and ( + metadata := current_media.metadata + ): + self._attr_media_content_id = metadata.get("item_id", current_media.url) + self._attr_media_artist = metadata.get("artist") + self._attr_media_album_name = metadata.get("album") + self._attr_media_title = metadata.get("title") + self._attr_media_image_url = metadata.get("image_url") + else: + self._attr_media_content_id = current_media.url if current_media else None + self._attr_media_artist = None + self._attr_media_album_name = None + self._attr_media_title = None + self._attr_media_image_url = None self._attr_media_content_type = "music" async def async_media_play(self) -> None: diff --git a/homeassistant/components/sma/__init__.py b/homeassistant/components/sma/__init__.py index 419fd6aa8ed..febd4e34aaf 100644 --- a/homeassistant/components/sma/__init__.py +++ b/homeassistant/components/sma/__init__.py @@ -1,4 +1,5 @@ """The sma integration.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/sma/config_flow.py b/homeassistant/components/sma/config_flow.py index 070610d6ae2..dcf1084f161 100644 --- a/homeassistant/components/sma/config_flow.py +++ b/homeassistant/components/sma/config_flow.py @@ -1,4 +1,5 @@ """Config flow for the sma integration.""" + from __future__ import annotations import logging @@ -7,9 +8,9 @@ from typing import Any import pysma import voluptuous as vol -from homeassistant import config_entries, core +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_SSL, CONF_VERIFY_SSL -from homeassistant.data_entry_flow import FlowResult +from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -18,9 +19,7 @@ from .const import CONF_GROUP, DOMAIN, GROUPS _LOGGER = logging.getLogger(__name__) -async def validate_input( - hass: core.HomeAssistant, data: dict[str, Any] -) -> dict[str, Any]: +async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]: """Validate the user input allows us to connect.""" session = async_get_clientsession(hass, verify_ssl=data[CONF_VERIFY_SSL]) @@ -37,7 +36,7 @@ async def validate_input( return device_info -class SmaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class SmaConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for SMA.""" VERSION = 1 @@ -54,7 +53,7 @@ class SmaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """First step in config flow.""" errors = {} if user_input is not None: diff --git a/homeassistant/components/sma/const.py b/homeassistant/components/sma/const.py index d51b3f6d316..dec99c1d1af 100644 --- a/homeassistant/components/sma/const.py +++ b/homeassistant/components/sma/const.py @@ -1,4 +1,5 @@ """Constants for the sma integration.""" + from homeassistant.const import Platform DOMAIN = "sma" diff --git a/homeassistant/components/sma/sensor.py b/homeassistant/components/sma/sensor.py index abf5c9a878f..9d580a76d9e 100644 --- a/homeassistant/components/sma/sensor.py +++ b/homeassistant/components/sma/sensor.py @@ -1,4 +1,5 @@ """SMA Solar Webconnect interface.""" + from __future__ import annotations from typing import TYPE_CHECKING @@ -843,19 +844,16 @@ async def async_setup_entry( if TYPE_CHECKING: assert config_entry.unique_id - entities = [] - for sensor in used_sensors: - entities.append( - SMAsensor( - coordinator, - config_entry.unique_id, - SENSOR_ENTITIES.get(sensor.name), - device_info, - sensor, - ) + async_add_entities( + SMAsensor( + coordinator, + config_entry.unique_id, + SENSOR_ENTITIES.get(sensor.name), + device_info, + sensor, ) - - async_add_entities(entities) + for sensor in used_sensors + ) class SMAsensor(CoordinatorEntity, SensorEntity): diff --git a/homeassistant/components/smappee/api.py b/homeassistant/components/smappee/api.py index f12f53ff27f..1a036b1072f 100644 --- a/homeassistant/components/smappee/api.py +++ b/homeassistant/components/smappee/api.py @@ -1,4 +1,5 @@ """API for Smappee bound to Home Assistant OAuth.""" + from asyncio import run_coroutine_threadsafe from pysmappee import api diff --git a/homeassistant/components/smappee/binary_sensor.py b/homeassistant/components/smappee/binary_sensor.py index ed09b51ff25..a653896f1c2 100644 --- a/homeassistant/components/smappee/binary_sensor.py +++ b/homeassistant/components/smappee/binary_sensor.py @@ -1,4 +1,5 @@ """Support for monitoring a Smappee appliance binary sensor.""" + from __future__ import annotations from homeassistant.components.binary_sensor import ( diff --git a/homeassistant/components/smappee/config_flow.py b/homeassistant/components/smappee/config_flow.py index e57071b4938..6ed18905233 100644 --- a/homeassistant/components/smappee/config_flow.py +++ b/homeassistant/components/smappee/config_flow.py @@ -1,12 +1,13 @@ """Config flow for Smappee.""" + import logging from pysmappee import helper, mqtt import voluptuous as vol from homeassistant.components import zeroconf +from homeassistant.config_entries import ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_IP_ADDRESS -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_entry_oauth2_flow from . import api @@ -39,7 +40,7 @@ class SmappeeFlowHandler( async def async_step_zeroconf( self, discovery_info: zeroconf.ZeroconfServiceInfo - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle zeroconf discovery.""" if not discovery_info.hostname.startswith(SUPPORTED_LOCAL_DEVICES): diff --git a/homeassistant/components/smappee/sensor.py b/homeassistant/components/smappee/sensor.py index ad6e5af963e..c984d936b06 100644 --- a/homeassistant/components/smappee/sensor.py +++ b/homeassistant/components/smappee/sensor.py @@ -1,4 +1,5 @@ """Support for monitoring a Smappee energy sensor.""" + from __future__ import annotations from dataclasses import dataclass, field @@ -18,26 +19,21 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN -@dataclass(frozen=True) -class SmappeeRequiredKeysMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class SmappeeSensorEntityDescription(SensorEntityDescription): + """Describes Smappee sensor entity.""" sensor_id: str -@dataclass(frozen=True) -class SmappeeSensorEntityDescription(SensorEntityDescription, SmappeeRequiredKeysMixin): - """Describes Smappee sensor entity.""" - - -@dataclass(frozen=True) +@dataclass(frozen=True, kw_only=True) class SmappeePollingSensorEntityDescription(SmappeeSensorEntityDescription): """Describes Smappee sensor entity.""" local_polling: bool = False -@dataclass(frozen=True) +@dataclass(frozen=True, kw_only=True) class SmappeeVoltageSensorEntityDescription(SmappeeSensorEntityDescription): """Describes Smappee sensor entity.""" diff --git a/homeassistant/components/smappee/switch.py b/homeassistant/components/smappee/switch.py index 238e41af8ff..1bc5d159145 100644 --- a/homeassistant/components/smappee/switch.py +++ b/homeassistant/components/smappee/switch.py @@ -1,4 +1,5 @@ """Support for interacting with Smappee Comport Plugs, Switches and Output Modules.""" + from typing import Any from homeassistant.components.switch import SwitchEntity @@ -35,18 +36,18 @@ async def async_setup_entry( ) ) elif actuator.type == "INFINITY_OUTPUT_MODULE": - for option in actuator.state_options: - entities.append( - SmappeeActuator( - smappee_base, - service_location, - actuator.name, - actuator_id, - actuator.type, - actuator.serialnumber, - actuator_state_option=option, - ) + entities.extend( + SmappeeActuator( + smappee_base, + service_location, + actuator.name, + actuator_id, + actuator.type, + actuator.serialnumber, + actuator_state_option=option, ) + for option in actuator.state_options + ) async_add_entities(entities, True) diff --git a/homeassistant/components/smart_meter_texas/__init__.py b/homeassistant/components/smart_meter_texas/__init__.py index 47b74c53db6..c6e466392f0 100644 --- a/homeassistant/components/smart_meter_texas/__init__.py +++ b/homeassistant/components/smart_meter_texas/__init__.py @@ -1,4 +1,5 @@ """The Smart Meter Texas integration.""" + import logging import ssl diff --git a/homeassistant/components/smart_meter_texas/config_flow.py b/homeassistant/components/smart_meter_texas/config_flow.py index dc0e4e93eff..f2fab31caaa 100644 --- a/homeassistant/components/smart_meter_texas/config_flow.py +++ b/homeassistant/components/smart_meter_texas/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Smart Meter Texas integration.""" + import logging from aiohttp import ClientError @@ -9,8 +10,10 @@ from smart_meter_texas.exceptions import ( ) import voluptuous as vol -from homeassistant import config_entries, core, exceptions +from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import aiohttp_client from .const import DOMAIN @@ -22,7 +25,7 @@ DATA_SCHEMA = vol.Schema( ) -async def validate_input(hass: core.HomeAssistant, data): +async def validate_input(hass: HomeAssistant, data): """Validate the user input allows us to connect. Data has the keys from DATA_SCHEMA with values provided by the user. @@ -44,7 +47,7 @@ async def validate_input(hass: core.HomeAssistant, data): return {"title": account.username} -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class SMTConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Smart Meter Texas.""" VERSION = 1 @@ -76,9 +79,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -class CannotConnect(exceptions.HomeAssistantError): +class CannotConnect(HomeAssistantError): """Error to indicate we cannot connect.""" -class InvalidAuth(exceptions.HomeAssistantError): +class InvalidAuth(HomeAssistantError): """Error to indicate there is invalid auth.""" diff --git a/homeassistant/components/smart_meter_texas/const.py b/homeassistant/components/smart_meter_texas/const.py index 3b87454a6a7..defe49f0be4 100644 --- a/homeassistant/components/smart_meter_texas/const.py +++ b/homeassistant/components/smart_meter_texas/const.py @@ -1,4 +1,5 @@ """Constants for the Smart Meter Texas integration.""" + from datetime import timedelta SCAN_INTERVAL = timedelta(hours=1) diff --git a/homeassistant/components/smart_meter_texas/sensor.py b/homeassistant/components/smart_meter_texas/sensor.py index a35a92bf257..80fc79671b5 100644 --- a/homeassistant/components/smart_meter_texas/sensor.py +++ b/homeassistant/components/smart_meter_texas/sensor.py @@ -1,4 +1,5 @@ """Support for Smart Meter Texas sensors.""" + from smart_meter_texas import Meter from homeassistant.components.sensor import ( diff --git a/homeassistant/components/smartthings/__init__.py b/homeassistant/components/smartthings/__init__.py index cdf04be29f3..8136806cd0b 100644 --- a/homeassistant/components/smartthings/__init__.py +++ b/homeassistant/components/smartthings/__init__.py @@ -1,4 +1,5 @@ """Support for SmartThings Cloud.""" + from __future__ import annotations import asyncio @@ -26,6 +27,7 @@ from homeassistant.helpers.dispatcher import ( from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType +from homeassistant.loader import async_get_loaded_integration from .config_flow import SmartThingsFlowHandler # noqa: F401 from .const import ( @@ -103,6 +105,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: api = SmartThings(async_get_clientsession(hass), entry.data[CONF_ACCESS_TOKEN]) + # Ensure platform modules are loaded since the DeviceBroker will + # import them below and we want them to be cached ahead of time + # so the integration does not do blocking I/O in the event loop + # to import the modules. + await async_get_loaded_integration(hass, DOMAIN).async_get_platforms(PLATFORMS) + remove_entry = False try: # See if the app is already setup. This occurs when there are diff --git a/homeassistant/components/smartthings/binary_sensor.py b/homeassistant/components/smartthings/binary_sensor.py index 25f9fa224ff..4bb60217eee 100644 --- a/homeassistant/components/smartthings/binary_sensor.py +++ b/homeassistant/components/smartthings/binary_sensor.py @@ -1,4 +1,5 @@ """Support for binary sensors through the SmartThings cloud API.""" + from __future__ import annotations from collections.abc import Sequence diff --git a/homeassistant/components/smartthings/climate.py b/homeassistant/components/smartthings/climate.py index 4c2afa45b7f..4c767cbfa30 100644 --- a/homeassistant/components/smartthings/climate.py +++ b/homeassistant/components/smartthings/climate.py @@ -1,4 +1,5 @@ """Support for climate devices through the SmartThings cloud API.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/smartthings/config_flow.py b/homeassistant/components/smartthings/config_flow.py index 5e3451dfbce..85f350b8fb3 100644 --- a/homeassistant/components/smartthings/config_flow.py +++ b/homeassistant/components/smartthings/config_flow.py @@ -1,4 +1,5 @@ """Config flow to configure SmartThings.""" + from http import HTTPStatus import logging @@ -7,7 +8,7 @@ from pysmartthings import APIResponseError, AppOAuth, SmartThings from pysmartthings.installedapp import format_install_url import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_ACCESS_TOKEN, CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -35,7 +36,7 @@ from .smartapp import ( _LOGGER = logging.getLogger(__name__) -class SmartThingsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class SmartThingsFlowHandler(ConfigFlow, domain=DOMAIN): """Handle configuration of SmartThings integrations.""" VERSION = 2 diff --git a/homeassistant/components/smartthings/const.py b/homeassistant/components/smartthings/const.py index 393242a30dd..e50837697e7 100644 --- a/homeassistant/components/smartthings/const.py +++ b/homeassistant/components/smartthings/const.py @@ -1,4 +1,5 @@ """Constants used by the SmartThings component and platforms.""" + from datetime import timedelta import re diff --git a/homeassistant/components/smartthings/cover.py b/homeassistant/components/smartthings/cover.py index 83522c61794..276a68176b4 100644 --- a/homeassistant/components/smartthings/cover.py +++ b/homeassistant/components/smartthings/cover.py @@ -1,4 +1,5 @@ """Support for covers through the SmartThings cloud API.""" + from __future__ import annotations from collections.abc import Sequence @@ -62,7 +63,8 @@ def get_capabilities(capabilities: Sequence[str]) -> Sequence[str] | None: # Must have one of the min_required if any(capability in capabilities for capability in min_required): # Return all capabilities supported/consumed - return min_required + [ + return [ + *min_required, Capability.battery, Capability.switch_level, Capability.window_shade_level, diff --git a/homeassistant/components/smartthings/fan.py b/homeassistant/components/smartthings/fan.py index 0e15ea7800a..215986dfb0d 100644 --- a/homeassistant/components/smartthings/fan.py +++ b/homeassistant/components/smartthings/fan.py @@ -1,4 +1,5 @@ """Support for fans through the SmartThings cloud API.""" + from __future__ import annotations from collections.abc import Sequence @@ -31,11 +32,9 @@ async def async_setup_entry( """Add fans for a config entry.""" broker = hass.data[DOMAIN][DATA_BROKERS][config_entry.entry_id] async_add_entities( - [ - SmartThingsFan(device) - for device in broker.devices.values() - if broker.any_assigned(device.device_id, "fan") - ] + SmartThingsFan(device) + for device in broker.devices.values() + if broker.any_assigned(device.device_id, "fan") ) @@ -60,9 +59,9 @@ def get_capabilities(capabilities: Sequence[str]) -> Sequence[str] | None: supported = [Capability.switch] - for capability in optional: - if capability in capabilities: - supported.append(capability) + supported.extend( + capability for capability in optional if capability in capabilities + ) return supported diff --git a/homeassistant/components/smartthings/light.py b/homeassistant/components/smartthings/light.py index 58623e08394..24a44a99d94 100644 --- a/homeassistant/components/smartthings/light.py +++ b/homeassistant/components/smartthings/light.py @@ -1,4 +1,5 @@ """Support for lights through the SmartThings cloud API.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/smartthings/lock.py b/homeassistant/components/smartthings/lock.py index c0fbc32fa19..0cd954e7542 100644 --- a/homeassistant/components/smartthings/lock.py +++ b/homeassistant/components/smartthings/lock.py @@ -1,4 +1,5 @@ """Support for locks through the SmartThings cloud API.""" + from __future__ import annotations from collections.abc import Sequence @@ -33,11 +34,9 @@ async def async_setup_entry( """Add locks for a config entry.""" broker = hass.data[DOMAIN][DATA_BROKERS][config_entry.entry_id] async_add_entities( - [ - SmartThingsLock(device) - for device in broker.devices.values() - if broker.any_assigned(device.device_id, "lock") - ] + SmartThingsLock(device) + for device in broker.devices.values() + if broker.any_assigned(device.device_id, "lock") ) diff --git a/homeassistant/components/smartthings/scene.py b/homeassistant/components/smartthings/scene.py index ffdb900237e..9756cef9f04 100644 --- a/homeassistant/components/smartthings/scene.py +++ b/homeassistant/components/smartthings/scene.py @@ -1,4 +1,5 @@ """Support for scenes through the SmartThings cloud API.""" + from typing import Any from homeassistant.components.scene import Scene @@ -16,7 +17,7 @@ async def async_setup_entry( ) -> None: """Add switches for a config entry.""" broker = hass.data[DOMAIN][DATA_BROKERS][config_entry.entry_id] - async_add_entities([SmartThingsScene(scene) for scene in broker.scenes.values()]) + async_add_entities(SmartThingsScene(scene) for scene in broker.scenes.values()) class SmartThingsScene(Scene): diff --git a/homeassistant/components/smartthings/sensor.py b/homeassistant/components/smartthings/sensor.py index 18016a88d29..13315c30031 100644 --- a/homeassistant/components/smartthings/sensor.py +++ b/homeassistant/components/smartthings/sensor.py @@ -1,4 +1,5 @@ """Support for sensors through the SmartThings cloud API.""" + from __future__ import annotations from collections import namedtuple diff --git a/homeassistant/components/smartthings/smartapp.py b/homeassistant/components/smartthings/smartapp.py index 78c0bfa86b1..1c18a39b1e6 100644 --- a/homeassistant/components/smartthings/smartapp.py +++ b/homeassistant/components/smartthings/smartapp.py @@ -1,4 +1,5 @@ """SmartApp functionality to receive cloud-push notifications.""" + import asyncio import functools import logging @@ -84,11 +85,9 @@ async def validate_installed_app(api, installed_app_id: str): installed_app = await api.installed_app(installed_app_id) if installed_app.installed_app_status != InstalledAppStatus.AUTHORIZED: raise RuntimeWarning( - "Installed SmartApp instance '{}' ({}) is not AUTHORIZED but instead {}".format( - installed_app.display_name, - installed_app.installed_app_id, - installed_app.installed_app_status, - ) + f"Installed SmartApp instance '{installed_app.display_name}' " + f"({installed_app.installed_app_id}) is not AUTHORIZED " + f"but instead {installed_app.installed_app_status}" ) return installed_app diff --git a/homeassistant/components/smartthings/switch.py b/homeassistant/components/smartthings/switch.py index e6432dcb50c..bd5f7bc0b68 100644 --- a/homeassistant/components/smartthings/switch.py +++ b/homeassistant/components/smartthings/switch.py @@ -1,4 +1,5 @@ """Support for switches through the SmartThings cloud API.""" + from __future__ import annotations from collections.abc import Sequence @@ -23,11 +24,9 @@ async def async_setup_entry( """Add switches for a config entry.""" broker = hass.data[DOMAIN][DATA_BROKERS][config_entry.entry_id] async_add_entities( - [ - SmartThingsSwitch(device) - for device in broker.devices.values() - if broker.any_assigned(device.device_id, "switch") - ] + SmartThingsSwitch(device) + for device in broker.devices.values() + if broker.any_assigned(device.device_id, "switch") ) diff --git a/homeassistant/components/smarttub/__init__.py b/homeassistant/components/smarttub/__init__.py index f98dbac86a1..8406fdc4c2f 100644 --- a/homeassistant/components/smarttub/__init__.py +++ b/homeassistant/components/smarttub/__init__.py @@ -1,4 +1,5 @@ """SmartTub integration.""" + from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/smarttub/binary_sensor.py b/homeassistant/components/smarttub/binary_sensor.py index 99037cd623c..cca0c6bc2ce 100644 --- a/homeassistant/components/smarttub/binary_sensor.py +++ b/homeassistant/components/smarttub/binary_sensor.py @@ -1,4 +1,5 @@ """Platform for binary sensor integration.""" + from __future__ import annotations from smarttub import SpaError, SpaReminder diff --git a/homeassistant/components/smarttub/climate.py b/homeassistant/components/smarttub/climate.py index 4921fca022d..f0bb84b3390 100644 --- a/homeassistant/components/smarttub/climate.py +++ b/homeassistant/components/smarttub/climate.py @@ -1,4 +1,5 @@ """Platform for climate integration.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/smarttub/config_flow.py b/homeassistant/components/smarttub/config_flow.py index e1637b86d84..60f14b03e45 100644 --- a/homeassistant/components/smarttub/config_flow.py +++ b/homeassistant/components/smarttub/config_flow.py @@ -1,4 +1,5 @@ """Config flow to configure the SmartTub integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -7,9 +8,8 @@ from typing import Any from smarttub import LoginFailed import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_EMAIL, CONF_PASSWORD -from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN from .controller import SmartTubController @@ -19,7 +19,7 @@ DATA_SCHEMA = vol.Schema( ) -class SmartTubConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class SmartTubConfigFlow(ConfigFlow, domain=DOMAIN): """SmartTub configuration flow.""" VERSION = 1 @@ -28,7 +28,7 @@ class SmartTubConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Instantiate config flow.""" super().__init__() self._reauth_input: Mapping[str, Any] | None = None - self._reauth_entry: config_entries.ConfigEntry | None = None + self._reauth_entry: ConfigEntry | None = None async def async_step_user(self, user_input=None): """Handle a flow initiated by the user.""" @@ -67,7 +67,9 @@ class SmartTubConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="user", data_schema=DATA_SCHEMA, errors=errors ) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Get new credentials if the current ones don't work anymore.""" self._reauth_input = entry_data self._reauth_entry = self.hass.config_entries.async_get_entry( diff --git a/homeassistant/components/smarttub/entity.py b/homeassistant/components/smarttub/entity.py index 6e6cb00a7d3..f9ab1d10bfe 100644 --- a/homeassistant/components/smarttub/entity.py +++ b/homeassistant/components/smarttub/entity.py @@ -1,4 +1,5 @@ """Base classes for SmartTub entities.""" + import smarttub from homeassistant.helpers.device_registry import DeviceInfo diff --git a/homeassistant/components/smarttub/icons.json b/homeassistant/components/smarttub/icons.json new file mode 100644 index 00000000000..7ae96d03383 --- /dev/null +++ b/homeassistant/components/smarttub/icons.json @@ -0,0 +1,8 @@ +{ + "services": { + "set_primary_filtration": "mdi:filter", + "set_secondary_filtration": "mdi:filter-multiple", + "snooze_reminder": "mdi:timer-pause", + "reset_reminder": "mdi:timer-sync" + } +} diff --git a/homeassistant/components/smarttub/light.py b/homeassistant/components/smarttub/light.py index d89cdba3367..b5fac0b34f6 100644 --- a/homeassistant/components/smarttub/light.py +++ b/homeassistant/components/smarttub/light.py @@ -1,4 +1,5 @@ """Platform for light integration.""" + from typing import Any from smarttub import SpaLight diff --git a/homeassistant/components/smarttub/sensor.py b/homeassistant/components/smarttub/sensor.py index c362e1ea8f0..3694ca81a6b 100644 --- a/homeassistant/components/smarttub/sensor.py +++ b/homeassistant/components/smarttub/sensor.py @@ -1,4 +1,5 @@ """Platform for sensor integration.""" + from enum import Enum import smarttub diff --git a/homeassistant/components/smarttub/switch.py b/homeassistant/components/smarttub/switch.py index aeeca46aaa9..6e1cf9bef2a 100644 --- a/homeassistant/components/smarttub/switch.py +++ b/homeassistant/components/smarttub/switch.py @@ -1,4 +1,5 @@ """Platform for switch integration.""" + import asyncio from typing import Any diff --git a/homeassistant/components/smarty/__init__.py b/homeassistant/components/smarty/__init__.py index e3cf1dcf287..cc2e3850ef9 100644 --- a/homeassistant/components/smarty/__init__.py +++ b/homeassistant/components/smarty/__init__.py @@ -1,4 +1,5 @@ """Support to control a Salda Smarty XP/XV ventilation unit.""" + from datetime import timedelta import ipaddress import logging diff --git a/homeassistant/components/smarty/binary_sensor.py b/homeassistant/components/smarty/binary_sensor.py index d9d757a71b5..cf40dc7b982 100644 --- a/homeassistant/components/smarty/binary_sensor.py +++ b/homeassistant/components/smarty/binary_sensor.py @@ -1,4 +1,5 @@ """Support for Salda Smarty XP/XV Ventilation Unit Binary Sensors.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/smarty/fan.py b/homeassistant/components/smarty/fan.py index d3ba407fa40..6d46e040033 100644 --- a/homeassistant/components/smarty/fan.py +++ b/homeassistant/components/smarty/fan.py @@ -1,4 +1,5 @@ """Platform to control a Salda Smarty XP/XV ventilation unit.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/smarty/sensor.py b/homeassistant/components/smarty/sensor.py index 57d681594cf..a0c15b3825f 100644 --- a/homeassistant/components/smarty/sensor.py +++ b/homeassistant/components/smarty/sensor.py @@ -1,4 +1,5 @@ """Support for Salda Smarty XP/XV Ventilation Unit Sensors.""" + from __future__ import annotations import datetime as dt diff --git a/homeassistant/components/smhi/__init__.py b/homeassistant/components/smhi/__init__.py index 1dbfb5ecedd..94bdfcc4559 100644 --- a/homeassistant/components/smhi/__init__.py +++ b/homeassistant/components/smhi/__init__.py @@ -1,4 +1,5 @@ """Support for the Swedish weather institute weather service.""" + from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_LATITUDE, diff --git a/homeassistant/components/smhi/config_flow.py b/homeassistant/components/smhi/config_flow.py index bfa38f317a9..b3350f6bb18 100644 --- a/homeassistant/components/smhi/config_flow.py +++ b/homeassistant/components/smhi/config_flow.py @@ -1,4 +1,5 @@ """Config flow to configure SMHI component.""" + from __future__ import annotations from typing import Any @@ -6,11 +7,15 @@ from typing import Any from smhi.smhi_lib import Smhi, SmhiForecastException import voluptuous as vol -from homeassistant import config_entries +from homeassistant.components.weather import DOMAIN as WEATHER_DOMAIN +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_LATITUDE, CONF_LOCATION, CONF_LONGITUDE, CONF_NAME from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResult -from homeassistant.helpers import aiohttp_client +from homeassistant.helpers import ( + aiohttp_client, + device_registry as dr, + entity_registry as er, +) from homeassistant.helpers.selector import LocationSelector from .const import DEFAULT_NAME, DOMAIN, HOME_LOCATION_NAME @@ -30,14 +35,15 @@ async def async_check_location( return True -class SmhiFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class SmhiFlowHandler(ConfigFlow, domain=DOMAIN): """Config flow for SMHI component.""" VERSION = 2 + config_entry: ConfigEntry | None async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" errors: dict[str, str] = {} @@ -74,3 +80,62 @@ class SmhiFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ), errors=errors, ) + + async def async_step_reconfigure( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle a reconfiguration flow initialized by the user.""" + self.config_entry = self.hass.config_entries.async_get_entry( + self.context["entry_id"] + ) + return await self.async_step_reconfigure_confirm() + + async def async_step_reconfigure_confirm( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle a reconfiguration flow initialized by the user.""" + errors: dict[str, str] = {} + assert self.config_entry + + if user_input is not None: + lat: float = user_input[CONF_LOCATION][CONF_LATITUDE] + lon: float = user_input[CONF_LOCATION][CONF_LONGITUDE] + if await async_check_location(self.hass, lon, lat): + unique_id = f"{lat}-{lon}" + await self.async_set_unique_id(unique_id) + self._abort_if_unique_id_configured() + + old_lat = self.config_entry.data[CONF_LOCATION][CONF_LATITUDE] + old_lon = self.config_entry.data[CONF_LOCATION][CONF_LONGITUDE] + + entity_reg = er.async_get(self.hass) + if entity := entity_reg.async_get_entity_id( + WEATHER_DOMAIN, DOMAIN, f"{old_lat}, {old_lon}" + ): + entity_reg.async_update_entity( + entity, new_unique_id=f"{lat}, {lon}" + ) + + device_reg = dr.async_get(self.hass) + if device := device_reg.async_get_device( + identifiers={(DOMAIN, f"{old_lat}, {old_lon}")} + ): + device_reg.async_update_device( + device.id, new_identifiers={(DOMAIN, f"{lat}, {lon}")} + ) + + return self.async_update_reload_and_abort( + self.config_entry, + unique_id=unique_id, + data={**self.config_entry.data, **user_input}, + reason="reconfigure_successful", + ) + errors["base"] = "wrong_location" + + schema = self.add_suggested_values_to_schema( + vol.Schema({vol.Required(CONF_LOCATION): LocationSelector()}), + self.config_entry.data, + ) + return self.async_show_form( + step_id="reconfigure_confirm", data_schema=schema, errors=errors + ) diff --git a/homeassistant/components/smhi/const.py b/homeassistant/components/smhi/const.py index cc1c4550723..11401119227 100644 --- a/homeassistant/components/smhi/const.py +++ b/homeassistant/components/smhi/const.py @@ -1,4 +1,5 @@ """Constants in smhi component.""" + from typing import Final from homeassistant.components.weather import DOMAIN as WEATHER_DOMAIN diff --git a/homeassistant/components/smhi/strings.json b/homeassistant/components/smhi/strings.json index 2b155f58f96..e78fee64a2b 100644 --- a/homeassistant/components/smhi/strings.json +++ b/homeassistant/components/smhi/strings.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_account%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", + "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]" }, "step": { "user": { @@ -10,6 +11,13 @@ "latitude": "[%key:common::config_flow::data::latitude%]", "longitude": "[%key:common::config_flow::data::longitude%]" } + }, + "reconfigure_confirm": { + "title": "Reconfigure your location in Sweden", + "data": { + "latitude": "[%key:common::config_flow::data::latitude%]", + "longitude": "[%key:common::config_flow::data::longitude%]" + } } }, "error": { diff --git a/homeassistant/components/smhi/weather.py b/homeassistant/components/smhi/weather.py index 5814db8168e..bf069f4b26a 100644 --- a/homeassistant/components/smhi/weather.py +++ b/homeassistant/components/smhi/weather.py @@ -1,4 +1,5 @@ """Support for the Swedish weather institute weather service.""" + from __future__ import annotations import asyncio @@ -194,35 +195,6 @@ class SmhiWeather(WeatherEntity): """Retry refresh weather forecast.""" await self.async_update(no_throttle=True) - @property - def forecast(self) -> list[Forecast] | None: - """Return the forecast.""" - if self._forecast_daily is None or len(self._forecast_daily) < 2: - return None - - data: list[Forecast] = [] - - for forecast in self._forecast_daily[1:]: - condition = CONDITION_MAP.get(forecast.symbol) - - data.append( - { - ATTR_FORECAST_TIME: forecast.valid_time.isoformat(), - ATTR_FORECAST_NATIVE_TEMP: forecast.temperature_max, - ATTR_FORECAST_NATIVE_TEMP_LOW: forecast.temperature_min, - ATTR_FORECAST_NATIVE_PRECIPITATION: forecast.total_precipitation, - ATTR_FORECAST_CONDITION: condition, - ATTR_FORECAST_NATIVE_PRESSURE: forecast.pressure, - ATTR_FORECAST_WIND_BEARING: forecast.wind_direction, - ATTR_FORECAST_NATIVE_WIND_SPEED: forecast.wind_speed, - ATTR_FORECAST_HUMIDITY: forecast.humidity, - ATTR_FORECAST_NATIVE_WIND_GUST_SPEED: forecast.wind_gust, - ATTR_FORECAST_CLOUD_COVERAGE: forecast.cloudiness, - } - ) - - return data - def _get_forecast_data( self, forecast_data: list[SmhiForecast] | None ) -> list[Forecast] | None: diff --git a/homeassistant/components/sms/__init__.py b/homeassistant/components/sms/__init__.py index a606b83896f..2d18d44de3a 100644 --- a/homeassistant/components/sms/__init__.py +++ b/homeassistant/components/sms/__init__.py @@ -1,4 +1,5 @@ """The sms component.""" + import logging import voluptuous as vol diff --git a/homeassistant/components/sms/config_flow.py b/homeassistant/components/sms/config_flow.py index df3530764cb..ff509bbbb97 100644 --- a/homeassistant/components/sms/config_flow.py +++ b/homeassistant/components/sms/config_flow.py @@ -1,11 +1,14 @@ """Config flow for SMS integration.""" + import logging import gammu import voluptuous as vol -from homeassistant import config_entries, core, exceptions +from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_DEVICE +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import selector from .const import CONF_BAUD_SPEED, DEFAULT_BAUD_SPEED, DEFAULT_BAUD_SPEEDS, DOMAIN @@ -23,7 +26,7 @@ DATA_SCHEMA = vol.Schema( ) -async def get_imei_from_config(hass: core.HomeAssistant, data): +async def get_imei_from_config(hass: HomeAssistant, data): """Validate the user input allows us to connect. Data has the keys from DATA_SCHEMA with values provided by the user. @@ -48,7 +51,7 @@ async def get_imei_from_config(hass: core.HomeAssistant, data): return imei -class SMSFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class SMSFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a config flow for SMS integration.""" VERSION = 1 @@ -81,5 +84,5 @@ class SMSFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_user(user_input) -class CannotConnect(exceptions.HomeAssistantError): +class CannotConnect(HomeAssistantError): """Error to indicate we cannot connect.""" diff --git a/homeassistant/components/sms/coordinator.py b/homeassistant/components/sms/coordinator.py index fd212fce4f2..7bc691afedf 100644 --- a/homeassistant/components/sms/coordinator.py +++ b/homeassistant/components/sms/coordinator.py @@ -1,4 +1,5 @@ """DataUpdateCoordinators for the sms integration.""" + import asyncio from datetime import timedelta import logging diff --git a/homeassistant/components/sms/gateway.py b/homeassistant/components/sms/gateway.py index 578b2191bd2..50abc9b39ef 100644 --- a/homeassistant/components/sms/gateway.py +++ b/homeassistant/components/sms/gateway.py @@ -1,4 +1,5 @@ """The sms gateway to interact with a GSM modem.""" + import logging import gammu diff --git a/homeassistant/components/sms/icons.json b/homeassistant/components/sms/icons.json new file mode 100644 index 00000000000..f863d7a35a4 --- /dev/null +++ b/homeassistant/components/sms/icons.json @@ -0,0 +1,12 @@ +{ + "entity": { + "sensor": { + "signal_percent": { + "default": "mdi:signal-cellular-3" + }, + "cid": { + "default": "mdi:radio-tower" + } + } + } +} diff --git a/homeassistant/components/sms/notify.py b/homeassistant/components/sms/notify.py index 21d3ab2beb5..3374681c0f3 100644 --- a/homeassistant/components/sms/notify.py +++ b/homeassistant/components/sms/notify.py @@ -1,4 +1,5 @@ """Support for SMS notification services.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/sms/sensor.py b/homeassistant/components/sms/sensor.py index d4c45b83d82..821200f68b1 100644 --- a/homeassistant/components/sms/sensor.py +++ b/homeassistant/components/sms/sensor.py @@ -1,4 +1,5 @@ """Support for SMS dongle sensor.""" + from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, @@ -25,7 +26,6 @@ SIGNAL_SENSORS = ( ), SensorEntityDescription( key="SignalPercent", - icon="mdi:signal-cellular-3", translation_key="signal_percent", native_unit_of_measurement=PERCENTAGE, entity_registry_enabled_default=True, @@ -62,7 +62,6 @@ NETWORK_SENSORS = ( SensorEntityDescription( key="CID", translation_key="cid", - icon="mdi:radio-tower", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), @@ -86,15 +85,14 @@ async def async_setup_entry( network_coordinator = sms_data[NETWORK_COORDINATOR] gateway = sms_data[GATEWAY] unique_id = str(await gateway.get_imei_async()) - entities = [] - for description in SIGNAL_SENSORS: - entities.append( - DeviceSensor(signal_coordinator, description, unique_id, gateway) - ) - for description in NETWORK_SENSORS: - entities.append( - DeviceSensor(network_coordinator, description, unique_id, gateway) - ) + entities = [ + DeviceSensor(signal_coordinator, description, unique_id, gateway) + for description in SIGNAL_SENSORS + ] + entities.extend( + DeviceSensor(network_coordinator, description, unique_id, gateway) + for description in NETWORK_SENSORS + ) async_add_entities(entities, True) diff --git a/homeassistant/components/smtp/icons.json b/homeassistant/components/smtp/icons.json new file mode 100644 index 00000000000..a03163179cb --- /dev/null +++ b/homeassistant/components/smtp/icons.json @@ -0,0 +1,5 @@ +{ + "services": { + "reload": "mdi:reload" + } +} diff --git a/homeassistant/components/smtp/notify.py b/homeassistant/components/smtp/notify.py index 87600650551..bac18576f06 100644 --- a/homeassistant/components/smtp/notify.py +++ b/homeassistant/components/smtp/notify.py @@ -1,4 +1,5 @@ """Mail (SMTP) notification service.""" + from __future__ import annotations from email.mime.application import MIMEApplication @@ -263,10 +264,6 @@ def _attach_file(hass, atch_name, content_id=""): file_name = os.path.basename(atch_name) url = "https://www.home-assistant.io/docs/configuration/basic/" raise ServiceValidationError( - f"Cannot send email with attachment '{file_name}' " - f"from directory '{file_path}' which is not secure to load data from. " - f"Only folders added to `{allow_list}` are accessible. " - f"See {url} for more information.", translation_domain=DOMAIN, translation_key="remote_path_not_allowed", translation_placeholders={ @@ -290,9 +287,9 @@ def _attach_file(hass, atch_name, content_id=""): atch_name, ) attachment = MIMEApplication(file_bytes, Name=os.path.basename(atch_name)) - attachment[ - "Content-Disposition" - ] = f'attachment; filename="{os.path.basename(atch_name)}"' + attachment["Content-Disposition"] = ( + f'attachment; filename="{os.path.basename(atch_name)}"' + ) else: if content_id: attachment.add_header("Content-ID", f"<{content_id}>") diff --git a/homeassistant/components/smud/__init__.py b/homeassistant/components/smud/__init__.py new file mode 100644 index 00000000000..ce33399ad45 --- /dev/null +++ b/homeassistant/components/smud/__init__.py @@ -0,0 +1 @@ +"""Virtual integration: Sacramento Municipal Utility District (SMUD).""" diff --git a/homeassistant/components/smud/manifest.json b/homeassistant/components/smud/manifest.json new file mode 100644 index 00000000000..7b9b5d9d397 --- /dev/null +++ b/homeassistant/components/smud/manifest.json @@ -0,0 +1,6 @@ +{ + "domain": "smud", + "name": "Sacramento Municipal Utility District (SMUD)", + "integration_type": "virtual", + "supported_by": "opower" +} diff --git a/homeassistant/components/snapcast/__init__.py b/homeassistant/components/snapcast/__init__.py index d8ff55cc175..a4163355944 100644 --- a/homeassistant/components/snapcast/__init__.py +++ b/homeassistant/components/snapcast/__init__.py @@ -1,4 +1,5 @@ """Snapcast Integration.""" + import logging import snapcast.control diff --git a/homeassistant/components/snapcast/config_flow.py b/homeassistant/components/snapcast/config_flow.py index 479d1d648b8..c9f69c48ab5 100644 --- a/homeassistant/components/snapcast/config_flow.py +++ b/homeassistant/components/snapcast/config_flow.py @@ -9,9 +9,8 @@ import snapcast.control from snapcast.control.server import CONTROL_PORT import voluptuous as vol -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_PORT -from homeassistant.data_entry_flow import FlowResult from .const import DEFAULT_TITLE, DOMAIN @@ -28,7 +27,7 @@ SNAPCAST_SCHEMA = vol.Schema( class SnapcastConfigFlow(ConfigFlow, domain=DOMAIN): """Snapcast config flow.""" - async def async_step_user(self, user_input=None) -> FlowResult: + async def async_step_user(self, user_input=None) -> ConfigFlowResult: """Handle first step.""" errors = {} if user_input: diff --git a/homeassistant/components/snapcast/const.py b/homeassistant/components/snapcast/const.py index ded57e6fb03..fbd5b903669 100644 --- a/homeassistant/components/snapcast/const.py +++ b/homeassistant/components/snapcast/const.py @@ -1,4 +1,5 @@ """Constants for Snapcast.""" + from homeassistant.const import Platform PLATFORMS: list[Platform] = [Platform.MEDIA_PLAYER] diff --git a/homeassistant/components/snapcast/icons.json b/homeassistant/components/snapcast/icons.json new file mode 100644 index 00000000000..bdc20665282 --- /dev/null +++ b/homeassistant/components/snapcast/icons.json @@ -0,0 +1,9 @@ +{ + "services": { + "join": "mdi:music-note-plus", + "unjoin": "mdi:music-note-minus", + "snapshot": "mdi:camera", + "restore": "mdi:camera-retake", + "set_latency": "mdi:camera-timer" + } +} diff --git a/homeassistant/components/snapcast/manifest.json b/homeassistant/components/snapcast/manifest.json index f59283bb5f6..ea40f45d8cc 100644 --- a/homeassistant/components/snapcast/manifest.json +++ b/homeassistant/components/snapcast/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/snapcast", "iot_class": "local_push", "loggers": ["construct", "snapcast"], - "requirements": ["snapcast==2.3.3"] + "requirements": ["snapcast==2.3.6"] } diff --git a/homeassistant/components/snapcast/media_player.py b/homeassistant/components/snapcast/media_player.py index ae2917a106d..0918d6465ad 100644 --- a/homeassistant/components/snapcast/media_player.py +++ b/homeassistant/components/snapcast/media_player.py @@ -1,4 +1,5 @@ """Support for interacting with Snapcast clients.""" + from __future__ import annotations from snapcast.control.server import Snapserver diff --git a/homeassistant/components/snapcast/server.py b/homeassistant/components/snapcast/server.py index bac51150eba..4714156c4c2 100644 --- a/homeassistant/components/snapcast/server.py +++ b/homeassistant/components/snapcast/server.py @@ -1,4 +1,5 @@ """Snapcast Integration.""" + from __future__ import annotations import logging @@ -65,7 +66,7 @@ class HomeAssistantSnapcast: self.server.set_on_connect_callback(None) self.server.set_on_disconnect_callback(None) self.server.set_new_client_callback(None) - await self.server.stop() + self.server.stop() def on_update(self) -> None: """Update all entities. @@ -99,8 +100,8 @@ class HomeAssistantSnapcast: ] del_entities.extend([x for x in self.clients if x not in clients]) - _LOGGER.debug("New clients: %s", str(new_clients)) - _LOGGER.debug("New groups: %s", str(new_groups)) + _LOGGER.debug("New clients: %s", str([c.name for c in new_clients])) + _LOGGER.debug("New groups: %s", str([g.name for g in new_groups])) _LOGGER.debug("Delete: %s", str(del_entities)) ent_reg = er.async_get(self.hass) diff --git a/homeassistant/components/snips/__init__.py b/homeassistant/components/snips/__init__.py index 33500217397..4731a0f324a 100644 --- a/homeassistant/components/snips/__init__.py +++ b/homeassistant/components/snips/__init__.py @@ -1,4 +1,5 @@ """Support for Snips on-device ASR and NLU.""" + from datetime import timedelta import json import logging diff --git a/homeassistant/components/snips/icons.json b/homeassistant/components/snips/icons.json new file mode 100644 index 00000000000..0d465465fe4 --- /dev/null +++ b/homeassistant/components/snips/icons.json @@ -0,0 +1,8 @@ +{ + "services": { + "feedback_off": "mdi:message-alert", + "feedback_on": "mdi:message-alert", + "say": "mdi:chat", + "say_action": "mdi:account-voice" + } +} diff --git a/homeassistant/components/snmp/const.py b/homeassistant/components/snmp/const.py index e51bbc33b90..842c0eaec9e 100644 --- a/homeassistant/components/snmp/const.py +++ b/homeassistant/components/snmp/const.py @@ -1,4 +1,5 @@ """SNMP constants.""" + CONF_ACCEPT_ERRORS = "accept_errors" CONF_AUTH_KEY = "auth_key" CONF_AUTH_PROTOCOL = "auth_protocol" diff --git a/homeassistant/components/snmp/device_tracker.py b/homeassistant/components/snmp/device_tracker.py index 696b079fd5e..4b8ab073b9c 100644 --- a/homeassistant/components/snmp/device_tracker.py +++ b/homeassistant/components/snmp/device_tracker.py @@ -1,4 +1,5 @@ """Support for fetching WiFi associations through SNMP.""" + from __future__ import annotations import binascii diff --git a/homeassistant/components/snmp/sensor.py b/homeassistant/components/snmp/sensor.py index a5915183ad0..f55cd07effb 100644 --- a/homeassistant/components/snmp/sensor.py +++ b/homeassistant/components/snmp/sensor.py @@ -1,9 +1,12 @@ """Support for displaying collected data over SNMP.""" + from __future__ import annotations from datetime import timedelta import logging +from struct import unpack +from pyasn1.codec.ber import decoder from pysnmp.error import PySnmpError import pysnmp.hlapi.asyncio as hlapi from pysnmp.hlapi.asyncio import ( @@ -17,6 +20,8 @@ from pysnmp.hlapi.asyncio import ( UsmUserData, getCmd, ) +from pysnmp.proto.rfc1902 import Opaque +from pysnmp.proto.rfc1905 import NoSuchObject import voluptuous as vol from homeassistant.components.sensor import CONF_STATE_CLASS, PLATFORM_SCHEMA @@ -164,7 +169,10 @@ async def async_setup_platform( errindication, _, _, _ = get_result if errindication and not accept_errors: - _LOGGER.error("Please check the details in the configuration file") + _LOGGER.error( + "Please check the details in the configuration file: %s", + errindication, + ) return name = config.get(CONF_NAME, Template(DEFAULT_NAME, hass)) @@ -247,10 +255,44 @@ class SnmpData: _LOGGER.error( "SNMP error: %s at %s", errstatus.prettyPrint(), - errindex and restable[-1][int(errindex) - 1] or "?", + restable[-1][int(errindex) - 1] if errindex else "?", ) elif (errindication or errstatus) and self._accept_errors: self.value = self._default_value else: for resrow in restable: - self.value = resrow[-1].prettyPrint() + self.value = self._decode_value(resrow[-1]) + + def _decode_value(self, value): + """Decode the different results we could get into strings.""" + + _LOGGER.debug( + "SNMP OID %s received type=%s and data %s", + self._baseoid, + type(value), + bytes(value), + ) + if isinstance(value, NoSuchObject): + _LOGGER.error( + "SNMP error for OID %s: No Such Object currently exists at this OID", + self._baseoid, + ) + return self._default_value + + if isinstance(value, Opaque): + # Float data type is not supported by the pyasn1 library, + # so we need to decode this type ourselves based on: + # https://tools.ietf.org/html/draft-perkins-opaque-01 + if bytes(value).startswith(b"\x9f\x78"): + return str(unpack("!f", bytes(value)[3:])[0]) + # Otherwise Opaque types should be asn1 encoded + try: + decoded_value, _ = decoder.decode(bytes(value)) + return str(decoded_value) + # pylint: disable=broad-except + except Exception as decode_exception: + _LOGGER.error( + "SNMP error in decoding opaque type: %s", decode_exception + ) + return self._default_value + return str(value) diff --git a/homeassistant/components/snmp/switch.py b/homeassistant/components/snmp/switch.py index 3578f0e0c1d..a447cdc8e9c 100644 --- a/homeassistant/components/snmp/switch.py +++ b/homeassistant/components/snmp/switch.py @@ -1,4 +1,5 @@ """Support for SNMP enabled switch.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/snooz/__init__.py b/homeassistant/components/snooz/__init__.py index 8349f781cf8..c97c89c2f4a 100644 --- a/homeassistant/components/snooz/__init__.py +++ b/homeassistant/components/snooz/__init__.py @@ -1,4 +1,5 @@ """The Snooz component.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/snooz/config_flow.py b/homeassistant/components/snooz/config_flow.py index 7174fbc358c..3962a44d8b9 100644 --- a/homeassistant/components/snooz/config_flow.py +++ b/homeassistant/components/snooz/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Snooz component.""" + from __future__ import annotations import asyncio @@ -14,9 +15,8 @@ from homeassistant.components.bluetooth import ( async_discovered_service_info, async_process_advertisements, ) -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_ADDRESS, CONF_NAME, CONF_TOKEN -from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN @@ -45,7 +45,7 @@ class SnoozConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_bluetooth( self, discovery_info: BluetoothServiceInfo - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the bluetooth discovery step.""" await self.async_set_unique_id(discovery_info.address) self._abort_if_unique_id_configured() @@ -57,7 +57,7 @@ class SnoozConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_bluetooth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm discovery.""" assert self._discovery is not None @@ -77,7 +77,7 @@ class SnoozConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the user step to pick discovered device.""" if user_input is not None: name = user_input[CONF_NAME] @@ -128,7 +128,7 @@ class SnoozConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_wait_for_pairing_mode( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Wait for device to enter pairing mode.""" if not self._pairing_task: self._pairing_task = self.hass.async_create_task( @@ -153,7 +153,7 @@ class SnoozConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_pairing_complete( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Create a configuration entry for a device that entered pairing mode.""" assert self._discovery @@ -166,7 +166,7 @@ class SnoozConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_pairing_timeout( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Inform the user that the device never entered pairing mode.""" if user_input is not None: return await self.async_step_wait_for_pairing_mode() @@ -174,7 +174,7 @@ class SnoozConfigFlow(ConfigFlow, domain=DOMAIN): self._set_confirm_only() return self.async_show_form(step_id="pairing_timeout") - def _create_snooz_entry(self, discovery: DiscoveredSnooz) -> FlowResult: + def _create_snooz_entry(self, discovery: DiscoveredSnooz) -> ConfigFlowResult: assert discovery.device.display_name return self.async_create_entry( title=discovery.device.display_name, diff --git a/homeassistant/components/snooz/fan.py b/homeassistant/components/snooz/fan.py index 5cb80cb4189..fd6e5e69556 100644 --- a/homeassistant/components/snooz/fan.py +++ b/homeassistant/components/snooz/fan.py @@ -1,4 +1,5 @@ """Fan representation of a Snooz device.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/snooz/icons.json b/homeassistant/components/snooz/icons.json new file mode 100644 index 00000000000..d9cccfff4ea --- /dev/null +++ b/homeassistant/components/snooz/icons.json @@ -0,0 +1,6 @@ +{ + "services": { + "transition_on": "mdi:blur", + "transition_off": "mdi:blur-off" + } +} diff --git a/homeassistant/components/solaredge/__init__.py b/homeassistant/components/solaredge/__init__.py index 0b685661ac3..69e02c1875c 100644 --- a/homeassistant/components/solaredge/__init__.py +++ b/homeassistant/components/solaredge/__init__.py @@ -1,4 +1,5 @@ """The SolarEdge integration.""" + from __future__ import annotations import socket diff --git a/homeassistant/components/solaredge/config_flow.py b/homeassistant/components/solaredge/config_flow.py index 8bdf6a4b4aa..b75af866549 100644 --- a/homeassistant/components/solaredge/config_flow.py +++ b/homeassistant/components/solaredge/config_flow.py @@ -1,4 +1,5 @@ """Config flow for the SolarEdge platform.""" + from __future__ import annotations from typing import Any @@ -7,16 +8,15 @@ from requests.exceptions import ConnectTimeout, HTTPError import solaredge import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_API_KEY, CONF_NAME from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.util import slugify from .const import CONF_SITE_ID, DEFAULT_NAME, DOMAIN -class SolarEdgeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class SolarEdgeConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow.""" VERSION = 1 @@ -56,7 +56,7 @@ class SolarEdgeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Step when user initializes a integration.""" self._errors = {} if user_input is not None: diff --git a/homeassistant/components/solaredge/const.py b/homeassistant/components/solaredge/const.py index aa6251ff433..6546b41d0ef 100644 --- a/homeassistant/components/solaredge/const.py +++ b/homeassistant/components/solaredge/const.py @@ -1,4 +1,5 @@ """Constants for the SolarEdge Monitoring API.""" + from datetime import timedelta import logging diff --git a/homeassistant/components/solaredge/coordinator.py b/homeassistant/components/solaredge/coordinator.py index 4938a54ed65..d2da99820d7 100644 --- a/homeassistant/components/solaredge/coordinator.py +++ b/homeassistant/components/solaredge/coordinator.py @@ -1,4 +1,5 @@ """Provides the data update coordinators for SolarEdge.""" + from __future__ import annotations from abc import ABC, abstractmethod diff --git a/homeassistant/components/solaredge/icons.json b/homeassistant/components/solaredge/icons.json new file mode 100644 index 00000000000..d190c2abddd --- /dev/null +++ b/homeassistant/components/solaredge/icons.json @@ -0,0 +1,33 @@ +{ + "entity": { + "sensor": { + "lifetime_energy": { + "default": "mdi:solar-power" + }, + "energy_this_year": { + "default": "mdi:solar-power" + }, + "energy_this_month": { + "default": "mdi:solar-power" + }, + "energy_today": { + "default": "mdi:solar-power" + }, + "current_power": { + "default": "mdi:solar-power" + }, + "power_consumption": { + "default": "mdi:flash" + }, + "solar_power": { + "default": "mdi:solar-power" + }, + "grid_power": { + "default": "mdi:power-plug" + }, + "storage_power": { + "default": "mdi:car-battery" + } + } + } +} diff --git a/homeassistant/components/solaredge/sensor.py b/homeassistant/components/solaredge/sensor.py index bb82da5fc89..5ec65a3b9a5 100644 --- a/homeassistant/components/solaredge/sensor.py +++ b/homeassistant/components/solaredge/sensor.py @@ -1,4 +1,5 @@ """Support for SolarEdge Monitoring API.""" + from __future__ import annotations from dataclasses import dataclass @@ -33,26 +34,18 @@ from .coordinator import ( ) -@dataclass(frozen=True) -class SolarEdgeSensorEntityRequiredKeyMixin: - """Sensor entity description with json_key for SolarEdge.""" +@dataclass(frozen=True, kw_only=True) +class SolarEdgeSensorEntityDescription(SensorEntityDescription): + """Sensor entity description for SolarEdge.""" json_key: str -@dataclass(frozen=True) -class SolarEdgeSensorEntityDescription( - SensorEntityDescription, SolarEdgeSensorEntityRequiredKeyMixin -): - """Sensor entity description for SolarEdge.""" - - SENSOR_TYPES = [ SolarEdgeSensorEntityDescription( key="lifetime_energy", json_key="lifeTimeData", translation_key="lifetime_energy", - icon="mdi:solar-power", state_class=SensorStateClass.TOTAL, native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, device_class=SensorDeviceClass.ENERGY, @@ -62,7 +55,6 @@ SENSOR_TYPES = [ json_key="lastYearData", translation_key="energy_this_year", entity_registry_enabled_default=False, - icon="mdi:solar-power", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, device_class=SensorDeviceClass.ENERGY, ), @@ -71,7 +63,6 @@ SENSOR_TYPES = [ json_key="lastMonthData", translation_key="energy_this_month", entity_registry_enabled_default=False, - icon="mdi:solar-power", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, device_class=SensorDeviceClass.ENERGY, ), @@ -80,7 +71,6 @@ SENSOR_TYPES = [ json_key="lastDayData", translation_key="energy_today", entity_registry_enabled_default=False, - icon="mdi:solar-power", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, device_class=SensorDeviceClass.ENERGY, ), @@ -88,7 +78,6 @@ SENSOR_TYPES = [ key="current_power", json_key="currentPower", translation_key="current_power", - icon="mdi:solar-power", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, @@ -134,28 +123,24 @@ SENSOR_TYPES = [ json_key="LOAD", translation_key="power_consumption", entity_registry_enabled_default=False, - icon="mdi:flash", ), SolarEdgeSensorEntityDescription( key="solar_power", json_key="PV", translation_key="solar_power", entity_registry_enabled_default=False, - icon="mdi:solar-power", ), SolarEdgeSensorEntityDescription( key="grid_power", json_key="GRID", translation_key="grid_power", entity_registry_enabled_default=False, - icon="mdi:power-plug", ), SolarEdgeSensorEntityDescription( key="storage_power", json_key="STORAGE", translation_key="storage_power", entity_registry_enabled_default=False, - icon="mdi:car-battery", ), SolarEdgeSensorEntityDescription( key="purchased_energy", diff --git a/homeassistant/components/solaredge_local/sensor.py b/homeassistant/components/solaredge_local/sensor.py index 0475489a6f4..2799d303a19 100644 --- a/homeassistant/components/solaredge_local/sensor.py +++ b/homeassistant/components/solaredge_local/sensor.py @@ -1,4 +1,5 @@ """Support for SolarEdge-local Monitoring API.""" + from __future__ import annotations from contextlib import suppress diff --git a/homeassistant/components/solarlog/__init__.py b/homeassistant/components/solarlog/__init__.py index 95cf5cc4567..d2a3c50295c 100644 --- a/homeassistant/components/solarlog/__init__.py +++ b/homeassistant/components/solarlog/__init__.py @@ -1,4 +1,5 @@ """Solar-Log integration.""" + from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/solarlog/config_flow.py b/homeassistant/components/solarlog/config_flow.py index 86fc0607bf3..83b9c600de8 100644 --- a/homeassistant/components/solarlog/config_flow.py +++ b/homeassistant/components/solarlog/config_flow.py @@ -1,4 +1,5 @@ """Config flow for solarlog integration.""" + import logging from urllib.parse import ParseResult, urlparse @@ -6,7 +7,7 @@ from requests.exceptions import HTTPError, Timeout from sunwatcher.solarlog.solarlog import SolarLog import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_HOST, CONF_NAME from homeassistant.core import HomeAssistant, callback from homeassistant.util import slugify @@ -24,7 +25,7 @@ def solarlog_entries(hass: HomeAssistant): } -class SolarLogConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class SolarLogConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for solarlog.""" VERSION = 1 diff --git a/homeassistant/components/solarlog/const.py b/homeassistant/components/solarlog/const.py index d8ba49adbec..31f17af83b5 100644 --- a/homeassistant/components/solarlog/const.py +++ b/homeassistant/components/solarlog/const.py @@ -1,4 +1,5 @@ """Constants for the Solar-Log integration.""" + from __future__ import annotations DOMAIN = "solarlog" diff --git a/homeassistant/components/solarlog/coordinator.py b/homeassistant/components/solarlog/coordinator.py index d363256f355..6af7c96302d 100644 --- a/homeassistant/components/solarlog/coordinator.py +++ b/homeassistant/components/solarlog/coordinator.py @@ -1,4 +1,5 @@ """DataUpdateCoordinator for solarlog integration.""" + from datetime import timedelta import logging from urllib.parse import ParseResult, urlparse diff --git a/homeassistant/components/solarlog/icons.json b/homeassistant/components/solarlog/icons.json new file mode 100644 index 00000000000..487f11acbe4 --- /dev/null +++ b/homeassistant/components/solarlog/icons.json @@ -0,0 +1,39 @@ +{ + "entity": { + "sensor": { + "power_ac": { + "default": "mdi:solar-power" + }, + "power_dc": { + "default": "mdi:solar-power" + }, + "yield_day": { + "default": "mdi:solar-power" + }, + "yield_yesterday": { + "default": "mdi:solar-power" + }, + "yield_month": { + "default": "mdi:solar-power" + }, + "yield_year": { + "default": "mdi:solar-power" + }, + "yield_total": { + "default": "mdi:solar-power" + }, + "total_power": { + "default": "mdi:solar-power" + }, + "alternator_loss": { + "default": "mdi:solar-power" + }, + "capacity": { + "default": "mdi:solar-power" + }, + "power_available": { + "default": "mdi:solar-power" + } + } + } +} diff --git a/homeassistant/components/solarlog/sensor.py b/homeassistant/components/solarlog/sensor.py index a8025c7fc0f..dcb4afcb863 100644 --- a/homeassistant/components/solarlog/sensor.py +++ b/homeassistant/components/solarlog/sensor.py @@ -1,4 +1,5 @@ """Platform for solarlog sensors.""" + from collections.abc import Callable from dataclasses import dataclass from datetime import datetime @@ -43,7 +44,6 @@ SENSOR_TYPES: tuple[SolarLogSensorEntityDescription, ...] = ( SolarLogSensorEntityDescription( key="power_ac", translation_key="power_ac", - icon="mdi:solar-power", native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, @@ -51,7 +51,6 @@ SENSOR_TYPES: tuple[SolarLogSensorEntityDescription, ...] = ( SolarLogSensorEntityDescription( key="power_dc", translation_key="power_dc", - icon="mdi:solar-power", native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, @@ -73,7 +72,6 @@ SENSOR_TYPES: tuple[SolarLogSensorEntityDescription, ...] = ( SolarLogSensorEntityDescription( key="yield_day", translation_key="yield_day", - icon="mdi:solar-power", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, value=lambda value: round(value / 1000, 3), @@ -81,7 +79,6 @@ SENSOR_TYPES: tuple[SolarLogSensorEntityDescription, ...] = ( SolarLogSensorEntityDescription( key="yield_yesterday", translation_key="yield_yesterday", - icon="mdi:solar-power", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, value=lambda value: round(value / 1000, 3), @@ -89,7 +86,6 @@ SENSOR_TYPES: tuple[SolarLogSensorEntityDescription, ...] = ( SolarLogSensorEntityDescription( key="yield_month", translation_key="yield_month", - icon="mdi:solar-power", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, value=lambda value: round(value / 1000, 3), @@ -97,7 +93,6 @@ SENSOR_TYPES: tuple[SolarLogSensorEntityDescription, ...] = ( SolarLogSensorEntityDescription( key="yield_year", translation_key="yield_year", - icon="mdi:solar-power", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, value=lambda value: round(value / 1000, 3), @@ -105,7 +100,6 @@ SENSOR_TYPES: tuple[SolarLogSensorEntityDescription, ...] = ( SolarLogSensorEntityDescription( key="yield_total", translation_key="yield_total", - icon="mdi:solar-power", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL, @@ -157,14 +151,12 @@ SENSOR_TYPES: tuple[SolarLogSensorEntityDescription, ...] = ( SolarLogSensorEntityDescription( key="total_power", translation_key="total_power", - icon="mdi:solar-power", native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, ), SolarLogSensorEntityDescription( key="alternator_loss", translation_key="alternator_loss", - icon="mdi:solar-power", native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, @@ -172,7 +164,6 @@ SENSOR_TYPES: tuple[SolarLogSensorEntityDescription, ...] = ( SolarLogSensorEntityDescription( key="capacity", translation_key="capacity", - icon="mdi:solar-power", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.POWER_FACTOR, state_class=SensorStateClass.MEASUREMENT, @@ -189,7 +180,6 @@ SENSOR_TYPES: tuple[SolarLogSensorEntityDescription, ...] = ( SolarLogSensorEntityDescription( key="power_available", translation_key="power_available", - icon="mdi:solar-power", native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, diff --git a/homeassistant/components/solax/__init__.py b/homeassistant/components/solax/__init__.py index 6ede5b5df02..b5e15043cec 100644 --- a/homeassistant/components/solax/__init__.py +++ b/homeassistant/components/solax/__init__.py @@ -1,4 +1,5 @@ """The solax component.""" + from solax import real_time_api from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/solax/config_flow.py b/homeassistant/components/solax/config_flow.py index 2334fd0def2..4055f1c46ae 100644 --- a/homeassistant/components/solax/config_flow.py +++ b/homeassistant/components/solax/config_flow.py @@ -1,4 +1,5 @@ """Config flow for solax integration.""" + from __future__ import annotations import logging @@ -8,9 +9,8 @@ from solax import real_time_api from solax.discovery import DiscoveryError import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD, CONF_PORT -from homeassistant.data_entry_flow import FlowResult import homeassistant.helpers.config_validation as cv from .const import DOMAIN @@ -39,12 +39,12 @@ async def validate_api(data) -> str: return response.serial_number -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class SolaxConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Solax.""" async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors: dict[str, Any] = {} if user_input is None: diff --git a/homeassistant/components/solax/const.py b/homeassistant/components/solax/const.py index 65894013adc..26d5962e385 100644 --- a/homeassistant/components/solax/const.py +++ b/homeassistant/components/solax/const.py @@ -1,6 +1,5 @@ """Constants for the solax integration.""" - DOMAIN = "solax" MANUFACTURER = "SolaX Power" diff --git a/homeassistant/components/solax/sensor.py b/homeassistant/components/solax/sensor.py index eee74c1007f..ccd1a8c96c9 100644 --- a/homeassistant/components/solax/sensor.py +++ b/homeassistant/components/solax/sensor.py @@ -1,4 +1,5 @@ """Support for Solax inverter via local API.""" + from __future__ import annotations import asyncio @@ -102,8 +103,12 @@ async def async_setup_entry( serial = resp.serial_number version = resp.version endpoint = RealTimeDataEndpoint(hass, api) - hass.async_add_job(endpoint.async_refresh) - async_track_time_interval(hass, endpoint.async_refresh, SCAN_INTERVAL) + entry.async_create_background_task( + hass, endpoint.async_refresh(), f"solax {entry.title} initial refresh" + ) + entry.async_on_unload( + async_track_time_interval(hass, endpoint.async_refresh, SCAN_INTERVAL) + ) devices = [] for sensor, (idx, measurement) in api.inverter.sensor_map().items(): description = SENSOR_DESCRIPTIONS[(measurement.unit, measurement.is_monotonic)] diff --git a/homeassistant/components/soma/__init__.py b/homeassistant/components/soma/__init__.py index bbcc29d7853..cd282a9f276 100644 --- a/homeassistant/components/soma/__init__.py +++ b/homeassistant/components/soma/__init__.py @@ -1,4 +1,5 @@ """Support for Soma Smartshades.""" + from __future__ import annotations from collections.abc import Callable, Coroutine diff --git a/homeassistant/components/soma/config_flow.py b/homeassistant/components/soma/config_flow.py index ca6f1fabf30..773a24d5b44 100644 --- a/homeassistant/components/soma/config_flow.py +++ b/homeassistant/components/soma/config_flow.py @@ -1,11 +1,12 @@ """Config flow for Soma.""" + import logging from api.soma_api import SomaApi from requests import RequestException import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_HOST, CONF_PORT from .const import DOMAIN @@ -15,7 +16,7 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_PORT = 3000 -class SomaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class SomaFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a config flow.""" VERSION = 1 diff --git a/homeassistant/components/soma/cover.py b/homeassistant/components/soma/cover.py index 4aa2559b140..a5d9507af4a 100644 --- a/homeassistant/components/soma/cover.py +++ b/homeassistant/components/soma/cover.py @@ -1,4 +1,5 @@ """Support for Soma Covers.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/soma/sensor.py b/homeassistant/components/soma/sensor.py index d1c0de188a0..4992ec5cde4 100644 --- a/homeassistant/components/soma/sensor.py +++ b/homeassistant/components/soma/sensor.py @@ -1,4 +1,5 @@ """Support for Soma sensors.""" + from datetime import timedelta from homeassistant.components.sensor import SensorDeviceClass, SensorEntity diff --git a/homeassistant/components/somfy_mylink/__init__.py b/homeassistant/components/somfy_mylink/__init__.py index 7883c88f0b8..ed9652de55a 100644 --- a/homeassistant/components/somfy_mylink/__init__.py +++ b/homeassistant/components/somfy_mylink/__init__.py @@ -1,4 +1,5 @@ """Component for the Somfy MyLink device supporting the Synergy API.""" + import logging from somfy_mylink_synergy import SomfyMyLinkSynergy diff --git a/homeassistant/components/somfy_mylink/config_flow.py b/homeassistant/components/somfy_mylink/config_flow.py index e42191c1230..6e68be45dff 100644 --- a/homeassistant/components/somfy_mylink/config_flow.py +++ b/homeassistant/components/somfy_mylink/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Somfy MyLink integration.""" + from __future__ import annotations from copy import deepcopy @@ -7,11 +8,17 @@ import logging from somfy_mylink_synergy import SomfyMyLinkSynergy import voluptuous as vol -from homeassistant import config_entries, core, exceptions from homeassistant.components import dhcp +from homeassistant.config_entries import ( + ConfigEntry, + ConfigEntryState, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.const import CONF_HOST, CONF_PORT -from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.device_registry import format_mac from .const import ( @@ -28,7 +35,7 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -async def validate_input(hass: core.HomeAssistant, data): +async def validate_input(hass: HomeAssistant, data): """Validate the user input allows us to connect. Data has the keys from schema with values provided by the user. @@ -49,7 +56,7 @@ async def validate_input(hass: core.HomeAssistant, data): return {"title": f"MyLink {data[CONF_HOST]}"} -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class SomfyConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Somfy MyLink.""" VERSION = 1 @@ -60,7 +67,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.mac = None self.ip_address = None - async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: + async def async_step_dhcp( + self, discovery_info: dhcp.DhcpServiceInfo + ) -> ConfigFlowResult: """Handle dhcp discovery.""" self._async_abort_entries_match({CONF_HOST: discovery_info.ip}) @@ -112,16 +121,16 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback def async_get_options_flow( - config_entry: config_entries.ConfigEntry, + config_entry: ConfigEntry, ) -> OptionsFlowHandler: """Get the options flow for this handler.""" return OptionsFlowHandler(config_entry) -class OptionsFlowHandler(config_entries.OptionsFlow): +class OptionsFlowHandler(OptionsFlow): """Handle a option flow for somfy_mylink.""" - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + def __init__(self, config_entry: ConfigEntry) -> None: """Initialize options flow.""" self.config_entry = config_entry self.options = deepcopy(dict(config_entry.options)) @@ -146,7 +155,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow): async def async_step_init(self, user_input=None): """Handle options flow.""" - if self.config_entry.state is not config_entries.ConfigEntryState.LOADED: + if self.config_entry.state is not ConfigEntryState.LOADED: _LOGGER.error("MyLink must be connected to manage device options") return self.async_abort(reason="cannot_connect") @@ -194,9 +203,9 @@ class OptionsFlowHandler(config_entries.OptionsFlow): ) -class CannotConnect(exceptions.HomeAssistantError): +class CannotConnect(HomeAssistantError): """Error to indicate we cannot connect.""" -class InvalidAuth(exceptions.HomeAssistantError): +class InvalidAuth(HomeAssistantError): """Error to indicate there is invalid auth.""" diff --git a/homeassistant/components/somfy_mylink/const.py b/homeassistant/components/somfy_mylink/const.py index bb9f2c5fd42..8669c73fb9b 100644 --- a/homeassistant/components/somfy_mylink/const.py +++ b/homeassistant/components/somfy_mylink/const.py @@ -1,4 +1,5 @@ """Component for the Somfy MyLink device supporting the Synergy API.""" + from homeassistant.const import Platform CONF_SYSTEM_ID = "system_id" diff --git a/homeassistant/components/somfy_mylink/cover.py b/homeassistant/components/somfy_mylink/cover.py index 38f5fdc12f8..577795d172b 100644 --- a/homeassistant/components/somfy_mylink/cover.py +++ b/homeassistant/components/somfy_mylink/cover.py @@ -1,4 +1,5 @@ """Cover Platform for the Somfy MyLink component.""" + import logging from typing import Any diff --git a/homeassistant/components/sonarr/__init__.py b/homeassistant/components/sonarr/__init__.py index 69d2ba76e22..89c247ebbfb 100644 --- a/homeassistant/components/sonarr/__init__.py +++ b/homeassistant/components/sonarr/__init__.py @@ -1,4 +1,5 @@ """The Sonarr component.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/sonarr/config_flow.py b/homeassistant/components/sonarr/config_flow.py index 3ea386faa78..9e84d040ad1 100644 --- a/homeassistant/components/sonarr/config_flow.py +++ b/homeassistant/components/sonarr/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Sonarr.""" + from __future__ import annotations from collections.abc import Mapping @@ -11,10 +12,14 @@ from aiopyarr.sonarr_client import SonarrClient import voluptuous as vol import yarl -from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.const import CONF_API_KEY, CONF_URL, CONF_VERIFY_SSL from homeassistant.core import HomeAssistant, callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import ( @@ -63,7 +68,9 @@ class SonarrConfigFlow(ConfigFlow, domain=DOMAIN): """Get the options flow for this handler.""" return SonarrOptionsFlowHandler(config_entry) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Handle configuration by re-auth.""" self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) @@ -71,7 +78,7 @@ class SonarrConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm reauth dialog.""" if user_input is None: assert self.entry is not None @@ -85,7 +92,7 @@ class SonarrConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initiated by the user.""" errors = {} @@ -122,7 +129,9 @@ class SonarrConfigFlow(ConfigFlow, domain=DOMAIN): errors=errors, ) - async def _async_reauth_update_entry(self, data: dict[str, Any]) -> FlowResult: + async def _async_reauth_update_entry( + self, data: dict[str, Any] + ) -> ConfigFlowResult: """Update existing config entry.""" assert self.entry is not None self.hass.config_entries.async_update_entry(self.entry, data=data) @@ -141,9 +150,9 @@ class SonarrConfigFlow(ConfigFlow, domain=DOMAIN): } if self.show_advanced_options: - data_schema[ - vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL) - ] = bool + data_schema[vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL)] = ( + bool + ) return data_schema @@ -157,7 +166,7 @@ class SonarrOptionsFlowHandler(OptionsFlow): async def async_step_init( self, user_input: dict[str, int] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Manage Sonarr options.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) diff --git a/homeassistant/components/sonarr/const.py b/homeassistant/components/sonarr/const.py index 5468953184a..7e703f02957 100644 --- a/homeassistant/components/sonarr/const.py +++ b/homeassistant/components/sonarr/const.py @@ -1,4 +1,5 @@ """Constants for Sonarr.""" + import logging DOMAIN = "sonarr" diff --git a/homeassistant/components/sonarr/coordinator.py b/homeassistant/components/sonarr/coordinator.py index 1010c196c21..2d807bcf140 100644 --- a/homeassistant/components/sonarr/coordinator.py +++ b/homeassistant/components/sonarr/coordinator.py @@ -1,4 +1,5 @@ """Data update coordinator for the Sonarr integration.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/sonarr/entity.py b/homeassistant/components/sonarr/entity.py index 6231ca3903a..7dc0d0ca147 100644 --- a/homeassistant/components/sonarr/entity.py +++ b/homeassistant/components/sonarr/entity.py @@ -1,4 +1,5 @@ """Base Entity for Sonarr.""" + from __future__ import annotations from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo diff --git a/homeassistant/components/sonarr/icons.json b/homeassistant/components/sonarr/icons.json new file mode 100644 index 00000000000..7980db52b29 --- /dev/null +++ b/homeassistant/components/sonarr/icons.json @@ -0,0 +1,24 @@ +{ + "entity": { + "sensor": { + "commands": { + "default": "mdi:code-braces" + }, + "diskspace": { + "default": "mdi:harddisk" + }, + "queue": { + "default": "mdi:download" + }, + "series": { + "default": "mdi:television" + }, + "upcoming": { + "default": "mdi:television" + }, + "wanted": { + "default": "mdi:television" + } + } + } +} diff --git a/homeassistant/components/sonarr/sensor.py b/homeassistant/components/sonarr/sensor.py index 5753d0d23ea..bdb647de39c 100644 --- a/homeassistant/components/sonarr/sensor.py +++ b/homeassistant/components/sonarr/sensor.py @@ -1,4 +1,5 @@ """Support for Sonarr sensors.""" + from __future__ import annotations from collections.abc import Callable @@ -53,9 +54,9 @@ def get_disk_space_attr(disks: list[Diskspace]) -> dict[str, str]: free = disk.freeSpace / 1024**3 total = disk.totalSpace / 1024**3 usage = free / total * 100 - attrs[ - disk.path - ] = f"{free:.2f}/{total:.2f}{UnitOfInformation.GIGABYTES} ({usage:.2f}%)" + attrs[disk.path] = ( + f"{free:.2f}/{total:.2f}{UnitOfInformation.GIGABYTES} ({usage:.2f}%)" + ) return attrs @@ -89,7 +90,6 @@ SENSOR_TYPES: dict[str, SonarrSensorEntityDescription[Any]] = { "commands": SonarrSensorEntityDescription[list[Command]]( key="commands", translation_key="commands", - icon="mdi:code-braces", native_unit_of_measurement="Commands", entity_registry_enabled_default=False, value_fn=len, @@ -98,7 +98,6 @@ SENSOR_TYPES: dict[str, SonarrSensorEntityDescription[Any]] = { "diskspace": SonarrSensorEntityDescription[list[Diskspace]]( key="diskspace", translation_key="diskspace", - icon="mdi:harddisk", native_unit_of_measurement=UnitOfInformation.GIGABYTES, device_class=SensorDeviceClass.DATA_SIZE, entity_registry_enabled_default=False, @@ -108,7 +107,6 @@ SENSOR_TYPES: dict[str, SonarrSensorEntityDescription[Any]] = { "queue": SonarrSensorEntityDescription[SonarrQueue]( key="queue", translation_key="queue", - icon="mdi:download", native_unit_of_measurement="Episodes", entity_registry_enabled_default=False, value_fn=lambda data: data.totalRecords, @@ -117,7 +115,6 @@ SENSOR_TYPES: dict[str, SonarrSensorEntityDescription[Any]] = { "series": SonarrSensorEntityDescription[list[SonarrSeries]]( key="series", translation_key="series", - icon="mdi:television", native_unit_of_measurement="Series", entity_registry_enabled_default=False, value_fn=len, @@ -131,7 +128,6 @@ SENSOR_TYPES: dict[str, SonarrSensorEntityDescription[Any]] = { "upcoming": SonarrSensorEntityDescription[list[SonarrCalendar]]( key="upcoming", translation_key="upcoming", - icon="mdi:television", native_unit_of_measurement="Episodes", value_fn=len, attributes_fn=lambda data: { @@ -141,7 +137,6 @@ SENSOR_TYPES: dict[str, SonarrSensorEntityDescription[Any]] = { "wanted": SonarrSensorEntityDescription[SonarrWantedMissing]( key="wanted", translation_key="wanted", - icon="mdi:television", native_unit_of_measurement="Episodes", entity_registry_enabled_default=False, value_fn=lambda data: data.totalRecords, diff --git a/homeassistant/components/songpal/__init__.py b/homeassistant/components/songpal/__init__.py index 8ab1cb18bdd..aa3f850c9e3 100644 --- a/homeassistant/components/songpal/__init__.py +++ b/homeassistant/components/songpal/__init__.py @@ -1,4 +1,5 @@ """The songpal component.""" + import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry diff --git a/homeassistant/components/songpal/config_flow.py b/homeassistant/components/songpal/config_flow.py index a1e7938f3f8..f8a0db3815d 100644 --- a/homeassistant/components/songpal/config_flow.py +++ b/homeassistant/components/songpal/config_flow.py @@ -1,4 +1,5 @@ """Config flow to configure songpal component.""" + from __future__ import annotations import logging @@ -7,10 +8,9 @@ from urllib.parse import urlparse from songpal import Device, SongpalException import voluptuous as vol -from homeassistant import config_entries from homeassistant.components import ssdp +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_NAME -from homeassistant.data_entry_flow import FlowResult from .const import CONF_ENDPOINT, DOMAIN @@ -27,7 +27,7 @@ class SongpalConfig: self.endpoint = endpoint -class SongpalConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class SongpalConfigFlow(ConfigFlow, domain=DOMAIN): """Songpal configuration flow.""" VERSION = 1 @@ -93,7 +93,9 @@ class SongpalConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): data={CONF_NAME: self.conf.name, CONF_ENDPOINT: self.conf.endpoint}, ) - async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: + async def async_step_ssdp( + self, discovery_info: ssdp.SsdpServiceInfo + ) -> ConfigFlowResult: """Handle a discovered Songpal device.""" await self.async_set_unique_id(discovery_info.upnp[ssdp.ATTR_UPNP_UDN]) self._abort_if_unique_id_configured() diff --git a/homeassistant/components/songpal/const.py b/homeassistant/components/songpal/const.py index 496618f35f0..04b005e2a09 100644 --- a/homeassistant/components/songpal/const.py +++ b/homeassistant/components/songpal/const.py @@ -1,4 +1,5 @@ """Constants for the Songpal component.""" + DOMAIN = "songpal" SET_SOUND_SETTING = "set_sound_setting" diff --git a/homeassistant/components/songpal/icons.json b/homeassistant/components/songpal/icons.json new file mode 100644 index 00000000000..1c831fbbd00 --- /dev/null +++ b/homeassistant/components/songpal/icons.json @@ -0,0 +1,5 @@ +{ + "services": { + "set_sound_setting": "mdi:volume-high" + } +} diff --git a/homeassistant/components/songpal/media_player.py b/homeassistant/components/songpal/media_player.py index 582e62a67eb..33dc65d5eaa 100644 --- a/homeassistant/components/songpal/media_player.py +++ b/homeassistant/components/songpal/media_player.py @@ -1,4 +1,5 @@ """Support for Songpal-enabled (Sony) media devices.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index 0df6a7422fe..028c412cd75 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -1,4 +1,5 @@ """Support to embed Sonos.""" + from __future__ import annotations import asyncio @@ -34,6 +35,7 @@ from homeassistant.helpers import ( from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_call_later, async_track_time_interval from homeassistant.helpers.typing import ConfigType +from homeassistant.util.async_ import create_eager_task from .alarms import SonosAlarms from .const import ( @@ -319,11 +321,15 @@ class SonosDiscoveryManager: zgs.total_requests, ) await asyncio.gather( - *(speaker.async_offline() for speaker in self.data.discovered.values()) + *( + create_eager_task(speaker.async_offline()) + for speaker in self.data.discovered.values() + ) ) if events_asyncio.event_listener: await events_asyncio.event_listener.async_stop() + @callback def _stop_manual_heartbeat(self, event: Event | None = None) -> None: if self.data.hosts_heartbeat: self.data.hosts_heartbeat() @@ -492,7 +498,8 @@ class SonosDiscoveryManager: self.hass, f"{SONOS_SPEAKER_ACTIVITY}-{uid}", source ) - async def _async_ssdp_discovered_player( + @callback + def _async_ssdp_discovered_player( self, info: ssdp.SsdpServiceInfo, change: ssdp.SsdpChange ) -> None: uid = info.upnp[ssdp.ATTR_UPNP_UDN] @@ -567,14 +574,18 @@ class SonosDiscoveryManager: 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 + EVENT_HOMEASSISTANT_STOP, + self._async_stop_event_listener, + run_immediately=True, ) ) _LOGGER.debug("Adding discovery job") if self.hosts: self.entry.async_on_unload( self.hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, self._stop_manual_heartbeat + EVENT_HOMEASSISTANT_STOP, + self._stop_manual_heartbeat, + run_immediately=True, ) ) await self.async_poll_manual_hosts() diff --git a/homeassistant/components/sonos/alarms.py b/homeassistant/components/sonos/alarms.py index e7cf05a1ff0..a18598fc545 100644 --- a/homeassistant/components/sonos/alarms.py +++ b/homeassistant/components/sonos/alarms.py @@ -1,4 +1,5 @@ """Class representing Sonos alarms.""" + from __future__ import annotations from collections.abc import Iterator diff --git a/homeassistant/components/sonos/binary_sensor.py b/homeassistant/components/sonos/binary_sensor.py index 4a41e572c1a..2c1e8af9961 100644 --- a/homeassistant/components/sonos/binary_sensor.py +++ b/homeassistant/components/sonos/binary_sensor.py @@ -1,4 +1,5 @@ """Entity representing a Sonos power sensor.""" + from __future__ import annotations import logging @@ -10,7 +11,7 @@ from homeassistant.components.binary_sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -31,12 +32,14 @@ async def async_setup_entry( ) -> None: """Set up Sonos from a config entry.""" - async def _async_create_battery_entity(speaker: SonosSpeaker) -> None: + @callback + def _async_create_battery_entity(speaker: SonosSpeaker) -> None: _LOGGER.debug("Creating battery binary_sensor on %s", speaker.zone_name) entity = SonosPowerEntity(speaker) async_add_entities([entity]) - async def _async_create_mic_entity(speaker: SonosSpeaker) -> None: + @callback + def _async_create_mic_entity(speaker: SonosSpeaker) -> None: _LOGGER.debug("Creating microphone binary_sensor on %s", speaker.zone_name) async_add_entities([SonosMicrophoneSensorEntity(speaker)]) @@ -90,7 +93,6 @@ class SonosMicrophoneSensorEntity(SonosEntity, BinarySensorEntity): """Representation of a Sonos microphone sensor entity.""" _attr_entity_category = EntityCategory.DIAGNOSTIC - _attr_icon = "mdi:microphone" _attr_translation_key = "microphone" def __init__(self, speaker: SonosSpeaker) -> None: diff --git a/homeassistant/components/sonos/config_flow.py b/homeassistant/components/sonos/config_flow.py index cc453f14691..a8ace6e35c5 100644 --- a/homeassistant/components/sonos/config_flow.py +++ b/homeassistant/components/sonos/config_flow.py @@ -1,10 +1,11 @@ """Config flow for SONOS.""" + from collections.abc import Awaitable import dataclasses from homeassistant.components import ssdp, zeroconf +from homeassistant.config_entries import ConfigFlowResult from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.config_entry_flow import DiscoveryFlowHandler from .const import DATA_SONOS_DISCOVERY_MANAGER, DOMAIN, UPNP_ST @@ -25,7 +26,7 @@ class SonosDiscoveryFlowHandler(DiscoveryFlowHandler[Awaitable[bool]], domain=DO async def async_step_zeroconf( self, discovery_info: zeroconf.ZeroconfServiceInfo - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by zeroconf.""" hostname = discovery_info.hostname if hostname is None or not hostname.lower().startswith("sonos"): diff --git a/homeassistant/components/sonos/const.py b/homeassistant/components/sonos/const.py index e42fb7d67c7..610a68afedf 100644 --- a/homeassistant/components/sonos/const.py +++ b/homeassistant/components/sonos/const.py @@ -1,4 +1,5 @@ """Const for Sonos.""" + from __future__ import annotations import datetime diff --git a/homeassistant/components/sonos/diagnostics.py b/homeassistant/components/sonos/diagnostics.py index 21e440673d6..b97b03b9be2 100644 --- a/homeassistant/components/sonos/diagnostics.py +++ b/homeassistant/components/sonos/diagnostics.py @@ -1,4 +1,5 @@ """Provides diagnostics for Sonos.""" + from __future__ import annotations import time diff --git a/homeassistant/components/sonos/entity.py b/homeassistant/components/sonos/entity.py index 05b69c54c50..bd7256493e8 100644 --- a/homeassistant/components/sonos/entity.py +++ b/homeassistant/components/sonos/entity.py @@ -1,4 +1,5 @@ """Entity representing a Sonos player.""" + from __future__ import annotations from abc import abstractmethod diff --git a/homeassistant/components/sonos/exception.py b/homeassistant/components/sonos/exception.py index 7ff5dacd293..6f7483f4188 100644 --- a/homeassistant/components/sonos/exception.py +++ b/homeassistant/components/sonos/exception.py @@ -1,4 +1,5 @@ """Sonos specific exceptions.""" + from homeassistant.components.media_player.errors import BrowseError from homeassistant.exceptions import HomeAssistantError diff --git a/homeassistant/components/sonos/favorites.py b/homeassistant/components/sonos/favorites.py index eeeb210b9ec..5050555a7cb 100644 --- a/homeassistant/components/sonos/favorites.py +++ b/homeassistant/components/sonos/favorites.py @@ -1,4 +1,5 @@ """Class representing Sonos favorites.""" + from __future__ import annotations from collections.abc import Iterator diff --git a/homeassistant/components/sonos/helpers.py b/homeassistant/components/sonos/helpers.py index 1005b6c7d6a..2070d37b1a4 100644 --- a/homeassistant/components/sonos/helpers.py +++ b/homeassistant/components/sonos/helpers.py @@ -1,4 +1,5 @@ """Helper methods for common tasks.""" + from __future__ import annotations from collections.abc import Callable @@ -38,15 +39,13 @@ _ReturnFuncType = Callable[Concatenate[_T, _P], _R | None] @overload def soco_error( errorcodes: None = ..., -) -> Callable[[_FuncType[_T, _P, _R]], _FuncType[_T, _P, _R]]: - ... +) -> Callable[[_FuncType[_T, _P, _R]], _FuncType[_T, _P, _R]]: ... @overload def soco_error( errorcodes: list[str], -) -> Callable[[_FuncType[_T, _P, _R]], _ReturnFuncType[_T, _P, _R]]: - ... +) -> Callable[[_FuncType[_T, _P, _R]], _ReturnFuncType[_T, _P, _R]]: ... def soco_error( diff --git a/homeassistant/components/sonos/household_coordinator.py b/homeassistant/components/sonos/household_coordinator.py index 29b9a005552..8fcecdf4d5e 100644 --- a/homeassistant/components/sonos/household_coordinator.py +++ b/homeassistant/components/sonos/household_coordinator.py @@ -1,4 +1,5 @@ """Class representing a Sonos household storage helper.""" + from __future__ import annotations import asyncio @@ -8,7 +9,7 @@ from typing import Any from soco import SoCo -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.debounce import Debouncer from .const import DATA_SONOS @@ -34,7 +35,8 @@ class SonosHouseholdCoordinator: self.update_cache(soco) self.hass.add_job(self._async_setup) - async def _async_setup(self) -> None: + @callback + def _async_setup(self) -> None: """Finish setup in async context.""" self.cache_update_lock = asyncio.Lock() self.async_poll = Debouncer[Coroutine[Any, Any, None]]( @@ -73,8 +75,8 @@ class SonosHouseholdCoordinator: self, soco: SoCo, update_id: int | None = None ) -> None: """Update the cache and update entities.""" - raise NotImplementedError() + raise NotImplementedError def update_cache(self, soco: SoCo, update_id: int | None = None) -> bool: """Update the cache of the household-level feature and return if cache has changed.""" - raise NotImplementedError() + raise NotImplementedError diff --git a/homeassistant/components/sonos/icons.json b/homeassistant/components/sonos/icons.json new file mode 100644 index 00000000000..e2545358ba6 --- /dev/null +++ b/homeassistant/components/sonos/icons.json @@ -0,0 +1,55 @@ +{ + "entity": { + "binary_sensor": { + "microphone": { + "default": "mdi:microphone" + } + }, + "sensor": { + "audio_input_format": { + "default": "mdi:import" + }, + "favorites": { + "default": "mdi:star" + } + }, + "switch": { + "loudness": { + "default": "mdi:bullhorn-variant" + }, + "surround_mode": { + "default": "mdi:music-note-plus" + }, + "night_mode": { + "default": "mdi:chat-sleep" + }, + "dialog_level": { + "default": "mdi:ear-hearing" + }, + "cross_fade": { + "default": "mdi:swap-horizontal" + }, + "status_light": { + "default": "mdi:led-on" + }, + "sub_enabled": { + "default": "mdi:dog" + }, + "surround_enabled": { + "default": "mdi:surround-sound" + }, + "buttons_enabled": { + "default": "mdi:gesture-tap" + } + } + }, + "services": { + "snapshot": "mdi:camera", + "restore": "mdi:camera-retake", + "set_sleep_timer": "mdi:alarm", + "clear_sleep_timer": "mdi:alarm-off", + "play_queue": "mdi:play", + "remove_from_queue": "mdi:playlist-remove", + "update_alarm": "mdi:alarm" + } +} diff --git a/homeassistant/components/sonos/manifest.json b/homeassistant/components/sonos/manifest.json index 929b6639e9f..58a0ec3b7ee 100644 --- a/homeassistant/components/sonos/manifest.json +++ b/homeassistant/components/sonos/manifest.json @@ -6,7 +6,6 @@ "config_flow": true, "dependencies": ["ssdp"], "documentation": "https://www.home-assistant.io/integrations/sonos", - "import_executor": true, "iot_class": "local_push", "loggers": ["soco"], "requirements": ["soco==0.30.2", "sonos-websocket==0.1.3"], diff --git a/homeassistant/components/sonos/media.py b/homeassistant/components/sonos/media.py index 0a402064fca..1f5432c440b 100644 --- a/homeassistant/components/sonos/media.py +++ b/homeassistant/components/sonos/media.py @@ -1,4 +1,5 @@ """Support for media metadata handling.""" + from __future__ import annotations import datetime diff --git a/homeassistant/components/sonos/media_browser.py b/homeassistant/components/sonos/media_browser.py index 1b32cb2473a..6e6f388ed50 100644 --- a/homeassistant/components/sonos/media_browser.py +++ b/homeassistant/components/sonos/media_browser.py @@ -1,4 +1,5 @@ """Support for media browsing.""" + from __future__ import annotations from collections.abc import Callable @@ -6,6 +7,7 @@ from contextlib import suppress from functools import partial import logging from typing import cast +import urllib.parse from soco.data_structures import DidlObject from soco.ms_data_structures import MusicServiceItem @@ -59,12 +61,14 @@ def get_thumbnail_url_full( media_content_id, media_content_type, ) - return getattr(item, "album_art_uri", None) + return urllib.parse.unquote(getattr(item, "album_art_uri", "")) - return get_browse_image_url( - media_content_type, - media_content_id, - media_image_id, + return urllib.parse.unquote( + get_browse_image_url( + media_content_type, + media_content_id, + media_image_id, + ) ) @@ -165,6 +169,7 @@ def build_item_response( payload["idstring"] = "A:ALBUMARTIST/" + "/".join( payload["idstring"].split("/")[2:] ) + payload["idstring"] = urllib.parse.unquote(payload["idstring"]) try: search_type = MEDIA_TYPES_TO_SONOS[payload["search_type"]] @@ -200,7 +205,7 @@ def build_item_response( if not title: try: - title = payload["idstring"].split("/")[1] + title = urllib.parse.unquote(payload["idstring"].split("/")[1]) except IndexError: title = LIBRARY_TITLES_MAPPING[payload["idstring"]] @@ -492,10 +497,24 @@ def get_media( """Fetch media/album.""" search_type = MEDIA_TYPES_TO_SONOS.get(search_type, search_type) + if search_type == "playlists": + # Format is S:TITLE or S:ITEM_ID + splits = item_id.split(":") + title = splits[1] if len(splits) > 1 else None + playlist = next( + ( + p + for p in media_library.get_playlists() + if (item_id == p.item_id or title == p.title) + ), + None, + ) + return playlist + if not item_id.startswith("A:ALBUM") and search_type == SONOS_ALBUM: item_id = "A:ALBUMARTIST/" + "/".join(item_id.split("/")[2:]) - search_term = item_id.split("/")[-1] + search_term = urllib.parse.unquote(item_id.split("/")[-1]) matches = media_library.get_music_library_information( search_type, search_term=search_term, full_album_art_uri=True ) diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 27059bba180..581bdaad37d 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -1,4 +1,5 @@ """Support to interface with Sonos players.""" + from __future__ import annotations import datetime @@ -625,13 +626,13 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): soco.play_uri(media_id, force_radio=is_radio) elif media_type == MediaType.PLAYLIST: if media_id.startswith("S:"): - item = media_browser.get_media(self.media.library, media_id, media_type) - soco.play_uri(item.get_uri()) - return - try: + playlist = media_browser.get_media( + self.media.library, media_id, media_type + ) + else: playlists = soco.get_sonos_playlists(complete_result=True) - playlist = next(p for p in playlists if p.title == media_id) - except StopIteration: + playlist = next((p for p in playlists if p.title == media_id), None) + if not playlist: _LOGGER.error('Could not find a Sonos playlist named "%s"', media_id) else: soco.clear_queue() diff --git a/homeassistant/components/sonos/number.py b/homeassistant/components/sonos/number.py index c74c5933ecf..f9e9fc8bee0 100644 --- a/homeassistant/components/sonos/number.py +++ b/homeassistant/components/sonos/number.py @@ -1,4 +1,5 @@ """Entity representing a Sonos number control.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/sonos/sensor.py b/homeassistant/components/sonos/sensor.py index ca3cc89d750..a089c09b33c 100644 --- a/homeassistant/components/sonos/sensor.py +++ b/homeassistant/components/sonos/sensor.py @@ -1,4 +1,5 @@ """Entity representing a Sonos battery level.""" + from __future__ import annotations import logging @@ -105,7 +106,6 @@ class SonosAudioInputFormatSensorEntity(SonosPollingEntity, SensorEntity): """Representation of a Sonos audio import format sensor entity.""" _attr_entity_category = EntityCategory.DIAGNOSTIC - _attr_icon = "mdi:import" _attr_translation_key = "audio_input_format" _attr_should_poll = True @@ -134,8 +134,8 @@ class SonosFavoritesEntity(SensorEntity): """Representation of a Sonos favorites info entity.""" _attr_entity_registry_enabled_default = False - _attr_icon = "mdi:star" _attr_name = "Sonos favorites" + _attr_translation_key = "favorites" _attr_native_unit_of_measurement = "items" _attr_should_poll = False diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 3c9e4692fdc..ebb2738c641 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -1,4 +1,5 @@ """Base class for common speaker tasks.""" + from __future__ import annotations import asyncio @@ -8,7 +9,7 @@ import datetime from functools import partial import logging import time -from typing import Any, cast +from typing import TYPE_CHECKING, Any, cast import defusedxml.ElementTree as ET from soco.core import SoCo @@ -30,7 +31,7 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_send, dispatcher_send, ) -from homeassistant.helpers.event import async_track_time_interval, track_time_interval +from homeassistant.helpers.event import async_track_time_interval from homeassistant.util import dt as dt_util from .alarms import SonosAlarms @@ -64,6 +65,9 @@ from .helpers import soco_error from .media import SonosMedia from .statistics import ActivityStatistics, EventStatistics +if TYPE_CHECKING: + from . import SonosData + NEVER_TIME = -1200.0 RESUB_COOLDOWN_SECONDS = 10.0 EVENT_CHARGING = { @@ -97,6 +101,7 @@ class SonosSpeaker: ) -> None: """Initialize a SonosSpeaker.""" self.hass = hass + self.data: SonosData = hass.data[DATA_SONOS] self.soco = soco self.websocket: SonosWebsocket | None = None self.household_id: str = soco.household_id @@ -122,7 +127,6 @@ class SonosSpeaker: zone_group_state_sub.callback = self.async_dispatch_event self._subscriptions.append(zone_group_state_sub) self._subscription_lock: asyncio.Lock | None = None - self._event_dispatchers: dict[str, Callable] = {} self._last_activity: float = NEVER_TIME self._last_event_cache: dict[str, Any] = {} self.activity_stats: ActivityStatistics = ActivityStatistics(self.zone_name) @@ -174,16 +178,25 @@ class SonosSpeaker: self.snapshot_group: list[SonosSpeaker] = [] self._group_members_missing: set[str] = set() - async def async_setup(self, entry: ConfigEntry) -> None: + async def async_setup( + self, entry: ConfigEntry, has_battery: bool, dispatches: list[tuple[Any, ...]] + ) -> None: """Complete setup in async context.""" + # Battery events can be infrequent, polling is still necessary + if has_battery: + self._battery_poll_timer = async_track_time_interval( + self.hass, self.async_poll_battery, BATTERY_SCAN_INTERVAL + ) + self.websocket = SonosWebsocket( self.soco.ip_address, player_id=self.soco.uid, session=async_get_clientsession(self.hass), ) + dispatch_pairs: tuple[tuple[str, Callable[..., Any]], ...] = ( (SONOS_CHECK_ACTIVITY, self.async_check_activity), - (SONOS_SPEAKER_ADDED, self.update_group_for_uid), + (SONOS_SPEAKER_ADDED, self.async_update_group_for_uid), (f"{SONOS_REBOOTED}-{self.soco.uid}", self.async_rebooted), (f"{SONOS_SPEAKER_ACTIVITY}-{self.soco.uid}", self.speaker_activity), (f"{SONOS_VANISHED}-{self.soco.uid}", self.async_vanished), @@ -198,6 +211,11 @@ class SonosSpeaker: ) ) + for dispatch in dispatches: + async_dispatcher_send(self.hass, *dispatch) + + await self.async_subscribe() + def setup(self, entry: ConfigEntry) -> None: """Run initial setup of the speaker.""" self.media.play_mode = self.soco.play_mode @@ -206,53 +224,34 @@ class SonosSpeaker: if self.is_coordinator: self.media.poll_media() - future = asyncio.run_coroutine_threadsafe( - self.async_setup(entry), self.hass.loop - ) - future.result(timeout=10) - - dispatcher_send(self.hass, SONOS_CREATE_LEVELS, self) + dispatches: list[tuple[Any, ...]] = [(SONOS_CREATE_LEVELS, self)] if audio_format := self.soco.soundbar_audio_input_format: - dispatcher_send( - self.hass, SONOS_CREATE_AUDIO_FORMAT_SENSOR, self, audio_format - ) + dispatches.append((SONOS_CREATE_AUDIO_FORMAT_SENSOR, self, audio_format)) + has_battery = False try: self.battery_info = self.fetch_battery_info() except SonosUpdateError: _LOGGER.debug("No battery available for %s", self.zone_name) else: - # Battery events can be infrequent, polling is still necessary - self._battery_poll_timer = track_time_interval( - self.hass, self.async_poll_battery, BATTERY_SCAN_INTERVAL - ) + has_battery = True dispatcher_send(self.hass, SONOS_CREATE_BATTERY, self) if (mic_enabled := self.soco.mic_enabled) is not None: self.mic_enabled = mic_enabled - dispatcher_send(self.hass, SONOS_CREATE_MIC_SENSOR, self) + dispatches.append((SONOS_CREATE_MIC_SENSOR, self)) if new_alarms := [ alarm.alarm_id for alarm in self.alarms if alarm.zone.uid == self.soco.uid ]: - dispatcher_send(self.hass, SONOS_CREATE_ALARM, self, new_alarms) + dispatches.append((SONOS_CREATE_ALARM, self, new_alarms)) - dispatcher_send(self.hass, SONOS_CREATE_SWITCHES, self) + dispatches.append((SONOS_CREATE_SWITCHES, self)) + dispatches.append((SONOS_CREATE_MEDIA_PLAYER, self)) + dispatches.append((SONOS_SPEAKER_ADDED, self.soco.uid)) - self._event_dispatchers = { - "AlarmClock": self.async_dispatch_alarms, - "AVTransport": self.async_dispatch_media_update, - "ContentDirectory": self.async_dispatch_favorites, - "DeviceProperties": self.async_dispatch_device_properties, - "RenderingControl": self.async_update_volume, - "ZoneGroupTopology": self.async_update_groups, - } - - dispatcher_send(self.hass, SONOS_CREATE_MEDIA_PLAYER, self) - dispatcher_send(self.hass, SONOS_SPEAKER_ADDED, self.soco.uid) - - self.hass.create_task(self.async_subscribe()) + self.hass.create_task(self.async_setup(entry, has_battery, dispatches)) # # Entity management @@ -272,12 +271,12 @@ class SonosSpeaker: @property def alarms(self) -> SonosAlarms: """Return the SonosAlarms instance for this household.""" - return self.hass.data[DATA_SONOS].alarms[self.household_id] + return self.data.alarms[self.household_id] @property def favorites(self) -> SonosFavorites: """Return the SonosFavorites instance for this household.""" - return self.hass.data[DATA_SONOS].favorites[self.household_id] + return self.data.favorites[self.household_id] @property def is_coordinator(self) -> bool: @@ -443,7 +442,7 @@ class SonosSpeaker: # Save most recently processed event variables for cache and diagnostics self._last_event_cache[event.service.service_type] = event.variables dispatcher = self._event_dispatchers[event.service.service_type] - dispatcher(event) + dispatcher(self, event) @callback def async_dispatch_alarms(self, event: SonosEvent) -> None: @@ -456,7 +455,9 @@ class SonosSpeaker: def async_dispatch_device_properties(self, event: SonosEvent) -> None: """Update device properties from an event.""" self.event_stats.process(event) - self.hass.async_create_task(self.async_update_device_properties(event)) + self.hass.async_create_task( + self.async_update_device_properties(event), eager_start=True + ) async def async_update_device_properties(self, event: SonosEvent) -> None: """Update device properties from an event.""" @@ -494,9 +495,7 @@ class SonosSpeaker: "x-rincon:" ): new_coordinator_uid = av_transport_uri.split(":")[-1] - if new_coordinator_speaker := self.hass.data[DATA_SONOS].discovered.get( - new_coordinator_uid - ): + if new_coordinator_speaker := self.data.discovered.get(new_coordinator_uid): _LOGGER.debug( "Media update coordinator (%s) received for %s", new_coordinator_speaker.zone_name, @@ -655,7 +654,7 @@ class SonosSpeaker: await self.async_unsubscribe() - self.hass.data[DATA_SONOS].discovery_known.discard(self.soco.uid) + self.data.discovery_known.discard(self.soco.uid) async def async_vanished(self, reason: str) -> None: """Handle removal of speaker when marked as vanished.""" @@ -782,15 +781,16 @@ class SonosSpeaker: """Update group topology when polling.""" self.hass.add_job(self.create_update_groups_coro()) - def update_group_for_uid(self, uid: str) -> None: + @callback + def async_update_group_for_uid(self, uid: str) -> None: """Update group topology if uid is missing.""" if uid not in self._group_members_missing: return - missing_zone = self.hass.data[DATA_SONOS].discovered[uid].zone_name + missing_zone = self.data.discovered[uid].zone_name _LOGGER.debug( "%s was missing, adding to %s group", missing_zone, self.zone_name ) - self.update_groups() + self.hass.async_create_task(self.create_update_groups_coro(), eager_start=True) @callback def async_update_groups(self, event: SonosEvent) -> None: @@ -837,7 +837,7 @@ class SonosSpeaker: if p.uid != coordinator_uid and p.is_visible ] - return [coordinator_uid] + joined_uids + return [coordinator_uid, *joined_uids] async def _async_extract_group(event: SonosEvent | None) -> list[str]: """Extract group layout from a topology event.""" @@ -864,7 +864,7 @@ class SonosSpeaker: sonos_group_entities = [] for uid in group: - speaker = self.hass.data[DATA_SONOS].discovered.get(uid) + speaker = self.data.discovered.get(uid) if speaker: self._group_members_missing.discard(uid) sonos_group.append(speaker) @@ -892,10 +892,7 @@ class SonosSpeaker: self.async_write_entity_states() for joined_uid in group[1:]: - joined_speaker: SonosSpeaker = self.hass.data[ - DATA_SONOS - ].discovered.get(joined_uid) - if joined_speaker: + if joined_speaker := self.data.discovered.get(joined_uid): joined_speaker.coordinator = self joined_speaker.sonos_group = sonos_group joined_speaker.sonos_group_entities = sonos_group_entities @@ -906,13 +903,13 @@ class SonosSpeaker: async def _async_handle_group_event(event: SonosEvent | None) -> None: """Get async lock and handle event.""" - async with self.hass.data[DATA_SONOS].topology_condition: + async with self.data.topology_condition: group = await _async_extract_group(event) if self.soco.uid == group[0]: _async_regroup(group) - self.hass.data[DATA_SONOS].topology_condition.notify_all() + self.data.topology_condition.notify_all() return _async_handle_group_event(event) @@ -1142,3 +1139,12 @@ class SonosSpeaker: """Update information about current volume settings.""" self.volume = self.soco.volume self.muted = self.soco.mute + + _event_dispatchers = { + "AlarmClock": async_dispatch_alarms, + "AVTransport": async_dispatch_media_update, + "ContentDirectory": async_dispatch_favorites, + "DeviceProperties": async_dispatch_device_properties, + "RenderingControl": async_update_volume, + "ZoneGroupTopology": async_update_groups, + } diff --git a/homeassistant/components/sonos/statistics.py b/homeassistant/components/sonos/statistics.py index b761469aea5..ec3486d47e7 100644 --- a/homeassistant/components/sonos/statistics.py +++ b/homeassistant/components/sonos/statistics.py @@ -1,4 +1,5 @@ """Class to track subscription event statistics.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/sonos/switch.py b/homeassistant/components/sonos/switch.py index c551d4a00d3..4bf5487b1a6 100644 --- a/homeassistant/components/sonos/switch.py +++ b/homeassistant/components/sonos/switch.py @@ -1,4 +1,5 @@ """Entity representing a Sonos Alarm.""" + from __future__ import annotations import datetime @@ -10,7 +11,7 @@ from soco.exceptions import SoCoSlaveException, SoCoUPnPException from homeassistant.components.switch import ENTITY_ID_FORMAT, SwitchEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_TIME, EntityCategory, Platform +from homeassistant.const import ATTR_TIME, EntityCategory from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -67,18 +68,6 @@ POLL_REQUIRED = ( ATTR_STATUS_LIGHT, ) -FEATURE_ICONS = { - ATTR_LOUDNESS: "mdi:bullhorn-variant", - ATTR_MUSIC_PLAYBACK_FULL_VOLUME: "mdi:music-note-plus", - ATTR_NIGHT_SOUND: "mdi:chat-sleep", - ATTR_SPEECH_ENHANCEMENT: "mdi:ear-hearing", - ATTR_CROSSFADE: "mdi:swap-horizontal", - ATTR_STATUS_LIGHT: "mdi:led-on", - ATTR_SUB_ENABLED: "mdi:dog", - ATTR_SURROUND_ENABLED: "mdi:surround-sound", - ATTR_TOUCH_CONTROLS: "mdi:gesture-tap", -} - WEEKEND_DAYS = (0, 6) @@ -90,9 +79,6 @@ async def async_setup_entry( """Set up Sonos from a config entry.""" async def _async_create_alarms(speaker: SonosSpeaker, alarm_ids: list[str]) -> None: - async_migrate_alarm_unique_ids( - hass, config_entry, speaker.household_id, alarm_ids - ) entities = [] created_alarms = ( hass.data[DATA_SONOS].alarms[speaker.household_id].created_alarm_ids @@ -122,10 +108,6 @@ async def async_setup_entry( available_soco_attributes, speaker ) for feature_type in available_features: - if feature_type == ATTR_SPEECH_ENHANCEMENT: - async_migrate_speech_enhancement_entity_unique_id( - hass, config_entry, speaker - ) _LOGGER.debug( "Creating %s switch on %s", feature_type, @@ -153,7 +135,6 @@ class SonosSwitchEntity(SonosPollingEntity, SwitchEntity): self._attr_entity_category = EntityCategory.CONFIG self._attr_translation_key = feature_type self._attr_unique_id = f"{speaker.soco.uid}-{feature_type}" - self._attr_icon = FEATURE_ICONS.get(feature_type) if feature_type in POLL_REQUIRED: self._attr_entity_registry_enabled_default = False @@ -353,88 +334,3 @@ class SonosAlarmEntity(SonosEntity, SwitchEntity): """Handle turn on/off of alarm switch.""" self.alarm.enabled = turn_on self.alarm.save() - - -@callback -def async_migrate_alarm_unique_ids( - hass: HomeAssistant, - config_entry: ConfigEntry, - household_id: str, - alarm_ids: list[str], -) -> None: - """Migrate alarm switch unique_ids in the entity registry to the new format.""" - entity_registry = er.async_get(hass) - registry_entries = er.async_entries_for_config_entry( - entity_registry, config_entry.entry_id - ) - - alarm_entries = [ - (entry.unique_id, entry) - for entry in registry_entries - if entry.domain == Platform.SWITCH and entry.original_icon == "mdi:alarm" - ] - - for old_unique_id, alarm_entry in alarm_entries: - if ":" in old_unique_id: - continue - - entry_alarm_id = old_unique_id.split("-")[-1] - if entry_alarm_id in alarm_ids: - new_unique_id = f"alarm-{household_id}:{entry_alarm_id}" - _LOGGER.debug( - "Migrating unique_id for %s from %s to %s", - alarm_entry.entity_id, - old_unique_id, - new_unique_id, - ) - entity_registry.async_update_entity( - alarm_entry.entity_id, new_unique_id=new_unique_id - ) - - -@callback -def async_migrate_speech_enhancement_entity_unique_id( - hass: HomeAssistant, - config_entry: ConfigEntry, - speaker: SonosSpeaker, -) -> None: - """Migrate Speech Enhancement switch entity unique_id.""" - entity_registry = er.async_get(hass) - registry_entries = er.async_entries_for_config_entry( - entity_registry, config_entry.entry_id - ) - - speech_enhancement_entries = [ - entry - for entry in registry_entries - if entry.domain == Platform.SWITCH - and entry.original_icon == FEATURE_ICONS[ATTR_SPEECH_ENHANCEMENT] - and entry.unique_id.startswith(speaker.soco.uid) - ] - - if len(speech_enhancement_entries) > 1: - _LOGGER.warning( - ( - "Migration of Speech Enhancement switches on %s failed," - " manual cleanup required: %s" - ), - speaker.zone_name, - [e.entity_id for e in speech_enhancement_entries], - ) - return - - if len(speech_enhancement_entries) == 1: - old_entry = speech_enhancement_entries[0] - if old_entry.unique_id.endswith("dialog_level"): - return - - new_unique_id = f"{speaker.soco.uid}-{ATTR_SPEECH_ENHANCEMENT}" - _LOGGER.debug( - "Migrating unique_id for %s from %s to %s", - old_entry.entity_id, - old_entry.unique_id, - new_unique_id, - ) - entity_registry.async_update_entity( - old_entry.entity_id, new_unique_id=new_unique_id - ) diff --git a/homeassistant/components/sony_projector/switch.py b/homeassistant/components/sony_projector/switch.py index 259ab8b154c..7ecff46d3bd 100644 --- a/homeassistant/components/sony_projector/switch.py +++ b/homeassistant/components/sony_projector/switch.py @@ -1,4 +1,5 @@ """Support for Sony projectors via SDCP network control.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/soundtouch/__init__.py b/homeassistant/components/soundtouch/__init__.py index b021604af4a..c35c1e6f9c3 100644 --- a/homeassistant/components/soundtouch/__init__.py +++ b/homeassistant/components/soundtouch/__init__.py @@ -1,4 +1,5 @@ """The soundtouch component.""" + import logging from libsoundtouch import soundtouch_device diff --git a/homeassistant/components/soundtouch/config_flow.py b/homeassistant/components/soundtouch/config_flow.py index 8f9b993d0d8..c8e8ce945db 100644 --- a/homeassistant/components/soundtouch/config_flow.py +++ b/homeassistant/components/soundtouch/config_flow.py @@ -1,14 +1,14 @@ """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.components.zeroconf import ZeroconfServiceInfo +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_validation as cv from .const import DOMAIN @@ -16,7 +16,7 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -class SoundtouchConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class SoundtouchConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Bose SoundTouch.""" VERSION = 1 @@ -53,7 +53,7 @@ class SoundtouchConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_zeroconf( self, discovery_info: ZeroconfServiceInfo - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initiated by a zeroconf discovery.""" self.host = discovery_info.host diff --git a/homeassistant/components/soundtouch/const.py b/homeassistant/components/soundtouch/const.py index a6b2b3c9f5f..ef9d7e71bf9 100644 --- a/homeassistant/components/soundtouch/const.py +++ b/homeassistant/components/soundtouch/const.py @@ -1,4 +1,5 @@ """Constants for the Bose SoundTouch component.""" + DOMAIN = "soundtouch" SERVICE_PLAY_EVERYWHERE = "play_everywhere" SERVICE_CREATE_ZONE = "create_zone" diff --git a/homeassistant/components/soundtouch/icons.json b/homeassistant/components/soundtouch/icons.json new file mode 100644 index 00000000000..0dd41f4f881 --- /dev/null +++ b/homeassistant/components/soundtouch/icons.json @@ -0,0 +1,8 @@ +{ + "services": { + "play_everywhere": "mdi:play", + "create_zone": "mdi:plus", + "add_zone_slave": "mdi:plus", + "remove_zone_slave": "mdi:minus" + } +} diff --git a/homeassistant/components/soundtouch/media_player.py b/homeassistant/components/soundtouch/media_player.py index 831b64f7056..0843cc1a826 100644 --- a/homeassistant/components/soundtouch/media_player.py +++ b/homeassistant/components/soundtouch/media_player.py @@ -1,4 +1,5 @@ """Support for interface with a Bose SoundTouch.""" + from __future__ import annotations from functools import partial diff --git a/homeassistant/components/spaceapi/__init__.py b/homeassistant/components/spaceapi/__init__.py index c18f150a925..93d448bd17f 100644 --- a/homeassistant/components/spaceapi/__init__.py +++ b/homeassistant/components/spaceapi/__init__.py @@ -1,9 +1,10 @@ """Support for the SpaceAPI.""" + from contextlib import suppress import voluptuous as vol -from homeassistant.components.http import HomeAssistantView +from homeassistant.components.http import KEY_HASS, HomeAssistantView from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_ICON, @@ -36,7 +37,7 @@ ATTR_RADIO_SHOW = "radio_show" ATTR_LAT = "lat" ATTR_LON = "lon" ATTR_API = "api" -ATTR_CLOSE = "close" +ATTR_CLOSED = "closed" ATTR_CONTACT = "contact" ATTR_ISSUE_REPORT_CHANNELS = "issue_report_channels" ATTR_LASTCHANGE = "lastchange" @@ -266,7 +267,7 @@ class APISpaceApiView(HomeAssistantView): @ha.callback def get(self, request): """Get SpaceAPI data.""" - hass = request.app["hass"] + hass = request.app[KEY_HASS] spaceapi = dict(hass.data[DATA_SPACEAPI]) is_sensors = spaceapi.get("sensors") @@ -292,7 +293,7 @@ class APISpaceApiView(HomeAssistantView): with suppress(KeyError): state[ATTR_ICON] = { ATTR_OPEN: spaceapi["state"][CONF_ICON_OPEN], - ATTR_CLOSE: spaceapi["state"][CONF_ICON_CLOSED], + ATTR_CLOSED: spaceapi["state"][CONF_ICON_CLOSED], } data = { diff --git a/homeassistant/components/spc/__init__.py b/homeassistant/components/spc/__init__.py index 49dc7b13fca..bb025d699fc 100644 --- a/homeassistant/components/spc/__init__.py +++ b/homeassistant/components/spc/__init__.py @@ -1,4 +1,5 @@ """Support for Vanderbilt (formerly Siemens) SPC alarm systems.""" + import logging from pyspcwebgw import SpcWebGateway diff --git a/homeassistant/components/spc/alarm_control_panel.py b/homeassistant/components/spc/alarm_control_panel.py index ace352b2ba0..d7f783b550d 100644 --- a/homeassistant/components/spc/alarm_control_panel.py +++ b/homeassistant/components/spc/alarm_control_panel.py @@ -1,4 +1,5 @@ """Support for Vanderbilt (formerly Siemens) SPC alarm systems.""" + from __future__ import annotations from pyspcwebgw import SpcWebGateway diff --git a/homeassistant/components/spc/binary_sensor.py b/homeassistant/components/spc/binary_sensor.py index a43551567e6..c79fa1f0c09 100644 --- a/homeassistant/components/spc/binary_sensor.py +++ b/homeassistant/components/spc/binary_sensor.py @@ -1,4 +1,5 @@ """Support for Vanderbilt (formerly Siemens) SPC alarm systems.""" + from __future__ import annotations from pyspcwebgw import SpcWebGateway diff --git a/homeassistant/components/speedtestdotnet/__init__.py b/homeassistant/components/speedtestdotnet/__init__.py index 1fb368b13c7..3c15f2fb820 100644 --- a/homeassistant/components/speedtestdotnet/__init__.py +++ b/homeassistant/components/speedtestdotnet/__init__.py @@ -1,4 +1,5 @@ """Support for testing internet speed via Speedtest.net.""" + from __future__ import annotations from functools import partial @@ -6,9 +7,10 @@ from functools import partial import speedtest from homeassistant.config_entries import ConfigEntry -from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, Platform -from homeassistant.core import CoreState, Event, HomeAssistant +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.start import async_at_started from .const import DOMAIN from .coordinator import SpeedTestDataCoordinator @@ -23,25 +25,20 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b partial(speedtest.Speedtest, secure=True) ) coordinator = SpeedTestDataCoordinator(hass, config_entry, api) - await hass.async_add_executor_job(coordinator.update_servers) except speedtest.SpeedtestException as err: raise ConfigEntryNotReady from err - async def _request_refresh(event: Event) -> None: - """Request a refresh.""" - await coordinator.async_request_refresh() - - if hass.state is CoreState.running: - await coordinator.async_config_entry_first_refresh() - else: - # Running a speed test during startup can prevent - # integrations from being able to setup because it - # can saturate the network interface. - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, _request_refresh) - hass.data[DOMAIN] = coordinator + async def _async_finish_startup(hass: HomeAssistant) -> None: + """Run this only when HA has finished its startup.""" + await coordinator.async_config_entry_first_refresh() + + # Don't start a speedtest during startup + async_at_started(hass, _async_finish_startup) + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) + config_entry.async_on_unload(config_entry.add_update_listener(update_listener)) return True @@ -53,3 +50,8 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> ): hass.data.pop(DOMAIN) return unload_ok + + +async def update_listener(hass: HomeAssistant, config_entry: ConfigEntry) -> None: + """Handle options update.""" + await hass.config_entries.async_reload(config_entry.entry_id) diff --git a/homeassistant/components/speedtestdotnet/config_flow.py b/homeassistant/components/speedtestdotnet/config_flow.py index 58ef53b2c9c..2ef2a70d745 100644 --- a/homeassistant/components/speedtestdotnet/config_flow.py +++ b/homeassistant/components/speedtestdotnet/config_flow.py @@ -1,13 +1,18 @@ """Config flow for Speedtest.net.""" + from __future__ import annotations from typing import Any import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult from .const import ( CONF_SERVER_ID, @@ -18,7 +23,7 @@ from .const import ( ) -class SpeedTestFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class SpeedTestFlowHandler(ConfigFlow, domain=DOMAIN): """Handle Speedtest.net config flow.""" VERSION = 1 @@ -26,14 +31,14 @@ class SpeedTestFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback def async_get_options_flow( - config_entry: config_entries.ConfigEntry, + config_entry: ConfigEntry, ) -> SpeedTestOptionsFlowHandler: """Get the options flow for this handler.""" return SpeedTestOptionsFlowHandler(config_entry) async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") @@ -44,17 +49,17 @@ class SpeedTestFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_create_entry(title=DEFAULT_NAME, data=user_input) -class SpeedTestOptionsFlowHandler(config_entries.OptionsFlow): +class SpeedTestOptionsFlowHandler(OptionsFlow): """Handle SpeedTest options.""" - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + def __init__(self, config_entry: ConfigEntry) -> None: """Initialize options flow.""" self.config_entry = config_entry self._servers: dict = {} async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Manage the options.""" errors: dict[str, str] = {} diff --git a/homeassistant/components/speedtestdotnet/const.py b/homeassistant/components/speedtestdotnet/const.py index 0d2625bfe33..2002d46c838 100644 --- a/homeassistant/components/speedtestdotnet/const.py +++ b/homeassistant/components/speedtestdotnet/const.py @@ -1,4 +1,5 @@ """Constants used by Speedtest.net.""" + from __future__ import annotations from typing import Final @@ -20,5 +21,3 @@ DEFAULT_SCAN_INTERVAL: Final = 60 DEFAULT_SERVER: Final = "*Auto Detect" ATTRIBUTION: Final = "Data retrieved from Speedtest.net by Ookla" - -ICON: Final = "mdi:speedometer" diff --git a/homeassistant/components/speedtestdotnet/coordinator.py b/homeassistant/components/speedtestdotnet/coordinator.py index e07bb94342d..299652ba0bd 100644 --- a/homeassistant/components/speedtestdotnet/coordinator.py +++ b/homeassistant/components/speedtestdotnet/coordinator.py @@ -38,10 +38,9 @@ class SpeedTestDataCoordinator(DataUpdateCoordinator[dict[str, Any]]): def update_servers(self) -> None: """Update list of test servers.""" test_servers = self.api.get_servers() - test_servers_list = [] - for servers in test_servers.values(): - for server in servers: - test_servers_list.append(server) + test_servers_list = [ + server for servers in test_servers.values() for server in servers + ] for server in sorted( test_servers_list, key=lambda server: ( diff --git a/homeassistant/components/speedtestdotnet/icons.json b/homeassistant/components/speedtestdotnet/icons.json new file mode 100644 index 00000000000..9f3beebdf21 --- /dev/null +++ b/homeassistant/components/speedtestdotnet/icons.json @@ -0,0 +1,15 @@ +{ + "entity": { + "sensor": { + "ping": { + "default": "mdi:speedometer" + }, + "download": { + "default": "mdi:speedometer" + }, + "upload": { + "default": "mdi:speedometer" + } + } + } +} diff --git a/homeassistant/components/speedtestdotnet/sensor.py b/homeassistant/components/speedtestdotnet/sensor.py index 53e80be0cc0..5bf1a6bea91 100644 --- a/homeassistant/components/speedtestdotnet/sensor.py +++ b/homeassistant/components/speedtestdotnet/sensor.py @@ -1,4 +1,5 @@ """Support for Speedtest.net internet speed testing sensor.""" + from __future__ import annotations from collections.abc import Callable @@ -28,7 +29,6 @@ from .const import ( ATTRIBUTION, DEFAULT_NAME, DOMAIN, - ICON, ) from .coordinator import SpeedTestDataCoordinator @@ -86,7 +86,6 @@ class SpeedtestSensor(CoordinatorEntity[SpeedTestDataCoordinator], SensorEntity) entity_description: SpeedtestSensorEntityDescription _attr_attribution = ATTRIBUTION _attr_has_entity_name = True - _attr_icon = ICON def __init__( self, diff --git a/homeassistant/components/spider/__init__.py b/homeassistant/components/spider/__init__.py index d4122a91d62..782486de2d8 100644 --- a/homeassistant/components/spider/__init__.py +++ b/homeassistant/components/spider/__init__.py @@ -1,4 +1,5 @@ """Support for Spider Smart devices.""" + import logging from spiderpy.spiderapi import SpiderApi, SpiderApiException, UnauthorizedException diff --git a/homeassistant/components/spider/climate.py b/homeassistant/components/spider/climate.py index 15ba19e9b3a..11e84a942f4 100644 --- a/homeassistant/components/spider/climate.py +++ b/homeassistant/components/spider/climate.py @@ -1,4 +1,5 @@ """Support for Spider thermostats.""" + from typing import Any from homeassistant.components.climate import ( diff --git a/homeassistant/components/spider/config_flow.py b/homeassistant/components/spider/config_flow.py index eaf30bc541f..a678ea73051 100644 --- a/homeassistant/components/spider/config_flow.py +++ b/homeassistant/components/spider/config_flow.py @@ -1,10 +1,11 @@ """Config flow for Spider.""" + import logging from spiderpy.spiderapi import SpiderApi, SpiderApiException, UnauthorizedException import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME from .const import DEFAULT_SCAN_INTERVAL, DOMAIN @@ -20,7 +21,7 @@ RESULT_CONN_ERROR = "conn_error" RESULT_SUCCESS = "success" -class SpiderConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class SpiderConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a Spider config flow.""" VERSION = 1 diff --git a/homeassistant/components/spider/const.py b/homeassistant/components/spider/const.py index e48e963637a..189763f4e98 100644 --- a/homeassistant/components/spider/const.py +++ b/homeassistant/components/spider/const.py @@ -1,4 +1,5 @@ """Constants for the Spider integration.""" + from homeassistant.const import Platform DOMAIN = "spider" diff --git a/homeassistant/components/spider/sensor.py b/homeassistant/components/spider/sensor.py index bce437437c6..70c38a40e15 100644 --- a/homeassistant/components/spider/sensor.py +++ b/homeassistant/components/spider/sensor.py @@ -1,4 +1,5 @@ """Support for Spider Powerplugs (energy & power).""" + from __future__ import annotations from homeassistant.components.sensor import ( diff --git a/homeassistant/components/spider/switch.py b/homeassistant/components/spider/switch.py index 508dcee9d73..63f0ec6cb69 100644 --- a/homeassistant/components/spider/switch.py +++ b/homeassistant/components/spider/switch.py @@ -1,4 +1,5 @@ """Support for Spider switches.""" + from typing import Any from homeassistant.components.switch import SwitchEntity diff --git a/homeassistant/components/splunk/__init__.py b/homeassistant/components/splunk/__init__.py index 32b63c42370..4294020eeee 100644 --- a/homeassistant/components/splunk/__init__.py +++ b/homeassistant/components/splunk/__init__.py @@ -1,4 +1,5 @@ """Support to send data to a Splunk instance.""" + from http import HTTPStatus import json import logging diff --git a/homeassistant/components/spotify/__init__.py b/homeassistant/components/spotify/__init__.py index ca9f63bbd1c..8d5183a459d 100644 --- a/homeassistant/components/spotify/__init__.py +++ b/homeassistant/components/spotify/__init__.py @@ -1,4 +1,5 @@ """The spotify integration.""" + from __future__ import annotations from dataclasses import dataclass @@ -88,14 +89,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return devices.get("devices", []) - device_coordinator: DataUpdateCoordinator[ - list[dict[str, Any]] - ] = DataUpdateCoordinator( - hass, - LOGGER, - name=f"{entry.title} Devices", - update_interval=timedelta(minutes=5), - update_method=_update_devices, + device_coordinator: DataUpdateCoordinator[list[dict[str, Any]]] = ( + DataUpdateCoordinator( + hass, + LOGGER, + name=f"{entry.title} Devices", + update_interval=timedelta(minutes=5), + update_method=_update_devices, + ) ) await device_coordinator.async_config_entry_first_refresh() diff --git a/homeassistant/components/spotify/browse_media.py b/homeassistant/components/spotify/browse_media.py index 162369fd27d..cc8f57be1bb 100644 --- a/homeassistant/components/spotify/browse_media.py +++ b/homeassistant/components/spotify/browse_media.py @@ -1,4 +1,5 @@ """Support for Spotify media browsing.""" + from __future__ import annotations from enum import StrEnum diff --git a/homeassistant/components/spotify/config_flow.py b/homeassistant/components/spotify/config_flow.py index bbfb92db091..0c60959362d 100644 --- a/homeassistant/components/spotify/config_flow.py +++ b/homeassistant/components/spotify/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Spotify.""" + from __future__ import annotations from collections.abc import Mapping @@ -7,8 +8,7 @@ from typing import Any from spotipy import Spotify -from homeassistant.config_entries import ConfigEntry -from homeassistant.data_entry_flow import FlowResult +from homeassistant.config_entries import ConfigEntry, ConfigFlowResult from homeassistant.helpers import config_entry_oauth2_flow from .const import DOMAIN, SPOTIFY_SCOPES @@ -34,7 +34,7 @@ class SpotifyFlowHandler( """Extra data that needs to be appended to the authorize url.""" return {"scope": ",".join(SPOTIFY_SCOPES)} - async def async_oauth_create_entry(self, data: dict[str, Any]) -> FlowResult: + async def async_oauth_create_entry(self, data: dict[str, Any]) -> ConfigFlowResult: """Create an entry for Spotify.""" spotify = Spotify(auth=data["token"]["access_token"]) @@ -56,7 +56,9 @@ class SpotifyFlowHandler( return self.async_create_entry(title=name, data=data) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Perform reauth upon migration of old entries.""" self.reauth_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] @@ -66,7 +68,7 @@ class SpotifyFlowHandler( async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm reauth dialog.""" if self.reauth_entry is None: return self.async_abort(reason="reauth_account_mismatch") diff --git a/homeassistant/components/spotify/icons.json b/homeassistant/components/spotify/icons.json new file mode 100644 index 00000000000..00c63141eae --- /dev/null +++ b/homeassistant/components/spotify/icons.json @@ -0,0 +1,9 @@ +{ + "entity": { + "media_player": { + "spotify": { + "default": "mdi:spotify" + } + } + } +} diff --git a/homeassistant/components/spotify/manifest.json b/homeassistant/components/spotify/manifest.json index 94475794fdf..84f2bc102e3 100644 --- a/homeassistant/components/spotify/manifest.json +++ b/homeassistant/components/spotify/manifest.json @@ -5,7 +5,6 @@ "config_flow": true, "dependencies": ["application_credentials"], "documentation": "https://www.home-assistant.io/integrations/spotify", - "import_executor": true, "integration_type": "service", "iot_class": "cloud_polling", "loggers": ["spotipy"], diff --git a/homeassistant/components/spotify/media_player.py b/homeassistant/components/spotify/media_player.py index 03b703220c3..487e58d8f8b 100644 --- a/homeassistant/components/spotify/media_player.py +++ b/homeassistant/components/spotify/media_player.py @@ -1,4 +1,5 @@ """Support for interacting with Spotify Connect.""" + from __future__ import annotations from asyncio import run_coroutine_threadsafe @@ -118,9 +119,9 @@ class SpotifyMediaPlayer(MediaPlayerEntity): """Representation of a Spotify controller.""" _attr_has_entity_name = True - _attr_icon = "mdi:spotify" _attr_media_image_remotely_accessible = False _attr_name = None + _attr_translation_key = "spotify" def __init__( self, diff --git a/homeassistant/components/spotify/system_health.py b/homeassistant/components/spotify/system_health.py index a22f7b8a821..963c3bfb0ef 100644 --- a/homeassistant/components/spotify/system_health.py +++ b/homeassistant/components/spotify/system_health.py @@ -1,4 +1,5 @@ """Provide info to system health.""" + from homeassistant.components import system_health from homeassistant.core import HomeAssistant, callback diff --git a/homeassistant/components/spotify/util.py b/homeassistant/components/spotify/util.py index e9af305a1d0..98bce980e5b 100644 --- a/homeassistant/components/spotify/util.py +++ b/homeassistant/components/spotify/util.py @@ -1,4 +1,5 @@ """Utils for Spotify.""" + from __future__ import annotations from typing import Any @@ -20,10 +21,10 @@ def resolve_spotify_media_type(media_content_type: str) -> str: def fetch_image_url(item: dict[str, Any], key="images") -> str | None: """Fetch image url.""" - try: - return item.get(key, [])[0].get("url") - except IndexError: - return None + source = item.get(key, []) + if isinstance(source, list) and source: + return source[0].get("url") + return None def spotify_uri_from_media_browser_url(media_content_id: str) -> str: diff --git a/homeassistant/components/sql/__init__.py b/homeassistant/components/sql/__init__.py index a4768165c25..71e3671ce96 100644 --- a/homeassistant/components/sql/__init__.py +++ b/homeassistant/components/sql/__init__.py @@ -1,4 +1,5 @@ """The sql component.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/sql/config_flow.py b/homeassistant/components/sql/config_flow.py index a697bdc51a7..5537c7ff3b0 100644 --- a/homeassistant/components/sql/config_flow.py +++ b/homeassistant/components/sql/config_flow.py @@ -1,4 +1,5 @@ """Adds config flow for SQL integration.""" + from __future__ import annotations import logging @@ -12,13 +13,18 @@ import sqlparse from sqlparse.exceptions import SQLParseError import voluptuous as vol -from homeassistant import config_entries from homeassistant.components.recorder import CONF_DB_URL, get_instance from homeassistant.components.sensor import ( CONF_STATE_CLASS, SensorDeviceClass, SensorStateClass, ) +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlowWithConfigEntry, +) from homeassistant.const import ( CONF_DEVICE_CLASS, CONF_NAME, @@ -26,7 +32,6 @@ from homeassistant.const import ( CONF_VALUE_TEMPLATE, ) from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import selector from .const import CONF_COLUMN_NAME, CONF_QUERY, DOMAIN @@ -128,7 +133,7 @@ def validate_query(db_url: str, query: str, column: str) -> bool: return True -class SQLConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class SQLConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for SQL integration.""" VERSION = 1 @@ -136,14 +141,14 @@ class SQLConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback def async_get_options_flow( - config_entry: config_entries.ConfigEntry, + config_entry: ConfigEntry, ) -> SQLOptionsFlowHandler: """Get the options flow for this handler.""" return SQLOptionsFlowHandler(config_entry) async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the user step.""" errors = {} description_placeholders = {} @@ -204,12 +209,12 @@ class SQLConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -class SQLOptionsFlowHandler(config_entries.OptionsFlowWithConfigEntry): +class SQLOptionsFlowHandler(OptionsFlowWithConfigEntry): """Handle SQL options.""" async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Manage SQL options.""" errors = {} description_placeholders = {} diff --git a/homeassistant/components/sql/const.py b/homeassistant/components/sql/const.py index 2443e617395..d8d13ab1699 100644 --- a/homeassistant/components/sql/const.py +++ b/homeassistant/components/sql/const.py @@ -1,4 +1,5 @@ """Adds constants for SQL integration.""" + import re from homeassistant.const import Platform diff --git a/homeassistant/components/sql/manifest.json b/homeassistant/components/sql/manifest.json index b440b795e0e..dd44af89237 100644 --- a/homeassistant/components/sql/manifest.json +++ b/homeassistant/components/sql/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sql", "iot_class": "local_polling", - "requirements": ["SQLAlchemy==2.0.27", "sqlparse==0.4.4"] + "requirements": ["SQLAlchemy==2.0.29", "sqlparse==0.4.4"] } diff --git a/homeassistant/components/sql/models.py b/homeassistant/components/sql/models.py index feac9ebf20c..872ceedde71 100644 --- a/homeassistant/components/sql/models.py +++ b/homeassistant/components/sql/models.py @@ -1,4 +1,5 @@ """The sql integration models.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/homeassistant/components/sql/sensor.py b/homeassistant/components/sql/sensor.py index 063627f9f43..68a6cb71f5b 100644 --- a/homeassistant/components/sql/sensor.py +++ b/homeassistant/components/sql/sensor.py @@ -1,4 +1,5 @@ """Sensor from an SQL Query.""" + from __future__ import annotations from datetime import date diff --git a/homeassistant/components/sql/util.py b/homeassistant/components/sql/util.py index 3dd0990b241..48fb53820ff 100644 --- a/homeassistant/components/sql/util.py +++ b/homeassistant/components/sql/util.py @@ -1,4 +1,5 @@ """Utils for sql.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/squeezebox/browse_media.py b/homeassistant/components/squeezebox/browse_media.py index c66bc8af9a5..b770a3e22a3 100644 --- a/homeassistant/components/squeezebox/browse_media.py +++ b/homeassistant/components/squeezebox/browse_media.py @@ -1,4 +1,5 @@ """Support for media browsing.""" + import contextlib from homeassistant.components import media_source diff --git a/homeassistant/components/squeezebox/config_flow.py b/homeassistant/components/squeezebox/config_flow.py index d2786bf213b..effa4f2c970 100644 --- a/homeassistant/components/squeezebox/config_flow.py +++ b/homeassistant/components/squeezebox/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Logitech Squeezebox integration.""" + import asyncio from http import HTTPStatus import logging @@ -7,10 +8,11 @@ from typing import TYPE_CHECKING from pysqueezebox import Server, async_discover import voluptuous as vol -from homeassistant import config_entries, data_entry_flow from homeassistant.components import dhcp from homeassistant.components.media_player import DOMAIN as MP_DOMAIN +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME +from homeassistant.data_entry_flow import AbortFlow from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.device_registry import format_mac from homeassistant.helpers.entity_registry import async_get @@ -61,7 +63,7 @@ def _base_schema(discovery_info=None): return vol.Schema(base_schema) -class SqueezeboxConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class SqueezeboxConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Logitech Squeezebox.""" VERSION = 1 @@ -187,7 +189,7 @@ class SqueezeboxConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_dhcp( self, discovery_info: dhcp.DhcpServiceInfo - ) -> data_entry_flow.FlowResult: + ) -> ConfigFlowResult: """Handle dhcp discovery of a Squeezebox player.""" _LOGGER.debug( "Reached dhcp discovery of a player with info: %s", discovery_info @@ -204,7 +206,7 @@ class SqueezeboxConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): # if we have detected this player, do nothing. if not, there must be a server out there for us to configure, so start the normal user flow (which tries to autodetect server) if registry.async_get_entity_id(MP_DOMAIN, DOMAIN, self.unique_id) is not None: # this player is already known, so do nothing other than mark as configured - raise data_entry_flow.AbortFlow("already_configured") + raise AbortFlow("already_configured") # if the player is unknown, then we likely need to configure its server return await self.async_step_user() diff --git a/homeassistant/components/squeezebox/const.py b/homeassistant/components/squeezebox/const.py index 38a9ef7668f..96a541a16ba 100644 --- a/homeassistant/components/squeezebox/const.py +++ b/homeassistant/components/squeezebox/const.py @@ -1,4 +1,5 @@ """Constants for the Squeezebox component.""" + DOMAIN = "squeezebox" ENTRY_PLAYERS = "entry_players" KNOWN_PLAYERS = "known_players" diff --git a/homeassistant/components/squeezebox/icons.json b/homeassistant/components/squeezebox/icons.json new file mode 100644 index 00000000000..d58f0d5634d --- /dev/null +++ b/homeassistant/components/squeezebox/icons.json @@ -0,0 +1,8 @@ +{ + "services": { + "call_method": "mdi:console", + "call_query": "mdi:database", + "sync": "mdi:sync", + "unsync": "mdi:sync-off" + } +} diff --git a/homeassistant/components/squeezebox/media_player.py b/homeassistant/components/squeezebox/media_player.py index 4e3d71eca24..007d880a263 100644 --- a/homeassistant/components/squeezebox/media_player.py +++ b/homeassistant/components/squeezebox/media_player.py @@ -1,4 +1,5 @@ """Support for interfacing to the Logitech SqueezeBox API.""" + from __future__ import annotations from datetime import datetime @@ -167,9 +168,9 @@ async def async_setup_entry( for player in players: hass.async_create_task(_discovered_player(player)) - hass.data[DOMAIN][config_entry.entry_id][ - PLAYER_DISCOVERY_UNSUB - ] = async_call_later(hass, DISCOVERY_INTERVAL, _discovery) + hass.data[DOMAIN][config_entry.entry_id][PLAYER_DISCOVERY_UNSUB] = ( + async_call_later(hass, DISCOVERY_INTERVAL, _discovery) + ) _LOGGER.debug("Adding player discovery job for LMS server: %s", host) config_entry.async_create_background_task( @@ -394,11 +395,11 @@ class SqueezeBoxEntity(MediaPlayerEntity): player_ids = { p.unique_id: p.entity_id for p in self.hass.data[DOMAIN][KNOWN_PLAYERS] } - sync_group = [] - for player in self._player.sync_group: - if player in player_ids: - sync_group.append(player_ids[player]) - return sync_group + return [ + player_ids[player] + for player in self._player.sync_group + if player in player_ids + ] @property def sync_group(self): @@ -549,8 +550,7 @@ class SqueezeBoxEntity(MediaPlayerEntity): """ all_params = [command] if parameters: - for parameter in parameters: - all_params.append(parameter) + all_params.extend(parameters) await self._player.async_query(*all_params) async def async_call_query(self, command, parameters=None): @@ -561,8 +561,7 @@ class SqueezeBoxEntity(MediaPlayerEntity): """ all_params = [command] if parameters: - for parameter in parameters: - all_params.append(parameter) + all_params.extend(parameters) self._query_result = await self._player.async_query(*all_params) _LOGGER.debug("call_query got result %s", self._query_result) diff --git a/homeassistant/components/srp_energy/__init__.py b/homeassistant/components/srp_energy/__init__.py index 98d1cdd421a..591ba5043e9 100644 --- a/homeassistant/components/srp_energy/__init__.py +++ b/homeassistant/components/srp_energy/__init__.py @@ -1,4 +1,5 @@ """The SRP Energy integration.""" + from srpenergy.client import SrpEnergyClient from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/srp_energy/config_flow.py b/homeassistant/components/srp_energy/config_flow.py index ac32e005e06..8ec53a20cc8 100644 --- a/homeassistant/components/srp_energy/config_flow.py +++ b/homeassistant/components/srp_energy/config_flow.py @@ -1,4 +1,5 @@ """Config flow for SRP Energy.""" + from __future__ import annotations from typing import Any @@ -6,10 +7,9 @@ from typing import Any from srpenergy.client import SrpEnergyClient import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_ID, CONF_NAME, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant, callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError from .const import CONF_IS_TOU, DOMAIN, LOGGER @@ -35,13 +35,13 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, return is_valid -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class SRPEnergyConfigFlow(ConfigFlow, domain=DOMAIN): """Handle an SRP Energy config flow.""" VERSION = 1 @callback - def _show_form(self, errors: dict[str, Any]) -> FlowResult: + def _show_form(self, errors: dict[str, Any]) -> ConfigFlowResult: """Show the form to the user.""" LOGGER.debug("Show Form") return self.async_show_form( @@ -62,7 +62,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" LOGGER.debug("Config entry") errors: dict[str, str] = {} diff --git a/homeassistant/components/srp_energy/const.py b/homeassistant/components/srp_energy/const.py index b2ab05f43d5..00b3b958740 100644 --- a/homeassistant/components/srp_energy/const.py +++ b/homeassistant/components/srp_energy/const.py @@ -1,4 +1,5 @@ """Constants for the SRP Energy integration.""" + from datetime import timedelta import logging diff --git a/homeassistant/components/srp_energy/coordinator.py b/homeassistant/components/srp_energy/coordinator.py index a72ea4d3334..60f73fc27c6 100644 --- a/homeassistant/components/srp_energy/coordinator.py +++ b/homeassistant/components/srp_energy/coordinator.py @@ -1,4 +1,5 @@ """DataUpdateCoordinator for the srp_energy integration.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/srp_energy/sensor.py b/homeassistant/components/srp_energy/sensor.py index 9e8b8d08de9..a9f5c25d6a5 100644 --- a/homeassistant/components/srp_energy/sensor.py +++ b/homeassistant/components/srp_energy/sensor.py @@ -1,4 +1,5 @@ """Support for SRP Energy Sensor.""" + from __future__ import annotations from homeassistant.components.sensor import ( diff --git a/homeassistant/components/srp_energy/strings.json b/homeassistant/components/srp_energy/strings.json index 35195ddb4f2..191d10a70dd 100644 --- a/homeassistant/components/srp_energy/strings.json +++ b/homeassistant/components/srp_energy/strings.json @@ -17,7 +17,7 @@ "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" } }, "entity": { diff --git a/homeassistant/components/ssdp/__init__.py b/homeassistant/components/ssdp/__init__.py index 69647925c47..b34105106e0 100644 --- a/homeassistant/components/ssdp/__init__.py +++ b/homeassistant/components/ssdp/__init__.py @@ -1,11 +1,13 @@ """The SSDP integration.""" + from __future__ import annotations import asyncio -from collections.abc import Awaitable, Callable, Mapping +from collections.abc import Callable, Coroutine, Mapping from dataclasses import dataclass, field from datetime import timedelta from enum import Enum +from functools import partial from ipaddress import IPv4Address, IPv6Address import logging import socket @@ -41,7 +43,7 @@ from homeassistant.const import ( MATCH_ALL, __version__ as current_version, ) -from homeassistant.core import Event, HomeAssistant, callback as core_callback +from homeassistant.core import Event, HassJob, HomeAssistant, callback as core_callback from homeassistant.data_entry_flow import BaseServiceInfo from homeassistant.helpers import config_validation as cv, discovery_flow from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -52,6 +54,7 @@ from homeassistant.helpers.system_info import async_get_system_info from homeassistant.helpers.typing import ConfigType from homeassistant.loader import async_get_ssdp, bind_hass from homeassistant.util.async_ import create_eager_task +from homeassistant.util.logging import catch_log_exception DOMAIN = "ssdp" SSDP_SCANNER = "scanner" @@ -123,7 +126,9 @@ class SsdpServiceInfo(BaseServiceInfo): SsdpChange = Enum("SsdpChange", "ALIVE BYEBYE UPDATE") -SsdpCallback = Callable[[SsdpServiceInfo, SsdpChange], Awaitable] +SsdpHassJobCallback = HassJob[ + [SsdpServiceInfo, SsdpChange], Coroutine[Any, Any, None] | None +] SSDP_SOURCE_SSDP_CHANGE_MAPPING: Mapping[SsdpSource, SsdpChange] = { SsdpSource.SEARCH_ALIVE: SsdpChange.ALIVE, @@ -134,10 +139,15 @@ SSDP_SOURCE_SSDP_CHANGE_MAPPING: Mapping[SsdpSource, SsdpChange] = { } +def _format_err(name: str, *args: Any) -> str: + """Format error message.""" + return f"Exception in SSDP callback {name}: {args}" + + @bind_hass async def async_register_callback( hass: HomeAssistant, - callback: SsdpCallback, + callback: Callable[[SsdpServiceInfo, SsdpChange], Coroutine[Any, Any, None] | None], match_dict: None | dict[str, str] = None, ) -> Callable[[], None]: """Register to receive a callback on ssdp broadcast. @@ -145,7 +155,14 @@ async def async_register_callback( Returns a callback that can be used to cancel the registration. """ scanner: Scanner = hass.data[DOMAIN][SSDP_SCANNER] - return await scanner.async_register_callback(callback, match_dict) + job = HassJob( + catch_log_exception( + callback, + partial(_format_err, str(callback)), + ), + f"ssdp callback {match_dict}", + ) + return await scanner.async_register_callback(job, match_dict) @bind_hass @@ -205,14 +222,18 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True -async def _async_process_callbacks( - callbacks: list[SsdpCallback], +@core_callback +def _async_process_callbacks( + hass: HomeAssistant, + callbacks: list[SsdpHassJobCallback], discovery_info: SsdpServiceInfo, ssdp_change: SsdpChange, ) -> None: for callback in callbacks: try: - await callback(discovery_info, ssdp_change) + hass.async_run_hass_job( + callback, discovery_info, ssdp_change, background=True + ) except Exception: # pylint: disable=broad-except _LOGGER.exception("Failed to callback info: %s", discovery_info) @@ -235,9 +256,9 @@ class IntegrationMatchers: def __init__(self) -> None: """Init optimized integration matching.""" - self._match_by_key: dict[ - str, dict[str, list[tuple[str, dict[str, str]]]] - ] | None = None + self._match_by_key: ( + dict[str, dict[str, list[tuple[str, dict[str, str]]]]] | None + ) = None @core_callback def async_setup( @@ -286,7 +307,7 @@ class Scanner: self._cancel_scan: Callable[[], None] | None = None self._ssdp_listeners: list[SsdpListener] = [] self._device_tracker = SsdpDeviceTracker() - self._callbacks: list[tuple[SsdpCallback, dict[str, str]]] = [] + self._callbacks: list[tuple[SsdpHassJobCallback, dict[str, str]]] = [] self._description_cache: DescriptionCache | None = None self.integration_matchers = integration_matchers @@ -296,7 +317,7 @@ class Scanner: return list(self._device_tracker.devices.values()) async def async_register_callback( - self, callback: SsdpCallback, match_dict: None | dict[str, str] = None + self, callback: SsdpHassJobCallback, match_dict: None | dict[str, str] = None ) -> Callable[[], None]: """Register a callback.""" if match_dict is None: @@ -309,7 +330,8 @@ class Scanner: for ssdp_device in self._ssdp_devices: for headers in ssdp_device.all_combined_headers.values(): if _async_headers_match(headers, lower_match_dict): - await _async_process_callbacks( + _async_process_callbacks( + self.hass, [callback], await self._async_headers_to_discovery_info( ssdp_device, headers @@ -370,7 +392,9 @@ class Scanner: await self._async_start_ssdp_listeners() - self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.async_stop) + self.hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, self.async_stop, run_immediately=True + ) self._cancel_scan = async_track_time_interval( self.hass, self.async_scan, SCAN_INTERVAL, name="SSDP scanner" ) @@ -425,7 +449,7 @@ class Scanner: def _async_get_matching_callbacks( self, combined_headers: CaseInsensitiveDict, - ) -> list[SsdpCallback]: + ) -> list[SsdpHassJobCallback]: """Return a list of callbacks that match.""" return [ callback @@ -450,10 +474,11 @@ class Scanner: _, info_desc = self._description_cache.peek_description_dict(location) if info_desc is None: # Fetch info desc in separate task and process from there. - self.hass.async_create_task( + self.hass.async_create_background_task( self._ssdp_listener_process_callback_with_lookup( ssdp_device, dst, source ), + name=f"ssdp_info_desc_lookup_{location}", eager_start=True, ) return @@ -508,10 +533,7 @@ class Scanner: if callbacks: ssdp_change = SSDP_SOURCE_SSDP_CHANGE_MAPPING[source] - self.hass.async_create_task( - _async_process_callbacks(callbacks, discovery_info, ssdp_change), - eager_start=True, - ) + _async_process_callbacks(self.hass, callbacks, discovery_info, ssdp_change) # Config flows should only be created for alive/update messages from alive devices if source == SsdpSource.ADVERTISEMENT_BYEBYE: @@ -732,9 +754,13 @@ class Server: async def async_start(self) -> None: """Start the server.""" bus = self.hass.bus - bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.async_stop) bus.async_listen_once( - EVENT_HOMEASSISTANT_STARTED, self._async_start_upnp_servers + EVENT_HOMEASSISTANT_STOP, self.async_stop, run_immediately=True + ) + bus.async_listen_once( + EVENT_HOMEASSISTANT_STARTED, + self._async_start_upnp_servers, + run_immediately=True, ) async def _async_get_instance_udn(self) -> str: diff --git a/homeassistant/components/ssdp/manifest.json b/homeassistant/components/ssdp/manifest.json index 6d8010d6b8e..5e549c31806 100644 --- a/homeassistant/components/ssdp/manifest.json +++ b/homeassistant/components/ssdp/manifest.json @@ -4,10 +4,9 @@ "codeowners": [], "dependencies": ["network"], "documentation": "https://www.home-assistant.io/integrations/ssdp", - "import_executor": true, "integration_type": "system", "iot_class": "local_push", "loggers": ["async_upnp_client"], "quality_scale": "internal", - "requirements": ["async-upnp-client==0.38.2"] + "requirements": ["async-upnp-client==0.38.3"] } diff --git a/homeassistant/components/starline/__init__.py b/homeassistant/components/starline/__init__.py index 2af7b4a75f4..17f3b7dc504 100644 --- a/homeassistant/components/starline/__init__.py +++ b/homeassistant/components/starline/__init__.py @@ -1,4 +1,5 @@ """The StarLine component.""" + from __future__ import annotations import voluptuous as vol diff --git a/homeassistant/components/starline/account.py b/homeassistant/components/starline/account.py index 2940dcf0579..d260ba3503e 100644 --- a/homeassistant/components/starline/account.py +++ b/homeassistant/components/starline/account.py @@ -1,4 +1,5 @@ """StarLine Account.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/starline/binary_sensor.py b/homeassistant/components/starline/binary_sensor.py index c0fe56df71e..0383fc8ade6 100644 --- a/homeassistant/components/starline/binary_sensor.py +++ b/homeassistant/components/starline/binary_sensor.py @@ -1,4 +1,5 @@ """Reads vehicle status from StarLine API.""" + from __future__ import annotations from homeassistant.components.binary_sensor import ( @@ -19,7 +20,6 @@ BINARY_SENSOR_TYPES: tuple[BinarySensorEntityDescription, ...] = ( BinarySensorEntityDescription( key="hbrake", translation_key="hand_brake", - icon="mdi:car-brake-parking", ), BinarySensorEntityDescription( key="hood", @@ -45,19 +45,16 @@ BINARY_SENSOR_TYPES: tuple[BinarySensorEntityDescription, ...] = ( key="hfree", translation_key="handsfree", entity_category=EntityCategory.DIAGNOSTIC, - icon="mdi:hand-back-right", ), BinarySensorEntityDescription( key="neutral", translation_key="neutral", entity_category=EntityCategory.DIAGNOSTIC, - icon="mdi:car-shift-pattern", ), BinarySensorEntityDescription( key="arm_moving_pb", translation_key="moving_ban", entity_category=EntityCategory.DIAGNOSTIC, - icon="mdi:car-off", ), ) diff --git a/homeassistant/components/starline/button.py b/homeassistant/components/starline/button.py index af6a05206e0..ea1a27adc15 100644 --- a/homeassistant/components/starline/button.py +++ b/homeassistant/components/starline/button.py @@ -1,4 +1,5 @@ """Support for StarLine button.""" + from __future__ import annotations from homeassistant.components.button import ButtonEntity, ButtonEntityDescription @@ -14,7 +15,6 @@ BUTTON_TYPES: tuple[ButtonEntityDescription, ...] = ( ButtonEntityDescription( key="poke", translation_key="horn", - icon="mdi:bullhorn-outline", ), ) @@ -24,12 +24,12 @@ async def async_setup_entry( ) -> None: """Set up the StarLine button.""" account: StarlineAccount = hass.data[DOMAIN][entry.entry_id] - entities = [] - for device in account.api.devices.values(): - if device.support_state: - for description in BUTTON_TYPES: - entities.append(StarlineButton(account, device, description)) - async_add_entities(entities) + async_add_entities( + StarlineButton(account, device, description) + for device in account.api.devices.values() + if device.support_state + for description in BUTTON_TYPES + ) class StarlineButton(StarlineEntity, ButtonEntity): diff --git a/homeassistant/components/starline/config_flow.py b/homeassistant/components/starline/config_flow.py index a00e24ca2a6..402a94c46b0 100644 --- a/homeassistant/components/starline/config_flow.py +++ b/homeassistant/components/starline/config_flow.py @@ -1,11 +1,13 @@ """Config flow to configure StarLine component.""" + from __future__ import annotations from starline import StarlineAuth import voluptuous as vol -from homeassistant import config_entries, core +from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import callback from .const import ( _LOGGER, @@ -24,7 +26,7 @@ from .const import ( ) -class StarlineFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class StarlineFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a StarLine config flow.""" VERSION = 1 @@ -84,7 +86,7 @@ class StarlineFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return await self._async_authenticate_user(error) return self._async_form_auth_captcha(error) - @core.callback + @callback def _async_form_auth_app(self, error=None): """Authenticate application form.""" errors = {} @@ -106,7 +108,7 @@ class StarlineFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - @core.callback + @callback def _async_form_auth_user(self, error=None): """Authenticate user form.""" errors = {} @@ -128,7 +130,7 @@ class StarlineFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - @core.callback + @callback def _async_form_auth_mfa(self, error=None): """Authenticate mfa form.""" errors = {} @@ -148,7 +150,7 @@ class StarlineFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): description_placeholders={"phone_number": self._phone_number}, ) - @core.callback + @callback def _async_form_auth_captcha(self, error=None): """Captcha verification form.""" errors = {} diff --git a/homeassistant/components/starline/const.py b/homeassistant/components/starline/const.py index 06465c7b50e..15db38e84fd 100644 --- a/homeassistant/components/starline/const.py +++ b/homeassistant/components/starline/const.py @@ -1,4 +1,5 @@ """StarLine constants.""" + import logging from homeassistant.const import Platform diff --git a/homeassistant/components/starline/device_tracker.py b/homeassistant/components/starline/device_tracker.py index 1ddcbc9373b..11b0d433787 100644 --- a/homeassistant/components/starline/device_tracker.py +++ b/homeassistant/components/starline/device_tracker.py @@ -1,4 +1,5 @@ """StarLine device tracker.""" + from homeassistant.components.device_tracker import SourceType, TrackerEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -15,11 +16,11 @@ async def async_setup_entry( ) -> None: """Set up StarLine entry.""" account: StarlineAccount = hass.data[DOMAIN][entry.entry_id] - entities = [] - for device in account.api.devices.values(): - if device.support_position: - entities.append(StarlineDeviceTracker(account, device)) - async_add_entities(entities) + async_add_entities( + StarlineDeviceTracker(account, device) + for device in account.api.devices.values() + if device.support_position + ) class StarlineDeviceTracker(StarlineEntity, TrackerEntity, RestoreEntity): @@ -60,8 +61,3 @@ class StarlineDeviceTracker(StarlineEntity, TrackerEntity, RestoreEntity): def source_type(self) -> SourceType: """Return the source type, eg gps or router, of the device.""" return SourceType.GPS - - @property - def icon(self): - """Return the icon to use in the frontend, if any.""" - return "mdi:map-marker-outline" diff --git a/homeassistant/components/starline/entity.py b/homeassistant/components/starline/entity.py index 27be5e2aace..74807996dfb 100644 --- a/homeassistant/components/starline/entity.py +++ b/homeassistant/components/starline/entity.py @@ -1,4 +1,5 @@ """StarLine base entity.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/starline/icons.json b/homeassistant/components/starline/icons.json new file mode 100644 index 00000000000..b98c4178af1 --- /dev/null +++ b/homeassistant/components/starline/icons.json @@ -0,0 +1,79 @@ +{ + "entity": { + "binary_sensor": { + "hand_brake": { + "default": "mdi:car-brake-parking" + }, + "handsfree": { + "default": "mdi:hand-back-right" + }, + "neutral": { + "default": "mdi:car-shift-pattern" + }, + "moving_ban": { + "default": "mdi:car-off" + } + }, + "button": { + "horn": { + "default": "mdi:bullhorn-outline" + } + }, + "device_tracker": { + "location": { + "default": "mdi:map-marker-outline" + } + }, + "sensor": { + "balance": { + "default": "mdi:cash-multiple" + }, + "fuel": { + "default": "mdi:fuel" + }, + "errors": { + "default": "mdi:alert-octagon" + }, + "mileage": { + "default": "mdi:counter" + }, + "gps_count": { + "default": "mdi:satellite-variant" + } + }, + "switch": { + "engine": { + "default": "mdi:engine-off-outline", + "state": { + "on": "mdi:engine-outline" + } + }, + "webasto": { + "default": "mdi:radiator-off", + "state": { + "on": "mdi:radiator" + } + }, + "additional_channel": { + "default": "mdi:access-point-network-off", + "state": { + "on": "mdi:access-point-network" + } + }, + "horn": { + "default": "mdi:bullhorn-outline" + }, + "service_mode": { + "default": "mdi:car-wrench", + "state": { + "on": "mdi:wrench-clock" + } + } + } + }, + "services": { + "update_state": "mdi:reload", + "set_scan_interval": "mdi:timer", + "set_scan_obd_interval": "mdi:timer" + } +} diff --git a/homeassistant/components/starline/lock.py b/homeassistant/components/starline/lock.py index f663c472a78..19aad1a19b2 100644 --- a/homeassistant/components/starline/lock.py +++ b/homeassistant/components/starline/lock.py @@ -1,4 +1,5 @@ """Support for StarLine lock.""" + from __future__ import annotations from typing import Any @@ -62,13 +63,6 @@ class StarlineLock(StarlineEntity, LockEntity): """ return self._device.alarm_state - @property - def icon(self) -> str: - """Icon to use in the frontend, if any.""" - return ( - "mdi:shield-check-outline" if self.is_locked else "mdi:shield-alert-outline" - ) - @property def is_locked(self) -> bool | None: """Return true if lock is locked.""" diff --git a/homeassistant/components/starline/sensor.py b/homeassistant/components/starline/sensor.py index 4f02ee1a1f6..a53751a3b23 100644 --- a/homeassistant/components/starline/sensor.py +++ b/homeassistant/components/starline/sensor.py @@ -1,4 +1,5 @@ """Reads vehicle status from StarLine API.""" + from __future__ import annotations from homeassistant.components.sensor import ( @@ -33,7 +34,6 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="balance", translation_key="balance", - icon="mdi:cash-multiple", ), SensorEntityDescription( key="ctemp", @@ -55,12 +55,10 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="fuel", translation_key="fuel", - icon="mdi:fuel", ), SensorEntityDescription( key="errors", translation_key="errors", - icon="mdi:alert-octagon", entity_category=EntityCategory.DIAGNOSTIC, ), SensorEntityDescription( @@ -68,12 +66,10 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( translation_key="mileage", native_unit_of_measurement=UnitOfLength.KILOMETERS, device_class=SensorDeviceClass.DISTANCE, - icon="mdi:counter", ), SensorEntityDescription( key="gps_count", translation_key="gps_count", - icon="mdi:satellite-variant", native_unit_of_measurement="satellites", ), ) diff --git a/homeassistant/components/starline/switch.py b/homeassistant/components/starline/switch.py index ef24dd52c02..8ca736d2ac5 100644 --- a/homeassistant/components/starline/switch.py +++ b/homeassistant/components/starline/switch.py @@ -1,7 +1,7 @@ """Support for StarLine switch.""" + from __future__ import annotations -from dataclasses import dataclass from typing import Any from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription @@ -14,53 +14,27 @@ from .account import StarlineAccount, StarlineDevice from .const import DOMAIN from .entity import StarlineEntity - -@dataclass(frozen=True) -class StarlineRequiredKeysMixin: - """Mixin for required keys.""" - - icon_on: str - icon_off: str - - -@dataclass(frozen=True) -class StarlineSwitchEntityDescription( - SwitchEntityDescription, StarlineRequiredKeysMixin -): - """Describes Starline switch entity.""" - - -SWITCH_TYPES: tuple[StarlineSwitchEntityDescription, ...] = ( - StarlineSwitchEntityDescription( +SWITCH_TYPES: tuple[SwitchEntityDescription, ...] = ( + SwitchEntityDescription( key="ign", translation_key="engine", - icon_on="mdi:engine-outline", - icon_off="mdi:engine-off-outline", ), - StarlineSwitchEntityDescription( + SwitchEntityDescription( key="webasto", translation_key="webasto", - icon_on="mdi:radiator", - icon_off="mdi:radiator-off", ), - StarlineSwitchEntityDescription( + SwitchEntityDescription( key="out", translation_key="additional_channel", - icon_on="mdi:access-point-network", - icon_off="mdi:access-point-network-off", ), # Deprecated and should be removed in 2024.8 - StarlineSwitchEntityDescription( + SwitchEntityDescription( key="poke", translation_key="horn", - icon_on="mdi:bullhorn-outline", - icon_off="mdi:bullhorn-outline", ), - StarlineSwitchEntityDescription( + SwitchEntityDescription( key="valet", translation_key="service_mode", - icon_on="mdi:wrench-clock", - icon_off="mdi:car-wrench", ), ) @@ -83,15 +57,13 @@ async def async_setup_entry( class StarlineSwitch(StarlineEntity, SwitchEntity): """Representation of a StarLine switch.""" - entity_description: StarlineSwitchEntityDescription - _attr_assumed_state = True def __init__( self, account: StarlineAccount, device: StarlineDevice, - description: StarlineSwitchEntityDescription, + description: SwitchEntityDescription, ) -> None: """Initialize the switch.""" super().__init__(account, device, description.key) @@ -109,15 +81,6 @@ class StarlineSwitch(StarlineEntity, SwitchEntity): return self._account.engine_attrs(self._device) return None - @property - def icon(self): - """Icon to use in the frontend, if any.""" - return ( - self.entity_description.icon_on - if self.is_on - else self.entity_description.icon_off - ) - @property def is_on(self): """Return True if entity is on.""" diff --git a/homeassistant/components/starlingbank/sensor.py b/homeassistant/components/starlingbank/sensor.py index f4a87837878..f6b11a41102 100644 --- a/homeassistant/components/starlingbank/sensor.py +++ b/homeassistant/components/starlingbank/sensor.py @@ -1,4 +1,5 @@ """Support for balance data via the Starling Bank API.""" + from __future__ import annotations from datetime import timedelta @@ -53,18 +54,18 @@ def setup_platform( ) -> None: """Set up the Sterling Bank sensor platform.""" - sensors = [] + sensors: list[StarlingBalanceSensor] = [] for account in config[CONF_ACCOUNTS]: try: starling_account = StarlingAccount( account[CONF_ACCESS_TOKEN], sandbox=account[CONF_SANDBOX] ) - for balance_type in account[CONF_BALANCE_TYPES]: - sensors.append( - StarlingBalanceSensor( - starling_account, account[CONF_NAME], balance_type - ) + sensors.extend( + StarlingBalanceSensor( + starling_account, account[CONF_NAME], balance_type ) + for balance_type in account[CONF_BALANCE_TYPES] + ) except requests.exceptions.HTTPError as error: _LOGGER.error( "Unable to set up Starling account '%s': %s", account[CONF_NAME], error diff --git a/homeassistant/components/starlink/__init__.py b/homeassistant/components/starlink/__init__.py index 3413c4ff595..17081a7491e 100644 --- a/homeassistant/components/starlink/__init__.py +++ b/homeassistant/components/starlink/__init__.py @@ -1,4 +1,5 @@ """The Starlink integration.""" + from __future__ import annotations from homeassistant.config_entries import ConfigEntry @@ -14,6 +15,7 @@ PLATFORMS = [ Platform.DEVICE_TRACKER, Platform.SENSOR, Platform.SWITCH, + Platform.TIME, ] diff --git a/homeassistant/components/starlink/binary_sensor.py b/homeassistant/components/starlink/binary_sensor.py index d346c19fec4..e48d28dcc44 100644 --- a/homeassistant/components/starlink/binary_sensor.py +++ b/homeassistant/components/starlink/binary_sensor.py @@ -32,20 +32,13 @@ async def async_setup_entry( ) -@dataclass(frozen=True) -class StarlinkBinarySensorEntityDescriptionMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class StarlinkBinarySensorEntityDescription(BinarySensorEntityDescription): + """Describes a Starlink binary sensor entity.""" value_fn: Callable[[StarlinkData], bool | None] -@dataclass(frozen=True) -class StarlinkBinarySensorEntityDescription( - BinarySensorEntityDescription, StarlinkBinarySensorEntityDescriptionMixin -): - """Describes a Starlink binary sensor entity.""" - - class StarlinkBinarySensorEntity(StarlinkEntity, BinarySensorEntity): """A BinarySensorEntity for Starlink devices. Handles creating unique IDs.""" diff --git a/homeassistant/components/starlink/button.py b/homeassistant/components/starlink/button.py index daf3122a00d..f8f18763d30 100644 --- a/homeassistant/components/starlink/button.py +++ b/homeassistant/components/starlink/button.py @@ -31,20 +31,13 @@ async def async_setup_entry( ) -@dataclass(frozen=True) -class StarlinkButtonEntityDescriptionMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class StarlinkButtonEntityDescription(ButtonEntityDescription): + """Describes a Starlink button entity.""" press_fn: Callable[[StarlinkUpdateCoordinator], Awaitable[None]] -@dataclass(frozen=True) -class StarlinkButtonEntityDescription( - ButtonEntityDescription, StarlinkButtonEntityDescriptionMixin -): - """Describes a Starlink button entity.""" - - class StarlinkButtonEntity(StarlinkEntity, ButtonEntity): """A ButtonEntity for Starlink devices. Handles creating unique IDs.""" diff --git a/homeassistant/components/starlink/config_flow.py b/homeassistant/components/starlink/config_flow.py index 987a84796f1..a64d5998556 100644 --- a/homeassistant/components/starlink/config_flow.py +++ b/homeassistant/components/starlink/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Starlink.""" + from __future__ import annotations from typing import Any @@ -6,9 +7,8 @@ from typing import Any from starlink_grpc import ChannelContext, GrpcError, get_id import voluptuous as vol -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_IP_ADDRESS -from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN @@ -22,7 +22,7 @@ class StarlinkConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Ask the user for a server address and a name for the system.""" errors = {} if user_input: diff --git a/homeassistant/components/starlink/coordinator.py b/homeassistant/components/starlink/coordinator.py index 95a5515ab21..ff33b3ecc41 100644 --- a/homeassistant/components/starlink/coordinator.py +++ b/homeassistant/components/starlink/coordinator.py @@ -1,10 +1,12 @@ """Contains the shared Coordinator for Starlink systems.""" + from __future__ import annotations import asyncio from dataclasses import dataclass from datetime import timedelta import logging +from zoneinfo import ZoneInfo from starlink_grpc import ( AlertDict, @@ -13,8 +15,10 @@ from starlink_grpc import ( LocationDict, ObstructionDict, StatusDict, + get_sleep_config, location_data, reboot, + set_sleep_config, set_stow_state, status_data, ) @@ -31,6 +35,7 @@ class StarlinkData: """Contains data pulled from the Starlink system.""" location: LocationDict + sleep: tuple[int, int, bool] status: StatusDict obstruction: ObstructionDict alert: AlertDict @@ -42,7 +47,7 @@ class StarlinkUpdateCoordinator(DataUpdateCoordinator[StarlinkData]): def __init__(self, hass: HomeAssistant, name: str, url: str) -> None: """Initialize an UpdateCoordinator for a group of sensors.""" self.channel_context = ChannelContext(target=url) - + self.timezone = ZoneInfo(hass.config.time_zone) super().__init__( hass, _LOGGER, @@ -59,7 +64,10 @@ class StarlinkUpdateCoordinator(DataUpdateCoordinator[StarlinkData]): location = await self.hass.async_add_executor_job( location_data, self.channel_context ) - return StarlinkData(location, *status) + sleep = await self.hass.async_add_executor_job( + get_sleep_config, self.channel_context + ) + return StarlinkData(location, sleep, *status) except GrpcError as exc: raise UpdateFailed from exc @@ -80,3 +88,45 @@ class StarlinkUpdateCoordinator(DataUpdateCoordinator[StarlinkData]): await self.hass.async_add_executor_job(reboot, self.channel_context) except GrpcError as exc: raise HomeAssistantError from exc + + async def async_set_sleep_schedule_enabled(self, sleep_schedule: bool) -> None: + """Set whether Starlink system uses the configured sleep schedule.""" + async with asyncio.timeout(4): + try: + await self.hass.async_add_executor_job( + set_sleep_config, + self.data.sleep[0], + self.data.sleep[1], + sleep_schedule, + self.channel_context, + ) + except GrpcError as exc: + raise HomeAssistantError from exc + + async def async_set_sleep_start(self, start: int) -> None: + """Set Starlink system sleep schedule start time.""" + async with asyncio.timeout(4): + try: + await self.hass.async_add_executor_job( + set_sleep_config, + start, + self.data.sleep[1], + self.data.sleep[2], + self.channel_context, + ) + except GrpcError as exc: + raise HomeAssistantError from exc + + async def async_set_sleep_duration(self, end: int) -> None: + """Set Starlink system sleep schedule end time.""" + async with asyncio.timeout(4): + try: + await self.hass.async_add_executor_job( + set_sleep_config, + self.data.sleep[0], + end, + self.data.sleep[2], + self.channel_context, + ) + except GrpcError as exc: + raise HomeAssistantError from exc diff --git a/homeassistant/components/starlink/device_tracker.py b/homeassistant/components/starlink/device_tracker.py index f260a7d1c32..84c0a4cac24 100644 --- a/homeassistant/components/starlink/device_tracker.py +++ b/homeassistant/components/starlink/device_tracker.py @@ -26,21 +26,14 @@ async def async_setup_entry( ) -@dataclass(frozen=True) -class StarlinkDeviceTrackerEntityDescriptionMixin: - """Describes a Starlink device tracker.""" +@dataclass(frozen=True, kw_only=True) +class StarlinkDeviceTrackerEntityDescription(EntityDescription): + """Describes a Starlink button entity.""" latitude_fn: Callable[[StarlinkData], float] longitude_fn: Callable[[StarlinkData], float] -@dataclass(frozen=True) -class StarlinkDeviceTrackerEntityDescription( - EntityDescription, StarlinkDeviceTrackerEntityDescriptionMixin -): - """Describes a Starlink button entity.""" - - DEVICE_TRACKERS = [ StarlinkDeviceTrackerEntityDescription( key="device_location", diff --git a/homeassistant/components/starlink/entity.py b/homeassistant/components/starlink/entity.py index b726beeef0d..e868e4f0645 100644 --- a/homeassistant/components/starlink/entity.py +++ b/homeassistant/components/starlink/entity.py @@ -1,4 +1,5 @@ """Contains base entity classes for Starlink entities.""" + from __future__ import annotations from homeassistant.helpers.device_registry import DeviceInfo diff --git a/homeassistant/components/starlink/icons.json b/homeassistant/components/starlink/icons.json new file mode 100644 index 00000000000..65cb273e24b --- /dev/null +++ b/homeassistant/components/starlink/icons.json @@ -0,0 +1,24 @@ +{ + "entity": { + "sensor": { + "ping": { + "default": "mdi:speedometer" + }, + "azimuth": { + "default": "mdi:compass" + }, + "elevation": { + "default": "mdi:compass" + }, + "uplink_throughput": { + "default": "mdi:upload" + }, + "downlink_throughput": { + "default": "mdi:download" + }, + "last_boot_time": { + "default": "mdi:clock" + } + } + } +} diff --git a/homeassistant/components/starlink/sensor.py b/homeassistant/components/starlink/sensor.py index d5116d49305..21f2400022c 100644 --- a/homeassistant/components/starlink/sensor.py +++ b/homeassistant/components/starlink/sensor.py @@ -1,4 +1,5 @@ """Contains sensors exposed by the Starlink integration.""" + from __future__ import annotations from collections.abc import Callable @@ -40,20 +41,13 @@ async def async_setup_entry( ) -@dataclass(frozen=True) -class StarlinkSensorEntityDescriptionMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class StarlinkSensorEntityDescription(SensorEntityDescription): + """Describes a Starlink sensor entity.""" value_fn: Callable[[StarlinkData], datetime | StateType] -@dataclass(frozen=True) -class StarlinkSensorEntityDescription( - SensorEntityDescription, StarlinkSensorEntityDescriptionMixin -): - """Describes a Starlink sensor entity.""" - - class StarlinkSensorEntity(StarlinkEntity, SensorEntity): """A SensorEntity for Starlink devices. Handles creating unique IDs.""" @@ -69,7 +63,6 @@ SENSORS: tuple[StarlinkSensorEntityDescription, ...] = ( StarlinkSensorEntityDescription( key="ping", translation_key="ping", - icon="mdi:speedometer", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfTime.MILLISECONDS, suggested_display_precision=0, @@ -78,7 +71,6 @@ SENSORS: tuple[StarlinkSensorEntityDescription, ...] = ( StarlinkSensorEntityDescription( key="azimuth", translation_key="azimuth", - icon="mdi:compass", state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=DEGREE, @@ -89,7 +81,6 @@ SENSORS: tuple[StarlinkSensorEntityDescription, ...] = ( StarlinkSensorEntityDescription( key="elevation", translation_key="elevation", - icon="mdi:compass", state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=DEGREE, @@ -100,7 +91,6 @@ SENSORS: tuple[StarlinkSensorEntityDescription, ...] = ( StarlinkSensorEntityDescription( key="uplink_throughput", translation_key="uplink_throughput", - icon="mdi:upload", state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.DATA_RATE, native_unit_of_measurement=UnitOfDataRate.BITS_PER_SECOND, @@ -110,7 +100,6 @@ SENSORS: tuple[StarlinkSensorEntityDescription, ...] = ( StarlinkSensorEntityDescription( key="downlink_throughput", translation_key="downlink_throughput", - icon="mdi:download", state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.DATA_RATE, native_unit_of_measurement=UnitOfDataRate.BITS_PER_SECOND, @@ -120,7 +109,6 @@ SENSORS: tuple[StarlinkSensorEntityDescription, ...] = ( StarlinkSensorEntityDescription( key="last_boot_time", translation_key="last_boot_time", - icon="mdi:clock", device_class=SensorDeviceClass.TIMESTAMP, entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda data: now() - timedelta(seconds=data.status["uptime"]), diff --git a/homeassistant/components/starlink/strings.json b/homeassistant/components/starlink/strings.json index bc6807e8ba7..36a4f176e70 100644 --- a/homeassistant/components/starlink/strings.json +++ b/homeassistant/components/starlink/strings.json @@ -75,6 +75,17 @@ "switch": { "stowed": { "name": "Stowed" + }, + "sleep_schedule": { + "name": "Sleep schedule" + } + }, + "time": { + "sleep_start": { + "name": "Sleep start" + }, + "sleep_end": { + "name": "Sleep end" } } } diff --git a/homeassistant/components/starlink/switch.py b/homeassistant/components/starlink/switch.py index 551afa8e73c..3534748127e 100644 --- a/homeassistant/components/starlink/switch.py +++ b/homeassistant/components/starlink/switch.py @@ -31,22 +31,15 @@ async def async_setup_entry( ) -@dataclass(frozen=True) -class StarlinkSwitchEntityDescriptionMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class StarlinkSwitchEntityDescription(SwitchEntityDescription): + """Describes a Starlink switch entity.""" value_fn: Callable[[StarlinkData], bool | None] turn_on_fn: Callable[[StarlinkUpdateCoordinator], Awaitable[None]] turn_off_fn: Callable[[StarlinkUpdateCoordinator], Awaitable[None]] -@dataclass(frozen=True) -class StarlinkSwitchEntityDescription( - SwitchEntityDescription, StarlinkSwitchEntityDescriptionMixin -): - """Describes a Starlink switch entity.""" - - class StarlinkSwitchEntity(StarlinkEntity, SwitchEntity): """A SwitchEntity for Starlink devices. Handles creating unique IDs.""" @@ -74,5 +67,17 @@ SWITCHES = [ value_fn=lambda data: data.status["state"] == "STOWED", turn_on_fn=lambda coordinator: coordinator.async_stow_starlink(True), turn_off_fn=lambda coordinator: coordinator.async_stow_starlink(False), - ) + ), + StarlinkSwitchEntityDescription( + key="sleep_schedule", + translation_key="sleep_schedule", + device_class=SwitchDeviceClass.SWITCH, + value_fn=lambda data: data.sleep[2], + turn_on_fn=lambda coordinator: coordinator.async_set_sleep_schedule_enabled( + True + ), + turn_off_fn=lambda coordinator: coordinator.async_set_sleep_schedule_enabled( + False + ), + ), ] diff --git a/homeassistant/components/starlink/time.py b/homeassistant/components/starlink/time.py new file mode 100644 index 00000000000..6475610564d --- /dev/null +++ b/homeassistant/components/starlink/time.py @@ -0,0 +1,107 @@ +"""Contains time pickers exposed by the Starlink integration.""" + +from __future__ import annotations + +from collections.abc import Awaitable, Callable +from dataclasses import dataclass +from datetime import UTC, datetime, time, tzinfo +import math + +from homeassistant.components.time import TimeEntity, TimeEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import StarlinkData, StarlinkUpdateCoordinator +from .entity import StarlinkEntity + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up all time entities for this entry.""" + coordinator = hass.data[DOMAIN][entry.entry_id] + + async_add_entities( + StarlinkTimeEntity(coordinator, description) for description in TIMES + ) + + +@dataclass(frozen=True, kw_only=True) +class StarlinkTimeEntityDescription(TimeEntityDescription): + """Describes a Starlink time entity.""" + + value_fn: Callable[[StarlinkData, tzinfo], time | None] + update_fn: Callable[[StarlinkUpdateCoordinator, time], Awaitable[None]] + available_fn: Callable[[StarlinkData], bool] + + +class StarlinkTimeEntity(StarlinkEntity, TimeEntity): + """A TimeEntity for Starlink devices. Handles creating unique IDs.""" + + entity_description: StarlinkTimeEntityDescription + + @property + def native_value(self) -> time | None: + """Return the value reported by the time.""" + return self.entity_description.value_fn( + self.coordinator.data, self.coordinator.timezone + ) + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self.entity_description.available_fn(self.coordinator.data) + + async def async_set_value(self, value: time) -> None: + """Change the time.""" + return await self.entity_description.update_fn(self.coordinator, value) + + +def _utc_minutes_to_time(utc_minutes: int, timezone: tzinfo) -> time: + hour = math.floor(utc_minutes / 60) + minute = utc_minutes % 60 + try: + utc = datetime.now(UTC).replace( + hour=hour, minute=minute, second=0, microsecond=0 + ) + except ValueError as exc: + raise HomeAssistantError from exc + return utc.astimezone(timezone).time() + + +def _time_to_utc_minutes(t: time, timezone: tzinfo) -> int: + try: + zoned_time = datetime.now(timezone).replace( + hour=t.hour, minute=t.minute, second=0, microsecond=0 + ) + except ValueError as exc: + raise HomeAssistantError from exc + utc_time = zoned_time.astimezone(UTC).time() + return (utc_time.hour * 60) + utc_time.minute + + +TIMES = [ + StarlinkTimeEntityDescription( + key="sleep_start", + translation_key="sleep_start", + value_fn=lambda data, timezone: _utc_minutes_to_time(data.sleep[0], timezone), + update_fn=lambda coordinator, time: coordinator.async_set_sleep_start( + _time_to_utc_minutes(time, coordinator.timezone) + ), + available_fn=lambda data: data.sleep[2], + ), + StarlinkTimeEntityDescription( + key="sleep_end", + translation_key="sleep_end", + value_fn=lambda data, timezone: _utc_minutes_to_time( + data.sleep[0] + data.sleep[1], timezone + ), + update_fn=lambda coordinator, time: coordinator.async_set_sleep_duration( + _time_to_utc_minutes(time, coordinator.timezone) + ), + available_fn=lambda data: data.sleep[2], + ), +] diff --git a/homeassistant/components/startca/sensor.py b/homeassistant/components/startca/sensor.py index ab53b039756..fad001d6d29 100644 --- a/homeassistant/components/startca/sensor.py +++ b/homeassistant/components/startca/sensor.py @@ -1,4 +1,5 @@ """Support for Start.ca Bandwidth Monitor.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/statistics/icons.json b/homeassistant/components/statistics/icons.json new file mode 100644 index 00000000000..a03163179cb --- /dev/null +++ b/homeassistant/components/statistics/icons.json @@ -0,0 +1,5 @@ +{ + "services": { + "reload": "mdi:reload" + } +} diff --git a/homeassistant/components/statistics/sensor.py b/homeassistant/components/statistics/sensor.py index 817780a9282..d995f529b7d 100644 --- a/homeassistant/components/statistics/sensor.py +++ b/homeassistant/components/statistics/sensor.py @@ -1,4 +1,5 @@ """Support for statistics for sensor values.""" + from __future__ import annotations from collections import deque @@ -33,6 +34,7 @@ from homeassistant.const import ( ) from homeassistant.core import ( CALLBACK_TYPE, + Event, HomeAssistant, State, callback, @@ -47,12 +49,7 @@ from homeassistant.helpers.event import ( ) from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.start import async_at_start -from homeassistant.helpers.typing import ( - ConfigType, - DiscoveryInfoType, - EventType, - StateType, -) +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType from homeassistant.util import dt as dt_util from homeassistant.util.enum import try_parse_enum @@ -203,8 +200,8 @@ def valid_state_characteristic_configuration(config: dict[str, Any]) -> dict[str not is_binary and characteristic not in STATS_NUMERIC_SUPPORT ): raise vol.ValueInvalid( - "The configured characteristic '{}' is not supported for the configured" - " source sensor".format(characteristic) + f"The configured characteristic '{characteristic}' is not supported " + "for the configured source sensor" ) return config @@ -323,9 +320,9 @@ class StatisticsSensor(SensorEntity): self.ages: deque[datetime] = deque(maxlen=self._samples_max_buffer_size) self.attributes: dict[str, StateType] = {} - self._state_characteristic_fn: Callable[ - [], StateType | datetime - ] = self._callable_characteristic_fn(self._state_characteristic) + self._state_characteristic_fn: Callable[[], StateType | datetime] = ( + self._callable_characteristic_fn(self._state_characteristic) + ) self._update_listener: CALLBACK_TYPE | None = None @@ -334,7 +331,7 @@ class StatisticsSensor(SensorEntity): @callback def async_stats_sensor_state_listener( - event: EventType[EventStateChangedData], + event: Event[EventStateChangedData], ) -> None: """Handle the sensor state changes.""" if (new_state := event.data["new_state"]) is None: diff --git a/homeassistant/components/statsd/__init__.py b/homeassistant/components/statsd/__init__.py index 046c01cba32..efe1c818025 100644 --- a/homeassistant/components/statsd/__init__.py +++ b/homeassistant/components/statsd/__init__.py @@ -1,4 +1,5 @@ """Support for sending data to StatsD.""" + import logging import statsd diff --git a/homeassistant/components/steam_online/__init__.py b/homeassistant/components/steam_online/__init__.py index 72f1cd2abb8..93b4a3eb370 100644 --- a/homeassistant/components/steam_online/__init__.py +++ b/homeassistant/components/steam_online/__init__.py @@ -1,4 +1,5 @@ """The Steam integration.""" + from __future__ import annotations from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/steam_online/config_flow.py b/homeassistant/components/steam_online/config_flow.py index 094db9ba207..bd38e79b133 100644 --- a/homeassistant/components/steam_online/config_flow.py +++ b/homeassistant/components/steam_online/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Steam integration.""" + from __future__ import annotations from collections.abc import Iterator, Mapping @@ -7,10 +8,15 @@ from typing import Any import steam import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ( + SOURCE_REAUTH, + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) 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 .const import CONF_ACCOUNT, CONF_ACCOUNTS, DOMAIN, LOGGER, PLACEHOLDERS @@ -27,24 +33,24 @@ def validate_input(user_input: dict[str, str]) -> dict[str, str | int]: return names["response"]["players"]["player"][0] -class SteamFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class SteamFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a config flow for Steam.""" def __init__(self) -> None: """Initialize the flow.""" - self.entry: config_entries.ConfigEntry | None = None + self.entry: ConfigEntry | None = None @staticmethod @callback def async_get_options_flow( - config_entry: config_entries.ConfigEntry, - ) -> config_entries.OptionsFlow: + config_entry: ConfigEntry, + ) -> OptionsFlow: """Get the options flow for this handler.""" return SteamOptionsFlowHandler(config_entry) async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initiated by the user.""" errors = {} if user_input is None and self.entry: @@ -65,7 +71,7 @@ class SteamFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors["base"] = "unknown" if not errors: entry = await self.async_set_unique_id(user_input[CONF_ACCOUNT]) - if entry and self.source == config_entries.SOURCE_REAUTH: + if entry and self.source == SOURCE_REAUTH: self.hass.config_entries.async_update_entry(entry, data=user_input) await self.hass.config_entries.async_reload(entry.entry_id) return self.async_abort(reason="reauth_successful") @@ -92,7 +98,9 @@ class SteamFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): description_placeholders=PLACEHOLDERS, ) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Handle a reauthorization flow request.""" self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) @@ -100,7 +108,7 @@ class SteamFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_reauth_confirm( self, user_input: dict[str, str] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm reauth dialog.""" if user_input is not None: return await self.async_step_user() @@ -116,17 +124,17 @@ def _batch_ids(ids: list[str]) -> Iterator[list[str]]: yield ids[i : i + MAX_IDS_TO_REQUEST] -class SteamOptionsFlowHandler(config_entries.OptionsFlow): +class SteamOptionsFlowHandler(OptionsFlow): """Handle Steam client options.""" - def __init__(self, entry: config_entries.ConfigEntry) -> None: + def __init__(self, entry: ConfigEntry) -> None: """Initialize options flow.""" self.entry = entry self.options = dict(entry.options) async def async_step_init( self, user_input: dict[str, dict[str, str]] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Manage Steam options.""" if user_input is not None: await self.hass.config_entries.async_unload(self.entry.entry_id) diff --git a/homeassistant/components/steam_online/const.py b/homeassistant/components/steam_online/const.py index 01f4410a7c9..3c1bb3892b9 100644 --- a/homeassistant/components/steam_online/const.py +++ b/homeassistant/components/steam_online/const.py @@ -1,4 +1,5 @@ """Steam constants.""" + import logging from typing import Final diff --git a/homeassistant/components/steam_online/coordinator.py b/homeassistant/components/steam_online/coordinator.py index 719acecd1f2..847fd297247 100644 --- a/homeassistant/components/steam_online/coordinator.py +++ b/homeassistant/components/steam_online/coordinator.py @@ -1,4 +1,5 @@ """Data update coordinator for the Steam integration.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/steam_online/entity.py b/homeassistant/components/steam_online/entity.py index 8ad6bd8c713..3ba23a3e58a 100644 --- a/homeassistant/components/steam_online/entity.py +++ b/homeassistant/components/steam_online/entity.py @@ -1,4 +1,5 @@ """Entity classes for the Steam integration.""" + from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/steam_online/sensor.py b/homeassistant/components/steam_online/sensor.py index d3ae69e2517..8e8b70eaeb9 100644 --- a/homeassistant/components/steam_online/sensor.py +++ b/homeassistant/components/steam_online/sensor.py @@ -1,4 +1,5 @@ """Sensor for Steam account status.""" + from __future__ import annotations from datetime import datetime diff --git a/homeassistant/components/steamist/__init__.py b/homeassistant/components/steamist/__init__.py index e84615b9352..8d8401ec6fd 100644 --- a/homeassistant/components/steamist/__init__.py +++ b/homeassistant/components/steamist/__init__.py @@ -1,4 +1,5 @@ """The Steamist integration.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/steamist/config_flow.py b/homeassistant/components/steamist/config_flow.py index 3d5fe000f35..9d2fa5c6c42 100644 --- a/homeassistant/components/steamist/config_flow.py +++ b/homeassistant/components/steamist/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Steamist integration.""" + from __future__ import annotations import logging @@ -8,11 +9,10 @@ from aiosteamist import Steamist from discovery30303 import Device30303, normalize_mac import voluptuous as vol -from homeassistant import config_entries from homeassistant.components import dhcp +from homeassistant.config_entries import ConfigEntryState, ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_DEVICE, CONF_HOST, CONF_MODEL, CONF_NAME from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import device_registry as dr from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.typing import DiscoveryInfoType @@ -28,7 +28,7 @@ from .discovery import ( _LOGGER = logging.getLogger(__name__) -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class SteamistConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Steamist.""" VERSION = 1 @@ -38,7 +38,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._discovered_devices: dict[str, Device30303] = {} self._discovered_device: Device30303 | None = None - async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: + async def async_step_dhcp( + self, discovery_info: dhcp.DhcpServiceInfo + ) -> ConfigFlowResult: """Handle discovery via dhcp.""" self._discovered_device = Device30303( ipaddress=discovery_info.ip, @@ -50,7 +52,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_integration_discovery( self, discovery_info: DiscoveryInfoType - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle integration discovery.""" self._discovered_device = Device30303( ipaddress=discovery_info["ipaddress"], @@ -60,7 +62,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) return await self._async_handle_discovery() - async def _async_handle_discovery(self) -> FlowResult: + async def _async_handle_discovery(self) -> ConfigFlowResult: """Handle any discovery.""" device = self._discovered_device assert device is not None @@ -70,10 +72,11 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(mac) for entry in self._async_current_entries(include_ignore=False): if entry.unique_id == mac or entry.data[CONF_HOST] == host: - if async_update_entry_from_discovery(self.hass, entry, device): - self.hass.async_create_task( - self.hass.config_entries.async_reload(entry.entry_id) - ) + if ( + async_update_entry_from_discovery(self.hass, entry, device) + and entry.state is not ConfigEntryState.SETUP_IN_PROGRESS + ): + self.hass.config_entries.async_schedule_reload(entry.entry_id) return self.async_abort(reason="already_configured") self.context[CONF_HOST] = host for progress in self._async_in_progress(): @@ -91,7 +94,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_discovery_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm discovery.""" assert self._discovered_device is not None device = self._discovered_device @@ -108,7 +111,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) @callback - def _async_create_entry_from_device(self, device: Device30303) -> FlowResult: + def _async_create_entry_from_device(self, device: Device30303) -> ConfigFlowResult: """Create a config entry from a device.""" self._async_abort_entries_match({CONF_HOST: device.ipaddress}) data = {CONF_HOST: device.ipaddress, CONF_NAME: device.name} @@ -121,7 +124,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_pick_device( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the step to pick discovered device.""" if user_input is not None: mac = user_input[CONF_DEVICE] @@ -153,7 +156,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors = {} diff --git a/homeassistant/components/steamist/const.py b/homeassistant/components/steamist/const.py index ae75193a3cc..d505e534f04 100644 --- a/homeassistant/components/steamist/const.py +++ b/homeassistant/components/steamist/const.py @@ -1,6 +1,5 @@ """Constants for the Steamist integration.""" - import aiohttp DOMAIN = "steamist" diff --git a/homeassistant/components/steamist/coordinator.py b/homeassistant/components/steamist/coordinator.py index 67aedf0af94..c5aa7be7ddc 100644 --- a/homeassistant/components/steamist/coordinator.py +++ b/homeassistant/components/steamist/coordinator.py @@ -1,4 +1,5 @@ """DataUpdateCoordinator for steamist.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/steamist/discovery.py b/homeassistant/components/steamist/discovery.py index cff97692979..5c3262ce4eb 100644 --- a/homeassistant/components/steamist/discovery.py +++ b/homeassistant/components/steamist/discovery.py @@ -1,4 +1,5 @@ """The Steamist integration discovery.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/steamist/entity.py b/homeassistant/components/steamist/entity.py index 78340dab363..aef2d652058 100644 --- a/homeassistant/components/steamist/entity.py +++ b/homeassistant/components/steamist/entity.py @@ -1,4 +1,5 @@ """Support for Steamist sensors.""" + from __future__ import annotations from aiosteamist import SteamistStatus diff --git a/homeassistant/components/steamist/icons.json b/homeassistant/components/steamist/icons.json new file mode 100644 index 00000000000..7baeade8bcd --- /dev/null +++ b/homeassistant/components/steamist/icons.json @@ -0,0 +1,9 @@ +{ + "entity": { + "switch": { + "steam_active": { + "default": "mdi:pot-steam" + } + } + } +} diff --git a/homeassistant/components/steamist/sensor.py b/homeassistant/components/steamist/sensor.py index dd51c485b4e..7c24d015513 100644 --- a/homeassistant/components/steamist/sensor.py +++ b/homeassistant/components/steamist/sensor.py @@ -1,4 +1,5 @@ """Support for Steamist sensors.""" + from __future__ import annotations from collections.abc import Callable @@ -30,20 +31,13 @@ UNIT_MAPPINGS = { } -@dataclass(frozen=True) -class SteamistSensorEntityDescriptionMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class SteamistSensorEntityDescription(SensorEntityDescription): + """Describes a Steamist sensor entity.""" value_fn: Callable[[SteamistStatus], int | None] -@dataclass(frozen=True) -class SteamistSensorEntityDescription( - SensorEntityDescription, SteamistSensorEntityDescriptionMixin -): - """Describes a Steamist sensor entity.""" - - SENSORS: tuple[SteamistSensorEntityDescription, ...] = ( SteamistSensorEntityDescription( key=_KEY_MINUTES_REMAIN, diff --git a/homeassistant/components/steamist/switch.py b/homeassistant/components/steamist/switch.py index a9a7526c560..91806f4fa0c 100644 --- a/homeassistant/components/steamist/switch.py +++ b/homeassistant/components/steamist/switch.py @@ -1,4 +1,5 @@ """Support for Steamist switches.""" + from __future__ import annotations from typing import Any @@ -14,7 +15,6 @@ from .entity import SteamistEntity ACTIVE_SWITCH = SwitchEntityDescription( key="active", - icon="mdi:pot-steam", translation_key="steam_active", ) diff --git a/homeassistant/components/stiebel_eltron/__init__.py b/homeassistant/components/stiebel_eltron/__init__.py index 13ca12f482e..a5e92312f3d 100644 --- a/homeassistant/components/stiebel_eltron/__init__.py +++ b/homeassistant/components/stiebel_eltron/__init__.py @@ -1,4 +1,5 @@ """The component for STIEBEL ELTRON heat pumps with ISGWeb Modbus module.""" + from datetime import timedelta import logging diff --git a/homeassistant/components/stiebel_eltron/climate.py b/homeassistant/components/stiebel_eltron/climate.py index cedd1b3dd90..41015ac16a4 100644 --- a/homeassistant/components/stiebel_eltron/climate.py +++ b/homeassistant/components/stiebel_eltron/climate.py @@ -1,4 +1,5 @@ """Support for stiebel_eltron climate platform.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/stookalert/__init__.py b/homeassistant/components/stookalert/__init__.py index 63458a2f78a..0ef9c7fa845 100644 --- a/homeassistant/components/stookalert/__init__.py +++ b/homeassistant/components/stookalert/__init__.py @@ -1,4 +1,5 @@ """The Stookalert integration.""" + from __future__ import annotations import stookalert diff --git a/homeassistant/components/stookalert/binary_sensor.py b/homeassistant/components/stookalert/binary_sensor.py index 0ee087a779e..a2fff52f2a3 100644 --- a/homeassistant/components/stookalert/binary_sensor.py +++ b/homeassistant/components/stookalert/binary_sensor.py @@ -1,4 +1,5 @@ """Support for Stookalert Binary Sensor.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/stookalert/config_flow.py b/homeassistant/components/stookalert/config_flow.py index 02189bc161f..0d3bc0c1761 100644 --- a/homeassistant/components/stookalert/config_flow.py +++ b/homeassistant/components/stookalert/config_flow.py @@ -1,12 +1,12 @@ """Config flow to configure the Stookalert integration.""" + from __future__ import annotations from typing import Any import voluptuous as vol -from homeassistant.config_entries import ConfigFlow -from homeassistant.data_entry_flow import FlowResult +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from .const import CONF_PROVINCE, DOMAIN, PROVINCES @@ -18,7 +18,7 @@ class StookalertFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" if user_input is not None: await self.async_set_unique_id(user_input[CONF_PROVINCE]) diff --git a/homeassistant/components/stookalert/const.py b/homeassistant/components/stookalert/const.py index 3d370fb135d..9896eea212a 100644 --- a/homeassistant/components/stookalert/const.py +++ b/homeassistant/components/stookalert/const.py @@ -1,4 +1,5 @@ """Constants for the Stookalert integration.""" + import logging from typing import Final diff --git a/homeassistant/components/stookalert/diagnostics.py b/homeassistant/components/stookalert/diagnostics.py index cf327174673..c15e808ae19 100644 --- a/homeassistant/components/stookalert/diagnostics.py +++ b/homeassistant/components/stookalert/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for Stookalert.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/stookwijzer/__init__.py b/homeassistant/components/stookwijzer/__init__.py index d1950eaf0a3..a714e3bd368 100644 --- a/homeassistant/components/stookwijzer/__init__.py +++ b/homeassistant/components/stookwijzer/__init__.py @@ -1,4 +1,5 @@ """The Stookwijzer integration.""" + from __future__ import annotations from stookwijzer import Stookwijzer diff --git a/homeassistant/components/stookwijzer/config_flow.py b/homeassistant/components/stookwijzer/config_flow.py index fdf4cc06f37..be53ce56390 100644 --- a/homeassistant/components/stookwijzer/config_flow.py +++ b/homeassistant/components/stookwijzer/config_flow.py @@ -1,13 +1,13 @@ """Config flow to configure the Stookwijzer integration.""" + from __future__ import annotations from typing import Any import voluptuous as vol -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_LATITUDE, CONF_LOCATION, CONF_LONGITUDE -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.selector import LocationSelector from .const import DOMAIN @@ -20,7 +20,7 @@ class StookwijzerFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" if user_input is not None: diff --git a/homeassistant/components/stookwijzer/const.py b/homeassistant/components/stookwijzer/const.py index 1a125da6a6b..e8cb3d818e6 100644 --- a/homeassistant/components/stookwijzer/const.py +++ b/homeassistant/components/stookwijzer/const.py @@ -1,4 +1,5 @@ """Constants for the Stookwijzer integration.""" + from enum import StrEnum import logging from typing import Final diff --git a/homeassistant/components/stookwijzer/diagnostics.py b/homeassistant/components/stookwijzer/diagnostics.py index e29606cb191..c7bf4fad14d 100644 --- a/homeassistant/components/stookwijzer/diagnostics.py +++ b/homeassistant/components/stookwijzer/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for Stookwijzer.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/stookwijzer/sensor.py b/homeassistant/components/stookwijzer/sensor.py index 312f8bdd02d..b8f9a660598 100644 --- a/homeassistant/components/stookwijzer/sensor.py +++ b/homeassistant/components/stookwijzer/sensor.py @@ -1,4 +1,5 @@ """Support for Stookwijzer Sensor.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index 837d11eca7c..44cf9177993 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -14,6 +14,7 @@ are no active output formats, the background worker is shut down and access tokens are expired. Alternatively, a Stream can be configured with keepalive to always keep workers active. """ + from __future__ import annotations import asyncio @@ -34,6 +35,8 @@ from homeassistant.core import Event, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType +from homeassistant.setup import SetupPhases, async_pause_setup +from homeassistant.util.async_ import create_eager_task from .const import ( ATTR_ENDPOINTS, @@ -188,32 +191,39 @@ CONFIG_SCHEMA = vol.Schema( ) -@callback -def update_pyav_logging(_event: Event | None = None) -> None: - """Adjust libav logging to only log when the stream logger is at DEBUG.""" +def set_pyav_logging(enable: bool) -> None: + """Turn PyAV logging on or off.""" + import av # pylint: disable=import-outside-toplevel - def set_pyav_logging(enable: bool) -> None: - """Turn PyAV logging on or off.""" - import av # pylint: disable=import-outside-toplevel - - av.logging.set_level(av.logging.VERBOSE if enable else av.logging.FATAL) - - # enable PyAV logging iff Stream logger is set to debug - set_pyav_logging(logging.getLogger(__name__).isEnabledFor(logging.DEBUG)) + av.logging.set_level(av.logging.VERBOSE if enable else av.logging.FATAL) async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up stream.""" + debug_enabled = _LOGGER.isEnabledFor(logging.DEBUG) + + @callback + def update_pyav_logging(_event: Event | None = None) -> None: + """Adjust libav logging to only log when the stream logger is at DEBUG.""" + nonlocal debug_enabled + if (new_debug_enabled := _LOGGER.isEnabledFor(logging.DEBUG)) == debug_enabled: + return + debug_enabled = new_debug_enabled + # enable PyAV logging iff Stream logger is set to debug + set_pyav_logging(new_debug_enabled) # Only pass through PyAV log messages if stream logging is above DEBUG cancel_logging_listener = hass.bus.async_listen( - EVENT_LOGGING_CHANGED, update_pyav_logging + EVENT_LOGGING_CHANGED, update_pyav_logging, run_immediately=True ) # libav.mp4 and libav.swscaler have a few unimportant messages that are logged # at logging.WARNING. Set those Logger levels to logging.ERROR for logging_namespace in ("libav.mp4", "libav.swscaler"): logging.getLogger(logging_namespace).setLevel(logging.ERROR) - update_pyav_logging() + + # This will load av so we run it in the executor + with async_pause_setup(hass, SetupPhases.WAIT_IMPORT_PACKAGES): + await hass.async_add_executor_job(set_pyav_logging, debug_enabled) # Keep import here so that we can import stream integration without installing reqs # pylint: disable-next=import-outside-toplevel @@ -249,14 +259,14 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: for stream in hass.data[DOMAIN][ATTR_STREAMS]: stream.dynamic_stream_settings.preload_stream = False if awaitables := [ - asyncio.create_task(stream.stop()) + create_eager_task(stream.stop()) for stream in hass.data[DOMAIN][ATTR_STREAMS] ]: await asyncio.wait(awaitables) _LOGGER.debug("Stopped stream workers") cancel_logging_listener() - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, shutdown) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, shutdown, run_immediately=True) return True @@ -399,6 +409,13 @@ class Stream: self._fast_restart_once = True self._thread_quit.set() + def _set_state(self, available: bool) -> None: + """Set the stream state by updating the callback.""" + # Call with call_soon_threadsafe since we know _async_update_state is always + # all callback function instead of using add_job which would have to work + # it out each time + self.hass.loop.call_soon_threadsafe(self._async_update_state, available) + def _run_worker(self) -> None: """Handle consuming streams and restart keepalive streams.""" # Keep import here so that we can import stream integration without installing reqs @@ -409,7 +426,7 @@ class Stream: wait_timeout = 0 while not self._thread_quit.wait(timeout=wait_timeout): start_time = time.time() - self.hass.add_job(self._async_update_state, True) + self._set_state(True) self._diagnostics.set_value( "keepalive", self.dynamic_stream_settings.preload_stream ) @@ -441,7 +458,7 @@ class Stream: continue break - self.hass.add_job(self._async_update_state, False) + self._set_state(False) # To avoid excessive restarts, wait before restarting # As the required recovery time may be different for different setups, start # with trying a short wait_timeout and increase it on each reconnection attempt. diff --git a/homeassistant/components/stream/const.py b/homeassistant/components/stream/const.py index eb954a6a8f5..a2fa065e019 100644 --- a/homeassistant/components/stream/const.py +++ b/homeassistant/components/stream/const.py @@ -1,4 +1,5 @@ """Constants for Stream component.""" + DOMAIN = "stream" ATTR_ENDPOINTS = "endpoints" diff --git a/homeassistant/components/stream/core.py b/homeassistant/components/stream/core.py index 1d2957b35a3..68c08a4f072 100644 --- a/homeassistant/components/stream/core.py +++ b/homeassistant/components/stream/core.py @@ -1,4 +1,5 @@ """Provides core stream functionality.""" + from __future__ import annotations import asyncio @@ -13,7 +14,7 @@ from typing import TYPE_CHECKING, Any from aiohttp import web import numpy as np -from homeassistant.components.http.view import HomeAssistantView +from homeassistant.components.http import KEY_HASS, HomeAssistantView from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers.event import async_call_later from homeassistant.util.decorator import Registry @@ -380,7 +381,7 @@ class StreamView(HomeAssistantView): self, request: web.Request, token: str, sequence: str = "", part_num: str = "" ) -> web.StreamResponse: """Start a GET request.""" - hass = request.app["hass"] + hass = request.app[KEY_HASS] stream = next( (s for s in hass.data[DOMAIN][ATTR_STREAMS] if s.access_token == token), @@ -388,7 +389,7 @@ class StreamView(HomeAssistantView): ) if not stream: - raise web.HTTPNotFound() + raise web.HTTPNotFound # Start worker if not already started await stream.start() @@ -399,7 +400,7 @@ class StreamView(HomeAssistantView): self, request: web.Request, stream: Stream, sequence: str, part_num: str ) -> web.StreamResponse: """Handle the stream request.""" - raise NotImplementedError() + raise NotImplementedError TRANSFORM_IMAGE_FUNCTION = ( diff --git a/homeassistant/components/stream/fmp4utils.py b/homeassistant/components/stream/fmp4utils.py index 7276e7a0d9b..e611e07cd71 100644 --- a/homeassistant/components/stream/fmp4utils.py +++ b/homeassistant/components/stream/fmp4utils.py @@ -1,4 +1,5 @@ """Utilities to help convert mp4s to fmp4s.""" + from __future__ import annotations from collections.abc import Generator @@ -91,7 +92,7 @@ def get_codec_string(mp4_bytes: bytes) -> str: stsd_box[112:116], byteorder="big" ) reverse = 0 - for i in range(0, 32): + for i in range(32): reverse |= general_profile_compatibility & 1 if i == 31: break @@ -169,7 +170,7 @@ def read_init(bytes_io: BufferedIOBase) -> bytes: ZERO32 = b"\x00\x00\x00\x00" ONE32 = b"\x00\x01\x00\x00" -NEGONE32 = b"\xFF\xFF\x00\x00" +NEGONE32 = b"\xff\xff\x00\x00" XYW_ROW = ZERO32 + ZERO32 + b"\x40\x00\x00\x00" ROTATE_RIGHT = (ZERO32 + ONE32 + ZERO32) + (NEGONE32 + ZERO32 + ZERO32) ROTATE_LEFT = (ZERO32 + NEGONE32 + ZERO32) + (ONE32 + ZERO32 + ZERO32) diff --git a/homeassistant/components/stream/hls.py b/homeassistant/components/stream/hls.py index cddb4413ed8..16694822b01 100644 --- a/homeassistant/components/stream/hls.py +++ b/homeassistant/components/stream/hls.py @@ -1,4 +1,5 @@ """Provide functionality to stream HLS.""" + from __future__ import annotations from http import HTTPStatus diff --git a/homeassistant/components/stream/recorder.py b/homeassistant/components/stream/recorder.py index a3441eb76da..6dfc09891b7 100644 --- a/homeassistant/components/stream/recorder.py +++ b/homeassistant/components/stream/recorder.py @@ -1,4 +1,5 @@ """Provide functionality to record stream.""" + from __future__ import annotations from collections import deque @@ -154,9 +155,10 @@ class RecorderOutput(StreamOutput): def write_transform_matrix_and_rename(video_path: str) -> None: """Update the transform matrix and write to the desired filename.""" - with open(video_path + ".tmp", mode="rb") as in_file, open( - video_path, mode="wb" - ) as out_file: + with ( + open(video_path + ".tmp", mode="rb") as in_file, + open(video_path, mode="wb") as out_file, + ): init = transform_init( read_init(in_file), self.dynamic_stream_settings.orientation ) diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py index 0badd8ebc42..87d9118f3a5 100644 --- a/homeassistant/components/stream/worker.py +++ b/homeassistant/components/stream/worker.py @@ -1,4 +1,5 @@ """Provides the worker thread needed for processing streams.""" + from __future__ import annotations from collections import defaultdict, deque @@ -67,9 +68,9 @@ class StreamState: """Initialize StreamState.""" self._stream_id: int = 0 self.hass = hass - self._outputs_callback: Callable[ - [], Mapping[str, StreamOutput] - ] = outputs_callback + self._outputs_callback: Callable[[], Mapping[str, StreamOutput]] = ( + outputs_callback + ) # sequence gets incremented before the first segment so the first segment # has a sequence number of 0. self._sequence = -1 @@ -160,21 +161,19 @@ class StreamMuxer: mode="w", format=SEGMENT_CONTAINER_FORMAT, container_options={ - **{ - # Removed skip_sidx - see: - # https://github.com/home-assistant/core/pull/39970 - # "cmaf" flag replaces several of the movflags used, - # but too recent to use for now - "movflags": "frag_custom+empty_moov+default_base_moof+frag_discont+negative_cts_offsets+skip_trailer+delay_moov", - # Sometimes the first segment begins with negative timestamps, - # and this setting just - # adjusts the timestamps in the output from that segment to start - # from 0. Helps from having to make some adjustments - # in test_durations - "avoid_negative_ts": "make_non_negative", - "fragment_index": str(sequence + 1), - "video_track_timescale": str(int(1 / input_vstream.time_base)), - }, + # Removed skip_sidx - see: + # https://github.com/home-assistant/core/pull/39970 + # "cmaf" flag replaces several of the movflags used, + # but too recent to use for now + "movflags": "frag_custom+empty_moov+default_base_moof+frag_discont+negative_cts_offsets+skip_trailer+delay_moov", + # Sometimes the first segment begins with negative timestamps, + # and this setting just + # adjusts the timestamps in the output from that segment to start + # from 0. Helps from having to make some adjustments + # in test_durations + "avoid_negative_ts": "make_non_negative", + "fragment_index": str(sequence + 1), + "video_track_timescale": str(int(1 / input_vstream.time_base)), # Only do extra fragmenting if we are using ll_hls # Let ffmpeg do the work using frag_duration # Fragment durations may exceed the 15% allowed variance but it seems ok diff --git a/homeassistant/components/streamlabswater/__init__.py b/homeassistant/components/streamlabswater/__init__.py index c3bbe5a96d4..46acc443d2e 100644 --- a/homeassistant/components/streamlabswater/__init__.py +++ b/homeassistant/components/streamlabswater/__init__.py @@ -81,12 +81,12 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async_create_issue( hass, DOMAIN, - f"deprecated_yaml_import_issue_${result['reason']}", + f"deprecated_yaml_import_issue_{result['reason']}", breaks_in_ha_version="2024.7.0", is_fixable=False, issue_domain=DOMAIN, severity=IssueSeverity.WARNING, - translation_key=f"deprecated_yaml_import_issue_${result['reason']}", + translation_key=f"deprecated_yaml_import_issue_{result['reason']}", translation_placeholders=ISSUE_PLACEHOLDER, ) return True diff --git a/homeassistant/components/streamlabswater/binary_sensor.py b/homeassistant/components/streamlabswater/binary_sensor.py index efc0eb24dd7..5a0073c25d3 100644 --- a/homeassistant/components/streamlabswater/binary_sensor.py +++ b/homeassistant/components/streamlabswater/binary_sensor.py @@ -1,4 +1,5 @@ """Support for Streamlabs Water Monitor Away Mode.""" + from __future__ import annotations from homeassistant.components.binary_sensor import BinarySensorEntity diff --git a/homeassistant/components/streamlabswater/config_flow.py b/homeassistant/components/streamlabswater/config_flow.py index 5cede037d5a..327e5dcdae3 100644 --- a/homeassistant/components/streamlabswater/config_flow.py +++ b/homeassistant/components/streamlabswater/config_flow.py @@ -1,4 +1,5 @@ """Config flow for StreamLabs integration.""" + from __future__ import annotations from typing import Any @@ -6,10 +7,9 @@ from typing import Any from streamlabswater.streamlabswater import StreamlabsClient import voluptuous as vol -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError from .const import DOMAIN, LOGGER @@ -32,7 +32,7 @@ class StreamlabsConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors: dict[str, str] = {} if user_input is not None: @@ -57,7 +57,7 @@ class StreamlabsConfigFlow(ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_import(self, user_input: dict[str, Any]) -> FlowResult: + async def async_step_import(self, user_input: dict[str, Any]) -> ConfigFlowResult: """Import the yaml config.""" self._async_abort_entries_match(user_input) try: diff --git a/homeassistant/components/streamlabswater/const.py b/homeassistant/components/streamlabswater/const.py index ee407d376d4..f0ac6613a42 100644 --- a/homeassistant/components/streamlabswater/const.py +++ b/homeassistant/components/streamlabswater/const.py @@ -1,4 +1,5 @@ """Constants for the StreamLabs integration.""" + import logging DOMAIN = "streamlabswater" diff --git a/homeassistant/components/streamlabswater/coordinator.py b/homeassistant/components/streamlabswater/coordinator.py index bcb2e7790d4..56e67abe222 100644 --- a/homeassistant/components/streamlabswater/coordinator.py +++ b/homeassistant/components/streamlabswater/coordinator.py @@ -1,4 +1,5 @@ """Coordinator for Streamlabs water integration.""" + from dataclasses import dataclass from datetime import timedelta diff --git a/homeassistant/components/streamlabswater/entity.py b/homeassistant/components/streamlabswater/entity.py index 4458523a07f..fb7031a9e76 100644 --- a/homeassistant/components/streamlabswater/entity.py +++ b/homeassistant/components/streamlabswater/entity.py @@ -1,4 +1,5 @@ """Base entity for Streamlabs integration.""" + from homeassistant.core import DOMAIN from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/streamlabswater/icons.json b/homeassistant/components/streamlabswater/icons.json new file mode 100644 index 00000000000..aebe224b35e --- /dev/null +++ b/homeassistant/components/streamlabswater/icons.json @@ -0,0 +1,5 @@ +{ + "services": { + "set_away_mode": "mdi:home" + } +} diff --git a/homeassistant/components/streamlabswater/sensor.py b/homeassistant/components/streamlabswater/sensor.py index d9bb76814b5..412b2187495 100644 --- a/homeassistant/components/streamlabswater/sensor.py +++ b/homeassistant/components/streamlabswater/sensor.py @@ -1,4 +1,5 @@ """Support for Streamlabs Water Monitor Usage.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/streamlabswater/strings.json b/homeassistant/components/streamlabswater/strings.json index 204f7e831ef..872a0d1f6ac 100644 --- a/homeassistant/components/streamlabswater/strings.json +++ b/homeassistant/components/streamlabswater/strings.json @@ -52,7 +52,7 @@ "issues": { "deprecated_yaml_import_issue_cannot_connect": { "title": "The Streamlabs water YAML configuration import failed", - "description": "Configuring Streamlabs water using YAML is being removed but there was an connection error importing your YAML configuration.\n\nEnsure connection to Streamlabs water works and restart Home Assistant to try again or remove the Streamlabs water YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually." + "description": "Configuring Streamlabs water using YAML is being removed but there was a connection error importing your YAML configuration.\n\nEnsure connection to Streamlabs water works and restart Home Assistant to try again or remove the Streamlabs water YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually." }, "deprecated_yaml_import_issue_unknown": { "title": "The Streamlabs water YAML configuration import failed", diff --git a/homeassistant/components/stt/__init__.py b/homeassistant/components/stt/__init__.py index c856a4817c9..676d8b8aa76 100644 --- a/homeassistant/components/stt/__init__.py +++ b/homeassistant/components/stt/__init__.py @@ -1,8 +1,8 @@ """Provide functionality to STT.""" + from __future__ import annotations from abc import abstractmethod -import asyncio from collections.abc import AsyncIterable from dataclasses import asdict import logging @@ -18,7 +18,7 @@ from aiohttp.web_exceptions import ( import voluptuous as vol from homeassistant.components import websocket_api -from homeassistant.components.http import HomeAssistantView +from homeassistant.components.http import KEY_HASS, HomeAssistantView from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN from homeassistant.core import HomeAssistant, callback @@ -126,8 +126,14 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: component.register_shutdown() platform_setups = async_setup_legacy(hass, config) - if platform_setups: - await asyncio.wait([asyncio.create_task(setup) for setup in platform_setups]) + for setup in platform_setups: + # Tasks are created as tracked tasks to ensure startup + # waits for them to finish, but we explicitly do not + # want to wait for them to finish here because we want + # any config entries that use stt as a base platform + # to be able to start with out having to wait for the + # legacy platforms to finish setting up. + hass.async_create_task(setup, eager_start=True) hass.http.register_view(SpeechToTextView(hass.data[DATA_PROVIDERS])) return True @@ -250,13 +256,13 @@ class SpeechToTextView(HomeAssistantView): async def post(self, request: web.Request, provider: str) -> web.Response: """Convert Speech (audio) to text.""" - hass: HomeAssistant = request.app["hass"] + hass = request.app[KEY_HASS] provider_entity: SpeechToTextEntity | None = None if ( not (provider_entity := async_get_speech_to_text_entity(hass, provider)) and provider not in self.providers ): - raise HTTPNotFound() + raise HTTPNotFound # Get metadata try: @@ -269,7 +275,7 @@ class SpeechToTextView(HomeAssistantView): # Check format if not stt_provider.check_metadata(metadata): - raise HTTPUnsupportedMediaType() + raise HTTPUnsupportedMediaType # Process audio stream result = await stt_provider.async_process_audio_stream( @@ -278,7 +284,7 @@ class SpeechToTextView(HomeAssistantView): else: # Check format if not provider_entity.check_metadata(metadata): - raise HTTPUnsupportedMediaType() + raise HTTPUnsupportedMediaType # Process audio stream result = await provider_entity.internal_async_process_audio_stream( @@ -290,12 +296,12 @@ class SpeechToTextView(HomeAssistantView): async def get(self, request: web.Request, provider: str) -> web.Response: """Return provider specific audio information.""" - hass: HomeAssistant = request.app["hass"] + hass = request.app[KEY_HASS] if ( not (provider_entity := async_get_speech_to_text_entity(hass, provider)) and provider not in self.providers ): - raise HTTPNotFound() + raise HTTPNotFound if not provider_entity: stt_provider = self._get_provider(hass, provider) diff --git a/homeassistant/components/stt/const.py b/homeassistant/components/stt/const.py index c9f5eb13d17..2df5bea0316 100644 --- a/homeassistant/components/stt/const.py +++ b/homeassistant/components/stt/const.py @@ -1,4 +1,5 @@ """STT constante.""" + from enum import Enum DOMAIN = "stt" diff --git a/homeassistant/components/stt/legacy.py b/homeassistant/components/stt/legacy.py index bd1cfbca3d2..997835ef9f8 100644 --- a/homeassistant/components/stt/legacy.py +++ b/homeassistant/components/stt/legacy.py @@ -1,4 +1,5 @@ """Handle legacy speech-to-text platforms.""" + from __future__ import annotations from abc import ABC, abstractmethod @@ -10,7 +11,11 @@ from homeassistant.config import config_per_platform from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import discovery from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from homeassistant.setup import async_prepare_setup_platform +from homeassistant.setup import ( + SetupPhases, + async_prepare_setup_platform, + async_start_setup, +) from .const import ( DATA_PROVIDERS, @@ -67,12 +72,20 @@ def async_setup_legacy( return try: - provider = await platform.async_get_engine(hass, p_config, discovery_info) + with async_start_setup( + hass, + integration=p_type, + group=str(id(p_config)), + phase=SetupPhases.PLATFORM_SETUP, + ): + provider = await platform.async_get_engine( + hass, p_config, discovery_info + ) - provider.name = p_type - provider.hass = hass + provider.name = p_type + provider.hass = hass - providers[provider.name] = provider + providers[provider.name] = provider except Exception: # pylint: disable=broad-except _LOGGER.exception("Error setting up platform: %s", p_type) return diff --git a/homeassistant/components/stt/models.py b/homeassistant/components/stt/models.py index 45322e2da07..9471316dc8e 100644 --- a/homeassistant/components/stt/models.py +++ b/homeassistant/components/stt/models.py @@ -1,4 +1,5 @@ """Speech-to-text data models.""" + from dataclasses import dataclass from .const import ( diff --git a/homeassistant/components/subaru/__init__.py b/homeassistant/components/subaru/__init__.py index 8a22391284f..d7169fc181e 100644 --- a/homeassistant/components/subaru/__init__.py +++ b/homeassistant/components/subaru/__init__.py @@ -1,4 +1,5 @@ """The Subaru integration.""" + from datetime import timedelta import logging import time diff --git a/homeassistant/components/subaru/config_flow.py b/homeassistant/components/subaru/config_flow.py index b21feab7843..5ecaf9670d7 100644 --- a/homeassistant/components/subaru/config_flow.py +++ b/homeassistant/components/subaru/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Subaru integration.""" + from __future__ import annotations from datetime import datetime @@ -14,7 +15,12 @@ from subarulink import ( from subarulink.const import COUNTRY_CAN, COUNTRY_USA import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.const import ( CONF_COUNTRY, CONF_DEVICE_ID, @@ -23,7 +29,6 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import aiohttp_client, config_validation as cv from .const import CONF_UPDATE_ENABLED, DOMAIN @@ -34,7 +39,7 @@ CONF_VALIDATION_CODE = "validation_code" PIN_SCHEMA = vol.Schema({vol.Required(CONF_PIN): str}) -class SubaruConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class SubaruConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Subaru.""" VERSION = 1 @@ -46,7 +51,7 @@ class SubaruConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the start of the config flow.""" error = None @@ -96,7 +101,7 @@ class SubaruConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback def async_get_options_flow( - config_entry: config_entries.ConfigEntry, + config_entry: ConfigEntry, ) -> OptionsFlowHandler: """Get the options flow for this handler.""" return OptionsFlowHandler(config_entry) @@ -129,7 +134,7 @@ class SubaruConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_two_factor( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Select contact method and request 2FA code from Subaru.""" error = None if user_input: @@ -157,7 +162,7 @@ class SubaruConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_two_factor_validate( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Validate received 2FA code with Subaru.""" error = None if user_input: @@ -182,7 +187,7 @@ class SubaruConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_pin( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle second part of config flow, if required.""" error = None if user_input and self.controller.update_saved_pin(user_input[CONF_PIN]): @@ -202,16 +207,16 @@ class SubaruConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form(step_id="pin", data_schema=PIN_SCHEMA, errors=error) -class OptionsFlowHandler(config_entries.OptionsFlow): +class OptionsFlowHandler(OptionsFlow): """Handle a option flow for Subaru.""" - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + 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: + ) -> ConfigFlowResult: """Handle options flow.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) diff --git a/homeassistant/components/subaru/const.py b/homeassistant/components/subaru/const.py index ab76c363f7e..d8692e6a8bc 100644 --- a/homeassistant/components/subaru/const.py +++ b/homeassistant/components/subaru/const.py @@ -1,4 +1,5 @@ """Constants for the Subaru integration.""" + from subarulink.const import ALL_DOORS, DRIVERS_DOOR, TAILGATE_DOOR from homeassistant.const import Platform diff --git a/homeassistant/components/subaru/device_tracker.py b/homeassistant/components/subaru/device_tracker.py index 4a8cb8ad5ee..5d25056312e 100644 --- a/homeassistant/components/subaru/device_tracker.py +++ b/homeassistant/components/subaru/device_tracker.py @@ -1,4 +1,5 @@ """Support for Subaru device tracker.""" + from __future__ import annotations from typing import Any @@ -35,11 +36,11 @@ async def async_setup_entry( entry: dict = hass.data[DOMAIN][config_entry.entry_id] coordinator: DataUpdateCoordinator = entry[ENTRY_COORDINATOR] vehicle_info: dict = entry[ENTRY_VEHICLES] - entities: list[SubaruDeviceTracker] = [] - for vehicle in vehicle_info.values(): - if vehicle[VEHICLE_HAS_REMOTE_SERVICE]: - entities.append(SubaruDeviceTracker(vehicle, coordinator)) - async_add_entities(entities) + async_add_entities( + SubaruDeviceTracker(vehicle, coordinator) + for vehicle in vehicle_info.values() + if vehicle[VEHICLE_HAS_REMOTE_SERVICE] + ) class SubaruDeviceTracker( @@ -47,7 +48,7 @@ class SubaruDeviceTracker( ): """Class for Subaru device tracker.""" - _attr_icon = "mdi:car" + _attr_translation_key = "location" _attr_has_entity_name = True _attr_name = None diff --git a/homeassistant/components/subaru/diagnostics.py b/homeassistant/components/subaru/diagnostics.py index 79ffcbe1792..0a26387d1c2 100644 --- a/homeassistant/components/subaru/diagnostics.py +++ b/homeassistant/components/subaru/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics for the Subaru integration.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/subaru/icons.json b/homeassistant/components/subaru/icons.json new file mode 100644 index 00000000000..f6c3597c3c3 --- /dev/null +++ b/homeassistant/components/subaru/icons.json @@ -0,0 +1,29 @@ +{ + "entity": { + "device_tracker": { + "location": { + "default": "mdi:car" + } + }, + "sensor": { + "odometer": { + "default": "mdi:road-variant" + }, + "average_fuel_consumption": { + "default": "mdi:leaf" + }, + "range": { + "default": "mdi:gas-station" + }, + "fuel_level": { + "default": "mdi:gas-station" + }, + "ev_range": { + "default": "mdi:ev-station" + } + } + }, + "services": { + "unlock_specific_door": "mdi:lock-open-variant" + } +} diff --git a/homeassistant/components/subaru/lock.py b/homeassistant/components/subaru/lock.py index 342fe34b97d..e21102f0b0c 100644 --- a/homeassistant/components/subaru/lock.py +++ b/homeassistant/components/subaru/lock.py @@ -1,4 +1,5 @@ """Support for Subaru door locks.""" + import logging from typing import Any diff --git a/homeassistant/components/subaru/remote_service.py b/homeassistant/components/subaru/remote_service.py index 04c87b6b8d2..acd71e186da 100644 --- a/homeassistant/components/subaru/remote_service.py +++ b/homeassistant/components/subaru/remote_service.py @@ -1,4 +1,5 @@ """Remote vehicle services for Subaru integration.""" + import logging from subarulink.exceptions import SubaruException diff --git a/homeassistant/components/subaru/sensor.py b/homeassistant/components/subaru/sensor.py index eda8c20b10e..bbb00a758dd 100644 --- a/homeassistant/components/subaru/sensor.py +++ b/homeassistant/components/subaru/sensor.py @@ -1,4 +1,5 @@ """Support for Subaru sensors.""" + from __future__ import annotations import logging @@ -57,7 +58,6 @@ SAFETY_SENSORS = [ key=sc.ODOMETER, translation_key="odometer", device_class=SensorDeviceClass.DISTANCE, - icon="mdi:road-variant", native_unit_of_measurement=UnitOfLength.KILOMETERS, state_class=SensorStateClass.TOTAL_INCREASING, ), @@ -68,7 +68,6 @@ API_GEN_2_SENSORS = [ SensorEntityDescription( key=sc.AVG_FUEL_CONSUMPTION, translation_key="average_fuel_consumption", - icon="mdi:leaf", native_unit_of_measurement=FUEL_CONSUMPTION_LITERS_PER_HUNDRED_KILOMETERS, state_class=SensorStateClass.MEASUREMENT, ), @@ -76,7 +75,6 @@ API_GEN_2_SENSORS = [ key=sc.DIST_TO_EMPTY, translation_key="range", device_class=SensorDeviceClass.DISTANCE, - icon="mdi:gas-station", native_unit_of_measurement=UnitOfLength.KILOMETERS, state_class=SensorStateClass.MEASUREMENT, ), @@ -115,7 +113,6 @@ API_GEN_3_SENSORS = [ SensorEntityDescription( key=sc.REMAINING_FUEL_PERCENT, translation_key="fuel_level", - icon="mdi:gas-station", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, ), @@ -127,7 +124,6 @@ EV_SENSORS = [ key=sc.EV_DISTANCE_TO_EMPTY, translation_key="ev_range", device_class=SensorDeviceClass.DISTANCE, - icon="mdi:ev-station", native_unit_of_measurement=UnitOfLength.MILES, state_class=SensorStateClass.MEASUREMENT, ), diff --git a/homeassistant/components/suez_water/__init__.py b/homeassistant/components/suez_water/__init__.py index 02d78dfee41..07944de2c81 100644 --- a/homeassistant/components/suez_water/__init__.py +++ b/homeassistant/components/suez_water/__init__.py @@ -1,4 +1,5 @@ """The Suez Water integration.""" + from __future__ import annotations from pysuez import SuezClient diff --git a/homeassistant/components/suez_water/config_flow.py b/homeassistant/components/suez_water/config_flow.py index d01b8035a0c..f3bfda91c3c 100644 --- a/homeassistant/components/suez_water/config_flow.py +++ b/homeassistant/components/suez_water/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Suez Water integration.""" + from __future__ import annotations import logging @@ -8,9 +9,8 @@ from pysuez import SuezClient from pysuez.client import PySuezError import voluptuous as vol -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError from .const import CONF_COUNTER_ID, DOMAIN @@ -51,7 +51,7 @@ class SuezWaterConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors: dict[str, str] = {} if user_input is not None: @@ -75,7 +75,7 @@ class SuezWaterConfigFlow(ConfigFlow, domain=DOMAIN): step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors ) - async def async_step_import(self, user_input: dict[str, Any]) -> FlowResult: + async def async_step_import(self, user_input: dict[str, Any]) -> ConfigFlowResult: """Import the yaml config.""" await self.async_set_unique_id(user_input[CONF_USERNAME]) self._abort_if_unique_id_configured() diff --git a/homeassistant/components/suez_water/sensor.py b/homeassistant/components/suez_water/sensor.py index 6df2e3870d7..f48e78bb153 100644 --- a/homeassistant/components/suez_water/sensor.py +++ b/homeassistant/components/suez_water/sensor.py @@ -1,4 +1,5 @@ """Sensor for Suez Water Consumption data.""" + from __future__ import annotations from datetime import timedelta @@ -73,12 +74,12 @@ async def async_setup_platform( async_create_issue( hass, DOMAIN, - f"deprecated_yaml_import_issue_${result['reason']}", + f"deprecated_yaml_import_issue_{result['reason']}", breaks_in_ha_version="2024.7.0", is_fixable=False, issue_domain=DOMAIN, severity=IssueSeverity.WARNING, - translation_key=f"deprecated_yaml_import_issue_${result['reason']}", + translation_key=f"deprecated_yaml_import_issue_{result['reason']}", translation_placeholders=ISSUE_PLACEHOLDER, ) @@ -123,28 +124,28 @@ class SuezSensor(SensorEntity): self._attr_extra_state_attributes["this_month_consumption"] = {} for item in self.client.attributes["thisMonthConsumption"]: - self._attr_extra_state_attributes["this_month_consumption"][ - item - ] = self.client.attributes["thisMonthConsumption"][item] + self._attr_extra_state_attributes["this_month_consumption"][item] = ( + self.client.attributes["thisMonthConsumption"][item] + ) self._attr_extra_state_attributes["previous_month_consumption"] = {} for item in self.client.attributes["previousMonthConsumption"]: self._attr_extra_state_attributes["previous_month_consumption"][ item ] = self.client.attributes["previousMonthConsumption"][item] - self._attr_extra_state_attributes[ - "highest_monthly_consumption" - ] = self.client.attributes["highestMonthlyConsumption"] - self._attr_extra_state_attributes[ - "last_year_overall" - ] = self.client.attributes["lastYearOverAll"] - self._attr_extra_state_attributes[ - "this_year_overall" - ] = self.client.attributes["thisYearOverAll"] + self._attr_extra_state_attributes["highest_monthly_consumption"] = ( + self.client.attributes["highestMonthlyConsumption"] + ) + self._attr_extra_state_attributes["last_year_overall"] = ( + self.client.attributes["lastYearOverAll"] + ) + self._attr_extra_state_attributes["this_year_overall"] = ( + self.client.attributes["thisYearOverAll"] + ) self._attr_extra_state_attributes["history"] = {} for item in self.client.attributes["history"]: - self._attr_extra_state_attributes["history"][ - item - ] = self.client.attributes["history"][item] + self._attr_extra_state_attributes["history"][item] = ( + self.client.attributes["history"][item] + ) except PySuezError: self._attr_available = False diff --git a/homeassistant/components/suez_water/strings.json b/homeassistant/components/suez_water/strings.json index b4b81a788b5..fd85565d297 100644 --- a/homeassistant/components/suez_water/strings.json +++ b/homeassistant/components/suez_water/strings.json @@ -32,7 +32,7 @@ }, "deprecated_yaml_import_issue_cannot_connect": { "title": "The Suez water YAML configuration import failed", - "description": "Configuring Suez water using YAML is being removed but there was an connection error importing your YAML configuration.\n\nEnsure connection to Suez water works and restart Home Assistant to try again or remove the Suez water YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually." + "description": "Configuring Suez water using YAML is being removed but there was a connection error importing your YAML configuration.\n\nEnsure connection to Suez water works and restart Home Assistant to try again or remove the Suez water YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually." }, "deprecated_yaml_import_issue_unknown": { "title": "The Suez water YAML configuration import failed", diff --git a/homeassistant/components/sun/__init__.py b/homeassistant/components/sun/__init__.py index feb68d76f6a..6308594f4bd 100644 --- a/homeassistant/components/sun/__init__.py +++ b/homeassistant/components/sun/__init__.py @@ -1,95 +1,42 @@ """Support for functionality to keep track of the sun.""" + from __future__ import annotations -from datetime import datetime, timedelta -import logging -from typing import Any - -from astral.location import Elevation, Location - from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import ( - EVENT_CORE_CONFIG_UPDATE, - SUN_EVENT_SUNRISE, - SUN_EVENT_SUNSET, - Platform, -) -from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback -from homeassistant.helpers import config_validation as cv, event -from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.sun import ( - get_astral_location, - get_location_astral_event_next, -) +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.typing import ConfigType -from homeassistant.util import dt as dt_util - -from .const import DOMAIN, SIGNAL_EVENTS_CHANGED, SIGNAL_POSITION_CHANGED - -_LOGGER = logging.getLogger(__name__) - -ENTITY_ID = "sun.sun" - -STATE_ABOVE_HORIZON = "above_horizon" -STATE_BELOW_HORIZON = "below_horizon" - -STATE_ATTR_AZIMUTH = "azimuth" -STATE_ATTR_ELEVATION = "elevation" -STATE_ATTR_RISING = "rising" -STATE_ATTR_NEXT_DAWN = "next_dawn" -STATE_ATTR_NEXT_DUSK = "next_dusk" -STATE_ATTR_NEXT_MIDNIGHT = "next_midnight" -STATE_ATTR_NEXT_NOON = "next_noon" -STATE_ATTR_NEXT_RISING = "next_rising" -STATE_ATTR_NEXT_SETTING = "next_setting" - -# The algorithm used here is somewhat complicated. It aims to cut down -# the number of sensor updates over the day. It's documented best in -# the PR for the change, see the Discussion section of: -# https://github.com/home-assistant/core/pull/23832 - - -# As documented in wikipedia: https://en.wikipedia.org/wiki/Twilight -# sun is: -# < -18° of horizon - all stars visible -PHASE_NIGHT = "night" -# 18°-12° - some stars not visible -PHASE_ASTRONOMICAL_TWILIGHT = "astronomical_twilight" -# 12°-6° - horizon visible -PHASE_NAUTICAL_TWILIGHT = "nautical_twilight" -# 6°-0° - objects visible -PHASE_TWILIGHT = "twilight" -# 0°-10° above horizon, sun low on horizon -PHASE_SMALL_DAY = "small_day" -# > 10° above horizon -PHASE_DAY = "day" - -# 4 mins is one degree of arc change of the sun on its circle. -# During the night and the middle of the day we don't update -# that much since it's not important. -_PHASE_UPDATES = { - PHASE_NIGHT: timedelta(minutes=4 * 5), - PHASE_ASTRONOMICAL_TWILIGHT: timedelta(minutes=4 * 2), - PHASE_NAUTICAL_TWILIGHT: timedelta(minutes=4 * 2), - PHASE_TWILIGHT: timedelta(minutes=4), - PHASE_SMALL_DAY: timedelta(minutes=2), - PHASE_DAY: timedelta(minutes=4), -} +# The sensor platform is pre-imported here to ensure +# it gets loaded when the base component is loaded +# as we will always load it and we do not want to have +# to wait for the import executor when its busy later +# in the startup process. +from . import sensor as sensor_pre_import # noqa: F401 +from .const import ( # noqa: F401 # noqa: F401 + DOMAIN, + STATE_ABOVE_HORIZON, + STATE_BELOW_HORIZON, +) +from .entity import Sun CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN) async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Track the state of the sun.""" - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data=config, + if not hass.config_entries.async_entries(DOMAIN): + # We avoid creating an import flow if its already + # setup since it will have to import the config_flow + # module. + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=config, + ) ) - ) return True @@ -109,221 +56,3 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: sun.remove_listeners() hass.states.async_remove(sun.entity_id) return unload_ok - - -class Sun(Entity): - """Representation of the Sun.""" - - _unrecorded_attributes = frozenset( - { - STATE_ATTR_AZIMUTH, - STATE_ATTR_ELEVATION, - STATE_ATTR_RISING, - STATE_ATTR_NEXT_DAWN, - STATE_ATTR_NEXT_DUSK, - STATE_ATTR_NEXT_MIDNIGHT, - STATE_ATTR_NEXT_NOON, - STATE_ATTR_NEXT_RISING, - STATE_ATTR_NEXT_SETTING, - } - ) - - _attr_name = "Sun" - entity_id = ENTITY_ID - # This entity is legacy and does not have a platform. - # We can't fix this easily without breaking changes. - _no_platform_reported = True - - location: Location - elevation: Elevation - next_rising: datetime - next_setting: datetime - next_dawn: datetime - next_dusk: datetime - next_midnight: datetime - next_noon: datetime - solar_elevation: float - solar_azimuth: float - rising: bool - _next_change: datetime - - def __init__(self, hass: HomeAssistant) -> None: - """Initialize the sun.""" - self.hass = hass - self.phase: str | None = None - - # This is normally done by async_internal_added_to_hass which is not called - # for sun because sun has no platform - self._state_info = { - "unrecorded_attributes": self._Entity__combined_unrecorded_attributes # type: ignore[attr-defined] - } - - self._config_listener: CALLBACK_TYPE | None = None - self._update_events_listener: CALLBACK_TYPE | None = None - self._update_sun_position_listener: CALLBACK_TYPE | None = None - self._config_listener = self.hass.bus.async_listen( - EVENT_CORE_CONFIG_UPDATE, self.update_location - ) - self.update_location(initial=True) - - @callback - def update_location(self, _: Event | None = None, initial: bool = False) -> None: - """Update location.""" - location, elevation = get_astral_location(self.hass) - if not initial and location == self.location: - return - self.location = location - self.elevation = elevation - if self._update_events_listener: - self._update_events_listener() - self.update_events() - - @callback - def remove_listeners(self) -> None: - """Remove listeners.""" - if self._config_listener: - self._config_listener() - if self._update_events_listener: - self._update_events_listener() - if self._update_sun_position_listener: - self._update_sun_position_listener() - - @property - def state(self) -> str: - """Return the state of the sun.""" - # 0.8333 is the same value as astral uses - if self.solar_elevation > -0.833: - return STATE_ABOVE_HORIZON - - return STATE_BELOW_HORIZON - - @property - def extra_state_attributes(self) -> dict[str, Any]: - """Return the state attributes of the sun.""" - return { - STATE_ATTR_NEXT_DAWN: self.next_dawn.isoformat(), - STATE_ATTR_NEXT_DUSK: self.next_dusk.isoformat(), - STATE_ATTR_NEXT_MIDNIGHT: self.next_midnight.isoformat(), - STATE_ATTR_NEXT_NOON: self.next_noon.isoformat(), - STATE_ATTR_NEXT_RISING: self.next_rising.isoformat(), - STATE_ATTR_NEXT_SETTING: self.next_setting.isoformat(), - STATE_ATTR_ELEVATION: self.solar_elevation, - STATE_ATTR_AZIMUTH: self.solar_azimuth, - STATE_ATTR_RISING: self.rising, - } - - def _check_event( - self, utc_point_in_time: datetime, sun_event: str, before: str | None - ) -> datetime: - next_utc = get_location_astral_event_next( - self.location, self.elevation, sun_event, utc_point_in_time - ) - if next_utc < self._next_change: - self._next_change = next_utc - self.phase = before - return next_utc - - @callback - def update_events(self, now: datetime | None = None) -> None: - """Update the attributes containing solar events.""" - # Grab current time in case system clock changed since last time we ran. - utc_point_in_time = dt_util.utcnow() - self._next_change = utc_point_in_time + timedelta(days=400) - - # Work our way around the solar cycle, figure out the next - # phase. Some of these are stored. - self.location.solar_depression = "astronomical" - self._check_event(utc_point_in_time, "dawn", PHASE_NIGHT) - self.location.solar_depression = "nautical" - self._check_event(utc_point_in_time, "dawn", PHASE_ASTRONOMICAL_TWILIGHT) - self.location.solar_depression = "civil" - self.next_dawn = self._check_event( - utc_point_in_time, "dawn", PHASE_NAUTICAL_TWILIGHT - ) - self.next_rising = self._check_event( - utc_point_in_time, SUN_EVENT_SUNRISE, PHASE_TWILIGHT - ) - self.location.solar_depression = -10 - self._check_event(utc_point_in_time, "dawn", PHASE_SMALL_DAY) - self.next_noon = self._check_event(utc_point_in_time, "noon", None) - self._check_event(utc_point_in_time, "dusk", PHASE_DAY) - self.next_setting = self._check_event( - utc_point_in_time, SUN_EVENT_SUNSET, PHASE_SMALL_DAY - ) - self.location.solar_depression = "civil" - self.next_dusk = self._check_event(utc_point_in_time, "dusk", PHASE_TWILIGHT) - self.location.solar_depression = "nautical" - self._check_event(utc_point_in_time, "dusk", PHASE_NAUTICAL_TWILIGHT) - self.location.solar_depression = "astronomical" - self._check_event(utc_point_in_time, "dusk", PHASE_ASTRONOMICAL_TWILIGHT) - self.next_midnight = self._check_event(utc_point_in_time, "midnight", None) - self.location.solar_depression = "civil" - - # if the event was solar midday or midnight, phase will now - # be None. Solar noon doesn't always happen when the sun is - # even in the day at the poles, so we can't rely on it. - # Need to calculate phase if next is noon or midnight - if self.phase is None: - elevation = self.location.solar_elevation(self._next_change, self.elevation) - if elevation >= 10: - self.phase = PHASE_DAY - elif elevation >= 0: - self.phase = PHASE_SMALL_DAY - elif elevation >= -6: - self.phase = PHASE_TWILIGHT - elif elevation >= -12: - self.phase = PHASE_NAUTICAL_TWILIGHT - elif elevation >= -18: - self.phase = PHASE_ASTRONOMICAL_TWILIGHT - else: - self.phase = PHASE_NIGHT - - self.rising = self.next_noon < self.next_midnight - - _LOGGER.debug( - "sun phase_update@%s: phase=%s", utc_point_in_time.isoformat(), self.phase - ) - if self._update_sun_position_listener: - self._update_sun_position_listener() - self.update_sun_position() - async_dispatcher_send(self.hass, SIGNAL_EVENTS_CHANGED) - - # Set timer for the next solar event - self._update_events_listener = event.async_track_point_in_utc_time( - self.hass, self.update_events, self._next_change - ) - _LOGGER.debug("next time: %s", self._next_change.isoformat()) - - @callback - def update_sun_position(self, now: datetime | None = None) -> None: - """Calculate the position of the sun.""" - # Grab current time in case system clock changed since last time we ran. - utc_point_in_time = dt_util.utcnow() - self.solar_azimuth = round( - self.location.solar_azimuth(utc_point_in_time, self.elevation), 2 - ) - self.solar_elevation = round( - self.location.solar_elevation(utc_point_in_time, self.elevation), 2 - ) - - _LOGGER.debug( - "sun position_update@%s: elevation=%s azimuth=%s", - utc_point_in_time.isoformat(), - self.solar_elevation, - self.solar_azimuth, - ) - self.async_write_ha_state() - - async_dispatcher_send(self.hass, SIGNAL_POSITION_CHANGED) - - # Next update as per the current phase - assert self.phase - delta = _PHASE_UPDATES[self.phase] - # if the next update is within 1.25 of the next - # position update just drop it - if utc_point_in_time + delta * 1.25 > self._next_change: - self._update_sun_position_listener = None - return - self._update_sun_position_listener = event.async_track_point_in_utc_time( - self.hass, self.update_sun_position, utc_point_in_time + delta - ) diff --git a/homeassistant/components/sun/config_flow.py b/homeassistant/components/sun/config_flow.py index ae2e5a42efc..30b64c60b9f 100644 --- a/homeassistant/components/sun/config_flow.py +++ b/homeassistant/components/sun/config_flow.py @@ -1,10 +1,10 @@ """Config flow to configure the Sun integration.""" + from __future__ import annotations from typing import Any -from homeassistant.config_entries import ConfigFlow -from homeassistant.data_entry_flow import FlowResult +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from .const import DEFAULT_NAME, DOMAIN @@ -16,16 +16,13 @@ class SunConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" - if self._async_current_entries(): - return self.async_abort(reason="single_instance_allowed") - if user_input is not None: return self.async_create_entry(title=DEFAULT_NAME, data={}) return self.async_show_form(step_id="user") - async def async_step_import(self, user_input: dict[str, Any]) -> FlowResult: + async def async_step_import(self, user_input: dict[str, Any]) -> ConfigFlowResult: """Handle import from configuration.yaml.""" return await self.async_step_user(user_input) diff --git a/homeassistant/components/sun/const.py b/homeassistant/components/sun/const.py index 245f8ca1d58..949bd4e2fbb 100644 --- a/homeassistant/components/sun/const.py +++ b/homeassistant/components/sun/const.py @@ -1,4 +1,5 @@ """Constants for the Sun integration.""" + from typing import Final DOMAIN: Final = "sun" @@ -7,3 +8,18 @@ DEFAULT_NAME: Final = "Sun" SIGNAL_POSITION_CHANGED = f"{DOMAIN}_position_changed" SIGNAL_EVENTS_CHANGED = f"{DOMAIN}_events_changed" + + +STATE_ABOVE_HORIZON = "above_horizon" +STATE_BELOW_HORIZON = "below_horizon" + + +STATE_ATTR_AZIMUTH = "azimuth" +STATE_ATTR_ELEVATION = "elevation" +STATE_ATTR_RISING = "rising" +STATE_ATTR_NEXT_DAWN = "next_dawn" +STATE_ATTR_NEXT_DUSK = "next_dusk" +STATE_ATTR_NEXT_MIDNIGHT = "next_midnight" +STATE_ATTR_NEXT_NOON = "next_noon" +STATE_ATTR_NEXT_RISING = "next_rising" +STATE_ATTR_NEXT_SETTING = "next_setting" diff --git a/homeassistant/components/sun/entity.py b/homeassistant/components/sun/entity.py new file mode 100644 index 00000000000..739784697e0 --- /dev/null +++ b/homeassistant/components/sun/entity.py @@ -0,0 +1,296 @@ +"""Support for functionality to keep track of the sun.""" + +from __future__ import annotations + +from datetime import datetime, timedelta +import logging +from typing import Any + +from astral.location import Elevation, Location + +from homeassistant.const import ( + EVENT_CORE_CONFIG_UPDATE, + SUN_EVENT_SUNRISE, + SUN_EVENT_SUNSET, +) +from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback +from homeassistant.helpers import event +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.sun import ( + get_astral_location, + get_location_astral_event_next, +) +from homeassistant.util import dt as dt_util + +from .const import ( + SIGNAL_EVENTS_CHANGED, + SIGNAL_POSITION_CHANGED, + STATE_ABOVE_HORIZON, + STATE_BELOW_HORIZON, +) + +_LOGGER = logging.getLogger(__name__) + +ENTITY_ID = "sun.sun" + +STATE_ATTR_AZIMUTH = "azimuth" +STATE_ATTR_ELEVATION = "elevation" +STATE_ATTR_RISING = "rising" +STATE_ATTR_NEXT_DAWN = "next_dawn" +STATE_ATTR_NEXT_DUSK = "next_dusk" +STATE_ATTR_NEXT_MIDNIGHT = "next_midnight" +STATE_ATTR_NEXT_NOON = "next_noon" +STATE_ATTR_NEXT_RISING = "next_rising" +STATE_ATTR_NEXT_SETTING = "next_setting" + +# The algorithm used here is somewhat complicated. It aims to cut down +# the number of sensor updates over the day. It's documented best in +# the PR for the change, see the Discussion section of: +# https://github.com/home-assistant/core/pull/23832 + + +# As documented in wikipedia: https://en.wikipedia.org/wiki/Twilight +# sun is: +# < -18° of horizon - all stars visible +PHASE_NIGHT = "night" +# 18°-12° - some stars not visible +PHASE_ASTRONOMICAL_TWILIGHT = "astronomical_twilight" +# 12°-6° - horizon visible +PHASE_NAUTICAL_TWILIGHT = "nautical_twilight" +# 6°-0° - objects visible +PHASE_TWILIGHT = "twilight" +# 0°-10° above horizon, sun low on horizon +PHASE_SMALL_DAY = "small_day" +# > 10° above horizon +PHASE_DAY = "day" + +# 4 mins is one degree of arc change of the sun on its circle. +# During the night and the middle of the day we don't update +# that much since it's not important. +_PHASE_UPDATES = { + PHASE_NIGHT: timedelta(minutes=4 * 5), + PHASE_ASTRONOMICAL_TWILIGHT: timedelta(minutes=4 * 2), + PHASE_NAUTICAL_TWILIGHT: timedelta(minutes=4 * 2), + PHASE_TWILIGHT: timedelta(minutes=4), + PHASE_SMALL_DAY: timedelta(minutes=2), + PHASE_DAY: timedelta(minutes=4), +} + + +class Sun(Entity): + """Representation of the Sun.""" + + _unrecorded_attributes = frozenset( + { + STATE_ATTR_AZIMUTH, + STATE_ATTR_ELEVATION, + STATE_ATTR_RISING, + STATE_ATTR_NEXT_DAWN, + STATE_ATTR_NEXT_DUSK, + STATE_ATTR_NEXT_MIDNIGHT, + STATE_ATTR_NEXT_NOON, + STATE_ATTR_NEXT_RISING, + STATE_ATTR_NEXT_SETTING, + } + ) + + _attr_name = "Sun" + entity_id = ENTITY_ID + # This entity is legacy and does not have a platform. + # We can't fix this easily without breaking changes. + _no_platform_reported = True + + location: Location + elevation: Elevation + next_rising: datetime + next_setting: datetime + next_dawn: datetime + next_dusk: datetime + next_midnight: datetime + next_noon: datetime + solar_elevation: float + solar_azimuth: float + rising: bool + _next_change: datetime + + def __init__(self, hass: HomeAssistant) -> None: + """Initialize the sun.""" + self.hass = hass + self.phase: str | None = None + + # This is normally done by async_internal_added_to_hass which is not called + # for sun because sun has no platform + self._state_info = { + "unrecorded_attributes": self._Entity__combined_unrecorded_attributes # type: ignore[attr-defined] + } + + self._config_listener: CALLBACK_TYPE | None = None + self._update_events_listener: CALLBACK_TYPE | None = None + self._update_sun_position_listener: CALLBACK_TYPE | None = None + self._config_listener = self.hass.bus.async_listen( + EVENT_CORE_CONFIG_UPDATE, self.update_location + ) + self.update_location(initial=True) + + @callback + def update_location(self, _: Event | None = None, initial: bool = False) -> None: + """Update location.""" + location, elevation = get_astral_location(self.hass) + if not initial and location == self.location: + return + self.location = location + self.elevation = elevation + if self._update_events_listener: + self._update_events_listener() + self.update_events() + + @callback + def remove_listeners(self) -> None: + """Remove listeners.""" + if self._config_listener: + self._config_listener() + if self._update_events_listener: + self._update_events_listener() + if self._update_sun_position_listener: + self._update_sun_position_listener() + + @property + def state(self) -> str: + """Return the state of the sun.""" + # 0.8333 is the same value as astral uses + if self.solar_elevation > -0.833: + return STATE_ABOVE_HORIZON + + return STATE_BELOW_HORIZON + + @property + def extra_state_attributes(self) -> dict[str, Any]: + """Return the state attributes of the sun.""" + return { + STATE_ATTR_NEXT_DAWN: self.next_dawn.isoformat(), + STATE_ATTR_NEXT_DUSK: self.next_dusk.isoformat(), + STATE_ATTR_NEXT_MIDNIGHT: self.next_midnight.isoformat(), + STATE_ATTR_NEXT_NOON: self.next_noon.isoformat(), + STATE_ATTR_NEXT_RISING: self.next_rising.isoformat(), + STATE_ATTR_NEXT_SETTING: self.next_setting.isoformat(), + STATE_ATTR_ELEVATION: self.solar_elevation, + STATE_ATTR_AZIMUTH: self.solar_azimuth, + STATE_ATTR_RISING: self.rising, + } + + def _check_event( + self, utc_point_in_time: datetime, sun_event: str, before: str | None + ) -> datetime: + next_utc = get_location_astral_event_next( + self.location, self.elevation, sun_event, utc_point_in_time + ) + if next_utc < self._next_change: + self._next_change = next_utc + self.phase = before + return next_utc + + @callback + def update_events(self, now: datetime | None = None) -> None: + """Update the attributes containing solar events.""" + # Grab current time in case system clock changed since last time we ran. + utc_point_in_time = dt_util.utcnow() + self._next_change = utc_point_in_time + timedelta(days=400) + + # Work our way around the solar cycle, figure out the next + # phase. Some of these are stored. + self.location.solar_depression = "astronomical" + self._check_event(utc_point_in_time, "dawn", PHASE_NIGHT) + self.location.solar_depression = "nautical" + self._check_event(utc_point_in_time, "dawn", PHASE_ASTRONOMICAL_TWILIGHT) + self.location.solar_depression = "civil" + self.next_dawn = self._check_event( + utc_point_in_time, "dawn", PHASE_NAUTICAL_TWILIGHT + ) + self.next_rising = self._check_event( + utc_point_in_time, SUN_EVENT_SUNRISE, PHASE_TWILIGHT + ) + self.location.solar_depression = -10 + self._check_event(utc_point_in_time, "dawn", PHASE_SMALL_DAY) + self.next_noon = self._check_event(utc_point_in_time, "noon", None) + self._check_event(utc_point_in_time, "dusk", PHASE_DAY) + self.next_setting = self._check_event( + utc_point_in_time, SUN_EVENT_SUNSET, PHASE_SMALL_DAY + ) + self.location.solar_depression = "civil" + self.next_dusk = self._check_event(utc_point_in_time, "dusk", PHASE_TWILIGHT) + self.location.solar_depression = "nautical" + self._check_event(utc_point_in_time, "dusk", PHASE_NAUTICAL_TWILIGHT) + self.location.solar_depression = "astronomical" + self._check_event(utc_point_in_time, "dusk", PHASE_ASTRONOMICAL_TWILIGHT) + self.next_midnight = self._check_event(utc_point_in_time, "midnight", None) + self.location.solar_depression = "civil" + + # if the event was solar midday or midnight, phase will now + # be None. Solar noon doesn't always happen when the sun is + # even in the day at the poles, so we can't rely on it. + # Need to calculate phase if next is noon or midnight + if self.phase is None: + elevation = self.location.solar_elevation(self._next_change, self.elevation) + if elevation >= 10: + self.phase = PHASE_DAY + elif elevation >= 0: + self.phase = PHASE_SMALL_DAY + elif elevation >= -6: + self.phase = PHASE_TWILIGHT + elif elevation >= -12: + self.phase = PHASE_NAUTICAL_TWILIGHT + elif elevation >= -18: + self.phase = PHASE_ASTRONOMICAL_TWILIGHT + else: + self.phase = PHASE_NIGHT + + self.rising = self.next_noon < self.next_midnight + + _LOGGER.debug( + "sun phase_update@%s: phase=%s", utc_point_in_time.isoformat(), self.phase + ) + if self._update_sun_position_listener: + self._update_sun_position_listener() + self.update_sun_position() + async_dispatcher_send(self.hass, SIGNAL_EVENTS_CHANGED) + + # Set timer for the next solar event + self._update_events_listener = event.async_track_point_in_utc_time( + self.hass, self.update_events, self._next_change + ) + _LOGGER.debug("next time: %s", self._next_change.isoformat()) + + @callback + def update_sun_position(self, now: datetime | None = None) -> None: + """Calculate the position of the sun.""" + # Grab current time in case system clock changed since last time we ran. + utc_point_in_time = dt_util.utcnow() + self.solar_azimuth = round( + self.location.solar_azimuth(utc_point_in_time, self.elevation), 2 + ) + self.solar_elevation = round( + self.location.solar_elevation(utc_point_in_time, self.elevation), 2 + ) + + _LOGGER.debug( + "sun position_update@%s: elevation=%s azimuth=%s", + utc_point_in_time.isoformat(), + self.solar_elevation, + self.solar_azimuth, + ) + self.async_write_ha_state() + + async_dispatcher_send(self.hass, SIGNAL_POSITION_CHANGED) + + # Next update as per the current phase + assert self.phase + delta = _PHASE_UPDATES[self.phase] + # if the next update is within 1.25 of the next + # position update just drop it + if utc_point_in_time + delta * 1.25 > self._next_change: + self._update_sun_position_listener = None + return + self._update_sun_position_listener = event.async_track_point_in_utc_time( + self.hass, self.update_sun_position, utc_point_in_time + delta + ) diff --git a/homeassistant/components/sun/manifest.json b/homeassistant/components/sun/manifest.json index 2fdcaafe114..f6b4ae1976b 100644 --- a/homeassistant/components/sun/manifest.json +++ b/homeassistant/components/sun/manifest.json @@ -5,5 +5,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sun", "iot_class": "calculated", - "quality_scale": "internal" + "quality_scale": "internal", + "single_config_entry": true } diff --git a/homeassistant/components/sun/sensor.py b/homeassistant/components/sun/sensor.py index 2a21b9d0246..018ba4fa994 100644 --- a/homeassistant/components/sun/sensor.py +++ b/homeassistant/components/sun/sensor.py @@ -1,4 +1,5 @@ """Sensor platform for Sun integration.""" + from __future__ import annotations from collections.abc import Callable @@ -20,8 +21,8 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType -from . import Sun from .const import DOMAIN, SIGNAL_EVENTS_CHANGED, SIGNAL_POSITION_CHANGED +from .entity import Sun ENTITY_ID_SENSOR_FORMAT = SENSOR_DOMAIN + ".sun_{}" diff --git a/homeassistant/components/sun/strings.json b/homeassistant/components/sun/strings.json index eb538eedf09..7c7accd8cc6 100644 --- a/homeassistant/components/sun/strings.json +++ b/homeassistant/components/sun/strings.json @@ -5,9 +5,6 @@ "user": { "description": "[%key:common::config_flow::description::confirm_setup%]" } - }, - "abort": { - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" } }, "entity_component": { diff --git a/homeassistant/components/sun/trigger.py b/homeassistant/components/sun/trigger.py index 3cc3cabfbd3..7724816d636 100644 --- a/homeassistant/components/sun/trigger.py +++ b/homeassistant/components/sun/trigger.py @@ -1,4 +1,5 @@ """Offer sun based automation rules.""" + from datetime import timedelta import voluptuous as vol diff --git a/homeassistant/components/sunweg/__init__.py b/homeassistant/components/sunweg/__init__.py index 9da91ccda0f..6c39a04127e 100644 --- a/homeassistant/components/sunweg/__init__.py +++ b/homeassistant/components/sunweg/__init__.py @@ -1,4 +1,5 @@ """The Sun WEG inverter sensor integration.""" + import datetime import json import logging diff --git a/homeassistant/components/sunweg/config_flow.py b/homeassistant/components/sunweg/config_flow.py index cd24a4722e9..c4af05a0cc9 100644 --- a/homeassistant/components/sunweg/config_flow.py +++ b/homeassistant/components/sunweg/config_flow.py @@ -1,16 +1,16 @@ """Config flow for Sun WEG integration.""" + from sunweg.api import APIHelper import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult from .const import CONF_PLANT_ID, DOMAIN -class SunWEGConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class SunWEGConfigFlow(ConfigFlow, domain=DOMAIN): """Config flow class.""" VERSION = 1 @@ -21,7 +21,7 @@ class SunWEGConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.data: dict = {} @callback - def _async_show_user_form(self, errors=None) -> FlowResult: + def _async_show_user_form(self, errors=None) -> ConfigFlowResult: """Show the form to the user.""" data_schema = vol.Schema( { @@ -34,7 +34,7 @@ class SunWEGConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="user", data_schema=data_schema, errors=errors ) - async def async_step_user(self, user_input=None) -> FlowResult: + async def async_step_user(self, user_input=None) -> ConfigFlowResult: """Handle the start of the config flow.""" if not user_input: return self._async_show_user_form() @@ -50,7 +50,7 @@ class SunWEGConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.data = user_input return await self.async_step_plant() - async def async_step_plant(self, user_input=None) -> FlowResult: + async def async_step_plant(self, user_input=None) -> ConfigFlowResult: """Handle adding a "plant" to Home Assistant.""" plant_list = await self.hass.async_add_executor_job(self.api.listPlants) diff --git a/homeassistant/components/sunweg/const.py b/homeassistant/components/sunweg/const.py index e4b2b242abf..11d24352962 100644 --- a/homeassistant/components/sunweg/const.py +++ b/homeassistant/components/sunweg/const.py @@ -1,4 +1,5 @@ """Define constants for the Sun WEG component.""" + from enum import Enum from homeassistant.const import Platform diff --git a/homeassistant/components/sunweg/manifest.json b/homeassistant/components/sunweg/manifest.json index b681ecc6d5f..3e41d331e8c 100644 --- a/homeassistant/components/sunweg/manifest.json +++ b/homeassistant/components/sunweg/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/sunweg/", "iot_class": "cloud_polling", "loggers": ["sunweg"], - "requirements": ["sunweg==2.1.0"] + "requirements": ["sunweg==2.1.1"] } diff --git a/homeassistant/components/sunweg/sensor.py b/homeassistant/components/sunweg/sensor.py index 42a3dc33d2b..004dd7276a7 100644 --- a/homeassistant/components/sunweg/sensor.py +++ b/homeassistant/components/sunweg/sensor.py @@ -1,4 +1,5 @@ """Read status of SunWEG inverters.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/sunweg/sensor_types/inverter.py b/homeassistant/components/sunweg/sensor_types/inverter.py index f406efb1a83..1010488b38a 100644 --- a/homeassistant/components/sunweg/sensor_types/inverter.py +++ b/homeassistant/components/sunweg/sensor_types/inverter.py @@ -1,4 +1,5 @@ """SunWEG Sensor definitions for the Inverter type.""" + from __future__ import annotations from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass diff --git a/homeassistant/components/sunweg/sensor_types/phase.py b/homeassistant/components/sunweg/sensor_types/phase.py index ca6b9374e0d..d9db6c7c714 100644 --- a/homeassistant/components/sunweg/sensor_types/phase.py +++ b/homeassistant/components/sunweg/sensor_types/phase.py @@ -1,4 +1,5 @@ """SunWEG Sensor definitions for the Phase type.""" + from __future__ import annotations from homeassistant.components.sensor import SensorDeviceClass diff --git a/homeassistant/components/sunweg/sensor_types/sensor_entity_description.py b/homeassistant/components/sunweg/sensor_types/sensor_entity_description.py index a47818b694b..8c792ab617f 100644 --- a/homeassistant/components/sunweg/sensor_types/sensor_entity_description.py +++ b/homeassistant/components/sunweg/sensor_types/sensor_entity_description.py @@ -1,4 +1,5 @@ """Sensor Entity Description for the SunWEG integration.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/homeassistant/components/sunweg/sensor_types/string.py b/homeassistant/components/sunweg/sensor_types/string.py index d3ee0a43c21..ec59da5d20d 100644 --- a/homeassistant/components/sunweg/sensor_types/string.py +++ b/homeassistant/components/sunweg/sensor_types/string.py @@ -1,4 +1,5 @@ """SunWEG Sensor definitions for the String type.""" + from __future__ import annotations from homeassistant.components.sensor import SensorDeviceClass diff --git a/homeassistant/components/sunweg/sensor_types/total.py b/homeassistant/components/sunweg/sensor_types/total.py index ed9d6171735..5ae8be6dba3 100644 --- a/homeassistant/components/sunweg/sensor_types/total.py +++ b/homeassistant/components/sunweg/sensor_types/total.py @@ -1,4 +1,5 @@ """SunWEG Sensor definitions for Totals.""" + from __future__ import annotations from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass diff --git a/homeassistant/components/supervisord/sensor.py b/homeassistant/components/supervisord/sensor.py index ce7efb11a3a..7939232cd6f 100644 --- a/homeassistant/components/supervisord/sensor.py +++ b/homeassistant/components/supervisord/sensor.py @@ -1,4 +1,5 @@ """Sensor for Supervisord process status.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/supla/__init__.py b/homeassistant/components/supla/__init__.py index 9652cae4aa4..46a3ec2b2c0 100644 --- a/homeassistant/components/supla/__init__.py +++ b/homeassistant/components/supla/__init__.py @@ -1,4 +1,5 @@ """Support for Supla devices.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/supla/cover.py b/homeassistant/components/supla/cover.py index 7f2857395b8..4cdee04b149 100644 --- a/homeassistant/components/supla/cover.py +++ b/homeassistant/components/supla/cover.py @@ -1,4 +1,5 @@ """Support for SUPLA covers - curtains, rollershutters, entry gate etc.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/supla/entity.py b/homeassistant/components/supla/entity.py index 244048973fa..fa257e39a06 100644 --- a/homeassistant/components/supla/entity.py +++ b/homeassistant/components/supla/entity.py @@ -1,4 +1,5 @@ """Base class for SUPLA channels.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/supla/switch.py b/homeassistant/components/supla/switch.py index d904455a3fe..5afcb9f08f6 100644 --- a/homeassistant/components/supla/switch.py +++ b/homeassistant/components/supla/switch.py @@ -1,4 +1,5 @@ """Support for SUPLA switch.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/surepetcare/__init__.py b/homeassistant/components/surepetcare/__init__.py index d4c337c4096..b9e2bb6a410 100644 --- a/homeassistant/components/surepetcare/__init__.py +++ b/homeassistant/components/surepetcare/__init__.py @@ -1,4 +1,5 @@ """The surepetcare integration.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/surepetcare/binary_sensor.py b/homeassistant/components/surepetcare/binary_sensor.py index 8cb750dc1d1..0c99985d514 100644 --- a/homeassistant/components/surepetcare/binary_sensor.py +++ b/homeassistant/components/surepetcare/binary_sensor.py @@ -1,4 +1,5 @@ """Support for Sure PetCare Flaps/Pets binary sensors.""" + from __future__ import annotations from typing import cast diff --git a/homeassistant/components/surepetcare/config_flow.py b/homeassistant/components/surepetcare/config_flow.py index 81607b582c1..dc11631de81 100644 --- a/homeassistant/components/surepetcare/config_flow.py +++ b/homeassistant/components/surepetcare/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Sure Petcare integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -9,10 +10,9 @@ import surepy from surepy.exceptions import SurePetcareAuthenticationError, SurePetcareError import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN, SURE_API_TIMEOUT @@ -42,7 +42,7 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, return {CONF_TOKEN: token} -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class SurePetCareConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Sure Petcare.""" VERSION = 1 @@ -53,7 +53,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" if user_input is None: return self.async_show_form(step_id="user", data_schema=USER_DATA_SCHEMA) @@ -83,14 +83,16 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="user", data_schema=USER_DATA_SCHEMA, errors=errors ) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Handle configuration by re-auth.""" self._username = entry_data[CONF_USERNAME] return await self.async_step_reauth_confirm() async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Dialog that informs the user that reauth is required.""" errors = {} if user_input is not None: diff --git a/homeassistant/components/surepetcare/const.py b/homeassistant/components/surepetcare/const.py index 6617137b026..e0940b41002 100644 --- a/homeassistant/components/surepetcare/const.py +++ b/homeassistant/components/surepetcare/const.py @@ -1,4 +1,5 @@ """Constants for the Sure Petcare component.""" + DOMAIN = "surepetcare" CONF_FEEDERS = "feeders" diff --git a/homeassistant/components/surepetcare/entity.py b/homeassistant/components/surepetcare/entity.py index e6a44d5bfa9..400f6a80ac9 100644 --- a/homeassistant/components/surepetcare/entity.py +++ b/homeassistant/components/surepetcare/entity.py @@ -1,4 +1,5 @@ """Entity for Surepetcare.""" + from __future__ import annotations from abc import abstractmethod diff --git a/homeassistant/components/surepetcare/icons.json b/homeassistant/components/surepetcare/icons.json new file mode 100644 index 00000000000..1db15b599df --- /dev/null +++ b/homeassistant/components/surepetcare/icons.json @@ -0,0 +1,6 @@ +{ + "services": { + "set_lock_state": "mdi:lock", + "set_pet_location": "mdi:dog" + } +} diff --git a/homeassistant/components/surepetcare/lock.py b/homeassistant/components/surepetcare/lock.py index 3161fa6e0bc..b933cc40637 100644 --- a/homeassistant/components/surepetcare/lock.py +++ b/homeassistant/components/surepetcare/lock.py @@ -1,4 +1,5 @@ """Support for Sure PetCare Flaps locks.""" + from __future__ import annotations from typing import Any @@ -22,25 +23,18 @@ async def async_setup_entry( ) -> None: """Set up Sure PetCare locks on a config entry.""" - entities: list[SurePetcareLock] = [] - coordinator: SurePetcareDataCoordinator = hass.data[DOMAIN][entry.entry_id] - for surepy_entity in coordinator.data.values(): - if surepy_entity.type not in [ - EntityType.CAT_FLAP, - EntityType.PET_FLAP, - ]: - continue - + async_add_entities( + SurePetcareLock(surepy_entity.id, coordinator, lock_state) + for surepy_entity in coordinator.data.values() + if surepy_entity.type in [EntityType.CAT_FLAP, EntityType.PET_FLAP] for lock_state in ( LockState.LOCKED_IN, LockState.LOCKED_OUT, LockState.LOCKED_ALL, - ): - entities.append(SurePetcareLock(surepy_entity.id, coordinator, lock_state)) - - async_add_entities(entities) + ) + ) class SurePetcareLock(SurePetcareEntity, LockEntity): diff --git a/homeassistant/components/surepetcare/sensor.py b/homeassistant/components/surepetcare/sensor.py index fbd228619cd..3618ac7d163 100644 --- a/homeassistant/components/surepetcare/sensor.py +++ b/homeassistant/components/surepetcare/sensor.py @@ -1,4 +1,5 @@ """Support for Sure PetCare Flaps/Pets sensors.""" + from __future__ import annotations from typing import cast diff --git a/homeassistant/components/swiss_hydrological_data/sensor.py b/homeassistant/components/swiss_hydrological_data/sensor.py index d51ce18ada4..e74d1f66046 100644 --- a/homeassistant/components/swiss_hydrological_data/sensor.py +++ b/homeassistant/components/swiss_hydrological_data/sensor.py @@ -1,4 +1,5 @@ """Support for hydrological data from the Fed. Office for the Environment.""" + from __future__ import annotations from datetime import timedelta @@ -72,12 +73,13 @@ def setup_platform( _LOGGER.error("The station doesn't exists: %s", station) return - entities = [] - - for condition in monitored_conditions: - entities.append(SwissHydrologicalDataSensor(hydro_data, station, condition)) - - add_entities(entities, True) + add_entities( + ( + SwissHydrologicalDataSensor(hydro_data, station, condition) + for condition in monitored_conditions + ), + True, + ) class SwissHydrologicalDataSensor(SensorEntity): diff --git a/homeassistant/components/swiss_public_transport/__init__.py b/homeassistant/components/swiss_public_transport/__init__.py index d0713ddf1d1..74a7d90cfb2 100644 --- a/homeassistant/components/swiss_public_transport/__init__.py +++ b/homeassistant/components/swiss_public_transport/__init__.py @@ -1,4 +1,5 @@ """The swiss_public_transport component.""" + import logging from opendata_transport import OpendataTransport diff --git a/homeassistant/components/swiss_public_transport/config_flow.py b/homeassistant/components/swiss_public_transport/config_flow.py index e864f31cd6c..6c5de3c7883 100644 --- a/homeassistant/components/swiss_public_transport/config_flow.py +++ b/homeassistant/components/swiss_public_transport/config_flow.py @@ -1,4 +1,5 @@ """Config flow for swiss_public_transport.""" + import logging from typing import Any @@ -9,9 +10,8 @@ from opendata_transport.exceptions import ( ) import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_NAME -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -27,7 +27,7 @@ DATA_SCHEMA = vol.Schema( _LOGGER = logging.getLogger(__name__) -class SwissPublicTransportConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class SwissPublicTransportConfigFlow(ConfigFlow, domain=DOMAIN): """Swiss public transport config flow.""" VERSION = 1 @@ -35,7 +35,7 @@ class SwissPublicTransportConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Async user step to set up the connection.""" errors: dict[str, str] = {} if user_input is not None: @@ -70,7 +70,7 @@ class SwissPublicTransportConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): description_placeholders=PLACEHOLDERS, ) - async def async_step_import(self, import_input: dict[str, Any]) -> FlowResult: + async def async_step_import(self, import_input: dict[str, Any]) -> ConfigFlowResult: """Async import step to set up the connection.""" await self.async_set_unique_id( f"{import_input[CONF_START]} {import_input[CONF_DESTINATION]}" diff --git a/homeassistant/components/swiss_public_transport/const.py b/homeassistant/components/swiss_public_transport/const.py index 6d9fb8bb960..6ae3cc9fd2f 100644 --- a/homeassistant/components/swiss_public_transport/const.py +++ b/homeassistant/components/swiss_public_transport/const.py @@ -7,6 +7,8 @@ CONF_START = "from" DEFAULT_NAME = "Next Destination" +SENSOR_CONNECTIONS_COUNT = 3 + PLACEHOLDERS = { "stationboard_url": "http://transport.opendata.ch/examples/stationboard.html", diff --git a/homeassistant/components/swiss_public_transport/coordinator.py b/homeassistant/components/swiss_public_transport/coordinator.py index 97253d5776e..7df593d5667 100644 --- a/homeassistant/components/swiss_public_transport/coordinator.py +++ b/homeassistant/components/swiss_public_transport/coordinator.py @@ -1,4 +1,5 @@ """DataUpdateCoordinator for the swiss_public_transport integration.""" + from __future__ import annotations from datetime import datetime, timedelta @@ -13,7 +14,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed import homeassistant.util.dt as dt_util -from .const import DOMAIN +from .const import DOMAIN, SENSOR_CONNECTIONS_COUNT _LOGGER = logging.getLogger(__name__) @@ -22,8 +23,8 @@ class DataConnection(TypedDict): """A connection data class.""" departure: datetime | None - next_departure: str | None - next_on_departure: str | None + next_departure: datetime | None + next_on_departure: datetime | None duration: str platform: str remaining_time: str @@ -34,7 +35,9 @@ class DataConnection(TypedDict): delay: int -class SwissPublicTransportDataUpdateCoordinator(DataUpdateCoordinator[DataConnection]): +class SwissPublicTransportDataUpdateCoordinator( + DataUpdateCoordinator[list[DataConnection]] +): """A SwissPublicTransport Data Update Coordinator.""" config_entry: ConfigEntry @@ -49,7 +52,22 @@ class SwissPublicTransportDataUpdateCoordinator(DataUpdateCoordinator[DataConnec ) self._opendata = opendata - async def _async_update_data(self) -> DataConnection: + def remaining_time(self, departure) -> timedelta | None: + """Calculate the remaining time for the departure.""" + departure_datetime = dt_util.parse_datetime(departure) + + if departure_datetime: + return departure_datetime - dt_util.as_local(dt_util.utcnow()) + return None + + def nth_departure_time(self, i: int) -> datetime | None: + """Get nth departure time.""" + connections = self._opendata.connections + if len(connections) > i and connections[i] is not None: + return dt_util.parse_datetime(connections[i]["departure"]) + return None + + async def _async_update_data(self) -> list[DataConnection]: try: await self._opendata.async_get_data() except OpendataTransportError as e: @@ -58,41 +76,22 @@ class SwissPublicTransportDataUpdateCoordinator(DataUpdateCoordinator[DataConnec ) raise UpdateFailed from e - departure_time = ( - dt_util.parse_datetime(self._opendata.connections[0]["departure"]) - if self._opendata.connections[0] is not None - else None - ) - next_departure_time = ( - dt_util.parse_datetime(self._opendata.connections[1]["departure"]) - if self._opendata.connections[1] is not None - else None - ) - next_on_departure_time = ( - dt_util.parse_datetime(self._opendata.connections[2]["departure"]) - if self._opendata.connections[2] is not None - else None - ) + connections = self._opendata.connections - if departure_time: - remaining_time = departure_time - dt_util.as_local(dt_util.utcnow()) - else: - remaining_time = None - - return DataConnection( - departure=departure_time, - next_departure=next_departure_time.isoformat() - if next_departure_time is not None - else None, - next_on_departure=next_on_departure_time.isoformat() - if next_on_departure_time is not None - else None, - train_number=self._opendata.connections[0]["number"], - platform=self._opendata.connections[0]["platform"], - transfers=self._opendata.connections[0]["transfers"], - duration=self._opendata.connections[0]["duration"], - start=self._opendata.from_name, - destination=self._opendata.to_name, - remaining_time=f"{remaining_time}", - delay=self._opendata.connections[0]["delay"], - ) + return [ + DataConnection( + departure=self.nth_departure_time(i), + next_departure=self.nth_departure_time(i + 1), + next_on_departure=self.nth_departure_time(i + 2), + train_number=connections[i]["number"], + platform=connections[i]["platform"], + transfers=connections[i]["transfers"], + duration=connections[i]["duration"], + start=self._opendata.from_name, + destination=self._opendata.to_name, + remaining_time=str(self.remaining_time(connections[i]["departure"])), + delay=connections[i]["delay"], + ) + for i in range(SENSOR_CONNECTIONS_COUNT) + if len(connections) > i and connections[i] is not None + ] diff --git a/homeassistant/components/swiss_public_transport/icons.json b/homeassistant/components/swiss_public_transport/icons.json new file mode 100644 index 00000000000..fac54b10809 --- /dev/null +++ b/homeassistant/components/swiss_public_transport/icons.json @@ -0,0 +1,9 @@ +{ + "entity": { + "sensor": { + "departure": { + "default": "mdi:bus" + } + } + } +} diff --git a/homeassistant/components/swiss_public_transport/sensor.py b/homeassistant/components/swiss_public_transport/sensor.py index ede2798f675..a4a9605a603 100644 --- a/homeassistant/components/swiss_public_transport/sensor.py +++ b/homeassistant/components/swiss_public_transport/sensor.py @@ -1,6 +1,9 @@ """Support for transport.opendata.ch.""" + from __future__ import annotations +from collections.abc import Callable +from dataclasses import dataclass from datetime import datetime, timedelta import logging from typing import TYPE_CHECKING @@ -12,6 +15,7 @@ from homeassistant.components.sensor import ( PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, + SensorEntityDescription, ) from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import CONF_NAME @@ -24,8 +28,15 @@ from homeassistant.helpers.issue_registry import IssueSeverity, async_create_iss from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import CONF_DESTINATION, CONF_START, DEFAULT_NAME, DOMAIN, PLACEHOLDERS -from .coordinator import SwissPublicTransportDataUpdateCoordinator +from .const import ( + CONF_DESTINATION, + CONF_START, + DEFAULT_NAME, + DOMAIN, + PLACEHOLDERS, + SENSOR_CONNECTIONS_COUNT, +) +from .coordinator import DataConnection, SwissPublicTransportDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) @@ -40,6 +51,33 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) +@dataclass(kw_only=True, frozen=True) +class SwissPublicTransportSensorEntityDescription(SensorEntityDescription): + """Describes swiss public transport sensor entity.""" + + exists_fn: Callable[[DataConnection], bool] + value_fn: Callable[[DataConnection], datetime | None] + + index: int + has_legacy_attributes: bool + + +SENSORS: tuple[SwissPublicTransportSensorEntityDescription, ...] = ( + *[ + SwissPublicTransportSensorEntityDescription( + key=f"departure{i or ''}", + translation_key=f"departure{i}", + device_class=SensorDeviceClass.TIMESTAMP, + has_legacy_attributes=i == 0, + value_fn=lambda data_connection: data_connection["departure"], + exists_fn=lambda data_connection: data_connection is not None, + index=i, + ) + for i in range(SENSOR_CONNECTIONS_COUNT) + ], +) + + async def async_setup_entry( hass: core.HomeAssistant, config_entry: config_entries.ConfigEntry, @@ -54,7 +92,8 @@ async def async_setup_entry( assert unique_id async_add_entities( - [SwissPublicTransportSensor(coordinator, unique_id)], + SwissPublicTransportSensor(coordinator, description, unique_id) + for description in SENSORS ) @@ -92,12 +131,12 @@ async def async_setup_platform( async_create_issue( hass, DOMAIN, - f"deprecated_yaml_import_issue_${result['reason']}", + f"deprecated_yaml_import_issue_{result['reason']}", breaks_in_ha_version="2024.7.0", is_fixable=False, issue_domain=DOMAIN, severity=IssueSeverity.WARNING, - translation_key=f"deprecated_yaml_import_issue_${result['reason']}", + translation_key=f"deprecated_yaml_import_issue_{result['reason']}", translation_placeholders=PLACEHOLDERS, ) @@ -107,35 +146,51 @@ class SwissPublicTransportSensor( ): """Implementation of a Swiss public transport sensor.""" + entity_description: SwissPublicTransportSensorEntityDescription _attr_attribution = "Data provided by transport.opendata.ch" - _attr_icon = "mdi:bus" _attr_has_entity_name = True - _attr_translation_key = "departure" - _attr_device_class = SensorDeviceClass.TIMESTAMP def __init__( self, coordinator: SwissPublicTransportDataUpdateCoordinator, + entity_description: SwissPublicTransportSensorEntityDescription, unique_id: str, ) -> None: """Initialize the sensor.""" super().__init__(coordinator) - self._attr_unique_id = f"{unique_id}_departure" + self.entity_description = entity_description + self._attr_unique_id = f"{unique_id}_{entity_description.key}" self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, unique_id)}, manufacturer="Opendata.ch", entry_type=DeviceEntryType.SERVICE, ) + @property + def enabled(self) -> bool: + """Enable the sensor if data is available.""" + return self.entity_description.exists_fn( + self.coordinator.data[self.entity_description.index] + ) + + @property + def native_value(self) -> datetime | None: + """Return the state of the sensor.""" + return self.entity_description.value_fn( + self.coordinator.data[self.entity_description.index] + ) + async def async_added_to_hass(self) -> None: """Prepare the extra attributes at start.""" - self._async_update_attrs() + if self.entity_description.has_legacy_attributes: + self._async_update_attrs() await super().async_added_to_hass() @callback def _handle_coordinator_update(self) -> None: """Handle the state update and prepare the extra state attributes.""" - self._async_update_attrs() + if self.entity_description.has_legacy_attributes: + self._async_update_attrs() return super()._handle_coordinator_update() @callback @@ -143,11 +198,8 @@ class SwissPublicTransportSensor( """Update the extra state attributes based on the coordinator data.""" self._attr_extra_state_attributes = { key: value - for key, value in self.coordinator.data.items() + for key, value in self.coordinator.data[ + self.entity_description.index + ].items() if key not in {"departure"} } - - @property - def native_value(self) -> datetime | None: - """Return the state of the sensor.""" - return self.coordinator.data["departure"] diff --git a/homeassistant/components/swiss_public_transport/strings.json b/homeassistant/components/swiss_public_transport/strings.json index 6d0eb53ad11..c080e785f2c 100644 --- a/homeassistant/components/swiss_public_transport/strings.json +++ b/homeassistant/components/swiss_public_transport/strings.json @@ -24,15 +24,21 @@ }, "entity": { "sensor": { - "departure": { + "departure0": { "name": "Departure" + }, + "departure1": { + "name": "Departure +1" + }, + "departure2": { + "name": "Departure +2" } } }, "issues": { "deprecated_yaml_import_issue_cannot_connect": { "title": "The swiss public transport YAML configuration import cannot connect to server", - "description": "Configuring swiss public transport using YAML is being removed but there was an connection error importing your YAML configuration.\n\nMake sure your home assistant can reach the [opendata server]({opendata_url}). In case the server is down, try again later." + "description": "Configuring swiss public transport using YAML is being removed but there was a connection error importing your YAML configuration.\n\nMake sure your home assistant can reach the [opendata server]({opendata_url}). In case the server is down, try again later." }, "deprecated_yaml_import_issue_bad_config": { "title": "The swiss public transport YAML configuration import request failed due to bad config", diff --git a/homeassistant/components/swisscom/device_tracker.py b/homeassistant/components/swisscom/device_tracker.py index 900117a54b7..cd393c79e09 100644 --- a/homeassistant/components/swisscom/device_tracker.py +++ b/homeassistant/components/swisscom/device_tracker.py @@ -1,4 +1,5 @@ """Support for Swisscom routers (Internet-Box).""" + from __future__ import annotations from contextlib import suppress diff --git a/homeassistant/components/switch/__init__.py b/homeassistant/components/switch/__init__.py index ce9b1477ad6..86c67248eea 100644 --- a/homeassistant/components/switch/__init__.py +++ b/homeassistant/components/switch/__init__.py @@ -1,4 +1,5 @@ """Component to interface with switches that can be controlled remotely.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/switch/device_action.py b/homeassistant/components/switch/device_action.py index ce9f0a36117..bff4ce6e396 100644 --- a/homeassistant/components/switch/device_action.py +++ b/homeassistant/components/switch/device_action.py @@ -1,4 +1,5 @@ """Provides device actions for switches.""" + from __future__ import annotations import voluptuous as vol diff --git a/homeassistant/components/switch/device_condition.py b/homeassistant/components/switch/device_condition.py index 7f47983ba67..f3a6c299529 100644 --- a/homeassistant/components/switch/device_condition.py +++ b/homeassistant/components/switch/device_condition.py @@ -1,4 +1,5 @@ """Provides device conditions for switches.""" + from __future__ import annotations import voluptuous as vol diff --git a/homeassistant/components/switch/device_trigger.py b/homeassistant/components/switch/device_trigger.py index 499b04bbaf3..6898a9954de 100644 --- a/homeassistant/components/switch/device_trigger.py +++ b/homeassistant/components/switch/device_trigger.py @@ -1,4 +1,5 @@ """Provides device triggers for switches.""" + from __future__ import annotations import voluptuous as vol diff --git a/homeassistant/components/switch/group.py b/homeassistant/components/switch/group.py deleted file mode 100644 index 234883ffd5a..00000000000 --- a/homeassistant/components/switch/group.py +++ /dev/null @@ -1,14 +0,0 @@ -"""Describe group states.""" - - -from homeassistant.components.group import GroupIntegrationRegistry -from homeassistant.const import STATE_OFF, STATE_ON -from homeassistant.core import HomeAssistant, callback - - -@callback -def async_describe_on_off_states( - hass: HomeAssistant, registry: GroupIntegrationRegistry -) -> None: - """Describe group on off states.""" - registry.on_off_states({STATE_ON}, STATE_OFF) diff --git a/homeassistant/components/switch/light.py b/homeassistant/components/switch/light.py index ffd345cea3b..25214822bdb 100644 --- a/homeassistant/components/switch/light.py +++ b/homeassistant/components/switch/light.py @@ -1,4 +1,5 @@ """Light support for switch entities.""" + from __future__ import annotations from typing import Any @@ -15,7 +16,7 @@ from homeassistant.const import ( STATE_ON, STATE_UNAVAILABLE, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import Event, HomeAssistant, callback from homeassistant.helpers import entity_registry as er import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -23,7 +24,7 @@ from homeassistant.helpers.event import ( EventStateChangedData, async_track_state_change_event, ) -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, EventType +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import DOMAIN as SWITCH_DOMAIN @@ -97,7 +98,7 @@ class LightSwitch(LightEntity): @callback def async_state_changed_listener( - event: EventType[EventStateChangedData] | None = None, + event: Event[EventStateChangedData] | None = None, ) -> None: """Handle child updates.""" if ( diff --git a/homeassistant/components/switch/reproduce_state.py b/homeassistant/components/switch/reproduce_state.py index 0a6d0de9602..aaed39d39b8 100644 --- a/homeassistant/components/switch/reproduce_state.py +++ b/homeassistant/components/switch/reproduce_state.py @@ -1,4 +1,5 @@ """Reproduce an Switch state.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/switch/significant_change.py b/homeassistant/components/switch/significant_change.py index 231085a3eef..ab7c6bc9281 100644 --- a/homeassistant/components/switch/significant_change.py +++ b/homeassistant/components/switch/significant_change.py @@ -1,4 +1,5 @@ """Helper to test significant Switch state changes.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/switch_as_x/__init__.py b/homeassistant/components/switch_as_x/__init__.py index d94c7c9f098..71cb9e9c225 100644 --- a/homeassistant/components/switch_as_x/__init__.py +++ b/homeassistant/components/switch_as_x/__init__.py @@ -1,4 +1,5 @@ """Component to wrap switch entities in entities of other domains.""" + from __future__ import annotations import logging @@ -8,10 +9,9 @@ import voluptuous as vol from homeassistant.components.homeassistant import exposed_entities from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ENTITY_ID -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import Event, HomeAssistant, callback from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.event import async_track_entity_registry_updated_event -from homeassistant.helpers.typing import EventType from .const import CONF_INVERT, CONF_TARGET_DOMAIN from .light import LightSwitch @@ -57,7 +57,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return False async def async_registry_updated( - event: EventType[er.EventEntityRegistryUpdatedData], + event: Event[er.EventEntityRegistryUpdatedData], ) -> None: """Handle entity registry update.""" data = event.data diff --git a/homeassistant/components/switch_as_x/config_flow.py b/homeassistant/components/switch_as_x/config_flow.py index e40e247f105..37df3affbad 100644 --- a/homeassistant/components/switch_as_x/config_flow.py +++ b/homeassistant/components/switch_as_x/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Switch as X integration.""" + from __future__ import annotations from collections.abc import Mapping diff --git a/homeassistant/components/switch_as_x/cover.py b/homeassistant/components/switch_as_x/cover.py index 37071ac6771..9d03965a242 100644 --- a/homeassistant/components/switch_as_x/cover.py +++ b/homeassistant/components/switch_as_x/cover.py @@ -1,4 +1,5 @@ """Cover support for switch entities.""" + from __future__ import annotations from typing import Any @@ -17,11 +18,10 @@ from homeassistant.const import ( SERVICE_TURN_ON, STATE_ON, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import Event, HomeAssistant, callback from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import EventStateChangedData -from homeassistant.helpers.typing import EventType from .const import CONF_INVERT from .entity import BaseInvertableEntity @@ -79,7 +79,7 @@ class CoverSwitch(BaseInvertableEntity, CoverEntity): @callback def async_state_changed_listener( - self, event: EventType[EventStateChangedData] | None = None + self, event: Event[EventStateChangedData] | None = None ) -> None: """Handle child updates.""" super().async_state_changed_listener(event) diff --git a/homeassistant/components/switch_as_x/entity.py b/homeassistant/components/switch_as_x/entity.py index 39c2a8cab60..e8e57570617 100644 --- a/homeassistant/components/switch_as_x/entity.py +++ b/homeassistant/components/switch_as_x/entity.py @@ -1,4 +1,5 @@ """Base entity for the Switch as X integration.""" + from __future__ import annotations from typing import Any @@ -12,7 +13,7 @@ from homeassistant.const import ( STATE_ON, STATE_UNAVAILABLE, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import Event, HomeAssistant, callback from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity import Entity, ToggleEntity @@ -20,7 +21,6 @@ from homeassistant.helpers.event import ( EventStateChangedData, async_track_state_change_event, ) -from homeassistant.helpers.typing import EventType from .const import DOMAIN as SWITCH_AS_X_DOMAIN @@ -69,7 +69,7 @@ class BaseEntity(Entity): @callback def async_state_changed_listener( - self, event: EventType[EventStateChangedData] | None = None + self, event: Event[EventStateChangedData] | None = None ) -> None: """Handle child updates.""" if ( @@ -85,7 +85,7 @@ class BaseEntity(Entity): @callback def _async_state_changed_listener( - event: EventType[EventStateChangedData] | None = None, + event: Event[EventStateChangedData] | None = None, ) -> None: """Handle child updates.""" self.async_state_changed_listener(event) @@ -172,7 +172,7 @@ class BaseToggleEntity(BaseEntity, ToggleEntity): @callback def async_state_changed_listener( - self, event: EventType[EventStateChangedData] | None = None + self, event: Event[EventStateChangedData] | None = None ) -> None: """Handle child updates.""" super().async_state_changed_listener(event) diff --git a/homeassistant/components/switch_as_x/fan.py b/homeassistant/components/switch_as_x/fan.py index d8c43cfe381..fb795b4f54a 100644 --- a/homeassistant/components/switch_as_x/fan.py +++ b/homeassistant/components/switch_as_x/fan.py @@ -1,4 +1,5 @@ """Fan support for switch entities.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/switch_as_x/light.py b/homeassistant/components/switch_as_x/light.py index e6183c95d91..59b816f7935 100644 --- a/homeassistant/components/switch_as_x/light.py +++ b/homeassistant/components/switch_as_x/light.py @@ -1,4 +1,5 @@ """Light support for switch entities.""" + from __future__ import annotations from homeassistant.components.light import ( diff --git a/homeassistant/components/switch_as_x/lock.py b/homeassistant/components/switch_as_x/lock.py index 528825c0300..5243ae184ee 100644 --- a/homeassistant/components/switch_as_x/lock.py +++ b/homeassistant/components/switch_as_x/lock.py @@ -1,4 +1,5 @@ """Lock support for switch entities.""" + from __future__ import annotations from typing import Any @@ -13,11 +14,10 @@ from homeassistant.const import ( SERVICE_TURN_ON, STATE_ON, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import Event, HomeAssistant, callback from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import EventStateChangedData -from homeassistant.helpers.typing import EventType from .const import CONF_INVERT from .entity import BaseInvertableEntity @@ -73,7 +73,7 @@ class LockSwitch(BaseInvertableEntity, LockEntity): @callback def async_state_changed_listener( - self, event: EventType[EventStateChangedData] | None = None + self, event: Event[EventStateChangedData] | None = None ) -> None: """Handle child updates.""" super().async_state_changed_listener(event) diff --git a/homeassistant/components/switch_as_x/siren.py b/homeassistant/components/switch_as_x/siren.py index c9981b17cfe..7d9a41d9cd9 100644 --- a/homeassistant/components/switch_as_x/siren.py +++ b/homeassistant/components/switch_as_x/siren.py @@ -1,4 +1,5 @@ """Siren support for switch entities.""" + from __future__ import annotations from homeassistant.components.siren import ( diff --git a/homeassistant/components/switch_as_x/valve.py b/homeassistant/components/switch_as_x/valve.py index 971338764a5..98f0e52c8a2 100644 --- a/homeassistant/components/switch_as_x/valve.py +++ b/homeassistant/components/switch_as_x/valve.py @@ -1,4 +1,5 @@ """Valve support for switch entities.""" + from __future__ import annotations from typing import Any @@ -17,11 +18,10 @@ from homeassistant.const import ( SERVICE_TURN_ON, STATE_ON, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import Event, HomeAssistant, callback from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import EventStateChangedData -from homeassistant.helpers.typing import EventType from .const import CONF_INVERT from .entity import BaseInvertableEntity @@ -80,7 +80,7 @@ class ValveSwitch(BaseInvertableEntity, ValveEntity): @callback def async_state_changed_listener( - self, event: EventType[EventStateChangedData] | None = None + self, event: Event[EventStateChangedData] | None = None ) -> None: """Handle child updates.""" super().async_state_changed_listener(event) diff --git a/homeassistant/components/switchbee/config_flow.py b/homeassistant/components/switchbee/config_flow.py index 8f109c7bf26..9b5139340b1 100644 --- a/homeassistant/components/switchbee/config_flow.py +++ b/homeassistant/components/switchbee/config_flow.py @@ -1,4 +1,5 @@ """Config flow for SwitchBee Smart Home integration.""" + from __future__ import annotations import logging @@ -8,10 +9,9 @@ from switchbee.api.central_unit import SwitchBeeError from switchbee.api.polling import CentralUnitPolling import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -53,14 +53,14 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> str: return format_mac(api.mac) -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class SwitchBeeConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for SwitchBee Smart Home.""" VERSION = 2 async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Show the setup form to the user.""" errors: dict[str, str] = {} diff --git a/homeassistant/components/switchbee/entity.py b/homeassistant/components/switchbee/entity.py index 6aae5adb3d6..c601324b2a5 100644 --- a/homeassistant/components/switchbee/entity.py +++ b/homeassistant/components/switchbee/entity.py @@ -1,4 +1,5 @@ """Support for SwitchBee entity.""" + import logging from typing import Generic, TypeVar, cast diff --git a/homeassistant/components/switchbot/binary_sensor.py b/homeassistant/components/switchbot/binary_sensor.py index 7169f01b38f..92e00a65d8a 100644 --- a/homeassistant/components/switchbot/binary_sensor.py +++ b/homeassistant/components/switchbot/binary_sensor.py @@ -1,4 +1,5 @@ """Support for SwitchBot binary sensors.""" + from __future__ import annotations from homeassistant.components.binary_sensor import ( diff --git a/homeassistant/components/switchbot/config_flow.py b/homeassistant/components/switchbot/config_flow.py index 35e76d8bbb3..06b95c6f8aa 100644 --- a/homeassistant/components/switchbot/config_flow.py +++ b/homeassistant/components/switchbot/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Switchbot.""" + from __future__ import annotations import logging @@ -18,7 +19,12 @@ from homeassistant.components.bluetooth import ( BluetoothServiceInfoBleak, async_discovered_service_info, ) -from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.const import ( CONF_ADDRESS, CONF_PASSWORD, @@ -26,7 +32,7 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import callback -from homeassistant.data_entry_flow import AbortFlow, FlowResult +from homeassistant.data_entry_flow import AbortFlow from .const import ( CONF_ENCRYPTION_KEY, @@ -78,7 +84,7 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_bluetooth( self, discovery_info: BluetoothServiceInfoBleak - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the bluetooth discovery step.""" _LOGGER.debug("Discovered bluetooth device: %s", discovery_info.as_dict()) await self.async_set_unique_id(format_unique_id(discovery_info.address)) @@ -109,7 +115,7 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN): async def _async_create_entry_from_discovery( self, user_input: dict[str, Any] - ) -> FlowResult: + ) -> ConfigFlowResult: """Create an entry from a discovery.""" assert self._discovered_adv is not None discovery = self._discovered_adv @@ -126,7 +132,7 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm a single device.""" assert self._discovered_adv is not None if user_input is not None: @@ -143,7 +149,7 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_password( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the password step.""" assert self._discovered_adv is not None if user_input is not None: @@ -162,7 +168,7 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_lock_auth( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the SwitchBot API auth step.""" errors = {} assert self._discovered_adv is not None @@ -204,7 +210,7 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_lock_choose_method( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the SwitchBot API chose method step.""" assert self._discovered_adv is not None @@ -218,7 +224,7 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_lock_key( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the encryption key step.""" errors = {} assert self._discovered_adv is not None @@ -285,7 +291,7 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the user step to pick discovered device.""" errors: dict[str, str] = {} device_adv: SwitchBotAdvertisement | None = None @@ -335,7 +341,7 @@ class SwitchbotOptionsFlowHandler(OptionsFlow): async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Manage Switchbot options.""" if user_input is not None: # Update common entity options for all other entities. diff --git a/homeassistant/components/switchbot/const.py b/homeassistant/components/switchbot/const.py index 0f7d1407fc5..9993bd95415 100644 --- a/homeassistant/components/switchbot/const.py +++ b/homeassistant/components/switchbot/const.py @@ -1,4 +1,5 @@ """Constants for the switchbot integration.""" + from enum import StrEnum from switchbot import SwitchbotModel diff --git a/homeassistant/components/switchbot/coordinator.py b/homeassistant/components/switchbot/coordinator.py index 29679605e8b..2c68b126fa5 100644 --- a/homeassistant/components/switchbot/coordinator.py +++ b/homeassistant/components/switchbot/coordinator.py @@ -1,4 +1,5 @@ """Provides the switchbot DataUpdateCoordinator.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/switchbot/cover.py b/homeassistant/components/switchbot/cover.py index 4883bf456c0..8039ff8ec15 100644 --- a/homeassistant/components/switchbot/cover.py +++ b/homeassistant/components/switchbot/cover.py @@ -1,4 +1,5 @@ """Support for SwitchBot curtains.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/switchbot/entity.py b/homeassistant/components/switchbot/entity.py index cf7f97a2692..bde69429bc3 100644 --- a/homeassistant/components/switchbot/entity.py +++ b/homeassistant/components/switchbot/entity.py @@ -1,4 +1,5 @@ """An abstract class common to all Switchbot entities.""" + from __future__ import annotations from collections.abc import Mapping diff --git a/homeassistant/components/switchbot/humidifier.py b/homeassistant/components/switchbot/humidifier.py index 5b53b410208..3871fcb7265 100644 --- a/homeassistant/components/switchbot/humidifier.py +++ b/homeassistant/components/switchbot/humidifier.py @@ -1,4 +1,5 @@ """Support for Switchbot humidifier.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/switchbot/light.py b/homeassistant/components/switchbot/light.py index 53b40bbf780..649a8b34c75 100644 --- a/homeassistant/components/switchbot/light.py +++ b/homeassistant/components/switchbot/light.py @@ -1,4 +1,5 @@ """Switchbot integration light platform.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/switchbot/lock.py b/homeassistant/components/switchbot/lock.py index 60f4fe66c26..7b58a2f5ac3 100644 --- a/homeassistant/components/switchbot/lock.py +++ b/homeassistant/components/switchbot/lock.py @@ -1,4 +1,5 @@ """Support for SwitchBot lock platform.""" + from typing import Any import switchbot diff --git a/homeassistant/components/switchbot/sensor.py b/homeassistant/components/switchbot/sensor.py index a408bcb58bc..2a25d84aa8d 100644 --- a/homeassistant/components/switchbot/sensor.py +++ b/homeassistant/components/switchbot/sensor.py @@ -1,4 +1,5 @@ """Support for SwitchBot sensors.""" + from __future__ import annotations from homeassistant.components.bluetooth import async_last_service_info diff --git a/homeassistant/components/switchbot/switch.py b/homeassistant/components/switchbot/switch.py index f62e4d3f918..26ceee203aa 100644 --- a/homeassistant/components/switchbot/switch.py +++ b/homeassistant/components/switchbot/switch.py @@ -1,4 +1,5 @@ """Support for Switchbot bot.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/switchbot_cloud/__init__.py b/homeassistant/components/switchbot_cloud/__init__.py index 8d3b2443b18..744d513f521 100644 --- a/homeassistant/components/switchbot_cloud/__init__.py +++ b/homeassistant/components/switchbot_cloud/__init__.py @@ -1,4 +1,5 @@ """The SwitchBot via API integration.""" + from asyncio import gather from dataclasses import dataclass, field from logging import getLogger diff --git a/homeassistant/components/switchbot_cloud/config_flow.py b/homeassistant/components/switchbot_cloud/config_flow.py index 5c99567968c..c01699b8c5d 100644 --- a/homeassistant/components/switchbot_cloud/config_flow.py +++ b/homeassistant/components/switchbot_cloud/config_flow.py @@ -6,9 +6,8 @@ from typing import Any from switchbot_api import CannotConnect, InvalidAuth, SwitchBotAPI import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_API_KEY, CONF_API_TOKEN -from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN, ENTRY_TITLE @@ -22,14 +21,14 @@ STEP_USER_DATA_SCHEMA = vol.Schema( ) -class SwitchBotCloudConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class SwitchBotCloudConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for SwitchBot via API.""" VERSION = 1 async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors: dict[str, str] = {} if user_input is not None: diff --git a/homeassistant/components/switchbot_cloud/const.py b/homeassistant/components/switchbot_cloud/const.py index ef69c9c1d02..b90a2f3a2ec 100644 --- a/homeassistant/components/switchbot_cloud/const.py +++ b/homeassistant/components/switchbot_cloud/const.py @@ -1,4 +1,5 @@ """Constants for the SwitchBot Cloud integration.""" + from datetime import timedelta from typing import Final diff --git a/homeassistant/components/switchbot_cloud/coordinator.py b/homeassistant/components/switchbot_cloud/coordinator.py index 92099ccde43..4c12e03a6f2 100644 --- a/homeassistant/components/switchbot_cloud/coordinator.py +++ b/homeassistant/components/switchbot_cloud/coordinator.py @@ -1,4 +1,5 @@ """SwitchBot Cloud coordinator.""" + from asyncio import timeout from logging import getLogger from typing import Any diff --git a/homeassistant/components/switchbot_cloud/entity.py b/homeassistant/components/switchbot_cloud/entity.py index 5d0e2ff09c3..7bb00cda945 100644 --- a/homeassistant/components/switchbot_cloud/entity.py +++ b/homeassistant/components/switchbot_cloud/entity.py @@ -1,4 +1,5 @@ """Base class for SwitchBot via API entities.""" + from typing import Any from switchbot_api import Commands, Device, Remote, SwitchBotAPI diff --git a/homeassistant/components/switchbot_cloud/switch.py b/homeassistant/components/switchbot_cloud/switch.py index 4f2cdc22ba9..fbcd4430f6e 100644 --- a/homeassistant/components/switchbot_cloud/switch.py +++ b/homeassistant/components/switchbot_cloud/switch.py @@ -1,4 +1,5 @@ """Support for SwitchBot switch.""" + from typing import Any from switchbot_api import CommonCommands, Device, PowerState, Remote, SwitchBotAPI diff --git a/homeassistant/components/switcher_kis/__init__.py b/homeassistant/components/switcher_kis/__init__.py index 79ef201efee..b3315bac2ca 100644 --- a/homeassistant/components/switcher_kis/__init__.py +++ b/homeassistant/components/switcher_kis/__init__.py @@ -1,4 +1,5 @@ """The Switcher integration.""" + from __future__ import annotations from datetime import timedelta @@ -97,9 +98,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: device.device_type.hex_rep, ) - coordinator = hass.data[DOMAIN][DATA_DEVICE][ - device.device_id - ] = SwitcherDataUpdateCoordinator(hass, entry, device) + coordinator = hass.data[DOMAIN][DATA_DEVICE][device.device_id] = ( + SwitcherDataUpdateCoordinator(hass, entry, device) + ) coordinator.async_setup() # Must be ready before dispatcher is called diff --git a/homeassistant/components/switcher_kis/button.py b/homeassistant/components/switcher_kis/button.py index 64571f15af0..b0e45f1374a 100644 --- a/homeassistant/components/switcher_kis/button.py +++ b/homeassistant/components/switcher_kis/button.py @@ -1,4 +1,5 @@ """Switcher integration Button platform.""" + from __future__ import annotations from collections.abc import Callable @@ -29,26 +30,18 @@ from .const import SIGNAL_DEVICE_ADD from .utils import get_breeze_remote_manager -@dataclass(frozen=True) -class SwitcherThermostatButtonDescriptionMixin: - """Mixin to describe a Switcher Thermostat Button entity.""" +@dataclass(frozen=True, kw_only=True) +class SwitcherThermostatButtonEntityDescription(ButtonEntityDescription): + """Class to describe a Switcher Thermostat Button entity.""" press_fn: Callable[[SwitcherType2Api, SwitcherBreezeRemote], SwitcherBaseResponse] supported: Callable[[SwitcherBreezeRemote], bool] -@dataclass(frozen=True) -class SwitcherThermostatButtonEntityDescription( - ButtonEntityDescription, SwitcherThermostatButtonDescriptionMixin -): - """Class to describe a Switcher Thermostat Button entity.""" - - THERMOSTAT_BUTTONS = [ SwitcherThermostatButtonEntityDescription( key="assume_on", translation_key="assume_on", - icon="mdi:fan", entity_category=EntityCategory.CONFIG, press_fn=lambda api, remote: api.control_breeze_device( remote, state=DeviceState.ON, update_state=True @@ -58,7 +51,6 @@ THERMOSTAT_BUTTONS = [ SwitcherThermostatButtonEntityDescription( key="assume_off", translation_key="assume_off", - icon="mdi:fan-off", entity_category=EntityCategory.CONFIG, press_fn=lambda api, remote: api.control_breeze_device( remote, state=DeviceState.OFF, update_state=True @@ -68,7 +60,6 @@ THERMOSTAT_BUTTONS = [ SwitcherThermostatButtonEntityDescription( key="vertical_swing_on", translation_key="vertical_swing_on", - icon="mdi:autorenew", press_fn=lambda api, remote: api.control_breeze_device( remote, swing=ThermostatSwing.ON ), @@ -77,7 +68,6 @@ THERMOSTAT_BUTTONS = [ SwitcherThermostatButtonEntityDescription( key="vertical_swing_off", translation_key="vertical_swing_off", - icon="mdi:autorenew-off", press_fn=lambda api, remote: api.control_breeze_device( remote, swing=ThermostatSwing.OFF ), diff --git a/homeassistant/components/switcher_kis/climate.py b/homeassistant/components/switcher_kis/climate.py index 180b71b1fe6..caf46ca8975 100644 --- a/homeassistant/components/switcher_kis/climate.py +++ b/homeassistant/components/switcher_kis/climate.py @@ -1,4 +1,5 @@ """Switcher integration Climate platform.""" + from __future__ import annotations from typing import Any, cast diff --git a/homeassistant/components/switcher_kis/config_flow.py b/homeassistant/components/switcher_kis/config_flow.py index d196bae8568..bd24481ce3f 100644 --- a/homeassistant/components/switcher_kis/config_flow.py +++ b/homeassistant/components/switcher_kis/config_flow.py @@ -1,19 +1,21 @@ """Config flow for Switcher integration.""" + from __future__ import annotations from typing import Any -from homeassistant import config_entries -from homeassistant.data_entry_flow import FlowResult +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from .const import DATA_DISCOVERY, DOMAIN from .utils import async_discover_devices -class SwitcherFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class SwitcherFlowHandler(ConfigFlow, domain=DOMAIN): """Handle Switcher config flow.""" - async def async_step_import(self, import_config: dict[str, Any]) -> FlowResult: + async def async_step_import( + self, import_config: dict[str, Any] + ) -> ConfigFlowResult: """Handle a flow initiated by import.""" if self._async_current_entries(True): return self.async_abort(reason="single_instance_allowed") @@ -22,7 +24,7 @@ class SwitcherFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the start of the config flow.""" if self._async_current_entries(True): return self.async_abort(reason="single_instance_allowed") @@ -37,7 +39,7 @@ class SwitcherFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle user-confirmation of the config flow.""" discovered_devices = await self.hass.data[DOMAIN][DATA_DISCOVERY] diff --git a/homeassistant/components/switcher_kis/const.py b/homeassistant/components/switcher_kis/const.py index fdd5b02fe9b..248b7afbc81 100644 --- a/homeassistant/components/switcher_kis/const.py +++ b/homeassistant/components/switcher_kis/const.py @@ -1,4 +1,5 @@ """Constants for the Switcher integration.""" + DOMAIN = "switcher_kis" CONF_DEVICE_PASSWORD = "device_password" diff --git a/homeassistant/components/switcher_kis/cover.py b/homeassistant/components/switcher_kis/cover.py index 4d81480e136..69ec501c4a7 100644 --- a/homeassistant/components/switcher_kis/cover.py +++ b/homeassistant/components/switcher_kis/cover.py @@ -1,4 +1,5 @@ """Switcher integration Cover platform.""" + from __future__ import annotations import logging @@ -125,6 +126,6 @@ class SwitcherCoverEntity( """Move the cover to a specific position.""" await self._async_call_api(API_SET_POSITON, kwargs[ATTR_POSITION]) - async def async_stop_cover(self, **_kwargs: Any) -> None: + async def async_stop_cover(self, **kwargs: Any) -> None: """Stop the cover.""" await self._async_call_api(API_STOP) diff --git a/homeassistant/components/switcher_kis/diagnostics.py b/homeassistant/components/switcher_kis/diagnostics.py index 765a3dde9e7..441f45198a2 100644 --- a/homeassistant/components/switcher_kis/diagnostics.py +++ b/homeassistant/components/switcher_kis/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for Switcher.""" + from __future__ import annotations from dataclasses import asdict diff --git a/homeassistant/components/switcher_kis/icons.json b/homeassistant/components/switcher_kis/icons.json new file mode 100644 index 00000000000..4d3576f1a99 --- /dev/null +++ b/homeassistant/components/switcher_kis/icons.json @@ -0,0 +1,30 @@ +{ + "entity": { + "button": { + "assume_on": { + "default": "mdi:fan" + }, + "assume_off": { + "default": "mdi:fan-off" + }, + "vertical_swing_on": { + "default": "mdi:autorenew" + }, + "vertical_swing_off": { + "default": "mdi:autorenew-off" + } + }, + "sensor": { + "remaining_time": { + "default": "mdi:av-timer" + }, + "auto_shutdown": { + "default": "mdi:progress-clock" + } + } + }, + "services": { + "set_auto_off": "mdi:progress-clock", + "turn_on_with_timer": "mdi:timer" + } +} diff --git a/homeassistant/components/switcher_kis/sensor.py b/homeassistant/components/switcher_kis/sensor.py index e9fa13fca8a..88da03fecea 100644 --- a/homeassistant/components/switcher_kis/sensor.py +++ b/homeassistant/components/switcher_kis/sensor.py @@ -1,4 +1,5 @@ """Switcher integration Sensor platform.""" + from __future__ import annotations from aioswitcher.device import DeviceCategory @@ -40,12 +41,10 @@ TIME_SENSORS: list[SensorEntityDescription] = [ SensorEntityDescription( key="remaining_time", translation_key="remaining_time", - icon="mdi:av-timer", ), SensorEntityDescription( key="auto_off_set", translation_key="auto_shutdown", - icon="mdi:progress-clock", entity_registry_enabled_default=False, ), ] diff --git a/homeassistant/components/switcher_kis/switch.py b/homeassistant/components/switcher_kis/switch.py index c24157f70fc..b7c79f6dbc3 100644 --- a/homeassistant/components/switcher_kis/switch.py +++ b/homeassistant/components/switcher_kis/switch.py @@ -1,4 +1,5 @@ """Switcher integration Switch platform.""" + from __future__ import annotations from datetime import timedelta @@ -33,6 +34,9 @@ from .const import ( _LOGGER = logging.getLogger(__name__) +API_CONTROL_DEVICE = "control_device" +API_SET_AUTO_SHUTDOWN = "set_auto_shutdown" + SERVICE_SET_AUTO_OFF_SCHEMA = { vol.Required(CONF_AUTO_OFF): cv.time_period_str, } @@ -140,13 +144,13 @@ class SwitcherBaseSwitchEntity( async def async_turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" - await self._async_call_api("control_device", Command.ON) + await self._async_call_api(API_CONTROL_DEVICE, Command.ON) self.control_result = True self.async_write_ha_state() async def async_turn_off(self, **kwargs: Any) -> None: """Turn the entity off.""" - await self._async_call_api("control_device", Command.OFF) + await self._async_call_api(API_CONTROL_DEVICE, Command.OFF) self.control_result = False self.async_write_ha_state() @@ -180,11 +184,11 @@ class SwitcherWaterHeaterSwitchEntity(SwitcherBaseSwitchEntity): async def async_set_auto_off_service(self, auto_off: timedelta) -> None: """Use for handling setting device auto-off service calls.""" - await self._async_call_api("set_auto_shutdown", auto_off) + await self._async_call_api(API_SET_AUTO_SHUTDOWN, auto_off) self.async_write_ha_state() async def async_turn_on_with_timer_service(self, timer_minutes: int) -> None: """Use for turning device on with a timer service calls.""" - await self._async_call_api("control_device", Command.ON, timer_minutes) + await self._async_call_api(API_CONTROL_DEVICE, Command.ON, timer_minutes) self.control_result = True self.async_write_ha_state() diff --git a/homeassistant/components/switcher_kis/utils.py b/homeassistant/components/switcher_kis/utils.py index ad0414ae806..d95c1122732 100644 --- a/homeassistant/components/switcher_kis/utils.py +++ b/homeassistant/components/switcher_kis/utils.py @@ -1,4 +1,5 @@ """Switcher integration helpers functions.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/switchmate/switch.py b/homeassistant/components/switchmate/switch.py index f88da064af6..ee8b65b47e2 100644 --- a/homeassistant/components/switchmate/switch.py +++ b/homeassistant/components/switchmate/switch.py @@ -1,4 +1,5 @@ """Support for Switchmate.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/syncthing/__init__.py b/homeassistant/components/syncthing/__init__.py index 1f492656166..28ec14a1935 100644 --- a/homeassistant/components/syncthing/__init__.py +++ b/homeassistant/components/syncthing/__init__.py @@ -1,4 +1,5 @@ """The syncthing integration.""" + import asyncio import logging diff --git a/homeassistant/components/syncthing/config_flow.py b/homeassistant/components/syncthing/config_flow.py index 7421a385f08..2d7d2ddcc92 100644 --- a/homeassistant/components/syncthing/config_flow.py +++ b/homeassistant/components/syncthing/config_flow.py @@ -1,9 +1,12 @@ """Config flow for syncthing integration.""" + import aiosyncthing import voluptuous as vol -from homeassistant import config_entries, core, exceptions +from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_TOKEN, CONF_URL, CONF_VERIFY_SSL +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from .const import DEFAULT_URL, DEFAULT_VERIFY_SSL, DOMAIN @@ -16,7 +19,7 @@ DATA_SCHEMA = vol.Schema( ) -async def validate_input(hass: core.HomeAssistant, data): +async def validate_input(hass: HomeAssistant, data): """Validate the user input allows us to connect.""" try: @@ -34,7 +37,7 @@ async def validate_input(hass: core.HomeAssistant, data): raise CannotConnect from error -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class SyncThingConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for syncthing.""" VERSION = 1 @@ -60,9 +63,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -class CannotConnect(exceptions.HomeAssistantError): +class CannotConnect(HomeAssistantError): """Error to indicate we cannot connect.""" -class InvalidAuth(exceptions.HomeAssistantError): +class InvalidAuth(HomeAssistantError): """Error to indicate there is invalid auth.""" diff --git a/homeassistant/components/syncthing/const.py b/homeassistant/components/syncthing/const.py index a9ec0ad0375..e1e78b0e778 100644 --- a/homeassistant/components/syncthing/const.py +++ b/homeassistant/components/syncthing/const.py @@ -1,4 +1,5 @@ """Constants for the syncthing integration.""" + from datetime import timedelta DOMAIN = "syncthing" @@ -20,14 +21,3 @@ EVENTS = { "StateChanged": STATE_CHANGED_RECEIVED, "FolderPaused": FOLDER_PAUSED_RECEIVED, } - - -FOLDER_SENSOR_ICONS = { - "paused": "mdi:folder-clock", - "scanning": "mdi:folder-search", - "syncing": "mdi:folder-sync", - "idle": "mdi:folder", -} - -FOLDER_SENSOR_ALERT_ICON = "mdi:folder-alert" -FOLDER_SENSOR_DEFAULT_ICON = "mdi:folder" diff --git a/homeassistant/components/syncthing/icons.json b/homeassistant/components/syncthing/icons.json new file mode 100644 index 00000000000..0e86b6e4fa3 --- /dev/null +++ b/homeassistant/components/syncthing/icons.json @@ -0,0 +1,16 @@ +{ + "entity": { + "sensor": { + "syncthing": { + "default": "mdi:folder-alert", + "state": { + "unknown": "mdi:folder", + "idle": "mdi:folder", + "scanning": "mdi:folder-search", + "syncing": "mdi:folder-sync", + "paused": "mdi:folder-clock" + } + } + } + } +} diff --git a/homeassistant/components/syncthing/sensor.py b/homeassistant/components/syncthing/sensor.py index c88de91cae0..fc1f9ae8aea 100644 --- a/homeassistant/components/syncthing/sensor.py +++ b/homeassistant/components/syncthing/sensor.py @@ -1,4 +1,5 @@ """Support for monitoring the Syncthing instance.""" + import aiosyncthing from homeassistant.components.sensor import SensorEntity @@ -13,9 +14,6 @@ from homeassistant.helpers.event import async_track_time_interval from .const import ( DOMAIN, FOLDER_PAUSED_RECEIVED, - FOLDER_SENSOR_ALERT_ICON, - FOLDER_SENSOR_DEFAULT_ICON, - FOLDER_SENSOR_ICONS, FOLDER_SUMMARY_RECEIVED, SCAN_INTERVAL, SERVER_AVAILABLE, @@ -57,6 +55,7 @@ class FolderSensor(SensorEntity): """A Syncthing folder sensor.""" _attr_should_poll = False + _attr_translation_key = "syncthing" STATE_ATTRIBUTES = { "errors": "errors", @@ -116,15 +115,6 @@ class FolderSensor(SensorEntity): """Could the device be accessed during the last update call.""" return self._state is not None - @property - def icon(self): - """Return the icon for this sensor.""" - if self._state is None: - return FOLDER_SENSOR_DEFAULT_ICON - if self.state in FOLDER_SENSOR_ICONS: - return FOLDER_SENSOR_ICONS[self.state] - return FOLDER_SENSOR_ALERT_ICON - @property def extra_state_attributes(self): """Return the state attributes.""" diff --git a/homeassistant/components/syncthru/__init__.py b/homeassistant/components/syncthru/__init__.py index 8d17f038819..5ad4a85cc09 100644 --- a/homeassistant/components/syncthru/__init__.py +++ b/homeassistant/components/syncthru/__init__.py @@ -1,4 +1,5 @@ """The syncthru component.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/syncthru/binary_sensor.py b/homeassistant/components/syncthru/binary_sensor.py index f5e23ea25ad..2b110c2af1d 100644 --- a/homeassistant/components/syncthru/binary_sensor.py +++ b/homeassistant/components/syncthru/binary_sensor.py @@ -1,4 +1,5 @@ """Support for Samsung Printers with SyncThru web interface.""" + from __future__ import annotations from pysyncthru import SyncThru, SyncthruState diff --git a/homeassistant/components/syncthru/config_flow.py b/homeassistant/components/syncthru/config_flow.py index 664de1f6d96..8cd1c2c7b3b 100644 --- a/homeassistant/components/syncthru/config_flow.py +++ b/homeassistant/components/syncthru/config_flow.py @@ -7,16 +7,15 @@ from pysyncthru import ConnectionMode, SyncThru, SyncThruAPINotSupported from url_normalize import url_normalize import voluptuous as vol -from homeassistant import config_entries from homeassistant.components import ssdp +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_NAME, CONF_URL -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import aiohttp_client from .const import DEFAULT_MODEL, DEFAULT_NAME_TEMPLATE, DOMAIN -class SyncThruConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class SyncThruConfigFlow(ConfigFlow, domain=DOMAIN): """Samsung SyncThru config flow.""" VERSION = 1 @@ -30,7 +29,9 @@ class SyncThruConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return await self._async_show_form(step_id="user") return await self._async_check_and_create("user", user_input) - async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: + async def async_step_ssdp( + self, discovery_info: ssdp.SsdpServiceInfo + ) -> ConfigFlowResult: """Handle SSDP initiated flow.""" await self.async_set_unique_id(discovery_info.upnp[ssdp.ATTR_UPNP_UDN]) self._abort_if_unique_id_configured() diff --git a/homeassistant/components/syncthru/sensor.py b/homeassistant/components/syncthru/sensor.py index f651556bddb..df2ffd99803 100644 --- a/homeassistant/components/syncthru/sensor.py +++ b/homeassistant/components/syncthru/sensor.py @@ -1,4 +1,5 @@ """Support for Samsung Printers with SyncThru web interface.""" + from __future__ import annotations from pysyncthru import SyncThru, SyncthruState @@ -21,7 +22,7 @@ COLORS = ["black", "cyan", "magenta", "yellow"] DRUM_COLORS = COLORS TONER_COLORS = COLORS TRAYS = range(1, 6) -OUTPUT_TRAYS = range(0, 6) +OUTPUT_TRAYS = range(6) DEFAULT_MONITORED_CONDITIONS = [] DEFAULT_MONITORED_CONDITIONS.extend([f"toner_{key}" for key in TONER_COLORS]) DEFAULT_MONITORED_CONDITIONS.extend([f"drum_{key}" for key in DRUM_COLORS]) @@ -61,15 +62,15 @@ async def async_setup_entry( SyncThruMainSensor(coordinator, name), SyncThruActiveAlertSensor(coordinator, name), ] - - for key in supp_toner: - entities.append(SyncThruTonerSensor(coordinator, name, key)) - for key in supp_drum: - entities.append(SyncThruDrumSensor(coordinator, name, key)) - for key in supp_tray: - entities.append(SyncThruInputTraySensor(coordinator, name, key)) - for int_key in supp_output_tray: - entities.append(SyncThruOutputTraySensor(coordinator, name, int_key)) + entities.extend(SyncThruTonerSensor(coordinator, name, key) for key in supp_toner) + entities.extend(SyncThruDrumSensor(coordinator, name, key) for key in supp_drum) + entities.extend( + SyncThruInputTraySensor(coordinator, name, key) for key in supp_tray + ) + entities.extend( + SyncThruOutputTraySensor(coordinator, name, int_key) + for int_key in supp_output_tray + ) async_add_entities(entities) diff --git a/homeassistant/components/synology_chat/notify.py b/homeassistant/components/synology_chat/notify.py index ca8fba53120..a36f073b8bb 100644 --- a/homeassistant/components/synology_chat/notify.py +++ b/homeassistant/components/synology_chat/notify.py @@ -1,4 +1,5 @@ """SynologyChat platform for notify component.""" + from __future__ import annotations from http import HTTPStatus diff --git a/homeassistant/components/synology_dsm/__init__.py b/homeassistant/components/synology_dsm/__init__.py index ecda3addcb5..ec93c92a698 100644 --- a/homeassistant/components/synology_dsm/__init__.py +++ b/homeassistant/components/synology_dsm/__init__.py @@ -1,4 +1,5 @@ """The Synology DSM component.""" + from __future__ import annotations from itertools import chain @@ -40,7 +41,7 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Synology DSM sensors.""" - # Migrate device indentifiers + # Migrate device identifiers dev_reg = dr.async_get(hass) devices: list[dr.DeviceEntry] = dr.async_entries_for_config_entry( dev_reg, entry.entry_id @@ -104,6 +105,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if ( SynoSurveillanceStation.INFO_API_KEY in available_apis and SynoSurveillanceStation.HOME_MODE_API_KEY in available_apis + and api.surveillance_station is not None ): coordinator_switches = SynologyDSMSwitchUpdateCoordinator(hass, entry, api) await coordinator_switches.async_config_entry_first_refresh() diff --git a/homeassistant/components/synology_dsm/binary_sensor.py b/homeassistant/components/synology_dsm/binary_sensor.py index 27c6b416cb4..7579f350774 100644 --- a/homeassistant/components/synology_dsm/binary_sensor.py +++ b/homeassistant/components/synology_dsm/binary_sensor.py @@ -1,4 +1,5 @@ """Support for Synology DSM binary sensors.""" + from __future__ import annotations from dataclasses import dataclass @@ -27,7 +28,7 @@ from .entity import ( from .models import SynologyDSMData -@dataclass(frozen=True) +@dataclass(frozen=True, kw_only=True) class SynologyDSMBinarySensorEntityDescription( BinarySensorEntityDescription, SynologyDSMEntityDescription ): diff --git a/homeassistant/components/synology_dsm/button.py b/homeassistant/components/synology_dsm/button.py index 0e737c48eb6..529682b4c6e 100644 --- a/homeassistant/components/synology_dsm/button.py +++ b/homeassistant/components/synology_dsm/button.py @@ -1,4 +1,5 @@ """Support for Synology DSM buttons.""" + from __future__ import annotations from collections.abc import Callable, Coroutine @@ -24,20 +25,13 @@ from .models import SynologyDSMData LOGGER = logging.getLogger(__name__) -@dataclass(frozen=True) -class SynologyDSMbuttonDescriptionMixin: - """Mixin to describe a Synology DSM button entity.""" +@dataclass(frozen=True, kw_only=True) +class SynologyDSMbuttonDescription(ButtonEntityDescription): + """Class to describe a Synology DSM button entity.""" press_action: Callable[[SynoApi], Callable[[], Coroutine[Any, Any, None]]] -@dataclass(frozen=True) -class SynologyDSMbuttonDescription( - ButtonEntityDescription, SynologyDSMbuttonDescriptionMixin -): - """Class to describe a Synology DSM button entity.""" - - BUTTONS: Final = [ SynologyDSMbuttonDescription( key="reboot", diff --git a/homeassistant/components/synology_dsm/camera.py b/homeassistant/components/synology_dsm/camera.py index 187db9fbba8..19f95c710d0 100644 --- a/homeassistant/components/synology_dsm/camera.py +++ b/homeassistant/components/synology_dsm/camera.py @@ -1,4 +1,5 @@ """Support for Synology DSM cameras.""" + from __future__ import annotations from dataclasses import dataclass @@ -35,7 +36,7 @@ from .models import SynologyDSMData _LOGGER = logging.getLogger(__name__) -@dataclass(frozen=True) +@dataclass(frozen=True, kw_only=True) class SynologyDSMCameraEntityDescription( CameraEntityDescription, SynologyDSMEntityDescription ): diff --git a/homeassistant/components/synology_dsm/common.py b/homeassistant/components/synology_dsm/common.py index b5a2c7bfad5..4bb52383148 100644 --- a/homeassistant/components/synology_dsm/common.py +++ b/homeassistant/components/synology_dsm/common.py @@ -1,4 +1,5 @@ """The Synology DSM component.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/synology_dsm/config_flow.py b/homeassistant/components/synology_dsm/config_flow.py index f49eb7feed1..c77b8196faf 100644 --- a/homeassistant/components/synology_dsm/config_flow.py +++ b/homeassistant/components/synology_dsm/config_flow.py @@ -1,4 +1,5 @@ """Config flow to configure the Synology DSM integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -17,9 +18,13 @@ from synology_dsm.exceptions import ( ) import voluptuous as vol -from homeassistant import exceptions from homeassistant.components import ssdp, zeroconf -from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.const import ( CONF_DISKS, CONF_HOST, @@ -34,7 +39,7 @@ from homeassistant.const import ( CONF_VERIFY_SSL, ) from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import DiscoveryInfoType @@ -130,7 +135,7 @@ class SynologyDSMFlowHandler(ConfigFlow, domain=DOMAIN): step_id: str, user_input: dict[str, Any] | None = None, errors: dict[str, str] | None = None, - ) -> FlowResult: + ) -> ConfigFlowResult: """Show the setup form to the user.""" if not user_input: user_input = {} @@ -156,7 +161,7 @@ class SynologyDSMFlowHandler(ConfigFlow, domain=DOMAIN): async def async_validate_input_create_entry( self, user_input: dict[str, Any], step_id: str - ) -> FlowResult: + ) -> ConfigFlowResult: """Process user input and create new or update existing config entry.""" host = user_input[CONF_HOST] port = user_input.get(CONF_PORT) @@ -231,7 +236,7 @@ class SynologyDSMFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initiated by the user.""" step = "user" if not user_input: @@ -240,7 +245,7 @@ class SynologyDSMFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_zeroconf( self, discovery_info: zeroconf.ZeroconfServiceInfo - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a discovered synology_dsm via zeroconf.""" discovered_macs = [ format_synology_mac(mac) @@ -253,7 +258,9 @@ class SynologyDSMFlowHandler(ConfigFlow, domain=DOMAIN): friendly_name = discovery_info.name.removesuffix(HTTP_SUFFIX) return await self._async_from_discovery(host, friendly_name, discovered_macs) - async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: + async def async_step_ssdp( + self, discovery_info: ssdp.SsdpServiceInfo + ) -> ConfigFlowResult: """Handle a discovered synology_dsm via ssdp.""" parsed_url = urlparse(discovery_info.ssdp_location) upnp_friendly_name: str = discovery_info.upnp[ssdp.ATTR_UPNP_FRIENDLY_NAME] @@ -267,7 +274,7 @@ class SynologyDSMFlowHandler(ConfigFlow, domain=DOMAIN): async def _async_from_discovery( self, host: str, friendly_name: str, discovered_macs: list[str] - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a discovered synology_dsm via zeroconf or ssdp.""" existing_entry = None for discovered_mac in discovered_macs: @@ -307,7 +314,7 @@ class SynologyDSMFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_link( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Link a config entry from discovery.""" step = "link" if not user_input: @@ -315,7 +322,9 @@ class SynologyDSMFlowHandler(ConfigFlow, domain=DOMAIN): user_input = {**self.discovered_conf, **user_input} return await self.async_validate_input_create_entry(user_input, step_id=step) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Perform reauth upon an API authentication error.""" self.reauth_conf = entry_data self.context["title_placeholders"][CONF_HOST] = entry_data[CONF_HOST] @@ -324,7 +333,7 @@ class SynologyDSMFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Perform reauth confirm upon an API authentication error.""" step = "reauth_confirm" if not user_input: @@ -334,7 +343,7 @@ class SynologyDSMFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_2sa( self, user_input: dict[str, Any], errors: dict[str, str] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Enter 2SA code to anthenticate.""" if not self.saved_user_input: self.saved_user_input = user_input @@ -370,7 +379,7 @@ class SynologyDSMOptionsFlowHandler(OptionsFlow): async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle options flow.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) @@ -419,5 +428,5 @@ async def _login_and_fetch_syno_info(api: SynologyDSM, otp_code: str | None) -> return api.information.serial # type: ignore[no-any-return] -class InvalidData(exceptions.HomeAssistantError): +class InvalidData(HomeAssistantError): """Error to indicate we get invalid data from the nas.""" diff --git a/homeassistant/components/synology_dsm/const.py b/homeassistant/components/synology_dsm/const.py index c5c9e590684..140e07e975b 100644 --- a/homeassistant/components/synology_dsm/const.py +++ b/homeassistant/components/synology_dsm/const.py @@ -1,4 +1,5 @@ """Constants for Synology DSM.""" + from __future__ import annotations from synology_dsm.api.surveillance_station.const import SNAPSHOT_PROFILE_BALANCED diff --git a/homeassistant/components/synology_dsm/coordinator.py b/homeassistant/components/synology_dsm/coordinator.py index 9d0ccfd86d2..bc896b1ad45 100644 --- a/homeassistant/components/synology_dsm/coordinator.py +++ b/homeassistant/components/synology_dsm/coordinator.py @@ -1,4 +1,5 @@ """synology_dsm coordinators.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/synology_dsm/diagnostics.py b/homeassistant/components/synology_dsm/diagnostics.py index 30af7f94282..d9b4131b078 100644 --- a/homeassistant/components/synology_dsm/diagnostics.py +++ b/homeassistant/components/synology_dsm/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for Synology DSM.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/synology_dsm/entity.py b/homeassistant/components/synology_dsm/entity.py index 8d53284fee7..4bd1e526194 100644 --- a/homeassistant/components/synology_dsm/entity.py +++ b/homeassistant/components/synology_dsm/entity.py @@ -1,4 +1,5 @@ """Entities for Synology DSM.""" + from __future__ import annotations from dataclasses import dataclass @@ -18,18 +19,13 @@ from .coordinator import ( _CoordinatorT = TypeVar("_CoordinatorT", bound=SynologyDSMUpdateCoordinator[Any]) -@dataclass(frozen=True) -class SynologyDSMRequiredKeysMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class SynologyDSMEntityDescription(EntityDescription): + """Generic Synology DSM entity description.""" api_key: str -@dataclass(frozen=True) -class SynologyDSMEntityDescription(EntityDescription, SynologyDSMRequiredKeysMixin): - """Generic Synology DSM entity description.""" - - class SynologyDSMBaseEntity(CoordinatorEntity[_CoordinatorT]): """Representation of a Synology NAS entry.""" diff --git a/homeassistant/components/synology_dsm/icons.json b/homeassistant/components/synology_dsm/icons.json new file mode 100644 index 00000000000..8b4fad457d5 --- /dev/null +++ b/homeassistant/components/synology_dsm/icons.json @@ -0,0 +1,81 @@ +{ + "entity": { + "sensor": { + "cpu_other_load": { + "default": "mdi:chip" + }, + "cpu_user_load": { + "default": "mdi:chip" + }, + "cpu_system_load": { + "default": "mdi:chip" + }, + "cpu_total_load": { + "default": "mdi:chip" + }, + "cpu_1min_load": { + "default": "mdi:chip" + }, + "cpu_5min_load": { + "default": "mdi:chip" + }, + "cpu_15min_load": { + "default": "mdi:chip" + }, + "memory_real_usage": { + "default": "mdi:memory" + }, + "memory_size": { + "default": "mdi:memory" + }, + "memory_cached": { + "default": "mdi:memory" + }, + "memory_available_swap": { + "default": "mdi:memory" + }, + "memory_available_real": { + "default": "mdi:memory" + }, + "memory_total_swap": { + "default": "mdi:memory" + }, + "memory_total_real": { + "default": "mdi:memory" + }, + "network_up": { + "default": "mdi:upload" + }, + "network_down": { + "default": "mdi:download" + }, + "volume_status": { + "default": "mdi:checkbox-marked-circle-outline" + }, + "volume_size_total": { + "default": "mdi:chart-pie" + }, + "volume_size_used": { + "default": "mdi:chart-pie" + }, + "volume_percentage_used": { + "default": "mdi:chart-pie" + }, + "disk_smart_status": { + "default": "mdi:checkbox-marked-circle-outline" + }, + "disk_status": { + "default": "mdi:checkbox-marked-circle-outline" + } + }, + "switch": { + "home_mode": { + "default": "mdi:home-account" + } + } + }, + "services": { + "reboot": "mdi:restart", + "shutdown": "mdi:power" + } +} diff --git a/homeassistant/components/synology_dsm/manifest.json b/homeassistant/components/synology_dsm/manifest.json index 2820c99f889..8060bce5c9b 100644 --- a/homeassistant/components/synology_dsm/manifest.json +++ b/homeassistant/components/synology_dsm/manifest.json @@ -5,7 +5,6 @@ "config_flow": true, "dependencies": ["http"], "documentation": "https://www.home-assistant.io/integrations/synology_dsm", - "import_executor": true, "iot_class": "local_polling", "loggers": ["synology_dsm"], "requirements": ["py-synologydsm-api==2.1.4"], diff --git a/homeassistant/components/synology_dsm/media_source.py b/homeassistant/components/synology_dsm/media_source.py index 3f30fe9b4e9..9a393813c3e 100644 --- a/homeassistant/components/synology_dsm/media_source.py +++ b/homeassistant/components/synology_dsm/media_source.py @@ -1,4 +1,5 @@ """Expose Synology DSM as a media source.""" + from __future__ import annotations import mimetypes @@ -90,20 +91,18 @@ class SynologyPhotosMediaSource(MediaSource): ) -> list[BrowseMediaSource]: """Handle browsing different diskstations.""" if not item.identifier: - ret = [] - for entry in self.entries: - ret.append( - BrowseMediaSource( - domain=DOMAIN, - identifier=entry.unique_id, - media_class=MediaClass.DIRECTORY, - media_content_type=MediaClass.IMAGE, - title=f"{entry.title} - {entry.unique_id}", - can_play=False, - can_expand=True, - ) + return [ + BrowseMediaSource( + domain=DOMAIN, + identifier=entry.unique_id, + media_class=MediaClass.DIRECTORY, + media_content_type=MediaClass.IMAGE, + title=f"{entry.title} - {entry.unique_id}", + can_play=False, + can_expand=True, ) - return ret + for entry in self.entries + ] identifier = SynologyPhotosMediaSourceIdentifier(item.identifier) diskstation: SynologyDSMData = self.hass.data[DOMAIN][identifier.unique_id] @@ -125,18 +124,18 @@ class SynologyPhotosMediaSource(MediaSource): can_expand=True, ) ] - for album in albums: - ret.append( - BrowseMediaSource( - domain=DOMAIN, - identifier=f"{item.identifier}/{album.album_id}", - media_class=MediaClass.DIRECTORY, - media_content_type=MediaClass.IMAGE, - title=album.name, - can_play=False, - can_expand=True, - ) + ret.extend( + BrowseMediaSource( + domain=DOMAIN, + identifier=f"{item.identifier}/{album.album_id}", + media_class=MediaClass.DIRECTORY, + media_content_type=MediaClass.IMAGE, + title=album.name, + can_play=False, + can_expand=True, ) + for album in albums + ) return ret @@ -213,18 +212,18 @@ class SynologyDsmMediaView(http.HomeAssistantView): ) -> web.Response: """Start a GET request.""" if not self.hass.data.get(DOMAIN): - raise web.HTTPNotFound() + raise web.HTTPNotFound # location: {cache_key}/{filename} cache_key, file_name = location.split("/") image_id = cache_key.split("_")[0] mime_type, _ = mimetypes.guess_type(file_name) if not isinstance(mime_type, str): - raise web.HTTPNotFound() + raise web.HTTPNotFound diskstation: SynologyDSMData = self.hass.data[DOMAIN][source_dir_id] item = SynoPhotosItem(image_id, "", "", "", cache_key, "") try: image = await diskstation.api.photos.download_item(item) except SynologyDSMException as exc: - raise web.HTTPNotFound() from exc + raise web.HTTPNotFound from exc return web.Response(body=image, content_type=mime_type) diff --git a/homeassistant/components/synology_dsm/models.py b/homeassistant/components/synology_dsm/models.py index 8c4341a2d37..4f51d329ded 100644 --- a/homeassistant/components/synology_dsm/models.py +++ b/homeassistant/components/synology_dsm/models.py @@ -1,4 +1,5 @@ """The synology_dsm integration models.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/homeassistant/components/synology_dsm/sensor.py b/homeassistant/components/synology_dsm/sensor.py index 76606303c93..47483ee4a63 100644 --- a/homeassistant/components/synology_dsm/sensor.py +++ b/homeassistant/components/synology_dsm/sensor.py @@ -1,4 +1,5 @@ """Support for Synology DSM sensors.""" + from __future__ import annotations from dataclasses import dataclass @@ -39,7 +40,7 @@ from .entity import ( from .models import SynologyDSMData -@dataclass(frozen=True) +@dataclass(frozen=True, kw_only=True) class SynologyDSMSensorEntityDescription( SensorEntityDescription, SynologyDSMEntityDescription ): @@ -52,7 +53,6 @@ UTILISATION_SENSORS: tuple[SynologyDSMSensorEntityDescription, ...] = ( key="cpu_other_load", translation_key="cpu_other_load", native_unit_of_measurement=PERCENTAGE, - icon="mdi:chip", entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, ), @@ -61,7 +61,6 @@ UTILISATION_SENSORS: tuple[SynologyDSMSensorEntityDescription, ...] = ( key="cpu_user_load", translation_key="cpu_user_load", native_unit_of_measurement=PERCENTAGE, - icon="mdi:chip", state_class=SensorStateClass.MEASUREMENT, ), SynologyDSMSensorEntityDescription( @@ -69,7 +68,6 @@ UTILISATION_SENSORS: tuple[SynologyDSMSensorEntityDescription, ...] = ( key="cpu_system_load", translation_key="cpu_system_load", native_unit_of_measurement=PERCENTAGE, - icon="mdi:chip", entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, ), @@ -78,7 +76,6 @@ UTILISATION_SENSORS: tuple[SynologyDSMSensorEntityDescription, ...] = ( key="cpu_total_load", translation_key="cpu_total_load", native_unit_of_measurement=PERCENTAGE, - icon="mdi:chip", state_class=SensorStateClass.MEASUREMENT, ), SynologyDSMSensorEntityDescription( @@ -86,7 +83,6 @@ UTILISATION_SENSORS: tuple[SynologyDSMSensorEntityDescription, ...] = ( key="cpu_1min_load", translation_key="cpu_1min_load", native_unit_of_measurement=ENTITY_UNIT_LOAD, - icon="mdi:chip", entity_registry_enabled_default=False, ), SynologyDSMSensorEntityDescription( @@ -94,21 +90,18 @@ UTILISATION_SENSORS: tuple[SynologyDSMSensorEntityDescription, ...] = ( key="cpu_5min_load", translation_key="cpu_5min_load", native_unit_of_measurement=ENTITY_UNIT_LOAD, - icon="mdi:chip", ), SynologyDSMSensorEntityDescription( api_key=SynoCoreUtilization.API_KEY, key="cpu_15min_load", translation_key="cpu_15min_load", native_unit_of_measurement=ENTITY_UNIT_LOAD, - icon="mdi:chip", ), SynologyDSMSensorEntityDescription( api_key=SynoCoreUtilization.API_KEY, key="memory_real_usage", translation_key="memory_real_usage", native_unit_of_measurement=PERCENTAGE, - icon="mdi:memory", state_class=SensorStateClass.MEASUREMENT, ), SynologyDSMSensorEntityDescription( @@ -119,7 +112,6 @@ UTILISATION_SENSORS: tuple[SynologyDSMSensorEntityDescription, ...] = ( suggested_unit_of_measurement=UnitOfInformation.MEGABYTES, suggested_display_precision=1, device_class=SensorDeviceClass.DATA_SIZE, - icon="mdi:memory", entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, ), @@ -131,7 +123,6 @@ UTILISATION_SENSORS: tuple[SynologyDSMSensorEntityDescription, ...] = ( suggested_unit_of_measurement=UnitOfInformation.MEGABYTES, suggested_display_precision=1, device_class=SensorDeviceClass.DATA_SIZE, - icon="mdi:memory", entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, ), @@ -143,7 +134,6 @@ UTILISATION_SENSORS: tuple[SynologyDSMSensorEntityDescription, ...] = ( suggested_unit_of_measurement=UnitOfInformation.MEGABYTES, suggested_display_precision=1, device_class=SensorDeviceClass.DATA_SIZE, - icon="mdi:memory", state_class=SensorStateClass.MEASUREMENT, ), SynologyDSMSensorEntityDescription( @@ -154,7 +144,6 @@ UTILISATION_SENSORS: tuple[SynologyDSMSensorEntityDescription, ...] = ( suggested_unit_of_measurement=UnitOfInformation.MEGABYTES, suggested_display_precision=1, device_class=SensorDeviceClass.DATA_SIZE, - icon="mdi:memory", state_class=SensorStateClass.MEASUREMENT, ), SynologyDSMSensorEntityDescription( @@ -165,7 +154,6 @@ UTILISATION_SENSORS: tuple[SynologyDSMSensorEntityDescription, ...] = ( suggested_unit_of_measurement=UnitOfInformation.MEGABYTES, suggested_display_precision=1, device_class=SensorDeviceClass.DATA_SIZE, - icon="mdi:memory", state_class=SensorStateClass.MEASUREMENT, ), SynologyDSMSensorEntityDescription( @@ -176,7 +164,6 @@ UTILISATION_SENSORS: tuple[SynologyDSMSensorEntityDescription, ...] = ( suggested_unit_of_measurement=UnitOfInformation.MEGABYTES, suggested_display_precision=1, device_class=SensorDeviceClass.DATA_SIZE, - icon="mdi:memory", state_class=SensorStateClass.MEASUREMENT, ), SynologyDSMSensorEntityDescription( @@ -187,7 +174,6 @@ UTILISATION_SENSORS: tuple[SynologyDSMSensorEntityDescription, ...] = ( suggested_unit_of_measurement=UnitOfDataRate.KILOBYTES_PER_SECOND, suggested_display_precision=1, device_class=SensorDeviceClass.DATA_RATE, - icon="mdi:upload", state_class=SensorStateClass.MEASUREMENT, ), SynologyDSMSensorEntityDescription( @@ -198,7 +184,6 @@ UTILISATION_SENSORS: tuple[SynologyDSMSensorEntityDescription, ...] = ( suggested_unit_of_measurement=UnitOfDataRate.KILOBYTES_PER_SECOND, suggested_display_precision=1, device_class=SensorDeviceClass.DATA_RATE, - icon="mdi:download", state_class=SensorStateClass.MEASUREMENT, ), ) @@ -207,7 +192,6 @@ STORAGE_VOL_SENSORS: tuple[SynologyDSMSensorEntityDescription, ...] = ( api_key=SynoStorage.API_KEY, key="volume_status", translation_key="volume_status", - icon="mdi:checkbox-marked-circle-outline", ), SynologyDSMSensorEntityDescription( api_key=SynoStorage.API_KEY, @@ -217,7 +201,6 @@ STORAGE_VOL_SENSORS: tuple[SynologyDSMSensorEntityDescription, ...] = ( suggested_unit_of_measurement=UnitOfInformation.TERABYTES, suggested_display_precision=2, device_class=SensorDeviceClass.DATA_SIZE, - icon="mdi:chart-pie", entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, ), @@ -229,7 +212,6 @@ STORAGE_VOL_SENSORS: tuple[SynologyDSMSensorEntityDescription, ...] = ( suggested_unit_of_measurement=UnitOfInformation.TERABYTES, suggested_display_precision=2, device_class=SensorDeviceClass.DATA_SIZE, - icon="mdi:chart-pie", state_class=SensorStateClass.MEASUREMENT, ), SynologyDSMSensorEntityDescription( @@ -237,7 +219,6 @@ STORAGE_VOL_SENSORS: tuple[SynologyDSMSensorEntityDescription, ...] = ( key="volume_percentage_used", translation_key="volume_percentage_used", native_unit_of_measurement=PERCENTAGE, - icon="mdi:chart-pie", ), SynologyDSMSensorEntityDescription( api_key=SynoStorage.API_KEY, @@ -262,7 +243,6 @@ STORAGE_DISK_SENSORS: tuple[SynologyDSMSensorEntityDescription, ...] = ( api_key=SynoStorage.API_KEY, key="disk_smart_status", translation_key="disk_smart_status", - icon="mdi:checkbox-marked-circle-outline", entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, ), @@ -270,7 +250,6 @@ STORAGE_DISK_SENSORS: tuple[SynologyDSMSensorEntityDescription, ...] = ( api_key=SynoStorage.API_KEY, key="disk_status", translation_key="disk_status", - icon="mdi:checkbox-marked-circle-outline", entity_category=EntityCategory.DIAGNOSTIC, ), SynologyDSMSensorEntityDescription( diff --git a/homeassistant/components/synology_dsm/service.py b/homeassistant/components/synology_dsm/service.py index 9797b808617..366f7d4ba3a 100644 --- a/homeassistant/components/synology_dsm/service.py +++ b/homeassistant/components/synology_dsm/service.py @@ -1,4 +1,5 @@ """The Synology DSM component.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/synology_dsm/switch.py b/homeassistant/components/synology_dsm/switch.py index 77dc854fa3a..6e1e38675a0 100644 --- a/homeassistant/components/synology_dsm/switch.py +++ b/homeassistant/components/synology_dsm/switch.py @@ -1,4 +1,5 @@ """Support for Synology DSM switch.""" + from __future__ import annotations from dataclasses import dataclass @@ -22,7 +23,7 @@ from .models import SynologyDSMData _LOGGER = logging.getLogger(__name__) -@dataclass(frozen=True) +@dataclass(frozen=True, kw_only=True) class SynologyDSMSwitchEntityDescription( SwitchEntityDescription, SynologyDSMEntityDescription ): @@ -34,7 +35,6 @@ SURVEILLANCE_SWITCH: tuple[SynologyDSMSwitchEntityDescription, ...] = ( api_key=SynoSurveillanceStation.HOME_MODE_API_KEY, key="home_mode", translation_key="home_mode", - icon="mdi:home-account", ), ) diff --git a/homeassistant/components/synology_dsm/update.py b/homeassistant/components/synology_dsm/update.py index c66fc3c3d73..7b1a36c57b3 100644 --- a/homeassistant/components/synology_dsm/update.py +++ b/homeassistant/components/synology_dsm/update.py @@ -1,4 +1,5 @@ """Support for Synology DSM update platform.""" + from __future__ import annotations from dataclasses import dataclass @@ -19,7 +20,7 @@ from .entity import SynologyDSMBaseEntity, SynologyDSMEntityDescription from .models import SynologyDSMData -@dataclass(frozen=True) +@dataclass(frozen=True, kw_only=True) class SynologyDSMUpdateEntityEntityDescription( UpdateEntityDescription, SynologyDSMEntityDescription ): diff --git a/homeassistant/components/synology_srm/device_tracker.py b/homeassistant/components/synology_srm/device_tracker.py index e67f7ecf34e..7c7343e88f6 100644 --- a/homeassistant/components/synology_srm/device_tracker.py +++ b/homeassistant/components/synology_srm/device_tracker.py @@ -1,4 +1,5 @@ """Device tracker for Synology SRM routers.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/syslog/notify.py b/homeassistant/components/syslog/notify.py index d4772e95259..b16d44fb504 100644 --- a/homeassistant/components/syslog/notify.py +++ b/homeassistant/components/syslog/notify.py @@ -1,4 +1,5 @@ """Syslog notification service.""" + from __future__ import annotations import syslog diff --git a/homeassistant/components/system_bridge/__init__.py b/homeassistant/components/system_bridge/__init__.py index d2f5c795b7f..03ef06dc914 100644 --- a/homeassistant/components/system_bridge/__init__.py +++ b/homeassistant/components/system_bridge/__init__.py @@ -1,17 +1,21 @@ """The System Bridge integration.""" + from __future__ import annotations import asyncio +from dataclasses import asdict import logging +from typing import Any from systembridgeconnector.exceptions import ( AuthenticationException, ConnectionClosedException, ConnectionErrorException, ) -from systembridgeconnector.version import SUPPORTED_VERSION, Version +from systembridgeconnector.version import Version from systembridgemodels.keyboard_key import KeyboardKey from systembridgemodels.keyboard_text import KeyboardText +from systembridgemodels.modules.processes import Process from systembridgemodels.open_path import OpenPath from systembridgemodels.open_url import OpenUrl import voluptuous as vol @@ -22,21 +26,34 @@ from homeassistant.const import ( CONF_COMMAND, CONF_ENTITY_ID, CONF_HOST, + CONF_ID, CONF_NAME, CONF_PATH, CONF_PORT, + CONF_TOKEN, CONF_URL, Platform, ) -from homeassistant.core import HomeAssistant, ServiceCall -from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.core import ( + HomeAssistant, + ServiceCall, + ServiceResponse, + SupportsResponse, +) +from homeassistant.exceptions import ( + ConfigEntryAuthFailed, + ConfigEntryNotReady, + ServiceValidationError, +) from homeassistant.helpers import ( config_validation as cv, device_registry as dr, discovery, ) from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue +from .config_flow import SystemBridgeConfigFlow from .const import DOMAIN, MODULES from .coordinator import SystemBridgeDataUpdateCoordinator @@ -54,6 +71,8 @@ CONF_BRIDGE = "bridge" CONF_KEY = "key" CONF_TEXT = "text" +SERVICE_GET_PROCESS_BY_ID = "get_process_by_id" +SERVICE_GET_PROCESSES_BY_NAME = "get_processes_by_name" SERVICE_OPEN_PATH = "open_path" SERVICE_POWER_COMMAND = "power_command" SERVICE_OPEN_URL = "open_url" @@ -80,16 +99,13 @@ async def async_setup_entry( version = Version( entry.data[CONF_HOST], entry.data[CONF_PORT], - entry.data[CONF_API_KEY], + entry.data[CONF_TOKEN], session=async_get_clientsession(hass), ) + supported = False try: async with asyncio.timeout(10): - if not await version.check_supported(): - raise ConfigEntryNotReady( - "You are not running a supported version of System Bridge. Please" - f" update to {SUPPORTED_VERSION} or higher." - ) + supported = await version.check_supported() except AuthenticationException as exception: _LOGGER.error("Authentication failed for %s: %s", entry.title, exception) raise ConfigEntryAuthFailed from exception @@ -102,6 +118,21 @@ async def async_setup_entry( f"Timed out waiting for {entry.title} ({entry.data[CONF_HOST]})." ) from exception + # If not supported, create an issue and raise ConfigEntryNotReady + if not supported: + async_create_issue( + hass=hass, + domain=DOMAIN, + issue_id=f"system_bridge_{entry.entry_id}_unsupported_version", + translation_key="unsupported_version", + translation_placeholders={"host": entry.data[CONF_HOST]}, + severity=IssueSeverity.ERROR, + is_fixable=False, + ) + raise ConfigEntryNotReady( + "You are not running a supported version of System Bridge. Please update to the latest version." + ) + coordinator = SystemBridgeDataUpdateCoordinator( hass, _LOGGER, @@ -122,6 +153,7 @@ async def async_setup_entry( f"Timed out waiting for {entry.title} ({entry.data[CONF_HOST]})." ) from exception + # Fetch initial data so we have data when entities subscribe await coordinator.async_config_entry_first_refresh() try: @@ -139,13 +171,6 @@ async def async_setup_entry( f"Timed out waiting for {entry.title} ({entry.data[CONF_HOST]})." ) from exception - _LOGGER.debug( - "Initial coordinator data for %s (%s):\n%s", - entry.title, - entry.data[CONF_HOST], - coordinator.data.json(), - ) - hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator @@ -183,55 +208,134 @@ async def async_setup_entry( if entry.entry_id in device_entry.config_entries ) except StopIteration as exception: - raise vol.Invalid from exception + raise vol.Invalid(f"Could not find device {device}") from exception raise vol.Invalid(f"Device {device} does not exist") - async def handle_open_path(call: ServiceCall) -> None: + async def handle_get_process_by_id(service_call: ServiceCall) -> ServiceResponse: + """Handle the get process by id service call.""" + _LOGGER.debug("Get process by id: %s", service_call.data) + coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][ + service_call.data[CONF_BRIDGE] + ] + processes: list[Process] = coordinator.data.processes + + # Find process.id from list, raise ServiceValidationError if not found + try: + return asdict( + next( + process + for process in processes + if process.id == service_call.data[CONF_ID] + ) + ) + except StopIteration as exception: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="process_not_found", + translation_placeholders={"id": service_call.data[CONF_ID]}, + ) from exception + + async def handle_get_processes_by_name( + service_call: ServiceCall, + ) -> ServiceResponse: + """Handle the get process by name service call.""" + _LOGGER.debug("Get process by name: %s", service_call.data) + coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][ + service_call.data[CONF_BRIDGE] + ] + processes: list[Process] = coordinator.data.processes + # Find processes from list + items: list[dict[str, Any]] = [ + asdict(process) + for process in processes + if process.name is not None + and service_call.data[CONF_NAME].lower() in process.name.lower() + ] + + return { + "count": len(items), + "processes": list(items), + } + + async def handle_open_path(service_call: ServiceCall) -> ServiceResponse: """Handle the open path service call.""" - _LOGGER.info("Open: %s", call.data) + _LOGGER.debug("Open path: %s", service_call.data) coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][ - call.data[CONF_BRIDGE] + service_call.data[CONF_BRIDGE] ] - await coordinator.websocket_client.open_path( - OpenPath(path=call.data[CONF_PATH]) + response = await coordinator.websocket_client.open_path( + OpenPath(path=service_call.data[CONF_PATH]) ) + return asdict(response) - async def handle_power_command(call: ServiceCall) -> None: + async def handle_power_command(service_call: ServiceCall) -> ServiceResponse: """Handle the power command service call.""" - _LOGGER.info("Power command: %s", call.data) + _LOGGER.debug("Power command: %s", service_call.data) coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][ - call.data[CONF_BRIDGE] + service_call.data[CONF_BRIDGE] ] - await getattr( + response = await getattr( coordinator.websocket_client, - POWER_COMMAND_MAP[call.data[CONF_COMMAND]], + POWER_COMMAND_MAP[service_call.data[CONF_COMMAND]], )() + return asdict(response) - async def handle_open_url(call: ServiceCall) -> None: + async def handle_open_url(service_call: ServiceCall) -> ServiceResponse: """Handle the open url service call.""" - _LOGGER.info("Open: %s", call.data) + _LOGGER.debug("Open URL: %s", service_call.data) coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][ - call.data[CONF_BRIDGE] + service_call.data[CONF_BRIDGE] ] - await coordinator.websocket_client.open_url(OpenUrl(url=call.data[CONF_URL])) + response = await coordinator.websocket_client.open_url( + OpenUrl(url=service_call.data[CONF_URL]) + ) + return asdict(response) - async def handle_send_keypress(call: ServiceCall) -> None: + async def handle_send_keypress(service_call: ServiceCall) -> ServiceResponse: """Handle the send_keypress service call.""" coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][ - call.data[CONF_BRIDGE] + service_call.data[CONF_BRIDGE] ] - await coordinator.websocket_client.keyboard_keypress( - KeyboardKey(key=call.data[CONF_KEY]) + response = await coordinator.websocket_client.keyboard_keypress( + KeyboardKey(key=service_call.data[CONF_KEY]) ) + return asdict(response) - async def handle_send_text(call: ServiceCall) -> None: + async def handle_send_text(service_call: ServiceCall) -> ServiceResponse: """Handle the send_keypress service call.""" coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][ - call.data[CONF_BRIDGE] + service_call.data[CONF_BRIDGE] ] - await coordinator.websocket_client.keyboard_text( - KeyboardText(text=call.data[CONF_TEXT]) + response = await coordinator.websocket_client.keyboard_text( + KeyboardText(text=service_call.data[CONF_TEXT]) ) + return asdict(response) + + hass.services.async_register( + DOMAIN, + SERVICE_GET_PROCESS_BY_ID, + handle_get_process_by_id, + schema=vol.Schema( + { + vol.Required(CONF_BRIDGE): valid_device, + vol.Required(CONF_ID): cv.positive_int, + }, + ), + supports_response=SupportsResponse.ONLY, + ) + + hass.services.async_register( + DOMAIN, + SERVICE_GET_PROCESSES_BY_NAME, + handle_get_processes_by_name, + schema=vol.Schema( + { + vol.Required(CONF_BRIDGE): valid_device, + vol.Required(CONF_NAME): cv.string, + }, + ), + supports_response=SupportsResponse.ONLY, + ) hass.services.async_register( DOMAIN, @@ -243,6 +347,7 @@ async def async_setup_entry( vol.Required(CONF_PATH): cv.string, }, ), + supports_response=SupportsResponse.ONLY, ) hass.services.async_register( @@ -255,6 +360,7 @@ async def async_setup_entry( vol.Required(CONF_COMMAND): vol.In(POWER_COMMAND_MAP), }, ), + supports_response=SupportsResponse.ONLY, ) hass.services.async_register( @@ -267,6 +373,7 @@ async def async_setup_entry( vol.Required(CONF_URL): cv.string, }, ), + supports_response=SupportsResponse.ONLY, ) hass.services.async_register( @@ -279,6 +386,7 @@ async def async_setup_entry( vol.Required(CONF_KEY): cv.string, }, ), + supports_response=SupportsResponse.ONLY, ) hass.services.async_register( @@ -291,6 +399,7 @@ async def async_setup_entry( vol.Required(CONF_TEXT): cv.string, }, ), + supports_response=SupportsResponse.ONLY, ) # Reload entry when its updated. @@ -328,3 +437,34 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: """Reload the config entry when it changed.""" await hass.config_entries.async_reload(entry.entry_id) + + +async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Migrate old entry.""" + _LOGGER.debug( + "Migrating from version %s.%s", + config_entry.version, + config_entry.minor_version, + ) + + if config_entry.version > SystemBridgeConfigFlow.VERSION: + return False + + if config_entry.minor_version < 2: + # Migrate to CONF_TOKEN, which was added in 1.2 + new_data = dict(config_entry.data) + new_data.setdefault(CONF_TOKEN, config_entry.data.get(CONF_API_KEY)) + + hass.config_entries.async_update_entry( + config_entry, + data=new_data, + minor_version=2, + ) + + _LOGGER.debug( + "Migration to version %s.%s successful", + config_entry.version, + config_entry.minor_version, + ) + + return True diff --git a/homeassistant/components/system_bridge/binary_sensor.py b/homeassistant/components/system_bridge/binary_sensor.py index 7c2607e3506..019b1df4639 100644 --- a/homeassistant/components/system_bridge/binary_sensor.py +++ b/homeassistant/components/system_bridge/binary_sensor.py @@ -1,4 +1,5 @@ """Support for System Bridge binary sensors.""" + from __future__ import annotations from collections.abc import Callable @@ -16,6 +17,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN from .coordinator import SystemBridgeDataUpdateCoordinator +from .data import SystemBridgeData from .entity import SystemBridgeEntity @@ -23,14 +25,33 @@ from .entity import SystemBridgeEntity class SystemBridgeBinarySensorEntityDescription(BinarySensorEntityDescription): """Class describing System Bridge binary sensor entities.""" - value: Callable = round + value_fn: Callable = round + + +def camera_in_use(data: SystemBridgeData) -> bool | None: + """Return if any camera is in use.""" + if data.system.camera_usage is not None: + return len(data.system.camera_usage) > 0 + return None BASE_BINARY_SENSOR_TYPES: tuple[SystemBridgeBinarySensorEntityDescription, ...] = ( + SystemBridgeBinarySensorEntityDescription( + key="camera_in_use", + translation_key="camera_in_use", + icon="mdi:webcam", + value_fn=camera_in_use, + ), + SystemBridgeBinarySensorEntityDescription( + key="pending_reboot", + translation_key="pending_reboot", + icon="mdi:restart", + value_fn=lambda data: data.system.pending_reboot, + ), SystemBridgeBinarySensorEntityDescription( key="version_available", device_class=BinarySensorDeviceClass.UPDATE, - value=lambda data: data.system.version_newer_available, + value_fn=lambda data: data.system.version_newer_available, ), ) @@ -38,7 +59,7 @@ BATTERY_BINARY_SENSOR_TYPES: tuple[SystemBridgeBinarySensorEntityDescription, .. SystemBridgeBinarySensorEntityDescription( key="battery_is_charging", device_class=BinarySensorDeviceClass.BATTERY_CHARGING, - value=lambda data: data.battery.is_charging, + value_fn=lambda data: data.battery.is_charging, ), ) @@ -49,23 +70,20 @@ async def async_setup_entry( """Set up System Bridge binary sensor based on a config entry.""" coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - entities = [] - for description in BASE_BINARY_SENSOR_TYPES: - entities.append( - SystemBridgeBinarySensor(coordinator, description, entry.data[CONF_PORT]) - ) + entities = [ + SystemBridgeBinarySensor(coordinator, description, entry.data[CONF_PORT]) + for description in BASE_BINARY_SENSOR_TYPES + ] if ( coordinator.data.battery and coordinator.data.battery.percentage and coordinator.data.battery.percentage > -1 ): - for description in BATTERY_BINARY_SENSOR_TYPES: - entities.append( - SystemBridgeBinarySensor( - coordinator, description, entry.data[CONF_PORT] - ) - ) + entities.extend( + SystemBridgeBinarySensor(coordinator, description, entry.data[CONF_PORT]) + for description in BATTERY_BINARY_SENSOR_TYPES + ) async_add_entities(entities) @@ -92,4 +110,4 @@ class SystemBridgeBinarySensor(SystemBridgeEntity, BinarySensorEntity): @property def is_on(self) -> bool: """Return the boolean state of the binary sensor.""" - return self.entity_description.value(self.coordinator.data) + return self.entity_description.value_fn(self.coordinator.data) diff --git a/homeassistant/components/system_bridge/config_flow.py b/homeassistant/components/system_bridge/config_flow.py index 0b6a8b4622b..ff24a2c730f 100644 --- a/homeassistant/components/system_bridge/config_flow.py +++ b/homeassistant/components/system_bridge/config_flow.py @@ -1,4 +1,5 @@ """Config flow for System Bridge integration.""" + from __future__ import annotations import asyncio @@ -12,28 +13,28 @@ from systembridgeconnector.exceptions import ( ConnectionErrorException, ) from systembridgeconnector.websocket_client import WebSocketClient -from systembridgemodels.get_data import GetData -from systembridgemodels.system import System +from systembridgemodels.modules import GetData import voluptuous as vol -from homeassistant import config_entries, exceptions from homeassistant.components import zeroconf -from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult +from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TOKEN from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResult +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN +from .data import SystemBridgeData _LOGGER = logging.getLogger(__name__) -STEP_AUTHENTICATE_DATA_SCHEMA = vol.Schema({vol.Required(CONF_API_KEY): cv.string}) +STEP_AUTHENTICATE_DATA_SCHEMA = vol.Schema({vol.Required(CONF_TOKEN): cv.string}) STEP_USER_DATA_SCHEMA = vol.Schema( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_PORT, default=9170): cv.string, - vol.Required(CONF_API_KEY): cv.string, + vol.Required(CONF_TOKEN): cv.string, } ) @@ -46,25 +47,41 @@ async def _validate_input( Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user. """ - host = data[CONF_HOST] + + system_bridge_data = SystemBridgeData() + + async def _async_handle_module( + module_name: str, + module: Any, + ) -> None: + """Handle data from the WebSocket client.""" + _LOGGER.debug("Set new data for: %s", module_name) + setattr(system_bridge_data, module_name, module) websocket_client = WebSocketClient( - host, + data[CONF_HOST], data[CONF_PORT], - data[CONF_API_KEY], + data[CONF_TOKEN], + session=async_get_clientsession(hass), ) + try: async with asyncio.timeout(15): - await websocket_client.connect(session=async_get_clientsession(hass)) - hass.async_create_task(websocket_client.listen()) + await websocket_client.connect() + hass.async_create_task( + websocket_client.listen(callback=_async_handle_module) + ) response = await websocket_client.get_data(GetData(modules=["system"])) - _LOGGER.debug("Got response: %s", response.json()) - if response.data is None or not isinstance(response.data, System): + _LOGGER.debug("Got response: %s", response) + if response is None: raise CannotConnect("No data received") - system: System = response.data + while system_bridge_data.system is None: + await asyncio.sleep(0.2) except AuthenticationException as exception: _LOGGER.warning( - "Authentication error when connecting to %s: %s", data[CONF_HOST], exception + "Authentication error when connecting to %s: %s", + data[CONF_HOST], + exception, ) raise InvalidAuth from exception except ( @@ -81,9 +98,9 @@ async def _validate_input( except ValueError as exception: raise CannotConnect from exception - _LOGGER.debug("Got System data: %s", system.json()) + _LOGGER.debug("Got System data: %s", system_bridge_data.system) - return {"hostname": host, "uuid": system.uuid} + return {"hostname": data[CONF_HOST], "uuid": system_bridge_data.system.uuid} async def _async_get_info( @@ -107,13 +124,14 @@ async def _async_get_info( return errors, None -class ConfigFlow( - config_entries.ConfigFlow, +class SystemBridgeConfigFlow( + ConfigFlow, domain=DOMAIN, ): """Handle a config flow for System Bridge.""" VERSION = 1 + MINOR_VERSION = 2 def __init__(self) -> None: """Initialize flow.""" @@ -123,7 +141,7 @@ class ConfigFlow( async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" if user_input is None: return self.async_show_form( @@ -144,7 +162,7 @@ class ConfigFlow( async def async_step_authenticate( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle getting the api-key for authentication.""" errors: dict[str, str] = {} @@ -177,7 +195,7 @@ class ConfigFlow( async def async_step_zeroconf( self, discovery_info: zeroconf.ZeroconfServiceInfo - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle zeroconf discovery.""" properties = discovery_info.properties host = properties.get("ip") @@ -198,7 +216,9 @@ class ConfigFlow( return await self.async_step_authenticate() - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Perform reauth upon an API authentication error.""" self._name = entry_data[CONF_HOST] self._input = { @@ -209,9 +229,9 @@ class ConfigFlow( return await self.async_step_authenticate() -class CannotConnect(exceptions.HomeAssistantError): +class CannotConnect(HomeAssistantError): """Error to indicate we cannot connect.""" -class InvalidAuth(exceptions.HomeAssistantError): +class InvalidAuth(HomeAssistantError): """Error to indicate there is invalid auth.""" diff --git a/homeassistant/components/system_bridge/const.py b/homeassistant/components/system_bridge/const.py index fc87b609b78..e58cdf5f72d 100644 --- a/homeassistant/components/system_bridge/const.py +++ b/homeassistant/components/system_bridge/const.py @@ -5,9 +5,9 @@ DOMAIN = "system_bridge" MODULES = [ "battery", "cpu", - "disk", - "display", - "gpu", + "disks", + "displays", + "gpus", "media", "memory", "processes", diff --git a/homeassistant/components/system_bridge/coordinator.py b/homeassistant/components/system_bridge/coordinator.py index 532092ab133..f810c69a873 100644 --- a/homeassistant/components/system_bridge/coordinator.py +++ b/homeassistant/components/system_bridge/coordinator.py @@ -1,4 +1,5 @@ """DataUpdateCoordinator for System Bridge.""" + from __future__ import annotations import asyncio @@ -7,60 +8,36 @@ from datetime import timedelta import logging from typing import Any -from pydantic import BaseModel # pylint: disable=no-name-in-module from systembridgeconnector.exceptions import ( AuthenticationException, ConnectionClosedException, ConnectionErrorException, ) from systembridgeconnector.websocket_client import WebSocketClient -from systembridgemodels.battery import Battery -from systembridgemodels.cpu import Cpu -from systembridgemodels.disk import Disk -from systembridgemodels.display import Display -from systembridgemodels.get_data import GetData -from systembridgemodels.gpu import Gpu -from systembridgemodels.media import Media -from systembridgemodels.media_directories import MediaDirectories -from systembridgemodels.media_files import File as MediaFile, MediaFiles +from systembridgemodels.media_directories import MediaDirectory +from systembridgemodels.media_files import MediaFile, MediaFiles from systembridgemodels.media_get_file import MediaGetFile from systembridgemodels.media_get_files import MediaGetFiles -from systembridgemodels.memory import Memory -from systembridgemodels.processes import Processes -from systembridgemodels.register_data_listener import RegisterDataListener -from systembridgemodels.system import System +from systembridgemodels.modules import GetData, RegisterDataListener +from systembridgemodels.response import Response from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - CONF_API_KEY, CONF_HOST, CONF_PORT, + CONF_TOKEN, EVENT_HOMEASSISTANT_STOP, ) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import DOMAIN, MODULES +from .data import SystemBridgeData -class SystemBridgeCoordinatorData(BaseModel): - """System Bridge Coordianator Data.""" - - battery: Battery = None - cpu: Cpu = None - disk: Disk = None - display: Display = None - gpu: Gpu = None - media: Media = None - memory: Memory = None - processes: Processes = None - system: System = None - - -class SystemBridgeDataUpdateCoordinator( - DataUpdateCoordinator[SystemBridgeCoordinatorData] -): +class SystemBridgeDataUpdateCoordinator(DataUpdateCoordinator[SystemBridgeData]): """Class to manage fetching System Bridge data from single endpoint.""" def __init__( @@ -74,15 +51,19 @@ class SystemBridgeDataUpdateCoordinator( self.title = entry.title self.unsub: Callable | None = None - self.systembridge_data = SystemBridgeCoordinatorData() + self.systembridge_data = SystemBridgeData() self.websocket_client = WebSocketClient( entry.data[CONF_HOST], entry.data[CONF_PORT], - entry.data[CONF_API_KEY], + entry.data[CONF_TOKEN], + session=async_get_clientsession(hass), ) super().__init__( - hass, LOGGER, name=DOMAIN, update_interval=timedelta(seconds=30) + hass, + LOGGER, + name=DOMAIN, + update_interval=timedelta(seconds=30), ) @property @@ -99,16 +80,14 @@ class SystemBridgeDataUpdateCoordinator( async def async_get_data( self, modules: list[str], - ) -> None: + ) -> Response: """Get data from WebSocket.""" if not self.websocket_client.connected: await self._setup_websocket() - self.hass.async_create_task( - self.websocket_client.get_data(GetData(modules=modules)) - ) + return await self.websocket_client.get_data(GetData(modules=modules)) - async def async_get_media_directories(self) -> MediaDirectories: + async def async_get_media_directories(self) -> list[MediaDirectory]: """Get media directories.""" return await self.websocket_client.get_directories() @@ -129,7 +108,7 @@ class SystemBridgeDataUpdateCoordinator( self, base: str, path: str, - ) -> MediaFile: + ) -> MediaFile | None: """Get media file.""" return await self.websocket_client.get_file( MediaGetFile( @@ -154,7 +133,11 @@ class SystemBridgeDataUpdateCoordinator( await self.websocket_client.listen(callback=self.async_handle_module) except AuthenticationException as exception: self.last_update_success = False - self.logger.error("Authentication failed for %s: %s", self.title, exception) + self.logger.error( + "Authentication failed while listening for %s: %s", + self.title, + exception, + ) if self.unsub: self.unsub() self.unsub = None @@ -187,9 +170,7 @@ class SystemBridgeDataUpdateCoordinator( """Use WebSocket for updates.""" try: async with asyncio.timeout(20): - await self.websocket_client.connect( - session=async_get_clientsession(self.hass), - ) + await self.websocket_client.connect() self.hass.async_create_background_task( self._listen_for_data(), @@ -199,14 +180,18 @@ class SystemBridgeDataUpdateCoordinator( await self.websocket_client.register_data_listener( RegisterDataListener(modules=MODULES) ) + self.last_update_success = True + self.async_update_listeners() except AuthenticationException as exception: - self.last_update_success = False - self.logger.error("Authentication failed for %s: %s", self.title, exception) + self.logger.error( + "Authentication failed at setup for %s: %s", self.title, exception + ) if self.unsub: self.unsub() self.unsub = None self.last_update_success = False self.async_update_listeners() + raise ConfigEntryAuthFailed from exception except ConnectionErrorException as exception: self.logger.warning( "Connection error occurred for %s. Will retry: %s", @@ -224,9 +209,6 @@ class SystemBridgeDataUpdateCoordinator( self.last_update_success = False self.async_update_listeners() - self.last_update_success = True - self.async_update_listeners() - async def close_websocket(_) -> None: """Close WebSocket connection.""" await self.websocket_client.close() @@ -236,7 +218,7 @@ class SystemBridgeDataUpdateCoordinator( EVENT_HOMEASSISTANT_STOP, close_websocket ) - async def _async_update_data(self) -> SystemBridgeCoordinatorData: + async def _async_update_data(self) -> SystemBridgeData: """Update System Bridge data from WebSocket.""" self.logger.debug( "_async_update_data - WebSocket Connected: %s", diff --git a/homeassistant/components/system_bridge/data.py b/homeassistant/components/system_bridge/data.py new file mode 100644 index 00000000000..f07e8d75f28 --- /dev/null +++ b/homeassistant/components/system_bridge/data.py @@ -0,0 +1,30 @@ +"""System Bridge integration data.""" + +from dataclasses import dataclass, field + +from systembridgemodels.modules import ( + CPU, + GPU, + Battery, + Disks, + Display, + Media, + Memory, + Process, + System, +) + + +@dataclass +class SystemBridgeData: + """System Bridge Data.""" + + battery: Battery = field(default_factory=Battery) + cpu: CPU = field(default_factory=CPU) + disks: Disks = None + displays: list[Display] = field(default_factory=list[Display]) + gpus: list[GPU] = field(default_factory=list[GPU]) + media: Media = field(default_factory=Media) + memory: Memory = None + processes: list[Process] = field(default_factory=list[Process]) + system: System = None diff --git a/homeassistant/components/system_bridge/entity.py b/homeassistant/components/system_bridge/entity.py index 72a6fc93977..b37e55cf406 100644 --- a/homeassistant/components/system_bridge/entity.py +++ b/homeassistant/components/system_bridge/entity.py @@ -1,4 +1,5 @@ """Base entity for the system bridge integration.""" + from homeassistant.helpers import device_registry as dr from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/system_bridge/icons.json b/homeassistant/components/system_bridge/icons.json new file mode 100644 index 00000000000..cc648889f0b --- /dev/null +++ b/homeassistant/components/system_bridge/icons.json @@ -0,0 +1,11 @@ +{ + "services": { + "get_process_by_id": "mdi:console", + "get_processes_by_name": "mdi:console", + "open_path": "mdi:folder-open", + "open_url": "mdi:web", + "send_keypress": "mdi:keyboard", + "send_text": "mdi:keyboard", + "power_command": "mdi:power" + } +} diff --git a/homeassistant/components/system_bridge/manifest.json b/homeassistant/components/system_bridge/manifest.json index 17c43fa4d24..b4365fda778 100644 --- a/homeassistant/components/system_bridge/manifest.json +++ b/homeassistant/components/system_bridge/manifest.json @@ -10,6 +10,6 @@ "iot_class": "local_push", "loggers": ["systembridgeconnector"], "quality_scale": "silver", - "requirements": ["systembridgeconnector==3.10.0"], + "requirements": ["systembridgeconnector==4.0.3"], "zeroconf": ["_system-bridge._tcp.local."] } diff --git a/homeassistant/components/system_bridge/media_player.py b/homeassistant/components/system_bridge/media_player.py index 02670d36fe3..aeff3b22fb2 100644 --- a/homeassistant/components/system_bridge/media_player.py +++ b/homeassistant/components/system_bridge/media_player.py @@ -1,10 +1,11 @@ """Support for System Bridge media players.""" + from __future__ import annotations import datetime as dt from typing import Final -from systembridgemodels.media_control import Action as MediaAction, MediaControl +from systembridgemodels.media_control import MediaAction, MediaControl from homeassistant.components.media_player import ( MediaPlayerDeviceClass, @@ -20,7 +21,8 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN -from .coordinator import SystemBridgeCoordinatorData, SystemBridgeDataUpdateCoordinator +from .coordinator import SystemBridgeDataUpdateCoordinator +from .data import SystemBridgeData from .entity import SystemBridgeEntity STATUS_CHANGING: Final[str] = "CHANGING" @@ -51,13 +53,13 @@ MEDIA_SET_REPEAT_MAP: Final[dict[RepeatMode, int]] = { RepeatMode.ALL: 2, } -MEDIA_PLAYER_DESCRIPTION: Final[ - MediaPlayerEntityDescription -] = MediaPlayerEntityDescription( - key="media", - translation_key="media", - icon="mdi:volume-high", - device_class=MediaPlayerDeviceClass.RECEIVER, +MEDIA_PLAYER_DESCRIPTION: Final[MediaPlayerEntityDescription] = ( + MediaPlayerEntityDescription( + key="media", + translation_key="media", + icon="mdi:volume-high", + device_class=MediaPlayerDeviceClass.RECEIVER, + ) ) @@ -126,7 +128,7 @@ class SystemBridgeMediaPlayer(SystemBridgeEntity, MediaPlayerEntity): return features @property - def _systembridge_data(self) -> SystemBridgeCoordinatorData: + def _systembridge_data(self) -> SystemBridgeData: """Return data for the entity.""" return self.coordinator.data @@ -202,7 +204,7 @@ class SystemBridgeMediaPlayer(SystemBridgeEntity, MediaPlayerEntity): """Send play command.""" await self.coordinator.websocket_client.media_control( MediaControl( - action=MediaAction.play, + action=MediaAction.PLAY.value, ) ) @@ -210,7 +212,7 @@ class SystemBridgeMediaPlayer(SystemBridgeEntity, MediaPlayerEntity): """Send pause command.""" await self.coordinator.websocket_client.media_control( MediaControl( - action=MediaAction.pause, + action=MediaAction.PAUSE.value, ) ) @@ -218,7 +220,7 @@ class SystemBridgeMediaPlayer(SystemBridgeEntity, MediaPlayerEntity): """Send stop command.""" await self.coordinator.websocket_client.media_control( MediaControl( - action=MediaAction.stop, + action=MediaAction.STOP.value, ) ) @@ -226,7 +228,7 @@ class SystemBridgeMediaPlayer(SystemBridgeEntity, MediaPlayerEntity): """Send previous track command.""" await self.coordinator.websocket_client.media_control( MediaControl( - action=MediaAction.previous, + action=MediaAction.PREVIOUS.value, ) ) @@ -234,7 +236,7 @@ class SystemBridgeMediaPlayer(SystemBridgeEntity, MediaPlayerEntity): """Send next track command.""" await self.coordinator.websocket_client.media_control( MediaControl( - action=MediaAction.next, + action=MediaAction.NEXT.value, ) ) @@ -245,7 +247,7 @@ class SystemBridgeMediaPlayer(SystemBridgeEntity, MediaPlayerEntity): """Enable/disable shuffle mode.""" await self.coordinator.websocket_client.media_control( MediaControl( - action=MediaAction.shuffle, + action=MediaAction.SHUFFLE.value, value=shuffle, ) ) @@ -257,7 +259,7 @@ class SystemBridgeMediaPlayer(SystemBridgeEntity, MediaPlayerEntity): """Set repeat mode.""" await self.coordinator.websocket_client.media_control( MediaControl( - action=MediaAction.repeat, + action=MediaAction.REPEAT.value, value=MEDIA_SET_REPEAT_MAP.get(repeat), ) ) diff --git a/homeassistant/components/system_bridge/media_source.py b/homeassistant/components/system_bridge/media_source.py index 3423946f637..54aa3cffaae 100644 --- a/homeassistant/components/system_bridge/media_source.py +++ b/homeassistant/components/system_bridge/media_source.py @@ -1,8 +1,9 @@ """System Bridge Media Source Implementation.""" + from __future__ import annotations -from systembridgemodels.media_directories import MediaDirectories -from systembridgemodels.media_files import File as MediaFile, MediaFiles +from systembridgemodels.media_directories import MediaDirectory +from systembridgemodels.media_files import MediaFile, MediaFiles from homeassistant.components.media_player import MediaClass from homeassistant.components.media_source import MEDIA_CLASS_MAP, MEDIA_MIME_TYPES @@ -13,7 +14,7 @@ from homeassistant.components.media_source.models import ( PlayMedia, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT +from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TOKEN from homeassistant.core import HomeAssistant from .const import DOMAIN @@ -87,22 +88,21 @@ class SystemBridgeSource(MediaSource): def _build_bridges(self) -> BrowseMediaSource: """Build bridges for System Bridge media.""" - children = [] - for entry in self.hass.config_entries.async_entries(DOMAIN): - if entry.entry_id is not None: - children.append( - BrowseMediaSource( - domain=DOMAIN, - identifier=entry.entry_id, - media_class=MediaClass.DIRECTORY, - media_content_type="", - title=entry.title, - can_play=False, - can_expand=True, - children=[], - children_media_class=MediaClass.DIRECTORY, - ) - ) + children = [ + BrowseMediaSource( + domain=DOMAIN, + identifier=entry.entry_id, + media_class=MediaClass.DIRECTORY, + media_content_type="", + title=entry.title, + can_play=False, + can_expand=True, + children=[], + children_media_class=MediaClass.DIRECTORY, + ) + for entry in self.hass.config_entries.async_entries(DOMAIN) + if entry.entry_id is not None + ] return BrowseMediaSource( domain=DOMAIN, @@ -123,13 +123,13 @@ def _build_base_url( """Build base url for System Bridge media.""" return ( f"http://{entry.data[CONF_HOST]}:{entry.data[CONF_PORT]}" - f"/api/media/file/data?apiKey={entry.data[CONF_API_KEY]}" + f"/api/media/file/data?token={entry.data[CONF_TOKEN]}" ) def _build_root_paths( entry: ConfigEntry, - media_directories: MediaDirectories, + media_directories: list[MediaDirectory], ) -> BrowseMediaSource: """Build base categories for System Bridge media.""" return BrowseMediaSource( @@ -152,7 +152,7 @@ def _build_root_paths( children=[], children_media_class=MediaClass.DIRECTORY, ) - for directory in media_directories.directories + for directory in media_directories ], children_media_class=MediaClass.DIRECTORY, ) diff --git a/homeassistant/components/system_bridge/notify.py b/homeassistant/components/system_bridge/notify.py index f8c00789ae5..0e2f058cc7c 100644 --- a/homeassistant/components/system_bridge/notify.py +++ b/homeassistant/components/system_bridge/notify.py @@ -1,4 +1,5 @@ """Support for System Bridge notification service.""" + from __future__ import annotations import logging @@ -71,6 +72,6 @@ class SystemBridgeNotificationService(BaseNotificationService): title=kwargs.get(ATTR_TITLE, data.get(ATTR_TITLE, ATTR_TITLE_DEFAULT)), ) - _LOGGER.debug("Sending notification: %s", notification.json()) + _LOGGER.debug("Sending notification: %s", notification) await self._coordinator.websocket_client.send_notification(notification) diff --git a/homeassistant/components/system_bridge/sensor.py b/homeassistant/components/system_bridge/sensor.py index 35cc0e00809..94c73a2ac05 100644 --- a/homeassistant/components/system_bridge/sensor.py +++ b/homeassistant/components/system_bridge/sensor.py @@ -1,4 +1,5 @@ """Support for System Bridge sensors.""" + from __future__ import annotations from collections.abc import Callable @@ -6,6 +7,10 @@ from dataclasses import dataclass from datetime import UTC, datetime, timedelta from typing import Final, cast +from systembridgemodels.modules.cpu import PerCPU +from systembridgemodels.modules.displays import Display +from systembridgemodels.modules.gpus import GPU + from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, @@ -26,10 +31,11 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import UNDEFINED, StateType -from homeassistant.util.dt import utcnow +from homeassistant.util import dt as dt_util from .const import DOMAIN -from .coordinator import SystemBridgeCoordinatorData, SystemBridgeDataUpdateCoordinator +from .coordinator import SystemBridgeDataUpdateCoordinator +from .data import SystemBridgeData from .entity import SystemBridgeEntity ATTR_AVAILABLE: Final = "available" @@ -49,90 +55,176 @@ class SystemBridgeSensorEntityDescription(SensorEntityDescription): value: Callable = round -def battery_time_remaining(data: SystemBridgeCoordinatorData) -> datetime | None: +def battery_time_remaining(data: SystemBridgeData) -> datetime | None: """Return the battery time remaining.""" - if (value := getattr(data.battery, "sensors_secsleft", None)) is not None: - return utcnow() + timedelta(seconds=value) + if (battery_time := data.battery.time_remaining) is not None: + return dt_util.utcnow() + timedelta(seconds=battery_time) return None -def cpu_power_package(data: SystemBridgeCoordinatorData) -> float | None: - """Return the CPU package power.""" - if data.cpu.power_package is not None: - return data.cpu.power_package - return None - - -def cpu_power_per_cpu( - data: SystemBridgeCoordinatorData, - cpu: int, -) -> float | None: - """Return CPU power per CPU.""" - if (value := getattr(data.cpu, f"power_per_cpu_{cpu}", None)) is not None: - return value - return None - - -def cpu_speed(data: SystemBridgeCoordinatorData) -> float | None: +def cpu_speed(data: SystemBridgeData) -> float | None: """Return the CPU speed.""" - if data.cpu.frequency_current is not None: - return round(data.cpu.frequency_current / 1000, 2) + if (cpu_frequency := data.cpu.frequency) is not None and ( + cpu_frequency.current + ) is not None: + return round(cpu_frequency.current / 1000, 2) return None -def gpu_core_clock_speed(data: SystemBridgeCoordinatorData, key: str) -> float | None: +def with_per_cpu(func) -> Callable: + """Wrap a function to ensure per CPU data is available.""" + + def wrapper(data: SystemBridgeData, index: int) -> float | None: + """Wrap a function to ensure per CPU data is available.""" + if data.cpu.per_cpu is not None and index < len(data.cpu.per_cpu): + return func(data.cpu.per_cpu[index]) + return None + + return wrapper + + +@with_per_cpu +def cpu_power_per_cpu(per_cpu: PerCPU) -> float | None: + """Return CPU power per CPU.""" + return per_cpu.power + + +@with_per_cpu +def cpu_usage_per_cpu(per_cpu: PerCPU) -> float | None: + """Return CPU usage per CPU.""" + return per_cpu.usage + + +def with_display(func) -> Callable: + """Wrap a function to ensure a Display is available.""" + + def wrapper(data: SystemBridgeData, index: int) -> Display | None: + """Wrap a function to ensure a Display is available.""" + if index < len(data.displays): + return func(data.displays[index]) + return None + + return wrapper + + +@with_display +def display_resolution_horizontal(display: Display) -> int | None: + """Return the Display resolution horizontal.""" + return display.resolution_horizontal + + +@with_display +def display_resolution_vertical(display: Display) -> int | None: + """Return the Display resolution vertical.""" + return display.resolution_vertical + + +@with_display +def display_refresh_rate(display: Display) -> float | None: + """Return the Display refresh rate.""" + return display.refresh_rate + + +def with_gpu(func) -> Callable: + """Wrap a function to ensure a GPU is available.""" + + def wrapper(data: SystemBridgeData, index: int) -> GPU | None: + """Wrap a function to ensure a GPU is available.""" + if index < len(data.gpus): + return func(data.gpus[index]) + return None + + return wrapper + + +@with_gpu +def gpu_core_clock_speed(gpu: GPU) -> float | None: """Return the GPU core clock speed.""" - if (value := getattr(data.gpu, f"{key}_core_clock", None)) is not None: - return round(value) - return None + return gpu.core_clock -def gpu_memory_clock_speed(data: SystemBridgeCoordinatorData, key: str) -> float | None: +@with_gpu +def gpu_fan_speed(gpu: GPU) -> float | None: + """Return the GPU fan speed.""" + return gpu.fan_speed + + +@with_gpu +def gpu_memory_clock_speed(gpu: GPU) -> float | None: """Return the GPU memory clock speed.""" - if (value := getattr(data.gpu, f"{key}_memory_clock", None)) is not None: - return round(value) - return None + return gpu.memory_clock -def gpu_memory_free(data: SystemBridgeCoordinatorData, key: str) -> float | None: +@with_gpu +def gpu_memory_free(gpu: GPU) -> float | None: """Return the free GPU memory.""" - if (value := getattr(data.gpu, f"{key}_memory_free", None)) is not None: - return round(value) - return None + return gpu.memory_free -def gpu_memory_used(data: SystemBridgeCoordinatorData, key: str) -> float | None: +@with_gpu +def gpu_memory_used(gpu: GPU) -> float | None: """Return the used GPU memory.""" - if (value := getattr(data.gpu, f"{key}_memory_used", None)) is not None: - return round(value) - return None + return gpu.memory_used -def gpu_memory_used_percentage( - data: SystemBridgeCoordinatorData, key: str -) -> float | None: +@with_gpu +def gpu_memory_used_percentage(gpu: GPU) -> float | None: """Return the used GPU memory percentage.""" - if ((used := getattr(data.gpu, f"{key}_memory_used", None)) is not None) and ( - (total := getattr(data.gpu, f"{key}_memory_total", None)) is not None - ): - return round( - used / total * 100, - 2, - ) + if (gpu.memory_used) is not None and (gpu.memory_total) is not None: + return round(gpu.memory_used / gpu.memory_total * 100, 2) return None -def memory_free(data: SystemBridgeCoordinatorData) -> float | None: +@with_gpu +def gpu_power_usage(gpu: GPU) -> float | None: + """Return the GPU power usage.""" + return gpu.power_usage + + +@with_gpu +def gpu_temperature(gpu: GPU) -> float | None: + """Return the GPU temperature.""" + return gpu.temperature + + +@with_gpu +def gpu_usage_percentage(gpu: GPU) -> float | None: + """Return the GPU usage percentage.""" + return gpu.core_load + + +def memory_free(data: SystemBridgeData) -> float | None: """Return the free memory.""" - if data.memory.virtual_free is not None: - return round(data.memory.virtual_free / 1000**3, 2) + if (virtual := data.memory.virtual) is not None and ( + free := virtual.free + ) is not None: + return round(free / 1000**3, 2) return None -def memory_used(data: SystemBridgeCoordinatorData) -> float | None: +def memory_used(data: SystemBridgeData) -> float | None: """Return the used memory.""" - if data.memory.virtual_used is not None: - return round(data.memory.virtual_used / 1000**3, 2) + if (virtual := data.memory.virtual) is not None and ( + used := virtual.used + ) is not None: + return round(used / 1000**3, 2) + return None + + +def partition_usage( + data: SystemBridgeData, + device_index: int, + partition_index: int, +) -> float | None: + """Return the used memory.""" + if ( + (devices := data.disks.devices) is not None + and device_index < len(devices) + and (partitions := devices[device_index].partitions) is not None + and partition_index < len(partitions) + and (usage := partitions[partition_index].usage) is not None + ): + return usage.percent return None @@ -151,7 +243,7 @@ BASE_SENSOR_TYPES: tuple[SystemBridgeSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, suggested_display_precision=2, icon="mdi:chip", - value=cpu_power_package, + value=lambda data: data.cpu.power, ), SystemBridgeSensorEntityDescription( key="cpu_speed", @@ -197,15 +289,14 @@ BASE_SENSOR_TYPES: tuple[SystemBridgeSensorEntityDescription, ...] = ( ), SystemBridgeSensorEntityDescription( key="memory_used_percentage", - translation_key="memory_used", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, icon="mdi:memory", - value=lambda data: data.memory.virtual_percent, + value=lambda data: data.memory.virtual.percent, ), SystemBridgeSensorEntityDescription( key="memory_used", - translation_key="amount_memory_used", + translation_key="memory_used", entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfInformation.GIGABYTES, @@ -224,7 +315,7 @@ BASE_SENSOR_TYPES: tuple[SystemBridgeSensorEntityDescription, ...] = ( translation_key="processes", state_class=SensorStateClass.MEASUREMENT, icon="mdi:counter", - value=lambda data: int(data.processes.count), + value=lambda data: len(data.processes), ), SystemBridgeSensorEntityDescription( key="processes_load", @@ -273,28 +364,33 @@ async def async_setup_entry( """Set up System Bridge sensor based on a config entry.""" coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - entities = [] - for description in BASE_SENSOR_TYPES: - entities.append( - SystemBridgeSensor(coordinator, description, entry.data[CONF_PORT]) - ) + entities = [ + SystemBridgeSensor(coordinator, description, entry.data[CONF_PORT]) + for description in BASE_SENSOR_TYPES + ] - for partition in coordinator.data.disk.partitions: - entities.append( + for index_device, device in enumerate(coordinator.data.disks.devices): + if device.partitions is None: + continue + + entities.extend( SystemBridgeSensor( coordinator, SystemBridgeSensorEntityDescription( - key=f"filesystem_{partition.replace(':', '')}", - name=f"{partition} space used", + key=f"filesystem_{partition.mount_point.replace(':', '')}", + name=f"{partition.mount_point} space used", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, icon="mdi:harddisk", - value=lambda data, p=partition: getattr( - data.disk, f"usage_{p}_percent", None + value=( + lambda data, + dk=index_device, + pk=index_partition: partition_usage(data, dk, pk) ), ), entry.data[CONF_PORT], ) + for index_partition, partition in enumerate(device.partitions) ) if ( @@ -302,24 +398,10 @@ async def async_setup_entry( and coordinator.data.battery.percentage and coordinator.data.battery.percentage > -1 ): - for description in BATTERY_SENSOR_TYPES: - entities.append( - SystemBridgeSensor(coordinator, description, entry.data[CONF_PORT]) - ) - - displays: list[dict[str, str]] = [] - if coordinator.data.display.displays is not None: - displays.extend( - { - "key": display, - "name": getattr(coordinator.data.display, f"{display}_name").replace( - "Display ", "" - ), - } - for display in coordinator.data.display.displays - if hasattr(coordinator.data.display, f"{display}_name") + entities.extend( + SystemBridgeSensor(coordinator, description, entry.data[CONF_PORT]) + for description in BATTERY_SENSOR_TYPES ) - display_count = len(displays) entities.append( SystemBridgeSensor( @@ -329,236 +411,214 @@ async def async_setup_entry( translation_key="displays_connected", state_class=SensorStateClass.MEASUREMENT, icon="mdi:monitor", - value=lambda _, count=display_count: count, + value=lambda data: len(data.displays) if data.displays else None, ), entry.data[CONF_PORT], ) ) - for _, display in enumerate(displays): - entities = [ - *entities, - SystemBridgeSensor( - coordinator, - SystemBridgeSensorEntityDescription( - key=f"display_{display['name']}_resolution_x", - name=f"Display {display['name']} resolution x", - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=PIXELS, - icon="mdi:monitor", - value=lambda data, k=display["key"]: getattr( - data.display, f"{k}_resolution_horizontal", None - ), - ), - entry.data[CONF_PORT], - ), - SystemBridgeSensor( - coordinator, - SystemBridgeSensorEntityDescription( - key=f"display_{display['name']}_resolution_y", - name=f"Display {display['name']} resolution y", - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=PIXELS, - icon="mdi:monitor", - value=lambda data, k=display["key"]: getattr( - data.display, f"{k}_resolution_vertical", None - ), - ), - entry.data[CONF_PORT], - ), - SystemBridgeSensor( - coordinator, - SystemBridgeSensorEntityDescription( - key=f"display_{display['name']}_refresh_rate", - name=f"Display {display['name']} refresh rate", - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=UnitOfFrequency.HERTZ, - device_class=SensorDeviceClass.FREQUENCY, - icon="mdi:monitor", - value=lambda data, k=display["key"]: getattr( - data.display, f"{k}_refresh_rate", None - ), - ), - entry.data[CONF_PORT], - ), - ] - - gpus: list[dict[str, str]] = [] - if coordinator.data.gpu.gpus is not None: - gpus.extend( - { - "key": gpu, - "name": getattr(coordinator.data.gpu, f"{gpu}_name"), - } - for gpu in coordinator.data.gpu.gpus - if hasattr(coordinator.data.gpu, f"{gpu}_name") - ) - - for index, gpu in enumerate(gpus): - entities = [ - *entities, - SystemBridgeSensor( - coordinator, - SystemBridgeSensorEntityDescription( - key=f"gpu_{index}_core_clock_speed", - name=f"{gpu['name']} clock speed", - entity_registry_enabled_default=False, - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=UnitOfFrequency.MEGAHERTZ, - device_class=SensorDeviceClass.FREQUENCY, - icon="mdi:speedometer", - value=lambda data, k=gpu["key"]: gpu_core_clock_speed(data, k), - ), - entry.data[CONF_PORT], - ), - SystemBridgeSensor( - coordinator, - SystemBridgeSensorEntityDescription( - key=f"gpu_{index}_memory_clock_speed", - name=f"{gpu['name']} memory clock speed", - entity_registry_enabled_default=False, - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=UnitOfFrequency.MEGAHERTZ, - device_class=SensorDeviceClass.FREQUENCY, - icon="mdi:speedometer", - value=lambda data, k=gpu["key"]: gpu_memory_clock_speed(data, k), - ), - entry.data[CONF_PORT], - ), - SystemBridgeSensor( - coordinator, - SystemBridgeSensorEntityDescription( - key=f"gpu_{index}_memory_free", - name=f"{gpu['name']} memory free", - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=UnitOfInformation.GIGABYTES, - device_class=SensorDeviceClass.DATA_SIZE, - icon="mdi:memory", - value=lambda data, k=gpu["key"]: gpu_memory_free(data, k), - ), - entry.data[CONF_PORT], - ), - SystemBridgeSensor( - coordinator, - SystemBridgeSensorEntityDescription( - key=f"gpu_{index}_memory_used_percentage", - name=f"{gpu['name']} memory used %", - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=PERCENTAGE, - icon="mdi:memory", - value=lambda data, k=gpu["key"]: gpu_memory_used_percentage( - data, k - ), - ), - entry.data[CONF_PORT], - ), - SystemBridgeSensor( - coordinator, - SystemBridgeSensorEntityDescription( - key=f"gpu_{index}_memory_used", - name=f"{gpu['name']} memory used", - entity_registry_enabled_default=False, - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=UnitOfInformation.GIGABYTES, - device_class=SensorDeviceClass.DATA_SIZE, - icon="mdi:memory", - value=lambda data, k=gpu["key"]: gpu_memory_used(data, k), - ), - entry.data[CONF_PORT], - ), - SystemBridgeSensor( - coordinator, - SystemBridgeSensorEntityDescription( - key=f"gpu_{index}_fan_speed", - name=f"{gpu['name']} fan speed", - entity_registry_enabled_default=False, - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=REVOLUTIONS_PER_MINUTE, - icon="mdi:fan", - value=lambda data, k=gpu["key"]: getattr( - data.gpu, f"{k}_fan_speed", None - ), - ), - entry.data[CONF_PORT], - ), - SystemBridgeSensor( - coordinator, - SystemBridgeSensorEntityDescription( - key=f"gpu_{index}_power_usage", - name=f"{gpu['name']} power usage", - entity_registry_enabled_default=False, - device_class=SensorDeviceClass.POWER, - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=UnitOfPower.WATT, - value=lambda data, k=gpu["key"]: getattr( - data.gpu, f"{k}_power", None - ), - ), - entry.data[CONF_PORT], - ), - SystemBridgeSensor( - coordinator, - SystemBridgeSensorEntityDescription( - key=f"gpu_{index}_temperature", - name=f"{gpu['name']} temperature", - entity_registry_enabled_default=False, - device_class=SensorDeviceClass.TEMPERATURE, - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=UnitOfTemperature.CELSIUS, - value=lambda data, k=gpu["key"]: getattr( - data.gpu, f"{k}_temperature", None - ), - ), - entry.data[CONF_PORT], - ), - SystemBridgeSensor( - coordinator, - SystemBridgeSensorEntityDescription( - key=f"gpu_{index}_usage_percentage", - name=f"{gpu['name']} usage %", - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=PERCENTAGE, - icon="mdi:percent", - value=lambda data, k=gpu["key"]: getattr( - data.gpu, f"{k}_core_load", None - ), - ), - entry.data[CONF_PORT], - ), - ] - - for index in range(coordinator.data.cpu.count): - entities.append( - SystemBridgeSensor( - coordinator, - SystemBridgeSensorEntityDescription( - key=f"processes_load_cpu_{index}", - name=f"Load CPU {index}", - entity_registry_enabled_default=False, - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=PERCENTAGE, - icon="mdi:percent", - value=lambda data, k=index: getattr(data.cpu, f"usage_{k}", None), - ), - entry.data[CONF_PORT], - ) - ) - if hasattr(coordinator.data.cpu, f"power_per_cpu_{index}"): - entities.append( + if coordinator.data.displays is not None: + for index, display in enumerate(coordinator.data.displays): + entities = [ + *entities, SystemBridgeSensor( coordinator, SystemBridgeSensorEntityDescription( - key=f"cpu_power_core_{index}", - name=f"CPU Core {index} Power", - entity_registry_enabled_default=False, - native_unit_of_measurement=UnitOfPower.WATT, + key=f"display_{display.id}_resolution_x", + name=f"Display {display.id} resolution x", state_class=SensorStateClass.MEASUREMENT, - suggested_display_precision=2, - icon="mdi:chip", - value=lambda data, k=index: cpu_power_per_cpu(data, k), + native_unit_of_measurement=PIXELS, + icon="mdi:monitor", + value=lambda data, k=index: display_resolution_horizontal( + data, k + ), ), entry.data[CONF_PORT], - ) + ), + SystemBridgeSensor( + coordinator, + SystemBridgeSensorEntityDescription( + key=f"display_{display.id}_resolution_y", + name=f"Display {display.id} resolution y", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=PIXELS, + icon="mdi:monitor", + value=lambda data, k=index: display_resolution_vertical( + data, k + ), + ), + entry.data[CONF_PORT], + ), + SystemBridgeSensor( + coordinator, + SystemBridgeSensorEntityDescription( + key=f"display_{display.id}_refresh_rate", + name=f"Display {display.id} refresh rate", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfFrequency.HERTZ, + device_class=SensorDeviceClass.FREQUENCY, + icon="mdi:monitor", + value=lambda data, k=index: display_refresh_rate(data, k), + ), + entry.data[CONF_PORT], + ), + ] + + for index, gpu in enumerate(coordinator.data.gpus): + entities.extend( + [ + SystemBridgeSensor( + coordinator, + SystemBridgeSensorEntityDescription( + key=f"gpu_{gpu.id}_core_clock_speed", + name=f"{gpu.name} clock speed", + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfFrequency.MEGAHERTZ, + device_class=SensorDeviceClass.FREQUENCY, + icon="mdi:speedometer", + value=lambda data, k=index: gpu_core_clock_speed(data, k), + ), + entry.data[CONF_PORT], + ), + SystemBridgeSensor( + coordinator, + SystemBridgeSensorEntityDescription( + key=f"gpu_{gpu.id}_memory_clock_speed", + name=f"{gpu.name} memory clock speed", + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfFrequency.MEGAHERTZ, + device_class=SensorDeviceClass.FREQUENCY, + icon="mdi:speedometer", + value=lambda data, k=index: gpu_memory_clock_speed(data, k), + ), + entry.data[CONF_PORT], + ), + SystemBridgeSensor( + coordinator, + SystemBridgeSensorEntityDescription( + key=f"gpu_{gpu.id}_memory_free", + name=f"{gpu.name} memory free", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfInformation.MEGABYTES, + device_class=SensorDeviceClass.DATA_SIZE, + icon="mdi:memory", + value=lambda data, k=index: gpu_memory_free(data, k), + ), + entry.data[CONF_PORT], + ), + SystemBridgeSensor( + coordinator, + SystemBridgeSensorEntityDescription( + key=f"gpu_{gpu.id}_memory_used_percentage", + name=f"{gpu.name} memory used %", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=PERCENTAGE, + icon="mdi:memory", + value=lambda data, k=index: gpu_memory_used_percentage(data, k), + ), + entry.data[CONF_PORT], + ), + SystemBridgeSensor( + coordinator, + SystemBridgeSensorEntityDescription( + key=f"gpu_{gpu.id}_memory_used", + name=f"{gpu.name} memory used", + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfInformation.MEGABYTES, + device_class=SensorDeviceClass.DATA_SIZE, + icon="mdi:memory", + value=lambda data, k=index: gpu_memory_used(data, k), + ), + entry.data[CONF_PORT], + ), + SystemBridgeSensor( + coordinator, + SystemBridgeSensorEntityDescription( + key=f"gpu_{gpu.id}_fan_speed", + name=f"{gpu.name} fan speed", + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=REVOLUTIONS_PER_MINUTE, + icon="mdi:fan", + value=lambda data, k=index: gpu_fan_speed(data, k), + ), + entry.data[CONF_PORT], + ), + SystemBridgeSensor( + coordinator, + SystemBridgeSensorEntityDescription( + key=f"gpu_{gpu.id}_power_usage", + name=f"{gpu.name} power usage", + entity_registry_enabled_default=False, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfPower.WATT, + value=lambda data, k=index: gpu_power_usage(data, k), + ), + entry.data[CONF_PORT], + ), + SystemBridgeSensor( + coordinator, + SystemBridgeSensorEntityDescription( + key=f"gpu_{gpu.id}_temperature", + name=f"{gpu.name} temperature", + entity_registry_enabled_default=False, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + value=lambda data, k=index: gpu_temperature(data, k), + ), + entry.data[CONF_PORT], + ), + SystemBridgeSensor( + coordinator, + SystemBridgeSensorEntityDescription( + key=f"gpu_{gpu.id}_usage_percentage", + name=f"{gpu.name} usage %", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=PERCENTAGE, + icon="mdi:percent", + value=lambda data, k=index: gpu_usage_percentage(data, k), + ), + entry.data[CONF_PORT], + ), + ] + ) + + if coordinator.data.cpu.per_cpu is not None: + for cpu in coordinator.data.cpu.per_cpu: + entities.extend( + [ + SystemBridgeSensor( + coordinator, + SystemBridgeSensorEntityDescription( + key=f"processes_load_cpu_{cpu.id}", + name=f"Load CPU {cpu.id}", + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=PERCENTAGE, + icon="mdi:percent", + value=lambda data, k=cpu.id: cpu_usage_per_cpu(data, k), + ), + entry.data[CONF_PORT], + ), + SystemBridgeSensor( + coordinator, + SystemBridgeSensorEntityDescription( + key=f"cpu_power_core_{cpu.id}", + name=f"CPU Core {cpu.id} Power", + entity_registry_enabled_default=False, + native_unit_of_measurement=UnitOfPower.WATT, + state_class=SensorStateClass.MEASUREMENT, + icon="mdi:chip", + value=lambda data, k=cpu.id: cpu_power_per_cpu(data, k), + ), + entry.data[CONF_PORT], + ), + ] ) async_add_entities(entities) diff --git a/homeassistant/components/system_bridge/services.yaml b/homeassistant/components/system_bridge/services.yaml index 49a7931789e..14f621d99fc 100644 --- a/homeassistant/components/system_bridge/services.yaml +++ b/homeassistant/components/system_bridge/services.yaml @@ -1,3 +1,27 @@ +get_process_by_id: + fields: + bridge: + required: true + selector: + device: + integration: system_bridge + id: + required: true + example: 1234 + selector: + number: +get_processes_by_name: + fields: + bridge: + required: true + selector: + device: + integration: system_bridge + name: + required: true + example: "chrome.exe" + selector: + text: open_path: fields: bridge: diff --git a/homeassistant/components/system_bridge/strings.json b/homeassistant/components/system_bridge/strings.json index d99a2cf4588..98a1fe4c08d 100644 --- a/homeassistant/components/system_bridge/strings.json +++ b/homeassistant/components/system_bridge/strings.json @@ -3,21 +3,22 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", + "unsupported_version": "Your version of System Bridge is not supported. Please upgrade to the latest version.", "unknown": "[%key:common::config_flow::error::unknown%]" }, "flow_title": "{name}", "step": { "authenticate": { "data": { - "api_key": "[%key:common::config_flow::data::api_key%]" + "token": "[%key:common::config_flow::data::api_token%]" }, - "description": "Please enter the API Key you set in your configuration for {name}." + "description": "Please enter the token set in your configuration for {name}." }, "user": { "data": { - "api_key": "[%key:common::config_flow::data::api_key%]", "host": "[%key:common::config_flow::data::host%]", - "port": "[%key:common::config_flow::data::port%]" + "port": "[%key:common::config_flow::data::port%]", + "token": "Token" }, "description": "Please enter your connection details." } @@ -29,6 +30,14 @@ } }, "entity": { + "binary_sensor": { + "camera_in_use": { + "name": "Camera in use" + }, + "pending_reboot": { + "name": "Pending reboot" + } + }, "media_player": { "media": { "name": "Media" @@ -85,6 +94,17 @@ } } }, + "exceptions": { + "process_not_found": { + "message": "Could not find process with id {id}." + } + }, + "issues": { + "unsupported_version": { + "title": "System Bridge Upgrade Required", + "description": "Your version of System Bridge for host {host} is not supported.\n\nPlease upgrade to the latest version." + } + }, "services": { "open_path": { "name": "Open path", @@ -100,6 +120,34 @@ } } }, + "get_process_by_id": { + "name": "Get process by ID", + "description": "Gets a process by the ID.", + "fields": { + "bridge": { + "name": "[%key:component::system_bridge::services::open_path::fields::bridge::name%]", + "description": "[%key:component::system_bridge::services::open_path::fields::bridge::description%]" + }, + "id": { + "name": "ID", + "description": "ID of the process to get." + } + } + }, + "get_processes_by_name": { + "name": "Get processes by name", + "description": "Gets a list of processes by the name.", + "fields": { + "bridge": { + "name": "[%key:component::system_bridge::services::open_path::fields::bridge::name%]", + "description": "[%key:component::system_bridge::services::open_path::fields::bridge::description%]" + }, + "name": { + "name": "Name", + "description": "Name of the process to get." + } + } + }, "open_url": { "name": "Open URL", "description": "Opens a URL on the server using the default application.", diff --git a/homeassistant/components/system_bridge/update.py b/homeassistant/components/system_bridge/update.py index 5f667fad30d..b0d341cee3b 100644 --- a/homeassistant/components/system_bridge/update.py +++ b/homeassistant/components/system_bridge/update.py @@ -1,4 +1,5 @@ """Support for System Bridge updates.""" + from __future__ import annotations from homeassistant.components.update import UpdateEntity diff --git a/homeassistant/components/system_health/__init__.py b/homeassistant/components/system_health/__init__.py index 7c4d0f9ac46..6a1e4830443 100644 --- a/homeassistant/components/system_health/__init__.py +++ b/homeassistant/components/system_health/__init__.py @@ -1,4 +1,5 @@ """Support for System health .""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/system_log/__init__.py b/homeassistant/components/system_log/__init__.py index 3ede14a2ad6..423f5c6f5d8 100644 --- a/homeassistant/components/system_log/__init__.py +++ b/homeassistant/components/system_log/__init__.py @@ -1,4 +1,5 @@ """Support for system log.""" + from __future__ import annotations from collections import OrderedDict, deque @@ -62,14 +63,19 @@ SERVICE_WRITE_SCHEMA = vol.Schema( def _figure_out_source( - record: logging.LogRecord, paths_re: re.Pattern[str] + record: logging.LogRecord, + paths_re: re.Pattern[str], + extracted_tb: traceback.StackSummary | None = None, ) -> tuple[str, int]: """Figure out where a log message came from.""" # 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 # exception). In that case, just use the file where the log was made from. if record.exc_info: - stack = [(x[0], x[1]) for x in traceback.extract_tb(record.exc_info[2])] + stack = [ + (x[0], x[1]) + for x in (extracted_tb or traceback.extract_tb(record.exc_info[2])) + ] for i, (filename, _) in enumerate(stack): # Slice the stack to the first frame that matches # the record pathname. @@ -166,7 +172,12 @@ class LogEntry: "key", ) - def __init__(self, record: logging.LogRecord, source: tuple[str, int]) -> None: + def __init__( + self, + record: logging.LogRecord, + paths_re: re.Pattern, + figure_out_source: bool = False, + ) -> None: """Initialize a log entry.""" self.first_occurred = self.timestamp = record.created self.name = record.name @@ -175,16 +186,20 @@ class LogEntry: # This must be manually tested when changing the code. self.message = deque([_safe_get_message(record)], maxlen=5) self.exception = "" - self.root_cause = None + self.root_cause: str | None = None + extracted_tb: traceback.StackSummary | None = None if record.exc_info: self.exception = "".join(traceback.format_exception(*record.exc_info)) - _, _, tb = record.exc_info - # Last line of traceback contains the root cause of the exception - if traceback.extract_tb(tb): - self.root_cause = str(traceback.extract_tb(tb)[-1]) - self.source = source + if extracted := traceback.extract_tb(record.exc_info[2]): + # Last line of traceback contains the root cause of the exception + extracted_tb = extracted + self.root_cause = str(extracted[-1]) + if figure_out_source: + self.source = _figure_out_source(record, paths_re, extracted_tb) + else: + self.source = (record.pathname, record.lineno) self.count = 1 - self.key = (self.name, source, self.root_cause) + self.key = (self.name, self.source, self.root_cause) def to_dict(self) -> dict[str, Any]: """Convert object into dict to maintain backward compatibility.""" @@ -258,7 +273,7 @@ class LogErrorHandler(logging.Handler): default upper limit is set to 50 (older entries are discarded) but can be changed if needed. """ - entry = LogEntry(record, _figure_out_source(record, self.paths_re)) + entry = LogEntry(record, self.paths_re, figure_out_source=True) self.records.add_entry(entry) if self.fire_event: self.hass.bus.fire(EVENT_SYSTEM_LOG, entry.to_dict()) @@ -277,7 +292,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: handler = LogErrorHandler( hass, conf[CONF_MAX_ENTRIES], conf[CONF_FIRE_EVENT], paths_re ) - handler.setLevel(logging.WARN) + handler.setLevel(logging.WARNING) hass.data[DOMAIN] = handler @@ -293,23 +308,22 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: websocket_api.async_register_command(hass, list_errors) - async def async_service_handler(service: ServiceCall) -> None: - """Handle logger services.""" - if service.service == "clear": - handler.records.clear() - return - if service.service == "write": - logger = logging.getLogger( - service.data.get(CONF_LOGGER, f"{__name__}.external") - ) - level = service.data[CONF_LEVEL] - getattr(logger, level)(service.data[CONF_MESSAGE]) + @callback + def _async_clear_service_handler(service: ServiceCall) -> None: + handler.records.clear() + + @callback + def _async_write_service_handler(service: ServiceCall) -> None: + name = service.data.get(CONF_LOGGER, f"{__name__}.external") + logger = logging.getLogger(name) + level = service.data[CONF_LEVEL] + getattr(logger, level)(service.data[CONF_MESSAGE]) hass.services.async_register( - DOMAIN, SERVICE_CLEAR, async_service_handler, schema=SERVICE_CLEAR_SCHEMA + DOMAIN, SERVICE_CLEAR, _async_clear_service_handler, schema=SERVICE_CLEAR_SCHEMA ) hass.services.async_register( - DOMAIN, SERVICE_WRITE, async_service_handler, schema=SERVICE_WRITE_SCHEMA + DOMAIN, SERVICE_WRITE, _async_write_service_handler, schema=SERVICE_WRITE_SCHEMA ) return True diff --git a/homeassistant/components/system_log/icons.json b/homeassistant/components/system_log/icons.json new file mode 100644 index 00000000000..436a6c34808 --- /dev/null +++ b/homeassistant/components/system_log/icons.json @@ -0,0 +1,6 @@ +{ + "services": { + "clear": "mdi:delete", + "write": "mdi:pencil" + } +} diff --git a/homeassistant/components/systemmonitor/__init__.py b/homeassistant/components/systemmonitor/__init__.py index 9fc5c91f085..25c131e547c 100644 --- a/homeassistant/components/systemmonitor/__init__.py +++ b/homeassistant/components/systemmonitor/__init__.py @@ -10,7 +10,9 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from .const import DOMAIN +from .const import DOMAIN, DOMAIN_COORDINATOR +from .coordinator import SystemMonitorCoordinator +from .util import get_all_disk_mounts _LOGGER = logging.getLogger(__name__) @@ -21,6 +23,25 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up System Monitor from a config entry.""" psutil_wrapper = await hass.async_add_executor_job(ha_psutil.PsutilWrapper) hass.data[DOMAIN] = psutil_wrapper + + disk_arguments = list(await hass.async_add_executor_job(get_all_disk_mounts, hass)) + legacy_resources: set[str] = set(entry.options.get("resources", [])) + for resource in legacy_resources: + if resource.startswith("disk_"): + split_index = resource.rfind("_") + _type = resource[:split_index] + argument = resource[split_index + 1 :] + _LOGGER.debug("Loading legacy %s with argument %s", _type, argument) + disk_arguments.append(argument) + + _LOGGER.debug("disk arguments to be added: %s", disk_arguments) + + coordinator: SystemMonitorCoordinator = SystemMonitorCoordinator( + hass, psutil_wrapper, disk_arguments + ) + await coordinator.async_config_entry_first_refresh() + hass.data[DOMAIN_COORDINATOR] = coordinator + 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/systemmonitor/binary_sensor.py b/homeassistant/components/systemmonitor/binary_sensor.py index 89c2e9d854e..9efd6f3b4e0 100644 --- a/homeassistant/components/systemmonitor/binary_sensor.py +++ b/homeassistant/components/systemmonitor/binary_sensor.py @@ -7,10 +7,9 @@ from dataclasses import dataclass from functools import lru_cache import logging import sys -from typing import Generic, Literal +from typing import Literal -from psutil import NoSuchProcess, Process -import psutil_home_assistant as ha_psutil +from psutil import NoSuchProcess from homeassistant.components.binary_sensor import ( DOMAIN as BINARY_SENSOR_DOMAIN, @@ -26,8 +25,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util import slugify -from .const import CONF_PROCESS, DOMAIN -from .coordinator import MonitorCoordinator, SystemMonitorProcessCoordinator, dataT +from .const import CONF_PROCESS, DOMAIN, DOMAIN_COORDINATOR +from .coordinator import SystemMonitorCoordinator _LOGGER = logging.getLogger(__name__) @@ -51,10 +50,10 @@ def get_cpu_icon() -> Literal["mdi:cpu-64-bit", "mdi:cpu-32-bit"]: return "mdi:cpu-32-bit" -def get_process(entity: SystemMonitorSensor[list[Process]]) -> bool: +def get_process(entity: SystemMonitorSensor) -> bool: """Return process.""" state = False - for proc in entity.coordinator.data: + for proc in entity.coordinator.data.processes: try: _LOGGER.debug("process %s for argument %s", proc.name(), entity.argument) if entity.argument == proc.name(): @@ -70,21 +69,21 @@ def get_process(entity: SystemMonitorSensor[list[Process]]) -> bool: @dataclass(frozen=True, kw_only=True) -class SysMonitorBinarySensorEntityDescription( - BinarySensorEntityDescription, Generic[dataT] -): +class SysMonitorBinarySensorEntityDescription(BinarySensorEntityDescription): """Describes System Monitor binary sensor entities.""" - value_fn: Callable[[SystemMonitorSensor[dataT]], bool] + value_fn: Callable[[SystemMonitorSensor], bool] + add_to_update: Callable[[SystemMonitorSensor], tuple[str, str]] -SENSOR_TYPES: tuple[SysMonitorBinarySensorEntityDescription[list[Process]], ...] = ( - SysMonitorBinarySensorEntityDescription[list[Process]]( +SENSOR_TYPES: tuple[SysMonitorBinarySensorEntityDescription, ...] = ( + SysMonitorBinarySensorEntityDescription( key="binary_process", translation_key="process", icon=get_cpu_icon(), value_fn=get_process, device_class=BinarySensorDeviceClass.RUNNING, + add_to_update=lambda entity: ("processes", ""), ), ) @@ -93,41 +92,35 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up System Montor binary sensors based on a config entry.""" - psutil_wrapper: ha_psutil.PsutilWrapper = hass.data[DOMAIN] + coordinator: SystemMonitorCoordinator = hass.data[DOMAIN_COORDINATOR] - entities: list[SystemMonitorSensor] = [] - process_coordinator = SystemMonitorProcessCoordinator( - hass, psutil_wrapper, "Process coordinator" + async_add_entities( + SystemMonitorSensor( + coordinator, + sensor_description, + entry.entry_id, + argument, + ) + for sensor_description in SENSOR_TYPES + for argument in entry.options.get(BINARY_SENSOR_DOMAIN, {}).get( + CONF_PROCESS, [] + ) ) - await process_coordinator.async_request_refresh() - - for sensor_description in SENSOR_TYPES: - _entry = entry.options.get(BINARY_SENSOR_DOMAIN, {}) - for argument in _entry.get(CONF_PROCESS, []): - entities.append( - SystemMonitorSensor( - process_coordinator, - sensor_description, - entry.entry_id, - argument, - ) - ) - async_add_entities(entities) class SystemMonitorSensor( - CoordinatorEntity[MonitorCoordinator[dataT]], BinarySensorEntity + CoordinatorEntity[SystemMonitorCoordinator], BinarySensorEntity ): """Implementation of a system monitor binary sensor.""" _attr_has_entity_name = True _attr_entity_category = EntityCategory.DIAGNOSTIC - entity_description: SysMonitorBinarySensorEntityDescription[dataT] + entity_description: SysMonitorBinarySensorEntityDescription def __init__( self, - coordinator: MonitorCoordinator[dataT], - sensor_description: SysMonitorBinarySensorEntityDescription[dataT], + coordinator: SystemMonitorCoordinator, + sensor_description: SysMonitorBinarySensorEntityDescription, entry_id: str, argument: str, ) -> None: @@ -144,6 +137,20 @@ class SystemMonitorSensor( ) self.argument = argument + async def async_added_to_hass(self) -> None: + """When added to hass.""" + self.coordinator.update_subscribers[ + self.entity_description.add_to_update(self) + ].add(self.entity_id) + return await super().async_added_to_hass() + + async def async_will_remove_from_hass(self) -> None: + """When removed from hass.""" + self.coordinator.update_subscribers[ + self.entity_description.add_to_update(self) + ].remove(self.entity_id) + return await super().async_will_remove_from_hass() + @property def is_on(self) -> bool | None: """Return true if the binary sensor is on.""" diff --git a/homeassistant/components/systemmonitor/config_flow.py b/homeassistant/components/systemmonitor/config_flow.py index b9b95a4a094..924f63c8d1c 100644 --- a/homeassistant/components/systemmonitor/config_flow.py +++ b/homeassistant/components/systemmonitor/config_flow.py @@ -1,4 +1,5 @@ """Adds config flow for System Monitor.""" + from __future__ import annotations from collections.abc import Mapping @@ -8,8 +9,8 @@ import voluptuous as vol from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.homeassistant import DOMAIN as HOMEASSISTANT_DOMAIN +from homeassistant.config_entries import ConfigFlowResult from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import entity_registry as er from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.schema_config_entry_flow import ( @@ -138,7 +139,9 @@ class SystemMonitorConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN): return "System Monitor" @callback - def async_create_entry(self, data: Mapping[str, Any], **kwargs: Any) -> FlowResult: + def async_create_entry( + self, data: Mapping[str, Any], **kwargs: Any + ) -> ConfigFlowResult: """Finish config flow and create a config entry.""" if self._async_current_entries(): return self.async_abort(reason="already_configured") diff --git a/homeassistant/components/systemmonitor/const.py b/homeassistant/components/systemmonitor/const.py index 1f254ca92d6..4a6000323d5 100644 --- a/homeassistant/components/systemmonitor/const.py +++ b/homeassistant/components/systemmonitor/const.py @@ -1,7 +1,7 @@ """Constants for System Monitor.""" DOMAIN = "systemmonitor" -DOMAIN_COORDINATORS = "systemmonitor_coordinators" +DOMAIN_COORDINATOR = "systemmonitor_coordinator" CONF_INDEX = "index" CONF_PROCESS = "process" diff --git a/homeassistant/components/systemmonitor/coordinator.py b/homeassistant/components/systemmonitor/coordinator.py index 6f93b9ddce8..d12eddbb14a 100644 --- a/homeassistant/components/systemmonitor/coordinator.py +++ b/homeassistant/components/systemmonitor/coordinator.py @@ -2,11 +2,11 @@ from __future__ import annotations -from abc import abstractmethod +from dataclasses import dataclass from datetime import datetime import logging import os -from typing import NamedTuple, TypeVar +from typing import Any, NamedTuple from psutil import Process from psutil._common import sdiskusage, shwtemp, snetio, snicaddr, sswap @@ -14,15 +14,43 @@ import psutil_home_assistant as ha_psutil from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_component import DEFAULT_SCAN_INTERVAL -from homeassistant.helpers.update_coordinator import ( - TimestampDataUpdateCoordinator, - UpdateFailed, -) +from homeassistant.helpers.update_coordinator import TimestampDataUpdateCoordinator from homeassistant.util import dt as dt_util _LOGGER = logging.getLogger(__name__) +@dataclass(frozen=True, kw_only=True, slots=True) +class SensorData: + """Sensor data.""" + + disk_usage: dict[str, sdiskusage] + swap: sswap + memory: VirtualMemory + io_counters: dict[str, snetio] + addresses: dict[str, list[snicaddr]] + load: tuple[float, float, float] + cpu_percent: float | None + boot_time: datetime + processes: list[Process] + temperatures: dict[str, list[shwtemp]] + + def as_dict(self) -> dict[str, Any]: + """Return as dict.""" + return { + "disk_usage": {k: str(v) for k, v in self.disk_usage.items()}, + "swap": str(self.swap), + "memory": str(self.memory), + "io_counters": {k: str(v) for k, v in self.io_counters.items()}, + "addresses": {k: str(v) for k, v in self.addresses.items()}, + "load": str(self.load), + "cpu_percent": str(self.cpu_percent), + "boot_time": str(self.boot_time), + "processes": str(self.processes), + "temperatures": {k: str(v) for k, v in self.temperatures.items()}, + } + + class VirtualMemory(NamedTuple): """Represents virtual memory. @@ -37,177 +65,148 @@ class VirtualMemory(NamedTuple): free: float -dataT = TypeVar( - "dataT", - bound=datetime - | dict[str, list[shwtemp]] - | dict[str, list[snicaddr]] - | dict[str, snetio] - | float - | list[Process] - | sswap - | VirtualMemory - | tuple[float, float, float] - | sdiskusage - | None, -) - - -class MonitorCoordinator(TimestampDataUpdateCoordinator[dataT]): - """A System monitor Base Data Update Coordinator.""" - - def __init__( - self, hass: HomeAssistant, psutil_wrapper: ha_psutil.PsutilWrapper, name: str - ) -> None: - """Initialize the coordinator.""" - super().__init__( - hass, - _LOGGER, - name=f"System Monitor {name}", - update_interval=DEFAULT_SCAN_INTERVAL, - always_update=False, - ) - self._psutil = psutil_wrapper.psutil - - async def _async_update_data(self) -> dataT: - """Fetch data.""" - return await self.hass.async_add_executor_job(self.update_data) - - @abstractmethod - def update_data(self) -> dataT: - """To be extended by data update coordinators.""" - - -class SystemMonitorDiskCoordinator(MonitorCoordinator[sdiskusage]): - """A System monitor Disk Data Update Coordinator.""" +class SystemMonitorCoordinator(TimestampDataUpdateCoordinator[SensorData]): + """A System monitor Data Update Coordinator.""" def __init__( self, hass: HomeAssistant, psutil_wrapper: ha_psutil.PsutilWrapper, - name: str, - argument: str, + arguments: list[str], ) -> None: - """Initialize the disk coordinator.""" - super().__init__(hass, psutil_wrapper, name) - self._argument = argument + """Initialize the coordinator.""" + super().__init__( + hass, + _LOGGER, + name="System Monitor update coordinator", + update_interval=DEFAULT_SCAN_INTERVAL, + always_update=False, + ) + self._psutil = psutil_wrapper.psutil + self._arguments = arguments + self.boot_time: datetime | None = None - def update_data(self) -> sdiskusage: - """Fetch data.""" - try: - usage: sdiskusage = self._psutil.disk_usage(self._argument) - _LOGGER.debug("sdiskusage: %s", usage) - return usage - except PermissionError as err: - raise UpdateFailed(f"No permission to access {self._argument}") from err - except OSError as err: - raise UpdateFailed(f"OS error for {self._argument}") from err - - -class SystemMonitorSwapCoordinator(MonitorCoordinator[sswap]): - """A System monitor Swap Data Update Coordinator.""" - - def update_data(self) -> sswap: - """Fetch data.""" - swap: sswap = self._psutil.swap_memory() - _LOGGER.debug("sswap: %s", swap) - return swap - - -class SystemMonitorMemoryCoordinator(MonitorCoordinator[VirtualMemory]): - """A System monitor Memory Data Update Coordinator.""" - - def update_data(self) -> VirtualMemory: - """Fetch data.""" - memory = self._psutil.virtual_memory() - _LOGGER.debug("memory: %s", memory) - return VirtualMemory( - memory.total, memory.available, memory.percent, memory.used, memory.free + self._initial_update: bool = True + self.update_subscribers: dict[tuple[str, str], set[str]] = ( + self.set_subscribers_tuples(arguments) ) + def set_subscribers_tuples( + self, arguments: list[str] + ) -> dict[tuple[str, str], set[str]]: + """Set tuples in subscribers dictionary.""" + _disk_defaults: dict[tuple[str, str], set[str]] = {} + for argument in arguments: + _disk_defaults[("disks", argument)] = set() + return { + **_disk_defaults, + ("swap", ""): set(), + ("memory", ""): set(), + ("io_counters", ""): set(), + ("addresses", ""): set(), + ("load", ""): set(), + ("cpu_percent", ""): set(), + ("boot", ""): set(), + ("processes", ""): set(), + ("temperatures", ""): set(), + } -class SystemMonitorNetIOCoordinator(MonitorCoordinator[dict[str, snetio]]): - """A System monitor Network IO Data Update Coordinator.""" - - def update_data(self) -> dict[str, snetio]: + async def _async_update_data(self) -> SensorData: """Fetch data.""" - io_counters: dict[str, snetio] = self._psutil.net_io_counters(pernic=True) - _LOGGER.debug("io_counters: %s", io_counters) - return io_counters + _LOGGER.debug("Update list is: %s", self.update_subscribers) + _data = await self.hass.async_add_executor_job(self.update_data) -class SystemMonitorNetAddrCoordinator(MonitorCoordinator[dict[str, list[snicaddr]]]): - """A System monitor Network Address Data Update Coordinator.""" + load: tuple = (None, None, None) + if self.update_subscribers[("load", "")] or self._initial_update: + load = os.getloadavg() + _LOGGER.debug("Load: %s", load) - def update_data(self) -> dict[str, list[snicaddr]]: - """Fetch data.""" - addresses: dict[str, list[snicaddr]] = self._psutil.net_if_addrs() - _LOGGER.debug("ip_addresses: %s", addresses) - return addresses + cpu_percent: float | None = None + if self.update_subscribers[("cpu_percent", "")] or self._initial_update: + cpu_percent = self._psutil.cpu_percent(interval=None) + _LOGGER.debug("cpu_percent: %s", cpu_percent) + self._initial_update = False + return SensorData( + disk_usage=_data["disks"], + swap=_data["swap"], + memory=_data["memory"], + io_counters=_data["io_counters"], + addresses=_data["addresses"], + load=load, + cpu_percent=cpu_percent, + boot_time=_data["boot_time"], + processes=_data["processes"], + temperatures=_data["temperatures"], + ) -class SystemMonitorLoadCoordinator( - MonitorCoordinator[tuple[float, float, float] | None] -): - """A System monitor Load Data Update Coordinator.""" + def update_data(self) -> dict[str, Any]: + """To be extended by data update coordinators.""" + disks: dict[str, sdiskusage] = {} + for argument in self._arguments: + if self.update_subscribers[("disks", argument)] or self._initial_update: + try: + usage: sdiskusage = self._psutil.disk_usage(argument) + _LOGGER.debug("sdiskusagefor %s: %s", argument, usage) + except PermissionError as err: + _LOGGER.warning( + "No permission to access %s, error %s", argument, err + ) + except OSError as err: + _LOGGER.warning("OS error for %s, error %s", argument, err) + else: + disks[argument] = usage - def update_data(self) -> tuple[float, float, float] | None: - """Coordinator is not async.""" + swap: sswap | None = None + if self.update_subscribers[("swap", "")] or self._initial_update: + swap = self._psutil.swap_memory() + _LOGGER.debug("sswap: %s", swap) - async def _async_update_data(self) -> tuple[float, float, float] | None: - """Fetch data.""" - return os.getloadavg() + memory = None + if self.update_subscribers[("memory", "")] or self._initial_update: + memory = self._psutil.virtual_memory() + _LOGGER.debug("memory: %s", memory) + memory = VirtualMemory( + memory.total, memory.available, memory.percent, memory.used, memory.free + ) + io_counters: dict[str, snetio] | None = None + if self.update_subscribers[("io_counters", "")] or self._initial_update: + io_counters = self._psutil.net_io_counters(pernic=True) + _LOGGER.debug("io_counters: %s", io_counters) -class SystemMonitorProcessorCoordinator(MonitorCoordinator[float | None]): - """A System monitor Processor Data Update Coordinator.""" + addresses: dict[str, list[snicaddr]] | None = None + if self.update_subscribers[("addresses", "")] or self._initial_update: + addresses = self._psutil.net_if_addrs() + _LOGGER.debug("ip_addresses: %s", addresses) - def update_data(self) -> float | None: - """Coordinator is not async.""" + if self._initial_update: + # Boot time only needs to refresh on first pass + self.boot_time = dt_util.utc_from_timestamp(self._psutil.boot_time()) + _LOGGER.debug("boot time: %s", self.boot_time) - async def _async_update_data(self) -> float | None: - """Get cpu usage. + processes = None + if self.update_subscribers[("processes", "")] or self._initial_update: + processes = self._psutil.process_iter() + _LOGGER.debug("processes: %s", processes) + processes = list(processes) - Unlikely the rest of the coordinators, this one is async - since it does not block and we need to make sure it runs - in the same thread every time as psutil checks the thread - tid and compares it against the previous one. - """ - cpu_percent: float = self._psutil.cpu_percent(interval=None) - _LOGGER.debug("cpu_percent: %s", cpu_percent) - if cpu_percent > 0.0: - return cpu_percent - return None + temps: dict[str, list[shwtemp]] = {} + if self.update_subscribers[("temperatures", "")] or self._initial_update: + try: + temps = self._psutil.sensors_temperatures() + _LOGGER.debug("temps: %s", temps) + except AttributeError: + _LOGGER.debug("OS does not provide temperature sensors") - -class SystemMonitorBootTimeCoordinator(MonitorCoordinator[datetime]): - """A System monitor Processor Data Update Coordinator.""" - - def update_data(self) -> datetime: - """Fetch data.""" - boot_time = dt_util.utc_from_timestamp(self._psutil.boot_time()) - _LOGGER.debug("boot time: %s", boot_time) - return boot_time - - -class SystemMonitorProcessCoordinator(MonitorCoordinator[list[Process]]): - """A System monitor Process Data Update Coordinator.""" - - def update_data(self) -> list[Process]: - """Fetch data.""" - processes = self._psutil.process_iter() - _LOGGER.debug("processes: %s", processes) - return list(processes) - - -class SystemMonitorCPUtempCoordinator(MonitorCoordinator[dict[str, list[shwtemp]]]): - """A System monitor CPU Temperature Data Update Coordinator.""" - - def update_data(self) -> dict[str, list[shwtemp]]: - """Fetch data.""" - try: - temps: dict[str, list[shwtemp]] = self._psutil.sensors_temperatures() - _LOGGER.debug("temps: %s", temps) - return temps - except AttributeError as err: - raise UpdateFailed("OS does not provide temperature sensors") from err + return { + "disks": disks, + "swap": swap, + "memory": memory, + "io_counters": io_counters, + "addresses": addresses, + "boot_time": self.boot_time, + "processes": processes, + "temperatures": temps, + } diff --git a/homeassistant/components/systemmonitor/diagnostics.py b/homeassistant/components/systemmonitor/diagnostics.py index d48097e936c..317758651d7 100644 --- a/homeassistant/components/systemmonitor/diagnostics.py +++ b/homeassistant/components/systemmonitor/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for Sensibo.""" + from __future__ import annotations from typing import Any @@ -6,23 +7,21 @@ from typing import Any from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from .const import DOMAIN_COORDINATORS -from .coordinator import MonitorCoordinator +from .const import DOMAIN_COORDINATOR +from .coordinator import SystemMonitorCoordinator async def async_get_config_entry_diagnostics( hass: HomeAssistant, entry: ConfigEntry ) -> dict[str, Any]: """Return diagnostics for Sensibo config entry.""" - coordinators: dict[str, MonitorCoordinator] = hass.data[DOMAIN_COORDINATORS] + coordinator: SystemMonitorCoordinator = hass.data[DOMAIN_COORDINATOR] - diag_data = {} - for _type, coordinator in coordinators.items(): - diag_data[_type] = { - "last_update_success": coordinator.last_update_success, - "last_update": str(coordinator.last_update_success_time), - "data": str(coordinator.data), - } + diag_data = { + "last_update_success": coordinator.last_update_success, + "last_update": str(coordinator.last_update_success_time), + "data": coordinator.data.as_dict(), + } return { "entry": entry.as_dict(), diff --git a/homeassistant/components/systemmonitor/icons.json b/homeassistant/components/systemmonitor/icons.json new file mode 100644 index 00000000000..b0ea54acc98 --- /dev/null +++ b/homeassistant/components/systemmonitor/icons.json @@ -0,0 +1,51 @@ +{ + "entity": { + "sensor": { + "disk_free": { + "default": "mdi:harddisk" + }, + "disk_use": { + "default": "mdi:harddisk" + }, + "disk_use_percent": { + "default": "mdi:harddisk" + }, + "ipv4_address": { + "default": "mdi:ip-network" + }, + "ipv6_address": { + "default": "mdi:ip-network" + }, + "memory_free": { + "default": "mdi:memory" + }, + "memory_use": { + "default": "mdi:memory" + }, + "memory_use_percent": { + "default": "mdi:memory" + }, + "network_in": { + "default": "mdi:server-network" + }, + "network_out": { + "default": "mdi:server-network" + }, + "packets_in": { + "default": "mdi:server-network" + }, + "packets_out": { + "default": "mdi:server-network" + }, + "swap_free": { + "default": "mdi:harddisk" + }, + "swap_use": { + "default": "mdi:harddisk" + }, + "swap_use_percent": { + "default": "mdi:harddisk" + } + } + } +} diff --git a/homeassistant/components/systemmonitor/sensor.py b/homeassistant/components/systemmonitor/sensor.py index 1ebf2ba44e4..e20f4703ab8 100644 --- a/homeassistant/components/systemmonitor/sensor.py +++ b/homeassistant/components/systemmonitor/sensor.py @@ -3,18 +3,18 @@ from __future__ import annotations from collections.abc import Callable +import contextlib from dataclasses import dataclass from datetime import datetime from functools import lru_cache +import ipaddress import logging import socket import sys import time -from typing import Any, Generic, Literal +from typing import Any, Literal -from psutil import NoSuchProcess, Process -from psutil._common import sdiskusage, shwtemp, snetio, snicaddr, sswap -import psutil_home_assistant as ha_psutil +from psutil import NoSuchProcess import voluptuous as vol from homeassistant.components.sensor import ( @@ -47,22 +47,8 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateTyp from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util import slugify -from .const import CONF_PROCESS, DOMAIN, DOMAIN_COORDINATORS, NET_IO_TYPES -from .coordinator import ( - MonitorCoordinator, - SystemMonitorBootTimeCoordinator, - SystemMonitorCPUtempCoordinator, - SystemMonitorDiskCoordinator, - SystemMonitorLoadCoordinator, - SystemMonitorMemoryCoordinator, - SystemMonitorNetAddrCoordinator, - SystemMonitorNetIOCoordinator, - SystemMonitorProcessCoordinator, - SystemMonitorProcessorCoordinator, - SystemMonitorSwapCoordinator, - VirtualMemory, - dataT, -) +from .const import CONF_PROCESS, DOMAIN, DOMAIN_COORDINATOR, NET_IO_TYPES +from .coordinator import SystemMonitorCoordinator from .util import get_all_disk_mounts, get_all_network_interfaces, read_cpu_temperature _LOGGER = logging.getLogger(__name__) @@ -87,17 +73,10 @@ def get_cpu_icon() -> Literal["mdi:cpu-64-bit", "mdi:cpu-32-bit"]: return "mdi:cpu-32-bit" -def get_processor_temperature( - entity: SystemMonitorSensor[dict[str, list[shwtemp]]], -) -> float | None: - """Return processor temperature.""" - return read_cpu_temperature(entity.hass, entity.coordinator.data) - - -def get_process(entity: SystemMonitorSensor[list[Process]]) -> str: +def get_process(entity: SystemMonitorSensor) -> str: """Return process.""" state = STATE_OFF - for proc in entity.coordinator.data: + for proc in entity.coordinator.data.processes: try: _LOGGER.debug("process %s for argument %s", proc.name(), entity.argument) if entity.argument == proc.name(): @@ -112,26 +91,26 @@ def get_process(entity: SystemMonitorSensor[list[Process]]) -> str: return state -def get_network(entity: SystemMonitorSensor[dict[str, snetio]]) -> float | None: +def get_network(entity: SystemMonitorSensor) -> float | None: """Return network in and out.""" - counters = entity.coordinator.data + counters = entity.coordinator.data.io_counters if entity.argument in counters: counter = counters[entity.argument][IO_COUNTER[entity.entity_description.key]] return round(counter / 1024**2, 1) return None -def get_packets(entity: SystemMonitorSensor[dict[str, snetio]]) -> float | None: +def get_packets(entity: SystemMonitorSensor) -> float | None: """Return packets in and out.""" - counters = entity.coordinator.data + counters = entity.coordinator.data.io_counters if entity.argument in counters: return counters[entity.argument][IO_COUNTER[entity.entity_description.key]] return None -def get_throughput(entity: SystemMonitorSensor[dict[str, snetio]]) -> float | None: +def get_throughput(entity: SystemMonitorSensor) -> float | None: """Return network throughput in and out.""" - counters = entity.coordinator.data + counters = entity.coordinator.data.io_counters state = None if entity.argument in counters: counter = counters[entity.argument][IO_COUNTER[entity.entity_description.key]] @@ -151,170 +130,201 @@ def get_throughput(entity: SystemMonitorSensor[dict[str, snetio]]) -> float | No def get_ip_address( - entity: SystemMonitorSensor[dict[str, list[snicaddr]]], + entity: SystemMonitorSensor, ) -> str | None: """Return network ip address.""" - addresses = entity.coordinator.data + addresses = entity.coordinator.data.addresses if entity.argument in addresses: for addr in addresses[entity.argument]: if addr.family == IF_ADDRS_FAMILY[entity.entity_description.key]: + address = ipaddress.ip_address(addr.address) + if address.version == 6 and ( + address.is_link_local or address.is_loopback + ): + continue return addr.address return None @dataclass(frozen=True, kw_only=True) -class SysMonitorSensorEntityDescription(SensorEntityDescription, Generic[dataT]): +class SysMonitorSensorEntityDescription(SensorEntityDescription): """Describes System Monitor sensor entities.""" - value_fn: Callable[[SystemMonitorSensor[dataT]], StateType | datetime] + value_fn: Callable[[SystemMonitorSensor], StateType | datetime] + add_to_update: Callable[[SystemMonitorSensor], tuple[str, str]] + none_is_unavailable: bool = False mandatory_arg: bool = False placeholder: str | None = None -SENSOR_TYPES: dict[str, SysMonitorSensorEntityDescription[Any]] = { - "disk_free": SysMonitorSensorEntityDescription[sdiskusage]( +SENSOR_TYPES: dict[str, SysMonitorSensorEntityDescription] = { + "disk_free": SysMonitorSensorEntityDescription( key="disk_free", translation_key="disk_free", placeholder="mount_point", native_unit_of_measurement=UnitOfInformation.GIBIBYTES, device_class=SensorDeviceClass.DATA_SIZE, - icon="mdi:harddisk", state_class=SensorStateClass.MEASUREMENT, - value_fn=lambda entity: round(entity.coordinator.data.free / 1024**3, 1), + value_fn=lambda entity: round( + entity.coordinator.data.disk_usage[entity.argument].free / 1024**3, 1 + ) + if entity.argument in entity.coordinator.data.disk_usage + else None, + none_is_unavailable=True, + add_to_update=lambda entity: ("disks", entity.argument), ), - "disk_use": SysMonitorSensorEntityDescription[sdiskusage]( + "disk_use": SysMonitorSensorEntityDescription( key="disk_use", translation_key="disk_use", placeholder="mount_point", native_unit_of_measurement=UnitOfInformation.GIBIBYTES, device_class=SensorDeviceClass.DATA_SIZE, - icon="mdi:harddisk", state_class=SensorStateClass.MEASUREMENT, - value_fn=lambda entity: round(entity.coordinator.data.used / 1024**3, 1), + value_fn=lambda entity: round( + entity.coordinator.data.disk_usage[entity.argument].used / 1024**3, 1 + ) + if entity.argument in entity.coordinator.data.disk_usage + else None, + none_is_unavailable=True, + add_to_update=lambda entity: ("disks", entity.argument), ), - "disk_use_percent": SysMonitorSensorEntityDescription[sdiskusage]( + "disk_use_percent": SysMonitorSensorEntityDescription( key="disk_use_percent", translation_key="disk_use_percent", placeholder="mount_point", native_unit_of_measurement=PERCENTAGE, - icon="mdi:harddisk", state_class=SensorStateClass.MEASUREMENT, - value_fn=lambda entity: entity.coordinator.data.percent, + value_fn=lambda entity: entity.coordinator.data.disk_usage[ + entity.argument + ].percent + if entity.argument in entity.coordinator.data.disk_usage + else None, + none_is_unavailable=True, + add_to_update=lambda entity: ("disks", entity.argument), ), - "ipv4_address": SysMonitorSensorEntityDescription[dict[str, list[snicaddr]]]( + "ipv4_address": SysMonitorSensorEntityDescription( key="ipv4_address", translation_key="ipv4_address", placeholder="ip_address", - icon="mdi:ip-network", mandatory_arg=True, value_fn=get_ip_address, + add_to_update=lambda entity: ("addresses", ""), ), - "ipv6_address": SysMonitorSensorEntityDescription[dict[str, list[snicaddr]]]( + "ipv6_address": SysMonitorSensorEntityDescription( key="ipv6_address", translation_key="ipv6_address", placeholder="ip_address", - icon="mdi:ip-network", mandatory_arg=True, value_fn=get_ip_address, + add_to_update=lambda entity: ("addresses", ""), ), - "last_boot": SysMonitorSensorEntityDescription[datetime]( + "last_boot": SysMonitorSensorEntityDescription( key="last_boot", translation_key="last_boot", device_class=SensorDeviceClass.TIMESTAMP, - value_fn=lambda entity: entity.coordinator.data, + value_fn=lambda entity: entity.coordinator.data.boot_time, + add_to_update=lambda entity: ("boot", ""), ), - "load_15m": SysMonitorSensorEntityDescription[tuple[float, float, float]]( + "load_15m": SysMonitorSensorEntityDescription( key="load_15m", translation_key="load_15m", icon=get_cpu_icon(), state_class=SensorStateClass.MEASUREMENT, - value_fn=lambda entity: round(entity.coordinator.data[2], 2), + value_fn=lambda entity: round(entity.coordinator.data.load[2], 2), + add_to_update=lambda entity: ("load", ""), ), - "load_1m": SysMonitorSensorEntityDescription[tuple[float, float, float]]( + "load_1m": SysMonitorSensorEntityDescription( key="load_1m", translation_key="load_1m", icon=get_cpu_icon(), state_class=SensorStateClass.MEASUREMENT, - value_fn=lambda entity: round(entity.coordinator.data[0], 2), + value_fn=lambda entity: round(entity.coordinator.data.load[0], 2), + add_to_update=lambda entity: ("load", ""), ), - "load_5m": SysMonitorSensorEntityDescription[tuple[float, float, float]]( + "load_5m": SysMonitorSensorEntityDescription( key="load_5m", translation_key="load_5m", icon=get_cpu_icon(), state_class=SensorStateClass.MEASUREMENT, - value_fn=lambda entity: round(entity.coordinator.data[1], 2), + value_fn=lambda entity: round(entity.coordinator.data.load[1], 2), + add_to_update=lambda entity: ("load", ""), ), - "memory_free": SysMonitorSensorEntityDescription[VirtualMemory]( + "memory_free": SysMonitorSensorEntityDescription( key="memory_free", translation_key="memory_free", native_unit_of_measurement=UnitOfInformation.MEBIBYTES, device_class=SensorDeviceClass.DATA_SIZE, - icon="mdi:memory", state_class=SensorStateClass.MEASUREMENT, - value_fn=lambda entity: round(entity.coordinator.data.available / 1024**2, 1), + value_fn=lambda entity: round( + entity.coordinator.data.memory.available / 1024**2, 1 + ), + add_to_update=lambda entity: ("memory", ""), ), - "memory_use": SysMonitorSensorEntityDescription[VirtualMemory]( + "memory_use": SysMonitorSensorEntityDescription( key="memory_use", translation_key="memory_use", native_unit_of_measurement=UnitOfInformation.MEBIBYTES, device_class=SensorDeviceClass.DATA_SIZE, - icon="mdi:memory", state_class=SensorStateClass.MEASUREMENT, value_fn=lambda entity: round( - (entity.coordinator.data.total - entity.coordinator.data.available) + ( + entity.coordinator.data.memory.total + - entity.coordinator.data.memory.available + ) / 1024**2, 1, ), + add_to_update=lambda entity: ("memory", ""), ), - "memory_use_percent": SysMonitorSensorEntityDescription[VirtualMemory]( + "memory_use_percent": SysMonitorSensorEntityDescription( key="memory_use_percent", translation_key="memory_use_percent", native_unit_of_measurement=PERCENTAGE, - icon="mdi:memory", state_class=SensorStateClass.MEASUREMENT, - value_fn=lambda entity: entity.coordinator.data.percent, + value_fn=lambda entity: entity.coordinator.data.memory.percent, + add_to_update=lambda entity: ("memory", ""), ), - "network_in": SysMonitorSensorEntityDescription[dict[str, snetio]]( + "network_in": SysMonitorSensorEntityDescription( key="network_in", translation_key="network_in", placeholder="interface", native_unit_of_measurement=UnitOfInformation.MEBIBYTES, device_class=SensorDeviceClass.DATA_SIZE, - icon="mdi:server-network", state_class=SensorStateClass.TOTAL_INCREASING, mandatory_arg=True, value_fn=get_network, + add_to_update=lambda entity: ("io_counters", ""), ), - "network_out": SysMonitorSensorEntityDescription[dict[str, snetio]]( + "network_out": SysMonitorSensorEntityDescription( key="network_out", translation_key="network_out", placeholder="interface", native_unit_of_measurement=UnitOfInformation.MEBIBYTES, device_class=SensorDeviceClass.DATA_SIZE, - icon="mdi:server-network", state_class=SensorStateClass.TOTAL_INCREASING, mandatory_arg=True, value_fn=get_network, + add_to_update=lambda entity: ("io_counters", ""), ), - "packets_in": SysMonitorSensorEntityDescription[dict[str, snetio]]( + "packets_in": SysMonitorSensorEntityDescription( key="packets_in", translation_key="packets_in", placeholder="interface", - icon="mdi:server-network", state_class=SensorStateClass.TOTAL_INCREASING, mandatory_arg=True, value_fn=get_packets, + add_to_update=lambda entity: ("io_counters", ""), ), - "packets_out": SysMonitorSensorEntityDescription[dict[str, snetio]]( + "packets_out": SysMonitorSensorEntityDescription( key="packets_out", translation_key="packets_out", placeholder="interface", - icon="mdi:server-network", state_class=SensorStateClass.TOTAL_INCREASING, mandatory_arg=True, value_fn=get_packets, + add_to_update=lambda entity: ("io_counters", ""), ), - "throughput_network_in": SysMonitorSensorEntityDescription[dict[str, snetio]]( + "throughput_network_in": SysMonitorSensorEntityDescription( key="throughput_network_in", translation_key="throughput_network_in", placeholder="interface", @@ -323,8 +333,9 @@ SENSOR_TYPES: dict[str, SysMonitorSensorEntityDescription[Any]] = { state_class=SensorStateClass.MEASUREMENT, mandatory_arg=True, value_fn=get_throughput, + add_to_update=lambda entity: ("io_counters", ""), ), - "throughput_network_out": SysMonitorSensorEntityDescription[dict[str, snetio]]( + "throughput_network_out": SysMonitorSensorEntityDescription( key="throughput_network_out", translation_key="throughput_network_out", placeholder="interface", @@ -333,60 +344,67 @@ SENSOR_TYPES: dict[str, SysMonitorSensorEntityDescription[Any]] = { state_class=SensorStateClass.MEASUREMENT, mandatory_arg=True, value_fn=get_throughput, + add_to_update=lambda entity: ("io_counters", ""), ), - "process": SysMonitorSensorEntityDescription[list[Process]]( + "process": SysMonitorSensorEntityDescription( key="process", translation_key="process", placeholder="process", icon=get_cpu_icon(), mandatory_arg=True, value_fn=get_process, + add_to_update=lambda entity: ("processes", ""), ), - "processor_use": SysMonitorSensorEntityDescription[float]( + "processor_use": SysMonitorSensorEntityDescription( key="processor_use", translation_key="processor_use", native_unit_of_measurement=PERCENTAGE, icon=get_cpu_icon(), state_class=SensorStateClass.MEASUREMENT, value_fn=lambda entity: ( - round(entity.coordinator.data) if entity.coordinator.data else None + round(entity.coordinator.data.cpu_percent) + if entity.coordinator.data.cpu_percent + else None ), + add_to_update=lambda entity: ("cpu_percent", ""), ), - "processor_temperature": SysMonitorSensorEntityDescription[ - dict[str, list[shwtemp]] - ]( + "processor_temperature": SysMonitorSensorEntityDescription( key="processor_temperature", translation_key="processor_temperature", native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, - value_fn=get_processor_temperature, + value_fn=lambda entity: read_cpu_temperature( + entity.coordinator.data.temperatures + ), + none_is_unavailable=True, + add_to_update=lambda entity: ("temperatures", ""), ), - "swap_free": SysMonitorSensorEntityDescription[sswap]( + "swap_free": SysMonitorSensorEntityDescription( key="swap_free", translation_key="swap_free", native_unit_of_measurement=UnitOfInformation.MEBIBYTES, device_class=SensorDeviceClass.DATA_SIZE, - icon="mdi:harddisk", state_class=SensorStateClass.MEASUREMENT, - value_fn=lambda entity: round(entity.coordinator.data.free / 1024**2, 1), + value_fn=lambda entity: round(entity.coordinator.data.swap.free / 1024**2, 1), + add_to_update=lambda entity: ("swap", ""), ), - "swap_use": SysMonitorSensorEntityDescription[sswap]( + "swap_use": SysMonitorSensorEntityDescription( key="swap_use", translation_key="swap_use", native_unit_of_measurement=UnitOfInformation.MEBIBYTES, device_class=SensorDeviceClass.DATA_SIZE, - icon="mdi:harddisk", state_class=SensorStateClass.MEASUREMENT, - value_fn=lambda entity: round(entity.coordinator.data.used / 1024**2, 1), + value_fn=lambda entity: round(entity.coordinator.data.swap.used / 1024**2, 1), + add_to_update=lambda entity: ("swap", ""), ), - "swap_use_percent": SysMonitorSensorEntityDescription[sswap]( + "swap_use_percent": SysMonitorSensorEntityDescription( key="swap_use_percent", translation_key="swap_use_percent", native_unit_of_measurement=PERCENTAGE, - icon="mdi:harddisk", state_class=SensorStateClass.MEASUREMENT, - value_fn=lambda entity: entity.coordinator.data.percent, + value_fn=lambda entity: entity.coordinator.data.swap.percent, + add_to_update=lambda entity: ("swap", ""), ), } @@ -482,68 +500,29 @@ async def async_setup_platform( ) -async def async_setup_entry( # noqa: C901 +async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up System Montor sensors based on a config entry.""" entities: list[SystemMonitorSensor] = [] legacy_resources: set[str] = set(entry.options.get("resources", [])) loaded_resources: set[str] = set() - psutil_wrapper: ha_psutil.PsutilWrapper = hass.data[DOMAIN] + coordinator: SystemMonitorCoordinator = hass.data[DOMAIN_COORDINATOR] + sensor_data = coordinator.data def get_arguments() -> dict[str, Any]: """Return startup information.""" - disk_arguments = get_all_disk_mounts(hass) - network_arguments = get_all_network_interfaces(hass) - try: - cpu_temperature = read_cpu_temperature(hass) - except AttributeError: - cpu_temperature = 0.0 return { - "disk_arguments": disk_arguments, - "network_arguments": network_arguments, - "cpu_temperature": cpu_temperature, + "disk_arguments": get_all_disk_mounts(hass), + "network_arguments": get_all_network_interfaces(hass), } + cpu_temperature: float | None = None + with contextlib.suppress(AttributeError): + cpu_temperature = read_cpu_temperature(sensor_data.temperatures) + startup_arguments = await hass.async_add_executor_job(get_arguments) - - disk_coordinators: dict[str, SystemMonitorDiskCoordinator] = {} - for argument in startup_arguments["disk_arguments"]: - disk_coordinators[argument] = SystemMonitorDiskCoordinator( - hass, psutil_wrapper, f"Disk {argument} coordinator", argument - ) - swap_coordinator = SystemMonitorSwapCoordinator( - hass, psutil_wrapper, "Swap coordinator" - ) - memory_coordinator = SystemMonitorMemoryCoordinator( - hass, psutil_wrapper, "Memory coordinator" - ) - net_io_coordinator = SystemMonitorNetIOCoordinator( - hass, psutil_wrapper, "Net IO coordnator" - ) - net_addr_coordinator = SystemMonitorNetAddrCoordinator( - hass, psutil_wrapper, "Net address coordinator" - ) - system_load_coordinator = SystemMonitorLoadCoordinator( - hass, psutil_wrapper, "System load coordinator" - ) - processor_coordinator = SystemMonitorProcessorCoordinator( - hass, psutil_wrapper, "Processor coordinator" - ) - boot_time_coordinator = SystemMonitorBootTimeCoordinator( - hass, psutil_wrapper, "Boot time coordinator" - ) - process_coordinator = SystemMonitorProcessCoordinator( - hass, psutil_wrapper, "Process coordinator" - ) - cpu_temp_coordinator = SystemMonitorCPUtempCoordinator( - hass, psutil_wrapper, "CPU temperature coordinator" - ) - - for argument in startup_arguments["disk_arguments"]: - disk_coordinators[argument] = SystemMonitorDiskCoordinator( - hass, psutil_wrapper, f"Disk {argument} coordinator", argument - ) + startup_arguments["cpu_temperature"] = cpu_temperature _LOGGER.debug("Setup from options %s", entry.options) @@ -556,7 +535,7 @@ async def async_setup_entry( # noqa: C901 loaded_resources.add(slugify(f"{_type}_{argument}")) entities.append( SystemMonitorSensor( - disk_coordinators[argument], + coordinator, sensor_description, entry.entry_id, argument, @@ -573,7 +552,7 @@ async def async_setup_entry( # noqa: C901 loaded_resources.add(slugify(f"{_type}_{argument}")) entities.append( SystemMonitorSensor( - net_addr_coordinator, + coordinator, sensor_description, entry.entry_id, argument, @@ -588,7 +567,7 @@ async def async_setup_entry( # noqa: C901 loaded_resources.add(slugify(f"{_type}_{argument}")) entities.append( SystemMonitorSensor( - boot_time_coordinator, + coordinator, sensor_description, entry.entry_id, argument, @@ -603,7 +582,7 @@ async def async_setup_entry( # noqa: C901 loaded_resources.add(slugify(f"{_type}_{argument}")) entities.append( SystemMonitorSensor( - system_load_coordinator, + coordinator, sensor_description, entry.entry_id, argument, @@ -618,7 +597,7 @@ async def async_setup_entry( # noqa: C901 loaded_resources.add(slugify(f"{_type}_{argument}")) entities.append( SystemMonitorSensor( - memory_coordinator, + coordinator, sensor_description, entry.entry_id, argument, @@ -634,7 +613,7 @@ async def async_setup_entry( # noqa: C901 loaded_resources.add(slugify(f"{_type}_{argument}")) entities.append( SystemMonitorSensor( - net_io_coordinator, + coordinator, sensor_description, entry.entry_id, argument, @@ -649,7 +628,7 @@ async def async_setup_entry( # noqa: C901 loaded_resources.add(slugify(f"{_type}_{argument}")) entities.append( SystemMonitorSensor( - process_coordinator, + coordinator, sensor_description, entry.entry_id, argument, @@ -678,7 +657,7 @@ async def async_setup_entry( # noqa: C901 loaded_resources.add(slugify(f"{_type}_{argument}")) entities.append( SystemMonitorSensor( - processor_coordinator, + coordinator, sensor_description, entry.entry_id, argument, @@ -696,7 +675,7 @@ async def async_setup_entry( # noqa: C901 loaded_resources.add(slugify(f"{_type}_{argument}")) entities.append( SystemMonitorSensor( - cpu_temp_coordinator, + coordinator, sensor_description, entry.entry_id, argument, @@ -711,7 +690,7 @@ async def async_setup_entry( # noqa: C901 loaded_resources.add(slugify(f"{_type}_{argument}")) entities.append( SystemMonitorSensor( - swap_coordinator, + coordinator, sensor_description, entry.entry_id, argument, @@ -735,13 +714,9 @@ async def async_setup_entry( # noqa: C901 _type = resource[:split_index] argument = resource[split_index + 1 :] _LOGGER.debug("Loading legacy %s with argument %s", _type, argument) - if not disk_coordinators.get(argument): - disk_coordinators[argument] = SystemMonitorDiskCoordinator( - hass, psutil_wrapper, f"Disk {argument} coordinator", argument - ) entities.append( SystemMonitorSensor( - disk_coordinators[argument], + coordinator, SENSOR_TYPES[_type], entry.entry_id, argument, @@ -749,23 +724,6 @@ async def async_setup_entry( # noqa: C901 ) ) - hass.data[DOMAIN_COORDINATORS] = {} - # No gathering to avoid swamping the executor - for argument, coordinator in disk_coordinators.items(): - hass.data[DOMAIN_COORDINATORS][f"disk_{argument}"] = coordinator - hass.data[DOMAIN_COORDINATORS]["boot_time"] = boot_time_coordinator - hass.data[DOMAIN_COORDINATORS]["cpu_temp"] = cpu_temp_coordinator - hass.data[DOMAIN_COORDINATORS]["memory"] = memory_coordinator - hass.data[DOMAIN_COORDINATORS]["net_addr"] = net_addr_coordinator - hass.data[DOMAIN_COORDINATORS]["net_io"] = net_io_coordinator - hass.data[DOMAIN_COORDINATORS]["process"] = process_coordinator - hass.data[DOMAIN_COORDINATORS]["processor"] = processor_coordinator - hass.data[DOMAIN_COORDINATORS]["swap"] = swap_coordinator - hass.data[DOMAIN_COORDINATORS]["system_load"] = system_load_coordinator - - for coordinator in hass.data[DOMAIN_COORDINATORS].values(): - await coordinator.async_request_refresh() - @callback def clean_obsolete_entities() -> None: """Remove entities which are disabled and not supported from setup.""" @@ -790,17 +748,18 @@ async def async_setup_entry( # noqa: C901 async_add_entities(entities) -class SystemMonitorSensor(CoordinatorEntity[MonitorCoordinator[dataT]], SensorEntity): +class SystemMonitorSensor(CoordinatorEntity[SystemMonitorCoordinator], SensorEntity): """Implementation of a system monitor sensor.""" _attr_has_entity_name = True _attr_entity_category = EntityCategory.DIAGNOSTIC - entity_description: SysMonitorSensorEntityDescription[dataT] + entity_description: SysMonitorSensorEntityDescription + argument: str def __init__( self, - coordinator: MonitorCoordinator[dataT], - sensor_description: SysMonitorSensorEntityDescription[dataT], + coordinator: SystemMonitorCoordinator, + sensor_description: SysMonitorSensorEntityDescription, entry_id: str, argument: str, legacy_enabled: bool = False, @@ -823,8 +782,36 @@ class SystemMonitorSensor(CoordinatorEntity[MonitorCoordinator[dataT]], SensorEn self.argument = argument self.value: int | None = None self.update_time: float | None = None + self._attr_native_value = self.entity_description.value_fn(self) + + async def async_added_to_hass(self) -> None: + """When added to hass.""" + self.coordinator.update_subscribers[ + self.entity_description.add_to_update(self) + ].add(self.entity_id) + return await super().async_added_to_hass() + + async def async_will_remove_from_hass(self) -> None: + """When removed from hass.""" + self.coordinator.update_subscribers[ + self.entity_description.add_to_update(self) + ].remove(self.entity_id) + return await super().async_will_remove_from_hass() + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + # Set the native value here so we can use it in available property + # without having to recalculate it + self._attr_native_value = self.entity_description.value_fn(self) + super()._handle_coordinator_update() @property - def native_value(self) -> StateType | datetime: - """Return the state.""" - return self.entity_description.value_fn(self) + def available(self) -> bool: + """Return if entity is available.""" + if self.entity_description.none_is_unavailable: + return ( + self.coordinator.last_update_success is True + and self.native_value is not None + ) + return super().available diff --git a/homeassistant/components/systemmonitor/util.py b/homeassistant/components/systemmonitor/util.py index c67d4771ff4..1889e443b2d 100644 --- a/homeassistant/components/systemmonitor/util.py +++ b/homeassistant/components/systemmonitor/util.py @@ -57,7 +57,7 @@ def get_all_network_interfaces(hass: HomeAssistant) -> set[str]: """Return all network interfaces on system.""" psutil_wrapper: ha_psutil = hass.data[DOMAIN] interfaces: set[str] = set() - for interface, _ in psutil_wrapper.psutil.net_if_addrs().items(): + for interface in psutil_wrapper.psutil.net_if_addrs(): if interface.startswith("veth"): # Don't load docker virtual network interfaces continue @@ -77,13 +77,8 @@ def get_all_running_processes(hass: HomeAssistant) -> set[str]: return processes -def read_cpu_temperature( - hass: HomeAssistant, temps: dict[str, list[shwtemp]] | None = None -) -> float | None: +def read_cpu_temperature(temps: dict[str, list[shwtemp]]) -> float | None: """Attempt to read CPU / processor temperature.""" - if temps is None: - psutil_wrapper: ha_psutil = hass.data[DOMAIN] - temps = psutil_wrapper.psutil.sensors_temperatures() entry: shwtemp _LOGGER.debug("CPU Temperatures: %s", temps) diff --git a/homeassistant/components/tado/__init__.py b/homeassistant/components/tado/__init__.py index c7225caaff9..5ab7a6f67b8 100644 --- a/homeassistant/components/tado/__init__.py +++ b/homeassistant/components/tado/__init__.py @@ -1,5 +1,6 @@ """Support for the (unofficial) Tado API.""" -from datetime import timedelta + +from datetime import datetime, timedelta import logging from PyTado.interface import Tado @@ -438,7 +439,8 @@ class TadoConnector: def set_meter_reading(self, reading: int) -> dict[str, str]: """Send meter reading to Tado.""" + dt: str = datetime.now().strftime("%Y-%m-%d") try: - return self.tado.set_eiq_meter_readings(reading=reading) + return self.tado.set_eiq_meter_readings(date=dt, reading=reading) except RequestException as exc: raise HomeAssistantError("Could not set meter reading") from exc diff --git a/homeassistant/components/tado/binary_sensor.py b/homeassistant/components/tado/binary_sensor.py index c033ef62e03..0e8cbd1d175 100644 --- a/homeassistant/components/tado/binary_sensor.py +++ b/homeassistant/components/tado/binary_sensor.py @@ -1,4 +1,5 @@ """Support for Tado sensors for each zone.""" + from __future__ import annotations from collections.abc import Callable @@ -33,19 +34,12 @@ from .entity import TadoDeviceEntity, TadoZoneEntity _LOGGER = logging.getLogger(__name__) -@dataclass(frozen=True) -class TadoBinarySensorEntityDescriptionMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class TadoBinarySensorEntityDescription(BinarySensorEntityDescription): + """Describes Tado binary sensor entity.""" state_fn: Callable[[Any], bool] - -@dataclass(frozen=True) -class TadoBinarySensorEntityDescription( - BinarySensorEntityDescription, TadoBinarySensorEntityDescriptionMixin -): - """Describes Tado binary sensor entity.""" - attributes_fn: Callable[[Any], dict[Any, StateType]] | None = None diff --git a/homeassistant/components/tado/climate.py b/homeassistant/components/tado/climate.py index 47bd2bc16f3..621f5a1ad61 100644 --- a/homeassistant/components/tado/climate.py +++ b/homeassistant/components/tado/climate.py @@ -401,7 +401,7 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): def set_timer( self, - temperature: float | None = None, + temperature: float, time_period: int | None = None, requested_overlay: str | None = None, ): @@ -418,7 +418,7 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): """Set offset on the entity.""" _LOGGER.debug( - "Setting temperature offset for device %s setting to (%d)", + "Setting temperature offset for device %s setting to (%.1f)", self._device_id, offset, ) @@ -485,12 +485,12 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): def extra_state_attributes(self) -> Mapping[str, Any] | None: """Return temperature offset.""" state_attr: dict[str, Any] = self._tado_zone_temp_offset - state_attr[ - HA_TERMINATION_TYPE - ] = self._tado_zone_data.default_overlay_termination_type - state_attr[ - HA_TERMINATION_DURATION - ] = self._tado_zone_data.default_overlay_termination_duration + state_attr[HA_TERMINATION_TYPE] = ( + self._tado_zone_data.default_overlay_termination_type + ) + state_attr[HA_TERMINATION_DURATION] = ( + self._tado_zone_data.default_overlay_termination_duration + ) return state_attr def set_swing_mode(self, swing_mode: str) -> None: diff --git a/homeassistant/components/tado/config_flow.py b/homeassistant/components/tado/config_flow.py index f9f4f80bde1..2074b62b8d0 100644 --- a/homeassistant/components/tado/config_flow.py +++ b/homeassistant/components/tado/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Tado integration.""" + from __future__ import annotations import logging @@ -9,11 +10,16 @@ from PyTado.interface import Tado import requests.exceptions import voluptuous as vol -from homeassistant import config_entries, core, exceptions from homeassistant.components import zeroconf +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError from .const import ( CONF_FALLBACK, @@ -34,9 +40,7 @@ DATA_SCHEMA = vol.Schema( ) -async def validate_input( - hass: core.HomeAssistant, data: dict[str, Any] -) -> dict[str, Any]: +async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]: """Validate the user input allows us to connect. Data has the keys from DATA_SCHEMA with values provided by the user. @@ -66,14 +70,14 @@ async def validate_input( return {"title": name, UNIQUE_ID: unique_id} -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class TadoConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Tado.""" VERSION = 1 async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors = {} if user_input is not None: @@ -102,7 +106,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_homekit( self, discovery_info: zeroconf.ZeroconfServiceInfo - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle HomeKit discovery.""" self._async_abort_entries_match() properties = { @@ -112,7 +116,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._abort_if_unique_id_configured() return await self.async_step_user() - async def async_step_import(self, import_config: dict[str, Any]) -> FlowResult: + async def async_step_import( + self, import_config: dict[str, Any] + ) -> ConfigFlowResult: """Import a config entry from configuration.yaml.""" _LOGGER.debug("Importing Tado from configuration.yaml") username = import_config[CONF_USERNAME] @@ -135,7 +141,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): CONF_PASSWORD: password, }, ) - except exceptions.HomeAssistantError: + except HomeAssistantError: return self.async_abort(reason="import_failed") except PyTado.exceptions.TadoWrongCredentialsException: return self.async_abort(reason="import_failed_invalid_auth") @@ -156,22 +162,22 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback def async_get_options_flow( - config_entry: config_entries.ConfigEntry, + config_entry: ConfigEntry, ) -> OptionsFlowHandler: """Get the options flow for this handler.""" return OptionsFlowHandler(config_entry) -class OptionsFlowHandler(config_entries.OptionsFlow): +class OptionsFlowHandler(OptionsFlow): """Handle an option flow for Tado.""" - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + 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: + ) -> ConfigFlowResult: """Handle options flow.""" if user_input is not None: return self.async_create_entry(data=user_input) @@ -189,13 +195,13 @@ class OptionsFlowHandler(config_entries.OptionsFlow): return self.async_show_form(step_id="init", data_schema=data_schema) -class CannotConnect(exceptions.HomeAssistantError): +class CannotConnect(HomeAssistantError): """Error to indicate we cannot connect.""" -class InvalidAuth(exceptions.HomeAssistantError): +class InvalidAuth(HomeAssistantError): """Error to indicate there is invalid auth.""" -class NoHomes(exceptions.HomeAssistantError): +class NoHomes(HomeAssistantError): """Error to indicate the account has no homes.""" diff --git a/homeassistant/components/tado/const.py b/homeassistant/components/tado/const.py index 6f32eb1a05c..c62352a6d95 100644 --- a/homeassistant/components/tado/const.py +++ b/homeassistant/components/tado/const.py @@ -102,7 +102,7 @@ CONST_EXCLUSIVE_OVERLAY_GROUP = ( # Heat always comes first since we get the -# min and max tempatures for the zone from +# min and max temperatures for the zone from # it. # Heat is preferred as it generally has a lower minimum temperature ORDERED_KNOWN_TADO_MODES = [ diff --git a/homeassistant/components/tado/device_tracker.py b/homeassistant/components/tado/device_tracker.py index eb57aeaec79..dea92ae3890 100644 --- a/homeassistant/components/tado/device_tracker.py +++ b/homeassistant/components/tado/device_tracker.py @@ -1,4 +1,5 @@ """Support for Tado Smart device trackers.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/tado/entity.py b/homeassistant/components/tado/entity.py index 417cfe939d4..38110b4fded 100644 --- a/homeassistant/components/tado/entity.py +++ b/homeassistant/components/tado/entity.py @@ -1,4 +1,5 @@ """Base class for Tado entity.""" + from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity import Entity diff --git a/homeassistant/components/tado/icons.json b/homeassistant/components/tado/icons.json new file mode 100644 index 00000000000..83ef6d4b332 --- /dev/null +++ b/homeassistant/components/tado/icons.json @@ -0,0 +1,8 @@ +{ + "services": { + "set_climate_timer": "mdi:timer", + "set_water_heater_timer": "mdi:timer", + "set_climate_temperature_offset": "mdi:thermometer", + "add_meter_reading": "mdi:counter" + } +} diff --git a/homeassistant/components/tado/sensor.py b/homeassistant/components/tado/sensor.py index 4ff12a6e51d..f8572ac3bc8 100644 --- a/homeassistant/components/tado/sensor.py +++ b/homeassistant/components/tado/sensor.py @@ -1,4 +1,5 @@ """Support for Tado sensors for each zone.""" + from __future__ import annotations from collections.abc import Callable @@ -36,19 +37,12 @@ from .entity import TadoHomeEntity, TadoZoneEntity _LOGGER = logging.getLogger(__name__) -@dataclass(frozen=True) -class TadoSensorEntityDescriptionMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class TadoSensorEntityDescription(SensorEntityDescription): + """Describes Tado sensor entity.""" state_fn: Callable[[Any], StateType] - -@dataclass(frozen=True) -class TadoSensorEntityDescription( - SensorEntityDescription, TadoSensorEntityDescriptionMixin -): - """Describes Tado sensor entity.""" - attributes_fn: Callable[[Any], dict[Any, StateType]] | None = None data_category: str | None = None diff --git a/homeassistant/components/tado/services.py b/homeassistant/components/tado/services.py index a5c5387ce94..d2a0250016f 100644 --- a/homeassistant/components/tado/services.py +++ b/homeassistant/components/tado/services.py @@ -1,4 +1,5 @@ """Services for the Tado integration.""" + import logging import voluptuous as vol diff --git a/homeassistant/components/tado/water_heater.py b/homeassistant/components/tado/water_heater.py index d3ec29ae356..99172228973 100644 --- a/homeassistant/components/tado/water_heater.py +++ b/homeassistant/components/tado/water_heater.py @@ -1,4 +1,5 @@ """Support for Tado hot water zones.""" + import logging from typing import Any diff --git a/homeassistant/components/tag/__init__.py b/homeassistant/components/tag/__init__.py index 59b0fa995e4..4fd20fff24b 100644 --- a/homeassistant/components/tag/__init__.py +++ b/homeassistant/components/tag/__init__.py @@ -1,4 +1,5 @@ """The Tag integration.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/tag/trigger.py b/homeassistant/components/tag/trigger.py index b6d77737eab..4f5f637982b 100644 --- a/homeassistant/components/tag/trigger.py +++ b/homeassistant/components/tag/trigger.py @@ -1,4 +1,5 @@ """Support for tag triggers.""" + from __future__ import annotations import voluptuous as vol diff --git a/homeassistant/components/tailscale/__init__.py b/homeassistant/components/tailscale/__init__.py index 3d0a8e30727..5498687332f 100644 --- a/homeassistant/components/tailscale/__init__.py +++ b/homeassistant/components/tailscale/__init__.py @@ -1,4 +1,5 @@ """The Tailscale integration.""" + from __future__ import annotations from tailscale import Device as TailscaleDevice diff --git a/homeassistant/components/tailscale/binary_sensor.py b/homeassistant/components/tailscale/binary_sensor.py index 00fa21279ea..35c73cd0223 100644 --- a/homeassistant/components/tailscale/binary_sensor.py +++ b/homeassistant/components/tailscale/binary_sensor.py @@ -1,4 +1,5 @@ """Support for Tailscale binary sensors.""" + from __future__ import annotations from collections.abc import Callable @@ -38,42 +39,36 @@ BINARY_SENSORS: tuple[TailscaleBinarySensorEntityDescription, ...] = ( TailscaleBinarySensorEntityDescription( key="client_supports_hair_pinning", translation_key="client_supports_hair_pinning", - icon="mdi:wan", entity_category=EntityCategory.DIAGNOSTIC, is_on_fn=lambda device: device.client_connectivity.client_supports.hair_pinning, ), TailscaleBinarySensorEntityDescription( key="client_supports_ipv6", translation_key="client_supports_ipv6", - icon="mdi:wan", entity_category=EntityCategory.DIAGNOSTIC, is_on_fn=lambda device: device.client_connectivity.client_supports.ipv6, ), TailscaleBinarySensorEntityDescription( key="client_supports_pcp", translation_key="client_supports_pcp", - icon="mdi:wan", entity_category=EntityCategory.DIAGNOSTIC, is_on_fn=lambda device: device.client_connectivity.client_supports.pcp, ), TailscaleBinarySensorEntityDescription( key="client_supports_pmp", translation_key="client_supports_pmp", - icon="mdi:wan", entity_category=EntityCategory.DIAGNOSTIC, is_on_fn=lambda device: device.client_connectivity.client_supports.pmp, ), TailscaleBinarySensorEntityDescription( key="client_supports_udp", translation_key="client_supports_udp", - icon="mdi:wan", entity_category=EntityCategory.DIAGNOSTIC, is_on_fn=lambda device: device.client_connectivity.client_supports.udp, ), TailscaleBinarySensorEntityDescription( key="client_supports_upnp", translation_key="client_supports_upnp", - icon="mdi:wan", entity_category=EntityCategory.DIAGNOSTIC, is_on_fn=lambda device: device.client_connectivity.client_supports.upnp, ), diff --git a/homeassistant/components/tailscale/config_flow.py b/homeassistant/components/tailscale/config_flow.py index 5f28c566801..ef70ed0afcc 100644 --- a/homeassistant/components/tailscale/config_flow.py +++ b/homeassistant/components/tailscale/config_flow.py @@ -1,4 +1,5 @@ """Config flow to configure the Tailscale integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -7,10 +8,9 @@ from typing import Any from tailscale import Tailscale, TailscaleAuthenticationError, TailscaleError import voluptuous as vol -from homeassistant.config_entries import ConfigEntry, ConfigFlow +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import CONF_TAILNET, DOMAIN @@ -36,7 +36,7 @@ class TailscaleFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" errors = {} @@ -82,7 +82,9 @@ class TailscaleFlowHandler(ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Handle initiation of re-authentication with Tailscale.""" self.reauth_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] @@ -91,7 +93,7 @@ class TailscaleFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle re-authentication with Tailscale.""" errors = {} diff --git a/homeassistant/components/tailscale/const.py b/homeassistant/components/tailscale/const.py index 7cdf0cddf71..8af45906a61 100644 --- a/homeassistant/components/tailscale/const.py +++ b/homeassistant/components/tailscale/const.py @@ -1,4 +1,5 @@ """Constants for the Tailscale integration.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/tailscale/coordinator.py b/homeassistant/components/tailscale/coordinator.py index d71116919e7..64ce0147664 100644 --- a/homeassistant/components/tailscale/coordinator.py +++ b/homeassistant/components/tailscale/coordinator.py @@ -1,4 +1,5 @@ """DataUpdateCoordinator for the Tailscale integration.""" + from __future__ import annotations from tailscale import Device, Tailscale, TailscaleAuthenticationError diff --git a/homeassistant/components/tailscale/diagnostics.py b/homeassistant/components/tailscale/diagnostics.py index 687cee7741f..f9e63491660 100644 --- a/homeassistant/components/tailscale/diagnostics.py +++ b/homeassistant/components/tailscale/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for Tailscale.""" + from __future__ import annotations import json diff --git a/homeassistant/components/tailscale/icons.json b/homeassistant/components/tailscale/icons.json new file mode 100644 index 00000000000..a5d3bec1884 --- /dev/null +++ b/homeassistant/components/tailscale/icons.json @@ -0,0 +1,29 @@ +{ + "entity": { + "binary_sensor": { + "client_supports_hair_pinning": { + "default": "mdi:wan" + }, + "client_supports_ipv6": { + "default": "mdi:wan" + }, + "client_supports_pcp": { + "default": "mdi:wan" + }, + "client_supports_pmp": { + "default": "mdi:wan" + }, + "client_supports_udp": { + "default": "mdi:wan" + }, + "client_supports_upnp": { + "default": "mdi:wan" + } + }, + "sensor": { + "ip": { + "default": "mdi:ip-network" + } + } + } +} diff --git a/homeassistant/components/tailscale/sensor.py b/homeassistant/components/tailscale/sensor.py index 5d2e615945b..99b91d17442 100644 --- a/homeassistant/components/tailscale/sensor.py +++ b/homeassistant/components/tailscale/sensor.py @@ -1,4 +1,5 @@ """Support for Tailscale sensors.""" + from __future__ import annotations from collections.abc import Callable @@ -39,7 +40,6 @@ SENSORS: tuple[TailscaleSensorEntityDescription, ...] = ( TailscaleSensorEntityDescription( key="ip", translation_key="ip", - icon="mdi:ip-network", entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda device: device.addresses[0] if device.addresses else None, ), diff --git a/homeassistant/components/tailwind/__init__.py b/homeassistant/components/tailwind/__init__.py index c7ceb88294a..9bd3bb40be0 100644 --- a/homeassistant/components/tailwind/__init__.py +++ b/homeassistant/components/tailwind/__init__.py @@ -1,4 +1,5 @@ """Integration for Tailwind devices.""" + from __future__ import annotations from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/tailwind/binary_sensor.py b/homeassistant/components/tailwind/binary_sensor.py index eaa0cbd1a08..e6a1aa67ae1 100644 --- a/homeassistant/components/tailwind/binary_sensor.py +++ b/homeassistant/components/tailwind/binary_sensor.py @@ -1,4 +1,5 @@ """Binary sensor entity platform for Tailwind.""" + from __future__ import annotations from collections.abc import Callable @@ -34,7 +35,6 @@ DESCRIPTIONS: tuple[TailwindDoorBinarySensorEntityDescription, ...] = ( translation_key="operational_problem", entity_category=EntityCategory.DIAGNOSTIC, device_class=BinarySensorDeviceClass.PROBLEM, - icon="mdi:garage-alert", is_on_fn=lambda door: door.locked_out, ), ) diff --git a/homeassistant/components/tailwind/button.py b/homeassistant/components/tailwind/button.py index 019b803901c..6073b8f7f58 100644 --- a/homeassistant/components/tailwind/button.py +++ b/homeassistant/components/tailwind/button.py @@ -1,4 +1,5 @@ """Button entity platform for Tailwind.""" + from __future__ import annotations from collections.abc import Awaitable, Callable diff --git a/homeassistant/components/tailwind/config_flow.py b/homeassistant/components/tailwind/config_flow.py index 97515f17f3f..7204e9c9202 100644 --- a/homeassistant/components/tailwind/config_flow.py +++ b/homeassistant/components/tailwind/config_flow.py @@ -1,4 +1,5 @@ """Config flow to configure the Tailwind integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -16,9 +17,9 @@ import voluptuous as vol from homeassistant.components import zeroconf from homeassistant.components.dhcp import DhcpServiceInfo -from homeassistant.config_entries import ConfigEntry, ConfigFlow +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_TOKEN -from homeassistant.data_entry_flow import AbortFlow, FlowResult +from homeassistant.data_entry_flow import AbortFlow from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.device_registry import format_mac from homeassistant.helpers.selector import ( @@ -44,7 +45,7 @@ class TailwindFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initiated by the user.""" errors = {} @@ -84,7 +85,7 @@ class TailwindFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_zeroconf( self, discovery_info: zeroconf.ZeroconfServiceInfo - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle zeroconf discovery of a Tailwind device.""" if not (device_id := discovery_info.properties.get("device_id")): return self.async_abort(reason="no_device_id") @@ -112,7 +113,7 @@ class TailwindFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_zeroconf_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initiated by zeroconf.""" errors = {} @@ -143,7 +144,7 @@ class TailwindFlowHandler(ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_reauth(self, _: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth(self, _: Mapping[str, Any]) -> ConfigFlowResult: """Handle initiation of re-authentication with a Tailwind device.""" self.reauth_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] @@ -152,7 +153,7 @@ class TailwindFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle re-authentication with a Tailwind device.""" errors = {} @@ -183,7 +184,9 @@ class TailwindFlowHandler(ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_dhcp(self, discovery_info: DhcpServiceInfo) -> FlowResult: + async def async_step_dhcp( + self, discovery_info: DhcpServiceInfo + ) -> ConfigFlowResult: """Handle dhcp discovery to update existing entries. This flow is triggered only by DHCP discovery of known devices. @@ -196,7 +199,9 @@ class TailwindFlowHandler(ConfigFlow, domain=DOMAIN): # abort the flow with an unknown error. return self.async_abort(reason="unknown") - async def _async_step_create_entry(self, *, host: str, token: str) -> FlowResult: + async def _async_step_create_entry( + self, *, host: str, token: str + ) -> ConfigFlowResult: """Create entry.""" tailwind = Tailwind( host=host, token=token, session=async_get_clientsession(self.hass) @@ -208,14 +213,13 @@ class TailwindFlowHandler(ConfigFlow, domain=DOMAIN): return self.async_abort(reason="unsupported_firmware") if self.reauth_entry: - self.hass.config_entries.async_update_entry( + return self.async_update_reload_and_abort( self.reauth_entry, - data={CONF_HOST: host, CONF_TOKEN: token}, + data={ + CONF_HOST: host, + CONF_TOKEN: token, + }, ) - self.hass.async_create_task( - self.hass.config_entries.async_reload(self.reauth_entry.entry_id) - ) - return self.async_abort(reason="reauth_successful") await self.async_set_unique_id( format_mac(status.mac_address), raise_on_progress=False diff --git a/homeassistant/components/tailwind/const.py b/homeassistant/components/tailwind/const.py index 99e5bb0f1bf..f4320d04374 100644 --- a/homeassistant/components/tailwind/const.py +++ b/homeassistant/components/tailwind/const.py @@ -1,4 +1,5 @@ """Constants for the Tailwind integration.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/tailwind/coordinator.py b/homeassistant/components/tailwind/coordinator.py index d918b093605..d7cbb248885 100644 --- a/homeassistant/components/tailwind/coordinator.py +++ b/homeassistant/components/tailwind/coordinator.py @@ -1,4 +1,5 @@ """Data update coordinator for Tailwind.""" + from datetime import timedelta from gotailwind import ( diff --git a/homeassistant/components/tailwind/cover.py b/homeassistant/components/tailwind/cover.py index 335c3404cdd..f54902dac4a 100644 --- a/homeassistant/components/tailwind/cover.py +++ b/homeassistant/components/tailwind/cover.py @@ -1,4 +1,5 @@ """Cover entity platform for Tailwind.""" + from __future__ import annotations from typing import Any @@ -70,19 +71,16 @@ class TailwindDoorCoverEntity(TailwindDoorEntity, CoverEntity): ) except TailwindDoorDisabledError as exc: raise HomeAssistantError( - str(exc), translation_domain=DOMAIN, translation_key="door_disabled", ) from exc except TailwindDoorLockedOutError as exc: raise HomeAssistantError( - str(exc), translation_domain=DOMAIN, translation_key="door_locked_out", ) from exc except TailwindError as exc: raise HomeAssistantError( - str(exc), translation_domain=DOMAIN, translation_key="communication_error", ) from exc @@ -105,19 +103,16 @@ class TailwindDoorCoverEntity(TailwindDoorEntity, CoverEntity): ) except TailwindDoorDisabledError as exc: raise HomeAssistantError( - str(exc), translation_domain=DOMAIN, translation_key="door_disabled", ) from exc except TailwindDoorLockedOutError as exc: raise HomeAssistantError( - str(exc), translation_domain=DOMAIN, translation_key="door_locked_out", ) from exc except TailwindError as exc: raise HomeAssistantError( - str(exc), translation_domain=DOMAIN, translation_key="communication_error", ) from exc diff --git a/homeassistant/components/tailwind/diagnostics.py b/homeassistant/components/tailwind/diagnostics.py index 50c9b2266e2..970bb5174eb 100644 --- a/homeassistant/components/tailwind/diagnostics.py +++ b/homeassistant/components/tailwind/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics platform for Tailwind.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/tailwind/entity.py b/homeassistant/components/tailwind/entity.py index 843cc600582..ec13dc7bd1f 100644 --- a/homeassistant/components/tailwind/entity.py +++ b/homeassistant/components/tailwind/entity.py @@ -1,4 +1,5 @@ """Base entity for the Tailwind integration.""" + from __future__ import annotations from homeassistant.helpers.device_registry import DeviceInfo diff --git a/homeassistant/components/tailwind/icons.json b/homeassistant/components/tailwind/icons.json new file mode 100644 index 00000000000..ed2c6af16b3 --- /dev/null +++ b/homeassistant/components/tailwind/icons.json @@ -0,0 +1,14 @@ +{ + "entity": { + "binary_sensor": { + "operational_problem": { + "default": "mdi:garage-alert" + } + }, + "number": { + "brightness": { + "default": "mdi:led-on" + } + } + } +} diff --git a/homeassistant/components/tailwind/number.py b/homeassistant/components/tailwind/number.py index 5853e5c2d30..63c01cf7e73 100644 --- a/homeassistant/components/tailwind/number.py +++ b/homeassistant/components/tailwind/number.py @@ -1,4 +1,5 @@ """Number entity platform for Tailwind.""" + from __future__ import annotations from collections.abc import Awaitable, Callable @@ -30,7 +31,6 @@ class TailwindNumberEntityDescription(NumberEntityDescription): DESCRIPTIONS = [ TailwindNumberEntityDescription( key="brightness", - icon="mdi:led-on", translation_key="brightness", entity_category=EntityCategory.CONFIG, native_step=1, @@ -77,7 +77,6 @@ class TailwindNumberEntity(TailwindEntity, NumberEntity): await self.entity_description.set_value_fn(self.coordinator.tailwind, value) except TailwindError as exc: raise HomeAssistantError( - str(exc), translation_domain=DOMAIN, translation_key="communication_error", ) from exc diff --git a/homeassistant/components/tami4/__init__.py b/homeassistant/components/tami4/__init__.py index 643363b1285..2755157214e 100644 --- a/homeassistant/components/tami4/__init__.py +++ b/homeassistant/components/tami4/__init__.py @@ -1,4 +1,5 @@ """The Tami4Edge integration.""" + from __future__ import annotations from Tami4EdgeAPI import Tami4EdgeAPI, exceptions diff --git a/homeassistant/components/tami4/button.py b/homeassistant/components/tami4/button.py index c17a296e219..2d8af3fcf89 100644 --- a/homeassistant/components/tami4/button.py +++ b/homeassistant/components/tami4/button.py @@ -1,4 +1,5 @@ """Button entities for Tami4Edge.""" + from collections.abc import Callable from dataclasses import dataclass import logging @@ -27,7 +28,6 @@ BUTTONS: tuple[Tami4EdgeButtonEntityDescription] = ( Tami4EdgeButtonEntityDescription( key="boil_water", translation_key="boil_water", - icon="mdi:kettle-steam", press_fn=lambda api: api.boil_water(), ), ) diff --git a/homeassistant/components/tami4/config_flow.py b/homeassistant/components/tami4/config_flow.py index b36ba9c46c0..3f70d0a99ca 100644 --- a/homeassistant/components/tami4/config_flow.py +++ b/homeassistant/components/tami4/config_flow.py @@ -1,4 +1,5 @@ """Config flow for edge integration.""" + from __future__ import annotations import logging @@ -8,8 +9,7 @@ from typing import Any from Tami4EdgeAPI import Tami4EdgeAPI, exceptions import voluptuous as vol -from homeassistant import config_entries -from homeassistant.data_entry_flow import FlowResult +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv @@ -23,7 +23,7 @@ _STEP_OTP_CODE_SCHEMA = vol.Schema({vol.Required("otp"): cv.string}) _PHONE_MATCHER = re.compile(r"^(\+?972)?0?(?P\d{8,9})$") -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class Tami4ConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Tami4Edge.""" VERSION = 1 @@ -32,7 +32,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the otp request step.""" errors = {} if user_input is not None: @@ -62,7 +62,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_otp( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the otp submission step.""" errors = {} if user_input is not None: diff --git a/homeassistant/components/tami4/const.py b/homeassistant/components/tami4/const.py index 4e64bdf896d..be737b5c974 100644 --- a/homeassistant/components/tami4/const.py +++ b/homeassistant/components/tami4/const.py @@ -1,4 +1,5 @@ """Constants for tami4 component.""" + DOMAIN = "tami4" CONF_PHONE = "phone" CONF_REFRESH_TOKEN = "refresh_token" diff --git a/homeassistant/components/tami4/coordinator.py b/homeassistant/components/tami4/coordinator.py index ef57af71012..78a3723a876 100644 --- a/homeassistant/components/tami4/coordinator.py +++ b/homeassistant/components/tami4/coordinator.py @@ -1,4 +1,5 @@ """Water quality coordinator for Tami4Edge.""" + from dataclasses import dataclass from datetime import date, timedelta import logging diff --git a/homeassistant/components/tami4/entity.py b/homeassistant/components/tami4/entity.py index 50c066b9b6d..d84cd82f39a 100644 --- a/homeassistant/components/tami4/entity.py +++ b/homeassistant/components/tami4/entity.py @@ -1,4 +1,5 @@ """Base entity for Tami4Edge.""" + from __future__ import annotations from Tami4EdgeAPI import Tami4EdgeAPI diff --git a/homeassistant/components/tami4/icons.json b/homeassistant/components/tami4/icons.json new file mode 100644 index 00000000000..d623bdc6007 --- /dev/null +++ b/homeassistant/components/tami4/icons.json @@ -0,0 +1,32 @@ +{ + "entity": { + "button": { + "boil_water": { + "default": "mdi:kettle-steam" + } + }, + "sensor": { + "uv_last_replacement": { + "default": "mdi:calendar" + }, + "uv_upcoming_replacement": { + "default": "mdi:calendar" + }, + "uv_status": { + "default": "mdi:clipboard-check-multiple" + }, + "filter_last_replacement": { + "default": "mdi:calendar" + }, + "filter_upcoming_replacement": { + "default": "mdi:calendar" + }, + "filter_status": { + "default": "mdi:clipboard-check-multiple" + }, + "filter_litters_passed": { + "default": "mdi:water" + } + } + } +} diff --git a/homeassistant/components/tami4/sensor.py b/homeassistant/components/tami4/sensor.py index df271da7309..3772ef0bccb 100644 --- a/homeassistant/components/tami4/sensor.py +++ b/homeassistant/components/tami4/sensor.py @@ -1,4 +1,5 @@ """Sensor entities for Tami4Edge.""" + import logging from Tami4EdgeAPI import Tami4EdgeAPI @@ -25,41 +26,34 @@ ENTITY_DESCRIPTIONS = [ SensorEntityDescription( key="uv_last_replacement", translation_key="uv_last_replacement", - icon="mdi:calendar", device_class=SensorDeviceClass.DATE, ), SensorEntityDescription( key="uv_upcoming_replacement", translation_key="uv_upcoming_replacement", - icon="mdi:calendar", device_class=SensorDeviceClass.DATE, ), SensorEntityDescription( key="uv_status", translation_key="uv_status", - icon="mdi:clipboard-check-multiple", ), SensorEntityDescription( key="filter_last_replacement", translation_key="filter_last_replacement", - icon="mdi:calendar", device_class=SensorDeviceClass.DATE, ), SensorEntityDescription( key="filter_upcoming_replacement", translation_key="filter_upcoming_replacement", - icon="mdi:calendar", device_class=SensorDeviceClass.DATE, ), SensorEntityDescription( key="filter_status", translation_key="filter_status", - icon="mdi:clipboard-check-multiple", ), SensorEntityDescription( key="filter_litters_passed", translation_key="filter_litters_passed", - icon="mdi:water", state_class=SensorStateClass.TOTAL, device_class=SensorDeviceClass.WATER, native_unit_of_measurement=UnitOfVolume.LITERS, @@ -75,17 +69,14 @@ async def async_setup_entry( api: Tami4EdgeAPI = data[API] coordinator: Tami4EdgeWaterQualityCoordinator = data[COORDINATOR] - entities = [] - for entity_description in ENTITY_DESCRIPTIONS: - entities.append( - Tami4EdgeSensorEntity( - coordinator=coordinator, - api=api, - entity_description=entity_description, - ) + async_add_entities( + Tami4EdgeSensorEntity( + coordinator=coordinator, + api=api, + entity_description=entity_description, ) - - async_add_entities(entities) + for entity_description in ENTITY_DESCRIPTIONS + ) class Tami4EdgeSensorEntity( diff --git a/homeassistant/components/tank_utility/sensor.py b/homeassistant/components/tank_utility/sensor.py index 0aecbb0f405..e0b3376ca2e 100644 --- a/homeassistant/components/tank_utility/sensor.py +++ b/homeassistant/components/tank_utility/sensor.py @@ -1,4 +1,5 @@ """Support for the Tank Utility propane monitor.""" + from __future__ import annotations import datetime diff --git a/homeassistant/components/tankerkoenig/__init__.py b/homeassistant/components/tankerkoenig/__init__.py index 3f86ef03df7..7443fa72b5b 100644 --- a/homeassistant/components/tankerkoenig/__init__.py +++ b/homeassistant/components/tankerkoenig/__init__.py @@ -1,4 +1,5 @@ """Ask tankerkoenig.de for petrol price information.""" + from __future__ import annotations from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/tankerkoenig/binary_sensor.py b/homeassistant/components/tankerkoenig/binary_sensor.py index 640708e1cb4..03ffb819a1f 100644 --- a/homeassistant/components/tankerkoenig/binary_sensor.py +++ b/homeassistant/components/tankerkoenig/binary_sensor.py @@ -1,4 +1,5 @@ """Tankerkoenig binary sensor integration.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/tankerkoenig/config_flow.py b/homeassistant/components/tankerkoenig/config_flow.py index 9bdf5ef0fe0..e5a84374a09 100644 --- a/homeassistant/components/tankerkoenig/config_flow.py +++ b/homeassistant/components/tankerkoenig/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Tankerkoenig.""" + from __future__ import annotations from collections.abc import Mapping @@ -13,7 +14,12 @@ from aiotankerkoenig import ( ) import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.const import ( CONF_API_KEY, CONF_LATITUDE, @@ -25,7 +31,6 @@ from homeassistant.const import ( UnitOfLength, ) from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.selector import ( @@ -52,7 +57,7 @@ async def async_get_nearby_stations( ) -class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class FlowHandler(ConfigFlow, domain=DOMAIN): """Handle a config flow.""" VERSION = 1 @@ -66,14 +71,14 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback def async_get_options_flow( - config_entry: config_entries.ConfigEntry, + config_entry: ConfigEntry, ) -> OptionsFlowHandler: """Get the options flow for this handler.""" return OptionsFlowHandler(config_entry) async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" if not user_input: return self._show_form_user() @@ -110,7 +115,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_select_station( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the step select_station of a flow initialized by the user.""" if not user_input: return self.async_show_form( @@ -126,13 +131,15 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): options={CONF_SHOW_ON_MAP: True}, ) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Perform reauth upon an API authentication error.""" return await self.async_step_reauth_confirm() async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Perform reauth confirm upon an API authentication error.""" if not user_input: return self._show_form_reauth() @@ -158,7 +165,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None, errors: dict[str, Any] | None = None, - ) -> FlowResult: + ) -> ConfigFlowResult: if user_input is None: user_input = {} return self.async_show_form( @@ -204,7 +211,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None, errors: dict[str, Any] | None = None, - ) -> FlowResult: + ) -> ConfigFlowResult: if user_input is None: user_input = {} return self.async_show_form( @@ -221,7 +228,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): def _create_entry( self, data: dict[str, Any], options: dict[str, Any] - ) -> FlowResult: + ) -> ConfigFlowResult: return self.async_create_entry( title=data[CONF_NAME], data=data, @@ -229,17 +236,17 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) -class OptionsFlowHandler(config_entries.OptionsFlow): +class OptionsFlowHandler(OptionsFlow): """Handle an options flow.""" - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + def __init__(self, config_entry: ConfigEntry) -> None: """Initialize options flow.""" self.config_entry = config_entry self._stations: dict[str, str] = {} async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle options flow.""" if user_input is not None: self.hass.config_entries.async_update_entry( diff --git a/homeassistant/components/tankerkoenig/coordinator.py b/homeassistant/components/tankerkoenig/coordinator.py index c2d91f20b8a..447099d2dca 100644 --- a/homeassistant/components/tankerkoenig/coordinator.py +++ b/homeassistant/components/tankerkoenig/coordinator.py @@ -1,4 +1,5 @@ """The Tankerkoenig update coordinator.""" + from __future__ import annotations from datetime import timedelta @@ -61,8 +62,18 @@ class TankerkoenigDataUpdateCoordinator(DataUpdateCoordinator): try: station = await self._tankerkoenig.station_details(station_id) except TankerkoenigInvalidKeyError as err: + _LOGGER.debug( + "invalid key error occur during setup of station %s %s", + station_id, + err, + ) raise ConfigEntryAuthFailed(err) from err except TankerkoenigConnectionError as err: + _LOGGER.debug( + "connection error occur during setup of station %s %s", + station_id, + err, + ) raise ConfigEntryNotReady(err) from err except TankerkoenigError as err: _LOGGER.error("Error when adding station %s %s", station_id, err) @@ -85,17 +96,27 @@ class TankerkoenigDataUpdateCoordinator(DataUpdateCoordinator): # The API seems to only return at most 10 results, so split the list in chunks of 10 # and merge it together. for index in range(ceil(len(station_ids) / 10)): + stations = station_ids[index * 10 : (index + 1) * 10] try: - data = await self._tankerkoenig.prices( - station_ids[index * 10 : (index + 1) * 10] - ) + data = await self._tankerkoenig.prices(stations) except TankerkoenigInvalidKeyError as err: + _LOGGER.debug( + "invalid key error occur during update of stations %s %s", + stations, + err, + ) raise ConfigEntryAuthFailed(err) from err + except TankerkoenigRateLimitError as err: + _LOGGER.warning( + "API rate limit reached, consider to increase polling interval" + ) + raise UpdateFailed(err) from err except (TankerkoenigError, TankerkoenigConnectionError) as err: - if isinstance(err, TankerkoenigRateLimitError): - _LOGGER.warning( - "API rate limit reached, consider to increase polling interval" - ) + _LOGGER.debug( + "error occur during update of stations %s %s", + stations, + err, + ) raise UpdateFailed(err) from err prices.update(data) diff --git a/homeassistant/components/tankerkoenig/diagnostics.py b/homeassistant/components/tankerkoenig/diagnostics.py index d5fd7c8cada..4846d2687a2 100644 --- a/homeassistant/components/tankerkoenig/diagnostics.py +++ b/homeassistant/components/tankerkoenig/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for Tankerkoenig.""" + from __future__ import annotations from dataclasses import asdict diff --git a/homeassistant/components/tankerkoenig/entity.py b/homeassistant/components/tankerkoenig/entity.py index 96dafa80580..316a8e37dee 100644 --- a/homeassistant/components/tankerkoenig/entity.py +++ b/homeassistant/components/tankerkoenig/entity.py @@ -1,4 +1,5 @@ """The tankerkoenig base entity.""" + from aiotankerkoenig import Station from homeassistant.const import ATTR_ID diff --git a/homeassistant/components/tankerkoenig/sensor.py b/homeassistant/components/tankerkoenig/sensor.py index c0394bd318f..f2fdc2c45b7 100644 --- a/homeassistant/components/tankerkoenig/sensor.py +++ b/homeassistant/components/tankerkoenig/sensor.py @@ -1,4 +1,5 @@ """Tankerkoenig sensor integration.""" + from __future__ import annotations import logging @@ -64,6 +65,19 @@ class FuelPriceSensor(TankerkoenigCoordinatorEntity, SensorEntity): _attr_attribution = ATTRIBUTION _attr_state_class = SensorStateClass.MEASUREMENT _attr_native_unit_of_measurement = CURRENCY_EURO + _unrecorded_attributes = frozenset( + { + ATTR_BRAND, + ATTR_CITY, + ATTR_HOUSE_NUMBER, + ATTR_POSTCODE, + ATTR_STATION_NAME, + ATTR_STREET, + ATTRIBUTION, + ATTR_LATITUDE, + ATTR_LONGITUDE, + } + ) def __init__( self, diff --git a/homeassistant/components/tapsaff/binary_sensor.py b/homeassistant/components/tapsaff/binary_sensor.py index 5ffa8690f94..a8b3c138db5 100644 --- a/homeassistant/components/tapsaff/binary_sensor.py +++ b/homeassistant/components/tapsaff/binary_sensor.py @@ -1,4 +1,5 @@ """Support for Taps Affs.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/tasmota/__init__.py b/homeassistant/components/tasmota/__init__.py index 7d4331f0d40..271cfba9b79 100644 --- a/homeassistant/components/tasmota/__init__.py +++ b/homeassistant/components/tasmota/__init__.py @@ -1,4 +1,5 @@ """The Tasmota integration.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/tasmota/binary_sensor.py b/homeassistant/components/tasmota/binary_sensor.py index d84087b3132..071cce81880 100644 --- a/homeassistant/components/tasmota/binary_sensor.py +++ b/homeassistant/components/tasmota/binary_sensor.py @@ -1,4 +1,5 @@ """Support for Tasmota binary sensors.""" + from __future__ import annotations from collections.abc import Callable @@ -42,12 +43,12 @@ async def async_setup_entry( ] ) - hass.data[ - DATA_REMOVE_DISCOVER_COMPONENT.format(binary_sensor.DOMAIN) - ] = async_dispatcher_connect( - hass, - TASMOTA_DISCOVERY_ENTITY_NEW.format(binary_sensor.DOMAIN), - async_discover, + hass.data[DATA_REMOVE_DISCOVER_COMPONENT.format(binary_sensor.DOMAIN)] = ( + async_dispatcher_connect( + hass, + TASMOTA_DISCOVERY_ENTITY_NEW.format(binary_sensor.DOMAIN), + async_discover, + ) ) diff --git a/homeassistant/components/tasmota/config_flow.py b/homeassistant/components/tasmota/config_flow.py index d8981090d58..9deb846f8e2 100644 --- a/homeassistant/components/tasmota/config_flow.py +++ b/homeassistant/components/tasmota/config_flow.py @@ -1,19 +1,19 @@ """Config flow for Tasmota.""" + from __future__ import annotations from typing import Any import voluptuous as vol -from homeassistant import config_entries from homeassistant.components.mqtt import valid_subscribe_topic -from homeassistant.data_entry_flow import FlowResult +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.helpers.service_info.mqtt import MqttServiceInfo from .const import CONF_DISCOVERY_PREFIX, DEFAULT_PREFIX, DOMAIN -class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class FlowHandler(ConfigFlow, domain=DOMAIN): """Handle a config flow.""" VERSION = 1 @@ -22,7 +22,9 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Initialize flow.""" self._prefix = DEFAULT_PREFIX - async def async_step_mqtt(self, discovery_info: MqttServiceInfo) -> FlowResult: + async def async_step_mqtt( + self, discovery_info: MqttServiceInfo + ) -> ConfigFlowResult: """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") @@ -45,7 +47,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") @@ -56,7 +58,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_config( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm the setup.""" errors = {} data = {CONF_DISCOVERY_PREFIX: self._prefix} @@ -85,7 +87,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm the setup.""" data = {CONF_DISCOVERY_PREFIX: self._prefix} diff --git a/homeassistant/components/tasmota/const.py b/homeassistant/components/tasmota/const.py index 2e38284e43d..1a2cb431a0b 100644 --- a/homeassistant/components/tasmota/const.py +++ b/homeassistant/components/tasmota/const.py @@ -1,4 +1,5 @@ """Constants used by multiple Tasmota modules.""" + from homeassistant.const import Platform CONF_DISCOVERY_PREFIX = "discovery_prefix" diff --git a/homeassistant/components/tasmota/cover.py b/homeassistant/components/tasmota/cover.py index 4b851a86406..4ab9464e9f9 100644 --- a/homeassistant/components/tasmota/cover.py +++ b/homeassistant/components/tasmota/cover.py @@ -1,4 +1,5 @@ """Support for Tasmota covers.""" + from __future__ import annotations from typing import Any @@ -40,12 +41,12 @@ async def async_setup_entry( [TasmotaCover(tasmota_entity=tasmota_entity, discovery_hash=discovery_hash)] ) - hass.data[ - DATA_REMOVE_DISCOVER_COMPONENT.format(COVER_DOMAIN) - ] = async_dispatcher_connect( - hass, - TASMOTA_DISCOVERY_ENTITY_NEW.format(COVER_DOMAIN), - async_discover, + hass.data[DATA_REMOVE_DISCOVER_COMPONENT.format(COVER_DOMAIN)] = ( + async_dispatcher_connect( + hass, + TASMOTA_DISCOVERY_ENTITY_NEW.format(COVER_DOMAIN), + async_discover, + ) ) diff --git a/homeassistant/components/tasmota/device_automation.py b/homeassistant/components/tasmota/device_automation.py index 98c7d1355c3..ef05585dd87 100644 --- a/homeassistant/components/tasmota/device_automation.py +++ b/homeassistant/components/tasmota/device_automation.py @@ -1,5 +1,8 @@ """Provides device automations for Tasmota.""" +from collections.abc import Mapping +from typing import Any + from hatasmota.const import AUTOMATION_TYPE_TRIGGER from hatasmota.models import DiscoveryHashType from hatasmota.trigger import TasmotaTrigger @@ -27,9 +30,9 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> N await async_remove_automations(hass, event.data["device_id"]) @callback - def _async_device_removed_filter(event: Event) -> bool: + def _async_device_removed_filter(event_data: Mapping[str, Any]) -> bool: """Filter device registry events.""" - return event.data["action"] == "remove" + return event_data["action"] == "remove" async def async_discover( tasmota_automation: TasmotaTrigger, discovery_hash: DiscoveryHashType @@ -40,12 +43,12 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> N hass, tasmota_automation, config_entry, discovery_hash ) - hass.data[ - DATA_REMOVE_DISCOVER_COMPONENT.format("device_automation") - ] = async_dispatcher_connect( - hass, - TASMOTA_DISCOVERY_ENTITY_NEW.format("device_automation"), - async_discover, + hass.data[DATA_REMOVE_DISCOVER_COMPONENT.format("device_automation")] = ( + async_dispatcher_connect( + hass, + TASMOTA_DISCOVERY_ENTITY_NEW.format("device_automation"), + async_discover, + ) ) hass.data[DATA_UNSUB].append( hass.bus.async_listen( diff --git a/homeassistant/components/tasmota/device_trigger.py b/homeassistant/components/tasmota/device_trigger.py index f01cdddb1db..a357dfff1c0 100644 --- a/homeassistant/components/tasmota/device_trigger.py +++ b/homeassistant/components/tasmota/device_trigger.py @@ -1,4 +1,5 @@ """Provides device automations for Tasmota.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/tasmota/discovery.py b/homeassistant/components/tasmota/discovery.py index 70cedd9dd3d..5d70330dbdf 100644 --- a/homeassistant/components/tasmota/discovery.py +++ b/homeassistant/components/tasmota/discovery.py @@ -1,4 +1,5 @@ """Support for Tasmota device discovery.""" + from __future__ import annotations from collections.abc import Awaitable, Callable diff --git a/homeassistant/components/tasmota/fan.py b/homeassistant/components/tasmota/fan.py index 38ba5fcc476..cdb0fb8d2f6 100644 --- a/homeassistant/components/tasmota/fan.py +++ b/homeassistant/components/tasmota/fan.py @@ -1,4 +1,5 @@ """Support for Tasmota fans.""" + from __future__ import annotations from typing import Any @@ -48,12 +49,12 @@ async def async_setup_entry( [TasmotaFan(tasmota_entity=tasmota_entity, discovery_hash=discovery_hash)] ) - hass.data[ - DATA_REMOVE_DISCOVER_COMPONENT.format(FAN_DOMAIN) - ] = async_dispatcher_connect( - hass, - TASMOTA_DISCOVERY_ENTITY_NEW.format(FAN_DOMAIN), - async_discover, + hass.data[DATA_REMOVE_DISCOVER_COMPONENT.format(FAN_DOMAIN)] = ( + async_dispatcher_connect( + hass, + TASMOTA_DISCOVERY_ENTITY_NEW.format(FAN_DOMAIN), + async_discover, + ) ) diff --git a/homeassistant/components/tasmota/light.py b/homeassistant/components/tasmota/light.py index f5b70eca9ce..5effc9c4997 100644 --- a/homeassistant/components/tasmota/light.py +++ b/homeassistant/components/tasmota/light.py @@ -1,4 +1,5 @@ """Support for Tasmota lights.""" + from __future__ import annotations from typing import Any @@ -56,12 +57,12 @@ async def async_setup_entry( [TasmotaLight(tasmota_entity=tasmota_entity, discovery_hash=discovery_hash)] ) - hass.data[ - DATA_REMOVE_DISCOVER_COMPONENT.format(light.DOMAIN) - ] = async_dispatcher_connect( - hass, - TASMOTA_DISCOVERY_ENTITY_NEW.format(light.DOMAIN), - async_discover, + hass.data[DATA_REMOVE_DISCOVER_COMPONENT.format(light.DOMAIN)] = ( + async_dispatcher_connect( + hass, + TASMOTA_DISCOVERY_ENTITY_NEW.format(light.DOMAIN), + async_discover, + ) ) diff --git a/homeassistant/components/tasmota/mixins.py b/homeassistant/components/tasmota/mixins.py index 48dbe51fd67..8c0da1bcc2a 100644 --- a/homeassistant/components/tasmota/mixins.py +++ b/homeassistant/components/tasmota/mixins.py @@ -1,4 +1,5 @@ """Tasmota entity mixins.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/tasmota/sensor.py b/homeassistant/components/tasmota/sensor.py index 29d3f5c8c8a..546e3eb4539 100644 --- a/homeassistant/components/tasmota/sensor.py +++ b/homeassistant/components/tasmota/sensor.py @@ -1,4 +1,5 @@ """Support for Tasmota sensors.""" + from __future__ import annotations from datetime import datetime @@ -251,12 +252,12 @@ async def async_setup_entry( ] ) - hass.data[ - DATA_REMOVE_DISCOVER_COMPONENT.format(sensor.DOMAIN) - ] = async_dispatcher_connect( - hass, - TASMOTA_DISCOVERY_ENTITY_NEW.format(sensor.DOMAIN), - async_discover, + hass.data[DATA_REMOVE_DISCOVER_COMPONENT.format(sensor.DOMAIN)] = ( + async_dispatcher_connect( + hass, + TASMOTA_DISCOVERY_ENTITY_NEW.format(sensor.DOMAIN), + async_discover, + ) ) diff --git a/homeassistant/components/tasmota/switch.py b/homeassistant/components/tasmota/switch.py index 8ee4d2f47ee..44c45621e09 100644 --- a/homeassistant/components/tasmota/switch.py +++ b/homeassistant/components/tasmota/switch.py @@ -1,4 +1,5 @@ """Support for Tasmota switches.""" + from typing import Any from hatasmota import relay as tasmota_relay @@ -37,12 +38,12 @@ async def async_setup_entry( ] ) - hass.data[ - DATA_REMOVE_DISCOVER_COMPONENT.format(switch.DOMAIN) - ] = async_dispatcher_connect( - hass, - TASMOTA_DISCOVERY_ENTITY_NEW.format(switch.DOMAIN), - async_discover, + hass.data[DATA_REMOVE_DISCOVER_COMPONENT.format(switch.DOMAIN)] = ( + async_dispatcher_connect( + hass, + TASMOTA_DISCOVERY_ENTITY_NEW.format(switch.DOMAIN), + async_discover, + ) ) diff --git a/homeassistant/components/tautulli/__init__.py b/homeassistant/components/tautulli/__init__.py index b7e62846ac1..b7fcf48cfdb 100644 --- a/homeassistant/components/tautulli/__init__.py +++ b/homeassistant/components/tautulli/__init__.py @@ -1,4 +1,5 @@ """The Tautulli integration.""" + from __future__ import annotations from pytautulli import PyTautulli, PyTautulliApiUser, PyTautulliHostConfiguration diff --git a/homeassistant/components/tautulli/config_flow.py b/homeassistant/components/tautulli/config_flow.py index 532852687da..a8378786d18 100644 --- a/homeassistant/components/tautulli/config_flow.py +++ b/homeassistant/components/tautulli/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Tautulli.""" + from __future__ import annotations from collections.abc import Mapping @@ -7,9 +8,8 @@ from typing import Any from pytautulli import PyTautulli, PyTautulliException, exceptions import voluptuous as vol -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_API_KEY, CONF_URL, CONF_VERIFY_SSL -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DEFAULT_NAME, DOMAIN @@ -22,7 +22,7 @@ class TautulliConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initiated by the user.""" errors = {} if user_input is not None: @@ -49,13 +49,15 @@ class TautulliConfigFlow(ConfigFlow, domain=DOMAIN): errors=errors or {}, ) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Handle a reauthorization flow request.""" return await self.async_step_reauth_confirm() async def async_step_reauth_confirm( self, user_input: dict[str, str] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm reauth dialog.""" errors = {} if user_input is not None and ( diff --git a/homeassistant/components/tautulli/const.py b/homeassistant/components/tautulli/const.py index c0ca923c3e5..8c5d5e4c8b4 100644 --- a/homeassistant/components/tautulli/const.py +++ b/homeassistant/components/tautulli/const.py @@ -1,4 +1,5 @@ """Constants for the Tautulli integration.""" + from logging import Logger, getLogger ATTR_TOP_USER = "top_user" diff --git a/homeassistant/components/tautulli/coordinator.py b/homeassistant/components/tautulli/coordinator.py index d6e827acd8e..be7dfce4e3a 100644 --- a/homeassistant/components/tautulli/coordinator.py +++ b/homeassistant/components/tautulli/coordinator.py @@ -1,4 +1,5 @@ """Data update coordinator for the Tautulli integration.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/tautulli/icons.json b/homeassistant/components/tautulli/icons.json new file mode 100644 index 00000000000..487f11e77f0 --- /dev/null +++ b/homeassistant/components/tautulli/icons.json @@ -0,0 +1,36 @@ +{ + "entity": { + "sensor": { + "watching_count": { + "default": "mdi:plex" + }, + "stream_count_direct_play": { + "default": "mdi:plex" + }, + "stream_count_direct_stream": { + "default": "mdi:plex" + }, + "stream_count_transcode": { + "default": "mdi:plex" + }, + "top_movies": { + "default": "mdi:movie-open" + }, + "top_tv": { + "default": "mdi:television" + }, + "top_user": { + "default": "mdi:walk" + }, + "state": { + "default": "mdi:plex" + }, + "progress": { + "default": "mdi:progress-clock" + }, + "transcode_decision": { + "default": "mdi:plex" + } + } + } +} diff --git a/homeassistant/components/tautulli/sensor.py b/homeassistant/components/tautulli/sensor.py index ca9de9df8de..f0d274bbe12 100644 --- a/homeassistant/components/tautulli/sensor.py +++ b/homeassistant/components/tautulli/sensor.py @@ -1,4 +1,5 @@ """A platform which allows you to get information from Tautulli.""" + from __future__ import annotations from collections.abc import Callable @@ -43,30 +44,21 @@ def get_top_stats( return value -@dataclass(frozen=True) -class TautulliSensorEntityMixin: - """Mixin for Tautulli sensor.""" +@dataclass(frozen=True, kw_only=True) +class TautulliSensorEntityDescription(SensorEntityDescription): + """Describes a Tautulli sensor.""" value_fn: Callable[[PyTautulliApiHomeStats, PyTautulliApiActivity, str], StateType] -@dataclass(frozen=True) -class TautulliSensorEntityDescription( - SensorEntityDescription, TautulliSensorEntityMixin -): - """Describes a Tautulli sensor.""" - - SENSOR_TYPES: tuple[TautulliSensorEntityDescription, ...] = ( TautulliSensorEntityDescription( - icon="mdi:plex", key="watching_count", translation_key="watching_count", 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", translation_key="stream_count_direct_play", entity_category=EntityCategory.DIAGNOSTIC, @@ -77,7 +69,6 @@ SENSOR_TYPES: tuple[TautulliSensorEntityDescription, ...] = ( ), ), TautulliSensorEntityDescription( - icon="mdi:plex", key="stream_count_direct_stream", translation_key="stream_count_direct_stream", entity_category=EntityCategory.DIAGNOSTIC, @@ -88,7 +79,6 @@ SENSOR_TYPES: tuple[TautulliSensorEntityDescription, ...] = ( ), ), TautulliSensorEntityDescription( - icon="mdi:plex", key="stream_count_transcode", translation_key="stream_count_transcode", entity_category=EntityCategory.DIAGNOSTIC, @@ -128,21 +118,18 @@ SENSOR_TYPES: tuple[TautulliSensorEntityDescription, ...] = ( value_fn=lambda home_stats, activity, _: cast(int, activity.wan_bandwidth), ), TautulliSensorEntityDescription( - icon="mdi:movie-open", key="top_movies", translation_key="top_movies", entity_registry_enabled_default=False, value_fn=get_top_stats, ), TautulliSensorEntityDescription( - icon="mdi:television", key="top_tv", translation_key="top_tv", entity_registry_enabled_default=False, value_fn=get_top_stats, ), TautulliSensorEntityDescription( - icon="mdi:walk", key=ATTR_TOP_USER, translation_key="top_user", entity_registry_enabled_default=False, @@ -151,23 +138,15 @@ SENSOR_TYPES: tuple[TautulliSensorEntityDescription, ...] = ( ) -@dataclass(frozen=True) -class TautulliSessionSensorEntityMixin: - """Mixin for Tautulli session sensor.""" +@dataclass(frozen=True, kw_only=True) +class TautulliSessionSensorEntityDescription(SensorEntityDescription): + """Describes a Tautulli session sensor.""" value_fn: Callable[[PyTautulliApiSession], StateType] -@dataclass(frozen=True) -class TautulliSessionSensorEntityDescription( - SensorEntityDescription, TautulliSessionSensorEntityMixin -): - """Describes a Tautulli session sensor.""" - - SESSION_SENSOR_TYPES: tuple[TautulliSessionSensorEntityDescription, ...] = ( TautulliSessionSensorEntityDescription( - icon="mdi:plex", key="state", translation_key="state", value_fn=lambda session: cast(str, session.state), @@ -179,7 +158,6 @@ SESSION_SENSOR_TYPES: tuple[TautulliSessionSensorEntityDescription, ...] = ( value_fn=lambda session: cast(str, session.full_title), ), TautulliSessionSensorEntityDescription( - icon="mdi:progress-clock", key="progress", translation_key="progress", native_unit_of_measurement=PERCENTAGE, @@ -194,7 +172,6 @@ SESSION_SENSOR_TYPES: tuple[TautulliSessionSensorEntityDescription, ...] = ( value_fn=lambda session: cast(str, session.stream_video_resolution), ), TautulliSessionSensorEntityDescription( - icon="mdi:plex", key="transcode_decision", translation_key="transcode_decision", entity_category=EntityCategory.DIAGNOSTIC, diff --git a/homeassistant/components/tcp/binary_sensor.py b/homeassistant/components/tcp/binary_sensor.py index ff07d163040..3e432778910 100644 --- a/homeassistant/components/tcp/binary_sensor.py +++ b/homeassistant/components/tcp/binary_sensor.py @@ -1,4 +1,5 @@ """Provides a binary sensor which gets its values from a TCP socket.""" + from __future__ import annotations from typing import Final diff --git a/homeassistant/components/tcp/common.py b/homeassistant/components/tcp/common.py index 31f230d3b23..46520134bf6 100644 --- a/homeassistant/components/tcp/common.py +++ b/homeassistant/components/tcp/common.py @@ -1,4 +1,5 @@ """Common code for TCP component.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/tcp/const.py b/homeassistant/components/tcp/const.py index 3a42736c753..98cdfa002fd 100644 --- a/homeassistant/components/tcp/const.py +++ b/homeassistant/components/tcp/const.py @@ -1,4 +1,5 @@ """Constants for TCP platform.""" + from __future__ import annotations from typing import Final diff --git a/homeassistant/components/tcp/model.py b/homeassistant/components/tcp/model.py index 814f6fdb126..8cbe10e0b0c 100644 --- a/homeassistant/components/tcp/model.py +++ b/homeassistant/components/tcp/model.py @@ -1,4 +1,5 @@ """Models for TCP platform.""" + from __future__ import annotations from typing import TypedDict diff --git a/homeassistant/components/tcp/sensor.py b/homeassistant/components/tcp/sensor.py index 609a4cc072a..6c1e6563c50 100644 --- a/homeassistant/components/tcp/sensor.py +++ b/homeassistant/components/tcp/sensor.py @@ -1,4 +1,5 @@ """Support for TCP socket based sensors.""" + from __future__ import annotations from typing import Final diff --git a/homeassistant/components/technove/__init__.py b/homeassistant/components/technove/__init__.py index 999cb2e2f34..d2d5b4255ba 100644 --- a/homeassistant/components/technove/__init__.py +++ b/homeassistant/components/technove/__init__.py @@ -1,4 +1,5 @@ """The TechnoVE integration.""" + from __future__ import annotations from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/technove/binary_sensor.py b/homeassistant/components/technove/binary_sensor.py index 09bf08baad6..e9570397dc1 100644 --- a/homeassistant/components/technove/binary_sensor.py +++ b/homeassistant/components/technove/binary_sensor.py @@ -1,4 +1,5 @@ """Support for TechnoVE binary sensor.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/technove/config_flow.py b/homeassistant/components/technove/config_flow.py index d85fd0ad152..0e4f026ba5c 100644 --- a/homeassistant/components/technove/config_flow.py +++ b/homeassistant/components/technove/config_flow.py @@ -6,9 +6,8 @@ from technove import Station as TechnoVEStation, TechnoVE, TechnoVEConnectionErr import voluptuous as vol from homeassistant.components import onboarding, zeroconf -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_MAC -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN @@ -23,7 +22,7 @@ class TechnoVEConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initiated by the user.""" errors = {} if user_input is not None: @@ -51,7 +50,7 @@ class TechnoVEConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_zeroconf( self, discovery_info: zeroconf.ZeroconfServiceInfo - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle zeroconf discovery.""" # Abort quick if the device with provided mac is already configured if mac := discovery_info.properties.get(CONF_MAC): @@ -78,7 +77,7 @@ class TechnoVEConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_zeroconf_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initiated by zeroconf.""" if user_input is not None or not onboarding.async_is_onboarded(self.hass): return self.async_create_entry( diff --git a/homeassistant/components/technove/const.py b/homeassistant/components/technove/const.py index 6dd7d567353..1c2bbe8aa83 100644 --- a/homeassistant/components/technove/const.py +++ b/homeassistant/components/technove/const.py @@ -1,4 +1,5 @@ """Constants for the TechnoVE integration.""" + from datetime import timedelta import logging diff --git a/homeassistant/components/technove/coordinator.py b/homeassistant/components/technove/coordinator.py index 66ec7d979f3..c89219e698f 100644 --- a/homeassistant/components/technove/coordinator.py +++ b/homeassistant/components/technove/coordinator.py @@ -1,4 +1,5 @@ """DataUpdateCoordinator for TechnoVE.""" + from __future__ import annotations from technove import Station as TechnoVEStation, TechnoVE, TechnoVEError diff --git a/homeassistant/components/technove/entity.py b/homeassistant/components/technove/entity.py index 964f2941301..8a01eccfe10 100644 --- a/homeassistant/components/technove/entity.py +++ b/homeassistant/components/technove/entity.py @@ -1,4 +1,5 @@ """Entity for TechnoVE.""" + from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/technove/helpers.py b/homeassistant/components/technove/helpers.py index 43bd8f04794..4d8bda38a25 100644 --- a/homeassistant/components/technove/helpers.py +++ b/homeassistant/components/technove/helpers.py @@ -1,4 +1,5 @@ """Helpers for TechnoVE.""" + from __future__ import annotations from collections.abc import Callable, Coroutine diff --git a/homeassistant/components/technove/icons.json b/homeassistant/components/technove/icons.json new file mode 100644 index 00000000000..ff47d3c32bc --- /dev/null +++ b/homeassistant/components/technove/icons.json @@ -0,0 +1,9 @@ +{ + "entity": { + "sensor": { + "ssid": { + "default": "mdi:wifi" + } + } + } +} diff --git a/homeassistant/components/technove/sensor.py b/homeassistant/components/technove/sensor.py index e4d3822ee1b..6f8e63ffbc6 100644 --- a/homeassistant/components/technove/sensor.py +++ b/homeassistant/components/technove/sensor.py @@ -1,4 +1,5 @@ """Platform for sensor integration.""" + from __future__ import annotations from collections.abc import Callable @@ -104,7 +105,6 @@ SENSORS: tuple[TechnoVESensorEntityDescription, ...] = ( TechnoVESensorEntityDescription( key="ssid", translation_key="ssid", - icon="mdi:wifi", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, value_fn=lambda station: station.info.network_ssid, diff --git a/homeassistant/components/technove/switch.py b/homeassistant/components/technove/switch.py index 3ee7f1c302d..72c95f676d9 100644 --- a/homeassistant/components/technove/switch.py +++ b/homeassistant/components/technove/switch.py @@ -1,4 +1,5 @@ """Support for TechnoVE switches.""" + from __future__ import annotations from collections.abc import Awaitable, Callable diff --git a/homeassistant/components/ted5000/sensor.py b/homeassistant/components/ted5000/sensor.py index 5c3e1d30c4a..99d8991a02e 100644 --- a/homeassistant/components/ted5000/sensor.py +++ b/homeassistant/components/ted5000/sensor.py @@ -1,4 +1,5 @@ """Support gathering ted5000 information.""" + from __future__ import annotations from contextlib import suppress @@ -77,12 +78,11 @@ def setup_platform( # Get MUT information to create the sensors. gateway.update() - entities = [] - for mtu in gateway.data: - for description in SENSORS: - entities.append(Ted5000Sensor(gateway, name, mtu, description)) - - add_entities(entities) + add_entities( + Ted5000Sensor(gateway, name, mtu, description) + for mtu in gateway.data + for description in SENSORS + ) class Ted5000Sensor(SensorEntity): diff --git a/homeassistant/components/tedee/__init__.py b/homeassistant/components/tedee/__init__.py index eeb0f8e0d5a..9468008ae8a 100644 --- a/homeassistant/components/tedee/__init__.py +++ b/homeassistant/components/tedee/__init__.py @@ -1,4 +1,5 @@ """Init the tedee component.""" + import logging from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/tedee/config_flow.py b/homeassistant/components/tedee/config_flow.py index 7c8c7b4c3ab..8465b332539 100644 --- a/homeassistant/components/tedee/config_flow.py +++ b/homeassistant/components/tedee/config_flow.py @@ -1,6 +1,7 @@ """Config flow for Tedee integration.""" from collections.abc import Mapping +import logging from typing import Any from pytedee_async import ( @@ -12,13 +13,14 @@ from pytedee_async import ( ) import voluptuous as vol -from homeassistant.config_entries import ConfigEntry, ConfigFlow +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import CONF_LOCAL_ACCESS_TOKEN, DOMAIN, NAME +_LOGGER = logging.getLogger(__name__) + class TedeeConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Tedee.""" @@ -27,7 +29,7 @@ class TedeeConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors: dict[str, str] = {} @@ -48,7 +50,8 @@ class TedeeConfigFlow(ConfigFlow, domain=DOMAIN): errors[CONF_LOCAL_ACCESS_TOKEN] = "invalid_api_key" except TedeeClientException: errors[CONF_HOST] = "invalid_host" - except TedeeDataUpdateException: + except TedeeDataUpdateException as exc: + _LOGGER.error("Error during local bridge discovery: %s", exc) errors["base"] = "cannot_connect" else: if self.reauth_entry: @@ -79,7 +82,9 @@ class TedeeConfigFlow(ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Perform reauth upon an API authentication error.""" self.reauth_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] @@ -88,7 +93,7 @@ class TedeeConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Dialog that informs the user that reauth is required.""" assert self.reauth_entry diff --git a/homeassistant/components/tedee/const.py b/homeassistant/components/tedee/const.py index bac5bfaec44..b5898871620 100644 --- a/homeassistant/components/tedee/const.py +++ b/homeassistant/components/tedee/const.py @@ -1,4 +1,5 @@ """Constants for the Tedee integration.""" + from datetime import timedelta DOMAIN = "tedee" diff --git a/homeassistant/components/tedee/coordinator.py b/homeassistant/components/tedee/coordinator.py index c846f2a8d9a..f3043b1d78d 100644 --- a/homeassistant/components/tedee/coordinator.py +++ b/homeassistant/components/tedee/coordinator.py @@ -1,4 +1,5 @@ """Coordinator for Tedee locks.""" + from collections.abc import Awaitable, Callable from datetime import timedelta import logging diff --git a/homeassistant/components/tedee/diagnostics.py b/homeassistant/components/tedee/diagnostics.py index d17c4c335bc..b4fb1d279fa 100644 --- a/homeassistant/components/tedee/diagnostics.py +++ b/homeassistant/components/tedee/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for tedee.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/tedee/icons.json b/homeassistant/components/tedee/icons.json new file mode 100644 index 00000000000..3f98462b22f --- /dev/null +++ b/homeassistant/components/tedee/icons.json @@ -0,0 +1,9 @@ +{ + "entity": { + "sensor": { + "pullspring_duration": { + "default": "mdi:timer-lock-open" + } + } + } +} diff --git a/homeassistant/components/tedee/lock.py b/homeassistant/components/tedee/lock.py index 1025942d787..a720652bcbc 100644 --- a/homeassistant/components/tedee/lock.py +++ b/homeassistant/components/tedee/lock.py @@ -1,4 +1,5 @@ """Tedee lock entities.""" + from typing import Any from pytedee_async import TedeeClientException, TedeeLock, TedeeLockState diff --git a/homeassistant/components/tedee/sensor.py b/homeassistant/components/tedee/sensor.py index 225686f6b18..cd01e9d04be 100644 --- a/homeassistant/components/tedee/sensor.py +++ b/homeassistant/components/tedee/sensor.py @@ -42,7 +42,6 @@ ENTITIES: tuple[TedeeSensorEntityDescription, ...] = ( device_class=SensorDeviceClass.DURATION, native_unit_of_measurement=UnitOfTime.SECONDS, state_class=SensorStateClass.MEASUREMENT, - icon="mdi:timer-lock-open", value_fn=lambda lock: lock.duration_pullspring, entity_category=EntityCategory.DIAGNOSTIC, ), diff --git a/homeassistant/components/telegram/icons.json b/homeassistant/components/telegram/icons.json new file mode 100644 index 00000000000..a03163179cb --- /dev/null +++ b/homeassistant/components/telegram/icons.json @@ -0,0 +1,5 @@ +{ + "services": { + "reload": "mdi:reload" + } +} diff --git a/homeassistant/components/telegram/notify.py b/homeassistant/components/telegram/notify.py index 05b966e66d7..e543715d37c 100644 --- a/homeassistant/components/telegram/notify.py +++ b/homeassistant/components/telegram/notify.py @@ -1,4 +1,5 @@ """Telegram platform for notify component.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py index 2ba7752a85f..6338996256b 100644 --- a/homeassistant/components/telegram_bot/__init__.py +++ b/homeassistant/components/telegram_bot/__init__.py @@ -1,15 +1,14 @@ """Support to send and receive Telegram messages.""" + from __future__ import annotations -from functools import partial -import importlib +import asyncio import io from ipaddress import ip_network import logging from typing import Any -import requests -from requests.auth import HTTPBasicAuth, HTTPDigestAuth +import httpx from telegram import ( Bot, CallbackQuery, @@ -21,10 +20,10 @@ from telegram import ( Update, User, ) +from telegram.constants import ParseMode from telegram.error import TelegramError -from telegram.ext import CallbackContext, Filters -from telegram.parsemode import ParseMode -from telegram.utils.request import Request +from telegram.ext import CallbackContext, filters +from telegram.request import HTTPXRequest import voluptuous as vol from homeassistant.const import ( @@ -39,8 +38,9 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.exceptions import TemplateError -import homeassistant.helpers.config_validation as cv +from homeassistant.helpers import config_validation as cv, issue_registry as ir from homeassistant.helpers.typing import ConfigType +from homeassistant.loader import async_get_loaded_integration _LOGGER = logging.getLogger(__name__) @@ -283,7 +283,7 @@ SERVICE_MAP = { } -def load_data( +async def load_data( hass, url=None, filepath=None, @@ -297,35 +297,48 @@ def load_data( try: if url is not None: # Load data from URL - params = {"timeout": 15} + params = {} + headers = {} if authentication == HTTP_BEARER_AUTHENTICATION and password is not None: - params["headers"] = {"Authorization": f"Bearer {password}"} + headers = {"Authorization": f"Bearer {password}"} elif username is not None and password is not None: if authentication == HTTP_DIGEST_AUTHENTICATION: - params["auth"] = HTTPDigestAuth(username, password) + params["auth"] = httpx.DigestAuth(username, password) else: - params["auth"] = HTTPBasicAuth(username, password) + params["auth"] = httpx.BasicAuth(username, password) if verify_ssl is not None: params["verify"] = verify_ssl + retry_num = 0 - while retry_num < num_retries: - req = requests.get(url, **params) - if not req.ok: - _LOGGER.warning( - "Status code %s (retry #%s) loading %s", - req.status_code, - retry_num + 1, - url, - ) - else: - data = io.BytesIO(req.content) - if data.read(): - data.seek(0) - data.name = url - return data - _LOGGER.warning("Empty data (retry #%s) in %s)", retry_num + 1, url) - retry_num += 1 - _LOGGER.warning("Can't load data in %s after %s retries", url, retry_num) + async with httpx.AsyncClient( + timeout=15, headers=headers, **params + ) as client: + while retry_num < num_retries: + req = await client.get(url) + if req.status_code != 200: + _LOGGER.warning( + "Status code %s (retry #%s) loading %s", + req.status_code, + retry_num + 1, + url, + ) + else: + data = io.BytesIO(req.content) + if data.read(): + data.seek(0) + data.name = url + return data + _LOGGER.warning( + "Empty data (retry #%s) in %s)", retry_num + 1, url + ) + retry_num += 1 + if retry_num < num_retries: + await asyncio.sleep( + 1 + ) # Add a sleep to allow other async operations to proceed + _LOGGER.warning( + "Can't load data in %s after %s retries", url, retry_num + ) elif filepath is not None: if hass.config.is_allowed_path(filepath): return open(filepath, "rb") @@ -342,15 +355,21 @@ def load_data( async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Telegram bot component.""" - if not config[DOMAIN]: + domain_config: list[dict[str, Any]] = config[DOMAIN] + + if not domain_config: return False - for p_config in config[DOMAIN]: - # Each platform config gets its own bot - bot = initialize_bot(p_config) - p_type = p_config.get(CONF_PLATFORM) + platforms = await async_get_loaded_integration(hass, DOMAIN).async_get_platforms( + {p_config[CONF_PLATFORM] for p_config in domain_config} + ) - platform = importlib.import_module(f".{p_config[CONF_PLATFORM]}", __name__) + for p_config in domain_config: + # Each platform config gets its own bot + bot = initialize_bot(hass, p_config) + p_type: str = p_config[CONF_PLATFORM] + + platform = platforms[p_type] _LOGGER.info("Setting up %s.%s", DOMAIN, p_type) try: @@ -406,9 +425,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: _LOGGER.debug("New telegram message %s: %s", msgtype, kwargs) if msgtype == SERVICE_SEND_MESSAGE: - await hass.async_add_executor_job( - partial(notify_service.send_message, **kwargs) - ) + await notify_service.send_message(**kwargs) elif msgtype in [ SERVICE_SEND_PHOTO, SERVICE_SEND_ANIMATION, @@ -416,33 +433,19 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: SERVICE_SEND_VOICE, SERVICE_SEND_DOCUMENT, ]: - await hass.async_add_executor_job( - partial(notify_service.send_file, msgtype, **kwargs) - ) + await notify_service.send_file(msgtype, **kwargs) elif msgtype == SERVICE_SEND_STICKER: - await hass.async_add_executor_job( - partial(notify_service.send_sticker, **kwargs) - ) + await notify_service.send_sticker(**kwargs) elif msgtype == SERVICE_SEND_LOCATION: - await hass.async_add_executor_job( - partial(notify_service.send_location, **kwargs) - ) + await notify_service.send_location(**kwargs) elif msgtype == SERVICE_SEND_POLL: - await hass.async_add_executor_job( - partial(notify_service.send_poll, **kwargs) - ) + await notify_service.send_poll(**kwargs) elif msgtype == SERVICE_ANSWER_CALLBACK_QUERY: - await hass.async_add_executor_job( - partial(notify_service.answer_callback_query, **kwargs) - ) + await notify_service.answer_callback_query(**kwargs) elif msgtype == SERVICE_DELETE_MESSAGE: - await hass.async_add_executor_job( - partial(notify_service.delete_message, **kwargs) - ) + await notify_service.delete_message(**kwargs) else: - await hass.async_add_executor_job( - partial(notify_service.edit_message, msgtype, **kwargs) - ) + await notify_service.edit_message(msgtype, **kwargs) # Register notification services for service_notif, schema in SERVICE_MAP.items(): @@ -453,18 +456,59 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True -def initialize_bot(p_config): +def initialize_bot(hass: HomeAssistant, p_config: dict) -> Bot: """Initialize telegram bot with proxy support.""" - api_key = p_config.get(CONF_API_KEY) - proxy_url = p_config.get(CONF_PROXY_URL) - proxy_params = p_config.get(CONF_PROXY_PARAMS) + api_key: str = p_config[CONF_API_KEY] + proxy_url: str | None = p_config.get(CONF_PROXY_URL) + proxy_params: dict | None = p_config.get(CONF_PROXY_PARAMS) if proxy_url is not None: - request = Request( - con_pool_size=8, proxy_url=proxy_url, urllib3_proxy_kwargs=proxy_params - ) + auth = None + if proxy_params is None: + # CONF_PROXY_PARAMS has been kept for backwards compatibility. + proxy_params = {} + elif "username" in proxy_params and "password" in proxy_params: + # Auth can actually be stuffed into the URL, but the docs have previously + # indicated to put them here. + auth = proxy_params.pop("username"), proxy_params.pop("password") + ir.async_create_issue( + hass, + DOMAIN, + "proxy_params_auth_deprecation", + breaks_in_ha_version="2024.10.0", + is_persistent=False, + is_fixable=False, + severity=ir.IssueSeverity.WARNING, + translation_placeholders={ + "proxy_params": CONF_PROXY_PARAMS, + "proxy_url": CONF_PROXY_URL, + "telegram_bot": "Telegram bot", + }, + translation_key="proxy_params_auth_deprecation", + learn_more_url="https://github.com/home-assistant/core/pull/112778", + ) + else: + ir.async_create_issue( + hass, + DOMAIN, + "proxy_params_deprecation", + breaks_in_ha_version="2024.10.0", + is_persistent=False, + is_fixable=False, + severity=ir.IssueSeverity.WARNING, + translation_placeholders={ + "proxy_params": CONF_PROXY_PARAMS, + "proxy_url": CONF_PROXY_URL, + "httpx": "httpx", + "telegram_bot": "Telegram bot", + }, + translation_key="proxy_params_deprecation", + learn_more_url="https://github.com/home-assistant/core/pull/112778", + ) + proxy = httpx.Proxy(proxy_url, auth=auth, **proxy_params) + request = HTTPXRequest(connection_pool_size=8, proxy=proxy) else: - request = Request(con_pool_size=8) + request = HTTPXRequest(connection_pool_size=8) return Bot(token=api_key, request=request) @@ -616,10 +660,12 @@ class TelegramNotificationService: ) return params - def _send_msg(self, func_send, msg_error, message_tag, *args_msg, **kwargs_msg): + async def _send_msg( + self, func_send, msg_error, message_tag, *args_msg, **kwargs_msg + ): """Send one message.""" try: - out = func_send(*args_msg, **kwargs_msg) + out = await func_send(*args_msg, **kwargs_msg) if not isinstance(out, bool) and hasattr(out, ATTR_MESSAGEID): chat_id = out.chat_id message_id = out[ATTR_MESSAGEID] @@ -636,7 +682,7 @@ class TelegramNotificationService: } if message_tag is not None: event_data[ATTR_MESSAGE_TAG] = message_tag - self.hass.bus.fire(EVENT_TELEGRAM_SENT, event_data) + self.hass.bus.async_fire(EVENT_TELEGRAM_SENT, event_data) elif not isinstance(out, bool): _LOGGER.warning( "Update last message: out_type:%s, out=%s", type(out), out @@ -647,14 +693,14 @@ class TelegramNotificationService: "%s: %s. Args: %s, kwargs: %s", msg_error, exc, args_msg, kwargs_msg ) - def send_message(self, message="", target=None, **kwargs): + async def send_message(self, message="", target=None, **kwargs): """Send a message to one or multiple pre-allowed chat IDs.""" title = kwargs.get(ATTR_TITLE) text = f"{title}\n{message}" if title else message params = self._get_msg_kwargs(kwargs) for chat_id in self._get_target_chat_ids(target): _LOGGER.debug("Send message in chat ID %s with params: %s", chat_id, params) - self._send_msg( + await self._send_msg( self.bot.send_message, "Error sending message", params[ATTR_MESSAGE_TAG], @@ -665,15 +711,15 @@ class TelegramNotificationService: disable_notification=params[ATTR_DISABLE_NOTIF], reply_to_message_id=params[ATTR_REPLY_TO_MSGID], reply_markup=params[ATTR_REPLYMARKUP], - timeout=params[ATTR_TIMEOUT], + read_timeout=params[ATTR_TIMEOUT], ) - def delete_message(self, chat_id=None, **kwargs): + async def delete_message(self, chat_id=None, **kwargs): """Delete a previously sent message.""" chat_id = self._get_target_chat_ids(chat_id)[0] message_id, _ = self._get_msg_ids(kwargs, chat_id) _LOGGER.debug("Delete message %s in chat ID %s", message_id, chat_id) - deleted = self._send_msg( + deleted = await self._send_msg( self.bot.delete_message, "Error deleting message", None, chat_id, message_id ) # reduce message_id anyway: @@ -682,7 +728,7 @@ class TelegramNotificationService: self._last_message_id[chat_id] -= 1 return deleted - def edit_message(self, type_edit, chat_id=None, **kwargs): + async def edit_message(self, type_edit, chat_id=None, **kwargs): """Edit a previously sent message.""" chat_id = self._get_target_chat_ids(chat_id)[0] message_id, inline_message_id = self._get_msg_ids(kwargs, chat_id) @@ -698,7 +744,7 @@ class TelegramNotificationService: title = kwargs.get(ATTR_TITLE) text = f"{title}\n{message}" if title else message _LOGGER.debug("Editing message with ID %s", message_id or inline_message_id) - return self._send_msg( + return await self._send_msg( self.bot.edit_message_text, "Error editing text message", params[ATTR_MESSAGE_TAG], @@ -709,10 +755,10 @@ class TelegramNotificationService: parse_mode=params[ATTR_PARSER], disable_web_page_preview=params[ATTR_DISABLE_WEB_PREV], reply_markup=params[ATTR_REPLYMARKUP], - timeout=params[ATTR_TIMEOUT], + read_timeout=params[ATTR_TIMEOUT], ) if type_edit == SERVICE_EDIT_CAPTION: - return self._send_msg( + return await self._send_msg( self.bot.edit_message_caption, "Error editing message attributes", params[ATTR_MESSAGE_TAG], @@ -721,11 +767,11 @@ class TelegramNotificationService: inline_message_id=inline_message_id, caption=kwargs.get(ATTR_CAPTION), reply_markup=params[ATTR_REPLYMARKUP], - timeout=params[ATTR_TIMEOUT], + read_timeout=params[ATTR_TIMEOUT], parse_mode=params[ATTR_PARSER], ) - return self._send_msg( + return await self._send_msg( self.bot.edit_message_reply_markup, "Error editing message attributes", params[ATTR_MESSAGE_TAG], @@ -733,10 +779,10 @@ class TelegramNotificationService: message_id=message_id, inline_message_id=inline_message_id, reply_markup=params[ATTR_REPLYMARKUP], - timeout=params[ATTR_TIMEOUT], + read_timeout=params[ATTR_TIMEOUT], ) - def answer_callback_query( + async def answer_callback_query( self, message, callback_query_id, show_alert=False, **kwargs ): """Answer a callback originated with a press in an inline keyboard.""" @@ -747,20 +793,20 @@ class TelegramNotificationService: message, show_alert, ) - self._send_msg( + await self._send_msg( self.bot.answer_callback_query, "Error sending answer callback query", params[ATTR_MESSAGE_TAG], callback_query_id, text=message, show_alert=show_alert, - timeout=params[ATTR_TIMEOUT], + read_timeout=params[ATTR_TIMEOUT], ) - def send_file(self, file_type=SERVICE_SEND_PHOTO, target=None, **kwargs): + async def send_file(self, file_type=SERVICE_SEND_PHOTO, target=None, **kwargs): """Send a photo, sticker, video, or document.""" params = self._get_msg_kwargs(kwargs) - file_content = load_data( + file_content = await load_data( self.hass, url=kwargs.get(ATTR_URL), filepath=kwargs.get(ATTR_FILE), @@ -775,7 +821,7 @@ class TelegramNotificationService: _LOGGER.debug("Sending file to chat ID %s", chat_id) if file_type == SERVICE_SEND_PHOTO: - self._send_msg( + await self._send_msg( self.bot.send_photo, "Error sending photo", params[ATTR_MESSAGE_TAG], @@ -785,12 +831,12 @@ class TelegramNotificationService: disable_notification=params[ATTR_DISABLE_NOTIF], reply_to_message_id=params[ATTR_REPLY_TO_MSGID], reply_markup=params[ATTR_REPLYMARKUP], - timeout=params[ATTR_TIMEOUT], + read_timeout=params[ATTR_TIMEOUT], parse_mode=params[ATTR_PARSER], ) elif file_type == SERVICE_SEND_STICKER: - self._send_msg( + await self._send_msg( self.bot.send_sticker, "Error sending sticker", params[ATTR_MESSAGE_TAG], @@ -799,11 +845,11 @@ class TelegramNotificationService: disable_notification=params[ATTR_DISABLE_NOTIF], reply_to_message_id=params[ATTR_REPLY_TO_MSGID], reply_markup=params[ATTR_REPLYMARKUP], - timeout=params[ATTR_TIMEOUT], + read_timeout=params[ATTR_TIMEOUT], ) elif file_type == SERVICE_SEND_VIDEO: - self._send_msg( + await self._send_msg( self.bot.send_video, "Error sending video", params[ATTR_MESSAGE_TAG], @@ -813,11 +859,11 @@ class TelegramNotificationService: disable_notification=params[ATTR_DISABLE_NOTIF], reply_to_message_id=params[ATTR_REPLY_TO_MSGID], reply_markup=params[ATTR_REPLYMARKUP], - timeout=params[ATTR_TIMEOUT], + read_timeout=params[ATTR_TIMEOUT], parse_mode=params[ATTR_PARSER], ) elif file_type == SERVICE_SEND_DOCUMENT: - self._send_msg( + await self._send_msg( self.bot.send_document, "Error sending document", params[ATTR_MESSAGE_TAG], @@ -827,11 +873,11 @@ class TelegramNotificationService: disable_notification=params[ATTR_DISABLE_NOTIF], reply_to_message_id=params[ATTR_REPLY_TO_MSGID], reply_markup=params[ATTR_REPLYMARKUP], - timeout=params[ATTR_TIMEOUT], + read_timeout=params[ATTR_TIMEOUT], parse_mode=params[ATTR_PARSER], ) elif file_type == SERVICE_SEND_VOICE: - self._send_msg( + await self._send_msg( self.bot.send_voice, "Error sending voice", params[ATTR_MESSAGE_TAG], @@ -841,10 +887,10 @@ class TelegramNotificationService: disable_notification=params[ATTR_DISABLE_NOTIF], reply_to_message_id=params[ATTR_REPLY_TO_MSGID], reply_markup=params[ATTR_REPLYMARKUP], - timeout=params[ATTR_TIMEOUT], + read_timeout=params[ATTR_TIMEOUT], ) elif file_type == SERVICE_SEND_ANIMATION: - self._send_msg( + await self._send_msg( self.bot.send_animation, "Error sending animation", params[ATTR_MESSAGE_TAG], @@ -854,7 +900,7 @@ class TelegramNotificationService: disable_notification=params[ATTR_DISABLE_NOTIF], reply_to_message_id=params[ATTR_REPLY_TO_MSGID], reply_markup=params[ATTR_REPLYMARKUP], - timeout=params[ATTR_TIMEOUT], + read_timeout=params[ATTR_TIMEOUT], parse_mode=params[ATTR_PARSER], ) @@ -862,13 +908,13 @@ class TelegramNotificationService: else: _LOGGER.error("Can't send file with kwargs: %s", kwargs) - def send_sticker(self, target=None, **kwargs): + async def send_sticker(self, target=None, **kwargs): """Send a sticker from a telegram sticker pack.""" params = self._get_msg_kwargs(kwargs) stickerid = kwargs.get(ATTR_STICKER_ID) if stickerid: for chat_id in self._get_target_chat_ids(target): - self._send_msg( + await self._send_msg( self.bot.send_sticker, "Error sending sticker", params[ATTR_MESSAGE_TAG], @@ -877,12 +923,12 @@ class TelegramNotificationService: disable_notification=params[ATTR_DISABLE_NOTIF], reply_to_message_id=params[ATTR_REPLY_TO_MSGID], reply_markup=params[ATTR_REPLYMARKUP], - timeout=params[ATTR_TIMEOUT], + read_timeout=params[ATTR_TIMEOUT], ) else: - self.send_file(SERVICE_SEND_STICKER, target, **kwargs) + await self.send_file(SERVICE_SEND_STICKER, target, **kwargs) - def send_location(self, latitude, longitude, target=None, **kwargs): + async def send_location(self, latitude, longitude, target=None, **kwargs): """Send a location.""" latitude = float(latitude) longitude = float(longitude) @@ -891,7 +937,7 @@ class TelegramNotificationService: _LOGGER.debug( "Send location %s/%s to chat ID %s", latitude, longitude, chat_id ) - self._send_msg( + await self._send_msg( self.bot.send_location, "Error sending location", params[ATTR_MESSAGE_TAG], @@ -900,10 +946,10 @@ class TelegramNotificationService: longitude=longitude, disable_notification=params[ATTR_DISABLE_NOTIF], reply_to_message_id=params[ATTR_REPLY_TO_MSGID], - timeout=params[ATTR_TIMEOUT], + read_timeout=params[ATTR_TIMEOUT], ) - def send_poll( + async def send_poll( self, question, options, @@ -917,7 +963,7 @@ class TelegramNotificationService: openperiod = kwargs.get(ATTR_OPEN_PERIOD) for chat_id in self._get_target_chat_ids(target): _LOGGER.debug("Send poll '%s' to chat ID %s", question, chat_id) - self._send_msg( + await self._send_msg( self.bot.send_poll, "Error sending poll", params[ATTR_MESSAGE_TAG], @@ -929,14 +975,14 @@ class TelegramNotificationService: open_period=openperiod, disable_notification=params[ATTR_DISABLE_NOTIF], reply_to_message_id=params[ATTR_REPLY_TO_MSGID], - timeout=params[ATTR_TIMEOUT], + read_timeout=params[ATTR_TIMEOUT], ) - def leave_chat(self, chat_id=None): + async def leave_chat(self, chat_id=None): """Remove bot from chat.""" chat_id = self._get_target_chat_ids(chat_id)[0] _LOGGER.debug("Leave from chat ID %s", chat_id) - leaved = self._send_msg( + leaved = await self._send_msg( self.bot.leave_chat, "Error leaving chat", None, chat_id ) return leaved @@ -950,8 +996,8 @@ class BaseTelegramBotEntity: self.allowed_chat_ids = config[CONF_ALLOWED_CHAT_IDS] self.hass = hass - def handle_update(self, update: Update, context: CallbackContext) -> bool: - """Handle updates from bot dispatcher set up by the respective platform.""" + async def handle_update(self, update: Update, context: CallbackContext) -> bool: + """Handle updates from bot application set up by the respective platform.""" _LOGGER.debug("Handling update %s", update) if not self.authorize_update(update): return False @@ -972,12 +1018,12 @@ class BaseTelegramBotEntity: return True _LOGGER.debug("Firing event %s: %s", event_type, event_data) - self.hass.bus.fire(event_type, event_data) + self.hass.bus.async_fire(event_type, event_data) return True @staticmethod - def _get_command_event_data(command_text: str) -> dict[str, str | list]: - if not command_text.startswith("/"): + def _get_command_event_data(command_text: str | None) -> dict[str, str | list]: + if not command_text or not command_text.startswith("/"): return {} command_parts = command_text.split() command = command_parts[0] @@ -990,7 +1036,7 @@ class BaseTelegramBotEntity: ATTR_CHAT_ID: message.chat.id, ATTR_DATE: message.date, } - if Filters.command.filter(message): + if filters.COMMAND.filter(message): # This is a command message - set event type to command and split data into command and args event_type = EVENT_TELEGRAM_COMMAND event_data.update(self._get_command_event_data(message.text)) diff --git a/homeassistant/components/telegram_bot/icons.json b/homeassistant/components/telegram_bot/icons.json new file mode 100644 index 00000000000..f410d387435 --- /dev/null +++ b/homeassistant/components/telegram_bot/icons.json @@ -0,0 +1,18 @@ +{ + "services": { + "send_message": "mdi:send", + "send_photo": "mdi:camera", + "send_sticker": "mdi:sticker", + "send_animation": "mdi:animation", + "send_video": "mdi:video", + "send_voice": "mdi:microphone", + "send_document": "mdi:file-document", + "send_location": "mdi:map-marker", + "send_poll": "mdi:poll", + "edit_message": "mdi:pencil", + "edit_caption": "mdi:pencil", + "edit_replymarkup": "mdi:pencil", + "answer_callback_query": "mdi:check", + "delete_message": "mdi:delete" + } +} diff --git a/homeassistant/components/telegram_bot/manifest.json b/homeassistant/components/telegram_bot/manifest.json index a4964638242..c176e6c2cdf 100644 --- a/homeassistant/components/telegram_bot/manifest.json +++ b/homeassistant/components/telegram_bot/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/telegram_bot", "iot_class": "cloud_push", "loggers": ["telegram"], - "requirements": ["python-telegram-bot==13.1", "PySocks==1.7.1"] + "requirements": ["python-telegram-bot[socks]==21.0.1"] } diff --git a/homeassistant/components/telegram_bot/polling.py b/homeassistant/components/telegram_bot/polling.py index 69bb4dc4963..45d2ee65b45 100644 --- a/homeassistant/components/telegram_bot/polling.py +++ b/homeassistant/components/telegram_bot/polling.py @@ -1,9 +1,10 @@ """Support for Telegram bot using polling.""" + import logging from telegram import Update from telegram.error import NetworkError, RetryAfter, TelegramError, TimedOut -from telegram.ext import CallbackContext, TypeHandler, Updater +from telegram.ext import ApplicationBuilder, CallbackContext, TypeHandler from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP @@ -22,7 +23,7 @@ async def async_setup_platform(hass, bot, config): return True -def process_error(update: Update, context: CallbackContext) -> None: +async def process_error(update: Update, context: CallbackContext) -> None: """Telegram bot error handler.""" try: if context.error: @@ -35,26 +36,29 @@ def process_error(update: Update, context: CallbackContext) -> None: class PollBot(BaseTelegramBotEntity): - """Controls the Updater object that holds the bot and a dispatcher. + """Controls the Application object that holds the bot and an updater. - The dispatcher is set up by the super class to pass telegram updates to `self.handle_update` + The application is set up to pass telegram updates to `self.handle_update` """ def __init__(self, hass, bot, config): - """Create Updater and Dispatcher before calling super().""" - self.bot = bot - self.updater = Updater(bot=bot, workers=4) - self.dispatcher = self.updater.dispatcher - self.dispatcher.add_handler(TypeHandler(Update, self.handle_update)) - self.dispatcher.add_error_handler(process_error) + """Create Application to poll for updates.""" super().__init__(hass, config) + self.bot = bot + self.application = ApplicationBuilder().bot(self.bot).build() + self.application.add_handler(TypeHandler(Update, self.handle_update)) + self.application.add_error_handler(process_error) - def start_polling(self, event=None): + async def start_polling(self, event=None): """Start the polling task.""" _LOGGER.debug("Starting polling") - self.updater.start_polling() + await self.application.initialize() + await self.application.updater.start_polling() + await self.application.start() - def stop_polling(self, event=None): + async def stop_polling(self, event=None): """Stop the polling task.""" _LOGGER.debug("Stopping polling") - self.updater.stop() + await self.application.updater.stop() + await self.application.stop() + await self.application.shutdown() diff --git a/homeassistant/components/telegram_bot/strings.json b/homeassistant/components/telegram_bot/strings.json index de5de685409..aad42081274 100644 --- a/homeassistant/components/telegram_bot/strings.json +++ b/homeassistant/components/telegram_bot/strings.json @@ -29,8 +29,8 @@ "description": "Disables link previews for links in the message." }, "timeout": { - "name": "Timeout", - "description": "Timeout for send message. Will help with timeout errors (poor internet connection, etc)s." + "name": "Read timeout", + "description": "Read timeout for send message. Will help with timeout errors (poor internet connection, etc)s." }, "keyboard": { "name": "Keyboard", @@ -95,8 +95,8 @@ "description": "Enable or disable SSL certificate verification. Set to false if you're downloading the file from a URL and you don't want to validate the SSL certificate of the server." }, "timeout": { - "name": "Timeout", - "description": "Timeout for send photo. Will help with timeout errors (poor internet connection, etc)." + "name": "Read timeout", + "description": "Read timeout for send photo." }, "keyboard": { "name": "[%key:component::telegram_bot::services::send_message::fields::keyboard::name%]", @@ -157,8 +157,8 @@ "description": "[%key:component::telegram_bot::services::send_photo::fields::verify_ssl::description%]" }, "timeout": { - "name": "Timeout", - "description": "Timeout for send sticker. Will help with timeout errors (poor internet connection, etc)." + "name": "Read timeout", + "description": "Read timeout for send sticker." }, "keyboard": { "name": "[%key:component::telegram_bot::services::send_message::fields::keyboard::name%]", @@ -223,7 +223,7 @@ "description": "[%key:component::telegram_bot::services::send_photo::fields::verify_ssl::description%]" }, "timeout": { - "name": "Timeout", + "name": "Read timeout", "description": "[%key:component::telegram_bot::services::send_sticker::fields::timeout::description%]" }, "keyboard": { @@ -289,8 +289,8 @@ "description": "[%key:component::telegram_bot::services::send_photo::fields::verify_ssl::description%]" }, "timeout": { - "name": "Timeout", - "description": "Timeout for send video. Will help with timeout errors (poor internet connection, etc)." + "name": "Read timeout", + "description": "Read timeout for send video." }, "keyboard": { "name": "[%key:component::telegram_bot::services::send_message::fields::keyboard::name%]", @@ -351,8 +351,8 @@ "description": "[%key:component::telegram_bot::services::send_photo::fields::verify_ssl::description%]" }, "timeout": { - "name": "Timeout", - "description": "Timeout for send voice. Will help with timeout errors (poor internet connection, etc)." + "name": "Read timeout", + "description": "Read timeout for send voice." }, "keyboard": { "name": "[%key:component::telegram_bot::services::send_message::fields::keyboard::name%]", @@ -417,8 +417,8 @@ "description": "[%key:component::telegram_bot::services::send_photo::fields::verify_ssl::description%]" }, "timeout": { - "name": "Timeout", - "description": "Timeout for send document. Will help with timeout errors (poor internet connection, etc)." + "name": "Read timeout", + "description": "Read timeout for send document." }, "keyboard": { "name": "[%key:component::telegram_bot::services::send_message::fields::keyboard::name%]", @@ -459,7 +459,7 @@ "description": "[%key:component::telegram_bot::services::send_message::fields::disable_notification::description%]" }, "timeout": { - "name": "Timeout", + "name": "Read timeout", "description": "[%key:component::telegram_bot::services::send_photo::fields::timeout::description%]" }, "keyboard": { @@ -513,8 +513,8 @@ "description": "[%key:component::telegram_bot::services::send_message::fields::disable_notification::description%]" }, "timeout": { - "name": "Timeout", - "description": "Timeout for send poll. Will help with timeout errors (poor internet connection, etc)." + "name": "Read timeout", + "description": "Read timeout for send poll." }, "message_tag": { "name": "[%key:component::telegram_bot::services::send_message::fields::message_tag::name%]", @@ -617,8 +617,8 @@ "description": "Show a permanent notification." }, "timeout": { - "name": "Timeout", - "description": "Timeout for sending the answer. Will help with timeout errors (poor internet connection, etc)." + "name": "Read timeout", + "description": "Read timeout for sending the answer." } } }, @@ -636,5 +636,15 @@ } } } + }, + "issues": { + "proxy_params_auth_deprecation": { + "title": "{telegram_bot}: Proxy authentication should be moved to the URL", + "description": "Authentication details for the the proxy configured in the {telegram_bot} integration should be moved into the {proxy_url} instead. Please update your configuration and restart Home Assistant to fix this issue.\n\nThe {proxy_params} config key will be removed in a future release." + }, + "proxy_params_deprecation": { + "title": "{telegram_bot}: Proxy params option will be removed", + "description": "The {proxy_params} config key for the {telegram_bot} integration will be removed in a future release.\n\nAuthentication can now be provided through the {proxy_url} key.\n\nThe underlying library has changed to {httpx} which is incompatible with previous parameters. If you still need this functionality for other options, please leave a comment on the learn more link.\n\nPlease update your configuration to remove the {proxy_params} key and restart Home Assistant to fix this issue." + } } } diff --git a/homeassistant/components/telegram_bot/webhooks.py b/homeassistant/components/telegram_bot/webhooks.py index c21cffa84b1..41835f955ed 100644 --- a/homeassistant/components/telegram_bot/webhooks.py +++ b/homeassistant/components/telegram_bot/webhooks.py @@ -1,4 +1,5 @@ """Support for Telegram bots using webhooks.""" + import datetime as dt from http import HTTPStatus from ipaddress import ip_address @@ -8,7 +9,7 @@ import string from telegram import Update from telegram.error import TimedOut -from telegram.ext import Dispatcher, TypeHandler +from telegram.ext import Application, TypeHandler from homeassistant.components.http import HomeAssistantView from homeassistant.const import EVENT_HOMEASSISTANT_STOP @@ -36,16 +37,17 @@ async def async_setup_platform(hass, bot, config): _LOGGER.error("Invalid telegram webhook %s must be https", pushbot.webhook_url) return False + await pushbot.start_application() webhook_registered = await pushbot.register_webhook() if not webhook_registered: return False - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, pushbot.deregister_webhook) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, pushbot.stop_application) hass.http.register_view( PushBotView( hass, bot, - pushbot.dispatcher, + pushbot.application, config[CONF_TRUSTED_NETWORKS], secret_token, ) @@ -57,13 +59,13 @@ class PushBot(BaseTelegramBotEntity): """Handles all the push/webhook logic and passes telegram updates to `self.handle_update`.""" def __init__(self, hass, bot, config, secret_token): - """Create Dispatcher before calling super().""" + """Create Application before calling super().""" self.bot = bot self.trusted_networks = config[CONF_TRUSTED_NETWORKS] self.secret_token = secret_token - # Dumb dispatcher that just gets our updates to our handler callback (self.handle_update) - self.dispatcher = Dispatcher(bot, None) - self.dispatcher.add_handler(TypeHandler(Update, self.handle_update)) + # Dumb Application that just gets our updates to our handler callback (self.handle_update) + self.application = Application.builder().bot(bot).updater(None).build() + self.application.add_handler(TypeHandler(Update, self.handle_update)) super().__init__(hass, config) self.base_url = config.get(CONF_URL) or get_url( @@ -71,15 +73,15 @@ class PushBot(BaseTelegramBotEntity): ) self.webhook_url = f"{self.base_url}{TELEGRAM_WEBHOOK_URL}" - def _try_to_set_webhook(self): + async def _try_to_set_webhook(self): _LOGGER.debug("Registering webhook URL: %s", self.webhook_url) retry_num = 0 while retry_num < 3: try: - return self.bot.set_webhook( + return await self.bot.set_webhook( self.webhook_url, api_kwargs={"secret_token": self.secret_token}, - timeout=5, + connect_timeout=5, ) except TimedOut: retry_num += 1 @@ -87,11 +89,14 @@ class PushBot(BaseTelegramBotEntity): return False + async def start_application(self): + """Handle starting the Application object.""" + await self.application.initialize() + await self.application.start() + async def register_webhook(self): """Query telegram and register the URL for our webhook.""" - current_status = await self.hass.async_add_executor_job( - self.bot.get_webhook_info - ) + current_status = await self.bot.get_webhook_info() # Some logging of Bot current status: last_error_date = getattr(current_status, "last_error_date", None) if (last_error_date is not None) and (isinstance(last_error_date, int)): @@ -105,7 +110,7 @@ class PushBot(BaseTelegramBotEntity): _LOGGER.debug("telegram webhook status: %s", current_status) if current_status and current_status["url"] != self.webhook_url: - result = await self.hass.async_add_executor_job(self._try_to_set_webhook) + result = await self._try_to_set_webhook() if result: _LOGGER.info("Set new telegram webhook %s", self.webhook_url) else: @@ -114,10 +119,16 @@ class PushBot(BaseTelegramBotEntity): return True - def deregister_webhook(self, event=None): + async def stop_application(self, event=None): + """Handle gracefully stopping the Application object.""" + await self.deregister_webhook() + await self.application.stop() + await self.application.shutdown() + + async def deregister_webhook(self): """Query telegram and deregister the URL for our webhook.""" _LOGGER.debug("Deregistering webhook URL") - return self.bot.delete_webhook() + await self.bot.delete_webhook() class PushBotView(HomeAssistantView): @@ -127,11 +138,11 @@ class PushBotView(HomeAssistantView): url = TELEGRAM_WEBHOOK_URL name = "telegram_webhooks" - def __init__(self, hass, bot, dispatcher, trusted_networks, secret_token): + def __init__(self, hass, bot, application, trusted_networks, secret_token): """Initialize by storing stuff needed for setting up our webhook endpoint.""" self.hass = hass self.bot = bot - self.dispatcher = dispatcher + self.application = application self.trusted_networks = trusted_networks self.secret_token = secret_token @@ -153,6 +164,6 @@ class PushBotView(HomeAssistantView): update = Update.de_json(update_data, self.bot) _LOGGER.debug("Received Update on %s: %s", self.url, update) - await self.hass.async_add_executor_job(self.dispatcher.process_update, update) + await self.application.process_update(update) return None diff --git a/homeassistant/components/tellduslive/__init__.py b/homeassistant/components/tellduslive/__init__.py index 92feb69c60c..92e61edec56 100644 --- a/homeassistant/components/tellduslive/__init__.py +++ b/homeassistant/components/tellduslive/__init__.py @@ -1,4 +1,5 @@ """Support for Telldus Live.""" + import asyncio from functools import partial import logging @@ -185,8 +186,9 @@ class TelldusLiveClient: self._hass.data[CONFIG_ENTRY_IS_SETUP].add(component) device_ids = [] if device.is_sensor: - for item in device.items: - device_ids.append((device.device_id, item.name, item.scale)) + device_ids.extend( + (device.device_id, item.name, item.scale) for item in device.items + ) else: device_ids.append(device_id) for _id in device_ids: diff --git a/homeassistant/components/tellduslive/binary_sensor.py b/homeassistant/components/tellduslive/binary_sensor.py index 4abe1dfd174..1eead7b55a5 100644 --- a/homeassistant/components/tellduslive/binary_sensor.py +++ b/homeassistant/components/tellduslive/binary_sensor.py @@ -1,4 +1,5 @@ """Support for binary sensors using Tellstick Net.""" + from homeassistant.components import binary_sensor from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/tellduslive/config_flow.py b/homeassistant/components/tellduslive/config_flow.py index 33910f6ead1..4537abcdece 100644 --- a/homeassistant/components/tellduslive/config_flow.py +++ b/homeassistant/components/tellduslive/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Tellduslive.""" + import asyncio import logging import os @@ -6,7 +7,7 @@ import os from tellduslive import Session, supports_local_api import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_HOST from homeassistant.util.json import load_json_object @@ -28,7 +29,7 @@ KEY_TOKEN_SECRET = "token_secret" _LOGGER = logging.getLogger(__name__) -class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class FlowHandler(ConfigFlow, domain=DOMAIN): """Handle a config flow.""" VERSION = 1 diff --git a/homeassistant/components/tellduslive/const.py b/homeassistant/components/tellduslive/const.py index 6b3bb1c6437..3a24f6b033a 100644 --- a/homeassistant/components/tellduslive/const.py +++ b/homeassistant/components/tellduslive/const.py @@ -1,4 +1,5 @@ """Consts used by TelldusLive.""" + from datetime import timedelta APPLICATION_NAME = "Home Assistant" diff --git a/homeassistant/components/tellduslive/cover.py b/homeassistant/components/tellduslive/cover.py index 2a32756aa1b..57c6ae9e7eb 100644 --- a/homeassistant/components/tellduslive/cover.py +++ b/homeassistant/components/tellduslive/cover.py @@ -1,4 +1,5 @@ """Support for Tellstick covers using Tellstick Net.""" + from typing import Any from homeassistant.components import cover diff --git a/homeassistant/components/tellduslive/entry.py b/homeassistant/components/tellduslive/entry.py index fdacc02bfca..77a04fabd06 100644 --- a/homeassistant/components/tellduslive/entry.py +++ b/homeassistant/components/tellduslive/entry.py @@ -1,4 +1,5 @@ """Base Entity for all TelldusLive entities.""" + from datetime import datetime import logging diff --git a/homeassistant/components/tellduslive/light.py b/homeassistant/components/tellduslive/light.py index 8284b386250..63af8a32527 100644 --- a/homeassistant/components/tellduslive/light.py +++ b/homeassistant/components/tellduslive/light.py @@ -1,4 +1,5 @@ """Support for Tellstick lights using Tellstick Net.""" + import logging from typing import Any diff --git a/homeassistant/components/tellduslive/sensor.py b/homeassistant/components/tellduslive/sensor.py index e15f89888b1..36520044101 100644 --- a/homeassistant/components/tellduslive/sensor.py +++ b/homeassistant/components/tellduslive/sensor.py @@ -1,4 +1,5 @@ """Support for Tellstick Net/Telstick Live sensors.""" + from __future__ import annotations from homeassistant.components import sensor diff --git a/homeassistant/components/tellduslive/switch.py b/homeassistant/components/tellduslive/switch.py index 5ae5a904689..c26a8dcf951 100644 --- a/homeassistant/components/tellduslive/switch.py +++ b/homeassistant/components/tellduslive/switch.py @@ -1,4 +1,5 @@ """Support for Tellstick switches using Tellstick Net.""" + from typing import Any from homeassistant.components import switch diff --git a/homeassistant/components/tellstick/__init__.py b/homeassistant/components/tellstick/__init__.py index 42685f03e03..1a60927e25f 100644 --- a/homeassistant/components/tellstick/__init__.py +++ b/homeassistant/components/tellstick/__init__.py @@ -1,4 +1,5 @@ """Support for Tellstick.""" + import logging import threading diff --git a/homeassistant/components/tellstick/cover.py b/homeassistant/components/tellstick/cover.py index 17da5684670..cb49d876e71 100644 --- a/homeassistant/components/tellstick/cover.py +++ b/homeassistant/components/tellstick/cover.py @@ -1,4 +1,5 @@ """Support for Tellstick covers.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/tellstick/light.py b/homeassistant/components/tellstick/light.py index 136dd3ebbd6..acbcf2d6cb5 100644 --- a/homeassistant/components/tellstick/light.py +++ b/homeassistant/components/tellstick/light.py @@ -1,4 +1,5 @@ """Support for Tellstick lights.""" + from __future__ import annotations from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity diff --git a/homeassistant/components/tellstick/sensor.py b/homeassistant/components/tellstick/sensor.py index cef118e721d..a2cba41b028 100644 --- a/homeassistant/components/tellstick/sensor.py +++ b/homeassistant/components/tellstick/sensor.py @@ -1,4 +1,5 @@ """Support for Tellstick sensors.""" + from __future__ import annotations from collections import namedtuple @@ -129,8 +130,8 @@ def setup_platform( sensor_name = str(tellcore_sensor.id) else: proto_id = f"{tellcore_sensor.protocol}{tellcore_sensor.id}" - proto_model_id = "{}{}{}".format( - tellcore_sensor.protocol, tellcore_sensor.model, tellcore_sensor.id + proto_model_id = ( + f"{tellcore_sensor.protocol}{tellcore_sensor.model}{tellcore_sensor.id}" ) if tellcore_sensor.id in named_sensors: sensor_name = named_sensors[tellcore_sensor.id] diff --git a/homeassistant/components/tellstick/switch.py b/homeassistant/components/tellstick/switch.py index 966a3defd5e..e3eb4825d91 100644 --- a/homeassistant/components/tellstick/switch.py +++ b/homeassistant/components/tellstick/switch.py @@ -1,4 +1,5 @@ """Support for Tellstick switches.""" + from __future__ import annotations from homeassistant.components.switch import SwitchEntity diff --git a/homeassistant/components/telnet/switch.py b/homeassistant/components/telnet/switch.py index 14e8900f000..6a6f758ff79 100644 --- a/homeassistant/components/telnet/switch.py +++ b/homeassistant/components/telnet/switch.py @@ -1,4 +1,5 @@ """Support for switch controlled using a telnet connection.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/temper/sensor.py b/homeassistant/components/temper/sensor.py index 20ad3102a4b..06ba656dd0d 100644 --- a/homeassistant/components/temper/sensor.py +++ b/homeassistant/components/temper/sensor.py @@ -1,4 +1,5 @@ """Support for getting temperature from TEMPer devices.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/template/__init__.py b/homeassistant/components/template/__init__.py index d52dc0cf166..6d4d3a9367c 100644 --- a/homeassistant/components/template/__init__.py +++ b/homeassistant/components/template/__init__.py @@ -1,4 +1,5 @@ """The template component.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/template/alarm_control_panel.py b/homeassistant/components/template/alarm_control_panel.py index 2cac5d74a7a..4a1af80e25c 100644 --- a/homeassistant/components/template/alarm_control_panel.py +++ b/homeassistant/components/template/alarm_control_panel.py @@ -1,4 +1,5 @@ """Support for Template alarm control panels.""" + from __future__ import annotations from enum import Enum diff --git a/homeassistant/components/template/binary_sensor.py b/homeassistant/components/template/binary_sensor.py index 427fe6221cd..654dad94867 100644 --- a/homeassistant/components/template/binary_sensor.py +++ b/homeassistant/components/template/binary_sensor.py @@ -1,4 +1,5 @@ """Support for exposing a templated binary sensor.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/homeassistant/components/template/button.py b/homeassistant/components/template/button.py index 2bb2f40d6b4..c58dfcf50b4 100644 --- a/homeassistant/components/template/button.py +++ b/homeassistant/components/template/button.py @@ -1,4 +1,5 @@ """Support for buttons which integrates with other components.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/template/config.py b/homeassistant/components/template/config.py index 9da43082d2b..42a57cfc4aa 100644 --- a/homeassistant/components/template/config.py +++ b/homeassistant/components/template/config.py @@ -1,4 +1,5 @@ """Template config validator.""" + import logging import voluptuous as vol diff --git a/homeassistant/components/template/config_flow.py b/homeassistant/components/template/config_flow.py index 686c12fa4ba..b1d11243469 100644 --- a/homeassistant/components/template/config_flow.py +++ b/homeassistant/components/template/config_flow.py @@ -1,4 +1,5 @@ """Config flow for the Template integration.""" + from __future__ import annotations from collections.abc import Callable, Coroutine, Mapping diff --git a/homeassistant/components/template/coordinator.py b/homeassistant/components/template/coordinator.py index a5a38bf7b1d..3319afa01c2 100644 --- a/homeassistant/components/template/coordinator.py +++ b/homeassistant/components/template/coordinator.py @@ -1,4 +1,5 @@ """Data update coordinator for trigger based template entities.""" + from collections.abc import Callable import logging @@ -46,7 +47,7 @@ class TriggerUpdateCoordinator(DataUpdateCoordinator): await self._attach_triggers() else: self._unsub_start = self.hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_START, self._attach_triggers + EVENT_HOMEASSISTANT_START, self._attach_triggers, run_immediately=True ) for platform_domain in PLATFORMS: diff --git a/homeassistant/components/template/cover.py b/homeassistant/components/template/cover.py index 5daa4531109..36ea9f93830 100644 --- a/homeassistant/components/template/cover.py +++ b/homeassistant/components/template/cover.py @@ -1,4 +1,5 @@ """Support for covers which integrate with other components.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/template/fan.py b/homeassistant/components/template/fan.py index 8aeede42552..106d3e4fd70 100644 --- a/homeassistant/components/template/fan.py +++ b/homeassistant/components/template/fan.py @@ -1,4 +1,5 @@ """Support for Template fans.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/template/icons.json b/homeassistant/components/template/icons.json new file mode 100644 index 00000000000..a03163179cb --- /dev/null +++ b/homeassistant/components/template/icons.json @@ -0,0 +1,5 @@ +{ + "services": { + "reload": "mdi:reload" + } +} diff --git a/homeassistant/components/template/image.py b/homeassistant/components/template/image.py index 227109d59e2..92f0fe7b9fa 100644 --- a/homeassistant/components/template/image.py +++ b/homeassistant/components/template/image.py @@ -1,4 +1,5 @@ """Support for image which integrates with other components.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/template/light.py b/homeassistant/components/template/light.py index 89c4826f1e6..71443789703 100644 --- a/homeassistant/components/template/light.py +++ b/homeassistant/components/template/light.py @@ -1,4 +1,5 @@ """Support for Template lights.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/template/lock.py b/homeassistant/components/template/lock.py index de483971ac6..3f9df4818fd 100644 --- a/homeassistant/components/template/lock.py +++ b/homeassistant/components/template/lock.py @@ -1,4 +1,5 @@ """Support for locks which integrates with other components.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/template/number.py b/homeassistant/components/template/number.py index 988cebf08ab..d4004ee9535 100644 --- a/homeassistant/components/template/number.py +++ b/homeassistant/components/template/number.py @@ -1,4 +1,5 @@ """Support for numbers which integrates with other components.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/template/select.py b/homeassistant/components/template/select.py index fea972a5d6f..650b236faee 100644 --- a/homeassistant/components/template/select.py +++ b/homeassistant/components/template/select.py @@ -1,4 +1,5 @@ """Support for selects which integrates with other components.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/template/sensor.py b/homeassistant/components/template/sensor.py index 3a3d0682805..a6dbedc6161 100644 --- a/homeassistant/components/template/sensor.py +++ b/homeassistant/components/template/sensor.py @@ -1,4 +1,5 @@ """Allows the creation of a sensor that breaks out state_attributes.""" + from __future__ import annotations from datetime import date, datetime diff --git a/homeassistant/components/template/switch.py b/homeassistant/components/template/switch.py index 5e75eafe233..f585cd929c0 100644 --- a/homeassistant/components/template/switch.py +++ b/homeassistant/components/template/switch.py @@ -1,4 +1,5 @@ """Support for switches which integrates with other components.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/template/template_entity.py b/homeassistant/components/template/template_entity.py index 9d08980da32..735fa7ddd23 100644 --- a/homeassistant/components/template/template_entity.py +++ b/homeassistant/components/template/template_entity.py @@ -1,4 +1,5 @@ """TemplateEntity utility class.""" + from __future__ import annotations from collections.abc import Callable, Mapping @@ -20,6 +21,7 @@ from homeassistant.const import ( from homeassistant.core import ( CALLBACK_TYPE, Context, + Event, HomeAssistant, State, callback, @@ -46,7 +48,7 @@ from homeassistant.helpers.trigger_template_entity import ( TEMPLATE_ENTITY_BASE_SCHEMA, make_template_entity_base_schema, ) -from homeassistant.helpers.typing import ConfigType, EventType +from homeassistant.helpers.typing import ConfigType from .const import ( CONF_ATTRIBUTE_TEMPLATES, @@ -189,7 +191,7 @@ class _TemplateAttribute: @callback def handle_result( self, - event: EventType[EventStateChangedData] | None, + event: Event[EventStateChangedData] | None, template: Template, last_result: str | None | TemplateError, result: str | TemplateError, @@ -269,15 +271,18 @@ class TemplateEntity(Entity): self._attr_extra_state_attributes = {} self._self_ref_update_count = 0 self._attr_unique_id = unique_id - self._preview_callback: Callable[ - [ - str | None, - dict[str, Any] | None, - dict[str, bool | set[str]] | None, - str | None, - ], - None, - ] | None = None + self._preview_callback: ( + Callable[ + [ + str | None, + dict[str, Any] | None, + dict[str, bool | set[str]] | None, + str | None, + ], + None, + ] + | None + ) = None if config is None: self._attribute_templates = attribute_templates self._availability_template = availability_template @@ -398,7 +403,7 @@ class TemplateEntity(Entity): @callback def _handle_results( self, - event: EventType[EventStateChangedData] | None, + event: Event[EventStateChangedData] | None, updates: list[TrackTemplateResult], ) -> None: """Call back the results to the attributes.""" diff --git a/homeassistant/components/template/trigger.py b/homeassistant/components/template/trigger.py index 327c988106e..8e95362ff88 100644 --- a/homeassistant/components/template/trigger.py +++ b/homeassistant/components/template/trigger.py @@ -1,4 +1,5 @@ """Offer template automation rules.""" + from datetime import timedelta import logging from typing import Any @@ -7,7 +8,7 @@ import voluptuous as vol from homeassistant import exceptions from homeassistant.const import CONF_FOR, CONF_PLATFORM, CONF_VALUE_TEMPLATE -from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback +from homeassistant.core import CALLBACK_TYPE, Event, HassJob, HomeAssistant, callback from homeassistant.helpers import config_validation as cv, template from homeassistant.helpers.event import ( EventStateChangedData, @@ -18,7 +19,7 @@ from homeassistant.helpers.event import ( ) from homeassistant.helpers.template import Template, result_as_boolean from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo -from homeassistant.helpers.typing import ConfigType, EventType +from homeassistant.helpers.typing import ConfigType _LOGGER = logging.getLogger(__name__) @@ -64,7 +65,7 @@ async def async_attach_trigger( @callback def template_listener( - event: EventType[EventStateChangedData] | None, + event: Event[EventStateChangedData] | None, updates: list[TrackTemplateResult], ) -> None: """Listen for state changes and calls action.""" diff --git a/homeassistant/components/template/trigger_entity.py b/homeassistant/components/template/trigger_entity.py index 5f5fbe5b99a..697cd827b9e 100644 --- a/homeassistant/components/template/trigger_entity.py +++ b/homeassistant/components/template/trigger_entity.py @@ -1,4 +1,5 @@ """Trigger entity.""" + from __future__ import annotations from homeassistant.core import HomeAssistant, callback diff --git a/homeassistant/components/template/vacuum.py b/homeassistant/components/template/vacuum.py index 4b693c8070c..9062f71d818 100644 --- a/homeassistant/components/template/vacuum.py +++ b/homeassistant/components/template/vacuum.py @@ -1,4 +1,5 @@ """Support for Template vacuums.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/template/weather.py b/homeassistant/components/template/weather.py index 0a00d1e79b4..e8981fb33f9 100644 --- a/homeassistant/components/template/weather.py +++ b/homeassistant/components/template/weather.py @@ -1,4 +1,5 @@ """Template platform that aggregates meteorological data.""" + from __future__ import annotations from dataclasses import asdict, dataclass @@ -117,7 +118,6 @@ WEATHER_SCHEMA = vol.Schema( vol.Optional(CONF_WIND_BEARING_TEMPLATE): cv.template, vol.Optional(CONF_OZONE_TEMPLATE): cv.template, vol.Optional(CONF_VISIBILITY_TEMPLATE): cv.template, - vol.Optional(CONF_FORECAST_TEMPLATE): cv.template, vol.Optional(CONF_FORECAST_DAILY_TEMPLATE): cv.template, vol.Optional(CONF_FORECAST_HOURLY_TEMPLATE): cv.template, vol.Optional(CONF_FORECAST_TWICE_DAILY_TEMPLATE): cv.template, @@ -192,7 +192,6 @@ class WeatherTemplate(TemplateEntity, WeatherEntity): self._wind_bearing_template = config.get(CONF_WIND_BEARING_TEMPLATE) self._ozone_template = config.get(CONF_OZONE_TEMPLATE) self._visibility_template = config.get(CONF_VISIBILITY_TEMPLATE) - self._forecast_template = config.get(CONF_FORECAST_TEMPLATE) self._forecast_daily_template = config.get(CONF_FORECAST_DAILY_TEMPLATE) self._forecast_hourly_template = config.get(CONF_FORECAST_HOURLY_TEMPLATE) self._forecast_twice_daily_template = config.get( @@ -226,7 +225,6 @@ class WeatherTemplate(TemplateEntity, WeatherEntity): self._cloud_coverage = None self._dew_point = None self._apparent_temperature = None - self._forecast: list[Forecast] = [] self._forecast_daily: list[Forecast] = [] self._forecast_hourly: list[Forecast] = [] self._forecast_twice_daily: list[Forecast] = [] @@ -299,11 +297,6 @@ class WeatherTemplate(TemplateEntity, WeatherEntity): """Return the apparent temperature.""" return self._apparent_temperature - @property - def forecast(self) -> list[Forecast]: - """Return the forecast.""" - return self._forecast - async def async_forecast_daily(self) -> list[Forecast]: """Return the daily forecast in native units.""" return self._forecast_daily @@ -393,11 +386,6 @@ class WeatherTemplate(TemplateEntity, WeatherEntity): "_apparent_temperature", self._apparent_temperature_template, ) - if self._forecast_template: - self.add_template_attribute( - "_forecast", - self._forecast_template, - ) if self._forecast_daily_template: self.add_template_attribute( @@ -432,7 +420,9 @@ class WeatherTemplate(TemplateEntity, WeatherEntity): """Save template result and trigger forecast listener.""" attr_result = None if isinstance(result, TemplateError) else result setattr(self, f"_forecast_{forecast_type}", attr_result) - self.hass.create_task(self.async_update_listeners([forecast_type])) + self.hass.async_create_task( + self.async_update_listeners([forecast_type]), eager_start=True + ) @callback def _validate_forecast( @@ -571,12 +561,12 @@ class TriggerWeatherEntity(TriggerEntity, WeatherEntity, RestoreEntity): and state.state not in (STATE_UNKNOWN, STATE_UNAVAILABLE) and (weather_data := await self.async_get_last_weather_data()) ): - self._rendered[ - CONF_APPARENT_TEMPERATURE_TEMPLATE - ] = weather_data.last_apparent_temperature - self._rendered[ - CONF_CLOUD_COVERAGE_TEMPLATE - ] = weather_data.last_cloud_coverage + self._rendered[CONF_APPARENT_TEMPERATURE_TEMPLATE] = ( + weather_data.last_apparent_temperature + ) + self._rendered[CONF_CLOUD_COVERAGE_TEMPLATE] = ( + weather_data.last_cloud_coverage + ) self._rendered[CONF_CONDITION_TEMPLATE] = state.state self._rendered[CONF_DEW_POINT_TEMPLATE] = weather_data.last_dew_point self._rendered[CONF_HUMIDITY_TEMPLATE] = weather_data.last_humidity @@ -585,9 +575,9 @@ class TriggerWeatherEntity(TriggerEntity, WeatherEntity, RestoreEntity): self._rendered[CONF_TEMPERATURE_TEMPLATE] = weather_data.last_temperature self._rendered[CONF_VISIBILITY_TEMPLATE] = weather_data.last_visibility self._rendered[CONF_WIND_BEARING_TEMPLATE] = weather_data.last_wind_bearing - self._rendered[ - CONF_WIND_GUST_SPEED_TEMPLATE - ] = weather_data.last_wind_gust_speed + self._rendered[CONF_WIND_GUST_SPEED_TEMPLATE] = ( + weather_data.last_wind_gust_speed + ) self._rendered[CONF_WIND_SPEED_TEMPLATE] = weather_data.last_wind_speed @property diff --git a/homeassistant/components/tensorflow/image_processing.py b/homeassistant/components/tensorflow/image_processing.py index e2fce4b94c2..632db28ca3a 100644 --- a/homeassistant/components/tensorflow/image_processing.py +++ b/homeassistant/components/tensorflow/image_processing.py @@ -1,4 +1,5 @@ """Support for performing TensorFlow classification on images.""" + from __future__ import annotations import io @@ -195,20 +196,16 @@ def setup_platform( labels, use_display_name=True ) - entities = [] - - for camera in config[CONF_SOURCE]: - entities.append( - TensorFlowImageProcessor( - hass, - camera[CONF_ENTITY_ID], - camera.get(CONF_NAME), - category_index, - config, - ) + add_entities( + TensorFlowImageProcessor( + hass, + camera[CONF_ENTITY_ID], + camera.get(CONF_NAME), + category_index, + config, ) - - add_entities(entities) + for camera in config[CONF_SOURCE] + ) class TensorFlowImageProcessor(ImageProcessingEntity): diff --git a/homeassistant/components/tesla_wall_connector/__init__.py b/homeassistant/components/tesla_wall_connector/__init__.py index 30e61dc7744..28ddc15ade7 100644 --- a/homeassistant/components/tesla_wall_connector/__init__.py +++ b/homeassistant/components/tesla_wall_connector/__init__.py @@ -1,4 +1,5 @@ """The Tesla Wall Connector integration.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/tesla_wall_connector/binary_sensor.py b/homeassistant/components/tesla_wall_connector/binary_sensor.py index d21d9a75e0b..cf8fbf53b52 100644 --- a/homeassistant/components/tesla_wall_connector/binary_sensor.py +++ b/homeassistant/components/tesla_wall_connector/binary_sensor.py @@ -1,4 +1,5 @@ """Binary Sensors for Tesla Wall Connector.""" + from dataclasses import dataclass import logging diff --git a/homeassistant/components/tesla_wall_connector/config_flow.py b/homeassistant/components/tesla_wall_connector/config_flow.py index 5b3cf9bd835..b00dd8f2b9d 100644 --- a/homeassistant/components/tesla_wall_connector/config_flow.py +++ b/homeassistant/components/tesla_wall_connector/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Tesla Wall Connector integration.""" + from __future__ import annotations import logging @@ -8,11 +9,10 @@ from tesla_wall_connector import WallConnector from tesla_wall_connector.exceptions import WallConnectorError import voluptuous as vol -from homeassistant import config_entries from homeassistant.components import dhcp +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN, WALLCONNECTOR_DEVICE_NAME, WALLCONNECTOR_SERIAL_NUMBER @@ -37,7 +37,7 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, } -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class TeslaWallConnectorConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Tesla Wall Connector.""" VERSION = 1 @@ -48,7 +48,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.ip_address: str | None = None self.serial_number = None - async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: + async def async_step_dhcp( + self, discovery_info: dhcp.DhcpServiceInfo + ) -> ConfigFlowResult: """Handle dhcp discovery.""" self.ip_address = discovery_info.ip _LOGGER.debug("Discovered Tesla Wall Connector at [%s]", self.ip_address) @@ -89,7 +91,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" data_schema = vol.Schema( {vol.Required(CONF_HOST, default=self.ip_address): str} diff --git a/homeassistant/components/tesla_wall_connector/icons.json b/homeassistant/components/tesla_wall_connector/icons.json new file mode 100644 index 00000000000..995edd74b04 --- /dev/null +++ b/homeassistant/components/tesla_wall_connector/icons.json @@ -0,0 +1,9 @@ +{ + "entity": { + "sensor": { + "status": { + "default": "mdi:ev-station" + } + } + } +} diff --git a/homeassistant/components/tesla_wall_connector/sensor.py b/homeassistant/components/tesla_wall_connector/sensor.py index bba01f8692d..9cbe14982f2 100644 --- a/homeassistant/components/tesla_wall_connector/sensor.py +++ b/homeassistant/components/tesla_wall_connector/sensor.py @@ -1,4 +1,5 @@ """Sensors for Tesla Wall Connector.""" + from dataclasses import dataclass import logging @@ -66,7 +67,6 @@ WALL_CONNECTOR_SENSORS = [ data[WALLCONNECTOR_DATA_VITALS].evse_state ), options=list(EVSE_STATE.values()), - icon="mdi:ev-station", ), WallConnectorSensorDescription( key="handle_temp_c", diff --git a/homeassistant/components/teslemetry/__init__.py b/homeassistant/components/teslemetry/__init__.py index fb74e905181..1da3533fef1 100644 --- a/homeassistant/components/teslemetry/__init__.py +++ b/homeassistant/components/teslemetry/__init__.py @@ -1,9 +1,14 @@ """Teslemetry integration.""" + import asyncio from typing import Final -from tesla_fleet_api import Teslemetry, VehicleSpecific -from tesla_fleet_api.exceptions import InvalidToken, PaymentRequired, TeslaFleetError +from tesla_fleet_api import EnergySpecific, Teslemetry, VehicleSpecific +from tesla_fleet_api.exceptions import ( + InvalidToken, + SubscriptionRequired, + TeslaFleetError, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ACCESS_TOKEN, Platform @@ -12,12 +17,13 @@ from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN, LOGGER -from .coordinator import TeslemetryVehicleDataCoordinator -from .models import TeslemetryVehicleData +from .coordinator import ( + TeslemetryEnergyDataCoordinator, + TeslemetryVehicleDataCoordinator, +) +from .models import TeslemetryData, TeslemetryEnergyData, TeslemetryVehicleData -PLATFORMS: Final = [ - Platform.CLIMATE, -] +PLATFORMS: Final = [Platform.CLIMATE, Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -35,36 +41,55 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except InvalidToken: LOGGER.error("Access token is invalid, unable to connect to Teslemetry") return False - except PaymentRequired: + except SubscriptionRequired: LOGGER.error("Subscription required, unable to connect to Telemetry") return False except TeslaFleetError as e: raise ConfigEntryNotReady from e # Create array of classes - data = [] + vehicles: list[TeslemetryVehicleData] = [] + energysites: list[TeslemetryEnergyData] = [] for product in products: - if "vin" not in product: - continue - vin = product["vin"] - - api = VehicleSpecific(teslemetry.vehicle, vin) - coordinator = TeslemetryVehicleDataCoordinator(hass, api) - data.append( - TeslemetryVehicleData( - api=api, - coordinator=coordinator, - vin=vin, + if "vin" in product: + vin = product["vin"] + api = VehicleSpecific(teslemetry.vehicle, vin) + coordinator = TeslemetryVehicleDataCoordinator(hass, api) + vehicles.append( + TeslemetryVehicleData( + api=api, + coordinator=coordinator, + vin=vin, + ) + ) + elif "energy_site_id" in product: + site_id = product["energy_site_id"] + api = EnergySpecific(teslemetry.energy, site_id) + energysites.append( + TeslemetryEnergyData( + api=api, + coordinator=TeslemetryEnergyDataCoordinator(hass, api), + id=site_id, + info=product, + ) ) - ) - # Do all coordinator first refresh simultaneously + # Do all coordinator first refreshes simultaneously await asyncio.gather( - *(vehicle.coordinator.async_config_entry_first_refresh() for vehicle in data) + *( + vehicle.coordinator.async_config_entry_first_refresh() + for vehicle in vehicles + ), + *( + energysite.coordinator.async_config_entry_first_refresh() + for energysite in energysites + ), ) # Setup Platforms - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = data + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = TeslemetryData( + vehicles, energysites + ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/teslemetry/climate.py b/homeassistant/components/teslemetry/climate.py index 748acbb8552..0835785d194 100644 --- a/homeassistant/components/teslemetry/climate.py +++ b/homeassistant/components/teslemetry/climate.py @@ -1,4 +1,5 @@ """Climate platform for Teslemetry integration.""" + from __future__ import annotations from typing import Any @@ -26,7 +27,7 @@ async def async_setup_entry( async_add_entities( TeslemetryClimateEntity(vehicle, TeslemetryClimateSide.DRIVER) - for vehicle in data + for vehicle in data.vehicles ) diff --git a/homeassistant/components/teslemetry/config_flow.py b/homeassistant/components/teslemetry/config_flow.py index 64a279132ad..f7fc5bbf805 100644 --- a/homeassistant/components/teslemetry/config_flow.py +++ b/homeassistant/components/teslemetry/config_flow.py @@ -1,4 +1,5 @@ """Config Flow for Teslemetry integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -6,12 +7,15 @@ from typing import Any from aiohttp import ClientConnectionError from tesla_fleet_api import Teslemetry -from tesla_fleet_api.exceptions import InvalidToken, PaymentRequired, TeslaFleetError +from tesla_fleet_api.exceptions import ( + InvalidToken, + SubscriptionRequired, + TeslaFleetError, +) import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_ACCESS_TOKEN -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN, LOGGER @@ -30,7 +34,7 @@ class TeslemetryConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: Mapping[str, Any] | None = None - ) -> FlowResult: + ) -> config_entries.ConfigFlowResult: """Get configuration from the user.""" errors: dict[str, str] = {} if user_input: @@ -42,7 +46,7 @@ class TeslemetryConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): await teslemetry.test() except InvalidToken: errors[CONF_ACCESS_TOKEN] = "invalid_access_token" - except PaymentRequired: + except SubscriptionRequired: errors["base"] = "subscription_required" except ClientConnectionError: errors["base"] = "cannot_connect" diff --git a/homeassistant/components/teslemetry/const.py b/homeassistant/components/teslemetry/const.py index 9b31a3270ca..0d9d129877f 100644 --- a/homeassistant/components/teslemetry/const.py +++ b/homeassistant/components/teslemetry/const.py @@ -1,4 +1,5 @@ """Constants used by Teslemetry integration.""" + from __future__ import annotations from enum import StrEnum diff --git a/homeassistant/components/teslemetry/coordinator.py b/homeassistant/components/teslemetry/coordinator.py index 35e8ccd3bcf..27ff45f75a3 100644 --- a/homeassistant/components/teslemetry/coordinator.py +++ b/homeassistant/components/teslemetry/coordinator.py @@ -1,8 +1,10 @@ """Teslemetry Data Coordinator.""" + from datetime import timedelta from typing import Any -from tesla_fleet_api import VehicleSpecific +from tesla_fleet_api import EnergySpecific, VehicleSpecific +from tesla_fleet_api.const import VehicleDataEndpoint from tesla_fleet_api.exceptions import TeslaFleetError, VehicleOffline from homeassistant.core import HomeAssistant @@ -12,21 +14,39 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda from .const import LOGGER, TeslemetryState SYNC_INTERVAL = 60 +ENDPOINTS = [ + VehicleDataEndpoint.CHARGE_STATE, + VehicleDataEndpoint.CLIMATE_STATE, + VehicleDataEndpoint.DRIVE_STATE, + VehicleDataEndpoint.LOCATION_DATA, + VehicleDataEndpoint.VEHICLE_STATE, + VehicleDataEndpoint.VEHICLE_CONFIG, +] -class TeslemetryVehicleDataCoordinator(DataUpdateCoordinator[dict[str, Any]]): - """Class to manage fetching data from the Teslemetry API.""" +class TeslemetryDataCoordinator(DataUpdateCoordinator[dict[str, Any]]): + """Base class for Teslemetry Data Coordinators.""" - def __init__(self, hass: HomeAssistant, api: VehicleSpecific) -> None: - """Initialize Teslemetry Data Update Coordinator.""" + name: str + + def __init__( + self, hass: HomeAssistant, api: VehicleSpecific | EnergySpecific + ) -> None: + """Initialize Teslemetry Vehicle Update Coordinator.""" super().__init__( hass, LOGGER, - name="Teslemetry Vehicle", + name=self.name, update_interval=timedelta(seconds=SYNC_INTERVAL), ) self.api = api + +class TeslemetryVehicleDataCoordinator(TeslemetryDataCoordinator): + """Class to manage fetching data from the Teslemetry API.""" + + name = "Teslemetry Vehicle" + async def async_config_entry_first_refresh(self) -> None: """Perform first refresh.""" try: @@ -43,7 +63,7 @@ class TeslemetryVehicleDataCoordinator(DataUpdateCoordinator[dict[str, Any]]): """Update vehicle data using Teslemetry API.""" try: - data = await self.api.vehicle_data() + data = await self.api.vehicle_data(endpoints=ENDPOINTS) except VehicleOffline: self.data["state"] = TeslemetryState.OFFLINE return self.data @@ -65,3 +85,24 @@ class TeslemetryVehicleDataCoordinator(DataUpdateCoordinator[dict[str, Any]]): else: result[key] = value return result + + +class TeslemetryEnergyDataCoordinator(TeslemetryDataCoordinator): + """Class to manage fetching data from the Teslemetry API.""" + + name = "Teslemetry Energy Site" + + async def _async_update_data(self) -> dict[str, Any]: + """Update energy site data using Teslemetry API.""" + + try: + data = await self.api.live_status() + except TeslaFleetError as e: + raise UpdateFailed(e.message) from e + + # Convert Wall Connectors from array to dict + data["response"]["wall_connectors"] = { + wc["din"]: wc for wc in data["response"].get("wall_connectors", []) + } + + return data["response"] diff --git a/homeassistant/components/teslemetry/entity.py b/homeassistant/components/teslemetry/entity.py index 024d0603e7e..eda3d26f341 100644 --- a/homeassistant/components/teslemetry/entity.py +++ b/homeassistant/components/teslemetry/entity.py @@ -10,12 +10,15 @@ from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN, MODELS, TeslemetryState -from .coordinator import TeslemetryVehicleDataCoordinator -from .models import TeslemetryVehicleData +from .coordinator import ( + TeslemetryEnergyDataCoordinator, + TeslemetryVehicleDataCoordinator, +) +from .models import TeslemetryEnergyData, TeslemetryVehicleData class TeslemetryVehicleEntity(CoordinatorEntity[TeslemetryVehicleDataCoordinator]): - """Parent class for Teslemetry Entities.""" + """Parent class for Teslemetry Vehicle Entities.""" _attr_has_entity_name = True @@ -45,6 +48,11 @@ class TeslemetryVehicleEntity(CoordinatorEntity[TeslemetryVehicleDataCoordinator serial_number=vehicle.vin, ) + @property + def _value(self) -> Any | None: + """Return a specific value from coordinator data.""" + return self.coordinator.data.get(self.key) + async def wake_up_if_asleep(self) -> None: """Wake up the vehicle if its asleep.""" async with self._wakelock: @@ -74,3 +82,65 @@ class TeslemetryVehicleEntity(CoordinatorEntity[TeslemetryVehicleDataCoordinator for key, value in args: self.coordinator.data[key] = value self.async_write_ha_state() + + +class TeslemetryEnergyEntity(CoordinatorEntity[TeslemetryEnergyDataCoordinator]): + """Parent class for Teslemetry Energy Entities.""" + + _attr_has_entity_name = True + + def __init__( + self, + energysite: TeslemetryEnergyData, + key: str, + ) -> None: + """Initialize common aspects of a Teslemetry entity.""" + super().__init__(energysite.coordinator) + self.key = key + self.api = energysite.api + + self._attr_translation_key = key + self._attr_unique_id = f"{energysite.id}-{key}" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, str(energysite.id))}, + manufacturer="Tesla", + configuration_url="https://teslemetry.com/console", + name=self.coordinator.data.get("site_name", "Energy Site"), + ) + + def get(self, key: str | None = None, default: Any | None = None) -> Any: + """Return a specific value from coordinator data.""" + return self.coordinator.data.get(key or self.key, default) + + +class TeslemetryWallConnectorEntity(CoordinatorEntity[TeslemetryEnergyDataCoordinator]): + """Parent class for Teslemetry Wall Connector Entities.""" + + _attr_has_entity_name = True + + def __init__( + self, + energysite: TeslemetryEnergyData, + din: str, + key: str, + ) -> None: + """Initialize common aspects of a Teslemetry entity.""" + super().__init__(energysite.coordinator) + self.din = din + self.key = key + + self._attr_translation_key = key + self._attr_unique_id = f"{energysite.id}-{din}-{key}" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, din)}, + manufacturer="Tesla", + configuration_url="https://teslemetry.com/console", + name="Wall Connector", + via_device=(DOMAIN, str(energysite.id)), + serial_number=din.split("-")[-1], + ) + + @property + def _value(self) -> int: + """Return a specific wall connector value from coordinator data.""" + return self.coordinator.data["wall_connectors"][self.din].get(self.key) diff --git a/homeassistant/components/teslemetry/icons.json b/homeassistant/components/teslemetry/icons.json index a4521b52945..b3b61831b0e 100644 --- a/homeassistant/components/teslemetry/icons.json +++ b/homeassistant/components/teslemetry/icons.json @@ -13,6 +13,68 @@ } } } + }, + "sensor": { + "battery_power": { + "default": "mdi:home-battery" + }, + "charge_state_charging_state": { + "default": "mdi:ev-station", + "state": { + "disconnected": "mdi:connection", + "no_power": "mdi:power-plug-off-outline", + "starting": "mdi:play-circle", + "stopped": "mdi:stop-circle" + } + }, + "drive_state_active_route_destination": { + "default": "mdi:routes" + }, + "drive_state_active_route_minutes_to_arrival": { + "default": "mdi:routes-clock" + }, + "drive_state_shift_state": { + "default": "mdi:car-shift-pattern", + "state": { + "d": "mdi:alpha-d", + "n": "mdi:alpha-n", + "p": "mdi:alpha-p", + "r": "mdi:alpha-r" + } + }, + "energy_left": { + "default": "mdi:battery" + }, + "generator_power": { + "default": "mdi:generator-stationary" + }, + "grid_power": { + "default": "mdi:transmission-tower" + }, + "grid_services_power": { + "default": "mdi:transmission-tower" + }, + "load_power": { + "default": "mdi:power-plug" + }, + "solar_power": { + "default": "mdi:solar-power" + }, + "total_pack_energy": { + "default": "mdi:battery-high" + }, + "vin": { + "default": "mdi:car-electric" + }, + "wall_connector_fault_state": { + "default": "mdi:ev-station" + }, + "wall_connector_power": { + "default": "mdi:ev-station" + }, + "wall_connector_state": { + "default": "mdi:ev-station" + } } } } diff --git a/homeassistant/components/teslemetry/manifest.json b/homeassistant/components/teslemetry/manifest.json index ab2d52f329d..7f3f1704f2d 100644 --- a/homeassistant/components/teslemetry/manifest.json +++ b/homeassistant/components/teslemetry/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/teslemetry", "iot_class": "cloud_polling", "loggers": ["tesla-fleet-api"], - "requirements": ["tesla-fleet-api==0.4.6"] + "requirements": ["tesla-fleet-api==0.4.9"] } diff --git a/homeassistant/components/teslemetry/models.py b/homeassistant/components/teslemetry/models.py index e5b27fa9279..d6f15e2e932 100644 --- a/homeassistant/components/teslemetry/models.py +++ b/homeassistant/components/teslemetry/models.py @@ -1,12 +1,24 @@ """The Teslemetry integration models.""" + from __future__ import annotations import asyncio from dataclasses import dataclass -from tesla_fleet_api import VehicleSpecific +from tesla_fleet_api import EnergySpecific, VehicleSpecific -from .coordinator import TeslemetryVehicleDataCoordinator +from .coordinator import ( + TeslemetryEnergyDataCoordinator, + TeslemetryVehicleDataCoordinator, +) + + +@dataclass +class TeslemetryData: + """Data for the Teslemetry integration.""" + + vehicles: list[TeslemetryVehicleData] + energysites: list[TeslemetryEnergyData] @dataclass @@ -17,3 +29,13 @@ class TeslemetryVehicleData: coordinator: TeslemetryVehicleDataCoordinator vin: str wakelock = asyncio.Lock() + + +@dataclass +class TeslemetryEnergyData: + """Data for a vehicle in the Teslemetry integration.""" + + api: EnergySpecific + coordinator: TeslemetryEnergyDataCoordinator + id: int + info: dict[str, str] diff --git a/homeassistant/components/teslemetry/sensor.py b/homeassistant/components/teslemetry/sensor.py new file mode 100644 index 00000000000..6284a0e5368 --- /dev/null +++ b/homeassistant/components/teslemetry/sensor.py @@ -0,0 +1,525 @@ +"""Sensor platform for Teslemetry integration.""" + +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from datetime import datetime, timedelta +from itertools import chain +from typing import cast + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + PERCENTAGE, + EntityCategory, + UnitOfElectricCurrent, + UnitOfElectricPotential, + UnitOfEnergy, + UnitOfLength, + UnitOfPower, + UnitOfPressure, + UnitOfSpeed, + UnitOfTemperature, + UnitOfTime, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType +from homeassistant.util import dt as dt_util +from homeassistant.util.variance import ignore_variance + +from .const import DOMAIN +from .entity import ( + TeslemetryEnergyEntity, + TeslemetryVehicleEntity, + TeslemetryWallConnectorEntity, +) +from .models import TeslemetryEnergyData, TeslemetryVehicleData + +CHARGE_STATES = { + "Starting": "starting", + "Charging": "charging", + "Stopped": "stopped", + "Complete": "complete", + "Disconnected": "disconnected", + "NoPower": "no_power", +} + +SHIFT_STATES = {"P": "p", "D": "d", "R": "r", "N": "n"} + + +@dataclass(frozen=True, kw_only=True) +class TeslemetrySensorEntityDescription(SensorEntityDescription): + """Describes Teslemetry Sensor entity.""" + + value_fn: Callable[[StateType], StateType | datetime] = lambda x: x + + +VEHICLE_DESCRIPTIONS: tuple[TeslemetrySensorEntityDescription, ...] = ( + TeslemetrySensorEntityDescription( + key="charge_state_charging_state", + options=list(CHARGE_STATES.values()), + device_class=SensorDeviceClass.ENUM, + value_fn=lambda value: CHARGE_STATES.get(cast(str, value)), + ), + TeslemetrySensorEntityDescription( + key="charge_state_battery_level", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=PERCENTAGE, + device_class=SensorDeviceClass.BATTERY, + ), + TeslemetrySensorEntityDescription( + key="charge_state_usable_battery_level", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=PERCENTAGE, + device_class=SensorDeviceClass.BATTERY, + entity_registry_enabled_default=False, + ), + TeslemetrySensorEntityDescription( + key="charge_state_charge_energy_added", + state_class=SensorStateClass.TOTAL_INCREASING, + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + suggested_display_precision=1, + ), + TeslemetrySensorEntityDescription( + key="charge_state_charger_power", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfPower.KILO_WATT, + device_class=SensorDeviceClass.POWER, + ), + TeslemetrySensorEntityDescription( + key="charge_state_charger_voltage", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + device_class=SensorDeviceClass.VOLTAGE, + entity_category=EntityCategory.DIAGNOSTIC, + ), + TeslemetrySensorEntityDescription( + key="charge_state_charger_actual_current", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + device_class=SensorDeviceClass.CURRENT, + entity_category=EntityCategory.DIAGNOSTIC, + ), + TeslemetrySensorEntityDescription( + key="charge_state_charge_rate", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfSpeed.MILES_PER_HOUR, + device_class=SensorDeviceClass.SPEED, + entity_category=EntityCategory.DIAGNOSTIC, + ), + TeslemetrySensorEntityDescription( + key="charge_state_conn_charge_cable", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + TeslemetrySensorEntityDescription( + key="charge_state_fast_charger_type", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + TeslemetrySensorEntityDescription( + key="charge_state_battery_range", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfLength.MILES, + device_class=SensorDeviceClass.DISTANCE, + suggested_display_precision=1, + ), + TeslemetrySensorEntityDescription( + key="charge_state_est_battery_range", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfLength.MILES, + device_class=SensorDeviceClass.DISTANCE, + suggested_display_precision=1, + entity_registry_enabled_default=False, + ), + TeslemetrySensorEntityDescription( + key="charge_state_ideal_battery_range", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfLength.MILES, + device_class=SensorDeviceClass.DISTANCE, + suggested_display_precision=1, + entity_registry_enabled_default=False, + ), + TeslemetrySensorEntityDescription( + key="drive_state_speed", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfSpeed.MILES_PER_HOUR, + device_class=SensorDeviceClass.SPEED, + entity_registry_enabled_default=False, + value_fn=lambda value: value or 0, + ), + TeslemetrySensorEntityDescription( + key="drive_state_power", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfPower.KILO_WATT, + device_class=SensorDeviceClass.POWER, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + value_fn=lambda value: value or 0, + ), + TeslemetrySensorEntityDescription( + key="drive_state_shift_state", + options=list(SHIFT_STATES.values()), + device_class=SensorDeviceClass.ENUM, + value_fn=lambda x: SHIFT_STATES.get(str(x), "p"), + entity_registry_enabled_default=False, + ), + TeslemetrySensorEntityDescription( + key="vehicle_state_odometer", + state_class=SensorStateClass.TOTAL_INCREASING, + native_unit_of_measurement=UnitOfLength.MILES, + device_class=SensorDeviceClass.DISTANCE, + suggested_display_precision=0, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + TeslemetrySensorEntityDescription( + key="vehicle_state_tpms_pressure_fl", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfPressure.BAR, + suggested_unit_of_measurement=UnitOfPressure.PSI, + device_class=SensorDeviceClass.PRESSURE, + suggested_display_precision=1, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + TeslemetrySensorEntityDescription( + key="vehicle_state_tpms_pressure_fr", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfPressure.BAR, + suggested_unit_of_measurement=UnitOfPressure.PSI, + device_class=SensorDeviceClass.PRESSURE, + suggested_display_precision=1, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + TeslemetrySensorEntityDescription( + key="vehicle_state_tpms_pressure_rl", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfPressure.BAR, + suggested_unit_of_measurement=UnitOfPressure.PSI, + device_class=SensorDeviceClass.PRESSURE, + suggested_display_precision=1, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + TeslemetrySensorEntityDescription( + key="vehicle_state_tpms_pressure_rr", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfPressure.BAR, + suggested_unit_of_measurement=UnitOfPressure.PSI, + device_class=SensorDeviceClass.PRESSURE, + suggested_display_precision=1, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + TeslemetrySensorEntityDescription( + key="climate_state_inside_temp", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + suggested_display_precision=1, + ), + TeslemetrySensorEntityDescription( + key="climate_state_outside_temp", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + suggested_display_precision=1, + ), + TeslemetrySensorEntityDescription( + key="climate_state_driver_temp_setting", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + suggested_display_precision=1, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + TeslemetrySensorEntityDescription( + key="climate_state_passenger_temp_setting", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + suggested_display_precision=1, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + TeslemetrySensorEntityDescription( + key="drive_state_active_route_traffic_minutes_delay", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfTime.MINUTES, + device_class=SensorDeviceClass.DURATION, + entity_registry_enabled_default=False, + ), + TeslemetrySensorEntityDescription( + key="drive_state_active_route_energy_at_arrival", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=PERCENTAGE, + device_class=SensorDeviceClass.BATTERY, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + TeslemetrySensorEntityDescription( + key="drive_state_active_route_miles_to_arrival", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfLength.MILES, + device_class=SensorDeviceClass.DISTANCE, + ), +) + + +@dataclass(frozen=True, kw_only=True) +class TeslemetryTimeEntityDescription(SensorEntityDescription): + """Describes Teslemetry Sensor entity.""" + + variance: int + + +VEHICLE_TIME_DESCRIPTIONS: tuple[TeslemetryTimeEntityDescription, ...] = ( + TeslemetryTimeEntityDescription( + key="charge_state_minutes_to_full_charge", + device_class=SensorDeviceClass.TIMESTAMP, + entity_category=EntityCategory.DIAGNOSTIC, + variance=4, + ), + TeslemetryTimeEntityDescription( + key="drive_state_active_route_minutes_to_arrival", + device_class=SensorDeviceClass.TIMESTAMP, + variance=1, + ), +) + +ENERGY_DESCRIPTIONS: tuple[SensorEntityDescription, ...] = ( + SensorEntityDescription( + key="solar_power", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfPower.WATT, + suggested_unit_of_measurement=UnitOfPower.KILO_WATT, + suggested_display_precision=2, + device_class=SensorDeviceClass.POWER, + ), + SensorEntityDescription( + key="energy_left", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, + suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + suggested_display_precision=2, + device_class=SensorDeviceClass.ENERGY_STORAGE, + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key="total_pack_energy", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, + suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + suggested_display_precision=2, + device_class=SensorDeviceClass.ENERGY_STORAGE, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="percentage_charged", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=PERCENTAGE, + device_class=SensorDeviceClass.BATTERY, + suggested_display_precision=2, + ), + SensorEntityDescription( + key="battery_power", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfPower.WATT, + suggested_unit_of_measurement=UnitOfPower.KILO_WATT, + suggested_display_precision=2, + device_class=SensorDeviceClass.POWER, + ), + SensorEntityDescription( + key="load_power", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfPower.WATT, + suggested_unit_of_measurement=UnitOfPower.KILO_WATT, + suggested_display_precision=2, + device_class=SensorDeviceClass.POWER, + ), + SensorEntityDescription( + key="grid_power", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfPower.WATT, + suggested_unit_of_measurement=UnitOfPower.KILO_WATT, + suggested_display_precision=2, + device_class=SensorDeviceClass.POWER, + ), + SensorEntityDescription( + key="grid_services_power", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfPower.WATT, + suggested_unit_of_measurement=UnitOfPower.KILO_WATT, + suggested_display_precision=2, + device_class=SensorDeviceClass.POWER, + ), + SensorEntityDescription( + key="generator_power", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfPower.WATT, + suggested_unit_of_measurement=UnitOfPower.KILO_WATT, + suggested_display_precision=2, + device_class=SensorDeviceClass.POWER, + entity_registry_enabled_default=False, + ), + SensorEntityDescription(key="island_status", device_class=SensorDeviceClass.ENUM), +) + +WALL_CONNECTOR_DESCRIPTIONS: tuple[SensorEntityDescription, ...] = ( + SensorEntityDescription( + key="wall_connector_state", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="wall_connector_fault_state", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="wall_connector_power", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfPower.WATT, + suggested_unit_of_measurement=UnitOfPower.KILO_WATT, + suggested_display_precision=2, + device_class=SensorDeviceClass.POWER, + ), + SensorEntityDescription( + key="vin", + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up the Teslemetry sensor platform from a config entry.""" + data = hass.data[DOMAIN][entry.entry_id] + + async_add_entities( + chain( + ( # Add vehicles + TeslemetryVehicleSensorEntity(vehicle, description) + for vehicle in data.vehicles + for description in VEHICLE_DESCRIPTIONS + ), + ( # Add vehicles time sensors + TeslemetryVehicleTimeSensorEntity(vehicle, description) + for vehicle in data.vehicles + for description in VEHICLE_TIME_DESCRIPTIONS + ), + ( # Add energy site live + TeslemetryEnergySensorEntity(energysite, description) + for energysite in data.energysites + for description in ENERGY_DESCRIPTIONS + if description.key in energysite.coordinator.data + ), + ( # Add wall connectors + TeslemetryWallConnectorSensorEntity(energysite, din, description) + for energysite in data.energysites + for din in energysite.coordinator.data.get("wall_connectors", {}) + for description in WALL_CONNECTOR_DESCRIPTIONS + ), + ) + ) + + +class TeslemetryVehicleSensorEntity(TeslemetryVehicleEntity, SensorEntity): + """Base class for Teslemetry vehicle metric sensors.""" + + entity_description: TeslemetrySensorEntityDescription + + def __init__( + self, + vehicle: TeslemetryVehicleData, + description: TeslemetrySensorEntityDescription, + ) -> None: + """Initialize the sensor.""" + super().__init__(vehicle, description.key) + + +class TeslemetryVehicleTimeSensorEntity(TeslemetryVehicleEntity, SensorEntity): + """Base class for Teslemetry vehicle metric sensors.""" + + entity_description: TeslemetryTimeEntityDescription + + def __init__( + self, + data: TeslemetryVehicleData, + description: TeslemetryTimeEntityDescription, + ) -> None: + """Initialize the sensor.""" + self.entity_description = description + self._get_timestamp = ignore_variance( + func=lambda value: dt_util.now() + timedelta(minutes=value), + ignored_variance=timedelta(minutes=description.variance), + ) + + super().__init__(data, description.key) + + @property + def native_value(self) -> datetime | None: + """Return the state of the sensor.""" + return self._get_timestamp(self._value) + + @property + def available(self) -> bool: + """Return the availability of the sensor.""" + return isinstance(self._value, int | float) and self._value > 0 + + +class TeslemetryEnergySensorEntity(TeslemetryEnergyEntity, SensorEntity): + """Base class for Teslemetry energy site metric sensors.""" + + entity_description: SensorEntityDescription + + def __init__( + self, + energysite: TeslemetryEnergyData, + description: SensorEntityDescription, + ) -> None: + """Initialize the sensor.""" + super().__init__(energysite, description.key) + self.entity_description = description + + @property + def native_value(self) -> StateType: + """Return the state of the sensor.""" + return self.get() + + +class TeslemetryWallConnectorSensorEntity(TeslemetryWallConnectorEntity, SensorEntity): + """Base class for Teslemetry energy site metric sensors.""" + + entity_description: SensorEntityDescription + + def __init__( + self, + energysite: TeslemetryEnergyData, + din: str, + description: SensorEntityDescription, + ) -> None: + """Initialize the sensor.""" + super().__init__( + energysite, + din, + description.key, + ) + self.entity_description = description + + @property + def native_value(self) -> StateType: + """Return the state of the sensor.""" + return self._value diff --git a/homeassistant/components/teslemetry/strings.json b/homeassistant/components/teslemetry/strings.json index 95b2266b2dd..fa4419fbfcb 100644 --- a/homeassistant/components/teslemetry/strings.json +++ b/homeassistant/components/teslemetry/strings.json @@ -30,6 +30,154 @@ } } } + }, + "sensor": { + "battery_power": { + "name": "Battery power" + }, + "charge_state_battery_range": { + "name": "Battery range" + }, + "charge_state_est_battery_range": { + "name": "Estimate battery range" + }, + "charge_state_ideal_battery_range": { + "name": "Ideal battery range" + }, + "charge_state_charge_energy_added": { + "name": "Charge energy added" + }, + "charge_state_charge_rate": { + "name": "Charge rate" + }, + "charge_state_charger_actual_current": { + "name": "Charger current" + }, + "charge_state_charger_power": { + "name": "Charger power" + }, + "charge_state_charger_voltage": { + "name": "Charger voltage" + }, + "charge_state_conn_charge_cable": { + "name": "Charge cable" + }, + "charge_state_fast_charger_type": { + "name": "Fast charger type" + }, + "charge_state_charging_state": { + "name": "Charging", + "state": { + "starting": "Starting", + "charging": "Charging", + "disconnected": "Disconnected", + "stopped": "Stopped", + "complete": "Complete", + "no_power": "No power" + } + }, + "charge_state_minutes_to_full_charge": { + "name": "Time to full charge" + }, + "charge_state_battery_level": { + "name": "Battery level" + }, + "charge_state_usable_battery_level": { + "name": "Usable battery level" + }, + "climate_state_driver_temp_setting": { + "name": "Driver temperature setting" + }, + "climate_state_inside_temp": { + "name": "Inside temperature" + }, + "climate_state_outside_temp": { + "name": "Outside temperature" + }, + "climate_state_passenger_temp_setting": { + "name": "Passenger temperature setting" + }, + "drive_state_active_route_destination": { + "name": "Destination" + }, + "drive_state_active_route_energy_at_arrival": { + "name": "State of charge at arrival" + }, + "drive_state_active_route_miles_to_arrival": { + "name": "Distance to arrival" + }, + "drive_state_active_route_minutes_to_arrival": { + "name": "Time to arrival" + }, + "drive_state_active_route_traffic_minutes_delay": { + "name": "Traffic delay" + }, + "drive_state_power": { + "name": "Power" + }, + "drive_state_shift_state": { + "name": "Shift state", + "state": { + "d": "Drive", + "n": "Neutral", + "p": "Park", + "r": "Reverse" + } + }, + "drive_state_speed": { + "name": "Speed" + }, + "energy_left": { + "name": "Energy left" + }, + "generator_power": { + "name": "Generator power" + }, + "grid_power": { + "name": "Grid power" + }, + "grid_services_power": { + "name": "Grid services power" + }, + "load_power": { + "name": "Load power" + }, + "percentage_charged": { + "name": "Percentage charged" + }, + "solar_power": { + "name": "Solar power" + }, + "total_pack_energy": { + "name": "Total pack energy" + }, + "vehicle_state_odometer": { + "name": "Odometer" + }, + "vehicle_state_tpms_pressure_fl": { + "name": "Tire pressure front left" + }, + "vehicle_state_tpms_pressure_fr": { + "name": "Tire pressure front right" + }, + "vehicle_state_tpms_pressure_rl": { + "name": "Tire pressure rear left" + }, + "vehicle_state_tpms_pressure_rr": { + "name": "Tire pressure rear right" + }, + "vin": { + "name": "Vehicle" + }, + "wall_connector_fault_state": { + "name": "Fault state code" + }, + "wall_connector_power": { + "name": "Power" + }, + "wall_connector_state": { + "name": "State code" + } } } } diff --git a/homeassistant/components/tessie/__init__.py b/homeassistant/components/tessie/__init__.py index 869cd46cf51..6ac96fe8865 100644 --- a/homeassistant/components/tessie/__init__.py +++ b/homeassistant/components/tessie/__init__.py @@ -1,4 +1,5 @@ """Tessie integration.""" + from http import HTTPStatus import logging diff --git a/homeassistant/components/tessie/binary_sensor.py b/homeassistant/components/tessie/binary_sensor.py index 34d80b4f932..9b7d6861dfb 100644 --- a/homeassistant/components/tessie/binary_sensor.py +++ b/homeassistant/components/tessie/binary_sensor.py @@ -1,4 +1,5 @@ """Binary Sensor platform for Tessie integration.""" + from __future__ import annotations from collections.abc import Callable @@ -33,7 +34,7 @@ DESCRIPTIONS: tuple[TessieBinarySensorEntityDescription, ...] = ( is_on=lambda x: x == TessieState.ONLINE, ), TessieBinarySensorEntityDescription( - key="charge_state_battery_heater_on", + key="climate_state_battery_heater", device_class=BinarySensorDeviceClass.HEAT, entity_category=EntityCategory.DIAGNOSTIC, ), diff --git a/homeassistant/components/tessie/button.py b/homeassistant/components/tessie/button.py index 62bf6f79a6e..c357863bc4b 100644 --- a/homeassistant/components/tessie/button.py +++ b/homeassistant/components/tessie/button.py @@ -1,4 +1,5 @@ """Button platform for Tessie integration.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/tessie/climate.py b/homeassistant/components/tessie/climate.py index c856e8211cc..4c763726851 100644 --- a/homeassistant/components/tessie/climate.py +++ b/homeassistant/components/tessie/climate.py @@ -1,4 +1,5 @@ """Climate platform for Tessie integration.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/tessie/config_flow.py b/homeassistant/components/tessie/config_flow.py index 97d9d44af70..5ab7280a90c 100644 --- a/homeassistant/components/tessie/config_flow.py +++ b/homeassistant/components/tessie/config_flow.py @@ -1,4 +1,5 @@ """Config Flow for Tessie integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -9,10 +10,8 @@ from aiohttp import ClientConnectionError, ClientResponseError from tessie_api import get_state_of_all_vehicles import voluptuous as vol -from homeassistant import config_entries -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_ACCESS_TOKEN -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN @@ -23,7 +22,7 @@ DESCRIPTION_PLACEHOLDERS = { } -class TessieConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class TessieConfigFlow(ConfigFlow, domain=DOMAIN): """Config Tessie API connection.""" VERSION = 1 @@ -34,7 +33,7 @@ class TessieConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: Mapping[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Get configuration from the user.""" errors: dict[str, str] = {} if user_input: @@ -64,7 +63,9 @@ class TessieConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_reauth(self, user_input: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, user_input: Mapping[str, Any] + ) -> ConfigFlowResult: """Handle re-auth.""" self._reauth_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] @@ -73,7 +74,7 @@ class TessieConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_reauth_confirm( self, user_input: Mapping[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Get update API Key from the user.""" errors: dict[str, str] = {} assert self._reauth_entry diff --git a/homeassistant/components/tessie/const.py b/homeassistant/components/tessie/const.py index 8ec063bf47c..f717d758f5a 100644 --- a/homeassistant/components/tessie/const.py +++ b/homeassistant/components/tessie/const.py @@ -1,4 +1,5 @@ """Constants used by Tessie integration.""" + from __future__ import annotations from enum import IntEnum, StrEnum diff --git a/homeassistant/components/tessie/coordinator.py b/homeassistant/components/tessie/coordinator.py index c2106af665f..19d2d2c4869 100644 --- a/homeassistant/components/tessie/coordinator.py +++ b/homeassistant/components/tessie/coordinator.py @@ -1,4 +1,5 @@ """Tessie Data Coordinator.""" + from datetime import timedelta from http import HTTPStatus import logging diff --git a/homeassistant/components/tessie/cover.py b/homeassistant/components/tessie/cover.py index 6b4393fce1f..8d275559007 100644 --- a/homeassistant/components/tessie/cover.py +++ b/homeassistant/components/tessie/cover.py @@ -1,4 +1,5 @@ """Cover platform for Tessie integration.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/tessie/device_tracker.py b/homeassistant/components/tessie/device_tracker.py index 9b1ddfcfe4f..da979e5fc31 100644 --- a/homeassistant/components/tessie/device_tracker.py +++ b/homeassistant/components/tessie/device_tracker.py @@ -1,4 +1,5 @@ """Device Tracker platform for Tessie integration.""" + from __future__ import annotations from homeassistant.components.device_tracker import SourceType diff --git a/homeassistant/components/tessie/entity.py b/homeassistant/components/tessie/entity.py index 718a7050953..e11a99348ed 100644 --- a/homeassistant/components/tessie/entity.py +++ b/homeassistant/components/tessie/entity.py @@ -69,7 +69,6 @@ class TessieEntity(CoordinatorEntity[TessieStateUpdateCoordinator]): name: str = getattr(self, "name", self.entity_id) reason: str = response.get("reason", "unknown") raise HomeAssistantError( - reason.replace("_", " "), translation_domain=DOMAIN, translation_key=reason.replace(" ", "_"), translation_placeholders={"name": name}, diff --git a/homeassistant/components/tessie/lock.py b/homeassistant/components/tessie/lock.py index 9a27e95c73e..09402055ee8 100644 --- a/homeassistant/components/tessie/lock.py +++ b/homeassistant/components/tessie/lock.py @@ -1,4 +1,5 @@ """Lock platform for Tessie integration.""" + from __future__ import annotations from typing import Any @@ -111,7 +112,6 @@ class TessieCableLockEntity(TessieEntity, LockEntity): async def async_lock(self, **kwargs: Any) -> None: """Charge cable Lock cannot be manually locked.""" raise ServiceValidationError( - "Insert cable to lock", translation_domain=DOMAIN, translation_key="no_cable", ) diff --git a/homeassistant/components/tessie/media_player.py b/homeassistant/components/tessie/media_player.py index c4392e1de1d..2b20bf89152 100644 --- a/homeassistant/components/tessie/media_player.py +++ b/homeassistant/components/tessie/media_player.py @@ -1,4 +1,5 @@ """Media Player platform for Tessie integration.""" + from __future__ import annotations from homeassistant.components.media_player import ( diff --git a/homeassistant/components/tessie/models.py b/homeassistant/components/tessie/models.py index 32466a6b2ac..c17947ed941 100644 --- a/homeassistant/components/tessie/models.py +++ b/homeassistant/components/tessie/models.py @@ -1,4 +1,5 @@ """The Tessie integration models.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/homeassistant/components/tessie/number.py b/homeassistant/components/tessie/number.py index ada088f1bd2..196ea877f61 100644 --- a/homeassistant/components/tessie/number.py +++ b/homeassistant/components/tessie/number.py @@ -1,4 +1,5 @@ """Number platform for Tessie integration.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/tessie/select.py b/homeassistant/components/tessie/select.py index 03436b44cfc..a7d8c42472d 100644 --- a/homeassistant/components/tessie/select.py +++ b/homeassistant/components/tessie/select.py @@ -1,4 +1,5 @@ """Select platform for Tessie integration.""" + from __future__ import annotations from tessie_api import set_seat_heat diff --git a/homeassistant/components/tessie/sensor.py b/homeassistant/components/tessie/sensor.py index 3e5a0a60aa3..dd893adb632 100644 --- a/homeassistant/components/tessie/sensor.py +++ b/homeassistant/components/tessie/sensor.py @@ -1,4 +1,5 @@ """Sensor platform for Tessie integration.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/tessie/strings.json b/homeassistant/components/tessie/strings.json index 62de4f276f4..8e1e47f934f 100644 --- a/homeassistant/components/tessie/strings.json +++ b/homeassistant/components/tessie/strings.json @@ -252,7 +252,7 @@ "state": { "name": "Status" }, - "charge_state_battery_heater_on": { + "climate_state_battery_heater": { "name": "Battery heater" }, "charge_state_charge_enable_request": { diff --git a/homeassistant/components/tessie/switch.py b/homeassistant/components/tessie/switch.py index b8ac2ede52b..225d65bf852 100644 --- a/homeassistant/components/tessie/switch.py +++ b/homeassistant/components/tessie/switch.py @@ -1,4 +1,5 @@ """Switch platform for Tessie integration.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/tessie/update.py b/homeassistant/components/tessie/update.py index 1d2fb59c492..77cb2a70de9 100644 --- a/homeassistant/components/tessie/update.py +++ b/homeassistant/components/tessie/update.py @@ -1,4 +1,5 @@ """Update platform for Tessie integration.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/text/__init__.py b/homeassistant/components/text/__init__.py index 89fad759f8b..cf29910cc34 100644 --- a/homeassistant/components/text/__init__.py +++ b/homeassistant/components/text/__init__.py @@ -1,4 +1,5 @@ """Component to allow setting text as platforms.""" + from __future__ import annotations from dataclasses import asdict, dataclass @@ -236,7 +237,7 @@ class TextEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): def set_value(self, value: str) -> None: """Change the value.""" - raise NotImplementedError() + raise NotImplementedError async def async_set_value(self, value: str) -> None: """Change the value.""" diff --git a/homeassistant/components/text/device_action.py b/homeassistant/components/text/device_action.py index 9d06d4b7441..94269ac12fb 100644 --- a/homeassistant/components/text/device_action.py +++ b/homeassistant/components/text/device_action.py @@ -1,4 +1,5 @@ """Provides device actions for Text.""" + from __future__ import annotations import voluptuous as vol diff --git a/homeassistant/components/text/reproduce_state.py b/homeassistant/components/text/reproduce_state.py index 99013a63a06..329ffd374dd 100644 --- a/homeassistant/components/text/reproduce_state.py +++ b/homeassistant/components/text/reproduce_state.py @@ -1,4 +1,5 @@ """Reproduce a Text entity state.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/tfiac/climate.py b/homeassistant/components/tfiac/climate.py index ea6a6f22d2b..74d2c3fbe7e 100644 --- a/homeassistant/components/tfiac/climate.py +++ b/homeassistant/components/tfiac/climate.py @@ -1,4 +1,5 @@ """Climate platform that offers a climate device for the TFIAC protocol.""" + from __future__ import annotations from concurrent import futures diff --git a/homeassistant/components/thermobeacon/__init__.py b/homeassistant/components/thermobeacon/__init__.py index 92b5ef4b4f6..073ff6bbdc3 100644 --- a/homeassistant/components/thermobeacon/__init__.py +++ b/homeassistant/components/thermobeacon/__init__.py @@ -1,4 +1,5 @@ """The ThermoBeacon integration.""" + from __future__ import annotations import logging @@ -25,14 +26,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: address = entry.unique_id assert address is not None data = ThermoBeaconBluetoothDeviceData() - coordinator = hass.data.setdefault(DOMAIN, {})[ - entry.entry_id - ] = PassiveBluetoothProcessorCoordinator( - hass, - _LOGGER, - address=address, - mode=BluetoothScanningMode.PASSIVE, - update_method=data.update, + coordinator = hass.data.setdefault(DOMAIN, {})[entry.entry_id] = ( + PassiveBluetoothProcessorCoordinator( + hass, + _LOGGER, + address=address, + mode=BluetoothScanningMode.PASSIVE, + update_method=data.update, + ) ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload( diff --git a/homeassistant/components/thermobeacon/config_flow.py b/homeassistant/components/thermobeacon/config_flow.py index 864e9532c0e..08994a41008 100644 --- a/homeassistant/components/thermobeacon/config_flow.py +++ b/homeassistant/components/thermobeacon/config_flow.py @@ -1,4 +1,5 @@ """Config flow for thermobeacon ble integration.""" + from __future__ import annotations from typing import Any @@ -10,9 +11,8 @@ from homeassistant.components.bluetooth import ( BluetoothServiceInfoBleak, async_discovered_service_info, ) -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_ADDRESS -from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN @@ -30,7 +30,7 @@ class ThermoBeaconConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_bluetooth( self, discovery_info: BluetoothServiceInfoBleak - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the bluetooth discovery step.""" await self.async_set_unique_id(discovery_info.address) self._abort_if_unique_id_configured() @@ -43,7 +43,7 @@ class ThermoBeaconConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_bluetooth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm discovery.""" assert self._discovered_device is not None device = self._discovered_device @@ -62,7 +62,7 @@ class ThermoBeaconConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the user step to pick discovered device.""" if user_input is not None: address = user_input[CONF_ADDRESS] diff --git a/homeassistant/components/thermobeacon/device.py b/homeassistant/components/thermobeacon/device.py index fe8a499d6ed..36af211876f 100644 --- a/homeassistant/components/thermobeacon/device.py +++ b/homeassistant/components/thermobeacon/device.py @@ -1,4 +1,5 @@ """Support for ThermoBeacon devices.""" + from __future__ import annotations from thermobeacon_ble import DeviceKey diff --git a/homeassistant/components/thermobeacon/sensor.py b/homeassistant/components/thermobeacon/sensor.py index c6fb978923e..6bf2e00c420 100644 --- a/homeassistant/components/thermobeacon/sensor.py +++ b/homeassistant/components/thermobeacon/sensor.py @@ -1,4 +1,5 @@ """Support for ThermoBeacon sensors.""" + from __future__ import annotations from thermobeacon_ble import ( diff --git a/homeassistant/components/thermopro/__init__.py b/homeassistant/components/thermopro/__init__.py index 7093484648f..2cd207818c5 100644 --- a/homeassistant/components/thermopro/__init__.py +++ b/homeassistant/components/thermopro/__init__.py @@ -1,4 +1,5 @@ """The ThermoPro Bluetooth integration.""" + from __future__ import annotations import logging @@ -25,14 +26,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: address = entry.unique_id assert address is not None data = ThermoProBluetoothDeviceData() - coordinator = hass.data.setdefault(DOMAIN, {})[ - entry.entry_id - ] = PassiveBluetoothProcessorCoordinator( - hass, - _LOGGER, - address=address, - mode=BluetoothScanningMode.ACTIVE, - update_method=data.update, + coordinator = hass.data.setdefault(DOMAIN, {})[entry.entry_id] = ( + PassiveBluetoothProcessorCoordinator( + hass, + _LOGGER, + address=address, + mode=BluetoothScanningMode.ACTIVE, + update_method=data.update, + ) ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload( diff --git a/homeassistant/components/thermopro/config_flow.py b/homeassistant/components/thermopro/config_flow.py index f7e03aff685..4d080c6e074 100644 --- a/homeassistant/components/thermopro/config_flow.py +++ b/homeassistant/components/thermopro/config_flow.py @@ -1,4 +1,5 @@ """Config flow for thermopro ble integration.""" + from __future__ import annotations from typing import Any @@ -10,9 +11,8 @@ from homeassistant.components.bluetooth import ( BluetoothServiceInfoBleak, async_discovered_service_info, ) -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_ADDRESS -from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN @@ -30,7 +30,7 @@ class ThermoProConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_bluetooth( self, discovery_info: BluetoothServiceInfoBleak - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the bluetooth discovery step.""" await self.async_set_unique_id(discovery_info.address) self._abort_if_unique_id_configured() @@ -43,7 +43,7 @@ class ThermoProConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_bluetooth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm discovery.""" assert self._discovered_device is not None device = self._discovered_device @@ -62,7 +62,7 @@ class ThermoProConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the user step to pick discovered device.""" if user_input is not None: address = user_input[CONF_ADDRESS] diff --git a/homeassistant/components/thermopro/sensor.py b/homeassistant/components/thermopro/sensor.py index 37cbf10323f..21915ca9998 100644 --- a/homeassistant/components/thermopro/sensor.py +++ b/homeassistant/components/thermopro/sensor.py @@ -1,4 +1,5 @@ """Support for thermopro ble sensors.""" + from __future__ import annotations from thermopro_ble import ( diff --git a/homeassistant/components/thermoworks_smoke/sensor.py b/homeassistant/components/thermoworks_smoke/sensor.py index 4b4878fa1c8..57621ba1055 100644 --- a/homeassistant/components/thermoworks_smoke/sensor.py +++ b/homeassistant/components/thermoworks_smoke/sensor.py @@ -2,6 +2,7 @@ Requires Smoke Gateway Wifi with an internet connection. """ + from __future__ import annotations import logging @@ -85,22 +86,21 @@ def setup_platform( try: mgr = thermoworks_smoke.initialize_app(email, password, True, excluded) - - # list of sensor devices - dev = [] - - # get list of registered devices - for serial in mgr.serials(): - for variable in monitored_variables: - dev.append(ThermoworksSmokeSensor(variable, serial, mgr)) - - add_entities(dev, True) except HTTPError as error: msg = f"{error.strerror}" if "EMAIL_NOT_FOUND" in msg or "INVALID_PASSWORD" in msg: _LOGGER.error("Invalid email and password combination") else: _LOGGER.error(msg) + else: + add_entities( + ( + ThermoworksSmokeSensor(variable, serial, mgr) + for serial in mgr.serials() + for variable in monitored_variables + ), + True, + ) class ThermoworksSmokeSensor(SensorEntity): @@ -159,9 +159,9 @@ class ThermoworksSmokeSensor(SensorEntity): if key and key not in EXCLUDE_KEYS: self._attr_extra_state_attributes[key] = val # store actual unit because attributes are not converted - self._attr_extra_state_attributes[ - "unit_of_min_max" - ] = self._attr_native_unit_of_measurement + self._attr_extra_state_attributes["unit_of_min_max"] = ( + self._attr_native_unit_of_measurement + ) except (RequestException, ValueError, KeyError): _LOGGER.warning("Could not update status for %s", self.name) diff --git a/homeassistant/components/thethingsnetwork/__init__.py b/homeassistant/components/thethingsnetwork/__init__.py index af0f9965f56..32850d05e57 100644 --- a/homeassistant/components/thethingsnetwork/__init__.py +++ b/homeassistant/components/thethingsnetwork/__init__.py @@ -1,4 +1,5 @@ """Support for The Things network.""" + import voluptuous as vol from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/thethingsnetwork/sensor.py b/homeassistant/components/thethingsnetwork/sensor.py index b9568a979fa..ae4fed8600e 100644 --- a/homeassistant/components/thethingsnetwork/sensor.py +++ b/homeassistant/components/thethingsnetwork/sensor.py @@ -1,4 +1,5 @@ """Support for The Things Network's Data storage integration.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/thingspeak/__init__.py b/homeassistant/components/thingspeak/__init__.py index e4788674845..fdf06a9709a 100644 --- a/homeassistant/components/thingspeak/__init__.py +++ b/homeassistant/components/thingspeak/__init__.py @@ -1,4 +1,5 @@ """Support for submitting data to Thingspeak.""" + import logging from requests.exceptions import RequestException diff --git a/homeassistant/components/thinkingcleaner/sensor.py b/homeassistant/components/thinkingcleaner/sensor.py index b56c9f61135..86c5a8813d8 100644 --- a/homeassistant/components/thinkingcleaner/sensor.py +++ b/homeassistant/components/thinkingcleaner/sensor.py @@ -1,4 +1,5 @@ """Support for ThinkingCleaner sensors.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/thinkingcleaner/switch.py b/homeassistant/components/thinkingcleaner/switch.py index 1befc53ffff..f99cda4347a 100644 --- a/homeassistant/components/thinkingcleaner/switch.py +++ b/homeassistant/components/thinkingcleaner/switch.py @@ -1,4 +1,5 @@ """Support for ThinkingCleaner switches.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/thomson/device_tracker.py b/homeassistant/components/thomson/device_tracker.py index 0ad2942fb04..2ba5505c6f3 100644 --- a/homeassistant/components/thomson/device_tracker.py +++ b/homeassistant/components/thomson/device_tracker.py @@ -1,4 +1,5 @@ """Support for THOMSON routers.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/thread/__init__.py b/homeassistant/components/thread/__init__.py index dd2527763ad..65a59e43f31 100644 --- a/homeassistant/components/thread/__init__.py +++ b/homeassistant/components/thread/__init__.py @@ -1,4 +1,5 @@ """The Thread integration.""" + from __future__ import annotations from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry diff --git a/homeassistant/components/thread/config_flow.py b/homeassistant/components/thread/config_flow.py index b294dfa51e7..b4b6eac0fc8 100644 --- a/homeassistant/components/thread/config_flow.py +++ b/homeassistant/components/thread/config_flow.py @@ -1,11 +1,11 @@ """Config flow for the Thread integration.""" + from __future__ import annotations from typing import Any from homeassistant.components import onboarding, zeroconf -from homeassistant.config_entries import ConfigFlow -from homeassistant.data_entry_flow import FlowResult +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from .const import DOMAIN @@ -17,28 +17,28 @@ class ThreadConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_import( self, import_data: dict[str, str] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Set up by import from async_setup.""" await self._async_handle_discovery_without_unique_id() return self.async_create_entry(title="Thread", data={}) async def async_step_user( self, user_input: dict[str, str] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Set up by import from async_setup.""" await self._async_handle_discovery_without_unique_id() return self.async_create_entry(title="Thread", data={}) async def async_step_zeroconf( self, discovery_info: zeroconf.ZeroconfServiceInfo - ) -> FlowResult: + ) -> ConfigFlowResult: """Set up because the user has border routers.""" await self._async_handle_discovery_without_unique_id() return await self.async_step_confirm() async def async_step_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm the setup.""" if user_input is not None or not onboarding.async_is_onboarded(self.hass): return self.async_create_entry(title="Thread", data={}) diff --git a/homeassistant/components/thread/dataset_store.py b/homeassistant/components/thread/dataset_store.py index b5a3b39ae26..de322510ef2 100644 --- a/homeassistant/components/thread/dataset_store.py +++ b/homeassistant/components/thread/dataset_store.py @@ -1,4 +1,5 @@ """Persistently store thread datasets.""" + from __future__ import annotations from asyncio import Event, Task, wait diff --git a/homeassistant/components/thread/discovery.py b/homeassistant/components/thread/discovery.py index ad1df757af4..49a77e9c87b 100644 --- a/homeassistant/components/thread/discovery.py +++ b/homeassistant/components/thread/discovery.py @@ -1,4 +1,5 @@ """The Thread integration.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/thread/manifest.json b/homeassistant/components/thread/manifest.json index 19d8fa76c66..65d4c9d044c 100644 --- a/homeassistant/components/thread/manifest.json +++ b/homeassistant/components/thread/manifest.json @@ -5,7 +5,6 @@ "config_flow": true, "dependencies": ["zeroconf"], "documentation": "https://www.home-assistant.io/integrations/thread", - "import_executor": true, "integration_type": "service", "iot_class": "local_polling", "requirements": ["python-otbr-api==2.6.0", "pyroute2==0.7.5"], diff --git a/homeassistant/components/thread/websocket_api.py b/homeassistant/components/thread/websocket_api.py index 9dd1971f91c..687c4067caf 100644 --- a/homeassistant/components/thread/websocket_api.py +++ b/homeassistant/components/thread/websocket_api.py @@ -1,4 +1,5 @@ """The thread websocket API.""" + from __future__ import annotations from typing import Any @@ -165,23 +166,22 @@ async def ws_list_datasets( """Get a list of thread datasets.""" store = await dataset_store.async_get_store(hass) - result = [] preferred_dataset = store.preferred_dataset - for dataset in store.datasets.values(): - result.append( - { - "channel": dataset.channel, - "created": dataset.created, - "dataset_id": dataset.id, - "extended_pan_id": dataset.extended_pan_id, - "network_name": dataset.network_name, - "pan_id": dataset.pan_id, - "preferred": dataset.id == preferred_dataset, - "preferred_border_agent_id": dataset.preferred_border_agent_id, - "preferred_extended_address": dataset.preferred_extended_address, - "source": dataset.source, - } - ) + result = [ + { + "channel": dataset.channel, + "created": dataset.created, + "dataset_id": dataset.id, + "extended_pan_id": dataset.extended_pan_id, + "network_name": dataset.network_name, + "pan_id": dataset.pan_id, + "preferred": dataset.id == preferred_dataset, + "preferred_border_agent_id": dataset.preferred_border_agent_id, + "preferred_extended_address": dataset.preferred_extended_address, + "source": dataset.source, + } + for dataset in store.datasets.values() + ] connection.send_result(msg["id"], {"datasets": result}) diff --git a/homeassistant/components/threshold/binary_sensor.py b/homeassistant/components/threshold/binary_sensor.py index 6382c79b9ce..364511ca291 100644 --- a/homeassistant/components/threshold/binary_sensor.py +++ b/homeassistant/components/threshold/binary_sensor.py @@ -1,4 +1,5 @@ """Support for monitoring if a sensor value is below/above a threshold.""" + from __future__ import annotations import logging @@ -21,7 +22,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import Event, HomeAssistant, callback from homeassistant.helpers import ( config_validation as cv, device_registry as dr, @@ -33,7 +34,7 @@ from homeassistant.helpers.event import ( EventStateChangedData, async_track_state_change_event, ) -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, EventType +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import CONF_HYSTERESIS, CONF_LOWER, CONF_UPPER @@ -214,7 +215,7 @@ class ThresholdSensor(BinarySensorEntity): @callback def async_threshold_sensor_state_listener( - event: EventType[EventStateChangedData], + event: Event[EventStateChangedData], ) -> None: """Handle sensor state changes.""" _update_sensor_state() diff --git a/homeassistant/components/threshold/config_flow.py b/homeassistant/components/threshold/config_flow.py index 31d51fee3f3..a8e330cab38 100644 --- a/homeassistant/components/threshold/config_flow.py +++ b/homeassistant/components/threshold/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Threshold integration.""" + from __future__ import annotations from collections.abc import Mapping diff --git a/homeassistant/components/tibber/__init__.py b/homeassistant/components/tibber/__init__.py index 52db8421781..7305cf835c5 100644 --- a/homeassistant/components/tibber/__init__.py +++ b/homeassistant/components/tibber/__init__.py @@ -1,4 +1,5 @@ """Support for Tibber.""" + import logging import aiohttp diff --git a/homeassistant/components/tibber/config_flow.py b/homeassistant/components/tibber/config_flow.py index 8c926c5cc81..abee3ea50bc 100644 --- a/homeassistant/components/tibber/config_flow.py +++ b/homeassistant/components/tibber/config_flow.py @@ -1,4 +1,5 @@ """Adds config flow for Tibber integration.""" + from __future__ import annotations from typing import Any @@ -7,9 +8,8 @@ import aiohttp import tibber import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_ACCESS_TOKEN -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN @@ -21,14 +21,14 @@ ERR_TOKEN = "invalid_access_token" TOKEN_URL = "https://developer.tibber.com/settings/access-token" -class TibberConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class TibberConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Tibber integration.""" VERSION = 1 async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" self._async_abort_entries_match() diff --git a/homeassistant/components/tibber/diagnostics.py b/homeassistant/components/tibber/diagnostics.py index 991d20e9e2d..2306aac23e1 100644 --- a/homeassistant/components/tibber/diagnostics.py +++ b/homeassistant/components/tibber/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for Tibber.""" + from __future__ import annotations from typing import Any @@ -17,11 +18,8 @@ async def async_get_config_entry_diagnostics( """Return diagnostics for a config entry.""" tibber_connection: tibber.Tibber = hass.data[DOMAIN] - diagnostics_data = {} - - homes = [] - for home in tibber_connection.get_homes(only_active=False): - homes.append( + return { + "homes": [ { "last_data_timestamp": home.last_data_timestamp, "has_active_subscription": home.has_active_subscription, @@ -29,7 +27,6 @@ async def async_get_config_entry_diagnostics( "last_cons_data_timestamp": home.last_cons_data_timestamp, "country": home.country, } - ) - diagnostics_data["homes"] = homes - - return diagnostics_data + for home in tibber_connection.get_homes(only_active=False) + ] + } diff --git a/homeassistant/components/tibber/notify.py b/homeassistant/components/tibber/notify.py index 997afa62359..b0816de39e2 100644 --- a/homeassistant/components/tibber/notify.py +++ b/homeassistant/components/tibber/notify.py @@ -1,4 +1,5 @@ """Support for Tibber notifications.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/tibber/sensor.py b/homeassistant/components/tibber/sensor.py index a2bd8d26f75..da2fd881a54 100644 --- a/homeassistant/components/tibber/sensor.py +++ b/homeassistant/components/tibber/sensor.py @@ -1,4 +1,5 @@ """Support for Tibber sensors.""" + from __future__ import annotations import datetime @@ -285,17 +286,19 @@ async def async_setup_entry( await home.update_info() except TimeoutError as err: _LOGGER.error("Timeout connecting to Tibber home: %s ", err) - raise PlatformNotReady() from err + raise PlatformNotReady from err except aiohttp.ClientError as err: _LOGGER.error("Error connecting to Tibber home: %s ", err) - raise PlatformNotReady() from err + raise PlatformNotReady from err if home.has_active_subscription: entities.append(TibberSensorElPrice(home)) if coordinator is None: coordinator = TibberDataCoordinator(hass, tibber_connection) - for entity_description in SENSORS: - entities.append(TibberDataSensor(home, coordinator, entity_description)) + entities.extend( + TibberDataSensor(home, coordinator, entity_description) + for entity_description in SENSORS + ) if home.has_real_time_consumption: await home.rt_subscribe( diff --git a/homeassistant/components/tikteck/light.py b/homeassistant/components/tikteck/light.py index 7022138d147..93549b26f48 100644 --- a/homeassistant/components/tikteck/light.py +++ b/homeassistant/components/tikteck/light.py @@ -1,4 +1,5 @@ """Support for Tikteck lights.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/tile/__init__.py b/homeassistant/components/tile/__init__.py index 1e8cebdd5a6..7dbeea1a4f3 100644 --- a/homeassistant/components/tile/__init__.py +++ b/homeassistant/components/tile/__init__.py @@ -1,4 +1,5 @@ """The Tile component.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/homeassistant/components/tile/config_flow.py b/homeassistant/components/tile/config_flow.py index 3ba1dc411ae..108d9b1b300 100644 --- a/homeassistant/components/tile/config_flow.py +++ b/homeassistant/components/tile/config_flow.py @@ -1,4 +1,5 @@ """Config flow to configure the Tile integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -8,9 +9,8 @@ from pytile import async_login from pytile.errors import InvalidAuthError, TileError import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import aiohttp_client from .const import DOMAIN, LOGGER @@ -29,7 +29,7 @@ STEP_USER_SCHEMA = vol.Schema( ) -class TileFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class TileFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a Tile config flow.""" VERSION = 1 @@ -39,7 +39,7 @@ class TileFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self._password: str | None = None self._username: str | None = None - async def _async_verify(self, step_id: str, schema: vol.Schema) -> FlowResult: + async def _async_verify(self, step_id: str, schema: vol.Schema) -> ConfigFlowResult: """Attempt to authenticate the provided credentials.""" assert self._username assert self._password @@ -71,18 +71,22 @@ class TileFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_create_entry(title=self._username, data=data) - async def async_step_import(self, import_config: dict[str, Any]) -> FlowResult: + async def async_step_import( + self, import_config: dict[str, Any] + ) -> ConfigFlowResult: """Import a config entry from configuration.yaml.""" return await self.async_step_user(import_config) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Handle configuration by re-auth.""" self._username = entry_data[CONF_USERNAME] return await self.async_step_reauth_confirm() async def async_step_reauth_confirm( self, user_input: dict[str, str] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle re-auth completion.""" if not user_input: return self.async_show_form( @@ -95,7 +99,7 @@ class TileFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the start of the config flow.""" if not user_input: return self.async_show_form(step_id="user", data_schema=STEP_USER_SCHEMA) diff --git a/homeassistant/components/tile/const.py b/homeassistant/components/tile/const.py index eed3eb698ef..50b02cff963 100644 --- a/homeassistant/components/tile/const.py +++ b/homeassistant/components/tile/const.py @@ -1,4 +1,5 @@ """Define Tile constants.""" + import logging DOMAIN = "tile" diff --git a/homeassistant/components/tile/device_tracker.py b/homeassistant/components/tile/device_tracker.py index df166d17db5..b33c2c592b8 100644 --- a/homeassistant/components/tile/device_tracker.py +++ b/homeassistant/components/tile/device_tracker.py @@ -1,4 +1,5 @@ """Support for Tile device trackers.""" + from __future__ import annotations import logging @@ -37,8 +38,6 @@ ATTR_RING_STATE = "ring_state" ATTR_TILE_NAME = "tile_name" ATTR_VOIP_STATE = "voip_state" -DEFAULT_ICON = "mdi:view-grid" - async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback @@ -83,9 +82,9 @@ async def async_setup_scanner( class TileDeviceTracker(CoordinatorEntity[DataUpdateCoordinator[None]], TrackerEntity): """Representation of a network infrastructure device.""" - _attr_icon = DEFAULT_ICON _attr_has_entity_name = True _attr_name = None + _attr_translation_key = "tile" def __init__( self, entry: ConfigEntry, coordinator: DataUpdateCoordinator[None], tile: Tile diff --git a/homeassistant/components/tile/diagnostics.py b/homeassistant/components/tile/diagnostics.py index dda2c3367e3..22991ef24c1 100644 --- a/homeassistant/components/tile/diagnostics.py +++ b/homeassistant/components/tile/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for Tile.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/tile/icons.json b/homeassistant/components/tile/icons.json new file mode 100644 index 00000000000..f6f38fe8cef --- /dev/null +++ b/homeassistant/components/tile/icons.json @@ -0,0 +1,9 @@ +{ + "entity": { + "device_tracker": { + "tile": { + "default": "mdi:view-grid" + } + } + } +} diff --git a/homeassistant/components/tile/manifest.json b/homeassistant/components/tile/manifest.json index 6f311fc5593..8dceddcb77f 100644 --- a/homeassistant/components/tile/manifest.json +++ b/homeassistant/components/tile/manifest.json @@ -7,5 +7,5 @@ "integration_type": "hub", "iot_class": "cloud_polling", "loggers": ["pytile"], - "requirements": ["pytile==2023.04.0"] + "requirements": ["pytile==2023.12.0"] } diff --git a/homeassistant/components/tilt_ble/__init__.py b/homeassistant/components/tilt_ble/__init__.py index eebb3660368..9ac2cb91aff 100644 --- a/homeassistant/components/tilt_ble/__init__.py +++ b/homeassistant/components/tilt_ble/__init__.py @@ -1,4 +1,5 @@ """The tilt_ble integration.""" + from __future__ import annotations import logging @@ -25,14 +26,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: address = entry.unique_id assert address is not None data = TiltBluetoothDeviceData() - coordinator = hass.data.setdefault(DOMAIN, {})[ - entry.entry_id - ] = PassiveBluetoothProcessorCoordinator( - hass, - _LOGGER, - address=address, - mode=BluetoothScanningMode.ACTIVE, - update_method=data.update, + coordinator = hass.data.setdefault(DOMAIN, {})[entry.entry_id] = ( + PassiveBluetoothProcessorCoordinator( + hass, + _LOGGER, + address=address, + mode=BluetoothScanningMode.ACTIVE, + update_method=data.update, + ) ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload( diff --git a/homeassistant/components/tilt_ble/config_flow.py b/homeassistant/components/tilt_ble/config_flow.py index d534622eb7b..5c1f9721aae 100644 --- a/homeassistant/components/tilt_ble/config_flow.py +++ b/homeassistant/components/tilt_ble/config_flow.py @@ -1,4 +1,5 @@ """Config flow for tilt_ble.""" + from __future__ import annotations from typing import Any @@ -10,9 +11,8 @@ from homeassistant.components.bluetooth import ( BluetoothServiceInfoBleak, async_discovered_service_info, ) -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_ADDRESS -from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN @@ -30,7 +30,7 @@ class TiltConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_bluetooth( self, discovery_info: BluetoothServiceInfoBleak - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the bluetooth discovery step.""" await self.async_set_unique_id(discovery_info.address) self._abort_if_unique_id_configured() @@ -43,7 +43,7 @@ class TiltConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_bluetooth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm discovery.""" assert self._discovered_device is not None device = self._discovered_device @@ -62,7 +62,7 @@ class TiltConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the user step to pick discovered device.""" if user_input is not None: address = user_input[CONF_ADDRESS] diff --git a/homeassistant/components/tilt_ble/sensor.py b/homeassistant/components/tilt_ble/sensor.py index 7edfec3643f..380bb90ca15 100644 --- a/homeassistant/components/tilt_ble/sensor.py +++ b/homeassistant/components/tilt_ble/sensor.py @@ -1,4 +1,5 @@ """Support for Tilt Hydrometers.""" + from __future__ import annotations from tilt_ble import DeviceClass, DeviceKey, SensorUpdate, Units diff --git a/homeassistant/components/time/__init__.py b/homeassistant/components/time/__init__.py index 387c42f0852..2e87aaac28d 100644 --- a/homeassistant/components/time/__init__.py +++ b/homeassistant/components/time/__init__.py @@ -1,4 +1,5 @@ """Component to allow setting time as platforms.""" + from __future__ import annotations from datetime import time, timedelta @@ -109,7 +110,7 @@ class TimeEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): def set_value(self, value: time) -> None: """Change the time.""" - raise NotImplementedError() + raise NotImplementedError async def async_set_value(self, value: time) -> None: """Change the time.""" diff --git a/homeassistant/components/time_date/__init__.py b/homeassistant/components/time_date/__init__.py index cdd69a2bc1f..151f5c6b39f 100644 --- a/homeassistant/components/time_date/__init__.py +++ b/homeassistant/components/time_date/__init__.py @@ -1,4 +1,5 @@ """The time_date component.""" + from __future__ import annotations from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/time_date/config_flow.py b/homeassistant/components/time_date/config_flow.py index 09a5f2503d0..f65978144c6 100644 --- a/homeassistant/components/time_date/config_flow.py +++ b/homeassistant/components/time_date/config_flow.py @@ -1,4 +1,5 @@ """Adds config flow for Time & Date integration.""" + from __future__ import annotations from collections.abc import Mapping diff --git a/homeassistant/components/time_date/const.py b/homeassistant/components/time_date/const.py index dde9497b9a3..5d13ec0203c 100644 --- a/homeassistant/components/time_date/const.py +++ b/homeassistant/components/time_date/const.py @@ -1,4 +1,5 @@ """Constants for the Time & Date integration.""" + from __future__ import annotations from typing import Final diff --git a/homeassistant/components/time_date/sensor.py b/homeassistant/components/time_date/sensor.py index bd0f9449aea..57bb87e6ea5 100644 --- a/homeassistant/components/time_date/sensor.py +++ b/homeassistant/components/time_date/sensor.py @@ -1,4 +1,5 @@ """Support for showing the date and the time.""" + from __future__ import annotations from collections.abc import Callable, Mapping diff --git a/homeassistant/components/timer/__init__.py b/homeassistant/components/timer/__init__.py index 4c611962436..72e93f5655a 100644 --- a/homeassistant/components/timer/__init__.py +++ b/homeassistant/components/timer/__init__.py @@ -1,4 +1,5 @@ """Support for Timers.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/timer/icons.json b/homeassistant/components/timer/icons.json new file mode 100644 index 00000000000..1e352f7280b --- /dev/null +++ b/homeassistant/components/timer/icons.json @@ -0,0 +1,10 @@ +{ + "services": { + "start": "mdi:play", + "pause": "mdi:pause", + "cancel": "mdi:cancel", + "finish": "mdi:check", + "change": "mdi:pencil", + "reload": "mdi:reload" + } +} diff --git a/homeassistant/components/timer/reproduce_state.py b/homeassistant/components/timer/reproduce_state.py index 5628c0b4bbc..3bdee08016c 100644 --- a/homeassistant/components/timer/reproduce_state.py +++ b/homeassistant/components/timer/reproduce_state.py @@ -1,4 +1,5 @@ """Reproduce an Timer state.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/tmb/sensor.py b/homeassistant/components/tmb/sensor.py index 7fe8630cc98..4ec86434ea0 100644 --- a/homeassistant/components/tmb/sensor.py +++ b/homeassistant/components/tmb/sensor.py @@ -1,4 +1,5 @@ """Support for TMB (Transports Metropolitans de Barcelona) Barcelona public transport.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/tod/__init__.py b/homeassistant/components/tod/__init__.py index e404826534e..4f3f365ea59 100644 --- a/homeassistant/components/tod/__init__.py +++ b/homeassistant/components/tod/__init__.py @@ -1,4 +1,5 @@ """The Times of the Day integration.""" + from __future__ import annotations from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/tod/binary_sensor.py b/homeassistant/components/tod/binary_sensor.py index d274960c211..c35f92fd27f 100644 --- a/homeassistant/components/tod/binary_sensor.py +++ b/homeassistant/components/tod/binary_sensor.py @@ -1,4 +1,5 @@ """Support for representing current time of the day as binary sensors.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/tod/config_flow.py b/homeassistant/components/tod/config_flow.py index 6e21b8046a1..0bbd5a528af 100644 --- a/homeassistant/components/tod/config_flow.py +++ b/homeassistant/components/tod/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Times of the Day integration.""" + from __future__ import annotations from collections.abc import Mapping diff --git a/homeassistant/components/todo/__init__.py b/homeassistant/components/todo/__init__.py index afcb8e28f74..74ee99b811f 100644 --- a/homeassistant/components/todo/__init__.py +++ b/homeassistant/components/todo/__init__.py @@ -107,7 +107,6 @@ def _validate_supported_features( continue if not supported_features or not supported_features & desc.required_feature: raise ServiceValidationError( - f"Entity does not support setting field '{desc.service_field}'", translation_domain=DOMAIN, translation_key="update_field_not_supported", translation_placeholders={"service_field": desc.service_field}, @@ -261,15 +260,15 @@ class TodoListEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): async def async_create_todo_item(self, item: TodoItem) -> None: """Add an item to the To-do list.""" - raise NotImplementedError() + raise NotImplementedError async def async_update_todo_item(self, item: TodoItem) -> None: """Update an item in the To-do list.""" - raise NotImplementedError() + raise NotImplementedError async def async_delete_todo_items(self, uids: list[str]) -> None: """Delete an item in the To-do list.""" - raise NotImplementedError() + raise NotImplementedError async def async_move_todo_item( self, uid: str, previous_uid: str | None = None @@ -280,7 +279,7 @@ class TodoListEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): in the list after the specified by `previous_uid` or `None` for the first position in the To-do list. """ - raise NotImplementedError() + raise NotImplementedError @final @callback @@ -485,7 +484,6 @@ async def _async_update_todo_item(entity: TodoListEntity, call: ServiceCall) -> found = _find_by_uid_or_summary(item, entity.todo_items) if not found: raise ServiceValidationError( - f"Unable to find To-do item '{item}'", translation_domain=DOMAIN, translation_key="item_not_found", translation_placeholders={"item": item}, @@ -518,7 +516,6 @@ async def _async_remove_todo_items(entity: TodoListEntity, call: ServiceCall) -> found = _find_by_uid_or_summary(item, entity.todo_items) if not found or not found.uid: raise ServiceValidationError( - f"Unable to find To-do item '{item}'", translation_domain=DOMAIN, translation_key="item_not_found", translation_placeholders={"item": item}, diff --git a/homeassistant/components/todo/intent.py b/homeassistant/components/todo/intent.py index 2cce9da9c0f..81d5ca2ae0c 100644 --- a/homeassistant/components/todo/intent.py +++ b/homeassistant/components/todo/intent.py @@ -1,4 +1,5 @@ """Intents for the todo integration.""" + from __future__ import annotations from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/todoist/calendar.py b/homeassistant/components/todoist/calendar.py index 40ceb71ee5f..9b8d0a7c08f 100644 --- a/homeassistant/components/todoist/calendar.py +++ b/homeassistant/components/todoist/calendar.py @@ -1,4 +1,5 @@ """Support for Todoist task management (https://todoist.com).""" + from __future__ import annotations from datetime import date, datetime, timedelta diff --git a/homeassistant/components/todoist/config_flow.py b/homeassistant/components/todoist/config_flow.py index 94b4ad31826..745f1775e87 100644 --- a/homeassistant/components/todoist/config_flow.py +++ b/homeassistant/components/todoist/config_flow.py @@ -8,9 +8,8 @@ from requests.exceptions import HTTPError from todoist_api_python.api_async import TodoistAPIAsync import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_TOKEN -from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN @@ -25,14 +24,14 @@ STEP_USER_DATA_SCHEMA = vol.Schema( ) -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class TodoistConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for todoist.""" VERSION = 1 async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") diff --git a/homeassistant/components/todoist/const.py b/homeassistant/components/todoist/const.py index 021111b48d7..1a66fc9764f 100644 --- a/homeassistant/components/todoist/const.py +++ b/homeassistant/components/todoist/const.py @@ -1,4 +1,5 @@ """Constants for the Todoist component.""" + from typing import Final CONF_EXTRA_PROJECTS: Final = "custom_projects" diff --git a/homeassistant/components/todoist/coordinator.py b/homeassistant/components/todoist/coordinator.py index 702c43883ea..e01b4ecb35a 100644 --- a/homeassistant/components/todoist/coordinator.py +++ b/homeassistant/components/todoist/coordinator.py @@ -1,4 +1,5 @@ """DataUpdateCoordinator for the Todoist component.""" + from datetime import timedelta import logging diff --git a/homeassistant/components/todoist/icons.json b/homeassistant/components/todoist/icons.json new file mode 100644 index 00000000000..d3b881d480c --- /dev/null +++ b/homeassistant/components/todoist/icons.json @@ -0,0 +1,5 @@ +{ + "services": { + "new_task": "mdi:checkbox-marked-circle-plus-outline" + } +} diff --git a/homeassistant/components/todoist/types.py b/homeassistant/components/todoist/types.py index e6b4d55fce2..da716131695 100644 --- a/homeassistant/components/todoist/types.py +++ b/homeassistant/components/todoist/types.py @@ -1,4 +1,5 @@ """Types for the Todoist component.""" + from __future__ import annotations from datetime import datetime diff --git a/homeassistant/components/tolo/__init__.py b/homeassistant/components/tolo/__init__.py index 2fc41fac3af..5fdcdea6c30 100644 --- a/homeassistant/components/tolo/__init__.py +++ b/homeassistant/components/tolo/__init__.py @@ -6,9 +6,7 @@ from datetime import timedelta import logging from typing import NamedTuple -from tololib import ToloClient -from tololib.errors import ResponseTimedOutError -from tololib.message_info import SettingsInfo, StatusInfo +from tololib import ToloClient, ToloSettings, ToloStatus from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, Platform @@ -31,6 +29,7 @@ PLATFORMS = [ Platform.NUMBER, Platform.SELECT, Platform.SENSOR, + Platform.SWITCH, ] _LOGGER = logging.getLogger(__name__) @@ -59,8 +58,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: class ToloSaunaData(NamedTuple): """Compound class for reflecting full state (status and info) of a TOLO Sauna.""" - status: StatusInfo - settings: SettingsInfo + status: ToloStatus + settings: ToloSettings class ToloSaunaUpdateCoordinator(DataUpdateCoordinator[ToloSaunaData]): # pylint: disable=hass-enforce-coordinator-module @@ -68,7 +67,11 @@ class ToloSaunaUpdateCoordinator(DataUpdateCoordinator[ToloSaunaData]): # pylin def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: """Initialize ToloSaunaUpdateCoordinator.""" - self.client = ToloClient(entry.data[CONF_HOST]) + self.client = ToloClient( + address=entry.data[CONF_HOST], + retry_timeout=DEFAULT_RETRY_TIMEOUT, + retry_count=DEFAULT_RETRY_COUNT, + ) super().__init__( hass=hass, logger=_LOGGER, @@ -81,13 +84,9 @@ class ToloSaunaUpdateCoordinator(DataUpdateCoordinator[ToloSaunaData]): # pylin def _get_tolo_sauna_data(self) -> ToloSaunaData: try: - status = self.client.get_status_info( - resend_timeout=DEFAULT_RETRY_TIMEOUT, retries=DEFAULT_RETRY_COUNT - ) - settings = self.client.get_settings_info( - resend_timeout=DEFAULT_RETRY_TIMEOUT, retries=DEFAULT_RETRY_COUNT - ) - except ResponseTimedOutError as error: + status = self.client.get_status() + settings = self.client.get_settings() + except TimeoutError as error: raise UpdateFailed("communication timeout") from error return ToloSaunaData(status, settings) diff --git a/homeassistant/components/tolo/binary_sensor.py b/homeassistant/components/tolo/binary_sensor.py index 124cd45d78b..f8cb442c92f 100644 --- a/homeassistant/components/tolo/binary_sensor.py +++ b/homeassistant/components/tolo/binary_sensor.py @@ -34,7 +34,6 @@ class ToloFlowInBinarySensor(ToloSaunaCoordinatorEntity, BinarySensorEntity): _attr_entity_category = EntityCategory.DIAGNOSTIC _attr_translation_key = "water_in_valve" _attr_device_class = BinarySensorDeviceClass.OPENING - _attr_icon = "mdi:water-plus-outline" def __init__( self, coordinator: ToloSaunaUpdateCoordinator, entry: ConfigEntry @@ -56,7 +55,6 @@ class ToloFlowOutBinarySensor(ToloSaunaCoordinatorEntity, BinarySensorEntity): _attr_entity_category = EntityCategory.DIAGNOSTIC _attr_translation_key = "water_out_valve" _attr_device_class = BinarySensorDeviceClass.OPENING - _attr_icon = "mdi:water-minus-outline" def __init__( self, coordinator: ToloSaunaUpdateCoordinator, entry: ConfigEntry diff --git a/homeassistant/components/tolo/button.py b/homeassistant/components/tolo/button.py index 3b81477ab37..9a8ac67b9fe 100644 --- a/homeassistant/components/tolo/button.py +++ b/homeassistant/components/tolo/button.py @@ -1,6 +1,6 @@ """TOLO Sauna Button controls.""" -from tololib.const import LampMode +from tololib import LampMode from homeassistant.components.button import ButtonEntity from homeassistant.config_entries import ConfigEntry @@ -30,7 +30,6 @@ class ToloLampNextColorButton(ToloSaunaCoordinatorEntity, ButtonEntity): """Button for switching to the next lamp color.""" _attr_entity_category = EntityCategory.CONFIG - _attr_icon = "mdi:palette" _attr_translation_key = "next_color" def __init__( diff --git a/homeassistant/components/tolo/climate.py b/homeassistant/components/tolo/climate.py index 033a4c5b51c..2994d97d54a 100644 --- a/homeassistant/components/tolo/climate.py +++ b/homeassistant/components/tolo/climate.py @@ -1,9 +1,16 @@ """TOLO Sauna climate controls (main sauna control).""" + from __future__ import annotations from typing import Any -from tololib.const import Calefaction +from tololib import ( + TARGET_HUMIDITY_MAX, + TARGET_HUMIDITY_MIN, + TARGET_TEMPERATURE_MAX, + TARGET_TEMPERATURE_MIN, + Calefaction, +) from homeassistant.components.climate import ( FAN_OFF, @@ -19,13 +26,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import ToloSaunaCoordinatorEntity, ToloSaunaUpdateCoordinator -from .const import ( - DEFAULT_MAX_HUMIDITY, - DEFAULT_MAX_TEMP, - DEFAULT_MIN_HUMIDITY, - DEFAULT_MIN_TEMP, - DOMAIN, -) +from .const import DOMAIN async def async_setup_entry( @@ -43,10 +44,10 @@ class SaunaClimate(ToloSaunaCoordinatorEntity, ClimateEntity): _attr_fan_modes = [FAN_ON, FAN_OFF] _attr_hvac_modes = [HVACMode.OFF, HVACMode.HEAT, HVACMode.DRY] - _attr_max_humidity = DEFAULT_MAX_HUMIDITY - _attr_max_temp = DEFAULT_MAX_TEMP - _attr_min_humidity = DEFAULT_MIN_HUMIDITY - _attr_min_temp = DEFAULT_MIN_TEMP + _attr_max_humidity = TARGET_HUMIDITY_MAX + _attr_max_temp = TARGET_TEMPERATURE_MAX + _attr_min_humidity = TARGET_HUMIDITY_MIN + _attr_min_temp = TARGET_TEMPERATURE_MIN _attr_name = None _attr_precision = PRECISION_WHOLE _attr_supported_features = ( diff --git a/homeassistant/components/tolo/config_flow.py b/homeassistant/components/tolo/config_flow.py index 14304f6653e..5cf91bdc3a8 100644 --- a/homeassistant/components/tolo/config_flow.py +++ b/homeassistant/components/tolo/config_flow.py @@ -5,17 +5,15 @@ from __future__ import annotations import logging from typing import Any -from tololib import ToloClient -from tololib.errors import ResponseTimedOutError +from tololib import ToloClient, ToloCommunicationError import voluptuous as vol from homeassistant.components import dhcp -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.device_registry import format_mac -from .const import DEFAULT_NAME, DEFAULT_RETRY_COUNT, DEFAULT_RETRY_TIMEOUT, DOMAIN +from .const import DEFAULT_NAME, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -31,16 +29,14 @@ class ToloSaunaConfigFlow(ConfigFlow, domain=DOMAIN): def _check_device_availability(host: str) -> bool: client = ToloClient(host) try: - result = client.get_status_info( - resend_timeout=DEFAULT_RETRY_TIMEOUT, retries=DEFAULT_RETRY_COUNT - ) - except ResponseTimedOutError: + result = client.get_status() + except ToloCommunicationError: return False return result is not None async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" errors = {} @@ -64,7 +60,9 @@ class ToloSaunaConfigFlow(ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: + async def async_step_dhcp( + self, discovery_info: dhcp.DhcpServiceInfo + ) -> ConfigFlowResult: """Handle a flow initialized by discovery.""" await self.async_set_unique_id(format_mac(discovery_info.macaddress)) self._abort_if_unique_id_configured({CONF_HOST: discovery_info.ip}) @@ -81,7 +79,7 @@ class ToloSaunaConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle user-confirmation of discovered node.""" if user_input is not None: self._async_abort_entries_match({CONF_HOST: self._discovered_host}) diff --git a/homeassistant/components/tolo/const.py b/homeassistant/components/tolo/const.py index 77bee92d521..40940c4283c 100644 --- a/homeassistant/components/tolo/const.py +++ b/homeassistant/components/tolo/const.py @@ -1,17 +1,25 @@ """Constants for the tolo integration.""" +from enum import Enum + +from tololib import AromaTherapySlot as ToloAromaTherapySlot, LampMode as ToloLampMode + DOMAIN = "tolo" DEFAULT_NAME = "TOLO Sauna" DEFAULT_RETRY_TIMEOUT = 1 DEFAULT_RETRY_COUNT = 3 -DEFAULT_MAX_TEMP = 60 -DEFAULT_MIN_TEMP = 20 -DEFAULT_MAX_HUMIDITY = 99 -DEFAULT_MIN_HUMIDITY = 60 +class AromaTherapySlot(Enum): + """Mapping to TOLO Aroma Therapy Slot.""" -POWER_TIMER_MAX = 60 -SALT_BATH_TIMER_MAX = 60 -FAN_TIMER_MAX = 60 + A = ToloAromaTherapySlot.A + B = ToloAromaTherapySlot.B + + +class LampMode(Enum): + """Mapping to TOLO Lamp Mode.""" + + MANUAL = ToloLampMode.MANUAL + AUTOMATIC = ToloLampMode.AUTOMATIC diff --git a/homeassistant/components/tolo/icons.json b/homeassistant/components/tolo/icons.json new file mode 100644 index 00000000000..7aeec065b41 --- /dev/null +++ b/homeassistant/components/tolo/icons.json @@ -0,0 +1,52 @@ +{ + "entity": { + "binary_sensor": { + "water_in_valve": { + "default": "mdi:water-plus-outline" + }, + "water_out_valve": { + "default": "mdi:water-minus-outline" + } + }, + "button": { + "next_color": { + "default": "mdi:palette" + } + }, + "number": { + "power_timer": { + "default": "mdi:power-settings" + }, + "salt_bath_timer": { + "default": "mdi:shaker-outline" + }, + "fan_timer": { + "default": "mdi:fan-auto" + } + }, + "select": { + "lamp_mode": { + "default": "mdi:lightbulb-multiple-outline" + } + }, + "sensor": { + "water_level": { + "default": "mdi:waves-arrow-up" + }, + "power_timer_remaining": { + "default": "mdi:power-settings" + }, + "salt_bath_timer_remaining": { + "default": "mdi:shaker-outline" + }, + "fan_timer_remaining": { + "default": "mdi:fan-auto" + } + }, + "switch": { + "aroma_therapy_on": { + "default": "mdi:scent" + } + } + } +} diff --git a/homeassistant/components/tolo/light.py b/homeassistant/components/tolo/light.py index 4b76d4270c6..809bb367072 100644 --- a/homeassistant/components/tolo/light.py +++ b/homeassistant/components/tolo/light.py @@ -1,4 +1,5 @@ """TOLO Sauna light controls.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/tolo/manifest.json b/homeassistant/components/tolo/manifest.json index 57a63e55cf3..14125a857f6 100644 --- a/homeassistant/components/tolo/manifest.json +++ b/homeassistant/components/tolo/manifest.json @@ -11,5 +11,5 @@ "documentation": "https://www.home-assistant.io/integrations/tolo", "iot_class": "local_polling", "loggers": ["tololib"], - "requirements": ["tololib==0.1.0b4"] + "requirements": ["tololib==1.1.0"] } diff --git a/homeassistant/components/tolo/number.py b/homeassistant/components/tolo/number.py index b31b5102394..2d2c20715fa 100644 --- a/homeassistant/components/tolo/number.py +++ b/homeassistant/components/tolo/number.py @@ -1,12 +1,18 @@ """TOLO Sauna number controls.""" + from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass from typing import Any -from tololib import ToloClient -from tololib.message_info import SettingsInfo +from tololib import ( + FAN_TIMER_MAX, + POWER_TIMER_MAX, + SALT_BATH_TIMER_MAX, + ToloClient, + ToloSettings, +) from homeassistant.components.number import NumberEntity, NumberEntityDescription from homeassistant.config_entries import ConfigEntry @@ -15,23 +21,16 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import ToloSaunaCoordinatorEntity, ToloSaunaUpdateCoordinator -from .const import DOMAIN, FAN_TIMER_MAX, POWER_TIMER_MAX, SALT_BATH_TIMER_MAX +from .const import DOMAIN -@dataclass(frozen=True) -class ToloNumberEntityDescriptionBase: - """Required values when describing TOLO Number entities.""" - - getter: Callable[[SettingsInfo], int | None] - setter: Callable[[ToloClient, int | None], Any] - - -@dataclass(frozen=True) -class ToloNumberEntityDescription( - NumberEntityDescription, ToloNumberEntityDescriptionBase -): +@dataclass(frozen=True, kw_only=True) +class ToloNumberEntityDescription(NumberEntityDescription): """Class describing TOLO Number entities.""" + getter: Callable[[ToloSettings], int | None] + setter: Callable[[ToloClient, int | None], Any] + entity_category = EntityCategory.CONFIG native_min_value = 0 native_step = 1 @@ -41,7 +40,6 @@ NUMBERS = ( ToloNumberEntityDescription( key="power_timer", translation_key="power_timer", - icon="mdi:power-settings", native_unit_of_measurement=UnitOfTime.MINUTES, native_max_value=POWER_TIMER_MAX, getter=lambda settings: settings.power_timer, @@ -50,7 +48,6 @@ NUMBERS = ( ToloNumberEntityDescription( key="salt_bath_timer", translation_key="salt_bath_timer", - icon="mdi:shaker-outline", native_unit_of_measurement=UnitOfTime.MINUTES, native_max_value=SALT_BATH_TIMER_MAX, getter=lambda settings: settings.salt_bath_timer, @@ -59,7 +56,6 @@ NUMBERS = ( ToloNumberEntityDescription( key="fan_timer", translation_key="fan_timer", - icon="mdi:fan-auto", native_unit_of_measurement=UnitOfTime.MINUTES, native_max_value=FAN_TIMER_MAX, getter=lambda settings: settings.fan_timer, diff --git a/homeassistant/components/tolo/select.py b/homeassistant/components/tolo/select.py index 8e4ecb47f48..96335cecc68 100644 --- a/homeassistant/components/tolo/select.py +++ b/homeassistant/components/tolo/select.py @@ -2,16 +2,52 @@ from __future__ import annotations -from tololib.const import LampMode +from collections.abc import Callable +from dataclasses import dataclass -from homeassistant.components.select import SelectEntity +from tololib import ToloClient, ToloSettings + +from homeassistant.components.select import SelectEntity, SelectEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import ToloSaunaCoordinatorEntity, ToloSaunaUpdateCoordinator -from .const import DOMAIN +from .const import DOMAIN, AromaTherapySlot, LampMode + + +@dataclass(frozen=True, kw_only=True) +class ToloSelectEntityDescription(SelectEntityDescription): + """Class describing TOLO select entities.""" + + options: list[str] + getter: Callable[[ToloSettings], str] + setter: Callable[[ToloClient, str], bool] + + +SELECTS = ( + ToloSelectEntityDescription( + key="lamp_mode", + translation_key="lamp_mode", + options=[lamp_mode.name.lower() for lamp_mode in LampMode], + getter=lambda settings: settings.lamp_mode.name.lower(), + setter=lambda client, option: client.set_lamp_mode( + LampMode[option.upper()].value + ), + ), + ToloSelectEntityDescription( + key="aroma_therapy_slot", + translation_key="aroma_therapy_slot", + options=[ + aroma_therapy_slot.name.lower() for aroma_therapy_slot in AromaTherapySlot + ], + getter=lambda settings: settings.aroma_therapy_slot.name.lower(), + setter=lambda client, option: client.set_aroma_therapy_slot( + AromaTherapySlot[option.upper()].value + ), + ), +) async def async_setup_entry( @@ -21,30 +57,39 @@ async def async_setup_entry( ) -> None: """Set up select entities for TOLO Sauna.""" coordinator = hass.data[DOMAIN][entry.entry_id] - async_add_entities([ToloLampModeSelect(coordinator, entry)]) + async_add_entities( + ToloSelectEntity(coordinator, entry, description) for description in SELECTS + ) -class ToloLampModeSelect(ToloSaunaCoordinatorEntity, SelectEntity): - """TOLO Sauna lamp mode select.""" +class ToloSelectEntity(ToloSaunaCoordinatorEntity, SelectEntity): + """TOLO select entity.""" _attr_entity_category = EntityCategory.CONFIG - _attr_icon = "mdi:lightbulb-multiple-outline" - _attr_options = [lamp_mode.name.lower() for lamp_mode in LampMode] - _attr_translation_key = "lamp_mode" + + entity_description: ToloSelectEntityDescription def __init__( - self, coordinator: ToloSaunaUpdateCoordinator, entry: ConfigEntry + self, + coordinator: ToloSaunaUpdateCoordinator, + entry: ConfigEntry, + entity_description: ToloSelectEntityDescription, ) -> None: - """Initialize lamp mode select entity.""" + """Initialize TOLO select entity.""" super().__init__(coordinator, entry) + self.entity_description = entity_description + self._attr_unique_id = f"{entry.entry_id}_{entity_description.key}" - self._attr_unique_id = f"{entry.entry_id}_lamp_mode" + @property + def options(self) -> list[str]: + """Return available select options.""" + return self.entity_description.options @property def current_option(self) -> str: - """Return current lamp mode.""" - return self.coordinator.data.settings.lamp_mode.name.lower() + """Return current select option.""" + return self.entity_description.getter(self.coordinator.data.settings) def select_option(self, option: str) -> None: - """Select lamp mode.""" - self.coordinator.client.set_lamp_mode(LampMode[option.upper()]) + """Select a select option.""" + self.entity_description.setter(self.coordinator.client, option) diff --git a/homeassistant/components/tolo/sensor.py b/homeassistant/components/tolo/sensor.py index ec57612a99f..bee01cc283f 100644 --- a/homeassistant/components/tolo/sensor.py +++ b/homeassistant/components/tolo/sensor.py @@ -1,10 +1,11 @@ """TOLO Sauna (non-binary, general) sensors.""" + from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass -from tololib.message_info import SettingsInfo, StatusInfo +from tololib import ToloSettings, ToloStatus from homeassistant.components.sensor import ( SensorDeviceClass, @@ -26,20 +27,13 @@ from . import ToloSaunaCoordinatorEntity, ToloSaunaUpdateCoordinator from .const import DOMAIN -@dataclass(frozen=True) -class ToloSensorEntityDescriptionBase: - """Required values when describing TOLO Sensor entities.""" - - getter: Callable[[StatusInfo], int | None] - availability_checker: Callable[[SettingsInfo, StatusInfo], bool] | None - - -@dataclass(frozen=True) -class ToloSensorEntityDescription( - SensorEntityDescription, ToloSensorEntityDescriptionBase -): +@dataclass(frozen=True, kw_only=True) +class ToloSensorEntityDescription(SensorEntityDescription): """Class describing TOLO Sensor entities.""" + getter: Callable[[ToloStatus], int | None] + availability_checker: Callable[[ToloSettings, ToloStatus], bool] | None + state_class = SensorStateClass.MEASUREMENT @@ -48,7 +42,6 @@ SENSORS = ( key="water_level", translation_key="water_level", entity_category=EntityCategory.DIAGNOSTIC, - icon="mdi:waves-arrow-up", native_unit_of_measurement=PERCENTAGE, getter=lambda status: status.water_level_percent, availability_checker=None, @@ -66,7 +59,6 @@ SENSORS = ( key="power_timer_remaining", translation_key="power_timer_remaining", entity_category=EntityCategory.DIAGNOSTIC, - icon="mdi:power-settings", native_unit_of_measurement=UnitOfTime.MINUTES, getter=lambda status: status.power_timer, availability_checker=lambda settings, status: status.power_on @@ -76,7 +68,6 @@ SENSORS = ( key="salt_bath_timer_remaining", translation_key="salt_bath_timer_remaining", entity_category=EntityCategory.DIAGNOSTIC, - icon="mdi:shaker-outline", native_unit_of_measurement=UnitOfTime.MINUTES, getter=lambda status: status.salt_bath_timer, availability_checker=lambda settings, status: status.salt_bath_on @@ -86,7 +77,6 @@ SENSORS = ( key="fan_timer_remaining", translation_key="fan_timer_remaining", entity_category=EntityCategory.DIAGNOSTIC, - icon="mdi:fan-auto", native_unit_of_measurement=UnitOfTime.MINUTES, getter=lambda status: status.fan_timer, availability_checker=lambda settings, status: status.fan_on diff --git a/homeassistant/components/tolo/strings.json b/homeassistant/components/tolo/strings.json index f48e26c5276..c55498b8d92 100644 --- a/homeassistant/components/tolo/strings.json +++ b/homeassistant/components/tolo/strings.json @@ -61,6 +61,13 @@ "automatic": "Automatic", "manual": "Manual" } + }, + "aroma_therapy_slot": { + "name": "Aroma therapy slot", + "state": { + "a": "Slot A", + "b": "Slot B" + } } }, "sensor": { @@ -79,6 +86,14 @@ "fan_timer_remaining": { "name": "Fan timer" } + }, + "switch": { + "aroma_therapy_on": { + "name": "Aroma therapy" + }, + "salt_bath_on": { + "name": "Salt bath" + } } } } diff --git a/homeassistant/components/tolo/switch.py b/homeassistant/components/tolo/switch.py new file mode 100644 index 00000000000..b90f548ee76 --- /dev/null +++ b/homeassistant/components/tolo/switch.py @@ -0,0 +1,83 @@ +"""TOLO Sauna switch controls.""" + +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from typing import Any + +from tololib import ToloClient, ToloStatus + +from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import ToloSaunaCoordinatorEntity, ToloSaunaUpdateCoordinator +from .const import DOMAIN + + +@dataclass(frozen=True, kw_only=True) +class ToloSwitchEntityDescription(SwitchEntityDescription): + """Class describing TOLO switch entities.""" + + getter: Callable[[ToloStatus], bool] + setter: Callable[[ToloClient, bool], bool] + + +SWITCHES = ( + ToloSwitchEntityDescription( + key="aroma_therapy_on", + translation_key="aroma_therapy_on", + getter=lambda status: status.aroma_therapy_on, + setter=lambda client, value: client.set_aroma_therapy_on(value), + ), + ToloSwitchEntityDescription( + key="salt_bath_on", + translation_key="salt_bath_on", + getter=lambda status: status.salt_bath_on, + setter=lambda client, value: client.set_salt_bath_on(value), + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up switch controls for TOLO Sauna.""" + coordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + ToloSwitchEntity(coordinator, entry, description) for description in SWITCHES + ) + + +class ToloSwitchEntity(ToloSaunaCoordinatorEntity, SwitchEntity): + """TOLO switch entity.""" + + entity_description: ToloSwitchEntityDescription + + def __init__( + self, + coordinator: ToloSaunaUpdateCoordinator, + entry: ConfigEntry, + entity_description: ToloSwitchEntityDescription, + ) -> None: + """Initialize TOLO switch entity.""" + super().__init__(coordinator, entry) + self.entity_description = entity_description + self._attr_unique_id = f"{entry.entry_id}_{entity_description.key}" + + @property + def is_on(self) -> bool: + """Return if the switch is currently on.""" + return self.entity_description.getter(self.coordinator.data.status) + + def turn_on(self, **kwargs: Any) -> None: + """Turn the switch on.""" + self.entity_description.setter(self.coordinator.client, True) + + def turn_off(self, **kwargs: Any) -> None: + """Turn the switch off.""" + self.entity_description.setter(self.coordinator.client, False) diff --git a/homeassistant/components/tomato/device_tracker.py b/homeassistant/components/tomato/device_tracker.py index d71dd45bcfe..d28fa505c61 100644 --- a/homeassistant/components/tomato/device_tracker.py +++ b/homeassistant/components/tomato/device_tracker.py @@ -1,4 +1,5 @@ """Support for Tomato routers.""" + from __future__ import annotations from http import HTTPStatus diff --git a/homeassistant/components/tomorrowio/__init__.py b/homeassistant/components/tomorrowio/__init__.py index ea179219153..3ff811369fd 100644 --- a/homeassistant/components/tomorrowio/__init__.py +++ b/homeassistant/components/tomorrowio/__init__.py @@ -1,4 +1,5 @@ """The Tomorrow.io integration.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/tomorrowio/config_flow.py b/homeassistant/components/tomorrowio/config_flow.py index aece537c867..1a8cd328045 100644 --- a/homeassistant/components/tomorrowio/config_flow.py +++ b/homeassistant/components/tomorrowio/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Tomorrow.io integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -13,8 +14,13 @@ from pytomorrowio.exceptions import ( from pytomorrowio.pytomorrowio import TomorrowioV4 import voluptuous as vol -from homeassistant import config_entries, core from homeassistant.components.zone import async_active_zone +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.const import ( CONF_API_KEY, CONF_FRIENDLY_NAME, @@ -24,7 +30,6 @@ from homeassistant.const import ( CONF_NAME, ) from homeassistant.core import HomeAssistant, callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.selector import LocationSelector, LocationSelectorConfig @@ -40,7 +45,7 @@ _LOGGER = logging.getLogger(__name__) def _get_config_schema( - hass: core.HomeAssistant, + hass: HomeAssistant, source: str | None, input_dict: dict[str, Any] | None = None, ) -> vol.Schema: @@ -83,16 +88,16 @@ def _get_unique_id(hass: HomeAssistant, input_dict: dict[str, Any]): ) -class TomorrowioOptionsConfigFlow(config_entries.OptionsFlow): +class TomorrowioOptionsConfigFlow(OptionsFlow): """Handle Tomorrow.io options.""" - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + def __init__(self, config_entry: ConfigEntry) -> None: """Initialize Tomorrow.io options flow.""" self._config_entry = config_entry async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Manage the Tomorrow.io options.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) @@ -109,7 +114,7 @@ class TomorrowioOptionsConfigFlow(config_entries.OptionsFlow): ) -class TomorrowioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class TomorrowioConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Tomorrow.io Weather API.""" VERSION = 1 @@ -117,14 +122,14 @@ class TomorrowioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback def async_get_options_flow( - config_entry: config_entries.ConfigEntry, + config_entry: ConfigEntry, ) -> TomorrowioOptionsConfigFlow: """Get the options flow for this handler.""" return TomorrowioOptionsConfigFlow(config_entry) async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors = {} if user_input is not None: diff --git a/homeassistant/components/tomorrowio/const.py b/homeassistant/components/tomorrowio/const.py index 7ad6ea60836..e727be38b16 100644 --- a/homeassistant/components/tomorrowio/const.py +++ b/homeassistant/components/tomorrowio/const.py @@ -1,4 +1,5 @@ """Constants for the Tomorrow.io integration.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/tomorrowio/icons.json b/homeassistant/components/tomorrowio/icons.json new file mode 100644 index 00000000000..aa1d3782546 --- /dev/null +++ b/homeassistant/components/tomorrowio/icons.json @@ -0,0 +1,48 @@ +{ + "entity": { + "sensor": { + "dew_point": { + "default": "mdi:thermometer-water" + }, + "cloud_base": { + "default": "mdi:cloud-arrow-down" + }, + "cloud_ceiling": { + "default": "mdi:cloud-arrow-up" + }, + "cloud_cover": { + "default": "mdi:cloud-percent" + }, + "wind_gust": { + "default": "mdi:weather-windy" + }, + "precipitation_type": { + "default": "mdi:weather-snowy-rainy" + }, + "health_concern": { + "default": "mdi:hospital" + }, + "china_mep_health_concern": { + "default": "mdi:hospital" + }, + "pollen_index": { + "default": "mdi:tree" + }, + "weed_pollen_index": { + "default": "mdi:flower-pollen" + }, + "grass_pollen_index": { + "default": "mdi:grass" + }, + "fire_index": { + "default": "mdi:fire" + }, + "uv_index": { + "default": "mdi:sun-wireless" + }, + "uv_radiation_health_concern": { + "default": "mdi:weather-sunny-alert" + } + } + } +} diff --git a/homeassistant/components/tomorrowio/sensor.py b/homeassistant/components/tomorrowio/sensor.py index 6b285378e7e..371121a9da3 100644 --- a/homeassistant/components/tomorrowio/sensor.py +++ b/homeassistant/components/tomorrowio/sensor.py @@ -1,4 +1,5 @@ """Sensor component that handles additional Tomorrowio data for your location.""" + from __future__ import annotations from abc import abstractmethod @@ -117,7 +118,6 @@ SENSOR_TYPES = ( key="dew_point", translation_key="dew_point", attribute=TMRW_ATTR_DEW_POINT, - icon="mdi:thermometer-water", native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, @@ -146,7 +146,6 @@ SENSOR_TYPES = ( key="cloud_base", translation_key="cloud_base", attribute=TMRW_ATTR_CLOUD_BASE, - icon="mdi:cloud-arrow-down", unit_imperial=UnitOfLength.MILES, unit_metric=UnitOfLength.KILOMETERS, device_class=SensorDeviceClass.DISTANCE, @@ -162,7 +161,6 @@ SENSOR_TYPES = ( key="cloud_ceiling", translation_key="cloud_ceiling", attribute=TMRW_ATTR_CLOUD_CEILING, - icon="mdi:cloud-arrow-up", unit_imperial=UnitOfLength.MILES, unit_metric=UnitOfLength.KILOMETERS, device_class=SensorDeviceClass.DISTANCE, @@ -177,7 +175,6 @@ SENSOR_TYPES = ( key="cloud_cover", translation_key="cloud_cover", attribute=TMRW_ATTR_CLOUD_COVER, - icon="mdi:cloud-percent", native_unit_of_measurement=PERCENTAGE, ), # Data comes in as m/s, convert to mi/h for imperial @@ -185,7 +182,6 @@ SENSOR_TYPES = ( key="wind_gust", translation_key="wind_gust", attribute=TMRW_ATTR_WIND_GUST, - icon="mdi:weather-windy", unit_imperial=UnitOfSpeed.MILES_PER_HOUR, unit_metric=UnitOfSpeed.METERS_PER_SECOND, device_class=SensorDeviceClass.SPEED, @@ -199,7 +195,6 @@ SENSOR_TYPES = ( translation_key="precipitation_type", attribute=TMRW_ATTR_PRECIPITATION_TYPE, value_map=PrecipitationType, - icon="mdi:weather-snowy-rainy", ), # Data comes in as ppb, convert to µg/m^3 # Molecular weight of Ozone is 48 @@ -272,7 +267,6 @@ SENSOR_TYPES = ( translation_key="health_concern", attribute=TMRW_ATTR_EPA_HEALTH_CONCERN, value_map=HealthConcernType, - icon="mdi:hospital", ), TomorrowioSensorEntityDescription( key="china_mep_air_quality_index", @@ -291,13 +285,11 @@ SENSOR_TYPES = ( translation_key="china_mep_health_concern", attribute=TMRW_ATTR_CHINA_HEALTH_CONCERN, value_map=HealthConcernType, - icon="mdi:hospital", ), TomorrowioSensorEntityDescription( key="tree_pollen_index", translation_key="pollen_index", attribute=TMRW_ATTR_POLLEN_TREE, - icon="mdi:tree", value_map=PollenIndex, ), TomorrowioSensorEntityDescription( @@ -305,34 +297,29 @@ SENSOR_TYPES = ( translation_key="weed_pollen_index", attribute=TMRW_ATTR_POLLEN_WEED, value_map=PollenIndex, - icon="mdi:flower-pollen", ), TomorrowioSensorEntityDescription( key="grass_pollen_index", translation_key="grass_pollen_index", attribute=TMRW_ATTR_POLLEN_GRASS, - icon="mdi:grass", value_map=PollenIndex, ), TomorrowioSensorEntityDescription( key="fire_index", translation_key="fire_index", attribute=TMRW_ATTR_FIRE_INDEX, - icon="mdi:fire", ), TomorrowioSensorEntityDescription( key="uv_index", translation_key="uv_index", attribute=TMRW_ATTR_UV_INDEX, state_class=SensorStateClass.MEASUREMENT, - icon="mdi:sun-wireless", ), TomorrowioSensorEntityDescription( key="uv_radiation_health_concern", translation_key="uv_radiation_health_concern", attribute=TMRW_ATTR_UV_HEALTH_CONCERN, value_map=UVDescription, - icon="mdi:weather-sunny-alert", ), ) diff --git a/homeassistant/components/tomorrowio/weather.py b/homeassistant/components/tomorrowio/weather.py index 06a147366e8..3b60f171bbe 100644 --- a/homeassistant/components/tomorrowio/weather.py +++ b/homeassistant/components/tomorrowio/weather.py @@ -1,4 +1,5 @@ """Weather component that handles meteorological data for your location.""" + from __future__ import annotations from datetime import datetime @@ -297,11 +298,6 @@ class TomorrowioWeatherEntity(TomorrowioEntity, SingleCoordinatorWeatherEntity): return forecasts - @property - def forecast(self) -> list[Forecast] | None: - """Return the forecast array.""" - return self._forecast(self.forecast_type) - @callback def _async_forecast_daily(self) -> list[Forecast] | None: """Return the daily forecast in native units.""" diff --git a/homeassistant/components/toon/__init__.py b/homeassistant/components/toon/__init__.py index 36f7ca12b84..43c787b2301 100644 --- a/homeassistant/components/toon/__init__.py +++ b/homeassistant/components/toon/__init__.py @@ -1,4 +1,5 @@ """Support for Toon van Eneco devices.""" + import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry diff --git a/homeassistant/components/toon/binary_sensor.py b/homeassistant/components/toon/binary_sensor.py index 6edc656df06..b184e5aacb7 100644 --- a/homeassistant/components/toon/binary_sensor.py +++ b/homeassistant/components/toon/binary_sensor.py @@ -1,4 +1,5 @@ """Support for Toon binary sensors.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/homeassistant/components/toon/climate.py b/homeassistant/components/toon/climate.py index 16fbdbdd356..1570a637f95 100644 --- a/homeassistant/components/toon/climate.py +++ b/homeassistant/components/toon/climate.py @@ -1,4 +1,5 @@ """Support for Toon thermostat.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/toon/config_flow.py b/homeassistant/components/toon/config_flow.py index 5c98e35bead..40e83c3c9be 100644 --- a/homeassistant/components/toon/config_flow.py +++ b/homeassistant/components/toon/config_flow.py @@ -1,4 +1,5 @@ """Config flow to configure the Toon component.""" + from __future__ import annotations import logging @@ -7,7 +8,7 @@ from typing import Any from toonapi import Agreement, Toon, ToonError import voluptuous as vol -from homeassistant.data_entry_flow import FlowResult +from homeassistant.config_entries import ConfigFlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.config_entry_oauth2_flow import AbstractOAuth2FlowHandler @@ -28,7 +29,7 @@ class ToonFlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN): """Return logger.""" return logging.getLogger(__name__) - async def async_oauth_create_entry(self, data: dict[str, Any]) -> FlowResult: + async def async_oauth_create_entry(self, data: dict[str, Any]) -> ConfigFlowResult: """Test connection and load up agreements.""" self.data = data @@ -48,7 +49,7 @@ class ToonFlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN): async def async_step_import( self, config: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Start a configuration flow based on imported data. This step is merely here to trigger "discovery" when the `toon` @@ -65,7 +66,7 @@ class ToonFlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN): async def async_step_agreement( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Select Toon agreement to add.""" if len(self.agreements) == 1: return await self._create_entry(self.agreements[0]) @@ -86,7 +87,7 @@ class ToonFlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN): agreement_index = agreements_list.index(user_input[CONF_AGREEMENT]) return await self._create_entry(self.agreements[agreement_index]) - async def _create_entry(self, agreement: Agreement) -> FlowResult: + async def _create_entry(self, agreement: Agreement) -> ConfigFlowResult: if CONF_MIGRATE in self.context: await self.hass.config_entries.async_remove(self.context[CONF_MIGRATE]) diff --git a/homeassistant/components/toon/const.py b/homeassistant/components/toon/const.py index bf70c54e5e0..d509a70f0b9 100644 --- a/homeassistant/components/toon/const.py +++ b/homeassistant/components/toon/const.py @@ -1,4 +1,5 @@ """Constants for the Toon integration.""" + from datetime import timedelta DOMAIN = "toon" diff --git a/homeassistant/components/toon/coordinator.py b/homeassistant/components/toon/coordinator.py index 58165935215..8d27438f7df 100644 --- a/homeassistant/components/toon/coordinator.py +++ b/homeassistant/components/toon/coordinator.py @@ -1,4 +1,5 @@ """Provides the Toon DataUpdateCoordinator.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/toon/helpers.py b/homeassistant/components/toon/helpers.py index 41e6cd1c6bb..cd4e55fd050 100644 --- a/homeassistant/components/toon/helpers.py +++ b/homeassistant/components/toon/helpers.py @@ -1,4 +1,5 @@ """Helpers for Toon.""" + from __future__ import annotations from collections.abc import Callable, Coroutine diff --git a/homeassistant/components/toon/icons.json b/homeassistant/components/toon/icons.json new file mode 100644 index 00000000000..650bf0b6d19 --- /dev/null +++ b/homeassistant/components/toon/icons.json @@ -0,0 +1,5 @@ +{ + "services": { + "update": "mdi:update" + } +} diff --git a/homeassistant/components/toon/models.py b/homeassistant/components/toon/models.py index 44986b02143..0c08c10bfaf 100644 --- a/homeassistant/components/toon/models.py +++ b/homeassistant/components/toon/models.py @@ -1,4 +1,5 @@ """DataUpdate Coordinator, and base Entity and Device models for Toon.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/homeassistant/components/toon/oauth2.py b/homeassistant/components/toon/oauth2.py index 95cde386215..2535cc5de7d 100644 --- a/homeassistant/components/toon/oauth2.py +++ b/homeassistant/components/toon/oauth2.py @@ -1,4 +1,5 @@ """OAuth2 implementations for Toon.""" + from __future__ import annotations from typing import Any, cast diff --git a/homeassistant/components/toon/sensor.py b/homeassistant/components/toon/sensor.py index 7ff9d2b67f7..09fdcb4e4ab 100644 --- a/homeassistant/components/toon/sensor.py +++ b/homeassistant/components/toon/sensor.py @@ -1,4 +1,5 @@ """Support for Toon sensors.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/homeassistant/components/toon/switch.py b/homeassistant/components/toon/switch.py index 8dddb657df0..b491505a8a5 100644 --- a/homeassistant/components/toon/switch.py +++ b/homeassistant/components/toon/switch.py @@ -1,4 +1,5 @@ """Support for Toon switches.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/homeassistant/components/torque/sensor.py b/homeassistant/components/torque/sensor.py index a2e1e35d2ae..8edf4fe49fc 100644 --- a/homeassistant/components/torque/sensor.py +++ b/homeassistant/components/torque/sensor.py @@ -1,14 +1,16 @@ """Support for the Torque OBD application.""" + from __future__ import annotations import re +from aiohttp import web import voluptuous as vol -from homeassistant.components.http import HomeAssistantView +from homeassistant.components.http import KEY_HASS, HomeAssistantView from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import CONF_EMAIL, CONF_NAME, DEGREE -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HassJob, HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -49,8 +51,8 @@ def setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the Torque platform.""" - vehicle = config.get(CONF_NAME) - email = config.get(CONF_EMAIL) + vehicle: str | None = config.get(CONF_NAME) + email: str | None = config.get(CONF_EMAIL) sensors: dict[int, TorqueSensor] = {} hass.http.register_view( @@ -64,21 +66,27 @@ class TorqueReceiveDataView(HomeAssistantView): url = API_PATH name = "api:torque" - def __init__(self, email, vehicle, sensors, add_entities): + def __init__( + self, + email: str | None, + vehicle: str | None, + sensors: dict[int, TorqueSensor], + add_entities: AddEntitiesCallback, + ) -> None: """Initialize a Torque view.""" self.email = email self.vehicle = vehicle self.sensors = sensors - self.add_entities = add_entities + self.add_entities_job = HassJob(add_entities) @callback - def get(self, request): + def get(self, request: web.Request) -> str | None: """Handle Torque data request.""" - hass = request.app["hass"] + hass: HomeAssistant = request.app[KEY_HASS] data = request.query if self.email is not None and self.email != data[SENSOR_EMAIL_FIELD]: - return + return None names = {} units = {} @@ -108,7 +116,7 @@ class TorqueReceiveDataView(HomeAssistantView): self.sensors[pid] = TorqueSensor( ENTITY_NAME_FORMAT.format(self.vehicle, name), units.get(pid) ) - hass.async_add_job(self.add_entities, [self.sensors[pid]]) + hass.async_add_hass_job(self.add_entities_job, [self.sensors[pid]]) return "OK!" diff --git a/homeassistant/components/totalconnect/alarm_control_panel.py b/homeassistant/components/totalconnect/alarm_control_panel.py index b89df6c9c25..3f2c51989f9 100644 --- a/homeassistant/components/totalconnect/alarm_control_panel.py +++ b/homeassistant/components/totalconnect/alarm_control_panel.py @@ -1,4 +1,5 @@ """Interfaces with TotalConnect alarm control panels.""" + from __future__ import annotations from total_connect_client import ArmingHelper @@ -35,21 +36,21 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up TotalConnect alarm panels based on a config entry.""" - alarms = [] + alarms: list[TotalConnectAlarm] = [] coordinator: TotalConnectDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] for location_id, location in coordinator.client.locations.items(): location_name = location.location_name - for partition_id in location.partitions: - alarms.append( - TotalConnectAlarm( - coordinator=coordinator, - name=location_name, - location_id=location_id, - partition_id=partition_id, - ) + alarms.extend( + TotalConnectAlarm( + coordinator=coordinator, + name=location_name, + location_id=location_id, + partition_id=partition_id, ) + for partition_id in location.partitions + ) async_add_entities(alarms) diff --git a/homeassistant/components/totalconnect/binary_sensor.py b/homeassistant/components/totalconnect/binary_sensor.py index 9caa642b5f4..c6c7c75e0b5 100644 --- a/homeassistant/components/totalconnect/binary_sensor.py +++ b/homeassistant/components/totalconnect/binary_sensor.py @@ -1,4 +1,5 @@ """Interfaces with TotalConnect sensors.""" + import logging from homeassistant.components.binary_sensor import ( diff --git a/homeassistant/components/totalconnect/config_flow.py b/homeassistant/components/totalconnect/config_flow.py index 8d35506af0f..19d8f09933e 100644 --- a/homeassistant/components/totalconnect/config_flow.py +++ b/homeassistant/components/totalconnect/config_flow.py @@ -1,4 +1,5 @@ """Config flow for the Total Connect component.""" + from __future__ import annotations from collections.abc import Mapping @@ -8,17 +9,21 @@ from total_connect_client.client import TotalConnectClient from total_connect_client.exceptions import AuthenticationError import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.const import CONF_LOCATION, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult from .const import AUTO_BYPASS, CONF_USERCODES, DOMAIN PASSWORD_DATA_SCHEMA = vol.Schema({vol.Required(CONF_PASSWORD): str}) -class TotalConnectConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class TotalConnectConfigFlow(ConfigFlow, domain=DOMAIN): """Total Connect config flow.""" VERSION = 1 @@ -125,7 +130,9 @@ class TotalConnectConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): description_placeholders={"location_id": location_for_user}, ) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Perform reauth upon an authentication error or no usercode.""" self.username = entry_data[CONF_USERNAME] self.usercodes = entry_data[CONF_USERCODES] @@ -173,16 +180,16 @@ class TotalConnectConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback def async_get_options_flow( - config_entry: config_entries.ConfigEntry, + config_entry: ConfigEntry, ) -> TotalConnectOptionsFlowHandler: """Get options flow.""" return TotalConnectOptionsFlowHandler(config_entry) -class TotalConnectOptionsFlowHandler(config_entries.OptionsFlow): +class TotalConnectOptionsFlowHandler(OptionsFlow): """TotalConnect options flow handler.""" - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + def __init__(self, config_entry: ConfigEntry) -> None: """Initialize options flow.""" self.config_entry = config_entry diff --git a/homeassistant/components/totalconnect/diagnostics.py b/homeassistant/components/totalconnect/diagnostics.py index 4a9a73c89a9..e3f9b9ba6b3 100644 --- a/homeassistant/components/totalconnect/diagnostics.py +++ b/homeassistant/components/totalconnect/diagnostics.py @@ -1,4 +1,5 @@ """Provides diagnostics for TotalConnect.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/totalconnect/icons.json b/homeassistant/components/totalconnect/icons.json new file mode 100644 index 00000000000..356ce0a929b --- /dev/null +++ b/homeassistant/components/totalconnect/icons.json @@ -0,0 +1,6 @@ +{ + "services": { + "arm_away_instant": "mdi:shield-lock", + "arm_home_instant": "mdi:shield-home" + } +} diff --git a/homeassistant/components/touchline/climate.py b/homeassistant/components/touchline/climate.py index 5004646a667..5b1c52534c5 100644 --- a/homeassistant/components/touchline/climate.py +++ b/homeassistant/components/touchline/climate.py @@ -1,4 +1,5 @@ """Platform for Roth Touchline floor heating controller.""" + from __future__ import annotations from typing import Any, NamedTuple @@ -54,10 +55,10 @@ def setup_platform( host = config[CONF_HOST] py_touchline = PyTouchline() number_of_devices = int(py_touchline.get_number_of_devices(host)) - devices = [] - for device_id in range(0, number_of_devices): - devices.append(Touchline(PyTouchline(device_id))) - add_entities(devices, True) + add_entities( + (Touchline(PyTouchline(device_id)) for device_id in range(number_of_devices)), + True, + ) class Touchline(ClimateEntity): diff --git a/homeassistant/components/tplink/__init__.py b/homeassistant/components/tplink/__init__.py index b8510f7ef81..fbb176b2d5f 100644 --- a/homeassistant/components/tplink/__init__.py +++ b/homeassistant/components/tplink/__init__.py @@ -1,4 +1,5 @@ """Component to embed TP-Link smart home devices.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/tplink/config_flow.py b/homeassistant/components/tplink/config_flow.py index 643748f175e..919e1a537e5 100644 --- a/homeassistant/components/tplink/config_flow.py +++ b/homeassistant/components/tplink/config_flow.py @@ -1,4 +1,5 @@ """Config flow for TP-Link.""" + from __future__ import annotations from collections.abc import Mapping @@ -15,9 +16,14 @@ from kasa import ( ) import voluptuous as vol -from homeassistant import config_entries from homeassistant.components import dhcp -from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry, ConfigEntryState +from homeassistant.config_entries import ( + SOURCE_REAUTH, + ConfigEntry, + ConfigEntryState, + ConfigFlow, + ConfigFlowResult, +) from homeassistant.const import ( CONF_ALIAS, CONF_DEVICE, @@ -28,7 +34,6 @@ from homeassistant.const import ( CONF_USERNAME, ) 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 @@ -46,7 +51,7 @@ STEP_AUTH_DATA_SCHEMA = vol.Schema( ) -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for tplink.""" VERSION = 1 @@ -58,7 +63,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._discovered_devices: dict[str, SmartDevice] = {} self._discovered_device: SmartDevice | None = None - async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: + async def async_step_dhcp( + self, discovery_info: dhcp.DhcpServiceInfo + ) -> ConfigFlowResult: """Handle discovery via dhcp.""" return await self._async_handle_discovery( discovery_info.ip, dr.format_mac(discovery_info.macaddress) @@ -66,7 +73,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_integration_discovery( self, discovery_info: DiscoveryInfoType - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle integration discovery.""" return await self._async_handle_discovery( discovery_info[CONF_HOST], @@ -77,7 +84,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @callback def _update_config_if_entry_in_setup_error( self, entry: ConfigEntry, host: str, config: dict - ) -> FlowResult | None: + ) -> ConfigFlowResult | None: """If discovery encounters a device that is in SETUP_ERROR or SETUP_RETRY update the device config.""" if entry.state not in ( ConfigEntryState.SETUP_ERROR, @@ -96,7 +103,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def _async_handle_discovery( self, host: str, formatted_mac: str, config: dict | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle any discovery.""" current_entry = await self.async_set_unique_id( formatted_mac, raise_on_progress=False @@ -131,7 +138,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_discovery_auth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Dialog that informs the user that auth is required.""" assert self._discovered_device is not None errors = {} @@ -190,7 +197,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_discovery_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm discovery.""" assert self._discovered_device is not None if user_input is not None: @@ -205,7 +212,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors: dict[str, str] = {} placeholders: dict[str, str] = {} @@ -237,7 +244,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user_auth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Dialog that informs the user that auth is required.""" errors: dict[str, str] = {} host = self.context[CONF_HOST] @@ -272,7 +279,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_pick_device( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the step to pick discovered device.""" if user_input is not None: mac = user_input[CONF_DEVICE] @@ -332,7 +339,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): _config_entries.flow.async_abort(flow["flow_id"]) @callback - def _async_create_entry_from_device(self, device: SmartDevice) -> FlowResult: + def _async_create_entry_from_device(self, device: SmartDevice) -> ConfigFlowResult: """Create a config entry from a smart device.""" self._abort_if_unique_id_configured(updates={CONF_HOST: device.host}) return self.async_create_entry( @@ -401,7 +408,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) return self._discovered_device - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Start the reauthentication flow if the device needs updated credentials.""" self.reauth_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] @@ -410,7 +419,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Dialog that informs the user that reauth is required.""" errors: dict[str, str] = {} placeholders: dict[str, str] = {} diff --git a/homeassistant/components/tplink/const.py b/homeassistant/components/tplink/const.py index 57047af8092..96892bacee7 100644 --- a/homeassistant/components/tplink/const.py +++ b/homeassistant/components/tplink/const.py @@ -1,4 +1,5 @@ """Const for TP-Link.""" + from __future__ import annotations from typing import Final diff --git a/homeassistant/components/tplink/coordinator.py b/homeassistant/components/tplink/coordinator.py index 798580ef3c2..94ad94de0ae 100644 --- a/homeassistant/components/tplink/coordinator.py +++ b/homeassistant/components/tplink/coordinator.py @@ -1,4 +1,5 @@ """Component to embed TP-Link smart home devices.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/tplink/diagnostics.py b/homeassistant/components/tplink/diagnostics.py index c1b0cf12bfc..e5e84b48162 100644 --- a/homeassistant/components/tplink/diagnostics.py +++ b/homeassistant/components/tplink/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for TPLink.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/tplink/entity.py b/homeassistant/components/tplink/entity.py index 987ac455ae1..4720fae1259 100644 --- a/homeassistant/components/tplink/entity.py +++ b/homeassistant/components/tplink/entity.py @@ -1,4 +1,5 @@ """Common code for tplink.""" + from __future__ import annotations from collections.abc import Awaitable, Callable, Coroutine diff --git a/homeassistant/components/tplink/icons.json b/homeassistant/components/tplink/icons.json new file mode 100644 index 00000000000..9b83b3abc85 --- /dev/null +++ b/homeassistant/components/tplink/icons.json @@ -0,0 +1,16 @@ +{ + "entity": { + "switch": { + "led": { + "default": "mdi:led-off", + "state": { + "on": "mdi:led-on" + } + } + } + }, + "services": { + "sequence_effect": "mdi:playlist-play", + "random_effect": "mdi:shuffle-variant" + } +} diff --git a/homeassistant/components/tplink/light.py b/homeassistant/components/tplink/light.py index e27ee7de49f..d007868930a 100644 --- a/homeassistant/components/tplink/light.py +++ b/homeassistant/components/tplink/light.py @@ -1,4 +1,5 @@ """Support for TPLink lights.""" + from __future__ import annotations from collections.abc import Sequence diff --git a/homeassistant/components/tplink/manifest.json b/homeassistant/components/tplink/manifest.json index f0a4696fd0b..a91e7e5a46f 100644 --- a/homeassistant/components/tplink/manifest.json +++ b/homeassistant/components/tplink/manifest.json @@ -266,7 +266,6 @@ } ], "documentation": "https://www.home-assistant.io/integrations/tplink", - "import_executor": true, "iot_class": "local_polling", "loggers": ["kasa"], "quality_scale": "platinum", diff --git a/homeassistant/components/tplink/models.py b/homeassistant/components/tplink/models.py index 4367f46711d..ced58d3d21f 100644 --- a/homeassistant/components/tplink/models.py +++ b/homeassistant/components/tplink/models.py @@ -1,4 +1,5 @@ """The tplink integration models.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/homeassistant/components/tplink/sensor.py b/homeassistant/components/tplink/sensor.py index a3bb35840b2..1f6b07365b5 100644 --- a/homeassistant/components/tplink/sensor.py +++ b/homeassistant/components/tplink/sensor.py @@ -1,4 +1,5 @@ """Support for TPLink HS100/HS110/HS200 smart switch energy sensors.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/homeassistant/components/tplink/switch.py b/homeassistant/components/tplink/switch.py index 3e81870d80f..da3dda9c041 100644 --- a/homeassistant/components/tplink/switch.py +++ b/homeassistant/components/tplink/switch.py @@ -1,4 +1,5 @@ """Support for TPLink HS100/HS110/HS200 smart switch.""" + from __future__ import annotations import logging @@ -36,8 +37,10 @@ async def async_setup_entry( if device.is_strip: # Historically we only add the children if the device is a strip _LOGGER.debug("Initializing strip with %s sockets", len(device.children)) - for child in device.children: - entities.append(SmartPlugSwitchChild(device, parent_coordinator, child)) + entities.extend( + SmartPlugSwitchChild(device, parent_coordinator, child) + for child in device.children + ) elif device.is_plug: entities.append(SmartPlugSwitch(device, parent_coordinator)) @@ -61,7 +64,7 @@ class SmartPlugLedSwitch(CoordinatedTPLinkEntity, SwitchEntity): ) -> None: """Initialize the LED switch.""" super().__init__(device, coordinator) - self._attr_unique_id = f"{self.device.mac}_led" + self._attr_unique_id = f"{device.mac}_led" self._async_update_attrs() @async_refresh_after @@ -77,9 +80,7 @@ class SmartPlugLedSwitch(CoordinatedTPLinkEntity, SwitchEntity): @callback def _async_update_attrs(self) -> None: """Update the entity's attributes.""" - is_on = self.device.led - self._attr_is_on = is_on - self._attr_icon = "mdi:led-on" if is_on else "mdi:led-off" + self._attr_is_on = self.device.led @callback def _handle_coordinator_update(self) -> None: diff --git a/homeassistant/components/tplink_lte/__init__.py b/homeassistant/components/tplink_lte/__init__.py index d64dc003576..ca9b8311ebe 100644 --- a/homeassistant/components/tplink_lte/__init__.py +++ b/homeassistant/components/tplink_lte/__init__.py @@ -1,4 +1,5 @@ """Support for TP-Link LTE modems.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/tplink_lte/notify.py b/homeassistant/components/tplink_lte/notify.py index eb742a5e4e9..674f09efcd7 100644 --- a/homeassistant/components/tplink_lte/notify.py +++ b/homeassistant/components/tplink_lte/notify.py @@ -1,4 +1,5 @@ """Support for TP-Link LTE notifications.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/tplink_omada/__init__.py b/homeassistant/components/tplink_omada/__init__.py index 265b31bce9c..fa022fcac77 100644 --- a/homeassistant/components/tplink_omada/__init__.py +++ b/homeassistant/components/tplink_omada/__init__.py @@ -1,4 +1,5 @@ """The TP-Link Omada integration.""" + from __future__ import annotations from tplink_omada_client import OmadaSite @@ -46,6 +47,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: site_client = await client.get_site_client(OmadaSite("", entry.data[CONF_SITE])) controller = OmadaSiteController(hass, site_client) + gateway_coordinator = await controller.get_gateway_coordinator() + if gateway_coordinator: + await gateway_coordinator.async_config_entry_first_refresh() + hass.data[DOMAIN][entry.entry_id] = controller await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/homeassistant/components/tplink_omada/binary_sensor.py b/homeassistant/components/tplink_omada/binary_sensor.py index d2679b8b8d4..c0304c4d1b2 100644 --- a/homeassistant/components/tplink_omada/binary_sensor.py +++ b/homeassistant/components/tplink_omada/binary_sensor.py @@ -1,19 +1,21 @@ """Support for TPLink Omada binary sensors.""" + from __future__ import annotations -from collections.abc import Callable, Generator +from collections.abc import Callable +from dataclasses import dataclass -from attr import dataclass -from tplink_omada_client.definitions import GatewayPortMode, LinkStatus +from tplink_omada_client.definitions import GatewayPortMode, LinkStatus, PoEMode from tplink_omada_client.devices import ( OmadaDevice, - OmadaGateway, + OmadaGatewayPortConfig, OmadaGatewayPortStatus, ) from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, BinarySensorEntity, + BinarySensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback @@ -31,94 +33,105 @@ async def async_setup_entry( ) -> None: """Set up binary sensors.""" controller: OmadaSiteController = hass.data[DOMAIN][config_entry.entry_id] - omada_client = controller.omada_client gateway_coordinator = await controller.get_gateway_coordinator() if not gateway_coordinator: return - gateway = await omada_client.get_gateway(gateway_coordinator.mac) - - async_add_entities( - get_gateway_port_status_sensors(gateway, hass, gateway_coordinator) - ) - - await gateway_coordinator.async_request_refresh() - - -def get_gateway_port_status_sensors( - gateway: OmadaGateway, hass: HomeAssistant, coordinator: OmadaGatewayCoordinator -) -> Generator[BinarySensorEntity, None, None]: - """Generate binary sensors for gateway ports.""" - for port in gateway.port_status: - if port.mode == GatewayPortMode.WAN: - yield OmadaGatewayPortBinarySensor( - coordinator, - gateway, - GatewayPortBinarySensorConfig( - port_number=port.port_number, - id_suffix="wan_link", - name_suffix="Internet Link", - device_class=BinarySensorDeviceClass.CONNECTIVITY, - update_func=lambda p: p.wan_connected, - ), - ) - if port.mode == GatewayPortMode.LAN: - yield OmadaGatewayPortBinarySensor( - coordinator, - gateway, - GatewayPortBinarySensorConfig( - port_number=port.port_number, - id_suffix="lan_status", - name_suffix="LAN Status", - device_class=BinarySensorDeviceClass.CONNECTIVITY, - update_func=lambda p: p.link_status == LinkStatus.LINK_UP, - ), + entities: list[OmadaDeviceEntity] = [] + for gateway in gateway_coordinator.data.values(): + entities.extend( + OmadaGatewayPortBinarySensor( + gateway_coordinator, gateway, p.port_number, desc ) + for p in gateway.port_configs + for desc in GATEWAY_PORT_SENSORS + if desc.exists_func(p) + ) + + async_add_entities(entities) -@dataclass -class GatewayPortBinarySensorConfig: - """Config for a binary status derived from a gateway port.""" +@dataclass(frozen=True, kw_only=True) +class GatewayPortBinarySensorEntityDescription(BinarySensorEntityDescription): + """Entity description for a binary status derived from a gateway port.""" - port_number: int - id_suffix: str - name_suffix: str - device_class: BinarySensorDeviceClass + exists_func: Callable[[OmadaGatewayPortConfig], bool] = lambda _: True update_func: Callable[[OmadaGatewayPortStatus], bool] -class OmadaGatewayPortBinarySensor(OmadaDeviceEntity[OmadaGateway], BinarySensorEntity): +GATEWAY_PORT_SENSORS: list[GatewayPortBinarySensorEntityDescription] = [ + GatewayPortBinarySensorEntityDescription( + key="wan_link", + translation_key="wan_link", + device_class=BinarySensorDeviceClass.CONNECTIVITY, + exists_func=lambda p: p.port_status.mode == GatewayPortMode.WAN, + update_func=lambda p: p.wan_connected, + ), + GatewayPortBinarySensorEntityDescription( + key="online_detection", + translation_key="online_detection", + device_class=BinarySensorDeviceClass.CONNECTIVITY, + exists_func=lambda p: p.port_status.mode == GatewayPortMode.WAN, + update_func=lambda p: p.online_detection, + ), + GatewayPortBinarySensorEntityDescription( + key="lan_status", + translation_key="lan_status", + device_class=BinarySensorDeviceClass.CONNECTIVITY, + exists_func=lambda p: p.port_status.mode == GatewayPortMode.LAN, + update_func=lambda p: p.link_status == LinkStatus.LINK_UP, + ), + GatewayPortBinarySensorEntityDescription( + key="poe_delivery", + translation_key="poe_delivery", + device_class=BinarySensorDeviceClass.POWER, + exists_func=lambda p: ( + p.port_status.mode == GatewayPortMode.LAN and p.poe_mode == PoEMode.ENABLED + ), + update_func=lambda p: p.poe_active, + ), +] + + +class OmadaGatewayPortBinarySensor( + OmadaDeviceEntity[OmadaGatewayCoordinator], BinarySensorEntity +): """Binary status of a property on an internet gateway.""" + entity_description: GatewayPortBinarySensorEntityDescription _attr_has_entity_name = True def __init__( self, coordinator: OmadaGatewayCoordinator, device: OmadaDevice, - config: GatewayPortBinarySensorConfig, + port_number: int, + entity_description: GatewayPortBinarySensorEntityDescription, ) -> None: """Initialize the gateway port binary sensor.""" super().__init__(coordinator, device) - self._config = config - self._attr_unique_id = f"{device.mac}_{config.port_number}_{config.id_suffix}" - self._attr_device_class = config.device_class + self.entity_description = entity_description + self._port_number = port_number + self._attr_unique_id = f"{device.mac}_{port_number}_{entity_description.key}" + self._attr_translation_placeholders = {"port_name": f"{port_number}"} - self._attr_name = f"Port {config.port_number} {config.name_suffix}" + async def async_added_to_hass(self) -> None: + """When entity is added to hass.""" + await super().async_added_to_hass() + self._do_update() + + def _do_update(self) -> None: + gateway = self.coordinator.data[self.device.mac] + + port = next( + p for p in gateway.port_status if p.port_number == self._port_number + ) + if port: + self._attr_is_on = self.entity_description.update_func(port) @callback def _handle_coordinator_update(self) -> None: """Handle updated data from the coordinator.""" - gateway = self.coordinator.data[self.device.mac] - - port = next( - p for p in gateway.port_status if p.port_number == self._config.port_number - ) - if port: - self._attr_is_on = self._config.update_func(port) - self._attr_available = True - else: - self._attr_available = False - + self._do_update() self.async_write_ha_state() diff --git a/homeassistant/components/tplink_omada/config_flow.py b/homeassistant/components/tplink_omada/config_flow.py index e49e8ccf657..4666968924d 100644 --- a/homeassistant/components/tplink_omada/config_flow.py +++ b/homeassistant/components/tplink_omada/config_flow.py @@ -1,4 +1,5 @@ """Config flow for TP-Link Omada integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -18,10 +19,9 @@ from tplink_omada_client.exceptions import ( ) import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_VERIFY_SSL from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import selector from homeassistant.helpers.aiohttp_client import ( async_create_clientsession, @@ -92,7 +92,7 @@ async def _validate_input(hass: HomeAssistant, data: dict[str, Any]) -> HubInfo: return HubInfo(controller_id, name, sites) -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class TpLinkOmadaConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for TP-Link Omada.""" VERSION = 1 @@ -105,7 +105,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors: dict[str, str] = {} @@ -130,7 +130,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_site( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle step to select site to manage.""" if user_input is None: @@ -159,14 +159,16 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_create_entry(title=display_name, data=self._omada_opts) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Perform reauth upon an API authentication error.""" self._omada_opts = dict(entry_data) return await self.async_step_reauth_confirm() async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Dialog that informs the user that reauth is required.""" errors: dict[str, str] = {} diff --git a/homeassistant/components/tplink_omada/coordinator.py b/homeassistant/components/tplink_omada/coordinator.py index a0f3e6ff9b3..893d2e2778d 100644 --- a/homeassistant/components/tplink_omada/coordinator.py +++ b/homeassistant/components/tplink_omada/coordinator.py @@ -1,4 +1,5 @@ """Generic Omada API coordinator.""" + import asyncio from datetime import timedelta import logging diff --git a/homeassistant/components/tplink_omada/entity.py b/homeassistant/components/tplink_omada/entity.py index 5008b7e4b18..a0bb562c652 100644 --- a/homeassistant/components/tplink_omada/entity.py +++ b/homeassistant/components/tplink_omada/entity.py @@ -1,5 +1,6 @@ """Base entity definitions.""" -from typing import Generic, TypeVar + +from typing import Any, Generic, TypeVar from tplink_omada_client.devices import OmadaDevice @@ -10,13 +11,13 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN from .coordinator import OmadaCoordinator -T = TypeVar("T") +T = TypeVar("T", bound="OmadaCoordinator[Any]") -class OmadaDeviceEntity(CoordinatorEntity[OmadaCoordinator[T]], Generic[T]): +class OmadaDeviceEntity(CoordinatorEntity[T], Generic[T]): """Common base class for all entities associated with Omada SDN Devices.""" - def __init__(self, coordinator: OmadaCoordinator[T], device: OmadaDevice) -> None: + def __init__(self, coordinator: T, device: OmadaDevice) -> None: """Initialize the device.""" super().__init__(coordinator) self.device = device diff --git a/homeassistant/components/tplink_omada/icons.json b/homeassistant/components/tplink_omada/icons.json new file mode 100644 index 00000000000..d0c407a9326 --- /dev/null +++ b/homeassistant/components/tplink_omada/icons.json @@ -0,0 +1,23 @@ +{ + "entity": { + "switch": { + "poe_control": { + "default": "mdi:ethernet" + }, + "wan_connect_ipv4": { + "default": "mdi:wan" + }, + "wan_connect_ipv6": { + "default": "mdi:wan" + } + }, + "binary_sensor": { + "online_detection": { + "default": "mdi:cloud-check", + "state": { + "off": "mdi:cloud-cancel" + } + } + } + } +} diff --git a/homeassistant/components/tplink_omada/manifest.json b/homeassistant/components/tplink_omada/manifest.json index 33fc85d7c79..9544470d7a9 100644 --- a/homeassistant/components/tplink_omada/manifest.json +++ b/homeassistant/components/tplink_omada/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/tplink_omada", "integration_type": "hub", "iot_class": "local_polling", - "requirements": ["tplink-omada-client==1.3.11"] + "requirements": ["tplink-omada-client==1.3.12"] } diff --git a/homeassistant/components/tplink_omada/strings.json b/homeassistant/components/tplink_omada/strings.json index 04fa6d162d3..49873b7d088 100644 --- a/homeassistant/components/tplink_omada/strings.json +++ b/homeassistant/components/tplink_omada/strings.json @@ -39,5 +39,32 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } + }, + "entity": { + "switch": { + "poe_control": { + "name": "Port {port_name} PoE" + }, + "wan_connect_ipv4": { + "name": "Port {port_name} Internet Connected" + }, + "wan_connect_ipv6": { + "name": "Port {port_name} Internet Connected (IPv6)" + } + }, + "binary_sensor": { + "wan_link": { + "name": "Port {port_name} Internet Link" + }, + "online_detection": { + "name": "Port {port_name} Online Detection" + }, + "lan_status": { + "name": "Port {port_name} LAN Status" + }, + "poe_delivery": { + "name": "Port {port_name} PoE Delivery" + } + } } } diff --git a/homeassistant/components/tplink_omada/switch.py b/homeassistant/components/tplink_omada/switch.py index f8a124b94fc..b8abb4cb773 100644 --- a/homeassistant/components/tplink_omada/switch.py +++ b/homeassistant/components/tplink_omada/switch.py @@ -1,23 +1,42 @@ """Support for TPLink Omada device toggle options.""" + from __future__ import annotations -from typing import Any +from collections.abc import Awaitable, Callable +from dataclasses import dataclass +from functools import partial +from typing import Any, Generic, TypeVar -from tplink_omada_client import SwitchPortOverrides -from tplink_omada_client.definitions import PoEMode -from tplink_omada_client.devices import OmadaSwitch, OmadaSwitchPortDetails +from tplink_omada_client import OmadaSiteClient, SwitchPortOverrides +from tplink_omada_client.definitions import GatewayPortMode, PoEMode, PortType +from tplink_omada_client.devices import ( + OmadaDevice, + OmadaGateway, + OmadaGatewayPortConfig, + OmadaGatewayPortStatus, + OmadaSwitch, + OmadaSwitchPortDetails, +) +from tplink_omada_client.omadasiteclient import GatewayPortSettings -from homeassistant.components.switch import SwitchEntity +from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN -from .controller import OmadaSiteController, OmadaSwitchPortCoordinator +from .controller import ( + OmadaGatewayCoordinator, + OmadaSiteController, + OmadaSwitchPortCoordinator, +) +from .coordinator import OmadaCoordinator from .entity import OmadaDeviceEntity -POE_SWITCH_ICON = "mdi:ethernet" +TPort = TypeVar("TPort") +TDevice = TypeVar("TDevice", bound="OmadaDevice") +TCoordinator = TypeVar("TCoordinator", bound="OmadaCoordinator[Any]") async def async_setup_entry( @@ -39,74 +58,247 @@ async def async_setup_entry( coordinator = controller.get_switch_port_coordinator(switch) await coordinator.async_request_refresh() - for idx, port_id in enumerate(coordinator.data): - if idx < switch.device_capabilities.poe_ports: - entities.append( - OmadaNetworkSwitchPortPoEControl(coordinator, switch, port_id) - ) + entities.extend( + OmadaDevicePortSwitchEntity[ + OmadaSwitchPortCoordinator, OmadaSwitch, OmadaSwitchPortDetails + ]( + coordinator, + switch, + port.port_id, + desc, + port_name=_get_switch_port_base_name(port), + ) + for port in coordinator.data.values() + for desc in SWITCH_PORT_DETAILS_SWITCHES + if desc.exists_func(switch, port) + ) + + gateway_coordinator = await controller.get_gateway_coordinator() + if gateway_coordinator: + for gateway in gateway_coordinator.data.values(): + entities.extend( + OmadaDevicePortSwitchEntity[ + OmadaGatewayCoordinator, OmadaGateway, OmadaGatewayPortStatus + ](gateway_coordinator, gateway, p.port_number, desc) + for p in gateway.port_status + for desc in GATEWAY_PORT_STATUS_SWITCHES + if desc.exists_func(gateway, p) + ) + entities.extend( + OmadaDevicePortSwitchEntity[ + OmadaGatewayCoordinator, OmadaGateway, OmadaGatewayPortConfig + ](gateway_coordinator, gateway, p.port_number, desc) + for p in gateway.port_configs + for desc in GATEWAY_PORT_CONFIG_SWITCHES + if desc.exists_func(gateway, p) + ) async_add_entities(entities) -def get_port_base_name(port: OmadaSwitchPortDetails) -> str: +def _get_switch_port_base_name(port: OmadaSwitchPortDetails) -> str: """Get display name for a switch port.""" if port.name == f"Port{port.port}": - return f"Port {port.port}" - return f"Port {port.port} ({port.name})" + return str(port.port) + return f"{port.port} ({port.name})" -class OmadaNetworkSwitchPortPoEControl( - OmadaDeviceEntity[OmadaSwitchPortDetails], SwitchEntity +@dataclass(frozen=True, kw_only=True) +class OmadaDevicePortSwitchEntityDescription( + SwitchEntityDescription, Generic[TCoordinator, TDevice, TPort] ): - """Representation of a PoE control toggle on a single network port on a switch.""" + """Entity description for a toggle switch derived from a network port on an Omada device.""" + + exists_func: Callable[[TDevice, TPort], bool] = lambda _, p: True + coordinator_update_func: Callable[ + [TCoordinator, TDevice, int | str], TPort | None + ] = lambda *_: None + set_func: Callable[[OmadaSiteClient, TDevice, TPort, bool], Awaitable[TPort]] + update_func: Callable[[TPort], bool] + refresh_after_set: bool = False + + +@dataclass(frozen=True, kw_only=True) +class OmadaSwitchPortSwitchEntityDescription( + OmadaDevicePortSwitchEntityDescription[ + OmadaSwitchPortCoordinator, OmadaSwitch, OmadaSwitchPortDetails + ] +): + """Entity description for a toggle switch for a feature of a Port on an Omada Switch.""" + + coordinator_update_func: Callable[ + [OmadaSwitchPortCoordinator, OmadaSwitch, int | str], + OmadaSwitchPortDetails | None, + ] = lambda coord, _, port_id: coord.data.get(str(port_id)) + + +@dataclass(frozen=True, kw_only=True) +class OmadaGatewayPortConfigSwitchEntityDescription( + OmadaDevicePortSwitchEntityDescription[ + OmadaGatewayCoordinator, OmadaGateway, OmadaGatewayPortConfig + ] +): + """Entity description for a toggle switch for a configuration of a Port on an Omada Gateway.""" + + coordinator_update_func: Callable[ + [OmadaGatewayCoordinator, OmadaGateway, int | str], + OmadaGatewayPortConfig | None, + ] = lambda coord, device, port_id: next( + p for p in coord.data[device.mac].port_configs if p.port_number == port_id + ) + + +@dataclass(frozen=True, kw_only=True) +class OmadaGatewayPortStatusSwitchEntityDescription( + OmadaDevicePortSwitchEntityDescription[ + OmadaGatewayCoordinator, OmadaGateway, OmadaGatewayPortStatus + ] +): + """Entity description for a toggle switch for a status of a Port on an Omada Gateway.""" + + coordinator_update_func: Callable[ + [OmadaGatewayCoordinator, OmadaGateway, int | str], OmadaGatewayPortStatus + ] = lambda coord, device, port_id: next( + p for p in coord.data[device.mac].port_status if p.port_number == port_id + ) + + +def _wan_connect_disconnect( + client: OmadaSiteClient, + device: OmadaDevice, + port: OmadaGatewayPortStatus, + enable: bool, + ipv6: bool, +) -> Awaitable[OmadaGatewayPortStatus]: + return client.set_gateway_wan_port_connect_state( + port.port_number, enable, device, ipv6=ipv6 + ) + + +SWITCH_PORT_DETAILS_SWITCHES: list[OmadaSwitchPortSwitchEntityDescription] = [ + OmadaSwitchPortSwitchEntityDescription( + key="poe", + translation_key="poe_control", + exists_func=lambda d, p: d.device_capabilities.supports_poe + and p.type != PortType.SFP, + set_func=lambda client, device, port, enable: client.update_switch_port( + device, port, overrides=SwitchPortOverrides(enable_poe=enable) + ), + update_func=lambda p: p.poe_mode != PoEMode.DISABLED, + entity_category=EntityCategory.CONFIG, + ) +] + +GATEWAY_PORT_STATUS_SWITCHES: list[OmadaGatewayPortStatusSwitchEntityDescription] = [ + OmadaGatewayPortStatusSwitchEntityDescription( + key="wan_connect_ipv4", + translation_key="wan_connect_ipv4", + exists_func=lambda _, p: p.mode == GatewayPortMode.WAN, + set_func=partial(_wan_connect_disconnect, ipv6=False), + update_func=lambda p: p.wan_connected, + refresh_after_set=True, + ), + OmadaGatewayPortStatusSwitchEntityDescription( + key="wan_connect_ipv6", + translation_key="wan_connect_ipv6", + exists_func=lambda _, p: p.mode == GatewayPortMode.WAN and p.wan_ipv6_enabled, + set_func=partial(_wan_connect_disconnect, ipv6=True), + update_func=lambda p: p.ipv6_wan_connected, + refresh_after_set=True, + ), +] + +GATEWAY_PORT_CONFIG_SWITCHES: list[OmadaGatewayPortConfigSwitchEntityDescription] = [ + OmadaGatewayPortConfigSwitchEntityDescription( + key="poe", + translation_key="poe_control", + exists_func=lambda _, port: port.poe_mode != PoEMode.NONE, + set_func=lambda client, device, port, enable: client.set_gateway_port_settings( + port.port_number, GatewayPortSettings(enable_poe=enable), device + ), + update_func=lambda p: p.poe_mode != PoEMode.DISABLED, + ), +] + + +class OmadaDevicePortSwitchEntity( + OmadaDeviceEntity[TCoordinator], + SwitchEntity, + Generic[TCoordinator, TDevice, TPort], +): + """Generic toggle switch entity for a Netork Port of an Omada Device.""" _attr_has_entity_name = True - _attr_entity_category = EntityCategory.CONFIG - _attr_icon = POE_SWITCH_ICON + _port_details: TPort | None = None + entity_description: OmadaDevicePortSwitchEntityDescription[ + TCoordinator, TDevice, TPort + ] def __init__( self, - coordinator: OmadaSwitchPortCoordinator, - device: OmadaSwitch, - port_id: str, + coordinator: TCoordinator, + device: TDevice, + port_id: int | str, + entity_description: OmadaDevicePortSwitchEntityDescription[ + TCoordinator, TDevice, TPort + ], + port_name: str | None = None, ) -> None: - """Initialize the PoE switch.""" + """Initialize the toggle switch.""" super().__init__(coordinator, device) - self.port_id = port_id - self.port_details = self.coordinator.data[port_id] - self.omada_client = self.coordinator.omada_client - self._attr_unique_id = f"{device.mac}_{port_id}_poe" - - self._attr_name = f"{get_port_base_name(self.port_details)} PoE" + self.entity_description = entity_description + self._device = device + self._port_id = port_id + self._attr_unique_id = f"{device.mac}_{port_id}_{entity_description.key}" + self._attr_translation_placeholders = {"port_name": port_name or str(port_id)} async def async_added_to_hass(self) -> None: """When entity is added to hass.""" await super().async_added_to_hass() - self._refresh_state() + self._do_update() - async def _async_turn_on_off_poe(self, enable: bool) -> None: - self.port_details = await self.omada_client.update_switch_port( - self.device, - self.port_details, - overrides=SwitchPortOverrides(enable_poe=enable), - ) - self._refresh_state() + async def _async_turn_on_off(self, enable: bool) -> None: + if self._port_details: + self._port_details = await self.entity_description.set_func( + self.coordinator.omada_client, self._device, self._port_details, enable + ) + + if self.entity_description.refresh_after_set: + # Refresh to make sure the requested changes stuck + self._attr_is_on = enable + await self.coordinator.async_request_refresh() + elif self._port_details: + self._attr_is_on = self.entity_description.update_func(self._port_details) + self.async_write_ha_state() async def async_turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" - await self._async_turn_on_off_poe(True) + await self._async_turn_on_off(True) async def async_turn_off(self, **kwargs: Any) -> None: """Turn the entity off.""" - await self._async_turn_on_off_poe(False) + await self._async_turn_on_off(False) - def _refresh_state(self) -> None: - self._attr_is_on = self.port_details.poe_mode != PoEMode.DISABLED - self.async_write_ha_state() + @property + def available(self) -> bool: + """Return true if entity is available.""" + return bool( + super().available + and self._port_details + and self.entity_description.exists_func(self._device, self._port_details) + ) + + def _do_update(self) -> None: + port = self.entity_description.coordinator_update_func( + self.coordinator, self._device, self._port_id + ) + if port: + self._port_details = port + self._attr_is_on = self.entity_description.update_func(port) @callback def _handle_coordinator_update(self) -> None: """Handle updated data from the coordinator.""" - self.port_details = self.coordinator.data[self.port_id] - self._refresh_state() + self._do_update() + self.async_write_ha_state() diff --git a/homeassistant/components/tplink_omada/update.py b/homeassistant/components/tplink_omada/update.py index 014302cec65..5e87d11474b 100644 --- a/homeassistant/components/tplink_omada/update.py +++ b/homeassistant/components/tplink_omada/update.py @@ -1,4 +1,5 @@ """Support for TPLink Omada device firmware updates.""" + from __future__ import annotations from datetime import timedelta @@ -87,7 +88,7 @@ async def async_setup_entry( class OmadaDeviceUpdate( - OmadaDeviceEntity[FirmwareUpdateStatus], + OmadaDeviceEntity[OmadaFirmwareUpdateCoodinator], UpdateEntity, ): """Firmware update status for Omada SDN devices.""" diff --git a/homeassistant/components/traccar/__init__.py b/homeassistant/components/traccar/__init__.py index 492f609907e..c42bbd1ab72 100644 --- a/homeassistant/components/traccar/__init__.py +++ b/homeassistant/components/traccar/__init__.py @@ -1,4 +1,5 @@ """Support for Traccar Client.""" + from http import HTTPStatus from aiohttp import web diff --git a/homeassistant/components/traccar/config_flow.py b/homeassistant/components/traccar/config_flow.py index 3d62d0a842d..a1c84e47467 100644 --- a/homeassistant/components/traccar/config_flow.py +++ b/homeassistant/components/traccar/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Traccar Client.""" + from homeassistant.helpers import config_entry_flow from .const import DOMAIN diff --git a/homeassistant/components/traccar/device_tracker.py b/homeassistant/components/traccar/device_tracker.py index dbcb30e3a23..d82b922f586 100644 --- a/homeassistant/components/traccar/device_tracker.py +++ b/homeassistant/components/traccar/device_tracker.py @@ -1,4 +1,5 @@ """Support for Traccar device tracking.""" + from __future__ import annotations from datetime import timedelta @@ -143,9 +144,9 @@ async def async_setup_entry( [TraccarEntity(device, latitude, longitude, battery, accuracy, attrs)] ) - hass.data[DOMAIN]["unsub_device_tracker"][ - entry.entry_id - ] = async_dispatcher_connect(hass, TRACKER_UPDATE, _receive_data) + hass.data[DOMAIN]["unsub_device_tracker"][entry.entry_id] = ( + async_dispatcher_connect(hass, TRACKER_UPDATE, _receive_data) + ) # Restore previously loaded devices dev_reg = dr.async_get(hass) diff --git a/homeassistant/components/traccar_server/__init__.py b/homeassistant/components/traccar_server/__init__.py index dac54f5e3f8..fc513136681 100644 --- a/homeassistant/components/traccar_server/__init__.py +++ b/homeassistant/components/traccar_server/__init__.py @@ -1,4 +1,5 @@ """The Traccar Server integration.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/traccar_server/config_flow.py b/homeassistant/components/traccar_server/config_flow.py index a2a7daaaa98..0fa97c8100e 100644 --- a/homeassistant/components/traccar_server/config_flow.py +++ b/homeassistant/components/traccar_server/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Traccar Server integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -17,7 +18,6 @@ from homeassistant.const import ( CONF_VERIFY_SSL, ) from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.schema_config_entry_flow import ( SchemaFlowFormStep, @@ -130,7 +130,7 @@ class TraccarServerConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None, - ) -> FlowResult: + ) -> config_entries.ConfigFlowResult: """Handle the initial step.""" errors: dict[str, str] = {} if user_input is not None: @@ -160,7 +160,9 @@ class TraccarServerConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_import(self, import_info: Mapping[str, Any]) -> FlowResult: + async def async_step_import( + self, import_info: Mapping[str, Any] + ) -> config_entries.ConfigFlowResult: """Import an entry.""" configured_port = str(import_info[CONF_PORT]) self._async_abort_entries_match( diff --git a/homeassistant/components/traccar_server/const.py b/homeassistant/components/traccar_server/const.py index ca95e706d61..36b88490f95 100644 --- a/homeassistant/components/traccar_server/const.py +++ b/homeassistant/components/traccar_server/const.py @@ -1,4 +1,5 @@ """Constants for the Traccar Server integration.""" + from logging import getLogger DOMAIN = "traccar_server" diff --git a/homeassistant/components/traccar_server/coordinator.py b/homeassistant/components/traccar_server/coordinator.py index 960fdc01fa0..3d44b1ecede 100644 --- a/homeassistant/components/traccar_server/coordinator.py +++ b/homeassistant/components/traccar_server/coordinator.py @@ -1,8 +1,10 @@ """Data update coordinator for Traccar Server.""" + from __future__ import annotations import asyncio from datetime import datetime +from logging import DEBUG as LOG_LEVEL_DEBUG from typing import TYPE_CHECKING, Any, TypedDict from pytraccar import ( @@ -91,8 +93,18 @@ class TraccarServerCoordinator(DataUpdateCoordinator[TraccarServerCoordinatorDat self._geofences = geofences + if self.logger.isEnabledFor(LOG_LEVEL_DEBUG): + self.logger.debug("Received devices: %s", devices) + self.logger.debug("Received positions: %s", positions) + for position in positions: - if (device := get_device(position["deviceId"], devices)) is None: + device_id = position["deviceId"] + if (device := get_device(device_id, devices)) is None: + self.logger.debug( + "Device %s not found for position: %s", + device_id, + position["id"], + ) continue if ( @@ -101,9 +113,14 @@ class TraccarServerCoordinator(DataUpdateCoordinator[TraccarServerCoordinatorDat device, position ) ) is None: + self.logger.debug( + "Skipping position update %s for %s due to accuracy filter", + position["id"], + device_id, + ) continue - data[device["id"]] = { + data[device_id] = { "device": device, "geofence": get_first_geofence( geofences, @@ -121,8 +138,8 @@ class TraccarServerCoordinator(DataUpdateCoordinator[TraccarServerCoordinatorDat self._should_log_subscription_error = True update_devices = set() for device in data.get("devices") or []: - device_id = device["id"] - if device_id not in self.data: + if (device_id := device["id"]) not in self.data: + self.logger.debug("Device %s not found in data", device_id) continue if ( @@ -138,8 +155,12 @@ class TraccarServerCoordinator(DataUpdateCoordinator[TraccarServerCoordinatorDat update_devices.add(device_id) for position in data.get("positions") or []: - device_id = position["deviceId"] - if device_id not in self.data: + if (device_id := position["deviceId"]) not in self.data: + self.logger.debug( + "Device %s for position %s not found in data", + device_id, + position["id"], + ) continue if ( @@ -148,6 +169,11 @@ class TraccarServerCoordinator(DataUpdateCoordinator[TraccarServerCoordinatorDat self.data[device_id]["device"], position ) ) is None: + self.logger.debug( + "Skipping position update %s for %s due to accuracy filter", + position["id"], + device_id, + ) continue self.data[device_id]["position"] = position diff --git a/homeassistant/components/traccar_server/device_tracker.py b/homeassistant/components/traccar_server/device_tracker.py index 226d942e465..e459cdacf14 100644 --- a/homeassistant/components/traccar_server/device_tracker.py +++ b/homeassistant/components/traccar_server/device_tracker.py @@ -1,4 +1,5 @@ """Support for Traccar server device tracking.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/traccar_server/diagnostics.py b/homeassistant/components/traccar_server/diagnostics.py index f4b1cc799cb..ea861a9bffa 100644 --- a/homeassistant/components/traccar_server/diagnostics.py +++ b/homeassistant/components/traccar_server/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics platform for Traccar Server.""" + from __future__ import annotations from typing import Any @@ -20,6 +21,20 @@ TO_REDACT = { } +def _entity_state( + hass: HomeAssistant, + entity: er.RegistryEntry, +) -> dict[str, Any] | None: + return ( + { + "state": state.state, + "attributes": state.attributes, + } + if (state := hass.states.get(entity.entity_id)) + else None + ) + + async def async_get_config_entry_diagnostics( hass: HomeAssistant, config_entry: ConfigEntry, @@ -42,10 +57,9 @@ async def async_get_config_entry_diagnostics( { "enity_id": entity.entity_id, "disabled": entity.disabled, - "state": {"state": state.state, "attributes": state.attributes}, + "state": _entity_state(hass, entity), } for entity in entities - if (state := hass.states.get(entity.entity_id)) is not None ], }, TO_REDACT, @@ -76,10 +90,9 @@ async def async_get_device_diagnostics( { "enity_id": entity.entity_id, "disabled": entity.disabled, - "state": {"state": state.state, "attributes": state.attributes}, + "state": _entity_state(hass, entity), } for entity in entities - if (state := hass.states.get(entity.entity_id)) is not None ], }, TO_REDACT, diff --git a/homeassistant/components/traccar_server/entity.py b/homeassistant/components/traccar_server/entity.py index 1c32008d09b..e773bf66562 100644 --- a/homeassistant/components/traccar_server/entity.py +++ b/homeassistant/components/traccar_server/entity.py @@ -1,4 +1,5 @@ """Base entity for Traccar Server.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/traccar_server/helpers.py b/homeassistant/components/traccar_server/helpers.py index ee812c35b8b..971f51376b8 100644 --- a/homeassistant/components/traccar_server/helpers.py +++ b/homeassistant/components/traccar_server/helpers.py @@ -1,4 +1,5 @@ """Helper functions for the Traccar Server integration.""" + from __future__ import annotations from pytraccar import DeviceModel, GeofenceModel diff --git a/homeassistant/components/trace/__init__.py b/homeassistant/components/trace/__init__.py index 43e591bc6e1..17fdf20368a 100644 --- a/homeassistant/components/trace/__init__.py +++ b/homeassistant/components/trace/__init__.py @@ -1,4 +1,5 @@ """Support for script and automation tracing and debugging.""" + from __future__ import annotations from collections.abc import Mapping @@ -67,7 +68,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: _LOGGER.error("Error storing traces", exc_info=exc) # Store traces when stopping hass - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_store_traces_at_stop) + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, _async_store_traces_at_stop, run_immediately=True + ) return True @@ -110,13 +113,9 @@ async def async_list_contexts( def _get_debug_traces(hass: HomeAssistant, key: str) -> list[dict[str, Any]]: """Return a serializable list of debug traces for a script or automation.""" - traces: list[dict[str, Any]] = [] - if traces_for_key := _get_data(hass).get(key): - for trace in traces_for_key.values(): - traces.append(trace.as_short_dict()) - - return traces + return [trace.as_short_dict() for trace in traces_for_key.values()] + return [] async def async_list_traces( @@ -176,7 +175,7 @@ async def async_restore_traces(hass: HomeAssistant) -> None: restored_traces = {} for key, traces in restored_traces.items(): - # Add stored traces in reversed order to priorize the newest traces + # Add stored traces in reversed order to prioritize the newest traces for json_trace in reversed(traces): if ( (stored_traces := _get_data(hass).get(key)) diff --git a/homeassistant/components/trace/models.py b/homeassistant/components/trace/models.py index 2fe37412dfb..9f65b05dcd5 100644 --- a/homeassistant/components/trace/models.py +++ b/homeassistant/components/trace/models.py @@ -1,4 +1,5 @@ """Containers for a script or automation trace.""" + from __future__ import annotations import abc diff --git a/homeassistant/components/trace/websocket_api.py b/homeassistant/components/trace/websocket_api.py index 6a5280aacf7..f1ea6133d43 100644 --- a/homeassistant/components/trace/websocket_api.py +++ b/homeassistant/components/trace/websocket_api.py @@ -1,4 +1,5 @@ """Websocket API for automation.""" + import json from typing import Any diff --git a/homeassistant/components/tractive/__init__.py b/homeassistant/components/tractive/__init__.py index 38080fffe6e..136e8b3632a 100644 --- a/homeassistant/components/tractive/__init__.py +++ b/homeassistant/components/tractive/__init__.py @@ -1,4 +1,5 @@ """The tractive integration.""" + from __future__ import annotations import asyncio @@ -39,7 +40,6 @@ from .const import ( SERVER_UNAVAILABLE, SWITCH_KEY_MAP, TRACKABLES, - TRACKER_ACTIVITY_STATUS_UPDATED, TRACKER_HARDWARE_STATUS_UPDATED, TRACKER_POSITION_UPDATED, TRACKER_SWITCH_STATUS_UPDATED, @@ -219,9 +219,6 @@ class TractiveClient: if server_was_unavailable: _LOGGER.debug("Tractive is back online") server_was_unavailable = False - if event["message"] == "activity_update": - self._send_activity_update(event) - continue if event["message"] == "wellness_overview": self._send_wellness_update(event) continue @@ -290,15 +287,6 @@ class TractiveClient: TRACKER_SWITCH_STATUS_UPDATED, event["tracker_id"], payload ) - def _send_activity_update(self, event: dict[str, Any]) -> None: - payload = { - ATTR_MINUTES_ACTIVE: event["progress"]["achieved_minutes"], - ATTR_DAILY_GOAL: event["progress"]["goal_minutes"], - } - self._dispatch_tracker_event( - TRACKER_ACTIVITY_STATUS_UPDATED, event["pet_id"], payload - ) - def _send_wellness_update(self, event: dict[str, Any]) -> None: sleep_day = None sleep_night = None @@ -308,6 +296,8 @@ class TractiveClient: payload = { ATTR_ACTIVITY_LABEL: event["wellness"].get("activity_label"), ATTR_CALORIES: event["activity"]["calories"], + ATTR_DAILY_GOAL: event["activity"]["minutes_goal"], + ATTR_MINUTES_ACTIVE: event["activity"]["minutes_active"], ATTR_MINUTES_DAY_SLEEP: sleep_day, ATTR_MINUTES_NIGHT_SLEEP: sleep_night, ATTR_MINUTES_REST: event["activity"]["minutes_rest"], diff --git a/homeassistant/components/tractive/binary_sensor.py b/homeassistant/components/tractive/binary_sensor.py index 940ff82687e..dd7237a2b38 100644 --- a/homeassistant/components/tractive/binary_sensor.py +++ b/homeassistant/components/tractive/binary_sensor.py @@ -1,4 +1,5 @@ """Support for Tractive binary sensors.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/tractive/config_flow.py b/homeassistant/components/tractive/config_flow.py index ba42aeb600d..a6b0d43a2b7 100644 --- a/homeassistant/components/tractive/config_flow.py +++ b/homeassistant/components/tractive/config_flow.py @@ -1,4 +1,5 @@ """Config flow for tractive integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -8,10 +9,9 @@ from typing import Any import aiotractive import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_EMAIL, CONF_PASSWORD from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError from .const import DOMAIN @@ -40,14 +40,14 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, return {"title": data[CONF_EMAIL], "user_id": user_id} -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class TractiveConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for tractive.""" VERSION = 1 async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" if user_input is None: return self.async_show_form(step_id="user", data_schema=USER_DATA_SCHEMA) @@ -70,13 +70,15 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="user", data_schema=USER_DATA_SCHEMA, errors=errors ) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Handle configuration by re-auth.""" return await self.async_step_reauth_confirm() async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Dialog that informs the user that reauth is required.""" errors = {} diff --git a/homeassistant/components/tractive/const.py b/homeassistant/components/tractive/const.py index acb4f6f7487..f26c0ee2345 100644 --- a/homeassistant/components/tractive/const.py +++ b/homeassistant/components/tractive/const.py @@ -26,7 +26,6 @@ CLIENT_ID = "625e5349c3c3b41c28a669f1" CLIENT = "client" TRACKABLES = "trackables" -TRACKER_ACTIVITY_STATUS_UPDATED = f"{DOMAIN}_tracker_activity_updated" TRACKER_HARDWARE_STATUS_UPDATED = f"{DOMAIN}_tracker_hardware_status_updated" TRACKER_POSITION_UPDATED = f"{DOMAIN}_tracker_position_updated" TRACKER_SWITCH_STATUS_UPDATED = f"{DOMAIN}_tracker_switch_updated" diff --git a/homeassistant/components/tractive/device_tracker.py b/homeassistant/components/tractive/device_tracker.py index c115a549fd4..134515469fc 100644 --- a/homeassistant/components/tractive/device_tracker.py +++ b/homeassistant/components/tractive/device_tracker.py @@ -1,4 +1,5 @@ """Support for Tractive device trackers.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/tractive/diagnostics.py b/homeassistant/components/tractive/diagnostics.py index 6defd91c0fb..f2bc80c51a1 100644 --- a/homeassistant/components/tractive/diagnostics.py +++ b/homeassistant/components/tractive/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for Tractive.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/tractive/entity.py b/homeassistant/components/tractive/entity.py index da7beb8bcdd..d6050c865b6 100644 --- a/homeassistant/components/tractive/entity.py +++ b/homeassistant/components/tractive/entity.py @@ -1,4 +1,5 @@ """A entity class for Tractive integration.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/tractive/sensor.py b/homeassistant/components/tractive/sensor.py index b563f536e21..1edee71467b 100644 --- a/homeassistant/components/tractive/sensor.py +++ b/homeassistant/components/tractive/sensor.py @@ -1,4 +1,5 @@ """Support for Tractive sensors.""" + from __future__ import annotations from collections.abc import Callable @@ -36,26 +37,18 @@ from .const import ( CLIENT, DOMAIN, TRACKABLES, - TRACKER_ACTIVITY_STATUS_UPDATED, TRACKER_HARDWARE_STATUS_UPDATED, TRACKER_WELLNESS_STATUS_UPDATED, ) from .entity import TractiveEntity -@dataclass(frozen=True) -class TractiveRequiredKeysMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class TractiveSensorEntityDescription(SensorEntityDescription): + """Class describing Tractive sensor entities.""" signal_prefix: str - -@dataclass(frozen=True) -class TractiveSensorEntityDescription( - SensorEntityDescription, TractiveRequiredKeysMixin -): - """Class describing Tractive sensor entities.""" - hardware_sensor: bool = False value_fn: Callable[[StateType], StateType] = lambda state: state @@ -114,6 +107,7 @@ SENSOR_TYPES: tuple[TractiveSensorEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, device_class=SensorDeviceClass.ENUM, options=[ + "inaccurate_position", "not_reporting", "operational", "system_shutdown_user", @@ -124,7 +118,7 @@ SENSOR_TYPES: tuple[TractiveSensorEntityDescription, ...] = ( key=ATTR_MINUTES_ACTIVE, translation_key="activity_time", native_unit_of_measurement=UnitOfTime.MINUTES, - signal_prefix=TRACKER_ACTIVITY_STATUS_UPDATED, + signal_prefix=TRACKER_WELLNESS_STATUS_UPDATED, state_class=SensorStateClass.TOTAL, ), TractiveSensorEntityDescription( @@ -145,7 +139,7 @@ SENSOR_TYPES: tuple[TractiveSensorEntityDescription, ...] = ( key=ATTR_DAILY_GOAL, translation_key="daily_goal", native_unit_of_measurement=UnitOfTime.MINUTES, - signal_prefix=TRACKER_ACTIVITY_STATUS_UPDATED, + signal_prefix=TRACKER_WELLNESS_STATUS_UPDATED, ), TractiveSensorEntityDescription( key=ATTR_MINUTES_DAY_SLEEP, diff --git a/homeassistant/components/tractive/strings.json b/homeassistant/components/tractive/strings.json index 82b7ecc295c..0690328c99c 100644 --- a/homeassistant/components/tractive/strings.json +++ b/homeassistant/components/tractive/strings.json @@ -70,6 +70,7 @@ "tracker_state": { "name": "Tracker state", "state": { + "inaccurate_position": "Inaccurate position", "not_reporting": "Not reporting", "operational": "Operational", "system_shutdown_user": "System shutdown user", diff --git a/homeassistant/components/tractive/switch.py b/homeassistant/components/tractive/switch.py index 4c838e5a468..52aa9f1e901 100644 --- a/homeassistant/components/tractive/switch.py +++ b/homeassistant/components/tractive/switch.py @@ -1,4 +1,5 @@ """Support for Tractive switches.""" + from __future__ import annotations from dataclasses import dataclass @@ -28,20 +29,13 @@ from .entity import TractiveEntity _LOGGER = logging.getLogger(__name__) -@dataclass(frozen=True) -class TractiveRequiredKeysMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class TractiveSwitchEntityDescription(SwitchEntityDescription): + """Class describing Tractive switch entities.""" method: Literal["async_set_buzzer", "async_set_led", "async_set_live_tracking"] -@dataclass(frozen=True) -class TractiveSwitchEntityDescription( - SwitchEntityDescription, TractiveRequiredKeysMixin -): - """Class describing Tractive switch entities.""" - - SWITCH_TYPES: tuple[TractiveSwitchEntityDescription, ...] = ( TractiveSwitchEntityDescription( key=ATTR_BUZZER, diff --git a/homeassistant/components/tradfri/__init__.py b/homeassistant/components/tradfri/__init__.py index aa61be1e782..2e267ffaa14 100644 --- a/homeassistant/components/tradfri/__init__.py +++ b/homeassistant/components/tradfri/__init__.py @@ -1,4 +1,5 @@ """Support for IKEA Tradfri.""" + from __future__ import annotations from datetime import datetime, timedelta diff --git a/homeassistant/components/tradfri/base_class.py b/homeassistant/components/tradfri/base_class.py index abb35df62aa..b06d0081477 100644 --- a/homeassistant/components/tradfri/base_class.py +++ b/homeassistant/components/tradfri/base_class.py @@ -1,4 +1,5 @@ """Base class for IKEA TRADFRI.""" + from __future__ import annotations from abc import abstractmethod diff --git a/homeassistant/components/tradfri/config_flow.py b/homeassistant/components/tradfri/config_flow.py index 9acdfb36a5d..8de40140339 100644 --- a/homeassistant/components/tradfri/config_flow.py +++ b/homeassistant/components/tradfri/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Tradfri.""" + from __future__ import annotations import asyncio @@ -9,11 +10,10 @@ from pytradfri import Gateway, RequestError from pytradfri.api.aiocoap_api import APIFactory import voluptuous as vol -from homeassistant import config_entries from homeassistant.components import zeroconf +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResult from .const import CONF_GATEWAY_ID, CONF_IDENTITY, CONF_KEY, DOMAIN @@ -29,7 +29,7 @@ class AuthError(Exception): self.code = code -class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class FlowHandler(ConfigFlow, domain=DOMAIN): """Handle a config flow.""" VERSION = 1 @@ -40,13 +40,13 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" return await self.async_step_auth() async def async_step_auth( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the authentication with a gateway.""" errors: dict[str, str] = {} @@ -82,7 +82,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_homekit( self, discovery_info: zeroconf.ZeroconfServiceInfo - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle homekit discovery.""" await self.async_set_unique_id( discovery_info.properties[zeroconf.ATTR_PROPERTIES_ID] @@ -107,7 +107,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self._host = host return await self.async_step_auth() - async def _entry_from_data(self, data: dict[str, Any]) -> FlowResult: + async def _entry_from_data(self, data: dict[str, Any]) -> ConfigFlowResult: """Create an entry from data.""" host = data[CONF_HOST] gateway_id = data[CONF_GATEWAY_ID] diff --git a/homeassistant/components/tradfri/const.py b/homeassistant/components/tradfri/const.py index 23b97efedd5..e42bb6f5f4d 100644 --- a/homeassistant/components/tradfri/const.py +++ b/homeassistant/components/tradfri/const.py @@ -1,4 +1,5 @@ """Consts used by Tradfri.""" + import logging LOGGER = logging.getLogger(__package__) diff --git a/homeassistant/components/tradfri/coordinator.py b/homeassistant/components/tradfri/coordinator.py index 0d90b4ee28c..5246545ae65 100644 --- a/homeassistant/components/tradfri/coordinator.py +++ b/homeassistant/components/tradfri/coordinator.py @@ -1,4 +1,5 @@ """Tradfri DataUpdateCoordinator.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/tradfri/cover.py b/homeassistant/components/tradfri/cover.py index c51918b4a4f..873b5f3cd07 100644 --- a/homeassistant/components/tradfri/cover.py +++ b/homeassistant/components/tradfri/cover.py @@ -1,4 +1,5 @@ """Support for IKEA Tradfri covers.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/tradfri/diagnostics.py b/homeassistant/components/tradfri/diagnostics.py index 271e2a226fe..4d89fd0081f 100644 --- a/homeassistant/components/tradfri/diagnostics.py +++ b/homeassistant/components/tradfri/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for IKEA Tradfri.""" + from __future__ import annotations from typing import Any, cast @@ -25,9 +26,10 @@ async def async_get_config_entry_diagnostics( ), ) - device_data: list = [] - for coordinator in coordinator_data[COORDINATOR_LIST]: - device_data.append(coordinator.device.device_info.model_number) + device_data: list = [ + coordinator.device.device_info.model_number + for coordinator in coordinator_data[COORDINATOR_LIST] + ] return { "gateway_version": device.sw_version, diff --git a/homeassistant/components/tradfri/icons.json b/homeassistant/components/tradfri/icons.json new file mode 100644 index 00000000000..5bacebb3595 --- /dev/null +++ b/homeassistant/components/tradfri/icons.json @@ -0,0 +1,12 @@ +{ + "entity": { + "sensor": { + "aqi": { + "default": "mdi:air-filter" + }, + "filter_life_remaining": { + "default": "mdi:clock-outline" + } + } + } +} diff --git a/homeassistant/components/tradfri/light.py b/homeassistant/components/tradfri/light.py index 769c8f6f9e1..ef65c6bf957 100644 --- a/homeassistant/components/tradfri/light.py +++ b/homeassistant/components/tradfri/light.py @@ -1,4 +1,5 @@ """Support for IKEA Tradfri lights.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/tradfri/sensor.py b/homeassistant/components/tradfri/sensor.py index 7f04b8aff03..5d3e63d3a5d 100644 --- a/homeassistant/components/tradfri/sensor.py +++ b/homeassistant/components/tradfri/sensor.py @@ -1,4 +1,5 @@ """Support for IKEA Tradfri sensors.""" + from __future__ import annotations from collections.abc import Callable @@ -37,21 +38,13 @@ from .const import ( from .coordinator import TradfriDeviceDataUpdateCoordinator -@dataclass(frozen=True) -class TradfriSensorEntityDescriptionMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class TradfriSensorEntityDescription(SensorEntityDescription): + """Class describing Tradfri sensor entities.""" value: Callable[[Device], Any | None] -@dataclass(frozen=True) -class TradfriSensorEntityDescription( - SensorEntityDescription, - TradfriSensorEntityDescriptionMixin, -): - """Class describing Tradfri sensor entities.""" - - def _get_air_quality(device: Device) -> int | None: """Fetch the air quality value.""" assert device.air_purifier_control is not None @@ -91,7 +84,6 @@ SENSOR_DESCRIPTIONS_FAN: tuple[TradfriSensorEntityDescription, ...] = ( translation_key="aqi", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - icon="mdi:air-filter", value=_get_air_quality, ), TradfriSensorEntityDescription( @@ -99,7 +91,6 @@ SENSOR_DESCRIPTIONS_FAN: tuple[TradfriSensorEntityDescription, ...] = ( translation_key="filter_life_remaining", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfTime.HOURS, - icon="mdi:clock-outline", value=_get_filter_time_left, ), ) diff --git a/homeassistant/components/tradfri/switch.py b/homeassistant/components/tradfri/switch.py index 2f6f1996157..4ad1424aa9a 100644 --- a/homeassistant/components/tradfri/switch.py +++ b/homeassistant/components/tradfri/switch.py @@ -1,4 +1,5 @@ """Support for IKEA Tradfri switches.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/trafikverket_camera/__init__.py b/homeassistant/components/trafikverket_camera/__init__.py index 7303ba6836b..998b667add3 100644 --- a/homeassistant/components/trafikverket_camera/__init__.py +++ b/homeassistant/components/trafikverket_camera/__init__.py @@ -1,4 +1,5 @@ """The trafikverket_camera component.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/trafikverket_camera/binary_sensor.py b/homeassistant/components/trafikverket_camera/binary_sensor.py index b725f6d2f95..56af099d54b 100644 --- a/homeassistant/components/trafikverket_camera/binary_sensor.py +++ b/homeassistant/components/trafikverket_camera/binary_sensor.py @@ -1,4 +1,5 @@ """Binary sensor platform for Trafikverket Camera integration.""" + from __future__ import annotations from collections.abc import Callable @@ -19,24 +20,16 @@ from .entity import TrafikverketCameraNonCameraEntity PARALLEL_UPDATES = 0 -@dataclass(frozen=True) -class DeviceBaseEntityDescriptionMixin: - """Mixin for required Trafikverket Camera base description keys.""" +@dataclass(frozen=True, kw_only=True) +class TVCameraSensorEntityDescription(BinarySensorEntityDescription): + """Describes Trafikverket Camera binary sensor entity.""" value_fn: Callable[[CameraData], bool | None] -@dataclass(frozen=True) -class TVCameraSensorEntityDescription( - BinarySensorEntityDescription, DeviceBaseEntityDescriptionMixin -): - """Describes Trafikverket Camera binary sensor entity.""" - - BINARY_SENSOR_TYPE = TVCameraSensorEntityDescription( key="active", translation_key="active", - icon="mdi:camera-outline", value_fn=lambda data: data.data.active, ) diff --git a/homeassistant/components/trafikverket_camera/camera.py b/homeassistant/components/trafikverket_camera/camera.py index 808d687a131..0fa70a886b2 100644 --- a/homeassistant/components/trafikverket_camera/camera.py +++ b/homeassistant/components/trafikverket_camera/camera.py @@ -1,4 +1,5 @@ """Camera for the Trafikverket Camera integration.""" + from __future__ import annotations from collections.abc import Mapping diff --git a/homeassistant/components/trafikverket_camera/config_flow.py b/homeassistant/components/trafikverket_camera/config_flow.py index 9db27eda622..1c2e025ece9 100644 --- a/homeassistant/components/trafikverket_camera/config_flow.py +++ b/homeassistant/components/trafikverket_camera/config_flow.py @@ -1,4 +1,5 @@ """Adds config flow for Trafikverket Camera integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -8,9 +9,8 @@ from pytrafikverket.exceptions import InvalidAuthentication, NoCameraFound, Unkn from pytrafikverket.trafikverket_camera import CameraInfo, TrafikverketCamera import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_API_KEY, CONF_ID, CONF_LOCATION -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.selector import ( SelectOptionDict, @@ -23,12 +23,12 @@ from homeassistant.helpers.selector import ( from .const import DOMAIN -class TVCameraConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class TVCameraConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Trafikverket Camera integration.""" VERSION = 3 - entry: config_entries.ConfigEntry | None + entry: ConfigEntry | None cameras: list[CameraInfo] api_key: str @@ -52,7 +52,9 @@ class TVCameraConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return (errors, cameras) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Handle re-authentication with Trafikverket.""" self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) @@ -60,7 +62,7 @@ class TVCameraConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm re-authentication with Trafikverket.""" errors: dict[str, str] = {} @@ -93,7 +95,7 @@ class TVCameraConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, str] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors: dict[str, str] = {} @@ -128,7 +130,7 @@ class TVCameraConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_multiple_cameras( self, user_input: dict[str, str] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle when multiple cameras.""" if user_input: diff --git a/homeassistant/components/trafikverket_camera/const.py b/homeassistant/components/trafikverket_camera/const.py index 728ba9f7bd5..276481752ad 100644 --- a/homeassistant/components/trafikverket_camera/const.py +++ b/homeassistant/components/trafikverket_camera/const.py @@ -1,4 +1,5 @@ """Adds constants for Trafikverket Camera integration.""" + from homeassistant.const import Platform DOMAIN = "trafikverket_camera" diff --git a/homeassistant/components/trafikverket_camera/coordinator.py b/homeassistant/components/trafikverket_camera/coordinator.py index 8270fecd487..03b70009189 100644 --- a/homeassistant/components/trafikverket_camera/coordinator.py +++ b/homeassistant/components/trafikverket_camera/coordinator.py @@ -1,4 +1,5 @@ """DataUpdateCoordinator for the Trafikverket Camera integration.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/homeassistant/components/trafikverket_camera/entity.py b/homeassistant/components/trafikverket_camera/entity.py index ec1d4d8f76b..c564c2673d3 100644 --- a/homeassistant/components/trafikverket_camera/entity.py +++ b/homeassistant/components/trafikverket_camera/entity.py @@ -1,4 +1,5 @@ """Base entity for Trafikverket Camera.""" + from __future__ import annotations from homeassistant.core import callback diff --git a/homeassistant/components/trafikverket_camera/icons.json b/homeassistant/components/trafikverket_camera/icons.json new file mode 100644 index 00000000000..46b006ff48b --- /dev/null +++ b/homeassistant/components/trafikverket_camera/icons.json @@ -0,0 +1,29 @@ +{ + "entity": { + "binary_sensor": { + "active": { + "default": "mdi:camera-outline" + } + }, + "sensor": { + "direction": { + "default": "mdi:sign-direction" + }, + "modified": { + "default": "mdi:camera-retake-outline" + }, + "photo_time": { + "default": "mdi:camera-timer" + }, + "photo_url": { + "default": "mdi:camera-outline" + }, + "status": { + "default": "mdi:camera-outline" + }, + "camera_type": { + "default": "mdi:camera-iris" + } + } + } +} diff --git a/homeassistant/components/trafikverket_camera/sensor.py b/homeassistant/components/trafikverket_camera/sensor.py index 678c703307c..f41eb1fa2a2 100644 --- a/homeassistant/components/trafikverket_camera/sensor.py +++ b/homeassistant/components/trafikverket_camera/sensor.py @@ -1,4 +1,5 @@ """Sensor platform for Trafikverket Camera integration.""" + from __future__ import annotations from collections.abc import Callable @@ -23,32 +24,23 @@ from .entity import TrafikverketCameraNonCameraEntity PARALLEL_UPDATES = 0 -@dataclass(frozen=True) -class DeviceBaseEntityDescriptionMixin: - """Mixin for required Trafikverket Camera base description keys.""" +@dataclass(frozen=True, kw_only=True) +class TVCameraSensorEntityDescription(SensorEntityDescription): + """Describes Trafikverket Camera sensor entity.""" value_fn: Callable[[CameraData], StateType | datetime] -@dataclass(frozen=True) -class TVCameraSensorEntityDescription( - SensorEntityDescription, DeviceBaseEntityDescriptionMixin -): - """Describes Trafikverket Camera sensor entity.""" - - SENSOR_TYPES: tuple[TVCameraSensorEntityDescription, ...] = ( TVCameraSensorEntityDescription( key="direction", translation_key="direction", native_unit_of_measurement=DEGREE, - icon="mdi:sign-direction", value_fn=lambda data: data.data.direction, ), TVCameraSensorEntityDescription( key="modified", translation_key="modified", - icon="mdi:camera-retake-outline", device_class=SensorDeviceClass.TIMESTAMP, value_fn=lambda data: data.data.modified, entity_registry_enabled_default=False, @@ -56,28 +48,24 @@ SENSOR_TYPES: tuple[TVCameraSensorEntityDescription, ...] = ( TVCameraSensorEntityDescription( key="photo_time", translation_key="photo_time", - icon="mdi:camera-timer", device_class=SensorDeviceClass.TIMESTAMP, value_fn=lambda data: data.data.phototime, ), TVCameraSensorEntityDescription( key="photo_url", translation_key="photo_url", - icon="mdi:camera-outline", value_fn=lambda data: data.data.photourl, entity_registry_enabled_default=False, ), TVCameraSensorEntityDescription( key="status", translation_key="status", - icon="mdi:camera-outline", value_fn=lambda data: data.data.status, entity_registry_enabled_default=False, ), TVCameraSensorEntityDescription( key="camera_type", translation_key="camera_type", - icon="mdi:camera-iris", value_fn=lambda data: data.data.camera_type, entity_registry_enabled_default=False, ), diff --git a/homeassistant/components/trafikverket_ferry/__init__.py b/homeassistant/components/trafikverket_ferry/__init__.py index c522acb6d12..8c8c121881f 100644 --- a/homeassistant/components/trafikverket_ferry/__init__.py +++ b/homeassistant/components/trafikverket_ferry/__init__.py @@ -1,4 +1,5 @@ """The trafikverket_ferry component.""" + from __future__ import annotations from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/trafikverket_ferry/config_flow.py b/homeassistant/components/trafikverket_ferry/config_flow.py index 2fb6cfb642a..3b79cc0f0bd 100644 --- a/homeassistant/components/trafikverket_ferry/config_flow.py +++ b/homeassistant/components/trafikverket_ferry/config_flow.py @@ -1,4 +1,5 @@ """Adds config flow for Trafikverket Ferry integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -8,9 +9,8 @@ from pytrafikverket import TrafikverketFerry from pytrafikverket.exceptions import InvalidAuthentication, NoFerryFound import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_API_KEY, CONF_NAME, CONF_WEEKDAY, WEEKDAYS -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import selector from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -44,12 +44,12 @@ DATA_SCHEMA_REAUTH = vol.Schema( ) -class TVFerryConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class TVFerryConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Trafikverket Ferry integration.""" VERSION = 1 - entry: config_entries.ConfigEntry | None + entry: ConfigEntry | None async def validate_input( self, api_key: str, ferry_from: str, ferry_to: str @@ -59,7 +59,9 @@ class TVFerryConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ferry_api = TrafikverketFerry(web_session, api_key) await ferry_api.async_get_next_ferry_stop(ferry_from, ferry_to) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Handle re-authentication with Trafikverket.""" self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) @@ -67,7 +69,7 @@ class TVFerryConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm re-authentication with Trafikverket.""" errors: dict[str, str] = {} @@ -104,7 +106,7 @@ class TVFerryConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the user step.""" errors: dict[str, str] = {} diff --git a/homeassistant/components/trafikverket_ferry/const.py b/homeassistant/components/trafikverket_ferry/const.py index cdcec530d08..4ec694cbf01 100644 --- a/homeassistant/components/trafikverket_ferry/const.py +++ b/homeassistant/components/trafikverket_ferry/const.py @@ -1,4 +1,5 @@ """Adds constants for Trafikverket Ferry integration.""" + from homeassistant.const import Platform DOMAIN = "trafikverket_ferry" diff --git a/homeassistant/components/trafikverket_ferry/coordinator.py b/homeassistant/components/trafikverket_ferry/coordinator.py index 4c209a3ba87..8d0492b1e43 100644 --- a/homeassistant/components/trafikverket_ferry/coordinator.py +++ b/homeassistant/components/trafikverket_ferry/coordinator.py @@ -1,4 +1,5 @@ """DataUpdateCoordinator for the Trafikverket Ferry integration.""" + from __future__ import annotations from datetime import date, datetime, time, timedelta diff --git a/homeassistant/components/trafikverket_ferry/icons.json b/homeassistant/components/trafikverket_ferry/icons.json new file mode 100644 index 00000000000..ca2536efcc5 --- /dev/null +++ b/homeassistant/components/trafikverket_ferry/icons.json @@ -0,0 +1,24 @@ +{ + "entity": { + "sensor": { + "departure_time": { + "default": "mdi:clock" + }, + "departure_from": { + "default": "mdi:ferry" + }, + "departure_to": { + "default": "mdi:ferry" + }, + "departure_modified": { + "default": "mdi:clock" + }, + "departure_time_next": { + "default": "mdi:clock" + }, + "departure_time_next_next": { + "default": "mdi:clock" + } + } + } +} diff --git a/homeassistant/components/trafikverket_ferry/sensor.py b/homeassistant/components/trafikverket_ferry/sensor.py index cd0682c12bc..93f2d1987b6 100644 --- a/homeassistant/components/trafikverket_ferry/sensor.py +++ b/homeassistant/components/trafikverket_ferry/sensor.py @@ -1,4 +1,5 @@ """Ferry information for departures, provided by Trafikverket.""" + from __future__ import annotations from collections.abc import Callable @@ -28,30 +29,21 @@ ATTR_TO = "to_harbour" ATTR_MODIFIED_TIME = "modified_time" ATTR_OTHER_INFO = "other_info" -ICON = "mdi:ferry" SCAN_INTERVAL = timedelta(minutes=5) -@dataclass(frozen=True) -class TrafikverketRequiredKeysMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class TrafikverketSensorEntityDescription(SensorEntityDescription): + """Describes Trafikverket sensor entity.""" value_fn: Callable[[dict[str, Any]], StateType | datetime] info_fn: Callable[[dict[str, Any]], StateType | list] | None -@dataclass(frozen=True) -class TrafikverketSensorEntityDescription( - SensorEntityDescription, TrafikverketRequiredKeysMixin -): - """Describes Trafikverket sensor entity.""" - - SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( TrafikverketSensorEntityDescription( key="departure_time", translation_key="departure_time", - icon="mdi:clock", device_class=SensorDeviceClass.TIMESTAMP, value_fn=lambda data: as_utc(data["departure_time"]), info_fn=lambda data: cast(list[str], data["departure_information"]), @@ -59,21 +51,18 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( TrafikverketSensorEntityDescription( key="departure_from", translation_key="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", translation_key="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", translation_key="departure_modified", - icon="mdi:clock", device_class=SensorDeviceClass.TIMESTAMP, value_fn=lambda data: as_utc(data["departure_modified"]), info_fn=lambda data: cast(list[str], data["departure_information"]), @@ -82,7 +71,6 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( TrafikverketSensorEntityDescription( key="departure_time_next", translation_key="departure_time_next", - icon="mdi:clock", device_class=SensorDeviceClass.TIMESTAMP, value_fn=lambda data: as_utc(data["departure_time_next"]), info_fn=None, @@ -91,7 +79,6 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( TrafikverketSensorEntityDescription( key="departure_time_next_next", translation_key="departure_time_next_next", - icon="mdi:clock", device_class=SensorDeviceClass.TIMESTAMP, value_fn=lambda data: as_utc(data["departure_time_next_next"]), info_fn=None, diff --git a/homeassistant/components/trafikverket_ferry/util.py b/homeassistant/components/trafikverket_ferry/util.py index a78f6f82f1a..a45e8b31daa 100644 --- a/homeassistant/components/trafikverket_ferry/util.py +++ b/homeassistant/components/trafikverket_ferry/util.py @@ -1,4 +1,5 @@ """Utils for trafikverket_ferry.""" + from __future__ import annotations from datetime import time diff --git a/homeassistant/components/trafikverket_train/__init__.py b/homeassistant/components/trafikverket_train/__init__.py index a7defa2956a..8b427c3431d 100644 --- a/homeassistant/components/trafikverket_train/__init__.py +++ b/homeassistant/components/trafikverket_train/__init__.py @@ -1,4 +1,5 @@ """The trafikverket_train component.""" + from __future__ import annotations from pytrafikverket import TrafikverketTrain diff --git a/homeassistant/components/trafikverket_train/config_flow.py b/homeassistant/components/trafikverket_train/config_flow.py index df05942add1..48e603eff02 100644 --- a/homeassistant/components/trafikverket_train/config_flow.py +++ b/homeassistant/components/trafikverket_train/config_flow.py @@ -1,4 +1,5 @@ """Adds config flow for Trafikverket Train integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -16,10 +17,14 @@ from pytrafikverket.exceptions import ( ) import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlowWithConfigEntry, +) from homeassistant.const import CONF_API_KEY, CONF_NAME, CONF_WEEKDAY, WEEKDAYS from homeassistant.core import HomeAssistant, callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.selector import ( @@ -116,22 +121,24 @@ async def validate_input( return errors -class TVTrainConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class TVTrainConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Trafikverket Train integration.""" VERSION = 1 - entry: config_entries.ConfigEntry | None + entry: ConfigEntry | None @staticmethod @callback def async_get_options_flow( - config_entry: config_entries.ConfigEntry, + config_entry: ConfigEntry, ) -> TVTrainOptionsFlowHandler: """Get the options flow for this handler.""" return TVTrainOptionsFlowHandler(config_entry) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Handle re-authentication with Trafikverket.""" self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) @@ -139,7 +146,7 @@ class TVTrainConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm re-authentication with Trafikverket.""" errors: dict[str, str] = {} @@ -175,7 +182,7 @@ class TVTrainConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the user step.""" errors: dict[str, str] = {} @@ -231,12 +238,12 @@ class TVTrainConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -class TVTrainOptionsFlowHandler(config_entries.OptionsFlowWithConfigEntry): +class TVTrainOptionsFlowHandler(OptionsFlowWithConfigEntry): """Handle Trafikverket Train options.""" async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Manage Trafikverket Train options.""" errors: dict[str, Any] = {} diff --git a/homeassistant/components/trafikverket_train/const.py b/homeassistant/components/trafikverket_train/const.py index e1852ce9ada..a97f3a547e2 100644 --- a/homeassistant/components/trafikverket_train/const.py +++ b/homeassistant/components/trafikverket_train/const.py @@ -1,4 +1,5 @@ """Adds constants for Trafikverket Train integration.""" + from homeassistant.const import Platform DOMAIN = "trafikverket_train" diff --git a/homeassistant/components/trafikverket_train/coordinator.py b/homeassistant/components/trafikverket_train/coordinator.py index d5402e44ec6..cf78228ed58 100644 --- a/homeassistant/components/trafikverket_train/coordinator.py +++ b/homeassistant/components/trafikverket_train/coordinator.py @@ -1,4 +1,5 @@ """DataUpdateCoordinator for the Trafikverket Train integration.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/homeassistant/components/trafikverket_train/icons.json b/homeassistant/components/trafikverket_train/icons.json new file mode 100644 index 00000000000..982e3f70b9c --- /dev/null +++ b/homeassistant/components/trafikverket_train/icons.json @@ -0,0 +1,39 @@ +{ + "entity": { + "sensor": { + "departure_time": { + "default": "mdi:clock" + }, + "departure_state": { + "default": "mdi:clock" + }, + "cancelled": { + "default": "mdi:alert" + }, + "delayed_time": { + "default": "mdi:clock" + }, + "planned_time": { + "default": "mdi:clock" + }, + "estimated_time": { + "default": "mdi:clock" + }, + "actual_time": { + "default": "mdi:clock" + }, + "other_info": { + "default": "mdi:information-variant" + }, + "deviation": { + "default": "mdi:alert" + }, + "departure_time_next": { + "default": "mdi:clock" + }, + "departure_time_next_next": { + "default": "mdi:clock" + } + } + } +} diff --git a/homeassistant/components/trafikverket_train/sensor.py b/homeassistant/components/trafikverket_train/sensor.py index 68865a64cb5..22d8aba4725 100644 --- a/homeassistant/components/trafikverket_train/sensor.py +++ b/homeassistant/components/trafikverket_train/sensor.py @@ -1,4 +1,5 @@ """Train information for departures and delays, provided by Trafikverket.""" + from __future__ import annotations from collections.abc import Callable, Mapping @@ -25,32 +26,23 @@ from .coordinator import TrainData, TVDataUpdateCoordinator ATTR_PRODUCT_FILTER = "product_filter" -@dataclass(frozen=True) -class TrafikverketRequiredKeysMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class TrafikverketSensorEntityDescription(SensorEntityDescription): + """Describes Trafikverket sensor entity.""" value_fn: Callable[[TrainData], StateType | datetime] -@dataclass(frozen=True) -class TrafikverketSensorEntityDescription( - SensorEntityDescription, TrafikverketRequiredKeysMixin -): - """Describes Trafikverket sensor entity.""" - - SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( TrafikverketSensorEntityDescription( key="departure_time", translation_key="departure_time", - icon="mdi:clock", device_class=SensorDeviceClass.TIMESTAMP, value_fn=lambda data: data.departure_time, ), TrafikverketSensorEntityDescription( key="departure_state", translation_key="departure_state", - icon="mdi:clock", value_fn=lambda data: data.departure_state, device_class=SensorDeviceClass.ENUM, options=["on_time", "delayed", "canceled"], @@ -58,13 +50,11 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( TrafikverketSensorEntityDescription( key="cancelled", translation_key="cancelled", - icon="mdi:alert", value_fn=lambda data: data.cancelled, ), TrafikverketSensorEntityDescription( key="delayed_time", translation_key="delayed_time", - icon="mdi:clock", device_class=SensorDeviceClass.DURATION, native_unit_of_measurement=UnitOfTime.SECONDS, value_fn=lambda data: data.delayed_time, @@ -72,7 +62,6 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( TrafikverketSensorEntityDescription( key="planned_time", translation_key="planned_time", - icon="mdi:clock", device_class=SensorDeviceClass.TIMESTAMP, value_fn=lambda data: data.planned_time, entity_registry_enabled_default=False, @@ -80,7 +69,6 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( TrafikverketSensorEntityDescription( key="estimated_time", translation_key="estimated_time", - icon="mdi:clock", device_class=SensorDeviceClass.TIMESTAMP, value_fn=lambda data: data.estimated_time, entity_registry_enabled_default=False, @@ -88,7 +76,6 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( TrafikverketSensorEntityDescription( key="actual_time", translation_key="actual_time", - icon="mdi:clock", device_class=SensorDeviceClass.TIMESTAMP, value_fn=lambda data: data.actual_time, entity_registry_enabled_default=False, @@ -96,26 +83,22 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( TrafikverketSensorEntityDescription( key="other_info", translation_key="other_info", - icon="mdi:information-variant", value_fn=lambda data: data.other_info, ), TrafikverketSensorEntityDescription( key="deviation", translation_key="deviation", - icon="mdi:alert", value_fn=lambda data: data.deviation, ), TrafikverketSensorEntityDescription( key="departure_time_next", translation_key="departure_time_next", - icon="mdi:clock", device_class=SensorDeviceClass.TIMESTAMP, value_fn=lambda data: data.departure_time_next, ), TrafikverketSensorEntityDescription( key="departure_time_next_next", translation_key="departure_time_next_next", - icon="mdi:clock", device_class=SensorDeviceClass.TIMESTAMP, value_fn=lambda data: data.departure_time_next_next, ), diff --git a/homeassistant/components/trafikverket_train/util.py b/homeassistant/components/trafikverket_train/util.py index c5553c4a4a7..b28a51d339d 100644 --- a/homeassistant/components/trafikverket_train/util.py +++ b/homeassistant/components/trafikverket_train/util.py @@ -1,4 +1,5 @@ """Utils for trafikverket_train.""" + from __future__ import annotations from datetime import date, time, timedelta diff --git a/homeassistant/components/trafikverket_weatherstation/__init__.py b/homeassistant/components/trafikverket_weatherstation/__init__.py index 13b88918133..e1cd9c90909 100644 --- a/homeassistant/components/trafikverket_weatherstation/__init__.py +++ b/homeassistant/components/trafikverket_weatherstation/__init__.py @@ -1,4 +1,5 @@ """The trafikverket_weatherstation component.""" + from __future__ import annotations from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/trafikverket_weatherstation/config_flow.py b/homeassistant/components/trafikverket_weatherstation/config_flow.py index 89cbd373665..05be4fc460e 100644 --- a/homeassistant/components/trafikverket_weatherstation/config_flow.py +++ b/homeassistant/components/trafikverket_weatherstation/config_flow.py @@ -1,4 +1,5 @@ """Adds config flow for Trafikverket Weather integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -12,21 +13,20 @@ from pytrafikverket.exceptions import ( from pytrafikverket.trafikverket_weather import TrafikverketWeather import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_API_KEY -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from .const import CONF_STATION, DOMAIN -class TVWeatherConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class TVWeatherConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Trafikverket Weatherstation integration.""" VERSION = 1 - entry: config_entries.ConfigEntry | None = None + entry: ConfigEntry | None = None async def validate_input(self, sensor_api: str, station: str) -> None: """Validate input from user input.""" @@ -36,7 +36,7 @@ class TVWeatherConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, str] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors = {} @@ -75,7 +75,9 @@ class TVWeatherConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Handle re-authentication with Trafikverket.""" self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) @@ -83,7 +85,7 @@ class TVWeatherConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm re-authentication with Trafikverket.""" errors: dict[str, str] = {} diff --git a/homeassistant/components/trafikverket_weatherstation/const.py b/homeassistant/components/trafikverket_weatherstation/const.py index 34c18359ee4..880de3867ba 100644 --- a/homeassistant/components/trafikverket_weatherstation/const.py +++ b/homeassistant/components/trafikverket_weatherstation/const.py @@ -1,4 +1,5 @@ """Adds constants for Trafikverket Weather integration.""" + from homeassistant.const import Platform DOMAIN = "trafikverket_weatherstation" diff --git a/homeassistant/components/trafikverket_weatherstation/coordinator.py b/homeassistant/components/trafikverket_weatherstation/coordinator.py index 40c551089d2..508ae7eec16 100644 --- a/homeassistant/components/trafikverket_weatherstation/coordinator.py +++ b/homeassistant/components/trafikverket_weatherstation/coordinator.py @@ -1,4 +1,5 @@ """DataUpdateCoordinator for the Trafikverket Weather integration.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/trafikverket_weatherstation/icons.json b/homeassistant/components/trafikverket_weatherstation/icons.json new file mode 100644 index 00000000000..555d79ee084 --- /dev/null +++ b/homeassistant/components/trafikverket_weatherstation/icons.json @@ -0,0 +1,21 @@ +{ + "entity": { + "sensor": { + "precipitation": { + "default": "mdi:weather-snowy-rainy" + }, + "wind_direction": { + "default": "mdi:flag-triangle" + }, + "wind_speed_max": { + "default": "mdi:weather-windy-variant" + }, + "measure_time": { + "default": "mdi:clock" + }, + "modified_time": { + "default": "mdi:clock" + } + } + } +} diff --git a/homeassistant/components/trafikverket_weatherstation/sensor.py b/homeassistant/components/trafikverket_weatherstation/sensor.py index 9c025237187..bd15c34ff01 100644 --- a/homeassistant/components/trafikverket_weatherstation/sensor.py +++ b/homeassistant/components/trafikverket_weatherstation/sensor.py @@ -1,4 +1,5 @@ """Weather information for air and road temperature (by Trafikverket).""" + from __future__ import annotations from collections.abc import Callable @@ -42,20 +43,13 @@ PRECIPITATION_TYPE = [ ] -@dataclass(frozen=True) -class TrafikverketRequiredKeysMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class TrafikverketSensorEntityDescription(SensorEntityDescription): + """Describes Trafikverket sensor entity.""" value_fn: Callable[[WeatherStationInfo], StateType | datetime] -@dataclass(frozen=True) -class TrafikverketSensorEntityDescription( - SensorEntityDescription, TrafikverketRequiredKeysMixin -): - """Describes Trafikverket sensor entity.""" - - def add_utc_timezone(date_time: datetime | None) -> datetime | None: """Add UTC timezone if datetime.""" if date_time: @@ -84,7 +78,6 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( key="precipitation", translation_key="precipitation", value_fn=lambda data: data.precipitationtype, - icon="mdi:weather-snowy-rainy", entity_registry_enabled_default=False, options=PRECIPITATION_TYPE, device_class=SensorDeviceClass.ENUM, @@ -94,7 +87,6 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( translation_key="wind_direction", value_fn=lambda data: data.winddirection, native_unit_of_measurement=DEGREE, - icon="mdi:flag-triangle", state_class=SensorStateClass.MEASUREMENT, ), TrafikverketSensorEntityDescription( @@ -110,7 +102,6 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( value_fn=lambda data: data.windforcemax or 0, native_unit_of_measurement=UnitOfSpeed.METERS_PER_SECOND, device_class=SensorDeviceClass.WIND_SPEED, - icon="mdi:weather-windy-variant", entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, ), @@ -133,7 +124,6 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( key="measure_time", translation_key="measure_time", value_fn=lambda data: data.measure_time, - icon="mdi:clock", entity_registry_enabled_default=False, device_class=SensorDeviceClass.TIMESTAMP, ), @@ -203,7 +193,6 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( key="modified_time", translation_key="modified_time", value_fn=lambda data: add_utc_timezone(data.modified_time), - icon="mdi:clock", entity_registry_enabled_default=False, device_class=SensorDeviceClass.TIMESTAMP, ), diff --git a/homeassistant/components/transmission/__init__.py b/homeassistant/components/transmission/__init__.py index df78c5d96aa..4dcc4d41950 100644 --- a/homeassistant/components/transmission/__init__.py +++ b/homeassistant/components/transmission/__init__.py @@ -1,4 +1,5 @@ """Support for the Transmission BitTorrent client API.""" + from __future__ import annotations from functools import partial @@ -20,7 +21,9 @@ from homeassistant.const import ( CONF_ID, CONF_NAME, CONF_PASSWORD, + CONF_PATH, CONF_PORT, + CONF_SSL, CONF_USERNAME, Platform, ) @@ -37,6 +40,8 @@ from .const import ( ATTR_TORRENT, CONF_ENTRY_ID, DEFAULT_DELETE_DATA, + DEFAULT_PATH, + DEFAULT_SSL, DOMAIN, SERVICE_ADD_TORRENT, SERVICE_REMOVE_TORRENT, @@ -210,12 +215,43 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> return unload_ok +async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Migrate an old config entry.""" + _LOGGER.debug( + "Migrating from version %s.%s", + config_entry.version, + config_entry.minor_version, + ) + + if config_entry.version == 1: + # Version 1.2 adds ssl and path + if config_entry.minor_version < 2: + new = {**config_entry.data} + + new[CONF_PATH] = DEFAULT_PATH + new[CONF_SSL] = DEFAULT_SSL + + hass.config_entries.async_update_entry( + config_entry, data=new, version=1, minor_version=2 + ) + + _LOGGER.debug( + "Migration to version %s.%s successful", + config_entry.version, + config_entry.minor_version, + ) + + return True + + async def get_api( hass: HomeAssistant, entry: dict[str, Any] ) -> transmission_rpc.Client: """Get Transmission client.""" + protocol = "https" if entry[CONF_SSL] else "http" host = entry[CONF_HOST] port = entry[CONF_PORT] + path = entry[CONF_PATH] username = entry.get(CONF_USERNAME) password = entry.get(CONF_PASSWORD) @@ -225,8 +261,10 @@ async def get_api( transmission_rpc.Client, username=username, password=password, + protocol=protocol, host=host, port=port, + path=path, ) ) _LOGGER.debug("Successfully connected to %s", host) diff --git a/homeassistant/components/transmission/config_flow.py b/homeassistant/components/transmission/config_flow.py index a987233fef0..62879d2d0af 100644 --- a/homeassistant/components/transmission/config_flow.py +++ b/homeassistant/components/transmission/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Transmission Bittorent Client.""" + from __future__ import annotations from collections.abc import Mapping @@ -6,10 +7,21 @@ from typing import Any import voluptuous as vol -from homeassistant import config_entries -from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_PATH, + CONF_PORT, + CONF_SSL, + CONF_USERNAME, +) from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult from . import get_api from .const import ( @@ -18,7 +30,9 @@ from .const import ( DEFAULT_LIMIT, DEFAULT_NAME, DEFAULT_ORDER, + DEFAULT_PATH, DEFAULT_PORT, + DEFAULT_SSL, DOMAIN, SUPPORTED_ORDER_MODES, ) @@ -26,7 +40,9 @@ from .errors import AuthenticationError, CannotConnect, UnknownError DATA_SCHEMA = vol.Schema( { + vol.Optional(CONF_SSL, default=DEFAULT_SSL): bool, vol.Required(CONF_HOST): str, + vol.Required(CONF_PATH, default=DEFAULT_PATH): str, vol.Optional(CONF_USERNAME): str, vol.Optional(CONF_PASSWORD): str, vol.Required(CONF_PORT, default=DEFAULT_PORT): int, @@ -34,23 +50,24 @@ DATA_SCHEMA = vol.Schema( ) -class TransmissionFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class TransmissionFlowHandler(ConfigFlow, domain=DOMAIN): """Handle Tansmission config flow.""" VERSION = 1 - _reauth_entry: config_entries.ConfigEntry | None + MINOR_VERSION = 2 + _reauth_entry: ConfigEntry | None @staticmethod @callback def async_get_options_flow( - config_entry: config_entries.ConfigEntry, + config_entry: ConfigEntry, ) -> TransmissionOptionsFlowHandler: """Get the options flow for this handler.""" return TransmissionOptionsFlowHandler(config_entry) async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" errors = {} @@ -79,7 +96,9 @@ class TransmissionFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Perform reauth upon an API authentication error.""" self._reauth_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] @@ -88,7 +107,7 @@ class TransmissionFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_reauth_confirm( self, user_input: dict[str, str] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm reauth dialog.""" errors = {} assert self._reauth_entry @@ -122,16 +141,16 @@ class TransmissionFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) -class TransmissionOptionsFlowHandler(config_entries.OptionsFlow): +class TransmissionOptionsFlowHandler(OptionsFlow): """Handle Transmission client options.""" - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + def __init__(self, config_entry: ConfigEntry) -> None: """Initialize Transmission options flow.""" self.config_entry = config_entry async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Manage the Transmission options.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) diff --git a/homeassistant/components/transmission/const.py b/homeassistant/components/transmission/const.py index 64b15c51691..0dd77fa6aa3 100644 --- a/homeassistant/components/transmission/const.py +++ b/homeassistant/components/transmission/const.py @@ -1,4 +1,5 @@ """Constants for the Transmission Bittorent Client component.""" + from __future__ import annotations from collections.abc import Callable @@ -30,7 +31,9 @@ DEFAULT_DELETE_DATA = False DEFAULT_LIMIT = 10 DEFAULT_ORDER = ORDER_OLDEST_FIRST DEFAULT_NAME = "Transmission" +DEFAULT_SSL = False DEFAULT_PORT = 9091 +DEFAULT_PATH = "/transmission/rpc" DEFAULT_SCAN_INTERVAL = 120 STATE_ATTR_TORRENT_INFO = "torrent_info" diff --git a/homeassistant/components/transmission/coordinator.py b/homeassistant/components/transmission/coordinator.py index d03ef5e37fb..1c379685c1c 100644 --- a/homeassistant/components/transmission/coordinator.py +++ b/homeassistant/components/transmission/coordinator.py @@ -1,4 +1,5 @@ """Coordinator for transmssion integration.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/transmission/errors.py b/homeassistant/components/transmission/errors.py index b5f74f7bf40..68d442c3a74 100644 --- a/homeassistant/components/transmission/errors.py +++ b/homeassistant/components/transmission/errors.py @@ -1,4 +1,5 @@ """Errors for the Transmission component.""" + from homeassistant.exceptions import HomeAssistantError diff --git a/homeassistant/components/transmission/icons.json b/homeassistant/components/transmission/icons.json new file mode 100644 index 00000000000..56ae46f933d --- /dev/null +++ b/homeassistant/components/transmission/icons.json @@ -0,0 +1,8 @@ +{ + "services": { + "add_torrent": "mdi:download", + "remove_torrent": "mdi:download-off", + "start_torrent": "mdi:play", + "stop_torrent": "mdi:stop" + } +} diff --git a/homeassistant/components/transmission/sensor.py b/homeassistant/components/transmission/sensor.py index 87bcb87da9a..9ee42045aab 100644 --- a/homeassistant/components/transmission/sensor.py +++ b/homeassistant/components/transmission/sensor.py @@ -1,4 +1,5 @@ """Support for monitoring the Transmission BitTorrent client API.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/transmission/strings.json b/homeassistant/components/transmission/strings.json index 8a73eb90829..20ae6ca723d 100644 --- a/homeassistant/components/transmission/strings.json +++ b/homeassistant/components/transmission/strings.json @@ -5,9 +5,14 @@ "title": "Set up Transmission Client", "data": { "host": "[%key:common::config_flow::data::host%]", - "username": "[%key:common::config_flow::data::username%]", "password": "[%key:common::config_flow::data::password%]", - "port": "[%key:common::config_flow::data::port%]" + "path": "[%key:common::config_flow::data::path%]", + "port": "[%key:common::config_flow::data::port%]", + "ssl": "[%key:common::config_flow::data::ssl%]", + "username": "[%key:common::config_flow::data::username%]" + }, + "data_description": { + "path": "The RPC request target path. E.g. `/transmission/rpc`" } }, "reauth_confirm": { diff --git a/homeassistant/components/transmission/switch.py b/homeassistant/components/transmission/switch.py index 643b2f0ba70..8e79d8246e0 100644 --- a/homeassistant/components/transmission/switch.py +++ b/homeassistant/components/transmission/switch.py @@ -1,4 +1,5 @@ """Support for setting the Transmission BitTorrent client Turtle Mode.""" + from collections.abc import Callable from dataclasses import dataclass import logging @@ -17,22 +18,15 @@ from .coordinator import TransmissionDataUpdateCoordinator _LOGGING = logging.getLogger(__name__) -@dataclass(frozen=True) -class TransmissionSwitchEntityDescriptionMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class TransmissionSwitchEntityDescription(SwitchEntityDescription): + """Entity description class for Transmission switches.""" is_on_func: Callable[[TransmissionDataUpdateCoordinator], bool | None] on_func: Callable[[TransmissionDataUpdateCoordinator], None] off_func: Callable[[TransmissionDataUpdateCoordinator], None] -@dataclass(frozen=True) -class TransmissionSwitchEntityDescription( - SwitchEntityDescription, TransmissionSwitchEntityDescriptionMixin -): - """Entity description class for Transmission switches.""" - - SWITCH_TYPES: tuple[TransmissionSwitchEntityDescription, ...] = ( TransmissionSwitchEntityDescription( key="on_off", diff --git a/homeassistant/components/transport_nsw/sensor.py b/homeassistant/components/transport_nsw/sensor.py index 520b1a5626b..4ec4301dc7b 100644 --- a/homeassistant/components/transport_nsw/sensor.py +++ b/homeassistant/components/transport_nsw/sensor.py @@ -1,4 +1,5 @@ """Support for Transport NSW (AU) to query next leave event.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/travisci/sensor.py b/homeassistant/components/travisci/sensor.py index 6a30c1b62ba..0a3118b3cca 100644 --- a/homeassistant/components/travisci/sensor.py +++ b/homeassistant/components/travisci/sensor.py @@ -1,4 +1,5 @@ """Component providing HA sensor support for Travis CI framework.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/trend/__init__.py b/homeassistant/components/trend/__init__.py index 91d50bcc928..7ec2d140c5e 100644 --- a/homeassistant/components/trend/__init__.py +++ b/homeassistant/components/trend/__init__.py @@ -1,4 +1,5 @@ """A sensor that monitors trends in other components.""" + from __future__ import annotations from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/trend/binary_sensor.py b/homeassistant/components/trend/binary_sensor.py index c86fb65e966..526228c2be1 100644 --- a/homeassistant/components/trend/binary_sensor.py +++ b/homeassistant/components/trend/binary_sensor.py @@ -1,4 +1,5 @@ """A sensor that monitors trends in other components.""" + from __future__ import annotations from collections import deque @@ -30,7 +31,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import Event, HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import generate_entity_id from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -40,7 +41,7 @@ from homeassistant.helpers.event import ( ) from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.restore_state import RestoreEntity -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, EventType +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util.dt import utcnow from . import PLATFORMS @@ -214,7 +215,7 @@ class SensorTrend(BinarySensorEntity, RestoreEntity): @callback def trend_sensor_state_listener( - event: EventType[EventStateChangedData], + event: Event[EventStateChangedData], ) -> None: """Handle state changes on the observed device.""" if (new_state := event.data["new_state"]) is None: diff --git a/homeassistant/components/trend/config_flow.py b/homeassistant/components/trend/config_flow.py index 3d29618281a..f91e81bf4e8 100644 --- a/homeassistant/components/trend/config_flow.py +++ b/homeassistant/components/trend/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Trend integration.""" + from __future__ import annotations from collections.abc import Mapping diff --git a/homeassistant/components/trend/const.py b/homeassistant/components/trend/const.py index 838056bfc4d..5c1c5b9629b 100644 --- a/homeassistant/components/trend/const.py +++ b/homeassistant/components/trend/const.py @@ -1,4 +1,5 @@ """Constant values for Trend integration.""" + DOMAIN = "trend" ATTR_ATTRIBUTE = "attribute" diff --git a/homeassistant/components/trend/icons.json b/homeassistant/components/trend/icons.json new file mode 100644 index 00000000000..a03163179cb --- /dev/null +++ b/homeassistant/components/trend/icons.json @@ -0,0 +1,5 @@ +{ + "services": { + "reload": "mdi:reload" + } +} diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py index 9a44382e851..8ea4617bbf3 100644 --- a/homeassistant/components/tts/__init__.py +++ b/homeassistant/components/tts/__init__.py @@ -1,4 +1,5 @@ """Provide functionality for TTS.""" + from __future__ import annotations from abc import abstractmethod @@ -15,7 +16,7 @@ import os import re import subprocess import tempfile -from typing import Any, TypedDict, final +from typing import Any, Final, TypedDict, final from aiohttp import web import mutagen @@ -98,6 +99,13 @@ ATTR_PREFERRED_SAMPLE_CHANNELS = "preferred_sample_channels" ATTR_MEDIA_PLAYER_ENTITY_ID = "media_player_entity_id" ATTR_VOICE = "voice" +_DEFAULT_FORMAT = "mp3" +_PREFFERED_FORMAT_OPTIONS: Final[set[str]] = { + ATTR_PREFERRED_FORMAT, + ATTR_PREFERRED_SAMPLE_RATE, + ATTR_PREFERRED_SAMPLE_CHANNELS, +} + CONF_LANG = "language" SERVICE_CLEAR_CACHE = "clear_cache" @@ -318,9 +326,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: platform_setups = await async_setup_legacy(hass, config) - if platform_setups: - await asyncio.wait([asyncio.create_task(setup) for setup in platform_setups]) - component.async_register_entity_service( "speak", { @@ -344,6 +349,15 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: schema=SCHEMA_SERVICE_CLEAR_CACHE, ) + for setup in platform_setups: + # Tasks are created as tracked tasks to ensure startup + # waits for them to finish, but we explicitly do not + # want to wait for them to finish here because we want + # any config entries that use tts as a base platform + # to be able to start with out having to wait for the + # legacy platforms to finish setting up. + hass.async_create_task(setup, eager_start=True) + return True @@ -456,7 +470,7 @@ class TextToSpeechEntity(RestoreEntity): self, message: str, language: str, options: dict[str, Any] ) -> TtsAudioType: """Load tts audio file from the engine.""" - raise NotImplementedError() + raise NotImplementedError async def async_get_tts_audio( self, message: str, language: str, options: dict[str, Any] @@ -500,24 +514,21 @@ class SpeechManager: self.file_cache: dict[str, str] = {} self.mem_cache: dict[str, TTSCache] = {} - async def async_init_cache(self) -> None: - """Init config folder and load file cache.""" + def _init_cache(self) -> dict[str, str]: + """Init cache folder and fetch files.""" try: - self.cache_dir = await self.hass.async_add_executor_job( - _init_tts_cache_dir, self.hass, self.cache_dir - ) + self.cache_dir = _init_tts_cache_dir(self.hass, self.cache_dir) except OSError as err: raise HomeAssistantError(f"Can't init cache dir {err}") from err try: - cache_files = await self.hass.async_add_executor_job( - _get_cache_files, self.cache_dir - ) + return _get_cache_files(self.cache_dir) except OSError as err: raise HomeAssistantError(f"Can't read cache dir {err}") from err - if cache_files: - self.file_cache.update(cache_files) + async def async_init_cache(self) -> None: + """Init config folder and load file cache.""" + self.file_cache.update(await self.hass.async_add_executor_job(self._init_cache)) async def async_clear_cache(self) -> None: """Read file cache and delete files.""" @@ -565,25 +576,23 @@ class SpeechManager: ): raise HomeAssistantError(f"Language '{language}' not supported") + options = options or {} + supported_options = engine_instance.supported_options or [] + # Update default options with provided options + invalid_opts: list[str] = [] merged_options = dict(engine_instance.default_options or {}) - merged_options.update(options or {}) + for option_name, option_value in options.items(): + # Only count an option as invalid if it's not a "preferred format" + # option. These are used as hints to the TTS system if supported, + # and otherwise as parameters to ffmpeg conversion. + if (option_name in supported_options) or ( + option_name in _PREFFERED_FORMAT_OPTIONS + ): + merged_options[option_name] = option_value + else: + invalid_opts.append(option_name) - supported_options = list(engine_instance.supported_options or []) - - # ATTR_PREFERRED_* options are always "supported" since they're used to - # convert audio after the TTS has run (if necessary). - supported_options.extend( - ( - ATTR_PREFERRED_FORMAT, - ATTR_PREFERRED_SAMPLE_RATE, - ATTR_PREFERRED_SAMPLE_CHANNELS, - ) - ) - - invalid_opts = [ - opt_name for opt_name in merged_options if opt_name not in supported_options - ] if invalid_opts: raise HomeAssistantError(f"Invalid options found: {invalid_opts}") @@ -683,10 +692,31 @@ class SpeechManager: This method is a coroutine. """ - options = options or {} + options = dict(options or {}) + supported_options = engine_instance.supported_options or [] - # Default to MP3 unless a different format is preferred - final_extension = options.get(ATTR_PREFERRED_FORMAT, "mp3") + # Extract preferred format options. + # + # These options are used by Assist pipelines, etc. to get a format that + # the voice satellite will support. + # + # The TTS system ideally supports options directly so we won't have + # to convert with ffmpeg later. If not, we pop the options here and + # perform the conversation after receiving the audio. + if ATTR_PREFERRED_FORMAT in supported_options: + final_extension = options.get(ATTR_PREFERRED_FORMAT, _DEFAULT_FORMAT) + else: + final_extension = options.pop(ATTR_PREFERRED_FORMAT, _DEFAULT_FORMAT) + + if ATTR_PREFERRED_SAMPLE_RATE in supported_options: + sample_rate = options.get(ATTR_PREFERRED_SAMPLE_RATE) + else: + sample_rate = options.pop(ATTR_PREFERRED_SAMPLE_RATE, None) + + if ATTR_PREFERRED_SAMPLE_CHANNELS in supported_options: + sample_channels = options.get(ATTR_PREFERRED_SAMPLE_CHANNELS) + else: + sample_channels = options.pop(ATTR_PREFERRED_SAMPLE_CHANNELS, None) async def get_tts_data() -> str: """Handle data available.""" @@ -712,8 +742,8 @@ class SpeechManager: # rate/format/channel count is requested. needs_conversion = ( (final_extension != extension) - or (ATTR_PREFERRED_SAMPLE_RATE in options) - or (ATTR_PREFERRED_SAMPLE_CHANNELS in options) + or (sample_rate is not None) + or (sample_channels is not None) ) if needs_conversion: @@ -722,8 +752,8 @@ class SpeechManager: extension, data, to_extension=final_extension, - to_sample_rate=options.get(ATTR_PREFERRED_SAMPLE_RATE), - to_sample_channels=options.get(ATTR_PREFERRED_SAMPLE_CHANNELS), + to_sample_rate=sample_rate, + to_sample_channels=sample_channels, ) # Create file infos diff --git a/homeassistant/components/tts/const.py b/homeassistant/components/tts/const.py index f721731330c..99015512498 100644 --- a/homeassistant/components/tts/const.py +++ b/homeassistant/components/tts/const.py @@ -1,4 +1,5 @@ """Text-to-speech constants.""" + ATTR_CACHE = "cache" ATTR_LANGUAGE = "language" ATTR_MESSAGE = "message" diff --git a/homeassistant/components/tts/helper.py b/homeassistant/components/tts/helper.py index 8cbfcbd8935..4b5ef168550 100644 --- a/homeassistant/components/tts/helper.py +++ b/homeassistant/components/tts/helper.py @@ -1,4 +1,5 @@ """Provide helper functions for the TTS.""" + from __future__ import annotations from typing import TYPE_CHECKING diff --git a/homeassistant/components/tts/legacy.py b/homeassistant/components/tts/legacy.py index 05be2e284e3..88249ed107b 100644 --- a/homeassistant/components/tts/legacy.py +++ b/homeassistant/components/tts/legacy.py @@ -1,4 +1,5 @@ """Provide the legacy TTS service provider interface.""" + from __future__ import annotations from abc import abstractmethod @@ -30,7 +31,11 @@ from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv from homeassistant.helpers.service import async_set_service_schema from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from homeassistant.setup import async_prepare_setup_platform +from homeassistant.setup import ( + SetupPhases, + async_prepare_setup_platform, + async_start_setup, +) from homeassistant.util.yaml import load_yaml_dict from .const import ( @@ -123,20 +128,26 @@ async def async_setup_legacy( return try: - if hasattr(platform, "async_get_engine"): - provider = await platform.async_get_engine( - hass, p_config, discovery_info - ) - else: - provider = await hass.async_add_executor_job( - platform.get_engine, hass, p_config, discovery_info - ) + with async_start_setup( + hass, + integration=p_type, + group=str(id(p_config)), + phase=SetupPhases.PLATFORM_SETUP, + ): + if hasattr(platform, "async_get_engine"): + provider = await platform.async_get_engine( + hass, p_config, discovery_info + ) + else: + provider = await hass.async_add_executor_job( + platform.get_engine, hass, p_config, discovery_info + ) - if provider is None: - _LOGGER.error("Error setting up platform: %s", p_type) - return + if provider is None: + _LOGGER.error("Error setting up platform: %s", p_type) + return - tts.async_register_legacy_engine(p_type, provider, p_config) + tts.async_register_legacy_engine(p_type, provider, p_config) except Exception: # pylint: disable=broad-except _LOGGER.exception("Error setting up platform: %s", p_type) return @@ -230,7 +241,7 @@ class Provider: self, message: str, language: str, options: dict[str, Any] ) -> TtsAudioType: """Load tts audio file from provider.""" - raise NotImplementedError() + raise NotImplementedError async def async_get_tts_audio( self, message: str, language: str, options: dict[str, Any] diff --git a/homeassistant/components/tts/media_source.py b/homeassistant/components/tts/media_source.py index 837a15a4f88..a907fc485c9 100644 --- a/homeassistant/components/tts/media_source.py +++ b/homeassistant/components/tts/media_source.py @@ -1,4 +1,5 @@ """Text-to-speech media source.""" + from __future__ import annotations import mimetypes diff --git a/homeassistant/components/tts/models.py b/homeassistant/components/tts/models.py index 1ea49b1e9ed..2d693571a0f 100644 --- a/homeassistant/components/tts/models.py +++ b/homeassistant/components/tts/models.py @@ -1,4 +1,5 @@ """Text-to-speech data models.""" + from dataclasses import dataclass diff --git a/homeassistant/components/tts/notify.py b/homeassistant/components/tts/notify.py index c2576e12bb5..e6963619043 100644 --- a/homeassistant/components/tts/notify.py +++ b/homeassistant/components/tts/notify.py @@ -1,4 +1,5 @@ """Support notifications through TTS service.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/tts/strings.json b/homeassistant/components/tts/strings.json index 2f0208ef8b5..2d1ac379c26 100644 --- a/homeassistant/components/tts/strings.json +++ b/homeassistant/components/tts/strings.json @@ -17,7 +17,7 @@ "description": "Stores this message locally so that when the text is requested again, the output can be produced more quickly." }, "language": { - "name": "Language", + "name": "[%key:common::config_flow::data::language%]", "description": "Language to use for speech generation." }, "options": { @@ -43,7 +43,7 @@ "description": "[%key:component::tts::services::say::fields::cache::description%]" }, "language": { - "name": "[%key:component::tts::services::say::fields::language::name%]", + "name": "[%key:common::config_flow::data::language%]", "description": "[%key:component::tts::services::say::fields::language::description%]" }, "options": { diff --git a/homeassistant/components/tuya/__init__.py b/homeassistant/components/tuya/__init__.py index a1ce3a60efe..ceb8f056c22 100644 --- a/homeassistant/components/tuya/__init__.py +++ b/homeassistant/components/tuya/__init__.py @@ -1,4 +1,5 @@ """Support for Tuya Smart devices.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/tuya/alarm_control_panel.py b/homeassistant/components/tuya/alarm_control_panel.py index 681f025f57b..59075cf00cd 100644 --- a/homeassistant/components/tuya/alarm_control_panel.py +++ b/homeassistant/components/tuya/alarm_control_panel.py @@ -1,4 +1,5 @@ """Support for Tuya Alarm.""" + from __future__ import annotations from enum import StrEnum @@ -70,11 +71,11 @@ async def async_setup_entry( for device_id in device_ids: device = hass_data.manager.device_map[device_id] if descriptions := ALARM.get(device.category): - for description in descriptions: - if description.key in device.status: - entities.append( - TuyaAlarmEntity(device, hass_data.manager, description) - ) + entities.extend( + TuyaAlarmEntity(device, hass_data.manager, description) + for description in descriptions + if description.key in device.status + ) async_add_entities(entities) async_discover_device([*hass_data.manager.device_map]) @@ -87,7 +88,6 @@ async def async_setup_entry( class TuyaAlarmEntity(TuyaEntity, AlarmControlPanelEntity): """Tuya Alarm Entity.""" - _attr_icon = "mdi:security" _attr_name = None def __init__( diff --git a/homeassistant/components/tuya/base.py b/homeassistant/components/tuya/base.py index 7c4e213fe65..8ff7041fd5e 100644 --- a/homeassistant/components/tuya/base.py +++ b/homeassistant/components/tuya/base.py @@ -1,4 +1,5 @@ """Tuya Home Assistant Base Device Model.""" + from __future__ import annotations import base64 @@ -165,8 +166,7 @@ class TuyaEntity(Entity): *, prefer_function: bool = False, dptype: Literal[DPType.ENUM], - ) -> EnumTypeData | None: - ... + ) -> EnumTypeData | None: ... @overload def find_dpcode( @@ -175,8 +175,7 @@ class TuyaEntity(Entity): *, prefer_function: bool = False, dptype: Literal[DPType.INTEGER], - ) -> IntegerTypeData | None: - ... + ) -> IntegerTypeData | None: ... @overload def find_dpcode( @@ -184,8 +183,7 @@ class TuyaEntity(Entity): dpcodes: str | DPCode | tuple[DPCode, ...] | None, *, prefer_function: bool = False, - ) -> DPCode | None: - ... + ) -> DPCode | None: ... def find_dpcode( self, diff --git a/homeassistant/components/tuya/binary_sensor.py b/homeassistant/components/tuya/binary_sensor.py index 5664801d76e..c9f4734a7df 100644 --- a/homeassistant/components/tuya/binary_sensor.py +++ b/homeassistant/components/tuya/binary_sensor.py @@ -1,4 +1,5 @@ """Support for Tuya binary sensors.""" + from __future__ import annotations from dataclasses import dataclass @@ -51,7 +52,6 @@ BINARY_SENSORS: dict[str, tuple[TuyaBinarySensorEntityDescription, ...]] = { "dgnbj": ( TuyaBinarySensorEntityDescription( key=DPCode.GAS_SENSOR_STATE, - icon="mdi:gas-cylinder", device_class=BinarySensorDeviceClass.GAS, on_value="alarm", ), @@ -76,14 +76,12 @@ BINARY_SENSORS: dict[str, tuple[TuyaBinarySensorEntityDescription, ...]] = { TuyaBinarySensorEntityDescription( key=DPCode.CO_STATE, translation_key="carbon_monoxide", - icon="mdi:molecule-co", device_class=BinarySensorDeviceClass.SAFETY, on_value="alarm", ), TuyaBinarySensorEntityDescription( key=DPCode.CO2_STATE, translation_key="carbon_dioxide", - icon="mdi:molecule-co2", device_class=BinarySensorDeviceClass.SAFETY, on_value="alarm", ), @@ -109,7 +107,6 @@ BINARY_SENSORS: dict[str, tuple[TuyaBinarySensorEntityDescription, ...]] = { ), TuyaBinarySensorEntityDescription( key=DPCode.SMOKE_SENSOR_STATE, - icon="mdi:smoke-detector", device_class=BinarySensorDeviceClass.SMOKE, on_value="alarm", ), @@ -146,7 +143,6 @@ BINARY_SENSORS: dict[str, tuple[TuyaBinarySensorEntityDescription, ...]] = { TuyaBinarySensorEntityDescription( key=DPCode.FEED_STATE, translation_key="feeding", - icon="mdi:information", on_value="feeding", ), ), @@ -329,14 +325,12 @@ BINARY_SENSORS: dict[str, tuple[TuyaBinarySensorEntityDescription, ...]] = { key=f"{DPCode.SHOCK_STATE}_drop", dpcode=DPCode.SHOCK_STATE, translation_key="drop", - icon="mdi:icon=package-down", on_value="drop", ), TuyaBinarySensorEntityDescription( key=f"{DPCode.SHOCK_STATE}_tilt", dpcode=DPCode.SHOCK_STATE, translation_key="tilt", - icon="mdi:spirit-level", on_value="tilt", ), ), diff --git a/homeassistant/components/tuya/button.py b/homeassistant/components/tuya/button.py index 5b936b305fb..a170ddb09e9 100644 --- a/homeassistant/components/tuya/button.py +++ b/homeassistant/components/tuya/button.py @@ -1,4 +1,5 @@ """Support for Tuya buttons.""" + from __future__ import annotations from tuya_sharing import CustomerDevice, Manager @@ -23,31 +24,26 @@ BUTTONS: dict[str, tuple[ButtonEntityDescription, ...]] = { ButtonEntityDescription( key=DPCode.RESET_DUSTER_CLOTH, translation_key="reset_duster_cloth", - icon="mdi:restart", entity_category=EntityCategory.CONFIG, ), ButtonEntityDescription( key=DPCode.RESET_EDGE_BRUSH, translation_key="reset_edge_brush", - icon="mdi:restart", entity_category=EntityCategory.CONFIG, ), ButtonEntityDescription( key=DPCode.RESET_FILTER, translation_key="reset_filter", - icon="mdi:air-filter", entity_category=EntityCategory.CONFIG, ), ButtonEntityDescription( key=DPCode.RESET_MAP, translation_key="reset_map", - icon="mdi:map-marker-remove", entity_category=EntityCategory.CONFIG, ), ButtonEntityDescription( key=DPCode.RESET_ROLL_BRUSH, translation_key="reset_roll_brush", - icon="mdi:restart", entity_category=EntityCategory.CONFIG, ), ), @@ -57,7 +53,6 @@ BUTTONS: dict[str, tuple[ButtonEntityDescription, ...]] = { ButtonEntityDescription( key=DPCode.SWITCH_USB6, translation_key="snooze", - icon="mdi:sleep", ), ), } @@ -76,11 +71,11 @@ async def async_setup_entry( for device_id in device_ids: device = hass_data.manager.device_map[device_id] if descriptions := BUTTONS.get(device.category): - for description in descriptions: - if description.key in device.status: - entities.append( - TuyaButtonEntity(device, hass_data.manager, description) - ) + entities.extend( + TuyaButtonEntity(device, hass_data.manager, description) + for description in descriptions + if description.key in device.status + ) async_add_entities(entities) diff --git a/homeassistant/components/tuya/camera.py b/homeassistant/components/tuya/camera.py index 07c4adb8889..79f8c1b1692 100644 --- a/homeassistant/components/tuya/camera.py +++ b/homeassistant/components/tuya/camera.py @@ -1,4 +1,5 @@ """Support for Tuya cameras.""" + from __future__ import annotations from tuya_sharing import CustomerDevice, Manager diff --git a/homeassistant/components/tuya/climate.py b/homeassistant/components/tuya/climate.py index 45adb532705..3be80193beb 100644 --- a/homeassistant/components/tuya/climate.py +++ b/homeassistant/components/tuya/climate.py @@ -1,4 +1,5 @@ """Support for Tuya Climate.""" + from __future__ import annotations from dataclasses import dataclass @@ -39,20 +40,13 @@ TUYA_HVAC_TO_HA = { } -@dataclass(frozen=True) -class TuyaClimateSensorDescriptionMixin: - """Define an entity description mixin for climate entities.""" +@dataclass(frozen=True, kw_only=True) +class TuyaClimateEntityDescription(ClimateEntityDescription): + """Describe an Tuya climate entity.""" switch_only_hvac_mode: HVACMode -@dataclass(frozen=True) -class TuyaClimateEntityDescription( - ClimateEntityDescription, TuyaClimateSensorDescriptionMixin -): - """Describe an Tuya climate entity.""" - - CLIMATE_DESCRIPTIONS: dict[str, TuyaClimateEntityDescription] = { # Air conditioner # https://developer.tuya.com/en/docs/iot/categorykt?id=Kaiuz0z71ov2n diff --git a/homeassistant/components/tuya/config_flow.py b/homeassistant/components/tuya/config_flow.py index e0ac5375b00..bdef321de7a 100644 --- a/homeassistant/components/tuya/config_flow.py +++ b/homeassistant/components/tuya/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Tuya.""" + from __future__ import annotations from collections.abc import Mapping @@ -7,8 +8,7 @@ from typing import Any from tuya_sharing import LoginControl import voluptuous as vol -from homeassistant.config_entries import ConfigEntry, ConfigFlow -from homeassistant.data_entry_flow import FlowResult +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.helpers import selector from .const import ( @@ -40,7 +40,7 @@ class TuyaConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Step user.""" errors = {} placeholders = {} @@ -75,7 +75,7 @@ class TuyaConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_scan( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Step scan.""" if user_input is None: return self.async_show_form( @@ -146,7 +146,7 @@ class TuyaConfigFlow(ConfigFlow, domain=DOMAIN): data=entry_data, ) - async def async_step_reauth(self, _: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth(self, _: Mapping[str, Any]) -> ConfigFlowResult: """Handle initiation of re-authentication with Tuya.""" self.__reauth_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] @@ -163,7 +163,7 @@ class TuyaConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_reauth_user_code( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle re-authentication with a Tuya.""" errors = {} placeholders = {} diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index 8f15114aa80..a9c53d807bc 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -1,4 +1,5 @@ """Constants for the Tuya integration.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/tuya/cover.py b/homeassistant/components/tuya/cover.py index 912087d2c8c..7dc54888ac4 100644 --- a/homeassistant/components/tuya/cover.py +++ b/homeassistant/components/tuya/cover.py @@ -1,4 +1,5 @@ """Support for Tuya Cover.""" + from __future__ import annotations from dataclasses import dataclass @@ -154,14 +155,14 @@ async def async_setup_entry( for device_id in device_ids: device = hass_data.manager.device_map[device_id] if descriptions := COVERS.get(device.category): - for description in descriptions: + entities.extend( + TuyaCoverEntity(device, hass_data.manager, description) + for description in descriptions if ( description.key in device.function or description.key in device.status_range - ): - entities.append( - TuyaCoverEntity(device, hass_data.manager, description) - ) + ) + ) async_add_entities(entities) diff --git a/homeassistant/components/tuya/diagnostics.py b/homeassistant/components/tuya/diagnostics.py index cdd0d5ed51c..f817261c8fc 100644 --- a/homeassistant/components/tuya/diagnostics.py +++ b/homeassistant/components/tuya/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for Tuya.""" + from __future__ import annotations from contextlib import suppress diff --git a/homeassistant/components/tuya/fan.py b/homeassistant/components/tuya/fan.py index 0971462e450..3925da1d507 100644 --- a/homeassistant/components/tuya/fan.py +++ b/homeassistant/components/tuya/fan.py @@ -1,4 +1,5 @@ """Support for Tuya Fan.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/tuya/humidifier.py b/homeassistant/components/tuya/humidifier.py index 7cc4fee03fc..927aaf8a74a 100644 --- a/homeassistant/components/tuya/humidifier.py +++ b/homeassistant/components/tuya/humidifier.py @@ -1,4 +1,5 @@ """Support for Tuya (de)humidifiers.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/homeassistant/components/tuya/icons.json b/homeassistant/components/tuya/icons.json new file mode 100644 index 00000000000..48ae61f36fd --- /dev/null +++ b/homeassistant/components/tuya/icons.json @@ -0,0 +1,373 @@ +{ + "entity": { + "binary_sensor": { + "carbon_monoxide": { + "default": "mdi:molecule-co" + }, + "carbon_dioxide": { + "default": "mdi:molecule-co2" + }, + "feeding": { + "default": "mdi:information" + }, + "drop": { + "default": "mdi:package-down" + }, + "tilt": { + "default": "mdi:spirit-level" + } + }, + "button": { + "reset_duster_cloth": { + "default": "mdi:restart" + }, + "reset_edge_brush": { + "default": "mdi:restart" + }, + "reset_filter": { + "default": "mdi:air-filter" + }, + "reset_map": { + "default": "mdi:map-marker-remove" + }, + "reset_roll_brush": { + "default": "mdi:restart" + }, + "snooze": { + "default": "mdi:sleep" + } + }, + "number": { + "heat_preservation_time": { + "default": "mdi:timer" + }, + "feed": { + "default": "mdi:bowl" + }, + "voice_times": { + "default": "mdi:microphone" + }, + "near_detection": { + "default": "mdi:signal-distance-variant" + }, + "far_detection": { + "default": "mdi:signal-distance-variant" + }, + "water_level": { + "default": "mdi:cup-water" + }, + "cook_time": { + "default": "mdi:timer" + }, + "volume": { + "default": "mdi:volume-high" + }, + "minimum_brightness": { + "default": "mdi:lightbulb-outline" + }, + "maximum_brightness": { + "default": "mdi:lightbulb-on-outline" + }, + "minimum_brightness_2": { + "default": "mdi:lightbulb-outline" + }, + "maximum_brightness_2": { + "default": "mdi:lightbulb-on-outline" + }, + "minimum_brightness_3": { + "default": "mdi:lightbulb-outline" + }, + "maximum_brightness_3": { + "default": "mdi:lightbulb-on-outline" + }, + "move_down": { + "default": "mdi:arrow-down-bold" + }, + "move_up": { + "default": "mdi:arrow-up-bold" + }, + "down_delay": { + "default": "mdi:timer" + }, + "temperature": { + "default": "mdi:thermometer" + } + }, + "select": { + "cups": { + "default": "mdi:numeric" + }, + "concentration": { + "default": "mdi:altimeter" + }, + "mode": { + "default": "mdi:coffee" + }, + "temperature_level": { + "default": "mdi:thermometer-lines" + }, + "weather_delay": { + "default": "mdi:weather-cloudy-clock" + }, + "decibel_sensitivity": { + "default": "mdi:volume-vibrate" + }, + "record_mode": { + "default": "mdi:record-rec" + }, + "basic_nightvision": { + "default": "mdi:theme-light-dark" + }, + "basic_anti_flicker": { + "default": "mdi:image-outline" + }, + "motion_sensitivity": { + "default": "mdi:motion-sensor" + }, + "vacuum_cistern": { + "default": "mdi:water-opacity" + }, + "vacuum_collection": { + "default": "mdi:air-filter" + }, + "vacuum_mode": { + "default": "mdi:layers-outline" + }, + "vertical_fan_angle": { + "default": "mdi:format-vertical-align-center" + }, + "horizontal_fan_angle": { + "default": "mdi:format-horizontal-align-center" + }, + "countdown": { + "default": "mdi:timer-cog-outline" + }, + "curtain_motor_mode": { + "default": "mdi:swap-horizontal" + }, + "humidifier_spray_mode": { + "default": "mdi:spray" + }, + "humidifier_level": { + "default": "mdi:spray" + }, + "humidifier_moodlighting": { + "default": "mdi:lightbulb-multiple" + }, + "target_humidity": { + "default": "mdi:water-percent" + } + }, + "sensor": { + "battery_state": { + "default": "mdi:battery" + }, + "gas": { + "default": "mdi:gas-cylinder" + }, + "carbon_monoxide": { + "default": "mdi:molecule-co" + }, + "carbon_dioxide": { + "default": "mdi:molecule-co2" + }, + "luminosity": { + "default": "mdi:brightness-6" + }, + "illuminance": { + "default": "mdi:brightness-6" + }, + "smoke_amount": { + "default": "mdi:smoke-detector" + }, + "last_amount": { + "default": "mdi:counter" + }, + "remaining_time": { + "default": "mdi:timer" + }, + "total_watering_time": { + "default": "mdi:history" + }, + "cleaning_area": { + "default": "mdi:texture-box" + }, + "cleaning_time": { + "default": "mdi:progress-clock" + }, + "total_cleaning_area": { + "default": "mdi:texture-box" + }, + "total_cleaning_time": { + "default": "mdi:history" + }, + "total_cleaning_times": { + "default": "mdi:counter" + }, + "duster_cloth_life": { + "default": "mdi:ticket-percent-outline" + }, + "side_brush_life": { + "default": "mdi:ticket-percent-outline" + }, + "filter_life": { + "default": "mdi:ticket-percent-outline" + }, + "rolling_brush_life": { + "default": "mdi:ticket-percent-outline" + }, + "last_operation_duration": { + "default": "mdi:progress-clock" + }, + "water_level": { + "default": "mdi:waves-arrow-up" + }, + "filter_utilization": { + "default": "mdi:ticket-percent-outline" + }, + "pm25": { + "default": "mdi:molecule" + }, + "total_operating_time": { + "default": "mdi:history" + }, + "total_absorption_particles": { + "default": "mdi:texture-box" + }, + "air_quality": { + "default": "mdi:air-filter" + } + }, + "switch": { + "start": { + "default": "mdi:kettle-steam" + }, + "disinfection": { + "default": "mdi:bacteria" + }, + "water": { + "default": "mdi:water" + }, + "slow_feed": { + "default": "mdi:speedometer-slow" + }, + "filter_reset": { + "default": "mdi:filter" + }, + "water_pump_reset": { + "default": "mdi:pump" + }, + "reset_of_water_usage_days": { + "default": "mdi:water-sync" + }, + "uv_sterilization": { + "default": "mdi:lightbulb" + }, + "child_lock": { + "default": "mdi:account-lock" + }, + "radio": { + "default": "mdi:radio" + }, + "alarm_1": { + "default": "mdi:alarm" + }, + "alarm_2": { + "default": "mdi:alarm" + }, + "alarm_3": { + "default": "mdi:alarm" + }, + "alarm_4": { + "default": "mdi:alarm" + }, + "sleep_aid": { + "default": "mdi:power-sleep" + }, + "ionizer": { + "default": "mdi:minus-circle-outline" + }, + "filter_cartridge_reset": { + "default": "mdi:filter" + }, + "humidification": { + "default": "mdi:water-percent" + }, + "switch": { + "default": "mdi:power" + }, + "do_not_disturb": { + "default": "mdi:minus-circle" + }, + "mute_voice": { + "default": "mdi:account-voice" + }, + "battery_lock": { + "default": "mdi:battery-lock" + }, + "cry_detection": { + "default": "mdi:emoticon-cry" + }, + "sound_detection": { + "default": "mdi:microphone-outline" + }, + "video_recording": { + "default": "mdi:record-rec" + }, + "motion_recording": { + "default": "mdi:record-rec" + }, + "privacy_mode": { + "default": "mdi:eye-off" + }, + "flip": { + "default": "mdi:flip-horizontal" + }, + "time_watermark": { + "default": "mdi:watermark" + }, + "wide_dynamic_range": { + "default": "mdi:watermark" + }, + "motion_tracking": { + "default": "mdi:motion-sensor" + }, + "motion_alarm": { + "default": "mdi:motion-sensor" + }, + "energy_saving": { + "default": "mdi:leaf" + }, + "open_window_detection": { + "default": "mdi:window-open" + }, + "spray": { + "default": "mdi:spray" + }, + "voice": { + "default": "mdi:account-voice" + }, + "anion": { + "default": "mdi:atom" + }, + "oxygen_bar": { + "default": "mdi:molecule" + }, + "natural_wind": { + "default": "mdi:weather-windy" + }, + "sound": { + "default": "mdi:minus-circle" + }, + "reverse": { + "default": "mdi:swap-horizontal" + }, + "sleep": { + "default": "mdi:power-sleep" + }, + "sterilization": { + "default": "mdi:minus-circle-outline" + } + } + } +} diff --git a/homeassistant/components/tuya/light.py b/homeassistant/components/tuya/light.py index 98d704326ae..d898e837d8e 100644 --- a/homeassistant/components/tuya/light.py +++ b/homeassistant/components/tuya/light.py @@ -1,4 +1,5 @@ """Support for the Tuya lights.""" + from __future__ import annotations from dataclasses import dataclass, field @@ -14,6 +15,7 @@ from homeassistant.components.light import ( ColorMode, LightEntity, LightEntityDescription, + filter_supported_color_modes, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory @@ -378,6 +380,10 @@ LIGHTS["cz"] = LIGHTS["kg"] # https://developer.tuya.com/en/docs/iot/s?id=K9gf7o5prgf7s LIGHTS["pc"] = LIGHTS["kg"] +# Dimmer (duplicate of `tgq`) +# https://developer.tuya.com/en/docs/iot/tgq?id=Kaof8ke9il4k4 +LIGHTS["tdq"] = LIGHTS["tgq"] + @dataclass class ColorData: @@ -415,11 +421,11 @@ async def async_setup_entry( for device_id in device_ids: device = hass_data.manager.device_map[device_id] if descriptions := LIGHTS.get(device.category): - for description in descriptions: - if description.key in device.status: - entities.append( - TuyaLightEntity(device, hass_data.manager, description) - ) + entities.extend( + TuyaLightEntity(device, hass_data.manager, description) + for description in descriptions + if description.key in device.status + ) async_add_entities(entities) @@ -442,6 +448,7 @@ class TuyaLightEntity(TuyaEntity, LightEntity): _color_data_type: ColorTypeData | None = None _color_mode: DPCode | None = None _color_temp: IntegerTypeData | None = None + _fixed_color_mode: ColorMode | None = None def __init__( self, @@ -453,7 +460,7 @@ class TuyaLightEntity(TuyaEntity, LightEntity): super().__init__(device, device_manager) self.entity_description = description self._attr_unique_id = f"{super().unique_id}{description.key}" - self._attr_supported_color_modes: set[ColorMode] = set() + color_modes: set[ColorMode] = {ColorMode.ONOFF} # Determine DPCodes self._color_mode_dpcode = self.find_dpcode( @@ -464,7 +471,7 @@ class TuyaLightEntity(TuyaEntity, LightEntity): description.brightness, dptype=DPType.INTEGER, prefer_function=True ): self._brightness = int_type - self._attr_supported_color_modes.add(ColorMode.BRIGHTNESS) + color_modes.add(ColorMode.BRIGHTNESS) self._brightness_max = self.find_dpcode( description.brightness_max, dptype=DPType.INTEGER ) @@ -476,13 +483,13 @@ class TuyaLightEntity(TuyaEntity, LightEntity): description.color_temp, dptype=DPType.INTEGER, prefer_function=True ): self._color_temp = int_type - self._attr_supported_color_modes.add(ColorMode.COLOR_TEMP) + color_modes.add(ColorMode.COLOR_TEMP) if ( dpcode := self.find_dpcode(description.color_data, prefer_function=True) ) and self.get_dptype(dpcode) == DPType.JSON: self._color_data_dpcode = dpcode - self._attr_supported_color_modes.add(ColorMode.HS) + color_modes.add(ColorMode.HS) if dpcode in self.device.function: values = cast(str, self.device.function[dpcode].values) else: @@ -503,8 +510,10 @@ class TuyaLightEntity(TuyaEntity, LightEntity): ): self._color_data_type = DEFAULT_COLOR_TYPE_DATA_V2 - if not self._attr_supported_color_modes: - self._attr_supported_color_modes = {ColorMode.ONOFF} + self._attr_supported_color_modes = filter_supported_color_modes(color_modes) + if len(self._attr_supported_color_modes) == 1: + # If the light supports only a single color mode, set it now + self._fixed_color_mode = next(iter(self._attr_supported_color_modes)) @property def is_on(self) -> bool: @@ -698,18 +707,19 @@ class TuyaLightEntity(TuyaEntity, LightEntity): @property def color_mode(self) -> ColorMode: """Return the color_mode of the light.""" - # We consider it to be in HS color mode, when work mode is anything + if self._fixed_color_mode: + # The light supports only a single color mode, return it + return self._fixed_color_mode + + # The light supports both color temperature and HS, determine which mode the + # light is in. We consider it to be in HS color mode, when work mode is anything # else than "white". if ( self._color_mode_dpcode and self.device.status.get(self._color_mode_dpcode) != WorkMode.WHITE ): return ColorMode.HS - if self._color_temp: - return ColorMode.COLOR_TEMP - if self._brightness: - return ColorMode.BRIGHTNESS - return ColorMode.ONOFF + return ColorMode.COLOR_TEMP def _get_color_data(self) -> ColorData | None: """Get current color data from device.""" diff --git a/homeassistant/components/tuya/number.py b/homeassistant/components/tuya/number.py index 8fc55d2c230..2be7deef89f 100644 --- a/homeassistant/components/tuya/number.py +++ b/homeassistant/components/tuya/number.py @@ -1,4 +1,5 @@ """Support for Tuya number.""" + from __future__ import annotations from tuya_sharing import CustomerDevice, Manager @@ -38,34 +39,29 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = { key=DPCode.TEMP_SET, translation_key="temperature", device_class=NumberDeviceClass.TEMPERATURE, - icon="mdi:thermometer", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.TEMP_SET_F, translation_key="temperature", device_class=NumberDeviceClass.TEMPERATURE, - icon="mdi:thermometer", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.TEMP_BOILING_C, translation_key="temperature_after_boiling", device_class=NumberDeviceClass.TEMPERATURE, - icon="mdi:thermometer", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.TEMP_BOILING_F, translation_key="temperature_after_boiling", device_class=NumberDeviceClass.TEMPERATURE, - icon="mdi:thermometer", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.WARM_TIME, translation_key="heat_preservation_time", - icon="mdi:timer", entity_category=EntityCategory.CONFIG, ), ), @@ -75,12 +71,10 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = { NumberEntityDescription( key=DPCode.MANUAL_FEED, translation_key="feed", - icon="mdi:bowl", ), NumberEntityDescription( key=DPCode.VOICE_TIMES, translation_key="voice_times", - icon="mdi:microphone", ), ), # Human Presence Sensor @@ -94,13 +88,11 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = { NumberEntityDescription( key=DPCode.NEAR_DETECTION, translation_key="near_detection", - icon="mdi:signal-distance-variant", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.FAR_DETECTION, translation_key="far_detection", - icon="mdi:signal-distance-variant", entity_category=EntityCategory.CONFIG, ), ), @@ -110,20 +102,17 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = { NumberEntityDescription( key=DPCode.WATER_SET, translation_key="water_level", - icon="mdi:cup-water", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.TEMP_SET, translation_key="temperature", device_class=NumberDeviceClass.TEMPERATURE, - icon="mdi:thermometer", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.WARM_TIME, translation_key="heat_preservation_time", - icon="mdi:timer", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( @@ -138,13 +127,11 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = { NumberEntityDescription( key=DPCode.COOK_TEMPERATURE, translation_key="cook_temperature", - icon="mdi:thermometer", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.COOK_TIME, translation_key="cook_time", - icon="mdi:timer", native_unit_of_measurement=UnitOfTime.MINUTES, entity_category=EntityCategory.CONFIG, ), @@ -160,7 +147,6 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = { NumberEntityDescription( key=DPCode.VOLUME_SET, translation_key="volume", - icon="mdi:volume-high", entity_category=EntityCategory.CONFIG, ), ), @@ -179,7 +165,6 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = { NumberEntityDescription( key=DPCode.BASIC_DEVICE_VOLUME, translation_key="volume", - icon="mdi:volume-high", entity_category=EntityCategory.CONFIG, ), ), @@ -189,37 +174,31 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = { NumberEntityDescription( key=DPCode.BRIGHTNESS_MIN_1, translation_key="minimum_brightness", - icon="mdi:lightbulb-outline", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.BRIGHTNESS_MAX_1, translation_key="maximum_brightness", - icon="mdi:lightbulb-on-outline", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.BRIGHTNESS_MIN_2, translation_key="minimum_brightness_2", - icon="mdi:lightbulb-outline", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.BRIGHTNESS_MAX_2, translation_key="maximum_brightness_2", - icon="mdi:lightbulb-on-outline", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.BRIGHTNESS_MIN_3, translation_key="minimum_brightness_3", - icon="mdi:lightbulb-outline", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.BRIGHTNESS_MAX_3, translation_key="maximum_brightness_3", - icon="mdi:lightbulb-on-outline", entity_category=EntityCategory.CONFIG, ), ), @@ -229,25 +208,21 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = { NumberEntityDescription( key=DPCode.BRIGHTNESS_MIN_1, translation_key="minimum_brightness", - icon="mdi:lightbulb-outline", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.BRIGHTNESS_MAX_1, translation_key="maximum_brightness", - icon="mdi:lightbulb-on-outline", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.BRIGHTNESS_MIN_2, translation_key="minimum_brightness_2", - icon="mdi:lightbulb-outline", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.BRIGHTNESS_MAX_2, translation_key="maximum_brightness_2", - icon="mdi:lightbulb-on-outline", entity_category=EntityCategory.CONFIG, ), ), @@ -265,21 +240,18 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = { NumberEntityDescription( key=DPCode.ARM_DOWN_PERCENT, translation_key="move_down", - icon="mdi:arrow-down-bold", native_unit_of_measurement=PERCENTAGE, entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.ARM_UP_PERCENT, translation_key="move_up", - icon="mdi:arrow-up-bold", native_unit_of_measurement=PERCENTAGE, entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.CLICK_SUSTAIN_TIME, translation_key="down_delay", - icon="mdi:timer", entity_category=EntityCategory.CONFIG, ), ), @@ -290,7 +262,6 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = { key=DPCode.TEMP, translation_key="temperature", device_class=NumberDeviceClass.TEMPERATURE, - icon="mdi:thermometer-lines", ), ), # Humidifier @@ -300,13 +271,11 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = { key=DPCode.TEMP_SET, translation_key="temperature", device_class=NumberDeviceClass.TEMPERATURE, - icon="mdi:thermometer-lines", ), NumberEntityDescription( key=DPCode.TEMP_SET_F, translation_key="temperature", device_class=NumberDeviceClass.TEMPERATURE, - icon="mdi:thermometer-lines", ), ), } @@ -325,11 +294,11 @@ async def async_setup_entry( for device_id in device_ids: device = hass_data.manager.device_map[device_id] if descriptions := NUMBERS.get(device.category): - for description in descriptions: - if description.key in device.status: - entities.append( - TuyaNumberEntity(device, hass_data.manager, description) - ) + entities.extend( + TuyaNumberEntity(device, hass_data.manager, description) + for description in descriptions + if description.key in device.status + ) async_add_entities(entities) @@ -390,10 +359,6 @@ class TuyaNumberEntity(TuyaEntity, NumberEntity): self._attr_device_class = None return - # If we still have a device class, we should not use an icon - if self.device_class: - self._attr_icon = None - # Found unit of measurement, use the standardized Unit # Use the target conversion unit (if set) self._attr_native_unit_of_measurement = ( diff --git a/homeassistant/components/tuya/scene.py b/homeassistant/components/tuya/scene.py index 8db3ef60658..dcc1aae1fba 100644 --- a/homeassistant/components/tuya/scene.py +++ b/homeassistant/components/tuya/scene.py @@ -1,4 +1,5 @@ """Support for Tuya scenes.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/tuya/select.py b/homeassistant/components/tuya/select.py index 5d712767697..6e128bfdcc4 100644 --- a/homeassistant/components/tuya/select.py +++ b/homeassistant/components/tuya/select.py @@ -1,4 +1,5 @@ """Support for Tuya select.""" + from __future__ import annotations from tuya_sharing import CustomerDevice, Manager @@ -33,12 +34,10 @@ SELECTS: dict[str, tuple[SelectEntityDescription, ...]] = { SelectEntityDescription( key=DPCode.CUP_NUMBER, translation_key="cups", - icon="mdi:numeric", ), SelectEntityDescription( key=DPCode.CONCENTRATION_SET, translation_key="concentration", - icon="mdi:altimeter", entity_category=EntityCategory.CONFIG, ), SelectEntityDescription( @@ -49,7 +48,6 @@ SELECTS: dict[str, tuple[SelectEntityDescription, ...]] = { SelectEntityDescription( key=DPCode.MODE, translation_key="mode", - icon="mdi:coffee", ), ), # Switch @@ -72,7 +70,6 @@ SELECTS: dict[str, tuple[SelectEntityDescription, ...]] = { SelectEntityDescription( key=DPCode.LEVEL, translation_key="temperature_level", - icon="mdi:thermometer-lines", ), ), # Smart Water Timer @@ -81,7 +78,6 @@ SELECTS: dict[str, tuple[SelectEntityDescription, ...]] = { SelectEntityDescription( key=DPCode.WEATHER_DELAY, translation_key="weather_delay", - icon="mdi:weather-cloudy-clock", entity_category=EntityCategory.CONFIG, ), ), @@ -109,31 +105,26 @@ SELECTS: dict[str, tuple[SelectEntityDescription, ...]] = { ), SelectEntityDescription( key=DPCode.DECIBEL_SENSITIVITY, - icon="mdi:volume-vibrate", entity_category=EntityCategory.CONFIG, translation_key="decibel_sensitivity", ), SelectEntityDescription( key=DPCode.RECORD_MODE, - icon="mdi:record-rec", entity_category=EntityCategory.CONFIG, translation_key="record_mode", ), SelectEntityDescription( key=DPCode.BASIC_NIGHTVISION, - icon="mdi:theme-light-dark", entity_category=EntityCategory.CONFIG, translation_key="basic_nightvision", ), SelectEntityDescription( key=DPCode.BASIC_ANTI_FLICKER, - icon="mdi:image-outline", entity_category=EntityCategory.CONFIG, translation_key="basic_anti_flicker", ), SelectEntityDescription( key=DPCode.MOTION_SENSITIVITY, - icon="mdi:motion-sensor", entity_category=EntityCategory.CONFIG, translation_key="motion_sensitivity", ), @@ -209,19 +200,16 @@ SELECTS: dict[str, tuple[SelectEntityDescription, ...]] = { SelectEntityDescription( key=DPCode.CISTERN, entity_category=EntityCategory.CONFIG, - icon="mdi:water-opacity", translation_key="vacuum_cistern", ), SelectEntityDescription( key=DPCode.COLLECTION_MODE, entity_category=EntityCategory.CONFIG, - icon="mdi:air-filter", translation_key="vacuum_collection", ), SelectEntityDescription( key=DPCode.MODE, entity_category=EntityCategory.CONFIG, - icon="mdi:layers-outline", translation_key="vacuum_mode", ), ), @@ -231,25 +219,21 @@ SELECTS: dict[str, tuple[SelectEntityDescription, ...]] = { SelectEntityDescription( key=DPCode.FAN_VERTICAL, entity_category=EntityCategory.CONFIG, - icon="mdi:format-vertical-align-center", translation_key="vertical_fan_angle", ), SelectEntityDescription( key=DPCode.FAN_HORIZONTAL, entity_category=EntityCategory.CONFIG, - icon="mdi:format-horizontal-align-center", translation_key="horizontal_fan_angle", ), SelectEntityDescription( key=DPCode.COUNTDOWN, entity_category=EntityCategory.CONFIG, - icon="mdi:timer-cog-outline", translation_key="countdown", ), SelectEntityDescription( key=DPCode.COUNTDOWN_SET, entity_category=EntityCategory.CONFIG, - icon="mdi:timer-cog-outline", translation_key="countdown", ), ), @@ -259,7 +243,6 @@ SELECTS: dict[str, tuple[SelectEntityDescription, ...]] = { SelectEntityDescription( key=DPCode.CONTROL_BACK_MODE, entity_category=EntityCategory.CONFIG, - icon="mdi:swap-horizontal", translation_key="curtain_motor_mode", ), SelectEntityDescription( @@ -274,31 +257,26 @@ SELECTS: dict[str, tuple[SelectEntityDescription, ...]] = { SelectEntityDescription( key=DPCode.SPRAY_MODE, entity_category=EntityCategory.CONFIG, - icon="mdi:spray", translation_key="humidifier_spray_mode", ), SelectEntityDescription( key=DPCode.LEVEL, entity_category=EntityCategory.CONFIG, - icon="mdi:spray", translation_key="humidifier_level", ), SelectEntityDescription( key=DPCode.MOODLIGHTING, entity_category=EntityCategory.CONFIG, - icon="mdi:lightbulb-multiple", translation_key="humidifier_moodlighting", ), SelectEntityDescription( key=DPCode.COUNTDOWN, entity_category=EntityCategory.CONFIG, - icon="mdi:timer-cog-outline", translation_key="countdown", ), SelectEntityDescription( key=DPCode.COUNTDOWN_SET, entity_category=EntityCategory.CONFIG, - icon="mdi:timer-cog-outline", translation_key="countdown", ), ), @@ -308,13 +286,11 @@ SELECTS: dict[str, tuple[SelectEntityDescription, ...]] = { SelectEntityDescription( key=DPCode.COUNTDOWN, entity_category=EntityCategory.CONFIG, - icon="mdi:timer-cog-outline", translation_key="countdown", ), SelectEntityDescription( key=DPCode.COUNTDOWN_SET, entity_category=EntityCategory.CONFIG, - icon="mdi:timer-cog-outline", translation_key="countdown", ), ), @@ -324,14 +300,12 @@ SELECTS: dict[str, tuple[SelectEntityDescription, ...]] = { SelectEntityDescription( key=DPCode.COUNTDOWN_SET, entity_category=EntityCategory.CONFIG, - icon="mdi:timer-cog-outline", translation_key="countdown", ), SelectEntityDescription( key=DPCode.DEHUMIDITY_SET_ENUM, translation_key="target_humidity", entity_category=EntityCategory.CONFIG, - icon="mdi:water-percent", ), ), } @@ -358,11 +332,11 @@ async def async_setup_entry( for device_id in device_ids: device = hass_data.manager.device_map[device_id] if descriptions := SELECTS.get(device.category): - for description in descriptions: - if description.key in device.status: - entities.append( - TuyaSelectEntity(device, hass_data.manager, description) - ) + entities.extend( + TuyaSelectEntity(device, hass_data.manager, description) + for description in descriptions + if description.key in device.status + ) async_add_entities(entities) diff --git a/homeassistant/components/tuya/sensor.py b/homeassistant/components/tuya/sensor.py index 80c76a0c253..df11840931d 100644 --- a/homeassistant/components/tuya/sensor.py +++ b/homeassistant/components/tuya/sensor.py @@ -1,4 +1,5 @@ """Support for Tuya sensors.""" + from __future__ import annotations from dataclasses import dataclass @@ -58,7 +59,6 @@ BATTERY_SENSORS: tuple[TuyaSensorEntityDescription, ...] = ( TuyaSensorEntityDescription( key=DPCode.BATTERY_STATE, translation_key="battery_state", - icon="mdi:battery", entity_category=EntityCategory.DIAGNOSTIC, ), TuyaSensorEntityDescription( @@ -88,7 +88,6 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { TuyaSensorEntityDescription( key=DPCode.GAS_SENSOR_VALUE, translation_key="gas", - icon="mdi:gas-cylinder", state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( @@ -112,14 +111,12 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { TuyaSensorEntityDescription( key=DPCode.CO_VALUE, translation_key="carbon_monoxide", - icon="mdi:molecule-co", device_class=SensorDeviceClass.CO, state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.CO2_VALUE, translation_key="carbon_dioxide", - icon="mdi:molecule-co2", device_class=SensorDeviceClass.CO2, state_class=SensorStateClass.MEASUREMENT, ), @@ -131,12 +128,10 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { TuyaSensorEntityDescription( key=DPCode.BRIGHT_STATE, translation_key="luminosity", - icon="mdi:brightness-6", ), TuyaSensorEntityDescription( key=DPCode.BRIGHT_VALUE, translation_key="illuminance", - icon="mdi:brightness-6", device_class=SensorDeviceClass.ILLUMINANCE, state_class=SensorStateClass.MEASUREMENT, ), @@ -155,7 +150,6 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { TuyaSensorEntityDescription( key=DPCode.SMOKE_SENSOR_VALUE, translation_key="smoke_amount", - icon="mdi:smoke-detector", entity_category=EntityCategory.DIAGNOSTIC, state_class=SensorStateClass.MEASUREMENT, ), @@ -238,7 +232,6 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { TuyaSensorEntityDescription( key=DPCode.FEED_REPORT, translation_key="last_amount", - icon="mdi:counter", state_class=SensorStateClass.MEASUREMENT, ), ), @@ -387,7 +380,6 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { TuyaSensorEntityDescription( key=DPCode.BRIGHT_STATE, translation_key="luminosity", - icon="mdi:brightness-6", ), TuyaSensorEntityDescription( key=DPCode.BRIGHT_VALUE, @@ -438,7 +430,6 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { key=DPCode.REMAIN_TIME, translation_key="remaining_time", native_unit_of_measurement=UnitOfTime.MINUTES, - icon="mdi:timer", ), ), # PIR Detector @@ -512,7 +503,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { TuyaSensorEntityDescription( key=DPCode.GAS_SENSOR_VALUE, name=None, - icon="mdi:gas-cylinder", + translation_key="gas", state_class=SensorStateClass.MEASUREMENT, ), *BATTERY_SENSORS, @@ -523,7 +514,6 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { TuyaSensorEntityDescription( key=DPCode.TIME_USE, translation_key="total_watering_time", - icon="mdi:history", state_class=SensorStateClass.TOTAL_INCREASING, entity_category=EntityCategory.DIAGNOSTIC, ), @@ -658,7 +648,6 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { TuyaSensorEntityDescription( key=DPCode.SMOKE_SENSOR_VALUE, translation_key="smoke_amount", - icon="mdi:smoke-detector", entity_category=EntityCategory.DIAGNOSTIC, state_class=SensorStateClass.MEASUREMENT, ), @@ -858,55 +847,46 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { TuyaSensorEntityDescription( key=DPCode.CLEAN_AREA, translation_key="cleaning_area", - icon="mdi:texture-box", state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.CLEAN_TIME, translation_key="cleaning_time", - icon="mdi:progress-clock", state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.TOTAL_CLEAN_AREA, translation_key="total_cleaning_area", - icon="mdi:texture-box", state_class=SensorStateClass.TOTAL_INCREASING, ), TuyaSensorEntityDescription( key=DPCode.TOTAL_CLEAN_TIME, translation_key="total_cleaning_time", - icon="mdi:history", state_class=SensorStateClass.TOTAL_INCREASING, ), TuyaSensorEntityDescription( key=DPCode.TOTAL_CLEAN_COUNT, translation_key="total_cleaning_times", - icon="mdi:counter", state_class=SensorStateClass.TOTAL_INCREASING, ), TuyaSensorEntityDescription( key=DPCode.DUSTER_CLOTH, translation_key="duster_cloth_life", - icon="mdi:ticket-percent-outline", state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.EDGE_BRUSH, translation_key="side_brush_life", - icon="mdi:ticket-percent-outline", state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.FILTER_LIFE, translation_key="filter_life", - icon="mdi:ticket-percent-outline", state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.ROLL_BRUSH, translation_key="rolling_brush_life", - icon="mdi:ticket-percent-outline", state_class=SensorStateClass.MEASUREMENT, ), ), @@ -917,7 +897,6 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { key=DPCode.TIME_TOTAL, translation_key="last_operation_duration", entity_category=EntityCategory.DIAGNOSTIC, - icon="mdi:progress-clock", ), ), # Humidifier @@ -945,7 +924,6 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { key=DPCode.LEVEL_CURRENT, translation_key="water_level", entity_category=EntityCategory.DIAGNOSTIC, - icon="mdi:waves-arrow-up", ), ), # Air Purifier @@ -955,14 +933,12 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { key=DPCode.FILTER, translation_key="filter_utilization", entity_category=EntityCategory.DIAGNOSTIC, - icon="mdi:ticket-percent-outline", ), TuyaSensorEntityDescription( key=DPCode.PM25, translation_key="pm25", device_class=SensorDeviceClass.PM25, state_class=SensorStateClass.MEASUREMENT, - icon="mdi:molecule", ), TuyaSensorEntityDescription( key=DPCode.TEMP, @@ -991,21 +967,18 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { TuyaSensorEntityDescription( key=DPCode.TOTAL_TIME, translation_key="total_operating_time", - icon="mdi:history", state_class=SensorStateClass.TOTAL_INCREASING, entity_category=EntityCategory.DIAGNOSTIC, ), TuyaSensorEntityDescription( key=DPCode.TOTAL_PM, translation_key="total_absorption_particles", - icon="mdi:texture-box", state_class=SensorStateClass.TOTAL_INCREASING, entity_category=EntityCategory.DIAGNOSTIC, ), TuyaSensorEntityDescription( key=DPCode.AIR_QUALITY, translation_key="air_quality", - icon="mdi:air-filter", ), ), # Fan @@ -1114,11 +1087,11 @@ async def async_setup_entry( for device_id in device_ids: device = hass_data.manager.device_map[device_id] if descriptions := SENSORS.get(device.category): - for description in descriptions: - if description.key in device.status: - entities.append( - TuyaSensorEntity(device, hass_data.manager, description) - ) + entities.extend( + TuyaSensorEntity(device, hass_data.manager, description) + for description in descriptions + if description.key in device.status + ) async_add_entities(entities) @@ -1191,10 +1164,6 @@ class TuyaSensorEntity(TuyaEntity, SensorEntity): self._attr_device_class = None return - # If we still have a device class, we should not use an icon - if self.device_class: - self._attr_icon = None - # Found unit of measurement, use the standardized Unit # Use the target conversion unit (if set) self._attr_native_unit_of_measurement = ( diff --git a/homeassistant/components/tuya/siren.py b/homeassistant/components/tuya/siren.py index baba339318d..04473e44e22 100644 --- a/homeassistant/components/tuya/siren.py +++ b/homeassistant/components/tuya/siren.py @@ -1,4 +1,5 @@ """Support for Tuya siren.""" + from __future__ import annotations from typing import Any @@ -59,11 +60,11 @@ async def async_setup_entry( for device_id in device_ids: device = hass_data.manager.device_map[device_id] if descriptions := SIRENS.get(device.category): - for description in descriptions: - if description.key in device.status: - entities.append( - TuyaSirenEntity(device, hass_data.manager, description) - ) + entities.extend( + TuyaSirenEntity(device, hass_data.manager, description) + for description in descriptions + if description.key in device.status + ) async_add_entities(entities) diff --git a/homeassistant/components/tuya/switch.py b/homeassistant/components/tuya/switch.py index a89dbbd7132..36debaeadde 100644 --- a/homeassistant/components/tuya/switch.py +++ b/homeassistant/components/tuya/switch.py @@ -1,4 +1,5 @@ """Support for Tuya switches.""" + from __future__ import annotations from typing import Any @@ -30,7 +31,6 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { SwitchEntityDescription( key=DPCode.START, translation_key="start", - icon="mdi:kettle-steam", ), SwitchEntityDescription( key=DPCode.WARM, @@ -44,12 +44,10 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { SwitchEntityDescription( key=DPCode.DISINFECTION, translation_key="disinfection", - icon="mdi:bacteria", ), SwitchEntityDescription( key=DPCode.WATER, translation_key="water", - icon="mdi:water", ), ), # Smart Pet Feeder @@ -58,7 +56,6 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { SwitchEntityDescription( key=DPCode.SLOW_FEED, translation_key="slow_feed", - icon="mdi:speedometer-slow", entity_category=EntityCategory.CONFIG, ), ), @@ -68,13 +65,11 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { SwitchEntityDescription( key=DPCode.FILTER_RESET, translation_key="filter_reset", - icon="mdi:filter", entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.PUMP_RESET, translation_key="water_pump_reset", - icon="mdi:pump", entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( @@ -84,13 +79,11 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { SwitchEntityDescription( key=DPCode.WATER_RESET, translation_key="reset_of_water_usage_days", - icon="mdi:water-sync", entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.UV, translation_key="uv_sterilization", - icon="mdi:lightbulb", entity_category=EntityCategory.CONFIG, ), ), @@ -110,7 +103,6 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { SwitchEntityDescription( key=DPCode.CHILD_LOCK, translation_key="child_lock", - icon="mdi:account-lock", entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( @@ -124,36 +116,30 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { SwitchEntityDescription( key=DPCode.SWITCH_1, translation_key="radio", - icon="mdi:radio", ), SwitchEntityDescription( key=DPCode.SWITCH_2, translation_key="alarm_1", - icon="mdi:alarm", entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.SWITCH_3, translation_key="alarm_2", - icon="mdi:alarm", entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.SWITCH_4, translation_key="alarm_3", - icon="mdi:alarm", entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.SWITCH_5, translation_key="alarm_4", - icon="mdi:alarm", entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.SWITCH_6, translation_key="sleep_aid", - icon="mdi:power-sleep", ), ), # Two-way temperature and humidity switch @@ -177,7 +163,6 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { SwitchEntityDescription( key=DPCode.CHILD_LOCK, translation_key="child_lock", - icon="mdi:account-lock", entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( @@ -256,19 +241,16 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { SwitchEntityDescription( key=DPCode.ANION, translation_key="ionizer", - icon="mdi:minus-circle-outline", entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.FILTER_RESET, translation_key="filter_cartridge_reset", - icon="mdi:filter", entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.LOCK, translation_key="child_lock", - icon="mdi:account-lock", entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( @@ -278,13 +260,11 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { SwitchEntityDescription( key=DPCode.WET, translation_key="humidification", - icon="mdi:water-percent", entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.UV, translation_key="uv_sterilization", - icon="mdi:minus-circle-outline", entity_category=EntityCategory.CONFIG, ), ), @@ -294,13 +274,11 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { SwitchEntityDescription( key=DPCode.ANION, translation_key="ionizer", - icon="mdi:minus-circle-outline", entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.LOCK, translation_key="child_lock", - icon="mdi:account-lock", entity_category=EntityCategory.CONFIG, ), ), @@ -310,13 +288,11 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { SwitchEntityDescription( key=DPCode.SWITCH, translation_key="switch", - icon="mdi:power", entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.START, translation_key="start", - icon="mdi:pot-steam", entity_category=EntityCategory.CONFIG, ), ), @@ -326,7 +302,6 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { SwitchEntityDescription( key=DPCode.CHILD_LOCK, translation_key="child_lock", - icon="mdi:account-lock", entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( @@ -404,13 +379,11 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { SwitchEntityDescription( key=DPCode.ANION, translation_key="ionizer", - icon="mdi:minus-circle-outline", entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.LOCK, translation_key="child_lock", - icon="mdi:account-lock", entity_category=EntityCategory.CONFIG, ), ), @@ -420,13 +393,11 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { SwitchEntityDescription( key=DPCode.SWITCH_DISTURB, translation_key="do_not_disturb", - icon="mdi:minus-circle", entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.VOICE_SWITCH, translation_key="mute_voice", - icon="mdi:account-voice", entity_category=EntityCategory.CONFIG, ), ), @@ -435,7 +406,6 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { SwitchEntityDescription( key=DPCode.SWITCH, translation_key="switch", - icon="mdi:sprinkler-variant", ), ), # Siren Alarm @@ -453,67 +423,56 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { SwitchEntityDescription( key=DPCode.WIRELESS_BATTERYLOCK, translation_key="battery_lock", - icon="mdi:battery-lock", entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.CRY_DETECTION_SWITCH, translation_key="cry_detection", - icon="mdi:emoticon-cry", entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.DECIBEL_SWITCH, translation_key="sound_detection", - icon="mdi:microphone-outline", entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.RECORD_SWITCH, translation_key="video_recording", - icon="mdi:record-rec", entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.MOTION_RECORD, translation_key="motion_recording", - icon="mdi:record-rec", entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.BASIC_PRIVATE, translation_key="privacy_mode", - icon="mdi:eye-off", entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.BASIC_FLIP, translation_key="flip", - icon="mdi:flip-horizontal", entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.BASIC_OSD, translation_key="time_watermark", - icon="mdi:watermark", entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.BASIC_WDR, translation_key="wide_dynamic_range", - icon="mdi:watermark", entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.MOTION_TRACKING, translation_key="motion_tracking", - icon="mdi:motion-sensor", entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.MOTION_SWITCH, translation_key="motion_alarm", - icon="mdi:motion-sensor", entity_category=EntityCategory.CONFIG, ), ), @@ -522,7 +481,6 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { SwitchEntityDescription( key=DPCode.SWITCH, translation_key="switch", - icon="mdi:cursor-pointer", ), ), # IoT Switch? @@ -551,7 +509,6 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { SwitchEntityDescription( key=DPCode.CHILD_LOCK, translation_key="child_lock", - icon="mdi:account-lock", entity_category=EntityCategory.CONFIG, ), ), @@ -561,7 +518,6 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { SwitchEntityDescription( key=DPCode.SWITCH_SAVE_ENERGY, translation_key="energy_saving", - icon="mdi:leaf", entity_category=EntityCategory.CONFIG, ), ), @@ -571,13 +527,11 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { SwitchEntityDescription( key=DPCode.CHILD_LOCK, translation_key="child_lock", - icon="mdi:account-lock", entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.WINDOW_CHECK, translation_key="open_window_detection", - icon="mdi:window-open", entity_category=EntityCategory.CONFIG, ), ), @@ -603,7 +557,6 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { SwitchEntityDescription( key=DPCode.DO_NOT_DISTURB, translation_key="do_not_disturb", - icon="mdi:minus-circle-outline", entity_category=EntityCategory.CONFIG, ), ), @@ -617,12 +570,10 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { SwitchEntityDescription( key=DPCode.SWITCH_SPRAY, translation_key="spray", - icon="mdi:spray", ), SwitchEntityDescription( key=DPCode.SWITCH_VOICE, translation_key="voice", - icon="mdi:account-voice", entity_category=EntityCategory.CONFIG, ), ), @@ -640,37 +591,31 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { SwitchEntityDescription( key=DPCode.ANION, translation_key="anion", - icon="mdi:atom", entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.HUMIDIFIER, translation_key="humidification", - icon="mdi:air-humidifier", entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.OXYGEN, translation_key="oxygen_bar", - icon="mdi:molecule", entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.FAN_COOL, translation_key="natural_wind", - icon="mdi:weather-windy", entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.FAN_BEEP, translation_key="sound", - icon="mdi:minus-circle", entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.CHILD_LOCK, translation_key="child_lock", - icon="mdi:account-lock", entity_category=EntityCategory.CONFIG, ), ), @@ -680,13 +625,11 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { SwitchEntityDescription( key=DPCode.CONTROL_BACK, translation_key="reverse", - icon="mdi:swap-horizontal", entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.OPPOSITE, translation_key="reverse", - icon="mdi:swap-horizontal", entity_category=EntityCategory.CONFIG, ), ), @@ -696,19 +639,16 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { SwitchEntityDescription( key=DPCode.SWITCH_SOUND, translation_key="voice", - icon="mdi:account-voice", entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.SLEEP, translation_key="sleep", - icon="mdi:power-sleep", entity_category=EntityCategory.CONFIG, ), SwitchEntityDescription( key=DPCode.STERILIZATION, translation_key="sterilization", - icon="mdi:minus-circle-outline", entity_category=EntityCategory.CONFIG, ), ), @@ -732,11 +672,11 @@ async def async_setup_entry( for device_id in device_ids: device = hass_data.manager.device_map[device_id] if descriptions := SWITCHES.get(device.category): - for description in descriptions: - if description.key in device.status: - entities.append( - TuyaSwitchEntity(device, hass_data.manager, description) - ) + entities.extend( + TuyaSwitchEntity(device, hass_data.manager, description) + for description in descriptions + if description.key in device.status + ) async_add_entities(entities) diff --git a/homeassistant/components/tuya/util.py b/homeassistant/components/tuya/util.py index 3b29a3e13cf..b6e6f17f49b 100644 --- a/homeassistant/components/tuya/util.py +++ b/homeassistant/components/tuya/util.py @@ -1,4 +1,5 @@ """Utility methods for the Tuya integration.""" + from __future__ import annotations diff --git a/homeassistant/components/tuya/vacuum.py b/homeassistant/components/tuya/vacuum.py index 14ae9c4c426..6774aaac8a1 100644 --- a/homeassistant/components/tuya/vacuum.py +++ b/homeassistant/components/tuya/vacuum.py @@ -1,4 +1,5 @@ """Support for Tuya Vacuums.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/twentemilieu/__init__.py b/homeassistant/components/twentemilieu/__init__.py index c4fe53c67f0..d9881b0b2c8 100644 --- a/homeassistant/components/twentemilieu/__init__.py +++ b/homeassistant/components/twentemilieu/__init__.py @@ -1,4 +1,5 @@ """Support for Twente Milieu.""" + from __future__ import annotations from datetime import date, timedelta @@ -33,14 +34,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: session=session, ) - coordinator: DataUpdateCoordinator[ - dict[WasteType, list[date]] - ] = DataUpdateCoordinator( - hass, - LOGGER, - name=DOMAIN, - update_interval=SCAN_INTERVAL, - update_method=twentemilieu.update, + coordinator: DataUpdateCoordinator[dict[WasteType, list[date]]] = ( + DataUpdateCoordinator( + hass, + LOGGER, + name=DOMAIN, + update_interval=SCAN_INTERVAL, + update_method=twentemilieu.update, + ) ) await coordinator.async_config_entry_first_refresh() diff --git a/homeassistant/components/twentemilieu/calendar.py b/homeassistant/components/twentemilieu/calendar.py index f4d1e51b171..8bd008e3eb3 100644 --- a/homeassistant/components/twentemilieu/calendar.py +++ b/homeassistant/components/twentemilieu/calendar.py @@ -1,4 +1,5 @@ """Support for Twente Milieu Calendar.""" + from __future__ import annotations from datetime import date, datetime, timedelta @@ -31,8 +32,8 @@ class TwenteMilieuCalendar(TwenteMilieuEntity, CalendarEntity): """Defines a Twente Milieu calendar.""" _attr_has_entity_name = True - _attr_icon = "mdi:delete-empty" _attr_name = None + _attr_translation_key = "calendar" def __init__( self, diff --git a/homeassistant/components/twentemilieu/config_flow.py b/homeassistant/components/twentemilieu/config_flow.py index 7a00222fe9b..e87dde3a699 100644 --- a/homeassistant/components/twentemilieu/config_flow.py +++ b/homeassistant/components/twentemilieu/config_flow.py @@ -1,4 +1,5 @@ """Config flow to configure the Twente Milieu integration.""" + from __future__ import annotations from typing import Any @@ -10,9 +11,8 @@ from twentemilieu import ( ) import voluptuous as vol -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_ID -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import CONF_HOUSE_LETTER, CONF_HOUSE_NUMBER, CONF_POST_CODE, DOMAIN @@ -25,7 +25,7 @@ class TwenteMilieuFlowHandler(ConfigFlow, domain=DOMAIN): async def _show_setup_form( self, errors: dict[str, str] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Show the setup form to the user.""" return self.async_show_form( step_id="user", @@ -41,7 +41,7 @@ class TwenteMilieuFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initiated by the user.""" if user_input is None: return await self._show_setup_form(user_input) diff --git a/homeassistant/components/twentemilieu/const.py b/homeassistant/components/twentemilieu/const.py index ac7354a42f2..e5415e09b81 100644 --- a/homeassistant/components/twentemilieu/const.py +++ b/homeassistant/components/twentemilieu/const.py @@ -1,4 +1,5 @@ """Constants for the Twente Milieu integration.""" + from datetime import timedelta import logging from typing import Final diff --git a/homeassistant/components/twentemilieu/diagnostics.py b/homeassistant/components/twentemilieu/diagnostics.py index acc3f802796..ea68473ae3b 100644 --- a/homeassistant/components/twentemilieu/diagnostics.py +++ b/homeassistant/components/twentemilieu/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for TwenteMilieu.""" + from __future__ import annotations from datetime import date diff --git a/homeassistant/components/twentemilieu/entity.py b/homeassistant/components/twentemilieu/entity.py index 5c1d71fa03b..1e0fa651998 100644 --- a/homeassistant/components/twentemilieu/entity.py +++ b/homeassistant/components/twentemilieu/entity.py @@ -1,4 +1,5 @@ """Base entity for the Twente Milieu integration.""" + from __future__ import annotations from datetime import date diff --git a/homeassistant/components/twentemilieu/icons.json b/homeassistant/components/twentemilieu/icons.json new file mode 100644 index 00000000000..b033178fbb8 --- /dev/null +++ b/homeassistant/components/twentemilieu/icons.json @@ -0,0 +1,26 @@ +{ + "entity": { + "calendar": { + "calendar": { + "default": "mdi:delete-empty" + } + }, + "sensor": { + "christmas_tree_pickup": { + "default": "mdi:pine-tree" + }, + "non_recyclable_waste_pickup": { + "default": "mdi:delete-empty" + }, + "organic_waste_pickup": { + "default": "mdi:delete-empty" + }, + "paper_waste_pickup": { + "default": "mdi:delete-empty" + }, + "packages_waste_pickup": { + "default": "mdi:delete-empty" + } + } + } +} diff --git a/homeassistant/components/twentemilieu/sensor.py b/homeassistant/components/twentemilieu/sensor.py index 32b4de47de4..f799fa62314 100644 --- a/homeassistant/components/twentemilieu/sensor.py +++ b/homeassistant/components/twentemilieu/sensor.py @@ -1,4 +1,5 @@ """Support for Twente Milieu sensors.""" + from __future__ import annotations from dataclasses import dataclass @@ -33,35 +34,30 @@ SENSORS: tuple[TwenteMilieuSensorDescription, ...] = ( key="tree", translation_key="christmas_tree_pickup", waste_type=WasteType.TREE, - icon="mdi:pine-tree", device_class=SensorDeviceClass.DATE, ), TwenteMilieuSensorDescription( key="Non-recyclable", translation_key="non_recyclable_waste_pickup", waste_type=WasteType.NON_RECYCLABLE, - icon="mdi:delete-empty", device_class=SensorDeviceClass.DATE, ), TwenteMilieuSensorDescription( key="Organic", translation_key="organic_waste_pickup", waste_type=WasteType.ORGANIC, - icon="mdi:delete-empty", device_class=SensorDeviceClass.DATE, ), TwenteMilieuSensorDescription( key="Paper", translation_key="paper_waste_pickup", waste_type=WasteType.PAPER, - icon="mdi:delete-empty", device_class=SensorDeviceClass.DATE, ), TwenteMilieuSensorDescription( key="Plastic", translation_key="packages_waste_pickup", waste_type=WasteType.PACKAGES, - icon="mdi:delete-empty", device_class=SensorDeviceClass.DATE, ), ) diff --git a/homeassistant/components/twilio/__init__.py b/homeassistant/components/twilio/__init__.py index e71f3181b55..72e69912774 100644 --- a/homeassistant/components/twilio/__init__.py +++ b/homeassistant/components/twilio/__init__.py @@ -1,4 +1,5 @@ """Support for Twilio.""" + from aiohttp import web from twilio.rest import Client import voluptuous as vol diff --git a/homeassistant/components/twilio/config_flow.py b/homeassistant/components/twilio/config_flow.py index 1539c1ffadc..14dd336f0d7 100644 --- a/homeassistant/components/twilio/config_flow.py +++ b/homeassistant/components/twilio/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Twilio.""" + from homeassistant.helpers import config_entry_flow from .const import DOMAIN diff --git a/homeassistant/components/twilio_call/notify.py b/homeassistant/components/twilio_call/notify.py index 44eaa0bf994..d3d128ccd25 100644 --- a/homeassistant/components/twilio_call/notify.py +++ b/homeassistant/components/twilio_call/notify.py @@ -1,4 +1,5 @@ """Twilio Call platform for notify component.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/twilio_sms/notify.py b/homeassistant/components/twilio_sms/notify.py index 0b0c724e479..2c04594f314 100644 --- a/homeassistant/components/twilio_sms/notify.py +++ b/homeassistant/components/twilio_sms/notify.py @@ -1,4 +1,5 @@ """Twilio SMS platform for notify component.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/twinkly/__init__.py b/homeassistant/components/twinkly/__init__.py index 3b47a10d499..b09e58ff12f 100644 --- a/homeassistant/components/twinkly/__init__.py +++ b/homeassistant/components/twinkly/__init__.py @@ -1,6 +1,5 @@ """The twinkly component.""" - from aiohttp import ClientError from ttls.client import Twinkly diff --git a/homeassistant/components/twinkly/config_flow.py b/homeassistant/components/twinkly/config_flow.py index 6d0785f648e..98802c8bd33 100644 --- a/homeassistant/components/twinkly/config_flow.py +++ b/homeassistant/components/twinkly/config_flow.py @@ -1,4 +1,5 @@ """Config flow to configure the Twinkly integration.""" + from __future__ import annotations import logging @@ -8,8 +9,8 @@ from aiohttp import ClientError from ttls.client import Twinkly from voluptuous import Required, Schema -from homeassistant import config_entries, data_entry_flow from homeassistant.components import dhcp +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_ID, CONF_MODEL, CONF_NAME from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -18,7 +19,7 @@ from .const import DEV_ID, DEV_MODEL, DEV_NAME, DOMAIN _LOGGER = logging.getLogger(__name__) -class TwinklyConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class TwinklyConfigFlow(ConfigFlow, domain=DOMAIN): """Handle twinkly config flow.""" VERSION = 1 @@ -53,7 +54,7 @@ class TwinklyConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_dhcp( self, discovery_info: dhcp.DhcpServiceInfo - ) -> data_entry_flow.FlowResult: + ) -> ConfigFlowResult: """Handle dhcp discovery for twinkly.""" self._async_abort_entries_match({CONF_HOST: discovery_info.ip}) device_info = await Twinkly( @@ -65,9 +66,7 @@ class TwinklyConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._discovered_device = (device_info, discovery_info.ip) return await self.async_step_discovery_confirm() - async def async_step_discovery_confirm( - self, user_input=None - ) -> data_entry_flow.FlowResult: + async def async_step_discovery_confirm(self, user_input=None) -> ConfigFlowResult: """Confirm discovery.""" assert self._discovered_device is not None device_info, host = self._discovered_device @@ -87,7 +86,7 @@ class TwinklyConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def _create_entry_from_device( self, device_info: dict[str, Any], host: str - ) -> data_entry_flow.FlowResult: + ) -> ConfigFlowResult: """Create entry from device data.""" return self.async_create_entry( title=device_info[DEV_NAME], diff --git a/homeassistant/components/twinkly/diagnostics.py b/homeassistant/components/twinkly/diagnostics.py index 598eab0fca5..e188e92ecd5 100644 --- a/homeassistant/components/twinkly/diagnostics.py +++ b/homeassistant/components/twinkly/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for Twinkly.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/twinkly/icons.json b/homeassistant/components/twinkly/icons.json new file mode 100644 index 00000000000..82c95aebce6 --- /dev/null +++ b/homeassistant/components/twinkly/icons.json @@ -0,0 +1,9 @@ +{ + "entity": { + "light": { + "light": { + "default": "mdi:string-lights" + } + } + } +} diff --git a/homeassistant/components/twinkly/light.py b/homeassistant/components/twinkly/light.py index 453ba900706..2749c9a7764 100644 --- a/homeassistant/components/twinkly/light.py +++ b/homeassistant/components/twinkly/light.py @@ -1,4 +1,5 @@ """The Twinkly light component.""" + from __future__ import annotations import logging @@ -66,7 +67,7 @@ class TwinklyLight(LightEntity): _attr_has_entity_name = True _attr_name = None - _attr_icon = "mdi:string-lights" + _attr_translation_key = "light" def __init__( self, @@ -129,10 +130,7 @@ class TwinklyLight(LightEntity): @property def effect_list(self) -> list[str]: """Return the list of saved effects.""" - effect_list = [] - for movie in self._movies: - effect_list.append(f"{movie['id']} {movie['name']}") - return effect_list + return [f"{movie['id']} {movie['name']}" for movie in self._movies] async def async_added_to_hass(self) -> None: """Device is added to hass.""" diff --git a/homeassistant/components/twitch/__init__.py b/homeassistant/components/twitch/__init__.py index fdc1a74d2d2..60c9dcabb36 100644 --- a/homeassistant/components/twitch/__init__.py +++ b/homeassistant/components/twitch/__init__.py @@ -1,4 +1,5 @@ """The Twitch component.""" + from __future__ import annotations from typing import cast diff --git a/homeassistant/components/twitch/config_flow.py b/homeassistant/components/twitch/config_flow.py index 128abf756fa..f9e121f3a17 100644 --- a/homeassistant/components/twitch/config_flow.py +++ b/homeassistant/components/twitch/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Twitch.""" + from __future__ import annotations from collections.abc import Mapping @@ -9,10 +10,10 @@ from twitchAPI.helper import first from twitchAPI.twitch import Twitch from twitchAPI.type import AuthScope, InvalidTokenException -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import ConfigEntry, ConfigFlowResult from homeassistant.const import CONF_ACCESS_TOKEN, CONF_CLIENT_ID, CONF_TOKEN from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN -from homeassistant.data_entry_flow import AbortFlow, FlowResult +from homeassistant.data_entry_flow import AbortFlow from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.helpers.config_entry_oauth2_flow import LocalOAuth2Implementation from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue @@ -46,7 +47,7 @@ class OAuth2FlowHandler( async def async_oauth_create_entry( self, data: dict[str, Any], - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" implementation = cast( LocalOAuth2Implementation, @@ -104,7 +105,9 @@ class OAuth2FlowHandler( description_placeholders={"title": self.reauth_entry.title}, ) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Perform reauth upon an API authentication error.""" self.reauth_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] @@ -113,13 +116,13 @@ class OAuth2FlowHandler( async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm reauth dialog.""" if user_input is None: return self.async_show_form(step_id="reauth_confirm") return await self.async_step_user() - async def async_step_import(self, config: dict[str, Any]) -> FlowResult: + async def async_step_import(self, config: dict[str, Any]) -> ConfigFlowResult: """Import from yaml.""" client = await Twitch( app_id=config[CONF_CLIENT_ID], diff --git a/homeassistant/components/twitch/const.py b/homeassistant/components/twitch/const.py index e99e76a94e9..b46bf8113b4 100644 --- a/homeassistant/components/twitch/const.py +++ b/homeassistant/components/twitch/const.py @@ -1,4 +1,5 @@ """Const for Twitch.""" + import logging from twitchAPI.twitch import AuthScope diff --git a/homeassistant/components/twitch/icons.json b/homeassistant/components/twitch/icons.json new file mode 100644 index 00000000000..54b07caf5f8 --- /dev/null +++ b/homeassistant/components/twitch/icons.json @@ -0,0 +1,9 @@ +{ + "entity": { + "sensor": { + "channel": { + "default": "mdi:twitch" + } + } + } +} diff --git a/homeassistant/components/twitch/sensor.py b/homeassistant/components/twitch/sensor.py index 2d2a79a6244..1107513080a 100644 --- a/homeassistant/components/twitch/sensor.py +++ b/homeassistant/components/twitch/sensor.py @@ -1,4 +1,5 @@ """Support for the Twitch stream status.""" + from __future__ import annotations from twitchAPI.helper import first @@ -47,8 +48,6 @@ ATTR_FOLLOW_SINCE = "following_since" ATTR_FOLLOWING = "followers" ATTR_VIEWS = "views" -ICON = "mdi:twitch" - STATE_OFFLINE = "offline" STATE_STREAMING = "streaming" @@ -122,7 +121,7 @@ async def async_setup_entry( class TwitchSensor(SensorEntity): """Representation of a Twitch channel.""" - _attr_icon = ICON + _attr_translation_key = "channel" def __init__( self, channel: TwitchUser, session: OAuth2Session, client: Twitch diff --git a/homeassistant/components/twitter/notify.py b/homeassistant/components/twitter/notify.py index c6d223d2413..718f4f7dbcf 100644 --- a/homeassistant/components/twitter/notify.py +++ b/homeassistant/components/twitter/notify.py @@ -1,4 +1,5 @@ """Twitter platform for notify component.""" + from __future__ import annotations from datetime import datetime, timedelta diff --git a/homeassistant/components/ubus/device_tracker.py b/homeassistant/components/ubus/device_tracker.py index f49a06be9dd..0aebea84c7d 100644 --- a/homeassistant/components/ubus/device_tracker.py +++ b/homeassistant/components/ubus/device_tracker.py @@ -1,4 +1,5 @@ """Support for OpenWRT (ubus) routers.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/ue_smart_radio/media_player.py b/homeassistant/components/ue_smart_radio/media_player.py index 7fc727cf9fe..90afca69816 100644 --- a/homeassistant/components/ue_smart_radio/media_player.py +++ b/homeassistant/components/ue_smart_radio/media_player.py @@ -1,4 +1,5 @@ """Support for Logitech UE Smart Radios.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/uk_transport/sensor.py b/homeassistant/components/uk_transport/sensor.py index 3412a004167..24a88724add 100644 --- a/homeassistant/components/uk_transport/sensor.py +++ b/homeassistant/components/uk_transport/sensor.py @@ -1,4 +1,5 @@ """Support for UK public transport data provided by transportapi.com.""" + from __future__ import annotations from datetime import datetime, timedelta diff --git a/homeassistant/components/ukraine_alarm/__init__.py b/homeassistant/components/ukraine_alarm/__init__.py index 1132bd56b72..b90fb20af75 100644 --- a/homeassistant/components/ukraine_alarm/__init__.py +++ b/homeassistant/components/ukraine_alarm/__init__.py @@ -1,4 +1,5 @@ """The ukraine_alarm component.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/ukraine_alarm/binary_sensor.py b/homeassistant/components/ukraine_alarm/binary_sensor.py index f5479917064..0eb8bd7b43c 100644 --- a/homeassistant/components/ukraine_alarm/binary_sensor.py +++ b/homeassistant/components/ukraine_alarm/binary_sensor.py @@ -1,4 +1,5 @@ """binary sensors for Ukraine Alarm integration.""" + from __future__ import annotations from homeassistant.components.binary_sensor import ( @@ -36,31 +37,26 @@ BINARY_SENSOR_TYPES: tuple[BinarySensorEntityDescription, ...] = ( key=ALERT_TYPE_AIR, translation_key="air", device_class=BinarySensorDeviceClass.SAFETY, - icon="mdi:cloud", ), BinarySensorEntityDescription( key=ALERT_TYPE_URBAN_FIGHTS, translation_key="urban_fights", device_class=BinarySensorDeviceClass.SAFETY, - icon="mdi:pistol", ), BinarySensorEntityDescription( key=ALERT_TYPE_ARTILLERY, translation_key="artillery", device_class=BinarySensorDeviceClass.SAFETY, - icon="mdi:tank", ), BinarySensorEntityDescription( key=ALERT_TYPE_CHEMICAL, translation_key="chemical", device_class=BinarySensorDeviceClass.SAFETY, - icon="mdi:chemical-weapon", ), BinarySensorEntityDescription( key=ALERT_TYPE_NUCLEAR, translation_key="nuclear", device_class=BinarySensorDeviceClass.SAFETY, - icon="mdi:nuke", ), ) diff --git a/homeassistant/components/ukraine_alarm/config_flow.py b/homeassistant/components/ukraine_alarm/config_flow.py index db17b55b2e9..bafe6d1fe11 100644 --- a/homeassistant/components/ukraine_alarm/config_flow.py +++ b/homeassistant/components/ukraine_alarm/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Ukraine Alarm.""" + from __future__ import annotations import logging @@ -7,7 +8,7 @@ import aiohttp from uasiren.client import Client import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_NAME, CONF_REGION from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -16,7 +17,7 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -class UkraineAlarmConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class UkraineAlarmConfigFlow(ConfigFlow, domain=DOMAIN): """Config flow for Ukraine Alarm.""" VERSION = 1 @@ -132,17 +133,5 @@ def _find(regions, region_id): def _make_regions_object(regions): - regions_list = [] - for region in regions: - regions_list.append( - { - "id": region["regionId"], - "name": region["regionName"], - } - ) - regions_list = sorted(regions_list, key=lambda region: region["name"].lower()) - regions_object = {} - for region in regions_list: - regions_object[region["id"]] = region["name"] - - return regions_object + regions = sorted(regions, key=lambda region: region["regionName"].lower()) + return {region["regionId"]: region["regionName"] for region in regions} diff --git a/homeassistant/components/ukraine_alarm/const.py b/homeassistant/components/ukraine_alarm/const.py index bb0902293d4..6634bacf698 100644 --- a/homeassistant/components/ukraine_alarm/const.py +++ b/homeassistant/components/ukraine_alarm/const.py @@ -1,4 +1,5 @@ """Consts for the Ukraine Alarm.""" + from __future__ import annotations from homeassistant.const import Platform diff --git a/homeassistant/components/ukraine_alarm/icons.json b/homeassistant/components/ukraine_alarm/icons.json new file mode 100644 index 00000000000..a5c198ec9d3 --- /dev/null +++ b/homeassistant/components/ukraine_alarm/icons.json @@ -0,0 +1,21 @@ +{ + "entity": { + "binary_sensor": { + "air": { + "default": "mdi:cloud" + }, + "urban_fights": { + "default": "mdi:pistol" + }, + "artillery": { + "default": "mdi:tank" + }, + "chemical": { + "default": "mdi:chemical-weapon" + }, + "nuclear": { + "default": "mdi:nuke" + } + } + } +} diff --git a/homeassistant/components/unifi/__init__.py b/homeassistant/components/unifi/__init__.py index dda91801084..5174a1a7796 100644 --- a/homeassistant/components/unifi/__init__.py +++ b/homeassistant/components/unifi/__init__.py @@ -49,6 +49,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) hub.async_update_device_registry() + hub.entity_loader.load_entities() if len(hass.data[UNIFI_DOMAIN]) == 1: async_setup_services(hass) diff --git a/homeassistant/components/unifi/button.py b/homeassistant/components/unifi/button.py index f03971267bb..45fc76c73df 100644 --- a/homeassistant/components/unifi/button.py +++ b/homeassistant/components/unifi/button.py @@ -2,11 +2,12 @@ Support for restarting UniFi devices. """ + from __future__ import annotations from collections.abc import Callable, Coroutine from dataclasses import dataclass -from typing import Any, Generic +from typing import Any import aiounifi from aiounifi.interfaces.api_handlers import ItemEvent @@ -40,7 +41,6 @@ from .entity import ( from .hub import UnifiHub -@callback async def async_restart_device_control_fn( api: aiounifi.Controller, obj_id: str ) -> None: @@ -48,7 +48,6 @@ async def async_restart_device_control_fn( await api.request(DeviceRestartRequest.create(obj_id)) -@callback async def async_power_cycle_port_control_fn( api: aiounifi.Controller, obj_id: str ) -> None: @@ -57,57 +56,39 @@ async def async_power_cycle_port_control_fn( await api.request(DevicePowerCyclePortRequest.create(mac, int(index))) -@dataclass(frozen=True) -class UnifiButtonEntityDescriptionMixin(Generic[HandlerT, ApiItemT]): - """Validate and load entities from different UniFi handlers.""" - - control_fn: Callable[[aiounifi.Controller, str], Coroutine[Any, Any, None]] - - -@dataclass(frozen=True) +@dataclass(frozen=True, kw_only=True) class UnifiButtonEntityDescription( - ButtonEntityDescription, - UnifiEntityDescription[HandlerT, ApiItemT], - UnifiButtonEntityDescriptionMixin[HandlerT, ApiItemT], + ButtonEntityDescription, UnifiEntityDescription[HandlerT, ApiItemT] ): """Class describing UniFi button entity.""" + control_fn: Callable[[aiounifi.Controller, str], Coroutine[Any, Any, None]] + ENTITY_DESCRIPTIONS: tuple[UnifiButtonEntityDescription, ...] = ( UnifiButtonEntityDescription[Devices, Device]( key="Device restart", entity_category=EntityCategory.CONFIG, - has_entity_name=True, device_class=ButtonDeviceClass.RESTART, - allowed_fn=lambda hub, obj_id: True, api_handler_fn=lambda api: api.devices, available_fn=async_device_available_fn, control_fn=async_restart_device_control_fn, device_info_fn=async_device_device_info_fn, - event_is_on=None, - event_to_subscribe=None, name_fn=lambda _: "Restart", object_fn=lambda api, obj_id: api.devices[obj_id], - should_poll=False, - supported_fn=lambda hub, obj_id: True, unique_id_fn=lambda hub, obj_id: f"device_restart-{obj_id}", ), UnifiButtonEntityDescription[Ports, Port]( key="PoE power cycle", entity_category=EntityCategory.CONFIG, - has_entity_name=True, device_class=ButtonDeviceClass.RESTART, - allowed_fn=lambda hub, obj_id: True, api_handler_fn=lambda api: api.ports, available_fn=async_device_available_fn, control_fn=async_power_cycle_port_control_fn, device_info_fn=async_device_device_info_fn, - event_is_on=None, - event_to_subscribe=None, name_fn=lambda port: f"{port.name} Power Cycle", object_fn=lambda api, obj_id: api.ports[obj_id], - should_poll=False, - supported_fn=lambda hub, obj_id: hub.api.ports[obj_id].port_poe, + supported_fn=lambda hub, obj_id: bool(hub.api.ports[obj_id].port_poe), unique_id_fn=lambda hub, obj_id: f"power_cycle-{obj_id}", ), ) @@ -119,9 +100,7 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up button platform for UniFi Network integration.""" - UnifiHub.register_platform( - hass, - config_entry, + UnifiHub.get_hub(hass, config_entry).entity_loader.register_platform( async_add_entities, UnifiButtonEntity, ENTITY_DESCRIPTIONS, diff --git a/homeassistant/components/unifi/config_flow.py b/homeassistant/components/unifi/config_flow.py index fabdc9849fa..79b5e035f41 100644 --- a/homeassistant/components/unifi/config_flow.py +++ b/homeassistant/components/unifi/config_flow.py @@ -5,6 +5,7 @@ Discovery of UniFi Network instances hosted on UDM and UDM Pro devices through SSDP. Reauthentication when issue with credentials are reported. Configuration of options through options flow. """ + from __future__ import annotations from collections.abc import Mapping @@ -17,8 +18,13 @@ from urllib.parse import urlparse from aiounifi.interfaces.sites import Sites import voluptuous as vol -from homeassistant import config_entries from homeassistant.components import ssdp +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, @@ -27,7 +33,6 @@ from homeassistant.const import ( CONF_VERIFY_SSL, ) from homeassistant.core import HomeAssistant, callback -from homeassistant.data_entry_flow import FlowResult import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import format_mac @@ -61,7 +66,7 @@ MODEL_PORTS = { } -class UnifiFlowHandler(config_entries.ConfigFlow, domain=UNIFI_DOMAIN): +class UnifiFlowHandler(ConfigFlow, domain=UNIFI_DOMAIN): """Handle a UniFi Network config flow.""" VERSION = 1 @@ -71,7 +76,7 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=UNIFI_DOMAIN): @staticmethod @callback def async_get_options_flow( - config_entry: config_entries.ConfigEntry, + config_entry: ConfigEntry, ) -> UnifiOptionsFlowHandler: """Get the options flow for this handler.""" return UnifiOptionsFlowHandler(config_entry) @@ -79,12 +84,12 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=UNIFI_DOMAIN): def __init__(self) -> None: """Initialize the UniFi Network flow.""" self.config: dict[str, Any] = {} - self.reauth_config_entry: config_entries.ConfigEntry | None = None + self.reauth_config_entry: ConfigEntry | None = None self.reauth_schema: dict[vol.Marker, Any] = {} async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" errors = {} @@ -144,7 +149,7 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=UNIFI_DOMAIN): async def async_step_site( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Select site to control.""" if user_input is not None: unique_id = user_input[CONF_SITE_ID] @@ -181,7 +186,9 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=UNIFI_DOMAIN): data_schema=vol.Schema({vol.Required(CONF_SITE_ID): vol.In(site_names)}), ) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Trigger a reauthentication flow.""" config_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] @@ -206,7 +213,9 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=UNIFI_DOMAIN): return await self.async_step_user() - async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: + async def async_step_ssdp( + self, discovery_info: ssdp.SsdpServiceInfo + ) -> ConfigFlowResult: """Handle a discovered UniFi device.""" parsed_url = urlparse(discovery_info.ssdp_location) model_description = discovery_info.upnp[ssdp.ATTR_UPNP_MODEL_DESCRIPTION] @@ -228,31 +237,31 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=UNIFI_DOMAIN): if (port := MODEL_PORTS.get(model_description)) is not None: self.config[CONF_PORT] = port - self.context[ - "configuration_url" - ] = f"https://{self.config[CONF_HOST]}:{port}" + self.context["configuration_url"] = ( + f"https://{self.config[CONF_HOST]}:{port}" + ) return await self.async_step_user() -class UnifiOptionsFlowHandler(config_entries.OptionsFlow): +class UnifiOptionsFlowHandler(OptionsFlow): """Handle Unifi Network options.""" hub: UnifiHub - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + def __init__(self, config_entry: ConfigEntry) -> None: """Initialize UniFi Network options flow.""" self.config_entry = config_entry self.options = dict(config_entry.options) async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Manage the UniFi Network options.""" if self.config_entry.entry_id not in self.hass.data[UNIFI_DOMAIN]: return self.async_abort(reason="integration_not_setup") self.hub = self.hass.data[UNIFI_DOMAIN][self.config_entry.entry_id] - self.options[CONF_BLOCK_CLIENT] = self.hub.option_block_clients + self.options[CONF_BLOCK_CLIENT] = self.hub.config.option_block_clients if self.show_advanced_options: return await self.async_step_configure_entity_sources() @@ -261,7 +270,7 @@ class UnifiOptionsFlowHandler(config_entries.OptionsFlow): async def async_step_simple_options( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """For users without advanced settings enabled.""" if user_input is not None: self.options.update(user_input) @@ -270,9 +279,9 @@ class UnifiOptionsFlowHandler(config_entries.OptionsFlow): clients_to_block = {} for client in self.hub.api.clients.values(): - clients_to_block[ - client.mac - ] = f"{client.name or client.hostname} ({client.mac})" + clients_to_block[client.mac] = ( + f"{client.name or client.hostname} ({client.mac})" + ) return self.async_show_form( step_id="simple_options", @@ -280,11 +289,11 @@ class UnifiOptionsFlowHandler(config_entries.OptionsFlow): { vol.Optional( CONF_TRACK_CLIENTS, - default=self.hub.option_track_clients, + default=self.hub.config.option_track_clients, ): bool, vol.Optional( CONF_TRACK_DEVICES, - default=self.hub.option_track_devices, + default=self.hub.config.option_track_devices, ): bool, vol.Optional( CONF_BLOCK_CLIENT, default=self.options[CONF_BLOCK_CLIENT] @@ -296,7 +305,7 @@ class UnifiOptionsFlowHandler(config_entries.OptionsFlow): async def async_step_configure_entity_sources( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Select sources for entities.""" if user_input is not None: self.options.update(user_input) @@ -329,7 +338,7 @@ class UnifiOptionsFlowHandler(config_entries.OptionsFlow): async def async_step_device_tracker( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Manage the device tracker options.""" if user_input is not None: self.options.update(user_input) @@ -353,7 +362,7 @@ class UnifiOptionsFlowHandler(config_entries.OptionsFlow): ssid_filter = {ssid: ssid for ssid in sorted(ssids)} selected_ssids_to_filter = [ - ssid for ssid in self.hub.option_ssid_filter if ssid in ssid_filter + ssid for ssid in self.hub.config.option_ssid_filter if ssid in ssid_filter ] return self.async_show_form( @@ -362,26 +371,28 @@ class UnifiOptionsFlowHandler(config_entries.OptionsFlow): { vol.Optional( CONF_TRACK_CLIENTS, - default=self.hub.option_track_clients, + default=self.hub.config.option_track_clients, ): bool, vol.Optional( CONF_TRACK_WIRED_CLIENTS, - default=self.hub.option_track_wired_clients, + default=self.hub.config.option_track_wired_clients, ): bool, vol.Optional( CONF_TRACK_DEVICES, - default=self.hub.option_track_devices, + default=self.hub.config.option_track_devices, ): bool, vol.Optional( CONF_SSID_FILTER, default=selected_ssids_to_filter ): cv.multi_select(ssid_filter), vol.Optional( CONF_DETECTION_TIME, - default=int(self.hub.option_detection_time.total_seconds()), + default=int( + self.hub.config.option_detection_time.total_seconds() + ), ): int, vol.Optional( CONF_IGNORE_WIRED_BUG, - default=self.hub.option_ignore_wired_bug, + default=self.hub.config.option_ignore_wired_bug, ): bool, } ), @@ -390,7 +401,7 @@ class UnifiOptionsFlowHandler(config_entries.OptionsFlow): async def async_step_client_control( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Manage configuration of network access controlled clients.""" if user_input is not None: self.options.update(user_input) @@ -399,9 +410,9 @@ class UnifiOptionsFlowHandler(config_entries.OptionsFlow): clients_to_block = {} for client in self.hub.api.clients.values(): - clients_to_block[ - client.mac - ] = f"{client.name or client.hostname} ({client.mac})" + clients_to_block[client.mac] = ( + f"{client.name or client.hostname} ({client.mac})" + ) selected_clients_to_block = [ client @@ -429,7 +440,7 @@ class UnifiOptionsFlowHandler(config_entries.OptionsFlow): async def async_step_statistics_sensors( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Manage the statistics sensors options.""" if user_input is not None: self.options.update(user_input) @@ -441,18 +452,18 @@ class UnifiOptionsFlowHandler(config_entries.OptionsFlow): { vol.Optional( CONF_ALLOW_BANDWIDTH_SENSORS, - default=self.hub.option_allow_bandwidth_sensors, + default=self.hub.config.option_allow_bandwidth_sensors, ): bool, vol.Optional( CONF_ALLOW_UPTIME_SENSORS, - default=self.hub.option_allow_uptime_sensors, + default=self.hub.config.option_allow_uptime_sensors, ): bool, } ), last_step=True, ) - async def _update_options(self) -> FlowResult: + async def _update_options(self) -> ConfigFlowResult: """Update config entry options.""" return self.async_create_entry(title="", data=self.options) diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index 87bc0b6c59b..96a8a5dc1f8 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -6,7 +6,7 @@ from collections.abc import Callable, Mapping from dataclasses import dataclass from datetime import timedelta import logging -from typing import Any, Generic +from typing import Any import aiounifi from aiounifi.interfaces.api_handlers import ItemEvent @@ -82,21 +82,21 @@ WIRELESS_DISCONNECTION = ( @callback def async_client_allowed_fn(hub: UnifiHub, obj_id: str) -> bool: """Check if client is allowed.""" - if obj_id in hub.option_supported_clients: + if obj_id in hub.config.option_supported_clients: return True - if not hub.option_track_clients: + if not hub.config.option_track_clients: return False client = hub.api.clients[obj_id] - if client.mac not in hub.wireless_clients: - if not hub.option_track_wired_clients: + if client.mac not in hub.entity_loader.wireless_clients: + if not hub.config.option_track_wired_clients: return False elif ( client.essid - and hub.option_ssid_filter - and client.essid not in hub.option_ssid_filter + and hub.config.option_ssid_filter + and client.essid not in hub.config.option_ssid_filter ): return False @@ -108,21 +108,21 @@ def async_client_is_connected_fn(hub: UnifiHub, obj_id: str) -> bool: """Check if device object is disabled.""" client = hub.api.clients[obj_id] - if hub.wireless_clients.is_wireless(client) and client.is_wired: - if not hub.option_ignore_wired_bug: + if hub.entity_loader.wireless_clients.is_wireless(client) and client.is_wired: + if not hub.config.option_ignore_wired_bug: return False # Wired bug in action if ( not client.is_wired and client.essid - and hub.option_ssid_filter - and client.essid not in hub.option_ssid_filter + and hub.config.option_ssid_filter + and client.essid not in hub.config.option_ssid_filter ): return False if ( dt_util.utcnow() - dt_util.utc_from_timestamp(client.last_seen or 0) - > hub.option_detection_time + > hub.config.option_detection_time ): return False @@ -136,9 +136,9 @@ def async_device_heartbeat_timedelta_fn(hub: UnifiHub, obj_id: str) -> timedelta return timedelta(seconds=device.next_interval + 60) -@dataclass(frozen=True) -class UnifiEntityTrackerDescriptionMixin(Generic[HandlerT, ApiItemT]): - """Device tracker local functions.""" +@dataclass(frozen=True, kw_only=True) +class UnifiTrackerEntityDescription(UnifiEntityDescription[HandlerT, ApiItemT]): + """Class describing UniFi device tracker entity.""" heartbeat_timedelta_fn: Callable[[UnifiHub, str], timedelta] ip_address_fn: Callable[[aiounifi.Controller, str], str | None] @@ -146,21 +146,11 @@ class UnifiEntityTrackerDescriptionMixin(Generic[HandlerT, ApiItemT]): hostname_fn: Callable[[aiounifi.Controller, str], str | None] -@dataclass(frozen=True) -class UnifiTrackerEntityDescription( - UnifiEntityDescription[HandlerT, ApiItemT], - UnifiEntityTrackerDescriptionMixin[HandlerT, ApiItemT], -): - """Class describing UniFi device tracker entity.""" - - ENTITY_DESCRIPTIONS: tuple[UnifiTrackerEntityDescription, ...] = ( UnifiTrackerEntityDescription[Clients, Client]( key="Client device scanner", - has_entity_name=True, allowed_fn=async_client_allowed_fn, api_handler_fn=lambda api: api.clients, - available_fn=lambda hub, obj_id: hub.available, device_info_fn=lambda api, obj_id: None, event_is_on=(WIRED_CONNECTION + WIRELESS_CONNECTION), event_to_subscribe=( @@ -169,31 +159,24 @@ ENTITY_DESCRIPTIONS: tuple[UnifiTrackerEntityDescription, ...] = ( + WIRELESS_CONNECTION + WIRELESS_DISCONNECTION ), - heartbeat_timedelta_fn=lambda hub, _: hub.option_detection_time, + heartbeat_timedelta_fn=lambda hub, _: hub.config.option_detection_time, is_connected_fn=async_client_is_connected_fn, name_fn=lambda client: client.name or client.hostname, object_fn=lambda api, obj_id: api.clients[obj_id], - should_poll=False, - supported_fn=lambda hub, obj_id: True, unique_id_fn=lambda hub, obj_id: f"{hub.site}-{obj_id}", ip_address_fn=lambda api, obj_id: api.clients[obj_id].ip, hostname_fn=lambda api, obj_id: api.clients[obj_id].hostname, ), UnifiTrackerEntityDescription[Devices, Device]( key="Device scanner", - has_entity_name=True, - allowed_fn=lambda hub, obj_id: hub.option_track_devices, + allowed_fn=lambda hub, obj_id: hub.config.option_track_devices, api_handler_fn=lambda api: api.devices, available_fn=async_device_available_fn, device_info_fn=lambda api, obj_id: None, - event_is_on=None, - event_to_subscribe=None, heartbeat_timedelta_fn=async_device_heartbeat_timedelta_fn, - is_connected_fn=lambda ctrlr, obj_id: ctrlr.api.devices[obj_id].state == 1, + is_connected_fn=lambda hub, obj_id: hub.api.devices[obj_id].state == 1, name_fn=lambda device: device.name or device.model, object_fn=lambda api, obj_id: api.devices[obj_id], - should_poll=False, - supported_fn=lambda hub, obj_id: True, unique_id_fn=lambda hub, obj_id: obj_id, ip_address_fn=lambda api, obj_id: api.devices[obj_id].ip, hostname_fn=lambda api, obj_id: None, @@ -232,8 +215,8 @@ async def async_setup_entry( ) -> None: """Set up device tracker for UniFi Network integration.""" async_update_unique_id(hass, config_entry) - UnifiHub.register_platform( - hass, config_entry, async_add_entities, UnifiScannerEntity, ENTITY_DESCRIPTIONS + UnifiHub.get_hub(hass, config_entry).entity_loader.register_platform( + async_add_entities, UnifiScannerEntity, ENTITY_DESCRIPTIONS ) diff --git a/homeassistant/components/unifi/diagnostics.py b/homeassistant/components/unifi/diagnostics.py index 2482f5ca314..7df082ca0a4 100644 --- a/homeassistant/components/unifi/diagnostics.py +++ b/homeassistant/components/unifi/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for UniFi Network.""" + from __future__ import annotations from collections.abc import Mapping diff --git a/homeassistant/components/unifi/entity.py b/homeassistant/components/unifi/entity.py index a88f4c9b657..e162b32ba42 100644 --- a/homeassistant/components/unifi/entity.py +++ b/homeassistant/components/unifi/entity.py @@ -1,4 +1,5 @@ """UniFi entity representation.""" + from __future__ import annotations from abc import abstractmethod @@ -93,27 +94,39 @@ def async_client_device_info_fn(hub: UnifiHub, obj_id: str) -> DeviceInfo: ) -@dataclass(frozen=True) -class UnifiDescription(Generic[HandlerT, ApiItemT]): - """Validate and load entities from different UniFi handlers.""" - - allowed_fn: Callable[[UnifiHub, str], bool] - api_handler_fn: Callable[[aiounifi.Controller], HandlerT] - available_fn: Callable[[UnifiHub, str], bool] - device_info_fn: Callable[[UnifiHub, str], DeviceInfo | None] - event_is_on: tuple[EventKey, ...] | None - event_to_subscribe: tuple[EventKey, ...] | None - name_fn: Callable[[ApiItemT], str | None] - object_fn: Callable[[aiounifi.Controller, str], ApiItemT] - should_poll: bool - supported_fn: Callable[[UnifiHub, str], bool | None] - unique_id_fn: Callable[[UnifiHub, str], str] - - -@dataclass(frozen=True) -class UnifiEntityDescription(EntityDescription, UnifiDescription[HandlerT, ApiItemT]): +@dataclass(frozen=True, kw_only=True) +class UnifiEntityDescription(EntityDescription, Generic[HandlerT, ApiItemT]): """UniFi Entity Description.""" + api_handler_fn: Callable[[aiounifi.Controller], HandlerT] + """Provide api_handler from api.""" + device_info_fn: Callable[[UnifiHub, str], DeviceInfo | None] + """Provide device info object based on hub and obj_id.""" + object_fn: Callable[[aiounifi.Controller, str], ApiItemT] + """Retrieve object based on api and obj_id.""" + unique_id_fn: Callable[[UnifiHub, str], str] + """Provide a unique ID based on hub and obj_id.""" + + # Optional functions + allowed_fn: Callable[[UnifiHub, str], bool] = lambda hub, obj_id: True + """Determine if config entry options allow creation of entity.""" + available_fn: Callable[[UnifiHub, str], bool] = lambda hub, obj_id: hub.available + """Determine if entity is available, default is if connection is working.""" + name_fn: Callable[[ApiItemT], str | None] = lambda obj: None + """Entity name function, can be used to extend entity name beyond device name.""" + supported_fn: Callable[[UnifiHub, str], bool] = lambda hub, obj_id: True + """Determine if UniFi object supports providing relevant data for entity.""" + + # Optional constants + has_entity_name = True # Part of EntityDescription + """Has entity name defaults to true.""" + event_is_on: tuple[EventKey, ...] | None = None + """Which UniFi events should be used to consider state 'on'.""" + event_to_subscribe: tuple[EventKey, ...] | None = None + """Which UniFi events to listen on.""" + should_poll: bool = False + """If entity needs to do regular checks on state.""" + class UnifiEntity(Entity, Generic[HandlerT, ApiItemT]): """Representation of a UniFi entity.""" @@ -132,7 +145,7 @@ class UnifiEntity(Entity, Generic[HandlerT, ApiItemT]): self.hub = hub self.entity_description = description - hub.known_objects.add((description.key, obj_id)) + hub.entity_loader.known_objects.add((description.key, obj_id)) self._removed = False @@ -153,7 +166,9 @@ class UnifiEntity(Entity, Generic[HandlerT, ApiItemT]): @callback def unregister_object() -> None: """Remove object ID from known_objects when unloaded.""" - self.hub.known_objects.discard((description.key, self._obj_id)) + self.hub.entity_loader.known_objects.discard( + (description.key, self._obj_id) + ) self.async_on_remove(unregister_object) @@ -255,4 +270,4 @@ class UnifiEntity(Entity, Generic[HandlerT, ApiItemT]): Perform additional action updating platform entity child class state. """ - raise NotImplementedError() + raise NotImplementedError diff --git a/homeassistant/components/unifi/errors.py b/homeassistant/components/unifi/errors.py index 568bd5fb842..d33e862cafd 100644 --- a/homeassistant/components/unifi/errors.py +++ b/homeassistant/components/unifi/errors.py @@ -1,4 +1,5 @@ """Errors for the UniFi Network integration.""" + from homeassistant.exceptions import HomeAssistantError diff --git a/homeassistant/components/unifi/hub/config.py b/homeassistant/components/unifi/hub/config.py new file mode 100644 index 00000000000..52b15e1353c --- /dev/null +++ b/homeassistant/components/unifi/hub/config.py @@ -0,0 +1,124 @@ +"""UniFi Network config entry abstraction.""" + +from __future__ import annotations + +from dataclasses import dataclass +from datetime import timedelta +import ssl +from typing import Literal, Self + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_PORT, + CONF_USERNAME, + CONF_VERIFY_SSL, +) + +from ..const import ( + CONF_ALLOW_BANDWIDTH_SENSORS, + CONF_ALLOW_UPTIME_SENSORS, + CONF_BLOCK_CLIENT, + CONF_CLIENT_SOURCE, + CONF_DETECTION_TIME, + CONF_DPI_RESTRICTIONS, + CONF_IGNORE_WIRED_BUG, + CONF_SITE_ID, + CONF_SSID_FILTER, + CONF_TRACK_CLIENTS, + CONF_TRACK_DEVICES, + CONF_TRACK_WIRED_CLIENTS, + DEFAULT_ALLOW_BANDWIDTH_SENSORS, + DEFAULT_ALLOW_UPTIME_SENSORS, + DEFAULT_DETECTION_TIME, + DEFAULT_DPI_RESTRICTIONS, + DEFAULT_IGNORE_WIRED_BUG, + DEFAULT_TRACK_CLIENTS, + DEFAULT_TRACK_DEVICES, + DEFAULT_TRACK_WIRED_CLIENTS, +) + + +@dataclass +class UnifiConfig: + """Represent a UniFi config entry.""" + + entry: ConfigEntry + + host: str + port: int + username: str + password: str + site: str + ssl_context: ssl.SSLContext | Literal[False] + + option_supported_clients: list[str] + """Allow creating entities from clients.""" + + # Device tracker options + + option_track_clients: list[str] + """Config entry option to not track clients.""" + option_track_wired_clients: list[str] + """Config entry option to not track wired clients.""" + option_track_devices: bool + """Config entry option to not track devices.""" + option_ssid_filter: set[str] + """Config entry option listing what SSIDs are being used to track clients.""" + option_detection_time: timedelta + """Config entry option defining number of seconds from last seen to away""" + option_ignore_wired_bug: bool + """Config entry option to ignore wired bug.""" + + # Client control options + + option_block_clients: list[str] + """Config entry option with list of clients to control network access.""" + option_dpi_restrictions: bool + """Config entry option to control DPI restriction groups.""" + + # Statistics sensor options + + option_allow_bandwidth_sensors: bool + """Config entry option to allow bandwidth sensors.""" + option_allow_uptime_sensors: bool + """Config entry option to allow uptime sensors.""" + + @classmethod + def from_config_entry(cls, config_entry: ConfigEntry) -> Self: + """Create object from config entry.""" + config = config_entry.data + options = config_entry.options + return cls( + entry=config_entry, + host=config[CONF_HOST], + username=config[CONF_USERNAME], + password=config[CONF_PASSWORD], + port=config[CONF_PORT], + site=config[CONF_SITE_ID], + ssl_context=config.get(CONF_VERIFY_SSL, False), + option_supported_clients=options.get(CONF_CLIENT_SOURCE, []), + option_track_clients=options.get(CONF_TRACK_CLIENTS, DEFAULT_TRACK_CLIENTS), + option_track_wired_clients=options.get( + CONF_TRACK_WIRED_CLIENTS, DEFAULT_TRACK_WIRED_CLIENTS + ), + option_track_devices=options.get(CONF_TRACK_DEVICES, DEFAULT_TRACK_DEVICES), + option_ssid_filter=set(options.get(CONF_SSID_FILTER, [])), + option_detection_time=timedelta( + seconds=options.get(CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME) + ), + option_ignore_wired_bug=options.get( + CONF_IGNORE_WIRED_BUG, DEFAULT_IGNORE_WIRED_BUG + ), + option_block_clients=options.get(CONF_BLOCK_CLIENT, []), + option_dpi_restrictions=options.get( + CONF_DPI_RESTRICTIONS, DEFAULT_DPI_RESTRICTIONS + ), + option_allow_bandwidth_sensors=options.get( + CONF_ALLOW_BANDWIDTH_SENSORS, DEFAULT_ALLOW_BANDWIDTH_SENSORS + ), + option_allow_uptime_sensors=options.get( + CONF_ALLOW_UPTIME_SENSORS, DEFAULT_ALLOW_UPTIME_SENSORS + ), + ) diff --git a/homeassistant/components/unifi/hub/entity_loader.py b/homeassistant/components/unifi/hub/entity_loader.py new file mode 100644 index 00000000000..30b5ba6e686 --- /dev/null +++ b/homeassistant/components/unifi/hub/entity_loader.py @@ -0,0 +1,180 @@ +"""UniFi Network entity loader. + +Central point to load entities for the different platforms. +Make sure expected clients are available for platforms. +""" + +from __future__ import annotations + +import asyncio +from datetime import timedelta +from functools import partial +from typing import TYPE_CHECKING + +from aiounifi.interfaces.api_handlers import ItemEvent + +from homeassistant.const import Platform +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_platform import AddEntitiesCallback +from homeassistant.helpers.entity_registry import async_entries_for_config_entry + +from ..const import LOGGER, UNIFI_WIRELESS_CLIENTS +from ..entity import UnifiEntity, UnifiEntityDescription + +if TYPE_CHECKING: + from .hub import UnifiHub + +CHECK_HEARTBEAT_INTERVAL = timedelta(seconds=1) + + +class UnifiEntityLoader: + """UniFi Network integration handling platforms for entity registration.""" + + def __init__(self, hub: UnifiHub) -> None: + """Initialize the UniFi entity loader.""" + self.hub = hub + self.api_updaters = ( + hub.api.clients.update, + hub.api.clients_all.update, + hub.api.devices.update, + hub.api.dpi_apps.update, + hub.api.dpi_groups.update, + hub.api.port_forwarding.update, + hub.api.sites.update, + hub.api.system_information.update, + hub.api.wlans.update, + ) + self.wireless_clients = hub.hass.data[UNIFI_WIRELESS_CLIENTS] + + self.platforms: list[ + tuple[ + AddEntitiesCallback, + type[UnifiEntity], + tuple[UnifiEntityDescription, ...], + bool, + ] + ] = [] + + self.known_objects: set[tuple[str, str]] = set() + """Tuples of entity description key and object ID of loaded entities.""" + + async def initialize(self) -> None: + """Initialize API data and extra client support.""" + await self._refresh_api_data() + self._restore_inactive_clients() + self.wireless_clients.update_clients(set(self.hub.api.clients.values())) + + async def _refresh_api_data(self) -> None: + """Refresh API data from network application.""" + results = await asyncio.gather( + *[update() for update in self.api_updaters], + return_exceptions=True, + ) + for result in results: + if result is not None: + LOGGER.warning("Exception on update %s", result) + + @callback + def _restore_inactive_clients(self) -> None: + """Restore inactive clients. + + Provide inactive clients to device tracker and switch platform. + """ + config = self.hub.config + entity_registry = er.async_get(self.hub.hass) + macs: list[str] = [ + entry.unique_id.split("-", 1)[1] + for entry in async_entries_for_config_entry( + entity_registry, config.entry.entry_id + ) + if entry.domain == Platform.DEVICE_TRACKER and "-" in entry.unique_id + ] + api = self.hub.api + for mac in config.option_supported_clients + config.option_block_clients + macs: + if mac not in api.clients and mac in api.clients_all: + api.clients.process_raw([dict(api.clients_all[mac].raw)]) + + @callback + def register_platform( + self, + async_add_entities: AddEntitiesCallback, + entity_class: type[UnifiEntity], + descriptions: tuple[UnifiEntityDescription, ...], + requires_admin: bool = False, + ) -> None: + """Register UniFi entity platforms.""" + self.platforms.append( + (async_add_entities, entity_class, descriptions, requires_admin) + ) + + @callback + def load_entities(self) -> None: + """Load entities into the registered UniFi platforms.""" + for ( + async_add_entities, + entity_class, + descriptions, + requires_admin, + ) in self.platforms: + if requires_admin and not self.hub.is_admin: + continue + self._load_entities(entity_class, descriptions, async_add_entities) + + @callback + def _should_add_entity( + self, description: UnifiEntityDescription, obj_id: str + ) -> bool: + """Validate if entity is allowed and supported before creating it.""" + return bool( + (description.key, obj_id) not in self.known_objects + and description.allowed_fn(self.hub, obj_id) + and description.supported_fn(self.hub, obj_id) + ) + + @callback + def _load_entities( + self, + unifi_platform_entity: type[UnifiEntity], + descriptions: tuple[UnifiEntityDescription, ...], + async_add_entities: AddEntitiesCallback, + ) -> None: + """Load entities and subscribe for future entities.""" + + @callback + def add_unifi_entities() -> None: + """Add currently known UniFi entities.""" + async_add_entities( + unifi_platform_entity(obj_id, self.hub, description) + for description in descriptions + for obj_id in description.api_handler_fn(self.hub.api) + if self._should_add_entity(description, obj_id) + ) + + add_unifi_entities() + + self.hub.config.entry.async_on_unload( + async_dispatcher_connect( + self.hub.hass, + self.hub.signal_options_update, + add_unifi_entities, + ) + ) + + # Subscribe for future entities + + @callback + def create_unifi_entity( + description: UnifiEntityDescription, event: ItemEvent, obj_id: str + ) -> None: + """Create new UniFi entity on event.""" + if self._should_add_entity(description, obj_id): + async_add_entities( + [unifi_platform_entity(obj_id, self.hub, description)] + ) + + for description in descriptions: + description.api_handler_fn(self.hub.api).subscribe( + partial(create_unifi_entity, description), ItemEvent.ADDED + ) diff --git a/homeassistant/components/unifi/hub/hub.py b/homeassistant/components/unifi/hub/hub.py index 0188adf5c3f..df91584f267 100644 --- a/homeassistant/components/unifi/hub/hub.py +++ b/homeassistant/components/unifi/hub/hub.py @@ -1,59 +1,27 @@ """UniFi Network abstraction.""" + from __future__ import annotations -from collections.abc import Iterable from datetime import datetime, timedelta -from functools import partial import aiounifi -from aiounifi.interfaces.api_handlers import ItemEvent from aiounifi.models.device import DeviceSetPoePortModeRequest from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST, Platform from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback -from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers import device_registry as dr from homeassistant.helpers.device_registry import ( DeviceEntry, DeviceEntryType, DeviceInfo, ) -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, - async_dispatcher_send, -) -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.entity_registry import async_entries_for_config_entry +from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_call_later, 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, - CONF_CLIENT_SOURCE, - CONF_DETECTION_TIME, - CONF_DPI_RESTRICTIONS, - CONF_IGNORE_WIRED_BUG, - CONF_SITE_ID, - CONF_SSID_FILTER, - CONF_TRACK_CLIENTS, - CONF_TRACK_DEVICES, - CONF_TRACK_WIRED_CLIENTS, - DEFAULT_ALLOW_BANDWIDTH_SENSORS, - DEFAULT_ALLOW_UPTIME_SENSORS, - DEFAULT_DETECTION_TIME, - DEFAULT_DPI_RESTRICTIONS, - DEFAULT_IGNORE_WIRED_BUG, - DEFAULT_TRACK_CLIENTS, - DEFAULT_TRACK_DEVICES, - DEFAULT_TRACK_WIRED_CLIENTS, - DOMAIN as UNIFI_DOMAIN, - PLATFORMS, - UNIFI_WIRELESS_CLIENTS, -) -from ..entity import UnifiEntity, UnifiEntityDescription +from ..const import ATTR_MANUFACTURER, CONF_SITE_ID, DOMAIN as UNIFI_DOMAIN, PLATFORMS +from .config import UnifiConfig +from .entity_loader import UnifiEntityLoader from .websocket import UnifiWebsocket CHECK_HEARTBEAT_INTERVAL = timedelta(seconds=1) @@ -67,177 +35,41 @@ class UnifiHub: ) -> None: """Initialize the system.""" self.hass = hass - self.config_entry = config_entry self.api = api + self.config = UnifiConfig.from_config_entry(config_entry) + self.entity_loader = UnifiEntityLoader(self) self.websocket = UnifiWebsocket(hass, api, self.signal_reachable) - self.wireless_clients = hass.data[UNIFI_WIRELESS_CLIENTS] - self.site = config_entry.data[CONF_SITE_ID] self.is_admin = False self._cancel_heartbeat_check: CALLBACK_TYPE | None = None self._heartbeat_time: dict[str, datetime] = {} - self.load_config_entry_options() - - self.entities: dict[str, str] = {} - self.known_objects: set[tuple[str, str]] = set() - self.poe_command_queue: dict[str, dict[int, str]] = {} self._cancel_poe_command: CALLBACK_TYPE | None = None - def load_config_entry_options(self) -> None: - """Store attributes to avoid property call overhead since they are called frequently.""" - options = self.config_entry.options - - # Allow creating entities from clients. - self.option_supported_clients: list[str] = options.get(CONF_CLIENT_SOURCE, []) - - # Device tracker options - - # Config entry option to not track clients. - self.option_track_clients = options.get( - CONF_TRACK_CLIENTS, DEFAULT_TRACK_CLIENTS - ) - # Config entry option to not track wired clients. - self.option_track_wired_clients = options.get( - CONF_TRACK_WIRED_CLIENTS, DEFAULT_TRACK_WIRED_CLIENTS - ) - # Config entry option to not track devices. - self.option_track_devices: bool = options.get( - CONF_TRACK_DEVICES, DEFAULT_TRACK_DEVICES - ) - # Config entry option listing what SSIDs are being used to track clients. - self.option_ssid_filter = set(options.get(CONF_SSID_FILTER, [])) - # Config entry option defining number of seconds from last seen to away - self.option_detection_time = timedelta( - seconds=options.get(CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME) - ) - # Config entry option to ignore wired bug. - self.option_ignore_wired_bug = options.get( - CONF_IGNORE_WIRED_BUG, DEFAULT_IGNORE_WIRED_BUG - ) - - # Client control options - - # Config entry option with list of clients to control network access. - self.option_block_clients: list[str] = options.get(CONF_BLOCK_CLIENT, []) - # Config entry option to control DPI restriction groups. - self.option_dpi_restrictions: bool = options.get( - CONF_DPI_RESTRICTIONS, DEFAULT_DPI_RESTRICTIONS - ) - - # Statistics sensor options - - # Config entry option to allow bandwidth sensors. - self.option_allow_bandwidth_sensors: bool = options.get( - CONF_ALLOW_BANDWIDTH_SENSORS, DEFAULT_ALLOW_BANDWIDTH_SENSORS - ) - # Config entry option to allow uptime sensors. - self.option_allow_uptime_sensors: bool = options.get( - CONF_ALLOW_UPTIME_SENSORS, DEFAULT_ALLOW_UPTIME_SENSORS - ) - - @property - def host(self) -> str: - """Return the host of this hub.""" - host: str = self.config_entry.data[CONF_HOST] - return host + @callback + @staticmethod + def get_hub(hass: HomeAssistant, config_entry: ConfigEntry) -> UnifiHub: + """Get UniFi hub from config entry.""" + hub: UnifiHub = hass.data[UNIFI_DOMAIN][config_entry.entry_id] + return hub @property def available(self) -> bool: """Websocket connection state.""" return self.websocket.available - @callback - @staticmethod - def register_platform( - hass: HomeAssistant, - config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, - entity_class: type[UnifiEntity], - descriptions: tuple[UnifiEntityDescription, ...], - requires_admin: bool = False, - ) -> None: - """Register platform for UniFi entity management.""" - hub: UnifiHub = hass.data[UNIFI_DOMAIN][config_entry.entry_id] - if requires_admin and not hub.is_admin: - return - hub.register_platform_add_entities( - entity_class, descriptions, async_add_entities - ) - - @callback - def _async_should_add_entity( - self, description: UnifiEntityDescription, obj_id: str - ) -> bool: - """Check if entity should be added.""" - return bool( - (description.key, obj_id) not in self.known_objects - and description.allowed_fn(self, obj_id) - and description.supported_fn(self, obj_id) - ) - - @callback - def register_platform_add_entities( - self, - unifi_platform_entity: type[UnifiEntity], - descriptions: tuple[UnifiEntityDescription, ...], - async_add_entities: AddEntitiesCallback, - ) -> None: - """Subscribe to UniFi API handlers and create entities.""" - - @callback - def async_load_entities(descriptions: Iterable[UnifiEntityDescription]) -> None: - """Load and subscribe to UniFi endpoints.""" - - @callback - def async_add_unifi_entities() -> None: - """Add UniFi entity.""" - async_add_entities( - unifi_platform_entity(obj_id, self, description) - for description in descriptions - for obj_id in description.api_handler_fn(self.api) - if self._async_should_add_entity(description, obj_id) - ) - - async_add_unifi_entities() - - @callback - def async_create_entity( - description: UnifiEntityDescription, event: ItemEvent, obj_id: str - ) -> None: - """Create new UniFi entity on event.""" - if self._async_should_add_entity(description, obj_id): - async_add_entities( - [unifi_platform_entity(obj_id, self, description)] - ) - - for description in descriptions: - description.api_handler_fn(self.api).subscribe( - partial(async_create_entity, description), ItemEvent.ADDED - ) - - self.config_entry.async_on_unload( - async_dispatcher_connect( - self.hass, - self.signal_options_update, - async_add_unifi_entities, - ) - ) - - async_load_entities(descriptions) - @property def signal_reachable(self) -> str: """Integration specific event to signal a change in connection status.""" - return f"unifi-reachable-{self.config_entry.entry_id}" + return f"unifi-reachable-{self.config.entry.entry_id}" @property def signal_options_update(self) -> str: """Event specific per UniFi entry to signal new options.""" - return f"unifi-options-{self.config_entry.entry_id}" + return f"unifi-options-{self.config.entry.entry_id}" @property def signal_heartbeat_missed(self) -> str: @@ -246,27 +78,12 @@ class UnifiHub: async def initialize(self) -> None: """Set up a UniFi Network instance.""" - await self.api.initialize() + await self.entity_loader.initialize() - assert self.config_entry.unique_id is not None - self.is_admin = self.api.sites[self.config_entry.unique_id].role == "admin" + assert self.config.entry.unique_id is not None + self.is_admin = self.api.sites[self.config.entry.unique_id].role == "admin" - # Restore device tracker clients that are not a part of active clients list. - macs: list[str] = [] - entity_registry = er.async_get(self.hass) - for entry in async_entries_for_config_entry( - entity_registry, self.config_entry.entry_id - ): - if entry.domain == Platform.DEVICE_TRACKER and "-" in entry.unique_id: - macs.append(entry.unique_id.split("-", 1)[1]) - - for mac in self.option_supported_clients + self.option_block_clients + macs: - if mac not in self.api.clients and mac in self.api.clients_all: - self.api.clients.process_raw([dict(self.api.clients_all[mac].raw)]) - - self.wireless_clients.update_clients(set(self.api.clients.values())) - - self.config_entry.add_update_listener(self.async_config_entry_updated) + 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 @@ -328,7 +145,7 @@ class UnifiHub: @property def device_info(self) -> DeviceInfo: """UniFi Network device info.""" - assert self.config_entry.unique_id is not None + assert self.config.entry.unique_id is not None version: str | None = None if sysinfo := next(iter(self.api.system_information.values()), None): @@ -336,7 +153,7 @@ class UnifiHub: return DeviceInfo( entry_type=DeviceEntryType.SERVICE, - identifiers={(UNIFI_DOMAIN, self.config_entry.unique_id)}, + identifiers={(UNIFI_DOMAIN, self.config.entry.unique_id)}, manufacturer=ATTR_MANUFACTURER, model="UniFi Network Application", name="UniFi Network", @@ -348,7 +165,7 @@ class UnifiHub: """Update device registry.""" device_registry = dr.async_get(self.hass) return device_registry.async_get_or_create( - config_entry_id=self.config_entry.entry_id, **self.device_info + config_entry_id=self.config.entry.entry_id, **self.device_info ) @staticmethod @@ -362,7 +179,7 @@ class UnifiHub: """ if not (hub := hass.data[UNIFI_DOMAIN].get(config_entry.entry_id)): return - hub.load_config_entry_options() + hub.config = UnifiConfig.from_config_entry(config_entry) async_dispatcher_send(hass, hub.signal_options_update) @callback @@ -382,7 +199,7 @@ class UnifiHub: await self.websocket.stop_and_wait() unload_ok = await self.hass.config_entries.async_unload_platforms( - self.config_entry, PLATFORMS + self.config.entry, PLATFORMS ) if not unload_ok: diff --git a/homeassistant/components/unifi/icons.json b/homeassistant/components/unifi/icons.json new file mode 100644 index 00000000000..2d5017a3187 --- /dev/null +++ b/homeassistant/components/unifi/icons.json @@ -0,0 +1,6 @@ +{ + "services": { + "reconnect_client": "mdi:sync", + "remove_clients": "mdi:delete" + } +} diff --git a/homeassistant/components/unifi/image.py b/homeassistant/components/unifi/image.py index a070c158772..285477fe133 100644 --- a/homeassistant/components/unifi/image.py +++ b/homeassistant/components/unifi/image.py @@ -2,11 +2,11 @@ Support for QR code for guest WLANs. """ + from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass -from typing import Generic from aiounifi.interfaces.api_handlers import ItemEvent from aiounifi.interfaces.wlans import Wlans @@ -36,39 +36,26 @@ def async_wlan_qr_code_image_fn(hub: UnifiHub, wlan: Wlan) -> bytes: return hub.api.wlans.generate_wlan_qr_code(wlan) -@dataclass(frozen=True) -class UnifiImageEntityDescriptionMixin(Generic[HandlerT, ApiItemT]): - """Validate and load entities from different UniFi handlers.""" +@dataclass(frozen=True, kw_only=True) +class UnifiImageEntityDescription( + ImageEntityDescription, UnifiEntityDescription[HandlerT, ApiItemT] +): + """Class describing UniFi image entity.""" image_fn: Callable[[UnifiHub, ApiItemT], bytes] value_fn: Callable[[ApiItemT], str | None] -@dataclass(frozen=True) -class UnifiImageEntityDescription( - ImageEntityDescription, - UnifiEntityDescription[HandlerT, ApiItemT], - UnifiImageEntityDescriptionMixin[HandlerT, ApiItemT], -): - """Class describing UniFi image entity.""" - - ENTITY_DESCRIPTIONS: tuple[UnifiImageEntityDescription, ...] = ( UnifiImageEntityDescription[Wlans, Wlan]( key="WLAN QR Code", entity_category=EntityCategory.DIAGNOSTIC, - has_entity_name=True, entity_registry_enabled_default=False, - allowed_fn=lambda hub, obj_id: True, api_handler_fn=lambda api: api.wlans, available_fn=async_wlan_available_fn, device_info_fn=async_wlan_device_info_fn, - event_is_on=None, - event_to_subscribe=None, name_fn=lambda wlan: "QR Code", object_fn=lambda api, obj_id: api.wlans[obj_id], - should_poll=False, - supported_fn=lambda hub, obj_id: True, unique_id_fn=lambda hub, obj_id: f"qr_code-{obj_id}", image_fn=async_wlan_qr_code_image_fn, value_fn=lambda obj: obj.x_passphrase, @@ -82,9 +69,7 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up image platform for UniFi Network integration.""" - UnifiHub.register_platform( - hass, - config_entry, + UnifiHub.get_hub(hass, config_entry).entity_loader.register_platform( async_add_entities, UnifiImageEntity, ENTITY_DESCRIPTIONS, diff --git a/homeassistant/components/unifi/manifest.json b/homeassistant/components/unifi/manifest.json index bcc25b22059..05dc2189908 100644 --- a/homeassistant/components/unifi/manifest.json +++ b/homeassistant/components/unifi/manifest.json @@ -4,12 +4,11 @@ "codeowners": ["@Kane610"], "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifi", - "import_executor": true, "integration_type": "hub", "iot_class": "local_push", "loggers": ["aiounifi"], "quality_scale": "platinum", - "requirements": ["aiounifi==72"], + "requirements": ["aiounifi==74"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/homeassistant/components/unifi/sensor.py b/homeassistant/components/unifi/sensor.py index ab76e662859..efb3eed4de4 100644 --- a/homeassistant/components/unifi/sensor.py +++ b/homeassistant/components/unifi/sensor.py @@ -3,13 +3,13 @@ Support for bandwidth sensors of network clients. Support for uptime sensors of network clients. """ + from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass from datetime import date, datetime, timedelta from decimal import Decimal -from typing import Generic from aiounifi.interfaces.api_handlers import ItemEvent from aiounifi.interfaces.clients import Clients @@ -56,23 +56,23 @@ from .hub import UnifiHub @callback def async_bandwidth_sensor_allowed_fn(hub: UnifiHub, obj_id: str) -> bool: """Check if client is allowed.""" - if obj_id in hub.option_supported_clients: + if obj_id in hub.config.option_supported_clients: return True - return hub.option_allow_bandwidth_sensors + return hub.config.option_allow_bandwidth_sensors @callback def async_uptime_sensor_allowed_fn(hub: UnifiHub, obj_id: str) -> bool: """Check if client is allowed.""" - if obj_id in hub.option_supported_clients: + if obj_id in hub.config.option_supported_clients: return True - return hub.option_allow_uptime_sensors + return hub.config.option_allow_uptime_sensors @callback def async_client_rx_value_fn(hub: UnifiHub, client: Client) -> float: """Calculate receiving data transfer value.""" - if hub.wireless_clients.is_wireless(client): + if hub.entity_loader.wireless_clients.is_wireless(client): return client.rx_bytes_r / 1000000 return client.wired_rx_bytes_r / 1000000 @@ -80,7 +80,7 @@ def async_client_rx_value_fn(hub: UnifiHub, client: Client) -> float: @callback def async_client_tx_value_fn(hub: UnifiHub, client: Client) -> float: """Calculate transmission data transfer value.""" - if hub.wireless_clients.is_wireless(client): + if hub.entity_loader.wireless_clients.is_wireless(client): return client.tx_bytes_r / 1000000 return client.wired_tx_bytes_r / 1000000 @@ -102,7 +102,7 @@ def async_wlan_client_value_fn(hub: UnifiHub, wlan: Wlan) -> int: for client in hub.api.clients.values() if client.essid == wlan.name and dt_util.utcnow() - dt_util.utc_from_timestamp(client.last_seen or 0) - < hub.option_detection_time + < hub.config.option_detection_time ] ) @@ -147,40 +147,35 @@ def async_client_is_connected_fn(hub: UnifiHub, obj_id: str) -> bool: if ( dt_util.utcnow() - dt_util.utc_from_timestamp(client.last_seen or 0) - > hub.option_detection_time + > hub.config.option_detection_time ): return False return True -@dataclass(frozen=True) -class UnifiSensorEntityDescriptionMixin(Generic[HandlerT, ApiItemT]): - """Validate and load entities from different UniFi handlers.""" - - value_fn: Callable[[UnifiHub, ApiItemT], datetime | float | str | None] - - @callback def async_device_state_value_fn(hub: UnifiHub, device: Device) -> str: """Retrieve the state of the device.""" return DEVICE_STATES[device.state] -@dataclass(frozen=True) +@dataclass(frozen=True, kw_only=True) class UnifiSensorEntityDescription( - SensorEntityDescription, - UnifiEntityDescription[HandlerT, ApiItemT], - UnifiSensorEntityDescriptionMixin[HandlerT, ApiItemT], + SensorEntityDescription, UnifiEntityDescription[HandlerT, ApiItemT] ): """Class describing UniFi sensor entity.""" + value_fn: Callable[[UnifiHub, ApiItemT], datetime | float | str | None] + + # Optional is_connected_fn: Callable[[UnifiHub, str], bool] | None = None - # Custom function to determine whether a state change should be recorded + """Calculate if source is connected.""" value_changed_fn: Callable[ [StateType | date | datetime | Decimal, datetime | float | str | None], bool, ] = lambda old, new: old != new + """Calculate whether a state change should be recorded.""" ENTITY_DESCRIPTIONS: tuple[UnifiSensorEntityDescription, ...] = ( @@ -191,18 +186,13 @@ ENTITY_DESCRIPTIONS: tuple[UnifiSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfDataRate.MEGABYTES_PER_SECOND, icon="mdi:upload", - has_entity_name=True, allowed_fn=async_bandwidth_sensor_allowed_fn, api_handler_fn=lambda api: api.clients, - available_fn=lambda hub, _: hub.available, device_info_fn=async_client_device_info_fn, - event_is_on=None, - event_to_subscribe=None, is_connected_fn=async_client_is_connected_fn, name_fn=lambda _: "RX", object_fn=lambda api, obj_id: api.clients[obj_id], - should_poll=False, - supported_fn=lambda hub, _: hub.option_allow_bandwidth_sensors, + supported_fn=lambda hub, _: hub.config.option_allow_bandwidth_sensors, unique_id_fn=lambda hub, obj_id: f"rx-{obj_id}", value_fn=async_client_rx_value_fn, ), @@ -213,18 +203,13 @@ ENTITY_DESCRIPTIONS: tuple[UnifiSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfDataRate.MEGABYTES_PER_SECOND, icon="mdi:download", - has_entity_name=True, allowed_fn=async_bandwidth_sensor_allowed_fn, api_handler_fn=lambda api: api.clients, - available_fn=lambda hub, _: hub.available, device_info_fn=async_client_device_info_fn, - event_is_on=None, - event_to_subscribe=None, is_connected_fn=async_client_is_connected_fn, name_fn=lambda _: "TX", object_fn=lambda api, obj_id: api.clients[obj_id], - should_poll=False, - supported_fn=lambda hub, _: hub.option_allow_bandwidth_sensors, + supported_fn=lambda hub, _: hub.config.option_allow_bandwidth_sensors, unique_id_fn=lambda hub, obj_id: f"tx-{obj_id}", value_fn=async_client_tx_value_fn, ), @@ -233,18 +218,13 @@ ENTITY_DESCRIPTIONS: tuple[UnifiSensorEntityDescription, ...] = ( device_class=SensorDeviceClass.POWER, entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=UnitOfPower.WATT, - has_entity_name=True, entity_registry_enabled_default=False, - allowed_fn=lambda hub, obj_id: True, api_handler_fn=lambda api: api.ports, available_fn=async_device_available_fn, device_info_fn=async_device_device_info_fn, - event_is_on=None, - event_to_subscribe=None, name_fn=lambda port: f"{port.name} PoE Power", object_fn=lambda api, obj_id: api.ports[obj_id], - should_poll=False, - supported_fn=lambda hub, obj_id: hub.api.ports[obj_id].port_poe, + supported_fn=lambda hub, obj_id: bool(hub.api.ports[obj_id].port_poe), unique_id_fn=lambda hub, obj_id: f"poe_power-{obj_id}", value_fn=lambda _, obj: obj.poe_power if obj.poe_mode != "off" else "0", ), @@ -252,36 +232,25 @@ ENTITY_DESCRIPTIONS: tuple[UnifiSensorEntityDescription, ...] = ( key="Client uptime", device_class=SensorDeviceClass.TIMESTAMP, entity_category=EntityCategory.DIAGNOSTIC, - has_entity_name=True, entity_registry_enabled_default=False, allowed_fn=async_uptime_sensor_allowed_fn, api_handler_fn=lambda api: api.clients, - available_fn=lambda hub, obj_id: hub.available, device_info_fn=async_client_device_info_fn, - event_is_on=None, - event_to_subscribe=None, name_fn=lambda client: "Uptime", object_fn=lambda api, obj_id: api.clients[obj_id], - should_poll=False, - supported_fn=lambda hub, _: hub.option_allow_uptime_sensors, + supported_fn=lambda hub, _: hub.config.option_allow_uptime_sensors, unique_id_fn=lambda hub, obj_id: f"uptime-{obj_id}", value_fn=async_client_uptime_value_fn, ), UnifiSensorEntityDescription[Wlans, Wlan]( key="WLAN clients", entity_category=EntityCategory.DIAGNOSTIC, - has_entity_name=True, state_class=SensorStateClass.MEASUREMENT, - allowed_fn=lambda hub, obj_id: True, api_handler_fn=lambda api: api.wlans, available_fn=async_wlan_available_fn, device_info_fn=async_wlan_device_info_fn, - event_is_on=None, - event_to_subscribe=None, - name_fn=lambda wlan: None, object_fn=lambda api, obj_id: api.wlans[obj_id], should_poll=True, - supported_fn=lambda hub, obj_id: True, unique_id_fn=lambda hub, obj_id: f"wlan_clients-{obj_id}", value_fn=async_wlan_client_value_fn, ), @@ -290,13 +259,9 @@ ENTITY_DESCRIPTIONS: tuple[UnifiSensorEntityDescription, ...] = ( device_class=SensorDeviceClass.POWER, entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=UnitOfPower.WATT, - has_entity_name=True, - allowed_fn=lambda hub, obj_id: True, api_handler_fn=lambda api: api.outlets, available_fn=async_device_available_fn, device_info_fn=async_device_device_info_fn, - event_is_on=None, - event_to_subscribe=None, name_fn=lambda outlet: f"{outlet.name} Outlet Power", object_fn=lambda api, obj_id: api.outlets[obj_id], should_poll=True, @@ -310,16 +275,11 @@ ENTITY_DESCRIPTIONS: tuple[UnifiSensorEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=UnitOfPower.WATT, suggested_display_precision=1, - has_entity_name=True, - allowed_fn=lambda hub, obj_id: True, api_handler_fn=lambda api: api.devices, available_fn=async_device_available_fn, device_info_fn=async_device_device_info_fn, - event_is_on=None, - event_to_subscribe=None, name_fn=lambda device: "AC Power Budget", object_fn=lambda api, obj_id: api.devices[obj_id], - should_poll=False, supported_fn=async_device_outlet_supported_fn, unique_id_fn=lambda hub, obj_id: f"ac_power_budget-{obj_id}", value_fn=lambda hub, device: device.outlet_ac_power_budget, @@ -330,16 +290,11 @@ ENTITY_DESCRIPTIONS: tuple[UnifiSensorEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=UnitOfPower.WATT, suggested_display_precision=1, - has_entity_name=True, - allowed_fn=lambda hub, obj_id: True, api_handler_fn=lambda api: api.devices, available_fn=async_device_available_fn, device_info_fn=async_device_device_info_fn, - event_is_on=None, - event_to_subscribe=None, name_fn=lambda device: "AC Power Consumption", object_fn=lambda api, obj_id: api.devices[obj_id], - should_poll=False, supported_fn=async_device_outlet_supported_fn, unique_id_fn=lambda hub, obj_id: f"ac_power_conumption-{obj_id}", value_fn=lambda hub, device: device.outlet_ac_power_consumption, @@ -348,17 +303,11 @@ ENTITY_DESCRIPTIONS: tuple[UnifiSensorEntityDescription, ...] = ( key="Device uptime", device_class=SensorDeviceClass.TIMESTAMP, entity_category=EntityCategory.DIAGNOSTIC, - has_entity_name=True, - allowed_fn=lambda hub, obj_id: True, api_handler_fn=lambda api: api.devices, available_fn=async_device_available_fn, device_info_fn=async_device_device_info_fn, - event_is_on=None, - event_to_subscribe=None, name_fn=lambda device: "Uptime", object_fn=lambda api, obj_id: api.devices[obj_id], - should_poll=False, - supported_fn=lambda hub, obj_id: True, unique_id_fn=lambda hub, obj_id: f"device_uptime-{obj_id}", value_fn=async_device_uptime_value_fn, value_changed_fn=async_device_uptime_value_changed_fn, @@ -368,35 +317,24 @@ ENTITY_DESCRIPTIONS: tuple[UnifiSensorEntityDescription, ...] = ( device_class=SensorDeviceClass.TEMPERATURE, entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=UnitOfTemperature.CELSIUS, - has_entity_name=True, - allowed_fn=lambda hub, obj_id: True, api_handler_fn=lambda api: api.devices, available_fn=async_device_available_fn, device_info_fn=async_device_device_info_fn, - event_is_on=None, - event_to_subscribe=None, name_fn=lambda device: "Temperature", object_fn=lambda api, obj_id: api.devices[obj_id], - should_poll=False, - supported_fn=lambda ctrlr, obj_id: ctrlr.api.devices[obj_id].has_temperature, + supported_fn=lambda hub, obj_id: hub.api.devices[obj_id].has_temperature, unique_id_fn=lambda hub, obj_id: f"device_temperature-{obj_id}", - value_fn=lambda ctrlr, device: device.general_temperature, + value_fn=lambda hub, device: device.general_temperature, ), UnifiSensorEntityDescription[Devices, Device]( key="Device State", device_class=SensorDeviceClass.ENUM, entity_category=EntityCategory.DIAGNOSTIC, - has_entity_name=True, - allowed_fn=lambda hub, obj_id: True, api_handler_fn=lambda api: api.devices, available_fn=async_device_available_fn, device_info_fn=async_device_device_info_fn, - event_is_on=None, - event_to_subscribe=None, name_fn=lambda device: "State", object_fn=lambda api, obj_id: api.devices[obj_id], - should_poll=False, - supported_fn=lambda hub, obj_id: True, unique_id_fn=lambda hub, obj_id: f"device_state-{obj_id}", value_fn=async_device_state_value_fn, options=list(DEVICE_STATES.values()), @@ -410,8 +348,8 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up sensors for UniFi Network integration.""" - UnifiHub.register_platform( - hass, config_entry, async_add_entities, UnifiSensorEntity, ENTITY_DESCRIPTIONS + UnifiHub.get_hub(hass, config_entry).entity_loader.register_platform( + async_add_entities, UnifiSensorEntity, ENTITY_DESCRIPTIONS ) @@ -449,7 +387,7 @@ class UnifiSensorEntity(UnifiEntity[HandlerT, ApiItemT], SensorEntity): if description.is_connected_fn(self.hub, self._obj_id): self.hub.async_heartbeat( self._attr_unique_id, - dt_util.utcnow() + self.hub.option_detection_time, + dt_util.utcnow() + self.hub.config.option_detection_time, ) async def async_added_to_hass(self) -> None: diff --git a/homeassistant/components/unifi/services.py b/homeassistant/components/unifi/services.py index 2017db4a0a8..096f4f27dae 100644 --- a/homeassistant/components/unifi/services.py +++ b/homeassistant/components/unifi/services.py @@ -1,4 +1,5 @@ """UniFi Network services.""" + from collections.abc import Mapping from typing import Any diff --git a/homeassistant/components/unifi/switch.py b/homeassistant/components/unifi/switch.py index 4a2785f0c17..6e073a655a5 100644 --- a/homeassistant/components/unifi/switch.py +++ b/homeassistant/components/unifi/switch.py @@ -5,12 +5,13 @@ Support for controlling network access of clients selected in option flow. Support for controlling deep packet inspection (DPI) restriction groups. Support for controlling WLAN availability. """ + from __future__ import annotations import asyncio from collections.abc import Callable, Coroutine from dataclasses import dataclass -from typing import Any, Generic +from typing import Any import aiounifi from aiounifi.interfaces.api_handlers import ItemEvent @@ -64,9 +65,9 @@ CLIENT_UNBLOCKED = (EventKey.WIRED_CLIENT_UNBLOCKED, EventKey.WIRELESS_CLIENT_UN @callback def async_block_client_allowed_fn(hub: UnifiHub, obj_id: str) -> bool: """Check if client is allowed.""" - if obj_id in hub.option_supported_clients: + if obj_id in hub.config.option_supported_clients: return True - return obj_id in hub.option_block_clients + return obj_id in hub.config.option_block_clients @callback @@ -95,7 +96,7 @@ def async_dpi_group_device_info_fn(hub: UnifiHub, obj_id: str) -> DeviceInfo: @callback def async_port_forward_device_info_fn(hub: UnifiHub, obj_id: str) -> DeviceInfo: """Create device registry entry for port forward.""" - unique_id = hub.config_entry.unique_id + unique_id = hub.config.entry.unique_id assert unique_id is not None return DeviceInfo( entry_type=DeviceEntryType.SERVICE, @@ -125,7 +126,7 @@ async def async_dpi_group_control_fn(hub: UnifiHub, obj_id: str, target: bool) - @callback -def async_outlet_supports_switching_fn(hub: UnifiHub, obj_id: str) -> bool: +def async_outlet_switching_supported_fn(hub: UnifiHub, obj_id: str) -> bool: """Determine if an outlet supports switching.""" outlet = hub.api.outlets[obj_id] return outlet.has_relay or outlet.caps in (1, 3) @@ -162,24 +163,20 @@ async def async_wlan_control_fn(hub: UnifiHub, obj_id: str, target: bool) -> Non await hub.api.request(WlanEnableRequest.create(obj_id, target)) -@dataclass(frozen=True) -class UnifiSwitchEntityDescriptionMixin(Generic[HandlerT, ApiItemT]): - """Validate and load entities from different UniFi handlers.""" +@dataclass(frozen=True, kw_only=True) +class UnifiSwitchEntityDescription( + SwitchEntityDescription, UnifiEntityDescription[HandlerT, ApiItemT] +): + """Class describing UniFi switch entity.""" control_fn: Callable[[UnifiHub, str, bool], Coroutine[Any, Any, None]] is_on_fn: Callable[[UnifiHub, ApiItemT], bool] - -@dataclass(frozen=True) -class UnifiSwitchEntityDescription( - SwitchEntityDescription, - UnifiEntityDescription[HandlerT, ApiItemT], - UnifiSwitchEntityDescriptionMixin[HandlerT, ApiItemT], -): - """Class describing UniFi switch entity.""" - + # Optional custom_subscribe: Callable[[aiounifi.Controller], SubscriptionT] | None = None + """Callback for additional subscriptions to any UniFi handler.""" only_event_for_state_change: bool = False + """Use only UniFi events to trigger state changes.""" ENTITY_DESCRIPTIONS: tuple[UnifiSwitchEntityDescription, ...] = ( @@ -187,119 +184,86 @@ ENTITY_DESCRIPTIONS: tuple[UnifiSwitchEntityDescription, ...] = ( key="Block client", device_class=SwitchDeviceClass.SWITCH, entity_category=EntityCategory.CONFIG, - has_entity_name=True, icon="mdi:ethernet", allowed_fn=async_block_client_allowed_fn, api_handler_fn=lambda api: api.clients, - available_fn=lambda hub, obj_id: hub.available, control_fn=async_block_client_control_fn, device_info_fn=async_client_device_info_fn, event_is_on=CLIENT_UNBLOCKED, event_to_subscribe=CLIENT_BLOCKED + CLIENT_UNBLOCKED, is_on_fn=lambda hub, client: not client.blocked, - name_fn=lambda client: None, object_fn=lambda api, obj_id: api.clients[obj_id], only_event_for_state_change=True, - should_poll=False, - supported_fn=lambda hub, obj_id: True, unique_id_fn=lambda hub, obj_id: f"block-{obj_id}", ), UnifiSwitchEntityDescription[DPIRestrictionGroups, DPIRestrictionGroup]( key="DPI restriction", + has_entity_name=False, entity_category=EntityCategory.CONFIG, icon="mdi:network", - allowed_fn=lambda hub, obj_id: hub.option_dpi_restrictions, + allowed_fn=lambda hub, obj_id: hub.config.option_dpi_restrictions, api_handler_fn=lambda api: api.dpi_groups, - available_fn=lambda hub, obj_id: hub.available, control_fn=async_dpi_group_control_fn, custom_subscribe=lambda api: api.dpi_apps.subscribe, device_info_fn=async_dpi_group_device_info_fn, - event_is_on=None, - event_to_subscribe=None, is_on_fn=async_dpi_group_is_on_fn, name_fn=lambda group: group.name, object_fn=lambda api, obj_id: api.dpi_groups[obj_id], - should_poll=False, - supported_fn=lambda c, obj_id: bool(c.api.dpi_groups[obj_id].dpiapp_ids), + supported_fn=lambda hub, obj_id: bool(hub.api.dpi_groups[obj_id].dpiapp_ids), unique_id_fn=lambda hub, obj_id: obj_id, ), UnifiSwitchEntityDescription[Outlets, Outlet]( key="Outlet control", device_class=SwitchDeviceClass.OUTLET, - has_entity_name=True, - allowed_fn=lambda hub, obj_id: True, api_handler_fn=lambda api: api.outlets, available_fn=async_device_available_fn, control_fn=async_outlet_control_fn, device_info_fn=async_device_device_info_fn, - event_is_on=None, - event_to_subscribe=None, is_on_fn=lambda hub, outlet: outlet.relay_state, name_fn=lambda outlet: outlet.name, object_fn=lambda api, obj_id: api.outlets[obj_id], - should_poll=False, - supported_fn=async_outlet_supports_switching_fn, + supported_fn=async_outlet_switching_supported_fn, unique_id_fn=lambda hub, obj_id: f"outlet-{obj_id}", ), UnifiSwitchEntityDescription[PortForwarding, PortForward]( key="Port forward control", device_class=SwitchDeviceClass.SWITCH, entity_category=EntityCategory.CONFIG, - has_entity_name=True, icon="mdi:upload-network", - allowed_fn=lambda hub, obj_id: True, api_handler_fn=lambda api: api.port_forwarding, - available_fn=lambda hub, obj_id: hub.available, control_fn=async_port_forward_control_fn, device_info_fn=async_port_forward_device_info_fn, - event_is_on=None, - event_to_subscribe=None, is_on_fn=lambda hub, port_forward: port_forward.enabled, name_fn=lambda port_forward: f"{port_forward.name}", object_fn=lambda api, obj_id: api.port_forwarding[obj_id], - should_poll=False, - supported_fn=lambda hub, obj_id: True, unique_id_fn=lambda hub, obj_id: f"port_forward-{obj_id}", ), UnifiSwitchEntityDescription[Ports, Port]( key="PoE port control", device_class=SwitchDeviceClass.OUTLET, entity_category=EntityCategory.CONFIG, - has_entity_name=True, entity_registry_enabled_default=False, icon="mdi:ethernet", - allowed_fn=lambda hub, obj_id: True, api_handler_fn=lambda api: api.ports, available_fn=async_device_available_fn, control_fn=async_poe_port_control_fn, device_info_fn=async_device_device_info_fn, - event_is_on=None, - event_to_subscribe=None, is_on_fn=lambda hub, port: port.poe_mode != "off", name_fn=lambda port: f"{port.name} PoE", object_fn=lambda api, obj_id: api.ports[obj_id], - should_poll=False, - supported_fn=lambda hub, obj_id: hub.api.ports[obj_id].port_poe, + supported_fn=lambda hub, obj_id: bool(hub.api.ports[obj_id].port_poe), unique_id_fn=lambda hub, obj_id: f"poe-{obj_id}", ), UnifiSwitchEntityDescription[Wlans, Wlan]( key="WLAN control", device_class=SwitchDeviceClass.SWITCH, entity_category=EntityCategory.CONFIG, - has_entity_name=True, icon="mdi:wifi-check", - allowed_fn=lambda hub, obj_id: True, api_handler_fn=lambda api: api.wlans, - available_fn=lambda hub, _: hub.available, control_fn=async_wlan_control_fn, device_info_fn=async_wlan_device_info_fn, - event_is_on=None, - event_to_subscribe=None, is_on_fn=lambda hub, wlan: wlan.enabled, - name_fn=lambda wlan: None, object_fn=lambda api, obj_id: api.wlans[obj_id], - should_poll=False, - supported_fn=lambda hub, obj_id: True, unique_id_fn=lambda hub, obj_id: f"wlan-{obj_id}", ), ) @@ -340,9 +304,7 @@ async def async_setup_entry( ) -> None: """Set up switches for UniFi Network integration.""" async_update_unique_id(hass, config_entry) - UnifiHub.register_platform( - hass, - config_entry, + UnifiHub.get_hub(hass, config_entry).entity_loader.register_platform( async_add_entities, UnifiSwitchEntity, ENTITY_DESCRIPTIONS, @@ -354,15 +316,11 @@ class UnifiSwitchEntity(UnifiEntity[HandlerT, ApiItemT], SwitchEntity): """Base representation of a UniFi switch.""" entity_description: UnifiSwitchEntityDescription[HandlerT, ApiItemT] - only_event_for_state_change = False @callback def async_initiate_state(self) -> None: """Initiate entity state.""" - self.async_update_state(ItemEvent.ADDED, self._obj_id) - self.only_event_for_state_change = ( - self.entity_description.only_event_for_state_change - ) + self.async_update_state(ItemEvent.ADDED, self._obj_id, first_update=True) async def async_turn_on(self, **kwargs: Any) -> None: """Turn on switch.""" @@ -373,12 +331,14 @@ class UnifiSwitchEntity(UnifiEntity[HandlerT, ApiItemT], SwitchEntity): await self.entity_description.control_fn(self.hub, self._obj_id, False) @callback - def async_update_state(self, event: ItemEvent, obj_id: str) -> None: + def async_update_state( + self, event: ItemEvent, obj_id: str, first_update: bool = False + ) -> None: """Update entity state. Update attr_is_on. """ - if self.only_event_for_state_change: + if not first_update and self.entity_description.only_event_for_state_change: return description = self.entity_description diff --git a/homeassistant/components/unifi/update.py b/homeassistant/components/unifi/update.py index b7f33b632b3..a8fe3c83427 100644 --- a/homeassistant/components/unifi/update.py +++ b/homeassistant/components/unifi/update.py @@ -1,10 +1,11 @@ """Update entities for Ubiquiti network devices.""" + from __future__ import annotations from collections.abc import Callable, Coroutine from dataclasses import dataclass import logging -from typing import Any, Generic, TypeVar +from typing import Any, TypeVar import aiounifi from aiounifi.interfaces.api_handlers import ItemEvent @@ -40,40 +41,26 @@ async def async_device_control_fn(api: aiounifi.Controller, obj_id: str) -> None await api.request(DeviceUpgradeRequest.create(obj_id)) -@dataclass(frozen=True) -class UnifiUpdateEntityDescriptionMixin(Generic[_HandlerT, _DataT]): - """Validate and load entities from different UniFi handlers.""" +@dataclass(frozen=True, kw_only=True) +class UnifiUpdateEntityDescription( + UpdateEntityDescription, UnifiEntityDescription[_HandlerT, _DataT] +): + """Class describing UniFi update entity.""" control_fn: Callable[[aiounifi.Controller, str], Coroutine[Any, Any, None]] state_fn: Callable[[aiounifi.Controller, _DataT], bool] -@dataclass(frozen=True) -class UnifiUpdateEntityDescription( - UpdateEntityDescription, - UnifiEntityDescription[_HandlerT, _DataT], - UnifiUpdateEntityDescriptionMixin[_HandlerT, _DataT], -): - """Class describing UniFi update entity.""" - - ENTITY_DESCRIPTIONS: tuple[UnifiUpdateEntityDescription, ...] = ( UnifiUpdateEntityDescription[Devices, Device]( key="Upgrade device", device_class=UpdateDeviceClass.FIRMWARE, - has_entity_name=True, - allowed_fn=lambda hub, obj_id: True, api_handler_fn=lambda api: api.devices, available_fn=async_device_available_fn, control_fn=async_device_control_fn, device_info_fn=async_device_device_info_fn, - event_is_on=None, - event_to_subscribe=None, - name_fn=lambda device: None, object_fn=lambda api, obj_id: api.devices[obj_id], - should_poll=False, state_fn=lambda api, device: device.state == 4, - supported_fn=lambda hub, obj_id: True, unique_id_fn=lambda hub, obj_id: f"device_update-{obj_id}", ), ) @@ -85,9 +72,7 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up update entities for UniFi Network integration.""" - UnifiHub.register_platform( - hass, - config_entry, + UnifiHub.get_hub(hass, config_entry).entity_loader.register_platform( async_add_entities, UnifiDeviceUpdateEntity, ENTITY_DESCRIPTIONS, diff --git a/homeassistant/components/unifi_direct/device_tracker.py b/homeassistant/components/unifi_direct/device_tracker.py index 77ce5d80cf9..51c9c412dad 100644 --- a/homeassistant/components/unifi_direct/device_tracker.py +++ b/homeassistant/components/unifi_direct/device_tracker.py @@ -1,4 +1,5 @@ """Support for Unifi AP direct access.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/unifiled/light.py b/homeassistant/components/unifiled/light.py index f1d3ad15a02..f69ea5712de 100644 --- a/homeassistant/components/unifiled/light.py +++ b/homeassistant/components/unifiled/light.py @@ -1,4 +1,5 @@ """Support for Unifi Led lights.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/unifiprotect/__init__.py b/homeassistant/components/unifiprotect/__init__.py index 076095a16b3..71c887cd870 100644 --- a/homeassistant/components/unifiprotect/__init__.py +++ b/homeassistant/components/unifiprotect/__init__.py @@ -1,4 +1,5 @@ """UniFi Protect Platform.""" + from __future__ import annotations from datetime import timedelta @@ -109,7 +110,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {})[entry.entry_id] = data_service entry.async_on_unload(entry.add_update_listener(_async_options_updated)) entry.async_on_unload( - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, data_service.async_stop) + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, data_service.async_stop, run_immediately=True + ) ) if not entry.options.get(CONF_ALLOW_EA, False) and ( diff --git a/homeassistant/components/unifiprotect/binary_sensor.py b/homeassistant/components/unifiprotect/binary_sensor.py index 4408075468f..f779fc7a1ad 100644 --- a/homeassistant/components/unifiprotect/binary_sensor.py +++ b/homeassistant/components/unifiprotect/binary_sensor.py @@ -1,4 +1,5 @@ """Component providing binary sensors for UniFi Protect.""" + from __future__ import annotations import dataclasses @@ -273,6 +274,15 @@ CAMERA_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( ufp_value="is_glass_break_detection_on", ufp_perm=PermRequired.NO_WRITE, ), + ProtectBinaryEntityDescription( + key="track_person", + name="Tracking: Person", + icon="mdi:walk", + entity_category=EntityCategory.DIAGNOSTIC, + ufp_required_field="is_ptz", + ufp_value="is_person_tracking_enabled", + ufp_perm=PermRequired.NO_WRITE, + ), ) LIGHT_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( @@ -448,6 +458,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = ( name="Package Detected", icon="mdi:package-variant-closed", ufp_value="is_package_currently_detected", + entity_registry_enabled_default=False, ufp_required_field="can_detect_package", ufp_enabled="is_package_detection_on", ufp_event_obj="last_package_detect_event", diff --git a/homeassistant/components/unifiprotect/button.py b/homeassistant/components/unifiprotect/button.py index 2046c12ddbd..db27306aedf 100644 --- a/homeassistant/components/unifiprotect/button.py +++ b/homeassistant/components/unifiprotect/button.py @@ -1,4 +1,5 @@ """Support for Ubiquiti's UniFi Protect NVR.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/homeassistant/components/unifiprotect/camera.py b/homeassistant/components/unifiprotect/camera.py index ca7abaac3c4..1e99bdff541 100644 --- a/homeassistant/components/unifiprotect/camera.py +++ b/homeassistant/components/unifiprotect/camera.py @@ -1,4 +1,5 @@ """Support for Ubiquiti's UniFi Protect NVR.""" + from __future__ import annotations from collections.abc import Generator @@ -17,8 +18,10 @@ from pyunifiprotect.data import ( from homeassistant.components.camera import Camera, CameraEntityFeature from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import issue_registry as ir from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.issue_registry import IssueSeverity from .const import ( ATTR_BITRATE, @@ -32,12 +35,40 @@ from .const import ( ) from .data import ProtectData from .entity import ProtectDeviceEntity -from .utils import async_dispatch_id as _ufpd +from .utils import async_dispatch_id as _ufpd, get_camera_base_name _LOGGER = logging.getLogger(__name__) -def get_camera_channels( +@callback +def _create_rtsp_repair( + hass: HomeAssistant, entry: ConfigEntry, data: ProtectData, camera: UFPCamera +) -> None: + edit_key = "readonly" + if camera.can_write(data.api.bootstrap.auth_user): + edit_key = "writable" + + translation_key = f"rtsp_disabled_{edit_key}" + issue_key = f"rtsp_disabled_{camera.id}" + + ir.async_create_issue( + hass, + DOMAIN, + issue_key, + is_fixable=True, + is_persistent=False, + learn_more_url="https://www.home-assistant.io/integrations/unifiprotect/#camera-streams", + severity=IssueSeverity.WARNING, + translation_key=translation_key, + translation_placeholders={"camera": camera.display_name}, + data={"entry_id": entry.entry_id, "camera_id": camera.id}, + ) + + +@callback +def _get_camera_channels( + hass: HomeAssistant, + entry: ConfigEntry, data: ProtectData, ufp_device: UFPCamera | None = None, ) -> Generator[tuple[UFPCamera, CameraChannel, bool], None, None]: @@ -69,15 +100,23 @@ def get_camera_channels( # no RTSP enabled use first channel with no stream if is_default: + _create_rtsp_repair(hass, entry, data, camera) yield camera, camera.channels[0], True + else: + ir.async_delete_issue(hass, DOMAIN, f"rtsp_disabled_{camera.id}") def _async_camera_entities( - data: ProtectData, ufp_device: UFPCamera | None = None + hass: HomeAssistant, + entry: ConfigEntry, + data: ProtectData, + ufp_device: UFPCamera | None = None, ) -> list[ProtectDeviceEntity]: disable_stream = data.disable_stream entities: list[ProtectDeviceEntity] = [] - for camera, channel, is_default in get_camera_channels(data, ufp_device): + for camera, channel, is_default in _get_camera_channels( + hass, entry, data, ufp_device + ): # do not enable streaming for package camera # 2 FPS causes a lot of buferring entities.append( @@ -118,7 +157,7 @@ async def async_setup_entry( if not isinstance(device, UFPCamera): return # type: ignore[unreachable] - entities = _async_camera_entities(data, ufp_device=device) + entities = _async_camera_entities(hass, entry, data, ufp_device=device) async_add_entities(entities) entry.async_on_unload( @@ -128,7 +167,7 @@ async def async_setup_entry( async_dispatcher_connect(hass, _ufpd(entry, DISPATCH_CHANNELS), _add_new_device) ) - entities = _async_camera_entities(data) + entities = _async_camera_entities(hass, entry, data) async_add_entities(entities) @@ -154,12 +193,13 @@ class ProtectCamera(ProtectDeviceEntity, Camera): super().__init__(data, camera) device = self.device + camera_name = get_camera_base_name(channel) if self._secure: self._attr_unique_id = f"{device.mac}_{channel.id}" - self._attr_name = f"{device.display_name} {channel.name}" + self._attr_name = f"{device.display_name} {camera_name}" else: self._attr_unique_id = f"{device.mac}_{channel.id}_insecure" - self._attr_name = f"{device.display_name} {channel.name} Insecure" + self._attr_name = f"{device.display_name} {camera_name} (Insecure)" # only the default (first) channel is enabled by default self._attr_entity_registry_enabled_default = is_default and secure diff --git a/homeassistant/components/unifiprotect/config_flow.py b/homeassistant/components/unifiprotect/config_flow.py index 29718c8ef35..19561a6003d 100644 --- a/homeassistant/components/unifiprotect/config_flow.py +++ b/homeassistant/components/unifiprotect/config_flow.py @@ -1,4 +1,5 @@ """Config Flow to configure UniFi Protect Integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -13,8 +14,15 @@ from pyunifiprotect.exceptions import ClientError, NotAuthorized from unifi_discovery import async_console_is_alive import voluptuous as vol -from homeassistant import config_entries from homeassistant.components import dhcp, ssdp +from homeassistant.config_entries import ( + SOURCE_IGNORE, + ConfigEntry, + ConfigEntryState, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.const import ( CONF_HOST, CONF_ID, @@ -24,7 +32,6 @@ from homeassistant.const import ( CONF_VERIFY_SSL, ) from homeassistant.core import HomeAssistant, callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import ( async_create_clientsession, async_get_clientsession, @@ -54,8 +61,8 @@ from .utils import _async_resolve, _async_short_mac, _async_unifi_mac_from_hass _LOGGER = logging.getLogger(__name__) ENTRY_FAILURE_STATES = ( - config_entries.ConfigEntryState.SETUP_ERROR, - config_entries.ConfigEntryState.SETUP_RETRY, + ConfigEntryState.SETUP_ERROR, + ConfigEntryState.SETUP_RETRY, ) @@ -72,7 +79,7 @@ def _host_is_direct_connect(host: str) -> bool: async def _async_console_is_offline( hass: HomeAssistant, - entry: config_entries.ConfigEntry, + entry: ConfigEntry, ) -> bool: """Check if a console is offline. @@ -89,7 +96,7 @@ async def _async_console_is_offline( ) -class ProtectFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class ProtectFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a UniFi Protect config flow.""" VERSION = 2 @@ -97,20 +104,24 @@ class ProtectFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Init the config flow.""" super().__init__() - self.entry: config_entries.ConfigEntry | None = None + self.entry: ConfigEntry | None = None self._discovered_device: dict[str, str] = {} - async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: + async def async_step_dhcp( + self, discovery_info: dhcp.DhcpServiceInfo + ) -> ConfigFlowResult: """Handle discovery via dhcp.""" _LOGGER.debug("Starting discovery via: %s", discovery_info) return await self._async_discovery_handoff() - async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: + async def async_step_ssdp( + self, discovery_info: ssdp.SsdpServiceInfo + ) -> ConfigFlowResult: """Handle a discovered UniFi device.""" _LOGGER.debug("Starting discovery via: %s", discovery_info) return await self._async_discovery_handoff() - async def _async_discovery_handoff(self) -> FlowResult: + async def _async_discovery_handoff(self) -> ConfigFlowResult: """Ensure discovery is active.""" # Discovery requires an additional check so we use # SSDP and DHCP to tell us to start it so it only @@ -120,7 +131,7 @@ class ProtectFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_integration_discovery( self, discovery_info: DiscoveryInfoType - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle integration discovery.""" self._discovered_device = discovery_info mac = _async_unifi_mac_from_hass(discovery_info["hw_addr"]) @@ -128,7 +139,7 @@ class ProtectFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): source_ip = discovery_info["source_ip"] direct_connect_domain = discovery_info["direct_connect_domain"] for entry in self._async_current_entries(): - if entry.source == config_entries.SOURCE_IGNORE: + if entry.source == SOURCE_IGNORE: if entry.unique_id == mac: return self.async_abort(reason="already_configured") continue @@ -164,7 +175,7 @@ class ProtectFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_discovery_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm discovery.""" errors: dict[str, str] = {} discovery_info = self._discovered_device @@ -212,13 +223,13 @@ class ProtectFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback def async_get_options_flow( - config_entry: config_entries.ConfigEntry, - ) -> config_entries.OptionsFlow: + config_entry: ConfigEntry, + ) -> OptionsFlow: """Get the options flow for this handler.""" return OptionsFlowHandler(config_entry) @callback - def _async_create_entry(self, title: str, data: dict[str, Any]) -> FlowResult: + def _async_create_entry(self, title: str, data: dict[str, Any]) -> ConfigFlowResult: return self.async_create_entry( title=title, data={**data, CONF_ID: title}, @@ -250,7 +261,8 @@ class ProtectFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): username=user_input[CONF_USERNAME], password=user_input[CONF_PASSWORD], verify_ssl=verify_ssl, - cache_dir=Path(self.hass.config.path(STORAGE_DIR, "unifiprotect_cache")), + cache_dir=Path(self.hass.config.path(STORAGE_DIR, "unifiprotect")), + config_dir=Path(self.hass.config.path(STORAGE_DIR, "unifiprotect")), ) errors = {} @@ -279,7 +291,9 @@ class ProtectFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return nvr_data, errors - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Perform reauth upon an API authentication error.""" self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) @@ -287,7 +301,7 @@ class ProtectFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm reauth.""" errors: dict[str, str] = {} assert self.entry is not None @@ -321,7 +335,7 @@ class ProtectFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initiated by the user.""" errors: dict[str, str] = {} @@ -362,16 +376,16 @@ class ProtectFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) -class OptionsFlowHandler(config_entries.OptionsFlow): +class OptionsFlowHandler(OptionsFlow): """Handle options.""" - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + 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: + ) -> ConfigFlowResult: """Manage the options.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) diff --git a/homeassistant/components/unifiprotect/data.py b/homeassistant/components/unifiprotect/data.py index b82e9ff37f1..c0a6d65ff7a 100644 --- a/homeassistant/components/unifiprotect/data.py +++ b/homeassistant/components/unifiprotect/data.py @@ -1,8 +1,10 @@ """Base class for protect data.""" + from __future__ import annotations from collections.abc import Callable, Generator, Iterable from datetime import datetime, timedelta +from functools import partial import logging from typing import Any, cast @@ -279,11 +281,7 @@ class ProtectData: self._hass, self._async_poll, self._update_interval ) self._subscriptions.setdefault(mac, []).append(update_callback) - - def _unsubscribe() -> None: - self.async_unsubscribe_device_id(mac, update_callback) - - return _unsubscribe + return partial(self.async_unsubscribe_device_id, mac, update_callback) @callback def async_unsubscribe_device_id( @@ -300,12 +298,10 @@ class ProtectData: @callback def _async_signal_device_update(self, device: ProtectDeviceType) -> None: """Call the callbacks for a device_id.""" - - if not self._subscriptions.get(device.mac): + if not (subscriptions := self._subscriptions.get(device.mac)): return - _LOGGER.debug("Updating device: %s (%s)", device.name, device.mac) - for update_callback in self._subscriptions[device.mac]: + for update_callback in subscriptions: update_callback(device) diff --git a/homeassistant/components/unifiprotect/diagnostics.py b/homeassistant/components/unifiprotect/diagnostics.py index 6d4ebcd975d..b85870a08c5 100644 --- a/homeassistant/components/unifiprotect/diagnostics.py +++ b/homeassistant/components/unifiprotect/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for UniFi Network.""" + from __future__ import annotations from typing import Any, cast diff --git a/homeassistant/components/unifiprotect/discovery.py b/homeassistant/components/unifiprotect/discovery.py index 885781c6557..860ebeb2787 100644 --- a/homeassistant/components/unifiprotect/discovery.py +++ b/homeassistant/components/unifiprotect/discovery.py @@ -1,4 +1,5 @@ """The unifiprotect integration discovery.""" + from __future__ import annotations from dataclasses import asdict diff --git a/homeassistant/components/unifiprotect/entity.py b/homeassistant/components/unifiprotect/entity.py index 59c716d4aa4..932cc75b9d0 100644 --- a/homeassistant/components/unifiprotect/entity.py +++ b/homeassistant/components/unifiprotect/entity.py @@ -1,4 +1,5 @@ """Shared Entity definition for UniFi Protect Integration.""" + from __future__ import annotations from collections.abc import Callable, Sequence @@ -194,9 +195,9 @@ class ProtectDeviceEntity(Entity): super().__init__() self.data: ProtectData = data self.device = device - self._async_get_ufp_enabled: Callable[ - [ProtectAdoptableDeviceModel], bool - ] | None = None + self._async_get_ufp_enabled: ( + Callable[[ProtectAdoptableDeviceModel], bool] | None + ) = None if description is None: self._attr_unique_id = f"{self.device.mac}" diff --git a/homeassistant/components/unifiprotect/icons.json b/homeassistant/components/unifiprotect/icons.json new file mode 100644 index 00000000000..b357a892ff4 --- /dev/null +++ b/homeassistant/components/unifiprotect/icons.json @@ -0,0 +1,9 @@ +{ + "services": { + "add_doorbell_text": "mdi:message-plus", + "remove_doorbell_text": "mdi:message-minus", + "set_default_doorbell_text": "mdi:message-processing", + "set_chime_paired_doorbells": "mdi:bell-cog", + "remove_privacy_zone": "mdi:eye-minus" + } +} diff --git a/homeassistant/components/unifiprotect/light.py b/homeassistant/components/unifiprotect/light.py index e068172037a..3ce236b3e23 100644 --- a/homeassistant/components/unifiprotect/light.py +++ b/homeassistant/components/unifiprotect/light.py @@ -1,4 +1,5 @@ """Component providing Lights for UniFi Protect.""" + from __future__ import annotations import logging @@ -44,12 +45,11 @@ async def async_setup_entry( async_dispatcher_connect(hass, _ufpd(entry, DISPATCH_ADOPT), _add_new_device) ) - entities = [] - for device in data.get_by_types({ModelType.LIGHT}): - if device.can_write(data.api.bootstrap.auth_user): - entities.append(ProtectLight(data, device)) - - async_add_entities(entities) + async_add_entities( + ProtectLight(data, device) + for device in data.get_by_types({ModelType.LIGHT}) + if device.can_write(data.api.bootstrap.auth_user) + ) def unifi_brightness_to_hass(value: int) -> int: diff --git a/homeassistant/components/unifiprotect/lock.py b/homeassistant/components/unifiprotect/lock.py index 5bfa65fccf9..c54f9b316ff 100644 --- a/homeassistant/components/unifiprotect/lock.py +++ b/homeassistant/components/unifiprotect/lock.py @@ -1,4 +1,5 @@ """Support for locks on Ubiquiti's UniFi Protect NVR.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index 1eb37befca0..a26fab2e80b 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -37,12 +37,11 @@ } ], "documentation": "https://www.home-assistant.io/integrations/unifiprotect", - "import_executor": true, "integration_type": "hub", "iot_class": "local_push", "loggers": ["pyunifiprotect", "unifi_discovery"], "quality_scale": "platinum", - "requirements": ["pyunifiprotect==5.0.2", "unifi-discovery==1.1.8"], + "requirements": ["pyunifiprotect==5.1.2", "unifi-discovery==1.1.8"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/homeassistant/components/unifiprotect/media_player.py b/homeassistant/components/unifiprotect/media_player.py index 82e2ccd0be0..50fec39e9cb 100644 --- a/homeassistant/components/unifiprotect/media_player.py +++ b/homeassistant/components/unifiprotect/media_player.py @@ -1,4 +1,5 @@ """Support for Ubiquiti's UniFi Protect NVR.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/unifiprotect/migrate.py b/homeassistant/components/unifiprotect/migrate.py index 3a6dde653b4..1fbf8bab8e2 100644 --- a/homeassistant/components/unifiprotect/migrate.py +++ b/homeassistant/components/unifiprotect/migrate.py @@ -1,19 +1,104 @@ """UniFi Protect data migrations.""" + from __future__ import annotations +from itertools import chain import logging from pyunifiprotect import ProtectApiClient -from pyunifiprotect.data import NVR, Bootstrap, ProtectAdoptableDeviceModel +from pyunifiprotect.data import Bootstrap +from typing_extensions import TypedDict +from homeassistant.components.automation import automations_with_entity +from homeassistant.components.script import scripts_with_entity from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform -from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import entity_registry as er, issue_registry as ir +from homeassistant.helpers.issue_registry import IssueSeverity + +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) +class EntityRef(TypedDict): + """Entity ref parameter variable.""" + + id: str + platform: Platform + + +class EntityUsage(TypedDict): + """Entity usages response variable.""" + + automations: dict[str, list[str]] + scripts: dict[str, list[str]] + + +@callback +def check_if_used( + hass: HomeAssistant, entry: ConfigEntry, entities: dict[str, EntityRef] +) -> dict[str, EntityUsage]: + """Check for usages of entities and return them.""" + + entity_registry = er.async_get(hass) + refs: dict[str, EntityUsage] = { + ref: {"automations": {}, "scripts": {}} for ref in entities + } + + for entity in er.async_entries_for_config_entry(entity_registry, entry.entry_id): + for ref_id, ref in entities.items(): + if ( + entity.domain == ref["platform"] + and entity.disabled_by is None + and ref["id"] in entity.unique_id + ): + entity_automations = automations_with_entity(hass, entity.entity_id) + entity_scripts = scripts_with_entity(hass, entity.entity_id) + if entity_automations: + refs[ref_id]["automations"][entity.entity_id] = entity_automations + if entity_scripts: + refs[ref_id]["scripts"][entity.entity_id] = entity_scripts + + return refs + + +@callback +def create_repair_if_used( + hass: HomeAssistant, + entry: ConfigEntry, + breaks_in: str, + entities: dict[str, EntityRef], +) -> None: + """Create repairs for used entities that are deprecated.""" + + usages = check_if_used(hass, entry, entities) + for ref_id, refs in usages.items(): + issue_id = f"deprecate_{ref_id}" + automations = refs["automations"] + scripts = refs["scripts"] + if automations or scripts: + items = sorted( + set(chain.from_iterable(chain(automations.values(), scripts.values()))) + ) + ir.async_create_issue( + hass, + DOMAIN, + issue_id, + is_fixable=False, + breaks_in_ha_version=breaks_in, + severity=IssueSeverity.WARNING, + translation_key=issue_id, + translation_placeholders={ + "items": "* `" + "`\n* `".join(items) + "`\n" + }, + ) + else: + _LOGGER.debug("No found usages of %s", ref_id) + ir.async_delete_issue(hass, DOMAIN, issue_id) + + async def async_migrate_data( hass: HomeAssistant, entry: ConfigEntry, @@ -22,132 +107,32 @@ async def async_migrate_data( ) -> None: """Run all valid UniFi Protect data migrations.""" - _LOGGER.debug("Start Migrate: async_migrate_buttons") - await async_migrate_buttons(hass, entry, protect, bootstrap) - _LOGGER.debug("Completed Migrate: async_migrate_buttons") - - _LOGGER.debug("Start Migrate: async_migrate_device_ids") - await async_migrate_device_ids(hass, entry, protect, bootstrap) - _LOGGER.debug("Completed Migrate: async_migrate_device_ids") + _LOGGER.debug("Start Migrate: async_deprecate_hdr_package") + async_deprecate_hdr_package(hass, entry) + _LOGGER.debug("Completed Migrate: async_deprecate_hdr_package") -async def async_migrate_buttons( - hass: HomeAssistant, - entry: ConfigEntry, - protect: ProtectApiClient, - bootstrap: Bootstrap, -) -> None: - """Migrate existing Reboot button unique IDs from {device_id} to {deivce_id}_reboot. +@callback +def async_deprecate_hdr_package(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Check for usages of hdr_mode switch and package sensor and raise repair if it is used. - This allows for additional types of buttons that are outside of just a reboot button. + UniFi Protect v3.0.22 changed how HDR works so it is no longer a simple on/off toggle. There is + Always On, Always Off and Auto. So it has been migrated to a select. The old switch is now deprecated. - Added in 2022.6.0. + Additionally, the Package sensor is no longer functional due to how events work so a repair to notify users. + + Added in 2024.4.0 """ - registry = er.async_get(hass) - to_migrate = [] - for entity in er.async_entries_for_config_entry(registry, entry.entry_id): - if entity.domain == Platform.BUTTON and "_" not in entity.unique_id: - _LOGGER.debug("Button %s needs migration", entity.entity_id) - to_migrate.append(entity) - - if len(to_migrate) == 0: - _LOGGER.debug("No button entities need migration") - return - - count = 0 - for button in to_migrate: - device = bootstrap.get_device_from_id(button.unique_id) - if device is None: - continue - - new_unique_id = f"{device.id}_reboot" - _LOGGER.debug( - "Migrating entity %s (old unique_id: %s, new unique_id: %s)", - button.entity_id, - button.unique_id, - new_unique_id, - ) - try: - registry.async_update_entity(button.entity_id, new_unique_id=new_unique_id) - except ValueError: - _LOGGER.warning( - "Could not migrate entity %s (old unique_id: %s, new unique_id: %s)", - button.entity_id, - button.unique_id, - new_unique_id, - ) - else: - count += 1 - - if count < len(to_migrate): - _LOGGER.warning("Failed to migate %s reboot buttons", len(to_migrate) - count) - - -async def async_migrate_device_ids( - hass: HomeAssistant, - entry: ConfigEntry, - protect: ProtectApiClient, - bootstrap: Bootstrap, -) -> None: - """Migrate unique IDs from {device_id}_{name} format to {mac}_{name} format. - - This makes devices persist better with in HA. Anything a device is unadopted/readopted or - the Protect instance has to rebuild the disk array, the device IDs of Protect devices - can change. This causes a ton of orphaned entities and loss of historical data. MAC - addresses are the one persistent identifier a device has that does not change. - - Added in 2022.7.0. - """ - - registry = er.async_get(hass) - to_migrate = [] - for entity in er.async_entries_for_config_entry(registry, entry.entry_id): - parts = entity.unique_id.split("_") - # device ID = 24 characters, MAC = 12 - if len(parts[0]) == 24: - _LOGGER.debug("Entity %s needs migration", entity.entity_id) - to_migrate.append(entity) - - if len(to_migrate) == 0: - _LOGGER.debug("No entities need migration to MAC address ID") - return - - count = 0 - for entity in to_migrate: - parts = entity.unique_id.split("_") - if parts[0] == bootstrap.nvr.id: - device: NVR | ProtectAdoptableDeviceModel | None = bootstrap.nvr - else: - device = bootstrap.get_device_from_id(parts[0]) - - if device is None: - continue - - new_unique_id = device.mac - if len(parts) > 1: - new_unique_id = f"{device.mac}_{'_'.join(parts[1:])}" - _LOGGER.debug( - "Migrating entity %s (old unique_id: %s, new unique_id: %s)", - entity.entity_id, - entity.unique_id, - new_unique_id, - ) - try: - registry.async_update_entity(entity.entity_id, new_unique_id=new_unique_id) - except ValueError as err: - _LOGGER.warning( - ( - "Could not migrate entity %s (old unique_id: %s, new unique_id:" - " %s): %s" - ), - entity.entity_id, - entity.unique_id, - new_unique_id, - err, - ) - else: - count += 1 - - if count < len(to_migrate): - _LOGGER.warning("Failed to migrate %s entities", len(to_migrate) - count) + create_repair_if_used( + hass, + entry, + "2024.10.0", + { + "hdr_switch": {"id": "hdr_mode", "platform": Platform.SWITCH}, + "package_sensor": { + "id": "smart_obj_package", + "platform": Platform.BINARY_SENSOR, + }, + }, + ) diff --git a/homeassistant/components/unifiprotect/models.py b/homeassistant/components/unifiprotect/models.py index f7da2f781ff..a9c79556135 100644 --- a/homeassistant/components/unifiprotect/models.py +++ b/homeassistant/components/unifiprotect/models.py @@ -1,4 +1,5 @@ """The unifiprotect integration models.""" + from __future__ import annotations from collections.abc import Callable, Coroutine diff --git a/homeassistant/components/unifiprotect/number.py b/homeassistant/components/unifiprotect/number.py index 68ae3a66d10..49c629ac42f 100644 --- a/homeassistant/components/unifiprotect/number.py +++ b/homeassistant/components/unifiprotect/number.py @@ -1,4 +1,5 @@ """Component providing number entities for UniFi Protect.""" + from __future__ import annotations from dataclasses import dataclass @@ -119,6 +120,20 @@ CAMERA_NUMBERS: tuple[ProtectNumberEntityDescription, ...] = ( ufp_set_method="set_chime_duration", ufp_perm=PermRequired.WRITE, ), + ProtectNumberEntityDescription( + key="icr_lux", + name="Infrared Custom Lux Trigger", + icon="mdi:white-balance-sunny", + entity_category=EntityCategory.CONFIG, + ufp_min=1, + ufp_max=30, + ufp_step=1, + ufp_required_field="feature_flags.has_led_ir", + ufp_value="icr_lux_display", + ufp_set_method="set_icr_custom_lux", + ufp_enabled="is_ir_led_slider_enabled", + ufp_perm=PermRequired.WRITE, + ), ) LIGHT_NUMBERS: tuple[ProtectNumberEntityDescription, ...] = ( diff --git a/homeassistant/components/unifiprotect/repairs.py b/homeassistant/components/unifiprotect/repairs.py index 254984da515..ddd5dc087a1 100644 --- a/homeassistant/components/unifiprotect/repairs.py +++ b/homeassistant/components/unifiprotect/repairs.py @@ -2,10 +2,10 @@ from __future__ import annotations -import logging from typing import cast from pyunifiprotect import ProtectApiClient +from pyunifiprotect.data import Bootstrap, Camera, ModelType from pyunifiprotect.data.types import FirmwareReleaseChannel import voluptuous as vol @@ -18,8 +18,6 @@ from homeassistant.helpers.issue_registry import async_get as async_get_issue_re from .const import CONF_ALLOW_EA from .utils import async_create_api_client -_LOGGER = logging.getLogger(__name__) - class ProtectRepair(RepairsFlow): """Handler for an issue fixing flow.""" @@ -27,7 +25,7 @@ class ProtectRepair(RepairsFlow): _api: ProtectApiClient _entry: ConfigEntry - def __init__(self, api: ProtectApiClient, entry: ConfigEntry) -> None: + def __init__(self, *, api: ProtectApiClient, entry: ConfigEntry) -> None: """Create flow.""" self._api = api @@ -46,7 +44,7 @@ class ProtectRepair(RepairsFlow): return description_placeholders -class EAConfirm(ProtectRepair): +class EAConfirmRepair(ProtectRepair): """Handler for an issue fixing flow.""" async def async_step_init( @@ -92,7 +90,7 @@ class EAConfirm(ProtectRepair): ) -class CloudAccount(ProtectRepair): +class CloudAccountRepair(ProtectRepair): """Handler for an issue fixing flow.""" async def async_step_init( @@ -119,6 +117,108 @@ class CloudAccount(ProtectRepair): return self.async_create_entry(data={}) +class RTSPRepair(ProtectRepair): + """Handler for an issue fixing flow.""" + + _camera_id: str + _camera: Camera | None + _bootstrap: Bootstrap | None + + def __init__( + self, + *, + api: ProtectApiClient, + entry: ConfigEntry, + camera_id: str, + ) -> None: + """Create flow.""" + + super().__init__(api=api, entry=entry) + self._camera_id = camera_id + self._bootstrap = None + self._camera = None + + @callback + def _async_get_placeholders(self) -> dict[str, str]: + description_placeholders = super()._async_get_placeholders() + if self._camera is not None: + description_placeholders["camera"] = self._camera.display_name + + return description_placeholders + + async def _get_boostrap(self) -> Bootstrap: + if self._bootstrap is None: + self._bootstrap = await self._api.get_bootstrap() + + return self._bootstrap + + async def _get_camera(self) -> Camera: + if self._camera is None: + bootstrap = await self._get_boostrap() + self._camera = bootstrap.cameras.get(self._camera_id) + assert self._camera is not None + return self._camera + + async def _enable_rtsp(self) -> None: + camera = await self._get_camera() + bootstrap = await self._get_boostrap() + user = bootstrap.users.get(bootstrap.auth_user_id) + if not user or not camera.can_write(user): + return + + channel = camera.channels[0] + channel.is_rtsp_enabled = True + await self._api.update_device( + ModelType.CAMERA, camera.id, {"channels": camera.unifi_dict()["channels"]} + ) + + 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_start() + + async def async_step_start( + self, user_input: dict[str, str] | None = None + ) -> data_entry_flow.FlowResult: + """Handle the first step of a fix flow.""" + + if user_input is None: + # make sure camera object is loaded for placeholders + await self._get_camera() + placeholders = self._async_get_placeholders() + return self.async_show_form( + step_id="start", + data_schema=vol.Schema({}), + description_placeholders=placeholders, + ) + + updated_camera = await self._api.get_camera(self._camera_id) + if not any(c.is_rtsp_enabled for c in updated_camera.channels): + await self._enable_rtsp() + + updated_camera = await self._api.get_camera(self._camera_id) + if any(c.is_rtsp_enabled for c in updated_camera.channels): + await self.hass.config_entries.async_reload(self._entry.entry_id) + return self.async_create_entry(data={}) + 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(data={}) + + placeholders = self._async_get_placeholders() + return self.async_show_form( + step_id="confirm", + data_schema=vol.Schema({}), + description_placeholders=placeholders, + ) + + async def async_create_fix_flow( hass: HomeAssistant, issue_id: str, @@ -129,10 +229,19 @@ async def async_create_fix_flow( entry_id = cast(str, data["entry_id"]) if (entry := hass.config_entries.async_get_entry(entry_id)) is not None: api = async_create_api_client(hass, entry) - return EAConfirm(api, entry) + return EAConfirmRepair(api=api, entry=entry) + elif data is not None and issue_id == "cloud_user": entry_id = cast(str, data["entry_id"]) if (entry := hass.config_entries.async_get_entry(entry_id)) is not None: api = async_create_api_client(hass, entry) - return CloudAccount(api, entry) + return CloudAccountRepair(api=api, entry=entry) + + elif data is not None and issue_id.startswith("rtsp_disabled_"): + entry_id = cast(str, data["entry_id"]) + camera_id = cast(str, data["camera_id"]) + if (entry := hass.config_entries.async_get_entry(entry_id)) is not None: + api = async_create_api_client(hass, entry) + return RTSPRepair(api=api, entry=entry, camera_id=camera_id) + return ConfirmRepairFlow() diff --git a/homeassistant/components/unifiprotect/select.py b/homeassistant/components/unifiprotect/select.py index e07a174659c..6ba90948fca 100644 --- a/homeassistant/components/unifiprotect/select.py +++ b/homeassistant/components/unifiprotect/select.py @@ -1,4 +1,5 @@ """Component providing select entities for UniFi Protect.""" + from __future__ import annotations from collections.abc import Callable @@ -41,6 +42,12 @@ from .utils import async_dispatch_id as _ufpd, async_get_light_motion_current _LOGGER = logging.getLogger(__name__) _KEY_LIGHT_MOTION = "light_motion" +HDR_MODES = [ + {"id": "always", "name": "Always On"}, + {"id": "off", "name": "Always Off"}, + {"id": "auto", "name": "Auto"}, +] + INFRARED_MODES = [ {"id": IRLEDMode.AUTO.value, "name": "Auto"}, {"id": IRLEDMode.ON.value, "name": "Always Enable"}, @@ -130,8 +137,10 @@ def _get_doorbell_options(api: ProtectApiClient) -> list[dict[str, Any]]: def _get_paired_camera_options(api: ProtectApiClient) -> list[dict[str, Any]]: options = [{"id": TYPE_EMPTY_VALUE, "name": "Not Paired"}] - for camera in api.bootstrap.cameras.values(): - options.append({"id": camera.id, "name": camera.display_name or camera.type}) + options.extend( + {"id": camera.id, "name": camera.display_name or camera.type} + for camera in api.bootstrap.cameras.values() + ) return options @@ -225,6 +234,17 @@ CAMERA_SELECTS: tuple[ProtectSelectEntityDescription, ...] = ( ufp_set_method="set_chime_type", ufp_perm=PermRequired.WRITE, ), + ProtectSelectEntityDescription( + key="hdr_mode", + name="HDR Mode", + icon="mdi:brightness-7", + entity_category=EntityCategory.CONFIG, + ufp_required_field="feature_flags.has_hdr", + ufp_options=HDR_MODES, + ufp_value="hdr_mode_display", + ufp_set_method="set_hdr_mode", + ufp_perm=PermRequired.WRITE, + ), ) LIGHT_SELECTS: tuple[ProtectSelectEntityDescription, ...] = ( diff --git a/homeassistant/components/unifiprotect/sensor.py b/homeassistant/components/unifiprotect/sensor.py index c4d1f8a530d..b19b3daadee 100644 --- a/homeassistant/components/unifiprotect/sensor.py +++ b/homeassistant/components/unifiprotect/sensor.py @@ -1,4 +1,5 @@ """Component providing sensors for UniFi Protect.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/homeassistant/components/unifiprotect/services.py b/homeassistant/components/unifiprotect/services.py index 90a2d5167c5..8c62664f55b 100644 --- a/homeassistant/components/unifiprotect/services.py +++ b/homeassistant/components/unifiprotect/services.py @@ -1,4 +1,5 @@ """UniFi Protect Integration services.""" + from __future__ import annotations import asyncio @@ -7,15 +8,15 @@ from typing import Any, cast from pydantic import ValidationError from pyunifiprotect.api import ProtectApiClient -from pyunifiprotect.data import Chime +from pyunifiprotect.data import Camera, Chime from pyunifiprotect.exceptions import ClientError import voluptuous as vol from homeassistant.components.binary_sensor import BinarySensorDeviceClass from homeassistant.config_entries import ConfigEntryState -from homeassistant.const import ATTR_DEVICE_ID, Platform +from homeassistant.const import ATTR_DEVICE_ID, ATTR_NAME, Platform from homeassistant.core import HomeAssistant, ServiceCall, callback -from homeassistant.exceptions import HomeAssistantError +from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers import ( config_validation as cv, device_registry as dr, @@ -29,6 +30,8 @@ from .data import async_ufp_instance_for_config_entry_ids SERVICE_ADD_DOORBELL_TEXT = "add_doorbell_text" SERVICE_REMOVE_DOORBELL_TEXT = "remove_doorbell_text" +SERVICE_SET_PRIVACY_ZONE = "set_privacy_zone" +SERVICE_REMOVE_PRIVACY_ZONE = "remove_privacy_zone" SERVICE_SET_DEFAULT_DOORBELL_TEXT = "set_default_doorbell_text" SERVICE_SET_CHIME_PAIRED = "set_chime_paired_doorbells" @@ -37,6 +40,7 @@ ALL_GLOBAL_SERIVCES = [ SERVICE_REMOVE_DOORBELL_TEXT, SERVICE_SET_DEFAULT_DOORBELL_TEXT, SERVICE_SET_CHIME_PAIRED, + SERVICE_REMOVE_PRIVACY_ZONE, ] DOORBELL_TEXT_SCHEMA = vol.All( @@ -59,6 +63,16 @@ CHIME_PAIRED_SCHEMA = vol.All( cv.has_at_least_one_key(ATTR_DEVICE_ID), ) +REMOVE_PRIVACY_ZONE_SCHEMA = vol.All( + vol.Schema( + { + **cv.ENTITY_SERVICE_FIELDS, + vol.Required(ATTR_NAME): cv.string, + }, + ), + cv.has_at_least_one_key(ATTR_DEVICE_ID), +) + @callback def _async_get_ufp_instance(hass: HomeAssistant, device_id: str) -> ProtectApiClient: @@ -76,6 +90,21 @@ def _async_get_ufp_instance(hass: HomeAssistant, device_id: str) -> ProtectApiCl raise HomeAssistantError(f"No device found for device id: {device_id}") +@callback +def _async_get_ufp_camera(hass: HomeAssistant, call: ServiceCall) -> Camera: + ref = async_extract_referenced_entity_ids(hass, call) + entity_registry = er.async_get(hass) + + entity_id = ref.indirectly_referenced.pop() + camera_entity = entity_registry.async_get(entity_id) + assert camera_entity is not None + assert camera_entity.device_id is not None + camera_mac = _async_unique_id_to_mac(camera_entity.unique_id) + + instance = _async_get_ufp_instance(hass, camera_entity.device_id) + return cast(Camera, instance.bootstrap.get_device_from_mac(camera_mac)) + + @callback def _async_get_protect_from_call( hass: HomeAssistant, call: ServiceCall @@ -122,6 +151,29 @@ async def set_default_doorbell_text(hass: HomeAssistant, call: ServiceCall) -> N await _async_service_call_nvr(hass, call, "set_default_doorbell_message", message) +async def remove_privacy_zone(hass: HomeAssistant, call: ServiceCall) -> None: + """Remove privacy zone from camera.""" + + name: str = call.data[ATTR_NAME] + camera = _async_get_ufp_camera(hass, call) + + remove_index: int | None = None + for index, zone in enumerate(camera.privacy_zones): + if zone.name == name: + remove_index = index + break + + if remove_index is None: + raise ServiceValidationError( + f"Could not find privacy zone with name {name} on camera {camera.display_name}." + ) + + def remove_zone() -> None: + camera.privacy_zones.pop(remove_index) + + await camera.queue_update(remove_zone) + + @callback def _async_unique_id_to_mac(unique_id: str) -> str: """Extract the MAC address from the registry entry unique id.""" @@ -189,6 +241,11 @@ def async_setup_services(hass: HomeAssistant) -> None: functools.partial(set_chime_paired_doorbells, hass), CHIME_PAIRED_SCHEMA, ), + ( + SERVICE_REMOVE_PRIVACY_ZONE, + functools.partial(remove_privacy_zone, hass), + REMOVE_PRIVACY_ZONE_SCHEMA, + ), ] for name, method, schema in services: if hass.services.has_service(DOMAIN, name): diff --git a/homeassistant/components/unifiprotect/services.yaml b/homeassistant/components/unifiprotect/services.yaml index 6998f540471..e747b9e7240 100644 --- a/homeassistant/components/unifiprotect/services.yaml +++ b/homeassistant/components/unifiprotect/services.yaml @@ -52,3 +52,16 @@ set_chime_paired_doorbells: integration: unifiprotect domain: binary_sensor device_class: occupancy +remove_privacy_zone: + fields: + device_id: + required: true + selector: + device: + integration: unifiprotect + entity: + domain: camera + name: + required: true + selector: + text: diff --git a/homeassistant/components/unifiprotect/strings.json b/homeassistant/components/unifiprotect/strings.json index bdc46217ab5..b83d514f836 100644 --- a/homeassistant/components/unifiprotect/strings.json +++ b/homeassistant/components/unifiprotect/strings.json @@ -90,6 +90,44 @@ } } } + }, + "rtsp_disabled_readonly": { + "title": "RTSPS is disabled on camera {camera}", + "fix_flow": { + "step": { + "start": { + "title": "RTSPS is disabled on camera {camera}", + "description": "RTSPS is disabled on the camera {camera}. RTSPS is required to be able to live stream your camera within Home Assistant. If you do not enable RTSPS, it may create an additional load on your UniFi Protect NVR, as any live video players will default to rapidly pulling snapshots from the camera.\n\nPlease [enable RTSPS]({learn_more}) on the camera and then come back and confirm this repair." + }, + "confirm": { + "title": "[%key:component::unifiprotect::issues::rtsp_disabled_readonly::fix_flow::step::start::title%]", + "description": "Are you sure you want to leave RTSPS disabled for {camera}?" + } + } + } + }, + "rtsp_disabled_writable": { + "title": "RTSPS is disabled on camera {camera}", + "fix_flow": { + "step": { + "start": { + "title": "[%key:component::unifiprotect::issues::rtsp_disabled_readonly::fix_flow::step::start::title%]", + "description": "RTSPS is disabled on the camera {camera}. RTSPS is required to live stream your camera within Home Assistant. If you do not enable RTSPS, it may create an additional load on your UniFi Protect NVR as any live video players will default to rapidly pulling snapshots from the camera.\n\nYou may manually [enable RTSPS]({learn_more}) on your selected camera quality channel or Home Assistant can automatically enable the highest quality channel for you. Confirm this repair once you have enabled the RTSPS channel or if you want Home Assistant to enable the highest quality automatically." + }, + "confirm": { + "title": "[%key:component::unifiprotect::issues::rtsp_disabled_readonly::fix_flow::step::start::title%]", + "description": "[%key:component::unifiprotect::issues::rtsp_disabled_readonly::fix_flow::step::confirm::description%]" + } + } + } + }, + "deprecate_hdr_switch": { + "title": "HDR Mode Switch Deprecated", + "description": "UniFi Protect v3 added a new state for HDR (auto). As a result, the HDR Mode Switch has been replaced with an HDR Mode Select, and it is deprecated.\n\nBelow are the detected automations or scripts that use one or more of the deprecated entities:\n{items}\nThe above list may be incomplete and it does not include any template usages inside of dashboards. Please update any templates, automations or scripts accordingly." + }, + "deprecate_package_sensor": { + "title": "Package Event Sensor Deprecated", + "description": "The package event sensor never tripped because of the way events are reported in UniFi Protect. As a result, the sensor is deprecated and will be removed.\n\nBelow are the detected automations or scripts that use one or more of the deprecated entities:\n{items}\nThe above list may be incomplete and it does not include any template usages inside of dashboards. Please update any templates, automations or scripts accordingly." } }, "entity": { @@ -157,6 +195,20 @@ "description": "The doorbells to link to the chime." } } + }, + "remove_privacy_zone": { + "name": "Remove camera privacy zone", + "description": "Use to remove a privacy zone from a camera.", + "fields": { + "device_id": { + "name": "Camera", + "description": "Camera you want to remove privacy zone from." + }, + "name": { + "name": "Privacy Zone Name", + "description": "The name of the zone to remove." + } + } } } } diff --git a/homeassistant/components/unifiprotect/switch.py b/homeassistant/components/unifiprotect/switch.py index 64890e17d4d..bd7cfa4d2a2 100644 --- a/homeassistant/components/unifiprotect/switch.py +++ b/homeassistant/components/unifiprotect/switch.py @@ -1,4 +1,5 @@ """Component providing Switches for UniFi Protect.""" + from __future__ import annotations from dataclasses import dataclass @@ -73,6 +74,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( name="HDR Mode", icon="mdi:brightness-7", entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, ufp_required_field="feature_flags.has_hdr", ufp_value="hdr_mode", ufp_set_method="set_hdr", @@ -298,6 +300,16 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ufp_set_method="set_glass_break_detection", ufp_perm=PermRequired.WRITE, ), + ProtectSwitchEntityDescription( + key="track_person", + name="Tracking: Person", + icon="mdi:walk", + entity_category=EntityCategory.CONFIG, + ufp_required_field="is_ptz", + ufp_value="is_person_tracking_enabled", + ufp_set_method="set_person_track", + ufp_perm=PermRequired.WRITE, + ), ) PRIVACY_MODE_SWITCH = ProtectSwitchEntityDescription[Camera]( diff --git a/homeassistant/components/unifiprotect/text.py b/homeassistant/components/unifiprotect/text.py index 2aebcfa1da9..584bd511ee5 100644 --- a/homeassistant/components/unifiprotect/text.py +++ b/homeassistant/components/unifiprotect/text.py @@ -1,4 +1,5 @@ """Text entities for UniFi Protect.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/homeassistant/components/unifiprotect/utils.py b/homeassistant/components/unifiprotect/utils.py index f07e1eb9554..8199d729943 100644 --- a/homeassistant/components/unifiprotect/utils.py +++ b/homeassistant/components/unifiprotect/utils.py @@ -1,4 +1,5 @@ """UniFi Protect Integration utils.""" + from __future__ import annotations from collections.abc import Generator, Iterable @@ -12,6 +13,7 @@ from aiohttp import CookieJar from pyunifiprotect import ProtectApiClient from pyunifiprotect.data import ( Bootstrap, + CameraChannel, Light, LightModeEnableType, LightModeType, @@ -143,5 +145,17 @@ def async_create_api_client( override_connection_host=entry.options.get(CONF_OVERRIDE_CHOST, False), ignore_stats=not entry.options.get(CONF_ALL_UPDATES, False), ignore_unadopted=False, - cache_dir=Path(hass.config.path(STORAGE_DIR, "unifiprotect_cache")), + cache_dir=Path(hass.config.path(STORAGE_DIR, "unifiprotect")), + config_dir=Path(hass.config.path(STORAGE_DIR, "unifiprotect")), ) + + +@callback +def get_camera_base_name(channel: CameraChannel) -> str: + """Get base name for cameras channel.""" + + camera_name = channel.name + if channel.name != "Package Camera": + camera_name = f"{channel.name} Resolution Channel" + + return camera_name diff --git a/homeassistant/components/unifiprotect/views.py b/homeassistant/components/unifiprotect/views.py index e05dcde1751..0aa7056976b 100644 --- a/homeassistant/components/unifiprotect/views.py +++ b/homeassistant/components/unifiprotect/views.py @@ -1,4 +1,5 @@ """UniFi Protect Integration views.""" + from __future__ import annotations from datetime import datetime diff --git a/homeassistant/components/universal/icons.json b/homeassistant/components/universal/icons.json new file mode 100644 index 00000000000..a03163179cb --- /dev/null +++ b/homeassistant/components/universal/icons.json @@ -0,0 +1,5 @@ +{ + "services": { + "reload": "mdi:reload" + } +} diff --git a/homeassistant/components/universal/media_player.py b/homeassistant/components/universal/media_player.py index 00f345fd248..90036e5d47c 100644 --- a/homeassistant/components/universal/media_player.py +++ b/homeassistant/components/universal/media_player.py @@ -1,4 +1,5 @@ """Combination of multiple media players for a universal controller.""" + from __future__ import annotations from copy import copy @@ -78,7 +79,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import Event, HomeAssistant, callback from homeassistant.exceptions import TemplateError from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity_component import EntityComponent @@ -92,7 +93,7 @@ from homeassistant.helpers.event import ( ) from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.service import async_call_from_config -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, EventType +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType ATTR_ACTIVE_CHILD = "active_child" @@ -185,7 +186,7 @@ class UniversalMediaPlayer(MediaPlayerEntity): @callback def _async_on_dependency_update( - event: EventType[EventStateChangedData], + event: Event[EventStateChangedData], ) -> None: """Update ha state when dependencies update.""" self.async_set_context(event.context) @@ -193,7 +194,7 @@ class UniversalMediaPlayer(MediaPlayerEntity): @callback def _async_on_template_update( - event: EventType[EventStateChangedData] | None, + event: Event[EventStateChangedData] | None, updates: list[TrackTemplateResult], ) -> None: """Update state when template state changes.""" @@ -657,7 +658,7 @@ class UniversalMediaPlayer(MediaPlayerEntity): component: EntityComponent[MediaPlayerEntity] = self.hass.data[DOMAIN] if entity_id and (entity := component.get_entity(entity_id)): return await entity.async_browse_media(media_content_type, media_content_id) - raise NotImplementedError() + raise NotImplementedError async def async_update(self) -> None: """Update state in HA.""" diff --git a/homeassistant/components/upb/__init__.py b/homeassistant/components/upb/__init__.py index 4e1b003a504..f2db6ff1b3c 100644 --- a/homeassistant/components/upb/__init__.py +++ b/homeassistant/components/upb/__init__.py @@ -1,4 +1,5 @@ """Support the UPB PIM.""" + import upb_lib from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/upb/config_flow.py b/homeassistant/components/upb/config_flow.py index 6d85febed9f..18a427a40bd 100644 --- a/homeassistant/components/upb/config_flow.py +++ b/homeassistant/components/upb/config_flow.py @@ -1,4 +1,5 @@ """Config flow for UPB PIM integration.""" + import asyncio from contextlib import suppress import logging @@ -7,8 +8,9 @@ from urllib.parse import urlparse import upb_lib import voluptuous as vol -from homeassistant import config_entries, exceptions +from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_ADDRESS, CONF_FILE_PATH, CONF_HOST, CONF_PROTOCOL +from homeassistant.exceptions import HomeAssistantError from .const import DOMAIN @@ -70,7 +72,7 @@ def _make_url_from_data(data): return f"{protocol}{address}" -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class UPBConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for UPB PIM.""" VERSION = 1 @@ -128,9 +130,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return urlparse(url).hostname in existing_hosts -class CannotConnect(exceptions.HomeAssistantError): +class CannotConnect(HomeAssistantError): """Error to indicate we cannot connect.""" -class InvalidUpbFile(exceptions.HomeAssistantError): +class InvalidUpbFile(HomeAssistantError): """Error to indicate there is invalid or missing UPB config file.""" diff --git a/homeassistant/components/upb/icons.json b/homeassistant/components/upb/icons.json new file mode 100644 index 00000000000..187f0f60970 --- /dev/null +++ b/homeassistant/components/upb/icons.json @@ -0,0 +1,12 @@ +{ + "services": { + "light_fade_start": "mdi:transition", + "light_fade_stop": "mdi:transition-masked", + "light_blink": "mdi:eye", + "link_deactivate": "mdi:link-off", + "link_goto": "mdi:link-variant", + "link_fade_start": "mdi:transition", + "link_fade_stop": "mdi:transition-masked", + "link_blink": "mdi:eye" + } +} diff --git a/homeassistant/components/upb/light.py b/homeassistant/components/upb/light.py index 50e6d50bb4c..eb20fc949dc 100644 --- a/homeassistant/components/upb/light.py +++ b/homeassistant/components/upb/light.py @@ -1,4 +1,5 @@ """Platform for UPB light integration.""" + from typing import Any from homeassistant.components.light import ( diff --git a/homeassistant/components/upb/scene.py b/homeassistant/components/upb/scene.py index d1272b7a1f6..9cf6788de4f 100644 --- a/homeassistant/components/upb/scene.py +++ b/homeassistant/components/upb/scene.py @@ -1,4 +1,5 @@ """Platform for UPB link integration.""" + from typing import Any from homeassistant.components.scene import Scene diff --git a/homeassistant/components/upc_connect/device_tracker.py b/homeassistant/components/upc_connect/device_tracker.py index 2b5ee2915ef..9e570c9d26b 100644 --- a/homeassistant/components/upc_connect/device_tracker.py +++ b/homeassistant/components/upc_connect/device_tracker.py @@ -1,4 +1,5 @@ """Support for UPC ConnectBox router.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/upcloud/__init__.py b/homeassistant/components/upcloud/__init__.py index 49ec97f073b..371dedab49c 100644 --- a/homeassistant/components/upcloud/__init__.py +++ b/homeassistant/components/upcloud/__init__.py @@ -1,4 +1,5 @@ """Support for UpCloud.""" + from __future__ import annotations import dataclasses diff --git a/homeassistant/components/upcloud/config_flow.py b/homeassistant/components/upcloud/config_flow.py index fda6c1d561b..20860df5553 100644 --- a/homeassistant/components/upcloud/config_flow.py +++ b/homeassistant/components/upcloud/config_flow.py @@ -9,17 +9,21 @@ import requests.exceptions import upcloud_api import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.const import CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult from .const import DEFAULT_SCAN_INTERVAL, DOMAIN _LOGGER = logging.getLogger(__name__) -class UpCloudConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class UpCloudConfigFlow(ConfigFlow, domain=DOMAIN): """UpCloud config flow.""" VERSION = 1 @@ -29,7 +33,7 @@ class UpCloudConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle user initiated flow.""" if user_input is None: return self._async_show_form(step_id="user") @@ -66,7 +70,7 @@ class UpCloudConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id: str, user_input: dict[str, Any] | None = None, errors: dict[str, str] | None = None, - ) -> FlowResult: + ) -> ConfigFlowResult: """Show our form.""" if user_input is None: user_input = {} @@ -88,22 +92,22 @@ class UpCloudConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback def async_get_options_flow( - config_entry: config_entries.ConfigEntry, + config_entry: ConfigEntry, ) -> UpCloudOptionsFlow: """Get options flow.""" return UpCloudOptionsFlow(config_entry) -class UpCloudOptionsFlow(config_entries.OptionsFlow): +class UpCloudOptionsFlow(OptionsFlow): """UpCloud options flow.""" - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + 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: + ) -> ConfigFlowResult: """Handle options flow.""" if user_input is not None: diff --git a/homeassistant/components/update/__init__.py b/homeassistant/components/update/__init__.py index 8ec14b6e3a8..f274da6f412 100644 --- a/homeassistant/components/update/__init__.py +++ b/homeassistant/components/update/__init__.py @@ -1,4 +1,5 @@ """Component to allow for providing device or service updates.""" + from __future__ import annotations from datetime import timedelta @@ -373,7 +374,7 @@ class UpdateEntity( The backup parameter indicates a backup should be taken before installing the update. """ - raise NotImplementedError() + raise NotImplementedError async def async_release_notes(self) -> str | None: """Return full release notes. @@ -389,7 +390,7 @@ class UpdateEntity( This is suitable for a long changelog that does not fit in the release_summary property. The returned string can contain markdown. """ - raise NotImplementedError() + raise NotImplementedError @property @final diff --git a/homeassistant/components/update/const.py b/homeassistant/components/update/const.py index c9340473298..0d7da94f656 100644 --- a/homeassistant/components/update/const.py +++ b/homeassistant/components/update/const.py @@ -1,4 +1,5 @@ """Constants for the update component.""" + from __future__ import annotations from enum import IntFlag diff --git a/homeassistant/components/update/device_trigger.py b/homeassistant/components/update/device_trigger.py index bd0e1a6e1b7..1058acc3ee3 100644 --- a/homeassistant/components/update/device_trigger.py +++ b/homeassistant/components/update/device_trigger.py @@ -1,4 +1,5 @@ """Provides device triggers for update entities.""" + from __future__ import annotations import voluptuous as vol diff --git a/homeassistant/components/update/significant_change.py b/homeassistant/components/update/significant_change.py index 8b37d227a1f..30f6dd3244e 100644 --- a/homeassistant/components/update/significant_change.py +++ b/homeassistant/components/update/significant_change.py @@ -1,4 +1,5 @@ """Helper to test significant update state changes.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/upnp/__init__.py b/homeassistant/components/upnp/__init__.py index 905a3de75f0..f2f3ffd0a1b 100644 --- a/homeassistant/components/upnp/__init__.py +++ b/homeassistant/components/upnp/__init__.py @@ -1,4 +1,5 @@ """UPnP/IGD integration.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/upnp/binary_sensor.py b/homeassistant/components/upnp/binary_sensor.py index 676b9588ddb..71c13d0c8a9 100644 --- a/homeassistant/components/upnp/binary_sensor.py +++ b/homeassistant/components/upnp/binary_sensor.py @@ -1,4 +1,5 @@ """Support for UPnP/IGD Binary Sensors.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/homeassistant/components/upnp/config_flow.py b/homeassistant/components/upnp/config_flow.py index c7882285b9c..a708403b6f2 100644 --- a/homeassistant/components/upnp/config_flow.py +++ b/homeassistant/components/upnp/config_flow.py @@ -1,4 +1,5 @@ """Config flow for UPNP.""" + from __future__ import annotations from collections.abc import Mapping @@ -7,11 +8,10 @@ from urllib.parse import urlparse import voluptuous as vol -from homeassistant import config_entries from homeassistant.components import ssdp from homeassistant.components.ssdp import SsdpServiceInfo +from homeassistant.config_entries import SOURCE_IGNORE, ConfigFlow, ConfigFlowResult from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResult from .const import ( CONFIG_ENTRY_HOST, @@ -74,7 +74,7 @@ def _is_igd_device(discovery_info: ssdp.SsdpServiceInfo) -> bool: return root_device_info.get(ssdp.ATTR_UPNP_DEVICE_TYPE) in {ST_IGD_V1, ST_IGD_V2} -class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class UpnpFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a UPnP/IGD config flow.""" VERSION = 1 @@ -99,7 +99,7 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: Mapping[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow start.""" LOGGER.debug("async_step_user: user_input: %s", user_input) @@ -150,7 +150,9 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): data_schema=data_schema, ) - async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: + async def async_step_ssdp( + self, discovery_info: ssdp.SsdpServiceInfo + ) -> ConfigFlowResult: """Handle a discovered UPnP/IGD device. This flow is triggered by the SSDP component. It will check if the @@ -200,7 +202,7 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): # Check ssdp_st to prevent swapping between IGDv1 and IGDv2. continue - if entry.source == config_entries.SOURCE_IGNORE: + if entry.source == SOURCE_IGNORE: # Host was already ignored. Don't update ignored entries. return self.async_abort(reason="discovery_ignored") @@ -224,7 +226,7 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_ssdp_confirm( self, user_input: Mapping[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm integration via SSDP.""" LOGGER.debug("async_step_ssdp_confirm: user_input: %s", user_input) if user_input is None: @@ -234,7 +236,7 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): discovery = self._remove_discovery(self.unique_id) return await self._async_create_entry_from_discovery(discovery) - async def async_step_ignore(self, user_input: dict[str, Any]) -> FlowResult: + async def async_step_ignore(self, user_input: dict[str, Any]) -> ConfigFlowResult: """Ignore this config flow.""" usn = user_input["unique_id"] discovery = self._remove_discovery(usn) @@ -254,7 +256,7 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def _async_create_entry_from_discovery( self, discovery: SsdpServiceInfo, - ) -> FlowResult: + ) -> ConfigFlowResult: """Create an entry from discovery.""" LOGGER.debug( "_async_create_entry_from_discovery: discovery: %s", diff --git a/homeassistant/components/upnp/const.py b/homeassistant/components/upnp/const.py index 1f33f7cc676..e7b44329546 100644 --- a/homeassistant/components/upnp/const.py +++ b/homeassistant/components/upnp/const.py @@ -1,4 +1,5 @@ """Constants for the IGD component.""" + from datetime import timedelta import logging diff --git a/homeassistant/components/upnp/device.py b/homeassistant/components/upnp/device.py index 2f52a5d008f..4dff753ac6a 100644 --- a/homeassistant/components/upnp/device.py +++ b/homeassistant/components/upnp/device.py @@ -1,4 +1,5 @@ """Home Assistant representation of an UPnP/IGD.""" + from __future__ import annotations from datetime import datetime @@ -37,7 +38,7 @@ def get_preferred_location(locations: set[str]) -> str: """Get the preferred location (an IPv4 location) from a set of locations.""" # Prefer IPv4 over IPv6. for location in locations: - if location.startswith("http://[") or location.startswith("https://["): + if location.startswith(("http://[", "https://[")): continue return location @@ -85,9 +86,9 @@ class Device: """Initialize UPnP/IGD device.""" self.hass = hass self._igd_device = igd_device - self.coordinator: DataUpdateCoordinator[ - dict[str, str | datetime | int | float | None] - ] | None = None + self.coordinator: ( + DataUpdateCoordinator[dict[str, str | datetime | int | float | None]] | None + ) = None self.original_udn: str | None = None async def async_get_mac_address(self) -> str | None: diff --git a/homeassistant/components/upnp/entity.py b/homeassistant/components/upnp/entity.py index 504602372f7..9fef27cb7a1 100644 --- a/homeassistant/components/upnp/entity.py +++ b/homeassistant/components/upnp/entity.py @@ -1,4 +1,5 @@ """Entity for UPnP/IGD.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/homeassistant/components/upnp/icons.json b/homeassistant/components/upnp/icons.json new file mode 100644 index 00000000000..1d4ebaf183d --- /dev/null +++ b/homeassistant/components/upnp/icons.json @@ -0,0 +1,39 @@ +{ + "entity": { + "sensor": { + "data_received": { + "default": "mdi:server-network" + }, + "data_sent": { + "default": "mdi:server-network" + }, + "packets_received": { + "default": "mdi:server-network" + }, + "packets_sent": { + "default": "mdi:server-network" + }, + "external_ip": { + "default": "mdi:server-network" + }, + "uptime": { + "default": "mdi:server-network" + }, + "wan_status": { + "default": "mdi:server-network" + }, + "download_speed": { + "default": "mdi:server-network" + }, + "upload_speed": { + "default": "mdi:server-network" + }, + "packet_download_speed": { + "default": "mdi:server-network" + }, + "packet_upload_speed": { + "default": "mdi:server-network" + } + } + } +} diff --git a/homeassistant/components/upnp/manifest.json b/homeassistant/components/upnp/manifest.json index edfde84a2ac..7d353a475c7 100644 --- a/homeassistant/components/upnp/manifest.json +++ b/homeassistant/components/upnp/manifest.json @@ -8,7 +8,7 @@ "integration_type": "device", "iot_class": "local_polling", "loggers": ["async_upnp_client"], - "requirements": ["async-upnp-client==0.38.2", "getmac==0.9.4"], + "requirements": ["async-upnp-client==0.38.3", "getmac==0.9.4"], "ssdp": [ { "st": "urn:schemas-upnp-org:device:InternetGatewayDevice:1" diff --git a/homeassistant/components/upnp/sensor.py b/homeassistant/components/upnp/sensor.py index e493118f58e..5d72904bfaf 100644 --- a/homeassistant/components/upnp/sensor.py +++ b/homeassistant/components/upnp/sensor.py @@ -1,4 +1,5 @@ """Support for UPnP/IGD Sensors.""" + from __future__ import annotations from dataclasses import dataclass @@ -50,7 +51,6 @@ SENSOR_DESCRIPTIONS: tuple[UpnpSensorEntityDescription, ...] = ( UpnpSensorEntityDescription( key=BYTES_RECEIVED, translation_key="data_received", - icon="mdi:server-network", device_class=SensorDeviceClass.DATA_SIZE, native_unit_of_measurement=UnitOfInformation.BYTES, entity_registry_enabled_default=False, @@ -60,7 +60,6 @@ SENSOR_DESCRIPTIONS: tuple[UpnpSensorEntityDescription, ...] = ( UpnpSensorEntityDescription( key=BYTES_SENT, translation_key="data_sent", - icon="mdi:server-network", device_class=SensorDeviceClass.DATA_SIZE, native_unit_of_measurement=UnitOfInformation.BYTES, entity_registry_enabled_default=False, @@ -70,7 +69,6 @@ SENSOR_DESCRIPTIONS: tuple[UpnpSensorEntityDescription, ...] = ( UpnpSensorEntityDescription( key=PACKETS_RECEIVED, translation_key="packets_received", - icon="mdi:server-network", native_unit_of_measurement=DATA_PACKETS, entity_registry_enabled_default=False, state_class=SensorStateClass.TOTAL_INCREASING, @@ -79,7 +77,6 @@ SENSOR_DESCRIPTIONS: tuple[UpnpSensorEntityDescription, ...] = ( UpnpSensorEntityDescription( key=PACKETS_SENT, translation_key="packets_sent", - icon="mdi:server-network", native_unit_of_measurement=DATA_PACKETS, entity_registry_enabled_default=False, state_class=SensorStateClass.TOTAL_INCREASING, @@ -88,13 +85,11 @@ SENSOR_DESCRIPTIONS: tuple[UpnpSensorEntityDescription, ...] = ( UpnpSensorEntityDescription( key=ROUTER_IP, translation_key="external_ip", - icon="mdi:server-network", entity_category=EntityCategory.DIAGNOSTIC, ), UpnpSensorEntityDescription( key=ROUTER_UPTIME, translation_key="uptime", - icon="mdi:server-network", native_unit_of_measurement=UnitOfTime.SECONDS, entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, @@ -103,7 +98,6 @@ SENSOR_DESCRIPTIONS: tuple[UpnpSensorEntityDescription, ...] = ( UpnpSensorEntityDescription( key=WAN_STATUS, translation_key="wan_status", - icon="mdi:server-network", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), @@ -112,7 +106,6 @@ SENSOR_DESCRIPTIONS: tuple[UpnpSensorEntityDescription, ...] = ( translation_key="download_speed", value_key=KIBIBYTES_PER_SEC_RECEIVED, unique_id="KiB/sec_received", - icon="mdi:server-network", device_class=SensorDeviceClass.DATA_RATE, native_unit_of_measurement=UnitOfDataRate.KIBIBYTES_PER_SECOND, state_class=SensorStateClass.MEASUREMENT, @@ -123,7 +116,6 @@ SENSOR_DESCRIPTIONS: tuple[UpnpSensorEntityDescription, ...] = ( translation_key="upload_speed", value_key=KIBIBYTES_PER_SEC_SENT, unique_id="KiB/sec_sent", - icon="mdi:server-network", device_class=SensorDeviceClass.DATA_RATE, native_unit_of_measurement=UnitOfDataRate.KIBIBYTES_PER_SECOND, state_class=SensorStateClass.MEASUREMENT, @@ -134,7 +126,6 @@ SENSOR_DESCRIPTIONS: tuple[UpnpSensorEntityDescription, ...] = ( translation_key="packet_download_speed", value_key=PACKETS_PER_SEC_RECEIVED, unique_id="packets/sec_received", - icon="mdi:server-network", native_unit_of_measurement=DATA_RATE_PACKETS_PER_SECOND, entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, @@ -145,7 +136,6 @@ SENSOR_DESCRIPTIONS: tuple[UpnpSensorEntityDescription, ...] = ( translation_key="packet_upload_speed", value_key=PACKETS_PER_SEC_SENT, unique_id="packets/sec_sent", - icon="mdi:server-network", native_unit_of_measurement=DATA_RATE_PACKETS_PER_SECOND, entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, diff --git a/homeassistant/components/uptime/__init__.py b/homeassistant/components/uptime/__init__.py index b2f912751f7..3b20abd14d2 100644 --- a/homeassistant/components/uptime/__init__.py +++ b/homeassistant/components/uptime/__init__.py @@ -1,4 +1,5 @@ """The Uptime integration.""" + from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/uptime/config_flow.py b/homeassistant/components/uptime/config_flow.py index edbe6d86f38..2cfec38d200 100644 --- a/homeassistant/components/uptime/config_flow.py +++ b/homeassistant/components/uptime/config_flow.py @@ -1,12 +1,12 @@ """Config flow to configure the Uptime integration.""" + from __future__ import annotations from typing import Any import voluptuous as vol -from homeassistant.config_entries import ConfigFlow -from homeassistant.data_entry_flow import FlowResult +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from .const import DOMAIN @@ -18,7 +18,7 @@ class UptimeConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") diff --git a/homeassistant/components/uptime/const.py b/homeassistant/components/uptime/const.py index 559e0f62273..9db69cbdfe6 100644 --- a/homeassistant/components/uptime/const.py +++ b/homeassistant/components/uptime/const.py @@ -1,4 +1,5 @@ """Constants for the Uptime integration.""" + from typing import Final from homeassistant.const import Platform diff --git a/homeassistant/components/uptime/sensor.py b/homeassistant/components/uptime/sensor.py index 55faf7ccb3a..266542de9d6 100644 --- a/homeassistant/components/uptime/sensor.py +++ b/homeassistant/components/uptime/sensor.py @@ -1,4 +1,5 @@ """Platform to retrieve uptime for Home Assistant.""" + from __future__ import annotations from homeassistant.components.sensor import SensorDeviceClass, SensorEntity diff --git a/homeassistant/components/uptimerobot/__init__.py b/homeassistant/components/uptimerobot/__init__.py index 58979d7defb..afff0c8fe03 100644 --- a/homeassistant/components/uptimerobot/__init__.py +++ b/homeassistant/components/uptimerobot/__init__.py @@ -1,4 +1,5 @@ """The UptimeRobot integration.""" + from __future__ import annotations from pyuptimerobot import UptimeRobot @@ -18,7 +19,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up UptimeRobot from a config entry.""" hass.data.setdefault(DOMAIN, {}) key: str = entry.data[CONF_API_KEY] - if key.startswith("ur") or key.startswith("m"): + if key.startswith(("ur", "m")): raise ConfigEntryAuthFailed( "Wrong API key type detected, use the 'main' API key" ) diff --git a/homeassistant/components/uptimerobot/binary_sensor.py b/homeassistant/components/uptimerobot/binary_sensor.py index 2710d5166c2..0c1bd972387 100644 --- a/homeassistant/components/uptimerobot/binary_sensor.py +++ b/homeassistant/components/uptimerobot/binary_sensor.py @@ -1,4 +1,5 @@ """UptimeRobot binary_sensor platform.""" + from __future__ import annotations from homeassistant.components.binary_sensor import ( diff --git a/homeassistant/components/uptimerobot/config_flow.py b/homeassistant/components/uptimerobot/config_flow.py index 14ec1ae6cdc..feb747c6b9e 100644 --- a/homeassistant/components/uptimerobot/config_flow.py +++ b/homeassistant/components/uptimerobot/config_flow.py @@ -1,4 +1,5 @@ """Config flow for UptimeRobot integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -14,9 +15,8 @@ from pyuptimerobot import ( ) import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult 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 API_ATTR_OK, DOMAIN, LOGGER @@ -24,7 +24,7 @@ from .const import API_ATTR_OK, DOMAIN, LOGGER STEP_USER_DATA_SCHEMA = vol.Schema({vol.Required(CONF_API_KEY): str}) -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class UptimeRobotConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for UptimeRobot.""" VERSION = 1 @@ -36,7 +36,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors: dict[str, str] = {} response: UptimeRobotApiResponse | UptimeRobotApiError | None = None key: str = data[CONF_API_KEY] - if key.startswith("ur") or key.startswith("m"): + if key.startswith(("ur", "m")): LOGGER.error("Wrong API key type detected, use the 'main' API key") errors["base"] = "not_main_key" return errors, None @@ -68,7 +68,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" if user_input is None: return self.async_show_form( @@ -85,13 +85,15 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors ) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Return the reauth confirm step.""" return await self.async_step_reauth_confirm() async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Dialog that informs the user that reauth is required.""" if user_input is None: return self.async_show_form( diff --git a/homeassistant/components/uptimerobot/const.py b/homeassistant/components/uptimerobot/const.py index e89c1c38e0e..1ac234afa64 100644 --- a/homeassistant/components/uptimerobot/const.py +++ b/homeassistant/components/uptimerobot/const.py @@ -1,4 +1,5 @@ """Constants for the UptimeRobot integration.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/uptimerobot/coordinator.py b/homeassistant/components/uptimerobot/coordinator.py index 4c1d3ea2c78..3069884eb99 100644 --- a/homeassistant/components/uptimerobot/coordinator.py +++ b/homeassistant/components/uptimerobot/coordinator.py @@ -1,4 +1,5 @@ """DataUpdateCoordinator for the uptimerobot integration.""" + from __future__ import annotations from pyuptimerobot import ( diff --git a/homeassistant/components/uptimerobot/diagnostics.py b/homeassistant/components/uptimerobot/diagnostics.py index 15173a5e43c..23c65373045 100644 --- a/homeassistant/components/uptimerobot/diagnostics.py +++ b/homeassistant/components/uptimerobot/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for UptimeRobot.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/uptimerobot/entity.py b/homeassistant/components/uptimerobot/entity.py index 3057bd7c220..71f7a2f1c00 100644 --- a/homeassistant/components/uptimerobot/entity.py +++ b/homeassistant/components/uptimerobot/entity.py @@ -1,4 +1,5 @@ """Base UptimeRobot entity.""" + from __future__ import annotations from pyuptimerobot import UptimeRobotMonitor diff --git a/homeassistant/components/uptimerobot/icons.json b/homeassistant/components/uptimerobot/icons.json new file mode 100644 index 00000000000..36bd3732f41 --- /dev/null +++ b/homeassistant/components/uptimerobot/icons.json @@ -0,0 +1,20 @@ +{ + "entity": { + "sensor": { + "monitor_status": { + "default": "mdi:television", + "state": { + "pause": "mdi:television-pause", + "up": "mdi:television-shimmer", + "seems_down": "mdi:television-off", + "down": "mdi:television-off" + } + } + }, + "switch": { + "monitor_status": { + "default": "mdi:cog" + } + } + } +} diff --git a/homeassistant/components/uptimerobot/sensor.py b/homeassistant/components/uptimerobot/sensor.py index 4ae40bf4134..c5ff8abf5d9 100644 --- a/homeassistant/components/uptimerobot/sensor.py +++ b/homeassistant/components/uptimerobot/sensor.py @@ -1,7 +1,6 @@ """UptimeRobot sensor platform.""" -from __future__ import annotations -from typing import TypedDict +from __future__ import annotations from homeassistant.components.sensor import ( SensorDeviceClass, @@ -17,20 +16,12 @@ from .const import DOMAIN from .coordinator import UptimeRobotDataUpdateCoordinator from .entity import UptimeRobotEntity - -class StatusValue(TypedDict): - """Sensor details.""" - - value: str - icon: str - - SENSORS_INFO = { - 0: StatusValue(value="pause", icon="mdi:television-pause"), - 1: StatusValue(value="not_checked_yet", icon="mdi:television"), - 2: StatusValue(value="up", icon="mdi:television-shimmer"), - 8: StatusValue(value="seems_down", icon="mdi:television-off"), - 9: StatusValue(value="down", icon="mdi:television-off"), + 0: "pause", + 1: "not_checked_yet", + 2: "up", + 8: "seems_down", + 9: "down", } @@ -63,9 +54,4 @@ class UptimeRobotSensor(UptimeRobotEntity, SensorEntity): @property def native_value(self) -> str: """Return the status of the monitor.""" - return SENSORS_INFO[self.monitor.status]["value"] - - @property - def icon(self) -> str: - """Return the status of the monitor.""" - return SENSORS_INFO[self.monitor.status]["icon"] + return SENSORS_INFO[self.monitor.status] diff --git a/homeassistant/components/uptimerobot/switch.py b/homeassistant/components/uptimerobot/switch.py index 3406c9fe21a..aa7d07e10fd 100644 --- a/homeassistant/components/uptimerobot/switch.py +++ b/homeassistant/components/uptimerobot/switch.py @@ -1,4 +1,5 @@ """UptimeRobot switch platform.""" + from __future__ import annotations from typing import Any @@ -40,7 +41,7 @@ async def async_setup_entry( class UptimeRobotSwitch(UptimeRobotEntity, SwitchEntity): """Representation of a UptimeRobot switch.""" - _attr_icon = "mdi:cog" + _attr_translation_key = "monitor_status" @property def is_on(self) -> bool: diff --git a/homeassistant/components/usb/__init__.py b/homeassistant/components/usb/__init__.py index 916e9b1ea32..48697c98ae7 100644 --- a/homeassistant/components/usb/__init__.py +++ b/homeassistant/components/usb/__init__.py @@ -1,4 +1,5 @@ """The USB Discovery integration.""" + from __future__ import annotations from collections.abc import Coroutine @@ -34,7 +35,7 @@ from .models import USBDevice from .utils import usb_device_from_port if TYPE_CHECKING: - from pyudev import Device + from pyudev import Device, MonitorObserver _LOGGER = logging.getLogger(__name__) @@ -206,8 +207,12 @@ class USBDiscovery: async def async_setup(self) -> None: """Set up USB Discovery.""" await self._async_start_monitor() - self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, self.async_start) - self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.async_stop) + self.hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STARTED, self.async_start, run_immediately=True + ) + self.hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, self.async_stop, run_immediately=True + ) async def async_start(self, event: Event) -> None: """Start USB Discovery and run a manual scan.""" @@ -227,6 +232,27 @@ class USBDiscovery: if info.get("docker"): return + if not ( + observer := await self.hass.async_add_executor_job( + self._get_monitor_observer + ) + ): + return + + def _stop_observer(event: Event) -> None: + observer.stop() + + self.hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, _stop_observer, run_immediately=True + ) + self.observer_active = True + + def _get_monitor_observer(self) -> MonitorObserver | None: + """Get the monitor observer. + + This runs in the executor because the import + does blocking I/O. + """ from pyudev import ( # pylint: disable=import-outside-toplevel Context, Monitor, @@ -236,7 +262,7 @@ class USBDiscovery: try: context = Context() except (ImportError, OSError): - return + return None monitor = Monitor.from_netlink(context) try: @@ -245,17 +271,14 @@ class USBDiscovery: _LOGGER.debug( "Unable to setup pyudev filtering; This is expected on WSL: %s", ex ) - return + return None + observer = MonitorObserver( monitor, callback=self._device_discovered, name="usb-observer" ) + observer.start() - - def _stop_observer(event: Event) -> None: - observer.stop() - - self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _stop_observer) - self.observer_active = True + return observer def _device_discovered(self, device: Device) -> None: """Call when the observer discovers a new usb tty device.""" diff --git a/homeassistant/components/usb/manifest.json b/homeassistant/components/usb/manifest.json index cd8c801d50c..71df5ba2c05 100644 --- a/homeassistant/components/usb/manifest.json +++ b/homeassistant/components/usb/manifest.json @@ -4,7 +4,6 @@ "codeowners": ["@bdraco"], "dependencies": ["websocket_api"], "documentation": "https://www.home-assistant.io/integrations/usb", - "import_executor": true, "integration_type": "system", "iot_class": "local_push", "quality_scale": "internal", diff --git a/homeassistant/components/usb/models.py b/homeassistant/components/usb/models.py index bdc8bc71ced..efc5b11c26e 100644 --- a/homeassistant/components/usb/models.py +++ b/homeassistant/components/usb/models.py @@ -1,4 +1,5 @@ """Models helper class for the usb integration.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/homeassistant/components/usb/utils.py b/homeassistant/components/usb/utils.py index d6bd96882b2..d1d6fb17f3c 100644 --- a/homeassistant/components/usb/utils.py +++ b/homeassistant/components/usb/utils.py @@ -1,4 +1,5 @@ """The USB Discovery integration.""" + from __future__ import annotations from serial.tools.list_ports_common import ListPortInfo diff --git a/homeassistant/components/usgs_earthquakes_feed/geo_location.py b/homeassistant/components/usgs_earthquakes_feed/geo_location.py index 1c6c1b04231..c8ee88a84ed 100644 --- a/homeassistant/components/usgs_earthquakes_feed/geo_location.py +++ b/homeassistant/components/usgs_earthquakes_feed/geo_location.py @@ -1,4 +1,5 @@ """Support for U.S. Geological Survey Earthquake Hazards Program Feeds.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/utility_meter/__init__.py b/homeassistant/components/utility_meter/__init__.py index a3b489dc55c..71df488de7e 100644 --- a/homeassistant/components/utility_meter/__init__.py +++ b/homeassistant/components/utility_meter/__init__.py @@ -1,4 +1,5 @@ """Support for tracking consumption over given periods of time.""" + from datetime import timedelta import logging @@ -163,8 +164,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: ) ) - hass.data[DATA_UTILITY][meter][CONF_TARIFF_ENTITY] = "{}.{}".format( - SELECT_DOMAIN, meter + hass.data[DATA_UTILITY][meter][CONF_TARIFF_ENTITY] = ( + f"{SELECT_DOMAIN}.{meter}" ) # add one meter for each tariff @@ -212,9 +213,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entity_entry = entity_registry.async_get_or_create( Platform.SELECT, DOMAIN, entry.entry_id, suggested_object_id=entry.title ) - hass.data[DATA_UTILITY][entry.entry_id][ - CONF_TARIFF_ENTITY - ] = entity_entry.entity_id + hass.data[DATA_UTILITY][entry.entry_id][CONF_TARIFF_ENTITY] = ( + entity_entry.entity_id + ) await hass.config_entries.async_forward_entry_setups( entry, (Platform.SELECT, Platform.SENSOR) ) diff --git a/homeassistant/components/utility_meter/config_flow.py b/homeassistant/components/utility_meter/config_flow.py index 0ca9ee12f58..e8acca88cbe 100644 --- a/homeassistant/components/utility_meter/config_flow.py +++ b/homeassistant/components/utility_meter/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Utility Meter integration.""" + from __future__ import annotations from collections.abc import Mapping diff --git a/homeassistant/components/utility_meter/const.py b/homeassistant/components/utility_meter/const.py index 4f62925069d..49799ba1e67 100644 --- a/homeassistant/components/utility_meter/const.py +++ b/homeassistant/components/utility_meter/const.py @@ -1,4 +1,5 @@ """Constants for the utility meter component.""" + DOMAIN = "utility_meter" QUARTER_HOURLY = "quarter-hourly" diff --git a/homeassistant/components/utility_meter/icons.json b/homeassistant/components/utility_meter/icons.json index 7260fbfbe96..3c447b4a810 100644 --- a/homeassistant/components/utility_meter/icons.json +++ b/homeassistant/components/utility_meter/icons.json @@ -10,5 +10,9 @@ "default": "mdi:clock-outline" } } + }, + "services": { + "reset": "mdi:numeric-0-box-outline", + "calibrate": "mdi:auto-fix" } } diff --git a/homeassistant/components/utility_meter/select.py b/homeassistant/components/utility_meter/select.py index 86433ca77f8..461fee3ba9f 100644 --- a/homeassistant/components/utility_meter/select.py +++ b/homeassistant/components/utility_meter/select.py @@ -1,4 +1,5 @@ """Support for tariff selection.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/utility_meter/sensor.py b/homeassistant/components/utility_meter/sensor.py index e9ad7a1ba30..26582df1b44 100644 --- a/homeassistant/components/utility_meter/sensor.py +++ b/homeassistant/components/utility_meter/sensor.py @@ -1,6 +1,8 @@ """Utility meter from sensors providing raw data.""" + from __future__ import annotations +from collections.abc import Mapping from dataclasses import dataclass from datetime import datetime, timedelta from decimal import Decimal, DecimalException, InvalidOperation @@ -12,6 +14,7 @@ import voluptuous as vol from homeassistant.components.sensor import ( ATTR_LAST_RESET, + DEVICE_CLASS_UNITS, RestoreSensor, SensorDeviceClass, SensorExtraStoredData, @@ -20,14 +23,14 @@ from homeassistant.components.sensor import ( from homeassistant.components.sensor.recorder import _suggest_report_issue from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( + ATTR_DEVICE_CLASS, ATTR_UNIT_OF_MEASUREMENT, CONF_NAME, CONF_UNIQUE_ID, STATE_UNAVAILABLE, STATE_UNKNOWN, - UnitOfEnergy, ) -from homeassistant.core import HomeAssistant, State, callback +from homeassistant.core import Event, HomeAssistant, State, callback from homeassistant.helpers import ( device_registry as dr, entity_platform, @@ -43,9 +46,10 @@ from homeassistant.helpers.event import ( ) from homeassistant.helpers.start import async_at_started from homeassistant.helpers.template import is_number -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, EventType +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import slugify import homeassistant.util.dt as dt_util +from homeassistant.util.enum import try_parse_enum from .const import ( ATTR_CRON_PATTERN, @@ -96,12 +100,6 @@ ATTR_LAST_PERIOD = "last_period" ATTR_LAST_VALID_STATE = "last_valid_state" ATTR_TARIFF = "tariff" -DEVICE_CLASS_MAP = { - UnitOfEnergy.WATT_HOUR: SensorDeviceClass.ENERGY, - UnitOfEnergy.KILO_WATT_HOUR: SensorDeviceClass.ENERGY, -} - - PRECISION = 3 PAUSED = "paused" COLLECTING = "collecting" @@ -312,6 +310,7 @@ class UtilitySensorExtraStoredData(SensorExtraStoredData): last_reset: datetime | None last_valid_state: Decimal | None status: str + input_device_class: SensorDeviceClass | None def as_dict(self) -> dict[str, Any]: """Return a dict representation of the utility sensor data.""" @@ -323,6 +322,7 @@ class UtilitySensorExtraStoredData(SensorExtraStoredData): str(self.last_valid_state) if self.last_valid_state else None ) data["status"] = self.status + data["input_device_class"] = str(self.input_device_class) return data @@ -342,6 +342,9 @@ class UtilitySensorExtraStoredData(SensorExtraStoredData): else None ) status: str = restored["status"] + input_device_class = try_parse_enum( + SensorDeviceClass, restored.get("input_device_class") + ) except KeyError: # restored is a dict, but does not have all values return None @@ -356,6 +359,7 @@ class UtilitySensorExtraStoredData(SensorExtraStoredData): last_reset, last_valid_state, status, + input_device_class, ) @@ -396,6 +400,7 @@ class UtilityMeterSensor(RestoreSensor): self._last_valid_state = None self._collecting = None self._name = name + self._input_device_class = None self._unit_of_measurement = None self._period = meter_type if meter_type is not None: @@ -415,9 +420,10 @@ class UtilityMeterSensor(RestoreSensor): self._tariff = tariff self._tariff_entity = tariff_entity - def start(self, unit): + def start(self, attributes: Mapping[str, Any]) -> None: """Initialize unit and state upon source initial update.""" - self._unit_of_measurement = unit + self._input_device_class = attributes.get(ATTR_DEVICE_CLASS) + self._unit_of_measurement = attributes.get(ATTR_UNIT_OF_MEASUREMENT) self._state = 0 self.async_write_ha_state() @@ -465,7 +471,7 @@ class UtilityMeterSensor(RestoreSensor): return None @callback - def async_reading(self, event: EventType[EventStateChangedData]) -> None: + def async_reading(self, event: Event[EventStateChangedData]) -> None: """Handle the sensor state changes.""" if ( source_state := self.hass.states.get(self._sensor_source_id) @@ -481,6 +487,7 @@ class UtilityMeterSensor(RestoreSensor): new_state = event.data["new_state"] if new_state is None: return + new_state_attributes: Mapping[str, Any] = new_state.attributes or {} # First check if the new_state is valid (see discussion in PR #88446) if (new_state_val := self._validate_state(new_state)) is None: @@ -497,7 +504,7 @@ class UtilityMeterSensor(RestoreSensor): for sensor in self.hass.data[DATA_UTILITY][self._parent_meter][ DATA_TARIFF_SENSORS ]: - sensor.start(new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)) + sensor.start(new_state_attributes) if self._unit_of_measurement is None: _LOGGER.warning( "Source sensor %s has no unit of measurement. Please %s", @@ -511,12 +518,13 @@ class UtilityMeterSensor(RestoreSensor): # If net_consumption is off, the adjustment must be non-negative self._state += adjustment # type: ignore[operator] # self._state will be set to by the start function if it is None, therefore it always has a valid Decimal value at this line - self._unit_of_measurement = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + self._input_device_class = new_state_attributes.get(ATTR_DEVICE_CLASS) + self._unit_of_measurement = new_state_attributes.get(ATTR_UNIT_OF_MEASUREMENT) self._last_valid_state = new_state_val self.async_write_ha_state() @callback - def async_tariff_change(self, event: EventType[EventStateChangedData]) -> None: + def async_tariff_change(self, event: Event[EventStateChangedData]) -> None: """Handle tariff changes.""" if (new_state := event.data["new_state"]) is None: return @@ -570,7 +578,7 @@ class UtilityMeterSensor(RestoreSensor): async def async_reset_meter(self, entity_id): """Reset meter.""" - if self._tariff_entity != entity_id: + if self._tariff is not None and self._tariff_entity != entity_id: return _LOGGER.debug("Reset utility meter <%s>", self.entity_id) self._last_reset = dt_util.utcnow() @@ -599,6 +607,7 @@ class UtilityMeterSensor(RestoreSensor): if (last_sensor_data := await self.async_get_last_sensor_data()) is not None: # new introduced in 2022.04 self._state = last_sensor_data.native_value + self._input_device_class = last_sensor_data.input_device_class self._unit_of_measurement = last_sensor_data.native_unit_of_measurement self._last_period = last_sensor_data.last_period self._last_reset = last_sensor_data.last_reset @@ -692,7 +701,11 @@ class UtilityMeterSensor(RestoreSensor): @property def device_class(self): """Return the device class of the sensor.""" - return DEVICE_CLASS_MAP.get(self._unit_of_measurement) + if self._input_device_class is not None: + return self._input_device_class + if self._unit_of_measurement in DEVICE_CLASS_UNITS[SensorDeviceClass.ENERGY]: + return SensorDeviceClass.ENERGY + return None @property def state_class(self): @@ -743,6 +756,7 @@ class UtilityMeterSensor(RestoreSensor): self._last_reset, self._last_valid_state, PAUSED if self._collecting is None else COLLECTING, + self._input_device_class, ) async def async_get_last_sensor_data(self) -> UtilitySensorExtraStoredData | None: diff --git a/homeassistant/components/uvc/camera.py b/homeassistant/components/uvc/camera.py index cecec49b36b..307db17c2b8 100644 --- a/homeassistant/components/uvc/camera.py +++ b/homeassistant/components/uvc/camera.py @@ -1,4 +1,5 @@ """Support for Ubiquiti's UVC cameras.""" + from __future__ import annotations from datetime import datetime diff --git a/homeassistant/components/v2c/__init__.py b/homeassistant/components/v2c/__init__.py index ea82a9b64fc..75d306b392a 100644 --- a/homeassistant/components/v2c/__init__.py +++ b/homeassistant/components/v2c/__init__.py @@ -1,4 +1,5 @@ """The V2C integration.""" + from __future__ import annotations from pytrydan import Trydan diff --git a/homeassistant/components/v2c/binary_sensor.py b/homeassistant/components/v2c/binary_sensor.py index b30c632174a..203cc9f3396 100644 --- a/homeassistant/components/v2c/binary_sensor.py +++ b/homeassistant/components/v2c/binary_sensor.py @@ -1,4 +1,5 @@ """Support for V2C binary sensors.""" + from __future__ import annotations from collections.abc import Callable @@ -20,20 +21,13 @@ from .coordinator import V2CUpdateCoordinator from .entity import V2CBaseEntity -@dataclass(frozen=True) -class V2CRequiredKeysMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class V2CBinarySensorEntityDescription(BinarySensorEntityDescription): + """Describes an EVSE binary sensor entity.""" value_fn: Callable[[Trydan], bool] -@dataclass(frozen=True) -class V2CBinarySensorEntityDescription( - BinarySensorEntityDescription, V2CRequiredKeysMixin -): - """Describes an EVSE binary sensor entity.""" - - TRYDAN_SENSORS = ( V2CBinarySensorEntityDescription( key="connected", diff --git a/homeassistant/components/v2c/config_flow.py b/homeassistant/components/v2c/config_flow.py index 382b41d3994..4d798795cbe 100644 --- a/homeassistant/components/v2c/config_flow.py +++ b/homeassistant/components/v2c/config_flow.py @@ -1,4 +1,5 @@ """Config flow for V2C integration.""" + from __future__ import annotations import logging @@ -8,9 +9,8 @@ from pytrydan import Trydan from pytrydan.exceptions import TrydanError import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.httpx_client import get_async_client from .const import DOMAIN @@ -24,14 +24,14 @@ STEP_USER_DATA_SCHEMA = vol.Schema( ) -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class V2CConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for V2C.""" VERSION = 1 async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors: dict[str, str] = {} if user_input is not None: diff --git a/homeassistant/components/v2c/coordinator.py b/homeassistant/components/v2c/coordinator.py index f61d58b844d..b121c84563c 100644 --- a/homeassistant/components/v2c/coordinator.py +++ b/homeassistant/components/v2c/coordinator.py @@ -1,4 +1,5 @@ """The v2c component.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/v2c/entity.py b/homeassistant/components/v2c/entity.py index ee3c94d8d0c..e71c4d5d7c5 100644 --- a/homeassistant/components/v2c/entity.py +++ b/homeassistant/components/v2c/entity.py @@ -1,4 +1,5 @@ """Support for V2C EVSE.""" + from __future__ import annotations from pytrydan import TrydanData diff --git a/homeassistant/components/v2c/icons.json b/homeassistant/components/v2c/icons.json new file mode 100644 index 00000000000..0c0609de347 --- /dev/null +++ b/homeassistant/components/v2c/icons.json @@ -0,0 +1,38 @@ +{ + "entity": { + "sensor": { + "charge_power": { + "default": "mdi:ev-station" + }, + "charge_energy": { + "default": "mdi:ev-station" + }, + "charge_time": { + "default": "mdi:timer" + }, + "house_power": { + "default": "mdi:home-lightning-bolt" + }, + "fv_power": { + "default": "mdi:solar-power-variant" + } + }, + "switch": { + "paused": { + "default": "mdi:pause" + }, + "locked": { + "default": "mdi:lock" + }, + "timer": { + "default": "mdi:timer" + }, + "dynamic": { + "default": "mdi:gauge" + }, + "pause_dynamic": { + "default": "mdi:pause" + } + } + } +} diff --git a/homeassistant/components/v2c/number.py b/homeassistant/components/v2c/number.py index dd20b0de787..376509c4780 100644 --- a/homeassistant/components/v2c/number.py +++ b/homeassistant/components/v2c/number.py @@ -1,4 +1,5 @@ """Number platform for V2C settings.""" + from __future__ import annotations from collections.abc import Callable, Coroutine @@ -24,21 +25,14 @@ MIN_INTENSITY = 6 MAX_INTENSITY = 32 -@dataclass(frozen=True) -class V2CSettingsRequiredKeysMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class V2CSettingsNumberEntityDescription(NumberEntityDescription): + """Describes V2C EVSE number entity.""" value_fn: Callable[[TrydanData], int] update_fn: Callable[[Trydan, int], Coroutine[Any, Any, None]] -@dataclass(frozen=True) -class V2CSettingsNumberEntityDescription( - NumberEntityDescription, V2CSettingsRequiredKeysMixin -): - """Describes V2C EVSE number entity.""" - - TRYDAN_NUMBER_SETTINGS = ( V2CSettingsNumberEntityDescription( key="intensity", diff --git a/homeassistant/components/v2c/sensor.py b/homeassistant/components/v2c/sensor.py index 0aa727fa408..871dd65aa75 100644 --- a/homeassistant/components/v2c/sensor.py +++ b/homeassistant/components/v2c/sensor.py @@ -1,4 +1,5 @@ """Support for V2C EVSE sensors.""" + from __future__ import annotations from collections.abc import Callable @@ -25,18 +26,13 @@ from .entity import V2CBaseEntity _LOGGER = logging.getLogger(__name__) -@dataclass(frozen=True) -class V2CRequiredKeysMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class V2CSensorEntityDescription(SensorEntityDescription): + """Describes an EVSE Power sensor entity.""" value_fn: Callable[[TrydanData], float] -@dataclass(frozen=True) -class V2CSensorEntityDescription(SensorEntityDescription, V2CRequiredKeysMixin): - """Describes an EVSE Power sensor entity.""" - - TRYDAN_SENSORS = ( V2CSensorEntityDescription( key="charge_power", @@ -50,7 +46,6 @@ TRYDAN_SENSORS = ( V2CSensorEntityDescription( key="charge_energy", translation_key="charge_energy", - icon="mdi:ev-station", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, state_class=SensorStateClass.TOTAL_INCREASING, device_class=SensorDeviceClass.ENERGY, @@ -59,7 +54,6 @@ TRYDAN_SENSORS = ( V2CSensorEntityDescription( key="charge_time", translation_key="charge_time", - icon="mdi:timer", native_unit_of_measurement=UnitOfTime.SECONDS, state_class=SensorStateClass.TOTAL_INCREASING, device_class=SensorDeviceClass.DURATION, @@ -68,7 +62,6 @@ TRYDAN_SENSORS = ( V2CSensorEntityDescription( key="house_power", translation_key="house_power", - icon="mdi:home-lightning-bolt", native_unit_of_measurement=UnitOfPower.WATT, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.POWER, @@ -77,7 +70,6 @@ TRYDAN_SENSORS = ( V2CSensorEntityDescription( key="fv_power", translation_key="fv_power", - icon="mdi:solar-power-variant", native_unit_of_measurement=UnitOfPower.WATT, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.POWER, diff --git a/homeassistant/components/v2c/switch.py b/homeassistant/components/v2c/switch.py index a8b4728c66d..0974a712153 100644 --- a/homeassistant/components/v2c/switch.py +++ b/homeassistant/components/v2c/switch.py @@ -1,4 +1,5 @@ """Switch platform for V2C EVSE.""" + from __future__ import annotations from collections.abc import Callable, Coroutine @@ -27,25 +28,19 @@ from .entity import V2CBaseEntity _LOGGER = logging.getLogger(__name__) -@dataclass(frozen=True) -class V2CRequiredKeysMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class V2CSwitchEntityDescription(SwitchEntityDescription): + """Describes a V2C EVSE switch entity.""" value_fn: Callable[[TrydanData], bool] turn_on_fn: Callable[[Trydan], Coroutine[Any, Any, Any]] turn_off_fn: Callable[[Trydan], Coroutine[Any, Any, Any]] -@dataclass(frozen=True) -class V2CSwitchEntityDescription(SwitchEntityDescription, V2CRequiredKeysMixin): - """Describes a V2C EVSE switch entity.""" - - TRYDAN_SWITCHES = ( V2CSwitchEntityDescription( key="paused", translation_key="paused", - icon="mdi:pause", value_fn=lambda evse_data: evse_data.paused == PauseState.PAUSED, turn_on_fn=lambda evse: evse.pause(), turn_off_fn=lambda evse: evse.resume(), @@ -53,7 +48,6 @@ TRYDAN_SWITCHES = ( V2CSwitchEntityDescription( key="locked", translation_key="locked", - icon="mdi:lock", value_fn=lambda evse_data: evse_data.locked == LockState.ENABLED, turn_on_fn=lambda evse: evse.lock(), turn_off_fn=lambda evse: evse.unlock(), @@ -61,7 +55,6 @@ TRYDAN_SWITCHES = ( V2CSwitchEntityDescription( key="timer", translation_key="timer", - icon="mdi:timer", value_fn=lambda evse_data: evse_data.timer == ChargePointTimerState.TIMER_ON, turn_on_fn=lambda evse: evse.timer(), turn_off_fn=lambda evse: evse.timer_disable(), @@ -69,7 +62,6 @@ TRYDAN_SWITCHES = ( V2CSwitchEntityDescription( key="dynamic", translation_key="dynamic", - icon="mdi:gauge", value_fn=lambda evse_data: evse_data.dynamic == DynamicState.ENABLED, turn_on_fn=lambda evse: evse.dynamic(), turn_off_fn=lambda evse: evse.dynamic_disable(), diff --git a/homeassistant/components/vacuum/__init__.py b/homeassistant/components/vacuum/__init__.py index 1bd9719c51c..bdf690ed63f 100644 --- a/homeassistant/components/vacuum/__init__.py +++ b/homeassistant/components/vacuum/__init__.py @@ -1,4 +1,5 @@ """Support for vacuum cleaner robots (botvacs).""" + from __future__ import annotations from collections.abc import Mapping @@ -34,6 +35,9 @@ from homeassistant.helpers.icon import icon_for_battery_level from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass +from . import group as group_pre_import # noqa: F401 +from .const import STATE_CLEANING, STATE_DOCKED, STATE_ERROR, STATE_RETURNING + if TYPE_CHECKING: from functools import cached_property else: @@ -63,11 +67,6 @@ SERVICE_PAUSE = "pause" SERVICE_STOP = "stop" -STATE_CLEANING = "cleaning" -STATE_DOCKED = "docked" -STATE_RETURNING = "returning" -STATE_ERROR = "error" - STATES = [STATE_CLEANING, STATE_DOCKED, STATE_RETURNING, STATE_ERROR] DEFAULT_NAME = "Vacuum cleaner robot" @@ -294,7 +293,7 @@ class StateVacuumEntity( def stop(self, **kwargs: Any) -> None: """Stop the vacuum cleaner.""" - raise NotImplementedError() + raise NotImplementedError async def async_stop(self, **kwargs: Any) -> None: """Stop the vacuum cleaner. @@ -305,7 +304,7 @@ class StateVacuumEntity( def return_to_base(self, **kwargs: Any) -> None: """Set the vacuum cleaner to return to the dock.""" - raise NotImplementedError() + raise NotImplementedError async def async_return_to_base(self, **kwargs: Any) -> None: """Set the vacuum cleaner to return to the dock. @@ -316,7 +315,7 @@ class StateVacuumEntity( def clean_spot(self, **kwargs: Any) -> None: """Perform a spot clean-up.""" - raise NotImplementedError() + raise NotImplementedError async def async_clean_spot(self, **kwargs: Any) -> None: """Perform a spot clean-up. @@ -327,7 +326,7 @@ class StateVacuumEntity( def locate(self, **kwargs: Any) -> None: """Locate the vacuum cleaner.""" - raise NotImplementedError() + raise NotImplementedError async def async_locate(self, **kwargs: Any) -> None: """Locate the vacuum cleaner. @@ -338,7 +337,7 @@ class StateVacuumEntity( def set_fan_speed(self, fan_speed: str, **kwargs: Any) -> None: """Set fan speed.""" - raise NotImplementedError() + raise NotImplementedError async def async_set_fan_speed(self, fan_speed: str, **kwargs: Any) -> None: """Set fan speed. @@ -356,7 +355,7 @@ class StateVacuumEntity( **kwargs: Any, ) -> None: """Send a command to a vacuum cleaner.""" - raise NotImplementedError() + raise NotImplementedError async def async_send_command( self, @@ -374,7 +373,7 @@ class StateVacuumEntity( def start(self) -> None: """Start or resume the cleaning task.""" - raise NotImplementedError() + raise NotImplementedError async def async_start(self) -> None: """Start or resume the cleaning task. @@ -385,7 +384,7 @@ class StateVacuumEntity( def pause(self) -> None: """Pause the cleaning task.""" - raise NotImplementedError() + raise NotImplementedError async def async_pause(self) -> None: """Pause the cleaning task. diff --git a/homeassistant/components/vacuum/const.py b/homeassistant/components/vacuum/const.py new file mode 100644 index 00000000000..f623d313b1a --- /dev/null +++ b/homeassistant/components/vacuum/const.py @@ -0,0 +1,8 @@ +"""Support for vacuum cleaner robots (botvacs).""" + +STATE_CLEANING = "cleaning" +STATE_DOCKED = "docked" +STATE_RETURNING = "returning" +STATE_ERROR = "error" + +STATES = [STATE_CLEANING, STATE_DOCKED, STATE_RETURNING, STATE_ERROR] diff --git a/homeassistant/components/vacuum/device_action.py b/homeassistant/components/vacuum/device_action.py index 0f212235673..82c00a57b5e 100644 --- a/homeassistant/components/vacuum/device_action.py +++ b/homeassistant/components/vacuum/device_action.py @@ -1,4 +1,5 @@ """Provides device automations for Vacuum.""" + from __future__ import annotations import voluptuous as vol diff --git a/homeassistant/components/vacuum/device_condition.py b/homeassistant/components/vacuum/device_condition.py index 8b7227e788e..f528b0918a1 100644 --- a/homeassistant/components/vacuum/device_condition.py +++ b/homeassistant/components/vacuum/device_condition.py @@ -1,4 +1,5 @@ """Provide the device automations for Vacuum.""" + from __future__ import annotations import voluptuous as vol diff --git a/homeassistant/components/vacuum/device_trigger.py b/homeassistant/components/vacuum/device_trigger.py index 95c1938ccfa..45b0696f871 100644 --- a/homeassistant/components/vacuum/device_trigger.py +++ b/homeassistant/components/vacuum/device_trigger.py @@ -1,4 +1,5 @@ """Provides device automations for Vacuum.""" + from __future__ import annotations import voluptuous as vol diff --git a/homeassistant/components/vacuum/group.py b/homeassistant/components/vacuum/group.py index e5a1734420f..3e874ec22e7 100644 --- a/homeassistant/components/vacuum/group.py +++ b/homeassistant/components/vacuum/group.py @@ -1,16 +1,18 @@ """Describe group states.""" +from typing import TYPE_CHECKING -from homeassistant.components.group import GroupIntegrationRegistry from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant, callback -from . import STATE_CLEANING, STATE_ERROR, STATE_RETURNING +if TYPE_CHECKING: + from homeassistant.components.group import GroupIntegrationRegistry +from .const import STATE_CLEANING, STATE_ERROR, STATE_RETURNING @callback def async_describe_on_off_states( - hass: HomeAssistant, registry: GroupIntegrationRegistry + hass: HomeAssistant, registry: "GroupIntegrationRegistry" ) -> None: """Describe group on off states.""" registry.on_off_states( diff --git a/homeassistant/components/vacuum/intent.py b/homeassistant/components/vacuum/intent.py index c485686aa23..534078ec8af 100644 --- a/homeassistant/components/vacuum/intent.py +++ b/homeassistant/components/vacuum/intent.py @@ -1,6 +1,5 @@ """Intents for the vacuum integration.""" - from homeassistant.core import HomeAssistant from homeassistant.helpers import intent diff --git a/homeassistant/components/vacuum/reproduce_state.py b/homeassistant/components/vacuum/reproduce_state.py index 4d0d6b4b12c..762cd6f2e90 100644 --- a/homeassistant/components/vacuum/reproduce_state.py +++ b/homeassistant/components/vacuum/reproduce_state.py @@ -1,4 +1,5 @@ """Reproduce an Vacuum state.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/vacuum/significant_change.py b/homeassistant/components/vacuum/significant_change.py index 5699050c7cb..857e6e822c5 100644 --- a/homeassistant/components/vacuum/significant_change.py +++ b/homeassistant/components/vacuum/significant_change.py @@ -1,4 +1,5 @@ """Helper to test significant Vacuum state changes.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/vallox/__init__.py b/homeassistant/components/vallox/__init__.py index d12f7b4ffa1..7c9234d35c2 100644 --- a/homeassistant/components/vallox/__init__.py +++ b/homeassistant/components/vallox/__init__.py @@ -1,4 +1,5 @@ """Support for Vallox ventilation units.""" + from __future__ import annotations import ipaddress @@ -47,6 +48,7 @@ CONFIG_SCHEMA = vol.Schema( PLATFORMS: list[str] = [ Platform.BINARY_SENSOR, + Platform.DATE, Platform.FAN, Platform.NUMBER, Platform.SENSOR, diff --git a/homeassistant/components/vallox/binary_sensor.py b/homeassistant/components/vallox/binary_sensor.py index f919e67fa14..fbcfa403738 100644 --- a/homeassistant/components/vallox/binary_sensor.py +++ b/homeassistant/components/vallox/binary_sensor.py @@ -1,4 +1,5 @@ """Support for Vallox ventilation unit binary sensors.""" + from __future__ import annotations from dataclasses import dataclass @@ -41,25 +42,17 @@ class ValloxBinarySensorEntity(ValloxEntity, BinarySensorEntity): return self.coordinator.data.get(self.entity_description.metric_key) == 1 -@dataclass(frozen=True) -class ValloxMetricKeyMixin: - """Dataclass to allow defining metric_key without a default value.""" +@dataclass(frozen=True, kw_only=True) +class ValloxBinarySensorEntityDescription(BinarySensorEntityDescription): + """Describes Vallox binary sensor entity.""" metric_key: str -@dataclass(frozen=True) -class ValloxBinarySensorEntityDescription( - BinarySensorEntityDescription, ValloxMetricKeyMixin -): - """Describes Vallox binary sensor entity.""" - - BINARY_SENSOR_ENTITIES: tuple[ValloxBinarySensorEntityDescription, ...] = ( ValloxBinarySensorEntityDescription( key="post_heater", translation_key="post_heater", - icon="mdi:radiator", metric_key="A_CYC_IO_HEATER", ), ) @@ -75,8 +68,6 @@ async def async_setup_entry( data = hass.data[DOMAIN][entry.entry_id] async_add_entities( - [ - ValloxBinarySensorEntity(data["name"], data["coordinator"], description) - for description in BINARY_SENSOR_ENTITIES - ] + ValloxBinarySensorEntity(data["name"], data["coordinator"], description) + for description in BINARY_SENSOR_ENTITIES ) diff --git a/homeassistant/components/vallox/config_flow.py b/homeassistant/components/vallox/config_flow.py index 6c6e3630023..4812097d4e0 100644 --- a/homeassistant/components/vallox/config_flow.py +++ b/homeassistant/components/vallox/config_flow.py @@ -1,4 +1,5 @@ """Config flow for the Vallox integration.""" + from __future__ import annotations import logging @@ -7,10 +8,9 @@ from typing import Any from vallox_websocket_api import Vallox, ValloxApiException import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_NAME from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError from homeassistant.util.network import is_ip_address @@ -35,14 +35,14 @@ async def validate_host(hass: HomeAssistant, host: str) -> None: await client.fetch_metric_data() -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class ValloxConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for the Vallox integration.""" VERSION = 1 async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" if user_input is None: return self.async_show_form( diff --git a/homeassistant/components/vallox/date.py b/homeassistant/components/vallox/date.py new file mode 100644 index 00000000000..0cdb7cdbb3f --- /dev/null +++ b/homeassistant/components/vallox/date.py @@ -0,0 +1,65 @@ +"""Support for Vallox date platform.""" + +from __future__ import annotations + +from datetime import date + +from vallox_websocket_api import Vallox + +from homeassistant.components.date import DateEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import EntityCategory +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import ValloxDataUpdateCoordinator, ValloxEntity +from .const import DOMAIN + + +class ValloxFilterChangeDateEntity(ValloxEntity, DateEntity): + """Representation of a Vallox filter change date entity.""" + + _attr_entity_category = EntityCategory.CONFIG + _attr_translation_key = "filter_change_date" + + def __init__( + self, + name: str, + coordinator: ValloxDataUpdateCoordinator, + client: Vallox, + ) -> None: + """Initialize the Vallox date.""" + super().__init__(name, coordinator) + + self._attr_unique_id = f"{self._device_uuid}-filter_change_date" + self._client = client + + @property + def native_value(self) -> date | None: + """Return the latest value.""" + + return self.coordinator.data.filter_change_date + + async def async_set_value(self, value: date) -> None: + """Change the date.""" + + await self._client.set_filter_change_date(value) + await self.coordinator.async_request_refresh() + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Vallox filter change date entity.""" + + data = hass.data[DOMAIN][entry.entry_id] + + async_add_entities( + [ + ValloxFilterChangeDateEntity( + data["name"], data["coordinator"], data["client"] + ) + ] + ) diff --git a/homeassistant/components/vallox/fan.py b/homeassistant/components/vallox/fan.py index 24448e6f53b..46f6fb022e4 100644 --- a/homeassistant/components/vallox/fan.py +++ b/homeassistant/components/vallox/fan.py @@ -1,4 +1,5 @@ """Support for the Vallox ventilation unit fan.""" + from __future__ import annotations from collections.abc import Mapping diff --git a/homeassistant/components/vallox/icons.json b/homeassistant/components/vallox/icons.json new file mode 100644 index 00000000000..67b41d216d2 --- /dev/null +++ b/homeassistant/components/vallox/icons.json @@ -0,0 +1,44 @@ +{ + "entity": { + "binary_sensor": { + "post_heater": { + "default": "mdi:radiator" + } + }, + "date": { + "filter_change_date": { + "default": "mdi:air-filter" + } + }, + "sensor": { + "current_profile": { + "default": "mdi:gauge" + }, + "fan_speed": { + "default": "mdi:fan" + }, + "extract_fan_speed": { + "default": "mdi:fan" + }, + "supply_fan_speed": { + "default": "mdi:fan" + }, + "cell_state": { + "default": "mdi:swap-horizontal-bold" + }, + "efficiency": { + "default": "mdi:gauge" + } + }, + "switch": { + "bypass_locked": { + "default": "mdi:arrow-horizontal-lock" + } + } + }, + "services": { + "set_profile_fan_speed_home": "mdi:home", + "set_profile_fan_speed_away": "mdi:walk", + "set_profile_fan_speed_boost": "mdi:speedometer" + } +} diff --git a/homeassistant/components/vallox/manifest.json b/homeassistant/components/vallox/manifest.json index 46cb765cc5e..9a57358cd14 100644 --- a/homeassistant/components/vallox/manifest.json +++ b/homeassistant/components/vallox/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/vallox", "iot_class": "local_polling", "loggers": ["vallox_websocket_api"], - "requirements": ["vallox-websocket-api==5.1.0"] + "requirements": ["vallox-websocket-api==5.1.1"] } diff --git a/homeassistant/components/vallox/number.py b/homeassistant/components/vallox/number.py index 044bc7e0a43..83316a13645 100644 --- a/homeassistant/components/vallox/number.py +++ b/homeassistant/components/vallox/number.py @@ -1,4 +1,5 @@ """Support for Vallox ventilation unit numbers.""" + from __future__ import annotations from dataclasses import dataclass @@ -58,18 +59,13 @@ class ValloxNumberEntity(ValloxEntity, NumberEntity): await self.coordinator.async_request_refresh() -@dataclass(frozen=True) -class ValloxMetricMixin: - """Holds Vallox metric key.""" +@dataclass(frozen=True, kw_only=True) +class ValloxNumberEntityDescription(NumberEntityDescription): + """Describes Vallox number entity.""" metric_key: str -@dataclass(frozen=True) -class ValloxNumberEntityDescription(NumberEntityDescription, ValloxMetricMixin): - """Describes Vallox number entity.""" - - NUMBER_ENTITIES: tuple[ValloxNumberEntityDescription, ...] = ( ValloxNumberEntityDescription( key="supply_air_target_home", @@ -77,7 +73,6 @@ NUMBER_ENTITIES: tuple[ValloxNumberEntityDescription, ...] = ( metric_key="A_CYC_HOME_AIR_TEMP_TARGET", device_class=NumberDeviceClass.TEMPERATURE, native_unit_of_measurement=UnitOfTemperature.CELSIUS, - icon="mdi:thermometer", native_min_value=5.0, native_max_value=25.0, native_step=1.0, @@ -88,7 +83,6 @@ NUMBER_ENTITIES: tuple[ValloxNumberEntityDescription, ...] = ( metric_key="A_CYC_AWAY_AIR_TEMP_TARGET", device_class=NumberDeviceClass.TEMPERATURE, native_unit_of_measurement=UnitOfTemperature.CELSIUS, - icon="mdi:thermometer", native_min_value=5.0, native_max_value=25.0, native_step=1.0, @@ -99,7 +93,6 @@ NUMBER_ENTITIES: tuple[ValloxNumberEntityDescription, ...] = ( metric_key="A_CYC_BOOST_AIR_TEMP_TARGET", device_class=NumberDeviceClass.TEMPERATURE, native_unit_of_measurement=UnitOfTemperature.CELSIUS, - icon="mdi:thermometer", native_min_value=5.0, native_max_value=25.0, native_step=1.0, @@ -114,10 +107,8 @@ async def async_setup_entry( data = hass.data[DOMAIN][entry.entry_id] async_add_entities( - [ - ValloxNumberEntity( - data["name"], data["coordinator"], description, data["client"] - ) - for description in NUMBER_ENTITIES - ] + ValloxNumberEntity( + data["name"], data["coordinator"], description, data["client"] + ) + for description in NUMBER_ENTITIES ) diff --git a/homeassistant/components/vallox/sensor.py b/homeassistant/components/vallox/sensor.py index 79dfeae8412..8fca6f3b05d 100644 --- a/homeassistant/components/vallox/sensor.py +++ b/homeassistant/components/vallox/sensor.py @@ -1,4 +1,5 @@ """Support for Vallox ventilation unit sensors.""" + from __future__ import annotations from dataclasses import dataclass @@ -138,14 +139,12 @@ SENSOR_ENTITIES: tuple[ValloxSensorEntityDescription, ...] = ( ValloxSensorEntityDescription( key="current_profile", translation_key="current_profile", - icon="mdi:gauge", entity_type=ValloxProfileSensor, ), ValloxSensorEntityDescription( key="fan_speed", translation_key="fan_speed", metric_key="A_CYC_FAN_SPEED", - icon="mdi:fan", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, entity_type=ValloxFanSpeedSensor, @@ -154,7 +153,6 @@ SENSOR_ENTITIES: tuple[ValloxSensorEntityDescription, ...] = ( key="extract_fan_speed", translation_key="extract_fan_speed", metric_key="A_CYC_EXTR_FAN_SPEED", - icon="mdi:fan", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=REVOLUTIONS_PER_MINUTE, entity_type=ValloxFanSpeedSensor, @@ -164,7 +162,6 @@ SENSOR_ENTITIES: tuple[ValloxSensorEntityDescription, ...] = ( key="supply_fan_speed", translation_key="supply_fan_speed", metric_key="A_CYC_SUPP_FAN_SPEED", - icon="mdi:fan", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=REVOLUTIONS_PER_MINUTE, entity_type=ValloxFanSpeedSensor, @@ -179,7 +176,6 @@ SENSOR_ENTITIES: tuple[ValloxSensorEntityDescription, ...] = ( ValloxSensorEntityDescription( key="cell_state", translation_key="cell_state", - icon="mdi:swap-horizontal-bold", metric_key="A_CYC_CELL_STATE", entity_type=ValloxCellStateSensor, ), @@ -243,7 +239,6 @@ SENSOR_ENTITIES: tuple[ValloxSensorEntityDescription, ...] = ( key="efficiency", translation_key="efficiency", metric_key="A_CYC_EXTRACT_EFFICIENCY", - icon="mdi:gauge", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, entity_registry_enabled_default=False, @@ -268,8 +263,6 @@ async def async_setup_entry( coordinator = hass.data[DOMAIN][entry.entry_id]["coordinator"] async_add_entities( - [ - description.entity_type(name, coordinator, description) - for description in SENSOR_ENTITIES - ] + description.entity_type(name, coordinator, description) + for description in SENSOR_ENTITIES ) diff --git a/homeassistant/components/vallox/strings.json b/homeassistant/components/vallox/strings.json index e3ade9a55c4..d23d54c75cb 100644 --- a/homeassistant/components/vallox/strings.json +++ b/homeassistant/components/vallox/strings.json @@ -84,6 +84,11 @@ "bypass_locked": { "name": "Bypass locked" } + }, + "date": { + "filter_change_date": { + "name": "Filter change date" + } } }, "services": { diff --git a/homeassistant/components/vallox/switch.py b/homeassistant/components/vallox/switch.py index fcc468c0fb2..90e2311bf95 100644 --- a/homeassistant/components/vallox/switch.py +++ b/homeassistant/components/vallox/switch.py @@ -1,4 +1,5 @@ """Support for Vallox ventilation unit switches.""" + from __future__ import annotations from dataclasses import dataclass @@ -61,23 +62,17 @@ class ValloxSwitchEntity(ValloxEntity, SwitchEntity): await self.coordinator.async_request_refresh() -@dataclass(frozen=True) -class ValloxMetricKeyMixin: - """Dataclass to allow defining metric_key without a default value.""" +@dataclass(frozen=True, kw_only=True) +class ValloxSwitchEntityDescription(SwitchEntityDescription): + """Describes Vallox switch entity.""" metric_key: str -@dataclass(frozen=True) -class ValloxSwitchEntityDescription(SwitchEntityDescription, ValloxMetricKeyMixin): - """Describes Vallox switch entity.""" - - SWITCH_ENTITIES: tuple[ValloxSwitchEntityDescription, ...] = ( ValloxSwitchEntityDescription( key="bypass_locked", translation_key="bypass_locked", - icon="mdi:arrow-horizontal-lock", metric_key="A_CYC_BYPASS_LOCKED", ), ) @@ -93,10 +88,8 @@ async def async_setup_entry( data = hass.data[DOMAIN][entry.entry_id] async_add_entities( - [ - ValloxSwitchEntity( - data["name"], data["coordinator"], description, data["client"] - ) - for description in SWITCH_ENTITIES - ] + ValloxSwitchEntity( + data["name"], data["coordinator"], description, data["client"] + ) + for description in SWITCH_ENTITIES ) diff --git a/homeassistant/components/valve/__init__.py b/homeassistant/components/valve/__init__.py index c04e25355ff..0363ef55832 100644 --- a/homeassistant/components/valve/__init__.py +++ b/homeassistant/components/valve/__init__.py @@ -1,4 +1,5 @@ """Support for Valve devices.""" + from __future__ import annotations from dataclasses import dataclass @@ -214,7 +215,7 @@ class ValveEntity(Entity): def open_valve(self) -> None: """Open the valve.""" - raise NotImplementedError() + raise NotImplementedError async def async_open_valve(self) -> None: """Open the valve.""" @@ -229,7 +230,7 @@ class ValveEntity(Entity): def close_valve(self) -> None: """Close valve.""" - raise NotImplementedError() + raise NotImplementedError async def async_close_valve(self) -> None: """Close valve.""" @@ -256,7 +257,7 @@ class ValveEntity(Entity): def set_valve_position(self, position: int) -> None: """Move the valve to a specific position.""" - raise NotImplementedError() + raise NotImplementedError async def async_set_valve_position(self, position: int) -> None: """Move the valve to a specific position.""" @@ -264,7 +265,7 @@ class ValveEntity(Entity): def stop_valve(self) -> None: """Stop the valve.""" - raise NotImplementedError() + raise NotImplementedError async def async_stop_valve(self) -> None: """Stop the valve.""" diff --git a/homeassistant/components/vasttrafik/sensor.py b/homeassistant/components/vasttrafik/sensor.py index 6a083232079..611f571336c 100644 --- a/homeassistant/components/vasttrafik/sensor.py +++ b/homeassistant/components/vasttrafik/sensor.py @@ -1,4 +1,5 @@ """Support for Västtrafik public transport.""" + from __future__ import annotations from datetime import datetime, timedelta @@ -64,10 +65,8 @@ def setup_platform( ) -> None: """Set up the departure sensor.""" planner = vasttrafik.JournyPlanner(config.get(CONF_KEY), config.get(CONF_SECRET)) - sensors = [] - - for departure in config[CONF_DEPARTURES]: - sensors.append( + add_entities( + ( VasttrafikDepartureSensor( planner, departure.get(CONF_NAME), @@ -76,8 +75,10 @@ def setup_platform( departure.get(CONF_LINES), departure.get(CONF_DELAY), ) - ) - add_entities(sensors, True) + for departure in config[CONF_DEPARTURES] + ), + True, + ) class VasttrafikDepartureSensor(SensorEntity): diff --git a/homeassistant/components/velbus/__init__.py b/homeassistant/components/velbus/__init__.py index 609823b1310..ea03c4b15f1 100644 --- a/homeassistant/components/velbus/__init__.py +++ b/homeassistant/components/velbus/__init__.py @@ -1,4 +1,5 @@ """Support for Velbus devices.""" + from __future__ import annotations from contextlib import suppress @@ -53,7 +54,7 @@ async def velbus_connect_task( def _migrate_device_identifiers(hass: HomeAssistant, entry_id: str) -> None: - """Migrate old device indentifiers.""" + """Migrate old device identifiers.""" dev_reg = dr.async_get(hass) devices: list[dr.DeviceEntry] = dr.async_entries_for_config_entry(dev_reg, entry_id) for device in devices: diff --git a/homeassistant/components/velbus/binary_sensor.py b/homeassistant/components/velbus/binary_sensor.py index 25591cc1cb0..5f363c1a035 100644 --- a/homeassistant/components/velbus/binary_sensor.py +++ b/homeassistant/components/velbus/binary_sensor.py @@ -1,4 +1,5 @@ """Support for Velbus Binary Sensors.""" + from velbusaio.channels import Button as VelbusButton from homeassistant.components.binary_sensor import BinarySensorEntity diff --git a/homeassistant/components/velbus/button.py b/homeassistant/components/velbus/button.py index 2a0392c48cb..bd5b81d67a0 100644 --- a/homeassistant/components/velbus/button.py +++ b/homeassistant/components/velbus/button.py @@ -1,4 +1,5 @@ """Support for Velbus Buttons.""" + from __future__ import annotations from velbusaio.channels import ( diff --git a/homeassistant/components/velbus/climate.py b/homeassistant/components/velbus/climate.py index 9afbfc683a8..34a565c2b37 100644 --- a/homeassistant/components/velbus/climate.py +++ b/homeassistant/components/velbus/climate.py @@ -1,4 +1,5 @@ """Support for Velbus thermostat.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/velbus/config_flow.py b/homeassistant/components/velbus/config_flow.py index 1888a177895..0b47dfe6498 100644 --- a/homeassistant/components/velbus/config_flow.py +++ b/homeassistant/components/velbus/config_flow.py @@ -1,4 +1,5 @@ """Config flow for the Velbus platform.""" + from __future__ import annotations from typing import Any @@ -7,16 +8,15 @@ import velbusaio.controller from velbusaio.exceptions import VelbusConnectionFailed import voluptuous as vol -from homeassistant import config_entries from homeassistant.components import usb +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_NAME, CONF_PORT -from homeassistant.data_entry_flow import FlowResult from homeassistant.util import slugify from .const import DOMAIN -class VelbusConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class VelbusConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow.""" VERSION = 2 @@ -27,7 +27,7 @@ class VelbusConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._device: str = "" self._title: str = "" - def _create_device(self, name: str, prt: str) -> FlowResult: + def _create_device(self, name: str, prt: str) -> ConfigFlowResult: """Create an entry async.""" return self.async_create_entry(title=name, data={CONF_PORT: prt}) @@ -44,7 +44,7 @@ class VelbusConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Step when user initializes a integration.""" self._errors = {} if user_input is not None: @@ -69,7 +69,9 @@ class VelbusConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors=self._errors, ) - async def async_step_usb(self, discovery_info: usb.UsbServiceInfo) -> FlowResult: + async def async_step_usb( + self, discovery_info: usb.UsbServiceInfo + ) -> ConfigFlowResult: """Handle USB Discovery.""" await self.async_set_unique_id( f"{discovery_info.vid}:{discovery_info.pid}_{discovery_info.serial_number}_{discovery_info.manufacturer}_{discovery_info.description}" @@ -89,7 +91,7 @@ class VelbusConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_discovery_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle Discovery confirmation.""" if user_input is not None: return self._create_device(self._title, self._device) diff --git a/homeassistant/components/velbus/const.py b/homeassistant/components/velbus/const.py index a3949646598..2d9f6e98a4c 100644 --- a/homeassistant/components/velbus/const.py +++ b/homeassistant/components/velbus/const.py @@ -1,4 +1,5 @@ """Const for Velbus.""" + from typing import Final from homeassistant.components.climate import ( diff --git a/homeassistant/components/velbus/cover.py b/homeassistant/components/velbus/cover.py index 46881fcdcaf..f37de104659 100644 --- a/homeassistant/components/velbus/cover.py +++ b/homeassistant/components/velbus/cover.py @@ -1,4 +1,5 @@ """Support for Velbus covers.""" + from __future__ import annotations from typing import Any @@ -26,10 +27,7 @@ async def async_setup_entry( """Set up Velbus switch based on config_entry.""" await hass.data[DOMAIN][entry.entry_id]["tsk"] cntrl = hass.data[DOMAIN][entry.entry_id]["cntrl"] - entities = [] - for channel in cntrl.get_all("cover"): - entities.append(VelbusCover(channel)) - async_add_entities(entities) + async_add_entities(VelbusCover(channel) for channel in cntrl.get_all("cover")) class VelbusCover(VelbusEntity, CoverEntity): diff --git a/homeassistant/components/velbus/diagnostics.py b/homeassistant/components/velbus/diagnostics.py index 5b991fa35fb..f7e29e2f57e 100644 --- a/homeassistant/components/velbus/diagnostics.py +++ b/homeassistant/components/velbus/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for Velbus.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/velbus/entity.py b/homeassistant/components/velbus/entity.py index 1a99f796eb2..202666e6123 100644 --- a/homeassistant/components/velbus/entity.py +++ b/homeassistant/components/velbus/entity.py @@ -1,4 +1,5 @@ """Support for Velbus devices.""" + from __future__ import annotations from collections.abc import Awaitable, Callable, Coroutine diff --git a/homeassistant/components/velbus/icons.json b/homeassistant/components/velbus/icons.json new file mode 100644 index 00000000000..a806782d189 --- /dev/null +++ b/homeassistant/components/velbus/icons.json @@ -0,0 +1,8 @@ +{ + "services": { + "sync_clock": "mdi:clock", + "scan": "mdi:magnify", + "clear_cache": "mdi:delete", + "set_memo_text": "mdi:note-text" + } +} diff --git a/homeassistant/components/velbus/light.py b/homeassistant/components/velbus/light.py index 1806c2905e9..7145576be6a 100644 --- a/homeassistant/components/velbus/light.py +++ b/homeassistant/components/velbus/light.py @@ -1,4 +1,5 @@ """Support for Velbus light.""" + from __future__ import annotations from typing import Any @@ -37,11 +38,10 @@ async def async_setup_entry( """Set up Velbus switch based on config_entry.""" await hass.data[DOMAIN][entry.entry_id]["tsk"] cntrl = hass.data[DOMAIN][entry.entry_id]["cntrl"] - entities: list[Entity] = [] - for channel in cntrl.get_all("light"): - entities.append(VelbusLight(channel)) - for channel in cntrl.get_all("led"): - entities.append(VelbusButtonLight(channel)) + entities: list[Entity] = [ + VelbusLight(channel) for channel in cntrl.get_all("light") + ] + entities.extend(VelbusButtonLight(channel) for channel in cntrl.get_all("led")) async_add_entities(entities) diff --git a/homeassistant/components/velbus/manifest.json b/homeassistant/components/velbus/manifest.json index c5f9ccd3563..1c51c58d238 100644 --- a/homeassistant/components/velbus/manifest.json +++ b/homeassistant/components/velbus/manifest.json @@ -13,7 +13,7 @@ "velbus-packet", "velbus-protocol" ], - "requirements": ["velbus-aio==2023.12.0"], + "requirements": ["velbus-aio==2024.4.0"], "usb": [ { "vid": "10CF", diff --git a/homeassistant/components/velbus/select.py b/homeassistant/components/velbus/select.py index 6e2b4d1a746..7eecb85fc47 100644 --- a/homeassistant/components/velbus/select.py +++ b/homeassistant/components/velbus/select.py @@ -1,4 +1,5 @@ """Support for Velbus select.""" + from velbusaio.channels import SelectedProgram from homeassistant.components.select import SelectEntity diff --git a/homeassistant/components/velbus/sensor.py b/homeassistant/components/velbus/sensor.py index 8e1f8bba74a..b765eebcddc 100644 --- a/homeassistant/components/velbus/sensor.py +++ b/homeassistant/components/velbus/sensor.py @@ -1,4 +1,5 @@ """Support for Velbus sensors.""" + from __future__ import annotations from velbusaio.channels import ButtonCounter, LightSensor, SensorNumber, Temperature diff --git a/homeassistant/components/velbus/switch.py b/homeassistant/components/velbus/switch.py index db7c165840e..1e6014b8d90 100644 --- a/homeassistant/components/velbus/switch.py +++ b/homeassistant/components/velbus/switch.py @@ -1,4 +1,5 @@ """Support for Velbus switches.""" + from typing import Any from velbusaio.channels import Relay as VelbusRelay @@ -20,10 +21,7 @@ async def async_setup_entry( """Set up Velbus switch based on config_entry.""" await hass.data[DOMAIN][entry.entry_id]["tsk"] cntrl = hass.data[DOMAIN][entry.entry_id]["cntrl"] - entities = [] - for channel in cntrl.get_all("switch"): - entities.append(VelbusSwitch(channel)) - async_add_entities(entities) + async_add_entities(VelbusSwitch(channel) for channel in cntrl.get_all("switch")) class VelbusSwitch(VelbusEntity, SwitchEntity): diff --git a/homeassistant/components/velux/__init__.py b/homeassistant/components/velux/__init__.py index 4c84eb687ad..4b89fc66a84 100644 --- a/homeassistant/components/velux/__init__.py +++ b/homeassistant/components/velux/__init__.py @@ -1,4 +1,5 @@ """Support for VELUX KLF 200 devices.""" + from pyvlx import Node, PyVLX, PyVLXException import voluptuous as vol diff --git a/homeassistant/components/velux/config_flow.py b/homeassistant/components/velux/config_flow.py index 57791ea01dd..da6502b86da 100644 --- a/homeassistant/components/velux/config_flow.py +++ b/homeassistant/components/velux/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Velux integration.""" + from typing import Any from pyvlx import PyVLX, PyVLXException @@ -7,7 +8,6 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_HOST, CONF_PASSWORD from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN -from homeassistant.data_entry_flow import FlowResult import homeassistant.helpers.config_validation as cv from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue @@ -24,7 +24,9 @@ DATA_SCHEMA = vol.Schema( class VeluxConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for velux.""" - async def async_step_import(self, config: dict[str, Any]) -> FlowResult: + async def async_step_import( + self, config: dict[str, Any] + ) -> config_entries.ConfigFlowResult: """Import a config entry.""" def create_repair(error: str | None = None) -> None: @@ -79,7 +81,7 @@ class VeluxConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, str] | None = None - ) -> FlowResult: + ) -> config_entries.ConfigFlowResult: """Handle the initial step.""" errors: dict[str, str] = {} diff --git a/homeassistant/components/velux/const.py b/homeassistant/components/velux/const.py index 9a686adf920..49a762e87ca 100644 --- a/homeassistant/components/velux/const.py +++ b/homeassistant/components/velux/const.py @@ -1,4 +1,5 @@ """Constants for the Velux integration.""" + from logging import getLogger from homeassistant.const import Platform diff --git a/homeassistant/components/velux/cover.py b/homeassistant/components/velux/cover.py index 2162e63096a..c8688e4d186 100644 --- a/homeassistant/components/velux/cover.py +++ b/homeassistant/components/velux/cover.py @@ -1,4 +1,5 @@ """Support for Velux covers.""" + from __future__ import annotations from typing import Any, cast @@ -26,12 +27,12 @@ async def async_setup_entry( hass: HomeAssistant, config: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up cover(s) for Velux platform.""" - entities = [] module = hass.data[DOMAIN][config.entry_id] - for node in module.pyvlx.nodes: - if isinstance(node, OpeningDevice): - entities.append(VeluxCover(node)) - async_add_entities(entities) + async_add_entities( + VeluxCover(node) + for node in module.pyvlx.nodes + if isinstance(node, OpeningDevice) + ) class VeluxCover(VeluxEntity, CoverEntity): diff --git a/homeassistant/components/velux/icons.json b/homeassistant/components/velux/icons.json new file mode 100644 index 00000000000..a16e7b50093 --- /dev/null +++ b/homeassistant/components/velux/icons.json @@ -0,0 +1,5 @@ +{ + "services": { + "reboot_gateway": "mdi:restart" + } +} diff --git a/homeassistant/components/velux/light.py b/homeassistant/components/velux/light.py index dae38f3d9bf..bbe9822648e 100644 --- a/homeassistant/components/velux/light.py +++ b/homeassistant/components/velux/light.py @@ -1,4 +1,5 @@ """Support for Velux lights.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/velux/scene.py b/homeassistant/components/velux/scene.py index 956663c23f1..30858b25002 100644 --- a/homeassistant/components/velux/scene.py +++ b/homeassistant/components/velux/scene.py @@ -1,4 +1,5 @@ """Support for VELUX scenes.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/venstar/__init__.py b/homeassistant/components/venstar/__init__.py index 78cb20b33cc..13368a60350 100644 --- a/homeassistant/components/venstar/__init__.py +++ b/homeassistant/components/venstar/__init__.py @@ -1,4 +1,5 @@ """The venstar component.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/venstar/binary_sensor.py b/homeassistant/components/venstar/binary_sensor.py index a5e15b04917..38bdc208d15 100644 --- a/homeassistant/components/venstar/binary_sensor.py +++ b/homeassistant/components/venstar/binary_sensor.py @@ -1,4 +1,5 @@ """Alarm sensors for the Venstar Thermostat.""" + from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, BinarySensorEntity, diff --git a/homeassistant/components/venstar/climate.py b/homeassistant/components/venstar/climate.py index a9ee56c4dbb..e0aacadffa7 100644 --- a/homeassistant/components/venstar/climate.py +++ b/homeassistant/components/venstar/climate.py @@ -1,4 +1,5 @@ """Support for Venstar WiFi Thermostats.""" + from __future__ import annotations import voluptuous as vol diff --git a/homeassistant/components/venstar/config_flow.py b/homeassistant/components/venstar/config_flow.py index 66ce22cb00b..5a193568c87 100644 --- a/homeassistant/components/venstar/config_flow.py +++ b/homeassistant/components/venstar/config_flow.py @@ -1,10 +1,11 @@ """Config flow to configure the Venstar integration.""" + from typing import Any from venstarcolortouch import VenstarColorTouch import voluptuous as vol -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, @@ -13,7 +14,6 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.typing import ConfigType @@ -54,7 +54,7 @@ class VenstarConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Create config entry. Show the setup form to the user.""" errors = {} @@ -85,7 +85,7 @@ class VenstarConfigFlow(ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_import(self, import_data: ConfigType) -> FlowResult: + async def async_step_import(self, import_data: ConfigType) -> ConfigFlowResult: """Import entry from configuration.yaml.""" self._async_abort_entries_match({CONF_HOST: import_data[CONF_HOST]}) return await self.async_step_user( diff --git a/homeassistant/components/venstar/const.py b/homeassistant/components/venstar/const.py index 31eb5724558..a485adad8e7 100644 --- a/homeassistant/components/venstar/const.py +++ b/homeassistant/components/venstar/const.py @@ -1,4 +1,5 @@ """The venstar component.""" + import logging DOMAIN = "venstar" diff --git a/homeassistant/components/venstar/sensor.py b/homeassistant/components/venstar/sensor.py index 1e31fb9407b..24b4b2f8b16 100644 --- a/homeassistant/components/venstar/sensor.py +++ b/homeassistant/components/venstar/sensor.py @@ -1,4 +1,5 @@ """Representation of Venstar sensors.""" + from __future__ import annotations from collections.abc import Callable @@ -65,20 +66,15 @@ SCHEDULE_PARTS: dict[int, str] = { } -@dataclass(frozen=True) -class VenstarSensorTypeMixin: - """Mixin for sensor required keys.""" +@dataclass(frozen=True, kw_only=True) +class VenstarSensorEntityDescription(SensorEntityDescription): + """Base description of a Sensor entity.""" value_fn: Callable[[VenstarDataUpdateCoordinator, str], Any] name_fn: Callable[[str], str] uom_fn: Callable[[Any], str | None] -@dataclass(frozen=True) -class VenstarSensorEntityDescription(SensorEntityDescription, VenstarSensorTypeMixin): - """Base description of a Sensor entity.""" - - async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, @@ -100,13 +96,11 @@ async def async_setup_entry( ) runtimes = coordinator.runtimes[-1] - for sensor_name in runtimes: - if sensor_name in RUNTIME_DEVICES: - entities.append( - VenstarSensor( - coordinator, config_entry, RUNTIME_ENTITY, sensor_name - ) - ) + entities.extend( + VenstarSensor(coordinator, config_entry, RUNTIME_ENTITY, sensor_name) + for sensor_name in runtimes + if sensor_name in RUNTIME_DEVICES + ) for description in INFO_ENTITIES: try: diff --git a/homeassistant/components/vera/__init__.py b/homeassistant/components/vera/__init__.py index a63e63d74c0..acbb89f4367 100644 --- a/homeassistant/components/vera/__init__.py +++ b/homeassistant/components/vera/__init__.py @@ -1,4 +1,5 @@ """Support for Vera devices.""" + from __future__ import annotations import asyncio @@ -128,14 +129,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if device_type is not None: vera_devices[device_type].append(device) - vera_scenes = [] - for scene in all_scenes: - vera_scenes.append(scene) - controller_data = ControllerData( controller=controller, devices=vera_devices, - scenes=vera_scenes, + scenes=all_scenes, config_entry=entry, ) diff --git a/homeassistant/components/vera/binary_sensor.py b/homeassistant/components/vera/binary_sensor.py index 82c7d187b88..d90f6a78858 100644 --- a/homeassistant/components/vera/binary_sensor.py +++ b/homeassistant/components/vera/binary_sensor.py @@ -1,4 +1,5 @@ """Support for Vera binary sensors.""" + from __future__ import annotations import pyvera as veraApi diff --git a/homeassistant/components/vera/climate.py b/homeassistant/components/vera/climate.py index 93d0fbf2aee..79a6c2566e0 100644 --- a/homeassistant/components/vera/climate.py +++ b/homeassistant/components/vera/climate.py @@ -1,4 +1,5 @@ """Support for Vera thermostats.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/vera/common.py b/homeassistant/components/vera/common.py index 658ed7904f4..76adeeab1d2 100644 --- a/homeassistant/components/vera/common.py +++ b/homeassistant/components/vera/common.py @@ -1,4 +1,5 @@ """Common vera code.""" + from __future__ import annotations from collections import defaultdict @@ -26,9 +27,7 @@ class ControllerData(NamedTuple): def get_configured_platforms(controller_data: ControllerData) -> set[Platform]: """Get configured platforms for a controller.""" - platforms: list[Platform] = [] - for platform in controller_data.devices: - platforms.append(platform) + platforms: list[Platform] = list(controller_data.devices) if controller_data.scenes: platforms.append(Platform.SCENE) diff --git a/homeassistant/components/vera/config_flow.py b/homeassistant/components/vera/config_flow.py index 00b45e00b11..fcb1e5f013e 100644 --- a/homeassistant/components/vera/config_flow.py +++ b/homeassistant/components/vera/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Vera.""" + from __future__ import annotations from collections.abc import Mapping @@ -10,11 +11,16 @@ import pyvera as pv from requests.exceptions import RequestException import voluptuous as vol -from homeassistant import config_entries -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import ( + SOURCE_IMPORT, + SOURCE_USER, + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.const import CONF_EXCLUDE, CONF_LIGHTS, CONF_SOURCE from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import entity_registry as er from .const import CONF_CONTROLLER, CONF_LEGACY_UNIQUE_ID, DOMAIN @@ -68,7 +74,7 @@ def options_data(user_input: dict[str, str]) -> dict[str, list[int]]: ) -class OptionsFlowHandler(config_entries.OptionsFlow): +class OptionsFlowHandler(OptionsFlow): """Options for the component.""" def __init__(self, config_entry: ConfigEntry) -> None: @@ -78,7 +84,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow): async def async_step_init( self, user_input: dict[str, str] | None = None, - ) -> FlowResult: + ) -> ConfigFlowResult: """Manage the options.""" if user_input is not None: return self.async_create_entry( @@ -92,7 +98,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow): ) -class VeraFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class VeraFlowHandler(ConfigFlow, domain=DOMAIN): """Vera config flow.""" @staticmethod @@ -103,26 +109,26 @@ class VeraFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle user initiated flow.""" if user_input is not None: return await self.async_step_finish( { **user_input, **options_data(user_input), - **{CONF_SOURCE: config_entries.SOURCE_USER}, - **{CONF_LEGACY_UNIQUE_ID: False}, + CONF_SOURCE: SOURCE_USER, + CONF_LEGACY_UNIQUE_ID: False, } ) return self.async_show_form( step_id="user", data_schema=vol.Schema( - {**{vol.Required(CONF_CONTROLLER): str}, **options_schema()} + {vol.Required(CONF_CONTROLLER): str, **options_schema()} ), ) - async def async_step_import(self, config: dict[str, Any]) -> FlowResult: + async def async_step_import(self, config: dict[str, Any]) -> ConfigFlowResult: """Handle a flow initialized by import.""" # If there are entities with the legacy unique_id, then this imported config @@ -142,12 +148,12 @@ class VeraFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_finish( { **config, - **{CONF_SOURCE: config_entries.SOURCE_IMPORT}, - **{CONF_LEGACY_UNIQUE_ID: use_legacy_unique_id}, + CONF_SOURCE: SOURCE_IMPORT, + CONF_LEGACY_UNIQUE_ID: use_legacy_unique_id, } ) - async def async_step_finish(self, config: dict[str, Any]) -> FlowResult: + async def async_step_finish(self, config: dict[str, Any]) -> ConfigFlowResult: """Validate and create config entry.""" base_url = config[CONF_CONTROLLER] = config[CONF_CONTROLLER].rstrip("/") controller = pv.VeraController(base_url) diff --git a/homeassistant/components/vera/const.py b/homeassistant/components/vera/const.py index 34ac7faa669..0eb7534a81d 100644 --- a/homeassistant/components/vera/const.py +++ b/homeassistant/components/vera/const.py @@ -1,4 +1,5 @@ """Vera constants.""" + DOMAIN = "vera" CONF_CONTROLLER = "vera_controller_url" diff --git a/homeassistant/components/vera/cover.py b/homeassistant/components/vera/cover.py index 25b345b7e31..542680925f2 100644 --- a/homeassistant/components/vera/cover.py +++ b/homeassistant/components/vera/cover.py @@ -1,4 +1,5 @@ """Support for Vera cover - curtains, rollershutters etc.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/vera/light.py b/homeassistant/components/vera/light.py index c76cd76ad19..86e5dfa6a91 100644 --- a/homeassistant/components/vera/light.py +++ b/homeassistant/components/vera/light.py @@ -1,4 +1,5 @@ """Support for Vera lights.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/vera/lock.py b/homeassistant/components/vera/lock.py index 8994076ca31..01509aa8388 100644 --- a/homeassistant/components/vera/lock.py +++ b/homeassistant/components/vera/lock.py @@ -1,4 +1,5 @@ """Support for Vera locks.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/vera/scene.py b/homeassistant/components/vera/scene.py index daa3a6fc530..22061f98929 100644 --- a/homeassistant/components/vera/scene.py +++ b/homeassistant/components/vera/scene.py @@ -1,4 +1,5 @@ """Support for Vera scenes.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/vera/sensor.py b/homeassistant/components/vera/sensor.py index 2cee8f309aa..97e6d6d6314 100644 --- a/homeassistant/components/vera/sensor.py +++ b/homeassistant/components/vera/sensor.py @@ -1,4 +1,5 @@ """Support for Vera sensors.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/vera/switch.py b/homeassistant/components/vera/switch.py index 011f777b1b2..3e594685d6b 100644 --- a/homeassistant/components/vera/switch.py +++ b/homeassistant/components/vera/switch.py @@ -1,4 +1,5 @@ """Support for Vera switches.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/verisure/__init__.py b/homeassistant/components/verisure/__init__.py index 7d2ea7b7d6d..9e5f0ca2703 100644 --- a/homeassistant/components/verisure/__init__.py +++ b/homeassistant/components/verisure/__init__.py @@ -1,4 +1,5 @@ """Support for Verisure devices.""" + from __future__ import annotations from contextlib import suppress diff --git a/homeassistant/components/verisure/alarm_control_panel.py b/homeassistant/components/verisure/alarm_control_panel.py index 26e74cceb9e..fc7e7551145 100644 --- a/homeassistant/components/verisure/alarm_control_panel.py +++ b/homeassistant/components/verisure/alarm_control_panel.py @@ -1,4 +1,5 @@ """Support for Verisure alarm control panels.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/verisure/binary_sensor.py b/homeassistant/components/verisure/binary_sensor.py index 19a60602540..542ee3485ce 100644 --- a/homeassistant/components/verisure/binary_sensor.py +++ b/homeassistant/components/verisure/binary_sensor.py @@ -1,4 +1,5 @@ """Support for Verisure binary sensors.""" + from __future__ import annotations from homeassistant.components.binary_sensor import ( diff --git a/homeassistant/components/verisure/camera.py b/homeassistant/components/verisure/camera.py index e0505328245..72f5ab93c70 100644 --- a/homeassistant/components/verisure/camera.py +++ b/homeassistant/components/verisure/camera.py @@ -1,4 +1,5 @@ """Support for Verisure cameras.""" + from __future__ import annotations import errno diff --git a/homeassistant/components/verisure/config_flow.py b/homeassistant/components/verisure/config_flow.py index d945463fa5e..ccf74cd6791 100644 --- a/homeassistant/components/verisure/config_flow.py +++ b/homeassistant/components/verisure/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Verisure integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -12,10 +13,14 @@ from verisure import ( ) import voluptuous as vol -from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) 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 ( @@ -45,7 +50,7 @@ class VerisureConfigFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors: dict[str, str] = {} @@ -102,7 +107,7 @@ class VerisureConfigFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_mfa( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle multifactor authentication step.""" errors: dict[str, str] = {} @@ -134,7 +139,7 @@ class VerisureConfigFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_installation( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Select Verisure installation to add.""" installations_data = await self.hass.async_add_executor_job( self.verisure.get_installations @@ -170,7 +175,9 @@ class VerisureConfigFlowHandler(ConfigFlow, domain=DOMAIN): }, ) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Handle initiation of re-authentication with Verisure.""" self.entry = cast( ConfigEntry, @@ -180,7 +187,7 @@ class VerisureConfigFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle re-authentication with Verisure.""" errors: dict[str, str] = {} @@ -250,7 +257,7 @@ class VerisureConfigFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_reauth_mfa( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle multifactor authentication step during re-authentication.""" errors: dict[str, str] = {} @@ -303,7 +310,7 @@ class VerisureOptionsFlowHandler(OptionsFlow): async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Manage Verisure options.""" errors: dict[str, Any] = {} diff --git a/homeassistant/components/verisure/const.py b/homeassistant/components/verisure/const.py index ac30c58fde5..5b1aa1a0740 100644 --- a/homeassistant/components/verisure/const.py +++ b/homeassistant/components/verisure/const.py @@ -1,4 +1,5 @@ """Constants for the Verisure integration.""" + from datetime import timedelta import logging diff --git a/homeassistant/components/verisure/coordinator.py b/homeassistant/components/verisure/coordinator.py index f31d36aa2da..930d862257b 100644 --- a/homeassistant/components/verisure/coordinator.py +++ b/homeassistant/components/verisure/coordinator.py @@ -1,4 +1,5 @@ """DataUpdateCoordinator for the Verisure integration.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/verisure/diagnostics.py b/homeassistant/components/verisure/diagnostics.py index 8dbffe6eee3..a14e6e00b98 100644 --- a/homeassistant/components/verisure/diagnostics.py +++ b/homeassistant/components/verisure/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for Verisure.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/verisure/icons.json b/homeassistant/components/verisure/icons.json new file mode 100644 index 00000000000..35f6960b1e8 --- /dev/null +++ b/homeassistant/components/verisure/icons.json @@ -0,0 +1,7 @@ +{ + "services": { + "capture_smartcam": "mdi:camera", + "enable_autolock": "mdi:lock", + "disable_autolock": "mdi:lock-off" + } +} diff --git a/homeassistant/components/verisure/lock.py b/homeassistant/components/verisure/lock.py index 8e57c9695c0..227356a2525 100644 --- a/homeassistant/components/verisure/lock.py +++ b/homeassistant/components/verisure/lock.py @@ -1,4 +1,5 @@ """Support for Verisure locks.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/verisure/sensor.py b/homeassistant/components/verisure/sensor.py index 51947484dca..4f6e6b3d3c5 100644 --- a/homeassistant/components/verisure/sensor.py +++ b/homeassistant/components/verisure/sensor.py @@ -1,4 +1,5 @@ """Support for Verisure sensors.""" + from __future__ import annotations from homeassistant.components.sensor import ( diff --git a/homeassistant/components/verisure/switch.py b/homeassistant/components/verisure/switch.py index 96992cadb75..e0238097e01 100644 --- a/homeassistant/components/verisure/switch.py +++ b/homeassistant/components/verisure/switch.py @@ -1,4 +1,5 @@ """Support for Verisure Smartplugs.""" + from __future__ import annotations from time import monotonic @@ -89,9 +90,9 @@ class VerisureSmartplug(CoordinatorEntity[VerisureDataUpdateCoordinator], Switch async def async_set_plug_state(self, state: bool) -> None: """Set smartplug state.""" - command: dict[ - str, str | dict[str, str] - ] = self.coordinator.verisure.set_smartplug(self.serial_number, state) + command: dict[str, str | dict[str, str]] = ( + self.coordinator.verisure.set_smartplug(self.serial_number, state) + ) await self.hass.async_add_executor_job( self.coordinator.verisure.request, command, diff --git a/homeassistant/components/versasense/__init__.py b/homeassistant/components/versasense/__init__.py index d1ca23d82af..f209234f8c2 100644 --- a/homeassistant/components/versasense/__init__.py +++ b/homeassistant/components/versasense/__init__.py @@ -1,4 +1,5 @@ """Support for VersaSense MicroPnP devices.""" + import logging import pyversasense as pyv diff --git a/homeassistant/components/versasense/const.py b/homeassistant/components/versasense/const.py index 5283f61ac26..4f24fe637bb 100644 --- a/homeassistant/components/versasense/const.py +++ b/homeassistant/components/versasense/const.py @@ -1,4 +1,5 @@ """Constants for versasense.""" + KEY_CONSUMER = "consumer" KEY_IDENTIFIER = "identifier" KEY_MEASUREMENT = "measurement" diff --git a/homeassistant/components/versasense/sensor.py b/homeassistant/components/versasense/sensor.py index 349ed429b33..59d092ccdc1 100644 --- a/homeassistant/components/versasense/sensor.py +++ b/homeassistant/components/versasense/sensor.py @@ -1,4 +1,5 @@ """Support for VersaSense sensor peripheral.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/versasense/switch.py b/homeassistant/components/versasense/switch.py index b57013b539d..195045882ff 100644 --- a/homeassistant/components/versasense/switch.py +++ b/homeassistant/components/versasense/switch.py @@ -1,4 +1,5 @@ """Support for VersaSense actuator peripheral.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/version/__init__.py b/homeassistant/components/version/__init__.py index f05c2147449..4112cc51e46 100644 --- a/homeassistant/components/version/__init__.py +++ b/homeassistant/components/version/__init__.py @@ -1,4 +1,5 @@ """The Version integration.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/version/binary_sensor.py b/homeassistant/components/version/binary_sensor.py index cdfcb45fb5c..ff4f51e409f 100644 --- a/homeassistant/components/version/binary_sensor.py +++ b/homeassistant/components/version/binary_sensor.py @@ -1,4 +1,5 @@ """Binary sensor platform for Version.""" + from __future__ import annotations from awesomeversion import AwesomeVersion diff --git a/homeassistant/components/version/config_flow.py b/homeassistant/components/version/config_flow.py index 2fd670a7342..17cd07aac6f 100644 --- a/homeassistant/components/version/config_flow.py +++ b/homeassistant/components/version/config_flow.py @@ -1,13 +1,13 @@ """Config flow for Version integration.""" + from __future__ import annotations from typing import Any import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_SOURCE -from homeassistant.data_entry_flow import FlowResult from .const import ( ATTR_VERSION_SOURCE, @@ -33,7 +33,7 @@ from .const import ( ) -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class VersionConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Version.""" VERSION = 1 @@ -45,7 +45,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None, - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial user step.""" if user_input is None: self._entry_data = DEFAULT_CONFIGURATION.copy() @@ -78,7 +78,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_version_source( self, user_input: dict[str, Any] | None = None, - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the version_source step.""" if user_input is None: if self._entry_data[CONF_SOURCE] in ( diff --git a/homeassistant/components/version/const.py b/homeassistant/components/version/const.py index 069da3dca64..c0a5062bedb 100644 --- a/homeassistant/components/version/const.py +++ b/homeassistant/components/version/const.py @@ -1,4 +1,5 @@ """Constants for the Version integration.""" + from __future__ import annotations from datetime import timedelta @@ -91,7 +92,8 @@ VERSION_SOURCE_MAP: Final[dict[str, str]] = { VERSION_SOURCE_PYPI: "pypi", } -VALID_SOURCES: Final[list[str]] = HA_VERSION_SOURCES + [ +VALID_SOURCES: Final[list[str]] = [ + *HA_VERSION_SOURCES, "hassio", # Kept to not break existing configurations "docker", # Kept to not break existing configurations ] diff --git a/homeassistant/components/version/coordinator.py b/homeassistant/components/version/coordinator.py index a41816c0824..05adf07642b 100644 --- a/homeassistant/components/version/coordinator.py +++ b/homeassistant/components/version/coordinator.py @@ -1,4 +1,5 @@ """Data update coordinator for Version entities.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/version/diagnostics.py b/homeassistant/components/version/diagnostics.py index 20b84fabce7..194027d6ef4 100644 --- a/homeassistant/components/version/diagnostics.py +++ b/homeassistant/components/version/diagnostics.py @@ -1,4 +1,5 @@ """Provides diagnostics for Version.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/version/icons.json b/homeassistant/components/version/icons.json new file mode 100644 index 00000000000..e957bc07e3d --- /dev/null +++ b/homeassistant/components/version/icons.json @@ -0,0 +1,9 @@ +{ + "entity": { + "sensor": { + "version": { + "default": "mdi:package-up" + } + } + } +} diff --git a/homeassistant/components/version/sensor.py b/homeassistant/components/version/sensor.py index 82e49155603..6b0565b8cb3 100644 --- a/homeassistant/components/version/sensor.py +++ b/homeassistant/components/version/sensor.py @@ -1,4 +1,5 @@ """Sensor that can display the current Home Assistant versions.""" + from __future__ import annotations from typing import Any @@ -31,6 +32,7 @@ async def async_setup_entry( entity_description=SensorEntityDescription( key=str(entry.data[CONF_SOURCE]), name=entity_name, + translation_key="version", ), ) ] @@ -41,8 +43,6 @@ async def async_setup_entry( class VersionSensorEntity(VersionEntity, SensorEntity): """Version sensor entity class.""" - _attr_icon = "mdi:package-up" - @property def native_value(self) -> StateType: """Return the native value of this sensor.""" diff --git a/homeassistant/components/vesync/__init__.py b/homeassistant/components/vesync/__init__.py index 55addd81066..e758636900b 100644 --- a/homeassistant/components/vesync/__init__.py +++ b/homeassistant/components/vesync/__init__.py @@ -1,4 +1,5 @@ """VeSync integration.""" + import logging from pyvesync import VeSync diff --git a/homeassistant/components/vesync/common.py b/homeassistant/components/vesync/common.py index 0e01a593021..0212a7afa57 100644 --- a/homeassistant/components/vesync/common.py +++ b/homeassistant/components/vesync/common.py @@ -1,4 +1,5 @@ """Common utilities for VeSync Component.""" + import logging from typing import Any diff --git a/homeassistant/components/vesync/config_flow.py b/homeassistant/components/vesync/config_flow.py index 3f469d5eb81..15f9f548e35 100644 --- a/homeassistant/components/vesync/config_flow.py +++ b/homeassistant/components/vesync/config_flow.py @@ -1,17 +1,18 @@ """Config flow utilities.""" + from collections import OrderedDict from pyvesync import VeSync import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import callback from .const import DOMAIN -class VeSyncFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class VeSyncFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a config flow.""" VERSION = 1 diff --git a/homeassistant/components/vesync/diagnostics.py b/homeassistant/components/vesync/diagnostics.py index 8043e93b9e4..b56c8fc5db6 100644 --- a/homeassistant/components/vesync/diagnostics.py +++ b/homeassistant/components/vesync/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for VeSync.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/vesync/fan.py b/homeassistant/components/vesync/fan.py index f0d4d02a9a3..1d8ea6463bf 100644 --- a/homeassistant/components/vesync/fan.py +++ b/homeassistant/components/vesync/fan.py @@ -1,4 +1,5 @@ """Support for VeSync fans.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/vesync/icons.json b/homeassistant/components/vesync/icons.json new file mode 100644 index 00000000000..a4bf4afd410 --- /dev/null +++ b/homeassistant/components/vesync/icons.json @@ -0,0 +1,5 @@ +{ + "services": { + "update_devices": "mdi:update" + } +} diff --git a/homeassistant/components/vesync/light.py b/homeassistant/components/vesync/light.py index 040e9d5696d..9b15e635903 100644 --- a/homeassistant/components/vesync/light.py +++ b/homeassistant/components/vesync/light.py @@ -1,4 +1,5 @@ """Support for VeSync bulbs and wall dimmers.""" + import logging from typing import Any diff --git a/homeassistant/components/vesync/sensor.py b/homeassistant/components/vesync/sensor.py index 8cde8ea1036..81f42f4c2ee 100644 --- a/homeassistant/components/vesync/sensor.py +++ b/homeassistant/components/vesync/sensor.py @@ -1,4 +1,5 @@ """Support for voltage, power & energy sensors for VeSync outlets.""" + from __future__ import annotations from collections.abc import Callable @@ -35,19 +36,12 @@ from .const import DEV_TYPE_TO_HA, DOMAIN, SKU_TO_BASE_DEVICE, VS_DISCOVERY, VS_ _LOGGER = logging.getLogger(__name__) -@dataclass(frozen=True) -class VeSyncSensorEntityDescriptionMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class VeSyncSensorEntityDescription(SensorEntityDescription): + """Describe VeSync sensor entity.""" value_fn: Callable[[VeSyncAirBypass | VeSyncOutlet | VeSyncSwitch], StateType] - -@dataclass(frozen=True) -class VeSyncSensorEntityDescription( - SensorEntityDescription, VeSyncSensorEntityDescriptionMixin -): - """Describe VeSync sensor entity.""" - exists_fn: Callable[[VeSyncAirBypass | VeSyncOutlet | VeSyncSwitch], bool] = ( lambda _: True ) @@ -200,12 +194,15 @@ async def async_setup_entry( @callback def _setup_entities(devices, async_add_entities): """Check if device is online and add entity.""" - entities = [] - for dev in devices: - for description in SENSORS: - if description.exists_fn(dev): - entities.append(VeSyncSensorEntity(dev, description)) - async_add_entities(entities, update_before_add=True) + async_add_entities( + ( + VeSyncSensorEntity(dev, description) + for dev in devices + for description in SENSORS + if description.exists_fn(dev) + ), + update_before_add=True, + ) class VeSyncSensorEntity(VeSyncBaseEntity, SensorEntity): diff --git a/homeassistant/components/vesync/switch.py b/homeassistant/components/vesync/switch.py index e6101b2ba51..1d0c3472d53 100644 --- a/homeassistant/components/vesync/switch.py +++ b/homeassistant/components/vesync/switch.py @@ -1,4 +1,5 @@ """Support for VeSync switches.""" + import logging from typing import Any diff --git a/homeassistant/components/viaggiatreno/sensor.py b/homeassistant/components/viaggiatreno/sensor.py index ce439b9e628..3738b0f956a 100644 --- a/homeassistant/components/viaggiatreno/sensor.py +++ b/homeassistant/components/viaggiatreno/sensor.py @@ -1,4 +1,5 @@ """Support for the Italian train system using ViaggiaTreno API.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/vicare/__init__.py b/homeassistant/components/vicare/__init__.py index eec5f097535..0c87cd6f4fe 100644 --- a/homeassistant/components/vicare/__init__.py +++ b/homeassistant/components/vicare/__init__.py @@ -1,4 +1,5 @@ """The ViCare integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -20,7 +21,13 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.storage import STORAGE_DIR -from .const import DEFAULT_CACHE_DURATION, DEVICE_LIST, DOMAIN, PLATFORMS +from .const import ( + DEFAULT_CACHE_DURATION, + DEVICE_LIST, + DOMAIN, + PLATFORMS, + UNSUPPORTED_DEVICES, +) from .types import ViCareDevice from .utils import get_device @@ -109,5 +116,5 @@ def get_supported_devices( return [ device_config for device_config in devices - if device_config.getModel() not in ["Heatbox1", "Heatbox2_SRC"] + if device_config.getModel() not in UNSUPPORTED_DEVICES ] diff --git a/homeassistant/components/vicare/binary_sensor.py b/homeassistant/components/vicare/binary_sensor.py index a78b1fe5dab..2df8a2f06d3 100644 --- a/homeassistant/components/vicare/binary_sensor.py +++ b/homeassistant/components/vicare/binary_sensor.py @@ -1,4 +1,5 @@ """Viessmann ViCare sensor device.""" + from __future__ import annotations from collections.abc import Callable @@ -48,14 +49,12 @@ CIRCUIT_SENSORS: tuple[ViCareBinarySensorEntityDescription, ...] = ( ViCareBinarySensorEntityDescription( key="circulationpump_active", translation_key="circulation_pump", - icon="mdi:pump", device_class=BinarySensorDeviceClass.RUNNING, value_getter=lambda api: api.getCirculationPumpActive(), ), ViCareBinarySensorEntityDescription( key="frost_protection_active", translation_key="frost_protection", - icon="mdi:snowflake", value_getter=lambda api: api.getFrostProtectionActive(), ), ) @@ -64,7 +63,6 @@ BURNER_SENSORS: tuple[ViCareBinarySensorEntityDescription, ...] = ( ViCareBinarySensorEntityDescription( key="burner_active", translation_key="burner", - icon="mdi:gas-burner", device_class=BinarySensorDeviceClass.RUNNING, value_getter=lambda api: api.getActive(), ), @@ -83,7 +81,6 @@ GLOBAL_SENSORS: tuple[ViCareBinarySensorEntityDescription, ...] = ( ViCareBinarySensorEntityDescription( key="solar_pump_active", translation_key="solar_pump", - icon="mdi:pump", device_class=BinarySensorDeviceClass.RUNNING, value_getter=lambda api: api.getSolarPumpActive(), ), @@ -96,14 +93,12 @@ GLOBAL_SENSORS: tuple[ViCareBinarySensorEntityDescription, ...] = ( ViCareBinarySensorEntityDescription( key="dhw_circulationpump_active", translation_key="domestic_hot_water_circulation_pump", - icon="mdi:pump", device_class=BinarySensorDeviceClass.RUNNING, value_getter=lambda api: api.getDomesticHotWaterCirculationPumpActive(), ), ViCareBinarySensorEntityDescription( key="dhw_pump_active", translation_key="domestic_hot_water_pump", - icon="mdi:pump", device_class=BinarySensorDeviceClass.RUNNING, value_getter=lambda api: api.getDomesticHotWaterPumpActive(), ), diff --git a/homeassistant/components/vicare/button.py b/homeassistant/components/vicare/button.py index ae32e66dff3..c927055dadd 100644 --- a/homeassistant/components/vicare/button.py +++ b/homeassistant/components/vicare/button.py @@ -1,4 +1,5 @@ """Viessmann ViCare button device.""" + from __future__ import annotations from contextlib import suppress @@ -39,7 +40,6 @@ BUTTON_DESCRIPTIONS: tuple[ViCareButtonEntityDescription, ...] = ( ViCareButtonEntityDescription( key="activate_onetimecharge", translation_key="activate_onetimecharge", - icon="mdi:shower-head", entity_category=EntityCategory.CONFIG, value_getter=lambda api: api.getOneTimeCharge(), value_setter=lambda api: api.activateOneTimeCharge(), diff --git a/homeassistant/components/vicare/climate.py b/homeassistant/components/vicare/climate.py index c419e50e98c..490048190fa 100644 --- a/homeassistant/components/vicare/climate.py +++ b/homeassistant/components/vicare/climate.py @@ -1,4 +1,5 @@ """Viessmann ViCare climate device.""" + from __future__ import annotations from contextlib import suppress @@ -198,14 +199,14 @@ class ViCareClimate(ViCareEntity, ClimateEntity): } with suppress(PyViCareNotSupportedFeatureError): - self._attributes[ - "heating_curve_slope" - ] = self._circuit.getHeatingCurveSlope() + self._attributes["heating_curve_slope"] = ( + self._circuit.getHeatingCurveSlope() + ) with suppress(PyViCareNotSupportedFeatureError): - self._attributes[ - "heating_curve_shift" - ] = self._circuit.getHeatingCurveShift() + self._attributes["heating_curve_shift"] = ( + self._circuit.getHeatingCurveShift() + ) with suppress(PyViCareNotSupportedFeatureError): self._attributes["vicare_modes"] = self._circuit.getModes() diff --git a/homeassistant/components/vicare/config_flow.py b/homeassistant/components/vicare/config_flow.py index 32ae4af0fe7..67ce4f2c186 100644 --- a/homeassistant/components/vicare/config_flow.py +++ b/homeassistant/components/vicare/config_flow.py @@ -1,4 +1,5 @@ """Config flow for ViCare integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -11,10 +12,9 @@ from PyViCare.PyViCareUtils import ( ) import voluptuous as vol -from homeassistant import config_entries from homeassistant.components import dhcp +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_CLIENT_ID, CONF_PASSWORD, CONF_USERNAME -from homeassistant.data_entry_flow import FlowResult import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import format_mac @@ -46,15 +46,15 @@ USER_SCHEMA = REAUTH_SCHEMA.extend( ) -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class ViCareConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for ViCare.""" VERSION = 1 - entry: config_entries.ConfigEntry | None + entry: ConfigEntry | None async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Invoke when a user initiates a flow via the user interface.""" if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") @@ -77,14 +77,16 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Handle re-authentication with ViCare.""" self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) return await self.async_step_reauth_confirm() async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm re-authentication with ViCare.""" errors: dict[str, str] = {} assert self.entry is not None @@ -115,7 +117,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: + async def async_step_dhcp( + self, discovery_info: dhcp.DhcpServiceInfo + ) -> ConfigFlowResult: """Invoke when a Viessmann MAC address is discovered on the network.""" formatted_mac = format_mac(discovery_info.macaddress) _LOGGER.debug("Found device with mac %s", formatted_mac) diff --git a/homeassistant/components/vicare/const.py b/homeassistant/components/vicare/const.py index 8b76344843a..24ab94778e3 100644 --- a/homeassistant/components/vicare/const.py +++ b/homeassistant/components/vicare/const.py @@ -1,4 +1,5 @@ """Constants for the ViCare integration.""" + import enum from homeassistant.const import Platform @@ -14,6 +15,15 @@ PLATFORMS = [ Platform.WATER_HEATER, ] +UNSUPPORTED_DEVICES = [ + "Heatbox1", + "Heatbox2_SRC", + "E3_FloorHeatingCircuitChannel", + "E3_FloorHeatingCircuitDistributorBox", + "E3_RoomControl_One_522", + "E3_RoomSensor", +] + DEVICE_LIST = "device_list" VICARE_NAME = "ViCare" diff --git a/homeassistant/components/vicare/diagnostics.py b/homeassistant/components/vicare/diagnostics.py index 23a3c8640c5..9182e96509f 100644 --- a/homeassistant/components/vicare/diagnostics.py +++ b/homeassistant/components/vicare/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for ViCare.""" + from __future__ import annotations import json @@ -18,12 +19,15 @@ async def async_get_config_entry_diagnostics( hass: HomeAssistant, entry: ConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - data = [] - for device in hass.data[DOMAIN][entry.entry_id][DEVICE_LIST]: - data.append( - json.loads(await hass.async_add_executor_job(device.config.dump_secure)) - ) + + def dump_devices() -> list[dict[str, Any]]: + """Dump devices.""" + return [ + json.loads(device.config.dump_secure()) + for device in hass.data[DOMAIN][entry.entry_id][DEVICE_LIST] + ] + return { "entry": async_redact_data(entry.as_dict(), TO_REDACT), - "data": data, + "data": await hass.async_add_executor_job(dump_devices), } diff --git a/homeassistant/components/vicare/entity.py b/homeassistant/components/vicare/entity.py index af35c7bf8dd..1bb2993cd3a 100644 --- a/homeassistant/components/vicare/entity.py +++ b/homeassistant/components/vicare/entity.py @@ -1,4 +1,5 @@ """Entities for the ViCare integration.""" + from PyViCare.PyViCareDevice import Device as PyViCareDevice from PyViCare.PyViCareDeviceConfig import PyViCareDeviceConfig diff --git a/homeassistant/components/vicare/icons.json b/homeassistant/components/vicare/icons.json new file mode 100644 index 00000000000..2f40d8a8822 --- /dev/null +++ b/homeassistant/components/vicare/icons.json @@ -0,0 +1,93 @@ +{ + "entity": { + "binary_sensor": { + "circulation_pump": { + "default": "mdi:pump" + }, + "frost_protection": { + "default": "mdi:snowflake" + }, + "burner": { + "default": "mdi:gas-burner" + }, + "solar_pump": { + "default": "mdi:pump" + }, + "domestic_hot_water_circulation_pump": { + "default": "mdi:pump" + }, + "domestic_hot_water_pump": { + "default": "mdi:pump" + } + }, + "button": { + "activate_onetimecharge": { + "default": "mdi:shower-head" + } + }, + "number": { + "heating_curve_shift": { + "default": "mdi:plus-minus-variant" + }, + "heating_curve_slope": { + "default": "mdi:slope-uphill" + } + }, + "sensor": { + "volumetric_flow": { + "default": "mdi:gauge" + }, + "ess_state_of_charge": { + "default": "mdi:home-battery" + }, + "pcc_transfer_power_exchange": { + "default": "mdi:transmission-tower" + }, + "pcc_energy_consumption": { + "default": "mdi:transmission-tower-export" + }, + "pcc_energy_feed_in": { + "default": "mdi:transmission-tower-import" + }, + "photovoltaic_energy_production_today": { + "default": "mdi:solar-power" + }, + "burner_starts": { + "default": "mdi:counter" + }, + "burner_hours": { + "default": "mdi:counter" + }, + "burner_modulation": { + "default": "mdi:percent" + }, + "compressor_starts": { + "default": "mdi:counter" + }, + "compressor_hours": { + "default": "mdi:counter" + }, + "compressor_hours_loadclass1": { + "default": "mdi:counter" + }, + "compressor_hours_loadclass2": { + "default": "mdi:counter" + }, + "compressor_hours_loadclass3": { + "default": "mdi:counter" + }, + "compressor_hours_loadclass4": { + "default": "mdi:counter" + }, + "compressor_hours_loadclass5": { + "default": "mdi:counter" + }, + "compressor_phase": { + "default": "mdi:information" + } + } + }, + "services": { + "set_vicare_mode": "mdi:cog" + } +} diff --git a/homeassistant/components/vicare/number.py b/homeassistant/components/vicare/number.py index 70fefb6e8db..f92241ceace 100644 --- a/homeassistant/components/vicare/number.py +++ b/homeassistant/components/vicare/number.py @@ -1,4 +1,5 @@ """Number for ViCare.""" + from __future__ import annotations from collections.abc import Callable @@ -52,7 +53,6 @@ CIRCUIT_ENTITY_DESCRIPTIONS: tuple[ViCareNumberEntityDescription, ...] = ( ViCareNumberEntityDescription( key="heating curve shift", translation_key="heating_curve_shift", - icon="mdi:plus-minus-variant", entity_category=EntityCategory.CONFIG, device_class=NumberDeviceClass.TEMPERATURE, native_unit_of_measurement=UnitOfTemperature.CELSIUS, @@ -70,7 +70,6 @@ CIRCUIT_ENTITY_DESCRIPTIONS: tuple[ViCareNumberEntityDescription, ...] = ( ViCareNumberEntityDescription( key="heating curve slope", translation_key="heating_curve_slope", - icon="mdi:slope-uphill", entity_category=EntityCategory.CONFIG, value_getter=lambda api: api.getHeatingCurveSlope(), value_setter=lambda api, slope: ( diff --git a/homeassistant/components/vicare/sensor.py b/homeassistant/components/vicare/sensor.py index b36b363fc15..41266f8bde7 100644 --- a/homeassistant/components/vicare/sensor.py +++ b/homeassistant/components/vicare/sensor.py @@ -1,4 +1,5 @@ """Viessmann ViCare sensor device.""" + from __future__ import annotations from collections.abc import Callable @@ -590,7 +591,6 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( ViCareSensorEntityDescription( key="volumetric_flow", translation_key="volumetric_flow", - icon="mdi:gauge", native_unit_of_measurement=UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR, value_getter=lambda api: api.getVolumetricFlowReturn() / 1000, entity_category=EntityCategory.DIAGNOSTIC, @@ -598,7 +598,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( ), ViCareSensorEntityDescription( key="ess_state_of_charge", - icon="mdi:home-battery", + translation_key="ess_state_of_charge", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.BATTERY, state_class=SensorStateClass.MEASUREMENT, @@ -620,10 +620,48 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( options=["charge", "discharge", "standby"], value_getter=lambda api: api.getElectricalEnergySystemOperationState(), ), + ViCareSensorEntityDescription( + key="ess_discharge_today", + translation_key="ess_discharge_today", + state_class=SensorStateClass.TOTAL_INCREASING, + value_getter=lambda api: api.getElectricalEnergySystemTransferDischargeCumulatedCurrentDay(), + unit_getter=lambda api: api.getElectricalEnergySystemTransferDischargeCumulatedUnit(), + ), + ViCareSensorEntityDescription( + key="ess_discharge_this_week", + translation_key="ess_discharge_this_week", + state_class=SensorStateClass.TOTAL_INCREASING, + value_getter=lambda api: api.getElectricalEnergySystemTransferDischargeCumulatedCurrentWeek(), + unit_getter=lambda api: api.getElectricalEnergySystemTransferDischargeCumulatedUnit(), + entity_registry_enabled_default=False, + ), + ViCareSensorEntityDescription( + key="ess_discharge_this_month", + translation_key="ess_discharge_this_month", + state_class=SensorStateClass.TOTAL_INCREASING, + value_getter=lambda api: api.getElectricalEnergySystemTransferDischargeCumulatedCurrentMonth(), + unit_getter=lambda api: api.getElectricalEnergySystemTransferDischargeCumulatedUnit(), + entity_registry_enabled_default=False, + ), + ViCareSensorEntityDescription( + key="ess_discharge_this_year", + translation_key="ess_discharge_this_year", + state_class=SensorStateClass.TOTAL_INCREASING, + value_getter=lambda api: api.getElectricalEnergySystemTransferDischargeCumulatedCurrentYear(), + unit_getter=lambda api: api.getElectricalEnergySystemTransferDischargeCumulatedUnit(), + entity_registry_enabled_default=False, + ), + ViCareSensorEntityDescription( + key="ess_discharge_total", + translation_key="ess_discharge_total", + state_class=SensorStateClass.TOTAL_INCREASING, + value_getter=lambda api: api.getElectricalEnergySystemTransferDischargeCumulatedLifeCycle(), + unit_getter=lambda api: api.getElectricalEnergySystemTransferDischargeCumulatedUnit(), + entity_registry_enabled_default=False, + ), ViCareSensorEntityDescription( key="pcc_transfer_power_exchange", translation_key="pcc_transfer_power_exchange", - icon="mdi:transmission-tower", native_unit_of_measurement=UnitOfPower.WATT, state_class=SensorStateClass.MEASUREMENT, value_getter=lambda api: api.getPointOfCommonCouplingTransferPowerExchange(), @@ -631,7 +669,6 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( ViCareSensorEntityDescription( key="pcc_energy_consumption", translation_key="pcc_energy_consumption", - icon="mdi:transmission-tower-export", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, state_class=SensorStateClass.TOTAL_INCREASING, value_getter=lambda api: api.getPointOfCommonCouplingTransferConsumptionTotal(), @@ -640,7 +677,6 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( ViCareSensorEntityDescription( key="pcc_energy_feed_in", translation_key="pcc_energy_feed_in", - icon="mdi:transmission-tower-import", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, state_class=SensorStateClass.TOTAL_INCREASING, value_getter=lambda api: api.getPointOfCommonCouplingTransferFeedInTotal(), @@ -657,7 +693,6 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( ViCareSensorEntityDescription( key="photovoltaic_energy_production_today", translation_key="photovoltaic_energy_production_today", - icon="mdi:solar-power", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, state_class=SensorStateClass.TOTAL_INCREASING, value_getter=lambda api: api.getPhotovoltaicProductionCumulatedCurrentDay(), @@ -688,7 +723,6 @@ BURNER_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( ViCareSensorEntityDescription( key="burner_starts", translation_key="burner_starts", - icon="mdi:counter", value_getter=lambda api: api.getStarts(), entity_category=EntityCategory.DIAGNOSTIC, state_class=SensorStateClass.TOTAL_INCREASING, @@ -696,7 +730,6 @@ BURNER_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( ViCareSensorEntityDescription( key="burner_hours", translation_key="burner_hours", - icon="mdi:counter", native_unit_of_measurement=UnitOfTime.HOURS, value_getter=lambda api: api.getHours(), entity_category=EntityCategory.DIAGNOSTIC, @@ -705,7 +738,6 @@ BURNER_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( ViCareSensorEntityDescription( key="burner_modulation", translation_key="burner_modulation", - icon="mdi:percent", native_unit_of_measurement=PERCENTAGE, value_getter=lambda api: api.getModulation(), state_class=SensorStateClass.MEASUREMENT, @@ -716,7 +748,6 @@ COMPRESSOR_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( ViCareSensorEntityDescription( key="compressor_starts", translation_key="compressor_starts", - icon="mdi:counter", value_getter=lambda api: api.getStarts(), entity_category=EntityCategory.DIAGNOSTIC, state_class=SensorStateClass.TOTAL_INCREASING, @@ -724,7 +755,6 @@ COMPRESSOR_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( ViCareSensorEntityDescription( key="compressor_hours", translation_key="compressor_hours", - icon="mdi:counter", native_unit_of_measurement=UnitOfTime.HOURS, value_getter=lambda api: api.getHours(), entity_category=EntityCategory.DIAGNOSTIC, @@ -733,7 +763,6 @@ COMPRESSOR_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( ViCareSensorEntityDescription( key="compressor_hours_loadclass1", translation_key="compressor_hours_loadclass1", - icon="mdi:counter", native_unit_of_measurement=UnitOfTime.HOURS, value_getter=lambda api: api.getHoursLoadClass1(), entity_category=EntityCategory.DIAGNOSTIC, @@ -743,7 +772,6 @@ COMPRESSOR_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( ViCareSensorEntityDescription( key="compressor_hours_loadclass2", translation_key="compressor_hours_loadclass2", - icon="mdi:counter", native_unit_of_measurement=UnitOfTime.HOURS, value_getter=lambda api: api.getHoursLoadClass2(), entity_category=EntityCategory.DIAGNOSTIC, @@ -753,7 +781,6 @@ COMPRESSOR_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( ViCareSensorEntityDescription( key="compressor_hours_loadclass3", translation_key="compressor_hours_loadclass3", - icon="mdi:counter", native_unit_of_measurement=UnitOfTime.HOURS, value_getter=lambda api: api.getHoursLoadClass3(), entity_category=EntityCategory.DIAGNOSTIC, @@ -763,7 +790,6 @@ COMPRESSOR_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( ViCareSensorEntityDescription( key="compressor_hours_loadclass4", translation_key="compressor_hours_loadclass4", - icon="mdi:counter", native_unit_of_measurement=UnitOfTime.HOURS, value_getter=lambda api: api.getHoursLoadClass4(), entity_category=EntityCategory.DIAGNOSTIC, @@ -773,7 +799,6 @@ COMPRESSOR_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( ViCareSensorEntityDescription( key="compressor_hours_loadclass5", translation_key="compressor_hours_loadclass5", - icon="mdi:counter", native_unit_of_measurement=UnitOfTime.HOURS, value_getter=lambda api: api.getHoursLoadClass5(), entity_category=EntityCategory.DIAGNOSTIC, @@ -783,7 +808,6 @@ COMPRESSOR_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( ViCareSensorEntityDescription( key="compressor_phase", translation_key="compressor_phase", - icon="mdi:information", value_getter=lambda api: api.getPhase(), entity_category=EntityCategory.DIAGNOSTIC, ), diff --git a/homeassistant/components/vicare/strings.json b/homeassistant/components/vicare/strings.json index 0541be9631f..5a69cae4d29 100644 --- a/homeassistant/components/vicare/strings.json +++ b/homeassistant/components/vicare/strings.json @@ -286,6 +286,21 @@ "standby": "Standby" } }, + "ess_discharge_today": { + "name": "Battery discharge today" + }, + "ess_discharge_this_week": { + "name": "Battery discharge this week" + }, + "ess_discharge_this_month": { + "name": "Battery discharge this month" + }, + "ess_discharge_this_year": { + "name": "Battery discharge this year" + }, + "ess_discharge_total": { + "name": "Battery discharge total" + }, "pcc_current_power_exchange": { "name": "Grid power exchange" }, diff --git a/homeassistant/components/vicare/types.py b/homeassistant/components/vicare/types.py index 83b15a6bcf7..2bed638bfb9 100644 --- a/homeassistant/components/vicare/types.py +++ b/homeassistant/components/vicare/types.py @@ -1,4 +1,5 @@ """Types for the ViCare integration.""" + from collections.abc import Callable from dataclasses import dataclass import enum diff --git a/homeassistant/components/vicare/utils.py b/homeassistant/components/vicare/utils.py index 649b1859442..2019f28a896 100644 --- a/homeassistant/components/vicare/utils.py +++ b/homeassistant/components/vicare/utils.py @@ -1,4 +1,5 @@ """ViCare helpers functions.""" + import logging from PyViCare.PyViCareDevice import Device as PyViCareDevice diff --git a/homeassistant/components/vicare/water_heater.py b/homeassistant/components/vicare/water_heater.py index 9a8fb7eb092..223217f4e13 100644 --- a/homeassistant/components/vicare/water_heater.py +++ b/homeassistant/components/vicare/water_heater.py @@ -1,4 +1,5 @@ """Viessmann ViCare water_heater device.""" + from __future__ import annotations from contextlib import suppress diff --git a/homeassistant/components/vilfo/__init__.py b/homeassistant/components/vilfo/__init__.py index 82fe4c7bb70..fe00fa494b5 100644 --- a/homeassistant/components/vilfo/__init__.py +++ b/homeassistant/components/vilfo/__init__.py @@ -1,4 +1,5 @@ """The Vilfo Router integration.""" + from datetime import timedelta import logging diff --git a/homeassistant/components/vilfo/config_flow.py b/homeassistant/components/vilfo/config_flow.py index f174a7697d3..47e45aecadd 100644 --- a/homeassistant/components/vilfo/config_flow.py +++ b/homeassistant/components/vilfo/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Vilfo Router integration.""" + import logging from vilfo import Client as VilfoClient @@ -8,8 +9,10 @@ from vilfo.exceptions import ( ) import voluptuous as vol -from homeassistant import config_entries, core, exceptions +from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST, CONF_ID, CONF_MAC +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.util.network import is_host_valid from .const import DOMAIN, ROUTER_DEFAULT_HOST @@ -62,7 +65,7 @@ def _try_connect_and_fetch_basic_info(host, token): return result -async def validate_input(hass: core.HomeAssistant, data): +async def validate_input(hass: HomeAssistant, data): """Validate the user input allows us to connect. Data has the keys from DATA_SCHEMA with values provided by the user. @@ -91,7 +94,7 @@ async def validate_input(hass: core.HomeAssistant, data): return config -class DomainConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class DomainConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Vilfo Router.""" VERSION = 1 @@ -122,13 +125,13 @@ class DomainConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -class CannotConnect(exceptions.HomeAssistantError): +class CannotConnect(HomeAssistantError): """Error to indicate we cannot connect.""" -class InvalidAuth(exceptions.HomeAssistantError): +class InvalidAuth(HomeAssistantError): """Error to indicate there is invalid auth.""" -class InvalidHost(exceptions.HomeAssistantError): +class InvalidHost(HomeAssistantError): """Error to indicate that hostname/IP address is invalid.""" diff --git a/homeassistant/components/vilfo/const.py b/homeassistant/components/vilfo/const.py index e562add4e0f..e129437df7e 100644 --- a/homeassistant/components/vilfo/const.py +++ b/homeassistant/components/vilfo/const.py @@ -1,4 +1,5 @@ """Constants for the Vilfo Router integration.""" + from __future__ import annotations DOMAIN = "vilfo" diff --git a/homeassistant/components/vilfo/icons.json b/homeassistant/components/vilfo/icons.json new file mode 100644 index 00000000000..0b2e2a45a16 --- /dev/null +++ b/homeassistant/components/vilfo/icons.json @@ -0,0 +1,12 @@ +{ + "entity": { + "sensor": { + "load": { + "default": "mdi:memory" + }, + "boot_time": { + "default": "mdi:timer-outline" + } + } + } +} diff --git a/homeassistant/components/vilfo/manifest.json b/homeassistant/components/vilfo/manifest.json index 850d4fecb27..9fa52072ddf 100644 --- a/homeassistant/components/vilfo/manifest.json +++ b/homeassistant/components/vilfo/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/vilfo", "iot_class": "local_polling", "loggers": ["vilfo"], - "requirements": ["vilfo-api-client==0.4.1"] + "requirements": ["vilfo-api-client==0.5.0"] } diff --git a/homeassistant/components/vilfo/sensor.py b/homeassistant/components/vilfo/sensor.py index c72edf1b7db..77a7df7a0a8 100644 --- a/homeassistant/components/vilfo/sensor.py +++ b/homeassistant/components/vilfo/sensor.py @@ -1,4 +1,5 @@ """Support for Vilfo Router sensors.""" + from dataclasses import dataclass from homeassistant.components.sensor import ( @@ -24,30 +25,23 @@ from .const import ( ) -@dataclass(frozen=True) -class VilfoRequiredKeysMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class VilfoSensorEntityDescription(SensorEntityDescription): + """Describes Vilfo sensor entity.""" api_key: str -@dataclass(frozen=True) -class VilfoSensorEntityDescription(SensorEntityDescription, VilfoRequiredKeysMixin): - """Describes Vilfo sensor entity.""" - - SENSOR_TYPES: tuple[VilfoSensorEntityDescription, ...] = ( VilfoSensorEntityDescription( key=ATTR_LOAD, translation_key=ATTR_LOAD, native_unit_of_measurement=PERCENTAGE, - icon="mdi:memory", api_key=ATTR_API_DATA_FIELD_LOAD, ), VilfoSensorEntityDescription( key=ATTR_BOOT_TIME, translation_key=ATTR_BOOT_TIME, - icon="mdi:timer-outline", api_key=ATTR_API_DATA_FIELD_BOOT_TIME, device_class=SensorDeviceClass.TIMESTAMP, ), diff --git a/homeassistant/components/vivotek/camera.py b/homeassistant/components/vivotek/camera.py index a61897f996e..8719d55ec29 100644 --- a/homeassistant/components/vivotek/camera.py +++ b/homeassistant/components/vivotek/camera.py @@ -1,4 +1,5 @@ """Support for Vivotek IP Cameras.""" + from __future__ import annotations from libpyvivotek import VivotekCamera diff --git a/homeassistant/components/vizio/__init__.py b/homeassistant/components/vizio/__init__.py index 2e468087725..b8df8fb4529 100644 --- a/homeassistant/components/vizio/__init__.py +++ b/homeassistant/components/vizio/__init__.py @@ -1,4 +1,5 @@ """The vizio component.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/vizio/config_flow.py b/homeassistant/components/vizio/config_flow.py index 792407d2545..fb5f74f4e09 100644 --- a/homeassistant/components/vizio/config_flow.py +++ b/homeassistant/components/vizio/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Vizio.""" + from __future__ import annotations import copy @@ -10,7 +11,6 @@ from pyvizio import VizioAsync, async_guess_device_type from pyvizio.const import APP_HOME import voluptuous as vol -from homeassistant import config_entries from homeassistant.components import zeroconf from homeassistant.components.media_player import MediaPlayerDeviceClass from homeassistant.config_entries import ( @@ -18,6 +18,9 @@ from homeassistant.config_entries import ( SOURCE_IMPORT, SOURCE_ZEROCONF, ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, ) from homeassistant.const import ( CONF_ACCESS_TOKEN, @@ -29,7 +32,6 @@ from homeassistant.const import ( CONF_PIN, ) from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.util.network import is_ip_address @@ -103,7 +105,7 @@ def _host_is_same(host1: str, host2: str) -> bool: return host1 == host2 -class VizioOptionsConfigFlow(config_entries.OptionsFlow): +class VizioOptionsConfigFlow(OptionsFlow): """Handle Vizio options.""" def __init__(self, config_entry: ConfigEntry) -> None: @@ -112,7 +114,7 @@ class VizioOptionsConfigFlow(config_entries.OptionsFlow): async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Manage the vizio options.""" if user_input is not None: if user_input.get(CONF_APPS_TO_INCLUDE_OR_EXCLUDE): @@ -173,7 +175,7 @@ class VizioOptionsConfigFlow(config_entries.OptionsFlow): return self.async_show_form(step_id="init", data_schema=options) -class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class VizioConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a Vizio config flow.""" VERSION = 1 @@ -193,7 +195,7 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._data: dict[str, Any] | None = None self._apps: dict[str, list] = {} - async def _create_entry(self, input_dict: dict[str, Any]) -> FlowResult: + async def _create_entry(self, input_dict: dict[str, Any]) -> ConfigFlowResult: """Create vizio config entry.""" # Remove extra keys that will not be used by entry setup input_dict.pop(CONF_APPS_TO_INCLUDE_OR_EXCLUDE, None) @@ -206,7 +208,7 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" errors: dict[str, str] = {} @@ -283,7 +285,9 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form(step_id="user", data_schema=schema, errors=errors) - async def async_step_import(self, import_config: dict[str, Any]) -> FlowResult: + async def async_step_import( + self, import_config: dict[str, Any] + ) -> ConfigFlowResult: """Import a config entry from configuration.yaml.""" # Check if new config entry matches any existing config entries for entry in self._async_current_entries(): @@ -347,7 +351,7 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_zeroconf( self, discovery_info: zeroconf.ZeroconfServiceInfo - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle zeroconf discovery.""" host = discovery_info.host # If host already has port, no need to add it again @@ -387,7 +391,7 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_pair_tv( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Start pairing process for TV. Ask user for PIN to complete pairing process. @@ -452,7 +456,7 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def _pairing_complete(self, step_id: str) -> FlowResult: + async def _pairing_complete(self, step_id: str) -> ConfigFlowResult: """Handle config flow completion.""" assert self._data if not self._must_show_form: @@ -466,7 +470,7 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_pairing_complete( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Complete non-import sourced config flow. Display final message to user confirming pairing. @@ -475,7 +479,7 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_pairing_complete_import( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Complete import sourced config flow. Display final message to user confirming pairing and displaying diff --git a/homeassistant/components/vizio/const.py b/homeassistant/components/vizio/const.py index 9615032c097..12de3af1cb0 100644 --- a/homeassistant/components/vizio/const.py +++ b/homeassistant/components/vizio/const.py @@ -1,4 +1,5 @@ """Constants used by vizio component.""" + from pyvizio.const import ( DEVICE_CLASS_SPEAKER as VIZIO_DEVICE_CLASS_SPEAKER, DEVICE_CLASS_TV as VIZIO_DEVICE_CLASS_TV, @@ -49,10 +50,6 @@ DEFAULT_VOLUME_STEP = 1 DEVICE_ID = "pyvizio" DOMAIN = "vizio" -ICON = { - MediaPlayerDeviceClass.TV: "mdi:television", - MediaPlayerDeviceClass.SPEAKER: "mdi:speaker", -} COMMON_SUPPORTED_COMMANDS = ( MediaPlayerEntityFeature.SELECT_SOURCE diff --git a/homeassistant/components/vizio/icons.json b/homeassistant/components/vizio/icons.json new file mode 100644 index 00000000000..ccdaf816bb0 --- /dev/null +++ b/homeassistant/components/vizio/icons.json @@ -0,0 +1,5 @@ +{ + "services": { + "update_setting": "mdi:cog" + } +} diff --git a/homeassistant/components/vizio/media_player.py b/homeassistant/components/vizio/media_player.py index db3995772d4..c19c091bb3d 100644 --- a/homeassistant/components/vizio/media_player.py +++ b/homeassistant/components/vizio/media_player.py @@ -1,4 +1,5 @@ """Vizio SmartCast Device support.""" + from __future__ import annotations from datetime import timedelta @@ -42,7 +43,6 @@ from .const import ( DEFAULT_VOLUME_STEP, DEVICE_ID, DOMAIN, - ICON, SERVICE_UPDATE_SETTING, SUPPORTED_COMMANDS, UPDATE_SETTING_SCHEMA, @@ -166,7 +166,6 @@ class VizioDevice(MediaPlayerEntity): self._attr_supported_features = SUPPORTED_COMMANDS[device_class] # Entity class attributes that will not change - self._attr_icon = ICON[device_class] unique_id = config_entry.unique_id assert unique_id self._attr_unique_id = unique_id diff --git a/homeassistant/components/vlc/media_player.py b/homeassistant/components/vlc/media_player.py index 9f28a60427c..53831fb8db0 100644 --- a/homeassistant/components/vlc/media_player.py +++ b/homeassistant/components/vlc/media_player.py @@ -1,4 +1,5 @@ """Provide functionality to interact with vlc devices on the network.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/vlc_telnet/__init__.py b/homeassistant/components/vlc_telnet/__init__.py index a8a0c16be8e..67c45c5dbdf 100644 --- a/homeassistant/components/vlc_telnet/__init__.py +++ b/homeassistant/components/vlc_telnet/__init__.py @@ -1,4 +1,5 @@ """The VLC media player Telnet integration.""" + from aiovlc.client import Client from aiovlc.exceptions import AuthError, ConnectError @@ -35,7 +36,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await vlc.login() except AuthError as err: await disconnect_vlc(vlc) - raise ConfigEntryAuthFailed() from err + raise ConfigEntryAuthFailed from err domain_data = hass.data.setdefault(DOMAIN, {}) domain_data[entry.entry_id] = {DATA_VLC: vlc, DATA_AVAILABLE: available} diff --git a/homeassistant/components/vlc_telnet/config_flow.py b/homeassistant/components/vlc_telnet/config_flow.py index 6995a16c3ab..67325686282 100644 --- a/homeassistant/components/vlc_telnet/config_flow.py +++ b/homeassistant/components/vlc_telnet/config_flow.py @@ -1,4 +1,5 @@ """Config flow for VLC media player Telnet integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -9,11 +10,11 @@ from aiovlc.client import Client from aiovlc.exceptions import AuthError, ConnectError import voluptuous as vol -from homeassistant import core, exceptions from homeassistant.components.hassio import HassioServiceInfo -from homeassistant.config_entries import ConfigEntry, ConfigFlow +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT -from homeassistant.data_entry_flow import FlowResult +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from .const import DEFAULT_PORT, DOMAIN @@ -46,9 +47,7 @@ async def vlc_connect(vlc: Client) -> None: await vlc.disconnect() -async def validate_input( - hass: core.HomeAssistant, data: dict[str, Any] -) -> dict[str, str]: +async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, str]: """Validate the user input allows us to connect.""" vlc = Client( password=data[CONF_PASSWORD], @@ -76,7 +75,7 @@ class VLCTelnetConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" if user_input is None: return self.async_show_form( @@ -105,7 +104,9 @@ class VLCTelnetConfigFlow(ConfigFlow, domain=DOMAIN): step_id="user", data_schema=user_form_schema(user_input), errors=errors ) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Handle reauth flow.""" self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) assert self.entry @@ -114,7 +115,7 @@ class VLCTelnetConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle reauth confirm.""" assert self.entry errors = {} @@ -149,7 +150,9 @@ class VLCTelnetConfigFlow(ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_hassio(self, discovery_info: HassioServiceInfo) -> FlowResult: + async def async_step_hassio( + self, discovery_info: HassioServiceInfo + ) -> ConfigFlowResult: """Handle the discovery step via hassio.""" await self.async_set_unique_id("hassio") self._abort_if_unique_id_configured(discovery_info.config) @@ -160,7 +163,7 @@ class VLCTelnetConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_hassio_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm Supervisor discovery.""" assert self.hassio_discovery if user_input is None: @@ -184,9 +187,9 @@ class VLCTelnetConfigFlow(ConfigFlow, domain=DOMAIN): return self.async_create_entry(title=info["title"], data=self.hassio_discovery) -class CannotConnect(exceptions.HomeAssistantError): +class CannotConnect(HomeAssistantError): """Error to indicate we cannot connect.""" -class InvalidAuth(exceptions.HomeAssistantError): +class InvalidAuth(HomeAssistantError): """Error to indicate there is invalid auth.""" diff --git a/homeassistant/components/vlc_telnet/const.py b/homeassistant/components/vlc_telnet/const.py index 432de5aa854..e51339bdd1e 100644 --- a/homeassistant/components/vlc_telnet/const.py +++ b/homeassistant/components/vlc_telnet/const.py @@ -1,4 +1,5 @@ """Integration shared constants.""" + import logging DATA_VLC = "vlc" diff --git a/homeassistant/components/vlc_telnet/media_player.py b/homeassistant/components/vlc_telnet/media_player.py index b84676776f5..fa021352d81 100644 --- a/homeassistant/components/vlc_telnet/media_player.py +++ b/homeassistant/components/vlc_telnet/media_player.py @@ -1,4 +1,5 @@ """Provide functionality to interact with the vlc telnet interface.""" + from __future__ import annotations from collections.abc import Awaitable, Callable, Coroutine diff --git a/homeassistant/components/vodafone_station/button.py b/homeassistant/components/vodafone_station/button.py index 3840af3d593..efea011a541 100644 --- a/homeassistant/components/vodafone_station/button.py +++ b/homeassistant/components/vodafone_station/button.py @@ -1,4 +1,5 @@ """Vodafone Station buttons.""" + from __future__ import annotations from collections.abc import Callable @@ -20,21 +21,14 @@ from .const import _LOGGER, DOMAIN from .coordinator import VodafoneStationRouter -@dataclass(frozen=True) -class VodafoneStationBaseEntityDescriptionMixin: - """Mixin to describe a Button entity.""" +@dataclass(frozen=True, kw_only=True) +class VodafoneStationEntityDescription(ButtonEntityDescription): + """Vodafone Station entity description.""" press_action: Callable[[VodafoneStationRouter], Any] is_suitable: Callable[[dict], bool] -@dataclass(frozen=True) -class VodafoneStationEntityDescription( - ButtonEntityDescription, VodafoneStationBaseEntityDescriptionMixin -): - """Vodafone Station entity description.""" - - BUTTON_TYPES: Final = ( VodafoneStationEntityDescription( key="reboot", diff --git a/homeassistant/components/vodafone_station/config_flow.py b/homeassistant/components/vodafone_station/config_flow.py index 987d4d71f41..ed7f63b6c39 100644 --- a/homeassistant/components/vodafone_station/config_flow.py +++ b/homeassistant/components/vodafone_station/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Vodafone Station integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -14,12 +15,12 @@ from homeassistant.components.device_tracker import ( from homeassistant.config_entries import ( ConfigEntry, ConfigFlow, + ConfigFlowResult, OptionsFlow, OptionsFlowWithConfigEntry, ) from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant, callback -from homeassistant.data_entry_flow import FlowResult from .const import _LOGGER, DEFAULT_HOST, DEFAULT_USERNAME, DOMAIN @@ -69,7 +70,7 @@ class VodafoneStationConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" if user_input is None: return self.async_show_form( @@ -101,7 +102,9 @@ class VodafoneStationConfigFlow(ConfigFlow, domain=DOMAIN): step_id="user", data_schema=user_form_schema(user_input), errors=errors ) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Handle reauth flow.""" self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) assert self.entry @@ -110,7 +113,7 @@ class VodafoneStationConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle reauth confirm.""" assert self.entry errors = {} @@ -153,7 +156,7 @@ class VodafoneStationOptionsFlowHandler(OptionsFlowWithConfigEntry): async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle options flow.""" if user_input is not None: diff --git a/homeassistant/components/vodafone_station/const.py b/homeassistant/components/vodafone_station/const.py index c4828e19951..14cfaabdf7a 100644 --- a/homeassistant/components/vodafone_station/const.py +++ b/homeassistant/components/vodafone_station/const.py @@ -1,4 +1,5 @@ """Vodafone Station constants.""" + import logging _LOGGER = logging.getLogger(__package__) diff --git a/homeassistant/components/vodafone_station/coordinator.py b/homeassistant/components/vodafone_station/coordinator.py index ff51f009f3c..cf096a93d50 100644 --- a/homeassistant/components/vodafone_station/coordinator.py +++ b/homeassistant/components/vodafone_station/coordinator.py @@ -1,4 +1,5 @@ """Support for Vodafone Station.""" + from dataclasses import dataclass from datetime import datetime, timedelta from typing import Any diff --git a/homeassistant/components/vodafone_station/device_tracker.py b/homeassistant/components/vodafone_station/device_tracker.py index 9f98da88d22..85ad834cd23 100644 --- a/homeassistant/components/vodafone_station/device_tracker.py +++ b/homeassistant/components/vodafone_station/device_tracker.py @@ -1,4 +1,5 @@ """Support for Vodafone Station routers.""" + from __future__ import annotations from aiovodafone import VodafoneStationDevice @@ -61,6 +62,8 @@ def async_add_new_tracked_entities( class VodafoneStationTracker(CoordinatorEntity[VodafoneStationRouter], ScannerEntity): """Representation of a Vodafone Station device.""" + _attr_translation_key = "device_tracker" + def __init__( self, coordinator: VodafoneStationRouter, device_info: VodafoneStationDeviceInfo ) -> None: @@ -98,11 +101,6 @@ class VodafoneStationTracker(CoordinatorEntity[VodafoneStationRouter], ScannerEn """Return the hostname of device.""" return self._attr_name - @property - def icon(self) -> str: - """Return device icon.""" - return "mdi:lan-connect" if self._device.connected else "mdi:lan-disconnect" - @property def ip_address(self) -> str | None: """Return the primary ip address of the device.""" diff --git a/homeassistant/components/vodafone_station/icons.json b/homeassistant/components/vodafone_station/icons.json new file mode 100644 index 00000000000..b504fc5cc5f --- /dev/null +++ b/homeassistant/components/vodafone_station/icons.json @@ -0,0 +1,44 @@ +{ + "entity": { + "device_tracker": { + "device_tracker": { + "default": "mdi:lan-disconnect", + "state": { + "home": "mdi:lan-connect" + } + } + }, + "sensor": { + "external_ipv4": { + "default": "mdi:earth" + }, + "external_ipv6": { + "default": "mdi:earth" + }, + "external_ip_key": { + "default": "mdi:earth" + }, + "active_connection": { + "default": "mdi:wan" + }, + "fw_version": { + "default": "mdi:new-box" + }, + "phone_num1": { + "default": "mdi:phone" + }, + "phone_num2": { + "default": "mdi:phone" + }, + "sys_cpu_usage": { + "default": "mdi:chip" + }, + "sys_memory_usage": { + "default": "mdi:memory" + }, + "sys_reboot_cause": { + "default": "mdi:restart-alert" + } + } + } +} diff --git a/homeassistant/components/vodafone_station/sensor.py b/homeassistant/components/vodafone_station/sensor.py index b383c2d193a..937c0220cbf 100644 --- a/homeassistant/components/vodafone_station/sensor.py +++ b/homeassistant/components/vodafone_station/sensor.py @@ -1,4 +1,5 @@ """Vodafone Station sensors.""" + from __future__ import annotations from collections.abc import Callable @@ -24,9 +25,9 @@ from .coordinator import VodafoneStationRouter NOT_AVAILABLE: list = ["", "N/A", "0.0.0.0"] -@dataclass(frozen=True) -class VodafoneStationBaseEntityDescription: - """Vodafone Station entity base description.""" +@dataclass(frozen=True, kw_only=True) +class VodafoneStationEntityDescription(SensorEntityDescription): + """Vodafone Station entity description.""" value: Callable[[Any, Any], Any] = ( lambda coordinator, key: coordinator.data.sensors[key] @@ -34,13 +35,6 @@ class VodafoneStationBaseEntityDescription: is_suitable: Callable[[dict], bool] = lambda val: True -@dataclass(frozen=True, kw_only=True) -class VodafoneStationEntityDescription( - VodafoneStationBaseEntityDescription, SensorEntityDescription -): - """Vodafone Station entity description.""" - - def _calculate_uptime(coordinator: VodafoneStationRouter, key: str) -> datetime: """Calculate device uptime.""" @@ -72,26 +66,22 @@ SENSOR_TYPES: Final = ( VodafoneStationEntityDescription( key="wan_ip4_addr", translation_key="external_ipv4", - icon="mdi:earth", is_suitable=lambda info: info["wan_ip4_addr"] not in NOT_AVAILABLE, ), VodafoneStationEntityDescription( key="wan_ip6_addr", translation_key="external_ipv6", - icon="mdi:earth", is_suitable=lambda info: info["wan_ip6_addr"] not in NOT_AVAILABLE, ), VodafoneStationEntityDescription( key="vf_internet_key_ip_addr", translation_key="external_ip_key", - icon="mdi:earth", is_suitable=lambda info: info["vf_internet_key_ip_addr"] not in NOT_AVAILABLE, ), VodafoneStationEntityDescription( key="inter_ip_address", translation_key="active_connection", device_class=SensorDeviceClass.ENUM, - icon="mdi:wan", options=LINE_TYPES, value=_line_connection, ), @@ -112,19 +102,16 @@ SENSOR_TYPES: Final = ( VodafoneStationEntityDescription( key="fw_version", translation_key="fw_version", - icon="mdi:new-box", entity_category=EntityCategory.DIAGNOSTIC, ), VodafoneStationEntityDescription( key="phone_num1", translation_key="phone_num1", - icon="mdi:phone", is_suitable=lambda info: info["phone_unavailable1"] == "0", ), VodafoneStationEntityDescription( key="phone_num2", translation_key="phone_num2", - icon="mdi:phone", is_suitable=lambda info: info["phone_unavailable2"] == "0", ), VodafoneStationEntityDescription( @@ -137,7 +124,6 @@ SENSOR_TYPES: Final = ( VodafoneStationEntityDescription( key="sys_cpu_usage", translation_key="sys_cpu_usage", - icon="mdi:chip", native_unit_of_measurement=PERCENTAGE, entity_category=EntityCategory.DIAGNOSTIC, value=lambda coordinator, key: float(coordinator.data.sensors[key][:-1]), @@ -145,7 +131,6 @@ SENSOR_TYPES: Final = ( VodafoneStationEntityDescription( key="sys_memory_usage", translation_key="sys_memory_usage", - icon="mdi:memory", native_unit_of_measurement=PERCENTAGE, entity_category=EntityCategory.DIAGNOSTIC, value=lambda coordinator, key: float(coordinator.data.sensors[key][:-1]), @@ -153,7 +138,6 @@ SENSOR_TYPES: Final = ( VodafoneStationEntityDescription( key="sys_reboot_cause", translation_key="sys_reboot_cause", - icon="mdi:restart-alert", entity_category=EntityCategory.DIAGNOSTIC, ), ) diff --git a/homeassistant/components/voicerss/tts.py b/homeassistant/components/voicerss/tts.py index 4ac2aae0a71..581f4090657 100644 --- a/homeassistant/components/voicerss/tts.py +++ b/homeassistant/components/voicerss/tts.py @@ -1,4 +1,5 @@ """Support for the voicerss speech service.""" + import asyncio from http import HTTPStatus import logging diff --git a/homeassistant/components/voip/__init__.py b/homeassistant/components/voip/__init__.py index f29705cf41b..9ab6a8bf0e8 100644 --- a/homeassistant/components/voip/__init__.py +++ b/homeassistant/components/voip/__init__.py @@ -1,4 +1,5 @@ """The Voice over IP integration.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/voip/config_flow.py b/homeassistant/components/voip/config_flow.py index 3af15bd2c0b..821c7f29a1e 100644 --- a/homeassistant/components/voip/config_flow.py +++ b/homeassistant/components/voip/config_flow.py @@ -1,4 +1,5 @@ """Config flow for VoIP integration.""" + from __future__ import annotations from typing import Any @@ -6,22 +7,26 @@ from typing import Any from voip_utils import SIP_PORT import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_validation as cv from .const import CONF_SIP_PORT, DOMAIN -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class VoIPConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for VoIP integration.""" VERSION = 1 async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") @@ -39,22 +44,22 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback def async_get_options_flow( - config_entry: config_entries.ConfigEntry, - ) -> config_entries.OptionsFlow: + config_entry: ConfigEntry, + ) -> OptionsFlow: """Create the options flow.""" return VoipOptionsFlowHandler(config_entry) -class VoipOptionsFlowHandler(config_entries.OptionsFlow): +class VoipOptionsFlowHandler(OptionsFlow): """Handle VoIP options.""" - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + 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: + ) -> ConfigFlowResult: """Manage the options.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) diff --git a/homeassistant/components/voip/devices.py b/homeassistant/components/voip/devices.py index 5da7a97ec24..9acc04f6879 100644 --- a/homeassistant/components/voip/devices.py +++ b/homeassistant/components/voip/devices.py @@ -1,4 +1,5 @@ """Class to manage devices.""" + from __future__ import annotations from collections.abc import Callable, Iterator @@ -96,7 +97,7 @@ class VoIPDevices: self.hass.bus.async_listen( dr.EVENT_DEVICE_REGISTRY_UPDATED, async_device_removed, - callback(lambda ev: ev.data.get("action") == "remove"), + callback(lambda event_data: event_data.get("action") == "remove"), ) ) diff --git a/homeassistant/components/voip/voip.py b/homeassistant/components/voip/voip.py index a41f0965e8f..4d97720934c 100644 --- a/homeassistant/components/voip/voip.py +++ b/homeassistant/components/voip/voip.py @@ -1,4 +1,5 @@ """Voice over IP (VoIP) implementation.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/volkszaehler/sensor.py b/homeassistant/components/volkszaehler/sensor.py index 83b242d0bc0..ce5691b1193 100644 --- a/homeassistant/components/volkszaehler/sensor.py +++ b/homeassistant/components/volkszaehler/sensor.py @@ -1,4 +1,5 @@ """Support for consuming values for the Volkszaehler API.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/volumio/browse_media.py b/homeassistant/components/volumio/browse_media.py index 019b4e33666..69a568731e1 100644 --- a/homeassistant/components/volumio/browse_media.py +++ b/homeassistant/components/volumio/browse_media.py @@ -1,4 +1,5 @@ """Support for media browsing.""" + import json from homeassistant.components.media_player import ( diff --git a/homeassistant/components/volumio/config_flow.py b/homeassistant/components/volumio/config_flow.py index f2b5c7e796c..e86fcd4417d 100644 --- a/homeassistant/components/volumio/config_flow.py +++ b/homeassistant/components/volumio/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Volumio integration.""" + from __future__ import annotations import logging @@ -6,11 +7,11 @@ import logging from pyvolumio import CannotConnectError, Volumio import voluptuous as vol -from homeassistant import config_entries, exceptions from homeassistant.components import zeroconf +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_ID, CONF_NAME, CONF_PORT from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN @@ -33,7 +34,7 @@ async def validate_input(hass, host, port): raise CannotConnect from error -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class VolumioConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Volumio.""" VERSION = 1 @@ -96,7 +97,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_zeroconf( self, discovery_info: zeroconf.ZeroconfServiceInfo - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle zeroconf discovery.""" self._host = discovery_info.host self._port = discovery_info.port @@ -121,5 +122,5 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -class CannotConnect(exceptions.HomeAssistantError): +class CannotConnect(HomeAssistantError): """Error to indicate we cannot connect.""" diff --git a/homeassistant/components/volumio/media_player.py b/homeassistant/components/volumio/media_player.py index a11ea62e355..5ba67d7974f 100644 --- a/homeassistant/components/volumio/media_player.py +++ b/homeassistant/components/volumio/media_player.py @@ -2,6 +2,7 @@ Volumio rest API: https://volumio.github.io/docs/API/REST_API.html """ + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/volvooncall/binary_sensor.py b/homeassistant/components/volvooncall/binary_sensor.py index 77e1b7183db..604dc2313bf 100644 --- a/homeassistant/components/volvooncall/binary_sensor.py +++ b/homeassistant/components/volvooncall/binary_sensor.py @@ -1,4 +1,5 @@ """Support for VOC.""" + from __future__ import annotations from contextlib import suppress @@ -31,21 +32,17 @@ async def async_setup_entry( @callback def async_discover_device(instruments: list[Instrument]) -> None: """Discover and add a discovered Volvo On Call binary sensor.""" - entities: list[VolvoSensor] = [] - - for instrument in instruments: - if instrument.component == "binary_sensor": - entities.append( - VolvoSensor( - coordinator, - instrument.vehicle.vin, - instrument.component, - instrument.attr, - instrument.slug_attr, - ) - ) - - async_add_entities(entities) + async_add_entities( + VolvoSensor( + coordinator, + instrument.vehicle.vin, + instrument.component, + instrument.attr, + instrument.slug_attr, + ) + for instrument in instruments + if instrument.component == "binary_sensor" + ) async_discover_device([*volvo_data.instruments]) diff --git a/homeassistant/components/volvooncall/config_flow.py b/homeassistant/components/volvooncall/config_flow.py index d56d10ded5a..1cb434e49bc 100644 --- a/homeassistant/components/volvooncall/config_flow.py +++ b/homeassistant/components/volvooncall/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Volvo On Call integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -8,14 +9,13 @@ from typing import Any import voluptuous as vol from volvooncall import Connection -from homeassistant import config_entries +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.const import ( CONF_PASSWORD, CONF_REGION, CONF_UNIT_SYSTEM, CONF_USERNAME, ) -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from . import VolvoData @@ -31,15 +31,15 @@ from .errors import InvalidAuth _LOGGER = logging.getLogger(__name__) -class VolvoOnCallConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class VolvoOnCallConfigFlow(ConfigFlow, domain=DOMAIN): """VolvoOnCall config flow.""" VERSION = 1 - _reauth_entry: config_entries.ConfigEntry | None = None + _reauth_entry: ConfigEntry | None = None async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle user step.""" errors = {} defaults = { @@ -106,7 +106,9 @@ class VolvoOnCallConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="user", data_schema=user_schema, errors=errors ) - async def async_step_reauth(self, user_input: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, user_input: Mapping[str, Any] + ) -> ConfigFlowResult: """Perform reauth upon an API authentication error.""" self._reauth_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] diff --git a/homeassistant/components/volvooncall/device_tracker.py b/homeassistant/components/volvooncall/device_tracker.py index 0cd61a336b7..51c2f08130b 100644 --- a/homeassistant/components/volvooncall/device_tracker.py +++ b/homeassistant/components/volvooncall/device_tracker.py @@ -1,4 +1,5 @@ """Support for tracking a Volvo.""" + from __future__ import annotations from volvooncall.dashboard import Instrument @@ -25,21 +26,17 @@ async def async_setup_entry( @callback def async_discover_device(instruments: list[Instrument]) -> None: """Discover and add a discovered Volvo On Call device tracker.""" - entities: list[VolvoTrackerEntity] = [] - - for instrument in instruments: - if instrument.component == "device_tracker": - entities.append( - VolvoTrackerEntity( - instrument.vehicle.vin, - instrument.component, - instrument.attr, - instrument.slug_attr, - coordinator, - ) - ) - - async_add_entities(entities) + async_add_entities( + VolvoTrackerEntity( + instrument.vehicle.vin, + instrument.component, + instrument.attr, + instrument.slug_attr, + coordinator, + ) + for instrument in instruments + if instrument.component == "device_tracker" + ) async_discover_device([*volvo_data.instruments]) diff --git a/homeassistant/components/volvooncall/errors.py b/homeassistant/components/volvooncall/errors.py index a3af1125b48..3736c5b9290 100644 --- a/homeassistant/components/volvooncall/errors.py +++ b/homeassistant/components/volvooncall/errors.py @@ -1,4 +1,5 @@ """Exceptions specific to volvooncall.""" + from homeassistant.exceptions import HomeAssistantError diff --git a/homeassistant/components/volvooncall/lock.py b/homeassistant/components/volvooncall/lock.py index a48b5dc6b65..cccd64bce05 100644 --- a/homeassistant/components/volvooncall/lock.py +++ b/homeassistant/components/volvooncall/lock.py @@ -28,21 +28,17 @@ async def async_setup_entry( @callback def async_discover_device(instruments: list[Instrument]) -> None: """Discover and add a discovered Volvo On Call lock.""" - entities: list[VolvoLock] = [] - - for instrument in instruments: - if instrument.component == "lock": - entities.append( - VolvoLock( - coordinator, - instrument.vehicle.vin, - instrument.component, - instrument.attr, - instrument.slug_attr, - ) - ) - - async_add_entities(entities) + async_add_entities( + VolvoLock( + coordinator, + instrument.vehicle.vin, + instrument.component, + instrument.attr, + instrument.slug_attr, + ) + for instrument in instruments + if instrument.component == "lock" + ) async_discover_device([*volvo_data.instruments]) diff --git a/homeassistant/components/volvooncall/sensor.py b/homeassistant/components/volvooncall/sensor.py index 0f4269732e3..a46c8671929 100644 --- a/homeassistant/components/volvooncall/sensor.py +++ b/homeassistant/components/volvooncall/sensor.py @@ -1,4 +1,5 @@ """Support for Volvo On Call sensors.""" + from __future__ import annotations from volvooncall.dashboard import Instrument @@ -25,21 +26,17 @@ async def async_setup_entry( @callback def async_discover_device(instruments: list[Instrument]) -> None: """Discover and add a discovered Volvo On Call sensor.""" - entities: list[VolvoSensor] = [] - - for instrument in instruments: - if instrument.component == "sensor": - entities.append( - VolvoSensor( - coordinator, - instrument.vehicle.vin, - instrument.component, - instrument.attr, - instrument.slug_attr, - ) - ) - - async_add_entities(entities) + async_add_entities( + VolvoSensor( + coordinator, + instrument.vehicle.vin, + instrument.component, + instrument.attr, + instrument.slug_attr, + ) + for instrument in instruments + if instrument.component == "sensor" + ) async_discover_device([*volvo_data.instruments]) diff --git a/homeassistant/components/volvooncall/switch.py b/homeassistant/components/volvooncall/switch.py index 89300cd54e1..23bc452ef66 100644 --- a/homeassistant/components/volvooncall/switch.py +++ b/homeassistant/components/volvooncall/switch.py @@ -1,4 +1,5 @@ """Support for Volvo heater.""" + from __future__ import annotations from typing import Any @@ -27,21 +28,17 @@ async def async_setup_entry( @callback def async_discover_device(instruments: list[Instrument]) -> None: """Discover and add a discovered Volvo On Call switch.""" - entities: list[VolvoSwitch] = [] - - for instrument in instruments: - if instrument.component == "switch": - entities.append( - VolvoSwitch( - coordinator, - instrument.vehicle.vin, - instrument.component, - instrument.attr, - instrument.slug_attr, - ) - ) - - async_add_entities(entities) + async_add_entities( + VolvoSwitch( + coordinator, + instrument.vehicle.vin, + instrument.component, + instrument.attr, + instrument.slug_attr, + ) + for instrument in instruments + if instrument.component == "switch" + ) async_discover_device([*volvo_data.instruments]) diff --git a/homeassistant/components/vulcan/calendar.py b/homeassistant/components/vulcan/calendar.py index 073ac88fbda..e068a772345 100644 --- a/homeassistant/components/vulcan/calendar.py +++ b/homeassistant/components/vulcan/calendar.py @@ -1,4 +1,5 @@ """Support for Vulcan Calendar platform.""" + from __future__ import annotations from datetime import date, datetime, timedelta @@ -110,11 +111,11 @@ class VulcanCalendarEntity(CalendarEntity): event_list = [] for item in events: event = CalendarEvent( - start=datetime.combine(item["date"], item["time"].from_).astimezone( - ZoneInfo("Europe/Warsaw") + start=datetime.combine( + item["date"], item["time"].from_, ZoneInfo("Europe/Warsaw") ), - end=datetime.combine(item["date"], item["time"].to).astimezone( - ZoneInfo("Europe/Warsaw") + end=datetime.combine( + item["date"], item["time"].to, ZoneInfo("Europe/Warsaw") ), summary=item["lesson"], location=item["room"], @@ -164,10 +165,10 @@ class VulcanCalendarEntity(CalendarEntity): ) self._event = CalendarEvent( start=datetime.combine( - new_event["date"], new_event["time"].from_ - ).astimezone(ZoneInfo("Europe/Warsaw")), - end=datetime.combine(new_event["date"], new_event["time"].to).astimezone( - ZoneInfo("Europe/Warsaw") + new_event["date"], new_event["time"].from_, ZoneInfo("Europe/Warsaw") + ), + end=datetime.combine( + new_event["date"], new_event["time"].to, ZoneInfo("Europe/Warsaw") ), summary=new_event["lesson"], location=new_event["room"], diff --git a/homeassistant/components/vulcan/config_flow.py b/homeassistant/components/vulcan/config_flow.py index 4547e6dd31e..ae44c507c6a 100644 --- a/homeassistant/components/vulcan/config_flow.py +++ b/homeassistant/components/vulcan/config_flow.py @@ -1,4 +1,5 @@ """Adds config flow for Vulcan.""" + from collections.abc import Mapping import logging from typing import Any @@ -16,9 +17,8 @@ from vulcan import ( Vulcan, ) -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_PIN, CONF_REGION, CONF_TOKEN -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from . import DOMAIN @@ -33,7 +33,7 @@ LOGIN_SCHEMA = { } -class VulcanFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class VulcanFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a Uonet+ Vulcan config flow.""" VERSION = 1 @@ -111,9 +111,9 @@ class VulcanFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): students = {} if self.students is not None: for student in self.students: - students[ - str(student.pupil.id) - ] = f"{student.pupil.first_name} {student.pupil.last_name}" + students[str(student.pupil.id)] = ( + f"{student.pupil.first_name} {student.pupil.last_name}" + ) if user_input is not None: student_id = user_input["student"] await self.async_set_unique_id(str(student_id)) @@ -190,9 +190,7 @@ class VulcanFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_add_next_config_entry(self, user_input=None): """Flow initialized when user is adding next entry of that integration.""" - existing_entries = [] - for entry in self.hass.config_entries.async_entries(DOMAIN): - existing_entries.append(entry) + existing_entries = self.hass.config_entries.async_entries(DOMAIN) errors = {} @@ -205,13 +203,14 @@ class VulcanFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): account = Account.load(existing_entries[0].data["account"]) client = Vulcan(keystore, account, async_get_clientsession(self.hass)) students = await client.get_students() - new_students = [] - existing_entry_ids = [] - for entry in self.hass.config_entries.async_entries(DOMAIN): - existing_entry_ids.append(entry.data["student_id"]) - for student in students: - if str(student.pupil.id) not in existing_entry_ids: - new_students.append(student) + existing_entry_ids = [ + entry.data["student_id"] for entry in existing_entries + ] + new_students = [ + student + for student in students + if str(student.pupil.id) not in existing_entry_ids + ] if not new_students: return self.async_abort(reason="all_student_already_configured") if len(new_students) == 1: @@ -241,7 +240,9 @@ class VulcanFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Perform reauth upon an API authentication error.""" return await self.async_step_reauth_confirm() @@ -275,9 +276,7 @@ class VulcanFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): keystore = credentials["keystore"] client = Vulcan(keystore, account, async_get_clientsession(self.hass)) students = await client.get_students() - existing_entries = [] - for entry in self.hass.config_entries.async_entries(DOMAIN): - existing_entries.append(entry) + existing_entries = self.hass.config_entries.async_entries(DOMAIN) matching_entries = False for student in students: for entry in existing_entries: diff --git a/homeassistant/components/vulcan/fetch_data.py b/homeassistant/components/vulcan/fetch_data.py index c706bfa805f..cd82346d5b7 100644 --- a/homeassistant/components/vulcan/fetch_data.py +++ b/homeassistant/components/vulcan/fetch_data.py @@ -87,9 +87,9 @@ async def get_student_info(client, student_id): if student.pupil.second_name: student_info["second_name"] = student.pupil.second_name student_info["last_name"] = student.pupil.last_name - student_info[ - "full_name" - ] = f"{student.pupil.first_name} {student.pupil.last_name}" + student_info["full_name"] = ( + f"{student.pupil.first_name} {student.pupil.last_name}" + ) student_info["id"] = student.pupil.id student_info["class"] = student.class_ student_info["school"] = student.school.name diff --git a/homeassistant/components/vultr/__init__.py b/homeassistant/components/vultr/__init__.py index 7bf3b4b07f5..36f43cf0ac0 100644 --- a/homeassistant/components/vultr/__init__.py +++ b/homeassistant/components/vultr/__init__.py @@ -1,4 +1,5 @@ """Support for Vultr.""" + from datetime import timedelta import logging diff --git a/homeassistant/components/vultr/binary_sensor.py b/homeassistant/components/vultr/binary_sensor.py index 1d877216e93..5c0db81e843 100644 --- a/homeassistant/components/vultr/binary_sensor.py +++ b/homeassistant/components/vultr/binary_sensor.py @@ -1,4 +1,5 @@ """Support for monitoring the state of Vultr subscriptions (VPS).""" + from __future__ import annotations import logging diff --git a/homeassistant/components/vultr/sensor.py b/homeassistant/components/vultr/sensor.py index a77fab62bd4..816a55736be 100644 --- a/homeassistant/components/vultr/sensor.py +++ b/homeassistant/components/vultr/sensor.py @@ -1,4 +1,5 @@ """Support for monitoring the state of Vultr Subscriptions.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/vultr/switch.py b/homeassistant/components/vultr/switch.py index a12cfab3e83..6758748b9f3 100644 --- a/homeassistant/components/vultr/switch.py +++ b/homeassistant/components/vultr/switch.py @@ -1,4 +1,5 @@ """Support for interacting with Vultr subscriptions.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/w800rf32/__init__.py b/homeassistant/components/w800rf32/__init__.py index 29701ec82ab..62b9ba810d9 100644 --- a/homeassistant/components/w800rf32/__init__.py +++ b/homeassistant/components/w800rf32/__init__.py @@ -1,4 +1,5 @@ """Support for w800rf32 devices.""" + import logging import voluptuous as vol diff --git a/homeassistant/components/w800rf32/binary_sensor.py b/homeassistant/components/w800rf32/binary_sensor.py index 2bc0c0eea75..49eec35cb1e 100644 --- a/homeassistant/components/w800rf32/binary_sensor.py +++ b/homeassistant/components/w800rf32/binary_sensor.py @@ -1,4 +1,5 @@ """Support for w800rf32 binary sensors.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/wake_on_lan/__init__.py b/homeassistant/components/wake_on_lan/__init__.py index 54809d6cec3..37837da683a 100644 --- a/homeassistant/components/wake_on_lan/__init__.py +++ b/homeassistant/components/wake_on_lan/__init__.py @@ -1,4 +1,5 @@ """Support for sending Wake-On-LAN magic packets.""" + from functools import partial import logging diff --git a/homeassistant/components/wake_on_lan/const.py b/homeassistant/components/wake_on_lan/const.py index 14f2bd0263f..2560ef40382 100644 --- a/homeassistant/components/wake_on_lan/const.py +++ b/homeassistant/components/wake_on_lan/const.py @@ -1,2 +1,3 @@ """Constants for the Wake-On-LAN component.""" + DOMAIN = "wake_on_lan" diff --git a/homeassistant/components/wake_on_lan/icons.json b/homeassistant/components/wake_on_lan/icons.json new file mode 100644 index 00000000000..6426c478157 --- /dev/null +++ b/homeassistant/components/wake_on_lan/icons.json @@ -0,0 +1,5 @@ +{ + "services": { + "send_magic_packet": "mdi:cube-send" + } +} diff --git a/homeassistant/components/wake_on_lan/switch.py b/homeassistant/components/wake_on_lan/switch.py index 446df402c87..a0b54fd8db0 100644 --- a/homeassistant/components/wake_on_lan/switch.py +++ b/homeassistant/components/wake_on_lan/switch.py @@ -1,4 +1,5 @@ """Support for wake on lan.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/wake_word/__init__.py b/homeassistant/components/wake_word/__init__.py index bf502023e2b..f05a61e34dc 100644 --- a/homeassistant/components/wake_word/__init__.py +++ b/homeassistant/components/wake_word/__init__.py @@ -1,4 +1,5 @@ """Provide functionality to wake word.""" + from __future__ import annotations from abc import abstractmethod diff --git a/homeassistant/components/wake_word/const.py b/homeassistant/components/wake_word/const.py index fdca6cfab6e..26027c61934 100644 --- a/homeassistant/components/wake_word/const.py +++ b/homeassistant/components/wake_word/const.py @@ -1,2 +1,3 @@ """Wake word constants.""" + DOMAIN = "wake_word" diff --git a/homeassistant/components/wake_word/models.py b/homeassistant/components/wake_word/models.py index c341df188ce..ba92b3a0799 100644 --- a/homeassistant/components/wake_word/models.py +++ b/homeassistant/components/wake_word/models.py @@ -1,4 +1,5 @@ """Wake word models.""" + from dataclasses import dataclass diff --git a/homeassistant/components/wallbox/__init__.py b/homeassistant/components/wallbox/__init__.py index 4ca6e768f64..4ea2cf98be1 100644 --- a/homeassistant/components/wallbox/__init__.py +++ b/homeassistant/components/wallbox/__init__.py @@ -1,4 +1,5 @@ """The Wallbox integration.""" + from __future__ import annotations from wallbox import Wallbox diff --git a/homeassistant/components/wallbox/config_flow.py b/homeassistant/components/wallbox/config_flow.py index 0f3782958d3..44c47149554 100644 --- a/homeassistant/components/wallbox/config_flow.py +++ b/homeassistant/components/wallbox/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Wallbox integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -7,9 +8,9 @@ from typing import Any import voluptuous as vol from wallbox import Wallbox -from homeassistant import config_entries, core +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from homeassistant.data_entry_flow import FlowResult +from homeassistant.core import HomeAssistant from .const import CONF_STATION, DOMAIN from .coordinator import InvalidAuth, WallboxCoordinator @@ -25,9 +26,7 @@ STEP_USER_DATA_SCHEMA = vol.Schema( ) -async def validate_input( - hass: core.HomeAssistant, data: dict[str, Any] -) -> dict[str, str]: +async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, str]: """Validate the user input allows to connect. Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user. @@ -41,14 +40,16 @@ async def validate_input( return {"title": "Wallbox Portal"} -class ConfigFlow(config_entries.ConfigFlow, domain=COMPONENT_DOMAIN): +class WallboxConfigFlow(ConfigFlow, domain=COMPONENT_DOMAIN): """Handle a config flow for Wallbox.""" def __init__(self) -> None: """Start the Wallbox config flow.""" - self._reauth_entry: config_entries.ConfigEntry | None = None + self._reauth_entry: ConfigEntry | None = None - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Perform reauth upon an API authentication error.""" self._reauth_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] @@ -58,7 +59,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=COMPONENT_DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" if user_input is None: return self.async_show_form( diff --git a/homeassistant/components/wallbox/const.py b/homeassistant/components/wallbox/const.py index 6caa3c070c8..69633cbda22 100644 --- a/homeassistant/components/wallbox/const.py +++ b/homeassistant/components/wallbox/const.py @@ -1,4 +1,5 @@ """Constants for the Wallbox integration.""" + from enum import StrEnum DOMAIN = "wallbox" diff --git a/homeassistant/components/wallbox/coordinator.py b/homeassistant/components/wallbox/coordinator.py index 96d66bb4395..4725e92ca84 100644 --- a/homeassistant/components/wallbox/coordinator.py +++ b/homeassistant/components/wallbox/coordinator.py @@ -1,4 +1,5 @@ """DataUpdateCoordinator for the wallbox integration.""" + from __future__ import annotations from collections.abc import Callable @@ -132,9 +133,9 @@ class WallboxCoordinator(DataUpdateCoordinator[dict[str, Any]]): data[CHARGER_ENERGY_PRICE_KEY] = data[CHARGER_DATA_KEY][ CHARGER_ENERGY_PRICE_KEY ] - data[ - CHARGER_CURRENCY_KEY - ] = f"{data[CHARGER_DATA_KEY][CHARGER_CURRENCY_KEY][CODE_KEY]}/kWh" + data[CHARGER_CURRENCY_KEY] = ( + f"{data[CHARGER_DATA_KEY][CHARGER_CURRENCY_KEY][CODE_KEY]}/kWh" + ) data[CHARGER_STATUS_DESCRIPTION_KEY] = CHARGER_STATUS.get( data[CHARGER_STATUS_ID_KEY], ChargerStatus.UNKNOWN diff --git a/homeassistant/components/wallbox/entity.py b/homeassistant/components/wallbox/entity.py index 1152530dbd1..489e81ed6b0 100644 --- a/homeassistant/components/wallbox/entity.py +++ b/homeassistant/components/wallbox/entity.py @@ -1,4 +1,5 @@ """Base entity for the wallbox integration.""" + from __future__ import annotations from homeassistant.helpers.device_registry import DeviceInfo diff --git a/homeassistant/components/wallbox/icons.json b/homeassistant/components/wallbox/icons.json new file mode 100644 index 00000000000..359e05cb441 --- /dev/null +++ b/homeassistant/components/wallbox/icons.json @@ -0,0 +1,27 @@ +{ + "entity": { + "sensor": { + "charging_speed": { + "default": "mdi:speedometer" + }, + "added_range": { + "default": "mdi:map-marker-distance" + }, + "cost": { + "default": "mdi:ev-station" + }, + "current_mode": { + "default": "mdi:ev-station" + }, + "depot_price": { + "default": "mdi:ev-station" + }, + "energy_price": { + "default": "mdi:ev-station" + }, + "status_description": { + "default": "mdi:ev-station" + } + } + } +} diff --git a/homeassistant/components/wallbox/lock.py b/homeassistant/components/wallbox/lock.py index 11a66a4814c..4853a9104f2 100644 --- a/homeassistant/components/wallbox/lock.py +++ b/homeassistant/components/wallbox/lock.py @@ -1,4 +1,5 @@ """Home Assistant component for accessing the Wallbox Portal API. The lock component creates a lock entity.""" + from __future__ import annotations from typing import Any @@ -42,11 +43,9 @@ async def async_setup_entry( raise PlatformNotReady from exc async_add_entities( - [ - WallboxLock(coordinator, description) - for ent in coordinator.data - if (description := LOCK_TYPES.get(ent)) - ] + WallboxLock(coordinator, description) + for ent in coordinator.data + if (description := LOCK_TYPES.get(ent)) ) diff --git a/homeassistant/components/wallbox/number.py b/homeassistant/components/wallbox/number.py index 76cf8316959..8ae4c473299 100644 --- a/homeassistant/components/wallbox/number.py +++ b/homeassistant/components/wallbox/number.py @@ -2,6 +2,7 @@ The number component allows control of charging current. """ + from __future__ import annotations from collections.abc import Awaitable, Callable @@ -38,22 +39,15 @@ def min_charging_current_value(coordinator: WallboxCoordinator) -> float: return 6 -@dataclass(frozen=True) -class WallboxNumberEntityDescriptionMixin: - """Load entities from different handlers.""" +@dataclass(frozen=True, kw_only=True) +class WallboxNumberEntityDescription(NumberEntityDescription): + """Describes Wallbox number entity.""" max_value_fn: Callable[[WallboxCoordinator], float] min_value_fn: Callable[[WallboxCoordinator], float] set_value_fn: Callable[[WallboxCoordinator], Callable[[float], Awaitable[None]]] -@dataclass(frozen=True) -class WallboxNumberEntityDescription( - NumberEntityDescription, WallboxNumberEntityDescriptionMixin -): - """Describes Wallbox number entity.""" - - NUMBER_TYPES: dict[str, WallboxNumberEntityDescription] = { CHARGER_MAX_CHARGING_CURRENT_KEY: WallboxNumberEntityDescription( key=CHARGER_MAX_CHARGING_CURRENT_KEY, @@ -92,11 +86,9 @@ async def async_setup_entry( raise PlatformNotReady from exc async_add_entities( - [ - WallboxNumber(coordinator, entry, description) - for ent in coordinator.data - if (description := NUMBER_TYPES.get(ent)) - ] + WallboxNumber(coordinator, entry, description) + for ent in coordinator.data + if (description := NUMBER_TYPES.get(ent)) ) diff --git a/homeassistant/components/wallbox/sensor.py b/homeassistant/components/wallbox/sensor.py index 5a825722d53..eadbc04dca2 100644 --- a/homeassistant/components/wallbox/sensor.py +++ b/homeassistant/components/wallbox/sensor.py @@ -1,4 +1,5 @@ """Home Assistant component for accessing the Wallbox Portal API. The sensor component creates multiple sensors regarding wallbox performance.""" + from __future__ import annotations from dataclasses import dataclass @@ -78,14 +79,12 @@ SENSOR_TYPES: dict[str, WallboxSensorEntityDescription] = { CHARGER_CHARGING_SPEED_KEY: WallboxSensorEntityDescription( key=CHARGER_CHARGING_SPEED_KEY, translation_key=CHARGER_CHARGING_SPEED_KEY, - icon="mdi:speedometer", precision=0, state_class=SensorStateClass.MEASUREMENT, ), CHARGER_ADDED_RANGE_KEY: WallboxSensorEntityDescription( key=CHARGER_ADDED_RANGE_KEY, translation_key=CHARGER_ADDED_RANGE_KEY, - icon="mdi:map-marker-distance", precision=0, native_unit_of_measurement=UnitOfLength.KILOMETERS, device_class=SensorDeviceClass.DISTANCE, @@ -110,7 +109,6 @@ SENSOR_TYPES: dict[str, WallboxSensorEntityDescription] = { CHARGER_COST_KEY: WallboxSensorEntityDescription( key=CHARGER_COST_KEY, translation_key=CHARGER_COST_KEY, - icon="mdi:ev-station", state_class=SensorStateClass.TOTAL_INCREASING, ), CHARGER_STATE_OF_CHARGE_KEY: WallboxSensorEntityDescription( @@ -123,26 +121,22 @@ SENSOR_TYPES: dict[str, WallboxSensorEntityDescription] = { CHARGER_CURRENT_MODE_KEY: WallboxSensorEntityDescription( key=CHARGER_CURRENT_MODE_KEY, translation_key=CHARGER_CURRENT_MODE_KEY, - icon="mdi:ev-station", ), CHARGER_DEPOT_PRICE_KEY: WallboxSensorEntityDescription( key=CHARGER_DEPOT_PRICE_KEY, translation_key=CHARGER_DEPOT_PRICE_KEY, - icon="mdi:ev-station", precision=2, state_class=SensorStateClass.MEASUREMENT, ), CHARGER_ENERGY_PRICE_KEY: WallboxSensorEntityDescription( key=CHARGER_ENERGY_PRICE_KEY, translation_key=CHARGER_ENERGY_PRICE_KEY, - icon="mdi:ev-station", precision=2, state_class=SensorStateClass.MEASUREMENT, ), CHARGER_STATUS_DESCRIPTION_KEY: WallboxSensorEntityDescription( key=CHARGER_STATUS_DESCRIPTION_KEY, translation_key=CHARGER_STATUS_DESCRIPTION_KEY, - icon="mdi:ev-station", ), CHARGER_MAX_CHARGING_CURRENT_KEY: WallboxSensorEntityDescription( key=CHARGER_MAX_CHARGING_CURRENT_KEY, @@ -161,11 +155,9 @@ async def async_setup_entry( coordinator: WallboxCoordinator = hass.data[DOMAIN][entry.entry_id] async_add_entities( - [ - WallboxSensor(coordinator, description) - for ent in coordinator.data - if (description := SENSOR_TYPES.get(ent)) - ] + WallboxSensor(coordinator, description) + for ent in coordinator.data + if (description := SENSOR_TYPES.get(ent)) ) diff --git a/homeassistant/components/wallbox/switch.py b/homeassistant/components/wallbox/switch.py index 2de6379eb18..06c2674579d 100644 --- a/homeassistant/components/wallbox/switch.py +++ b/homeassistant/components/wallbox/switch.py @@ -1,4 +1,5 @@ """Home Assistant component for accessing the Wallbox Portal API. The switch component creates a switch entity.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/waqi/__init__.py b/homeassistant/components/waqi/__init__.py index d4e41095b26..e9feca75ee7 100644 --- a/homeassistant/components/waqi/__init__.py +++ b/homeassistant/components/waqi/__init__.py @@ -1,4 +1,5 @@ """The World Air Quality Index (WAQI) integration.""" + from __future__ import annotations from aiowaqi import WAQIClient @@ -7,7 +8,6 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession -import homeassistant.helpers.entity_registry as er from .const import DOMAIN from .coordinator import WAQIDataUpdateCoordinator @@ -18,8 +18,6 @@ PLATFORMS: list[Platform] = [Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up World Air Quality Index (WAQI) from a config entry.""" - await _migrate_unique_ids(hass, entry) - client = WAQIClient(session=async_get_clientsession(hass)) client.authenticate(entry.data[CONF_API_KEY]) @@ -38,16 +36,3 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN].pop(entry.entry_id) return unload_ok - - -async def _migrate_unique_ids(hass: HomeAssistant, entry: ConfigEntry) -> None: - """Migrate pre-config flow unique ids.""" - entity_registry = er.async_get(hass) - registry_entries = er.async_entries_for_config_entry( - entity_registry, entry.entry_id - ) - for reg_entry in registry_entries: - if isinstance(reg_entry.unique_id, int): # type: ignore[unreachable] - entity_registry.async_update_entity( # type: ignore[unreachable] - reg_entry.entity_id, new_unique_id=f"{reg_entry.unique_id}_air_quality" - ) diff --git a/homeassistant/components/waqi/config_flow.py b/homeassistant/components/waqi/config_flow.py index 55740913487..068cb1a5020 100644 --- a/homeassistant/components/waqi/config_flow.py +++ b/homeassistant/components/waqi/config_flow.py @@ -1,4 +1,5 @@ """Config flow for World Air Quality Index (WAQI) integration.""" + from __future__ import annotations import logging @@ -12,27 +13,22 @@ from aiowaqi import ( ) import voluptuous as vol -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import ( CONF_API_KEY, CONF_LATITUDE, CONF_LOCATION, CONF_LONGITUDE, CONF_METHOD, - CONF_NAME, ) -from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN -from homeassistant.data_entry_flow import AbortFlow, FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.selector import ( LocationSelector, SelectSelector, SelectSelectorConfig, ) -from homeassistant.helpers.typing import ConfigType -from .const import CONF_STATION_NUMBER, DOMAIN, ISSUE_PLACEHOLDER +from .const import CONF_STATION_NUMBER, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -66,7 +62,7 @@ class WAQIConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors: dict[str, str] = {} if user_input is not None: @@ -107,7 +103,7 @@ class WAQIConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_map( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Add measuring station via map.""" errors: dict[str, str] = {} if user_input is not None: @@ -149,7 +145,7 @@ class WAQIConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_station_number( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Add measuring station via station number.""" errors: dict[str, str] = {} if user_input is not None: @@ -182,7 +178,7 @@ class WAQIConfigFlow(ConfigFlow, domain=DOMAIN): async def _async_create_entry( self, measuring_station: WAQIAirQuality - ) -> FlowResult: + ) -> ConfigFlowResult: await self.async_set_unique_id(str(measuring_station.station_id)) self._abort_if_unique_id_configured() return self.async_create_entry( @@ -192,43 +188,3 @@ class WAQIConfigFlow(ConfigFlow, domain=DOMAIN): CONF_STATION_NUMBER: measuring_station.station_id, }, ) - - async def async_step_import(self, import_config: ConfigType) -> FlowResult: - """Handle importing from yaml.""" - await self.async_set_unique_id(str(import_config[CONF_STATION_NUMBER])) - try: - self._abort_if_unique_id_configured() - except AbortFlow as exc: - async_create_issue( - self.hass, - DOMAIN, - "deprecated_yaml_import_issue_already_configured", - breaks_in_ha_version="2024.4.0", - is_fixable=False, - severity=IssueSeverity.ERROR, - translation_key="deprecated_yaml_import_issue_already_configured", - translation_placeholders=ISSUE_PLACEHOLDER, - ) - raise exc - - async_create_issue( - self.hass, - HOMEASSISTANT_DOMAIN, - f"deprecated_yaml_{DOMAIN}", - breaks_in_ha_version="2024.4.0", - is_fixable=False, - issue_domain=DOMAIN, - severity=IssueSeverity.WARNING, - translation_key="deprecated_yaml", - translation_placeholders={ - "domain": DOMAIN, - "integration_title": "World Air Quality Index", - }, - ) - return self.async_create_entry( - title=import_config[CONF_NAME], - data={ - CONF_API_KEY: import_config[CONF_API_KEY], - CONF_STATION_NUMBER: import_config[CONF_STATION_NUMBER], - }, - ) diff --git a/homeassistant/components/waqi/const.py b/homeassistant/components/waqi/const.py index 2847a29b8ad..c5ffea20b46 100644 --- a/homeassistant/components/waqi/const.py +++ b/homeassistant/components/waqi/const.py @@ -1,4 +1,5 @@ """Constants for the World Air Quality Index (WAQI) integration.""" + import logging DOMAIN = "waqi" diff --git a/homeassistant/components/waqi/coordinator.py b/homeassistant/components/waqi/coordinator.py index b7beef8fda9..d1a44e9f5b8 100644 --- a/homeassistant/components/waqi/coordinator.py +++ b/homeassistant/components/waqi/coordinator.py @@ -1,4 +1,5 @@ """Coordinator for the World Air Quality Index (WAQI) integration.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/waqi/sensor.py b/homeassistant/components/waqi/sensor.py index 43be729e10f..ce967a9b538 100644 --- a/homeassistant/components/waqi/sensor.py +++ b/homeassistant/components/waqi/sensor.py @@ -1,4 +1,5 @@ """Support for the World Air Quality Index service.""" + from __future__ import annotations from collections.abc import Callable, Mapping @@ -6,14 +7,8 @@ from dataclasses import dataclass import logging from typing import Any -from aiowaqi import ( - WAQIAirQuality, - WAQIAuthenticationError, - WAQIClient, - WAQIConnectionError, -) +from aiowaqi import WAQIAirQuality from aiowaqi.models import Pollutant -import voluptuous as vol from homeassistant.components.sensor import ( SensorDeviceClass, @@ -21,28 +16,21 @@ from homeassistant.components.sensor import ( SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_TEMPERATURE, ATTR_TIME, - CONF_API_KEY, - CONF_NAME, - CONF_TOKEN, PERCENTAGE, UnitOfPressure, UnitOfTemperature, ) 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.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType +from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import CONF_STATION_NUMBER, DOMAIN, ISSUE_PLACEHOLDER +from .const import DOMAIN from .coordinator import WAQIDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) @@ -56,116 +44,15 @@ ATTR_PM2_5 = "pm_2_5" ATTR_PRESSURE = "pressure" ATTR_SULFUR_DIOXIDE = "sulfur_dioxide" -ATTRIBUTION = "Data provided by the World Air Quality Index project" -ATTR_ICON = "mdi:cloud" - -CONF_LOCATIONS = "locations" -CONF_STATIONS = "stations" - -TIMEOUT = 10 - -PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_STATIONS): cv.ensure_list, - vol.Required(CONF_TOKEN): cv.string, - vol.Required(CONF_LOCATIONS): cv.ensure_list, - } -) - - -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the requested World Air Quality Index locations.""" - - token = config[CONF_TOKEN] - station_filter = config.get(CONF_STATIONS) - locations = config[CONF_LOCATIONS] - - client = WAQIClient(session=async_get_clientsession(hass), request_timeout=TIMEOUT) - client.authenticate(token) - station_count = 0 - try: - for location_name in locations: - stations = await client.search(location_name) - _LOGGER.debug("The following stations were returned: %s", stations) - for station in stations: - station_count = station_count + 1 - if not station_filter or { - station.station_id, - station.station.external_url, - station.station.name, - } & set(station_filter): - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={ - CONF_STATION_NUMBER: station.station_id, - CONF_NAME: station.station.name, - CONF_API_KEY: config[CONF_TOKEN], - }, - ) - ) - except WAQIAuthenticationError as err: - async_create_issue( - hass, - DOMAIN, - "deprecated_yaml_import_issue_invalid_auth", - breaks_in_ha_version="2024.4.0", - is_fixable=False, - issue_domain=DOMAIN, - severity=IssueSeverity.WARNING, - translation_key="deprecated_yaml_import_issue_invalid_auth", - translation_placeholders=ISSUE_PLACEHOLDER, - ) - _LOGGER.exception("Could not authenticate with WAQI") - raise PlatformNotReady from err - except WAQIConnectionError as err: - async_create_issue( - hass, - DOMAIN, - "deprecated_yaml_import_issue_cannot_connect", - breaks_in_ha_version="2024.4.0", - is_fixable=False, - issue_domain=DOMAIN, - severity=IssueSeverity.WARNING, - translation_key="deprecated_yaml_import_issue_cannot_connect", - translation_placeholders=ISSUE_PLACEHOLDER, - ) - _LOGGER.exception("Failed to connect to WAQI servers") - raise PlatformNotReady from err - if station_count == 0: - async_create_issue( - hass, - DOMAIN, - "deprecated_yaml_import_issue_none_found", - breaks_in_ha_version="2024.4.0", - is_fixable=False, - issue_domain=DOMAIN, - severity=IssueSeverity.WARNING, - translation_key="deprecated_yaml_import_issue_none_found", - translation_placeholders=ISSUE_PLACEHOLDER, - ) - - -@dataclass(frozen=True) -class WAQIMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class WAQISensorEntityDescription(SensorEntityDescription): + """Describes WAQI sensor entity.""" available_fn: Callable[[WAQIAirQuality], bool] value_fn: Callable[[WAQIAirQuality], StateType] -@dataclass(frozen=True) -class WAQISensorEntityDescription(SensorEntityDescription, WAQIMixin): - """Describes WAQI sensor entity.""" - - SENSORS: list[WAQISensorEntityDescription] = [ WAQISensorEntityDescription( key="air_quality", @@ -305,6 +192,7 @@ class WaqiSensor(CoordinatorEntity[WAQIDataUpdateCoordinator], SensorEntity): @property def extra_state_attributes(self) -> Mapping[str, Any] | None: """Return old state attributes if the entity is AQI entity.""" + # These are deprecated and will be removed in 2024.5 if self.entity_description.key != "air_quality": return None attrs: dict[str, Any] = {} diff --git a/homeassistant/components/waqi/strings.json b/homeassistant/components/waqi/strings.json index de287318508..a1feb217249 100644 --- a/homeassistant/components/waqi/strings.json +++ b/homeassistant/components/waqi/strings.json @@ -36,24 +36,6 @@ } } }, - "issues": { - "deprecated_yaml_import_issue_invalid_auth": { - "title": "The World Air Quality Index YAML configuration import failed", - "description": "Configuring World Air Quality Index using YAML is being removed but there was an authentication error importing your YAML configuration.\n\nCorrect the YAML configuration and restart Home Assistant to try again or remove the World Air Quality Index YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually." - }, - "deprecated_yaml_import_issue_cannot_connect": { - "title": "The WAQI YAML configuration import failed", - "description": "Configuring World Air Quality Index using YAML is being removed but there was an connection error importing your YAML configuration.\n\nEnsure connection to WAQI works and restart Home Assistant to try again or remove the World Air Quality Index YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually." - }, - "deprecated_yaml_import_issue_already_configured": { - "title": "The WAQI YAML configuration import failed", - "description": "Configuring World Air Quality Index using YAML is being removed but the measuring station was already imported when trying to import the YAML configuration.\n\nEnsure the imported configuration is correct and remove the World Air Quality Index YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually." - }, - "deprecated_yaml_import_issue_none_found": { - "title": "The WAQI YAML configuration import failed", - "description": "Configuring World Air Quality Index using YAML is being removed but there weren't any stations imported because they couldn't be found.\n\nEnsure the imported configuration is correct and remove the World Air Quality Index YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually." - } - }, "entity": { "sensor": { "carbon_monoxide": { diff --git a/homeassistant/components/water_heater/__init__.py b/homeassistant/components/water_heater/__init__.py index 79572973090..167acb85914 100644 --- a/homeassistant/components/water_heater/__init__.py +++ b/homeassistant/components/water_heater/__init__.py @@ -1,4 +1,5 @@ """Support for water heater devices.""" + from __future__ import annotations from collections.abc import Mapping @@ -41,6 +42,8 @@ from homeassistant.helpers.temperature import display_temp as show_temp from homeassistant.helpers.typing import ConfigType from homeassistant.util.unit_conversion import TemperatureConverter +from . import group as group_pre_import # noqa: F401 + if TYPE_CHECKING: from functools import cached_property else: @@ -328,7 +331,7 @@ class WaterHeaterEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): def set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" - raise NotImplementedError() + raise NotImplementedError async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" @@ -338,7 +341,7 @@ class WaterHeaterEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): def turn_on(self, **kwargs: Any) -> None: """Turn the water heater on.""" - raise NotImplementedError() + raise NotImplementedError async def async_turn_on(self, **kwargs: Any) -> None: """Turn the water heater on.""" @@ -346,7 +349,7 @@ class WaterHeaterEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): def turn_off(self, **kwargs: Any) -> None: """Turn the water heater off.""" - raise NotImplementedError() + raise NotImplementedError async def async_turn_off(self, **kwargs: Any) -> None: """Turn the water heater off.""" @@ -354,7 +357,7 @@ class WaterHeaterEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): def set_operation_mode(self, operation_mode: str) -> None: """Set new target operation mode.""" - raise NotImplementedError() + raise NotImplementedError async def async_set_operation_mode(self, operation_mode: str) -> None: """Set new target operation mode.""" @@ -365,8 +368,6 @@ class WaterHeaterEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): """Handle a set target operation mode service call.""" if self.operation_list is None: raise ServiceValidationError( - f"Operation mode {operation_mode} not valid for " - f"entity {self.entity_id}. The operation list is not defined", translation_domain=DOMAIN, translation_key="operation_list_not_defined", translation_placeholders={ @@ -377,9 +378,6 @@ class WaterHeaterEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): if operation_mode not in self.operation_list: operation_list = ", ".join(self.operation_list) raise ServiceValidationError( - f"Operation mode {operation_mode} not valid for " - f"entity {self.entity_id}. Valid " - f"operation modes are: {operation_list}", translation_domain=DOMAIN, translation_key="not_valid_operation_mode", translation_placeholders={ @@ -392,7 +390,7 @@ class WaterHeaterEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): def turn_away_mode_on(self) -> None: """Turn away mode on.""" - raise NotImplementedError() + raise NotImplementedError async def async_turn_away_mode_on(self) -> None: """Turn away mode on.""" @@ -400,7 +398,7 @@ class WaterHeaterEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): def turn_away_mode_off(self) -> None: """Turn away mode off.""" - raise NotImplementedError() + raise NotImplementedError async def async_turn_away_mode_off(self) -> None: """Turn away mode off.""" diff --git a/homeassistant/components/water_heater/const.py b/homeassistant/components/water_heater/const.py new file mode 100644 index 00000000000..5bf0816348c --- /dev/null +++ b/homeassistant/components/water_heater/const.py @@ -0,0 +1,8 @@ +"""Support for water heater devices.""" + +STATE_ECO = "eco" +STATE_ELECTRIC = "electric" +STATE_PERFORMANCE = "performance" +STATE_HIGH_DEMAND = "high_demand" +STATE_HEAT_PUMP = "heat_pump" +STATE_GAS = "gas" diff --git a/homeassistant/components/water_heater/device_action.py b/homeassistant/components/water_heater/device_action.py index 5b0fe46934c..49cfc7e9a07 100644 --- a/homeassistant/components/water_heater/device_action.py +++ b/homeassistant/components/water_heater/device_action.py @@ -1,4 +1,5 @@ """Provides device automations for Water Heater.""" + from __future__ import annotations import voluptuous as vol diff --git a/homeassistant/components/water_heater/group.py b/homeassistant/components/water_heater/group.py index 59d5478b1ab..72347c8a442 100644 --- a/homeassistant/components/water_heater/group.py +++ b/homeassistant/components/water_heater/group.py @@ -1,11 +1,13 @@ """Describe group states.""" +from typing import TYPE_CHECKING -from homeassistant.components.group import GroupIntegrationRegistry from homeassistant.const import STATE_OFF from homeassistant.core import HomeAssistant, callback -from . import ( +if TYPE_CHECKING: + from homeassistant.components.group import GroupIntegrationRegistry +from .const import ( STATE_ECO, STATE_ELECTRIC, STATE_GAS, @@ -17,7 +19,7 @@ from . import ( @callback def async_describe_on_off_states( - hass: HomeAssistant, registry: GroupIntegrationRegistry + hass: HomeAssistant, registry: "GroupIntegrationRegistry" ) -> None: """Describe group on off states.""" registry.on_off_states( diff --git a/homeassistant/components/water_heater/reproduce_state.py b/homeassistant/components/water_heater/reproduce_state.py index b3be0f62273..de0bb320020 100644 --- a/homeassistant/components/water_heater/reproduce_state.py +++ b/homeassistant/components/water_heater/reproduce_state.py @@ -1,4 +1,5 @@ """Reproduce an Water heater state.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/water_heater/significant_change.py b/homeassistant/components/water_heater/significant_change.py index bacb0232ee3..c0db97c6e40 100644 --- a/homeassistant/components/water_heater/significant_change.py +++ b/homeassistant/components/water_heater/significant_change.py @@ -1,4 +1,5 @@ """Helper to test significant Water Heater state changes.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/waterfurnace/__init__.py b/homeassistant/components/waterfurnace/__init__.py index 50031216609..4c150b99f43 100644 --- a/homeassistant/components/waterfurnace/__init__.py +++ b/homeassistant/components/waterfurnace/__init__.py @@ -1,4 +1,5 @@ """Support for Waterfurnaces.""" + from datetime import timedelta import logging import threading diff --git a/homeassistant/components/waterfurnace/sensor.py b/homeassistant/components/waterfurnace/sensor.py index 9d60de29264..1e03ad88cc8 100644 --- a/homeassistant/components/waterfurnace/sensor.py +++ b/homeassistant/components/waterfurnace/sensor.py @@ -1,4 +1,5 @@ """Support for Waterfurnace.""" + from __future__ import annotations from homeassistant.components.sensor import ( @@ -103,12 +104,9 @@ def setup_platform( if discovery_info is None: return - sensors = [] client = hass.data[WF_DOMAIN] - for description in SENSORS: - sensors.append(WaterFurnaceSensor(client, description)) - add_entities(sensors) + add_entities(WaterFurnaceSensor(client, description) for description in SENSORS) class WaterFurnaceSensor(SensorEntity): diff --git a/homeassistant/components/watson_iot/__init__.py b/homeassistant/components/watson_iot/__init__.py index 5e3a150bed1..8a412f81575 100644 --- a/homeassistant/components/watson_iot/__init__.py +++ b/homeassistant/components/watson_iot/__init__.py @@ -1,4 +1,5 @@ """Support for the IBM Watson IoT Platform.""" + import logging import queue import threading diff --git a/homeassistant/components/watson_tts/tts.py b/homeassistant/components/watson_tts/tts.py index 7adb1b1582f..3cf1582e008 100644 --- a/homeassistant/components/watson_tts/tts.py +++ b/homeassistant/components/watson_tts/tts.py @@ -1,4 +1,5 @@ """Support for IBM Watson TTS integration.""" + import logging from ibm_cloud_sdk_core.authenticators import IAMAuthenticator diff --git a/homeassistant/components/watttime/__init__.py b/homeassistant/components/watttime/__init__.py index cac73f597f6..6b32cf723a3 100644 --- a/homeassistant/components/watttime/__init__.py +++ b/homeassistant/components/watttime/__init__.py @@ -1,4 +1,5 @@ """The WattTime integration.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/watttime/config_flow.py b/homeassistant/components/watttime/config_flow.py index 12601c0af83..549f6fc7679 100644 --- a/homeassistant/components/watttime/config_flow.py +++ b/homeassistant/components/watttime/config_flow.py @@ -1,4 +1,5 @@ """Config flow for WattTime integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -8,8 +9,12 @@ from aiowatttime import Client from aiowatttime.errors import CoordinatesNotFoundError, InvalidCredentialsError import voluptuous as vol -from homeassistant import config_entries -from homeassistant.config_entries import ConfigEntry, OptionsFlow +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.const import ( CONF_LATITUDE, CONF_LONGITUDE, @@ -18,7 +23,6 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import aiohttp_client, config_validation as cv from .const import ( @@ -68,7 +72,7 @@ def get_unique_id(data: dict[str, Any]) -> str: return f"{data[CONF_LATITUDE]}, {data[CONF_LONGITUDE]}" -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class WattTimeConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for WattTime.""" VERSION = 1 @@ -80,7 +84,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def _async_validate_credentials( self, username: str, password: str, error_step_id: str, error_schema: vol.Schema - ) -> FlowResult: + ) -> ConfigFlowResult: """Validate input credentials and proceed accordingly.""" session = aiohttp_client.async_get_clientsession(self.hass) @@ -128,7 +132,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_coordinates( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the coordinates step.""" if not user_input: return self.async_show_form( @@ -174,7 +178,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_location( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the "pick a location" step.""" if not user_input: return self.async_show_form( @@ -190,14 +194,16 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) return await self.async_step_coordinates() - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Handle configuration by re-auth.""" self._data = {**entry_data} return await self.async_step_reauth_confirm() async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle re-auth completion.""" if not user_input: return self.async_show_form( @@ -217,7 +223,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" if not user_input: return self.async_show_form( @@ -232,7 +238,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -class WattTimeOptionsFlowHandler(config_entries.OptionsFlow): +class WattTimeOptionsFlowHandler(OptionsFlow): """Handle a WattTime options flow.""" def __init__(self, entry: ConfigEntry) -> None: @@ -241,7 +247,7 @@ class WattTimeOptionsFlowHandler(config_entries.OptionsFlow): async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Manage the options.""" if user_input is not None: return self.async_create_entry(data=user_input) diff --git a/homeassistant/components/watttime/const.py b/homeassistant/components/watttime/const.py index ce2731e7832..fe517af6d65 100644 --- a/homeassistant/components/watttime/const.py +++ b/homeassistant/components/watttime/const.py @@ -1,4 +1,5 @@ """Constants for the WattTime integration.""" + import logging DOMAIN = "watttime" diff --git a/homeassistant/components/watttime/diagnostics.py b/homeassistant/components/watttime/diagnostics.py index 2808e8e3c35..adedcd13835 100644 --- a/homeassistant/components/watttime/diagnostics.py +++ b/homeassistant/components/watttime/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for WattTime.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/watttime/icons.json b/homeassistant/components/watttime/icons.json new file mode 100644 index 00000000000..e4c9cfa4f5b --- /dev/null +++ b/homeassistant/components/watttime/icons.json @@ -0,0 +1,12 @@ +{ + "entity": { + "sensor": { + "marginal_operating_emissions_rate": { + "default": "mdi:blur" + }, + "relative_marginal_emissions_intensity": { + "default": "mdi:blur" + } + } + } +} diff --git a/homeassistant/components/watttime/sensor.py b/homeassistant/components/watttime/sensor.py index ca5b0d06fa2..c6cc81580d7 100644 --- a/homeassistant/components/watttime/sensor.py +++ b/homeassistant/components/watttime/sensor.py @@ -1,4 +1,5 @@ """Support for WattTime sensors.""" + from __future__ import annotations from collections.abc import Mapping @@ -38,14 +39,12 @@ REALTIME_EMISSIONS_SENSOR_DESCRIPTIONS = ( SensorEntityDescription( key=SENSOR_TYPE_REALTIME_EMISSIONS_MOER, translation_key="marginal_operating_emissions_rate", - icon="mdi:blur", native_unit_of_measurement=f"{UnitOfMass.POUNDS} CO2/MWh", state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=SENSOR_TYPE_REALTIME_EMISSIONS_PERCENT, translation_key="relative_marginal_emissions_intensity", - icon="mdi:blur", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, ), diff --git a/homeassistant/components/waze_travel_time/__init__.py b/homeassistant/components/waze_travel_time/__init__.py index beaa2ecc69a..9c131f3242c 100644 --- a/homeassistant/components/waze_travel_time/__init__.py +++ b/homeassistant/components/waze_travel_time/__init__.py @@ -1,4 +1,5 @@ """The waze_travel_time component.""" + import asyncio from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/waze_travel_time/config_flow.py b/homeassistant/components/waze_travel_time/config_flow.py index 60134452025..a196c5f4f57 100644 --- a/homeassistant/components/waze_travel_time/config_flow.py +++ b/homeassistant/components/waze_travel_time/config_flow.py @@ -1,12 +1,17 @@ """Config flow for Waze Travel Time integration.""" + from __future__ import annotations import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.const import CONF_NAME, CONF_REGION from homeassistant.core import HomeAssistant, callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.selector import ( BooleanSelector, SelectSelector, @@ -86,14 +91,14 @@ def default_options(hass: HomeAssistant) -> dict[str, str | bool]: return defaults -class WazeOptionsFlow(config_entries.OptionsFlow): +class WazeOptionsFlow(OptionsFlow): """Handle an options flow for Waze Travel Time.""" - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + def __init__(self, config_entry: ConfigEntry) -> None: """Initialize waze options flow.""" self.config_entry = config_entry - async def async_step_init(self, user_input=None) -> FlowResult: + async def async_step_init(self, user_input=None) -> ConfigFlowResult: """Handle the initial step.""" if user_input is not None: return self.async_create_entry( @@ -109,7 +114,7 @@ class WazeOptionsFlow(config_entries.OptionsFlow): ) -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class WazeConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Waze Travel Time.""" VERSION = 1 @@ -117,12 +122,12 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback def async_get_options_flow( - config_entry: config_entries.ConfigEntry, + config_entry: ConfigEntry, ) -> WazeOptionsFlow: """Get the options flow for this handler.""" return WazeOptionsFlow(config_entry) - async def async_step_user(self, user_input=None) -> FlowResult: + async def async_step_user(self, user_input=None) -> ConfigFlowResult: """Handle the initial step.""" errors = {} user_input = user_input or {} diff --git a/homeassistant/components/waze_travel_time/const.py b/homeassistant/components/waze_travel_time/const.py index 572676e1966..84e41c3963f 100644 --- a/homeassistant/components/waze_travel_time/const.py +++ b/homeassistant/components/waze_travel_time/const.py @@ -1,4 +1,5 @@ """Constants for waze_travel_time.""" + from __future__ import annotations DOMAIN = "waze_travel_time" diff --git a/homeassistant/components/waze_travel_time/helpers.py b/homeassistant/components/waze_travel_time/helpers.py index 0659424429f..c6fe4d0c9bd 100644 --- a/homeassistant/components/waze_travel_time/helpers.py +++ b/homeassistant/components/waze_travel_time/helpers.py @@ -1,4 +1,5 @@ """Helpers for Waze Travel Time integration.""" + import logging from pywaze.route_calculator import WazeRouteCalculator, WRCError @@ -19,7 +20,7 @@ async def is_valid_config_entry( httpx_client = get_async_client(hass) client = WazeRouteCalculator(region=region, client=httpx_client) try: - await client.calc_all_routes_info(resolved_origin, resolved_destination) + await client.calc_routes(resolved_origin, resolved_destination) except WRCError as error: _LOGGER.error("Error trying to validate entry: %s", error) return False diff --git a/homeassistant/components/waze_travel_time/icons.json b/homeassistant/components/waze_travel_time/icons.json new file mode 100644 index 00000000000..54d3183363e --- /dev/null +++ b/homeassistant/components/waze_travel_time/icons.json @@ -0,0 +1,9 @@ +{ + "entity": { + "sensor": { + "waze_travel_time": { + "default": "mdi:car" + } + } + } +} diff --git a/homeassistant/components/waze_travel_time/manifest.json b/homeassistant/components/waze_travel_time/manifest.json index 728a91e4933..4fc08cf983d 100644 --- a/homeassistant/components/waze_travel_time/manifest.json +++ b/homeassistant/components/waze_travel_time/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/waze_travel_time", "iot_class": "cloud_polling", "loggers": ["pywaze", "homeassistant.helpers.location"], - "requirements": ["pywaze==0.5.1"] + "requirements": ["pywaze==1.0.0"] } diff --git a/homeassistant/components/waze_travel_time/sensor.py b/homeassistant/components/waze_travel_time/sensor.py index ef372e5fd33..518de269bc5 100644 --- a/homeassistant/components/waze_travel_time/sensor.py +++ b/homeassistant/components/waze_travel_time/sensor.py @@ -1,4 +1,5 @@ """Support for Waze travel time sensor.""" + from __future__ import annotations import asyncio @@ -90,6 +91,7 @@ class WazeTravelTime(SensorEntity): identifiers={(DOMAIN, DOMAIN)}, configuration_url="https://www.waze.com", ) + _attr_translation_key = "waze_travel_time" def __init__( self, @@ -105,7 +107,6 @@ class WazeTravelTime(SensorEntity): self._attr_name = name self._origin = origin self._destination = destination - self._attr_icon = "mdi:car" self._state = None async def async_added_to_hass(self) -> None: @@ -195,7 +196,7 @@ class WazeTravelTimeData: routes = {} try: - routes = await self.client.calc_all_routes_info( + routes = await self.client.calc_routes( self.origin, self.destination, vehicle_type=vehicle_type, @@ -203,29 +204,37 @@ class WazeTravelTimeData: avoid_subscription_roads=avoid_subscription_roads, avoid_ferries=avoid_ferries, real_time=realtime, + alternatives=3, ) if incl_filter not in {None, ""}: - routes = { - k: v - for k, v in routes.items() - if incl_filter.lower() in k.lower() - } + routes = [ + r + for r in routes + if any( + incl_filter.lower() == street_name.lower() + for street_name in r.street_names + ) + ] if excl_filter not in {None, ""}: - routes = { - k: v - for k, v in routes.items() - if excl_filter.lower() not in k.lower() - } + routes = [ + r + for r in routes + if not any( + excl_filter.lower() == street_name.lower() + for street_name in r.street_names + ) + ] - if routes: - route = list(routes)[0] - else: + if len(routes) < 1: _LOGGER.warning("No routes found") return - self.duration, distance = routes[route] + route = routes[0] + + self.duration = route.duration + distance = route.distance if units == IMPERIAL_UNITS: # Convert to miles. @@ -235,10 +244,7 @@ class WazeTravelTimeData: else: self.distance = distance - self.route = route + self.route = route.name except WRCError as exp: _LOGGER.warning("Error on retrieving data: %s", exp) return - except KeyError: - _LOGGER.error("Error retrieving data from server") - return diff --git a/homeassistant/components/waze_travel_time/strings.json b/homeassistant/components/waze_travel_time/strings.json index 61b93f13f17..2a5017a5b9f 100644 --- a/homeassistant/components/waze_travel_time/strings.json +++ b/homeassistant/components/waze_travel_time/strings.json @@ -26,8 +26,8 @@ "data": { "units": "Units", "vehicle_type": "Vehicle Type", - "incl_filter": "Substring in Description of Selected Route", - "excl_filter": "Substring NOT in Description of Selected Route", + "incl_filter": "Streetname which must be part of the Selected Route", + "excl_filter": "Streetname which must NOT be part of the Selected Route", "realtime": "Realtime Travel Time?", "avoid_toll_roads": "Avoid Toll Roads?", "avoid_ferries": "Avoid Ferries?", diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index bdc8ae4d514..404154ade2b 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -1,8 +1,8 @@ """Weather component that handles meteorological data for your location.""" + from __future__ import annotations import abc -import asyncio from collections.abc import Callable, Iterable from contextlib import suppress from datetime import timedelta @@ -47,7 +47,6 @@ from homeassistant.helpers.config_validation import ( # noqa: F401 ) from homeassistant.helpers.entity import ABCCachedProperties, Entity, EntityDescription from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.entity_platform import EntityPlatform import homeassistant.helpers.issue_registry as ir from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import ( @@ -55,12 +54,12 @@ from homeassistant.helpers.update_coordinator import ( DataUpdateCoordinator, TimestampDataUpdateCoordinator, ) -from homeassistant.loader import async_get_issue_tracker, async_suggest_report_issue from homeassistant.util.dt import utcnow from homeassistant.util.json import JsonValueType from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM -from .const import ( # noqa: F401 +from . import group as group_pre_import # noqa: F401 +from .const import ( ATTR_WEATHER_APPARENT_TEMPERATURE, ATTR_WEATHER_CLOUD_COVERAGE, ATTR_WEATHER_DEW_POINT, @@ -109,7 +108,6 @@ ATTR_CONDITION_SNOWY_RAINY = "snowy-rainy" ATTR_CONDITION_SUNNY = "sunny" ATTR_CONDITION_WINDY = "windy" ATTR_CONDITION_WINDY_VARIANT = "windy-variant" -ATTR_FORECAST = "forecast" ATTR_FORECAST_IS_DAYTIME: Final = "is_daytime" ATTR_FORECAST_CONDITION: Final = "condition" ATTR_FORECAST_HUMIDITY: Final = "humidity" @@ -304,13 +302,8 @@ CACHED_PROPERTIES_WITH_ATTR_ = { class WeatherEntity(Entity, PostInit, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): """ABC for weather data.""" - _entity_component_unrecorded_attributes = frozenset({ATTR_FORECAST}) - entity_description: WeatherEntityDescription _attr_condition: str | None = None - # _attr_forecast is deprecated, implement async_forecast_daily, - # async_forecast_hourly or async_forecast_twice daily instead - _attr_forecast: list[Forecast] | None = None _attr_humidity: float | None = None _attr_ozone: float | None = None _attr_cloud_coverage: int | None = None @@ -336,8 +329,6 @@ class WeatherEntity(Entity, PostInit, cached_properties=CACHED_PROPERTIES_WITH_A Literal["daily", "hourly", "twice_daily"], list[Callable[[list[JsonValueType] | None], None]], ] - __weather_reported_legacy_forecast = False - __weather_legacy_forecast = False _weather_option_temperature_unit: str | None = None _weather_option_pressure_unit: str | None = None @@ -349,77 +340,6 @@ class WeatherEntity(Entity, PostInit, cached_properties=CACHED_PROPERTIES_WITH_A """Finish initializing.""" self._forecast_listeners = {"daily": [], "hourly": [], "twice_daily": []} - def __init_subclass__(cls, **kwargs: Any) -> None: - """Post initialisation processing.""" - super().__init_subclass__(**kwargs) - if ( - "forecast" in cls.__dict__ - and cls.async_forecast_daily is WeatherEntity.async_forecast_daily - and cls.async_forecast_hourly is WeatherEntity.async_forecast_hourly - and cls.async_forecast_twice_daily - is WeatherEntity.async_forecast_twice_daily - ): - cls.__weather_legacy_forecast = True - - @callback - def add_to_platform_start( - self, - hass: HomeAssistant, - platform: EntityPlatform, - parallel_updates: asyncio.Semaphore | None, - ) -> None: - """Start adding an entity to a platform.""" - super().add_to_platform_start(hass, platform, parallel_updates) - if self.__weather_legacy_forecast: - self._report_legacy_forecast(hass) - - def _report_legacy_forecast(self, hass: HomeAssistant) -> None: - """Log warning and create an issue if the entity imlpements legacy forecast.""" - if "custom_components" not in type(self).__module__: - # Do not report core integrations as they are already fixed or PR is open. - return - - report_issue = async_suggest_report_issue( - hass, - integration_domain=self.platform.platform_name, - module=type(self).__module__, - ) - _LOGGER.warning( - ( - "%s::%s implements the `forecast` property or sets " - "`self._attr_forecast` in a subclass of WeatherEntity, this is " - "deprecated and will be unsupported from Home Assistant 2024.3." - " Please %s" - ), - self.platform.platform_name, - self.__class__.__name__, - report_issue, - ) - - translation_placeholders = {"platform": self.platform.platform_name} - translation_key = "deprecated_weather_forecast_no_url" - issue_tracker = async_get_issue_tracker( - hass, - integration_domain=self.platform.platform_name, - module=type(self).__module__, - ) - if issue_tracker: - translation_placeholders["issue_tracker"] = issue_tracker - translation_key = "deprecated_weather_forecast_url" - ir.async_create_issue( - self.hass, - DOMAIN, - f"deprecated_weather_forecast_{self.platform.platform_name}", - breaks_in_ha_version="2024.3.0", - is_fixable=False, - is_persistent=False, - issue_domain=self.platform.platform_name, - severity=ir.IssueSeverity.WARNING, - translation_key=translation_key, - translation_placeholders=translation_placeholders, - ) - self.__weather_reported_legacy_forecast = True - async def async_internal_added_to_hass(self) -> None: """Call when the weather entity is added to hass.""" await super().async_internal_added_to_hass() @@ -603,23 +523,6 @@ class WeatherEntity(Entity, PostInit, cached_properties=CACHED_PROPERTIES_WITH_A return self._default_visibility_unit - @property - def forecast(self) -> list[Forecast] | None: - """Return the forecast in native units. - - Should not be overridden by integrations. Kept for backwards compatibility. - """ - if ( - self._attr_forecast is not None - and type(self).async_forecast_daily is WeatherEntity.async_forecast_daily - and type(self).async_forecast_hourly is WeatherEntity.async_forecast_hourly - and type(self).async_forecast_twice_daily - is WeatherEntity.async_forecast_twice_daily - and not self.__weather_reported_legacy_forecast - ): - self._report_legacy_forecast(self.hass) - return self._attr_forecast - async def async_forecast_daily(self) -> list[Forecast] | None: """Return the daily forecast in native units.""" raise NotImplementedError @@ -673,7 +576,7 @@ class WeatherEntity(Entity, PostInit, cached_properties=CACHED_PROPERTIES_WITH_A @final @property - def state_attributes(self) -> dict[str, Any]: # noqa: C901 + def state_attributes(self) -> dict[str, Any]: """Return the state attributes, converted. Attributes are configured from native units to user-configured units. @@ -802,9 +705,6 @@ class WeatherEntity(Entity, PostInit, cached_properties=CACHED_PROPERTIES_WITH_A data[ATTR_WEATHER_VISIBILITY_UNIT] = self._visibility_unit data[ATTR_WEATHER_PRECIPITATION_UNIT] = self._precipitation_unit - if self.forecast: - data[ATTR_FORECAST] = self._convert_forecast(self.forecast) - return data @final diff --git a/homeassistant/components/weather/const.py b/homeassistant/components/weather/const.py index c6da2c28c71..0b5246ab31c 100644 --- a/homeassistant/components/weather/const.py +++ b/homeassistant/components/weather/const.py @@ -1,4 +1,5 @@ """Constants for weather.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/weather/group.py b/homeassistant/components/weather/group.py index 2ac081496cd..13a70cc4b6b 100644 --- a/homeassistant/components/weather/group.py +++ b/homeassistant/components/weather/group.py @@ -1,13 +1,16 @@ """Describe group states.""" +from typing import TYPE_CHECKING -from homeassistant.components.group import GroupIntegrationRegistry from homeassistant.core import HomeAssistant, callback +if TYPE_CHECKING: + from homeassistant.components.group import GroupIntegrationRegistry + @callback def async_describe_on_off_states( - hass: HomeAssistant, registry: GroupIntegrationRegistry + hass: HomeAssistant, registry: "GroupIntegrationRegistry" ) -> None: """Describe group on off states.""" registry.exclude_domain() diff --git a/homeassistant/components/weather/intent.py b/homeassistant/components/weather/intent.py index 4fd22ceb0a9..c216fcda17d 100644 --- a/homeassistant/components/weather/intent.py +++ b/homeassistant/components/weather/intent.py @@ -1,4 +1,5 @@ """Intents for the weather integration.""" + from __future__ import annotations import voluptuous as vol diff --git a/homeassistant/components/weather/significant_change.py b/homeassistant/components/weather/significant_change.py index 87e1246ce85..d36139904f5 100644 --- a/homeassistant/components/weather/significant_change.py +++ b/homeassistant/components/weather/significant_change.py @@ -1,4 +1,5 @@ """Helper to test significant Weather state changes.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/weather/strings.json b/homeassistant/components/weather/strings.json index 8879bf158f3..77c9cce864b 100644 --- a/homeassistant/components/weather/strings.json +++ b/homeassistant/components/weather/strings.json @@ -110,14 +110,6 @@ } }, "issues": { - "deprecated_weather_forecast_url": { - "title": "The {platform} custom integration is using deprecated weather forecast", - "description": "The custom integration `{platform}` implements the `forecast` property or sets `self._attr_forecast` in a subclass of WeatherEntity.\n\nPlease create a bug report at {issue_tracker}.\n\nOnce an updated version of `{platform}` is available, install it and restart Home Assistant to fix this issue." - }, - "deprecated_weather_forecast_no_url": { - "title": "[%key:component::weather::issues::deprecated_weather_forecast_url::title%]", - "description": "The custom integration `{platform}` implements the `forecast` property or sets `self._attr_forecast` in a subclass of WeatherEntity.\n\nPlease report it to the author of the {platform} integration.\n\nOnce an updated version of `{platform}` is available, install it and restart Home Assistant to fix this issue." - }, "deprecated_service_weather_get_forecast": { "title": "Detected use of deprecated service `weather.get_forecast`", "fix_flow": { diff --git a/homeassistant/components/weather/websocket_api.py b/homeassistant/components/weather/websocket_api.py index 39a487dcb2f..98adbd1bd02 100644 --- a/homeassistant/components/weather/websocket_api.py +++ b/homeassistant/components/weather/websocket_api.py @@ -1,4 +1,5 @@ """The weather websocket API.""" + from __future__ import annotations from typing import Any, Literal diff --git a/homeassistant/components/weatherflow/__init__.py b/homeassistant/components/weatherflow/__init__.py index fbd206b63f5..819ad90b354 100644 --- a/homeassistant/components/weatherflow/__init__.py +++ b/homeassistant/components/weatherflow/__init__.py @@ -1,4 +1,5 @@ """Get data from Smart Weather station via UDP.""" + from __future__ import annotations from pyweatherflowudp.client import EVENT_DEVICE_DISCOVERED, WeatherFlowListener diff --git a/homeassistant/components/weatherflow/config_flow.py b/homeassistant/components/weatherflow/config_flow.py index d4ee319e70b..11951d60272 100644 --- a/homeassistant/components/weatherflow/config_flow.py +++ b/homeassistant/components/weatherflow/config_flow.py @@ -1,4 +1,5 @@ """Config flow for WeatherFlow.""" + from __future__ import annotations import asyncio @@ -9,9 +10,8 @@ from typing import Any from pyweatherflowudp.client import EVENT_DEVICE_DISCOVERED, WeatherFlowListener from pyweatherflowudp.errors import AddressInUseError, EndpointError, ListenerError -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult from .const import ( DOMAIN, @@ -42,14 +42,14 @@ async def _async_can_discover_devices() -> bool: return True -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class WeatherFlowConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for WeatherFlow.""" VERSION = 1 async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" # Only allow a single instance of integration since the listener diff --git a/homeassistant/components/weatherflow/icons.json b/homeassistant/components/weatherflow/icons.json new file mode 100644 index 00000000000..71a8b48415d --- /dev/null +++ b/homeassistant/components/weatherflow/icons.json @@ -0,0 +1,21 @@ +{ + "entity": { + "sensor": { + "lightning_average_distance": { + "default": "mdi:lightning-bolt" + }, + "lightning_count": { + "default": "mdi:lightning-bolt" + }, + "precipitation_type": { + "default": "mdi:weather-rainy" + }, + "wind_direction": { + "default": "mdi:compass-outline" + }, + "wind_direction_average": { + "default": "mdi:compass-outline" + } + } + } +} diff --git a/homeassistant/components/weatherflow/sensor.py b/homeassistant/components/weatherflow/sensor.py index bbdd79e1533..cacede55c42 100644 --- a/homeassistant/components/weatherflow/sensor.py +++ b/homeassistant/components/weatherflow/sensor.py @@ -1,4 +1,5 @@ """Sensors for the weatherflow integration.""" + from __future__ import annotations from collections.abc import Callable @@ -46,13 +47,6 @@ from homeassistant.util.unit_system import METRIC_SYSTEM from .const import DOMAIN, LOGGER, format_dispatch_call -@dataclass(frozen=True) -class WeatherFlowSensorRequiredKeysMixin: - """Mixin for required keys.""" - - raw_data_conv_fn: Callable[[WeatherFlowDevice], datetime | StateType] - - def precipitation_raw_conversion_fn(raw_data: Enum): """Parse parse precipitation type.""" if raw_data.name.lower() == "unknown": @@ -60,14 +54,14 @@ def precipitation_raw_conversion_fn(raw_data: Enum): return raw_data.name.lower() -@dataclass(frozen=True) -class WeatherFlowSensorEntityDescription( - SensorEntityDescription, WeatherFlowSensorRequiredKeysMixin -): +@dataclass(frozen=True, kw_only=True) +class WeatherFlowSensorEntityDescription(SensorEntityDescription): """Describes WeatherFlow sensor entity.""" + raw_data_conv_fn: Callable[[WeatherFlowDevice], datetime | StateType] + event_subscriptions: list[str] = field(default_factory=lambda: [EVENT_OBSERVATION]) - imperial_suggested_unit: None | str = None + imperial_suggested_unit: str | None = None def get_native_value(self, device: WeatherFlowDevice) -> datetime | StateType: """Return the parsed sensor value.""" @@ -138,7 +132,6 @@ SENSORS: tuple[WeatherFlowSensorEntityDescription, ...] = ( ), WeatherFlowSensorEntityDescription( key="lightning_strike_average_distance", - icon="mdi:lightning-bolt", state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.DISTANCE, native_unit_of_measurement=UnitOfLength.KILOMETERS, @@ -149,7 +142,6 @@ SENSORS: tuple[WeatherFlowSensorEntityDescription, ...] = ( WeatherFlowSensorEntityDescription( key="lightning_strike_count", translation_key="lightning_count", - icon="mdi:lightning-bolt", state_class=SensorStateClass.TOTAL, raw_data_conv_fn=lambda raw_data: raw_data, ), @@ -158,12 +150,10 @@ SENSORS: tuple[WeatherFlowSensorEntityDescription, ...] = ( translation_key="precipitation_type", device_class=SensorDeviceClass.ENUM, options=["none", "rain", "hail", "rain_hail", "unknown"], - icon="mdi:weather-rainy", raw_data_conv_fn=precipitation_raw_conversion_fn, ), WeatherFlowSensorEntityDescription( key="rain_accumulation_previous_minute", - icon="mdi:weather-rainy", native_unit_of_measurement=UnitOfPrecipitationDepth.MILLIMETERS, state_class=SensorStateClass.TOTAL, device_class=SensorDeviceClass.PRECIPITATION, @@ -174,7 +164,6 @@ SENSORS: tuple[WeatherFlowSensorEntityDescription, ...] = ( key="rain_rate", state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.PRECIPITATION_INTENSITY, - icon="mdi:weather-rainy", native_unit_of_measurement=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR, raw_data_conv_fn=lambda raw_data: raw_data.magnitude, ), @@ -242,7 +231,6 @@ SENSORS: tuple[WeatherFlowSensorEntityDescription, ...] = ( WeatherFlowSensorEntityDescription( key="wind_gust", translation_key="wind_gust", - icon="mdi:weather-windy", device_class=SensorDeviceClass.WIND_SPEED, native_unit_of_measurement=UnitOfSpeed.METERS_PER_SECOND, state_class=SensorStateClass.MEASUREMENT, @@ -252,7 +240,6 @@ SENSORS: tuple[WeatherFlowSensorEntityDescription, ...] = ( WeatherFlowSensorEntityDescription( key="wind_lull", translation_key="wind_lull", - icon="mdi:weather-windy", device_class=SensorDeviceClass.WIND_SPEED, native_unit_of_measurement=UnitOfSpeed.METERS_PER_SECOND, state_class=SensorStateClass.MEASUREMENT, @@ -262,7 +249,6 @@ SENSORS: tuple[WeatherFlowSensorEntityDescription, ...] = ( WeatherFlowSensorEntityDescription( key="wind_speed", device_class=SensorDeviceClass.WIND_SPEED, - icon="mdi:weather-windy", event_subscriptions=[EVENT_RAPID_WIND, EVENT_OBSERVATION], native_unit_of_measurement=UnitOfSpeed.METERS_PER_SECOND, state_class=SensorStateClass.MEASUREMENT, @@ -272,7 +258,6 @@ SENSORS: tuple[WeatherFlowSensorEntityDescription, ...] = ( WeatherFlowSensorEntityDescription( key="wind_average", translation_key="wind_speed_average", - icon="mdi:weather-windy", device_class=SensorDeviceClass.WIND_SPEED, native_unit_of_measurement=UnitOfSpeed.METERS_PER_SECOND, state_class=SensorStateClass.MEASUREMENT, @@ -282,7 +267,6 @@ SENSORS: tuple[WeatherFlowSensorEntityDescription, ...] = ( WeatherFlowSensorEntityDescription( key="wind_direction", translation_key="wind_direction", - icon="mdi:compass-outline", native_unit_of_measurement=DEGREE, state_class=SensorStateClass.MEASUREMENT, event_subscriptions=[EVENT_RAPID_WIND, EVENT_OBSERVATION], @@ -291,7 +275,6 @@ SENSORS: tuple[WeatherFlowSensorEntityDescription, ...] = ( WeatherFlowSensorEntityDescription( key="wind_direction_average", translation_key="wind_direction_average", - icon="mdi:compass-outline", native_unit_of_measurement=DEGREE, state_class=SensorStateClass.MEASUREMENT, raw_data_conv_fn=lambda raw_data: raw_data.magnitude, diff --git a/homeassistant/components/weatherflow_cloud/__init__.py b/homeassistant/components/weatherflow_cloud/__init__.py index 24b862433bd..a40386100e7 100644 --- a/homeassistant/components/weatherflow_cloud/__init__.py +++ b/homeassistant/components/weatherflow_cloud/__init__.py @@ -1,4 +1,5 @@ """The WeatherflowCloud integration.""" + from __future__ import annotations from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/weatherflow_cloud/config_flow.py b/homeassistant/components/weatherflow_cloud/config_flow.py index 85c1acbb807..6e6212042e1 100644 --- a/homeassistant/components/weatherflow_cloud/config_flow.py +++ b/homeassistant/components/weatherflow_cloud/config_flow.py @@ -1,4 +1,5 @@ """Config flow for WeatherflowCloud integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -10,7 +11,6 @@ from weatherflow4py.api import WeatherFlowRestAPI from homeassistant import config_entries from homeassistant.const import CONF_API_TOKEN -from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN @@ -32,7 +32,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - async def async_step_reauth(self, user_input: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, user_input: Mapping[str, Any] + ) -> config_entries.ConfigFlowResult: """Handle a flow for reauth.""" errors = {} @@ -58,7 +60,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> config_entries.ConfigFlowResult: """Handle a flow initialized by the user.""" errors = {} diff --git a/homeassistant/components/weatherflow_cloud/const.py b/homeassistant/components/weatherflow_cloud/const.py index 73245346b50..43594863e14 100644 --- a/homeassistant/components/weatherflow_cloud/const.py +++ b/homeassistant/components/weatherflow_cloud/const.py @@ -1,4 +1,5 @@ """Constants for the WeatherflowCloud integration.""" + import logging DOMAIN = "weatherflow_cloud" diff --git a/homeassistant/components/weatherflow_cloud/coordinator.py b/homeassistant/components/weatherflow_cloud/coordinator.py index 7b9ddaafaae..78b4f3be223 100644 --- a/homeassistant/components/weatherflow_cloud/coordinator.py +++ b/homeassistant/components/weatherflow_cloud/coordinator.py @@ -1,9 +1,10 @@ """Data coordinator for WeatherFlow Cloud Data.""" + from datetime import timedelta from aiohttp import ClientResponseError from weatherflow4py.api import WeatherFlowRestAPI -from weatherflow4py.models.unified import WeatherFlowData +from weatherflow4py.models.rest.unified import WeatherFlowDataREST from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed @@ -13,7 +14,7 @@ from .const import DOMAIN, LOGGER class WeatherFlowCloudDataUpdateCoordinator( - DataUpdateCoordinator[dict[int, WeatherFlowData]] + DataUpdateCoordinator[dict[int, WeatherFlowDataREST]] ): """Class to manage fetching REST Based WeatherFlow Forecast data.""" @@ -28,7 +29,7 @@ class WeatherFlowCloudDataUpdateCoordinator( update_interval=timedelta(minutes=15), ) - async def _async_update_data(self) -> dict[int, WeatherFlowData]: + async def _async_update_data(self) -> dict[int, WeatherFlowDataREST]: """Fetch data from WeatherFlow Forecast.""" try: async with self.weather_api: diff --git a/homeassistant/components/weatherflow_cloud/manifest.json b/homeassistant/components/weatherflow_cloud/manifest.json index 72a49c0cf19..8376bd1b50d 100644 --- a/homeassistant/components/weatherflow_cloud/manifest.json +++ b/homeassistant/components/weatherflow_cloud/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/weatherflow_cloud", "iot_class": "cloud_polling", - "requirements": ["weatherflow4py==0.1.17"] + "requirements": ["weatherflow4py==0.2.17"] } diff --git a/homeassistant/components/weatherflow_cloud/weather.py b/homeassistant/components/weatherflow_cloud/weather.py index b4ed6a3a9d8..23aa6b1a031 100644 --- a/homeassistant/components/weatherflow_cloud/weather.py +++ b/homeassistant/components/weatherflow_cloud/weather.py @@ -1,7 +1,8 @@ """Support for WeatherFlow Forecast weather service.""" + from __future__ import annotations -from weatherflow4py.models.unified import WeatherFlowData +from weatherflow4py.models.rest.unified import WeatherFlowDataREST from homeassistant.components.weather import ( Forecast, @@ -78,7 +79,7 @@ class WeatherFlowWeather( ) @property - def local_data(self) -> WeatherFlowData: + def local_data(self) -> WeatherFlowDataREST: """Return the local weather data object for this station.""" return self.coordinator.data[self.station_id] diff --git a/homeassistant/components/weatherkit/__init__.py b/homeassistant/components/weatherkit/__init__.py index 307b2272d6c..49158182696 100644 --- a/homeassistant/components/weatherkit/__init__.py +++ b/homeassistant/components/weatherkit/__init__.py @@ -1,4 +1,5 @@ """Integration for Apple's WeatherKit API.""" + from __future__ import annotations from apple_weatherkit.client import ( diff --git a/homeassistant/components/weatherkit/config_flow.py b/homeassistant/components/weatherkit/config_flow.py index 657a80547ab..760516e894d 100644 --- a/homeassistant/components/weatherkit/config_flow.py +++ b/homeassistant/components/weatherkit/config_flow.py @@ -1,4 +1,5 @@ """Adds config flow for WeatherKit.""" + from __future__ import annotations from collections.abc import Mapping @@ -12,7 +13,7 @@ from apple_weatherkit.client import ( ) import voluptuous as vol -from homeassistant import config_entries, data_entry_flow +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_LATITUDE, CONF_LOCATION, CONF_LONGITUDE from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.selector import ( @@ -53,7 +54,7 @@ class WeatherKitUnsupportedLocationError(Exception): """Error to indicate a location is unsupported.""" -class WeatherKitFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class WeatherKitFlowHandler(ConfigFlow, domain=DOMAIN): """Config flow for WeatherKit.""" VERSION = 1 @@ -61,7 +62,7 @@ class WeatherKitFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None, - ) -> data_entry_flow.FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" errors = {} if user_input is not None: diff --git a/homeassistant/components/weatherkit/const.py b/homeassistant/components/weatherkit/const.py index e35dd33c561..99cd50df8fa 100644 --- a/homeassistant/components/weatherkit/const.py +++ b/homeassistant/components/weatherkit/const.py @@ -1,4 +1,5 @@ """Constants for WeatherKit.""" + from logging import Logger, getLogger LOGGER: Logger = getLogger(__package__) diff --git a/homeassistant/components/weatherkit/coordinator.py b/homeassistant/components/weatherkit/coordinator.py index 824c85781ea..ddabba2fc1f 100644 --- a/homeassistant/components/weatherkit/coordinator.py +++ b/homeassistant/components/weatherkit/coordinator.py @@ -1,4 +1,5 @@ """DataUpdateCoordinator for WeatherKit integration.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/weatherkit/icons.json b/homeassistant/components/weatherkit/icons.json new file mode 100644 index 00000000000..c555c58f420 --- /dev/null +++ b/homeassistant/components/weatherkit/icons.json @@ -0,0 +1,9 @@ +{ + "entity": { + "sensor": { + "pressure_trend": { + "default": "mdi:gauge" + } + } + } +} diff --git a/homeassistant/components/weatherkit/sensor.py b/homeassistant/components/weatherkit/sensor.py index 38b4a60cba5..d9c17bb855a 100644 --- a/homeassistant/components/weatherkit/sensor.py +++ b/homeassistant/components/weatherkit/sensor.py @@ -1,6 +1,5 @@ """WeatherKit sensors.""" - from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, @@ -28,7 +27,6 @@ SENSORS = ( SensorEntityDescription( key="pressureTrend", device_class=SensorDeviceClass.ENUM, - icon="mdi:gauge", options=["rising", "falling", "steady"], translation_key="pressure_trend", ), diff --git a/homeassistant/components/webhook/__init__.py b/homeassistant/components/webhook/__init__.py index 00b27fdb647..f2d1416e2c9 100644 --- a/homeassistant/components/webhook/__init__.py +++ b/homeassistant/components/webhook/__init__.py @@ -1,4 +1,5 @@ """Webhooks for Home Assistant.""" + from __future__ import annotations from collections.abc import Awaitable, Callable, Iterable @@ -14,7 +15,7 @@ from aiohttp.web import Request, Response import voluptuous as vol from homeassistant.components import websocket_api -from homeassistant.components.http.view import HomeAssistantView +from homeassistant.components.http import KEY_HASS, HomeAssistantView from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.network import get_url, is_cloud_connection @@ -88,9 +89,9 @@ def async_generate_id() -> str: @bind_hass def async_generate_url(hass: HomeAssistant, webhook_id: str) -> str: """Generate the full URL for a webhook_id.""" - return "{}{}".format( - get_url(hass, prefer_external=True, allow_cloud=False), - async_generate_path(webhook_id), + return ( + f"{get_url(hass, prefer_external=True, allow_cloud=False)}" + f"{async_generate_path(webhook_id)}" ) @@ -202,14 +203,13 @@ class WebhookView(HomeAssistantView): async def _handle(self, request: Request, webhook_id: str) -> Response: """Handle webhook call.""" _LOGGER.debug("Handling webhook %s payload for %s", request.method, webhook_id) - hass = request.app["hass"] + hass = request.app[KEY_HASS] return await async_handle_webhook(hass, webhook_id, request) get = _handle head = _handle post = _handle put = _handle - get = _handle @websocket_api.websocket_command( diff --git a/homeassistant/components/webhook/trigger.py b/homeassistant/components/webhook/trigger.py index 05bb53564bd..b4fd3008cd8 100644 --- a/homeassistant/components/webhook/trigger.py +++ b/homeassistant/components/webhook/trigger.py @@ -1,4 +1,5 @@ """Offer webhook triggered automation rules.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/homeassistant/components/webmin/config_flow.py b/homeassistant/components/webmin/config_flow.py index 783590d35ba..1d9c86edbac 100644 --- a/homeassistant/components/webmin/config_flow.py +++ b/homeassistant/components/webmin/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Webmin.""" + from __future__ import annotations from collections.abc import Mapping diff --git a/homeassistant/components/webmin/coordinator.py b/homeassistant/components/webmin/coordinator.py index 9a725ee2a77..28c8d54b0d2 100644 --- a/homeassistant/components/webmin/coordinator.py +++ b/homeassistant/components/webmin/coordinator.py @@ -1,4 +1,5 @@ """Data update coordinator for the Webmin integration.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/webmin/diagnostics.py b/homeassistant/components/webmin/diagnostics.py new file mode 100644 index 00000000000..390db73814a --- /dev/null +++ b/homeassistant/components/webmin/diagnostics.py @@ -0,0 +1,36 @@ +"""Diagnostics support for Webmin.""" + +from typing import Any + +from homeassistant.components.diagnostics import async_redact_data +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_UNIQUE_ID, CONF_USERNAME +from homeassistant.core import HomeAssistant + +from .const import DOMAIN +from .coordinator import WebminUpdateCoordinator + +TO_REDACT = { + CONF_HOST, + CONF_UNIQUE_ID, + CONF_USERNAME, + CONF_PASSWORD, + "address", + "address6", + "ether", + "broadcast", + "device", + "dir", + "title", + "entry_id", +} + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + coordinator: WebminUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + return async_redact_data( + {"entry": entry.as_dict(), "data": coordinator.data}, TO_REDACT + ) diff --git a/homeassistant/components/webmin/manifest.json b/homeassistant/components/webmin/manifest.json index a15ca0a1f0d..12a03830cb8 100644 --- a/homeassistant/components/webmin/manifest.json +++ b/homeassistant/components/webmin/manifest.json @@ -7,5 +7,5 @@ "integration_type": "device", "iot_class": "local_polling", "loggers": ["webmin"], - "requirements": ["webmin-xmlrpc==0.0.1"] + "requirements": ["webmin-xmlrpc==0.0.2"] } diff --git a/homeassistant/components/webmin/sensor.py b/homeassistant/components/webmin/sensor.py index f20f8f9b625..90d3fd71532 100644 --- a/homeassistant/components/webmin/sensor.py +++ b/homeassistant/components/webmin/sensor.py @@ -1,4 +1,5 @@ """Support for Webmin sensors.""" + from __future__ import annotations from homeassistant.components.sensor import ( diff --git a/homeassistant/components/webostv/__init__.py b/homeassistant/components/webostv/__init__.py index 6e960ceb143..479407c3199 100644 --- a/homeassistant/components/webostv/__init__.py +++ b/homeassistant/components/webostv/__init__.py @@ -1,4 +1,5 @@ """Support for LG webOS Smart TV.""" + from __future__ import annotations from contextlib import suppress diff --git a/homeassistant/components/webostv/config_flow.py b/homeassistant/components/webostv/config_flow.py index 1669e5a4c89..f380e49f8a3 100644 --- a/homeassistant/components/webostv/config_flow.py +++ b/homeassistant/components/webostv/config_flow.py @@ -1,4 +1,5 @@ """Config flow to configure webostv component.""" + from __future__ import annotations from collections.abc import Mapping @@ -10,10 +11,15 @@ from aiowebostv import WebOsTvPairError import voluptuous as vol from homeassistant.components import ssdp -from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.const import CONF_CLIENT_SECRET, CONF_HOST, CONF_NAME from homeassistant.core import callback -from homeassistant.data_entry_flow import AbortFlow, FlowResult +from homeassistant.data_entry_flow import AbortFlow from homeassistant.helpers import config_validation as cv from . import async_control_connect, update_client_key @@ -51,7 +57,7 @@ class FlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" errors: dict[str, str] = {} if user_input is not None: @@ -82,7 +88,7 @@ class FlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_pairing( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Display pairing form.""" self._async_check_configured_entry() @@ -107,7 +113,9 @@ class FlowHandler(ConfigFlow, domain=DOMAIN): return self.async_show_form(step_id="pairing", errors=errors) - async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: + async def async_step_ssdp( + self, discovery_info: ssdp.SsdpServiceInfo + ) -> ConfigFlowResult: """Handle a flow initialized by discovery.""" assert discovery_info.ssdp_location host = urlparse(discovery_info.ssdp_location).hostname @@ -129,7 +137,9 @@ class FlowHandler(ConfigFlow, domain=DOMAIN): self._uuid = uuid return await self.async_step_pairing() - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Perform reauth upon an WebOsTvPairError.""" self._host = entry_data[CONF_HOST] self._entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) @@ -137,7 +147,7 @@ class FlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Dialog that informs the user that reauth is required.""" assert self._entry is not None @@ -168,7 +178,7 @@ class OptionsFlowHandler(OptionsFlow): async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Manage the options.""" errors = {} if user_input is not None: diff --git a/homeassistant/components/webostv/const.py b/homeassistant/components/webostv/const.py index 84675196d86..c20060cae91 100644 --- a/homeassistant/components/webostv/const.py +++ b/homeassistant/components/webostv/const.py @@ -1,4 +1,5 @@ """Constants used for LG webOS Smart TV.""" + import asyncio from aiowebostv import WebOsTvCommandError diff --git a/homeassistant/components/webostv/device_trigger.py b/homeassistant/components/webostv/device_trigger.py index c7e5701af02..0175df5d828 100644 --- a/homeassistant/components/webostv/device_trigger.py +++ b/homeassistant/components/webostv/device_trigger.py @@ -1,4 +1,5 @@ """Provides device automations for control of LG webOS Smart TV.""" + from __future__ import annotations import voluptuous as vol diff --git a/homeassistant/components/webostv/diagnostics.py b/homeassistant/components/webostv/diagnostics.py index 5d88d61aa9d..1657fb71d26 100644 --- a/homeassistant/components/webostv/diagnostics.py +++ b/homeassistant/components/webostv/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for LG webOS Smart TV.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/webostv/helpers.py b/homeassistant/components/webostv/helpers.py index b61e4af3e6b..edcfdcfed8b 100644 --- a/homeassistant/components/webostv/helpers.py +++ b/homeassistant/components/webostv/helpers.py @@ -1,4 +1,5 @@ """Helper functions for webOS Smart TV.""" + from __future__ import annotations from aiowebostv import WebOsClient diff --git a/homeassistant/components/webostv/icons.json b/homeassistant/components/webostv/icons.json new file mode 100644 index 00000000000..deb9729a99f --- /dev/null +++ b/homeassistant/components/webostv/icons.json @@ -0,0 +1,7 @@ +{ + "services": { + "button": "mdi:button-pointer", + "command": "mdi:console", + "select_sound_output": "mdi:volume-source" + } +} diff --git a/homeassistant/components/webostv/media_player.py b/homeassistant/components/webostv/media_player.py index aefb6e77444..647cf64ea8e 100644 --- a/homeassistant/components/webostv/media_player.py +++ b/homeassistant/components/webostv/media_player.py @@ -1,4 +1,5 @@ """Support for interface with an LG webOS Smart TV.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/webostv/notify.py b/homeassistant/components/webostv/notify.py index c4cefc3cffe..43320687ce8 100644 --- a/homeassistant/components/webostv/notify.py +++ b/homeassistant/components/webostv/notify.py @@ -1,4 +1,5 @@ """Support for LG WebOS TV notification service.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/webostv/trigger.py b/homeassistant/components/webostv/trigger.py index 4d237993f95..3290aa4a448 100644 --- a/homeassistant/components/webostv/trigger.py +++ b/homeassistant/components/webostv/trigger.py @@ -1,4 +1,5 @@ """webOS Smart TV trigger dispatcher.""" + from __future__ import annotations from typing import cast diff --git a/homeassistant/components/webostv/triggers/turn_on.py b/homeassistant/components/webostv/triggers/turn_on.py index 403219f1372..f2ecb8aa98d 100644 --- a/homeassistant/components/webostv/triggers/turn_on.py +++ b/homeassistant/components/webostv/triggers/turn_on.py @@ -1,4 +1,5 @@ """webOS Smart TV device turn on trigger.""" + from __future__ import annotations import voluptuous as vol diff --git a/homeassistant/components/websocket_api/__init__.py b/homeassistant/components/websocket_api/__init__.py index f7086cc81db..291b652ac09 100644 --- a/homeassistant/components/websocket_api/__init__.py +++ b/homeassistant/components/websocket_api/__init__.py @@ -1,4 +1,5 @@ """WebSocket based API for Home Assistant.""" + from __future__ import annotations from typing import Final, cast diff --git a/homeassistant/components/websocket_api/auth.py b/homeassistant/components/websocket_api/auth.py index 3940e1333d0..a15f76632c1 100644 --- a/homeassistant/components/websocket_api/auth.py +++ b/homeassistant/components/websocket_api/auth.py @@ -1,4 +1,5 @@ """Handle the auth of a connection.""" + from __future__ import annotations from collections.abc import Callable, Coroutine @@ -89,10 +90,10 @@ class AuthPhase: refresh_token.user, refresh_token, ) - conn.subscriptions[ - "auth" - ] = self._hass.auth.async_register_revoke_token_callback( - refresh_token.id, self._cancel_ws + conn.subscriptions["auth"] = ( + self._hass.auth.async_register_revoke_token_callback( + refresh_token.id, self._cancel_ws + ) ) await self._send_bytes_text(AUTH_OK_MESSAGE) self._logger.debug("Auth OK") diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index 368785c17bc..191ea1ea996 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -1,4 +1,5 @@ """Commands part of Websocket API.""" + from __future__ import annotations from collections.abc import Callable @@ -47,7 +48,6 @@ from homeassistant.helpers.json import ( json_bytes, ) from homeassistant.helpers.service import async_get_all_descriptions -from homeassistant.helpers.typing import EventType from homeassistant.loader import ( Integration, IntegrationNotFound, @@ -55,7 +55,7 @@ from homeassistant.loader import ( async_get_integration_descriptions, async_get_integrations, ) -from homeassistant.setup import DATA_SETUP_TIME, async_get_loaded_integrations +from homeassistant.setup import async_get_loaded_integrations, async_get_setup_timings from homeassistant.util.json import format_unserializable_data from . import const, decorators, messages @@ -367,7 +367,7 @@ def _forward_entity_changes( entity_ids: set[str], user: User, msg_id: int, - event: Event, + event: Event[EventStateChangedData], ) -> None: """Forward entity state changed events to websocket.""" entity_id = event.data["entity_id"] @@ -539,12 +539,11 @@ def handle_integration_setup_info( hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] ) -> None: """Handle integrations command.""" - setup_time: dict[str, float] = hass.data[DATA_SETUP_TIME] connection.send_result( msg["id"], [ {"domain": integration, "seconds": seconds} - for integration, seconds in setup_time.items() + for integration, seconds in async_get_setup_timings(hass).items() ], ) @@ -621,7 +620,7 @@ async def handle_render_template( @callback def _template_listener( - event: EventType[EventStateChangedData] | None, + event: Event[EventStateChangedData] | None, updates: list[TrackTemplateResult], ) -> None: track_template_result = updates.pop() diff --git a/homeassistant/components/websocket_api/connection.py b/homeassistant/components/websocket_api/connection.py index aa7bcefadae..63b4418a19d 100644 --- a/homeassistant/components/websocket_api/connection.py +++ b/homeassistant/components/websocket_api/connection.py @@ -1,4 +1,5 @@ """Connection session.""" + from __future__ import annotations from collections.abc import Callable, Hashable diff --git a/homeassistant/components/websocket_api/const.py b/homeassistant/components/websocket_api/const.py index 9a44f80a5c8..25d3ff8dcb3 100644 --- a/homeassistant/components/websocket_api/const.py +++ b/homeassistant/components/websocket_api/const.py @@ -1,4 +1,5 @@ """Websocket constants.""" + from __future__ import annotations from collections.abc import Awaitable, Callable @@ -7,7 +8,7 @@ from typing import TYPE_CHECKING, Any, Final from homeassistant.core import HomeAssistant if TYPE_CHECKING: - from .connection import ActiveConnection # noqa: F401 + from .connection import ActiveConnection WebSocketCommandHandler = Callable[ diff --git a/homeassistant/components/websocket_api/decorators.py b/homeassistant/components/websocket_api/decorators.py index b4c72d497cd..51643752a0f 100644 --- a/homeassistant/components/websocket_api/decorators.py +++ b/homeassistant/components/websocket_api/decorators.py @@ -1,4 +1,5 @@ """Decorators for the Websocket API.""" + from __future__ import annotations from collections.abc import Callable @@ -62,7 +63,7 @@ def require_admin(func: const.WebSocketCommandHandler) -> const.WebSocketCommand user = connection.user if user is None or not user.is_admin: - raise Unauthorized() + raise Unauthorized func(hass, connection, msg) diff --git a/homeassistant/components/websocket_api/error.py b/homeassistant/components/websocket_api/error.py index 5d4ca93105d..4ed80adb365 100644 --- a/homeassistant/components/websocket_api/error.py +++ b/homeassistant/components/websocket_api/error.py @@ -1,4 +1,5 @@ """WebSocket API related errors.""" + from homeassistant.exceptions import HomeAssistantError diff --git a/homeassistant/components/websocket_api/http.py b/homeassistant/components/websocket_api/http.py index 82c54a08136..83d68ee21ea 100644 --- a/homeassistant/components/websocket_api/http.py +++ b/homeassistant/components/websocket_api/http.py @@ -1,4 +1,5 @@ """View to accept incoming websocket connection.""" + from __future__ import annotations import asyncio @@ -11,7 +12,7 @@ from typing import TYPE_CHECKING, Any, Final from aiohttp import WSMsgType, web -from homeassistant.components.http import HomeAssistantView +from homeassistant.components.http import KEY_HASS, HomeAssistantView from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import Event, HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -49,7 +50,7 @@ class WebsocketAPIView(HomeAssistantView): async def get(self, request: web.Request) -> web.WebSocketResponse: """Handle an incoming websocket connection.""" - return await WebSocketHandler(request.app["hass"], request).async_handle() + return await WebSocketHandler(request.app[KEY_HASS], request).async_handle() class WebSocketAdapter(logging.LoggerAdapter): @@ -291,7 +292,7 @@ class WebSocketHandler: self._handle_task = asyncio.current_task() unsub_stop = hass.bus.async_listen( - EVENT_HOMEASSISTANT_STOP, self._async_handle_hass_stop + EVENT_HOMEASSISTANT_STOP, self._async_handle_hass_stop, run_immediately=True ) writer = wsock._writer # pylint: disable=protected-access diff --git a/homeassistant/components/websocket_api/messages.py b/homeassistant/components/websocket_api/messages.py index 3916cdd3af7..8de43c57f00 100644 --- a/homeassistant/components/websocket_api/messages.py +++ b/homeassistant/components/websocket_api/messages.py @@ -1,9 +1,10 @@ """Message templates for websocket commands.""" + from __future__ import annotations from functools import lru_cache import logging -from typing import TYPE_CHECKING, Any, Final, cast +from typing import Any, Final import voluptuous as vol @@ -16,6 +17,7 @@ from homeassistant.const import ( ) from homeassistant.core import Event, State from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.event import EventStateChangedData from homeassistant.helpers.json import ( JSON_DUMP, find_paths_unserializable_data, @@ -140,7 +142,7 @@ def _partial_cached_event_message(event: Event) -> bytes: ) -def cached_state_diff_message(iden: int, event: Event) -> bytes: +def cached_state_diff_message(iden: int, event: Event[EventStateChangedData]) -> bytes: """Return an event message. Serialize to json once per message. @@ -160,7 +162,7 @@ def cached_state_diff_message(iden: int, event: Event) -> bytes: @lru_cache(maxsize=128) -def _partial_cached_state_diff_message(event: Event) -> bytes: +def _partial_cached_state_diff_message(event: Event[EventStateChangedData]) -> bytes: """Cache and serialize the event to json. The message is constructed without the id which @@ -174,7 +176,7 @@ def _partial_cached_state_diff_message(event: Event) -> bytes: ) -def _state_diff_event(event: Event) -> dict: +def _state_diff_event(event: Event[EventStateChangedData]) -> dict: """Convert a state_changed event to the minimal version. State update example @@ -187,16 +189,12 @@ def _state_diff_event(event: Event) -> dict: """ if (event_new_state := event.data["new_state"]) is None: return {ENTITY_EVENT_REMOVE: [event.data["entity_id"]]} - if TYPE_CHECKING: - event_new_state = cast(State, event_new_state) if (event_old_state := event.data["old_state"]) is None: return { ENTITY_EVENT_ADD: { event_new_state.entity_id: event_new_state.as_compressed_state } } - if TYPE_CHECKING: - event_old_state = cast(State, event_old_state) return _state_diff(event_old_state, event_new_state) diff --git a/homeassistant/components/websocket_api/sensor.py b/homeassistant/components/websocket_api/sensor.py index 5857ead2c11..7d668466bc2 100644 --- a/homeassistant/components/websocket_api/sensor.py +++ b/homeassistant/components/websocket_api/sensor.py @@ -1,4 +1,5 @@ """Entity to track connections to websocket API.""" + from __future__ import annotations from homeassistant.components.sensor import SensorEntity diff --git a/homeassistant/components/wemo/__init__.py b/homeassistant/components/wemo/__init__.py index 2f4d4c84c5c..8a9a122c03c 100644 --- a/homeassistant/components/wemo/__init__.py +++ b/homeassistant/components/wemo/__init__.py @@ -1,4 +1,5 @@ """Support for WeMo device discovery.""" + from __future__ import annotations from collections.abc import Callable, Coroutine, Sequence @@ -91,11 +92,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: discovery_responder = pywemo.ssdp.DiscoveryResponder(registry.port) await hass.async_add_executor_job(discovery_responder.start) - async def _on_hass_stop(_: Event) -> None: - await hass.async_add_executor_job(discovery_responder.stop) - await hass.async_add_executor_job(registry.stop) + def _on_hass_stop(_: Event) -> None: + discovery_responder.stop() + registry.stop() - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _on_hass_stop) + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, _on_hass_stop, run_immediately=True + ) yaml_config = config.get(DOMAIN, {}) hass.data[DOMAIN] = WemoData( @@ -118,7 +121,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a wemo config entry.""" wemo_data = async_wemo_data(hass) dispatcher = WemoDispatcher(entry) - discovery = WemoDiscovery(hass, dispatcher, wemo_data.static_config) + discovery = WemoDiscovery(hass, dispatcher, wemo_data.static_config, entry) wemo_data.config_entry_data = WemoConfigEntryData( device_coordinators={}, discovery=discovery, @@ -190,6 +193,7 @@ class WemoDispatcher: platforms = set(WEMO_MODEL_DISPATCH.get(wemo.model_name, [Platform.SWITCH])) platforms.add(Platform.SENSOR) + platforms_to_load: list[Platform] = [] for platform in platforms: # Three cases: # - Platform is loaded, dispatch discovery @@ -202,11 +206,14 @@ class WemoDispatcher: self._dispatch_backlog[platform].append(coordinator) else: self._dispatch_backlog[platform] = [coordinator] - hass.async_create_task( - hass.config_entries.async_forward_entry_setup( - self._config_entry, platform - ) + platforms_to_load.append(platform) + + if platforms_to_load: + hass.async_create_task( + hass.config_entries.async_forward_entry_setups( + self._config_entry, platforms_to_load ) + ) self._added_serial_numbers.add(wemo.serial_number) self._failed_serial_numbers.discard(wemo.serial_number) @@ -245,6 +252,7 @@ class WemoDiscovery: hass: HomeAssistant, wemo_dispatcher: WemoDispatcher, static_config: Sequence[HostPortTuple], + entry: ConfigEntry, ) -> None: """Initialize the WemoDiscovery.""" self._hass = hass @@ -252,7 +260,8 @@ class WemoDiscovery: self._stop: CALLBACK_TYPE | None = None self._scan_delay = 0 self._static_config = static_config - self._discover_job: HassJob[[datetime], Coroutine[Any, Any, None]] | None = None + self._discover_job: HassJob[[datetime], None] | None = None + self._entry = entry async def async_discover_and_schedule( self, event_time: datetime | None = None @@ -273,13 +282,23 @@ class WemoDiscovery: self.MAX_SECONDS_BETWEEN_SCANS, ) if not self._discover_job: - self._discover_job = HassJob(self.async_discover_and_schedule) + self._discover_job = HassJob(self._async_discover_and_schedule_callback) self._stop = async_call_later( self._hass, self._scan_delay, self._discover_job, ) + @callback + def _async_discover_and_schedule_callback(self, event_time: datetime) -> None: + """Run the periodic background scanning.""" + self._entry.async_create_background_task( + self._hass, + self.async_discover_and_schedule(), + name="wemo_discovery", + eager_start=True, + ) + @callback def async_stop_discovery(self) -> None: """Stop the periodic background scanning.""" diff --git a/homeassistant/components/wemo/config_flow.py b/homeassistant/components/wemo/config_flow.py index e4626b4deaf..97a9eb34057 100644 --- a/homeassistant/components/wemo/config_flow.py +++ b/homeassistant/components/wemo/config_flow.py @@ -8,9 +8,8 @@ from typing import Any, get_type_hints import pywemo import voluptuous as vol -from homeassistant.config_entries import ConfigEntry, OptionsFlow +from homeassistant.config_entries import ConfigEntry, ConfigFlowResult, OptionsFlow from homeassistant.core import HomeAssistant, callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.config_entry_flow import DiscoveryFlowHandler from .const import DOMAIN @@ -45,7 +44,7 @@ class WemoOptionsFlow(OptionsFlow): async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Manage options for the WeMo component.""" errors: dict[str, str] | None = None if user_input is not None: diff --git a/homeassistant/components/wemo/const.py b/homeassistant/components/wemo/const.py index ec59e713b0d..0bde98acb24 100644 --- a/homeassistant/components/wemo/const.py +++ b/homeassistant/components/wemo/const.py @@ -1,4 +1,5 @@ """Constants for the Belkin Wemo component.""" + DOMAIN = "wemo" SERVICE_SET_HUMIDITY = "set_humidity" diff --git a/homeassistant/components/wemo/device_trigger.py b/homeassistant/components/wemo/device_trigger.py index 077c32fb1ad..d9cadcdd576 100644 --- a/homeassistant/components/wemo/device_trigger.py +++ b/homeassistant/components/wemo/device_trigger.py @@ -1,4 +1,5 @@ """Triggers for WeMo devices.""" + from __future__ import annotations from pywemo.subscribe import EVENT_TYPE_LONG_PRESS diff --git a/homeassistant/components/wemo/entity.py b/homeassistant/components/wemo/entity.py index cbb2f31c79d..a6fe677d357 100644 --- a/homeassistant/components/wemo/entity.py +++ b/homeassistant/components/wemo/entity.py @@ -1,4 +1,5 @@ """Classes shared among Wemo entities.""" + from __future__ import annotations from collections.abc import Generator diff --git a/homeassistant/components/wemo/fan.py b/homeassistant/components/wemo/fan.py index 39abdba6e82..89b20bdde25 100644 --- a/homeassistant/components/wemo/fan.py +++ b/homeassistant/components/wemo/fan.py @@ -1,4 +1,5 @@ """Support for WeMo humidifier.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/wemo/icons.json b/homeassistant/components/wemo/icons.json new file mode 100644 index 00000000000..c5ddf5912d6 --- /dev/null +++ b/homeassistant/components/wemo/icons.json @@ -0,0 +1,6 @@ +{ + "services": { + "set_humidity": "mdi:water-percent", + "reset_filter_life": "mdi:refresh" + } +} diff --git a/homeassistant/components/wemo/light.py b/homeassistant/components/wemo/light.py index 0205a10521d..00c5204eba9 100644 --- a/homeassistant/components/wemo/light.py +++ b/homeassistant/components/wemo/light.py @@ -1,4 +1,5 @@ """Support for Belkin WeMo lights.""" + from __future__ import annotations from typing import Any, cast diff --git a/homeassistant/components/wemo/manifest.json b/homeassistant/components/wemo/manifest.json index fa0b618b3f9..71a1eac62a8 100644 --- a/homeassistant/components/wemo/manifest.json +++ b/homeassistant/components/wemo/manifest.json @@ -7,7 +7,6 @@ "homekit": { "models": ["Socket", "Wemo"] }, - "import_executor": true, "iot_class": "local_push", "loggers": ["pywemo"], "requirements": ["pywemo==1.4.0"], diff --git a/homeassistant/components/wemo/sensor.py b/homeassistant/components/wemo/sensor.py index ecb0c16055c..555e2591832 100644 --- a/homeassistant/components/wemo/sensor.py +++ b/homeassistant/components/wemo/sensor.py @@ -1,4 +1,5 @@ """Support for power sensors in WeMo Insight devices.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/wemo/switch.py b/homeassistant/components/wemo/switch.py index 508621ba415..14e3013afc1 100644 --- a/homeassistant/components/wemo/switch.py +++ b/homeassistant/components/wemo/switch.py @@ -1,4 +1,5 @@ """Support for WeMo switches.""" + from __future__ import annotations from datetime import datetime, timedelta @@ -89,8 +90,9 @@ class WemoSwitch(WemoBinaryStateEntity, SwitchEntity): def as_uptime(_seconds: int) -> str: """Format seconds into uptime string in the format: 00d 00h 00m 00s.""" uptime = datetime(1, 1, 1) + timedelta(seconds=_seconds) - return "{:0>2d}d {:0>2d}h {:0>2d}m {:0>2d}s".format( - uptime.day - 1, uptime.hour, uptime.minute, uptime.second + return ( + f"{uptime.day - 1:0>2d}d {uptime.hour:0>2d}h " + f"{uptime.minute:0>2d}m {uptime.second:0>2d}s" ) @property diff --git a/homeassistant/components/wemo/wemo_device.py b/homeassistant/components/wemo/wemo_device.py index a54610e9a8b..2a4185a7640 100644 --- a/homeassistant/components/wemo/wemo_device.py +++ b/homeassistant/components/wemo/wemo_device.py @@ -1,11 +1,12 @@ """Home Assistant wrapper for a pyWeMo device.""" + from __future__ import annotations import asyncio from dataclasses import dataclass, fields from datetime import timedelta import logging -from typing import Literal +from typing import TYPE_CHECKING, Literal from pywemo import Insight, LongPressMixin, WeMoDevice from pywemo.exceptions import ActionException, PyWeMoException @@ -35,7 +36,7 @@ from .models import async_wemo_data _LOGGER = logging.getLogger(__name__) # Literal values must match options.error keys from strings.json. -ErrorStringKey = Literal["long_press_requires_subscription"] # noqa: F821 +ErrorStringKey = Literal["long_press_requires_subscription"] # Literal values must match options.step.init.data keys from strings.json. OptionsFieldKey = Literal["enable_subscription", "enable_long_press"] @@ -91,7 +92,7 @@ class DeviceCoordinator(DataUpdateCoordinator[None]): # pylint: disable=hass-en options: Options | None = None - def __init__(self, hass: HomeAssistant, wemo: WeMoDevice, device_id: str) -> None: + def __init__(self, hass: HomeAssistant, wemo: WeMoDevice) -> None: """Initialize DeviceCoordinator.""" super().__init__( hass, @@ -101,11 +102,16 @@ class DeviceCoordinator(DataUpdateCoordinator[None]): # pylint: disable=hass-en ) self.hass = hass self.wemo = wemo - self.device_id = device_id + self.device_id: str | None = None self.device_info = _create_device_info(wemo) self.supports_long_press = isinstance(wemo, LongPressMixin) self.update_lock = asyncio.Lock() + @callback + def async_setup(self, device_id: str) -> None: + """Set up the device coordinator.""" + self.device_id = device_id + def subscription_callback( self, _device: WeMoDevice, event_type: str, params: str ) -> None: @@ -129,6 +135,9 @@ class DeviceCoordinator(DataUpdateCoordinator[None]): # pylint: disable=hass-en async def async_shutdown(self) -> None: """Unregister push subscriptions and remove from coordinators dict.""" await super().async_shutdown() + if TYPE_CHECKING: + # mypy doesn't known that the device_id is set in async_setup. + assert self.device_id is not None del _async_coordinators(self.hass)[self.device_id] assert self.options # Always set by async_register_device. if self.options.enable_subscription: @@ -221,7 +230,10 @@ class DeviceCoordinator(DataUpdateCoordinator[None]): # pylint: disable=hass-en async def _async_update_data(self) -> None: """Update WeMo state.""" # No need to poll if the device will push updates. - if not self.should_poll: + # The device_id will not be set until after the first + # update so we should not check should_poll until after + # the device_id is set. + if self.device_id and not self.should_poll: return # If an update is in progress, we don't do anything. @@ -244,7 +256,7 @@ class DeviceCoordinator(DataUpdateCoordinator[None]): # pylint: disable=hass-en def _create_device_info(wemo: WeMoDevice) -> DeviceInfo: """Create device information. Modify if special device.""" _dev_info = _device_info(wemo) - if wemo.model_name == "DLI emulated Belkin Socket": + if wemo.model_name.lower() == "dli emulated belkin socket": _dev_info[ATTR_CONFIGURATION_URL] = f"http://{wemo.host}" _dev_info[ATTR_IDENTIFIERS] = {(DOMAIN, wemo.serial_number[:-1])} return _dev_info @@ -265,15 +277,15 @@ async def async_register_device( hass: HomeAssistant, config_entry: ConfigEntry, wemo: WeMoDevice ) -> DeviceCoordinator: """Register a device with home assistant and enable pywemo event callbacks.""" - # Ensure proper communication with the device and get the initial state. - await hass.async_add_executor_job(wemo.get_state, True) - + device = DeviceCoordinator(hass, wemo) + await device.async_refresh() + if not device.last_update_success and device.last_exception: + raise device.last_exception device_registry = async_get_device_registry(hass) entry = device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, **_create_device_info(wemo) ) - - device = DeviceCoordinator(hass, wemo, entry.id) + device.async_setup(device_id=entry.id) _async_coordinators(hass)[entry.id] = device config_entry.async_on_unload( diff --git a/homeassistant/components/whirlpool/__init__.py b/homeassistant/components/whirlpool/__init__.py index 10b26801c10..36f8fbec59d 100644 --- a/homeassistant/components/whirlpool/__init__.py +++ b/homeassistant/components/whirlpool/__init__.py @@ -1,4 +1,5 @@ """The Whirlpool Appliances integration.""" + from dataclasses import dataclass import logging @@ -13,8 +14,7 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import CONF_REGIONS_MAP, DOMAIN -from .util import get_brand_for_region +from .const import CONF_BRAND, CONF_BRANDS_MAP, CONF_REGIONS_MAP, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -27,8 +27,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: session = async_get_clientsession(hass) region = CONF_REGIONS_MAP[entry.data.get(CONF_REGION, "EU")] - brand = get_brand_for_region(region) + brand = CONF_BRANDS_MAP[entry.data.get(CONF_BRAND, "Whirlpool")] backend_selector = BackendSelector(brand, region) + auth = Auth( backend_selector, entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD], session ) diff --git a/homeassistant/components/whirlpool/climate.py b/homeassistant/components/whirlpool/climate.py index 48b9b99c1e2..aa399746006 100644 --- a/homeassistant/components/whirlpool/climate.py +++ b/homeassistant/components/whirlpool/climate.py @@ -1,4 +1,5 @@ """Platform for climate integration.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/whirlpool/config_flow.py b/homeassistant/components/whirlpool/config_flow.py index dbd3f9b6fd4..5e1cb102d77 100644 --- a/homeassistant/components/whirlpool/config_flow.py +++ b/homeassistant/components/whirlpool/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Whirlpool Appliances integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -11,13 +12,13 @@ from whirlpool.appliancesmanager import AppliancesManager from whirlpool.auth import Auth from whirlpool.backendselector import BackendSelector -from homeassistant import config_entries, core, exceptions +from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_PASSWORD, CONF_REGION, CONF_USERNAME -from homeassistant.data_entry_flow import FlowResult +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import CONF_REGIONS_MAP, DOMAIN -from .util import get_brand_for_region +from .const import CONF_BRAND, CONF_BRANDS_MAP, CONF_REGIONS_MAP, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -27,22 +28,26 @@ STEP_USER_DATA_SCHEMA = vol.Schema( vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str, vol.Required(CONF_REGION): vol.In(list(CONF_REGIONS_MAP)), + vol.Required(CONF_BRAND): vol.In(list(CONF_BRANDS_MAP)), } ) -REAUTH_SCHEMA = vol.Schema({vol.Required(CONF_PASSWORD): str}) +REAUTH_SCHEMA = vol.Schema( + { + vol.Required(CONF_PASSWORD): str, + vol.Required(CONF_BRAND): vol.In(list(CONF_BRANDS_MAP)), + } +) -async def validate_input( - hass: core.HomeAssistant, data: dict[str, str] -) -> dict[str, str]: +async def validate_input(hass: HomeAssistant, data: dict[str, str]) -> dict[str, str]: """Validate the user input allows us to connect. Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user. """ session = async_get_clientsession(hass) region = CONF_REGIONS_MAP[data[CONF_REGION]] - brand = get_brand_for_region(region) + brand = CONF_BRANDS_MAP[data[CONF_BRAND]] backend_selector = BackendSelector(brand, region) auth = Auth(backend_selector, data[CONF_USERNAME], data[CONF_PASSWORD], session) try: @@ -55,19 +60,22 @@ async def validate_input( appliances_manager = AppliancesManager(backend_selector, auth, session) await appliances_manager.fetch_appliances() - if appliances_manager.aircons is None and appliances_manager.washer_dryers is None: + + if not appliances_manager.aircons and not appliances_manager.washer_dryers: raise NoAppliances return {"title": data[CONF_USERNAME]} -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class WhirlpoolConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Whirlpool Sixth Sense.""" VERSION = 1 - entry: config_entries.ConfigEntry | None + entry: ConfigEntry | None - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Handle re-authentication with Whirlpool Sixth Sense.""" self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) @@ -75,17 +83,15 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm re-authentication with Whirlpool Sixth Sense.""" errors: dict[str, str] = {} if user_input: assert self.entry is not None password = user_input[CONF_PASSWORD] - data = { - **self.entry.data, - CONF_PASSWORD: password, - } + brand = user_input[CONF_BRAND] + data = {**self.entry.data, CONF_PASSWORD: password, CONF_BRAND: brand} try: await validate_input(self.hass, data) @@ -94,13 +100,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): except (CannotConnect, TimeoutError): errors["base"] = "cannot_connect" else: - self.hass.config_entries.async_update_entry( - self.entry, - data={ - **self.entry.data, - CONF_PASSWORD: password, - }, - ) + self.hass.config_entries.async_update_entry(self.entry, data=data) await self.hass.config_entries.async_reload(self.entry.entry_id) return self.async_abort(reason="reauth_successful") @@ -110,7 +110,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_user(self, user_input=None) -> FlowResult: + async def async_step_user(self, user_input=None) -> ConfigFlowResult: """Handle the initial step.""" if user_input is None: return self.async_show_form( @@ -142,13 +142,13 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -class CannotConnect(exceptions.HomeAssistantError): +class CannotConnect(HomeAssistantError): """Error to indicate we cannot connect.""" -class InvalidAuth(exceptions.HomeAssistantError): +class InvalidAuth(HomeAssistantError): """Error to indicate there is invalid auth.""" -class NoAppliances(exceptions.HomeAssistantError): +class NoAppliances(HomeAssistantError): """Error to indicate no supported appliances in the user account.""" diff --git a/homeassistant/components/whirlpool/const.py b/homeassistant/components/whirlpool/const.py index b472c7f9156..63a58f54c1d 100644 --- a/homeassistant/components/whirlpool/const.py +++ b/homeassistant/components/whirlpool/const.py @@ -1,10 +1,17 @@ """Constants for the Whirlpool Appliances integration.""" -from whirlpool.backendselector import Region +from whirlpool.backendselector import Brand, Region DOMAIN = "whirlpool" +CONF_BRAND = "brand" CONF_REGIONS_MAP = { "EU": Region.EU, "US": Region.US, } + +CONF_BRANDS_MAP = { + "Whirlpool": Brand.Whirlpool, + "Maytag": Brand.Maytag, + "KitchenAid": Brand.KitchenAid, +} diff --git a/homeassistant/components/whirlpool/diagnostics.py b/homeassistant/components/whirlpool/diagnostics.py new file mode 100644 index 00000000000..9b1dd00e7bd --- /dev/null +++ b/homeassistant/components/whirlpool/diagnostics.py @@ -0,0 +1,49 @@ +"""Diagnostics support for Whirlpool.""" + +from __future__ import annotations + +from typing import Any + +from homeassistant.components.diagnostics import async_redact_data +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from . import WhirlpoolData +from .const import DOMAIN + +TO_REDACT = { + "SERIAL_NUMBER", + "macaddress", + "username", + "password", + "token", + "unique_id", + "SAID", +} + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, + config_entry: ConfigEntry, +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + + whirlpool: WhirlpoolData = hass.data[DOMAIN][config_entry.entry_id] + diagnostics_data = { + "Washer_dryers": { + wd["NAME"]: dict(wd.items()) + for wd in whirlpool.appliances_manager.washer_dryers + }, + "aircons": { + ac["NAME"]: dict(ac.items()) for ac in whirlpool.appliances_manager.aircons + }, + "ovens": { + oven["NAME"]: dict(oven.items()) + for oven in whirlpool.appliances_manager.ovens + }, + } + + return { + "config_entry": async_redact_data(config_entry.as_dict(), TO_REDACT), + "appliances": async_redact_data(diagnostics_data, TO_REDACT), + } diff --git a/homeassistant/components/whirlpool/manifest.json b/homeassistant/components/whirlpool/manifest.json index 4c3ce680323..ee7861588ed 100644 --- a/homeassistant/components/whirlpool/manifest.json +++ b/homeassistant/components/whirlpool/manifest.json @@ -7,5 +7,5 @@ "integration_type": "hub", "iot_class": "cloud_push", "loggers": ["whirlpool"], - "requirements": ["whirlpool-sixth-sense==0.18.4"] + "requirements": ["whirlpool-sixth-sense==0.18.7"] } diff --git a/homeassistant/components/whirlpool/sensor.py b/homeassistant/components/whirlpool/sensor.py index 227c0e9f653..8c74f01298e 100644 --- a/homeassistant/components/whirlpool/sensor.py +++ b/homeassistant/components/whirlpool/sensor.py @@ -1,4 +1,5 @@ """The Washer/Dryer Sensor for Whirlpool Appliances.""" + from __future__ import annotations from collections.abc import Callable @@ -86,23 +87,16 @@ def washer_state(washer: WasherDryer) -> str | None: if func(washer): return cycle_name - return MACHINE_STATE.get(machine_state, None) + return MACHINE_STATE.get(machine_state) -@dataclass(frozen=True) -class WhirlpoolSensorEntityDescriptionMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class WhirlpoolSensorEntityDescription(SensorEntityDescription): + """Describes Whirlpool Washer sensor entity.""" value_fn: Callable -@dataclass(frozen=True) -class WhirlpoolSensorEntityDescription( - SensorEntityDescription, WhirlpoolSensorEntityDescriptionMixin -): - """Describes Whirlpool Washer sensor entity.""" - - SENSORS: tuple[WhirlpoolSensorEntityDescription, ...] = ( WhirlpoolSensorEntityDescription( key="state", @@ -212,7 +206,7 @@ class WasherDryerClass(SensorEntity): self._wd.register_attr_callback(self.async_write_ha_state) async def async_will_remove_from_hass(self) -> None: - """Close Whrilpool Appliance sockets before removing.""" + """Close Whirlpool Appliance sockets before removing.""" self._wd.unregister_attr_callback(self.async_write_ha_state) @property diff --git a/homeassistant/components/whirlpool/strings.json b/homeassistant/components/whirlpool/strings.json index a24e42304d0..b1658947263 100644 --- a/homeassistant/components/whirlpool/strings.json +++ b/homeassistant/components/whirlpool/strings.json @@ -2,9 +2,27 @@ "config": { "step": { "user": { + "title": "Configure your Whirlpool account", "data": { "username": "[%key:common::config_flow::data::username%]", - "password": "[%key:common::config_flow::data::password%]" + "password": "[%key:common::config_flow::data::password%]", + "region": "Region", + "brand": "Brand" + }, + "data_description": { + "brand": "Please choose the brand of the mobile app you use, or the brand of the appliances in your account" + } + }, + "reauth_confirm": { + "title": "Correct your Whirlpool account credentials", + "description": "For 'brand', please choose the brand of the mobile app you use, or the brand of the appliances in your account", + "data": { + "password": "[%key:common::config_flow::data::password%]", + "region": "Region", + "brand": "Brand" + }, + "data_description": { + "brand": "Please choose the brand of the mobile app you use, or the brand of the appliances in your account" } } }, diff --git a/homeassistant/components/whirlpool/util.py b/homeassistant/components/whirlpool/util.py deleted file mode 100644 index 55b094f76ac..00000000000 --- a/homeassistant/components/whirlpool/util.py +++ /dev/null @@ -1,8 +0,0 @@ -"""Utility functions for the Whirlpool Sixth Sense integration.""" - -from whirlpool.backendselector import Brand, Region - - -def get_brand_for_region(region: Region) -> Brand: - """Get the correct brand for each region.""" - return Brand.Maytag if region == Region.US else Brand.Whirlpool diff --git a/homeassistant/components/whois/__init__.py b/homeassistant/components/whois/__init__.py index 746e83a2677..b9f5938d93b 100644 --- a/homeassistant/components/whois/__init__.py +++ b/homeassistant/components/whois/__init__.py @@ -1,4 +1,5 @@ """The Whois integration.""" + from __future__ import annotations from whois import Domain, query as whois_query diff --git a/homeassistant/components/whois/config_flow.py b/homeassistant/components/whois/config_flow.py index 640daaa314c..cb4326d996d 100644 --- a/homeassistant/components/whois/config_flow.py +++ b/homeassistant/components/whois/config_flow.py @@ -1,4 +1,5 @@ """Config flow to configure the Whois integration.""" + from __future__ import annotations from typing import Any @@ -12,9 +13,8 @@ from whois.exceptions import ( WhoisCommandFailed, ) -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_DOMAIN -from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN @@ -28,7 +28,7 @@ class WhoisFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" errors = {} diff --git a/homeassistant/components/whois/const.py b/homeassistant/components/whois/const.py index 3fbbd6ff3ab..f196053f48d 100644 --- a/homeassistant/components/whois/const.py +++ b/homeassistant/components/whois/const.py @@ -1,4 +1,5 @@ """Constants for the Whois integration.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/whois/diagnostics.py b/homeassistant/components/whois/diagnostics.py index 19b3822e55c..0f93461d8d8 100644 --- a/homeassistant/components/whois/diagnostics.py +++ b/homeassistant/components/whois/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for Whois.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/whois/icons.json b/homeassistant/components/whois/icons.json new file mode 100644 index 00000000000..459ae252138 --- /dev/null +++ b/homeassistant/components/whois/icons.json @@ -0,0 +1,24 @@ +{ + "entity": { + "sensor": { + "admin": { + "default": "mdi:account-star" + }, + "days_until_expiration": { + "default": "mdi:calendar-clock" + }, + "owner": { + "default": "mdi:account" + }, + "registrant": { + "default": "mdi:account-edit" + }, + "registrar": { + "default": "mdi:store" + }, + "reseller": { + "default": "mdi:store" + } + } + } +} diff --git a/homeassistant/components/whois/sensor.py b/homeassistant/components/whois/sensor.py index 7118701a868..fe193b16eea 100644 --- a/homeassistant/components/whois/sensor.py +++ b/homeassistant/components/whois/sensor.py @@ -1,4 +1,5 @@ """Get WHOIS information for a given host.""" + from __future__ import annotations from collections.abc import Callable @@ -61,7 +62,6 @@ SENSORS: tuple[WhoisSensorEntityDescription, ...] = ( WhoisSensorEntityDescription( key="admin", translation_key="admin", - icon="mdi:account-star", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, value_fn=lambda domain: getattr(domain, "admin", None), @@ -76,7 +76,6 @@ SENSORS: tuple[WhoisSensorEntityDescription, ...] = ( WhoisSensorEntityDescription( key="days_until_expiration", translation_key="days_until_expiration", - icon="mdi:calendar-clock", native_unit_of_measurement=UnitOfTime.DAYS, value_fn=_days_until_expiration, ), @@ -97,7 +96,6 @@ SENSORS: tuple[WhoisSensorEntityDescription, ...] = ( WhoisSensorEntityDescription( key="owner", translation_key="owner", - icon="mdi:account", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, value_fn=lambda domain: getattr(domain, "owner", None), @@ -105,7 +103,6 @@ SENSORS: tuple[WhoisSensorEntityDescription, ...] = ( WhoisSensorEntityDescription( key="registrant", translation_key="registrant", - icon="mdi:account-edit", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, value_fn=lambda domain: getattr(domain, "registrant", None), @@ -113,7 +110,6 @@ SENSORS: tuple[WhoisSensorEntityDescription, ...] = ( WhoisSensorEntityDescription( key="registrar", translation_key="registrar", - icon="mdi:store", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, value_fn=lambda domain: domain.registrar if domain.registrar else None, @@ -121,7 +117,6 @@ SENSORS: tuple[WhoisSensorEntityDescription, ...] = ( WhoisSensorEntityDescription( key="reseller", translation_key="reseller", - icon="mdi:store", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, value_fn=lambda domain: getattr(domain, "reseller", None), diff --git a/homeassistant/components/wiffi/__init__.py b/homeassistant/components/wiffi/__init__.py index 3a35ec1ed29..c465bc0d2ca 100644 --- a/homeassistant/components/wiffi/__init__.py +++ b/homeassistant/components/wiffi/__init__.py @@ -1,4 +1,5 @@ """Component for wiffi support.""" + from datetime import timedelta import errno import logging diff --git a/homeassistant/components/wiffi/binary_sensor.py b/homeassistant/components/wiffi/binary_sensor.py index cb1e1da41d8..23aebd122f2 100644 --- a/homeassistant/components/wiffi/binary_sensor.py +++ b/homeassistant/components/wiffi/binary_sensor.py @@ -1,4 +1,5 @@ """Binary sensor platform support for wiffi devices.""" + from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback diff --git a/homeassistant/components/wiffi/config_flow.py b/homeassistant/components/wiffi/config_flow.py index d6da03c2134..17262dd0276 100644 --- a/homeassistant/components/wiffi/config_flow.py +++ b/homeassistant/components/wiffi/config_flow.py @@ -2,6 +2,7 @@ Used by UI to setup a wiffi integration. """ + from __future__ import annotations import errno @@ -9,14 +10,14 @@ import errno import voluptuous as vol from wiffi import WiffiTcpServer -from homeassistant import config_entries +from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow from homeassistant.const import CONF_PORT, CONF_TIMEOUT from homeassistant.core import callback from .const import DEFAULT_PORT, DEFAULT_TIMEOUT, DOMAIN -class WiffiFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class WiffiFlowHandler(ConfigFlow, domain=DOMAIN): """Wiffi server setup config flow.""" VERSION = 1 @@ -24,7 +25,7 @@ class WiffiFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback def async_get_options_flow( - config_entry: config_entries.ConfigEntry, + config_entry: ConfigEntry, ) -> OptionsFlowHandler: """Create Wiffi server setup option flow.""" return OptionsFlowHandler(config_entry) @@ -67,10 +68,10 @@ class WiffiFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) -class OptionsFlowHandler(config_entries.OptionsFlow): +class OptionsFlowHandler(OptionsFlow): """Wiffi server setup option flow.""" - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + def __init__(self, config_entry: ConfigEntry) -> None: """Initialize options flow.""" self.config_entry = config_entry diff --git a/homeassistant/components/wiffi/sensor.py b/homeassistant/components/wiffi/sensor.py index e460a346bd7..7b64628085a 100644 --- a/homeassistant/components/wiffi/sensor.py +++ b/homeassistant/components/wiffi/sensor.py @@ -1,4 +1,5 @@ """Sensor platform support for wiffi devices.""" + from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, diff --git a/homeassistant/components/wilight/config_flow.py b/homeassistant/components/wilight/config_flow.py index b7df1932cab..52b3b426c20 100644 --- a/homeassistant/components/wilight/config_flow.py +++ b/homeassistant/components/wilight/config_flow.py @@ -1,12 +1,12 @@ """Config flow to configure WiLight.""" + from urllib.parse import urlparse import pywilight from homeassistant.components import ssdp -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST -from homeassistant.data_entry_flow import FlowResult from . import DOMAIN @@ -50,7 +50,9 @@ class WiLightFlowHandler(ConfigFlow, domain=DOMAIN): } return self.async_create_entry(title=self._title, data=data) - async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: + async def async_step_ssdp( + self, discovery_info: ssdp.SsdpServiceInfo + ) -> ConfigFlowResult: """Handle a discovered WiLight.""" # Filter out basic information if ( diff --git a/homeassistant/components/wilight/cover.py b/homeassistant/components/wilight/cover.py index aa50b79f139..4ae4692db40 100644 --- a/homeassistant/components/wilight/cover.py +++ b/homeassistant/components/wilight/cover.py @@ -1,4 +1,5 @@ """Support for WiLight Cover.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/wilight/fan.py b/homeassistant/components/wilight/fan.py index ba9a108f636..5c05575c4f8 100644 --- a/homeassistant/components/wilight/fan.py +++ b/homeassistant/components/wilight/fan.py @@ -1,4 +1,5 @@ """Support for WiLight Fan.""" + from __future__ import annotations from typing import Any @@ -55,7 +56,6 @@ class WiLightFan(WiLightDevice, FanEntity): """Representation of a WiLights fan.""" _attr_name = None - _attr_icon = "mdi:fan" _attr_speed_count = len(ORDERED_NAMED_FAN_SPEEDS) _attr_supported_features = FanEntityFeature.SET_SPEED | FanEntityFeature.DIRECTION diff --git a/homeassistant/components/wilight/icons.json b/homeassistant/components/wilight/icons.json new file mode 100644 index 00000000000..3c5d0112de1 --- /dev/null +++ b/homeassistant/components/wilight/icons.json @@ -0,0 +1,17 @@ +{ + "entity": { + "switch": { + "watering": { + "default": "mdi:water" + }, + "pause": { + "default": "mdi:pause-circle-outline" + } + } + }, + "services": { + "set_watering_time": "mdi:timer", + "set_pause_time": "mdi:timer-pause", + "set_trigger": "mdi:gesture-tap-button" + } +} diff --git a/homeassistant/components/wilight/light.py b/homeassistant/components/wilight/light.py index b17eac36f09..1a51ecd884e 100644 --- a/homeassistant/components/wilight/light.py +++ b/homeassistant/components/wilight/light.py @@ -1,4 +1,5 @@ """Support for WiLight lights.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/wilight/parent_device.py b/homeassistant/components/wilight/parent_device.py index 8091e78cc76..6e96274f0a4 100644 --- a/homeassistant/components/wilight/parent_device.py +++ b/homeassistant/components/wilight/parent_device.py @@ -1,4 +1,5 @@ """The WiLight Device integration.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/wilight/support.py b/homeassistant/components/wilight/support.py index 9470bf74c8a..39578618d50 100644 --- a/homeassistant/components/wilight/support.py +++ b/homeassistant/components/wilight/support.py @@ -1,4 +1,5 @@ """Support for config validation using voluptuous and Translate Trigger.""" + from __future__ import annotations import calendar diff --git a/homeassistant/components/wilight/switch.py b/homeassistant/components/wilight/switch.py index 334d750b1e1..94e39492626 100644 --- a/homeassistant/components/wilight/switch.py +++ b/homeassistant/components/wilight/switch.py @@ -1,4 +1,5 @@ """Support for WiLight switches.""" + from __future__ import annotations from typing import Any @@ -56,10 +57,6 @@ VALID_TRIGGER_INDEX = vol.All( DESC_WATERING = "watering" DESC_PAUSE = "pause" -# Icons of the valve switch entities -ICON_WATERING = "mdi:water" -ICON_PAUSE = "mdi:pause-circle-outline" - def entities_from_discovered_wilight(api_device: PyWiLightDevice) -> tuple[Any]: """Parse configuration and add WiLight switch entities.""" @@ -149,7 +146,6 @@ class WiLightValveSwitch(WiLightDevice, SwitchEntity): """Representation of a WiLights Valve switch.""" _attr_translation_key = "watering" - _attr_icon = ICON_WATERING @property def is_on(self) -> bool: @@ -266,7 +262,6 @@ class WiLightValvePauseSwitch(WiLightDevice, SwitchEntity): """Representation of a WiLights Valve Pause switch.""" _attr_translation_key = "pause" - _attr_icon = ICON_PAUSE @property def is_on(self) -> bool: diff --git a/homeassistant/components/wirelesstag/__init__.py b/homeassistant/components/wirelesstag/__init__.py index cfbdb6bdc92..78d22bb79d9 100644 --- a/homeassistant/components/wirelesstag/__init__.py +++ b/homeassistant/components/wirelesstag/__init__.py @@ -1,4 +1,5 @@ """Support for Wireless Sensor Tags.""" + import logging from requests.exceptions import ConnectTimeout, HTTPError diff --git a/homeassistant/components/wirelesstag/binary_sensor.py b/homeassistant/components/wirelesstag/binary_sensor.py index 64a1097bcab..85efab16e70 100644 --- a/homeassistant/components/wirelesstag/binary_sensor.py +++ b/homeassistant/components/wirelesstag/binary_sensor.py @@ -1,4 +1,5 @@ """Binary sensor support for Wireless Sensor Tags.""" + from __future__ import annotations import voluptuous as vol diff --git a/homeassistant/components/wirelesstag/sensor.py b/homeassistant/components/wirelesstag/sensor.py index 8ae20031723..0e88272a41c 100644 --- a/homeassistant/components/wirelesstag/sensor.py +++ b/homeassistant/components/wirelesstag/sensor.py @@ -1,4 +1,5 @@ """Sensor support for Wireless Sensor Tags platform.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/wirelesstag/switch.py b/homeassistant/components/wirelesstag/switch.py index 7f4008623b1..0eafea0699b 100644 --- a/homeassistant/components/wirelesstag/switch.py +++ b/homeassistant/components/wirelesstag/switch.py @@ -1,4 +1,5 @@ """Switch implementation for Wireless Sensor Tags (wirelesstag.net).""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/withings/__init__.py b/homeassistant/components/withings/__init__.py index f42fb7a57b9..c14fb465731 100644 --- a/homeassistant/components/withings/__init__.py +++ b/homeassistant/components/withings/__init__.py @@ -2,6 +2,7 @@ For more details about this platform, please refer to the documentation at """ + from __future__ import annotations import asyncio @@ -9,7 +10,7 @@ from collections.abc import Awaitable, Callable import contextlib from dataclasses import dataclass, field from datetime import timedelta -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, cast from aiohttp.hdrs import METH_POST from aiohttp.web import Request, Response @@ -192,82 +193,31 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {})[entry.entry_id] = withings_data - register_lock = asyncio.Lock() - webhooks_registered = False - - async def unregister_webhook( - _: Any, - ) -> None: - nonlocal webhooks_registered - async with register_lock: - LOGGER.debug( - "Unregister Withings webhook (%s)", entry.data[CONF_WEBHOOK_ID] - ) - webhook_unregister(hass, entry.data[CONF_WEBHOOK_ID]) - await async_unsubscribe_webhooks(client) - for coordinator in withings_data.coordinators: - coordinator.webhook_subscription_listener(False) - webhooks_registered = False - - async def register_webhook( - _: Any, - ) -> None: - nonlocal webhooks_registered - async with register_lock: - if webhooks_registered: - return - if cloud.async_active_subscription(hass): - webhook_url = await _async_cloudhook_generate_url(hass, entry) - else: - webhook_url = webhook_generate_url(hass, entry.data[CONF_WEBHOOK_ID]) - url = URL(webhook_url) - if url.scheme != "https" or url.port != 443: - LOGGER.warning( - "Webhook not registered - " - "https and port 443 is required to register the webhook" - ) - return - - webhook_name = "Withings" - if entry.title != DEFAULT_TITLE: - webhook_name = f"{DEFAULT_TITLE} {entry.title}" - - webhook_register( - hass, - DOMAIN, - webhook_name, - entry.data[CONF_WEBHOOK_ID], - get_webhook_handler(withings_data), - allowed_methods=[METH_POST], - ) - LOGGER.debug("Registered Withings webhook at hass: %s", webhook_url) - - await async_subscribe_webhooks(client, webhook_url) - for coordinator in withings_data.coordinators: - coordinator.webhook_subscription_listener(True) - LOGGER.debug("Registered Withings webhook at Withings: %s", webhook_url) - entry.async_on_unload( - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, unregister_webhook) - ) - webhooks_registered = True + webhook_manager = WithingsWebhookManager(hass, entry) async def manage_cloudhook(state: cloud.CloudConnectionState) -> None: LOGGER.debug("Cloudconnection state changed to %s", state) if state is cloud.CloudConnectionState.CLOUD_CONNECTED: - await register_webhook(None) + await webhook_manager.register_webhook(None) if state is cloud.CloudConnectionState.CLOUD_DISCONNECTED: - await unregister_webhook(None) - entry.async_on_unload(async_call_later(hass, 30, register_webhook)) + await webhook_manager.unregister_webhook(None) + entry.async_on_unload( + async_call_later(hass, 30, webhook_manager.register_webhook) + ) if cloud.async_active_subscription(hass): if cloud.async_is_connected(hass): - entry.async_on_unload(async_call_later(hass, 1, register_webhook)) + entry.async_on_unload( + async_call_later(hass, 1, webhook_manager.register_webhook) + ) entry.async_on_unload( cloud.async_listen_connection_change(hass, manage_cloudhook) ) else: - entry.async_on_unload(async_call_later(hass, 1, register_webhook)) + entry.async_on_unload( + async_call_later(hass, 1, webhook_manager.register_webhook) + ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) @@ -309,6 +259,85 @@ async def async_subscribe_webhooks(client: WithingsClient, webhook_url: str) -> await client.subscribe_notification(webhook_url, notification) +class WithingsWebhookManager: + """Manager that manages the Withings webhooks.""" + + _webhooks_registered = False + _register_lock = asyncio.Lock() + + def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: + """Initialize webhook manager.""" + self.hass = hass + self.entry = entry + + @property + def withings_data(self) -> WithingsData: + """Return Withings data.""" + return cast(WithingsData, self.hass.data[DOMAIN][self.entry.entry_id]) + + async def unregister_webhook( + self, + _: Any, + ) -> None: + """Unregister webhooks at Withings.""" + async with self._register_lock: + LOGGER.debug( + "Unregister Withings webhook (%s)", self.entry.data[CONF_WEBHOOK_ID] + ) + webhook_unregister(self.hass, self.entry.data[CONF_WEBHOOK_ID]) + await async_unsubscribe_webhooks(self.withings_data.client) + for coordinator in self.withings_data.coordinators: + coordinator.webhook_subscription_listener(False) + self._webhooks_registered = False + + async def register_webhook( + self, + _: Any, + ) -> None: + """Register webhooks at Withings.""" + async with self._register_lock: + if self._webhooks_registered: + return + if cloud.async_active_subscription(self.hass): + webhook_url = await _async_cloudhook_generate_url(self.hass, self.entry) + else: + webhook_url = webhook_generate_url( + self.hass, self.entry.data[CONF_WEBHOOK_ID] + ) + url = URL(webhook_url) + if url.scheme != "https" or url.port != 443: + LOGGER.warning( + "Webhook not registered - " + "https and port 443 is required to register the webhook" + ) + return + + webhook_name = "Withings" + if self.entry.title != DEFAULT_TITLE: + webhook_name = f"{DEFAULT_TITLE} {self.entry.title}" + + webhook_register( + self.hass, + DOMAIN, + webhook_name, + self.entry.data[CONF_WEBHOOK_ID], + get_webhook_handler(self.withings_data), + allowed_methods=[METH_POST], + ) + LOGGER.debug("Registered Withings webhook at hass: %s", webhook_url) + + await async_subscribe_webhooks(self.withings_data.client, webhook_url) + for coordinator in self.withings_data.coordinators: + coordinator.webhook_subscription_listener(True) + LOGGER.debug("Registered Withings webhook at Withings: %s", webhook_url) + self.entry.async_on_unload( + self.hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, self.unregister_webhook + ) + ) + self._webhooks_registered = True + + async def async_unsubscribe_webhooks(client: WithingsClient) -> None: """Unsubscribe to all Withings webhooks.""" current_webhooks = await client.list_notification_configurations() diff --git a/homeassistant/components/withings/binary_sensor.py b/homeassistant/components/withings/binary_sensor.py index 12583ba4758..89e2c3227ae 100644 --- a/homeassistant/components/withings/binary_sensor.py +++ b/homeassistant/components/withings/binary_sensor.py @@ -1,4 +1,5 @@ """Sensors flow for Withings.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/withings/calendar.py b/homeassistant/components/withings/calendar.py index 132f00936f3..3e543e8e9ef 100644 --- a/homeassistant/components/withings/calendar.py +++ b/homeassistant/components/withings/calendar.py @@ -1,4 +1,5 @@ """Calendar platform for Withings.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/withings/config_flow.py b/homeassistant/components/withings/config_flow.py index 31c40bf9791..1b92f23685f 100644 --- a/homeassistant/components/withings/config_flow.py +++ b/homeassistant/components/withings/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Withings.""" + from __future__ import annotations from collections.abc import Mapping @@ -8,9 +9,8 @@ from typing import Any from aiowithings import AuthScope from homeassistant.components.webhook import async_generate_id -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import ConfigEntry, ConfigFlowResult from homeassistant.const import CONF_TOKEN, CONF_WEBHOOK_ID -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_entry_oauth2_flow from .const import CONF_USE_WEBHOOK, DEFAULT_TITLE, DOMAIN @@ -44,7 +44,9 @@ class WithingsFlowHandler( ) } - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Perform reauth upon an API authentication error.""" self.reauth_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] @@ -53,13 +55,13 @@ class WithingsFlowHandler( async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm reauth dialog.""" 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[str, Any]) -> FlowResult: + async def async_oauth_create_entry(self, data: dict[str, Any]) -> ConfigFlowResult: """Create an entry for the flow, or update existing entry.""" user_id = str(data[CONF_TOKEN]["userid"]) if not self.reauth_entry: diff --git a/homeassistant/components/withings/const.py b/homeassistant/components/withings/const.py index a4a34375459..a87fc8bfe83 100644 --- a/homeassistant/components/withings/const.py +++ b/homeassistant/components/withings/const.py @@ -1,4 +1,5 @@ """Constants used by the Withings component.""" + import logging LOGGER = logging.getLogger(__package__) diff --git a/homeassistant/components/withings/coordinator.py b/homeassistant/components/withings/coordinator.py index 2639ccccf7d..19b362dfa0a 100644 --- a/homeassistant/components/withings/coordinator.py +++ b/homeassistant/components/withings/coordinator.py @@ -1,4 +1,5 @@ """Withings coordinator.""" + from abc import abstractmethod from datetime import date, datetime, timedelta from typing import TypeVar diff --git a/homeassistant/components/withings/diagnostics.py b/homeassistant/components/withings/diagnostics.py index 31c9ffef569..bc51036e6ec 100644 --- a/homeassistant/components/withings/diagnostics.py +++ b/homeassistant/components/withings/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for Withings.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/withings/entity.py b/homeassistant/components/withings/entity.py index 7f3e694533c..4c9b27c72fc 100644 --- a/homeassistant/components/withings/entity.py +++ b/homeassistant/components/withings/entity.py @@ -1,4 +1,5 @@ """Base entity for Withings.""" + from __future__ import annotations from typing import TypeVar diff --git a/homeassistant/components/withings/sensor.py b/homeassistant/components/withings/sensor.py index d882cd8cddd..a3862485da4 100644 --- a/homeassistant/components/withings/sensor.py +++ b/homeassistant/components/withings/sensor.py @@ -1,4 +1,5 @@ """Sensors flow for Withings.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/wiz/__init__.py b/homeassistant/components/wiz/__init__.py index 26d4e33a7a6..130cc73efd3 100644 --- a/homeassistant/components/wiz/__init__.py +++ b/homeassistant/components/wiz/__init__.py @@ -1,4 +1,5 @@ """WiZ Platform integration.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/wiz/binary_sensor.py b/homeassistant/components/wiz/binary_sensor.py index 6b3caf23a1c..b58e120a9dd 100644 --- a/homeassistant/components/wiz/binary_sensor.py +++ b/homeassistant/components/wiz/binary_sensor.py @@ -1,4 +1,5 @@ """WiZ integration binary sensor platform.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/wiz/config_flow.py b/homeassistant/components/wiz/config_flow.py index f2d109bd6bb..3220856b89d 100644 --- a/homeassistant/components/wiz/config_flow.py +++ b/homeassistant/components/wiz/config_flow.py @@ -1,4 +1,5 @@ """Config flow for WiZ Platform.""" + from __future__ import annotations import logging @@ -10,9 +11,9 @@ from pywizlight.exceptions import WizLightConnectionError, WizLightTimeOutError import voluptuous as vol from homeassistant.components import dhcp, onboarding -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST -from homeassistant.data_entry_flow import AbortFlow, FlowResult +from homeassistant.data_entry_flow import AbortFlow from homeassistant.util.network import is_ip_address from .const import DEFAULT_NAME, DISCOVER_SCAN_TIMEOUT, DOMAIN, WIZ_CONNECT_EXCEPTIONS @@ -36,7 +37,9 @@ class WizConfigFlow(ConfigFlow, domain=DOMAIN): """Initialize the config flow.""" self._discovered_devices: dict[str, DiscoveredBulb] = {} - async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: + async def async_step_dhcp( + self, discovery_info: dhcp.DhcpServiceInfo + ) -> ConfigFlowResult: """Handle discovery via dhcp.""" self._discovered_device = DiscoveredBulb( discovery_info.ip, discovery_info.macaddress @@ -45,14 +48,14 @@ class WizConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_integration_discovery( self, discovery_info: dict[str, str] - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle integration discovery.""" self._discovered_device = DiscoveredBulb( discovery_info["ip_address"], discovery_info["mac_address"] ) return await self._async_handle_discovery() - async def _async_handle_discovery(self) -> FlowResult: + async def _async_handle_discovery(self) -> ConfigFlowResult: """Handle any discovery.""" device = self._discovered_device _LOGGER.debug("Discovered device: %s", device) @@ -81,7 +84,7 @@ class WizConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_discovery_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm discovery.""" ip_address = self._discovered_device.ip_address if user_input is not None or not onboarding.async_is_onboarded(self.hass): @@ -104,7 +107,7 @@ class WizConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_pick_device( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the step to pick discovered device.""" if user_input is not None: device = self._discovered_devices[user_input[CONF_DEVICE]] @@ -146,7 +149,7 @@ class WizConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" errors = {} if user_input is not None: diff --git a/homeassistant/components/wiz/const.py b/homeassistant/components/wiz/const.py index 1aeb2ada580..78074a3d5fb 100644 --- a/homeassistant/components/wiz/const.py +++ b/homeassistant/components/wiz/const.py @@ -1,4 +1,5 @@ """Constants for the WiZ Platform integration.""" + from datetime import timedelta from pywizlight.exceptions import ( diff --git a/homeassistant/components/wiz/diagnostics.py b/homeassistant/components/wiz/diagnostics.py index 4fdf62b3c8c..5f617ebafe9 100644 --- a/homeassistant/components/wiz/diagnostics.py +++ b/homeassistant/components/wiz/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for WiZ.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/wiz/discovery.py b/homeassistant/components/wiz/discovery.py index 350ddfe278a..118ed20ff87 100644 --- a/homeassistant/components/wiz/discovery.py +++ b/homeassistant/components/wiz/discovery.py @@ -1,4 +1,5 @@ """The wiz integration discovery.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/wiz/entity.py b/homeassistant/components/wiz/entity.py index 87c3171d836..e7a95234e16 100644 --- a/homeassistant/components/wiz/entity.py +++ b/homeassistant/components/wiz/entity.py @@ -1,4 +1,5 @@ """WiZ integration entities.""" + from __future__ import annotations from abc import abstractmethod diff --git a/homeassistant/components/wiz/icons.json b/homeassistant/components/wiz/icons.json new file mode 100644 index 00000000000..896f28a8ef1 --- /dev/null +++ b/homeassistant/components/wiz/icons.json @@ -0,0 +1,12 @@ +{ + "entity": { + "number": { + "effect_speed": { + "default": "mdi:speedometer" + }, + "dual_head_ratio": { + "default": "mdi:floor-lamp-dual" + } + } + } +} diff --git a/homeassistant/components/wiz/light.py b/homeassistant/components/wiz/light.py index 3a6ba2a9f5b..aece184720d 100644 --- a/homeassistant/components/wiz/light.py +++ b/homeassistant/components/wiz/light.py @@ -1,4 +1,5 @@ """WiZ integration light platform.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/wiz/models.py b/homeassistant/components/wiz/models.py index 547ff830303..125a8cfa73b 100644 --- a/homeassistant/components/wiz/models.py +++ b/homeassistant/components/wiz/models.py @@ -1,4 +1,5 @@ """WiZ integration models.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/homeassistant/components/wiz/number.py b/homeassistant/components/wiz/number.py index 91436674d7f..46708ac001e 100644 --- a/homeassistant/components/wiz/number.py +++ b/homeassistant/components/wiz/number.py @@ -1,4 +1,5 @@ """Support for WiZ effect speed numbers.""" + from __future__ import annotations from collections.abc import Callable, Coroutine @@ -46,7 +47,6 @@ NUMBERS: tuple[WizNumberEntityDescription, ...] = ( native_min_value=10, native_max_value=200, native_step=1, - icon="mdi:speedometer", value_fn=lambda device: cast(int | None, device.state.get_speed()), set_value_fn=_async_set_speed, required_feature="effect", @@ -58,7 +58,6 @@ NUMBERS: tuple[WizNumberEntityDescription, ...] = ( native_min_value=0, native_max_value=100, native_step=1, - icon="mdi:floor-lamp-dual", value_fn=lambda device: cast(int | None, device.state.get_ratio()), set_value_fn=_async_set_ratio, required_feature="dual_head", diff --git a/homeassistant/components/wiz/sensor.py b/homeassistant/components/wiz/sensor.py index a66c37fabb5..aae443e60d0 100644 --- a/homeassistant/components/wiz/sensor.py +++ b/homeassistant/components/wiz/sensor.py @@ -1,4 +1,5 @@ """Support for WiZ sensors.""" + from __future__ import annotations from homeassistant.components.sensor import ( diff --git a/homeassistant/components/wiz/switch.py b/homeassistant/components/wiz/switch.py index 1bebaba7579..d94bf12da9f 100644 --- a/homeassistant/components/wiz/switch.py +++ b/homeassistant/components/wiz/switch.py @@ -1,4 +1,5 @@ """WiZ integration switch platform.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/wiz/utils.py b/homeassistant/components/wiz/utils.py index 278989b5b2b..4849e0fb22c 100644 --- a/homeassistant/components/wiz/utils.py +++ b/homeassistant/components/wiz/utils.py @@ -1,4 +1,5 @@ """WiZ utils.""" + from __future__ import annotations from pywizlight import BulbType diff --git a/homeassistant/components/wled/__init__.py b/homeassistant/components/wled/__init__.py index fe6ac5a1fc1..6f5bb25b162 100644 --- a/homeassistant/components/wled/__init__.py +++ b/homeassistant/components/wled/__init__.py @@ -1,4 +1,5 @@ """Support for WLED.""" + from __future__ import annotations from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/wled/binary_sensor.py b/homeassistant/components/wled/binary_sensor.py index 6191235f423..260c43c8ba0 100644 --- a/homeassistant/components/wled/binary_sensor.py +++ b/homeassistant/components/wled/binary_sensor.py @@ -1,4 +1,5 @@ """Support for WLED binary sensor.""" + from __future__ import annotations from homeassistant.components.binary_sensor import ( diff --git a/homeassistant/components/wled/button.py b/homeassistant/components/wled/button.py index 430ee067486..7d3047c7c35 100644 --- a/homeassistant/components/wled/button.py +++ b/homeassistant/components/wled/button.py @@ -1,4 +1,5 @@ """Support for WLED button.""" + from __future__ import annotations from homeassistant.components.button import ButtonDeviceClass, ButtonEntity diff --git a/homeassistant/components/wled/config_flow.py b/homeassistant/components/wled/config_flow.py index cbb78545e2b..c40753b686a 100644 --- a/homeassistant/components/wled/config_flow.py +++ b/homeassistant/components/wled/config_flow.py @@ -1,4 +1,5 @@ """Config flow to configure the WLED integration.""" + from __future__ import annotations from typing import Any @@ -7,10 +8,14 @@ import voluptuous as vol from wled import WLED, Device, WLEDConnectionError from homeassistant.components import onboarding, zeroconf -from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlowWithConfigEntry, +) from homeassistant.const import CONF_HOST, CONF_MAC from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import CONF_KEEP_MAIN_LIGHT, DEFAULT_KEEP_MAIN_LIGHT, DOMAIN @@ -31,7 +36,7 @@ class WLEDFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initiated by the user.""" errors = {} @@ -64,7 +69,7 @@ class WLEDFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_zeroconf( self, discovery_info: zeroconf.ZeroconfServiceInfo - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle zeroconf discovery.""" # Abort quick if the mac address is provided by discovery info if mac := discovery_info.properties.get(CONF_MAC): @@ -95,7 +100,7 @@ class WLEDFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_zeroconf_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initiated by zeroconf.""" if user_input is not None or not onboarding.async_is_onboarded(self.hass): return self.async_create_entry( @@ -117,16 +122,12 @@ class WLEDFlowHandler(ConfigFlow, domain=DOMAIN): return await wled.update() -class WLEDOptionsFlowHandler(OptionsFlow): +class WLEDOptionsFlowHandler(OptionsFlowWithConfigEntry): """Handle WLED options.""" - def __init__(self, config_entry: ConfigEntry) -> None: - """Initialize WLED options flow.""" - self.config_entry = config_entry - async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Manage WLED options.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) @@ -137,7 +138,7 @@ class WLEDOptionsFlowHandler(OptionsFlow): { vol.Optional( CONF_KEEP_MAIN_LIGHT, - default=self.config_entry.options.get( + default=self.options.get( CONF_KEEP_MAIN_LIGHT, DEFAULT_KEEP_MAIN_LIGHT ), ): bool, diff --git a/homeassistant/components/wled/const.py b/homeassistant/components/wled/const.py index cee9984a3f6..f698347537c 100644 --- a/homeassistant/components/wled/const.py +++ b/homeassistant/components/wled/const.py @@ -1,4 +1,5 @@ """Constants for the WLED integration.""" + from datetime import timedelta import logging diff --git a/homeassistant/components/wled/coordinator.py b/homeassistant/components/wled/coordinator.py index 6bbcb1747f0..f6219c63cb8 100644 --- a/homeassistant/components/wled/coordinator.py +++ b/homeassistant/components/wled/coordinator.py @@ -1,4 +1,5 @@ """DataUpdateCoordinator for WLED.""" + from __future__ import annotations from wled import WLED, Device as WLEDDevice, WLEDConnectionClosedError, WLEDError diff --git a/homeassistant/components/wled/diagnostics.py b/homeassistant/components/wled/diagnostics.py index d0b3de5eb6b..f1eed3fc0aa 100644 --- a/homeassistant/components/wled/diagnostics.py +++ b/homeassistant/components/wled/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for WLED.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/wled/helpers.py b/homeassistant/components/wled/helpers.py index b4b5ee4c892..ad9a02b38ca 100644 --- a/homeassistant/components/wled/helpers.py +++ b/homeassistant/components/wled/helpers.py @@ -1,4 +1,5 @@ """Helpers for WLED.""" + from __future__ import annotations from collections.abc import Callable, Coroutine diff --git a/homeassistant/components/wled/light.py b/homeassistant/components/wled/light.py index 4327261d4be..1e31f090c70 100644 --- a/homeassistant/components/wled/light.py +++ b/homeassistant/components/wled/light.py @@ -1,4 +1,5 @@ """Support for LED lights.""" + from __future__ import annotations from functools import partial diff --git a/homeassistant/components/wled/models.py b/homeassistant/components/wled/models.py index 81405190228..ac7103303cc 100644 --- a/homeassistant/components/wled/models.py +++ b/homeassistant/components/wled/models.py @@ -1,4 +1,5 @@ """Models for WLED.""" + from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/wled/number.py b/homeassistant/components/wled/number.py index fd734c07fbc..e6142c1cea6 100644 --- a/homeassistant/components/wled/number.py +++ b/homeassistant/components/wled/number.py @@ -1,4 +1,5 @@ """Support for LED numbers.""" + from __future__ import annotations from collections.abc import Callable @@ -139,7 +140,8 @@ def async_update_segments( # Process new segments, add them to Home Assistant for segment_id in segment_ids - current_ids: current_ids.add(segment_id) - for desc in NUMBERS: - new_entities.append(WLEDNumber(coordinator, segment_id, desc)) + new_entities.extend( + WLEDNumber(coordinator, segment_id, desc) for desc in NUMBERS + ) async_add_entities(new_entities) diff --git a/homeassistant/components/wled/select.py b/homeassistant/components/wled/select.py index 36aff0f4536..755cd5746e8 100644 --- a/homeassistant/components/wled/select.py +++ b/homeassistant/components/wled/select.py @@ -1,4 +1,5 @@ """Support for LED selects.""" + from __future__ import annotations from functools import partial diff --git a/homeassistant/components/wled/sensor.py b/homeassistant/components/wled/sensor.py index a2e052eacd9..daf5748021f 100644 --- a/homeassistant/components/wled/sensor.py +++ b/homeassistant/components/wled/sensor.py @@ -1,4 +1,5 @@ """Support for WLED sensors.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/wled/switch.py b/homeassistant/components/wled/switch.py index f42e1cc7f9f..a5e998ec548 100644 --- a/homeassistant/components/wled/switch.py +++ b/homeassistant/components/wled/switch.py @@ -1,4 +1,5 @@ """Support for WLED switches.""" + from __future__ import annotations from functools import partial diff --git a/homeassistant/components/wled/update.py b/homeassistant/components/wled/update.py index 954279366be..bde2986a841 100644 --- a/homeassistant/components/wled/update.py +++ b/homeassistant/components/wled/update.py @@ -1,4 +1,5 @@ """Support for WLED updates.""" + from __future__ import annotations from typing import Any, cast diff --git a/homeassistant/components/wolflink/__init__.py b/homeassistant/components/wolflink/__init__.py index a8c09fc664e..e1c23893f75 100644 --- a/homeassistant/components/wolflink/__init__.py +++ b/homeassistant/components/wolflink/__init__.py @@ -1,4 +1,5 @@ """The Wolf SmartSet Service integration.""" + from datetime import timedelta import logging diff --git a/homeassistant/components/wolflink/config_flow.py b/homeassistant/components/wolflink/config_flow.py index bffc742f202..bfa66648b4b 100644 --- a/homeassistant/components/wolflink/config_flow.py +++ b/homeassistant/components/wolflink/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Wolf SmartSet Service integration.""" + import logging from httpcore import ConnectError @@ -6,7 +7,7 @@ import voluptuous as vol from wolf_comm.token_auth import InvalidAuth from wolf_comm.wolf_client import WolfClient -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from .const import DEVICE_GATEWAY, DEVICE_ID, DEVICE_NAME, DOMAIN @@ -18,7 +19,7 @@ USER_SCHEMA = vol.Schema( ) -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class WolfLinkConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Wolf SmartSet Service.""" VERSION = 1 diff --git a/homeassistant/components/wolflink/sensor.py b/homeassistant/components/wolflink/sensor.py index 2a030f69171..3179a9ff6bd 100644 --- a/homeassistant/components/wolflink/sensor.py +++ b/homeassistant/components/wolflink/sensor.py @@ -1,4 +1,5 @@ """The Wolf SmartSet sensors.""" + from __future__ import annotations from wolf_comm.models import ( diff --git a/homeassistant/components/workday/__init__.py b/homeassistant/components/workday/__init__.py index edada92aef4..077a6710b8d 100644 --- a/homeassistant/components/workday/__init__.py +++ b/homeassistant/components/workday/__init__.py @@ -1,6 +1,9 @@ """Sensor to indicate whether the current day is a workday.""" + from __future__ import annotations +from functools import partial + from holidays import HolidayBase, country_holidays from homeassistant.config_entries import ConfigEntry @@ -12,7 +15,7 @@ from homeassistant.helpers.issue_registry import IssueSeverity, async_create_iss from .const import CONF_PROVINCE, DOMAIN, PLATFORMS -def _validate_country_and_province( +async def _async_validate_country_and_province( hass: HomeAssistant, entry: ConfigEntry, country: str | None, province: str | None ) -> None: """Validate country and province.""" @@ -20,7 +23,7 @@ def _validate_country_and_province( if not country: return try: - country_holidays(country) + await hass.async_add_executor_job(country_holidays, country) except NotImplementedError as ex: async_create_issue( hass, @@ -38,7 +41,9 @@ def _validate_country_and_province( if not province: return try: - country_holidays(country, subdiv=province) + await hass.async_add_executor_job( + partial(country_holidays, country, subdiv=province) + ) except NotImplementedError as ex: async_create_issue( hass, @@ -65,10 +70,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: country: str | None = entry.options.get(CONF_COUNTRY) province: str | None = entry.options.get(CONF_PROVINCE) - _validate_country_and_province(hass, entry, country, province) + await _async_validate_country_and_province(hass, entry, country, province) if country and CONF_LANGUAGE not in entry.options: - cls: HolidayBase = country_holidays(country, subdiv=province) + cls: HolidayBase = await hass.async_add_executor_job( + partial(country_holidays, country, subdiv=province) + ) default_language = cls.default_language new_options = entry.options.copy() new_options[CONF_LANGUAGE] = default_language diff --git a/homeassistant/components/workday/config_flow.py b/homeassistant/components/workday/config_flow.py index 9f7e829a244..a66a9c51588 100644 --- a/homeassistant/components/workday/config_flow.py +++ b/homeassistant/components/workday/config_flow.py @@ -1,4 +1,5 @@ """Adds config flow for Workday integration.""" + from __future__ import annotations from functools import partial @@ -10,11 +11,12 @@ import voluptuous as vol from homeassistant.config_entries import ( ConfigEntry, ConfigFlow, + ConfigFlowResult, OptionsFlowWithConfigEntry, ) from homeassistant.const import CONF_COUNTRY, CONF_LANGUAGE, CONF_NAME from homeassistant.core import callback -from homeassistant.data_entry_flow import AbortFlow, FlowResult +from homeassistant.data_entry_flow import AbortFlow from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.selector import ( CountrySelector, @@ -64,9 +66,7 @@ def add_province_and_language_to_schema( _country = country_holidays(country=country) if country_default_language := (_country.default_language): selectable_languages = _country.supported_languages - new_selectable_languages = [] - for lang in selectable_languages: - new_selectable_languages.append(lang[:2]) + new_selectable_languages = [lang[:2] for lang in selectable_languages] language_schema = { vol.Optional( CONF_LANGUAGE, default=country_default_language @@ -200,7 +200,7 @@ class WorkdayConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the user initial step.""" errors: dict[str, str] = {} @@ -228,7 +228,7 @@ class WorkdayConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_options( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle remaining flow.""" errors: dict[str, str] = {} if user_input is not None: @@ -290,7 +290,7 @@ class WorkdayOptionsFlowHandler(OptionsFlowWithConfigEntry): async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Manage Workday options.""" errors: dict[str, str] = {} diff --git a/homeassistant/components/workday/const.py b/homeassistant/components/workday/const.py index ad9375830dd..6a46f1e824b 100644 --- a/homeassistant/components/workday/const.py +++ b/homeassistant/components/workday/const.py @@ -1,4 +1,5 @@ """Add constants for Workday integration.""" + from __future__ import annotations import logging @@ -7,7 +8,7 @@ from homeassistant.const import WEEKDAYS, Platform LOGGER = logging.getLogger(__package__) -ALLOWED_DAYS = WEEKDAYS + ["holiday"] +ALLOWED_DAYS = [*WEEKDAYS, "holiday"] DOMAIN = "workday" PLATFORMS = [Platform.BINARY_SENSOR] diff --git a/homeassistant/components/workday/icons.json b/homeassistant/components/workday/icons.json new file mode 100644 index 00000000000..10d3c93a288 --- /dev/null +++ b/homeassistant/components/workday/icons.json @@ -0,0 +1,5 @@ +{ + "services": { + "check_date": "mdi:calendar-check" + } +} diff --git a/homeassistant/components/workday/manifest.json b/homeassistant/components/workday/manifest.json index 96a3b53797c..314f4c6bcf4 100644 --- a/homeassistant/components/workday/manifest.json +++ b/homeassistant/components/workday/manifest.json @@ -7,5 +7,5 @@ "iot_class": "local_polling", "loggers": ["holidays"], "quality_scale": "internal", - "requirements": ["holidays==0.44"] + "requirements": ["holidays==0.46"] } diff --git a/homeassistant/components/worldclock/sensor.py b/homeassistant/components/worldclock/sensor.py index e626add2534..9b2cb600ac1 100644 --- a/homeassistant/components/worldclock/sensor.py +++ b/homeassistant/components/worldclock/sensor.py @@ -1,4 +1,5 @@ """Support for showing the time in a different time zone.""" + from __future__ import annotations from datetime import tzinfo diff --git a/homeassistant/components/worldtidesinfo/sensor.py b/homeassistant/components/worldtidesinfo/sensor.py index 1a5c7ae39a2..a4d663cc184 100644 --- a/homeassistant/components/worldtidesinfo/sensor.py +++ b/homeassistant/components/worldtidesinfo/sensor.py @@ -1,4 +1,5 @@ """Support for the worldtides.info API.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/worxlandroid/sensor.py b/homeassistant/components/worxlandroid/sensor.py index 16073a3d862..10f40bea685 100644 --- a/homeassistant/components/worxlandroid/sensor.py +++ b/homeassistant/components/worxlandroid/sensor.py @@ -1,4 +1,5 @@ """Support for Worx Landroid mower.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/ws66i/__init__.py b/homeassistant/components/ws66i/__init__.py index 64f049aa9ac..1993f38e0ab 100644 --- a/homeassistant/components/ws66i/__init__.py +++ b/homeassistant/components/ws66i/__init__.py @@ -1,4 +1,5 @@ """The Soundavo WS66i 6-Zone Amplifier integration.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/ws66i/config_flow.py b/homeassistant/components/ws66i/config_flow.py index a7deb74eb3e..5692ffcb81b 100644 --- a/homeassistant/components/ws66i/config_flow.py +++ b/homeassistant/components/ws66i/config_flow.py @@ -1,4 +1,5 @@ """Config flow for WS66i 6-Zone Amplifier integration.""" + from __future__ import annotations import logging @@ -7,8 +8,10 @@ from typing import Any from pyws66i import WS66i, get_ws66i import voluptuous as vol -from homeassistant import config_entries, core, exceptions +from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow from homeassistant.const import CONF_IP_ADDRESS +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError from .const import ( CONF_SOURCE_1, @@ -40,7 +43,7 @@ DATA_SCHEMA = vol.Schema({vol.Required(CONF_IP_ADDRESS): str}) FIRST_ZONE = 11 -@core.callback +@callback def _sources_from_config(data): sources_config = { str(idx + 1): data.get(source) for idx, source in enumerate(SOURCES) @@ -70,7 +73,7 @@ def _verify_connection(ws66i: WS66i) -> bool: async def validate_input( - hass: core.HomeAssistant, input_data: dict[str, Any] + hass: HomeAssistant, input_data: dict[str, Any] ) -> dict[str, Any]: """Validate the user input. @@ -86,7 +89,7 @@ async def validate_input( return {CONF_IP_ADDRESS: input_data[CONF_IP_ADDRESS]} -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class WS66iConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for WS66i 6-Zone Amplifier.""" VERSION = 1 @@ -115,15 +118,15 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) @staticmethod - @core.callback + @callback def async_get_options_flow( - config_entry: config_entries.ConfigEntry, + config_entry: ConfigEntry, ) -> Ws66iOptionsFlowHandler: """Define the config flow to handle options.""" return Ws66iOptionsFlowHandler(config_entry) -@core.callback +@callback def _key_for_source(index, source, previous_sources): key = vol.Required( source, description={"suggested_value": previous_sources[str(index)]} @@ -132,10 +135,10 @@ def _key_for_source(index, source, previous_sources): return key -class Ws66iOptionsFlowHandler(config_entries.OptionsFlow): +class Ws66iOptionsFlowHandler(OptionsFlow): """Handle a WS66i options flow.""" - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + def __init__(self, config_entry: ConfigEntry) -> None: """Initialize.""" self.config_entry = config_entry @@ -160,5 +163,5 @@ class Ws66iOptionsFlowHandler(config_entries.OptionsFlow): ) -class CannotConnect(exceptions.HomeAssistantError): +class CannotConnect(HomeAssistantError): """Error to indicate we cannot connect.""" diff --git a/homeassistant/components/ws66i/const.py b/homeassistant/components/ws66i/const.py index f824d991c1d..f56a100d4fe 100644 --- a/homeassistant/components/ws66i/const.py +++ b/homeassistant/components/ws66i/const.py @@ -1,4 +1,5 @@ """Constants for the Soundavo WS66i 6-Zone Amplifier Media Player component.""" + from datetime import timedelta DOMAIN = "ws66i" diff --git a/homeassistant/components/ws66i/coordinator.py b/homeassistant/components/ws66i/coordinator.py index be8ae3aad38..013e4d02b15 100644 --- a/homeassistant/components/ws66i/coordinator.py +++ b/homeassistant/components/ws66i/coordinator.py @@ -1,4 +1,5 @@ """Coordinator for WS66i.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/ws66i/media_player.py b/homeassistant/components/ws66i/media_player.py index 7119002cbc4..a2cd7ba471b 100644 --- a/homeassistant/components/ws66i/media_player.py +++ b/homeassistant/components/ws66i/media_player.py @@ -1,4 +1,5 @@ """Support for interfacing with WS66i 6 zone home audio controller.""" + from pyws66i import WS66i, ZoneStatus from homeassistant.components.media_player import ( diff --git a/homeassistant/components/ws66i/models.py b/homeassistant/components/ws66i/models.py index 84f481b9a4a..3c46d071790 100644 --- a/homeassistant/components/ws66i/models.py +++ b/homeassistant/components/ws66i/models.py @@ -1,4 +1,5 @@ """The ws66i integration models.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/homeassistant/components/wsdot/sensor.py b/homeassistant/components/wsdot/sensor.py index d9df2b7abaf..14e21f79282 100644 --- a/homeassistant/components/wsdot/sensor.py +++ b/homeassistant/components/wsdot/sensor.py @@ -1,4 +1,5 @@ """Support for Washington State Department of Transportation (WSDOT) data.""" + from __future__ import annotations from datetime import datetime, timedelta, timezone diff --git a/homeassistant/components/wyoming/__init__.py b/homeassistant/components/wyoming/__init__.py index 88e490d6dc9..3ef71e2901b 100644 --- a/homeassistant/components/wyoming/__init__.py +++ b/homeassistant/components/wyoming/__init__.py @@ -1,4 +1,5 @@ """The Wyoming integration.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/wyoming/config_flow.py b/homeassistant/components/wyoming/config_flow.py index 528165712b5..8461d9e83ac 100644 --- a/homeassistant/components/wyoming/config_flow.py +++ b/homeassistant/components/wyoming/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Wyoming integration.""" + from __future__ import annotations import logging @@ -7,10 +8,9 @@ from urllib.parse import urlparse import voluptuous as vol -from homeassistant import config_entries from homeassistant.components import hassio, zeroconf +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT -from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN from .data import WyomingService @@ -25,7 +25,7 @@ STEP_USER_DATA_SCHEMA = vol.Schema( ) -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class WyomingConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Wyoming integration.""" VERSION = 1 @@ -36,7 +36,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" if user_input is None: return self.async_show_form( @@ -62,7 +62,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_hassio( self, discovery_info: hassio.HassioServiceInfo - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle Supervisor add-on discovery.""" _LOGGER.debug("Supervisor discovery info: %s", discovery_info) await self.async_set_unique_id(discovery_info.uuid) @@ -79,7 +79,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_hassio_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm Supervisor discovery.""" errors: dict[str, str] = {} @@ -104,7 +104,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_zeroconf( self, discovery_info: zeroconf.ZeroconfServiceInfo - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle zeroconf discovery.""" _LOGGER.debug("Zeroconf discovery info: %s", discovery_info) if discovery_info.port is None: @@ -131,7 +131,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_zeroconf_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initiated by zeroconf.""" assert self._service is not None assert self._name is not None diff --git a/homeassistant/components/wyoming/devices.py b/homeassistant/components/wyoming/devices.py index 6865669fbf0..2ca66f3b21a 100644 --- a/homeassistant/components/wyoming/devices.py +++ b/homeassistant/components/wyoming/devices.py @@ -1,4 +1,5 @@ """Class to manage satellite devices.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/wyoming/error.py b/homeassistant/components/wyoming/error.py index 40b2e70ce69..0ccd35462b7 100644 --- a/homeassistant/components/wyoming/error.py +++ b/homeassistant/components/wyoming/error.py @@ -1,4 +1,5 @@ """Errors for the Wyoming integration.""" + from homeassistant.exceptions import HomeAssistantError diff --git a/homeassistant/components/wyoming/models.py b/homeassistant/components/wyoming/models.py index dce45d509eb..066af144d78 100644 --- a/homeassistant/components/wyoming/models.py +++ b/homeassistant/components/wyoming/models.py @@ -1,4 +1,5 @@ """Models for wyoming.""" + from dataclasses import dataclass from .data import WyomingService diff --git a/homeassistant/components/wyoming/stt.py b/homeassistant/components/wyoming/stt.py index 8a21ef051fc..227fa3a0eca 100644 --- a/homeassistant/components/wyoming/stt.py +++ b/homeassistant/components/wyoming/stt.py @@ -1,4 +1,5 @@ """Support for Wyoming speech-to-text services.""" + from collections.abc import AsyncIterable import logging diff --git a/homeassistant/components/wyoming/tts.py b/homeassistant/components/wyoming/tts.py index f024f925514..65ce4d942f1 100644 --- a/homeassistant/components/wyoming/tts.py +++ b/homeassistant/components/wyoming/tts.py @@ -1,4 +1,5 @@ """Support for Wyoming text-to-speech services.""" + from collections import defaultdict import io import logging diff --git a/homeassistant/components/x10/light.py b/homeassistant/components/x10/light.py index b7e331e2199..8f105d9c695 100644 --- a/homeassistant/components/x10/light.py +++ b/homeassistant/components/x10/light.py @@ -1,4 +1,5 @@ """Support for X10 lights.""" + from __future__ import annotations import logging @@ -33,7 +34,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def x10_command(command): """Execute X10 command and check output.""" - return check_output(["heyu"] + command.split(" "), stderr=STDOUT) + return check_output(["heyu", *command.split(" ")], stderr=STDOUT) def get_unit_status(code): diff --git a/homeassistant/components/xbox/__init__.py b/homeassistant/components/xbox/__init__.py index 37e11dd2693..3c9b5a44f04 100644 --- a/homeassistant/components/xbox/__init__.py +++ b/homeassistant/components/xbox/__init__.py @@ -1,4 +1,5 @@ """The xbox integration.""" + from __future__ import annotations from contextlib import suppress diff --git a/homeassistant/components/xbox/api.py b/homeassistant/components/xbox/api.py index 04714afa33e..a0c2d4cfb16 100644 --- a/homeassistant/components/xbox/api.py +++ b/homeassistant/components/xbox/api.py @@ -1,4 +1,5 @@ """API for xbox bound to Home Assistant OAuth.""" + from aiohttp import ClientSession from xbox.webapi.authentication.manager import AuthenticationManager from xbox.webapi.authentication.models import OAuth2TokenResponse diff --git a/homeassistant/components/xbox/base_sensor.py b/homeassistant/components/xbox/base_sensor.py index 9aecb100df0..7769d639f44 100644 --- a/homeassistant/components/xbox/base_sensor.py +++ b/homeassistant/components/xbox/base_sensor.py @@ -1,4 +1,5 @@ """Base Sensor for the Xbox Integration.""" + from __future__ import annotations from yarl import URL diff --git a/homeassistant/components/xbox/binary_sensor.py b/homeassistant/components/xbox/binary_sensor.py index 3c262bce82e..ffd99cde30e 100644 --- a/homeassistant/components/xbox/binary_sensor.py +++ b/homeassistant/components/xbox/binary_sensor.py @@ -1,4 +1,5 @@ """Xbox friends binary sensors.""" + from __future__ import annotations from functools import partial diff --git a/homeassistant/components/xbox/browse_media.py b/homeassistant/components/xbox/browse_media.py index 138b0bd612c..060712b5f9f 100644 --- a/homeassistant/components/xbox/browse_media.py +++ b/homeassistant/components/xbox/browse_media.py @@ -1,4 +1,5 @@ """Support for media browsing.""" + from __future__ import annotations from typing import NamedTuple @@ -108,18 +109,18 @@ async def build_item_response( content_types = sorted( {app.content_type for app in apps.result if app.content_type in TYPE_MAP} ) - for c_type in content_types: - children.append( - BrowseMedia( - media_class=MediaClass.DIRECTORY, - media_content_id=c_type, - media_content_type=TYPE_MAP[c_type].type, - title=f"{c_type}s", - can_play=False, - can_expand=True, - children_media_class=TYPE_MAP[c_type].cls, - ) + children.extend( + BrowseMedia( + media_class=MediaClass.DIRECTORY, + media_content_id=c_type, + media_content_type=TYPE_MAP[c_type].type, + title=f"{c_type}s", + can_play=False, + can_expand=True, + children_media_class=TYPE_MAP[c_type].cls, ) + for c_type in content_types + ) return library_info diff --git a/homeassistant/components/xbox/config_flow.py b/homeassistant/components/xbox/config_flow.py index ef00d1381e5..e1434aac67c 100644 --- a/homeassistant/components/xbox/config_flow.py +++ b/homeassistant/components/xbox/config_flow.py @@ -1,4 +1,5 @@ """Config flow for xbox.""" + import logging from homeassistant.helpers import config_entry_oauth2_flow diff --git a/homeassistant/components/xbox/manifest.json b/homeassistant/components/xbox/manifest.json index 67e53e326ee..30a6c3bc700 100644 --- a/homeassistant/components/xbox/manifest.json +++ b/homeassistant/components/xbox/manifest.json @@ -5,7 +5,6 @@ "config_flow": true, "dependencies": ["auth", "application_credentials"], "documentation": "https://www.home-assistant.io/integrations/xbox", - "import_executor": true, "iot_class": "cloud_polling", "requirements": ["xbox-webapi==2.0.11"] } diff --git a/homeassistant/components/xbox/media_player.py b/homeassistant/components/xbox/media_player.py index 060720338e8..f2cbc2e7c87 100644 --- a/homeassistant/components/xbox/media_player.py +++ b/homeassistant/components/xbox/media_player.py @@ -1,4 +1,5 @@ """Xbox Media Player Support.""" + from __future__ import annotations import re diff --git a/homeassistant/components/xbox/media_source.py b/homeassistant/components/xbox/media_source.py index 6a2def82a96..ea444ce1bc9 100644 --- a/homeassistant/components/xbox/media_source.py +++ b/homeassistant/components/xbox/media_source.py @@ -1,4 +1,5 @@ """Xbox Media Source Implementation.""" + from __future__ import annotations from contextlib import suppress diff --git a/homeassistant/components/xbox/remote.py b/homeassistant/components/xbox/remote.py index fdb4e80cf9e..a720025a1e6 100644 --- a/homeassistant/components/xbox/remote.py +++ b/homeassistant/components/xbox/remote.py @@ -1,4 +1,5 @@ """Xbox Remote support.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/xbox/sensor.py b/homeassistant/components/xbox/sensor.py index 77d52719c88..4e258399a5d 100644 --- a/homeassistant/components/xbox/sensor.py +++ b/homeassistant/components/xbox/sensor.py @@ -1,4 +1,5 @@ """Xbox friends binary sensors.""" + from __future__ import annotations from functools import partial diff --git a/homeassistant/components/xeoma/camera.py b/homeassistant/components/xeoma/camera.py index 31b80618d9e..7d6abde8535 100644 --- a/homeassistant/components/xeoma/camera.py +++ b/homeassistant/components/xeoma/camera.py @@ -1,4 +1,5 @@ """Support for Xeoma Cameras.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/xiaomi/camera.py b/homeassistant/components/xiaomi/camera.py index e9d686a6365..f3e850a7839 100644 --- a/homeassistant/components/xiaomi/camera.py +++ b/homeassistant/components/xiaomi/camera.py @@ -1,4 +1,5 @@ """Component providing support for Xiaomi Cameras.""" + from __future__ import annotations from ftplib import FTP, error_perm diff --git a/homeassistant/components/xiaomi/device_tracker.py b/homeassistant/components/xiaomi/device_tracker.py index f277060304a..76227d89e94 100644 --- a/homeassistant/components/xiaomi/device_tracker.py +++ b/homeassistant/components/xiaomi/device_tracker.py @@ -1,4 +1,5 @@ """Support for Xiaomi Mi routers.""" + from __future__ import annotations from http import HTTPStatus @@ -167,7 +168,7 @@ def _get_token(host, username, password): except KeyError: error_message = ( "Xiaomi token cannot be refreshed, response from " - + "url: [%s] \nwith parameter: [%s] \nwas: [%s]" + "url: [%s] \nwith parameter: [%s] \nwas: [%s]" ) _LOGGER.exception(error_message, url, data, result) return diff --git a/homeassistant/components/xiaomi_aqara/__init__.py b/homeassistant/components/xiaomi_aqara/__init__.py index f7bc1910521..ee7948a237e 100644 --- a/homeassistant/components/xiaomi_aqara/__init__.py +++ b/homeassistant/components/xiaomi_aqara/__init__.py @@ -1,4 +1,5 @@ """Support for Xiaomi Gateways.""" + import asyncio from datetime import timedelta import logging @@ -387,7 +388,7 @@ class XiaomiDevice(Entity): def parse_data(self, data, raw_data): """Parse data sent by gateway.""" - raise NotImplementedError() + raise NotImplementedError def _add_gateway_to_schema(hass, schema): diff --git a/homeassistant/components/xiaomi_aqara/binary_sensor.py b/homeassistant/components/xiaomi_aqara/binary_sensor.py index 591e97304db..89071432c2b 100644 --- a/homeassistant/components/xiaomi_aqara/binary_sensor.py +++ b/homeassistant/components/xiaomi_aqara/binary_sensor.py @@ -1,4 +1,5 @@ """Support for Xiaomi aqara binary sensors.""" + import logging from homeassistant.components.binary_sensor import ( diff --git a/homeassistant/components/xiaomi_aqara/config_flow.py b/homeassistant/components/xiaomi_aqara/config_flow.py index 2fae1796e45..8f391c8ddf3 100644 --- a/homeassistant/components/xiaomi_aqara/config_flow.py +++ b/homeassistant/components/xiaomi_aqara/config_flow.py @@ -1,15 +1,15 @@ """Config flow to configure Xiaomi Aqara.""" + import logging from socket import gaierror import voluptuous as vol from xiaomi_gateway import MULTICAST_PORT, XiaomiGateway, XiaomiGatewayDiscovery -from homeassistant import config_entries from homeassistant.components import zeroconf +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONF_PORT, CONF_PROTOCOL from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.device_registry import format_mac from .const import ( @@ -44,7 +44,7 @@ GATEWAY_SETTINGS = vol.Schema( ) -class XiaomiAqaraFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class XiaomiAqaraFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a Xiaomi Aqara config flow.""" VERSION = 1 @@ -148,7 +148,7 @@ class XiaomiAqaraFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_zeroconf( self, discovery_info: zeroconf.ZeroconfServiceInfo - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle zeroconf discovery.""" name = discovery_info.name self.host = discovery_info.host @@ -158,9 +158,7 @@ class XiaomiAqaraFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_abort(reason="not_xiaomi_aqara") # Check if the discovered device is an xiaomi aqara gateway. - if not ( - name.startswith(ZEROCONF_GATEWAY) or name.startswith(ZEROCONF_ACPARTNER) - ): + if not (name.startswith((ZEROCONF_GATEWAY, ZEROCONF_ACPARTNER))): _LOGGER.debug( ( "Xiaomi device '%s' discovered with host %s, not identified as" @@ -206,7 +204,7 @@ class XiaomiAqaraFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): valid_key = True if valid_key: - # format_mac, for a gateway the sid equels the mac address + # format_mac, for a gateway the sid equals the mac address mac_address = format_mac(self.sid) # set unique_id diff --git a/homeassistant/components/xiaomi_aqara/cover.py b/homeassistant/components/xiaomi_aqara/cover.py index b6de7189d83..64c9f6f208a 100644 --- a/homeassistant/components/xiaomi_aqara/cover.py +++ b/homeassistant/components/xiaomi_aqara/cover.py @@ -1,4 +1,5 @@ """Support for Xiaomi curtain.""" + from typing import Any from homeassistant.components.cover import ATTR_POSITION, CoverEntity diff --git a/homeassistant/components/xiaomi_aqara/icons.json b/homeassistant/components/xiaomi_aqara/icons.json new file mode 100644 index 00000000000..4975414833d --- /dev/null +++ b/homeassistant/components/xiaomi_aqara/icons.json @@ -0,0 +1,8 @@ +{ + "services": { + "add_device": "mdi:cellphone-link", + "play_ringtone": "mdi:music", + "remove_device": "mdi:cellphone-link", + "stop_ringtone": "mdi:music-off" + } +} diff --git a/homeassistant/components/xiaomi_aqara/light.py b/homeassistant/components/xiaomi_aqara/light.py index 822d4121c68..fc19a22eb5f 100644 --- a/homeassistant/components/xiaomi_aqara/light.py +++ b/homeassistant/components/xiaomi_aqara/light.py @@ -1,4 +1,5 @@ """Support for Xiaomi Gateway Light.""" + import binascii import logging import struct @@ -105,7 +106,7 @@ class XiaomiGatewayLight(XiaomiDevice, LightEntity): self._brightness = int(100 * kwargs[ATTR_BRIGHTNESS] / 255) rgb = color_util.color_hs_to_RGB(*self._hs) - rgba = (self._brightness,) + rgb + rgba = (self._brightness, *rgb) rgbhex = binascii.hexlify(struct.pack("BBBB", *rgba)).decode("ASCII") rgbhex = int(rgbhex, 16) diff --git a/homeassistant/components/xiaomi_aqara/lock.py b/homeassistant/components/xiaomi_aqara/lock.py index fea729b2b47..90afbe15911 100644 --- a/homeassistant/components/xiaomi_aqara/lock.py +++ b/homeassistant/components/xiaomi_aqara/lock.py @@ -1,4 +1,5 @@ """Support for Xiaomi Aqara locks.""" + from __future__ import annotations from homeassistant.components.lock import LockEntity @@ -27,12 +28,12 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Perform the setup for Xiaomi devices.""" - entities = [] gateway = hass.data[DOMAIN][GATEWAYS_KEY][config_entry.entry_id] - for device in gateway.devices["lock"]: - if device["model"] == "lock.aq1": - entities.append(XiaomiAqaraLock(device, "Lock", gateway, config_entry)) - async_add_entities(entities) + async_add_entities( + XiaomiAqaraLock(device, "Lock", gateway, config_entry) + for device in gateway.devices["lock"] + if device["model"] == "lock.aq1" + ) class XiaomiAqaraLock(LockEntity, XiaomiDevice): diff --git a/homeassistant/components/xiaomi_aqara/sensor.py b/homeassistant/components/xiaomi_aqara/sensor.py index e114adaa8c4..4b354a6e730 100644 --- a/homeassistant/components/xiaomi_aqara/sensor.py +++ b/homeassistant/components/xiaomi_aqara/sensor.py @@ -1,4 +1,5 @@ """Support for Xiaomi Aqara sensors.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/xiaomi_aqara/switch.py b/homeassistant/components/xiaomi_aqara/switch.py index 0ea6a65f68e..b6bd2ca1e6a 100644 --- a/homeassistant/components/xiaomi_aqara/switch.py +++ b/homeassistant/components/xiaomi_aqara/switch.py @@ -1,4 +1,5 @@ """Support for Xiaomi Aqara binary sensors.""" + import logging from typing import Any diff --git a/homeassistant/components/xiaomi_ble/__init__.py b/homeassistant/components/xiaomi_ble/__init__.py index 3adafc6d05e..19c1f3feea1 100644 --- a/homeassistant/components/xiaomi_ble/__init__.py +++ b/homeassistant/components/xiaomi_ble/__init__.py @@ -1,4 +1,5 @@ """The Xiaomi Bluetooth integration.""" + from __future__ import annotations import logging @@ -167,25 +168,27 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return await data.async_poll(connectable_device) device_registry = async_get(hass) - coordinator = hass.data.setdefault(DOMAIN, {})[ - entry.entry_id - ] = XiaomiActiveBluetoothProcessorCoordinator( - hass, - _LOGGER, - address=address, - mode=BluetoothScanningMode.PASSIVE, - update_method=lambda service_info: process_service_info( - hass, entry, data, service_info, device_registry - ), - needs_poll_method=_needs_poll, - device_data=data, - discovered_event_classes=set(entry.data.get(CONF_DISCOVERED_EVENT_CLASSES, [])), - poll_method=_async_poll, - # We will take advertisements from non-connectable devices - # since we will trade the BLEDevice for a connectable one - # if we need to poll it - connectable=False, - entry=entry, + coordinator = hass.data.setdefault(DOMAIN, {})[entry.entry_id] = ( + XiaomiActiveBluetoothProcessorCoordinator( + hass, + _LOGGER, + address=address, + mode=BluetoothScanningMode.PASSIVE, + update_method=lambda service_info: process_service_info( + hass, entry, data, service_info, device_registry + ), + needs_poll_method=_needs_poll, + device_data=data, + discovered_event_classes=set( + entry.data.get(CONF_DISCOVERED_EVENT_CLASSES, []) + ), + poll_method=_async_poll, + # We will take advertisements from non-connectable devices + # since we will trade the BLEDevice for a connectable one + # if we need to poll it + connectable=False, + entry=entry, + ) ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload( diff --git a/homeassistant/components/xiaomi_ble/binary_sensor.py b/homeassistant/components/xiaomi_ble/binary_sensor.py index cd6f7b453bb..c8d4666e482 100644 --- a/homeassistant/components/xiaomi_ble/binary_sensor.py +++ b/homeassistant/components/xiaomi_ble/binary_sensor.py @@ -1,4 +1,5 @@ """Support for Xiaomi binary sensors.""" + from __future__ import annotations from xiaomi_ble.parser import ( @@ -41,6 +42,10 @@ BINARY_SENSOR_DESCRIPTIONS = { key=XiaomiBinarySensorDeviceClass.LIGHT, device_class=BinarySensorDeviceClass.LIGHT, ), + XiaomiBinarySensorDeviceClass.LOCK: BinarySensorEntityDescription( + key=XiaomiBinarySensorDeviceClass.LOCK, + device_class=BinarySensorDeviceClass.LOCK, + ), XiaomiBinarySensorDeviceClass.MOISTURE: BinarySensorEntityDescription( key=XiaomiBinarySensorDeviceClass.MOISTURE, device_class=BinarySensorDeviceClass.MOISTURE, @@ -61,6 +66,16 @@ BINARY_SENSOR_DESCRIPTIONS = { key=XiaomiBinarySensorDeviceClass.SMOKE, device_class=BinarySensorDeviceClass.SMOKE, ), + ExtendedBinarySensorDeviceClass.ANTILOCK: BinarySensorEntityDescription( + key=ExtendedBinarySensorDeviceClass.ANTILOCK, + ), + ExtendedBinarySensorDeviceClass.ARMED: BinarySensorEntityDescription( + key=ExtendedBinarySensorDeviceClass.ARMED, + icon="mdi:shield-check", + ), + ExtendedBinarySensorDeviceClass.CHILDLOCK: BinarySensorEntityDescription( + key=ExtendedBinarySensorDeviceClass.CHILDLOCK, + ), ExtendedBinarySensorDeviceClass.DEVICE_FORCIBLY_REMOVED: BinarySensorEntityDescription( key=ExtendedBinarySensorDeviceClass.DEVICE_FORCIBLY_REMOVED, device_class=BinarySensorDeviceClass.PROBLEM, @@ -73,6 +88,10 @@ BINARY_SENSOR_DESCRIPTIONS = { key=ExtendedBinarySensorDeviceClass.DOOR_STUCK, device_class=BinarySensorDeviceClass.PROBLEM, ), + ExtendedBinarySensorDeviceClass.FINGERPRINT: BinarySensorEntityDescription( + key=ExtendedBinarySensorDeviceClass.FINGERPRINT, + icon="mdi:fingerprint", + ), ExtendedBinarySensorDeviceClass.KNOCK_ON_THE_DOOR: BinarySensorEntityDescription( key=ExtendedBinarySensorDeviceClass.KNOCK_ON_THE_DOOR, ), diff --git a/homeassistant/components/xiaomi_ble/config_flow.py b/homeassistant/components/xiaomi_ble/config_flow.py index 576d49296e9..8209c9565bd 100644 --- a/homeassistant/components/xiaomi_ble/config_flow.py +++ b/homeassistant/components/xiaomi_ble/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Xiaomi Bluetooth integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -16,9 +17,8 @@ from homeassistant.components.bluetooth import ( async_discovered_service_info, async_process_advertisements, ) -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_ADDRESS -from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN @@ -76,7 +76,7 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_bluetooth( self, discovery_info: BluetoothServiceInfo - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the bluetooth discovery step.""" await self.async_set_unique_id(discovery_info.address) self._abort_if_unique_id_configured() @@ -109,7 +109,7 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_get_encryption_key_legacy( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Enter a legacy bindkey for a v2/v3 MiBeacon device.""" assert self._discovery_info assert self._discovered_device @@ -143,7 +143,7 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_get_encryption_key_4_5( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Enter a bindkey for a v4/v5 MiBeacon device.""" assert self._discovery_info assert self._discovered_device @@ -177,7 +177,7 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_bluetooth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm discovery.""" if user_input is not None or not onboarding.async_is_onboarded(self.hass): return self._async_get_or_create_entry() @@ -190,7 +190,7 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_confirm_slow( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Ack that device is slow.""" if user_input is not None: return self._async_get_or_create_entry() @@ -203,7 +203,7 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the user step to pick discovered device.""" if user_input is not None: address = user_input[CONF_ADDRESS] @@ -260,7 +260,9 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): data_schema=vol.Schema({vol.Required(CONF_ADDRESS): vol.In(titles)}), ) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """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 @@ -279,7 +281,9 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): # Otherwise there wasn't actually encryption so abort return self.async_abort(reason="reauth_successful") - def _async_get_or_create_entry(self, bindkey: str | None = None) -> FlowResult: + def _async_get_or_create_entry( + self, bindkey: str | None = None + ) -> ConfigFlowResult: data: dict[str, Any] = {} if bindkey: diff --git a/homeassistant/components/xiaomi_ble/const.py b/homeassistant/components/xiaomi_ble/const.py index 5f9dea9eb45..8ea99cf1f84 100644 --- a/homeassistant/components/xiaomi_ble/const.py +++ b/homeassistant/components/xiaomi_ble/const.py @@ -1,4 +1,5 @@ """Constants for the Xiaomi Bluetooth integration.""" + from __future__ import annotations from typing import Final, TypedDict @@ -19,13 +20,21 @@ EVENT_PROPERTIES: Final = "event_properties" XIAOMI_BLE_EVENT: Final = "xiaomi_ble_event" EVENT_CLASS_BUTTON: Final = "button" -EVENT_CLASS_DIMMER: Final = "dimmer" -EVENT_CLASS_MOTION: Final = "motion" EVENT_CLASS_CUBE: Final = "cube" +EVENT_CLASS_DIMMER: Final = "dimmer" +EVENT_CLASS_ERROR: Final = "error" +EVENT_CLASS_FINGERPRINT: Final = "fingerprint" +EVENT_CLASS_LOCK: Final = "lock" +EVENT_CLASS_MOTION: Final = "motion" BUTTON: Final = "button" CUBE: Final = "cube" DIMMER: Final = "dimmer" +ERROR: Final = "error" +FINGERPRINT: Final = "fingerprint" +LOCK: Final = "lock" +LOCK_FINGERPRINT = "lock_fingerprint" +MOTION_DEVICE: Final = "motion_device" DOUBLE_BUTTON: Final = "double_button" TRIPPLE_BUTTON: Final = "tripple_button" REMOTE: Final = "remote" @@ -39,7 +48,6 @@ BUTTON_PRESS_LONG: Final = "button_press_long" BUTTON_PRESS_DOUBLE_LONG: Final = "button_press_double_long" DOUBLE_BUTTON_PRESS_DOUBLE_LONG: Final = "double_button_press_double_long" TRIPPLE_BUTTON_PRESS_DOUBLE_LONG: Final = "tripple_button_press_double_long" -MOTION_DEVICE: Final = "motion_device" class XiaomiBleEvent(TypedDict): diff --git a/homeassistant/components/xiaomi_ble/coordinator.py b/homeassistant/components/xiaomi_ble/coordinator.py index a935f3ea199..ef5212584d8 100644 --- a/homeassistant/components/xiaomi_ble/coordinator.py +++ b/homeassistant/components/xiaomi_ble/coordinator.py @@ -1,4 +1,5 @@ """The Xiaomi BLE integration.""" + from collections.abc import Callable, Coroutine from logging import Logger from typing import Any diff --git a/homeassistant/components/xiaomi_ble/device.py b/homeassistant/components/xiaomi_ble/device.py index 5714db4eadd..4f712a7a77c 100644 --- a/homeassistant/components/xiaomi_ble/device.py +++ b/homeassistant/components/xiaomi_ble/device.py @@ -1,4 +1,5 @@ """Support for Xioami BLE devices.""" + from __future__ import annotations from xiaomi_ble import DeviceKey diff --git a/homeassistant/components/xiaomi_ble/device_trigger.py b/homeassistant/components/xiaomi_ble/device_trigger.py index 8d281ddc8a9..119424788db 100644 --- a/homeassistant/components/xiaomi_ble/device_trigger.py +++ b/homeassistant/components/xiaomi_ble/device_trigger.py @@ -1,4 +1,5 @@ """Provides device triggers for Xiaomi BLE.""" + from __future__ import annotations from dataclasses import dataclass @@ -31,12 +32,19 @@ from .const import ( DOMAIN, DOUBLE_BUTTON, DOUBLE_BUTTON_PRESS_DOUBLE_LONG, + ERROR, EVENT_CLASS, EVENT_CLASS_BUTTON, EVENT_CLASS_CUBE, EVENT_CLASS_DIMMER, + EVENT_CLASS_ERROR, + EVENT_CLASS_FINGERPRINT, + EVENT_CLASS_LOCK, EVENT_CLASS_MOTION, EVENT_TYPE, + FINGERPRINT, + LOCK, + LOCK_FINGERPRINT, MOTION, MOTION_DEVICE, REMOTE, @@ -61,6 +69,51 @@ TRIGGERS_BY_TYPE = { "rotate_left_pressed", "rotate_right_pressed", ], + ERROR: [ + "frequent_unlocking_with_incorrect_password", + "frequent_unlocking_with_wrong_fingerprints", + "operation_timeout_password_input_timeout", + "lock_picking", + "reset_button_is_pressed", + "the_wrong_key_is_frequently_unlocked", + "foreign_body_in_the_keyhole", + "the_key_has_not_been_taken_out", + "error_nfc_frequently_unlocks", + "timeout_is_not_locked_as_required", + "failure_to_unlock_frequently_in_multiple_ways", + "unlocking_the_face_frequently_fails", + "failure_to_unlock_the_vein_frequently", + "hijacking_alarm", + "unlock_inside_the_door_after_arming", + "palmprints_frequently_fail_to_unlock", + "the_safe_was_moved", + "the_battery_level_is_less_than_10_percent", + "the_battery_level_is_less_than_5_percent", + "the_fingerprint_sensor_is_abnormal", + "the_accessory_battery_is_low", + "mechanical_failure", + "the_lock_sensor_is_faulty", + ], + FINGERPRINT: [ + "match_successful", + "match_failed", + "low_quality_too_light_fuzzy", + "insufficient_area", + "skin_is_too_dry", + "skin_is_too_wet", + ], + LOCK: [ + "lock_outside_the_door", + "unlock_outside_the_door", + "lock_inside_the_door", + "unlock_inside_the_door", + "locked", + "turn_on_antilock", + "release_the_antilock", + "turn_on_child_lock", + "turn_off_child_lock", + "abnormal", + ], MOTION_DEVICE: ["motion_detected"], } @@ -70,6 +123,10 @@ EVENT_TYPES = { DIMMER: ["dimmer"], DOUBLE_BUTTON: ["button_left", "button_right"], TRIPPLE_BUTTON: ["button_left", "button_middle", "button_right"], + ERROR: ["error"], + FINGERPRINT: ["fingerprint"], + LOCK: ["lock"], + MOTION: ["motion"], REMOTE: [ "button_on", "button_off", @@ -105,7 +162,6 @@ EVENT_TYPES = { "button_increase_wind_speed", "button_decrease_wind_speed", ], - MOTION: ["motion"], } @@ -149,6 +205,26 @@ TRIGGER_MODEL_DATA = { event_types=EVENT_TYPES[TRIPPLE_BUTTON], triggers=TRIGGERS_BY_TYPE[BUTTON_PRESS_DOUBLE_LONG], ), + ERROR: TriggerModelData( + event_class=EVENT_CLASS_ERROR, + event_types=EVENT_TYPES[ERROR], + triggers=TRIGGERS_BY_TYPE[ERROR], + ), + LOCK_FINGERPRINT: TriggerModelData( + event_class=EVENT_CLASS_FINGERPRINT, + event_types=EVENT_TYPES[LOCK] + EVENT_TYPES[FINGERPRINT], + triggers=TRIGGERS_BY_TYPE[LOCK] + TRIGGERS_BY_TYPE[FINGERPRINT], + ), + LOCK: TriggerModelData( + event_class=EVENT_CLASS_LOCK, + event_types=EVENT_TYPES[LOCK], + triggers=TRIGGERS_BY_TYPE[LOCK], + ), + MOTION_DEVICE: TriggerModelData( + event_class=EVENT_CLASS_MOTION, + event_types=EVENT_TYPES[MOTION], + triggers=TRIGGERS_BY_TYPE[MOTION_DEVICE], + ), REMOTE: TriggerModelData( event_class=EVENT_CLASS_BUTTON, event_types=EVENT_TYPES[REMOTE], @@ -169,11 +245,6 @@ TRIGGER_MODEL_DATA = { event_types=EVENT_TYPES[REMOTE_VENFAN], triggers=TRIGGERS_BY_TYPE[BUTTON_PRESS_LONG], ), - MOTION_DEVICE: TriggerModelData( - event_class=EVENT_CLASS_MOTION, - event_types=EVENT_TYPES[MOTION], - triggers=TRIGGERS_BY_TYPE[MOTION_DEVICE], - ), } @@ -185,6 +256,7 @@ MODEL_DATA = { "K9BB-1BTN": TRIGGER_MODEL_DATA[BUTTON_PRESS_DOUBLE_LONG], "YLAI003": TRIGGER_MODEL_DATA[BUTTON_PRESS_DOUBLE_LONG], "XMWXKG01LM": TRIGGER_MODEL_DATA[BUTTON_PRESS_DOUBLE_LONG], + "PTX_YK1_QMIMB": TRIGGER_MODEL_DATA[BUTTON_PRESS_DOUBLE_LONG], "K9B-1BTN": TRIGGER_MODEL_DATA[BUTTON_PRESS_DOUBLE_LONG], "XMWXKG01YL": TRIGGER_MODEL_DATA[DOUBLE_BUTTON_PRESS_DOUBLE_LONG], "K9B-2BTN": TRIGGER_MODEL_DATA[DOUBLE_BUTTON_PRESS_DOUBLE_LONG], @@ -196,6 +268,13 @@ MODEL_DATA = { "MUE4094RT": TRIGGER_MODEL_DATA[MOTION_DEVICE], "XMMF01JQD": TRIGGER_MODEL_DATA[CUBE], "YLKG07YL/YLKG08YL": TRIGGER_MODEL_DATA[DIMMER], + "DSL-C08": TRIGGER_MODEL_DATA[LOCK], + "XMZNMS08LM": TRIGGER_MODEL_DATA[LOCK], + "Lockin-SV40": TRIGGER_MODEL_DATA[LOCK_FINGERPRINT], + "MJZNMSQ01YD": TRIGGER_MODEL_DATA[LOCK_FINGERPRINT], + "XMZNMS04LM": TRIGGER_MODEL_DATA[LOCK_FINGERPRINT], + "ZNMS16LM": TRIGGER_MODEL_DATA[LOCK_FINGERPRINT], + "ZNMS17LM": TRIGGER_MODEL_DATA[LOCK_FINGERPRINT], } diff --git a/homeassistant/components/xiaomi_ble/event.py b/homeassistant/components/xiaomi_ble/event.py index 2c1550dc5d7..e39a4adb3c7 100644 --- a/homeassistant/components/xiaomi_ble/event.py +++ b/homeassistant/components/xiaomi_ble/event.py @@ -1,4 +1,5 @@ """Support for Xiaomi event entities.""" + from __future__ import annotations from dataclasses import replace @@ -20,6 +21,9 @@ from .const import ( EVENT_CLASS_BUTTON, EVENT_CLASS_CUBE, EVENT_CLASS_DIMMER, + EVENT_CLASS_ERROR, + EVENT_CLASS_FINGERPRINT, + EVENT_CLASS_LOCK, EVENT_CLASS_MOTION, EVENT_PROPERTIES, EVENT_TYPE, @@ -58,6 +62,63 @@ DESCRIPTIONS_BY_EVENT_CLASS = { "rotate_right_pressed", ], ), + EVENT_CLASS_ERROR: EventEntityDescription( + key=EVENT_CLASS_ERROR, + translation_key="error", + event_types=[ + "frequent_unlocking_with_incorrect_password", + "frequent_unlocking_with_wrong_fingerprints", + "operation_timeout_password_input_timeout", + "lock_picking", + "reset_button_is_pressed", + "the_wrong_key_is_frequently_unlocked", + "foreign_body_in_the_keyhole", + "the_key_has_not_been_taken_out", + "error_nfc_frequently_unlocks", + "timeout_is_not_locked_as_required", + "failure_to_unlock_frequently_in_multiple_ways", + "unlocking_the_face_frequently_fails", + "failure_to_unlock_the_vein_frequently", + "hijacking_alarm", + "unlock_inside_the_door_after_arming", + "palmprints_frequently_fail_to_unlock", + "the_safe_was_moved", + "the_battery_level_is_less_than_10_percent", + "the_battery_is_less_than_5_percent", + "the_fingerprint_sensor_is_abnormal", + "the_accessory_battery_is_low", + "mechanical_failure", + "the_lock_sensor_is_faulty", + ], + ), + EVENT_CLASS_FINGERPRINT: EventEntityDescription( + key=EVENT_CLASS_FINGERPRINT, + translation_key="fingerprint", + event_types=[ + "match_successful", + "match_failed", + "low_quality_too_light_fuzzy", + "insufficient_area", + "skin_is_too_dry", + "skin_is_too_wet", + ], + ), + EVENT_CLASS_LOCK: EventEntityDescription( + key=EVENT_CLASS_LOCK, + translation_key="lock", + event_types=[ + "lock_outside_the_door", + "unlock_outside_the_door", + "lock_inside_the_door", + "unlock_inside_the_door", + "locked", + "turn_on_antilock", + "release_the_antilock", + "turn_on_child_lock", + "turn_off_child_lock", + "abnormal", + ], + ), EVENT_CLASS_MOTION: EventEntityDescription( key=EVENT_CLASS_MOTION, translation_key="motion", diff --git a/homeassistant/components/xiaomi_ble/manifest.json b/homeassistant/components/xiaomi_ble/manifest.json index 22629d3e326..ef0556b6966 100644 --- a/homeassistant/components/xiaomi_ble/manifest.json +++ b/homeassistant/components/xiaomi_ble/manifest.json @@ -23,7 +23,6 @@ "config_flow": true, "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/xiaomi_ble", - "import_executor": true, "iot_class": "local_push", - "requirements": ["xiaomi-ble==0.25.2"] + "requirements": ["xiaomi-ble==0.28.0"] } diff --git a/homeassistant/components/xiaomi_ble/sensor.py b/homeassistant/components/xiaomi_ble/sensor.py index cdb7b3a8fd8..c5354a54394 100644 --- a/homeassistant/components/xiaomi_ble/sensor.py +++ b/homeassistant/components/xiaomi_ble/sensor.py @@ -1,4 +1,5 @@ """Support for xiaomi ble sensors.""" + from __future__ import annotations from xiaomi_ble import DeviceClass, SensorUpdate, Units @@ -131,23 +132,31 @@ SENSOR_DESCRIPTIONS = { state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, ), - # Used for e.g. consumable sensor on WX08ZM and M1S-T500 + # E.g. consumable sensor on WX08ZM and M1S-T500 (ExtendedSensorDeviceClass.CONSUMABLE, Units.PERCENTAGE): SensorEntityDescription( key=str(ExtendedSensorDeviceClass.CONSUMABLE), native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, ), - # Used for score after brushing with a toothbrush + # Score after brushing with a toothbrush (ExtendedSensorDeviceClass.SCORE, None): SensorEntityDescription( key=str(ExtendedSensorDeviceClass.SCORE), state_class=SensorStateClass.MEASUREMENT, ), - # Used for counting during brushing + # Counting during brushing (ExtendedSensorDeviceClass.COUNTER, Units.TIME_SECONDS): SensorEntityDescription( key=str(ExtendedSensorDeviceClass.COUNTER), native_unit_of_measurement=UnitOfTime.SECONDS, state_class=SensorStateClass.MEASUREMENT, ), + # Key id for locks and fingerprint readers + (ExtendedSensorDeviceClass.KEY_ID, None): SensorEntityDescription( + key=str(ExtendedSensorDeviceClass.KEY_ID), icon="mdi:identifier" + ), + # Lock method for locks + (ExtendedSensorDeviceClass.LOCK_METHOD, None): SensorEntityDescription( + key=str(ExtendedSensorDeviceClass.LOCK_METHOD), icon="mdi:key-variant" + ), } diff --git a/homeassistant/components/xiaomi_ble/strings.json b/homeassistant/components/xiaomi_ble/strings.json index d764a436f4c..2f2b705ff60 100644 --- a/homeassistant/components/xiaomi_ble/strings.json +++ b/homeassistant/components/xiaomi_ble/strings.json @@ -48,7 +48,23 @@ "rotate_left": "Rotate Left", "rotate_right": "Rotate Right", "rotate_left_pressed": "Rotate Left (Pressed)", - "rotate_right_pressed": "Rotate Right (Pressed)" + "rotate_right_pressed": "Rotate Right (Pressed)", + "match_successful": "Match successful", + "match_failed": "Match failed", + "low_quality_too_light_fuzzy": "Low quality (too light, fuzzy)", + "insufficient_area": "Insufficient area", + "skin_is_too_dry": "Skin is too dry", + "skin_is_too_wet": "Skin is too wet", + "lock_outside_the_door": "Lock outside the door", + "unlock_outside_the_door": "Unlock outside the door", + "lock_inside_the_door": "Lock inside the door", + "unlock_inside_the_door": "Unlock outside the door", + "locked": "Locked", + "turn_on_antilock": "Turn on antilock", + "release_the_antilock": "Release antilock", + "turn_on_child_lock": "Turn on child lock", + "turn_off_child_lock": "Turn off child lock", + "abnormal": "Abnormal" }, "trigger_type": { "button": "Button \"{subtype}\"", @@ -79,6 +95,8 @@ "button_increase_wind_speed": "Button Increase Wind Speed \"{subtype}\"", "button_decrease_wind_speed": "Button Decrease Wind Speed \"{subtype}\"", "dimmer": "{subtype}", + "fingerprint": "{subtype}", + "lock": "{subtype}", "motion": "{subtype}", "cube": "{subtype}" } @@ -120,6 +138,69 @@ } } }, + "error": { + "state_attributes": { + "event_type": { + "state": { + "frequent_unlocking_with_incorrect_password": "Frequent unlocking with incorrect password", + "frequent_unlocking_with_wrong_fingerprints": "Frequent unlocking with wrong fingerprints", + "operation_timeout_password_input_timeout": "Operation timeout password input timeout", + "lock_picking": "Lock picking", + "reset_button_is_pressed": "Reset button is pressed", + "the_wrong_key_is_frequently_unlocked": "The wrong key is frequently unlocked", + "foreign_body_in_the_keyhole": "Foreign body in the keyhole", + "the_key_has_not_been_taken_out": "The key has not been taken out", + "error_nfc_frequently_unlocks": "Error NFC frequently unlocks", + "timeout_is_not_locked_as_required": "Timeout is not locked as required", + "failure_to_unlock_frequently_in_multiple_ways": "Failure to unlock frequently in multiple ways", + "unlocking_the_face_frequently_fails": "Unlocking the face frequently fails", + "failure_to_unlock_the_vein_frequently": "Failure to unlock the vein frequently", + "hijacking_alarm": "Hijacking alarm", + "unlock_inside_the_door_after_arming": "Unlock inside the door after arming", + "palmprints_frequently_fail_to_unlock": "Palmprints frequently fail to unlock", + "the_safe_was_moved": "The safe was moved", + "the_battery_level_is_less_than_10_percent": "The battery level is less than 10%", + "the_battery_level_is_less_than_5_percent": "The battery level is less than 5%", + "the_fingerprint_sensor_is_abnormal": "The fingerprint sensor is abnormal", + "the_accessory_battery_is_low": "The accessory battery is low", + "mechanical_failure": "Mechanical failure", + "the_lock_sensor_is_faulty": "The lock sensor is faulty" + } + } + } + }, + "fingerprint": { + "state_attributes": { + "event_type": { + "state": { + "match_successful": "[%key:component::xiaomi_ble::device_automation::trigger_subtype::match_successful%]", + "match_failed": "[%key:component::xiaomi_ble::device_automation::trigger_subtype::match_failed%]", + "low_quality_too_light_fuzzy": "[%key:component::xiaomi_ble::device_automation::trigger_subtype::low_quality_too_light_fuzzy%]", + "insufficient_area": "[%key:component::xiaomi_ble::device_automation::trigger_subtype::insufficient_area%]", + "skin_is_too_dry": "[%key:component::xiaomi_ble::device_automation::trigger_subtype::skin_is_too_dry%]", + "skin_is_too_wet": "[%key:component::xiaomi_ble::device_automation::trigger_subtype::skin_is_too_wet%]" + } + } + } + }, + "lock": { + "state_attributes": { + "event_type": { + "state": { + "lock_outside_the_door": "[%key:component::xiaomi_ble::device_automation::trigger_subtype::lock_outside_the_door%]", + "unlock_outside_the_door": "[%key:component::xiaomi_ble::device_automation::trigger_subtype::unlock_outside_the_door%]", + "lock_inside_the_door": "[%key:component::xiaomi_ble::device_automation::trigger_subtype::lock_inside_the_door%]", + "unlock_inside_the_door": "[%key:component::xiaomi_ble::device_automation::trigger_subtype::unlock_inside_the_door%]", + "locked": "[%key:component::xiaomi_ble::device_automation::trigger_subtype::locked%]", + "turn_on_antilock": "[%key:component::xiaomi_ble::device_automation::trigger_subtype::turn_on_antilock%]", + "release_the_antilock": "[%key:component::xiaomi_ble::device_automation::trigger_subtype::release_the_antilock%]", + "turn_on_child_lock": "[%key:component::xiaomi_ble::device_automation::trigger_subtype::turn_on_child_lock%]", + "turn_off_child_lock": "[%key:component::xiaomi_ble::device_automation::trigger_subtype::turn_off_child_lock%]", + "abnormal": "[%key:component::xiaomi_ble::device_automation::trigger_subtype::abnormal%]" + } + } + } + }, "motion": { "state_attributes": { "event_type": { diff --git a/homeassistant/components/xiaomi_miio/__init__.py b/homeassistant/components/xiaomi_miio/__init__.py index 716d4a04fa7..5384bd93a7e 100644 --- a/homeassistant/components/xiaomi_miio/__init__.py +++ b/homeassistant/components/xiaomi_miio/__init__.py @@ -1,4 +1,5 @@ """Support for Xiaomi Miio.""" + from __future__ import annotations import asyncio @@ -339,10 +340,8 @@ async def async_create_miio_device_and_coordinator( device = AirFreshA1(host, token, lazy_discover=lazy_discover) elif model == MODEL_AIRFRESH_T2017: device = AirFreshT2017(host, token, lazy_discover=lazy_discover) - elif ( - model in MODELS_VACUUM - or model.startswith(ROBOROCK_GENERIC) - or model.startswith(ROCKROBO_GENERIC) + elif model in MODELS_VACUUM or model.startswith( + (ROBOROCK_GENERIC, ROCKROBO_GENERIC) ): # TODO: add lazy_discover as argument when python-miio add support # pylint: disable=fixme device = RoborockVacuum(host, token) @@ -411,9 +410,9 @@ async def async_setup_gateway_entry(hass: HomeAssistant, entry: ConfigEntry) -> try: await gateway.async_connect_gateway(host, token) except AuthException as error: - raise ConfigEntryAuthFailed() from error + raise ConfigEntryAuthFailed from error except SetupException as error: - raise ConfigEntryNotReady() from error + raise ConfigEntryNotReady from error gateway_info = gateway.gateway_info device_registry = dr.async_get(hass) diff --git a/homeassistant/components/xiaomi_miio/air_quality.py b/homeassistant/components/xiaomi_miio/air_quality.py index f9248ba5ff3..80dd751a98c 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 diff --git a/homeassistant/components/xiaomi_miio/alarm_control_panel.py b/homeassistant/components/xiaomi_miio/alarm_control_panel.py index e92dd76be39..72530227e88 100644 --- a/homeassistant/components/xiaomi_miio/alarm_control_panel.py +++ b/homeassistant/components/xiaomi_miio/alarm_control_panel.py @@ -1,4 +1,5 @@ """Support for Xiomi Gateway alarm control panels.""" + from __future__ import annotations from functools import partial diff --git a/homeassistant/components/xiaomi_miio/binary_sensor.py b/homeassistant/components/xiaomi_miio/binary_sensor.py index e1b06175493..7729ce27d29 100644 --- a/homeassistant/components/xiaomi_miio/binary_sensor.py +++ b/homeassistant/components/xiaomi_miio/binary_sensor.py @@ -1,4 +1,5 @@ """Support for Xiaomi Miio binary sensors.""" + from __future__ import annotations from collections.abc import Callable, Iterable diff --git a/homeassistant/components/xiaomi_miio/button.py b/homeassistant/components/xiaomi_miio/button.py index 4ebbf34f295..38e6afa5ffb 100644 --- a/homeassistant/components/xiaomi_miio/button.py +++ b/homeassistant/components/xiaomi_miio/button.py @@ -1,4 +1,5 @@ """Support for Xiaomi buttons.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/homeassistant/components/xiaomi_miio/config_flow.py b/homeassistant/components/xiaomi_miio/config_flow.py index 379db82042b..e2a129e147d 100644 --- a/homeassistant/components/xiaomi_miio/config_flow.py +++ b/homeassistant/components/xiaomi_miio/config_flow.py @@ -1,4 +1,5 @@ """Config flow to configure Xiaomi Miio.""" + from __future__ import annotations from collections.abc import Mapping @@ -10,12 +11,16 @@ from micloud import MiCloud from micloud.micloudexception import MiCloudAccessDenied import voluptuous as vol -from homeassistant import config_entries from homeassistant.components import zeroconf -from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry +from homeassistant.config_entries import ( + SOURCE_REAUTH, + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.const import CONF_DEVICE, CONF_HOST, CONF_MAC, CONF_MODEL, CONF_TOKEN from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.device_registry import format_mac from .const import ( @@ -56,16 +61,16 @@ DEVICE_CLOUD_CONFIG = vol.Schema( ) -class OptionsFlowHandler(config_entries.OptionsFlow): +class OptionsFlowHandler(OptionsFlow): """Options for the component.""" - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + def __init__(self, config_entry: ConfigEntry) -> None: """Init object.""" self.config_entry = config_entry async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Manage the options.""" errors = {} if user_input is not None: @@ -104,7 +109,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow): ) -class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class XiaomiMiioFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a Xiaomi Miio config flow.""" VERSION = 1 @@ -127,7 +132,9 @@ class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Get the options flow.""" return OptionsFlowHandler(config_entry) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Perform reauth upon an authentication error or missing cloud credentials.""" self.host = entry_data[CONF_HOST] self.token = entry_data[CONF_TOKEN] @@ -137,7 +144,7 @@ class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Dialog that informs the user that reauth is required.""" if user_input is not None: return await self.async_step_cloud() @@ -145,13 +152,13 @@ class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" return await self.async_step_cloud() async def async_step_zeroconf( self, discovery_info: zeroconf.ZeroconfServiceInfo - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle zeroconf discovery.""" name = discovery_info.name self.host = discovery_info.host @@ -213,7 +220,7 @@ class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_cloud( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Configure a xiaomi miio device through the Miio Cloud.""" errors = {} if user_input is not None: @@ -290,7 +297,7 @@ class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_select( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle multiple cloud devices found.""" errors: dict[str, str] = {} if user_input is not None: @@ -308,7 +315,7 @@ class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_manual( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Configure a xiaomi miio device Manually.""" errors: dict[str, str] = {} if user_input is not None: @@ -327,7 +334,7 @@ class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_connect( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Connect to a xiaomi miio device.""" errors: dict[str, str] = {} if self.host is None or self.token is None: diff --git a/homeassistant/components/xiaomi_miio/const.py b/homeassistant/components/xiaomi_miio/const.py index ef9668dbee4..d643602531d 100644 --- a/homeassistant/components/xiaomi_miio/const.py +++ b/homeassistant/components/xiaomi_miio/const.py @@ -1,4 +1,5 @@ """Constants for the Xiaomi Miio component.""" + from miio.integrations.vacuum.roborock.vacuum import ( ROCKROBO_E2, ROCKROBO_S4, diff --git a/homeassistant/components/xiaomi_miio/device.py b/homeassistant/components/xiaomi_miio/device.py index 0c87f74a7e6..2f5e6e299e9 100644 --- a/homeassistant/components/xiaomi_miio/device.py +++ b/homeassistant/components/xiaomi_miio/device.py @@ -1,4 +1,5 @@ """Code to handle a Xiaomi Device.""" + import datetime from enum import Enum from functools import partial diff --git a/homeassistant/components/xiaomi_miio/device_tracker.py b/homeassistant/components/xiaomi_miio/device_tracker.py index 977dc29ac42..ba73ccc57f0 100644 --- a/homeassistant/components/xiaomi_miio/device_tracker.py +++ b/homeassistant/components/xiaomi_miio/device_tracker.py @@ -1,4 +1,5 @@ """Support for Xiaomi Mi WiFi Repeater 2.""" + from __future__ import annotations import logging @@ -61,18 +62,14 @@ class XiaomiMiioDeviceScanner(DeviceScanner): async def async_scan_devices(self): """Scan for devices and return a list containing found device IDs.""" - devices = [] try: station_info = await self.hass.async_add_executor_job(self.device.status) _LOGGER.debug("Got new station info: %s", station_info) - - for device in station_info.associated_stations: - devices.append(device["mac"]) - except DeviceException as ex: _LOGGER.error("Unable to fetch the state: %s", ex) + return [] - return devices + return [device["mac"] for device in station_info.associated_stations] async def async_get_device_name(self, device): """Return None. diff --git a/homeassistant/components/xiaomi_miio/diagnostics.py b/homeassistant/components/xiaomi_miio/diagnostics.py index eb823cd5abc..749bea45f96 100644 --- a/homeassistant/components/xiaomi_miio/diagnostics.py +++ b/homeassistant/components/xiaomi_miio/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for Xiaomi Miio.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py index 30383426210..75533513b5e 100644 --- a/homeassistant/components/xiaomi_miio/fan.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -1,4 +1,5 @@ """Support for Xiaomi Mi Air Purifier and Xiaomi Mi Air Humidifier.""" + from __future__ import annotations from abc import abstractmethod diff --git a/homeassistant/components/xiaomi_miio/gateway.py b/homeassistant/components/xiaomi_miio/gateway.py index e1b3aee9ff4..39e8ce503a4 100644 --- a/homeassistant/components/xiaomi_miio/gateway.py +++ b/homeassistant/components/xiaomi_miio/gateway.py @@ -1,4 +1,5 @@ """Code to handle a Xiaomi Gateway.""" + import logging from construct.core import ChecksumError diff --git a/homeassistant/components/xiaomi_miio/humidifier.py b/homeassistant/components/xiaomi_miio/humidifier.py index f2660bef68a..8367b063102 100644 --- a/homeassistant/components/xiaomi_miio/humidifier.py +++ b/homeassistant/components/xiaomi_miio/humidifier.py @@ -1,4 +1,5 @@ """Support for Xiaomi Mi Air Purifier and Xiaomi Mi Air Humidifier with humidifier entity.""" + import logging import math from typing import Any @@ -158,7 +159,7 @@ class XiaomiGenericHumidifier(XiaomiCoordinatedMiioEntity, HumidifierEntity): self._state = False self.async_write_ha_state() - def translate_humidity(self, humidity): + def translate_humidity(self, humidity: float) -> float | None: """Translate the target humidity to the first valid step.""" return ( math.ceil(percentage_to_ranged_value((1, self._humidity_steps), humidity)) @@ -239,7 +240,7 @@ class XiaomiAirHumidifier(XiaomiGenericHumidifier, HumidifierEntity): else None ) - async def async_set_humidity(self, humidity: int) -> None: + async def async_set_humidity(self, humidity: float) -> None: """Set the target humidity of the humidifier and set the mode to auto.""" target_humidity = self.translate_humidity(humidity) if not target_humidity: @@ -317,7 +318,7 @@ class XiaomiAirHumidifierMiot(XiaomiAirHumidifier): ) return None - async def async_set_humidity(self, humidity: int) -> None: + async def async_set_humidity(self, humidity: float) -> None: """Set the target humidity of the humidifier and set the mode to auto.""" target_humidity = self.translate_humidity(humidity) if not target_humidity: @@ -392,7 +393,7 @@ class XiaomiAirHumidifierMjjsq(XiaomiAirHumidifier): return self._target_humidity return None - async def async_set_humidity(self, humidity: int) -> None: + async def async_set_humidity(self, humidity: float) -> None: """Set the target humidity of the humidifier and set the mode to Humidity.""" target_humidity = self.translate_humidity(humidity) if not target_humidity: diff --git a/homeassistant/components/xiaomi_miio/icons.json b/homeassistant/components/xiaomi_miio/icons.json new file mode 100644 index 00000000000..bbd3f6607d7 --- /dev/null +++ b/homeassistant/components/xiaomi_miio/icons.json @@ -0,0 +1,28 @@ +{ + "services": { + "fan_reset_filter": "mdi:refresh", + "fan_set_extra_features": "mdi:cog", + "light_set_scene": "mdi:palette", + "light_set_delayed_turn_off": "mdi:timer", + "light_reminder_on": "mdi:alarm", + "light_reminder_off": "mdi:alarm-off", + "light_night_light_mode_on": "mdi:weather-night", + "light_night_light_mode_off": "mdi:weather-sunny", + "light_eyecare_mode_on": "mdi:eye", + "light_eyecare_mode_off": "mdi:eye-off", + "remote_learn_command": "mdi:remote", + "remote_set_led_on": "mdi:led-on", + "remote_set_led_off": "mdi:led-off", + "switch_set_wifi_led_on": "mdi:wifi", + "switch_set_wifi_led_off": "mdi:wifi-off", + "switch_set_power_price": "mdi:currency-usd", + "switch_set_power_mode": "mdi:power", + "vacuum_remote_control_start": "mdi:play", + "vacuum_remote_control_stop": "mdi:stop", + "vacuum_remote_control_move": "mdi:remote", + "vacuum_remote_control_move_step": "mdi:remote", + "vacuum_clean_zone": "mdi:map-marker", + "vacuum_goto": "mdi:map-marker", + "vacuum_clean_segment": "mdi:map-marker" + } +} diff --git a/homeassistant/components/xiaomi_miio/light.py b/homeassistant/components/xiaomi_miio/light.py index 8d198ae2a8f..cbbf12f9ab1 100644 --- a/homeassistant/components/xiaomi_miio/light.py +++ b/homeassistant/components/xiaomi_miio/light.py @@ -1,4 +1,5 @@ """Support for Xiaomi Philips Lights.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/xiaomi_miio/number.py b/homeassistant/components/xiaomi_miio/number.py index 3e952c1ab3f..a0ae0ea5078 100644 --- a/homeassistant/components/xiaomi_miio/number.py +++ b/homeassistant/components/xiaomi_miio/number.py @@ -1,4 +1,5 @@ """Motor speed support for Xiaomi Mi Air Humidifier.""" + from __future__ import annotations import dataclasses @@ -108,17 +109,12 @@ ATTR_OSCILLATION_ANGLE = "angle" ATTR_VOLUME = "volume" -@dataclass(frozen=True) -class XiaomiMiioNumberMixin: +@dataclass(frozen=True, kw_only=True) +class XiaomiMiioNumberDescription(NumberEntityDescription): """A class that describes number entities.""" method: str - -@dataclass(frozen=True) -class XiaomiMiioNumberDescription(NumberEntityDescription, XiaomiMiioNumberMixin): - """A class that describes number entities.""" - available_with_device_off: bool = True diff --git a/homeassistant/components/xiaomi_miio/remote.py b/homeassistant/components/xiaomi_miio/remote.py index ffae02a2ee4..c1234b77bbc 100644 --- a/homeassistant/components/xiaomi_miio/remote.py +++ b/homeassistant/components/xiaomi_miio/remote.py @@ -1,4 +1,5 @@ """Support for the Xiaomi IR Remote (Chuangmi IR).""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/xiaomi_miio/select.py b/homeassistant/components/xiaomi_miio/select.py index b70dab1921a..bef39535176 100644 --- a/homeassistant/components/xiaomi_miio/select.py +++ b/homeassistant/components/xiaomi_miio/select.py @@ -1,4 +1,5 @@ """Support led_brightness for Mi Air Humidifier.""" + from __future__ import annotations from dataclasses import dataclass, field @@ -210,27 +211,24 @@ async def async_setup_entry( if model not in MODEL_TO_ATTR_MAP: return - entities = [] unique_id = config_entry.unique_id device = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE] coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR] attributes = MODEL_TO_ATTR_MAP[model] - for description in SELECTOR_TYPES: - for attribute in attributes: - if description.key == attribute.attr_name: - entities.append( - XiaomiGenericSelector( - device, - config_entry, - f"{description.key}_{unique_id}", - coordinator, - description, - attribute.enum_class, - ) - ) - - async_add_entities(entities) + async_add_entities( + XiaomiGenericSelector( + device, + config_entry, + f"{description.key}_{unique_id}", + coordinator, + description, + attribute.enum_class, + ) + for description in SELECTOR_TYPES + for attribute in attributes + if description.key == attribute.attr_name + ) class XiaomiSelector(XiaomiCoordinatedMiioEntity, SelectEntity): diff --git a/homeassistant/components/xiaomi_miio/sensor.py b/homeassistant/components/xiaomi_miio/sensor.py index a8435d6a8a1..9f70ef6bb17 100644 --- a/homeassistant/components/xiaomi_miio/sensor.py +++ b/homeassistant/components/xiaomi_miio/sensor.py @@ -1,4 +1,5 @@ """Support for Xiaomi Mi Air Quality Monitor (PM2.5) and Humidifier.""" + from __future__ import annotations from collections.abc import Iterable @@ -830,10 +831,8 @@ async def async_setup_entry( sensors = PURIFIER_MIIO_SENSORS elif model in MODELS_PURIFIER_MIOT: sensors = PURIFIER_MIOT_SENSORS - elif ( - model in MODELS_VACUUM - or model.startswith(ROBOROCK_GENERIC) - or model.startswith(ROCKROBO_GENERIC) + elif model in MODELS_VACUUM or model.startswith( + (ROBOROCK_GENERIC, ROCKROBO_GENERIC) ): return _setup_vacuum_sensors(hass, config_entry, async_add_entities) diff --git a/homeassistant/components/xiaomi_miio/switch.py b/homeassistant/components/xiaomi_miio/switch.py index 68714f1a6ff..02517d00c57 100644 --- a/homeassistant/components/xiaomi_miio/switch.py +++ b/homeassistant/components/xiaomi_miio/switch.py @@ -1,4 +1,5 @@ """Support for Xiaomi Smart WiFi Socket and Smart Power Strip.""" + from __future__ import annotations import asyncio @@ -219,21 +220,14 @@ MODEL_TO_FEATURES_MAP = { } -@dataclass(frozen=True) -class XiaomiMiioSwitchRequiredKeyMixin: +@dataclass(frozen=True, kw_only=True) +class XiaomiMiioSwitchDescription(SwitchEntityDescription): """A class that describes switch entities.""" feature: int method_on: str method_off: str - -@dataclass(frozen=True) -class XiaomiMiioSwitchDescription( - SwitchEntityDescription, XiaomiMiioSwitchRequiredKeyMixin -): - """A class that describes switch entities.""" - available_with_device_off: bool = True @@ -355,7 +349,6 @@ async def async_setup_entry( async def async_setup_coordinated_entry(hass, config_entry, async_add_entities): """Set up the coordinated switch from a config entry.""" - entities = [] model = config_entry.data[CONF_MODEL] unique_id = config_entry.unique_id device = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE] @@ -377,19 +370,17 @@ async def async_setup_coordinated_entry(hass, config_entry, async_add_entities): elif model in MODELS_PURIFIER_MIOT: device_features = FEATURE_FLAGS_AIRPURIFIER_MIOT - for description in SWITCH_TYPES: - if description.feature & device_features: - entities.append( - XiaomiGenericCoordinatedSwitch( - device, - config_entry, - f"{description.key}_{unique_id}", - coordinator, - description, - ) - ) - - async_add_entities(entities) + async_add_entities( + XiaomiGenericCoordinatedSwitch( + device, + config_entry, + f"{description.key}_{unique_id}", + coordinator, + description, + ) + for description in SWITCH_TYPES + if description.feature & device_features + ) async def async_setup_other_entry(hass, config_entry, async_add_entities): diff --git a/homeassistant/components/xiaomi_miio/vacuum.py b/homeassistant/components/xiaomi_miio/vacuum.py index 73e2e54b62f..41f2c2386e1 100644 --- a/homeassistant/components/xiaomi_miio/vacuum.py +++ b/homeassistant/components/xiaomi_miio/vacuum.py @@ -1,4 +1,5 @@ """Support for the Xiaomi vacuum cleaner robot.""" + from __future__ import annotations from functools import partial diff --git a/homeassistant/components/xiaomi_tv/media_player.py b/homeassistant/components/xiaomi_tv/media_player.py index 82444c26f65..da692d21bfc 100644 --- a/homeassistant/components/xiaomi_tv/media_player.py +++ b/homeassistant/components/xiaomi_tv/media_player.py @@ -1,4 +1,5 @@ """Add support for the Xiaomi TVs.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/xmpp/notify.py b/homeassistant/components/xmpp/notify.py index 0150e761838..50797536aee 100644 --- a/homeassistant/components/xmpp/notify.py +++ b/homeassistant/components/xmpp/notify.py @@ -1,4 +1,5 @@ """Jabber (XMPP) notification service.""" + from __future__ import annotations from concurrent.futures import TimeoutError as FutTimeoutError diff --git a/homeassistant/components/xs1/__init__.py b/homeassistant/components/xs1/__init__.py index 67452ce9426..e24fbc0181e 100644 --- a/homeassistant/components/xs1/__init__.py +++ b/homeassistant/components/xs1/__init__.py @@ -1,4 +1,5 @@ """Support for the EZcontrol XS1 gateway.""" + import asyncio import logging diff --git a/homeassistant/components/xs1/climate.py b/homeassistant/components/xs1/climate.py index 949d2330347..e594f32adff 100644 --- a/homeassistant/components/xs1/climate.py +++ b/homeassistant/components/xs1/climate.py @@ -1,4 +1,5 @@ """Support for XS1 climate devices.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/xs1/sensor.py b/homeassistant/components/xs1/sensor.py index 4855c8b8dcf..e98fd33743b 100644 --- a/homeassistant/components/xs1/sensor.py +++ b/homeassistant/components/xs1/sensor.py @@ -1,4 +1,5 @@ """Support for XS1 sensors.""" + from __future__ import annotations from xs1_api_client.api_constants import ActuatorType diff --git a/homeassistant/components/xs1/switch.py b/homeassistant/components/xs1/switch.py index 0b231a0303c..c2af652d6ad 100644 --- a/homeassistant/components/xs1/switch.py +++ b/homeassistant/components/xs1/switch.py @@ -1,4 +1,5 @@ """Support for XS1 switches.""" + from __future__ import annotations from typing import Any @@ -22,14 +23,12 @@ def setup_platform( """Set up the XS1 switch platform.""" actuators = hass.data[COMPONENT_DOMAIN][ACTUATORS] - switch_entities = [] - for actuator in actuators: - if (actuator.type() == ActuatorType.SWITCH) or ( - actuator.type() == ActuatorType.DIMMER - ): - switch_entities.append(XS1SwitchEntity(actuator)) - - add_entities(switch_entities) + add_entities( + XS1SwitchEntity(actuator) + for actuator in actuators + if (actuator.type() == ActuatorType.SWITCH) + or (actuator.type() == ActuatorType.DIMMER) + ) class XS1SwitchEntity(XS1DeviceEntity, SwitchEntity): diff --git a/homeassistant/components/yale_smart_alarm/__init__.py b/homeassistant/components/yale_smart_alarm/__init__.py index dac5a98d738..94728ee020c 100644 --- a/homeassistant/components/yale_smart_alarm/__init__.py +++ b/homeassistant/components/yale_smart_alarm/__init__.py @@ -1,4 +1,5 @@ """The yale_smart_alarm component.""" + from __future__ import annotations from homeassistant.components.lock import CONF_DEFAULT_CODE, DOMAIN as LOCK_DOMAIN diff --git a/homeassistant/components/yale_smart_alarm/alarm_control_panel.py b/homeassistant/components/yale_smart_alarm/alarm_control_panel.py index 31851ad3ceb..7cfa6ffe4b9 100644 --- a/homeassistant/components/yale_smart_alarm/alarm_control_panel.py +++ b/homeassistant/components/yale_smart_alarm/alarm_control_panel.py @@ -1,4 +1,5 @@ """Support for Yale Alarm.""" + from __future__ import annotations from typing import TYPE_CHECKING @@ -82,8 +83,6 @@ class YaleAlarmDevice(YaleAlarmEntity, AlarmControlPanelEntity): ) except YALE_ALL_ERRORS as error: raise HomeAssistantError( - f"Could not set alarm for {self.coordinator.entry.data[CONF_NAME]}:" - f" {error}", translation_domain=DOMAIN, translation_key="set_alarm", translation_placeholders={ @@ -97,7 +96,6 @@ class YaleAlarmDevice(YaleAlarmEntity, AlarmControlPanelEntity): self.async_write_ha_state() return raise HomeAssistantError( - "Could not change alarm, check system ready for arming", translation_domain=DOMAIN, translation_key="could_not_change_alarm", ) diff --git a/homeassistant/components/yale_smart_alarm/binary_sensor.py b/homeassistant/components/yale_smart_alarm/binary_sensor.py index 2aad449a3f7..67fe1d74293 100644 --- a/homeassistant/components/yale_smart_alarm/binary_sensor.py +++ b/homeassistant/components/yale_smart_alarm/binary_sensor.py @@ -1,4 +1,5 @@ """Binary sensors for Yale Alarm.""" + from __future__ import annotations from homeassistant.components.binary_sensor import ( @@ -51,11 +52,12 @@ async def async_setup_entry( coordinator: YaleDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ COORDINATOR ] - sensors: list[YaleDoorSensor | YaleProblemSensor] = [] - for data in coordinator.data["door_windows"]: - sensors.append(YaleDoorSensor(coordinator, data)) - for description in SENSOR_TYPES: - sensors.append(YaleProblemSensor(coordinator, description)) + sensors: list[YaleDoorSensor | YaleProblemSensor] = [ + YaleDoorSensor(coordinator, data) for data in coordinator.data["door_windows"] + ] + sensors.extend( + YaleProblemSensor(coordinator, description) for description in SENSOR_TYPES + ) async_add_entities(sensors) diff --git a/homeassistant/components/yale_smart_alarm/button.py b/homeassistant/components/yale_smart_alarm/button.py index 901cd1863ee..54fc905d1aa 100644 --- a/homeassistant/components/yale_smart_alarm/button.py +++ b/homeassistant/components/yale_smart_alarm/button.py @@ -1,4 +1,5 @@ """Support for Yale Smart Alarm button.""" + from __future__ import annotations from typing import TYPE_CHECKING @@ -16,7 +17,6 @@ BUTTON_TYPES = ( ButtonEntityDescription( key="panic", translation_key="panic", - icon="mdi:alarm-light", ), ) diff --git a/homeassistant/components/yale_smart_alarm/config_flow.py b/homeassistant/components/yale_smart_alarm/config_flow.py index ff813d43d78..644160a8d93 100644 --- a/homeassistant/components/yale_smart_alarm/config_flow.py +++ b/homeassistant/components/yale_smart_alarm/config_flow.py @@ -1,4 +1,5 @@ """Adds config flow for Yale Smart Alarm integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -8,10 +9,14 @@ import voluptuous as vol from yalesmartalarmclient.client import YaleSmartAlarmClient from yalesmartalarmclient.exceptions import AuthenticationError -from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult import homeassistant.helpers.config_validation as cv from .const import ( @@ -54,14 +59,16 @@ class YaleConfigFlow(ConfigFlow, domain=DOMAIN): """Get the options flow for this handler.""" return YaleOptionsFlowHandler(config_entry) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Handle initiation of re-authentication with Yale.""" self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) return await self.async_step_reauth_confirm() async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Dialog that informs the user that reauth is required.""" errors = {} @@ -102,7 +109,7 @@ class YaleConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors = {} @@ -153,7 +160,7 @@ class YaleOptionsFlowHandler(OptionsFlow): async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Manage Yale options.""" errors: dict[str, Any] = {} diff --git a/homeassistant/components/yale_smart_alarm/const.py b/homeassistant/components/yale_smart_alarm/const.py index 71b34a3011a..58126449e53 100644 --- a/homeassistant/components/yale_smart_alarm/const.py +++ b/homeassistant/components/yale_smart_alarm/const.py @@ -1,4 +1,5 @@ """Yale integration constants.""" + import logging from yalesmartalarmclient.client import ( diff --git a/homeassistant/components/yale_smart_alarm/coordinator.py b/homeassistant/components/yale_smart_alarm/coordinator.py index e1cff8fb2a5..642704b637d 100644 --- a/homeassistant/components/yale_smart_alarm/coordinator.py +++ b/homeassistant/components/yale_smart_alarm/coordinator.py @@ -1,4 +1,5 @@ """DataUpdateCoordinator for the Yale integration.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/yale_smart_alarm/diagnostics.py b/homeassistant/components/yale_smart_alarm/diagnostics.py index c650ff5f5ed..99ec977de20 100644 --- a/homeassistant/components/yale_smart_alarm/diagnostics.py +++ b/homeassistant/components/yale_smart_alarm/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for Yale Smart Alarm.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/yale_smart_alarm/icons.json b/homeassistant/components/yale_smart_alarm/icons.json new file mode 100644 index 00000000000..4cb5888a406 --- /dev/null +++ b/homeassistant/components/yale_smart_alarm/icons.json @@ -0,0 +1,9 @@ +{ + "entity": { + "button": { + "panic": { + "default": "mdi:alarm-light" + } + } + } +} diff --git a/homeassistant/components/yale_smart_alarm/lock.py b/homeassistant/components/yale_smart_alarm/lock.py index c5a9bb79ba8..7a7b3aa4af4 100644 --- a/homeassistant/components/yale_smart_alarm/lock.py +++ b/homeassistant/components/yale_smart_alarm/lock.py @@ -1,4 +1,5 @@ """Lock for Yale Alarm.""" + from __future__ import annotations from typing import TYPE_CHECKING, Any @@ -79,7 +80,6 @@ class YaleDoorlock(YaleEntity, LockEntity): ) except YALE_ALL_ERRORS as error: raise HomeAssistantError( - f"Could not set lock for {self.lock_name}: {error}", translation_domain=DOMAIN, translation_key="set_lock", translation_placeholders={ @@ -93,7 +93,6 @@ class YaleDoorlock(YaleEntity, LockEntity): self.async_write_ha_state() return raise HomeAssistantError( - "Could not set lock, check system ready for lock", translation_domain=DOMAIN, translation_key="could_not_change_lock", ) diff --git a/homeassistant/components/yalexs_ble/__init__.py b/homeassistant/components/yalexs_ble/__init__.py index 5082029af29..f608da6cd60 100644 --- a/homeassistant/components/yalexs_ble/__init__.py +++ b/homeassistant/components/yalexs_ble/__init__.py @@ -1,4 +1,5 @@ """The Yale Access Bluetooth integration.""" + from __future__ import annotations from yalexs_ble import ( @@ -126,7 +127,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(_async_update_listener)) entry.async_on_unload( - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_shutdown) + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, _async_shutdown, run_immediately=True + ) ) return True diff --git a/homeassistant/components/yalexs_ble/binary_sensor.py b/homeassistant/components/yalexs_ble/binary_sensor.py index 8213baf33aa..a127aa66b93 100644 --- a/homeassistant/components/yalexs_ble/binary_sensor.py +++ b/homeassistant/components/yalexs_ble/binary_sensor.py @@ -1,4 +1,5 @@ """Support for yalexs ble binary sensors.""" + from __future__ import annotations from yalexs_ble import ConnectionInfo, DoorStatus, LockInfo, LockState diff --git a/homeassistant/components/yalexs_ble/config_flow.py b/homeassistant/components/yalexs_ble/config_flow.py index ecfbd45f36e..3ec7f675d7a 100644 --- a/homeassistant/components/yalexs_ble/config_flow.py +++ b/homeassistant/components/yalexs_ble/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Yale Access Bluetooth integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -16,15 +17,20 @@ from yalexs_ble import ( ) from yalexs_ble.const import YALE_MFR_ID -from homeassistant import config_entries, data_entry_flow from homeassistant.components.bluetooth import ( BluetoothServiceInfoBleak, async_ble_device_from_address, async_discovered_service_info, ) +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.const import CONF_ADDRESS from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult +from homeassistant.data_entry_flow import AbortFlow from homeassistant.helpers.typing import DiscoveryInfoType from .const import CONF_ALWAYS_CONNECTED, CONF_KEY, CONF_LOCAL_NAME, CONF_SLOT, DOMAIN @@ -57,7 +63,7 @@ async def async_validate_lock_or_error( return {} -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class YalexsConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Yale Access Bluetooth.""" VERSION = 1 @@ -67,11 +73,11 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._discovery_info: BluetoothServiceInfoBleak | None = None self._discovered_devices: dict[str, BluetoothServiceInfoBleak] = {} self._lock_cfg: ValidatedLockConfig | None = None - self._reauth_entry: config_entries.ConfigEntry | None = None + self._reauth_entry: ConfigEntry | None = None async def async_step_bluetooth( self, discovery_info: BluetoothServiceInfoBleak - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the bluetooth discovery step.""" await self.async_set_unique_id(discovery_info.address) self._abort_if_unique_id_configured() @@ -86,7 +92,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_integration_discovery( self, discovery_info: DiscoveryInfoType - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a discovered integration.""" lock_cfg = ValidatedLockConfig( discovery_info["name"], @@ -137,7 +143,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): # and entered the keys. We abort the discovery flow since # we assume they do not want to use the discovered keys for # some reason. - raise data_entry_flow.AbortFlow("already_in_progress") + raise AbortFlow("already_in_progress") hass.config_entries.flow.async_abort(progress["flow_id"]) self._lock_cfg = lock_cfg @@ -150,7 +156,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_integration_discovery_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a confirmation of discovered integration.""" assert self._discovery_info is not None assert self._lock_cfg is not None @@ -174,7 +180,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): }, ) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Handle configuration by re-auth.""" self._reauth_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] @@ -183,7 +191,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_reauth_validate( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle reauth and validation.""" errors = {} reauth_entry = self._reauth_entry @@ -221,7 +229,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the user step to pick discovered device.""" errors: dict[str, str] = {} @@ -296,28 +304,28 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback def async_get_options_flow( - config_entry: config_entries.ConfigEntry, + config_entry: ConfigEntry, ) -> YaleXSBLEOptionsFlowHandler: """Get the options flow for this handler.""" return YaleXSBLEOptionsFlowHandler(config_entry) -class YaleXSBLEOptionsFlowHandler(config_entries.OptionsFlow): +class YaleXSBLEOptionsFlowHandler(OptionsFlow): """Handle YaleXSBLE options.""" - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + def __init__(self, config_entry: ConfigEntry) -> None: """Initialize YaleXSBLE options flow.""" self.entry = config_entry async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Manage the YaleXSBLE options.""" return await self.async_step_device_options() async def async_step_device_options( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Manage the YaleXSBLE devices options.""" if user_input is not None: return self.async_create_entry( diff --git a/homeassistant/components/yalexs_ble/entity.py b/homeassistant/components/yalexs_ble/entity.py index 9135f0c0896..afa80b8e313 100644 --- a/homeassistant/components/yalexs_ble/entity.py +++ b/homeassistant/components/yalexs_ble/entity.py @@ -1,4 +1,5 @@ """The yalexs_ble integration entities.""" + from __future__ import annotations from yalexs_ble import ConnectionInfo, LockInfo, LockState diff --git a/homeassistant/components/yalexs_ble/lock.py b/homeassistant/components/yalexs_ble/lock.py index f6fa1917d7e..9f508b1a8ee 100644 --- a/homeassistant/components/yalexs_ble/lock.py +++ b/homeassistant/components/yalexs_ble/lock.py @@ -1,4 +1,5 @@ """Support for Yale Access Bluetooth locks.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/yalexs_ble/models.py b/homeassistant/components/yalexs_ble/models.py index 3b83b52cf73..cc6b3697e72 100644 --- a/homeassistant/components/yalexs_ble/models.py +++ b/homeassistant/components/yalexs_ble/models.py @@ -1,4 +1,5 @@ """The yalexs_ble integration models.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/homeassistant/components/yalexs_ble/sensor.py b/homeassistant/components/yalexs_ble/sensor.py index da698d1b501..1fc0601996e 100644 --- a/homeassistant/components/yalexs_ble/sensor.py +++ b/homeassistant/components/yalexs_ble/sensor.py @@ -1,4 +1,5 @@ """Support for yalexs ble sensors.""" + from __future__ import annotations from collections.abc import Callable @@ -27,20 +28,13 @@ from .entity import YALEXSBLEEntity from .models import YaleXSBLEData -@dataclass(frozen=True) -class YaleXSBLERequiredKeysMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class YaleXSBLESensorEntityDescription(SensorEntityDescription): + """Describes Yale Access Bluetooth sensor entity.""" value_fn: Callable[[LockState, LockInfo, ConnectionInfo], int | float | None] -@dataclass(frozen=True) -class YaleXSBLESensorEntityDescription( - SensorEntityDescription, YaleXSBLERequiredKeysMixin -): - """Describes Yale Access Bluetooth sensor entity.""" - - SENSORS: tuple[YaleXSBLESensorEntityDescription, ...] = ( YaleXSBLESensorEntityDescription( key="", # No key for the original RSSI sensor unique id diff --git a/homeassistant/components/yalexs_ble/util.py b/homeassistant/components/yalexs_ble/util.py index e361e141a42..328aa2b6375 100644 --- a/homeassistant/components/yalexs_ble/util.py +++ b/homeassistant/components/yalexs_ble/util.py @@ -1,4 +1,5 @@ """The yalexs_ble integration models.""" + from __future__ import annotations import platform diff --git a/homeassistant/components/yamaha/const.py b/homeassistant/components/yamaha/const.py index bcfdc55a511..c0f4e34dd50 100644 --- a/homeassistant/components/yamaha/const.py +++ b/homeassistant/components/yamaha/const.py @@ -1,4 +1,5 @@ """Constants for the Yamaha component.""" + DOMAIN = "yamaha" CURSOR_TYPE_DOWN = "down" CURSOR_TYPE_LEFT = "left" diff --git a/homeassistant/components/yamaha/icons.json b/homeassistant/components/yamaha/icons.json new file mode 100644 index 00000000000..f7075508b0d --- /dev/null +++ b/homeassistant/components/yamaha/icons.json @@ -0,0 +1,7 @@ +{ + "services": { + "enable_output": "mdi:audio-input-stereo-minijack", + "menu_cursor": "mdi:cursor-default", + "select_scene": "mdi:palette" + } +} diff --git a/homeassistant/components/yamaha/media_player.py b/homeassistant/components/yamaha/media_player.py index e2658c21f37..c648994c38d 100644 --- a/homeassistant/components/yamaha/media_player.py +++ b/homeassistant/components/yamaha/media_player.py @@ -1,4 +1,5 @@ """Support for Yamaha Receivers.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/yamaha_musiccast/__init__.py b/homeassistant/components/yamaha_musiccast/__init__.py index 5242aa90819..667b411e6c4 100644 --- a/homeassistant/components/yamaha_musiccast/__init__.py +++ b/homeassistant/components/yamaha_musiccast/__init__.py @@ -1,4 +1,5 @@ """The MusicCast integration.""" + from __future__ import annotations from datetime import timedelta @@ -119,7 +120,7 @@ class MusicCastDataUpdateCoordinator(DataUpdateCoordinator[MusicCastData]): # p try: await self.musiccast.fetch() except MusicCastConnectionException as exception: - raise UpdateFailed() from exception + raise UpdateFailed from exception return self.musiccast.data diff --git a/homeassistant/components/yamaha_musiccast/config_flow.py b/homeassistant/components/yamaha_musiccast/config_flow.py index b64f5aba6b7..34d352b790e 100644 --- a/homeassistant/components/yamaha_musiccast/config_flow.py +++ b/homeassistant/components/yamaha_musiccast/config_flow.py @@ -1,4 +1,5 @@ """Config flow for MusicCast.""" + from __future__ import annotations import logging @@ -32,7 +33,7 @@ class MusicCastFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> data_entry_flow.FlowResult: + ) -> data_entry_flow.ConfigFlowResult: """Handle a flow initiated by the user.""" # Request user input, unless we are preparing discovery flow if user_input is None: @@ -74,7 +75,7 @@ class MusicCastFlowHandler(ConfigFlow, domain=DOMAIN): def _show_setup_form( self, errors: dict | None = None - ) -> data_entry_flow.FlowResult: + ) -> data_entry_flow.ConfigFlowResult: """Show the setup form to the user.""" return self.async_show_form( step_id="user", @@ -84,7 +85,7 @@ class MusicCastFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_ssdp( self, discovery_info: ssdp.SsdpServiceInfo - ) -> data_entry_flow.FlowResult: + ) -> data_entry_flow.ConfigFlowResult: """Handle ssdp discoveries.""" if not await MusicCastDevice.check_yamaha_ssdp( discovery_info.ssdp_location, async_get_clientsession(self.hass) @@ -116,7 +117,9 @@ class MusicCastFlowHandler(ConfigFlow, domain=DOMAIN): return await self.async_step_confirm() - async def async_step_confirm(self, user_input=None) -> data_entry_flow.FlowResult: + async def async_step_confirm( + self, user_input=None + ) -> data_entry_flow.ConfigFlowResult: """Allow the user to confirm adding the device.""" if user_input is not None: return self.async_create_entry( diff --git a/homeassistant/components/yamaha_musiccast/media_player.py b/homeassistant/components/yamaha_musiccast/media_player.py index 42549fb20d9..3edf524c8ad 100644 --- a/homeassistant/components/yamaha_musiccast/media_player.py +++ b/homeassistant/components/yamaha_musiccast/media_player.py @@ -1,4 +1,5 @@ """Implementation of the musiccast media player.""" + from __future__ import annotations import contextlib @@ -675,7 +676,7 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): return [self] entities = self.get_all_mc_entities() clients = [entity for entity in entities if entity.is_part_of_group(self)] - return [self] + clients + return [self, *clients] @property def musiccast_zone_entity(self) -> MusicCastMediaPlayer: @@ -900,13 +901,13 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): return _LOGGER.debug("%s updates his group members", self.entity_id) - client_ips_for_removal = [] - for expected_client_ip in self.coordinator.data.group_client_list: - if expected_client_ip not in [ - entity.ip_address for entity in self.musiccast_group - ]: - # The client is no longer part of the group. Prepare removal. - client_ips_for_removal.append(expected_client_ip) + client_ips_for_removal = [ + expected_client_ip + for expected_client_ip in self.coordinator.data.group_client_list + # The client is no longer part of the group. Prepare removal. + if expected_client_ip + not in [entity.ip_address for entity in self.musiccast_group] + ] if client_ips_for_removal: _LOGGER.debug( @@ -926,4 +927,4 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): @callback def async_schedule_check_client_list(self): """Schedule async_check_client_list.""" - self.hass.create_task(self.async_check_client_list()) + self.hass.async_create_task(self.async_check_client_list(), eager_start=True) diff --git a/homeassistant/components/yamaha_musiccast/number.py b/homeassistant/components/yamaha_musiccast/number.py index 105cb0edb3a..a5a591379c6 100644 --- a/homeassistant/components/yamaha_musiccast/number.py +++ b/homeassistant/components/yamaha_musiccast/number.py @@ -1,4 +1,5 @@ """Number entities for musiccast.""" + from __future__ import annotations from aiomusiccast.capabilities import NumberSetter @@ -19,16 +20,18 @@ async def async_setup_entry( """Set up MusicCast number entities based on a config entry.""" coordinator: MusicCastDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - number_entities = [] + number_entities = [ + NumberCapability(coordinator, capability) + for capability in coordinator.data.capabilities + if isinstance(capability, NumberSetter) + ] - for capability in coordinator.data.capabilities: - if isinstance(capability, NumberSetter): - number_entities.append(NumberCapability(coordinator, capability)) - - for zone, data in coordinator.data.zones.items(): - for capability in data.capabilities: - if isinstance(capability, NumberSetter): - number_entities.append(NumberCapability(coordinator, capability, zone)) + number_entities.extend( + NumberCapability(coordinator, capability, zone) + for zone, data in coordinator.data.zones.items() + for capability in data.capabilities + if isinstance(capability, NumberSetter) + ) async_add_entities(number_entities) diff --git a/homeassistant/components/yamaha_musiccast/select.py b/homeassistant/components/yamaha_musiccast/select.py index 8ef9df1ba2f..b068b956e1b 100644 --- a/homeassistant/components/yamaha_musiccast/select.py +++ b/homeassistant/components/yamaha_musiccast/select.py @@ -1,4 +1,5 @@ """The select entities for musiccast.""" + from __future__ import annotations from aiomusiccast.capabilities import OptionSetter @@ -20,18 +21,18 @@ async def async_setup_entry( """Set up MusicCast select entities based on a config entry.""" coordinator: MusicCastDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - select_entities = [] + select_entities = [ + SelectableCapability(coordinator, capability) + for capability in coordinator.data.capabilities + if isinstance(capability, OptionSetter) + ] - for capability in coordinator.data.capabilities: - if isinstance(capability, OptionSetter): - select_entities.append(SelectableCapability(coordinator, capability)) - - for zone, data in coordinator.data.zones.items(): - for capability in data.capabilities: - if isinstance(capability, OptionSetter): - select_entities.append( - SelectableCapability(coordinator, capability, zone) - ) + select_entities.extend( + SelectableCapability(coordinator, capability, zone) + for zone, data in coordinator.data.zones.items() + for capability in data.capabilities + if isinstance(capability, OptionSetter) + ) async_add_entities(select_entities) diff --git a/homeassistant/components/yamaha_musiccast/switch.py b/homeassistant/components/yamaha_musiccast/switch.py index f48e7c11713..2ae8388027a 100644 --- a/homeassistant/components/yamaha_musiccast/switch.py +++ b/homeassistant/components/yamaha_musiccast/switch.py @@ -1,4 +1,5 @@ """The switch entities for musiccast.""" + from typing import Any from aiomusiccast.capabilities import BinarySetter @@ -19,16 +20,18 @@ async def async_setup_entry( """Set up MusicCast sensor based on a config entry.""" coordinator: MusicCastDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - switch_entities = [] + switch_entities = [ + SwitchCapability(coordinator, capability) + for capability in coordinator.data.capabilities + if isinstance(capability, BinarySetter) + ] - for capability in coordinator.data.capabilities: - if isinstance(capability, BinarySetter): - switch_entities.append(SwitchCapability(coordinator, capability)) - - for zone, data in coordinator.data.zones.items(): - for capability in data.capabilities: - if isinstance(capability, BinarySetter): - switch_entities.append(SwitchCapability(coordinator, capability, zone)) + switch_entities.extend( + SwitchCapability(coordinator, capability, zone) + for zone, data in coordinator.data.zones.items() + for capability in data.capabilities + if isinstance(capability, BinarySetter) + ) async_add_entities(switch_entities) diff --git a/homeassistant/components/yandex_transport/sensor.py b/homeassistant/components/yandex_transport/sensor.py index 1fbae6c88a6..bcef8248aa3 100644 --- a/homeassistant/components/yandex_transport/sensor.py +++ b/homeassistant/components/yandex_transport/sensor.py @@ -1,4 +1,5 @@ """Service for obtaining information about closer bus from Transport Yandex Service.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/yandextts/tts.py b/homeassistant/components/yandextts/tts.py index ca4f8400022..1a5fc4a7903 100644 --- a/homeassistant/components/yandextts/tts.py +++ b/homeassistant/components/yandextts/tts.py @@ -1,4 +1,5 @@ """Support for the yandex speechkit tts service.""" + import asyncio from http import HTTPStatus import logging diff --git a/homeassistant/components/yardian/__init__.py b/homeassistant/components/yardian/__init__.py index d6cee9015b8..eafdc8e68db 100644 --- a/homeassistant/components/yardian/__init__.py +++ b/homeassistant/components/yardian/__init__.py @@ -1,4 +1,5 @@ """The Yardian integration.""" + from __future__ import annotations from pyyardian import AsyncYardianClient diff --git a/homeassistant/components/yardian/config_flow.py b/homeassistant/components/yardian/config_flow.py index 99258965f21..e23ca536d4e 100644 --- a/homeassistant/components/yardian/config_flow.py +++ b/homeassistant/components/yardian/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Yardian integration.""" + from __future__ import annotations import logging @@ -12,9 +13,8 @@ from pyyardian import ( ) import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN, PRODUCT_NAME @@ -29,7 +29,7 @@ STEP_USER_DATA_SCHEMA = vol.Schema( ) -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class YardianConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Yardian.""" VERSION = 1 @@ -45,7 +45,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors: dict[str, str] = {} if user_input is not None: diff --git a/homeassistant/components/yardian/icons.json b/homeassistant/components/yardian/icons.json new file mode 100644 index 00000000000..79bcc32adf2 --- /dev/null +++ b/homeassistant/components/yardian/icons.json @@ -0,0 +1,12 @@ +{ + "entity": { + "switch": { + "switch": { + "default": "mdi:water" + } + } + }, + "services": { + "start_irrigation": "mdi:water" + } +} diff --git a/homeassistant/components/yardian/switch.py b/homeassistant/components/yardian/switch.py index 8598e4a8732..549331b6b5f 100644 --- a/homeassistant/components/yardian/switch.py +++ b/homeassistant/components/yardian/switch.py @@ -1,4 +1,5 @@ """Support for Yardian integration.""" + from __future__ import annotations from typing import Any @@ -47,8 +48,8 @@ async def async_setup_entry( class YardianSwitch(CoordinatorEntity[YardianUpdateCoordinator], SwitchEntity): """Representation of a Yardian switch.""" - _attr_icon = "mdi:water" _attr_has_entity_name = True + _attr_translation_key = "switch" def __init__(self, coordinator: YardianUpdateCoordinator, zone_id) -> None: """Initialize a Yardian Switch Device.""" diff --git a/homeassistant/components/yeelight/__init__.py b/homeassistant/components/yeelight/__init__.py index f77e4d08dc9..0ed75318ac7 100644 --- a/homeassistant/components/yeelight/__init__.py +++ b/homeassistant/components/yeelight/__init__.py @@ -1,4 +1,5 @@ """Support for Xiaomi Yeelight WiFi color bulb.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/yeelight/binary_sensor.py b/homeassistant/components/yeelight/binary_sensor.py index 88779e03b6c..9993272d510 100644 --- a/homeassistant/components/yeelight/binary_sensor.py +++ b/homeassistant/components/yeelight/binary_sensor.py @@ -1,4 +1,5 @@ """Sensor platform support for yeelight.""" + import logging from homeassistant.components.binary_sensor import BinarySensorEntity diff --git a/homeassistant/components/yeelight/config_flow.py b/homeassistant/components/yeelight/config_flow.py index 43f90511893..d7bf4e25996 100644 --- a/homeassistant/components/yeelight/config_flow.py +++ b/homeassistant/components/yeelight/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Yeelight integration.""" + from __future__ import annotations import logging @@ -9,12 +10,17 @@ import yeelight from yeelight.aio import AsyncBulb from yeelight.main import get_known_models -from homeassistant import config_entries, exceptions from homeassistant.components import dhcp, onboarding, ssdp, zeroconf -from homeassistant.config_entries import ConfigEntry, ConfigEntryState +from homeassistant.config_entries import ( + ConfigEntry, + ConfigEntryState, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) from homeassistant.const import CONF_DEVICE, CONF_HOST, CONF_ID, CONF_MODEL, CONF_NAME from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult +from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv from .const import ( @@ -40,7 +46,7 @@ MODEL_UNKNOWN = "unknown" _LOGGER = logging.getLogger(__name__) -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class YeelightConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Yeelight.""" VERSION = 1 @@ -59,19 +65,21 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_homekit( self, discovery_info: zeroconf.ZeroconfServiceInfo - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle discovery from homekit.""" self._discovered_ip = discovery_info.host return await self._async_handle_discovery() - async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: + async def async_step_dhcp( + self, discovery_info: dhcp.DhcpServiceInfo + ) -> ConfigFlowResult: """Handle discovery from dhcp.""" self._discovered_ip = discovery_info.ip return await self._async_handle_discovery() async def async_step_zeroconf( self, discovery_info: zeroconf.ZeroconfServiceInfo - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle discovery from zeroconf.""" self._discovered_ip = discovery_info.host await self.async_set_unique_id( @@ -79,7 +87,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) return await self._async_handle_discovery_with_unique_id() - async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: + async def async_step_ssdp( + self, discovery_info: ssdp.SsdpServiceInfo + ) -> ConfigFlowResult: """Handle discovery from ssdp.""" self._discovered_ip = urlparse(discovery_info.ssdp_headers["location"]).hostname await self.async_set_unique_id(discovery_info.ssdp_headers["id"]) @@ -272,7 +282,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return MODEL_UNKNOWN -class OptionsFlowHandler(config_entries.OptionsFlow): +class OptionsFlowHandler(OptionsFlow): """Handle a option flow for Yeelight.""" def __init__(self, config_entry: ConfigEntry) -> None: @@ -323,5 +333,5 @@ class OptionsFlowHandler(config_entries.OptionsFlow): ) -class CannotConnect(exceptions.HomeAssistantError): +class CannotConnect(HomeAssistantError): """Error to indicate we cannot connect.""" diff --git a/homeassistant/components/yeelight/device.py b/homeassistant/components/yeelight/device.py index bb5159c0b3b..c42fd072728 100644 --- a/homeassistant/components/yeelight/device.py +++ b/homeassistant/components/yeelight/device.py @@ -1,4 +1,5 @@ """Support for Xiaomi Yeelight WiFi color bulb.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/yeelight/entity.py b/homeassistant/components/yeelight/entity.py index 8056ea085b7..c0bc45f6a51 100644 --- a/homeassistant/components/yeelight/entity.py +++ b/homeassistant/components/yeelight/entity.py @@ -1,4 +1,5 @@ """Support for Xiaomi Yeelight WiFi color bulb.""" + from __future__ import annotations from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/yeelight/icons.json b/homeassistant/components/yeelight/icons.json new file mode 100644 index 00000000000..bf0d0c497f0 --- /dev/null +++ b/homeassistant/components/yeelight/icons.json @@ -0,0 +1,19 @@ +{ + "entity": { + "light": { + "nightlight": { + "default": "mdi:weather-night" + } + } + }, + "services": { + "set_mode": "mdi:cog", + "set_color_scene": "mdi:palette", + "set_hsv_scene": "mdi:palette", + "set_color_temp_scene": "mdi:palette", + "set_color_flow_scene": "mdi:palette", + "set_auto_delay_off_scene": "mdi:timer", + "start_flow": "mdi:play", + "set_music_mode": "mdi:music" + } +} diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py index abc17b8abd8..ede652dd037 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -1,4 +1,5 @@ """Light platform support for yeelight.""" + from __future__ import annotations from collections.abc import Callable, Coroutine @@ -1001,7 +1002,6 @@ class YeelightNightLightMode(YeelightBaseLight): """Representation of a Yeelight when in nightlight mode.""" _attr_color_mode = ColorMode.BRIGHTNESS - _attr_icon = "mdi:weather-night" _attr_supported_color_modes = {ColorMode.BRIGHTNESS} _attr_translation_key = "nightlight" diff --git a/homeassistant/components/yeelight/manifest.json b/homeassistant/components/yeelight/manifest.json index 20f8ed3ed4d..e9f304d38cb 100644 --- a/homeassistant/components/yeelight/manifest.json +++ b/homeassistant/components/yeelight/manifest.json @@ -17,7 +17,7 @@ "iot_class": "local_push", "loggers": ["async_upnp_client", "yeelight"], "quality_scale": "platinum", - "requirements": ["yeelight==0.7.14", "async-upnp-client==0.38.2"], + "requirements": ["yeelight==0.7.14", "async-upnp-client==0.38.3"], "zeroconf": [ { "type": "_miio._udp.local.", diff --git a/homeassistant/components/yeelight/scanner.py b/homeassistant/components/yeelight/scanner.py index 8fa41bb92b1..6ca12e9bd01 100644 --- a/homeassistant/components/yeelight/scanner.py +++ b/homeassistant/components/yeelight/scanner.py @@ -1,10 +1,12 @@ """Support for Xiaomi Yeelight WiFi color bulb.""" + from __future__ import annotations import asyncio -from collections.abc import Callable, ValuesView +from collections.abc import ValuesView import contextlib from datetime import datetime +from functools import partial from ipaddress import IPv4Address import logging from typing import Self @@ -18,6 +20,7 @@ from homeassistant.components import network, ssdp from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback from homeassistant.helpers import discovery_flow from homeassistant.helpers.event import async_call_later, async_track_time_interval +from homeassistant.util.async_ import create_eager_task from .const import ( DISCOVERY_ATTEMPTS, @@ -32,6 +35,12 @@ from .const import ( _LOGGER = logging.getLogger(__name__) +@callback +def _set_future_if_not_done(future: asyncio.Future[None]) -> None: + if not future.done(): + future.set_result(None) + + class YeelightScanner: """Scan for Yeelight devices.""" @@ -53,26 +62,18 @@ class YeelightScanner: self._host_capabilities: dict[str, CaseInsensitiveDict] = {} self._track_interval: CALLBACK_TYPE | None = None self._listeners: list[SsdpSearchListener] = [] - self._connected_events: list[asyncio.Event] = [] + self._setup_future: asyncio.Future[None] | None = None async def async_setup(self) -> None: """Set up the scanner.""" - if self._connected_events: - await self._async_wait_connected() - return - - for idx, source_ip in enumerate(await self._async_build_source_set()): - self._connected_events.append(asyncio.Event()) - - def _wrap_async_connected_idx(idx) -> Callable[[], None]: - """Create a function to capture the idx cell variable.""" - - @callback - def _async_connected() -> None: - self._connected_events[idx].set() - - return _async_connected + if self._setup_future is not None: + return await self._setup_future + self._setup_future = self._hass.loop.create_future() + connected_futures: list[asyncio.Future[None]] = [] + for source_ip in await self._async_build_source_set(): + future = self._hass.loop.create_future() + connected_futures.append(future) source = (str(source_ip), 0) self._listeners.append( SsdpSearchListener( @@ -80,12 +81,15 @@ class YeelightScanner: search_target=SSDP_ST, target=SSDP_TARGET, source=source, - connect_callback=_wrap_async_connected_idx(idx), + connect_callback=partial(_set_future_if_not_done, future), ) ) results = await asyncio.gather( - *(listener.async_start() for listener in self._listeners), + *( + create_eager_task(listener.async_start()) + for listener in self._listeners + ), return_exceptions=True, ) failed_listeners = [] @@ -98,20 +102,17 @@ class YeelightScanner: result, ) failed_listeners.append(self._listeners[idx]) - self._connected_events[idx].set() + _set_future_if_not_done(connected_futures[idx]) for listener in failed_listeners: self._listeners.remove(listener) - await self._async_wait_connected() + await asyncio.wait(connected_futures) self._track_interval = async_track_time_interval( self._hass, self.async_scan, DISCOVERY_INTERVAL, cancel_on_shutdown=True ) self.async_scan() - - async def _async_wait_connected(self): - """Wait for the listeners to be up and connected.""" - await asyncio.gather(*(event.wait() for event in self._connected_events)) + _set_future_if_not_done(self._setup_future) async def _async_build_source_set(self) -> set[IPv4Address]: """Build the list of ssdp sources.""" diff --git a/homeassistant/components/yeelightsunflower/light.py b/homeassistant/components/yeelightsunflower/light.py index c50c176c157..45b662846d5 100644 --- a/homeassistant/components/yeelightsunflower/light.py +++ b/homeassistant/components/yeelightsunflower/light.py @@ -1,4 +1,5 @@ """Support for Yeelight Sunflower color bulbs (not Yeelight Blue or WiFi).""" + from __future__ import annotations import logging diff --git a/homeassistant/components/yi/camera.py b/homeassistant/components/yi/camera.py index 632260a899c..fbc3294e25d 100644 --- a/homeassistant/components/yi/camera.py +++ b/homeassistant/components/yi/camera.py @@ -1,4 +1,5 @@ """Support for Xiaomi Cameras (HiSilicon Hi3518e V200).""" + from __future__ import annotations import logging diff --git a/homeassistant/components/yolink/__init__.py b/homeassistant/components/yolink/__init__.py index 270bd550038..fec678ce435 100644 --- a/homeassistant/components/yolink/__init__.py +++ b/homeassistant/components/yolink/__init__.py @@ -1,4 +1,5 @@ """The yolink integration.""" + from __future__ import annotations import asyncio @@ -45,6 +46,7 @@ PLATFORMS = [ Platform.SENSOR, Platform.SIREN, Platform.SWITCH, + Platform.VALVE, ] diff --git a/homeassistant/components/yolink/api.py b/homeassistant/components/yolink/api.py index 0991baed23f..fd6f8832faf 100644 --- a/homeassistant/components/yolink/api.py +++ b/homeassistant/components/yolink/api.py @@ -1,4 +1,5 @@ """API for yolink bound to Home Assistant OAuth.""" + from aiohttp import ClientSession from yolink.auth_mgr import YoLinkAuthMgr diff --git a/homeassistant/components/yolink/binary_sensor.py b/homeassistant/components/yolink/binary_sensor.py index 0762a3b5c60..07a1fb07cc0 100644 --- a/homeassistant/components/yolink/binary_sensor.py +++ b/homeassistant/components/yolink/binary_sensor.py @@ -1,4 +1,5 @@ """YoLink BinarySensor.""" + from __future__ import annotations from collections.abc import Callable @@ -49,7 +50,6 @@ SENSOR_DEVICE_TYPE = [ SENSOR_TYPES: tuple[YoLinkBinarySensorEntityDescription, ...] = ( YoLinkBinarySensorEntityDescription( key="door_state", - icon="mdi:door", device_class=BinarySensorDeviceClass.DOOR, value=lambda value: value == "open" if value is not None else None, exists_fn=lambda device: device.device_type == ATTR_DEVICE_DOOR_SENSOR, @@ -99,16 +99,14 @@ async def async_setup_entry( for device_coordinator in device_coordinators.values() if device_coordinator.device.device_type in SENSOR_DEVICE_TYPE ] - entities = [] - for binary_sensor_device_coordinator in binary_sensor_device_coordinators: - for description in SENSOR_TYPES: - if description.exists_fn(binary_sensor_device_coordinator.device): - entities.append( - YoLinkBinarySensorEntity( - config_entry, binary_sensor_device_coordinator, description - ) - ) - async_add_entities(entities) + async_add_entities( + YoLinkBinarySensorEntity( + config_entry, binary_sensor_device_coordinator, description + ) + for binary_sensor_device_coordinator in binary_sensor_device_coordinators + for description in SENSOR_TYPES + if description.exists_fn(binary_sensor_device_coordinator.device) + ) class YoLinkBinarySensorEntity(YoLinkEntity, BinarySensorEntity): diff --git a/homeassistant/components/yolink/climate.py b/homeassistant/components/yolink/climate.py index a1e2fdd90a2..21e0a71ebcb 100644 --- a/homeassistant/components/yolink/climate.py +++ b/homeassistant/components/yolink/climate.py @@ -1,4 +1,5 @@ """YoLink Thermostat.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/yolink/config_flow.py b/homeassistant/components/yolink/config_flow.py index 128cd6cb35c..abdac696248 100644 --- a/homeassistant/components/yolink/config_flow.py +++ b/homeassistant/components/yolink/config_flow.py @@ -1,12 +1,12 @@ """Config flow for yolink.""" + from __future__ import annotations from collections.abc import Mapping import logging from typing import Any -from homeassistant.config_entries import ConfigEntry -from homeassistant.data_entry_flow import FlowResult +from homeassistant.config_entries import ConfigEntry, ConfigFlowResult from homeassistant.helpers import config_entry_oauth2_flow from .const import DOMAIN @@ -31,20 +31,22 @@ class OAuth2FlowHandler( scopes = ["create"] return {"scope": " ".join(scopes)} - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Perform reauth upon an API authentication error.""" self._reauth_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] ) return await self.async_step_reauth_confirm() - async def async_step_reauth_confirm(self, user_input=None) -> FlowResult: + async def async_step_reauth_confirm(self, user_input=None) -> ConfigFlowResult: """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) -> FlowResult: + async def async_oauth_create_entry(self, data: dict) -> ConfigFlowResult: """Create an oauth config entry or update existing entry for reauth.""" if existing_entry := self._reauth_entry: self.hass.config_entries.async_update_entry( @@ -56,7 +58,7 @@ class OAuth2FlowHandler( async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow start.""" existing_entry = await self.async_set_unique_id(DOMAIN) if existing_entry and not self._reauth_entry: diff --git a/homeassistant/components/yolink/const.py b/homeassistant/components/yolink/const.py index 3d341c8b4fb..110b9cb9810 100644 --- a/homeassistant/components/yolink/const.py +++ b/homeassistant/components/yolink/const.py @@ -14,3 +14,5 @@ ATTR_REPEAT = "repeat" ATTR_TONE = "tone" YOLINK_EVENT = f"{DOMAIN}_event" YOLINK_OFFLINE_TIME = 32400 + +DEV_MODEL_WATER_METER_YS5007 = "YS5007" diff --git a/homeassistant/components/yolink/coordinator.py b/homeassistant/components/yolink/coordinator.py index f2c942caab9..b7db36541b1 100644 --- a/homeassistant/components/yolink/coordinator.py +++ b/homeassistant/components/yolink/coordinator.py @@ -1,4 +1,5 @@ """YoLink DataUpdateCoordinator.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/yolink/cover.py b/homeassistant/components/yolink/cover.py index 6cc1ea3acd6..b2454bd0d4a 100644 --- a/homeassistant/components/yolink/cover.py +++ b/homeassistant/components/yolink/cover.py @@ -1,4 +1,5 @@ """YoLink Garage Door.""" + from __future__ import annotations from typing import Any @@ -59,11 +60,13 @@ class YoLinkCoverEntity(YoLinkEntity, CoverEntity): """Update HA Entity State.""" if (state_val := state.get("state")) is None: return - if self.coordinator.paired_device is None: + if self.coordinator.paired_device is None or state_val == "error": self._attr_is_closed = None + self._attr_available = False self.async_write_ha_state() elif state_val in ["open", "closed"]: self._attr_is_closed = state_val == "closed" + self._attr_available = True self.async_write_ha_state() async def toggle_garage_state(self) -> None: diff --git a/homeassistant/components/yolink/device_trigger.py b/homeassistant/components/yolink/device_trigger.py index aac860c6a27..b7f83623be5 100644 --- a/homeassistant/components/yolink/device_trigger.py +++ b/homeassistant/components/yolink/device_trigger.py @@ -1,4 +1,5 @@ """Provides device triggers for YoLink.""" + from __future__ import annotations from typing import Any @@ -54,17 +55,15 @@ async def async_get_triggers( if not registry_device or registry_device.model != ATTR_DEVICE_SMART_REMOTER: return [] - triggers = [] - for trigger in DEVICE_TRIGGER_TYPES[ATTR_DEVICE_SMART_REMOTER]: - triggers.append( - { - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_PLATFORM: "device", - CONF_TYPE: trigger, - } - ) - return triggers + return [ + { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_PLATFORM: "device", + CONF_TYPE: trigger, + } + for trigger in DEVICE_TRIGGER_TYPES[ATTR_DEVICE_SMART_REMOTER] + ] async def async_attach_trigger( diff --git a/homeassistant/components/yolink/entity.py b/homeassistant/components/yolink/entity.py index 0221bd94a7e..d9ca2968493 100644 --- a/homeassistant/components/yolink/entity.py +++ b/homeassistant/components/yolink/entity.py @@ -1,4 +1,5 @@ """Support for YoLink Device.""" + from __future__ import annotations from abc import abstractmethod diff --git a/homeassistant/components/yolink/icons.json b/homeassistant/components/yolink/icons.json new file mode 100644 index 00000000000..ee9037c864a --- /dev/null +++ b/homeassistant/components/yolink/icons.json @@ -0,0 +1,31 @@ +{ + "entity": { + "number": { + "config_volume": { + "default": "mdi:volume-high" + } + }, + "sensor": { + "power_failure_alarm": { + "default": "mdi:flash" + }, + "power_failure_alarm_mute": { + "default": "mdi:volume-mute" + }, + "power_failure_alarm_volume": { + "default": "mdi:volume-high" + }, + "power_failure_alarm_beep": { + "default": "mdi:bullhorn" + } + }, + "switch": { + "manipulator_state": { + "default": "mdi:pipe" + } + } + }, + "services": { + "play_on_speaker_hub": "mdi:speaker" + } +} diff --git a/homeassistant/components/yolink/light.py b/homeassistant/components/yolink/light.py index 248a42df60c..e07d17f7d74 100644 --- a/homeassistant/components/yolink/light.py +++ b/homeassistant/components/yolink/light.py @@ -1,4 +1,5 @@ """YoLink Dimmer.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/yolink/lock.py b/homeassistant/components/yolink/lock.py index 3b0f68c175c..177a8808de1 100644 --- a/homeassistant/components/yolink/lock.py +++ b/homeassistant/components/yolink/lock.py @@ -1,4 +1,5 @@ """YoLink Lock.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/yolink/manifest.json b/homeassistant/components/yolink/manifest.json index aae5be3f9d3..8b3b071161c 100644 --- a/homeassistant/components/yolink/manifest.json +++ b/homeassistant/components/yolink/manifest.json @@ -6,5 +6,5 @@ "dependencies": ["auth", "application_credentials"], "documentation": "https://www.home-assistant.io/integrations/yolink", "iot_class": "cloud_push", - "requirements": ["yolink-api==0.3.7"] + "requirements": ["yolink-api==0.4.1"] } diff --git a/homeassistant/components/yolink/number.py b/homeassistant/components/yolink/number.py index a7ba89e1f6c..7b7b582382b 100644 --- a/homeassistant/components/yolink/number.py +++ b/homeassistant/components/yolink/number.py @@ -23,7 +23,7 @@ from .const import DOMAIN from .coordinator import YoLinkCoordinator from .entity import YoLinkEntity -OPTIONS_VALUME = "options_volume" +OPTIONS_VOLUME = "options_volume" @dataclass(frozen=True, kw_only=True) @@ -49,14 +49,13 @@ def get_volume_value(state: dict[str, Any]) -> int | None: DEVICE_CONFIG_DESCRIPTIONS: tuple[YoLinkNumberTypeConfigEntityDescription, ...] = ( YoLinkNumberTypeConfigEntityDescription( - key=OPTIONS_VALUME, + key=OPTIONS_VOLUME, translation_key="config_volume", native_min_value=1, native_max_value=16, mode=NumberMode.SLIDER, native_step=1.0, native_unit_of_measurement=None, - icon="mdi:volume-high", exists_fn=lambda device: device.device_type in SUPPORT_SET_VOLUME_DEVICES, should_update_entity=lambda value: value is not None, value=get_volume_value, @@ -76,18 +75,16 @@ async def async_setup_entry( for device_coordinator in device_coordinators.values() if device_coordinator.device.device_type in NUMBER_TYPE_CONF_SUPPORT_DEVICES ] - entities = [] - for config_device_coordinator in config_device_coordinators: - for description in DEVICE_CONFIG_DESCRIPTIONS: - if description.exists_fn(config_device_coordinator.device): - entities.append( - YoLinkNumberTypeConfigEntity( - config_entry, - config_device_coordinator, - description, - ) - ) - async_add_entities(entities) + async_add_entities( + YoLinkNumberTypeConfigEntity( + config_entry, + config_device_coordinator, + description, + ) + for config_device_coordinator in config_device_coordinators + for description in DEVICE_CONFIG_DESCRIPTIONS + if description.exists_fn(config_device_coordinator.device) + ) class YoLinkNumberTypeConfigEntity(YoLinkEntity, NumberEntity): @@ -124,7 +121,7 @@ class YoLinkNumberTypeConfigEntity(YoLinkEntity, NumberEntity): """Update the current value.""" if ( self.coordinator.device.device_type == ATTR_DEVICE_SPEAKER_HUB - and self.entity_description.key == OPTIONS_VALUME + and self.entity_description.key == OPTIONS_VOLUME ): await self.update_speaker_hub_volume(value) self._attr_native_value = value diff --git a/homeassistant/components/yolink/sensor.py b/homeassistant/components/yolink/sensor.py index ace13353341..6badeefbdb3 100644 --- a/homeassistant/components/yolink/sensor.py +++ b/homeassistant/components/yolink/sensor.py @@ -1,4 +1,5 @@ """YoLink Sensor.""" + from __future__ import annotations from collections.abc import Callable @@ -22,6 +23,8 @@ from yolink.const import ( ATTR_DEVICE_TH_SENSOR, ATTR_DEVICE_THERMOSTAT, ATTR_DEVICE_VIBRATION_SENSOR, + ATTR_DEVICE_WATER_DEPTH_SENSOR, + ATTR_DEVICE_WATER_METER_CONTROLLER, ATTR_GARAGE_DOOR_CONTROLLER, ) from yolink.device import YoLinkDevice @@ -37,7 +40,9 @@ from homeassistant.const import ( PERCENTAGE, SIGNAL_STRENGTH_DECIBELS_MILLIWATT, EntityCategory, + UnitOfLength, UnitOfTemperature, + UnitOfVolume, ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -72,6 +77,8 @@ SENSOR_DEVICE_TYPE = [ ATTR_DEVICE_TH_SENSOR, ATTR_DEVICE_THERMOSTAT, ATTR_DEVICE_VIBRATION_SENSOR, + ATTR_DEVICE_WATER_DEPTH_SENSOR, + ATTR_DEVICE_WATER_METER_CONTROLLER, ATTR_DEVICE_LOCK, ATTR_DEVICE_MANIPULATOR, ATTR_DEVICE_CO_SMOKE_SENSOR, @@ -91,6 +98,8 @@ BATTERY_POWER_SENSOR = [ ATTR_DEVICE_LOCK, ATTR_DEVICE_MANIPULATOR, ATTR_DEVICE_CO_SMOKE_SENSOR, + ATTR_DEVICE_WATER_DEPTH_SENSOR, + ATTR_DEVICE_WATER_METER_CONTROLLER, ] MCU_DEV_TEMPERATURE_SENSOR = [ @@ -114,7 +123,7 @@ def cvt_volume(val: int | None) -> str | None: if val is None: return None volume_level = {1: "low", 2: "medium", 3: "high"} - return volume_level.get(val, None) + return volume_level.get(val) SENSOR_TYPES: tuple[YoLinkSensorEntityDescription, ...] = ( @@ -164,7 +173,6 @@ SENSOR_TYPES: tuple[YoLinkSensorEntityDescription, ...] = ( key="state", translation_key="power_failure_alarm", device_class=SensorDeviceClass.ENUM, - icon="mdi:flash", options=["normal", "alert", "off"], exists_fn=lambda device: device.device_type in ATTR_DEVICE_POWER_FAILURE_ALARM, ), @@ -172,7 +180,6 @@ SENSOR_TYPES: tuple[YoLinkSensorEntityDescription, ...] = ( key="mute", translation_key="power_failure_alarm_mute", device_class=SensorDeviceClass.ENUM, - icon="mdi:volume-mute", options=["muted", "unmuted"], exists_fn=lambda device: device.device_type in ATTR_DEVICE_POWER_FAILURE_ALARM, value=lambda value: "muted" if value is True else "unmuted", @@ -181,7 +188,6 @@ SENSOR_TYPES: tuple[YoLinkSensorEntityDescription, ...] = ( key="sound", translation_key="power_failure_alarm_volume", device_class=SensorDeviceClass.ENUM, - icon="mdi:volume-high", options=["low", "medium", "high"], exists_fn=lambda device: device.device_type in ATTR_DEVICE_POWER_FAILURE_ALARM, value=cvt_volume, @@ -190,11 +196,27 @@ SENSOR_TYPES: tuple[YoLinkSensorEntityDescription, ...] = ( key="beep", translation_key="power_failure_alarm_beep", device_class=SensorDeviceClass.ENUM, - icon="mdi:bullhorn", options=["enabled", "disabled"], exists_fn=lambda device: device.device_type in ATTR_DEVICE_POWER_FAILURE_ALARM, value=lambda value: "enabled" if value is True else "disabled", ), + YoLinkSensorEntityDescription( + key="waterDepth", + device_class=SensorDeviceClass.DISTANCE, + native_unit_of_measurement=UnitOfLength.METERS, + exists_fn=lambda device: device.device_type in ATTR_DEVICE_WATER_DEPTH_SENSOR, + ), + YoLinkSensorEntityDescription( + key="meter_reading", + translation_key="water_meter_reading", + device_class=SensorDeviceClass.WATER, + icon="mdi:gauge", + native_unit_of_measurement=UnitOfVolume.CUBIC_METERS, + state_class=SensorStateClass.TOTAL_INCREASING, + should_update_entity=lambda value: value is not None, + exists_fn=lambda device: device.device_type + in ATTR_DEVICE_WATER_METER_CONTROLLER, + ), ) @@ -210,18 +232,16 @@ async def async_setup_entry( for device_coordinator in device_coordinators.values() if device_coordinator.device.device_type in SENSOR_DEVICE_TYPE ] - entities = [] - for sensor_device_coordinator in sensor_device_coordinators: - for description in SENSOR_TYPES: - if description.exists_fn(sensor_device_coordinator.device): - entities.append( - YoLinkSensorEntity( - config_entry, - sensor_device_coordinator, - description, - ) - ) - async_add_entities(entities) + async_add_entities( + YoLinkSensorEntity( + config_entry, + sensor_device_coordinator, + description, + ) + for sensor_device_coordinator in sensor_device_coordinators + for description in SENSOR_TYPES + if description.exists_fn(sensor_device_coordinator.device) + ) class YoLinkSensorEntity(YoLinkEntity, SensorEntity): diff --git a/homeassistant/components/yolink/services.py b/homeassistant/components/yolink/services.py index e41e3dce260..a011d493dc9 100644 --- a/homeassistant/components/yolink/services.py +++ b/homeassistant/components/yolink/services.py @@ -36,7 +36,6 @@ def async_register_services(hass: HomeAssistant) -> None: break if entry is None or entry.state == ConfigEntryState.NOT_LOADED: raise ServiceValidationError( - "Config entry not found or not loaded!", translation_domain=DOMAIN, translation_key="invalid_config_entry", ) diff --git a/homeassistant/components/yolink/siren.py b/homeassistant/components/yolink/siren.py index 4a35e9506e9..9e02f50bb70 100644 --- a/homeassistant/components/yolink/siren.py +++ b/homeassistant/components/yolink/siren.py @@ -1,4 +1,5 @@ """YoLink Siren.""" + from __future__ import annotations from collections.abc import Callable @@ -54,16 +55,12 @@ async def async_setup_entry( for device_coordinator in device_coordinators.values() if device_coordinator.device.device_type in DEVICE_TYPE ] - entities = [] - for siren_device_coordinator in siren_device_coordinators: - for description in DEVICE_TYPES: - if description.exists_fn(siren_device_coordinator.device): - entities.append( - YoLinkSirenEntity( - config_entry, siren_device_coordinator, description - ) - ) - async_add_entities(entities) + async_add_entities( + YoLinkSirenEntity(config_entry, siren_device_coordinator, description) + for siren_device_coordinator in siren_device_coordinators + for description in DEVICE_TYPES + if description.exists_fn(siren_device_coordinator.device) + ) class YoLinkSirenEntity(YoLinkEntity, SirenEntity): diff --git a/homeassistant/components/yolink/strings.json b/homeassistant/components/yolink/strings.json index 83e712328f9..bc8fb435e76 100644 --- a/homeassistant/components/yolink/strings.json +++ b/homeassistant/components/yolink/strings.json @@ -73,12 +73,20 @@ "enabled": "[%key:common::state::enabled%]", "disabled": "[%key:common::state::disabled%]" } + }, + "water_meter_reading": { + "name": "Water meter reading" } }, "number": { "config_volume": { "name": "Volume" } + }, + "valve": { + "meter_valve_state": { + "name": "Valve state" + } } }, "services": { diff --git a/homeassistant/components/yolink/switch.py b/homeassistant/components/yolink/switch.py index 69a958ba6d1..7a24ec1bd13 100644 --- a/homeassistant/components/yolink/switch.py +++ b/homeassistant/components/yolink/switch.py @@ -1,4 +1,5 @@ """YoLink Switch.""" + from __future__ import annotations from collections.abc import Callable @@ -46,8 +47,8 @@ DEVICE_TYPES: tuple[YoLinkSwitchEntityDescription, ...] = ( ), YoLinkSwitchEntityDescription( key="manipulator_state", + translation_key="manipulator_state", name=None, - icon="mdi:pipe", exists_fn=lambda device: device.device_type == ATTR_DEVICE_MANIPULATOR, ), YoLinkSwitchEntityDescription( @@ -113,16 +114,12 @@ async def async_setup_entry( for device_coordinator in device_coordinators.values() if device_coordinator.device.device_type in DEVICE_TYPE ] - entities = [] - for switch_device_coordinator in switch_device_coordinators: - for description in DEVICE_TYPES: - if description.exists_fn(switch_device_coordinator.device): - entities.append( - YoLinkSwitchEntity( - config_entry, switch_device_coordinator, description - ) - ) - async_add_entities(entities) + async_add_entities( + YoLinkSwitchEntity(config_entry, switch_device_coordinator, description) + for switch_device_coordinator in switch_device_coordinators + for description in DEVICE_TYPES + if description.exists_fn(switch_device_coordinator.device) + ) class YoLinkSwitchEntity(YoLinkEntity, SwitchEntity): diff --git a/homeassistant/components/yolink/valve.py b/homeassistant/components/yolink/valve.py new file mode 100644 index 00000000000..a24ad7d385d --- /dev/null +++ b/homeassistant/components/yolink/valve.py @@ -0,0 +1,115 @@ +"""YoLink Valve.""" + +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass + +from yolink.client_request import ClientRequest +from yolink.const import ATTR_DEVICE_WATER_METER_CONTROLLER +from yolink.device import YoLinkDevice + +from homeassistant.components.valve import ( + ValveDeviceClass, + ValveEntity, + ValveEntityDescription, + ValveEntityFeature, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DEV_MODEL_WATER_METER_YS5007, DOMAIN +from .coordinator import YoLinkCoordinator +from .entity import YoLinkEntity + + +@dataclass(frozen=True) +class YoLinkValveEntityDescription(ValveEntityDescription): + """YoLink ValveEntityDescription.""" + + exists_fn: Callable[[YoLinkDevice], bool] = lambda _: True + value: Callable = lambda state: state + + +DEVICE_TYPES: tuple[YoLinkValveEntityDescription, ...] = ( + YoLinkValveEntityDescription( + key="valve_state", + translation_key="meter_valve_state", + device_class=ValveDeviceClass.WATER, + value=lambda value: value == "closed" if value is not None else None, + exists_fn=lambda device: device.device_type + == ATTR_DEVICE_WATER_METER_CONTROLLER + and not device.device_model_name.startswith(DEV_MODEL_WATER_METER_YS5007), + ), +) + +DEVICE_TYPE = [ATTR_DEVICE_WATER_METER_CONTROLLER] + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up YoLink valve from a config entry.""" + device_coordinators = hass.data[DOMAIN][config_entry.entry_id].device_coordinators + valve_device_coordinators = [ + device_coordinator + for device_coordinator in device_coordinators.values() + if device_coordinator.device.device_type in DEVICE_TYPE + ] + async_add_entities( + YoLinkValveEntity(config_entry, valve_device_coordinator, description) + for valve_device_coordinator in valve_device_coordinators + for description in DEVICE_TYPES + if description.exists_fn(valve_device_coordinator.device) + ) + + +class YoLinkValveEntity(YoLinkEntity, ValveEntity): + """YoLink Valve Entity.""" + + entity_description: YoLinkValveEntityDescription + + def __init__( + self, + config_entry: ConfigEntry, + coordinator: YoLinkCoordinator, + description: YoLinkValveEntityDescription, + ) -> None: + """Init YoLink valve.""" + super().__init__(config_entry, coordinator) + self._attr_supported_features = ( + ValveEntityFeature.OPEN | ValveEntityFeature.CLOSE + ) + self.entity_description = description + self._attr_unique_id = ( + f"{coordinator.device.device_id} {self.entity_description.key}" + ) + + @callback + def update_entity_state(self, state: dict[str, str | list[str]]) -> None: + """Update HA Entity State.""" + if ( + attr_val := self.entity_description.value( + state.get(self.entity_description.key) + ) + ) is None: + return + self._attr_is_closed = attr_val + self.async_write_ha_state() + + async def _async_invoke_device(self, state: str) -> None: + """Call setState api to change valve state.""" + await self.call_device(ClientRequest("setState", {"valve": state})) + self._attr_is_closed = state == "close" + self.async_write_ha_state() + + async def async_open_valve(self) -> None: + """Open the valve.""" + await self._async_invoke_device("open") + + async def async_close_valve(self) -> None: + """Close valve.""" + await self._async_invoke_device("close") diff --git a/homeassistant/components/youless/__init__.py b/homeassistant/components/youless/__init__.py index 5724f417a7f..a968d052922 100644 --- a/homeassistant/components/youless/__init__.py +++ b/homeassistant/components/youless/__init__.py @@ -1,4 +1,5 @@ """The youless integration.""" + from datetime import timedelta import logging from urllib.error import URLError diff --git a/homeassistant/components/youless/config_flow.py b/homeassistant/components/youless/config_flow.py index 2cf79ae64e0..d13d2ed076f 100644 --- a/homeassistant/components/youless/config_flow.py +++ b/homeassistant/components/youless/config_flow.py @@ -1,4 +1,5 @@ """Config flow for youless integration.""" + from __future__ import annotations import logging @@ -8,9 +9,8 @@ from urllib.error import HTTPError, URLError import voluptuous as vol from youless_api import YoulessAPI -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_DEVICE, CONF_HOST -from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN @@ -26,7 +26,7 @@ class YoulessConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors = {} if user_input is not None: diff --git a/homeassistant/components/youless/sensor.py b/homeassistant/components/youless/sensor.py index 36175ae9cf3..81cd8b384d2 100644 --- a/homeassistant/components/youless/sensor.py +++ b/homeassistant/components/youless/sensor.py @@ -1,4 +1,5 @@ """The sensor entity for the Youless integration.""" + from __future__ import annotations from youless_api import YoulessAPI diff --git a/homeassistant/components/youtube/__init__.py b/homeassistant/components/youtube/__init__.py index 46c3b0d1902..8460a105fcb 100644 --- a/homeassistant/components/youtube/__init__.py +++ b/homeassistant/components/youtube/__init__.py @@ -1,4 +1,5 @@ """Support for YouTube.""" + from __future__ import annotations from aiohttp.client_exceptions import ClientError, ClientResponseError diff --git a/homeassistant/components/youtube/api.py b/homeassistant/components/youtube/api.py index b98169e3589..beb0a9a70dc 100644 --- a/homeassistant/components/youtube/api.py +++ b/homeassistant/components/youtube/api.py @@ -1,4 +1,5 @@ """API for YouTube bound to Home Assistant OAuth.""" + from youtubeaio.types import AuthScope from youtubeaio.youtube import YouTube diff --git a/homeassistant/components/youtube/application_credentials.py b/homeassistant/components/youtube/application_credentials.py index ba6188d9a3d..72c1b9a72d4 100644 --- a/homeassistant/components/youtube/application_credentials.py +++ b/homeassistant/components/youtube/application_credentials.py @@ -1,4 +1,5 @@ """application_credentials platform for YouTube.""" + from homeassistant.components.application_credentials import AuthorizationServer from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/youtube/config_flow.py b/homeassistant/components/youtube/config_flow.py index cf0d61b5d38..025ed8780e6 100644 --- a/homeassistant/components/youtube/config_flow.py +++ b/homeassistant/components/youtube/config_flow.py @@ -1,4 +1,5 @@ """Config flow for YouTube integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -10,10 +11,13 @@ from youtubeaio.helper import first from youtubeaio.types import AuthScope, ForbiddenError from youtubeaio.youtube import YouTube -from homeassistant.config_entries import ConfigEntry, OptionsFlowWithConfigEntry +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlowResult, + OptionsFlowWithConfigEntry, +) from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.selector import ( @@ -67,7 +71,9 @@ class OAuth2FlowHandler( "prompt": "consent", } - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: """Perform reauth upon an API authentication error.""" self.reauth_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] @@ -76,7 +82,7 @@ class OAuth2FlowHandler( async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm reauth dialog.""" if user_input is None: return self.async_show_form(step_id="reauth_confirm") @@ -89,7 +95,7 @@ class OAuth2FlowHandler( await self._youtube.set_user_authentication(token, [AuthScope.READ_ONLY]) return self._youtube - async def async_oauth_create_entry(self, data: dict[str, Any]) -> FlowResult: + async def async_oauth_create_entry(self, data: dict[str, Any]) -> ConfigFlowResult: """Create an entry for the flow, or update existing entry.""" try: youtube = await self.get_resource(data[CONF_TOKEN][CONF_ACCESS_TOKEN]) @@ -129,7 +135,7 @@ class OAuth2FlowHandler( async def async_step_channels( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Select which channels to track.""" if user_input: return self.async_create_entry( @@ -164,7 +170,7 @@ class YouTubeOptionsFlowHandler(OptionsFlowWithConfigEntry): async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Initialize form.""" if user_input is not None: return self.async_create_entry( diff --git a/homeassistant/components/youtube/const.py b/homeassistant/components/youtube/const.py index 63c4480c007..a663c487d0a 100644 --- a/homeassistant/components/youtube/const.py +++ b/homeassistant/components/youtube/const.py @@ -1,4 +1,5 @@ """Constants for YouTube integration.""" + import logging DEFAULT_ACCESS = ["https://www.googleapis.com/auth/youtube.readonly"] diff --git a/homeassistant/components/youtube/coordinator.py b/homeassistant/components/youtube/coordinator.py index 07420233baf..4599342c84d 100644 --- a/homeassistant/components/youtube/coordinator.py +++ b/homeassistant/components/youtube/coordinator.py @@ -1,4 +1,5 @@ """DataUpdateCoordinator for the YouTube integration.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/youtube/diagnostics.py b/homeassistant/components/youtube/diagnostics.py index 7cd32d50d27..9a898b7e2de 100644 --- a/homeassistant/components/youtube/diagnostics.py +++ b/homeassistant/components/youtube/diagnostics.py @@ -1,4 +1,5 @@ """Diagnostics support for YouTube.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/youtube/entity.py b/homeassistant/components/youtube/entity.py index 6f7f0b28dd2..698b14fa6a7 100644 --- a/homeassistant/components/youtube/entity.py +++ b/homeassistant/components/youtube/entity.py @@ -1,4 +1,5 @@ """Entity representing a YouTube account.""" + from __future__ import annotations from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo diff --git a/homeassistant/components/youtube/icons.json b/homeassistant/components/youtube/icons.json new file mode 100644 index 00000000000..34867058633 --- /dev/null +++ b/homeassistant/components/youtube/icons.json @@ -0,0 +1,12 @@ +{ + "entity": { + "sensor": { + "latest_upload": { + "default": "mdi:youtube" + }, + "subscribers": { + "default": "mdi:youtube-subscription" + } + } + } +} diff --git a/homeassistant/components/youtube/sensor.py b/homeassistant/components/youtube/sensor.py index d037a8c3c4b..bc69f92e8fd 100644 --- a/homeassistant/components/youtube/sensor.py +++ b/homeassistant/components/youtube/sensor.py @@ -1,4 +1,5 @@ """Support for YouTube Sensors.""" + from __future__ import annotations from collections.abc import Callable @@ -26,9 +27,9 @@ from .const import ( from .entity import YouTubeChannelEntity -@dataclass(frozen=True) -class YouTubeMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class YouTubeSensorEntityDescription(SensorEntityDescription): + """Describes YouTube sensor entity.""" available_fn: Callable[[Any], bool] value_fn: Callable[[Any], StateType] @@ -36,16 +37,10 @@ class YouTubeMixin: attributes_fn: Callable[[Any], dict[str, Any] | None] | None -@dataclass(frozen=True) -class YouTubeSensorEntityDescription(SensorEntityDescription, YouTubeMixin): - """Describes YouTube sensor entity.""" - - SENSOR_TYPES = [ YouTubeSensorEntityDescription( key="latest_upload", translation_key="latest_upload", - icon="mdi:youtube", available_fn=lambda channel: channel[ATTR_LATEST_VIDEO] is not None, value_fn=lambda channel: channel[ATTR_LATEST_VIDEO][ATTR_TITLE], entity_picture_fn=lambda channel: channel[ATTR_LATEST_VIDEO][ATTR_THUMBNAIL], @@ -57,7 +52,6 @@ SENSOR_TYPES = [ YouTubeSensorEntityDescription( key="subscribers", translation_key="subscribers", - icon="mdi:youtube-subscription", native_unit_of_measurement="subscribers", available_fn=lambda _: True, value_fn=lambda channel: channel[ATTR_SUBSCRIBER_COUNT], diff --git a/homeassistant/components/zabbix/__init__.py b/homeassistant/components/zabbix/__init__.py index a59465bf400..58d3c1fd3f2 100644 --- a/homeassistant/components/zabbix/__init__.py +++ b/homeassistant/components/zabbix/__init__.py @@ -1,4 +1,5 @@ """Support for Zabbix.""" + from contextlib import suppress import json import logging @@ -139,9 +140,7 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: float_keys_count = len(float_keys) float_keys.update(floats) if len(float_keys) != float_keys_count: - floats_discovery = [] - for float_key in float_keys: - floats_discovery.append({"{#KEY}": float_key}) + floats_discovery = [{"{#KEY}": float_key} for float_key in float_keys] metric = ZabbixMetric( publish_states_host, "homeassistant.floats_discovery", diff --git a/homeassistant/components/zabbix/sensor.py b/homeassistant/components/zabbix/sensor.py index 2fd2d48faba..eaa06367408 100644 --- a/homeassistant/components/zabbix/sensor.py +++ b/homeassistant/components/zabbix/sensor.py @@ -1,4 +1,5 @@ """Support for Zabbix sensors.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/zamg/__init__.py b/homeassistant/components/zamg/__init__.py index 0e57bef64ff..f6241e53fbe 100644 --- a/homeassistant/components/zamg/__init__.py +++ b/homeassistant/components/zamg/__init__.py @@ -1,4 +1,5 @@ """The zamg component.""" + from __future__ import annotations from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/zamg/config_flow.py b/homeassistant/components/zamg/config_flow.py index d20af9b274b..24045ba8f4e 100644 --- a/homeassistant/components/zamg/config_flow.py +++ b/homeassistant/components/zamg/config_flow.py @@ -1,4 +1,5 @@ """Config Flow for the zamg integration.""" + from __future__ import annotations from typing import Any @@ -7,14 +8,13 @@ import voluptuous as vol from zamg import ZamgData from zamg.exceptions import ZamgApiError, ZamgNoDataError -from homeassistant import config_entries -from homeassistant.data_entry_flow import FlowResult +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import CONF_STATION_ID, DOMAIN, LOGGER -class ZamgConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class ZamgConfigFlow(ConfigFlow, domain=DOMAIN): """Config flow for zamg integration.""" VERSION = 1 @@ -23,7 +23,7 @@ class ZamgConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initiated by the user.""" if self._client is None: self._client = ZamgData() diff --git a/homeassistant/components/zamg/coordinator.py b/homeassistant/components/zamg/coordinator.py index f785524e866..d53c743f500 100644 --- a/homeassistant/components/zamg/coordinator.py +++ b/homeassistant/components/zamg/coordinator.py @@ -1,4 +1,5 @@ """Data Update coordinator for ZAMG weather data.""" + from __future__ import annotations from zamg import ZamgData as ZamgDevice diff --git a/homeassistant/components/zamg/sensor.py b/homeassistant/components/zamg/sensor.py index adc07212a5f..7c7f5fd6c16 100644 --- a/homeassistant/components/zamg/sensor.py +++ b/homeassistant/components/zamg/sensor.py @@ -1,4 +1,5 @@ """Sensor for the zamg integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -37,18 +38,13 @@ from .const import ( from .coordinator import ZamgDataUpdateCoordinator -@dataclass(frozen=True) -class ZamgRequiredKeysMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class ZamgSensorEntityDescription(SensorEntityDescription): + """Describes Zamg sensor entity.""" para_name: str -@dataclass(frozen=True) -class ZamgSensorEntityDescription(SensorEntityDescription, ZamgRequiredKeysMixin): - """Describes Zamg sensor entity.""" - - SENSOR_TYPES: tuple[ZamgSensorEntityDescription, ...] = ( ZamgSensorEntityDescription( key="pressure", diff --git a/homeassistant/components/zamg/weather.py b/homeassistant/components/zamg/weather.py index e855bde29d8..241b2232eeb 100644 --- a/homeassistant/components/zamg/weather.py +++ b/homeassistant/components/zamg/weather.py @@ -1,4 +1,5 @@ """Sensor for the zamg integration.""" + from __future__ import annotations from homeassistant.components.weather import WeatherEntity diff --git a/homeassistant/components/zengge/light.py b/homeassistant/components/zengge/light.py index 2d4ba4614e6..5de4f3fdce3 100644 --- a/homeassistant/components/zengge/light.py +++ b/homeassistant/components/zengge/light.py @@ -1,4 +1,5 @@ """Support for Zengge lights.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index 344c174242a..66c41c19474 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -1,4 +1,5 @@ """Support for exposing Home Assistant via Zeroconf.""" + from __future__ import annotations import contextlib @@ -23,7 +24,11 @@ from zeroconf.asyncio import AsyncServiceBrowser, AsyncServiceInfo from homeassistant import config_entries from homeassistant.components import network -from homeassistant.const import EVENT_HOMEASSISTANT_STOP, __version__ +from homeassistant.const import ( + EVENT_HOMEASSISTANT_CLOSE, + EVENT_HOMEASSISTANT_STOP, + __version__, +) from homeassistant.core import Event, HomeAssistant, callback from homeassistant.data_entry_flow import BaseServiceInfo from homeassistant.helpers import discovery_flow, instance_id @@ -162,7 +167,11 @@ async def _async_get_instance(hass: HomeAssistant, **zcargs: Any) -> HaAsyncZero """Stop Zeroconf.""" await aio_zc.ha_async_close() - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_stop_zeroconf) + # Wait to the close event to shutdown zeroconf to give + # integrations time to send a good bye message + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_CLOSE, _async_stop_zeroconf, run_immediately=True + ) hass.data[DOMAIN] = aio_zc return aio_zc @@ -239,7 +248,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def _async_zeroconf_hass_stop(_event: Event) -> None: await discovery.async_stop() - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_zeroconf_hass_stop) + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, _async_zeroconf_hass_stop, run_immediately=True + ) async_when_setup_or_start(hass, "frontend", _async_zeroconf_hass_start) return True @@ -362,9 +373,11 @@ class ZeroconfDiscovery: # We want to make sure we know about other HomeAssistant # instances as soon as possible to avoid name conflicts # so we always browse for ZEROCONF_TYPE - for hk_type in (ZEROCONF_TYPE, *HOMEKIT_TYPES): - if hk_type not in self.zeroconf_types: - types.append(hk_type) + types.extend( + hk_type + for hk_type in (ZEROCONF_TYPE, *HOMEKIT_TYPES) + if hk_type not in self.zeroconf_types + ) _LOGGER.debug("Starting Zeroconf browser for: %s", types) self.async_service_browser = AsyncServiceBrowser( self.zeroconf, types, handlers=[self.async_service_update] @@ -414,10 +427,11 @@ class ZeroconfDiscovery: if async_service_info.load_from_cache(zeroconf): self._async_process_service_update(async_service_info, service_type, name) else: - self.hass.async_create_task( + self.hass.async_create_background_task( self._async_lookup_and_process_service_update( zeroconf, async_service_info, service_type, name - ) + ), + name=f"zeroconf lookup {name}.{service_type}", ) async def _async_lookup_and_process_service_update( diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index f7ca2eeeed0..7c489517dd7 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -4,10 +4,9 @@ "codeowners": ["@bdraco"], "dependencies": ["network", "api"], "documentation": "https://www.home-assistant.io/integrations/zeroconf", - "import_executor": true, "integration_type": "system", "iot_class": "local_push", "loggers": ["zeroconf"], "quality_scale": "internal", - "requirements": ["zeroconf==0.131.0"] + "requirements": ["zeroconf==0.132.0"] } diff --git a/homeassistant/components/zerproc/__init__.py b/homeassistant/components/zerproc/__init__.py index edd41d1a8e3..953720038cc 100644 --- a/homeassistant/components/zerproc/__init__.py +++ b/homeassistant/components/zerproc/__init__.py @@ -1,4 +1,5 @@ """Zerproc lights integration.""" + from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/zerproc/config_flow.py b/homeassistant/components/zerproc/config_flow.py index a9fd20ce241..f4401c6c390 100644 --- a/homeassistant/components/zerproc/config_flow.py +++ b/homeassistant/components/zerproc/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Zerproc.""" + import logging import pyzerproc diff --git a/homeassistant/components/zerproc/const.py b/homeassistant/components/zerproc/const.py index 69d5fcfb740..4922bef91d0 100644 --- a/homeassistant/components/zerproc/const.py +++ b/homeassistant/components/zerproc/const.py @@ -1,4 +1,5 @@ """Constants for the Zerproc integration.""" + DOMAIN = "zerproc" DATA_ADDRESSES = "addresses" diff --git a/homeassistant/components/zerproc/icons.json b/homeassistant/components/zerproc/icons.json new file mode 100644 index 00000000000..82c95aebce6 --- /dev/null +++ b/homeassistant/components/zerproc/icons.json @@ -0,0 +1,9 @@ +{ + "entity": { + "light": { + "light": { + "default": "mdi:string-lights" + } + } + } +} diff --git a/homeassistant/components/zerproc/light.py b/homeassistant/components/zerproc/light.py index c6be3c70e65..71bb38dd80f 100644 --- a/homeassistant/components/zerproc/light.py +++ b/homeassistant/components/zerproc/light.py @@ -1,4 +1,5 @@ """Zerproc light platform.""" + from __future__ import annotations from datetime import timedelta @@ -77,13 +78,13 @@ async def async_setup_entry( class ZerprocLight(LightEntity): - """Representation of an Zerproc Light.""" + """Representation of a Zerproc Light.""" _attr_color_mode = ColorMode.HS - _attr_icon = "mdi:string-lights" _attr_supported_color_modes = {ColorMode.HS} _attr_has_entity_name = True _attr_name = None + _attr_translation_key = "light" def __init__(self, light) -> None: """Initialize a Zerproc light.""" diff --git a/homeassistant/components/zestimate/sensor.py b/homeassistant/components/zestimate/sensor.py index 9b520c46819..8bbda7de73a 100644 --- a/homeassistant/components/zestimate/sensor.py +++ b/homeassistant/components/zestimate/sensor.py @@ -1,4 +1,5 @@ """Support for zestimate data from zillow.com.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/zeversolar/__init__.py b/homeassistant/components/zeversolar/__init__.py index cff5cf413e5..cb48579367b 100644 --- a/homeassistant/components/zeversolar/__init__.py +++ b/homeassistant/components/zeversolar/__init__.py @@ -1,4 +1,5 @@ """The Zeversolar integration.""" + from __future__ import annotations from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/zeversolar/config_flow.py b/homeassistant/components/zeversolar/config_flow.py index f749b9d471c..e4deae47c8f 100644 --- a/homeassistant/components/zeversolar/config_flow.py +++ b/homeassistant/components/zeversolar/config_flow.py @@ -1,4 +1,5 @@ """Config flow for zeversolar integration.""" + from __future__ import annotations import logging @@ -7,9 +8,8 @@ from typing import Any import voluptuous as vol import zeversolar -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_validation as cv from .const import DOMAIN @@ -23,14 +23,14 @@ STEP_USER_DATA_SCHEMA = vol.Schema( ) -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class ZeverSolarConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for zeversolar.""" VERSION = 1 async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" if user_input is None: return self.async_show_form( diff --git a/homeassistant/components/zeversolar/coordinator.py b/homeassistant/components/zeversolar/coordinator.py index 554fe195eab..9f6ff49eaf8 100644 --- a/homeassistant/components/zeversolar/coordinator.py +++ b/homeassistant/components/zeversolar/coordinator.py @@ -1,4 +1,5 @@ """Zeversolar coordinator.""" + from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/zeversolar/entity.py b/homeassistant/components/zeversolar/entity.py index 77ae5ee61f8..18ac4dcde32 100644 --- a/homeassistant/components/zeversolar/entity.py +++ b/homeassistant/components/zeversolar/entity.py @@ -1,4 +1,5 @@ """Base Entity for Zeversolar sensors.""" + from __future__ import annotations from homeassistant.helpers.device_registry import DeviceInfo diff --git a/homeassistant/components/zeversolar/icons.json b/homeassistant/components/zeversolar/icons.json new file mode 100644 index 00000000000..8e30a4df86b --- /dev/null +++ b/homeassistant/components/zeversolar/icons.json @@ -0,0 +1,12 @@ +{ + "entity": { + "sensor": { + "pac": { + "default": "mdi:solar-power-variant" + }, + "energy_today": { + "default": "mdi:home-battery" + } + } + } +} diff --git a/homeassistant/components/zeversolar/sensor.py b/homeassistant/components/zeversolar/sensor.py index 9e2333a1e24..5023e274267 100644 --- a/homeassistant/components/zeversolar/sensor.py +++ b/homeassistant/components/zeversolar/sensor.py @@ -1,4 +1,5 @@ """Support for the Zeversolar platform.""" + from __future__ import annotations from collections.abc import Callable @@ -22,24 +23,17 @@ from .coordinator import ZeversolarCoordinator from .entity import ZeversolarEntity -@dataclass(frozen=True) -class ZeversolarEntityDescriptionMixin: - """Mixin for required keys.""" +@dataclass(frozen=True, kw_only=True) +class ZeversolarEntityDescription(SensorEntityDescription): + """Describes Zeversolar sensor entity.""" value_fn: Callable[[zeversolar.ZeverSolarData], zeversolar.kWh | zeversolar.Watt] -@dataclass(frozen=True) -class ZeversolarEntityDescription( - SensorEntityDescription, ZeversolarEntityDescriptionMixin -): - """Describes Zeversolar sensor entity.""" - - SENSOR_TYPES = ( ZeversolarEntityDescription( key="pac", - icon="mdi:solar-power-variant", + translation_key="pac", native_unit_of_measurement=UnitOfPower.WATT, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, @@ -49,7 +43,6 @@ SENSOR_TYPES = ( ZeversolarEntityDescription( key="energy_today", translation_key="energy_today", - icon="mdi:home-battery", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, state_class=SensorStateClass.TOTAL_INCREASING, device_class=SensorDeviceClass.ENERGY, diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index 34ba0d33482..de761138ce1 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -1,4 +1,5 @@ """Support for Zigbee Home Automation devices.""" + import asyncio import contextlib import copy @@ -123,8 +124,8 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b zha_data = get_zha_data(hass) if zha_data.yaml_config.get(CONF_ENABLE_QUIRKS, True): - setup_quirks( - custom_quirks_path=zha_data.yaml_config.get(CONF_CUSTOM_QUIRKS_PATH) + await hass.async_add_import_executor_job( + setup_quirks, zha_data.yaml_config.get(CONF_CUSTOM_QUIRKS_PATH) ) # Load and cache device trigger information early diff --git a/homeassistant/components/zha/alarm_control_panel.py b/homeassistant/components/zha/alarm_control_panel.py index 7f1f6a85d15..7750e7f280d 100644 --- a/homeassistant/components/zha/alarm_control_panel.py +++ b/homeassistant/components/zha/alarm_control_panel.py @@ -1,4 +1,5 @@ """Alarm control panels on Zigbee Home Automation networks.""" + from __future__ import annotations import functools diff --git a/homeassistant/components/zha/backup.py b/homeassistant/components/zha/backup.py index e125a8085f6..25d5a83b6a4 100644 --- a/homeassistant/components/zha/backup.py +++ b/homeassistant/components/zha/backup.py @@ -1,4 +1,5 @@ """Backup platform for the ZHA integration.""" + import logging from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/zha/binary_sensor.py b/homeassistant/components/zha/binary_sensor.py index aed0a16a681..6ffb6d6f909 100644 --- a/homeassistant/components/zha/binary_sensor.py +++ b/homeassistant/components/zha/binary_sensor.py @@ -1,10 +1,11 @@ """Binary sensors on Zigbee Home Automation networks.""" + from __future__ import annotations import functools -from typing import Any +import logging -from zigpy.quirks.v2 import BinarySensorMetadata, EntityMetadata +from zigpy.quirks.v2 import BinarySensorMetadata import zigpy.types as t from zigpy.zcl.clusters.general import OnOff from zigpy.zcl.clusters.security import IasZone @@ -27,11 +28,11 @@ from .core.const import ( CLUSTER_HANDLER_OCCUPANCY, CLUSTER_HANDLER_ON_OFF, CLUSTER_HANDLER_ZONE, - QUIRK_METADATA, + ENTITY_METADATA, SIGNAL_ADD_ENTITIES, SIGNAL_ATTR_UPDATED, ) -from .core.helpers import get_zha_data +from .core.helpers import get_zha_data, validate_device_class from .core.registries import ZHA_ENTITIES from .entity import ZhaEntity @@ -51,6 +52,8 @@ CONFIG_DIAGNOSTIC_MATCH = functools.partial( ZHA_ENTITIES.config_diagnostic_match, Platform.BINARY_SENSOR ) +_LOGGER = logging.getLogger(__name__) + async def async_setup_entry( hass: HomeAssistant, @@ -79,15 +82,21 @@ class BinarySensor(ZhaEntity, BinarySensorEntity): def __init__(self, unique_id, zha_device, cluster_handlers, **kwargs) -> None: """Initialize the ZHA binary sensor.""" self._cluster_handler = cluster_handlers[0] - if QUIRK_METADATA in kwargs: - self._init_from_quirks_metadata(kwargs[QUIRK_METADATA]) + if ENTITY_METADATA in kwargs: + self._init_from_quirks_metadata(kwargs[ENTITY_METADATA]) super().__init__(unique_id, zha_device, cluster_handlers, **kwargs) - def _init_from_quirks_metadata(self, entity_metadata: EntityMetadata) -> None: + def _init_from_quirks_metadata(self, entity_metadata: BinarySensorMetadata) -> None: """Init this entity from the quirks metadata.""" super()._init_from_quirks_metadata(entity_metadata) - binary_sensor_metadata: BinarySensorMetadata = entity_metadata.entity_metadata - self._attribute_name = binary_sensor_metadata.attribute_name + self._attribute_name = entity_metadata.attribute_name + if entity_metadata.device_class is not None: + self._attr_device_class = validate_device_class( + BinarySensorDeviceClass, + entity_metadata.device_class, + Platform.BINARY_SENSOR.value, + _LOGGER, + ) async def async_added_to_hass(self) -> None: """Run when about to be added to hass.""" @@ -208,36 +217,6 @@ class IASZone(BinarySensor): """Parse the raw attribute into a bool state.""" return BinarySensor.parse(value & 3) # use only bit 0 and 1 for alarm state - # temporary code to migrate old IasZone sensors to update attribute cache state once - # remove in 2024.4.0 - @property - def extra_state_attributes(self) -> dict[str, Any]: - """Return state attributes.""" - return {"migrated_to_cache": True} # writing new state means we're migrated - - # temporary migration code - @callback - def async_restore_last_state(self, last_state): - """Restore previous state.""" - # trigger migration if extra state attribute is not present - if "migrated_to_cache" not in last_state.attributes: - self.migrate_to_zigpy_cache(last_state) - - # temporary migration code - @callback - def migrate_to_zigpy_cache(self, last_state): - """Save old IasZone sensor state to attribute cache.""" - # previous HA versions did not update the attribute cache for IasZone sensors, so do it once here - # a HA state write is triggered shortly afterwards and writes the "migrated_to_cache" extra state attribute - if last_state.state == STATE_ON: - migrated_state = IasZone.ZoneStatus.Alarm_1 - else: - migrated_state = IasZone.ZoneStatus(0) - - self._cluster_handler.cluster.update_attribute( - IasZone.attributes_by_name[self._attribute_name].id, migrated_state - ) - @STRICT_MATCH(cluster_handler_names=CLUSTER_HANDLER_ZONE, models={"WL4200", "WL4200S"}) class SinopeLeakStatus(BinarySensor): @@ -357,5 +336,4 @@ class AqaraE1CurtainMotorOpenedByHandBinarySensor(BinarySensor): _unique_id_suffix = "hand_open" _attribute_name = "hand_open" _attr_translation_key = "hand_open" - _attr_icon = "mdi:hand-wave" _attr_entity_category = EntityCategory.DIAGNOSTIC diff --git a/homeassistant/components/zha/button.py b/homeassistant/components/zha/button.py index 2c0028cd3d1..33102062443 100644 --- a/homeassistant/components/zha/button.py +++ b/homeassistant/components/zha/button.py @@ -1,15 +1,12 @@ """Support for ZHA button.""" + from __future__ import annotations import functools import logging from typing import TYPE_CHECKING, Any, Self -from zigpy.quirks.v2 import ( - EntityMetadata, - WriteAttributeButtonMetadata, - ZCLCommandButtonMetadata, -) +from zigpy.quirks.v2 import WriteAttributeButtonMetadata, ZCLCommandButtonMetadata from homeassistant.components.button import ButtonDeviceClass, ButtonEntity from homeassistant.config_entries import ConfigEntry @@ -19,7 +16,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from .core import discovery -from .core.const import CLUSTER_HANDLER_IDENTIFY, QUIRK_METADATA, SIGNAL_ADD_ENTITIES +from .core.const import CLUSTER_HANDLER_IDENTIFY, ENTITY_METADATA, SIGNAL_ADD_ENTITIES from .core.helpers import get_zha_data from .core.registries import ZHA_ENTITIES from .entity import ZhaEntity @@ -75,17 +72,18 @@ class ZHAButton(ZhaEntity, ButtonEntity): ) -> None: """Init this button.""" self._cluster_handler: ClusterHandler = cluster_handlers[0] - if QUIRK_METADATA in kwargs: - self._init_from_quirks_metadata(kwargs[QUIRK_METADATA]) + if ENTITY_METADATA in kwargs: + self._init_from_quirks_metadata(kwargs[ENTITY_METADATA]) super().__init__(unique_id, zha_device, cluster_handlers, **kwargs) - def _init_from_quirks_metadata(self, entity_metadata: EntityMetadata) -> None: + def _init_from_quirks_metadata( + self, entity_metadata: ZCLCommandButtonMetadata + ) -> None: """Init this entity from the quirks metadata.""" super()._init_from_quirks_metadata(entity_metadata) - button_metadata: ZCLCommandButtonMetadata = entity_metadata.entity_metadata - self._command_name = button_metadata.command_name - self._args = button_metadata.args - self._kwargs = button_metadata.kwargs + self._command_name = entity_metadata.command_name + self._args = entity_metadata.args + self._kwargs = entity_metadata.kwargs def get_args(self) -> list[Any]: """Return the arguments to use in the command.""" @@ -147,16 +145,17 @@ class ZHAAttributeButton(ZhaEntity, ButtonEntity): ) -> None: """Init this button.""" self._cluster_handler: ClusterHandler = cluster_handlers[0] - if QUIRK_METADATA in kwargs: - self._init_from_quirks_metadata(kwargs[QUIRK_METADATA]) + if ENTITY_METADATA in kwargs: + self._init_from_quirks_metadata(kwargs[ENTITY_METADATA]) super().__init__(unique_id, zha_device, cluster_handlers, **kwargs) - def _init_from_quirks_metadata(self, entity_metadata: EntityMetadata) -> None: + def _init_from_quirks_metadata( + self, entity_metadata: WriteAttributeButtonMetadata + ) -> None: """Init this entity from the quirks metadata.""" super()._init_from_quirks_metadata(entity_metadata) - button_metadata: WriteAttributeButtonMetadata = entity_metadata.entity_metadata - self._attribute_name = button_metadata.attribute_name - self._attribute_value = button_metadata.attribute_value + self._attribute_name = entity_metadata.attribute_name + self._attribute_value = entity_metadata.attribute_value async def async_press(self) -> None: """Write attribute with defined value.""" diff --git a/homeassistant/components/zha/climate.py b/homeassistant/components/zha/climate.py index cbc759e7008..61c5f28ca8f 100644 --- a/homeassistant/components/zha/climate.py +++ b/homeassistant/components/zha/climate.py @@ -3,6 +3,7 @@ For more details on this platform, please refer to the documentation at https://home-assistant.io/components/zha.climate/ """ + from __future__ import annotations from datetime import datetime, timedelta diff --git a/homeassistant/components/zha/config_flow.py b/homeassistant/components/zha/config_flow.py index 3e6dd05a8b5..42febb3b36d 100644 --- a/homeassistant/components/zha/config_flow.py +++ b/homeassistant/components/zha/config_flow.py @@ -1,4 +1,5 @@ """Config flow for ZHA.""" + from __future__ import annotations import collections @@ -12,15 +13,24 @@ import voluptuous as vol import zigpy.backups from zigpy.config import CONF_DEVICE, CONF_DEVICE_PATH -from homeassistant import config_entries from homeassistant.components import onboarding, usb, zeroconf from homeassistant.components.file_upload import process_uploaded_file from homeassistant.components.hassio import AddonError, AddonState from homeassistant.components.homeassistant_hardware import silabs_multiprotocol_addon from homeassistant.components.homeassistant_yellow import hardware as yellow_hardware +from homeassistant.config_entries import ( + SOURCE_IGNORE, + SOURCE_ZEROCONF, + ConfigEntry, + ConfigEntryBaseFlow, + ConfigEntryState, + ConfigFlow, + ConfigFlowResult, + OperationNotAllowed, + OptionsFlow, +) from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant, callback -from homeassistant.data_entry_flow import FlowHandler, FlowResult from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.selector import FileSelector, FileSelectorConfig from homeassistant.util import dt as dt_util @@ -122,7 +132,7 @@ async def list_serial_ports(hass: HomeAssistant) -> list[ListPortInfo]: return ports -class BaseZhaFlow(FlowHandler): +class BaseZhaFlow(ConfigEntryBaseFlow): """Mixin for common ZHA flow steps and forms.""" _hass: HomeAssistant @@ -146,7 +156,7 @@ class BaseZhaFlow(FlowHandler): self._hass = hass self._radio_mgr.hass = hass - async def _async_create_radio_entry(self) -> FlowResult: + async def _async_create_radio_entry(self) -> ConfigFlowResult: """Create a config entry with the current flow state.""" assert self._title is not None assert self._radio_mgr.radio_type is not None @@ -168,7 +178,7 @@ class BaseZhaFlow(FlowHandler): async def async_step_choose_serial_port( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Choose a serial port.""" ports = await list_serial_ports(self.hass) list_of_ports = [ @@ -232,7 +242,7 @@ class BaseZhaFlow(FlowHandler): async def async_step_manual_pick_radio_type( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Manually select the radio type.""" if user_input is not None: self._radio_mgr.radio_type = RadioType.get_by_description( @@ -257,7 +267,7 @@ class BaseZhaFlow(FlowHandler): async def async_step_manual_port_config( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Enter port settings specific for this type of radio.""" assert self._radio_mgr.radio_type is not None errors = {} @@ -286,7 +296,7 @@ class BaseZhaFlow(FlowHandler): if param not in SUPPORTED_PORT_SETTINGS: continue - if source == config_entries.SOURCE_ZEROCONF and param == CONF_BAUDRATE: + if source == SOURCE_ZEROCONF and param == CONF_BAUDRATE: value = 115200 param = vol.Required(CONF_BAUDRATE, default=value) elif ( @@ -307,7 +317,7 @@ class BaseZhaFlow(FlowHandler): async def async_step_verify_radio( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Add a warning step to dissuade the use of deprecated radios.""" assert self._radio_mgr.radio_type is not None @@ -327,7 +337,7 @@ class BaseZhaFlow(FlowHandler): async def async_step_choose_formation_strategy( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Choose how to deal with the current radio's settings.""" await self._radio_mgr.async_load_network_settings() @@ -370,20 +380,20 @@ class BaseZhaFlow(FlowHandler): async def async_step_reuse_settings( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Reuse the existing network settings on the stick.""" return await self._async_create_radio_entry() async def async_step_form_initial_network( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Form an initial network.""" # This step exists only for translations, it does nothing new return await self.async_step_form_new_network(user_input) async def async_step_form_new_network( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Form a brand-new network.""" await self._radio_mgr.async_form_network() return await self._async_create_radio_entry() @@ -399,7 +409,7 @@ class BaseZhaFlow(FlowHandler): async def async_step_upload_manual_backup( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Upload and restore a coordinator backup JSON file.""" errors = {} @@ -427,7 +437,7 @@ class BaseZhaFlow(FlowHandler): async def async_step_choose_automatic_backup( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Choose an automatic backup.""" if self.show_advanced_options: # Always show the PAN IDs when in advanced mode @@ -466,7 +476,7 @@ class BaseZhaFlow(FlowHandler): async def async_step_maybe_confirm_ezsp_restore( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm restore for EZSP radios that require permanent IEEE writes.""" call_step_2 = await self._radio_mgr.async_restore_backup_step_1() if not call_step_2: @@ -486,7 +496,7 @@ class BaseZhaFlow(FlowHandler): ) -class ZhaConfigFlowHandler(BaseZhaFlow, config_entries.ConfigFlow, domain=DOMAIN): +class ZhaConfigFlowHandler(BaseZhaFlow, ConfigFlow, domain=DOMAIN): """Handle a config flow.""" VERSION = 4 @@ -500,7 +510,7 @@ class ZhaConfigFlowHandler(BaseZhaFlow, config_entries.ConfigFlow, domain=DOMAIN if not current_entry: return - if current_entry.source != config_entries.SOURCE_IGNORE: + if current_entry.source != SOURCE_IGNORE: self._abort_if_unique_id_configured() else: # Only update the current entry if it is an ignored discovery @@ -516,14 +526,14 @@ class ZhaConfigFlowHandler(BaseZhaFlow, config_entries.ConfigFlow, domain=DOMAIN @staticmethod @callback def async_get_options_flow( - config_entry: config_entries.ConfigEntry, - ) -> config_entries.OptionsFlow: + config_entry: ConfigEntry, + ) -> OptionsFlow: """Create the options flow.""" return ZhaOptionsFlowHandler(config_entry) async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a ZHA config flow start.""" if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") @@ -532,7 +542,7 @@ class ZhaConfigFlowHandler(BaseZhaFlow, config_entries.ConfigFlow, domain=DOMAIN async def async_step_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm a discovery.""" self._set_confirm_only() @@ -570,7 +580,9 @@ class ZhaConfigFlowHandler(BaseZhaFlow, config_entries.ConfigFlow, domain=DOMAIN description_placeholders={CONF_NAME: self._title}, ) - async def async_step_usb(self, discovery_info: usb.UsbServiceInfo) -> FlowResult: + async def async_step_usb( + self, discovery_info: usb.UsbServiceInfo + ) -> ConfigFlowResult: """Handle usb discovery.""" vid = discovery_info.vid pid = discovery_info.pid @@ -589,7 +601,7 @@ class ZhaConfigFlowHandler(BaseZhaFlow, config_entries.ConfigFlow, domain=DOMAIN if self.hass.config_entries.flow.async_progress_by_handler(DECONZ_DOMAIN): return self.async_abort(reason="not_zha_device") for entry in self.hass.config_entries.async_entries(DECONZ_DOMAIN): - if entry.source != config_entries.SOURCE_IGNORE: + if entry.source != SOURCE_IGNORE: return self.async_abort(reason="not_zha_device") self._radio_mgr.device_path = dev_path @@ -606,7 +618,7 @@ class ZhaConfigFlowHandler(BaseZhaFlow, config_entries.ConfigFlow, domain=DOMAIN async def async_step_zeroconf( self, discovery_info: zeroconf.ZeroconfServiceInfo - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle zeroconf discovery.""" # Hostname is format: livingroom.local. @@ -642,7 +654,7 @@ class ZhaConfigFlowHandler(BaseZhaFlow, config_entries.ConfigFlow, domain=DOMAIN async def async_step_hardware( self, data: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle hardware flow.""" try: discovery_data = HARDWARE_DISCOVERY_SCHEMA(data) @@ -668,10 +680,10 @@ class ZhaConfigFlowHandler(BaseZhaFlow, config_entries.ConfigFlow, domain=DOMAIN return await self.async_step_confirm() -class ZhaOptionsFlowHandler(BaseZhaFlow, config_entries.OptionsFlow): +class ZhaOptionsFlowHandler(BaseZhaFlow, OptionsFlow): """Handle an options flow.""" - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + def __init__(self, config_entry: ConfigEntry) -> None: """Initialize options flow.""" super().__init__() self.config_entry = config_entry @@ -683,11 +695,11 @@ class ZhaOptionsFlowHandler(BaseZhaFlow, config_entries.OptionsFlow): async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Launch the options flow.""" if user_input is not None: # OperationNotAllowed: ZHA is not running - with suppress(config_entries.OperationNotAllowed): + with suppress(OperationNotAllowed): await self.hass.config_entries.async_unload(self.config_entry.entry_id) return await self.async_step_prompt_migrate_or_reconfigure() @@ -696,7 +708,7 @@ class ZhaOptionsFlowHandler(BaseZhaFlow, config_entries.OptionsFlow): async def async_step_prompt_migrate_or_reconfigure( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm if we are migrating adapters or just re-configuring.""" return self.async_show_menu( @@ -709,13 +721,13 @@ class ZhaOptionsFlowHandler(BaseZhaFlow, config_entries.OptionsFlow): async def async_step_intent_reconfigure( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Virtual step for when the user is reconfiguring the integration.""" return await self.async_step_choose_serial_port() async def async_step_intent_migrate( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm the user wants to reset their current radio.""" if user_input is not None: @@ -727,7 +739,7 @@ class ZhaOptionsFlowHandler(BaseZhaFlow, config_entries.OptionsFlow): async def async_step_instruct_unplug( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Instruct the user to unplug the current radio, if possible.""" if user_input is not None: @@ -762,8 +774,8 @@ class ZhaOptionsFlowHandler(BaseZhaFlow, config_entries.OptionsFlow): def async_remove(self): """Maybe reload ZHA if the flow is aborted.""" if self.config_entry.state not in ( - config_entries.ConfigEntryState.SETUP_ERROR, - config_entries.ConfigEntryState.NOT_LOADED, + ConfigEntryState.SETUP_ERROR, + ConfigEntryState.NOT_LOADED, ): return diff --git a/homeassistant/components/zha/core/cluster_handlers/__init__.py b/homeassistant/components/zha/core/cluster_handlers/__init__.py index 1f7485d4922..ae7b0945230 100644 --- a/homeassistant/components/zha/core/cluster_handlers/__init__.py +++ b/homeassistant/components/zha/core/cluster_handlers/__init__.py @@ -1,4 +1,5 @@ """Cluster handlers module for Zigbee Home Automation.""" + from __future__ import annotations from collections.abc import Awaitable, Callable, Coroutine, Iterator @@ -334,9 +335,9 @@ class ClusterHandler(LogMixin): return for record in res: - event_data[self.cluster.find_attribute(record.attrid).name][ - "status" - ] = record.status.name + event_data[self.cluster.find_attribute(record.attrid).name]["status"] = ( + record.status.name + ) failed = [ self.cluster.find_attribute(record.attrid).name for record in res @@ -555,7 +556,7 @@ class ClusterHandler(LogMixin): def log(self, level, msg, *args, **kwargs): """Log a message.""" msg = f"[%s:%s]: {msg}" - args = (self._endpoint.device.nwk, self._id) + args + args = (self._endpoint.device.nwk, self._id, *args) _LOGGER.log(level, msg, *args, **kwargs) def __getattr__(self, name): @@ -619,7 +620,7 @@ class ZDOClusterHandler(LogMixin): def log(self, level, msg, *args, **kwargs): """Log a message.""" msg = f"[%s:ZDO](%s): {msg}" - args = (self._zha_device.nwk, self._zha_device.model) + args + args = (self._zha_device.nwk, self._zha_device.model, *args) _LOGGER.log(level, msg, *args, **kwargs) diff --git a/homeassistant/components/zha/core/cluster_handlers/closures.py b/homeassistant/components/zha/core/cluster_handlers/closures.py index 879765aec3c..c57ad507317 100644 --- a/homeassistant/components/zha/core/cluster_handlers/closures.py +++ b/homeassistant/components/zha/core/cluster_handlers/closures.py @@ -1,4 +1,5 @@ """Closures cluster handlers module for Zigbee Home Automation.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/zha/core/cluster_handlers/general.py b/homeassistant/components/zha/core/cluster_handlers/general.py index d2927f6d028..438fc6b1723 100644 --- a/homeassistant/components/zha/core/cluster_handlers/general.py +++ b/homeassistant/components/zha/core/cluster_handlers/general.py @@ -1,4 +1,5 @@ """General cluster handlers module for Zigbee Home Automation.""" + from __future__ import annotations from collections.abc import Coroutine @@ -552,6 +553,13 @@ class OtaClientClusterHandler(ClientClusterHandler): Ota.AttributeDefs.current_file_version.name: True, } + @callback + def attribute_updated(self, attrid: int, value: Any, timestamp: Any) -> None: + """Handle an attribute updated on this cluster.""" + # We intentionally avoid the `ClientClusterHandler` attribute update handler: + # it emits a logbook event on every update, which pollutes the logbook + ClusterHandler.attribute_updated(self, attrid, value, timestamp) + @property def current_file_version(self) -> int | None: """Return cached value of current_file_version attribute.""" diff --git a/homeassistant/components/zha/core/cluster_handlers/helpers.py b/homeassistant/components/zha/core/cluster_handlers/helpers.py index f4444f3995c..46557bf23a8 100644 --- a/homeassistant/components/zha/core/cluster_handlers/helpers.py +++ b/homeassistant/components/zha/core/cluster_handlers/helpers.py @@ -1,4 +1,5 @@ """Helpers for use with ZHA Zigbee cluster handlers.""" + from . import ClusterHandler diff --git a/homeassistant/components/zha/core/cluster_handlers/homeautomation.py b/homeassistant/components/zha/core/cluster_handlers/homeautomation.py index bb7b96d367e..b287cb98f6a 100644 --- a/homeassistant/components/zha/core/cluster_handlers/homeautomation.py +++ b/homeassistant/components/zha/core/cluster_handlers/homeautomation.py @@ -1,4 +1,5 @@ """Home automation cluster handlers module for Zigbee Home Automation.""" + from __future__ import annotations import enum diff --git a/homeassistant/components/zha/core/cluster_handlers/hvac.py b/homeassistant/components/zha/core/cluster_handlers/hvac.py index 4c03d31135e..d455ade4e66 100644 --- a/homeassistant/components/zha/core/cluster_handlers/hvac.py +++ b/homeassistant/components/zha/core/cluster_handlers/hvac.py @@ -3,6 +3,7 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/integrations/zha/ """ + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/zha/core/cluster_handlers/lighting.py b/homeassistant/components/zha/core/cluster_handlers/lighting.py index bb3ac3c80e3..f19ad311f9e 100644 --- a/homeassistant/components/zha/core/cluster_handlers/lighting.py +++ b/homeassistant/components/zha/core/cluster_handlers/lighting.py @@ -1,4 +1,5 @@ """Lighting cluster handlers module for Zigbee Home Automation.""" + from __future__ import annotations from zigpy.zcl.clusters.lighting import Ballast, Color diff --git a/homeassistant/components/zha/core/cluster_handlers/manufacturerspecific.py b/homeassistant/components/zha/core/cluster_handlers/manufacturerspecific.py index 608a256606f..dc8af821724 100644 --- a/homeassistant/components/zha/core/cluster_handlers/manufacturerspecific.py +++ b/homeassistant/components/zha/core/cluster_handlers/manufacturerspecific.py @@ -1,4 +1,5 @@ """Manufacturer specific cluster handlers module for Zigbee Home Automation.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/zha/core/cluster_handlers/measurement.py b/homeassistant/components/zha/core/cluster_handlers/measurement.py index be079328228..768de8c4c73 100644 --- a/homeassistant/components/zha/core/cluster_handlers/measurement.py +++ b/homeassistant/components/zha/core/cluster_handlers/measurement.py @@ -1,4 +1,5 @@ """Measurement cluster handlers module for Zigbee Home Automation.""" + from __future__ import annotations from typing import TYPE_CHECKING diff --git a/homeassistant/components/zha/core/cluster_handlers/protocol.py b/homeassistant/components/zha/core/cluster_handlers/protocol.py index 14f01a55b6a..e1e3d7a5413 100644 --- a/homeassistant/components/zha/core/cluster_handlers/protocol.py +++ b/homeassistant/components/zha/core/cluster_handlers/protocol.py @@ -1,4 +1,5 @@ """Protocol cluster handlers module for Zigbee Home Automation.""" + from zigpy.zcl.clusters.protocol import ( AnalogInputExtended, AnalogInputRegular, diff --git a/homeassistant/components/zha/core/cluster_handlers/security.py b/homeassistant/components/zha/core/cluster_handlers/security.py index c37fdc43766..8ebe09cef03 100644 --- a/homeassistant/components/zha/core/cluster_handlers/security.py +++ b/homeassistant/components/zha/core/cluster_handlers/security.py @@ -3,6 +3,7 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/integrations/zha/ """ + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/zha/core/cluster_handlers/smartenergy.py b/homeassistant/components/zha/core/cluster_handlers/smartenergy.py index 65a02a01e02..d167b8b1752 100644 --- a/homeassistant/components/zha/core/cluster_handlers/smartenergy.py +++ b/homeassistant/components/zha/core/cluster_handlers/smartenergy.py @@ -1,4 +1,5 @@ """Smart energy cluster handlers module for Zigbee Home Automation.""" + from __future__ import annotations import enum diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index fd54351739e..74110d390ed 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -1,4 +1,5 @@ """All constants related to the ZHA component.""" + from __future__ import annotations import enum @@ -219,6 +220,8 @@ DISCOVERY_KEY = "zha_discovery_info" DOMAIN = "zha" +ENTITY_METADATA = "entity_metadata" + GROUP_ID = "group_id" GROUP_IDS = "group_ids" GROUP_NAME = "group_name" @@ -232,8 +235,6 @@ PRESET_SCHEDULE = "Schedule" PRESET_COMPLEX = "Complex" PRESET_TEMP_MANUAL = "Temporary manual" -QUIRK_METADATA = "quirk_metadata" - ZCL_INIT_ATTRS = "ZCL_INIT_ATTRS" ZHA_ALARM_OPTIONS = "zha_alarm_options" diff --git a/homeassistant/components/zha/core/decorators.py b/homeassistant/components/zha/core/decorators.py index 192f6848989..b8e15024811 100644 --- a/homeassistant/components/zha/core/decorators.py +++ b/homeassistant/components/zha/core/decorators.py @@ -1,4 +1,5 @@ """Decorators for ZHA core registries.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index f1b7ec60728..e6d9f3e66b5 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -1,4 +1,5 @@ """Device for Zigbee Home Automation.""" + from __future__ import annotations import asyncio @@ -1004,5 +1005,5 @@ class ZHADevice(LogMixin): def log(self, level: int, msg: str, *args: Any, **kwargs: Any) -> None: """Log a message.""" msg = f"[%s](%s): {msg}" - args = (self.nwk, self.model) + args + args = (self.nwk, self.model, *args) _LOGGER.log(level, msg, *args, **kwargs) diff --git a/homeassistant/components/zha/core/discovery.py b/homeassistant/components/zha/core/discovery.py index 221c601827e..3c342d14060 100644 --- a/homeassistant/components/zha/core/discovery.py +++ b/homeassistant/components/zha/core/discovery.py @@ -1,4 +1,5 @@ """Device discovery functions for Zigbee Home Automation.""" + from __future__ import annotations from collections import Counter @@ -84,12 +85,18 @@ QUIRKS_ENTITY_META_TO_ENTITY_CLASS = { WriteAttributeButtonMetadata, EntityType.CONFIG, ): button.ZHAAttributeButton, + ( + Platform.BUTTON, + WriteAttributeButtonMetadata, + EntityType.STANDARD, + ): button.ZHAAttributeButton, (Platform.BUTTON, ZCLCommandButtonMetadata, EntityType.CONFIG): button.ZHAButton, ( Platform.BUTTON, ZCLCommandButtonMetadata, EntityType.DIAGNOSTIC, ): button.ZHAButton, + (Platform.BUTTON, ZCLCommandButtonMetadata, EntityType.STANDARD): button.ZHAButton, ( Platform.BINARY_SENSOR, BinarySensorMetadata, @@ -110,6 +117,7 @@ QUIRKS_ENTITY_META_TO_ENTITY_CLASS = { (Platform.SENSOR, ZCLSensorMetadata, EntityType.DIAGNOSTIC): sensor.Sensor, (Platform.SENSOR, ZCLSensorMetadata, EntityType.STANDARD): sensor.Sensor, (Platform.SELECT, ZCLEnumMetadata, EntityType.CONFIG): select.ZCLEnumSelectEntity, + (Platform.SELECT, ZCLEnumMetadata, EntityType.STANDARD): select.ZCLEnumSelectEntity, ( Platform.SELECT, ZCLEnumMetadata, @@ -223,7 +231,7 @@ class ProbeEndpoint: for ( cluster_details, - quirk_metadata_list, + entity_metadata_list, ) in zigpy_device.exposes_metadata.items(): endpoint_id, cluster_id, cluster_type = cluster_details @@ -264,11 +272,11 @@ class ProbeEndpoint: ) assert cluster_handler - for quirk_metadata in quirk_metadata_list: - platform = Platform(quirk_metadata.entity_platform.value) - metadata_type = type(quirk_metadata.entity_metadata) + for entity_metadata in entity_metadata_list: + platform = Platform(entity_metadata.entity_platform.value) + metadata_type = type(entity_metadata) entity_class = QUIRKS_ENTITY_META_TO_ENTITY_CLASS.get( - (platform, metadata_type, quirk_metadata.entity_type) + (platform, metadata_type, entity_metadata.entity_type) ) if entity_class is None: @@ -279,7 +287,7 @@ class ProbeEndpoint: device.name, { zha_const.CLUSTER_DETAILS: cluster_details, - zha_const.QUIRK_METADATA: quirk_metadata, + zha_const.ENTITY_METADATA: entity_metadata, }, ) continue @@ -287,14 +295,14 @@ class ProbeEndpoint: # automatically add the attribute to ZCL_INIT_ATTRS for the cluster # handler if it is not already in the list if ( - hasattr(quirk_metadata.entity_metadata, "attribute_name") - and quirk_metadata.entity_metadata.attribute_name + hasattr(entity_metadata, "attribute_name") + and entity_metadata.attribute_name not in cluster_handler.ZCL_INIT_ATTRS ): init_attrs = cluster_handler.ZCL_INIT_ATTRS.copy() - init_attrs[ - quirk_metadata.entity_metadata.attribute_name - ] = quirk_metadata.attribute_initialized_from_cache + init_attrs[entity_metadata.attribute_name] = ( + entity_metadata.attribute_initialized_from_cache + ) cluster_handler.__dict__[zha_const.ZCL_INIT_ATTRS] = init_attrs endpoint.async_new_entity( @@ -302,7 +310,7 @@ class ProbeEndpoint: entity_class, endpoint.unique_id, [cluster_handler], - quirk_metadata=quirk_metadata, + entity_metadata=entity_metadata, ) _LOGGER.debug( diff --git a/homeassistant/components/zha/core/endpoint.py b/homeassistant/components/zha/core/endpoint.py index 37a2c951a7f..b0d617eb2c2 100644 --- a/homeassistant/components/zha/core/endpoint.py +++ b/homeassistant/components/zha/core/endpoint.py @@ -1,4 +1,5 @@ """Representation of a Zigbee endpoint for zha.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index 14fd329f1bc..83aa12fbfa1 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -1,4 +1,5 @@ """Virtual gateway for Zigbee Home Automation.""" + from __future__ import annotations import asyncio @@ -29,7 +30,7 @@ from zigpy.state import State 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.components.system_log import LogEntry from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry as dr, entity_registry as er @@ -132,9 +133,9 @@ class ZHAGateway: self._groups: dict[int, ZHAGroup] = {} self.application_controller: ControllerApplication = None self.coordinator_zha_device: ZHADevice = None # type: ignore[assignment] - self._device_registry: collections.defaultdict[ - EUI64, list[EntityReference] - ] = collections.defaultdict(list) + self._device_registry: collections.defaultdict[EUI64, list[EntityReference]] = ( + collections.defaultdict(list) + ) self._log_levels: dict[str, dict[str, int]] = { DEBUG_LEVEL_ORIGINAL: async_capture_log_levels(), DEBUG_LEVEL_CURRENT: async_capture_log_levels(), @@ -451,9 +452,9 @@ class ZHAGateway: self, device: ZHADevice, entity_refs: list[EntityReference] | None ) -> None: if entity_refs is not None: - remove_tasks: list[asyncio.Future[Any]] = [] - for entity_ref in entity_refs: - remove_tasks.append(entity_ref.remove_future) + remove_tasks: list[asyncio.Future[Any]] = [ + entity_ref.remove_future for entity_ref in entity_refs + ] if remove_tasks: await asyncio.wait(remove_tasks) @@ -782,9 +783,7 @@ class ZHAGateway: _LOGGER.debug("Group: 0x%04x could not be found", group_id) return if group.members: - tasks = [] - for member in group.members: - tasks.append(member.async_remove_from_group()) + tasks = [member.async_remove_from_group() for member in group.members] if tasks: await asyncio.gather(*tasks) self.application_controller.groups.pop(group_id) @@ -870,10 +869,9 @@ class LogRelayHandler(logging.Handler): def emit(self, record: LogRecord) -> None: """Relay log message via dispatcher.""" - if record.levelno >= logging.WARN: - entry = LogEntry(record, _figure_out_source(record, self.paths_re)) - else: - entry = LogEntry(record, (record.pathname, record.lineno)) + entry = LogEntry( + record, self.paths_re, figure_out_source=record.levelno >= logging.WARNING + ) async_dispatcher_send( self.hass, ZHA_GW_MSG, diff --git a/homeassistant/components/zha/core/group.py b/homeassistant/components/zha/core/group.py index a62c00e7106..a6156ab63b7 100644 --- a/homeassistant/components/zha/core/group.py +++ b/homeassistant/components/zha/core/group.py @@ -1,4 +1,5 @@ """Group for Zigbee Home Automation.""" + from __future__ import annotations import asyncio @@ -130,7 +131,7 @@ class ZHAGroupMember(LogMixin): def log(self, level: int, msg: str, *args: Any, **kwargs) -> None: """Log a message.""" msg = f"[%s](%s): {msg}" - args = (f"0x{self._zha_group.group_id:04x}", self.endpoint_id) + args + args = (f"0x{self._zha_group.group_id:04x}", self.endpoint_id, *args) _LOGGER.log(level, msg, *args, **kwargs) @@ -175,13 +176,12 @@ class ZHAGroup(LogMixin): async def async_add_members(self, members: list[GroupMember]) -> None: """Add members to this group.""" if len(members) > 1: - tasks = [] - for member in members: - tasks.append( - self._zha_gateway.devices[member.ieee].async_add_endpoint_to_group( - member.endpoint_id, self.group_id - ) + tasks = [ + self._zha_gateway.devices[member.ieee].async_add_endpoint_to_group( + member.endpoint_id, self.group_id ) + for member in members + ] await asyncio.gather(*tasks) else: await self._zha_gateway.devices[ @@ -191,15 +191,12 @@ class ZHAGroup(LogMixin): async def async_remove_members(self, members: list[GroupMember]) -> None: """Remove members from this group.""" if len(members) > 1: - tasks = [] - for member in members: - tasks.append( - self._zha_gateway.devices[ - member.ieee - ].async_remove_endpoint_from_group( - member.endpoint_id, self.group_id - ) + tasks = [ + self._zha_gateway.devices[member.ieee].async_remove_endpoint_from_group( + member.endpoint_id, self.group_id ) + for member in members + ] await asyncio.gather(*tasks) else: await self._zha_gateway.devices[ @@ -209,12 +206,11 @@ class ZHAGroup(LogMixin): @property def member_entity_ids(self) -> list[str]: """Return the ZHA entity ids for all entities for the members of this group.""" - all_entity_ids: list[str] = [] - for member in self.members: - entity_references = member.associated_entities - for entity_reference in entity_references: - all_entity_ids.append(entity_reference["entity_id"]) - return all_entity_ids + return [ + entity_reference["entity_id"] + for member in self.members + for entity_reference in member.associated_entities + ] def get_domain_entity_ids(self, domain: str) -> list[str]: """Return entity ids from the entity domain for this group.""" @@ -246,5 +242,5 @@ class ZHAGroup(LogMixin): def log(self, level: int, msg: str, *args: Any, **kwargs) -> None: """Log a message.""" msg = f"[%s](%s): {msg}" - args = (self.name, self.group_id) + args + args = (self.name, self.group_id, *args) _LOGGER.log(level, msg, *args, **kwargs) diff --git a/homeassistant/components/zha/core/helpers.py b/homeassistant/components/zha/core/helpers.py index 5506ffb8289..1a001cab381 100644 --- a/homeassistant/components/zha/core/helpers.py +++ b/homeassistant/components/zha/core/helpers.py @@ -3,6 +3,7 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/integrations/zha/ """ + from __future__ import annotations import binascii @@ -13,7 +14,7 @@ from dataclasses import dataclass import enum import logging import re -from typing import TYPE_CHECKING, Any, ParamSpec, TypeVar +from typing import TYPE_CHECKING, Any, ParamSpec, TypeVar, overload import voluptuous as vol import zigpy.exceptions @@ -23,8 +24,33 @@ import zigpy.zcl from zigpy.zcl.foundation import CommandSchema import zigpy.zdo.types as zdo_types +from homeassistant.components.binary_sensor import BinarySensorDeviceClass +from homeassistant.components.number import NumberDeviceClass +from homeassistant.components.sensor import SensorDeviceClass from homeassistant.config_entries import ConfigEntry -from homeassistant.const import Platform +from homeassistant.const import ( + Platform, + UnitOfApparentPower, + UnitOfDataRate, + UnitOfElectricCurrent, + UnitOfElectricPotential, + UnitOfEnergy, + UnitOfFrequency, + UnitOfInformation, + UnitOfIrradiance, + UnitOfLength, + UnitOfMass, + UnitOfPower, + UnitOfPrecipitationDepth, + UnitOfPressure, + UnitOfSoundPressure, + UnitOfSpeed, + UnitOfTemperature, + UnitOfTime, + UnitOfVolume, + UnitOfVolumeFlowRate, + UnitOfVolumetricFlux, +) from homeassistant.core import HomeAssistant, State, callback from homeassistant.helpers import config_validation as cv, device_registry as dr from homeassistant.helpers.typing import ConfigType @@ -217,7 +243,7 @@ def async_get_zha_config_value( ) -def async_cluster_exists(hass, cluster_id, skip_coordinator=True): +def async_cluster_exists(hass: HomeAssistant, cluster_id, skip_coordinator=True): """Determine if a device containing the specified in cluster is paired.""" zha_gateway = get_zha_gateway(hass) zha_devices = zha_gateway.devices.values() @@ -423,3 +449,80 @@ def get_zha_gateway(hass: HomeAssistant) -> ZHAGateway: raise ValueError("No gateway object exists") return zha_gateway + + +UNITS_OF_MEASURE = { + UnitOfApparentPower.__name__: UnitOfApparentPower, + UnitOfPower.__name__: UnitOfPower, + UnitOfEnergy.__name__: UnitOfEnergy, + UnitOfElectricCurrent.__name__: UnitOfElectricCurrent, + UnitOfElectricPotential.__name__: UnitOfElectricPotential, + UnitOfTemperature.__name__: UnitOfTemperature, + UnitOfTime.__name__: UnitOfTime, + UnitOfLength.__name__: UnitOfLength, + UnitOfFrequency.__name__: UnitOfFrequency, + UnitOfPressure.__name__: UnitOfPressure, + UnitOfSoundPressure.__name__: UnitOfSoundPressure, + UnitOfVolume.__name__: UnitOfVolume, + UnitOfVolumeFlowRate.__name__: UnitOfVolumeFlowRate, + UnitOfMass.__name__: UnitOfMass, + UnitOfIrradiance.__name__: UnitOfIrradiance, + UnitOfVolumetricFlux.__name__: UnitOfVolumetricFlux, + UnitOfPrecipitationDepth.__name__: UnitOfPrecipitationDepth, + UnitOfSpeed.__name__: UnitOfSpeed, + UnitOfInformation.__name__: UnitOfInformation, + UnitOfDataRate.__name__: UnitOfDataRate, +} + + +def validate_unit(quirks_unit: enum.Enum) -> enum.Enum: + """Validate and return a unit of measure.""" + return UNITS_OF_MEASURE[type(quirks_unit).__name__](quirks_unit.value) + + +@overload +def validate_device_class( + device_class_enum: type[BinarySensorDeviceClass], + metadata_value, + platform: str, + logger: logging.Logger, +) -> BinarySensorDeviceClass | None: ... + + +@overload +def validate_device_class( + device_class_enum: type[SensorDeviceClass], + metadata_value, + platform: str, + logger: logging.Logger, +) -> SensorDeviceClass | None: ... + + +@overload +def validate_device_class( + device_class_enum: type[NumberDeviceClass], + metadata_value, + platform: str, + logger: logging.Logger, +) -> NumberDeviceClass | None: ... + + +def validate_device_class( + device_class_enum: type[BinarySensorDeviceClass] + | type[SensorDeviceClass] + | type[NumberDeviceClass], + metadata_value: enum.Enum, + platform: str, + logger: logging.Logger, +) -> BinarySensorDeviceClass | SensorDeviceClass | NumberDeviceClass | None: + """Validate and return a device class.""" + try: + return device_class_enum(metadata_value.value) + except ValueError as ex: + logger.warning( + "Quirks provided an invalid device class: %s for platform %s: %s", + metadata_value, + platform, + ex, + ) + return None diff --git a/homeassistant/components/zha/core/registries.py b/homeassistant/components/zha/core/registries.py index b302116694d..b9110a8dcde 100644 --- a/homeassistant/components/zha/core/registries.py +++ b/homeassistant/components/zha/core/registries.py @@ -1,4 +1,5 @@ """Mapping registries for Zigbee Home Automation.""" + from __future__ import annotations import collections @@ -107,12 +108,12 @@ DEVICE_CLASS = { DEVICE_CLASS = collections.defaultdict(dict, DEVICE_CLASS) CLUSTER_HANDLER_ONLY_CLUSTERS = SetRegistry() -CLIENT_CLUSTER_HANDLER_REGISTRY: DictRegistry[ - type[ClientClusterHandler] -] = DictRegistry() -ZIGBEE_CLUSTER_HANDLER_REGISTRY: NestedDictRegistry[ - type[ClusterHandler] -] = NestedDictRegistry() +CLIENT_CLUSTER_HANDLER_REGISTRY: DictRegistry[type[ClientClusterHandler]] = ( + DictRegistry() +) +ZIGBEE_CLUSTER_HANDLER_REGISTRY: NestedDictRegistry[type[ClusterHandler]] = ( + NestedDictRegistry() +) WEIGHT_ATTR = attrgetter("weight") @@ -277,9 +278,9 @@ class ZHAEntityRegistry: def __init__(self) -> None: """Initialize Registry instance.""" - self._strict_registry: dict[ - Platform, dict[MatchRule, type[ZhaEntity]] - ] = collections.defaultdict(dict) + self._strict_registry: dict[Platform, dict[MatchRule, type[ZhaEntity]]] = ( + collections.defaultdict(dict) + ) self._multi_entity_registry: dict[ Platform, dict[int | str | None, dict[MatchRule, list[type[ZhaEntity]]]] ] = collections.defaultdict( @@ -291,9 +292,9 @@ class ZHAEntityRegistry: lambda: collections.defaultdict(lambda: collections.defaultdict(list)) ) self._group_registry: dict[str, type[ZhaGroupEntity]] = {} - self.single_device_matches: dict[ - Platform, dict[EUI64, list[str]] - ] = collections.defaultdict(lambda: collections.defaultdict(list)) + self.single_device_matches: dict[Platform, dict[EUI64, list[str]]] = ( + collections.defaultdict(lambda: collections.defaultdict(list)) + ) def get_entity( self, @@ -323,9 +324,9 @@ class ZHAEntityRegistry: dict[Platform, list[EntityClassAndClusterHandlers]], list[ClusterHandler] ]: """Match ZHA cluster handlers to potentially multiple ZHA Entity classes.""" - result: dict[ - Platform, list[EntityClassAndClusterHandlers] - ] = collections.defaultdict(list) + result: dict[Platform, list[EntityClassAndClusterHandlers]] = ( + collections.defaultdict(list) + ) all_claimed: set[ClusterHandler] = set() for component, stop_match_groups in self._multi_entity_registry.items(): for stop_match_grp, matches in stop_match_groups.items(): @@ -356,9 +357,9 @@ class ZHAEntityRegistry: dict[Platform, list[EntityClassAndClusterHandlers]], list[ClusterHandler] ]: """Match ZHA cluster handlers to potentially multiple ZHA Entity classes.""" - result: dict[ - Platform, list[EntityClassAndClusterHandlers] - ] = collections.defaultdict(list) + result: dict[Platform, list[EntityClassAndClusterHandlers]] = ( + collections.defaultdict(list) + ) all_claimed: set[ClusterHandler] = set() for ( component, diff --git a/homeassistant/components/zha/cover.py b/homeassistant/components/zha/cover.py index d94a2f907d1..718b6fed3a2 100644 --- a/homeassistant/components/zha/cover.py +++ b/homeassistant/components/zha/cover.py @@ -1,4 +1,5 @@ """Support for ZHA covers.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/zha/device_action.py b/homeassistant/components/zha/device_action.py index d393cfb1471..076cb1d420e 100644 --- a/homeassistant/components/zha/device_action.py +++ b/homeassistant/components/zha/device_action.py @@ -1,4 +1,5 @@ """Provides device actions for ZHA devices.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/zha/device_tracker.py b/homeassistant/components/zha/device_tracker.py index ea27c58eb19..9c96fd0e346 100644 --- a/homeassistant/components/zha/device_tracker.py +++ b/homeassistant/components/zha/device_tracker.py @@ -1,4 +1,5 @@ """Support for the ZHA platform.""" + from __future__ import annotations import functools diff --git a/homeassistant/components/zha/diagnostics.py b/homeassistant/components/zha/diagnostics.py index 57088818c66..fff816777c0 100644 --- a/homeassistant/components/zha/diagnostics.py +++ b/homeassistant/components/zha/diagnostics.py @@ -1,4 +1,5 @@ """Provides diagnostics for ZHA.""" + from __future__ import annotations import dataclasses diff --git a/homeassistant/components/zha/entity.py b/homeassistant/components/zha/entity.py index 3f127c74c0e..f9f63321d44 100644 --- a/homeassistant/components/zha/entity.py +++ b/homeassistant/components/zha/entity.py @@ -1,4 +1,5 @@ """Entity for Zigbee Home Automation.""" + from __future__ import annotations import asyncio @@ -10,7 +11,7 @@ from typing import TYPE_CHECKING, Any, Self from zigpy.quirks.v2 import EntityMetadata, EntityType from homeassistant.const import ATTR_NAME, EntityCategory -from homeassistant.core import CALLBACK_TYPE, callback +from homeassistant.core import CALLBACK_TYPE, Event, callback from homeassistant.helpers import entity from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE, DeviceInfo @@ -23,7 +24,6 @@ from homeassistant.helpers.event import ( async_track_state_change_event, ) from homeassistant.helpers.restore_state import RestoreEntity -from homeassistant.helpers.typing import EventType from .core.const import ( ATTR_MANUFACTURER, @@ -140,7 +140,7 @@ class BaseZhaEntity(LogMixin, entity.Entity): def log(self, level: int, msg: str, *args, **kwargs): """Log a message.""" msg = f"%s: {msg}" - args = (self.entity_id,) + args + args = (self.entity_id, *args) _LOGGER.log(level, msg, *args, **kwargs) @@ -182,25 +182,28 @@ class ZhaEntity(BaseZhaEntity, RestoreEntity): if entity_metadata.initially_disabled: self._attr_entity_registry_enabled_default = False - if entity_metadata.translation_key: - self._attr_translation_key = entity_metadata.translation_key - - if hasattr(entity_metadata.entity_metadata, "attribute_name"): - if not entity_metadata.translation_key: - self._attr_translation_key = ( - entity_metadata.entity_metadata.attribute_name - ) - self._unique_id_suffix = entity_metadata.entity_metadata.attribute_name - elif hasattr(entity_metadata.entity_metadata, "command_name"): - if not entity_metadata.translation_key: - self._attr_translation_key = ( - entity_metadata.entity_metadata.command_name - ) - self._unique_id_suffix = entity_metadata.entity_metadata.command_name + has_device_class = hasattr(entity_metadata, "device_class") + has_attribute_name = hasattr(entity_metadata, "attribute_name") + has_command_name = hasattr(entity_metadata, "command_name") + if not has_device_class or ( + has_device_class and entity_metadata.device_class is None + ): + if entity_metadata.translation_key: + self._attr_translation_key = entity_metadata.translation_key + elif has_attribute_name: + self._attr_translation_key = entity_metadata.attribute_name + elif has_command_name: + self._attr_translation_key = entity_metadata.command_name + if has_attribute_name: + self._unique_id_suffix = entity_metadata.attribute_name + elif has_command_name: + self._unique_id_suffix = entity_metadata.command_name if entity_metadata.entity_type is EntityType.CONFIG: self._attr_entity_category = EntityCategory.CONFIG elif entity_metadata.entity_type is EntityType.DIAGNOSTIC: self._attr_entity_category = EntityCategory.DIAGNOSTIC + else: + self._attr_entity_category = None @property def available(self) -> bool: @@ -311,6 +314,10 @@ class ZhaGroupEntity(BaseZhaEntity): self._handled_group_membership = True await self.async_remove(force_remove=True) + if len(self._group.members) >= 2: + async_dispatcher_send( + self.hass, SIGNAL_GROUP_ENTITY_REMOVED, self._group_id + ) async def async_added_to_hass(self) -> None: """Register callbacks.""" @@ -337,17 +344,8 @@ class ZhaGroupEntity(BaseZhaEntity): self.hass, self._entity_ids, self.async_state_changed_listener ) - def send_removed_signal(): - async_dispatcher_send( - self.hass, SIGNAL_GROUP_ENTITY_REMOVED, self._group_id - ) - - self.async_on_remove(send_removed_signal) - @callback - def async_state_changed_listener( - self, event: EventType[EventStateChangedData] - ) -> None: + def async_state_changed_listener(self, event: Event[EventStateChangedData]) -> None: """Handle child updates.""" # Delay to ensure that we get updates from all members before updating the group assert self._change_listener_debouncer diff --git a/homeassistant/components/zha/fan.py b/homeassistant/components/zha/fan.py index 7364aed0d1b..3677befb76e 100644 --- a/homeassistant/components/zha/fan.py +++ b/homeassistant/components/zha/fan.py @@ -1,4 +1,5 @@ """Fans on Zigbee Home Automation networks.""" + from __future__ import annotations from abc import abstractmethod diff --git a/homeassistant/components/zha/icons.json b/homeassistant/components/zha/icons.json new file mode 100644 index 00000000000..9b060e8105a --- /dev/null +++ b/homeassistant/components/zha/icons.json @@ -0,0 +1,178 @@ +{ + "entity": { + "binary_sensor": { + "hand_open": { + "default": "mdi:hand-wave" + } + }, + "number": { + "timer_duration": { + "default": "mdi:timer" + }, + "filter_life_time": { + "default": "mdi:timer" + }, + "dimming_speed_up_remote": { + "default": "mdi:speedometer" + }, + "button_delay": { + "default": "mdi:speedometer" + }, + "dimming_speed_up_local": { + "default": "mdi:speedometer" + }, + "ramp_rate_off_to_on_local": { + "default": "mdi:speedometer" + }, + "ramp_rate_off_to_on_remote": { + "default": "mdi:speedometer" + }, + "dimming_speed_down_remote": { + "default": "mdi:speedometer" + }, + "dimming_speed_down_local": { + "default": "mdi:speedometer" + }, + "ramp_rate_on_to_off_local": { + "default": "mdi:speedometer" + }, + "ramp_rate_on_to_off_remote": { + "default": "mdi:speedometer" + }, + "minimum_level": { + "default": "mdi:brightness-percent" + }, + "maximum_level": { + "default": "mdi:brightness-percent" + }, + "auto_off_timer": { + "default": "mdi:timer" + }, + "quick_start_time": { + "default": "mdi:speedometer" + }, + "load_level_indicator_timeout": { + "default": "mdi:timer" + }, + "led_color_when_on": { + "default": "mdi:palette" + }, + "led_color_when_off": { + "default": "mdi:palette" + }, + "led_intensity_when_on": { + "default": "mdi:brightness-percent" + }, + "led_intensity_when_off": { + "default": "mdi:brightness-percent" + }, + "double_tap_up_level": { + "default": "mdi:brightness-percent" + }, + "double_tap_down_level": { + "default": "mdi:brightness-percent" + }, + "serving_size": { + "default": "mdi:counter" + }, + "portion_weight": { + "default": "mdi:weight-gram" + }, + "away_preset_temperature": { + "default": "mdi:temperature-celsius" + }, + "local_temperature_calibration": { + "default": "mdi:temperature-celsius" + }, + "presence_detection_timeout": { + "default": "mdi:timer-edit" + } + }, + "select": { + "feeding_mode": { + "default": "mdi:wrench-clock" + }, + "keypad_lockout": { + "default": "mdi:lock" + } + }, + "sensor": { + "timer_time_left": { + "default": "mdi:timer" + }, + "device_run_time": { + "default": "mdi:timer" + }, + "filter_run_time": { + "default": "mdi:timer" + }, + "last_feeding_source": { + "default": "mdi:devices" + }, + "last_feeding_size": { + "default": "mdi:counter" + }, + "portions_dispensed_today": { + "default": "mdi:counter" + }, + "weight_dispensed_today": { + "default": "mdi:weight-gram" + }, + "smoke_density": { + "default": "mdi:google-circles-communities" + }, + "last_illumination_state": { + "default": "mdi:theme-light-dark" + }, + "pi_heating_demand": { + "default": "mdi:radiator" + }, + "setpoint_change_source": { + "default": "mdi:thermostat" + }, + "hooks_state": { + "default": "mdi:hook" + } + }, + "switch": { + "led_indicator": { + "default": "mdi:led-on" + }, + "child_lock": { + "default": "mdi:account-lock" + }, + "heartbeat_indicator": { + "default": "mdi:heart-flash" + }, + "linkage_alarm": { + "default": "mdi:shield-link-variant" + }, + "buzzer_manual_mute": { + "default": "mdi:volume-off" + }, + "buzzer_manual_alarm": { + "default": "mdi:bullhorn" + }, + "inverted": { + "default": "mdi:arrow-up-down" + }, + "hooks_locked": { + "default": "mdi:lock" + } + } + }, + "services": { + "permit": "mdi:cellphone-link", + "remove": "mdi:cellphone-remove", + "reconfigure_device": "mdi:cellphone-cog", + "set_zigbee_cluster_attribute": "mdi:cog", + "issue_zigbee_cluster_command": "mdi:console", + "issue_zigbee_group_command": "mdi:console", + "warning_device_squawk": "mdi:alert", + "warning_device_warn": "mdi:alert", + "clear_lock_user_code": "mdi:lock-remove", + "enable_lock_user_code": "mdi:lock", + "disable_lock_user_code": "mdi:lock-off", + "set_lock_user_code": "mdi:lock" + } +} diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index aa117c7ef9b..8d65899707e 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -1,4 +1,5 @@ """Lights on Zigbee Home Automation networks.""" + from __future__ import annotations from collections import Counter diff --git a/homeassistant/components/zha/lock.py b/homeassistant/components/zha/lock.py index ccfb5434154..fa719075c05 100644 --- a/homeassistant/components/zha/lock.py +++ b/homeassistant/components/zha/lock.py @@ -1,4 +1,5 @@ """Locks on Zigbee Home Automation networks.""" + import functools from typing import Any diff --git a/homeassistant/components/zha/logbook.py b/homeassistant/components/zha/logbook.py index ce9c1f1227b..e63ef565824 100644 --- a/homeassistant/components/zha/logbook.py +++ b/homeassistant/components/zha/logbook.py @@ -1,4 +1,5 @@ """Describe ZHA logbook events.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index fc050c9b2d1..e9d75584064 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -6,7 +6,6 @@ "config_flow": true, "dependencies": ["file_upload"], "documentation": "https://www.home-assistant.io/integrations/zha", - "import_executor": true, "iot_class": "local_polling", "loggers": [ "aiosqlite", @@ -25,9 +24,9 @@ "bellows==0.38.1", "pyserial==3.5", "pyserial-asyncio==0.6", - "zha-quirks==0.0.112", + "zha-quirks==0.0.113", "zigpy-deconz==0.23.1", - "zigpy==0.63.4", + "zigpy==0.63.5", "zigpy-xbee==0.20.1", "zigpy-zigate==0.12.0", "zigpy-znp==0.12.1", diff --git a/homeassistant/components/zha/number.py b/homeassistant/components/zha/number.py index c452752f14b..8af2fe178c8 100644 --- a/homeassistant/components/zha/number.py +++ b/homeassistant/components/zha/number.py @@ -1,14 +1,15 @@ """Support for ZHA AnalogOutput cluster.""" + from __future__ import annotations import functools import logging from typing import TYPE_CHECKING, Any, Self -from zigpy.quirks.v2 import EntityMetadata, NumberMetadata +from zigpy.quirks.v2 import NumberMetadata from zigpy.zcl.clusters.hvac import Thermostat -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 EntityCategory, Platform, UnitOfMass, UnitOfTemperature from homeassistant.core import HomeAssistant, callback @@ -25,11 +26,11 @@ from .core.const import ( CLUSTER_HANDLER_LEVEL, CLUSTER_HANDLER_OCCUPANCY, CLUSTER_HANDLER_THERMOSTAT, - QUIRK_METADATA, + ENTITY_METADATA, SIGNAL_ADD_ENTITIES, SIGNAL_ATTR_UPDATED, ) -from .core.helpers import get_zha_data +from .core.helpers import get_zha_data, validate_device_class, validate_unit from .core.registries import ZHA_ENTITIES from .entity import ZhaEntity @@ -402,7 +403,7 @@ class ZHANumberConfigurationEntity(ZhaEntity, NumberEntity): Return entity if it is a supported configuration, otherwise return None """ cluster_handler = cluster_handlers[0] - if QUIRK_METADATA not in kwargs and ( + if ENTITY_METADATA not in kwargs and ( cls._attribute_name in cluster_handler.cluster.unsupported_attributes or cls._attribute_name not in cluster_handler.cluster.attributes_by_name or cluster_handler.cluster.get(cls._attribute_name) is None @@ -425,26 +426,34 @@ class ZHANumberConfigurationEntity(ZhaEntity, NumberEntity): ) -> None: """Init this number configuration entity.""" self._cluster_handler: ClusterHandler = cluster_handlers[0] - if QUIRK_METADATA in kwargs: - self._init_from_quirks_metadata(kwargs[QUIRK_METADATA]) + if ENTITY_METADATA in kwargs: + self._init_from_quirks_metadata(kwargs[ENTITY_METADATA]) super().__init__(unique_id, zha_device, cluster_handlers, **kwargs) - def _init_from_quirks_metadata(self, entity_metadata: EntityMetadata) -> None: + def _init_from_quirks_metadata(self, entity_metadata: NumberMetadata) -> None: """Init this entity from the quirks metadata.""" super()._init_from_quirks_metadata(entity_metadata) - number_metadata: NumberMetadata = entity_metadata.entity_metadata - self._attribute_name = number_metadata.attribute_name + self._attribute_name = entity_metadata.attribute_name - if number_metadata.min is not None: - self._attr_native_min_value = number_metadata.min - if number_metadata.max is not None: - self._attr_native_max_value = number_metadata.max - if number_metadata.step is not None: - self._attr_native_step = number_metadata.step - if number_metadata.unit is not None: - self._attr_native_unit_of_measurement = number_metadata.unit - if number_metadata.multiplier is not None: - self._attr_multiplier = number_metadata.multiplier + if entity_metadata.min is not None: + self._attr_native_min_value = entity_metadata.min + if entity_metadata.max is not None: + self._attr_native_max_value = entity_metadata.max + if entity_metadata.step is not None: + self._attr_native_step = entity_metadata.step + if entity_metadata.multiplier is not None: + self._attr_multiplier = entity_metadata.multiplier + if entity_metadata.device_class is not None: + self._attr_device_class = validate_device_class( + NumberDeviceClass, + entity_metadata.device_class, + Platform.NUMBER.value, + _LOGGER, + ) + if entity_metadata.device_class is None and entity_metadata.unit is not None: + self._attr_native_unit_of_measurement = validate_unit( + entity_metadata.unit + ).value @property def native_value(self) -> float: @@ -596,7 +605,6 @@ class TimerDurationMinutes(ZHANumberConfigurationEntity): _unique_id_suffix = "timer_duration" _attr_entity_category = EntityCategory.CONFIG - _attr_icon: str = ICONS[14] _attr_native_min_value: float = 0x00 _attr_native_max_value: float = 0x257 _attr_native_unit_of_measurement: str | None = UNITS[72] @@ -611,7 +619,6 @@ class FilterLifeTime(ZHANumberConfigurationEntity): _unique_id_suffix = "filter_life_time" _attr_entity_category = EntityCategory.CONFIG - _attr_icon: str = ICONS[14] _attr_native_min_value: float = 0x00 _attr_native_max_value: float = 0xFFFFFFFF _attr_native_unit_of_measurement: str | None = UNITS[72] @@ -642,7 +649,6 @@ class InovelliRemoteDimmingUpSpeed(ZHANumberConfigurationEntity): _unique_id_suffix = "dimming_speed_up_remote" _attr_entity_category = EntityCategory.CONFIG - _attr_icon: str = ICONS[3] _attr_native_min_value: float = 0 _attr_native_max_value: float = 126 _attribute_name = "dimming_speed_up_remote" @@ -656,7 +662,6 @@ class InovelliButtonDelay(ZHANumberConfigurationEntity): _unique_id_suffix = "button_delay" _attr_entity_category = EntityCategory.CONFIG - _attr_icon: str = ICONS[3] _attr_native_min_value: float = 0 _attr_native_max_value: float = 9 _attribute_name = "button_delay" @@ -670,7 +675,6 @@ class InovelliLocalDimmingUpSpeed(ZHANumberConfigurationEntity): _unique_id_suffix = "dimming_speed_up_local" _attr_entity_category = EntityCategory.CONFIG - _attr_icon: str = ICONS[3] _attr_native_min_value: float = 0 _attr_native_max_value: float = 127 _attribute_name = "dimming_speed_up_local" @@ -684,7 +688,6 @@ class InovelliLocalRampRateOffToOn(ZHANumberConfigurationEntity): _unique_id_suffix = "ramp_rate_off_to_on_local" _attr_entity_category = EntityCategory.CONFIG - _attr_icon: str = ICONS[3] _attr_native_min_value: float = 0 _attr_native_max_value: float = 127 _attribute_name = "ramp_rate_off_to_on_local" @@ -698,7 +701,6 @@ class InovelliRemoteDimmingSpeedOffToOn(ZHANumberConfigurationEntity): _unique_id_suffix = "ramp_rate_off_to_on_remote" _attr_entity_category = EntityCategory.CONFIG - _attr_icon: str = ICONS[3] _attr_native_min_value: float = 0 _attr_native_max_value: float = 127 _attribute_name = "ramp_rate_off_to_on_remote" @@ -712,7 +714,6 @@ class InovelliRemoteDimmingDownSpeed(ZHANumberConfigurationEntity): _unique_id_suffix = "dimming_speed_down_remote" _attr_entity_category = EntityCategory.CONFIG - _attr_icon: str = ICONS[3] _attr_native_min_value: float = 0 _attr_native_max_value: float = 127 _attribute_name = "dimming_speed_down_remote" @@ -726,7 +727,6 @@ class InovelliLocalDimmingDownSpeed(ZHANumberConfigurationEntity): _unique_id_suffix = "dimming_speed_down_local" _attr_entity_category = EntityCategory.CONFIG - _attr_icon: str = ICONS[3] _attr_native_min_value: float = 0 _attr_native_max_value: float = 127 _attribute_name = "dimming_speed_down_local" @@ -740,7 +740,6 @@ class InovelliLocalRampRateOnToOff(ZHANumberConfigurationEntity): _unique_id_suffix = "ramp_rate_on_to_off_local" _attr_entity_category = EntityCategory.CONFIG - _attr_icon: str = ICONS[3] _attr_native_min_value: float = 0 _attr_native_max_value: float = 127 _attribute_name = "ramp_rate_on_to_off_local" @@ -754,7 +753,6 @@ class InovelliRemoteDimmingSpeedOnToOff(ZHANumberConfigurationEntity): _unique_id_suffix = "ramp_rate_on_to_off_remote" _attr_entity_category = EntityCategory.CONFIG - _attr_icon: str = ICONS[3] _attr_native_min_value: float = 0 _attr_native_max_value: float = 127 _attribute_name = "ramp_rate_on_to_off_remote" @@ -768,7 +766,6 @@ class InovelliMinimumLoadDimmingLevel(ZHANumberConfigurationEntity): _unique_id_suffix = "minimum_level" _attr_entity_category = EntityCategory.CONFIG - _attr_icon: str = ICONS[16] _attr_native_min_value: float = 1 _attr_native_max_value: float = 254 _attribute_name = "minimum_level" @@ -782,7 +779,6 @@ class InovelliMaximumLoadDimmingLevel(ZHANumberConfigurationEntity): _unique_id_suffix = "maximum_level" _attr_entity_category = EntityCategory.CONFIG - _attr_icon: str = ICONS[16] _attr_native_min_value: float = 2 _attr_native_max_value: float = 255 _attribute_name = "maximum_level" @@ -796,7 +792,6 @@ class InovelliAutoShutoffTimer(ZHANumberConfigurationEntity): _unique_id_suffix = "auto_off_timer" _attr_entity_category = EntityCategory.CONFIG - _attr_icon: str = ICONS[14] _attr_native_min_value: float = 0 _attr_native_max_value: float = 32767 _attribute_name = "auto_off_timer" @@ -812,7 +807,6 @@ class InovelliQuickStartTime(ZHANumberConfigurationEntity): _unique_id_suffix = "quick_start_time" _attr_entity_category = EntityCategory.CONFIG - _attr_icon: str = ICONS[3] _attr_native_min_value: float = 0 _attr_native_max_value: float = 10 _attribute_name = "quick_start_time" @@ -826,7 +820,6 @@ class InovelliLoadLevelIndicatorTimeout(ZHANumberConfigurationEntity): _unique_id_suffix = "load_level_indicator_timeout" _attr_entity_category = EntityCategory.CONFIG - _attr_icon: str = ICONS[14] _attr_native_min_value: float = 0 _attr_native_max_value: float = 11 _attribute_name = "load_level_indicator_timeout" @@ -840,7 +833,6 @@ class InovelliDefaultAllLEDOnColor(ZHANumberConfigurationEntity): _unique_id_suffix = "led_color_when_on" _attr_entity_category = EntityCategory.CONFIG - _attr_icon: str = ICONS[15] _attr_native_min_value: float = 0 _attr_native_max_value: float = 255 _attribute_name = "led_color_when_on" @@ -854,7 +846,6 @@ class InovelliDefaultAllLEDOffColor(ZHANumberConfigurationEntity): _unique_id_suffix = "led_color_when_off" _attr_entity_category = EntityCategory.CONFIG - _attr_icon: str = ICONS[15] _attr_native_min_value: float = 0 _attr_native_max_value: float = 255 _attribute_name = "led_color_when_off" @@ -868,7 +859,6 @@ class InovelliDefaultAllLEDOnIntensity(ZHANumberConfigurationEntity): _unique_id_suffix = "led_intensity_when_on" _attr_entity_category = EntityCategory.CONFIG - _attr_icon: str = ICONS[16] _attr_native_min_value: float = 0 _attr_native_max_value: float = 100 _attribute_name = "led_intensity_when_on" @@ -882,7 +872,6 @@ class InovelliDefaultAllLEDOffIntensity(ZHANumberConfigurationEntity): _unique_id_suffix = "led_intensity_when_off" _attr_entity_category = EntityCategory.CONFIG - _attr_icon: str = ICONS[16] _attr_native_min_value: float = 0 _attr_native_max_value: float = 100 _attribute_name = "led_intensity_when_off" @@ -896,7 +885,6 @@ class InovelliDoubleTapUpLevel(ZHANumberConfigurationEntity): _unique_id_suffix = "double_tap_up_level" _attr_entity_category = EntityCategory.CONFIG - _attr_icon: str = ICONS[16] _attr_native_min_value: float = 2 _attr_native_max_value: float = 254 _attribute_name = "double_tap_up_level" @@ -910,7 +898,6 @@ class InovelliDoubleTapDownLevel(ZHANumberConfigurationEntity): _unique_id_suffix = "double_tap_down_level" _attr_entity_category = EntityCategory.CONFIG - _attr_icon: str = ICONS[16] _attr_native_min_value: float = 0 _attr_native_max_value: float = 254 _attribute_name = "double_tap_down_level" @@ -932,7 +919,6 @@ class AqaraPetFeederServingSize(ZHANumberConfigurationEntity): _attr_translation_key: str = "serving_size" _attr_mode: NumberMode = NumberMode.BOX - _attr_icon: str = "mdi:counter" @CONFIG_DIAGNOSTIC_MATCH( @@ -951,7 +937,6 @@ class AqaraPetFeederPortionWeight(ZHANumberConfigurationEntity): _attr_mode: NumberMode = NumberMode.BOX _attr_native_unit_of_measurement: str = UnitOfMass.GRAMS - _attr_icon: str = "mdi:weight-gram" @CONFIG_DIAGNOSTIC_MATCH( @@ -971,7 +956,6 @@ class AqaraThermostatAwayTemp(ZHANumberConfigurationEntity): _attr_mode: NumberMode = NumberMode.SLIDER _attr_native_unit_of_measurement: str = UnitOfTemperature.CELSIUS - _attr_icon: str = ICONS[0] @CONFIG_DIAGNOSTIC_MATCH( @@ -992,7 +976,6 @@ class ThermostatLocalTempCalibration(ZHANumberConfigurationEntity): _attr_mode: NumberMode = NumberMode.SLIDER _attr_native_unit_of_measurement: str = UnitOfTemperature.CELSIUS - _attr_icon: str = ICONS[0] @CONFIG_DIAGNOSTIC_MATCH( @@ -1024,7 +1007,6 @@ class SonoffPresenceSenorTimeout(ZHANumberConfigurationEntity): _attr_translation_key: str = "presence_detection_timeout" _attr_mode: NumberMode = NumberMode.BOX - _attr_icon: str = "mdi:timer-edit" # pylint: disable-next=hass-invalid-inheritance # needs fixing diff --git a/homeassistant/components/zha/radio_manager.py b/homeassistant/components/zha/radio_manager.py index d3ca03de8d8..44b7304c58e 100644 --- a/homeassistant/components/zha/radio_manager.py +++ b/homeassistant/components/zha/radio_manager.py @@ -1,4 +1,5 @@ """Config flow for ZHA.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/zha/repairs/__init__.py b/homeassistant/components/zha/repairs/__init__.py index a3c2ea6f292..3d8f2553baa 100644 --- a/homeassistant/components/zha/repairs/__init__.py +++ b/homeassistant/components/zha/repairs/__init__.py @@ -1,4 +1,5 @@ """ZHA repairs for common environmental and device problems.""" + from __future__ import annotations from typing import Any, cast diff --git a/homeassistant/components/zha/repairs/network_settings_inconsistent.py b/homeassistant/components/zha/repairs/network_settings_inconsistent.py index 0a478f4b36a..2598ff8f98a 100644 --- a/homeassistant/components/zha/repairs/network_settings_inconsistent.py +++ b/homeassistant/components/zha/repairs/network_settings_inconsistent.py @@ -1,4 +1,5 @@ """ZHA repair for inconsistent network settings.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/zha/repairs/wrong_silabs_firmware.py b/homeassistant/components/zha/repairs/wrong_silabs_firmware.py index 93c5489eda7..5b1f85e1a29 100644 --- a/homeassistant/components/zha/repairs/wrong_silabs_firmware.py +++ b/homeassistant/components/zha/repairs/wrong_silabs_firmware.py @@ -1,4 +1,5 @@ """ZHA repairs for common environmental and device problems.""" + from __future__ import annotations import enum @@ -100,7 +101,7 @@ async def warn_on_wrong_silabs_firmware(hass: HomeAssistant, device: str) -> boo if app_type == ApplicationType.EZSP: # If connecting fails but we somehow probe EZSP (e.g. stuck in bootloader), # reconnect, it should work - raise AlreadyRunningEZSP() + raise AlreadyRunningEZSP hardware_type = _detect_radio_hardware(hass, device) ir.async_create_issue( diff --git a/homeassistant/components/zha/select.py b/homeassistant/components/zha/select.py index 53acc5cdd02..98d5debd999 100644 --- a/homeassistant/components/zha/select.py +++ b/homeassistant/components/zha/select.py @@ -1,4 +1,5 @@ """Support for ZHA controls using the select platform.""" + from __future__ import annotations from enum import Enum @@ -10,7 +11,7 @@ from zhaquirks.quirk_ids import TUYA_PLUG_MANUFACTURER, TUYA_PLUG_ONOFF from zhaquirks.xiaomi.aqara.magnet_ac01 import OppleCluster as MagnetAC01OppleCluster from zhaquirks.xiaomi.aqara.switch_acn047 import OppleCluster as T2RelayOppleCluster from zigpy import types -from zigpy.quirks.v2 import EntityMetadata, ZCLEnumMetadata +from zigpy.quirks.v2 import ZCLEnumMetadata from zigpy.zcl.clusters.general import OnOff from zigpy.zcl.clusters.security import IasWd @@ -28,7 +29,7 @@ from .core.const import ( CLUSTER_HANDLER_INOVELLI, CLUSTER_HANDLER_OCCUPANCY, CLUSTER_HANDLER_ON_OFF, - QUIRK_METADATA, + ENTITY_METADATA, SIGNAL_ADD_ENTITIES, SIGNAL_ATTR_UPDATED, Strobe, @@ -178,7 +179,7 @@ class ZCLEnumSelectEntity(ZhaEntity, SelectEntity): Return entity if it is a supported configuration, otherwise return None """ cluster_handler = cluster_handlers[0] - if QUIRK_METADATA not in kwargs and ( + if ENTITY_METADATA not in kwargs and ( cls._attribute_name in cluster_handler.cluster.unsupported_attributes or cls._attribute_name not in cluster_handler.cluster.attributes_by_name or cluster_handler.cluster.get(cls._attribute_name) is None @@ -201,17 +202,16 @@ class ZCLEnumSelectEntity(ZhaEntity, SelectEntity): ) -> None: """Init this select entity.""" self._cluster_handler: ClusterHandler = cluster_handlers[0] - if QUIRK_METADATA in kwargs: - self._init_from_quirks_metadata(kwargs[QUIRK_METADATA]) + if ENTITY_METADATA in kwargs: + self._init_from_quirks_metadata(kwargs[ENTITY_METADATA]) self._attr_options = [entry.name.replace("_", " ") for entry in self._enum] super().__init__(unique_id, zha_device, cluster_handlers, **kwargs) - def _init_from_quirks_metadata(self, entity_metadata: EntityMetadata) -> None: + def _init_from_quirks_metadata(self, entity_metadata: ZCLEnumMetadata) -> None: """Init this entity from the quirks metadata.""" super()._init_from_quirks_metadata(entity_metadata) - zcl_enum_metadata: ZCLEnumMetadata = entity_metadata.entity_metadata - self._attribute_name = zcl_enum_metadata.attribute_name - self._enum = zcl_enum_metadata.enum + self._attribute_name = entity_metadata.attribute_name + self._enum = entity_metadata.enum @property def current_option(self) -> str | None: @@ -624,7 +624,6 @@ class AqaraPetFeederMode(ZCLEnumSelectEntity): _attribute_name = "feeding_mode" _enum = AqaraFeedingMode _attr_translation_key: str = "feeding_mode" - _attr_icon: str = "mdi:wrench-clock" class AqaraThermostatPresetMode(types.enum8): @@ -689,4 +688,3 @@ class KeypadLockout(ZCLEnumSelectEntity): _attribute_name: str = "keypad_lockout" _enum = KeypadLockoutEnum _attr_translation_key: str = "keypad_lockout" - _attr_icon: str = "mdi:lock" diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index 6a68b55a8be..91fe302291a 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -1,4 +1,5 @@ """Sensors on Zigbee Home Automation networks.""" + from __future__ import annotations import asyncio @@ -12,7 +13,7 @@ import random from typing import TYPE_CHECKING, Any, Self from zigpy import types -from zigpy.quirks.v2 import EntityMetadata, ZCLEnumMetadata, ZCLSensorMetadata +from zigpy.quirks.v2 import ZCLEnumMetadata, ZCLSensorMetadata from zigpy.state import Counter, State from zigpy.zcl.clusters.closures import WindowCovering from zigpy.zcl.clusters.general import Basic @@ -70,11 +71,11 @@ from .core.const import ( CLUSTER_HANDLER_TEMPERATURE, CLUSTER_HANDLER_THERMOSTAT, DATA_ZHA, - QUIRK_METADATA, + ENTITY_METADATA, SIGNAL_ADD_ENTITIES, SIGNAL_ATTR_UPDATED, ) -from .core.helpers import get_zha_data +from .core.helpers import get_zha_data, validate_device_class, validate_unit from .core.registries import SMARTTHINGS_HUMIDITY_CLUSTER, ZHA_ENTITIES from .entity import BaseZhaEntity, ZhaEntity @@ -153,7 +154,7 @@ class Sensor(ZhaEntity, SensorEntity): Return entity if it is a supported configuration, otherwise return None """ cluster_handler = cluster_handlers[0] - if QUIRK_METADATA not in kwargs and ( + if ENTITY_METADATA not in kwargs and ( cls._attribute_name in cluster_handler.cluster.unsupported_attributes or cls._attribute_name not in cluster_handler.cluster.attributes_by_name ): @@ -175,21 +176,29 @@ class Sensor(ZhaEntity, SensorEntity): ) -> None: """Init this sensor.""" self._cluster_handler: ClusterHandler = cluster_handlers[0] - if QUIRK_METADATA in kwargs: - self._init_from_quirks_metadata(kwargs[QUIRK_METADATA]) + if ENTITY_METADATA in kwargs: + self._init_from_quirks_metadata(kwargs[ENTITY_METADATA]) super().__init__(unique_id, zha_device, cluster_handlers, **kwargs) - def _init_from_quirks_metadata(self, entity_metadata: EntityMetadata) -> None: + def _init_from_quirks_metadata(self, entity_metadata: ZCLSensorMetadata) -> None: """Init this entity from the quirks metadata.""" super()._init_from_quirks_metadata(entity_metadata) - sensor_metadata: ZCLSensorMetadata = entity_metadata.entity_metadata - self._attribute_name = sensor_metadata.attribute_name - if sensor_metadata.divisor is not None: - self._divisor = sensor_metadata.divisor - if sensor_metadata.multiplier is not None: - self._multiplier = sensor_metadata.multiplier - if sensor_metadata.unit is not None: - self._attr_native_unit_of_measurement = sensor_metadata.unit + self._attribute_name = entity_metadata.attribute_name + if entity_metadata.divisor is not None: + self._divisor = entity_metadata.divisor + if entity_metadata.multiplier is not None: + self._multiplier = entity_metadata.multiplier + if entity_metadata.device_class is not None: + self._attr_device_class = validate_device_class( + SensorDeviceClass, + entity_metadata.device_class, + Platform.SENSOR.value, + _LOGGER, + ) + if entity_metadata.device_class is None and entity_metadata.unit is not None: + self._attr_native_unit_of_measurement = validate_unit( + entity_metadata.unit + ).value async def async_added_to_hass(self) -> None: """Run when about to be added to hass.""" @@ -354,12 +363,22 @@ class EnumSensor(Sensor): _attr_device_class: SensorDeviceClass = SensorDeviceClass.ENUM _enum: type[enum.Enum] - def _init_from_quirks_metadata(self, entity_metadata: EntityMetadata) -> None: + def __init__( + self, + unique_id: str, + zha_device: ZHADevice, + cluster_handlers: list[ClusterHandler], + **kwargs: Any, + ) -> None: + """Init this sensor.""" + super().__init__(unique_id, zha_device, cluster_handlers, **kwargs) + self._attr_options = [e.name for e in self._enum] + + def _init_from_quirks_metadata(self, entity_metadata: ZCLEnumMetadata) -> None: """Init this entity from the quirks metadata.""" ZhaEntity._init_from_quirks_metadata(self, entity_metadata) # pylint: disable=protected-access - sensor_metadata: ZCLEnumMetadata = entity_metadata.entity_metadata - self._attribute_name = sensor_metadata.attribute_name - self._enum = sensor_metadata.enum + self._attribute_name = entity_metadata.attribute_name + self._enum = entity_metadata.enum def formatter(self, value: int) -> str | None: """Use name of enum.""" @@ -1279,7 +1298,6 @@ class TimeLeft(Sensor): _attribute_name = "timer_time_left" _unique_id_suffix = "time_left" _attr_device_class: SensorDeviceClass = SensorDeviceClass.DURATION - _attr_icon = "mdi:timer" _attr_translation_key: str = "timer_time_left" _attr_native_unit_of_measurement = UnitOfTime.MINUTES @@ -1292,7 +1310,6 @@ class IkeaDeviceRunTime(Sensor): _attribute_name = "device_run_time" _unique_id_suffix = "device_run_time" _attr_device_class: SensorDeviceClass = SensorDeviceClass.DURATION - _attr_icon = "mdi:timer" _attr_translation_key: str = "device_run_time" _attr_native_unit_of_measurement = UnitOfTime.MINUTES _attr_entity_category: EntityCategory = EntityCategory.DIAGNOSTIC @@ -1306,7 +1323,6 @@ class IkeaFilterRunTime(Sensor): _attribute_name = "filter_run_time" _unique_id_suffix = "filter_run_time" _attr_device_class: SensorDeviceClass = SensorDeviceClass.DURATION - _attr_icon = "mdi:timer" _attr_translation_key: str = "filter_run_time" _attr_native_unit_of_measurement = UnitOfTime.MINUTES _attr_entity_category: EntityCategory = EntityCategory.DIAGNOSTIC @@ -1327,7 +1343,6 @@ class AqaraPetFeederLastFeedingSource(EnumSensor): _attribute_name = "last_feeding_source" _unique_id_suffix = "last_feeding_source" _attr_translation_key: str = "last_feeding_source" - _attr_icon = "mdi:devices" _enum = AqaraFeedingSource @@ -1339,7 +1354,6 @@ class AqaraPetFeederLastFeedingSize(Sensor): _attribute_name = "last_feeding_size" _unique_id_suffix = "last_feeding_size" _attr_translation_key: str = "last_feeding_size" - _attr_icon: str = "mdi:counter" @MULTI_MATCH(cluster_handler_names="opple_cluster", models={"aqara.feeder.acn001"}) @@ -1351,7 +1365,6 @@ class AqaraPetFeederPortionsDispensed(Sensor): _unique_id_suffix = "portions_dispensed" _attr_translation_key: str = "portions_dispensed_today" _attr_state_class: SensorStateClass = SensorStateClass.TOTAL_INCREASING - _attr_icon: str = "mdi:counter" @MULTI_MATCH(cluster_handler_names="opple_cluster", models={"aqara.feeder.acn001"}) @@ -1364,7 +1377,6 @@ class AqaraPetFeederWeightDispensed(Sensor): _attr_translation_key: str = "weight_dispensed_today" _attr_native_unit_of_measurement = UnitOfMass.GRAMS _attr_state_class: SensorStateClass = SensorStateClass.TOTAL_INCREASING - _attr_icon: str = "mdi:weight-gram" @MULTI_MATCH(cluster_handler_names="opple_cluster", models={"lumi.sensor_smoke.acn03"}) @@ -1377,7 +1389,6 @@ class AqaraSmokeDensityDbm(Sensor): _attr_translation_key: str = "smoke_density" _attr_native_unit_of_measurement = "dB/m" _attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT - _attr_icon: str = "mdi:google-circles-communities" _attr_suggested_display_precision: int = 3 @@ -1396,7 +1407,6 @@ class SonoffPresenceSenorIlluminationStatus(EnumSensor): _attribute_name = "last_illumination_state" _unique_id_suffix = "last_illumination" _attr_translation_key: str = "last_illumination_state" - _attr_icon: str = "mdi:theme-light-dark" _enum = SonoffIlluminationStates @@ -1411,7 +1421,6 @@ class PiHeatingDemand(Sensor): _unique_id_suffix = "pi_heating_demand" _attribute_name = "pi_heating_demand" _attr_translation_key: str = "pi_heating_demand" - _attr_icon: str = "mdi:radiator" _attr_native_unit_of_measurement = PERCENTAGE _decimals = 0 _attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT @@ -1437,7 +1446,6 @@ class SetpointChangeSource(EnumSensor): _unique_id_suffix = "setpoint_change_source" _attribute_name = "setpoint_change_source" _attr_translation_key: str = "setpoint_change_source" - _attr_icon: str = "mdi:thermostat" _attr_entity_category = EntityCategory.DIAGNOSTIC _enum = SetpointChangeSourceEnum diff --git a/homeassistant/components/zha/siren.py b/homeassistant/components/zha/siren.py index 717eb2df033..3aab332f746 100644 --- a/homeassistant/components/zha/siren.py +++ b/homeassistant/components/zha/siren.py @@ -1,4 +1,5 @@ """Support for ZHA sirens.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/zha/switch.py b/homeassistant/components/zha/switch.py index 960124c4a8a..14da2344cd4 100644 --- a/homeassistant/components/zha/switch.py +++ b/homeassistant/components/zha/switch.py @@ -1,4 +1,5 @@ """Switches on Zigbee Home Automation networks.""" + from __future__ import annotations import functools @@ -6,7 +7,7 @@ import logging from typing import TYPE_CHECKING, Any, Self from zhaquirks.quirk_ids import TUYA_PLUG_ONOFF -from zigpy.quirks.v2 import EntityMetadata, SwitchMetadata +from zigpy.quirks.v2 import SwitchMetadata from zigpy.zcl.clusters.closures import ConfigStatus, WindowCovering, WindowCoveringMode from zigpy.zcl.clusters.general import OnOff from zigpy.zcl.foundation import Status @@ -24,7 +25,7 @@ from .core.const import ( CLUSTER_HANDLER_COVER, CLUSTER_HANDLER_INOVELLI, CLUSTER_HANDLER_ON_OFF, - QUIRK_METADATA, + ENTITY_METADATA, SIGNAL_ADD_ENTITIES, SIGNAL_ATTR_UPDATED, ) @@ -191,7 +192,7 @@ class ZHASwitchConfigurationEntity(ZhaEntity, SwitchEntity): Return entity if it is a supported configuration, otherwise return None """ cluster_handler = cluster_handlers[0] - if QUIRK_METADATA not in kwargs and ( + if ENTITY_METADATA not in kwargs and ( cls._attribute_name in cluster_handler.cluster.unsupported_attributes or cls._attribute_name not in cluster_handler.cluster.attributes_by_name or cluster_handler.cluster.get(cls._attribute_name) is None @@ -214,21 +215,20 @@ class ZHASwitchConfigurationEntity(ZhaEntity, SwitchEntity): ) -> None: """Init this number configuration entity.""" self._cluster_handler: ClusterHandler = cluster_handlers[0] - if QUIRK_METADATA in kwargs: - self._init_from_quirks_metadata(kwargs[QUIRK_METADATA]) + if ENTITY_METADATA in kwargs: + self._init_from_quirks_metadata(kwargs[ENTITY_METADATA]) super().__init__(unique_id, zha_device, cluster_handlers, **kwargs) - def _init_from_quirks_metadata(self, entity_metadata: EntityMetadata) -> None: + def _init_from_quirks_metadata(self, entity_metadata: SwitchMetadata) -> None: """Init this entity from the quirks metadata.""" super()._init_from_quirks_metadata(entity_metadata) - switch_metadata: SwitchMetadata = entity_metadata.entity_metadata - self._attribute_name = switch_metadata.attribute_name - if switch_metadata.invert_attribute_name: - self._inverter_attribute_name = switch_metadata.invert_attribute_name - if switch_metadata.force_inverted: - self._force_inverted = switch_metadata.force_inverted - self._off_value = switch_metadata.off_value - self._on_value = switch_metadata.on_value + self._attribute_name = entity_metadata.attribute_name + if entity_metadata.invert_attribute_name: + self._inverter_attribute_name = entity_metadata.invert_attribute_name + if entity_metadata.force_inverted: + self._force_inverted = entity_metadata.force_inverted + self._off_value = entity_metadata.off_value + self._on_value = entity_metadata.on_value async def async_added_to_hass(self) -> None: """Run when about to be added to hass.""" @@ -512,7 +512,6 @@ class AqaraPetFeederLEDIndicator(ZHASwitchConfigurationEntity): _attribute_name = "disable_led_indicator" _attr_translation_key = "led_indicator" _force_inverted = True - _attr_icon: str = "mdi:led-on" @CONFIG_DIAGNOSTIC_MATCH( @@ -524,7 +523,6 @@ class AqaraPetFeederChildLock(ZHASwitchConfigurationEntity): _unique_id_suffix = "child_lock" _attribute_name = "child_lock" _attr_translation_key = "child_lock" - _attr_icon: str = "mdi:account-lock" @CONFIG_DIAGNOSTIC_MATCH( @@ -536,7 +534,6 @@ class TuyaChildLockSwitch(ZHASwitchConfigurationEntity): _unique_id_suffix = "child_lock" _attribute_name = "child_lock" _attr_translation_key = "child_lock" - _attr_icon: str = "mdi:account-lock" @CONFIG_DIAGNOSTIC_MATCH( @@ -570,7 +567,6 @@ class AqaraThermostatChildLock(ZHASwitchConfigurationEntity): _unique_id_suffix = "child_lock" _attribute_name = "child_lock" _attr_translation_key = "child_lock" - _attr_icon: str = "mdi:account-lock" @CONFIG_DIAGNOSTIC_MATCH( @@ -582,7 +578,6 @@ class AqaraHeartbeatIndicator(ZHASwitchConfigurationEntity): _unique_id_suffix = "heartbeat_indicator" _attribute_name = "heartbeat_indicator" _attr_translation_key = "heartbeat_indicator" - _attr_icon: str = "mdi:heart-flash" @CONFIG_DIAGNOSTIC_MATCH( @@ -594,7 +589,6 @@ class AqaraLinkageAlarm(ZHASwitchConfigurationEntity): _unique_id_suffix = "linkage_alarm" _attribute_name = "linkage_alarm" _attr_translation_key = "linkage_alarm" - _attr_icon: str = "mdi:shield-link-variant" @CONFIG_DIAGNOSTIC_MATCH( @@ -606,7 +600,6 @@ class AqaraBuzzerManualMute(ZHASwitchConfigurationEntity): _unique_id_suffix = "buzzer_manual_mute" _attribute_name = "buzzer_manual_mute" _attr_translation_key = "buzzer_manual_mute" - _attr_icon: str = "mdi:volume-off" @CONFIG_DIAGNOSTIC_MATCH( @@ -618,7 +611,6 @@ class AqaraBuzzerManualAlarm(ZHASwitchConfigurationEntity): _unique_id_suffix = "buzzer_manual_alarm" _attribute_name = "buzzer_manual_alarm" _attr_translation_key = "buzzer_manual_alarm" - _attr_icon: str = "mdi:bullhorn" @CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names=CLUSTER_HANDLER_COVER) @@ -631,7 +623,6 @@ class WindowCoveringInversionSwitch(ZHASwitchConfigurationEntity): _unique_id_suffix = "inverted" _attribute_name = WindowCovering.AttributeDefs.config_status.name _attr_translation_key = "inverted" - _attr_icon: str = "mdi:arrow-up-down" @classmethod def create_entity( @@ -725,4 +716,3 @@ class AqaraE1CurtainMotorHooksLockedSwitch(ZHASwitchConfigurationEntity): _unique_id_suffix = "hooks_lock" _attribute_name = "hooks_lock" _attr_translation_key = "hooks_locked" - _attr_icon: str = "mdi:lock" diff --git a/homeassistant/components/zha/update.py b/homeassistant/components/zha/update.py index d45c24253be..0cb80d13119 100644 --- a/homeassistant/components/zha/update.py +++ b/homeassistant/components/zha/update.py @@ -1,4 +1,5 @@ """Representation of ZHA updates.""" + from __future__ import annotations import functools @@ -71,7 +72,7 @@ async def async_setup_entry( config_entry.async_on_unload(unsub) -class ZHAFirmwareUpdateCoordinator(DataUpdateCoordinator): # pylint: disable=hass-enforce-coordinator-module +class ZHAFirmwareUpdateCoordinator(DataUpdateCoordinator[None]): # pylint: disable=hass-enforce-coordinator-module """Firmware update coordinator that broadcasts updates network-wide.""" def __init__( @@ -93,7 +94,9 @@ class ZHAFirmwareUpdateCoordinator(DataUpdateCoordinator): # pylint: disable=ha @CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names=CLUSTER_HANDLER_OTA) -class ZHAFirmwareUpdateEntity(ZhaEntity, CoordinatorEntity, UpdateEntity): +class ZHAFirmwareUpdateEntity( + ZhaEntity, CoordinatorEntity[ZHAFirmwareUpdateCoordinator], UpdateEntity +): """Representation of a ZHA firmware update entity.""" _unique_id_suffix = "firmware_update" @@ -127,14 +130,9 @@ class ZHAFirmwareUpdateEntity(ZhaEntity, CoordinatorEntity, UpdateEntity): def _get_cluster_version(self) -> str | None: """Synchronize current file version with the cluster.""" - device = self._ota_cluster_handler._endpoint.device # pylint: disable=protected-access - if self._ota_cluster_handler.current_file_version is not None: return f"0x{self._ota_cluster_handler.current_file_version:08x}" - if device.sw_version is not None: - return device.sw_version - return None @callback diff --git a/homeassistant/components/zha/websocket_api.py b/homeassistant/components/zha/websocket_api.py index e3e67ea0e41..f9a92acdd4c 100644 --- a/homeassistant/components/zha/websocket_api.py +++ b/homeassistant/components/zha/websocket_api.py @@ -1,4 +1,5 @@ """Web socket API for Zigbee Home Automation devices.""" + from __future__ import annotations import asyncio @@ -387,30 +388,30 @@ async def websocket_get_groupable_devices( zha_gateway = get_zha_gateway(hass) devices = [device for device in zha_gateway.devices.values() if device.is_groupable] - groupable_devices = [] + groupable_devices: list[dict[str, Any]] = [] for device in devices: entity_refs = zha_gateway.device_registry[device.ieee] - for ep_id in device.async_get_groupable_endpoints(): - groupable_devices.append( - { - "endpoint_id": ep_id, - "entities": [ - { - "name": _get_entity_name(zha_gateway, entity_ref), - "original_name": _get_entity_original_name( - zha_gateway, entity_ref - ), - } - for entity_ref in entity_refs - if list(entity_ref.cluster_handlers.values())[ - 0 - ].cluster.endpoint.endpoint_id - == ep_id - ], - "device": device.zha_device_info, - } - ) + groupable_devices.extend( + { + "endpoint_id": ep_id, + "entities": [ + { + "name": _get_entity_name(zha_gateway, entity_ref), + "original_name": _get_entity_original_name( + zha_gateway, entity_ref + ), + } + for entity_ref in entity_refs + if list(entity_ref.cluster_handlers.values())[ + 0 + ].cluster.endpoint.endpoint_id + == ep_id + ], + "device": device.zha_device_info, + } + for ep_id in device.async_get_groupable_endpoints() + ) connection.send_result(msg[ID], groupable_devices) @@ -520,9 +521,9 @@ async def websocket_remove_groups( group_ids: list[int] = msg[GROUP_IDS] if len(group_ids) > 1: - tasks = [] - for group_id in group_ids: - tasks.append(zha_gateway.async_remove_zigpy_group(group_id)) + tasks = [ + zha_gateway.async_remove_zigpy_group(group_id) for group_id in group_ids + ] await asyncio.gather(*tasks) else: await zha_gateway.async_remove_zigpy_group(group_ids[0]) @@ -1097,7 +1098,7 @@ async def websocket_update_zha_configuration( """Update the ZHA configuration.""" zha_gateway = get_zha_gateway(hass) options = zha_gateway.config_entry.options - data_to_save = {**options, **{CUSTOM_CONFIGURATION: msg["data"]}} + data_to_save = {**options, CUSTOM_CONFIGURATION: msg["data"]} for section, schema in ZHA_CONFIG_SCHEMAS.items(): for entry in schema.schema: diff --git a/homeassistant/components/zhong_hong/climate.py b/homeassistant/components/zhong_hong/climate.py index fbada765cde..b0a8f02a2f3 100644 --- a/homeassistant/components/zhong_hong/climate.py +++ b/homeassistant/components/zhong_hong/climate.py @@ -1,4 +1,5 @@ """Support for ZhongHong HVAC Controller.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/ziggo_mediabox_xl/media_player.py b/homeassistant/components/ziggo_mediabox_xl/media_player.py index 20a78c80ac5..7c97d38cff3 100644 --- a/homeassistant/components/ziggo_mediabox_xl/media_player.py +++ b/homeassistant/components/ziggo_mediabox_xl/media_player.py @@ -1,4 +1,5 @@ """Support for interface with a Ziggo Mediabox XL.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/zodiac/config_flow.py b/homeassistant/components/zodiac/config_flow.py index 4acb3873031..a9ed49568ca 100644 --- a/homeassistant/components/zodiac/config_flow.py +++ b/homeassistant/components/zodiac/config_flow.py @@ -1,10 +1,10 @@ """Config flow to configure the Zodiac integration.""" + from __future__ import annotations from typing import Any -from homeassistant.config_entries import ConfigFlow -from homeassistant.data_entry_flow import FlowResult +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from .const import DEFAULT_NAME, DOMAIN @@ -16,7 +16,7 @@ class ZodiacConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") diff --git a/homeassistant/components/zodiac/const.py b/homeassistant/components/zodiac/const.py index f50e108c2aa..9b2600b0c96 100644 --- a/homeassistant/components/zodiac/const.py +++ b/homeassistant/components/zodiac/const.py @@ -1,4 +1,5 @@ """Constants for Zodiac.""" + DOMAIN = "zodiac" DEFAULT_NAME = "Zodiac" diff --git a/homeassistant/components/zodiac/icons.json b/homeassistant/components/zodiac/icons.json new file mode 100644 index 00000000000..2bb2f614f85 --- /dev/null +++ b/homeassistant/components/zodiac/icons.json @@ -0,0 +1,22 @@ +{ + "entity": { + "sensor": { + "sign": { + "default": "mdi:zodiac-aries", + "state": { + "taurus": "mdi:zodiac-taurus", + "gemini": "mdi:zodiac-gemini", + "cancer": "mdi:zodiac-cancer", + "leo": "mdi:zodiac-leo", + "virgo": "mdi:zodiac-virgo", + "libra": "mdi:zodiac-libra", + "scorpio": "mdi:zodiac-scorpio", + "sagittarius": "mdi:zodiac-sagittarius", + "capricorn": "mdi:zodiac-capricorn", + "aquarius": "mdi:zodiac-aquarius", + "pisces": "mdi:zodiac-pisces" + } + } + } + } +} diff --git a/homeassistant/components/zodiac/sensor.py b/homeassistant/components/zodiac/sensor.py index 2e79f3804ab..d7cac07a322 100644 --- a/homeassistant/components/zodiac/sensor.py +++ b/homeassistant/components/zodiac/sensor.py @@ -1,4 +1,5 @@ """Support for tracking the zodiac sign.""" + from __future__ import annotations from homeassistant.components.sensor import SensorDeviceClass, SensorEntity @@ -145,21 +146,6 @@ ZODIAC_BY_DATE = ( ), ) -ZODIAC_ICONS = { - SIGN_ARIES: "mdi:zodiac-aries", - SIGN_TAURUS: "mdi:zodiac-taurus", - SIGN_GEMINI: "mdi:zodiac-gemini", - SIGN_CANCER: "mdi:zodiac-cancer", - SIGN_LEO: "mdi:zodiac-leo", - SIGN_VIRGO: "mdi:zodiac-virgo", - SIGN_LIBRA: "mdi:zodiac-libra", - SIGN_SCORPIO: "mdi:zodiac-scorpio", - SIGN_SAGITTARIUS: "mdi:zodiac-sagittarius", - SIGN_CAPRICORN: "mdi:zodiac-capricorn", - SIGN_AQUARIUS: "mdi:zodiac-aquarius", - SIGN_PISCES: "mdi:zodiac-pisces", -} - async def async_setup_entry( hass: HomeAssistant, @@ -211,6 +197,5 @@ class ZodiacSensor(SensorEntity): today.month == sign[1][1] and today.day <= sign[1][0] ): self._attr_native_value = sign[2] - self._attr_icon = ZODIAC_ICONS.get(sign[2]) self._attr_extra_state_attributes = sign[3] break diff --git a/homeassistant/components/zone/__init__.py b/homeassistant/components/zone/__init__.py index 86593c36737..2fda501c447 100644 --- a/homeassistant/components/zone/__init__.py +++ b/homeassistant/components/zone/__init__.py @@ -1,4 +1,5 @@ """Support for the definition of zones.""" + from __future__ import annotations from collections.abc import Callable, Iterable @@ -37,7 +38,7 @@ from homeassistant.helpers import ( service, storage, ) -from homeassistant.helpers.typing import ConfigType, EventType +from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass from homeassistant.util.location import distance @@ -165,7 +166,7 @@ def async_setup_track_zone_entity_ids(hass: HomeAssistant) -> None: @callback def _async_add_zone_entity_id( - event_: EventType[event.EventStateChangedData], + event_: Event[event.EventStateChangedData], ) -> None: """Add zone entity ID.""" zone_entity_ids.append(event_.data["entity_id"]) @@ -173,7 +174,7 @@ def async_setup_track_zone_entity_ids(hass: HomeAssistant) -> None: @callback def _async_remove_zone_entity_id( - event_: EventType[event.EventStateChangedData], + event_: Event[event.EventStateChangedData], ) -> None: """Remove zone entity ID.""" zone_entity_ids.remove(event_.data["entity_id"]) @@ -280,7 +281,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Handle core config updated.""" await home_zone.async_update_config(_home_conf(hass)) - hass.bus.async_listen(EVENT_CORE_CONFIG_UPDATE, core_config_updated) + hass.bus.async_listen( + EVENT_CORE_CONFIG_UPDATE, core_config_updated, run_immediately=True + ) hass.data[DOMAIN] = storage_collection @@ -388,7 +391,7 @@ class Zone(collection.CollectionEntity): @callback def _person_state_change_listener( - self, evt: EventType[event.EventStateChangedData] + self, evt: Event[event.EventStateChangedData] ) -> None: person_entity_id = evt.data["entity_id"] cur_count = len(self._persons_in_zone) diff --git a/homeassistant/components/zone/config_flow.py b/homeassistant/components/zone/config_flow.py index de163146ab7..ed71a79470e 100644 --- a/homeassistant/components/zone/config_flow.py +++ b/homeassistant/components/zone/config_flow.py @@ -4,10 +4,11 @@ This is no longer in use. This file is around so that existing config entries will remain to be loaded and then automatically migrated to the storage collection. """ -from homeassistant import config_entries + +from homeassistant.config_entries import ConfigFlow from .const import DOMAIN -class ZoneConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class ZoneConfigFlow(ConfigFlow, domain=DOMAIN): """Stub zone config flow class.""" diff --git a/homeassistant/components/zone/icons.json b/homeassistant/components/zone/icons.json new file mode 100644 index 00000000000..a03163179cb --- /dev/null +++ b/homeassistant/components/zone/icons.json @@ -0,0 +1,5 @@ +{ + "services": { + "reload": "mdi:reload" + } +} diff --git a/homeassistant/components/zone/trigger.py b/homeassistant/components/zone/trigger.py index 09f93f9b786..8aea25f1f6c 100644 --- a/homeassistant/components/zone/trigger.py +++ b/homeassistant/components/zone/trigger.py @@ -1,4 +1,5 @@ """Offer zone automation rules.""" + from __future__ import annotations import logging @@ -12,7 +13,7 @@ from homeassistant.const import ( CONF_PLATFORM, CONF_ZONE, ) -from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback +from homeassistant.core import CALLBACK_TYPE, Event, HassJob, HomeAssistant, callback from homeassistant.helpers import ( condition, config_validation as cv, @@ -24,7 +25,7 @@ from homeassistant.helpers.event import ( async_track_state_change_event, ) from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo -from homeassistant.helpers.typing import ConfigType, EventType +from homeassistant.helpers.typing import ConfigType EVENT_ENTER = "enter" EVENT_LEAVE = "leave" @@ -74,7 +75,7 @@ async def async_attach_trigger( job = HassJob(action) @callback - def zone_automation_listener(zone_event: EventType[EventStateChangedData]) -> None: + def zone_automation_listener(zone_event: Event[EventStateChangedData]) -> None: """Listen for state changes and calls action.""" entity = zone_event.data["entity_id"] from_s = zone_event.data["old_state"] diff --git a/homeassistant/components/zoneminder/__init__.py b/homeassistant/components/zoneminder/__init__.py index 1ff73048440..0510ff58d35 100644 --- a/homeassistant/components/zoneminder/__init__.py +++ b/homeassistant/components/zoneminder/__init__.py @@ -1,4 +1,5 @@ """Support for ZoneMinder.""" + import logging from requests.exceptions import ConnectionError as RequestsConnectionError diff --git a/homeassistant/components/zoneminder/binary_sensor.py b/homeassistant/components/zoneminder/binary_sensor.py index 268823c9470..926780fc6da 100644 --- a/homeassistant/components/zoneminder/binary_sensor.py +++ b/homeassistant/components/zoneminder/binary_sensor.py @@ -1,4 +1,5 @@ """Support for ZoneMinder binary sensors.""" + from __future__ import annotations from zoneminder.zm import ZoneMinder diff --git a/homeassistant/components/zoneminder/camera.py b/homeassistant/components/zoneminder/camera.py index d8b2aa805e7..ab938472ed7 100644 --- a/homeassistant/components/zoneminder/camera.py +++ b/homeassistant/components/zoneminder/camera.py @@ -1,4 +1,5 @@ """Support for ZoneMinder camera streaming.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/zoneminder/icons.json b/homeassistant/components/zoneminder/icons.json new file mode 100644 index 00000000000..8ca180d7399 --- /dev/null +++ b/homeassistant/components/zoneminder/icons.json @@ -0,0 +1,5 @@ +{ + "services": { + "set_run_state": "mdi:cog" + } +} diff --git a/homeassistant/components/zoneminder/sensor.py b/homeassistant/components/zoneminder/sensor.py index 47863b5a5df..700344f44da 100644 --- a/homeassistant/components/zoneminder/sensor.py +++ b/homeassistant/components/zoneminder/sensor.py @@ -1,4 +1,5 @@ """Support for ZoneMinder sensors.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/zoneminder/switch.py b/homeassistant/components/zoneminder/switch.py index b722ef53a77..48cbe58a876 100644 --- a/homeassistant/components/zoneminder/switch.py +++ b/homeassistant/components/zoneminder/switch.py @@ -1,4 +1,5 @@ """Support for ZoneMinder switches.""" + from __future__ import annotations import logging @@ -39,16 +40,16 @@ def setup_platform( on_state = MonitorState(config.get(CONF_COMMAND_ON)) off_state = MonitorState(config.get(CONF_COMMAND_OFF)) - switches = [] + switches: list[ZMSwitchMonitors] = [] zm_client: ZoneMinder for zm_client in hass.data[ZONEMINDER_DOMAIN].values(): if not (monitors := zm_client.get_monitors()): raise PlatformNotReady( "Switch could not fetch any monitors from ZoneMinder" ) - - for monitor in monitors: - switches.append(ZMSwitchMonitors(monitor, on_state, off_state)) + switches.extend( + ZMSwitchMonitors(monitor, on_state, off_state) for monitor in monitors + ) add_entities(switches) diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index 1e2a17fdf63..52c7804bb8f 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -1,4 +1,5 @@ """The Z-Wave JS integration.""" + from __future__ import annotations import asyncio @@ -185,9 +186,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Create a task to allow the config entry to be unloaded before the driver is ready. # Unloading the config entry is needed if the client listen task errors. start_client_task = hass.async_create_task(start_client(hass, entry, client)) - hass.data[DOMAIN].setdefault(entry.entry_id, {})[ - DATA_START_CLIENT_TASK - ] = start_client_task + hass.data[DOMAIN].setdefault(entry.entry_id, {})[DATA_START_CLIENT_TASK] = ( + start_client_task + ) return True diff --git a/homeassistant/components/zwave_js/addon.py b/homeassistant/components/zwave_js/addon.py index f9adf9f19fb..12d81146c03 100644 --- a/homeassistant/components/zwave_js/addon.py +++ b/homeassistant/components/zwave_js/addon.py @@ -1,4 +1,5 @@ """Provide add-on management.""" + from __future__ import annotations from homeassistant.components.hassio import AddonManager diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index 5aa27ada977..dfb7442d678 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -1,4 +1,5 @@ """Websocket API for Z-Wave JS.""" + from __future__ import annotations from collections.abc import Callable, Coroutine @@ -55,8 +56,7 @@ from zwave_js_server.model.utils import ( from zwave_js_server.util.node import async_set_config_parameter from homeassistant.components import websocket_api -from homeassistant.components.http import require_admin -from homeassistant.components.http.view import HomeAssistantView +from homeassistant.components.http import KEY_HASS, HomeAssistantView, require_admin from homeassistant.components.websocket_api import ( ERR_INVALID_FORMAT, ERR_NOT_FOUND, @@ -2196,7 +2196,7 @@ class FirmwareUploadView(HomeAssistantView): @require_admin async def post(self, request: web.Request, device_id: str) -> web.Response: """Handle upload.""" - hass = request.app["hass"] + hass = request.app[KEY_HASS] try: node = async_get_node_from_device_id(hass, device_id, self._dev_reg) @@ -2339,7 +2339,7 @@ async def websocket_subscribe_controller_statistics( client: Client, driver: Driver, ) -> None: - """Subsribe to the statistics updates for a controller.""" + """Subscribe to the statistics updates for a controller.""" @callback def async_cleanup() -> None: @@ -2434,7 +2434,7 @@ async def websocket_subscribe_node_statistics( msg: dict[str, Any], node: Node, ) -> None: - """Subsribe to the statistics updates for a node.""" + """Subscribe to the statistics updates for a node.""" @callback def async_cleanup() -> None: diff --git a/homeassistant/components/zwave_js/binary_sensor.py b/homeassistant/components/zwave_js/binary_sensor.py index cb460f37000..79181e818a2 100644 --- a/homeassistant/components/zwave_js/binary_sensor.py +++ b/homeassistant/components/zwave_js/binary_sensor.py @@ -1,4 +1,5 @@ """Representation of Z-Wave binary sensors.""" + from __future__ import annotations from dataclasses import dataclass @@ -58,20 +59,13 @@ class NotificationZWaveJSEntityDescription(BinarySensorEntityDescription): states: tuple[str, ...] | None = None -@dataclass(frozen=True) -class PropertyZWaveJSMixin: - """Represent the mixin for property sensor descriptions.""" +@dataclass(frozen=True, kw_only=True) +class PropertyZWaveJSEntityDescription(BinarySensorEntityDescription): + """Represent the entity description for property name sensors.""" on_states: tuple[str, ...] -@dataclass(frozen=True) -class PropertyZWaveJSEntityDescription( - BinarySensorEntityDescription, PropertyZWaveJSMixin -): - """Represent the entity description for property name sensors.""" - - # Mappings for Notification sensors # https://github.com/zwave-js/node-zwave-js/blob/master/packages/config/config/notifications.json NOTIFICATION_SENSOR_MAPPINGS: tuple[NotificationZWaveJSEntityDescription, ...] = ( @@ -276,7 +270,9 @@ async def async_setup_entry( if state_key == "0": continue - notification_description: NotificationZWaveJSEntityDescription | None = None + notification_description: ( + NotificationZWaveJSEntityDescription | None + ) = None for description in NOTIFICATION_SENSOR_MAPPINGS: if ( diff --git a/homeassistant/components/zwave_js/button.py b/homeassistant/components/zwave_js/button.py index 876cf60b4cb..5526faf9c59 100644 --- a/homeassistant/components/zwave_js/button.py +++ b/homeassistant/components/zwave_js/button.py @@ -1,4 +1,5 @@ """Representation of Z-Wave buttons.""" + from __future__ import annotations from zwave_js_server.client import Client as ZwaveClient diff --git a/homeassistant/components/zwave_js/climate.py b/homeassistant/components/zwave_js/climate.py index 2f84b52b7da..04e3d8c3950 100644 --- a/homeassistant/components/zwave_js/climate.py +++ b/homeassistant/components/zwave_js/climate.py @@ -1,4 +1,5 @@ """Representation of Z-Wave thermostats.""" + from __future__ import annotations from typing import Any, cast diff --git a/homeassistant/components/zwave_js/config_flow.py b/homeassistant/components/zwave_js/config_flow.py index c3fd2836048..ca05dc2117b 100644 --- a/homeassistant/components/zwave_js/config_flow.py +++ b/homeassistant/components/zwave_js/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Z-Wave JS integration.""" + from __future__ import annotations from abc import ABC, abstractmethod @@ -11,7 +12,6 @@ from serial.tools import list_ports import voluptuous as vol from zwave_js_server.version import VersionInfo, get_server_version -from homeassistant import config_entries, exceptions from homeassistant.components import usb from homeassistant.components.hassio import ( AddonError, @@ -22,14 +22,21 @@ from homeassistant.components.hassio import ( is_hassio, ) from homeassistant.components.zeroconf import ZeroconfServiceInfo +from homeassistant.config_entries import ( + SOURCE_USB, + ConfigEntriesFlowManager, + ConfigEntry, + ConfigEntryBaseFlow, + ConfigEntryState, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, + OptionsFlowManager, +) from homeassistant.const import CONF_NAME, CONF_URL from homeassistant.core import HomeAssistant, callback -from homeassistant.data_entry_flow import ( - AbortFlow, - FlowHandler, - FlowManager, - FlowResult, -) +from homeassistant.data_entry_flow import AbortFlow, FlowManager +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession from . import disconnect_client @@ -156,7 +163,7 @@ async def async_get_usb_ports(hass: HomeAssistant) -> dict[str, str]: return await hass.async_add_executor_job(get_usb_ports) -class BaseZwaveJSFlow(FlowHandler, ABC): +class BaseZwaveJSFlow(ConfigEntryBaseFlow, ABC): """Represent the base config flow for Z-Wave JS.""" def __init__(self) -> None: @@ -176,12 +183,12 @@ class BaseZwaveJSFlow(FlowHandler, ABC): @property @abstractmethod - def flow_manager(self) -> FlowManager: + def flow_manager(self) -> FlowManager[ConfigFlowResult, str]: """Return the flow manager of the flow.""" async def async_step_install_addon( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Install Z-Wave JS add-on.""" if not self.install_task: self.install_task = self.hass.async_create_task(self._async_install_addon()) @@ -207,13 +214,13 @@ class BaseZwaveJSFlow(FlowHandler, ABC): async def async_step_install_failed( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Add-on installation failed.""" return self.async_abort(reason="addon_install_failed") async def async_step_start_addon( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Start Z-Wave JS add-on.""" if not self.start_task: self.start_task = self.hass.async_create_task(self._async_start_addon()) @@ -237,7 +244,7 @@ class BaseZwaveJSFlow(FlowHandler, ABC): async def async_step_start_failed( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Add-on start failed.""" return self.async_abort(reason="addon_start_failed") @@ -275,13 +282,13 @@ class BaseZwaveJSFlow(FlowHandler, ABC): @abstractmethod async def async_step_configure_addon( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Ask for config for Z-Wave JS add-on.""" @abstractmethod async def async_step_finish_addon_setup( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Prepare info needed to complete the config entry. Get add-on discovery info and server version info. @@ -325,7 +332,7 @@ class BaseZwaveJSFlow(FlowHandler, ABC): return discovery_info_config -class ConfigFlow(BaseZwaveJSFlow, config_entries.ConfigFlow, domain=DOMAIN): +class ZWaveJSConfigFlow(BaseZwaveJSFlow, ConfigFlow, domain=DOMAIN): """Handle a config flow for Z-Wave JS.""" VERSION = 1 @@ -338,19 +345,19 @@ class ConfigFlow(BaseZwaveJSFlow, config_entries.ConfigFlow, domain=DOMAIN): self._usb_discovery = False @property - def flow_manager(self) -> config_entries.ConfigEntriesFlowManager: + def flow_manager(self) -> ConfigEntriesFlowManager: """Return the correct flow manager.""" return self.hass.config_entries.flow @staticmethod @callback def async_get_options_flow( - config_entry: config_entries.ConfigEntry, + config_entry: ConfigEntry, ) -> OptionsFlowHandler: """Return the options flow.""" return OptionsFlowHandler(config_entry) - async def async_step_import(self, data: dict[str, Any]) -> FlowResult: + async def async_step_import(self, data: dict[str, Any]) -> ConfigFlowResult: """Handle imported data. This step will be used when importing data @@ -364,7 +371,7 @@ class ConfigFlow(BaseZwaveJSFlow, config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" if is_hassio(self.hass): return await self.async_step_on_supervisor() @@ -373,7 +380,7 @@ class ConfigFlow(BaseZwaveJSFlow, config_entries.ConfigFlow, domain=DOMAIN): async def async_step_zeroconf( self, discovery_info: ZeroconfServiceInfo - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle zeroconf discovery.""" home_id = str(discovery_info.properties["homeId"]) await self.async_set_unique_id(home_id) @@ -384,7 +391,7 @@ class ConfigFlow(BaseZwaveJSFlow, config_entries.ConfigFlow, domain=DOMAIN): async def async_step_zeroconf_confirm( self, user_input: dict | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm the setup.""" if user_input is not None: return await self.async_step_manual({CONF_URL: self.ws_address}) @@ -398,7 +405,9 @@ class ConfigFlow(BaseZwaveJSFlow, config_entries.ConfigFlow, domain=DOMAIN): }, ) - async def async_step_usb(self, discovery_info: usb.UsbServiceInfo) -> FlowResult: + async def async_step_usb( + self, discovery_info: usb.UsbServiceInfo + ) -> ConfigFlowResult: """Handle USB Discovery.""" if not is_hassio(self.hass): return self.async_abort(reason="discovery_requires_supervisor") @@ -441,7 +450,7 @@ class ConfigFlow(BaseZwaveJSFlow, config_entries.ConfigFlow, domain=DOMAIN): async def async_step_usb_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle USB Discovery confirmation.""" if user_input is None: return self.async_show_form( @@ -455,7 +464,7 @@ class ConfigFlow(BaseZwaveJSFlow, config_entries.ConfigFlow, domain=DOMAIN): async def async_step_manual( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a manual configuration.""" if user_input is None: return self.async_show_form( @@ -491,7 +500,9 @@ class ConfigFlow(BaseZwaveJSFlow, config_entries.ConfigFlow, domain=DOMAIN): step_id="manual", data_schema=get_manual_schema(user_input), errors=errors ) - async def async_step_hassio(self, discovery_info: HassioServiceInfo) -> FlowResult: + async def async_step_hassio( + self, discovery_info: HassioServiceInfo + ) -> ConfigFlowResult: """Receive configuration from add-on discovery info. This flow is triggered by the Z-Wave JS add-on. @@ -517,7 +528,7 @@ class ConfigFlow(BaseZwaveJSFlow, config_entries.ConfigFlow, domain=DOMAIN): async def async_step_hassio_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Confirm the add-on discovery.""" if user_input is not None: return await self.async_step_on_supervisor( @@ -528,7 +539,7 @@ class ConfigFlow(BaseZwaveJSFlow, config_entries.ConfigFlow, domain=DOMAIN): async def async_step_on_supervisor( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle logic when on Supervisor host.""" if user_input is None: return self.async_show_form( @@ -563,7 +574,7 @@ class ConfigFlow(BaseZwaveJSFlow, config_entries.ConfigFlow, domain=DOMAIN): async def async_step_configure_addon( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Ask for config for Z-Wave JS add-on.""" addon_info = await self._async_get_addon_info() addon_config = addon_info.options @@ -628,7 +639,7 @@ class ConfigFlow(BaseZwaveJSFlow, config_entries.ConfigFlow, domain=DOMAIN): async def async_step_finish_addon_setup( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Prepare info needed to complete the config entry. Get add-on discovery info and server version info. @@ -638,7 +649,7 @@ class ConfigFlow(BaseZwaveJSFlow, config_entries.ConfigFlow, domain=DOMAIN): discovery_info = await self._async_get_addon_discovery_info() self.ws_address = f"ws://{discovery_info['host']}:{discovery_info['port']}" - if not self.unique_id or self.context["source"] == config_entries.SOURCE_USB: + if not self.unique_id or self.context["source"] == SOURCE_USB: if not self.version_info: try: self.version_info = await async_get_version_info( @@ -664,7 +675,7 @@ class ConfigFlow(BaseZwaveJSFlow, config_entries.ConfigFlow, domain=DOMAIN): return self._async_create_entry_from_vars() @callback - def _async_create_entry_from_vars(self) -> FlowResult: + def _async_create_entry_from_vars(self) -> ConfigFlowResult: """Return a config entry for the flow.""" # Abort any other flows that may be in progress for progress in self._async_in_progress(): @@ -685,10 +696,10 @@ class ConfigFlow(BaseZwaveJSFlow, config_entries.ConfigFlow, domain=DOMAIN): ) -class OptionsFlowHandler(BaseZwaveJSFlow, config_entries.OptionsFlow): +class OptionsFlowHandler(BaseZwaveJSFlow, OptionsFlow): """Handle an options flow for Z-Wave JS.""" - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + def __init__(self, config_entry: ConfigEntry) -> None: """Set up the options flow.""" super().__init__() self.config_entry = config_entry @@ -696,7 +707,7 @@ class OptionsFlowHandler(BaseZwaveJSFlow, config_entries.OptionsFlow): self.revert_reason: str | None = None @property - def flow_manager(self) -> config_entries.OptionsFlowManager: + def flow_manager(self) -> OptionsFlowManager: """Return the correct flow manager.""" return self.hass.config_entries.options @@ -707,7 +718,7 @@ class OptionsFlowHandler(BaseZwaveJSFlow, config_entries.OptionsFlow): async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Manage the options.""" if is_hassio(self.hass): return await self.async_step_on_supervisor() @@ -716,7 +727,7 @@ class OptionsFlowHandler(BaseZwaveJSFlow, config_entries.OptionsFlow): async def async_step_manual( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a manual configuration.""" if user_input is None: return self.async_show_form( @@ -759,7 +770,7 @@ class OptionsFlowHandler(BaseZwaveJSFlow, config_entries.OptionsFlow): async def async_step_on_supervisor( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle logic when on Supervisor host.""" if user_input is None: return self.async_show_form( @@ -780,7 +791,7 @@ class OptionsFlowHandler(BaseZwaveJSFlow, config_entries.OptionsFlow): async def async_step_configure_addon( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Ask for config for Z-Wave JS add-on.""" addon_info = await self._async_get_addon_info() addon_config = addon_info.options @@ -819,7 +830,7 @@ class OptionsFlowHandler(BaseZwaveJSFlow, config_entries.OptionsFlow): if ( self.config_entry.data.get(CONF_USE_ADDON) - and self.config_entry.state == config_entries.ConfigEntryState.LOADED + and self.config_entry.state == ConfigEntryState.LOADED ): # Disconnect integration before restarting add-on. await disconnect_client(self.hass, self.config_entry) @@ -868,13 +879,13 @@ class OptionsFlowHandler(BaseZwaveJSFlow, config_entries.OptionsFlow): async def async_step_start_failed( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Add-on start failed.""" return await self.async_revert_addon_config(reason="addon_start_failed") async def async_step_finish_addon_setup( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Prepare info needed to complete the config entry update. Get add-on discovery info and server version info. @@ -918,7 +929,7 @@ class OptionsFlowHandler(BaseZwaveJSFlow, config_entries.OptionsFlow): self.hass.config_entries.async_schedule_reload(self.config_entry.entry_id) return self.async_create_entry(title=TITLE, data={}) - async def async_revert_addon_config(self, reason: str) -> FlowResult: + async def async_revert_addon_config(self, reason: str) -> ConfigFlowResult: """Abort the options flow. If the add-on options have been changed, revert those and restart add-on. @@ -944,11 +955,11 @@ class OptionsFlowHandler(BaseZwaveJSFlow, config_entries.OptionsFlow): return await self.async_step_configure_addon(addon_config_input) -class CannotConnect(exceptions.HomeAssistantError): +class CannotConnect(HomeAssistantError): """Indicate connection error.""" -class InvalidInput(exceptions.HomeAssistantError): +class InvalidInput(HomeAssistantError): """Error to indicate input data is invalid.""" def __init__(self, error: str) -> None: diff --git a/homeassistant/components/zwave_js/config_validation.py b/homeassistant/components/zwave_js/config_validation.py index 9fc502bdafb..6c060f90ce5 100644 --- a/homeassistant/components/zwave_js/config_validation.py +++ b/homeassistant/components/zwave_js/config_validation.py @@ -1,4 +1,5 @@ """Config validation for the Z-Wave JS integration.""" + from typing import Any import voluptuous as vol diff --git a/homeassistant/components/zwave_js/const.py b/homeassistant/components/zwave_js/const.py index 656620d01dd..f022cd42d20 100644 --- a/homeassistant/components/zwave_js/const.py +++ b/homeassistant/components/zwave_js/const.py @@ -1,4 +1,5 @@ """Constants for the Z-Wave JS integration.""" + from __future__ import annotations import logging diff --git a/homeassistant/components/zwave_js/cover.py b/homeassistant/components/zwave_js/cover.py index 27919a17614..f0ef1913bbb 100644 --- a/homeassistant/components/zwave_js/cover.py +++ b/homeassistant/components/zwave_js/cover.py @@ -1,4 +1,5 @@ """Support for Z-Wave cover devices.""" + from __future__ import annotations from typing import Any, cast @@ -378,12 +379,11 @@ class ZWaveWindowCovering(CoverPositionMixin, CoverTiltMixin): assert self._attr_supported_features self._attr_supported_features ^= set_position_feature - additional_info: list[str] = [] - for value in (self._current_position_value, self._current_tilt_value): - if value and value.property_key_name: - additional_info.append( - value.property_key_name.removesuffix(f" {NO_POSITION_SUFFIX}") - ) + additional_info: list[str] = [ + value.property_key_name.removesuffix(f" {NO_POSITION_SUFFIX}") + for value in (self._current_position_value, self._current_tilt_value) + if value and value.property_key_name + ] self._attr_name = self.generate_name(additional_info=additional_info) self._attr_device_class = CoverDeviceClass.WINDOW diff --git a/homeassistant/components/zwave_js/device_action.py b/homeassistant/components/zwave_js/device_action.py index b9b0c3a6e86..bec9c8e55ab 100644 --- a/homeassistant/components/zwave_js/device_action.py +++ b/homeassistant/components/zwave_js/device_action.py @@ -1,4 +1,5 @@ """Provides device actions for Z-Wave JS.""" + from __future__ import annotations from collections import defaultdict @@ -237,15 +238,15 @@ async def async_get_actions( CONF_SUBTYPE: f"Endpoint {endpoint} (All)", } ) - for meter_type in endpoint_data[ATTR_METER_TYPE]: - actions.append( - { - **base_action, - CONF_TYPE: SERVICE_RESET_METER, - ATTR_METER_TYPE: meter_type, - CONF_SUBTYPE: f"Endpoint {endpoint} ({meter_type.name})", - } - ) + actions.extend( + { + **base_action, + CONF_TYPE: SERVICE_RESET_METER, + ATTR_METER_TYPE: meter_type, + CONF_SUBTYPE: f"Endpoint {endpoint} ({meter_type.name})", + } + for meter_type in endpoint_data[ATTR_METER_TYPE] + ) return actions diff --git a/homeassistant/components/zwave_js/device_automation_helpers.py b/homeassistant/components/zwave_js/device_automation_helpers.py index 2c375485e6b..5c94b2bb02d 100644 --- a/homeassistant/components/zwave_js/device_automation_helpers.py +++ b/homeassistant/components/zwave_js/device_automation_helpers.py @@ -1,4 +1,5 @@ """Provides helpers for Z-Wave JS device automations.""" + from __future__ import annotations from zwave_js_server.client import Client as ZwaveClient diff --git a/homeassistant/components/zwave_js/device_condition.py b/homeassistant/components/zwave_js/device_condition.py index 26b4c637b6e..dcd42d4d85d 100644 --- a/homeassistant/components/zwave_js/device_condition.py +++ b/homeassistant/components/zwave_js/device_condition.py @@ -1,4 +1,5 @@ """Provide the device conditions for Z-Wave JS.""" + from __future__ import annotations from typing import cast diff --git a/homeassistant/components/zwave_js/device_trigger.py b/homeassistant/components/zwave_js/device_trigger.py index d2b6ab7af15..49027d4d43b 100644 --- a/homeassistant/components/zwave_js/device_trigger.py +++ b/homeassistant/components/zwave_js/device_trigger.py @@ -1,4 +1,5 @@ """Provides device triggers for Z-Wave JS.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/zwave_js/diagnostics.py b/homeassistant/components/zwave_js/diagnostics.py index afae214ab2b..777d45efddb 100644 --- a/homeassistant/components/zwave_js/diagnostics.py +++ b/homeassistant/components/zwave_js/diagnostics.py @@ -1,4 +1,5 @@ """Provides diagnostics for Z-Wave JS.""" + from __future__ import annotations from copy import deepcopy diff --git a/homeassistant/components/zwave_js/discovery.py b/homeassistant/components/zwave_js/discovery.py index dfe2294e710..272f6e3ddc0 100644 --- a/homeassistant/components/zwave_js/discovery.py +++ b/homeassistant/components/zwave_js/discovery.py @@ -1,4 +1,5 @@ """Map Z-Wave nodes and values to Home Assistant entities.""" + from __future__ import annotations from collections.abc import Generator diff --git a/homeassistant/components/zwave_js/discovery_data_template.py b/homeassistant/components/zwave_js/discovery_data_template.py index 61a0cfdb802..7eb85e0ea4d 100644 --- a/homeassistant/components/zwave_js/discovery_data_template.py +++ b/homeassistant/components/zwave_js/discovery_data_template.py @@ -1,4 +1,5 @@ """Data template classes for discovery used to generate additional data for setup.""" + from __future__ import annotations from collections.abc import Iterable, Mapping diff --git a/homeassistant/components/zwave_js/entity.py b/homeassistant/components/zwave_js/entity.py index e7e110e7db6..4a6f87cc032 100644 --- a/homeassistant/components/zwave_js/entity.py +++ b/homeassistant/components/zwave_js/entity.py @@ -1,4 +1,5 @@ """Generic Z-Wave Entity Class.""" + from __future__ import annotations from collections.abc import Sequence @@ -202,7 +203,7 @@ class ZWaveBaseEntity(Entity): property_key=primary_value.property_key, ) in self.info.node.values - for endpoint_idx in range(0, primary_value.endpoint) + for endpoint_idx in range(primary_value.endpoint) ): name += f" ({primary_value.endpoint})" diff --git a/homeassistant/components/zwave_js/event.py b/homeassistant/components/zwave_js/event.py index 93860b6273e..2b170bdf5bd 100644 --- a/homeassistant/components/zwave_js/event.py +++ b/homeassistant/components/zwave_js/event.py @@ -1,4 +1,5 @@ """Support for Z-Wave controls using the event platform.""" + from __future__ import annotations from zwave_js_server.client import Client as ZwaveClient diff --git a/homeassistant/components/zwave_js/fan.py b/homeassistant/components/zwave_js/fan.py index d4247b65c8b..4cf9a5d40cf 100644 --- a/homeassistant/components/zwave_js/fan.py +++ b/homeassistant/components/zwave_js/fan.py @@ -1,4 +1,5 @@ """Support for Z-Wave fans.""" + from __future__ import annotations import math diff --git a/homeassistant/components/zwave_js/helpers.py b/homeassistant/components/zwave_js/helpers.py index c8eb02ad6cb..4f1b902d8ba 100644 --- a/homeassistant/components/zwave_js/helpers.py +++ b/homeassistant/components/zwave_js/helpers.py @@ -1,4 +1,5 @@ """Helper functions for Z-Wave JS integration.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/zwave_js/humidifier.py b/homeassistant/components/zwave_js/humidifier.py index 14a43bea3af..4030115ab1f 100644 --- a/homeassistant/components/zwave_js/humidifier.py +++ b/homeassistant/components/zwave_js/humidifier.py @@ -1,4 +1,5 @@ """Representation of Z-Wave humidifiers.""" + from __future__ import annotations from dataclasses import dataclass @@ -34,9 +35,9 @@ from .entity import ZWaveBaseEntity PARALLEL_UPDATES = 0 -@dataclass(frozen=True) -class ZwaveHumidifierEntityDescriptionRequiredKeys: - """A class for humidifier entity description required keys.""" +@dataclass(frozen=True, kw_only=True) +class ZwaveHumidifierEntityDescription(HumidifierEntityDescription): + """A class that describes the humidifier or dehumidifier entity.""" # The "on" control mode for this entity, e.g. HUMIDIFY for humidifier on_mode: HumidityControlMode @@ -48,13 +49,6 @@ class ZwaveHumidifierEntityDescriptionRequiredKeys: setpoint_type: HumidityControlSetpointType -@dataclass(frozen=True) -class ZwaveHumidifierEntityDescription( - HumidifierEntityDescription, ZwaveHumidifierEntityDescriptionRequiredKeys -): - """A class that describes the humidifier or dehumidifier entity.""" - - HUMIDIFIER_ENTITY_DESCRIPTION = ZwaveHumidifierEntityDescription( key="humidifier", device_class=HumidifierDeviceClass.HUMIDIFIER, diff --git a/homeassistant/components/zwave_js/light.py b/homeassistant/components/zwave_js/light.py index b105b556e24..eba2d4a0cce 100644 --- a/homeassistant/components/zwave_js/light.py +++ b/homeassistant/components/zwave_js/light.py @@ -1,4 +1,5 @@ """Support for Z-Wave lights.""" + from __future__ import annotations from typing import Any, cast diff --git a/homeassistant/components/zwave_js/lock.py b/homeassistant/components/zwave_js/lock.py index 59faf7fbbb6..d102e5b5f22 100644 --- a/homeassistant/components/zwave_js/lock.py +++ b/homeassistant/components/zwave_js/lock.py @@ -1,4 +1,5 @@ """Representation of Z-Wave locks.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/zwave_js/logbook.py b/homeassistant/components/zwave_js/logbook.py index 1f634ba5ffd..315793b9726 100644 --- a/homeassistant/components/zwave_js/logbook.py +++ b/homeassistant/components/zwave_js/logbook.py @@ -1,4 +1,5 @@ """Describe Z-Wave JS logbook events.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index 40c896c516a..a06de5cb8ee 100644 --- a/homeassistant/components/zwave_js/manifest.json +++ b/homeassistant/components/zwave_js/manifest.json @@ -5,7 +5,6 @@ "config_flow": true, "dependencies": ["http", "repairs", "usb", "websocket_api"], "documentation": "https://www.home-assistant.io/integrations/zwave_js", - "import_executor": true, "integration_type": "hub", "iot_class": "local_push", "loggers": ["zwave_js_server"], diff --git a/homeassistant/components/zwave_js/migrate.py b/homeassistant/components/zwave_js/migrate.py index a7543c8f6f6..bde53137dc1 100644 --- a/homeassistant/components/zwave_js/migrate.py +++ b/homeassistant/components/zwave_js/migrate.py @@ -1,4 +1,5 @@ """Functions used to migrate unique IDs for Z-Wave JS entities.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/homeassistant/components/zwave_js/number.py b/homeassistant/components/zwave_js/number.py index 6aa4a57ea4c..15262710095 100644 --- a/homeassistant/components/zwave_js/number.py +++ b/homeassistant/components/zwave_js/number.py @@ -1,4 +1,5 @@ """Support for Z-Wave controls using the number platform.""" + from __future__ import annotations from collections.abc import Mapping diff --git a/homeassistant/components/zwave_js/repairs.py b/homeassistant/components/zwave_js/repairs.py index 1010d9abd90..e515ae10549 100644 --- a/homeassistant/components/zwave_js/repairs.py +++ b/homeassistant/components/zwave_js/repairs.py @@ -1,4 +1,5 @@ """Repairs for Z-Wave JS.""" + from __future__ import annotations from homeassistant import data_entry_flow diff --git a/homeassistant/components/zwave_js/scripts/convert_device_diagnostics_to_fixture.py b/homeassistant/components/zwave_js/scripts/convert_device_diagnostics_to_fixture.py index 826f3eebe0c..1005c3bb4db 100644 --- a/homeassistant/components/zwave_js/scripts/convert_device_diagnostics_to_fixture.py +++ b/homeassistant/components/zwave_js/scripts/convert_device_diagnostics_to_fixture.py @@ -1,4 +1,5 @@ """Script to convert a device diagnostics file to a fixture.""" + from __future__ import annotations import argparse diff --git a/homeassistant/components/zwave_js/select.py b/homeassistant/components/zwave_js/select.py index e838949d3e1..c970c17f5f0 100644 --- a/homeassistant/components/zwave_js/select.py +++ b/homeassistant/components/zwave_js/select.py @@ -1,4 +1,5 @@ """Support for Z-Wave controls using the select platform.""" + from __future__ import annotations from typing import cast diff --git a/homeassistant/components/zwave_js/sensor.py b/homeassistant/components/zwave_js/sensor.py index af3bc8a622e..f799a70110d 100644 --- a/homeassistant/components/zwave_js/sensor.py +++ b/homeassistant/components/zwave_js/sensor.py @@ -1,4 +1,5 @@ """Representation of Z-Wave sensors.""" + from __future__ import annotations from collections.abc import Callable, Mapping diff --git a/homeassistant/components/zwave_js/services.py b/homeassistant/components/zwave_js/services.py index e8ef1df4b96..5567c64ab97 100644 --- a/homeassistant/components/zwave_js/services.py +++ b/homeassistant/components/zwave_js/services.py @@ -1,4 +1,5 @@ """Methods and classes related to executing Z-Wave commands.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/zwave_js/siren.py b/homeassistant/components/zwave_js/siren.py index 7df88f7dca4..b3f54ae9904 100644 --- a/homeassistant/components/zwave_js/siren.py +++ b/homeassistant/components/zwave_js/siren.py @@ -1,4 +1,5 @@ """Support for Z-Wave controls using the siren platform.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/zwave_js/switch.py b/homeassistant/components/zwave_js/switch.py index 409bcd1dbb7..30ee5fb72bc 100644 --- a/homeassistant/components/zwave_js/switch.py +++ b/homeassistant/components/zwave_js/switch.py @@ -1,4 +1,5 @@ """Representation of Z-Wave switches.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/zwave_js/trigger.py b/homeassistant/components/zwave_js/trigger.py index 94cb05b1b20..9cb1a3e1d7e 100644 --- a/homeassistant/components/zwave_js/trigger.py +++ b/homeassistant/components/zwave_js/trigger.py @@ -1,4 +1,5 @@ """Z-Wave JS trigger dispatcher.""" + from __future__ import annotations from homeassistant.const import CONF_PLATFORM diff --git a/homeassistant/components/zwave_js/triggers/event.py b/homeassistant/components/zwave_js/triggers/event.py index edc10d4a16e..6cf4a31c0eb 100644 --- a/homeassistant/components/zwave_js/triggers/event.py +++ b/homeassistant/components/zwave_js/triggers/event.py @@ -1,4 +1,5 @@ """Offer Z-Wave JS event listening automation trigger.""" + from __future__ import annotations from collections.abc import Callable @@ -196,9 +197,9 @@ async def async_attach_trigger( else: payload["description"] = primary_desc - payload[ - "description" - ] = f"{payload['description']} with event data: {event_data}" + payload["description"] = ( + f"{payload['description']} with event data: {event_data}" + ) hass.async_run_hass_job(job, {"trigger": payload}) @@ -238,15 +239,14 @@ async def async_attach_trigger( unsubs.append( node.on(event_name, functools.partial(async_on_event, device=device)) ) - - for driver in drivers: - unsubs.append( - async_dispatcher_connect( - hass, - f"{DOMAIN}_{driver.controller.home_id}_connected_to_server", - _create_zwave_listeners, - ) + unsubs.extend( + async_dispatcher_connect( + hass, + f"{DOMAIN}_{driver.controller.home_id}_connected_to_server", + _create_zwave_listeners, ) + for driver in drivers + ) _create_zwave_listeners() diff --git a/homeassistant/components/zwave_js/triggers/trigger_helpers.py b/homeassistant/components/zwave_js/triggers/trigger_helpers.py index 0fd9c3b4291..1dbe1f48f0a 100644 --- a/homeassistant/components/zwave_js/triggers/trigger_helpers.py +++ b/homeassistant/components/zwave_js/triggers/trigger_helpers.py @@ -1,4 +1,5 @@ """Helpers for Z-Wave JS custom triggers.""" + from zwave_js_server.client import Client as ZwaveClient from homeassistant.config_entries import ConfigEntryState diff --git a/homeassistant/components/zwave_js/triggers/value_updated.py b/homeassistant/components/zwave_js/triggers/value_updated.py index c44a0c6336a..4814eba0757 100644 --- a/homeassistant/components/zwave_js/triggers/value_updated.py +++ b/homeassistant/components/zwave_js/triggers/value_updated.py @@ -1,4 +1,5 @@ """Offer Z-Wave JS value updated listening automation trigger.""" + from __future__ import annotations from collections.abc import Callable @@ -193,14 +194,14 @@ async def async_attach_trigger( ) ) - for driver in drivers: - unsubs.append( - async_dispatcher_connect( - hass, - f"{DOMAIN}_{driver.controller.home_id}_connected_to_server", - _create_zwave_listeners, - ) + unsubs.extend( + async_dispatcher_connect( + hass, + f"{DOMAIN}_{driver.controller.home_id}_connected_to_server", + _create_zwave_listeners, ) + for driver in drivers + ) _create_zwave_listeners() diff --git a/homeassistant/components/zwave_js/update.py b/homeassistant/components/zwave_js/update.py index f3e60f925e6..3fdbab8aacf 100644 --- a/homeassistant/components/zwave_js/update.py +++ b/homeassistant/components/zwave_js/update.py @@ -1,4 +1,5 @@ """Representation of Z-Wave updates.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/components/zwave_me/__init__.py b/homeassistant/components/zwave_me/__init__.py index e35f55d6fda..66f02246792 100644 --- a/homeassistant/components/zwave_me/__init__.py +++ b/homeassistant/components/zwave_me/__init__.py @@ -1,4 +1,5 @@ """The Z-Wave-Me WS integration.""" + import logging from zwave_me_ws import ZWaveMe, ZWaveMeData @@ -27,7 +28,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: registry = dr.async_get(hass) controller.remove_stale_devices(registry) return True - raise ConfigEntryNotReady() + raise ConfigEntryNotReady async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/zwave_me/binary_sensor.py b/homeassistant/components/zwave_me/binary_sensor.py index f1ee6896b25..3be8f912b6d 100644 --- a/homeassistant/components/zwave_me/binary_sensor.py +++ b/homeassistant/components/zwave_me/binary_sensor.py @@ -1,4 +1,5 @@ """Representation of a sensorBinary.""" + from __future__ import annotations from zwave_me_ws import ZWaveMeData diff --git a/homeassistant/components/zwave_me/button.py b/homeassistant/components/zwave_me/button.py index 7e0b4f02728..f7f1d5d7945 100644 --- a/homeassistant/components/zwave_me/button.py +++ b/homeassistant/components/zwave_me/button.py @@ -1,4 +1,5 @@ """Representation of a toggleButton.""" + from homeassistant.components.button import ButtonEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback diff --git a/homeassistant/components/zwave_me/climate.py b/homeassistant/components/zwave_me/climate.py index 35e0d745619..02112e51617 100644 --- a/homeassistant/components/zwave_me/climate.py +++ b/homeassistant/components/zwave_me/climate.py @@ -1,4 +1,5 @@ """Representation of a thermostat.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/zwave_me/config_flow.py b/homeassistant/components/zwave_me/config_flow.py index 0c7d77b0153..1444bfc1b95 100644 --- a/homeassistant/components/zwave_me/config_flow.py +++ b/homeassistant/components/zwave_me/config_flow.py @@ -1,4 +1,5 @@ """Config flow to configure ZWaveMe integration.""" + from __future__ import annotations import logging @@ -6,10 +7,9 @@ import logging from url_normalize import url_normalize import voluptuous as vol -from homeassistant import config_entries from homeassistant.components.zeroconf import ZeroconfServiceInfo +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_TOKEN, CONF_URL -from homeassistant.data_entry_flow import FlowResult from . import helpers from .const import DOMAIN @@ -17,7 +17,7 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -class ZWaveMeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class ZWaveMeConfigFlow(ConfigFlow, domain=DOMAIN): """ZWaveMe integration config flow.""" def __init__(self) -> None: @@ -28,7 +28,7 @@ class ZWaveMeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, str] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by the user or started with zeroconf.""" errors = {} placeholders = { @@ -88,7 +88,7 @@ class ZWaveMeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_zeroconf( self, discovery_info: ZeroconfServiceInfo - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a discovered Z-Wave accessory - get url to pass into user step. This flow is triggered by the discovery component. diff --git a/homeassistant/components/zwave_me/const.py b/homeassistant/components/zwave_me/const.py index 1ec4f8d1601..8dcfc369db7 100644 --- a/homeassistant/components/zwave_me/const.py +++ b/homeassistant/components/zwave_me/const.py @@ -1,4 +1,5 @@ """Constants for ZWaveMe.""" + from enum import StrEnum from homeassistant.const import Platform diff --git a/homeassistant/components/zwave_me/cover.py b/homeassistant/components/zwave_me/cover.py index e717df027e3..4794e807049 100644 --- a/homeassistant/components/zwave_me/cover.py +++ b/homeassistant/components/zwave_me/cover.py @@ -1,4 +1,5 @@ """Representation of a cover.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/zwave_me/fan.py b/homeassistant/components/zwave_me/fan.py index c332fb305c5..25ccec9a0fb 100644 --- a/homeassistant/components/zwave_me/fan.py +++ b/homeassistant/components/zwave_me/fan.py @@ -1,4 +1,5 @@ """Representation of a fan.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/zwave_me/helpers.py b/homeassistant/components/zwave_me/helpers.py index 0d53512d1cb..3b5cb4ad0be 100644 --- a/homeassistant/components/zwave_me/helpers.py +++ b/homeassistant/components/zwave_me/helpers.py @@ -1,4 +1,5 @@ """Helpers for zwave_me config flow.""" + from __future__ import annotations from zwave_me_ws import ZWaveMe diff --git a/homeassistant/components/zwave_me/light.py b/homeassistant/components/zwave_me/light.py index 7fad88a4a49..b1065d45160 100644 --- a/homeassistant/components/zwave_me/light.py +++ b/homeassistant/components/zwave_me/light.py @@ -1,4 +1,5 @@ """Representation of an RGB light.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/zwave_me/lock.py b/homeassistant/components/zwave_me/lock.py index 17e64ff1602..6218dac1627 100644 --- a/homeassistant/components/zwave_me/lock.py +++ b/homeassistant/components/zwave_me/lock.py @@ -1,4 +1,5 @@ """Representation of a doorlock.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/components/zwave_me/number.py b/homeassistant/components/zwave_me/number.py index 4e9acc1f76b..28fd8abe460 100644 --- a/homeassistant/components/zwave_me/number.py +++ b/homeassistant/components/zwave_me/number.py @@ -1,4 +1,5 @@ """Representation of a switchMultilevel.""" + from homeassistant.components.number import NumberEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback diff --git a/homeassistant/components/zwave_me/sensor.py b/homeassistant/components/zwave_me/sensor.py index f96e2d789ff..20470e6e62b 100644 --- a/homeassistant/components/zwave_me/sensor.py +++ b/homeassistant/components/zwave_me/sensor.py @@ -1,4 +1,5 @@ """Representation of a sensorMultilevel.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/components/zwave_me/siren.py b/homeassistant/components/zwave_me/siren.py index c6757f61ad7..a1bf8081616 100644 --- a/homeassistant/components/zwave_me/siren.py +++ b/homeassistant/components/zwave_me/siren.py @@ -1,4 +1,5 @@ """Representation of a sirenBinary.""" + from typing import Any from homeassistant.components.siren import SirenEntity, SirenEntityFeature diff --git a/homeassistant/components/zwave_me/switch.py b/homeassistant/components/zwave_me/switch.py index c9a824c5e0d..4c11f079b12 100644 --- a/homeassistant/components/zwave_me/switch.py +++ b/homeassistant/components/zwave_me/switch.py @@ -1,4 +1,5 @@ """Representation of a switchBinary.""" + import logging from typing import Any diff --git a/homeassistant/config.py b/homeassistant/config.py index 3e593a564a2..c570e36c6c1 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -1,12 +1,14 @@ """Module to help with parsing and generating configuration files.""" + from __future__ import annotations +import asyncio from collections import OrderedDict -from collections.abc import Callable, Iterable, Sequence +from collections.abc import Callable, Hashable, Iterable, Sequence from contextlib import suppress from dataclasses import dataclass from enum import StrEnum -from functools import reduce +from functools import partial, reduce import logging import operator import os @@ -56,14 +58,16 @@ from .const import ( LEGACY_CONF_WHITELIST_EXTERNAL_DIRS, __version__, ) -from .core import DOMAIN as CONF_CORE, ConfigSource, HomeAssistant, callback +from .core import DOMAIN as HA_DOMAIN, ConfigSource, HomeAssistant, callback from .exceptions import ConfigValidationError, HomeAssistantError from .generated.currencies import HISTORIC_CURRENCIES from .helpers import config_validation as cv, issue_registry as ir from .helpers.entity_values import EntityValues +from .helpers.translation import async_get_exception_message from .helpers.typing import ConfigType from .loader import ComponentProtocol, Integration, IntegrationNotFound from .requirements import RequirementsNotFound, async_get_integration_with_requirements +from .util.async_ import create_eager_task from .util.package import is_docker_env from .util.unit_system import get_unit_system, validate_unit_system from .util.yaml import SECRET_YAML, Secrets, YamlTypeError, load_yaml_dict @@ -127,13 +131,23 @@ class ConfigErrorTranslationKey(StrEnum): CONFIG_PLATFORM_IMPORT_ERR = "config_platform_import_err" CONFIG_VALIDATOR_UNKNOWN_ERR = "config_validator_unknown_err" CONFIG_SCHEMA_UNKNOWN_ERR = "config_schema_unknown_err" - PLATFORM_VALIDATOR_UNKNOWN_ERR = "platform_validator_unknown_err" PLATFORM_COMPONENT_LOAD_ERR = "platform_component_load_err" PLATFORM_COMPONENT_LOAD_EXC = "platform_component_load_exc" PLATFORM_SCHEMA_VALIDATOR_ERR = "platform_schema_validator_err" # translation key in case multiple errors occurred - INTEGRATION_CONFIG_ERROR = "integration_config_error" + MULTIPLE_INTEGRATION_CONFIG_ERRORS = "multiple_integration_config_errors" + + +_CONFIG_LOG_SHOW_STACK_TRACE: dict[ConfigErrorTranslationKey, bool] = { + ConfigErrorTranslationKey.COMPONENT_IMPORT_ERR: False, + ConfigErrorTranslationKey.CONFIG_PLATFORM_IMPORT_ERR: False, + ConfigErrorTranslationKey.CONFIG_VALIDATOR_UNKNOWN_ERR: True, + ConfigErrorTranslationKey.CONFIG_SCHEMA_UNKNOWN_ERR: True, + ConfigErrorTranslationKey.PLATFORM_COMPONENT_LOAD_ERR: False, + ConfigErrorTranslationKey.PLATFORM_COMPONENT_LOAD_EXC: True, + ConfigErrorTranslationKey.PLATFORM_SCHEMA_VALIDATOR_ERR: True, +} @dataclass @@ -244,12 +258,12 @@ CUSTOMIZE_CONFIG_SCHEMA = vol.Schema( def _raise_issue_if_historic_currency(hass: HomeAssistant, currency: str) -> None: if currency not in HISTORIC_CURRENCIES: - ir.async_delete_issue(hass, "homeassistant", "historic_currency") + ir.async_delete_issue(hass, HA_DOMAIN, "historic_currency") return ir.async_create_issue( hass, - "homeassistant", + HA_DOMAIN, "historic_currency", is_fixable=False, learn_more_url="homeassistant://config/general", @@ -261,12 +275,12 @@ def _raise_issue_if_historic_currency(hass: HomeAssistant, currency: str) -> Non def _raise_issue_if_no_country(hass: HomeAssistant, country: str | None) -> None: if country is not None: - ir.async_delete_issue(hass, "homeassistant", "country_not_configured") + ir.async_delete_issue(hass, HA_DOMAIN, "country_not_configured") return ir.async_create_issue( hass, - "homeassistant", + HA_DOMAIN, "country_not_configured", is_fixable=False, learn_more_url="homeassistant://config/general", @@ -285,7 +299,7 @@ def _raise_issue_if_legacy_templates( if legacy_templates: ir.async_create_issue( hass, - "homeassistant", + HA_DOMAIN, "legacy_templates_true", is_fixable=False, breaks_in_ha_version="2024.7.0", @@ -294,12 +308,12 @@ def _raise_issue_if_legacy_templates( ) return - ir.async_delete_issue(hass, "homeassistant", "legacy_templates_true") + ir.async_delete_issue(hass, HA_DOMAIN, "legacy_templates_true") if legacy_templates is False: ir.async_create_issue( hass, - "homeassistant", + HA_DOMAIN, "legacy_templates_false", is_fixable=False, breaks_in_ha_version="2024.7.0", @@ -307,7 +321,7 @@ def _raise_issue_if_legacy_templates( translation_key="legacy_templates_false", ) else: - ir.async_delete_issue(hass, "homeassistant", "legacy_templates_false") + ir.async_delete_issue(hass, HA_DOMAIN, "legacy_templates_false") def _validate_currency(data: Any) -> Any: @@ -500,12 +514,12 @@ async def async_hass_config_yaml(hass: HomeAssistant) -> dict: for invalid_domain in invalid_domains: config.pop(invalid_domain) - core_config = config.get(CONF_CORE, {}) + core_config = config.get(HA_DOMAIN, {}) try: await merge_packages_config(hass, config, core_config.get(CONF_PACKAGES, {})) except vol.Invalid as exc: suffix = "" - if annotation := find_annotation(config, [CONF_CORE, CONF_PACKAGES] + exc.path): + if annotation := find_annotation(config, [HA_DOMAIN, CONF_PACKAGES, *exc.path]): suffix = f" at {_relpath(hass, annotation[0])}, line {annotation[1]}" _LOGGER.error( "Invalid package configuration '%s'%s: %s", CONF_PACKAGES, suffix, exc @@ -728,7 +742,7 @@ def stringify_invalid( ) else: message_prefix = f"Invalid config for '{domain}'" - if domain != CONF_CORE and link: + if domain != HA_DOMAIN and link: message_suffix = f", please check the docs at {link}" else: message_suffix = "" @@ -811,7 +825,7 @@ def format_homeassistant_error( if annotation := find_annotation(config, [domain]): message_prefix += f" at {_relpath(hass, annotation[0])}, line {annotation[1]}" message = f"{message_prefix}: {str(exc) or repr(exc)}" - if domain != CONF_CORE and link: + if domain != HA_DOMAIN and link: message += f", please check the docs at {link}" return message @@ -930,7 +944,7 @@ async def async_process_ha_core_config(hass: HomeAssistant, config: dict) -> Non cust_glob = OrderedDict(config[CONF_CUSTOMIZE_GLOB]) for name, pkg in config[CONF_PACKAGES].items(): - if (pkg_cust := pkg.get(CONF_CORE)) is None: + if (pkg_cust := pkg.get(HA_DOMAIN)) is None: continue try: @@ -954,7 +968,7 @@ def _log_pkg_error( ) -> None: """Log an error while merging packages.""" message_prefix = f"Setup of package '{package}'" - if annotation := find_annotation(config, [CONF_CORE, CONF_PACKAGES, package]): + if annotation := find_annotation(config, [HA_DOMAIN, CONF_PACKAGES, package]): message_prefix += f" at {_relpath(hass, annotation[0])}, line {annotation[1]}" _LOGGER.error("%s failed: %s", message_prefix, message) @@ -1069,7 +1083,7 @@ async def merge_packages_config( continue for comp_name, comp_conf in pack_conf.items(): - if comp_name == CONF_CORE: + if comp_name == HA_DOMAIN: continue try: domain = cv.domain_key(comp_name) @@ -1083,7 +1097,7 @@ async def merge_packages_config( integration = await async_get_integration_with_requirements( hass, domain ) - component = integration.get_component() + component = await integration.async_get_component() except LOAD_EXCEPTIONS as exc: _log_pkg_error( hass, @@ -1098,7 +1112,9 @@ async def merge_packages_config( continue try: - config_platform: ModuleType | None = integration.get_platform("config") + config_platform: ( + ModuleType | None + ) = await integration.async_get_platform("config") # Test if config platform has a config validator if not hasattr(config_platform, "async_validate_config"): config_platform = None @@ -1178,48 +1194,16 @@ def _get_log_message_and_stack_print_pref( platform_config = platform_exception.config link = platform_exception.integration_link - placeholders: dict[str, str] = {"domain": domain, "error": str(exception)} - - log_message_mapping: dict[ConfigErrorTranslationKey, tuple[str, bool]] = { - ConfigErrorTranslationKey.COMPONENT_IMPORT_ERR: ( - f"Unable to import {domain}: {exception}", - False, - ), - ConfigErrorTranslationKey.CONFIG_PLATFORM_IMPORT_ERR: ( - f"Error importing config platform {domain}: {exception}", - False, - ), - ConfigErrorTranslationKey.CONFIG_VALIDATOR_UNKNOWN_ERR: ( - f"Unknown error calling {domain} config validator", - True, - ), - ConfigErrorTranslationKey.CONFIG_SCHEMA_UNKNOWN_ERR: ( - f"Unknown error calling {domain} CONFIG_SCHEMA", - True, - ), - ConfigErrorTranslationKey.PLATFORM_VALIDATOR_UNKNOWN_ERR: ( - f"Unknown error validating {platform_path} platform config with {domain} " - "component platform schema", - True, - ), - ConfigErrorTranslationKey.PLATFORM_COMPONENT_LOAD_ERR: ( - f"Platform error: {domain} - {exception}", - False, - ), - ConfigErrorTranslationKey.PLATFORM_COMPONENT_LOAD_EXC: ( - f"Platform error: {domain} - {exception}", - True, - ), - ConfigErrorTranslationKey.PLATFORM_SCHEMA_VALIDATOR_ERR: ( - f"Unknown error validating config for {platform_path} platform " - f"for {domain} component with PLATFORM_SCHEMA", - True, - ), + placeholders: dict[str, str] = { + "domain": domain, + "error": str(exception), + "p_name": platform_path, } - log_message_show_stack_trace = log_message_mapping.get( + + show_stack_trace: bool | None = _CONFIG_LOG_SHOW_STACK_TRACE.get( platform_exception.translation_key ) - if log_message_show_stack_trace is None: + if show_stack_trace is None: # If no pre defined log_message is set, we generate an enriched error # message, so we can notify about it during setup show_stack_trace = False @@ -1242,9 +1226,14 @@ def _get_log_message_and_stack_print_pref( show_stack_trace = True return (log_message, show_stack_trace, placeholders) - assert isinstance(log_message_show_stack_trace, tuple) + # Generate the log message from the English translations + log_message = async_get_exception_message( + HA_DOMAIN, + platform_exception.translation_key, + translation_placeholders=placeholders, + ) - return (*log_message_show_stack_trace, placeholders) + return (log_message, show_stack_trace, placeholders) async def async_process_component_and_handle_errors( @@ -1300,7 +1289,7 @@ def async_drop_config_annotations( # Don't drop annotations from the homeassistant integration because it may # have configuration for other integrations as packages. - if integration.domain in config and integration.domain != CONF_CORE: + if integration.domain in config and integration.domain != HA_DOMAIN: drop_config_annotations_rec(config[integration.domain]) return config @@ -1343,21 +1332,16 @@ def async_handle_component_errors( if len(config_exception_info) == 1: translation_key = platform_exception.translation_key else: - translation_key = ConfigErrorTranslationKey.INTEGRATION_CONFIG_ERROR + translation_key = ConfigErrorTranslationKey.MULTIPLE_INTEGRATION_CONFIG_ERRORS errors = str(len(config_exception_info)) - log_message = ( - f"Failed to process component config for integration {domain} " - f"due to multiple errors ({errors}), check the logs for more information." - ) placeholders = { "domain": domain, "errors": errors, } raise ConfigValidationError( - str(log_message), + translation_key, [platform_exception.exception for platform_exception in config_exception_info], - translation_domain="homeassistant", - translation_key=translation_key, + translation_domain=HA_DOMAIN, translation_placeholders=placeholders, ) @@ -1388,6 +1372,35 @@ def config_per_platform( yield platform, item +def extract_platform_integrations( + config: ConfigType, domains: set[str] +) -> dict[str, set[str]]: + """Find all the platforms in a configuration. + + Returns a dictionary with domain as key and a set of platforms as value. + """ + platform_integrations: dict[str, set[str]] = {} + for key, domain_config in config.items(): + try: + domain = cv.domain_key(key) + except vol.Invalid: + continue + if domain not in domains: + continue + + if not isinstance(domain_config, list): + domain_config = [domain_config] + + for item in domain_config: + try: + platform = item.get(CONF_PLATFORM) + except AttributeError: + continue + if platform and isinstance(platform, Hashable): + platform_integrations.setdefault(domain, set()).add(platform) + return platform_integrations + + def extract_domain_configs(config: ConfigType, domain: str) -> Sequence[str]: """Extract keys from config for given domain name. @@ -1402,10 +1415,72 @@ def extract_domain_configs(config: ConfigType, domain: str) -> Sequence[str]: return domain_configs -async def async_process_component_config( # noqa: C901 +@dataclass(slots=True) +class _PlatformIntegration: + """Class to hold platform integration information.""" + + path: str # integration.platform; ex: filter.sensor + name: str # integration; ex: filter + integration: Integration # + config: ConfigType # un-validated config + validated_config: ConfigType # component validated config + + +async def _async_load_and_validate_platform_integration( + domain: str, + integration_docs: str | None, + config_exceptions: list[ConfigExceptionInfo], + p_integration: _PlatformIntegration, +) -> ConfigType | None: + """Load a platform integration and validate its config.""" + try: + platform = await p_integration.integration.async_get_platform(domain) + except LOAD_EXCEPTIONS as exc: + exc_info = ConfigExceptionInfo( + exc, + ConfigErrorTranslationKey.PLATFORM_COMPONENT_LOAD_EXC, + p_integration.path, + p_integration.config, + integration_docs, + ) + config_exceptions.append(exc_info) + return None + + # If the platform does not have a config schema + # the top level component validated schema will be used + if not hasattr(platform, "PLATFORM_SCHEMA"): + return p_integration.validated_config + + # Validate platform specific schema + try: + return platform.PLATFORM_SCHEMA(p_integration.config) # type: ignore[no-any-return] + except vol.Invalid as exc: + exc_info = ConfigExceptionInfo( + exc, + ConfigErrorTranslationKey.PLATFORM_CONFIG_VALIDATION_ERR, + p_integration.path, + p_integration.config, + p_integration.integration.documentation, + ) + config_exceptions.append(exc_info) + except Exception as exc: # pylint: disable=broad-except + exc_info = ConfigExceptionInfo( + exc, + ConfigErrorTranslationKey.PLATFORM_SCHEMA_VALIDATOR_ERR, + p_integration.name, + p_integration.config, + p_integration.integration.documentation, + ) + config_exceptions.append(exc_info) + + return None + + +async def async_process_component_config( hass: HomeAssistant, config: ConfigType, integration: Integration, + component: ComponentProtocol | None = None, ) -> IntegrationConfigInfo: """Check component configuration. @@ -1417,31 +1492,13 @@ async def async_process_component_config( # noqa: C901 integration_docs = integration.documentation config_exceptions: list[ConfigExceptionInfo] = [] - try: - component = integration.get_component() - except LOAD_EXCEPTIONS as exc: - exc_info = ConfigExceptionInfo( - exc, - ConfigErrorTranslationKey.COMPONENT_IMPORT_ERR, - domain, - config, - integration_docs, - ) - config_exceptions.append(exc_info) - return IntegrationConfigInfo(None, config_exceptions) - - # Check if the integration has a custom config validator - config_validator = None - try: - config_validator = integration.get_platform("config") - except ImportError as err: - # Filter out import error of the config platform. - # If the config platform contains bad imports, make sure - # that still fails. - if err.name != f"{integration.pkg_path}.config": + if not component: + try: + component = await integration.async_get_component() + except LOAD_EXCEPTIONS as exc: exc_info = ConfigExceptionInfo( - err, - ConfigErrorTranslationKey.CONFIG_PLATFORM_IMPORT_ERR, + exc, + ConfigErrorTranslationKey.COMPONENT_IMPORT_ERR, domain, config, integration_docs, @@ -1449,6 +1506,30 @@ async def async_process_component_config( # noqa: C901 config_exceptions.append(exc_info) return IntegrationConfigInfo(None, config_exceptions) + # Check if the integration has a custom config validator + config_validator = None + # A successful call to async_get_component will prime + # the cache for platforms_exists to ensure it does no + # blocking I/O + if integration.platforms_exists(("config",)): + # If the config platform cannot possibly exist, don't try to load it. + try: + config_validator = await integration.async_get_platform("config") + except ImportError as err: + # Filter out import error of the config platform. + # If the config platform contains bad imports, make sure + # that still fails. + if err.name != f"{integration.pkg_path}.config": + exc_info = ConfigExceptionInfo( + err, + ConfigErrorTranslationKey.CONFIG_PLATFORM_IMPORT_ERR, + domain, + config, + integration_docs, + ) + config_exceptions.append(exc_info) + return IntegrationConfigInfo(None, config_exceptions) + if config_validator is not None and hasattr( config_validator, "async_validate_config" ): @@ -1509,6 +1590,7 @@ async def async_process_component_config( # noqa: C901 if component_platform_schema is None: return IntegrationConfigInfo(config, []) + platform_integrations_to_load: list[_PlatformIntegration] = [] platforms: list[ConfigType] = [] for p_name, p_config in config_per_platform(config, domain): # Validate component specific platform schema @@ -1556,45 +1638,44 @@ async def async_process_component_config( # noqa: C901 config_exceptions.append(exc_info) continue - try: - platform = p_integration.get_platform(domain) - except LOAD_EXCEPTIONS as exc: - exc_info = ConfigExceptionInfo( - exc, - ConfigErrorTranslationKey.PLATFORM_COMPONENT_LOAD_EXC, - platform_path, - p_config, - integration_docs, + platform_integration = _PlatformIntegration( + platform_path, p_name, p_integration, p_config, p_validated + ) + platform_integrations_to_load.append(platform_integration) + + # + # Since bootstrap will order base platform (ie sensor) integrations + # first, we eagerly gather importing the platforms that need to be + # validated for the base platform since everything that uses the + # base platform has to wait for it to finish. + # + # For example if `hue` where to load first and than called + # `async_forward_entry_setup` for the `sensor` platform it would have to + # wait for the sensor platform to finish loading before it could continue. + # Since the base `sensor` platform must also import all of its platform + # integrations to do validation before it can finish setup, its important + # that the platform integrations are imported first so we do not waste + # time importing `hue` first when we could have been importing the platforms + # that the base `sensor` platform need to load to do validation and allow + # all integrations that need the base `sensor` platform to proceed with setup. + # + if platform_integrations_to_load: + async_load_and_validate = partial( + _async_load_and_validate_platform_integration, + domain, + integration_docs, + config_exceptions, + ) + platforms.extend( + validated_config + for validated_config in await asyncio.gather( + *( + create_eager_task(async_load_and_validate(p_integration)) + for p_integration in platform_integrations_to_load + ) ) - config_exceptions.append(exc_info) - continue - - # Validate platform specific schema - if hasattr(platform, "PLATFORM_SCHEMA"): - try: - p_validated = platform.PLATFORM_SCHEMA(p_config) - except vol.Invalid as exc: - exc_info = ConfigExceptionInfo( - exc, - ConfigErrorTranslationKey.PLATFORM_CONFIG_VALIDATION_ERR, - platform_path, - p_config, - p_integration.documentation, - ) - config_exceptions.append(exc_info) - continue - except Exception as exc: # pylint: disable=broad-except - exc_info = ConfigExceptionInfo( - exc, - ConfigErrorTranslationKey.PLATFORM_SCHEMA_VALIDATOR_ERR, - p_name, - p_config, - p_integration.documentation, - ) - config_exceptions.append(exc_info) - continue - - platforms.append(p_validated) + if validated_config is not None + ) # Create a copy of the configuration with all config for current # component removed and add validated config back in. diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 1ca40886da2..42194641f7f 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -1,4 +1,5 @@ """Manage config entries in Home Assistant.""" + from __future__ import annotations import asyncio @@ -46,7 +47,7 @@ from .exceptions import ( ) from .helpers import device_registry, entity_registry, issue_registry as ir, storage from .helpers.debounce import Debouncer -from .helpers.dispatcher import async_dispatcher_send +from .helpers.dispatcher import SignalType, async_dispatcher_send from .helpers.event import ( RANDOM_MICROSECOND_MAX, RANDOM_MICROSECOND_MIN, @@ -56,7 +57,14 @@ from .helpers.frame import report from .helpers.json import json_bytes, json_fragment from .helpers.typing import UNDEFINED, ConfigType, DiscoveryInfoType, UndefinedType from .loader import async_suggest_report_issue -from .setup import DATA_SETUP_DONE, async_process_deps_reqs, async_setup_component +from .setup import ( + DATA_SETUP_DONE, + SetupPhases, + async_pause_setup, + async_process_deps_reqs, + async_setup_component, + async_start_setup, +) from .util import uuid as uuid_util from .util.async_ import create_eager_task from .util.decorator import Registry @@ -87,6 +95,7 @@ SOURCE_IMPORT = "import" SOURCE_INTEGRATION_DISCOVERY = "integration_discovery" SOURCE_MQTT = "mqtt" SOURCE_SSDP = "ssdp" +SOURCE_SYSTEM = "system" SOURCE_USB = "usb" SOURCE_USER = "user" SOURCE_ZEROCONF = "zeroconf" @@ -104,6 +113,9 @@ SOURCE_UNIGNORE = "unignore" # This is used to signal that re-authentication is required by the user. SOURCE_REAUTH = "reauth" +# This is used to initiate a reconfigure flow by the user. +SOURCE_RECONFIGURE = "reconfigure" + HANDLERS: Registry[str, type[ConfigFlow]] = Registry() STORAGE_KEY = "core.config_entries" @@ -177,7 +189,9 @@ RECONFIGURE_NOTIFICATION_ID = "config_entry_reconfigure" EVENT_FLOW_DISCOVERED = "config_entry_discovered" -SIGNAL_CONFIG_ENTRY_CHANGED = "config_entry_changed" +SIGNAL_CONFIG_ENTRY_CHANGED = SignalType["ConfigEntryChange", "ConfigEntry"]( + "config_entry_changed" +) NO_RESET_TRIES_STATES = { ConfigEntryState.SETUP_RETRY, @@ -242,6 +256,13 @@ UPDATE_ENTRY_CONFIG_ENTRY_ATTRS = { } +class ConfigFlowResult(FlowResult, total=False): + """Typed result dict for config flow.""" + + minor_version: int + version: int + + class ConfigEntry: """Hold a configuration entry.""" @@ -340,6 +361,9 @@ class ConfigEntry: # Supports options self._supports_options: bool | None = None + # Supports reconfigure + self._supports_reconfigure: bool | None = None + # Listeners to call on update self.update_listeners: list[UpdateListenerType] = [] @@ -350,14 +374,16 @@ class ConfigEntry: self._async_cancel_retry_setup: Callable[[], Any] | None = None # Hold list for actions to call on unload. - self._on_unload: list[ - Callable[[], Coroutine[Any, Any, None] | None] - ] | None = None + self._on_unload: list[Callable[[], Coroutine[Any, Any, None] | None]] | None = ( + None + ) # Reload lock to prevent conflicting reloads self.reload_lock = asyncio.Lock() # Reauth lock to prevent concurrent reauth flows self._reauth_lock = asyncio.Lock() + # Reconfigure lock to prevent concurrent reconfigure flows + self._reconfigure_lock = asyncio.Lock() self._tasks: set[asyncio.Future[Any]] = set() self._background_tasks: set[asyncio.Future[Any]] = set() @@ -410,6 +436,20 @@ class ConfigEntry: ) return self._supports_options or False + @property + def supports_reconfigure(self) -> bool: + """Return if entry supports config options.""" + if self._supports_reconfigure is None and ( + handler := HANDLERS.get(self.domain) + ): + # work out if handler has support for reconfigure step + object.__setattr__( + self, + "_supports_reconfigure", + hasattr(handler, "async_step_reconfigure"), + ) + return self._supports_reconfigure or False + def clear_cache(self) -> None: """Clear cached properties.""" with contextlib.suppress(AttributeError): @@ -427,6 +467,7 @@ class ConfigEntry: "supports_options": self.supports_options, "supports_remove_device": self.supports_remove_device or False, "supports_unload": self.supports_unload or False, + "supports_reconfigure": self.supports_reconfigure or False, "pref_disable_new_entities": self.pref_disable_new_entities, "pref_disable_polling": self.pref_disable_polling, "disabled_by": self.disabled_by, @@ -459,9 +500,8 @@ class ConfigEntry: self.supports_remove_device = await support_remove_from_device( hass, self.domain ) - try: - component = integration.get_component() + component = await integration.async_get_component() except ImportError as err: _LOGGER.error( "Error importing integration %s to set up %s configuration entry: %s", @@ -498,10 +538,17 @@ class ConfigEntry: self._async_set_state(hass, ConfigEntryState.MIGRATION_ERROR, None) return + setup_phase = SetupPhases.CONFIG_ENTRY_SETUP + else: + setup_phase = SetupPhases.CONFIG_ENTRY_PLATFORM_SETUP + error_reason = None try: - result = await component.async_setup_entry(hass, self) + with async_start_setup( + hass, integration=self.domain, group=self.entry_id, phase=setup_phase + ): + result = await component.async_setup_entry(hass, self) if not isinstance(result, bool): _LOGGER.error( # type: ignore[unreachable] @@ -560,6 +607,7 @@ class ConfigEntry: HassJob( functools.partial(self._async_setup_again, hass), job_type=HassJobType.Callback, + cancel_on_shutdown=True, ), ) else: @@ -609,7 +657,11 @@ class ConfigEntry: # Check again when we fire in case shutdown # has started so we do not block shutdown if not hass.is_stopping: - hass.async_create_task(self.async_setup(hass), eager_start=True) + hass.async_create_task( + self.async_setup(hass), + f"config entry retry {self.domain} {self.title}", + eager_start=True, + ) @callback def async_shutdown(self) -> None: @@ -648,7 +700,7 @@ class ConfigEntry: self._async_set_state(hass, ConfigEntryState.NOT_LOADED, None) return True - component = integration.get_component() + component = await integration.async_get_component() if integration.domain == self.domain: if not self.state.recoverable: @@ -705,7 +757,7 @@ class ConfigEntry: # entry. return - component = integration.get_component() + component = await integration.async_get_component() if not hasattr(component, "async_remove_entry"): return try: @@ -754,7 +806,7 @@ class ConfigEntry: if not (integration := self._integration_for_domain): integration = await loader.async_get_integration(hass, self.domain) - component = integration.get_component() + component = await integration.async_get_component() supports_migrate = hasattr(component, "async_migrate_entry") if not supports_migrate: if same_major_version: @@ -822,7 +874,7 @@ class ConfigEntry: if self._on_unload is not None: while self._on_unload: if job := self._on_unload.pop()(): - self.async_create_task(hass, job) + self.async_create_task(hass, job, eager_start=True) if not self._tasks and not self._background_tasks: return @@ -853,8 +905,8 @@ class ConfigEntry: """Start a reauth flow.""" # We will check this again in the task when we hold the lock, # but we also check it now to try to avoid creating the task. - if any(self.async_get_active_flows(hass, {SOURCE_REAUTH})): - # Reauth flow already in progress for this entry + if any(self.async_get_active_flows(hass, {SOURCE_RECONFIGURE, SOURCE_REAUTH})): + # Reauth or Reconfigure flow already in progress for this entry return hass.async_create_task( self._async_init_reauth(hass, context, data), @@ -869,8 +921,10 @@ class ConfigEntry: ) -> None: """Start a reauth flow.""" async with self._reauth_lock: - if any(self.async_get_active_flows(hass, {SOURCE_REAUTH})): - # Reauth flow already in progress for this entry + if any( + self.async_get_active_flows(hass, {SOURCE_RECONFIGURE, SOURCE_REAUTH}) + ): + # Reauth or Reconfigure flow already in progress for this entry return result = await hass.config_entries.flow.async_init( self.domain, @@ -900,10 +954,53 @@ class ConfigEntry: translation_placeholders={"name": self.title}, ) + @callback + def async_start_reconfigure( + self, + hass: HomeAssistant, + context: dict[str, Any] | None = None, + data: dict[str, Any] | None = None, + ) -> None: + """Start a reconfigure flow.""" + # We will check this again in the task when we hold the lock, + # but we also check it now to try to avoid creating the task. + if any(self.async_get_active_flows(hass, {SOURCE_RECONFIGURE, SOURCE_REAUTH})): + # Reconfigure or reauth flow already in progress for this entry + return + hass.async_create_task( + self._async_init_reconfigure(hass, context, data), + f"config entry reconfigure {self.title} {self.domain} {self.entry_id}", + ) + + async def _async_init_reconfigure( + self, + hass: HomeAssistant, + context: dict[str, Any] | None = None, + data: dict[str, Any] | None = None, + ) -> None: + """Start a reconfigure flow.""" + async with self._reconfigure_lock: + if any( + self.async_get_active_flows(hass, {SOURCE_RECONFIGURE, SOURCE_REAUTH}) + ): + # Reconfigure or reauth flow already in progress for this entry + return + await hass.config_entries.flow.async_init( + self.domain, + context={ + "source": SOURCE_RECONFIGURE, + "entry_id": self.entry_id, + "title_placeholders": {"name": self.title}, + "unique_id": self.unique_id, + } + | (context or {}), + data=self.data | (data or {}), + ) + @callback def async_get_active_flows( self, hass: HomeAssistant, sources: set[str] - ) -> Generator[FlowResult, None, None]: + ) -> Generator[ConfigFlowResult, None, None]: """Get any active flows of certain sources for this entry.""" return ( flow @@ -951,7 +1048,13 @@ class ConfigEntry: Background tasks are automatically canceled when config entry is unloaded. - target: target to call. + A background task is different from a normal task: + + - Will not block startup + - Will be automatically cancelled on shutdown + - Calls to async_block_till_done will not wait for completion + + This method must be run in the event loop. """ task = hass.async_create_background_task(target, name, eager_start) if task.done(): @@ -970,9 +1073,11 @@ class FlowCancelledError(Exception): """Error to indicate that a flow has been cancelled.""" -class ConfigEntriesFlowManager(data_entry_flow.FlowManager): +class ConfigEntriesFlowManager(data_entry_flow.FlowManager[ConfigFlowResult, str]): """Manage all the config entry flows that are in progress.""" + _flow_result = ConfigFlowResult + def __init__( self, hass: HomeAssistant, @@ -1010,7 +1115,7 @@ class ConfigEntriesFlowManager(data_entry_flow.FlowManager): async def async_init( self, handler: str, *, context: dict[str, Any] | None = None, data: Any = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Start a configuration flow.""" if not context or "source" not in context: raise KeyError("Context not set or doesn't have a source set") @@ -1024,7 +1129,7 @@ class ConfigEntriesFlowManager(data_entry_flow.FlowManager): and await _support_single_config_entry_only(self.hass, handler) and self.config_entries.async_entries(handler, include_ignore=False) ): - return FlowResult( + return ConfigFlowResult( type=data_entry_flow.FlowResultType.ABORT, flow_id=flow_id, handler=handler, @@ -1035,9 +1140,9 @@ class ConfigEntriesFlowManager(data_entry_flow.FlowManager): loop = self.hass.loop if context["source"] == SOURCE_IMPORT: - self._pending_import_flows.setdefault(handler, {})[ - flow_id - ] = loop.create_future() + self._pending_import_flows.setdefault(handler, {})[flow_id] = ( + loop.create_future() + ) cancel_init_future = loop.create_future() self._initialize_futures.setdefault(handler, []).append(cancel_init_future) @@ -1065,7 +1170,7 @@ class ConfigEntriesFlowManager(data_entry_flow.FlowManager): handler: str, context: dict, data: Any, - ) -> tuple[data_entry_flow.FlowHandler, FlowResult]: + ) -> tuple[ConfigFlow, ConfigFlowResult]: """Run the init in a task to allow it to be canceled at shutdown.""" flow = await self.async_create_flow(handler, context=context, data=data) if not flow: @@ -1093,8 +1198,10 @@ class ConfigEntriesFlowManager(data_entry_flow.FlowManager): self._discovery_debouncer.async_shutdown() async def async_finish_flow( - self, flow: data_entry_flow.FlowHandler, result: data_entry_flow.FlowResult - ) -> data_entry_flow.FlowResult: + self, + flow: data_entry_flow.FlowHandler[ConfigFlowResult, str], + result: ConfigFlowResult, + ) -> ConfigFlowResult: """Finish a config flow and add an entry.""" flow = cast(ConfigFlow, flow) @@ -1128,7 +1235,7 @@ class ConfigEntriesFlowManager(data_entry_flow.FlowManager): and flow.context["source"] != SOURCE_IGNORE and self.config_entries.async_entries(flow.handler, include_ignore=False) ): - return FlowResult( + return ConfigFlowResult( type=data_entry_flow.FlowResultType.ABORT, flow_id=flow.flow_id, handler=flow.handler, @@ -1213,7 +1320,9 @@ class ConfigEntriesFlowManager(data_entry_flow.FlowManager): return flow async def async_post_init( - self, flow: data_entry_flow.FlowHandler, result: data_entry_flow.FlowResult + self, + flow: data_entry_flow.FlowHandler[ConfigFlowResult, str], + result: ConfigFlowResult, ) -> None: """After a flow is initialised trigger new flow notifications.""" source = flow.context["source"] @@ -1386,6 +1495,11 @@ class ConfigEntries: """Return entry with matching entry_id.""" return self._entries.data.get(entry_id) + @callback + def async_entry_ids(self) -> list[str]: + """Return entry ids.""" + return list(self._entries.data) + @callback def async_entries( self, @@ -1494,7 +1608,9 @@ class ConfigEntries: old_conf_migrate_func=_old_conf_migrator, ) - self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self._async_shutdown) + self.hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, self._async_shutdown, run_immediately=True + ) if config is None: self._entries = ConfigEntryItems(self.hass) @@ -1738,10 +1854,14 @@ class ConfigEntries: self, entry: ConfigEntry, platforms: Iterable[Platform | str] ) -> None: """Forward the setup of an entry to platforms.""" + integration = await loader.async_get_integration(self.hass, entry.domain) + if not integration.platforms_are_loaded(platforms): + with async_pause_setup(self.hass, SetupPhases.WAIT_IMPORT_PLATFORMS): + await integration.async_get_platforms(platforms) await asyncio.gather( *( create_eager_task( - self.async_forward_entry_setup(entry, platform), + self._async_forward_entry_setup(entry, platform, False), name=f"config entry forward setup {entry.title} {entry.domain} {entry.entry_id} {platform}", ) for platform in platforms @@ -1757,15 +1877,32 @@ class ConfigEntries: component also has related platforms, the component will have to forward the entry to be setup by that component. """ + return await self._async_forward_entry_setup(entry, domain, True) + + async def _async_forward_entry_setup( + self, entry: ConfigEntry, domain: Platform | str, preload_platform: bool + ) -> bool: + """Forward the setup of an entry to a different component.""" # Setup Component if not set up yet if domain not in self.hass.config.components: - result = await async_setup_component(self.hass, domain, self._hass_config) + with async_pause_setup(self.hass, SetupPhases.WAIT_BASE_PLATFORM_SETUP): + result = await async_setup_component( + self.hass, domain, self._hass_config + ) if not result: return False - integration = await loader.async_get_integration(self.hass, domain) + if preload_platform: + # If this is a late setup, we need to make sure the platform is loaded + # so we do not end up waiting for when the EntityComponent calls + # async_prepare_setup_platform + integration = await loader.async_get_integration(self.hass, entry.domain) + if not integration.platforms_are_loaded((domain,)): + with async_pause_setup(self.hass, SetupPhases.WAIT_IMPORT_PLATFORMS): + await integration.async_get_platform(domain) + integration = await loader.async_get_integration(self.hass, domain) await entry.async_setup(self.hass, integration=integration) return True @@ -1852,7 +1989,13 @@ def _async_abort_entries_match( raise data_entry_flow.AbortFlow("already_configured") -class ConfigFlow(data_entry_flow.FlowHandler): +class ConfigEntryBaseFlow(data_entry_flow.FlowHandler[ConfigFlowResult, str]): + """Base class for config and option flows.""" + + _flow_result = ConfigFlowResult + + +class ConfigFlow(ConfigEntryBaseFlow): """Base class for config flows with some helpers.""" def __init_subclass__(cls, *, domain: str | None = None, **kwargs: Any) -> None: @@ -2008,7 +2151,7 @@ class ConfigFlow(data_entry_flow.FlowHandler): self, include_uninitialized: bool = False, match_context: dict[str, Any] | None = None, - ) -> list[data_entry_flow.FlowResult]: + ) -> list[ConfigFlowResult]: """Return other in progress flows for current domain.""" return [ flw @@ -2020,22 +2163,18 @@ class ConfigFlow(data_entry_flow.FlowHandler): if flw["flow_id"] != self.flow_id ] - async def async_step_ignore( - self, user_input: dict[str, Any] - ) -> data_entry_flow.FlowResult: + async def async_step_ignore(self, user_input: dict[str, Any]) -> ConfigFlowResult: """Ignore this config flow.""" await self.async_set_unique_id(user_input["unique_id"], raise_on_progress=False) return self.async_create_entry(title=user_input["title"], data={}) - async def async_step_unignore( - self, user_input: dict[str, Any] - ) -> data_entry_flow.FlowResult: + async def async_step_unignore(self, user_input: dict[str, Any]) -> ConfigFlowResult: """Rediscover a config entry by it's unique_id.""" return self.async_abort(reason="not_implemented") async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> data_entry_flow.FlowResult: + ) -> ConfigFlowResult: """Handle a flow initiated by the user.""" return self.async_abort(reason="not_implemented") @@ -2068,14 +2207,14 @@ class ConfigFlow(data_entry_flow.FlowHandler): async def _async_step_discovery_without_unique_id( self, - ) -> data_entry_flow.FlowResult: + ) -> ConfigFlowResult: """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: + ) -> ConfigFlowResult: """Handle a flow initialized by discovery.""" return await self._async_step_discovery_without_unique_id() @@ -2085,7 +2224,7 @@ class ConfigFlow(data_entry_flow.FlowHandler): *, reason: str, description_placeholders: Mapping[str, str] | None = None, - ) -> data_entry_flow.FlowResult: + ) -> ConfigFlowResult: """Abort the config flow.""" # Remove reauth notification if no reauth flows are in progress if self.source == SOURCE_REAUTH and not any( @@ -2104,55 +2243,53 @@ class ConfigFlow(data_entry_flow.FlowHandler): async def async_step_bluetooth( self, discovery_info: BluetoothServiceInfoBleak - ) -> data_entry_flow.FlowResult: + ) -> ConfigFlowResult: """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: + ) -> ConfigFlowResult: """Handle a flow initialized by DHCP discovery.""" return await self._async_step_discovery_without_unique_id() async def async_step_hassio( self, discovery_info: HassioServiceInfo - ) -> data_entry_flow.FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by HASS IO discovery.""" return await self._async_step_discovery_without_unique_id() async def async_step_integration_discovery( self, discovery_info: DiscoveryInfoType - ) -> data_entry_flow.FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by integration specific discovery.""" return await self._async_step_discovery_without_unique_id() async def async_step_homekit( self, discovery_info: ZeroconfServiceInfo - ) -> data_entry_flow.FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by Homekit discovery.""" return await self._async_step_discovery_without_unique_id() async def async_step_mqtt( self, discovery_info: MqttServiceInfo - ) -> data_entry_flow.FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by MQTT discovery.""" return await self._async_step_discovery_without_unique_id() async def async_step_ssdp( self, discovery_info: SsdpServiceInfo - ) -> data_entry_flow.FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by SSDP discovery.""" return await self._async_step_discovery_without_unique_id() - async def async_step_usb( - self, discovery_info: UsbServiceInfo - ) -> data_entry_flow.FlowResult: + async def async_step_usb(self, discovery_info: UsbServiceInfo) -> ConfigFlowResult: """Handle a flow initialized by USB discovery.""" return await self._async_step_discovery_without_unique_id() async def async_step_zeroconf( self, discovery_info: ZeroconfServiceInfo - ) -> data_entry_flow.FlowResult: + ) -> ConfigFlowResult: """Handle a flow initialized by Zeroconf discovery.""" return await self._async_step_discovery_without_unique_id() @@ -2165,7 +2302,7 @@ class ConfigFlow(data_entry_flow.FlowHandler): description: str | None = None, description_placeholders: Mapping[str, str] | None = None, options: Mapping[str, Any] | None = None, - ) -> data_entry_flow.FlowResult: + ) -> ConfigFlowResult: """Finish config flow and create a config entry.""" result = super().async_create_entry( title=title, @@ -2175,6 +2312,8 @@ class ConfigFlow(data_entry_flow.FlowHandler): ) result["options"] = options or {} + result["minor_version"] = self.MINOR_VERSION + result["version"] = self.VERSION return result @@ -2188,7 +2327,7 @@ class ConfigFlow(data_entry_flow.FlowHandler): data: Mapping[str, Any] | UndefinedType = UNDEFINED, options: Mapping[str, Any] | UndefinedType = UNDEFINED, reason: str = "reauth_successful", - ) -> data_entry_flow.FlowResult: + ) -> ConfigFlowResult: """Update config entry, reload config entry and finish config flow.""" result = self.hass.config_entries.async_update_entry( entry=entry, @@ -2202,9 +2341,11 @@ class ConfigFlow(data_entry_flow.FlowHandler): return self.async_abort(reason=reason) -class OptionsFlowManager(data_entry_flow.FlowManager): +class OptionsFlowManager(data_entry_flow.FlowManager[ConfigFlowResult, str]): """Flow to set options for a configuration entry.""" + _flow_result = ConfigFlowResult + def _async_get_config_entry(self, config_entry_id: str) -> ConfigEntry: """Return config entry or raise if not found.""" entry = self.hass.config_entries.async_get_entry(config_entry_id) @@ -2229,8 +2370,10 @@ class OptionsFlowManager(data_entry_flow.FlowManager): return handler.async_get_options_flow(entry) async def async_finish_flow( - self, flow: data_entry_flow.FlowHandler, result: data_entry_flow.FlowResult - ) -> data_entry_flow.FlowResult: + self, + flow: data_entry_flow.FlowHandler[ConfigFlowResult, str], + result: ConfigFlowResult, + ) -> ConfigFlowResult: """Finish an options flow and update options for configuration entry. Flow.handler and entry_id is the same thing to map flow with entry. @@ -2249,7 +2392,9 @@ class OptionsFlowManager(data_entry_flow.FlowManager): result["result"] = True return result - async def _async_setup_preview(self, flow: data_entry_flow.FlowHandler) -> None: + async def _async_setup_preview( + self, flow: data_entry_flow.FlowHandler[ConfigFlowResult, str] + ) -> None: """Set up preview for an option flow handler.""" entry = self._async_get_config_entry(flow.handler) await _load_integration(self.hass, entry.domain, {}) @@ -2258,7 +2403,7 @@ class OptionsFlowManager(data_entry_flow.FlowManager): await flow.async_setup_preview(self.hass) -class OptionsFlow(data_entry_flow.FlowHandler): +class OptionsFlow(ConfigEntryBaseFlow): """Base class for config options flows.""" handler: str @@ -2391,16 +2536,16 @@ class EntityRegistryDisabledHandler: @callback -def _handle_entry_updated_filter(event: Event) -> bool: +def _handle_entry_updated_filter(event_data: Mapping[str, Any]) -> bool: """Handle entity registry entry update filter. Only handle changes to "disabled_by". If "disabled_by" was CONFIG_ENTRY, reload is not needed. """ if ( - event.data["action"] != "update" - or "disabled_by" not in event.data["changes"] - or event.data["changes"]["disabled_by"] + event_data["action"] != "update" + or "disabled_by" not in event_data["changes"] + or event_data["changes"]["disabled_by"] is entity_registry.RegistryEntryDisabler.CONFIG_ENTRY ): return False @@ -2410,14 +2555,14 @@ def _handle_entry_updated_filter(event: Event) -> bool: async def support_entry_unload(hass: HomeAssistant, domain: str) -> bool: """Test if a domain supports entry unloading.""" integration = await loader.async_get_integration(hass, domain) - component = integration.get_component() + component = await integration.async_get_component() return hasattr(component, "async_unload_entry") async def support_remove_from_device(hass: HomeAssistant, domain: str) -> bool: """Test if a domain supports being removed from a device.""" integration = await loader.async_get_integration(hass, domain) - component = integration.get_component() + component = await integration.async_get_component() return hasattr(component, "async_remove_config_entry_device") diff --git a/homeassistant/const.py b/homeassistant/const.py index fc39bccf854..6e08c49f970 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,4 +1,5 @@ """Constants used by Home Assistant components.""" + from __future__ import annotations from enum import StrEnum @@ -12,17 +13,18 @@ from .helpers.deprecation import ( check_if_deprecated_constant, dir_with_deprecated_constants, ) +from .util.signal_type import SignalType APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 -MINOR_VERSION: Final = 3 -PATCH_VERSION: Final = "3" +MINOR_VERSION: Final = 4 +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, 11, 0) +REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) REQUIRED_NEXT_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) # Truthy date string triggers showing related deprecation warning messages. -REQUIRED_NEXT_PYTHON_HA_RELEASE: Final = "2024.4" +REQUIRED_NEXT_PYTHON_HA_RELEASE: Final = "" # Format for platform files PLATFORM_FORMAT: Final = "{platform}.{domain}" @@ -305,6 +307,7 @@ EVENT_LOGGING_CHANGED: Final = "logging_changed" EVENT_SERVICE_REGISTERED: Final = "service_registered" EVENT_SERVICE_REMOVED: Final = "service_removed" EVENT_STATE_CHANGED: Final = "state_changed" +EVENT_STATE_REPORTED: Final = "state_reported" EVENT_THEMES_UPDATED: Final = "themes_updated" EVENT_PANELS_UPDATED: Final = "panels_updated" EVENT_LOVELACE_UPDATED: Final = "lovelace_updated" @@ -509,6 +512,12 @@ ATTR_AREA_ID: Final = "area_id" # Contains one string, the device ID ATTR_DEVICE_ID: Final = "device_id" +# Contains one string or a list of strings, each being an floor id +ATTR_FLOOR_ID: Final = "floor_id" + +# Contains one string or a list of strings, each being an label id +ATTR_LABEL_ID: Final = "label_id" + # String with a friendly name for the entity ATTR_FRIENDLY_NAME: Final = "friendly_name" @@ -1214,6 +1223,7 @@ CONCENTRATION_PARTS_PER_BILLION: Final = "ppb" class UnitOfSpeed(StrEnum): """Speed units.""" + BEAUFORT = "Beaufort" FEET_PER_SECOND = "ft/s" METERS_PER_SECOND = "m/s" KILOMETERS_PER_HOUR = "km/h" @@ -1451,11 +1461,11 @@ _DEPRECATED_DATA_RATE_GIBIBYTES_PER_SECOND: Final = DeprecatedConstantEnum( # States -COMPRESSED_STATE_STATE = "s" -COMPRESSED_STATE_ATTRIBUTES = "a" -COMPRESSED_STATE_CONTEXT = "c" -COMPRESSED_STATE_LAST_CHANGED = "lc" -COMPRESSED_STATE_LAST_UPDATED = "lu" +COMPRESSED_STATE_STATE: Final = "s" +COMPRESSED_STATE_ATTRIBUTES: Final = "a" +COMPRESSED_STATE_CONTEXT: Final = "c" +COMPRESSED_STATE_LAST_CHANGED: Final = "lc" +COMPRESSED_STATE_LAST_UPDATED: Final = "lu" # #### SERVICES #### SERVICE_TURN_ON: Final = "turn_on" @@ -1600,7 +1610,9 @@ CAST_APP_ID_HOMEASSISTANT_LOVELACE: Final = "A078F6B0" # User used by Supervisor HASSIO_USER_NAME = "Supervisor" -SIGNAL_BOOTSTRAP_INTEGRATIONS = "bootstrap_integrations" +SIGNAL_BOOTSTRAP_INTEGRATIONS: SignalType[dict[str, float]] = SignalType( + "bootstrap_integrations" +) # hass.data key for logging information. diff --git a/homeassistant/core.py b/homeassistant/core.py index 0f038149d63..4794b284fd2 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -3,6 +3,7 @@ Home Assistant is a Home Automation framework for observing the state of entities and react to changes. """ + from __future__ import annotations import asyncio @@ -35,14 +36,17 @@ from typing import ( Any, Generic, Literal, + NotRequired, ParamSpec, Self, - TypeVar, + TypedDict, + TypeVarTuple, cast, overload, ) from urllib.parse import urlparse +from typing_extensions import TypeVar import voluptuous as vol import yarl @@ -64,9 +68,11 @@ from .const import ( EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP, + EVENT_LOGGING_CHANGED, EVENT_SERVICE_REGISTERED, EVENT_SERVICE_REMOVED, EVENT_STATE_CHANGED, + EVENT_STATE_REPORTED, MATCH_ALL, MAX_LENGTH_EVENT_EVENT_TYPE, MAX_LENGTH_STATE_STATE, @@ -130,9 +136,11 @@ _T = TypeVar("_T") _R = TypeVar("_R") _R_co = TypeVar("_R_co", covariant=True) _P = ParamSpec("_P") +_Ts = TypeVarTuple("_Ts") # Internal; not helpers.typing.UNDEFINED due to circular dependency _UNDEF: dict[Any, Any] = {} _CallableT = TypeVar("_CallableT", bound=Callable[..., Any]) +_DataT = TypeVar("_DataT", bound=Mapping[str, Any], default=Mapping[str, Any]) CALLBACK_TYPE = Callable[[], None] CORE_STORAGE_KEY = "core.config" @@ -170,6 +178,11 @@ TIMEOUT_EVENT_START = 15 MAX_EXPECTED_ENTITY_IDS = 16384 +EVENTS_EXCLUDED_FROM_MATCH_ALL = { + EVENT_HOMEASSISTANT_CLOSE, + EVENT_STATE_REPORTED, +} + _LOGGER = logging.getLogger(__name__) @@ -289,8 +302,6 @@ class HassJob(Generic[_P, _R_co]): we run the job. """ - __slots__ = ("job_type", "target", "name", "_cancel_on_shutdown") - def __init__( self, target: Callable[_P, _R_co], @@ -302,8 +313,13 @@ class HassJob(Generic[_P, _R_co]): """Create a job object.""" self.target = target self.name = name - self.job_type = job_type or _get_hassjob_callable_job_type(target) self._cancel_on_shutdown = cancel_on_shutdown + self._job_type = job_type + + @cached_property + def job_type(self) -> HassJobType: + """Return the job type.""" + return self._job_type or get_hassjob_callable_job_type(self.target) @property def cancel_on_shutdown(self) -> bool | None: @@ -323,7 +339,7 @@ class HassJobWithArgs: args: Iterable[Any] -def _get_hassjob_callable_job_type(target: Callable[..., Any]) -> HassJobType: +def get_hassjob_callable_job_type(target: Callable[..., Any]) -> HassJobType: """Determine the job type from the callable.""" # Check for partials to properly determine if coroutine function check_target = target @@ -376,6 +392,8 @@ class HomeAssistant: # pylint: disable-next=import-outside-toplevel from . import loader + # This is a dictionary that any component can store any data on. + self.data: dict[str, Any] = {} self.loop = asyncio.get_running_loop() self._tasks: set[asyncio.Future[Any]] = set() self._background_tasks: set[asyncio.Future[Any]] = set() @@ -385,8 +403,6 @@ class HomeAssistant: self.config = Config(self, config_dir) self.components = loader.Components(self) self.helpers = loader.Helpers(self) - # This is a dictionary that any component can store any data on. - self.data: dict[str, Any] = {} self.state: CoreState = CoreState.not_running self.exit_code: int = 0 # If not None, use to signal end-of-loop @@ -399,6 +415,18 @@ class HomeAssistant: max_workers=1, thread_name_prefix="ImportExecutor" ) + @property + def _active_tasks(self) -> set[asyncio.Future[Any]]: + """Return all active tasks. + + This property is used in bootstrap to log all active tasks + so we can identify what is blocking startup. + + This property is marked as private to avoid accidental use + as it is not guaranteed to be present in future versions. + """ + return self._tasks + @cached_property def is_running(self) -> bool: """Return if Home Assistant is running.""" @@ -482,8 +510,10 @@ class HomeAssistant: " phase. We're going to continue anyway. Please report the" " following info at" " https://github.com/home-assistant/core/issues: %s" + " The system is waiting for tasks: %s" ), ", ".join(self.config.components), + self._tasks, ) # Allow automations to set up the start triggers before changing state @@ -501,7 +531,7 @@ class HomeAssistant: self.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) def add_job( - self, target: Callable[..., Any] | Coroutine[Any, Any, Any], *args: Any + self, target: Callable[[*_Ts], Any] | Coroutine[Any, Any, Any], *args: *_Ts ) -> None: """Add a job to be executed by the event loop or by an executor. @@ -513,34 +543,53 @@ class HomeAssistant: """ if target is None: raise ValueError("Don't call add_job with None") - self.loop.call_soon_threadsafe(self.async_add_job, target, *args) + if asyncio.iscoroutine(target): + self.loop.call_soon_threadsafe( + functools.partial(self.async_create_task, target, eager_start=True) + ) + return + if TYPE_CHECKING: + target = cast(Callable[[*_Ts], Any], target) + self.loop.call_soon_threadsafe( + functools.partial( + self.async_add_hass_job, HassJob(target), *args, eager_start=True + ) + ) @overload @callback def async_add_job( - self, target: Callable[..., Coroutine[Any, Any, _R]], *args: Any - ) -> asyncio.Future[_R] | None: - ... + self, + target: Callable[[*_Ts], Coroutine[Any, Any, _R]], + *args: *_Ts, + eager_start: bool = False, + ) -> asyncio.Future[_R] | None: ... @overload @callback def async_add_job( - self, target: Callable[..., Coroutine[Any, Any, _R] | _R], *args: Any - ) -> asyncio.Future[_R] | None: - ... + self, + target: Callable[[*_Ts], Coroutine[Any, Any, _R] | _R], + *args: *_Ts, + eager_start: bool = False, + ) -> asyncio.Future[_R] | None: ... @overload @callback def async_add_job( - self, target: Coroutine[Any, Any, _R], *args: Any - ) -> asyncio.Future[_R] | None: - ... + self, + target: Coroutine[Any, Any, _R], + *args: Any, + eager_start: bool = False, + ) -> asyncio.Future[_R] | None: ... @callback def async_add_job( self, - target: Callable[..., Coroutine[Any, Any, _R] | _R] | Coroutine[Any, Any, _R], - *args: Any, + target: Callable[[*_Ts], Coroutine[Any, Any, _R] | _R] + | Coroutine[Any, Any, _R], + *args: *_Ts, + eager_start: bool = False, ) -> asyncio.Future[_R] | None: """Add a job to be executed by the event loop or by an executor. @@ -552,40 +601,64 @@ class HomeAssistant: target: target to call. args: parameters for method to call. """ + # late import to avoid circular imports + from .helpers import frame # pylint: disable=import-outside-toplevel + + frame.report( + "calls `async_add_job`, which is deprecated and will be removed in Home " + "Assistant 2025.4; Please review " + "https://developers.home-assistant.io/blog/2024/03/13/deprecate_add_run_job" + " for replacement options", + error_if_core=False, + ) + if target is None: raise ValueError("Don't call async_add_job with None") if asyncio.iscoroutine(target): - return self.async_create_task(target) + return self.async_create_task(target, eager_start=eager_start) # This code path is performance sensitive and uses # if TYPE_CHECKING to avoid the overhead of constructing # the type used for the cast. For history see: # https://github.com/home-assistant/core/pull/71960 if TYPE_CHECKING: - target = cast(Callable[..., Coroutine[Any, Any, _R] | _R], target) - return self.async_add_hass_job(HassJob(target), *args) + target = cast(Callable[[*_Ts], Coroutine[Any, Any, _R] | _R], target) + return self.async_add_hass_job(HassJob(target), *args, eager_start=eager_start) @overload @callback def async_add_hass_job( - self, hassjob: HassJob[..., Coroutine[Any, Any, _R]], *args: Any - ) -> asyncio.Future[_R] | None: - ... + self, + hassjob: HassJob[..., Coroutine[Any, Any, _R]], + *args: Any, + eager_start: bool = False, + background: bool = False, + ) -> asyncio.Future[_R] | None: ... @overload @callback def async_add_hass_job( - self, hassjob: HassJob[..., Coroutine[Any, Any, _R] | _R], *args: Any - ) -> asyncio.Future[_R] | None: - ... + self, + hassjob: HassJob[..., Coroutine[Any, Any, _R] | _R], + *args: Any, + eager_start: bool = False, + background: bool = False, + ) -> asyncio.Future[_R] | None: ... @callback def async_add_hass_job( - self, hassjob: HassJob[..., Coroutine[Any, Any, _R] | _R], *args: Any + self, + hassjob: HassJob[..., Coroutine[Any, Any, _R] | _R], + *args: Any, + eager_start: bool = False, + background: bool = False, ) -> asyncio.Future[_R] | None: """Add a HassJob from within the event loop. + If eager_start is True, coroutine functions will be scheduled eagerly. + If background is True, the task will created as a background task. + This method must be run in the event loop. hassjob: HassJob to call. args: parameters for method to call. @@ -600,7 +673,16 @@ class HomeAssistant: hassjob.target = cast( Callable[..., Coroutine[Any, Any, _R]], hassjob.target ) - task = self.loop.create_task(hassjob.target(*args), name=hassjob.name) + # Use loop.create_task + # to avoid the extra function call in asyncio.create_task. + if eager_start: + task = create_eager_task( + hassjob.target(*args), name=hassjob.name, loop=self.loop + ) + if task.done(): + return task + else: + task = self.loop.create_task(hassjob.target(*args), name=hassjob.name) elif hassjob.job_type is HassJobType.Callback: if TYPE_CHECKING: hassjob.target = cast(Callable[..., _R], hassjob.target) @@ -611,8 +693,9 @@ class HomeAssistant: hassjob.target = cast(Callable[..., _R], hassjob.target) task = self.loop.run_in_executor(None, hassjob.target, *args) - self._tasks.add(task) - task.add_done_callback(self._tasks.remove) + task_bucket = self._background_tasks if background else self._tasks + task_bucket.add(task) + task.add_done_callback(task_bucket.remove) return task @@ -623,7 +706,9 @@ class HomeAssistant: target: target to call. """ - self.loop.call_soon_threadsafe(self.async_create_task, target, name) + self.loop.call_soon_threadsafe( + functools.partial(self.async_create_task, target, name, eager_start=True) + ) @callback def async_create_task( @@ -644,6 +729,8 @@ class HomeAssistant: if task.done(): return task else: + # Use loop.create_task + # to avoid the extra function call in asyncio.create_task. task = self.loop.create_task(target, name=name) self._tasks.add(task) task.add_done_callback(self._tasks.remove) @@ -655,9 +742,17 @@ class HomeAssistant: ) -> asyncio.Task[_R]: """Create a task from within the event loop. - This is a background task which will not block startup and will be - automatically cancelled on shutdown. If you are using this in your - integration, use the create task methods on the config entry instead. + This type of task is for background tasks that usually run for + the lifetime of Home Assistant or an integration's setup. + + A background task is different from a normal task: + + - Will not block startup + - Will be automatically cancelled on shutdown + - Calls to async_block_till_done will not wait for completion + + If you are using this in your integration, use the create task + methods on the config entry instead. This method must be run in the event loop. """ @@ -666,6 +761,8 @@ class HomeAssistant: if task.done(): return task else: + # Use loop.create_task + # to avoid the extra function call in asyncio.create_task. task = self.loop.create_task(target, name=name) self._background_tasks.add(task) task.add_done_callback(self._background_tasks.remove) @@ -673,47 +770,59 @@ class HomeAssistant: @callback def async_add_executor_job( - self, target: Callable[..., _T], *args: Any + self, target: Callable[[*_Ts], _T], *args: *_Ts ) -> asyncio.Future[_T]: """Add an executor job from within the event loop.""" task = self.loop.run_in_executor(None, target, *args) - self._tasks.add(task) - task.add_done_callback(self._tasks.remove) + + tracked = asyncio.current_task() in self._tasks + task_bucket = self._tasks if tracked else self._background_tasks + task_bucket.add(task) + task.add_done_callback(task_bucket.remove) return task @callback def async_add_import_executor_job( - self, target: Callable[..., _T], *args: Any + self, target: Callable[[*_Ts], _T], *args: *_Ts ) -> asyncio.Future[_T]: - """Add an import executor job from within the event loop.""" - task = self.loop.run_in_executor(self.import_executor, target, *args) - self._tasks.add(task) - task.add_done_callback(self._tasks.remove) - return task + """Add an import executor job from within the event loop. + + The future returned from this method must be awaited in the event loop. + """ + return self.loop.run_in_executor(self.import_executor, target, *args) @overload @callback def async_run_hass_job( - self, hassjob: HassJob[..., Coroutine[Any, Any, _R]], *args: Any - ) -> asyncio.Future[_R] | None: - ... + self, + hassjob: HassJob[..., Coroutine[Any, Any, _R]], + *args: Any, + background: bool = False, + ) -> asyncio.Future[_R] | None: ... @overload @callback def async_run_hass_job( - self, hassjob: HassJob[..., Coroutine[Any, Any, _R] | _R], *args: Any - ) -> asyncio.Future[_R] | None: - ... + self, + hassjob: HassJob[..., Coroutine[Any, Any, _R] | _R], + *args: Any, + background: bool = False, + ) -> asyncio.Future[_R] | None: ... @callback def async_run_hass_job( - self, hassjob: HassJob[..., Coroutine[Any, Any, _R] | _R], *args: Any + self, + hassjob: HassJob[..., Coroutine[Any, Any, _R] | _R], + *args: Any, + background: bool = False, ) -> asyncio.Future[_R] | None: """Run a HassJob from within the event loop. This method must be run in the event loop. + If background is True, the task will created as a background task. + hassjob: HassJob args: parameters for method to call. """ @@ -727,34 +836,34 @@ class HomeAssistant: hassjob.target(*args) return None - return self.async_add_hass_job(hassjob, *args) + return self.async_add_hass_job( + hassjob, *args, eager_start=True, background=background + ) @overload @callback def async_run_job( - self, target: Callable[..., Coroutine[Any, Any, _R]], *args: Any - ) -> asyncio.Future[_R] | None: - ... + self, target: Callable[[*_Ts], Coroutine[Any, Any, _R]], *args: *_Ts + ) -> asyncio.Future[_R] | None: ... @overload @callback def async_run_job( - self, target: Callable[..., Coroutine[Any, Any, _R] | _R], *args: Any - ) -> asyncio.Future[_R] | None: - ... + self, target: Callable[[*_Ts], Coroutine[Any, Any, _R] | _R], *args: *_Ts + ) -> asyncio.Future[_R] | None: ... @overload @callback def async_run_job( self, target: Coroutine[Any, Any, _R], *args: Any - ) -> asyncio.Future[_R] | None: - ... + ) -> asyncio.Future[_R] | None: ... @callback def async_run_job( self, - target: Callable[..., Coroutine[Any, Any, _R] | _R] | Coroutine[Any, Any, _R], - *args: Any, + target: Callable[[*_Ts], Coroutine[Any, Any, _R] | _R] + | Coroutine[Any, Any, _R], + *args: *_Ts, ) -> asyncio.Future[_R] | None: """Run a job from within the event loop. @@ -763,15 +872,26 @@ class HomeAssistant: target: target to call. args: parameters for method to call. """ + # late import to avoid circular imports + from .helpers import frame # pylint: disable=import-outside-toplevel + + frame.report( + "calls `async_run_job`, which is deprecated and will be removed in Home " + "Assistant 2025.4; Please review " + "https://developers.home-assistant.io/blog/2024/03/13/deprecate_add_run_job" + " for replacement options", + error_if_core=False, + ) + if asyncio.iscoroutine(target): - return self.async_create_task(target) + return self.async_create_task(target, eager_start=True) # This code path is performance sensitive and uses # if TYPE_CHECKING to avoid the overhead of constructing # the type used for the cast. For history see: # https://github.com/home-assistant/core/pull/71960 if TYPE_CHECKING: - target = cast(Callable[..., Coroutine[Any, Any, _R] | _R], target) + target = cast(Callable[[*_Ts], Coroutine[Any, Any, _R] | _R], target) return self.async_run_hass_job(HassJob(target), *args) def block_till_done(self) -> None: @@ -780,16 +900,19 @@ class HomeAssistant: self.async_block_till_done(), self.loop ).result() - async def async_block_till_done(self) -> None: + async def async_block_till_done(self, wait_background_tasks: bool = False) -> None: """Block until all pending work is done.""" # To flush out any call_soon_threadsafe await asyncio.sleep(0) start_time: float | None = None current_task = asyncio.current_task() - while tasks := [ task - for task in self._tasks + for task in ( + self._tasks | self._background_tasks + if wait_background_tasks + else self._tasks + ) if task is not current_task and not cancelling(task) ]: await self._await_and_log_pending(tasks) @@ -826,15 +949,13 @@ class HomeAssistant: @callback def async_add_shutdown_job( self, hassjob: HassJob[..., Coroutine[Any, Any, Any]], *args: Any - ) -> CALLBACK_TYPE: - ... + ) -> CALLBACK_TYPE: ... @overload @callback def async_add_shutdown_job( self, hassjob: HassJob[..., Coroutine[Any, Any, Any] | Any], *args: Any - ) -> CALLBACK_TYPE: - ... + ) -> CALLBACK_TYPE: ... @callback def async_add_shutdown_job( @@ -1019,7 +1140,7 @@ class HomeAssistant: if ( not handle.cancelled() and (args := handle._args) # pylint: disable=protected-access - and type(job := args[0]) is HassJob # noqa: E721 + and type(job := args[0]) is HassJob and job.cancel_on_shutdown ): handle.cancel() @@ -1043,7 +1164,7 @@ class Context: self.id = id or ulid_now() self.user_id = user_id self.parent_id = parent_id - self.origin_event: Event | None = None + self.origin_event: Event[Any] | None = None def __eq__(self, other: Any) -> bool: """Compare contexts.""" @@ -1088,32 +1209,32 @@ class EventOrigin(enum.Enum): return self.value -class Event: +class Event(Generic[_DataT]): """Representation of an event within the bus.""" def __init__( self, event_type: str, - data: Mapping[str, Any] | None = None, + data: _DataT | None = None, origin: EventOrigin = EventOrigin.local, - time_fired: datetime.datetime | None = None, + time_fired_timestamp: float | None = None, context: Context | None = None, ) -> None: """Initialize a new event.""" self.event_type = event_type - self.data = data or {} + self.data: _DataT = data or {} # type: ignore[assignment] self.origin = origin - self.time_fired = time_fired or dt_util.utcnow() + self.time_fired_timestamp = time_fired_timestamp or time.time() if not context: - context = Context(id=ulid_at_time(self.time_fired.timestamp())) + context = Context(id=ulid_at_time(self.time_fired_timestamp)) self.context = context if not context.origin_event: context.origin_event = self @cached_property - def time_fired_timestamp(self) -> float: + def time_fired(self) -> datetime.datetime: """Return time fired as a timestamp.""" - return self.time_fired.timestamp() + return dt_util.utc_from_timestamp(self.time_fired_timestamp) @cached_property def _as_dict(self) -> dict[str, Any]: @@ -1163,18 +1284,22 @@ class Event: def __repr__(self) -> str: """Return the representation.""" - if self.data: - return ( - f"" - ) + return _event_repr(self.event_type, self.origin, self.data) - return f"" + +def _event_repr( + event_type: str, origin: EventOrigin, data: Mapping[str, Any] | None +) -> str: + """Return the representation.""" + if data: + return f"" + + return f"" _FilterableJobType = tuple[ - HassJob[[Event], Coroutine[Any, Any, None] | None], # job - Callable[[Event], bool] | None, # event_filter + HassJob[[Event[_DataT]], Coroutine[Any, Any, None] | None], # job + Callable[[_DataT], bool] | None, # event_filter bool, # run_immediately ] @@ -1182,7 +1307,7 @@ _FilterableJobType = tuple[ @dataclass(slots=True) class _OneTimeListener: hass: HomeAssistant - listener: Callable[[Event], Coroutine[Any, Any, None] | None] + listener_job: HassJob[[Event], Coroutine[Any, Any, None] | None] remove: CALLBACK_TYPE | None = None @callback @@ -1193,27 +1318,40 @@ class _OneTimeListener: return self.remove() self.remove = None - self.hass.async_run_job(self.listener, event) + self.hass.async_run_hass_job(self.listener_job, event) def __repr__(self) -> str: """Return the representation of the listener and source module.""" - module = inspect.getmodule(self.listener) + module = inspect.getmodule(self.listener_job.target) if module: - return f"<_OneTimeListener {module.__name__}:{self.listener}>" - return f"<_OneTimeListener {self.listener}>" + return f"<_OneTimeListener {module.__name__}:{self.listener_job.target}>" + return f"<_OneTimeListener {self.listener_job.target}>" + + +# Empty list, used by EventBus._async_fire +EMPTY_LIST: list[Any] = [] class EventBus: """Allow the firing of and listening for events.""" - __slots__ = ("_listeners", "_match_all_listeners", "_hass") + __slots__ = ("_debug", "_hass", "_listeners", "_match_all_listeners") def __init__(self, hass: HomeAssistant) -> None: """Initialize a new event bus.""" - self._listeners: dict[str, list[_FilterableJobType]] = {} - self._match_all_listeners: list[_FilterableJobType] = [] + self._listeners: dict[str, list[_FilterableJobType[Any]]] = {} + self._match_all_listeners: list[_FilterableJobType[Any]] = [] self._listeners[MATCH_ALL] = self._match_all_listeners self._hass = hass + self._async_logging_changed() + self.async_listen( + EVENT_LOGGING_CHANGED, self._async_logging_changed, run_immediately=True + ) + + @callback + def _async_logging_changed(self, event: Event | None = None) -> None: + """Handle logging change.""" + self._debug = _LOGGER.isEnabledFor(logging.DEBUG) @callback def async_listeners(self) -> dict[str, int]: @@ -1247,7 +1385,7 @@ class EventBus: event_data: Mapping[str, Any] | None = None, origin: EventOrigin = EventOrigin.local, context: Context | None = None, - time_fired: datetime.datetime | None = None, + time_fired: float | None = None, ) -> None: """Fire an event. @@ -1257,33 +1395,63 @@ class EventBus: raise MaxLengthExceeded( event_type, "event_type", MAX_LENGTH_EVENT_EVENT_TYPE ) + return self._async_fire(event_type, event_data, origin, context, time_fired) - listeners = self._listeners.get(event_type, []) - match_all_listeners = self._match_all_listeners + @callback + def _async_fire( + self, + event_type: str, + event_data: Mapping[str, Any] | None = None, + origin: EventOrigin = EventOrigin.local, + context: Context | None = None, + time_fired: float | None = None, + ) -> None: + """Fire an event. - event = Event(event_type, event_data, origin, time_fired, context) + This method must be run in the event loop. + """ - if _LOGGER.isEnabledFor(logging.DEBUG): - _LOGGER.debug("Bus:Handling %s", event) + if self._debug: + _LOGGER.debug( + "Bus:Handling %s", _event_repr(event_type, origin, event_data) + ) - if not listeners and not match_all_listeners: + listeners = self._listeners.get(event_type, EMPTY_LIST) + if event_type not in EVENTS_EXCLUDED_FROM_MATCH_ALL: + match_all_listeners = self._match_all_listeners + else: + match_all_listeners = EMPTY_LIST + if event_type == EVENT_STATE_CHANGED: + aliased_listeners = self._listeners.get(EVENT_STATE_REPORTED, EMPTY_LIST) + else: + aliased_listeners = EMPTY_LIST + listeners = listeners + match_all_listeners + aliased_listeners + if not listeners: return - # EVENT_HOMEASSISTANT_CLOSE should not be sent to MATCH_ALL listeners - if event_type != EVENT_HOMEASSISTANT_CLOSE: - listeners = match_all_listeners + listeners + event: Event | None = None for job, event_filter, run_immediately in listeners: if event_filter is not None: try: - if not event_filter(event): + if event_data is None or not event_filter(event_data): continue except Exception: # pylint: disable=broad-except _LOGGER.exception("Error in event filter") continue + + if not event: + event = Event( + event_type, + event_data, + origin, + time_fired, + context, + ) + if run_immediately: try: - job.target(event) + self._hass.async_run_hass_job(job, event) except Exception: # pylint: disable=broad-except _LOGGER.exception("Error running job: %s", job) else: @@ -1292,7 +1460,7 @@ class EventBus: def listen( self, event_type: str, - listener: Callable[[Event], Coroutine[Any, Any, None] | None], + listener: Callable[[Event[Any]], Coroutine[Any, Any, None] | None], ) -> CALLBACK_TYPE: """Listen for all events or events of a specific type. @@ -1313,8 +1481,8 @@ class EventBus: def async_listen( self, event_type: str, - listener: Callable[[Event], Coroutine[Any, Any, None] | None], - event_filter: Callable[[Event], bool] | None = None, + listener: Callable[[Event[_DataT]], Coroutine[Any, Any, None] | None], + event_filter: Callable[[_DataT], bool] | None = None, run_immediately: bool = False, ) -> CALLBACK_TYPE: """Listen for all events or events of a specific type. @@ -1326,23 +1494,27 @@ class EventBus: @callback that returns a boolean value, determines if the listener callable should run. - If run_immediately is passed, the callback will be run - right away instead of using call_soon. Only use this if - the callback results in scheduling another task. + If run_immediately is passed: + - callbacks will be run right away instead of using call_soon. + - coroutine functions will be scheduled eagerly. This method must be run in the event loop. """ - job_type: HassJobType | None = None if event_filter is not None and not is_callback_check_partial(event_filter): raise HomeAssistantError(f"Event filter {event_filter} is not a callback") - if run_immediately: - if not is_callback_check_partial(listener): - raise HomeAssistantError(f"Event listener {listener} is not a callback") - job_type = HassJobType.Callback + if event_type == EVENT_STATE_REPORTED: + if not event_filter: + raise HomeAssistantError( + f"Event filter is required for event {event_type}" + ) + if not run_immediately: + raise HomeAssistantError( + f"Run immediately must be set to True for event {event_type}" + ) return self._async_listen_filterable_job( event_type, ( - HassJob(listener, f"listen {event_type}", job_type=job_type), + HassJob(listener, f"listen {event_type}"), event_filter, run_immediately, ), @@ -1350,7 +1522,7 @@ class EventBus: @callback def _async_listen_filterable_job( - self, event_type: str, filterable_job: _FilterableJobType + self, event_type: str, filterable_job: _FilterableJobType[Any] ) -> CALLBACK_TYPE: self._listeners.setdefault(event_type, []).append(filterable_job) return functools.partial( @@ -1360,7 +1532,7 @@ class EventBus: def listen_once( self, event_type: str, - listener: Callable[[Event], Coroutine[Any, Any, None] | None], + listener: Callable[[Event[Any]], Coroutine[Any, Any, None] | None], ) -> CALLBACK_TYPE: """Listen once for event of a specific type. @@ -1383,7 +1555,8 @@ class EventBus: def async_listen_once( self, event_type: str, - listener: Callable[[Event], Coroutine[Any, Any, None] | None], + listener: Callable[[Event[Any]], Coroutine[Any, Any, None] | None], + run_immediately: bool = False, ) -> CALLBACK_TYPE: """Listen once for event of a specific type. @@ -1394,7 +1567,7 @@ class EventBus: This method must be run in the event loop. """ - one_time_listener = _OneTimeListener(self._hass, listener) + one_time_listener = _OneTimeListener(self._hass, HassJob(listener)) remove = self._async_listen_filterable_job( event_type, ( @@ -1404,7 +1577,7 @@ class EventBus: job_type=HassJobType.Callback, ), None, - False, + run_immediately, ), ) one_time_listener.remove = remove @@ -1432,14 +1605,25 @@ class EventBus: ) +class CompressedState(TypedDict): + """Compressed dict of a state.""" + + s: str # COMPRESSED_STATE_STATE + a: ReadOnlyDict[str, Any] # COMPRESSED_STATE_ATTRIBUTES + c: str | dict[str, Any] # COMPRESSED_STATE_CONTEXT + lc: float # COMPRESSED_STATE_LAST_CHANGED + lu: NotRequired[float] # COMPRESSED_STATE_LAST_UPDATED + + class State: """Object to represent a state within the state machine. entity_id: the entity that is represented. state: the state of the entity attributes: extra information on entity and state - last_changed: last time the state was changed, not the attributes. - last_updated: last time this object was updated. + last_changed: last time the state was changed. + last_reported: last time the state was reported. + last_updated: last time the state or attributes were changed. context: Context in which it was created domain: Domain of this state. object_id: Object id of this state. @@ -1451,6 +1635,7 @@ class State: state: str, attributes: Mapping[str, Any] | None = None, last_changed: datetime.datetime | None = None, + last_reported: datetime.datetime | None = None, last_updated: datetime.datetime | None = None, context: Context | None = None, validate_entity_id: bool | None = True, @@ -1472,11 +1657,12 @@ class State: # State only creates and expects a ReadOnlyDict so # there is no need to check for subclassing with # isinstance here so we can use the faster type check. - if type(attributes) is not ReadOnlyDict: # noqa: E721 + if type(attributes) is not ReadOnlyDict: self.attributes = ReadOnlyDict(attributes or {}) else: self.attributes = attributes - self.last_updated = last_updated or dt_util.utcnow() + self.last_reported = last_reported or dt_util.utcnow() + self.last_updated = last_updated or self.last_reported self.last_changed = last_changed or self.last_updated self.context = context or Context() self.state_info = state_info @@ -1489,16 +1675,21 @@ class State: "_", " " ) - @cached_property - def last_updated_timestamp(self) -> float: - """Timestamp of last update.""" - return self.last_updated.timestamp() - @cached_property def last_changed_timestamp(self) -> float: """Timestamp of last change.""" return self.last_changed.timestamp() + @cached_property + def last_reported_timestamp(self) -> float: + """Timestamp of last report.""" + return self.last_reported.timestamp() + + @cached_property + def last_updated_timestamp(self) -> float: + """Timestamp of last update.""" + return self.last_updated.timestamp() + @cached_property def _as_dict(self) -> dict[str, Any]: """Return a dict representation of the State. @@ -1511,11 +1702,16 @@ class State: last_updated_isoformat = last_changed_isoformat else: last_updated_isoformat = self.last_updated.isoformat() + if self.last_changed == self.last_reported: + last_reported_isoformat = last_changed_isoformat + else: + last_reported_isoformat = self.last_reported.isoformat() return { "entity_id": self.entity_id, "state": self.state, "attributes": self.attributes, "last_changed": last_changed_isoformat, + "last_reported": last_reported_isoformat, "last_updated": last_updated_isoformat, # _as_dict is marked as protected # to avoid callers outside of this module @@ -1561,7 +1757,7 @@ class State: return json_fragment(self.as_dict_json) @cached_property - def as_compressed_state(self) -> dict[str, Any]: + def as_compressed_state(self) -> CompressedState: """Build a compressed dict of a state for adds. Omits the lu (last_updated) if it matches (lc) last_changed. @@ -1576,16 +1772,16 @@ class State: # to avoid callers outside of this module # from misusing it by mistake. context = state_context._as_dict # pylint: disable=protected-access - compressed_state = { + compressed_state: CompressedState = { COMPRESSED_STATE_STATE: self.state, COMPRESSED_STATE_ATTRIBUTES: self.attributes, COMPRESSED_STATE_CONTEXT: context, COMPRESSED_STATE_LAST_CHANGED: self.last_changed_timestamp, } if self.last_changed != self.last_updated: - compressed_state[ - COMPRESSED_STATE_LAST_UPDATED - ] = self.last_updated_timestamp + compressed_state[COMPRESSED_STATE_LAST_UPDATED] = ( + self.last_updated_timestamp + ) return compressed_state @cached_property @@ -1610,15 +1806,17 @@ class State: return None last_changed = json_dict.get("last_changed") - if isinstance(last_changed, str): last_changed = dt_util.parse_datetime(last_changed) last_updated = json_dict.get("last_updated") - if isinstance(last_updated, str): last_updated = dt_util.parse_datetime(last_updated) + last_reported = json_dict.get("last_reported") + if isinstance(last_reported, str): + last_reported = dt_util.parse_datetime(last_reported) + if context := json_dict.get("context"): context = Context(id=context.get("id"), user_id=context.get("user_id")) @@ -1626,9 +1824,10 @@ class State: json_dict["entity_id"], json_dict["state"], json_dict.get("attributes"), - last_changed, - last_updated, - context, + last_changed=last_changed, + last_reported=last_reported, + last_updated=last_updated, + context=context, ) def expire(self) -> None: @@ -1827,7 +2026,7 @@ class StateMachine: return False old_state.expire() - self._bus.async_fire( + self._bus._async_fire( # pylint: disable=protected-access EVENT_STATE_CHANGED, {"entity_id": entity_id, "old_state": old_state, "new_state": None}, context=context, @@ -1922,38 +2121,55 @@ class StateMachine: same_attr = old_state.attributes == attributes last_changed = old_state.last_changed if same_state else None + # It is much faster to convert a timestamp to a utc datetime object + # than converting a utc datetime object to a timestamp since cpython + # does not have a fast path for handling the UTC timezone and has to do + # multiple local timezone conversions. + # + # from_timestamp implementation: + # https://github.com/python/cpython/blob/c90a862cdcf55dc1753c6466e5fa4a467a13ae24/Modules/_datetimemodule.c#L2936 + # + # timestamp implementation: + # https://github.com/python/cpython/blob/c90a862cdcf55dc1753c6466e5fa4a467a13ae24/Modules/_datetimemodule.c#L6387 + # https://github.com/python/cpython/blob/c90a862cdcf55dc1753c6466e5fa4a467a13ae24/Modules/_datetimemodule.c#L6323 + timestamp = time.time() + now = dt_util.utc_from_timestamp(timestamp) + if same_state and same_attr: + # mypy does not understand this is only possible if old_state is not None + old_last_reported = old_state.last_reported # type: ignore[union-attr] + old_state.last_reported = now # type: ignore[union-attr] + self._bus._async_fire( # pylint: disable=protected-access + EVENT_STATE_REPORTED, + { + "entity_id": entity_id, + "old_last_reported": old_last_reported, + "new_state": old_state, + }, + context=context, + time_fired=timestamp, + ) return if context is None: - # It is much faster to convert a timestamp to a utc datetime object - # than converting a utc datetime object to a timestamp since cpython - # does not have a fast path for handling the UTC timezone and has to do - # multiple local timezone conversions. - # - # from_timestamp implementation: - # https://github.com/python/cpython/blob/c90a862cdcf55dc1753c6466e5fa4a467a13ae24/Modules/_datetimemodule.c#L2936 - # - # timestamp implementation: - # https://github.com/python/cpython/blob/c90a862cdcf55dc1753c6466e5fa4a467a13ae24/Modules/_datetimemodule.c#L6387 - # https://github.com/python/cpython/blob/c90a862cdcf55dc1753c6466e5fa4a467a13ae24/Modules/_datetimemodule.c#L6323 - timestamp = time.time() - now = dt_util.utc_from_timestamp(timestamp) + if TYPE_CHECKING: + assert timestamp is not None context = Context(id=ulid_at_time(timestamp)) - else: - now = dt_util.utcnow() if same_attr: if TYPE_CHECKING: assert old_state is not None attributes = old_state.attributes + # This is intentionally called with positional only arguments for performance + # reasons state = State( entity_id, new_state, attributes, last_changed, now, + now, context, old_state is None, state_info, @@ -1961,11 +2177,11 @@ class StateMachine: if old_state is not None: old_state.expire() self._states[entity_id] = state - self._bus.async_fire( + self._bus._async_fire( # pylint: disable=protected-access EVENT_STATE_CHANGED, {"entity_id": entity_id, "old_state": old_state, "new_state": state}, context=context, - time_fired=now, + time_fired=timestamp, ) @@ -2001,9 +2217,10 @@ class Service: service: str, context: Context | None = None, supports_response: SupportsResponse = SupportsResponse.NONE, + job_type: HassJobType | None = None, ) -> None: """Initialize a service.""" - self.job = HassJob(func, f"service {domain}.{service}") + self.job = HassJob(func, f"service {domain}.{service}", job_type=job_type) self.schema = schema self.supports_response = supports_response @@ -2145,6 +2362,7 @@ class ServiceRegistry: ], schema: vol.Schema | None = None, supports_response: SupportsResponse = SupportsResponse.NONE, + job_type: HassJobType | None = None, ) -> None: """Register a service. @@ -2155,7 +2373,12 @@ class ServiceRegistry: domain = domain.lower() service = service.lower() service_obj = Service( - service_func, schema, domain, service, supports_response=supports_response + service_func, + schema, + domain, + service, + supports_response=supports_response, + job_type=job_type, ) if domain in self._services: @@ -2297,7 +2520,7 @@ class ServiceRegistry: domain, service, processed_data, context, return_response ) - self._hass.bus.async_fire( + self._hass.bus._async_fire( # pylint: disable=protected-access EVENT_CALL_SERVICE, { ATTR_DOMAIN: domain, @@ -2370,7 +2593,7 @@ class Config: """Initialize a new config object.""" self.hass = hass - self._store = self._ConfigStore(self.hass) + self._store = self._ConfigStore(self.hass, config_dir) self.latitude: float = 0 self.longitude: float = 0 @@ -2637,7 +2860,7 @@ class Config: class _ConfigStore(Store[dict[str, Any]]): """Class to help storing Config data.""" - def __init__(self, hass: HomeAssistant) -> None: + def __init__(self, hass: HomeAssistant, config_dir: str) -> None: """Initialize storage class.""" super().__init__( hass, @@ -2646,6 +2869,7 @@ class Config: private=True, atomic_writes=True, minor_version=CORE_STORAGE_MINOR_VERSION, + config_dir=config_dir, ) self._original_unit_system: str | None = None # from old store 1.1 diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index bbb6621cfcc..6a1453c9ff3 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -1,4 +1,5 @@ """Classes to help gather user submissions.""" + from __future__ import annotations import abc @@ -11,8 +12,9 @@ from enum import StrEnum from functools import partial import logging from types import MappingProxyType -from typing import Any, Required, TypedDict +from typing import Any, Generic, Required, TypedDict +from typing_extensions import TypeVar import voluptuous as vol from .core import HomeAssistant, callback @@ -75,6 +77,7 @@ FLOW_NOT_COMPLETE_STEPS = { FlowResultType.MENU, } + STEP_ID_OPTIONAL_STEPS = { FlowResultType.EXTERNAL_STEP, FlowResultType.FORM, @@ -83,6 +86,10 @@ STEP_ID_OPTIONAL_STEPS = { } +_FlowResultT = TypeVar("_FlowResultT", bound="FlowResult[Any]", default="FlowResult") +_HandlerT = TypeVar("_HandlerT", default=str) + + @dataclass(slots=True) class BaseServiceInfo: """Base class for discovery ServiceInfo.""" @@ -133,7 +140,7 @@ class AbortFlow(FlowError): self.description_placeholders = description_placeholders -class FlowResult(TypedDict, total=False): +class FlowResult(TypedDict, Generic[_HandlerT], total=False): """Typed result dict.""" context: dict[str, Any] @@ -144,10 +151,9 @@ class FlowResult(TypedDict, total=False): errors: dict[str, str] | None extra: str flow_id: Required[str] - handler: Required[str] + handler: Required[_HandlerT] last_step: bool | None menu_options: list[str] | dict[str, str] - minor_version: int options: Mapping[str, Any] preview: str | None progress_action: str @@ -160,27 +166,6 @@ class FlowResult(TypedDict, total=False): translation_domain: str type: FlowResultType url: str - version: int - - -@callback -def _async_flow_handler_to_flow_result( - flows: Iterable[FlowHandler], include_uninitialized: bool -) -> list[FlowResult]: - """Convert a list of FlowHandler to a partial FlowResult that can be serialized.""" - results = [] - for flow in flows: - if not include_uninitialized and flow.cur_step is None: - continue - result = FlowResult( - flow_id=flow.flow_id, - handler=flow.handler, - context=flow.context, - ) - if flow.cur_step: - result["step_id"] = flow.cur_step["step_id"] - results.append(result) - return results def _map_error_to_schema_errors( @@ -206,28 +191,34 @@ def _map_error_to_schema_errors( schema_errors[path_part_str] = error.error_message -class FlowManager(abc.ABC): +class FlowManager(abc.ABC, Generic[_FlowResultT, _HandlerT]): """Manage all the flows that are in progress.""" + _flow_result: type[_FlowResultT] = FlowResult # type: ignore[assignment] + def __init__( self, hass: HomeAssistant, ) -> None: """Initialize the flow manager.""" self.hass = hass - self._preview: set[str] = set() - self._progress: dict[str, FlowHandler] = {} - self._handler_progress_index: dict[str, set[FlowHandler]] = {} - self._init_data_process_index: dict[type, set[FlowHandler]] = {} + self._preview: set[_HandlerT] = set() + self._progress: dict[str, FlowHandler[_FlowResultT, _HandlerT]] = {} + self._handler_progress_index: dict[ + _HandlerT, set[FlowHandler[_FlowResultT, _HandlerT]] + ] = {} + self._init_data_process_index: dict[ + type, set[FlowHandler[_FlowResultT, _HandlerT]] + ] = {} @abc.abstractmethod async def async_create_flow( self, - handler_key: str, + handler_key: _HandlerT, *, context: dict[str, Any] | None = None, data: dict[str, Any] | None = None, - ) -> FlowHandler: + ) -> FlowHandler[_FlowResultT, _HandlerT]: """Create a flow for specified handler. Handler key is the domain of the component that we want to set up. @@ -235,16 +226,18 @@ class FlowManager(abc.ABC): @abc.abstractmethod async def async_finish_flow( - self, flow: FlowHandler, result: FlowResult - ) -> FlowResult: + self, flow: FlowHandler[_FlowResultT, _HandlerT], result: _FlowResultT + ) -> _FlowResultT: """Finish a data entry flow.""" - async def async_post_init(self, flow: FlowHandler, result: FlowResult) -> None: + async def async_post_init( + self, flow: FlowHandler[_FlowResultT, _HandlerT], result: _FlowResultT + ) -> None: """Entry has finished executing its first step asynchronously.""" @callback def async_has_matching_flow( - self, handler: str, match_context: dict[str, Any], data: Any + self, handler: _HandlerT, match_context: dict[str, Any], data: Any ) -> bool: """Check if an existing matching flow is in progress. @@ -262,32 +255,32 @@ class FlowManager(abc.ABC): return False @callback - def async_get(self, flow_id: str) -> FlowResult: + def async_get(self, flow_id: str) -> _FlowResultT: """Return a flow in progress as a partial FlowResult.""" if (flow := self._progress.get(flow_id)) is None: raise UnknownFlow - return _async_flow_handler_to_flow_result([flow], False)[0] + return self._async_flow_handler_to_flow_result([flow], False)[0] @callback - def async_progress(self, include_uninitialized: bool = False) -> list[FlowResult]: + def async_progress(self, include_uninitialized: bool = False) -> list[_FlowResultT]: """Return the flows in progress as a partial FlowResult.""" - return _async_flow_handler_to_flow_result( + return self._async_flow_handler_to_flow_result( self._progress.values(), include_uninitialized ) @callback def async_progress_by_handler( self, - handler: str, + handler: _HandlerT, include_uninitialized: bool = False, match_context: dict[str, Any] | None = None, - ) -> list[FlowResult]: + ) -> list[_FlowResultT]: """Return the flows in progress by handler as a partial FlowResult. If match_context is specified, only return flows with a context that is a superset of match_context. """ - return _async_flow_handler_to_flow_result( + return self._async_flow_handler_to_flow_result( self._async_progress_by_handler(handler, match_context), include_uninitialized, ) @@ -298,9 +291,9 @@ class FlowManager(abc.ABC): init_data_type: type, matcher: Callable[[Any], bool], include_uninitialized: bool = False, - ) -> list[FlowResult]: + ) -> list[_FlowResultT]: """Return flows in progress init matching by data type as a partial FlowResult.""" - return _async_flow_handler_to_flow_result( + return self._async_flow_handler_to_flow_result( ( progress for progress in self._init_data_process_index.get(init_data_type, set()) @@ -311,8 +304,8 @@ class FlowManager(abc.ABC): @callback def _async_progress_by_handler( - self, handler: str, match_context: dict[str, Any] | None - ) -> list[FlowHandler]: + self, handler: _HandlerT, match_context: dict[str, Any] | None + ) -> list[FlowHandler[_FlowResultT, _HandlerT]]: """Return the flows in progress by handler. If match_context is specified, only return flows with a context that @@ -328,8 +321,12 @@ class FlowManager(abc.ABC): ] async def async_init( - self, handler: str, *, context: dict[str, Any] | None = None, data: Any = None - ) -> FlowResult: + self, + handler: _HandlerT, + *, + context: dict[str, Any] | None = None, + data: Any = None, + ) -> _FlowResultT: """Start a data entry flow.""" if context is None: context = {} @@ -352,9 +349,9 @@ class FlowManager(abc.ABC): async def async_configure( self, flow_id: str, user_input: dict | None = None - ) -> FlowResult: + ) -> _FlowResultT: """Continue a data entry flow.""" - result: FlowResult | None = None + result: _FlowResultT | None = None while not result or result["type"] == FlowResultType.SHOW_PROGRESS_DONE: result = await self._async_configure(flow_id, user_input) flow = self._progress.get(flow_id) @@ -364,7 +361,7 @@ class FlowManager(abc.ABC): async def _async_configure( self, flow_id: str, user_input: dict | None = None - ) -> FlowResult: + ) -> _FlowResultT: """Continue a data entry flow.""" if (flow := self._progress.get(flow_id)) is None: raise UnknownFlow @@ -376,7 +373,7 @@ class FlowManager(abc.ABC): data_schema := cur_step.get("data_schema") ) is not None and user_input is not None: try: - user_input = data_schema(user_input) + user_input = data_schema(user_input) # type: ignore[operator] except vol.Invalid as ex: raised_errors = [ex] if isinstance(ex, vol.MultipleInvalid): @@ -458,7 +455,9 @@ class FlowManager(abc.ABC): self._async_remove_flow_progress(flow_id) @callback - def _async_add_flow_progress(self, flow: FlowHandler) -> None: + def _async_add_flow_progress( + self, flow: FlowHandler[_FlowResultT, _HandlerT] + ) -> None: """Add a flow to in progress.""" if flow.init_data is not None: init_data_type = type(flow.init_data) @@ -467,7 +466,9 @@ class FlowManager(abc.ABC): self._handler_progress_index.setdefault(flow.handler, set()).add(flow) @callback - def _async_remove_flow_from_index(self, flow: FlowHandler) -> None: + def _async_remove_flow_from_index( + self, flow: FlowHandler[_FlowResultT, _HandlerT] + ) -> None: """Remove a flow from in progress.""" if flow.init_data is not None: init_data_type = type(flow.init_data) @@ -492,17 +493,24 @@ class FlowManager(abc.ABC): _LOGGER.exception("Error removing %s flow: %s", flow.handler, err) async def _async_handle_step( - self, flow: FlowHandler, step_id: str, user_input: dict | BaseServiceInfo | None - ) -> FlowResult: + self, + flow: FlowHandler[_FlowResultT, _HandlerT], + step_id: str, + user_input: dict | BaseServiceInfo | None, + ) -> _FlowResultT: """Handle a step of a flow.""" self._raise_if_step_does_not_exist(flow, step_id) method = f"async_step_{step_id}" try: - result: FlowResult = await getattr(flow, method)(user_input) + result: _FlowResultT = await getattr(flow, method)(user_input) except AbortFlow as err: - result = _create_abort_data( - flow.flow_id, flow.handler, err.reason, err.description_placeholders + result = self._flow_result( + type=FlowResultType.ABORT, + flow_id=flow.flow_id, + handler=flow.handler, + reason=err.reason, + description_placeholders=err.description_placeholders, ) # Setup the flow handler's preview if needed @@ -521,7 +529,8 @@ class FlowManager(abc.ABC): if ( result["type"] == FlowResultType.SHOW_PROGRESS - and (progress_task := result.pop("progress_task", None)) + # Mypy does not agree with using pop on _FlowResultT + and (progress_task := result.pop("progress_task", None)) # type: ignore[arg-type] and progress_task != flow.async_get_progress_task() ): # The flow's progress task was changed, register a callback on it @@ -532,8 +541,9 @@ class FlowManager(abc.ABC): def schedule_configure(_: asyncio.Task) -> None: self.hass.async_create_task(call_configure()) - progress_task.add_done_callback(schedule_configure) - flow.async_set_progress_task(progress_task) + # The mypy ignores are a consequence of mypy not accepting the pop above + progress_task.add_done_callback(schedule_configure) # type: ignore[attr-defined] + flow.async_set_progress_task(progress_task) # type: ignore[arg-type] elif result["type"] != FlowResultType.SHOW_PROGRESS: flow.async_cancel_progress_task() @@ -560,7 +570,9 @@ class FlowManager(abc.ABC): return result - def _raise_if_step_does_not_exist(self, flow: FlowHandler, step_id: str) -> None: + def _raise_if_step_does_not_exist( + self, flow: FlowHandler[_FlowResultT, _HandlerT], step_id: str + ) -> None: """Raise if the step does not exist.""" method = f"async_step_{step_id}" @@ -570,24 +582,49 @@ class FlowManager(abc.ABC): f"Handler {self.__class__.__name__} doesn't support step {step_id}" ) - async def _async_setup_preview(self, flow: FlowHandler) -> None: + async def _async_setup_preview( + self, flow: FlowHandler[_FlowResultT, _HandlerT] + ) -> None: """Set up preview for a flow handler.""" if flow.handler not in self._preview: self._preview.add(flow.handler) await flow.async_setup_preview(self.hass) + @callback + def _async_flow_handler_to_flow_result( + self, + flows: Iterable[FlowHandler[_FlowResultT, _HandlerT]], + include_uninitialized: bool, + ) -> list[_FlowResultT]: + """Convert a list of FlowHandler to a partial FlowResult that can be serialized.""" + results = [] + for flow in flows: + if not include_uninitialized and flow.cur_step is None: + continue + result = self._flow_result( + flow_id=flow.flow_id, + handler=flow.handler, + context=flow.context, + ) + if flow.cur_step: + result["step_id"] = flow.cur_step["step_id"] + results.append(result) + return results -class FlowHandler: + +class FlowHandler(Generic[_FlowResultT, _HandlerT]): """Handle a data entry flow.""" + _flow_result: type[_FlowResultT] = FlowResult # type: ignore[assignment] + # Set by flow manager - cur_step: FlowResult | None = None + cur_step: _FlowResultT | None = None # While not purely typed, it makes typehinting more useful for us # and removes the need for constant None checks or asserts. flow_id: str = None # type: ignore[assignment] hass: HomeAssistant = None # type: ignore[assignment] - handler: str = None # type: ignore[assignment] + handler: _HandlerT = None # type: ignore[assignment] # Ensure the attribute has a subscriptable, but immutable, default value. context: dict[str, Any] = MappingProxyType({}) # type: ignore[assignment] @@ -657,12 +694,12 @@ class FlowHandler: description_placeholders: Mapping[str, str | None] | None = None, last_step: bool | None = None, preview: str | None = None, - ) -> FlowResult: + ) -> _FlowResultT: """Return the definition of a form to gather user input. The step_id parameter is deprecated and will be removed in a future release. """ - flow_result = FlowResult( + flow_result = self._flow_result( type=FlowResultType.FORM, flow_id=self.flow_id, handler=self.handler, @@ -684,11 +721,9 @@ class FlowHandler: data: Mapping[str, Any], description: str | None = None, description_placeholders: Mapping[str, str] | None = None, - ) -> FlowResult: + ) -> _FlowResultT: """Finish flow.""" - flow_result = FlowResult( - version=self.VERSION, - minor_version=self.MINOR_VERSION, + flow_result = self._flow_result( type=FlowResultType.CREATE_ENTRY, flow_id=self.flow_id, handler=self.handler, @@ -707,10 +742,14 @@ class FlowHandler: *, reason: str, description_placeholders: Mapping[str, str] | None = None, - ) -> FlowResult: + ) -> _FlowResultT: """Abort the flow.""" - return _create_abort_data( - self.flow_id, self.handler, reason, description_placeholders + return self._flow_result( + type=FlowResultType.ABORT, + flow_id=self.flow_id, + handler=self.handler, + reason=reason, + description_placeholders=description_placeholders, ) @callback @@ -720,12 +759,12 @@ class FlowHandler: step_id: str | None = None, url: str, description_placeholders: Mapping[str, str] | None = None, - ) -> FlowResult: + ) -> _FlowResultT: """Return the definition of an external step for the user to take. The step_id parameter is deprecated and will be removed in a future release. """ - flow_result = FlowResult( + flow_result = self._flow_result( type=FlowResultType.EXTERNAL_STEP, flow_id=self.flow_id, handler=self.handler, @@ -737,9 +776,9 @@ class FlowHandler: return flow_result @callback - def async_external_step_done(self, *, next_step_id: str) -> FlowResult: + def async_external_step_done(self, *, next_step_id: str) -> _FlowResultT: """Return the definition of an external step for the user to take.""" - return FlowResult( + return self._flow_result( type=FlowResultType.EXTERNAL_STEP_DONE, flow_id=self.flow_id, handler=self.handler, @@ -754,7 +793,7 @@ class FlowHandler: progress_action: str, description_placeholders: Mapping[str, str] | None = None, progress_task: asyncio.Task[Any] | None = None, - ) -> FlowResult: + ) -> _FlowResultT: """Show a progress message to the user, without user input allowed. The step_id parameter is deprecated and will be removed in a future release. @@ -777,7 +816,7 @@ class FlowHandler: if progress_task is None: self.deprecated_show_progress = True - flow_result = FlowResult( + flow_result = self._flow_result( type=FlowResultType.SHOW_PROGRESS, flow_id=self.flow_id, handler=self.handler, @@ -790,9 +829,9 @@ class FlowHandler: return flow_result @callback - def async_show_progress_done(self, *, next_step_id: str) -> FlowResult: + def async_show_progress_done(self, *, next_step_id: str) -> _FlowResultT: """Mark the progress done.""" - return FlowResult( + return self._flow_result( type=FlowResultType.SHOW_PROGRESS_DONE, flow_id=self.flow_id, handler=self.handler, @@ -806,13 +845,13 @@ class FlowHandler: step_id: str | None = None, menu_options: list[str] | dict[str, str], description_placeholders: Mapping[str, str] | None = None, - ) -> FlowResult: + ) -> _FlowResultT: """Show a navigation menu to the user. Options dict maps step_id => i18n label The step_id parameter is deprecated and will be removed in a future release. """ - flow_result = FlowResult( + flow_result = self._flow_result( type=FlowResultType.MENU, flow_id=self.flow_id, handler=self.handler, @@ -853,23 +892,6 @@ class FlowHandler: self.__progress_task = progress_task -@callback -def _create_abort_data( - flow_id: str, - handler: str, - reason: str, - description_placeholders: Mapping[str, str] | None = None, -) -> FlowResult: - """Return the definition of an external step for the user to take.""" - return FlowResult( - type=FlowResultType.ABORT, - flow_id=flow_id, - handler=handler, - reason=reason, - description_placeholders=description_placeholders, - ) - - # These can be removed if no deprecated constant are in this module anymore __getattr__ = partial(check_if_deprecated_constant, module_globals=globals()) __dir__ = partial( diff --git a/homeassistant/exceptions.py b/homeassistant/exceptions.py index 8d5e2bbde95..bdf4d8c060b 100644 --- a/homeassistant/exceptions.py +++ b/homeassistant/exceptions.py @@ -1,7 +1,8 @@ """The exceptions used by Home Assistant.""" + from __future__ import annotations -from collections.abc import Generator, Sequence +from collections.abc import Callable, Generator, Sequence from dataclasses import dataclass from typing import TYPE_CHECKING @@ -9,9 +10,31 @@ if TYPE_CHECKING: from .core import Context +_function_cache: dict[str, Callable[[str, str, dict[str, str] | None], str]] = {} + + +def import_async_get_exception_message() -> ( + Callable[[str, str, dict[str, str] | None], str] +): + """Return a method that can fetch a translated exception message. + + Defaults to English, requires translations to already be cached. + """ + + # pylint: disable-next=import-outside-toplevel + from .helpers.translation import ( + async_get_exception_message as async_get_exception_message_import, + ) + + return async_get_exception_message_import + + class HomeAssistantError(Exception): """General Home Assistant exception occurred.""" + _message: str | None = None + generate_message: bool = False + def __init__( self, *args: object, @@ -20,35 +43,63 @@ class HomeAssistantError(Exception): translation_placeholders: dict[str, str] | None = None, ) -> None: """Initialize exception.""" + if not args and translation_key and translation_domain: + self.generate_message = True + args = (translation_key,) + super().__init__(*args) self.translation_domain = translation_domain self.translation_key = translation_key self.translation_placeholders = translation_placeholders + def __str__(self) -> str: + """Return exception message. + + If no message was passed to `__init__`, the exception message is generated from + the translation_key. The message will be in English, regardless of the configured + language. + """ + + if self._message: + return self._message + + if not self.generate_message: + self._message = super().__str__() + return self._message + + if TYPE_CHECKING: + assert self.translation_key is not None + assert self.translation_domain is not None + + if "async_get_exception_message" not in _function_cache: + _function_cache["async_get_exception_message"] = ( + import_async_get_exception_message() + ) + + self._message = _function_cache["async_get_exception_message"]( + self.translation_domain, self.translation_key, self.translation_placeholders + ) + return self._message + class ConfigValidationError(HomeAssistantError, ExceptionGroup[Exception]): """A validation exception occurred when validating the configuration.""" def __init__( self, - message: str, + message_translation_key: str, exceptions: list[Exception], translation_domain: str | None = None, - translation_key: str | None = None, translation_placeholders: dict[str, str] | None = None, ) -> None: """Initialize exception.""" super().__init__( - *(message, exceptions), + *(message_translation_key, exceptions), translation_domain=translation_domain, - translation_key=translation_key, + translation_key=message_translation_key, translation_placeholders=translation_placeholders, ) - self._message = message - - def __str__(self) -> str: - """Return exception message string.""" - return self._message + self.generate_message = True class ServiceValidationError(HomeAssistantError): @@ -87,7 +138,7 @@ class ConditionError(HomeAssistantError): def output(self, indent: int) -> Generator[str, None, None]: """Yield an indented representation.""" - raise NotImplementedError() + raise NotImplementedError def __str__(self) -> str: """Return string representation.""" @@ -208,18 +259,13 @@ class ServiceNotFound(HomeAssistantError): def __init__(self, domain: str, service: str) -> None: """Initialize error.""" super().__init__( - self, - f"Service {domain}.{service} not found.", translation_domain="homeassistant", translation_key="service_not_found", translation_placeholders={"domain": domain, "service": service}, ) self.domain = domain self.service = service - - def __str__(self) -> str: - """Return string representation.""" - return f"Service {self.domain}.{self.service} not found." + self.generate_message = True class MaxLengthExceeded(HomeAssistantError): @@ -228,15 +274,18 @@ class MaxLengthExceeded(HomeAssistantError): def __init__(self, value: str, property_name: str, max_length: int) -> None: """Initialize error.""" super().__init__( - self, - ( - f"Value {value} for property {property_name} has a max length of " - f"{max_length} characters" - ), + translation_domain="homeassistant", + translation_key="max_length_exceeded", + translation_placeholders={ + "value": value, + "property_name": property_name, + "max_length": str(max_length), + }, ) self.value = value self.property_name = property_name self.max_length = max_length + self.generate_message = True class DependencyError(HomeAssistantError): @@ -245,7 +294,6 @@ class DependencyError(HomeAssistantError): def __init__(self, failed_dependencies: list[str]) -> None: """Initialize error.""" super().__init__( - self, f"Could not setup dependencies: {', '.join(failed_dependencies)}", ) self.failed_dependencies = failed_dependencies diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index c0b21c0a81d..cd8174bab1f 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -266,6 +266,22 @@ BLUETOOTH: list[dict[str, bool | str | int | list[int]]] = [ "domain": "keymitt_ble", "local_name": "mib*", }, + { + "domain": "lamarzocco", + "local_name": "MICRA_*", + }, + { + "domain": "lamarzocco", + "local_name": "MINI_*", + }, + { + "domain": "lamarzocco", + "local_name": "GS3_*", + }, + { + "domain": "lamarzocco", + "local_name": "GS3AV_*", + }, { "domain": "ld2410_ble", "local_name": "HLK-LD2410B_*", @@ -412,6 +428,11 @@ BLUETOOTH: list[dict[str, bool | str | int | list[int]]] = [ "manufacturer_id": 89, "service_uuid": "0000fee5-0000-1000-8000-00805f9b34fb", }, + { + "connectable": True, + "domain": "motionblinds_ble", + "local_name": "MOTION_*", + }, { "domain": "oralb", "manufacturer_id": 220, diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 3dc07c4b287..8d46c8be240 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -118,6 +118,7 @@ FLOWS = { "dnsip", "doorbird", "dormakaba_dkey", + "downloader", "dremel_3d_printer", "drop_connect", "dsmr", @@ -179,6 +180,7 @@ FLOWS = { "fronius", "frontier_silicon", "fully_kiosk", + "fyta", "garages_amsterdam", "gardena_bluetooth", "gdacs", @@ -224,6 +226,7 @@ FLOWS = { "homekit_controller", "homematicip_cloud", "homewizard", + "homeworks", "honeywell", "huawei_lte", "hue", @@ -323,6 +326,7 @@ FLOWS = { "moon", "mopeka", "motion_blinds", + "motionblinds_ble", "motioneye", "motionmount", "mqtt", @@ -356,6 +360,7 @@ FLOWS = { "nzbget", "obihai", "octoprint", + "ollama", "omnilogic", "oncue", "ondilo_ico", @@ -438,6 +443,7 @@ FLOWS = { "romy", "roomba", "roon", + "rova", "rpi_power", "rtsp_to_webrtc", "ruckus_unleashed", @@ -457,6 +463,7 @@ FLOWS = { "sensorpush", "sentry", "senz", + "seventeentrack", "sfr_box", "sharkiq", "shelly", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 11aab652967..b8abac5145b 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -15,7 +15,8 @@ "name": "AccuWeather", "integration_type": "service", "config_flow": true, - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "single_config_entry": true }, "acer_projector": { "name": "Acer Projector", @@ -168,7 +169,7 @@ "airzone_cloud": { "integration_type": "hub", "config_flow": true, - "iot_class": "cloud_polling", + "iot_class": "cloud_push", "name": "Airzone Cloud" } } @@ -1297,7 +1298,7 @@ "downloader": { "name": "Downloader", "integration_type": "hub", - "config_flow": false + "config_flow": true }, "dremel_3d_printer": { "name": "Dremel 3D Printer", @@ -2048,6 +2049,12 @@ "config_flow": false, "iot_class": "local_polling" }, + "fyta": { + "name": "FYTA", + "integration_type": "hub", + "config_flow": true, + "iot_class": "cloud_polling" + }, "garadget": { "name": "Garadget", "integration_type": "hub", @@ -2897,7 +2904,8 @@ "name": "Jellyfin", "integration_type": "service", "config_flow": true, - "iot_class": "local_polling" + "iot_class": "local_polling", + "single_config_entry": true }, "jewish_calendar": { "name": "Jewish Calendar", @@ -3022,7 +3030,8 @@ "name": "KNX", "integration_type": "hub", "config_flow": true, - "iot_class": "local_push" + "iot_class": "local_push", + "single_config_entry": true }, "kodi": { "name": "Kodi", @@ -3376,7 +3385,7 @@ }, "homeworks": { "integration_type": "hub", - "config_flow": false, + "config_flow": true, "iot_class": "local_push", "name": "Lutron Homeworks" } @@ -3719,11 +3728,22 @@ "config_flow": true, "iot_class": "local_push" }, - "motion_blinds": { + "motionblinds": { "name": "Motionblinds", - "integration_type": "hub", - "config_flow": true, - "iot_class": "local_push" + "integrations": { + "motion_blinds": { + "integration_type": "hub", + "config_flow": true, + "iot_class": "local_push", + "name": "Motionblinds" + }, + "motionblinds_ble": { + "integration_type": "device", + "config_flow": true, + "iot_class": "assumed_state", + "name": "Motionblinds Bluetooth" + } + } }, "motioneye": { "name": "motionEye", @@ -4121,6 +4141,12 @@ "config_flow": false, "iot_class": "cloud_polling" }, + "ollama": { + "name": "Ollama", + "integration_type": "service", + "config_flow": true, + "iot_class": "local_polling" + }, "ombi": { "name": "Ombi", "integration_type": "hub", @@ -4181,12 +4207,6 @@ "config_flow": false, "iot_class": "cloud_push" }, - "opencv": { - "name": "OpenCV", - "integration_type": "hub", - "config_flow": false, - "iot_class": "local_push" - }, "openerz": { "name": "Open ERZ", "integration_type": "hub", @@ -5039,7 +5059,7 @@ "rova": { "name": "ROVA", "integration_type": "hub", - "config_flow": false, + "config_flow": true, "iot_class": "cloud_polling" }, "rss_feed_template": { @@ -5271,8 +5291,8 @@ }, "seventeentrack": { "name": "17TRACK", - "integration_type": "hub", - "config_flow": false, + "integration_type": "service", + "config_flow": true, "iot_class": "cloud_polling" }, "sfr_box": { @@ -5485,6 +5505,11 @@ "config_flow": false, "iot_class": "cloud_push" }, + "smud": { + "name": "Sacramento Municipal Utility District (SMUD)", + "integration_type": "virtual", + "supported_by": "opower" + }, "snapcast": { "name": "Snapcast", "integration_type": "hub", @@ -5737,7 +5762,8 @@ "sun": { "integration_type": "hub", "config_flow": true, - "iot_class": "calculated" + "iot_class": "calculated", + "single_config_entry": true }, "sunweg": { "name": "Sun WEG", diff --git a/homeassistant/generated/usb.py b/homeassistant/generated/usb.py index faf8abb775c..e66a5861d18 100644 --- a/homeassistant/generated/usb.py +++ b/homeassistant/generated/usb.py @@ -10,6 +10,12 @@ USB = [ "pid": "EA60", "vid": "10C4", }, + { + "description": "*home assistant connect zbt-1*", + "domain": "homeassistant_sky_connect", + "pid": "EA60", + "vid": "10C4", + }, { "domain": "insteon", "vid": "10BF", diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index 0f16977097d..baf922cdc99 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -411,6 +411,11 @@ ZEROCONF = { "domain": "elgato", }, ], + "_elmax-ssl._tcp.local.": [ + { + "domain": "elmax", + }, + ], "_enphase-envoy._tcp.local.": [ { "domain": "enphase_envoy", diff --git a/homeassistant/helpers/__init__.py b/homeassistant/helpers/__init__.py index 52197e83495..9f72445822e 100644 --- a/homeassistant/helpers/__init__.py +++ b/homeassistant/helpers/__init__.py @@ -1,4 +1,5 @@ """Helper methods for components within Home Assistant.""" + from __future__ import annotations from collections.abc import Iterable, Sequence diff --git a/homeassistant/helpers/aiohttp_client.py b/homeassistant/helpers/aiohttp_client.py index cc0be0d5515..15437b00183 100644 --- a/homeassistant/helpers/aiohttp_client.py +++ b/homeassistant/helpers/aiohttp_client.py @@ -1,4 +1,5 @@ """Helper for aiohttp webclient stuff.""" + from __future__ import annotations import asyncio @@ -31,8 +32,9 @@ if TYPE_CHECKING: DATA_CONNECTOR = "aiohttp_connector" DATA_CLIENTSESSION = "aiohttp_clientsession" -SERVER_SOFTWARE = "{0}/{1} aiohttp/{2} Python/{3[0]}.{3[1]}".format( - APPLICATION_NAME, __version__, aiohttp.__version__, sys.version_info +SERVER_SOFTWARE = ( + f"{APPLICATION_NAME}/{__version__} " + f"aiohttp/{aiohttp.__version__} Python/{sys.version_info[0]}.{sys.version_info[1]}" ) ENABLE_CLEANUP_CLOSED = not (3, 11, 1) <= sys.version_info < (3, 11, 4) @@ -189,11 +191,11 @@ async def async_aiohttp_proxy_web( except TimeoutError as err: # Timeout trying to start the web request - raise HTTPGatewayTimeout() from err + raise HTTPGatewayTimeout from err except aiohttp.ClientError as err: # Something went wrong with the connection - raise HTTPBadGateway() from err + raise HTTPBadGateway from err try: return await async_aiohttp_proxy_stream( diff --git a/homeassistant/helpers/area_registry.py b/homeassistant/helpers/area_registry.py index 38c554ffda3..fc535bed610 100644 --- a/homeassistant/helpers/area_registry.py +++ b/homeassistant/helpers/area_registry.py @@ -1,8 +1,8 @@ """Provide a way to connect devices to one physical location.""" + from __future__ import annotations -from collections import UserDict -from collections.abc import Iterable, ValuesView +from collections.abc import Iterable import dataclasses from typing import Any, Literal, TypedDict, cast @@ -10,6 +10,12 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.util import slugify from . import device_registry as dr, entity_registry as er +from .normalized_name_base_registry import ( + NormalizedNameBaseRegistryEntry, + NormalizedNameBaseRegistryItems, + normalize_name, +) +from .registry import BaseRegistry from .storage import Store from .typing import UNDEFINED, UndefinedType @@ -18,7 +24,6 @@ EVENT_AREA_REGISTRY_UPDATED = "area_registry_updated" STORAGE_KEY = "core.area_registry" STORAGE_VERSION_MAJOR = 1 STORAGE_VERSION_MINOR = 6 -SAVE_DELAY = 10 class EventAreaRegistryUpdatedData(TypedDict): @@ -29,7 +34,7 @@ class EventAreaRegistryUpdatedData(TypedDict): @dataclasses.dataclass(frozen=True, kw_only=True, slots=True) -class AreaEntry: +class AreaEntry(NormalizedNameBaseRegistryEntry): """Area Registry Entry.""" aliases: set[str] @@ -37,57 +42,9 @@ class AreaEntry: icon: str | None id: str labels: set[str] = dataclasses.field(default_factory=set) - name: str - normalized_name: str picture: str | None -class AreaRegistryItems(UserDict[str, AreaEntry]): - """Container for area registry items, maps area id -> entry. - - Maintains an additional index: - - normalized name -> entry - """ - - def __init__(self) -> None: - """Initialize the container.""" - super().__init__() - self._normalized_names: dict[str, AreaEntry] = {} - - def values(self) -> ValuesView[AreaEntry]: - """Return the underlying values to avoid __iter__ overhead.""" - return self.data.values() - - def __setitem__(self, key: str, entry: AreaEntry) -> None: - """Add an item.""" - data = self.data - normalized_name = normalize_area_name(entry.name) - - if key in data: - old_entry = data[key] - if ( - normalized_name != old_entry.normalized_name - and normalized_name in self._normalized_names - ): - raise ValueError( - f"The name {entry.name} ({normalized_name}) is already in use" - ) - del self._normalized_names[old_entry.normalized_name] - data[key] = entry - self._normalized_names[normalized_name] = entry - - def __delitem__(self, key: str) -> None: - """Remove an item.""" - entry = self[key] - normalized_name = normalize_area_name(entry.name) - del self._normalized_names[normalized_name] - super().__delitem__(key) - - def get_area_by_name(self, name: str) -> AreaEntry | None: - """Get area by name.""" - return self._normalized_names.get(normalize_area_name(name)) - - class AreaRegistryStore(Store[dict[str, list[dict[str, Any]]]]): """Store area registry data.""" @@ -130,10 +87,10 @@ class AreaRegistryStore(Store[dict[str, list[dict[str, Any]]]]): return old_data -class AreaRegistry: +class AreaRegistry(BaseRegistry): """Class to hold a registry of areas.""" - areas: AreaRegistryItems + areas: NormalizedNameBaseRegistryItems[AreaEntry] _area_data: dict[str, AreaEntry] def __init__(self, hass: HomeAssistant) -> None: @@ -159,7 +116,7 @@ class AreaRegistry: @callback def async_get_area_by_name(self, name: str) -> AreaEntry | None: """Get area by name.""" - return self.areas.get_area_by_name(name) + return self.areas.get_by_name(name) @callback def async_list_areas(self) -> Iterable[AreaEntry]: @@ -185,7 +142,7 @@ class AreaRegistry: picture: str | None = None, ) -> AreaEntry: """Create a new area.""" - normalized_name = normalize_area_name(name) + normalized_name = normalize_name(name) if self.async_get_area_by_name(name): raise ValueError(f"The name {name} ({normalized_name}) is already in use") @@ -281,7 +238,7 @@ class AreaRegistry: if name is not UNDEFINED and name != old.name: new_values["name"] = name - new_values["normalized_name"] = normalize_area_name(name) + new_values["normalized_name"] = normalize_name(name) if not new_values: return old @@ -297,12 +254,12 @@ class AreaRegistry: data = await self._store.async_load() - areas = AreaRegistryItems() + areas = NormalizedNameBaseRegistryItems[AreaEntry]() 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"]) + normalized_name = normalize_name(area["name"]) areas[area["id"]] = AreaEntry( aliases=set(area["aliases"]), floor_id=area["floor_id"], @@ -317,11 +274,6 @@ class AreaRegistry: self.areas = areas self._area_data = areas.data - @callback - def async_schedule_save(self) -> None: - """Schedule saving the area registry.""" - self._store.async_delay_save(self._data_to_save, SAVE_DELAY) - @callback def _data_to_save(self) -> dict[str, list[dict[str, Any]]]: """Return data of area registry to store in a file.""" @@ -362,10 +314,11 @@ class AreaRegistry: @callback def _removed_from_registry_filter( - event: fr.EventFloorRegistryUpdated | lr.EventLabelRegistryUpdated, + event_data: fr.EventFloorRegistryUpdatedData + | lr.EventLabelRegistryUpdatedData, ) -> bool: """Filter all except for the item removed from registry events.""" - return event.data["action"] == "remove" + return event_data["action"] == "remove" @callback def _handle_floor_registry_update(event: fr.EventFloorRegistryUpdated) -> None: @@ -377,8 +330,9 @@ class AreaRegistry: self.hass.bus.async_listen( event_type=fr.EVENT_FLOOR_REGISTRY_UPDATED, - event_filter=_removed_from_registry_filter, # type: ignore[arg-type] - listener=_handle_floor_registry_update, # type: ignore[arg-type] + event_filter=_removed_from_registry_filter, + listener=_handle_floor_registry_update, + run_immediately=True, ) @callback @@ -393,8 +347,9 @@ class AreaRegistry: self.hass.bus.async_listen( event_type=lr.EVENT_LABEL_REGISTRY_UPDATED, - event_filter=_removed_from_registry_filter, # type: ignore[arg-type] - listener=_handle_label_registry_update, # type: ignore[arg-type] + event_filter=_removed_from_registry_filter, + listener=_handle_label_registry_update, + run_immediately=True, ) @@ -421,8 +376,3 @@ def async_entries_for_floor(registry: AreaRegistry, floor_id: str) -> list[AreaE def async_entries_for_label(registry: AreaRegistry, label_id: str) -> list[AreaEntry]: """Return entries that match a label.""" return [area for area in registry.areas.values() if label_id in area.labels] - - -def normalize_area_name(area_name: str) -> str: - """Normalize an area name by removing whitespace and case folding.""" - return area_name.casefold().replace(" ", "") diff --git a/homeassistant/helpers/category_registry.py b/homeassistant/helpers/category_registry.py new file mode 100644 index 00000000000..ee0c8c1bb88 --- /dev/null +++ b/homeassistant/helpers/category_registry.py @@ -0,0 +1,209 @@ +"""Provide a way to categorize things within a defined scope.""" + +from __future__ import annotations + +from collections.abc import Iterable +import dataclasses +from dataclasses import dataclass, field +from typing import Literal, TypedDict, cast + +from homeassistant.core import HomeAssistant, callback +from homeassistant.util.ulid import ulid_now + +from .registry import BaseRegistry +from .typing import UNDEFINED, EventType, UndefinedType + +DATA_REGISTRY = "category_registry" +EVENT_CATEGORY_REGISTRY_UPDATED = "category_registry_updated" +STORAGE_KEY = "core.category_registry" +STORAGE_VERSION_MAJOR = 1 + + +class EventCategoryRegistryUpdatedData(TypedDict): + """Event data for when the category registry is updated.""" + + action: Literal["create", "remove", "update"] + scope: str + category_id: str + + +EventCategoryRegistryUpdated = EventType[EventCategoryRegistryUpdatedData] + + +@dataclass(slots=True, kw_only=True, frozen=True) +class CategoryEntry: + """Category registry entry.""" + + category_id: str = field(default_factory=ulid_now) + icon: str | None = None + name: str + + +class CategoryRegistry(BaseRegistry): + """Class to hold a registry of categories by scope.""" + + def __init__(self, hass: HomeAssistant) -> None: + """Initialize the category registry.""" + self.hass = hass + self.categories: dict[str, dict[str, CategoryEntry]] = {} + self._store = hass.helpers.storage.Store( + STORAGE_VERSION_MAJOR, + STORAGE_KEY, + atomic_writes=True, + ) + + @callback + def async_get_category( + self, *, scope: str, category_id: str + ) -> CategoryEntry | None: + """Get category by ID.""" + if scope not in self.categories: + return None + return self.categories[scope].get(category_id) + + @callback + def async_list_categories(self, *, scope: str) -> Iterable[CategoryEntry]: + """Get all categories.""" + if scope not in self.categories: + return [] + return self.categories[scope].values() + + @callback + def async_create( + self, + *, + name: str, + scope: str, + icon: str | None = None, + ) -> CategoryEntry: + """Create a new category.""" + self._async_ensure_name_is_available(scope, name) + category = CategoryEntry( + icon=icon, + name=name, + ) + + if scope not in self.categories: + self.categories[scope] = {} + + self.categories[scope][category.category_id] = category + + self.async_schedule_save() + self.hass.bus.async_fire( + EVENT_CATEGORY_REGISTRY_UPDATED, + EventCategoryRegistryUpdatedData( + action="create", scope=scope, category_id=category.category_id + ), + ) + return category + + @callback + def async_delete(self, *, scope: str, category_id: str) -> None: + """Delete category.""" + del self.categories[scope][category_id] + self.hass.bus.async_fire( + EVENT_CATEGORY_REGISTRY_UPDATED, + EventCategoryRegistryUpdatedData( + action="remove", + scope=scope, + category_id=category_id, + ), + ) + self.async_schedule_save() + + @callback + def async_update( + self, + *, + scope: str, + category_id: str, + icon: str | None | UndefinedType = UNDEFINED, + name: str | UndefinedType = UNDEFINED, + ) -> CategoryEntry: + """Update name or icon of the category.""" + old = self.categories[scope][category_id] + changes = {} + + if icon is not UNDEFINED and icon != old.icon: + changes["icon"] = icon + + if name is not UNDEFINED and name != old.name: + changes["name"] = name + self._async_ensure_name_is_available(scope, name, category_id) + + if not changes: + return old + + new = self.categories[scope][category_id] = dataclasses.replace(old, **changes) # type: ignore[arg-type] + + self.async_schedule_save() + self.hass.bus.async_fire( + EVENT_CATEGORY_REGISTRY_UPDATED, + EventCategoryRegistryUpdatedData( + action="update", scope=scope, category_id=category_id + ), + ) + + return new + + async def async_load(self) -> None: + """Load the category registry.""" + data = await self._store.async_load() + category_entries: dict[str, dict[str, CategoryEntry]] = {} + + if data is not None: + for scope, categories in data["categories"].items(): + category_entries[scope] = { + category["category_id"]: CategoryEntry( + category_id=category["category_id"], + icon=category["icon"], + name=category["name"], + ) + for category in categories + } + + self.categories = category_entries + + @callback + def _data_to_save(self) -> dict[str, dict[str, list[dict[str, str | None]]]]: + """Return data of category registry to store in a file.""" + return { + "categories": { + scope: [ + { + "category_id": entry.category_id, + "icon": entry.icon, + "name": entry.name, + } + for entry in entries.values() + ] + for scope, entries in self.categories.items() + } + } + + @callback + def _async_ensure_name_is_available( + self, scope: str, name: str, category_id: str | None = None + ) -> None: + """Ensure name is available within the scope.""" + if scope not in self.categories: + return + for category in self.categories[scope].values(): + if ( + category.name.casefold() == name.casefold() + and category.category_id != category_id + ): + raise ValueError(f"The name '{name}' is already in use") + + +@callback +def async_get(hass: HomeAssistant) -> CategoryRegistry: + """Get category registry.""" + return cast(CategoryRegistry, hass.data[DATA_REGISTRY]) + + +async def async_load(hass: HomeAssistant) -> None: + """Load category registry.""" + assert DATA_REGISTRY not in hass.data + hass.data[DATA_REGISTRY] = CategoryRegistry(hass) + await hass.data[DATA_REGISTRY].async_load() diff --git a/homeassistant/helpers/check_config.py b/homeassistant/helpers/check_config.py index b362d68ad55..78dddb12381 100644 --- a/homeassistant/helpers/check_config.py +++ b/homeassistant/helpers/check_config.py @@ -1,4 +1,5 @@ """Helper to check the configuration file.""" + from __future__ import annotations from collections import OrderedDict @@ -11,7 +12,6 @@ import voluptuous as vol from homeassistant import loader from homeassistant.config import ( # type: ignore[attr-defined] - CONF_CORE, CONF_PACKAGES, CORE_CONFIG_SCHEMA, YAML_CONFIG_FILE, @@ -22,7 +22,7 @@ from homeassistant.config import ( # type: ignore[attr-defined] load_yaml_config_file, merge_packages_config, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import DOMAIN as HA_DOMAIN, HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.requirements import ( RequirementsNotFound, @@ -157,10 +157,10 @@ async def async_check_ha_config_file( # noqa: C901 return result.add_error(f"Error loading {config_path}: {err}") # Extract and validate core [homeassistant] config - core_config = config.pop(CONF_CORE, {}) + core_config = config.pop(HA_DOMAIN, {}) try: core_config = CORE_CONFIG_SCHEMA(core_config) - result[CONF_CORE] = core_config + result[HA_DOMAIN] = core_config # Merge packages await merge_packages_config( @@ -168,8 +168,8 @@ async def async_check_ha_config_file( # noqa: C901 ) except vol.Invalid as err: result.add_error( - format_schema_error(hass, err, CONF_CORE, core_config), - CONF_CORE, + format_schema_error(hass, err, HA_DOMAIN, core_config), + HA_DOMAIN, core_config, ) core_config = {} @@ -191,22 +191,23 @@ async def async_check_ha_config_file( # noqa: C901 continue try: - component = integration.get_component() + component = await integration.async_get_component() except ImportError as ex: result.add_warning(f"Component error: {domain} - {ex}") continue # Check if the integration has a custom config validator config_validator = None - try: - config_validator = integration.get_platform("config") - except ImportError as err: - # Filter out import error of the config platform. - # If the config platform contains bad imports, make sure - # that still fails. - if err.name != f"{integration.pkg_path}.config": - result.add_error(f"Error importing config platform {domain}: {err}") - continue + if integration.platforms_exists(("config",)): + try: + config_validator = await integration.async_get_platform("config") + except ImportError as err: + # Filter out import error of the config platform. + # If the config platform contains bad imports, make sure + # that still fails. + if err.name != f"{integration.pkg_path}.config": + result.add_error(f"Error importing config platform {domain}: {err}") + continue if config_validator is not None and hasattr( config_validator, "async_validate_config" @@ -270,7 +271,7 @@ async def async_check_ha_config_file( # noqa: C901 p_integration = await async_get_integration_with_requirements( hass, p_name ) - platform = p_integration.get_platform(domain) + platform = await p_integration.async_get_platform(domain) except loader.IntegrationNotFound as ex: # We get this error if an integration is not found. In recovery mode and # safe mode, this currently happens for all custom integrations. Don't diff --git a/homeassistant/helpers/collection.py b/homeassistant/helpers/collection.py index c3c2ae4ec37..6e833e338db 100644 --- a/homeassistant/helpers/collection.py +++ b/homeassistant/helpers/collection.py @@ -1,4 +1,5 @@ """Helper to deal with YAML + storage.""" + from __future__ import annotations from abc import ABC, abstractmethod @@ -220,10 +221,10 @@ class YamlCollection(ObservableCollection[dict]): self.data[item_id] = item change_sets.append(CollectionChangeSet(event, item_id, item)) - for item_id in old_ids: - change_sets.append( - CollectionChangeSet(CHANGE_REMOVED, item_id, self.data.pop(item_id)) - ) + change_sets.extend( + CollectionChangeSet(CHANGE_REMOVED, item_id, self.data.pop(item_id)) + for item_id in old_ids + ) if change_sets: await self.notify_changes(change_sets) diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index adbaa7e3efa..e906148efdb 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -1,4 +1,5 @@ """Offer reusable conditions.""" + from __future__ import annotations import asyncio @@ -197,7 +198,7 @@ async def _async_get_condition_platform( f'Invalid condition "{platform}" specified {config}' ) from None try: - return integration.get_platform("condition") + return await integration.async_get_platform("condition") except ImportError: raise HomeAssistantError( f"Integration '{platform}' does not provide condition support" @@ -361,7 +362,7 @@ def numeric_state( ).result() -def async_numeric_state( # noqa: C901 +def async_numeric_state( hass: HomeAssistant, entity: None | str | State, below: float | str | None = None, diff --git a/homeassistant/helpers/config_entry_flow.py b/homeassistant/helpers/config_entry_flow.py index 6cdedf98f97..f2247e533a8 100644 --- a/homeassistant/helpers/config_entry_flow.py +++ b/homeassistant/helpers/config_entry_flow.py @@ -1,4 +1,5 @@ """Helpers for data entry flows for config entries.""" + from __future__ import annotations from collections.abc import Awaitable, Callable @@ -8,7 +9,6 @@ from typing import TYPE_CHECKING, Any, Generic, TypeVar, cast from homeassistant import config_entries from homeassistant.components import onboarding from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResult from .typing import DiscoveryInfoType @@ -46,7 +46,7 @@ class DiscoveryFlowHandler(config_entries.ConfigFlow, Generic[_R]): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> config_entries.ConfigFlowResult: """Handle a flow initialized by the user.""" if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") @@ -57,7 +57,7 @@ class DiscoveryFlowHandler(config_entries.ConfigFlow, Generic[_R]): async def async_step_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> config_entries.ConfigFlowResult: """Confirm setup.""" if user_input is None and onboarding.async_is_onboarded(self.hass): self._set_confirm_only() @@ -69,8 +69,7 @@ class DiscoveryFlowHandler(config_entries.ConfigFlow, Generic[_R]): if not (has_devices := bool(in_progress)): has_devices = await cast( - "asyncio.Future[bool]", - self.hass.async_add_job(self._discovery_function, self.hass), + "asyncio.Future[bool]", self._discovery_function(self.hass) ) if not has_devices: @@ -87,7 +86,7 @@ class DiscoveryFlowHandler(config_entries.ConfigFlow, Generic[_R]): async def async_step_discovery( self, discovery_info: DiscoveryInfoType - ) -> FlowResult: + ) -> config_entries.ConfigFlowResult: """Handle a flow initialized by discovery.""" if self._async_in_progress() or self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") @@ -98,7 +97,7 @@ class DiscoveryFlowHandler(config_entries.ConfigFlow, Generic[_R]): async def async_step_bluetooth( self, discovery_info: BluetoothServiceInfoBleak - ) -> FlowResult: + ) -> config_entries.ConfigFlowResult: """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") @@ -107,7 +106,9 @@ class DiscoveryFlowHandler(config_entries.ConfigFlow, Generic[_R]): return await self.async_step_confirm() - async def async_step_dhcp(self, discovery_info: DhcpServiceInfo) -> FlowResult: + async def async_step_dhcp( + self, discovery_info: DhcpServiceInfo + ) -> config_entries.ConfigFlowResult: """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") @@ -118,7 +119,7 @@ class DiscoveryFlowHandler(config_entries.ConfigFlow, Generic[_R]): async def async_step_homekit( self, discovery_info: ZeroconfServiceInfo - ) -> FlowResult: + ) -> config_entries.ConfigFlowResult: """Handle a flow initialized by Homekit discovery.""" if self._async_in_progress() or self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") @@ -127,7 +128,9 @@ class DiscoveryFlowHandler(config_entries.ConfigFlow, Generic[_R]): return await self.async_step_confirm() - async def async_step_mqtt(self, discovery_info: MqttServiceInfo) -> FlowResult: + async def async_step_mqtt( + self, discovery_info: MqttServiceInfo + ) -> config_entries.ConfigFlowResult: """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") @@ -138,7 +141,7 @@ class DiscoveryFlowHandler(config_entries.ConfigFlow, Generic[_R]): async def async_step_zeroconf( self, discovery_info: ZeroconfServiceInfo - ) -> FlowResult: + ) -> config_entries.ConfigFlowResult: """Handle a flow initialized by Zeroconf discovery.""" if self._async_in_progress() or self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") @@ -147,7 +150,9 @@ class DiscoveryFlowHandler(config_entries.ConfigFlow, Generic[_R]): return await self.async_step_confirm() - async def async_step_ssdp(self, discovery_info: SsdpServiceInfo) -> FlowResult: + async def async_step_ssdp( + self, discovery_info: SsdpServiceInfo + ) -> config_entries.ConfigFlowResult: """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") @@ -156,7 +161,9 @@ class DiscoveryFlowHandler(config_entries.ConfigFlow, Generic[_R]): return await self.async_step_confirm() - async def async_step_import(self, _: dict[str, Any] | None) -> FlowResult: + async def async_step_import( + self, _: dict[str, Any] | None + ) -> config_entries.ConfigFlowResult: """Handle a flow initialized by import.""" if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") @@ -205,7 +212,7 @@ class WebhookFlowHandler(config_entries.ConfigFlow): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> config_entries.ConfigFlowResult: """Handle a user initiated set up flow to create a webhook.""" if not self._allow_multiple and self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") @@ -213,21 +220,33 @@ class WebhookFlowHandler(config_entries.ConfigFlow): if user_input is None: return self.async_show_form(step_id="user") - webhook_id = self.hass.components.webhook.async_generate_id() + # Local import to be sure cloud is loaded and setup + # pylint: disable-next=import-outside-toplevel + from homeassistant.components.cloud import ( + async_active_subscription, + async_create_cloudhook, + async_is_connected, + ) - if ( - "cloud" in self.hass.config.components - and self.hass.components.cloud.async_active_subscription() + # Local import to be sure webhook is loaded and setup + # pylint: disable-next=import-outside-toplevel + from homeassistant.components.webhook import ( + async_generate_id, + async_generate_url, + ) + + webhook_id = async_generate_id() + + if "cloud" in self.hass.config.components and async_active_subscription( + self.hass ): - if not self.hass.components.cloud.async_is_connected(): + if not async_is_connected(self.hass): return self.async_abort(reason="cloud_not_connected") - webhook_url = await self.hass.components.cloud.async_create_cloudhook( - webhook_id - ) + webhook_url = await async_create_cloudhook(self.hass, webhook_id) cloudhook = True else: - webhook_url = self.hass.components.webhook.async_generate_url(webhook_id) + webhook_url = async_generate_url(self.hass, webhook_id) cloudhook = False self._description_placeholder["webhook_url"] = webhook_url @@ -260,4 +279,8 @@ async def webhook_async_remove_entry( if not entry.data.get("cloudhook") or "cloud" not in hass.config.components: return - await hass.components.cloud.async_delete_cloudhook(entry.data["webhook_id"]) + # Local import to be sure cloud is loaded and setup + # pylint: disable-next=import-outside-toplevel + from homeassistant.components.cloud import async_delete_cloudhook + + await async_delete_cloudhook(hass, entry.data["webhook_id"]) diff --git a/homeassistant/helpers/config_entry_oauth2_flow.py b/homeassistant/helpers/config_entry_oauth2_flow.py index d99cc1d4f76..caf47432623 100644 --- a/homeassistant/helpers/config_entry_oauth2_flow.py +++ b/homeassistant/helpers/config_entry_oauth2_flow.py @@ -5,6 +5,7 @@ This module exists of the following parts: - OAuth2 implementation that works with local provided client ID/secret """ + from __future__ import annotations from abc import ABC, ABCMeta, abstractmethod @@ -25,7 +26,6 @@ from yarl import URL from homeassistant import config_entries from homeassistant.components import http from homeassistant.core import HomeAssistant, callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.loader import async_get_application_credentials from .aiohttp_client import async_get_clientsession @@ -253,7 +253,7 @@ class AbstractOAuth2FlowHandler(config_entries.ConfigFlow, metaclass=ABCMeta): async def async_step_pick_implementation( self, user_input: dict | None = None - ) -> FlowResult: + ) -> config_entries.ConfigFlowResult: """Handle a flow start.""" implementations = await async_get_implementations(self.hass, self.DOMAIN) @@ -286,7 +286,7 @@ class AbstractOAuth2FlowHandler(config_entries.ConfigFlow, metaclass=ABCMeta): async def async_step_auth( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> config_entries.ConfigFlowResult: """Create an entry for auth.""" # Flow has been triggered by external data if user_input is not None: @@ -314,7 +314,7 @@ class AbstractOAuth2FlowHandler(config_entries.ConfigFlow, metaclass=ABCMeta): async def async_step_creation( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> config_entries.ConfigFlowResult: """Create config entry from external data.""" _LOGGER.debug("Creating config entry from external data") @@ -353,14 +353,18 @@ class AbstractOAuth2FlowHandler(config_entries.ConfigFlow, metaclass=ABCMeta): {"auth_implementation": self.flow_impl.domain, "token": token} ) - async def async_step_authorize_rejected(self, data: None = None) -> FlowResult: + async def async_step_authorize_rejected( + self, data: None = None + ) -> config_entries.ConfigFlowResult: """Step to handle flow rejection.""" return self.async_abort( reason="user_rejected_authorize", description_placeholders={"error": self.external_data["error"]}, ) - async def async_oauth_create_entry(self, data: dict) -> FlowResult: + async def async_oauth_create_entry( + self, data: dict + ) -> config_entries.ConfigFlowResult: """Create an entry for the flow. Ok to override if you want to fetch extra info or even add another step. @@ -369,7 +373,7 @@ class AbstractOAuth2FlowHandler(config_entries.ConfigFlow, metaclass=ABCMeta): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> config_entries.ConfigFlowResult: """Handle a flow start.""" return await self.async_step_pick_implementation(user_input) @@ -435,9 +439,9 @@ def async_add_implementation_provider( If no implementation found, return None. """ - hass.data.setdefault(DATA_PROVIDERS, {})[ - provider_domain - ] = async_provide_implementation + hass.data.setdefault(DATA_PROVIDERS, {})[provider_domain] = ( + async_provide_implementation + ) class OAuth2AuthorizeCallbackView(http.HomeAssistantView): @@ -452,7 +456,7 @@ class OAuth2AuthorizeCallbackView(http.HomeAssistantView): if "state" not in request.query: return web.Response(text="Missing state parameter") - hass = request.app["hass"] + hass = request.app[http.KEY_HASS] state = _decode_jwt(hass, request.query["state"]) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 59e4f09d26f..f7245607be7 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -1,4 +1,5 @@ """Helpers for config validation using voluptuous.""" + from __future__ import annotations from collections.abc import Callable, Hashable @@ -28,6 +29,8 @@ from homeassistant.const import ( ATTR_AREA_ID, ATTR_DEVICE_ID, ATTR_ENTITY_ID, + ATTR_FLOOR_ID, + ATTR_LABEL_ID, CONF_ABOVE, CONF_ALIAS, CONF_ATTRIBUTE, @@ -158,15 +161,15 @@ def path(value: Any) -> str: # https://github.com/alecthomas/voluptuous/issues/115#issuecomment-144464666 def has_at_least_one_key(*keys: Any) -> Callable[[dict], dict]: """Validate that at least one key exists.""" + key_set = set(keys) def validate(obj: dict) -> dict: """Test keys exist in dict.""" if not isinstance(obj, dict): raise vol.Invalid("expected dictionary") - for k in obj: - if k in keys: - return obj + if not key_set.isdisjoint(obj): + return obj expected = ", ".join(str(k) for k in keys) raise vol.Invalid(f"must contain at least one of {expected}.") @@ -281,18 +284,15 @@ def isdir(value: Any) -> str: @overload -def ensure_list(value: None) -> list[Any]: - ... +def ensure_list(value: None) -> list[Any]: ... @overload -def ensure_list(value: list[_T]) -> list[_T]: - ... +def ensure_list(value: list[_T]) -> list[_T]: ... @overload -def ensure_list(value: list[_T] | _T) -> list[_T]: - ... +def ensure_list(value: list[_T] | _T) -> list[_T]: ... def ensure_list(value: _T | None) -> list[_T] | list[Any]: @@ -623,7 +623,7 @@ def string(value: Any) -> str: # This is expected to be the most common case, so check it first. if ( type(value) is str # noqa: E721 - or type(value) is NodeStrClass # noqa: E721 + or type(value) is NodeStrClass or isinstance(value, str) ): return value @@ -904,7 +904,7 @@ def _deprecated_or_removed( try: near = ( f"near {config.__config_file__}" # type: ignore[attr-defined] - f":{config.__line__} " + f":{config.__line__} " # type: ignore[attr-defined] ) except AttributeError: near = "" @@ -1215,6 +1215,12 @@ ENTITY_SERVICE_FIELDS = { vol.Optional(ATTR_AREA_ID): vol.Any( ENTITY_MATCH_NONE, vol.All(ensure_list, [vol.Any(dynamic_template, str)]) ), + vol.Optional(ATTR_FLOOR_ID): vol.Any( + ENTITY_MATCH_NONE, vol.All(ensure_list, [vol.Any(dynamic_template, str)]) + ), + vol.Optional(ATTR_LABEL_ID): vol.Any( + ENTITY_MATCH_NONE, vol.All(ensure_list, [vol.Any(dynamic_template, str)]) + ), } TARGET_SERVICE_FIELDS = { @@ -1232,12 +1238,19 @@ TARGET_SERVICE_FIELDS = { vol.Optional(ATTR_AREA_ID): vol.Any( ENTITY_MATCH_NONE, vol.All(ensure_list, [vol.Any(dynamic_template, str)]) ), + vol.Optional(ATTR_FLOOR_ID): vol.Any( + ENTITY_MATCH_NONE, vol.All(ensure_list, [vol.Any(dynamic_template, str)]) + ), + vol.Optional(ATTR_LABEL_ID): vol.Any( + ENTITY_MATCH_NONE, vol.All(ensure_list, [vol.Any(dynamic_template, str)]) + ), } -def make_entity_service_schema( - schema: dict, *, extra: int = vol.PREVENT_EXTRA -) -> vol.Schema: +_HAS_ENTITY_SERVICE_FIELD = has_at_least_one_key(*ENTITY_SERVICE_FIELDS) + + +def _make_entity_service_schema(schema: dict, extra: int) -> vol.Schema: """Create an entity service schema.""" return vol.Schema( vol.All( @@ -1250,11 +1263,26 @@ def make_entity_service_schema( }, extra=extra, ), - has_at_least_one_key(*ENTITY_SERVICE_FIELDS), + _HAS_ENTITY_SERVICE_FIELD, ) ) +BASE_ENTITY_SCHEMA = _make_entity_service_schema({}, vol.PREVENT_EXTRA) + + +def make_entity_service_schema( + schema: dict, *, extra: int = vol.PREVENT_EXTRA +) -> vol.Schema: + """Create an entity service schema.""" + if not schema and extra == vol.PREVENT_EXTRA: + # If the schema is empty and we don't allow extra keys, we can return + # the base schema and avoid compiling a new schema which is the case + # for ~50% of services. + return BASE_ENTITY_SCHEMA + return _make_entity_service_schema(schema, extra) + + SCRIPT_CONVERSATION_RESPONSE_SCHEMA = vol.Any(template, None) @@ -1322,7 +1350,8 @@ SERVICE_SCHEMA = vol.All( ) NUMERIC_STATE_THRESHOLD_SCHEMA = vol.Any( - vol.Coerce(float), vol.All(str, entity_domain(["input_number", "number", "sensor"])) + vol.Coerce(float), + vol.All(str, entity_domain(["input_number", "number", "sensor", "zone"])), ) CONDITION_BASE_SCHEMA = { @@ -1797,54 +1826,36 @@ SCRIPT_ACTION_WAIT_FOR_TRIGGER = "wait_for_trigger" SCRIPT_ACTION_WAIT_TEMPLATE = "wait_template" +ACTIONS_MAP = { + CONF_DELAY: SCRIPT_ACTION_DELAY, + CONF_WAIT_TEMPLATE: SCRIPT_ACTION_WAIT_TEMPLATE, + CONF_CONDITION: SCRIPT_ACTION_CHECK_CONDITION, + "and": SCRIPT_ACTION_CHECK_CONDITION, + "or": SCRIPT_ACTION_CHECK_CONDITION, + "not": SCRIPT_ACTION_CHECK_CONDITION, + CONF_EVENT: SCRIPT_ACTION_FIRE_EVENT, + CONF_DEVICE_ID: SCRIPT_ACTION_DEVICE_AUTOMATION, + CONF_SCENE: SCRIPT_ACTION_ACTIVATE_SCENE, + CONF_REPEAT: SCRIPT_ACTION_REPEAT, + CONF_CHOOSE: SCRIPT_ACTION_CHOOSE, + CONF_WAIT_FOR_TRIGGER: SCRIPT_ACTION_WAIT_FOR_TRIGGER, + CONF_VARIABLES: SCRIPT_ACTION_VARIABLES, + CONF_IF: SCRIPT_ACTION_IF, + CONF_SERVICE: SCRIPT_ACTION_CALL_SERVICE, + CONF_SERVICE_TEMPLATE: SCRIPT_ACTION_CALL_SERVICE, + CONF_STOP: SCRIPT_ACTION_STOP, + CONF_PARALLEL: SCRIPT_ACTION_PARALLEL, + CONF_SET_CONVERSATION_RESPONSE: SCRIPT_ACTION_SET_CONVERSATION_RESPONSE, +} + +ACTIONS_SET = set(ACTIONS_MAP) + + def determine_script_action(action: dict[str, Any]) -> str: """Determine action type.""" - if CONF_DELAY in action: - return SCRIPT_ACTION_DELAY - - if CONF_WAIT_TEMPLATE in action: - return SCRIPT_ACTION_WAIT_TEMPLATE - - if any(key in action for key in (CONF_CONDITION, "and", "or", "not")): - return SCRIPT_ACTION_CHECK_CONDITION - - if CONF_EVENT in action: - return SCRIPT_ACTION_FIRE_EVENT - - if CONF_DEVICE_ID in action: - return SCRIPT_ACTION_DEVICE_AUTOMATION - - if CONF_SCENE in action: - return SCRIPT_ACTION_ACTIVATE_SCENE - - if CONF_REPEAT in action: - return SCRIPT_ACTION_REPEAT - - if CONF_CHOOSE in action: - return SCRIPT_ACTION_CHOOSE - - if CONF_WAIT_FOR_TRIGGER in action: - return SCRIPT_ACTION_WAIT_FOR_TRIGGER - - if CONF_VARIABLES in action: - return SCRIPT_ACTION_VARIABLES - - if CONF_IF in action: - return SCRIPT_ACTION_IF - - if CONF_SERVICE in action or CONF_SERVICE_TEMPLATE in action: - return SCRIPT_ACTION_CALL_SERVICE - - if CONF_STOP in action: - return SCRIPT_ACTION_STOP - - if CONF_PARALLEL in action: - return SCRIPT_ACTION_PARALLEL - - if CONF_SET_CONVERSATION_RESPONSE in action: - return SCRIPT_ACTION_SET_CONVERSATION_RESPONSE - - raise ValueError("Unable to determine action") + if not (actions := ACTIONS_SET.intersection(action)): + raise ValueError("Unable to determine action") + return ACTIONS_MAP[actions.pop()] ACTION_TYPE_SCHEMAS: dict[str, Callable[[Any], dict]] = { diff --git a/homeassistant/helpers/data_entry_flow.py b/homeassistant/helpers/data_entry_flow.py index 695fbbf7633..1edeb28d88f 100644 --- a/homeassistant/helpers/data_entry_flow.py +++ b/homeassistant/helpers/data_entry_flow.py @@ -1,10 +1,12 @@ """Helpers for the data entry flow.""" + from __future__ import annotations from http import HTTPStatus -from typing import Any +from typing import Any, Generic from aiohttp import web +from typing_extensions import TypeVar import voluptuous as vol import voluptuous_serialize @@ -14,11 +16,17 @@ from homeassistant.components.http.data_validator import RequestDataValidator from . import config_validation as cv +_FlowManagerT = TypeVar( + "_FlowManagerT", + bound="data_entry_flow.FlowManager[Any]", + default=data_entry_flow.FlowManager, +) -class _BaseFlowManagerView(HomeAssistantView): + +class _BaseFlowManagerView(HomeAssistantView, Generic[_FlowManagerT]): """Foundation for flow manager views.""" - def __init__(self, flow_mgr: data_entry_flow.FlowManager) -> None: + def __init__(self, flow_mgr: _FlowManagerT) -> None: """Initialize the flow manager index view.""" self._flow_mgr = flow_mgr @@ -48,36 +56,39 @@ class _BaseFlowManagerView(HomeAssistantView): return data -class FlowManagerIndexView(_BaseFlowManagerView): +class FlowManagerIndexView(_BaseFlowManagerView[_FlowManagerT]): """View to create config flows.""" @RequestDataValidator( vol.Schema( { - vol.Required("handler"): vol.Any(str, list), + vol.Required("handler"): str, vol.Optional("show_advanced_options", default=False): cv.boolean, }, extra=vol.ALLOW_EXTRA, ) ) async def post(self, request: web.Request, data: dict[str, Any]) -> web.Response: - """Handle a POST request.""" - if isinstance(data["handler"], list): - handler = tuple(data["handler"]) - else: - handler = data["handler"] + """Initialize a POST request. + Override `_post_impl` in subclasses which need + to implement their own `RequestDataValidator` + """ + return await self._post_impl(request, data) + + async def _post_impl( + self, request: web.Request, data: dict[str, Any] + ) -> web.Response: + """Handle a POST request.""" try: result = await self._flow_mgr.async_init( - handler, # type: ignore[arg-type] + data["handler"], context=self.get_context(data), ) 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 - ) + except data_entry_flow.UnknownStep as err: + return self.json_message(str(err), HTTPStatus.BAD_REQUEST) result = self._prepare_result_json(result) @@ -88,7 +99,7 @@ class FlowManagerIndexView(_BaseFlowManagerView): return {"show_advanced_options": data["show_advanced_options"]} -class FlowManagerResourceView(_BaseFlowManagerView): +class FlowManagerResourceView(_BaseFlowManagerView[_FlowManagerT]): """View to interact with the flow manager.""" async def get(self, request: web.Request, /, flow_id: str) -> web.Response: diff --git a/homeassistant/helpers/debounce.py b/homeassistant/helpers/debounce.py index 298d20485a0..de8f5eb4d53 100644 --- a/homeassistant/helpers/debounce.py +++ b/homeassistant/helpers/debounce.py @@ -1,4 +1,5 @@ """Debounce helper.""" + from __future__ import annotations import asyncio @@ -22,6 +23,7 @@ class Debouncer(Generic[_R_co]): cooldown: float, immediate: bool, function: Callable[[], _R_co] | None = None, + background: bool = False, ) -> None: """Initialize debounce. @@ -37,6 +39,7 @@ class Debouncer(Generic[_R_co]): self._timer_task: asyncio.TimerHandle | None = None self._execute_at_end_of_timer: bool = False self._execute_lock = asyncio.Lock() + self._background = background self._job: HassJob[[], _R_co] | None = ( None if function is None @@ -108,7 +111,9 @@ class Debouncer(Generic[_R_co]): assert self._job is not None try: - if task := self.hass.async_run_hass_job(self._job): + if task := self.hass.async_run_hass_job( + self._job, background=self._background + ): await task finally: self._schedule_timer() @@ -129,7 +134,9 @@ class Debouncer(Generic[_R_co]): return try: - if task := self.hass.async_run_hass_job(self._job): + if task := self.hass.async_run_hass_job( + self._job, background=self._background + ): await task except Exception: # pylint: disable=broad-except self.logger.exception("Unexpected exception from %s", self.function) @@ -156,13 +163,18 @@ class Debouncer(Generic[_R_co]): def _on_debounce(self) -> None: """Create job task, but only if pending.""" self._timer_task = None - if self._execute_at_end_of_timer: - self._execute_at_end_of_timer = False + if not self._execute_at_end_of_timer: + return + self._execute_at_end_of_timer = False + name = f"debouncer {self._job} finish cooldown={self.cooldown}, immediate={self.immediate}" + if not self._background: self.hass.async_create_task( - self._handle_timer_finish(), - f"debouncer {self._job} finish cooldown={self.cooldown}, immediate={self.immediate}", - eager_start=True, + self._handle_timer_finish(), name, eager_start=True ) + return + self.hass.async_create_background_task( + self._handle_timer_finish(), name, eager_start=True + ) @callback def _schedule_timer(self) -> None: diff --git a/homeassistant/helpers/deprecation.py b/homeassistant/helpers/deprecation.py index cf76bc78aa5..6e70bbc7635 100644 --- a/homeassistant/helpers/deprecation.py +++ b/homeassistant/helpers/deprecation.py @@ -1,4 +1,5 @@ """Deprecation helpers for Home Assistant.""" + from __future__ import annotations from collections.abc import Callable @@ -277,7 +278,7 @@ def check_if_deprecated_constant(name: str, module_globals: dict[str, Any]) -> A # specifies that __getattr__ should raise AttributeError if the attribute is not # found. # https://peps.python.org/pep-0562/#specification - raise AttributeError(msg) # noqa: TRY004 + raise AttributeError(msg) _print_deprecation_warning_internal( name, diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 826a4cc200e..9666ad302ad 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -1,4 +1,5 @@ """Provide a way to connect entities belonging to one device.""" + from __future__ import annotations from collections import UserDict @@ -8,7 +9,6 @@ from functools import lru_cache, partial import logging import time from typing import TYPE_CHECKING, Any, Literal, TypedDict, TypeVar, cast -from urllib.parse import urlparse import attr from yarl import URL @@ -30,7 +30,8 @@ from .deprecation import ( dir_with_deprecated_constants, ) from .frame import report -from .json import JSON_DUMP, find_paths_unserializable_data, json_bytes +from .json import JSON_DUMP, find_paths_unserializable_data, json_bytes, json_fragment +from .registry import BaseRegistry from .typing import UNDEFINED, UndefinedType if TYPE_CHECKING: @@ -45,7 +46,7 @@ EVENT_DEVICE_REGISTRY_UPDATED = "device_registry_updated" STORAGE_KEY = "core.device_registry" STORAGE_VERSION_MAJOR = 1 STORAGE_VERSION_MINOR = 5 -SAVE_DELAY = 10 + CLEANUP_DELAY = 10 CONNECTION_BLUETOOTH = "bluetooth" @@ -210,22 +211,22 @@ def _validate_device_info( return device_info_type +_cached_parse_url = lru_cache(maxsize=512)(URL) +"""Parse a URL and cache the result.""" + + def _validate_configuration_url(value: Any) -> str | None: """Validate and convert configuration_url.""" if value is None: return None - if ( - isinstance(value, URL) - and (value.scheme not in CONFIGURATION_URL_SCHEMES or not value.host) - ) or ( - (parsed_url := urlparse(str(value))) - and ( - parsed_url.scheme not in CONFIGURATION_URL_SCHEMES - or not parsed_url.hostname - ) - ): + + url_as_str = str(value) + url = value if type(value) is URL else _cached_parse_url(url_as_str) + + if url.scheme not in CONFIGURATION_URL_SCHEMES or not url.host: raise ValueError(f"invalid configuration_url '{value}'") - return str(value) + + return url_as_str @attr.s(frozen=True) @@ -261,6 +262,9 @@ class DeviceEntry: @property def dict_repr(self) -> dict[str, Any]: """Return a dict representation of the entry.""" + # Convert sets and tuples to lists + # so the JSON serializer does not have to do + # it every time return { "area_id": self.area_id, "configuration_url": self.configuration_url, @@ -271,6 +275,7 @@ class DeviceEntry: "hw_version": self.hw_version, "id": self.id, "identifiers": list(self.identifiers), + "labels": list(self.labels), "manufacturer": self.manufacturer, "model": self.model, "name_by_user": self.name_by_user, @@ -296,8 +301,35 @@ class DeviceEntry: ) return None + @cached_property + def as_storage_fragment(self) -> json_fragment: + """Return a json fragment for storage.""" + return json_fragment( + json_bytes( + { + "area_id": self.area_id, + "config_entries": list(self.config_entries), + "configuration_url": self.configuration_url, + "connections": list(self.connections), + "disabled_by": self.disabled_by, + "entry_type": self.entry_type, + "hw_version": self.hw_version, + "id": self.id, + "identifiers": list(self.identifiers), + "labels": list(self.labels), + "manufacturer": self.manufacturer, + "model": self.model, + "name_by_user": self.name_by_user, + "name": self.name, + "serial_number": self.serial_number, + "sw_version": self.sw_version, + "via_device_id": self.via_device_id, + } + ) + ) -@attr.s(slots=True, frozen=True) + +@attr.s(frozen=True) class DeletedDeviceEntry: """Deleted Device Registry Entry.""" @@ -323,6 +355,21 @@ class DeletedDeviceEntry: is_new=True, ) + @cached_property + def as_storage_fragment(self) -> json_fragment: + """Return a json fragment for storage.""" + return json_fragment( + json_bytes( + { + "config_entries": list(self.config_entries), + "connections": list(self.connections), + "identifiers": list(self.identifiers), + "id": self.id, + "orphaned_timestamp": self.orphaned_timestamp, + } + ) + ) + @lru_cache(maxsize=512) def format_mac(mac: str) -> str: @@ -456,7 +503,7 @@ class DeviceRegistryItems(UserDict[str, _EntryTypeT]): return None -class DeviceRegistry: +class DeviceRegistry(BaseRegistry): """Class to hold a registry of devices.""" devices: DeviceRegistryItems[DeviceEntry] @@ -708,7 +755,7 @@ class DeviceRegistry: config_entries = old.config_entries if merge_identifiers is not UNDEFINED and new_identifiers is not UNDEFINED: - raise HomeAssistantError() + raise HomeAssistantError if isinstance(disabled_by, str) and not isinstance( disabled_by, DeviceEntryDisabler @@ -899,49 +946,14 @@ class DeviceRegistry: self._device_data = devices.data @callback - def async_schedule_save(self) -> None: - """Schedule saving the device registry.""" - self._store.async_delay_save(self._data_to_save, SAVE_DELAY) - - @callback - def _data_to_save(self) -> dict[str, list[dict[str, Any]]]: + def _data_to_save(self) -> dict[str, Any]: """Return data of device registry to store in a file.""" - data: dict[str, list[dict[str, Any]]] = {} - - data["devices"] = [ - { - "area_id": entry.area_id, - "config_entries": list(entry.config_entries), - "configuration_url": entry.configuration_url, - "connections": list(entry.connections), - "disabled_by": entry.disabled_by, - "entry_type": entry.entry_type, - "hw_version": entry.hw_version, - "id": entry.id, - "identifiers": list(entry.identifiers), - "labels": list(entry.labels), - "manufacturer": entry.manufacturer, - "model": entry.model, - "name_by_user": entry.name_by_user, - "name": entry.name, - "serial_number": entry.serial_number, - "sw_version": entry.sw_version, - "via_device_id": entry.via_device_id, - } - for entry in self.devices.values() - ] - data["deleted_devices"] = [ - { - "config_entries": list(entry.config_entries), - "connections": list(entry.connections), - "identifiers": list(entry.identifiers), - "id": entry.id, - "orphaned_timestamp": entry.orphaned_timestamp, - } - for entry in self.deleted_devices.values() - ] - - return data + return { + "devices": [entry.as_storage_fragment for entry in self.devices.values()], + "deleted_devices": [ + entry.as_storage_fragment for entry in self.deleted_devices.values() + ], + } @callback def async_clear_config_entry(self, config_entry_id: str) -> None: @@ -1090,7 +1102,7 @@ def async_cleanup( ) -> None: """Clean up device registry.""" # Find all devices that are referenced by a config_entry. - config_entry_ids = {entry.entry_id for entry in hass.config_entries.async_entries()} + config_entry_ids = set(hass.config_entries.async_entry_ids()) references_config_entries = { device.id for device in dev_reg.devices.values() @@ -1099,9 +1111,13 @@ def async_cleanup( } # Find all devices that are referenced in the entity registry. - references_entities = {entry.device_id for entry in ent_reg.entities.values()} + device_ids_referenced_by_entities = set(ent_reg.entities.get_device_ids()) - orphan = set(dev_reg.devices) - references_entities - references_config_entries + orphan = ( + set(dev_reg.devices) + - device_ids_referenced_by_entities + - references_config_entries + ) for dev_id in orphan: dev_reg.async_remove_device(dev_id) @@ -1128,10 +1144,10 @@ def async_setup_cleanup(hass: HomeAssistant, dev_reg: DeviceRegistry) -> None: @callback def _label_removed_from_registry_filter( - event: lr.EventLabelRegistryUpdated, + event_data: lr.EventLabelRegistryUpdatedData, ) -> bool: """Filter all except for the remove action from label registry events.""" - return event.data["action"] == "remove" + return event_data["action"] == "remove" @callback def _handle_label_registry_update(event: lr.EventLabelRegistryUpdated) -> None: @@ -1140,8 +1156,9 @@ def async_setup_cleanup(hass: HomeAssistant, dev_reg: DeviceRegistry) -> None: hass.bus.async_listen( event_type=lr.EVENT_LABEL_REGISTRY_UPDATED, - event_filter=_label_removed_from_registry_filter, # type: ignore[arg-type] - listener=_handle_label_registry_update, # type: ignore[arg-type] + event_filter=_label_removed_from_registry_filter, + listener=_handle_label_registry_update, + run_immediately=True, ) @callback @@ -1160,12 +1177,12 @@ def async_setup_cleanup(hass: HomeAssistant, dev_reg: DeviceRegistry) -> None: debounced_cleanup.async_schedule_call() @callback - def entity_registry_changed_filter(event: Event) -> bool: + def entity_registry_changed_filter(event_data: Mapping[str, Any]) -> bool: """Handle entity updated or removed filter.""" if ( - event.data["action"] == "update" - and "device_id" not in event.data["changes"] - ) or event.data["action"] == "create": + event_data["action"] == "update" + and "device_id" not in event_data["changes"] + ) or event_data["action"] == "create": return False return True @@ -1175,6 +1192,7 @@ def async_setup_cleanup(hass: HomeAssistant, dev_reg: DeviceRegistry) -> None: entity_registry.EVENT_ENTITY_REGISTRY_UPDATED, _async_entity_registry_changed, event_filter=entity_registry_changed_filter, + run_immediately=True, ) return @@ -1184,10 +1202,13 @@ def async_setup_cleanup(hass: HomeAssistant, dev_reg: DeviceRegistry) -> None: entity_registry.EVENT_ENTITY_REGISTRY_UPDATED, _async_entity_registry_changed, event_filter=entity_registry_changed_filter, + run_immediately=True, ) await debounced_cleanup.async_call() - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, startup_clean) + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STARTED, startup_clean, run_immediately=True + ) @callback def _on_homeassistant_stop(event: Event) -> None: diff --git a/homeassistant/helpers/discovery.py b/homeassistant/helpers/discovery.py index 7045966c529..4b5a0117be7 100644 --- a/homeassistant/helpers/discovery.py +++ b/homeassistant/helpers/discovery.py @@ -5,6 +5,7 @@ There are two different types of discoveries that can be fired/listened for. - listen_platform/discover_platform is for platforms. These are used by components to allow discovery of their platforms. """ + from __future__ import annotations from collections.abc import Callable, Coroutine @@ -14,10 +15,13 @@ from homeassistant import core, setup from homeassistant.const import Platform from homeassistant.loader import bind_hass +from ..util.signal_type import SignalTypeFormat from .dispatcher import async_dispatcher_connect, async_dispatcher_send from .typing import ConfigType, DiscoveryInfoType -SIGNAL_PLATFORM_DISCOVERED = "discovery.platform_discovered_{}" +SIGNAL_PLATFORM_DISCOVERED: SignalTypeFormat[DiscoveryDict] = SignalTypeFormat( + "discovery.platform_discovered_{}" +) EVENT_LOAD_PLATFORM = "load_platform.{}" ATTR_PLATFORM = "platform" ATTR_DISCOVERED = "discovered" diff --git a/homeassistant/helpers/discovery_flow.py b/homeassistant/helpers/discovery_flow.py index c4698de1f52..e24b405c685 100644 --- a/homeassistant/helpers/discovery_flow.py +++ b/homeassistant/helpers/discovery_flow.py @@ -1,12 +1,13 @@ """The discovery flow helper.""" + from __future__ import annotations from collections.abc import Coroutine from typing import Any, NamedTuple +from homeassistant.config_entries import ConfigFlowResult from homeassistant.const import EVENT_HOMEASSISTANT_STARTED from homeassistant.core import CoreState, Event, HomeAssistant, callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.loader import bind_hass from homeassistant.util.async_ import gather_with_limited_concurrency @@ -40,7 +41,7 @@ def async_create_flow( @callback def _async_init_flow( hass: HomeAssistant, domain: str, context: dict[str, Any], data: Any -) -> Coroutine[None, None, FlowResult] | None: +) -> Coroutine[None, None, ConfigFlowResult] | None: """Create a discovery flow.""" # Avoid spawning flows that have the same initial discovery data # as ones in progress as it may cause additional device probing @@ -81,7 +82,9 @@ class FlowDispatcher: @callback def async_setup(self) -> None: """Set up the flow disptcher.""" - self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, self._async_start) + self.hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STARTED, self._async_start, run_immediately=True + ) async def _async_start(self, event: Event) -> None: """Start processing pending flows.""" diff --git a/homeassistant/helpers/dispatcher.py b/homeassistant/helpers/dispatcher.py index 59d680a60ee..c1194c7da01 100644 --- a/homeassistant/helpers/dispatcher.py +++ b/homeassistant/helpers/dispatcher.py @@ -1,44 +1,26 @@ """Helpers for Home Assistant dispatcher & internal component/platform.""" + from __future__ import annotations from collections.abc import Callable, Coroutine -from dataclasses import dataclass from functools import partial import logging -from typing import Any, Generic, TypeVarTuple, overload +from typing import Any, TypeVarTuple, overload from homeassistant.core import HassJob, HomeAssistant, callback from homeassistant.loader import bind_hass from homeassistant.util.async_ import run_callback_threadsafe from homeassistant.util.logging import catch_log_exception +# Explicit reexport of 'SignalType' for backwards compatibility +from homeassistant.util.signal_type import SignalType as SignalType # noqa: PLC0414 + _Ts = TypeVarTuple("_Ts") _LOGGER = logging.getLogger(__name__) DATA_DISPATCHER = "dispatcher" -@dataclass(frozen=True) -class SignalType(Generic[*_Ts]): - """Generic string class for signal to improve typing.""" - - name: str - - def __hash__(self) -> int: - """Return hash of name.""" - - return hash(self.name) - - def __eq__(self, other: Any) -> bool: - """Check equality for dict keys to be compatible with str.""" - - if isinstance(other, str): - return self.name == other - if isinstance(other, SignalType): - return self.name == other.name - return False - - _DispatcherDataType = dict[ SignalType[*_Ts] | str, dict[ @@ -52,16 +34,14 @@ _DispatcherDataType = dict[ @bind_hass def dispatcher_connect( hass: HomeAssistant, signal: SignalType[*_Ts], target: Callable[[*_Ts], None] -) -> Callable[[], None]: - ... +) -> Callable[[], None]: ... @overload @bind_hass def dispatcher_connect( hass: HomeAssistant, signal: str, target: Callable[..., None] -) -> Callable[[], None]: - ... +) -> Callable[[], None]: ... @bind_hass # type: ignore[misc] # workaround; exclude typing of 2 overload in func def @@ -107,8 +87,7 @@ def _async_remove_dispatcher( @bind_hass def async_dispatcher_connect( hass: HomeAssistant, signal: SignalType[*_Ts], target: Callable[[*_Ts], Any] -) -> Callable[[], None]: - ... +) -> Callable[[], None]: ... @overload @@ -116,8 +95,7 @@ def async_dispatcher_connect( @bind_hass def async_dispatcher_connect( hass: HomeAssistant, signal: str, target: Callable[..., Any] -) -> Callable[[], None]: - ... +) -> Callable[[], None]: ... @callback @@ -149,14 +127,14 @@ def async_dispatcher_connect( @overload @bind_hass -def dispatcher_send(hass: HomeAssistant, signal: SignalType[*_Ts], *args: *_Ts) -> None: - ... +def dispatcher_send( + hass: HomeAssistant, signal: SignalType[*_Ts], *args: *_Ts +) -> None: ... @overload @bind_hass -def dispatcher_send(hass: HomeAssistant, signal: str, *args: Any) -> None: - ... +def dispatcher_send(hass: HomeAssistant, signal: str, *args: Any) -> None: ... @bind_hass # type: ignore[misc] # workaround; exclude typing of 2 overload in func def @@ -194,15 +172,13 @@ def _generate_job( @bind_hass def async_dispatcher_send( hass: HomeAssistant, signal: SignalType[*_Ts], *args: *_Ts -) -> None: - ... +) -> None: ... @overload @callback @bind_hass -def async_dispatcher_send(hass: HomeAssistant, signal: str, *args: Any) -> None: - ... +def async_dispatcher_send(hass: HomeAssistant, signal: str, *args: Any) -> None: ... @callback diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 3517d41314b..988ce29ade2 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -1,4 +1,5 @@ """An abstract class for entities.""" + from __future__ import annotations from abc import ABCMeta @@ -10,6 +11,7 @@ from enum import Enum, IntFlag, auto import functools as ft import logging import math +from operator import attrgetter import sys from timeit import default_timer as timer from types import FunctionType @@ -46,8 +48,11 @@ from homeassistant.const import ( from homeassistant.core import ( CALLBACK_TYPE, Context, + Event, + HassJobType, HomeAssistant, callback, + get_hassjob_callable_job_type, get_release_channel, ) from homeassistant.exceptions import ( @@ -65,7 +70,7 @@ from .event import ( async_track_device_registry_updated_event, async_track_entity_registry_updated_event, ) -from .typing import UNDEFINED, EventType, StateType, UndefinedType +from .typing import UNDEFINED, StateType, UndefinedType if TYPE_CHECKING: from functools import cached_property @@ -327,25 +332,12 @@ class CachedProperties(type): Raises AttributeError if the __attr_ attribute does not exist """ # Invalidate the cache of the cached property - try: # noqa: SIM105 suppress is much slower - delattr(o, name) - except AttributeError: - pass + o.__dict__.pop(name, None) # Delete the __attr_ attribute delattr(o, private_attr_name) return _deleter - def getter(name: str) -> Callable[[Any], Any]: - """Create a getter for an _attr_ property.""" - private_attr_name = f"__attr_{name}" - - def _getter(o: Any) -> Any: - """Get an _attr_ property from the backing __attr attribute.""" - return getattr(o, private_attr_name) - - return _getter - def setter(name: str) -> Callable[[Any, Any], None]: """Create a setter for an _attr_ property.""" private_attr_name = f"__attr_{name}" @@ -359,16 +351,16 @@ class CachedProperties(type): if getattr(o, private_attr_name, _SENTINEL) == val: return setattr(o, private_attr_name, val) - try: # noqa: SIM105 suppress is much slower - delattr(o, name) - except AttributeError: - pass + # Invalidate the cache of the cached property + o.__dict__.pop(name, None) return _setter def make_property(name: str) -> property: """Help create a property object.""" - return property(fget=getter(name), fset=setter(name), fdel=deleter(name)) + return property( + fget=attrgetter(f"__attr_{name}"), fset=setter(name), fdel=deleter(name) + ) def wrap_attr(cls: CachedProperties, property_name: str) -> None: """Wrap a cached property's corresponding _attr in a property. @@ -524,6 +516,8 @@ class Entity( __combined_unrecorded_attributes: frozenset[str] = ( _entity_component_unrecorded_attributes | _unrecorded_attributes ) + # Job type cache + _job_types: dict[str, HassJobType] | None = None # StateInfo. Set by EntityPlatform by calling async_internal_added_to_hass # While not purely typed, it makes typehinting more useful for us @@ -565,6 +559,20 @@ class Entity( cls._entity_component_unrecorded_attributes | cls._unrecorded_attributes ) + def get_hassjob_type(self, function_name: str) -> HassJobType: + """Get the job type function for the given name. + + This is used for entity service calls to avoid + figuring out the job type each time. + """ + if not self._job_types: + self._job_types = {} + if function_name not in self._job_types: + self._job_types[function_name] = get_hassjob_callable_job_type( + getattr(self, function_name) + ) + return self._job_types[function_name] + @cached_property def should_poll(self) -> bool: """Return True if entity has to be polled for state. @@ -607,7 +615,7 @@ class Entity( def _device_class_name_helper( self, - component_translations: dict[str, Any], + component_translations: dict[str, str], ) -> str | None: """Return a translated name of the entity based on its device class.""" if not self.has_entity_name: @@ -672,7 +680,7 @@ class Entity( def _name_internal( self, device_class_name: str | None, - platform_translations: dict[str, Any], + platform_translations: dict[str, str], ) -> str | UndefinedType | None: """Return the name of the entity.""" if hasattr(self, "_attr_name"): @@ -682,8 +690,6 @@ class Entity( and (name_translation_key := self._name_translation_key) and (name := platform_translations.get(name_translation_key)) ): - if TYPE_CHECKING: - assert isinstance(name, str) return self._substitute_name_placeholders(name) if hasattr(self, "entity_description"): description_name = self.entity_description.name @@ -951,7 +957,7 @@ class Entity( _LOGGER.warning( ( "Entity %s (%s) is using self.async_update_ha_state(), without" - " enabling force_update. Instead it should use" + " enabling force_refresh. Instead it should use" " self.async_write_ha_state(), please %s" ), self.entity_id, @@ -1430,7 +1436,10 @@ class Entity( self.async_on_remove( async_track_entity_registry_updated_event( - self.hass, self.entity_id, self._async_registry_updated + self.hass, + self.entity_id, + self._async_registry_updated, + job_type=HassJobType.Callback, ) ) self._async_subscribe_device_updates() @@ -1449,7 +1458,7 @@ class Entity( @callback def _async_registry_updated( - self, event: EventType[er.EventEntityRegistryUpdatedData] + self, event: Event[er.EventEntityRegistryUpdatedData] ) -> None: """Handle entity registry update.""" action = event.data["action"] @@ -1461,7 +1470,7 @@ class Entity( ) async def _async_process_registry_update_or_remove( - self, event: EventType[er.EventEntityRegistryUpdatedData] + self, event: Event[er.EventEntityRegistryUpdatedData] ) -> None: """Handle entity registry update or remove.""" data = event.data @@ -1514,7 +1523,7 @@ class Entity( @callback def _async_device_registry_updated( - self, event: EventType[EventDeviceRegistryUpdatedData] + self, event: Event[EventDeviceRegistryUpdatedData] ) -> None: """Handle device registry update.""" data = event.data @@ -1545,6 +1554,7 @@ class Entity( self.hass, device_id, self._async_device_registry_updated, + job_type=HassJobType.Callback, ) if ( not self._on_remove @@ -1638,7 +1648,7 @@ class ToggleEntity( def turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" - raise NotImplementedError() + raise NotImplementedError async def async_turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" @@ -1646,7 +1656,7 @@ class ToggleEntity( def turn_off(self, **kwargs: Any) -> None: """Turn the entity off.""" - raise NotImplementedError() + raise NotImplementedError async def async_turn_off(self, **kwargs: Any) -> None: """Turn the entity off.""" diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index 389dd69900a..b764a29a686 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -1,4 +1,5 @@ """Helpers for components that manage entities.""" + from __future__ import annotations import asyncio @@ -22,6 +23,7 @@ from homeassistant.const import ( from homeassistant.core import ( Event, HassJob, + HassJobType, HomeAssistant, ServiceCall, ServiceResponse, @@ -118,7 +120,9 @@ class EntityComponent(Generic[_EntityT]): Note: this is only required if the integration never calls `setup` or `async_setup`. """ - self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self._async_shutdown) + self.hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, self._async_shutdown, run_immediately=True + ) def setup(self, config: ConfigType) -> None: """Set up a full entity component. @@ -152,16 +156,16 @@ class EntityComponent(Generic[_EntityT]): # Generic discovery listener for loading platform dynamically # Refer to: homeassistant.helpers.discovery.async_load_platform() - async def component_platform_discovered( - platform: str, info: dict[str, Any] | None - ) -> None: - """Handle the loading of a platform.""" - await self.async_setup_platform(platform, {}, info) - discovery.async_listen_platform( - self.hass, self.domain, component_platform_discovered + self.hass, self.domain, self._async_component_platform_discovered ) + async def _async_component_platform_discovered( + self, platform: str, info: dict[str, Any] | None + ) -> None: + """Handle the loading of a platform.""" + await self.async_setup_platform(platform, {}, info) + async def async_setup_entry(self, config_entry: ConfigEntry) -> bool: """Set up a config entry.""" platform_type = config_entry.domain @@ -278,6 +282,7 @@ class EntityComponent(Generic[_EntityT]): ), schema, supports_response, + job_type=HassJobType.Coroutinefunction, ) async def async_setup_platform( diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 3a441e75e84..1cff472af72 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -1,4 +1,5 @@ """Class to manage the entities for a single platform.""" + from __future__ import annotations import asyncio @@ -31,7 +32,7 @@ from homeassistant.core import ( ) from homeassistant.exceptions import HomeAssistantError, PlatformNotReady from homeassistant.generated import languages -from homeassistant.setup import async_start_setup +from homeassistant.setup import SetupPhases, async_start_setup from homeassistant.util.async_ import create_eager_task from . import ( @@ -129,10 +130,10 @@ class EntityPlatform: # Storage for entities for this specific platform only # which are indexed by entity_id self.entities: dict[str, Entity] = {} - self.component_translations: dict[str, Any] = {} - self.platform_translations: dict[str, Any] = {} - self.object_id_component_translations: dict[str, Any] = {} - self.object_id_platform_translations: dict[str, Any] = {} + self.component_translations: dict[str, str] = {} + self.platform_translations: dict[str, str] = {} + self.object_id_component_translations: dict[str, str] = {} + self.object_id_platform_translations: dict[str, str] = {} self._tasks: list[asyncio.Task[None]] = [] # Stop tracking tasks after setup is completed self._setup_complete = False @@ -162,9 +163,9 @@ class EntityPlatform: # with the child dict indexed by entity_id # # This is usually media_player.yamaha, light.hue, switch.tplink, etc. - domain_platform_entities: dict[ - tuple[str, str], dict[str, Entity] - ] = hass.data.setdefault(DATA_DOMAIN_PLATFORM_ENTITIES, {}) + domain_platform_entities: dict[tuple[str, str], dict[str, Entity]] = ( + hass.data.setdefault(DATA_DOMAIN_PLATFORM_ENTITIES, {}) + ) key = (domain, platform_name) self.domain_platform_entities = domain_platform_entities.setdefault(key, {}) @@ -283,7 +284,13 @@ class EntityPlatform: discovery_info, ) - await self._async_setup_platform(async_create_setup_awaitable) + with async_start_setup( + hass, + integration=self.platform_name, + group=str(id(platform_config)), + phase=SetupPhases.PLATFORM_SETUP, + ): + await self._async_setup_platform(async_create_setup_awaitable) @callback def async_shutdown(self) -> None: @@ -340,85 +347,82 @@ class EntityPlatform: self.platform_name, SLOW_SETUP_WARNING, ) - with async_start_setup(hass, [full_name]): - try: - awaitable = async_create_setup_awaitable() - if asyncio.iscoroutine(awaitable): - awaitable = create_eager_task(awaitable) + try: + awaitable = async_create_setup_awaitable() + if asyncio.iscoroutine(awaitable): + awaitable = create_eager_task(awaitable) - async with hass.timeout.async_timeout(SLOW_SETUP_MAX_WAIT, self.domain): - await asyncio.shield(awaitable) + async with hass.timeout.async_timeout(SLOW_SETUP_MAX_WAIT, self.domain): + await asyncio.shield(awaitable) - # Block till all entities are done - while self._tasks: - # Await all tasks even if they are done - # to ensure exceptions are propagated - pending = self._tasks.copy() - self._tasks.clear() - await asyncio.gather(*pending) + # Block till all entities are done + while self._tasks: + # Await all tasks even if they are done + # to ensure exceptions are propagated + pending = self._tasks.copy() + self._tasks.clear() + await asyncio.gather(*pending) - hass.config.components.add(full_name) - self._setup_complete = True - return True - except PlatformNotReady as ex: - tries += 1 - wait_time = min(tries, 6) * PLATFORM_NOT_READY_BASE_WAIT_TIME - message = str(ex) - ready_message = f"ready yet: {message}" if message else "ready yet" - if tries == 1: - logger.warning( - "Platform %s not %s; Retrying in background in %d seconds", - self.platform_name, - ready_message, - wait_time, - ) - else: - logger.debug( - "Platform %s not %s; Retrying in %d seconds", - self.platform_name, - ready_message, - wait_time, - ) - - async def setup_again(*_args: Any) -> None: - """Run setup again.""" - self._async_cancel_retry_setup = None - await self._async_setup_platform( - async_create_setup_awaitable, tries - ) - - if hass.state is CoreState.running: - self._async_cancel_retry_setup = async_call_later( - hass, wait_time, setup_again - ) - else: - self._async_cancel_retry_setup = hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STARTED, setup_again - ) - return False - except TimeoutError: - logger.error( - ( - "Setup of platform %s is taking longer than %s seconds." - " Startup will proceed without waiting any longer." - ), + hass.config.components.add(full_name) + self._setup_complete = True + return True + except PlatformNotReady as ex: + tries += 1 + wait_time = min(tries, 6) * PLATFORM_NOT_READY_BASE_WAIT_TIME + message = str(ex) + ready_message = f"ready yet: {message}" if message else "ready yet" + if tries == 1: + logger.warning( + "Platform %s not %s; Retrying in background in %d seconds", self.platform_name, - SLOW_SETUP_MAX_WAIT, + ready_message, + wait_time, ) - return False - except Exception: # pylint: disable=broad-except - logger.exception( - "Error while setting up %s platform for %s", + else: + logger.debug( + "Platform %s not %s; Retrying in %d seconds", self.platform_name, - self.domain, + ready_message, + wait_time, ) - return False - finally: - warn_task.cancel() + + async def setup_again(*_args: Any) -> None: + """Run setup again.""" + self._async_cancel_retry_setup = None + await self._async_setup_platform(async_create_setup_awaitable, tries) + + if hass.state is CoreState.running: + self._async_cancel_retry_setup = async_call_later( + hass, wait_time, setup_again + ) + else: + self._async_cancel_retry_setup = hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STARTED, setup_again + ) + return False + except TimeoutError: + logger.error( + ( + "Setup of platform %s is taking longer than %s seconds." + " Startup will proceed without waiting any longer." + ), + self.platform_name, + SLOW_SETUP_MAX_WAIT, + ) + return False + except Exception: # pylint: disable=broad-except + logger.exception( + "Error while setting up %s platform for %s", + self.platform_name, + self.domain, + ) + return False + finally: + warn_task.cancel() async def _async_get_translations( self, language: str, category: str, integration: str - ) -> dict[str, Any]: + ) -> dict[str, str]: """Get translations for a language, category, and integration.""" try: return await translation.async_get_translations( @@ -641,7 +645,19 @@ class EntityPlatform: @callback def _async_handle_interval_callback(self, now: datetime) -> None: """Update all the entity states in a single platform.""" - self.hass.async_create_task(self._update_entity_states(now), eager_start=True) + if self.config_entry: + self.config_entry.async_create_background_task( + self.hass, + self._update_entity_states(now), + name=f"EntityPlatform poll {self.domain}.{self.platform_name}", + eager_start=True, + ) + else: + self.hass.async_create_background_task( + self._update_entity_states(now), + name=f"EntityPlatform poll {self.domain}.{self.platform_name}", + eager_start=True, + ) def _entity_id_already_exists(self, entity_id: str) -> tuple[bool, bool]: """Check if an entity_id already exists. diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 50ecbc1fb59..ef9274c6ceb 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -7,10 +7,11 @@ The Entity Registry will persist itself 10 seconds after a new entity is registered. Registering a new entity while a timer is in progress resets the timer. """ + from __future__ import annotations from collections import UserDict -from collections.abc import Callable, Iterable, Mapping, ValuesView +from collections.abc import Callable, Iterable, KeysView, Mapping, ValuesView from datetime import datetime, timedelta from enum import StrEnum import logging @@ -51,7 +52,8 @@ from homeassistant.util.read_only_dict import ReadOnlyDict from . import device_registry as dr, storage from .device_registry import EVENT_DEVICE_REGISTRY_UPDATED -from .json import JSON_DUMP, find_paths_unserializable_data, json_bytes +from .json import JSON_DUMP, find_paths_unserializable_data, json_bytes, json_fragment +from .registry import BaseRegistry from .typing import UNDEFINED, UndefinedType if TYPE_CHECKING: @@ -61,11 +63,11 @@ T = TypeVar("T") DATA_REGISTRY = "entity_registry" EVENT_ENTITY_REGISTRY_UPDATED = "entity_registry_updated" -SAVE_DELAY = 10 + _LOGGER = logging.getLogger(__name__) STORAGE_VERSION_MAJOR = 1 -STORAGE_VERSION_MINOR = 13 +STORAGE_VERSION_MINOR = 14 STORAGE_KEY = "core.entity_registry" CLEANUP_INTERVAL = 3600 * 24 @@ -134,11 +136,12 @@ EntityOptionsType = Mapping[str, Mapping[str, Any]] ReadOnlyEntityOptionsType = ReadOnlyDict[str, ReadOnlyDict[str, Any]] DISPLAY_DICT_OPTIONAL = ( - ("ai", "area_id"), - ("lb", "labels"), - ("di", "device_id"), - ("ic", "icon"), - ("tk", "translation_key"), + # key, attr_name, convert_to_list + ("ai", "area_id", False), + ("lb", "labels", True), + ("di", "device_id", False), + ("ic", "icon", False), + ("tk", "translation_key", False), ) @@ -161,6 +164,7 @@ class RegistryEntry: previous_unique_id: str | None = attr.ib(default=None) aliases: set[str] = attr.ib(factory=set) area_id: str | None = attr.ib(default=None) + categories: dict[str, str] = attr.ib(factory=dict) capabilities: Mapping[str, Any] | None = attr.ib(default=None) config_entry_id: str | None = attr.ib(default=None) device_class: str | None = attr.ib(default=None) @@ -211,9 +215,12 @@ class RegistryEntry: Returns None if there's no data needed for display. """ display_dict: dict[str, Any] = {"ei": self.entity_id, "pl": self.platform} - for key, attr_name in DISPLAY_DICT_OPTIONAL: + for key, attr_name, convert_list in DISPLAY_DICT_OPTIONAL: if (attr_val := getattr(self, attr_name)) is not None: - display_dict[key] = attr_val + # Convert sets and tuples to lists + # so the JSON serializer does not have to do + # it every time + display_dict[key] = list(attr_val) if convert_list else attr_val if (category := self.entity_category) is not None: display_dict["ec"] = ENTITY_CATEGORY_VALUE_TO_INDEX[category] if self.hidden_by is not None: @@ -251,8 +258,12 @@ class RegistryEntry: @cached_property def as_partial_dict(self) -> dict[str, Any]: """Return a partial dict representation of the entry.""" + # Convert sets and tuples to lists + # so the JSON serializer does not have to do + # it every time return { "area_id": self.area_id, + "categories": self.categories, "config_entry_id": self.config_entry_id, "device_id": self.device_id, "disabled_by": self.disabled_by, @@ -262,7 +273,7 @@ class RegistryEntry: "hidden_by": self.hidden_by, "icon": self.icon, "id": self.id, - "labels": self.labels, + "labels": list(self.labels), "name": self.name, "options": self.options, "original_name": self.original_name, @@ -274,9 +285,12 @@ class RegistryEntry: @cached_property def extended_dict(self) -> dict[str, Any]: """Return a extended dict representation of the entry.""" + # Convert sets and tuples to lists + # so the JSON serializer does not have to do + # it every time return { **self.as_partial_dict, - "aliases": self.aliases, + "aliases": list(self.aliases), "capabilities": self.capabilities, "device_class": self.device_class, "original_device_class": self.original_device_class, @@ -299,6 +313,42 @@ class RegistryEntry: ) return None + @cached_property + def as_storage_fragment(self) -> json_fragment: + """Return a json fragment for storage.""" + return json_fragment( + json_bytes( + { + "aliases": list(self.aliases), + "area_id": self.area_id, + "categories": self.categories, + "capabilities": self.capabilities, + "config_entry_id": self.config_entry_id, + "device_class": self.device_class, + "device_id": self.device_id, + "disabled_by": self.disabled_by, + "entity_category": self.entity_category, + "entity_id": self.entity_id, + "hidden_by": self.hidden_by, + "icon": self.icon, + "id": self.id, + "has_entity_name": self.has_entity_name, + "labels": list(self.labels), + "name": self.name, + "options": self.options, + "original_device_class": self.original_device_class, + "original_icon": self.original_icon, + "original_name": self.original_name, + "platform": self.platform, + "supported_features": self.supported_features, + "translation_key": self.translation_key, + "unique_id": self.unique_id, + "previous_unique_id": self.previous_unique_id, + "unit_of_measurement": self.unit_of_measurement, + } + ) + ) + @callback def write_unavailable_state(self, hass: HomeAssistant) -> None: """Write the unavailable state to the state machine.""" @@ -328,7 +378,7 @@ class RegistryEntry: hass.states.async_set(self.entity_id, STATE_UNAVAILABLE, attrs) -@attr.s(slots=True, frozen=True) +@attr.s(frozen=True) class DeletedRegistryEntry: """Deleted Entity Registry Entry.""" @@ -345,6 +395,22 @@ class DeletedRegistryEntry: """Compute domain value.""" return split_entity_id(self.entity_id)[0] + @cached_property + def as_storage_fragment(self) -> json_fragment: + """Return a json fragment for storage.""" + return json_fragment( + json_bytes( + { + "config_entry_id": self.config_entry_id, + "entity_id": self.entity_id, + "id": self.id, + "orphaned_timestamp": self.orphaned_timestamp, + "platform": self.platform, + "unique_id": self.unique_id, + } + ) + ) + class EntityRegistryStore(storage.Store[dict[str, list[dict[str, Any]]]]): """Store entity registry data.""" @@ -435,6 +501,11 @@ class EntityRegistryStore(storage.Store[dict[str, list[dict[str, Any]]]]): for entity in data["entities"]: entity["labels"] = [] + if old_major_version == 1 and old_minor_version < 14: + # Version 1.14 adds categories + for entity in data["entities"]: + entity["categories"] = {} + if old_major_version > 1: raise NotImplementedError return data @@ -511,6 +582,10 @@ class EntityRegistryItems(UserDict[str, RegistryEntry]): self._unindex_entry(key) super().__delitem__(key) + def get_device_ids(self) -> KeysView[str]: + """Return device ids.""" + return self._device_id_index.keys() + def get_entity_id(self, key: tuple[str, str, str]) -> str | None: """Get entity_id from (domain, platform, unique_id).""" return self._index.get(key) @@ -545,7 +620,7 @@ class EntityRegistryItems(UserDict[str, RegistryEntry]): return [data[key] for key in self._area_id_index.get(area_id, ())] -class EntityRegistry: +class EntityRegistry(BaseRegistry): """Class to hold a registry of entities.""" deleted_entities: dict[tuple[str, str, str], DeletedRegistryEntry] @@ -563,32 +638,11 @@ class EntityRegistry: minor_version=STORAGE_VERSION_MINOR, ) self.hass.bus.async_listen( - EVENT_DEVICE_REGISTRY_UPDATED, self.async_device_modified + EVENT_DEVICE_REGISTRY_UPDATED, + self.async_device_modified, + run_immediately=True, ) - @callback - def async_get_device_class_lookup( - self, domain_device_classes: set[tuple[str, str | None]] - ) -> dict[str, dict[tuple[str, str | None], str]]: - """Return a lookup of entity ids for devices which have matching entities. - - Entities must match a set of (domain, device_class) tuples. - The result is indexed by device_id, then by the matching (domain, device_class) - """ - lookup: dict[str, dict[tuple[str, str | None], str]] = {} - for entity in self.entities.values(): - if not entity.device_id: - continue - device_class = entity.device_class or entity.original_device_class - domain_device_class = (entity.domain, device_class) - if domain_device_class not in domain_device_classes: - continue - if entity.device_id not in lookup: - lookup[entity.device_id] = {domain_device_class: entity.entity_id} - else: - lookup[entity.device_id][domain_device_class] = entity.entity_id - return lookup - @callback def async_is_registered(self, entity_id: str) -> bool: """Check if an entity_id is currently registered.""" @@ -612,6 +666,11 @@ class EntityRegistry: """Check if an entity_id is currently registered.""" return self.entities.get_entity_id((domain, platform, unique_id)) + @callback + def async_device_ids(self) -> list[str]: + """Return known device ids.""" + return list(self.entities.get_device_ids()) + def _entity_id_available( self, entity_id: str, known_object_ids: Iterable[str] | None ) -> bool: @@ -880,6 +939,7 @@ class EntityRegistry: *, aliases: set[str] | UndefinedType = UNDEFINED, area_id: str | None | UndefinedType = UNDEFINED, + categories: dict[str, str] | UndefinedType = UNDEFINED, capabilities: Mapping[str, Any] | None | UndefinedType = UNDEFINED, config_entry_id: str | None | UndefinedType = UNDEFINED, device_class: str | None | UndefinedType = UNDEFINED, @@ -931,6 +991,7 @@ class EntityRegistry: for attr_name, value in ( ("aliases", aliases), ("area_id", area_id), + ("categories", categories), ("capabilities", capabilities), ("config_entry_id", config_entry_id), ("device_class", device_class), @@ -1009,6 +1070,7 @@ class EntityRegistry: *, aliases: set[str] | UndefinedType = UNDEFINED, area_id: str | None | UndefinedType = UNDEFINED, + categories: dict[str, str] | UndefinedType = UNDEFINED, capabilities: Mapping[str, Any] | None | UndefinedType = UNDEFINED, config_entry_id: str | None | UndefinedType = UNDEFINED, device_class: str | None | UndefinedType = UNDEFINED, @@ -1034,6 +1096,7 @@ class EntityRegistry: entity_id, aliases=aliases, area_id=area_id, + categories=categories, capabilities=capabilities, config_entry_id=config_entry_id, device_class=device_class, @@ -1124,6 +1187,7 @@ class EntityRegistry: entities[entity["entity_id"]] = RegistryEntry( aliases=set(entity["aliases"]), area_id=entity["area_id"], + categories=entity["categories"], capabilities=entity["capabilities"], config_entry_id=entity["config_entry_id"], device_class=entity["device_class"], @@ -1173,59 +1237,26 @@ class EntityRegistry: self.entities = entities self._entities_data = entities.data - @callback - def async_schedule_save(self) -> None: - """Schedule saving the entity registry.""" - self._store.async_delay_save(self._data_to_save, SAVE_DELAY) - @callback def _data_to_save(self) -> dict[str, Any]: """Return data of entity registry to store in a file.""" - data: dict[str, Any] = {} + return { + "entities": [entry.as_storage_fragment for entry in self.entities.values()], + "deleted_entities": [ + entry.as_storage_fragment for entry in self.deleted_entities.values() + ], + } - data["entities"] = [ - { - "aliases": list(entry.aliases), - "area_id": entry.area_id, - "capabilities": entry.capabilities, - "config_entry_id": entry.config_entry_id, - "device_class": entry.device_class, - "device_id": entry.device_id, - "disabled_by": entry.disabled_by, - "entity_category": entry.entity_category, - "entity_id": entry.entity_id, - "hidden_by": entry.hidden_by, - "icon": entry.icon, - "id": entry.id, - "has_entity_name": entry.has_entity_name, - "labels": list(entry.labels), - "name": entry.name, - "options": entry.options, - "original_device_class": entry.original_device_class, - "original_icon": entry.original_icon, - "original_name": entry.original_name, - "platform": entry.platform, - "supported_features": entry.supported_features, - "translation_key": entry.translation_key, - "unique_id": entry.unique_id, - "previous_unique_id": entry.previous_unique_id, - "unit_of_measurement": entry.unit_of_measurement, - } - for entry in self.entities.values() - ] - data["deleted_entities"] = [ - { - "config_entry_id": entry.config_entry_id, - "entity_id": entry.entity_id, - "id": entry.id, - "orphaned_timestamp": entry.orphaned_timestamp, - "platform": entry.platform, - "unique_id": entry.unique_id, - } - for entry in self.deleted_entities.values() - ] - - return data + @callback + def async_clear_category_id(self, scope: str, category_id: str) -> None: + """Clear category id from registry entries.""" + for entity_id, entry in self.entities.items(): + if ( + existing_category_id := entry.categories.get(scope) + ) and category_id == existing_category_id: + categories = entry.categories.copy() + del categories[scope] + self.async_update_entity(entity_id, categories=categories) @callback def async_clear_label_id(self, label_id: str) -> None: @@ -1316,6 +1347,21 @@ def async_entries_for_label( return [entry for entry in registry.entities.values() if label_id in entry.labels] +@callback +def async_entries_for_category( + registry: EntityRegistry, scope: str, category_id: str +) -> list[RegistryEntry]: + """Return entries that match a category in a scope.""" + return [ + entry + for entry in registry.entities.values() + if ( + (existing_category_id := entry.categories.get(scope)) + and category_id == existing_category_id + ) + ] + + @callback def async_entries_for_config_entry( registry: EntityRegistry, config_entry_id: str @@ -1358,14 +1404,15 @@ def async_config_entry_disabled_by_changed( def _async_setup_cleanup(hass: HomeAssistant, registry: EntityRegistry) -> None: """Clean up device registry when entities removed.""" # pylint: disable-next=import-outside-toplevel - from . import event, label_registry as lr + from . import category_registry as cr, event, label_registry as lr @callback - def _label_removed_from_registry_filter( - event: lr.EventLabelRegistryUpdated, + def _removed_from_registry_filter( + event_data: lr.EventLabelRegistryUpdatedData + | cr.EventCategoryRegistryUpdatedData, ) -> bool: - """Filter all except for the remove action from label registry events.""" - return event.data["action"] == "remove" + """Filter all except for the remove action from registry events.""" + return event_data["action"] == "remove" @callback def _handle_label_registry_update(event: lr.EventLabelRegistryUpdated) -> None: @@ -1374,8 +1421,23 @@ def _async_setup_cleanup(hass: HomeAssistant, registry: EntityRegistry) -> None: hass.bus.async_listen( event_type=lr.EVENT_LABEL_REGISTRY_UPDATED, - event_filter=_label_removed_from_registry_filter, # type: ignore[arg-type] - listener=_handle_label_registry_update, # type: ignore[arg-type] + event_filter=_removed_from_registry_filter, + listener=_handle_label_registry_update, + run_immediately=True, + ) + + @callback + def _handle_category_registry_update( + event: cr.EventCategoryRegistryUpdated, + ) -> None: + """Update entity that have a category that has been removed.""" + registry.async_clear_category_id(event.data["scope"], event.data["category_id"]) + + hass.bus.async_listen( + event_type=cr.EVENT_CATEGORY_REGISTRY_UPDATED, + event_filter=_removed_from_registry_filter, + listener=_handle_category_registry_update, + run_immediately=True, ) @callback @@ -1394,7 +1456,9 @@ def _async_setup_cleanup(hass: HomeAssistant, registry: EntityRegistry) -> None: """Cancel cleanup.""" cancel() - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _on_homeassistant_stop) + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, _on_homeassistant_stop, run_immediately=True + ) @callback @@ -1402,9 +1466,9 @@ def _async_setup_entity_restore(hass: HomeAssistant, registry: EntityRegistry) - """Set up the entity restore mechanism.""" @callback - def cleanup_restored_states_filter(event: Event) -> bool: + def cleanup_restored_states_filter(event_data: Mapping[str, Any]) -> bool: """Clean up restored states filter.""" - return bool(event.data["action"] == "remove") + return bool(event_data["action"] == "remove") @callback def cleanup_restored_states(event: Event) -> None: @@ -1420,6 +1484,7 @@ def _async_setup_entity_restore(hass: HomeAssistant, registry: EntityRegistry) - EVENT_ENTITY_REGISTRY_UPDATED, cleanup_restored_states, event_filter=cleanup_restored_states_filter, + run_immediately=True, ) if hass.is_running: @@ -1436,7 +1501,9 @@ def _async_setup_entity_restore(hass: HomeAssistant, registry: EntityRegistry) - entry.write_unavailable_state(hass) - hass.bus.async_listen(EVENT_HOMEASSISTANT_START, _write_unavailable_states) + hass.bus.async_listen( + EVENT_HOMEASSISTANT_START, _write_unavailable_states, run_immediately=True + ) async def async_migrate_entries( diff --git a/homeassistant/helpers/entity_values.py b/homeassistant/helpers/entity_values.py index fe4a4249c54..7e7bdc7be41 100644 --- a/homeassistant/helpers/entity_values.py +++ b/homeassistant/helpers/entity_values.py @@ -1,4 +1,5 @@ """A class to hold entity values.""" + from __future__ import annotations from collections import OrderedDict diff --git a/homeassistant/helpers/entityfilter.py b/homeassistant/helpers/entityfilter.py index dd61357f53e..837c5e2bc1d 100644 --- a/homeassistant/helpers/entityfilter.py +++ b/homeassistant/helpers/entityfilter.py @@ -1,4 +1,5 @@ """Helper class to implement include/exclude of entities and domains.""" + from __future__ import annotations from collections.abc import Callable @@ -141,10 +142,9 @@ def _convert_globs_to_pattern(globs: list[str] | None) -> re.Pattern[str] | None if globs is None: return None - translated_patterns: list[str] = [] - for glob in set(globs): - if pattern := fnmatch.translate(glob): - translated_patterns.append(pattern) + translated_patterns: list[str] = [ + pattern for glob in set(globs) if (pattern := fnmatch.translate(glob)) + ] if not translated_patterns: return None diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 0dc3115466a..749c6d3e6e4 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -1,4 +1,5 @@ """Helpers for listening to events.""" + from __future__ import annotations import asyncio @@ -31,6 +32,7 @@ from homeassistant.const import ( ) from homeassistant.core import ( CALLBACK_TYPE, + Event, HassJob, HassJobType, HomeAssistant, @@ -54,7 +56,7 @@ from .entity_registry import ( from .ratelimit import KeyedRateLimit from .sun import get_astral_event_next from .template import RenderInfo, Template, result_as_boolean -from .typing import EventType, TemplateVarsType +from .typing import TemplateVarsType TRACK_STATE_CHANGE_CALLBACKS = "track_state_change_callbacks" TRACK_STATE_CHANGE_LISTENER = "track_state_change_listener" @@ -98,16 +100,16 @@ class _KeyedEventTracker(Generic[_TypedDictT]): dispatcher_callable: Callable[ [ HomeAssistant, - dict[str, list[HassJob[[EventType[_TypedDictT]], Any]]], - EventType[_TypedDictT], + dict[str, list[HassJob[[Event[_TypedDictT]], Any]]], + Event[_TypedDictT], ], None, ] filter_callable: Callable[ [ HomeAssistant, - dict[str, list[HassJob[[EventType[_TypedDictT]], Any]]], - EventType[_TypedDictT], + dict[str, list[HassJob[[Event[_TypedDictT]], Any]]], + _TypedDictT, ], bool, ] @@ -139,7 +141,7 @@ class TrackTemplate: template: Template variables: TemplateVarsType - rate_limit: timedelta | None = None + rate_limit: float | None = None @dataclass(slots=True) @@ -235,11 +237,11 @@ def async_track_state_change( job = HassJob(action, f"track state change {entity_ids} {from_state} {to_state}") @callback - def state_change_filter(event: EventType[EventStateChangedData]) -> bool: + def state_change_filter(event_data: EventStateChangedData) -> bool: """Handle specific state changes.""" if from_state is not None: old_state_str: str | None = None - if (old_state := event.data["old_state"]) is not None: + if (old_state := event_data["old_state"]) is not None: old_state_str = old_state.state if not match_from_state(old_state_str): @@ -247,7 +249,7 @@ def async_track_state_change( if to_state is not None: new_state_str: str | None = None - if (new_state := event.data["new_state"]) is not None: + if (new_state := event_data["new_state"]) is not None: new_state_str = new_state.state if not match_to_state(new_state_str): @@ -256,7 +258,7 @@ def async_track_state_change( return True @callback - def state_change_dispatcher(event: EventType[EventStateChangedData]) -> None: + def state_change_dispatcher(event: Event[EventStateChangedData]) -> None: """Handle specific state changes.""" hass.async_run_hass_job( job, @@ -266,9 +268,9 @@ def async_track_state_change( ) @callback - def state_change_listener(event: EventType[EventStateChangedData]) -> None: + def state_change_listener(event: Event[EventStateChangedData]) -> None: """Handle specific state changes.""" - if not state_change_filter(event): + if not state_change_filter(event.data): return state_change_dispatcher(event) @@ -285,9 +287,7 @@ def async_track_state_change( return async_track_state_change_event(hass, entity_ids, state_change_listener) return hass.bus.async_listen( - EVENT_STATE_CHANGED, - state_change_dispatcher, # type: ignore[arg-type] - event_filter=state_change_filter, # type: ignore[arg-type] + EVENT_STATE_CHANGED, state_change_dispatcher, event_filter=state_change_filter ) @@ -298,7 +298,8 @@ track_state_change = threaded_listener_factory(async_track_state_change) def async_track_state_change_event( hass: HomeAssistant, entity_ids: str | Iterable[str], - action: Callable[[EventType[EventStateChangedData]], Any], + action: Callable[[Event[EventStateChangedData]], Any], + job_type: HassJobType | None = None, ) -> CALLBACK_TYPE: """Track specific state change events indexed by entity_id. @@ -313,14 +314,14 @@ def async_track_state_change_event( """ if not (entity_ids := _async_string_to_lower_list(entity_ids)): return _remove_empty_listener - return _async_track_state_change_event(hass, entity_ids, action) + return _async_track_state_change_event(hass, entity_ids, action, job_type) @callback def _async_dispatch_entity_id_event( hass: HomeAssistant, - callbacks: dict[str, list[HassJob[[EventType[EventStateChangedData]], Any]]], - event: EventType[EventStateChangedData], + callbacks: dict[str, list[HassJob[[Event[EventStateChangedData]], Any]]], + event: Event[EventStateChangedData], ) -> None: """Dispatch to listeners.""" if not (callbacks_list := callbacks.get(event.data["entity_id"])): @@ -339,11 +340,11 @@ def _async_dispatch_entity_id_event( @callback def _async_state_change_filter( hass: HomeAssistant, - callbacks: dict[str, list[HassJob[[EventType[EventStateChangedData]], Any]]], - event: EventType[EventStateChangedData], + callbacks: dict[str, list[HassJob[[Event[EventStateChangedData]], Any]]], + event_data: EventStateChangedData, ) -> bool: """Filter state changes by entity_id.""" - return event.data["entity_id"] in callbacks + return event_data["entity_id"] in callbacks _KEYED_TRACK_STATE_CHANGE = _KeyedEventTracker( @@ -360,10 +361,13 @@ _KEYED_TRACK_STATE_CHANGE = _KeyedEventTracker( def _async_track_state_change_event( hass: HomeAssistant, entity_ids: str | Iterable[str], - action: Callable[[EventType[EventStateChangedData]], Any], + action: Callable[[Event[EventStateChangedData]], Any], + job_type: HassJobType | None, ) -> CALLBACK_TYPE: """async_track_state_change_event without lowercasing.""" - return _async_track_event(_KEYED_TRACK_STATE_CHANGE, hass, entity_ids, action) + return _async_track_event( + _KEYED_TRACK_STATE_CHANGE, hass, entity_ids, action, job_type + ) @callback @@ -376,8 +380,8 @@ def _remove_listener( hass: HomeAssistant, listeners_key: str, keys: Iterable[str], - job: HassJob[[EventType[_TypedDictT]], Any], - callbacks: dict[str, list[HassJob[[EventType[_TypedDictT]], Any]]], + job: HassJob[[Event[_TypedDictT]], Any], + callbacks: dict[str, list[HassJob[[Event[_TypedDictT]], Any]]], ) -> None: """Remove listener.""" for key in keys: @@ -396,7 +400,8 @@ def _async_track_event( tracker: _KeyedEventTracker[_TypedDictT], hass: HomeAssistant, keys: str | Iterable[str], - action: Callable[[EventType[_TypedDictT]], None], + action: Callable[[Event[_TypedDictT]], None], + job_type: HassJobType | None, ) -> CALLBACK_TYPE: """Track an event by a specific key. @@ -415,7 +420,7 @@ def _async_track_event( hass_data = hass.data callbacks_key = tracker.callbacks_key - callbacks: dict[str, list[HassJob[[EventType[_TypedDictT]], Any]]] | None + callbacks: dict[str, list[HassJob[[Event[_TypedDictT]], Any]]] | None if not (callbacks := hass_data.get(callbacks_key)): callbacks = hass_data[callbacks_key] = {} @@ -429,7 +434,7 @@ def _async_track_event( run_immediately=tracker.run_immediately, ) - job = HassJob(action, f"track {tracker.event_type} event {keys}") + job = HassJob(action, f"track {tracker.event_type} event {keys}", job_type=job_type) for key in keys: if callback_list := callbacks.get(key): @@ -443,10 +448,8 @@ def _async_track_event( @callback def _async_dispatch_old_entity_id_or_entity_id_event( hass: HomeAssistant, - callbacks: dict[ - str, list[HassJob[[EventType[EventEntityRegistryUpdatedData]], Any]] - ], - event: EventType[EventEntityRegistryUpdatedData], + callbacks: dict[str, list[HassJob[[Event[EventEntityRegistryUpdatedData]], Any]]], + event: Event[EventEntityRegistryUpdatedData], ) -> None: """Dispatch to listeners.""" if not ( @@ -469,13 +472,11 @@ def _async_dispatch_old_entity_id_or_entity_id_event( @callback def _async_entity_registry_updated_filter( hass: HomeAssistant, - callbacks: dict[ - str, list[HassJob[[EventType[EventEntityRegistryUpdatedData]], Any]] - ], - event: EventType[EventEntityRegistryUpdatedData], + callbacks: dict[str, list[HassJob[[Event[EventEntityRegistryUpdatedData]], Any]]], + event_data: EventEntityRegistryUpdatedData, ) -> bool: """Filter entity registry updates by entity_id.""" - return event.data.get("old_entity_id", event.data["entity_id"]) in callbacks + return event_data.get("old_entity_id", event_data["entity_id"]) in callbacks _KEYED_TRACK_ENTITY_REGISTRY_UPDATED = _KeyedEventTracker( @@ -493,7 +494,8 @@ _KEYED_TRACK_ENTITY_REGISTRY_UPDATED = _KeyedEventTracker( def async_track_entity_registry_updated_event( hass: HomeAssistant, entity_ids: str | Iterable[str], - action: Callable[[EventType[EventEntityRegistryUpdatedData]], Any], + action: Callable[[Event[EventEntityRegistryUpdatedData]], Any], + job_type: HassJobType | None = None, ) -> CALLBACK_TYPE: """Track specific entity registry updated events indexed by entity_id. @@ -502,32 +504,25 @@ def async_track_entity_registry_updated_event( Similar to async_track_state_change_event. """ return _async_track_event( - _KEYED_TRACK_ENTITY_REGISTRY_UPDATED, - hass, - entity_ids, - action, + _KEYED_TRACK_ENTITY_REGISTRY_UPDATED, hass, entity_ids, action, job_type ) @callback def _async_device_registry_updated_filter( hass: HomeAssistant, - callbacks: dict[ - str, list[HassJob[[EventType[EventDeviceRegistryUpdatedData]], Any]] - ], - event: EventType[EventDeviceRegistryUpdatedData], + callbacks: dict[str, list[HassJob[[Event[EventDeviceRegistryUpdatedData]], Any]]], + event_data: EventDeviceRegistryUpdatedData, ) -> bool: """Filter device registry updates by device_id.""" - return event.data["device_id"] in callbacks + return event_data["device_id"] in callbacks @callback def _async_dispatch_device_id_event( hass: HomeAssistant, - callbacks: dict[ - str, list[HassJob[[EventType[EventDeviceRegistryUpdatedData]], Any]] - ], - event: EventType[EventDeviceRegistryUpdatedData], + callbacks: dict[str, list[HassJob[[Event[EventDeviceRegistryUpdatedData]], Any]]], + event: Event[EventDeviceRegistryUpdatedData], ) -> None: """Dispatch to listeners.""" if not (callbacks_list := callbacks.get(event.data["device_id"])): @@ -557,25 +552,23 @@ _KEYED_TRACK_DEVICE_REGISTRY_UPDATED = _KeyedEventTracker( def async_track_device_registry_updated_event( hass: HomeAssistant, device_ids: str | Iterable[str], - action: Callable[[EventType[EventDeviceRegistryUpdatedData]], Any], + action: Callable[[Event[EventDeviceRegistryUpdatedData]], Any], + job_type: HassJobType | None = None, ) -> CALLBACK_TYPE: """Track specific device registry updated events indexed by device_id. Similar to async_track_entity_registry_updated_event. """ return _async_track_event( - _KEYED_TRACK_DEVICE_REGISTRY_UPDATED, - hass, - device_ids, - action, + _KEYED_TRACK_DEVICE_REGISTRY_UPDATED, hass, device_ids, action, job_type ) @callback def _async_dispatch_domain_event( hass: HomeAssistant, - callbacks: dict[str, list[HassJob[[EventType[EventStateChangedData]], Any]]], - event: EventType[EventStateChangedData], + callbacks: dict[str, list[HassJob[[Event[EventStateChangedData]], Any]]], + event: Event[EventStateChangedData], ) -> None: """Dispatch domain event listeners.""" domain = split_entity_id(event.data["entity_id"])[0] @@ -591,13 +584,13 @@ def _async_dispatch_domain_event( @callback def _async_domain_added_filter( hass: HomeAssistant, - callbacks: dict[str, list[HassJob[[EventType[EventStateChangedData]], Any]]], - event: EventType[EventStateChangedData], + callbacks: dict[str, list[HassJob[[Event[EventStateChangedData]], Any]]], + event_data: EventStateChangedData, ) -> bool: """Filter state changes by entity_id.""" - return event.data["old_state"] is None and ( + return event_data["old_state"] is None and ( MATCH_ALL in callbacks - or split_entity_id(event.data["entity_id"])[0] in callbacks + or split_entity_id(event_data["entity_id"])[0] in callbacks ) @@ -605,12 +598,13 @@ def _async_domain_added_filter( def async_track_state_added_domain( hass: HomeAssistant, domains: str | Iterable[str], - action: Callable[[EventType[EventStateChangedData]], Any], + action: Callable[[Event[EventStateChangedData]], Any], + job_type: HassJobType | None = None, ) -> CALLBACK_TYPE: """Track state change events when an entity is added to domains.""" if not (domains := _async_string_to_lower_list(domains)): return _remove_empty_listener - return _async_track_state_added_domain(hass, domains, action) + return _async_track_state_added_domain(hass, domains, action, job_type) _KEYED_TRACK_STATE_ADDED_DOMAIN = _KeyedEventTracker( @@ -627,22 +621,25 @@ _KEYED_TRACK_STATE_ADDED_DOMAIN = _KeyedEventTracker( def _async_track_state_added_domain( hass: HomeAssistant, domains: str | Iterable[str], - action: Callable[[EventType[EventStateChangedData]], Any], + action: Callable[[Event[EventStateChangedData]], Any], + job_type: HassJobType | None, ) -> CALLBACK_TYPE: """Track state change events when an entity is added to domains.""" - return _async_track_event(_KEYED_TRACK_STATE_ADDED_DOMAIN, hass, domains, action) + return _async_track_event( + _KEYED_TRACK_STATE_ADDED_DOMAIN, hass, domains, action, job_type + ) @callback def _async_domain_removed_filter( hass: HomeAssistant, - callbacks: dict[str, list[HassJob[[EventType[EventStateChangedData]], Any]]], - event: EventType[EventStateChangedData], + callbacks: dict[str, list[HassJob[[Event[EventStateChangedData]], Any]]], + event_data: EventStateChangedData, ) -> bool: """Filter state changes by entity_id.""" - return event.data["new_state"] is None and ( + return event_data["new_state"] is None and ( MATCH_ALL in callbacks - or split_entity_id(event.data["entity_id"])[0] in callbacks + or split_entity_id(event_data["entity_id"])[0] in callbacks ) @@ -660,10 +657,13 @@ _KEYED_TRACK_STATE_REMOVED_DOMAIN = _KeyedEventTracker( def async_track_state_removed_domain( hass: HomeAssistant, domains: str | Iterable[str], - action: Callable[[EventType[EventStateChangedData]], Any], + action: Callable[[Event[EventStateChangedData]], Any], + job_type: HassJobType | None = None, ) -> CALLBACK_TYPE: """Track state change events when an entity is removed from domains.""" - return _async_track_event(_KEYED_TRACK_STATE_REMOVED_DOMAIN, hass, domains, action) + return _async_track_event( + _KEYED_TRACK_STATE_REMOVED_DOMAIN, hass, domains, action, job_type + ) @callback @@ -681,7 +681,7 @@ class _TrackStateChangeFiltered: self, hass: HomeAssistant, track_states: TrackStates, - action: Callable[[EventType[EventStateChangedData]], Any], + action: Callable[[Event[EventStateChangedData]], Any], ) -> None: """Handle removal / refresh of tracker init.""" self.hass = hass @@ -781,11 +781,11 @@ class _TrackStateChangeFiltered: return self._listeners[_ENTITIES_LISTENER] = _async_track_state_change_event( - self.hass, entities, self._action + self.hass, entities, self._action, self._action_as_hassjob.job_type ) @callback - def _state_added(self, event: EventType[EventStateChangedData]) -> None: + def _state_added(self, event: Event[EventStateChangedData]) -> None: self._cancel_listener(_ENTITIES_LISTENER) self._setup_entities_listener( self._last_track_states.domains, self._last_track_states.entities @@ -798,14 +798,13 @@ class _TrackStateChangeFiltered: return self._listeners[_DOMAINS_LISTENER] = _async_track_state_added_domain( - self.hass, domains, self._state_added + self.hass, domains, self._state_added, HassJobType.Callback ) @callback def _setup_all_listener(self) -> None: self._listeners[_ALL_LISTENER] = self.hass.bus.async_listen( - EVENT_STATE_CHANGED, - self._action, # type: ignore[arg-type] + EVENT_STATE_CHANGED, self._action ) @@ -814,7 +813,7 @@ class _TrackStateChangeFiltered: def async_track_state_change_filtered( hass: HomeAssistant, track_states: TrackStates, - action: Callable[[EventType[EventStateChangedData]], Any], + action: Callable[[Event[EventStateChangedData]], Any], ) -> _TrackStateChangeFiltered: """Track state changes with a TrackStates filter that can be updated. @@ -888,7 +887,7 @@ def async_track_template( @callback def _template_changed_listener( - event: EventType[EventStateChangedData] | None, + event: Event[EventStateChangedData] | None, updates: list[TrackTemplateResult], ) -> None: """Check if condition is correct and run action.""" @@ -1078,8 +1077,8 @@ class TrackTemplateResultInfo: def _render_template_if_ready( self, track_template_: TrackTemplate, - now: datetime, - event: EventType[EventStateChangedData] | None, + now: float, + event: Event[EventStateChangedData] | None, ) -> bool | TrackTemplateResult: """Re-render the template if conditions match. @@ -1168,7 +1167,7 @@ class TrackTemplateResultInfo: @callback def _refresh( self, - event: EventType[EventStateChangedData] | None, + event: Event[EventStateChangedData] | None, track_templates: Iterable[TrackTemplate] | None = None, replayed: bool | None = False, ) -> None: @@ -1186,7 +1185,7 @@ class TrackTemplateResultInfo: """ updates: list[TrackTemplateResult] = [] info_changed = False - now = event.time_fired if not replayed and event else dt_util.utcnow() + now = event.time_fired_timestamp if not replayed and event else time.time() block_updates = False super_template = self._track_templates[0] if self._has_super_template else None @@ -1264,7 +1263,7 @@ class TrackTemplateResultInfo: TrackTemplateResultListener = Callable[ [ - EventType[EventStateChangedData] | None, + Event[EventStateChangedData] | None, list[TrackTemplateResult], ], Coroutine[Any, Any, None] | None, @@ -1372,7 +1371,7 @@ def async_track_same_state( hass.async_run_hass_job(job) @callback - def state_for_cancel_listener(event: EventType[EventStateChangedData]) -> None: + def state_for_cancel_listener(event: Event[EventStateChangedData]) -> None: """Fire on changes and cancel for listener if changed.""" entity = event.data["entity_id"] from_state = event.data["old_state"] @@ -1385,8 +1384,7 @@ def async_track_same_state( if entity_ids == MATCH_ALL: async_remove_state_for_cancel = hass.bus.async_listen( - EVENT_STATE_CHANGED, - state_for_cancel_listener, # type: ignore[arg-type] + EVENT_STATE_CHANGED, state_for_cancel_listener ) else: async_remove_state_for_cancel = async_track_state_change_event( @@ -1601,7 +1599,7 @@ class _TrackTimeInterval: self._track_job, hass.loop.time() + self.seconds, ) - hass.async_run_hass_job(self._run_job, now) + hass.async_run_hass_job(self._run_job, now, background=True) @callback def async_cancel(self) -> None: @@ -1686,7 +1684,7 @@ class SunListener: """Handle solar event.""" self._unsub_sun = None self._listen_next_sun_event() - self.hass.async_run_hass_job(self.job) + self.hass.async_run_hass_job(self.job, background=True) @callback def _handle_config_event(self, _event: Any) -> None: @@ -1772,7 +1770,7 @@ class _TrackUTCTimeChange: # time when the timer was scheduled utc_now = time_tracker_utcnow() localized_now = dt_util.as_local(utc_now) if self.local else utc_now - hass.async_run_hass_job(self.job, localized_now) + hass.async_run_hass_job(self.job, localized_now, background=True) if TYPE_CHECKING: assert self._pattern_time_change_listener_job is not None self._cancel_callback = async_track_point_in_utc_time( @@ -1910,7 +1908,7 @@ def _render_infos_to_track_states(render_infos: Iterable[RenderInfo]) -> TrackSt @callback def _event_triggers_rerender( - event: EventType[EventStateChangedData], info: RenderInfo + event: Event[EventStateChangedData], info: RenderInfo ) -> bool: """Determine if a template should be re-rendered from an event.""" entity_id = event.data["entity_id"] @@ -1926,10 +1924,10 @@ def _event_triggers_rerender( @callback def _rate_limit_for_event( - event: EventType[EventStateChangedData], + event: Event[EventStateChangedData], info: RenderInfo, track_template_: TrackTemplate, -) -> timedelta | None: +) -> float | None: """Determine the rate limit for an event.""" # Specifically referenced entities are excluded # from the rate limit @@ -1939,7 +1937,7 @@ def _rate_limit_for_event( if track_template_.rate_limit is not None: return track_template_.rate_limit - rate_limit: timedelta | None = info.rate_limit + rate_limit: float | None = info.rate_limit return rate_limit diff --git a/homeassistant/helpers/floor_registry.py b/homeassistant/helpers/floor_registry.py index 1149bbd1729..b168b81c1a9 100644 --- a/homeassistant/helpers/floor_registry.py +++ b/homeassistant/helpers/floor_registry.py @@ -1,23 +1,28 @@ """Provide a way to assign areas to floors in one's home.""" + from __future__ import annotations -from collections import UserDict -from collections.abc import Iterable, ValuesView +from collections.abc import Iterable import dataclasses from dataclasses import dataclass from typing import TYPE_CHECKING, Literal, TypedDict, cast -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import Event, HomeAssistant, callback from homeassistant.util import slugify +from .normalized_name_base_registry import ( + NormalizedNameBaseRegistryEntry, + NormalizedNameBaseRegistryItems, + normalize_name, +) +from .registry import BaseRegistry from .storage import Store -from .typing import UNDEFINED, EventType, UndefinedType +from .typing import UNDEFINED, UndefinedType DATA_REGISTRY = "floor_registry" EVENT_FLOOR_REGISTRY_UPDATED = "floor_registry_updated" STORAGE_KEY = "core.floor_registry" STORAGE_VERSION_MAJOR = 1 -SAVE_DELAY = 10 class EventFloorRegistryUpdatedData(TypedDict): @@ -27,83 +32,35 @@ class EventFloorRegistryUpdatedData(TypedDict): floor_id: str -EventFloorRegistryUpdated = EventType[EventFloorRegistryUpdatedData] +EventFloorRegistryUpdated = Event[EventFloorRegistryUpdatedData] @dataclass(slots=True, kw_only=True, frozen=True) -class FloorEntry: +class FloorEntry(NormalizedNameBaseRegistryEntry): """Floor registry entry.""" aliases: set[str] floor_id: str icon: str | None = None - level: int = 0 - name: str - normalized_name: str + level: int | None = None -class FloorRegistryItems(UserDict[str, FloorEntry]): - """Container for floor registry items, maps floor id -> entry. - - Maintains an additional index: - - normalized name -> entry - """ - - def __init__(self) -> None: - """Initialize the container.""" - super().__init__() - self._normalized_names: dict[str, FloorEntry] = {} - - def values(self) -> ValuesView[FloorEntry]: - """Return the underlying values to avoid __iter__ overhead.""" - return self.data.values() - - def __setitem__(self, key: str, entry: FloorEntry) -> None: - """Add an item.""" - data = self.data - normalized_name = _normalize_floor_name(entry.name) - - if key in data: - old_entry = data[key] - if ( - normalized_name != old_entry.normalized_name - and normalized_name in self._normalized_names - ): - raise ValueError( - f"The name {entry.name} ({normalized_name}) is already in use" - ) - del self._normalized_names[old_entry.normalized_name] - data[key] = entry - self._normalized_names[normalized_name] = entry - - def __delitem__(self, key: str) -> None: - """Remove an item.""" - entry = self[key] - normalized_name = _normalize_floor_name(entry.name) - del self._normalized_names[normalized_name] - super().__delitem__(key) - - def get_floor_by_name(self, name: str) -> FloorEntry | None: - """Get floor by name.""" - return self._normalized_names.get(_normalize_floor_name(name)) - - -class FloorRegistry: +class FloorRegistry(BaseRegistry): """Class to hold a registry of floors.""" - floors: FloorRegistryItems + floors: NormalizedNameBaseRegistryItems[FloorEntry] _floor_data: dict[str, FloorEntry] def __init__(self, hass: HomeAssistant) -> None: """Initialize the floor registry.""" self.hass = hass - self._store: Store[ - dict[str, list[dict[str, str | int | list[str] | None]]] - ] = Store( - hass, - STORAGE_VERSION_MAJOR, - STORAGE_KEY, - atomic_writes=True, + self._store: Store[dict[str, list[dict[str, str | int | list[str] | None]]]] = ( + Store( + hass, + STORAGE_VERSION_MAJOR, + STORAGE_KEY, + atomic_writes=True, + ) ) @callback @@ -118,7 +75,7 @@ class FloorRegistry: @callback def async_get_floor_by_name(self, name: str) -> FloorEntry | None: """Get floor by name.""" - return self.floors.get_floor_by_name(name) + return self.floors.get_by_name(name) @callback def async_list_floors(self) -> Iterable[FloorEntry]: @@ -142,7 +99,7 @@ class FloorRegistry: *, aliases: set[str] | None = None, icon: str | None = None, - level: int = 0, + level: int | None = None, ) -> FloorEntry: """Create a new floor.""" if floor := self.async_get_floor_by_name(name): @@ -150,7 +107,7 @@ class FloorRegistry: f"The name {name} ({floor.normalized_name}) is already in use" ) - normalized_name = _normalize_floor_name(name) + normalized_name = normalize_name(name) floor = FloorEntry( aliases=aliases or set(), @@ -208,7 +165,7 @@ class FloorRegistry: } if name is not UNDEFINED and name != old.name: changes["name"] = name - changes["normalized_name"] = _normalize_floor_name(name) + changes["normalized_name"] = normalize_name(name) if not changes: return old @@ -229,7 +186,7 @@ class FloorRegistry: async def async_load(self) -> None: """Load the floor registry.""" data = await self._store.async_load() - floors = FloorRegistryItems() + floors = NormalizedNameBaseRegistryItems[FloorEntry]() if data is not None: for floor in data["floors"]: @@ -240,7 +197,7 @@ class FloorRegistry: assert isinstance(floor["name"], str) assert isinstance(floor["floor_id"], str) - normalized_name = _normalize_floor_name(floor["name"]) + normalized_name = normalize_name(floor["name"]) floors[floor["floor_id"]] = FloorEntry( aliases=set(floor["aliases"]), icon=floor["icon"], @@ -253,11 +210,6 @@ class FloorRegistry: self.floors = floors self._floor_data = floors.data - @callback - def async_schedule_save(self) -> None: - """Schedule saving the floor registry.""" - self._store.async_delay_save(self._data_to_save, SAVE_DELAY) - @callback def _data_to_save(self) -> dict[str, list[dict[str, str | int | list[str] | None]]]: """Return data of floor registry to store in a file.""" @@ -286,8 +238,3 @@ async def async_load(hass: HomeAssistant) -> None: assert DATA_REGISTRY not in hass.data hass.data[DATA_REGISTRY] = FloorRegistry(hass) await hass.data[DATA_REGISTRY].async_load() - - -def _normalize_floor_name(floor_name: str) -> str: - """Normalize a floor name by removing whitespace and case folding.""" - return floor_name.casefold().replace(" ", "") diff --git a/homeassistant/helpers/frame.py b/homeassistant/helpers/frame.py index 04f16ebddd0..ee092717753 100644 --- a/homeassistant/helpers/frame.py +++ b/homeassistant/helpers/frame.py @@ -1,4 +1,5 @@ """Provide frame helper for finding the current frame context.""" + from __future__ import annotations import asyncio @@ -6,15 +7,21 @@ from collections.abc import Callable from contextlib import suppress from dataclasses import dataclass import functools +import linecache import logging import sys -from traceback import FrameSummary, extract_stack -from typing import Any, TypeVar, cast +from types import FrameType +from typing import TYPE_CHECKING, Any, TypeVar, cast from homeassistant.core import HomeAssistant, async_get_hass from homeassistant.exceptions import HomeAssistantError from homeassistant.loader import async_suggest_report_issue +if TYPE_CHECKING: + from functools import cached_property +else: + from homeassistant.backports.functools import cached_property + _LOGGER = logging.getLogger(__name__) # Keep track of integrations already reported to prevent flooding @@ -28,10 +35,25 @@ class IntegrationFrame: """Integration frame container.""" custom_integration: bool - frame: FrameSummary integration: str module: str | None relative_filename: str + _frame: FrameType + + @cached_property + def line_number(self) -> int: + """Return the line number of the frame.""" + return self._frame.f_lineno + + @cached_property + def filename(self) -> str: + """Return the filename of the frame.""" + return self._frame.f_code.co_filename + + @cached_property + def line(self) -> str: + """Return the line of the frame.""" + return (linecache.getline(self.filename, self.line_number) or "?").strip() def get_integration_logger(fallback_name: str) -> logging.Logger: @@ -54,19 +76,28 @@ def get_integration_logger(fallback_name: str) -> logging.Logger: return logging.getLogger(logger_name) +def get_current_frame(depth: int = 0) -> FrameType: + """Return the current frame.""" + # Add one to depth since get_current_frame is included + return sys._getframe(depth + 1) # pylint: disable=protected-access + + def get_integration_frame(exclude_integrations: set | None = None) -> IntegrationFrame: """Return the frame, integration and integration path of the current stack frame.""" found_frame = None if not exclude_integrations: exclude_integrations = set() - for frame in reversed(extract_stack()): + frame: FrameType | None = get_current_frame() + while frame is not None: + filename = frame.f_code.co_filename + for path in ("custom_components/", "homeassistant/components/"): try: - index = frame.filename.index(path) + index = filename.index(path) start = index + len(path) - end = frame.filename.index("/", start) - integration = frame.filename[start:end] + end = filename.index("/", start) + integration = filename[start:end] if integration not in exclude_integrations: found_frame = frame @@ -77,6 +108,8 @@ def get_integration_frame(exclude_integrations: set | None = None) -> Integratio if found_frame is not None: break + frame = frame.f_back + if found_frame is None: raise MissingIntegrationFrame @@ -84,16 +117,16 @@ def get_integration_frame(exclude_integrations: set | None = None) -> Integratio for module, module_obj in dict(sys.modules).items(): if not hasattr(module_obj, "__file__"): continue - if module_obj.__file__ == found_frame.filename: + if module_obj.__file__ == found_frame.f_code.co_filename: found_module = module break return IntegrationFrame( custom_integration=path == "custom_components/", - frame=found_frame, integration=integration, module=found_module, - relative_filename=found_frame.filename[index:], + relative_filename=found_frame.f_code.co_filename[index:], + _frame=found_frame, ) @@ -137,9 +170,8 @@ def _report_integration( Async friendly. """ - found_frame = integration_frame.frame # Keep track of integrations already reported to prevent flooding - key = f"{found_frame.filename}:{found_frame.lineno}" + key = f"{integration_frame.filename}:{integration_frame.line_number}" if key in _REPORTED_INTEGRATIONS: return _REPORTED_INTEGRATIONS.add(key) @@ -160,8 +192,8 @@ def _report_integration( integration_frame.integration, what, integration_frame.relative_filename, - found_frame.lineno, - (found_frame.line or "?").strip(), + integration_frame.line_number, + integration_frame.line, report_issue, ) diff --git a/homeassistant/helpers/group.py b/homeassistant/helpers/group.py index 437df226118..7d4eeb6d133 100644 --- a/homeassistant/helpers/group.py +++ b/homeassistant/helpers/group.py @@ -1,4 +1,5 @@ """Helper for groups.""" + from __future__ import annotations from collections.abc import Iterable diff --git a/homeassistant/helpers/http.py b/homeassistant/helpers/http.py index 63ff173a3a0..a464056fc07 100644 --- a/homeassistant/helpers/http.py +++ b/homeassistant/helpers/http.py @@ -1,4 +1,5 @@ """Helper to track the current http request.""" + from __future__ import annotations import asyncio @@ -10,13 +11,13 @@ from typing import Any, Final from aiohttp import web from aiohttp.typedefs import LooseHeaders -from aiohttp.web import Request +from aiohttp.web import AppKey, Request from aiohttp.web_exceptions import ( HTTPBadRequest, HTTPInternalServerError, HTTPUnauthorized, ) -from aiohttp.web_urldispatcher import AbstractRoute +from aiohttp.web_urldispatcher import AbstractResource, AbstractRoute import voluptuous as vol from homeassistant import exceptions @@ -29,7 +30,11 @@ from .json import find_paths_unserializable_data, json_bytes, json_dumps _LOGGER = logging.getLogger(__name__) +AllowCorsType = Callable[[AbstractRoute | AbstractResource], None] KEY_AUTHENTICATED: Final = "ha_authenticated" +KEY_ALLOW_ALL_CORS = AppKey[AllowCorsType]("allow_all_cors") +KEY_ALLOW_CONFIGRED_CORS = AppKey[AllowCorsType]("allow_configured_cors") +KEY_HASS: AppKey[HomeAssistant] = AppKey("hass") current_request: ContextVar[Request | None] = ContextVar( "current_request", default=None @@ -53,7 +58,7 @@ def request_handler_factory( authenticated = request.get(KEY_AUTHENTICATED, False) if view.requires_auth and not authenticated: - raise HTTPUnauthorized() + raise HTTPUnauthorized if _LOGGER.isEnabledFor(logging.DEBUG): _LOGGER.debug( @@ -69,11 +74,11 @@ def request_handler_factory( else: result = handler(request, **request.match_info) except vol.Invalid as err: - raise HTTPBadRequest() from err + raise HTTPBadRequest from err except exceptions.ServiceNotFound as err: - raise HTTPInternalServerError() from err + raise HTTPInternalServerError from err except exceptions.Unauthorized as err: - raise HTTPUnauthorized() from err + raise HTTPUnauthorized from err if isinstance(result, web.StreamResponse): # The method handler returned a ready-made Response, how nice of it @@ -161,7 +166,7 @@ class HomeAssistantView: ) -> None: """Register the view with a router.""" assert self.url is not None, "No url set for view" - urls = [self.url] + self.extra_urls + urls = [self.url, *self.extra_urls] routes: list[AbstractRoute] = [] for method in ("get", "post", "delete", "put", "patch", "head", "options"): @@ -170,14 +175,13 @@ class HomeAssistantView: handler = request_handler_factory(hass, self, handler) - for url in urls: - routes.append(router.add_route(method, url, handler)) + routes.extend(router.add_route(method, url, handler) for url in urls) # Use `get` because CORS middleware is not be loaded in emulated_hue if self.cors_allowed: - allow_cors = app.get("allow_all_cors") + allow_cors = app.get(KEY_ALLOW_ALL_CORS) else: - allow_cors = app.get("allow_configured_cors") + allow_cors = app.get(KEY_ALLOW_CONFIGRED_CORS) if allow_cors: for route in routes: diff --git a/homeassistant/helpers/httpx_client.py b/homeassistant/helpers/httpx_client.py index ed02f8a710e..2855705b9c1 100644 --- a/homeassistant/helpers/httpx_client.py +++ b/homeassistant/helpers/httpx_client.py @@ -1,4 +1,5 @@ """Helper for httpx.""" + from __future__ import annotations from collections.abc import Callable @@ -25,8 +26,9 @@ KEEP_ALIVE_TIMEOUT = 15 DATA_ASYNC_CLIENT = "httpx_async_client" DATA_ASYNC_CLIENT_NOVERIFY = "httpx_async_client_noverify" DEFAULT_LIMITS = limits = httpx.Limits(keepalive_expiry=KEEP_ALIVE_TIMEOUT) -SERVER_SOFTWARE = "{0}/{1} httpx/{2} Python/{3[0]}.{3[1]}".format( - APPLICATION_NAME, __version__, httpx.__version__, sys.version_info +SERVER_SOFTWARE = ( + f"{APPLICATION_NAME}/{__version__} " + f"httpx/{httpx.__version__} Python/{sys.version_info[0]}.{sys.version_info[1]}" ) USER_AGENT = "User-Agent" diff --git a/homeassistant/helpers/icon.py b/homeassistant/helpers/icon.py index f1638732527..973c93674b1 100644 --- a/homeassistant/helpers/icon.py +++ b/homeassistant/helpers/icon.py @@ -1,4 +1,5 @@ """Icon helper methods.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/helpers/importlib.py b/homeassistant/helpers/importlib.py new file mode 100644 index 00000000000..00af75f6d8e --- /dev/null +++ b/homeassistant/helpers/importlib.py @@ -0,0 +1,65 @@ +"""Helper to import modules from asyncio.""" + +from __future__ import annotations + +import asyncio +from contextlib import suppress +import importlib +import logging +import sys +from types import ModuleType + +from homeassistant.core import HomeAssistant + +_LOGGER = logging.getLogger(__name__) + +DATA_IMPORT_CACHE = "import_cache" +DATA_IMPORT_FUTURES = "import_futures" +DATA_IMPORT_FAILURES = "import_failures" + + +def _get_module(cache: dict[str, ModuleType], name: str) -> ModuleType: + """Get a module.""" + cache[name] = importlib.import_module(name) + return cache[name] + + +async def async_import_module(hass: HomeAssistant, name: str) -> ModuleType: + """Import a module or return it from the cache.""" + cache: dict[str, ModuleType] = hass.data.setdefault(DATA_IMPORT_CACHE, {}) + if module := cache.get(name): + return module + + failure_cache: dict[str, BaseException] = hass.data.setdefault( + DATA_IMPORT_FAILURES, {} + ) + if exception := failure_cache.get(name): + raise exception + + import_futures: dict[str, asyncio.Future[ModuleType]] + import_futures = hass.data.setdefault(DATA_IMPORT_FUTURES, {}) + + if future := import_futures.get(name): + return await future + + if name in sys.modules: + return _get_module(cache, name) + + import_future = hass.loop.create_future() + import_futures[name] = import_future + try: + module = await hass.async_add_import_executor_job(_get_module, cache, name) + import_future.set_result(module) + except BaseException as ex: + failure_cache[name] = ex + import_future.set_exception(ex) + with suppress(BaseException): + # Set the exception retrieved flag on the future since + # it will never be retrieved unless there + # are concurrent calls + import_future.result() + raise + finally: + del import_futures[name] + + return module diff --git a/homeassistant/helpers/instance_id.py b/homeassistant/helpers/instance_id.py index 5bb8be5a9fe..8bad8f90b9c 100644 --- a/homeassistant/helpers/instance_id.py +++ b/homeassistant/helpers/instance_id.py @@ -1,4 +1,5 @@ """Helper to create a unique instance ID.""" + from __future__ import annotations import logging diff --git a/homeassistant/helpers/integration_platform.py b/homeassistant/helpers/integration_platform.py index 138722bd455..6d474557748 100644 --- a/homeassistant/helpers/integration_platform.py +++ b/homeassistant/helpers/integration_platform.py @@ -1,4 +1,5 @@ """Helpers to help with integration platforms.""" + from __future__ import annotations import asyncio @@ -10,18 +11,17 @@ from types import ModuleType from typing import Any from homeassistant.const import EVENT_COMPONENT_LOADED -from homeassistant.core import HassJob, HomeAssistant, callback +from homeassistant.core import Event, HassJob, HomeAssistant, callback from homeassistant.loader import ( Integration, async_get_integrations, async_get_loaded_integration, + async_register_preload_platform, bind_hass, ) from homeassistant.setup import ATTR_COMPONENT, EventComponentLoaded from homeassistant.util.logging import catch_log_exception -from .typing import EventType - _LOGGER = logging.getLogger(__name__) DATA_INTEGRATION_PLATFORMS = "integration_platforms" @@ -36,55 +36,114 @@ class IntegrationPlatform: @callback -def _get_platform( - integration: Integration | Exception, component_name: str, platform_name: str -) -> ModuleType | None: - """Get a platform from an integration.""" - if isinstance(integration, Exception): - _LOGGER.exception( - "Error importing integration %s for %s", - component_name, - platform_name, - ) - return None - - try: - return integration.get_platform(platform_name) - except ImportError as err: - if f"{component_name}.{platform_name}" not in str(err): - _LOGGER.exception( - "Unexpected error importing %s/%s.py", - component_name, - platform_name, - ) - - return None - - -@callback -def _async_process_integration_platforms_for_component( +def _async_integration_platform_component_loaded( hass: HomeAssistant, integration_platforms: list[IntegrationPlatform], - event: EventType[EventComponentLoaded], + event: Event[EventComponentLoaded], ) -> None: """Process integration platforms for a component.""" - component_name = event.data[ATTR_COMPONENT] - if "." in component_name: + if "." in (component_name := event.data[ATTR_COMPONENT]): return integration = async_get_loaded_integration(hass, component_name) + # First filter out platforms that the integration already processed. + integration_platforms_by_name: dict[str, IntegrationPlatform] = {} for integration_platform in integration_platforms: - if component_name in integration_platform.seen_components or not ( - platform := _get_platform( - integration, component_name, integration_platform.platform_name - ) - ): + if component_name in integration_platform.seen_components: continue integration_platform.seen_components.add(component_name) - hass.async_run_hass_job( - integration_platform.process_job, hass, component_name, platform + integration_platforms_by_name[integration_platform.platform_name] = ( + integration_platform ) + if not integration_platforms_by_name: + return + + # Next, check which platforms exist for this integration. + platforms_that_exist = integration.platforms_exists(integration_platforms_by_name) + if not platforms_that_exist: + return + + # If everything is already loaded, we can avoid creating a task. + can_use_cache = True + platforms: dict[str, ModuleType] = {} + for platform_name in platforms_that_exist: + if platform := integration.get_platform_cached(platform_name): + platforms[platform_name] = platform + else: + can_use_cache = False + break + + if can_use_cache: + _process_integration_platforms( + hass, + integration, + platforms, + integration_platforms_by_name, + ) + return + + # At least one of the platforms is not loaded, we need to load them + # so we have to fall back to creating a task. + hass.async_create_task( + _async_process_integration_platforms_for_component( + hass, integration, platforms_that_exist, integration_platforms_by_name + ), + eager_start=True, + ) + + +async def _async_process_integration_platforms_for_component( + hass: HomeAssistant, + integration: Integration, + platforms_that_exist: list[str], + integration_platforms_by_name: dict[str, IntegrationPlatform], +) -> None: + """Process integration platforms for a component.""" + # Now we know which platforms to load, let's load them. + try: + platforms = await integration.async_get_platforms(platforms_that_exist) + except ImportError: + _LOGGER.debug( + "Unexpected error importing integration platforms for %s", + integration.domain, + ) + return + + if futures := _process_integration_platforms( + hass, + integration, + platforms, + integration_platforms_by_name, + ): + await asyncio.gather(*futures) + + +@callback +def _process_integration_platforms( + hass: HomeAssistant, + integration: Integration, + platforms: dict[str, ModuleType], + integration_platforms_by_name: dict[str, IntegrationPlatform], +) -> list[asyncio.Future[Awaitable[None] | None]]: + """Process integration platforms for a component. + + Only the platforms that are passed in will be processed. + """ + return [ + future + for platform_name, platform in platforms.items() + if (integration_platform := integration_platforms_by_name[platform_name]) + and ( + future := hass.async_run_hass_job( + integration_platform.process_job, + hass, + integration.domain, + platform, + ) + ) + ] + def _format_err(name: str, platform_name: str, *args: Any) -> str: """Format error message.""" @@ -97,6 +156,7 @@ async def async_process_integration_platforms( platform_name: str, # Any = platform. process_platform: Callable[[HomeAssistant, str, Any], Awaitable[None] | None], + wait_for_platforms: bool = False, ) -> None: """Process a specific platform for all current and future loaded integrations.""" if DATA_INTEGRATION_PLATFORMS not in hass.data: @@ -105,14 +165,16 @@ async def async_process_integration_platforms( hass.bus.async_listen( EVENT_COMPONENT_LOADED, partial( - _async_process_integration_platforms_for_component, + _async_integration_platform_component_loaded, hass, integration_platforms, ), + run_immediately=True, ) else: integration_platforms = hass.data[DATA_INTEGRATION_PLATFORMS] + async_register_preload_platform(hass, platform_name) top_level_components = {comp for comp in hass.config.components if "." not in comp} process_job = HassJob( catch_log_exception( @@ -124,16 +186,72 @@ async def async_process_integration_platforms( integration_platform = IntegrationPlatform( platform_name, process_job, top_level_components ) + # Tell the loader that it should try to pre-load the integration + # for any future components that are loaded so we can reduce the + # amount of import executor usage. + async_register_preload_platform(hass, platform_name) integration_platforms.append(integration_platform) - if not top_level_components: return + # We create a task here for two reasons: + # + # 1. We want the integration that provides the integration platform to + # not be delayed by waiting on each individual platform to be processed + # since the import or the integration platforms themselves may have to + # schedule I/O or executor jobs. + # + # 2. We want the behavior to be the same as if the integration that has + # the integration platform is loaded after the platform is processed. + # + # We use hass.async_create_task instead of asyncio.create_task because + # we want to make sure that startup waits for the task to complete. + # + future = hass.async_create_task( + _async_process_integration_platforms( + hass, platform_name, top_level_components.copy(), process_job + ), + eager_start=True, + ) + if wait_for_platforms: + await future + + +async def _async_process_integration_platforms( + hass: HomeAssistant, + platform_name: str, + top_level_components: set[str], + process_job: HassJob, +) -> None: + """Process integration platforms for a component.""" integrations = await async_get_integrations(hass, top_level_components) - if futures := [ - future - for comp in top_level_components - if (platform := _get_platform(integrations[comp], comp, platform_name)) - and (future := hass.async_run_hass_job(process_job, hass, comp, platform)) - ]: + loaded_integrations: list[Integration] = [ + integration + for integration in integrations.values() + if not isinstance(integration, Exception) + ] + # Finally, fetch the platforms for each integration and process them. + # This uses the import executor in a loop. If there are a lot + # of integration with the integration platform to process, + # this could be a bottleneck. + futures: list[asyncio.Future[None]] = [] + for integration in loaded_integrations: + if not integration.platforms_exists((platform_name,)): + continue + try: + platform = await integration.async_get_platform(platform_name) + except ImportError: + _LOGGER.debug( + "Unexpected error importing %s for %s", + platform_name, + integration.domain, + ) + continue + + if future := hass.async_run_hass_job( + process_job, hass, integration.domain, platform + ): + futures.append(future) + + if futures: await asyncio.gather(*futures) diff --git a/homeassistant/helpers/intent.py b/homeassistant/helpers/intent.py index 82385f0cda8..fcebf91b854 100644 --- a/homeassistant/helpers/intent.py +++ b/homeassistant/helpers/intent.py @@ -24,7 +24,13 @@ from homeassistant.core import Context, HomeAssistant, State, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.loader import bind_hass -from . import area_registry, config_validation as cv, device_registry, entity_registry +from . import ( + area_registry, + config_validation as cv, + device_registry, + entity_registry, + floor_registry, +) _LOGGER = logging.getLogger(__name__) _SlotsType = dict[str, Any] @@ -144,16 +150,18 @@ class NoStatesMatchedError(IntentError): def __init__( self, - name: str | None, - area: str | None, - domains: set[str] | None, - device_classes: set[str] | None, + name: str | None = None, + area: str | None = None, + floor: str | None = None, + domains: set[str] | None = None, + device_classes: set[str] | None = None, ) -> None: """Initialize error.""" super().__init__() self.name = name self.area = area + self.floor = floor self.domains = domains self.device_classes = device_classes @@ -197,11 +205,7 @@ def _has_name( if (entity is None) or (not entity.aliases): return False - for alias in entity.aliases: - if name == alias.casefold(): - return True - - return False + return any(name == alias.casefold() for alias in entity.aliases) def _find_area( @@ -224,12 +228,35 @@ def _find_area( return None -def _filter_by_area( +def _find_floor( + id_or_name: str, floors: floor_registry.FloorRegistry +) -> floor_registry.FloorEntry | None: + """Find an floor by id or name, checking aliases too.""" + floor = floors.async_get_floor(id_or_name) or floors.async_get_floor_by_name( + id_or_name + ) + if floor is not None: + return floor + + # Check floor aliases + for maybe_floor in floors.floors.values(): + if not maybe_floor.aliases: + continue + + for floor_alias in maybe_floor.aliases: + if id_or_name == floor_alias.casefold(): + return maybe_floor + + return None + + +def _filter_by_areas( states_and_entities: list[tuple[State, entity_registry.RegistryEntry | None]], - area: area_registry.AreaEntry, + areas: Iterable[area_registry.AreaEntry], devices: device_registry.DeviceRegistry, ) -> Iterable[tuple[State, entity_registry.RegistryEntry | None]]: """Filter state/entity pairs by an area.""" + filter_area_ids: set[str | None] = {a.id for a in areas} entity_area_ids: dict[str, str | None] = {} for _state, entity in states_and_entities: if entity is None: @@ -245,7 +272,7 @@ def _filter_by_area( entity_area_ids[entity.id] = device.area_id for state, entity in states_and_entities: - if (entity is not None) and (entity_area_ids.get(entity.id) == area.id): + if (entity is not None) and (entity_area_ids.get(entity.id) in filter_area_ids): yield (state, entity) @@ -256,11 +283,14 @@ def async_match_states( name: str | None = None, area_name: str | None = None, area: area_registry.AreaEntry | None = None, + floor_name: str | None = None, + floor: floor_registry.FloorEntry | None = None, domains: Collection[str] | None = None, device_classes: Collection[str] | None = None, states: Iterable[State] | None = None, entities: entity_registry.EntityRegistry | None = None, areas: area_registry.AreaRegistry | None = None, + floors: floor_registry.FloorRegistry | None = None, devices: device_registry.DeviceRegistry | None = None, assistant: str | None = None, ) -> Iterable[State]: @@ -272,6 +302,15 @@ def async_match_states( if entities is None: entities = entity_registry.async_get(hass) + if devices is None: + devices = device_registry.async_get(hass) + + if areas is None: + areas = area_registry.async_get(hass) + + if floors is None: + floors = floor_registry.async_get(hass) + # Gather entities states_and_entities: list[tuple[State, entity_registry.RegistryEntry | None]] = [] for state in states: @@ -298,20 +337,35 @@ def async_match_states( if _is_device_class(state, entity, device_classes) ] + filter_areas: list[area_registry.AreaEntry] = [] + + if (floor is None) and (floor_name is not None): + # Look up floor by name + floor = _find_floor(floor_name, floors) + if floor is None: + _LOGGER.warning("Floor not found: %s", floor_name) + return + + if floor is not None: + filter_areas = [ + a for a in areas.async_list_areas() if a.floor_id == floor.floor_id + ] + if (area is None) and (area_name is not None): # Look up area by name - if areas is None: - areas = area_registry.async_get(hass) - area = _find_area(area_name, areas) - assert area is not None, f"No area named {area_name}" + if area is None: + _LOGGER.warning("Area not found: %s", area_name) + return if area is not None: - # Filter by states/entities by area - if devices is None: - devices = device_registry.async_get(hass) + filter_areas = [area] - states_and_entities = list(_filter_by_area(states_and_entities, area, devices)) + if filter_areas: + # Filter by states/entities by area + states_and_entities = list( + _filter_by_areas(states_and_entities, filter_areas, devices) + ) if assistant is not None: # Filter by exposure @@ -322,9 +376,6 @@ def async_match_states( ] if name is not None: - if devices is None: - devices = device_registry.async_get(hass) - # Filter by name name = name.casefold() @@ -379,7 +430,7 @@ class IntentHandler: async def async_handle(self, intent_obj: Intent) -> IntentResponse: """Handle the intent.""" - raise NotImplementedError() + raise NotImplementedError def __repr__(self) -> str: """Represent a string of an intent handler.""" @@ -393,7 +444,7 @@ class DynamicServiceIntentHandler(IntentHandler): """ slot_schema = { - vol.Any("name", "area"): cv.string, + vol.Any("name", "area", "floor"): cv.string, vol.Optional("domain"): vol.All(cv.ensure_list, [cv.string]), vol.Optional("device_class"): vol.All(cv.ensure_list, [cv.string]), } @@ -443,7 +494,7 @@ class DynamicServiceIntentHandler(IntentHandler): self, intent_obj: Intent, state: State ) -> tuple[str, str]: """Get the domain and service name to call.""" - raise NotImplementedError() + raise NotImplementedError async def async_handle(self, intent_obj: Intent) -> IntentResponse: """Handle the hass intent.""" @@ -457,7 +508,7 @@ class DynamicServiceIntentHandler(IntentHandler): # Don't match on name if targeting all entities entity_name = None - # Look up area first to fail early + # Look up area to fail early area_slot = slots.get("area", {}) area_id = area_slot.get("value") area_name = area_slot.get("text") @@ -468,6 +519,17 @@ class DynamicServiceIntentHandler(IntentHandler): if area is None: raise IntentHandleError(f"No area named {area_name}") + # Look up floor to fail early + floor_slot = slots.get("floor", {}) + floor_id = floor_slot.get("value") + floor_name = floor_slot.get("text") + floor: floor_registry.FloorEntry | None = None + if floor_id is not None: + floors = floor_registry.async_get(hass) + floor = floors.async_get_floor(floor_id) + if floor is None: + raise IntentHandleError(f"No floor named {floor_name}") + # Optional domain/device class filters. # Convert to sets for speed. domains: set[str] | None = None @@ -484,6 +546,7 @@ class DynamicServiceIntentHandler(IntentHandler): hass, name=entity_name, area=area, + floor=floor, domains=domains, device_classes=device_classes, assistant=intent_obj.assistant, @@ -495,6 +558,7 @@ class DynamicServiceIntentHandler(IntentHandler): raise NoStatesMatchedError( name=entity_text or entity_name, area=area_name or area_id, + floor=floor_name or floor_id, domains=domains, device_classes=device_classes, ) diff --git a/homeassistant/helpers/issue_registry.py b/homeassistant/helpers/issue_registry.py index 27d568a13de..11bde0edf6b 100644 --- a/homeassistant/helpers/issue_registry.py +++ b/homeassistant/helpers/issue_registry.py @@ -1,4 +1,5 @@ """Persistently store issues raised by integrations.""" + from __future__ import annotations import dataclasses @@ -14,6 +15,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.util.async_ import run_callback_threadsafe import homeassistant.util.dt as dt_util +from .registry import BaseRegistry from .storage import Store DATA_REGISTRY = "issue_registry" @@ -21,7 +23,6 @@ EVENT_REPAIRS_ISSUE_REGISTRY_UPDATED = "repairs_issue_registry_updated" STORAGE_KEY = "repairs.issue_registry" STORAGE_VERSION_MAJOR = 1 STORAGE_VERSION_MINOR = 2 -SAVE_DELAY = 10 class IssueSeverity(StrEnum): @@ -92,7 +93,7 @@ class IssueRegistryStore(Store[dict[str, list[dict[str, Any]]]]): return old_data -class IssueRegistry: +class IssueRegistry(BaseRegistry): """Class to hold a registry of issues.""" def __init__(self, hass: HomeAssistant, *, read_only: bool = False) -> None: @@ -259,11 +260,6 @@ class IssueRegistry: 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.""" diff --git a/homeassistant/helpers/json.py b/homeassistant/helpers/json.py index ba2486a196e..28b3d509a0c 100644 --- a/homeassistant/helpers/json.py +++ b/homeassistant/helpers/json.py @@ -1,4 +1,5 @@ """Helpers to help with encoding Home Assistant objects in JSON.""" + from collections import deque from collections.abc import Callable import datetime diff --git a/homeassistant/helpers/label_registry.py b/homeassistant/helpers/label_registry.py index 9c7f20a6515..b3ca89140a1 100644 --- a/homeassistant/helpers/label_registry.py +++ b/homeassistant/helpers/label_registry.py @@ -1,23 +1,28 @@ """Provide a way to label and group anything.""" + from __future__ import annotations -from collections import UserDict -from collections.abc import Iterable, ValuesView +from collections.abc import Iterable import dataclasses from dataclasses import dataclass from typing import Literal, TypedDict, cast -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import Event, HomeAssistant, callback from homeassistant.util import slugify +from .normalized_name_base_registry import ( + NormalizedNameBaseRegistryEntry, + NormalizedNameBaseRegistryItems, + normalize_name, +) +from .registry import BaseRegistry from .storage import Store -from .typing import UNDEFINED, EventType, UndefinedType +from .typing import UNDEFINED, UndefinedType DATA_REGISTRY = "label_registry" EVENT_LABEL_REGISTRY_UPDATED = "label_registry_updated" STORAGE_KEY = "core.label_registry" STORAGE_VERSION_MAJOR = 1 -SAVE_DELAY = 10 class EventLabelRegistryUpdatedData(TypedDict): @@ -27,71 +32,23 @@ class EventLabelRegistryUpdatedData(TypedDict): label_id: str -EventLabelRegistryUpdated = EventType[EventLabelRegistryUpdatedData] +EventLabelRegistryUpdated = Event[EventLabelRegistryUpdatedData] -@dataclass(slots=True, frozen=True) -class LabelEntry: +@dataclass(slots=True, frozen=True, kw_only=True) +class LabelEntry(NormalizedNameBaseRegistryEntry): """Label Registry Entry.""" label_id: str - name: str - normalized_name: str description: str | None = None color: str | None = None icon: str | None = None -class LabelRegistryItems(UserDict[str, LabelEntry]): - """Container for label registry items, maps label id -> entry. - - Maintains an additional index: - - normalized name -> entry - """ - - def __init__(self) -> None: - """Initialize the container.""" - super().__init__() - self._normalized_names: dict[str, LabelEntry] = {} - - def values(self) -> ValuesView[LabelEntry]: - """Return the underlying values to avoid __iter__ overhead.""" - return self.data.values() - - def __setitem__(self, key: str, entry: LabelEntry) -> None: - """Add an item.""" - data = self.data - normalized_name = _normalize_label_name(entry.name) - - if key in data: - old_entry = data[key] - if ( - normalized_name != old_entry.normalized_name - and normalized_name in self._normalized_names - ): - raise ValueError( - f"The name {entry.name} ({normalized_name}) is already in use" - ) - del self._normalized_names[old_entry.normalized_name] - data[key] = entry - self._normalized_names[normalized_name] = entry - - def __delitem__(self, key: str) -> None: - """Remove an item.""" - entry = self[key] - normalized_name = _normalize_label_name(entry.name) - del self._normalized_names[normalized_name] - super().__delitem__(key) - - def get_label_by_name(self, name: str) -> LabelEntry | None: - """Get label by name.""" - return self._normalized_names.get(_normalize_label_name(name)) - - -class LabelRegistry: +class LabelRegistry(BaseRegistry): """Class to hold a registry of labels.""" - labels: LabelRegistryItems + labels: NormalizedNameBaseRegistryItems[LabelEntry] _label_data: dict[str, LabelEntry] def __init__(self, hass: HomeAssistant) -> None: @@ -116,7 +73,7 @@ class LabelRegistry: @callback def async_get_label_by_name(self, name: str) -> LabelEntry | None: """Get label by name.""" - return self.labels.get_label_by_name(name) + return self.labels.get_by_name(name) @callback def async_list_labels(self) -> Iterable[LabelEntry]: @@ -148,7 +105,7 @@ class LabelRegistry: f"The name {name} ({label.normalized_name}) is already in use" ) - normalized_name = _normalize_label_name(name) + normalized_name = normalize_name(name) label = LabelEntry( color=color, @@ -207,7 +164,7 @@ class LabelRegistry: if name is not UNDEFINED and name != old.name: changes["name"] = name - changes["normalized_name"] = _normalize_label_name(name) + changes["normalized_name"] = normalize_name(name) if not changes: return old @@ -228,7 +185,7 @@ class LabelRegistry: async def async_load(self) -> None: """Load the label registry.""" data = await self._store.async_load() - labels = LabelRegistryItems() + labels = NormalizedNameBaseRegistryItems[LabelEntry]() if data is not None: for label in data["labels"]: @@ -236,7 +193,7 @@ class LabelRegistry: if label["label_id"] is None or label["name"] is None: continue - normalized_name = _normalize_label_name(label["name"]) + normalized_name = normalize_name(label["name"]) labels[label["label_id"]] = LabelEntry( color=label["color"], description=label["description"], @@ -249,11 +206,6 @@ class LabelRegistry: self.labels = labels self._label_data = labels.data - @callback - def async_schedule_save(self) -> None: - """Schedule saving the label 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 label registry to store in a file.""" @@ -282,8 +234,3 @@ async def async_load(hass: HomeAssistant) -> None: assert DATA_REGISTRY not in hass.data hass.data[DATA_REGISTRY] = LabelRegistry(hass) await hass.data[DATA_REGISTRY].async_load() - - -def _normalize_label_name(label_name: str) -> str: - """Normalize a label name by removing whitespace and case folding.""" - return label_name.casefold().replace(" ", "") diff --git a/homeassistant/helpers/location.py b/homeassistant/helpers/location.py index 086150115da..a12de4f9029 100644 --- a/homeassistant/helpers/location.py +++ b/homeassistant/helpers/location.py @@ -1,4 +1,5 @@ """Location helpers for Home Assistant.""" + from __future__ import annotations from collections.abc import Iterable diff --git a/homeassistant/helpers/network.py b/homeassistant/helpers/network.py index 58ca191feb0..ed6339f9996 100644 --- a/homeassistant/helpers/network.py +++ b/homeassistant/helpers/network.py @@ -1,11 +1,12 @@ """Network helpers.""" + from __future__ import annotations from collections.abc import Callable from contextlib import suppress from ipaddress import ip_address -from typing import cast +from hass_nabucasa import remote import yarl from homeassistant.components import http @@ -40,7 +41,11 @@ def get_supervisor_network_url( hass: HomeAssistant, *, allow_ssl: bool = False ) -> str | None: """Get URL for home assistant within supervisor network.""" - if hass.config.api is None or not hass.components.hassio.is_hassio(): + # Local import to avoid circular dependencies + # pylint: disable-next=import-outside-toplevel + from homeassistant.components.hassio import is_hassio + + if hass.config.api is None or not is_hassio(hass): return None scheme = "http" @@ -170,14 +175,17 @@ def get_url( and request_host is not None and hass.config.api is not None ): + # Local import to avoid circular dependencies + # pylint: disable-next=import-outside-toplevel + from homeassistant.components.hassio import get_host_info, is_hassio + scheme = "https" if hass.config.api.use_ssl else "http" current_url = yarl.URL.build( scheme=scheme, host=request_host, port=hass.config.api.port ) known_hostnames = ["localhost"] - if hass.components.hassio.is_hassio(): - host_info = hass.components.hassio.get_host_info() + if is_hassio(hass) and (host_info := get_host_info(hass)): known_hostnames.extend( [host_info["hostname"], f"{host_info['hostname']}.local"] ) @@ -290,9 +298,16 @@ def _get_external_url( def _get_cloud_url(hass: HomeAssistant, require_current_request: bool = False) -> str: """Get external Home Assistant Cloud URL of this instance.""" if "cloud" in hass.config.components: + # Local import to avoid circular dependencies + # pylint: disable-next=import-outside-toplevel + from homeassistant.components.cloud import ( + CloudNotAvailable, + async_remote_ui_url, + ) + try: - cloud_url = yarl.URL(cast(str, hass.components.cloud.async_remote_ui_url())) - except hass.components.cloud.CloudNotAvailable as err: + cloud_url = yarl.URL(async_remote_ui_url(hass)) + except CloudNotAvailable as err: raise NoURLAvailableError from err if not require_current_request or cloud_url.host == _get_request_host(): @@ -307,6 +322,4 @@ def is_cloud_connection(hass: HomeAssistant) -> bool: if "cloud" not in hass.config.components: return False - from hass_nabucasa import remote # pylint: disable=import-outside-toplevel - return remote.is_cloud_request.get() diff --git a/homeassistant/helpers/normalized_name_base_registry.py b/homeassistant/helpers/normalized_name_base_registry.py new file mode 100644 index 00000000000..16280a73750 --- /dev/null +++ b/homeassistant/helpers/normalized_name_base_registry.py @@ -0,0 +1,68 @@ +"""Provide a base class for registries that use a normalized name index.""" + +from collections import UserDict +from collections.abc import ValuesView +from dataclasses import dataclass +from typing import TypeVar + + +@dataclass(slots=True, frozen=True, kw_only=True) +class NormalizedNameBaseRegistryEntry: + """Normalized Name Base Registry Entry.""" + + name: str + normalized_name: str + + +_VT = TypeVar("_VT", bound=NormalizedNameBaseRegistryEntry) + + +def normalize_name(name: str) -> str: + """Normalize a name by removing whitespace and case folding.""" + return name.casefold().replace(" ", "") + + +class NormalizedNameBaseRegistryItems(UserDict[str, _VT]): + """Base container for normalized name registry items, maps key -> entry. + + Maintains an additional index: + - normalized name -> entry + """ + + def __init__(self) -> None: + """Initialize the container.""" + super().__init__() + self._normalized_names: dict[str, _VT] = {} + + def values(self) -> ValuesView[_VT]: + """Return the underlying values to avoid __iter__ overhead.""" + return self.data.values() + + def __setitem__(self, key: str, entry: _VT) -> None: + """Add an item.""" + data = self.data + normalized_name = normalize_name(entry.name) + + if key in data: + old_entry = data[key] + if ( + normalized_name != old_entry.normalized_name + and normalized_name in self._normalized_names + ): + raise ValueError( + f"The name {entry.name} ({normalized_name}) is already in use" + ) + del self._normalized_names[old_entry.normalized_name] + data[key] = entry + self._normalized_names[normalized_name] = entry + + def __delitem__(self, key: str) -> None: + """Remove an item.""" + entry = self[key] + normalized_name = normalize_name(entry.name) + del self._normalized_names[normalized_name] + super().__delitem__(key) + + def get_by_name(self, name: str) -> _VT | None: + """Get entry by name.""" + return self._normalized_names.get(normalize_name(name)) diff --git a/homeassistant/helpers/ratelimit.py b/homeassistant/helpers/ratelimit.py index 12a4cfac406..516d4134f76 100644 --- a/homeassistant/helpers/ratelimit.py +++ b/homeassistant/helpers/ratelimit.py @@ -1,14 +1,14 @@ """Ratelimit helper.""" + from __future__ import annotations import asyncio from collections.abc import Callable, Hashable -from datetime import datetime, timedelta import logging +import time from typing import TypeVarTuple from homeassistant.core import HomeAssistant, callback -import homeassistant.util.dt as dt_util _Ts = TypeVarTuple("_Ts") @@ -24,7 +24,7 @@ class KeyedRateLimit: ) -> None: """Initialize ratelimit tracker.""" self.hass = hass - self._last_triggered: dict[Hashable, datetime] = {} + self._last_triggered: dict[Hashable, float] = {} self._rate_limit_timers: dict[Hashable, asyncio.TimerHandle] = {} @callback @@ -33,10 +33,10 @@ class KeyedRateLimit: return bool(self._rate_limit_timers and key in self._rate_limit_timers) @callback - def async_triggered(self, key: Hashable, now: datetime | None = None) -> None: + def async_triggered(self, key: Hashable, now: float | None = None) -> None: """Call when the action we are tracking was triggered.""" self.async_cancel_timer(key) - self._last_triggered[key] = now or dt_util.utcnow() + self._last_triggered[key] = now or time.time() @callback def async_cancel_timer(self, key: Hashable) -> None: @@ -57,11 +57,11 @@ class KeyedRateLimit: def async_schedule_action( self, key: Hashable, - rate_limit: timedelta | None, - now: datetime, + rate_limit: float | None, + now: float, action: Callable[[*_Ts], None], *args: *_Ts, - ) -> datetime | None: + ) -> float | None: """Check rate limits and schedule an action if we hit the limit. If the rate limit is hit: @@ -96,7 +96,7 @@ class KeyedRateLimit: if key not in self._rate_limit_timers: self._rate_limit_timers[key] = self.hass.loop.call_later( - (next_call_time - now).total_seconds(), + next_call_time - now, action, *args, ) diff --git a/homeassistant/helpers/redact.py b/homeassistant/helpers/redact.py index f8df73b9180..ad06f58a50a 100644 --- a/homeassistant/helpers/redact.py +++ b/homeassistant/helpers/redact.py @@ -1,4 +1,5 @@ """Helpers to redact sensitive data.""" + from __future__ import annotations from collections.abc import Callable, Iterable, Mapping @@ -33,15 +34,13 @@ def partial_redact( @overload def async_redact_data( # type: ignore[overload-overlap] data: Mapping, to_redact: Iterable[Any] | Mapping[Any, Callable[[_ValueT], _ValueT]] -) -> dict: - ... +) -> dict: ... @overload def async_redact_data( data: _T, to_redact: Iterable[Any] | Mapping[Any, Callable[[_ValueT], _ValueT]] -) -> _T: - ... +) -> _T: ... @callback diff --git a/homeassistant/helpers/registry.py b/homeassistant/helpers/registry.py new file mode 100644 index 00000000000..d5b1035531a --- /dev/null +++ b/homeassistant/helpers/registry.py @@ -0,0 +1,34 @@ +"""Provide a base implementation for registries.""" + +from __future__ import annotations + +from abc import ABC, abstractmethod +from typing import TYPE_CHECKING, Any + +from homeassistant.core import CoreState, HomeAssistant, callback + +if TYPE_CHECKING: + from .storage import Store + +SAVE_DELAY = 10 +SAVE_DELAY_LONG = 180 + + +class BaseRegistry(ABC): + """Class to implement a registry.""" + + hass: HomeAssistant + _store: Store + + @callback + def async_schedule_save(self) -> None: + """Schedule saving the registry.""" + # Schedule the save past startup to avoid writing + # the file while the system is starting. + delay = SAVE_DELAY if self.hass.state is CoreState.running else SAVE_DELAY_LONG + self._store.async_delay_save(self._data_to_save, delay) + + @callback + @abstractmethod + def _data_to_save(self) -> dict[str, Any]: + """Return data of registry to store in a file.""" diff --git a/homeassistant/helpers/reload.py b/homeassistant/helpers/reload.py index 983b4e2da52..ffd6bdeb50d 100644 --- a/homeassistant/helpers/reload.py +++ b/homeassistant/helpers/reload.py @@ -1,4 +1,5 @@ """Class to reload platforms.""" + from __future__ import annotations import asyncio @@ -74,7 +75,7 @@ async def _resetup_platform( root_config[platform_domain].append(p_config) - component = integration.get_component() + component = await integration.async_get_component() if hasattr(component, "async_reset_platform"): # If the integration has its own way to reset @@ -138,8 +139,7 @@ async def _async_reconfig_platform( @overload async def async_integration_yaml_config( hass: HomeAssistant, integration_name: str -) -> ConfigType | None: - ... +) -> ConfigType | None: ... @overload @@ -148,8 +148,7 @@ async def async_integration_yaml_config( integration_name: str, *, raise_on_failure: Literal[True], -) -> ConfigType: - ... +) -> ConfigType: ... @overload @@ -158,8 +157,7 @@ async def async_integration_yaml_config( integration_name: str, *, raise_on_failure: Literal[False] | bool, -) -> ConfigType | None: - ... +) -> ConfigType | None: ... async def async_integration_yaml_config( diff --git a/homeassistant/helpers/restore_state.py b/homeassistant/helpers/restore_state.py index 7df83cd0ab9..7979247c8b0 100644 --- a/homeassistant/helpers/restore_state.py +++ b/homeassistant/helpers/restore_state.py @@ -1,4 +1,5 @@ """Support for restoring entity states on startup.""" + from __future__ import annotations from abc import ABC, abstractmethod @@ -143,7 +144,8 @@ class RestoreStateData: """Set up up the instance of this data helper.""" await self.async_load() - async def hass_start(hass: HomeAssistant) -> None: + @callback + def hass_start(hass: HomeAssistant) -> None: """Start the restore state task.""" self.async_setup_dump() @@ -251,7 +253,7 @@ class RestoreStateData: # Dump states when stopping hass self.hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, _async_dump_states_at_stop + EVENT_HOMEASSISTANT_STOP, _async_dump_states_at_stop, run_immediately=True ) @callback diff --git a/homeassistant/helpers/schema_config_entry_flow.py b/homeassistant/helpers/schema_config_entry_flow.py index 2bbad0ed63a..0486c7b6f8c 100644 --- a/homeassistant/helpers/schema_config_entry_flow.py +++ b/homeassistant/helpers/schema_config_entry_flow.py @@ -1,4 +1,5 @@ """Helpers for creating schema based data entry flows.""" + from __future__ import annotations from abc import ABC, abstractmethod @@ -10,9 +11,15 @@ from typing import Any, cast import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, + OptionsFlowWithConfigEntry, +) from homeassistant.core import HomeAssistant, callback, split_entity_id -from homeassistant.data_entry_flow import FlowResult, UnknownHandler +from homeassistant.data_entry_flow import UnknownHandler from . import entity_registry as er, selector from .typing import UNDEFINED, UndefinedType @@ -31,9 +38,11 @@ class SchemaFlowStep: class SchemaFlowFormStep(SchemaFlowStep): """Define a config or options flow form step.""" - schema: vol.Schema | Callable[ - [SchemaCommonFlowHandler], Coroutine[Any, Any, vol.Schema | None] - ] | None = None + schema: ( + vol.Schema + | Callable[[SchemaCommonFlowHandler], Coroutine[Any, Any, vol.Schema | None]] + | None + ) = None """Optional voluptuous schema, or function which returns a schema or None, for requesting and validating user input. @@ -43,9 +52,13 @@ class SchemaFlowFormStep(SchemaFlowStep): user input is requested. """ - validate_user_input: Callable[ - [SchemaCommonFlowHandler, dict[str, Any]], Coroutine[Any, Any, dict[str, Any]] - ] | None = None + validate_user_input: ( + Callable[ + [SchemaCommonFlowHandler, dict[str, Any]], + Coroutine[Any, Any, dict[str, Any]], + ] + | None + ) = None """Optional function to validate user input. - The `validate_user_input` function is called if the schema validates successfully. @@ -54,9 +67,9 @@ class SchemaFlowFormStep(SchemaFlowStep): - The `validate_user_input` should raise `SchemaFlowError` if user input is invalid. """ - next_step: Callable[ - [dict[str, Any]], Coroutine[Any, Any, str | None] - ] | str | None = None + next_step: ( + Callable[[dict[str, Any]], Coroutine[Any, Any, str | None]] | str | None + ) = None """Optional property to identify next step. - If `next_step` is a function, it is called if the schema validates successfully or @@ -66,9 +79,11 @@ class SchemaFlowFormStep(SchemaFlowStep): - If `next_step` is None, the flow is ended with `FlowResultType.CREATE_ENTRY`. """ - suggested_values: Callable[ - [SchemaCommonFlowHandler], Coroutine[Any, Any, dict[str, Any]] - ] | None | UndefinedType = UNDEFINED + suggested_values: ( + Callable[[SchemaCommonFlowHandler], Coroutine[Any, Any, dict[str, Any]]] + | None + | UndefinedType + ) = UNDEFINED """Optional property to populate suggested values. - If `suggested_values` is UNDEFINED, each key in the schema will get a suggested @@ -126,7 +141,7 @@ class SchemaCommonFlowHandler: async def async_step( self, step_id: str, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a step.""" if isinstance(self._flow[step_id], SchemaFlowFormStep): return await self._async_form_step(step_id, user_input) @@ -141,7 +156,7 @@ class SchemaCommonFlowHandler: async def _async_form_step( self, step_id: str, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a form step.""" form_step: SchemaFlowFormStep = cast(SchemaFlowFormStep, self._flow[step_id]) @@ -204,7 +219,7 @@ class SchemaCommonFlowHandler: async def _show_next_step_or_create_entry( self, form_step: SchemaFlowFormStep - ) -> FlowResult: + ) -> ConfigFlowResult: next_step_id_or_end_flow: str | None if callable(form_step.next_step): @@ -222,7 +237,7 @@ class SchemaCommonFlowHandler: next_step_id: str, error: SchemaFlowError | None = None, user_input: dict[str, Any] | None = None, - ) -> FlowResult: + ) -> ConfigFlowResult: """Show form for next step.""" if isinstance(self._flow[next_step_id], SchemaFlowMenuStep): menu_step = cast(SchemaFlowMenuStep, self._flow[next_step_id]) @@ -271,7 +286,7 @@ class SchemaCommonFlowHandler: async def _async_menu_step( self, step_id: str, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a menu step.""" menu_step: SchemaFlowMenuStep = cast(SchemaFlowMenuStep, self._flow[step_id]) return self._handler.async_show_menu( @@ -280,7 +295,7 @@ class SchemaCommonFlowHandler: ) -class SchemaConfigFlowHandler(config_entries.ConfigFlow, ABC): +class SchemaConfigFlowHandler(ConfigFlow, ABC): """Handle a schema based config flow.""" config_flow: Mapping[str, SchemaFlowStep] @@ -294,8 +309,8 @@ class SchemaConfigFlowHandler(config_entries.ConfigFlow, ABC): @callback def _async_get_options_flow( - config_entry: config_entries.ConfigEntry, - ) -> config_entries.OptionsFlow: + config_entry: ConfigEntry, + ) -> OptionsFlow: """Get the options flow for this handler.""" if cls.options_flow is None: raise UnknownHandler @@ -324,9 +339,7 @@ class SchemaConfigFlowHandler(config_entries.ConfigFlow, ABC): @classmethod @callback - def async_supports_options_flow( - cls, config_entry: config_entries.ConfigEntry - ) -> bool: + def async_supports_options_flow(cls, config_entry: ConfigEntry) -> bool: """Return options flow support for this handler.""" return cls.options_flow is not None @@ -335,13 +348,13 @@ class SchemaConfigFlowHandler(config_entries.ConfigFlow, ABC): step_id: str, ) -> Callable[ [SchemaConfigFlowHandler, dict[str, Any] | None], - Coroutine[Any, Any, FlowResult], + Coroutine[Any, Any, ConfigFlowResult], ]: """Generate a step handler.""" async def _async_step( self: SchemaConfigFlowHandler, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle a config flow step.""" # pylint: disable-next=protected-access result = await self._common_handler.async_step(step_id, user_input) @@ -382,7 +395,7 @@ class SchemaConfigFlowHandler(config_entries.ConfigFlow, ABC): self, data: Mapping[str, Any], **kwargs: Any, - ) -> FlowResult: + ) -> ConfigFlowResult: """Finish config flow and create a config entry.""" self.async_config_flow_finished(data) return super().async_create_entry( @@ -390,12 +403,12 @@ class SchemaConfigFlowHandler(config_entries.ConfigFlow, ABC): ) -class SchemaOptionsFlowHandler(config_entries.OptionsFlowWithConfigEntry): +class SchemaOptionsFlowHandler(OptionsFlowWithConfigEntry): """Handle a schema based options flow.""" def __init__( self, - config_entry: config_entries.ConfigEntry, + config_entry: ConfigEntry, options_flow: Mapping[str, SchemaFlowStep], async_options_flow_finished: Callable[[HomeAssistant, Mapping[str, Any]], None] | None = None, @@ -430,13 +443,13 @@ class SchemaOptionsFlowHandler(config_entries.OptionsFlowWithConfigEntry): step_id: str, ) -> Callable[ [SchemaConfigFlowHandler, dict[str, Any] | None], - Coroutine[Any, Any, FlowResult], + Coroutine[Any, Any, ConfigFlowResult], ]: """Generate a step handler.""" async def _async_step( self: SchemaConfigFlowHandler, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle an options flow step.""" # pylint: disable-next=protected-access result = await self._common_handler.async_step(step_id, user_input) @@ -449,7 +462,7 @@ class SchemaOptionsFlowHandler(config_entries.OptionsFlowWithConfigEntry): self, data: Mapping[str, Any], **kwargs: Any, - ) -> FlowResult: + ) -> ConfigFlowResult: """Finish config flow and create a config entry.""" if self._async_options_flow_finished: self._async_options_flow_finished(self.hass, data) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index ee5015ad862..a86df259f11 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -1,9 +1,10 @@ """Helpers to execute scripts.""" + from __future__ import annotations import asyncio from collections.abc import AsyncGenerator, Callable, Mapping, Sequence -from contextlib import asynccontextmanager, suppress +from contextlib import asynccontextmanager from contextvars import ContextVar from copy import copy from dataclasses import dataclass @@ -12,8 +13,9 @@ from functools import partial import itertools import logging from types import MappingProxyType -from typing import TYPE_CHECKING, Any, TypedDict, TypeVar, cast +from typing import TYPE_CHECKING, Any, Literal, TypedDict, TypeVar, cast +import async_interrupt import voluptuous as vol from homeassistant import exceptions @@ -24,6 +26,8 @@ from homeassistant.const import ( ATTR_AREA_ID, ATTR_DEVICE_ID, ATTR_ENTITY_ID, + ATTR_FLOOR_ID, + ATTR_LABEL_ID, CONF_ALIAS, CONF_CHOOSE, CONF_CONDITION, @@ -75,11 +79,13 @@ from homeassistant.core import ( callback, ) from homeassistant.util import slugify +from homeassistant.util.async_ import create_eager_task from homeassistant.util.dt import utcnow +from homeassistant.util.signal_type import SignalType, SignalTypeFormat from . import condition, config_validation as cv, service, template from .condition import ConditionCheckerType, trace_condition_function -from .dispatcher import SignalType, async_dispatcher_connect, async_dispatcher_send +from .dispatcher import async_dispatcher_connect, async_dispatcher_send from .event import async_call_later, async_track_template from .script_variables import ScriptVariables from .trace import ( @@ -127,7 +133,7 @@ CONF_MAX = "max" DEFAULT_MAX = 10 CONF_MAX_EXCEEDED = "max_exceeded" -_MAX_EXCEEDED_CHOICES = list(LOGSEVERITY) + ["SILENT"] +_MAX_EXCEEDED_CHOICES = [*LOGSEVERITY, "SILENT"] DEFAULT_MAX_EXCEEDED = "WARNING" ATTR_CUR = "current" @@ -150,12 +156,24 @@ _SHUTDOWN_MAX_WAIT = 60 ACTION_TRACE_NODE_MAX_LEN = 20 # Max length of a trace node for repeated actions SCRIPT_BREAKPOINT_HIT = SignalType[str, str, str]("script_breakpoint_hit") -SCRIPT_DEBUG_CONTINUE_STOP = "script_debug_continue_stop_{}_{}" +SCRIPT_DEBUG_CONTINUE_STOP: SignalTypeFormat[Literal["continue", "stop"]] = ( + SignalTypeFormat("script_debug_continue_stop_{}_{}") +) SCRIPT_DEBUG_CONTINUE_ALL = "script_debug_continue_all" script_stack_cv: ContextVar[list[int] | None] = ContextVar("script_stack", default=None) +class ScriptStoppedError(Exception): + """Error to indicate that the script has been stopped.""" + + +def _set_result_unless_done(future: asyncio.Future[None]) -> None: + """Set result of future unless it is done.""" + if not future.done(): + future.set_result(None) + + def action_trace_append(variables, path): """Append a TraceElement to trace[path].""" trace_element = TraceElement(variables, path) @@ -167,7 +185,7 @@ def action_trace_append(variables, path): async def trace_action( hass: HomeAssistant, script_run: _ScriptRun, - stop: asyncio.Event, + stop: asyncio.Future[None], variables: dict[str, Any], ) -> AsyncGenerator[TraceElement, None]: """Trace action execution.""" @@ -198,13 +216,15 @@ async def trace_action( ): async_dispatcher_send(hass, SCRIPT_BREAKPOINT_HIT, key, run_id, path) - done = asyncio.Event() + done = hass.loop.create_future() @callback - def async_continue_stop(command=None): + def async_continue_stop( + command: Literal["continue", "stop"] | None = None, + ) -> None: if command == "stop": - stop.set() - done.set() + _set_result_unless_done(stop) + _set_result_unless_done(done) signal = SCRIPT_DEBUG_CONTINUE_STOP.format(key, run_id) remove_signal1 = async_dispatcher_connect(hass, signal, async_continue_stop) @@ -212,10 +232,7 @@ async def trace_action( hass, SCRIPT_DEBUG_CONTINUE_ALL, async_continue_stop ) - tasks = [hass.async_create_task(flag.wait()) for flag in (stop, done)] - await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED) - for task in tasks: - task.cancel() + await asyncio.wait([stop, done], return_when=asyncio.FIRST_COMPLETED) remove_signal1() remove_signal2() @@ -391,12 +408,13 @@ class _ScriptRun: self._context = context self._log_exceptions = log_exceptions self._step = -1 - self._stop = asyncio.Event() + self._started = False + self._stop = hass.loop.create_future() self._stopped = asyncio.Event() self._conversation_response: str | None | UndefinedType = UNDEFINED def _changed(self) -> None: - if not self._stop.is_set(): + if not self._stop.done(): self._script._changed() # pylint: disable=protected-access async def _async_get_condition(self, config): @@ -419,6 +437,7 @@ class _ScriptRun: async def async_run(self) -> ScriptRunResult | None: """Run script.""" + self._started = True # Push the script to the script execution stack if (script_stack := script_stack_cv.get()) is None: script_stack = [] @@ -429,7 +448,7 @@ class _ScriptRun: try: self._log("Running %s", self._script.running_description) for self._step, self._action in enumerate(self._script.sequence): - if self._stop.is_set(): + if self._stop.done(): script_execution_set("cancelled") break await self._async_step(log_exceptions=False) @@ -468,7 +487,7 @@ class _ScriptRun: async with trace_action( self._hass, self, self._stop, self._variables ) as trace_element: - if self._stop.is_set(): + if self._stop.done(): return action = cv.determine_script_action(self._action) @@ -480,8 +499,8 @@ class _ScriptRun: trace_set_result(enabled=False) return + handler = f"_async_{action}_step" try: - handler = f"_async_{action}_step" await getattr(self, handler)() except Exception as ex: # pylint: disable=broad-except self._handle_exception( @@ -499,8 +518,13 @@ class _ScriptRun: async def async_stop(self) -> None: """Stop script run.""" - self._stop.set() - await self._stopped.wait() + _set_result_unless_done(self._stop) + # If the script was never started + # the stopped event will never be + # set because the script will never + # start running + if self._started: + await self._stopped.wait() def _handle_exception( self, exception: Exception, continue_on_error: bool, log_exceptions: bool @@ -568,9 +592,9 @@ class _ScriptRun: level=level, ) - def _get_pos_time_period_template(self, key): + def _get_pos_time_period_template(self, key: str) -> timedelta: try: - return cv.positive_time_period( + return cv.positive_time_period( # type: ignore[no-any-return] template.render_complex(self._action[key], self._variables) ) except (exceptions.TemplateError, vol.Invalid) as ex: @@ -585,26 +609,39 @@ class _ScriptRun: async def _async_delay_step(self): """Handle delay.""" - delay = self._get_pos_time_period_template(CONF_DELAY) + delay_delta = self._get_pos_time_period_template(CONF_DELAY) - self._step_log(f"delay {delay}") + self._step_log(f"delay {delay_delta}") - delay = delay.total_seconds() + delay = delay_delta.total_seconds() self._changed() - trace_set_result(delay=delay, done=False) - try: - async with asyncio.timeout(delay): - await self._stop.wait() - except TimeoutError: + if not delay: + # Handle an empty delay trace_set_result(delay=delay, done=True) + return + + trace_set_result(delay=delay, done=False) + futures, timeout_handle, timeout_future = self._async_futures_with_timeout( + delay + ) + + try: + await asyncio.wait(futures, return_when=asyncio.FIRST_COMPLETED) + finally: + if timeout_future.done(): + trace_set_result(delay=delay, done=True) + else: + timeout_handle.cancel() + + def _get_timeout_seconds_from_action(self) -> float | None: + """Get the timeout from the action.""" + if CONF_TIMEOUT in self._action: + return self._get_pos_time_period_template(CONF_TIMEOUT).total_seconds() + return None async def _async_wait_template_step(self): """Handle a wait template.""" - if CONF_TIMEOUT in self._action: - timeout = self._get_pos_time_period_template(CONF_TIMEOUT).total_seconds() - else: - timeout = None - + timeout = self._get_timeout_seconds_from_action() self._step_log("wait template", timeout) self._variables["wait"] = {"remaining": timeout, "completed": False} @@ -618,74 +655,47 @@ class _ScriptRun: self._variables["wait"]["completed"] = True return + futures, timeout_handle, timeout_future = self._async_futures_with_timeout( + timeout + ) + done = self._hass.loop.create_future() + futures.append(done) + @callback def async_script_wait(entity_id, from_s, to_s): """Handle script after template condition is true.""" - # pylint: disable=protected-access - wait_var = self._variables["wait"] - if to_context and to_context._when: - wait_var["remaining"] = to_context._when - self._hass.loop.time() - else: - wait_var["remaining"] = timeout - wait_var["completed"] = True - done.set() + self._async_set_remaining_time_var(timeout_handle) + self._variables["wait"]["completed"] = True + _set_result_unless_done(done) - to_context = None unsub = async_track_template( self._hass, wait_template, async_script_wait, self._variables ) - self._changed() - done = asyncio.Event() - tasks = [ - self._hass.async_create_task(flag.wait()) for flag in (self._stop, done) - ] - try: - async with asyncio.timeout(timeout) as to_context: - await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED) - except TimeoutError as ex: - self._variables["wait"]["remaining"] = 0.0 - if not self._action.get(CONF_CONTINUE_ON_TIMEOUT, True): - self._log(_TIMEOUT_MSG) - trace_set_result(wait=self._variables["wait"], timeout=True) - raise _AbortScript from ex - finally: - for task in tasks: - task.cancel() - unsub() + await self._async_wait_with_optional_timeout( + futures, timeout_handle, timeout_future, unsub + ) + + def _async_set_remaining_time_var( + self, timeout_handle: asyncio.TimerHandle | None + ) -> None: + """Set the remaining time variable for a wait step.""" + wait_var = self._variables["wait"] + if timeout_handle: + wait_var["remaining"] = timeout_handle.when() - self._hass.loop.time() + else: + wait_var["remaining"] = None async def _async_run_long_action(self, long_task: asyncio.Task[_T]) -> _T | None: """Run a long task while monitoring for stop request.""" - - async def async_cancel_long_task() -> None: - # Stop long task and wait for it to finish. - long_task.cancel() - with suppress(Exception): - await long_task - - # Wait for long task while monitoring for a stop request. - stop_task = self._hass.async_create_task(self._stop.wait()) try: - await asyncio.wait( - {long_task, stop_task}, return_when=asyncio.FIRST_COMPLETED - ) - # If our task is cancelled, then cancel long task, too. Note that if long task - # is cancelled otherwise the CancelledError exception will not be raised to - # here due to the call to asyncio.wait(). Rather we'll check for that below. - except asyncio.CancelledError: - await async_cancel_long_task() - raise - finally: - stop_task.cancel() - - if long_task.cancelled(): - raise asyncio.CancelledError - if long_task.done(): - # Propagate any exceptions that occurred. - return long_task.result() - # Stopped before long task completed, so cancel it. - await async_cancel_long_task() - return None + async with async_interrupt.interrupt(self._stop, ScriptStoppedError, None): + # if stop is set, interrupt will cancel inside the context + # manager which will cancel long_task, and raise + # ScriptStoppedError outside the context manager + return await long_task + except ScriptStoppedError as ex: + raise asyncio.CancelledError from ex async def _async_call_service_step(self): """Call the service specified in the action.""" @@ -727,8 +737,9 @@ class _ScriptRun: blocking=True, context=self._context, return_response=return_response, - ) - ), + ), + eager_start=True, + ) ) if response_variable: self._variables[response_variable] = response_data @@ -858,7 +869,7 @@ class _ScriptRun: for iteration in range(1, count + 1): set_repeat_var(iteration, count) await async_run_sequence(iteration, extra_msg) - if self._stop.is_set(): + if self._stop.done(): break elif CONF_FOR_EACH in repeat: @@ -886,7 +897,7 @@ class _ScriptRun: for iteration, item in enumerate(items, 1): set_repeat_var(iteration, count, item) extra_msg = f" of {count} with item: {repr(item)}" - if self._stop.is_set(): + if self._stop.done(): break await async_run_sequence(iteration, extra_msg) @@ -897,7 +908,7 @@ class _ScriptRun: for iteration in itertools.count(1): set_repeat_var(iteration) try: - if self._stop.is_set(): + if self._stop.done(): break if not self._test_conditions(conditions, "while"): break @@ -915,7 +926,7 @@ class _ScriptRun: set_repeat_var(iteration) await async_run_sequence(iteration) try: - if self._stop.is_set(): + if self._stop.done(): break if self._test_conditions(conditions, "until") in [True, None]: break @@ -975,12 +986,35 @@ class _ScriptRun: with trace_path("else"): await self._async_run_script(if_data["if_else"]) + def _async_futures_with_timeout( + self, + timeout: float | None, + ) -> tuple[ + list[asyncio.Future[None]], + asyncio.TimerHandle | None, + asyncio.Future[None] | None, + ]: + """Return a list of futures to wait for. + + The list will contain the stop future. + + If timeout is set, a timeout future and handle will be created + and will be added to the list of futures. + """ + timeout_handle: asyncio.TimerHandle | None = None + timeout_future: asyncio.Future[None] | None = None + futures: list[asyncio.Future[None]] = [self._stop] + if timeout: + timeout_future = self._hass.loop.create_future() + timeout_handle = self._hass.loop.call_later( + timeout, _set_result_unless_done, timeout_future + ) + futures.append(timeout_future) + return futures, timeout_handle, timeout_future + async def _async_wait_for_trigger_step(self): """Wait for a trigger event.""" - if CONF_TIMEOUT in self._action: - timeout = self._get_pos_time_period_template(CONF_TIMEOUT).total_seconds() - else: - timeout = None + timeout = self._get_timeout_seconds_from_action() self._step_log("wait for trigger", timeout) @@ -988,22 +1022,20 @@ class _ScriptRun: self._variables["wait"] = {"remaining": timeout, "trigger": None} trace_set_result(wait=self._variables["wait"]) - done = asyncio.Event() + futures, timeout_handle, timeout_future = self._async_futures_with_timeout( + timeout + ) + done = self._hass.loop.create_future() + futures.append(done) async def async_done(variables, context=None): - # pylint: disable=protected-access - wait_var = self._variables["wait"] - if to_context and to_context._when: - wait_var["remaining"] = to_context._when - self._hass.loop.time() - else: - wait_var["remaining"] = timeout - wait_var["trigger"] = variables["trigger"] - done.set() + self._async_set_remaining_time_var(timeout_handle) + self._variables["wait"]["trigger"] = variables["trigger"] + _set_result_unless_done(done) def log_cb(level, msg, **kwargs): self._log(msg, level=level, **kwargs) - to_context = None remove_triggers = await async_initialize_triggers( self._hass, self._action[CONF_WAIT_FOR_TRIGGER], @@ -1015,24 +1047,31 @@ class _ScriptRun: ) if not remove_triggers: return - self._changed() - tasks = [ - self._hass.async_create_task(flag.wait()) for flag in (self._stop, done) - ] + await self._async_wait_with_optional_timeout( + futures, timeout_handle, timeout_future, remove_triggers + ) + + async def _async_wait_with_optional_timeout( + self, + futures: list[asyncio.Future[None]], + timeout_handle: asyncio.TimerHandle | None, + timeout_future: asyncio.Future[None] | None, + unsub: Callable[[], None], + ) -> None: try: - async with asyncio.timeout(timeout) as to_context: - await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED) - except TimeoutError as ex: - self._variables["wait"]["remaining"] = 0.0 - if not self._action.get(CONF_CONTINUE_ON_TIMEOUT, True): - self._log(_TIMEOUT_MSG) - trace_set_result(wait=self._variables["wait"], timeout=True) - raise _AbortScript from ex + await asyncio.wait(futures, return_when=asyncio.FIRST_COMPLETED) + if timeout_future and timeout_future.done(): + self._variables["wait"]["remaining"] = 0.0 + if not self._action.get(CONF_CONTINUE_ON_TIMEOUT, True): + self._log(_TIMEOUT_MSG) + trace_set_result(wait=self._variables["wait"], timeout=True) + raise _AbortScript from TimeoutError() finally: - for task in tasks: - task.cancel() - remove_triggers() + if timeout_future and not timeout_future.done() and timeout_handle: + timeout_handle.cancel() + + unsub() async def _async_variables_step(self): """Set a variable value.""" @@ -1099,7 +1138,7 @@ class _ScriptRun: """Execute a script.""" result = await self._async_run_long_action( self._hass.async_create_task( - script.async_run(self._variables, self._context) + script.async_run(self._variables, self._context), eager_start=True ) ) if result and result.conversation_response is not UNDEFINED: @@ -1115,29 +1154,17 @@ class _QueuedScriptRun(_ScriptRun): """Run script.""" # Wait for previous run, if any, to finish by attempting to acquire the script's # shared lock. At the same time monitor if we've been told to stop. - lock_task = self._hass.async_create_task( - self._script._queue_lck.acquire() # pylint: disable=protected-access - ) - stop_task = self._hass.async_create_task(self._stop.wait()) try: - await asyncio.wait( - {lock_task, stop_task}, return_when=asyncio.FIRST_COMPLETED - ) - except asyncio.CancelledError: + async with async_interrupt.interrupt(self._stop, ScriptStoppedError, None): + await self._script._queue_lck.acquire() # pylint: disable=protected-access + except ScriptStoppedError as ex: + # If we've been told to stop, then just finish up. self._finish() - raise - else: - self.lock_acquired = lock_task.done() and not lock_task.cancelled() - finally: - lock_task.cancel() - stop_task.cancel() + raise asyncio.CancelledError from ex - # If we've been told to stop, then just finish up. Otherwise, we've acquired the - # lock so we can go ahead and start the run. - if self._stop.is_set(): - self._finish() - else: - await super().async_run() + self.lock_acquired = True + # We've acquired the lock so we can go ahead and start the run. + await super().async_run() def _finish(self) -> None: if self.lock_acquired: @@ -1365,17 +1392,34 @@ class Script: """Return true if the current mode support max.""" return self.script_mode in (SCRIPT_MODE_PARALLEL, SCRIPT_MODE_QUEUED) + @cached_property + def referenced_labels(self) -> set[str]: + """Return a set of referenced labels.""" + referenced_labels: set[str] = set() + Script._find_referenced_target(ATTR_LABEL_ID, referenced_labels, self.sequence) + return referenced_labels + + @cached_property + def referenced_floors(self) -> set[str]: + """Return a set of referenced fooors.""" + referenced_floors: set[str] = set() + Script._find_referenced_target(ATTR_FLOOR_ID, referenced_floors, self.sequence) + return referenced_floors + @cached_property def referenced_areas(self) -> set[str]: """Return a set of referenced areas.""" referenced_areas: set[str] = set() - Script._find_referenced_areas(referenced_areas, self.sequence) + Script._find_referenced_target(ATTR_AREA_ID, referenced_areas, self.sequence) return referenced_areas @staticmethod - def _find_referenced_areas( - referenced: set[str], sequence: Sequence[dict[str, Any]] + def _find_referenced_target( + target: Literal["area_id", "floor_id", "label_id"], + referenced: set[str], + sequence: Sequence[dict[str, Any]], ) -> None: + """Find referenced target in a sequence.""" for step in sequence: action = cv.determine_script_action(step) @@ -1385,22 +1429,28 @@ class Script: step.get(CONF_SERVICE_DATA), step.get(CONF_SERVICE_DATA_TEMPLATE), ): - _referenced_extract_ids(data, ATTR_AREA_ID, referenced) + _referenced_extract_ids(data, target, referenced) elif action == cv.SCRIPT_ACTION_CHOOSE: for choice in step[CONF_CHOOSE]: - Script._find_referenced_areas(referenced, choice[CONF_SEQUENCE]) + Script._find_referenced_target( + target, referenced, choice[CONF_SEQUENCE] + ) if CONF_DEFAULT in step: - Script._find_referenced_areas(referenced, step[CONF_DEFAULT]) + Script._find_referenced_target( + target, referenced, step[CONF_DEFAULT] + ) elif action == cv.SCRIPT_ACTION_IF: - Script._find_referenced_areas(referenced, step[CONF_THEN]) + Script._find_referenced_target(target, referenced, step[CONF_THEN]) if CONF_ELSE in step: - Script._find_referenced_areas(referenced, step[CONF_ELSE]) + Script._find_referenced_target(target, referenced, step[CONF_ELSE]) elif action == cv.SCRIPT_ACTION_PARALLEL: for script in step[CONF_PARALLEL]: - Script._find_referenced_areas(referenced, script[CONF_SEQUENCE]) + Script._find_referenced_target( + target, referenced, script[CONF_SEQUENCE] + ) @cached_property def referenced_devices(self) -> set[str]: @@ -1592,12 +1642,12 @@ class Script: await self.async_stop(update_state=False, spare=run) if started_action: - self._hass.async_run_job(started_action) + started_action() self.last_triggered = utcnow() self._changed() try: - return await asyncio.shield(run.async_run()) + return await asyncio.shield(create_eager_task(run.async_run())) except asyncio.CancelledError: await run.async_stop() self._changed() diff --git a/homeassistant/helpers/script_variables.py b/homeassistant/helpers/script_variables.py index 6c5edfd0ac3..043101b9b86 100644 --- a/homeassistant/helpers/script_variables.py +++ b/homeassistant/helpers/script_variables.py @@ -1,4 +1,5 @@ """Script variables.""" + from __future__ import annotations from collections.abc import Mapping diff --git a/homeassistant/helpers/selector.py b/homeassistant/helpers/selector.py index 8f2d9bf4938..c4db601fac6 100644 --- a/homeassistant/helpers/selector.py +++ b/homeassistant/helpers/selector.py @@ -1,4 +1,5 @@ """Selectors for Home Assistant.""" + from __future__ import annotations from collections.abc import Callable, Mapping, Sequence @@ -843,6 +844,48 @@ class EntitySelector(Selector[EntitySelectorConfig]): return cast(list, vol.Schema([validate])(data)) # Output is a list +class FloorSelectorConfig(TypedDict, total=False): + """Class to represent an floor selector config.""" + + entity: EntityFilterSelectorConfig | list[EntityFilterSelectorConfig] + device: DeviceFilterSelectorConfig | list[DeviceFilterSelectorConfig] + multiple: bool + + +@SELECTORS.register("floor") +class FloorSelector(Selector[AreaSelectorConfig]): + """Selector of a single or list of floors.""" + + selector_type = "floor" + + CONFIG_SCHEMA = vol.Schema( + { + vol.Optional("entity"): vol.All( + cv.ensure_list, + [ENTITY_FILTER_SELECTOR_CONFIG_SCHEMA], + ), + vol.Optional("device"): vol.All( + cv.ensure_list, + [DEVICE_FILTER_SELECTOR_CONFIG_SCHEMA], + ), + vol.Optional("multiple", default=False): cv.boolean, + } + ) + + def __init__(self, config: FloorSelectorConfig | None = None) -> None: + """Instantiate a selector.""" + super().__init__(config) + + def __call__(self, data: Any) -> str | list[str]: + """Validate the passed selection.""" + if not self.config["multiple"]: + floor_id: str = vol.Schema(str)(data) + return floor_id + if not isinstance(data, list): + raise vol.Invalid("Value should be a list") + return [vol.Schema(str)(val) for val in data] + + class IconSelectorConfig(TypedDict, total=False): """Class to represent an icon selector config.""" @@ -870,6 +913,38 @@ class IconSelector(Selector[IconSelectorConfig]): return icon +class LabelSelectorConfig(TypedDict, total=False): + """Class to represent a label selector config.""" + + multiple: bool + + +@SELECTORS.register("label") +class LabelSelector(Selector[LabelSelectorConfig]): + """Selector of a single or list of labels.""" + + selector_type = "label" + + CONFIG_SCHEMA = vol.Schema( + { + vol.Optional("multiple", default=False): cv.boolean, + } + ) + + def __init__(self, config: LabelSelectorConfig | None = None) -> None: + """Instantiate a selector.""" + super().__init__(config) + + def __call__(self, data: Any) -> str | list[str]: + """Validate the passed selection.""" + if not self.config["multiple"]: + label_id: str = vol.Schema(str)(data) + return label_id + if not isinstance(data, list): + raise vol.Invalid("Value should be a list") + return [vol.Schema(str)(val) for val in data] + + class LanguageSelectorConfig(TypedDict, total=False): """Class to represent an language selector config.""" diff --git a/homeassistant/helpers/sensor.py b/homeassistant/helpers/sensor.py index 0785a78850a..3cccfb661ee 100644 --- a/homeassistant/helpers/sensor.py +++ b/homeassistant/helpers/sensor.py @@ -1,4 +1,5 @@ """Common functions related to sensor device management.""" + from __future__ import annotations from typing import TYPE_CHECKING diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 9feabbb45e2..da27df9d139 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -1,4 +1,5 @@ """Service calling related helpers.""" + from __future__ import annotations import asyncio @@ -17,6 +18,8 @@ from homeassistant.const import ( ATTR_AREA_ID, ATTR_DEVICE_ID, ATTR_ENTITY_ID, + ATTR_FLOOR_ID, + ATTR_LABEL_ID, CONF_ENTITY_ID, CONF_SERVICE, CONF_SERVICE_DATA, @@ -52,6 +55,8 @@ from . import ( config_validation as cv, device_registry, entity_registry, + floor_registry, + label_registry, template, translation, ) @@ -193,7 +198,7 @@ class ServiceParams(TypedDict): class ServiceTargetSelector: """Class to hold a target selector for a service.""" - __slots__ = ("entity_ids", "device_ids", "area_ids") + __slots__ = ("entity_ids", "device_ids", "area_ids", "floor_ids", "label_ids") def __init__(self, service_call: ServiceCall) -> None: """Extract ids from service call data.""" @@ -201,6 +206,8 @@ class ServiceTargetSelector: entity_ids: str | list | None = service_call_data.get(ATTR_ENTITY_ID) device_ids: str | list | None = service_call_data.get(ATTR_DEVICE_ID) area_ids: str | list | None = service_call_data.get(ATTR_AREA_ID) + floor_ids: str | list | None = service_call_data.get(ATTR_FLOOR_ID) + label_ids: str | list | None = service_call_data.get(ATTR_LABEL_ID) self.entity_ids = ( set(cv.ensure_list(entity_ids)) if _has_match(entity_ids) else set() @@ -209,11 +216,23 @@ class ServiceTargetSelector: set(cv.ensure_list(device_ids)) if _has_match(device_ids) else set() ) self.area_ids = set(cv.ensure_list(area_ids)) if _has_match(area_ids) else set() + self.floor_ids = ( + set(cv.ensure_list(floor_ids)) if _has_match(floor_ids) else set() + ) + self.label_ids = ( + set(cv.ensure_list(label_ids)) if _has_match(label_ids) else set() + ) @property def has_any_selector(self) -> bool: """Determine if any selectors are present.""" - return bool(self.entity_ids or self.device_ids or self.area_ids) + return bool( + self.entity_ids + or self.device_ids + or self.area_ids + or self.floor_ids + or self.label_ids + ) @dataclasses.dataclass(slots=True) @@ -223,24 +242,29 @@ class SelectedEntities: # Entities that were explicitly mentioned. referenced: set[str] = dataclasses.field(default_factory=set) - # Entities that were referenced via device/area ID. + # Entities that were referenced via device/area/floor/label ID. # Should not trigger a warning when they don't exist. indirectly_referenced: set[str] = dataclasses.field(default_factory=set) # Referenced items that could not be found. missing_devices: set[str] = dataclasses.field(default_factory=set) missing_areas: set[str] = dataclasses.field(default_factory=set) + missing_floors: set[str] = dataclasses.field(default_factory=set) + missing_labels: set[str] = dataclasses.field(default_factory=set) # Referenced devices referenced_devices: set[str] = dataclasses.field(default_factory=set) + referenced_areas: set[str] = dataclasses.field(default_factory=set) def log_missing(self, missing_entities: set[str]) -> None: """Log about missing items.""" parts = [] for label, items in ( + ("floors", self.missing_floors), ("areas", self.missing_areas), ("devices", self.missing_devices), ("entities", missing_entities), + ("labels", self.missing_labels), ): if items: parts.append(f"{label} {', '.join(sorted(items))}") @@ -455,7 +479,7 @@ def _has_match(ids: str | list[str] | None) -> TypeGuard[str | list[str]]: @bind_hass -def async_extract_referenced_entity_ids( +def async_extract_referenced_entity_ids( # noqa: C901 hass: HomeAssistant, service_call: ServiceCall, expand_group: bool = True ) -> SelectedEntities: """Extract referenced entity IDs from a service call.""" @@ -471,37 +495,77 @@ def async_extract_referenced_entity_ids( selected.referenced.update(entity_ids) - if not selector.device_ids and not selector.area_ids: + if ( + not selector.device_ids + and not selector.area_ids + and not selector.floor_ids + and not selector.label_ids + ): return selected ent_reg = entity_registry.async_get(hass) dev_reg = device_registry.async_get(hass) area_reg = area_registry.async_get(hass) + floor_reg = floor_registry.async_get(hass) + label_reg = label_registry.async_get(hass) - for device_id in selector.device_ids: - if device_id not in dev_reg.devices: - selected.missing_devices.add(device_id) + for floor_id in selector.floor_ids: + if floor_id not in floor_reg.floors: + selected.missing_floors.add(floor_id) for area_id in selector.area_ids: if area_id not in area_reg.areas: selected.missing_areas.add(area_id) + for device_id in selector.device_ids: + if device_id not in dev_reg.devices: + selected.missing_devices.add(device_id) + + for label_id in selector.label_ids: + if label_id not in label_reg.labels: + selected.missing_labels.add(label_id) + + # Find areas, devices & entities for targeted labels + if selector.label_ids: + for area_entry in area_reg.areas.values(): + if area_entry.labels.intersection(selector.label_ids): + selected.referenced_areas.add(area_entry.id) + + for device_entry in dev_reg.devices.values(): + if device_entry.labels.intersection(selector.label_ids): + selected.referenced_devices.add(device_entry.id) + + for entity_entry in ent_reg.entities.values(): + if ( + entity_entry.entity_category is None + and entity_entry.hidden_by is None + and entity_entry.labels.intersection(selector.label_ids) + ): + selected.indirectly_referenced.add(entity_entry.entity_id) + + # Find areas for targeted floors + if selector.floor_ids: + for area_entry in area_reg.areas.values(): + if area_entry.id and area_entry.floor_id in selector.floor_ids: + selected.referenced_areas.add(area_entry.id) + # Find devices for targeted areas selected.referenced_devices.update(selector.device_ids) - if selector.area_ids: + selected.referenced_areas.update(selector.area_ids) + if selected.referenced_areas: for device_entry in dev_reg.devices.values(): - if device_entry.area_id in selector.area_ids: + if device_entry.area_id in selected.referenced_areas: selected.referenced_devices.add(device_entry.id) - if not selector.area_ids and not selected.referenced_devices: + if not selected.referenced_areas and not selected.referenced_devices: return selected entities = ent_reg.entities # Add indirectly referenced by area selected.indirectly_referenced.update( entry.entity_id - for area_id in selector.area_ids + for area_id in selected.referenced_areas # The entity's area matches a targeted area for entry in entities.get_entries_for_area_id(area_id) # Do not add entities which are hidden or which are config @@ -592,9 +656,9 @@ async def async_get_all_descriptions( hass: HomeAssistant, ) -> dict[str, dict[str, Any]]: """Return descriptions (i.e. user documentation) for all service calls.""" - descriptions_cache: dict[ - tuple[str, str], dict[str, Any] | None - ] = hass.data.setdefault(SERVICE_DESCRIPTION_CACHE, {}) + descriptions_cache: dict[tuple[str, str], dict[str, Any] | None] = ( + hass.data.setdefault(SERVICE_DESCRIPTION_CACHE, {}) + ) # We don't mutate services here so we avoid calling # async_services which makes a copy of every services @@ -632,16 +696,18 @@ async def async_get_all_descriptions( ints_or_excs = await async_get_integrations(hass, domains_with_missing_services) integrations: list[Integration] = [] for domain, int_or_exc in ints_or_excs.items(): - if type(int_or_exc) is Integration: # noqa: E721 + if type(int_or_exc) is Integration and int_or_exc.has_services: integrations.append(int_or_exc) continue if TYPE_CHECKING: assert isinstance(int_or_exc, Exception) _LOGGER.error("Failed to load integration: %s", domain, exc_info=int_or_exc) - contents = await hass.async_add_executor_job( - _load_services_files, hass, integrations - ) - loaded = dict(zip(domains_with_missing_services, contents)) + + if integrations: + contents = await hass.async_add_executor_job( + _load_services_files, hass, integrations + ) + loaded = dict(zip(domains_with_missing_services, contents)) # Load translations for all service domains translations = await translation.async_get_translations( @@ -742,9 +808,9 @@ def async_set_service_schema( domain = domain.lower() service = service.lower() - descriptions_cache: dict[ - tuple[str, str], dict[str, Any] | None - ] = hass.data.setdefault(SERVICE_DESCRIPTION_CACHE, {}) + descriptions_cache: dict[tuple[str, str], dict[str, Any] | None] = ( + hass.data.setdefault(SERVICE_DESCRIPTION_CACHE, {}) + ) description = { "name": schema.get("name", ""), @@ -962,9 +1028,11 @@ async def _handle_entity_call( task: asyncio.Future[ServiceResponse] | None if isinstance(func, str): - task = hass.async_run_hass_job( - HassJob(partial(getattr(entity, func), **data)) # type: ignore[arg-type] + job = HassJob( + partial(getattr(entity, func), **data), # type: ignore[arg-type] + job_type=entity.get_hassjob_type(func), ) + task = hass.async_run_hass_job(job) else: task = hass.async_run_hass_job(func, entity, data) @@ -990,7 +1058,7 @@ async def _handle_entity_call( async def _async_admin_handler( hass: HomeAssistant, - service_job: HassJob[[None], Callable[[ServiceCall], Awaitable[None] | None]], + service_job: HassJob[[ServiceCall], Awaitable[None] | None], call: ServiceCall, ) -> None: """Run an admin service.""" diff --git a/homeassistant/helpers/service_info/mqtt.py b/homeassistant/helpers/service_info/mqtt.py index 906072a2d4b..172a5eeff33 100644 --- a/homeassistant/helpers/service_info/mqtt.py +++ b/homeassistant/helpers/service_info/mqtt.py @@ -1,4 +1,5 @@ """MQTT Discovery data.""" + from dataclasses import dataclass import datetime as dt diff --git a/homeassistant/helpers/signal.py b/homeassistant/helpers/signal.py index c7035d5a0d2..baaa36e83ce 100644 --- a/homeassistant/helpers/signal.py +++ b/homeassistant/helpers/signal.py @@ -1,4 +1,5 @@ """Signal handling related helpers.""" + import asyncio import logging import signal diff --git a/homeassistant/helpers/significant_change.py b/homeassistant/helpers/significant_change.py index 12b78b75fa2..44b103e5c27 100644 --- a/homeassistant/helpers/significant_change.py +++ b/homeassistant/helpers/significant_change.py @@ -26,6 +26,7 @@ The following cases will never be passed to your function: - if either state is unknown/unavailable - state adding/removing """ + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/helpers/singleton.py b/homeassistant/helpers/singleton.py index 5579106bb55..91e7a671b69 100644 --- a/homeassistant/helpers/singleton.py +++ b/homeassistant/helpers/singleton.py @@ -1,4 +1,5 @@ """Helper to help coordinating calls.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/helpers/start.py b/homeassistant/helpers/start.py index 30e8466070e..148b416e087 100644 --- a/homeassistant/helpers/start.py +++ b/homeassistant/helpers/start.py @@ -1,4 +1,5 @@ """Helpers to help during startup.""" + from __future__ import annotations from collections.abc import Callable, Coroutine diff --git a/homeassistant/helpers/state.py b/homeassistant/helpers/state.py index dae63b4ead1..71b1b2658e2 100644 --- a/homeassistant/helpers/state.py +++ b/homeassistant/helpers/state.py @@ -1,4 +1,5 @@ """Helpers that help with state related things.""" + from __future__ import annotations import asyncio @@ -53,7 +54,9 @@ async def async_reproduce_state( return try: - platform: ModuleType = integration.get_platform("reproduce_state") + platform: ModuleType = await integration.async_get_platform( + "reproduce_state" + ) except ImportError: _LOGGER.warning("Integration %s does not support reproduce state", domain) return diff --git a/homeassistant/helpers/storage.py b/homeassistant/helpers/storage.py index 44460ffa601..2413a53e605 100644 --- a/homeassistant/helpers/storage.py +++ b/homeassistant/helpers/storage.py @@ -1,17 +1,23 @@ """Helper to help store data.""" + from __future__ import annotations import asyncio -from collections.abc import Callable, Mapping, Sequence +from collections.abc import Callable, Iterable, Mapping, Sequence from contextlib import suppress from copy import deepcopy import inspect from json import JSONDecodeError, JSONEncoder import logging import os +from pathlib import Path from typing import TYPE_CHECKING, Any, Generic, TypeVar -from homeassistant.const import EVENT_HOMEASSISTANT_FINAL_WRITE +from homeassistant.const import ( + EVENT_HOMEASSISTANT_FINAL_WRITE, + EVENT_HOMEASSISTANT_STARTED, + EVENT_HOMEASSISTANT_STOP, +) from homeassistant.core import ( CALLBACK_TYPE, DOMAIN as HOMEASSISTANT_DOMAIN, @@ -21,7 +27,7 @@ from homeassistant.core import ( callback, ) from homeassistant.exceptions import HomeAssistantError -from homeassistant.loader import MAX_LOAD_CONCURRENTLY, bind_hass +from homeassistant.loader import bind_hass from homeassistant.util import json as json_util import homeassistant.util.dt as dt_util from homeassistant.util.file import WriteError @@ -36,12 +42,15 @@ else: # mypy: allow-untyped-calls, allow-untyped-defs, no-warn-return-any # mypy: no-check-untyped-defs +MAX_LOAD_CONCURRENTLY = 6 STORAGE_DIR = ".storage" _LOGGER = logging.getLogger(__name__) STORAGE_SEMAPHORE = "storage_semaphore" +STORAGE_MANAGER = "storage_manager" +MANAGER_CLEANUP_DELAY = 60 _T = TypeVar("_T", bound=Mapping[str, Any] | Sequence[Any]) @@ -86,6 +95,147 @@ async def async_migrator( return config +def get_internal_store_manager( + hass: HomeAssistant, config_dir: str | None = None +) -> _StoreManager: + """Get the store manager. + + This function is not part of the API and should only be + used in the Home Assistant core internals. It is not + guaranteed to be stable. + """ + if STORAGE_MANAGER not in hass.data: + manager = _StoreManager(hass, config_dir or hass.config.config_dir) + hass.data[STORAGE_MANAGER] = manager + return hass.data[STORAGE_MANAGER] + + +class _StoreManager: + """Class to help storing data. + + The store manager is used to cache and manage storage files. + """ + + def __init__(self, hass: HomeAssistant, config_dir: str) -> None: + """Initialize storage manager class.""" + self._hass = hass + self._invalidated: set[str] = set() + self._files: set[str] | None = None + self._data_preload: dict[str, json_util.JsonValueType] = {} + self._storage_path: Path = Path(config_dir).joinpath(STORAGE_DIR) + self._cancel_cleanup: asyncio.TimerHandle | None = None + + async def async_initialize(self) -> None: + """Initialize the storage manager.""" + hass = self._hass + await hass.async_add_executor_job(self._initialize_files) + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STARTED, + self._async_schedule_cleanup, + run_immediately=True, + ) + + @callback + def async_invalidate(self, key: str) -> None: + """Invalidate cache. + + Store calls this when its going to save data + to ensure that the cache is not used after that. + """ + if "/" not in key: + self._invalidated.add(key) + self._data_preload.pop(key, None) + + @callback + def async_fetch( + self, key: str + ) -> tuple[bool, json_util.JsonValueType | None] | None: + """Fetch data from cache.""" + # + # If the key is invalidated, we don't need to check the cache + # If async_initialize has not been called yet, we don't know + # if the file exists or not so its a cache miss + # + # It is very important that we check if self._files is None + # because we do not want to incorrectly return a cache miss + # because async_initialize has not been called yet as it would + # cause the Store to return None when it should not. + # + # The "/" in key check is to prevent the cache from being used + # for subdirs in case we have a key like "hacs/XXX" + # + if "/" in key or key in self._invalidated or self._files is None: + _LOGGER.debug("%s: Cache miss", key) + return None + + # If async_initialize has been called and the key is not in self._files + # then the file does not exist + if key not in self._files: + _LOGGER.debug("%s: Cache hit, does not exist", key) + return (False, None) + + # If the key is in the preload cache, return it + if data := self._data_preload.pop(key, None): + _LOGGER.debug("%s: Cache hit data", key) + return (True, data) + + _LOGGER.debug("%s: Cache miss, not preloaded", key) + return None + + @callback + def _async_schedule_cleanup(self, _event: Event) -> None: + """Schedule the cleanup of old files.""" + self._cancel_cleanup = self._hass.loop.call_later( + MANAGER_CLEANUP_DELAY, self._async_cleanup + ) + # Handle the case where we stop in the first 60s + self._hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, + self._async_cancel_and_cleanup, + run_immediately=True, + ) + + @callback + def _async_cancel_and_cleanup(self, _event: Event) -> None: + """Cancel the cleanup of old files.""" + self._async_cleanup() + if self._cancel_cleanup: + self._cancel_cleanup.cancel() + self._cancel_cleanup = None + + @callback + def _async_cleanup(self) -> None: + """Cleanup unused cache. + + If nothing consumes the cache 60s after startup or when we + stop Home Assistant, we'll clear the cache. + """ + self._data_preload.clear() + + async def async_preload(self, keys: Iterable[str]) -> None: + """Cache the keys.""" + # If async_initialize has not been called yet, we can't preload + if self._files is not None and (existing := self._files.intersection(keys)): + await self._hass.async_add_executor_job(self._preload, existing) + + def _preload(self, keys: Iterable[str]) -> None: + """Cache the keys.""" + storage_path = self._storage_path + data_preload = self._data_preload + for key in keys: + storage_file: Path = storage_path.joinpath(key) + try: + if storage_file.is_file(): + data_preload[key] = json_util.load_json(storage_file) + except Exception as ex: # pylint: disable=broad-except + _LOGGER.debug("Error loading %s: %s", key, ex) + + def _initialize_files(self) -> None: + """Initialize the cache.""" + if self._storage_path.exists(): + self._files = set(os.listdir(self._storage_path)) + + @bind_hass class Store(Generic[_T]): """Class to help storing data.""" @@ -101,6 +251,7 @@ class Store(Generic[_T]): encoder: type[JSONEncoder] | None = None, minor_version: int = 1, read_only: bool = False, + config_dir: str | None = None, ) -> None: """Initialize storage class.""" self.version = version @@ -117,6 +268,7 @@ class Store(Generic[_T]): self._atomic_writes = atomic_writes self._read_only = read_only self._next_write_time = 0.0 + self._manager = get_internal_store_manager(hass, config_dir) @cached_property def path(self): @@ -168,6 +320,10 @@ class Store(Generic[_T]): # We make a copy because code might assume it's safe to mutate loaded data # and we don't want that to mess with what we're trying to store. data = deepcopy(data) + elif cache := self._manager.async_fetch(self.key): + exists, data = cache + if not exists: + return None else: try: data = await self.hass.async_add_executor_job( @@ -364,6 +520,7 @@ class Store(Generic[_T]): async def _async_handle_write_data(self, *_args): """Handle writing the config.""" async with self._write_lock: + self._manager.async_invalidate(self.key) self._async_cleanup_delay_listener() self._async_cleanup_final_write_listener() @@ -372,10 +529,6 @@ class Store(Generic[_T]): return data = self._data - - if "data_func" in data: - data["data"] = data.pop("data_func")() - self._data = None if self._read_only: @@ -393,6 +546,9 @@ class Store(Generic[_T]): """Write the data.""" os.makedirs(os.path.dirname(path), exist_ok=True) + if "data_func" in data: + data["data"] = data.pop("data_func")() + _LOGGER.debug("Writing data for %s to %s", self.key, path) json_helper.save_json( path, @@ -408,6 +564,7 @@ class Store(Generic[_T]): async def async_remove(self) -> None: """Remove all data.""" + self._manager.async_invalidate(self.key) self._async_cleanup_delay_listener() self._async_cleanup_final_write_listener() diff --git a/homeassistant/helpers/sun.py b/homeassistant/helpers/sun.py index cf944dfc479..a490a7a8213 100644 --- a/homeassistant/helpers/sun.py +++ b/homeassistant/helpers/sun.py @@ -1,4 +1,5 @@ """Helpers for sun events.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/helpers/system_info.py b/homeassistant/helpers/system_info.py index 8af04c11c18..ec8badaddc3 100644 --- a/homeassistant/helpers/system_info.py +++ b/homeassistant/helpers/system_info.py @@ -1,4 +1,5 @@ """Helper to gather system info.""" + from __future__ import annotations from functools import cache @@ -6,13 +7,15 @@ from getpass import getuser import logging import os import platform -from typing import Any +from typing import TYPE_CHECKING, Any from homeassistant.const import __version__ as current_version from homeassistant.core import HomeAssistant from homeassistant.loader import bind_hass from homeassistant.util.package import is_docker_env, is_virtual_env +from .importlib import async_import_module + _LOGGER = logging.getLogger(__name__) @@ -30,7 +33,17 @@ cached_get_user = cache(getuser) @bind_hass async def async_get_system_info(hass: HomeAssistant) -> dict[str, Any]: """Return info about the system.""" - is_hassio = hass.components.hassio.is_hassio() + # Local import to avoid circular dependencies + # We use the import helper because hassio + # may not be loaded yet and we don't want to + # do blocking I/O in the event loop to import it. + if TYPE_CHECKING: + # pylint: disable-next=import-outside-toplevel + from homeassistant.components import hassio + else: + hassio = await async_import_module(hass, "homeassistant.components.hassio") + + is_hassio = hassio.is_hassio(hass) info_object = { "installation_type": "Unknown", @@ -68,11 +81,11 @@ async def async_get_system_info(hass: HomeAssistant) -> dict[str, Any]: # Enrich with Supervisor information if is_hassio: - if not (info := hass.components.hassio.get_info()): + if not (info := hassio.get_info(hass)): _LOGGER.warning("No Home Assistant Supervisor info available") info = {} - host = hass.components.hassio.get_host_info() or {} + host = hassio.get_host_info(hass) or {} info_object["supervisor"] = info.get("supervisor") info_object["host_os"] = host.get("operating_system") info_object["docker_version"] = info.get("docker") diff --git a/homeassistant/helpers/temperature.py b/homeassistant/helpers/temperature.py index 15d38063f63..0311486fdd2 100644 --- a/homeassistant/helpers/temperature.py +++ b/homeassistant/helpers/temperature.py @@ -1,4 +1,5 @@ """Temperature helpers for Home Assistant.""" + from __future__ import annotations from numbers import Number diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 86e3385a21b..5f692e0de89 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -1,4 +1,5 @@ """Template helper methods for rendering strings with Home Assistant data.""" + from __future__ import annotations from ast import literal_eval @@ -78,7 +79,15 @@ from homeassistant.util.json import JSON_DECODE_EXCEPTIONS, json_loads from homeassistant.util.read_only_dict import ReadOnlyDict from homeassistant.util.thread import ThreadWithException -from . import area_registry, device_registry, entity_registry, location as loc_helper +from . import ( + area_registry, + device_registry, + entity_registry, + floor_registry as fr, + issue_registry, + label_registry, + location as loc_helper, +) from .singleton import singleton from .translation import async_translate_state from .typing import TemplateVarsType @@ -123,8 +132,8 @@ _T = TypeVar("_T") _R = TypeVar("_R") _P = ParamSpec("_P") -ALL_STATES_RATE_LIMIT = timedelta(minutes=1) -DOMAIN_STATES_RATE_LIMIT = timedelta(seconds=1) +ALL_STATES_RATE_LIMIT = 60 # seconds +DOMAIN_STATES_RATE_LIMIT = 1 # seconds _render_info: ContextVar[RenderInfo | None] = ContextVar("_render_info", default=None) @@ -202,8 +211,12 @@ def async_setup(hass: HomeAssistant) -> bool: cancel = async_track_time_interval( hass, _async_adjust_lru_sizes, timedelta(minutes=10) ) - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, _async_adjust_lru_sizes) - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, callback(lambda _: cancel())) + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_START, _async_adjust_lru_sizes, run_immediately=True + ) + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, callback(lambda _: cancel()), run_immediately=True + ) return True @@ -362,7 +375,7 @@ class RenderInfo: self.domains: collections.abc.Set[str] = set() self.domains_lifecycle: collections.abc.Set[str] = set() self.entities: collections.abc.Set[str] = set() - self.rate_limit: timedelta | None = None + self.rate_limit: float | None = None self.has_time = False def __repr__(self) -> str: @@ -1030,6 +1043,12 @@ class TemplateStateBase(State): self._collect_state() return self._state.last_changed + @property + def last_reported(self) -> datetime: # type: ignore[override] + """Wrap State.last_reported.""" + self._collect_state() + return self._state.last_reported + @property def last_updated(self) -> datetime: # type: ignore[override] """Wrap State.last_updated.""" @@ -1190,13 +1209,11 @@ def _resolve_state( @overload -def forgiving_boolean(value: Any) -> bool | object: - ... +def forgiving_boolean(value: Any) -> bool | object: ... @overload -def forgiving_boolean(value: Any, default: _T) -> bool | _T: - ... +def forgiving_boolean(value: Any, default: _T) -> bool | _T: ... def forgiving_boolean( @@ -1234,6 +1251,7 @@ def expand(hass: HomeAssistant, *args: Any) -> Iterable[State]: search = list(args) found = {} + sources = entity_helper.entity_sources(hass) while search: entity = search.pop() if isinstance(entity, str): @@ -1249,14 +1267,17 @@ def expand(hass: HomeAssistant, *args: Any) -> Iterable[State]: # ignore other types continue - if entity_id.startswith(_GROUP_DOMAIN_PREFIX) or ( - (source := entity_helper.entity_sources(hass).get(entity_id)) - and source["domain"] == "group" + if entity_id in found: + continue + + domain = entity.domain + if domain == "group" or ( + (source := sources.get(entity_id)) and source["domain"] == "group" ): # Collect state will be called in here since it's wrapped if group_entities := entity.attributes.get(ATTR_ENTITY_ID): search += group_entities - elif entity_id.startswith(_ZONE_DOMAIN_PREFIX): + elif domain == "zone": if zone_entities := entity.attributes.get(ATTR_PERSONS): search += zone_entities else: @@ -1280,19 +1301,23 @@ def integration_entities(hass: HomeAssistant, entry_name: str) -> Iterable[str]: or provide a config entry title for filtering between instances of the same integration. """ - # first try if this is a config entry match - conf_entry = next( - ( - entry.entry_id - for entry in hass.config_entries.async_entries() - if entry.title == entry_name - ), - None, - ) - if conf_entry is not None: - ent_reg = entity_registry.async_get(hass) - entries = entity_registry.async_entries_for_config_entry(ent_reg, conf_entry) - return [entry.entity_id for entry in entries] + + # Don't allow searching for config entries without title + if not entry_name: + return [] + + # first try if there are any config entries with a matching title + entities: list[str] = [] + ent_reg = entity_registry.async_get(hass) + for entry in hass.config_entries.async_entries(): + if entry.title != entry_name: + continue + entries = entity_registry.async_entries_for_config_entry( + ent_reg, entry.entry_id + ) + entities.extend(entry.entity_id for entry in entries) + if entities: + return entities # fallback to just returning all entities for a domain # pylint: disable-next=import-outside-toplevel @@ -1357,6 +1382,76 @@ def is_device_attr( return bool(device_attr(hass, device_or_entity_id, attr_name) == attr_value) +def issues(hass: HomeAssistant) -> dict[tuple[str, str], dict[str, Any]]: + """Return all open issues.""" + current_issues = issue_registry.async_get(hass).issues + # Use JSON for safe representation + return {k: v.to_json() for (k, v) in current_issues.items()} + + +def issue(hass: HomeAssistant, domain: str, issue_id: str) -> dict[str, Any] | None: + """Get issue by domain and issue_id.""" + result = issue_registry.async_get(hass).async_get_issue(domain, issue_id) + if result: + return result.to_json() + return None + + +def floors(hass: HomeAssistant) -> Iterable[str | None]: + """Return all floors.""" + floor_registry = fr.async_get(hass) + return [floor.floor_id for floor in floor_registry.async_list_floors()] + + +def floor_id(hass: HomeAssistant, lookup_value: Any) -> str | None: + """Get the floor ID from a floor name.""" + floor_registry = fr.async_get(hass) + if floor := floor_registry.async_get_floor_by_name(str(lookup_value)): + return floor.floor_id + + if aid := area_id(hass, lookup_value): + area_reg = area_registry.async_get(hass) + if area := area_reg.async_get_area(aid): + return area.floor_id + + return None + + +def floor_name(hass: HomeAssistant, lookup_value: str) -> str | None: + """Get the floor name from a floor id.""" + floor_registry = fr.async_get(hass) + if floor := floor_registry.async_get_floor(lookup_value): + return floor.name + + if aid := area_id(hass, lookup_value): + area_reg = area_registry.async_get(hass) + if ( + (area := area_reg.async_get_area(aid)) + and area.floor_id + and (floor := floor_registry.async_get_floor(area.floor_id)) + ): + return floor.name + + return None + + +def floor_areas(hass: HomeAssistant, floor_id_or_name: str) -> Iterable[str]: + """Return area IDs for a given floor ID or name.""" + _floor_id: str | None + # If floor_name returns a value, we know the input was an ID, otherwise we + # assume it's a name, and if it's neither, we return early + if floor_name(hass, floor_id_or_name) is not None: + _floor_id = floor_id_or_name + else: + _floor_id = floor_id(hass, floor_id_or_name) + if _floor_id is None: + return [] + + area_reg = area_registry.async_get(hass) + entries = area_registry.async_entries_for_floor(area_reg, _floor_id) + return [entry.id for entry in entries if entry.id] + + def areas(hass: HomeAssistant) -> Iterable[str | None]: """Return all areas.""" area_reg = area_registry.async_get(hass) @@ -1482,6 +1577,92 @@ def area_devices(hass: HomeAssistant, area_id_or_name: str) -> Iterable[str]: return [entry.id for entry in entries] +def labels(hass: HomeAssistant, lookup_value: Any = None) -> Iterable[str | None]: + """Return all labels, or those from a area ID, device ID, or entity ID.""" + label_reg = label_registry.async_get(hass) + if lookup_value is None: + return [label.label_id for label in label_reg.async_list_labels()] + + ent_reg = entity_registry.async_get(hass) + + # Import here, not at top-level to avoid circular import + from . import config_validation as cv # pylint: disable=import-outside-toplevel + + lookup_value = str(lookup_value) + + try: + cv.entity_id(lookup_value) + except vol.Invalid: + pass + else: + if entity := ent_reg.async_get(lookup_value): + return list(entity.labels) + + # Check if this could be a device ID + dev_reg = device_registry.async_get(hass) + if device := dev_reg.async_get(lookup_value): + return list(device.labels) + + # Check if this could be a area ID + area_reg = area_registry.async_get(hass) + if area := area_reg.async_get_area(lookup_value): + return list(area.labels) + + return [] + + +def label_id(hass: HomeAssistant, lookup_value: Any) -> str | None: + """Get the label ID from a label name.""" + label_reg = label_registry.async_get(hass) + if label := label_reg.async_get_label_by_name(str(lookup_value)): + return label.label_id + return None + + +def label_name(hass: HomeAssistant, lookup_value: str) -> str | None: + """Get the label name from a label ID.""" + label_reg = label_registry.async_get(hass) + if label := label_reg.async_get_label(lookup_value): + return label.name + return None + + +def _label_id_or_name(hass: HomeAssistant, label_id_or_name: str) -> str | None: + """Get the label ID from a label name or ID.""" + # If label_name returns a value, we know the input was an ID, otherwise we + # assume it's a name, and if it's neither, we return early. + if label_name(hass, label_id_or_name) is not None: + return label_id_or_name + return label_id(hass, label_id_or_name) + + +def label_areas(hass: HomeAssistant, label_id_or_name: str) -> Iterable[str]: + """Return areas for a given label ID or name.""" + if (_label_id := _label_id_or_name(hass, label_id_or_name)) is None: + return [] + area_reg = area_registry.async_get(hass) + entries = area_registry.async_entries_for_label(area_reg, _label_id) + return [entry.id for entry in entries] + + +def label_devices(hass: HomeAssistant, label_id_or_name: str) -> Iterable[str]: + """Return device IDs for a given label ID or name.""" + if (_label_id := _label_id_or_name(hass, label_id_or_name)) is None: + return [] + dev_reg = device_registry.async_get(hass) + entries = device_registry.async_entries_for_label(dev_reg, _label_id) + return [entry.id for entry in entries] + + +def label_entities(hass: HomeAssistant, label_id_or_name: str) -> Iterable[str]: + """Return entities for a given label ID or name.""" + if (_label_id := _label_id_or_name(hass, label_id_or_name)) is None: + return [] + ent_reg = entity_registry.async_get(hass) + entries = entity_registry.async_entries_for_label(ent_reg, _label_id) + return [entry.entity_id for entry in entries] + + def closest(hass, *args): """Find closest entity. @@ -2619,8 +2800,12 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): self.globals["device_id"] = hassfunction(device_id) self.filters["device_id"] = self.globals["device_id"] + self.globals["issues"] = hassfunction(issues) + + self.globals["issue"] = hassfunction(issue) + self.filters["issue"] = self.globals["issue"] + self.globals["areas"] = hassfunction(areas) - self.filters["areas"] = self.globals["areas"] self.globals["area_id"] = hassfunction(area_id) self.filters["area_id"] = self.globals["area_id"] @@ -2634,9 +2819,39 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): self.globals["area_devices"] = hassfunction(area_devices) self.filters["area_devices"] = self.globals["area_devices"] + self.globals["floors"] = hassfunction(floors) + self.filters["floors"] = self.globals["floors"] + + self.globals["floor_id"] = hassfunction(floor_id) + self.filters["floor_id"] = self.globals["floor_id"] + + self.globals["floor_name"] = hassfunction(floor_name) + self.filters["floor_name"] = self.globals["floor_name"] + + self.globals["floor_areas"] = hassfunction(floor_areas) + self.filters["floor_areas"] = self.globals["floor_areas"] + self.globals["integration_entities"] = hassfunction(integration_entities) self.filters["integration_entities"] = self.globals["integration_entities"] + self.globals["labels"] = hassfunction(labels) + self.filters["labels"] = self.globals["labels"] + + self.globals["label_id"] = hassfunction(label_id) + self.filters["label_id"] = self.globals["label_id"] + + self.globals["label_name"] = hassfunction(label_name) + self.filters["label_name"] = self.globals["label_name"] + + self.globals["label_areas"] = hassfunction(label_areas) + self.filters["label_areas"] = self.globals["label_areas"] + + self.globals["label_devices"] = hassfunction(label_devices) + self.filters["label_devices"] = self.globals["label_devices"] + + self.globals["label_entities"] = hassfunction(label_entities) + self.filters["label_entities"] = self.globals["label_entities"] + if limited: # Only device_entities is available to limited templates, mark other # functions and filters as unsupported. @@ -2666,8 +2881,12 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): "device_id", "area_id", "area_name", + "floor_id", + "floor_name", "relative_time", "today_at", + "label_id", + "label_name", ] hass_filters = [ "closest", @@ -2675,7 +2894,11 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): "device_id", "area_id", "area_name", + "floor_id", + "floor_name", "has_value", + "label_id", + "label_name", ] hass_tests = [ "has_value", @@ -2746,8 +2969,7 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): filename: str | None = None, raw: Literal[False] = False, defer_init: bool = False, - ) -> CodeType: - ... + ) -> CodeType: ... @overload def compile( @@ -2757,8 +2979,7 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): filename: str | None = None, raw: Literal[True] = ..., defer_init: bool = False, - ) -> str: - ... + ) -> str: ... def compile( self, diff --git a/homeassistant/helpers/trace.py b/homeassistant/helpers/trace.py index 21154914f17..1f5aa47f4e2 100644 --- a/homeassistant/helpers/trace.py +++ b/homeassistant/helpers/trace.py @@ -1,4 +1,5 @@ """Helpers for script and condition tracing.""" + from __future__ import annotations from collections import deque diff --git a/homeassistant/helpers/translation.py b/homeassistant/helpers/translation.py index c2caecaa184..acc4f146e8b 100644 --- a/homeassistant/helpers/translation.py +++ b/homeassistant/helpers/translation.py @@ -1,8 +1,10 @@ """Translation string lookup helpers.""" + from __future__ import annotations import asyncio from collections.abc import Iterable, Mapping +from contextlib import suppress import logging import string from typing import Any @@ -12,7 +14,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.core import Event, HomeAssistant, async_get_hass, callback from homeassistant.loader import ( Integration, async_get_config_flows, @@ -162,41 +164,49 @@ async def _async_get_component_strings( translations_by_language: dict[str, dict[str, Any]] = {} # Determine paths of missing components/platforms files_to_load_by_language: dict[str, dict[str, str]] = {} + loaded_translations_by_language: dict[str, dict[str, Any]] = {} + has_files_to_load = False for language in languages: files_to_load: dict[str, str] = {} files_to_load_by_language[language] = files_to_load + translations_by_language[language] = {} - loaded_translations: dict[str, Any] = {} - translations_by_language[language] = loaded_translations - - for loaded in components: - domain = loaded.partition(".")[0] - if not (integration := integrations.get(domain)): + for comp in components: + domain, _, platform = comp.partition(".") + if ( + not (integration := integrations.get(domain)) + or not integration.has_translations + ): continue - path = component_translation_path(loaded, language, integration) - # No translation available - if path is None: - loaded_translations[loaded] = {} - else: - files_to_load[loaded] = path - - if not files_to_load: - return translations_by_language - - # Load files - loaded_translations_by_language = await hass.async_add_executor_job( - _load_translations_files_by_language, files_to_load_by_language - ) - - # Translations that miss "title" will get integration put in. - for language, loaded_translations in loaded_translations_by_language.items(): - for loaded, loaded_translation in loaded_translations.items(): - if "." in loaded: + if platform and integration.is_built_in: + # Legacy state translations are no longer used for built-in integrations + # and we avoid trying to load them. This is a temporary measure to allow + # them to keep working for custom integrations until we can fully remove + # them. continue - if "title" not in loaded_translation: - loaded_translation["title"] = integrations[loaded].name + if path := component_translation_path(comp, language, integration): + files_to_load[comp] = path + has_files_to_load = True + + if has_files_to_load: + loaded_translations_by_language = await hass.async_add_executor_job( + _load_translations_files_by_language, files_to_load_by_language + ) + + for language in languages: + loaded_translations = loaded_translations_by_language.setdefault(language, {}) + for comp in components: + if "." in comp: + continue + + # Translations that miss "title" will get integration put in. + component_translations = loaded_translations.setdefault(comp, {}) + if "title" not in component_translations and ( + integration := integrations.get(comp) + ): + component_translations["title"] = integration.name translations_by_language[language].update(loaded_translations) @@ -482,11 +492,11 @@ def async_setup(hass: HomeAssistant) -> None: hass.data[TRANSLATION_FLATTEN_CACHE] = cache @callback - def _async_load_translations_filter(event: Event) -> bool: + def _async_load_translations_filter(event_data: Mapping[str, Any]) -> bool: """Filter out unwanted events.""" nonlocal current_language if ( - new_language := event.data.get("language") + new_language := event_data.get("language") ) and new_language != current_language: current_language = new_language return True @@ -519,6 +529,35 @@ def async_translations_loaded(hass: HomeAssistant, components: set[str]) -> bool ) +@callback +def async_get_exception_message( + translation_domain: str, + translation_key: str, + translation_placeholders: dict[str, str] | None = None, +) -> str: + """Return a translated exception message. + + Defaults to English, requires translations to already be cached. + """ + language = "en" + hass = async_get_hass() + localize_key = ( + f"component.{translation_domain}.exceptions.{translation_key}.message" + ) + translations = async_get_cached_translations(hass, language, "exceptions") + if localize_key in translations: + if message := translations[localize_key]: + message = message.rstrip(".") + if not translation_placeholders: + return message + with suppress(KeyError): + message = message.format(**translation_placeholders) + return message + + # We return the translation key when was not found in the cache + return translation_key + + @callback def async_translate_state( hass: HomeAssistant, diff --git a/homeassistant/helpers/trigger.py b/homeassistant/helpers/trigger.py index c9ca76cdf72..cb14102cb04 100644 --- a/homeassistant/helpers/trigger.py +++ b/homeassistant/helpers/trigger.py @@ -1,4 +1,5 @@ """Triggers.""" + from __future__ import annotations import asyncio @@ -28,6 +29,7 @@ from homeassistant.core import ( ) from homeassistant.exceptions import HomeAssistantError from homeassistant.loader import IntegrationNotFound, async_get_integration +from homeassistant.util.async_ import create_eager_task from .typing import ConfigType, TemplateVarsType @@ -222,7 +224,7 @@ async def _async_get_trigger_platform( except IntegrationNotFound: raise vol.Invalid(f"Invalid platform '{platform}' specified") from None try: - return integration.get_platform("trigger") + return await integration.async_get_platform("trigger") except ImportError: raise vol.Invalid( f"Integration '{platform}' does not provide trigger support" @@ -305,7 +307,7 @@ async def async_initialize_triggers( variables: TemplateVarsType = None, ) -> CALLBACK_TYPE | None: """Initialize triggers.""" - triggers: list[Coroutine[Any, Any, CALLBACK_TYPE]] = [] + triggers: list[asyncio.Task[CALLBACK_TYPE]] = [] for idx, conf in enumerate(trigger_config): # Skip triggers that are not enabled if not conf.get(CONF_ENABLED, True): @@ -325,8 +327,10 @@ async def async_initialize_triggers( ) triggers.append( - platform.async_attach_trigger( - hass, conf, _trigger_action_wrapper(hass, action, conf), info + create_eager_task( + platform.async_attach_trigger( + hass, conf, _trigger_action_wrapper(hass, action, conf), info + ) ) ) diff --git a/homeassistant/helpers/trigger_template_entity.py b/homeassistant/helpers/trigger_template_entity.py index bc7deceefef..7b1c4ab8078 100644 --- a/homeassistant/helpers/trigger_template_entity.py +++ b/homeassistant/helpers/trigger_template_entity.py @@ -1,4 +1,5 @@ """TemplateEntity utility class.""" + from __future__ import annotations import contextlib diff --git a/homeassistant/helpers/typing.py b/homeassistant/helpers/typing.py index 9e3f9de34fa..0f372689809 100644 --- a/homeassistant/helpers/typing.py +++ b/homeassistant/helpers/typing.py @@ -1,7 +1,8 @@ """Typing Helpers for Home Assistant.""" + from collections.abc import Mapping from enum import Enum -from typing import Any, Generic, TypeVar +from typing import Any, TypeVar import homeassistant.core @@ -33,11 +34,6 @@ UNDEFINED = UndefinedType._singleton # pylint: disable=protected-access # They are kept in order not to break custom integrations # that may rely on them. # In due time they will be removed. +EventType = homeassistant.core.Event HomeAssistantType = homeassistant.core.HomeAssistant ServiceCallType = homeassistant.core.ServiceCall - - -class EventType(homeassistant.core.Event, Generic[_DataT]): - """Generic Event class to better type data.""" - - data: _DataT # type: ignore[assignment] diff --git a/homeassistant/helpers/update_coordinator.py b/homeassistant/helpers/update_coordinator.py index 018ba1d13e4..287e69f7085 100644 --- a/homeassistant/helpers/update_coordinator.py +++ b/homeassistant/helpers/update_coordinator.py @@ -1,4 +1,5 @@ """Helpers to help coordinate updates.""" + from __future__ import annotations from abc import abstractmethod @@ -8,22 +9,16 @@ from datetime import datetime, timedelta import logging from random import randint from time import monotonic -from typing import Any, Generic, Protocol, TypeVar +from typing import Any, Generic, Protocol import urllib.error import aiohttp import requests +from typing_extensions import TypeVar from homeassistant import config_entries from homeassistant.const import EVENT_HOMEASSISTANT_STOP -from homeassistant.core import ( - CALLBACK_TYPE, - Event, - HassJob, - HassJobType, - HomeAssistant, - callback, -) +from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback from homeassistant.exceptions import ( ConfigEntryAuthFailed, ConfigEntryError, @@ -37,12 +32,14 @@ from .debounce import Debouncer REQUEST_REFRESH_DEFAULT_COOLDOWN = 10 REQUEST_REFRESH_DEFAULT_IMMEDIATE = True -_DataT = TypeVar("_DataT") +_DataT = TypeVar("_DataT", default=dict[str, Any]) _BaseDataUpdateCoordinatorT = TypeVar( "_BaseDataUpdateCoordinatorT", bound="BaseDataUpdateCoordinatorProtocol" ) _DataUpdateCoordinatorT = TypeVar( - "_DataUpdateCoordinatorT", bound="DataUpdateCoordinator[Any]" + "_DataUpdateCoordinatorT", + bound="DataUpdateCoordinator[Any]", + default="DataUpdateCoordinator[dict[str, Any]]", ) @@ -104,18 +101,6 @@ class DataUpdateCoordinator(BaseDataUpdateCoordinatorProtocol, Generic[_DataT]): ) self._listeners: dict[CALLBACK_TYPE, tuple[CALLBACK_TYPE, object | None]] = {} - job_name = "DataUpdateCoordinator" - type_name = type(self).__name__ - if type_name != job_name: - job_name += f" {type_name}" - job_name += f" {name}" - if entry := self.config_entry: - job_name += f" {entry.title} {entry.domain} {entry.entry_id}" - self._job = HassJob( - self.__wrap_handle_refresh_interval, - job_name, - job_type=HassJobType.Callback, - ) self._unsub_refresh: CALLBACK_TYPE | None = None self._unsub_shutdown: CALLBACK_TYPE | None = None self._request_refresh_task: asyncio.TimerHandle | None = None @@ -151,7 +136,7 @@ class DataUpdateCoordinator(BaseDataUpdateCoordinatorProtocol, Generic[_DataT]): await self.async_shutdown() self._unsub_shutdown = self.hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, _on_hass_stop + EVENT_HOMEASSISTANT_STOP, _on_hass_stop, run_immediately=True ) @callback @@ -247,13 +232,25 @@ class DataUpdateCoordinator(BaseDataUpdateCoordinatorProtocol, Generic[_DataT]): int(loop.time()) + self._microsecond + self._update_interval_seconds ) self._unsub_refresh = loop.call_at( - next_refresh, hass.async_run_hass_job, self._job + next_refresh, self.__wrap_handle_refresh_interval ).cancel @callback def __wrap_handle_refresh_interval(self) -> None: """Handle a refresh interval occurrence.""" - self.hass.async_create_task(self._handle_refresh_interval(), eager_start=True) + if self.config_entry: + self.config_entry.async_create_background_task( + self.hass, + self._handle_refresh_interval(), + name=f"{self.name} - {self.config_entry.title} - refresh", + eager_start=True, + ) + else: + self.hass.async_create_background_task( + self._handle_refresh_interval(), + name=f"{self.name} - refresh", + eager_start=True, + ) async def _handle_refresh_interval(self, _now: datetime | None = None) -> None: """Handle a refresh interval occurrence.""" diff --git a/homeassistant/loader.py b/homeassistant/loader.py index 8afbf3c5124..48fd3cd54c2 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -3,6 +3,7 @@ This module has quite some complex parts. I have tried to add as much documentation as possible to keep it understandable. """ + from __future__ import annotations import asyncio @@ -12,6 +13,7 @@ from dataclasses import dataclass import functools as ft import importlib import logging +import os import pathlib import sys import time @@ -26,6 +28,7 @@ from awesomeversion import ( import voluptuous as vol from . import generated +from .const import Platform from .core import HomeAssistant, callback from .generated.application_credentials import APPLICATION_CREDENTIALS from .generated.bluetooth import BLUETOOTH @@ -52,6 +55,33 @@ _CallableT = TypeVar("_CallableT", bound=Callable[..., Any]) _LOGGER = logging.getLogger(__name__) +# +# Integration.get_component will check preload platforms and +# try to import the code to avoid a thundering heard of import +# executor jobs later in the startup process. +# +# default platforms are prepopulated in this list to ensure that +# by the time the component is loaded, we check if the platform is +# available. +# +# This list can be extended by calling async_register_preload_platform +# +BASE_PRELOAD_PLATFORMS = [ + "config", + "config_flow", + "diagnostics", + "energy", + "group", + "logbook", + "hardware", + "intent", + "media_source", + "recorder", + "repairs", + "system_health", + "trigger", +] + @dataclass class BlockedIntegration: @@ -70,6 +100,7 @@ DATA_COMPONENTS = "components" DATA_INTEGRATIONS = "integrations" DATA_MISSING_PLATFORMS = "missing_platforms" DATA_CUSTOM_COMPONENTS = "custom_components" +DATA_PRELOAD_PLATFORMS = "preload_platforms" PACKAGE_CUSTOM_COMPONENTS = "custom_components" PACKAGE_BUILTIN = "homeassistant.components" CUSTOM_WARNING = ( @@ -78,10 +109,15 @@ CUSTOM_WARNING = ( "cause stability problems, be sure to disable it if you " "experience issues with Home Assistant" ) +IMPORT_EVENT_LOOP_WARNING = ( + "We found an integration %s which is configured to " + "to import its code in the event loop. This component might " + "cause stability problems, be sure to disable it if you " + "experience issues with Home Assistant" +) _UNDEF = object() # Internal; not helpers.typing.UNDEFINED due to circular dependency -MAX_LOAD_CONCURRENTLY = 4 MOVED_ZEROCONF_PROPS = ("macaddress", "model", "manufacturer") @@ -173,7 +209,7 @@ class Manifest(TypedDict, total=False): disabled: str domain: str integration_type: Literal[ - "entity", "device", "hardware", "helper", "hub", "service", "system" + "entity", "device", "hardware", "helper", "hub", "service", "system", "virtual" ] dependencies: list[str] after_dependencies: list[str] @@ -204,6 +240,7 @@ def async_setup(hass: HomeAssistant) -> None: hass.data[DATA_COMPONENTS] = {} hass.data[DATA_INTEGRATIONS] = {} hass.data[DATA_MISSING_PLATFORMS] = {} + hass.data[DATA_PRELOAD_PLATFORMS] = BASE_PRELOAD_PLATFORMS.copy() def manifest_from_legacy_module(domain: str, module: ModuleType) -> Manifest: @@ -259,9 +296,9 @@ async def async_get_custom_components( hass: HomeAssistant, ) -> dict[str, Integration]: """Return cached list of custom integrations.""" - comps_or_future: dict[str, Integration] | asyncio.Future[ - dict[str, Integration] - ] | None = hass.data.get(DATA_CUSTOM_COMPONENTS) + comps_or_future: ( + dict[str, Integration] | asyncio.Future[dict[str, Integration]] | None + ) = hass.data.get(DATA_CUSTOM_COMPONENTS) if comps_or_future is None: future = hass.data[DATA_CUSTOM_COMPONENTS] = hass.loop.create_future() @@ -580,6 +617,14 @@ async def async_get_mqtt(hass: HomeAssistant) -> dict[str, list[str]]: return mqtt +@callback +def async_register_preload_platform(hass: HomeAssistant, platform_name: str) -> None: + """Register a platform to be preloaded.""" + preload_platforms: list[str] = hass.data[DATA_PRELOAD_PLATFORMS] + if platform_name not in preload_platforms: + preload_platforms.append(platform_name) + + class Integration: """An integration in Home Assistant.""" @@ -602,13 +647,21 @@ class Integration: ) continue + file_path = manifest_path.parent + # Avoid the listdir for virtual integrations + # as they cannot have any platforms + is_virtual = manifest.get("integration_type") == "virtual" integration = cls( hass, f"{root_module.__name__}.{domain}", - manifest_path.parent, + file_path, manifest, + None if is_virtual else set(os.listdir(file_path)), ) + if not integration.import_executor: + _LOGGER.warning(IMPORT_EVENT_LOOP_WARNING, integration.domain) + if integration.is_built_in: return integration @@ -675,6 +728,7 @@ class Integration: pkg_path: str, file_path: pathlib.Path, manifest: Manifest, + top_level_files: set[str] | None = None, ) -> None: """Initialize an integration.""" self.hass = hass @@ -690,7 +744,15 @@ class Integration: self._all_dependencies_resolved = True self._all_dependencies = set() + platforms_to_preload: list[str] = hass.data[DATA_PRELOAD_PLATFORMS] + self._platforms_to_preload = platforms_to_preload + self._component_future: asyncio.Future[ComponentProtocol] | None = None self._import_futures: dict[str, asyncio.Future[ModuleType]] = {} + cache: dict[str, ModuleType | ComponentProtocol] = hass.data[DATA_COMPONENTS] + self._cache = cache + missing_platforms_cache: dict[str, bool] = hass.data[DATA_MISSING_PLATFORMS] + self._missing_platforms_cache = missing_platforms_cache + self._top_level_files = top_level_files or set() _LOGGER.info("Loaded %s from %s", self.domain, pkg_path) @cached_property @@ -756,14 +818,28 @@ class Integration: @cached_property def integration_type( self, - ) -> Literal["entity", "device", "hardware", "helper", "hub", "service", "system"]: + ) -> Literal[ + "entity", "device", "hardware", "helper", "hub", "service", "system", "virtual" + ]: """Return the integration type.""" return self.manifest.get("integration_type", "hub") @cached_property def import_executor(self) -> bool: """Import integration in the executor.""" - return self.manifest.get("import_executor") or False + # If the integration does not explicitly set import_executor, we default to + # True. + return self.manifest.get("import_executor", True) + + @cached_property + def has_translations(self) -> bool: + """Return if the integration has translations.""" + return "translations" in self._top_level_files + + @cached_property + def has_services(self) -> bool: + """Return if the integration has services.""" + return "services.yaml" in self._top_level_files @property def mqtt(self) -> list[str] | None: @@ -871,35 +947,66 @@ class Integration: and will check if import_executor is set and load it in the executor, otherwise it will load it in the event loop. """ + domain = self.domain + if domain in (cache := self._cache): + return cache[domain] + + if self._component_future: + return await self._component_future + if debug := _LOGGER.isEnabledFor(logging.DEBUG): start = time.perf_counter() - domain = self.domain - load_executor = ( - self.import_executor - and f"hass.components.{domain}" not in sys.modules - and f"custom_components.{domain}" not in sys.modules - ) + # Some integrations fail on import because they call functions incorrectly. # So we do it before validating config to catch these errors. - if load_executor: + load_executor = self.import_executor and ( + self.pkg_path not in sys.modules + or (self.config_flow and f"{self.pkg_path}.config_flow" not in sys.modules) + ) + if not load_executor: + comp = self._get_component() + if debug: + _LOGGER.debug( + "Component %s import took %.3f seconds (loaded_executor=False)", + self.domain, + time.perf_counter() - start, + ) + return comp + + self._component_future = self.hass.loop.create_future() + try: try: - comp = await self.hass.async_add_import_executor_job(self.get_component) + comp = await self.hass.async_add_import_executor_job( + self._get_component, True + ) except ImportError as ex: load_executor = False - _LOGGER.debug("Failed to import %s in executor", domain, exc_info=ex) + _LOGGER.debug( + "Failed to import %s in executor", self.domain, exc_info=ex + ) # If importing in the executor deadlocks because there is a circular # dependency, we fall back to the event loop. - comp = self.get_component() - else: - comp = self.get_component() + comp = self._get_component() + self._component_future.set_result(comp) + except BaseException as ex: + self._component_future.set_exception(ex) + with suppress(BaseException): + # Set the exception retrieved flag on the future since + # it will never be retrieved unless there + # are concurrent calls to async_get_component + self._component_future.result() + raise + finally: + self._component_future = None if debug: _LOGGER.debug( "Component %s import took %.3f seconds (loaded_executor=%s)", - domain, + self.domain, time.perf_counter() - start, load_executor, ) + return comp def get_component(self) -> ComponentProtocol: @@ -908,16 +1015,25 @@ class Integration: This method must be thread-safe as it's called from the executor and the event loop. + This method checks the cache and if the component is not loaded + it will load it in the executor if import_executor is set, otherwise + it will load it in the event loop. + This is mostly a thin wrapper around importlib.import_module with a dict cache which is thread-safe since importlib has appropriate locks. """ - cache: dict[str, ComponentProtocol] = self.hass.data[DATA_COMPONENTS] - if self.domain in cache: - return cache[self.domain] + domain = self.domain + if domain in (cache := self._cache): + return cache[domain] + return self._get_component() + def _get_component(self, preload_platforms: bool = False) -> ComponentProtocol: + """Return the component.""" + cache = self._cache + domain = self.domain try: - cache[self.domain] = cast( + cache[domain] = cast( ComponentProtocol, importlib.import_module(self.pkg_path) ) except ImportError: @@ -931,84 +1047,175 @@ class Integration: ) raise ImportError(f"Exception importing {self.pkg_path}") from err - return cache[self.domain] + if preload_platforms: + for platform_name in self.platforms_exists(self._platforms_to_preload): + with suppress(ImportError): + self.get_platform(platform_name) + + return cache[domain] + + def _load_platforms(self, platform_names: Iterable[str]) -> dict[str, ModuleType]: + """Load platforms for an integration.""" + return { + platform_name: self._load_platform(platform_name) + for platform_name in platform_names + } async def async_get_platform(self, platform_name: str) -> ModuleType: """Return a platform for an integration.""" + # Fast path for a single platform when it is already cached. + # This is the common case. + if platform := self._cache.get(f"{self.domain}.{platform_name}"): + return platform # type: ignore[return-value] + platforms = await self.async_get_platforms((platform_name,)) + return platforms[platform_name] + + async def async_get_platforms( + self, platform_names: Iterable[Platform | str] + ) -> dict[str, ModuleType]: + """Return a platforms for an integration.""" domain = self.domain - full_name = f"{self.domain}.{platform_name}" - if platform := self._get_platform_cached(full_name): - return platform - if future := self._import_futures.get(full_name): - return await future - if debug := _LOGGER.isEnabledFor(logging.DEBUG): - start = time.perf_counter() - import_future = self.hass.loop.create_future() - self._import_futures[full_name] = import_future - load_executor = ( - self.import_executor - and domain not in self.hass.config.components - and f"hass.components.{domain}" not in sys.modules - and f"custom_components.{domain}" not in sys.modules - ) - try: - if load_executor: - try: - platform = await self.hass.async_add_import_executor_job( - self._load_platform, platform_name - ) - except ImportError as ex: - _LOGGER.debug( - "Failed to import %s in executor", domain, exc_info=ex - ) - load_executor = False - # If importing in the executor deadlocks because there is a circular - # dependency, we fall back to the event loop. - platform = self._load_platform(platform_name) + platforms: dict[str, ModuleType] = {} + + load_executor_platforms: list[str] = [] + load_event_loop_platforms: list[str] = [] + in_progress_imports: dict[str, asyncio.Future[ModuleType]] = {} + import_futures: list[tuple[str, asyncio.Future[ModuleType]]] = [] + + for platform_name in platform_names: + if platform := self._get_platform_cached_or_raise(platform_name): + platforms[platform_name] = platform + continue + + # Another call to async_get_platforms is already importing this platform + if future := self._import_futures.get(platform_name): + in_progress_imports[platform_name] = future + continue + + full_name = f"{domain}.{platform_name}" + if ( + self.import_executor + and full_name not in self.hass.config.components + and f"{self.pkg_path}.{platform_name}" not in sys.modules + ): + load_executor_platforms.append(platform_name) else: - platform = self._load_platform(platform_name) - import_future.set_result(platform) - except BaseException as ex: - import_future.set_exception(ex) - with suppress(BaseException): - # Clear the exception retrieved flag on the future since - # it will never be retrieved unless there - # are concurrent calls to async_get_platform - import_future.result() - raise - finally: - self._import_futures.pop(full_name) + load_event_loop_platforms.append(platform_name) - if debug: - _LOGGER.debug( - "Importing platform %s took %.2fs (loaded_executor=%s)", - full_name, - time.perf_counter() - start, - load_executor, - ) + import_future = self.hass.loop.create_future() + self._import_futures[platform_name] = import_future + import_futures.append((platform_name, import_future)) - return platform + if load_executor_platforms or load_event_loop_platforms: + if debug := _LOGGER.isEnabledFor(logging.DEBUG): + start = time.perf_counter() - def _get_platform_cached(self, full_name: str) -> ModuleType | None: + try: + if load_executor_platforms: + try: + platforms.update( + await self.hass.async_add_import_executor_job( + self._load_platforms, platform_names + ) + ) + except ImportError as ex: + _LOGGER.debug( + "Failed to import %s platforms %s in executor", + domain, + load_executor_platforms, + exc_info=ex, + ) + # If importing in the executor deadlocks because there is a circular + # dependency, we fall back to the event loop. + load_event_loop_platforms.extend(load_executor_platforms) + + if load_event_loop_platforms: + platforms.update(self._load_platforms(platform_names)) + + for platform_name, import_future in import_futures: + import_future.set_result(platforms[platform_name]) + + except BaseException as ex: + for _, import_future in import_futures: + import_future.set_exception(ex) + with suppress(BaseException): + # Set the exception retrieved flag on the future since + # it will never be retrieved unless there + # are concurrent calls to async_get_platforms + import_future.result() + raise + + finally: + for platform_name, _ in import_futures: + self._import_futures.pop(platform_name) + + if debug: + _LOGGER.debug( + "Importing platforms for %s executor=%s loop=%s took %.2fs", + domain, + load_executor_platforms, + load_event_loop_platforms, + time.perf_counter() - start, + ) + + if in_progress_imports: + for platform_name, future in in_progress_imports.items(): + platforms[platform_name] = await future + + return platforms + + def _get_platform_cached_or_raise(self, platform_name: str) -> ModuleType | None: """Return a platform for an integration from cache.""" - cache: dict[str, ModuleType] = self.hass.data[DATA_COMPONENTS] - if full_name in cache: - return cache[full_name] - - missing_platforms_cache: dict[str, ImportError] = self.hass.data[ - DATA_MISSING_PLATFORMS - ] - if full_name in missing_platforms_cache: - raise missing_platforms_cache[full_name] - + full_name = f"{self.domain}.{platform_name}" + if full_name in self._cache: + # the cache is either a ModuleType or a ComponentProtocol + # but we only care about the ModuleType here + return self._cache[full_name] # type: ignore[return-value] + if full_name in self._missing_platforms_cache: + raise ModuleNotFoundError( + f"Platform {full_name} not found", + name=f"{self.pkg_path}.{platform_name}", + ) return None + def platforms_are_loaded(self, platform_names: Iterable[str]) -> bool: + """Check if a platforms are loaded for an integration.""" + return all( + f"{self.domain}.{platform_name}" in self._cache + for platform_name in platform_names + ) + + def get_platform_cached(self, platform_name: str) -> ModuleType | None: + """Return a platform for an integration from cache.""" + return self._cache.get(f"{self.domain}.{platform_name}") # type: ignore[return-value] + def get_platform(self, platform_name: str) -> ModuleType: """Return a platform for an integration.""" - if platform := self._get_platform_cached(f"{self.domain}.{platform_name}"): + if platform := self._get_platform_cached_or_raise(platform_name): return platform return self._load_platform(platform_name) + def platforms_exists(self, platform_names: Iterable[str]) -> list[str]: + """Check if a platforms exists for an integration. + + This method is thread-safe and can be called from the executor + or event loop without doing blocking I/O. + """ + files = self._top_level_files + domain = self.domain + existing_platforms: list[str] = [] + missing_platforms = self._missing_platforms_cache + for platform_name in platform_names: + full_name = f"{domain}.{platform_name}" + if full_name not in missing_platforms and ( + f"{platform_name}.py" in files or platform_name in files + ): + existing_platforms.append(platform_name) + continue + missing_platforms[full_name] = True + + return existing_platforms + def _load_platform(self, platform_name: str) -> ModuleType: """Load a platform for an integration. @@ -1023,14 +1230,13 @@ class Integration: cache: dict[str, ModuleType] = self.hass.data[DATA_COMPONENTS] try: cache[full_name] = self._import_platform(platform_name) - except ImportError as ex: + except ModuleNotFoundError: if self.domain in cache: # If the domain is loaded, cache that the platform # does not exist so we do not try to load it again - missing_platforms_cache: dict[str, ImportError] = self.hass.data[ - DATA_MISSING_PLATFORMS - ] - missing_platforms_cache[full_name] = ex + self._missing_platforms_cache[full_name] = True + raise + except ImportError: raise except RuntimeError as err: # _DeadlockError inherits from RuntimeError @@ -1103,7 +1309,7 @@ def async_get_loaded_integration(hass: HomeAssistant, domain: str) -> Integratio cache = cast(dict[str, Integration | asyncio.Future[None]], cache) int_or_fut = cache.get(domain, _UNDEF) # Integration is never subclassed, so we can check for type - if type(int_or_fut) is Integration: # noqa: E721 + if type(int_or_fut) is Integration: return int_or_fut raise IntegrationNotLoaded(domain) @@ -1130,7 +1336,7 @@ async def async_get_integrations( for domain in domains: int_or_fut = cache.get(domain, _UNDEF) # Integration is never subclassed, so we can check for type - if type(int_or_fut) is Integration: # noqa: E721 + if type(int_or_fut) is Integration: results[domain] = int_or_fut elif int_or_fut is not _UNDEF: in_progress[domain] = cast(asyncio.Future[None], int_or_fut) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index c92dad2ae35..6bb6bd4d2d3 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -1,14 +1,14 @@ # Automatically generated by gen_requirements_all.py, do not edit -aiodhcpwatcher==0.8.2 -aiodiscover==1.6.1 +aiodhcpwatcher==1.0.0 +aiodiscover==2.0.0 aiohttp-fast-url-dispatcher==0.3.0 aiohttp-zlib-ng==0.3.1 aiohttp==3.9.3 aiohttp_cors==0.7.0 astral==2.2 async-interrupt==1.1.1 -async-upnp-client==0.38.2 +async-upnp-client==0.38.3 atomicwrites-homeassistant==1.4.1 attrs==23.2.0 awesomeversion==24.2.0 @@ -16,7 +16,7 @@ bcrypt==4.1.2 bleak-retry-connector==3.4.0 bleak==0.21.1 bluetooth-adapters==0.18.0 -bluetooth-auto-recovery==1.3.0 +bluetooth-auto-recovery==1.4.0 bluetooth-data-tools==1.19.0 cached_ipaddress==0.3.0 certifi>=2021.5.30 @@ -25,16 +25,15 @@ cryptography==42.0.5 dbus-fast==2.21.1 fnv-hash-fast==0.5.0 ha-av==10.1.1 -ha-ffmpeg==3.1.0 +ha-ffmpeg==3.2.0 habluetooth==2.4.2 -hass-nabucasa==0.78.0 +hass-nabucasa==0.79.0 hassil==1.6.1 home-assistant-bluetooth==1.12.0 -home-assistant-frontend==20240307.0 -home-assistant-intents==2024.3.12 +home-assistant-frontend==20240403.1 +home-assistant-intents==2024.4.3 httpx==0.27.0 ifaddr==0.2.0 -janus==1.0.0 Jinja2==3.1.3 lru-dict==1.3.0 mutagen==1.47.0 @@ -46,14 +45,14 @@ pip>=21.3.1 psutil-home-assistant==0.0.1 PyJWT==2.8.0 PyNaCl==1.5.0 -pyOpenSSL==24.0.0 +pyOpenSSL==24.1.0 pyserial==3.5 python-slugify==8.0.4 PyTurboJPEG==1.7.1 pyudev==0.23.2 PyYAML==6.0.1 requests==2.31.0 -SQLAlchemy==2.0.27 +SQLAlchemy==2.0.29 typing-extensions>=4.10.0,<5.0 ulid-transform==0.9.0 urllib3>=1.26.5,<2 @@ -61,7 +60,7 @@ voluptuous-serialize==2.6.0 voluptuous==0.13.1 webrtc-noise-gain==1.2.3 yarl==1.9.4 -zeroconf==0.131.0 +zeroconf==0.132.0 # Constrain pycryptodome to avoid vulnerability # see https://github.com/home-assistant/core/pull/16238 @@ -106,7 +105,7 @@ regex==2021.8.28 # these requirements are quite loose. As the entire stack has some outstanding issues, and # even newer versions seem to introduce new issues, it's useful for us to pin all these # requirements so we can directly link HA versions to these library versions. -anyio==4.1.0 +anyio==4.3.0 h11==0.14.0 httpcore==1.0.4 @@ -184,3 +183,10 @@ pandas==2.1.4 # chacha20poly1305-reuseable==0.12.0 is incompatible with cryptography==42.0.x chacha20poly1305-reuseable>=0.12.1 + +# pycountry<23.12.11 imports setuptools at run time +# https://github.com/pycountry/pycountry/blob/ea69bab36f00df58624a0e490fdad4ccdc14268b/HISTORY.txt#L39 +pycountry>=23.12.11 + +# scapy<2.5.0 will not work with python3.12 +scapy>=2.5.0 diff --git a/homeassistant/requirements.py b/homeassistant/requirements.py index f1add1ff3f8..e78398ebf03 100644 --- a/homeassistant/requirements.py +++ b/homeassistant/requirements.py @@ -1,4 +1,5 @@ """Module to handle installing requirements.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/runner.py b/homeassistant/runner.py index 622e69ecf8c..f036c7d6322 100644 --- a/homeassistant/runner.py +++ b/homeassistant/runner.py @@ -1,4 +1,5 @@ """Run Home Assistant.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/scripts/__init__.py b/homeassistant/scripts/__init__.py index 5b781d4eb37..f0600b70f48 100644 --- a/homeassistant/scripts/__init__.py +++ b/homeassistant/scripts/__init__.py @@ -1,4 +1,5 @@ """Home Assistant command line scripts.""" + from __future__ import annotations import argparse diff --git a/homeassistant/scripts/auth.py b/homeassistant/scripts/auth.py index dd3b9b7ba48..fff57c7adfe 100644 --- a/homeassistant/scripts/auth.py +++ b/homeassistant/scripts/auth.py @@ -1,4 +1,5 @@ """Script to manage users for the Home Assistant auth provider.""" + import argparse import asyncio import logging diff --git a/homeassistant/scripts/benchmark/__init__.py b/homeassistant/scripts/benchmark/__init__.py index a04493a8935..07f3d06f4cc 100644 --- a/homeassistant/scripts/benchmark/__init__.py +++ b/homeassistant/scripts/benchmark/__init__.py @@ -1,4 +1,5 @@ """Script to run benchmarks.""" + from __future__ import annotations import argparse @@ -96,7 +97,7 @@ async def fire_events_with_filter(hass): events_to_fire = 10**6 @core.callback - def event_filter(event): + def event_filter(event_data): """Filter event.""" return False diff --git a/homeassistant/scripts/check_config.py b/homeassistant/scripts/check_config.py index 5a9f62c938e..d38e24a24da 100644 --- a/homeassistant/scripts/check_config.py +++ b/homeassistant/scripts/check_config.py @@ -1,4 +1,5 @@ """Script to check the configuration file.""" + from __future__ import annotations import argparse @@ -175,7 +176,7 @@ def check(config_dir, secrets=False): "secrets": OrderedDict(), # secret cache and secrets loaded "except": OrderedDict(), # critical exceptions raised (with config) "warn": OrderedDict(), # non critical exceptions raised (with config) - #'components' is a HomeAssistantConfig # noqa: E265 + #'components' is a HomeAssistantConfig "secret_cache": {}, } diff --git a/homeassistant/scripts/ensure_config.py b/homeassistant/scripts/ensure_config.py index 786b16ca923..e1ae7bc9142 100644 --- a/homeassistant/scripts/ensure_config.py +++ b/homeassistant/scripts/ensure_config.py @@ -1,4 +1,5 @@ """Script to ensure a configuration file exists.""" + import argparse import asyncio import os diff --git a/homeassistant/scripts/macos/__init__.py b/homeassistant/scripts/macos/__init__.py index 7668c73be9d..f629492ec39 100644 --- a/homeassistant/scripts/macos/__init__.py +++ b/homeassistant/scripts/macos/__init__.py @@ -1,4 +1,5 @@ """Script to install/uninstall HA into OS X.""" + import os import time diff --git a/homeassistant/setup.py b/homeassistant/setup.py index 0cdbcec3ff3..2e64fefee77 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -1,12 +1,15 @@ """All methods needed to bootstrap a Home Assistant instance.""" + from __future__ import annotations import asyncio -from collections.abc import Awaitable, Callable, Generator, Iterable +from collections import defaultdict +from collections.abc import Awaitable, Callable, Generator, Mapping import contextlib +import contextvars +from enum import StrEnum import logging.handlers import time -from timeit import default_timer as timer from types import ModuleType from typing import Any, Final, TypedDict @@ -27,10 +30,14 @@ from .core import ( from .exceptions import DependencyError, HomeAssistantError from .helpers import translation from .helpers.issue_registry import IssueSeverity, async_create_issue -from .helpers.typing import ConfigType, EventType -from .util import ensure_unique_string +from .helpers.typing import ConfigType from .util.async_ import create_eager_task +current_setup_group: contextvars.ContextVar[tuple[str, str | None] | None] = ( + contextvars.ContextVar("current_setup_group", default=None) +) + + _LOGGER = logging.getLogger(__name__) ATTR_COMPONENT: Final = "component" @@ -53,12 +60,12 @@ DATA_SETUP = "setup_tasks" # is finished, regardless of if the setup was successful or not. DATA_SETUP_DONE = "setup_done" -# DATA_SETUP_STARTED is a dict [str, float], indicating when an attempt +# DATA_SETUP_STARTED is a dict [tuple[str, str | None], float], indicating when an attempt # to setup a component started. DATA_SETUP_STARTED = "setup_started" -# DATA_SETUP_TIME is a dict [str, timedelta], indicating how time was spent -# setting up a component. +# DATA_SETUP_TIME is a defaultdict[str, defaultdict[str | None, defaultdict[SetupPhases, float]]] +# indicating how time was spent setting up a component and each group (config entry). DATA_SETUP_TIME = "setup_time" DATA_DEPS_REQS = "deps_reqs_processed" @@ -278,6 +285,19 @@ async def _async_setup_component( # noqa: C901 log_error(f"Dependency is disabled - {integration.disabled}") return False + integration_set = {domain} + + load_translations_task: asyncio.Task[None] | None = None + if integration.has_translations and not translation.async_translations_loaded( + hass, integration_set + ): + # For most cases we expect the translations are already + # loaded since we try to load them in bootstrap ahead of time. + # If for some reason the background task in bootstrap was too slow + # or the integration was added after bootstrap, we will load them here. + load_translations_task = create_eager_task( + translation.async_load_integrations(hass, integration_set) + ) # Validate all dependencies exist and there are no circular dependencies if not await integration.resolve_dependencies(): return False @@ -299,7 +319,7 @@ async def _async_setup_component( # noqa: C901 return False integration_config_info = await conf_util.async_process_component_config( - hass, config, integration + hass, config, integration, component ) conf_util.async_handle_component_errors(hass, integration_config_info, integration) processed_config = conf_util.async_drop_config_annotations( @@ -343,21 +363,9 @@ async def _async_setup_component( # noqa: C901 }, ) - start = timer() _LOGGER.info("Setting up %s", domain) - integration_set = {domain} - load_translations_task: asyncio.Task[None] | None = None - if not translation.async_translations_loaded(hass, integration_set): - # For most cases we expect the translations are already - # loaded since we try to load them in bootstrap ahead of time. - # If for some reason the background task in bootstrap was too slow - # or the integration was added after bootstrap, we will load them here. - load_translations_task = create_eager_task( - translation.async_load_integrations(hass, integration_set) - ) - - with async_start_setup(hass, integration_set): + with async_start_setup(hass, integration=domain, phase=SetupPhases.SETUP): if hasattr(component, "PLATFORM_SCHEMA"): # Entity components have their own warning warn_task = None @@ -404,11 +412,8 @@ async def _async_setup_component( # noqa: C901 async_notify_setup_error(hass, domain, integration.documentation) return False finally: - end = timer() if warn_task: warn_task.cancel() - _LOGGER.info("Setup of domain %s took %.1f seconds", domain, end - start) - if result is False: log_error("Integration failed to initialize.") return False @@ -419,28 +424,32 @@ async def _async_setup_component( # noqa: C901 ) return False - # Flush out async_setup calling create_task. Fragile but covered by test. + if load_translations_task: + await load_translations_task + + if integration.platforms_exists(("config_flow",)): + # If the integration has a config_flow, flush out async_setup calling create_task + # with an asyncio.sleep(0) so we can wait for import flows. + # Fragile but covered by test. await asyncio.sleep(0) await hass.config_entries.flow.async_wait_import_flow_initialized(domain) - if load_translations_task: - await load_translations_task - # Add to components before the entry.async_setup - # call to avoid a deadlock when forwarding platforms - hass.config.components.add(domain) + # Add to components before the entry.async_setup + # call to avoid a deadlock when forwarding platforms + hass.config.components.add(domain) - if entries := hass.config_entries.async_entries( - domain, include_ignore=False, include_disabled=False - ): - await asyncio.gather( - *( - create_eager_task( - entry.async_setup(hass, integration=integration), - name=f"config entry setup {entry.title} {entry.domain} {entry.entry_id}", - ) - for entry in entries + if entries := hass.config_entries.async_entries( + domain, include_ignore=False, include_disabled=False + ): + await asyncio.gather( + *( + create_eager_task( + entry.async_setup(hass, integration=integration), + name=f"config entry setup {entry.title} {entry.domain} {entry.entry_id}", ) + for entry in entries ) + ) # Cleanup if domain in hass.data[DATA_SETUP]: @@ -482,8 +491,27 @@ async def async_prepare_setup_platform( log_error(str(err)) return None + # Platforms cannot exist on their own, they are part of their integration. + # If the integration is not set up yet, and can be set up, set it up. + # + # We do this before we import the platform so the platform already knows + # where the top level component is. + # + if load_top_level_component := integration.domain not in hass.config.components: + try: + component = await integration.async_get_component() + except ImportError as exc: + log_error(f"Unable to import the component ({exc}).") + return None + + if not integration.platforms_exists((domain,)): + log_error( + f"Platform not found (No module named '{integration.pkg_path}.{domain}')" + ) + return None + try: - platform = integration.get_platform(domain) + platform = await integration.async_get_platform(domain) except ImportError as exc: log_error(f"Platform not found ({exc}).") return None @@ -494,13 +522,7 @@ async def async_prepare_setup_platform( # Platforms cannot exist on their own, they are part of their integration. # If the integration is not set up yet, and can be set up, set it up. - if integration.domain not in hass.config.components: - try: - component = integration.get_component() - except ImportError as exc: - log_error(f"Unable to import the component ({exc}).") - return None - + if load_top_level_component: if ( hasattr(component, "setup") or hasattr(component, "async_setup") ) and not await async_setup_component(hass, integration.domain, hass_config): @@ -577,27 +599,30 @@ def _async_when_setup( listeners: list[CALLBACK_TYPE] = [] - async def _matched_event(event: Event) -> None: + async def _matched_event(event: Event[Any]) -> None: """Call the callback when we matched an event.""" for listener in listeners: listener() await when_setup() @callback - def _async_is_component_filter(event: EventType[EventComponentLoaded]) -> bool: + def _async_is_component_filter(event_data: EventComponentLoaded) -> bool: """Check if the event is for the component.""" - return event.data[ATTR_COMPONENT] == component + return event_data[ATTR_COMPONENT] == component listeners.append( hass.bus.async_listen( EVENT_COMPONENT_LOADED, _matched_event, - event_filter=_async_is_component_filter, # type: ignore[arg-type] + event_filter=_async_is_component_filter, + run_immediately=True, ) ) if start_event: listeners.append( - hass.bus.async_listen(EVENT_HOMEASSISTANT_START, _matched_event) + hass.bus.async_listen( + EVENT_HOMEASSISTANT_START, _matched_event, run_immediately=True + ) ) @@ -615,27 +640,170 @@ def async_get_loaded_integrations(hass: core.HomeAssistant) -> set[str]: return integrations +class SetupPhases(StrEnum): + """Constants for setup time measurements.""" + + SETUP = "setup" + """Set up of a component in __init__.py.""" + CONFIG_ENTRY_SETUP = "config_entry_setup" + """Set up of a config entry in __init__.py.""" + PLATFORM_SETUP = "platform_setup" + """Set up of a platform integration. + + ex async_setup_platform or setup_platform or + a legacy platform like device_tracker.legacy + """ + CONFIG_ENTRY_PLATFORM_SETUP = "config_entry_platform_setup" + """Set up of a platform in a config entry after the config entry is setup. + + This is only for platforms that are not awaited in async_setup_entry. + """ + WAIT_BASE_PLATFORM_SETUP = "wait_base_component" + """Wait time for the base component to be setup.""" + WAIT_IMPORT_PLATFORMS = "wait_import_platforms" + """Wait time for the platforms to import.""" + WAIT_IMPORT_PACKAGES = "wait_import_packages" + """Wait time for the packages to import.""" + + +def _setup_started( + hass: core.HomeAssistant, +) -> dict[tuple[str, str | None], float]: + """Return the setup started dict.""" + if DATA_SETUP_STARTED not in hass.data: + hass.data[DATA_SETUP_STARTED] = {} + return hass.data[DATA_SETUP_STARTED] # type: ignore[no-any-return] + + +@contextlib.contextmanager +def async_pause_setup( + hass: core.HomeAssistant, phase: SetupPhases +) -> Generator[None, None, None]: + """Keep track of time we are blocked waiting for other operations. + + We want to count the time we wait for importing and + setting up the base components so we can subtract it + from the total setup time. + """ + if not (running := current_setup_group.get()) or running not in _setup_started( + hass + ): + # This means we are likely in a late platform setup + # that is running in a task so we do not want + # to subtract out the time later as nothing is waiting + # for the code inside the context manager to finish. + yield + return + + started = time.monotonic() + try: + yield + finally: + time_taken = time.monotonic() - started + integration, group = running + # Add negative time for the time we waited + _setup_times(hass)[integration][group][phase] = -time_taken + _LOGGER.debug( + "Adding wait for %s for %s (%s) of %.2f", + phase, + integration, + group, + time_taken, + ) + + +def _setup_times( + hass: core.HomeAssistant, +) -> defaultdict[str, defaultdict[str | None, defaultdict[SetupPhases, float]]]: + """Return the setup timings default dict.""" + if DATA_SETUP_TIME not in hass.data: + hass.data[DATA_SETUP_TIME] = defaultdict( + lambda: defaultdict(lambda: defaultdict(float)) + ) + return hass.data[DATA_SETUP_TIME] # type: ignore[no-any-return] + + @contextlib.contextmanager def async_start_setup( - hass: core.HomeAssistant, components: Iterable[str] + hass: core.HomeAssistant, + integration: str, + phase: SetupPhases, + group: str | None = None, ) -> Generator[None, None, None]: - """Keep track of when setup starts and finishes.""" - setup_started = hass.data.setdefault(DATA_SETUP_STARTED, {}) + """Keep track of when setup starts and finishes. + + :param hass: Home Assistant instance + :param integration: The integration that is being setup + :param phase: The phase of setup + :param group: The group (config entry/platform instance) that is being setup + + A group is a group of setups that run in parallel. + + """ + if hass.is_stopping or hass.state is core.CoreState.running: + # Don't track setup times when we are shutting down or already running + # as we present the timings as "Integration startup time", and we + # don't want to add all the setup retry times to that. + yield + return + + setup_started = _setup_started(hass) + current = (integration, group) + if current in setup_started: + # We are already inside another async_start_setup, this like means we + # are setting up a platform inside async_setup_entry so we should not + # record this as a new setup + yield + return + started = time.monotonic() - unique_components: dict[str, str] = {} - for domain in components: - unique = ensure_unique_string(domain, setup_started) - unique_components[unique] = domain - setup_started[unique] = started + current_setup_group.set(current) + setup_started[current] = started - yield + try: + yield + finally: + time_taken = time.monotonic() - started + del setup_started[current] + group_setup_times = _setup_times(hass)[integration][group] + # We may see the phase multiple times if there are multiple + # platforms, but we only care about the longest time. + group_setup_times[phase] = max(group_setup_times[phase], time_taken) + if group is None: + _LOGGER.info( + "Setup of domain %s took %.2f seconds", integration, time_taken + ) + elif _LOGGER.isEnabledFor(logging.DEBUG): + wait_time = -sum(value for value in group_setup_times.values() if value < 0) + calculated_time = time_taken - wait_time + _LOGGER.debug( + "Phase %s for %s (%s) took %.2fs (elapsed=%.2fs) (wait_time=%.2fs)", + phase, + integration, + group, + calculated_time, + time_taken, + wait_time, + ) - setup_time: dict[str, float] = hass.data.setdefault(DATA_SETUP_TIME, {}) - time_taken = time.monotonic() - started - for unique, domain in unique_components.items(): - del setup_started[unique] - integration = domain.partition(".")[0] - if integration in setup_time: - setup_time[integration] += time_taken - else: - setup_time[integration] = time_taken + +@callback +def async_get_setup_timings(hass: core.HomeAssistant) -> dict[str, float]: + """Return timing data for each integration.""" + setup_time = _setup_times(hass) + domain_timings: dict[str, float] = {} + top_level_timings: Mapping[SetupPhases, float] + for domain, timings in setup_time.items(): + top_level_timings = timings.get(None, {}) + total_top_level = sum(top_level_timings.values()) + # Groups (config entries/platform instance) are setup in parallel so we + # take the max of the group timings and add it to the top level + group_totals = { + group: sum(group_timings.values()) + for group, group_timings in timings.items() + if group is not None + } + group_max = max(group_totals.values(), default=0) + domain_timings[domain] = total_top_level + group_max + + return domain_timings diff --git a/homeassistant/strings.json b/homeassistant/strings.json index cfe1061bce6..97bba2fb3b7 100644 --- a/homeassistant/strings.json +++ b/homeassistant/strings.json @@ -96,7 +96,8 @@ "location": "Location", "pin": "PIN code", "mode": "Mode", - "path": "Path" + "path": "Path", + "language": "Language" }, "create_entry": { "authenticated": "Successfully authenticated" @@ -129,6 +130,7 @@ "oauth2_unauthorized": "OAuth authorization error while obtaining access token.", "oauth2_failed": "Error while obtaining access token.", "reauth_successful": "Re-authentication was successful", + "reconfigure_successful": "Re-configuration was successful", "unknown_authorize_url_generation": "Unknown error generating an authorize URL.", "cloud_not_connected": "Not connected to Home Assistant Cloud." } diff --git a/homeassistant/util/__init__.py b/homeassistant/util/__init__.py index 0a4b156fc8c..1ee33bdd173 100644 --- a/homeassistant/util/__init__.py +++ b/homeassistant/util/__init__.py @@ -1,4 +1,5 @@ """Helper methods for various modules.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/util/aiohttp.py b/homeassistant/util/aiohttp.py index ceb5e502221..94906e29f00 100644 --- a/homeassistant/util/aiohttp.py +++ b/homeassistant/util/aiohttp.py @@ -1,4 +1,5 @@ """Utilities to help with aiohttp.""" + from __future__ import annotations from http import HTTPStatus diff --git a/homeassistant/util/async_.py b/homeassistant/util/async_.py index 36589e01d65..8c042242e0b 100644 --- a/homeassistant/util/async_.py +++ b/homeassistant/util/async_.py @@ -1,15 +1,14 @@ """Asyncio utilities.""" + from __future__ import annotations from asyncio import AbstractEventLoop, Future, Semaphore, Task, gather, get_running_loop -from collections.abc import Awaitable, Callable +from collections.abc import Awaitable, Callable, Coroutine import concurrent.futures from contextlib import suppress import functools import logging -import sys import threading -from traceback import extract_stack from typing import Any, ParamSpec, TypeVar, TypeVarTuple from homeassistant.exceptions import HomeAssistantError @@ -23,35 +22,20 @@ _R = TypeVar("_R") _P = ParamSpec("_P") _Ts = TypeVarTuple("_Ts") -if sys.version_info >= (3, 12, 0): - def create_eager_task( - coro: Awaitable[_T], - *, - name: str | None = None, - loop: AbstractEventLoop | None = None, - ) -> Task[_T]: - """Create a task from a coroutine and schedule it to run immediately.""" - return Task( - coro, - loop=loop or get_running_loop(), - name=name, - eager_start=True, # type: ignore[call-arg] - ) -else: - - def create_eager_task( - coro: Awaitable[_T], - *, - name: str | None = None, - loop: AbstractEventLoop | None = None, - ) -> Task[_T]: - """Create a task from a coroutine and schedule it to run immediately.""" - return Task( - coro, - loop=loop or get_running_loop(), - name=name, - ) +def create_eager_task( + coro: Coroutine[Any, Any, _T], + *, + name: str | None = None, + loop: AbstractEventLoop | None = None, +) -> Task[_T]: + """Create a task from a coroutine and schedule it to run immediately.""" + return Task( + coro, + loop=loop or get_running_loop(), + name=name, + eager_start=True, + ) def cancelling(task: Future[Any]) -> bool: @@ -116,14 +100,6 @@ def check_loop( The default advisory message is 'Use `await hass.async_add_executor_job()' Set `advise_msg` to an alternate message if the solution differs. """ - # pylint: disable=import-outside-toplevel - from homeassistant.core import HomeAssistant, async_get_hass - from homeassistant.helpers.frame import ( - MissingIntegrationFrame, - get_integration_frame, - ) - from homeassistant.loader import async_suggest_report_issue - try: get_running_loop() in_loop = True @@ -133,18 +109,32 @@ def check_loop( if not in_loop: return + # Import only after we know we are running in the event loop + # so threads do not have to pay the late import cost. + # pylint: disable=import-outside-toplevel + from homeassistant.core import HomeAssistant, async_get_hass + from homeassistant.helpers.frame import ( + MissingIntegrationFrame, + get_current_frame, + get_integration_frame, + ) + from homeassistant.loader import async_suggest_report_issue + found_frame = None - stack = extract_stack() - - if ( - func.__name__ == "sleep" - and len(stack) >= 3 - and stack[-3].filename.endswith("pydevd.py") - ): - # Don't report `time.sleep` injected by the debugger (pydevd.py) - # stack[-1] is us, stack[-2] is protected_loop_func, stack[-3] is the offender - return + if func.__name__ == "sleep": + # + # Avoid extracting the stack unless we need to since it + # will have to access the linecache which can do blocking + # I/O and we are trying to avoid blocking calls. + # + # frame[1] is us + # frame[2] is protected_loop_func + # frame[3] is the offender + with suppress(ValueError): + offender_frame = get_current_frame(3) + if offender_frame.f_code.co_filename.endswith("pydevd.py"): + return try: integration_frame = get_integration_frame() @@ -167,7 +157,6 @@ def check_loop( module=integration_frame.module, ) - found_frame = integration_frame.frame _LOGGER.warning( ( "Detected blocking call to %s inside the event loop by %sintegration '%s' " @@ -177,8 +166,8 @@ def check_loop( "custom " if integration_frame.custom_integration else "", integration_frame.integration, integration_frame.relative_filename, - found_frame.lineno, - (found_frame.line or "?").strip(), + integration_frame.line_number, + integration_frame.line, report_issue, ) @@ -186,8 +175,8 @@ def check_loop( raise RuntimeError( "Blocking calls must be done in the executor or a separate thread;" f" {advise_msg or 'Use `await hass.async_add_executor_job()`'}; at" - f" {integration_frame.relative_filename}, line {found_frame.lineno}:" - f" {(found_frame.line or '?').strip()}" + f" {integration_frame.relative_filename}, line {integration_frame.line_number}:" + f" {integration_frame.line}" ) diff --git a/homeassistant/util/color.py b/homeassistant/util/color.py index 0ab4ac8c6c1..ab5c4037f9b 100644 --- a/homeassistant/util/color.py +++ b/homeassistant/util/color.py @@ -1,4 +1,5 @@ """Color util methods.""" + from __future__ import annotations import colorsys diff --git a/homeassistant/util/decorator.py b/homeassistant/util/decorator.py index c648f6f1cab..5bd817de103 100644 --- a/homeassistant/util/decorator.py +++ b/homeassistant/util/decorator.py @@ -1,4 +1,5 @@ """Decorator utility functions.""" + from __future__ import annotations from collections.abc import Callable, Hashable diff --git a/homeassistant/util/dt.py b/homeassistant/util/dt.py index 47863d32e67..39976cce5f7 100644 --- a/homeassistant/util/dt.py +++ b/homeassistant/util/dt.py @@ -1,4 +1,5 @@ """Helper methods to handle the time in Home Assistant.""" + from __future__ import annotations import bisect @@ -178,20 +179,17 @@ def start_of_local_day(dt_or_d: dt.date | dt.datetime | None = None) -> dt.datet # All rights reserved. # https://github.com/django/django/blob/main/LICENSE @overload -def parse_datetime(dt_str: str) -> dt.datetime | None: - ... +def parse_datetime(dt_str: str) -> dt.datetime | None: ... @overload -def parse_datetime(dt_str: str, *, raise_on_error: Literal[True]) -> dt.datetime: - ... +def parse_datetime(dt_str: str, *, raise_on_error: Literal[True]) -> dt.datetime: ... @overload def parse_datetime( dt_str: str, *, raise_on_error: Literal[False] | bool -) -> dt.datetime | None: - ... +) -> dt.datetime | None: ... def parse_datetime(dt_str: str, *, raise_on_error: bool = False) -> dt.datetime | None: diff --git a/homeassistant/util/enum.py b/homeassistant/util/enum.py index f0de2206f1f..d0ef010f8bb 100644 --- a/homeassistant/util/enum.py +++ b/homeassistant/util/enum.py @@ -1,4 +1,5 @@ """Helpers for working with enums.""" + from collections.abc import Callable import contextlib from enum import Enum diff --git a/homeassistant/util/executor.py b/homeassistant/util/executor.py index 145c2ba4f04..cfd81e26e34 100644 --- a/homeassistant/util/executor.py +++ b/homeassistant/util/executor.py @@ -1,4 +1,5 @@ """Executor util helpers.""" + from __future__ import annotations from concurrent.futures import ThreadPoolExecutor diff --git a/homeassistant/util/file.py b/homeassistant/util/file.py index 1af65fa51d7..6d1c9b6e522 100644 --- a/homeassistant/util/file.py +++ b/homeassistant/util/file.py @@ -1,4 +1,5 @@ """File utility functions.""" + from __future__ import annotations import logging diff --git a/homeassistant/util/frozen_dataclass_compat.py b/homeassistant/util/frozen_dataclass_compat.py index 858084bcabc..fa86ce8ff87 100644 --- a/homeassistant/util/frozen_dataclass_compat.py +++ b/homeassistant/util/frozen_dataclass_compat.py @@ -3,13 +3,12 @@ This module enabled a non-breaking transition from mutable to frozen dataclasses derived from EntityDescription and sub classes thereof. """ + from __future__ import annotations import dataclasses import sys -from typing import Any - -from typing_extensions import dataclass_transform +from typing import Any, dataclass_transform def _class_fields(cls: type, kw_only: bool) -> list[tuple[str, Any, Any]]: @@ -56,9 +55,7 @@ class FrozenOrThawed(type): def _make_dataclass(cls, name: str, bases: tuple[type, ...], kw_only: bool) -> None: class_fields = _class_fields(cls, kw_only) - dataclass_bases = [] - for base in bases: - dataclass_bases.append(getattr(base, "_dataclass", base)) + dataclass_bases = [getattr(base, "_dataclass", base) for base in bases] cls._dataclass = dataclasses.make_dataclass( name, class_fields, bases=tuple(dataclass_bases), frozen=True ) diff --git a/homeassistant/util/json.py b/homeassistant/util/json.py index 3a337cf0e18..9a30ae8f104 100644 --- a/homeassistant/util/json.py +++ b/homeassistant/util/json.py @@ -1,4 +1,5 @@ """JSON utility functions.""" + from __future__ import annotations from collections.abc import Callable diff --git a/homeassistant/util/language.py b/homeassistant/util/language.py index 73db81c91ce..8644f8014b6 100644 --- a/homeassistant/util/language.py +++ b/homeassistant/util/language.py @@ -1,4 +1,5 @@ """Helper methods for language selection in Home Assistant.""" + from __future__ import annotations from collections.abc import Iterable @@ -138,7 +139,7 @@ class Dialect: region_idx = pref_regions.index(dialect.region) else: # Can't happen, but mypy is not smart enough - raise ValueError() + raise ValueError # More preferred regions are at the front. # Add 1 to boost above a weak match where no regions are set. diff --git a/homeassistant/util/limited_size_dict.py b/homeassistant/util/limited_size_dict.py index a12618af0ec..6166a6c8239 100644 --- a/homeassistant/util/limited_size_dict.py +++ b/homeassistant/util/limited_size_dict.py @@ -1,4 +1,5 @@ """Helpers for script and automation tracing and debugging.""" + from __future__ import annotations from collections import OrderedDict diff --git a/homeassistant/util/location.py b/homeassistant/util/location.py index d2179ecd112..24c49c5427c 100644 --- a/homeassistant/util/location.py +++ b/homeassistant/util/location.py @@ -2,6 +2,7 @@ detect_location_info and elevation are mocked by default during tests. """ + from __future__ import annotations from functools import lru_cache diff --git a/homeassistant/util/logging.py b/homeassistant/util/logging.py index 0f86cde50fe..489b6493ef1 100644 --- a/homeassistant/util/logging.py +++ b/homeassistant/util/logging.py @@ -1,4 +1,5 @@ """Logging utilities.""" + from __future__ import annotations import asyncio @@ -138,15 +139,13 @@ def _callback_wrapper( @overload def catch_log_exception( func: Callable[[*_Ts], Coroutine[Any, Any, Any]], format_err: Callable[[*_Ts], Any] -) -> Callable[[*_Ts], Coroutine[Any, Any, None]]: - ... +) -> Callable[[*_Ts], Coroutine[Any, Any, None]]: ... @overload def catch_log_exception( func: Callable[[*_Ts], Any], format_err: Callable[[*_Ts], Any] -) -> Callable[[*_Ts], None] | Callable[[*_Ts], Coroutine[Any, Any, None]]: - ... +) -> Callable[[*_Ts], None] | Callable[[*_Ts], Coroutine[Any, Any, None]]: ... def catch_log_exception( diff --git a/homeassistant/util/network.py b/homeassistant/util/network.py index 46eaece25c4..08a2c2a3967 100644 --- a/homeassistant/util/network.py +++ b/homeassistant/util/network.py @@ -1,4 +1,5 @@ """Network utilities.""" + from __future__ import annotations from ipaddress import IPv4Address, IPv6Address, ip_address, ip_network diff --git a/homeassistant/util/package.py b/homeassistant/util/package.py index ce6276ef4d4..44f9be3272f 100644 --- a/homeassistant/util/package.py +++ b/homeassistant/util/package.py @@ -1,4 +1,5 @@ """Helpers to install PyPi packages.""" + from __future__ import annotations import asyncio diff --git a/homeassistant/util/percentage.py b/homeassistant/util/percentage.py index cc4835022d3..e01af5400f4 100644 --- a/homeassistant/util/percentage.py +++ b/homeassistant/util/percentage.py @@ -1,4 +1,5 @@ """Percentage util functions.""" + from __future__ import annotations from typing import TypeVar @@ -80,7 +81,7 @@ def ranged_value_to_percentage( def percentage_to_ranged_value( - low_high_range: tuple[float, float], percentage: int + low_high_range: tuple[float, float], percentage: float ) -> float: """Given a range of low and high values convert a percentage to a single value. diff --git a/homeassistant/util/pil.py b/homeassistant/util/pil.py index 58dd48dec5e..733f640ce48 100644 --- a/homeassistant/util/pil.py +++ b/homeassistant/util/pil.py @@ -2,6 +2,7 @@ Can only be used by integrations that have pillow in their requirements. """ + from __future__ import annotations from PIL.ImageDraw import ImageDraw diff --git a/homeassistant/util/read_only_dict.py b/homeassistant/util/read_only_dict.py index bb93dd41298..90245ce7ca9 100644 --- a/homeassistant/util/read_only_dict.py +++ b/homeassistant/util/read_only_dict.py @@ -1,4 +1,5 @@ """Read only dictionary.""" + from typing import Any, TypeVar diff --git a/homeassistant/util/scaling.py b/homeassistant/util/scaling.py index 70e2ac2516a..9b2a4f4afcb 100644 --- a/homeassistant/util/scaling.py +++ b/homeassistant/util/scaling.py @@ -1,4 +1,5 @@ """Scaling util functions.""" + from __future__ import annotations diff --git a/homeassistant/util/signal_type.py b/homeassistant/util/signal_type.py new file mode 100644 index 00000000000..be634ce6ba9 --- /dev/null +++ b/homeassistant/util/signal_type.py @@ -0,0 +1,43 @@ +"""Define SignalTypes for dispatcher.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Generic, TypeVarTuple + +_Ts = TypeVarTuple("_Ts") + + +@dataclass(frozen=True) +class _SignalTypeBase(Generic[*_Ts]): + """Generic base class for SignalType.""" + + name: str + + def __hash__(self) -> int: + """Return hash of name.""" + + return hash(self.name) + + def __eq__(self, other: Any) -> bool: + """Check equality for dict keys to be compatible with str.""" + + if isinstance(other, str): + return self.name == other + if isinstance(other, SignalType): + return self.name == other.name + return False + + +@dataclass(frozen=True, eq=False) +class SignalType(_SignalTypeBase[*_Ts]): + """Generic string class for signal to improve typing.""" + + +@dataclass(frozen=True, eq=False) +class SignalTypeFormat(_SignalTypeBase[*_Ts]): + """Generic string class for signal. Requires call to 'format' before use.""" + + def format(self, *args: Any, **kwargs: Any) -> SignalType[*_Ts]: + """Format name and return new SignalType instance.""" + return SignalType(self.name.format(*args, **kwargs)) diff --git a/homeassistant/util/ssl.py b/homeassistant/util/ssl.py index 6bfbec88a33..7c1e653ce75 100644 --- a/homeassistant/util/ssl.py +++ b/homeassistant/util/ssl.py @@ -1,4 +1,5 @@ """Helper to create SSL contexts.""" + import contextlib from enum import StrEnum from functools import cache diff --git a/homeassistant/util/thread.py b/homeassistant/util/thread.py index 0d600486f2f..7673d962d74 100644 --- a/homeassistant/util/thread.py +++ b/homeassistant/util/thread.py @@ -1,4 +1,5 @@ """Threading util helpers.""" + import ctypes import inspect import logging diff --git a/homeassistant/util/timeout.py b/homeassistant/util/timeout.py index 33cc0bd18e6..cf73ee6b220 100644 --- a/homeassistant/util/timeout.py +++ b/homeassistant/util/timeout.py @@ -3,6 +3,7 @@ Set of helper classes to handle timeouts of tasks with advanced options like zones and freezing of timeouts. """ + from __future__ import annotations import asyncio diff --git a/homeassistant/util/ulid.py b/homeassistant/util/ulid.py index 818b8015549..65f1b8226c0 100644 --- a/homeassistant/util/ulid.py +++ b/homeassistant/util/ulid.py @@ -1,4 +1,5 @@ """Helpers to generate ulids.""" + from __future__ import annotations from ulid_transform import ( diff --git a/homeassistant/util/unit_conversion.py b/homeassistant/util/unit_conversion.py index fe1974f2bee..04ce0715192 100644 --- a/homeassistant/util/unit_conversion.py +++ b/homeassistant/util/unit_conversion.py @@ -1,4 +1,5 @@ """Typing Helpers for Home Assistant.""" + from __future__ import annotations from collections.abc import Callable @@ -334,6 +335,7 @@ class SpeedConverter(BaseUnitConverter): UnitOfSpeed.KNOTS: _HRS_TO_SECS / _NAUTICAL_MILE_TO_M, UnitOfSpeed.METERS_PER_SECOND: 1, UnitOfSpeed.MILES_PER_HOUR: _HRS_TO_SECS / _MILE_TO_M, + UnitOfSpeed.BEAUFORT: 1, } VALID_UNITS = { UnitOfVolumetricFlux.INCHES_PER_DAY, @@ -345,8 +347,73 @@ class SpeedConverter(BaseUnitConverter): UnitOfSpeed.KNOTS, UnitOfSpeed.METERS_PER_SECOND, UnitOfSpeed.MILES_PER_HOUR, + UnitOfSpeed.BEAUFORT, } + @classmethod + @lru_cache + def converter_factory( + cls, from_unit: str | None, to_unit: str | None + ) -> Callable[[float], float]: + """Return a function to convert a speed from one unit to another.""" + if from_unit == to_unit: + # Return a function that does nothing. This is not + # in _converter_factory because we do not want to wrap + # it with the None check in converter_factory_allow_none. + return lambda value: value + + return cls._converter_factory(from_unit, to_unit) + + @classmethod + @lru_cache + def converter_factory_allow_none( + cls, from_unit: str | None, to_unit: str | None + ) -> Callable[[float | None], float | None]: + """Return a function to convert a speed from one unit to another which allows None.""" + if from_unit == to_unit: + # Return a function that does nothing. This is not + # in _converter_factory because we do not want to wrap + # it with the None check in this case. + return lambda value: value + + convert = cls._converter_factory(from_unit, to_unit) + return lambda value: None if value is None else convert(value) + + @classmethod + def _converter_factory( + cls, from_unit: str | None, to_unit: str | None + ) -> Callable[[float], float]: + """Convert a speed from one unit to another, eg. 14m/s will return 7Bft.""" + # We cannot use the implementation from BaseUnitConverter here because the + # Beaufort scale is not a constant value to divide or multiply with. + if ( + from_unit not in SpeedConverter.VALID_UNITS + or to_unit not in SpeedConverter.VALID_UNITS + ): + raise HomeAssistantError( + UNIT_NOT_RECOGNIZED_TEMPLATE.format(to_unit, cls.UNIT_CLASS) + ) + + if from_unit == UnitOfSpeed.BEAUFORT: + to_ratio = cls._UNIT_CONVERSION[to_unit] + return lambda val: cls._beaufort_to_ms(val) * to_ratio + if to_unit == UnitOfSpeed.BEAUFORT: + from_ratio = cls._UNIT_CONVERSION[from_unit] + return lambda val: cls._ms_to_beaufort(val / from_ratio) + + from_ratio, to_ratio = cls._get_from_to_ratio(from_unit, to_unit) + return lambda val: (val / from_ratio) * to_ratio + + @classmethod + def _ms_to_beaufort(cls, ms: float) -> float: + """Convert a speed in m/s to Beaufort.""" + return float(round(((ms / 0.836) ** 2) ** (1 / 3))) + + @classmethod + def _beaufort_to_ms(cls, beaufort: float) -> float: + """Convert a speed in Beaufort to m/s.""" + return float(0.836 * beaufort ** (3 / 2)) + class TemperatureConverter(BaseUnitConverter): """Utility to convert temperature values.""" diff --git a/homeassistant/util/unit_system.py b/homeassistant/util/unit_system.py index c9da324e8a5..bd31b4286ab 100644 --- a/homeassistant/util/unit_system.py +++ b/homeassistant/util/unit_system.py @@ -1,4 +1,5 @@ """Unit system helper class and methods.""" + from __future__ import annotations from numbers import Number diff --git a/homeassistant/util/variance.py b/homeassistant/util/variance.py index d28bdc9d63e..b109e5c476c 100644 --- a/homeassistant/util/variance.py +++ b/homeassistant/util/variance.py @@ -1,4 +1,5 @@ """Util functions to help filter out similar results.""" + from __future__ import annotations from collections.abc import Callable @@ -13,22 +14,19 @@ _P = ParamSpec("_P") @overload def ignore_variance( func: Callable[_P, int], ignored_variance: int -) -> Callable[_P, int]: - ... +) -> Callable[_P, int]: ... @overload def ignore_variance( func: Callable[_P, float], ignored_variance: float -) -> Callable[_P, float]: - ... +) -> Callable[_P, float]: ... @overload def ignore_variance( func: Callable[_P, datetime], ignored_variance: timedelta -) -> Callable[_P, datetime]: - ... +) -> Callable[_P, datetime]: ... def ignore_variance(func: Callable[_P, _R], ignored_variance: Any) -> Callable[_P, _R]: diff --git a/homeassistant/util/yaml/__init__.py b/homeassistant/util/yaml/__init__.py index fe4f01677cd..cf90b223cb6 100644 --- a/homeassistant/util/yaml/__init__.py +++ b/homeassistant/util/yaml/__init__.py @@ -1,4 +1,5 @@ """YAML utility functions.""" + from .const import SECRET_YAML from .dumper import dump, save_yaml from .input import UndefinedSubstitution, extract_inputs, substitute diff --git a/homeassistant/util/yaml/const.py b/homeassistant/util/yaml/const.py index 9d930b50fd6..811c7d149f7 100644 --- a/homeassistant/util/yaml/const.py +++ b/homeassistant/util/yaml/const.py @@ -1,2 +1,3 @@ """Constants.""" + SECRET_YAML = "secrets.yaml" diff --git a/homeassistant/util/yaml/dumper.py b/homeassistant/util/yaml/dumper.py index ec4700ef17e..61772b6989d 100644 --- a/homeassistant/util/yaml/dumper.py +++ b/homeassistant/util/yaml/dumper.py @@ -1,4 +1,5 @@ """Custom dumper and representers.""" + from collections import OrderedDict from typing import Any diff --git a/homeassistant/util/yaml/input.py b/homeassistant/util/yaml/input.py index ab5948db605..ff9b37f18f1 100644 --- a/homeassistant/util/yaml/input.py +++ b/homeassistant/util/yaml/input.py @@ -1,4 +1,5 @@ """Deal with YAML input.""" + from __future__ import annotations from typing import Any diff --git a/homeassistant/util/yaml/loader.py b/homeassistant/util/yaml/loader.py index 055284b1e18..28027c97211 100644 --- a/homeassistant/util/yaml/loader.py +++ b/homeassistant/util/yaml/loader.py @@ -1,4 +1,5 @@ """Custom loader.""" + from __future__ import annotations from collections.abc import Callable, Iterator @@ -292,8 +293,7 @@ def _add_reference( obj: list | NodeListClass, loader: LoaderType, node: yaml.nodes.Node, -) -> NodeListClass: - ... +) -> NodeListClass: ... @overload @@ -301,13 +301,13 @@ def _add_reference( obj: str | NodeStrClass, loader: LoaderType, node: yaml.nodes.Node, -) -> NodeStrClass: - ... +) -> NodeStrClass: ... @overload -def _add_reference(obj: _DictT, loader: LoaderType, node: yaml.nodes.Node) -> _DictT: - ... +def _add_reference( + obj: _DictT, loader: LoaderType, node: yaml.nodes.Node +) -> _DictT: ... def _add_reference( # type: ignore[no-untyped-def] @@ -318,7 +318,7 @@ def _add_reference( # type: ignore[no-untyped-def] obj = NodeListClass(obj) if isinstance(obj, str): obj = NodeStrClass(obj) - try: # noqa: SIM105 suppress is much slower + try: # suppress is much slower setattr(obj, "__config_file__", loader.get_name) setattr(obj, "__line__", node.start_mark.line + 1) except AttributeError: diff --git a/homeassistant/util/yaml/objects.py b/homeassistant/util/yaml/objects.py index 6aedc85cf60..70c229c1a2f 100644 --- a/homeassistant/util/yaml/objects.py +++ b/homeassistant/util/yaml/objects.py @@ -1,4 +1,5 @@ """Custom yaml object types.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/mypy.ini b/mypy.ini index 224508fb6bc..81f6f553eb6 100644 --- a/mypy.ini +++ b/mypy.ini @@ -3,7 +3,7 @@ # To update, run python3 -m script.hassfest -p mypy_config [mypy] -python_version = 3.11 +python_version = 3.12 plugins = pydantic.mypy show_error_codes = true follow_imports = silent @@ -2041,6 +2041,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.homeworks.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.http.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/pylint/plugins/hass_enforce_coordinator_module.py b/pylint/plugins/hass_enforce_coordinator_module.py index 3546632547b..924b69f1b86 100644 --- a/pylint/plugins/hass_enforce_coordinator_module.py +++ b/pylint/plugins/hass_enforce_coordinator_module.py @@ -1,4 +1,5 @@ """Plugin for checking if coordinator is in its own module.""" + from __future__ import annotations from astroid import nodes diff --git a/pylint/plugins/hass_enforce_sorted_platforms.py b/pylint/plugins/hass_enforce_sorted_platforms.py index f597a7a1191..aa6a6c16efa 100644 --- a/pylint/plugins/hass_enforce_sorted_platforms.py +++ b/pylint/plugins/hass_enforce_sorted_platforms.py @@ -1,4 +1,5 @@ """Plugin for checking sorted platforms list.""" + from __future__ import annotations from astroid import nodes diff --git a/pylint/plugins/hass_enforce_super_call.py b/pylint/plugins/hass_enforce_super_call.py index f2efb8bc8a2..b0f523aef72 100644 --- a/pylint/plugins/hass_enforce_super_call.py +++ b/pylint/plugins/hass_enforce_super_call.py @@ -1,4 +1,5 @@ """Plugin for checking super calls.""" + from __future__ import annotations from astroid import nodes diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index b2620dd3e1e..7d48fa4b2e3 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -1,4 +1,5 @@ """Plugin to enforce type hints on specific functions.""" + from __future__ import annotations from dataclasses import dataclass @@ -55,11 +56,12 @@ class TypeHintMatch: ) -@dataclass +@dataclass(kw_only=True) class ClassTypeHintMatch: """Class for pattern matching.""" base_class: str + exclude_base_classes: set[str] | None = None matches: list[TypeHintMatch] @@ -481,6 +483,7 @@ _CLASS_MATCH: dict[str, list[ClassTypeHintMatch]] = { "config_flow": [ ClassTypeHintMatch( base_class="FlowHandler", + exclude_base_classes={"ConfigEntryBaseFlow"}, matches=[ TypeHintMatch( function_name="async_step_*", @@ -504,56 +507,71 @@ _CLASS_MATCH: dict[str, list[ClassTypeHintMatch]] = { arg_types={ 1: "DhcpServiceInfo", }, - return_type="FlowResult", + return_type="ConfigFlowResult", ), TypeHintMatch( function_name="async_step_hassio", arg_types={ 1: "HassioServiceInfo", }, - return_type="FlowResult", + return_type="ConfigFlowResult", ), TypeHintMatch( function_name="async_step_homekit", arg_types={ 1: "ZeroconfServiceInfo", }, - return_type="FlowResult", + return_type="ConfigFlowResult", ), TypeHintMatch( function_name="async_step_mqtt", arg_types={ 1: "MqttServiceInfo", }, - return_type="FlowResult", + return_type="ConfigFlowResult", ), TypeHintMatch( function_name="async_step_reauth", arg_types={ 1: "Mapping[str, Any]", }, - return_type="FlowResult", + return_type="ConfigFlowResult", ), TypeHintMatch( function_name="async_step_ssdp", arg_types={ 1: "SsdpServiceInfo", }, - return_type="FlowResult", + return_type="ConfigFlowResult", ), TypeHintMatch( function_name="async_step_usb", arg_types={ 1: "UsbServiceInfo", }, - return_type="FlowResult", + return_type="ConfigFlowResult", ), TypeHintMatch( function_name="async_step_zeroconf", arg_types={ 1: "ZeroconfServiceInfo", }, - return_type="FlowResult", + return_type="ConfigFlowResult", + ), + TypeHintMatch( + function_name="async_step_*", + arg_types={}, + return_type="ConfigFlowResult", + ), + ], + ), + ClassTypeHintMatch( + base_class="OptionsFlow", + matches=[ + TypeHintMatch( + function_name="async_step_*", + arg_types={}, + return_type="ConfigFlowResult", ), ], ), @@ -1007,11 +1025,11 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { ), TypeHintMatch( function_name="current_humidity", - return_type=["int", None], + return_type=["float", None], ), TypeHintMatch( function_name="target_humidity", - return_type=["int", None], + return_type=["float", None], ), TypeHintMatch( function_name="hvac_mode", @@ -1153,11 +1171,11 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { ), TypeHintMatch( function_name="min_humidity", - return_type="int", + return_type="float", ), TypeHintMatch( function_name="max_humidity", - return_type="int", + return_type="float", ), ], ), @@ -1531,11 +1549,11 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { ), TypeHintMatch( function_name="min_humidity", - return_type=["int"], + return_type=["float"], ), TypeHintMatch( function_name="max_humidity", - return_type=["int"], + return_type=["float"], ), TypeHintMatch( function_name="mode", @@ -1547,7 +1565,7 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { ), TypeHintMatch( function_name="target_humidity", - return_type=["int", None], + return_type=["float", None], ), TypeHintMatch( function_name="set_humidity", @@ -2950,6 +2968,15 @@ def _is_valid_type( ): return True + # Special case for int in argument type + if ( + expected_type == "int" + and not in_return + and isinstance(node, nodes.Name) + and node.name in ("float", "int") + ): + return True + # Name occurs when a namespace is not used, eg. "HomeAssistant" if isinstance(node, nodes.Name) and node.name == expected_type: return True @@ -3032,10 +3059,7 @@ def _get_named_annotation( def _has_valid_annotations( annotations: list[nodes.NodeNG | None], ) -> bool: - for annotation in annotations: - if annotation is not None: - return True - return False + return any(annotation is not None for annotation in annotations) def _get_module_platform(module_name: str) -> str | None: @@ -3126,11 +3150,19 @@ class HassTypeHintChecker(BaseChecker): ancestor: nodes.ClassDef checked_class_methods: set[str] = set() ancestors = list(node.ancestors()) # cache result for inside loop - for class_matches in self._class_matchers: + for class_matcher in self._class_matchers: + skip_matcher = False + if exclude_base_classes := class_matcher.exclude_base_classes: + for ancestor in ancestors: + if ancestor.name in exclude_base_classes: + skip_matcher = True + break + if skip_matcher: + continue for ancestor in ancestors: - if ancestor.name == class_matches.base_class: + if ancestor.name == class_matcher.base_class: self._visit_class_functions( - node, class_matches.matches, checked_class_methods + node, class_matcher.matches, checked_class_methods ) def _visit_class_functions( diff --git a/pylint/plugins/hass_imports.py b/pylint/plugins/hass_imports.py index f28986b90e2..b8ec65e4460 100644 --- a/pylint/plugins/hass_imports.py +++ b/pylint/plugins/hass_imports.py @@ -1,4 +1,5 @@ """Plugin for checking imports.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/pylint/plugins/hass_inheritance.py b/pylint/plugins/hass_inheritance.py index 7ae24ec6e6d..e386986fa23 100644 --- a/pylint/plugins/hass_inheritance.py +++ b/pylint/plugins/hass_inheritance.py @@ -1,4 +1,5 @@ """Plugin to enforce type hints on specific functions.""" + from __future__ import annotations import re diff --git a/pylint/plugins/hass_logger.py b/pylint/plugins/hass_logger.py index e92fad2bdc0..6cbb72d4f78 100644 --- a/pylint/plugins/hass_logger.py +++ b/pylint/plugins/hass_logger.py @@ -1,4 +1,5 @@ """Plugin for logger invocations.""" + from __future__ import annotations from astroid import nodes diff --git a/pyproject.toml b/pyproject.toml index fbab6c50d43..e0f07fac6b6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.3.3" +version = "2024.4.0" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" @@ -18,10 +18,10 @@ classifiers = [ "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: OS Independent", - "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Topic :: Home Automation", ] -requires-python = ">=3.11.0" +requires-python = ">=3.12.0" dependencies = [ "aiohttp==3.9.3", "aiohttp_cors==0.7.0", @@ -35,9 +35,10 @@ dependencies = [ "bcrypt==4.1.2", "certifi>=2021.5.30", "ciso8601==2.3.1", + "fnv-hash-fast==0.5.0", # hass-nabucasa is imported by helpers which don't depend on the cloud # integration - "hass-nabucasa==0.78.0", + "hass-nabucasa==0.79.0", # When bumping httpx, please check the version pins of # httpcore, anyio, and h11 in gen_requirements_all "httpx==0.27.0", @@ -48,14 +49,16 @@ dependencies = [ "PyJWT==2.8.0", # PyJWT has loose dependency. We want the latest one. "cryptography==42.0.5", - # pyOpenSSL 23.2.0 is required to work with cryptography 41+ - "pyOpenSSL==24.0.0", + "Pillow==10.2.0", + "pyOpenSSL==24.1.0", "orjson==3.9.15", "packaging>=23.1", "pip>=21.3.1", + "psutil-home-assistant==0.0.1", "python-slugify==8.0.4", "PyYAML==6.0.1", "requests==2.31.0", + "SQLAlchemy==2.0.29", "typing-extensions>=4.10.0,<5.0", "ulid-transform==0.9.0", # Constrain urllib3 to ensure we deal with CVE-2020-26137 and CVE-2021-33503 @@ -87,7 +90,7 @@ include-package-data = true include = ["homeassistant*"] [tool.pylint.MAIN] -py-version = "3.11" +py-version = "3.12" ignore = [ "tests", ] @@ -227,7 +230,7 @@ disable = [ "duplicate-value", # F "eval-used", # S307 "exec-used", # S102 - # "expression-not-assigned", # B018, ruff catches new occurrences, needs more work + "expression-not-assigned", # B018 "f-string-without-interpolation", # F541 "forgotten-debug-statement", # T100 "format-string-without-interpolation", # F @@ -244,7 +247,7 @@ disable = [ "misplaced-future", # F404 "named-expr-without-context", # PLW0131 "nested-min-max", # PLW3301 - # "pointless-statement", # B018, ruff catches new occurrences, needs more work + "pointless-statement", # B018 "raise-missing-from", # B904 # "redefined-builtin", # A001, ruff is way more stricter, needs work "try-except-raise", # TRY302 @@ -272,6 +275,7 @@ disable = [ "unidiomatic-typecheck", # E721 "unnecessary-direct-lambda-call", # PLC3002 "unnecessary-lambda-assignment", # PLC3001 + "unnecessary-pass", # PIE790 "unneeded-not", # SIM208 "useless-import-alias", # PLC0414 "wrong-import-order", # I001 @@ -451,9 +455,9 @@ filterwarnings = [ # https://github.com/michaeldavie/env_canada/blob/v0.6.1/env_canada/ec_cache.py "ignore:Inheritance class CacheClientSession from ClientSession is discouraged:DeprecationWarning:env_canada.ec_cache", # https://github.com/allenporter/ical/pull/215 - # https://github.com/allenporter/ical/blob/6.1.1/ical/util.py#L20-L22 + # https://github.com/allenporter/ical/blob/7.0.3/ical/util.py#L20-L22 "ignore:datetime.*utcnow\\(\\) is deprecated and scheduled for removal:DeprecationWarning:ical.util", - # https://github.com/bachya/regenmaschine/blob/2024.01.0/regenmaschine/client.py#L51 + # https://github.com/bachya/regenmaschine/blob/2024.03.0/regenmaschine/client.py#L52 "ignore:ssl.TLSVersion.SSLv3 is deprecated:DeprecationWarning:regenmaschine.client", # -- Setuptools DeprecationWarnings @@ -464,49 +468,52 @@ filterwarnings = [ # -- tracked upstream / open PRs # https://github.com/certbot/certbot/issues/9828 - v2.8.0 "ignore:X509Extension support in pyOpenSSL is deprecated. You should use the APIs in cryptography:DeprecationWarning:acme.crypto_util", - # https://github.com/tschamm/boschshcpy/pull/39 - v0.2.88 - "ignore:pkg_resources is deprecated as an API:DeprecationWarning:boschshcpy.api", # https://github.com/influxdata/influxdb-client-python/issues/603 - v1.37.0 "ignore:datetime.*utcfromtimestamp\\(\\) is deprecated and scheduled for removal:DeprecationWarning:influxdb_client.client.write.point", # https://github.com/beetbox/mediafile/issues/67 - v0.12.0 "ignore:'imghdr' is deprecated and slated for removal in Python 3.13:DeprecationWarning:mediafile", - # https://github.com/PythonCharmers/python-future/issues/488 - v0.18.3 - "ignore:the imp module is deprecated in favour of importlib and slated for removal in Python 3.12:DeprecationWarning:future.standard_library", # https://github.com/foxel/python_ndms2_client/issues/6 - v0.1.3 # https://github.com/foxel/python_ndms2_client/pull/8 "ignore:'telnetlib' is deprecated and slated for removal in Python 3.13:DeprecationWarning:ndms2_client.connection", - # https://github.com/pytest-dev/pytest-cov/issues/557 - v4.1.0 - # Should resolve itself once pytest-xdist 4.0 is released and the option is removed - "ignore:The --rsyncdir command line argument and rsyncdirs config variable are deprecated:DeprecationWarning:xdist.plugin", # -- fixed, waiting for release / update - # https://github.com/mkmer/AIOAladdinConnect/commit/8851fff4473d80d70ac518db2533f0fbef63b69c - >0.1.58 + # https://github.com/mkmer/AIOAladdinConnect/commit/8851fff4473d80d70ac518db2533f0fbef63b69c - >=0.2.0 "ignore:module 'sre_constants' is deprecated:DeprecationWarning:AIOAladdinConnect", + # https://github.com/timmo001/aioazuredevops/commit/7c6a41bed45805396cd96e0696372c79b5416612 - >=1.4.0 + "ignore:\"(is|is not)\" with 'int' literal. Did you mean \"(==|!=)\"?:SyntaxWarning:.*aioazuredevops.client", # https://github.com/ludeeus/aiogithubapi/pull/208 - >=23.9.0 "ignore:datetime.*utcnow\\(\\) is deprecated and scheduled for removal:DeprecationWarning:aiogithubapi.namespaces.events", # https://github.com/bachya/aiopurpleair/pull/200 - >=2023.10.0 "ignore:datetime.*utcfromtimestamp\\(\\) is deprecated and scheduled for removal:DeprecationWarning:aiopurpleair.helpers.validators", - # https://github.com/kiorky/croniter/pull/52 - >=2.0.0 - "ignore:datetime.*utcfromtimestamp\\(\\) is deprecated and scheduled for removal:DeprecationWarning:croniter.croniter", + # https://github.com/rossengeorgiev/aprs-python/commit/5e79c810355fc2df4348581779815f2981493e3f - >=0.7.1 + "ignore:invalid escape sequence:SyntaxWarning:.*aprslib.parsing.weather", + # https://github.com/tschamm/boschshcpy/pull/39 - >=0.2.89 + "ignore:pkg_resources is deprecated as an API:DeprecationWarning:boschshcpy.api", + # https://github.com/DataDog/datadogpy/pull/290 - >=0.23.0 + "ignore:invalid escape sequence:SyntaxWarning:.*datadog.dogstatsd.base", # https://github.com/fwestenberg/devialet/pull/6 - >1.4.5 "ignore:datetime.*utcnow\\(\\) is deprecated and scheduled for removal:DeprecationWarning:devialet.devialet_api", # https://github.com/jaraco/jaraco.abode/commit/9e3e789efc96cddcaa15f920686bbeb79a7469e0 - update jaraco.abode to >=5.1.0 "ignore:`jaraco.functools.call_aside` is deprecated, use `jaraco.functools.invoke` instead:DeprecationWarning:jaraco.abode.helpers.timeline", + # https://github.com/majuss/lupupy/pull/15 - >0.3.2 + "ignore:\"is not\" with 'str' literal. Did you mean \"!=\"?:SyntaxWarning:.*lupupy.devices.alarm", # https://github.com/nextcord/nextcord/pull/1095 - >2.6.1 "ignore:pkg_resources is deprecated as an API:DeprecationWarning:nextcord.health_check", + # https://github.com/timmo001/ovoenergy/pull/68 - >=1.3.0 + "ignore:\"is not\" with 'int' literal. Did you mean \"!=\"?:SyntaxWarning:.*ovoenergy.ovoenergy", # https://github.com/eclipse/paho.mqtt.python/issues/653 - >=2.0.0 # https://github.com/eclipse/paho.mqtt.python/pull/665 "ignore:ssl.PROTOCOL_TLS is deprecated:DeprecationWarning:paho.mqtt.client", - # https://github.com/bachya/pytile/pull/280 - >=2023.10.0 - "ignore:datetime.*utcfromtimestamp\\(\\) is deprecated and scheduled for removal:DeprecationWarning:pytile.tile", - # https://github.com/rytilahti/python-miio/pull/1809 - >0.5.12 + # https://github.com/rytilahti/python-miio/pull/1809 - >=0.6.0.dev0 "ignore:datetime.*utcnow\\(\\) is deprecated and scheduled for removal:DeprecationWarning:miio.protocol", "ignore:datetime.*utcnow\\(\\) is deprecated and scheduled for removal:DeprecationWarning:miio.miioprotocol", # https://github.com/hunterjm/python-onvif-zeep-async/pull/51 - >3.1.12 "ignore:datetime.*utcnow\\(\\) is deprecated and scheduled for removal:DeprecationWarning:onvif.client", - # Fixed upstream in python-telegram-bot - >=20.0 - "ignore:python-telegram-bot is using upstream urllib3:UserWarning:telegram.utils.request", - # https://github.com/xeniter/romy/pull/1 - >0.0.7 + # https://github.com/okunishinishi/python-stringcase/commit/6a5c5bbd3fe5337862abc7fd0853a0f36e18b2e1 - >1.2.0 + "ignore:invalid escape sequence:SyntaxWarning:.*stringcase", + # https://github.com/pyudev/pyudev/pull/466 - >=0.24.0 + "ignore:invalid escape sequence:SyntaxWarning:.*pyudev.monitor", + # https://github.com/xeniter/romy/pull/1 - >=0.0.8 "ignore:with timeout\\(\\) is deprecated, use async with timeout\\(\\) instead:DeprecationWarning:romy.utils", # https://github.com/grahamwetzler/smart-meter-texas/pull/143 - >0.5.3 "ignore:ssl.OP_NO_SSL\\*/ssl.OP_NO_TLS\\* options are deprecated:DeprecationWarning:smart_meter_texas", @@ -530,20 +537,35 @@ filterwarnings = [ "ignore:setDaemon\\(\\) is deprecated, set the daemon attribute instead:DeprecationWarning:pylutron", # Wrong stacklevel # https://bugs.launchpad.net/beautifulsoup/+bug/2034451 - "ignore:It looks like you're parsing an XML document using an HTML parser:UserWarning:bs4.builder", + "ignore:It looks like you're parsing an XML document using an HTML parser:UserWarning:html.parser", # New in aiohttp - v3.9.0 "ignore:It is recommended to use web.AppKey instances for keys:UserWarning:(homeassistant|tests|aiohttp_cors)", + # - SyntaxWarnings + # https://pypi.org/project/aprslib/ - v0.7.2 - 2022-07-10 + "ignore:invalid escape sequence:SyntaxWarning:.*aprslib.parsing.common", + # https://pypi.org/project/pyblackbird/ - v0.6 - 2023-03-15 + # https://github.com/koolsb/pyblackbird/pull/9 -> closed + "ignore:invalid escape sequence:SyntaxWarning:.*pyblackbird", + # https://pypi.org/project/pybotvac/ - v0.0.24 - 2023-01-02 + # https://github.com/stianaske/pybotvac/pull/81 -> closed + "ignore:invalid escape sequence:SyntaxWarning:.*pybotvac.robot", + # https://github.com/pkkid/python-plexapi/pull/1244 - v4.15.10 -> new issue same file + "ignore:invalid escape sequence:SyntaxWarning:.*plexapi.base", + # https://pypi.org/project/pyws66i/ - v1.1 - 2022-04-05 + "ignore:invalid escape sequence:SyntaxWarning:.*pyws66i", + # https://pypi.org/project/sleekxmppfs/ - v1.4.1 - 2022-08-18 + "ignore:invalid escape sequence:SyntaxWarning:.*sleekxmppfs.thirdparty.mini_dateutil", # -- unmaintained projects, last release about 2+ years # https://pypi.org/project/agent-py/ - v0.0.23 - 2020-06-04 "ignore:with timeout\\(\\) is deprecated:DeprecationWarning:agent.a", # https://pypi.org/project/aiomodernforms/ - v0.1.8 - 2021-06-27 "ignore:with timeout\\(\\) is deprecated:DeprecationWarning:aiomodernforms.modernforms", + # https://pypi.org/project/alarmdecoder/ - v1.13.11 - 2021-06-01 + "ignore:invalid escape sequence:SyntaxWarning:.*alarmdecoder", # https://pypi.org/project/directv/ - v0.4.0 - 2020-09-12 "ignore:with timeout\\(\\) is deprecated:DeprecationWarning:directv.directv", "ignore:datetime.*utcnow\\(\\) is deprecated and scheduled for removal:DeprecationWarning:directv.models", - # https://pypi.org/project/emulated-roku/ - v0.2.1 - 2020-01-23 (archived) - "ignore:loop argument is deprecated:DeprecationWarning:emulated_roku", # https://pypi.org/project/foobot_async/ - v1.0.0 - 2020-11-24 "ignore:with timeout\\(\\) is deprecated:DeprecationWarning:foobot_async", # https://pypi.org/project/influxdb/ - v5.3.1 - 2020-11-11 (archived) @@ -558,10 +580,23 @@ filterwarnings = [ "ignore:ssl.PROTOCOL_TLS is deprecated:DeprecationWarning:lomond.session", # https://pypi.org/project/oauth2client/ - v4.1.3 - 2018-09-07 (archived) "ignore:datetime.*utcnow\\(\\) is deprecated and scheduled for removal:DeprecationWarning:oauth2client.client", + # https://pypi.org/project/opuslib/ - v3.0.1 - 2018-01-16 + "ignore:\"is not\" with 'int' literal. Did you mean \"!=\"?:SyntaxWarning:.*opuslib.api.decoder", # https://pypi.org/project/passlib/ - v1.7.4 - 2020-10-08 "ignore:'crypt' is deprecated and slated for removal in Python 3.13:DeprecationWarning:passlib.utils", + # https://pypi.org/project/plumlightpad/ - v0.0.11 - 2018-10-16 + "ignore:invalid escape sequence:SyntaxWarning:.*plumlightpad.plumdiscovery", + "ignore:\"is\" with 'int' literal. Did you mean \"==\"?:SyntaxWarning:.*plumlightpad.(lightpad|logicalload)", + # https://pypi.org/project/pure-python-adb/ - v0.3.0.dev0 - 2020-08-05 + "ignore:invalid escape sequence:SyntaxWarning:.*ppadb", + # https://pypi.org/project/pydub/ - v0.25.1 - 2021-03-10 + "ignore:invalid escape sequence:SyntaxWarning:.*pydub.utils", + # https://pypi.org/project/pyiss/ - v1.0.1 - 2016-12-19 + "ignore:\"is\" with 'int' literal. Did you mean \"==\"?:SyntaxWarning:.*pyiss", # https://pypi.org/project/PyMetEireann/ - v2021.8.0 - 2021-08-16 "ignore:datetime.*utcnow\\(\\) is deprecated and scheduled for removal:DeprecationWarning:meteireann", + # https://pypi.org/project/PyPasser/ - v0.0.5 - 2021-10-21 + "ignore:invalid escape sequence:SyntaxWarning:.*pypasser.utils", # https://pypi.org/project/pyqwikswitch/ - v0.94 - 2019-08-19 "ignore:client.loop property is deprecated:DeprecationWarning:pyqwikswitch.async_", "ignore:with timeout\\(\\) is deprecated:DeprecationWarning:pyqwikswitch.async_", @@ -571,6 +606,8 @@ filterwarnings = [ "ignore:defusedxml.cElementTree is deprecated, import from defusedxml.ElementTree instead:DeprecationWarning:rxv.ssdp", # https://pypi.org/project/vilfo-api-client/ - v0.4.1 - 2021-11-06 "ignore:Function 'semver.compare' is deprecated. Deprecated since version 3.0.0:PendingDeprecationWarning:.*vilfo.client", + # https://pypi.org/project/vobject/ - v0.9.6.1 - 2018-07-18 + "ignore:invalid escape sequence:SyntaxWarning:.*vobject.base", # https://pypi.org/project/webrtcvad/ - v2.0.10 - 2017-01-08 "ignore:pkg_resources is deprecated as an API:DeprecationWarning:webrtcvad", ] @@ -578,10 +615,14 @@ filterwarnings = [ [tool.ruff.lint] select = [ "B002", # Python does not support the unary prefix increment + "B005", # Using .strip() with multi-character strings is misleading "B007", # Loop control variable {name} not used within loop body "B014", # Exception handler with duplicate exception + "B015", # Pointless comparison. Did you mean to assign a value? Otherwise, prepend assert or remove it. + "B018", # Found useless attribute access. Either assign it to a variable or remove it. "B023", # Function definition does not bind loop variable {name} "B026", # Star-arg unpacking after a keyword argument is strongly discouraged + "B032", # Possible unintentional type annotation (using :). Did you mean to assign (using =)? "B904", # Use raise from to specify exception cause "C", # complexity "COM818", # Trailing comma on bare tuple prohibited @@ -592,18 +633,21 @@ select = [ "F", # pyflakes/autoflake "G", # flake8-logging-format "I", # isort + "ISC", # flake8-implicit-str-concat "ICN001", # import concentions; {name} should be imported as {asname} + "LOG", # flake8-logging "N804", # First argument of a class method should be named cls "N805", # First argument of a method should be named self "N815", # Variable {name} in class scope should not be mixedCase + "PERF", # Perflint "PGH004", # Use specific rule codes when using noqa - "PLC0414", # Useless import alias. Import alias does not rename original package. - "PLC", # pylint - "PLE", # pylint - "PLR", # pylint - "PLW", # pylint - "Q000", # Double quotes found but single quotes preferred + "PIE", # flake8-pie + "PL", # pylint + "PT", # flake8-pytest-style + "RSE", # flake8-raise + "RUF005", # Consider iterable unpacking instead of concatenation "RUF006", # Store a reference to the return value of asyncio.create_task + # "RUF100", # Unused `noqa` directive; temporarily every now and then to clean them up "S102", # Use of exec detected "S103", # bad-file-permissions "S108", # hardcoded-temp-file @@ -622,15 +666,7 @@ select = [ "S604", # call-with-shell-equals-true "S608", # hardcoded-sql-expression "S609", # unix-command-wildcard-injection - "SIM105", # Use contextlib.suppress({exception}) instead of try-except-pass - "SIM114", # Combine if branches using logical or operator - "SIM117", # Merge with-statements that use the same scope - "SIM118", # Use {key} in {dict} instead of {key} in {dict}.keys() - "SIM201", # Use {left} != {right} instead of not {left} == {right} - "SIM208", # Use {expr} instead of not (not {expr}) - "SIM212", # Use {a} if {a} else {b} instead of {b} if not {a} else {a} - "SIM300", # Yoda conditions. Use 'age == 42' instead of '42 == age'. - "SIM401", # Use get from dict with default instead of an if block + "SIM", # flake8-simplify "T100", # Trace found: {name} used "T20", # flake8-print "TID251", # Banned imports @@ -649,23 +685,26 @@ ignore = [ "E501", # line too long "E731", # do not assign a lambda expression, use a def - # Ignore ignored, as the rule is now back in preview/nursery, which cannot - # be ignored anymore without warnings. - # https://github.com/astral-sh/ruff/issues/7491 - # "PLC1901", # Lots of false positives - - # False positives https://github.com/astral-sh/ruff/issues/5386 - "PLC0208", # Use a sequence type instead of a `set` when iterating over values + "PLC1901", # {existing} can be simplified to {replacement} as an empty string is falsey; too many false positives "PLR0911", # Too many return statements ({returns} > {max_returns}) "PLR0912", # Too many branches ({branches} > {max_branches}) "PLR0913", # Too many arguments to function call ({c_args} > {max_args}) "PLR0915", # Too many statements ({statements} > {max_statements}) "PLR2004", # Magic value used in comparison, consider replacing {value} with a constant variable "PLW2901", # Outer {outer_kind} variable {name} overwritten by inner {inner_kind} target + "PT004", # Fixture {fixture} does not return anything, add leading underscore + "PT011", # pytest.raises({exception}) is too broad, set the `match` parameter or use a more specific exception + "PT012", # `pytest.raises()` block should contain a single simple statement + "PT018", # Assertion should be broken down into multiple parts + "SIM102", # Use a single if statement instead of nested if statements + "SIM108", # Use ternary operator {contents} instead of if-else-block + "SIM115", # Use context handler for opening files "UP006", # keep type annotation style as is "UP007", # keep type annotation style as is # Ignored due to performance: https://github.com/charliermarsh/ruff/issues/2923 "UP038", # Use `X | Y` in `isinstance` call instead of `(X, Y)` + # Ignored due to incompatible with mypy: https://github.com/python/mypy/issues/15238 + "UP040", # Checks for use of TypeAlias annotation for declaring type aliases. # May conflict with the formatter, https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules "W191", @@ -674,22 +713,22 @@ ignore = [ "E117", "D206", "D300", - "Q000", - "Q001", - "Q002", - "Q003", + "Q", "COM812", "COM819", "ISC001", - "ISC002", # Disabled because ruff does not understand type of __all__ generated by a function "PLE0605", + + # temporarily disabled + "PT019" ] [tool.ruff.lint.flake8-import-conventions.extend-aliases] voluptuous = "vol" "homeassistant.helpers.area_registry" = "ar" +"homeassistant.helpers.category_registry" = "cr" "homeassistant.helpers.config_validation" = "cv" "homeassistant.helpers.device_registry" = "dr" "homeassistant.helpers.entity_registry" = "er" @@ -700,6 +739,7 @@ voluptuous = "vol" [tool.ruff.lint.flake8-pytest-style] fixture-parentheses = false +mark-parentheses = false [tool.ruff.lint.flake8-tidy-imports.banned-api] "async_timeout".msg = "use asyncio.timeout instead" diff --git a/requirements.txt b/requirements.txt index 23ab0b08567..1dd9b1811d3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,7 +15,8 @@ awesomeversion==24.2.0 bcrypt==4.1.2 certifi>=2021.5.30 ciso8601==2.3.1 -hass-nabucasa==0.78.0 +fnv-hash-fast==0.5.0 +hass-nabucasa==0.79.0 httpx==0.27.0 home-assistant-bluetooth==1.12.0 ifaddr==0.2.0 @@ -23,13 +24,16 @@ Jinja2==3.1.3 lru-dict==1.3.0 PyJWT==2.8.0 cryptography==42.0.5 -pyOpenSSL==24.0.0 +Pillow==10.2.0 +pyOpenSSL==24.1.0 orjson==3.9.15 packaging>=23.1 pip>=21.3.1 +psutil-home-assistant==0.0.1 python-slugify==8.0.4 PyYAML==6.0.1 requests==2.31.0 +SQLAlchemy==2.0.29 typing-extensions>=4.10.0,<5.0 ulid-transform==0.9.0 urllib3>=1.26.5,<2 diff --git a/requirements_all.txt b/requirements_all.txt index d65690c3a42..76dc587d6b9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -31,7 +31,7 @@ HAP-python==4.9.1 HATasmota==0.8.0 # homeassistant.components.mastodon -Mastodon.py==1.5.1 +Mastodon.py==1.8.1 # homeassistant.components.doods # homeassistant.components.generic @@ -45,7 +45,7 @@ Mastodon.py==1.5.1 Pillow==10.2.0 # homeassistant.components.plex -PlexAPI==4.15.10 +PlexAPI==4.15.11 # homeassistant.components.progettihwsw ProgettiHWSW==0.1.3 @@ -73,7 +73,7 @@ PyMetEireann==2021.8.0 # homeassistant.components.met # homeassistant.components.norway_air -PyMetno==0.11.0 +PyMetno==0.12.0 # homeassistant.components.keymitt_ble PyMicroBot==0.0.17 @@ -92,9 +92,6 @@ PyQRCode==1.2.1 # homeassistant.components.rmvtransport PyRMVtransport==0.3.3 -# homeassistant.components.telegram_bot -PySocks==1.7.1 - # homeassistant.components.switchbot PySwitchbot==0.45.0 @@ -128,7 +125,7 @@ RtmAPI==0.7.2 # homeassistant.components.recorder # homeassistant.components.sql -SQLAlchemy==2.0.27 +SQLAlchemy==2.0.29 # homeassistant.components.tami4 Tami4EdgeAPI==2.1 @@ -188,7 +185,7 @@ aio-georss-gdacs==0.9 aioairq==0.3.2 # homeassistant.components.airzone_cloud -aioairzone-cloud==0.4.5 +aioairzone-cloud==0.4.6 # homeassistant.components.airzone aioairzone==0.7.6 @@ -200,13 +197,13 @@ aioambient==2024.01.0 aioapcaccess==0.4.2 # homeassistant.components.aseko_pool_live -aioaseko==0.0.2 +aioaseko==0.1.1 # homeassistant.components.asuswrt aioasuswrt==1.4.0 # homeassistant.components.husqvarna_automower -aioautomower==2024.3.3 +aioautomower==2024.3.4 # homeassistant.components.azure_devops aioazuredevops==1.3.5 @@ -221,13 +218,13 @@ aiobotocore==2.9.1 aiocomelit==0.9.0 # homeassistant.components.dhcp -aiodhcpwatcher==0.8.2 +aiodhcpwatcher==1.0.0 # homeassistant.components.dhcp -aiodiscover==1.6.1 +aiodiscover==2.0.0 # homeassistant.components.dnsip -aiodns==3.0.0 +aiodns==3.1.1 # homeassistant.components.eafm aioeafm==0.1.2 @@ -245,7 +242,7 @@ aioelectricitymaps==0.4.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==23.0.0 +aioesphomeapi==23.2.0 # homeassistant.components.flo aioflo==2021.11.0 @@ -317,8 +314,11 @@ aionanoleaf==0.2.1 # homeassistant.components.notion aionotion==2024.03.0 +# homeassistant.components.nut +aionut==4.3.2 + # homeassistant.components.oncue -aiooncue==0.3.5 +aiooncue==0.3.7 # homeassistant.components.openexchangerates aioopenexchangerates==0.4.0 @@ -336,7 +336,7 @@ aiopulse==0.4.4 aiopurpleair==2022.12.1 # homeassistant.components.hunterdouglas_powerview -aiopvapi==3.0.2 +aiopvapi==3.1.1 # homeassistant.components.pvpc_hourly_pricing aiopvpc==4.2.2 @@ -350,7 +350,7 @@ aiopyarr==23.4.0 aioqsw==0.3.5 # homeassistant.components.rainforest_raven -aioraven==0.5.2 +aioraven==0.5.3 # homeassistant.components.recollect_waste aiorecollect==2023.09.0 @@ -368,13 +368,13 @@ aioruuvigateway==0.1.0 aiosenz==1.0.0 # homeassistant.components.shelly -aioshelly==8.1.1 +aioshelly==8.2.0 # homeassistant.components.skybell aioskybell==22.7.0 # homeassistant.components.slimproto -aioslimproto==2.3.3 +aioslimproto==3.0.0 # homeassistant.components.steamist aiosteamist==0.3.2 @@ -392,7 +392,7 @@ aiotankerkoenig==0.4.1 aiotractive==0.5.6 # homeassistant.components.unifi -aiounifi==72 +aiounifi==74 # homeassistant.components.vlc_telnet aiovlc==0.1.0 @@ -458,7 +458,7 @@ anthemav==1.4.1 apple_weatherkit==1.1.2 # homeassistant.components.apprise -apprise==1.7.2 +apprise==1.7.4 # homeassistant.components.aprs aprslib==0.7.0 @@ -487,7 +487,7 @@ asterisk_mbox==0.5.0 # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.38.2 +async-upnp-client==0.38.3 # homeassistant.components.keyboard_remote asyncinotify==4.0.2 @@ -514,7 +514,7 @@ aurorapy==0.2.7 # avion==0.10 # homeassistant.components.axis -axis==58 +axis==60 # homeassistant.components.azure_event_hub azure-eventhub==5.11.1 @@ -571,7 +571,7 @@ blinkpy==0.22.6 blockchain==1.4.4 # homeassistant.components.blue_current -bluecurrent-api==1.0.6 +bluecurrent-api==1.2.3 # homeassistant.components.bluemaestro bluemaestro-ble==0.2.3 @@ -584,7 +584,7 @@ bluemaestro-ble==0.2.3 bluetooth-adapters==0.18.0 # homeassistant.components.bluetooth -bluetooth-auto-recovery==1.3.0 +bluetooth-auto-recovery==1.4.0 # homeassistant.components.bluetooth # homeassistant.components.ld2410_ble @@ -603,7 +603,7 @@ boschshcpy==0.2.82 boto3==1.33.13 # homeassistant.components.bring -bring-api==0.5.6 +bring-api==0.5.7 # homeassistant.components.broadlink broadlink==0.18.3 @@ -621,7 +621,7 @@ brunt==1.2.0 bt-proximity==0.2.1 # homeassistant.components.bthome -bthome-ble==3.8.0 +bthome-ble==3.8.1 # homeassistant.components.bt_home_hub_5 bthomehub5-devicelist==0.1.1 @@ -864,10 +864,10 @@ fivem-api==0.1.2 fixerio==1.0.0a0 # homeassistant.components.fjaraskupan -fjaraskupan==2.2.0 +fjaraskupan==2.3.0 # homeassistant.components.flexit_bacnet -flexit_bacnet==2.1.0 +flexit_bacnet==2.2.1 # homeassistant.components.flipr flipr-api==1.5.1 @@ -898,6 +898,9 @@ freesms==0.2.0 # homeassistant.components.fritzbox_callmonitor fritzconnection[qr]==1.13.2 +# homeassistant.components.fyta +fyta_cli==0.3.3 + # homeassistant.components.google_translate gTTS==2.2.4 @@ -905,10 +908,10 @@ gTTS==2.2.4 gardena-bluetooth==1.4.1 # homeassistant.components.google_assistant_sdk -gassist-text==0.0.10 +gassist-text==0.0.11 # homeassistant.components.google -gcal-sync==6.0.3 +gcal-sync==6.0.4 # homeassistant.components.geniushub geniushub-client==0.7.1 @@ -1019,7 +1022,7 @@ h2==4.1.0 ha-av==10.1.1 # homeassistant.components.ffmpeg -ha-ffmpeg==3.1.0 +ha-ffmpeg==3.2.0 # homeassistant.components.iotawatt ha-iotawattpy==0.1.1 @@ -1034,7 +1037,7 @@ habitipy==0.2.0 habluetooth==2.4.2 # homeassistant.components.cloud -hass-nabucasa==0.78.0 +hass-nabucasa==0.79.0 # homeassistant.components.splunk hass-splunk==0.1.1 @@ -1071,13 +1074,13 @@ hole==0.8.0 # homeassistant.components.holiday # homeassistant.components.workday -holidays==0.44 +holidays==0.46 # homeassistant.components.frontend -home-assistant-frontend==20240307.0 +home-assistant-frontend==20240403.1 # homeassistant.components.conversation -home-assistant-intents==2024.3.12 +home-assistant-intents==2024.4.3 # homeassistant.components.home_connect homeconnect==0.7.2 @@ -1156,9 +1159,6 @@ iperf3==0.1.11 # homeassistant.components.gogogate2 ismartgate==5.0.1 -# homeassistant.components.file_upload -janus==1.0.0 - # homeassistant.components.abode jaraco.abode==3.3.0 @@ -1327,6 +1327,9 @@ mopeka-iot-ble==0.7.0 # homeassistant.components.motion_blinds motionblinds==0.6.23 +# homeassistant.components.motionblinds_ble +motionblindsble==0.0.9 + # homeassistant.components.motioneye motioneye-client==0.3.14 @@ -1409,11 +1412,10 @@ nsw-fuel-api-client==1.1.0 nuheat==1.0.1 # homeassistant.components.numato -numato-gpio==0.12.0 +numato-gpio==0.13.0 # homeassistant.components.compensation # homeassistant.components.iqvia -# homeassistant.components.opencv # homeassistant.components.stream # homeassistant.components.tensorflow # homeassistant.components.trend @@ -1434,11 +1436,14 @@ odp-amsterdam==6.0.1 # homeassistant.components.oem oemthermostat==1.1.1 +# homeassistant.components.ollama +ollama-hass==0.1.7 + # homeassistant.components.omnilogic omnilogic==0.4.5 # homeassistant.components.ondilo_ico -ondilo==0.2.0 +ondilo==0.4.0 # homeassistant.components.onkyo onkyo-eiscp==1.2.7 @@ -1455,9 +1460,6 @@ open-meteo==0.3.1 # homeassistant.components.openai_conversation openai==1.3.8 -# homeassistant.components.opencv -# opencv-python-headless==4.6.0.66 - # homeassistant.components.openerz openerz-api==0.3.0 @@ -1480,7 +1482,7 @@ openwrt-luci-rpc==1.1.17 openwrt-ubus-rpc==0.0.2 # homeassistant.components.opower -opower==0.3.1 +opower==0.4.2 # homeassistant.components.oralb oralb-ble==0.17.6 @@ -1545,7 +1547,7 @@ plexauth==0.0.6 plexwebsocket==0.0.14 # homeassistant.components.plugwise -plugwise==0.36.3 +plugwise==0.37.1 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 @@ -1560,7 +1562,7 @@ poolsense==0.0.8 praw==7.5.0 # homeassistant.components.islamic_prayer_times -prayer-times-calculator==0.0.10 +prayer-times-calculator==0.0.12 # homeassistant.components.proliphix proliphix==0.4.1 @@ -1646,7 +1648,7 @@ pyCEC==0.5.2 pyControl4==1.1.0 # homeassistant.components.duotecno -pyDuotecno==2024.1.2 +pyDuotecno==2024.3.2 # homeassistant.components.electrasmart pyElectra==1.2.0 @@ -1794,7 +1796,7 @@ pyedimax==0.2.1 pyefergy==22.1.1 # homeassistant.components.enphase_envoy -pyenphase==1.19.2 +pyenphase==1.20.1 # homeassistant.components.envisalink pyenvisalink==4.6 @@ -1989,14 +1991,11 @@ pynetgear==0.10.10 pynetio==0.1.9.1 # homeassistant.components.nobo_hub -pynobo==1.6.0 +pynobo==1.8.0 # homeassistant.components.nuki pynuki==1.6.3 -# homeassistant.components.nut -pynut2==2.1.2 - # homeassistant.components.nws pynws==1.6.0 @@ -2036,7 +2035,7 @@ pyotgw==2.1.3 pyotp==2.8.0 # homeassistant.components.overkiz -pyoverkiz==1.13.8 +pyoverkiz==1.13.9 # homeassistant.components.openweathermap pyowm==3.2.0 @@ -2066,7 +2065,7 @@ pyprof2calltree==1.4.5 pyprosegur==0.0.9 # homeassistant.components.prusalink -pyprusalink==2.0.0 +pyprusalink==2.1.1 # homeassistant.components.ps4 pyps4-2ndscreen==1.3.1 @@ -2090,7 +2089,7 @@ pyrecswitch==1.0.2 pyrepetierng==0.1.0 # homeassistant.components.risco -pyrisco==0.5.10 +pyrisco==0.6.0 # homeassistant.components.rituals_perfume_genie pyrituals==0.0.6 @@ -2233,7 +2232,7 @@ python-gitlab==1.6.0 python-homeassistant-analytics==0.6.0 # homeassistant.components.homewizard -python-homewizard-energy==4.3.1 +python-homewizard-energy==v5.0.0 # homeassistant.components.hp_ilo python-hpilo==4.4.3 @@ -2303,7 +2302,7 @@ python-tado==0.17.4 python-technove==1.2.2 # homeassistant.components.telegram_bot -python-telegram-bot==13.1 +python-telegram-bot[socks]==21.0.1 # homeassistant.components.vlc python-vlc==3.0.18122 @@ -2312,7 +2311,7 @@ python-vlc==3.0.18122 pythonegardia==1.0.52 # homeassistant.components.tile -pytile==2023.04.0 +pytile==2023.12.0 # homeassistant.components.tomorrowio pytomorrowio==0.3.6 @@ -2340,7 +2339,7 @@ pytrydan==0.4.0 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==5.0.2 +pyunifiprotect==5.1.2 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 @@ -2367,7 +2366,7 @@ pyvlx==0.2.21 pyvolumio==0.1.5 # homeassistant.components.waze_travel_time -pywaze==0.5.1 +pywaze==1.0.0 # homeassistant.components.weatherflow pyweatherflowudp==1.4.5 @@ -2409,7 +2408,7 @@ qnapstats==0.4.0 quantum-gateway==0.0.8 # homeassistant.components.radio_browser -radios==0.2.0 +radios==0.3.1 # homeassistant.components.radiotherm radiotherm==2.1.0 @@ -2427,7 +2426,7 @@ raspyrfm-client==1.2.8 refoss-ha==1.2.0 # homeassistant.components.rainmachine -regenmaschine==2024.01.0 +regenmaschine==2024.03.0 # homeassistant.components.renault renault-api==0.2.1 @@ -2445,7 +2444,7 @@ rfk101py==0.0.1 rflink==0.0.66 # homeassistant.components.ring -ring-doorbell[listen]==0.8.7 +ring-doorbell[listen]==0.8.9 # homeassistant.components.fleetgo ritassist==0.9.2 @@ -2463,7 +2462,7 @@ rokuecp==0.19.2 romy==0.0.7 # homeassistant.components.roomba -roombapy==1.6.13 +roombapy==1.8.1 # homeassistant.components.roon roonapi==0.1.6 @@ -2563,7 +2562,7 @@ smart-meter-texas==0.4.7 smhi-pkg==1.0.16 # homeassistant.components.snapcast -snapcast==2.3.3 +snapcast==2.3.6 # homeassistant.components.sonos soco==0.30.2 @@ -2638,7 +2637,7 @@ subarulink==0.7.9 sunwatcher==0.2.1 # homeassistant.components.sunweg -sunweg==2.1.0 +sunweg==2.1.1 # homeassistant.components.surepetcare surepy==0.9.0 @@ -2653,7 +2652,7 @@ switchbot-api==2.0.0 synology-srm==0.2.0 # homeassistant.components.system_bridge -systembridgeconnector==3.10.0 +systembridgeconnector==4.0.3 # homeassistant.components.tailscale tailscale==0.6.0 @@ -2683,7 +2682,7 @@ temperusb==1.6.1 # tensorflow==2.5.0 # homeassistant.components.teslemetry -tesla-fleet-api==0.4.6 +tesla-fleet-api==0.4.9 # homeassistant.components.powerwall tesla-powerwall==0.5.1 @@ -2722,7 +2721,7 @@ tmb==0.0.4 todoist-api-python==2.1.2 # homeassistant.components.tolo -tololib==0.1.0b4 +tololib==1.1.0 # homeassistant.components.toon toonapi==0.3.0 @@ -2734,7 +2733,7 @@ total-connect-client==2023.2 tp-connected==0.0.4 # homeassistant.components.tplink_omada -tplink-omada-client==1.3.11 +tplink-omada-client==1.3.12 # homeassistant.components.transmission transmission-rpc==7.0.3 @@ -2790,19 +2789,19 @@ uvcclient==0.11.0 vacuum-map-parser-roborock==0.1.1 # homeassistant.components.vallox -vallox-websocket-api==5.1.0 +vallox-websocket-api==5.1.1 # homeassistant.components.rdw vehicle==2.2.1 # homeassistant.components.velbus -velbus-aio==2023.12.0 +velbus-aio==2024.4.0 # homeassistant.components.venstar venstarcolortouch==0.19 # homeassistant.components.vilfo -vilfo-api-client==0.4.1 +vilfo-api-client==0.5.0 # homeassistant.components.voip voip-utils==0.1.0 @@ -2839,16 +2838,16 @@ watchdog==2.3.1 waterfurnace==1.1.0 # homeassistant.components.weatherflow_cloud -weatherflow4py==0.1.17 +weatherflow4py==0.2.17 # homeassistant.components.webmin -webmin-xmlrpc==0.0.1 +webmin-xmlrpc==0.0.2 # homeassistant.components.assist_pipeline webrtc-noise-gain==1.2.3 # homeassistant.components.whirlpool -whirlpool-sixth-sense==0.18.4 +whirlpool-sixth-sense==0.18.7 # homeassistant.components.whois whois==0.9.27 @@ -2872,7 +2871,7 @@ wyoming==1.5.3 xbox-webapi==2.0.11 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.25.2 +xiaomi-ble==0.28.0 # homeassistant.components.knx xknx==2.12.2 @@ -2899,7 +2898,7 @@ yalesmartalarmclient==0.3.9 yalexs-ble==2.4.2 # homeassistant.components.august -yalexs==1.11.4 +yalexs==2.0.0 # homeassistant.components.yeelight yeelight==0.7.14 @@ -2908,7 +2907,7 @@ yeelight==0.7.14 yeelightsunflower==0.0.10 # homeassistant.components.yolink -yolink-api==0.3.7 +yolink-api==0.4.1 # homeassistant.components.youless youless-api==1.0.1 @@ -2917,7 +2916,7 @@ youless-api==1.0.1 youtubeaio==1.1.5 # homeassistant.components.media_extractor -yt-dlp==2023.11.16 +yt-dlp==2024.03.10 # homeassistant.components.zamg zamg==0.3.6 @@ -2926,13 +2925,13 @@ zamg==0.3.6 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.131.0 +zeroconf==0.132.0 # homeassistant.components.zeversolar zeversolar==0.3.1 # homeassistant.components.zha -zha-quirks==0.0.112 +zha-quirks==0.0.113 # homeassistant.components.zhong_hong zhong-hong-hvac==1.0.12 @@ -2953,7 +2952,7 @@ zigpy-zigate==0.12.0 zigpy-znp==0.12.1 # homeassistant.components.zha -zigpy==0.63.4 +zigpy==0.63.5 # homeassistant.components.zoneminder zm-py==0.5.4 diff --git a/requirements_test.txt b/requirements_test.txt index f3f0bfe5b8e..4dd02246a6e 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -8,45 +8,47 @@ -c homeassistant/package_constraints.txt -r requirements_test_pre_commit.txt astroid==3.1.0 -coverage==7.4.3 +coverage==7.4.4 freezegun==1.4.0 mock-open==1.4.0 -mypy==1.8.0 -pre-commit==3.6.2 +mypy-dev==1.10.0a3 +pre-commit==3.7.0 pydantic==1.10.12 pylint==3.1.0 pylint-per-file-ignores==1.3.2 -pipdeptree==2.15.1 -pytest-asyncio==0.23.5 +pipdeptree==2.16.1 +pytest-asyncio==0.23.6 pytest-aiohttp==1.0.5 -pytest-cov==4.1.0 +pytest-cov==5.0.0 pytest-freezer==0.4.8 +pytest-github-actions-annotate-failures==0.2.0 pytest-socket==0.7.0 pytest-test-groups==1.0.3 pytest-sugar==1.0.0 -pytest-timeout==2.2.0 -pytest-unordered==0.5.2 +pytest-timeout==2.3.1 +pytest-unordered==0.6.0 pytest-picked==0.5.0 pytest-xdist==3.3.1 -pytest==8.0.2 +pytest==8.1.1 requests-mock==1.11.0 -respx==0.20.2 +respx==0.21.0 syrupy==4.6.1 tqdm==4.66.2 -types-aiofiles==23.2.0.20240106 +types-aiofiles==23.2.0.20240311 types-atomicwrites==1.4.5.1 -types-croniter==1.0.6 -types-beautifulsoup4==4.12.0.20240106 +types-croniter==2.0.0.20240321 +types-beautifulsoup4==4.12.0.20240229 types-caldav==1.3.0.20240106 types-chardet==0.1.5 -types-decorator==5.1.8.20240106 -types-paho-mqtt==1.6.0.20240106 -types-pillow==10.2.0.20240111 +types-decorator==5.1.8.20240310 +types-paho-mqtt==1.6.0.20240321 +types-pillow==10.2.0.20240324 types-protobuf==4.24.0.20240106 -types-psutil==5.9.5.20240106 -types-python-dateutil==2.8.19.20240106 -types-python-slugify==8.0.2.20240127 -types-pytz==2023.3.1.1 -types-PyYAML==6.0.12.12 +types-psutil==5.9.5.20240316 +types-python-dateutil==2.9.0.20240316 +types-python-slugify==8.0.2.20240310 +types-pytz==2024.1.0.20240203 +types-PyYAML==6.0.12.20240311 types-requests==2.31.0.3 types-xmltodict==0.13.0.3 +uv==0.1.24 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2698706aac8..6f329b782aa 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -39,7 +39,7 @@ HATasmota==0.8.0 Pillow==10.2.0 # homeassistant.components.plex -PlexAPI==4.15.10 +PlexAPI==4.15.11 # homeassistant.components.progettihwsw ProgettiHWSW==0.1.3 @@ -61,7 +61,7 @@ PyMetEireann==2021.8.0 # homeassistant.components.met # homeassistant.components.norway_air -PyMetno==0.11.0 +PyMetno==0.12.0 # homeassistant.components.keymitt_ble PyMicroBot==0.0.17 @@ -80,9 +80,6 @@ PyQRCode==1.2.1 # homeassistant.components.rmvtransport PyRMVtransport==0.3.3 -# homeassistant.components.telegram_bot -PySocks==1.7.1 - # homeassistant.components.switchbot PySwitchbot==0.45.0 @@ -113,7 +110,7 @@ RtmAPI==0.7.2 # homeassistant.components.recorder # homeassistant.components.sql -SQLAlchemy==2.0.27 +SQLAlchemy==2.0.29 # homeassistant.components.tami4 Tami4EdgeAPI==2.1 @@ -167,7 +164,7 @@ aio-georss-gdacs==0.9 aioairq==0.3.2 # homeassistant.components.airzone_cloud -aioairzone-cloud==0.4.5 +aioairzone-cloud==0.4.6 # homeassistant.components.airzone aioairzone==0.7.6 @@ -179,13 +176,13 @@ aioambient==2024.01.0 aioapcaccess==0.4.2 # homeassistant.components.aseko_pool_live -aioaseko==0.0.2 +aioaseko==0.1.1 # homeassistant.components.asuswrt aioasuswrt==1.4.0 # homeassistant.components.husqvarna_automower -aioautomower==2024.3.3 +aioautomower==2024.3.4 # homeassistant.components.azure_devops aioazuredevops==1.3.5 @@ -200,13 +197,13 @@ aiobotocore==2.9.1 aiocomelit==0.9.0 # homeassistant.components.dhcp -aiodhcpwatcher==0.8.2 +aiodhcpwatcher==1.0.0 # homeassistant.components.dhcp -aiodiscover==1.6.1 +aiodiscover==2.0.0 # homeassistant.components.dnsip -aiodns==3.0.0 +aiodns==3.1.1 # homeassistant.components.eafm aioeafm==0.1.2 @@ -224,7 +221,7 @@ aioelectricitymaps==0.4.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==23.0.0 +aioesphomeapi==23.2.0 # homeassistant.components.flo aioflo==2021.11.0 @@ -290,8 +287,11 @@ aionanoleaf==0.2.1 # homeassistant.components.notion aionotion==2024.03.0 +# homeassistant.components.nut +aionut==4.3.2 + # homeassistant.components.oncue -aiooncue==0.3.5 +aiooncue==0.3.7 # homeassistant.components.openexchangerates aioopenexchangerates==0.4.0 @@ -309,7 +309,7 @@ aiopulse==0.4.4 aiopurpleair==2022.12.1 # homeassistant.components.hunterdouglas_powerview -aiopvapi==3.0.2 +aiopvapi==3.1.1 # homeassistant.components.pvpc_hourly_pricing aiopvpc==4.2.2 @@ -323,7 +323,7 @@ aiopyarr==23.4.0 aioqsw==0.3.5 # homeassistant.components.rainforest_raven -aioraven==0.5.2 +aioraven==0.5.3 # homeassistant.components.recollect_waste aiorecollect==2023.09.0 @@ -341,13 +341,13 @@ aioruuvigateway==0.1.0 aiosenz==1.0.0 # homeassistant.components.shelly -aioshelly==8.1.1 +aioshelly==8.2.0 # homeassistant.components.skybell aioskybell==22.7.0 # homeassistant.components.slimproto -aioslimproto==2.3.3 +aioslimproto==3.0.0 # homeassistant.components.steamist aiosteamist==0.3.2 @@ -365,7 +365,7 @@ aiotankerkoenig==0.4.1 aiotractive==0.5.6 # homeassistant.components.unifi -aiounifi==72 +aiounifi==74 # homeassistant.components.vlc_telnet aiovlc==0.1.0 @@ -422,7 +422,7 @@ anthemav==1.4.1 apple_weatherkit==1.1.2 # homeassistant.components.apprise -apprise==1.7.2 +apprise==1.7.4 # homeassistant.components.aprs aprslib==0.7.0 @@ -442,7 +442,7 @@ asterisk_mbox==0.5.0 # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.38.2 +async-upnp-client==0.38.3 # homeassistant.components.sleepiq asyncsleepiq==1.5.2 @@ -454,7 +454,7 @@ auroranoaa==0.0.3 aurorapy==0.2.7 # homeassistant.components.axis -axis==58 +axis==60 # homeassistant.components.azure_event_hub azure-eventhub==5.11.1 @@ -490,7 +490,7 @@ blebox-uniapi==2.2.2 blinkpy==0.22.6 # homeassistant.components.blue_current -bluecurrent-api==1.0.6 +bluecurrent-api==1.2.3 # homeassistant.components.bluemaestro bluemaestro-ble==0.2.3 @@ -499,7 +499,7 @@ bluemaestro-ble==0.2.3 bluetooth-adapters==0.18.0 # homeassistant.components.bluetooth -bluetooth-auto-recovery==1.3.0 +bluetooth-auto-recovery==1.4.0 # homeassistant.components.bluetooth # homeassistant.components.ld2410_ble @@ -514,7 +514,7 @@ bond-async==0.2.1 boschshcpy==0.2.82 # homeassistant.components.bring -bring-api==0.5.6 +bring-api==0.5.7 # homeassistant.components.broadlink broadlink==0.18.3 @@ -529,7 +529,7 @@ brottsplatskartan==1.0.5 brunt==1.2.0 # homeassistant.components.bthome -bthome-ble==3.8.0 +bthome-ble==3.8.1 # homeassistant.components.buienradar buienradar==1.0.5 @@ -702,10 +702,10 @@ fitbit==0.3.1 fivem-api==0.1.2 # homeassistant.components.fjaraskupan -fjaraskupan==2.2.0 +fjaraskupan==2.3.0 # homeassistant.components.flexit_bacnet -flexit_bacnet==2.1.0 +flexit_bacnet==2.2.1 # homeassistant.components.flipr flipr-api==1.5.1 @@ -730,6 +730,9 @@ freebox-api==1.1.0 # homeassistant.components.fritzbox_callmonitor fritzconnection[qr]==1.13.2 +# homeassistant.components.fyta +fyta_cli==0.3.3 + # homeassistant.components.google_translate gTTS==2.2.4 @@ -737,10 +740,10 @@ gTTS==2.2.4 gardena-bluetooth==1.4.1 # homeassistant.components.google_assistant_sdk -gassist-text==0.0.10 +gassist-text==0.0.11 # homeassistant.components.google -gcal-sync==6.0.3 +gcal-sync==6.0.4 # homeassistant.components.geocaching geocachingapi==0.2.1 @@ -830,7 +833,7 @@ h2==4.1.0 ha-av==10.1.1 # homeassistant.components.ffmpeg -ha-ffmpeg==3.1.0 +ha-ffmpeg==3.2.0 # homeassistant.components.iotawatt ha-iotawattpy==0.1.1 @@ -845,7 +848,7 @@ habitipy==0.2.0 habluetooth==2.4.2 # homeassistant.components.cloud -hass-nabucasa==0.78.0 +hass-nabucasa==0.79.0 # homeassistant.components.conversation hassil==1.6.1 @@ -870,13 +873,13 @@ hole==0.8.0 # homeassistant.components.holiday # homeassistant.components.workday -holidays==0.44 +holidays==0.46 # homeassistant.components.frontend -home-assistant-frontend==20240307.0 +home-assistant-frontend==20240403.1 # homeassistant.components.conversation -home-assistant-intents==2024.3.12 +home-assistant-intents==2024.4.3 # homeassistant.components.home_connect homeconnect==0.7.2 @@ -934,9 +937,6 @@ intellifire4py==2.2.2 # homeassistant.components.gogogate2 ismartgate==5.0.1 -# homeassistant.components.file_upload -janus==1.0.0 - # homeassistant.components.abode jaraco.abode==3.3.0 @@ -1066,6 +1066,9 @@ mopeka-iot-ble==0.7.0 # homeassistant.components.motion_blinds motionblinds==0.6.23 +# homeassistant.components.motionblinds_ble +motionblindsble==0.0.9 + # homeassistant.components.motioneye motioneye-client==0.3.14 @@ -1127,11 +1130,10 @@ nsw-fuel-api-client==1.1.0 nuheat==1.0.1 # homeassistant.components.numato -numato-gpio==0.12.0 +numato-gpio==0.13.0 # homeassistant.components.compensation # homeassistant.components.iqvia -# homeassistant.components.opencv # homeassistant.components.stream # homeassistant.components.tensorflow # homeassistant.components.trend @@ -1146,11 +1148,14 @@ objgraph==3.5.0 # homeassistant.components.garages_amsterdam odp-amsterdam==6.0.1 +# homeassistant.components.ollama +ollama-hass==0.1.7 + # homeassistant.components.omnilogic omnilogic==0.4.5 # homeassistant.components.ondilo_ico -ondilo==0.2.0 +ondilo==0.4.0 # homeassistant.components.onvif onvif-zeep-async==3.1.12 @@ -1171,7 +1176,7 @@ openerz-api==0.3.0 openhomedevice==2.2.0 # homeassistant.components.opower -opower==0.3.1 +opower==0.4.2 # homeassistant.components.oralb oralb-ble==0.17.6 @@ -1213,7 +1218,7 @@ plexauth==0.0.6 plexwebsocket==0.0.14 # homeassistant.components.plugwise -plugwise==0.36.3 +plugwise==0.37.1 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 @@ -1225,7 +1230,7 @@ poolsense==0.0.8 praw==7.5.0 # homeassistant.components.islamic_prayer_times -prayer-times-calculator==0.0.10 +prayer-times-calculator==0.0.12 # homeassistant.components.prometheus prometheus-client==0.17.1 @@ -1293,7 +1298,7 @@ pyCEC==0.5.2 pyControl4==1.1.0 # homeassistant.components.duotecno -pyDuotecno==2024.1.2 +pyDuotecno==2024.3.2 # homeassistant.components.electrasmart pyElectra==1.2.0 @@ -1390,7 +1395,7 @@ pyeconet==0.1.22 pyefergy==22.1.1 # homeassistant.components.enphase_envoy -pyenphase==1.19.2 +pyenphase==1.20.1 # homeassistant.components.everlights pyeverlights==0.1.0 @@ -1440,6 +1445,9 @@ pyhiveapi==0.5.16 # homeassistant.components.homematic pyhomematic==0.1.77 +# homeassistant.components.homeworks +pyhomeworks==0.0.6 + # homeassistant.components.ialarm pyialarm==2.2.0 @@ -1537,14 +1545,11 @@ pymysensors==0.24.0 pynetgear==0.10.10 # homeassistant.components.nobo_hub -pynobo==1.6.0 +pynobo==1.8.0 # homeassistant.components.nuki pynuki==1.6.3 -# homeassistant.components.nut -pynut2==2.1.2 - # homeassistant.components.nws pynws==1.6.0 @@ -1578,7 +1583,7 @@ pyotgw==2.1.3 pyotp==2.8.0 # homeassistant.components.overkiz -pyoverkiz==1.13.8 +pyoverkiz==1.13.9 # homeassistant.components.openweathermap pyowm==3.2.0 @@ -1605,7 +1610,7 @@ pyprof2calltree==1.4.5 pyprosegur==0.0.9 # homeassistant.components.prusalink -pyprusalink==2.0.0 +pyprusalink==2.1.1 # homeassistant.components.ps4 pyps4-2ndscreen==1.3.1 @@ -1617,7 +1622,7 @@ pyqwikswitch==0.93 pyrainbird==4.0.2 # homeassistant.components.risco -pyrisco==0.5.10 +pyrisco==0.6.0 # homeassistant.components.rituals_perfume_genie pyrituals==0.0.6 @@ -1718,7 +1723,7 @@ python-fullykiosk==0.0.12 python-homeassistant-analytics==0.6.0 # homeassistant.components.homewizard -python-homewizard-energy==4.3.1 +python-homewizard-energy==v5.0.0 # homeassistant.components.izone python-izone==1.2.9 @@ -1773,10 +1778,10 @@ python-tado==0.17.4 python-technove==1.2.2 # homeassistant.components.telegram_bot -python-telegram-bot==13.1 +python-telegram-bot[socks]==21.0.1 # homeassistant.components.tile -pytile==2023.04.0 +pytile==2023.12.0 # homeassistant.components.tomorrowio pytomorrowio==0.3.6 @@ -1801,7 +1806,7 @@ pytrydan==0.4.0 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==5.0.2 +pyunifiprotect==5.1.2 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 @@ -1822,7 +1827,7 @@ pyvlx==0.2.21 pyvolumio==0.1.5 # homeassistant.components.waze_travel_time -pywaze==0.5.1 +pywaze==1.0.0 # homeassistant.components.weatherflow pyweatherflowudp==1.4.5 @@ -1855,7 +1860,7 @@ qingping-ble==0.10.0 qnapstats==0.4.0 # homeassistant.components.radio_browser -radios==0.2.0 +radios==0.3.1 # homeassistant.components.radiotherm radiotherm==2.1.0 @@ -1867,7 +1872,7 @@ rapt-ble==0.1.2 refoss-ha==1.2.0 # homeassistant.components.rainmachine -regenmaschine==2024.01.0 +regenmaschine==2024.03.0 # homeassistant.components.renault renault-api==0.2.1 @@ -1882,7 +1887,7 @@ reolink-aio==0.8.9 rflink==0.0.66 # homeassistant.components.ring -ring-doorbell[listen]==0.8.7 +ring-doorbell[listen]==0.8.9 # homeassistant.components.roku rokuecp==0.19.2 @@ -1891,11 +1896,14 @@ rokuecp==0.19.2 romy==0.0.7 # homeassistant.components.roomba -roombapy==1.6.13 +roombapy==1.8.1 # homeassistant.components.roon roonapi==0.1.6 +# homeassistant.components.rova +rova==0.4.1 + # homeassistant.components.rpi_power rpi-bad-power==0.1.0 @@ -1961,7 +1969,7 @@ smart-meter-texas==0.4.7 smhi-pkg==1.0.16 # homeassistant.components.snapcast -snapcast==2.3.3 +snapcast==2.3.6 # homeassistant.components.sonos soco==0.30.2 @@ -2030,7 +2038,7 @@ subarulink==0.7.9 sunwatcher==0.2.1 # homeassistant.components.sunweg -sunweg==2.1.0 +sunweg==2.1.1 # homeassistant.components.surepetcare surepy==0.9.0 @@ -2039,7 +2047,7 @@ surepy==0.9.0 switchbot-api==2.0.0 # homeassistant.components.system_bridge -systembridgeconnector==3.10.0 +systembridgeconnector==4.0.3 # homeassistant.components.tailscale tailscale==0.6.0 @@ -2054,7 +2062,7 @@ temescal==0.5 temperusb==1.6.1 # homeassistant.components.teslemetry -tesla-fleet-api==0.4.6 +tesla-fleet-api==0.4.9 # homeassistant.components.powerwall tesla-powerwall==0.5.1 @@ -2078,7 +2086,7 @@ tilt-ble==0.2.3 todoist-api-python==2.1.2 # homeassistant.components.tolo -tololib==0.1.0b4 +tololib==1.1.0 # homeassistant.components.toon toonapi==0.3.0 @@ -2087,7 +2095,7 @@ toonapi==0.3.0 total-connect-client==2023.2 # homeassistant.components.tplink_omada -tplink-omada-client==1.3.11 +tplink-omada-client==1.3.12 # homeassistant.components.transmission transmission-rpc==7.0.3 @@ -2137,19 +2145,19 @@ uvcclient==0.11.0 vacuum-map-parser-roborock==0.1.1 # homeassistant.components.vallox -vallox-websocket-api==5.1.0 +vallox-websocket-api==5.1.1 # homeassistant.components.rdw vehicle==2.2.1 # homeassistant.components.velbus -velbus-aio==2023.12.0 +velbus-aio==2024.4.0 # homeassistant.components.venstar venstarcolortouch==0.19 # homeassistant.components.vilfo -vilfo-api-client==0.4.1 +vilfo-api-client==0.5.0 # homeassistant.components.voip voip-utils==0.1.0 @@ -2177,16 +2185,16 @@ wallbox==0.6.0 watchdog==2.3.1 # homeassistant.components.weatherflow_cloud -weatherflow4py==0.1.17 +weatherflow4py==0.2.17 # homeassistant.components.webmin -webmin-xmlrpc==0.0.1 +webmin-xmlrpc==0.0.2 # homeassistant.components.assist_pipeline webrtc-noise-gain==1.2.3 # homeassistant.components.whirlpool -whirlpool-sixth-sense==0.18.4 +whirlpool-sixth-sense==0.18.7 # homeassistant.components.whois whois==0.9.27 @@ -2207,7 +2215,7 @@ wyoming==1.5.3 xbox-webapi==2.0.11 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.25.2 +xiaomi-ble==0.28.0 # homeassistant.components.knx xknx==2.12.2 @@ -2231,13 +2239,13 @@ yalesmartalarmclient==0.3.9 yalexs-ble==2.4.2 # homeassistant.components.august -yalexs==1.11.4 +yalexs==2.0.0 # homeassistant.components.yeelight yeelight==0.7.14 # homeassistant.components.yolink -yolink-api==0.3.7 +yolink-api==0.4.1 # homeassistant.components.youless youless-api==1.0.1 @@ -2246,19 +2254,19 @@ youless-api==1.0.1 youtubeaio==1.1.5 # homeassistant.components.media_extractor -yt-dlp==2023.11.16 +yt-dlp==2024.03.10 # homeassistant.components.zamg zamg==0.3.6 # homeassistant.components.zeroconf -zeroconf==0.131.0 +zeroconf==0.132.0 # homeassistant.components.zeversolar zeversolar==0.3.1 # homeassistant.components.zha -zha-quirks==0.0.112 +zha-quirks==0.0.113 # homeassistant.components.zha zigpy-deconz==0.23.1 @@ -2273,7 +2281,7 @@ zigpy-zigate==0.12.0 zigpy-znp==0.12.1 # homeassistant.components.zha -zigpy==0.63.4 +zigpy==0.63.5 # homeassistant.components.zwave_js zwave-js-server-python==0.55.3 diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index 32d3f9e0c8b..cb64db20dcd 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -1,5 +1,5 @@ # Automatically generated from .pre-commit-config.yaml by gen_requirements_all.py, do not edit -codespell==2.2.2 -ruff==0.2.1 -yamllint==1.32.0 +codespell==2.2.6 +ruff==0.3.4 +yamllint==1.35.1 diff --git a/script/const.py b/script/const.py index de9b559e634..2f0b12784b5 100644 --- a/script/const.py +++ b/script/const.py @@ -1,4 +1,5 @@ """Script constants.""" + from pathlib import Path COMPONENT_DIR = Path("homeassistant/components") diff --git a/script/countries.py b/script/countries.py index 0d776f0805d..d67caa4da65 100644 --- a/script/countries.py +++ b/script/countries.py @@ -3,6 +3,7 @@ ISO does not publish a machine readable list free of charge, so the list is generated with help of the pycountry package. """ + from pathlib import Path import pycountry diff --git a/script/currencies.py b/script/currencies.py index 753b3363626..388df83e088 100644 --- a/script/currencies.py +++ b/script/currencies.py @@ -1,4 +1,5 @@ """Helper script to update currency list from the official source.""" + from pathlib import Path from bs4 import BeautifulSoup diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index e3fb7c390c0..9a9ff6821c7 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 """Generate updated constraint and requirements files.""" + from __future__ import annotations import difflib @@ -28,7 +29,6 @@ COMMENT_REQUIREMENTS = ( "decora-wifi", "evdev", "face-recognition", - "opencv-python-headless", "pybluez", "pycocotools", "pycups", @@ -98,7 +98,7 @@ regex==2021.8.28 # these requirements are quite loose. As the entire stack has some outstanding issues, and # even newer versions seem to introduce new issues, it's useful for us to pin all these # requirements so we can directly link HA versions to these library versions. -anyio==4.1.0 +anyio==4.3.0 h11==0.14.0 httpcore==1.0.4 @@ -176,6 +176,13 @@ pandas==2.1.4 # chacha20poly1305-reuseable==0.12.0 is incompatible with cryptography==42.0.x chacha20poly1305-reuseable>=0.12.1 + +# pycountry<23.12.11 imports setuptools at run time +# https://github.com/pycountry/pycountry/blob/ea69bab36f00df58624a0e490fdad4ccdc14268b/HISTORY.txt#L39 +pycountry>=23.12.11 + +# scapy<2.5.0 will not work with python3.12 +scapy>=2.5.0 """ GENERATED_MESSAGE = ( @@ -342,8 +349,7 @@ def generate_requirements_list(reqs: dict[str, list[str]]) -> str: """Generate a pip file based on requirements.""" output = [] for pkg, requirements in sorted(reqs.items(), key=itemgetter(0)): - for req in sorted(requirements): - output.append(f"\n# {req}") + output.extend(f"\n# {req}" for req in sorted(requirements)) if comment_requirement(pkg): output.append(f"\n# {pkg}\n") @@ -430,15 +436,17 @@ def gather_constraints() -> str: return ( GENERATED_MESSAGE + "\n".join( - sorted( - { - *core_requirements(), - *gather_recursive_requirements("default_config"), - *gather_recursive_requirements("mqtt"), - }, - key=str.lower, - ) - + [""] + [ + *sorted( + { + *core_requirements(), + *gather_recursive_requirements("default_config"), + *gather_recursive_requirements("mqtt"), + }, + key=str.lower, + ), + "", + ] ) + CONSTRAINT_BASE ) diff --git a/script/hassfest/__main__.py b/script/hassfest/__main__.py index 308c006defc..bcb19a14c37 100644 --- a/script/hassfest/__main__.py +++ b/script/hassfest/__main__.py @@ -1,4 +1,5 @@ """Validate manifests.""" + from __future__ import annotations import argparse diff --git a/script/hassfest/application_credentials.py b/script/hassfest/application_credentials.py index 1be644054c4..40cd15b4676 100644 --- a/script/hassfest/application_credentials.py +++ b/script/hassfest/application_credentials.py @@ -1,4 +1,5 @@ """Generate application_credentials data.""" + from __future__ import annotations from .model import Config, Integration diff --git a/script/hassfest/bluetooth.py b/script/hassfest/bluetooth.py index 295bcac1d1e..d724905f9cd 100644 --- a/script/hassfest/bluetooth.py +++ b/script/hassfest/bluetooth.py @@ -1,4 +1,5 @@ """Generate bluetooth file.""" + from __future__ import annotations from .model import Config, Integration @@ -15,8 +16,7 @@ def generate_and_validate(integrations: dict[str, Integration]) -> str: if not match_types: continue - for entry in match_types: - match_list.append({"domain": domain, **entry}) + match_list.extend({"domain": domain, **entry} for entry in match_types) return format_python_namespace( {"BLUETOOTH": match_list}, diff --git a/script/hassfest/brand.py b/script/hassfest/brand.py index 80f7416a018..fe47d31067a 100644 --- a/script/hassfest/brand.py +++ b/script/hassfest/brand.py @@ -1,4 +1,5 @@ """Brand validation.""" + from __future__ import annotations import voluptuous as vol diff --git a/script/hassfest/codeowners.py b/script/hassfest/codeowners.py index 458391b1fb4..15e34c23416 100644 --- a/script/hassfest/codeowners.py +++ b/script/hassfest/codeowners.py @@ -1,4 +1,5 @@ """Generate CODEOWNERS.""" + from __future__ import annotations from .model import Config, Integration diff --git a/script/hassfest/config_flow.py b/script/hassfest/config_flow.py index 3f5479fd118..382e77bde74 100644 --- a/script/hassfest/config_flow.py +++ b/script/hassfest/config_flow.py @@ -1,4 +1,5 @@ """Generate config flow file.""" + from __future__ import annotations import json diff --git a/script/hassfest/config_schema.py b/script/hassfest/config_schema.py index da2de9a6013..141b087472b 100644 --- a/script/hassfest/config_schema.py +++ b/script/hassfest/config_schema.py @@ -1,15 +1,18 @@ """Validate integrations which can be setup from YAML have config schemas.""" + from __future__ import annotations import ast +from homeassistant.core import DOMAIN as HA_DOMAIN + from .model import Config, Integration CONFIG_SCHEMA_IGNORE = { # Configuration under the homeassistant key is a special case, it's handled by # conf_util.async_process_ha_core_config already during bootstrapping, not by # a schema in the homeassistant integration. - "homeassistant", + HA_DOMAIN, } @@ -32,10 +35,7 @@ def _has_function( module: ast.Module, _type: ast.AsyncFunctionDef | ast.FunctionDef, name: str ) -> bool: """Test if the module defines a function.""" - for item in module.body: - if type(item) == _type and item.name == name: - return True - return False + return any(type(item) == _type and item.name == name for item in module.body) def _has_import(module: ast.Module, name: str) -> bool: diff --git a/script/hassfest/coverage.py b/script/hassfest/coverage.py index b2ee99e896b..64951fb0288 100644 --- a/script/hassfest/coverage.py +++ b/script/hassfest/coverage.py @@ -1,4 +1,5 @@ """Validate coverage files.""" + from __future__ import annotations from pathlib import Path diff --git a/script/hassfest/dependencies.py b/script/hassfest/dependencies.py index 31fd31dfc96..d9ec114e5bb 100644 --- a/script/hassfest/dependencies.py +++ b/script/hassfest/dependencies.py @@ -1,4 +1,5 @@ """Validate dependencies.""" + from __future__ import annotations import ast diff --git a/script/hassfest/dhcp.py b/script/hassfest/dhcp.py index 882e39f300e..67543a772fc 100644 --- a/script/hassfest/dhcp.py +++ b/script/hassfest/dhcp.py @@ -1,4 +1,5 @@ """Generate dhcp file.""" + from __future__ import annotations from .model import Config, Integration @@ -15,8 +16,7 @@ def generate_and_validate(integrations: dict[str, Integration]) -> str: if not match_types: continue - for entry in match_types: - match_list.append({"domain": domain, **entry}) + match_list.extend({"domain": domain, **entry} for entry in match_types) return format_python_namespace( {"DHCP": match_list}, diff --git a/script/hassfest/docker.py b/script/hassfest/docker.py index 2856c1ee0ea..4e348d4ae6c 100644 --- a/script/hassfest/docker.py +++ b/script/hassfest/docker.py @@ -1,8 +1,10 @@ """Generate and validate the dockerfile.""" + from homeassistant import core from homeassistant.util import executor, thread from .model import Config, Integration +from .requirements import PACKAGE_REGEX, PIP_VERSION_RANGE_SEPARATOR DOCKERFILE_TEMPLATE = r"""# Automatically generated by hassfest. # @@ -12,47 +14,47 @@ FROM ${{BUILD_FROM}} # Synchronize with homeassistant/core.py:async_stop ENV \ - S6_SERVICES_GRACETIME={timeout} + S6_SERVICES_GRACETIME={timeout} \ + UV_SYSTEM_PYTHON=true ARG QEMU_CPU +# Install uv +RUN pip3 install uv=={uv_version} + WORKDIR /usr/src ## Setup Home Assistant Core dependencies COPY requirements.txt homeassistant/ COPY homeassistant/package_constraints.txt homeassistant/homeassistant/ RUN \ - pip3 install \ - --only-binary=:all: \ + uv pip install \ + --no-build \ -r homeassistant/requirements.txt COPY requirements_all.txt home_assistant_frontend-* home_assistant_intents-* homeassistant/ RUN \ - if ls homeassistant/home_assistant_frontend*.whl 1> /dev/null 2>&1; then \ - pip3 install homeassistant/home_assistant_frontend-*.whl; \ - fi \ - && if ls homeassistant/home_assistant_intents*.whl 1> /dev/null 2>&1; then \ - pip3 install homeassistant/home_assistant_intents-*.whl; \ + if ls homeassistant/home_assistant_*.whl 1> /dev/null 2>&1; then \ + uv pip install homeassistant/home_assistant_*.whl; \ fi \ && if [ "${{BUILD_ARCH}}" = "i386" ]; then \ LD_PRELOAD="/usr/local/lib/libjemalloc.so.2" \ MALLOC_CONF="background_thread:true,metadata_thp:auto,dirty_decay_ms:20000,muzzy_decay_ms:20000" \ - linux32 pip3 install \ - --only-binary=:all: \ + linux32 uv pip install \ + --no-build \ -r homeassistant/requirements_all.txt; \ else \ LD_PRELOAD="/usr/local/lib/libjemalloc.so.2" \ MALLOC_CONF="background_thread:true,metadata_thp:auto,dirty_decay_ms:20000,muzzy_decay_ms:20000" \ - pip3 install \ - --only-binary=:all: \ + uv pip install \ + --no-build \ -r homeassistant/requirements_all.txt; \ fi ## Setup Home Assistant Core COPY . homeassistant/ RUN \ - pip3 install \ - --only-binary=:all: \ + uv pip install \ -e ./homeassistant \ && python3 -m compileall \ homeassistant/homeassistant @@ -64,6 +66,28 @@ WORKDIR /config """ +def _get_uv_version() -> str: + with open("requirements_test.txt") as fp: + for _, line in enumerate(fp): + if match := PACKAGE_REGEX.match(line): + pkg, sep, version = match.groups() + + if pkg != "uv": + continue + + if sep != "==" or not version: + raise RuntimeError( + 'Requirement uv need to be pinned "uv==".' + ) + + for part in version.split(";", 1)[0].split(","): + version_part = PIP_VERSION_RANGE_SEPARATOR.match(part) + if version_part: + return version_part.group(2) + + raise RuntimeError("Invalid uv requirement in requirements_test.txt") + + def _generate_dockerfile() -> str: timeout = ( core.STOPPING_STAGE_SHUTDOWN_TIMEOUT @@ -74,7 +98,9 @@ def _generate_dockerfile() -> str: + thread.THREADING_SHUTDOWN_TIMEOUT + 10 ) - return DOCKERFILE_TEMPLATE.format(timeout=timeout * 1000) + return DOCKERFILE_TEMPLATE.format( + timeout=timeout * 1000, uv_version=_get_uv_version() + ) def validate(integrations: dict[str, Integration], config: Config) -> None: diff --git a/script/hassfest/icons.py b/script/hassfest/icons.py index 60dc2b79f56..b7ba2fbb402 100644 --- a/script/hassfest/icons.py +++ b/script/hassfest/icons.py @@ -1,4 +1,5 @@ """Validate integration icon translation files.""" + from __future__ import annotations from typing import Any @@ -46,7 +47,7 @@ def ensure_not_same_as_default(value: dict) -> dict: return value -def icon_schema(integration_type: str) -> vol.Schema: +def icon_schema(integration_type: str, no_entity_platform: bool) -> vol.Schema: """Create a icon schema.""" state_validator = cv.schema_with_slug_keys( @@ -77,7 +78,7 @@ def icon_schema(integration_type: str) -> vol.Schema: ) if integration_type in ("entity", "helper", "system"): - if integration_type != "entity": + if integration_type != "entity" or no_entity_platform: field = vol.Optional("entity_component") else: field = vol.Required("entity_component") @@ -111,7 +112,7 @@ def icon_schema(integration_type: str) -> vol.Schema: return schema -def validate_icon_file(config: Config, integration: Integration) -> None: # noqa: C901 +def validate_icon_file(config: Config, integration: Integration) -> None: """Validate icon file for integration.""" icons_file = integration.path / "icons.json" if not icons_file.is_file(): @@ -125,7 +126,9 @@ def validate_icon_file(config: Config, integration: Integration) -> None: # noq integration.add_error("icons", f"Invalid JSON in {name}: {err}") return - schema = icon_schema(integration.integration_type) + no_entity_platform = integration.domain in ("notify", "image_processing") + + schema = icon_schema(integration.integration_type, no_entity_platform) try: schema(icons) diff --git a/script/hassfest/json.py b/script/hassfest/json.py index 5e3d05f78dc..5e4568cd241 100644 --- a/script/hassfest/json.py +++ b/script/hassfest/json.py @@ -1,4 +1,5 @@ """Validate integration JSON files.""" + from __future__ import annotations import json diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py index 6339d8293ec..0c7f48b9af3 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -1,4 +1,5 @@ """Manifest validation.""" + from __future__ import annotations from enum import IntEnum @@ -265,7 +266,6 @@ INTEGRATION_MANIFEST_SCHEMA = vol.Schema( vol.Optional("loggers"): [str], vol.Optional("disabled"): str, vol.Optional("iot_class"): vol.In(SUPPORTED_IOT_CLASSES), - vol.Optional("import_executor"): bool, vol.Optional("single_config_entry"): bool, } ) @@ -293,6 +293,7 @@ def manifest_schema(value: dict[str, Any]) -> vol.Schema: CUSTOM_INTEGRATION_MANIFEST_SCHEMA = INTEGRATION_MANIFEST_SCHEMA.extend( { vol.Optional("version"): vol.All(str, verify_version), + vol.Optional("import_executor"): bool, } ) @@ -397,8 +398,15 @@ def validate(integrations: dict[str, Integration], config: Config) -> None: manifests_resorted.append(integration.manifest_path) if config.action == "generate" and manifests_resorted: subprocess.run( - ["pre-commit", "run", "--hook-stage", "manual", "prettier", "--files"] - + manifests_resorted, + [ + "pre-commit", + "run", + "--hook-stage", + "manual", + "prettier", + "--files", + *manifests_resorted, + ], stdout=subprocess.DEVNULL, check=True, ) diff --git a/script/hassfest/metadata.py b/script/hassfest/metadata.py index 091c1b88e30..bd3ac4514e7 100644 --- a/script/hassfest/metadata.py +++ b/script/hassfest/metadata.py @@ -1,4 +1,5 @@ """Package metadata validation.""" + import tomllib from homeassistant.const import REQUIRED_PYTHON_VER, __version__ diff --git a/script/hassfest/model.py b/script/hassfest/model.py index 7df65b8221e..736fb6874be 100644 --- a/script/hassfest/model.py +++ b/script/hassfest/model.py @@ -1,4 +1,5 @@ """Models for manifest validator.""" + from __future__ import annotations from dataclasses import dataclass, field diff --git a/script/hassfest/mqtt.py b/script/hassfest/mqtt.py index 2619e22911a..b2112d9bb6a 100644 --- a/script/hassfest/mqtt.py +++ b/script/hassfest/mqtt.py @@ -1,4 +1,5 @@ """Generate MQTT file.""" + from __future__ import annotations from collections import defaultdict diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 5513105fa17..c02ebd8de2e 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -1,4 +1,5 @@ """Generate mypy config.""" + from __future__ import annotations from collections.abc import Iterable diff --git a/script/hassfest/requirements.py b/script/hassfest/requirements.py index 8b9f73336fe..18d560f840f 100644 --- a/script/hassfest/requirements.py +++ b/script/hassfest/requirements.py @@ -1,4 +1,5 @@ """Validate requirements.""" + from __future__ import annotations from collections import deque diff --git a/script/hassfest/serializer.py b/script/hassfest/serializer.py index b56306a8d7e..1de4c48a0c4 100644 --- a/script/hassfest/serializer.py +++ b/script/hassfest/serializer.py @@ -1,4 +1,5 @@ """Hassfest utils.""" + from __future__ import annotations from collections.abc import Collection, Iterable, Mapping diff --git a/script/hassfest/services.py b/script/hassfest/services.py index 580294705cf..c962d84e6e1 100644 --- a/script/hassfest/services.py +++ b/script/hassfest/services.py @@ -1,4 +1,5 @@ """Validate dependencies.""" + from __future__ import annotations import contextlib @@ -29,7 +30,6 @@ CORE_INTEGRATION_FIELD_SCHEMA = vol.Schema( { vol.Optional("example"): exists, vol.Optional("default"): exists, - vol.Optional("values"): exists, vol.Optional("required"): bool, vol.Optional("advanced"): bool, vol.Optional(CONF_SELECTOR): selector.validate_selector, @@ -139,6 +139,13 @@ def validate_services(config: Config, integration: Integration) -> None: ) return + icons_file = integration.path / "icons.json" + icons = {} + if icons_file.is_file(): + with contextlib.suppress(ValueError): + icons = json.loads(icons_file.read_text()) + service_icons = icons.get("services", {}) + # Try loading translation strings if integration.core: strings_file = integration.path / "strings.json" @@ -155,9 +162,18 @@ def validate_services(config: Config, integration: Integration) -> None: if not integration.core: error_msg_suffix = f"and is not {error_msg_suffix}" - # For each service in the integration, check if the description if set, - # if not, check if it's in the strings file. If not, add an error. + # For each service in the integration: + # 1. Check if the service description is set, if not, + # check if it's in the strings file else add an error. + # 2. Check if the service has an icon set in icons.json. + # raise an error if not., for service_name, service_schema in services.items(): + if integration.core and service_name not in service_icons: + # This is enforced for Core integrations only + integration.add_error( + "services", + f"Service {service_name} has no icon in icons.json.", + ) if service_schema is None: continue if "name" not in service_schema: diff --git a/script/hassfest/ssdp.py b/script/hassfest/ssdp.py index de548ee2992..0a61284eb46 100644 --- a/script/hassfest/ssdp.py +++ b/script/hassfest/ssdp.py @@ -1,4 +1,5 @@ """Generate ssdp file.""" + from __future__ import annotations from collections import defaultdict diff --git a/script/hassfest/translations.py b/script/hassfest/translations.py index 1501e4983c5..724f65eafb6 100644 --- a/script/hassfest/translations.py +++ b/script/hassfest/translations.py @@ -1,4 +1,5 @@ """Validate integration translation files.""" + from __future__ import annotations from functools import partial @@ -440,7 +441,10 @@ def gen_platform_strings_schema(config: Config, integration: Integration) -> vol ONBOARDING_SCHEMA = vol.Schema( - {vol.Required("area"): {str: translation_value_validator}} + { + vol.Required("area"): {str: translation_value_validator}, + vol.Required("dashboard"): {str: {"title": translation_value_validator}}, + } ) diff --git a/script/hassfest/usb.py b/script/hassfest/usb.py index c5e3148f7b1..84cafc973ad 100644 --- a/script/hassfest/usb.py +++ b/script/hassfest/usb.py @@ -1,4 +1,5 @@ """Generate usb file.""" + from __future__ import annotations from .model import Config, Integration @@ -15,13 +16,13 @@ def generate_and_validate(integrations: dict[str, Integration]) -> str: if not match_types: continue - for entry in match_types: - match_list.append( - { - "domain": domain, - **{k: v for k, v in entry.items() if k != "known_devices"}, - } - ) + match_list.extend( + { + "domain": domain, + **{k: v for k, v in entry.items() if k != "known_devices"}, + } + for entry in match_types + ) return format_python_namespace({"USB": match_list}) diff --git a/script/hassfest/zeroconf.py b/script/hassfest/zeroconf.py index bb6a84e1e2c..63f10fcf294 100644 --- a/script/hassfest/zeroconf.py +++ b/script/hassfest/zeroconf.py @@ -1,4 +1,5 @@ """Generate zeroconf file.""" + from __future__ import annotations from collections import defaultdict diff --git a/script/inspect_schemas.py b/script/inspect_schemas.py index cd72f55a855..a8ffe0afb60 100755 --- a/script/inspect_schemas.py +++ b/script/inspect_schemas.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 """Inspect all component SCHEMAS.""" + import importlib import os import pkgutil diff --git a/script/languages.py b/script/languages.py index f55fc21db93..bfc811a0905 100644 --- a/script/languages.py +++ b/script/languages.py @@ -1,4 +1,5 @@ """Helper script to update language list from the frontend source.""" + import json from pathlib import Path import sys diff --git a/script/lint_and_test.py b/script/lint_and_test.py index 48809ae4dcd..0b0562a0a84 100755 --- a/script/lint_and_test.py +++ b/script/lint_and_test.py @@ -3,6 +3,7 @@ This is NOT a full CI/linting replacement, only a quick check during development. """ + import asyncio from collections import namedtuple from contextlib import suppress diff --git a/script/microsoft_tts.py b/script/microsoft_tts.py index bc0b577c97c..f3761db2f71 100644 --- a/script/microsoft_tts.py +++ b/script/microsoft_tts.py @@ -1,4 +1,5 @@ """Helper script to update supported languages for Microsoft text-to-speech (TTS).""" + from pathlib import Path from lxml import html diff --git a/script/scaffold/__main__.py b/script/scaffold/__main__.py index c303fc2c247..f81f9144f98 100644 --- a/script/scaffold/__main__.py +++ b/script/scaffold/__main__.py @@ -1,4 +1,5 @@ """Validate manifests.""" + import argparse from pathlib import Path import subprocess diff --git a/script/scaffold/const.py b/script/scaffold/const.py index cf66bb4e2ae..2f8126b3393 100644 --- a/script/scaffold/const.py +++ b/script/scaffold/const.py @@ -1,4 +1,5 @@ """Constants for scaffolding.""" + from pathlib import Path COMPONENT_DIR = Path("homeassistant/components") diff --git a/script/scaffold/docs.py b/script/scaffold/docs.py index 3ce86b0e138..a969fa699dd 100644 --- a/script/scaffold/docs.py +++ b/script/scaffold/docs.py @@ -1,4 +1,5 @@ """Print links to relevant docs.""" + from .model import Info DATA = { diff --git a/script/scaffold/gather_info.py b/script/scaffold/gather_info.py index 47106e7f956..cfa2669ebfe 100644 --- a/script/scaffold/gather_info.py +++ b/script/scaffold/gather_info.py @@ -1,4 +1,5 @@ """Gather info for scaffolding.""" + import json from homeassistant.util import slugify diff --git a/script/scaffold/generate.py b/script/scaffold/generate.py index 197c36e22d1..0bee69b93f8 100644 --- a/script/scaffold/generate.py +++ b/script/scaffold/generate.py @@ -1,4 +1,5 @@ """Generate an integration.""" + from pathlib import Path from .model import Info diff --git a/script/scaffold/model.py b/script/scaffold/model.py index 2b1ee71fc63..3b5a5e50fe4 100644 --- a/script/scaffold/model.py +++ b/script/scaffold/model.py @@ -1,4 +1,5 @@ """Models for scaffolding.""" + from __future__ import annotations import json diff --git a/script/scaffold/templates/backup/integration/backup.py b/script/scaffold/templates/backup/integration/backup.py index 88df5ead221..ab3eb1be440 100644 --- a/script/scaffold/templates/backup/integration/backup.py +++ b/script/scaffold/templates/backup/integration/backup.py @@ -1,4 +1,5 @@ """Backup platform for the NEW_NAME integration.""" + from homeassistant.core import HomeAssistant diff --git a/script/scaffold/templates/backup/tests/test_backup.py b/script/scaffold/templates/backup/tests/test_backup.py index 43d23ac7a90..9f3c2be657c 100644 --- a/script/scaffold/templates/backup/tests/test_backup.py +++ b/script/scaffold/templates/backup/tests/test_backup.py @@ -1,4 +1,5 @@ """Test the NEW_NAME backup platform.""" + from homeassistant.components.NEW_DOMAIN.backup import ( async_post_backup, async_pre_backup, diff --git a/script/scaffold/templates/config_flow/integration/__init__.py b/script/scaffold/templates/config_flow/integration/__init__.py index 4d74c5d41ff..87391f1733e 100644 --- a/script/scaffold/templates/config_flow/integration/__init__.py +++ b/script/scaffold/templates/config_flow/integration/__init__.py @@ -1,4 +1,5 @@ """The NEW_NAME integration.""" + from __future__ import annotations from homeassistant.config_entries import ConfigEntry diff --git a/script/scaffold/templates/config_flow/integration/config_flow.py b/script/scaffold/templates/config_flow/integration/config_flow.py index caef6c2e729..797ca5c7066 100644 --- a/script/scaffold/templates/config_flow/integration/config_flow.py +++ b/script/scaffold/templates/config_flow/integration/config_flow.py @@ -1,4 +1,5 @@ """Config flow for NEW_NAME integration.""" + from __future__ import annotations import logging @@ -6,10 +7,9 @@ from typing import Any import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError from .const import DOMAIN @@ -68,14 +68,14 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, return {"title": "Name of the device"} -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class ConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for NEW_NAME.""" VERSION = 1 async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors: dict[str, str] = {} if user_input is not None: diff --git a/script/scaffold/templates/config_flow/tests/conftest.py b/script/scaffold/templates/config_flow/tests/conftest.py index 05993acc329..84b6bb381bf 100644 --- a/script/scaffold/templates/config_flow/tests/conftest.py +++ b/script/scaffold/templates/config_flow/tests/conftest.py @@ -1,4 +1,5 @@ """Common fixtures for the NEW_NAME tests.""" + from collections.abc import Generator from unittest.mock import AsyncMock, patch 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 bb9e6380cdc..9a712834bae 100644 --- a/script/scaffold/templates/config_flow/tests/test_config_flow.py +++ b/script/scaffold/templates/config_flow/tests/test_config_flow.py @@ -1,4 +1,5 @@ """Test the NEW_NAME config flow.""" + from unittest.mock import AsyncMock, patch from homeassistant import config_entries diff --git a/script/scaffold/templates/config_flow_discovery/integration/__init__.py b/script/scaffold/templates/config_flow_discovery/integration/__init__.py index efa59ddd82a..4d18fecc2fa 100644 --- a/script/scaffold/templates/config_flow_discovery/integration/__init__.py +++ b/script/scaffold/templates/config_flow_discovery/integration/__init__.py @@ -1,4 +1,5 @@ """The NEW_NAME integration.""" + from __future__ import annotations from homeassistant.config_entries import ConfigEntry diff --git a/script/scaffold/templates/config_flow_discovery/integration/config_flow.py b/script/scaffold/templates/config_flow_discovery/integration/config_flow.py index 4e1b70a51f3..e2cfed40e1d 100644 --- a/script/scaffold/templates/config_flow_discovery/integration/config_flow.py +++ b/script/scaffold/templates/config_flow_discovery/integration/config_flow.py @@ -1,4 +1,5 @@ """Config flow for NEW_NAME.""" + import my_pypi_dependency from homeassistant.core import HomeAssistant diff --git a/script/scaffold/templates/config_flow_helper/integration/__init__.py b/script/scaffold/templates/config_flow_helper/integration/__init__.py index 2ad917394b9..c8817fb76ad 100644 --- a/script/scaffold/templates/config_flow_helper/integration/__init__.py +++ b/script/scaffold/templates/config_flow_helper/integration/__init__.py @@ -1,4 +1,5 @@ """The NEW_NAME integration.""" + from __future__ import annotations from homeassistant.config_entries import ConfigEntry diff --git a/script/scaffold/templates/config_flow_helper/integration/config_flow.py b/script/scaffold/templates/config_flow_helper/integration/config_flow.py index f9b8e81d364..5d89fec2da2 100644 --- a/script/scaffold/templates/config_flow_helper/integration/config_flow.py +++ b/script/scaffold/templates/config_flow_helper/integration/config_flow.py @@ -1,4 +1,5 @@ """Config flow for NEW_NAME integration.""" + from __future__ import annotations from collections.abc import Mapping diff --git a/script/scaffold/templates/config_flow_helper/integration/sensor.py b/script/scaffold/templates/config_flow_helper/integration/sensor.py index fbf92bfdd6b..741b2e85eb2 100644 --- a/script/scaffold/templates/config_flow_helper/integration/sensor.py +++ b/script/scaffold/templates/config_flow_helper/integration/sensor.py @@ -1,4 +1,5 @@ """Sensor platform for NEW_NAME integration.""" + from __future__ import annotations from homeassistant.components.sensor import SensorEntity diff --git a/script/scaffold/templates/config_flow_helper/tests/conftest.py b/script/scaffold/templates/config_flow_helper/tests/conftest.py index 05993acc329..84b6bb381bf 100644 --- a/script/scaffold/templates/config_flow_helper/tests/conftest.py +++ b/script/scaffold/templates/config_flow_helper/tests/conftest.py @@ -1,4 +1,5 @@ """Common fixtures for the NEW_NAME tests.""" + from collections.abc import Generator from unittest.mock import AsyncMock, patch 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 d21c66797d8..809902fa0dd 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 @@ -1,4 +1,5 @@ """Test the NEW_NAME config flow.""" + from unittest.mock import AsyncMock import pytest @@ -13,7 +14,7 @@ from tests.common import MockConfigEntry pytestmark = pytest.mark.usefixtures("mock_setup_entry") -@pytest.mark.parametrize("platform", ("sensor",)) +@pytest.mark.parametrize("platform", ["sensor"]) async def test_config_flow( hass: HomeAssistant, mock_setup_entry: AsyncMock, platform ) -> None: @@ -61,7 +62,7 @@ def get_suggested(schema, key): raise Exception -@pytest.mark.parametrize("platform", ("sensor",)) +@pytest.mark.parametrize("platform", ["sensor"]) async def test_options(hass: HomeAssistant, platform) -> None: """Test reconfiguring.""" input_sensor_1_entity_id = "sensor.input1" diff --git a/script/scaffold/templates/config_flow_helper/tests/test_init.py b/script/scaffold/templates/config_flow_helper/tests/test_init.py index 0e86874c745..73ac28da059 100644 --- a/script/scaffold/templates/config_flow_helper/tests/test_init.py +++ b/script/scaffold/templates/config_flow_helper/tests/test_init.py @@ -1,4 +1,5 @@ """Test the NEW_NAME integration.""" + import pytest from homeassistant.components.NEW_DOMAIN.const import DOMAIN @@ -8,7 +9,7 @@ from homeassistant.helpers import entity_registry as er from tests.common import MockConfigEntry -@pytest.mark.parametrize("platform", ("sensor",)) +@pytest.mark.parametrize("platform", ["sensor"]) async def test_setup_and_remove_config_entry( hass: HomeAssistant, platform: str, diff --git a/script/scaffold/templates/config_flow_oauth2/integration/__init__.py b/script/scaffold/templates/config_flow_oauth2/integration/__init__.py index 213740005e5..7e7641a535b 100644 --- a/script/scaffold/templates/config_flow_oauth2/integration/__init__.py +++ b/script/scaffold/templates/config_flow_oauth2/integration/__init__.py @@ -1,4 +1,5 @@ """The NEW_NAME integration.""" + from __future__ import annotations from homeassistant.config_entries import ConfigEntry diff --git a/script/scaffold/templates/config_flow_oauth2/integration/api.py b/script/scaffold/templates/config_flow_oauth2/integration/api.py index 9ae65bb4d85..3f4aa3cfb82 100644 --- a/script/scaffold/templates/config_flow_oauth2/integration/api.py +++ b/script/scaffold/templates/config_flow_oauth2/integration/api.py @@ -1,4 +1,5 @@ """API for NEW_NAME bound to Home Assistant OAuth.""" + from asyncio import run_coroutine_threadsafe from aiohttp import ClientSession diff --git a/script/scaffold/templates/config_flow_oauth2/integration/config_flow.py b/script/scaffold/templates/config_flow_oauth2/integration/config_flow.py index a035a65dbb3..d855cc2a70b 100644 --- a/script/scaffold/templates/config_flow_oauth2/integration/config_flow.py +++ b/script/scaffold/templates/config_flow_oauth2/integration/config_flow.py @@ -1,4 +1,5 @@ """Config flow for NEW_NAME.""" + import logging from homeassistant.helpers import config_entry_oauth2_flow diff --git a/script/scaffold/templates/config_flow_oauth2/tests/test_config_flow.py b/script/scaffold/templates/config_flow_oauth2/tests/test_config_flow.py index d0000b800df..6e3a2047c6e 100644 --- a/script/scaffold/templates/config_flow_oauth2/tests/test_config_flow.py +++ b/script/scaffold/templates/config_flow_oauth2/tests/test_config_flow.py @@ -42,7 +42,7 @@ async def test_full_flow( ) -> None: """Check full flow.""" result = await hass.config_entries.flow.async_init( - "NEW_DOMAIN", context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) state = config_entry_oauth2_flow._encode_jwt( hass, diff --git a/script/scaffold/templates/device_action/integration/device_action.py b/script/scaffold/templates/device_action/integration/device_action.py index 4732d9bd71c..02be0afa602 100644 --- a/script/scaffold/templates/device_action/integration/device_action.py +++ b/script/scaffold/templates/device_action/integration/device_action.py @@ -1,4 +1,5 @@ """Provides device actions for NEW_NAME.""" + from __future__ import annotations import voluptuous as vol diff --git a/script/scaffold/templates/device_action/tests/test_device_action.py b/script/scaffold/templates/device_action/tests/test_device_action.py index a6e8f99854a..2ecf3e95f9a 100644 --- a/script/scaffold/templates/device_action/tests/test_device_action.py +++ b/script/scaffold/templates/device_action/tests/test_device_action.py @@ -1,4 +1,5 @@ """The tests for NEW_NAME device actions.""" + import pytest from pytest_unordered import unordered @@ -48,13 +49,13 @@ async def test_get_actions( @pytest.mark.parametrize( - "hidden_by,entity_category", - ( + ("hidden_by", "entity_category"), + [ (er.RegistryEntryHider.INTEGRATION, None), (er.RegistryEntryHider.USER, None), (None, EntityCategory.CONFIG), (None, EntityCategory.DIAGNOSTIC), - ), + ], ) async def test_get_actions_hidden_auxiliary( hass: HomeAssistant, diff --git a/script/scaffold/templates/device_condition/integration/device_condition.py b/script/scaffold/templates/device_condition/integration/device_condition.py index 00acd23698a..ba7752a6d60 100644 --- a/script/scaffold/templates/device_condition/integration/device_condition.py +++ b/script/scaffold/templates/device_condition/integration/device_condition.py @@ -1,4 +1,5 @@ """Provide the device conditions for NEW_NAME.""" + from __future__ import annotations import voluptuous as vol diff --git a/script/scaffold/templates/device_condition/tests/test_device_condition.py b/script/scaffold/templates/device_condition/tests/test_device_condition.py index 1f6e0ffde3c..ad6d527bd65 100644 --- a/script/scaffold/templates/device_condition/tests/test_device_condition.py +++ b/script/scaffold/templates/device_condition/tests/test_device_condition.py @@ -1,4 +1,5 @@ """The tests for NEW_NAME device conditions.""" + from __future__ import annotations import pytest diff --git a/script/scaffold/templates/device_trigger/integration/device_trigger.py b/script/scaffold/templates/device_trigger/integration/device_trigger.py index 4179bf5248a..8dcf438efc0 100644 --- a/script/scaffold/templates/device_trigger/integration/device_trigger.py +++ b/script/scaffold/templates/device_trigger/integration/device_trigger.py @@ -1,4 +1,5 @@ """Provides device triggers for NEW_NAME.""" + from __future__ import annotations from typing import Any diff --git a/script/scaffold/templates/device_trigger/tests/test_device_trigger.py b/script/scaffold/templates/device_trigger/tests/test_device_trigger.py index 177c095b601..54b202c978c 100644 --- a/script/scaffold/templates/device_trigger/tests/test_device_trigger.py +++ b/script/scaffold/templates/device_trigger/tests/test_device_trigger.py @@ -1,4 +1,5 @@ """The tests for NEW_NAME device triggers.""" + import pytest from pytest_unordered import unordered diff --git a/script/scaffold/templates/integration/integration/__init__.py b/script/scaffold/templates/integration/integration/__init__.py index e30cd400bf2..66da2698015 100644 --- a/script/scaffold/templates/integration/integration/__init__.py +++ b/script/scaffold/templates/integration/integration/__init__.py @@ -1,4 +1,5 @@ """The NEW_NAME integration.""" + from __future__ import annotations import voluptuous as vol diff --git a/script/scaffold/templates/reproduce_state/integration/reproduce_state.py b/script/scaffold/templates/reproduce_state/integration/reproduce_state.py index 4247a1dc8d2..ae1eb622e5b 100644 --- a/script/scaffold/templates/reproduce_state/integration/reproduce_state.py +++ b/script/scaffold/templates/reproduce_state/integration/reproduce_state.py @@ -1,4 +1,5 @@ """Reproduce an NEW_NAME state.""" + from __future__ import annotations import asyncio diff --git a/script/scaffold/templates/reproduce_state/tests/test_reproduce_state.py b/script/scaffold/templates/reproduce_state/tests/test_reproduce_state.py index cb4e37c0933..a8cb7b09c96 100644 --- a/script/scaffold/templates/reproduce_state/tests/test_reproduce_state.py +++ b/script/scaffold/templates/reproduce_state/tests/test_reproduce_state.py @@ -1,4 +1,5 @@ """Test reproduce state for NEW_NAME.""" + import pytest from homeassistant.core import HomeAssistant, State diff --git a/script/scaffold/templates/significant_change/integration/significant_change.py b/script/scaffold/templates/significant_change/integration/significant_change.py index 8e6022f171b..803edc035b2 100644 --- a/script/scaffold/templates/significant_change/integration/significant_change.py +++ b/script/scaffold/templates/significant_change/integration/significant_change.py @@ -1,4 +1,5 @@ """Helper to test significant NEW_NAME state changes.""" + from __future__ import annotations from typing import Any diff --git a/script/scaffold/templates/significant_change/tests/test_significant_change.py b/script/scaffold/templates/significant_change/tests/test_significant_change.py index 89194cc52a3..519a80d8de6 100644 --- a/script/scaffold/templates/significant_change/tests/test_significant_change.py +++ b/script/scaffold/templates/significant_change/tests/test_significant_change.py @@ -1,4 +1,5 @@ """Test the NEW_NAME significant change platform.""" + from homeassistant.components.NEW_DOMAIN.significant_change import ( async_check_significant_change, ) diff --git a/script/translations/__main__.py b/script/translations/__main__.py index 52a39038107..c381b621e69 100644 --- a/script/translations/__main__.py +++ b/script/translations/__main__.py @@ -1,4 +1,5 @@ """Validate manifests.""" + import argparse import importlib from pathlib import Path diff --git a/script/translations/clean.py b/script/translations/clean.py index 0f2eb40300d..0403e04f789 100644 --- a/script/translations/clean.py +++ b/script/translations/clean.py @@ -1,4 +1,5 @@ """Find translation keys that are in Lokalise but no longer defined in source.""" + import argparse from .const import CORE_PROJECT_ID, FRONTEND_DIR, FRONTEND_PROJECT_ID, INTEGRATIONS_DIR diff --git a/script/translations/const.py b/script/translations/const.py index ef8e3f2df74..9ff8aeb2d70 100644 --- a/script/translations/const.py +++ b/script/translations/const.py @@ -1,4 +1,5 @@ """Translation constants.""" + import pathlib CORE_PROJECT_ID = "130246255a974bd3b5e8a1.51616605" diff --git a/script/translations/deduplicate.py b/script/translations/deduplicate.py index 27764f0987f..8cc4cee3b10 100644 --- a/script/translations/deduplicate.py +++ b/script/translations/deduplicate.py @@ -1,6 +1,5 @@ """Deduplicate translations in strings.json.""" - import argparse import json from pathlib import Path diff --git a/script/translations/develop.py b/script/translations/develop.py index 3e386afb641..14e3c320c3e 100644 --- a/script/translations/develop.py +++ b/script/translations/develop.py @@ -1,4 +1,5 @@ """Compile the current translation strings files for testing.""" + import argparse import json from pathlib import Path diff --git a/script/translations/download.py b/script/translations/download.py index d02b5c869aa..958a4b35a7b 100755 --- a/script/translations/download.py +++ b/script/translations/download.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 """Merge all translation sources into a single JSON file.""" + from __future__ import annotations import json diff --git a/script/translations/error.py b/script/translations/error.py index 210af95f325..4811210ae3c 100644 --- a/script/translations/error.py +++ b/script/translations/error.py @@ -1,4 +1,5 @@ """Errors for translations.""" + import json diff --git a/script/translations/frontend.py b/script/translations/frontend.py index bb0e98e1c93..289c885fe33 100644 --- a/script/translations/frontend.py +++ b/script/translations/frontend.py @@ -1,4 +1,5 @@ """Write updated translations to the frontend.""" + import argparse import json diff --git a/script/translations/lokalise.py b/script/translations/lokalise.py index a23291169f4..5cd0a695471 100644 --- a/script/translations/lokalise.py +++ b/script/translations/lokalise.py @@ -1,4 +1,5 @@ """API for Lokalise.""" + from __future__ import annotations from pprint import pprint diff --git a/script/translations/migrate.py b/script/translations/migrate.py index c3057800973..0f51e49c5a9 100644 --- a/script/translations/migrate.py +++ b/script/translations/migrate.py @@ -1,4 +1,5 @@ """Migrate things.""" + import json import pathlib from pprint import pprint @@ -268,9 +269,9 @@ def find_frontend_states(): for device_class, dev_class_states in domain_to_write.items(): to_device_class = "_" if device_class == "default" else device_class for key in dev_class_states: - to_migrate[ - f"{from_key_base}::{device_class}::{key}" - ] = f"{to_key_base}::{to_device_class}::{key}" + to_migrate[f"{from_key_base}::{device_class}::{key}"] = ( + f"{to_key_base}::{to_device_class}::{key}" + ) # Rewrite "default" device class to _ if "default" in domain_to_write: diff --git a/script/translations/upload.py b/script/translations/upload.py index eaf1c07ad91..ee4a57bc00a 100755 --- a/script/translations/upload.py +++ b/script/translations/upload.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 """Merge all translation sources into a single JSON file.""" + import json import os import pathlib diff --git a/script/translations/util.py b/script/translations/util.py index aab98e049d9..8892bb46b7a 100644 --- a/script/translations/util.py +++ b/script/translations/util.py @@ -1,4 +1,5 @@ """Translation utils.""" + import argparse import json import os diff --git a/script/version_bump.py b/script/version_bump.py index 3a6c0fa7540..6c24c40c4e3 100755 --- a/script/version_bump.py +++ b/script/version_bump.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 """Helper script to bump the current version.""" + import argparse import re import subprocess @@ -23,7 +24,9 @@ def _bump_release(release, bump_type): return major, minor, patch -def bump_version(version, bump_type): +def bump_version( + version: Version, bump_type: str, *, nightly_version: str | None = None +) -> Version: """Return a new version given a current version and action.""" to_change = {} @@ -82,14 +85,23 @@ def bump_version(version, bump_type): to_change["pre"] = ("b", 0) elif bump_type == "nightly": - # Convert 0.70.0d0 to 0.70.0d20190424, fails when run on non dev release + # Convert 0.70.0d0 to 0.70.0d201904241254, fails when run on non dev release if not version.is_devrelease: raise ValueError("Can only be run on dev release") - to_change["dev"] = ("dev", dt_util.utcnow().strftime("%Y%m%d")) + new_dev = dt_util.utcnow().strftime("%Y%m%d%H%M") + if nightly_version: + new_version = Version(nightly_version) + if new_version.release != version.release: + raise ValueError("Nightly version must have the same release version") + if not new_version.is_devrelease: + raise ValueError("Nightly version must be a dev version") + new_dev = new_version.dev + + to_change["dev"] = ("dev", new_dev) else: - assert False, f"Unsupported type: {bump_type}" + raise ValueError(f"Unsupported type: {bump_type}") temp = Version("0") temp._version = version._version._replace(**to_change) @@ -145,7 +157,7 @@ def write_ci_workflow(version: Version) -> None: fp.write(content) -def main(): +def main() -> None: """Execute script.""" parser = argparse.ArgumentParser(description="Bump version of Home Assistant") parser.add_argument( @@ -156,8 +168,15 @@ def main(): parser.add_argument( "--commit", action="store_true", help="Create a version bump commit." ) + parser.add_argument( + "--set-nightly-version", help="Set the nightly version to", type=str + ) + arguments = parser.parse_args() + if arguments.set_nightly_version and arguments.type != "nightly": + parser.error("--set-nightly-version requires type set to nightly.") + if ( arguments.commit and subprocess.run(["git", "diff", "--quiet"], check=False).returncode == 1 @@ -166,7 +185,9 @@ def main(): return current = Version(const.__version__) - bumped = bump_version(current, arguments.type) + bumped = bump_version( + current, arguments.type, nightly_version=arguments.set_nightly_version + ) assert bumped > current, "BUG! New version is not newer than old version" write_version(bumped) @@ -180,7 +201,7 @@ def main(): subprocess.run(["git", "commit", "-nam", f"Bump version to {bumped}"], check=True) -def test_bump_version(): +def test_bump_version() -> None: """Make sure it all works.""" import pytest @@ -203,12 +224,27 @@ def test_bump_version(): assert bump_version(Version("0.56.0.dev0"), "minor") == Version("0.56.0") assert bump_version(Version("0.56.2.dev0"), "minor") == Version("0.57.0") - today = dt_util.utcnow().strftime("%Y%m%d") + now = dt_util.utcnow().strftime("%Y%m%d%H%M") assert bump_version(Version("0.56.0.dev0"), "nightly") == Version( - f"0.56.0.dev{today}" + f"0.56.0.dev{now}" ) - with pytest.raises(ValueError): - assert bump_version(Version("0.56.0"), "nightly") + assert bump_version( + Version("2024.4.0.dev20240327"), + "nightly", + nightly_version="2024.4.0.dev202403271315", + ) == Version("2024.4.0.dev202403271315") + with pytest.raises(ValueError, match="Can only be run on dev release"): + bump_version(Version("0.56.0"), "nightly") + with pytest.raises( + ValueError, match="Nightly version must have the same release version" + ): + bump_version( + Version("0.56.0.dev0"), + "nightly", + nightly_version="2024.4.0.dev202403271315", + ) + with pytest.raises(ValueError, match="Nightly version must be a dev version"): + bump_version(Version("0.56.0.dev0"), "nightly", nightly_version="0.56.0") if __name__ == "__main__": diff --git a/tests/auth/mfa_modules/test_insecure_example.py b/tests/auth/mfa_modules/test_insecure_example.py index d22bfaa0719..f7f8a327059 100644 --- a/tests/auth/mfa_modules/test_insecure_example.py +++ b/tests/auth/mfa_modules/test_insecure_example.py @@ -1,4 +1,5 @@ """Test the example module auth module.""" + from homeassistant import auth, data_entry_flow from homeassistant.auth.mfa_modules import auth_mfa_module_from_config from homeassistant.auth.models import Credentials diff --git a/tests/auth/mfa_modules/test_notify.py b/tests/auth/mfa_modules/test_notify.py index d5b7e0bbc62..23b8811dbf9 100644 --- a/tests/auth/mfa_modules/test_notify.py +++ b/tests/auth/mfa_modules/test_notify.py @@ -1,4 +1,5 @@ """Test the HMAC-based One Time Password (MFA) auth module.""" + import asyncio from unittest.mock import patch @@ -182,8 +183,9 @@ async def test_login_flow_validates_mfa(hass: HomeAssistant) -> None: assert len(notify_calls) == 1 # retry twice - with patch("pyotp.HOTP.verify", return_value=False), patch( - "pyotp.HOTP.at", return_value=MOCK_CODE_2 + with ( + patch("pyotp.HOTP.verify", return_value=False), + patch("pyotp.HOTP.at", return_value=MOCK_CODE_2), ): result = await hass.auth.login_flow.async_configure( result["flow_id"], {"code": "invalid-code"} diff --git a/tests/auth/mfa_modules/test_totp.py b/tests/auth/mfa_modules/test_totp.py index e5b1a930dd7..961db3f44ca 100644 --- a/tests/auth/mfa_modules/test_totp.py +++ b/tests/auth/mfa_modules/test_totp.py @@ -1,4 +1,5 @@ """Test the Time-based One Time Password (MFA) auth module.""" + import asyncio from unittest.mock import patch diff --git a/tests/auth/permissions/test_entities.py b/tests/auth/permissions/test_entities.py index ce539d51dde..7f5355b3cc0 100644 --- a/tests/auth/permissions/test_entities.py +++ b/tests/auth/permissions/test_entities.py @@ -1,4 +1,5 @@ """Tests for entity permissions.""" + import pytest import voluptuous as vol diff --git a/tests/auth/permissions/test_merge.py b/tests/auth/permissions/test_merge.py index f6251e52998..aeba076b170 100644 --- a/tests/auth/permissions/test_merge.py +++ b/tests/auth/permissions/test_merge.py @@ -1,4 +1,5 @@ """Tests for permissions merging.""" + from homeassistant.auth.permissions.merge import merge_policies diff --git a/tests/auth/permissions/test_system_policies.py b/tests/auth/permissions/test_system_policies.py index c6d9202524b..81b53154485 100644 --- a/tests/auth/permissions/test_system_policies.py +++ b/tests/auth/permissions/test_system_policies.py @@ -1,4 +1,5 @@ """Test system policies.""" + from homeassistant.auth.permissions import ( POLICY_SCHEMA, PolicyPermissions, diff --git a/tests/auth/providers/test_homeassistant.py b/tests/auth/providers/test_homeassistant.py index 222911a61c2..dc5c255579c 100644 --- a/tests/auth/providers/test_homeassistant.py +++ b/tests/auth/providers/test_homeassistant.py @@ -1,4 +1,5 @@ """Test the Home Assistant local auth provider.""" + import asyncio from unittest.mock import Mock, patch @@ -40,6 +41,7 @@ async def test_validating_password_invalid_user(data, hass: HomeAssistant) -> No async def test_not_allow_set_id() -> None: """Test we are not allowed to set an ID in config.""" hass = Mock() + hass.data = {} with pytest.raises(vol.Invalid): await auth_provider_from_config( hass, None, {"type": "homeassistant", "id": "invalid"} diff --git a/tests/auth/providers/test_insecure_example.py b/tests/auth/providers/test_insecure_example.py index ceb8b02ae65..f0043231c04 100644 --- a/tests/auth/providers/test_insecure_example.py +++ b/tests/auth/providers/test_insecure_example.py @@ -1,4 +1,5 @@ """Tests for the insecure example auth provider.""" + from unittest.mock import AsyncMock import uuid diff --git a/tests/auth/providers/test_legacy_api_password.py b/tests/auth/providers/test_legacy_api_password.py index 75c4f733285..9f1f98aeaf0 100644 --- a/tests/auth/providers/test_legacy_api_password.py +++ b/tests/auth/providers/test_legacy_api_password.py @@ -1,4 +1,5 @@ """Tests for the legacy_api_password auth provider.""" + import pytest from homeassistant import auth, data_entry_flow diff --git a/tests/auth/providers/test_trusted_networks.py b/tests/auth/providers/test_trusted_networks.py index 3ccff990b9c..2f84a256f2d 100644 --- a/tests/auth/providers/test_trusted_networks.py +++ b/tests/auth/providers/test_trusted_networks.py @@ -1,4 +1,5 @@ """Test the Trusted Networks auth provider.""" + from ipaddress import ip_address, ip_network from unittest.mock import Mock, patch diff --git a/tests/auth/test_auth_store.py b/tests/auth/test_auth_store.py index 858d4d082b1..3d62190eab6 100644 --- a/tests/auth/test_auth_store.py +++ b/tests/auth/test_auth_store.py @@ -1,75 +1,79 @@ """Tests for the auth store.""" + import asyncio from datetime import timedelta from typing import Any from unittest.mock import patch from freezegun import freeze_time +from freezegun.api import FrozenDateTimeFactory import pytest from homeassistant.auth import auth_store from homeassistant.core import HomeAssistant from homeassistant.util import dt as dt_util +MOCK_STORAGE_DATA = { + "version": 1, + "data": { + "credentials": [], + "users": [ + { + "id": "user-id", + "is_active": True, + "is_owner": True, + "name": "Paulus", + "system_generated": False, + }, + { + "id": "system-id", + "is_active": True, + "is_owner": True, + "name": "Hass.io", + "system_generated": True, + }, + ], + "refresh_tokens": [ + { + "access_token_expiration": 1800.0, + "client_id": "http://localhost:8123/", + "created_at": "2018-10-03T13:43:19.774637+00:00", + "id": "user-token-id", + "jwt_key": "some-key", + "last_used_at": "2018-10-03T13:43:19.774712+00:00", + "token": "some-token", + "user_id": "user-id", + "version": "1.2.3", + }, + { + "access_token_expiration": 1800.0, + "client_id": None, + "created_at": "2018-10-03T13:43:19.774637+00:00", + "id": "system-token-id", + "jwt_key": "some-key", + "last_used_at": "2018-10-03T13:43:19.774712+00:00", + "token": "some-token", + "user_id": "system-id", + }, + { + "access_token_expiration": 1800.0, + "client_id": "http://localhost:8123/", + "created_at": "2018-10-03T13:43:19.774637+00:00", + "id": "hidden-because-no-jwt-id", + "last_used_at": "2018-10-03T13:43:19.774712+00:00", + "token": "some-token", + "user_id": "user-id", + }, + ], + }, +} + async def test_loading_no_group_data_format( hass: HomeAssistant, hass_storage: dict[str, Any] ) -> None: """Test we correctly load old data without any groups.""" - hass_storage[auth_store.STORAGE_KEY] = { - "version": 1, - "data": { - "credentials": [], - "users": [ - { - "id": "user-id", - "is_active": True, - "is_owner": True, - "name": "Paulus", - "system_generated": False, - }, - { - "id": "system-id", - "is_active": True, - "is_owner": True, - "name": "Hass.io", - "system_generated": True, - }, - ], - "refresh_tokens": [ - { - "access_token_expiration": 1800.0, - "client_id": "http://localhost:8123/", - "created_at": "2018-10-03T13:43:19.774637+00:00", - "id": "user-token-id", - "jwt_key": "some-key", - "last_used_at": "2018-10-03T13:43:19.774712+00:00", - "token": "some-token", - "user_id": "user-id", - "version": "1.2.3", - }, - { - "access_token_expiration": 1800.0, - "client_id": None, - "created_at": "2018-10-03T13:43:19.774637+00:00", - "id": "system-token-id", - "jwt_key": "some-key", - "last_used_at": "2018-10-03T13:43:19.774712+00:00", - "token": "some-token", - "user_id": "system-id", - }, - { - "access_token_expiration": 1800.0, - "client_id": "http://localhost:8123/", - "created_at": "2018-10-03T13:43:19.774637+00:00", - "id": "hidden-because-no-jwt-id", - "last_used_at": "2018-10-03T13:43:19.774712+00:00", - "token": "some-token", - "user_id": "user-id", - }, - ], - }, - } + hass_storage[auth_store.STORAGE_KEY] = MOCK_STORAGE_DATA store = auth_store.AuthStore(hass) await store.async_load() @@ -112,63 +116,7 @@ async def test_loading_all_access_group_data_format( hass: HomeAssistant, hass_storage: dict[str, Any] ) -> None: """Test we correctly load old data with single group.""" - hass_storage[auth_store.STORAGE_KEY] = { - "version": 1, - "data": { - "credentials": [], - "users": [ - { - "id": "user-id", - "is_active": True, - "is_owner": True, - "name": "Paulus", - "system_generated": False, - "group_ids": ["abcd-all-access"], - }, - { - "id": "system-id", - "is_active": True, - "is_owner": True, - "name": "Hass.io", - "system_generated": True, - }, - ], - "groups": [{"id": "abcd-all-access", "name": "All Access"}], - "refresh_tokens": [ - { - "access_token_expiration": 1800.0, - "client_id": "http://localhost:8123/", - "created_at": "2018-10-03T13:43:19.774637+00:00", - "id": "user-token-id", - "jwt_key": "some-key", - "last_used_at": "2018-10-03T13:43:19.774712+00:00", - "token": "some-token", - "user_id": "user-id", - "version": "1.2.3", - }, - { - "access_token_expiration": 1800.0, - "client_id": None, - "created_at": "2018-10-03T13:43:19.774637+00:00", - "id": "system-token-id", - "jwt_key": "some-key", - "last_used_at": "2018-10-03T13:43:19.774712+00:00", - "token": "some-token", - "user_id": "system-id", - "version": None, - }, - { - "access_token_expiration": 1800.0, - "client_id": "http://localhost:8123/", - "created_at": "2018-10-03T13:43:19.774637+00:00", - "id": "hidden-because-no-jwt-id", - "last_used_at": "2018-10-03T13:43:19.774712+00:00", - "token": "some-token", - "user_id": "user-id", - }, - ], - }, - } + hass_storage[auth_store.STORAGE_KEY] = MOCK_STORAGE_DATA store = auth_store.AuthStore(hass) await store.async_load() @@ -253,13 +201,13 @@ async def test_system_groups_store_id_and_name( async def test_loading_only_once(hass: HomeAssistant) -> None: """Test only one storage load is allowed.""" store = auth_store.AuthStore(hass) - with patch( - "homeassistant.helpers.entity_registry.async_get" - ) as mock_ent_registry, patch( - "homeassistant.helpers.device_registry.async_get" - ) as mock_dev_registry, patch( - "homeassistant.helpers.storage.Store.async_load", return_value=None - ) as mock_load: + with ( + patch("homeassistant.helpers.entity_registry.async_get") as mock_ent_registry, + patch("homeassistant.helpers.device_registry.async_get") as mock_dev_registry, + patch( + "homeassistant.helpers.storage.Store.async_load", return_value=None + ) as mock_load, + ): await store.async_load() with pytest.raises(RuntimeError, match="Auth storage is already loaded"): await store.async_load() @@ -334,3 +282,26 @@ async def test_add_expire_at_property( assert token1.expire_at == now.timestamp() + timedelta(days=80).total_seconds() assert token2.expire_at assert token2.expire_at == now.timestamp() + timedelta(days=90).total_seconds() + + +async def test_loading_does_not_write_right_away( + hass: HomeAssistant, hass_storage: dict[str, Any], freezer: FrozenDateTimeFactory +) -> None: + """Test after calling load we wait five minutes to write.""" + hass_storage[auth_store.STORAGE_KEY] = MOCK_STORAGE_DATA + + store = auth_store.AuthStore(hass) + await store.async_load() + + # Wipe storage so we can verify if it was written + hass_storage[auth_store.STORAGE_KEY] = {} + + freezer.tick(auth_store.DEFAULT_SAVE_DELAY) + await hass.async_block_till_done() + assert hass_storage[auth_store.STORAGE_KEY] == {} + freezer.tick(auth_store.INITIAL_LOAD_SAVE_DELAY) + # Once for scheduling the task + await hass.async_block_till_done() + # Once for the task + await hass.async_block_till_done() + assert hass_storage[auth_store.STORAGE_KEY] != {} diff --git a/tests/auth/test_init.py b/tests/auth/test_init.py index b561b17112b..4cf6b2cc5f7 100644 --- a/tests/auth/test_init.py +++ b/tests/auth/test_init.py @@ -1,4 +1,5 @@ """Tests for the Home Assistant auth module.""" + from datetime import timedelta import time from typing import Any @@ -555,10 +556,13 @@ async def test_refresh_token_provider_validation(mock_hass) -> None: assert manager.async_create_access_token(refresh_token, ip) is not None - with patch( - "homeassistant.auth.providers.insecure_example.ExampleAuthProvider.async_validate_refresh_token", - side_effect=InvalidAuthError("Invalid access"), - ) as call, pytest.raises(InvalidAuthError): + with ( + patch( + "homeassistant.auth.providers.insecure_example.ExampleAuthProvider.async_validate_refresh_token", + side_effect=InvalidAuthError("Invalid access"), + ) as call, + pytest.raises(InvalidAuthError), + ): manager.async_create_access_token(refresh_token, ip) call.assert_called_with(refresh_token, ip) @@ -1102,9 +1106,10 @@ async def test_async_remove_user_fail_if_remove_credential_fails( """Test removing a user.""" await hass.auth.async_link_user(hass_admin_user, hass_admin_credential) - with patch.object( - hass.auth, "async_remove_credentials", side_effect=ValueError - ), pytest.raises(ValueError): + with ( + patch.object(hass.auth, "async_remove_credentials", side_effect=ValueError), + pytest.raises(ValueError), + ): await hass.auth.async_remove_user(hass_admin_user) diff --git a/tests/auth/test_models.py b/tests/auth/test_models.py index 3f0ad7acc1d..639e43287b2 100644 --- a/tests/auth/test_models.py +++ b/tests/auth/test_models.py @@ -1,4 +1,5 @@ """Tests for the auth models.""" + from homeassistant.auth import models, permissions diff --git a/tests/common.py b/tests/common.py index 14cacdf5d68..0ac0ee4556b 100644 --- a/tests/common.py +++ b/tests/common.py @@ -1,4 +1,5 @@ """Test the helper method for writing tests.""" + from __future__ import annotations import asyncio @@ -16,7 +17,7 @@ import os import pathlib import threading import time -from types import ModuleType +from types import FrameType, ModuleType from typing import Any, NoReturn, TypeVar from unittest.mock import AsyncMock, Mock, patch @@ -37,7 +38,7 @@ from homeassistant.components.device_automation import ( # noqa: F401 _async_get_device_automation_capabilities as async_get_device_automation_capabilities, ) from homeassistant.config import async_process_component_config -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigEntry, ConfigFlow from homeassistant.const import ( DEVICE_DEFAULT_NAME, EVENT_HOMEASSISTANT_CLOSE, @@ -58,6 +59,7 @@ from homeassistant.core import ( ) from homeassistant.helpers import ( area_registry as ar, + category_registry as cr, device_registry as dr, entity, entity_platform, @@ -74,11 +76,14 @@ from homeassistant.helpers import ( translation, ) from homeassistant.helpers.dispatcher import ( + SignalType, async_dispatcher_connect, async_dispatcher_send, ) +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.json import JSONEncoder, _orjson_default_encoder, json_dumps -from homeassistant.helpers.typing import ConfigType, StateType +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.setup import setup_component from homeassistant.util.async_ import run_callback_threadsafe import homeassistant.util.dt as dt_util @@ -218,12 +223,10 @@ class StoreWithoutWriteLoad(storage.Store[_T]): async def async_test_home_assistant( event_loop: asyncio.AbstractEventLoop | None = None, load_registries: bool = True, - storage_dir: str | None = None, + config_dir: str | None = None, ) -> AsyncGenerator[HomeAssistant, None]: """Return a Home Assistant object pointing at test config dir.""" - hass = HomeAssistant(get_test_config_dir()) - if storage_dir: - hass.config.config_dir = storage_dir + hass = HomeAssistant(config_dir or get_test_config_dir()) store = auth_store.AuthStore(hass) hass.auth = auth.AuthManager(hass, store, {}, {}) ensure_auth_manager_loaded(hass.auth) @@ -234,7 +237,7 @@ async def async_test_home_assistant( orig_async_create_task = hass.async_create_task orig_tz = dt_util.DEFAULT_TIME_ZONE - def async_add_job(target, *args): + def async_add_job(target, *args, eager_start: bool = False): """Add job.""" check_target = target while isinstance(check_target, ft.partial): @@ -245,7 +248,7 @@ async def async_test_home_assistant( fut.set_result(target(*args)) return fut - return orig_async_add_job(target, *args) + return orig_async_add_job(target, *args, eager_start=eager_start) def async_add_executor_job(target, *args): """Add executor job.""" @@ -295,7 +298,9 @@ async def async_test_home_assistant( }, ) hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, hass.config_entries._async_shutdown + EVENT_HOMEASSISTANT_STOP, + hass.config_entries._async_shutdown, + run_immediately=True, ) # Load the registries @@ -307,30 +312,38 @@ async def async_test_home_assistant( hass ) if load_registries: - with patch.object( - StoreWithoutWriteLoad, "async_load", return_value=None - ), patch( - "homeassistant.helpers.area_registry.AreaRegistryStore", - StoreWithoutWriteLoad, - ), patch( - "homeassistant.helpers.device_registry.DeviceRegistryStore", - StoreWithoutWriteLoad, - ), patch( - "homeassistant.helpers.entity_registry.EntityRegistryStore", - StoreWithoutWriteLoad, - ), patch( - "homeassistant.helpers.storage.Store", # Floor & label registry are different - StoreWithoutWriteLoad, - ), patch( - "homeassistant.helpers.issue_registry.IssueRegistryStore", - StoreWithoutWriteLoad, - ), patch( - "homeassistant.helpers.restore_state.RestoreStateData.async_setup_dump", - return_value=None, - ), patch( - "homeassistant.helpers.restore_state.start.async_at_start", + with ( + patch.object(StoreWithoutWriteLoad, "async_load", return_value=None), + patch( + "homeassistant.helpers.area_registry.AreaRegistryStore", + StoreWithoutWriteLoad, + ), + patch( + "homeassistant.helpers.device_registry.DeviceRegistryStore", + StoreWithoutWriteLoad, + ), + patch( + "homeassistant.helpers.entity_registry.EntityRegistryStore", + StoreWithoutWriteLoad, + ), + patch( + "homeassistant.helpers.storage.Store", # Floor & label registry are different + StoreWithoutWriteLoad, + ), + patch( + "homeassistant.helpers.issue_registry.IssueRegistryStore", + StoreWithoutWriteLoad, + ), + patch( + "homeassistant.helpers.restore_state.RestoreStateData.async_setup_dump", + return_value=None, + ), + patch( + "homeassistant.helpers.restore_state.start.async_at_start", + ), ): await ar.async_load(hass) + await cr.async_load(hass) await dr.async_load(hass) await er.async_load(hass) await fr.async_load(hass) @@ -510,12 +523,15 @@ def _async_fire_time_changed( future_seconds = task.when() - (hass.loop.time() + _MONOTONIC_RESOLUTION) if fire_all or mock_seconds_into_future >= future_seconds: - with patch( - "homeassistant.helpers.event.time_tracker_utcnow", - return_value=utc_datetime, - ), patch( - "homeassistant.helpers.event.time_tracker_timestamp", - return_value=timestamp, + with ( + patch( + "homeassistant.helpers.event.time_tracker_utcnow", + return_value=utc_datetime, + ), + patch( + "homeassistant.helpers.event.time_tracker_timestamp", + return_value=timestamp, + ), ): task._run() task.cancel() @@ -899,7 +915,9 @@ class MockEntityPlatform(entity_platform.EntityPlatform): def _async_on_stop(_: Event) -> None: self.async_shutdown() - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_on_stop) + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, _async_on_stop, run_immediately=True + ) class MockToggleEntity(entity.ToggleEntity): @@ -1075,11 +1093,11 @@ def assert_setup_component(count, domain=None): """ config = {} - async def mock_psc(hass, config_input, integration): + async def mock_psc(hass, config_input, integration, component=None): """Mock the prepare_setup_component to capture config.""" domain_input = integration.domain integration_config_info = await async_process_component_config( - hass, config_input, integration + hass, config_input, integration, component ) res = integration_config_info.config config[domain_input] = None if res is None else res.get(domain_input) @@ -1096,9 +1114,9 @@ def assert_setup_component(count, domain=None): yield config if domain is None: - assert len(config) == 1, "assert_setup_component requires DOMAIN: {}".format( - list(config.keys()) - ) + assert ( + len(config) == 1 + ), f"assert_setup_component requires DOMAIN: {list(config.keys())}" domain = list(config.keys())[0] res = config.get(domain) @@ -1273,11 +1291,6 @@ class MockEntity(entity.Entity): """Return the ste of the polling.""" return self._handle("should_poll") - @property - def state(self) -> StateType: - """Return the state of the entity.""" - return self._handle("state") - @property def supported_features(self) -> int | None: """Info about supported features.""" @@ -1351,6 +1364,10 @@ def mock_storage( # To ensure that the data can be serialized _LOGGER.debug("Writing data to %s: %s", store.key, data_to_write) raise_contains_mocks(data_to_write) + + if "data_func" in data_to_write: + data_to_write["data"] = data_to_write.pop("data_func")() + encoder = store._encoder if encoder and encoder is not JSONEncoder: # If they pass a custom encoder that is not the @@ -1358,24 +1375,28 @@ def mock_storage( dump = ft.partial(json.dumps, cls=store._encoder) else: dump = _orjson_default_encoder - data[store.key] = json.loads(dump(data_to_write)) + data[store.key] = json_loads(dump(data_to_write)) async def mock_remove(store: storage.Store) -> None: """Remove data.""" data.pop(store.key, None) - with patch( - "homeassistant.helpers.storage.Store._async_load", - side_effect=mock_async_load, - autospec=True, - ), patch( - "homeassistant.helpers.storage.Store._async_write_data", - side_effect=mock_write_data, - autospec=True, - ), patch( - "homeassistant.helpers.storage.Store.async_remove", - side_effect=mock_remove, - autospec=True, + with ( + patch( + "homeassistant.helpers.storage.Store._async_load", + side_effect=mock_async_load, + autospec=True, + ), + patch( + "homeassistant.helpers.storage.Store._async_write_data", + side_effect=mock_write_data, + autospec=True, + ), + patch( + "homeassistant.helpers.storage.Store.async_remove", + side_effect=mock_remove, + autospec=True, + ), ): yield data @@ -1418,6 +1439,7 @@ def mock_integration( else f"{loader.PACKAGE_CUSTOM_COMPONENTS}.{module.DOMAIN}", pathlib.Path(""), module.mock_manifest(), + set(), ) def mock_import_platform(platform_name: str) -> NoReturn: @@ -1439,19 +1461,23 @@ def mock_integration( def mock_platform( - hass: HomeAssistant, platform_path: str, module: Mock | MockPlatform | None = None + hass: HomeAssistant, + platform_path: str, + module: Mock | MockPlatform | None = None, + built_in=True, ) -> None: """Mock a platform. platform_path is in form hue.config_flow. """ - domain = platform_path.split(".")[0] + domain, _, platform_name = platform_path.partition(".") integration_cache = hass.data[loader.DATA_INTEGRATIONS] module_cache = hass.data[loader.DATA_COMPONENTS] if domain not in integration_cache: - mock_integration(hass, MockModule(domain)) + mock_integration(hass, MockModule(domain), built_in=built_in) + integration_cache[domain]._top_level_files.add(f"{platform_name}.py") _LOGGER.info("Adding mock integration platform: %s", platform_path) module_cache[platform_path] = module or Mock() @@ -1470,7 +1496,9 @@ def async_capture_events(hass: HomeAssistant, event_name: str) -> list[Event]: @callback -def async_mock_signal(hass: HomeAssistant, signal: str) -> list[tuple[Any]]: +def async_mock_signal( + hass: HomeAssistant, signal: SignalType[Any] | str +) -> list[tuple[Any]]: """Catch all dispatches to a signal.""" calls = [] @@ -1616,3 +1644,60 @@ def help_test_all(module: ModuleType) -> None: assert set(module.__all__) == { itm for itm in module.__dir__() if not itm.startswith("_") } + + +def extract_stack_to_frame(extract_stack: list[Mock]) -> FrameType: + """Convert an extract stack to a frame list.""" + stack = list(extract_stack) + for frame in stack: + frame.f_back = None + frame.f_code.co_filename = frame.filename + frame.f_lineno = int(frame.lineno) + + top_frame = stack.pop() + current_frame = top_frame + while stack and (next_frame := stack.pop()): + current_frame.f_back = next_frame + current_frame = next_frame + + return top_frame + + +def setup_test_component_platform( + hass: HomeAssistant, + domain: str, + entities: Sequence[Entity], + from_config_entry: bool = False, + built_in: bool = True, +) -> MockPlatform: + """Mock a test component platform for tests.""" + + async def _async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, + ) -> None: + """Set up a test component platform.""" + async_add_entities(entities) + + platform = MockPlatform( + async_setup_platform=_async_setup_platform, + ) + + # avoid creating config entry setup if not needed + if from_config_entry: + + async def _async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, + ) -> None: + """Set up a test component platform.""" + async_add_entities(entities) + + platform.async_setup_entry = _async_setup_entry + platform.async_setup_platform = None + + mock_platform(hass, f"test.{domain}", platform, built_in=built_in) + return platform diff --git a/tests/components/abode/common.py b/tests/components/abode/common.py index f9ae52a2709..22ee95cfa57 100644 --- a/tests/components/abode/common.py +++ b/tests/components/abode/common.py @@ -1,4 +1,5 @@ """Common methods used across tests for Abode.""" + from unittest.mock import patch from homeassistant.components.abode import DOMAIN as ABODE_DOMAIN @@ -22,8 +23,9 @@ async def setup_platform(hass: HomeAssistant, platform: str) -> MockConfigEntry: ) mock_entry.add_to_hass(hass) - with patch("homeassistant.components.abode.PLATFORMS", [platform]), patch( - "jaraco.abode.event_controller.sio" + with ( + patch("homeassistant.components.abode.PLATFORMS", [platform]), + patch("jaraco.abode.event_controller.sio"), ): assert await async_setup_component(hass, ABODE_DOMAIN, {}) await hass.async_block_till_done() diff --git a/tests/components/abode/conftest.py b/tests/components/abode/conftest.py index 1f9ff37ecf1..0e5e24b24f4 100644 --- a/tests/components/abode/conftest.py +++ b/tests/components/abode/conftest.py @@ -1,4 +1,5 @@ """Configuration for Abode tests.""" + from collections.abc import Generator from unittest.mock import AsyncMock, patch diff --git a/tests/components/abode/test_alarm_control_panel.py b/tests/components/abode/test_alarm_control_panel.py index c5500717c5a..428e2791ee2 100644 --- a/tests/components/abode/test_alarm_control_panel.py +++ b/tests/components/abode/test_alarm_control_panel.py @@ -1,4 +1,5 @@ """Tests for the Abode alarm control panel device.""" + from unittest.mock import PropertyMock, patch from jaraco.abode.helpers import constants as CONST diff --git a/tests/components/abode/test_binary_sensor.py b/tests/components/abode/test_binary_sensor.py index 987eea7d891..1fcc032c48d 100644 --- a/tests/components/abode/test_binary_sensor.py +++ b/tests/components/abode/test_binary_sensor.py @@ -1,4 +1,5 @@ """Tests for the Abode binary sensor device.""" + from homeassistant.components.abode import ATTR_DEVICE_ID from homeassistant.components.abode.const import ATTRIBUTION from homeassistant.components.binary_sensor import ( diff --git a/tests/components/abode/test_camera.py b/tests/components/abode/test_camera.py index d0c47eff045..5cf3263876b 100644 --- a/tests/components/abode/test_camera.py +++ b/tests/components/abode/test_camera.py @@ -1,4 +1,5 @@ """Tests for the Abode camera device.""" + from unittest.mock import patch from homeassistant.components.abode.const import DOMAIN as ABODE_DOMAIN diff --git a/tests/components/abode/test_config_flow.py b/tests/components/abode/test_config_flow.py index 7619cf9325d..2f73ee052c1 100644 --- a/tests/components/abode/test_config_flow.py +++ b/tests/components/abode/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for the Abode config flow.""" + from http import HTTPStatus from unittest.mock import patch diff --git a/tests/components/abode/test_cover.py b/tests/components/abode/test_cover.py index bc3abd32cd1..cdbec0ddf68 100644 --- a/tests/components/abode/test_cover.py +++ b/tests/components/abode/test_cover.py @@ -1,4 +1,5 @@ """Tests for the Abode cover device.""" + from unittest.mock import patch from homeassistant.components.abode import ATTR_DEVICE_ID diff --git a/tests/components/abode/test_init.py b/tests/components/abode/test_init.py index ae7ed51e086..e6e5da35a5e 100644 --- a/tests/components/abode/test_init.py +++ b/tests/components/abode/test_init.py @@ -1,4 +1,5 @@ """Tests for the Abode module.""" + from http import HTTPStatus from unittest.mock import patch @@ -56,9 +57,10 @@ async def test_unload_entry(hass: HomeAssistant) -> None: """Test unloading the Abode entry.""" mock_entry = await setup_platform(hass, ALARM_DOMAIN) - with patch("jaraco.abode.client.Client.logout") as mock_logout, patch( - "jaraco.abode.event_controller.EventController.stop" - ) as mock_events_stop: + with ( + patch("jaraco.abode.client.Client.logout") as mock_logout, + patch("jaraco.abode.event_controller.EventController.stop") as mock_events_stop, + ): assert await hass.config_entries.async_unload(mock_entry.entry_id) mock_logout.assert_called_once() mock_events_stop.assert_called_once() @@ -70,19 +72,22 @@ async def test_unload_entry(hass: HomeAssistant) -> None: async def test_invalid_credentials(hass: HomeAssistant) -> None: """Test Abode credentials changing.""" - with patch( - "homeassistant.components.abode.Abode", - side_effect=AbodeAuthenticationException( - (HTTPStatus.BAD_REQUEST, "auth error") + with ( + patch( + "homeassistant.components.abode.Abode", + side_effect=AbodeAuthenticationException( + (HTTPStatus.BAD_REQUEST, "auth error") + ), ), - ), patch( - "homeassistant.components.abode.config_flow.AbodeFlowHandler.async_step_reauth", - return_value={ - "type": data_entry_flow.FlowResultType.FORM, - "flow_id": "mock_flow", - "step_id": "reauth_confirm", - }, - ) as mock_async_step_reauth: + patch( + "homeassistant.components.abode.config_flow.AbodeFlowHandler.async_step_reauth", + return_value={ + "type": data_entry_flow.FlowResultType.FORM, + "flow_id": "mock_flow", + "step_id": "reauth_confirm", + }, + ) as mock_async_step_reauth, + ): await setup_platform(hass, ALARM_DOMAIN) mock_async_step_reauth.assert_called_once() diff --git a/tests/components/abode/test_light.py b/tests/components/abode/test_light.py index d7fd719a2b9..fc9000a39f8 100644 --- a/tests/components/abode/test_light.py +++ b/tests/components/abode/test_light.py @@ -1,4 +1,5 @@ """Tests for the Abode light device.""" + from unittest.mock import patch from homeassistant.components.abode import ATTR_DEVICE_ID diff --git a/tests/components/abode/test_lock.py b/tests/components/abode/test_lock.py index ac988a1ee12..6be1aef22ca 100644 --- a/tests/components/abode/test_lock.py +++ b/tests/components/abode/test_lock.py @@ -1,4 +1,5 @@ """Tests for the Abode lock device.""" + from unittest.mock import patch from homeassistant.components.abode import ATTR_DEVICE_ID diff --git a/tests/components/abode/test_sensor.py b/tests/components/abode/test_sensor.py index 9f4b3374fc2..e92748bb162 100644 --- a/tests/components/abode/test_sensor.py +++ b/tests/components/abode/test_sensor.py @@ -1,4 +1,5 @@ """Tests for the Abode sensor device.""" + from homeassistant.components.abode import ATTR_DEVICE_ID from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorDeviceClass from homeassistant.const import ( diff --git a/tests/components/abode/test_switch.py b/tests/components/abode/test_switch.py index b5b93d05481..9f8e4d3205b 100644 --- a/tests/components/abode/test_switch.py +++ b/tests/components/abode/test_switch.py @@ -1,4 +1,5 @@ """Tests for the Abode switch device.""" + from unittest.mock import patch from homeassistant.components.abode import ( diff --git a/tests/components/accuweather/__init__.py b/tests/components/accuweather/__init__.py index 3c7f81450c6..afaa5bbef25 100644 --- a/tests/components/accuweather/__init__.py +++ b/tests/components/accuweather/__init__.py @@ -1,4 +1,5 @@ """Tests for AccuWeather.""" + from unittest.mock import PropertyMock, patch from homeassistant.components.accuweather.const import DOMAIN @@ -37,16 +38,20 @@ async def init_integration( if unsupported_icon: current["WeatherIcon"] = 999 - with patch( - "homeassistant.components.accuweather.AccuWeather.async_get_current_conditions", - return_value=current, - ), patch( - "homeassistant.components.accuweather.AccuWeather.async_get_daily_forecast", - return_value=forecast, - ), patch( - "homeassistant.components.accuweather.AccuWeather.requests_remaining", - new_callable=PropertyMock, - return_value=10, + with ( + patch( + "homeassistant.components.accuweather.AccuWeather.async_get_current_conditions", + return_value=current, + ), + patch( + "homeassistant.components.accuweather.AccuWeather.async_get_daily_forecast", + return_value=forecast, + ), + patch( + "homeassistant.components.accuweather.AccuWeather.requests_remaining", + new_callable=PropertyMock, + return_value=10, + ), ): entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) diff --git a/tests/components/accuweather/test_config_flow.py b/tests/components/accuweather/test_config_flow.py index 35b6e095c0f..c9d95c34b7c 100644 --- a/tests/components/accuweather/test_config_flow.py +++ b/tests/components/accuweather/test_config_flow.py @@ -1,4 +1,5 @@ """Define tests for the AccuWeather config flow.""" + from unittest.mock import PropertyMock, patch from accuweather import ApiError, InvalidApiKeyError, RequestsExceededError @@ -118,11 +119,14 @@ async def test_integration_already_exists(hass: HomeAssistant) -> None: async def test_create_entry(hass: HomeAssistant) -> None: """Test that the user step works.""" - with patch( - "homeassistant.components.accuweather.AccuWeather._async_get_data", - return_value=load_json_object_fixture("accuweather/location_data.json"), - ), patch( - "homeassistant.components.accuweather.async_setup_entry", return_value=True + with ( + patch( + "homeassistant.components.accuweather.AccuWeather._async_get_data", + return_value=load_json_object_fixture("accuweather/location_data.json"), + ), + patch( + "homeassistant.components.accuweather.async_setup_entry", return_value=True + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -147,20 +151,25 @@ async def test_options_flow(hass: HomeAssistant) -> None: ) config_entry.add_to_hass(hass) - with patch( - "homeassistant.components.accuweather.AccuWeather._async_get_data", - return_value=load_json_object_fixture("accuweather/location_data.json"), - ), patch( - "homeassistant.components.accuweather.AccuWeather.async_get_current_conditions", - return_value=load_json_object_fixture( - "accuweather/current_conditions_data.json" + with ( + patch( + "homeassistant.components.accuweather.AccuWeather._async_get_data", + return_value=load_json_object_fixture("accuweather/location_data.json"), + ), + patch( + "homeassistant.components.accuweather.AccuWeather.async_get_current_conditions", + return_value=load_json_object_fixture( + "accuweather/current_conditions_data.json" + ), + ), + patch( + "homeassistant.components.accuweather.AccuWeather.async_get_daily_forecast" + ), + patch( + "homeassistant.components.accuweather.AccuWeather.requests_remaining", + new_callable=PropertyMock, + return_value=10, ), - ), patch( - "homeassistant.components.accuweather.AccuWeather.async_get_daily_forecast" - ), patch( - "homeassistant.components.accuweather.AccuWeather.requests_remaining", - new_callable=PropertyMock, - return_value=10, ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/accuweather/test_diagnostics.py b/tests/components/accuweather/test_diagnostics.py index 7c13f318cc3..ab77fc337d0 100644 --- a/tests/components/accuweather/test_diagnostics.py +++ b/tests/components/accuweather/test_diagnostics.py @@ -1,4 +1,5 @@ """Test AccuWeather diagnostics.""" + from syrupy import SnapshotAssertion from homeassistant.core import HomeAssistant diff --git a/tests/components/accuweather/test_init.py b/tests/components/accuweather/test_init.py index 342cc2f5914..bb5b67e7918 100644 --- a/tests/components/accuweather/test_init.py +++ b/tests/components/accuweather/test_init.py @@ -1,4 +1,5 @@ """Test init of AccuWeather integration.""" + from datetime import timedelta from unittest.mock import patch @@ -100,13 +101,16 @@ async def test_update_interval_forecast(hass: HomeAssistant) -> None: forecast = load_json_array_fixture("accuweather/forecast_data.json") future = utcnow() + timedelta(minutes=80) - with patch( - "homeassistant.components.accuweather.AccuWeather.async_get_current_conditions", - return_value=current, - ) as mock_current, patch( - "homeassistant.components.accuweather.AccuWeather.async_get_daily_forecast", - return_value=forecast, - ) as mock_forecast: + with ( + patch( + "homeassistant.components.accuweather.AccuWeather.async_get_current_conditions", + return_value=current, + ) as mock_current, + patch( + "homeassistant.components.accuweather.AccuWeather.async_get_daily_forecast", + return_value=forecast, + ) as mock_forecast, + ): assert mock_current.call_count == 0 assert mock_forecast.call_count == 0 diff --git a/tests/components/accuweather/test_sensor.py b/tests/components/accuweather/test_sensor.py index eb5e26a8e20..8e6e01a4578 100644 --- a/tests/components/accuweather/test_sensor.py +++ b/tests/components/accuweather/test_sensor.py @@ -1,4 +1,5 @@ """Test sensor of AccuWeather integration.""" + from datetime import timedelta from unittest.mock import PropertyMock, patch @@ -576,15 +577,18 @@ async def test_availability(hass: HomeAssistant) -> None: assert state.state == STATE_UNAVAILABLE future = utcnow() + timedelta(minutes=120) - with patch( - "homeassistant.components.accuweather.AccuWeather.async_get_current_conditions", - return_value=load_json_object_fixture( - "accuweather/current_conditions_data.json" + with ( + patch( + "homeassistant.components.accuweather.AccuWeather.async_get_current_conditions", + return_value=load_json_object_fixture( + "accuweather/current_conditions_data.json" + ), + ), + patch( + "homeassistant.components.accuweather.AccuWeather.requests_remaining", + new_callable=PropertyMock, + return_value=10, ), - ), patch( - "homeassistant.components.accuweather.AccuWeather.requests_remaining", - new_callable=PropertyMock, - return_value=10, ): async_fire_time_changed(hass, future) await hass.async_block_till_done() @@ -604,16 +608,20 @@ async def test_manual_update_entity(hass: HomeAssistant) -> None: current = load_json_object_fixture("accuweather/current_conditions_data.json") forecast = load_json_array_fixture("accuweather/forecast_data.json") - with patch( - "homeassistant.components.accuweather.AccuWeather.async_get_current_conditions", - return_value=current, - ) as mock_current, patch( - "homeassistant.components.accuweather.AccuWeather.async_get_daily_forecast", - return_value=forecast, - ) as mock_forecast, patch( - "homeassistant.components.accuweather.AccuWeather.requests_remaining", - new_callable=PropertyMock, - return_value=10, + with ( + patch( + "homeassistant.components.accuweather.AccuWeather.async_get_current_conditions", + return_value=current, + ) as mock_current, + patch( + "homeassistant.components.accuweather.AccuWeather.async_get_daily_forecast", + return_value=forecast, + ) as mock_forecast, + patch( + "homeassistant.components.accuweather.AccuWeather.requests_remaining", + new_callable=PropertyMock, + return_value=10, + ), ): await hass.services.async_call( "homeassistant", @@ -664,13 +672,16 @@ async def test_state_update(hass: HomeAssistant) -> None: ) current_condition["Ceiling"]["Metric"]["Value"] = 3300 - with patch( - "homeassistant.components.accuweather.AccuWeather.async_get_current_conditions", - return_value=current_condition, - ), patch( - "homeassistant.components.accuweather.AccuWeather.requests_remaining", - new_callable=PropertyMock, - return_value=10, + with ( + patch( + "homeassistant.components.accuweather.AccuWeather.async_get_current_conditions", + return_value=current_condition, + ), + patch( + "homeassistant.components.accuweather.AccuWeather.requests_remaining", + new_callable=PropertyMock, + return_value=10, + ), ): async_fire_time_changed(hass, future) await hass.async_block_till_done() diff --git a/tests/components/accuweather/test_system_health.py b/tests/components/accuweather/test_system_health.py index b7acc318883..6321071eaa5 100644 --- a/tests/components/accuweather/test_system_health.py +++ b/tests/components/accuweather/test_system_health.py @@ -1,4 +1,5 @@ """Test AccuWeather system health.""" + import asyncio from unittest.mock import Mock @@ -19,6 +20,7 @@ async def test_accuweather_system_health( aioclient_mock.get("https://dataservice.accuweather.com/", text="") hass.config.components.add(DOMAIN) assert await async_setup_component(hass, "system_health", {}) + await hass.async_block_till_done() hass.data[DOMAIN] = {} hass.data[DOMAIN]["0123xyz"] = {} @@ -43,6 +45,7 @@ async def test_accuweather_system_health_fail( aioclient_mock.get("https://dataservice.accuweather.com/", exc=ClientError) hass.config.components.add(DOMAIN) assert await async_setup_component(hass, "system_health", {}) + await hass.async_block_till_done() hass.data[DOMAIN] = {} hass.data[DOMAIN]["0123xyz"] = {} diff --git a/tests/components/accuweather/test_weather.py b/tests/components/accuweather/test_weather.py index 920e5cf82b9..0b9d3e28fb2 100644 --- a/tests/components/accuweather/test_weather.py +++ b/tests/components/accuweather/test_weather.py @@ -1,4 +1,5 @@ """Test weather of AccuWeather integration.""" + from datetime import timedelta from unittest.mock import PropertyMock, patch @@ -8,18 +9,7 @@ from syrupy.assertion import SnapshotAssertion from homeassistant.components.accuweather.const import ATTRIBUTION from homeassistant.components.weather import ( - ATTR_FORECAST, - ATTR_FORECAST_APPARENT_TEMP, - ATTR_FORECAST_CLOUD_COVERAGE, ATTR_FORECAST_CONDITION, - ATTR_FORECAST_PRECIPITATION, - ATTR_FORECAST_PRECIPITATION_PROBABILITY, - ATTR_FORECAST_TEMP, - ATTR_FORECAST_TEMP_LOW, - ATTR_FORECAST_TIME, - ATTR_FORECAST_WIND_BEARING, - ATTR_FORECAST_WIND_GUST_SPEED, - ATTR_FORECAST_WIND_SPEED, ATTR_WEATHER_APPARENT_TEMPERATURE, ATTR_WEATHER_CLOUD_COVERAGE, ATTR_WEATHER_DEW_POINT, @@ -34,7 +24,6 @@ from homeassistant.components.weather import ( DOMAIN as WEATHER_DOMAIN, LEGACY_SERVICE_GET_FORECAST, SERVICE_GET_FORECASTS, - WeatherEntityFeature, ) from homeassistant.const import ( ATTR_ATTRIBUTION, @@ -57,16 +46,13 @@ from tests.common import ( from tests.typing import WebSocketGenerator -async def test_weather_without_forecast( - hass: HomeAssistant, entity_registry: er.EntityRegistry -) -> None: +async def test_weather(hass: HomeAssistant, entity_registry: er.EntityRegistry) -> None: """Test states of the weather without forecast.""" await init_integration(hass) state = hass.states.get("weather.home") assert state assert state.state == "sunny" - assert not state.attributes.get(ATTR_FORECAST) assert state.attributes.get(ATTR_WEATHER_HUMIDITY) == 67 assert state.attributes.get(ATTR_WEATHER_PRESSURE) == 1012.0 assert state.attributes.get(ATTR_WEATHER_TEMPERATURE) == 22.6 @@ -86,49 +72,6 @@ async def test_weather_without_forecast( assert entry.unique_id == "0123456" -async def test_weather_with_forecast( - hass: HomeAssistant, entity_registry: er.EntityRegistry -) -> None: - """Test states of the weather with forecast.""" - await init_integration(hass, forecast=True) - - state = hass.states.get("weather.home") - assert state - assert state.state == "sunny" - assert state.attributes.get(ATTR_WEATHER_HUMIDITY) == 67 - assert state.attributes.get(ATTR_WEATHER_PRESSURE) == 1012.0 - assert state.attributes.get(ATTR_WEATHER_TEMPERATURE) == 22.6 - assert state.attributes.get(ATTR_WEATHER_VISIBILITY) == 16.1 - assert state.attributes.get(ATTR_WEATHER_WIND_BEARING) == 180 - assert state.attributes.get(ATTR_WEATHER_WIND_SPEED) == 14.5 # 4.03 m/s -> km/h - assert state.attributes.get(ATTR_WEATHER_APPARENT_TEMPERATURE) == 22.8 - assert state.attributes.get(ATTR_WEATHER_DEW_POINT) == 16.2 - assert state.attributes.get(ATTR_WEATHER_CLOUD_COVERAGE) == 10 - assert state.attributes.get(ATTR_WEATHER_WIND_GUST_SPEED) == 20.3 - assert state.attributes.get(ATTR_WEATHER_UV_INDEX) == 6 - assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert ( - state.attributes[ATTR_SUPPORTED_FEATURES] == WeatherEntityFeature.FORECAST_DAILY - ) - forecast = state.attributes.get(ATTR_FORECAST)[0] - assert forecast.get(ATTR_FORECAST_CONDITION) == "lightning-rainy" - assert forecast.get(ATTR_FORECAST_PRECIPITATION) == 2.5 - assert forecast.get(ATTR_FORECAST_PRECIPITATION_PROBABILITY) == 60 - assert forecast.get(ATTR_FORECAST_TEMP) == 29.5 - assert forecast.get(ATTR_FORECAST_TEMP_LOW) == 15.4 - assert forecast.get(ATTR_FORECAST_TIME) == "2020-07-26T05:00:00+00:00" - assert forecast.get(ATTR_FORECAST_WIND_BEARING) == 166 - assert forecast.get(ATTR_FORECAST_WIND_SPEED) == 13.0 # 3.61 m/s -> km/h - assert forecast.get(ATTR_FORECAST_CLOUD_COVERAGE) == 58 - assert forecast.get(ATTR_FORECAST_APPARENT_TEMP) == 29.8 - assert forecast.get(ATTR_FORECAST_WIND_GUST_SPEED) == 29.6 - assert forecast.get(ATTR_WEATHER_UV_INDEX) == 5 - - entry = entity_registry.async_get("weather.home") - assert entry - assert entry.unique_id == "0123456" - - async def test_availability(hass: HomeAssistant) -> None: """Ensure that we mark the entities unavailable correctly when service is offline.""" await init_integration(hass) @@ -151,15 +94,18 @@ async def test_availability(hass: HomeAssistant) -> None: assert state.state == STATE_UNAVAILABLE future = utcnow() + timedelta(minutes=120) - with patch( - "homeassistant.components.accuweather.AccuWeather.async_get_current_conditions", - return_value=load_json_object_fixture( - "accuweather/current_conditions_data.json" + with ( + patch( + "homeassistant.components.accuweather.AccuWeather.async_get_current_conditions", + return_value=load_json_object_fixture( + "accuweather/current_conditions_data.json" + ), + ), + patch( + "homeassistant.components.accuweather.AccuWeather.requests_remaining", + new_callable=PropertyMock, + return_value=10, ), - ), patch( - "homeassistant.components.accuweather.AccuWeather.requests_remaining", - new_callable=PropertyMock, - return_value=10, ): async_fire_time_changed(hass, future) await hass.async_block_till_done() @@ -179,16 +125,20 @@ async def test_manual_update_entity(hass: HomeAssistant) -> None: current = load_json_object_fixture("accuweather/current_conditions_data.json") forecast = load_json_array_fixture("accuweather/forecast_data.json") - with patch( - "homeassistant.components.accuweather.AccuWeather.async_get_current_conditions", - return_value=current, - ) as mock_current, patch( - "homeassistant.components.accuweather.AccuWeather.async_get_daily_forecast", - return_value=forecast, - ) as mock_forecast, patch( - "homeassistant.components.accuweather.AccuWeather.requests_remaining", - new_callable=PropertyMock, - return_value=10, + with ( + patch( + "homeassistant.components.accuweather.AccuWeather.async_get_current_conditions", + return_value=current, + ) as mock_current, + patch( + "homeassistant.components.accuweather.AccuWeather.async_get_daily_forecast", + return_value=forecast, + ) as mock_forecast, + patch( + "homeassistant.components.accuweather.AccuWeather.requests_remaining", + new_callable=PropertyMock, + return_value=10, + ), ): await hass.services.async_call( "homeassistant", @@ -270,16 +220,20 @@ async def test_forecast_subscription( current = load_json_object_fixture("accuweather/current_conditions_data.json") forecast = load_json_array_fixture("accuweather/forecast_data.json") - with patch( - "homeassistant.components.accuweather.AccuWeather.async_get_current_conditions", - return_value=current, - ), patch( - "homeassistant.components.accuweather.AccuWeather.async_get_daily_forecast", - return_value=forecast, - ), patch( - "homeassistant.components.accuweather.AccuWeather.requests_remaining", - new_callable=PropertyMock, - return_value=10, + with ( + patch( + "homeassistant.components.accuweather.AccuWeather.async_get_current_conditions", + return_value=current, + ), + patch( + "homeassistant.components.accuweather.AccuWeather.async_get_daily_forecast", + return_value=forecast, + ), + patch( + "homeassistant.components.accuweather.AccuWeather.requests_remaining", + new_callable=PropertyMock, + return_value=10, + ), ): freezer.tick(timedelta(minutes=80) + timedelta(seconds=1)) 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 4b726e046b5..c39470ebbb6 100644 --- a/tests/components/acmeda/test_config_flow.py +++ b/tests/components/acmeda/test_config_flow.py @@ -1,4 +1,5 @@ """Define tests for the Acmeda config flow.""" + from unittest.mock import patch import aiopulse diff --git a/tests/components/adax/test_config_flow.py b/tests/components/adax/test_config_flow.py index f19d90a5772..b2342b7c2a7 100644 --- a/tests/components/adax/test_config_flow.py +++ b/tests/components/adax/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Adax config flow.""" + from unittest.mock import patch import adax_local @@ -41,13 +42,16 @@ async def test_form(hass: HomeAssistant) -> None: ) assert result2["type"] == FlowResultType.FORM - with patch( - "adax.get_adax_token", - return_value="test_token", - ), patch( - "homeassistant.components.adax.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "adax.get_adax_token", + return_value="test_token", + ), + patch( + "homeassistant.components.adax.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], TEST_DATA, @@ -148,12 +152,16 @@ async def test_local_create_entry(hass: HomeAssistant) -> None: WIFI_PSWD: "pswd", } - with patch( - "homeassistant.components.adax.async_setup_entry", - return_value=True, - ), patch( - "homeassistant.components.adax.config_flow.adax_local.AdaxConfig", autospec=True - ) as mock_client_class: + with ( + patch( + "homeassistant.components.adax.async_setup_entry", + return_value=True, + ), + patch( + "homeassistant.components.adax.config_flow.adax_local.AdaxConfig", + autospec=True, + ) as mock_client_class, + ): client = mock_client_class.return_value client.configure_device.return_value = True client.device_ip = "192.168.1.4" diff --git a/tests/components/adguard/test_config_flow.py b/tests/components/adguard/test_config_flow.py index f62172ebfad..3f12dd1508a 100644 --- a/tests/components/adguard/test_config_flow.py +++ b/tests/components/adguard/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for the AdGuard Home config flow.""" + import aiohttp from homeassistant import config_entries, data_entry_flow diff --git a/tests/components/advantage_air/conftest.py b/tests/components/advantage_air/conftest.py index 9da0a176309..fbd45c70396 100644 --- a/tests/components/advantage_air/conftest.py +++ b/tests/components/advantage_air/conftest.py @@ -1,4 +1,5 @@ """Fixtures for advantage_air.""" + from __future__ import annotations import pytest diff --git a/tests/components/advantage_air/snapshots/test_climate.ambr b/tests/components/advantage_air/snapshots/test_climate.ambr index 6b8c18e7c87..bd1fb431ae1 100644 --- a/tests/components/advantage_air/snapshots/test_climate.ambr +++ b/tests/components/advantage_air/snapshots/test_climate.ambr @@ -54,6 +54,7 @@ 'context': , 'entity_id': 'climate.myauto', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'heat_cool', }) diff --git a/tests/components/advantage_air/snapshots/test_switch.ambr b/tests/components/advantage_air/snapshots/test_switch.ambr index 2060c0798ed..cc050bb6a4d 100644 --- a/tests/components/advantage_air/snapshots/test_switch.ambr +++ b/tests/components/advantage_air/snapshots/test_switch.ambr @@ -27,6 +27,7 @@ 'context': , 'entity_id': 'switch.myzone_myfan', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }) diff --git a/tests/components/advantage_air/test_binary_sensor.py b/tests/components/advantage_air/test_binary_sensor.py index 19b0dba2eda..4395fb82542 100644 --- a/tests/components/advantage_air/test_binary_sensor.py +++ b/tests/components/advantage_air/test_binary_sensor.py @@ -1,4 +1,5 @@ """Test the Advantage Air Binary Sensor Platform.""" + from datetime import timedelta from unittest.mock import AsyncMock diff --git a/tests/components/advantage_air/test_config_flow.py b/tests/components/advantage_air/test_config_flow.py index 64d445a0b20..134cfee9f68 100644 --- a/tests/components/advantage_air/test_config_flow.py +++ b/tests/components/advantage_air/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Advantage Air config flow.""" + from unittest.mock import AsyncMock, patch from advantage_air import ApiError @@ -20,13 +21,16 @@ async def test_form(hass: HomeAssistant) -> None: assert result1["step_id"] == "user" assert result1["errors"] == {} - with patch( - "homeassistant.components.advantage_air.config_flow.advantage_air.async_get", - new=AsyncMock(return_value=TEST_SYSTEM_DATA), - ) as mock_get, patch( - "homeassistant.components.advantage_air.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.advantage_air.config_flow.advantage_air.async_get", + new=AsyncMock(return_value=TEST_SYSTEM_DATA), + ) as mock_get, + patch( + "homeassistant.components.advantage_air.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result1["flow_id"], USER_INPUT, diff --git a/tests/components/advantage_air/test_cover.py b/tests/components/advantage_air/test_cover.py index 8166b5da941..4752601d9ad 100644 --- a/tests/components/advantage_air/test_cover.py +++ b/tests/components/advantage_air/test_cover.py @@ -1,4 +1,5 @@ """Test the Advantage Air Cover Platform.""" + from unittest.mock import AsyncMock from homeassistant.components.cover import ( diff --git a/tests/components/advantage_air/test_diagnostics.py b/tests/components/advantage_air/test_diagnostics.py index 80de9019715..c225ab21863 100644 --- a/tests/components/advantage_air/test_diagnostics.py +++ b/tests/components/advantage_air/test_diagnostics.py @@ -1,4 +1,5 @@ """Test the Advantage Air Diagnostics.""" + from unittest.mock import AsyncMock from syrupy.assertion import SnapshotAssertion diff --git a/tests/components/advantage_air/test_init.py b/tests/components/advantage_air/test_init.py index 21cadbc4b3d..e700485c75a 100644 --- a/tests/components/advantage_air/test_init.py +++ b/tests/components/advantage_air/test_init.py @@ -1,4 +1,5 @@ """Test the Advantage Air Initialization.""" + from unittest.mock import AsyncMock from advantage_air import ApiError diff --git a/tests/components/advantage_air/test_light.py b/tests/components/advantage_air/test_light.py index 4d21781772d..b22c22fa185 100644 --- a/tests/components/advantage_air/test_light.py +++ b/tests/components/advantage_air/test_light.py @@ -1,6 +1,5 @@ """Test the Advantage Air Switch Platform.""" - from unittest.mock import AsyncMock from homeassistant.components.light import ( diff --git a/tests/components/advantage_air/test_select.py b/tests/components/advantage_air/test_select.py index 3367595d777..f0ed2b41e36 100644 --- a/tests/components/advantage_air/test_select.py +++ b/tests/components/advantage_air/test_select.py @@ -1,6 +1,5 @@ """Test the Advantage Air Select Platform.""" - from unittest.mock import AsyncMock from homeassistant.components.select import ( diff --git a/tests/components/advantage_air/test_sensor.py b/tests/components/advantage_air/test_sensor.py index 0099e1844c6..967afe20ddb 100644 --- a/tests/components/advantage_air/test_sensor.py +++ b/tests/components/advantage_air/test_sensor.py @@ -1,4 +1,5 @@ """Test the Advantage Air Sensor Platform.""" + from datetime import timedelta from unittest.mock import AsyncMock diff --git a/tests/components/aemet/conftest.py b/tests/components/aemet/conftest.py index 606f01c5403..ead27103348 100644 --- a/tests/components/aemet/conftest.py +++ b/tests/components/aemet/conftest.py @@ -1,4 +1,5 @@ """Test fixtures for aemet.""" + from collections.abc import Generator from unittest.mock import AsyncMock, patch diff --git a/tests/components/aemet/test_config_flow.py b/tests/components/aemet/test_config_flow.py index d4cedcaf3d0..a7a689381e0 100644 --- a/tests/components/aemet/test_config_flow.py +++ b/tests/components/aemet/test_config_flow.py @@ -1,4 +1,5 @@ """Define tests for the AEMET OpenData config flow.""" + from unittest.mock import AsyncMock, MagicMock, patch from aemet_opendata.exceptions import AuthError diff --git a/tests/components/aemet/test_coordinator.py b/tests/components/aemet/test_coordinator.py index a91256a9518..e830f50c54a 100644 --- a/tests/components/aemet/test_coordinator.py +++ b/tests/components/aemet/test_coordinator.py @@ -1,4 +1,5 @@ """Define tests for the AEMET OpenData coordinator.""" + from unittest.mock import patch from aemet_opendata.exceptions import AemetError @@ -29,7 +30,7 @@ async def test_coordinator_error( ): freezer.tick(WEATHER_UPDATE_INTERVAL) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get("weather.aemet") assert state.state == STATE_UNAVAILABLE diff --git a/tests/components/aemet/test_init.py b/tests/components/aemet/test_init.py index 7a4f73dc62b..df69349848b 100644 --- a/tests/components/aemet/test_init.py +++ b/tests/components/aemet/test_init.py @@ -1,4 +1,5 @@ """Define tests for the AEMET OpenData init.""" + from unittest.mock import patch from aemet_opendata.exceptions import AemetTimeout diff --git a/tests/components/aemet/test_weather.py b/tests/components/aemet/test_weather.py index 595275427f4..ec2c088fe6d 100644 --- a/tests/components/aemet/test_weather.py +++ b/tests/components/aemet/test_weather.py @@ -1,4 +1,5 @@ """The sensor tests for the AEMET OpenData platform.""" + import datetime from unittest.mock import patch @@ -6,19 +7,10 @@ from freezegun.api import FrozenDateTimeFactory import pytest from syrupy.assertion import SnapshotAssertion -from homeassistant.components.aemet.const import ATTRIBUTION, DOMAIN +from homeassistant.components.aemet.const import ATTRIBUTION from homeassistant.components.aemet.coordinator import WEATHER_UPDATE_INTERVAL from homeassistant.components.weather import ( ATTR_CONDITION_SNOWY, - ATTR_FORECAST, - ATTR_FORECAST_CONDITION, - ATTR_FORECAST_PRECIPITATION, - ATTR_FORECAST_PRECIPITATION_PROBABILITY, - ATTR_FORECAST_TEMP, - ATTR_FORECAST_TEMP_LOW, - ATTR_FORECAST_TIME, - ATTR_FORECAST_WIND_BEARING, - ATTR_FORECAST_WIND_SPEED, ATTR_WEATHER_HUMIDITY, ATTR_WEATHER_PRESSURE, ATTR_WEATHER_TEMPERATURE, @@ -31,8 +23,6 @@ from homeassistant.components.weather import ( ) from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er -import homeassistant.util.dt as dt_util from .util import async_init_integration, mock_api_call @@ -59,62 +49,6 @@ async def test_aemet_weather( assert state.attributes[ATTR_WEATHER_WIND_BEARING] == 122.0 assert state.attributes[ATTR_WEATHER_WIND_GUST_SPEED] == 12.2 assert state.attributes[ATTR_WEATHER_WIND_SPEED] == 3.2 - forecast = state.attributes[ATTR_FORECAST][0] - assert forecast[ATTR_FORECAST_CONDITION] == ATTR_CONDITION_SNOWY - assert ATTR_FORECAST_PRECIPITATION not in forecast - assert forecast[ATTR_FORECAST_PRECIPITATION_PROBABILITY] == 0 - assert forecast[ATTR_FORECAST_TEMP] == 2 - assert forecast[ATTR_FORECAST_TEMP_LOW] == -1 - assert ( - forecast[ATTR_FORECAST_TIME] - == dt_util.parse_datetime("2021-01-08 23:00:00+00:00").isoformat() - ) - assert forecast[ATTR_FORECAST_WIND_BEARING] == 90.0 - assert forecast[ATTR_FORECAST_WIND_SPEED] == 0.0 - - state = hass.states.get("weather.aemet_hourly") - assert state is None - - -async def test_aemet_weather_legacy( - hass: HomeAssistant, - freezer: FrozenDateTimeFactory, - entity_registry: er.EntityRegistry, -) -> None: - """Test states of legacy weather.""" - - entity_registry.async_get_or_create( - WEATHER_DOMAIN, - DOMAIN, - "None hourly", - ) - - hass.config.set_time_zone("UTC") - freezer.move_to("2021-01-09 12:00:00+00:00") - await async_init_integration(hass) - - state = hass.states.get("weather.aemet_daily") - assert state - assert state.state == ATTR_CONDITION_SNOWY - assert state.attributes[ATTR_ATTRIBUTION] == ATTRIBUTION - assert state.attributes[ATTR_WEATHER_HUMIDITY] == 99.0 - assert state.attributes[ATTR_WEATHER_PRESSURE] == 1004.4 # 100440.0 Pa -> hPa - assert state.attributes[ATTR_WEATHER_TEMPERATURE] == -0.7 - assert state.attributes[ATTR_WEATHER_WIND_BEARING] == 122.0 - assert state.attributes[ATTR_WEATHER_WIND_GUST_SPEED] == 12.2 - assert state.attributes[ATTR_WEATHER_WIND_SPEED] == 3.2 - forecast = state.attributes[ATTR_FORECAST][0] - assert forecast[ATTR_FORECAST_CONDITION] == ATTR_CONDITION_SNOWY - assert ATTR_FORECAST_PRECIPITATION not in forecast - assert forecast[ATTR_FORECAST_PRECIPITATION_PROBABILITY] == 0 - assert forecast[ATTR_FORECAST_TEMP] == 2 - assert forecast[ATTR_FORECAST_TEMP_LOW] == -1 - assert ( - forecast[ATTR_FORECAST_TIME] - == dt_util.parse_datetime("2021-01-08 23:00:00+00:00").isoformat() - ) - assert forecast[ATTR_FORECAST_WIND_BEARING] == 90.0 - assert forecast[ATTR_FORECAST_WIND_SPEED] == 0.0 state = hass.states.get("weather.aemet_hourly") assert state is None diff --git a/tests/components/aemet/util.py b/tests/components/aemet/util.py index 9714da1c5e7..81a184864a4 100644 --- a/tests/components/aemet/util.py +++ b/tests/components/aemet/util.py @@ -1,4 +1,5 @@ """Tests for the AEMET OpenData integration.""" + from typing import Any from unittest.mock import patch diff --git a/tests/components/aftership/conftest.py b/tests/components/aftership/conftest.py index e3fdc00bc30..0bea797dce6 100644 --- a/tests/components/aftership/conftest.py +++ b/tests/components/aftership/conftest.py @@ -1,4 +1,5 @@ """Common fixtures for the AfterShip tests.""" + from collections.abc import Generator from unittest.mock import AsyncMock, patch diff --git a/tests/components/aftership/test_config_flow.py b/tests/components/aftership/test_config_flow.py index 4668e7a61e4..d5e34ac0ae2 100644 --- a/tests/components/aftership/test_config_flow.py +++ b/tests/components/aftership/test_config_flow.py @@ -1,16 +1,14 @@ """Test AfterShip config flow.""" + from unittest.mock import AsyncMock, patch from pyaftership import AfterShipException from homeassistant.components.aftership.const import DOMAIN -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER +from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType -from homeassistant.helpers import issue_registry as ir - -from tests.common import MockConfigEntry async def test_full_user_flow(hass: HomeAssistant, mock_setup_entry) -> None: @@ -75,40 +73,3 @@ async def test_flow_cannot_connect(hass: HomeAssistant, mock_setup_entry) -> Non assert result["data"] == { CONF_API_KEY: "mock-api-key", } - - -async def test_import_flow( - hass: HomeAssistant, issue_registry: ir.IssueRegistry, mock_setup_entry -) -> None: - """Test importing yaml config.""" - - with patch( - "homeassistant.components.aftership.config_flow.AfterShip", - return_value=AsyncMock(), - ) as mock_aftership: - mock_aftership.return_value.trackings.return_value.list.return_value = {} - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={CONF_API_KEY: "yaml-api-key"}, - ) - assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["title"] == "AfterShip" - assert result["data"] == { - CONF_API_KEY: "yaml-api-key", - } - assert len(issue_registry.issues) == 1 - - -async def test_import_flow_already_exists( - hass: HomeAssistant, issue_registry: ir.IssueRegistry -) -> None: - """Test importing yaml config where entry already exists.""" - entry = MockConfigEntry(domain=DOMAIN, data={CONF_API_KEY: "yaml-api-key"}) - entry.add_to_hass(hass) - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data={CONF_API_KEY: "yaml-api-key"} - ) - assert result["type"] == FlowResultType.ABORT - assert result["reason"] == "already_configured" - assert len(issue_registry.issues) == 1 diff --git a/tests/components/agent_dvr/conftest.py b/tests/components/agent_dvr/conftest.py index da2cd90ed18..e9f719a6eeb 100644 --- a/tests/components/agent_dvr/conftest.py +++ b/tests/components/agent_dvr/conftest.py @@ -1,4 +1,5 @@ """Test fixtures for Agent DVR.""" + from collections.abc import Generator from unittest.mock import AsyncMock, patch diff --git a/tests/components/agent_dvr/test_config_flow.py b/tests/components/agent_dvr/test_config_flow.py index 9d1be278ada..958ec97a3ca 100644 --- a/tests/components/agent_dvr/test_config_flow.py +++ b/tests/components/agent_dvr/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for the Agent DVR config flow.""" + import pytest from homeassistant import data_entry_flow diff --git a/tests/components/agent_dvr/test_init.py b/tests/components/agent_dvr/test_init.py index b601e79d817..5b360430b78 100644 --- a/tests/components/agent_dvr/test_init.py +++ b/tests/components/agent_dvr/test_init.py @@ -1,4 +1,5 @@ """Test Agent DVR integration.""" + from unittest.mock import AsyncMock, patch from agent import AgentError diff --git a/tests/components/air_quality/test_air_quality.py b/tests/components/air_quality/test_air_quality.py index faaebda0aae..7bc21dee03c 100644 --- a/tests/components/air_quality/test_air_quality.py +++ b/tests/components/air_quality/test_air_quality.py @@ -1,4 +1,5 @@ """The tests for the Air Quality component.""" + import pytest from homeassistant.components.air_quality import ATTR_N2O, ATTR_OZONE, ATTR_PM_10 diff --git a/tests/components/airly/__init__.py b/tests/components/airly/__init__.py index ca26dbaf87f..cf76296d49a 100644 --- a/tests/components/airly/__init__.py +++ b/tests/components/airly/__init__.py @@ -1,4 +1,5 @@ """Tests for Airly.""" + from homeassistant.components.airly.const import DOMAIN from tests.common import MockConfigEntry, load_fixture diff --git a/tests/components/airly/snapshots/test_sensor.ambr b/tests/components/airly/snapshots/test_sensor.ambr index 83b90cab3a6..23a4d13cd00 100644 --- a/tests/components/airly/snapshots/test_sensor.ambr +++ b/tests/components/airly/snapshots/test_sensor.ambr @@ -50,6 +50,7 @@ 'context': , 'entity_id': 'sensor.home_carbon_monoxide', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '162.49', }) @@ -103,6 +104,7 @@ 'context': , 'entity_id': 'sensor.home_common_air_quality_index', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '7.29', }) @@ -157,6 +159,7 @@ 'context': , 'entity_id': 'sensor.home_humidity', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '68.35', }) @@ -213,6 +216,7 @@ 'context': , 'entity_id': 'sensor.home_nitrogen_dioxide', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '16.04', }) @@ -269,6 +273,7 @@ 'context': , 'entity_id': 'sensor.home_ozone', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '41.52', }) @@ -323,6 +328,7 @@ 'context': , 'entity_id': 'sensor.home_pm1', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '2.83', }) @@ -379,6 +385,7 @@ 'context': , 'entity_id': 'sensor.home_pm10', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '6.06', }) @@ -435,6 +442,7 @@ 'context': , 'entity_id': 'sensor.home_pm2_5', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '4.37', }) @@ -489,6 +497,7 @@ 'context': , 'entity_id': 'sensor.home_pressure', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '1019.86', }) @@ -545,6 +554,7 @@ 'context': , 'entity_id': 'sensor.home_sulphur_dioxide', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '13.97', }) @@ -599,6 +609,7 @@ 'context': , 'entity_id': 'sensor.home_temperature', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '14.37', }) diff --git a/tests/components/airly/test_config_flow.py b/tests/components/airly/test_config_flow.py index 2abd9bd1204..fcc024f7cee 100644 --- a/tests/components/airly/test_config_flow.py +++ b/tests/components/airly/test_config_flow.py @@ -1,4 +1,5 @@ """Define tests for the Airly config flow.""" + from http import HTTPStatus from airly.exceptions import AirlyError diff --git a/tests/components/airly/test_init.py b/tests/components/airly/test_init.py index f24a75bbb6e..6fc26110186 100644 --- a/tests/components/airly/test_init.py +++ b/tests/components/airly/test_init.py @@ -1,4 +1,5 @@ """Test init of Airly integration.""" + from typing import Any from freezegun.api import FrozenDateTimeFactory @@ -195,7 +196,7 @@ async def test_unload_entry( assert not hass.data.get(DOMAIN) -@pytest.mark.parametrize("old_identifier", ((DOMAIN, 123, 456), (DOMAIN, "123", "456"))) +@pytest.mark.parametrize("old_identifier", [(DOMAIN, 123, 456), (DOMAIN, "123", "456")]) async def test_migrate_device_entry( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, diff --git a/tests/components/airly/test_sensor.py b/tests/components/airly/test_sensor.py index 2a2bf9fb923..19f073496db 100644 --- a/tests/components/airly/test_sensor.py +++ b/tests/components/airly/test_sensor.py @@ -1,4 +1,5 @@ """Test sensor of Airly integration.""" + from datetime import timedelta from http import HTTPStatus from unittest.mock import patch diff --git a/tests/components/airly/test_system_health.py b/tests/components/airly/test_system_health.py index 38f4378a2e3..4ae94ca280c 100644 --- a/tests/components/airly/test_system_health.py +++ b/tests/components/airly/test_system_health.py @@ -1,4 +1,5 @@ """Test Airly system health.""" + import asyncio from unittest.mock import Mock @@ -19,6 +20,7 @@ async def test_airly_system_health( aioclient_mock.get("https://airapi.airly.eu/v2/", text="") hass.config.components.add(DOMAIN) assert await async_setup_component(hass, "system_health", {}) + await hass.async_block_till_done() hass.data[DOMAIN] = {} hass.data[DOMAIN]["0123xyz"] = Mock( @@ -47,6 +49,7 @@ async def test_airly_system_health_fail( aioclient_mock.get("https://airapi.airly.eu/v2/", exc=ClientError) hass.config.components.add(DOMAIN) assert await async_setup_component(hass, "system_health", {}) + await hass.async_block_till_done() hass.data[DOMAIN] = {} hass.data[DOMAIN]["0123xyz"] = Mock( diff --git a/tests/components/airnow/conftest.py b/tests/components/airnow/conftest.py index 0356c6f3395..1010a45b8fb 100644 --- a/tests/components/airnow/conftest.py +++ b/tests/components/airnow/conftest.py @@ -1,4 +1,5 @@ """Define fixtures for AirNow tests.""" + import json from unittest.mock import AsyncMock, patch @@ -58,7 +59,8 @@ def mock_api_get_fixture(data): @pytest.fixture(name="setup_airnow") async def setup_airnow_fixture(hass, config, mock_api_get): """Define a fixture to set up AirNow.""" - with patch("pyairnow.WebServiceAPI._get", mock_api_get), patch( - "homeassistant.components.airnow.PLATFORMS", [] + with ( + patch("pyairnow.WebServiceAPI._get", mock_api_get), + patch("homeassistant.components.airnow.PLATFORMS", []), ): yield diff --git a/tests/components/airnow/test_config_flow.py b/tests/components/airnow/test_config_flow.py index f4a0fdeec1e..ece28a77a87 100644 --- a/tests/components/airnow/test_config_flow.py +++ b/tests/components/airnow/test_config_flow.py @@ -1,4 +1,5 @@ """Test the AirNow config flow.""" + from unittest.mock import AsyncMock, patch from pyairnow.errors import AirNowError, EmptyResponseError, InvalidKeyError diff --git a/tests/components/airnow/test_diagnostics.py b/tests/components/airnow/test_diagnostics.py index 50ff3ed2b32..78f6c410fdf 100644 --- a/tests/components/airnow/test_diagnostics.py +++ b/tests/components/airnow/test_diagnostics.py @@ -1,4 +1,5 @@ """Test AirNow diagnostics.""" + from syrupy import SnapshotAssertion from homeassistant.core import HomeAssistant diff --git a/tests/components/airq/conftest.py b/tests/components/airq/conftest.py index 28053c9c20a..647569b63f0 100644 --- a/tests/components/airq/conftest.py +++ b/tests/components/airq/conftest.py @@ -1,4 +1,5 @@ """Test fixtures for air-Q.""" + from collections.abc import Generator from unittest.mock import AsyncMock, patch diff --git a/tests/components/airq/test_config_flow.py b/tests/components/airq/test_config_flow.py index 1619440a6f7..9c5492eaa20 100644 --- a/tests/components/airq/test_config_flow.py +++ b/tests/components/airq/test_config_flow.py @@ -1,4 +1,5 @@ """Test the air-Q config flow.""" + from unittest.mock import patch from aioairq import DeviceInfo, InvalidAuth @@ -36,8 +37,9 @@ async def test_form(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.FORM assert result["errors"] is None - with patch("aioairq.AirQ.validate"), patch( - "aioairq.AirQ.fetch_device_info", return_value=TEST_DEVICE_INFO + with ( + patch("aioairq.AirQ.validate"), + patch("aioairq.AirQ.fetch_device_info", return_value=TEST_DEVICE_INFO), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -92,8 +94,9 @@ async def test_duplicate_error(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch("aioairq.AirQ.validate"), patch( - "aioairq.AirQ.fetch_device_info", return_value=TEST_DEVICE_INFO + with ( + patch("aioairq.AirQ.validate"), + patch("aioairq.AirQ.fetch_device_info", return_value=TEST_DEVICE_INFO), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], TEST_USER_DATA diff --git a/tests/components/airthings/test_config_flow.py b/tests/components/airthings/test_config_flow.py index 3228b3c7229..2ea157f09b1 100644 --- a/tests/components/airthings/test_config_flow.py +++ b/tests/components/airthings/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Airthings config flow.""" + from unittest.mock import patch import airthings @@ -26,13 +27,16 @@ async def test_form(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.FORM assert result["errors"] is None - with patch( - "airthings.get_token", - return_value="test_token", - ), patch( - "homeassistant.components.airthings.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "airthings.get_token", + return_value="test_token", + ), + patch( + "homeassistant.components.airthings.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], TEST_DATA, diff --git a/tests/components/airthings_ble/__init__.py b/tests/components/airthings_ble/__init__.py index 50e3e0069bb..d60b42eddf2 100644 --- a/tests/components/airthings_ble/__init__.py +++ b/tests/components/airthings_ble/__init__.py @@ -1,4 +1,5 @@ """Tests for the Airthings BLE integration.""" + from __future__ import annotations from unittest.mock import patch @@ -63,7 +64,7 @@ WAVE_SERVICE_INFO = BluetoothServiceInfoBleak( service_data={ # Sensor data "b42e2a68-ade7-11e4-89d3-123b93f75cba": bytearray( - b"\x01\x02\x03\x04\x00\x05\x00\x06\x00\x07\x00\x08\x00\x09\x00\x0A" + b"\x01\x02\x03\x04\x00\x05\x00\x06\x00\x07\x00\x08\x00\x09\x00\x0a" ), # Manufacturer "00002a29-0000-1000-8000-00805f9b34fb": bytearray(b"Airthings AS"), @@ -108,7 +109,7 @@ VIEW_PLUS_SERVICE_INFO = BluetoothServiceInfoBleak( manufacturer_data={820: b"\xe4/\xa5\xae\t\x00"}, service_data={ "b42eb4a6-ade7-11e4-89d3-123b93f75cba": bytearray( - b"\x01\x02\x03\x04\x00\x05\x00\x06\x00\x07\x00\x08\x00\x09\x00\x0A" + b"\x01\x02\x03\x04\x00\x05\x00\x06\x00\x07\x00\x08\x00\x09\x00\x0a" ), # Manufacturer "00002a29-0000-1000-8000-00805f9b34fb": bytearray(b"Airthings AS"), diff --git a/tests/components/airthings_ble/test_config_flow.py b/tests/components/airthings_ble/test_config_flow.py index 2f20d889a85..edeb08abb74 100644 --- a/tests/components/airthings_ble/test_config_flow.py +++ b/tests/components/airthings_ble/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Airthings BLE config flow.""" + from unittest.mock import patch from airthings_ble import AirthingsDevice, AirthingsDeviceType @@ -25,13 +26,16 @@ from tests.common import MockConfigEntry async def test_bluetooth_discovery(hass: HomeAssistant) -> None: """Test discovery via bluetooth with a valid device.""" - with patch_async_ble_device_from_address(WAVE_SERVICE_INFO), patch_airthings_ble( - AirthingsDevice( - manufacturer="Airthings AS", - model=AirthingsDeviceType.WAVE_PLUS, - name="Airthings Wave Plus", - identifier="123456", - ) + with ( + patch_async_ble_device_from_address(WAVE_SERVICE_INFO), + patch_airthings_ble( + AirthingsDevice( + manufacturer="Airthings AS", + model=AirthingsDeviceType.WAVE_PLUS, + name="Airthings Wave Plus", + identifier="123456", + ) + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -73,9 +77,10 @@ async def test_bluetooth_discovery_airthings_ble_update_failed( """Test discovery via bluetooth but there's an exception from airthings-ble.""" for loop in [(Exception(), "unknown"), (BleakError(), "cannot_connect")]: exc, reason = loop - with patch_async_ble_device_from_address( - WAVE_SERVICE_INFO - ), patch_airthings_ble(side_effect=exc): + with ( + patch_async_ble_device_from_address(WAVE_SERVICE_INFO), + patch_airthings_ble(side_effect=exc), + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_BLUETOOTH}, @@ -104,16 +109,20 @@ async def test_bluetooth_discovery_already_setup(hass: HomeAssistant) -> None: async def test_user_setup(hass: HomeAssistant) -> None: """Test the user initiated form.""" - with patch( - "homeassistant.components.airthings_ble.config_flow.async_discovered_service_info", - return_value=[WAVE_SERVICE_INFO], - ), patch_async_ble_device_from_address(WAVE_SERVICE_INFO), patch_airthings_ble( - AirthingsDevice( - manufacturer="Airthings AS", - model=AirthingsDeviceType.WAVE_PLUS, - name="Airthings Wave Plus", - identifier="123456", - ) + with ( + patch( + "homeassistant.components.airthings_ble.config_flow.async_discovered_service_info", + return_value=[WAVE_SERVICE_INFO], + ), + patch_async_ble_device_from_address(WAVE_SERVICE_INFO), + patch_airthings_ble( + AirthingsDevice( + manufacturer="Airthings AS", + model=AirthingsDeviceType.WAVE_PLUS, + name="Airthings Wave Plus", + identifier="123456", + ) + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} @@ -175,11 +184,13 @@ async def test_user_setup_existing_and_unknown_device(hass: HomeAssistant) -> No async def test_user_setup_unknown_error(hass: HomeAssistant) -> None: """Test the user initiated form with an unknown error.""" - with patch( - "homeassistant.components.airthings_ble.config_flow.async_discovered_service_info", - return_value=[WAVE_SERVICE_INFO], - ), patch_async_ble_device_from_address(WAVE_SERVICE_INFO), patch_airthings_ble( - None, Exception() + with ( + patch( + "homeassistant.components.airthings_ble.config_flow.async_discovered_service_info", + return_value=[WAVE_SERVICE_INFO], + ), + patch_async_ble_device_from_address(WAVE_SERVICE_INFO), + patch_airthings_ble(None, Exception()), ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} @@ -191,11 +202,13 @@ async def test_user_setup_unknown_error(hass: HomeAssistant) -> None: async def test_user_setup_unable_to_connect(hass: HomeAssistant) -> None: """Test the user initiated form with a device that's failing connection.""" - with patch( - "homeassistant.components.airthings_ble.config_flow.async_discovered_service_info", - return_value=[WAVE_SERVICE_INFO], - ), patch_async_ble_device_from_address(WAVE_SERVICE_INFO), patch_airthings_ble( - side_effect=BleakError("An error") + with ( + patch( + "homeassistant.components.airthings_ble.config_flow.async_discovered_service_info", + return_value=[WAVE_SERVICE_INFO], + ), + patch_async_ble_device_from_address(WAVE_SERVICE_INFO), + patch_airthings_ble(side_effect=BleakError("An error")), ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} diff --git a/tests/components/airthings_ble/test_sensor.py b/tests/components/airthings_ble/test_sensor.py index 1bf036b735d..eee4de263d6 100644 --- a/tests/components/airthings_ble/test_sensor.py +++ b/tests/components/airthings_ble/test_sensor.py @@ -1,4 +1,5 @@ """Test the Airthings Wave sensor.""" + import logging from homeassistant.components.airthings_ble.const import DOMAIN diff --git a/tests/components/airtouch4/test_config_flow.py b/tests/components/airtouch4/test_config_flow.py index 243fc603ef5..e5e3672f69d 100644 --- a/tests/components/airtouch4/test_config_flow.py +++ b/tests/components/airtouch4/test_config_flow.py @@ -1,4 +1,5 @@ """Test the AirTouch 4 config flow.""" + from unittest.mock import AsyncMock, Mock, patch from airtouch4pyapi.airtouch import AirTouch, AirTouchAc, AirTouchGroup, AirTouchStatus @@ -23,13 +24,16 @@ async def test_form(hass: HomeAssistant) -> None: mock_airtouch.GetAcs = Mock(return_value=[mock_ac]) mock_airtouch.GetGroups = Mock(return_value=[mock_groups]) - with patch( - "homeassistant.components.airtouch4.config_flow.AirTouch", - return_value=mock_airtouch, - ), patch( - "homeassistant.components.airtouch4.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.airtouch4.config_flow.AirTouch", + return_value=mock_airtouch, + ), + patch( + "homeassistant.components.airtouch4.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "0.0.0.1"} ) diff --git a/tests/components/airtouch5/conftest.py b/tests/components/airtouch5/conftest.py index 836ce81301a..016843e6874 100644 --- a/tests/components/airtouch5/conftest.py +++ b/tests/components/airtouch5/conftest.py @@ -1,4 +1,5 @@ """Common fixtures for the Airtouch 5 tests.""" + from collections.abc import Generator from unittest.mock import AsyncMock, patch diff --git a/tests/components/airtouch5/test_config_flow.py b/tests/components/airtouch5/test_config_flow.py index 4f608fd4788..8b4b9890e57 100644 --- a/tests/components/airtouch5/test_config_flow.py +++ b/tests/components/airtouch5/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Airtouch 5 config flow.""" + from unittest.mock import AsyncMock, patch import pytest diff --git a/tests/components/airvisual/conftest.py b/tests/components/airvisual/conftest.py index 58b8864ea9c..1538af28a08 100644 --- a/tests/components/airvisual/conftest.py +++ b/tests/components/airvisual/conftest.py @@ -1,4 +1,5 @@ """Define test fixtures for AirVisual.""" + from collections.abc import Generator import json from unittest.mock import AsyncMock, Mock, patch @@ -112,18 +113,23 @@ def integration_type_fixture(): @pytest.fixture(name="mock_pyairvisual") async def mock_pyairvisual_fixture(cloud_api, node_samba): """Define a fixture to patch pyairvisual.""" - with patch( - "homeassistant.components.airvisual.CloudAPI", - return_value=cloud_api, - ), patch( - "homeassistant.components.airvisual.config_flow.CloudAPI", - return_value=cloud_api, - ), patch( - "homeassistant.components.airvisual_pro.NodeSamba", - return_value=node_samba, - ), patch( - "homeassistant.components.airvisual_pro.config_flow.NodeSamba", - return_value=node_samba, + with ( + patch( + "homeassistant.components.airvisual.CloudAPI", + return_value=cloud_api, + ), + patch( + "homeassistant.components.airvisual.config_flow.CloudAPI", + return_value=cloud_api, + ), + patch( + "homeassistant.components.airvisual_pro.NodeSamba", + return_value=node_samba, + ), + patch( + "homeassistant.components.airvisual_pro.config_flow.NodeSamba", + return_value=node_samba, + ), ): yield diff --git a/tests/components/airvisual/test_config_flow.py b/tests/components/airvisual/test_config_flow.py index 1761f55d17f..8c5bbded662 100644 --- a/tests/components/airvisual/test_config_flow.py +++ b/tests/components/airvisual/test_config_flow.py @@ -1,4 +1,5 @@ """Define tests for the AirVisual config flow.""" + from unittest.mock import AsyncMock, patch from pyairvisual.cloud_api import ( diff --git a/tests/components/airvisual/test_diagnostics.py b/tests/components/airvisual/test_diagnostics.py index 32a083ec985..072e4559705 100644 --- a/tests/components/airvisual/test_diagnostics.py +++ b/tests/components/airvisual/test_diagnostics.py @@ -1,4 +1,5 @@ """Test AirVisual diagnostics.""" + from syrupy import SnapshotAssertion from homeassistant.core import HomeAssistant diff --git a/tests/components/airvisual/test_init.py b/tests/components/airvisual/test_init.py index 4f71e75da1e..e6cd5968cea 100644 --- a/tests/components/airvisual/test_init.py +++ b/tests/components/airvisual/test_init.py @@ -1,4 +1,5 @@ """Define tests for AirVisual init.""" + from unittest.mock import patch from homeassistant.components.airvisual import ( diff --git a/tests/components/airvisual_pro/conftest.py b/tests/components/airvisual_pro/conftest.py index 9ebe13c83e6..719b25b3cdf 100644 --- a/tests/components/airvisual_pro/conftest.py +++ b/tests/components/airvisual_pro/conftest.py @@ -1,4 +1,5 @@ """Define test fixtures for AirVisual Pro.""" + from collections.abc import Generator import json from unittest.mock import AsyncMock, Mock, patch @@ -74,11 +75,14 @@ def pro_fixture(connect, data, disconnect): @pytest.fixture(name="setup_airvisual_pro") async def setup_airvisual_pro_fixture(hass, config, pro): """Define a fixture to set up AirVisual Pro.""" - with patch( - "homeassistant.components.airvisual_pro.config_flow.NodeSamba", return_value=pro - ), patch( - "homeassistant.components.airvisual_pro.NodeSamba", return_value=pro - ), patch("homeassistant.components.airvisual.PLATFORMS", []): + with ( + patch( + "homeassistant.components.airvisual_pro.config_flow.NodeSamba", + return_value=pro, + ), + patch("homeassistant.components.airvisual_pro.NodeSamba", return_value=pro), + patch("homeassistant.components.airvisual.PLATFORMS", []), + ): assert await async_setup_component(hass, DOMAIN, config) await hass.async_block_till_done() yield diff --git a/tests/components/airvisual_pro/test_config_flow.py b/tests/components/airvisual_pro/test_config_flow.py index f1c6d93e357..b0469b5288b 100644 --- a/tests/components/airvisual_pro/test_config_flow.py +++ b/tests/components/airvisual_pro/test_config_flow.py @@ -1,4 +1,5 @@ """Test the AirVisual Pro config flow.""" + from unittest.mock import AsyncMock, patch from pyairvisual.node import ( diff --git a/tests/components/airvisual_pro/test_diagnostics.py b/tests/components/airvisual_pro/test_diagnostics.py index 7c69a7e636f..dd87d00be30 100644 --- a/tests/components/airvisual_pro/test_diagnostics.py +++ b/tests/components/airvisual_pro/test_diagnostics.py @@ -1,4 +1,5 @@ """Test AirVisual Pro diagnostics.""" + from syrupy import SnapshotAssertion from homeassistant.core import HomeAssistant diff --git a/tests/components/airzone/test_climate.py b/tests/components/airzone/test_climate.py index f33d1a8b28a..fa972bd3899 100644 --- a/tests/components/airzone/test_climate.py +++ b/tests/components/airzone/test_climate.py @@ -1,4 +1,5 @@ """The climate tests for the Airzone platform.""" + import copy from unittest.mock import patch @@ -228,18 +229,23 @@ async def test_airzone_create_climates(hass: HomeAssistant) -> None: HVAC_MOCK_CHANGED[API_SYSTEMS][0][API_DATA][0][API_MAX_TEMP] = 25 HVAC_MOCK_CHANGED[API_SYSTEMS][0][API_DATA][0][API_MIN_TEMP] = 10 - with patch( - "homeassistant.components.airzone.AirzoneLocalApi.get_dhw", - return_value=HVAC_DHW_MOCK, - ), patch( - "homeassistant.components.airzone.AirzoneLocalApi.get_hvac", - return_value=HVAC_MOCK_CHANGED, - ), patch( - "homeassistant.components.airzone.AirzoneLocalApi.get_hvac_systems", - return_value=HVAC_SYSTEMS_MOCK, - ), patch( - "homeassistant.components.airzone.AirzoneLocalApi.get_webserver", - return_value=HVAC_WEBSERVER_MOCK, + with ( + patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_dhw", + return_value=HVAC_DHW_MOCK, + ), + patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_hvac", + return_value=HVAC_MOCK_CHANGED, + ), + patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_hvac_systems", + return_value=HVAC_SYSTEMS_MOCK, + ), + patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_webserver", + return_value=HVAC_WEBSERVER_MOCK, + ), ): async_fire_time_changed(hass, utcnow() + SCAN_INTERVAL) await hass.async_block_till_done() @@ -442,18 +448,23 @@ async def test_airzone_climate_set_hvac_mode(hass: HomeAssistant) -> None: HVAC_MOCK_NO_SET_POINT = copy.deepcopy(HVAC_MOCK) del HVAC_MOCK_NO_SET_POINT[API_SYSTEMS][0][API_DATA][0][API_SET_POINT] - with patch( - "homeassistant.components.airzone.AirzoneLocalApi.get_dhw", - return_value=HVAC_DHW_MOCK, - ), patch( - "homeassistant.components.airzone.AirzoneLocalApi.get_hvac", - return_value=HVAC_MOCK_NO_SET_POINT, - ), patch( - "homeassistant.components.airzone.AirzoneLocalApi.get_hvac_systems", - return_value=HVAC_SYSTEMS_MOCK, - ), patch( - "homeassistant.components.airzone.AirzoneLocalApi.get_webserver", - return_value=HVAC_WEBSERVER_MOCK, + with ( + patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_dhw", + return_value=HVAC_DHW_MOCK, + ), + patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_hvac", + return_value=HVAC_MOCK_NO_SET_POINT, + ), + patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_hvac_systems", + return_value=HVAC_SYSTEMS_MOCK, + ), + patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_webserver", + return_value=HVAC_WEBSERVER_MOCK, + ), ): async_fire_time_changed(hass, utcnow() + SCAN_INTERVAL) await hass.async_block_till_done() @@ -477,10 +488,13 @@ async def test_airzone_climate_set_hvac_slave_error(hass: HomeAssistant) -> None await async_init_integration(hass) - with patch( - "homeassistant.components.airzone.AirzoneLocalApi.put_hvac", - return_value=HVAC_MOCK, - ), pytest.raises(HomeAssistantError): + with ( + patch( + "homeassistant.components.airzone.AirzoneLocalApi.put_hvac", + return_value=HVAC_MOCK, + ), + pytest.raises(HomeAssistantError), + ): await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_HVAC_MODE, @@ -569,10 +583,13 @@ async def test_airzone_climate_set_temp_error(hass: HomeAssistant) -> None: await async_init_integration(hass) - with patch( - "homeassistant.components.airzone.AirzoneLocalApi.put_hvac", - side_effect=AirzoneError, - ), pytest.raises(HomeAssistantError): + with ( + patch( + "homeassistant.components.airzone.AirzoneLocalApi.put_hvac", + side_effect=AirzoneError, + ), + pytest.raises(HomeAssistantError), + ): await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, diff --git a/tests/components/airzone/test_config_flow.py b/tests/components/airzone/test_config_flow.py index cd1190c2af5..c47e2b1a3dd 100644 --- a/tests/components/airzone/test_config_flow.py +++ b/tests/components/airzone/test_config_flow.py @@ -46,24 +46,31 @@ TEST_PORT = 3000 async def test_form(hass: HomeAssistant) -> None: """Test that the form is served with valid input.""" - with patch( - "homeassistant.components.airzone.async_setup_entry", - return_value=True, - ) as mock_setup_entry, patch( - "homeassistant.components.airzone.AirzoneLocalApi.get_dhw", - return_value=HVAC_DHW_MOCK, - ), patch( - "homeassistant.components.airzone.AirzoneLocalApi.get_hvac", - return_value=HVAC_MOCK, - ), patch( - "homeassistant.components.airzone.AirzoneLocalApi.get_hvac_systems", - side_effect=SystemOutOfRange, - ), patch( - "homeassistant.components.airzone.AirzoneLocalApi.get_version", - return_value=HVAC_VERSION_MOCK, - ), patch( - "homeassistant.components.airzone.AirzoneLocalApi.get_webserver", - return_value=HVAC_WEBSERVER_MOCK, + with ( + patch( + "homeassistant.components.airzone.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_dhw", + return_value=HVAC_DHW_MOCK, + ), + patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_hvac", + return_value=HVAC_MOCK, + ), + patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_hvac_systems", + side_effect=SystemOutOfRange, + ), + patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_version", + return_value=HVAC_VERSION_MOCK, + ), + patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_webserver", + return_value=HVAC_WEBSERVER_MOCK, + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} @@ -95,24 +102,31 @@ async def test_form(hass: HomeAssistant) -> None: async def test_form_invalid_system_id(hass: HomeAssistant) -> None: """Test Invalid System ID 0.""" - with patch( - "homeassistant.components.airzone.async_setup_entry", - return_value=True, - ) as mock_setup_entry, patch( - "homeassistant.components.airzone.AirzoneLocalApi.get_dhw", - side_effect=HotWaterNotAvailable, - ), patch( - "homeassistant.components.airzone.AirzoneLocalApi.get_hvac", - side_effect=InvalidSystem, - ) as mock_hvac, patch( - "homeassistant.components.airzone.AirzoneLocalApi.get_hvac_systems", - side_effect=SystemOutOfRange, - ), patch( - "homeassistant.components.airzone.AirzoneLocalApi.get_version", - return_value=HVAC_VERSION_MOCK, - ), patch( - "homeassistant.components.airzone.AirzoneLocalApi.get_webserver", - side_effect=InvalidMethod, + with ( + patch( + "homeassistant.components.airzone.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_dhw", + side_effect=HotWaterNotAvailable, + ), + patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_hvac", + side_effect=InvalidSystem, + ) as mock_hvac, + patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_hvac_systems", + side_effect=SystemOutOfRange, + ), + patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_version", + return_value=HVAC_VERSION_MOCK, + ), + patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_webserver", + side_effect=InvalidMethod, + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONFIG @@ -197,24 +211,31 @@ async def test_dhcp_flow(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.FORM assert result["step_id"] == "discovered_connection" - with patch( - "homeassistant.components.airzone.async_setup_entry", - return_value=True, - ) as mock_setup_entry, patch( - "homeassistant.components.airzone.AirzoneLocalApi.get_dhw", - return_value=HVAC_DHW_MOCK, - ), patch( - "homeassistant.components.airzone.AirzoneLocalApi.get_hvac", - return_value=HVAC_MOCK, - ), patch( - "homeassistant.components.airzone.AirzoneLocalApi.get_hvac_systems", - side_effect=SystemOutOfRange, - ), patch( - "homeassistant.components.airzone.AirzoneLocalApi.get_version", - return_value=HVAC_VERSION_MOCK, - ), patch( - "homeassistant.components.airzone.AirzoneLocalApi.get_webserver", - return_value=HVAC_WEBSERVER_MOCK, + with ( + patch( + "homeassistant.components.airzone.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_dhw", + return_value=HVAC_DHW_MOCK, + ), + patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_hvac", + return_value=HVAC_MOCK, + ), + patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_hvac_systems", + side_effect=SystemOutOfRange, + ), + patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_version", + return_value=HVAC_VERSION_MOCK, + ), + patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_webserver", + return_value=HVAC_WEBSERVER_MOCK, + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -278,24 +299,31 @@ async def test_dhcp_connection_error(hass: HomeAssistant) -> None: assert result["errors"] == {"base": "cannot_connect"} - with patch( - "homeassistant.components.airzone.async_setup_entry", - return_value=True, - ) as mock_setup_entry, patch( - "homeassistant.components.airzone.AirzoneLocalApi.get_dhw", - return_value=HVAC_DHW_MOCK, - ), patch( - "homeassistant.components.airzone.AirzoneLocalApi.get_hvac", - return_value=HVAC_MOCK, - ), patch( - "homeassistant.components.airzone.AirzoneLocalApi.get_hvac_systems", - side_effect=SystemOutOfRange, - ), patch( - "homeassistant.components.airzone.AirzoneLocalApi.get_version", - return_value=HVAC_VERSION_MOCK, - ), patch( - "homeassistant.components.airzone.AirzoneLocalApi.get_webserver", - return_value=HVAC_WEBSERVER_MOCK, + with ( + patch( + "homeassistant.components.airzone.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_dhw", + return_value=HVAC_DHW_MOCK, + ), + patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_hvac", + return_value=HVAC_MOCK, + ), + patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_hvac_systems", + side_effect=SystemOutOfRange, + ), + patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_version", + return_value=HVAC_VERSION_MOCK, + ), + patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_webserver", + return_value=HVAC_WEBSERVER_MOCK, + ), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -334,24 +362,31 @@ async def test_dhcp_invalid_system_id(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.FORM assert result["step_id"] == "discovered_connection" - with patch( - "homeassistant.components.airzone.async_setup_entry", - return_value=True, - ) as mock_setup_entry, patch( - "homeassistant.components.airzone.AirzoneLocalApi.get_dhw", - side_effect=HotWaterNotAvailable, - ), patch( - "homeassistant.components.airzone.AirzoneLocalApi.get_hvac", - side_effect=InvalidSystem, - ) as mock_hvac, patch( - "homeassistant.components.airzone.AirzoneLocalApi.get_hvac_systems", - side_effect=SystemOutOfRange, - ), patch( - "homeassistant.components.airzone.AirzoneLocalApi.get_version", - return_value=HVAC_VERSION_MOCK, - ), patch( - "homeassistant.components.airzone.AirzoneLocalApi.get_webserver", - side_effect=InvalidMethod, + with ( + patch( + "homeassistant.components.airzone.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_dhw", + side_effect=HotWaterNotAvailable, + ), + patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_hvac", + side_effect=InvalidSystem, + ) as mock_hvac, + patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_hvac_systems", + side_effect=SystemOutOfRange, + ), + patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_version", + return_value=HVAC_VERSION_MOCK, + ), + patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_webserver", + side_effect=InvalidMethod, + ), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], diff --git a/tests/components/airzone/test_coordinator.py b/tests/components/airzone/test_coordinator.py index 62f6a15fe35..06c77bebb81 100644 --- a/tests/components/airzone/test_coordinator.py +++ b/tests/components/airzone/test_coordinator.py @@ -30,21 +30,27 @@ async def test_coordinator_client_connector_error(hass: HomeAssistant) -> None: ) config_entry.add_to_hass(hass) - with patch( - "homeassistant.components.airzone.AirzoneLocalApi.get_dhw", - side_effect=HotWaterNotAvailable, - ), patch( - "homeassistant.components.airzone.AirzoneLocalApi.get_hvac", - return_value=HVAC_MOCK, - ) as mock_hvac, patch( - "homeassistant.components.airzone.AirzoneLocalApi.get_hvac_systems", - side_effect=SystemOutOfRange, - ), patch( - "homeassistant.components.airzone.AirzoneLocalApi.get_version", - return_value=HVAC_VERSION_MOCK, - ), patch( - "homeassistant.components.airzone.AirzoneLocalApi.get_webserver", - side_effect=InvalidMethod, + with ( + patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_dhw", + side_effect=HotWaterNotAvailable, + ), + patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_hvac", + return_value=HVAC_MOCK, + ) as mock_hvac, + patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_hvac_systems", + side_effect=SystemOutOfRange, + ), + patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_version", + return_value=HVAC_VERSION_MOCK, + ), + patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_webserver", + side_effect=InvalidMethod, + ), ): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/airzone/test_init.py b/tests/components/airzone/test_init.py index 8936fa3e282..293fc75acb5 100644 --- a/tests/components/airzone/test_init.py +++ b/tests/components/airzone/test_init.py @@ -22,21 +22,27 @@ async def test_unique_id_migrate( config_entry = MockConfigEntry(domain=DOMAIN, data=CONFIG) config_entry.add_to_hass(hass) - with patch( - "homeassistant.components.airzone.AirzoneLocalApi.get_dhw", - side_effect=HotWaterNotAvailable, - ), patch( - "homeassistant.components.airzone.AirzoneLocalApi.get_hvac", - return_value=HVAC_MOCK, - ), patch( - "homeassistant.components.airzone.AirzoneLocalApi.get_hvac_systems", - side_effect=SystemOutOfRange, - ), patch( - "homeassistant.components.airzone.AirzoneLocalApi.get_version", - return_value=HVAC_VERSION_MOCK, - ), patch( - "homeassistant.components.airzone.AirzoneLocalApi.get_webserver", - side_effect=InvalidMethod, + with ( + patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_dhw", + side_effect=HotWaterNotAvailable, + ), + patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_hvac", + return_value=HVAC_MOCK, + ), + patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_hvac_systems", + side_effect=SystemOutOfRange, + ), + patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_version", + return_value=HVAC_VERSION_MOCK, + ), + patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_webserver", + side_effect=InvalidMethod, + ), ): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -47,21 +53,27 @@ async def test_unique_id_migrate( == f"{config_entry.entry_id}_1:1_temp" ) - with patch( - "homeassistant.components.airzone.AirzoneLocalApi.get_dhw", - side_effect=HotWaterNotAvailable, - ), patch( - "homeassistant.components.airzone.AirzoneLocalApi.get_hvac", - return_value=HVAC_MOCK, - ), patch( - "homeassistant.components.airzone.AirzoneLocalApi.get_hvac_systems", - side_effect=SystemOutOfRange, - ), patch( - "homeassistant.components.airzone.AirzoneLocalApi.get_version", - return_value=HVAC_VERSION_MOCK, - ), patch( - "homeassistant.components.airzone.AirzoneLocalApi.get_webserver", - return_value=HVAC_WEBSERVER_MOCK, + with ( + patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_dhw", + side_effect=HotWaterNotAvailable, + ), + patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_hvac", + return_value=HVAC_MOCK, + ), + patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_hvac_systems", + side_effect=SystemOutOfRange, + ), + patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_version", + return_value=HVAC_VERSION_MOCK, + ), + patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_webserver", + return_value=HVAC_WEBSERVER_MOCK, + ), ): await hass.config_entries.async_reload(config_entry.entry_id) await hass.async_block_till_done() @@ -83,12 +95,15 @@ async def test_unload_entry(hass: HomeAssistant) -> None: ) config_entry.add_to_hass(hass) - with patch( - "homeassistant.components.airzone.AirzoneLocalApi.validate", - return_value=None, - ), patch( - "homeassistant.components.airzone.AirzoneLocalApi.update", - return_value=None, + with ( + patch( + "homeassistant.components.airzone.AirzoneLocalApi.validate", + return_value=None, + ), + patch( + "homeassistant.components.airzone.AirzoneLocalApi.update", + return_value=None, + ), ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/airzone/test_sensor.py b/tests/components/airzone/test_sensor.py index 1511cd4362c..3d4c54522fc 100644 --- a/tests/components/airzone/test_sensor.py +++ b/tests/components/airzone/test_sensor.py @@ -91,21 +91,27 @@ async def test_airzone_sensors_availability( HVAC_MOCK_UNAVAILABLE_ZONE = copy.deepcopy(HVAC_MOCK) del HVAC_MOCK_UNAVAILABLE_ZONE[API_SYSTEMS][0][API_DATA][1] - with patch( - "homeassistant.components.airzone.AirzoneLocalApi.get_dhw", - return_value=HVAC_DHW_MOCK, - ), patch( - "homeassistant.components.airzone.AirzoneLocalApi.get_hvac", - return_value=HVAC_MOCK_UNAVAILABLE_ZONE, - ), patch( - "homeassistant.components.airzone.AirzoneLocalApi.get_hvac_systems", - return_value=HVAC_SYSTEMS_MOCK, - ), patch( - "homeassistant.components.airzone.AirzoneLocalApi.get_version", - return_value=HVAC_VERSION_MOCK, - ), patch( - "homeassistant.components.airzone.AirzoneLocalApi.get_webserver", - return_value=HVAC_WEBSERVER_MOCK, + with ( + patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_dhw", + return_value=HVAC_DHW_MOCK, + ), + patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_hvac", + return_value=HVAC_MOCK_UNAVAILABLE_ZONE, + ), + patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_hvac_systems", + return_value=HVAC_SYSTEMS_MOCK, + ), + patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_version", + return_value=HVAC_VERSION_MOCK, + ), + patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_webserver", + return_value=HVAC_WEBSERVER_MOCK, + ), ): async_fire_time_changed(hass, utcnow() + SCAN_INTERVAL) await hass.async_block_till_done() diff --git a/tests/components/airzone/test_water_heater.py b/tests/components/airzone/test_water_heater.py index a1157192f23..fa4507223e2 100644 --- a/tests/components/airzone/test_water_heater.py +++ b/tests/components/airzone/test_water_heater.py @@ -1,4 +1,5 @@ """The water heater tests for the Airzone platform.""" + from unittest.mock import patch from aioairzone.const import ( @@ -210,10 +211,13 @@ async def test_airzone_water_heater_set_temp_error(hass: HomeAssistant) -> None: await async_init_integration(hass) - with patch( - "homeassistant.components.airzone.AirzoneLocalApi.put_hvac", - side_effect=AirzoneError, - ), pytest.raises(HomeAssistantError): + with ( + patch( + "homeassistant.components.airzone.AirzoneLocalApi.put_hvac", + side_effect=AirzoneError, + ), + pytest.raises(HomeAssistantError), + ): await hass.services.async_call( WATER_HEATER_DOMAIN, SERVICE_SET_TEMPERATURE, diff --git a/tests/components/airzone/util.py b/tests/components/airzone/util.py index f83eceaae9c..c5c2d5972d4 100644 --- a/tests/components/airzone/util.py +++ b/tests/components/airzone/util.py @@ -322,21 +322,27 @@ async def async_init_integration( ) config_entry.add_to_hass(hass) - with patch( - "homeassistant.components.airzone.AirzoneLocalApi.get_dhw", - return_value=HVAC_DHW_MOCK, - ), patch( - "homeassistant.components.airzone.AirzoneLocalApi.get_hvac", - return_value=HVAC_MOCK, - ), patch( - "homeassistant.components.airzone.AirzoneLocalApi.get_hvac_systems", - return_value=HVAC_SYSTEMS_MOCK, - ), patch( - "homeassistant.components.airzone.AirzoneLocalApi.get_version", - return_value=HVAC_VERSION_MOCK, - ), patch( - "homeassistant.components.airzone.AirzoneLocalApi.get_webserver", - return_value=HVAC_WEBSERVER_MOCK, + with ( + patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_dhw", + return_value=HVAC_DHW_MOCK, + ), + patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_hvac", + return_value=HVAC_MOCK, + ), + patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_hvac_systems", + return_value=HVAC_SYSTEMS_MOCK, + ), + patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_version", + return_value=HVAC_VERSION_MOCK, + ), + patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_webserver", + return_value=HVAC_WEBSERVER_MOCK, + ), ): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/airzone_cloud/conftest.py b/tests/components/airzone_cloud/conftest.py new file mode 100644 index 00000000000..b289efd3fb9 --- /dev/null +++ b/tests/components/airzone_cloud/conftest.py @@ -0,0 +1,21 @@ +"""Tests for the Airzone integration.""" + +from unittest.mock import patch + +import pytest + + +@pytest.fixture(autouse=True) +def airzone_cloud_no_websockets(): + """Fixture to completely disable Airzone Cloud WebSockets.""" + with ( + patch( + "homeassistant.components.airzone_cloud.AirzoneCloudApi._update_websockets", + return_value=False, + ), + patch( + "homeassistant.components.airzone_cloud.AirzoneCloudApi.connect_installation_websockets", + return_value=None, + ), + ): + yield diff --git a/tests/components/airzone_cloud/test_climate.py b/tests/components/airzone_cloud/test_climate.py index 7c273dc8bc2..9bfaf5683a1 100644 --- a/tests/components/airzone_cloud/test_climate.py +++ b/tests/components/airzone_cloud/test_climate.py @@ -1,13 +1,16 @@ """The climate tests for the Airzone Cloud platform.""" + from unittest.mock import patch +from aioairzone_cloud.const import API_DEFAULT_TEMP_STEP from aioairzone_cloud.exceptions import AirzoneCloudError import pytest -from homeassistant.components.airzone.const import API_TEMPERATURE_STEP from homeassistant.components.climate import ( ATTR_CURRENT_HUMIDITY, ATTR_CURRENT_TEMPERATURE, + ATTR_FAN_MODE, + ATTR_FAN_MODES, ATTR_HVAC_ACTION, ATTR_HVAC_MODE, ATTR_HVAC_MODES, @@ -15,6 +18,11 @@ from homeassistant.components.climate import ( ATTR_MIN_TEMP, ATTR_TARGET_TEMP_STEP, DOMAIN as CLIMATE_DOMAIN, + FAN_AUTO, + FAN_HIGH, + FAN_LOW, + FAN_MEDIUM, + SERVICE_SET_FAN_MODE, SERVICE_SET_HVAC_MODE, SERVICE_SET_TEMPERATURE, HVACAction, @@ -42,6 +50,12 @@ async def test_airzone_create_climates(hass: HomeAssistant) -> None: assert state.state == HVACMode.OFF assert ATTR_CURRENT_HUMIDITY not in state.attributes assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 21.0 + assert state.attributes[ATTR_FAN_MODE] == FAN_HIGH + assert state.attributes[ATTR_FAN_MODES] == [ + FAN_LOW, + FAN_MEDIUM, + FAN_HIGH, + ] assert state.attributes[ATTR_HVAC_ACTION] == HVACAction.OFF assert state.attributes[ATTR_HVAC_MODES] == [ HVACMode.HEAT_COOL, @@ -53,13 +67,22 @@ async def test_airzone_create_climates(hass: HomeAssistant) -> None: ] assert state.attributes[ATTR_MAX_TEMP] == 30 assert state.attributes[ATTR_MIN_TEMP] == 15 - assert state.attributes[ATTR_TARGET_TEMP_STEP] == API_TEMPERATURE_STEP + assert state.attributes[ATTR_TARGET_TEMP_STEP] == API_DEFAULT_TEMP_STEP assert state.attributes[ATTR_TEMPERATURE] == 22.0 state = hass.states.get("climate.bron_pro") assert state.state == HVACMode.COOL assert ATTR_CURRENT_HUMIDITY not in state.attributes assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 20.0 + assert state.attributes[ATTR_FAN_MODE] == "60%" + assert state.attributes[ATTR_FAN_MODES] == [ + FAN_AUTO, + "20%", + "40%", + "60%", + "80%", + "100%", + ] assert state.attributes[ATTR_HVAC_ACTION] == HVACAction.COOLING assert state.attributes[ATTR_HVAC_MODES] == [ HVACMode.HEAT_COOL, @@ -71,7 +94,7 @@ async def test_airzone_create_climates(hass: HomeAssistant) -> None: ] assert state.attributes[ATTR_MAX_TEMP] == 30 assert state.attributes[ATTR_MIN_TEMP] == 15 - assert state.attributes[ATTR_TARGET_TEMP_STEP] == API_TEMPERATURE_STEP + assert state.attributes[ATTR_TARGET_TEMP_STEP] == API_DEFAULT_TEMP_STEP assert state.attributes[ATTR_TEMPERATURE] == 22.0 # Groups @@ -79,6 +102,8 @@ async def test_airzone_create_climates(hass: HomeAssistant) -> None: assert state.state == HVACMode.COOL assert state.attributes[ATTR_CURRENT_HUMIDITY] == 27 assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 22.5 + assert ATTR_FAN_MODE not in state.attributes + assert ATTR_FAN_MODES not in state.attributes assert state.attributes[ATTR_HVAC_ACTION] == HVACAction.COOLING assert state.attributes[ATTR_HVAC_MODES] == [ HVACMode.COOL, @@ -89,7 +114,7 @@ async def test_airzone_create_climates(hass: HomeAssistant) -> None: ] assert state.attributes[ATTR_MAX_TEMP] == 30 assert state.attributes[ATTR_MIN_TEMP] == 15 - assert state.attributes[ATTR_TARGET_TEMP_STEP] == API_TEMPERATURE_STEP + assert state.attributes[ATTR_TARGET_TEMP_STEP] == API_DEFAULT_TEMP_STEP assert state.attributes[ATTR_TEMPERATURE] == 24.0 # Installations @@ -97,6 +122,8 @@ async def test_airzone_create_climates(hass: HomeAssistant) -> None: assert state.state == HVACMode.COOL assert state.attributes[ATTR_CURRENT_HUMIDITY] == 27 assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 21.5 + assert ATTR_FAN_MODE not in state.attributes + assert ATTR_FAN_MODES not in state.attributes assert state.attributes[ATTR_HVAC_ACTION] == HVACAction.COOLING assert state.attributes[ATTR_HVAC_MODES] == [ HVACMode.HEAT_COOL, @@ -108,7 +135,7 @@ async def test_airzone_create_climates(hass: HomeAssistant) -> None: ] assert state.attributes[ATTR_MAX_TEMP] == 30 assert state.attributes[ATTR_MIN_TEMP] == 15 - assert state.attributes[ATTR_TARGET_TEMP_STEP] == API_TEMPERATURE_STEP + assert state.attributes[ATTR_TARGET_TEMP_STEP] == API_DEFAULT_TEMP_STEP assert state.attributes[ATTR_TEMPERATURE] == 23.0 # Zones @@ -116,6 +143,8 @@ async def test_airzone_create_climates(hass: HomeAssistant) -> None: assert state.state == HVACMode.OFF assert state.attributes[ATTR_CURRENT_HUMIDITY] == 24 assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 25.0 + assert ATTR_FAN_MODE not in state.attributes + assert ATTR_FAN_MODES not in state.attributes assert state.attributes[ATTR_HVAC_ACTION] == HVACAction.OFF assert state.attributes[ATTR_HVAC_MODES] == [ HVACMode.COOL, @@ -126,13 +155,15 @@ async def test_airzone_create_climates(hass: HomeAssistant) -> None: ] assert state.attributes[ATTR_MAX_TEMP] == 30 assert state.attributes[ATTR_MIN_TEMP] == 15 - assert state.attributes[ATTR_TARGET_TEMP_STEP] == API_TEMPERATURE_STEP + assert state.attributes[ATTR_TARGET_TEMP_STEP] == API_DEFAULT_TEMP_STEP assert state.attributes[ATTR_TEMPERATURE] == 24.0 state = hass.states.get("climate.salon") assert state.state == HVACMode.COOL assert state.attributes[ATTR_CURRENT_HUMIDITY] == 30 assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 20.0 + assert ATTR_FAN_MODE not in state.attributes + assert ATTR_FAN_MODES not in state.attributes assert state.attributes[ATTR_HVAC_ACTION] == HVACAction.COOLING assert state.attributes[ATTR_HVAC_MODES] == [ HVACMode.COOL, @@ -143,7 +174,7 @@ async def test_airzone_create_climates(hass: HomeAssistant) -> None: ] assert state.attributes[ATTR_MAX_TEMP] == 30 assert state.attributes[ATTR_MIN_TEMP] == 15 - assert state.attributes[ATTR_TARGET_TEMP_STEP] == API_TEMPERATURE_STEP + assert state.attributes[ATTR_TARGET_TEMP_STEP] == API_DEFAULT_TEMP_STEP assert state.attributes[ATTR_TEMPERATURE] == 24.0 @@ -269,6 +300,47 @@ async def test_airzone_climate_turn_on_off(hass: HomeAssistant) -> None: assert state.state == HVACMode.OFF +async def test_airzone_climate_set_fan_mode(hass: HomeAssistant) -> None: + """Test setting the fan mode.""" + + await async_init_integration(hass) + + # Aidoos + with patch( + "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_patch_device", + return_value=None, + ): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_FAN_MODE, + { + ATTR_ENTITY_ID: "climate.bron", + ATTR_FAN_MODE: FAN_LOW, + }, + blocking=True, + ) + + state = hass.states.get("climate.bron") + assert state.attributes[ATTR_FAN_MODE] == FAN_LOW + + with patch( + "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_patch_device", + return_value=None, + ): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_FAN_MODE, + { + ATTR_ENTITY_ID: "climate.bron_pro", + ATTR_FAN_MODE: FAN_AUTO, + }, + blocking=True, + ) + + state = hass.states.get("climate.bron_pro") + assert state.attributes[ATTR_FAN_MODE] == FAN_AUTO + + async def test_airzone_climate_set_hvac_mode(hass: HomeAssistant) -> None: """Test setting the HVAC mode.""" @@ -420,10 +492,13 @@ async def test_airzone_climate_set_hvac_slave_error(hass: HomeAssistant) -> None await async_init_integration(hass) - with patch( - "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_patch_device", - return_value=None, - ), pytest.raises(HomeAssistantError): + with ( + patch( + "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_patch_device", + return_value=None, + ), + pytest.raises(HomeAssistantError), + ): await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_HVAC_MODE, @@ -508,10 +583,13 @@ async def test_airzone_climate_set_temp_error(hass: HomeAssistant) -> None: await async_init_integration(hass) # Aidoos - with patch( - "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_patch_device", - side_effect=AirzoneCloudError, - ), pytest.raises(HomeAssistantError): + with ( + patch( + "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_patch_device", + side_effect=AirzoneCloudError, + ), + pytest.raises(HomeAssistantError), + ): await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, @@ -526,10 +604,13 @@ async def test_airzone_climate_set_temp_error(hass: HomeAssistant) -> None: assert state.attributes[ATTR_TEMPERATURE] == 22.0 # Groups - with patch( - "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_put_group", - side_effect=AirzoneCloudError, - ), pytest.raises(HomeAssistantError): + with ( + patch( + "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_put_group", + side_effect=AirzoneCloudError, + ), + pytest.raises(HomeAssistantError), + ): await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, @@ -544,10 +625,13 @@ async def test_airzone_climate_set_temp_error(hass: HomeAssistant) -> None: assert state.attributes[ATTR_TEMPERATURE] == 24.0 # Installations - with patch( - "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_put_installation", - side_effect=AirzoneCloudError, - ), pytest.raises(HomeAssistantError): + with ( + patch( + "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_put_installation", + side_effect=AirzoneCloudError, + ), + pytest.raises(HomeAssistantError), + ): await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, @@ -562,10 +646,13 @@ async def test_airzone_climate_set_temp_error(hass: HomeAssistant) -> None: assert state.attributes[ATTR_TEMPERATURE] == 23.0 # Zones - with patch( - "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_patch_device", - side_effect=AirzoneCloudError, - ), pytest.raises(HomeAssistantError): + with ( + patch( + "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_patch_device", + side_effect=AirzoneCloudError, + ), + pytest.raises(HomeAssistantError), + ): await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, diff --git a/tests/components/airzone_cloud/test_config_flow.py b/tests/components/airzone_cloud/test_config_flow.py index ec031d4bf25..e1d31e28d4b 100644 --- a/tests/components/airzone_cloud/test_config_flow.py +++ b/tests/components/airzone_cloud/test_config_flow.py @@ -23,24 +23,31 @@ from .util import ( async def test_form(hass: HomeAssistant) -> None: """Test that the form is served with valid input.""" - with patch( - "homeassistant.components.airzone_cloud.async_setup_entry", - return_value=True, - ) as mock_setup_entry, patch( - "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_get_device_status", - side_effect=mock_get_device_status, - ), patch( - "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_get_installation", - return_value=GET_INSTALLATION_MOCK, - ), patch( - "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_get_installations", - return_value=GET_INSTALLATIONS_MOCK, - ), patch( - "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_get_webserver", - side_effect=mock_get_webserver, - ), patch( - "homeassistant.components.airzone_cloud.AirzoneCloudApi.login", - return_value=None, + with ( + patch( + "homeassistant.components.airzone_cloud.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + patch( + "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_get_device_status", + side_effect=mock_get_device_status, + ), + patch( + "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_get_installation", + return_value=GET_INSTALLATION_MOCK, + ), + patch( + "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_get_installations", + return_value=GET_INSTALLATIONS_MOCK, + ), + patch( + "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_get_webserver", + side_effect=mock_get_webserver, + ), + patch( + "homeassistant.components.airzone_cloud.AirzoneCloudApi.login", + return_value=None, + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} @@ -87,21 +94,27 @@ async def test_form(hass: HomeAssistant) -> None: async def test_installations_list_error(hass: HomeAssistant) -> None: """Test connection error.""" - with patch( - "homeassistant.components.airzone_cloud.async_setup_entry", - return_value=True, - ), patch( - "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_get_device_status", - side_effect=mock_get_device_status, - ), patch( - "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_get_installations", - side_effect=AirzoneCloudError, - ), patch( - "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_get_webserver", - side_effect=mock_get_webserver, - ), patch( - "homeassistant.components.airzone_cloud.AirzoneCloudApi.login", - return_value=None, + with ( + patch( + "homeassistant.components.airzone_cloud.async_setup_entry", + return_value=True, + ), + patch( + "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_get_device_status", + side_effect=mock_get_device_status, + ), + patch( + "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_get_installations", + side_effect=AirzoneCloudError, + ), + patch( + "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_get_webserver", + side_effect=mock_get_webserver, + ), + patch( + "homeassistant.components.airzone_cloud.AirzoneCloudApi.login", + return_value=None, + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} diff --git a/tests/components/airzone_cloud/test_coordinator.py b/tests/components/airzone_cloud/test_coordinator.py index a2307b94335..b4b7afd6086 100644 --- a/tests/components/airzone_cloud/test_coordinator.py +++ b/tests/components/airzone_cloud/test_coordinator.py @@ -31,24 +31,27 @@ async def test_coordinator_client_connector_error(hass: HomeAssistant) -> None: ) config_entry.add_to_hass(hass) - with patch( - "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_get_device_status", - side_effect=mock_get_device_status, - ) as mock_device_status, patch( - "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_get_installation", - return_value=GET_INSTALLATION_MOCK, - ) as mock_installation, patch( - "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_get_installations", - return_value=GET_INSTALLATIONS_MOCK, - ) as mock_installations, patch( - "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_get_webserver", - side_effect=mock_get_webserver, - ) as mock_webserver, patch( - "homeassistant.components.airzone_cloud.AirzoneCloudApi.login", - return_value=None, - ), patch( - "homeassistant.components.airzone_cloud.AirzoneCloudApi._update_websockets", - return_value=False, + with ( + patch( + "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_get_device_status", + side_effect=mock_get_device_status, + ) as mock_device_status, + patch( + "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_get_installation", + return_value=GET_INSTALLATION_MOCK, + ) as mock_installation, + patch( + "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_get_installations", + return_value=GET_INSTALLATIONS_MOCK, + ) as mock_installations, + patch( + "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_get_webserver", + side_effect=mock_get_webserver, + ) as mock_webserver, + patch( + "homeassistant.components.airzone_cloud.AirzoneCloudApi.login", + return_value=None, + ), ): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -65,7 +68,7 @@ async def test_coordinator_client_connector_error(hass: HomeAssistant) -> None: mock_device_status.side_effect = AirzoneCloudError async_fire_time_changed(hass, utcnow() + SCAN_INTERVAL) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) mock_device_status.assert_called() diff --git a/tests/components/airzone_cloud/test_init.py b/tests/components/airzone_cloud/test_init.py index f8a7a710e08..b5b4bcebaa8 100644 --- a/tests/components/airzone_cloud/test_init.py +++ b/tests/components/airzone_cloud/test_init.py @@ -21,21 +21,27 @@ async def test_unload_entry(hass: HomeAssistant) -> None: ) config_entry.add_to_hass(hass) - with patch( - "homeassistant.components.airzone_cloud.AirzoneCloudApi.login", - return_value=None, - ), patch( - "homeassistant.components.airzone_cloud.AirzoneCloudApi.logout", - return_value=None, - ), patch( - "homeassistant.components.airzone_cloud.AirzoneCloudApi.list_installations", - return_value=[], - ), patch( - "homeassistant.components.airzone_cloud.AirzoneCloudApi.update_installation", - return_value=None, - ), patch( - "homeassistant.components.airzone_cloud.AirzoneCloudApi.update", - return_value=None, + with ( + patch( + "homeassistant.components.airzone_cloud.AirzoneCloudApi.login", + return_value=None, + ), + patch( + "homeassistant.components.airzone_cloud.AirzoneCloudApi.logout", + return_value=None, + ), + patch( + "homeassistant.components.airzone_cloud.AirzoneCloudApi.list_installations", + return_value=[], + ), + patch( + "homeassistant.components.airzone_cloud.AirzoneCloudApi.update_installation", + return_value=None, + ), + patch( + "homeassistant.components.airzone_cloud.AirzoneCloudApi.update", + return_value=None, + ), ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/airzone_cloud/util.py b/tests/components/airzone_cloud/util.py index 98ff7c65478..ea0dbf9f736 100644 --- a/tests/components/airzone_cloud/util.py +++ b/tests/components/airzone_cloud/util.py @@ -432,21 +432,27 @@ async def async_init_integration( ) config_entry.add_to_hass(hass) - with patch( - "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_get_device_status", - side_effect=mock_get_device_status, - ), patch( - "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_get_installation", - return_value=GET_INSTALLATION_MOCK, - ), patch( - "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_get_installations", - return_value=GET_INSTALLATIONS_MOCK, - ), patch( - "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_get_webserver", - side_effect=mock_get_webserver, - ), patch( - "homeassistant.components.airzone_cloud.AirzoneCloudApi.login", - return_value=None, + with ( + patch( + "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_get_device_status", + side_effect=mock_get_device_status, + ), + patch( + "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_get_installation", + return_value=GET_INSTALLATION_MOCK, + ), + patch( + "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_get_installations", + return_value=GET_INSTALLATIONS_MOCK, + ), + patch( + "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_get_webserver", + side_effect=mock_get_webserver, + ), + patch( + "homeassistant.components.airzone_cloud.AirzoneCloudApi.login", + return_value=None, + ), ): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/aladdin_connect/conftest.py b/tests/components/aladdin_connect/conftest.py index 3f5fc4f8f97..979c30bdcea 100644 --- a/tests/components/aladdin_connect/conftest.py +++ b/tests/components/aladdin_connect/conftest.py @@ -1,4 +1,5 @@ """Fixtures for the Aladdin Connect integration tests.""" + from unittest import mock from unittest.mock import AsyncMock diff --git a/tests/components/aladdin_connect/test_config_flow.py b/tests/components/aladdin_connect/test_config_flow.py index 6f879994fbe..90cf269b3f8 100644 --- a/tests/components/aladdin_connect/test_config_flow.py +++ b/tests/components/aladdin_connect/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Aladdin Connect config flow.""" + from unittest.mock import MagicMock, patch from AIOAladdinConnect.session_manager import InvalidPasswordError @@ -22,12 +23,16 @@ async def test_form(hass: HomeAssistant, mock_aladdinconnect_api: MagicMock) -> assert result["type"] == FlowResultType.FORM assert result["errors"] is None - with patch( - "homeassistant.components.aladdin_connect.config_flow.AladdinConnectClient", - return_value=mock_aladdinconnect_api, - ), patch( - "homeassistant.components.aladdin_connect.async_setup_entry", return_value=True - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.aladdin_connect.config_flow.AladdinConnectClient", + return_value=mock_aladdinconnect_api, + ), + patch( + "homeassistant.components.aladdin_connect.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -157,12 +162,15 @@ async def test_reauth_flow( assert result["type"] == FlowResultType.FORM assert result["errors"] == {} - with patch( - "homeassistant.components.aladdin_connect.async_setup_entry", - return_value=True, - ), patch( - "homeassistant.components.aladdin_connect.config_flow.AladdinConnectClient", - return_value=mock_aladdinconnect_api, + with ( + patch( + "homeassistant.components.aladdin_connect.async_setup_entry", + return_value=True, + ), + patch( + "homeassistant.components.aladdin_connect.config_flow.AladdinConnectClient", + return_value=mock_aladdinconnect_api, + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -205,15 +213,19 @@ async def test_reauth_flow_auth_error( assert result["errors"] == {} mock_aladdinconnect_api.login.return_value = False mock_aladdinconnect_api.login.side_effect = InvalidPasswordError - with patch( - "homeassistant.components.aladdin_connect.config_flow.AladdinConnectClient", - return_value=mock_aladdinconnect_api, - ), patch( - "homeassistant.components.aladdin_connect.cover.async_setup_entry", - return_value=True, - ), patch( - "homeassistant.components.aladdin_connect.cover.async_setup_entry", - return_value=True, + with ( + patch( + "homeassistant.components.aladdin_connect.config_flow.AladdinConnectClient", + return_value=mock_aladdinconnect_api, + ), + patch( + "homeassistant.components.aladdin_connect.cover.async_setup_entry", + return_value=True, + ), + patch( + "homeassistant.components.aladdin_connect.cover.async_setup_entry", + return_value=True, + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], diff --git a/tests/components/aladdin_connect/test_cover.py b/tests/components/aladdin_connect/test_cover.py index ba82ec6589a..9ad9febc762 100644 --- a/tests/components/aladdin_connect/test_cover.py +++ b/tests/components/aladdin_connect/test_cover.py @@ -1,4 +1,5 @@ """Test the Aladdin Connect Cover.""" + from unittest.mock import AsyncMock, MagicMock, patch from AIOAladdinConnect import session_manager diff --git a/tests/components/aladdin_connect/test_diagnostics.py b/tests/components/aladdin_connect/test_diagnostics.py index 4d5fe903798..48741c77cd1 100644 --- a/tests/components/aladdin_connect/test_diagnostics.py +++ b/tests/components/aladdin_connect/test_diagnostics.py @@ -1,4 +1,5 @@ """Test AccuWeather diagnostics.""" + from unittest.mock import MagicMock, patch from syrupy import SnapshotAssertion diff --git a/tests/components/aladdin_connect/test_init.py b/tests/components/aladdin_connect/test_init.py index 2fc09d1641d..c995fb5074d 100644 --- a/tests/components/aladdin_connect/test_init.py +++ b/tests/components/aladdin_connect/test_init.py @@ -1,4 +1,5 @@ """Test for Aladdin Connect init logic.""" + from unittest.mock import MagicMock, patch from AIOAladdinConnect.session_manager import InvalidPasswordError @@ -25,12 +26,15 @@ async def test_setup_get_doors_errors(hass: HomeAssistant) -> None: unique_id="test-id", ) config_entry.add_to_hass(hass) - with patch( - "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.login", - return_value=True, - ), patch( - "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.get_doors", - return_value=None, + with ( + patch( + "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.login", + return_value=True, + ), + patch( + "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.get_doors", + return_value=None, + ), ): assert await hass.config_entries.async_setup(config_entry.entry_id) is True await hass.async_block_till_done() diff --git a/tests/components/aladdin_connect/test_model.py b/tests/components/aladdin_connect/test_model.py index e802ae53b74..84b1c9ae40a 100644 --- a/tests/components/aladdin_connect/test_model.py +++ b/tests/components/aladdin_connect/test_model.py @@ -1,4 +1,5 @@ """Test the Aladdin Connect model class.""" + from homeassistant.components.aladdin_connect.model import DoorDevice from homeassistant.core import HomeAssistant diff --git a/tests/components/aladdin_connect/test_sensor.py b/tests/components/aladdin_connect/test_sensor.py index dca8ecaa513..9c229e2ac5e 100644 --- a/tests/components/aladdin_connect/test_sensor.py +++ b/tests/components/aladdin_connect/test_sensor.py @@ -1,4 +1,5 @@ """Test the Aladdin Connect Sensors.""" + from datetime import timedelta from unittest.mock import AsyncMock, MagicMock, patch @@ -52,7 +53,7 @@ async def test_sensors( assert entry.disabled assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION update_entry = entity_registry.async_update_entity( - entry.entity_id, **{"disabled_by": None} + entry.entity_id, disabled_by=None ) await hass.async_block_till_done() assert update_entry != entry @@ -74,7 +75,7 @@ async def test_sensors( assert entry.disabled assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION update_entry = entity_registry.async_update_entity( - entry.entity_id, **{"disabled_by": None} + entry.entity_id, disabled_by=None ) await hass.async_block_till_done() assert update_entry != entry @@ -83,7 +84,7 @@ async def test_sensors( assert state is None update_entry = entity_registry.async_update_entity( - entry.entity_id, **{"disabled_by": None} + entry.entity_id, disabled_by=None ) await hass.async_block_till_done() async_fire_time_changed( @@ -134,7 +135,7 @@ async def test_sensors_model_01( assert entry.disabled assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION update_entry = entity_registry.async_update_entity( - entry.entity_id, **{"disabled_by": None} + entry.entity_id, disabled_by=None ) await hass.async_block_till_done() assert update_entry != entry @@ -143,7 +144,7 @@ async def test_sensors_model_01( assert state is None update_entry = entity_registry.async_update_entity( - entry.entity_id, **{"disabled_by": None} + entry.entity_id, disabled_by=None ) await hass.async_block_till_done() async_fire_time_changed( diff --git a/tests/components/alarm_control_panel/common.py b/tests/components/alarm_control_panel/common.py index e46bac2fc1f..9ec419d8cf0 100644 --- a/tests/components/alarm_control_panel/common.py +++ b/tests/components/alarm_control_panel/common.py @@ -3,7 +3,12 @@ All containing methods are legacy helpers that should not be used by new components. Instead call the service directly. """ -from homeassistant.components.alarm_control_panel import DOMAIN + +from homeassistant.components.alarm_control_panel import ( + DOMAIN, + AlarmControlPanelEntity, + AlarmControlPanelEntityFeature, +) from homeassistant.const import ( ATTR_CODE, ATTR_ENTITY_ID, @@ -15,8 +20,16 @@ from homeassistant.const import ( SERVICE_ALARM_ARM_VACATION, SERVICE_ALARM_DISARM, SERVICE_ALARM_TRIGGER, + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_ARMED_VACATION, + STATE_ALARM_DISARMED, + STATE_ALARM_TRIGGERED, ) +from tests.common import MockEntity + async def async_alarm_disarm(hass, code=None, entity_id=ENTITY_MATCH_ALL): """Send the alarm the command for disarm.""" @@ -97,3 +110,51 @@ async def async_alarm_arm_custom_bypass(hass, code=None, entity_id=ENTITY_MATCH_ await hass.services.async_call( DOMAIN, SERVICE_ALARM_ARM_CUSTOM_BYPASS, data, blocking=True ) + + +class MockAlarm(MockEntity, AlarmControlPanelEntity): + """Mock Alarm control panel class.""" + + _attr_supported_features = ( + AlarmControlPanelEntityFeature.ARM_HOME + | AlarmControlPanelEntityFeature.ARM_AWAY + | AlarmControlPanelEntityFeature.ARM_NIGHT + | AlarmControlPanelEntityFeature.TRIGGER + | AlarmControlPanelEntityFeature.ARM_VACATION + ) + + @property + def code_arm_required(self): + """Whether the code is required for arm actions.""" + return self._handle("code_arm_required") + + def alarm_arm_away(self, code=None): + """Send arm away command.""" + self._attr_state = STATE_ALARM_ARMED_AWAY + self.schedule_update_ha_state() + + def alarm_arm_home(self, code=None): + """Send arm home command.""" + self._attr_state = STATE_ALARM_ARMED_HOME + self.schedule_update_ha_state() + + def alarm_arm_night(self, code=None): + """Send arm night command.""" + self._attr_state = STATE_ALARM_ARMED_NIGHT + self.schedule_update_ha_state() + + def alarm_arm_vacation(self, code=None): + """Send arm night command.""" + self._attr_state = STATE_ALARM_ARMED_VACATION + self.schedule_update_ha_state() + + def alarm_disarm(self, code=None): + """Send disarm command.""" + if code == "1234": + self._attr_state = STATE_ALARM_DISARMED + self.schedule_update_ha_state() + + def alarm_trigger(self, code=None): + """Send alarm trigger command.""" + self._attr_state = STATE_ALARM_TRIGGERED + self.schedule_update_ha_state() diff --git a/tests/components/alarm_control_panel/conftest.py b/tests/components/alarm_control_panel/conftest.py new file mode 100644 index 00000000000..cda3d81b26e --- /dev/null +++ b/tests/components/alarm_control_panel/conftest.py @@ -0,0 +1,22 @@ +"""Fixturs for Alarm Control Panel tests.""" + +import pytest + +from tests.components.alarm_control_panel.common import MockAlarm + + +@pytest.fixture +def mock_alarm_control_panel_entities() -> dict[str, MockAlarm]: + """Mock Alarm control panel class.""" + return { + "arm_code": MockAlarm( + name="Alarm arm code", + code_arm_required=True, + unique_id="unique_arm_code", + ), + "no_arm_code": MockAlarm( + name="Alarm no arm code", + code_arm_required=False, + unique_id="unique_no_arm_code", + ), + } diff --git a/tests/components/alarm_control_panel/test_device_action.py b/tests/components/alarm_control_panel/test_device_action.py index 08ccef37336..afcfa0a7a12 100644 --- a/tests/components/alarm_control_panel/test_device_action.py +++ b/tests/components/alarm_control_panel/test_device_action.py @@ -1,4 +1,5 @@ """The tests for Alarm control panel device actions.""" + import pytest from pytest_unordered import unordered @@ -27,7 +28,9 @@ from tests.common import ( MockConfigEntry, async_get_device_automation_capabilities, async_get_device_automations, + setup_test_component_platform, ) +from tests.components.alarm_control_panel.common import MockAlarm @pytest.fixture(autouse=True, name="stub_blueprint_populate") @@ -131,12 +134,12 @@ async def test_get_actions( @pytest.mark.parametrize( ("hidden_by", "entity_category"), - ( + [ (er.RegistryEntryHider.INTEGRATION, None), (er.RegistryEntryHider.USER, None), (None, EntityCategory.CONFIG), (None, EntityCategory.DIAGNOSTIC), - ), + ], ) async def test_get_actions_hidden_auxiliary( hass: HomeAssistant, @@ -222,11 +225,12 @@ async def test_get_action_capabilities( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - enable_custom_integrations: None, + mock_alarm_control_panel_entities: dict[str, MockAlarm], ) -> None: """Test we get the expected capabilities from a sensor trigger.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() + setup_test_component_platform( + hass, DOMAIN, mock_alarm_control_panel_entities.values() + ) assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) await hass.async_block_till_done() @@ -239,7 +243,7 @@ async def test_get_action_capabilities( entity_registry.async_get_or_create( DOMAIN, "test", - platform.ENTITIES["no_arm_code"].unique_id, + mock_alarm_control_panel_entities["no_arm_code"].unique_id, device_id=device_entry.id, ) @@ -269,11 +273,12 @@ async def test_get_action_capabilities_legacy( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - enable_custom_integrations: None, + mock_alarm_control_panel_entities: dict[str, MockAlarm], ) -> None: """Test we get the expected capabilities from a sensor trigger.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() + setup_test_component_platform( + hass, DOMAIN, mock_alarm_control_panel_entities.values() + ) assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) await hass.async_block_till_done() @@ -286,7 +291,7 @@ async def test_get_action_capabilities_legacy( entity_registry.async_get_or_create( DOMAIN, "test", - platform.ENTITIES["no_arm_code"].unique_id, + mock_alarm_control_panel_entities["no_arm_code"].unique_id, device_id=device_entry.id, ) @@ -317,11 +322,12 @@ async def test_get_action_capabilities_arm_code( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - enable_custom_integrations: None, + mock_alarm_control_panel_entities: dict[str, MockAlarm], ) -> None: """Test we get the expected capabilities from a sensor trigger.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() + setup_test_component_platform( + hass, DOMAIN, mock_alarm_control_panel_entities.values() + ) assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) await hass.async_block_till_done() @@ -334,7 +340,7 @@ async def test_get_action_capabilities_arm_code( entity_registry.async_get_or_create( DOMAIN, "test", - platform.ENTITIES["arm_code"].unique_id, + mock_alarm_control_panel_entities["arm_code"].unique_id, device_id=device_entry.id, ) @@ -372,11 +378,12 @@ async def test_get_action_capabilities_arm_code_legacy( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - enable_custom_integrations: None, + mock_alarm_control_panel_entities: dict[str, MockAlarm], ) -> None: """Test we get the expected capabilities from a sensor trigger.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() + setup_test_component_platform( + hass, DOMAIN, mock_alarm_control_panel_entities.values() + ) assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) await hass.async_block_till_done() @@ -389,7 +396,7 @@ async def test_get_action_capabilities_arm_code_legacy( entity_registry.async_get_or_create( DOMAIN, "test", - platform.ENTITIES["arm_code"].unique_id, + mock_alarm_control_panel_entities["arm_code"].unique_id, device_id=device_entry.id, ) @@ -428,11 +435,12 @@ async def test_action( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - enable_custom_integrations: None, + mock_alarm_control_panel_entities: dict[str, MockAlarm], ) -> None: """Test for turn_on and turn_off actions.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() + setup_test_component_platform( + hass, DOMAIN, mock_alarm_control_panel_entities.values() + ) config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) @@ -443,7 +451,7 @@ async def test_action( entity_entry = entity_registry.async_get_or_create( DOMAIN, "test", - platform.ENTITIES["no_arm_code"].unique_id, + mock_alarm_control_panel_entities["no_arm_code"].unique_id, device_id=device_entry.id, ) @@ -559,11 +567,12 @@ async def test_action_legacy( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - enable_custom_integrations: None, + mock_alarm_control_panel_entities: dict[str, MockAlarm], ) -> None: """Test for turn_on and turn_off actions.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() + setup_test_component_platform( + hass, DOMAIN, mock_alarm_control_panel_entities.values() + ) config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) @@ -574,7 +583,7 @@ async def test_action_legacy( entity_entry = entity_registry.async_get_or_create( DOMAIN, "test", - platform.ENTITIES["no_arm_code"].unique_id, + mock_alarm_control_panel_entities["no_arm_code"].unique_id, device_id=device_entry.id, ) diff --git a/tests/components/alarm_control_panel/test_device_condition.py b/tests/components/alarm_control_panel/test_device_condition.py index 6e85c94379f..d95574b7c9f 100644 --- a/tests/components/alarm_control_panel/test_device_condition.py +++ b/tests/components/alarm_control_panel/test_device_condition.py @@ -1,4 +1,5 @@ """The tests for Alarm control panel device conditions.""" + import pytest from pytest_unordered import unordered @@ -138,12 +139,12 @@ async def test_get_conditions( @pytest.mark.parametrize( ("hidden_by", "entity_category"), - ( + [ (er.RegistryEntryHider.INTEGRATION, None), (er.RegistryEntryHider.USER, None), (None, EntityCategory.CONFIG), (None, EntityCategory.DIAGNOSTIC), - ), + ], ) async def test_get_conditions_hidden_auxiliary( hass: HomeAssistant, diff --git a/tests/components/alarm_control_panel/test_device_trigger.py b/tests/components/alarm_control_panel/test_device_trigger.py index 70d700bb290..be241ef241e 100644 --- a/tests/components/alarm_control_panel/test_device_trigger.py +++ b/tests/components/alarm_control_panel/test_device_trigger.py @@ -1,4 +1,5 @@ """The tests for Alarm control panel device triggers.""" + from datetime import timedelta import pytest @@ -130,12 +131,12 @@ async def test_get_triggers( @pytest.mark.parametrize( ("hidden_by", "entity_category"), - ( + [ (er.RegistryEntryHider.INTEGRATION, None), (er.RegistryEntryHider.USER, None), (None, EntityCategory.CONFIG), (None, EntityCategory.DIAGNOSTIC), - ), + ], ) async def test_get_triggers_hidden_auxiliary( hass: HomeAssistant, diff --git a/tests/components/alarm_control_panel/test_reproduce_state.py b/tests/components/alarm_control_panel/test_reproduce_state.py index 61bb8f18397..c7984b0793e 100644 --- a/tests/components/alarm_control_panel/test_reproduce_state.py +++ b/tests/components/alarm_control_panel/test_reproduce_state.py @@ -1,4 +1,5 @@ """Test reproduce state for Alarm control panel.""" + import pytest from homeassistant.const import ( diff --git a/tests/components/alarm_control_panel/test_significant_change.py b/tests/components/alarm_control_panel/test_significant_change.py index d65a1d5cb00..e614467098a 100644 --- a/tests/components/alarm_control_panel/test_significant_change.py +++ b/tests/components/alarm_control_panel/test_significant_change.py @@ -1,4 +1,5 @@ """Test the Alarm Control Panel significant change platform.""" + import pytest from homeassistant.components.alarm_control_panel import ( diff --git a/tests/components/alarmdecoder/test_config_flow.py b/tests/components/alarmdecoder/test_config_flow.py index 22df6e445c3..614d055405e 100644 --- a/tests/components/alarmdecoder/test_config_flow.py +++ b/tests/components/alarmdecoder/test_config_flow.py @@ -1,4 +1,5 @@ """Test the AlarmDecoder config flow.""" + from unittest.mock import patch from alarmdecoder.util import NoDeviceError @@ -73,12 +74,14 @@ async def test_setups(hass: HomeAssistant, protocol, connection, title) -> None: assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "protocol" - with patch("homeassistant.components.alarmdecoder.config_flow.AdExt.open"), patch( - "homeassistant.components.alarmdecoder.config_flow.AdExt.close" - ), patch( - "homeassistant.components.alarmdecoder.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch("homeassistant.components.alarmdecoder.config_flow.AdExt.open"), + patch("homeassistant.components.alarmdecoder.config_flow.AdExt.close"), + patch( + "homeassistant.components.alarmdecoder.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result = await hass.config_entries.flow.async_configure( result["flow_id"], connection ) @@ -116,20 +119,26 @@ async def test_setup_connection_error(hass: HomeAssistant) -> None: assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "protocol" - with patch( - "homeassistant.components.alarmdecoder.config_flow.AdExt.open", - side_effect=NoDeviceError, - ), patch("homeassistant.components.alarmdecoder.config_flow.AdExt.close"): + with ( + patch( + "homeassistant.components.alarmdecoder.config_flow.AdExt.open", + side_effect=NoDeviceError, + ), + patch("homeassistant.components.alarmdecoder.config_flow.AdExt.close"), + ): result = await hass.config_entries.flow.async_configure( result["flow_id"], connection_settings ) assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} - with patch( - "homeassistant.components.alarmdecoder.config_flow.AdExt.open", - side_effect=Exception, - ), patch("homeassistant.components.alarmdecoder.config_flow.AdExt.close"): + with ( + patch( + "homeassistant.components.alarmdecoder.config_flow.AdExt.open", + side_effect=Exception, + ), + patch("homeassistant.components.alarmdecoder.config_flow.AdExt.close"), + ): result = await hass.config_entries.flow.async_configure( result["flow_id"], connection_settings ) diff --git a/tests/components/alert/test_init.py b/tests/components/alert/test_init.py index 8dfbb437646..7c4030b56da 100644 --- a/tests/components/alert/test_init.py +++ b/tests/components/alert/test_init.py @@ -342,6 +342,6 @@ async def test_done_message_state_tracker_reset_on_cancel(hass: HomeAssistant) - entity._cancel = lambda *args: None assert entity._send_done_message is False entity._send_done_message = True - hass.async_add_job(entity.end_alerting) + await entity.end_alerting() await hass.async_block_till_done() assert entity._send_done_message is False diff --git a/tests/components/alert/test_reproduce_state.py b/tests/components/alert/test_reproduce_state.py index bca2a22ce7e..e0f817443dc 100644 --- a/tests/components/alert/test_reproduce_state.py +++ b/tests/components/alert/test_reproduce_state.py @@ -1,4 +1,5 @@ """Test reproduce state for Alert.""" + import pytest from homeassistant.core import HomeAssistant, State diff --git a/tests/components/alexa/test_auth.py b/tests/components/alexa/test_auth.py index 6a1865d96e0..8d4308ba792 100644 --- a/tests/components/alexa/test_auth.py +++ b/tests/components/alexa/test_auth.py @@ -1,4 +1,5 @@ """Test Alexa auth endpoints.""" + from homeassistant.components.alexa.auth import Auth from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.core import HomeAssistant diff --git a/tests/components/alexa/test_capabilities.py b/tests/components/alexa/test_capabilities.py index 5011fee8838..7efc851a9c5 100644 --- a/tests/components/alexa/test_capabilities.py +++ b/tests/components/alexa/test_capabilities.py @@ -1,4 +1,5 @@ """Test Alexa capabilities.""" + from typing import Any from unittest.mock import patch diff --git a/tests/components/alexa/test_common.py b/tests/components/alexa/test_common.py index 8c9cea526b6..0cc4d995efa 100644 --- a/tests/components/alexa/test_common.py +++ b/tests/components/alexa/test_common.py @@ -1,4 +1,5 @@ """Test helpers for the Alexa integration.""" + from unittest.mock import Mock from uuid import uuid4 diff --git a/tests/components/alexa/test_config.py b/tests/components/alexa/test_config.py index 9536e3d471b..62330c8059c 100644 --- a/tests/components/alexa/test_config.py +++ b/tests/components/alexa/test_config.py @@ -1,4 +1,5 @@ """Test config.""" + import asyncio from unittest.mock import patch diff --git a/tests/components/alexa/test_entities.py b/tests/components/alexa/test_entities.py index c7949253af0..9ec490c4f83 100644 --- a/tests/components/alexa/test_entities.py +++ b/tests/components/alexa/test_entities.py @@ -1,4 +1,5 @@ """Test Alexa entity representation.""" + from typing import Any from unittest.mock import patch diff --git a/tests/components/alexa/test_init.py b/tests/components/alexa/test_init.py index 279fbfc4cef..3c6c54b7c76 100644 --- a/tests/components/alexa/test_init.py +++ b/tests/components/alexa/test_init.py @@ -1,4 +1,5 @@ """Tests for alexa.""" + from homeassistant.components.alexa.const import EVENT_ALEXA_SMART_HOME from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component @@ -11,6 +12,7 @@ async def test_humanify_alexa_event(hass: HomeAssistant) -> None: hass.config.components.add("recorder") await async_setup_component(hass, "alexa", {}) await async_setup_component(hass, "logbook", {}) + await hass.async_block_till_done() hass.states.async_set("light.kitchen", "on", {"friendly_name": "Kitchen Light"}) results = mock_humanify( diff --git a/tests/components/alexa/test_intent.py b/tests/components/alexa/test_intent.py index c63825b3c12..4670db4ffa9 100644 --- a/tests/components/alexa/test_intent.py +++ b/tests/components/alexa/test_intent.py @@ -1,4 +1,5 @@ """The tests for the Alexa component.""" + from http import HTTPStatus import json diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 97b8bac4cd1..b7e6a5e53ac 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -1,4 +1,5 @@ """Test for smart home alexa support.""" + from typing import Any from unittest.mock import AsyncMock, MagicMock, patch @@ -5463,9 +5464,8 @@ async def test_camera_discovery(hass: HomeAssistant, mock_stream: None) -> None: ) hass.config.components.add("cloud") - with patch.object( - hass.components.cloud, - "async_remote_ui_url", + with patch( + "homeassistant.components.cloud.async_remote_ui_url", return_value="https://example.nabu.casa", ): appliance = await discovery_test(device, hass) @@ -5494,9 +5494,8 @@ async def test_camera_discovery_without_stream(hass: HomeAssistant) -> None: ) hass.config.components.add("cloud") - with patch.object( - hass.components.cloud, - "async_remote_ui_url", + with patch( + "homeassistant.components.cloud.async_remote_ui_url", return_value="https://example.nabu.casa", ): appliance = await discovery_test(device, hass) diff --git a/tests/components/alexa/test_smart_home_http.py b/tests/components/alexa/test_smart_home_http.py index 1426eac5c5d..153442552a4 100644 --- a/tests/components/alexa/test_smart_home_http.py +++ b/tests/components/alexa/test_smart_home_http.py @@ -1,4 +1,5 @@ """Test Smart Home HTTP endpoints.""" + from http import HTTPStatus import json import logging diff --git a/tests/components/alexa/test_state_report.py b/tests/components/alexa/test_state_report.py index 08d198145df..6bd7caccc38 100644 --- a/tests/components/alexa/test_state_report.py +++ b/tests/components/alexa/test_state_report.py @@ -1,4 +1,5 @@ """Test report state.""" + import json from unittest.mock import AsyncMock, patch @@ -409,7 +410,7 @@ async def test_report_state_number( } if unit: - state["unit_of_measurement"]: unit + state["unit_of_measurement"] = unit hass.states.async_set( f"{domain}.test_{domain}", diff --git a/tests/components/amberelectric/conftest.py b/tests/components/amberelectric/conftest.py index f7d7d6623e1..8912c248a71 100644 --- a/tests/components/amberelectric/conftest.py +++ b/tests/components/amberelectric/conftest.py @@ -1,4 +1,5 @@ """Provide common Amber fixtures.""" + from collections.abc import Generator from unittest.mock import AsyncMock, patch diff --git a/tests/components/amberelectric/test_binary_sensor.py b/tests/components/amberelectric/test_binary_sensor.py index fb95cd1c41e..92877c57c61 100644 --- a/tests/components/amberelectric/test_binary_sensor.py +++ b/tests/components/amberelectric/test_binary_sensor.py @@ -1,4 +1,5 @@ """Test the Amber Electric Sensors.""" + from __future__ import annotations from collections.abc import AsyncGenerator diff --git a/tests/components/amberelectric/test_coordinator.py b/tests/components/amberelectric/test_coordinator.py index 7808d1adcde..cb3912cb5ac 100644 --- a/tests/components/amberelectric/test_coordinator.py +++ b/tests/components/amberelectric/test_coordinator.py @@ -1,4 +1,5 @@ """Tests for the Amber Electric Data Coordinator.""" + from __future__ import annotations from collections.abc import Generator diff --git a/tests/components/amberelectric/test_sensor.py b/tests/components/amberelectric/test_sensor.py index 286345dba10..c2d4886bbe9 100644 --- a/tests/components/amberelectric/test_sensor.py +++ b/tests/components/amberelectric/test_sensor.py @@ -1,4 +1,5 @@ """Test the Amber Electric Sensors.""" + from collections.abc import AsyncGenerator from unittest.mock import Mock, patch diff --git a/tests/components/ambiclimate/test_config_flow.py b/tests/components/ambiclimate/test_config_flow.py index 162bc43c6c0..e9b85eaaa40 100644 --- a/tests/components/ambiclimate/test_config_flow.py +++ b/tests/components/ambiclimate/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for the Ambiclimate config flow.""" + from unittest.mock import AsyncMock, patch import ambiclimate @@ -6,6 +7,7 @@ import pytest from homeassistant import config_entries, data_entry_flow from homeassistant.components.ambiclimate import config_flow +from homeassistant.components.http import KEY_HASS from homeassistant.config import async_process_ha_core_config from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.core import HomeAssistant @@ -127,11 +129,11 @@ async def test_view(hass: HomeAssistant) -> None: request = aiohttp.MockRequest( b"", query_string="code=test_code", mock_source="test" ) - request.app = {"hass": hass} + request.app = {KEY_HASS: hass} view = config_flow.AmbiclimateAuthCallbackView() assert await view.get(request) == "OK!" request = aiohttp.MockRequest(b"", query_string="", mock_source="test") - request.app = {"hass": hass} + request.app = {KEY_HASS: hass} view = config_flow.AmbiclimateAuthCallbackView() assert await view.get(request) == "No code" diff --git a/tests/components/ambient_station/conftest.py b/tests/components/ambient_station/conftest.py index ab5eb6239c8..adbd6777727 100644 --- a/tests/components/ambient_station/conftest.py +++ b/tests/components/ambient_station/conftest.py @@ -1,4 +1,5 @@ """Define test fixtures for Ambient PWS.""" + import json from unittest.mock import AsyncMock, Mock, patch @@ -52,10 +53,13 @@ def data_station_fixture(): @pytest.fixture(name="mock_aioambient") async def mock_aioambient_fixture(api): """Define a fixture to patch aioambient.""" - with patch( - "homeassistant.components.ambient_station.config_flow.API", - return_value=api, - ), patch("aioambient.websocket.Websocket.connect"): + with ( + patch( + "homeassistant.components.ambient_station.config_flow.API", + return_value=api, + ), + patch("aioambient.websocket.Websocket.connect"), + ): yield diff --git a/tests/components/ambient_station/test_config_flow.py b/tests/components/ambient_station/test_config_flow.py index 23124f5ec43..ae3af962b0a 100644 --- a/tests/components/ambient_station/test_config_flow.py +++ b/tests/components/ambient_station/test_config_flow.py @@ -1,4 +1,5 @@ """Define tests for the Ambient PWS config flow.""" + from unittest.mock import AsyncMock, patch from aioambient.errors import AmbientError diff --git a/tests/components/ambient_station/test_diagnostics.py b/tests/components/ambient_station/test_diagnostics.py index 4c7a0f66f6a..bc034d0e6f3 100644 --- a/tests/components/ambient_station/test_diagnostics.py +++ b/tests/components/ambient_station/test_diagnostics.py @@ -1,4 +1,5 @@ """Test Ambient PWS diagnostics.""" + from syrupy import SnapshotAssertion from homeassistant.components.ambient_station import DOMAIN diff --git a/tests/components/analytics/test_analytics.py b/tests/components/analytics/test_analytics.py index f6b813606a6..da8d45d41ad 100644 --- a/tests/components/analytics/test_analytics.py +++ b/tests/components/analytics/test_analytics.py @@ -1,4 +1,5 @@ """The tests for the analytics .""" + from collections.abc import Generator from typing import Any from unittest.mock import AsyncMock, Mock, PropertyMock, patch @@ -90,12 +91,15 @@ async def test_load_with_supervisor_diagnostics(hass: HomeAssistant) -> None: """Test loading with a supervisor that has diagnostics enabled.""" analytics = Analytics(hass) assert not analytics.preferences[ATTR_DIAGNOSTICS] - with patch( - "homeassistant.components.hassio.get_supervisor_info", - side_effect=Mock(return_value={"diagnostics": True}), - ), patch( - "homeassistant.components.hassio.is_hassio", - side_effect=Mock(return_value=True), + with ( + patch( + "homeassistant.components.hassio.get_supervisor_info", + side_effect=Mock(return_value={"diagnostics": True}), + ), + patch( + "homeassistant.components.hassio.is_hassio", + side_effect=Mock(return_value=True), + ), ): await analytics.load() assert analytics.preferences[ATTR_DIAGNOSTICS] @@ -108,12 +112,15 @@ async def test_load_with_supervisor_without_diagnostics(hass: HomeAssistant) -> assert analytics.preferences[ATTR_DIAGNOSTICS] - with patch( - "homeassistant.components.hassio.get_supervisor_info", - side_effect=Mock(return_value={"diagnostics": False}), - ), patch( - "homeassistant.components.hassio.is_hassio", - side_effect=Mock(return_value=True), + with ( + patch( + "homeassistant.components.hassio.get_supervisor_info", + side_effect=Mock(return_value={"diagnostics": False}), + ), + patch( + "homeassistant.components.hassio.is_hassio", + side_effect=Mock(return_value=True), + ), ): await analytics.load() @@ -189,23 +196,29 @@ async def test_send_base_with_supervisor( await analytics.save_preferences({ATTR_BASE: True}) assert analytics.preferences[ATTR_BASE] - with patch( - "homeassistant.components.hassio.get_supervisor_info", - side_effect=Mock( - return_value={"supported": True, "healthy": True, "arch": "amd64"} + with ( + patch( + "homeassistant.components.hassio.get_supervisor_info", + side_effect=Mock( + return_value={"supported": True, "healthy": True, "arch": "amd64"} + ), + ), + patch( + "homeassistant.components.hassio.get_os_info", + side_effect=Mock(return_value={"board": "blue", "version": "123"}), + ), + patch( + "homeassistant.components.hassio.get_info", + side_effect=Mock(return_value={}), + ), + patch( + "homeassistant.components.hassio.get_host_info", + side_effect=Mock(return_value={}), + ), + patch( + "homeassistant.components.hassio.is_hassio", + side_effect=Mock(return_value=True), ), - ), patch( - "homeassistant.components.hassio.get_os_info", - side_effect=Mock(return_value={"board": "blue", "version": "123"}), - ), patch( - "homeassistant.components.hassio.get_info", - side_effect=Mock(return_value={}), - ), patch( - "homeassistant.components.hassio.get_host_info", - side_effect=Mock(return_value={}), - ), patch( - "homeassistant.components.hassio.is_hassio", - side_effect=Mock(return_value=True), ): await analytics.load() @@ -269,38 +282,45 @@ async def test_send_usage_with_supervisor( assert analytics.preferences[ATTR_USAGE] hass.config.components = ["default_config"] - with patch( - "homeassistant.components.hassio.get_supervisor_info", - side_effect=Mock( - return_value={ - "healthy": True, - "supported": True, - "arch": "amd64", - "addons": [{"slug": "test_addon"}], - } + with ( + patch( + "homeassistant.components.hassio.get_supervisor_info", + side_effect=Mock( + return_value={ + "healthy": True, + "supported": True, + "arch": "amd64", + "addons": [{"slug": "test_addon"}], + } + ), ), - ), patch( - "homeassistant.components.hassio.get_os_info", - side_effect=Mock(return_value={}), - ), patch( - "homeassistant.components.hassio.get_info", - side_effect=Mock(return_value={}), - ), patch( - "homeassistant.components.hassio.get_host_info", - side_effect=Mock(return_value={}), - ), patch( - "homeassistant.components.hassio.async_get_addon_info", - side_effect=AsyncMock( - return_value={ - "slug": "test_addon", - "protected": True, - "version": "1", - "auto_update": False, - } + patch( + "homeassistant.components.hassio.get_os_info", + side_effect=Mock(return_value={}), + ), + patch( + "homeassistant.components.hassio.get_info", + side_effect=Mock(return_value={}), + ), + patch( + "homeassistant.components.hassio.get_host_info", + side_effect=Mock(return_value={}), + ), + patch( + "homeassistant.components.hassio.async_get_addon_info", + side_effect=AsyncMock( + return_value={ + "slug": "test_addon", + "protected": True, + "version": "1", + "auto_update": False, + } + ), + ), + patch( + "homeassistant.components.hassio.is_hassio", + side_effect=Mock(return_value=True), ), - ), patch( - "homeassistant.components.hassio.is_hassio", - side_effect=Mock(return_value=True), ): await analytics.send_analytics() @@ -460,9 +480,12 @@ async def test_send_statistics_async_get_integration_unknown_exception( assert analytics.preferences[ATTR_STATISTICS] hass.config.components = ["default_config"] - with pytest.raises(ValueError), patch( - "homeassistant.components.analytics.analytics.async_get_integrations", - return_value={"any": ValueError()}, + with ( + pytest.raises(ValueError), + patch( + "homeassistant.components.analytics.analytics.async_get_integrations", + return_value={"any": ValueError()}, + ), ): await analytics.send_analytics() @@ -481,38 +504,45 @@ async def test_send_statistics_with_supervisor( assert analytics.preferences[ATTR_BASE] assert analytics.preferences[ATTR_STATISTICS] - with patch( - "homeassistant.components.hassio.get_supervisor_info", - side_effect=Mock( - return_value={ - "healthy": True, - "supported": True, - "arch": "amd64", - "addons": [{"slug": "test_addon"}], - } + with ( + patch( + "homeassistant.components.hassio.get_supervisor_info", + side_effect=Mock( + return_value={ + "healthy": True, + "supported": True, + "arch": "amd64", + "addons": [{"slug": "test_addon"}], + } + ), ), - ), patch( - "homeassistant.components.hassio.get_os_info", - side_effect=Mock(return_value={}), - ), patch( - "homeassistant.components.hassio.get_info", - side_effect=Mock(return_value={}), - ), patch( - "homeassistant.components.hassio.get_host_info", - side_effect=Mock(return_value={}), - ), patch( - "homeassistant.components.hassio.async_get_addon_info", - side_effect=AsyncMock( - return_value={ - "slug": "test_addon", - "protected": True, - "version": "1", - "auto_update": False, - } + patch( + "homeassistant.components.hassio.get_os_info", + side_effect=Mock(return_value={}), + ), + patch( + "homeassistant.components.hassio.get_info", + side_effect=Mock(return_value={}), + ), + patch( + "homeassistant.components.hassio.get_host_info", + side_effect=Mock(return_value={}), + ), + patch( + "homeassistant.components.hassio.async_get_addon_info", + side_effect=AsyncMock( + return_value={ + "slug": "test_addon", + "protected": True, + "version": "1", + "auto_update": False, + } + ), + ), + patch( + "homeassistant.components.hassio.is_hassio", + side_effect=Mock(return_value=True), ), - ), patch( - "homeassistant.components.hassio.is_hassio", - side_effect=Mock(return_value=True), ): await analytics.send_analytics() @@ -640,12 +670,16 @@ async def test_send_with_no_energy( await analytics.save_preferences({ATTR_BASE: True, ATTR_USAGE: True}) - with patch( - "homeassistant.components.analytics.analytics.energy_is_configured", AsyncMock() - ) as energy_is_configured, patch( - "homeassistant.components.analytics.analytics.get_recorder_instance", - Mock(), - ) as get_recorder_instance: + with ( + patch( + "homeassistant.components.analytics.analytics.energy_is_configured", + AsyncMock(), + ) as energy_is_configured, + patch( + "homeassistant.components.analytics.analytics.get_recorder_instance", + Mock(), + ) as get_recorder_instance, + ): energy_is_configured.return_value = False get_recorder_instance.return_value = Mock(database_engine=Mock()) await analytics.send_analytics() @@ -848,21 +882,24 @@ async def test_not_check_config_entries_if_yaml( ) mock_config_entry.add_to_hass(hass) - with patch( - "homeassistant.components.analytics.analytics.async_get_integrations", - return_value={ - "default_config": mock_integration( - hass, - MockModule( - "default_config", - async_setup=AsyncMock(return_value=True), - partial_manifest={"config_flow": True}, + with ( + patch( + "homeassistant.components.analytics.analytics.async_get_integrations", + return_value={ + "default_config": mock_integration( + hass, + MockModule( + "default_config", + async_setup=AsyncMock(return_value=True), + partial_manifest={"config_flow": True}, + ), ), - ), - }, - ), patch( - "homeassistant.config.load_yaml_config_file", - return_value={"default_config": {}}, + }, + ), + patch( + "homeassistant.config.load_yaml_config_file", + return_value={"default_config": {}}, + ), ): await analytics.send_analytics() diff --git a/tests/components/analytics/test_init.py b/tests/components/analytics/test_init.py index a9cb5d28767..cf8d4838415 100644 --- a/tests/components/analytics/test_init.py +++ b/tests/components/analytics/test_init.py @@ -1,4 +1,5 @@ """The tests for the analytics .""" + from unittest.mock import patch from homeassistant.components.analytics.const import ANALYTICS_ENDPOINT_URL, DOMAIN diff --git a/tests/components/analytics_insights/__init__.py b/tests/components/analytics_insights/__init__.py index 9e20a72c438..7dad32c2ed5 100644 --- a/tests/components/analytics_insights/__init__.py +++ b/tests/components/analytics_insights/__init__.py @@ -1,4 +1,5 @@ """Tests for the Homeassistant Analytics integration.""" + from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry diff --git a/tests/components/analytics_insights/conftest.py b/tests/components/analytics_insights/conftest.py index 6ca98d294e6..03bd24faeea 100644 --- a/tests/components/analytics_insights/conftest.py +++ b/tests/components/analytics_insights/conftest.py @@ -1,4 +1,5 @@ """Common fixtures for the Homeassistant Analytics tests.""" + from collections.abc import Generator from unittest.mock import AsyncMock, patch @@ -28,12 +29,15 @@ def mock_setup_entry() -> Generator[AsyncMock, None, None]: @pytest.fixture def mock_analytics_client() -> Generator[AsyncMock, None, None]: """Mock a Homeassistant Analytics client.""" - with patch( - "homeassistant.components.analytics_insights.HomeassistantAnalyticsClient", - autospec=True, - ) as mock_client, patch( - "homeassistant.components.analytics_insights.config_flow.HomeassistantAnalyticsClient", - new=mock_client, + with ( + patch( + "homeassistant.components.analytics_insights.HomeassistantAnalyticsClient", + autospec=True, + ) as mock_client, + patch( + "homeassistant.components.analytics_insights.config_flow.HomeassistantAnalyticsClient", + new=mock_client, + ), ): client = mock_client.return_value client.get_current_analytics.return_value = CurrentAnalytics.from_json( diff --git a/tests/components/analytics_insights/snapshots/test_sensor.ambr b/tests/components/analytics_insights/snapshots/test_sensor.ambr index 80e53d18e98..d7eeed7955c 100644 --- a/tests/components/analytics_insights/snapshots/test_sensor.ambr +++ b/tests/components/analytics_insights/snapshots/test_sensor.ambr @@ -44,6 +44,7 @@ 'context': , 'entity_id': 'sensor.homeassistant_analytics_hacs_custom', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '157481', }) @@ -93,6 +94,7 @@ 'context': , 'entity_id': 'sensor.homeassistant_analytics_myq', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0', }) @@ -142,6 +144,7 @@ 'context': , 'entity_id': 'sensor.homeassistant_analytics_spotify', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '24388', }) @@ -191,6 +194,7 @@ 'context': , 'entity_id': 'sensor.homeassistant_analytics_youtube', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '339', }) diff --git a/tests/components/analytics_insights/test_config_flow.py b/tests/components/analytics_insights/test_config_flow.py index 6ddbe285df7..49ec0ce8d52 100644 --- a/tests/components/analytics_insights/test_config_flow.py +++ b/tests/components/analytics_insights/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Homeassistant Analytics config flow.""" + from typing import Any from unittest.mock import AsyncMock diff --git a/tests/components/analytics_insights/test_init.py b/tests/components/analytics_insights/test_init.py index 08b898f13c1..4f1ca7cda95 100644 --- a/tests/components/analytics_insights/test_init.py +++ b/tests/components/analytics_insights/test_init.py @@ -1,4 +1,5 @@ """Test the Home Assistant analytics init module.""" + from __future__ import annotations from unittest.mock import AsyncMock diff --git a/tests/components/analytics_insights/test_sensor.py b/tests/components/analytics_insights/test_sensor.py index 83ea2885456..e0850bbd55b 100644 --- a/tests/components/analytics_insights/test_sensor.py +++ b/tests/components/analytics_insights/test_sensor.py @@ -1,4 +1,5 @@ """Test the Home Assistant analytics sensor module.""" + from datetime import timedelta from unittest.mock import AsyncMock, patch diff --git a/tests/components/android_ip_webcam/conftest.py b/tests/components/android_ip_webcam/conftest.py index 83a040222f8..17fc3e451a3 100644 --- a/tests/components/android_ip_webcam/conftest.py +++ b/tests/components/android_ip_webcam/conftest.py @@ -1,4 +1,5 @@ """Fixtures for tests.""" + from http import HTTPStatus import pytest diff --git a/tests/components/android_ip_webcam/test_config_flow.py b/tests/components/android_ip_webcam/test_config_flow.py index 881585ed5dc..2e4522188eb 100644 --- a/tests/components/android_ip_webcam/test_config_flow.py +++ b/tests/components/android_ip_webcam/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Android IP Webcam config flow.""" + from unittest.mock import Mock, patch import aiohttp diff --git a/tests/components/android_ip_webcam/test_init.py b/tests/components/android_ip_webcam/test_init.py index fa5f551e9b1..9aa677b8708 100644 --- a/tests/components/android_ip_webcam/test_init.py +++ b/tests/components/android_ip_webcam/test_init.py @@ -1,6 +1,5 @@ """Tests for the Android IP Webcam integration.""" - from unittest.mock import Mock import aiohttp diff --git a/tests/components/androidtv/patchers.py b/tests/components/androidtv/patchers.py index aae99b34438..67393a21f41 100644 --- a/tests/components/androidtv/patchers.py +++ b/tests/components/androidtv/patchers.py @@ -1,4 +1,5 @@ """Define patches used for androidtv tests.""" + from unittest.mock import patch from androidtv.constants import CMD_DEVICE_PROPERTIES, CMD_MAC_ETH0, CMD_MAC_WLAN0 diff --git a/tests/components/androidtv/test_config_flow.py b/tests/components/androidtv/test_config_flow.py index ad7d3be290d..afebe9903ce 100644 --- a/tests/components/androidtv/test_config_flow.py +++ b/tests/components/androidtv/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for the AndroidTV config flow.""" + from typing import Any from unittest.mock import patch @@ -107,10 +108,13 @@ async def test_user( assert flow_result["step_id"] == "user" # test with all provided - with patch( - CONNECT_METHOD, - return_value=(MockConfigDevice(eth_mac, wifi_mac), None), - ), PATCH_SETUP_ENTRY as mock_setup_entry: + with ( + patch( + CONNECT_METHOD, + return_value=(MockConfigDevice(eth_mac, wifi_mac), None), + ), + PATCH_SETUP_ENTRY as mock_setup_entry, + ): result = await hass.config_entries.flow.async_configure( flow_result["flow_id"], user_input=config ) @@ -128,10 +132,15 @@ async def test_user_adbkey(hass: HomeAssistant) -> None: config_data = CONFIG_PYTHON_ADB.copy() config_data[CONF_ADBKEY] = ADBKEY - with patch( - CONNECT_METHOD, - return_value=(MockConfigDevice(), None), - ), PATCH_ISFILE, PATCH_ACCESS, PATCH_SETUP_ENTRY as mock_setup_entry: + with ( + patch( + CONNECT_METHOD, + return_value=(MockConfigDevice(), None), + ), + PATCH_ISFILE, + PATCH_ACCESS, + PATCH_SETUP_ENTRY as mock_setup_entry, + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER, "show_advanced_options": True}, @@ -160,10 +169,13 @@ async def test_error_both_key_server(hass: HomeAssistant) -> None: assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "key_and_server"} - with patch( - CONNECT_METHOD, - return_value=(MockConfigDevice(), None), - ), PATCH_SETUP_ENTRY: + with ( + patch( + CONNECT_METHOD, + return_value=(MockConfigDevice(), None), + ), + PATCH_SETUP_ENTRY, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=CONFIG_ADB_SERVER ) @@ -187,10 +199,13 @@ async def test_error_invalid_key(hass: HomeAssistant) -> None: assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "adbkey_not_file"} - with patch( - CONNECT_METHOD, - return_value=(MockConfigDevice(), None), - ), PATCH_SETUP_ENTRY: + with ( + patch( + CONNECT_METHOD, + return_value=(MockConfigDevice(), None), + ), + PATCH_SETUP_ENTRY, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=CONFIG_ADB_SERVER ) @@ -298,10 +313,13 @@ async def test_on_connect_failed(hass: HomeAssistant) -> None: assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} - with patch( - CONNECT_METHOD, - return_value=(MockConfigDevice(), None), - ), PATCH_SETUP_ENTRY: + with ( + patch( + CONNECT_METHOD, + return_value=(MockConfigDevice(), None), + ), + PATCH_SETUP_ENTRY, + ): result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], user_input=CONFIG_ADB_SERVER ) diff --git a/tests/components/androidtv/test_media_player.py b/tests/components/androidtv/test_media_player.py index da56133abb0..63923a57996 100644 --- a/tests/components/androidtv/test_media_player.py +++ b/tests/components/androidtv/test_media_player.py @@ -1,4 +1,5 @@ """The tests for the androidtv platform.""" + from datetime import timedelta import logging from typing import Any @@ -234,9 +235,10 @@ async def test_reconnect( patch_key, entity_id, config_entry = _setup(config) config_entry.add_to_hass(hass) - with patchers.patch_connect(True)[patch_key], patchers.patch_shell( - SHELL_RESPONSE_OFF - )[patch_key]: + with ( + patchers.patch_connect(True)[patch_key], + patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key], + ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -248,9 +250,10 @@ async def test_reconnect( caplog.clear() caplog.set_level(logging.WARNING) - with patchers.patch_connect(False)[patch_key], patchers.patch_shell(error=True)[ - patch_key - ]: + with ( + patchers.patch_connect(False)[patch_key], + patchers.patch_shell(error=True)[patch_key], + ): for _ in range(5): await async_update_entity(hass, entity_id) state = hass.states.get(entity_id) @@ -262,9 +265,11 @@ async def test_reconnect( assert caplog.record_tuples[1][1] == logging.WARNING caplog.set_level(logging.DEBUG) - with patchers.patch_connect(True)[patch_key], patchers.patch_shell( - SHELL_RESPONSE_STANDBY - )[patch_key], patchers.PATCH_SCREENCAP: + with ( + patchers.patch_connect(True)[patch_key], + patchers.patch_shell(SHELL_RESPONSE_STANDBY)[patch_key], + patchers.PATCH_SCREENCAP, + ): await async_update_entity(hass, entity_id) state = hass.states.get(entity_id) @@ -292,9 +297,10 @@ async def test_adb_shell_returns_none( patch_key, entity_id, config_entry = _setup(config) config_entry.add_to_hass(hass) - with patchers.patch_connect(True)[patch_key], patchers.patch_shell( - SHELL_RESPONSE_OFF - )[patch_key]: + with ( + patchers.patch_connect(True)[patch_key], + patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key], + ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -303,9 +309,10 @@ async def test_adb_shell_returns_none( assert state is not None assert state.state != STATE_UNAVAILABLE - with patchers.patch_shell(None)[patch_key], patchers.patch_shell(error=True)[ - patch_key - ]: + with ( + patchers.patch_shell(None)[patch_key], + patchers.patch_shell(error=True)[patch_key], + ): await async_update_entity(hass, entity_id) state = hass.states.get(entity_id) assert state is not None @@ -317,9 +324,11 @@ async def test_setup_with_adbkey(hass: HomeAssistant) -> None: patch_key, entity_id, config_entry = _setup(CONFIG_ANDROID_PYTHON_ADB_KEY) config_entry.add_to_hass(hass) - with patchers.patch_connect(True)[patch_key], patchers.patch_shell( - SHELL_RESPONSE_OFF - )[patch_key], patchers.PATCH_ISFILE: + with ( + patchers.patch_connect(True)[patch_key], + patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key], + patchers.PATCH_ISFILE, + ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -347,9 +356,10 @@ async def test_sources(hass: HomeAssistant, config: dict[str, Any]) -> None: config_entry.add_to_hass(hass) hass.config_entries.async_update_entry(config_entry, options={CONF_APPS: conf_apps}) - with patchers.patch_connect(True)[patch_key], patchers.patch_shell( - SHELL_RESPONSE_OFF - )[patch_key]: + with ( + patchers.patch_connect(True)[patch_key], + patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key], + ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -417,9 +427,10 @@ async def test_exclude_sources( config_entry, options={CONF_EXCLUDE_UNNAMED_APPS: True, CONF_APPS: conf_apps} ) - with patchers.patch_connect(True)[patch_key], patchers.patch_shell( - SHELL_RESPONSE_OFF - )[patch_key]: + with ( + patchers.patch_connect(True)[patch_key], + patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key], + ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -461,9 +472,10 @@ async def _test_select_source( config_entry.add_to_hass(hass) hass.config_entries.async_update_entry(config_entry, options={CONF_APPS: conf_apps}) - with patchers.patch_connect(True)[patch_key], patchers.patch_shell( - SHELL_RESPONSE_OFF - )[patch_key]: + with ( + patchers.patch_connect(True)[patch_key], + patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key], + ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -568,9 +580,12 @@ async def test_setup_fail( patch_key, entity_id, config_entry = _setup(config) config_entry.add_to_hass(hass) - with patchers.patch_connect(connect)[patch_key], patchers.patch_shell( - SHELL_RESPONSE_OFF, error=True, exc=AdbShellTimeoutException - )[patch_key]: + with ( + patchers.patch_connect(connect)[patch_key], + patchers.patch_shell( + SHELL_RESPONSE_OFF, error=True, exc=AdbShellTimeoutException + )[patch_key], + ): assert await hass.config_entries.async_setup(config_entry.entry_id) is False await hass.async_block_till_done() @@ -587,9 +602,10 @@ async def test_adb_command(hass: HomeAssistant) -> None: command = "test command" response = "test response" - with patchers.patch_connect(True)[patch_key], patchers.patch_shell( - SHELL_RESPONSE_OFF - )[patch_key]: + with ( + patchers.patch_connect(True)[patch_key], + patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key], + ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -616,9 +632,10 @@ async def test_adb_command_unicode_decode_error(hass: HomeAssistant) -> None: command = "test command" response = b"test response" - with patchers.patch_connect(True)[patch_key], patchers.patch_shell( - SHELL_RESPONSE_OFF - )[patch_key]: + with ( + patchers.patch_connect(True)[patch_key], + patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key], + ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -645,9 +662,10 @@ async def test_adb_command_key(hass: HomeAssistant) -> None: command = "HOME" response = None - with patchers.patch_connect(True)[patch_key], patchers.patch_shell( - SHELL_RESPONSE_OFF - )[patch_key]: + with ( + patchers.patch_connect(True)[patch_key], + patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key], + ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -674,9 +692,10 @@ async def test_adb_command_get_properties(hass: HomeAssistant) -> None: command = "GET_PROPERTIES" response = {"test key": "test value"} - with patchers.patch_connect(True)[patch_key], patchers.patch_shell( - SHELL_RESPONSE_OFF - )[patch_key]: + with ( + patchers.patch_connect(True)[patch_key], + patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key], + ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -703,9 +722,10 @@ async def test_learn_sendevent(hass: HomeAssistant) -> None: config_entry.add_to_hass(hass) response = "sendevent 1 2 3 4" - with patchers.patch_connect(True)[patch_key], patchers.patch_shell( - SHELL_RESPONSE_OFF - )[patch_key]: + with ( + patchers.patch_connect(True)[patch_key], + patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key], + ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -731,9 +751,10 @@ async def test_update_lock_not_acquired(hass: HomeAssistant) -> None: patch_key, entity_id, config_entry = _setup(CONFIG_ANDROID_DEFAULT) config_entry.add_to_hass(hass) - with patchers.patch_connect(True)[patch_key], patchers.patch_shell( - SHELL_RESPONSE_OFF - )[patch_key]: + with ( + patchers.patch_connect(True)[patch_key], + patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key], + ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -743,18 +764,22 @@ async def test_update_lock_not_acquired(hass: HomeAssistant) -> None: assert state is not None assert state.state == STATE_OFF - with patch( - "androidtv.androidtv.androidtv_async.AndroidTVAsync.update", - side_effect=LockNotAcquiredException, - ), patchers.patch_shell(SHELL_RESPONSE_STANDBY)[patch_key]: + with ( + patch( + "androidtv.androidtv.androidtv_async.AndroidTVAsync.update", + side_effect=LockNotAcquiredException, + ), + patchers.patch_shell(SHELL_RESPONSE_STANDBY)[patch_key], + ): await async_update_entity(hass, entity_id) state = hass.states.get(entity_id) assert state is not None assert state.state == STATE_OFF - with patchers.patch_shell(SHELL_RESPONSE_STANDBY)[ - patch_key - ], patchers.PATCH_SCREENCAP: + with ( + patchers.patch_shell(SHELL_RESPONSE_STANDBY)[patch_key], + patchers.PATCH_SCREENCAP, + ): await async_update_entity(hass, entity_id) state = hass.states.get(entity_id) assert state is not None @@ -768,9 +793,10 @@ async def test_download(hass: HomeAssistant) -> None: device_path = "device/path" local_path = "local/path" - with patchers.patch_connect(True)[patch_key], patchers.patch_shell( - SHELL_RESPONSE_OFF - )[patch_key]: + with ( + patchers.patch_connect(True)[patch_key], + patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key], + ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -789,10 +815,9 @@ async def test_download(hass: HomeAssistant) -> None: patch_pull.assert_not_called() # Successful download - with patch( - "androidtv.basetv.basetv_async.BaseTVAsync.adb_pull" - ) as patch_pull, patch.object( - hass.config, "is_allowed_path", return_value=True + with ( + patch("androidtv.basetv.basetv_async.BaseTVAsync.adb_pull") as patch_pull, + patch.object(hass.config, "is_allowed_path", return_value=True), ): await hass.services.async_call( DOMAIN, @@ -814,9 +839,10 @@ async def test_upload(hass: HomeAssistant) -> None: device_path = "device/path" local_path = "local/path" - with patchers.patch_connect(True)[patch_key], patchers.patch_shell( - SHELL_RESPONSE_OFF - )[patch_key]: + with ( + patchers.patch_connect(True)[patch_key], + patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key], + ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -835,10 +861,9 @@ async def test_upload(hass: HomeAssistant) -> None: patch_push.assert_not_called() # Successful upload - with patch( - "androidtv.basetv.basetv_async.BaseTVAsync.adb_push" - ) as patch_push, patch.object( - hass.config, "is_allowed_path", return_value=True + with ( + patch("androidtv.basetv.basetv_async.BaseTVAsync.adb_push") as patch_push, + patch.object(hass.config, "is_allowed_path", return_value=True), ): await hass.services.async_call( DOMAIN, @@ -858,9 +883,10 @@ async def test_androidtv_volume_set(hass: HomeAssistant) -> None: patch_key, entity_id, config_entry = _setup(CONFIG_ANDROID_DEFAULT) config_entry.add_to_hass(hass) - with patchers.patch_connect(True)[patch_key], patchers.patch_shell( - SHELL_RESPONSE_OFF - )[patch_key]: + with ( + patchers.patch_connect(True)[patch_key], + patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key], + ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -887,15 +913,17 @@ async def test_get_image_http( patch_key, entity_id, config_entry = _setup(CONFIG_ANDROID_DEFAULT) config_entry.add_to_hass(hass) - with patchers.patch_connect(True)[patch_key], patchers.patch_shell( - SHELL_RESPONSE_OFF - )[patch_key]: + with ( + patchers.patch_connect(True)[patch_key], + patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key], + ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - with patchers.patch_shell("11")[ - patch_key - ], patchers.PATCH_SCREENCAP as patch_screen_cap: + with ( + patchers.patch_shell("11")[patch_key], + patchers.PATCH_SCREENCAP as patch_screen_cap, + ): await async_update_entity(hass, entity_id) patch_screen_cap.assert_called() @@ -912,20 +940,20 @@ async def test_get_image_http( assert content == b"image" next_update = utcnow() + timedelta(seconds=30) - with patchers.patch_shell("11")[ - patch_key - ], patchers.PATCH_SCREENCAP as patch_screen_cap, patch( - "homeassistant.util.utcnow", return_value=next_update + with ( + patchers.patch_shell("11")[patch_key], + patchers.PATCH_SCREENCAP as patch_screen_cap, + patch("homeassistant.util.utcnow", return_value=next_update), ): async_fire_time_changed(hass, next_update, True) await hass.async_block_till_done() patch_screen_cap.assert_not_called() next_update = utcnow() + timedelta(seconds=60) - with patchers.patch_shell("11")[ - patch_key - ], patchers.PATCH_SCREENCAP as patch_screen_cap, patch( - "homeassistant.util.utcnow", return_value=next_update + with ( + patchers.patch_shell("11")[patch_key], + patchers.PATCH_SCREENCAP as patch_screen_cap, + patch("homeassistant.util.utcnow", return_value=next_update), ): async_fire_time_changed(hass, next_update, True) await hass.async_block_till_done() @@ -938,15 +966,19 @@ async def test_get_image_http_fail(hass: HomeAssistant) -> None: patch_key, entity_id, config_entry = _setup(CONFIG_ANDROID_DEFAULT) config_entry.add_to_hass(hass) - with patchers.patch_connect(True)[patch_key], patchers.patch_shell( - SHELL_RESPONSE_OFF - )[patch_key]: + with ( + patchers.patch_connect(True)[patch_key], + patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key], + ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - with patchers.patch_shell("11")[patch_key], patch( - "androidtv.basetv.basetv_async.BaseTVAsync.adb_screencap", - side_effect=ConnectionResetError, + with ( + patchers.patch_shell("11")[patch_key], + patch( + "androidtv.basetv.basetv_async.BaseTVAsync.adb_screencap", + side_effect=ConnectionResetError, + ), ): await async_update_entity(hass, entity_id) @@ -967,9 +999,10 @@ async def test_get_image_disabled(hass: HomeAssistant) -> None: config_entry, options={CONF_SCREENCAP: False} ) - with patchers.patch_connect(True)[patch_key], patchers.patch_shell( - SHELL_RESPONSE_OFF - )[patch_key]: + with ( + patchers.patch_connect(True)[patch_key], + patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key], + ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -1024,9 +1057,10 @@ async def test_services_androidtv(hass: HomeAssistant) -> None: assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - with patchers.patch_shell(SHELL_RESPONSE_STANDBY)[ - patch_key - ], patchers.PATCH_SCREENCAP: + with ( + patchers.patch_shell(SHELL_RESPONSE_STANDBY)[patch_key], + patchers.PATCH_SCREENCAP, + ): await _test_service( hass, entity_id, SERVICE_MEDIA_NEXT_TRACK, "media_next_track" ) @@ -1074,9 +1108,10 @@ async def test_services_firetv(hass: HomeAssistant) -> None: assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - with patchers.patch_shell(SHELL_RESPONSE_STANDBY)[ - patch_key - ], patchers.PATCH_SCREENCAP: + with ( + patchers.patch_shell(SHELL_RESPONSE_STANDBY)[patch_key], + patchers.PATCH_SCREENCAP, + ): await _test_service(hass, entity_id, SERVICE_MEDIA_STOP, "back") await _test_service(hass, entity_id, SERVICE_TURN_OFF, "adb_shell") await _test_service(hass, entity_id, SERVICE_TURN_ON, "adb_shell") @@ -1092,9 +1127,10 @@ async def test_volume_mute(hass: HomeAssistant) -> None: assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - with patchers.patch_shell(SHELL_RESPONSE_STANDBY)[ - patch_key - ], patchers.PATCH_SCREENCAP: + with ( + patchers.patch_shell(SHELL_RESPONSE_STANDBY)[patch_key], + patchers.PATCH_SCREENCAP, + ): service_data = {ATTR_ENTITY_ID: entity_id, ATTR_MEDIA_VOLUME_MUTED: True} with patch( "androidtv.androidtv.androidtv_async.AndroidTVAsync.mute_volume", @@ -1132,9 +1168,10 @@ async def test_connection_closed_on_ha_stop(hass: HomeAssistant) -> None: patch_key, _, config_entry = _setup(CONFIG_ANDROID_DEFAULT) config_entry.add_to_hass(hass) - with patchers.patch_connect(True)[patch_key], patchers.patch_shell( - SHELL_RESPONSE_OFF - )[patch_key]: + with ( + patchers.patch_connect(True)[patch_key], + patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key], + ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -1152,9 +1189,10 @@ async def test_exception(hass: HomeAssistant) -> None: patch_key, entity_id, config_entry = _setup(CONFIG_ANDROID_DEFAULT) config_entry.add_to_hass(hass) - with patchers.patch_connect(True)[patch_key], patchers.patch_shell( - SHELL_RESPONSE_OFF - )[patch_key]: + with ( + patchers.patch_connect(True)[patch_key], + patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key], + ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -1182,9 +1220,10 @@ async def test_options_reload(hass: HomeAssistant) -> None: patch_key, entity_id, config_entry = _setup(CONFIG_ANDROID_DEFAULT) config_entry.add_to_hass(hass) - with patchers.patch_connect(True)[patch_key], patchers.patch_shell( - SHELL_RESPONSE_OFF - )[patch_key]: + with ( + patchers.patch_connect(True)[patch_key], + patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key], + ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/androidtv_remote/conftest.py b/tests/components/androidtv_remote/conftest.py index b981581becd..3b69da6d742 100644 --- a/tests/components/androidtv_remote/conftest.py +++ b/tests/components/androidtv_remote/conftest.py @@ -1,4 +1,5 @@ """Fixtures for the Android TV Remote integration tests.""" + from collections.abc import Callable, Generator from unittest.mock import AsyncMock, MagicMock, patch diff --git a/tests/components/androidtv_remote/test_config_flow.py b/tests/components/androidtv_remote/test_config_flow.py index fb4bc829160..f4e141ce952 100644 --- a/tests/components/androidtv_remote/test_config_flow.py +++ b/tests/components/androidtv_remote/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Android TV Remote config flow.""" + from ipaddress import ip_address from unittest.mock import AsyncMock, MagicMock diff --git a/tests/components/androidtv_remote/test_diagnostics.py b/tests/components/androidtv_remote/test_diagnostics.py index 93410fd4511..09f38a32201 100644 --- a/tests/components/androidtv_remote/test_diagnostics.py +++ b/tests/components/androidtv_remote/test_diagnostics.py @@ -1,4 +1,5 @@ """Tests for the diagnostics data provided by the Android TV Remote integration.""" + from unittest.mock import MagicMock from syrupy.assertion import SnapshotAssertion diff --git a/tests/components/androidtv_remote/test_init.py b/tests/components/androidtv_remote/test_init.py index f3f61eb268e..ad97df2b9c9 100644 --- a/tests/components/androidtv_remote/test_init.py +++ b/tests/components/androidtv_remote/test_init.py @@ -1,4 +1,5 @@ """Tests for the Android TV Remote integration.""" + from collections.abc import Callable from unittest.mock import AsyncMock, MagicMock diff --git a/tests/components/androidtv_remote/test_media_player.py b/tests/components/androidtv_remote/test_media_player.py index 0e9013550c2..c7937e9e02d 100644 --- a/tests/components/androidtv_remote/test_media_player.py +++ b/tests/components/androidtv_remote/test_media_player.py @@ -1,4 +1,5 @@ """Tests for the Android TV Remote remote platform.""" + from unittest.mock import MagicMock, call from androidtvremote2 import ConnectionClosed diff --git a/tests/components/androidtv_remote/test_remote.py b/tests/components/androidtv_remote/test_remote.py index 5157361a158..eba955a6aba 100644 --- a/tests/components/androidtv_remote/test_remote.py +++ b/tests/components/androidtv_remote/test_remote.py @@ -1,4 +1,5 @@ """Tests for the Android TV Remote remote platform.""" + from unittest.mock import MagicMock, call from androidtvremote2 import ConnectionClosed diff --git a/tests/components/anova/__init__.py b/tests/components/anova/__init__.py index aa58ee5bbb5..03cfb7589d0 100644 --- a/tests/components/anova/__init__.py +++ b/tests/components/anova/__init__.py @@ -1,4 +1,5 @@ """Tests for the Anova integration.""" + from __future__ import annotations from unittest.mock import patch @@ -46,13 +47,15 @@ async def async_init_integration( error: str | None = None, ) -> ConfigEntry: """Set up the Anova integration in Home Assistant.""" - with patch( - "homeassistant.components.anova.coordinator.AnovaPrecisionCooker.update" - ) as update_patch, patch( - "homeassistant.components.anova.AnovaApi.authenticate" - ), patch( - "homeassistant.components.anova.AnovaApi.get_devices", - ) as device_patch: + with ( + patch( + "homeassistant.components.anova.coordinator.AnovaPrecisionCooker.update" + ) as update_patch, + patch("homeassistant.components.anova.AnovaApi.authenticate"), + patch( + "homeassistant.components.anova.AnovaApi.get_devices", + ) as device_patch, + ): update_patch.return_value = ONLINE_UPDATE device_patch.return_value = [ AnovaPrecisionCooker(None, DEVICE_UNIQUE_ID, "type_sample", None) diff --git a/tests/components/anova/conftest.py b/tests/components/anova/conftest.py index 34f713502dd..3e904bb1415 100644 --- a/tests/components/anova/conftest.py +++ b/tests/components/anova/conftest.py @@ -1,4 +1,5 @@ """Common fixtures for Anova.""" + from unittest.mock import AsyncMock, patch from anova_wifi import AnovaApi, AnovaPrecisionCooker, InvalidLogin, NoDevicesFound @@ -24,7 +25,7 @@ async def anova_api( async def get_devices_side_effect(): if not api_mock.existing_devices: api_mock.existing_devices = [] - api_mock.existing_devices = api_mock.existing_devices + [new_device] + api_mock.existing_devices = [*api_mock.existing_devices, new_device] return [new_device] api_mock.authenticate.side_effect = authenticate_side_effect @@ -50,7 +51,7 @@ async def anova_api_no_devices( api_mock.jwt = "my_test_jwt" async def get_devices_side_effect(): - raise NoDevicesFound() + raise NoDevicesFound api_mock.authenticate.side_effect = authenticate_side_effect api_mock.get_devices.side_effect = get_devices_side_effect @@ -72,7 +73,7 @@ async def anova_api_wrong_login( api_mock = AsyncMock() async def authenticate_side_effect(): - raise InvalidLogin() + raise InvalidLogin api_mock.authenticate.side_effect = authenticate_side_effect diff --git a/tests/components/anova/test_config_flow.py b/tests/components/anova/test_config_flow.py index d1255876137..6ea988dc53a 100644 --- a/tests/components/anova/test_config_flow.py +++ b/tests/components/anova/test_config_flow.py @@ -16,15 +16,16 @@ async def test_flow_user( hass: HomeAssistant, ) -> None: """Test user initialized flow.""" - with patch( - "homeassistant.components.anova.config_flow.AnovaApi.authenticate", - ) as auth_patch, patch( - "homeassistant.components.anova.AnovaApi.get_devices" - ) as device_patch, patch( - "homeassistant.components.anova.AnovaApi.authenticate" - ), patch( - "homeassistant.components.anova.config_flow.AnovaApi.get_devices" - ) as config_flow_device_patch: + with ( + patch( + "homeassistant.components.anova.config_flow.AnovaApi.authenticate", + ) as auth_patch, + patch("homeassistant.components.anova.AnovaApi.get_devices") as device_patch, + patch("homeassistant.components.anova.AnovaApi.authenticate"), + patch( + "homeassistant.components.anova.config_flow.AnovaApi.get_devices" + ) as config_flow_device_patch, + ): auth_patch.return_value = True device_patch.return_value = [ AnovaPrecisionCooker(None, DEVICE_UNIQUE_ID, "type_sample", None) @@ -50,13 +51,15 @@ async def test_flow_user( async def test_flow_user_already_configured(hass: HomeAssistant) -> None: """Test user initialized flow with duplicate device.""" - with patch( - "homeassistant.components.anova.config_flow.AnovaApi.authenticate", - ) as auth_patch, patch( - "homeassistant.components.anova.AnovaApi.get_devices" - ) as device_patch, patch( - "homeassistant.components.anova.config_flow.AnovaApi.get_devices" - ) as config_flow_device_patch: + with ( + patch( + "homeassistant.components.anova.config_flow.AnovaApi.authenticate", + ) as auth_patch, + patch("homeassistant.components.anova.AnovaApi.get_devices") as device_patch, + patch( + "homeassistant.components.anova.config_flow.AnovaApi.get_devices" + ) as config_flow_device_patch, + ): auth_patch.return_value = True device_patch.return_value = [ AnovaPrecisionCooker(None, DEVICE_UNIQUE_ID, "type_sample", None) @@ -115,11 +118,12 @@ async def test_flow_unknown_error(hass: HomeAssistant) -> None: async def test_flow_no_devices(hass: HomeAssistant) -> None: """Test unknown error throwing error.""" - with patch( - "homeassistant.components.anova.config_flow.AnovaApi.authenticate" - ), patch( - "homeassistant.components.anova.config_flow.AnovaApi.get_devices", - side_effect=NoDevicesFound(), + with ( + patch("homeassistant.components.anova.config_flow.AnovaApi.authenticate"), + patch( + "homeassistant.components.anova.config_flow.AnovaApi.get_devices", + side_effect=NoDevicesFound(), + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, diff --git a/tests/components/anthemav/conftest.py b/tests/components/anthemav/conftest.py index 4c1abdd3c9b..b615b2710c0 100644 --- a/tests/components/anthemav/conftest.py +++ b/tests/components/anthemav/conftest.py @@ -1,4 +1,5 @@ """Fixtures for anthemav integration tests.""" + from collections.abc import Callable from unittest.mock import AsyncMock, MagicMock, patch diff --git a/tests/components/anthemav/test_config_flow.py b/tests/components/anthemav/test_config_flow.py index caa76006976..a0a6bf82762 100644 --- a/tests/components/anthemav/test_config_flow.py +++ b/tests/components/anthemav/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Anthem A/V Receivers config flow.""" + from unittest.mock import AsyncMock, patch from anthemav.device_error import DeviceError diff --git a/tests/components/anthemav/test_init.py b/tests/components/anthemav/test_init.py index 019668769ed..6989ffc69c5 100644 --- a/tests/components/anthemav/test_init.py +++ b/tests/components/anthemav/test_init.py @@ -1,4 +1,5 @@ """Test the Anthem A/V Receivers config flow.""" + from collections.abc import Callable from unittest.mock import ANY, AsyncMock, patch diff --git a/tests/components/anthemav/test_media_player.py b/tests/components/anthemav/test_media_player.py index 9dd8af24efb..a9a945c86a1 100644 --- a/tests/components/anthemav/test_media_player.py +++ b/tests/components/anthemav/test_media_player.py @@ -1,4 +1,5 @@ """Test the Anthem A/V Receivers config flow.""" + from collections.abc import Callable from unittest.mock import AsyncMock diff --git a/tests/components/aosmith/conftest.py b/tests/components/aosmith/conftest.py index fe35f6b337d..74e995deaf1 100644 --- a/tests/components/aosmith/conftest.py +++ b/tests/components/aosmith/conftest.py @@ -1,4 +1,5 @@ """Common fixtures for the A. O. Smith tests.""" + from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch diff --git a/tests/components/aosmith/snapshots/test_sensor.ambr b/tests/components/aosmith/snapshots/test_sensor.ambr index 1b3043a7f3e..150e0c2934f 100644 --- a/tests/components/aosmith/snapshots/test_sensor.ambr +++ b/tests/components/aosmith/snapshots/test_sensor.ambr @@ -10,6 +10,7 @@ 'context': , 'entity_id': 'sensor.my_water_heater_energy_usage', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '132.825', }) @@ -28,6 +29,7 @@ 'context': , 'entity_id': 'sensor.my_water_heater_hot_water_availability', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'low', }) diff --git a/tests/components/aosmith/snapshots/test_water_heater.ambr b/tests/components/aosmith/snapshots/test_water_heater.ambr index a4be3d107f3..c3740341c17 100644 --- a/tests/components/aosmith/snapshots/test_water_heater.ambr +++ b/tests/components/aosmith/snapshots/test_water_heater.ambr @@ -21,6 +21,7 @@ 'context': , 'entity_id': 'water_heater.my_water_heater', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'heat_pump', }) @@ -41,6 +42,7 @@ 'context': , 'entity_id': 'water_heater.my_water_heater', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'electric', }) diff --git a/tests/components/aosmith/test_config_flow.py b/tests/components/aosmith/test_config_flow.py index d6cf1655b14..32f259f552c 100644 --- a/tests/components/aosmith/test_config_flow.py +++ b/tests/components/aosmith/test_config_flow.py @@ -1,4 +1,5 @@ """Test the A. O. Smith config flow.""" + from datetime import timedelta from unittest.mock import AsyncMock, MagicMock, patch @@ -123,13 +124,17 @@ async def test_reauth_flow( assert len(flows) == 1 assert flows[0]["step_id"] == "reauth_confirm" - with patch( - "homeassistant.components.aosmith.config_flow.AOSmithAPIClient.get_devices", - return_value=[], - ), patch( - "homeassistant.components.aosmith.config_flow.AOSmithAPIClient.get_energy_use_data", - return_value=[], - ), patch("homeassistant.components.aosmith.async_setup_entry", return_value=True): + with ( + patch( + "homeassistant.components.aosmith.config_flow.AOSmithAPIClient.get_devices", + return_value=[], + ), + patch( + "homeassistant.components.aosmith.config_flow.AOSmithAPIClient.get_energy_use_data", + return_value=[], + ), + patch("homeassistant.components.aosmith.async_setup_entry", return_value=True), + ): result2 = await hass.config_entries.flow.async_configure( flows[0]["flow_id"], {CONF_PASSWORD: FIXTURE_USER_INPUT[CONF_PASSWORD]}, @@ -177,10 +182,13 @@ async def test_reauth_flow_retry( assert result2["errors"] == {"base": "invalid_auth"} # Second attempt at reauth - authentication succeeds - with patch( - "homeassistant.components.aosmith.config_flow.AOSmithAPIClient.get_devices", - return_value=[], - ), patch("homeassistant.components.aosmith.async_setup_entry", return_value=True): + with ( + patch( + "homeassistant.components.aosmith.config_flow.AOSmithAPIClient.get_devices", + return_value=[], + ), + patch("homeassistant.components.aosmith.async_setup_entry", return_value=True), + ): result3 = await hass.config_entries.flow.async_configure( flows[0]["flow_id"], {CONF_PASSWORD: FIXTURE_USER_INPUT[CONF_PASSWORD]}, diff --git a/tests/components/aosmith/test_init.py b/tests/components/aosmith/test_init.py index 7e081686790..940b0cbc6b5 100644 --- a/tests/components/aosmith/test_init.py +++ b/tests/components/aosmith/test_init.py @@ -59,12 +59,15 @@ async def test_config_entry_not_ready_get_energy_use_data_error( ) ] - with patch( - "homeassistant.components.aosmith.config_flow.AOSmithAPIClient.get_devices", - return_value=get_devices_fixture, - ), patch( - "homeassistant.components.aosmith.config_flow.AOSmithAPIClient.get_energy_use_data", - side_effect=AOSmithUnknownException("Unknown error"), + with ( + patch( + "homeassistant.components.aosmith.config_flow.AOSmithAPIClient.get_devices", + return_value=get_devices_fixture, + ), + patch( + "homeassistant.components.aosmith.config_flow.AOSmithAPIClient.get_energy_use_data", + side_effect=AOSmithUnknownException("Unknown error"), + ), ): await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/apache_kafka/test_init.py b/tests/components/apache_kafka/test_init.py index 2f8b035cda9..93d9619f0c3 100644 --- a/tests/components/apache_kafka/test_init.py +++ b/tests/components/apache_kafka/test_init.py @@ -1,4 +1,5 @@ """The tests for the Apache Kafka component.""" + from __future__ import annotations from asyncio import AbstractEventLoop @@ -42,9 +43,11 @@ class MockKafkaClient: @pytest.fixture(name="mock_client") def mock_client_fixture(): """Mock the apache kafka client.""" - with patch(f"{PRODUCER_PATH}.start") as start, patch( - f"{PRODUCER_PATH}.send_and_wait" - ) as send_and_wait, patch(f"{PRODUCER_PATH}.__init__", return_value=None) as init: + with ( + patch(f"{PRODUCER_PATH}.start") as start, + patch(f"{PRODUCER_PATH}.send_and_wait") as send_and_wait, + patch(f"{PRODUCER_PATH}.__init__", return_value=None) as init, + ): yield MockKafkaClient(init, start, send_and_wait) diff --git a/tests/components/apcupsd/__init__.py b/tests/components/apcupsd/__init__.py index 4c4e0af8705..b75f3eab3af 100644 --- a/tests/components/apcupsd/__init__.py +++ b/tests/components/apcupsd/__init__.py @@ -1,4 +1,5 @@ """Tests for the APCUPSd component.""" + from collections import OrderedDict from typing import Final from unittest.mock import patch diff --git a/tests/components/apcupsd/snapshots/test_diagnostics.ambr b/tests/components/apcupsd/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..a3c4d16da2f --- /dev/null +++ b/tests/components/apcupsd/snapshots/test_diagnostics.ambr @@ -0,0 +1,46 @@ +# serializer version: 1 +# name: test_diagnostics + dict({ + 'ALARMDEL': '30 Seconds', + 'APC': '001,038,0985', + 'BATTDATE': '1970-01-01', + 'BATTV': '13.7 Volts', + 'BCHARGE': '100.0 Percent', + 'CABLE': 'USB Cable', + 'CUMONBATT': '8 Seconds', + 'DATE': '1970-01-01 00:00:00 0000', + 'DRIVER': 'USB UPS Driver', + 'END APC': '1970-01-01 00:00:00 0000', + 'FIRMWARE': '928.a8 .D USB FW:a8', + 'HITRANS': '139.0 Volts', + 'ITEMP': '34.6 C Internal', + 'LASTSTEST': '1970-01-01 00:00:00 0000', + 'LASTXFER': 'Automatic or explicit self test', + 'LINEV': '124.0 Volts', + 'LOADPCT': '14.0 Percent', + 'LOTRANS': '92.0 Volts', + 'MAXTIME': '0 Seconds', + 'MBATTCHG': '5 Percent', + 'MINTIMEL': '3 Minutes', + 'MODEL': 'Back-UPS ES 600', + 'NOMAPNT': '60.0 VA', + 'NOMBATTV': '12.0 Volts', + 'NOMINV': '120 Volts', + 'NOMPOWER': '330 Watts', + 'NUMXFERS': '1', + 'OUTCURNT': '0.88 Amps', + 'SELFTEST': 'NO', + 'SENSE': 'Medium', + 'SERIALNO': '**REDACTED**', + 'STATFLAG': '0x05000008', + 'STATUS': 'ONLINE', + 'STESTI': '7 days', + 'TIMELEFT': '51.0 Minutes', + 'TONBATT': '0 Seconds', + 'UPSMODE': 'Stand Alone', + 'UPSNAME': 'MyUPS', + 'VERSION': '3.14.14 (31 May 2016) unknown', + 'XOFFBATT': '1970-01-01 00:00:00 0000', + 'XONBATT': '1970-01-01 00:00:00 0000', + }) +# --- diff --git a/tests/components/apcupsd/test_binary_sensor.py b/tests/components/apcupsd/test_binary_sensor.py index 033b1ff6b82..7616a960b21 100644 --- a/tests/components/apcupsd/test_binary_sensor.py +++ b/tests/components/apcupsd/test_binary_sensor.py @@ -1,6 +1,8 @@ """Test binary sensors of APCUPSd integration.""" + from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er +from homeassistant.util import slugify from . import MOCK_STATUS, async_init_integration @@ -11,12 +13,13 @@ async def test_binary_sensor( """Test states of binary sensor.""" await async_init_integration(hass, status=MOCK_STATUS) - state = hass.states.get("binary_sensor.ups_online_status") + device_slug, serialno = slugify(MOCK_STATUS["UPSNAME"]), MOCK_STATUS["SERIALNO"] + state = hass.states.get(f"binary_sensor.{device_slug}_online_status") assert state assert state.state == "on" - entry = entity_registry.async_get("binary_sensor.ups_online_status") + entry = entity_registry.async_get(f"binary_sensor.{device_slug}_online_status") assert entry - assert entry.unique_id == "XXXXXXXXXXXX_statflag" + assert entry.unique_id == f"{serialno}_statflag" async def test_no_binary_sensor(hass: HomeAssistant) -> None: @@ -25,5 +28,6 @@ async def test_no_binary_sensor(hass: HomeAssistant) -> None: status.pop("STATFLAG") await async_init_integration(hass, status=status) - state = hass.states.get("binary_sensor.ups_online_status") + device_slug = slugify(MOCK_STATUS["UPSNAME"]) + state = hass.states.get(f"binary_sensor.{device_slug}_online_status") assert state is None diff --git a/tests/components/apcupsd/test_config_flow.py b/tests/components/apcupsd/test_config_flow.py index 6a69d4e974e..bed0e78ad55 100644 --- a/tests/components/apcupsd/test_config_flow.py +++ b/tests/components/apcupsd/test_config_flow.py @@ -1,4 +1,5 @@ """Test APCUPSd config flow setup process.""" + from copy import copy from unittest.mock import patch @@ -36,17 +37,6 @@ async def test_config_flow_cannot_connect(hass: HomeAssistant) -> None: assert result["errors"]["base"] == "cannot_connect" -async def test_config_flow_no_status(hass: HomeAssistant) -> None: - """Test config flow setup with successful connection but no status is reported.""" - with patch("aioapcaccess.request_status", return_value={}): # Returns no status. - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} - ) - result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == FlowResultType.ABORT - assert result["reason"] == "no_status" - - async def test_config_flow_duplicate(hass: HomeAssistant) -> None: """Test duplicate config flow setup.""" # First add an exiting config entry to hass. diff --git a/tests/components/apcupsd/test_diagnostics.py b/tests/components/apcupsd/test_diagnostics.py new file mode 100644 index 00000000000..5dfce28a989 --- /dev/null +++ b/tests/components/apcupsd/test_diagnostics.py @@ -0,0 +1,19 @@ +"""Test APCUPSd diagnostics reporting abilities.""" + +from syrupy.assertion import SnapshotAssertion + +from homeassistant.core import HomeAssistant + +from tests.components.apcupsd import async_init_integration +from tests.components.diagnostics import get_diagnostics_for_config_entry +from tests.typing import ClientSessionGenerator + + +async def test_diagnostics( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + snapshot: SnapshotAssertion, +) -> None: + """Test diagnostics.""" + entry = await async_init_integration(hass) + assert await get_diagnostics_for_config_entry(hass, hass_client, entry) == snapshot diff --git a/tests/components/apcupsd/test_init.py b/tests/components/apcupsd/test_init.py index c65efe25bb9..723ec164eae 100644 --- a/tests/components/apcupsd/test_init.py +++ b/tests/components/apcupsd/test_init.py @@ -1,4 +1,5 @@ """Test init of APCUPSd integration.""" + import asyncio from collections import OrderedDict from unittest.mock import patch @@ -11,30 +12,48 @@ from homeassistant.config_entries import SOURCE_USER, ConfigEntryState from homeassistant.const import STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr -from homeassistant.util import utcnow +from homeassistant.util import slugify, utcnow from . import CONF_DATA, MOCK_MINIMAL_STATUS, MOCK_STATUS, async_init_integration from tests.common import MockConfigEntry, async_fire_time_changed -@pytest.mark.parametrize("status", (MOCK_STATUS, MOCK_MINIMAL_STATUS)) +@pytest.mark.parametrize( + "status", + [ + # Contains "SERIALNO" and "UPSNAME" fields. + # We should create devices for the entities and prefix their IDs with "MyUPS". + MOCK_STATUS, + # Contains "SERIALNO" but no "UPSNAME" field. + # We should create devices for the entities and prefix their IDs with default "APC UPS". + MOCK_MINIMAL_STATUS | {"SERIALNO": "XXXX"}, + # Does not contain either "SERIALNO" field. + # We should _not_ create devices for the entities and their IDs will not have prefixes. + MOCK_MINIMAL_STATUS, + ], +) async def test_async_setup_entry(hass: HomeAssistant, status: OrderedDict) -> None: """Test a successful setup entry.""" # Minimal status does not contain "SERIALNO" field, which is used to determine the # unique ID of this integration. But, the integration should work fine without it. + # In such a case, the device will not be added either await async_init_integration(hass, status=status) + prefix = "" + if "SERIALNO" in status: + prefix = slugify(status.get("UPSNAME", "APC UPS")) + "_" + # Verify successful setup by querying the status sensor. - state = hass.states.get("binary_sensor.ups_online_status") - assert state is not None + state = hass.states.get(f"binary_sensor.{prefix}online_status") + assert state assert state.state != STATE_UNAVAILABLE assert state.state == "on" @pytest.mark.parametrize( "status", - ( + [ # We should not create device entries if SERIALNO is not reported. MOCK_MINIMAL_STATUS, # We should set the device name to be the friendly UPSNAME field if available. @@ -43,7 +62,7 @@ async def test_async_setup_entry(hass: HomeAssistant, status: OrderedDict) -> No MOCK_MINIMAL_STATUS | {"SERIALNO": "XXXX"}, # We should create all fields of the device entry if they are available. MOCK_STATUS, - ), + ], ) async def test_device_entry( hass: HomeAssistant, status: OrderedDict, device_registry: dr.DeviceRegistry @@ -92,15 +111,35 @@ async def test_multiple_integrations(hass: HomeAssistant) -> None: assert len(hass.config_entries.async_entries(DOMAIN)) == 2 assert all(entry.state is ConfigEntryState.LOADED for entry in entries) - state1 = hass.states.get("sensor.ups_load") - state2 = hass.states.get("sensor.ups_load_2") + # Since the two UPS device names are the same, we will have to add a "_2" suffix. + device_slug = slugify(MOCK_STATUS["UPSNAME"]) + state1 = hass.states.get(f"sensor.{device_slug}_load") + state2 = hass.states.get(f"sensor.{device_slug}_load_2") assert state1 is not None and state2 is not None assert state1.state != state2.state +async def test_multiple_integrations_different_devices(hass: HomeAssistant) -> None: + """Test successful setup for multiple entries with different device names.""" + status1 = MOCK_STATUS | {"SERIALNO": "XXXXX1", "UPSNAME": "MyUPS1"} + status2 = MOCK_STATUS | {"SERIALNO": "XXXXX2", "UPSNAME": "MyUPS2"} + entries = ( + await async_init_integration(hass, host="test1", status=status1), + await async_init_integration(hass, host="test2", status=status2), + ) + + assert len(hass.config_entries.async_entries(DOMAIN)) == 2 + assert all(entry.state is ConfigEntryState.LOADED for entry in entries) + + # The device names are different, so they are prefixed differently. + state1 = hass.states.get("sensor.myups1_load") + state2 = hass.states.get("sensor.myups2_load") + assert state1 is not None and state2 is not None + + @pytest.mark.parametrize( "error", - (OSError(), asyncio.IncompleteReadError(partial=b"", expected=0)), + [OSError(), asyncio.IncompleteReadError(partial=b"", expected=0)], ) async def test_connection_error(hass: HomeAssistant, error: Exception) -> None: """Test connection error during integration setup.""" @@ -153,7 +192,8 @@ async def test_availability(hass: HomeAssistant) -> None: """Ensure that we mark the entity's availability properly when network is down / back up.""" await async_init_integration(hass) - state = hass.states.get("sensor.ups_load") + device_slug = slugify(MOCK_STATUS["UPSNAME"]) + state = hass.states.get(f"sensor.{device_slug}_load") assert state assert state.state != STATE_UNAVAILABLE assert pytest.approx(float(state.state)) == 14.0 @@ -166,7 +206,7 @@ async def test_availability(hass: HomeAssistant) -> None: await hass.async_block_till_done() # Sensors should be marked as unavailable. - state = hass.states.get("sensor.ups_load") + state = hass.states.get(f"sensor.{device_slug}_load") assert state assert state.state == STATE_UNAVAILABLE @@ -178,7 +218,7 @@ async def test_availability(hass: HomeAssistant) -> None: await hass.async_block_till_done() # Sensors should be online now with the new value. - state = hass.states.get("sensor.ups_load") + state = hass.states.get(f"sensor.{device_slug}_load") assert state assert state.state != STATE_UNAVAILABLE assert pytest.approx(float(state.state)) == 15.0 diff --git a/tests/components/apcupsd/test_sensor.py b/tests/components/apcupsd/test_sensor.py index 24aae1d3937..0c7d174a5e8 100644 --- a/tests/components/apcupsd/test_sensor.py +++ b/tests/components/apcupsd/test_sensor.py @@ -22,6 +22,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component +from homeassistant.util import slugify from homeassistant.util.dt import utcnow from . import MOCK_STATUS, async_init_integration @@ -32,17 +33,18 @@ from tests.common import async_fire_time_changed async def test_sensor(hass: HomeAssistant, entity_registry: er.EntityRegistry) -> None: """Test states of sensor.""" await async_init_integration(hass, status=MOCK_STATUS) + device_slug, serialno = slugify(MOCK_STATUS["UPSNAME"]), MOCK_STATUS["SERIALNO"] # Test a representative string sensor. - state = hass.states.get("sensor.ups_mode") + state = hass.states.get(f"sensor.{device_slug}_mode") assert state assert state.state == "Stand Alone" - entry = entity_registry.async_get("sensor.ups_mode") + entry = entity_registry.async_get(f"sensor.{device_slug}_mode") assert entry - assert entry.unique_id == "XXXXXXXXXXXX_upsmode" + assert entry.unique_id == f"{serialno}_upsmode" # Test two representative voltage sensors. - state = hass.states.get("sensor.ups_input_voltage") + state = hass.states.get(f"sensor.{device_slug}_input_voltage") assert state assert state.state == "124.0" assert ( @@ -50,11 +52,11 @@ async def test_sensor(hass: HomeAssistant, entity_registry: er.EntityRegistry) - ) assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.VOLTAGE - entry = entity_registry.async_get("sensor.ups_input_voltage") + entry = entity_registry.async_get(f"sensor.{device_slug}_input_voltage") assert entry - assert entry.unique_id == "XXXXXXXXXXXX_linev" + assert entry.unique_id == f"{serialno}_linev" - state = hass.states.get("sensor.ups_battery_voltage") + state = hass.states.get(f"sensor.{device_slug}_battery_voltage") assert state assert state.state == "13.7" assert ( @@ -62,38 +64,59 @@ async def test_sensor(hass: HomeAssistant, entity_registry: er.EntityRegistry) - ) assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.VOLTAGE - entry = entity_registry.async_get("sensor.ups_battery_voltage") + entry = entity_registry.async_get(f"sensor.{device_slug}_battery_voltage") assert entry - assert entry.unique_id == "XXXXXXXXXXXX_battv" + assert entry.unique_id == f"{serialno}_battv" - # test a representative time sensor. - state = hass.states.get("sensor.ups_self_test_interval") + # Test a representative time sensor. + state = hass.states.get(f"sensor.{device_slug}_self_test_interval") assert state assert state.state == "7" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTime.DAYS - entry = entity_registry.async_get("sensor.ups_self_test_interval") + entry = entity_registry.async_get(f"sensor.{device_slug}_self_test_interval") assert entry - assert entry.unique_id == "XXXXXXXXXXXX_stesti" + assert entry.unique_id == f"{serialno}_stesti" # Test a representative percentage sensor. - state = hass.states.get("sensor.ups_load") + state = hass.states.get(f"sensor.{device_slug}_load") assert state assert state.state == "14.0" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - entry = entity_registry.async_get("sensor.ups_load") + entry = entity_registry.async_get(f"sensor.{device_slug}_load") assert entry - assert entry.unique_id == "XXXXXXXXXXXX_loadpct" + assert entry.unique_id == f"{serialno}_loadpct" # Test a representative wattage sensor. - state = hass.states.get("sensor.ups_nominal_output_power") + state = hass.states.get(f"sensor.{device_slug}_nominal_output_power") assert state assert state.state == "330" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPower.WATT assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER - entry = entity_registry.async_get("sensor.ups_nominal_output_power") + entry = entity_registry.async_get(f"sensor.{device_slug}_nominal_output_power") assert entry - assert entry.unique_id == "XXXXXXXXXXXX_nompower" + assert entry.unique_id == f"{serialno}_nompower" + + +async def test_sensor_name(hass: HomeAssistant) -> None: + """Test if sensor name follows the recommended entity naming scheme. + + See https://developers.home-assistant.io/docs/core/entity/#entity-naming for more details. + """ + await async_init_integration(hass, status=MOCK_STATUS) + + all_states = hass.states.async_all() + assert len(all_states) != 0 + + device_name = MOCK_STATUS["UPSNAME"] + for state in all_states: + # Friendly name must start with the device name. + friendly_name = state.name + assert friendly_name.startswith(device_name) + + # Entity names should start with a capital letter, the rest of the words are lower case. + entity_name = friendly_name.removeprefix(device_name).strip() + assert entity_name == entity_name.capitalize() async def test_sensor_disabled( @@ -102,15 +125,16 @@ async def test_sensor_disabled( """Test sensor disabled by default.""" await async_init_integration(hass) + device_slug, serialno = slugify(MOCK_STATUS["UPSNAME"]), MOCK_STATUS["SERIALNO"] # Test a representative integration-disabled sensor. - entry = entity_registry.async_get("sensor.ups_model") + entry = entity_registry.async_get(f"sensor.{device_slug}_model") assert entry.disabled - assert entry.unique_id == "XXXXXXXXXXXX_model" + assert entry.unique_id == f"{serialno}_model" assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION # Test enabling entity. updated_entry = entity_registry.async_update_entity( - entry.entity_id, **{"disabled_by": None} + entry.entity_id, disabled_by=None ) assert updated_entry != entry @@ -121,7 +145,8 @@ async def test_state_update(hass: HomeAssistant) -> None: """Ensure the sensor state changes after updating the data.""" await async_init_integration(hass) - state = hass.states.get("sensor.ups_load") + device_slug = slugify(MOCK_STATUS["UPSNAME"]) + state = hass.states.get(f"sensor.{device_slug}_load") assert state assert state.state != STATE_UNAVAILABLE assert state.state == "14.0" @@ -132,7 +157,7 @@ async def test_state_update(hass: HomeAssistant) -> None: async_fire_time_changed(hass, future) await hass.async_block_till_done() - state = hass.states.get("sensor.ups_load") + state = hass.states.get(f"sensor.{device_slug}_load") assert state assert state.state != STATE_UNAVAILABLE assert state.state == "15.0" @@ -142,8 +167,9 @@ async def test_manual_update_entity(hass: HomeAssistant) -> None: """Test manual update entity via service homeassistant/update_entity.""" await async_init_integration(hass) + device_slug = slugify(MOCK_STATUS["UPSNAME"]) # Assert the initial state of sensor.ups_load. - state = hass.states.get("sensor.ups_load") + state = hass.states.get(f"sensor.{device_slug}_load") assert state assert state.state != STATE_UNAVAILABLE assert state.state == "14.0" @@ -163,7 +189,12 @@ async def test_manual_update_entity(hass: HomeAssistant) -> None: await hass.services.async_call( "homeassistant", "update_entity", - {ATTR_ENTITY_ID: ["sensor.ups_load", "sensor.ups_battery"]}, + { + ATTR_ENTITY_ID: [ + f"sensor.{device_slug}_load", + f"sensor.{device_slug}_battery", + ] + }, blocking=True, ) # Even if we requested updates for two entities, our integration should smartly @@ -171,7 +202,7 @@ async def test_manual_update_entity(hass: HomeAssistant) -> None: assert mock_request_status.call_count == 1 # The new state should be effective. - state = hass.states.get("sensor.ups_load") + state = hass.states.get(f"sensor.{device_slug}_load") assert state assert state.state != STATE_UNAVAILABLE assert state.state == "15.0" @@ -184,6 +215,7 @@ async def test_multiple_manual_update_entity(hass: HomeAssistant) -> None: """ await async_init_integration(hass) + device_slug = slugify(MOCK_STATUS["UPSNAME"]) # Setup HASS for calling the update_entity service. await async_setup_component(hass, "homeassistant", {}) @@ -196,7 +228,12 @@ async def test_multiple_manual_update_entity(hass: HomeAssistant) -> None: await hass.services.async_call( "homeassistant", "update_entity", - {ATTR_ENTITY_ID: ["sensor.ups_load", "sensor.ups_input_voltage"]}, + { + ATTR_ENTITY_ID: [ + f"sensor.{device_slug}_load", + f"sensor.{device_slug}_input_voltage", + ] + }, blocking=True, ) assert mock_request_status.call_count == 1 diff --git a/tests/components/api/test_init.py b/tests/components/api/test_init.py index 49da0078229..0ac2e5973fe 100644 --- a/tests/components/api/test_init.py +++ b/tests/components/api/test_init.py @@ -1,4 +1,5 @@ """The tests for the Home Assistant API component.""" + import asyncio from http import HTTPStatus import json diff --git a/tests/components/apple_tv/__init__.py b/tests/components/apple_tv/__init__.py index 118c3d6f735..514d77bde4d 100644 --- a/tests/components/apple_tv/__init__.py +++ b/tests/components/apple_tv/__init__.py @@ -1,4 +1,5 @@ """Tests for Apple TV.""" + import pytest # Make asserts in the common module display differences diff --git a/tests/components/apple_tv/test_config_flow.py b/tests/components/apple_tv/test_config_flow.py index 9b9020c3cf1..28d87ef1b03 100644 --- a/tests/components/apple_tv/test_config_flow.py +++ b/tests/components/apple_tv/test_config_flow.py @@ -1,4 +1,5 @@ """Test config flow.""" + from ipaddress import IPv4Address, ip_address from unittest.mock import ANY, Mock, patch diff --git a/tests/components/apple_tv/test_remote.py b/tests/components/apple_tv/test_remote.py index db2a4964f6c..f831518d75a 100644 --- a/tests/components/apple_tv/test_remote.py +++ b/tests/components/apple_tv/test_remote.py @@ -1,4 +1,5 @@ """Test apple_tv remote.""" + from unittest.mock import AsyncMock import pytest diff --git a/tests/components/application_credentials/test_init.py b/tests/components/application_credentials/test_init.py index 807eff4ef8d..2d44aec4461 100644 --- a/tests/components/application_credentials/test_init.py +++ b/tests/components/application_credentials/test_init.py @@ -1,4 +1,5 @@ """Test the Developer Credentials integration.""" + from __future__ import annotations from collections.abc import Callable, Generator diff --git a/tests/components/apprise/test_notify.py b/tests/components/apprise/test_notify.py index ead8f735236..7d37d7a5d99 100644 --- a/tests/components/apprise/test_notify.py +++ b/tests/components/apprise/test_notify.py @@ -1,4 +1,5 @@ """The tests for the apprise notification platform.""" + from pathlib import Path from unittest.mock import MagicMock, patch @@ -33,12 +34,15 @@ async def test_apprise_config_load_fail02(hass: HomeAssistant) -> None: BASE_COMPONENT: {"name": "test", "platform": "apprise", "config": "/path/"} } - with patch( - "homeassistant.components.apprise.notify.apprise.Apprise.add", - return_value=False, - ), patch( - "homeassistant.components.apprise.notify.apprise.AppriseConfig.add", - return_value=True, + with ( + patch( + "homeassistant.components.apprise.notify.apprise.Apprise.add", + return_value=False, + ), + patch( + "homeassistant.components.apprise.notify.apprise.AppriseConfig.add", + return_value=True, + ), ): assert await async_setup_component(hass, BASE_COMPONENT, config) await hass.async_block_till_done() @@ -120,7 +124,7 @@ async def test_apprise_notification(hass: HomeAssistant) -> None: # Validate calls were made under the hood correctly obj.add.assert_called_once_with(config[BASE_COMPONENT]["url"]) obj.notify.assert_called_once_with( - **{"body": data["message"], "title": data["title"], "tag": None} + body=data["message"], title=data["title"], tag=None ) @@ -161,7 +165,7 @@ async def test_apprise_multiple_notification(hass: HomeAssistant) -> None: # Validate 2 calls were made under the hood assert obj.add.call_count == 2 obj.notify.assert_called_once_with( - **{"body": data["message"], "title": data["title"], "tag": None} + body=data["message"], title=data["title"], tag=None ) @@ -203,5 +207,5 @@ async def test_apprise_notification_with_target( # Validate calls were made under the hood correctly apprise_obj.notify.assert_called_once_with( - **{"body": data["message"], "title": data["title"], "tag": data["target"]} + body=data["message"], title=data["title"], tag=data["target"] ) diff --git a/tests/components/aprs/test_device_tracker.py b/tests/components/aprs/test_device_tracker.py index 858a0f041de..ca2a5ce1833 100644 --- a/tests/components/aprs/test_device_tracker.py +++ b/tests/components/aprs/test_device_tracker.py @@ -1,4 +1,5 @@ """Test APRS device tracker.""" + from collections.abc import Generator from unittest.mock import MagicMock, Mock, patch diff --git a/tests/components/aranet/test_config_flow.py b/tests/components/aranet/test_config_flow.py index 4c3575f64b5..f3558c66daf 100644 --- a/tests/components/aranet/test_config_flow.py +++ b/tests/components/aranet/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Aranet config flow.""" + from unittest.mock import patch from homeassistant import config_entries diff --git a/tests/components/aranet/test_sensor.py b/tests/components/aranet/test_sensor.py index 0b2b4771069..20aea65989d 100644 --- a/tests/components/aranet/test_sensor.py +++ b/tests/components/aranet/test_sensor.py @@ -1,4 +1,5 @@ """Test the Aranet sensors.""" + from homeassistant.components.aranet.const import DOMAIN from homeassistant.components.sensor import ATTR_STATE_CLASS from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT diff --git a/tests/components/arcam_fmj/conftest.py b/tests/components/arcam_fmj/conftest.py index ba32951efe4..f5a9ab6315a 100644 --- a/tests/components/arcam_fmj/conftest.py +++ b/tests/components/arcam_fmj/conftest.py @@ -1,4 +1,5 @@ """Tests for the arcam_fmj component.""" + from unittest.mock import Mock, patch from arcam.fmj.client import Client @@ -97,9 +98,14 @@ async def player_setup_fixture(hass, state_1, state_2, client): await async_setup_component(hass, "homeassistant", {}) - with patch("homeassistant.components.arcam_fmj.Client", return_value=client), patch( - "homeassistant.components.arcam_fmj.media_player.State", side_effect=state_mock - ), patch("homeassistant.components.arcam_fmj._run_client", return_value=None): + with ( + patch("homeassistant.components.arcam_fmj.Client", return_value=client), + patch( + "homeassistant.components.arcam_fmj.media_player.State", + side_effect=state_mock, + ), + patch("homeassistant.components.arcam_fmj._run_client", return_value=None), + ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() yield MOCK_ENTITY_ID diff --git a/tests/components/arcam_fmj/test_config_flow.py b/tests/components/arcam_fmj/test_config_flow.py index 38053797215..470a91feb3b 100644 --- a/tests/components/arcam_fmj/test_config_flow.py +++ b/tests/components/arcam_fmj/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for the Arcam FMJ config flow module.""" + from dataclasses import replace from unittest.mock import AsyncMock, patch diff --git a/tests/components/arcam_fmj/test_device_trigger.py b/tests/components/arcam_fmj/test_device_trigger.py index d073e9c75da..a8510628b26 100644 --- a/tests/components/arcam_fmj/test_device_trigger.py +++ b/tests/components/arcam_fmj/test_device_trigger.py @@ -1,4 +1,5 @@ """The tests for Arcam FMJ Receiver control device triggers.""" + import pytest from homeassistant.components.arcam_fmj.const import DOMAIN diff --git a/tests/components/arcam_fmj/test_media_player.py b/tests/components/arcam_fmj/test_media_player.py index 9287e8dbc18..0baa8ba6870 100644 --- a/tests/components/arcam_fmj/test_media_player.py +++ b/tests/components/arcam_fmj/test_media_player.py @@ -1,4 +1,5 @@ """Tests for arcam fmj receivers.""" + from math import isclose from unittest.mock import ANY, PropertyMock, patch diff --git a/tests/components/aseko_pool_live/test_config_flow.py b/tests/components/aseko_pool_live/test_config_flow.py index 9d7a5b83d78..2566dfd61e6 100644 --- a/tests/components/aseko_pool_live/test_config_flow.py +++ b/tests/components/aseko_pool_live/test_config_flow.py @@ -1,37 +1,44 @@ """Test the Aseko Pool Live config flow.""" -from unittest.mock import AsyncMock, patch + +from unittest.mock import patch from aioaseko import AccountInfo, APIUnavailable, InvalidAuthCredentials import pytest -from homeassistant import config_entries, setup +from homeassistant import config_entries from homeassistant.components.aseko_pool_live.const import DOMAIN -from homeassistant.const import CONF_ACCESS_TOKEN, CONF_EMAIL, CONF_PASSWORD +from homeassistant.const import CONF_EMAIL, CONF_PASSWORD from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from tests.common import MockConfigEntry -async def test_form(hass: HomeAssistant) -> None: + +async def test_async_step_user_form(hass: HomeAssistant) -> None: """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == FlowResultType.FORM assert result["errors"] == {} - with patch( - "homeassistant.components.aseko_pool_live.config_flow.WebAccount.login", - return_value=AccountInfo("aseko@example.com", "a_user_id", "any_language"), - ), patch( - "homeassistant.components.aseko_pool_live.config_flow.MobileAccount", - ) as mock_mobile_account, patch( - "homeassistant.components.aseko_pool_live.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - mobile_account = mock_mobile_account.return_value - mobile_account.login = AsyncMock() - mobile_account.access_token = "any_access_token" + +async def test_async_step_user_success(hass: HomeAssistant) -> 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.aseko_pool_live.config_flow.WebAccount.login", + return_value=AccountInfo("aseko@example.com", "a_user_id", "any_language"), + ), + patch( + "homeassistant.components.aseko_pool_live.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -43,23 +50,56 @@ async def test_form(hass: HomeAssistant) -> None: assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "aseko@example.com" - assert result2["data"] == {CONF_ACCESS_TOKEN: "any_access_token"} + assert result2["data"] == { + CONF_EMAIL: "aseko@example.com", + CONF_PASSWORD: "passw0rd", + } assert len(mock_setup_entry.mock_calls) == 1 @pytest.mark.parametrize( - ("error_web", "error_mobile", "reason"), + ("error_web", "reason"), [ - (APIUnavailable, None, "cannot_connect"), - (InvalidAuthCredentials, None, "invalid_auth"), - (Exception, None, "unknown"), - (None, APIUnavailable, "cannot_connect"), - (None, InvalidAuthCredentials, "invalid_auth"), - (None, Exception, "unknown"), + (APIUnavailable, "cannot_connect"), + (InvalidAuthCredentials, "invalid_auth"), + (Exception, "unknown"), + ], +) +async def test_async_step_user_exception( + hass: HomeAssistant, error_web: Exception, reason: str +) -> 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.aseko_pool_live.config_flow.WebAccount.login", + return_value=AccountInfo("aseko@example.com", "a_user_id", "any_language"), + side_effect=error_web, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_EMAIL: "aseko@example.com", + CONF_PASSWORD: "passw0rd", + }, + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {"base": reason} + + +@pytest.mark.parametrize( + ("error_web", "reason"), + [ + (APIUnavailable, "cannot_connect"), + (InvalidAuthCredentials, "invalid_auth"), + (Exception, "unknown"), ], ) async def test_get_account_info_exceptions( - hass: HomeAssistant, error_web: Exception, error_mobile: Exception, reason: str + hass: HomeAssistant, error_web: Exception, reason: str ) -> None: """Test we handle config flow exceptions.""" result = await hass.config_entries.flow.async_init( @@ -70,9 +110,6 @@ async def test_get_account_info_exceptions( "homeassistant.components.aseko_pool_live.config_flow.WebAccount.login", return_value=AccountInfo("aseko@example.com", "a_user_id", "any_language"), side_effect=error_web, - ), patch( - "homeassistant.components.aseko_pool_live.config_flow.MobileAccount.login", - side_effect=error_mobile, ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -84,3 +121,84 @@ async def test_get_account_info_exceptions( assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": reason} + + +async def test_async_step_reauth_success(hass: HomeAssistant) -> None: + """Test successful reauthentication.""" + + mock_entry = MockConfigEntry( + domain=DOMAIN, + unique_id="UID", + data={CONF_EMAIL: "aseko@example.com"}, + ) + mock_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": mock_entry.entry_id, + }, + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + assert result["errors"] == {} + + with patch( + "homeassistant.components.aseko_pool_live.config_flow.WebAccount.login", + return_value=AccountInfo("aseko@example.com", "a_user_id", "any_language"), + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_EMAIL: "aseko@example.com", CONF_PASSWORD: "passw0rd"}, + ) + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.parametrize( + ("error_web", "reason"), + [ + (APIUnavailable, "cannot_connect"), + (InvalidAuthCredentials, "invalid_auth"), + (Exception, "unknown"), + ], +) +async def test_async_step_reauth_exception( + hass: HomeAssistant, error_web: Exception, reason: str +) -> None: + """Test we get the form.""" + + mock_entry = MockConfigEntry( + domain=DOMAIN, + unique_id="UID", + data={CONF_EMAIL: "aseko@example.com"}, + ) + mock_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": mock_entry.entry_id, + }, + ) + + with patch( + "homeassistant.components.aseko_pool_live.config_flow.WebAccount.login", + return_value=AccountInfo("aseko@example.com", "a_user_id", "any_language"), + side_effect=error_web, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_EMAIL: "aseko@example.com", + CONF_PASSWORD: "passw0rd", + }, + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {"base": reason} diff --git a/tests/components/assist_pipeline/__init__.py b/tests/components/assist_pipeline/__init__.py index 40aa48fbc54..7400fe32d70 100644 --- a/tests/components/assist_pipeline/__init__.py +++ b/tests/components/assist_pipeline/__init__.py @@ -1,4 +1,5 @@ """Tests for the Voice Assistant integration.""" + MANY_LANGUAGES = [ "ar", "bg", diff --git a/tests/components/assist_pipeline/conftest.py b/tests/components/assist_pipeline/conftest.py index 0c9d83200b4..9f098150288 100644 --- a/tests/components/assist_pipeline/conftest.py +++ b/tests/components/assist_pipeline/conftest.py @@ -1,4 +1,5 @@ """Test fixtures for voice assistant.""" + from __future__ import annotations from collections.abc import AsyncIterable, Generator @@ -110,6 +111,7 @@ class MockTTSProvider(tts.Provider): tts.Voice("fran_drescher", "Fran Drescher"), ] } + _supported_options = ["voice", "age", tts.ATTR_AUDIO_OUTPUT] @property def default_language(self) -> str: @@ -129,7 +131,7 @@ class MockTTSProvider(tts.Provider): @property def supported_options(self) -> list[str]: """Return list of supported options like voice, emotions.""" - return ["voice", "age", tts.ATTR_AUDIO_OUTPUT] + return self._supported_options def get_tts_audio( self, message: str, language: str, options: dict[str, Any] diff --git a/tests/components/assist_pipeline/test_init.py b/tests/components/assist_pipeline/test_init.py index 882d3a80fb3..c6f45044cb3 100644 --- a/tests/components/assist_pipeline/test_init.py +++ b/tests/components/assist_pipeline/test_init.py @@ -1,4 +1,5 @@ """Test Voice Assistant init.""" + import asyncio from dataclasses import asdict import itertools as it @@ -10,7 +11,7 @@ import wave import pytest from syrupy.assertion import SnapshotAssertion -from homeassistant.components import assist_pipeline, stt, tts +from homeassistant.components import assist_pipeline, media_source, stt, tts from homeassistant.components.assist_pipeline.const import ( CONF_DEBUG_RECORDING_DIR, DOMAIN, @@ -18,9 +19,14 @@ from homeassistant.components.assist_pipeline.const import ( from homeassistant.core import Context, HomeAssistant from homeassistant.setup import async_setup_component -from .conftest import MockSttProvider, MockSttProviderEntity, MockWakeWordEntity +from .conftest import ( + MockSttProvider, + MockSttProviderEntity, + MockTTSProvider, + MockWakeWordEntity, +) -from tests.typing import WebSocketGenerator +from tests.typing import ClientSessionGenerator, WebSocketGenerator BYTES_ONE_SECOND = 16000 * 2 @@ -728,15 +734,17 @@ def test_pipeline_run_equality(hass: HomeAssistant, init_components) -> None: async def test_tts_audio_output( hass: HomeAssistant, - mock_stt_provider: MockSttProvider, + hass_client: ClientSessionGenerator, + mock_tts_provider: MockTTSProvider, init_components, pipeline_data: assist_pipeline.pipeline.PipelineData, snapshot: SnapshotAssertion, ) -> None: """Test using tts_audio_output with wav sets options correctly.""" + client = await hass_client() + assert await async_setup_component(hass, media_source.DOMAIN, {}) - def event_callback(event): - pass + events: list[assist_pipeline.PipelineEvent] = [] pipeline_store = pipeline_data.pipeline_store pipeline_id = pipeline_store.async_get_preferred_item() @@ -752,7 +760,7 @@ async def test_tts_audio_output( pipeline=pipeline, start_stage=assist_pipeline.PipelineStage.TTS, end_stage=assist_pipeline.PipelineStage.TTS, - event_callback=event_callback, + event_callback=events.append, tts_audio_output="wav", ), ) @@ -763,3 +771,87 @@ async def test_tts_audio_output( assert pipeline_input.run.tts_options.get(tts.ATTR_PREFERRED_FORMAT) == "wav" assert pipeline_input.run.tts_options.get(tts.ATTR_PREFERRED_SAMPLE_RATE) == 16000 assert pipeline_input.run.tts_options.get(tts.ATTR_PREFERRED_SAMPLE_CHANNELS) == 1 + + with patch.object(mock_tts_provider, "get_tts_audio") as mock_get_tts_audio: + await pipeline_input.execute() + + for event in events: + if event.type == assist_pipeline.PipelineEventType.TTS_END: + # We must fetch the media URL to trigger the TTS + assert event.data + media_id = event.data["tts_output"]["media_id"] + resolved = await media_source.async_resolve_media(hass, media_id, None) + await client.get(resolved.url) + + # Ensure that no unsupported options were passed in + assert mock_get_tts_audio.called + options = mock_get_tts_audio.call_args_list[0].kwargs["options"] + extra_options = set(options).difference(mock_tts_provider.supported_options) + assert len(extra_options) == 0, extra_options + + +async def test_tts_supports_preferred_format( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + mock_tts_provider: MockTTSProvider, + init_components, + pipeline_data: assist_pipeline.pipeline.PipelineData, + snapshot: SnapshotAssertion, +) -> None: + """Test that preferred format options are given to the TTS system if supported.""" + client = await hass_client() + assert await async_setup_component(hass, media_source.DOMAIN, {}) + + events: list[assist_pipeline.PipelineEvent] = [] + + pipeline_store = pipeline_data.pipeline_store + pipeline_id = pipeline_store.async_get_preferred_item() + pipeline = assist_pipeline.pipeline.async_get_pipeline(hass, pipeline_id) + + pipeline_input = assist_pipeline.pipeline.PipelineInput( + tts_input="This is a test.", + conversation_id=None, + device_id=None, + run=assist_pipeline.pipeline.PipelineRun( + hass, + context=Context(), + pipeline=pipeline, + start_stage=assist_pipeline.PipelineStage.TTS, + end_stage=assist_pipeline.PipelineStage.TTS, + event_callback=events.append, + tts_audio_output="wav", + ), + ) + await pipeline_input.validate() + + # Make the TTS provider support preferred format options + supported_options = list(mock_tts_provider.supported_options or []) + supported_options.extend( + [ + tts.ATTR_PREFERRED_FORMAT, + tts.ATTR_PREFERRED_SAMPLE_RATE, + tts.ATTR_PREFERRED_SAMPLE_CHANNELS, + ] + ) + + with ( + patch.object(mock_tts_provider, "_supported_options", supported_options), + patch.object(mock_tts_provider, "get_tts_audio") as mock_get_tts_audio, + ): + await pipeline_input.execute() + + for event in events: + if event.type == assist_pipeline.PipelineEventType.TTS_END: + # We must fetch the media URL to trigger the TTS + assert event.data + media_id = event.data["tts_output"]["media_id"] + resolved = await media_source.async_resolve_media(hass, media_id, None) + await client.get(resolved.url) + + assert mock_get_tts_audio.called + options = mock_get_tts_audio.call_args_list[0].kwargs["options"] + + # We should have received preferred format options in get_tts_audio + assert tts.ATTR_PREFERRED_FORMAT in options + assert tts.ATTR_PREFERRED_SAMPLE_RATE in options + assert tts.ATTR_PREFERRED_SAMPLE_CHANNELS in options diff --git a/tests/components/assist_pipeline/test_logbook.py b/tests/components/assist_pipeline/test_logbook.py index c1e0633ed57..d8c79c40cb6 100644 --- a/tests/components/assist_pipeline/test_logbook.py +++ b/tests/components/assist_pipeline/test_logbook.py @@ -1,4 +1,5 @@ """The tests for assist_pipeline logbook.""" + from homeassistant.components import assist_pipeline, logbook from homeassistant.const import ATTR_DEVICE_ID from homeassistant.core import HomeAssistant @@ -15,6 +16,7 @@ async def test_recording_event( """Test recording event.""" hass.config.components.add("recorder") assert await async_setup_component(hass, "logbook", {}) + await hass.async_block_till_done() entry = MockConfigEntry() entry.add_to_hass(hass) diff --git a/tests/components/assist_pipeline/test_pipeline.py b/tests/components/assist_pipeline/test_pipeline.py index 35913df7400..3bfe6605839 100644 --- a/tests/components/assist_pipeline/test_pipeline.py +++ b/tests/components/assist_pipeline/test_pipeline.py @@ -1,4 +1,5 @@ """Websocket tests for Voice Assistant integration.""" + from collections.abc import AsyncGenerator from typing import Any from unittest.mock import ANY, patch @@ -85,12 +86,12 @@ async def test_load_pipelines(hass: HomeAssistant, init_components) -> None: "wake_word_id": "wakeword_id_3", }, ] - pipeline_ids = [] pipeline_data: PipelineData = hass.data[DOMAIN] store1 = pipeline_data.pipeline_store - for pipeline in pipelines: - pipeline_ids.append((await store1.async_create_item(pipeline)).id) + pipeline_ids = [ + (await store1.async_create_item(pipeline)).id for pipeline in pipelines + ] assert len(store1.data) == 4 # 3 manually created plus a default pipeline assert store1.async_get_preferred_item() == list(store1.data)[0] @@ -400,9 +401,10 @@ async def test_default_pipeline( hass.config.country = ha_country hass.config.language = ha_language - with patch.object( - mock_stt_provider, "_supported_languages", MANY_LANGUAGES - ), patch.object(mock_tts_provider, "_supported_languages", MANY_LANGUAGES): + with ( + patch.object(mock_stt_provider, "_supported_languages", MANY_LANGUAGES), + patch.object(mock_tts_provider, "_supported_languages", MANY_LANGUAGES), + ): assert await async_setup_component(hass, "assist_pipeline", {}) pipeline_data: PipelineData = hass.data[DOMAIN] diff --git a/tests/components/assist_pipeline/test_ring_buffer.py b/tests/components/assist_pipeline/test_ring_buffer.py index 22185c3ad5b..7531bcdad80 100644 --- a/tests/components/assist_pipeline/test_ring_buffer.py +++ b/tests/components/assist_pipeline/test_ring_buffer.py @@ -1,4 +1,5 @@ """Tests for audio ring buffer.""" + from homeassistant.components.assist_pipeline.ring_buffer import RingBuffer diff --git a/tests/components/assist_pipeline/test_vad.py b/tests/components/assist_pipeline/test_vad.py index 57b567c49df..139ae915263 100644 --- a/tests/components/assist_pipeline/test_vad.py +++ b/tests/components/assist_pipeline/test_vad.py @@ -1,4 +1,5 @@ """Tests for voice command segmenter.""" + import itertools as it from unittest.mock import patch diff --git a/tests/components/assist_pipeline/test_websocket.py b/tests/components/assist_pipeline/test_websocket.py index 9138819de12..0883046f3a1 100644 --- a/tests/components/assist_pipeline/test_websocket.py +++ b/tests/components/assist_pipeline/test_websocket.py @@ -1,4 +1,5 @@ """Websocket tests for Voice Assistant integration.""" + import asyncio import base64 from typing import Any @@ -380,12 +381,15 @@ async def test_audio_pipeline_no_wake_word_entity( """Test timeout from a pipeline run with audio input/output + wake word.""" client = await hass_ws_client(hass) - with patch( - "homeassistant.components.wake_word.async_default_entity", - return_value="wake_word.bad-entity-id", - ), patch( - "homeassistant.components.wake_word.async_get_wake_word_detection_entity", - return_value=None, + with ( + patch( + "homeassistant.components.wake_word.async_default_entity", + return_value="wake_word.bad-entity-id", + ), + patch( + "homeassistant.components.wake_word.async_get_wake_word_detection_entity", + return_value=None, + ), ): await client.send_json_auto_id( { @@ -1699,15 +1703,19 @@ async def test_list_pipeline_languages_with_aliases( """Test listing pipeline languages using aliases.""" client = await hass_ws_client(hass) - with patch( - "homeassistant.components.conversation.async_get_conversation_languages", - return_value={"he", "nb"}, - ), patch( - "homeassistant.components.stt.async_get_speech_to_text_languages", - return_value={"he", "no"}, - ), patch( - "homeassistant.components.tts.async_get_text_to_speech_languages", - return_value={"iw", "nb"}, + with ( + patch( + "homeassistant.components.conversation.async_get_conversation_languages", + return_value={"he", "nb"}, + ), + patch( + "homeassistant.components.stt.async_get_speech_to_text_languages", + return_value={"he", "no"}, + ), + patch( + "homeassistant.components.tts.async_get_text_to_speech_languages", + return_value={"iw", "nb"}, + ), ): await client.send_json_auto_id({"type": "assist_pipeline/language/list"}) @@ -2391,7 +2399,7 @@ async def test_device_capture_queue_full( def put_nowait(self, item): if item is not None: - raise asyncio.QueueFull() + raise asyncio.QueueFull super().put_nowait(item) diff --git a/tests/components/asterisk_mbox/test_init.py b/tests/components/asterisk_mbox/test_init.py index 17a8fe67f36..9c6bbf01f0e 100644 --- a/tests/components/asterisk_mbox/test_init.py +++ b/tests/components/asterisk_mbox/test_init.py @@ -1,4 +1,5 @@ """Test mailbox.""" + from collections.abc import Generator from unittest.mock import Mock, patch diff --git a/tests/components/asuswrt/test_config_flow.py b/tests/components/asuswrt/test_config_flow.py index 0b5b0ace720..08ab2ae6c98 100644 --- a/tests/components/asuswrt/test_config_flow.py +++ b/tests/components/asuswrt/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for the AsusWrt config flow.""" + from socket import gaierror from unittest.mock import patch diff --git a/tests/components/asuswrt/test_sensor.py b/tests/components/asuswrt/test_sensor.py index 0ee90b111f5..3de830f3f34 100644 --- a/tests/components/asuswrt/test_sensor.py +++ b/tests/components/asuswrt/test_sensor.py @@ -1,4 +1,5 @@ """Tests for the AsusWrt sensor.""" + from datetime import timedelta from pyasuswrt.exceptions import AsusWrtError, AsusWrtNotAvailableInfoError diff --git a/tests/components/atag/conftest.py b/tests/components/atag/conftest.py index a1270696540..567b835d8b4 100644 --- a/tests/components/atag/conftest.py +++ b/tests/components/atag/conftest.py @@ -1,4 +1,5 @@ """Provide common Atag fixtures.""" + import asyncio from collections.abc import Generator from unittest.mock import AsyncMock, patch diff --git a/tests/components/atag/test_climate.py b/tests/components/atag/test_climate.py index da5eefa589b..bc78ee58216 100644 --- a/tests/components/atag/test_climate.py +++ b/tests/components/atag/test_climate.py @@ -1,4 +1,5 @@ """Tests for the Atag climate platform.""" + from unittest.mock import PropertyMock, patch from homeassistant.components.atag.climate import DOMAIN, PRESET_MAP diff --git a/tests/components/atag/test_config_flow.py b/tests/components/atag/test_config_flow.py index 69e2327c616..138790e77e8 100644 --- a/tests/components/atag/test_config_flow.py +++ b/tests/components/atag/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for the Atag config flow.""" + from unittest.mock import PropertyMock, patch import pytest diff --git a/tests/components/atag/test_sensors.py b/tests/components/atag/test_sensors.py index 358fe27804a..df3f8101c58 100644 --- a/tests/components/atag/test_sensors.py +++ b/tests/components/atag/test_sensors.py @@ -1,4 +1,5 @@ """Tests for the Atag sensor platform.""" + from homeassistant.components.atag.sensor import SENSORS from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er diff --git a/tests/components/atag/test_water_heater.py b/tests/components/atag/test_water_heater.py index 49425972d88..e7abd2e07d9 100644 --- a/tests/components/atag/test_water_heater.py +++ b/tests/components/atag/test_water_heater.py @@ -1,4 +1,5 @@ """Tests for the Atag water heater platform.""" + from unittest.mock import patch from homeassistant.components.atag import DOMAIN diff --git a/tests/components/august/conftest.py b/tests/components/august/conftest.py index 1cb52966fea..8640ffeecd4 100644 --- a/tests/components/august/conftest.py +++ b/tests/components/august/conftest.py @@ -1,4 +1,5 @@ """August tests conftest.""" + from unittest.mock import patch import pytest diff --git a/tests/components/august/mocks.py b/tests/components/august/mocks.py index 910c1d29ed6..75145df2509 100644 --- a/tests/components/august/mocks.py +++ b/tests/components/august/mocks.py @@ -1,4 +1,5 @@ """Mocks for the august component.""" + from __future__ import annotations from collections.abc import Iterable @@ -25,11 +26,12 @@ from yalexs.activity import ( LockOperationActivity, ) from yalexs.authenticator import AuthenticationState +from yalexs.const import Brand from yalexs.doorbell import Doorbell, DoorbellDetail from yalexs.lock import Lock, LockDetail from yalexs.pubnub_async import AugustPubNub -from homeassistant.components.august.const import CONF_LOGIN_METHOD, DOMAIN +from homeassistant.components.august.const import CONF_BRAND, CONF_LOGIN_METHOD, DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant @@ -37,13 +39,14 @@ from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry, load_fixture -def _mock_get_config(): +def _mock_get_config(brand: Brand = Brand.AUGUST): """Return a default august config.""" return { DOMAIN: { CONF_LOGIN_METHOD: "email", CONF_USERNAME: "mocked_username", CONF_PASSWORD: "mocked_password", + CONF_BRAND: brand, } } @@ -58,7 +61,7 @@ def _mock_authenticator(auth_state): @patch("homeassistant.components.august.gateway.ApiAsync") @patch("homeassistant.components.august.gateway.AuthenticatorAsync.async_authenticate") async def _mock_setup_august( - hass, api_instance, pubnub_mock, authenticate_mock, api_mock + hass, api_instance, pubnub_mock, authenticate_mock, api_mock, brand ): """Set up august integration.""" authenticate_mock.side_effect = MagicMock( @@ -69,12 +72,13 @@ async def _mock_setup_august( api_mock.return_value = api_instance entry = MockConfigEntry( domain=DOMAIN, - data=_mock_get_config()[DOMAIN], + data=_mock_get_config(brand)[DOMAIN], options={}, ) entry.add_to_hass(hass) - with patch("homeassistant.components.august.async_create_pubnub"), patch( - "homeassistant.components.august.AugustPubNub", return_value=pubnub_mock + with ( + patch("homeassistant.components.august.async_create_pubnub"), + patch("homeassistant.components.august.AugustPubNub", return_value=pubnub_mock), ): assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -87,21 +91,26 @@ async def _create_august_with_devices( api_call_side_effects: dict[str, Any] | None = None, activities: list[Any] | None = None, pubnub: AugustPubNub | None = None, + brand: Brand = Brand.AUGUST, ) -> ConfigEntry: entry, _ = await _create_august_api_with_devices( - hass, devices, api_call_side_effects, activities, pubnub + hass, devices, api_call_side_effects, activities, pubnub, brand ) return entry -async def _create_august_api_with_devices( # noqa: C901 - hass, devices, api_call_side_effects=None, activities=None, pubnub=None +async def _create_august_api_with_devices( + hass, + devices, + api_call_side_effects=None, + activities=None, + pubnub=None, + brand=Brand.AUGUST, ): if api_call_side_effects is None: api_call_side_effects = {} if pubnub is None: pubnub = AugustPubNub() - device_data = {"doorbells": [], "locks": []} for device in devices: if isinstance(device, LockDetail): @@ -110,7 +119,13 @@ async def _create_august_api_with_devices( # noqa: C901 ) elif isinstance(device, DoorbellDetail): device_data["doorbells"].append( - {"base": _mock_august_doorbell(device.device_id), "detail": device} + { + "base": _mock_august_doorbell( + deviceid=device.device_id, + brand=device._data.get("brand", Brand.AUGUST), + ), + "detail": device, + } ) else: raise ValueError # noqa: TRY004 @@ -122,10 +137,7 @@ async def _create_august_api_with_devices( # noqa: C901 raise ValueError def _get_base_devices(device_type): - base_devices = [] - for device in device_data[device_type]: - base_devices.append(device["base"]) - return base_devices + return [device["base"] for device in device_data[device_type]] def get_lock_detail_side_effect(access_token, device_id): return _get_device_detail("locks", device_id) @@ -181,7 +193,7 @@ async def _create_august_api_with_devices( # noqa: C901 ) api_instance, entry = await _mock_setup_august_with_api_side_effects( - hass, api_call_side_effects, pubnub + hass, api_call_side_effects, pubnub, brand ) if device_data["locks"]: @@ -192,7 +204,9 @@ async def _create_august_api_with_devices( # noqa: C901 return entry, api_instance -async def _mock_setup_august_with_api_side_effects(hass, api_call_side_effects, pubnub): +async def _mock_setup_august_with_api_side_effects( + hass, api_call_side_effects, pubnub, brand=Brand.AUGUST +): api_instance = MagicMock(name="Api") if api_call_side_effects["get_lock_detail"]: @@ -235,7 +249,9 @@ async def _mock_setup_august_with_api_side_effects(hass, api_call_side_effects, api_instance.async_status_async = AsyncMock() api_instance.async_get_user = AsyncMock(return_value={"UserID": "abc"}) - return api_instance, await _mock_setup_august(hass, api_instance, pubnub) + return api_instance, await _mock_setup_august( + hass, api_instance, pubnub, brand=brand + ) def _mock_august_authentication(token_text, token_timestamp, state): @@ -252,13 +268,18 @@ def _mock_august_lock(lockid="mocklockid1", houseid="mockhouseid1"): return Lock(lockid, _mock_august_lock_data(lockid=lockid, houseid=houseid)) -def _mock_august_doorbell(deviceid="mockdeviceid1", houseid="mockhouseid1"): +def _mock_august_doorbell( + deviceid="mockdeviceid1", houseid="mockhouseid1", brand=Brand.AUGUST +): return Doorbell( - deviceid, _mock_august_doorbell_data(deviceid=deviceid, houseid=houseid) + deviceid, + _mock_august_doorbell_data(deviceid=deviceid, houseid=houseid, brand=brand), ) -def _mock_august_doorbell_data(deviceid="mockdeviceid1", houseid="mockhouseid1"): +def _mock_august_doorbell_data( + deviceid="mockdeviceid1", houseid="mockhouseid1", brand=Brand.AUGUST +): return { "_id": deviceid, "DeviceID": deviceid, diff --git a/tests/components/august/test_binary_sensor.py b/tests/components/august/test_binary_sensor.py index 72352477b4a..377a5bf2897 100644 --- a/tests/components/august/test_binary_sensor.py +++ b/tests/components/august/test_binary_sensor.py @@ -1,4 +1,5 @@ """The binary_sensor tests for the august platform.""" + import datetime import time from unittest.mock import Mock, patch diff --git a/tests/components/august/test_button.py b/tests/components/august/test_button.py index eb0bce2faca..8ae2bc8a70d 100644 --- a/tests/components/august/test_button.py +++ b/tests/components/august/test_button.py @@ -1,4 +1,5 @@ """The button tests for the august platform.""" + from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant diff --git a/tests/components/august/test_camera.py b/tests/components/august/test_camera.py index d1998dc4c49..539a26cc30f 100644 --- a/tests/components/august/test_camera.py +++ b/tests/components/august/test_camera.py @@ -1,7 +1,11 @@ """The camera tests for the august platform.""" + from http import HTTPStatus from unittest.mock import patch +from yalexs.const import Brand +from yalexs.doorbell import ContentTokenExpired + from homeassistant.const import STATE_IDLE from homeassistant.core import HomeAssistant @@ -19,7 +23,7 @@ async def test_create_doorbell( with patch.object( doorbell_one, "async_get_doorbell_image", create=False, return_value="image" ): - await _create_august_with_devices(hass, [doorbell_one]) + await _create_august_with_devices(hass, [doorbell_one], brand=Brand.AUGUST) camera_k98gidt45gul_name_camera = hass.states.get( "camera.k98gidt45gul_name_camera" @@ -35,3 +39,55 @@ async def test_create_doorbell( assert resp.status == HTTPStatus.OK body = await resp.text() assert body == "image" + + +async def test_doorbell_refresh_content_token_recover( + hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator +) -> None: + """Test camera image content token expired.""" + doorbell_two = await _mock_doorbell_from_fixture(hass, "get_doorbell.json") + with patch.object( + doorbell_two, + "async_get_doorbell_image", + create=False, + side_effect=[ContentTokenExpired, "image"], + ): + await _create_august_with_devices( + hass, + [doorbell_two], + brand=Brand.YALE_HOME, + ) + url = hass.states.get("camera.k98gidt45gul_name_camera").attributes[ + "entity_picture" + ] + + client = await hass_client_no_auth() + resp = await client.get(url) + assert resp.status == HTTPStatus.OK + body = await resp.text() + assert body == "image" + + +async def test_doorbell_refresh_content_token_fail( + hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator +) -> None: + """Test camera image content token expired.""" + doorbell_two = await _mock_doorbell_from_fixture(hass, "get_doorbell.json") + with patch.object( + doorbell_two, + "async_get_doorbell_image", + create=False, + side_effect=ContentTokenExpired, + ): + await _create_august_with_devices( + hass, + [doorbell_two], + brand=Brand.YALE_HOME, + ) + url = hass.states.get("camera.k98gidt45gul_name_camera").attributes[ + "entity_picture" + ] + + client = await hass_client_no_auth() + resp = await client.get(url) + assert resp.status == HTTPStatus.INTERNAL_SERVER_ERROR diff --git a/tests/components/august/test_config_flow.py b/tests/components/august/test_config_flow.py index f30828a5d72..e1e6f622c2e 100644 --- a/tests/components/august/test_config_flow.py +++ b/tests/components/august/test_config_flow.py @@ -1,4 +1,5 @@ """Test the August config flow.""" + from unittest.mock import patch from yalexs.authenticator import ValidationResult @@ -33,13 +34,16 @@ async def test_form(hass: HomeAssistant) -> None: assert result["type"] is FlowResultType.FORM assert result["errors"] == {} - with patch( - "homeassistant.components.august.config_flow.AugustGateway.async_authenticate", - return_value=True, - ), patch( - "homeassistant.components.august.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.august.config_flow.AugustGateway.async_authenticate", + return_value=True, + ), + patch( + "homeassistant.components.august.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -141,13 +145,16 @@ async def test_form_needs_validate(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch( - "homeassistant.components.august.config_flow.AugustGateway.async_authenticate", - side_effect=RequireValidation, - ), patch( - "homeassistant.components.august.gateway.AuthenticatorAsync.async_send_verification_code", - return_value=True, - ) as mock_send_verification_code: + with ( + patch( + "homeassistant.components.august.config_flow.AugustGateway.async_authenticate", + side_effect=RequireValidation, + ), + patch( + "homeassistant.components.august.gateway.AuthenticatorAsync.async_send_verification_code", + return_value=True, + ) as mock_send_verification_code, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -163,16 +170,20 @@ async def test_form_needs_validate(hass: HomeAssistant) -> None: assert result2["step_id"] == "validation" # Try with the WRONG verification code give us the form back again - with patch( - "homeassistant.components.august.config_flow.AugustGateway.async_authenticate", - side_effect=RequireValidation, - ), patch( - "homeassistant.components.august.gateway.AuthenticatorAsync.async_validate_verification_code", - return_value=ValidationResult.INVALID_VERIFICATION_CODE, - ) as mock_validate_verification_code, patch( - "homeassistant.components.august.gateway.AuthenticatorAsync.async_send_verification_code", - return_value=True, - ) as mock_send_verification_code: + with ( + patch( + "homeassistant.components.august.config_flow.AugustGateway.async_authenticate", + side_effect=RequireValidation, + ), + patch( + "homeassistant.components.august.gateway.AuthenticatorAsync.async_validate_verification_code", + return_value=ValidationResult.INVALID_VERIFICATION_CODE, + ) as mock_validate_verification_code, + patch( + "homeassistant.components.august.gateway.AuthenticatorAsync.async_send_verification_code", + return_value=True, + ) as mock_send_verification_code, + ): result3 = await hass.config_entries.flow.async_configure( result["flow_id"], {VERIFICATION_CODE_KEY: "incorrect"}, @@ -187,18 +198,23 @@ async def test_form_needs_validate(hass: HomeAssistant) -> None: assert result3["step_id"] == "validation" # Try with the CORRECT verification code and we setup - with patch( - "homeassistant.components.august.config_flow.AugustGateway.async_authenticate", - return_value=True, - ), patch( - "homeassistant.components.august.gateway.AuthenticatorAsync.async_validate_verification_code", - return_value=ValidationResult.VALIDATED, - ) as mock_validate_verification_code, patch( - "homeassistant.components.august.gateway.AuthenticatorAsync.async_send_verification_code", - return_value=True, - ) as mock_send_verification_code, patch( - "homeassistant.components.august.async_setup_entry", return_value=True - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.august.config_flow.AugustGateway.async_authenticate", + return_value=True, + ), + patch( + "homeassistant.components.august.gateway.AuthenticatorAsync.async_validate_verification_code", + return_value=ValidationResult.VALIDATED, + ) as mock_validate_verification_code, + patch( + "homeassistant.components.august.gateway.AuthenticatorAsync.async_send_verification_code", + return_value=True, + ) as mock_send_verification_code, + patch( + "homeassistant.components.august.async_setup_entry", return_value=True + ) as mock_setup_entry, + ): result4 = await hass.config_entries.flow.async_configure( result["flow_id"], {VERIFICATION_CODE_KEY: "correct"}, @@ -242,13 +258,16 @@ async def test_form_reauth(hass: HomeAssistant) -> None: assert result["type"] is FlowResultType.FORM assert result["errors"] == {} - with patch( - "homeassistant.components.august.config_flow.AugustGateway.async_authenticate", - return_value=True, - ), patch( - "homeassistant.components.august.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.august.config_flow.AugustGateway.async_authenticate", + return_value=True, + ), + patch( + "homeassistant.components.august.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -285,13 +304,16 @@ async def test_form_reauth_with_2fa(hass: HomeAssistant) -> None: assert result["type"] is FlowResultType.FORM assert result["errors"] == {} - with patch( - "homeassistant.components.august.config_flow.AugustGateway.async_authenticate", - side_effect=RequireValidation, - ), patch( - "homeassistant.components.august.gateway.AuthenticatorAsync.async_send_verification_code", - return_value=True, - ) as mock_send_verification_code: + with ( + patch( + "homeassistant.components.august.config_flow.AugustGateway.async_authenticate", + side_effect=RequireValidation, + ), + patch( + "homeassistant.components.august.gateway.AuthenticatorAsync.async_send_verification_code", + return_value=True, + ) as mock_send_verification_code, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -306,18 +328,23 @@ async def test_form_reauth_with_2fa(hass: HomeAssistant) -> None: assert result2["step_id"] == "validation" # Try with the CORRECT verification code and we setup - with patch( - "homeassistant.components.august.config_flow.AugustGateway.async_authenticate", - return_value=True, - ), patch( - "homeassistant.components.august.gateway.AuthenticatorAsync.async_validate_verification_code", - return_value=ValidationResult.VALIDATED, - ) as mock_validate_verification_code, patch( - "homeassistant.components.august.gateway.AuthenticatorAsync.async_send_verification_code", - return_value=True, - ) as mock_send_verification_code, patch( - "homeassistant.components.august.async_setup_entry", return_value=True - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.august.config_flow.AugustGateway.async_authenticate", + return_value=True, + ), + patch( + "homeassistant.components.august.gateway.AuthenticatorAsync.async_validate_verification_code", + return_value=ValidationResult.VALIDATED, + ) as mock_validate_verification_code, + patch( + "homeassistant.components.august.gateway.AuthenticatorAsync.async_send_verification_code", + return_value=True, + ) as mock_send_verification_code, + patch( + "homeassistant.components.august.async_setup_entry", return_value=True + ) as mock_setup_entry, + ): result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], {VERIFICATION_CODE_KEY: "correct"}, @@ -353,13 +380,16 @@ async def test_switching_brands(hass: HomeAssistant) -> None: assert result["type"] is FlowResultType.FORM assert result["errors"] == {} - with patch( - "homeassistant.components.august.config_flow.AugustGateway.async_authenticate", - return_value=True, - ), patch( - "homeassistant.components.august.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.august.config_flow.AugustGateway.async_authenticate", + return_value=True, + ), + patch( + "homeassistant.components.august.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { diff --git a/tests/components/august/test_diagnostics.py b/tests/components/august/test_diagnostics.py index 72008f02d03..0b00bde7b23 100644 --- a/tests/components/august/test_diagnostics.py +++ b/tests/components/august/test_diagnostics.py @@ -1,4 +1,5 @@ """Test august diagnostics.""" + from syrupy import SnapshotAssertion from homeassistant.core import HomeAssistant diff --git a/tests/components/august/test_gateway.py b/tests/components/august/test_gateway.py index 2a364304c4b..535e547d915 100644 --- a/tests/components/august/test_gateway.py +++ b/tests/components/august/test_gateway.py @@ -1,4 +1,5 @@ """The gateway tests for the august platform.""" + from unittest.mock import MagicMock, patch from yalexs.authenticator_common import AuthenticationState diff --git a/tests/components/august/test_init.py b/tests/components/august/test_init.py index 81d8992948f..6795491abe3 100644 --- a/tests/components/august/test_init.py +++ b/tests/components/august/test_init.py @@ -1,4 +1,5 @@ """The tests for the august platform.""" + from unittest.mock import Mock, patch from aiohttp import ClientResponseError diff --git a/tests/components/august/test_lock.py b/tests/components/august/test_lock.py index bc2cd23b23d..39c1745d967 100644 --- a/tests/components/august/test_lock.py +++ b/tests/components/august/test_lock.py @@ -1,4 +1,5 @@ """The lock tests for the august platform.""" + import datetime from unittest.mock import Mock diff --git a/tests/components/august/test_sensor.py b/tests/components/august/test_sensor.py index 10b7eb86235..0227ee64ef1 100644 --- a/tests/components/august/test_sensor.py +++ b/tests/components/august/test_sensor.py @@ -1,4 +1,5 @@ """The sensor tests for the august platform.""" + from typing import Any from homeassistant import core as ha diff --git a/tests/components/aurora/test_config_flow.py b/tests/components/aurora/test_config_flow.py index ebd7780900a..d8f42e48842 100644 --- a/tests/components/aurora/test_config_flow.py +++ b/tests/components/aurora/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Aurora config flow.""" + from unittest.mock import patch from aiohttp import ClientError @@ -24,13 +25,16 @@ async def test_form(hass: HomeAssistant) -> None: assert result["type"] == "form" assert result["errors"] == {} - with patch( - "homeassistant.components.aurora.config_flow.AuroraForecast.get_forecast_data", - return_value=True, - ), patch( - "homeassistant.components.aurora.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.aurora.config_flow.AuroraForecast.get_forecast_data", + return_value=True, + ), + patch( + "homeassistant.components.aurora.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], DATA, diff --git a/tests/components/aurora_abb_powerone/test_config_flow.py b/tests/components/aurora_abb_powerone/test_config_flow.py index 3b5b375ed8b..fbeaff2f4f8 100644 --- a/tests/components/aurora_abb_powerone/test_config_flow.py +++ b/tests/components/aurora_abb_powerone/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Aurora ABB PowerOne Solar PV config flow.""" + from unittest.mock import patch from aurorapy.client import AuroraError, AuroraTimeoutError @@ -32,25 +33,32 @@ async def test_form(hass: HomeAssistant) -> None: assert result["type"] == "form" assert result["errors"] == {} - with patch( - "aurorapy.client.AuroraSerialClient.connect", - return_value=None, - ), patch( - "aurorapy.client.AuroraSerialClient.serial_number", - return_value="9876543", - ), patch( - "aurorapy.client.AuroraSerialClient.version", - return_value="9.8.7.6", - ), patch( - "aurorapy.client.AuroraSerialClient.pn", - return_value="A.B.C", - ), patch( - "aurorapy.client.AuroraSerialClient.firmware", - return_value="1.234", - ) as mock_setup, patch( - "homeassistant.components.aurora_abb_powerone.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "aurorapy.client.AuroraSerialClient.connect", + return_value=None, + ), + patch( + "aurorapy.client.AuroraSerialClient.serial_number", + return_value="9876543", + ), + patch( + "aurorapy.client.AuroraSerialClient.version", + return_value="9.8.7.6", + ), + patch( + "aurorapy.client.AuroraSerialClient.pn", + return_value="A.B.C", + ), + patch( + "aurorapy.client.AuroraSerialClient.firmware", + return_value="1.234", + ) as mock_setup, + patch( + "homeassistant.components.aurora_abb_powerone.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_PORT: "/dev/ttyUSB7", CONF_ADDRESS: 7}, @@ -134,16 +142,20 @@ async def test_form_invalid_com_ports(hass: HomeAssistant) -> None: ) assert result2["errors"] == {"base": "cannot_connect"} - with patch( - "aurorapy.client.AuroraSerialClient.connect", - side_effect=AuroraError("...Some other message!!!123..."), - return_value=None, - ), patch( - "serial.Serial.isOpen", - return_value=True, - ), patch( - "aurorapy.client.AuroraSerialClient.close", - ) as mock_clientclose: + with ( + patch( + "aurorapy.client.AuroraSerialClient.connect", + side_effect=AuroraError("...Some other message!!!123..."), + return_value=None, + ), + patch( + "serial.Serial.isOpen", + return_value=True, + ), + patch( + "aurorapy.client.AuroraSerialClient.close", + ) as mock_clientclose, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_PORT: "/dev/ttyUSB7", CONF_ADDRESS: 7}, diff --git a/tests/components/aurora_abb_powerone/test_init.py b/tests/components/aurora_abb_powerone/test_init.py index a330507c779..18b9854773e 100644 --- a/tests/components/aurora_abb_powerone/test_init.py +++ b/tests/components/aurora_abb_powerone/test_init.py @@ -1,4 +1,5 @@ """Pytest modules for Aurora ABB Powerone PV inverter sensor integration.""" + from unittest.mock import patch from homeassistant.components.aurora_abb_powerone.const import ( @@ -16,18 +17,24 @@ from tests.common import MockConfigEntry async def test_unload_entry(hass: HomeAssistant) -> None: """Test unloading the aurora_abb_powerone entry.""" - with patch("aurorapy.client.AuroraSerialClient.connect", return_value=None), patch( - "aurorapy.client.AuroraSerialClient.serial_number", - return_value="9876543", - ), patch( - "aurorapy.client.AuroraSerialClient.version", - return_value="9.8.7.6", - ), patch( - "aurorapy.client.AuroraSerialClient.pn", - return_value="A.B.C", - ), patch( - "aurorapy.client.AuroraSerialClient.firmware", - return_value="1.234", + with ( + patch("aurorapy.client.AuroraSerialClient.connect", return_value=None), + patch( + "aurorapy.client.AuroraSerialClient.serial_number", + return_value="9876543", + ), + patch( + "aurorapy.client.AuroraSerialClient.version", + return_value="9.8.7.6", + ), + patch( + "aurorapy.client.AuroraSerialClient.pn", + return_value="A.B.C", + ), + patch( + "aurorapy.client.AuroraSerialClient.firmware", + return_value="1.234", + ), ): mock_entry = MockConfigEntry( domain=DOMAIN, diff --git a/tests/components/aurora_abb_powerone/test_sensor.py b/tests/components/aurora_abb_powerone/test_sensor.py index 4dbbf5f0048..4bc5a5d3086 100644 --- a/tests/components/aurora_abb_powerone/test_sensor.py +++ b/tests/components/aurora_abb_powerone/test_sensor.py @@ -1,8 +1,10 @@ """Test the Aurora ABB PowerOne Solar PV sensors.""" + from unittest.mock import patch from aurorapy.client import AuroraError, AuroraTimeoutError from freezegun.api import FrozenDateTimeFactory +import pytest from homeassistant.components.aurora_abb_powerone.const import ( ATTR_DEVICE_NAME, @@ -12,8 +14,10 @@ from homeassistant.components.aurora_abb_powerone.const import ( DOMAIN, SCAN_INTERVAL, ) +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ATTR_SERIAL_NUMBER, CONF_ADDRESS, CONF_PORT from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_registry import EntityRegistry, RegistryEntryDisabler from tests.common import MockConfigEntry, async_fire_time_changed @@ -28,8 +32,14 @@ TEST_CONFIG = { def _simulated_returns(index, global_measure=None): returns = { + 1: 235.9476, # voltage + 2: 2.7894, # current 3: 45.678, # power + 4: 50.789, # frequency + 6: 1.2345, # leak dcdc + 7: 2.3456, # leak inverter 21: 9.876, # temperature + 30: 0.1234, # Isolation resistance 5: 12345, # energy } return returns[index] @@ -54,30 +64,37 @@ def _mock_config_entry(): ) -async def test_sensors(hass: HomeAssistant) -> None: +async def test_sensors(hass: HomeAssistant, entity_registry: EntityRegistry) -> None: """Test data coming back from inverter.""" mock_entry = _mock_config_entry() - with patch("aurorapy.client.AuroraSerialClient.connect", return_value=None), patch( - "aurorapy.client.AuroraSerialClient.measure", - side_effect=_simulated_returns, - ), patch( - "aurorapy.client.AuroraSerialClient.alarms", return_value=["No alarm"] - ), patch( - "aurorapy.client.AuroraSerialClient.serial_number", - return_value="9876543", - ), patch( - "aurorapy.client.AuroraSerialClient.version", - return_value="9.8.7.6", - ), patch( - "aurorapy.client.AuroraSerialClient.pn", - return_value="A.B.C", - ), patch( - "aurorapy.client.AuroraSerialClient.firmware", - return_value="1.234", - ), patch( - "aurorapy.client.AuroraSerialClient.cumulated_energy", - side_effect=_simulated_returns, + with ( + patch("aurorapy.client.AuroraSerialClient.connect", return_value=None), + patch( + "aurorapy.client.AuroraSerialClient.measure", + side_effect=_simulated_returns, + ), + patch("aurorapy.client.AuroraSerialClient.alarms", return_value=["No alarm"]), + patch( + "aurorapy.client.AuroraSerialClient.serial_number", + return_value="9876543", + ), + patch( + "aurorapy.client.AuroraSerialClient.version", + return_value="9.8.7.6", + ), + patch( + "aurorapy.client.AuroraSerialClient.pn", + return_value="A.B.C", + ), + patch( + "aurorapy.client.AuroraSerialClient.firmware", + return_value="1.234", + ), + patch( + "aurorapy.client.AuroraSerialClient.cumulated_energy", + side_effect=_simulated_returns, + ), ): mock_entry.add_to_hass(hass) await hass.config_entries.async_setup(mock_entry.entry_id) @@ -95,31 +112,71 @@ async def test_sensors(hass: HomeAssistant) -> None: assert energy assert energy.state == "12.35" + # Test the 'disabled by default' sensors. + sensors = [ + ("sensor.mydevicename_grid_voltage", "235.9"), + ("sensor.mydevicename_grid_current", "2.8"), + ("sensor.mydevicename_frequency", "50.8"), + ("sensor.mydevicename_dc_dc_leak_current", "1.2345"), + ("sensor.mydevicename_inverter_leak_current", "2.3456"), + ("sensor.mydevicename_isolation_resistance", "0.1234"), + ] + for entity_id, _ in sensors: + assert not hass.states.get(entity_id) + assert ( + entry := entity_registry.async_get(entity_id) + ), f"Entity registry entry for {entity_id} is missing" + assert entry.disabled + assert entry.disabled_by is RegistryEntryDisabler.INTEGRATION + + # re-enable it + entity_registry.async_update_entity(entity_id=entity_id, disabled_by=None) + + # must reload the integration when enabling an entity + await hass.config_entries.async_unload(mock_entry.entry_id) + await hass.async_block_till_done() + assert mock_entry.state is ConfigEntryState.NOT_LOADED + mock_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + for entity_id, value in sensors: + item = hass.states.get(entity_id) + assert item + assert item.state == value + async def test_sensor_dark(hass: HomeAssistant, freezer: FrozenDateTimeFactory) -> None: """Test that darkness (no comms) is handled correctly.""" mock_entry = _mock_config_entry() # sun is up - with patch("aurorapy.client.AuroraSerialClient.connect", return_value=None), patch( - "aurorapy.client.AuroraSerialClient.measure", side_effect=_simulated_returns - ), patch( - "aurorapy.client.AuroraSerialClient.alarms", return_value=["No alarm"] - ), patch( - "aurorapy.client.AuroraSerialClient.cumulated_energy", - side_effect=_simulated_returns, - ), patch( - "aurorapy.client.AuroraSerialClient.serial_number", - return_value="9876543", - ), patch( - "aurorapy.client.AuroraSerialClient.version", - return_value="9.8.7.6", - ), patch( - "aurorapy.client.AuroraSerialClient.pn", - return_value="A.B.C", - ), patch( - "aurorapy.client.AuroraSerialClient.firmware", - return_value="1.234", + with ( + patch("aurorapy.client.AuroraSerialClient.connect", return_value=None), + patch( + "aurorapy.client.AuroraSerialClient.measure", side_effect=_simulated_returns + ), + patch("aurorapy.client.AuroraSerialClient.alarms", return_value=["No alarm"]), + patch( + "aurorapy.client.AuroraSerialClient.cumulated_energy", + side_effect=_simulated_returns, + ), + patch( + "aurorapy.client.AuroraSerialClient.serial_number", + return_value="9876543", + ), + patch( + "aurorapy.client.AuroraSerialClient.version", + return_value="9.8.7.6", + ), + patch( + "aurorapy.client.AuroraSerialClient.pn", + return_value="A.B.C", + ), + patch( + "aurorapy.client.AuroraSerialClient.firmware", + return_value="1.234", + ), ): mock_entry.add_to_hass(hass) await hass.config_entries.async_setup(mock_entry.entry_id) @@ -130,58 +187,100 @@ async def test_sensor_dark(hass: HomeAssistant, freezer: FrozenDateTimeFactory) assert power.state == "45.7" # sunset - with patch("aurorapy.client.AuroraSerialClient.connect", return_value=None), patch( - "aurorapy.client.AuroraSerialClient.measure", - side_effect=AuroraTimeoutError("No response after 10 seconds"), - ), patch( - "aurorapy.client.AuroraSerialClient.cumulated_energy", - side_effect=AuroraTimeoutError("No response after 3 tries"), - ), patch("aurorapy.client.AuroraSerialClient.alarms", return_value=["No alarm"]): + with ( + patch("aurorapy.client.AuroraSerialClient.connect", return_value=None), + patch( + "aurorapy.client.AuroraSerialClient.measure", + side_effect=AuroraTimeoutError("No response after 10 seconds"), + ), + patch( + "aurorapy.client.AuroraSerialClient.cumulated_energy", + side_effect=AuroraTimeoutError("No response after 3 tries"), + ), + patch("aurorapy.client.AuroraSerialClient.alarms", return_value=["No alarm"]), + ): freezer.tick(SCAN_INTERVAL * 2) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) power = hass.states.get("sensor.mydevicename_total_energy") assert power.state == "unknown" # sun rose again - with patch("aurorapy.client.AuroraSerialClient.connect", return_value=None), patch( - "aurorapy.client.AuroraSerialClient.measure", side_effect=_simulated_returns - ), patch( - "aurorapy.client.AuroraSerialClient.cumulated_energy", - side_effect=_simulated_returns, - ), patch("aurorapy.client.AuroraSerialClient.alarms", return_value=["No alarm"]): + with ( + patch("aurorapy.client.AuroraSerialClient.connect", return_value=None), + patch( + "aurorapy.client.AuroraSerialClient.measure", side_effect=_simulated_returns + ), + patch( + "aurorapy.client.AuroraSerialClient.cumulated_energy", + side_effect=_simulated_returns, + ), + patch("aurorapy.client.AuroraSerialClient.alarms", return_value=["No alarm"]), + ): freezer.tick(SCAN_INTERVAL * 4) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) power = hass.states.get("sensor.mydevicename_power_output") assert power is not None assert power.state == "45.7" # sunset - with patch("aurorapy.client.AuroraSerialClient.connect", return_value=None), patch( - "aurorapy.client.AuroraSerialClient.measure", - side_effect=AuroraTimeoutError("No response after 10 seconds"), - ), patch( - "aurorapy.client.AuroraSerialClient.cumulated_energy", - side_effect=AuroraError("No response after 10 seconds"), - ), patch("aurorapy.client.AuroraSerialClient.alarms", return_value=["No alarm"]): + with ( + patch("aurorapy.client.AuroraSerialClient.connect", return_value=None), + patch( + "aurorapy.client.AuroraSerialClient.measure", + side_effect=AuroraTimeoutError("No response after 10 seconds"), + ), + patch( + "aurorapy.client.AuroraSerialClient.cumulated_energy", + side_effect=AuroraError("No response after 10 seconds"), + ), + patch("aurorapy.client.AuroraSerialClient.alarms", return_value=["No alarm"]), + ): freezer.tick(SCAN_INTERVAL * 6) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) power = hass.states.get("sensor.mydevicename_power_output") assert power.state == "unknown" # should this be 'available'? -async def test_sensor_unknown_error(hass: HomeAssistant) -> None: +async def test_sensor_unknown_error( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + freezer: FrozenDateTimeFactory, +) -> None: """Test other comms error is handled correctly.""" mock_entry = _mock_config_entry() - with patch("aurorapy.client.AuroraSerialClient.connect", return_value=None), patch( - "aurorapy.client.AuroraSerialClient.measure", - side_effect=AuroraError("another error"), - ), patch( - "aurorapy.client.AuroraSerialClient.alarms", return_value=["No alarm"] - ), patch("serial.Serial.isOpen", return_value=True): + # sun is up + with ( + patch("aurorapy.client.AuroraSerialClient.connect", return_value=None), + patch( + "aurorapy.client.AuroraSerialClient.measure", side_effect=_simulated_returns + ), + patch("aurorapy.client.AuroraSerialClient.alarms", return_value=["No alarm"]), + patch( + "aurorapy.client.AuroraSerialClient.cumulated_energy", + side_effect=_simulated_returns, + ), + ): mock_entry.add_to_hass(hass) await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() + + with ( + patch("aurorapy.client.AuroraSerialClient.connect", return_value=None), + patch( + "aurorapy.client.AuroraSerialClient.measure", + side_effect=AuroraError("another error"), + ), + patch("aurorapy.client.AuroraSerialClient.alarms", return_value=["No alarm"]), + patch("serial.Serial.isOpen", return_value=True), + ): + freezer.tick(SCAN_INTERVAL * 2) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + assert ( + "Exception: AuroraError('another error') occurred, 2 retries remaining" + in caplog.text + ) power = hass.states.get("sensor.mydevicename_power_output") - assert power is None + assert power.state == "unavailable" diff --git a/tests/components/aussie_broadband/common.py b/tests/components/aussie_broadband/common.py index 5d050388b38..1c992d116d1 100644 --- a/tests/components/aussie_broadband/common.py +++ b/tests/components/aussie_broadband/common.py @@ -1,4 +1,5 @@ """Aussie Broadband common helpers for tests.""" + from unittest.mock import patch from homeassistant.components.aussie_broadband.const import ( @@ -49,20 +50,24 @@ async def setup_platform( ) mock_entry.add_to_hass(hass) - with patch("homeassistant.components.aussie_broadband.PLATFORMS", platforms), patch( - "aussiebb.asyncio.AussieBB.__init__", return_value=None - ), patch( - "aussiebb.asyncio.AussieBB.login", - return_value=True, - side_effect=side_effect, - ), patch( - "aussiebb.asyncio.AussieBB.get_services", - return_value=FAKE_SERVICES, - side_effect=side_effect, - ), patch( - "aussiebb.asyncio.AussieBB.get_usage", - return_value=usage, - side_effect=usage_effect, + with ( + patch("homeassistant.components.aussie_broadband.PLATFORMS", platforms), + patch("aussiebb.asyncio.AussieBB.__init__", return_value=None), + patch( + "aussiebb.asyncio.AussieBB.login", + return_value=True, + side_effect=side_effect, + ), + patch( + "aussiebb.asyncio.AussieBB.get_services", + return_value=FAKE_SERVICES, + side_effect=side_effect, + ), + patch( + "aussiebb.asyncio.AussieBB.get_usage", + return_value=usage, + side_effect=usage_effect, + ), ): await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/aussie_broadband/snapshots/test_diagnostics.ambr b/tests/components/aussie_broadband/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..e419fac09c9 --- /dev/null +++ b/tests/components/aussie_broadband/snapshots/test_diagnostics.ambr @@ -0,0 +1,40 @@ +# serializer version: 1 +# name: test_select_async_setup_entry + dict({ + 'services': list([ + dict({ + 'service': dict({ + 'coordinator': '**REDACTED**', + 'description': '**REDACTED**', + 'name': 'NBN', + 'service_id': '12345678', + 'type': 'NBN', + }), + 'usage': dict({ + }), + }), + dict({ + 'service': dict({ + 'coordinator': '**REDACTED**', + 'description': '**REDACTED**', + 'name': 'Mobile', + 'service_id': '87654321', + 'type': 'PhoneMobile', + }), + 'usage': dict({ + }), + }), + dict({ + 'service': dict({ + 'coordinator': '**REDACTED**', + 'description': '**REDACTED**', + 'name': 'VOIP', + 'service_id': '23456789', + 'type': 'VOIP', + }), + 'usage': dict({ + }), + }), + ]), + }) +# --- diff --git a/tests/components/aussie_broadband/test_config_flow.py b/tests/components/aussie_broadband/test_config_flow.py index 9b4be6d9463..f08b56502b8 100644 --- a/tests/components/aussie_broadband/test_config_flow.py +++ b/tests/components/aussie_broadband/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Aussie Broadband config flow.""" + from unittest.mock import patch from aiohttp import ClientConnectionError @@ -24,14 +25,15 @@ async def test_form(hass: HomeAssistant) -> None: assert result1["type"] == FlowResultType.FORM assert result1["errors"] is None - with patch("aussiebb.asyncio.AussieBB.__init__", return_value=None), patch( - "aussiebb.asyncio.AussieBB.login", return_value=True - ), patch( - "aussiebb.asyncio.AussieBB.get_services", return_value=FAKE_SERVICES - ), patch( - "homeassistant.components.aussie_broadband.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch("aussiebb.asyncio.AussieBB.__init__", return_value=None), + patch("aussiebb.asyncio.AussieBB.login", return_value=True), + patch("aussiebb.asyncio.AussieBB.get_services", return_value=FAKE_SERVICES), + patch( + "homeassistant.components.aussie_broadband.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result1["flow_id"], FAKE_DATA, @@ -51,14 +53,17 @@ async def test_already_configured(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch("aussiebb.asyncio.AussieBB.__init__", return_value=None), patch( - "aussiebb.asyncio.AussieBB.login", return_value=True - ), patch( - "aussiebb.asyncio.AussieBB.get_services", return_value=[FAKE_SERVICES[0]] - ), patch( - "homeassistant.components.aussie_broadband.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch("aussiebb.asyncio.AussieBB.__init__", return_value=None), + patch("aussiebb.asyncio.AussieBB.login", return_value=True), + patch( + "aussiebb.asyncio.AussieBB.get_services", return_value=[FAKE_SERVICES[0]] + ), + patch( + "homeassistant.components.aussie_broadband.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): await hass.config_entries.flow.async_configure( result1["flow_id"], FAKE_DATA, @@ -69,14 +74,17 @@ async def test_already_configured(hass: HomeAssistant) -> None: result3 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch("aussiebb.asyncio.AussieBB.__init__", return_value=None), patch( - "aussiebb.asyncio.AussieBB.login", return_value=True - ), patch( - "aussiebb.asyncio.AussieBB.get_services", return_value=[FAKE_SERVICES[0]] - ), patch( - "homeassistant.components.aussie_broadband.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch("aussiebb.asyncio.AussieBB.__init__", return_value=None), + patch("aussiebb.asyncio.AussieBB.login", return_value=True), + patch( + "aussiebb.asyncio.AussieBB.get_services", return_value=[FAKE_SERVICES[0]] + ), + patch( + "homeassistant.components.aussie_broadband.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result4 = await hass.config_entries.flow.async_configure( result3["flow_id"], FAKE_DATA, @@ -95,12 +103,15 @@ async def test_no_services(hass: HomeAssistant) -> None: assert result1["type"] == FlowResultType.FORM assert result1["errors"] is None - with patch("aussiebb.asyncio.AussieBB.__init__", return_value=None), patch( - "aussiebb.asyncio.AussieBB.login", return_value=True - ), patch("aussiebb.asyncio.AussieBB.get_services", return_value=[]), patch( - "homeassistant.components.aussie_broadband.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch("aussiebb.asyncio.AussieBB.__init__", return_value=None), + patch("aussiebb.asyncio.AussieBB.login", return_value=True), + patch("aussiebb.asyncio.AussieBB.get_services", return_value=[]), + patch( + "homeassistant.components.aussie_broadband.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result1["flow_id"], FAKE_DATA, @@ -118,8 +129,9 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch("aussiebb.asyncio.AussieBB.__init__", return_value=None), patch( - "aussiebb.asyncio.AussieBB.login", side_effect=AuthenticationException() + with ( + patch("aussiebb.asyncio.AussieBB.__init__", return_value=None), + patch("aussiebb.asyncio.AussieBB.login", side_effect=AuthenticationException()), ): result2 = await hass.config_entries.flow.async_configure( result1["flow_id"], @@ -136,8 +148,9 @@ async def test_form_network_issue(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch("aussiebb.asyncio.AussieBB.__init__", return_value=None), patch( - "aussiebb.asyncio.AussieBB.login", side_effect=ClientConnectionError() + with ( + patch("aussiebb.asyncio.AussieBB.__init__", return_value=None), + patch("aussiebb.asyncio.AussieBB.login", side_effect=ClientConnectionError()), ): result2 = await hass.config_entries.flow.async_configure( result1["flow_id"], @@ -156,13 +169,16 @@ async def test_reauth(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data=FAKE_DATA ) - with patch("aussiebb.asyncio.AussieBB.__init__", return_value=None), patch( - "aussiebb.asyncio.AussieBB.login", return_value=True - ), patch( - "aussiebb.asyncio.AussieBB.get_services", return_value=[FAKE_SERVICES[0]] - ), patch( - "homeassistant.components.aussie_broadband.async_setup_entry", - return_value=True, + with ( + patch("aussiebb.asyncio.AussieBB.__init__", return_value=None), + patch("aussiebb.asyncio.AussieBB.login", return_value=True), + patch( + "aussiebb.asyncio.AussieBB.get_services", return_value=[FAKE_SERVICES[0]] + ), + patch( + "homeassistant.components.aussie_broadband.async_setup_entry", + return_value=True, + ), ): result2 = await hass.config_entries.flow.async_configure( result1["flow_id"], @@ -184,9 +200,13 @@ async def test_reauth(hass: HomeAssistant) -> None: ) assert result5["step_id"] == "reauth_confirm" - with patch("aussiebb.asyncio.AussieBB.__init__", return_value=None), patch( - "aussiebb.asyncio.AussieBB.login", side_effect=AuthenticationException() - ), patch("aussiebb.asyncio.AussieBB.get_services", return_value=[FAKE_SERVICES[0]]): + with ( + patch("aussiebb.asyncio.AussieBB.__init__", return_value=None), + patch("aussiebb.asyncio.AussieBB.login", side_effect=AuthenticationException()), + patch( + "aussiebb.asyncio.AussieBB.get_services", return_value=[FAKE_SERVICES[0]] + ), + ): result6 = await hass.config_entries.flow.async_configure( result5["flow_id"], { @@ -198,9 +218,13 @@ async def test_reauth(hass: HomeAssistant) -> None: assert result6["step_id"] == "reauth_confirm" # Test successful reauth - with patch("aussiebb.asyncio.AussieBB.__init__", return_value=None), patch( - "aussiebb.asyncio.AussieBB.login", return_value=True - ), patch("aussiebb.asyncio.AussieBB.get_services", return_value=[FAKE_SERVICES[0]]): + with ( + patch("aussiebb.asyncio.AussieBB.__init__", return_value=None), + patch("aussiebb.asyncio.AussieBB.login", return_value=True), + patch( + "aussiebb.asyncio.AussieBB.get_services", return_value=[FAKE_SERVICES[0]] + ), + ): result7 = await hass.config_entries.flow.async_configure( result6["flow_id"], { diff --git a/tests/components/aussie_broadband/test_diagnostics.py b/tests/components/aussie_broadband/test_diagnostics.py new file mode 100644 index 00000000000..b95e581feff --- /dev/null +++ b/tests/components/aussie_broadband/test_diagnostics.py @@ -0,0 +1,22 @@ +"""Test the Aussie Broadband Diagnostics.""" + +from syrupy.assertion import SnapshotAssertion + +from homeassistant.core import HomeAssistant + +from .common import setup_platform + +from tests.components.diagnostics import get_diagnostics_for_config_entry +from tests.typing import ClientSessionGenerator + + +async def test_select_async_setup_entry( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + snapshot: SnapshotAssertion, +) -> None: + """Test diagnostics platform.""" + + entry = await setup_platform(hass, []) + diag = await get_diagnostics_for_config_entry(hass, hass_client, entry) + assert diag == snapshot diff --git a/tests/components/aussie_broadband/test_init.py b/tests/components/aussie_broadband/test_init.py index e16a721f5dc..00f7e5e7a83 100644 --- a/tests/components/aussie_broadband/test_init.py +++ b/tests/components/aussie_broadband/test_init.py @@ -1,4 +1,5 @@ """Test the Aussie Broadband init.""" + from unittest.mock import patch from aiohttp import ClientConnectionError @@ -22,7 +23,7 @@ async def test_unload(hass: HomeAssistant) -> None: 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", + "homeassistant.components.aussie_broadband.config_flow.AussieBroadbandConfigFlow.async_step_reauth", return_value={ "type": data_entry_flow.FlowResultType.FORM, "flow_id": "mock_flow", diff --git a/tests/components/aussie_broadband/test_sensor.py b/tests/components/aussie_broadband/test_sensor.py index d4c8fb2e32b..ba26e9d67ec 100644 --- a/tests/components/aussie_broadband/test_sensor.py +++ b/tests/components/aussie_broadband/test_sensor.py @@ -1,4 +1,5 @@ """Aussie Broadband sensor platform tests.""" + from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import STATE_UNKNOWN from homeassistant.core import HomeAssistant diff --git a/tests/components/auth/__init__.py b/tests/components/auth/__init__.py index 8b731934913..18904cb2710 100644 --- a/tests/components/auth/__init__.py +++ b/tests/components/auth/__init__.py @@ -1,4 +1,5 @@ """Tests for the auth component.""" + from typing import Any from homeassistant import auth diff --git a/tests/components/auth/conftest.py b/tests/components/auth/conftest.py index b7e69a44a0d..a17661f5635 100644 --- a/tests/components/auth/conftest.py +++ b/tests/components/auth/conftest.py @@ -1,4 +1,5 @@ """Test configuration for auth.""" + import pytest diff --git a/tests/components/auth/test_indieauth.py b/tests/components/auth/test_indieauth.py index 612d97808a7..2a8d6894dc6 100644 --- a/tests/components/auth/test_indieauth.py +++ b/tests/components/auth/test_indieauth.py @@ -1,4 +1,5 @@ """Tests for the client validator.""" + import asyncio from unittest.mock import patch diff --git a/tests/components/auth/test_init.py b/tests/components/auth/test_init.py index 3926cd2f82b..18b86f561d0 100644 --- a/tests/components/auth/test_init.py +++ b/tests/components/auth/test_init.py @@ -1,4 +1,5 @@ """Integration tests for the auth component.""" + from datetime import timedelta from http import HTTPStatus import logging diff --git a/tests/components/auth/test_init_link_user.py b/tests/components/auth/test_init_link_user.py index 507d24e5dac..d1a5fa51af2 100644 --- a/tests/components/auth/test_init_link_user.py +++ b/tests/components/auth/test_init_link_user.py @@ -1,4 +1,5 @@ """Tests for the link user flow.""" + from http import HTTPStatus from unittest.mock import patch diff --git a/tests/components/auth/test_login_flow.py b/tests/components/auth/test_login_flow.py index c8b0261b79c..af9a2cf62f1 100644 --- a/tests/components/auth/test_login_flow.py +++ b/tests/components/auth/test_login_flow.py @@ -1,4 +1,5 @@ """Tests for the login flow.""" + from http import HTTPStatus from typing import Any from unittest.mock import patch @@ -218,11 +219,15 @@ async def test_invalid_redirect_uri( assert resp.status == HTTPStatus.OK step = await resp.json() - with patch( - "homeassistant.components.auth.indieauth.fetch_redirect_uris", return_value=[] - ), patch( - "homeassistant.components.http.ban.process_wrong_login" - ) as mock_process_wrong_login: + with ( + patch( + "homeassistant.components.auth.indieauth.fetch_redirect_uris", + return_value=[], + ), + patch( + "homeassistant.components.http.ban.process_wrong_login" + ) as mock_process_wrong_login, + ): resp = await client.post( f"/auth/login_flow/{step['flow_id']}", json={ diff --git a/tests/components/auth/test_mfa_setup_flow.py b/tests/components/auth/test_mfa_setup_flow.py index d87eb0cba73..f8c3153fba6 100644 --- a/tests/components/auth/test_mfa_setup_flow.py +++ b/tests/components/auth/test_mfa_setup_flow.py @@ -1,4 +1,5 @@ """Tests for the mfa setup flow.""" + from homeassistant import data_entry_flow from homeassistant.auth import auth_manager_from_config from homeassistant.components.auth import mfa_setup_flow diff --git a/tests/components/automation/test_blueprint.py b/tests/components/automation/test_blueprint.py index 5df33f9b4b8..24d77800508 100644 --- a/tests/components/automation/test_blueprint.py +++ b/tests/components/automation/test_blueprint.py @@ -1,4 +1,5 @@ """Test built-in blueprints.""" + import asyncio import contextlib from datetime import timedelta diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index 462f748c15e..00a7e6980d7 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -1,4 +1,5 @@ """The tests for the automation component.""" + import asyncio from datetime import timedelta import logging @@ -753,7 +754,7 @@ async def test_automation_stops(hass: HomeAssistant, calls, service) -> None: assert len(calls) == (1 if service == "turn_off_no_stop" else 0) -@pytest.mark.parametrize("extra_config", ({}, {"id": "sun"})) +@pytest.mark.parametrize("extra_config", [{}, {"id": "sun"}]) async def test_reload_unchanged_does_not_stop( hass: HomeAssistant, calls, extra_config ) -> None: @@ -961,7 +962,7 @@ async def test_reload_identical_automations_without_id( @pytest.mark.parametrize( "automation_config", - ( + [ { "trigger": {"platform": "event", "event_type": "test_event"}, "action": [{"service": "test.automation"}], @@ -1028,7 +1029,7 @@ async def test_reload_identical_automations_without_id( }, }, }, - ), + ], ) async def test_reload_unchanged_automation( hass: HomeAssistant, calls, automation_config @@ -1064,7 +1065,7 @@ async def test_reload_unchanged_automation( assert len(calls) == 2 -@pytest.mark.parametrize("extra_config", ({}, {"id": "sun"})) +@pytest.mark.parametrize("extra_config", [{}, {"id": "sun"}]) async def test_reload_automation_when_blueprint_changes( hass: HomeAssistant, calls, extra_config ) -> None: @@ -1101,14 +1102,17 @@ async def test_reload_automation_when_blueprint_changes( blueprint_config["action"] = [blueprint_config["action"]] blueprint_config["action"].append(blueprint_config["action"][-1]) - with patch( - "homeassistant.config.load_yaml_config_file", - autospec=True, - return_value=config, - ), patch( - "homeassistant.components.blueprint.models.yaml.load_yaml_dict", - autospec=True, - return_value=blueprint_config, + with ( + patch( + "homeassistant.config.load_yaml_config_file", + autospec=True, + return_value=config, + ), + patch( + "homeassistant.components.blueprint.models.yaml.load_yaml_dict", + autospec=True, + return_value=blueprint_config, + ), ): await hass.services.async_call( automation.DOMAIN, SERVICE_RELOAD, blocking=True @@ -1361,7 +1365,7 @@ async def test_automation_not_trigger_on_bootstrap(hass: HomeAssistant) -> None: @pytest.mark.parametrize( ("broken_config", "problem", "details"), - ( + [ ( {}, "could not be validated", @@ -1402,7 +1406,7 @@ async def test_automation_not_trigger_on_bootstrap(hass: HomeAssistant) -> None: "failed to setup actions", "Unknown entity registry entry abcdabcdabcdabcdabcdabcdabcdabcd.", ), - ), + ], ) async def test_automation_bad_config_validation( hass: HomeAssistant, @@ -1560,6 +1564,10 @@ async def test_extraction_functions_not_setup(hass: HomeAssistant) -> None: assert automation.devices_in_automation(hass, "automation.test") == [] assert automation.automations_with_entity(hass, "light.in_both") == [] assert automation.entities_in_automation(hass, "automation.test") == [] + assert automation.automations_with_floor(hass, "floor-in-both") == [] + assert automation.floors_in_automation(hass, "automation.test") == [] + assert automation.automations_with_label(hass, "label-in-both") == [] + assert automation.labels_in_automation(hass, "automation.test") == [] async def test_extraction_functions_unknown_automation(hass: HomeAssistant) -> None: @@ -1569,6 +1577,8 @@ async def test_extraction_functions_unknown_automation(hass: HomeAssistant) -> N assert automation.blueprint_in_automation(hass, "automation.unknown") is None assert automation.devices_in_automation(hass, "automation.unknown") == [] assert automation.entities_in_automation(hass, "automation.unknown") == [] + assert automation.floors_in_automation(hass, "automation.unknown") == [] + assert automation.labels_in_automation(hass, "automation.unknown") == [] async def test_extraction_functions_unavailable_automation(hass: HomeAssistant) -> None: @@ -1594,6 +1604,10 @@ async def test_extraction_functions_unavailable_automation(hass: HomeAssistant) assert automation.devices_in_automation(hass, entity_id) == [] assert automation.automations_with_entity(hass, "light.in_both") == [] assert automation.entities_in_automation(hass, entity_id) == [] + assert automation.automations_with_floor(hass, "floor-in-both") == [] + assert automation.floors_in_automation(hass, entity_id) == [] + assert automation.automations_with_label(hass, "label-in-both") == [] + assert automation.labels_in_automation(hass, entity_id) == [] async def test_extraction_functions( @@ -1693,6 +1707,14 @@ async def test_extraction_functions( "service": "test.test", "target": {"area_id": "area-in-both"}, }, + { + "service": "test.test", + "target": {"floor_id": "floor-in-both"}, + }, + { + "service": "test.test", + "target": {"label_id": "label-in-both"}, + }, ], }, { @@ -1812,6 +1834,22 @@ async def test_extraction_functions( "service": "test.test", "target": {"area_id": "area-in-last"}, }, + { + "service": "test.test", + "target": {"floor_id": "floor-in-both"}, + }, + { + "service": "test.test", + "target": {"floor_id": "floor-in-last"}, + }, + { + "service": "test.test", + "target": {"label_id": "label-in-both"}, + }, + { + "service": "test.test", + "target": {"label_id": "label-in-last"}, + }, ], }, ] @@ -1854,6 +1892,22 @@ async def test_extraction_functions( "area-in-both", "area-in-last", } + assert set(automation.automations_with_floor(hass, "floor-in-both")) == { + "automation.test1", + "automation.test3", + } + assert set(automation.floors_in_automation(hass, "automation.test3")) == { + "floor-in-both", + "floor-in-last", + } + assert set(automation.automations_with_label(hass, "label-in-both")) == { + "automation.test1", + "automation.test3", + } + assert set(automation.labels_in_automation(hass, "automation.test3")) == { + "label-in-both", + "label-in-last", + } assert automation.blueprint_in_automation(hass, "automation.test3") is None @@ -1862,6 +1916,7 @@ async def test_logbook_humanify_automation_triggered_event(hass: HomeAssistant) hass.config.components.add("recorder") await async_setup_component(hass, automation.DOMAIN, {}) await async_setup_component(hass, "logbook", {}) + await hass.async_block_till_done() event1, event2 = mock_humanify( hass, @@ -2139,7 +2194,7 @@ async def test_blueprint_automation(hass: HomeAssistant, calls) -> None: @pytest.mark.parametrize( ("blueprint_inputs", "problem", "details"), - ( + [ ( # No input {}, @@ -2165,7 +2220,7 @@ async def test_blueprint_automation(hass: HomeAssistant, calls) -> None: " data['action'][0]['service']" ), ), - ), + ], ) async def test_blueprint_automation_bad_config( hass: HomeAssistant, @@ -2348,21 +2403,21 @@ async def test_trigger_condition_explicit_id(hass: HomeAssistant, calls) -> None @pytest.mark.parametrize( ("automation_mode", "automation_runs"), - ( + [ (SCRIPT_MODE_PARALLEL, 2), (SCRIPT_MODE_QUEUED, 2), (SCRIPT_MODE_RESTART, 2), (SCRIPT_MODE_SINGLE, 1), - ), + ], ) @pytest.mark.parametrize( ("script_mode", "script_warning_msg"), - ( + [ (SCRIPT_MODE_PARALLEL, "script1: Maximum number of runs exceeded"), (SCRIPT_MODE_QUEUED, "script1: Disallowed recursion detected"), (SCRIPT_MODE_RESTART, "script1: Disallowed recursion detected"), (SCRIPT_MODE_SINGLE, "script1: Already running"), - ), + ], ) @pytest.mark.parametrize("wait_for_stop_scripts_after_shutdown", [True]) async def test_recursive_automation_starting_script( diff --git a/tests/components/automation/test_logbook.py b/tests/components/automation/test_logbook.py index 802977a63be..4aa494ad5b7 100644 --- a/tests/components/automation/test_logbook.py +++ b/tests/components/automation/test_logbook.py @@ -1,4 +1,5 @@ """Test automation logbook.""" + from homeassistant.components import automation from homeassistant.core import Context, HomeAssistant from homeassistant.setup import async_setup_component @@ -11,6 +12,7 @@ async def test_humanify_automation_trigger_event(hass: HomeAssistant) -> None: hass.config.components.add("recorder") assert await async_setup_component(hass, "automation", {}) assert await async_setup_component(hass, "logbook", {}) + await hass.async_block_till_done() context = Context() event1, event2 = mock_humanify( diff --git a/tests/components/automation/test_recorder.py b/tests/components/automation/test_recorder.py index 4aa84dbd602..c983cc949ad 100644 --- a/tests/components/automation/test_recorder.py +++ b/tests/components/automation/test_recorder.py @@ -1,4 +1,5 @@ """The tests for automation recorder.""" + from __future__ import annotations import pytest diff --git a/tests/components/automation/test_reproduce_state.py b/tests/components/automation/test_reproduce_state.py index 88ed26b564c..2b987572ae4 100644 --- a/tests/components/automation/test_reproduce_state.py +++ b/tests/components/automation/test_reproduce_state.py @@ -1,4 +1,5 @@ """Test reproduce state for Automation.""" + import pytest from homeassistant.core import HomeAssistant, State diff --git a/tests/components/awair/__init__.py b/tests/components/awair/__init__.py index e8b93e47fd7..f93866263a2 100644 --- a/tests/components/awair/__init__.py +++ b/tests/components/awair/__init__.py @@ -1,6 +1,5 @@ """Tests for the awair component.""" - from unittest.mock import patch from homeassistant.components.awair import DOMAIN diff --git a/tests/components/awair/test_config_flow.py b/tests/components/awair/test_config_flow.py index b9f466174af..c8b9ea262a8 100644 --- a/tests/components/awair/test_config_flow.py +++ b/tests/components/awair/test_config_flow.py @@ -1,4 +1,5 @@ """Define tests for the Awair config flow.""" + from typing import Any from unittest.mock import Mock, patch @@ -154,10 +155,13 @@ async def test_reauth(hass: HomeAssistant, user, cloud_devices) -> None: assert result["step_id"] == "reauth_confirm" assert result["errors"] == {CONF_ACCESS_TOKEN: "invalid_access_token"} - with patch( - "python_awair.AwairClient.query", - side_effect=[user, cloud_devices], - ), patch("homeassistant.components.awair.async_setup_entry", return_value=True): + with ( + patch( + "python_awair.AwairClient.query", + side_effect=[user, cloud_devices], + ), + patch("homeassistant.components.awair.async_setup_entry", return_value=True), + ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=CLOUD_CONFIG, @@ -198,12 +202,15 @@ async def test_reauth_error(hass: HomeAssistant) -> None: async def test_create_cloud_entry(hass: HomeAssistant, user, cloud_devices) -> None: """Test overall flow when using cloud api.""" - with patch( - "python_awair.AwairClient.query", - side_effect=[user, cloud_devices], - ), patch( - "homeassistant.components.awair.async_setup_entry", - return_value=True, + with ( + patch( + "python_awair.AwairClient.query", + side_effect=[user, cloud_devices], + ), + patch( + "homeassistant.components.awair.async_setup_entry", + return_value=True, + ), ): menu_step = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CLOUD_CONFIG @@ -228,9 +235,12 @@ async def test_create_cloud_entry(hass: HomeAssistant, user, cloud_devices) -> N async def test_create_local_entry(hass: HomeAssistant, local_devices) -> None: """Test overall flow when using local API.""" - with patch("python_awair.AwairClient.query", side_effect=[local_devices]), patch( - "homeassistant.components.awair.async_setup_entry", - return_value=True, + with ( + patch("python_awair.AwairClient.query", side_effect=[local_devices]), + patch( + "homeassistant.components.awair.async_setup_entry", + return_value=True, + ), ): menu_step = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=LOCAL_CONFIG @@ -286,9 +296,12 @@ async def test_create_local_entry_from_discovery( {}, ) - with patch("python_awair.AwairClient.query", side_effect=[local_devices]), patch( - "homeassistant.components.awair.async_setup_entry", - return_value=True, + with ( + patch("python_awair.AwairClient.query", side_effect=[local_devices]), + patch( + "homeassistant.components.awair.async_setup_entry", + return_value=True, + ), ): result = await hass.config_entries.flow.async_configure( form_step["flow_id"], @@ -336,9 +349,12 @@ async def test_create_local_entry_awair_error(hass: HomeAssistant) -> None: async def test_create_zeroconf_entry(hass: HomeAssistant, local_devices) -> None: """Test overall flow when using discovery.""" - with patch("python_awair.AwairClient.query", side_effect=[local_devices]), patch( - "homeassistant.components.awair.async_setup_entry", - return_value=True, + with ( + patch("python_awair.AwairClient.query", side_effect=[local_devices]), + patch( + "homeassistant.components.awair.async_setup_entry", + return_value=True, + ), ): confirm_step = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=ZEROCONF_DISCOVERY @@ -381,14 +397,16 @@ async def test_zeroconf_discovery_update_configuration( ) config_entry.add_to_hass(hass) - with patch( - "homeassistant.components.awair.async_setup_entry", - return_value=True, - ) as mock_setup_entry, patch( - "python_awair.AwairClient.query", side_effect=[local_devices] - ), patch( - "homeassistant.components.awair.async_setup_entry", - return_value=True, + with ( + patch( + "homeassistant.components.awair.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + patch("python_awair.AwairClient.query", side_effect=[local_devices]), + patch( + "homeassistant.components.awair.async_setup_entry", + return_value=True, + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -407,14 +425,16 @@ async def test_zeroconf_during_onboarding( hass: HomeAssistant, local_devices: Any ) -> None: """Test the zeroconf creates an entry during onboarding.""" - with patch( - "homeassistant.components.awair.async_setup_entry", - return_value=True, - ) as mock_setup_entry, patch( - "python_awair.AwairClient.query", side_effect=[local_devices] - ), patch( - "homeassistant.components.onboarding.async_is_onboarded", - return_value=False, + with ( + patch( + "homeassistant.components.awair.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + patch("python_awair.AwairClient.query", side_effect=[local_devices]), + patch( + "homeassistant.components.onboarding.async_is_onboarded", + return_value=False, + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=ZEROCONF_DISCOVERY diff --git a/tests/components/awair/test_init.py b/tests/components/awair/test_init.py index f3a4bb636e6..954dd51a5e5 100644 --- a/tests/components/awair/test_init.py +++ b/tests/components/awair/test_init.py @@ -1,4 +1,5 @@ """Test Awair init.""" + from unittest.mock import patch from homeassistant.core import HomeAssistant diff --git a/tests/components/awair/test_sensor.py b/tests/components/awair/test_sensor.py index 849ac59a22f..8af1fdd9c7c 100644 --- a/tests/components/awair/test_sensor.py +++ b/tests/components/awair/test_sensor.py @@ -1,4 +1,5 @@ """Tests for the Awair sensor platform.""" + from unittest.mock import patch from homeassistant.components.awair.const import ( diff --git a/tests/components/aws/test_init.py b/tests/components/aws/test_init.py index 2a69f02e35d..9589ad6c037 100644 --- a/tests/components/aws/test_init.py +++ b/tests/components/aws/test_init.py @@ -1,4 +1,5 @@ """Tests for the aws component config and setup.""" + import json from unittest.mock import AsyncMock, MagicMock, call, patch as async_patch @@ -160,10 +161,13 @@ async def test_access_key_credential(hass: HomeAssistant) -> None: async def test_notify_credential(hass: HomeAssistant) -> None: """Test notify service can use access key directly.""" mock_session = MockAioSession() - with async_patch( - "homeassistant.components.aws.AioSession", return_value=mock_session - ), async_patch( - "homeassistant.components.aws.notify.AioSession", return_value=mock_session + with ( + async_patch( + "homeassistant.components.aws.AioSession", return_value=mock_session + ), + async_patch( + "homeassistant.components.aws.notify.AioSession", return_value=mock_session + ), ): await async_setup_component( hass, @@ -194,10 +198,13 @@ async def test_notify_credential(hass: HomeAssistant) -> None: async def test_notify_credential_profile(hass: HomeAssistant) -> None: """Test notify service can use profile directly.""" mock_session = MockAioSession() - with async_patch( - "homeassistant.components.aws.AioSession", return_value=mock_session - ), async_patch( - "homeassistant.components.aws.notify.AioSession", return_value=mock_session + with ( + async_patch( + "homeassistant.components.aws.AioSession", return_value=mock_session + ), + async_patch( + "homeassistant.components.aws.notify.AioSession", return_value=mock_session + ), ): await async_setup_component( hass, diff --git a/tests/components/axis/conftest.py b/tests/components/axis/conftest.py index 8de16ee7990..b50a28df49f 100644 --- a/tests/components/axis/conftest.py +++ b/tests/components/axis/conftest.py @@ -1,15 +1,19 @@ """Axis conftest.""" + from __future__ import annotations -from collections.abc import Generator +from collections.abc import Callable, Generator from copy import deepcopy +from types import MappingProxyType +from typing import Any from unittest.mock import AsyncMock, patch from axis.rtsp import Signal, State import pytest import respx -from homeassistant.components.axis.const import CONF_EVENTS, DOMAIN as AXIS_DOMAIN +from homeassistant.components.axis.const import DOMAIN as AXIS_DOMAIN +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_HOST, CONF_MODEL, @@ -18,9 +22,12 @@ from homeassistant.const import ( CONF_PORT, CONF_USERNAME, ) +from homeassistant.core import HomeAssistant from .const import ( API_DISCOVERY_RESPONSE, + APP_AOA_RESPONSE, + APP_VMD4_RESPONSE, APPLICATIONS_LIST_RESPONSE, BASIC_DEVICE_INFO_RESPONSE, BRAND_RESPONSE, @@ -36,11 +43,9 @@ from .const import ( PTZ_RESPONSE, STREAM_PROFILES_RESPONSE, VIEW_AREAS_RESPONSE, - VMD4_RESPONSE, ) from tests.common import MockConfigEntry -from tests.components.light.conftest import mock_light_profiles # noqa: F401 @pytest.fixture @@ -56,28 +61,33 @@ def mock_setup_entry() -> Generator[AsyncMock, None, None]: @pytest.fixture(name="config_entry") -def config_entry_fixture(hass, config, options, config_entry_version): +def config_entry_fixture( + hass: HomeAssistant, + config_entry_data: MappingProxyType[str, Any], + config_entry_options: MappingProxyType[str, Any], + config_entry_version: int, +) -> ConfigEntry: """Define a config entry fixture.""" - entry = MockConfigEntry( + config_entry = MockConfigEntry( domain=AXIS_DOMAIN, entry_id="676abe5b73621446e6550a2e86ffe3dd", unique_id=FORMATTED_MAC, - data=config, - options=options, + data=config_entry_data, + options=config_entry_options, version=config_entry_version, ) - entry.add_to_hass(hass) - return entry + config_entry.add_to_hass(hass) + return config_entry @pytest.fixture(name="config_entry_version") -def config_entry_version_fixture(request): +def config_entry_version_fixture() -> int: """Define a config entry version fixture.""" return 3 -@pytest.fixture(name="config") -def config_fixture(): +@pytest.fixture(name="config_entry_data") +def config_entry_data_fixture() -> MappingProxyType[str, Any]: """Define a config entry data fixture.""" return { CONF_HOST: DEFAULT_HOST, @@ -89,10 +99,10 @@ def config_fixture(): } -@pytest.fixture(name="options") -def options_fixture(request): +@pytest.fixture(name="config_entry_options") +def config_entry_options_fixture() -> MappingProxyType[str, Any]: """Define a config entry options fixture.""" - return {CONF_EVENTS: True} + return {} # Axis API fixtures @@ -100,105 +110,110 @@ def options_fixture(request): @pytest.fixture(name="mock_vapix_requests") def default_request_fixture( - respx_mock, port_management_payload, param_properties_payload, param_ports_payload -): + respx_mock: respx, + port_management_payload: dict[str, Any], + param_properties_payload: dict[str, Any], + param_ports_payload: dict[str, Any], +) -> Callable[[str], None]: """Mock default Vapix requests responses.""" - def __mock_default_requests(host): - path = f"http://{host}:80" + def __mock_default_requests(host: str) -> None: + respx_mock(base_url=f"http://{host}:80") if host != DEFAULT_HOST: - respx.post(f"{path}/axis-cgi/apidiscovery.cgi").respond( + respx.post("/axis-cgi/apidiscovery.cgi").respond( json=API_DISCOVERY_RESPONSE, ) - respx.post(f"{path}/axis-cgi/basicdeviceinfo.cgi").respond( + respx.post("/axis-cgi/basicdeviceinfo.cgi").respond( json=BASIC_DEVICE_INFO_RESPONSE, ) - respx.post(f"{path}/axis-cgi/io/portmanagement.cgi").respond( + respx.post("/axis-cgi/io/portmanagement.cgi").respond( json=port_management_payload, ) - respx.post(f"{path}/axis-cgi/mqtt/client.cgi").respond( + respx.post("/axis-cgi/mqtt/client.cgi").respond( json=MQTT_CLIENT_RESPONSE, ) - respx.post(f"{path}/axis-cgi/streamprofile.cgi").respond( + respx.post("/axis-cgi/streamprofile.cgi").respond( json=STREAM_PROFILES_RESPONSE, ) - respx.post(f"{path}/axis-cgi/viewarea/info.cgi").respond( - json=VIEW_AREAS_RESPONSE - ) + respx.post("/axis-cgi/viewarea/info.cgi").respond(json=VIEW_AREAS_RESPONSE) respx.post( - f"{path}/axis-cgi/param.cgi", + "/axis-cgi/param.cgi", data={"action": "list", "group": "root.Brand"}, ).respond( text=BRAND_RESPONSE, headers={"Content-Type": "text/plain"}, ) respx.post( - f"{path}/axis-cgi/param.cgi", + "/axis-cgi/param.cgi", data={"action": "list", "group": "root.Image"}, ).respond( text=IMAGE_RESPONSE, headers={"Content-Type": "text/plain"}, ) respx.post( - f"{path}/axis-cgi/param.cgi", + "/axis-cgi/param.cgi", data={"action": "list", "group": "root.Input"}, ).respond( text=PORTS_RESPONSE, headers={"Content-Type": "text/plain"}, ) respx.post( - f"{path}/axis-cgi/param.cgi", + "/axis-cgi/param.cgi", data={"action": "list", "group": "root.IOPort"}, ).respond( text=param_ports_payload, headers={"Content-Type": "text/plain"}, ) respx.post( - f"{path}/axis-cgi/param.cgi", + "/axis-cgi/param.cgi", data={"action": "list", "group": "root.Output"}, ).respond( text=PORTS_RESPONSE, headers={"Content-Type": "text/plain"}, ) respx.post( - f"{path}/axis-cgi/param.cgi", + "/axis-cgi/param.cgi", data={"action": "list", "group": "root.Properties"}, ).respond( text=param_properties_payload, headers={"Content-Type": "text/plain"}, ) respx.post( - f"{path}/axis-cgi/param.cgi", + "/axis-cgi/param.cgi", data={"action": "list", "group": "root.PTZ"}, ).respond( text=PTZ_RESPONSE, headers={"Content-Type": "text/plain"}, ) respx.post( - f"{path}/axis-cgi/param.cgi", + "/axis-cgi/param.cgi", data={"action": "list", "group": "root.StreamProfile"}, ).respond( text=STREAM_PROFILES_RESPONSE, headers={"Content-Type": "text/plain"}, ) - respx.post(f"{path}/axis-cgi/applications/list.cgi").respond( + respx.post("/axis-cgi/applications/list.cgi").respond( text=APPLICATIONS_LIST_RESPONSE, headers={"Content-Type": "text/xml"}, ) - respx.post(f"{path}/local/vmd/control.cgi").respond(json=VMD4_RESPONSE) + respx.post("/local/fenceguard/control.cgi").respond(json=APP_VMD4_RESPONSE) + respx.post("/local/loiteringguard/control.cgi").respond(json=APP_VMD4_RESPONSE) + respx.post("/local/motionguard/control.cgi").respond(json=APP_VMD4_RESPONSE) + respx.post("/local/vmd/control.cgi").respond(json=APP_VMD4_RESPONSE) + respx.post("/local/objectanalytics/control.cgi").respond(json=APP_AOA_RESPONSE) return __mock_default_requests @pytest.fixture -def api_discovery_items(): +def api_discovery_items() -> dict[str, Any]: """Additional Apidiscovery items.""" return {} @pytest.fixture(autouse=True) -def api_discovery_fixture(api_discovery_items): +def api_discovery_fixture(api_discovery_items: dict[str, Any]) -> None: """Apidiscovery mock response.""" data = deepcopy(API_DISCOVERY_RESPONSE) if api_discovery_items: @@ -207,34 +222,36 @@ def api_discovery_fixture(api_discovery_items): @pytest.fixture(name="port_management_payload") -def io_port_management_data_fixture(): +def io_port_management_data_fixture() -> dict[str, Any]: """Property parameter data.""" return PORT_MANAGEMENT_RESPONSE @pytest.fixture(name="param_properties_payload") -def param_properties_data_fixture(): +def param_properties_data_fixture() -> dict[str, Any]: """Property parameter data.""" return PROPERTIES_RESPONSE @pytest.fixture(name="param_ports_payload") -def param_ports_data_fixture(): +def param_ports_data_fixture() -> dict[str, Any]: """Property parameter data.""" return PORTS_RESPONSE @pytest.fixture(name="setup_default_vapix_requests") -def default_vapix_requests_fixture(mock_vapix_requests): +def default_vapix_requests_fixture(mock_vapix_requests: Callable[[str], None]) -> None: """Mock default Vapix requests responses.""" mock_vapix_requests(DEFAULT_HOST) @pytest.fixture(name="prepare_config_entry") -async def prep_config_entry_fixture(hass, config_entry, setup_default_vapix_requests): +async def prep_config_entry_fixture( + hass: HomeAssistant, config_entry: ConfigEntry, setup_default_vapix_requests: None +) -> Callable[[], ConfigEntry]: """Fixture factory to set up Axis network device.""" - async def __mock_setup_config_entry(): + async def __mock_setup_config_entry() -> ConfigEntry: assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() return config_entry @@ -243,7 +260,9 @@ async def prep_config_entry_fixture(hass, config_entry, setup_default_vapix_requ @pytest.fixture(name="setup_config_entry") -async def setup_config_entry_fixture(hass, config_entry, setup_default_vapix_requests): +async def setup_config_entry_fixture( + hass: HomeAssistant, config_entry: ConfigEntry, setup_default_vapix_requests: None +) -> ConfigEntry: """Define a fixture to set up Axis network device.""" assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -254,24 +273,24 @@ async def setup_config_entry_fixture(hass, config_entry, setup_default_vapix_req @pytest.fixture(autouse=True) -def mock_axis_rtspclient(): +def mock_axis_rtspclient() -> Generator[Callable[[dict | None, str], None], None, None]: """No real RTSP communication allowed.""" with patch("axis.stream_manager.RTSPClient") as rtsp_client_mock: rtsp_client_mock.return_value.session.state = State.STOPPED - async def start_stream(): + async def start_stream() -> None: """Set state to playing when calling RTSPClient.start.""" rtsp_client_mock.return_value.session.state = State.PLAYING rtsp_client_mock.return_value.start = start_stream - def stop_stream(): + def stop_stream() -> None: """Set state to stopped when calling RTSPClient.stop.""" rtsp_client_mock.return_value.session.state = State.STOPPED rtsp_client_mock.return_value.stop = stop_stream - def make_rtsp_call(data: dict | None = None, state: str = ""): + def make_rtsp_call(data: dict | None = None, state: str = "") -> None: """Generate a RTSP call.""" axis_streammanager_session_callback = rtsp_client_mock.call_args[0][4] @@ -287,7 +306,9 @@ def mock_axis_rtspclient(): @pytest.fixture(autouse=True) -def mock_rtsp_event(mock_axis_rtspclient): +def mock_rtsp_event( + mock_axis_rtspclient: Callable[[dict | None, str], None], +) -> Callable[[str, str, str, str, str, str], None]: """Fixture to allow mocking received RTSP events.""" def send_event( @@ -338,7 +359,9 @@ def mock_rtsp_event(mock_axis_rtspclient): @pytest.fixture(autouse=True) -def mock_rtsp_signal_state(mock_axis_rtspclient): +def mock_rtsp_signal_state( + mock_axis_rtspclient: Callable[[dict | None, str], None], +) -> Callable[[bool], None]: """Fixture to allow mocking RTSP state signalling.""" def send_signal(connected: bool) -> None: diff --git a/tests/components/axis/const.py b/tests/components/axis/const.py index df1d0aa4529..16b9d17f99e 100644 --- a/tests/components/axis/const.py +++ b/tests/components/axis/const.py @@ -1,6 +1,6 @@ """Constants for Axis integration tests.""" -from axis.vapix.models.api import CONTEXT +from axis.models.api import CONTEXT MAC = "00408C123456" FORMATTED_MAC = "00:40:8c:12:34:56" @@ -35,7 +35,11 @@ API_DISCOVERY_PORT_MANAGEMENT = { } APPLICATIONS_LIST_RESPONSE = """ + + + + """ BASIC_DEVICE_INFO_RESPONSE = { @@ -70,6 +74,7 @@ MQTT_CLIENT_RESPONSE = { "status": {"state": "active", "connectionStatus": "Connected"}, "config": { "server": {"protocol": "tcp", "host": "192.168.0.90", "port": 1883}, + "deviceTopicPrefix": f"axis/{MAC}", }, }, } @@ -95,7 +100,7 @@ PORT_MANAGEMENT_RESPONSE = { }, } -VMD4_RESPONSE = { +APP_VMD4_RESPONSE = { "apiVersion": "1.4", "method": "getConfiguration", "context": CONTEXT, @@ -108,6 +113,46 @@ VMD4_RESPONSE = { }, } +APP_AOA_RESPONSE = { + "apiVersion": "1.0", + "context": "Axis library", + "data": { + "devices": [{"id": 1, "rotation": 180, "type": "camera"}], + "metadataOverlay": [], + "perspectives": [], + "scenarios": [ + { + "devices": [{"id": 1}], + "filters": [ + {"distance": 5, "type": "distanceSwayingObject"}, + {"time": 1, "type": "timeShortLivedLimit"}, + {"height": 3, "type": "sizePercentage", "width": 3}, + ], + "id": 1, + "name": "Scenario 1", + "objectClassifications": [], + "perspectives": [], + "presets": [], + "triggers": [ + { + "type": "includeArea", + "vertices": [ + [-0.97, -0.97], + [-0.97, 0.97], + [0.97, 0.97], + [0.97, -0.97], + ], + } + ], + "type": "motion", + }, + ], + "status": {}, + }, + "method": "getConfiguration", +} + + BRAND_RESPONSE = """root.Brand.Brand=AXIS root.Brand.ProdFullName=AXIS M1065-LW Network Camera root.Brand.ProdNbr=M1065-LW diff --git a/tests/components/axis/snapshots/test_diagnostics.ambr b/tests/components/axis/snapshots/test_diagnostics.ambr index b5647a08543..8ea316d00cf 100644 --- a/tests/components/axis/snapshots/test_diagnostics.ambr +++ b/tests/components/axis/snapshots/test_diagnostics.ambr @@ -19,7 +19,7 @@ }), ]), 'basic_device_info': dict({ - '__type': "", + '__type': "", 'repr': "DeviceInformation(id='0', architecture='str', brand='str', build_date='str', firmware_version='9.80.1', hardware_id='str', product_full_name='str', product_number='M1065-LW', product_short_name='str', product_type='Network Camera', product_variant='str', serial_number='00408C123456', soc='str', soc_serial_number='str', web_url='str')", }), 'camera_sources': dict({ @@ -41,7 +41,6 @@ 'entry_id': '676abe5b73621446e6550a2e86ffe3dd', 'minor_version': 1, 'options': dict({ - 'events': True, }), 'pref_disable_new_entities': False, 'pref_disable_polling': False, diff --git a/tests/components/axis/test_binary_sensor.py b/tests/components/axis/test_binary_sensor.py index 45fd8fe2f6c..dd7674d7d3f 100644 --- a/tests/components/axis/test_binary_sensor.py +++ b/tests/components/axis/test_binary_sensor.py @@ -1,69 +1,237 @@ """Axis binary sensor platform tests.""" -from homeassistant.components.axis.const import DOMAIN as AXIS_DOMAIN + +from collections.abc import Callable + +import pytest + from homeassistant.components.binary_sensor import ( DOMAIN as BINARY_SENSOR_DOMAIN, BinarySensorDeviceClass, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant -from homeassistant.setup import async_setup_component from .const import NAME -async def test_platform_manually_configured(hass: HomeAssistant) -> None: - """Test that nothing happens when platform is manually configured.""" - assert ( - await async_setup_component( - hass, - BINARY_SENSOR_DOMAIN, - {BINARY_SENSOR_DOMAIN: {"platform": AXIS_DOMAIN}}, - ) - is True - ) - - assert AXIS_DOMAIN not in hass.data - - -async def test_no_binary_sensors(hass: HomeAssistant, setup_config_entry) -> None: - """Test that no sensors in Axis results in no sensor entities.""" - assert not hass.states.async_entity_ids(BINARY_SENSOR_DOMAIN) - - +@pytest.mark.parametrize( + ("event", "entity"), + [ + ( + { + "topic": "tns1:VideoSource/tnsaxis:DayNightVision", + "source_name": "VideoSourceConfigurationToken", + "source_idx": "1", + "data_type": "DayNight", + "data_value": "1", + }, + { + "id": f"{BINARY_SENSOR_DOMAIN}.{NAME}_daynight_1", + "state": STATE_ON, + "name": f"{NAME} DayNight 1", + "device_class": BinarySensorDeviceClass.LIGHT, + }, + ), + ( + { + "topic": "tns1:AudioSource/tnsaxis:TriggerLevel", + "source_name": "channel", + "source_idx": "1", + "data_type": "Sound", + "data_value": "0", + }, + { + "id": f"{BINARY_SENSOR_DOMAIN}.{NAME}_sound_1", + "state": STATE_OFF, + "name": f"{NAME} Sound 1", + "device_class": BinarySensorDeviceClass.SOUND, + }, + ), + ( + { + "topic": "tns1:Device/tnsaxis:IO/Port", + "data_type": "state", + "data_value": "0", + "operation": "Initialized", + "source_name": "port", + "source_idx": "0", + }, + { + "id": f"{BINARY_SENSOR_DOMAIN}.{NAME}_pir_sensor", + "state": STATE_OFF, + "name": f"{NAME} PIR sensor", + "device_class": BinarySensorDeviceClass.CONNECTIVITY, + }, + ), + ( + { + "topic": "tns1:Device/tnsaxis:Sensor/PIR", + "data_type": "state", + "data_value": "0", + "source_name": "sensor", + "source_idx": "0", + }, + { + "id": f"{BINARY_SENSOR_DOMAIN}.{NAME}_pir_0", + "state": STATE_OFF, + "name": f"{NAME} PIR 0", + "device_class": BinarySensorDeviceClass.MOTION, + }, + ), + ( + { + "topic": "tnsaxis:CameraApplicationPlatform/FenceGuard/Camera1Profile1", + "data_type": "active", + "data_value": "1", + }, + { + "id": f"{BINARY_SENSOR_DOMAIN}.{NAME}_fence_guard_profile_1", + "state": STATE_ON, + "name": f"{NAME} Fence Guard Profile 1", + "device_class": BinarySensorDeviceClass.MOTION, + }, + ), + ( + { + "topic": "tnsaxis:CameraApplicationPlatform/MotionGuard/Camera1Profile1", + "data_type": "active", + "data_value": "1", + }, + { + "id": f"{BINARY_SENSOR_DOMAIN}.{NAME}_motion_guard_profile_1", + "state": STATE_ON, + "name": f"{NAME} Motion Guard Profile 1", + "device_class": BinarySensorDeviceClass.MOTION, + }, + ), + ( + { + "topic": "tnsaxis:CameraApplicationPlatform/LoiteringGuard/Camera1Profile1", + "data_type": "active", + "data_value": "1", + }, + { + "id": f"{BINARY_SENSOR_DOMAIN}.{NAME}_loitering_guard_profile_1", + "state": STATE_ON, + "name": f"{NAME} Loitering Guard Profile 1", + "device_class": BinarySensorDeviceClass.MOTION, + }, + ), + ( + { + "topic": "tnsaxis:CameraApplicationPlatform/VMD/Camera1Profile1", + "data_type": "active", + "data_value": "1", + }, + { + "id": f"{BINARY_SENSOR_DOMAIN}.{NAME}_vmd4_profile_1", + "state": STATE_ON, + "name": f"{NAME} VMD4 Profile 1", + "device_class": BinarySensorDeviceClass.MOTION, + }, + ), + ( + { + "topic": "tnsaxis:CameraApplicationPlatform/ObjectAnalytics/Device1Scenario1", + "data_type": "active", + "data_value": "1", + }, + { + "id": f"{BINARY_SENSOR_DOMAIN}.{NAME}_object_analytics_scenario_1", + "state": STATE_ON, + "name": f"{NAME} Object Analytics Scenario 1", + "device_class": BinarySensorDeviceClass.MOTION, + }, + ), + # Events with names generated from event ID and topic + ( + { + "topic": "tnsaxis:CameraApplicationPlatform/VMD/Camera1Profile9", + "data_type": "active", + "data_value": "1", + }, + { + "id": f"{BINARY_SENSOR_DOMAIN}.{NAME}_vmd4_camera1profile9", + "state": STATE_ON, + "name": f"{NAME} VMD4 Camera1Profile9", + "device_class": BinarySensorDeviceClass.MOTION, + }, + ), + ( + { + "topic": "tnsaxis:CameraApplicationPlatform/ObjectAnalytics/Device1Scenario8", + "data_type": "active", + "data_value": "1", + }, + { + "id": f"{BINARY_SENSOR_DOMAIN}.{NAME}_object_analytics_device1scenario8", + "state": STATE_ON, + "name": f"{NAME} Object Analytics Device1Scenario8", + "device_class": BinarySensorDeviceClass.MOTION, + }, + ), + ], +) async def test_binary_sensors( - hass: HomeAssistant, setup_config_entry, mock_rtsp_event + hass: HomeAssistant, + setup_config_entry: ConfigEntry, + mock_rtsp_event: Callable[[str, str, str, str, str, str], None], + event: dict[str, str], + entity: dict[str, str], ) -> None: """Test that sensors are loaded properly.""" - mock_rtsp_event( - topic="tns1:Device/tnsaxis:Sensor/PIR", - data_type="state", - data_value="0", - source_name="sensor", - source_idx="0", - ) - mock_rtsp_event( - topic="tnsaxis:CameraApplicationPlatform/VMD/Camera1Profile1", - data_type="active", - data_value="1", - ) - # Unsupported event - mock_rtsp_event( - topic="tns1:PTZController/tnsaxis:PTZPresets/Channel_1", - data_type="on_preset", - data_value="1", - source_name="PresetToken", - source_idx="0", - ) + mock_rtsp_event(**event) await hass.async_block_till_done() - assert len(hass.states.async_entity_ids(BINARY_SENSOR_DOMAIN)) == 2 + assert len(hass.states.async_entity_ids(BINARY_SENSOR_DOMAIN)) == 1 - pir = hass.states.get(f"{BINARY_SENSOR_DOMAIN}.{NAME}_pir_0") - assert pir.state == STATE_OFF - assert pir.name == f"{NAME} PIR 0" - assert pir.attributes["device_class"] == BinarySensorDeviceClass.MOTION + state = hass.states.get(entity["id"]) + assert state.state == entity["state"] + assert state.name == entity["name"] + assert state.attributes["device_class"] == entity["device_class"] - vmd4 = hass.states.get(f"{BINARY_SENSOR_DOMAIN}.{NAME}_vmd4_profile_1") - assert vmd4.state == STATE_ON - assert vmd4.name == f"{NAME} VMD4 Profile 1" - assert vmd4.attributes["device_class"] == BinarySensorDeviceClass.MOTION + +@pytest.mark.parametrize( + ("event"), + [ + # Event with unsupported topic + { + "topic": "tns1:PTZController/tnsaxis:PTZPresets/Channel_1", + "data_type": "on_preset", + "data_value": "1", + "source_name": "PresetToken", + "source_idx": "0", + }, + # Event with unsupported source_idx + { + "topic": "tns1:Device/tnsaxis:IO/Port", + "data_type": "state", + "data_value": "0", + "operation": "Initialized", + "source_name": "port", + "source_idx": "-1", + }, + # Event with unsupported ID in topic 'ANY' + { + "topic": "tnsaxis:CameraApplicationPlatform/VMD/Camera1ProfileANY", + "data_type": "active", + "data_value": "1", + }, + { + "topic": "tnsaxis:CameraApplicationPlatform/ObjectAnalytics/Device1ScenarioANY", + "data_type": "active", + "data_value": "1", + }, + ], +) +async def test_unsupported_events( + hass: HomeAssistant, + setup_config_entry: ConfigEntry, + mock_rtsp_event: Callable[[str, str, str, str, str, str], None], + event: dict[str, str], +) -> None: + """Validate nothing breaks with unsupported events.""" + mock_rtsp_event(**event) + await hass.async_block_till_done() + assert len(hass.states.async_entity_ids(BINARY_SENSOR_DOMAIN)) == 0 diff --git a/tests/components/axis/test_camera.py b/tests/components/axis/test_camera.py index 440bb17b08e..e184f2014b3 100644 --- a/tests/components/axis/test_camera.py +++ b/tests/components/axis/test_camera.py @@ -1,5 +1,7 @@ """Axis camera platform tests.""" +from collections.abc import Callable + import pytest from homeassistant.components import camera @@ -8,6 +10,7 @@ from homeassistant.components.axis.const import ( DOMAIN as AXIS_DOMAIN, ) from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN +from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_IDLE from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component @@ -27,7 +30,7 @@ async def test_platform_manually_configured(hass: HomeAssistant) -> None: assert AXIS_DOMAIN not in hass.data -async def test_camera(hass: HomeAssistant, setup_config_entry) -> None: +async def test_camera(hass: HomeAssistant, setup_config_entry: ConfigEntry) -> None: """Test that Axis camera platform is loaded properly.""" assert len(hass.states.async_entity_ids(CAMERA_DOMAIN)) == 1 @@ -46,9 +49,9 @@ async def test_camera(hass: HomeAssistant, setup_config_entry) -> None: ) -@pytest.mark.parametrize("options", [{CONF_STREAM_PROFILE: "profile_1"}]) +@pytest.mark.parametrize("config_entry_options", [{CONF_STREAM_PROFILE: "profile_1"}]) async def test_camera_with_stream_profile( - hass: HomeAssistant, setup_config_entry + hass: HomeAssistant, setup_config_entry: ConfigEntry ) -> None: """Test that Axis camera entity is using the correct path with stream profike.""" assert len(hass.states.async_entity_ids(CAMERA_DOMAIN)) == 1 @@ -83,7 +86,9 @@ root.Properties.System.SerialNumber={MAC} @pytest.mark.parametrize("param_properties_payload", [property_data]) -async def test_camera_disabled(hass: HomeAssistant, prepare_config_entry) -> None: +async def test_camera_disabled( + hass: HomeAssistant, prepare_config_entry: Callable[[], ConfigEntry] +) -> None: """Test that Axis camera platform is loaded properly but does not create camera entity.""" await prepare_config_entry() assert len(hass.states.async_entity_ids(CAMERA_DOMAIN)) == 0 diff --git a/tests/components/axis/test_config_flow.py b/tests/components/axis/test_config_flow.py index a37b0ccd12d..a6a0235b118 100644 --- a/tests/components/axis/test_config_flow.py +++ b/tests/components/axis/test_config_flow.py @@ -1,4 +1,5 @@ """Test Axis config flow.""" + from ipaddress import ip_address from unittest.mock import patch @@ -7,7 +8,6 @@ import pytest from homeassistant.components import dhcp, ssdp, zeroconf from homeassistant.components.axis import config_flow from homeassistant.components.axis.const import ( - CONF_EVENTS, CONF_STREAM_PROFILE, CONF_VIDEO_SOURCE, DEFAULT_STREAM_PROFILE, @@ -18,6 +18,7 @@ from homeassistant.config_entries import ( SOURCE_DHCP, SOURCE_IGNORE, SOURCE_REAUTH, + SOURCE_RECONFIGURE, SOURCE_SSDP, SOURCE_USER, SOURCE_ZEROCONF, @@ -28,6 +29,7 @@ from homeassistant.const import ( CONF_NAME, CONF_PASSWORD, CONF_PORT, + CONF_PROTOCOL, CONF_USERNAME, ) from homeassistant.core import HomeAssistant @@ -65,6 +67,7 @@ async def test_flow_manual_configuration( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={ + CONF_PROTOCOL: "http", CONF_HOST: "1.2.3.4", CONF_USERNAME: "user", CONF_PASSWORD: "pass", @@ -75,6 +78,7 @@ async def test_flow_manual_configuration( assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == f"M1065-LW - {MAC}" assert result["data"] == { + CONF_PROTOCOL: "http", CONF_HOST: "1.2.3.4", CONF_USERNAME: "user", CONF_PASSWORD: "pass", @@ -101,6 +105,7 @@ async def test_manual_configuration_update_configuration( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={ + CONF_PROTOCOL: "http", CONF_HOST: "2.3.4.5", CONF_USERNAME: "user", CONF_PASSWORD: "pass", @@ -124,12 +129,13 @@ async def test_flow_fails_faulty_credentials(hass: HomeAssistant) -> None: assert result["step_id"] == "user" with patch( - "homeassistant.components.axis.config_flow.get_axis_device", + "homeassistant.components.axis.config_flow.get_axis_api", side_effect=config_flow.AuthenticationRequired, ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={ + CONF_PROTOCOL: "http", CONF_HOST: "1.2.3.4", CONF_USERNAME: "user", CONF_PASSWORD: "pass", @@ -150,12 +156,13 @@ async def test_flow_fails_cannot_connect(hass: HomeAssistant) -> None: assert result["step_id"] == "user" with patch( - "homeassistant.components.axis.config_flow.get_axis_device", + "homeassistant.components.axis.config_flow.get_axis_api", side_effect=config_flow.CannotConnect, ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={ + CONF_PROTOCOL: "http", CONF_HOST: "1.2.3.4", CONF_USERNAME: "user", CONF_PASSWORD: "pass", @@ -191,6 +198,7 @@ async def test_flow_create_entry_multiple_existing_entries_of_same_model( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={ + CONF_PROTOCOL: "http", CONF_HOST: "1.2.3.4", CONF_USERNAME: "user", CONF_PASSWORD: "pass", @@ -201,6 +209,7 @@ async def test_flow_create_entry_multiple_existing_entries_of_same_model( assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == f"M1065-LW - {MAC}" assert result["data"] == { + CONF_PROTOCOL: "http", CONF_HOST: "1.2.3.4", CONF_USERNAME: "user", CONF_PASSWORD: "pass", @@ -233,21 +242,62 @@ async def test_reauth_flow_update_configuration( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={ + CONF_PROTOCOL: "https", CONF_HOST: "2.3.4.5", CONF_USERNAME: "user2", CONF_PASSWORD: "pass2", - CONF_PORT: 80, + CONF_PORT: 443, }, ) await hass.async_block_till_done() assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" + assert mock_config_entry.data[CONF_PROTOCOL] == "https" assert mock_config_entry.data[CONF_HOST] == "2.3.4.5" + assert mock_config_entry.data[CONF_PORT] == 443 assert mock_config_entry.data[CONF_USERNAME] == "user2" assert mock_config_entry.data[CONF_PASSWORD] == "pass2" +async def test_reconfiguration_flow_update_configuration( + hass: HomeAssistant, mock_config_entry, mock_vapix_requests +) -> None: + """Test that config flow reconfiguration updates configured device.""" + assert mock_config_entry.data[CONF_HOST] == "1.2.3.4" + assert mock_config_entry.data[CONF_USERNAME] == "root" + assert mock_config_entry.data[CONF_PASSWORD] == "pass" + + result = await hass.config_entries.flow.async_init( + AXIS_DOMAIN, + context={ + "source": SOURCE_RECONFIGURE, + "entry_id": mock_config_entry.entry_id, + }, + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + + mock_vapix_requests("2.3.4.5") + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: "2.3.4.5", + CONF_USERNAME: "user", + }, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + assert mock_config_entry.data[CONF_PROTOCOL] == "http" + assert mock_config_entry.data[CONF_HOST] == "2.3.4.5" + assert mock_config_entry.data[CONF_PORT] == 80 + assert mock_config_entry.data[CONF_USERNAME] == "user" + assert mock_config_entry.data[CONF_PASSWORD] == "pass" + + @pytest.mark.parametrize( ("source", "discovery_info"), [ @@ -334,6 +384,7 @@ async def test_discovery_flow( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={ + CONF_PROTOCOL: "http", CONF_HOST: "1.2.3.4", CONF_USERNAME: "user", CONF_PASSWORD: "pass", @@ -344,6 +395,7 @@ async def test_discovery_flow( assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == f"M1065-LW - {MAC}" assert result["data"] == { + CONF_PROTOCOL: "http", CONF_HOST: "1.2.3.4", CONF_USERNAME: "user", CONF_PASSWORD: "pass", @@ -430,7 +482,7 @@ async def test_discovered_device_already_configured( "presentationURL": "http://2.3.4.5:8080/", }, ), - 8080, + 80, ), ( SOURCE_ZEROCONF, @@ -443,7 +495,7 @@ async def test_discovered_device_already_configured( properties={"macaddress": MAC}, type="mock_type", ), - 8080, + 80, ), ], ) @@ -607,7 +659,6 @@ async def test_option_flow(hass: HomeAssistant, setup_config_entry) -> None: assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == { - CONF_EVENTS: True, CONF_STREAM_PROFILE: "profile_1", CONF_VIDEO_SOURCE: 1, } diff --git a/tests/components/axis/test_diagnostics.py b/tests/components/axis/test_diagnostics.py index af11fdc388a..026e1ae4d22 100644 --- a/tests/components/axis/test_diagnostics.py +++ b/tests/components/axis/test_diagnostics.py @@ -1,4 +1,5 @@ """Test Axis diagnostics.""" + import pytest from syrupy import SnapshotAssertion diff --git a/tests/components/axis/test_device.py b/tests/components/axis/test_hub.py similarity index 63% rename from tests/components/axis/test_device.py rename to tests/components/axis/test_hub.py index 0672abbb46b..1ae6db05427 100644 --- a/tests/components/axis/test_device.py +++ b/tests/components/axis/test_hub.py @@ -1,4 +1,5 @@ """Test Axis device.""" + from ipaddress import ip_address from unittest import mock from unittest.mock import Mock, patch @@ -8,6 +9,7 @@ import pytest from homeassistant.components import axis, zeroconf from homeassistant.components.axis.const import DOMAIN as AXIS_DOMAIN +from homeassistant.components.axis.hub import AxisHub from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.config_entries import SOURCE_ZEROCONF from homeassistant.const import ( @@ -33,55 +35,55 @@ from tests.common import async_fire_mqtt_message from tests.typing import MqttMockHAClient -@pytest.fixture(name="forward_entry_setup") +@pytest.fixture(name="forward_entry_setups") def hass_mock_forward_entry_setup(hass): - """Mock async_forward_entry_setup.""" - with patch.object(hass.config_entries, "async_forward_entry_setup") as forward_mock: + """Mock async_forward_entry_setups.""" + with patch.object( + hass.config_entries, "async_forward_entry_setups" + ) as forward_mock: yield forward_mock async def test_device_setup( hass: HomeAssistant, - forward_entry_setup, - config, + forward_entry_setups, + config_entry_data, setup_config_entry, device_registry: dr.DeviceRegistry, ) -> None: """Successful setup.""" - device = hass.data[AXIS_DOMAIN][setup_config_entry.entry_id] + hub = AxisHub.get_hub(hass, setup_config_entry) - assert device.api.vapix.firmware_version == "9.10.1" - assert device.api.vapix.product_number == "M1065-LW" - assert device.api.vapix.product_type == "Network Camera" - assert device.api.vapix.serial_number == "00408C123456" + assert hub.api.vapix.firmware_version == "9.10.1" + assert hub.api.vapix.product_number == "M1065-LW" + assert hub.api.vapix.product_type == "Network Camera" + assert hub.api.vapix.serial_number == "00408C123456" - assert len(forward_entry_setup.mock_calls) == 4 - assert forward_entry_setup.mock_calls[0][1][1] == "binary_sensor" - assert forward_entry_setup.mock_calls[1][1][1] == "camera" - assert forward_entry_setup.mock_calls[2][1][1] == "light" - assert forward_entry_setup.mock_calls[3][1][1] == "switch" + assert len(forward_entry_setups.mock_calls) == 1 + platforms = set(forward_entry_setups.mock_calls[0][1][1]) + assert platforms == {"binary_sensor", "camera", "light", "switch"} - assert device.host == config[CONF_HOST] - assert device.model == config[CONF_MODEL] - assert device.name == config[CONF_NAME] - assert device.unique_id == FORMATTED_MAC + assert hub.config.host == config_entry_data[CONF_HOST] + assert hub.config.model == config_entry_data[CONF_MODEL] + assert hub.config.name == config_entry_data[CONF_NAME] + assert hub.unique_id == FORMATTED_MAC device_entry = device_registry.async_get_device( - identifiers={(AXIS_DOMAIN, device.unique_id)} + identifiers={(AXIS_DOMAIN, hub.unique_id)} ) - assert device_entry.configuration_url == device.api.config.url + assert device_entry.configuration_url == hub.api.config.url @pytest.mark.parametrize("api_discovery_items", [API_DISCOVERY_BASIC_DEVICE_INFO]) async def test_device_info(hass: HomeAssistant, setup_config_entry) -> None: """Verify other path of device information works.""" - device = hass.data[AXIS_DOMAIN][setup_config_entry.entry_id] + hub = AxisHub.get_hub(hass, setup_config_entry) - assert device.api.vapix.firmware_version == "9.80.1" - assert device.api.vapix.product_number == "M1065-LW" - assert device.api.vapix.product_type == "Network Camera" - assert device.api.vapix.serial_number == "00408C123456" + assert hub.api.vapix.firmware_version == "9.80.1" + assert hub.api.vapix.product_number == "M1065-LW" + assert hub.api.vapix.product_type == "Network Camera" + assert hub.api.vapix.serial_number == "00408C123456" @pytest.mark.parametrize("api_discovery_items", [API_DISCOVERY_MQTT]) @@ -89,9 +91,9 @@ async def test_device_support_mqtt( hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_config_entry ) -> None: """Successful setup.""" - mqtt_mock.async_subscribe.assert_called_with(f"{MAC}/#", mock.ANY, 0, "utf-8") + mqtt_mock.async_subscribe.assert_called_with(f"axis/{MAC}/#", mock.ANY, 0, "utf-8") - topic = f"{MAC}/event/tns:onvif/Device/tns:axis/Sensor/PIR/$source/sensor/0" + topic = f"axis/{MAC}/event/tns:onvif/Device/tns:axis/Sensor/PIR/$source/sensor/0" message = ( b'{"timestamp": 1590258472044, "topic": "onvif:Device/axis:Sensor/PIR",' b' "message": {"source": {"sensor": "0"}, "key": {}, "data": {"state": "1"}}}' @@ -111,8 +113,8 @@ async def test_update_address( hass: HomeAssistant, setup_config_entry, mock_vapix_requests ) -> None: """Test update address works.""" - device = hass.data[AXIS_DOMAIN][setup_config_entry.entry_id] - assert device.api.config.host == "1.2.3.4" + hub = AxisHub.get_hub(hass, setup_config_entry) + assert hub.api.config.host == "1.2.3.4" with patch( "homeassistant.components.axis.async_setup_entry", return_value=True @@ -133,7 +135,7 @@ async def test_update_address( ) await hass.async_block_till_done() - assert device.api.config.host == "2.3.4.5" + assert hub.api.config.host == "2.3.4.5" assert len(mock_setup_entry.mock_calls) == 1 @@ -171,18 +173,11 @@ async def test_device_unavailable( assert hass.states.get(f"{BINARY_SENSOR_DOMAIN}.{NAME}_sound_1").state == STATE_OFF -async def test_device_reset(hass: HomeAssistant, setup_config_entry) -> None: - """Successfully reset device.""" - device = hass.data[AXIS_DOMAIN][setup_config_entry.entry_id] - result = await device.async_reset() - assert result is True - - async def test_device_not_accessible( hass: HomeAssistant, config_entry, setup_default_vapix_requests ) -> None: """Failed setup schedules a retry of setup.""" - with patch.object(axis, "get_axis_device", side_effect=axis.errors.CannotConnect): + with patch.object(axis, "get_axis_api", side_effect=axis.errors.CannotConnect): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() assert hass.data[AXIS_DOMAIN] == {} @@ -192,9 +187,12 @@ async def test_device_trigger_reauth_flow( hass: HomeAssistant, config_entry, setup_default_vapix_requests ) -> None: """Failed authentication trigger a reauthentication flow.""" - with patch.object( - axis, "get_axis_device", side_effect=axis.errors.AuthenticationRequired - ), patch.object(hass.config_entries.flow, "async_init") as mock_flow_init: + with ( + patch.object( + axis, "get_axis_api", side_effect=axis.errors.AuthenticationRequired + ), + patch.object(hass.config_entries.flow, "async_init") as mock_flow_init, + ): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() mock_flow_init.assert_called_once() @@ -205,44 +203,53 @@ async def test_device_unknown_error( hass: HomeAssistant, config_entry, setup_default_vapix_requests ) -> None: """Unknown errors are handled.""" - with patch.object(axis, "get_axis_device", side_effect=Exception): + with patch.object(axis, "get_axis_api", side_effect=Exception): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() assert hass.data[AXIS_DOMAIN] == {} -async def test_shutdown(config) -> None: +async def test_shutdown(config_entry_data) -> None: """Successful shutdown.""" hass = Mock() entry = Mock() - entry.data = config + entry.data = config_entry_data - axis_device = axis.device.AxisNetworkDevice(hass, entry, Mock()) + mock_api = Mock() + mock_api.vapix.serial_number = FORMATTED_MAC + axis_device = axis.hub.AxisHub(hass, entry, mock_api) await axis_device.shutdown(None) assert len(axis_device.api.stream.stop.mock_calls) == 1 -async def test_get_device_fails(hass: HomeAssistant, config) -> None: +async def test_get_device_fails(hass: HomeAssistant, config_entry_data) -> None: """Device unauthorized yields authentication required error.""" - with patch( - "axis.vapix.vapix.Vapix.initialize", side_effect=axislib.Unauthorized - ), pytest.raises(axis.errors.AuthenticationRequired): - await axis.device.get_axis_device(hass, config) + with ( + patch( + "axis.interfaces.vapix.Vapix.initialize", side_effect=axislib.Unauthorized + ), + pytest.raises(axis.errors.AuthenticationRequired), + ): + await axis.hub.get_axis_api(hass, config_entry_data) -async def test_get_device_device_unavailable(hass: HomeAssistant, config) -> None: +async def test_get_device_device_unavailable( + hass: HomeAssistant, config_entry_data +) -> None: """Device unavailable yields cannot connect error.""" - with patch( - "axis.vapix.vapix.Vapix.request", side_effect=axislib.RequestError - ), pytest.raises(axis.errors.CannotConnect): - await axis.device.get_axis_device(hass, config) + with ( + patch("axis.interfaces.vapix.Vapix.request", side_effect=axislib.RequestError), + pytest.raises(axis.errors.CannotConnect), + ): + await axis.hub.get_axis_api(hass, config_entry_data) -async def test_get_device_unknown_error(hass: HomeAssistant, config) -> None: +async def test_get_device_unknown_error(hass: HomeAssistant, config_entry_data) -> None: """Device yield unknown error.""" - with patch( - "axis.vapix.vapix.Vapix.request", side_effect=axislib.AxisException - ), pytest.raises(axis.errors.AuthenticationRequired): - await axis.device.get_axis_device(hass, config) + with ( + patch("axis.interfaces.vapix.Vapix.request", side_effect=axislib.AxisException), + pytest.raises(axis.errors.AuthenticationRequired), + ): + await axis.hub.get_axis_api(hass, config_entry_data) diff --git a/tests/components/axis/test_init.py b/tests/components/axis/test_init.py index 28cfff17ed3..7a22597197b 100644 --- a/tests/components/axis/test_init.py +++ b/tests/components/axis/test_init.py @@ -1,4 +1,5 @@ """Test Axis component setup process.""" + from unittest.mock import AsyncMock, Mock, patch import pytest @@ -18,7 +19,7 @@ async def test_setup_entry_fails(hass: HomeAssistant, config_entry) -> None: mock_device = Mock() mock_device.async_setup = AsyncMock(return_value=False) - with patch.object(axis, "AxisNetworkDevice") as mock_device_class: + with patch.object(axis, "AxisHub") as mock_device_class: mock_device_class.return_value = mock_device assert not await hass.config_entries.async_setup(config_entry.entry_id) diff --git a/tests/components/axis/test_light.py b/tests/components/axis/test_light.py index b5503e1486a..5cde6b74fc4 100644 --- a/tests/components/axis/test_light.py +++ b/tests/components/axis/test_light.py @@ -1,12 +1,15 @@ """Axis light platform tests.""" + +from collections.abc import Callable +from typing import Any from unittest.mock import patch -from axis.vapix.models.api import CONTEXT +from axis.models.api import CONTEXT import pytest import respx -from homeassistant.components.axis.const import DOMAIN as AXIS_DOMAIN from homeassistant.components.light import ATTR_BRIGHTNESS, DOMAIN as LIGHT_DOMAIN +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_TURN_OFF, @@ -15,7 +18,6 @@ from homeassistant.const import ( STATE_ON, ) from homeassistant.core import HomeAssistant -from homeassistant.setup import async_setup_component from .const import DEFAULT_HOST, NAME @@ -27,7 +29,7 @@ API_DISCOVERY_LIGHT_CONTROL = { @pytest.fixture -def light_control_items(): +def light_control_items() -> list[dict[str, Any]]: """Available lights.""" return [ { @@ -46,7 +48,7 @@ def light_control_items(): @pytest.fixture(autouse=True) -def light_control_fixture(light_control_items): +def light_control_fixture(light_control_items: list[dict[str, Any]]) -> None: """Light control mock response.""" data = { "apiVersion": "1.1", @@ -66,24 +68,12 @@ def light_control_fixture(light_control_items): ) -async def test_platform_manually_configured(hass: HomeAssistant) -> None: - """Test that nothing happens when platform is manually configured.""" - assert await async_setup_component( - hass, LIGHT_DOMAIN, {LIGHT_DOMAIN: {"platform": AXIS_DOMAIN}} - ) - - assert AXIS_DOMAIN not in hass.data - - -async def test_no_lights(hass: HomeAssistant, setup_config_entry) -> None: - """Test that no light events in Axis results in no light entities.""" - assert not hass.states.async_entity_ids(LIGHT_DOMAIN) - - @pytest.mark.parametrize("api_discovery_items", [API_DISCOVERY_LIGHT_CONTROL]) @pytest.mark.parametrize("light_control_items", [[]]) async def test_no_light_entity_without_light_control_representation( - hass: HomeAssistant, setup_config_entry, mock_rtsp_event + hass: HomeAssistant, + setup_config_entry: ConfigEntry, + mock_rtsp_event: Callable[[str, str, str, str, str, str], None], ) -> None: """Verify no lights entities get created without light control representation.""" mock_rtsp_event( @@ -101,10 +91,10 @@ async def test_no_light_entity_without_light_control_representation( @pytest.mark.parametrize("api_discovery_items", [API_DISCOVERY_LIGHT_CONTROL]) async def test_lights( hass: HomeAssistant, - respx_mock, - setup_config_entry, - mock_rtsp_event, - api_discovery_items, + respx_mock: respx, + setup_config_entry: ConfigEntry, + mock_rtsp_event: Callable[[str, str, str, str, str, str], None], + api_discovery_items: dict[str, Any], ) -> None: """Test that lights are loaded properly.""" # Add light @@ -159,9 +149,12 @@ async def test_lights( assert light_0.name == f"{NAME} IR Light 0" # Turn on, set brightness, light already on - with patch("axis.vapix.vapix.LightHandler.activate_light") as mock_activate, patch( - "axis.vapix.vapix.LightHandler.set_manual_intensity" - ) as mock_set_intensity: + with ( + patch("axis.interfaces.vapix.LightHandler.activate_light") as mock_activate, + patch( + "axis.interfaces.vapix.LightHandler.set_manual_intensity" + ) as mock_set_intensity, + ): await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, @@ -172,7 +165,9 @@ async def test_lights( mock_set_intensity.assert_called_once_with("led0", 29) # Turn off - with patch("axis.vapix.vapix.LightHandler.deactivate_light") as mock_deactivate: + with patch( + "axis.interfaces.vapix.LightHandler.deactivate_light" + ) as mock_deactivate: await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_OFF, @@ -195,9 +190,12 @@ async def test_lights( assert light_0.state == STATE_OFF # Turn on, set brightness - with patch("axis.vapix.vapix.LightHandler.activate_light") as mock_activate, patch( - "axis.vapix.vapix.LightHandler.set_manual_intensity" - ) as mock_set_intensity: + with ( + patch("axis.interfaces.vapix.LightHandler.activate_light") as mock_activate, + patch( + "axis.interfaces.vapix.LightHandler.set_manual_intensity" + ) as mock_set_intensity, + ): await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, @@ -208,7 +206,9 @@ async def test_lights( mock_set_intensity.assert_not_called() # Turn off, light already off - with patch("axis.vapix.vapix.LightHandler.deactivate_light") as mock_deactivate: + with patch( + "axis.interfaces.vapix.LightHandler.deactivate_light" + ) as mock_deactivate: await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_OFF, diff --git a/tests/components/axis/test_switch.py b/tests/components/axis/test_switch.py index 14e27c3437a..b9202d42e25 100644 --- a/tests/components/axis/test_switch.py +++ b/tests/components/axis/test_switch.py @@ -1,11 +1,13 @@ """Axis switch platform tests.""" + +from collections.abc import Callable from unittest.mock import patch -from axis.vapix.models.api import CONTEXT +from axis.models.api import CONTEXT import pytest -from homeassistant.components.axis.const import DOMAIN as AXIS_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_TURN_OFF, @@ -14,25 +16,9 @@ from homeassistant.const import ( STATE_ON, ) from homeassistant.core import HomeAssistant -from homeassistant.setup import async_setup_component from .const import API_DISCOVERY_PORT_MANAGEMENT, NAME - -async def test_platform_manually_configured(hass: HomeAssistant) -> None: - """Test that nothing happens when platform is manually configured.""" - assert await async_setup_component( - hass, SWITCH_DOMAIN, {SWITCH_DOMAIN: {"platform": AXIS_DOMAIN}} - ) - - assert AXIS_DOMAIN not in hass.data - - -async def test_no_switches(hass: HomeAssistant, setup_config_entry) -> None: - """Test that no output events in Axis results in no switch entities.""" - assert not hass.states.async_entity_ids(SWITCH_DOMAIN) - - PORT_DATA = """root.IOPort.I0.Configurable=yes root.IOPort.I0.Direction=output root.IOPort.I0.Output.Name=Doorbell @@ -46,7 +32,9 @@ root.IOPort.I1.Output.Active=open @pytest.mark.parametrize("param_ports_payload", [PORT_DATA]) async def test_switches_with_port_cgi( - hass: HomeAssistant, setup_config_entry, mock_rtsp_event + hass: HomeAssistant, + setup_config_entry: ConfigEntry, + mock_rtsp_event: Callable[[str, str, str, str, str, str], None], ) -> None: """Test that switches are loaded properly using port.cgi.""" mock_rtsp_event( @@ -77,7 +65,7 @@ async def test_switches_with_port_cgi( assert relay_0.state == STATE_OFF assert relay_0.name == f"{NAME} Doorbell" - with patch("axis.vapix.vapix.Ports.close") as mock_turn_on: + with patch("axis.interfaces.vapix.Ports.close") as mock_turn_on: await hass.services.async_call( SWITCH_DOMAIN, SERVICE_TURN_ON, @@ -86,7 +74,7 @@ async def test_switches_with_port_cgi( ) mock_turn_on.assert_called_once_with("0") - with patch("axis.vapix.vapix.Ports.open") as mock_turn_off: + with patch("axis.interfaces.vapix.Ports.open") as mock_turn_off: await hass.services.async_call( SWITCH_DOMAIN, SERVICE_TURN_OFF, @@ -129,7 +117,9 @@ PORT_MANAGEMENT_RESPONSE = { @pytest.mark.parametrize("api_discovery_items", [API_DISCOVERY_PORT_MANAGEMENT]) @pytest.mark.parametrize("port_management_payload", [PORT_MANAGEMENT_RESPONSE]) async def test_switches_with_port_management( - hass: HomeAssistant, setup_config_entry, mock_rtsp_event + hass: HomeAssistant, + setup_config_entry: ConfigEntry, + mock_rtsp_event: Callable[[str, str, str, str, str, str], None], ) -> None: """Test that switches are loaded properly using port management.""" mock_rtsp_event( @@ -173,7 +163,7 @@ async def test_switches_with_port_management( assert hass.states.get(f"{SWITCH_DOMAIN}.{NAME}_relay_1").state == STATE_ON - with patch("axis.vapix.vapix.IoPortManagement.close") as mock_turn_on: + with patch("axis.interfaces.vapix.IoPortManagement.close") as mock_turn_on: await hass.services.async_call( SWITCH_DOMAIN, SERVICE_TURN_ON, @@ -182,7 +172,7 @@ async def test_switches_with_port_management( ) mock_turn_on.assert_called_once_with("0") - with patch("axis.vapix.vapix.IoPortManagement.open") as mock_turn_off: + with patch("axis.interfaces.vapix.IoPortManagement.open") as mock_turn_off: await hass.services.async_call( SWITCH_DOMAIN, SERVICE_TURN_OFF, diff --git a/tests/components/azure_devops/test_config_flow.py b/tests/components/azure_devops/test_config_flow.py index 713d3f31a2f..84c2b5d3cca 100644 --- a/tests/components/azure_devops/test_config_flow.py +++ b/tests/components/azure_devops/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Azure DevOps config flow.""" + from unittest.mock import patch from aioazuredevops.core import DevOpsProject @@ -133,14 +134,18 @@ async def test_reauth_connection_error(hass: HomeAssistant) -> None: async def test_project_error(hass: HomeAssistant) -> None: """Test we show user form on Azure DevOps connection error.""" - with patch( - "homeassistant.components.azure_devops.config_flow.DevOpsClient.authorized", - return_value=True, - ), patch( - "homeassistant.components.azure_devops.config_flow.DevOpsClient.authorize", - ), patch( - "homeassistant.components.azure_devops.config_flow.DevOpsClient.get_project", - return_value=None, + with ( + patch( + "homeassistant.components.azure_devops.config_flow.DevOpsClient.authorized", + return_value=True, + ), + patch( + "homeassistant.components.azure_devops.config_flow.DevOpsClient.authorize", + ), + patch( + "homeassistant.components.azure_devops.config_flow.DevOpsClient.get_project", + return_value=None, + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -162,14 +167,18 @@ async def test_project_error(hass: HomeAssistant) -> None: async def test_reauth_project_error(hass: HomeAssistant) -> None: """Test we show user form on Azure DevOps project error.""" - with patch( - "homeassistant.components.azure_devops.config_flow.DevOpsClient.authorize", - ), patch( - "homeassistant.components.azure_devops.config_flow.DevOpsClient.authorized", - return_value=True, - ), patch( - "homeassistant.components.azure_devops.config_flow.DevOpsClient.get_project", - return_value=None, + with ( + patch( + "homeassistant.components.azure_devops.config_flow.DevOpsClient.authorize", + ), + patch( + "homeassistant.components.azure_devops.config_flow.DevOpsClient.authorized", + return_value=True, + ), + patch( + "homeassistant.components.azure_devops.config_flow.DevOpsClient.get_project", + return_value=None, + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -212,15 +221,19 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: assert result["step_id"] == "reauth" assert result["errors"] == {"base": "invalid_auth"} - with patch( - "homeassistant.components.azure_devops.config_flow.DevOpsClient.authorize", - ), patch( - "homeassistant.components.azure_devops.config_flow.DevOpsClient.authorized", - return_value=True, - ), patch( - "homeassistant.components.azure_devops.config_flow.DevOpsClient.get_project", - return_value=DevOpsProject( - "abcd-abcd-abcd-abcd", FIXTURE_USER_INPUT[CONF_PROJECT] + with ( + patch( + "homeassistant.components.azure_devops.config_flow.DevOpsClient.authorize", + ), + patch( + "homeassistant.components.azure_devops.config_flow.DevOpsClient.authorized", + return_value=True, + ), + patch( + "homeassistant.components.azure_devops.config_flow.DevOpsClient.get_project", + return_value=DevOpsProject( + "abcd-abcd-abcd-abcd", FIXTURE_USER_INPUT[CONF_PROJECT] + ), ), ): result2 = await hass.config_entries.flow.async_configure( @@ -235,18 +248,23 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: async def test_full_flow_implementation(hass: HomeAssistant) -> None: """Test registering an integration and finishing flow works.""" - with patch( - "homeassistant.components.azure_devops.async_setup_entry", - return_value=True, - ) as mock_setup_entry, patch( - "homeassistant.components.azure_devops.config_flow.DevOpsClient.authorized", - return_value=True, - ), patch( - "homeassistant.components.azure_devops.config_flow.DevOpsClient.authorize", - ), patch( - "homeassistant.components.azure_devops.config_flow.DevOpsClient.get_project", - return_value=DevOpsProject( - "abcd-abcd-abcd-abcd", FIXTURE_USER_INPUT[CONF_PROJECT] + with ( + patch( + "homeassistant.components.azure_devops.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + patch( + "homeassistant.components.azure_devops.config_flow.DevOpsClient.authorized", + return_value=True, + ), + patch( + "homeassistant.components.azure_devops.config_flow.DevOpsClient.authorize", + ), + patch( + "homeassistant.components.azure_devops.config_flow.DevOpsClient.get_project", + return_value=DevOpsProject( + "abcd-abcd-abcd-abcd", FIXTURE_USER_INPUT[CONF_PROJECT] + ), ), ): result = await hass.config_entries.flow.async_init( diff --git a/tests/components/azure_event_hub/conftest.py b/tests/components/azure_event_hub/conftest.py index fac93e32ab9..622b11000d7 100644 --- a/tests/components/azure_event_hub/conftest.py +++ b/tests/components/azure_event_hub/conftest.py @@ -1,4 +1,5 @@ """Test fixtures for AEH.""" + from dataclasses import dataclass from datetime import timedelta import logging diff --git a/tests/components/azure_event_hub/const.py b/tests/components/azure_event_hub/const.py index 1daf100238c..1d887139479 100644 --- a/tests/components/azure_event_hub/const.py +++ b/tests/components/azure_event_hub/const.py @@ -1,4 +1,5 @@ """Constants for testing AEH.""" + from homeassistant.components.azure_event_hub.const import ( CONF_EVENT_HUB_CON_STRING, CONF_EVENT_HUB_INSTANCE_NAME, diff --git a/tests/components/azure_event_hub/test_config_flow.py b/tests/components/azure_event_hub/test_config_flow.py index 8cebbe6fbd4..38454e46dd1 100644 --- a/tests/components/azure_event_hub/test_config_flow.py +++ b/tests/components/azure_event_hub/test_config_flow.py @@ -1,4 +1,5 @@ """Test the AEH config flow.""" + import logging from unittest.mock import AsyncMock diff --git a/tests/components/azure_event_hub/test_init.py b/tests/components/azure_event_hub/test_init.py index 13ded3c31be..0d5cfff80e9 100644 --- a/tests/components/azure_event_hub/test_init.py +++ b/tests/components/azure_event_hub/test_init.py @@ -1,4 +1,5 @@ """Test the init functions for AEH.""" + from datetime import timedelta import logging from unittest.mock import patch diff --git a/tests/components/backup/common.py b/tests/components/backup/common.py index 824057b6500..70b33d2de3f 100644 --- a/tests/components/backup/common.py +++ b/tests/components/backup/common.py @@ -1,4 +1,5 @@ """Common helpers for the Backup integration tests.""" + from __future__ import annotations from pathlib import Path diff --git a/tests/components/backup/test_http.py b/tests/components/backup/test_http.py index cb10d1b9947..baf1798534a 100644 --- a/tests/components/backup/test_http.py +++ b/tests/components/backup/test_http.py @@ -1,4 +1,5 @@ """Tests for the Backup integration.""" + from unittest.mock import patch from aiohttp import web @@ -20,12 +21,16 @@ async def test_downloading_backup( client = await hass_client() - with patch( - "homeassistant.components.backup.http.BackupManager.get_backup", - return_value=TEST_BACKUP, - ), patch("pathlib.Path.exists", return_value=True), patch( - "homeassistant.components.backup.http.FileResponse", - return_value=web.Response(text=""), + with ( + patch( + "homeassistant.components.backup.http.BackupManager.get_backup", + return_value=TEST_BACKUP, + ), + patch("pathlib.Path.exists", return_value=True), + patch( + "homeassistant.components.backup.http.FileResponse", + return_value=web.Response(text=""), + ), ): resp = await client.get("/api/backup/download/abc123") assert resp.status == 200 diff --git a/tests/components/backup/test_init.py b/tests/components/backup/test_init.py index 1e164abb1bb..9fdfa978f94 100644 --- a/tests/components/backup/test_init.py +++ b/tests/components/backup/test_init.py @@ -1,4 +1,5 @@ """Tests for the Backup integration.""" + from unittest.mock import patch import pytest diff --git a/tests/components/backup/test_manager.py b/tests/components/backup/test_manager.py index f7ecab0efa1..41749298819 100644 --- a/tests/components/backup/test_manager.py +++ b/tests/components/backup/test_manager.py @@ -1,4 +1,5 @@ """Tests for the Backup integration.""" + from __future__ import annotations from pathlib import Path @@ -29,28 +30,37 @@ async def _mock_backup_generation(manager: BackupManager): Path(".storage"), ] - with patch( - "homeassistant.components.backup.manager.SecureTarFile" - ) as mocked_tarfile, patch("pathlib.Path.iterdir", _mock_iterdir), patch( - "pathlib.Path.stat", MagicMock(st_size=123) - ), patch("pathlib.Path.is_file", lambda x: x.name != ".storage"), patch( - "pathlib.Path.is_dir", - lambda x: x.name == ".storage", - ), patch( - "pathlib.Path.exists", - lambda x: x != manager.backup_dir, - ), patch( - "pathlib.Path.is_symlink", - lambda _: False, - ), patch( - "pathlib.Path.mkdir", - MagicMock(), - ), patch( - "homeassistant.components.backup.manager.json_bytes", - return_value=b"{}", # Empty JSON - ) as mocked_json_bytes, patch( - "homeassistant.components.backup.manager.HAVERSION", - "2025.1.0", + with ( + patch( + "homeassistant.components.backup.manager.SecureTarFile" + ) as mocked_tarfile, + patch("pathlib.Path.iterdir", _mock_iterdir), + patch("pathlib.Path.stat", MagicMock(st_size=123)), + patch("pathlib.Path.is_file", lambda x: x.name != ".storage"), + patch( + "pathlib.Path.is_dir", + lambda x: x.name == ".storage", + ), + patch( + "pathlib.Path.exists", + lambda x: x != manager.backup_dir, + ), + patch( + "pathlib.Path.is_symlink", + lambda _: False, + ), + patch( + "pathlib.Path.mkdir", + MagicMock(), + ), + patch( + "homeassistant.components.backup.manager.json_bytes", + return_value=b"{}", # Empty JSON + ) as mocked_json_bytes, + patch( + "homeassistant.components.backup.manager.HAVERSION", + "2025.1.0", + ), ): await manager.generate_backup() @@ -81,18 +91,21 @@ async def test_constructor(hass: HomeAssistant) -> None: async def test_load_backups(hass: HomeAssistant) -> None: """Test loading backups.""" manager = BackupManager(hass) - with patch("pathlib.Path.glob", return_value=[TEST_BACKUP.path]), patch( - "tarfile.open", return_value=MagicMock() - ), patch( - "homeassistant.components.backup.manager.json_loads_object", - return_value={ - "slug": TEST_BACKUP.slug, - "name": TEST_BACKUP.name, - "date": TEST_BACKUP.date, - }, - ), patch( - "pathlib.Path.stat", - return_value=MagicMock(st_size=TEST_BACKUP.size), + with ( + patch("pathlib.Path.glob", return_value=[TEST_BACKUP.path]), + patch("tarfile.open", return_value=MagicMock()), + patch( + "homeassistant.components.backup.manager.json_loads_object", + return_value={ + "slug": TEST_BACKUP.slug, + "name": TEST_BACKUP.name, + "date": TEST_BACKUP.date, + }, + ), + patch( + "pathlib.Path.stat", + return_value=MagicMock(st_size=TEST_BACKUP.size), + ), ): await manager.load_backups() backups = await manager.get_backups() @@ -105,12 +118,13 @@ async def test_load_backups_with_exception( ) -> None: """Test loading backups with exception.""" manager = BackupManager(hass) - with patch("pathlib.Path.glob", return_value=[TEST_BACKUP.path]), patch( - "tarfile.open", side_effect=OSError("Test ecxeption") + with ( + patch("pathlib.Path.glob", return_value=[TEST_BACKUP.path]), + patch("tarfile.open", side_effect=OSError("Test exception")), ): await manager.load_backups() backups = await manager.get_backups() - assert f"Unable to read backup {TEST_BACKUP.path}: Test ecxeption" in caplog.text + assert f"Unable to read backup {TEST_BACKUP.path}: Test exception" in caplog.text assert backups == {} @@ -199,6 +213,7 @@ async def test_loading_platforms( ), ) await manager.load_platforms() + await hass.async_block_till_done() assert manager.loaded_platforms assert len(manager.platforms) == 1 @@ -218,6 +233,7 @@ async def test_not_loading_bad_platforms( await _setup_mock_domain(hass) await manager.load_platforms() + await hass.async_block_till_done() assert manager.loaded_platforms assert len(manager.platforms) == 0 diff --git a/tests/components/backup/test_websocket.py b/tests/components/backup/test_websocket.py index e79b958be20..79d682c69fe 100644 --- a/tests/components/backup/test_websocket.py +++ b/tests/components/backup/test_websocket.py @@ -1,4 +1,5 @@ """Tests for the Backup integration.""" + from unittest.mock import patch import pytest @@ -26,10 +27,10 @@ def sync_access_token_proxy( @pytest.mark.parametrize( "with_hassio", - ( + [ pytest.param(True, id="with_hassio"), pytest.param(False, id="without_hassio"), - ), + ], ) async def test_info( hass: HomeAssistant, @@ -53,10 +54,10 @@ async def test_info( @pytest.mark.parametrize( "with_hassio", - ( + [ pytest.param(True, id="with_hassio"), pytest.param(False, id="without_hassio"), - ), + ], ) async def test_remove( hass: HomeAssistant, @@ -79,10 +80,10 @@ async def test_remove( @pytest.mark.parametrize( "with_hassio", - ( + [ pytest.param(True, id="with_hassio"), pytest.param(False, id="without_hassio"), - ), + ], ) async def test_generate( hass: HomeAssistant, @@ -110,10 +111,10 @@ async def test_generate( ) @pytest.mark.parametrize( ("with_hassio"), - ( + [ pytest.param(True, id="with_hassio"), pytest.param(False, id="without_hassio"), - ), + ], ) async def test_backup_end( hass: HomeAssistant, @@ -144,10 +145,10 @@ async def test_backup_end( ) @pytest.mark.parametrize( ("with_hassio"), - ( + [ pytest.param(True, id="with_hassio"), pytest.param(False, id="without_hassio"), - ), + ], ) async def test_backup_start( hass: HomeAssistant, @@ -173,11 +174,11 @@ async def test_backup_start( @pytest.mark.parametrize( "exception", - ( + [ TimeoutError(), HomeAssistantError("Boom"), Exception("Boom"), - ), + ], ) async def test_backup_end_excepion( hass: HomeAssistant, @@ -202,11 +203,11 @@ async def test_backup_end_excepion( @pytest.mark.parametrize( "exception", - ( + [ TimeoutError(), HomeAssistantError("Boom"), Exception("Boom"), - ), + ], ) async def test_backup_start_excepion( hass: HomeAssistant, diff --git a/tests/components/baf/__init__.py b/tests/components/baf/__init__.py index 4e435dc1a2e..648f235349d 100644 --- a/tests/components/baf/__init__.py +++ b/tests/components/baf/__init__.py @@ -1,6 +1,5 @@ """Tests for the Big Ass Fans integration.""" - import asyncio from aiobafi6 import Device diff --git a/tests/components/baf/test_config_flow.py b/tests/components/baf/test_config_flow.py index f15c624447c..c7c56179839 100644 --- a/tests/components/baf/test_config_flow.py +++ b/tests/components/baf/test_config_flow.py @@ -1,4 +1,5 @@ """Test the baf config flow.""" + from ipaddress import ip_address from unittest.mock import patch @@ -32,10 +33,13 @@ async def test_form_user(hass: HomeAssistant) -> None: assert result["type"] == "form" assert result["errors"] == {} - with _patch_device_config_flow(), patch( - "homeassistant.components.baf.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + _patch_device_config_flow(), + patch( + "homeassistant.components.baf.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_IP_ADDRESS: "127.0.0.1"}, @@ -181,10 +185,13 @@ async def test_user_flow_is_not_blocked_by_discovery(hass: HomeAssistant) -> Non assert result["type"] == "form" assert result["errors"] == {} - with _patch_device_config_flow(), patch( - "homeassistant.components.baf.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + _patch_device_config_flow(), + patch( + "homeassistant.components.baf.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_IP_ADDRESS: "127.0.0.1"}, diff --git a/tests/components/baf/test_init.py b/tests/components/baf/test_init.py index c87237892ad..f2616fdd96d 100644 --- a/tests/components/baf/test_init.py +++ b/tests/components/baf/test_init.py @@ -1,4 +1,5 @@ """Test the baf init flow.""" + from unittest.mock import patch from aiobafi6.exceptions import DeviceUUIDMismatchError diff --git a/tests/components/balboa/__init__.py b/tests/components/balboa/__init__.py index 9ea41eb2463..a27293e955f 100644 --- a/tests/components/balboa/__init__.py +++ b/tests/components/balboa/__init__.py @@ -1,4 +1,5 @@ """Test the Balboa Spa Client integration.""" + from __future__ import annotations from unittest.mock import MagicMock diff --git a/tests/components/balboa/conftest.py b/tests/components/balboa/conftest.py index fa2787dd6e2..fce022572c3 100644 --- a/tests/components/balboa/conftest.py +++ b/tests/components/balboa/conftest.py @@ -1,4 +1,5 @@ """Provide common fixtures.""" + from __future__ import annotations from collections.abc import Callable, Generator diff --git a/tests/components/balboa/test_binary_sensor.py b/tests/components/balboa/test_binary_sensor.py index ee5f2bc353c..bcce2b96a0b 100644 --- a/tests/components/balboa/test_binary_sensor.py +++ b/tests/components/balboa/test_binary_sensor.py @@ -1,4 +1,5 @@ """Tests of the climate entity of the balboa integration.""" + from __future__ import annotations from unittest.mock import MagicMock diff --git a/tests/components/balboa/test_climate.py b/tests/components/balboa/test_climate.py index a4b758eeab8..c75244ecb94 100644 --- a/tests/components/balboa/test_climate.py +++ b/tests/components/balboa/test_climate.py @@ -1,4 +1,5 @@ """Tests of the climate entity of the balboa integration.""" + from __future__ import annotations from unittest.mock import MagicMock, patch diff --git a/tests/components/balboa/test_config_flow.py b/tests/components/balboa/test_config_flow.py index 95c415b8909..66bc47d23f0 100644 --- a/tests/components/balboa/test_config_flow.py +++ b/tests/components/balboa/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Balboa Spa Client config flow.""" + from unittest.mock import MagicMock, patch from pybalboa.exceptions import SpaConnectionError @@ -25,13 +26,16 @@ async def test_form(hass: HomeAssistant, client: MagicMock) -> None: assert result["type"] == FlowResultType.FORM assert result["errors"] == {} - with patch( - "homeassistant.components.balboa.config_flow.SpaClient.__aenter__", - return_value=client, - ), patch( - "homeassistant.components.balboa.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.balboa.config_flow.SpaClient.__aenter__", + return_value=client, + ), + patch( + "homeassistant.components.balboa.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], TEST_DATA, @@ -112,12 +116,15 @@ async def test_already_configured(hass: HomeAssistant, client: MagicMock) -> Non assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" - with patch( - "homeassistant.components.balboa.config_flow.SpaClient.__aenter__", - return_value=client, - ), patch( - "homeassistant.components.balboa.async_setup_entry", - return_value=True, + with ( + patch( + "homeassistant.components.balboa.config_flow.SpaClient.__aenter__", + return_value=client, + ), + patch( + "homeassistant.components.balboa.async_setup_entry", + return_value=True, + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], diff --git a/tests/components/balboa/test_fan.py b/tests/components/balboa/test_fan.py index fbf9f1854cd..878a14784f7 100644 --- a/tests/components/balboa/test_fan.py +++ b/tests/components/balboa/test_fan.py @@ -1,4 +1,5 @@ """Tests of the pump fan entity of the balboa integration.""" + from __future__ import annotations from unittest.mock import MagicMock diff --git a/tests/components/balboa/test_light.py b/tests/components/balboa/test_light.py index e20e55c5980..da969a7e2d8 100644 --- a/tests/components/balboa/test_light.py +++ b/tests/components/balboa/test_light.py @@ -1,4 +1,5 @@ """Tests of the light entity of the balboa integration.""" + from __future__ import annotations from unittest.mock import MagicMock diff --git a/tests/components/bang_olufsen/conftest.py b/tests/components/bang_olufsen/conftest.py index 8c212ef16be..d076316e36c 100644 --- a/tests/components/bang_olufsen/conftest.py +++ b/tests/components/bang_olufsen/conftest.py @@ -1,5 +1,6 @@ """Test fixtures for bang_olufsen.""" +from collections.abc import Generator from unittest.mock import AsyncMock, patch from mozart_api.models import BeolinkPeer @@ -18,25 +19,6 @@ from .const import ( from tests.common import MockConfigEntry -class MockMozartClient: - """Class for mocking MozartClient objects and methods.""" - - async def __aenter__(self): - """Mock async context entry.""" - - async def __aexit__(self, exc_type, exc, tb): - """Mock async context exit.""" - - # API call results - get_beolink_self_result = BeolinkPeer( - friendly_name=TEST_FRIENDLY_NAME, jid=TEST_JID_1 - ) - - # API endpoints - get_beolink_self = AsyncMock() - get_beolink_self.return_value = get_beolink_self_result - - @pytest.fixture def mock_config_entry(): """Mock config entry.""" @@ -49,17 +31,25 @@ def mock_config_entry(): @pytest.fixture -def mock_client(): +def mock_mozart_client() -> Generator[AsyncMock, None, None]: """Mock MozartClient.""" - client = MockMozartClient() - - with patch("mozart_api.mozart_client.MozartClient", return_value=client): + with ( + patch( + "homeassistant.components.bang_olufsen.MozartClient", autospec=True + ) as mock_client, + patch( + "homeassistant.components.bang_olufsen.config_flow.MozartClient", + new=mock_client, + ), + ): + client = mock_client.return_value + client.get_beolink_self = AsyncMock() + client.get_beolink_self.return_value = BeolinkPeer( + friendly_name=TEST_FRIENDLY_NAME, jid=TEST_JID_1 + ) yield client - # Reset mocked API call counts and side effects - client.get_beolink_self.reset_mock(side_effect=True) - @pytest.fixture def mock_setup_entry(): diff --git a/tests/components/bang_olufsen/const.py b/tests/components/bang_olufsen/const.py index 1b13e1b3412..187f93108a1 100644 --- a/tests/components/bang_olufsen/const.py +++ b/tests/components/bang_olufsen/const.py @@ -1,6 +1,5 @@ """Constants used for testing the bang_olufsen integration.""" - from ipaddress import IPv4Address, IPv6Address from homeassistant.components.bang_olufsen.const import ( diff --git a/tests/components/bang_olufsen/test_config_flow.py b/tests/components/bang_olufsen/test_config_flow.py index dd42c4c5c8c..d813ddf185b 100644 --- a/tests/components/bang_olufsen/test_config_flow.py +++ b/tests/components/bang_olufsen/test_config_flow.py @@ -1,6 +1,5 @@ """Test the bang_olufsen config_flow.""" - from unittest.mock import Mock from aiohttp.client_exceptions import ClientConnectorError @@ -13,7 +12,6 @@ from homeassistant.const import CONF_SOURCE from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType -from .conftest import MockMozartClient from .const import ( TEST_DATA_CREATE_ENTRY, TEST_DATA_USER, @@ -27,10 +25,10 @@ pytestmark = pytest.mark.usefixtures("mock_setup_entry") async def test_config_flow_timeout_error( - hass: HomeAssistant, mock_client: MockMozartClient + hass: HomeAssistant, mock_mozart_client ) -> None: """Test we handle timeout_error.""" - mock_client.get_beolink_self.side_effect = TimeoutError() + mock_mozart_client.get_beolink_self.side_effect = TimeoutError() result_user = await hass.config_entries.flow.async_init( handler=DOMAIN, @@ -40,14 +38,16 @@ async def test_config_flow_timeout_error( assert result_user["type"] == FlowResultType.FORM assert result_user["errors"] == {"base": "timeout_error"} - assert mock_client.get_beolink_self.call_count == 1 + assert mock_mozart_client.get_beolink_self.call_count == 1 async def test_config_flow_client_connector_error( - hass: HomeAssistant, mock_client: MockMozartClient + hass: HomeAssistant, mock_mozart_client ) -> None: """Test we handle client_connector_error.""" - mock_client.get_beolink_self.side_effect = ClientConnectorError(Mock(), Mock()) + mock_mozart_client.get_beolink_self.side_effect = ClientConnectorError( + Mock(), Mock() + ) result_user = await hass.config_entries.flow.async_init( handler=DOMAIN, @@ -57,7 +57,7 @@ async def test_config_flow_client_connector_error( assert result_user["type"] == FlowResultType.FORM assert result_user["errors"] == {"base": "client_connector_error"} - assert mock_client.get_beolink_self.call_count == 1 + assert mock_mozart_client.get_beolink_self.call_count == 1 async def test_config_flow_invalid_ip(hass: HomeAssistant) -> None: @@ -73,10 +73,10 @@ async def test_config_flow_invalid_ip(hass: HomeAssistant) -> None: async def test_config_flow_api_exception( - hass: HomeAssistant, mock_client: MockMozartClient + hass: HomeAssistant, mock_mozart_client ) -> None: """Test we handle api_exception.""" - mock_client.get_beolink_self.side_effect = ApiException() + mock_mozart_client.get_beolink_self.side_effect = ApiException() result_user = await hass.config_entries.flow.async_init( handler=DOMAIN, @@ -86,10 +86,10 @@ async def test_config_flow_api_exception( assert result_user["type"] == FlowResultType.FORM assert result_user["errors"] == {"base": "api_exception"} - assert mock_client.get_beolink_self.call_count == 1 + assert mock_mozart_client.get_beolink_self.call_count == 1 -async def test_config_flow(hass: HomeAssistant, mock_client: MockMozartClient) -> None: +async def test_config_flow(hass: HomeAssistant, mock_mozart_client) -> None: """Test config flow.""" result_init = await hass.config_entries.flow.async_init( @@ -109,12 +109,10 @@ async def test_config_flow(hass: HomeAssistant, mock_client: MockMozartClient) - assert result_user["type"] == FlowResultType.CREATE_ENTRY assert result_user["data"] == TEST_DATA_CREATE_ENTRY - assert mock_client.get_beolink_self.call_count == 1 + assert mock_mozart_client.get_beolink_self.call_count == 1 -async def test_config_flow_zeroconf( - hass: HomeAssistant, mock_client: MockMozartClient -) -> None: +async def test_config_flow_zeroconf(hass: HomeAssistant, mock_mozart_client) -> None: """Test zeroconf discovery.""" result_zeroconf = await hass.config_entries.flow.async_init( @@ -134,7 +132,7 @@ async def test_config_flow_zeroconf( assert result_confirm["type"] == FlowResultType.CREATE_ENTRY assert result_confirm["data"] == TEST_DATA_CREATE_ENTRY - assert mock_client.get_beolink_self.call_count == 0 + assert mock_mozart_client.get_beolink_self.call_count == 0 async def test_config_flow_zeroconf_not_mozart_device(hass: HomeAssistant) -> None: diff --git a/tests/components/bayesian/test_binary_sensor.py b/tests/components/bayesian/test_binary_sensor.py index e4c3e38695a..2c94da10ce8 100644 --- a/tests/components/bayesian/test_binary_sensor.py +++ b/tests/components/bayesian/test_binary_sensor.py @@ -1,4 +1,5 @@ """The test for the bayesian sensor platform.""" + import json from unittest.mock import patch @@ -402,7 +403,7 @@ async def test_multiple_observations(hass: HomeAssistant) -> None: state = hass.states.get("binary_sensor.test_binary") - for _, attrs in state.attributes.items(): + for attrs in state.attributes.values(): json.dumps(attrs) assert state.attributes.get("occurred_observation_entities") == [] assert state.attributes.get("probability") == 0.2 @@ -474,7 +475,7 @@ async def test_multiple_numeric_observations(hass: HomeAssistant) -> None: state = hass.states.get("binary_sensor.test_binary") - for _, attrs in state.attributes.items(): + for attrs in state.attributes.values(): json.dumps(attrs) assert state.attributes.get("occurred_observation_entities") == [] assert state.attributes.get("probability") == 0.1 @@ -782,7 +783,7 @@ async def test_state_attributes_are_serializable(hass: HomeAssistant) -> None: state.attributes.get("occurred_observation_entities") ) - for _, attrs in state.attributes.items(): + for attrs in state.attributes.values(): json.dumps(attrs) diff --git a/tests/components/binary_sensor/common.py b/tests/components/binary_sensor/common.py new file mode 100644 index 00000000000..bfa9b8e2d52 --- /dev/null +++ b/tests/components/binary_sensor/common.py @@ -0,0 +1,19 @@ +"""Common test utilities for binary_sensor entity component tests.""" + +from homeassistant.components.binary_sensor import BinarySensorEntity + +from tests.common import MockEntity + + +class MockBinarySensor(MockEntity, BinarySensorEntity): + """Mock Binary Sensor class.""" + + @property + def is_on(self): + """Return true if the binary sensor is on.""" + return self._handle("is_on") + + @property + def device_class(self): + """Return the class of this sensor.""" + return self._handle("device_class") diff --git a/tests/components/binary_sensor/conftest.py b/tests/components/binary_sensor/conftest.py new file mode 100644 index 00000000000..33fcbc24089 --- /dev/null +++ b/tests/components/binary_sensor/conftest.py @@ -0,0 +1,21 @@ +"""Fixtures for binary_sensor entity component tests.""" + +import pytest + +from homeassistant.components.binary_sensor import BinarySensorDeviceClass + +from .common import MockBinarySensor + + +@pytest.fixture +def mock_binary_sensor_entities() -> dict[str, MockBinarySensor]: + """Return mock binary sensors.""" + return { + device_class: MockBinarySensor( + name=f"{device_class} sensor", + is_on=True, + unique_id=f"unique_{device_class}", + device_class=device_class, + ) + for device_class in BinarySensorDeviceClass + } diff --git a/tests/components/binary_sensor/test_device_condition.py b/tests/components/binary_sensor/test_device_condition.py index c902caf31ae..83451313bad 100644 --- a/tests/components/binary_sensor/test_device_condition.py +++ b/tests/components/binary_sensor/test_device_condition.py @@ -1,4 +1,5 @@ """The test for binary_sensor device automation.""" + from datetime import timedelta from freezegun import freeze_time @@ -15,11 +16,14 @@ 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 .common import MockBinarySensor + from tests.common import ( MockConfigEntry, async_get_device_automation_capabilities, async_get_device_automations, async_mock_service, + setup_test_component_platform, ) @@ -38,11 +42,10 @@ async def test_get_conditions( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - enable_custom_integrations: None, + mock_binary_sensor_entities: dict[str, MockBinarySensor], ) -> None: """Test we get the expected conditions from a binary_sensor.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() + setup_test_component_platform(hass, DOMAIN, mock_binary_sensor_entities.values()) assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) await hass.async_block_till_done() binary_sensor_entries = {} @@ -57,7 +60,7 @@ async def test_get_conditions( binary_sensor_entries[device_class] = entity_registry.async_get_or_create( DOMAIN, "test", - platform.ENTITIES[device_class].unique_id, + mock_binary_sensor_entities[device_class].unique_id, device_id=device_entry.id, ) @@ -81,12 +84,12 @@ async def test_get_conditions( @pytest.mark.parametrize( ("hidden_by", "entity_category"), - ( + [ (er.RegistryEntryHider.INTEGRATION, None), (er.RegistryEntryHider.USER, None), (None, EntityCategory.CONFIG), (None, EntityCategory.DIAGNOSTIC), - ), + ], ) async def test_get_conditions_hidden_auxiliary( hass: HomeAssistant, @@ -237,12 +240,10 @@ async def test_if_state( device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, calls, - enable_custom_integrations: None, + mock_binary_sensor_entities: dict[str, MockBinarySensor], ) -> None: """Test for turn_on and turn_off conditions.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - - platform.init() + setup_test_component_platform(hass, DOMAIN, mock_binary_sensor_entities.values()) assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) await hass.async_block_till_done() @@ -252,7 +253,7 @@ async def test_if_state( config_entry_id=config_entry.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) - entry = entity_registry.async_get(platform.ENTITIES["battery"].entity_id) + entry = entity_registry.async_get(mock_binary_sensor_entities["battery"].entity_id) entity_registry.async_update_entity(entry.entity_id, device_id=device_entry.id) assert await async_setup_component( @@ -323,12 +324,10 @@ async def test_if_state_legacy( device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, calls, - enable_custom_integrations: None, + mock_binary_sensor_entities: dict[str, MockBinarySensor], ) -> None: """Test for turn_on and turn_off conditions.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - - platform.init() + setup_test_component_platform(hass, DOMAIN, mock_binary_sensor_entities.values()) assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) await hass.async_block_till_done() @@ -338,7 +337,7 @@ async def test_if_state_legacy( config_entry_id=config_entry.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) - entry = entity_registry.async_get(platform.ENTITIES["battery"].entity_id) + entry = entity_registry.async_get(mock_binary_sensor_entities["battery"].entity_id) entity_registry.async_update_entity(entry.entity_id, device_id=device_entry.id) assert await async_setup_component( @@ -383,16 +382,14 @@ async def test_if_fires_on_for_condition( device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, calls, - enable_custom_integrations: None, + mock_binary_sensor_entities: dict[str, MockBinarySensor], ) -> None: """Test for firing if condition is on with delay.""" point1 = dt_util.utcnow() point2 = point1 + timedelta(seconds=10) point3 = point2 + timedelta(seconds=10) - platform = getattr(hass.components, f"test.{DOMAIN}") - - platform.init() + setup_test_component_platform(hass, DOMAIN, mock_binary_sensor_entities.values()) assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) await hass.async_block_till_done() @@ -402,7 +399,7 @@ async def test_if_fires_on_for_condition( config_entry_id=config_entry.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) - entry = entity_registry.async_get(platform.ENTITIES["battery"].entity_id) + entry = entity_registry.async_get(mock_binary_sensor_entities["battery"].entity_id) entity_registry.async_update_entity(entry.entity_id, device_id=device_entry.id) with freeze_time(point1) as time_freeze: diff --git a/tests/components/binary_sensor/test_device_trigger.py b/tests/components/binary_sensor/test_device_trigger.py index 47abb29ae86..ad7bd9c3528 100644 --- a/tests/components/binary_sensor/test_device_trigger.py +++ b/tests/components/binary_sensor/test_device_trigger.py @@ -1,4 +1,5 @@ """The test for binary_sensor device automation.""" + from datetime import timedelta import pytest @@ -14,12 +15,15 @@ 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 .common import MockBinarySensor + from tests.common import ( MockConfigEntry, async_fire_time_changed, async_get_device_automation_capabilities, async_get_device_automations, async_mock_service, + setup_test_component_platform, ) @@ -38,12 +42,11 @@ async def test_get_triggers( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - enable_custom_integrations: None, + mock_binary_sensor_entities: dict[str, MockBinarySensor], ) -> None: """Test we get the expected triggers from a binary_sensor.""" registry_entries: dict[BinarySensorDeviceClass, er.RegistryEntry] = {} - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() + setup_test_component_platform(hass, DOMAIN, mock_binary_sensor_entities.values()) assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) await hass.async_block_till_done() @@ -57,7 +60,7 @@ async def test_get_triggers( registry_entries[device_class] = entity_registry.async_get_or_create( DOMAIN, "test", - platform.ENTITIES[device_class].unique_id, + mock_binary_sensor_entities[device_class].unique_id, device_id=device_entry.id, ) @@ -81,12 +84,12 @@ async def test_get_triggers( @pytest.mark.parametrize( ("hidden_by", "entity_category"), - ( + [ (er.RegistryEntryHider.INTEGRATION, None), (er.RegistryEntryHider.USER, None), (None, EntityCategory.CONFIG), (None, EntityCategory.DIAGNOSTIC), - ), + ], ) async def test_get_triggers_hidden_auxiliary( hass: HomeAssistant, @@ -131,11 +134,11 @@ async def test_get_triggers_no_state( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, + mock_binary_sensor_entities: dict[str, MockBinarySensor], ) -> None: """Test we get the expected triggers from a binary_sensor.""" registry_entries: dict[BinarySensorDeviceClass, er.RegistryEntry] = {} - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() + setup_test_component_platform(hass, DOMAIN, mock_binary_sensor_entities.values()) config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) @@ -238,11 +241,10 @@ async def test_if_fires_on_state_change( device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, calls, - enable_custom_integrations: None, + mock_binary_sensor_entities: dict[str, MockBinarySensor], ) -> None: """Test for on and off triggers firing.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() + setup_test_component_platform(hass, DOMAIN, mock_binary_sensor_entities.values()) assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) await hass.async_block_till_done() @@ -255,7 +257,7 @@ async def test_if_fires_on_state_change( entry = entity_registry.async_get_or_create( DOMAIN, "test", - platform.ENTITIES["battery"].unique_id, + mock_binary_sensor_entities["battery"].unique_id, device_id=device_entry.id, ) @@ -340,12 +342,10 @@ async def test_if_fires_on_state_change_with_for( device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, calls, - enable_custom_integrations: None, + mock_binary_sensor_entities: dict[str, MockBinarySensor], ) -> None: """Test for triggers firing with delay.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - - platform.init() + setup_test_component_platform(hass, DOMAIN, mock_binary_sensor_entities.values()) assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) await hass.async_block_till_done() @@ -358,7 +358,7 @@ async def test_if_fires_on_state_change_with_for( entry = entity_registry.async_get_or_create( DOMAIN, "test", - platform.ENTITIES["battery"].unique_id, + mock_binary_sensor_entities["battery"].unique_id, device_id=device_entry.id, ) @@ -417,12 +417,10 @@ async def test_if_fires_on_state_change_legacy( device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, calls, - enable_custom_integrations: None, + mock_binary_sensor_entities: dict[str, MockBinarySensor], ) -> None: """Test for triggers firing.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - - platform.init() + setup_test_component_platform(hass, DOMAIN, mock_binary_sensor_entities.values()) assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) await hass.async_block_till_done() @@ -435,7 +433,7 @@ async def test_if_fires_on_state_change_legacy( entry = entity_registry.async_get_or_create( DOMAIN, "test", - platform.ENTITIES["battery"].unique_id, + mock_binary_sensor_entities["battery"].unique_id, device_id=device_entry.id, ) @@ -478,6 +476,7 @@ async def test_if_fires_on_state_change_legacy( hass.states.async_set(entry.entity_id, STATE_OFF) await hass.async_block_till_done() assert len(calls) == 1 - assert calls[0].data["some"] == "turn_off device - {} - on - off - None".format( - entry.entity_id + assert ( + calls[0].data["some"] + == f"turn_off device - {entry.entity_id} - on - off - None" ) diff --git a/tests/components/binary_sensor/test_init.py b/tests/components/binary_sensor/test_init.py index 6ca189113b9..335b9b40d50 100644 --- a/tests/components/binary_sensor/test_init.py +++ b/tests/components/binary_sensor/test_init.py @@ -1,4 +1,5 @@ """The tests for the Binary sensor component.""" + from collections.abc import Generator from unittest import mock @@ -10,6 +11,8 @@ from homeassistant.const import STATE_OFF, STATE_ON, EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback +from .common import MockBinarySensor + from tests.common import ( MockConfigEntry, MockModule, @@ -20,7 +23,6 @@ from tests.common import ( mock_integration, mock_platform, ) -from tests.testing_config.custom_components.test.binary_sensor import MockBinarySensor TEST_DOMAIN = "test" diff --git a/tests/components/binary_sensor/test_significant_change.py b/tests/components/binary_sensor/test_significant_change.py index fed3581eeee..3042630540a 100644 --- a/tests/components/binary_sensor/test_significant_change.py +++ b/tests/components/binary_sensor/test_significant_change.py @@ -1,4 +1,5 @@ """Test the Binary Sensor significant change platform.""" + from homeassistant.components.binary_sensor.significant_change import ( async_check_significant_change, ) diff --git a/tests/components/blackbird/test_media_player.py b/tests/components/blackbird/test_media_player.py index de8be9d4ed5..3b0465ef208 100644 --- a/tests/components/blackbird/test_media_player.py +++ b/tests/components/blackbird/test_media_player.py @@ -1,4 +1,5 @@ """The tests for the Monoprice Blackbird media player platform.""" + from collections import defaultdict from unittest import mock @@ -175,7 +176,7 @@ async def setup_blackbird(hass, mock_blackbird): """Set up blackbird.""" with mock.patch( "homeassistant.components.blackbird.media_player.get_blackbird", - new=lambda *a: mock_blackbird, + return_value=mock_blackbird, ): await hass.async_add_executor_job( setup_platform, diff --git a/tests/components/blebox/conftest.py b/tests/components/blebox/conftest.py index 82698633c30..868d936d83a 100644 --- a/tests/components/blebox/conftest.py +++ b/tests/components/blebox/conftest.py @@ -1,4 +1,5 @@ """PyTest fixtures and test helpers.""" + from unittest import mock from unittest.mock import AsyncMock, PropertyMock, patch diff --git a/tests/components/blebox/test_binary_sensor.py b/tests/components/blebox/test_binary_sensor.py index 3c05a425b12..f04feab86de 100644 --- a/tests/components/blebox/test_binary_sensor.py +++ b/tests/components/blebox/test_binary_sensor.py @@ -1,4 +1,5 @@ """Blebox binary_sensor entities test.""" + from unittest.mock import AsyncMock, PropertyMock import blebox_uniapi diff --git a/tests/components/blebox/test_button.py b/tests/components/blebox/test_button.py index cc5ab769853..fe596c41e33 100644 --- a/tests/components/blebox/test_button.py +++ b/tests/components/blebox/test_button.py @@ -1,4 +1,5 @@ """Blebox button entities tests.""" + import logging from unittest.mock import PropertyMock diff --git a/tests/components/blebox/test_climate.py b/tests/components/blebox/test_climate.py index 6ea6d995900..8ba0c3f630e 100644 --- a/tests/components/blebox/test_climate.py +++ b/tests/components/blebox/test_climate.py @@ -1,4 +1,5 @@ """BleBox climate entities tests.""" + import logging from unittest.mock import AsyncMock, PropertyMock diff --git a/tests/components/blebox/test_config_flow.py b/tests/components/blebox/test_config_flow.py index dafba61d77a..3f59ed022fd 100644 --- a/tests/components/blebox/test_config_flow.py +++ b/tests/components/blebox/test_config_flow.py @@ -1,4 +1,5 @@ """Test Home Assistant config flow for BleBox devices.""" + from ipaddress import ip_address from unittest.mock import DEFAULT, AsyncMock, PropertyMock, patch diff --git a/tests/components/blebox/test_cover.py b/tests/components/blebox/test_cover.py index 8691c886faa..1596de134c0 100644 --- a/tests/components/blebox/test_cover.py +++ b/tests/components/blebox/test_cover.py @@ -1,4 +1,5 @@ """BleBox cover entities tests.""" + import logging from unittest.mock import AsyncMock, PropertyMock diff --git a/tests/components/blebox/test_init.py b/tests/components/blebox/test_init.py index 00ee62568a5..f406df51bd4 100644 --- a/tests/components/blebox/test_init.py +++ b/tests/components/blebox/test_init.py @@ -1,4 +1,5 @@ """BleBox devices setup tests.""" + import logging import blebox_uniapi diff --git a/tests/components/blebox/test_light.py b/tests/components/blebox/test_light.py index 47f38ba815b..bfa5478265b 100644 --- a/tests/components/blebox/test_light.py +++ b/tests/components/blebox/test_light.py @@ -1,4 +1,5 @@ """BleBox light entities tests.""" + import logging from unittest.mock import AsyncMock, PropertyMock diff --git a/tests/components/blebox/test_sensor.py b/tests/components/blebox/test_sensor.py index 68990a09a32..56c240705b0 100644 --- a/tests/components/blebox/test_sensor.py +++ b/tests/components/blebox/test_sensor.py @@ -1,4 +1,5 @@ """Blebox sensors tests.""" + import logging from unittest.mock import AsyncMock, PropertyMock diff --git a/tests/components/blebox/test_switch.py b/tests/components/blebox/test_switch.py index db98a2705b2..7d1da7ba4c7 100644 --- a/tests/components/blebox/test_switch.py +++ b/tests/components/blebox/test_switch.py @@ -1,4 +1,5 @@ """Blebox switch tests.""" + import logging from unittest.mock import AsyncMock, PropertyMock diff --git a/tests/components/blink/conftest.py b/tests/components/blink/conftest.py index d15d35e1c08..b18fdf7615e 100644 --- a/tests/components/blink/conftest.py +++ b/tests/components/blink/conftest.py @@ -1,4 +1,5 @@ """Fixtures for the Blink integration tests.""" + from unittest.mock import AsyncMock, MagicMock, create_autospec, patch from uuid import uuid4 diff --git a/tests/components/blink/test_config_flow.py b/tests/components/blink/test_config_flow.py index e5ce3c83fb7..b5fbf19ef9b 100644 --- a/tests/components/blink/test_config_flow.py +++ b/tests/components/blink/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Blink config flow.""" + from unittest.mock import patch from blinkpy.auth import LoginError @@ -18,13 +19,17 @@ async def test_form(hass: HomeAssistant) -> None: assert result["type"] == "form" assert result["errors"] == {} - with patch("homeassistant.components.blink.config_flow.Auth.startup"), patch( - "homeassistant.components.blink.config_flow.Auth.check_key_required", - return_value=False, - ), patch( - "homeassistant.components.blink.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch("homeassistant.components.blink.config_flow.Auth.startup"), + patch( + "homeassistant.components.blink.config_flow.Auth.check_key_required", + return_value=False, + ), + patch( + "homeassistant.components.blink.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"username": "blink@example.com", "password": "example"}, @@ -54,9 +59,12 @@ async def test_form_2fa(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch("homeassistant.components.blink.config_flow.Auth.startup"), patch( - "homeassistant.components.blink.config_flow.Auth.check_key_required", - return_value=True, + with ( + patch("homeassistant.components.blink.config_flow.Auth.startup"), + patch( + "homeassistant.components.blink.config_flow.Auth.check_key_required", + return_value=True, + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -66,18 +74,24 @@ async def test_form_2fa(hass: HomeAssistant) -> None: assert result2["type"] == "form" assert result2["step_id"] == "2fa" - with patch("homeassistant.components.blink.config_flow.Auth.startup"), patch( - "homeassistant.components.blink.config_flow.Auth.check_key_required", - return_value=False, - ), patch( - "homeassistant.components.blink.config_flow.Auth.send_auth_key", - return_value=True, - ), patch( - "homeassistant.components.blink.config_flow.Blink.setup_urls", - return_value=True, - ), patch( - "homeassistant.components.blink.async_setup_entry", return_value=True - ) as mock_setup_entry: + with ( + patch("homeassistant.components.blink.config_flow.Auth.startup"), + patch( + "homeassistant.components.blink.config_flow.Auth.check_key_required", + return_value=False, + ), + patch( + "homeassistant.components.blink.config_flow.Auth.send_auth_key", + return_value=True, + ), + patch( + "homeassistant.components.blink.config_flow.Blink.setup_urls", + return_value=True, + ), + patch( + "homeassistant.components.blink.async_setup_entry", return_value=True + ) as mock_setup_entry, + ): result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], {"pin": "1234"} ) @@ -96,9 +110,12 @@ async def test_form_2fa_connect_error(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch("homeassistant.components.blink.config_flow.Auth.startup"), patch( - "homeassistant.components.blink.config_flow.Auth.check_key_required", - return_value=True, + with ( + patch("homeassistant.components.blink.config_flow.Auth.startup"), + patch( + "homeassistant.components.blink.config_flow.Auth.check_key_required", + return_value=True, + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -108,18 +125,24 @@ async def test_form_2fa_connect_error(hass: HomeAssistant) -> None: assert result2["type"] == "form" assert result2["step_id"] == "2fa" - with patch("homeassistant.components.blink.config_flow.Auth.startup"), patch( - "homeassistant.components.blink.config_flow.Auth.check_key_required", - return_value=False, - ), patch( - "homeassistant.components.blink.config_flow.Auth.send_auth_key", - return_value=True, - ), patch( - "homeassistant.components.blink.config_flow.Blink.setup_urls", - side_effect=BlinkSetupError, - ), patch( - "homeassistant.components.blink.async_setup_entry", - return_value=True, + with ( + patch("homeassistant.components.blink.config_flow.Auth.startup"), + patch( + "homeassistant.components.blink.config_flow.Auth.check_key_required", + return_value=False, + ), + patch( + "homeassistant.components.blink.config_flow.Auth.send_auth_key", + return_value=True, + ), + patch( + "homeassistant.components.blink.config_flow.Blink.setup_urls", + side_effect=BlinkSetupError, + ), + patch( + "homeassistant.components.blink.async_setup_entry", + return_value=True, + ), ): result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], {"pin": "1234"} @@ -136,9 +159,12 @@ async def test_form_2fa_invalid_key(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch("homeassistant.components.blink.config_flow.Auth.startup"), patch( - "homeassistant.components.blink.config_flow.Auth.check_key_required", - return_value=True, + with ( + patch("homeassistant.components.blink.config_flow.Auth.startup"), + patch( + "homeassistant.components.blink.config_flow.Auth.check_key_required", + return_value=True, + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -148,20 +174,26 @@ async def test_form_2fa_invalid_key(hass: HomeAssistant) -> None: assert result2["type"] == "form" assert result2["step_id"] == "2fa" - with patch( - "homeassistant.components.blink.config_flow.Auth.startup", - ), patch( - "homeassistant.components.blink.config_flow.Auth.check_key_required", - return_value=False, - ), patch( - "homeassistant.components.blink.config_flow.Auth.send_auth_key", - return_value=False, - ), patch( - "homeassistant.components.blink.config_flow.Blink.setup_urls", - return_value=True, - ), patch( - "homeassistant.components.blink.async_setup_entry", - return_value=True, + with ( + patch( + "homeassistant.components.blink.config_flow.Auth.startup", + ), + patch( + "homeassistant.components.blink.config_flow.Auth.check_key_required", + return_value=False, + ), + patch( + "homeassistant.components.blink.config_flow.Auth.send_auth_key", + return_value=False, + ), + patch( + "homeassistant.components.blink.config_flow.Blink.setup_urls", + return_value=True, + ), + patch( + "homeassistant.components.blink.async_setup_entry", + return_value=True, + ), ): result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], {"pin": "1234"} @@ -178,9 +210,12 @@ async def test_form_2fa_unknown_error(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch("homeassistant.components.blink.config_flow.Auth.startup"), patch( - "homeassistant.components.blink.config_flow.Auth.check_key_required", - return_value=True, + with ( + patch("homeassistant.components.blink.config_flow.Auth.startup"), + patch( + "homeassistant.components.blink.config_flow.Auth.check_key_required", + return_value=True, + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -190,18 +225,24 @@ async def test_form_2fa_unknown_error(hass: HomeAssistant) -> None: assert result2["type"] == "form" assert result2["step_id"] == "2fa" - with patch("homeassistant.components.blink.config_flow.Auth.startup"), patch( - "homeassistant.components.blink.config_flow.Auth.check_key_required", - return_value=False, - ), patch( - "homeassistant.components.blink.config_flow.Auth.send_auth_key", - return_value=True, - ), patch( - "homeassistant.components.blink.config_flow.Blink.setup_urls", - side_effect=KeyError, - ), patch( - "homeassistant.components.blink.async_setup_entry", - return_value=True, + with ( + patch("homeassistant.components.blink.config_flow.Auth.startup"), + patch( + "homeassistant.components.blink.config_flow.Auth.check_key_required", + return_value=False, + ), + patch( + "homeassistant.components.blink.config_flow.Auth.send_auth_key", + return_value=True, + ), + patch( + "homeassistant.components.blink.config_flow.Blink.setup_urls", + side_effect=KeyError, + ), + patch( + "homeassistant.components.blink.async_setup_entry", + return_value=True, + ), ): result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], {"pin": "1234"} diff --git a/tests/components/blink/test_diagnostics.py b/tests/components/blink/test_diagnostics.py index d447203dae6..3b120d23038 100644 --- a/tests/components/blink/test_diagnostics.py +++ b/tests/components/blink/test_diagnostics.py @@ -1,4 +1,5 @@ """Test Blink diagnostics.""" + from unittest.mock import MagicMock from syrupy import SnapshotAssertion diff --git a/tests/components/blink/test_init.py b/tests/components/blink/test_init.py index 454195c4d60..1f3a4c956c4 100644 --- a/tests/components/blink/test_init.py +++ b/tests/components/blink/test_init.py @@ -1,4 +1,5 @@ """Test the Blink init.""" + from unittest.mock import AsyncMock, MagicMock from aiohttp import ClientError diff --git a/tests/components/blink/test_services.py b/tests/components/blink/test_services.py index 40b9cce7bad..d2685bd04eb 100644 --- a/tests/components/blink/test_services.py +++ b/tests/components/blink/test_services.py @@ -1,4 +1,5 @@ """Test the Blink services.""" + from unittest.mock import AsyncMock, MagicMock, Mock import pytest diff --git a/tests/components/blue_current/__init__.py b/tests/components/blue_current/__init__.py index 63d8d084fae..97acff39a62 100644 --- a/tests/components/blue_current/__init__.py +++ b/tests/components/blue_current/__init__.py @@ -1,4 +1,5 @@ """Tests for the Blue Current integration.""" + from __future__ import annotations from asyncio import Event, Future @@ -30,15 +31,21 @@ def create_client_mock( future_container: FutureContainer, started_loop: Event, charge_point: dict, - status: dict | None, - grid: dict | None, + status: dict, + grid: dict, ) -> MagicMock: """Create a mock of the bluecurrent-api Client.""" client_mock = MagicMock(spec=Client) + received_charge_points = Event() - async def start_loop(receiver): + async def wait_for_charge_points(): + """Wait until chargepoints are received.""" + await received_charge_points.wait() + + async def connect(receiver, on_open): """Set the receiver and await future.""" client_mock.receiver = receiver + await on_open() started_loop.set() started_loop.clear() @@ -49,13 +56,13 @@ def create_client_mock( async def get_charge_points() -> None: """Send a list of charge points to the callback.""" - await started_loop.wait() await client_mock.receiver( { "object": "CHARGE_POINTS", "data": [charge_point], } ) + received_charge_points.set() async def get_status(evse_id: str) -> None: """Send the status of a charge point to the callback.""" @@ -70,7 +77,8 @@ def create_client_mock( """Send the grid status to the callback.""" await client_mock.receiver({"object": "GRID_STATUS", "data": grid}) - client_mock.start_loop.side_effect = start_loop + client_mock.connect.side_effect = connect + client_mock.wait_for_charge_points.side_effect = wait_for_charge_points client_mock.get_charge_points.side_effect = get_charge_points client_mock.get_status.side_effect = get_status client_mock.get_grid_status.side_effect = get_grid_status @@ -91,6 +99,12 @@ async def init_integration( if charge_point is None: charge_point = DEFAULT_CHARGE_POINT + if status is None: + status = {} + + if grid is None: + grid = {} + future_container = FutureContainer(hass.loop.create_future()) started_loop = Event() @@ -98,8 +112,9 @@ async def init_integration( hass, future_container, started_loop, charge_point, status, grid ) - with patch("homeassistant.components.blue_current.PLATFORMS", [platform]), patch( - "homeassistant.components.blue_current.Client", return_value=client_mock + with ( + patch("homeassistant.components.blue_current.PLATFORMS", [platform]), + patch("homeassistant.components.blue_current.Client", return_value=client_mock), ): config_entry.add_to_hass(hass) diff --git a/tests/components/blue_current/test_config_flow.py b/tests/components/blue_current/test_config_flow.py index b34decd8264..b5dad155618 100644 --- a/tests/components/blue_current/test_config_flow.py +++ b/tests/components/blue_current/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Blue Current config flow.""" + from unittest.mock import patch import pytest @@ -35,15 +36,19 @@ async def test_user(hass: HomeAssistant) -> None: assert result["errors"] == {} assert result["type"] == FlowResultType.FORM - with patch( - "homeassistant.components.blue_current.config_flow.Client.validate_api_token", - return_value="1234", - ), patch( - "homeassistant.components.blue_current.config_flow.Client.get_email", - return_value="test@email.com", - ), patch( - "homeassistant.components.blue_current.async_setup_entry", - return_value=True, + with ( + patch( + "homeassistant.components.blue_current.config_flow.Client.validate_api_token", + return_value="1234", + ), + patch( + "homeassistant.components.blue_current.config_flow.Client.get_email", + return_value="test@email.com", + ), + patch( + "homeassistant.components.blue_current.async_setup_entry", + return_value=True, + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -82,15 +87,19 @@ async def test_flow_fails(hass: HomeAssistant, error: Exception, message: str) - assert result["errors"]["base"] == message assert result["type"] == FlowResultType.FORM - with patch( - "homeassistant.components.blue_current.config_flow.Client.validate_api_token", - return_value="1234", - ), patch( - "homeassistant.components.blue_current.config_flow.Client.get_email", - return_value="test@email.com", - ), patch( - "homeassistant.components.blue_current.async_setup_entry", - return_value=True, + with ( + patch( + "homeassistant.components.blue_current.config_flow.Client.validate_api_token", + return_value="1234", + ), + patch( + "homeassistant.components.blue_current.config_flow.Client.get_email", + return_value="test@email.com", + ), + patch( + "homeassistant.components.blue_current.async_setup_entry", + return_value=True, + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -120,12 +129,22 @@ async def test_reauth( expected_api_token: str, ) -> None: """Test reauth flow.""" - with patch( - "homeassistant.components.blue_current.config_flow.Client.validate_api_token", - return_value=customer_id, - ), patch( - "homeassistant.components.blue_current.config_flow.Client.get_email", - return_value="test@email.com", + with ( + patch( + "homeassistant.components.blue_current.config_flow.Client.validate_api_token", + return_value=customer_id, + ), + patch( + "homeassistant.components.blue_current.config_flow.Client.get_email", + return_value="test@email.com", + ), + patch( + "homeassistant.components.blue_current.config_flow.Client.wait_for_charge_points", + ), + patch( + "homeassistant.components.blue_current.Client.connect", + lambda self, on_data, on_open: hass.loop.create_future(), + ), ): config_entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( diff --git a/tests/components/blue_current/test_init.py b/tests/components/blue_current/test_init.py index ce6eb2d9716..06cc6b27c26 100644 --- a/tests/components/blue_current/test_init.py +++ b/tests/components/blue_current/test_init.py @@ -1,4 +1,5 @@ """Test Blue Current Init Component.""" + from datetime import timedelta from unittest.mock import patch @@ -28,15 +29,23 @@ async def test_load_unload_entry( hass: HomeAssistant, config_entry: MockConfigEntry ) -> None: """Test load and unload entry.""" - with patch("homeassistant.components.blue_current.Client", autospec=True): + with ( + patch("homeassistant.components.blue_current.Client.validate_api_token"), + patch("homeassistant.components.blue_current.Client.wait_for_charge_points"), + patch("homeassistant.components.blue_current.Client.disconnect"), + patch( + "homeassistant.components.blue_current.Client.connect", + lambda self, on_data, on_open: hass.loop.create_future(), + ), + ): config_entry.add_to_hass(hass) await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state == ConfigEntryState.LOADED - await hass.config_entries.async_unload(config_entry.entry_id) - await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.NOT_LOADED + await hass.config_entries.async_unload(config_entry.entry_id) + await hass.async_block_till_done() + assert config_entry.state == ConfigEntryState.NOT_LOADED @pytest.mark.parametrize( @@ -53,44 +62,33 @@ async def test_config_exceptions( config_error: IntegrationError, ) -> None: """Test if the correct config error is raised when connecting to the api fails.""" - with patch( - "homeassistant.components.blue_current.Client.connect", - side_effect=api_error, - ), pytest.raises(config_error): + with ( + patch( + "homeassistant.components.blue_current.Client.validate_api_token", + side_effect=api_error, + ), + pytest.raises(config_error), + ): config_entry.add_to_hass(hass) await async_setup_entry(hass, config_entry) -async def test_start_loop(hass: HomeAssistant, config_entry: MockConfigEntry) -> None: - """Test start_loop.""" +async def test_connect_websocket_error( + hass: HomeAssistant, config_entry: MockConfigEntry +) -> None: + """Test reconnect when connect throws a WebsocketError.""" - with patch("homeassistant.components.blue_current.SMALL_DELAY", 0): + with patch("homeassistant.components.blue_current.DELAY", 0): mock_client, started_loop, future_container = await init_integration( hass, config_entry ) - future_container.future.set_exception(BlueCurrentException) + future_container.future.set_exception(WebsocketError) await started_loop.wait() assert mock_client.connect.call_count == 2 -async def test_reconnect_websocket_error( - hass: HomeAssistant, config_entry: MockConfigEntry -) -> None: - """Test reconnect when connect throws a WebsocketError.""" - - with patch("homeassistant.components.blue_current.LARGE_DELAY", 0): - mock_client, started_loop, future_container = await init_integration( - hass, config_entry - ) - future_container.future.set_exception(BlueCurrentException) - mock_client.connect.side_effect = [WebsocketError, None] - - await started_loop.wait() - assert mock_client.connect.call_count == 3 - - -async def test_reconnect_request_limit_reached_error( +async def test_connect_request_limit_reached_error( hass: HomeAssistant, config_entry: MockConfigEntry ) -> None: """Test reconnect when connect throws a RequestLimitReached.""" @@ -98,10 +96,9 @@ async def test_reconnect_request_limit_reached_error( mock_client, started_loop, future_container = await init_integration( hass, config_entry ) - future_container.future.set_exception(BlueCurrentException) - mock_client.connect.side_effect = [RequestLimitReached, None] + future_container.future.set_exception(RequestLimitReached) mock_client.get_next_reset_delta.return_value = timedelta(seconds=0) await started_loop.wait() assert mock_client.get_next_reset_delta.call_count == 1 - assert mock_client.connect.call_count == 3 + assert mock_client.connect.call_count == 2 diff --git a/tests/components/blue_current/test_sensor.py b/tests/components/blue_current/test_sensor.py index 68b42498c2f..5213cc0ff72 100644 --- a/tests/components/blue_current/test_sensor.py +++ b/tests/components/blue_current/test_sensor.py @@ -1,4 +1,5 @@ """The tests for Blue current sensors.""" + from datetime import datetime import pytest diff --git a/tests/components/bluemaestro/__init__.py b/tests/components/bluemaestro/__init__.py index bd9b86e040f..412bc3cb7b3 100644 --- a/tests/components/bluemaestro/__init__.py +++ b/tests/components/bluemaestro/__init__.py @@ -1,6 +1,5 @@ """Tests for the BlueMaestro integration.""" - from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo NOT_BLUEMAESTRO_SERVICE_INFO = BluetoothServiceInfo( diff --git a/tests/components/bluemaestro/test_config_flow.py b/tests/components/bluemaestro/test_config_flow.py index df3a6385e7c..f87ea053ffe 100644 --- a/tests/components/bluemaestro/test_config_flow.py +++ b/tests/components/bluemaestro/test_config_flow.py @@ -1,4 +1,5 @@ """Test the BlueMaestro config flow.""" + from unittest.mock import patch from homeassistant import config_entries diff --git a/tests/components/bluemaestro/test_sensor.py b/tests/components/bluemaestro/test_sensor.py index db66fbee2c2..fdcb16730ff 100644 --- a/tests/components/bluemaestro/test_sensor.py +++ b/tests/components/bluemaestro/test_sensor.py @@ -1,4 +1,5 @@ """Test the BlueMaestro sensors.""" + from homeassistant.components.bluemaestro.const import DOMAIN from homeassistant.components.sensor import ATTR_STATE_CLASS from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT diff --git a/tests/components/blueprint/test_default_blueprints.py b/tests/components/blueprint/test_default_blueprints.py index 28047e0a41e..9bd60a7cb6b 100644 --- a/tests/components/blueprint/test_default_blueprints.py +++ b/tests/components/blueprint/test_default_blueprints.py @@ -1,4 +1,5 @@ """Test default blueprints.""" + import importlib import logging import pathlib diff --git a/tests/components/blueprint/test_importer.py b/tests/components/blueprint/test_importer.py index d41c8417b57..76f3ff36d05 100644 --- a/tests/components/blueprint/test_importer.py +++ b/tests/components/blueprint/test_importer.py @@ -1,4 +1,5 @@ """Test blueprint importing.""" + import json from pathlib import Path @@ -118,10 +119,10 @@ async def test_fetch_blueprint_from_community_url( @pytest.mark.parametrize( "url", - ( + [ "https://raw.githubusercontent.com/balloob/home-assistant-config/main/blueprints/automation/motion_light.yaml", "https://github.com/balloob/home-assistant-config/blob/main/blueprints/automation/motion_light.yaml", - ), + ], ) async def test_fetch_blueprint_from_github_url( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, url: str diff --git a/tests/components/blueprint/test_models.py b/tests/components/blueprint/test_models.py index c11a467de9b..96e72e2b4cc 100644 --- a/tests/components/blueprint/test_models.py +++ b/tests/components/blueprint/test_models.py @@ -1,4 +1,5 @@ """Test blueprint models.""" + import logging from unittest.mock import AsyncMock, patch @@ -206,14 +207,18 @@ async def test_domain_blueprints_get_blueprint_errors( """Test domain blueprints.""" assert hass.data["blueprint"]["automation"] is domain_bps - with pytest.raises(errors.FailedToLoad), patch( - "homeassistant.util.yaml.load_yaml", side_effect=FileNotFoundError + with ( + pytest.raises(errors.FailedToLoad), + patch("homeassistant.util.yaml.load_yaml", side_effect=FileNotFoundError), ): await domain_bps.async_get_blueprint("non-existing-path") - with patch( - "homeassistant.util.yaml.load_yaml", return_value={"blueprint": "invalid"} - ), pytest.raises(errors.FailedToLoad): + with ( + patch( + "homeassistant.util.yaml.load_yaml", return_value={"blueprint": "invalid"} + ), + pytest.raises(errors.FailedToLoad), + ): await domain_bps.async_get_blueprint("non-existing-path") @@ -239,8 +244,9 @@ async def test_domain_blueprints_inputs_from_config(domain_bps, blueprint_1) -> with pytest.raises(errors.InvalidBlueprintInputs): await domain_bps.async_inputs_from_config({"not-referencing": "use_blueprint"}) - with pytest.raises(errors.MissingInput), patch.object( - domain_bps, "async_get_blueprint", return_value=blueprint_1 + with ( + pytest.raises(errors.MissingInput), + patch.object(domain_bps, "async_get_blueprint", return_value=blueprint_1), ): await domain_bps.async_inputs_from_config( {"use_blueprint": {"path": "bla.yaml", "input": {}}} diff --git a/tests/components/blueprint/test_schemas.py b/tests/components/blueprint/test_schemas.py index 4c941b56744..0440a759f2f 100644 --- a/tests/components/blueprint/test_schemas.py +++ b/tests/components/blueprint/test_schemas.py @@ -1,4 +1,5 @@ """Test schemas.""" + import logging import pytest @@ -11,7 +12,7 @@ _LOGGER = logging.getLogger(__name__) @pytest.mark.parametrize( "blueprint", - ( + [ # Test allow extra { "trigger": "Test allow extra", @@ -51,7 +52,7 @@ _LOGGER = logging.getLogger(__name__) }, } }, - ), + ], ) def test_blueprint_schema(blueprint) -> None: """Test different schemas.""" @@ -64,7 +65,7 @@ def test_blueprint_schema(blueprint) -> None: @pytest.mark.parametrize( "blueprint", - ( + [ # no domain {"blueprint": {}}, # non existing key in blueprint @@ -93,7 +94,7 @@ def test_blueprint_schema(blueprint) -> None: }, } }, - ), + ], ) def test_blueprint_schema_invalid(blueprint) -> None: """Test different schemas.""" @@ -103,11 +104,11 @@ def test_blueprint_schema_invalid(blueprint) -> None: @pytest.mark.parametrize( "bp_instance", - ( + [ {"path": "hello.yaml"}, {"path": "hello.yaml", "input": {}}, {"path": "hello.yaml", "input": {"hello": None}}, - ), + ], ) def test_blueprint_instance_fields(bp_instance) -> None: """Test blueprint instance fields.""" diff --git a/tests/components/blueprint/test_websocket_api.py b/tests/components/blueprint/test_websocket_api.py index b0439896c25..93d97dfd036 100644 --- a/tests/components/blueprint/test_websocket_api.py +++ b/tests/components/blueprint/test_websocket_api.py @@ -1,4 +1,5 @@ """Test websocket API.""" + from pathlib import Path from unittest.mock import Mock, patch @@ -399,7 +400,7 @@ async def test_delete_non_exist_file_blueprint( @pytest.mark.parametrize( "automation_config", - ( + [ { "automation": { "use_blueprint": { @@ -412,7 +413,7 @@ async def test_delete_non_exist_file_blueprint( } } }, - ), + ], ) async def test_delete_blueprint_in_use_by_automation( hass: HomeAssistant, @@ -445,7 +446,7 @@ async def test_delete_blueprint_in_use_by_automation( @pytest.mark.parametrize( "script_config", - ( + [ { "script": { "test_script": { @@ -458,7 +459,7 @@ async def test_delete_blueprint_in_use_by_automation( } } }, - ), + ], ) async def test_delete_blueprint_in_use_by_script( hass: HomeAssistant, diff --git a/tests/components/bluetooth/__init__.py b/tests/components/bluetooth/__init__.py index f4616abf8e5..675f3de67ee 100644 --- a/tests/components/bluetooth/__init__.py +++ b/tests/components/bluetooth/__init__.py @@ -1,6 +1,5 @@ """Tests for the Bluetooth integration.""" - from collections.abc import Iterable from contextlib import contextmanager import itertools @@ -59,13 +58,14 @@ BLE_DEVICE_DEFAULTS = { @contextmanager def patch_bluetooth_time(mock_time: float) -> None: """Patch the bluetooth time.""" - with patch( - "homeassistant.components.bluetooth.MONOTONIC_TIME", return_value=mock_time - ), patch( - "habluetooth.base_scanner.monotonic_time_coarse", return_value=mock_time - ), patch( - "habluetooth.manager.monotonic_time_coarse", return_value=mock_time - ), patch("habluetooth.scanner.monotonic_time_coarse", return_value=mock_time): + with ( + patch( + "homeassistant.components.bluetooth.MONOTONIC_TIME", return_value=mock_time + ), + patch("habluetooth.base_scanner.monotonic_time_coarse", return_value=mock_time), + patch("habluetooth.manager.monotonic_time_coarse", return_value=mock_time), + patch("habluetooth.scanner.monotonic_time_coarse", return_value=mock_time), + ): yield diff --git a/tests/components/bluetooth/conftest.py b/tests/components/bluetooth/conftest.py index 3c23c7428fb..c1e040ccd49 100644 --- a/tests/components/bluetooth/conftest.py +++ b/tests/components/bluetooth/conftest.py @@ -32,18 +32,21 @@ def disable_bluetooth_auto_recovery(): @pytest.fixture(name="operating_system_85") def mock_operating_system_85(): """Mock running Home Assistant Operating system 8.5.""" - with patch("homeassistant.components.hassio.is_hassio", return_value=True), patch( - "homeassistant.components.hassio.get_os_info", - return_value={ - "version": "8.5", - "version_latest": "10.0.dev20220912", - "update_available": False, - "board": "odroid-n2", - "boot": "B", - "data_disk": "/dev/mmcblk1p4", - }, - ), patch("homeassistant.components.hassio.get_info", return_value={}), patch( - "homeassistant.components.hassio.get_host_info", return_value={} + with ( + patch("homeassistant.components.hassio.is_hassio", return_value=True), + patch( + "homeassistant.components.hassio.get_os_info", + return_value={ + "version": "8.5", + "version_latest": "10.0.dev20220912", + "update_available": False, + "board": "odroid-n2", + "boot": "B", + "data_disk": "/dev/mmcblk1p4", + }, + ), + patch("homeassistant.components.hassio.get_info", return_value={}), + patch("homeassistant.components.hassio.get_host_info", return_value={}), ): yield @@ -51,18 +54,21 @@ def mock_operating_system_85(): @pytest.fixture(name="operating_system_90") def mock_operating_system_90(): """Mock running Home Assistant Operating system 9.0.""" - with patch("homeassistant.components.hassio.is_hassio", return_value=True), patch( - "homeassistant.components.hassio.get_os_info", - return_value={ - "version": "9.0.dev20220912", - "version_latest": "10.0.dev20220912", - "update_available": False, - "board": "odroid-n2", - "boot": "B", - "data_disk": "/dev/mmcblk1p4", - }, - ), patch("homeassistant.components.hassio.get_info", return_value={}), patch( - "homeassistant.components.hassio.get_host_info", return_value={} + with ( + patch("homeassistant.components.hassio.is_hassio", return_value=True), + patch( + "homeassistant.components.hassio.get_os_info", + return_value={ + "version": "9.0.dev20220912", + "version_latest": "10.0.dev20220912", + "update_available": False, + "board": "odroid-n2", + "boot": "B", + "data_disk": "/dev/mmcblk1p4", + }, + ), + patch("homeassistant.components.hassio.get_info", return_value={}), + patch("homeassistant.components.hassio.get_host_info", return_value={}), ): yield @@ -70,46 +76,62 @@ def mock_operating_system_90(): @pytest.fixture(name="macos_adapter") def macos_adapter(): """Fixture that mocks the macos adapter.""" - with patch("bleak.get_platform_scanner_backend_type"), patch( - "homeassistant.components.bluetooth.platform.system", - return_value="Darwin", - ), patch( - "habluetooth.scanner.platform.system", - return_value="Darwin", - ), patch( - "bluetooth_adapters.systems.platform.system", - return_value="Darwin", - ), patch("habluetooth.scanner.SYSTEM", "Darwin"): + with ( + patch("bleak.get_platform_scanner_backend_type"), + patch( + "homeassistant.components.bluetooth.platform.system", + return_value="Darwin", + ), + patch( + "habluetooth.scanner.platform.system", + return_value="Darwin", + ), + patch( + "bluetooth_adapters.systems.platform.system", + return_value="Darwin", + ), + patch("habluetooth.scanner.SYSTEM", "Darwin"), + ): yield @pytest.fixture(name="windows_adapter") def windows_adapter(): """Fixture that mocks the windows adapter.""" - with patch( - "bluetooth_adapters.systems.platform.system", - return_value="Windows", - ), patch("habluetooth.scanner.SYSTEM", "Windows"): + with ( + patch( + "bluetooth_adapters.systems.platform.system", + return_value="Windows", + ), + patch("habluetooth.scanner.SYSTEM", "Windows"), + ): yield @pytest.fixture(name="no_adapters") def no_adapter_fixture(): """Fixture that mocks no adapters on Linux.""" - with patch( - "homeassistant.components.bluetooth.platform.system", - return_value="Linux", - ), patch( - "habluetooth.scanner.platform.system", - return_value="Linux", - ), patch( - "bluetooth_adapters.systems.platform.system", - return_value="Linux", - ), patch("habluetooth.scanner.SYSTEM", "Linux"), patch( - "bluetooth_adapters.systems.linux.LinuxAdapters.refresh", - ), patch( - "bluetooth_adapters.systems.linux.LinuxAdapters.adapters", - {}, + with ( + patch( + "homeassistant.components.bluetooth.platform.system", + return_value="Linux", + ), + patch( + "habluetooth.scanner.platform.system", + return_value="Linux", + ), + patch( + "bluetooth_adapters.systems.platform.system", + return_value="Linux", + ), + patch("habluetooth.scanner.SYSTEM", "Linux"), + patch( + "bluetooth_adapters.systems.linux.LinuxAdapters.refresh", + ), + patch( + "bluetooth_adapters.systems.linux.LinuxAdapters.adapters", + {}, + ), ): yield @@ -117,31 +139,38 @@ def no_adapter_fixture(): @pytest.fixture(name="one_adapter") def one_adapter_fixture(): """Fixture that mocks one adapter on Linux.""" - with patch( - "homeassistant.components.bluetooth.platform.system", - return_value="Linux", - ), patch( - "habluetooth.scanner.platform.system", - return_value="Linux", - ), patch( - "bluetooth_adapters.systems.platform.system", - return_value="Linux", - ), patch("habluetooth.scanner.SYSTEM", "Linux"), patch( - "bluetooth_adapters.systems.linux.LinuxAdapters.refresh", - ), patch( - "bluetooth_adapters.systems.linux.LinuxAdapters.adapters", - { - "hci0": { - "address": "00:00:00:00:00:01", - "hw_version": "usb:v1D6Bp0246d053F", - "passive_scan": True, - "sw_version": "homeassistant", - "manufacturer": "ACME", - "product": "Bluetooth Adapter 5.0", - "product_id": "aa01", - "vendor_id": "cc01", + with ( + patch( + "homeassistant.components.bluetooth.platform.system", + return_value="Linux", + ), + patch( + "habluetooth.scanner.platform.system", + return_value="Linux", + ), + patch( + "bluetooth_adapters.systems.platform.system", + return_value="Linux", + ), + patch("habluetooth.scanner.SYSTEM", "Linux"), + patch( + "bluetooth_adapters.systems.linux.LinuxAdapters.refresh", + ), + patch( + "bluetooth_adapters.systems.linux.LinuxAdapters.adapters", + { + "hci0": { + "address": "00:00:00:00:00:01", + "hw_version": "usb:v1D6Bp0246d053F", + "passive_scan": True, + "sw_version": "homeassistant", + "manufacturer": "ACME", + "product": "Bluetooth Adapter 5.0", + "product_id": "aa01", + "vendor_id": "cc01", + }, }, - }, + ), ): yield @@ -149,39 +178,43 @@ def one_adapter_fixture(): @pytest.fixture(name="two_adapters") def two_adapters_fixture(): """Fixture that mocks two adapters on Linux.""" - with patch( - "homeassistant.components.bluetooth.platform.system", return_value="Linux" - ), patch( - "habluetooth.scanner.platform.system", - return_value="Linux", - ), patch("bluetooth_adapters.systems.platform.system", return_value="Linux"), patch( - "bluetooth_adapters.systems.linux.LinuxAdapters.refresh" - ), patch( - "bluetooth_adapters.systems.linux.LinuxAdapters.adapters", - { - "hci0": { - "address": "00:00:00:00:00:01", - "hw_version": "usb:v1D6Bp0246d053F", - "passive_scan": False, - "sw_version": "homeassistant", - "manufacturer": "ACME", - "product": "Bluetooth Adapter 5.0", - "product_id": "aa01", - "vendor_id": "cc01", - "connection_slots": 1, + with ( + patch( + "homeassistant.components.bluetooth.platform.system", return_value="Linux" + ), + patch( + "habluetooth.scanner.platform.system", + return_value="Linux", + ), + patch("bluetooth_adapters.systems.platform.system", return_value="Linux"), + patch("bluetooth_adapters.systems.linux.LinuxAdapters.refresh"), + patch( + "bluetooth_adapters.systems.linux.LinuxAdapters.adapters", + { + "hci0": { + "address": "00:00:00:00:00:01", + "hw_version": "usb:v1D6Bp0246d053F", + "passive_scan": False, + "sw_version": "homeassistant", + "manufacturer": "ACME", + "product": "Bluetooth Adapter 5.0", + "product_id": "aa01", + "vendor_id": "cc01", + "connection_slots": 1, + }, + "hci1": { + "address": "00:00:00:00:00:02", + "hw_version": "usb:v1D6Bp0246d053F", + "passive_scan": True, + "sw_version": "homeassistant", + "manufacturer": "ACME", + "product": "Bluetooth Adapter 5.0", + "product_id": "aa01", + "vendor_id": "cc01", + "connection_slots": 2, + }, }, - "hci1": { - "address": "00:00:00:00:00:02", - "hw_version": "usb:v1D6Bp0246d053F", - "passive_scan": True, - "sw_version": "homeassistant", - "manufacturer": "ACME", - "product": "Bluetooth Adapter 5.0", - "product_id": "aa01", - "vendor_id": "cc01", - "connection_slots": 2, - }, - }, + ), ): yield @@ -189,27 +222,31 @@ def two_adapters_fixture(): @pytest.fixture(name="one_adapter_old_bluez") def one_adapter_old_bluez(): """Fixture that mocks two adapters on Linux.""" - with patch( - "homeassistant.components.bluetooth.platform.system", return_value="Linux" - ), patch( - "habluetooth.scanner.platform.system", - return_value="Linux", - ), patch("bluetooth_adapters.systems.platform.system", return_value="Linux"), patch( - "bluetooth_adapters.systems.linux.LinuxAdapters.refresh" - ), patch( - "bluetooth_adapters.systems.linux.LinuxAdapters.adapters", - { - "hci0": { - "address": "00:00:00:00:00:01", - "hw_version": "usb:v1D6Bp0246d053F", - "passive_scan": False, - "sw_version": "homeassistant", - "manufacturer": "ACME", - "product": "Bluetooth Adapter 5.0", - "product_id": "aa01", - "vendor_id": "cc01", + with ( + patch( + "homeassistant.components.bluetooth.platform.system", return_value="Linux" + ), + patch( + "habluetooth.scanner.platform.system", + return_value="Linux", + ), + patch("bluetooth_adapters.systems.platform.system", return_value="Linux"), + patch("bluetooth_adapters.systems.linux.LinuxAdapters.refresh"), + patch( + "bluetooth_adapters.systems.linux.LinuxAdapters.adapters", + { + "hci0": { + "address": "00:00:00:00:00:01", + "hw_version": "usb:v1D6Bp0246d053F", + "passive_scan": False, + "sw_version": "homeassistant", + "manufacturer": "ACME", + "product": "Bluetooth Adapter 5.0", + "product_id": "aa01", + "vendor_id": "cc01", + }, }, - }, + ), ): yield diff --git a/tests/components/bluetooth/test_active_update_coordinator.py b/tests/components/bluetooth/test_active_update_coordinator.py index 83fee1456cd..e3178f84336 100644 --- a/tests/components/bluetooth/test_active_update_coordinator.py +++ b/tests/components/bluetooth/test_active_update_coordinator.py @@ -1,4 +1,5 @@ """Tests for the Bluetooth integration ActiveBluetoothDataUpdateCoordinator.""" + from __future__ import annotations import asyncio @@ -128,7 +129,7 @@ async def test_basic_usage( cancel = coordinator.async_start() inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert coordinator.passive_data == {"rssi": GENERIC_BLUETOOTH_SERVICE_INFO.rssi} assert coordinator.data == {"fake": "data"} @@ -174,13 +175,13 @@ async def test_bleak_error_during_polling( cancel = coordinator.async_start() inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert coordinator.passive_data == {"rssi": GENERIC_BLUETOOTH_SERVICE_INFO.rssi} assert coordinator.data is None assert coordinator.last_poll_successful is False inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO_2) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert coordinator.passive_data == {"rssi": GENERIC_BLUETOOTH_SERVICE_INFO_2.rssi} assert coordinator.data == {"fake": "data"} assert coordinator.last_poll_successful is True @@ -227,13 +228,13 @@ async def test_generic_exception_during_polling( cancel = coordinator.async_start() inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert coordinator.passive_data == {"rssi": GENERIC_BLUETOOTH_SERVICE_INFO.rssi} assert coordinator.data is None assert coordinator.last_poll_successful is False inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO_2) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert coordinator.passive_data == {"rssi": GENERIC_BLUETOOTH_SERVICE_INFO_2.rssi} assert coordinator.data == {"fake": "data"} assert coordinator.last_poll_successful is True @@ -279,7 +280,7 @@ async def test_polling_debounce( inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO) inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO_2) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert coordinator.passive_data == {"rssi": GENERIC_BLUETOOTH_SERVICE_INFO.rssi} # We should only get one poll because of the debounce assert coordinator.data == {"poll_count": 1} @@ -315,7 +316,9 @@ async def test_polling_debounce_with_custom_debouncer( mode=BluetoothScanningMode.ACTIVE, needs_poll_method=_needs_poll, poll_method=_poll_method, - poll_debouncer=Debouncer(hass, _LOGGER, cooldown=0.1, immediate=True), + poll_debouncer=Debouncer( + hass, _LOGGER, cooldown=0.1, immediate=True, background=True + ), ) assert coordinator.available is False # no data yet @@ -326,7 +329,7 @@ async def test_polling_debounce_with_custom_debouncer( inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO) inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO_2) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert coordinator.passive_data == {"rssi": GENERIC_BLUETOOTH_SERVICE_INFO.rssi} # We should only get one poll because of the debounce assert coordinator.data == {"poll_count": 1} @@ -370,25 +373,25 @@ async def test_polling_rejecting_the_first_time( cancel = coordinator.async_start() inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert coordinator.passive_data == {"rssi": GENERIC_BLUETOOTH_SERVICE_INFO.rssi} # First poll is rejected, so no data yet assert coordinator.data is None inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert coordinator.passive_data == {"rssi": GENERIC_BLUETOOTH_SERVICE_INFO.rssi} # Data is the same so no poll check assert coordinator.data is None inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO_2) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert coordinator.passive_data == {"rssi": GENERIC_BLUETOOTH_SERVICE_INFO_2.rssi} # Data is different so poll is done assert coordinator.data == {"fake": "data"} inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert coordinator.passive_data == {"rssi": GENERIC_BLUETOOTH_SERVICE_INFO.rssi} # Data is different again so poll is done assert coordinator.data == {"fake": "data"} @@ -433,19 +436,19 @@ async def test_no_polling_after_stop_event( assert needs_poll_calls == 0 inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert coordinator.passive_data == {"rssi": GENERIC_BLUETOOTH_SERVICE_INFO.rssi} assert coordinator.data == {"fake": "data"} assert needs_poll_calls == 1 hass.set_state(CoreState.stopping) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert needs_poll_calls == 1 # Should not generate a poll now inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO_2) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert needs_poll_calls == 1 cancel() diff --git a/tests/components/bluetooth/test_active_update_processor.py b/tests/components/bluetooth/test_active_update_processor.py index 00562a20daf..e854233451e 100644 --- a/tests/components/bluetooth/test_active_update_processor.py +++ b/tests/components/bluetooth/test_active_update_processor.py @@ -1,4 +1,5 @@ """Tests for the Bluetooth integration PassiveBluetoothDataUpdateCoordinator.""" + from __future__ import annotations import asyncio @@ -83,7 +84,7 @@ async def test_basic_usage( cancel = coordinator.async_start() inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert coordinator.available is True @@ -126,10 +127,7 @@ async def test_poll_can_be_skipped( needs_poll_method=_poll_needed, poll_method=_poll, poll_debouncer=Debouncer( - hass, - _LOGGER, - cooldown=0, - immediate=True, + hass, _LOGGER, cooldown=0, immediate=True, background=True ), ) assert coordinator.available is False # no data yet @@ -141,19 +139,19 @@ async def test_poll_can_be_skipped( cancel = coordinator.async_start() inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert async_handle_update.mock_calls[-1] == call({"testdata": True}) flag = False inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO_2) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert async_handle_update.mock_calls[-1] == call({"testdata": None}, True) flag = True inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert async_handle_update.mock_calls[-1] == call({"testdata": True}) cancel() @@ -207,7 +205,7 @@ async def test_bleak_error_and_recover( # First poll fails inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert async_handle_update.mock_calls[-1] == call({"testdata": None}, False) assert ( @@ -218,7 +216,7 @@ async def test_bleak_error_and_recover( # Second poll works flag = False inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO_2) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert async_handle_update.mock_calls[-1] == call({"testdata": False}) cancel() @@ -271,13 +269,13 @@ async def test_poll_failure_and_recover( # First poll fails inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert async_handle_update.mock_calls[-1] == call({"testdata": None}, False) # Second poll works flag = False inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO_2) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert async_handle_update.mock_calls[-1] == call({"testdata": False}) cancel() @@ -328,8 +326,8 @@ async def test_second_poll_needed( # Second poll gets stuck behind first poll inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO_2) - await hass.async_block_till_done() - assert async_handle_update.mock_calls[-1] == call({"testdata": 1}) + await hass.async_block_till_done(wait_background_tasks=True) + assert async_handle_update.mock_calls[1] == call({"testdata": 1}) cancel() @@ -380,7 +378,7 @@ async def test_rate_limit( # Third poll gets stuck behind first poll doesn't get queued inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert async_handle_update.mock_calls[-1] == call({"testdata": 1}) cancel() @@ -424,7 +422,7 @@ async def test_no_polling_after_stop_event( cancel = coordinator.async_start() inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert needs_poll_calls == 1 assert coordinator.available is True @@ -437,12 +435,12 @@ async def test_no_polling_after_stop_event( assert async_handle_update.mock_calls[1] == call({"testdata": 1}) hass.set_state(CoreState.stopping) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert needs_poll_calls == 1 # Should not generate a poll now that CoreState is stopping inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO_2) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert needs_poll_calls == 1 cancel() diff --git a/tests/components/bluetooth/test_advertisement_tracker.py b/tests/components/bluetooth/test_advertisement_tracker.py index f90b82fc379..12d34e0a7bc 100644 --- a/tests/components/bluetooth/test_advertisement_tracker.py +++ b/tests/components/bluetooth/test_advertisement_tracker.py @@ -1,4 +1,5 @@ """Tests for the Bluetooth integration advertisement tracking.""" + from datetime import timedelta import time diff --git a/tests/components/bluetooth/test_api.py b/tests/components/bluetooth/test_api.py index bc65874b0cc..a3ec3814a92 100644 --- a/tests/components/bluetooth/test_api.py +++ b/tests/components/bluetooth/test_api.py @@ -1,4 +1,5 @@ """Tests for the Bluetooth integration API.""" + import time from bleak.backends.scanner import AdvertisementData, BLEDevice diff --git a/tests/components/bluetooth/test_base_scanner.py b/tests/components/bluetooth/test_base_scanner.py index e1d64115e86..0839c9c56a4 100644 --- a/tests/components/bluetooth/test_base_scanner.py +++ b/tests/components/bluetooth/test_base_scanner.py @@ -1,4 +1,5 @@ """Tests for the Bluetooth base scanner models.""" + from __future__ import annotations from datetime import timedelta @@ -347,11 +348,14 @@ async def test_restore_history_remote_adapter( if address != "E3:A5:63:3E:5E:23": timestamps[address] = now - with patch( - "bluetooth_adapters.systems.linux.LinuxAdapters.history", - {}, - ), patch( - "bluetooth_adapters.systems.linux.LinuxAdapters.refresh", + with ( + patch( + "bluetooth_adapters.systems.linux.LinuxAdapters.history", + {}, + ), + patch( + "bluetooth_adapters.systems.linux.LinuxAdapters.refresh", + ), ): assert await async_setup_component(hass, bluetooth.DOMAIN, {}) await hass.async_block_till_done() diff --git a/tests/components/bluetooth/test_config_flow.py b/tests/components/bluetooth/test_config_flow.py index 1489f349cf4..9ca674e2d32 100644 --- a/tests/components/bluetooth/test_config_flow.py +++ b/tests/components/bluetooth/test_config_flow.py @@ -1,4 +1,5 @@ """Test the bluetooth config flow.""" + from unittest.mock import MagicMock, patch from bluetooth_adapters import DEFAULT_ADDRESS, AdapterDetails @@ -54,11 +55,12 @@ async def test_async_step_user_macos(hass: HomeAssistant, macos_adapter: None) - ) assert result["type"] == FlowResultType.FORM assert result["step_id"] == "single_adapter" - with patch( - "homeassistant.components.bluetooth.async_setup", return_value=True - ), patch( - "homeassistant.components.bluetooth.async_setup_entry", return_value=True - ) as mock_setup_entry: + with ( + patch("homeassistant.components.bluetooth.async_setup", return_value=True), + 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={} ) @@ -79,11 +81,12 @@ async def test_async_step_user_linux_one_adapter( ) assert result["type"] == FlowResultType.FORM assert result["step_id"] == "single_adapter" - with patch( - "homeassistant.components.bluetooth.async_setup", return_value=True - ), patch( - "homeassistant.components.bluetooth.async_setup_entry", return_value=True - ) as mock_setup_entry: + with ( + patch("homeassistant.components.bluetooth.async_setup", return_value=True), + 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={} ) @@ -104,11 +107,12 @@ async def test_async_step_user_linux_two_adapters( ) assert result["type"] == FlowResultType.FORM assert result["step_id"] == "multiple_adapters" - with patch( - "homeassistant.components.bluetooth.async_setup", return_value=True - ), patch( - "homeassistant.components.bluetooth.async_setup_entry", return_value=True - ) as mock_setup_entry: + with ( + patch("homeassistant.components.bluetooth.async_setup", return_value=True), + 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={CONF_ADAPTER: "hci1"} ) @@ -150,11 +154,12 @@ async def test_async_step_integration_discovery(hass: HomeAssistant) -> None: ) assert result["type"] == FlowResultType.FORM assert result["step_id"] == "single_adapter" - with patch( - "homeassistant.components.bluetooth.async_setup", return_value=True - ), patch( - "homeassistant.components.bluetooth.async_setup_entry", return_value=True - ) as mock_setup_entry: + with ( + patch("homeassistant.components.bluetooth.async_setup", return_value=True), + 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={} ) @@ -175,14 +180,16 @@ async def test_async_step_integration_discovery_during_onboarding_one_adapter( manufacturer="ACME", ) - with patch( - "homeassistant.components.bluetooth.async_setup", return_value=True - ), 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: + with ( + patch("homeassistant.components.bluetooth.async_setup", return_value=True), + 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}, @@ -212,14 +219,16 @@ async def test_async_step_integration_discovery_during_onboarding_two_adapters( manufacturer="ACME", ) - with patch( - "homeassistant.components.bluetooth.async_setup", return_value=True - ), 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: + with ( + patch("homeassistant.components.bluetooth.async_setup", return_value=True), + 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}, @@ -253,14 +262,16 @@ async def test_async_step_integration_discovery_during_onboarding( manufacturer="ACME", ) - with patch( - "homeassistant.components.bluetooth.async_setup", return_value=True - ), 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: + with ( + patch("homeassistant.components.bluetooth.async_setup", return_value=True), + 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}, diff --git a/tests/components/bluetooth/test_diagnostics.py b/tests/components/bluetooth/test_diagnostics.py index eae5f6507ac..3d29080d56c 100644 --- a/tests/components/bluetooth/test_diagnostics.py +++ b/tests/components/bluetooth/test_diagnostics.py @@ -1,4 +1,5 @@ """Test bluetooth diagnostics.""" + from unittest.mock import ANY, MagicMock, patch from bleak.backends.scanner import AdvertisementData, BLEDevice @@ -56,27 +57,30 @@ async def test_diagnostics( # error if the test is not running on linux since we won't have the correct # deps installed when testing on MacOS. - with patch( - "homeassistant.components.bluetooth.diagnostics.platform.system", - return_value="Linux", - ), patch( - "homeassistant.components.bluetooth.diagnostics.get_dbus_managed_objects", - return_value={ - "org.bluez": { - "/org/bluez/hci0": { - "org.bluez.Adapter1": { - "Name": "BlueZ 5.63", - "Alias": "BlueZ 5.63", - "Modalias": "usb:v1D6Bp0246d0540", - "Discovering": False, - }, - "org.bluez.AdvertisementMonitorManager1": { - "SupportedMonitorTypes": ["or_patterns"], - "SupportedFeatures": [], - }, + with ( + patch( + "homeassistant.components.bluetooth.diagnostics.platform.system", + return_value="Linux", + ), + patch( + "homeassistant.components.bluetooth.diagnostics.get_dbus_managed_objects", + return_value={ + "org.bluez": { + "/org/bluez/hci0": { + "org.bluez.Adapter1": { + "Name": "BlueZ 5.63", + "Alias": "BlueZ 5.63", + "Modalias": "usb:v1D6Bp0246d0540", + "Discovering": False, + }, + "org.bluez.AdvertisementMonitorManager1": { + "SupportedMonitorTypes": ["or_patterns"], + "SupportedFeatures": [], + }, + } } - } - }, + }, + ), ): entry2 = MockConfigEntry( domain=bluetooth.DOMAIN, data={}, unique_id="00:00:00:00:00:02" @@ -235,12 +239,15 @@ async def test_diagnostics_macos( local_name="wohand", service_uuids=[], manufacturer_data={1: b"\x01"} ) - with patch( - "homeassistant.components.bluetooth.diagnostics.platform.system", - return_value="Darwin", - ), patch( - "homeassistant.components.bluetooth.diagnostics.get_dbus_managed_objects", - return_value={}, + with ( + patch( + "homeassistant.components.bluetooth.diagnostics.platform.system", + return_value="Darwin", + ), + patch( + "homeassistant.components.bluetooth.diagnostics.get_dbus_managed_objects", + return_value={}, + ), ): entry1 = MockConfigEntry( domain=bluetooth.DOMAIN, @@ -413,12 +420,15 @@ async def test_diagnostics_remote_adapter( MONOTONIC_TIME(), ) - with patch( - "homeassistant.components.bluetooth.diagnostics.platform.system", - return_value="Linux", - ), patch( - "homeassistant.components.bluetooth.diagnostics.get_dbus_managed_objects", - return_value={}, + with ( + patch( + "homeassistant.components.bluetooth.diagnostics.platform.system", + return_value="Linux", + ), + patch( + "homeassistant.components.bluetooth.diagnostics.get_dbus_managed_objects", + return_value={}, + ), ): entry1 = hass.config_entries.async_entries(bluetooth.DOMAIN)[0] connector = ( diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index e2003229213..e9198362d8f 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -1,4 +1,5 @@ """Tests for the Bluetooth integration.""" + import asyncio from datetime import timedelta import time @@ -65,9 +66,13 @@ async def test_setup_and_stop( 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"): + 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: {}} ) @@ -185,12 +190,17 @@ async def test_setup_and_stop_no_bluetooth( mock_bt = [ {"domain": "switchbot", "service_uuid": "cba20d00-224d-11e6-9fb8-0002a5d5c51b"} ] - with patch( - "habluetooth.scanner.OriginalBleakScanner", - side_effect=BleakError, - ) as mock_ha_bleak_scanner, patch( - "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt - ), patch("homeassistant.components.bluetooth.discovery_flow.async_create_flow"): + with ( + patch( + "habluetooth.scanner.OriginalBleakScanner", + side_effect=BleakError, + ) as mock_ha_bleak_scanner, + patch( + "homeassistant.components.bluetooth.async_get_bluetooth", + return_value=mock_bt, + ), + patch("homeassistant.components.bluetooth.discovery_flow.async_create_flow"), + ): await async_setup_with_one_adapter(hass) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -206,11 +216,15 @@ async def test_setup_and_stop_broken_bluetooth( ) -> None: """Test we fail gracefully when bluetooth/dbus is broken.""" mock_bt = [] - with patch( - "habluetooth.scanner.OriginalBleakScanner.start", - side_effect=BleakError, - ), patch( - "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt + with ( + patch( + "habluetooth.scanner.OriginalBleakScanner.start", + side_effect=BleakError, + ), + patch( + "homeassistant.components.bluetooth.async_get_bluetooth", + return_value=mock_bt, + ), ): await async_setup_with_default_adapter(hass) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -231,11 +245,16 @@ async def test_setup_and_stop_broken_bluetooth_hanging( async def _mock_hang(): await asyncio.sleep(1) - with patch.object(scanner, "START_TIMEOUT", 0), patch( - "habluetooth.scanner.OriginalBleakScanner.start", - side_effect=_mock_hang, - ), patch( - "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt + with ( + patch.object(scanner, "START_TIMEOUT", 0), + patch( + "habluetooth.scanner.OriginalBleakScanner.start", + side_effect=_mock_hang, + ), + patch( + "homeassistant.components.bluetooth.async_get_bluetooth", + return_value=mock_bt, + ), ): await async_setup_with_default_adapter(hass) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -251,11 +270,15 @@ async def test_setup_and_retry_adapter_not_yet_available( ) -> None: """Test we retry if the adapter is not yet available.""" mock_bt = [] - with patch( - "habluetooth.scanner.OriginalBleakScanner.start", - side_effect=BleakError, - ), patch( - "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt + with ( + patch( + "habluetooth.scanner.OriginalBleakScanner.start", + side_effect=BleakError, + ), + patch( + "homeassistant.components.bluetooth.async_get_bluetooth", + return_value=mock_bt, + ), ): await async_setup_with_default_adapter(hass) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -286,11 +309,15 @@ async def test_no_race_during_manual_reload_in_retry_state( ) -> None: """Test we can successfully reload when the entry is in a retry state.""" mock_bt = [] - with patch( - "habluetooth.scanner.OriginalBleakScanner.start", - side_effect=BleakError, - ), patch( - "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt + with ( + patch( + "habluetooth.scanner.OriginalBleakScanner.start", + side_effect=BleakError, + ), + patch( + "homeassistant.components.bluetooth.async_get_bluetooth", + return_value=mock_bt, + ), ): await async_setup_with_default_adapter(hass) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -322,11 +349,15 @@ async def test_calling_async_discovered_devices_no_bluetooth( ) -> None: """Test we fail gracefully when asking for discovered devices and there is no blueooth.""" mock_bt = [] - with patch( - "habluetooth.scanner.OriginalBleakScanner", - side_effect=FileNotFoundError, - ), patch( - "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt + with ( + patch( + "habluetooth.scanner.OriginalBleakScanner", + side_effect=FileNotFoundError, + ), + patch( + "homeassistant.components.bluetooth.async_get_bluetooth", + return_value=mock_bt, + ), ): await async_setup_with_default_adapter(hass) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -346,9 +377,13 @@ async def test_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: + with ( + patch( + "homeassistant.components.bluetooth.async_get_bluetooth", + return_value=mock_bt, + ), + patch.object(hass.config_entries.flow, "async_init") as mock_config_flow, + ): await async_setup_with_default_adapter(hass) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -443,9 +478,13 @@ async def test_discovery_match_by_service_uuid_connectable( "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: + with ( + patch( + "homeassistant.components.bluetooth.async_get_bluetooth", + return_value=mock_bt, + ), + patch.object(hass.config_entries.flow, "async_init") as mock_config_flow, + ): await async_setup_with_default_adapter(hass) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -490,9 +529,13 @@ async def test_discovery_match_by_service_uuid_not_connectable( "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: + with ( + patch( + "homeassistant.components.bluetooth.async_get_bluetooth", + return_value=mock_bt, + ), + patch.object(hass.config_entries.flow, "async_init") as mock_config_flow, + ): await async_setup_with_default_adapter(hass) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -535,9 +578,13 @@ async def test_discovery_match_by_name_connectable_false( "local_name": "Qingping*", } ] - with patch( - "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt - ), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow: + with ( + patch( + "homeassistant.components.bluetooth.async_get_bluetooth", + return_value=mock_bt, + ), + patch.object(hass.config_entries.flow, "async_init") as mock_config_flow, + ): await async_setup_with_default_adapter(hass) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -1062,9 +1109,13 @@ async def test_rediscovery( 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: + with ( + patch( + "homeassistant.components.bluetooth.async_get_bluetooth", + return_value=mock_bt, + ), + patch.object(hass.config_entries.flow, "async_init") as mock_config_flow, + ): await async_setup_with_default_adapter(hass) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -1103,11 +1154,20 @@ async def test_async_discovered_device_api( ) -> None: """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_and_advertisement_data", # Must patch before we setup - {"44:44:33:11:23:45": (MagicMock(address="44:44:33:11:23:45"), MagicMock())}, + with ( + patch( + "homeassistant.components.bluetooth.async_get_bluetooth", + return_value=mock_bt, + ), + patch( + "bleak.BleakScanner.discovered_devices_and_advertisement_data", # Must patch before we setup + { + "44:44:33:11:23:45": ( + MagicMock(address="44:44:33:11:23:45"), + MagicMock(), + ) + }, + ), ): assert not bluetooth.async_discovered_service_info(hass) assert not bluetooth.async_address_present(hass, "44:44:22:22:11:22") @@ -1206,9 +1266,13 @@ async def test_register_callbacks( """Fake subscriber for the BleakScanner.""" callbacks.append((service_info, change)) - with patch( - "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt - ), patch.object(hass.config_entries.flow, "async_init"): + with ( + patch( + "homeassistant.components.bluetooth.async_get_bluetooth", + return_value=mock_bt, + ), + patch.object(hass.config_entries.flow, "async_init"), + ): await async_setup_with_default_adapter(hass) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -1287,9 +1351,13 @@ async def test_register_callbacks_raises_exception( callbacks.append((service_info, change)) raise ValueError - with patch( - "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt - ), patch.object(hass.config_entries.flow, "async_init"): + with ( + patch( + "homeassistant.components.bluetooth.async_get_bluetooth", + return_value=mock_bt, + ), + patch.object(hass.config_entries.flow, "async_init"), + ): await async_setup_with_default_adapter(hass) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -2492,9 +2560,12 @@ async def test_wrapped_instance_with_broken_callbacks( hass: HomeAssistant, mock_bleak_scanner_start: MagicMock, enable_bluetooth: None ) -> None: """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"): + with ( + patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=[] + ), + patch.object(hass.config_entries.flow, "async_init"), + ): await async_setup_with_default_adapter(hass) with patch.object(hass.config_entries.flow, "async_init"): @@ -2674,11 +2745,20 @@ async def test_async_ble_device_from_address( ) -> None: """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_and_advertisement_data", # Must patch before we setup - {"44:44:33:11:23:45": (MagicMock(address="44:44:33:11:23:45"), MagicMock())}, + with ( + patch( + "homeassistant.components.bluetooth.async_get_bluetooth", + return_value=mock_bt, + ), + patch( + "bleak.BleakScanner.discovered_devices_and_advertisement_data", # Must patch before we setup + { + "44:44:33:11:23:45": ( + MagicMock(address="44:44:33:11:23:45"), + MagicMock(), + ) + }, + ), ): assert not bluetooth.async_discovered_service_info(hass) assert not bluetooth.async_address_present(hass, "44:44:22:22:11:22") @@ -2813,11 +2893,13 @@ async def test_auto_detect_bluetooth_adapters_linux_none_found( hass: HomeAssistant, ) -> None: """Test we auto detect bluetooth adapters on linux with no adapters found.""" - with patch( - "bluetooth_adapters.systems.platform.system", return_value="Linux" - ), patch("bluetooth_adapters.systems.linux.LinuxAdapters.refresh"), patch( - "bluetooth_adapters.systems.linux.LinuxAdapters.adapters", - {}, + with ( + patch("bluetooth_adapters.systems.platform.system", return_value="Linux"), + patch("bluetooth_adapters.systems.linux.LinuxAdapters.refresh"), + patch( + "bluetooth_adapters.systems.linux.LinuxAdapters.adapters", + {}, + ), ): assert await async_setup_component(hass, bluetooth.DOMAIN, {}) await hass.async_block_till_done() @@ -2922,24 +3004,26 @@ async def test_discover_new_usb_adapters( saved_callback() assert not hass.config_entries.flow.async_progress(DOMAIN) - with patch( - "bluetooth_adapters.systems.platform.system", return_value="Linux" - ), patch("bluetooth_adapters.systems.linux.LinuxAdapters.refresh"), patch( - "bluetooth_adapters.systems.linux.LinuxAdapters.adapters", - { - "hci0": { - "address": "00:00:00:00:00:01", - "hw_version": "usb:v1D6Bp0246d053F", - "passive_scan": False, - "sw_version": "homeassistant", + with ( + patch("bluetooth_adapters.systems.platform.system", return_value="Linux"), + patch("bluetooth_adapters.systems.linux.LinuxAdapters.refresh"), + patch( + "bluetooth_adapters.systems.linux.LinuxAdapters.adapters", + { + "hci0": { + "address": "00:00:00:00:00:01", + "hw_version": "usb:v1D6Bp0246d053F", + "passive_scan": False, + "sw_version": "homeassistant", + }, + "hci1": { + "address": "00:00:00:00:00:02", + "hw_version": "usb:v1D6Bp0246d053F", + "passive_scan": False, + "sw_version": "homeassistant", + }, }, - "hci1": { - "address": "00:00:00:00:00:02", - "hw_version": "usb:v1D6Bp0246d053F", - "passive_scan": False, - "sw_version": "homeassistant", - }, - }, + ), ): for wait_sec in range(10, 20): async_fire_time_changed( @@ -2978,11 +3062,13 @@ async def test_discover_new_usb_adapters_with_firmware_fallback_delay( saved_callback() assert not hass.config_entries.flow.async_progress(DOMAIN) - with patch( - "bluetooth_adapters.systems.platform.system", return_value="Linux" - ), patch("bluetooth_adapters.systems.linux.LinuxAdapters.refresh"), patch( - "bluetooth_adapters.systems.linux.LinuxAdapters.adapters", - {}, + with ( + patch("bluetooth_adapters.systems.platform.system", return_value="Linux"), + patch("bluetooth_adapters.systems.linux.LinuxAdapters.refresh"), + patch( + "bluetooth_adapters.systems.linux.LinuxAdapters.adapters", + {}, + ), ): async_fire_time_changed( hass, dt_util.utcnow() + timedelta(BLUETOOTH_DISCOVERY_COOLDOWN_SECONDS * 2) @@ -2991,24 +3077,26 @@ async def test_discover_new_usb_adapters_with_firmware_fallback_delay( assert len(hass.config_entries.flow.async_progress(DOMAIN)) == 0 - with patch( - "bluetooth_adapters.systems.platform.system", return_value="Linux" - ), patch("bluetooth_adapters.systems.linux.LinuxAdapters.refresh"), patch( - "bluetooth_adapters.systems.linux.LinuxAdapters.adapters", - { - "hci0": { - "address": "00:00:00:00:00:01", - "hw_version": "usb:v1D6Bp0246d053F", - "passive_scan": False, - "sw_version": "homeassistant", + with ( + patch("bluetooth_adapters.systems.platform.system", return_value="Linux"), + patch("bluetooth_adapters.systems.linux.LinuxAdapters.refresh"), + patch( + "bluetooth_adapters.systems.linux.LinuxAdapters.adapters", + { + "hci0": { + "address": "00:00:00:00:00:01", + "hw_version": "usb:v1D6Bp0246d053F", + "passive_scan": False, + "sw_version": "homeassistant", + }, + "hci1": { + "address": "00:00:00:00:00:02", + "hw_version": "usb:v1D6Bp0246d053F", + "passive_scan": False, + "sw_version": "homeassistant", + }, }, - "hci1": { - "address": "00:00:00:00:00:02", - "hw_version": "usb:v1D6Bp0246d053F", - "passive_scan": False, - "sw_version": "homeassistant", - }, - }, + ), ): async_fire_time_changed( hass, diff --git a/tests/components/bluetooth/test_manager.py b/tests/components/bluetooth/test_manager.py index 4726c12f681..cb2be8a0e8d 100644 --- a/tests/components/bluetooth/test_manager.py +++ b/tests/components/bluetooth/test_manager.py @@ -1,4 +1,5 @@ """Tests for the Bluetooth integration manager.""" + from collections.abc import Generator from datetime import timedelta import time @@ -596,7 +597,7 @@ async def test_connectable_advertisement_can_be_retrieved_with_best_path_is_non_ ) -> None: """Test we can still get a connectable BLEDevice when the best path is non-connectable. - In this case the the device is closer to a non-connectable scanner, but the + In this case the device is closer to a non-connectable scanner, but the at least one connectable scanner has the device in range. """ @@ -1027,14 +1028,16 @@ async def test_goes_unavailable_dismisses_discovery_and_makes_discoverable( not in non_connectable_scanner.discovered_devices_and_advertisement_data ) monotonic_now = time.monotonic() - with patch.object( - hass.config_entries.flow, - "async_progress_by_init_data_type", - return_value=[{"flow_id": "mock_flow_id"}], - ) as mock_async_progress_by_init_data_type, patch.object( - hass.config_entries.flow, "async_abort" - ) as mock_async_abort, patch_bluetooth_time( - monotonic_now + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS, + with ( + patch.object( + hass.config_entries.flow, + "async_progress_by_init_data_type", + return_value=[{"flow_id": "mock_flow_id"}], + ) as mock_async_progress_by_init_data_type, + patch.object(hass.config_entries.flow, "async_abort") as mock_async_abort, + patch_bluetooth_time( + monotonic_now + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS, + ), ): async_fire_time_changed( hass, dt_util.utcnow() + timedelta(seconds=UNAVAILABLE_TRACK_SECONDS) diff --git a/tests/components/bluetooth/test_models.py b/tests/components/bluetooth/test_models.py index 680d7c2e798..087d443c5a0 100644 --- a/tests/components/bluetooth/test_models.py +++ b/tests/components/bluetooth/test_models.py @@ -1,4 +1,5 @@ """Tests for the Bluetooth integration models.""" + from __future__ import annotations from unittest.mock import patch @@ -115,10 +116,15 @@ async def test_wrapped_bleak_client_local_adapter_only( ) client = HaBleakClientWrapper(switchbot_device) - with patch( - "bleak.backends.bluezdbus.client.BleakClientBlueZDBus.connect", - return_value=True, - ), patch("bleak.backends.bluezdbus.client.BleakClientBlueZDBus.is_connected", True): + with ( + patch( + "bleak.backends.bluezdbus.client.BleakClientBlueZDBus.connect", + return_value=True, + ), + patch( + "bleak.backends.bluezdbus.client.BleakClientBlueZDBus.is_connected", True + ), + ): assert await client.connect() is True assert client.is_connected is True client.set_disconnected_callback(lambda client: None) @@ -200,10 +206,15 @@ async def test_wrapped_bleak_client_set_disconnected_callback_after_connected( "esp32_has_connection_slot", ) client = HaBleakClientWrapper(switchbot_proxy_device_has_connection_slot) - with patch( - "bleak.backends.bluezdbus.client.BleakClientBlueZDBus.connect", - return_value=True, - ), patch("bleak.backends.bluezdbus.client.BleakClientBlueZDBus.is_connected", True): + with ( + patch( + "bleak.backends.bluezdbus.client.BleakClientBlueZDBus.connect", + return_value=True, + ), + patch( + "bleak.backends.bluezdbus.client.BleakClientBlueZDBus.is_connected", True + ), + ): assert await client.connect() is True assert client.is_connected is True client.set_disconnected_callback(lambda client: None) @@ -239,9 +250,10 @@ async def test_ble_device_with_proxy_client_out_of_connections_no_scanners( ] client = HaBleakClientWrapper(switchbot_proxy_device_no_connection_slot) - with patch( - "bleak.backends.bluezdbus.client.BleakClientBlueZDBus.connect" - ), pytest.raises(BleakError): + with ( + patch("bleak.backends.bluezdbus.client.BleakClientBlueZDBus.connect"), + pytest.raises(BleakError), + ): await client.connect() assert client.is_connected is False client.set_disconnected_callback(lambda client: None) @@ -303,9 +315,10 @@ async def test_ble_device_with_proxy_client_out_of_connections( ] client = HaBleakClientWrapper(switchbot_proxy_device_no_connection_slot) - with patch( - "bleak.backends.bluezdbus.client.BleakClientBlueZDBus.connect" - ), pytest.raises(BleakError): + with ( + patch("bleak.backends.bluezdbus.client.BleakClientBlueZDBus.connect"), + pytest.raises(BleakError), + ): await client.connect() assert client.is_connected is False client.set_disconnected_callback(lambda client: None) diff --git a/tests/components/bluetooth/test_passive_update_coordinator.py b/tests/components/bluetooth/test_passive_update_coordinator.py index b6e50ebc565..54d4f8d5662 100644 --- a/tests/components/bluetooth/test_passive_update_coordinator.py +++ b/tests/components/bluetooth/test_passive_update_coordinator.py @@ -1,4 +1,5 @@ """Tests for the Bluetooth integration PassiveBluetoothDataUpdateCoordinator.""" + from __future__ import annotations from datetime import timedelta @@ -163,8 +164,9 @@ async def test_unavailable_callbacks_mark_the_coordinator_unavailable( monotonic_now = start_monotonic + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1 - with patch_bluetooth_time(monotonic_now), patch_all_discovered_devices( - [MagicMock(address="44:44:33:11:23:45")] + with ( + patch_bluetooth_time(monotonic_now), + patch_all_discovered_devices([MagicMock(address="44:44:33:11:23:45")]), ): async_fire_time_changed( hass, @@ -179,9 +181,12 @@ async def test_unavailable_callbacks_mark_the_coordinator_unavailable( monotonic_now = start_monotonic + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 2 - with patch_bluetooth_time( - monotonic_now, - ), patch_all_discovered_devices([MagicMock(address="44:44:33:11:23:45")]): + with ( + patch_bluetooth_time( + monotonic_now, + ), + patch_all_discovered_devices([MagicMock(address="44:44:33:11:23:45")]), + ): async_fire_time_changed( hass, dt_util.utcnow() diff --git a/tests/components/bluetooth/test_passive_update_processor.py b/tests/components/bluetooth/test_passive_update_processor.py index 4c9aa7a94b8..3578e2e6f6f 100644 --- a/tests/components/bluetooth/test_passive_update_processor.py +++ b/tests/components/bluetooth/test_passive_update_processor.py @@ -1,4 +1,5 @@ """Tests for the Bluetooth integration.""" + from __future__ import annotations from datetime import timedelta @@ -472,9 +473,12 @@ async def test_unavailable_after_no_data( assert processor.available is True monotonic_now = start_monotonic + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1 - with patch_bluetooth_time( - monotonic_now, - ), patch_all_discovered_devices([MagicMock(address="44:44:33:11:23:45")]): + with ( + patch_bluetooth_time( + monotonic_now, + ), + patch_all_discovered_devices([MagicMock(address="44:44:33:11:23:45")]), + ): async_fire_time_changed( hass, dt_util.utcnow() + timedelta(seconds=UNAVAILABLE_TRACK_SECONDS) ) @@ -490,9 +494,12 @@ async def test_unavailable_after_no_data( monotonic_now = start_monotonic + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 2 - with patch_bluetooth_time( - monotonic_now, - ), patch_all_discovered_devices([MagicMock(address="44:44:33:11:23:45")]): + with ( + patch_bluetooth_time( + monotonic_now, + ), + patch_all_discovered_devices([MagicMock(address="44:44:33:11:23:45")]), + ): async_fire_time_changed( hass, dt_util.utcnow() + timedelta(seconds=UNAVAILABLE_TRACK_SECONDS) ) diff --git a/tests/components/bluetooth/test_scanner.py b/tests/components/bluetooth/test_scanner.py index 837c058fa6b..504122fb671 100644 --- a/tests/components/bluetooth/test_scanner.py +++ b/tests/components/bluetooth/test_scanner.py @@ -1,4 +1,5 @@ """Tests for the Bluetooth integration scanners.""" + import asyncio from datetime import timedelta import time @@ -65,9 +66,12 @@ async def test_dbus_socket_missing_in_container( ) -> None: """Test we handle dbus being missing in the container.""" - with patch("habluetooth.scanner.is_docker_env", return_value=True), patch( - "habluetooth.scanner.OriginalBleakScanner.start", - side_effect=FileNotFoundError, + with ( + patch("habluetooth.scanner.is_docker_env", return_value=True), + patch( + "habluetooth.scanner.OriginalBleakScanner.start", + side_effect=FileNotFoundError, + ), ): await async_setup_with_one_adapter(hass) @@ -85,9 +89,12 @@ async def test_dbus_socket_missing( ) -> None: """Test we handle dbus being missing.""" - with patch("habluetooth.scanner.is_docker_env", return_value=False), patch( - "habluetooth.scanner.OriginalBleakScanner.start", - side_effect=FileNotFoundError, + with ( + patch("habluetooth.scanner.is_docker_env", return_value=False), + patch( + "habluetooth.scanner.OriginalBleakScanner.start", + side_effect=FileNotFoundError, + ), ): await async_setup_with_one_adapter(hass) @@ -105,9 +112,12 @@ async def test_dbus_broken_pipe_in_container( ) -> None: """Test we handle dbus broken pipe in the container.""" - with patch("habluetooth.scanner.is_docker_env", return_value=True), patch( - "habluetooth.scanner.OriginalBleakScanner.start", - side_effect=BrokenPipeError, + with ( + patch("habluetooth.scanner.is_docker_env", return_value=True), + patch( + "habluetooth.scanner.OriginalBleakScanner.start", + side_effect=BrokenPipeError, + ), ): await async_setup_with_one_adapter(hass) @@ -126,9 +136,12 @@ async def test_dbus_broken_pipe( ) -> None: """Test we handle dbus broken pipe.""" - with patch("habluetooth.scanner.is_docker_env", return_value=False), patch( - "habluetooth.scanner.OriginalBleakScanner.start", - side_effect=BrokenPipeError, + with ( + patch("habluetooth.scanner.is_docker_env", return_value=False), + patch( + "habluetooth.scanner.OriginalBleakScanner.start", + side_effect=BrokenPipeError, + ), ): await async_setup_with_one_adapter(hass) @@ -167,12 +180,15 @@ async def test_adapter_needs_reset_at_start( ) -> None: """Test we cycle the adapter when it needs a restart.""" - with patch( - "habluetooth.scanner.OriginalBleakScanner.start", - side_effect=[BleakError(error), None], - ), patch( - "habluetooth.util.recover_adapter", return_value=True - ) as mock_recover_adapter: + with ( + patch( + "habluetooth.scanner.OriginalBleakScanner.start", + side_effect=[BleakError(error), None], + ), + patch( + "habluetooth.util.recover_adapter", return_value=True + ) as mock_recover_adapter, + ): await async_setup_with_one_adapter(hass) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -298,11 +314,14 @@ async def test_adapter_recovery(hass: HomeAssistant, one_adapter: None) -> None: scanner = MockBleakScanner() start_time_monotonic = time.monotonic() - with patch_bluetooth_time( - start_time_monotonic, - ), patch( - "habluetooth.scanner.OriginalBleakScanner", - return_value=scanner, + with ( + patch_bluetooth_time( + start_time_monotonic, + ), + patch( + "habluetooth.scanner.OriginalBleakScanner", + return_value=scanner, + ), ): await async_setup_with_one_adapter(hass) @@ -330,13 +349,16 @@ async def test_adapter_recovery(hass: HomeAssistant, one_adapter: None) -> None: assert called_start == 1 # We hit the timer with no detections, so we reset the adapter and restart the scanner - with patch_bluetooth_time( - start_time_monotonic - + SCANNER_WATCHDOG_TIMEOUT - + SCANNER_WATCHDOG_INTERVAL.total_seconds(), - ), patch( - "habluetooth.util.recover_adapter", return_value=True - ) as mock_recover_adapter: + with ( + patch_bluetooth_time( + start_time_monotonic + + SCANNER_WATCHDOG_TIMEOUT + + SCANNER_WATCHDOG_INTERVAL.total_seconds(), + ), + patch( + "habluetooth.util.recover_adapter", return_value=True + ) as mock_recover_adapter, + ): async_fire_time_changed(hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL) await hass.async_block_till_done() @@ -383,11 +405,14 @@ async def test_adapter_scanner_fails_to_start_first_time( scanner = MockBleakScanner() start_time_monotonic = time.monotonic() - with patch_bluetooth_time( - start_time_monotonic, - ), patch( - "habluetooth.scanner.OriginalBleakScanner", - return_value=scanner, + with ( + patch_bluetooth_time( + start_time_monotonic, + ), + patch( + "habluetooth.scanner.OriginalBleakScanner", + return_value=scanner, + ), ): await async_setup_with_one_adapter(hass) @@ -415,13 +440,16 @@ async def test_adapter_scanner_fails_to_start_first_time( assert called_start == 1 # We hit the timer with no detections, so we reset the adapter and restart the scanner - with patch_bluetooth_time( - start_time_monotonic - + SCANNER_WATCHDOG_TIMEOUT - + SCANNER_WATCHDOG_INTERVAL.total_seconds(), - ), patch( - "habluetooth.util.recover_adapter", return_value=True - ) as mock_recover_adapter: + with ( + patch_bluetooth_time( + start_time_monotonic + + SCANNER_WATCHDOG_TIMEOUT + + SCANNER_WATCHDOG_INTERVAL.total_seconds(), + ), + patch( + "habluetooth.util.recover_adapter", return_value=True + ) as mock_recover_adapter, + ): async_fire_time_changed(hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL) await hass.async_block_till_done() @@ -430,13 +458,16 @@ async def test_adapter_scanner_fails_to_start_first_time( # We hit the timer again the previous start call failed, make sure # we try again - with patch_bluetooth_time( - start_time_monotonic - + SCANNER_WATCHDOG_TIMEOUT - + SCANNER_WATCHDOG_INTERVAL.total_seconds(), - ), patch( - "habluetooth.util.recover_adapter", return_value=True - ) as mock_recover_adapter: + with ( + patch_bluetooth_time( + start_time_monotonic + + SCANNER_WATCHDOG_TIMEOUT + + SCANNER_WATCHDOG_INTERVAL.total_seconds(), + ), + patch( + "habluetooth.util.recover_adapter", return_value=True + ) as mock_recover_adapter, + ): async_fire_time_changed(hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL) await hass.async_block_till_done() @@ -489,17 +520,22 @@ async def test_adapter_fails_to_start_and_takes_a_bit_to_init( scanner = MockBleakScanner() start_time_monotonic = time.monotonic() - with patch( - "habluetooth.scanner.ADAPTER_INIT_TIME", - 0, - ), patch_bluetooth_time( - start_time_monotonic, - ), patch( - "habluetooth.scanner.OriginalBleakScanner", - return_value=scanner, - ), patch( - "habluetooth.util.recover_adapter", return_value=True - ) as mock_recover_adapter: + with ( + patch( + "habluetooth.scanner.ADAPTER_INIT_TIME", + 0, + ), + patch_bluetooth_time( + start_time_monotonic, + ), + patch( + "habluetooth.scanner.OriginalBleakScanner", + return_value=scanner, + ), + patch( + "habluetooth.util.recover_adapter", return_value=True + ) as mock_recover_adapter, + ): await async_setup_with_one_adapter(hass) assert called_start == 3 @@ -539,15 +575,20 @@ async def test_restart_takes_longer_than_watchdog_time( scanner = MockBleakScanner() start_time_monotonic = time.monotonic() - with patch( - "habluetooth.scanner.ADAPTER_INIT_TIME", - 0, - ), patch_bluetooth_time( - start_time_monotonic, - ), patch( - "habluetooth.scanner.OriginalBleakScanner", - return_value=scanner, - ), patch("habluetooth.util.recover_adapter", return_value=True): + with ( + patch( + "habluetooth.scanner.ADAPTER_INIT_TIME", + 0, + ), + patch_bluetooth_time( + start_time_monotonic, + ), + patch( + "habluetooth.scanner.OriginalBleakScanner", + return_value=scanner, + ), + patch("habluetooth.util.recover_adapter", return_value=True), + ): await async_setup_with_one_adapter(hass) assert called_start == 1 diff --git a/tests/components/bluetooth/test_usage.py b/tests/components/bluetooth/test_usage.py index 0edff02aa0e..35aa0eb9022 100644 --- a/tests/components/bluetooth/test_usage.py +++ b/tests/components/bluetooth/test_usage.py @@ -1,4 +1,5 @@ """Tests for the Bluetooth integration.""" + from unittest.mock import patch import bleak diff --git a/tests/components/bluetooth/test_wrappers.py b/tests/components/bluetooth/test_wrappers.py index e3531a57447..0630d671038 100644 --- a/tests/components/bluetooth/test_wrappers.py +++ b/tests/components/bluetooth/test_wrappers.py @@ -1,4 +1,5 @@ """Tests for the Bluetooth integration.""" + from __future__ import annotations from contextlib import contextmanager @@ -206,11 +207,12 @@ async def test_test_switch_adapters_when_out_of_slots( hass ) # hci0 has 2 slots, hci1 has 1 slot - with patch.object( - manager.slot_manager, "release_slot" - ) as release_slot_mock, patch.object( - manager.slot_manager, "allocate_slot", return_value=True - ) as allocate_slot_mock: + with ( + patch.object(manager.slot_manager, "release_slot") as release_slot_mock, + patch.object( + manager.slot_manager, "allocate_slot", return_value=True + ) as allocate_slot_mock, + ): ble_device = hci0_device_advs["00:00:00:00:00:01"][0] client = bleak.BleakClient(ble_device) assert await client.connect() is True @@ -218,11 +220,12 @@ async def test_test_switch_adapters_when_out_of_slots( assert release_slot_mock.call_count == 0 # All adapters are out of slots - with patch.object( - manager.slot_manager, "release_slot" - ) as release_slot_mock, patch.object( - manager.slot_manager, "allocate_slot", return_value=False - ) as allocate_slot_mock: + with ( + patch.object(manager.slot_manager, "release_slot") as release_slot_mock, + patch.object( + manager.slot_manager, "allocate_slot", return_value=False + ) as allocate_slot_mock, + ): ble_device = hci0_device_advs["00:00:00:00:00:02"][0] client = bleak.BleakClient(ble_device) with pytest.raises(bleak.exc.BleakError): @@ -236,14 +239,15 @@ async def test_test_switch_adapters_when_out_of_slots( return True return False - with patch.object( - manager.slot_manager, "release_slot" - ) as release_slot_mock, patch.object( - manager.slot_manager, "allocate_slot", _allocate_slot_mock - ) as allocate_slot_mock: + with ( + patch.object(manager.slot_manager, "release_slot") as release_slot_mock, + patch.object( + manager.slot_manager, "allocate_slot", _allocate_slot_mock + ) as allocate_slot_mock, + ): ble_device = hci0_device_advs["00:00:00:00:00:03"][0] client = bleak.BleakClient(ble_device) - await client.connect() is True + assert await client.connect() is True assert release_slot_mock.call_count == 0 cancel_hci0() @@ -263,11 +267,12 @@ async def test_release_slot_on_connect_failure( hass ) # hci0 has 2 slots, hci1 has 1 slot - with patch.object( - manager.slot_manager, "release_slot" - ) as release_slot_mock, patch.object( - manager.slot_manager, "allocate_slot", return_value=True - ) as allocate_slot_mock: + with ( + patch.object(manager.slot_manager, "release_slot") as release_slot_mock, + patch.object( + manager.slot_manager, "allocate_slot", return_value=True + ) as allocate_slot_mock, + ): ble_device = hci0_device_advs["00:00:00:00:00:01"][0] client = bleak.BleakClient(ble_device) assert await client.connect() is False @@ -291,11 +296,12 @@ async def test_release_slot_on_connect_exception( hass ) # hci0 has 2 slots, hci1 has 1 slot - with patch.object( - manager.slot_manager, "release_slot" - ) as release_slot_mock, patch.object( - manager.slot_manager, "allocate_slot", return_value=True - ) as allocate_slot_mock: + with ( + patch.object(manager.slot_manager, "release_slot") as release_slot_mock, + patch.object( + manager.slot_manager, "allocate_slot", return_value=True + ) as allocate_slot_mock, + ): ble_device = hci0_device_advs["00:00:00:00:00:01"][0] client = bleak.BleakClient(ble_device) with pytest.raises(Exception): diff --git a/tests/components/bluetooth_le_tracker/test_device_tracker.py b/tests/components/bluetooth_le_tracker/test_device_tracker.py index 78ce96bde99..627f2ffadcc 100644 --- a/tests/components/bluetooth_le_tracker/test_device_tracker.py +++ b/tests/components/bluetooth_le_tracker/test_device_tracker.py @@ -1,4 +1,5 @@ """Test Bluetooth LE device tracker.""" + from datetime import timedelta from unittest.mock import patch @@ -102,6 +103,7 @@ async def test_do_not_see_device_if_time_not_updated( CONF_CONSIDER_HOME: timedelta(minutes=10), } result = await async_setup_component(hass, DOMAIN, {DOMAIN: config}) + await hass.async_block_till_done() assert result # Tick until device seen enough times for to be registered for tracking @@ -245,6 +247,7 @@ async def test_preserve_new_tracked_device_name( CONF_TRACK_NEW: True, } assert await async_setup_component(hass, DOMAIN, {DOMAIN: config}) + await hass.async_block_till_done() # Seen once here; return without name when seen subsequent times device = BluetoothServiceInfoBleak( @@ -314,6 +317,7 @@ async def test_tracking_battery_times_out( CONF_TRACK_NEW: True, } result = await async_setup_component(hass, DOMAIN, {DOMAIN: config}) + await hass.async_block_till_done() assert result # Tick until device seen enough times for to be registered for tracking @@ -448,6 +452,7 @@ async def test_tracking_battery_successful( CONF_TRACK_NEW: True, } result = await async_setup_component(hass, DOMAIN, {DOMAIN: config}) + await hass.async_block_till_done() assert result # Tick until device seen enough times for to be registered for tracking diff --git a/tests/components/bmw_connected_drive/__init__.py b/tests/components/bmw_connected_drive/__init__.py index 020e4c978ed..84384e6b482 100644 --- a/tests/components/bmw_connected_drive/__init__.py +++ b/tests/components/bmw_connected_drive/__init__.py @@ -1,6 +1,5 @@ """Tests for the for the BMW Connected Drive integration.""" - from bimmer_connected.const import REMOTE_SERVICE_BASE_URL, VEHICLE_CHARGING_BASE_URL import respx diff --git a/tests/components/bmw_connected_drive/conftest.py b/tests/components/bmw_connected_drive/conftest.py index 4191c7a4dd2..c3a89e28bd6 100644 --- a/tests/components/bmw_connected_drive/conftest.py +++ b/tests/components/bmw_connected_drive/conftest.py @@ -1,6 +1,5 @@ """Fixtures for BMW tests.""" - from collections.abc import Generator from bimmer_connected.tests import ALL_CHARGING_SETTINGS, ALL_STATES diff --git a/tests/components/bmw_connected_drive/snapshots/test_button.ambr b/tests/components/bmw_connected_drive/snapshots/test_button.ambr index 16d70c5f183..17866878ba3 100644 --- a/tests/components/bmw_connected_drive/snapshots/test_button.ambr +++ b/tests/components/bmw_connected_drive/snapshots/test_button.ambr @@ -9,6 +9,7 @@ 'context': , 'entity_id': 'button.ix_xdrive50_flash_lights', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -20,6 +21,7 @@ 'context': , 'entity_id': 'button.ix_xdrive50_sound_horn', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -31,6 +33,7 @@ 'context': , 'entity_id': 'button.ix_xdrive50_activate_air_conditioning', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -42,6 +45,7 @@ 'context': , 'entity_id': 'button.ix_xdrive50_deactivate_air_conditioning', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -53,6 +57,7 @@ 'context': , 'entity_id': 'button.ix_xdrive50_find_vehicle', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -64,6 +69,7 @@ 'context': , 'entity_id': 'button.i4_edrive40_flash_lights', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -75,6 +81,7 @@ 'context': , 'entity_id': 'button.i4_edrive40_sound_horn', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -86,6 +93,7 @@ 'context': , 'entity_id': 'button.i4_edrive40_activate_air_conditioning', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -97,6 +105,7 @@ 'context': , 'entity_id': 'button.i4_edrive40_deactivate_air_conditioning', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -108,6 +117,7 @@ 'context': , 'entity_id': 'button.i4_edrive40_find_vehicle', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -119,6 +129,7 @@ 'context': , 'entity_id': 'button.m340i_xdrive_flash_lights', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -130,6 +141,7 @@ 'context': , 'entity_id': 'button.m340i_xdrive_sound_horn', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -141,6 +153,7 @@ 'context': , 'entity_id': 'button.m340i_xdrive_activate_air_conditioning', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -152,6 +165,7 @@ 'context': , 'entity_id': 'button.m340i_xdrive_deactivate_air_conditioning', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -163,6 +177,7 @@ 'context': , 'entity_id': 'button.m340i_xdrive_find_vehicle', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -174,6 +189,7 @@ 'context': , 'entity_id': 'button.i3_rex_flash_lights', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -185,6 +201,7 @@ 'context': , 'entity_id': 'button.i3_rex_sound_horn', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -196,6 +213,7 @@ 'context': , 'entity_id': 'button.i3_rex_activate_air_conditioning', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -207,6 +225,7 @@ 'context': , 'entity_id': 'button.i3_rex_find_vehicle', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), diff --git a/tests/components/bmw_connected_drive/snapshots/test_number.ambr b/tests/components/bmw_connected_drive/snapshots/test_number.ambr index 1e478fafb09..93580ddc7b7 100644 --- a/tests/components/bmw_connected_drive/snapshots/test_number.ambr +++ b/tests/components/bmw_connected_drive/snapshots/test_number.ambr @@ -14,6 +14,7 @@ 'context': , 'entity_id': 'number.ix_xdrive50_target_soc', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '80', }), @@ -30,6 +31,7 @@ 'context': , 'entity_id': 'number.i4_edrive40_target_soc', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '80', }), diff --git a/tests/components/bmw_connected_drive/snapshots/test_select.ambr b/tests/components/bmw_connected_drive/snapshots/test_select.ambr index a8b5e4ecca6..e72708345b1 100644 --- a/tests/components/bmw_connected_drive/snapshots/test_select.ambr +++ b/tests/components/bmw_connected_drive/snapshots/test_select.ambr @@ -25,6 +25,7 @@ 'context': , 'entity_id': 'select.ix_xdrive50_ac_charging_limit', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '16', }), @@ -40,6 +41,7 @@ 'context': , 'entity_id': 'select.ix_xdrive50_charging_mode', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'IMMEDIATE_CHARGING', }), @@ -67,6 +69,7 @@ 'context': , 'entity_id': 'select.i4_edrive40_ac_charging_limit', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '16', }), @@ -82,6 +85,7 @@ 'context': , 'entity_id': 'select.i4_edrive40_charging_mode', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'IMMEDIATE_CHARGING', }), @@ -97,6 +101,7 @@ 'context': , 'entity_id': 'select.i3_rex_charging_mode', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'DELAYED_CHARGING', }), diff --git a/tests/components/bmw_connected_drive/snapshots/test_sensor.ambr b/tests/components/bmw_connected_drive/snapshots/test_sensor.ambr index b20189f5403..e28b4485af0 100644 --- a/tests/components/bmw_connected_drive/snapshots/test_sensor.ambr +++ b/tests/components/bmw_connected_drive/snapshots/test_sensor.ambr @@ -11,6 +11,7 @@ 'context': , 'entity_id': 'sensor.ix_xdrive50_remaining_range_total', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '340', }), @@ -24,6 +25,7 @@ 'context': , 'entity_id': 'sensor.ix_xdrive50_mileage', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '1121', }), @@ -36,6 +38,7 @@ 'context': , 'entity_id': 'sensor.ix_xdrive50_charging_end_time', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '2023-06-22T10:40:00+00:00', }), @@ -47,6 +50,7 @@ 'context': , 'entity_id': 'sensor.ix_xdrive50_charging_status', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'CHARGING', }), @@ -61,6 +65,7 @@ 'context': , 'entity_id': 'sensor.ix_xdrive50_remaining_battery_percent', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '70', }), @@ -74,6 +79,7 @@ 'context': , 'entity_id': 'sensor.ix_xdrive50_remaining_range_electric', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '340', }), @@ -86,6 +92,7 @@ 'context': , 'entity_id': 'sensor.ix_xdrive50_charging_target', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '80', }), @@ -99,6 +106,7 @@ 'context': , 'entity_id': 'sensor.i4_edrive40_remaining_range_total', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '472', }), @@ -112,6 +120,7 @@ 'context': , 'entity_id': 'sensor.i4_edrive40_mileage', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '1121', }), @@ -124,6 +133,7 @@ 'context': , 'entity_id': 'sensor.i4_edrive40_charging_end_time', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '2023-06-22T10:40:00+00:00', }), @@ -135,6 +145,7 @@ 'context': , 'entity_id': 'sensor.i4_edrive40_charging_status', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'NOT_CHARGING', }), @@ -149,6 +160,7 @@ 'context': , 'entity_id': 'sensor.i4_edrive40_remaining_battery_percent', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '80', }), @@ -162,6 +174,7 @@ 'context': , 'entity_id': 'sensor.i4_edrive40_remaining_range_electric', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '472', }), @@ -174,6 +187,7 @@ 'context': , 'entity_id': 'sensor.i4_edrive40_charging_target', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '80', }), @@ -187,6 +201,7 @@ 'context': , 'entity_id': 'sensor.m340i_xdrive_remaining_range_total', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '629', }), @@ -200,6 +215,7 @@ 'context': , 'entity_id': 'sensor.m340i_xdrive_mileage', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '1121', }), @@ -213,6 +229,7 @@ 'context': , 'entity_id': 'sensor.m340i_xdrive_remaining_fuel', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '40', }), @@ -226,6 +243,7 @@ 'context': , 'entity_id': 'sensor.m340i_xdrive_remaining_range_fuel', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '629', }), @@ -239,6 +257,7 @@ 'context': , 'entity_id': 'sensor.m340i_xdrive_remaining_fuel_percent', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '80', }), @@ -252,6 +271,7 @@ 'context': , 'entity_id': 'sensor.i3_rex_remaining_range_total', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '279', }), @@ -265,6 +285,7 @@ 'context': , 'entity_id': 'sensor.i3_rex_mileage', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '137009', }), @@ -277,6 +298,7 @@ 'context': , 'entity_id': 'sensor.i3_rex_charging_end_time', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -288,6 +310,7 @@ 'context': , 'entity_id': 'sensor.i3_rex_charging_status', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'WAITING_FOR_CHARGING', }), @@ -302,6 +325,7 @@ 'context': , 'entity_id': 'sensor.i3_rex_remaining_battery_percent', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '82', }), @@ -315,6 +339,7 @@ 'context': , 'entity_id': 'sensor.i3_rex_remaining_range_electric', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '174', }), @@ -327,6 +352,7 @@ 'context': , 'entity_id': 'sensor.i3_rex_charging_target', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '100', }), @@ -340,6 +366,7 @@ 'context': , 'entity_id': 'sensor.i3_rex_remaining_fuel', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '6', }), @@ -353,6 +380,7 @@ 'context': , 'entity_id': 'sensor.i3_rex_remaining_range_fuel', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '105', }), @@ -366,6 +394,7 @@ 'context': , 'entity_id': 'sensor.i3_rex_remaining_fuel_percent', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), diff --git a/tests/components/bmw_connected_drive/snapshots/test_switch.ambr b/tests/components/bmw_connected_drive/snapshots/test_switch.ambr index 009ff8306f0..a3c8ffb6d3b 100644 --- a/tests/components/bmw_connected_drive/snapshots/test_switch.ambr +++ b/tests/components/bmw_connected_drive/snapshots/test_switch.ambr @@ -9,6 +9,7 @@ 'context': , 'entity_id': 'switch.ix_xdrive50_climate', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }), @@ -20,6 +21,7 @@ 'context': , 'entity_id': 'switch.ix_xdrive50_charging', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }), @@ -31,6 +33,7 @@ 'context': , 'entity_id': 'switch.i4_edrive40_climate', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }), @@ -42,6 +45,7 @@ 'context': , 'entity_id': 'switch.m340i_xdrive_climate', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }), diff --git a/tests/components/bmw_connected_drive/test_button.py b/tests/components/bmw_connected_drive/test_button.py index 9cea5f2fd91..f55e199682f 100644 --- a/tests/components/bmw_connected_drive/test_button.py +++ b/tests/components/bmw_connected_drive/test_button.py @@ -1,4 +1,5 @@ """Test BMW buttons.""" + from unittest.mock import AsyncMock from bimmer_connected.models import MyBMWRemoteServiceError diff --git a/tests/components/bmw_connected_drive/test_config_flow.py b/tests/components/bmw_connected_drive/test_config_flow.py index 3540df851e9..ab7366e9da4 100644 --- a/tests/components/bmw_connected_drive/test_config_flow.py +++ b/tests/components/bmw_connected_drive/test_config_flow.py @@ -1,4 +1,5 @@ """Test the for the BMW Connected Drive config flow.""" + from copy import deepcopy from unittest.mock import patch @@ -100,14 +101,17 @@ async def test_api_error(hass: HomeAssistant) -> None: async def test_full_user_flow_implementation(hass: HomeAssistant) -> None: """Test registering an integration and finishing flow works.""" - with patch( - "bimmer_connected.api.authentication.MyBMWAuthentication.login", - side_effect=login_sideeffect, - autospec=True, - ), patch( - "homeassistant.components.bmw_connected_drive.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "bimmer_connected.api.authentication.MyBMWAuthentication.login", + side_effect=login_sideeffect, + autospec=True, + ), + patch( + "homeassistant.components.bmw_connected_drive.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, @@ -122,13 +126,16 @@ async def test_full_user_flow_implementation(hass: HomeAssistant) -> None: async def test_options_flow_implementation(hass: HomeAssistant) -> None: """Test config flow options.""" - with patch( - "bimmer_connected.account.MyBMWAccount.get_vehicles", - return_value=[], - ), patch( - "homeassistant.components.bmw_connected_drive.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "bimmer_connected.account.MyBMWAccount.get_vehicles", + return_value=[], + ), + patch( + "homeassistant.components.bmw_connected_drive.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): config_entry = MockConfigEntry(**FIXTURE_CONFIG_ENTRY) config_entry.add_to_hass(hass) @@ -155,14 +162,17 @@ async def test_options_flow_implementation(hass: HomeAssistant) -> None: async def test_reauth(hass: HomeAssistant) -> None: """Test the reauth form.""" - with patch( - "bimmer_connected.api.authentication.MyBMWAuthentication.login", - side_effect=login_sideeffect, - autospec=True, - ), patch( - "homeassistant.components.bmw_connected_drive.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "bimmer_connected.api.authentication.MyBMWAuthentication.login", + side_effect=login_sideeffect, + autospec=True, + ), + patch( + "homeassistant.components.bmw_connected_drive.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): wrong_password = "wrong" config_entry_with_wrong_password = deepcopy(FIXTURE_CONFIG_ENTRY) diff --git a/tests/components/bmw_connected_drive/test_coordinator.py b/tests/components/bmw_connected_drive/test_coordinator.py index bf47b7fed29..c449a9c4a59 100644 --- a/tests/components/bmw_connected_drive/test_coordinator.py +++ b/tests/components/bmw_connected_drive/test_coordinator.py @@ -1,4 +1,5 @@ """Test BMW coordinator.""" + from datetime import timedelta from unittest.mock import patch diff --git a/tests/components/bmw_connected_drive/test_diagnostics.py b/tests/components/bmw_connected_drive/test_diagnostics.py index 11c2b055f6d..2f58bc0e4a0 100644 --- a/tests/components/bmw_connected_drive/test_diagnostics.py +++ b/tests/components/bmw_connected_drive/test_diagnostics.py @@ -1,7 +1,6 @@ """Test BMW diagnostics.""" + import datetime -import os -import time import pytest from syrupy.assertion import SnapshotAssertion @@ -19,7 +18,7 @@ from tests.components.diagnostics import ( from tests.typing import ClientSessionGenerator -@pytest.mark.freeze_time(datetime.datetime(2022, 7, 10, 11)) +@pytest.mark.freeze_time(datetime.datetime(2022, 7, 10, 11, tzinfo=datetime.UTC)) async def test_config_entry_diagnostics( hass: HomeAssistant, hass_client: ClientSessionGenerator, @@ -28,10 +27,6 @@ async def test_config_entry_diagnostics( ) -> None: """Test config entry diagnostics.""" - # Make sure that local timezone for test is UTC - os.environ["TZ"] = "UTC" - time.tzset() - mock_config_entry = await setup_mocked_integration(hass) diagnostics = await get_diagnostics_for_config_entry( @@ -41,7 +36,7 @@ async def test_config_entry_diagnostics( assert diagnostics == snapshot -@pytest.mark.freeze_time(datetime.datetime(2022, 7, 10, 11)) +@pytest.mark.freeze_time(datetime.datetime(2022, 7, 10, 11, tzinfo=datetime.UTC)) async def test_device_diagnostics( hass: HomeAssistant, hass_client: ClientSessionGenerator, @@ -51,10 +46,6 @@ async def test_device_diagnostics( ) -> None: """Test device diagnostics.""" - # Make sure that local timezone for test is UTC - os.environ["TZ"] = "UTC" - time.tzset() - mock_config_entry = await setup_mocked_integration(hass) reg_device = device_registry.async_get_device( @@ -69,7 +60,7 @@ async def test_device_diagnostics( assert diagnostics == snapshot -@pytest.mark.freeze_time(datetime.datetime(2022, 7, 10, 11)) +@pytest.mark.freeze_time(datetime.datetime(2022, 7, 10, 11, tzinfo=datetime.UTC)) async def test_device_diagnostics_vehicle_not_found( hass: HomeAssistant, hass_client: ClientSessionGenerator, @@ -79,10 +70,6 @@ async def test_device_diagnostics_vehicle_not_found( ) -> None: """Test device diagnostics when the vehicle cannot be found.""" - # Make sure that local timezone for test is UTC - os.environ["TZ"] = "UTC" - time.tzset() - mock_config_entry = await setup_mocked_integration(hass) reg_device = device_registry.async_get_device( diff --git a/tests/components/bmw_connected_drive/test_init.py b/tests/components/bmw_connected_drive/test_init.py index c57bfc5a9b0..b8081d8d119 100644 --- a/tests/components/bmw_connected_drive/test_init.py +++ b/tests/components/bmw_connected_drive/test_init.py @@ -1,4 +1,5 @@ """Test Axis component setup process.""" + from unittest.mock import patch import pytest diff --git a/tests/components/bmw_connected_drive/test_number.py b/tests/components/bmw_connected_drive/test_number.py index bcd880fa0a6..30214555b92 100644 --- a/tests/components/bmw_connected_drive/test_number.py +++ b/tests/components/bmw_connected_drive/test_number.py @@ -1,4 +1,5 @@ """Test BMW numbers.""" + from unittest.mock import AsyncMock from bimmer_connected.models import MyBMWAPIError, MyBMWRemoteServiceError diff --git a/tests/components/bmw_connected_drive/test_select.py b/tests/components/bmw_connected_drive/test_select.py index 1860ed19720..cb20805c809 100644 --- a/tests/components/bmw_connected_drive/test_select.py +++ b/tests/components/bmw_connected_drive/test_select.py @@ -1,4 +1,5 @@ """Test BMW selects.""" + from unittest.mock import AsyncMock from bimmer_connected.models import MyBMWAPIError, MyBMWRemoteServiceError diff --git a/tests/components/bmw_connected_drive/test_sensor.py b/tests/components/bmw_connected_drive/test_sensor.py index c6cb12cf047..a066b967250 100644 --- a/tests/components/bmw_connected_drive/test_sensor.py +++ b/tests/components/bmw_connected_drive/test_sensor.py @@ -1,4 +1,5 @@ """Test BMW sensors.""" + from freezegun import freeze_time import pytest import respx diff --git a/tests/components/bmw_connected_drive/test_switch.py b/tests/components/bmw_connected_drive/test_switch.py index c050f4b6cc2..b759c33ca3b 100644 --- a/tests/components/bmw_connected_drive/test_switch.py +++ b/tests/components/bmw_connected_drive/test_switch.py @@ -1,4 +1,5 @@ """Test BMW switches.""" + from unittest.mock import AsyncMock from bimmer_connected.models import MyBMWAPIError, MyBMWRemoteServiceError diff --git a/tests/components/bond/common.py b/tests/components/bond/common.py index d97ef9a7a31..619aa03572a 100644 --- a/tests/components/bond/common.py +++ b/tests/components/bond/common.py @@ -1,4 +1,5 @@ """Common methods used across tests for Bond.""" + from __future__ import annotations from contextlib import nullcontext @@ -62,14 +63,16 @@ async def setup_bond_entity( """Set up Bond entity.""" config_entry.add_to_hass(hass) - with patch_start_bpup(), patch_bond_bridge(enabled=patch_bridge), patch_bond_token( - enabled=patch_token - ), patch_bond_version(enabled=patch_version), patch_bond_device_ids( - enabled=patch_device_ids - ), patch_setup_entry("cover", enabled=patch_platforms), patch_setup_entry( - "fan", enabled=patch_platforms - ), patch_setup_entry("light", enabled=patch_platforms), patch_setup_entry( - "switch", enabled=patch_platforms + with ( + patch_start_bpup(), + patch_bond_bridge(enabled=patch_bridge), + patch_bond_token(enabled=patch_token), + patch_bond_version(enabled=patch_version), + patch_bond_device_ids(enabled=patch_device_ids), + patch_setup_entry("cover", enabled=patch_platforms), + patch_setup_entry("fan", enabled=patch_platforms), + patch_setup_entry("light", enabled=patch_platforms), + patch_setup_entry("switch", enabled=patch_platforms), ): return await hass.config_entries.async_setup(config_entry.entry_id) @@ -93,16 +96,16 @@ async def setup_platform( ) mock_entry.add_to_hass(hass) - with patch( - "homeassistant.components.bond.PLATFORMS", [platform] - ), patch_bond_version(return_value=bond_version), patch_bond_bridge( - return_value=bridge - ), patch_bond_token(return_value=token), patch_bond_device_ids( - return_value=[bond_device_id] - ), patch_start_bpup(), patch_bond_device( - return_value=discovered_device - ), patch_bond_device_properties(return_value=props), patch_bond_device_state( - return_value=state + with ( + patch("homeassistant.components.bond.PLATFORMS", [platform]), + patch_bond_version(return_value=bond_version), + patch_bond_bridge(return_value=bridge), + patch_bond_token(return_value=token), + patch_bond_device_ids(return_value=[bond_device_id]), + patch_start_bpup(), + patch_bond_device(return_value=discovered_device), + patch_bond_device_properties(return_value=props), + patch_bond_device_state(return_value=state), ): assert await async_setup_component(hass, BOND_DOMAIN, {}) await hass.async_block_till_done() diff --git a/tests/components/bond/conftest.py b/tests/components/bond/conftest.py index 378c3340e29..d9067194e17 100644 --- a/tests/components/bond/conftest.py +++ b/tests/components/bond/conftest.py @@ -1,2 +1,3 @@ """bond conftest.""" + from tests.components.light.conftest import mock_light_profiles # noqa: F401 diff --git a/tests/components/bond/test_button.py b/tests/components/bond/test_button.py index 6984831626d..8c8f38db72b 100644 --- a/tests/components/bond/test_button.py +++ b/tests/components/bond/test_button.py @@ -1,4 +1,5 @@ """Tests for the Bond button device.""" + from bond_async import Action, DeviceType from homeassistant.components.bond.button import STEP_SIZE diff --git a/tests/components/bond/test_config_flow.py b/tests/components/bond/test_config_flow.py index 7d639309ddc..bfe61c536d9 100644 --- a/tests/components/bond/test_config_flow.py +++ b/tests/components/bond/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Bond config flow.""" + from __future__ import annotations from http import HTTPStatus @@ -37,11 +38,15 @@ async def test_user_form(hass: HomeAssistant) -> None: assert result["type"] == "form" assert result["errors"] == {} - with patch_bond_version( - return_value={"bondid": "ZXXX12345"} - ), patch_bond_device_ids( - return_value=["f6776c11", "f6776c12"] - ), patch_bond_bridge(), patch_bond_device_properties(), patch_bond_device(), patch_bond_device_state(), _patch_async_setup_entry() as mock_setup_entry: + with ( + patch_bond_version(return_value={"bondid": "ZXXX12345"}), + patch_bond_device_ids(return_value=["f6776c11", "f6776c12"]), + patch_bond_bridge(), + patch_bond_device_properties(), + patch_bond_device(), + patch_bond_device_state(), + _patch_async_setup_entry() as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_HOST: "some host", CONF_ACCESS_TOKEN: "test-token"}, @@ -66,17 +71,19 @@ async def test_user_form_with_non_bridge(hass: HomeAssistant) -> None: assert result["type"] == "form" assert result["errors"] == {} - with patch_bond_version( - return_value={"bondid": "KXXX12345"} - ), patch_bond_device_ids( - return_value=["f6776c11"] - ), patch_bond_device_properties(), patch_bond_device( - return_value={ - "name": "New Fan", - } - ), patch_bond_bridge( - return_value={} - ), patch_bond_device_state(), _patch_async_setup_entry() as mock_setup_entry: + with ( + patch_bond_version(return_value={"bondid": "KXXX12345"}), + patch_bond_device_ids(return_value=["f6776c11"]), + patch_bond_device_properties(), + patch_bond_device( + return_value={ + "name": "New Fan", + } + ), + patch_bond_bridge(return_value={}), + patch_bond_device_state(), + _patch_async_setup_entry() as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_HOST: "some host", CONF_ACCESS_TOKEN: "test-token"}, @@ -98,10 +105,12 @@ async def test_user_form_invalid_auth(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch_bond_version( - return_value={"bond_id": "ZXXX12345"} - ), patch_bond_bridge(), patch_bond_device_ids( - side_effect=ClientResponseError(Mock(), Mock(), status=401), + with ( + patch_bond_version(return_value={"bond_id": "ZXXX12345"}), + patch_bond_bridge(), + patch_bond_device_ids( + side_effect=ClientResponseError(Mock(), Mock(), status=401), + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -118,9 +127,11 @@ async def test_user_form_cannot_connect(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch_bond_version( - side_effect=ClientConnectionError() - ), patch_bond_bridge(), patch_bond_device_ids(): + with ( + patch_bond_version(side_effect=ClientConnectionError()), + patch_bond_bridge(), + patch_bond_device_ids(), + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_HOST: "some host", CONF_ACCESS_TOKEN: "test-token"}, @@ -136,9 +147,11 @@ async def test_user_form_old_firmware(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch_bond_version( - return_value={"no_bond_id": "present"} - ), patch_bond_bridge(), patch_bond_device_ids(): + with ( + patch_bond_version(return_value={"no_bond_id": "present"}), + patch_bond_bridge(), + patch_bond_device_ids(), + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_HOST: "some host", CONF_ACCESS_TOKEN: "test-token"}, @@ -180,9 +193,12 @@ async def test_user_form_one_entry_per_device_allowed(hass: HomeAssistant) -> No DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch_bond_version( - return_value={"bondid": "already-registered-bond-id"} - ), patch_bond_bridge(), patch_bond_device_ids(), _patch_async_setup_entry() as mock_setup_entry: + with ( + patch_bond_version(return_value={"bondid": "already-registered-bond-id"}), + patch_bond_bridge(), + patch_bond_device_ids(), + _patch_async_setup_entry() as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_HOST: "some host", CONF_ACCESS_TOKEN: "test-token"}, @@ -215,9 +231,12 @@ async def test_zeroconf_form(hass: HomeAssistant) -> None: assert result["type"] == "form" assert result["errors"] == {} - with patch_bond_version( - return_value={"bondid": "ZXXX12345"} - ), patch_bond_bridge(), patch_bond_device_ids(), _patch_async_setup_entry() as mock_setup_entry: + with ( + patch_bond_version(return_value={"bondid": "ZXXX12345"}), + patch_bond_bridge(), + patch_bond_device_ids(), + _patch_async_setup_entry() as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_ACCESS_TOKEN: "test-token"}, @@ -254,7 +273,12 @@ async def test_zeroconf_form_token_unavailable(hass: HomeAssistant) -> None: assert result["type"] == "form" assert result["errors"] == {} - with patch_bond_version(), patch_bond_bridge(), patch_bond_device_ids(), _patch_async_setup_entry() as mock_setup_entry: + with ( + patch_bond_version(), + patch_bond_bridge(), + patch_bond_device_ids(), + _patch_async_setup_entry() as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_ACCESS_TOKEN: "test-token"}, @@ -291,7 +315,12 @@ async def test_zeroconf_form_token_times_out(hass: HomeAssistant) -> None: assert result["type"] == "form" assert result["errors"] == {} - with patch_bond_version(), patch_bond_bridge(), patch_bond_device_ids(), _patch_async_setup_entry() as mock_setup_entry: + with ( + patch_bond_version(), + patch_bond_bridge(), + patch_bond_device_ids(), + _patch_async_setup_entry() as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_ACCESS_TOKEN: "test-token"}, @@ -310,11 +339,12 @@ async def test_zeroconf_form_token_times_out(hass: HomeAssistant) -> None: async def test_zeroconf_form_with_token_available(hass: HomeAssistant) -> None: """Test we get the discovery form when we can get the token.""" - with patch_bond_version(return_value={"bondid": "ZXXX12345"}), patch_bond_token( - return_value={"token": "discovered-token"} - ), patch_bond_bridge( - return_value={"name": "discovered-name"} - ), patch_bond_device_ids(): + with ( + patch_bond_version(return_value={"bondid": "ZXXX12345"}), + patch_bond_token(return_value={"token": "discovered-token"}), + patch_bond_bridge(return_value={"name": "discovered-name"}), + patch_bond_device_ids(), + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, @@ -353,9 +383,12 @@ async def test_zeroconf_form_with_token_available_name_unavailable( ) -> None: """Test we get the discovery form when we can get the token but the name is unavailable.""" - with patch_bond_version( - side_effect=ClientResponseError(Mock(), (), status=HTTPStatus.BAD_REQUEST) - ), patch_bond_token(return_value={"token": "discovered-token"}): + with ( + patch_bond_version( + side_effect=ClientResponseError(Mock(), (), status=HTTPStatus.BAD_REQUEST) + ), + patch_bond_token(return_value={"token": "discovered-token"}), + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, @@ -481,8 +514,9 @@ async def test_zeroconf_already_configured_refresh_token(hass: HomeAssistant) -> await hass.config_entries.async_setup(entry.entry_id) assert entry.state is ConfigEntryState.SETUP_ERROR - with _patch_async_setup_entry() as mock_setup_entry, patch_bond_token( - return_value={"token": "discovered-token"} + with ( + _patch_async_setup_entry() as mock_setup_entry, + patch_bond_token(return_value={"token": "discovered-token"}), ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -519,8 +553,9 @@ async def test_zeroconf_already_configured_no_reload_same_host( ) entry.add_to_hass(hass) - with _patch_async_setup_entry() as mock_setup_entry, patch_bond_token( - return_value={"token": "correct-token"} + with ( + _patch_async_setup_entry() as mock_setup_entry, + patch_bond_token(return_value={"token": "correct-token"}), ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -575,9 +610,10 @@ async def _help_test_form_unexpected_error( DOMAIN, context={"source": source}, data=initial_input ) - with patch_bond_version( - return_value={"bond_id": "ZXXX12345"} - ), patch_bond_device_ids(side_effect=error): + with ( + patch_bond_version(return_value={"bond_id": "ZXXX12345"}), + patch_bond_device_ids(side_effect=error), + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input ) diff --git a/tests/components/bond/test_cover.py b/tests/components/bond/test_cover.py index e489f8550d6..e438a830eb5 100644 --- a/tests/components/bond/test_cover.py +++ b/tests/components/bond/test_cover.py @@ -1,4 +1,5 @@ """Tests for the Bond cover device.""" + from datetime import timedelta from bond_async import Action, DeviceType @@ -264,8 +265,9 @@ async def test_set_position_cover(hass: HomeAssistant) -> None: bond_device_id="test-device-id", ) - with patch_bond_action() as mock_hold, patch_bond_device_state( - return_value={"position": 0, "open": 1} + with ( + patch_bond_action() as mock_hold, + patch_bond_device_state(return_value={"position": 0, "open": 1}), ): await hass.services.async_call( COVER_DOMAIN, @@ -281,8 +283,9 @@ async def test_set_position_cover(hass: HomeAssistant) -> None: assert entity_state.state == STATE_OPEN assert entity_state.attributes[ATTR_CURRENT_POSITION] == 100 - with patch_bond_action() as mock_hold, patch_bond_device_state( - return_value={"position": 100, "open": 0} + with ( + patch_bond_action() as mock_hold, + patch_bond_device_state(return_value={"position": 100, "open": 0}), ): await hass.services.async_call( COVER_DOMAIN, @@ -298,8 +301,9 @@ async def test_set_position_cover(hass: HomeAssistant) -> None: assert entity_state.state == STATE_CLOSED assert entity_state.attributes[ATTR_CURRENT_POSITION] == 0 - with patch_bond_action() as mock_hold, patch_bond_device_state( - return_value={"position": 40, "open": 1} + with ( + patch_bond_action() as mock_hold, + patch_bond_device_state(return_value={"position": 40, "open": 1}), ): await hass.services.async_call( COVER_DOMAIN, diff --git a/tests/components/bond/test_diagnostics.py b/tests/components/bond/test_diagnostics.py index f8d0313ee9c..d919c74178b 100644 --- a/tests/components/bond/test_diagnostics.py +++ b/tests/components/bond/test_diagnostics.py @@ -1,4 +1,5 @@ """Test bond diagnostics.""" + from homeassistant.components.fan import DOMAIN as FAN_DOMAIN from homeassistant.core import HomeAssistant diff --git a/tests/components/bond/test_entity.py b/tests/components/bond/test_entity.py index bcb61ddc92d..a2d38787ed1 100644 --- a/tests/components/bond/test_entity.py +++ b/tests/components/bond/test_entity.py @@ -1,4 +1,5 @@ """Tests for the Bond entities.""" + from datetime import timedelta from unittest.mock import patch diff --git a/tests/components/bond/test_fan.py b/tests/components/bond/test_fan.py index e202433c8d6..6a0160fbec9 100644 --- a/tests/components/bond/test_fan.py +++ b/tests/components/bond/test_fan.py @@ -1,4 +1,5 @@ """Tests for the Bond fan device.""" + from __future__ import annotations from datetime import timedelta @@ -252,13 +253,17 @@ async def test_turn_on_fan_preset_mode_not_supported(hass: HomeAssistant) -> Non props={"max_speed": 6}, ) - with patch_bond_action(), patch_bond_device_state(), pytest.raises( - NotValidPresetModeError + with ( + patch_bond_action(), + patch_bond_device_state(), + pytest.raises(NotValidPresetModeError), ): await turn_fan_on(hass, "fan.name_1", preset_mode=PRESET_MODE_BREEZE) - with patch_bond_action(), patch_bond_device_state(), pytest.raises( - NotValidPresetModeError + with ( + patch_bond_action(), + patch_bond_device_state(), + pytest.raises(NotValidPresetModeError), ): await hass.services.async_call( FAN_DOMAIN, @@ -380,9 +385,11 @@ async def test_set_speed_belief_speed_api_error(hass: HomeAssistant) -> None: hass, FAN_DOMAIN, ceiling_fan("name-1"), bond_device_id="test-device-id" ) - with pytest.raises( - HomeAssistantError - ), patch_bond_action_returns_clientresponseerror(), patch_bond_device_state(): + with ( + pytest.raises(HomeAssistantError), + patch_bond_action_returns_clientresponseerror(), + patch_bond_device_state(), + ): await hass.services.async_call( BOND_DOMAIN, SERVICE_SET_FAN_SPEED_TRACKED_STATE, diff --git a/tests/components/bond/test_init.py b/tests/components/bond/test_init.py index 6453fa39807..167cd9aa401 100644 --- a/tests/components/bond/test_init.py +++ b/tests/components/bond/test_init.py @@ -1,4 +1,5 @@ """Tests for the Bond module.""" + from unittest.mock import MagicMock, Mock from aiohttp import ClientConnectionError, ClientResponseError @@ -88,20 +89,21 @@ async def test_async_setup_entry_sets_up_hub_and_supported_domains( data={CONF_HOST: "some host", CONF_ACCESS_TOKEN: "test-token"}, ) - with patch_bond_bridge(), patch_bond_version( - return_value={ - "bondid": "ZXXX12345", - "target": "test-model", - "fw_ver": "test-version", - "mcu_ver": "test-hw-version", - } - ), patch_setup_entry("cover") as mock_cover_async_setup_entry, patch_setup_entry( - "fan" - ) as mock_fan_async_setup_entry, patch_setup_entry( - "light" - ) as mock_light_async_setup_entry, patch_setup_entry( - "switch" - ) as mock_switch_async_setup_entry: + with ( + patch_bond_bridge(), + patch_bond_version( + return_value={ + "bondid": "ZXXX12345", + "target": "test-model", + "fw_ver": "test-version", + "mcu_ver": "test-hw-version", + } + ), + patch_setup_entry("cover") as mock_cover_async_setup_entry, + patch_setup_entry("fan") as mock_fan_async_setup_entry, + patch_setup_entry("light") as mock_light_async_setup_entry, + patch_setup_entry("switch") as mock_switch_async_setup_entry, + ): result = await setup_bond_entity(hass, config_entry, patch_device_ids=True) assert result is True await hass.async_block_till_done() @@ -170,21 +172,25 @@ async def test_old_identifiers_are_removed( name="old", ) - with patch_bond_bridge(), patch_bond_version( - return_value={ - "bondid": "ZXXX12345", - "target": "test-model", - "fw_ver": "test-version", - } - ), patch_start_bpup(), patch_bond_device_ids( - return_value=["bond-device-id", "device_id"] - ), patch_bond_device( - return_value={ - "name": "test1", - "type": DeviceType.GENERIC_DEVICE, - } - ), patch_bond_device_properties(return_value={}), patch_bond_device_state( - return_value={} + with ( + patch_bond_bridge(), + patch_bond_version( + return_value={ + "bondid": "ZXXX12345", + "target": "test-model", + "fw_ver": "test-version", + } + ), + patch_start_bpup(), + patch_bond_device_ids(return_value=["bond-device-id", "device_id"]), + patch_bond_device( + return_value={ + "name": "test1", + "type": DeviceType.GENERIC_DEVICE, + } + ), + patch_bond_device_properties(return_value={}), + patch_bond_device_state(return_value={}), ): assert await hass.config_entries.async_setup(config_entry.entry_id) is True await hass.async_block_till_done() @@ -209,24 +215,26 @@ async def test_smart_by_bond_device_suggested_area( config_entry.add_to_hass(hass) - with patch_bond_bridge( - side_effect=ClientResponseError(Mock(), Mock(), status=404) - ), patch_bond_version( - return_value={ - "bondid": "KXXX12345", - "target": "test-model", - "fw_ver": "test-version", - } - ), patch_start_bpup(), patch_bond_device_ids( - return_value=["bond-device-id", "device_id"] - ), patch_bond_device( - return_value={ - "name": "test1", - "type": DeviceType.GENERIC_DEVICE, - "location": "Den", - } - ), patch_bond_device_properties(return_value={}), patch_bond_device_state( - return_value={} + with ( + patch_bond_bridge(side_effect=ClientResponseError(Mock(), Mock(), status=404)), + patch_bond_version( + return_value={ + "bondid": "KXXX12345", + "target": "test-model", + "fw_ver": "test-version", + } + ), + patch_start_bpup(), + patch_bond_device_ids(return_value=["bond-device-id", "device_id"]), + patch_bond_device( + return_value={ + "name": "test1", + "type": DeviceType.GENERIC_DEVICE, + "location": "Den", + } + ), + patch_bond_device_properties(return_value={}), + patch_bond_device_state(return_value={}), ): assert await hass.config_entries.async_setup(config_entry.entry_id) is True await hass.async_block_till_done() @@ -251,27 +259,31 @@ async def test_bridge_device_suggested_area( config_entry.add_to_hass(hass) - with patch_bond_bridge( - return_value={ - "name": "Office Bridge", - "location": "Office", - } - ), patch_bond_version( - return_value={ - "bondid": "ZXXX12345", - "target": "test-model", - "fw_ver": "test-version", - } - ), patch_start_bpup(), patch_bond_device_ids( - return_value=["bond-device-id", "device_id"] - ), patch_bond_device( - return_value={ - "name": "test1", - "type": DeviceType.GENERIC_DEVICE, - "location": "Bathroom", - } - ), patch_bond_device_properties(return_value={}), patch_bond_device_state( - return_value={} + with ( + patch_bond_bridge( + return_value={ + "name": "Office Bridge", + "location": "Office", + } + ), + patch_bond_version( + return_value={ + "bondid": "ZXXX12345", + "target": "test-model", + "fw_ver": "test-version", + } + ), + patch_start_bpup(), + patch_bond_device_ids(return_value=["bond-device-id", "device_id"]), + patch_bond_device( + return_value={ + "name": "test1", + "type": DeviceType.GENERIC_DEVICE, + "location": "Bathroom", + } + ), + patch_bond_device_properties(return_value={}), + patch_bond_device_state(return_value={}), ): assert await hass.config_entries.async_setup(config_entry.entry_id) is True await hass.async_block_till_done() diff --git a/tests/components/bond/test_light.py b/tests/components/bond/test_light.py index 10395f395dd..37cd82fc321 100644 --- a/tests/components/bond/test_light.py +++ b/tests/components/bond/test_light.py @@ -1,4 +1,5 @@ """Tests for the Bond light device.""" + from datetime import timedelta from bond_async import Action, DeviceType @@ -329,9 +330,11 @@ async def test_light_set_brightness_belief_api_error(hass: HomeAssistant) -> Non bond_device_id="test-device-id", ) - with pytest.raises( - HomeAssistantError - ), patch_bond_action_returns_clientresponseerror(), patch_bond_device_state(): + with ( + pytest.raises(HomeAssistantError), + patch_bond_action_returns_clientresponseerror(), + patch_bond_device_state(), + ): await hass.services.async_call( DOMAIN, SERVICE_SET_LIGHT_BRIGHTNESS_TRACKED_STATE, @@ -373,9 +376,11 @@ async def test_fp_light_set_brightness_belief_api_error(hass: HomeAssistant) -> bond_device_id="test-device-id", ) - with pytest.raises( - HomeAssistantError - ), patch_bond_action_returns_clientresponseerror(), patch_bond_device_state(): + with ( + pytest.raises(HomeAssistantError), + patch_bond_action_returns_clientresponseerror(), + patch_bond_device_state(), + ): await hass.services.async_call( DOMAIN, SERVICE_SET_LIGHT_BRIGHTNESS_TRACKED_STATE, @@ -484,9 +489,11 @@ async def test_light_set_power_belief_api_error(hass: HomeAssistant) -> None: bond_device_id="test-device-id", ) - with pytest.raises( - HomeAssistantError - ), patch_bond_action_returns_clientresponseerror(), patch_bond_device_state(): + with ( + pytest.raises(HomeAssistantError), + patch_bond_action_returns_clientresponseerror(), + patch_bond_device_state(), + ): await hass.services.async_call( DOMAIN, SERVICE_SET_LIGHT_POWER_TRACKED_STATE, @@ -528,9 +535,11 @@ async def test_fp_light_set_power_belief_api_error(hass: HomeAssistant) -> None: bond_device_id="test-device-id", ) - with pytest.raises( - HomeAssistantError - ), patch_bond_action_returns_clientresponseerror(), patch_bond_device_state(): + with ( + pytest.raises(HomeAssistantError), + patch_bond_action_returns_clientresponseerror(), + patch_bond_device_state(), + ): await hass.services.async_call( DOMAIN, SERVICE_SET_LIGHT_POWER_TRACKED_STATE, diff --git a/tests/components/bond/test_switch.py b/tests/components/bond/test_switch.py index 1ab9ef2165c..3d3ad663656 100644 --- a/tests/components/bond/test_switch.py +++ b/tests/components/bond/test_switch.py @@ -1,4 +1,5 @@ """Tests for the Bond switch device.""" + from datetime import timedelta from bond_async import Action, DeviceType @@ -111,9 +112,11 @@ async def test_switch_set_power_belief_api_error(hass: HomeAssistant) -> None: hass, SWITCH_DOMAIN, generic_device("name-1"), bond_device_id="test-device-id" ) - with pytest.raises( - HomeAssistantError - ), patch_bond_action_returns_clientresponseerror(), patch_bond_device_state(): + with ( + pytest.raises(HomeAssistantError), + patch_bond_action_returns_clientresponseerror(), + patch_bond_device_state(), + ): await hass.services.async_call( BOND_DOMAIN, SERVICE_SET_POWER_TRACKED_STATE, diff --git a/tests/components/bosch_shc/test_config_flow.py b/tests/components/bosch_shc/test_config_flow.py index b2920cfde0a..2fe2b98308d 100644 --- a/tests/components/bosch_shc/test_config_flow.py +++ b/tests/components/bosch_shc/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Bosch SHC config flow.""" + from ipaddress import ip_address from unittest.mock import PropertyMock, mock_open, patch @@ -43,17 +44,21 @@ async def test_form_user(hass: HomeAssistant, mock_zeroconf: None) -> None: assert result["step_id"] == "user" assert result["errors"] == {} - with patch( - "boschshcpy.session.SHCSession.mdns_info", - return_value=SHCInformation, - ), patch( - "boschshcpy.information.SHCInformation.name", - new_callable=PropertyMock, - return_value="shc012345", - ), patch( - "boschshcpy.information.SHCInformation.unique_id", - new_callable=PropertyMock, - return_value="test-mac", + with ( + patch( + "boschshcpy.session.SHCSession.mdns_info", + return_value=SHCInformation, + ), + patch( + "boschshcpy.information.SHCInformation.name", + new_callable=PropertyMock, + return_value="shc012345", + ), + patch( + "boschshcpy.information.SHCInformation.unique_id", + new_callable=PropertyMock, + return_value="test-mac", + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -64,19 +69,23 @@ async def test_form_user(hass: HomeAssistant, mock_zeroconf: None) -> None: assert result2["step_id"] == "credentials" assert result2["errors"] == {} - with patch( - "boschshcpy.register_client.SHCRegisterClient.register", - return_value={ - "token": "abc:123", - "cert": b"content_cert", - "key": b"content_key", - }, - ), patch("os.mkdir"), patch( - "homeassistant.components.bosch_shc.config_flow.open" - ), patch("boschshcpy.session.SHCSession.authenticate") as mock_authenticate, patch( - "homeassistant.components.bosch_shc.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "boschshcpy.register_client.SHCRegisterClient.register", + return_value={ + "token": "abc:123", + "cert": b"content_cert", + "key": b"content_key", + }, + ), + patch("os.mkdir"), + patch("homeassistant.components.bosch_shc.config_flow.open"), + patch("boschshcpy.session.SHCSession.authenticate") as mock_authenticate, + patch( + "homeassistant.components.bosch_shc.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], {"password": "test"}, @@ -149,17 +158,21 @@ async def test_form_pairing_error(hass: HomeAssistant, mock_zeroconf: None) -> N DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch( - "boschshcpy.session.SHCSession.mdns_info", - return_value=SHCInformation, - ), patch( - "boschshcpy.information.SHCInformation.name", - new_callable=PropertyMock, - return_value="shc012345", - ), patch( - "boschshcpy.information.SHCInformation.unique_id", - new_callable=PropertyMock, - return_value="test-mac", + with ( + patch( + "boschshcpy.session.SHCSession.mdns_info", + return_value=SHCInformation, + ), + patch( + "boschshcpy.information.SHCInformation.name", + new_callable=PropertyMock, + return_value="shc012345", + ), + patch( + "boschshcpy.information.SHCInformation.unique_id", + new_callable=PropertyMock, + return_value="test-mac", + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -191,17 +204,21 @@ async def test_form_user_invalid_auth(hass: HomeAssistant, mock_zeroconf: None) DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch( - "boschshcpy.session.SHCSession.mdns_info", - return_value=SHCInformation, - ), patch( - "boschshcpy.information.SHCInformation.name", - new_callable=PropertyMock, - return_value="shc012345", - ), patch( - "boschshcpy.information.SHCInformation.unique_id", - new_callable=PropertyMock, - return_value="test-mac", + with ( + patch( + "boschshcpy.session.SHCSession.mdns_info", + return_value=SHCInformation, + ), + patch( + "boschshcpy.information.SHCInformation.name", + new_callable=PropertyMock, + return_value="shc012345", + ), + patch( + "boschshcpy.information.SHCInformation.unique_id", + new_callable=PropertyMock, + return_value="test-mac", + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -212,18 +229,21 @@ async def test_form_user_invalid_auth(hass: HomeAssistant, mock_zeroconf: None) assert result2["step_id"] == "credentials" assert result2["errors"] == {} - with patch( - "boschshcpy.register_client.SHCRegisterClient.register", - return_value={ - "token": "abc:123", - "cert": b"content_cert", - "key": b"content_key", - }, - ), patch("os.mkdir"), patch( - "homeassistant.components.bosch_shc.config_flow.open" - ), patch( - "boschshcpy.session.SHCSession.authenticate", - side_effect=SHCAuthenticationError, + with ( + patch( + "boschshcpy.register_client.SHCRegisterClient.register", + return_value={ + "token": "abc:123", + "cert": b"content_cert", + "key": b"content_key", + }, + ), + patch("os.mkdir"), + patch("homeassistant.components.bosch_shc.config_flow.open"), + patch( + "boschshcpy.session.SHCSession.authenticate", + side_effect=SHCAuthenticationError, + ), ): result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], @@ -244,17 +264,21 @@ async def test_form_validate_connection_error( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch( - "boschshcpy.session.SHCSession.mdns_info", - return_value=SHCInformation, - ), patch( - "boschshcpy.information.SHCInformation.name", - new_callable=PropertyMock, - return_value="shc012345", - ), patch( - "boschshcpy.information.SHCInformation.unique_id", - new_callable=PropertyMock, - return_value="test-mac", + with ( + patch( + "boschshcpy.session.SHCSession.mdns_info", + return_value=SHCInformation, + ), + patch( + "boschshcpy.information.SHCInformation.name", + new_callable=PropertyMock, + return_value="shc012345", + ), + patch( + "boschshcpy.information.SHCInformation.unique_id", + new_callable=PropertyMock, + return_value="test-mac", + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -265,18 +289,21 @@ async def test_form_validate_connection_error( assert result2["step_id"] == "credentials" assert result2["errors"] == {} - with patch( - "boschshcpy.register_client.SHCRegisterClient.register", - return_value={ - "token": "abc:123", - "cert": b"content_cert", - "key": b"content_key", - }, - ), patch("os.mkdir"), patch( - "homeassistant.components.bosch_shc.config_flow.open" - ), patch( - "boschshcpy.session.SHCSession.authenticate", - side_effect=SHCConnectionError, + with ( + patch( + "boschshcpy.register_client.SHCRegisterClient.register", + return_value={ + "token": "abc:123", + "cert": b"content_cert", + "key": b"content_key", + }, + ), + patch("os.mkdir"), + patch("homeassistant.components.bosch_shc.config_flow.open"), + patch( + "boschshcpy.session.SHCSession.authenticate", + side_effect=SHCConnectionError, + ), ): result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], @@ -297,17 +324,21 @@ async def test_form_validate_session_error( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch( - "boschshcpy.session.SHCSession.mdns_info", - return_value=SHCInformation, - ), patch( - "boschshcpy.information.SHCInformation.name", - new_callable=PropertyMock, - return_value="shc012345", - ), patch( - "boschshcpy.information.SHCInformation.unique_id", - new_callable=PropertyMock, - return_value="test-mac", + with ( + patch( + "boschshcpy.session.SHCSession.mdns_info", + return_value=SHCInformation, + ), + patch( + "boschshcpy.information.SHCInformation.name", + new_callable=PropertyMock, + return_value="shc012345", + ), + patch( + "boschshcpy.information.SHCInformation.unique_id", + new_callable=PropertyMock, + return_value="test-mac", + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -318,18 +349,21 @@ async def test_form_validate_session_error( assert result2["step_id"] == "credentials" assert result2["errors"] == {} - with patch( - "boschshcpy.register_client.SHCRegisterClient.register", - return_value={ - "token": "abc:123", - "cert": b"content_cert", - "key": b"content_key", - }, - ), patch("os.mkdir"), patch( - "homeassistant.components.bosch_shc.config_flow.open" - ), patch( - "boschshcpy.session.SHCSession.authenticate", - side_effect=SHCSessionError(""), + with ( + patch( + "boschshcpy.register_client.SHCRegisterClient.register", + return_value={ + "token": "abc:123", + "cert": b"content_cert", + "key": b"content_key", + }, + ), + patch("os.mkdir"), + patch("homeassistant.components.bosch_shc.config_flow.open"), + patch( + "boschshcpy.session.SHCSession.authenticate", + side_effect=SHCSessionError(""), + ), ): result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], @@ -350,17 +384,21 @@ async def test_form_validate_exception( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch( - "boschshcpy.session.SHCSession.mdns_info", - return_value=SHCInformation, - ), patch( - "boschshcpy.information.SHCInformation.name", - new_callable=PropertyMock, - return_value="shc012345", - ), patch( - "boschshcpy.information.SHCInformation.unique_id", - new_callable=PropertyMock, - return_value="test-mac", + with ( + patch( + "boschshcpy.session.SHCSession.mdns_info", + return_value=SHCInformation, + ), + patch( + "boschshcpy.information.SHCInformation.name", + new_callable=PropertyMock, + return_value="shc012345", + ), + patch( + "boschshcpy.information.SHCInformation.unique_id", + new_callable=PropertyMock, + return_value="test-mac", + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -371,18 +409,21 @@ async def test_form_validate_exception( assert result2["step_id"] == "credentials" assert result2["errors"] == {} - with patch( - "boschshcpy.register_client.SHCRegisterClient.register", - return_value={ - "token": "abc:123", - "cert": b"content_cert", - "key": b"content_key", - }, - ), patch("os.mkdir"), patch( - "homeassistant.components.bosch_shc.config_flow.open" - ), patch( - "boschshcpy.session.SHCSession.authenticate", - side_effect=Exception, + with ( + patch( + "boschshcpy.register_client.SHCRegisterClient.register", + return_value={ + "token": "abc:123", + "cert": b"content_cert", + "key": b"content_key", + }, + ), + patch("os.mkdir"), + patch("homeassistant.components.bosch_shc.config_flow.open"), + patch( + "boschshcpy.session.SHCSession.authenticate", + side_effect=Exception, + ), ): result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], @@ -409,17 +450,21 @@ async def test_form_already_configured( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch( - "boschshcpy.session.SHCSession.mdns_info", - return_value=SHCInformation, - ), patch( - "boschshcpy.information.SHCInformation.name", - new_callable=PropertyMock, - return_value="shc012345", - ), patch( - "boschshcpy.information.SHCInformation.unique_id", - new_callable=PropertyMock, - return_value="test-mac", + with ( + patch( + "boschshcpy.session.SHCSession.mdns_info", + return_value=SHCInformation, + ), + patch( + "boschshcpy.information.SHCInformation.name", + new_callable=PropertyMock, + return_value="shc012345", + ), + patch( + "boschshcpy.information.SHCInformation.unique_id", + new_callable=PropertyMock, + return_value="test-mac", + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -436,17 +481,21 @@ async def test_form_already_configured( async def test_zeroconf(hass: HomeAssistant, mock_zeroconf: None) -> None: """Test we get the form.""" - with patch( - "boschshcpy.session.SHCSession.mdns_info", - return_value=SHCInformation, - ), patch( - "boschshcpy.information.SHCInformation.name", - new_callable=PropertyMock, - return_value="shc012345", - ), patch( - "boschshcpy.information.SHCInformation.unique_id", - new_callable=PropertyMock, - return_value="test-mac", + with ( + patch( + "boschshcpy.session.SHCSession.mdns_info", + return_value=SHCInformation, + ), + patch( + "boschshcpy.information.SHCInformation.name", + new_callable=PropertyMock, + return_value="shc012345", + ), + patch( + "boschshcpy.information.SHCInformation.unique_id", + new_callable=PropertyMock, + return_value="test-mac", + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -470,21 +519,25 @@ async def test_zeroconf(hass: HomeAssistant, mock_zeroconf: None) -> None: assert result2["type"] == "form" assert result2["step_id"] == "credentials" - with patch( - "boschshcpy.register_client.SHCRegisterClient.register", - return_value={ - "token": "abc:123", - "cert": b"content_cert", - "key": b"content_key", - }, - ), patch("os.mkdir"), patch( - "homeassistant.components.bosch_shc.config_flow.open" - ), patch( - "boschshcpy.session.SHCSession.authenticate", - ), patch( - "homeassistant.components.bosch_shc.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "boschshcpy.register_client.SHCRegisterClient.register", + return_value={ + "token": "abc:123", + "cert": b"content_cert", + "key": b"content_key", + }, + ), + patch("os.mkdir"), + patch("homeassistant.components.bosch_shc.config_flow.open"), + patch( + "boschshcpy.session.SHCSession.authenticate", + ), + patch( + "homeassistant.components.bosch_shc.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], {"password": "test"}, @@ -513,17 +566,21 @@ async def test_zeroconf_already_configured( ) entry.add_to_hass(hass) - with patch( - "boschshcpy.session.SHCSession.mdns_info", - return_value=SHCInformation, - ), patch( - "boschshcpy.information.SHCInformation.name", - new_callable=PropertyMock, - return_value="shc012345", - ), patch( - "boschshcpy.information.SHCInformation.unique_id", - new_callable=PropertyMock, - return_value="test-mac", + with ( + patch( + "boschshcpy.session.SHCSession.mdns_info", + return_value=SHCInformation, + ), + patch( + "boschshcpy.information.SHCInformation.name", + new_callable=PropertyMock, + return_value="shc012345", + ), + patch( + "boschshcpy.information.SHCInformation.unique_id", + new_callable=PropertyMock, + return_value="test-mac", + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -596,17 +653,21 @@ async def test_reauth(hass: HomeAssistant, mock_zeroconf: None) -> None: assert result["type"] == "form" assert result["step_id"] == "reauth_confirm" - with patch( - "boschshcpy.session.SHCSession.mdns_info", - return_value=SHCInformation, - ), patch( - "boschshcpy.information.SHCInformation.name", - new_callable=PropertyMock, - return_value="shc012345", - ), patch( - "boschshcpy.information.SHCInformation.unique_id", - new_callable=PropertyMock, - return_value="test-mac", + with ( + patch( + "boschshcpy.session.SHCSession.mdns_info", + return_value=SHCInformation, + ), + patch( + "boschshcpy.information.SHCInformation.name", + new_callable=PropertyMock, + return_value="shc012345", + ), + patch( + "boschshcpy.information.SHCInformation.unique_id", + new_callable=PropertyMock, + return_value="test-mac", + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -617,19 +678,23 @@ async def test_reauth(hass: HomeAssistant, mock_zeroconf: None) -> None: assert result2["step_id"] == "credentials" assert result2["errors"] == {} - with patch( - "boschshcpy.register_client.SHCRegisterClient.register", - return_value={ - "token": "abc:123", - "cert": b"content_cert", - "key": b"content_key", - }, - ), patch("os.mkdir"), patch( - "homeassistant.components.bosch_shc.config_flow.open" - ), patch("boschshcpy.session.SHCSession.authenticate"), patch( - "homeassistant.components.bosch_shc.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "boschshcpy.register_client.SHCRegisterClient.register", + return_value={ + "token": "abc:123", + "cert": b"content_cert", + "key": b"content_key", + }, + ), + patch("os.mkdir"), + patch("homeassistant.components.bosch_shc.config_flow.open"), + patch("boschshcpy.session.SHCSession.authenticate"), + patch( + "homeassistant.components.bosch_shc.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], {"password": "test"}, @@ -651,9 +716,12 @@ async def test_tls_assets_writer(hass: HomeAssistant) -> None: "cert": b"content_cert", "key": b"content_key", } - with patch("os.mkdir"), patch( - "homeassistant.components.bosch_shc.config_flow.open", mock_open() - ) as mocked_file: + with ( + patch("os.mkdir"), + patch( + "homeassistant.components.bosch_shc.config_flow.open", mock_open() + ) as mocked_file, + ): write_tls_asset(hass, CONF_SHC_CERT, assets["cert"]) mocked_file.assert_called_with( hass.config.path(DOMAIN, CONF_SHC_CERT), "w", encoding="utf8" diff --git a/tests/components/braviatv/conftest.py b/tests/components/braviatv/conftest.py index e4ee2ebc868..33f55fbb390 100644 --- a/tests/components/braviatv/conftest.py +++ b/tests/components/braviatv/conftest.py @@ -1,4 +1,5 @@ """Test fixtures for Bravia TV.""" + from collections.abc import Generator from unittest.mock import AsyncMock, patch diff --git a/tests/components/braviatv/test_config_flow.py b/tests/components/braviatv/test_config_flow.py index 0f1d08792fa..673344017f7 100644 --- a/tests/components/braviatv/test_config_flow.py +++ b/tests/components/braviatv/test_config_flow.py @@ -1,4 +1,5 @@ """Define tests for the Bravia TV config flow.""" + from unittest.mock import patch from pybravia import ( @@ -107,11 +108,14 @@ async def test_ssdp_discovery(hass: HomeAssistant) -> None: assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "confirm" - with patch("pybravia.BraviaClient.connect"), patch( - "pybravia.BraviaClient.pair" - ), patch("pybravia.BraviaClient.set_wol_mode"), patch( - "pybravia.BraviaClient.get_system_info", - return_value=BRAVIA_SYSTEM_INFO, + with ( + patch("pybravia.BraviaClient.connect"), + patch("pybravia.BraviaClient.pair"), + patch("pybravia.BraviaClient.set_wol_mode"), + patch( + "pybravia.BraviaClient.get_system_info", + return_value=BRAVIA_SYSTEM_INFO, + ), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} @@ -200,10 +204,13 @@ async def test_user_invalid_host(hass: HomeAssistant) -> None: ) async def test_pin_form_error(hass: HomeAssistant, side_effect, error_message) -> None: """Test that PIN form errors are correct.""" - with patch( - "pybravia.BraviaClient.connect", - side_effect=side_effect, - ), patch("pybravia.BraviaClient.pair"): + with ( + patch( + "pybravia.BraviaClient.connect", + side_effect=side_effect, + ), + patch("pybravia.BraviaClient.pair"), + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: "bravia-host"} ) @@ -272,11 +279,14 @@ async def test_duplicate_error(hass: HomeAssistant) -> None: ) config_entry.add_to_hass(hass) - with patch("pybravia.BraviaClient.connect"), patch( - "pybravia.BraviaClient.pair" - ), patch("pybravia.BraviaClient.set_wol_mode"), patch( - "pybravia.BraviaClient.get_system_info", - return_value=BRAVIA_SYSTEM_INFO, + with ( + patch("pybravia.BraviaClient.connect"), + patch("pybravia.BraviaClient.pair"), + patch("pybravia.BraviaClient.set_wol_mode"), + patch( + "pybravia.BraviaClient.get_system_info", + return_value=BRAVIA_SYSTEM_INFO, + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: "bravia-host"} @@ -296,11 +306,14 @@ async def test_create_entry(hass: HomeAssistant) -> None: """Test that entry is added correctly with PIN auth.""" uuid = await instance_id.async_get(hass) - with patch("pybravia.BraviaClient.connect"), patch( - "pybravia.BraviaClient.pair" - ), patch("pybravia.BraviaClient.set_wol_mode"), patch( - "pybravia.BraviaClient.get_system_info", - return_value=BRAVIA_SYSTEM_INFO, + with ( + patch("pybravia.BraviaClient.connect"), + patch("pybravia.BraviaClient.pair"), + patch("pybravia.BraviaClient.set_wol_mode"), + patch( + "pybravia.BraviaClient.get_system_info", + return_value=BRAVIA_SYSTEM_INFO, + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: "bravia-host"} @@ -335,11 +348,13 @@ async def test_create_entry(hass: HomeAssistant) -> None: async def test_create_entry_psk(hass: HomeAssistant) -> None: """Test that entry is added correctly with PSK auth.""" - with patch("pybravia.BraviaClient.connect"), patch( - "pybravia.BraviaClient.set_wol_mode" - ), patch( - "pybravia.BraviaClient.get_system_info", - return_value=BRAVIA_SYSTEM_INFO, + with ( + patch("pybravia.BraviaClient.connect"), + patch("pybravia.BraviaClient.set_wol_mode"), + patch( + "pybravia.BraviaClient.get_system_info", + return_value=BRAVIA_SYSTEM_INFO, + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: "bravia-host"} @@ -391,15 +406,20 @@ async def test_reauth_successful(hass: HomeAssistant, use_psk, new_pin) -> None: ) config_entry.add_to_hass(hass) - with patch("pybravia.BraviaClient.connect"), patch( - "pybravia.BraviaClient.get_power_status", - return_value="active", - ), patch( - "pybravia.BraviaClient.get_external_status", - return_value=BRAVIA_SOURCES, - ), patch( - "pybravia.BraviaClient.send_rest_req", - return_value={}, + with ( + patch("pybravia.BraviaClient.connect"), + patch( + "pybravia.BraviaClient.get_power_status", + return_value="active", + ), + patch( + "pybravia.BraviaClient.get_external_status", + return_value=BRAVIA_SOURCES, + ), + patch( + "pybravia.BraviaClient.send_rest_req", + return_value={}, + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, diff --git a/tests/components/braviatv/test_diagnostics.py b/tests/components/braviatv/test_diagnostics.py index d0974774e7b..13f6c92fb76 100644 --- a/tests/components/braviatv/test_diagnostics.py +++ b/tests/components/braviatv/test_diagnostics.py @@ -1,4 +1,5 @@ """Test the BraviaTV diagnostics.""" + from unittest.mock import patch from syrupy import SnapshotAssertion @@ -55,16 +56,17 @@ async def test_entry_diagnostics( ) config_entry.add_to_hass(hass) - with patch("pybravia.BraviaClient.connect"), patch( - "pybravia.BraviaClient.pair" - ), patch("pybravia.BraviaClient.set_wol_mode"), patch( - "pybravia.BraviaClient.get_system_info", return_value=BRAVIA_SYSTEM_INFO - ), patch("pybravia.BraviaClient.get_power_status", return_value="active"), patch( - "pybravia.BraviaClient.get_external_status", return_value=INPUTS - ), patch("pybravia.BraviaClient.get_volume_info", return_value={}), patch( - "pybravia.BraviaClient.get_playing_info", return_value={} - ), patch("pybravia.BraviaClient.get_app_list", return_value=[]), patch( - "pybravia.BraviaClient.get_content_list_all", return_value=[] + with ( + patch("pybravia.BraviaClient.connect"), + patch("pybravia.BraviaClient.pair"), + patch("pybravia.BraviaClient.set_wol_mode"), + patch("pybravia.BraviaClient.get_system_info", return_value=BRAVIA_SYSTEM_INFO), + patch("pybravia.BraviaClient.get_power_status", return_value="active"), + patch("pybravia.BraviaClient.get_external_status", return_value=INPUTS), + patch("pybravia.BraviaClient.get_volume_info", return_value={}), + patch("pybravia.BraviaClient.get_playing_info", return_value={}), + patch("pybravia.BraviaClient.get_app_list", return_value=[]), + patch("pybravia.BraviaClient.get_content_list_all", return_value=[]), ): assert await async_setup_component(hass, DOMAIN, {}) result = await get_diagnostics_for_config_entry(hass, hass_client, config_entry) diff --git a/tests/components/bring/conftest.py b/tests/components/bring/conftest.py index 0b15d31eecb..e399e18dfbe 100644 --- a/tests/components/bring/conftest.py +++ b/tests/components/bring/conftest.py @@ -1,4 +1,5 @@ """Common fixtures for the Bring! tests.""" + from collections.abc import Generator from unittest.mock import AsyncMock, patch @@ -27,12 +28,15 @@ def mock_setup_entry() -> Generator[AsyncMock, None, None]: @pytest.fixture def mock_bring_client() -> Generator[AsyncMock, None, None]: """Mock a Bring client.""" - with patch( - "homeassistant.components.bring.Bring", - autospec=True, - ) as mock_client, patch( - "homeassistant.components.bring.config_flow.Bring", - new=mock_client, + with ( + patch( + "homeassistant.components.bring.Bring", + autospec=True, + ) as mock_client, + patch( + "homeassistant.components.bring.config_flow.Bring", + new=mock_client, + ), ): client = mock_client.return_value client.uuid = UUID diff --git a/tests/components/bring/test_config_flow.py b/tests/components/bring/test_config_flow.py index f5a8398539a..29abad94fad 100644 --- a/tests/components/bring/test_config_flow.py +++ b/tests/components/bring/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Bring! config flow.""" + from unittest.mock import AsyncMock from bring_api.exceptions import ( diff --git a/tests/components/bring/test_init.py b/tests/components/bring/test_init.py index e5fcedefc22..6bf9fd1cb54 100644 --- a/tests/components/bring/test_init.py +++ b/tests/components/bring/test_init.py @@ -1,4 +1,5 @@ """Unit tests for the bring integration.""" + from unittest.mock import AsyncMock import pytest diff --git a/tests/components/broadlink/__init__.py b/tests/components/broadlink/__init__.py index 8cdb4f478a3..c9245fb16fa 100644 --- a/tests/components/broadlink/__init__.py +++ b/tests/components/broadlink/__init__.py @@ -1,4 +1,5 @@ """Tests for the Broadlink integration.""" + from dataclasses import dataclass from unittest.mock import MagicMock, patch diff --git a/tests/components/broadlink/conftest.py b/tests/components/broadlink/conftest.py index 0a9ee4813da..c20c444c342 100644 --- a/tests/components/broadlink/conftest.py +++ b/tests/components/broadlink/conftest.py @@ -1,4 +1,5 @@ """Broadlink test helpers.""" + from unittest.mock import patch import pytest diff --git a/tests/components/broadlink/test_config_flow.py b/tests/components/broadlink/test_config_flow.py index 09365b8f5f4..143742d3a9a 100644 --- a/tests/components/broadlink/test_config_flow.py +++ b/tests/components/broadlink/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Broadlink config flow.""" + import errno import socket from unittest.mock import call, patch @@ -20,9 +21,12 @@ DEVICE_FACTORY = "homeassistant.components.broadlink.config_flow.blk.gendevice" @pytest.fixture(autouse=True) def broadlink_setup_fixture(): """Mock broadlink entry setup.""" - with patch( - "homeassistant.components.broadlink.async_setup", return_value=True - ), patch("homeassistant.components.broadlink.async_setup_entry", return_value=True): + with ( + patch("homeassistant.components.broadlink.async_setup", return_value=True), + patch( + "homeassistant.components.broadlink.async_setup_entry", return_value=True + ), + ): yield diff --git a/tests/components/broadlink/test_device.py b/tests/components/broadlink/test_device.py index b97911262ef..c9f22dbcbf8 100644 --- a/tests/components/broadlink/test_device.py +++ b/tests/components/broadlink/test_device.py @@ -1,4 +1,5 @@ """Tests for Broadlink devices.""" + from unittest.mock import patch import broadlink.exceptions as blke @@ -19,11 +20,10 @@ async def test_device_setup(hass: HomeAssistant) -> None: """Test a successful setup.""" device = get_device("Office") - with patch.object( - hass.config_entries, "async_forward_entry_setup" - ) as mock_forward, patch.object( - hass.config_entries.flow, "async_init" - ) as mock_init: + with ( + patch.object(hass.config_entries, "async_forward_entry_setups") as mock_forward, + patch.object(hass.config_entries.flow, "async_init") as mock_init, + ): mock_setup = await device.setup_entry(hass) assert mock_setup.entry.state is ConfigEntryState.LOADED @@ -31,9 +31,9 @@ async def test_device_setup(hass: HomeAssistant) -> None: assert mock_setup.api.get_fwversion.call_count == 1 assert mock_setup.factory.call_count == 1 - forward_entries = {c[1][1] for c in mock_forward.mock_calls} + forward_entries = set(mock_forward.mock_calls[0][1][1]) domains = get_domains(mock_setup.api.type) - assert mock_forward.call_count == len(domains) + assert mock_forward.call_count == 1 assert forward_entries == domains assert mock_init.call_count == 0 @@ -44,11 +44,10 @@ async def test_device_setup_authentication_error(hass: HomeAssistant) -> None: mock_api = device.get_mock_api() mock_api.auth.side_effect = blke.AuthenticationError() - with patch.object( - hass.config_entries, "async_forward_entry_setup" - ) as mock_forward, patch.object( - hass.config_entries.flow, "async_init" - ) as mock_init: + with ( + patch.object(hass.config_entries, "async_forward_entry_setups") as mock_forward, + patch.object(hass.config_entries.flow, "async_init") as mock_init, + ): mock_setup = await device.setup_entry(hass, mock_api=mock_api) assert mock_setup.entry.state is ConfigEntryState.SETUP_ERROR @@ -68,11 +67,10 @@ async def test_device_setup_network_timeout(hass: HomeAssistant) -> None: mock_api = device.get_mock_api() mock_api.auth.side_effect = blke.NetworkTimeoutError() - with patch.object( - hass.config_entries, "async_forward_entry_setup" - ) as mock_forward, patch.object( - hass.config_entries.flow, "async_init" - ) as mock_init: + with ( + patch.object(hass.config_entries, "async_forward_entry_setups") as mock_forward, + patch.object(hass.config_entries.flow, "async_init") as mock_init, + ): mock_setup = await device.setup_entry(hass, mock_api=mock_api) assert mock_setup.entry.state is ConfigEntryState.SETUP_RETRY @@ -87,11 +85,10 @@ async def test_device_setup_os_error(hass: HomeAssistant) -> None: mock_api = device.get_mock_api() mock_api.auth.side_effect = OSError() - with patch.object( - hass.config_entries, "async_forward_entry_setup" - ) as mock_forward, patch.object( - hass.config_entries.flow, "async_init" - ) as mock_init: + with ( + patch.object(hass.config_entries, "async_forward_entry_setups") as mock_forward, + patch.object(hass.config_entries.flow, "async_init") as mock_init, + ): mock_setup = await device.setup_entry(hass, mock_api=mock_api) assert mock_setup.entry.state is ConfigEntryState.SETUP_RETRY @@ -106,11 +103,10 @@ async def test_device_setup_broadlink_exception(hass: HomeAssistant) -> None: mock_api = device.get_mock_api() mock_api.auth.side_effect = blke.BroadlinkException() - with patch.object( - hass.config_entries, "async_forward_entry_setup" - ) as mock_forward, patch.object( - hass.config_entries.flow, "async_init" - ) as mock_init: + with ( + patch.object(hass.config_entries, "async_forward_entry_setups") as mock_forward, + patch.object(hass.config_entries.flow, "async_init") as mock_init, + ): mock_setup = await device.setup_entry(hass, mock_api=mock_api) assert mock_setup.entry.state is ConfigEntryState.SETUP_ERROR @@ -125,11 +121,10 @@ async def test_device_setup_update_network_timeout(hass: HomeAssistant) -> None: mock_api = device.get_mock_api() mock_api.check_sensors.side_effect = blke.NetworkTimeoutError() - with patch.object( - hass.config_entries, "async_forward_entry_setup" - ) as mock_forward, patch.object( - hass.config_entries.flow, "async_init" - ) as mock_init: + with ( + patch.object(hass.config_entries, "async_forward_entry_setups") as mock_forward, + patch.object(hass.config_entries.flow, "async_init") as mock_init, + ): mock_setup = await device.setup_entry(hass, mock_api=mock_api) assert mock_setup.entry.state is ConfigEntryState.SETUP_RETRY @@ -148,20 +143,19 @@ async def test_device_setup_update_authorization_error(hass: HomeAssistant) -> N {"temperature": 30}, ) - with patch.object( - hass.config_entries, "async_forward_entry_setup" - ) as mock_forward, patch.object( - hass.config_entries.flow, "async_init" - ) as mock_init: + with ( + patch.object(hass.config_entries, "async_forward_entry_setups") as mock_forward, + patch.object(hass.config_entries.flow, "async_init") as mock_init, + ): mock_setup = await device.setup_entry(hass, mock_api=mock_api) assert mock_setup.entry.state is ConfigEntryState.LOADED assert mock_setup.api.auth.call_count == 2 assert mock_setup.api.check_sensors.call_count == 2 - forward_entries = {c[1][1] for c in mock_forward.mock_calls} + forward_entries = set(mock_forward.mock_calls[0][1][1]) domains = get_domains(mock_api.type) - assert mock_forward.call_count == len(domains) + assert mock_forward.call_count == 1 assert forward_entries == domains assert mock_init.call_count == 0 @@ -173,11 +167,10 @@ async def test_device_setup_update_authentication_error(hass: HomeAssistant) -> mock_api.check_sensors.side_effect = blke.AuthorizationError() mock_api.auth.side_effect = (None, blke.AuthenticationError()) - with patch.object( - hass.config_entries, "async_forward_entry_setup" - ) as mock_forward, patch.object( - hass.config_entries.flow, "async_init" - ) as mock_init: + with ( + patch.object(hass.config_entries, "async_forward_entry_setups") as mock_forward, + patch.object(hass.config_entries.flow, "async_init") as mock_init, + ): mock_setup = await device.setup_entry(hass, mock_api=mock_api) assert mock_setup.entry.state is ConfigEntryState.SETUP_RETRY @@ -198,11 +191,10 @@ async def test_device_setup_update_broadlink_exception(hass: HomeAssistant) -> N mock_api = device.get_mock_api() mock_api.check_sensors.side_effect = blke.BroadlinkException() - with patch.object( - hass.config_entries, "async_forward_entry_setup" - ) as mock_forward, patch.object( - hass.config_entries.flow, "async_init" - ) as mock_init: + with ( + patch.object(hass.config_entries, "async_forward_entry_setups") as mock_forward, + patch.object(hass.config_entries.flow, "async_init") as mock_init, + ): mock_setup = await device.setup_entry(hass, mock_api=mock_api) assert mock_setup.entry.state is ConfigEntryState.SETUP_RETRY @@ -220,13 +212,15 @@ async def test_device_setup_get_fwversion_broadlink_exception( mock_api = device.get_mock_api() mock_api.get_fwversion.side_effect = blke.BroadlinkException() - with patch.object(hass.config_entries, "async_forward_entry_setup") as mock_forward: + with patch.object( + hass.config_entries, "async_forward_entry_setups" + ) as mock_forward: mock_setup = await device.setup_entry(hass, mock_api=mock_api) assert mock_setup.entry.state is ConfigEntryState.LOADED - forward_entries = {c[1][1] for c in mock_forward.mock_calls} + forward_entries = set(mock_forward.mock_calls[0][1][1]) domains = get_domains(mock_setup.api.type) - assert mock_forward.call_count == len(domains) + assert mock_forward.call_count == 1 assert forward_entries == domains @@ -236,13 +230,15 @@ async def test_device_setup_get_fwversion_os_error(hass: HomeAssistant) -> None: mock_api = device.get_mock_api() mock_api.get_fwversion.side_effect = OSError() - with patch.object(hass.config_entries, "async_forward_entry_setup") as mock_forward: + with patch.object( + hass.config_entries, "async_forward_entry_setups" + ) as mock_forward: mock_setup = await device.setup_entry(hass, mock_api=mock_api) assert mock_setup.entry.state is ConfigEntryState.LOADED - forward_entries = {c[1][1] for c in mock_forward.mock_calls} + forward_entries = set(mock_forward.mock_calls[0][1][1]) domains = get_domains(mock_setup.api.type) - assert mock_forward.call_count == len(domains) + assert mock_forward.call_count == 1 assert forward_entries == domains @@ -280,7 +276,7 @@ async def test_device_unload_works(hass: HomeAssistant) -> None: """Test we unload the device.""" device = get_device("Office") - with patch.object(hass.config_entries, "async_forward_entry_setup"): + with patch.object(hass.config_entries, "async_forward_entry_setups"): mock_setup = await device.setup_entry(hass) with patch.object( @@ -301,8 +297,9 @@ async def test_device_unload_authentication_error(hass: HomeAssistant) -> None: mock_api = device.get_mock_api() mock_api.auth.side_effect = blke.AuthenticationError() - with patch.object(hass.config_entries, "async_forward_entry_setup"), patch.object( - hass.config_entries.flow, "async_init" + with ( + patch.object(hass.config_entries, "async_forward_entry_setups"), + patch.object(hass.config_entries.flow, "async_init"), ): mock_setup = await device.setup_entry(hass, mock_api=mock_api) @@ -321,7 +318,7 @@ async def test_device_unload_update_failed(hass: HomeAssistant) -> None: mock_api = device.get_mock_api() mock_api.check_sensors.side_effect = blke.NetworkTimeoutError() - with patch.object(hass.config_entries, "async_forward_entry_setup"): + with patch.object(hass.config_entries, "async_forward_entry_setups"): mock_setup = await device.setup_entry(hass, mock_api=mock_api) with patch.object( diff --git a/tests/components/broadlink/test_heartbeat.py b/tests/components/broadlink/test_heartbeat.py index 566dd4ba86f..d6ce7104bf7 100644 --- a/tests/components/broadlink/test_heartbeat.py +++ b/tests/components/broadlink/test_heartbeat.py @@ -1,4 +1,5 @@ """Tests for Broadlink heartbeats.""" + from unittest.mock import call, patch import pytest @@ -50,7 +51,7 @@ async def test_heartbeat_trigger_right_time(hass: HomeAssistant) -> None: async_fire_time_changed( hass, dt_util.utcnow() + BroadlinkHeartbeat.HEARTBEAT_INTERVAL ) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert mock_ping.call_count == 1 assert mock_ping.call_args == call(device.host) @@ -68,7 +69,7 @@ async def test_heartbeat_do_not_trigger_before_time(hass: HomeAssistant) -> None hass, dt_util.utcnow() + BroadlinkHeartbeat.HEARTBEAT_INTERVAL // 2, ) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert mock_ping.call_count == 0 @@ -87,6 +88,7 @@ async def test_heartbeat_unload(hass: HomeAssistant) -> None: async_fire_time_changed( hass, dt_util.utcnow() + BroadlinkHeartbeat.HEARTBEAT_INTERVAL ) + await hass.async_block_till_done(wait_background_tasks=True) assert mock_ping.call_count == 0 @@ -107,7 +109,7 @@ async def test_heartbeat_do_not_unload(hass: HomeAssistant) -> None: async_fire_time_changed( hass, dt_util.utcnow() + BroadlinkHeartbeat.HEARTBEAT_INTERVAL ) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert mock_ping.call_count == 1 assert mock_ping.call_args == call(device_b.host) diff --git a/tests/components/broadlink/test_helpers.py b/tests/components/broadlink/test_helpers.py index 937615d7e51..e3ca11a3e2d 100644 --- a/tests/components/broadlink/test_helpers.py +++ b/tests/components/broadlink/test_helpers.py @@ -1,4 +1,5 @@ """Tests for Broadlink helper functions.""" + import pytest import voluptuous as vol diff --git a/tests/components/broadlink/test_remote.py b/tests/components/broadlink/test_remote.py index 5665f7529d5..a55bf63f227 100644 --- a/tests/components/broadlink/test_remote.py +++ b/tests/components/broadlink/test_remote.py @@ -1,4 +1,5 @@ """Tests for Broadlink remotes.""" + from base64 import b64decode from unittest.mock import call diff --git a/tests/components/broadlink/test_sensors.py b/tests/components/broadlink/test_sensors.py index e00350b7627..e7908104663 100644 --- a/tests/components/broadlink/test_sensors.py +++ b/tests/components/broadlink/test_sensors.py @@ -1,4 +1,5 @@ """Tests for Broadlink sensors.""" + from datetime import timedelta from homeassistant.components.broadlink.const import DOMAIN diff --git a/tests/components/broadlink/test_switch.py b/tests/components/broadlink/test_switch.py index 93bad2db295..2d4eb8e0e0b 100644 --- a/tests/components/broadlink/test_switch.py +++ b/tests/components/broadlink/test_switch.py @@ -1,4 +1,5 @@ """Tests for Broadlink switches.""" + from homeassistant.components.broadlink.const import DOMAIN from homeassistant.components.switch import ( DOMAIN as SWITCH_DOMAIN, diff --git a/tests/components/brother/__init__.py b/tests/components/brother/__init__.py index 8e24c2d8058..b5a3f8ed5ef 100644 --- a/tests/components/brother/__init__.py +++ b/tests/components/brother/__init__.py @@ -1,4 +1,5 @@ """Tests for Brother Printer integration.""" + import json from unittest.mock import patch @@ -23,9 +24,12 @@ async def init_integration( entry.add_to_hass(hass) if not skip_setup: - with patch("brother.Brother.initialize"), patch( - "brother.Brother._get_data", - return_value=json.loads(load_fixture("printer_data.json", "brother")), + with ( + patch("brother.Brother.initialize"), + patch( + "brother.Brother._get_data", + return_value=json.loads(load_fixture("printer_data.json", "brother")), + ), ): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/brother/conftest.py b/tests/components/brother/conftest.py index 9e81cce9d12..1834cb2c36b 100644 --- a/tests/components/brother/conftest.py +++ b/tests/components/brother/conftest.py @@ -1,4 +1,5 @@ """Test fixtures for brother.""" + from collections.abc import Generator from unittest.mock import AsyncMock, patch diff --git a/tests/components/brother/test_config_flow.py b/tests/components/brother/test_config_flow.py index 3d83ecfcb7c..2eff4ed2770 100644 --- a/tests/components/brother/test_config_flow.py +++ b/tests/components/brother/test_config_flow.py @@ -1,4 +1,5 @@ """Define tests for the Brother Printer config flow.""" + from ipaddress import ip_address import json from unittest.mock import patch @@ -32,9 +33,12 @@ async def test_show_form(hass: HomeAssistant) -> None: async def test_create_entry_with_hostname(hass: HomeAssistant) -> None: """Test that the user step works with printer hostname.""" - with patch("brother.Brother.initialize"), patch( - "brother.Brother._get_data", - return_value=json.loads(load_fixture("printer_data.json", "brother")), + with ( + patch("brother.Brother.initialize"), + patch( + "brother.Brother._get_data", + return_value=json.loads(load_fixture("printer_data.json", "brother")), + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -50,9 +54,12 @@ async def test_create_entry_with_hostname(hass: HomeAssistant) -> None: async def test_create_entry_with_ipv4_address(hass: HomeAssistant) -> None: """Test that the user step works with printer IPv4 address.""" - with patch("brother.Brother.initialize"), patch( - "brother.Brother._get_data", - return_value=json.loads(load_fixture("printer_data.json", "brother")), + with ( + patch("brother.Brother.initialize"), + patch( + "brother.Brother._get_data", + return_value=json.loads(load_fixture("printer_data.json", "brother")), + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONFIG @@ -66,9 +73,12 @@ async def test_create_entry_with_ipv4_address(hass: HomeAssistant) -> None: async def test_create_entry_with_ipv6_address(hass: HomeAssistant) -> None: """Test that the user step works with printer IPv6 address.""" - with patch("brother.Brother.initialize"), patch( - "brother.Brother._get_data", - return_value=json.loads(load_fixture("printer_data.json", "brother")), + with ( + patch("brother.Brother.initialize"), + patch( + "brother.Brother._get_data", + return_value=json.loads(load_fixture("printer_data.json", "brother")), + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -96,8 +106,9 @@ async def test_invalid_hostname(hass: HomeAssistant) -> None: @pytest.mark.parametrize("exc", [ConnectionError, TimeoutError]) async def test_connection_error(hass: HomeAssistant, exc: Exception) -> None: """Test connection to host error.""" - with patch("brother.Brother.initialize"), patch( - "brother.Brother._get_data", side_effect=exc + with ( + patch("brother.Brother.initialize"), + patch("brother.Brother._get_data", side_effect=exc), ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONFIG @@ -108,8 +119,9 @@ async def test_connection_error(hass: HomeAssistant, exc: Exception) -> None: async def test_snmp_error(hass: HomeAssistant) -> None: """Test SNMP error.""" - with patch("brother.Brother.initialize"), patch( - "brother.Brother._get_data", side_effect=SnmpError("error") + with ( + patch("brother.Brother.initialize"), + patch("brother.Brother._get_data", side_effect=SnmpError("error")), ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONFIG @@ -120,8 +132,9 @@ async def test_snmp_error(hass: HomeAssistant) -> None: async def test_unsupported_model_error(hass: HomeAssistant) -> None: """Test unsupported printer model error.""" - with patch("brother.Brother.initialize"), patch( - "brother.Brother._get_data", side_effect=UnsupportedModelError("error") + with ( + patch("brother.Brother.initialize"), + patch("brother.Brother._get_data", side_effect=UnsupportedModelError("error")), ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONFIG @@ -133,9 +146,12 @@ async def test_unsupported_model_error(hass: HomeAssistant) -> None: async def test_device_exists_abort(hass: HomeAssistant) -> None: """Test we abort config flow if Brother printer already configured.""" - with patch("brother.Brother.initialize"), patch( - "brother.Brother._get_data", - return_value=json.loads(load_fixture("printer_data.json", "brother")), + with ( + patch("brother.Brother.initialize"), + patch( + "brother.Brother._get_data", + return_value=json.loads(load_fixture("printer_data.json", "brother")), + ), ): MockConfigEntry(domain=DOMAIN, unique_id="0123456789", data=CONFIG).add_to_hass( hass @@ -151,8 +167,9 @@ async def test_device_exists_abort(hass: HomeAssistant) -> None: @pytest.mark.parametrize("exc", [ConnectionError, TimeoutError, SnmpError("error")]) async def test_zeroconf_exception(hass: HomeAssistant, exc: Exception) -> None: """Test we abort zeroconf flow on exception.""" - with patch("brother.Brother.initialize"), patch( - "brother.Brother._get_data", side_effect=exc + with ( + patch("brother.Brother.initialize"), + patch("brother.Brother._get_data", side_effect=exc), ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -174,9 +191,10 @@ async def test_zeroconf_exception(hass: HomeAssistant, exc: Exception) -> None: async def test_zeroconf_unsupported_model(hass: HomeAssistant) -> None: """Test unsupported printer model error.""" - with patch("brother.Brother.initialize"), patch( - "brother.Brother._get_data" - ) as mock_get_data: + with ( + patch("brother.Brother.initialize"), + patch("brother.Brother._get_data") as mock_get_data, + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, @@ -198,9 +216,12 @@ async def test_zeroconf_unsupported_model(hass: HomeAssistant) -> None: async def test_zeroconf_device_exists_abort(hass: HomeAssistant) -> None: """Test we abort zeroconf flow if Brother printer already configured.""" - with patch("brother.Brother.initialize"), patch( - "brother.Brother._get_data", - return_value=json.loads(load_fixture("printer_data.json", "brother")), + with ( + patch("brother.Brother.initialize"), + patch( + "brother.Brother._get_data", + return_value=json.loads(load_fixture("printer_data.json", "brother")), + ), ): entry = MockConfigEntry( domain=DOMAIN, @@ -234,9 +255,10 @@ async def test_zeroconf_no_probe_existing_device(hass: HomeAssistant) -> None: """Test we do not probe the device is the host is already configured.""" entry = MockConfigEntry(domain=DOMAIN, unique_id="0123456789", data=CONFIG) entry.add_to_hass(hass) - with patch("brother.Brother.initialize"), patch( - "brother.Brother._get_data" - ) as mock_get_data: + with ( + patch("brother.Brother.initialize"), + patch("brother.Brother._get_data") as mock_get_data, + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, @@ -259,9 +281,12 @@ async def test_zeroconf_no_probe_existing_device(hass: HomeAssistant) -> None: async def test_zeroconf_confirm_create_entry(hass: HomeAssistant) -> None: """Test zeroconf confirmation and create config entry.""" - with patch("brother.Brother.initialize"), patch( - "brother.Brother._get_data", - return_value=json.loads(load_fixture("printer_data.json", "brother")), + with ( + patch("brother.Brother.initialize"), + patch( + "brother.Brother._get_data", + return_value=json.loads(load_fixture("printer_data.json", "brother")), + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, diff --git a/tests/components/brother/test_diagnostics.py b/tests/components/brother/test_diagnostics.py index 26ed77931b4..2ea9faa151e 100644 --- a/tests/components/brother/test_diagnostics.py +++ b/tests/components/brother/test_diagnostics.py @@ -1,4 +1,5 @@ """Test Brother diagnostics.""" + from datetime import datetime import json from unittest.mock import Mock, patch @@ -24,11 +25,13 @@ async def test_entry_diagnostics( entry = await init_integration(hass, skip_setup=True) test_time = datetime(2019, 11, 11, 9, 10, 32, tzinfo=UTC) - with patch("brother.Brother.initialize"), patch( - "brother.datetime", now=Mock(return_value=test_time) - ), patch( - "brother.Brother._get_data", - return_value=json.loads(load_fixture("printer_data.json", "brother")), + with ( + patch("brother.Brother.initialize"), + patch("brother.datetime", now=Mock(return_value=test_time)), + patch( + "brother.Brother._get_data", + return_value=json.loads(load_fixture("printer_data.json", "brother")), + ), ): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/brother/test_init.py b/tests/components/brother/test_init.py index cd439a3a41f..582e64c71ae 100644 --- a/tests/components/brother/test_init.py +++ b/tests/components/brother/test_init.py @@ -1,4 +1,5 @@ """Test init of Brother integration.""" + from unittest.mock import patch from brother import SnmpError @@ -33,8 +34,9 @@ async def test_config_not_ready(hass: HomeAssistant) -> None: data={CONF_HOST: "localhost", CONF_TYPE: "laser"}, ) - with patch("brother.Brother.initialize"), patch( - "brother.Brother._get_data", side_effect=ConnectionError() + with ( + patch("brother.Brother.initialize"), + patch("brother.Brother._get_data", side_effect=ConnectionError()), ): entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) diff --git a/tests/components/brother/test_sensor.py b/tests/components/brother/test_sensor.py index 58690e5605e..ff29f8cb368 100644 --- a/tests/components/brother/test_sensor.py +++ b/tests/components/brother/test_sensor.py @@ -1,4 +1,5 @@ """Test sensor of Brother integration.""" + from datetime import datetime, timedelta import json from unittest.mock import Mock, patch @@ -45,11 +46,13 @@ async def test_sensors(hass: HomeAssistant, entity_registry: er.EntityRegistry) disabled_by=None, ) test_time = datetime(2019, 11, 11, 9, 10, 32, tzinfo=UTC) - with patch("brother.Brother.initialize"), patch( - "brother.datetime", now=Mock(return_value=test_time) - ), patch( - "brother.Brother._get_data", - return_value=json.loads(load_fixture("printer_data.json", "brother")), + with ( + patch("brother.Brother.initialize"), + patch("brother.datetime", now=Mock(return_value=test_time)), + patch( + "brother.Brother._get_data", + return_value=json.loads(load_fixture("printer_data.json", "brother")), + ), ): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -393,8 +396,9 @@ async def test_availability(hass: HomeAssistant) -> None: assert state.state == "waiting" future = utcnow() + timedelta(minutes=5) - with patch("brother.Brother.initialize"), patch( - "brother.Brother._get_data", side_effect=ConnectionError() + with ( + patch("brother.Brother.initialize"), + patch("brother.Brother._get_data", side_effect=ConnectionError()), ): async_fire_time_changed(hass, future) await hass.async_block_till_done() @@ -404,9 +408,12 @@ async def test_availability(hass: HomeAssistant) -> None: assert state.state == STATE_UNAVAILABLE future = utcnow() + timedelta(minutes=10) - with patch("brother.Brother.initialize"), patch( - "brother.Brother._get_data", - return_value=json.loads(load_fixture("printer_data.json", "brother")), + with ( + patch("brother.Brother.initialize"), + patch( + "brother.Brother._get_data", + return_value=json.loads(load_fixture("printer_data.json", "brother")), + ), ): async_fire_time_changed(hass, future) await hass.async_block_till_done() diff --git a/tests/components/brottsplatskartan/conftest.py b/tests/components/brottsplatskartan/conftest.py index 430fec9620b..6d3769edd71 100644 --- a/tests/components/brottsplatskartan/conftest.py +++ b/tests/components/brottsplatskartan/conftest.py @@ -1,4 +1,5 @@ """Test fixtures for Brottplatskartan.""" + from collections.abc import Generator from unittest.mock import AsyncMock, patch diff --git a/tests/components/brottsplatskartan/test_config_flow.py b/tests/components/brottsplatskartan/test_config_flow.py index f27139ad381..3571277c6f3 100644 --- a/tests/components/brottsplatskartan/test_config_flow.py +++ b/tests/components/brottsplatskartan/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Brottsplatskartan config flow.""" + from __future__ import annotations import pytest diff --git a/tests/components/brottsplatskartan/test_init.py b/tests/components/brottsplatskartan/test_init.py index 6205fddc9da..e5d18d28203 100644 --- a/tests/components/brottsplatskartan/test_init.py +++ b/tests/components/brottsplatskartan/test_init.py @@ -1,4 +1,5 @@ """Test Brottsplatskartan component setup process.""" + from __future__ import annotations from unittest.mock import patch diff --git a/tests/components/brunt/conftest.py b/tests/components/brunt/conftest.py index 8ae0bbaf317..f9a518292ac 100644 --- a/tests/components/brunt/conftest.py +++ b/tests/components/brunt/conftest.py @@ -1,4 +1,5 @@ """Configuration for brunt tests.""" + from collections.abc import Generator from unittest.mock import AsyncMock, patch diff --git a/tests/components/brunt/test_config_flow.py b/tests/components/brunt/test_config_flow.py index c8b0a3955c8..dfa1e9f992a 100644 --- a/tests/components/brunt/test_config_flow.py +++ b/tests/components/brunt/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Brunt config flow.""" + from unittest.mock import AsyncMock, Mock, patch from aiohttp import ClientResponseError diff --git a/tests/components/bsblan/conftest.py b/tests/components/bsblan/conftest.py index b7939e4cb50..a9120832ac4 100644 --- a/tests/components/bsblan/conftest.py +++ b/tests/components/bsblan/conftest.py @@ -1,4 +1,5 @@ """Fixtures for BSBLAN integration tests.""" + from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch @@ -42,10 +43,9 @@ def mock_setup_entry() -> Generator[AsyncMock, None, None]: def mock_bsblan(request: pytest.FixtureRequest) -> Generator[None, MagicMock, None]: """Return a mocked BSBLAN client.""" - with patch( - "homeassistant.components.bsblan.BSBLAN", autospec=True - ) as bsblan_mock, patch( - "homeassistant.components.bsblan.config_flow.BSBLAN", new=bsblan_mock + with ( + patch("homeassistant.components.bsblan.BSBLAN", autospec=True) as bsblan_mock, + patch("homeassistant.components.bsblan.config_flow.BSBLAN", new=bsblan_mock), ): bsblan = bsblan_mock.return_value bsblan.info.return_value = Info.parse_raw(load_fixture("info.json", DOMAIN)) diff --git a/tests/components/bsblan/test_config_flow.py b/tests/components/bsblan/test_config_flow.py index d82c32463d8..db2b0f8f85c 100644 --- a/tests/components/bsblan/test_config_flow.py +++ b/tests/components/bsblan/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for the BSBLan device config flow.""" + from unittest.mock import AsyncMock, MagicMock from bsblan import BSBLANConnectionError diff --git a/tests/components/bsblan/test_init.py b/tests/components/bsblan/test_init.py index 34ee30a35e1..a9c3605f67f 100644 --- a/tests/components/bsblan/test_init.py +++ b/tests/components/bsblan/test_init.py @@ -1,4 +1,5 @@ """Tests for the BSBLan integration.""" + from unittest.mock import MagicMock from bsblan import BSBLANConnectionError diff --git a/tests/components/bthome/__init__.py b/tests/components/bthome/__init__.py index de46cd8231d..ae7231b8740 100644 --- a/tests/components/bthome/__init__.py +++ b/tests/components/bthome/__init__.py @@ -1,6 +1,5 @@ """Tests for the BTHome integration.""" - from homeassistant.components.bluetooth import BluetoothServiceInfoBleak from tests.components.bluetooth import generate_advertisement_data, generate_ble_device diff --git a/tests/components/bthome/test_binary_sensor.py b/tests/components/bthome/test_binary_sensor.py index c38bec3ba44..dd12bbd4ee0 100644 --- a/tests/components/bthome/test_binary_sensor.py +++ b/tests/components/bthome/test_binary_sensor.py @@ -1,4 +1,5 @@ """Test BTHome binary sensors.""" + from datetime import timedelta import logging import time @@ -67,7 +68,7 @@ _LOGGER = logging.getLogger(__name__) "A4:C1:38:8D:18:B2", make_bthome_v1_adv( "A4:C1:38:8D:18:B2", - b"\x02\x0F\x01", + b"\x02\x0f\x01", ), None, [ @@ -153,7 +154,7 @@ async def test_v1_binary_sensors( "A4:C1:38:8D:18:B2", make_bthome_v2_adv( "A4:C1:38:8D:18:B2", - b"\x40\x0F\x01", + b"\x40\x0f\x01", ), None, [ diff --git a/tests/components/bthome/test_config_flow.py b/tests/components/bthome/test_config_flow.py index ee983148fd4..1a785858752 100644 --- a/tests/components/bthome/test_config_flow.py +++ b/tests/components/bthome/test_config_flow.py @@ -1,4 +1,5 @@ """Test the BTHome config flow.""" + from unittest.mock import patch from bthome_ble import BTHomeBluetoothDeviceData as DeviceData @@ -40,12 +41,15 @@ async def test_async_step_bluetooth_valid_device(hass: HomeAssistant) -> None: async def test_async_step_bluetooth_during_onboarding(hass: HomeAssistant) -> None: """Test discovery via bluetooth during onboarding.""" - with patch( - "homeassistant.components.bthome.async_setup_entry", return_value=True - ) as mock_setup_entry, patch( - "homeassistant.components.onboarding.async_is_onboarded", - return_value=False, - ) as mock_onboarding: + with ( + patch( + "homeassistant.components.bthome.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}, diff --git a/tests/components/bthome/test_device_trigger.py b/tests/components/bthome/test_device_trigger.py index 85169e80394..240eb7ab3d8 100644 --- a/tests/components/bthome/test_device_trigger.py +++ b/tests/components/bthome/test_device_trigger.py @@ -1,4 +1,5 @@ """Test BTHome BLE events.""" + import pytest from homeassistant.components import automation @@ -58,7 +59,7 @@ async def test_event_long_press(hass: HomeAssistant) -> None: # Emit long press event inject_bluetooth_service_info_bleak( hass, - make_bthome_v2_adv(mac, b"\x40\x3A\x04"), + make_bthome_v2_adv(mac, b"\x40\x3a\x04"), ) # wait for the event @@ -81,7 +82,7 @@ async def test_event_rotate_dimmer(hass: HomeAssistant) -> None: # Emit rotate dimmer 3 steps left event inject_bluetooth_service_info_bleak( hass, - make_bthome_v2_adv(mac, b"\x40\x3C\x01\x03"), + make_bthome_v2_adv(mac, b"\x40\x3c\x01\x03"), ) # wait for the event @@ -104,7 +105,7 @@ async def test_get_triggers_button(hass: HomeAssistant) -> None: # Emit long press event so it creates the device in the registry inject_bluetooth_service_info_bleak( hass, - make_bthome_v2_adv(mac, b"\x40\x3A\x04"), + make_bthome_v2_adv(mac, b"\x40\x3a\x04"), ) # wait for the event @@ -140,7 +141,7 @@ async def test_get_triggers_dimmer(hass: HomeAssistant) -> None: # Emit rotate left with 3 steps event so it creates the device in the registry inject_bluetooth_service_info_bleak( hass, - make_bthome_v2_adv(mac, b"\x40\x3C\x01\x03"), + make_bthome_v2_adv(mac, b"\x40\x3c\x01\x03"), ) # wait for the event @@ -236,7 +237,7 @@ async def test_if_fires_on_motion_detected(hass: HomeAssistant, calls) -> None: # Emit a button event so it creates the device in the registry inject_bluetooth_service_info_bleak( hass, - make_bthome_v2_adv(mac, b"\x40\x3A\x03"), + make_bthome_v2_adv(mac, b"\x40\x3a\x03"), ) # # wait for the event @@ -271,7 +272,7 @@ async def test_if_fires_on_motion_detected(hass: HomeAssistant, calls) -> None: # Emit long press event inject_bluetooth_service_info_bleak( hass, - make_bthome_v2_adv(mac, b"\x40\x3A\x04"), + make_bthome_v2_adv(mac, b"\x40\x3a\x04"), ) await hass.async_block_till_done() diff --git a/tests/components/bthome/test_event.py b/tests/components/bthome/test_event.py index f6cf3fd49c7..34a74087110 100644 --- a/tests/components/bthome/test_event.py +++ b/tests/components/bthome/test_event.py @@ -23,7 +23,7 @@ from tests.components.bluetooth import ( "A4:C1:38:8D:18:B2", make_bthome_v2_adv( "A4:C1:38:8D:18:B2", - b"\x40\x3A\x00\x3A\x01\x3A\x03", + b"\x40\x3a\x00\x3a\x01\x3a\x03", ), None, [ @@ -43,7 +43,7 @@ from tests.components.bluetooth import ( "A4:C1:38:8D:18:B2", make_bthome_v2_adv( "A4:C1:38:8D:18:B2", - b"\x40\x3A\x04", + b"\x40\x3a\x04", ), None, [ diff --git a/tests/components/bthome/test_logbook.py b/tests/components/bthome/test_logbook.py index f68197f9fe5..60b211e1d75 100644 --- a/tests/components/bthome/test_logbook.py +++ b/tests/components/bthome/test_logbook.py @@ -1,4 +1,5 @@ """The tests for bthome logbook.""" + from homeassistant.components.bthome.const import ( BTHOME_BLE_EVENT, DOMAIN, diff --git a/tests/components/bthome/test_sensor.py b/tests/components/bthome/test_sensor.py index 481520f0434..f1cffa8583f 100644 --- a/tests/components/bthome/test_sensor.py +++ b/tests/components/bthome/test_sensor.py @@ -1,4 +1,5 @@ """Test the BTHome sensors.""" + from datetime import timedelta import logging import time @@ -161,7 +162,7 @@ _LOGGER = logging.getLogger(__name__) "A4:C1:38:8D:18:B2", make_bthome_v1_adv( "A4:C1:38:8D:18:B2", - b"\x23\x08\xCA\x06", + b"\x23\x08\xca\x06", ), None, [ @@ -481,7 +482,7 @@ async def test_v1_sensors( "A4:C1:38:8D:18:B2", make_bthome_v2_adv( "A4:C1:38:8D:18:B2", - b"\x40\x06\x5E\x1F", + b"\x40\x06\x5e\x1f", ), None, [ @@ -498,7 +499,7 @@ async def test_v1_sensors( "A4:C1:38:8D:18:B2", make_bthome_v2_adv( "A4:C1:38:8D:18:B2", - b"\x40\x07\x3E\x1d", + b"\x40\x07\x3e\x1d", ), None, [ @@ -515,7 +516,7 @@ async def test_v1_sensors( "A4:C1:38:8D:18:B2", make_bthome_v2_adv( "A4:C1:38:8D:18:B2", - b"\x40\x08\xCA\x06", + b"\x40\x08\xca\x06", ), None, [ @@ -676,7 +677,7 @@ async def test_v1_sensors( "A4:C1:38:8D:18:B2", make_bthome_v2_adv( "A4:C1:38:8D:18:B2", - b"\x40\x3F\x02\x0c", + b"\x40\x3f\x02\x0c", ), None, [ @@ -693,7 +694,7 @@ async def test_v1_sensors( "A4:C1:38:8D:18:B2", make_bthome_v2_adv( "A4:C1:38:8D:18:B2", - b"\x40\x40\x0C\x00", + b"\x40\x40\x0c\x00", ), None, [ @@ -710,7 +711,7 @@ async def test_v1_sensors( "A4:C1:38:8D:18:B2", make_bthome_v2_adv( "A4:C1:38:8D:18:B2", - b"\x40\x41\x4E\x00", + b"\x40\x41\x4e\x00", ), None, [ @@ -727,7 +728,7 @@ async def test_v1_sensors( "A4:C1:38:8D:18:B2", make_bthome_v2_adv( "A4:C1:38:8D:18:B2", - b"\x40\x42\x4E\x34\x00", + b"\x40\x42\x4e\x34\x00", ), None, [ @@ -744,7 +745,7 @@ async def test_v1_sensors( "A4:C1:38:8D:18:B2", make_bthome_v2_adv( "A4:C1:38:8D:18:B2", - b"\x40\x43\x4E\x34", + b"\x40\x43\x4e\x34", ), None, [ @@ -761,7 +762,7 @@ async def test_v1_sensors( "A4:C1:38:8D:18:B2", make_bthome_v2_adv( "A4:C1:38:8D:18:B2", - b"\x40\x44\x4E\x34", + b"\x40\x44\x4e\x34", ), None, [ @@ -828,7 +829,7 @@ async def test_v1_sensors( "A4:C1:38:8D:18:B2", make_bthome_v2_adv( "A4:C1:38:8D:18:B2", - b"\x40\x48\xDC\x87", + b"\x40\x48\xdc\x87", ), None, [ @@ -845,7 +846,7 @@ async def test_v1_sensors( "A4:C1:38:8D:18:B2", make_bthome_v2_adv( "A4:C1:38:8D:18:B2", - b"\x40\x49\xDC\x87", + b"\x40\x49\xdc\x87", ), None, [ @@ -862,7 +863,7 @@ async def test_v1_sensors( "A4:C1:38:8D:18:B2", make_bthome_v2_adv( "A4:C1:38:8D:18:B2", - b"\x44\x50\x5D\x39\x61\x64", + b"\x44\x50\x5d\x39\x61\x64", ), None, [ @@ -946,7 +947,7 @@ async def test_v1_sensors( "A4:C1:38:8D:18:B2", make_bthome_v2_adv( "A4:C1:38:8D:18:B2", - b"\x44\x53\x0C\x48\x65\x6C\x6C\x6F\x20\x57\x6F\x72\x6C\x64\x21", + b"\x44\x53\x0c\x48\x65\x6c\x6c\x6f\x20\x57\x6f\x72\x6c\x64\x21", ), None, [ @@ -961,7 +962,7 @@ async def test_v1_sensors( "A4:C1:38:8D:18:B2", make_bthome_v2_adv( "A4:C1:38:8D:18:B2", - b"\x44\x54\x0C\x48\x65\x6C\x6C\x6F\x20\x57\x6F\x72\x6C\x64\x21", + b"\x44\x54\x0c\x48\x65\x6c\x6c\x6f\x20\x57\x6f\x72\x6c\x64\x21", ), None, [ diff --git a/tests/components/buienradar/conftest.py b/tests/components/buienradar/conftest.py index b896e54e628..616976b292f 100644 --- a/tests/components/buienradar/conftest.py +++ b/tests/components/buienradar/conftest.py @@ -1,4 +1,5 @@ """Test fixtures for buienradar2.""" + from collections.abc import Generator from unittest.mock import AsyncMock, patch diff --git a/tests/components/buienradar/test_camera.py b/tests/components/buienradar/test_camera.py index f048f8d69a7..799fa37c7e3 100644 --- a/tests/components/buienradar/test_camera.py +++ b/tests/components/buienradar/test_camera.py @@ -1,4 +1,5 @@ """The tests for generic camera component.""" + import asyncio from contextlib import suppress import copy diff --git a/tests/components/buienradar/test_init.py b/tests/components/buienradar/test_init.py index 1f46cd667ea..dd09e3e5236 100644 --- a/tests/components/buienradar/test_init.py +++ b/tests/components/buienradar/test_init.py @@ -1,4 +1,5 @@ """Tests for the buienradar component.""" + from homeassistant.components.buienradar.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE diff --git a/tests/components/buienradar/test_sensor.py b/tests/components/buienradar/test_sensor.py index fb83d7a13db..ea5ef74f72e 100644 --- a/tests/components/buienradar/test_sensor.py +++ b/tests/components/buienradar/test_sensor.py @@ -1,4 +1,5 @@ """The tests for the Buienradar sensor platform.""" + from http import HTTPStatus from homeassistant.components.buienradar.const import DOMAIN diff --git a/tests/components/buienradar/test_weather.py b/tests/components/buienradar/test_weather.py index d4c4af5f62a..6a3b3e202b2 100644 --- a/tests/components/buienradar/test_weather.py +++ b/tests/components/buienradar/test_weather.py @@ -1,4 +1,5 @@ """The tests for the buienradar weather component.""" + from http import HTTPStatus from homeassistant.components.buienradar.const import DOMAIN diff --git a/tests/components/button/conftest.py b/tests/components/button/conftest.py new file mode 100644 index 00000000000..75d5509efc9 --- /dev/null +++ b/tests/components/button/conftest.py @@ -0,0 +1,51 @@ +"""Fixtures for the button entity component tests.""" + +import logging + +import pytest + +from homeassistant.components.button import DOMAIN, ButtonEntity +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType + +from .const import TEST_DOMAIN + +from tests.common import MockEntity, MockPlatform, mock_platform + +_LOGGER = logging.getLogger(__name__) + + +class MockButtonEntity(MockEntity, ButtonEntity): + """Mock Button class.""" + + def press(self) -> None: + """Press the button.""" + _LOGGER.info("The button has been pressed") + + +@pytest.fixture +async def setup_platform(hass: HomeAssistant) -> None: + """Set up the button entity platform.""" + + async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, + ) -> None: + """Set up test button platform.""" + async_add_entities( + [ + MockButtonEntity( + name="button 1", + unique_id="unique_button_1", + ), + ] + ) + + mock_platform( + hass, + f"{TEST_DOMAIN}.{DOMAIN}", + MockPlatform(async_setup_platform=async_setup_platform), + ) diff --git a/tests/components/button/const.py b/tests/components/button/const.py new file mode 100644 index 00000000000..9cb21e53550 --- /dev/null +++ b/tests/components/button/const.py @@ -0,0 +1,3 @@ +"""Constants for the button entity component tests.""" + +TEST_DOMAIN = "test" diff --git a/tests/components/button/test_device_action.py b/tests/components/button/test_device_action.py index 3fefa580724..f0d34e25e37 100644 --- a/tests/components/button/test_device_action.py +++ b/tests/components/button/test_device_action.py @@ -1,4 +1,5 @@ """The tests for Button device actions.""" + import pytest from pytest_unordered import unordered @@ -49,12 +50,12 @@ async def test_get_actions( @pytest.mark.parametrize( ("hidden_by", "entity_category"), - ( + [ (er.RegistryEntryHider.INTEGRATION, None), (er.RegistryEntryHider.USER, None), (None, EntityCategory.CONFIG), (None, EntityCategory.DIAGNOSTIC), - ), + ], ) async def test_get_actions_hidden_auxiliary( hass: HomeAssistant, diff --git a/tests/components/button/test_device_trigger.py b/tests/components/button/test_device_trigger.py index e231fc3ae19..034b8ed7e6e 100644 --- a/tests/components/button/test_device_trigger.py +++ b/tests/components/button/test_device_trigger.py @@ -1,4 +1,5 @@ """The tests for Button device triggers.""" + from __future__ import annotations import pytest @@ -58,12 +59,12 @@ async def test_get_triggers( @pytest.mark.parametrize( ("hidden_by", "entity_category"), - ( + [ (er.RegistryEntryHider.INTEGRATION, None), (er.RegistryEntryHider.USER, None), (None, EntityCategory.CONFIG), (None, EntityCategory.DIAGNOSTIC), - ), + ], ) async def test_get_triggers_hidden_auxiliary( hass, diff --git a/tests/components/button/test_init.py b/tests/components/button/test_init.py index acf7bd39e10..0641bbe29dc 100644 --- a/tests/components/button/test_init.py +++ b/tests/components/button/test_init.py @@ -1,4 +1,5 @@ """The tests for the Button component.""" + from collections.abc import Generator from datetime import timedelta from unittest.mock import MagicMock @@ -25,6 +26,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util +from .const import TEST_DOMAIN + from tests.common import ( MockConfigEntry, MockModule, @@ -35,8 +38,6 @@ from tests.common import ( mock_restore_cache, ) -TEST_DOMAIN = "test" - async def test_button(hass: HomeAssistant) -> None: """Test getting data from the mocked button entity.""" @@ -59,11 +60,9 @@ async def test_custom_integration( caplog: pytest.LogCaptureFixture, enable_custom_integrations: None, freezer: FrozenDateTimeFactory, + setup_platform: None, ) -> None: """Test we integration.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) await hass.async_block_till_done() @@ -97,14 +96,11 @@ async def test_custom_integration( async def test_restore_state( - hass: HomeAssistant, enable_custom_integrations: None + hass: HomeAssistant, enable_custom_integrations: None, setup_platform: None ) -> None: """Test we restore state integration.""" mock_restore_cache(hass, (State("button.button_1", "2021-01-01T23:59:59+00:00"),)) - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) await hass.async_block_till_done() @@ -112,14 +108,11 @@ async def test_restore_state( async def test_restore_state_does_not_restore_unavailable( - hass: HomeAssistant, enable_custom_integrations: None + hass: HomeAssistant, enable_custom_integrations: None, setup_platform: None ) -> None: """Test we restore state integration except for unavailable.""" mock_restore_cache(hass, (State("button.button_1", STATE_UNAVAILABLE),)) - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) await hass.async_block_till_done() diff --git a/tests/components/caldav/conftest.py b/tests/components/caldav/conftest.py index 504103afe13..686a0b75a76 100644 --- a/tests/components/caldav/conftest.py +++ b/tests/components/caldav/conftest.py @@ -1,4 +1,5 @@ """Test fixtures for caldav.""" + from unittest.mock import Mock, patch import pytest diff --git a/tests/components/caldav/test_calendar.py b/tests/components/caldav/test_calendar.py index 11f1524b4b0..942a4913f6e 100644 --- a/tests/components/caldav/test_calendar.py +++ b/tests/components/caldav/test_calendar.py @@ -1,4 +1,5 @@ """The tests for the webdav calendar component.""" + from collections.abc import Awaitable, Callable import datetime from http import HTTPStatus diff --git a/tests/components/caldav/test_todo.py b/tests/components/caldav/test_todo.py index 7b67a1af714..67fc5f7f443 100644 --- a/tests/components/caldav/test_todo.py +++ b/tests/components/caldav/test_todo.py @@ -1,4 +1,5 @@ """The tests for the webdav todo component.""" + from datetime import UTC, date, datetime from typing import Any from unittest.mock import MagicMock, Mock diff --git a/tests/components/calendar/conftest.py b/tests/components/calendar/conftest.py index 29d4bb9f5ff..7a3f27c8e08 100644 --- a/tests/components/calendar/conftest.py +++ b/tests/components/calendar/conftest.py @@ -1,4 +1,5 @@ """Test fixtures for calendar sensor platforms.""" + from collections.abc import Generator import datetime import secrets diff --git a/tests/components/calendar/test_init.py b/tests/components/calendar/test_init.py index d786ce8d8ad..c2842eafb2c 100644 --- a/tests/components/calendar/test_init.py +++ b/tests/components/calendar/test_init.py @@ -1,4 +1,5 @@ """The tests for the calendar component.""" + from __future__ import annotations from collections.abc import Generator diff --git a/tests/components/calendar/test_recorder.py b/tests/components/calendar/test_recorder.py index ef6c7658a89..aeddebc226c 100644 --- a/tests/components/calendar/test_recorder.py +++ b/tests/components/calendar/test_recorder.py @@ -1,4 +1,5 @@ """The tests for calendar recorder.""" + from datetime import timedelta from typing import Any diff --git a/tests/components/calendar/test_trigger.py b/tests/components/calendar/test_trigger.py index 0111f11c27b..050329cd855 100644 --- a/tests/components/calendar/test_trigger.py +++ b/tests/components/calendar/test_trigger.py @@ -6,6 +6,7 @@ tests use a fixture that mocks out events returned by the calendar entity, and create events using a relative time offset and then advance the clock forward exercising the triggers. """ + from __future__ import annotations from collections.abc import AsyncIterator, Callable, Generator @@ -56,7 +57,7 @@ class FakeSchedule: """Test fixture class for return events in a specific date range.""" def __init__(self, hass: HomeAssistant, freezer: FrozenDateTimeFactory) -> None: - """Initiailize FakeSchedule.""" + """Initialize FakeSchedule.""" self.hass = hass self.freezer = freezer diff --git a/tests/components/camera/common.py b/tests/components/camera/common.py index e30de46c07b..9cacf85d907 100644 --- a/tests/components/camera/common.py +++ b/tests/components/camera/common.py @@ -3,6 +3,7 @@ All containing methods are legacy helpers that should not be used by new components. Instead call the service directly. """ + from unittest.mock import Mock EMPTY_8_6_JPEG = b"empty_8_6" diff --git a/tests/components/camera/conftest.py b/tests/components/camera/conftest.py index bb5812680f0..ee8c5df7d65 100644 --- a/tests/components/camera/conftest.py +++ b/tests/components/camera/conftest.py @@ -1,4 +1,5 @@ """Test helpers for camera.""" + from unittest.mock import PropertyMock, patch import pytest @@ -62,12 +63,15 @@ async def mock_camera_web_rtc_fixture(hass): ) await hass.async_block_till_done() - with patch( - "homeassistant.components.camera.Camera.frontend_stream_type", - new_callable=PropertyMock(return_value=StreamType.WEB_RTC), - ), patch( - "homeassistant.components.camera.Camera.async_handle_web_rtc_offer", - return_value=WEBRTC_ANSWER, + with ( + patch( + "homeassistant.components.camera.Camera.frontend_stream_type", + new_callable=PropertyMock(return_value=StreamType.WEB_RTC), + ), + patch( + "homeassistant.components.camera.Camera.async_handle_web_rtc_offer", + return_value=WEBRTC_ANSWER, + ), ): yield @@ -84,14 +88,16 @@ async def mock_camera_with_device_fixture(): def __get__(self, obj, obj_type=None): return obj.name - with patch( - "homeassistant.components.camera.Camera.has_entity_name", - new_callable=PropertyMock(return_value=True), - ), patch( - "homeassistant.components.camera.Camera.unique_id", new=UniqueIdMock() - ), patch( - "homeassistant.components.camera.Camera.device_info", - new_callable=PropertyMock(return_value=dev_info), + with ( + patch( + "homeassistant.components.camera.Camera.has_entity_name", + new_callable=PropertyMock(return_value=True), + ), + patch("homeassistant.components.camera.Camera.unique_id", new=UniqueIdMock()), + patch( + "homeassistant.components.camera.Camera.device_info", + new_callable=PropertyMock(return_value=dev_info), + ), ): yield diff --git a/tests/components/camera/test_img_util.py b/tests/components/camera/test_img_util.py index 5be8e49c1d3..5c3d3712f38 100644 --- a/tests/components/camera/test_img_util.py +++ b/tests/components/camera/test_img_util.py @@ -1,4 +1,5 @@ """Test img_util module.""" + from unittest.mock import patch import pytest @@ -37,25 +38,33 @@ def test_scale_jpeg_camera_image() -> None: camera_image = Image("image/jpeg", EMPTY_16_12_JPEG) turbo_jpeg = mock_turbo_jpeg(first_width=16, first_height=12) - with patch("turbojpeg.TurboJPEG", return_value=False): + with patch( + "homeassistant.components.camera.img_util.TurboJPEG", return_value=False + ): TurboJPEGSingleton() assert scale_jpeg_camera_image(camera_image, 16, 12) == camera_image.content turbo_jpeg = mock_turbo_jpeg(first_width=16, first_height=12) turbo_jpeg.decode_header.side_effect = OSError - with patch("turbojpeg.TurboJPEG", return_value=turbo_jpeg): + with patch( + "homeassistant.components.camera.img_util.TurboJPEG", return_value=turbo_jpeg + ): TurboJPEGSingleton() assert scale_jpeg_camera_image(camera_image, 16, 12) == camera_image.content turbo_jpeg = mock_turbo_jpeg(first_width=16, first_height=12) - with patch("turbojpeg.TurboJPEG", return_value=turbo_jpeg): + with patch( + "homeassistant.components.camera.img_util.TurboJPEG", return_value=turbo_jpeg + ): TurboJPEGSingleton() assert scale_jpeg_camera_image(camera_image, 16, 12) == EMPTY_16_12_JPEG turbo_jpeg = mock_turbo_jpeg( first_width=16, first_height=12, second_width=8, second_height=6 ) - with patch("turbojpeg.TurboJPEG", return_value=turbo_jpeg): + with patch( + "homeassistant.components.camera.img_util.TurboJPEG", return_value=turbo_jpeg + ): TurboJPEGSingleton() jpeg_bytes = scale_jpeg_camera_image(camera_image, 8, 6) @@ -64,7 +73,9 @@ def test_scale_jpeg_camera_image() -> None: turbo_jpeg = mock_turbo_jpeg( first_width=640, first_height=480, second_width=640, second_height=480 ) - with patch("turbojpeg.TurboJPEG", return_value=turbo_jpeg): + with patch( + "homeassistant.components.camera.img_util.TurboJPEG", return_value=turbo_jpeg + ): TurboJPEGSingleton() jpeg_bytes = scale_jpeg_camera_image(camera_image, 320, 480) @@ -74,7 +85,9 @@ def test_scale_jpeg_camera_image() -> None: def test_turbojpeg_load_failure() -> None: """Handle libjpegturbo not being installed.""" _clear_turbojpeg_singleton() - with patch("turbojpeg.TurboJPEG", side_effect=Exception): + with patch( + "homeassistant.components.camera.img_util.TurboJPEG", side_effect=Exception + ): TurboJPEGSingleton() assert TurboJPEGSingleton.instance() is False diff --git a/tests/components/camera/test_init.py b/tests/components/camera/test_init.py index 528c13bc08c..ccec2b6f50c 100644 --- a/tests/components/camera/test_init.py +++ b/tests/components/camera/test_init.py @@ -1,4 +1,5 @@ """The tests for the camera component.""" + from http import HTTPStatus import io from types import ModuleType @@ -111,14 +112,17 @@ async def test_get_image_from_camera_with_width_height( turbo_jpeg = mock_turbo_jpeg( first_width=16, first_height=12, second_width=300, second_height=200 ) - with patch( - "homeassistant.components.camera.img_util.TurboJPEGSingleton.instance", - return_value=turbo_jpeg, - ), patch( - "homeassistant.components.demo.camera.Path.read_bytes", - autospec=True, - return_value=b"Test", - ) as mock_camera: + with ( + patch( + "homeassistant.components.camera.img_util.TurboJPEGSingleton.instance", + return_value=turbo_jpeg, + ), + patch( + "homeassistant.components.demo.camera.Path.read_bytes", + autospec=True, + return_value=b"Test", + ) as mock_camera, + ): image = await camera.async_get_image( hass, "camera.demo_camera", width=640, height=480 ) @@ -135,14 +139,17 @@ async def test_get_image_from_camera_with_width_height_scaled( turbo_jpeg = mock_turbo_jpeg( first_width=16, first_height=12, second_width=300, second_height=200 ) - with patch( - "homeassistant.components.camera.img_util.TurboJPEGSingleton.instance", - return_value=turbo_jpeg, - ), patch( - "homeassistant.components.demo.camera.Path.read_bytes", - autospec=True, - return_value=b"Valid jpeg", - ) as mock_camera: + with ( + patch( + "homeassistant.components.camera.img_util.TurboJPEGSingleton.instance", + return_value=turbo_jpeg, + ), + patch( + "homeassistant.components.demo.camera.Path.read_bytes", + autospec=True, + return_value=b"Valid jpeg", + ) as mock_camera, + ): image = await camera.async_get_image( hass, "camera.demo_camera", width=4, height=3 ) @@ -160,14 +167,17 @@ async def test_get_image_from_camera_not_jpeg( turbo_jpeg = mock_turbo_jpeg( first_width=16, first_height=12, second_width=300, second_height=200 ) - with patch( - "homeassistant.components.camera.img_util.TurboJPEGSingleton.instance", - return_value=turbo_jpeg, - ), patch( - "homeassistant.components.demo.camera.Path.read_bytes", - autospec=True, - return_value=b"png", - ) as mock_camera: + with ( + patch( + "homeassistant.components.camera.img_util.TurboJPEGSingleton.instance", + return_value=turbo_jpeg, + ), + patch( + "homeassistant.components.demo.camera.Path.read_bytes", + autospec=True, + return_value=b"png", + ) as mock_camera, + ): image = await camera.async_get_image( hass, "camera.demo_camera_png", width=4, height=3 ) @@ -192,28 +202,37 @@ async def test_get_image_without_exists_camera( hass: HomeAssistant, image_mock_url ) -> None: """Try to get image without exists camera.""" - with patch( - "homeassistant.helpers.entity_component.EntityComponent.get_entity", - return_value=None, - ), pytest.raises(HomeAssistantError): + with ( + patch( + "homeassistant.helpers.entity_component.EntityComponent.get_entity", + return_value=None, + ), + pytest.raises(HomeAssistantError), + ): await camera.async_get_image(hass, "camera.demo_camera") async def test_get_image_with_timeout(hass: HomeAssistant, image_mock_url) -> None: """Try to get image with timeout.""" - with patch( - "homeassistant.components.demo.camera.DemoCamera.async_camera_image", - side_effect=TimeoutError, - ), pytest.raises(HomeAssistantError): + with ( + patch( + "homeassistant.components.demo.camera.DemoCamera.async_camera_image", + side_effect=TimeoutError, + ), + pytest.raises(HomeAssistantError), + ): await camera.async_get_image(hass, "camera.demo_camera") async def test_get_image_fails(hass: HomeAssistant, image_mock_url) -> None: """Try to get image with timeout.""" - with patch( - "homeassistant.components.demo.camera.DemoCamera.async_camera_image", - return_value=None, - ), pytest.raises(HomeAssistantError): + with ( + patch( + "homeassistant.components.demo.camera.DemoCamera.async_camera_image", + return_value=None, + ), + pytest.raises(HomeAssistantError), + ): await camera.async_get_image(hass, "camera.demo_camera") @@ -221,9 +240,13 @@ async def test_snapshot_service(hass: HomeAssistant, mock_camera) -> None: """Test snapshot service.""" mopen = mock_open() - with patch("homeassistant.components.camera.open", mopen, create=True), patch( - "homeassistant.components.camera.os.makedirs", - ), patch.object(hass.config, "is_allowed_path", return_value=True): + with ( + patch("homeassistant.components.camera.open", mopen, create=True), + patch( + "homeassistant.components.camera.os.makedirs", + ), + patch.object(hass.config, "is_allowed_path", return_value=True), + ): await hass.services.async_call( camera.DOMAIN, camera.SERVICE_SNAPSHOT, @@ -246,9 +269,13 @@ async def test_snapshot_service_not_allowed_path( """Test snapshot service with a not allowed path.""" mopen = mock_open() - with patch("homeassistant.components.camera.open", mopen, create=True), patch( - "homeassistant.components.camera.os.makedirs", - ), pytest.raises(HomeAssistantError, match="/test/snapshot.jpg"): + with ( + patch("homeassistant.components.camera.open", mopen, create=True), + patch( + "homeassistant.components.camera.os.makedirs", + ), + pytest.raises(HomeAssistantError, match="/test/snapshot.jpg"), + ): await hass.services.async_call( camera.DOMAIN, camera.SERVICE_SNAPSHOT, @@ -285,12 +312,15 @@ async def test_websocket_camera_stream( """Test camera/stream websocket command.""" await async_setup_component(hass, "camera", {}) - with patch( - "homeassistant.components.camera.Stream.endpoint_url", - return_value="http://home.assistant/playlist.m3u8", - ) as mock_stream_view_url, patch( - "homeassistant.components.demo.camera.DemoCamera.stream_source", - return_value="http://example.com", + with ( + patch( + "homeassistant.components.camera.Stream.endpoint_url", + return_value="http://home.assistant/playlist.m3u8", + ) as mock_stream_view_url, + patch( + "homeassistant.components.demo.camera.DemoCamera.stream_source", + return_value="http://example.com", + ), ): # Request playlist through WebSocket client = await hass_ws_client(hass) @@ -443,11 +473,14 @@ async def test_handle_play_stream_service( {"external_url": "https://example.com"}, ) await async_setup_component(hass, "media_player", {}) - with patch( - "homeassistant.components.camera.Stream.endpoint_url", - ) as mock_request_stream, patch( - "homeassistant.components.demo.camera.DemoCamera.stream_source", - return_value="http://example.com", + with ( + patch( + "homeassistant.components.camera.Stream.endpoint_url", + ) as mock_request_stream, + patch( + "homeassistant.components.demo.camera.DemoCamera.stream_source", + return_value="http://example.com", + ), ): # Call service await hass.services.async_call( @@ -467,15 +500,19 @@ async def test_handle_play_stream_service( async def test_no_preload_stream(hass: HomeAssistant, mock_stream) -> None: """Test camera preload preference.""" demo_settings = camera.DynamicStreamSettings() - with patch( - "homeassistant.components.camera.Stream.endpoint_url", - ) as mock_request_stream, patch( - "homeassistant.components.camera.prefs.CameraPreferences.get_dynamic_stream_settings", - return_value=demo_settings, - ), patch( - "homeassistant.components.demo.camera.DemoCamera.stream_source", - new_callable=PropertyMock, - ) as mock_stream_source: + with ( + patch( + "homeassistant.components.camera.Stream.endpoint_url", + ) as mock_request_stream, + patch( + "homeassistant.components.camera.prefs.CameraPreferences.get_dynamic_stream_settings", + return_value=demo_settings, + ), + patch( + "homeassistant.components.demo.camera.DemoCamera.stream_source", + new_callable=PropertyMock, + ) as mock_stream_source, + ): mock_stream_source.return_value = io.BytesIO() await async_setup_component(hass, "camera", {DOMAIN: {"platform": "demo"}}) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -486,14 +523,16 @@ async def test_no_preload_stream(hass: HomeAssistant, mock_stream) -> None: async def test_preload_stream(hass: HomeAssistant, mock_stream) -> None: """Test camera preload preference.""" demo_settings = camera.DynamicStreamSettings(preload_stream=True) - with patch( - "homeassistant.components.camera.create_stream" - ) as mock_create_stream, patch( - "homeassistant.components.camera.prefs.CameraPreferences.get_dynamic_stream_settings", - return_value=demo_settings, - ), patch( - "homeassistant.components.demo.camera.DemoCamera.stream_source", - return_value="http://example.com", + with ( + patch("homeassistant.components.camera.create_stream") as mock_create_stream, + patch( + "homeassistant.components.camera.prefs.CameraPreferences.get_dynamic_stream_settings", + return_value=demo_settings, + ), + patch( + "homeassistant.components.demo.camera.DemoCamera.stream_source", + return_value="http://example.com", + ), ): mock_create_stream.return_value.start = AsyncMock() assert await async_setup_component( @@ -507,9 +546,10 @@ async def test_preload_stream(hass: HomeAssistant, mock_stream) -> None: async def test_record_service_invalid_path(hass: HomeAssistant, mock_camera) -> None: """Test record service with invalid path.""" - with patch.object( - hass.config, "is_allowed_path", return_value=False - ), pytest.raises(HomeAssistantError): + with ( + patch.object(hass.config, "is_allowed_path", return_value=False), + pytest.raises(HomeAssistantError), + ): # Call service await hass.services.async_call( camera.DOMAIN, @@ -524,13 +564,16 @@ async def test_record_service_invalid_path(hass: HomeAssistant, mock_camera) -> async def test_record_service(hass: HomeAssistant, mock_camera, mock_stream) -> None: """Test record service.""" - with patch( - "homeassistant.components.demo.camera.DemoCamera.stream_source", - return_value="http://example.com", - ), patch( - "homeassistant.components.stream.Stream.async_record", - autospec=True, - ) as mock_record: + with ( + patch( + "homeassistant.components.demo.camera.DemoCamera.stream_source", + return_value="http://example.com", + ), + patch( + "homeassistant.components.stream.Stream.async_record", + autospec=True, + ) as mock_record, + ): # Call service await hass.services.async_call( camera.DOMAIN, @@ -726,15 +769,19 @@ async def test_stream_unavailable( """Camera state.""" await async_setup_component(hass, "camera", {}) - with patch( - "homeassistant.components.camera.Stream.endpoint_url", - return_value="http://home.assistant/playlist.m3u8", - ), patch( - "homeassistant.components.demo.camera.DemoCamera.stream_source", - return_value="http://example.com", - ), patch( - "homeassistant.components.camera.Stream.set_update_callback", - ) as mock_update_callback: + with ( + patch( + "homeassistant.components.camera.Stream.endpoint_url", + return_value="http://home.assistant/playlist.m3u8", + ), + patch( + "homeassistant.components.demo.camera.DemoCamera.stream_source", + return_value="http://example.com", + ), + patch( + "homeassistant.components.camera.Stream.set_update_callback", + ) as mock_update_callback, + ): # Request playlist through WebSocket. We just want to create the stream # but don't care about the result. client = await hass_ws_client(hass) @@ -919,12 +966,15 @@ async def test_use_stream_for_stills( client = await hass_client() - with patch( - "homeassistant.components.demo.camera.DemoCamera.stream_source", - return_value=None, - ) as mock_stream_source, patch( - "homeassistant.components.demo.camera.DemoCamera.use_stream_for_stills", - return_value=True, + with ( + patch( + "homeassistant.components.demo.camera.DemoCamera.stream_source", + return_value=None, + ) as mock_stream_source, + patch( + "homeassistant.components.demo.camera.DemoCamera.use_stream_for_stills", + return_value=True, + ), ): # First test when the integration does not support stream should fail resp = await client.get("/api/camera_proxy/camera.demo_camera_without_stream") @@ -937,14 +987,16 @@ async def test_use_stream_for_stills( mock_stream_source.assert_called_once() assert resp.status == HTTPStatus.INTERNAL_SERVER_ERROR - with patch( - "homeassistant.components.demo.camera.DemoCamera.stream_source", - return_value="rtsp://some_source", - ) as mock_stream_source, patch( - "homeassistant.components.camera.create_stream" - ) as mock_create_stream, patch( - "homeassistant.components.demo.camera.DemoCamera.use_stream_for_stills", - return_value=True, + with ( + patch( + "homeassistant.components.demo.camera.DemoCamera.stream_source", + return_value="rtsp://some_source", + ) as mock_stream_source, + patch("homeassistant.components.camera.create_stream") as mock_create_stream, + patch( + "homeassistant.components.demo.camera.DemoCamera.use_stream_for_stills", + return_value=True, + ), ): # Now test when creating the stream succeeds mock_stream = Mock() diff --git a/tests/components/camera/test_media_source.py b/tests/components/camera/test_media_source.py index a70bb262103..3dd0399a710 100644 --- a/tests/components/camera/test_media_source.py +++ b/tests/components/camera/test_media_source.py @@ -1,4 +1,5 @@ """Test camera media source.""" + from unittest.mock import PropertyMock, patch import pytest @@ -123,9 +124,12 @@ async def test_resolving_errors(hass: HomeAssistant, mock_camera_hls) -> None: ) assert str(exc_info.value) == "Could not resolve media item: camera.non_existing" - with pytest.raises(media_source.Unresolvable) as exc_info, patch( - "homeassistant.components.camera.Camera.frontend_stream_type", - new_callable=PropertyMock(return_value=StreamType.WEB_RTC), + with ( + pytest.raises(media_source.Unresolvable) as exc_info, + patch( + "homeassistant.components.camera.Camera.frontend_stream_type", + new_callable=PropertyMock(return_value=StreamType.WEB_RTC), + ), ): await media_source.async_resolve_media( hass, "media-source://camera/camera.demo_camera", None diff --git a/tests/components/camera/test_recorder.py b/tests/components/camera/test_recorder.py index df2b8cbe737..e07b376aefb 100644 --- a/tests/components/camera/test_recorder.py +++ b/tests/components/camera/test_recorder.py @@ -1,4 +1,5 @@ """The tests for camera recorder.""" + from __future__ import annotations from datetime import timedelta diff --git a/tests/components/camera/test_significant_change.py b/tests/components/camera/test_significant_change.py index b1e1eb66589..a2a7ef20e71 100644 --- a/tests/components/camera/test_significant_change.py +++ b/tests/components/camera/test_significant_change.py @@ -1,4 +1,5 @@ """Test the Camera significant change platform.""" + from homeassistant.components.camera import STATE_IDLE, STATE_RECORDING from homeassistant.components.camera.significant_change import ( async_check_significant_change, diff --git a/tests/components/canary/__init__.py b/tests/components/canary/__init__.py index 46737929dc5..8aed2fa1337 100644 --- a/tests/components/canary/__init__.py +++ b/tests/components/canary/__init__.py @@ -1,4 +1,5 @@ """Tests for the Canary integration.""" + from unittest.mock import MagicMock, PropertyMock, patch from canary.model import SensorType diff --git a/tests/components/canary/conftest.py b/tests/components/canary/conftest.py index 546dbca39de..336e6577ecc 100644 --- a/tests/components/canary/conftest.py +++ b/tests/components/canary/conftest.py @@ -1,4 +1,5 @@ """Define fixtures available for all tests.""" + from unittest.mock import MagicMock, patch from canary.api import Api @@ -14,9 +15,10 @@ def mock_ffmpeg(hass): @pytest.fixture def canary(hass): """Mock the CanaryApi for easier testing.""" - with patch.object(Api, "login", return_value=True), patch( - "homeassistant.components.canary.Api" - ) as mock_canary: + with ( + patch.object(Api, "login", return_value=True), + patch("homeassistant.components.canary.Api") as mock_canary, + ): instance = mock_canary.return_value = Api( "test-username", "test-password", @@ -38,9 +40,10 @@ def canary(hass): @pytest.fixture def canary_config_flow(hass): """Mock the CanaryApi for easier config flow testing.""" - with patch.object(Api, "login", return_value=True), patch( - "homeassistant.components.canary.config_flow.Api" - ) as mock_canary: + with ( + patch.object(Api, "login", return_value=True), + patch("homeassistant.components.canary.config_flow.Api") as mock_canary, + ): instance = mock_canary.return_value = Api( "test-username", "test-password", diff --git a/tests/components/canary/test_alarm_control_panel.py b/tests/components/canary/test_alarm_control_panel.py index 6e1096d31c9..83e801d67c4 100644 --- a/tests/components/canary/test_alarm_control_panel.py +++ b/tests/components/canary/test_alarm_control_panel.py @@ -1,4 +1,5 @@ """The tests for the Canary alarm_control_panel platform.""" + from unittest.mock import PropertyMock, patch from canary.const import LOCATION_MODE_AWAY, LOCATION_MODE_HOME, LOCATION_MODE_NIGHT diff --git a/tests/components/canary/test_config_flow.py b/tests/components/canary/test_config_flow.py index 6fcd4290ba8..3c32c683a39 100644 --- a/tests/components/canary/test_config_flow.py +++ b/tests/components/canary/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Canary config flow.""" + from unittest.mock import patch from requests import ConnectTimeout, HTTPError @@ -26,7 +27,10 @@ async def test_user_form(hass: HomeAssistant, canary_config_flow) -> None: assert result["type"] == FlowResultType.FORM assert result["errors"] == {} - with _patch_async_setup() as mock_setup, _patch_async_setup_entry() as mock_setup_entry: + with ( + _patch_async_setup() as mock_setup, + _patch_async_setup_entry() as mock_setup_entry, + ): result = await hass.config_entries.flow.async_configure( result["flow_id"], USER_INPUT, diff --git a/tests/components/canary/test_init.py b/tests/components/canary/test_init.py index 403454662d8..e0d1c532efc 100644 --- a/tests/components/canary/test_init.py +++ b/tests/components/canary/test_init.py @@ -1,4 +1,5 @@ """The tests for the Canary component.""" + from unittest.mock import patch from requests import ConnectTimeout diff --git a/tests/components/canary/test_sensor.py b/tests/components/canary/test_sensor.py index f8e26289691..afcf9f16db4 100644 --- a/tests/components/canary/test_sensor.py +++ b/tests/components/canary/test_sensor.py @@ -1,4 +1,5 @@ """The tests for the Canary sensor platform.""" + from datetime import timedelta from unittest.mock import patch diff --git a/tests/components/cast/conftest.py b/tests/components/cast/conftest.py index 817c4428098..d9ebb24696e 100644 --- a/tests/components/cast/conftest.py +++ b/tests/components/cast/conftest.py @@ -68,27 +68,35 @@ def cast_mock( """Mock pychromecast.""" ignore_cec_orig = list(pychromecast.IGNORE_CEC) - with patch( - "homeassistant.components.cast.discovery.pychromecast.discovery.CastBrowser", - castbrowser_mock, - ), patch( - "homeassistant.components.cast.helpers.dial.get_cast_type", - get_cast_type_mock, - ), patch( - "homeassistant.components.cast.helpers.dial.get_multizone_status", - get_multizone_status_mock, - ), patch( - "homeassistant.components.cast.media_player.MultizoneManager", - return_value=mz_mock, - ), patch( - "homeassistant.components.cast.media_player.zeroconf.async_get_instance", - AsyncMock(), - ), patch( - "homeassistant.components.cast.media_player.quick_play", - quick_play_mock, - ), patch( - "homeassistant.components.cast.media_player.pychromecast.get_chromecast_from_cast_info", - get_chromecast_mock, + with ( + patch( + "homeassistant.components.cast.discovery.pychromecast.discovery.CastBrowser", + castbrowser_mock, + ), + patch( + "homeassistant.components.cast.helpers.dial.get_cast_type", + get_cast_type_mock, + ), + patch( + "homeassistant.components.cast.helpers.dial.get_multizone_status", + get_multizone_status_mock, + ), + patch( + "homeassistant.components.cast.media_player.MultizoneManager", + return_value=mz_mock, + ), + patch( + "homeassistant.components.cast.media_player.zeroconf.async_get_instance", + AsyncMock(), + ), + patch( + "homeassistant.components.cast.media_player.quick_play", + quick_play_mock, + ), + patch( + "homeassistant.components.cast.media_player.pychromecast.get_chromecast_from_cast_info", + get_chromecast_mock, + ), ): yield diff --git a/tests/components/cast/test_config_flow.py b/tests/components/cast/test_config_flow.py index 9b5c2d56d4c..a7b9311e88b 100644 --- a/tests/components/cast/test_config_flow.py +++ b/tests/components/cast/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for the Cast config flow.""" + from unittest.mock import ANY, patch import pytest @@ -13,13 +14,15 @@ from tests.common import MockConfigEntry async def test_creating_entry_sets_up_media_player(hass: HomeAssistant) -> None: """Test setting up Cast loads the media player.""" - with patch( - "homeassistant.components.cast.media_player.async_setup_entry", - return_value=True, - ) as mock_setup, patch( - "pychromecast.discovery.discover_chromecasts", return_value=(True, None) - ), patch( - "pychromecast.discovery.stop_discovery", + with ( + patch( + "homeassistant.components.cast.media_player.async_setup_entry", + return_value=True, + ) as mock_setup, + patch("pychromecast.discovery.discover_chromecasts", return_value=(True, None)), + patch( + "pychromecast.discovery.stop_discovery", + ), ): result = await hass.config_entries.flow.async_init( cast.DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -275,7 +278,7 @@ async def test_known_hosts(hass: HomeAssistant, castbrowser_mock) -> None: result["flow_id"], {"known_hosts": "192.168.0.1, 192.168.0.2"} ) assert result["type"] == "create_entry" - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) config_entry = hass.config_entries.async_entries("cast")[0] assert castbrowser_mock.return_value.start_discovery.call_count == 1 @@ -288,7 +291,7 @@ async def test_known_hosts(hass: HomeAssistant, castbrowser_mock) -> None: user_input={"known_hosts": "192.168.0.11, 192.168.0.12"}, ) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) castbrowser_mock.return_value.start_discovery.assert_not_called() castbrowser_mock.assert_not_called() diff --git a/tests/components/cast/test_helpers.py b/tests/components/cast/test_helpers.py index d19c4f212d0..84914db2b3a 100644 --- a/tests/components/cast/test_helpers.py +++ b/tests/components/cast/test_helpers.py @@ -17,7 +17,7 @@ from tests.test_util.aiohttp import AiohttpClientMocker @pytest.mark.parametrize( ("url", "fixture", "content_type"), - ( + [ ( "http://a.files.bbci.co.uk/media/live/manifesto/audio/simulcast/hls/nonuk/sbr_low/ak/bbc_radio_fourfm.m3u8", "bbc_radio_fourfm.m3u8", @@ -33,7 +33,7 @@ from tests.test_util.aiohttp import AiohttpClientMocker "rthkaudio2.m3u8", None, ), - ), + ], ) async def test_hls_playlist_supported( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, url, fixture, content_type @@ -47,7 +47,7 @@ async def test_hls_playlist_supported( @pytest.mark.parametrize( ("url", "fixture", "content_type", "expected_playlist"), - ( + [ ( "https://sverigesradio.se/topsy/direkt/209-hi-mp3.m3u", "209-hi-mp3.m3u", @@ -96,7 +96,7 @@ async def test_hls_playlist_supported( ) ], ), - ), + ], ) async def test_parse_playlist( hass: HomeAssistant, @@ -115,7 +115,7 @@ async def test_parse_playlist( @pytest.mark.parametrize( ("url", "fixture"), - ( + [ ("http://sverigesradio.se/164-hi-aac.pls", "164-hi-aac_invalid_entries.pls"), ("http://sverigesradio.se/164-hi-aac.pls", "164-hi-aac_invalid_file.pls"), ("http://sverigesradio.se/164-hi-aac.pls", "164-hi-aac_invalid_version.pls"), @@ -126,7 +126,7 @@ async def test_parse_playlist( ("http://sverigesradio.se/164-hi-aac.pls", "164-hi-aac_no_version.pls"), ("https://sverigesradio.se/209-hi-mp3.m3u", "209-hi-mp3_bad_url.m3u"), ("https://sverigesradio.se/209-hi-mp3.m3u", "empty.m3u"), - ), + ], ) async def test_parse_bad_playlist( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, url, fixture @@ -139,10 +139,10 @@ async def test_parse_bad_playlist( @pytest.mark.parametrize( ("url", "exc"), - ( + [ ("http://sverigesradio.se/164-hi-aac.pls", TimeoutError), ("http://sverigesradio.se/164-hi-aac.pls", client_exceptions.ClientError), - ), + ], ) async def test_parse_http_error( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, url, exc diff --git a/tests/components/cast/test_home_assistant_cast.py b/tests/components/cast/test_home_assistant_cast.py index 637ba53fc93..74ab776ec3b 100644 --- a/tests/components/cast/test_home_assistant_cast.py +++ b/tests/components/cast/test_home_assistant_cast.py @@ -1,4 +1,5 @@ """Test Home Assistant Cast.""" + from unittest.mock import patch import pytest @@ -120,9 +121,10 @@ async def test_remove_entry(hass: HomeAssistant, mock_zeroconf: None) -> None: entry.add_to_hass(hass) - with patch( - "pychromecast.discovery.discover_chromecasts", return_value=(True, None) - ), patch("pychromecast.discovery.stop_discovery"): + with ( + patch("pychromecast.discovery.discover_chromecasts", return_value=(True, None)), + patch("pychromecast.discovery.stop_discovery"), + ): assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() assert "cast" in hass.config.components diff --git a/tests/components/cast/test_media_player.py b/tests/components/cast/test_media_player.py index 66d23043935..8381f27398a 100644 --- a/tests/components/cast/test_media_player.py +++ b/tests/components/cast/test_media_player.py @@ -1,4 +1,5 @@ """The tests for the Cast Media player platform.""" + from __future__ import annotations import asyncio @@ -115,7 +116,7 @@ async def async_setup_cast(hass, config=None): """Set up the cast platform.""" if config is None: config = {} - data = {**{"ignore_cec": [], "known_hosts": [], "uuid": []}, **config} + data = {"ignore_cec": [], "known_hosts": [], "uuid": [], **config} with patch( "homeassistant.helpers.entity_platform.EntityPlatform._async_schedule_add_entities_for_entry" ) as add_entities: @@ -136,8 +137,8 @@ async def async_setup_cast_internal_discovery(hass, config=None): return_value=browser, ) as cast_browser: add_entities = await async_setup_cast(hass, config) - await hass.async_block_till_done() - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) + await hass.async_block_till_done(wait_background_tasks=True) assert browser.start_discovery.call_count == 1 @@ -190,22 +191,26 @@ async def async_setup_media_player_cast(hass: HomeAssistant, info: ChromecastInf chromecast = get_fake_chromecast(info) zconf = get_fake_zconf(host=info.cast_info.host, port=info.cast_info.port) - with patch( - "homeassistant.components.cast.discovery.pychromecast.get_chromecast_from_cast_info", - return_value=chromecast, - ) as get_chromecast, patch( - "homeassistant.components.cast.discovery.pychromecast.discovery.CastBrowser", - return_value=browser, - ) as cast_browser, patch( - "homeassistant.components.cast.discovery.ChromeCastZeroconf.get_zeroconf", - return_value=zconf, + with ( + patch( + "homeassistant.components.cast.discovery.pychromecast.get_chromecast_from_cast_info", + return_value=chromecast, + ) as get_chromecast, + patch( + "homeassistant.components.cast.discovery.pychromecast.discovery.CastBrowser", + return_value=browser, + ) as cast_browser, + patch( + "homeassistant.components.cast.discovery.ChromeCastZeroconf.get_zeroconf", + return_value=zconf, + ), ): data = {"ignore_cec": [], "known_hosts": [], "uuid": [str(info.uuid)]} entry = MockConfigEntry(data=data, domain="cast") entry.add_to_hass(hass) assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) + await hass.async_block_till_done(wait_background_tasks=True) discovery_callback = cast_browser.call_args[0][0].add_cast @@ -579,13 +584,16 @@ async def test_discover_dynamic_group( tasks.append(real_create_task(coroutine)) # Discover cast service - with patch( - "homeassistant.components.cast.discovery.ChromeCastZeroconf.get_zeroconf", - return_value=zconf_1, - ), patch.object( - hass, - "async_create_background_task", - wraps=create_task, + with ( + patch( + "homeassistant.components.cast.discovery.ChromeCastZeroconf.get_zeroconf", + return_value=zconf_1, + ), + patch.object( + hass, + "async_create_background_task", + wraps=create_task, + ), ): discover_cast( pychromecast.discovery.MDNSServiceInfo("service"), @@ -605,13 +613,16 @@ async def test_discover_dynamic_group( ) # Discover other dynamic group cast service - with patch( - "homeassistant.components.cast.discovery.ChromeCastZeroconf.get_zeroconf", - return_value=zconf_2, - ), patch.object( - hass, - "async_create_background_task", - wraps=create_task, + with ( + patch( + "homeassistant.components.cast.discovery.ChromeCastZeroconf.get_zeroconf", + return_value=zconf_2, + ), + patch.object( + hass, + "async_create_background_task", + wraps=create_task, + ), ): discover_cast( pychromecast.discovery.MDNSServiceInfo("service"), @@ -631,13 +642,16 @@ async def test_discover_dynamic_group( ) # Get update for cast service - with patch( - "homeassistant.components.cast.discovery.ChromeCastZeroconf.get_zeroconf", - return_value=zconf_1, - ), patch.object( - hass, - "async_create_background_task", - wraps=create_task, + with ( + patch( + "homeassistant.components.cast.discovery.ChromeCastZeroconf.get_zeroconf", + return_value=zconf_1, + ), + patch.object( + hass, + "async_create_background_task", + wraps=create_task, + ), ): discover_cast( pychromecast.discovery.MDNSServiceInfo("service"), @@ -753,7 +767,7 @@ async def test_entity_availability(hass: HomeAssistant) -> None: assert state.state == "unavailable" -@pytest.mark.parametrize(("port", "entry_type"), ((8009, None), (12345, None))) +@pytest.mark.parametrize(("port", "entry_type"), [(8009, None), (12345, None)]) async def test_device_registry( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, @@ -1260,7 +1274,7 @@ async def test_entity_play_media_sign_URL(hass: HomeAssistant, quick_play_mock) @pytest.mark.parametrize( ("url", "fixture", "playlist_item"), - ( + [ # Test title is extracted from m3u playlist ( "https://sverigesradio.se/topsy/direkt/209-hi-mp3.m3u", @@ -1306,7 +1320,7 @@ async def test_entity_play_media_sign_URL(hass: HomeAssistant, quick_play_mock) "media_type": "audio", }, ), - ), + ], ) async def test_entity_play_media_playlist( hass: HomeAssistant, diff --git a/tests/components/ccm15/conftest.py b/tests/components/ccm15/conftest.py index 910a74fa0bc..6098a95b3ce 100644 --- a/tests/components/ccm15/conftest.py +++ b/tests/components/ccm15/conftest.py @@ -1,4 +1,5 @@ """Common fixtures for the Midea ccm15 AC Controller tests.""" + from collections.abc import Generator from unittest.mock import AsyncMock, patch diff --git a/tests/components/ccm15/snapshots/test_climate.ambr b/tests/components/ccm15/snapshots/test_climate.ambr index 63f6612784b..10423919187 100644 --- a/tests/components/ccm15/snapshots/test_climate.ambr +++ b/tests/components/ccm15/snapshots/test_climate.ambr @@ -141,6 +141,7 @@ 'context': , 'entity_id': 'climate.midea_0', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }) @@ -179,6 +180,7 @@ 'context': , 'entity_id': 'climate.midea_1', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'cool', }) @@ -320,6 +322,7 @@ 'context': , 'entity_id': 'climate.midea_0', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unavailable', }) @@ -353,6 +356,7 @@ 'context': , 'entity_id': 'climate.midea_1', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unavailable', }) diff --git a/tests/components/ccm15/test_climate.py b/tests/components/ccm15/test_climate.py index 36a77aa15ab..329caafd11c 100644 --- a/tests/components/ccm15/test_climate.py +++ b/tests/components/ccm15/test_climate.py @@ -1,4 +1,5 @@ """Unit test for CCM15 coordinator component.""" + from datetime import timedelta from unittest.mock import AsyncMock, patch diff --git a/tests/components/ccm15/test_config_flow.py b/tests/components/ccm15/test_config_flow.py index 9b6314228cc..87c93179f4e 100644 --- a/tests/components/ccm15/test_config_flow.py +++ b/tests/components/ccm15/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Midea ccm15 AC Controller config flow.""" + from unittest.mock import AsyncMock, patch from homeassistant import config_entries diff --git a/tests/components/ccm15/test_diagnostics.py b/tests/components/ccm15/test_diagnostics.py index 3700faa51ce..a433591d86e 100644 --- a/tests/components/ccm15/test_diagnostics.py +++ b/tests/components/ccm15/test_diagnostics.py @@ -1,4 +1,5 @@ """Test CCM15 diagnostics.""" + from unittest.mock import AsyncMock from syrupy import SnapshotAssertion diff --git a/tests/components/ccm15/test_init.py b/tests/components/ccm15/test_init.py index b65f170a656..3069b61f10f 100644 --- a/tests/components/ccm15/test_init.py +++ b/tests/components/ccm15/test_init.py @@ -1,4 +1,5 @@ """Tests for the ccm15 component.""" + from unittest.mock import AsyncMock from homeassistant.components.ccm15.const import DOMAIN diff --git a/tests/components/cert_expiry/conftest.py b/tests/components/cert_expiry/conftest.py index 0a3f5420f60..41c2d90b1a0 100644 --- a/tests/components/cert_expiry/conftest.py +++ b/tests/components/cert_expiry/conftest.py @@ -1,4 +1,5 @@ """Configuration for cert_expiry tests.""" + from collections.abc import Generator from unittest.mock import AsyncMock, patch diff --git a/tests/components/cert_expiry/const.py b/tests/components/cert_expiry/const.py index 9ddbeca61c3..14a002fc6e5 100644 --- a/tests/components/cert_expiry/const.py +++ b/tests/components/cert_expiry/const.py @@ -1,3 +1,4 @@ """Constants for cert_expiry tests.""" + PORT = 443 HOST = "example.com" diff --git a/tests/components/cert_expiry/helpers.py b/tests/components/cert_expiry/helpers.py index cf7cff511f7..929ad7e6f9a 100644 --- a/tests/components/cert_expiry/helpers.py +++ b/tests/components/cert_expiry/helpers.py @@ -1,4 +1,5 @@ """Helpers for Cert Expiry tests.""" + from datetime import datetime, timedelta from homeassistant.util import dt as dt_util diff --git a/tests/components/cert_expiry/test_config_flow.py b/tests/components/cert_expiry/test_config_flow.py index 1e72e708d44..aa5f32c0ca2 100644 --- a/tests/components/cert_expiry/test_config_flow.py +++ b/tests/components/cert_expiry/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for the Cert Expiry config flow.""" + import socket import ssl from unittest.mock import patch @@ -64,11 +65,14 @@ async def test_user_with_bad_cert(hass: HomeAssistant) -> None: async def test_import_host_only(hass: HomeAssistant) -> None: """Test import with host only.""" - with patch( - "homeassistant.components.cert_expiry.config_flow.get_cert_expiry_timestamp" - ), patch( - "homeassistant.components.cert_expiry.coordinator.get_cert_expiry_timestamp", - return_value=future_timestamp(1), + with ( + patch( + "homeassistant.components.cert_expiry.config_flow.get_cert_expiry_timestamp" + ), + patch( + "homeassistant.components.cert_expiry.coordinator.get_cert_expiry_timestamp", + return_value=future_timestamp(1), + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -86,11 +90,14 @@ async def test_import_host_only(hass: HomeAssistant) -> None: async def test_import_host_and_port(hass: HomeAssistant) -> None: """Test import with host and port.""" - with patch( - "homeassistant.components.cert_expiry.config_flow.get_cert_expiry_timestamp" - ), patch( - "homeassistant.components.cert_expiry.coordinator.get_cert_expiry_timestamp", - return_value=future_timestamp(1), + with ( + patch( + "homeassistant.components.cert_expiry.config_flow.get_cert_expiry_timestamp" + ), + patch( + "homeassistant.components.cert_expiry.coordinator.get_cert_expiry_timestamp", + return_value=future_timestamp(1), + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -108,11 +115,14 @@ async def test_import_host_and_port(hass: HomeAssistant) -> None: async def test_import_non_default_port(hass: HomeAssistant) -> None: """Test import with host and non-default port.""" - with patch( - "homeassistant.components.cert_expiry.config_flow.get_cert_expiry_timestamp" - ), patch( - "homeassistant.components.cert_expiry.coordinator.get_cert_expiry_timestamp", - return_value=future_timestamp(1), + with ( + patch( + "homeassistant.components.cert_expiry.config_flow.get_cert_expiry_timestamp" + ), + patch( + "homeassistant.components.cert_expiry.coordinator.get_cert_expiry_timestamp", + return_value=future_timestamp(1), + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -130,11 +140,14 @@ async def test_import_non_default_port(hass: HomeAssistant) -> None: async def test_import_with_name(hass: HomeAssistant) -> None: """Test import with name (deprecated).""" - with patch( - "homeassistant.components.cert_expiry.config_flow.get_cert_expiry_timestamp" - ), patch( - "homeassistant.components.cert_expiry.coordinator.get_cert_expiry_timestamp", - return_value=future_timestamp(1), + with ( + patch( + "homeassistant.components.cert_expiry.config_flow.get_cert_expiry_timestamp" + ), + patch( + "homeassistant.components.cert_expiry.coordinator.get_cert_expiry_timestamp", + return_value=future_timestamp(1), + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, diff --git a/tests/components/cert_expiry/test_init.py b/tests/components/cert_expiry/test_init.py index 0e0ff1444eb..312b87affd3 100644 --- a/tests/components/cert_expiry/test_init.py +++ b/tests/components/cert_expiry/test_init.py @@ -1,4 +1,5 @@ """Tests for Cert Expiry setup.""" + from datetime import timedelta from unittest.mock import patch @@ -41,11 +42,14 @@ async def test_setup_with_config(hass: HomeAssistant) -> None: next_update = dt_util.utcnow() + timedelta(seconds=20) async_fire_time_changed(hass, next_update) - with patch( - "homeassistant.components.cert_expiry.config_flow.get_cert_expiry_timestamp" - ), patch( - "homeassistant.components.cert_expiry.coordinator.get_cert_expiry_timestamp", - return_value=future_timestamp(1), + with ( + patch( + "homeassistant.components.cert_expiry.config_flow.get_cert_expiry_timestamp" + ), + patch( + "homeassistant.components.cert_expiry.coordinator.get_cert_expiry_timestamp", + return_value=future_timestamp(1), + ), ): await hass.async_block_till_done() diff --git a/tests/components/cert_expiry/test_sensors.py b/tests/components/cert_expiry/test_sensors.py index 1c66a1c91ff..d68e973e1fa 100644 --- a/tests/components/cert_expiry/test_sensors.py +++ b/tests/components/cert_expiry/test_sensors.py @@ -1,4 +1,5 @@ """Tests for the Cert Expiry sensors.""" + from datetime import timedelta import socket import ssl @@ -84,9 +85,12 @@ async def test_update_sensor(hass: HomeAssistant) -> None: starting_time = static_datetime() timestamp = future_timestamp(100) - with freeze_time(starting_time), patch( - "homeassistant.components.cert_expiry.coordinator.get_cert_expiry_timestamp", - return_value=timestamp, + with ( + freeze_time(starting_time), + patch( + "homeassistant.components.cert_expiry.coordinator.get_cert_expiry_timestamp", + return_value=timestamp, + ), ): entry.add_to_hass(hass) assert await hass.config_entries.async_setup(entry.entry_id) @@ -100,9 +104,12 @@ async def test_update_sensor(hass: HomeAssistant) -> None: assert state.attributes.get("is_valid") next_update = starting_time + timedelta(hours=24) - with freeze_time(next_update), patch( - "homeassistant.components.cert_expiry.coordinator.get_cert_expiry_timestamp", - return_value=timestamp, + with ( + freeze_time(next_update), + patch( + "homeassistant.components.cert_expiry.coordinator.get_cert_expiry_timestamp", + return_value=timestamp, + ), ): async_fire_time_changed(hass, utcnow() + timedelta(hours=24)) await hass.async_block_till_done() @@ -128,9 +135,12 @@ async def test_update_sensor_network_errors(hass: HomeAssistant) -> None: starting_time = static_datetime() timestamp = future_timestamp(100) - with freeze_time(starting_time), patch( - "homeassistant.components.cert_expiry.coordinator.get_cert_expiry_timestamp", - return_value=timestamp, + with ( + freeze_time(starting_time), + patch( + "homeassistant.components.cert_expiry.coordinator.get_cert_expiry_timestamp", + return_value=timestamp, + ), ): entry.add_to_hass(hass) assert await hass.config_entries.async_setup(entry.entry_id) @@ -145,9 +155,12 @@ async def test_update_sensor_network_errors(hass: HomeAssistant) -> None: next_update = starting_time + timedelta(hours=24) - with freeze_time(next_update), patch( - "homeassistant.components.cert_expiry.helper.async_get_cert", - side_effect=socket.gaierror, + with ( + freeze_time(next_update), + patch( + "homeassistant.components.cert_expiry.helper.async_get_cert", + side_effect=socket.gaierror, + ), ): async_fire_time_changed(hass, utcnow() + timedelta(hours=24)) await hass.async_block_till_done() @@ -157,9 +170,12 @@ async def test_update_sensor_network_errors(hass: HomeAssistant) -> None: state = hass.states.get("sensor.example_com_cert_expiry") assert state.state == STATE_UNAVAILABLE - with freeze_time(next_update), patch( - "homeassistant.components.cert_expiry.coordinator.get_cert_expiry_timestamp", - return_value=timestamp, + with ( + freeze_time(next_update), + patch( + "homeassistant.components.cert_expiry.coordinator.get_cert_expiry_timestamp", + return_value=timestamp, + ), ): async_fire_time_changed(hass, utcnow() + timedelta(hours=48)) await hass.async_block_till_done() @@ -173,9 +189,12 @@ async def test_update_sensor_network_errors(hass: HomeAssistant) -> None: next_update = starting_time + timedelta(hours=72) - with freeze_time(next_update), patch( - "homeassistant.components.cert_expiry.helper.async_get_cert", - side_effect=ssl.SSLError("something bad"), + with ( + freeze_time(next_update), + patch( + "homeassistant.components.cert_expiry.helper.async_get_cert", + side_effect=ssl.SSLError("something bad"), + ), ): async_fire_time_changed(hass, utcnow() + timedelta(hours=72)) await hass.async_block_till_done() @@ -188,9 +207,12 @@ async def test_update_sensor_network_errors(hass: HomeAssistant) -> None: next_update = starting_time + timedelta(hours=96) - with freeze_time(next_update), patch( - "homeassistant.components.cert_expiry.helper.async_get_cert", - side_effect=Exception(), + with ( + freeze_time(next_update), + patch( + "homeassistant.components.cert_expiry.helper.async_get_cert", + side_effect=Exception(), + ), ): async_fire_time_changed(hass, utcnow() + timedelta(hours=96)) await hass.async_block_till_done() diff --git a/tests/components/clicksend_tts/test_notify.py b/tests/components/clicksend_tts/test_notify.py index ae814aa1332..e73f0576d9e 100644 --- a/tests/components/clicksend_tts/test_notify.py +++ b/tests/components/clicksend_tts/test_notify.py @@ -1,4 +1,5 @@ """The test for the Facebook notify module.""" + import base64 from http import HTTPStatus import logging diff --git a/tests/components/climate/common.py b/tests/components/climate/common.py index 7d66b886810..20f6bfd880d 100644 --- a/tests/components/climate/common.py +++ b/tests/components/climate/common.py @@ -3,6 +3,7 @@ All containing methods are legacy helpers that should not be used by new components. Instead call the service directly. """ + from homeassistant.components.climate import ( _LOGGER, ATTR_AUX_HEAT, diff --git a/tests/components/climate/conftest.py b/tests/components/climate/conftest.py index 2db96a20a0b..c65414ea68d 100644 --- a/tests/components/climate/conftest.py +++ b/tests/components/climate/conftest.py @@ -1,4 +1,5 @@ """Fixtures for Climate platform tests.""" + from collections.abc import Generator import pytest diff --git a/tests/components/climate/test_device_action.py b/tests/components/climate/test_device_action.py index 1fc379487ed..3ee5a9b8edd 100644 --- a/tests/components/climate/test_device_action.py +++ b/tests/components/climate/test_device_action.py @@ -1,4 +1,5 @@ """The tests for Climate device actions.""" + import pytest from pytest_unordered import unordered import voluptuous_serialize @@ -96,12 +97,12 @@ async def test_get_actions( @pytest.mark.parametrize( ("hidden_by", "entity_category"), - ( + [ (RegistryEntryHider.INTEGRATION, None), (RegistryEntryHider.USER, None), (None, EntityCategory.CONFIG), (None, EntityCategory.DIAGNOSTIC), - ), + ], ) async def test_get_actions_hidden_auxiliary( hass: HomeAssistant, diff --git a/tests/components/climate/test_device_condition.py b/tests/components/climate/test_device_condition.py index 4dc365e59ee..d9345a0516c 100644 --- a/tests/components/climate/test_device_condition.py +++ b/tests/components/climate/test_device_condition.py @@ -1,4 +1,5 @@ """The tests for Climate device conditions.""" + import pytest from pytest_unordered import unordered import voluptuous_serialize @@ -100,12 +101,12 @@ async def test_get_conditions( @pytest.mark.parametrize( ("hidden_by", "entity_category"), - ( + [ (RegistryEntryHider.INTEGRATION, None), (RegistryEntryHider.USER, None), (None, EntityCategory.CONFIG), (None, EntityCategory.DIAGNOSTIC), - ), + ], ) async def test_get_conditions_hidden_auxiliary( hass: HomeAssistant, diff --git a/tests/components/climate/test_device_trigger.py b/tests/components/climate/test_device_trigger.py index 59efb66ff65..7dbe106bd4f 100644 --- a/tests/components/climate/test_device_trigger.py +++ b/tests/components/climate/test_device_trigger.py @@ -1,4 +1,5 @@ """The tests for Climate device triggers.""" + import pytest from pytest_unordered import unordered import voluptuous_serialize @@ -87,12 +88,12 @@ async def test_get_triggers( @pytest.mark.parametrize( ("hidden_by", "entity_category"), - ( + [ (RegistryEntryHider.INTEGRATION, None), (RegistryEntryHider.USER, None), (None, EntityCategory.CONFIG), (None, EntityCategory.DIAGNOSTIC), - ), + ], ) async def test_get_triggers_hidden_auxiliary( hass: HomeAssistant, diff --git a/tests/components/climate/test_init.py b/tests/components/climate/test_init.py index 11ec825f96c..ed942fb1464 100644 --- a/tests/components/climate/test_init.py +++ b/tests/components/climate/test_init.py @@ -1,4 +1,5 @@ """The tests for the climate component.""" + from __future__ import annotations from enum import Enum @@ -22,12 +23,14 @@ from homeassistant.components.climate.const import ( SERVICE_SET_FAN_MODE, SERVICE_SET_PRESET_MODE, SERVICE_SET_SWING_MODE, + SERVICE_SET_TEMPERATURE, ClimateEntityFeature, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import SERVICE_TURN_OFF, SERVICE_TURN_ON, UnitOfTemperature from homeassistant.core import HomeAssistant from homeassistant.exceptions import ServiceValidationError +from homeassistant.helpers import issue_registry as ir from homeassistant.helpers.entity_platform import AddEntitiesCallback from tests.common import ( @@ -156,11 +159,12 @@ async def test_sync_turn_off(hass: HomeAssistant) -> None: def _create_tuples(enum: Enum, constant_prefix: str) -> list[tuple[Enum, str]]: - result = [] - for enum in enum: - if enum not in [ClimateEntityFeature.TURN_ON, ClimateEntityFeature.TURN_OFF]: - result.append((enum, constant_prefix)) - return result + return [ + (enum_field, constant_prefix) + for enum_field in enum + if enum_field + not in [ClimateEntityFeature.TURN_ON, ClimateEntityFeature.TURN_OFF] + ] @pytest.mark.parametrize( @@ -298,7 +302,7 @@ async def test_preset_mode_validation( with pytest.raises( ServiceValidationError, - match="The preset_mode invalid is not a valid preset_mode: home, away", + match="Preset mode invalid is not valid. Valid preset modes are: home, away", ) as exc: await hass.services.async_call( DOMAIN, @@ -311,13 +315,13 @@ async def test_preset_mode_validation( ) assert ( str(exc.value) - == "The preset_mode invalid is not a valid preset_mode: home, away" + == "Preset mode invalid is not valid. Valid preset modes are: home, away" ) assert exc.value.translation_key == "not_valid_preset_mode" with pytest.raises( ServiceValidationError, - match="The swing_mode invalid is not a valid swing_mode: auto, off", + match="Swing mode invalid is not valid. Valid swing modes are: auto, off", ) as exc: await hass.services.async_call( DOMAIN, @@ -329,13 +333,14 @@ async def test_preset_mode_validation( blocking=True, ) assert ( - str(exc.value) == "The swing_mode invalid is not a valid swing_mode: auto, off" + str(exc.value) + == "Swing mode invalid is not valid. Valid swing modes are: auto, off" ) assert exc.value.translation_key == "not_valid_swing_mode" with pytest.raises( ServiceValidationError, - match="The fan_mode invalid is not a valid fan_mode: auto, off", + match="Fan mode invalid is not valid. Valid fan modes are: auto, off", ) as exc: await hass.services.async_call( DOMAIN, @@ -346,7 +351,10 @@ async def test_preset_mode_validation( }, blocking=True, ) - assert str(exc.value) == "The fan_mode invalid is not a valid fan_mode: auto, off" + assert ( + str(exc.value) + == "Fan mode invalid is not valid. Valid fan modes are: auto, off" + ) assert exc.value.translation_key == "not_valid_fan_mode" @@ -765,3 +773,309 @@ async def test_sync_toggle(hass: HomeAssistant) -> None: await climate.async_toggle() assert climate.toggle.called + + +ISSUE_TRACKER = "https://blablabla.com" + + +@pytest.mark.parametrize( + ( + "manifest_extra", + "translation_key", + "translation_placeholders_extra", + "report", + "module", + ), + [ + ( + {}, + "deprecated_climate_aux_no_url", + {}, + "report it to the author of the 'test' custom integration", + "custom_components.test.climate", + ), + ( + {"issue_tracker": ISSUE_TRACKER}, + "deprecated_climate_aux_url_custom", + {"issue_tracker": ISSUE_TRACKER}, + "create a bug report at https://blablabla.com", + "custom_components.test.climate", + ), + ], +) +async def test_issue_aux_property_deprecated( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + config_flow_fixture: None, + manifest_extra: dict[str, str], + translation_key: str, + translation_placeholders_extra: dict[str, str], + report: str, + module: str, +) -> None: + """Test the issue is raised on deprecated auxiliary heater attributes.""" + + class MockClimateEntityWithAux(MockClimateEntity): + """Mock climate class with mocked aux heater.""" + + _attr_supported_features = ( + ClimateEntityFeature.AUX_HEAT | ClimateEntityFeature.TARGET_TEMPERATURE + ) + + @property + def is_aux_heat(self) -> bool | None: + """Return true if aux heater. + + Requires ClimateEntityFeature.AUX_HEAT. + """ + return True + + async def async_turn_aux_heat_on(self) -> None: + """Turn auxiliary heater on.""" + await self.hass.async_add_executor_job(self.turn_aux_heat_on) + + async def async_turn_aux_heat_off(self) -> None: + """Turn auxiliary heater off.""" + await self.hass.async_add_executor_job(self.turn_aux_heat_off) + + # Fake the module is custom component or built in + MockClimateEntityWithAux.__module__ = module + + climate_entity = MockClimateEntityWithAux( + name="Testing", + entity_id="climate.testing", + ) + + async def async_setup_entry_init( + hass: HomeAssistant, config_entry: ConfigEntry + ) -> bool: + """Set up test config entry.""" + await hass.config_entries.async_forward_entry_setups(config_entry, [DOMAIN]) + return True + + async def async_setup_entry_climate_platform( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, + ) -> None: + """Set up test weather platform via config entry.""" + async_add_entities([climate_entity]) + + mock_integration( + hass, + MockModule( + "test", + async_setup_entry=async_setup_entry_init, + partial_manifest=manifest_extra, + ), + built_in=False, + ) + mock_platform( + hass, + "test.climate", + MockPlatform(async_setup_entry=async_setup_entry_climate_platform), + ) + + config_entry = MockConfigEntry(domain="test") + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert climate_entity.state == HVACMode.HEAT + + issues = ir.async_get(hass) + issue = issues.async_get_issue("climate", "deprecated_climate_aux_test") + assert issue + assert issue.issue_domain == "test" + assert issue.issue_id == "deprecated_climate_aux_test" + assert issue.translation_key == translation_key + assert ( + issue.translation_placeholders + == {"platform": "test"} | translation_placeholders_extra + ) + + assert ( + "test::MockClimateEntityWithAux implements the `is_aux_heat` property or uses " + "the auxiliary heater methods in a subclass of ClimateEntity which is deprecated " + f"and will be unsupported from Home Assistant 2024.10. Please {report}" + ) in caplog.text + + # Assert we only log warning once + caplog.clear() + await hass.services.async_call( + DOMAIN, + SERVICE_SET_TEMPERATURE, + { + "entity_id": "climate.test", + "temperature": "25", + }, + blocking=True, + ) + await hass.async_block_till_done() + + assert ("implements the `is_aux_heat` property") not in caplog.text + + +@pytest.mark.parametrize( + ( + "manifest_extra", + "translation_key", + "translation_placeholders_extra", + "report", + "module", + ), + [ + ( + {"issue_tracker": ISSUE_TRACKER}, + "deprecated_climate_aux_url", + {"issue_tracker": ISSUE_TRACKER}, + "create a bug report at https://blablabla.com", + "homeassistant.components.test.climate", + ), + ], +) +async def test_no_issue_aux_property_deprecated_for_core( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + config_flow_fixture: None, + manifest_extra: dict[str, str], + translation_key: str, + translation_placeholders_extra: dict[str, str], + report: str, + module: str, +) -> None: + """Test the no issue on deprecated auxiliary heater attributes for core integrations.""" + + class MockClimateEntityWithAux(MockClimateEntity): + """Mock climate class with mocked aux heater.""" + + _attr_supported_features = ClimateEntityFeature.AUX_HEAT + + @property + def is_aux_heat(self) -> bool | None: + """Return true if aux heater. + + Requires ClimateEntityFeature.AUX_HEAT. + """ + return True + + async def async_turn_aux_heat_on(self) -> None: + """Turn auxiliary heater on.""" + await self.hass.async_add_executor_job(self.turn_aux_heat_on) + + async def async_turn_aux_heat_off(self) -> None: + """Turn auxiliary heater off.""" + await self.hass.async_add_executor_job(self.turn_aux_heat_off) + + # Fake the module is custom component or built in + MockClimateEntityWithAux.__module__ = module + + climate_entity = MockClimateEntityWithAux( + name="Testing", + entity_id="climate.testing", + ) + + async def async_setup_entry_init( + hass: HomeAssistant, config_entry: ConfigEntry + ) -> bool: + """Set up test config entry.""" + await hass.config_entries.async_forward_entry_setups(config_entry, [DOMAIN]) + return True + + async def async_setup_entry_climate_platform( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, + ) -> None: + """Set up test weather platform via config entry.""" + async_add_entities([climate_entity]) + + mock_integration( + hass, + MockModule( + "test", + async_setup_entry=async_setup_entry_init, + partial_manifest=manifest_extra, + ), + built_in=False, + ) + mock_platform( + hass, + "test.climate", + MockPlatform(async_setup_entry=async_setup_entry_climate_platform), + ) + + config_entry = MockConfigEntry(domain="test") + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert climate_entity.state == HVACMode.HEAT + + issues = ir.async_get(hass) + issue = issues.async_get_issue("climate", "deprecated_climate_aux_test") + assert not issue + + assert ( + "test::MockClimateEntityWithAux implements the `is_aux_heat` property or uses " + "the auxiliary heater methods in a subclass of ClimateEntity which is deprecated " + f"and will be unsupported from Home Assistant 2024.10. Please {report}" + ) not in caplog.text + + +async def test_no_issue_no_aux_property( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + config_flow_fixture: None, +) -> None: + """Test the issue is raised on deprecated auxiliary heater attributes.""" + + climate_entity = MockClimateEntity( + name="Testing", + entity_id="climate.testing", + ) + + async def async_setup_entry_init( + hass: HomeAssistant, config_entry: ConfigEntry + ) -> bool: + """Set up test config entry.""" + await hass.config_entries.async_forward_entry_setups(config_entry, [DOMAIN]) + return True + + async def async_setup_entry_climate_platform( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, + ) -> None: + """Set up test weather platform via config entry.""" + async_add_entities([climate_entity]) + + mock_integration( + hass, + MockModule( + "test", + async_setup_entry=async_setup_entry_init, + ), + built_in=False, + ) + mock_platform( + hass, + "test.climate", + MockPlatform(async_setup_entry=async_setup_entry_climate_platform), + ) + + config_entry = MockConfigEntry(domain="test") + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert climate_entity.state == HVACMode.HEAT + + issues = ir.async_get(hass) + assert len(issues.issues) == 0 + + assert ( + "test::MockClimateEntityWithAux implements the `is_aux_heat` property or uses " + "the auxiliary heater methods in a subclass of ClimateEntity which is deprecated " + "and will be unsupported from Home Assistant 2024.10." + ) not in caplog.text diff --git a/tests/components/climate/test_recorder.py b/tests/components/climate/test_recorder.py index 150d70d1dba..f3ecedb9cd4 100644 --- a/tests/components/climate/test_recorder.py +++ b/tests/components/climate/test_recorder.py @@ -1,4 +1,5 @@ """The tests for climate recorder.""" + from __future__ import annotations from datetime import timedelta diff --git a/tests/components/climate/test_reproduce_state.py b/tests/components/climate/test_reproduce_state.py index b8719fd8fd0..636ab326a2b 100644 --- a/tests/components/climate/test_reproduce_state.py +++ b/tests/components/climate/test_reproduce_state.py @@ -1,4 +1,5 @@ """The tests for reproduction of state.""" + import pytest from homeassistant.components.climate import ( diff --git a/tests/components/climate/test_significant_change.py b/tests/components/climate/test_significant_change.py index 369e5e67004..f060344722a 100644 --- a/tests/components/climate/test_significant_change.py +++ b/tests/components/climate/test_significant_change.py @@ -1,4 +1,5 @@ """Test the Climate significant change platform.""" + import pytest from homeassistant.components.climate import ( diff --git a/tests/components/cloud/__init__.py b/tests/components/cloud/__init__.py index e6e793ed106..2b4a95a61d9 100644 --- a/tests/components/cloud/__init__.py +++ b/tests/components/cloud/__init__.py @@ -1,4 +1,5 @@ """Tests for the cloud component.""" + from unittest.mock import AsyncMock, patch from hass_nabucasa import Cloud diff --git a/tests/components/cloud/conftest.py b/tests/components/cloud/conftest.py index 798b169393a..0147556a888 100644 --- a/tests/components/cloud/conftest.py +++ b/tests/components/cloud/conftest.py @@ -1,4 +1,5 @@ """Fixtures for cloud tests.""" + from collections.abc import AsyncGenerator, Callable, Coroutine from typing import Any from unittest.mock import DEFAULT, MagicMock, PropertyMock, patch @@ -235,8 +236,9 @@ def mock_cloud_login(hass, mock_cloud_setup): @pytest.fixture(name="mock_auth") def mock_auth_fixture(): """Mock check token.""" - with patch("hass_nabucasa.auth.CognitoAuth.async_check_token"), patch( - "hass_nabucasa.auth.CognitoAuth.async_renew_access_token" + with ( + patch("hass_nabucasa.auth.CognitoAuth.async_check_token"), + patch("hass_nabucasa.auth.CognitoAuth.async_renew_access_token"), ): yield diff --git a/tests/components/cloud/test_account_link.py b/tests/components/cloud/test_account_link.py index 14f99fe0fb1..99a21734588 100644 --- a/tests/components/cloud/test_account_link.py +++ b/tests/components/cloud/test_account_link.py @@ -1,4 +1,5 @@ """Test account link services.""" + import asyncio import logging from time import time @@ -53,30 +54,34 @@ async def test_setup_provide_implementation(hass: HomeAssistant) -> None: legacy_entry.add_to_hass(hass) account_link.async_setup(hass) - with patch( - "homeassistant.components.cloud.account_link._get_services", - return_value=[ - {"service": "test", "min_version": "0.1.0"}, - {"service": "too_new", "min_version": "1000000.0.0"}, - {"service": "dev", "min_version": "2022.9.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, - }, - ], - ), patch( - "homeassistant.components.cloud.account_link.HA_VERSION", "2022.9.0.dev20220817" + with ( + patch( + "homeassistant.components.cloud.account_link._get_services", + return_value=[ + {"service": "test", "min_version": "0.1.0"}, + {"service": "too_new", "min_version": "1000000.0.0"}, + {"service": "dev", "min_version": "2022.9.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, + }, + ], + ), + patch( + "homeassistant.components.cloud.account_link.HA_VERSION", + "2022.9.0.dev20220817", + ), ): assert ( await config_entry_oauth2_flow.async_get_implementations( @@ -131,10 +136,13 @@ async def test_get_services_cached(hass: HomeAssistant) -> None: services = 1 - with patch.object(account_link, "CACHE_TIMEOUT", 0), patch( - "hass_nabucasa.account_link.async_fetch_available_services", - side_effect=lambda _: services, - ) as mock_fetch: + with ( + patch.object(account_link, "CACHE_TIMEOUT", 0), + patch( + "hass_nabucasa.account_link.async_fetch_available_services", + side_effect=lambda _: services, + ) as mock_fetch, + ): assert await account_link._get_services(hass) == 1 services = 2 @@ -158,9 +166,12 @@ async def test_get_services_error(hass: HomeAssistant) -> None: """Test that we cache services.""" hass.data["cloud"] = None - with patch.object(account_link, "CACHE_TIMEOUT", 0), patch( - "hass_nabucasa.account_link.async_fetch_available_services", - side_effect=TimeoutError, + with ( + patch.object(account_link, "CACHE_TIMEOUT", 0), + patch( + "hass_nabucasa.account_link.async_fetch_available_services", + side_effect=TimeoutError, + ), ): assert await account_link._get_services(hass) == [] assert account_link.DATA_SERVICES not in hass.data diff --git a/tests/components/cloud/test_alexa_config.py b/tests/components/cloud/test_alexa_config.py index 3ac2417247c..a6b05198ca4 100644 --- a/tests/components/cloud/test_alexa_config.py +++ b/tests/components/cloud/test_alexa_config.py @@ -1,4 +1,5 @@ """Test Alexa config.""" + import contextlib from unittest.mock import AsyncMock, Mock, patch @@ -330,9 +331,12 @@ def patch_sync_helper(): to_remove.extend([ent_id for ent_id in to_rem if ent_id not in to_remove]) return True - with patch("homeassistant.components.cloud.alexa_config.SYNC_DELAY", 0), patch( - "homeassistant.components.cloud.alexa_config.CloudAlexaConfig._sync_helper", - side_effect=sync_helper, + with ( + patch("homeassistant.components.cloud.alexa_config.SYNC_DELAY", 0), + patch( + "homeassistant.components.cloud.alexa_config.CloudAlexaConfig._sync_helper", + side_effect=sync_helper, + ), ): yield to_update, to_remove @@ -484,10 +488,13 @@ async def test_alexa_update_report_state( await conf.async_initialize() await conf.set_authorized(True) - with patch( - "homeassistant.components.cloud.alexa_config.CloudAlexaConfig.async_sync_entities", - ) as mock_sync, patch( - "homeassistant.components.cloud.alexa_config.CloudAlexaConfig.async_enable_proactive_mode", + with ( + patch( + "homeassistant.components.cloud.alexa_config.CloudAlexaConfig.async_sync_entities", + ) as mock_sync, + patch( + "homeassistant.components.cloud.alexa_config.CloudAlexaConfig.async_enable_proactive_mode", + ), ): await cloud_prefs.async_update(alexa_report_state=True) await hass.async_block_till_done() diff --git a/tests/components/cloud/test_assist_pipeline.py b/tests/components/cloud/test_assist_pipeline.py index 7f1411dab45..5c2fc074898 100644 --- a/tests/components/cloud/test_assist_pipeline.py +++ b/tests/components/cloud/test_assist_pipeline.py @@ -1,4 +1,5 @@ """Test the cloud assist pipeline.""" + import pytest from homeassistant.components.cloud.assist_pipeline import ( diff --git a/tests/components/cloud/test_binary_sensor.py b/tests/components/cloud/test_binary_sensor.py index 6505be1fe10..5e83fa34c3c 100644 --- a/tests/components/cloud/test_binary_sensor.py +++ b/tests/components/cloud/test_binary_sensor.py @@ -1,4 +1,5 @@ """Tests for the cloud binary sensor.""" + from collections.abc import Generator from unittest.mock import MagicMock, patch diff --git a/tests/components/cloud/test_client.py b/tests/components/cloud/test_client.py index 66a5ed8e4ad..5e15aa32b6f 100644 --- a/tests/components/cloud/test_client.py +++ b/tests/components/cloud/test_client.py @@ -1,4 +1,5 @@ """Test the cloud.iot module.""" + from datetime import timedelta from unittest.mock import AsyncMock, MagicMock, Mock, PropertyMock, patch @@ -7,6 +8,7 @@ from aiohttp import web from hass_nabucasa.client import RemoteActivationNotAllowed import pytest +from homeassistant.components import webhook from homeassistant.components.cloud import DOMAIN from homeassistant.components.cloud.client import ( VALID_REPAIR_TRANSLATION_KEYS, @@ -205,7 +207,7 @@ async def test_webhook_msg( received.append(request) return web.json_response({"from": "handler"}) - hass.components.webhook.async_register("test", "Test", "mock-webhook-id", handler) + webhook.async_register(hass, "test", "Test", "mock-webhook-id", handler) response = await cloud.client.async_webhook_message( { @@ -366,9 +368,10 @@ async def test_system_msg(hass: HomeAssistant) -> None: async def test_cloud_connection_info(hass: HomeAssistant) -> None: """Test connection info msg.""" - with patch("hass_nabucasa.Cloud.initialize"), patch( - "uuid.UUID.hex", new_callable=PropertyMock - ) as hexmock: + with ( + patch("hass_nabucasa.Cloud.initialize"), + patch("uuid.UUID.hex", new_callable=PropertyMock) as hexmock, + ): hexmock.return_value = "12345678901234567890" setup = await async_setup_component(hass, "cloud", {"cloud": {}}) assert setup diff --git a/tests/components/cloud/test_config_flow.py b/tests/components/cloud/test_config_flow.py index ee4e37276dc..6b506d6b883 100644 --- a/tests/components/cloud/test_config_flow.py +++ b/tests/components/cloud/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Home Assistant Cloud config flow.""" + from unittest.mock import patch from homeassistant.components.cloud.const import DOMAIN @@ -10,12 +11,15 @@ from tests.common import MockConfigEntry async def test_config_flow(hass: HomeAssistant) -> None: """Test create cloud entry.""" - with patch( - "homeassistant.components.cloud.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.cloud.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.cloud.async_setup", return_value=True + ) as mock_setup, + patch( + "homeassistant.components.cloud.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": "system"} ) diff --git a/tests/components/cloud/test_google_config.py b/tests/components/cloud/test_google_config.py index 77648353f67..66530bfa3f8 100644 --- a/tests/components/cloud/test_google_config.py +++ b/tests/components/cloud/test_google_config.py @@ -1,6 +1,7 @@ """Test the Cloud Google Config.""" + from http import HTTPStatus -from unittest.mock import Mock, patch +from unittest.mock import Mock, PropertyMock, patch from freezegun import freeze_time import pytest @@ -68,9 +69,12 @@ async def test_google_update_report_state( mock_conf._cloud.subscription_expired = False - with patch.object(mock_conf, "async_sync_entities") as mock_sync, patch( - "homeassistant.components.google_assistant.report_state.async_enable_report_state" - ) as mock_report_state: + with ( + patch.object(mock_conf, "async_sync_entities") as mock_sync, + patch( + "homeassistant.components.google_assistant.report_state.async_enable_report_state" + ) as mock_report_state, + ): await cloud_prefs.async_update(google_report_state=True) await hass.async_block_till_done() @@ -89,9 +93,12 @@ async def test_google_update_report_state_subscription_expired( assert mock_conf._cloud.subscription_expired - with patch.object(mock_conf, "async_sync_entities") as mock_sync, patch( - "homeassistant.components.google_assistant.report_state.async_enable_report_state" - ) as mock_report_state: + with ( + patch.object(mock_conf, "async_sync_entities") as mock_sync, + patch( + "homeassistant.components.google_assistant.report_state.async_enable_report_state" + ) as mock_report_state, + ): await cloud_prefs.async_update(google_report_state=True) await hass.async_block_till_done() @@ -153,8 +160,9 @@ async def test_google_update_expose_trigger_sync( await hass.async_block_till_done() await config.async_connect_agent_user("mock-user-id") - with patch.object(config, "async_sync_entities") as mock_sync, patch.object( - ga_helpers, "SYNC_DELAY", 0 + with ( + patch.object(config, "async_sync_entities") as mock_sync, + patch.object(ga_helpers, "SYNC_DELAY", 0), ): expose_entity(hass, light_entry.entity_id, True) await hass.async_block_till_done() @@ -163,8 +171,9 @@ async def test_google_update_expose_trigger_sync( assert len(mock_sync.mock_calls) == 1 - with patch.object(config, "async_sync_entities") as mock_sync, patch.object( - ga_helpers, "SYNC_DELAY", 0 + with ( + patch.object(config, "async_sync_entities") as mock_sync, + patch.object(ga_helpers, "SYNC_DELAY", 0), ): expose_entity(hass, light_entry.entity_id, False) expose_entity(hass, binary_sensor_entry.entity_id, True) @@ -193,10 +202,10 @@ async def test_google_entity_registry_sync( await config.async_initialize() await config.async_connect_agent_user("mock-user-id") - with patch.object( - config, "async_schedule_google_sync_all" - ) as mock_sync, patch.object(config, "async_sync_entities_all"), patch.object( - ga_helpers, "SYNC_DELAY", 0 + with ( + patch.object(config, "async_schedule_google_sync_all") as mock_sync, + patch.object(config, "async_sync_entities_all"), + patch.object(ga_helpers, "SYNC_DELAY", 0), ): # Created entity entry = entity_registry.async_get_or_create( @@ -340,12 +349,13 @@ async def test_sync_google_on_home_assistant_start( config = CloudGoogleConfig( hass, GACTIONS_SCHEMA({}), "mock-user-id", cloud_prefs, hass.data["cloud"] ) - hass.set_state(CoreState.starting) + hass.set_state(CoreState.not_running) with patch.object(config, "async_sync_entities_all") as mock_sync: await config.async_initialize() + await hass.async_block_till_done() assert len(mock_sync.mock_calls) == 0 - hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() assert len(mock_sync.mock_calls) == 1 @@ -508,7 +518,7 @@ async def test_google_config_migrate_expose_entity_prefs( google_settings_version: int, ) -> None: """Test migrating Google entity config.""" - hass.set_state(CoreState.starting) + hass.set_state(CoreState.not_running) assert await async_setup_component(hass, "homeassistant", {}) hass.states.async_set("light.state_only", "on") @@ -621,7 +631,7 @@ async def test_google_config_migrate_expose_entity_prefs_v2_no_exposed( entity_registry: er.EntityRegistry, ) -> None: """Test migrating Google entity config from v2 to v3 when no entity is exposed.""" - hass.set_state(CoreState.starting) + hass.set_state(CoreState.not_running) assert await async_setup_component(hass, "homeassistant", {}) hass.states.async_set("light.state_only", "on") @@ -668,7 +678,7 @@ async def test_google_config_migrate_expose_entity_prefs_v2_exposed( entity_registry: er.EntityRegistry, ) -> None: """Test migrating Google entity config from v2 to v3 when an entity is exposed.""" - hass.set_state(CoreState.starting) + hass.set_state(CoreState.not_running) assert await async_setup_component(hass, "homeassistant", {}) hass.states.async_set("light.state_only", "on") @@ -715,7 +725,7 @@ async def test_google_config_migrate_expose_entity_prefs_default_none( entity_registry: er.EntityRegistry, ) -> None: """Test migrating Google entity config.""" - hass.set_state(CoreState.starting) + hass.set_state(CoreState.not_running) assert await async_setup_component(hass, "homeassistant", {}) entity_default = entity_registry.async_get_or_create( @@ -752,7 +762,7 @@ async def test_google_config_migrate_expose_entity_prefs_default( entity_registry: er.EntityRegistry, ) -> None: """Test migrating Google entity config.""" - hass.set_state(CoreState.starting) + hass.set_state(CoreState.not_running) assert await async_setup_component(hass, "homeassistant", {}) @@ -855,3 +865,43 @@ async def test_google_config_get_agent_user_id( == config.agent_user_id ) assert config.get_agent_user_id_from_webhook("other_id") != config.agent_user_id + + +async def test_google_config_get_agent_users( + hass: HomeAssistant, mock_cloud_login, cloud_prefs +) -> None: + """Test overridden async_get_agent_users method.""" + username_mock = PropertyMock(return_value="blah") + + # We should not call Cloud.username when not logged in + cloud_prefs._prefs["google_connected"] = True + assert cloud_prefs.google_connected + mock_cloud = Mock(is_logged_in=False) + type(mock_cloud).username = username_mock + config = CloudGoogleConfig( + hass, GACTIONS_SCHEMA({}), "mock-user-id", cloud_prefs, mock_cloud + ) + assert config.async_get_agent_users() == () + username_mock.assert_not_called() + + # We should not call Cloud.username when not connected + cloud_prefs._prefs["google_connected"] = False + assert not cloud_prefs.google_connected + mock_cloud = Mock(is_logged_in=True) + type(mock_cloud).username = username_mock + config = CloudGoogleConfig( + hass, GACTIONS_SCHEMA({}), "mock-user-id", cloud_prefs, mock_cloud + ) + assert config.async_get_agent_users() == () + username_mock.assert_not_called() + + # Logged in and connected + cloud_prefs._prefs["google_connected"] = True + assert cloud_prefs.google_connected + mock_cloud = Mock(is_logged_in=True) + type(mock_cloud).username = username_mock + config = CloudGoogleConfig( + hass, GACTIONS_SCHEMA({}), "mock-user-id", cloud_prefs, mock_cloud + ) + assert config.async_get_agent_users() == ("blah",) + username_mock.assert_called() diff --git a/tests/components/cloud/test_http_api.py b/tests/components/cloud/test_http_api.py index 9abe0088b20..0dad7cfa882 100644 --- a/tests/components/cloud/test_http_api.py +++ b/tests/components/cloud/test_http_api.py @@ -1,13 +1,16 @@ """Tests for the HTTP API for the cloud component.""" + from copy import deepcopy from http import HTTPStatus +import json from typing import Any from unittest.mock import AsyncMock, MagicMock, Mock, patch import aiohttp -from hass_nabucasa import thingtalk, voice +from hass_nabucasa import thingtalk from hass_nabucasa.auth import Unauthenticated, UnknownError from hass_nabucasa.const import STATE_CONNECTED +from hass_nabucasa.voice import TTS_VOICES import pytest from homeassistant.components.alexa import errors as alexa_errors @@ -16,6 +19,7 @@ from homeassistant.components.assist_pipeline.pipeline import STORAGE_KEY from homeassistant.components.cloud.const import DEFAULT_EXPOSED_DOMAINS, DOMAIN from homeassistant.components.google_assistant.helpers import GoogleEntity from homeassistant.components.homeassistant import exposed_entities +from homeassistant.components.websocket_api import ERR_INVALID_FORMAT from homeassistant.core import HomeAssistant, State from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component @@ -466,19 +470,17 @@ async def test_register_view_with_location( with patch( "homeassistant.components.cloud.http_api.async_detect_location_info", return_value=LocationInfo( - **{ - "country_code": "XX", - "zip_code": "12345", - "region_code": "GH", - "ip": "1.2.3.4", - "city": "Gotham", - "region_name": "Gotham", - "time_zone": "Earth/Gotham", - "currency": "XXX", - "latitude": "12.34567", - "longitude": "12.34567", - "use_metric": True, - } + country_code="XX", + zip_code="12345", + region_code="GH", + ip="1.2.3.4", + city="Gotham", + region_name="Gotham", + time_zone="Earth/Gotham", + currency="XXX", + latitude="12.34567", + longitude="12.34567", + use_metric=True, ), ): req = await cloud_client.post( @@ -698,6 +700,45 @@ async def test_resend_confirm_view_unknown_error( assert req.status == HTTPStatus.BAD_GATEWAY +async def test_websocket_remove_data( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + cloud: MagicMock, + setup_cloud: None, +) -> None: + """Test removing cloud data.""" + cloud.id_token = None + client = await hass_ws_client(hass) + + with patch.object(cloud.client.prefs, "async_erase_config") as mock_erase_config: + await client.send_json_auto_id({"type": "cloud/remove_data"}) + response = await client.receive_json() + + assert response["success"] + cloud.remove_data.assert_awaited_once_with() + mock_erase_config.assert_awaited_once_with() + + +async def test_websocket_remove_data_logged_in( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + cloud: MagicMock, + setup_cloud: None, +) -> None: + """Test removing cloud data.""" + cloud.iot.state = STATE_CONNECTED + client = await hass_ws_client(hass) + + await client.send_json_auto_id({"type": "cloud/remove_data"}) + response = await client.receive_json() + + assert not response["success"] + assert response["error"] == { + "code": "logged_in", + "message": "Can't remove data when logged in.", + } + + async def test_websocket_status( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, @@ -708,14 +749,17 @@ async def test_websocket_status( cloud.iot.state = STATE_CONNECTED client = await hass_ws_client(hass) - with patch.dict( - "homeassistant.components.google_assistant.const.DOMAIN_TO_GOOGLE_TYPES", - {"light": None}, - clear=True, - ), patch.dict( - "homeassistant.components.alexa.entities.ENTITY_ADAPTERS", - {"switch": None}, - clear=True, + with ( + patch.dict( + "homeassistant.components.google_assistant.const.DOMAIN_TO_GOOGLE_TYPES", + {"light": None}, + clear=True, + ), + patch.dict( + "homeassistant.components.alexa.entities.ENTITY_ADAPTERS", + {"switch": None}, + clear=True, + ), ): await client.send_json({"id": 5, "type": "cloud/status"}) response = await client.receive_json() @@ -736,7 +780,7 @@ async def test_websocket_status( "google_report_state": True, "remote_allow_remote_enable": True, "remote_enabled": False, - "tts_default_voice": ["en-US", "female"], + "tts_default_voice": ["en-US", "JennyNeural"], }, "alexa_entities": { "include_domains": [], @@ -858,14 +902,13 @@ async def test_websocket_update_preferences( client = await hass_ws_client(hass) - await client.send_json( + await client.send_json_auto_id( { - "id": 5, "type": "cloud/update_prefs", "alexa_enabled": False, "google_enabled": False, "google_secure_devices_pin": "1234", - "tts_default_voice": ["en-GB", "male"], + "tts_default_voice": ["en-GB", "RyanNeural"], "remote_allow_remote_enable": False, } ) @@ -876,7 +919,34 @@ async def test_websocket_update_preferences( assert not cloud.client.prefs.alexa_enabled assert cloud.client.prefs.google_secure_devices_pin == "1234" assert cloud.client.prefs.remote_allow_remote_enable is False - assert cloud.client.prefs.tts_default_voice == ("en-GB", "male") + assert cloud.client.prefs.tts_default_voice == ("en-GB", "RyanNeural") + + +@pytest.mark.parametrize( + ("language", "voice"), [("en-GB", "bad_voice"), ("bad_language", "RyanNeural")] +) +async def test_websocket_update_preferences_bad_voice( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + cloud: MagicMock, + setup_cloud: None, + language: str, + voice: str, +) -> None: + """Test updating preference.""" + client = await hass_ws_client(hass) + + await client.send_json_auto_id( + { + "type": "cloud/update_prefs", + "tts_default_voice": [language, voice], + } + ) + response = await client.receive_json() + + assert not response["success"] + assert response["error"]["code"] == ERR_INVALID_FORMAT + assert cloud.client.prefs.tts_default_voice == ("en-US", "JennyNeural") async def test_websocket_update_preferences_alexa_report_state( @@ -887,14 +957,20 @@ async def test_websocket_update_preferences_alexa_report_state( """Test updating alexa_report_state sets alexa authorized.""" client = await hass_ws_client(hass) - with patch( - ( - "homeassistant.components.cloud.alexa_config.CloudAlexaConfig" - ".async_get_access_token" + with ( + patch( + "homeassistant.components.cloud.alexa_config.CloudAlexaConfig.async_sync_entities" ), - ), patch( - "homeassistant.components.cloud.alexa_config.CloudAlexaConfig.set_authorized" - ) as set_authorized_mock: + patch( + ( + "homeassistant.components.cloud.alexa_config.CloudAlexaConfig" + ".async_get_access_token" + ), + ), + patch( + "homeassistant.components.cloud.alexa_config.CloudAlexaConfig.set_authorized" + ) as set_authorized_mock, + ): set_authorized_mock.assert_not_called() await client.send_json( @@ -903,6 +979,7 @@ async def test_websocket_update_preferences_alexa_report_state( response = await client.receive_json() set_authorized_mock.assert_called_once_with(True) + await hass.async_block_till_done() assert response["success"] @@ -915,15 +992,18 @@ async def test_websocket_update_preferences_require_relink( """Test updating preference requires relink.""" client = await hass_ws_client(hass) - with patch( - ( - "homeassistant.components.cloud.alexa_config.CloudAlexaConfig" - ".async_get_access_token" + with ( + patch( + ( + "homeassistant.components.cloud.alexa_config.CloudAlexaConfig" + ".async_get_access_token" + ), + side_effect=alexa_errors.RequireRelink, ), - side_effect=alexa_errors.RequireRelink, - ), patch( - "homeassistant.components.cloud.alexa_config.CloudAlexaConfig.set_authorized" - ) as set_authorized_mock: + patch( + "homeassistant.components.cloud.alexa_config.CloudAlexaConfig.set_authorized" + ) as set_authorized_mock, + ): set_authorized_mock.assert_not_called() await client.send_json( @@ -945,15 +1025,18 @@ async def test_websocket_update_preferences_no_token( """Test updating preference no token available.""" client = await hass_ws_client(hass) - with patch( - ( - "homeassistant.components.cloud.alexa_config.CloudAlexaConfig" - ".async_get_access_token" + with ( + patch( + ( + "homeassistant.components.cloud.alexa_config.CloudAlexaConfig" + ".async_get_access_token" + ), + side_effect=alexa_errors.NoTokenAvailable, ), - side_effect=alexa_errors.NoTokenAvailable, - ), patch( - "homeassistant.components.cloud.alexa_config.CloudAlexaConfig.set_authorized" - ) as set_authorized_mock: + patch( + "homeassistant.components.cloud.alexa_config.CloudAlexaConfig.set_authorized" + ) as set_authorized_mock, + ): set_authorized_mock.assert_not_called() await client.send_json( @@ -1287,13 +1370,16 @@ async def test_list_alexa_entities( "interfaces": ["Alexa.PowerController", "Alexa.EndpointHealth", "Alexa"], } - with patch( - ( - "homeassistant.components.cloud.alexa_config.CloudAlexaConfig" - ".async_get_access_token" + with ( + patch( + ( + "homeassistant.components.cloud.alexa_config.CloudAlexaConfig" + ".async_get_access_token" + ), + ), + patch( + "homeassistant.components.cloud.alexa_config.alexa_state_report.async_send_add_or_update_message" ), - ), patch( - "homeassistant.components.cloud.alexa_config.alexa_state_report.async_send_add_or_update_message" ): # Add the entity to the entity registry entity_registry.async_get_or_create( @@ -1555,24 +1641,23 @@ async def test_tts_info( setup_cloud: None, ) -> None: """Test that we can get TTS info.""" - # Verify the format is as expected - assert voice.MAP_VOICE[("en-US", voice.Gender.FEMALE)] == "JennyNeural" - client = await hass_ws_client(hass) - with patch.dict( - "homeassistant.components.cloud.http_api.MAP_VOICE", - { - ("en-US", voice.Gender.MALE): "GuyNeural", - ("en-US", voice.Gender.FEMALE): "JennyNeural", - }, - clear=True, - ): - await client.send_json({"id": 5, "type": "cloud/tts/info"}) - response = await client.receive_json() + await client.send_json_auto_id({"type": "cloud/tts/info"}) + response = await client.receive_json() assert response["success"] - assert response["result"] == {"languages": [["en-US", "male"], ["en-US", "female"]]} + assert response["result"] == { + "languages": json.loads( + json.dumps( + [ + (language, voice) + for language, voices in TTS_VOICES.items() + for voice in voices + ] + ) + ) + } @pytest.mark.parametrize( diff --git a/tests/components/cloud/test_init.py b/tests/components/cloud/test_init.py index 4cef8c8437e..9cc1324ebc1 100644 --- a/tests/components/cloud/test_init.py +++ b/tests/components/cloud/test_init.py @@ -1,4 +1,5 @@ """Test the cloud component.""" + from collections.abc import Callable, Coroutine from typing import Any from unittest.mock import MagicMock, patch @@ -71,12 +72,14 @@ async def test_remote_services( with patch("hass_nabucasa.remote.RemoteUI.connect") as mock_connect: await hass.services.async_call(DOMAIN, "remote_connect", blocking=True) + await hass.async_block_till_done() assert mock_connect.called assert cloud.client.remote_autostart with patch("hass_nabucasa.remote.RemoteUI.disconnect") as mock_disconnect: await hass.services.async_call(DOMAIN, "remote_disconnect", blocking=True) + await hass.async_block_till_done() assert mock_disconnect.called assert not cloud.client.remote_autostart @@ -84,8 +87,9 @@ async def test_remote_services( # Test admin access required non_admin_context = Context(user_id=hass_read_only_user.id) - with patch("hass_nabucasa.remote.RemoteUI.connect") as mock_connect, pytest.raises( - Unauthorized + with ( + patch("hass_nabucasa.remote.RemoteUI.connect") as mock_connect, + pytest.raises(Unauthorized), ): await hass.services.async_call( DOMAIN, "remote_connect", blocking=True, context=non_admin_context @@ -93,9 +97,10 @@ async def test_remote_services( assert mock_connect.called is False - with patch( - "hass_nabucasa.remote.RemoteUI.disconnect" - ) as mock_disconnect, pytest.raises(Unauthorized): + with ( + patch("hass_nabucasa.remote.RemoteUI.disconnect") as mock_disconnect, + pytest.raises(Unauthorized), + ): await hass.services.async_call( DOMAIN, "remote_disconnect", blocking=True, context=non_admin_context ) diff --git a/tests/components/cloud/test_prefs.py b/tests/components/cloud/test_prefs.py index 608f6ef3df0..9b0fa4c01d7 100644 --- a/tests/components/cloud/test_prefs.py +++ b/tests/components/cloud/test_prefs.py @@ -1,12 +1,15 @@ """Test Cloud preferences.""" + from typing import Any -from unittest.mock import patch +from unittest.mock import ANY, MagicMock, patch import pytest from homeassistant.auth.const import GROUP_ID_ADMIN +from homeassistant.components.cloud.const import DOMAIN, PREF_TTS_DEFAULT_VOICE from homeassistant.components.cloud.prefs import STORAGE_KEY, CloudPreferences from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component async def test_set_username(hass: HomeAssistant) -> None: @@ -25,8 +28,34 @@ async def test_set_username(hass: HomeAssistant) -> None: assert prefs.google_enabled +async def test_erase_config(hass: HomeAssistant) -> None: + """Test erasing config.""" + prefs = CloudPreferences(hass) + await prefs.async_initialize() + assert prefs._prefs == { + **prefs._empty_config(""), + "google_local_webhook_id": ANY, + "instance_id": ANY, + } + + await prefs.async_update(google_enabled=False) + assert prefs._prefs == { + **prefs._empty_config(""), + "google_enabled": False, + "google_local_webhook_id": ANY, + "instance_id": ANY, + } + + await prefs.async_erase_config() + assert prefs._prefs == { + **prefs._empty_config(""), + "google_local_webhook_id": ANY, + "instance_id": ANY, + } + + async def test_set_username_migration(hass: HomeAssistant) -> None: - """Test we not clear config if we had no username.""" + """Test we do not clear config if we had no username.""" prefs = CloudPreferences(hass) with patch.object(prefs, "_empty_config", return_value=prefs._empty_config(None)): @@ -122,3 +151,26 @@ async def test_import_google_assistant_settings( prefs = CloudPreferences(hass) await prefs.async_initialize() assert prefs.google_connected == google_connected + + +@pytest.mark.parametrize( + ("stored_language", "expected_language", "voice"), + [("en-US", "en-US", "GuyNeural"), ("missing_language", "en-US", "JennyNeural")], +) +async def test_tts_default_voice_legacy_gender( + hass: HomeAssistant, + cloud: MagicMock, + hass_storage: dict[str, Any], + stored_language: str, + expected_language: str, + voice: str, +) -> None: + """Test tts with legacy gender as default tts voice setting in storage.""" + hass_storage[STORAGE_KEY] = { + "version": 1, + "data": {PREF_TTS_DEFAULT_VOICE: [stored_language, "male"]}, + } + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + await hass.async_block_till_done() + + assert cloud.client.prefs.tts_default_voice == (expected_language, voice) diff --git a/tests/components/cloud/test_repairs.py b/tests/components/cloud/test_repairs.py index 0e662c30ee7..abfc917016d 100644 --- a/tests/components/cloud/test_repairs.py +++ b/tests/components/cloud/test_repairs.py @@ -1,4 +1,5 @@ """Test cloud repairs.""" + from collections.abc import Generator from datetime import timedelta from http import HTTPStatus @@ -147,13 +148,11 @@ async def test_legacy_subscription_repair_flow( flow_id = data["flow_id"] assert data == { - "version": 1, "type": "create_entry", "flow_id": flow_id, "handler": DOMAIN, "description": None, "description_placeholders": None, - "minor_version": 1, } assert not issue_registry.async_get_issue( diff --git a/tests/components/cloud/test_stt.py b/tests/components/cloud/test_stt.py index 305780e33e1..540aa173beb 100644 --- a/tests/components/cloud/test_stt.py +++ b/tests/components/cloud/test_stt.py @@ -1,4 +1,5 @@ """Test the speech-to-text platform for the cloud integration.""" + from collections.abc import AsyncGenerator from copy import deepcopy from http import HTTPStatus diff --git a/tests/components/cloud/test_subscription.py b/tests/components/cloud/test_subscription.py index c7297e35744..22839b585fd 100644 --- a/tests/components/cloud/test_subscription.py +++ b/tests/components/cloud/test_subscription.py @@ -1,4 +1,5 @@ """Test cloud subscription functions.""" + from unittest.mock import AsyncMock, Mock from hass_nabucasa import Cloud diff --git a/tests/components/cloud/test_system_health.py b/tests/components/cloud/test_system_health.py index 5480cd557fd..c6e738011d6 100644 --- a/tests/components/cloud/test_system_health.py +++ b/tests/components/cloud/test_system_health.py @@ -1,4 +1,5 @@ """Test cloud system health.""" + import asyncio from collections.abc import Callable, Coroutine from typing import Any diff --git a/tests/components/cloud/test_tts.py b/tests/components/cloud/test_tts.py index db43c40b69a..06dbcf174a7 100644 --- a/tests/components/cloud/test_tts.py +++ b/tests/components/cloud/test_tts.py @@ -1,20 +1,31 @@ """Tests for cloud tts.""" + from collections.abc import AsyncGenerator, Callable, Coroutine from copy import deepcopy from http import HTTPStatus from typing import Any from unittest.mock import AsyncMock, MagicMock, patch -from hass_nabucasa.voice import MAP_VOICE, VoiceError, VoiceTokenError +from hass_nabucasa.voice import TTS_VOICES, VoiceError, VoiceTokenError import pytest import voluptuous as vol from homeassistant.components.assist_pipeline.pipeline import STORAGE_KEY from homeassistant.components.cloud import DOMAIN, const, tts -from homeassistant.components.tts import DOMAIN as TTS_DOMAIN +from homeassistant.components.media_player import ( + ATTR_MEDIA_CONTENT_ID, + DOMAIN as DOMAIN_MP, + SERVICE_PLAY_MEDIA, +) +from homeassistant.components.tts import ( + ATTR_LANGUAGE, + ATTR_MEDIA_PLAYER_ENTITY_ID, + ATTR_MESSAGE, + DOMAIN as TTS_DOMAIN, +) from homeassistant.components.tts.helper import get_engine_instance from homeassistant.config import async_process_ha_core_config -from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN +from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE, STATE_UNKNOWN from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_registry import EntityRegistry from homeassistant.helpers.issue_registry import IssueRegistry, IssueSeverity @@ -22,6 +33,8 @@ from homeassistant.setup import async_setup_component from . import PIPELINE_DATA +from tests.common import async_mock_service +from tests.components.tts.common import get_media_source_url from tests.typing import ClientSessionGenerator @@ -43,7 +56,11 @@ async def internal_url_mock(hass: HomeAssistant) -> None: def test_default_exists() -> None: """Test our default language exists.""" - assert const.DEFAULT_TTS_DEFAULT_VOICE in MAP_VOICE + assert const.DEFAULT_TTS_DEFAULT_VOICE[0] in TTS_VOICES + assert ( + const.DEFAULT_TTS_DEFAULT_VOICE[1] + in TTS_VOICES[const.DEFAULT_TTS_DEFAULT_VOICE[0]] + ) def test_schema() -> None: @@ -100,26 +117,28 @@ async def test_prefs_default_voice( """Test cloud provider uses the preferences.""" assert await async_setup_component(hass, "homeassistant", {}) assert await async_setup_component(hass, TTS_DOMAIN, {TTS_DOMAIN: platform_config}) + await hass.async_block_till_done() assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) await hass.async_block_till_done() - assert cloud.client.prefs.tts_default_voice == ("en-US", "female") + assert cloud.client.prefs.tts_default_voice == ("en-US", "JennyNeural") on_start_callback = cloud.register_on_start.call_args[0][0] await on_start_callback() + await hass.async_block_till_done() engine = get_engine_instance(hass, engine_id) assert engine is not None # The platform config provider will be overridden by the discovery info provider. assert engine.default_language == "en-US" - assert engine.default_options == {"gender": "female", "audio_output": "mp3"} + assert engine.default_options == {"audio_output": "mp3"} - await set_cloud_prefs({"tts_default_voice": ("nl-NL", "male")}) + await set_cloud_prefs({"tts_default_voice": ("nl-NL", "MaartenNeural")}) await hass.async_block_till_done() assert engine.default_language == "nl-NL" - assert engine.default_options == {"gender": "male", "audio_output": "mp3"} + assert engine.default_options == {"audio_output": "mp3"} async def test_deprecated_platform_config( @@ -221,11 +240,11 @@ async def test_get_tts_audio( "url": ( "http://example.local:8123/api/tts_proxy/" "42f18378fd4393d18c8dd11d03fa9563c1e54491" - f"_en-us_e09b5a0968_{expected_url_suffix}.mp3" + f"_en-us_6e8b81ac47_{expected_url_suffix}.mp3" ), "path": ( "/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491" - f"_en-us_e09b5a0968_{expected_url_suffix}.mp3" + f"_en-us_6e8b81ac47_{expected_url_suffix}.mp3" ), } await hass.async_block_till_done() @@ -234,7 +253,8 @@ async def test_get_tts_audio( assert mock_process_tts.call_args is not None assert mock_process_tts.call_args.kwargs["text"] == "There is someone at the door." assert mock_process_tts.call_args.kwargs["language"] == "en-US" - assert mock_process_tts.call_args.kwargs["gender"] == "female" + assert mock_process_tts.call_args.kwargs["gender"] is None + assert mock_process_tts.call_args.kwargs["voice"] == "JennyNeural" assert mock_process_tts.call_args.kwargs["output"] == "mp3" @@ -273,11 +293,11 @@ async def test_get_tts_audio_logged_out( "url": ( "http://example.local:8123/api/tts_proxy/" "42f18378fd4393d18c8dd11d03fa9563c1e54491" - f"_en-us_e09b5a0968_{expected_url_suffix}.mp3" + f"_en-us_6e8b81ac47_{expected_url_suffix}.mp3" ), "path": ( "/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491" - f"_en-us_e09b5a0968_{expected_url_suffix}.mp3" + f"_en-us_6e8b81ac47_{expected_url_suffix}.mp3" ), } await hass.async_block_till_done() @@ -286,7 +306,8 @@ async def test_get_tts_audio_logged_out( assert mock_process_tts.call_args is not None assert mock_process_tts.call_args.kwargs["text"] == "There is someone at the door." assert mock_process_tts.call_args.kwargs["language"] == "en-US" - assert mock_process_tts.call_args.kwargs["gender"] == "female" + assert mock_process_tts.call_args.kwargs["gender"] is None + assert mock_process_tts.call_args.kwargs["voice"] == "JennyNeural" assert mock_process_tts.call_args.kwargs["output"] == "mp3" @@ -337,11 +358,11 @@ async def test_tts_entity( "url": ( "http://example.local:8123/api/tts_proxy/" "42f18378fd4393d18c8dd11d03fa9563c1e54491" - f"_en-us_e09b5a0968_{entity_id}.mp3" + f"_en-us_6e8b81ac47_{entity_id}.mp3" ), "path": ( "/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491" - f"_en-us_e09b5a0968_{entity_id}.mp3" + f"_en-us_6e8b81ac47_{entity_id}.mp3" ), } await hass.async_block_till_done() @@ -350,7 +371,8 @@ async def test_tts_entity( assert mock_process_tts.call_args is not None assert mock_process_tts.call_args.kwargs["text"] == "There is someone at the door." assert mock_process_tts.call_args.kwargs["language"] == "en-US" - assert mock_process_tts.call_args.kwargs["gender"] == "female" + assert mock_process_tts.call_args.kwargs["gender"] is None + assert mock_process_tts.call_args.kwargs["voice"] == "JennyNeural" assert mock_process_tts.call_args.kwargs["output"] == "mp3" state = hass.states.get(entity_id) @@ -477,11 +499,11 @@ async def test_deprecated_voice( "url": ( "http://example.local:8123/api/tts_proxy/" "42f18378fd4393d18c8dd11d03fa9563c1e54491" - f"_{language.lower()}_1c4ec2f170_{expected_url_suffix}.mp3" + f"_{language.lower()}_87567e3e29_{expected_url_suffix}.mp3" ), "path": ( "/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491" - f"_{language.lower()}_1c4ec2f170_{expected_url_suffix}.mp3" + f"_{language.lower()}_87567e3e29_{expected_url_suffix}.mp3" ), } await hass.async_block_till_done() @@ -490,7 +512,7 @@ async def test_deprecated_voice( assert mock_process_tts.call_args is not None assert mock_process_tts.call_args.kwargs["text"] == "There is someone at the door." assert mock_process_tts.call_args.kwargs["language"] == language - assert mock_process_tts.call_args.kwargs["gender"] == "female" + assert mock_process_tts.call_args.kwargs["gender"] is None assert mock_process_tts.call_args.kwargs["voice"] == replacement_voice assert mock_process_tts.call_args.kwargs["output"] == "mp3" issue = issue_registry.async_get_issue( @@ -510,25 +532,25 @@ async def test_deprecated_voice( "url": ( "http://example.local:8123/api/tts_proxy/" "42f18378fd4393d18c8dd11d03fa9563c1e54491" - f"_{language.lower()}_a1c3b0ac0e_{expected_url_suffix}.mp3" + f"_{language.lower()}_13646b7d32_{expected_url_suffix}.mp3" ), "path": ( "/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491" - f"_{language.lower()}_a1c3b0ac0e_{expected_url_suffix}.mp3" + f"_{language.lower()}_13646b7d32_{expected_url_suffix}.mp3" ), } await hass.async_block_till_done() + issue_id = f"deprecated_voice_{deprecated_voice}" + assert mock_process_tts.call_count == 1 assert mock_process_tts.call_args is not None assert mock_process_tts.call_args.kwargs["text"] == "There is someone at the door." assert mock_process_tts.call_args.kwargs["language"] == language - assert mock_process_tts.call_args.kwargs["gender"] == "female" + assert mock_process_tts.call_args.kwargs["gender"] is None assert mock_process_tts.call_args.kwargs["voice"] == replacement_voice assert mock_process_tts.call_args.kwargs["output"] == "mp3" - issue = issue_registry.async_get_issue( - "cloud", f"deprecated_voice_{deprecated_voice}" - ) + issue = issue_registry.async_get_issue("cloud", issue_id) assert issue is not None assert issue.breaks_in_ha_version == "2024.8.0" assert issue.is_fixable is True @@ -539,3 +561,252 @@ async def test_deprecated_voice( "deprecated_voice": deprecated_voice, "replacement_voice": replacement_voice, } + + resp = await client.post( + "/api/repairs/issues/fix", + json={"handler": DOMAIN, "issue_id": issue.issue_id}, + ) + + assert resp.status == HTTPStatus.OK + data = await resp.json() + + flow_id = data["flow_id"] + assert data == { + "type": "form", + "flow_id": flow_id, + "handler": DOMAIN, + "step_id": "confirm", + "data_schema": [], + "errors": None, + "description_placeholders": { + "deprecated_voice": "XiaoxuanNeural", + "replacement_voice": "XiaozhenNeural", + }, + "last_step": None, + "preview": None, + } + + resp = await client.post(f"/api/repairs/issues/fix/{flow_id}") + + assert resp.status == HTTPStatus.OK + data = await resp.json() + + flow_id = data["flow_id"] + assert data == { + "type": "create_entry", + "flow_id": flow_id, + "handler": DOMAIN, + "description": None, + "description_placeholders": None, + } + + assert not issue_registry.async_get_issue(DOMAIN, issue_id) + + +@pytest.mark.parametrize( + ("data", "expected_url_suffix"), + [ + ({"platform": DOMAIN}, DOMAIN), + ({"engine_id": DOMAIN}, DOMAIN), + ({"engine_id": "tts.home_assistant_cloud"}, "tts.home_assistant_cloud"), + ], +) +async def test_deprecated_gender( + hass: HomeAssistant, + issue_registry: IssueRegistry, + cloud: MagicMock, + hass_client: ClientSessionGenerator, + data: dict[str, Any], + expected_url_suffix: str, +) -> None: + """Test we create an issue when a deprecated gender is used for text-to-speech.""" + language = "zh-CN" + gender_option = "male" + mock_process_tts = AsyncMock( + return_value=b"", + ) + cloud.voice.process_tts = mock_process_tts + + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + await hass.async_block_till_done() + await cloud.login("test-user", "test-pass") + client = await hass_client() + + # Test without deprecated gender option. + url = "/api/tts_get_url" + data |= { + "message": "There is someone at the door.", + "language": language, + } + + req = await client.post(url, json=data) + assert req.status == HTTPStatus.OK + response = await req.json() + + assert response == { + "url": ( + "http://example.local:8123/api/tts_proxy/" + "42f18378fd4393d18c8dd11d03fa9563c1e54491" + f"_{language.lower()}_6e8b81ac47_{expected_url_suffix}.mp3" + ), + "path": ( + "/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491" + f"_{language.lower()}_6e8b81ac47_{expected_url_suffix}.mp3" + ), + } + await hass.async_block_till_done() + + assert mock_process_tts.call_count == 1 + assert mock_process_tts.call_args is not None + assert mock_process_tts.call_args.kwargs["text"] == "There is someone at the door." + assert mock_process_tts.call_args.kwargs["language"] == language + assert mock_process_tts.call_args.kwargs["voice"] == "XiaoxiaoNeural" + assert mock_process_tts.call_args.kwargs["output"] == "mp3" + issue = issue_registry.async_get_issue("cloud", "deprecated_gender") + assert issue is None + mock_process_tts.reset_mock() + + # Test with deprecated gender option. + data["options"] = {"gender": gender_option} + + req = await client.post(url, json=data) + assert req.status == HTTPStatus.OK + response = await req.json() + + assert response == { + "url": ( + "http://example.local:8123/api/tts_proxy/" + "42f18378fd4393d18c8dd11d03fa9563c1e54491" + f"_{language.lower()}_dd0e95eb04_{expected_url_suffix}.mp3" + ), + "path": ( + "/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491" + f"_{language.lower()}_dd0e95eb04_{expected_url_suffix}.mp3" + ), + } + await hass.async_block_till_done() + + issue_id = "deprecated_gender" + + assert mock_process_tts.call_count == 1 + assert mock_process_tts.call_args is not None + assert mock_process_tts.call_args.kwargs["text"] == "There is someone at the door." + assert mock_process_tts.call_args.kwargs["language"] == language + assert mock_process_tts.call_args.kwargs["gender"] == gender_option + assert mock_process_tts.call_args.kwargs["voice"] == "XiaoxiaoNeural" + assert mock_process_tts.call_args.kwargs["output"] == "mp3" + issue = issue_registry.async_get_issue("cloud", issue_id) + assert issue is not None + assert issue.breaks_in_ha_version == "2024.10.0" + assert issue.is_fixable is True + assert issue.is_persistent is True + assert issue.severity == IssueSeverity.WARNING + assert issue.translation_key == "deprecated_gender" + assert issue.translation_placeholders == { + "integration_name": "Home Assistant Cloud", + "deprecated_option": "gender", + "replacement_option": "voice", + } + + resp = await client.post( + "/api/repairs/issues/fix", + json={"handler": DOMAIN, "issue_id": issue.issue_id}, + ) + + assert resp.status == HTTPStatus.OK + data = await resp.json() + + flow_id = data["flow_id"] + assert data == { + "type": "form", + "flow_id": flow_id, + "handler": DOMAIN, + "step_id": "confirm", + "data_schema": [], + "errors": None, + "description_placeholders": { + "integration_name": "Home Assistant Cloud", + "deprecated_option": "gender", + "replacement_option": "voice", + }, + "last_step": None, + "preview": None, + } + + resp = await client.post(f"/api/repairs/issues/fix/{flow_id}") + + assert resp.status == HTTPStatus.OK + data = await resp.json() + + flow_id = data["flow_id"] + assert data == { + "type": "create_entry", + "flow_id": flow_id, + "handler": DOMAIN, + "description": None, + "description_placeholders": None, + } + + assert not issue_registry.async_get_issue(DOMAIN, issue_id) + + +@pytest.mark.parametrize( + ("service", "service_data"), + [ + ( + "speak", + { + ATTR_ENTITY_ID: "tts.home_assistant_cloud", + ATTR_LANGUAGE: "id-ID", + ATTR_MEDIA_PLAYER_ENTITY_ID: "media_player.something", + ATTR_MESSAGE: "There is someone at the door.", + }, + ), + ( + "cloud_say", + { + ATTR_ENTITY_ID: "media_player.something", + ATTR_LANGUAGE: "id-ID", + ATTR_MESSAGE: "There is someone at the door.", + }, + ), + ], +) +async def test_tts_services( + hass: HomeAssistant, + cloud: MagicMock, + hass_client: ClientSessionGenerator, + service: str, + service_data: dict[str, Any], +) -> None: + """Test tts services.""" + calls = async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + mock_process_tts = AsyncMock(return_value=b"") + cloud.voice.process_tts = mock_process_tts + + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + await hass.async_block_till_done() + await cloud.login("test-user", "test-pass") + client = await hass_client() + + await hass.services.async_call( + domain=TTS_DOMAIN, + service=service, + service_data=service_data, + blocking=True, + ) + + assert len(calls) == 1 + + url = await get_media_source_url(hass, calls[0].data[ATTR_MEDIA_CONTENT_ID]) + await hass.async_block_till_done() + response = await client.get(url) + assert response.status == HTTPStatus.OK + await hass.async_block_till_done() + + assert mock_process_tts.call_count == 1 + assert mock_process_tts.call_args is not None + assert mock_process_tts.call_args.kwargs["text"] == "There is someone at the door." + assert mock_process_tts.call_args.kwargs["language"] == service_data[ATTR_LANGUAGE] + assert mock_process_tts.call_args.kwargs["voice"] == "GadisNeural" + assert mock_process_tts.call_args.kwargs["output"] == "mp3" diff --git a/tests/components/cloudflare/__init__.py b/tests/components/cloudflare/__init__.py index 6b9e77dcb2a..ce9c6844f5a 100644 --- a/tests/components/cloudflare/__init__.py +++ b/tests/components/cloudflare/__init__.py @@ -1,4 +1,5 @@ """Tests for the Cloudflare integration.""" + from __future__ import annotations from unittest.mock import AsyncMock, patch diff --git a/tests/components/cloudflare/conftest.py b/tests/components/cloudflare/conftest.py index de0e1a85b77..81b52dd291d 100644 --- a/tests/components/cloudflare/conftest.py +++ b/tests/components/cloudflare/conftest.py @@ -1,4 +1,5 @@ """Define fixtures available for all tests.""" + from unittest.mock import patch import pytest diff --git a/tests/components/cloudflare/test_config_flow.py b/tests/components/cloudflare/test_config_flow.py index 21ee364eca3..142eab621e5 100644 --- a/tests/components/cloudflare/test_config_flow.py +++ b/tests/components/cloudflare/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Cloudflare config flow.""" + import pycfdns from homeassistant.components.cloudflare.const import CONF_RECORDS, DOMAIN diff --git a/tests/components/cloudflare/test_helpers.py b/tests/components/cloudflare/test_helpers.py index 74bf8420f8a..2d0546882dd 100644 --- a/tests/components/cloudflare/test_helpers.py +++ b/tests/components/cloudflare/test_helpers.py @@ -1,4 +1,5 @@ """Test Cloudflare integration helpers.""" + from homeassistant.components.cloudflare.helpers import get_zone_id diff --git a/tests/components/cloudflare/test_init.py b/tests/components/cloudflare/test_init.py index 02001f83995..2d66d3c8752 100644 --- a/tests/components/cloudflare/test_init.py +++ b/tests/components/cloudflare/test_init.py @@ -1,4 +1,5 @@ """Test the Cloudflare integration.""" + from datetime import timedelta from unittest.mock import patch @@ -38,7 +39,7 @@ async def test_unload_entry(hass: HomeAssistant, cfupdate) -> None: @pytest.mark.parametrize( "side_effect", - (pycfdns.ComunicationException(),), + [pycfdns.ComunicationException()], ) async def test_async_setup_raises_entry_not_ready( hass: HomeAssistant, cfupdate, side_effect @@ -124,10 +125,13 @@ async def test_integration_services_with_issue(hass: HomeAssistant, cfupdate) -> entry = await init_integration(hass) assert entry.state is ConfigEntryState.LOADED - with patch( - "homeassistant.components.cloudflare.async_detect_location_info", - return_value=None, - ), pytest.raises(HomeAssistantError, match="Could not get external IPv4 address"): + with ( + patch( + "homeassistant.components.cloudflare.async_detect_location_info", + return_value=None, + ), + pytest.raises(HomeAssistantError, match="Could not get external IPv4 address"), + ): await hass.services.async_call( DOMAIN, SERVICE_UPDATE_RECORDS, @@ -208,7 +212,7 @@ async def test_integration_update_interval( async_fire_time_changed( hass, dt_util.utcnow() + timedelta(minutes=DEFAULT_UPDATE_INTERVAL) ) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert len(instance.update_dns_record.mock_calls) == 2 assert "All target records are up to date" not in caplog.text @@ -216,12 +220,12 @@ async def test_integration_update_interval( async_fire_time_changed( hass, dt_util.utcnow() + timedelta(minutes=DEFAULT_UPDATE_INTERVAL) ) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert len(instance.update_dns_record.mock_calls) == 2 instance.list_dns_records.side_effect = pycfdns.ComunicationException() async_fire_time_changed( hass, dt_util.utcnow() + timedelta(minutes=DEFAULT_UPDATE_INTERVAL) ) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert len(instance.update_dns_record.mock_calls) == 2 diff --git a/tests/components/co2signal/__init__.py b/tests/components/co2signal/__init__.py index 65764d75fe4..394db24347b 100644 --- a/tests/components/co2signal/__init__.py +++ b/tests/components/co2signal/__init__.py @@ -1,4 +1,5 @@ """Tests for the CO2 Signal integration.""" + from aioelectricitymaps.models import ( CarbonIntensityData, CarbonIntensityResponse, diff --git a/tests/components/co2signal/conftest.py b/tests/components/co2signal/conftest.py index 8eb0116bc88..64972e6403f 100644 --- a/tests/components/co2signal/conftest.py +++ b/tests/components/co2signal/conftest.py @@ -1,4 +1,5 @@ """Fixtures for Electricity maps integration tests.""" + from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch @@ -17,12 +18,15 @@ from tests.components.co2signal import VALID_RESPONSE def mock_electricity_maps() -> Generator[None, MagicMock, None]: """Mock the ElectricityMaps client.""" - with patch( - "homeassistant.components.co2signal.ElectricityMaps", - autospec=True, - ) as electricity_maps, patch( - "homeassistant.components.co2signal.config_flow.ElectricityMaps", - new=electricity_maps, + with ( + patch( + "homeassistant.components.co2signal.ElectricityMaps", + autospec=True, + ) as electricity_maps, + patch( + "homeassistant.components.co2signal.config_flow.ElectricityMaps", + new=electricity_maps, + ), ): client = electricity_maps.return_value client.latest_carbon_intensity_by_coordinates.return_value = VALID_RESPONSE diff --git a/tests/components/co2signal/snapshots/test_sensor.ambr b/tests/components/co2signal/snapshots/test_sensor.ambr index 0d0ae2cedb5..3702521e4c3 100644 --- a/tests/components/co2signal/snapshots/test_sensor.ambr +++ b/tests/components/co2signal/snapshots/test_sensor.ambr @@ -46,6 +46,7 @@ 'context': , 'entity_id': 'sensor.electricity_maps_co2_intensity', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '45.9862319009581', }) @@ -97,6 +98,7 @@ 'context': , 'entity_id': 'sensor.electricity_maps_grid_fossil_fuel_percentage', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '5.4611827419371', }) diff --git a/tests/components/co2signal/test_config_flow.py b/tests/components/co2signal/test_config_flow.py index 518a747f852..e3bf9e3c818 100644 --- a/tests/components/co2signal/test_config_flow.py +++ b/tests/components/co2signal/test_config_flow.py @@ -1,4 +1,5 @@ """Test the CO2 Signal config flow.""" + from unittest.mock import AsyncMock, patch from aioelectricitymaps import ( diff --git a/tests/components/co2signal/test_sensor.py b/tests/components/co2signal/test_sensor.py index 4d663e1026b..d3e02023142 100644 --- a/tests/components/co2signal/test_sensor.py +++ b/tests/components/co2signal/test_sensor.py @@ -1,4 +1,5 @@ """Tests Electricity Maps sensor platform.""" + from datetime import timedelta from unittest.mock import AsyncMock diff --git a/tests/components/coinbase/common.py b/tests/components/coinbase/common.py index 0f8930dbeff..3421c4ce838 100644 --- a/tests/components/coinbase/common.py +++ b/tests/components/coinbase/common.py @@ -1,4 +1,5 @@ """Collection of helpers.""" + from homeassistant.components.coinbase.const import ( CONF_CURRENCIES, CONF_EXCHANGE_RATES, diff --git a/tests/components/coinbase/const.py b/tests/components/coinbase/const.py index 138b941c62c..dcd14555ca3 100644 --- a/tests/components/coinbase/const.py +++ b/tests/components/coinbase/const.py @@ -1,6 +1,5 @@ """Constants for testing the Coinbase integration.""" - GOOD_CURRENCY = "BTC" GOOD_CURRENCY_2 = "USD" GOOD_CURRENCY_3 = "EUR" diff --git a/tests/components/coinbase/test_config_flow.py b/tests/components/coinbase/test_config_flow.py index 592481df129..79b0115bc7c 100644 --- a/tests/components/coinbase/test_config_flow.py +++ b/tests/components/coinbase/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Coinbase config flow.""" + import logging from unittest.mock import patch @@ -34,18 +35,21 @@ async def test_form(hass: HomeAssistant) -> None: assert result["type"] == "form" assert result["errors"] == {} - with patch( - "coinbase.wallet.client.Client.get_current_user", - return_value=mock_get_current_user(), - ), patch( - "coinbase.wallet.client.Client.get_accounts", new=mocked_get_accounts - ), patch( - "coinbase.wallet.client.Client.get_exchange_rates", - return_value=mock_get_exchange_rates(), - ), patch( - "homeassistant.components.coinbase.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "coinbase.wallet.client.Client.get_current_user", + return_value=mock_get_current_user(), + ), + patch("coinbase.wallet.client.Client.get_accounts", new=mocked_get_accounts), + patch( + "coinbase.wallet.client.Client.get_exchange_rates", + return_value=mock_get_exchange_rates(), + ), + patch( + "homeassistant.components.coinbase.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -189,17 +193,20 @@ async def test_form_catch_all_exception(hass: HomeAssistant) -> None: async def test_option_form(hass: HomeAssistant) -> None: """Test we handle a good wallet currency option.""" - with patch( - "coinbase.wallet.client.Client.get_current_user", - return_value=mock_get_current_user(), - ), patch( - "coinbase.wallet.client.Client.get_accounts", new=mocked_get_accounts - ), patch( - "coinbase.wallet.client.Client.get_exchange_rates", - return_value=mock_get_exchange_rates(), - ), patch( - "homeassistant.components.coinbase.update_listener" - ) as mock_update_listener: + with ( + patch( + "coinbase.wallet.client.Client.get_current_user", + return_value=mock_get_current_user(), + ), + patch("coinbase.wallet.client.Client.get_accounts", new=mocked_get_accounts), + patch( + "coinbase.wallet.client.Client.get_exchange_rates", + return_value=mock_get_exchange_rates(), + ), + patch( + "homeassistant.components.coinbase.update_listener" + ) as mock_update_listener, + ): config_entry = await init_mock_coinbase(hass) await hass.async_block_till_done() result = await hass.config_entries.options.async_init(config_entry.entry_id) @@ -219,14 +226,16 @@ async def test_option_form(hass: HomeAssistant) -> None: async def test_form_bad_account_currency(hass: HomeAssistant) -> None: """Test we handle a bad currency option.""" - with patch( - "coinbase.wallet.client.Client.get_current_user", - return_value=mock_get_current_user(), - ), patch( - "coinbase.wallet.client.Client.get_accounts", new=mocked_get_accounts - ), patch( - "coinbase.wallet.client.Client.get_exchange_rates", - return_value=mock_get_exchange_rates(), + with ( + patch( + "coinbase.wallet.client.Client.get_current_user", + return_value=mock_get_current_user(), + ), + patch("coinbase.wallet.client.Client.get_accounts", new=mocked_get_accounts), + patch( + "coinbase.wallet.client.Client.get_exchange_rates", + return_value=mock_get_exchange_rates(), + ), ): config_entry = await init_mock_coinbase(hass) result = await hass.config_entries.options.async_init(config_entry.entry_id) @@ -246,14 +255,16 @@ async def test_form_bad_account_currency(hass: HomeAssistant) -> None: async def test_form_bad_exchange_rate(hass: HomeAssistant) -> None: """Test we handle a bad exchange rate.""" - with patch( - "coinbase.wallet.client.Client.get_current_user", - return_value=mock_get_current_user(), - ), patch( - "coinbase.wallet.client.Client.get_accounts", new=mocked_get_accounts - ), patch( - "coinbase.wallet.client.Client.get_exchange_rates", - return_value=mock_get_exchange_rates(), + with ( + patch( + "coinbase.wallet.client.Client.get_current_user", + return_value=mock_get_current_user(), + ), + patch("coinbase.wallet.client.Client.get_accounts", new=mocked_get_accounts), + patch( + "coinbase.wallet.client.Client.get_exchange_rates", + return_value=mock_get_exchange_rates(), + ), ): config_entry = await init_mock_coinbase(hass) result = await hass.config_entries.options.async_init(config_entry.entry_id) @@ -272,14 +283,16 @@ async def test_form_bad_exchange_rate(hass: HomeAssistant) -> None: async def test_option_catch_all_exception(hass: HomeAssistant) -> None: """Test we handle an unknown exception in the option flow.""" - with patch( - "coinbase.wallet.client.Client.get_current_user", - return_value=mock_get_current_user(), - ), patch( - "coinbase.wallet.client.Client.get_accounts", new=mocked_get_accounts - ), patch( - "coinbase.wallet.client.Client.get_exchange_rates", - return_value=mock_get_exchange_rates(), + with ( + patch( + "coinbase.wallet.client.Client.get_current_user", + return_value=mock_get_current_user(), + ), + patch("coinbase.wallet.client.Client.get_accounts", new=mocked_get_accounts), + patch( + "coinbase.wallet.client.Client.get_exchange_rates", + return_value=mock_get_exchange_rates(), + ), ): config_entry = await init_mock_coinbase(hass) result = await hass.config_entries.options.async_init(config_entry.entry_id) diff --git a/tests/components/coinbase/test_diagnostics.py b/tests/components/coinbase/test_diagnostics.py index 897722b32b4..e30bdef30b8 100644 --- a/tests/components/coinbase/test_diagnostics.py +++ b/tests/components/coinbase/test_diagnostics.py @@ -1,4 +1,5 @@ """Test the Coinbase diagnostics.""" + from unittest.mock import patch from syrupy import SnapshotAssertion @@ -23,14 +24,16 @@ async def test_entry_diagnostics( ) -> None: """Test we handle a and redact a diagnostics request.""" - with patch( - "coinbase.wallet.client.Client.get_current_user", - return_value=mock_get_current_user(), - ), patch( - "coinbase.wallet.client.Client.get_accounts", new=mocked_get_accounts - ), patch( - "coinbase.wallet.client.Client.get_exchange_rates", - return_value=mock_get_exchange_rates(), + with ( + patch( + "coinbase.wallet.client.Client.get_current_user", + return_value=mock_get_current_user(), + ), + patch("coinbase.wallet.client.Client.get_accounts", new=mocked_get_accounts), + patch( + "coinbase.wallet.client.Client.get_exchange_rates", + return_value=mock_get_exchange_rates(), + ), ): config_entry = await init_mock_coinbase(hass) await hass.async_block_till_done() diff --git a/tests/components/coinbase/test_init.py b/tests/components/coinbase/test_init.py index c518c71098d..5af762f557a 100644 --- a/tests/components/coinbase/test_init.py +++ b/tests/components/coinbase/test_init.py @@ -1,4 +1,5 @@ """Test the Coinbase integration.""" + from unittest.mock import patch from homeassistant import config_entries @@ -27,15 +28,19 @@ from .const import ( async def test_unload_entry(hass: HomeAssistant) -> None: """Test successful unload of entry.""" - with patch( - "coinbase.wallet.client.Client.get_current_user", - return_value=mock_get_current_user(), - ), patch( - "coinbase.wallet.client.Client.get_accounts", - new=mocked_get_accounts, - ), patch( - "coinbase.wallet.client.Client.get_exchange_rates", - return_value=mock_get_exchange_rates(), + with ( + patch( + "coinbase.wallet.client.Client.get_current_user", + return_value=mock_get_current_user(), + ), + patch( + "coinbase.wallet.client.Client.get_accounts", + new=mocked_get_accounts, + ), + patch( + "coinbase.wallet.client.Client.get_exchange_rates", + return_value=mock_get_exchange_rates(), + ), ): entry = await init_mock_coinbase(hass) @@ -54,14 +59,16 @@ async def test_option_updates( ) -> None: """Test handling option updates.""" - with patch( - "coinbase.wallet.client.Client.get_current_user", - return_value=mock_get_current_user(), - ), patch( - "coinbase.wallet.client.Client.get_accounts", new=mocked_get_accounts - ), patch( - "coinbase.wallet.client.Client.get_exchange_rates", - return_value=mock_get_exchange_rates(), + with ( + patch( + "coinbase.wallet.client.Client.get_current_user", + return_value=mock_get_current_user(), + ), + patch("coinbase.wallet.client.Client.get_accounts", new=mocked_get_accounts), + patch( + "coinbase.wallet.client.Client.get_exchange_rates", + return_value=mock_get_exchange_rates(), + ), ): config_entry = await init_mock_coinbase(hass) await hass.async_block_till_done() @@ -132,14 +139,16 @@ async def test_ignore_vaults_wallets( ) -> None: """Test vaults are ignored in wallet sensors.""" - with patch( - "coinbase.wallet.client.Client.get_current_user", - return_value=mock_get_current_user(), - ), patch( - "coinbase.wallet.client.Client.get_accounts", new=mocked_get_accounts - ), patch( - "coinbase.wallet.client.Client.get_exchange_rates", - return_value=mock_get_exchange_rates(), + with ( + patch( + "coinbase.wallet.client.Client.get_current_user", + return_value=mock_get_current_user(), + ), + patch("coinbase.wallet.client.Client.get_accounts", new=mocked_get_accounts), + patch( + "coinbase.wallet.client.Client.get_exchange_rates", + return_value=mock_get_exchange_rates(), + ), ): config_entry = await init_mock_coinbase(hass, currencies=[GOOD_CURRENCY]) await hass.async_block_till_done() diff --git a/tests/components/color_extractor/conftest.py b/tests/components/color_extractor/conftest.py index 299c8019f94..5c7d1136088 100644 --- a/tests/components/color_extractor/conftest.py +++ b/tests/components/color_extractor/conftest.py @@ -1,4 +1,5 @@ """Common fixtures for the Color extractor tests.""" + import pytest from homeassistant.components.color_extractor.const import DOMAIN diff --git a/tests/components/color_extractor/test_config_flow.py b/tests/components/color_extractor/test_config_flow.py index 9dc928da73f..844712f1938 100644 --- a/tests/components/color_extractor/test_config_flow.py +++ b/tests/components/color_extractor/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for the Color extractor config flow.""" + from unittest.mock import patch import pytest diff --git a/tests/components/color_extractor/test_init.py b/tests/components/color_extractor/test_init.py index b4874b575e8..cf4354db48d 100644 --- a/tests/components/color_extractor/test_init.py +++ b/tests/components/color_extractor/test_init.py @@ -1,4 +1,5 @@ """Test Color extractor component setup process.""" + from homeassistant.components.color_extractor import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant diff --git a/tests/components/color_extractor/test_service.py b/tests/components/color_extractor/test_service.py index 647d945f158..6ad4830c2c4 100644 --- a/tests/components/color_extractor/test_service.py +++ b/tests/components/color_extractor/test_service.py @@ -1,4 +1,5 @@ """Tests for color_extractor component service calls.""" + import base64 import io from typing import Any diff --git a/tests/components/comelit/test_config_flow.py b/tests/components/comelit/test_config_flow.py index 0a0dc04eae0..851e179dd95 100644 --- a/tests/components/comelit/test_config_flow.py +++ b/tests/components/comelit/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for Comelit SimpleHome config flow.""" + from typing import Any from unittest.mock import patch @@ -27,11 +28,15 @@ async def test_full_flow( hass: HomeAssistant, class_api: str, user_input: dict[str, Any] ) -> None: """Test starting a flow by user.""" - with patch( - f"aiocomelit.api.{class_api}.login", - ), patch( - f"aiocomelit.api.{class_api}.logout", - ), patch("homeassistant.components.comelit.async_setup_entry") as mock_setup_entry: + with ( + patch( + f"aiocomelit.api.{class_api}.login", + ), + patch( + f"aiocomelit.api.{class_api}.logout", + ), + patch("homeassistant.components.comelit.async_setup_entry") as mock_setup_entry, + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) @@ -68,13 +73,17 @@ async def test_exception_connection(hass: HomeAssistant, side_effect, error) -> assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == "user" - with patch( - "aiocomelit.api.ComeliteSerialBridgeApi.login", - side_effect=side_effect, - ), patch( - "aiocomelit.api.ComeliteSerialBridgeApi.logout", - ), patch( - "homeassistant.components.comelit.async_setup_entry", + with ( + patch( + "aiocomelit.api.ComeliteSerialBridgeApi.login", + side_effect=side_effect, + ), + patch( + "aiocomelit.api.ComeliteSerialBridgeApi.logout", + ), + patch( + "homeassistant.components.comelit.async_setup_entry", + ), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_BRIDGE_DATA @@ -92,13 +101,16 @@ async def test_reauth_successful(hass: HomeAssistant) -> None: mock_config = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_BRIDGE_DATA) mock_config.add_to_hass(hass) - with patch( - "aiocomelit.api.ComeliteSerialBridgeApi.login", - ), patch( - "aiocomelit.api.ComeliteSerialBridgeApi.logout", - ), patch("homeassistant.components.comelit.async_setup_entry"), patch( - "requests.get" - ) as mock_request_get: + with ( + patch( + "aiocomelit.api.ComeliteSerialBridgeApi.login", + ), + patch( + "aiocomelit.api.ComeliteSerialBridgeApi.logout", + ), + patch("homeassistant.components.comelit.async_setup_entry"), + patch("requests.get") as mock_request_get, + ): mock_request_get.return_value.status_code = 200 result = await hass.config_entries.flow.async_init( @@ -136,11 +148,13 @@ async def test_reauth_not_successful(hass: HomeAssistant, side_effect, error) -> mock_config = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_BRIDGE_DATA) mock_config.add_to_hass(hass) - with patch( - "aiocomelit.api.ComeliteSerialBridgeApi.login", side_effect=side_effect - ), patch( - "aiocomelit.api.ComeliteSerialBridgeApi.logout", - ), patch("homeassistant.components.comelit.async_setup_entry"): + with ( + patch("aiocomelit.api.ComeliteSerialBridgeApi.login", side_effect=side_effect), + patch( + "aiocomelit.api.ComeliteSerialBridgeApi.logout", + ), + patch("homeassistant.components.comelit.async_setup_entry"), + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_REAUTH, "entry_id": mock_config.entry_id}, diff --git a/tests/components/comfoconnect/test_sensor.py b/tests/components/comfoconnect/test_sensor.py index a2cfdd3d13f..cea5ed0122f 100644 --- a/tests/components/comfoconnect/test_sensor.py +++ b/tests/components/comfoconnect/test_sensor.py @@ -1,4 +1,5 @@ """Tests for the comfoconnect sensor platform.""" + # import json from unittest.mock import patch diff --git a/tests/components/command_line/__init__.py b/tests/components/command_line/__init__.py index d79b3e27db3..736ca68b43d 100644 --- a/tests/components/command_line/__init__.py +++ b/tests/components/command_line/__init__.py @@ -1 +1,30 @@ """Tests for command_line component.""" + +import asyncio +from contextlib import contextmanager +from unittest.mock import MagicMock, patch + + +@contextmanager +def mock_asyncio_subprocess_run( + response: bytes = b"", returncode: int = 0, exception: Exception = None +): + """Mock create_subprocess_shell.""" + + class MockProcess(asyncio.subprocess.Process): + @property + def returncode(self): + return returncode + + async def communicate(self): + if exception: + raise exception + return response, b"" + + mock_process = MockProcess(MagicMock(), MagicMock(), MagicMock()) + + with patch( + "homeassistant.components.command_line.utils.asyncio.create_subprocess_shell", + return_value=mock_process, + ) as mock: + yield mock diff --git a/tests/components/command_line/test_binary_sensor.py b/tests/components/command_line/test_binary_sensor.py index 7975660fda3..fd726ab77a4 100644 --- a/tests/components/command_line/test_binary_sensor.py +++ b/tests/components/command_line/test_binary_sensor.py @@ -1,4 +1,5 @@ """The tests for the Command line Binary sensor platform.""" + from __future__ import annotations import asyncio @@ -21,6 +22,8 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.util import dt as dt_util +from . import mock_asyncio_subprocess_run + from tests.common import async_fire_time_changed @@ -321,7 +324,7 @@ async def test_availability( hass.states.async_set("sensor.input1", "on") freezer.tick(timedelta(minutes=1)) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) entity_state = hass.states.get("binary_sensor.test") assert entity_state @@ -329,13 +332,10 @@ async def test_availability( hass.states.async_set("sensor.input1", "off") await hass.async_block_till_done() - with patch( - "homeassistant.components.command_line.utils.subprocess.check_output", - return_value=b"0", - ): + with mock_asyncio_subprocess_run(b"0"): freezer.tick(timedelta(minutes=1)) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) entity_state = hass.states.get("binary_sensor.test") assert entity_state diff --git a/tests/components/command_line/test_cover.py b/tests/components/command_line/test_cover.py index 901fc39eb34..8b98d8d1623 100644 --- a/tests/components/command_line/test_cover.py +++ b/tests/components/command_line/test_cover.py @@ -1,4 +1,5 @@ """The tests the cover command line platform.""" + from __future__ import annotations import asyncio @@ -30,16 +31,15 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er import homeassistant.util.dt as dt_util +from . import mock_asyncio_subprocess_run + from tests.common import async_fire_time_changed async def test_no_poll_when_cover_has_no_command_state(hass: HomeAssistant) -> None: """Test that the cover does not polls when there's no state command.""" - with patch( - "homeassistant.components.command_line.utils.subprocess.check_output", - return_value=b"50\n", - ) as check_output: + with mock_asyncio_subprocess_run(b"50\n") as mock_subprocess_run: assert await setup.async_setup_component( hass, COVER_DOMAIN, @@ -51,7 +51,7 @@ async def test_no_poll_when_cover_has_no_command_state(hass: HomeAssistant) -> N ) async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) await hass.async_block_till_done() - assert not check_output.called + assert not mock_subprocess_run.called @pytest.mark.parametrize( @@ -74,17 +74,13 @@ async def test_poll_when_cover_has_command_state( ) -> None: """Test that the cover polls when there's a state command.""" - with patch( - "homeassistant.components.command_line.utils.subprocess.check_output", - return_value=b"50\n", - ) as check_output: + with mock_asyncio_subprocess_run(b"50\n") as mock_subprocess_run: async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) await hass.async_block_till_done() - check_output.assert_called_once_with( + mock_subprocess_run.assert_called_once_with( "echo state", - shell=True, # noqa: S604 # shell by design - timeout=15, close_fds=False, + stdout=-1, ) @@ -269,7 +265,7 @@ async def test_updating_to_often( not in caplog.text ) async_fire_time_changed(hass, dt_util.now() + timedelta(seconds=11)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert called called.clear() @@ -286,7 +282,7 @@ async def test_updating_to_often( wait_till_event.set() # Finish processing update - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert called assert ( "Updating Command Line Cover Test took longer than the scheduled update interval" @@ -331,7 +327,7 @@ async def test_updating_manually( await hass.async_block_till_done() async_fire_time_changed(hass, dt_util.now() + timedelta(seconds=10)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert called called.clear() @@ -371,7 +367,7 @@ async def test_availability( hass.states.async_set("sensor.input1", "on") freezer.tick(timedelta(minutes=1)) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) entity_state = hass.states.get("cover.test") assert entity_state @@ -379,13 +375,10 @@ async def test_availability( hass.states.async_set("sensor.input1", "off") await hass.async_block_till_done() - with patch( - "homeassistant.components.command_line.utils.subprocess.check_output", - return_value=b"50\n", - ): + with mock_asyncio_subprocess_run(b"50\n"): freezer.tick(timedelta(minutes=1)) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) entity_state = hass.states.get("cover.test") assert entity_state diff --git a/tests/components/command_line/test_init.py b/tests/components/command_line/test_init.py index 53f985961f3..3fbd0e0f898 100644 --- a/tests/components/command_line/test_init.py +++ b/tests/components/command_line/test_init.py @@ -1,4 +1,5 @@ """Test Command line component setup process.""" + from __future__ import annotations from datetime import timedelta @@ -19,7 +20,7 @@ async def test_setup_config(hass: HomeAssistant, load_yaml_integration: None) -> """Test setup from yaml.""" async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=10)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state_binary_sensor = hass.states.get("binary_sensor.test") state_sensor = hass.states.get("sensor.test") diff --git a/tests/components/command_line/test_notify.py b/tests/components/command_line/test_notify.py index 96ad5ce2ee8..98bfb856bb8 100644 --- a/tests/components/command_line/test_notify.py +++ b/tests/components/command_line/test_notify.py @@ -1,4 +1,5 @@ """The tests for the command line notification platform.""" + from __future__ import annotations import os diff --git a/tests/components/command_line/test_sensor.py b/tests/components/command_line/test_sensor.py index 64227116cfe..26f97e37543 100644 --- a/tests/components/command_line/test_sensor.py +++ b/tests/components/command_line/test_sensor.py @@ -1,9 +1,9 @@ """The tests for the Command line sensor platform.""" + from __future__ import annotations import asyncio from datetime import timedelta -import subprocess from typing import Any from unittest.mock import patch @@ -22,6 +22,8 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.util import dt as dt_util +from . import mock_asyncio_subprocess_run + from tests.common import async_fire_time_changed @@ -106,7 +108,7 @@ async def test_template_render( hass, dt_util.utcnow() + timedelta(minutes=1), ) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) entity_state = hass.states.get("sensor.test") assert entity_state @@ -132,22 +134,18 @@ async def test_template_render_with_quote(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - with patch( - "homeassistant.components.command_line.utils.subprocess.check_output", - return_value=b"Works\n", - ) as check_output: + with mock_asyncio_subprocess_run(b"Works\n") as mock_subprocess_run: # Give time for template to load async_fire_time_changed( hass, dt_util.utcnow() + timedelta(minutes=1), ) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) - assert len(check_output.mock_calls) == 1 - check_output.assert_called_with( + assert len(mock_subprocess_run.mock_calls) == 1 + mock_subprocess_run.assert_called_with( 'echo "sensor_value" "3 4"', - shell=True, # noqa: S604 # shell by design - timeout=15, + stdout=-1, close_fds=False, ) @@ -679,10 +677,7 @@ async def test_template_not_error_when_data_is_none( ) -> None: """Test command sensor with template not logging error when data is None.""" - with patch( - "homeassistant.components.command_line.utils.subprocess.check_output", - side_effect=subprocess.CalledProcessError, - ): + with mock_asyncio_subprocess_run(returncode=1): await setup.async_setup_component( hass, DOMAIN, @@ -739,7 +734,7 @@ async def test_availability( hass.states.async_set("sensor.input1", "on") freezer.tick(timedelta(minutes=1)) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) entity_state = hass.states.get("sensor.test") assert entity_state @@ -747,13 +742,10 @@ async def test_availability( hass.states.async_set("sensor.input1", "off") await hass.async_block_till_done() - with patch( - "homeassistant.components.command_line.utils.subprocess.check_output", - return_value=b"January 17, 2022", - ): + with mock_asyncio_subprocess_run(b"January 17, 2022"): freezer.tick(timedelta(minutes=1)) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) entity_state = hass.states.get("sensor.test") assert entity_state diff --git a/tests/components/command_line/test_switch.py b/tests/components/command_line/test_switch.py index 47d9184f4f9..c464ded34fb 100644 --- a/tests/components/command_line/test_switch.py +++ b/tests/components/command_line/test_switch.py @@ -1,11 +1,11 @@ """The tests for the Command line switch platform.""" + from __future__ import annotations import asyncio from datetime import timedelta import json import os -import subprocess import tempfile from unittest.mock import patch @@ -32,6 +32,8 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er import homeassistant.util.dt as dt_util +from . import mock_asyncio_subprocess_run + from tests.common import async_fire_time_changed @@ -348,7 +350,7 @@ async def test_switch_command_state_fail( await hass.async_block_till_done() async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) entity_state = hass.states.get("switch.test") assert entity_state @@ -374,13 +376,7 @@ async def test_switch_command_state_code_exceptions( ) -> None: """Test that switch state code exceptions are handled correctly.""" - with patch( - "homeassistant.components.command_line.utils.subprocess.check_output", - side_effect=[ - subprocess.TimeoutExpired("cmd", 10), - subprocess.SubprocessError(), - ], - ) as check_output: + with mock_asyncio_subprocess_run(exception=asyncio.TimeoutError) as run: await setup.async_setup_component( hass, DOMAIN, @@ -401,12 +397,13 @@ async def test_switch_command_state_code_exceptions( async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) await hass.async_block_till_done() - assert check_output.called + assert run.called assert "Timeout for command" in caplog.text + with mock_asyncio_subprocess_run(returncode=127) as run: async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL * 2) await hass.async_block_till_done() - assert check_output.called + assert run.called assert "Error trying to exec command" in caplog.text @@ -415,13 +412,7 @@ async def test_switch_command_state_value_exceptions( ) -> None: """Test that switch state value exceptions are handled correctly.""" - with patch( - "homeassistant.components.command_line.utils.subprocess.check_output", - side_effect=[ - subprocess.TimeoutExpired("cmd", 10), - subprocess.SubprocessError(), - ], - ) as check_output: + with mock_asyncio_subprocess_run(exception=asyncio.TimeoutError) as run: await setup.async_setup_component( hass, DOMAIN, @@ -443,13 +434,14 @@ async def test_switch_command_state_value_exceptions( async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) await hass.async_block_till_done() - assert check_output.call_count == 1 + assert run.call_count == 1 assert "Timeout for command" in caplog.text + with mock_asyncio_subprocess_run(returncode=127) as run: async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL * 2) await hass.async_block_till_done() - assert check_output.call_count == 2 - assert "Error trying to exec command" in caplog.text + assert run.call_count == 1 + assert "Command failed (with return code 127)" in caplog.text async def test_unique_id( @@ -742,7 +734,7 @@ async def test_availability( hass.states.async_set("sensor.input1", "on") freezer.tick(timedelta(minutes=1)) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) entity_state = hass.states.get("switch.test") assert entity_state @@ -750,13 +742,10 @@ async def test_availability( hass.states.async_set("sensor.input1", "off") await hass.async_block_till_done() - with patch( - "homeassistant.components.command_line.utils.subprocess.check_output", - return_value=b"50\n", - ): + with mock_asyncio_subprocess_run(b"50\n"): freezer.tick(timedelta(minutes=1)) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) entity_state = hass.states.get("switch.test") assert entity_state diff --git a/tests/components/compensation/test_sensor.py b/tests/components/compensation/test_sensor.py index 5bc5a5e1c39..877a4f972a9 100644 --- a/tests/components/compensation/test_sensor.py +++ b/tests/components/compensation/test_sensor.py @@ -1,4 +1,5 @@ """The tests for the integration sensor platform.""" + import pytest from homeassistant.components.compensation.const import CONF_PRECISION, DOMAIN diff --git a/tests/components/config/conftest.py b/tests/components/config/conftest.py index e6f1532428e..ffd2f764922 100644 --- a/tests/components/config/conftest.py +++ b/tests/components/config/conftest.py @@ -1,4 +1,5 @@ """Test fixtures for the config integration.""" + from contextlib import contextmanager from copy import deepcopy import json @@ -50,18 +51,22 @@ def mock_config_store(data=None): _LOGGER.info("Reading data from configuration.yaml: %s", result) return result - with patch( - "homeassistant.components.config._read", - side_effect=mock_read, - autospec=True, - ), patch( - "homeassistant.components.config._write", - side_effect=mock_write, - autospec=True, - ), patch( - "homeassistant.config.async_hass_config_yaml", - side_effect=mock_async_hass_config_yaml, - autospec=True, + with ( + patch( + "homeassistant.components.config.view._read", + side_effect=mock_read, + autospec=True, + ), + patch( + "homeassistant.components.config.view._write", + side_effect=mock_write, + autospec=True, + ), + patch( + "homeassistant.config.async_hass_config_yaml", + side_effect=mock_async_hass_config_yaml, + autospec=True, + ), ): yield data diff --git a/tests/components/config/test_area_registry.py b/tests/components/config/test_area_registry.py index c4e275651ff..fb59725fd29 100644 --- a/tests/components/config/test_area_registry.py +++ b/tests/components/config/test_area_registry.py @@ -1,4 +1,5 @@ """Test area_registry API.""" + import pytest from pytest_unordered import unordered @@ -29,6 +30,8 @@ async def test_list_areas( aliases={"alias_1", "alias_2"}, icon="mdi:garage", picture="/image/example.png", + floor_id="first_floor", + labels={"label_1", "label_2"}, ) await client.send_json_auto_id({"type": "config/area_registry/list"}) @@ -38,14 +41,18 @@ async def test_list_areas( { "aliases": [], "area_id": area1.id, + "floor_id": None, "icon": None, + "labels": [], "name": "mock 1", "picture": None, }, { "aliases": unordered(["alias_1", "alias_2"]), "area_id": area2.id, + "floor_id": "first_floor", "icon": "mdi:garage", + "labels": unordered(["label_1", "label_2"]), "name": "mock 2", "picture": "/image/example.png", }, @@ -66,7 +73,9 @@ async def test_create_area( assert msg["result"] == { "aliases": [], "area_id": ANY, + "floor_id": None, "icon": None, + "labels": [], "name": "mock", "picture": None, } @@ -76,7 +85,9 @@ async def test_create_area( await client.send_json_auto_id( { "aliases": ["alias_1", "alias_2"], + "floor_id": "first_floor", "icon": "mdi:garage", + "labels": ["label_1", "label_2"], "name": "mock 2", "picture": "/image/example.png", "type": "config/area_registry/create", @@ -88,7 +99,9 @@ async def test_create_area( assert msg["result"] == { "aliases": unordered(["alias_1", "alias_2"]), "area_id": ANY, + "floor_id": "first_floor", "icon": "mdi:garage", + "labels": unordered(["label_1", "label_2"]), "name": "mock 2", "picture": "/image/example.png", } @@ -157,7 +170,9 @@ async def test_update_area( { "aliases": ["alias_1", "alias_2"], "area_id": area.id, + "floor_id": "first_floor", "icon": "mdi:garage", + "labels": ["label_1", "label_2"], "name": "mock 2", "picture": "/image/example.png", "type": "config/area_registry/update", @@ -169,7 +184,9 @@ async def test_update_area( assert msg["result"] == { "aliases": unordered(["alias_1", "alias_2"]), "area_id": area.id, + "floor_id": "first_floor", "icon": "mdi:garage", + "labels": unordered(["label_1", "label_2"]), "name": "mock 2", "picture": "/image/example.png", } @@ -179,7 +196,9 @@ async def test_update_area( { "aliases": ["alias_1", "alias_1"], "area_id": area.id, + "floor_id": None, "icon": None, + "labels": [], "picture": None, "type": "config/area_registry/update", } @@ -190,7 +209,9 @@ async def test_update_area( assert msg["result"] == { "aliases": ["alias_1"], "area_id": area.id, + "floor_id": None, "icon": None, + "labels": [], "name": "mock 2", "picture": None, } diff --git a/tests/components/config/test_auth.py b/tests/components/config/test_auth.py index 21ca81e74ac..b839d2de7a0 100644 --- a/tests/components/config/test_auth.py +++ b/tests/components/config/test_auth.py @@ -1,4 +1,5 @@ """Test config entries API.""" + import pytest from homeassistant.auth import models as auth_models diff --git a/tests/components/config/test_auth_provider_homeassistant.py b/tests/components/config/test_auth_provider_homeassistant.py index a8d5a1a23fd..d2631cd7a7c 100644 --- a/tests/components/config/test_auth_provider_homeassistant.py +++ b/tests/components/config/test_auth_provider_homeassistant.py @@ -1,4 +1,5 @@ """Test config entries API.""" + from typing import Any import pytest diff --git a/tests/components/config/test_automation.py b/tests/components/config/test_automation.py index 1a099c05b16..80f68b96fe1 100644 --- a/tests/components/config/test_automation.py +++ b/tests/components/config/test_automation.py @@ -1,4 +1,5 @@ """Test Automation config panel.""" + from http import HTTPStatus import json from typing import Any @@ -8,6 +9,7 @@ import pytest from homeassistant.bootstrap import async_setup_component from homeassistant.components import config +from homeassistant.components.config import automation from homeassistant.const import STATE_ON, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -25,7 +27,7 @@ def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None: async def setup_automation( hass, automation_config, - stub_blueprint_populate, # noqa: F811 + stub_blueprint_populate, ): """Set up automation integration.""" assert await async_setup_component( @@ -33,7 +35,7 @@ async def setup_automation( ) -@pytest.mark.parametrize("automation_config", ({},)) +@pytest.mark.parametrize("automation_config", [{}]) async def test_get_automation_config( hass: HomeAssistant, hass_client: ClientSessionGenerator, @@ -41,7 +43,7 @@ async def test_get_automation_config( setup_automation, ) -> None: """Test getting automation config.""" - with patch.object(config, "SECTIONS", ["automation"]): + with patch.object(config, "SECTIONS", [automation]): await async_setup_component(hass, "config", {}) client = await hass_client() @@ -56,7 +58,7 @@ async def test_get_automation_config( assert result == {"id": "moon"} -@pytest.mark.parametrize("automation_config", ({},)) +@pytest.mark.parametrize("automation_config", [{}]) async def test_update_automation_config( hass: HomeAssistant, hass_client: ClientSessionGenerator, @@ -64,7 +66,7 @@ async def test_update_automation_config( setup_automation, ) -> None: """Test updating automation config.""" - with patch.object(config, "SECTIONS", ["automation"]): + with patch.object(config, "SECTIONS", [automation]): await async_setup_component(hass, "config", {}) assert sorted(hass.states.async_entity_ids("automation")) == [] @@ -95,7 +97,7 @@ async def test_update_automation_config( assert new_data[1] == {"id": "moon", "trigger": [], "condition": [], "action": []} -@pytest.mark.parametrize("automation_config", ({},)) +@pytest.mark.parametrize("automation_config", [{}]) @pytest.mark.parametrize( ("updated_config", "validation_error"), [ @@ -153,7 +155,7 @@ async def test_update_automation_config_with_error( validation_error: str, ) -> None: """Test updating automation config with errors.""" - with patch.object(config, "SECTIONS", ["automation"]): + with patch.object(config, "SECTIONS", [automation]): await async_setup_component(hass, "config", {}) assert sorted(hass.states.async_entity_ids("automation")) == [] @@ -177,7 +179,7 @@ async def test_update_automation_config_with_error( assert validation_error not in caplog.text -@pytest.mark.parametrize("automation_config", ({},)) +@pytest.mark.parametrize("automation_config", [{}]) @pytest.mark.parametrize( ("updated_config", "validation_error"), [ @@ -206,7 +208,7 @@ async def test_update_automation_config_with_blueprint_substitution_error( validation_error: str, ) -> None: """Test updating automation config with errors.""" - with patch.object(config, "SECTIONS", ["automation"]): + with patch.object(config, "SECTIONS", [automation]): await async_setup_component(hass, "config", {}) assert sorted(hass.states.async_entity_ids("automation")) == [] @@ -234,7 +236,7 @@ async def test_update_automation_config_with_blueprint_substitution_error( assert validation_error not in caplog.text -@pytest.mark.parametrize("automation_config", ({},)) +@pytest.mark.parametrize("automation_config", [{}]) async def test_update_remove_key_automation_config( hass: HomeAssistant, hass_client: ClientSessionGenerator, @@ -242,7 +244,7 @@ async def test_update_remove_key_automation_config( setup_automation, ) -> None: """Test updating automation config while removing a key.""" - with patch.object(config, "SECTIONS", ["automation"]): + with patch.object(config, "SECTIONS", [automation]): await async_setup_component(hass, "config", {}) assert sorted(hass.states.async_entity_ids("automation")) == [] @@ -273,7 +275,7 @@ async def test_update_remove_key_automation_config( assert new_data[1] == {"id": "moon", "trigger": [], "condition": [], "action": []} -@pytest.mark.parametrize("automation_config", ({},)) +@pytest.mark.parametrize("automation_config", [{}]) async def test_bad_formatted_automations( hass: HomeAssistant, hass_client: ClientSessionGenerator, @@ -281,7 +283,7 @@ async def test_bad_formatted_automations( setup_automation, ) -> None: """Test that we handle automations without ID.""" - with patch.object(config, "SECTIONS", ["automation"]): + with patch.object(config, "SECTIONS", [automation]): await async_setup_component(hass, "config", {}) assert sorted(hass.states.async_entity_ids("automation")) == [] @@ -321,7 +323,7 @@ async def test_bad_formatted_automations( @pytest.mark.parametrize( "automation_config", - ( + [ [ { "id": "sun", @@ -334,7 +336,7 @@ async def test_bad_formatted_automations( "action": {"service": "test.automation"}, }, ], - ), + ], ) async def test_delete_automation( hass: HomeAssistant, @@ -347,7 +349,7 @@ async def test_delete_automation( assert len(entity_registry.entities) == 2 - with patch.object(config, "SECTIONS", ["automation"]): + with patch.object(config, "SECTIONS", [automation]): assert await async_setup_component(hass, "config", {}) assert sorted(hass.states.async_entity_ids("automation")) == [ @@ -376,7 +378,7 @@ async def test_delete_automation( assert len(entity_registry.entities) == 1 -@pytest.mark.parametrize("automation_config", ({},)) +@pytest.mark.parametrize("automation_config", [{}]) async def test_api_calls_require_admin( hass: HomeAssistant, hass_client: ClientSessionGenerator, @@ -385,7 +387,7 @@ async def test_api_calls_require_admin( setup_automation, ) -> None: """Test cloud APIs endpoints do not work as a normal user.""" - with patch.object(config, "SECTIONS", ["automation"]): + with patch.object(config, "SECTIONS", [automation]): await async_setup_component(hass, "config", {}) hass_config_store["automations.yaml"] = [{"id": "sun"}, {"id": "moon"}] diff --git a/tests/components/config/test_category_registry.py b/tests/components/config/test_category_registry.py new file mode 100644 index 00000000000..b4d171535b6 --- /dev/null +++ b/tests/components/config/test_category_registry.py @@ -0,0 +1,381 @@ +"""Test category registry API.""" + +import pytest + +from homeassistant.components.config import category_registry +from homeassistant.core import HomeAssistant +from homeassistant.helpers import category_registry as cr + +from tests.common import ANY +from tests.typing import MockHAClientWebSocket, WebSocketGenerator + + +@pytest.fixture(name="client") +async def client_fixture( + hass: HomeAssistant, hass_ws_client: WebSocketGenerator +) -> MockHAClientWebSocket: + """Fixture that can interact with the config manager API.""" + category_registry.async_setup(hass) + return await hass_ws_client(hass) + + +async def test_list_categories( + client: MockHAClientWebSocket, + category_registry: cr.CategoryRegistry, +) -> None: + """Test list entries.""" + category1 = category_registry.async_create( + scope="automation", + name="Energy saving", + icon="mdi:leaf", + ) + category2 = category_registry.async_create( + scope="automation", + name="Something else", + icon="mdi:home", + ) + category3 = category_registry.async_create( + scope="zone", + name="Grocery stores", + icon="mdi:store", + ) + + assert len(category_registry.categories) == 2 + assert len(category_registry.categories["automation"]) == 2 + assert len(category_registry.categories["zone"]) == 1 + + await client.send_json_auto_id( + {"type": "config/category_registry/list", "scope": "automation"} + ) + + msg = await client.receive_json() + + assert len(msg["result"]) == 2 + assert msg["result"][0] == { + "category_id": category1.category_id, + "name": "Energy saving", + "icon": "mdi:leaf", + } + assert msg["result"][1] == { + "category_id": category2.category_id, + "name": "Something else", + "icon": "mdi:home", + } + + await client.send_json_auto_id( + {"type": "config/category_registry/list", "scope": "zone"} + ) + + msg = await client.receive_json() + + assert len(msg["result"]) == 1 + assert msg["result"][0] == { + "category_id": category3.category_id, + "name": "Grocery stores", + "icon": "mdi:store", + } + + +async def test_create_category( + client: MockHAClientWebSocket, + category_registry: cr.CategoryRegistry, +) -> None: + """Test create entry.""" + await client.send_json_auto_id( + { + "type": "config/category_registry/create", + "scope": "automation", + "name": "Energy saving", + "icon": "mdi:leaf", + } + ) + + msg = await client.receive_json() + + assert len(category_registry.categories) == 1 + assert len(category_registry.categories["automation"]) == 1 + + assert msg["result"] == { + "icon": "mdi:leaf", + "category_id": ANY, + "name": "Energy saving", + } + + await client.send_json_auto_id( + { + "scope": "automation", + "name": "Something else", + "type": "config/category_registry/create", + } + ) + + msg = await client.receive_json() + + assert len(category_registry.categories) == 1 + assert len(category_registry.categories["automation"]) == 2 + + assert msg["result"] == { + "icon": None, + "category_id": ANY, + "name": "Something else", + } + + # Test adding the same one again in a different scope + await client.send_json_auto_id( + { + "type": "config/category_registry/create", + "scope": "script", + "name": "Energy saving", + "icon": "mdi:leaf", + } + ) + + msg = await client.receive_json() + + assert len(category_registry.categories) == 2 + assert len(category_registry.categories["automation"]) == 2 + assert len(category_registry.categories["script"]) == 1 + + assert msg["result"] == { + "icon": "mdi:leaf", + "category_id": ANY, + "name": "Energy saving", + } + + +async def test_create_category_with_name_already_in_use( + client: MockHAClientWebSocket, + category_registry: cr.CategoryRegistry, +) -> None: + """Test create entry that should fail.""" + category_registry.async_create( + scope="automation", + name="Energy saving", + ) + assert len(category_registry.categories) == 1 + assert len(category_registry.categories["automation"]) == 1 + + await client.send_json_auto_id( + { + "scope": "automation", + "name": "ENERGY SAVING", + "type": "config/category_registry/create", + } + ) + + msg = await client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == "invalid_info" + assert msg["error"]["message"] == "The name 'ENERGY SAVING' is already in use" + assert len(category_registry.categories) == 1 + assert len(category_registry.categories["automation"]) == 1 + + +async def test_delete_category( + client: MockHAClientWebSocket, + category_registry: cr.CategoryRegistry, +) -> None: + """Test delete entry.""" + category = category_registry.async_create( + scope="automation", + name="Energy saving", + icon="mdi:leaf", + ) + assert len(category_registry.categories) == 1 + assert len(category_registry.categories["automation"]) == 1 + + await client.send_json_auto_id( + { + "scope": "automation", + "category_id": category.category_id, + "type": "config/category_registry/delete", + } + ) + + msg = await client.receive_json() + + assert msg["success"] + assert len(category_registry.categories) == 1 + assert not category_registry.categories["automation"] + + +async def test_delete_non_existing_category( + client: MockHAClientWebSocket, + category_registry: cr.CategoryRegistry, +) -> None: + """Test delete entry that should fail.""" + category = category_registry.async_create( + scope="automation", + name="Energy saving", + icon="mdi:leaf", + ) + assert len(category_registry.categories) == 1 + assert len(category_registry.categories["automation"]) == 1 + + await client.send_json_auto_id( + { + "scope": "automation", + "category_id": "idkfa", + "type": "config/category_registry/delete", + } + ) + + msg = await client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == "invalid_info" + assert msg["error"]["message"] == "Category ID doesn't exist" + assert len(category_registry.categories) == 1 + assert len(category_registry.categories["automation"]) == 1 + + await client.send_json_auto_id( + { + "scope": "bullshizzle", + "category_id": category.category_id, + "type": "config/category_registry/delete", + } + ) + + msg = await client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == "invalid_info" + assert msg["error"]["message"] == "Category ID doesn't exist" + assert len(category_registry.categories) == 1 + assert len(category_registry.categories["automation"]) == 1 + + +async def test_update_category( + client: MockHAClientWebSocket, + category_registry: cr.CategoryRegistry, +) -> None: + """Test update entry.""" + category = category_registry.async_create( + scope="automation", + name="Energy saving", + ) + assert len(category_registry.categories) == 1 + assert len(category_registry.categories["automation"]) == 1 + + await client.send_json_auto_id( + { + "scope": "automation", + "category_id": category.category_id, + "name": "ENERGY SAVING", + "icon": "mdi:left", + "type": "config/category_registry/update", + } + ) + + msg = await client.receive_json() + + assert len(category_registry.categories) == 1 + assert len(category_registry.categories["automation"]) == 1 + assert msg["result"] == { + "icon": "mdi:left", + "category_id": category.category_id, + "name": "ENERGY SAVING", + } + + await client.send_json_auto_id( + { + "scope": "automation", + "category_id": category.category_id, + "name": "Energy saving", + "icon": None, + "type": "config/category_registry/update", + } + ) + + msg = await client.receive_json() + + assert len(category_registry.categories) == 1 + assert len(category_registry.categories["automation"]) == 1 + assert msg["result"] == { + "icon": None, + "category_id": category.category_id, + "name": "Energy saving", + } + + +async def test_update_with_name_already_in_use( + client: MockHAClientWebSocket, + category_registry: cr.CategoryRegistry, +) -> None: + """Test update entry.""" + category_registry.async_create( + scope="automation", + name="Energy saving", + ) + category = category_registry.async_create( + scope="automation", + name="Something else", + ) + assert len(category_registry.categories) == 1 + assert len(category_registry.categories["automation"]) == 2 + + await client.send_json_auto_id( + { + "scope": "automation", + "category_id": category.category_id, + "name": "ENERGY SAVING", + "type": "config/category_registry/update", + } + ) + + msg = await client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == "invalid_info" + assert msg["error"]["message"] == "The name 'ENERGY SAVING' is already in use" + assert len(category_registry.categories) == 1 + assert len(category_registry.categories["automation"]) == 2 + + +async def test_update_non_existing_category( + client: MockHAClientWebSocket, + category_registry: cr.CategoryRegistry, +) -> None: + """Test update entry that should fail.""" + category = category_registry.async_create( + scope="automation", + name="Energy saving", + icon="mdi:leaf", + ) + assert len(category_registry.categories) == 1 + assert len(category_registry.categories["automation"]) == 1 + + await client.send_json_auto_id( + { + "scope": "automation", + "category_id": "idkfa", + "name": "New category name", + "type": "config/category_registry/update", + } + ) + + msg = await client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == "invalid_info" + assert msg["error"]["message"] == "Category ID doesn't exist" + assert len(category_registry.categories) == 1 + assert len(category_registry.categories["automation"]) == 1 + + await client.send_json_auto_id( + { + "scope": "bullshizzle", + "category_id": category.category_id, + "name": "New category name", + "type": "config/category_registry/update", + } + ) + + msg = await client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == "invalid_info" + assert msg["error"]["message"] == "Category ID doesn't exist" + assert len(category_registry.categories) == 1 + assert len(category_registry.categories["automation"]) == 1 diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index 844b4bdb3b4..b4ef32b864c 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -1,8 +1,10 @@ """Test config entries API.""" + from collections import OrderedDict from http import HTTPStatus from unittest.mock import ANY, AsyncMock, patch +from aiohttp.test_utils import TestClient import pytest import voluptuous as vol @@ -39,7 +41,7 @@ def mock_test_component(hass): @pytest.fixture -async def client(hass, hass_client): +async def client(hass, hass_client) -> TestClient: """Fixture that can interact with the config manager API.""" await async_setup_component(hass, "http", {}) config_entries.async_setup(hass) @@ -121,6 +123,7 @@ async def test_get_entries(hass: HomeAssistant, client, clear_handlers) -> None: "title": "Test 1", "source": "bla", "state": core_ce.ConfigEntryState.NOT_LOADED.value, + "supports_reconfigure": False, "supports_options": True, "supports_remove_device": False, "supports_unload": True, @@ -134,6 +137,7 @@ async def test_get_entries(hass: HomeAssistant, client, clear_handlers) -> None: "title": "Test 2", "source": "bla2", "state": core_ce.ConfigEntryState.SETUP_ERROR.value, + "supports_reconfigure": False, "supports_options": False, "supports_remove_device": False, "supports_unload": False, @@ -147,6 +151,7 @@ async def test_get_entries(hass: HomeAssistant, client, clear_handlers) -> None: "title": "Test 3", "source": "bla3", "state": core_ce.ConfigEntryState.NOT_LOADED.value, + "supports_reconfigure": False, "supports_options": False, "supports_remove_device": False, "supports_unload": False, @@ -160,6 +165,7 @@ async def test_get_entries(hass: HomeAssistant, client, clear_handlers) -> None: "title": "Test 4", "source": "bla4", "state": core_ce.ConfigEntryState.NOT_LOADED.value, + "supports_reconfigure": False, "supports_options": False, "supports_remove_device": False, "supports_unload": False, @@ -173,6 +179,7 @@ async def test_get_entries(hass: HomeAssistant, client, clear_handlers) -> None: "title": "Test 5", "source": "bla5", "state": core_ce.ConfigEntryState.NOT_LOADED.value, + "supports_reconfigure": False, "supports_options": False, "supports_remove_device": False, "supports_unload": False, @@ -322,11 +329,11 @@ async def test_reload_entry_in_setup_retry( @pytest.mark.parametrize( ("type_filter", "result"), - ( + [ (None, {"hello", "another", "world"}), ("integration", {"hello", "another"}), ("helper", {"world"}), - ), + ], ) async def test_available_flows( hass: HomeAssistant, client, type_filter, result @@ -521,6 +528,7 @@ async def test_create_account( "entry_id": entries[0].entry_id, "source": core_ce.SOURCE_USER, "state": core_ce.ConfigEntryState.LOADED.value, + "supports_reconfigure": False, "supports_options": False, "supports_remove_device": False, "supports_unload": False, @@ -599,6 +607,7 @@ async def test_two_step_flow( "entry_id": entries[0].entry_id, "source": core_ce.SOURCE_USER, "state": core_ce.ConfigEntryState.LOADED.value, + "supports_reconfigure": False, "supports_options": False, "supports_remove_device": False, "supports_unload": False, @@ -941,10 +950,8 @@ async def test_two_step_options_flow(hass: HomeAssistant, client) -> None: "handler": "test1", "type": "create_entry", "title": "Enable disable", - "version": 1, "description": None, "description_placeholders": None, - "minor_version": 1, } @@ -1053,6 +1060,7 @@ async def test_get_single( "reason": None, "source": "user", "state": "loaded", + "supports_reconfigure": False, "supports_options": False, "supports_remove_device": False, "supports_unload": False, @@ -1387,6 +1395,7 @@ async def test_get_matching_entries_ws( "reason": None, "source": "bla", "state": "not_loaded", + "supports_reconfigure": False, "supports_options": False, "supports_remove_device": False, "supports_unload": False, @@ -1401,6 +1410,7 @@ async def test_get_matching_entries_ws( "reason": "Unsupported API", "source": "bla2", "state": "setup_error", + "supports_reconfigure": False, "supports_options": False, "supports_remove_device": False, "supports_unload": False, @@ -1415,6 +1425,7 @@ async def test_get_matching_entries_ws( "reason": None, "source": "bla3", "state": "not_loaded", + "supports_reconfigure": False, "supports_options": False, "supports_remove_device": False, "supports_unload": False, @@ -1429,6 +1440,7 @@ async def test_get_matching_entries_ws( "reason": None, "source": "bla4", "state": "not_loaded", + "supports_reconfigure": False, "supports_options": False, "supports_remove_device": False, "supports_unload": False, @@ -1443,6 +1455,7 @@ async def test_get_matching_entries_ws( "reason": None, "source": "bla5", "state": "not_loaded", + "supports_reconfigure": False, "supports_options": False, "supports_remove_device": False, "supports_unload": False, @@ -1468,6 +1481,7 @@ async def test_get_matching_entries_ws( "reason": None, "source": "bla", "state": "not_loaded", + "supports_reconfigure": False, "supports_options": False, "supports_remove_device": False, "supports_unload": False, @@ -1492,6 +1506,7 @@ async def test_get_matching_entries_ws( "reason": None, "source": "bla4", "state": "not_loaded", + "supports_reconfigure": False, "supports_options": False, "supports_remove_device": False, "supports_unload": False, @@ -1506,6 +1521,7 @@ async def test_get_matching_entries_ws( "reason": None, "source": "bla5", "state": "not_loaded", + "supports_reconfigure": False, "supports_options": False, "supports_remove_device": False, "supports_unload": False, @@ -1530,6 +1546,7 @@ async def test_get_matching_entries_ws( "reason": None, "source": "bla", "state": "not_loaded", + "supports_reconfigure": False, "supports_options": False, "supports_remove_device": False, "supports_unload": False, @@ -1544,6 +1561,7 @@ async def test_get_matching_entries_ws( "reason": None, "source": "bla3", "state": "not_loaded", + "supports_reconfigure": False, "supports_options": False, "supports_remove_device": False, "supports_unload": False, @@ -1574,6 +1592,7 @@ async def test_get_matching_entries_ws( "reason": None, "source": "bla", "state": "not_loaded", + "supports_reconfigure": False, "supports_options": False, "supports_remove_device": False, "supports_unload": False, @@ -1588,6 +1607,7 @@ async def test_get_matching_entries_ws( "reason": "Unsupported API", "source": "bla2", "state": "setup_error", + "supports_reconfigure": False, "supports_options": False, "supports_remove_device": False, "supports_unload": False, @@ -1602,6 +1622,7 @@ async def test_get_matching_entries_ws( "reason": None, "source": "bla3", "state": "not_loaded", + "supports_reconfigure": False, "supports_options": False, "supports_remove_device": False, "supports_unload": False, @@ -1616,6 +1637,7 @@ async def test_get_matching_entries_ws( "reason": None, "source": "bla4", "state": "not_loaded", + "supports_reconfigure": False, "supports_options": False, "supports_remove_device": False, "supports_unload": False, @@ -1630,6 +1652,7 @@ async def test_get_matching_entries_ws( "reason": None, "source": "bla5", "state": "not_loaded", + "supports_reconfigure": False, "supports_options": False, "supports_remove_device": False, "supports_unload": False, @@ -1728,6 +1751,7 @@ async def test_subscribe_entries_ws( "reason": None, "source": "bla", "state": "not_loaded", + "supports_reconfigure": False, "supports_options": False, "supports_remove_device": False, "supports_unload": False, @@ -1745,6 +1769,7 @@ async def test_subscribe_entries_ws( "reason": "Unsupported API", "source": "bla2", "state": "setup_error", + "supports_reconfigure": False, "supports_options": False, "supports_remove_device": False, "supports_unload": False, @@ -1762,6 +1787,7 @@ async def test_subscribe_entries_ws( "reason": None, "source": "bla3", "state": "not_loaded", + "supports_reconfigure": False, "supports_options": False, "supports_remove_device": False, "supports_unload": False, @@ -1783,6 +1809,7 @@ async def test_subscribe_entries_ws( "reason": None, "source": "bla", "state": "not_loaded", + "supports_reconfigure": False, "supports_options": False, "supports_remove_device": False, "supports_unload": False, @@ -1805,6 +1832,7 @@ async def test_subscribe_entries_ws( "reason": None, "source": "bla", "state": "not_loaded", + "supports_reconfigure": False, "supports_options": False, "supports_remove_device": False, "supports_unload": False, @@ -1827,6 +1855,7 @@ async def test_subscribe_entries_ws( "reason": None, "source": "bla", "state": "not_loaded", + "supports_reconfigure": False, "supports_options": False, "supports_remove_device": False, "supports_unload": False, @@ -1908,6 +1937,7 @@ async def test_subscribe_entries_ws_filtered( "reason": None, "source": "bla", "state": "not_loaded", + "supports_reconfigure": False, "supports_options": False, "supports_remove_device": False, "supports_unload": False, @@ -1925,6 +1955,7 @@ async def test_subscribe_entries_ws_filtered( "reason": None, "source": "bla3", "state": "not_loaded", + "supports_reconfigure": False, "supports_options": False, "supports_remove_device": False, "supports_unload": False, @@ -1948,6 +1979,7 @@ async def test_subscribe_entries_ws_filtered( "reason": None, "source": "bla", "state": "not_loaded", + "supports_reconfigure": False, "supports_options": False, "supports_remove_device": False, "supports_unload": False, @@ -1969,6 +2001,7 @@ async def test_subscribe_entries_ws_filtered( "reason": None, "source": "bla3", "state": "not_loaded", + "supports_reconfigure": False, "supports_options": False, "supports_remove_device": False, "supports_unload": False, @@ -1992,6 +2025,7 @@ async def test_subscribe_entries_ws_filtered( "reason": None, "source": "bla", "state": "not_loaded", + "supports_reconfigure": False, "supports_options": False, "supports_remove_device": False, "supports_unload": False, @@ -2014,6 +2048,7 @@ async def test_subscribe_entries_ws_filtered( "reason": None, "source": "bla", "state": "not_loaded", + "supports_reconfigure": False, "supports_options": False, "supports_remove_device": False, "supports_unload": False, @@ -2108,3 +2143,123 @@ async def test_flow_with_multiple_schema_errors_base( "latitude": "required key not provided", } } + + +async def test_supports_reconfigure( + hass: HomeAssistant, client, enable_custom_integrations: None +) -> None: + """Test a flow that support reconfigure step.""" + mock_platform(hass, "test.config_flow", None) + + mock_integration( + hass, MockModule("test", async_setup_entry=AsyncMock(return_value=True)) + ) + + class TestFlow(core_ce.ConfigFlow): + VERSION = 1 + + async def async_step_user(self, user_input=None): + return self.async_create_entry( + title="Test Entry", data={"secret": "account_token"} + ) + + async def async_step_reconfigure(self, user_input=None): + if user_input is None: + return self.async_show_form( + step_id="reconfigure", data_schema=vol.Schema({}) + ) + return self.async_create_entry( + title="Test Entry", data={"secret": "account_token"} + ) + + with patch.dict(HANDLERS, {"test": TestFlow}): + resp = await client.post( + "/api/config/config_entries/flow", + json={"handler": "test", "entry_id": "1"}, + ) + + assert resp.status == HTTPStatus.OK + + data = await resp.json() + flow_id = data.pop("flow_id") + + assert data == { + "type": "form", + "handler": "test", + "step_id": "reconfigure", + "data_schema": [], + "last_step": None, + "preview": None, + "description_placeholders": None, + "errors": None, + } + + with patch.dict(HANDLERS, {"test": TestFlow}): + resp = await client.post( + f"/api/config/config_entries/flow/{flow_id}", + json={}, + ) + assert resp.status == HTTPStatus.OK + + entries = hass.config_entries.async_entries("test") + assert len(entries) == 1 + + data = await resp.json() + data.pop("flow_id") + assert data == { + "handler": "test", + "title": "Test Entry", + "type": "create_entry", + "version": 1, + "result": { + "disabled_by": None, + "domain": "test", + "entry_id": entries[0].entry_id, + "source": core_ce.SOURCE_RECONFIGURE, + "state": core_ce.ConfigEntryState.LOADED.value, + "supports_reconfigure": True, + "supports_options": False, + "supports_remove_device": False, + "supports_unload": False, + "pref_disable_new_entities": False, + "pref_disable_polling": False, + "title": "Test Entry", + "reason": None, + }, + "description": None, + "description_placeholders": None, + "options": {}, + "minor_version": 1, + } + + +async def test_does_not_support_reconfigure( + hass: HomeAssistant, client: TestClient, enable_custom_integrations: None +) -> None: + """Test a flow that does not support reconfigure step.""" + mock_platform(hass, "test.config_flow", None) + + mock_integration( + hass, MockModule("test", async_setup_entry=AsyncMock(return_value=True)) + ) + + class TestFlow(core_ce.ConfigFlow): + VERSION = 1 + + async def async_step_user(self, user_input=None): + return self.async_create_entry( + title="Test Entry", data={"secret": "account_token"} + ) + + with patch.dict(HANDLERS, {"test": TestFlow}): + resp = await client.post( + "/api/config/config_entries/flow", + json={"handler": "test", "entry_id": "1"}, + ) + + assert resp.status == HTTPStatus.BAD_REQUEST + response = await resp.text() + assert ( + response + == '{"message":"Handler ConfigEntriesFlowManager doesn\'t support step reconfigure"}' + ) diff --git a/tests/components/config/test_core.py b/tests/components/config/test_core.py index bd21e5e7d30..da8a60ca6fd 100644 --- a/tests/components/config/test_core.py +++ b/tests/components/config/test_core.py @@ -1,4 +1,5 @@ """Test core config.""" + from http import HTTPStatus from unittest.mock import Mock, patch @@ -6,6 +7,7 @@ import pytest from homeassistant.bootstrap import async_setup_component from homeassistant.components import config +from homeassistant.components.config import core from homeassistant.components.websocket_api.const import TYPE_RESULT from homeassistant.const import ( CONF_UNIT_SYSTEM, @@ -23,7 +25,7 @@ from tests.typing import ClientSessionGenerator, WebSocketGenerator @pytest.fixture async def client(hass, hass_ws_client): """Fixture that can interact with the config manager API.""" - with patch.object(config, "SECTIONS", ["core"]): + with patch.object(config, "SECTIONS", [core]): assert await async_setup_component(hass, "config", {}) return await hass_ws_client(hass) @@ -32,7 +34,7 @@ async def test_validate_config_ok( hass: HomeAssistant, hass_client: ClientSessionGenerator ) -> None: """Test checking config.""" - with patch.object(config, "SECTIONS", ["core"]): + with patch.object(config, "SECTIONS", [core]): await async_setup_component(hass, "config", {}) client = await hass_client() @@ -95,7 +97,7 @@ async def test_validate_config_requires_admin( hass_read_only_access_token: str, ) -> None: """Test checking configuration does not work as a normal user.""" - with patch.object(config, "SECTIONS", ["core"]): + with patch.object(config, "SECTIONS", [core]): await async_setup_component(hass, "config", {}) client = await hass_client(hass_read_only_access_token) @@ -118,9 +120,12 @@ async def test_websocket_core_update(hass: HomeAssistant, client) -> None: assert hass.config.country != "SE" assert hass.config.language != "sv" - with patch("homeassistant.util.dt.set_default_time_zone") as mock_set_tz, patch( - "homeassistant.components.config.core.async_update_suggested_units" - ) as mock_update_sensor_units: + with ( + patch("homeassistant.util.dt.set_default_time_zone") as mock_set_tz, + patch( + "homeassistant.components.config.core.async_update_suggested_units" + ) as mock_update_sensor_units, + ): await client.send_json( { "id": 5, @@ -158,9 +163,12 @@ async def test_websocket_core_update(hass: HomeAssistant, client) -> None: assert len(mock_set_tz.mock_calls) == 1 assert mock_set_tz.mock_calls[0][1][0] == dt_util.get_time_zone("America/New_York") - with patch("homeassistant.util.dt.set_default_time_zone") as mock_set_tz, patch( - "homeassistant.components.config.core.async_update_suggested_units" - ) as mock_update_sensor_units: + with ( + patch("homeassistant.util.dt.set_default_time_zone") as mock_set_tz, + patch( + "homeassistant.components.config.core.async_update_suggested_units" + ) as mock_update_sensor_units, + ): await client.send_json( { "id": 6, @@ -180,7 +188,7 @@ async def test_websocket_core_update_not_admin( ) -> None: """Test core config fails for non admin.""" hass_admin_user.groups = [] - with patch.object(config, "SECTIONS", ["core"]): + with patch.object(config, "SECTIONS", [core]): await async_setup_component(hass, "config", {}) client = await hass_ws_client(hass) diff --git a/tests/components/config/test_device_registry.py b/tests/components/config/test_device_registry.py index 53746d2aa34..f88ae42b98a 100644 --- a/tests/components/config/test_device_registry.py +++ b/tests/components/config/test_device_registry.py @@ -1,5 +1,7 @@ """Test device_registry API.""" + import pytest +from pytest_unordered import unordered from homeassistant.components.config import device_registry from homeassistant.core import HomeAssistant @@ -63,6 +65,7 @@ async def test_list_devices( "entry_type": None, "hw_version": None, "identifiers": [["bridgeid", "0123"]], + "labels": [], "manufacturer": "manufacturer", "model": "model", "name_by_user": None, @@ -80,6 +83,7 @@ async def test_list_devices( "entry_type": dr.DeviceEntryType.SERVICE, "hw_version": None, "identifiers": [["bridgeid", "1234"]], + "labels": [], "manufacturer": "manufacturer", "model": "model", "name_by_user": None, @@ -110,6 +114,7 @@ async def test_list_devices( "hw_version": None, "id": device1.id, "identifiers": [["bridgeid", "0123"]], + "labels": [], "manufacturer": "manufacturer", "model": "model", "name_by_user": None, @@ -127,13 +132,13 @@ async def test_list_devices( @pytest.mark.parametrize( ("payload_key", "payload_value"), [ - ["area_id", "12345A"], - ["area_id", None], - ["disabled_by", dr.DeviceEntryDisabler.USER], - ["disabled_by", "user"], - ["disabled_by", None], - ["name_by_user", "Test Friendly Name"], - ["name_by_user", None], + ("area_id", "12345A"), + ("area_id", None), + ("disabled_by", dr.DeviceEntryDisabler.USER), + ("disabled_by", "user"), + ("disabled_by", None), + ("name_by_user", "Test Friendly Name"), + ("name_by_user", None), ], ) async def test_update_device( @@ -179,6 +184,45 @@ async def test_update_device( assert isinstance(device.disabled_by, (dr.DeviceEntryDisabler, type(None))) +async def test_update_device_labels( + hass: HomeAssistant, + client: MockHAClientWebSocket, + device_registry: dr.DeviceRegistry, +) -> None: + """Test update entry labels.""" + entry = MockConfigEntry(title=None) + entry.add_to_hass(hass) + device = device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + connections={("ethernet", "12:34:56:78:90:AB:CD:EF")}, + identifiers={("bridgeid", "0123")}, + manufacturer="manufacturer", + model="model", + ) + + assert not device.labels + + await client.send_json_auto_id( + { + "type": "config/device_registry/update", + "device_id": device.id, + "labels": ["label1", "label2"], + } + ) + + msg = await client.receive_json() + await hass.async_block_till_done() + assert len(device_registry.devices) == 1 + + device = device_registry.async_get_device( + identifiers={("bridgeid", "0123")}, + connections={("ethernet", "12:34:56:78:90:AB:CD:EF")}, + ) + + assert msg["result"]["labels"] == unordered(["label1", "label2"]) + assert device.labels == {"label1", "label2"} + + async def test_remove_config_entry_from_device( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, diff --git a/tests/components/config/test_entity_registry.py b/tests/components/config/test_entity_registry.py index 7069b22bf09..d61d9d7f892 100644 --- a/tests/components/config/test_entity_registry.py +++ b/tests/components/config/test_entity_registry.py @@ -1,4 +1,5 @@ """Test entity_registry API.""" + import pytest from pytest_unordered import unordered @@ -60,6 +61,7 @@ async def test_list_entities( assert msg["result"] == [ { "area_id": None, + "categories": {}, "config_entry_id": None, "device_id": None, "disabled_by": None, @@ -79,6 +81,7 @@ async def test_list_entities( }, { "area_id": None, + "categories": {}, "config_entry_id": None, "device_id": None, "disabled_by": None, @@ -125,6 +128,7 @@ async def test_list_entities( assert msg["result"] == [ { "area_id": None, + "categories": {}, "config_entry_id": None, "device_id": None, "disabled_by": None, @@ -348,6 +352,7 @@ async def test_get_entity(hass: HomeAssistant, client: MockHAClientWebSocket) -> "aliases": [], "area_id": None, "capabilities": None, + "categories": {}, "config_entry_id": None, "device_class": None, "device_id": None, @@ -381,6 +386,7 @@ async def test_get_entity(hass: HomeAssistant, client: MockHAClientWebSocket) -> "aliases": [], "area_id": None, "capabilities": None, + "categories": {}, "config_entry_id": None, "device_class": None, "device_id": None, @@ -439,6 +445,7 @@ async def test_get_entities(hass: HomeAssistant, client: MockHAClientWebSocket) "aliases": [], "area_id": None, "capabilities": None, + "categories": {}, "config_entry_id": None, "device_class": None, "device_id": None, @@ -463,6 +470,7 @@ async def test_get_entities(hass: HomeAssistant, client: MockHAClientWebSocket) "aliases": [], "area_id": None, "capabilities": None, + "categories": {}, "config_entry_id": None, "device_class": None, "device_id": None, @@ -513,16 +521,18 @@ async def test_update_entity( assert state.name == "before update" assert state.attributes[ATTR_ICON] == "icon:before update" - # UPDATE AREA, DEVICE_CLASS, HIDDEN_BY, ICON AND NAME + # Update area, categories, device_class, hidden_by, icon, labels & name await client.send_json_auto_id( { "type": "config/entity_registry/update", "entity_id": "test_domain.world", "aliases": ["alias_1", "alias_2"], "area_id": "mock-area-id", + "categories": {"scope1": "id", "scope2": "id"}, "device_class": "custom_device_class", "hidden_by": "user", # We exchange strings over the WS API, not enums "icon": "icon:after update", + "labels": ["label1", "label2"], "name": "after update", } ) @@ -534,6 +544,7 @@ async def test_update_entity( "aliases": unordered(["alias_1", "alias_2"]), "area_id": "mock-area-id", "capabilities": None, + "categories": {"scope1": "id", "scope2": "id"}, "config_entry_id": None, "device_class": "custom_device_class", "device_id": None, @@ -544,7 +555,7 @@ async def test_update_entity( "hidden_by": "user", # We exchange strings over the WS API, not enums "icon": "icon:after update", "id": ANY, - "labels": [], + "labels": unordered(["label1", "label2"]), "name": "after update", "options": {}, "original_device_class": None, @@ -560,7 +571,7 @@ async def test_update_entity( assert state.name == "after update" assert state.attributes[ATTR_ICON] == "icon:after update" - # UPDATE HIDDEN_BY TO ILLEGAL VALUE + # Update hidden_by to illegal value await client.send_json_auto_id( { "type": "config/entity_registry/update", @@ -574,7 +585,7 @@ async def test_update_entity( assert registry.entities["test_domain.world"].hidden_by is RegistryEntryHider.USER - # UPDATE DISABLED_BY TO USER + # Update disabled_by to user await client.send_json_auto_id( { "type": "config/entity_registry/update", @@ -591,7 +602,7 @@ async def test_update_entity( registry.entities["test_domain.world"].disabled_by is RegistryEntryDisabler.USER ) - # UPDATE DISABLED_BY TO NONE + # Update disabled_by to None await client.send_json_auto_id( { "type": "config/entity_registry/update", @@ -607,6 +618,7 @@ async def test_update_entity( "aliases": unordered(["alias_1", "alias_2"]), "area_id": "mock-area-id", "capabilities": None, + "categories": {"scope1": "id", "scope2": "id"}, "config_entry_id": None, "device_class": "custom_device_class", "device_id": None, @@ -617,7 +629,7 @@ async def test_update_entity( "hidden_by": "user", # We exchange strings over the WS API, not enums "icon": "icon:after update", "id": ANY, - "labels": [], + "labels": unordered(["label1", "label2"]), "name": "after update", "options": {}, "original_device_class": None, @@ -630,7 +642,7 @@ async def test_update_entity( "require_restart": True, } - # UPDATE ENTITY OPTION + # Update entity option await client.send_json_auto_id( { "type": "config/entity_registry/update", @@ -647,6 +659,7 @@ async def test_update_entity( "aliases": unordered(["alias_1", "alias_2"]), "area_id": "mock-area-id", "capabilities": None, + "categories": {"scope1": "id", "scope2": "id"}, "config_entry_id": None, "device_class": "custom_device_class", "device_id": None, @@ -657,7 +670,127 @@ async def test_update_entity( "hidden_by": "user", # We exchange strings over the WS API, not enums "icon": "icon:after update", "id": ANY, - "labels": [], + "labels": unordered(["label1", "label2"]), + "name": "after update", + "options": {"sensor": {"unit_of_measurement": "beard_second"}}, + "original_device_class": None, + "original_icon": None, + "original_name": None, + "platform": "test_platform", + "translation_key": None, + "unique_id": "1234", + }, + } + + # Add a category to the entity + await client.send_json_auto_id( + { + "type": "config/entity_registry/update", + "entity_id": "test_domain.world", + "categories": {"scope3": "id"}, + } + ) + + msg = await client.receive_json() + assert msg["success"] + + assert msg["result"] == { + "entity_entry": { + "aliases": unordered(["alias_1", "alias_2"]), + "area_id": "mock-area-id", + "capabilities": None, + "categories": {"scope1": "id", "scope2": "id", "scope3": "id"}, + "config_entry_id": None, + "device_class": "custom_device_class", + "device_id": None, + "disabled_by": None, + "entity_category": None, + "entity_id": "test_domain.world", + "has_entity_name": False, + "hidden_by": "user", # We exchange strings over the WS API, not enums + "icon": "icon:after update", + "id": ANY, + "labels": unordered(["label1", "label2"]), + "name": "after update", + "options": {"sensor": {"unit_of_measurement": "beard_second"}}, + "original_device_class": None, + "original_icon": None, + "original_name": None, + "platform": "test_platform", + "translation_key": None, + "unique_id": "1234", + }, + } + + # Move the entity to a different category + await client.send_json_auto_id( + { + "type": "config/entity_registry/update", + "entity_id": "test_domain.world", + "categories": {"scope3": "other_id"}, + } + ) + + msg = await client.receive_json() + assert msg["success"] + + assert msg["result"] == { + "entity_entry": { + "aliases": unordered(["alias_1", "alias_2"]), + "area_id": "mock-area-id", + "capabilities": None, + "categories": {"scope1": "id", "scope2": "id", "scope3": "other_id"}, + "config_entry_id": None, + "device_class": "custom_device_class", + "device_id": None, + "disabled_by": None, + "entity_category": None, + "entity_id": "test_domain.world", + "has_entity_name": False, + "hidden_by": "user", # We exchange strings over the WS API, not enums + "icon": "icon:after update", + "id": ANY, + "labels": unordered(["label1", "label2"]), + "name": "after update", + "options": {"sensor": {"unit_of_measurement": "beard_second"}}, + "original_device_class": None, + "original_icon": None, + "original_name": None, + "platform": "test_platform", + "translation_key": None, + "unique_id": "1234", + }, + } + + # Move the entity to a different category + await client.send_json_auto_id( + { + "type": "config/entity_registry/update", + "entity_id": "test_domain.world", + "categories": {"scope2": None}, + } + ) + + msg = await client.receive_json() + assert msg["success"] + + assert msg["result"] == { + "entity_entry": { + "aliases": unordered(["alias_1", "alias_2"]), + "area_id": "mock-area-id", + "capabilities": None, + "categories": {"scope1": "id", "scope3": "other_id"}, + "config_entry_id": None, + "device_class": "custom_device_class", + "device_id": None, + "disabled_by": None, + "entity_category": None, + "entity_id": "test_domain.world", + "has_entity_name": False, + "hidden_by": "user", # We exchange strings over the WS API, not enums + "icon": "icon:after update", + "id": ANY, + "labels": unordered(["label1", "label2"]), "name": "after update", "options": {"sensor": {"unit_of_measurement": "beard_second"}}, "original_device_class": None, @@ -701,6 +834,7 @@ async def test_update_entity_require_restart( "aliases": [], "area_id": None, "capabilities": None, + "categories": {}, "config_entry_id": config_entry.entry_id, "device_class": None, "device_id": None, @@ -814,6 +948,7 @@ async def test_update_entity_no_changes( "aliases": [], "area_id": None, "capabilities": None, + "categories": {}, "config_entry_id": None, "device_class": None, "device_id": None, @@ -903,6 +1038,7 @@ async def test_update_entity_id( "aliases": [], "area_id": None, "capabilities": None, + "categories": {}, "config_entry_id": None, "device_class": None, "device_id": None, diff --git a/tests/components/config/test_floor_registry.py b/tests/components/config/test_floor_registry.py index 6928a82898e..b4e3907bc4d 100644 --- a/tests/components/config/test_floor_registry.py +++ b/tests/components/config/test_floor_registry.py @@ -1,5 +1,7 @@ """Test floor registry API.""" + import pytest +from pytest_unordered import unordered from homeassistant.components.config import floor_registry from homeassistant.core import HomeAssistant @@ -25,6 +27,7 @@ async def test_list_floors( floor_registry.async_create("First floor") floor_registry.async_create( name="Second floor", + aliases={"top floor", "attic"}, icon="mdi:home-floor-2", level=2, ) @@ -37,12 +40,14 @@ async def test_list_floors( assert len(msg["result"]) == len(floor_registry.floors) assert msg["result"][0] == { + "aliases": [], "icon": None, "floor_id": "first_floor", "name": "First floor", - "level": 0, + "level": None, } assert msg["result"][1] == { + "aliases": unordered(["top floor", "attic"]), "icon": "mdi:home-floor-2", "floor_id": "second_floor", "name": "Second floor", @@ -63,16 +68,18 @@ async def test_create_floor( assert len(floor_registry.floors) == 1 assert msg["result"] == { + "aliases": [], "icon": None, "floor_id": "first_floor", "name": "First floor", - "level": 0, + "level": None, } await client.send_json_auto_id( { "name": "Second floor", "type": "config/floor_registry/create", + "aliases": ["top floor", "attic"], "icon": "mdi:home-floor-2", "level": 2, } @@ -82,6 +89,7 @@ async def test_create_floor( assert len(floor_registry.floors) == 2 assert msg["result"] == { + "aliases": unordered(["top floor", "attic"]), "icon": "mdi:home-floor-2", "floor_id": "second_floor", "name": "Second floor", @@ -164,6 +172,7 @@ async def test_update_floor( { "floor_id": floor.floor_id, "name": "Second floor", + "aliases": ["top floor", "attic"], "icon": "mdi:home-floor-2", "type": "config/floor_registry/update", "level": 2, @@ -174,6 +183,7 @@ async def test_update_floor( assert len(floor_registry.floors) == 1 assert msg["result"] == { + "aliases": unordered(["top floor", "attic"]), "icon": "mdi:home-floor-2", "floor_id": floor.floor_id, "name": "Second floor", @@ -184,8 +194,9 @@ async def test_update_floor( { "floor_id": floor.floor_id, "name": "First floor", + "aliases": [], "icon": None, - "level": 1, + "level": None, "type": "config/floor_registry/update", } ) @@ -194,10 +205,11 @@ async def test_update_floor( assert len(floor_registry.floors) == 1 assert msg["result"] == { + "aliases": [], "icon": None, "floor_id": floor.floor_id, "name": "First floor", - "level": 1, + "level": None, } diff --git a/tests/components/config/test_init.py b/tests/components/config/test_init.py index 4dd786edfd1..135cea28eff 100644 --- a/tests/components/config/test_init.py +++ b/tests/components/config/test_init.py @@ -1,4 +1,5 @@ """Test config init.""" + from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component diff --git a/tests/components/config/test_label_registry.py b/tests/components/config/test_label_registry.py index f65654462b9..040b3bfe28a 100644 --- a/tests/components/config/test_label_registry.py +++ b/tests/components/config/test_label_registry.py @@ -1,4 +1,5 @@ """Test label registry API.""" + import pytest from homeassistant.components.config import label_registry @@ -98,6 +99,27 @@ async def test_create_label( "name": "MOCKERY", } + await client.send_json_auto_id( + { + "name": "MAGIC", + "type": "config/label_registry/create", + "color": "indigo", + "description": "This is the third label", + "icon": "mdi:three", + } + ) + + msg = await client.receive_json() + + assert len(label_registry.labels) == 3 + assert msg["result"] == { + "color": "indigo", + "description": "This is the third label", + "icon": "mdi:three", + "label_id": "magic", + "name": "MAGIC", + } + async def test_create_label_with_name_already_in_use( client: MockHAClientWebSocket, @@ -209,6 +231,28 @@ async def test_update_label( "name": "UPDATED AGAIN", } + await client.send_json_auto_id( + { + "label_id": label.label_id, + "name": "UPDATED YET AGAIN", + "icon": None, + "color": "primary", + "description": None, + "type": "config/label_registry/update", + } + ) + + msg = await client.receive_json() + + assert len(label_registry.labels) == 1 + assert msg["result"] == { + "color": "primary", + "description": None, + "icon": None, + "label_id": "mock", + "name": "UPDATED YET AGAIN", + } + async def test_update_with_name_already_in_use( client: MockHAClientWebSocket, diff --git a/tests/components/config/test_scene.py b/tests/components/config/test_scene.py index 9fd596f7f91..6ca42e7f56d 100644 --- a/tests/components/config/test_scene.py +++ b/tests/components/config/test_scene.py @@ -1,4 +1,5 @@ """Test Automation config panel.""" + from http import HTTPStatus import json from unittest.mock import ANY, patch @@ -7,6 +8,7 @@ import pytest from homeassistant.bootstrap import async_setup_component from homeassistant.components import config +from homeassistant.components.config import scene from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -17,9 +19,10 @@ from tests.typing import ClientSessionGenerator async def setup_scene(hass, scene_config): """Set up scene integration.""" assert await async_setup_component(hass, "scene", {"scene": scene_config}) + await hass.async_block_till_done() -@pytest.mark.parametrize("scene_config", ({},)) +@pytest.mark.parametrize("scene_config", [{}]) async def test_create_scene( hass: HomeAssistant, hass_client: ClientSessionGenerator, @@ -27,7 +30,7 @@ async def test_create_scene( setup_scene, ) -> None: """Test creating a scene.""" - with patch.object(config, "SECTIONS", ["scene"]): + with patch.object(config, "SECTIONS", [scene]): await async_setup_component(hass, "config", {}) assert sorted(hass.states.async_entity_ids("scene")) == [] @@ -66,7 +69,7 @@ async def test_create_scene( ] -@pytest.mark.parametrize("scene_config", ({},)) +@pytest.mark.parametrize("scene_config", [{}]) async def test_update_scene( hass: HomeAssistant, hass_client: ClientSessionGenerator, @@ -74,7 +77,7 @@ async def test_update_scene( setup_scene, ) -> None: """Test updating a scene.""" - with patch.object(config, "SECTIONS", ["scene"]): + with patch.object(config, "SECTIONS", [scene]): await async_setup_component(hass, "config", {}) assert sorted(hass.states.async_entity_ids("scene")) == [] @@ -114,7 +117,7 @@ async def test_update_scene( ] -@pytest.mark.parametrize("scene_config", ({},)) +@pytest.mark.parametrize("scene_config", [{}]) async def test_bad_formatted_scene( hass: HomeAssistant, hass_client: ClientSessionGenerator, @@ -122,7 +125,7 @@ async def test_bad_formatted_scene( setup_scene, ) -> None: """Test that we handle scene without ID.""" - with patch.object(config, "SECTIONS", ["scene"]): + with patch.object(config, "SECTIONS", [scene]): await async_setup_component(hass, "config", {}) assert sorted(hass.states.async_entity_ids("scene")) == [] @@ -174,12 +177,12 @@ async def test_bad_formatted_scene( @pytest.mark.parametrize( "scene_config", - ( + [ [ {"id": "light_on", "name": "Light on", "entities": {}}, {"id": "light_off", "name": "Light off", "entities": {}}, ], - ), + ], ) async def test_delete_scene( hass: HomeAssistant, @@ -192,7 +195,7 @@ async def test_delete_scene( assert len(entity_registry.entities) == 2 - with patch.object(config, "SECTIONS", ["scene"]): + with patch.object(config, "SECTIONS", [scene]): assert await async_setup_component(hass, "config", {}) assert sorted(hass.states.async_entity_ids("scene")) == [ @@ -223,7 +226,7 @@ async def test_delete_scene( assert len(entity_registry.entities) == 1 -@pytest.mark.parametrize("scene_config", ({},)) +@pytest.mark.parametrize("scene_config", [{}]) async def test_api_calls_require_admin( hass: HomeAssistant, hass_client: ClientSessionGenerator, @@ -232,7 +235,7 @@ async def test_api_calls_require_admin( setup_scene, ) -> None: """Test scene APIs endpoints do not work as a normal user.""" - with patch.object(config, "SECTIONS", ["scene"]): + with patch.object(config, "SECTIONS", [scene]): await async_setup_component(hass, "config", {}) hass_config_store["scenes.yaml"] = [ diff --git a/tests/components/config/test_script.py b/tests/components/config/test_script.py index 7cf8cf5833e..3c1970a9bca 100644 --- a/tests/components/config/test_script.py +++ b/tests/components/config/test_script.py @@ -1,4 +1,5 @@ """Tests for config/script.""" + from http import HTTPStatus import json from typing import Any @@ -8,6 +9,7 @@ import pytest from homeassistant.bootstrap import async_setup_component from homeassistant.components import config +from homeassistant.components.config import script from homeassistant.const import STATE_OFF, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -22,17 +24,17 @@ def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None: @pytest.fixture(autouse=True) -async def setup_script(hass, script_config, stub_blueprint_populate): # noqa: F811 +async def setup_script(hass, script_config, stub_blueprint_populate): """Set up script integration.""" assert await async_setup_component(hass, "script", {"script": script_config}) -@pytest.mark.parametrize("script_config", ({},)) +@pytest.mark.parametrize("script_config", [{}]) async def test_get_script_config( hass: HomeAssistant, hass_client: ClientSessionGenerator, hass_config_store ) -> None: """Test getting script config.""" - with patch.object(config, "SECTIONS", ["script"]): + with patch.object(config, "SECTIONS", [script]): await async_setup_component(hass, "config", {}) client = await hass_client() @@ -50,12 +52,12 @@ async def test_get_script_config( assert result == {"alias": "Moon"} -@pytest.mark.parametrize("script_config", ({},)) +@pytest.mark.parametrize("script_config", [{}]) async def test_update_script_config( hass: HomeAssistant, hass_client: ClientSessionGenerator, hass_config_store ) -> None: """Test updating script config.""" - with patch.object(config, "SECTIONS", ["script"]): + with patch.object(config, "SECTIONS", [script]): await async_setup_component(hass, "config", {}) assert sorted(hass.states.async_entity_ids("script")) == [] @@ -86,12 +88,12 @@ async def test_update_script_config( assert new_data["moon"] == {"alias": "Moon updated", "sequence": []} -@pytest.mark.parametrize("script_config", ({},)) +@pytest.mark.parametrize("script_config", [{}]) async def test_invalid_object_id( hass: HomeAssistant, hass_client: ClientSessionGenerator, hass_config_store ) -> None: """Test creating a script with an invalid object_id.""" - with patch.object(config, "SECTIONS", ["script"]): + with patch.object(config, "SECTIONS", [script]): await async_setup_component(hass, "config", {}) assert sorted(hass.states.async_entity_ids("script")) == [] @@ -120,7 +122,7 @@ async def test_invalid_object_id( assert new_data == {} -@pytest.mark.parametrize("script_config", ({},)) +@pytest.mark.parametrize("script_config", [{}]) @pytest.mark.parametrize( ("updated_config", "validation_error"), [ @@ -156,7 +158,7 @@ async def test_update_script_config_with_error( validation_error: str, ) -> None: """Test updating script config with errors.""" - with patch.object(config, "SECTIONS", ["script"]): + with patch.object(config, "SECTIONS", [script]): await async_setup_component(hass, "config", {}) assert sorted(hass.states.async_entity_ids("script")) == [] @@ -180,7 +182,7 @@ async def test_update_script_config_with_error( assert validation_error not in caplog.text -@pytest.mark.parametrize("script_config", ({},)) +@pytest.mark.parametrize("script_config", [{}]) @pytest.mark.parametrize( ("updated_config", "validation_error"), [ @@ -207,7 +209,7 @@ async def test_update_script_config_with_blueprint_substitution_error( validation_error: str, ) -> None: """Test updating script config with errors.""" - with patch.object(config, "SECTIONS", ["script"]): + with patch.object(config, "SECTIONS", [script]): await async_setup_component(hass, "config", {}) assert sorted(hass.states.async_entity_ids("script")) == [] @@ -235,12 +237,12 @@ async def test_update_script_config_with_blueprint_substitution_error( assert validation_error not in caplog.text -@pytest.mark.parametrize("script_config", ({},)) +@pytest.mark.parametrize("script_config", [{}]) async def test_update_remove_key_script_config( hass: HomeAssistant, hass_client: ClientSessionGenerator, hass_config_store ) -> None: """Test updating script config while removing a key.""" - with patch.object(config, "SECTIONS", ["script"]): + with patch.object(config, "SECTIONS", [script]): await async_setup_component(hass, "config", {}) assert sorted(hass.states.async_entity_ids("script")) == [] @@ -273,12 +275,12 @@ async def test_update_remove_key_script_config( @pytest.mark.parametrize( "script_config", - ( + [ { "one": {"alias": "Light on", "sequence": []}, "two": {"alias": "Light off", "sequence": []}, }, - ), + ], ) async def test_delete_script( hass: HomeAssistant, @@ -287,7 +289,7 @@ async def test_delete_script( hass_config_store, ) -> None: """Test deleting a script.""" - with patch.object(config, "SECTIONS", ["script"]): + with patch.object(config, "SECTIONS", [script]): await async_setup_component(hass, "config", {}) assert sorted(hass.states.async_entity_ids("script")) == [ @@ -318,7 +320,7 @@ async def test_delete_script( assert len(entity_registry.entities) == 1 -@pytest.mark.parametrize("script_config", ({},)) +@pytest.mark.parametrize("script_config", [{}]) async def test_api_calls_require_admin( hass: HomeAssistant, hass_client: ClientSessionGenerator, @@ -326,7 +328,7 @@ async def test_api_calls_require_admin( hass_config_store, ) -> None: """Test script APIs endpoints do not work as a normal user.""" - with patch.object(config, "SECTIONS", ["script"]): + with patch.object(config, "SECTIONS", [script]): await async_setup_component(hass, "config", {}) hass_config_store["scripts.yaml"] = { diff --git a/tests/components/conftest.py b/tests/components/conftest.py index adf79a2ef96..d84fb3600ab 100644 --- a/tests/components/conftest.py +++ b/tests/components/conftest.py @@ -1,10 +1,17 @@ """Fixtures for component testing.""" + from collections.abc import Generator -from typing import Any +from typing import TYPE_CHECKING, Any from unittest.mock import MagicMock, patch import pytest +from homeassistant.const import STATE_OFF, STATE_ON + +if TYPE_CHECKING: + from tests.components.light.common import MockLight + from tests.components.sensor.common import MockSensor + @pytest.fixture(scope="session", autouse=True) def patch_zeroconf_multiple_catcher() -> Generator[None, None, None]: @@ -100,3 +107,23 @@ def prevent_ffmpeg_subprocess() -> Generator[None, None, None]: "homeassistant.components.ffmpeg.FFVersion.get_version", return_value="6.0" ): yield + + +@pytest.fixture +def mock_light_entities() -> list["MockLight"]: + """Return mocked light entities.""" + from tests.components.light.common import MockLight + + return [ + MockLight("Ceiling", STATE_ON), + MockLight("Ceiling", STATE_OFF), + MockLight(None, STATE_OFF), + ] + + +@pytest.fixture +def mock_sensor_entities() -> dict[str, "MockSensor"]: + """Return mocked sensor entities.""" + from tests.components.sensor.common import get_mock_sensor_entities + + return get_mock_sensor_entities() diff --git a/tests/components/control4/test_config_flow.py b/tests/components/control4/test_config_flow.py index 4909ead6c48..8ec6df063e5 100644 --- a/tests/components/control4/test_config_flow.py +++ b/tests/components/control4/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Control4 config flow.""" + from unittest.mock import AsyncMock, patch from pyControl4.account import C4Account @@ -52,16 +53,20 @@ async def test_form(hass: HomeAssistant) -> None: c4_account = _get_mock_c4_account() c4_director = _get_mock_c4_director() - with patch( - "homeassistant.components.control4.config_flow.C4Account", - return_value=c4_account, - ), patch( - "homeassistant.components.control4.config_flow.C4Director", - return_value=c4_director, - ), patch( - "homeassistant.components.control4.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.control4.config_flow.C4Account", + return_value=c4_account, + ), + patch( + "homeassistant.components.control4.config_flow.C4Director", + return_value=c4_director, + ), + patch( + "homeassistant.components.control4.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -135,12 +140,15 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch( - "homeassistant.components.control4.config_flow.Control4Validator.authenticate", - return_value=True, - ), patch( - "homeassistant.components.control4.config_flow.C4Director", - side_effect=Unauthorized("message"), + with ( + patch( + "homeassistant.components.control4.config_flow.Control4Validator.authenticate", + return_value=True, + ), + patch( + "homeassistant.components.control4.config_flow.C4Director", + side_effect=Unauthorized("message"), + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], diff --git a/tests/components/conversation/__init__.py b/tests/components/conversation/__init__.py index 648f8f33811..7209148e21f 100644 --- a/tests/components/conversation/__init__.py +++ b/tests/components/conversation/__init__.py @@ -1,4 +1,5 @@ """Tests for the conversation component.""" + from __future__ import annotations from typing import Literal diff --git a/tests/components/conversation/conftest.py b/tests/components/conversation/conftest.py index a08823255e9..cf6b4567228 100644 --- a/tests/components/conversation/conftest.py +++ b/tests/components/conversation/conftest.py @@ -1,4 +1,5 @@ """Conversation test helpers.""" + from unittest.mock import patch import pytest @@ -35,8 +36,9 @@ def mock_agent_support_all(hass): @pytest.fixture(autouse=True) def mock_shopping_list_io(): """Stub out the persistence.""" - with patch("homeassistant.components.shopping_list.ShoppingData.save"), patch( - "homeassistant.components.shopping_list.ShoppingData.async_load" + with ( + patch("homeassistant.components.shopping_list.ShoppingData.save"), + patch("homeassistant.components.shopping_list.ShoppingData.async_load"), ): yield diff --git a/tests/components/conversation/test_default_agent.py b/tests/components/conversation/test_default_agent.py index 4b4f9ade3eb..8f38459a8da 100644 --- a/tests/components/conversation/test_default_agent.py +++ b/tests/components/conversation/test_default_agent.py @@ -17,6 +17,7 @@ from homeassistant.helpers import ( device_registry as dr, entity, entity_registry as er, + floor_registry as fr, intent, ) from homeassistant.setup import async_setup_component @@ -331,6 +332,7 @@ async def test_device_area_context( # Create 2 lights in each area area_lights = defaultdict(list) + all_lights = [] for area in (area_kitchen, area_bedroom): for i in range(2): light_entity = entity_registry.async_get_or_create( @@ -345,6 +347,7 @@ async def test_device_area_context( attributes={ATTR_FRIENDLY_NAME: f"{area.name} light {i}"}, ) area_lights[area.id].append(light_entity) + all_lights.append(light_entity) # Create voice satellites in each area entry = MockConfigEntry() @@ -412,7 +415,7 @@ async def test_device_area_context( } turn_on_calls.clear() - # Turn off all lights in the area of the otherkj device + # Turn off all lights in the area of the other device result = await conversation.async_converse( hass, "turn lights off", @@ -436,16 +439,18 @@ async def test_device_area_context( } turn_off_calls.clear() - # Not providing a device id should not match + # Turn on/off all lights also works for command in ("on", "off"): result = await conversation.async_converse( hass, f"turn {command} all lights", None, Context(), None ) - assert result.response.response_type == intent.IntentResponseType.ERROR - assert ( - result.response.error_code - == intent.IntentResponseErrorCode.NO_VALID_TARGETS - ) + await hass.async_block_till_done() + assert result.response.response_type == intent.IntentResponseType.ACTION_DONE + + # All lights should have been targeted + assert {s.entity_id for s in result.response.matched_states} == { + e.entity_id for e in all_lights + } async def test_error_no_device(hass: HomeAssistant, init_components) -> None: @@ -476,6 +481,20 @@ async def test_error_no_area(hass: HomeAssistant, init_components) -> None: ) +async def test_error_no_floor(hass: HomeAssistant, init_components) -> None: + """Test error message when floor is missing.""" + result = await conversation.async_converse( + hass, "turn on all the lights on missing floor", None, Context(), None + ) + + assert result.response.response_type == intent.IntentResponseType.ERROR + assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS + assert ( + result.response.speech["plain"]["speech"] + == "Sorry, I am not aware of any floor called missing" + ) + + async def test_error_no_device_in_area( hass: HomeAssistant, init_components, area_registry: ar.AreaRegistry ) -> None: @@ -545,6 +564,48 @@ async def test_error_no_domain_in_area( ) +async def test_error_no_domain_in_floor( + hass: HomeAssistant, + init_components, + area_registry: ar.AreaRegistry, + floor_registry: fr.FloorRegistry, +) -> None: + """Test error message when no devices/entities for a domain exist on a floor.""" + floor_ground = floor_registry.async_create("ground") + area_kitchen = area_registry.async_get_or_create("kitchen_id") + area_kitchen = area_registry.async_update( + area_kitchen.id, name="kitchen", floor_id=floor_ground.floor_id + ) + result = await conversation.async_converse( + hass, "turn on all lights on the ground floor", None, Context(), None + ) + + assert result.response.response_type == intent.IntentResponseType.ERROR + assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS + assert ( + result.response.speech["plain"]["speech"] + == "Sorry, I am not aware of any light on the ground floor" + ) + + # Add a new floor/area to trigger registry event handlers + floor_upstairs = floor_registry.async_create("upstairs") + area_bedroom = area_registry.async_get_or_create("bedroom_id") + area_bedroom = area_registry.async_update( + area_bedroom.id, name="bedroom", floor_id=floor_upstairs.floor_id + ) + + result = await conversation.async_converse( + hass, "turn on all lights upstairs", None, Context(), None + ) + + assert result.response.response_type == intent.IntentResponseType.ERROR + assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS + assert ( + result.response.speech["plain"]["speech"] + == "Sorry, I am not aware of any light on the upstairs floor" + ) + + async def test_error_no_device_class(hass: HomeAssistant, init_components) -> None: """Test error message when no entities of a device class exist.""" @@ -732,7 +793,7 @@ async def test_no_states_matched_default_error( with patch( "homeassistant.components.conversation.default_agent.intent.async_handle", - side_effect=intent.NoStatesMatchedError(None, None, None, None), + side_effect=intent.NoStatesMatchedError(), ): result = await conversation.async_converse( hass, "turn on lights in the kitchen", None, Context(), None @@ -755,11 +816,16 @@ async def test_empty_aliases( area_registry: ar.AreaRegistry, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, + floor_registry: fr.FloorRegistry, ) -> None: """Test that empty aliases are not added to slot lists.""" + floor_1 = floor_registry.async_create("first floor", aliases={" "}) + area_kitchen = area_registry.async_get_or_create("kitchen_id") area_kitchen = area_registry.async_update(area_kitchen.id, name="kitchen") - area_kitchen = area_registry.async_update(area_kitchen.id, aliases={" "}) + area_kitchen = area_registry.async_update( + area_kitchen.id, aliases={" "}, floor_id=floor_1 + ) entry = MockConfigEntry() entry.add_to_hass(hass) @@ -795,7 +861,7 @@ async def test_empty_aliases( slot_lists = mock_recognize_all.call_args[0][2] # Slot lists should only contain non-empty text - assert slot_lists.keys() == {"area", "name"} + assert slot_lists.keys() == {"area", "name", "floor"} areas = slot_lists["area"] assert len(areas.values) == 1 assert areas.values[0].value_out == area_kitchen.id @@ -806,6 +872,11 @@ async def test_empty_aliases( assert names.values[0].value_out == kitchen_light.name assert names.values[0].text_in.text == kitchen_light.name + floors = slot_lists["floor"] + assert len(floors.values) == 1 + assert floors.values[0].value_out == floor_1.floor_id + assert floors.values[0].text_in.text == floor_1.name + async def test_all_domains_loaded(hass: HomeAssistant, init_components) -> None: """Test that sentences for all domains are always loaded.""" diff --git a/tests/components/conversation/test_default_agent_intents.py b/tests/components/conversation/test_default_agent_intents.py index edf7e17682e..9636ac07f63 100644 --- a/tests/components/conversation/test_default_agent_intents.py +++ b/tests/components/conversation/test_default_agent_intents.py @@ -1,16 +1,27 @@ """Test intents for the default agent.""" - import pytest -from homeassistant.components import conversation, cover, media_player, vacuum, valve +from homeassistant.components import ( + conversation, + cover, + light, + media_player, + vacuum, + valve, +) from homeassistant.components.cover import intent as cover_intent from homeassistant.components.homeassistant.exposed_entities import async_expose_entity from homeassistant.components.media_player import intent as media_player_intent from homeassistant.components.vacuum import intent as vaccum_intent from homeassistant.const import STATE_CLOSED from homeassistant.core import Context, HomeAssistant -from homeassistant.helpers import intent +from homeassistant.helpers import ( + area_registry as ar, + entity_registry as er, + floor_registry as fr, + intent, +) from homeassistant.setup import async_setup_component from tests.common import async_mock_service @@ -245,3 +256,92 @@ async def test_media_player_intents( "entity_id": entity_id, media_player.ATTR_MEDIA_VOLUME_LEVEL: 0.75, } + + +async def test_turn_floor_lights_on_off( + hass: HomeAssistant, + init_components, + entity_registry: er.EntityRegistry, + area_registry: ar.AreaRegistry, + floor_registry: fr.FloorRegistry, +) -> None: + """Test that we can turn lights on/off for an entire floor.""" + floor_ground = floor_registry.async_create("ground", aliases={"downstairs"}) + floor_upstairs = floor_registry.async_create("upstairs") + + # Kitchen and living room are on the ground floor + area_kitchen = area_registry.async_get_or_create("kitchen_id") + area_kitchen = area_registry.async_update( + area_kitchen.id, name="kitchen", floor_id=floor_ground.floor_id + ) + + area_living_room = area_registry.async_get_or_create("living_room_id") + area_living_room = area_registry.async_update( + area_living_room.id, name="living_room", floor_id=floor_ground.floor_id + ) + + # Bedroom is upstairs + area_bedroom = area_registry.async_get_or_create("bedroom_id") + area_bedroom = area_registry.async_update( + area_bedroom.id, name="bedroom", floor_id=floor_upstairs.floor_id + ) + + # One light per area + kitchen_light = entity_registry.async_get_or_create( + "light", "demo", "kitchen_light" + ) + kitchen_light = entity_registry.async_update_entity( + kitchen_light.entity_id, area_id=area_kitchen.id + ) + hass.states.async_set(kitchen_light.entity_id, "off") + + living_room_light = entity_registry.async_get_or_create( + "light", "demo", "living_room_light" + ) + living_room_light = entity_registry.async_update_entity( + living_room_light.entity_id, area_id=area_living_room.id + ) + hass.states.async_set(living_room_light.entity_id, "off") + + bedroom_light = entity_registry.async_get_or_create( + "light", "demo", "bedroom_light" + ) + bedroom_light = entity_registry.async_update_entity( + bedroom_light.entity_id, area_id=area_bedroom.id + ) + hass.states.async_set(bedroom_light.entity_id, "off") + + # Target by floor + on_calls = async_mock_service(hass, light.DOMAIN, light.SERVICE_TURN_ON) + result = await conversation.async_converse( + hass, "turn on all lights downstairs", None, Context(), None + ) + + assert len(on_calls) == 2 + assert result.response.response_type == intent.IntentResponseType.ACTION_DONE + assert {s.entity_id for s in result.response.matched_states} == { + kitchen_light.entity_id, + living_room_light.entity_id, + } + + on_calls.clear() + result = await conversation.async_converse( + hass, "upstairs lights on", None, Context(), None + ) + + assert len(on_calls) == 1 + assert result.response.response_type == intent.IntentResponseType.ACTION_DONE + assert {s.entity_id for s in result.response.matched_states} == { + bedroom_light.entity_id + } + + off_calls = async_mock_service(hass, light.DOMAIN, light.SERVICE_TURN_OFF) + result = await conversation.async_converse( + hass, "turn upstairs lights off", None, Context(), None + ) + + assert len(off_calls) == 1 + assert result.response.response_type == intent.IntentResponseType.ACTION_DONE + assert {s.entity_id for s in result.response.matched_states} == { + bedroom_light.entity_id + } diff --git a/tests/components/conversation/test_init.py b/tests/components/conversation/test_init.py index 61712761250..1ef8c8b30d7 100644 --- a/tests/components/conversation/test_init.py +++ b/tests/components/conversation/test_init.py @@ -1,4 +1,5 @@ """The tests for the Conversation component.""" + from http import HTTPStatus from typing import Any from unittest.mock import patch @@ -23,7 +24,13 @@ from homeassistant.setup import async_setup_component from . import expose_entity, expose_new -from tests.common import MockConfigEntry, MockUser, async_mock_service +from tests.common import ( + MockConfigEntry, + MockUser, + async_mock_service, + setup_test_component_platform, +) +from tests.components.light.common import MockLight from tests.typing import ClientSessionGenerator, WebSocketGenerator AGENT_ID_OPTIONS = [None, conversation.HOME_ASSISTANT_AGENT] @@ -255,7 +262,6 @@ async def test_http_processing_intent_entity_renamed( hass_client: ClientSessionGenerator, hass_admin_user: MockUser, entity_registry: er.EntityRegistry, - enable_custom_integrations: None, snapshot: SnapshotAssertion, ) -> None: """Test processing intent via HTTP API with entities renamed later. @@ -263,13 +269,11 @@ async def test_http_processing_intent_entity_renamed( We want to ensure that renaming an entity later busts the cache so that the new name is used. """ - platform = getattr(hass.components, "test.light") - platform.init(empty=True) - - entity = platform.MockLight("kitchen light", "on") + entity = MockLight("kitchen light", "on") entity._attr_unique_id = "1234" entity.entity_id = "light.kitchen" - platform.ENTITIES.append(entity) + setup_test_component_platform(hass, LIGHT_DOMAIN, [entity]) + assert await async_setup_component( hass, LIGHT_DOMAIN, @@ -346,7 +350,6 @@ async def test_http_processing_intent_entity_exposed( hass_client: ClientSessionGenerator, hass_admin_user: MockUser, entity_registry: er.EntityRegistry, - enable_custom_integrations: None, snapshot: SnapshotAssertion, ) -> None: """Test processing intent via HTTP API with manual expose. @@ -354,13 +357,11 @@ async def test_http_processing_intent_entity_exposed( We want to ensure that manually exposing an entity later busts the cache so that the new setting is used. """ - platform = getattr(hass.components, "test.light") - platform.init(empty=True) - - entity = platform.MockLight("kitchen light", "on") + entity = MockLight("kitchen light", "on") entity._attr_unique_id = "1234" entity.entity_id = "light.kitchen" - platform.ENTITIES.append(entity) + setup_test_component_platform(hass, LIGHT_DOMAIN, [entity]) + assert await async_setup_component( hass, LIGHT_DOMAIN, @@ -451,20 +452,17 @@ async def test_http_processing_intent_conversion_not_expose_new( hass_client: ClientSessionGenerator, hass_admin_user: MockUser, entity_registry: er.EntityRegistry, - enable_custom_integrations: None, snapshot: SnapshotAssertion, ) -> None: """Test processing intent via HTTP API when not exposing new entities.""" # Disable exposing new entities to the default agent expose_new(hass, False) - platform = getattr(hass.components, "test.light") - platform.init(empty=True) - - entity = platform.MockLight("kitchen light", "on") + entity = MockLight("kitchen light", "on") entity._attr_unique_id = "1234" entity.entity_id = "light.kitchen" - platform.ENTITIES.append(entity) + setup_test_component_platform(hass, LIGHT_DOMAIN, [entity]) + assert await async_setup_component( hass, LIGHT_DOMAIN, @@ -501,8 +499,8 @@ async def test_http_processing_intent_conversion_not_expose_new( @pytest.mark.parametrize("agent_id", AGENT_ID_OPTIONS) -@pytest.mark.parametrize("sentence", ("turn on kitchen", "turn kitchen on")) -@pytest.mark.parametrize("conversation_id", ("my_new_conversation", None)) +@pytest.mark.parametrize("sentence", ["turn on kitchen", "turn kitchen on"]) +@pytest.mark.parametrize("conversation_id", ["my_new_conversation", None]) async def test_turn_on_intent( hass: HomeAssistant, init_components, conversation_id, sentence, agent_id, snapshot ) -> None: @@ -534,9 +532,12 @@ async def test_turn_on_intent( async def test_service_fails(hass: HomeAssistant, init_components) -> None: """Test calling the turn on intent.""" - with pytest.raises(HomeAssistantError), patch( - "homeassistant.components.conversation.async_converse", - side_effect=intent.IntentHandleError, + with ( + pytest.raises(HomeAssistantError), + patch( + "homeassistant.components.conversation.async_converse", + side_effect=intent.IntentHandleError, + ), ): await hass.services.async_call( "conversation", @@ -546,7 +547,7 @@ async def test_service_fails(hass: HomeAssistant, init_components) -> None: ) -@pytest.mark.parametrize("sentence", ("turn off kitchen", "turn kitchen off")) +@pytest.mark.parametrize("sentence", ["turn off kitchen", "turn kitchen off"]) async def test_turn_off_intent(hass: HomeAssistant, init_components, sentence) -> None: """Test calling the turn on intent.""" hass.states.async_set("light.kitchen", "on") @@ -597,7 +598,7 @@ async def test_http_api_handle_failure( # Raise an error during intent handling def async_handle_error(*args, **kwargs): - raise intent.IntentHandleError() + raise intent.IntentHandleError with patch("homeassistant.helpers.intent.async_handle", new=async_handle_error): resp = await client.post( @@ -625,7 +626,7 @@ async def test_http_api_unexpected_failure( # Raise an "unexpected" error during intent handling def async_handle_error(*args, **kwargs): - raise intent.IntentUnexpectedError() + raise intent.IntentUnexpectedError with patch("homeassistant.helpers.intent.async_handle", new=async_handle_error): resp = await client.post( diff --git a/tests/components/conversation/test_trigger.py b/tests/components/conversation/test_trigger.py index f95976a4638..221789b49e0 100644 --- a/tests/components/conversation/test_trigger.py +++ b/tests/components/conversation/test_trigger.py @@ -5,7 +5,8 @@ import logging import pytest import voluptuous as vol -from homeassistant.core import HomeAssistant +from homeassistant.components import conversation +from homeassistant.core import Context, HomeAssistant from homeassistant.helpers import trigger from homeassistant.setup import async_setup_component @@ -51,9 +52,7 @@ async def test_if_fires_on_event(hass: HomeAssistant, calls, setup_comp) -> None service_response = await hass.services.async_call( "conversation", "process", - { - "text": "Ha ha ha", - }, + {"text": "Ha ha ha"}, blocking=True, return_response=True, ) @@ -69,6 +68,7 @@ async def test_if_fires_on_event(hass: HomeAssistant, calls, setup_comp) -> None "sentence": "Ha ha ha", "slots": {}, "details": {}, + "device_id": None, } @@ -160,6 +160,7 @@ async def test_response_same_sentence(hass: HomeAssistant, calls, setup_comp) -> "sentence": "test sentence", "slots": {}, "details": {}, + "device_id": None, } @@ -311,6 +312,7 @@ async def test_same_trigger_multiple_sentences( "sentence": "hello", "slots": {}, "details": {}, + "device_id": None, } @@ -488,4 +490,40 @@ async def test_wildcards(hass: HomeAssistant, calls, setup_comp) -> None: "value": "the beatles", }, }, + "device_id": None, } + + +async def test_trigger_with_device_id(hass: HomeAssistant) -> None: + """Test that a trigger receives a device_id.""" + assert await async_setup_component(hass, "homeassistant", {}) + assert await async_setup_component(hass, "conversation", {}) + assert await async_setup_component( + hass, + "automation", + { + "automation": { + "trigger": { + "platform": "conversation", + "command": ["test sentence"], + }, + "action": { + "set_conversation_response": "{{ trigger.device_id }}", + }, + } + }, + ) + + agent = await conversation._get_agent_manager(hass).async_get_agent() + assert isinstance(agent, conversation.DefaultAgent) + + result = await agent.async_process( + conversation.ConversationInput( + text="test sentence", + context=Context(), + conversation_id=None, + device_id="my_device", + language=hass.config.language, + ) + ) + assert result.response.speech["plain"]["speech"] == "my_device" diff --git a/tests/components/conversation/test_util.py b/tests/components/conversation/test_util.py index 8b26c61a651..72a334232c1 100644 --- a/tests/components/conversation/test_util.py +++ b/tests/components/conversation/test_util.py @@ -1,4 +1,5 @@ """Test the conversation utils.""" + from homeassistant.components.conversation.util import create_matcher diff --git a/tests/components/coolmaster/conftest.py b/tests/components/coolmaster/conftest.py index fadce747d6a..7ddf1fd5942 100644 --- a/tests/components/coolmaster/conftest.py +++ b/tests/components/coolmaster/conftest.py @@ -1,4 +1,5 @@ """Fixtures for the Coolmaster integration.""" + from __future__ import annotations import copy @@ -71,7 +72,7 @@ class CoolMasterNetUnitMock: async def set_swing(self, value: str | None) -> CoolMasterNetUnitMock: """Set the swing mode.""" if value == "": - raise ValueError() + raise ValueError self._attributes["swing"] = value return CoolMasterNetUnitMock(self.unit_id, self._attributes) diff --git a/tests/components/coolmaster/test_binary_sensor.py b/tests/components/coolmaster/test_binary_sensor.py index 2f5c8c5f1be..77e26ed803c 100644 --- a/tests/components/coolmaster/test_binary_sensor.py +++ b/tests/components/coolmaster/test_binary_sensor.py @@ -1,4 +1,5 @@ """The test for the Coolmaster binary sensor platform.""" + from __future__ import annotations from homeassistant.config_entries import ConfigEntry diff --git a/tests/components/coolmaster/test_button.py b/tests/components/coolmaster/test_button.py index 67461f63087..5d2ea07649e 100644 --- a/tests/components/coolmaster/test_button.py +++ b/tests/components/coolmaster/test_button.py @@ -1,4 +1,5 @@ """The test for the Coolmaster button platform.""" + from __future__ import annotations from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS diff --git a/tests/components/coolmaster/test_climate.py b/tests/components/coolmaster/test_climate.py index 0e306faa8ab..ddc4b5b53d6 100644 --- a/tests/components/coolmaster/test_climate.py +++ b/tests/components/coolmaster/test_climate.py @@ -1,4 +1,5 @@ """The test for the Coolmaster climate platform.""" + from __future__ import annotations from pycoolmasternet_async import SWING_MODES diff --git a/tests/components/coolmaster/test_config_flow.py b/tests/components/coolmaster/test_config_flow.py index 47a31538560..ef7828e126d 100644 --- a/tests/components/coolmaster/test_config_flow.py +++ b/tests/components/coolmaster/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Coolmaster config flow.""" + from unittest.mock import patch from homeassistant import config_entries @@ -23,13 +24,16 @@ async def test_form(hass: HomeAssistant) -> None: assert result["type"] == "form" assert result["errors"] is None - with patch( - "homeassistant.components.coolmaster.config_flow.CoolMasterNet.status", - return_value={"test_id": "test_unit"}, - ), patch( - "homeassistant.components.coolmaster.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.coolmaster.config_flow.CoolMasterNet.status", + return_value={"test_id": "test_unit"}, + ), + patch( + "homeassistant.components.coolmaster.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], _flow_data() ) diff --git a/tests/components/coolmaster/test_init.py b/tests/components/coolmaster/test_init.py index ce6dd8f60a4..4a90d0d9276 100644 --- a/tests/components/coolmaster/test_init.py +++ b/tests/components/coolmaster/test_init.py @@ -1,4 +1,5 @@ """The test for the Coolmaster integration.""" + from homeassistant.components.coolmaster.const import DOMAIN from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.core import HomeAssistant diff --git a/tests/components/coolmaster/test_sensor.py b/tests/components/coolmaster/test_sensor.py index 3072106ec62..388edb5096b 100644 --- a/tests/components/coolmaster/test_sensor.py +++ b/tests/components/coolmaster/test_sensor.py @@ -1,4 +1,5 @@ """The test for the Coolmaster sensor platform.""" + from __future__ import annotations from homeassistant.config_entries import ConfigEntry diff --git a/tests/components/counter/common.py b/tests/components/counter/common.py index 5f47e4faa77..b5156c1a432 100644 --- a/tests/components/counter/common.py +++ b/tests/components/counter/common.py @@ -3,6 +3,7 @@ All containing methods are legacy helpers that should not be used by new components. Instead call the service directly. """ + from homeassistant.components.counter import ( DOMAIN, SERVICE_DECREMENT, @@ -18,7 +19,7 @@ from homeassistant.loader import bind_hass @bind_hass def async_increment(hass, entity_id): """Increment a counter.""" - hass.async_add_job( + hass.async_create_task( hass.services.async_call(DOMAIN, SERVICE_INCREMENT, {ATTR_ENTITY_ID: entity_id}) ) @@ -27,7 +28,7 @@ def async_increment(hass, entity_id): @bind_hass def async_decrement(hass, entity_id): """Decrement a counter.""" - hass.async_add_job( + hass.async_create_task( hass.services.async_call(DOMAIN, SERVICE_DECREMENT, {ATTR_ENTITY_ID: entity_id}) ) @@ -36,6 +37,6 @@ def async_decrement(hass, entity_id): @bind_hass def async_reset(hass, entity_id): """Reset a counter.""" - hass.async_add_job( + hass.async_create_task( hass.services.async_call(DOMAIN, SERVICE_RESET, {ATTR_ENTITY_ID: entity_id}) ) diff --git a/tests/components/counter/test_init.py b/tests/components/counter/test_init.py index a52c083d10f..c0bd6344adb 100644 --- a/tests/components/counter/test_init.py +++ b/tests/components/counter/test_init.py @@ -1,4 +1,5 @@ """The tests for the counter component.""" + import logging import pytest diff --git a/tests/components/counter/test_reproduce_state.py b/tests/components/counter/test_reproduce_state.py index 44d0eca4d72..420c9b8907d 100644 --- a/tests/components/counter/test_reproduce_state.py +++ b/tests/components/counter/test_reproduce_state.py @@ -1,4 +1,5 @@ """Test reproduce state for Counter.""" + import pytest from homeassistant.core import HomeAssistant, State diff --git a/tests/components/cover/common.py b/tests/components/cover/common.py new file mode 100644 index 00000000000..d9f67e73f17 --- /dev/null +++ b/tests/components/cover/common.py @@ -0,0 +1,77 @@ +"""Collection of helper methods and classes for cover tests.""" + +from typing import Any + +from homeassistant.components.cover import CoverEntity, CoverEntityFeature +from homeassistant.const import STATE_CLOSED, STATE_CLOSING, STATE_OPEN, STATE_OPENING + +from tests.common import MockEntity + + +class MockCover(MockEntity, CoverEntity): + """Mock Cover class.""" + + def __init__( + self, reports_opening_closing: bool | None = None, **values: Any + ) -> None: + """Initialize a mock cover entity.""" + + super().__init__(**values) + self._reports_opening_closing = ( + reports_opening_closing + if reports_opening_closing is not None + else CoverEntityFeature.STOP in self.supported_features + ) + + @property + def is_closed(self): + """Return if the cover is closed or not.""" + if "state" in self._values and self._values["state"] == STATE_CLOSED: + return True + + return self.current_cover_position == 0 + + @property + def is_opening(self): + """Return if the cover is opening or not.""" + if "state" in self._values: + return self._values["state"] == STATE_OPENING + + return False + + @property + def is_closing(self): + """Return if the cover is closing or not.""" + if "state" in self._values: + return self._values["state"] == STATE_CLOSING + + return False + + def open_cover(self, **kwargs) -> None: + """Open cover.""" + if self._reports_opening_closing: + self._values["state"] = STATE_OPENING + else: + self._values["state"] = STATE_OPEN + + def close_cover(self, **kwargs) -> None: + """Close cover.""" + if self._reports_opening_closing: + self._values["state"] = STATE_CLOSING + else: + self._values["state"] = STATE_CLOSED + + def stop_cover(self, **kwargs) -> None: + """Stop cover.""" + assert CoverEntityFeature.STOP in self.supported_features + self._values["state"] = STATE_CLOSED if self.is_closed else STATE_OPEN + + @property + def current_cover_position(self): + """Return current position of cover.""" + return self._handle("current_cover_position") + + @property + def current_cover_tilt_position(self): + """Return current position of cover tilt.""" + return self._handle("current_cover_tilt_position") diff --git a/tests/components/cover/conftest.py b/tests/components/cover/conftest.py new file mode 100644 index 00000000000..1fc0de1fc2e --- /dev/null +++ b/tests/components/cover/conftest.py @@ -0,0 +1,67 @@ +"""Fixtures for cover entity components tests.""" + +import pytest + +from homeassistant.components.cover import CoverEntityFeature + +from .common import MockCover + + +@pytest.fixture +def mock_cover_entities() -> list[MockCover]: + """Return a list of MockCover instances.""" + return [ + MockCover( + name="Simple cover", + unique_id="unique_cover", + supported_features=CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE, + ), + MockCover( + name="Set position cover", + unique_id="unique_set_pos_cover", + current_cover_position=50, + supported_features=CoverEntityFeature.OPEN + | CoverEntityFeature.CLOSE + | CoverEntityFeature.STOP + | CoverEntityFeature.SET_POSITION, + ), + MockCover( + name="Simple tilt cover", + unique_id="unique_tilt_cover", + supported_features=CoverEntityFeature.OPEN + | CoverEntityFeature.CLOSE + | CoverEntityFeature.OPEN_TILT + | CoverEntityFeature.CLOSE_TILT, + ), + MockCover( + name="Set tilt position cover", + unique_id="unique_set_pos_tilt_cover", + current_cover_tilt_position=50, + supported_features=CoverEntityFeature.OPEN + | CoverEntityFeature.CLOSE + | CoverEntityFeature.OPEN_TILT + | CoverEntityFeature.CLOSE_TILT + | CoverEntityFeature.STOP_TILT + | CoverEntityFeature.SET_TILT_POSITION, + ), + MockCover( + name="All functions cover", + unique_id="unique_all_functions_cover", + current_cover_position=50, + current_cover_tilt_position=50, + supported_features=CoverEntityFeature.OPEN + | CoverEntityFeature.CLOSE + | CoverEntityFeature.STOP + | CoverEntityFeature.SET_POSITION + | CoverEntityFeature.OPEN_TILT + | CoverEntityFeature.CLOSE_TILT + | CoverEntityFeature.STOP_TILT + | CoverEntityFeature.SET_TILT_POSITION, + ), + MockCover( + name="Simple with opening/closing cover", + unique_id="unique_opening_closing_cover", + supported_features=CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE, + reports_opening_closing=True, + ), + ] diff --git a/tests/components/cover/test_device_action.py b/tests/components/cover/test_device_action.py index c476f78702e..43bf7431626 100644 --- a/tests/components/cover/test_device_action.py +++ b/tests/components/cover/test_device_action.py @@ -1,4 +1,5 @@ """The tests for Cover device actions.""" + import pytest from pytest_unordered import unordered @@ -16,7 +17,9 @@ from tests.common import ( async_get_device_automation_capabilities, async_get_device_automations, async_mock_service, + setup_test_component_platform, ) +from tests.components.cover.common import MockCover @pytest.fixture(autouse=True, name="stub_blueprint_populate") @@ -93,12 +96,12 @@ async def test_get_actions( @pytest.mark.parametrize( ("hidden_by", "entity_category"), - ( + [ (RegistryEntryHider.INTEGRATION, None), (RegistryEntryHider.USER, None), (None, EntityCategory.CONFIG), (None, EntityCategory.DIAGNOSTIC), - ), + ], ) async def test_get_actions_hidden_auxiliary( hass: HomeAssistant, @@ -144,26 +147,20 @@ async def test_get_action_capabilities( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - enable_custom_integrations: None, ) -> None: """Test we get the expected capabilities from a cover action.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init(empty=True) - platform.ENTITIES.append( - platform.MockCover( - name="Set position cover", - is_on=True, - unique_id="unique_set_pos_cover", - current_cover_position=50, - supported_features=CoverEntityFeature.OPEN - | CoverEntityFeature.CLOSE - | CoverEntityFeature.STOP - | CoverEntityFeature.OPEN_TILT - | CoverEntityFeature.CLOSE_TILT - | CoverEntityFeature.STOP_TILT, - ), + ent = MockCover( + name="Set position cover", + unique_id="unique_set_pos_cover", + current_cover_position=50, + supported_features=CoverEntityFeature.OPEN + | CoverEntityFeature.CLOSE + | CoverEntityFeature.STOP + | CoverEntityFeature.OPEN_TILT + | CoverEntityFeature.CLOSE_TILT + | CoverEntityFeature.STOP_TILT, ) - ent = platform.ENTITIES[0] + setup_test_component_platform(hass, DOMAIN, [ent]) assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) await hass.async_block_till_done() @@ -194,26 +191,20 @@ async def test_get_action_capabilities_legacy( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - enable_custom_integrations: None, ) -> None: """Test we get the expected capabilities from a cover action.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init(empty=True) - platform.ENTITIES.append( - platform.MockCover( - name="Set position cover", - is_on=True, - unique_id="unique_set_pos_cover", - current_cover_position=50, - supported_features=CoverEntityFeature.OPEN - | CoverEntityFeature.CLOSE - | CoverEntityFeature.STOP - | CoverEntityFeature.OPEN_TILT - | CoverEntityFeature.CLOSE_TILT - | CoverEntityFeature.STOP_TILT, - ), + ent = MockCover( + name="Set position cover", + unique_id="unique_set_pos_cover", + current_cover_position=50, + supported_features=CoverEntityFeature.OPEN + | CoverEntityFeature.CLOSE + | CoverEntityFeature.STOP + | CoverEntityFeature.OPEN_TILT + | CoverEntityFeature.CLOSE_TILT + | CoverEntityFeature.STOP_TILT, ) - ent = platform.ENTITIES[0] + setup_test_component_platform(hass, DOMAIN, [ent]) assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) await hass.async_block_till_done() @@ -245,12 +236,11 @@ async def test_get_action_capabilities_set_pos( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - enable_custom_integrations: None, + mock_cover_entities: list[MockCover], ) -> None: """Test we get the expected capabilities from a cover action.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() - ent = platform.ENTITIES[1] + setup_test_component_platform(hass, DOMAIN, mock_cover_entities) + ent = mock_cover_entities[1] assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) await hass.async_block_till_done() @@ -296,12 +286,11 @@ async def test_get_action_capabilities_set_tilt_pos( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - enable_custom_integrations: None, + mock_cover_entities: list[MockCover], ) -> None: """Test we get the expected capabilities from a cover action.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() - ent = platform.ENTITIES[3] + setup_test_component_platform(hass, DOMAIN, mock_cover_entities) + ent = mock_cover_entities[3] assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) await hass.async_block_till_done() @@ -353,7 +342,7 @@ async def test_action( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - enable_custom_integrations: None, + mock_cover_entities: list[MockCover], ) -> None: """Test for cover actions.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -440,7 +429,7 @@ async def test_action_legacy( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - enable_custom_integrations: None, + mock_cover_entities: list[MockCover], ) -> None: """Test for cover actions.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -487,7 +476,7 @@ async def test_action_tilt( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - enable_custom_integrations: None, + mock_cover_entities: list[MockCover], ) -> None: """Test for cover tilt actions.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -558,7 +547,7 @@ async def test_action_set_position( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - enable_custom_integrations: None, + mock_cover_entities: list[MockCover], ) -> None: """Test for cover set position actions.""" config_entry = MockConfigEntry(domain="test", data={}) diff --git a/tests/components/cover/test_device_condition.py b/tests/components/cover/test_device_condition.py index 2dcc719f35f..a58f94f44f3 100644 --- a/tests/components/cover/test_device_condition.py +++ b/tests/components/cover/test_device_condition.py @@ -1,4 +1,5 @@ """The tests for Cover device conditions.""" + import pytest from pytest_unordered import unordered @@ -24,7 +25,9 @@ from tests.common import ( async_get_device_automation_capabilities, async_get_device_automations, async_mock_service, + setup_test_component_platform, ) +from tests.components.cover.common import MockCover @pytest.fixture(autouse=True, name="stub_blueprint_populate") @@ -122,12 +125,12 @@ async def test_get_conditions( @pytest.mark.parametrize( ("hidden_by", "entity_category"), - ( + [ (RegistryEntryHider.INTEGRATION, None), (RegistryEntryHider.USER, None), (None, EntityCategory.CONFIG), (None, EntityCategory.DIAGNOSTIC), - ), + ], ) async def test_get_conditions_hidden_auxiliary( hass: HomeAssistant, @@ -173,12 +176,11 @@ async def test_get_condition_capabilities( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - enable_custom_integrations: None, + mock_cover_entities: list[MockCover], ) -> None: """Test we get the expected capabilities from a cover condition.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() - ent = platform.ENTITIES[0] + setup_test_component_platform(hass, DOMAIN, mock_cover_entities) + ent = mock_cover_entities[0] assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) await hass.async_block_till_done() @@ -207,12 +209,11 @@ async def test_get_condition_capabilities_legacy( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - enable_custom_integrations: None, + mock_cover_entities: list[MockCover], ) -> None: """Test we get the expected capabilities from a cover condition.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() - ent = platform.ENTITIES[0] + setup_test_component_platform(hass, DOMAIN, mock_cover_entities) + ent = mock_cover_entities[0] assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) await hass.async_block_till_done() @@ -244,12 +245,11 @@ async def test_get_condition_capabilities_set_pos( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - enable_custom_integrations: None, + mock_cover_entities: list[MockCover], ) -> None: """Test we get the expected capabilities from a cover condition.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() - ent = platform.ENTITIES[1] + setup_test_component_platform(hass, DOMAIN, mock_cover_entities) + ent = mock_cover_entities[1] assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) await hass.async_block_till_done() @@ -301,12 +301,12 @@ async def test_get_condition_capabilities_set_tilt_pos( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - enable_custom_integrations: None, + mock_cover_entities: list[MockCover], ) -> None: """Test we get the expected capabilities from a cover condition.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() - ent = platform.ENTITIES[3] + setup_test_component_platform(hass, DOMAIN, mock_cover_entities) + + ent = mock_cover_entities[3] assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) await hass.async_block_till_done() @@ -559,12 +559,11 @@ async def test_if_position( entity_registry: er.EntityRegistry, calls, caplog: pytest.LogCaptureFixture, - enable_custom_integrations: None, + mock_cover_entities: list[MockCover], ) -> None: """Test for position conditions.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() - ent = platform.ENTITIES[1] + setup_test_component_platform(hass, DOMAIN, mock_cover_entities) + ent = mock_cover_entities[1] assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) await hass.async_block_till_done() @@ -720,12 +719,11 @@ async def test_if_tilt_position( entity_registry: er.EntityRegistry, calls, caplog: pytest.LogCaptureFixture, - enable_custom_integrations: None, + mock_cover_entities: list[MockCover], ) -> None: """Test for tilt position conditions.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() - ent = platform.ENTITIES[3] + setup_test_component_platform(hass, DOMAIN, mock_cover_entities) + ent = mock_cover_entities[3] assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) await hass.async_block_till_done() diff --git a/tests/components/cover/test_device_trigger.py b/tests/components/cover/test_device_trigger.py index e464ff87c3f..5db52b6d618 100644 --- a/tests/components/cover/test_device_trigger.py +++ b/tests/components/cover/test_device_trigger.py @@ -1,4 +1,5 @@ """The tests for Cover device triggers.""" + from datetime import timedelta import pytest @@ -27,7 +28,9 @@ from tests.common import ( async_get_device_automation_capabilities, async_get_device_automations, async_mock_service, + setup_test_component_platform, ) +from tests.components.cover.common import MockCover @pytest.fixture(autouse=True, name="stub_blueprint_populate") @@ -123,12 +126,12 @@ async def test_get_triggers( @pytest.mark.parametrize( ("hidden_by", "entity_category"), - ( + [ (RegistryEntryHider.INTEGRATION, None), (RegistryEntryHider.USER, None), (None, EntityCategory.CONFIG), (None, EntityCategory.DIAGNOSTIC), - ), + ], ) async def test_get_triggers_hidden_auxiliary( hass: HomeAssistant, @@ -174,12 +177,11 @@ async def test_get_trigger_capabilities( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - enable_custom_integrations: None, + mock_cover_entities: list[MockCover], ) -> None: """Test we get the expected capabilities from a cover trigger.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() - ent = platform.ENTITIES[0] + setup_test_component_platform(hass, DOMAIN, mock_cover_entities) + ent = mock_cover_entities[0] assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) await hass.async_block_till_done() @@ -212,12 +214,11 @@ async def test_get_trigger_capabilities_legacy( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - enable_custom_integrations: None, + mock_cover_entities: list[MockCover], ) -> None: """Test we get the expected capabilities from a cover trigger.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() - ent = platform.ENTITIES[0] + setup_test_component_platform(hass, DOMAIN, mock_cover_entities) + ent = mock_cover_entities[0] assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) await hass.async_block_till_done() @@ -251,12 +252,11 @@ async def test_get_trigger_capabilities_set_pos( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - enable_custom_integrations: None, + mock_cover_entities: list[MockCover], ) -> None: """Test we get the expected capabilities from a cover trigger.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() - ent = platform.ENTITIES[1] + setup_test_component_platform(hass, DOMAIN, mock_cover_entities) + ent = mock_cover_entities[1] assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) await hass.async_block_till_done() @@ -316,12 +316,11 @@ async def test_get_trigger_capabilities_set_tilt_pos( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - enable_custom_integrations: None, + mock_cover_entities: list[MockCover], ) -> None: """Test we get the expected capabilities from a cover trigger.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() - ent = platform.ENTITIES[3] + setup_test_component_platform(hass, DOMAIN, mock_cover_entities) + ent = mock_cover_entities[3] assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) await hass.async_block_till_done() @@ -662,13 +661,12 @@ async def test_if_fires_on_position( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, + mock_cover_entities: list[MockCover], calls, - enable_custom_integrations: None, ) -> None: """Test for position triggers.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() - ent = platform.ENTITIES[1] + setup_test_component_platform(hass, DOMAIN, mock_cover_entities) + ent = mock_cover_entities[1] assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) await hass.async_block_till_done() @@ -779,11 +777,11 @@ async def test_if_fires_on_position( ) == sorted( [ ( - "is_pos_gt_45_lt_90 - device - cover.set_position_cover - closed - open" + f"is_pos_gt_45_lt_90 - device - {entry.entity_id} - closed - open" " - None" ), - "is_pos_lt_90 - device - cover.set_position_cover - closed - open - None", - "is_pos_gt_45 - device - cover.set_position_cover - open - closed - None", + f"is_pos_lt_90 - device - {entry.entity_id} - closed - open - None", + f"is_pos_gt_45 - device - {entry.entity_id} - open - closed - None", ] ) @@ -798,7 +796,7 @@ async def test_if_fires_on_position( assert len(calls) == 4 assert ( calls[3].data["some"] - == "is_pos_lt_90 - device - cover.set_position_cover - closed - closed - None" + == f"is_pos_lt_90 - device - {entry.entity_id} - closed - closed - None" ) hass.states.async_set( @@ -808,7 +806,7 @@ async def test_if_fires_on_position( assert len(calls) == 5 assert ( calls[4].data["some"] - == "is_pos_gt_45 - device - cover.set_position_cover - closed - closed - None" + == f"is_pos_gt_45 - device - {entry.entity_id} - closed - closed - None" ) @@ -817,12 +815,11 @@ async def test_if_fires_on_tilt_position( device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, calls, - enable_custom_integrations: None, + mock_cover_entities: list[MockCover], ) -> None: """Test for tilt position triggers.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() - ent = platform.ENTITIES[1] + setup_test_component_platform(hass, DOMAIN, mock_cover_entities) + ent = mock_cover_entities[1] assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) await hass.async_block_till_done() @@ -935,11 +932,11 @@ async def test_if_fires_on_tilt_position( ) == sorted( [ ( - "is_pos_gt_45_lt_90 - device - cover.set_position_cover - closed - open" + f"is_pos_gt_45_lt_90 - device - {entry.entity_id} - closed - open" " - None" ), - "is_pos_lt_90 - device - cover.set_position_cover - closed - open - None", - "is_pos_gt_45 - device - cover.set_position_cover - open - closed - None", + f"is_pos_lt_90 - device - {entry.entity_id} - closed - open - None", + f"is_pos_gt_45 - device - {entry.entity_id} - open - closed - None", ] ) @@ -954,7 +951,7 @@ async def test_if_fires_on_tilt_position( assert len(calls) == 4 assert ( calls[3].data["some"] - == "is_pos_lt_90 - device - cover.set_position_cover - closed - closed - None" + == f"is_pos_lt_90 - device - {entry.entity_id} - closed - closed - None" ) hass.states.async_set( @@ -964,5 +961,5 @@ async def test_if_fires_on_tilt_position( assert len(calls) == 5 assert ( calls[4].data["some"] - == "is_pos_gt_45 - device - cover.set_position_cover - closed - closed - None" + == f"is_pos_gt_45 - device - {entry.entity_id} - closed - closed - None" ) diff --git a/tests/components/cover/test_init.py b/tests/components/cover/test_init.py index 0503017f634..ec090b878f2 100644 --- a/tests/components/cover/test_init.py +++ b/tests/components/cover/test_init.py @@ -1,4 +1,5 @@ """The tests for Cover.""" + from enum import Enum import pytest @@ -16,14 +17,21 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -from tests.common import help_test_all, import_and_test_deprecated_constant_enum +from tests.common import ( + help_test_all, + import_and_test_deprecated_constant_enum, + setup_test_component_platform, +) +from tests.components.cover.common import MockCover -async def test_services(hass: HomeAssistant, enable_custom_integrations: None) -> None: +async def test_services( + hass: HomeAssistant, + mock_cover_entities: list[MockCover], +) -> None: """Test the provided services.""" - platform = getattr(hass.components, "test.cover") + setup_test_component_platform(hass, cover.DOMAIN, mock_cover_entities) - platform.init() assert await async_setup_component( hass, cover.DOMAIN, {cover.DOMAIN: {CONF_PLATFORM: "test"}} ) @@ -35,7 +43,7 @@ async def test_services(hass: HomeAssistant, enable_custom_integrations: None) - # ent4 = cover with all tilt functions but no position # ent5 = cover with all functions # ent6 = cover with only open/close, but also reports opening/closing - ent1, ent2, ent3, ent4, ent5, ent6 = platform.ENTITIES + ent1, ent2, ent3, ent4, ent5, ent6 = mock_cover_entities # Test init all covers should be open assert is_open(hass, ent1) @@ -139,10 +147,7 @@ def is_closing(hass, ent): def _create_tuples(enum: Enum, constant_prefix: str) -> list[tuple[Enum, str]]: - result = [] - for enum in enum: - result.append((enum, constant_prefix)) - return result + return [(enum_field, constant_prefix) for enum_field in enum] def test_all() -> None: diff --git a/tests/components/cover/test_reproduce_state.py b/tests/components/cover/test_reproduce_state.py index 70f8201fedc..f5dd01745d3 100644 --- a/tests/components/cover/test_reproduce_state.py +++ b/tests/components/cover/test_reproduce_state.py @@ -1,4 +1,5 @@ """Test reproduce state for Cover.""" + import pytest from homeassistant.components.cover import ( diff --git a/tests/components/cover/test_significant_change.py b/tests/components/cover/test_significant_change.py index 9ddb2cb9498..5e5032dce3f 100644 --- a/tests/components/cover/test_significant_change.py +++ b/tests/components/cover/test_significant_change.py @@ -1,4 +1,5 @@ """Test the Cover significant change platform.""" + import pytest from homeassistant.components.cover import ( diff --git a/tests/components/cpuspeed/conftest.py b/tests/components/cpuspeed/conftest.py index be5a87b8d13..82dfb5eac30 100644 --- a/tests/components/cpuspeed/conftest.py +++ b/tests/components/cpuspeed/conftest.py @@ -1,4 +1,5 @@ """Fixtures for CPU Speed integration tests.""" + from __future__ import annotations from collections.abc import Generator diff --git a/tests/components/cpuspeed/test_diagnostics.py b/tests/components/cpuspeed/test_diagnostics.py index 2c91566216d..a596c7d62d9 100644 --- a/tests/components/cpuspeed/test_diagnostics.py +++ b/tests/components/cpuspeed/test_diagnostics.py @@ -1,4 +1,5 @@ """Tests for the diagnostics data provided by the CPU Speed integration.""" + from unittest.mock import patch from syrupy import SnapshotAssertion diff --git a/tests/components/cpuspeed/test_init.py b/tests/components/cpuspeed/test_init.py index cdb86ba2f46..76158f22473 100644 --- a/tests/components/cpuspeed/test_init.py +++ b/tests/components/cpuspeed/test_init.py @@ -1,4 +1,5 @@ """Tests for the CPU Speed integration.""" + from unittest.mock import MagicMock import pytest diff --git a/tests/components/crownstone/test_config_flow.py b/tests/components/crownstone/test_config_flow.py index d112a0f7bd3..04f69f3a74a 100644 --- a/tests/components/crownstone/test_config_flow.py +++ b/tests/components/crownstone/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for the Crownstone integration.""" + from __future__ import annotations from collections.abc import Generator diff --git a/tests/components/daikin/test_config_flow.py b/tests/components/daikin/test_config_flow.py index 942137e8f6d..ece17b6aafe 100644 --- a/tests/components/daikin/test_config_flow.py +++ b/tests/components/daikin/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for the Daikin config flow.""" + from ipaddress import ip_address from unittest.mock import PropertyMock, patch diff --git a/tests/components/daikin/test_init.py b/tests/components/daikin/test_init.py index 7c4467c3031..01b21ebb6fd 100644 --- a/tests/components/daikin/test_init.py +++ b/tests/components/daikin/test_init.py @@ -1,4 +1,5 @@ """Define tests for the Daikin init.""" + from datetime import timedelta from unittest.mock import AsyncMock, PropertyMock, patch diff --git a/tests/components/daikin/test_temperature_format.py b/tests/components/daikin/test_temperature_format.py index bc92c1ab10b..c6ea5c0fea1 100644 --- a/tests/components/daikin/test_temperature_format.py +++ b/tests/components/daikin/test_temperature_format.py @@ -1,4 +1,5 @@ """The tests for the Daikin target temperature conversion.""" + from homeassistant.components.daikin.climate import format_target_temperature diff --git a/tests/components/datadog/test_init.py b/tests/components/datadog/test_init.py index 1ae795f2e95..51d698186b7 100644 --- a/tests/components/datadog/test_init.py +++ b/tests/components/datadog/test_init.py @@ -1,4 +1,5 @@ """The tests for the Datadog component.""" + from unittest import mock from unittest.mock import patch @@ -22,8 +23,9 @@ async def test_datadog_setup_full(hass: HomeAssistant) -> None: """Test setup with all data.""" config = {datadog.DOMAIN: {"host": "host", "port": 123, "rate": 1, "prefix": "foo"}} - with patch("homeassistant.components.datadog.initialize") as mock_init, patch( - "homeassistant.components.datadog.statsd" + with ( + patch("homeassistant.components.datadog.initialize") as mock_init, + patch("homeassistant.components.datadog.statsd"), ): assert await async_setup_component(hass, datadog.DOMAIN, config) @@ -33,8 +35,9 @@ async def test_datadog_setup_full(hass: HomeAssistant) -> None: async def test_datadog_setup_defaults(hass: HomeAssistant) -> None: """Test setup with defaults.""" - with patch("homeassistant.components.datadog.initialize") as mock_init, patch( - "homeassistant.components.datadog.statsd" + with ( + patch("homeassistant.components.datadog.initialize") as mock_init, + patch("homeassistant.components.datadog.statsd"), ): assert await async_setup_component( hass, @@ -54,9 +57,10 @@ async def test_datadog_setup_defaults(hass: HomeAssistant) -> None: async def test_logbook_entry(hass: HomeAssistant) -> None: """Test event listener.""" - with patch("homeassistant.components.datadog.initialize"), patch( - "homeassistant.components.datadog.statsd" - ) as mock_statsd: + with ( + patch("homeassistant.components.datadog.initialize"), + patch("homeassistant.components.datadog.statsd") as mock_statsd, + ): assert await async_setup_component( hass, datadog.DOMAIN, @@ -84,9 +88,10 @@ async def test_logbook_entry(hass: HomeAssistant) -> None: async def test_state_changed(hass: HomeAssistant) -> None: """Test event listener.""" - with patch("homeassistant.components.datadog.initialize"), patch( - "homeassistant.components.datadog.statsd" - ) as mock_statsd: + with ( + patch("homeassistant.components.datadog.initialize"), + patch("homeassistant.components.datadog.statsd") as mock_statsd, + ): assert await async_setup_component( hass, datadog.DOMAIN, diff --git a/tests/components/date/test_init.py b/tests/components/date/test_init.py index 2ae17673119..f0a0094f8b8 100644 --- a/tests/components/date/test_init.py +++ b/tests/components/date/test_init.py @@ -1,4 +1,5 @@ """The tests for the date component.""" + from datetime import date from homeassistant.components.date import DOMAIN, SERVICE_SET_VALUE, DateEntity diff --git a/tests/components/datetime/test_init.py b/tests/components/datetime/test_init.py index 6f2e2db29a1..f85754f5e1f 100644 --- a/tests/components/datetime/test_init.py +++ b/tests/components/datetime/test_init.py @@ -1,4 +1,5 @@ """The tests for the datetime component.""" + from datetime import UTC, datetime from zoneinfo import ZoneInfo diff --git a/tests/components/debugpy/test_init.py b/tests/components/debugpy/test_init.py index 97d08e13bfc..e4d77fc480a 100644 --- a/tests/components/debugpy/test_init.py +++ b/tests/components/debugpy/test_init.py @@ -1,4 +1,5 @@ """Tests for the Remote Python Debugger integration.""" + from unittest.mock import patch import pytest diff --git a/tests/components/deconz/conftest.py b/tests/components/deconz/conftest.py index 44411ca40cf..d0f0f11c99b 100644 --- a/tests/components/deconz/conftest.py +++ b/tests/components/deconz/conftest.py @@ -1,4 +1,5 @@ """deconz conftest.""" + from __future__ import annotations from unittest.mock import patch diff --git a/tests/components/deconz/test_alarm_control_panel.py b/tests/components/deconz/test_alarm_control_panel.py index 14eee701c67..ec926491724 100644 --- a/tests/components/deconz/test_alarm_control_panel.py +++ b/tests/components/deconz/test_alarm_control_panel.py @@ -1,4 +1,5 @@ """deCONZ alarm control panel platform tests.""" + from unittest.mock import patch from pydeconz.models.sensor.ancillary_control import AncillaryControlPanel diff --git a/tests/components/deconz/test_binary_sensor.py b/tests/components/deconz/test_binary_sensor.py index 68396c8ff9c..9fd57926f44 100644 --- a/tests/components/deconz/test_binary_sensor.py +++ b/tests/components/deconz/test_binary_sensor.py @@ -1,9 +1,10 @@ """deCONZ binary sensor platform tests.""" + from unittest.mock import patch import pytest -from homeassistant.components.binary_sensor import DOMAIN, BinarySensorDeviceClass +from homeassistant.components.binary_sensor import BinarySensorDeviceClass from homeassistant.components.deconz.const import ( CONF_ALLOW_CLIP_SENSOR, CONF_ALLOW_NEW_DEVICES, @@ -68,7 +69,6 @@ TEST_DATA = [ "device_count": 3, "entity_id": "binary_sensor.alarm_10", "unique_id": "00:15:8d:00:02:b5:d1:80-01-0500-alarm", - "old_unique_id": "00:15:8d:00:02:b5:d1:80-01-0500", "state": STATE_OFF, "entity_category": None, "device_class": BinarySensorDeviceClass.SAFETY, @@ -110,7 +110,6 @@ TEST_DATA = [ "device_count": 3, "entity_id": "binary_sensor.cave_co", "unique_id": "00:15:8d:00:02:a5:21:24-01-0101-carbon_monoxide", - "old_unique_id": "00:15:8d:00:02:a5:21:24-01-0101", "state": STATE_OFF, "entity_category": None, "device_class": BinarySensorDeviceClass.CO, @@ -146,7 +145,6 @@ TEST_DATA = [ "device_count": 3, "entity_id": "binary_sensor.sensor_kitchen_smoke", "unique_id": "00:15:8d:00:01:d9:3e:7c-01-0500-fire", - "old_unique_id": "00:15:8d:00:01:d9:3e:7c-01-0500", "state": STATE_OFF, "entity_category": None, "device_class": BinarySensorDeviceClass.SMOKE, @@ -183,7 +181,6 @@ TEST_DATA = [ "device_count": 3, "entity_id": "binary_sensor.sensor_kitchen_smoke_test_mode", "unique_id": "00:15:8d:00:01:d9:3e:7c-01-0500-in_test_mode", - "old_unique_id": "00:15:8d:00:01:d9:3e:7c-test mode", "state": STATE_OFF, "entity_category": EntityCategory.DIAGNOSTIC, "device_class": BinarySensorDeviceClass.SMOKE, @@ -216,7 +213,6 @@ TEST_DATA = [ "device_count": 2, "entity_id": "binary_sensor.kitchen_switch", "unique_id": "kitchen-switch-flag", - "old_unique_id": "kitchen-switch", "state": STATE_ON, "entity_category": None, "device_class": None, @@ -254,7 +250,6 @@ TEST_DATA = [ "device_count": 3, "entity_id": "binary_sensor.back_door", "unique_id": "00:15:8d:00:02:2b:96:b4-01-0006-open", - "old_unique_id": "00:15:8d:00:02:2b:96:b4-01-0006", "state": STATE_OFF, "entity_category": None, "device_class": BinarySensorDeviceClass.OPENING, @@ -301,7 +296,6 @@ TEST_DATA = [ "device_count": 3, "entity_id": "binary_sensor.motion_sensor_4", "unique_id": "00:17:88:01:03:28:8c:9b-02-0406-presence", - "old_unique_id": "00:17:88:01:03:28:8c:9b-02-0406", "state": STATE_OFF, "entity_category": None, "device_class": BinarySensorDeviceClass.MOTION, @@ -343,7 +337,6 @@ TEST_DATA = [ "device_count": 3, "entity_id": "binary_sensor.water2", "unique_id": "00:15:8d:00:02:2f:07:db-01-0500-water", - "old_unique_id": "00:15:8d:00:02:2f:07:db-01-0500", "state": STATE_OFF, "entity_category": None, "device_class": BinarySensorDeviceClass.MOISTURE, @@ -389,7 +382,6 @@ TEST_DATA = [ "device_count": 3, "entity_id": "binary_sensor.vibration_1", "unique_id": "00:15:8d:00:02:a5:21:24-01-0101-vibration", - "old_unique_id": "00:15:8d:00:02:a5:21:24-01-0101", "state": STATE_ON, "entity_category": None, "device_class": BinarySensorDeviceClass.VIBRATION, @@ -428,7 +420,6 @@ TEST_DATA = [ "device_count": 3, "entity_id": "binary_sensor.presence_sensor_tampered", "unique_id": "00:00:00:00:00:00:00:00-00-tampered", - "old_unique_id": "00:00:00:00:00:00:00:00-tampered", "state": STATE_OFF, "entity_category": EntityCategory.DIAGNOSTIC, "device_class": BinarySensorDeviceClass.TAMPER, @@ -462,7 +453,6 @@ TEST_DATA = [ "device_count": 3, "entity_id": "binary_sensor.presence_sensor_low_battery", "unique_id": "00:00:00:00:00:00:00:00-00-low_battery", - "old_unique_id": "00:00:00:00:00:00:00:00-low battery", "state": STATE_OFF, "entity_category": EntityCategory.DIAGNOSTIC, "device_class": BinarySensorDeviceClass.BATTERY, @@ -488,15 +478,6 @@ async def test_binary_sensors( expected, ) -> None: """Test successful creation of binary sensor entities.""" - - # Create entity entry to migrate to new unique ID - entity_registry.async_get_or_create( - DOMAIN, - DECONZ_DOMAIN, - expected["old_unique_id"], - suggested_object_id=expected["entity_id"].replace(DOMAIN, ""), - ) - with patch.dict(DECONZ_WEB_REQUEST, {"sensors": {"1": sensor_data}}): config_entry = await setup_deconz_integration( hass, aioclient_mock, options={CONF_ALLOW_CLIP_SENSOR: True} diff --git a/tests/components/deconz/test_button.py b/tests/components/deconz/test_button.py index 7f4dd59bf16..4d85270ddca 100644 --- a/tests/components/deconz/test_button.py +++ b/tests/components/deconz/test_button.py @@ -1,4 +1,5 @@ """deCONZ button platform tests.""" + from unittest.mock import patch import pytest diff --git a/tests/components/deconz/test_climate.py b/tests/components/deconz/test_climate.py index dd0de559ba8..0e51f31cec4 100644 --- a/tests/components/deconz/test_climate.py +++ b/tests/components/deconz/test_climate.py @@ -1,4 +1,5 @@ """deCONZ climate platform tests.""" + from unittest.mock import patch import pytest diff --git a/tests/components/deconz/test_config_flow.py b/tests/components/deconz/test_config_flow.py index 7874b7899c8..a6910ef4b55 100644 --- a/tests/components/deconz/test_config_flow.py +++ b/tests/components/deconz/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for deCONZ config flow.""" + import logging from unittest.mock import patch diff --git a/tests/components/deconz/test_cover.py b/tests/components/deconz/test_cover.py index 54a2c0f65a2..69452c3285e 100644 --- a/tests/components/deconz/test_cover.py +++ b/tests/components/deconz/test_cover.py @@ -1,4 +1,5 @@ """deCONZ cover platform tests.""" + from unittest.mock import patch from homeassistant.components.cover import ( diff --git a/tests/components/deconz/test_deconz_event.py b/tests/components/deconz/test_deconz_event.py index 403feb07915..1193f348e38 100644 --- a/tests/components/deconz/test_deconz_event.py +++ b/tests/components/deconz/test_deconz_event.py @@ -1,4 +1,5 @@ """Test deCONZ remote events.""" + from unittest.mock import patch from pydeconz.models.sensor.ancillary_control import ( diff --git a/tests/components/deconz/test_device_trigger.py b/tests/components/deconz/test_device_trigger.py index 4c3344f5822..329cf0405db 100644 --- a/tests/components/deconz/test_device_trigger.py +++ b/tests/components/deconz/test_device_trigger.py @@ -1,4 +1,5 @@ """deCONZ device automation tests.""" + from unittest.mock import Mock, patch import pytest diff --git a/tests/components/deconz/test_diagnostics.py b/tests/components/deconz/test_diagnostics.py index e7e470cdf81..bfbc27b206d 100644 --- a/tests/components/deconz/test_diagnostics.py +++ b/tests/components/deconz/test_diagnostics.py @@ -1,4 +1,5 @@ """Test deCONZ diagnostics.""" + from pydeconz.websocket import State from syrupy import SnapshotAssertion diff --git a/tests/components/deconz/test_fan.py b/tests/components/deconz/test_fan.py index 7360e442dfa..5da0398c3e6 100644 --- a/tests/components/deconz/test_fan.py +++ b/tests/components/deconz/test_fan.py @@ -1,4 +1,5 @@ """deCONZ fan platform tests.""" + from unittest.mock import patch import pytest diff --git a/tests/components/deconz/test_gateway.py b/tests/components/deconz/test_gateway.py index 84a57fe7595..5a55fb64090 100644 --- a/tests/components/deconz/test_gateway.py +++ b/tests/components/deconz/test_gateway.py @@ -1,4 +1,5 @@ """Test deCONZ gateway.""" + from copy import deepcopy from unittest.mock import patch @@ -17,10 +18,7 @@ from homeassistant.components.cover import DOMAIN as COVER_DOMAIN from homeassistant.components.deconz.config_flow import DECONZ_MANUFACTURERURL from homeassistant.components.deconz.const import DOMAIN as DECONZ_DOMAIN from homeassistant.components.deconz.errors import AuthenticationRequired, CannotConnect -from homeassistant.components.deconz.gateway import ( - get_deconz_session, - get_gateway_from_config_entry, -) +from homeassistant.components.deconz.hub import DeconzHub, get_deconz_api from homeassistant.components.fan import DOMAIN as FAN_DOMAIN from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN @@ -144,40 +142,39 @@ async def test_gateway_setup( ) -> None: """Successful setup.""" with patch( - "homeassistant.config_entries.ConfigEntries.async_forward_entry_setup", + "homeassistant.config_entries.ConfigEntries.async_forward_entry_setups", return_value=True, ) as forward_entry_setup: config_entry = await setup_deconz_integration(hass, aioclient_mock) - gateway = get_gateway_from_config_entry(hass, config_entry) + gateway = DeconzHub.get_hub(hass, config_entry) assert gateway.bridgeid == BRIDGEID assert gateway.master is True - assert gateway.option_allow_clip_sensor is False - assert gateway.option_allow_deconz_groups is True - assert gateway.option_allow_new_devices is True + assert gateway.config.allow_clip_sensor is False + assert gateway.config.allow_deconz_groups is True + assert gateway.config.allow_new_devices is True assert len(gateway.deconz_ids) == 0 assert len(hass.states.async_all()) == 0 assert forward_entry_setup.mock_calls[0][1] == ( config_entry, - ALARM_CONTROL_PANEL_DOMAIN, + [ + ALARM_CONTROL_PANEL_DOMAIN, + BINARY_SENSOR_DOMAIN, + BUTTON_DOMAIN, + CLIMATE_DOMAIN, + COVER_DOMAIN, + FAN_DOMAIN, + LIGHT_DOMAIN, + LOCK_DOMAIN, + NUMBER_DOMAIN, + SCENE_DOMAIN, + SELECT_DOMAIN, + SENSOR_DOMAIN, + SIREN_DOMAIN, + SWITCH_DOMAIN, + ], ) - assert forward_entry_setup.mock_calls[1][1] == ( - config_entry, - BINARY_SENSOR_DOMAIN, - ) - assert forward_entry_setup.mock_calls[2][1] == (config_entry, BUTTON_DOMAIN) - assert forward_entry_setup.mock_calls[3][1] == (config_entry, CLIMATE_DOMAIN) - assert forward_entry_setup.mock_calls[4][1] == (config_entry, COVER_DOMAIN) - assert forward_entry_setup.mock_calls[5][1] == (config_entry, FAN_DOMAIN) - assert forward_entry_setup.mock_calls[6][1] == (config_entry, LIGHT_DOMAIN) - assert forward_entry_setup.mock_calls[7][1] == (config_entry, LOCK_DOMAIN) - assert forward_entry_setup.mock_calls[8][1] == (config_entry, NUMBER_DOMAIN) - assert forward_entry_setup.mock_calls[9][1] == (config_entry, SCENE_DOMAIN) - assert forward_entry_setup.mock_calls[10][1] == (config_entry, SELECT_DOMAIN) - assert forward_entry_setup.mock_calls[11][1] == (config_entry, SENSOR_DOMAIN) - assert forward_entry_setup.mock_calls[12][1] == (config_entry, SIREN_DOMAIN) - assert forward_entry_setup.mock_calls[13][1] == (config_entry, SWITCH_DOMAIN) gateway_entry = device_registry.async_get_device( identifiers={(DECONZ_DOMAIN, gateway.bridgeid)} @@ -200,7 +197,7 @@ async def test_gateway_device_configuration_url_when_addon( config_entry = await setup_deconz_integration( hass, aioclient_mock, source=SOURCE_HASSIO ) - gateway = get_gateway_from_config_entry(hass, config_entry) + gateway = DeconzHub.get_hub(hass, config_entry) gateway_entry = device_registry.async_get_device( identifiers={(DECONZ_DOMAIN, gateway.bridgeid)} @@ -247,7 +244,7 @@ async def test_update_address( ) -> None: """Make sure that connection status triggers a dispatcher send.""" config_entry = await setup_deconz_integration(hass, aioclient_mock) - gateway = get_gateway_from_config_entry(hass, config_entry) + gateway = DeconzHub.get_hub(hass, config_entry) assert gateway.api.host == "1.2.3.4" with patch( @@ -279,7 +276,7 @@ async def test_reset_after_successful_setup( ) -> None: """Make sure that connection status triggers a dispatcher send.""" config_entry = await setup_deconz_integration(hass, aioclient_mock) - gateway = get_gateway_from_config_entry(hass, config_entry) + gateway = DeconzHub.get_hub(hass, config_entry) result = await gateway.async_reset() await hass.async_block_till_done() @@ -287,10 +284,11 @@ async def test_reset_after_successful_setup( assert result is True -async def test_get_deconz_session(hass: HomeAssistant) -> None: +async def test_get_deconz_api(hass: HomeAssistant) -> None: """Successful call.""" + config_entry = MockConfigEntry(domain=DECONZ_DOMAIN, data=ENTRY_CONFIG) with patch("pydeconz.DeconzSession.refresh_state", return_value=True): - assert await get_deconz_session(hass, ENTRY_CONFIG) + assert await get_deconz_api(hass, config_entry) @pytest.mark.parametrize( @@ -302,12 +300,16 @@ async def test_get_deconz_session(hass: HomeAssistant) -> None: (pydeconz.Unauthorized, AuthenticationRequired), ], ) -async def test_get_deconz_session_fails( +async def test_get_deconz_api_fails( hass: HomeAssistant, side_effect, raised_exception ) -> None: """Failed call.""" - with patch( - "pydeconz.DeconzSession.refresh_state", - side_effect=side_effect, - ), pytest.raises(raised_exception): - assert await get_deconz_session(hass, ENTRY_CONFIG) + config_entry = MockConfigEntry(domain=DECONZ_DOMAIN, data=ENTRY_CONFIG) + with ( + patch( + "pydeconz.DeconzSession.refresh_state", + side_effect=side_effect, + ), + pytest.raises(raised_exception), + ): + assert await get_deconz_api(hass, config_entry) diff --git a/tests/components/deconz/test_init.py b/tests/components/deconz/test_init.py index fe364779059..0555f70f5e6 100644 --- a/tests/components/deconz/test_init.py +++ b/tests/components/deconz/test_init.py @@ -1,26 +1,19 @@ """Test deCONZ component setup process.""" + import asyncio from unittest.mock import patch from homeassistant.components.deconz import ( - DeconzGateway, + DeconzHub, async_setup_entry, async_unload_entry, - async_update_group_unique_id, -) -from homeassistant.components.deconz.const import ( - CONF_GROUP_ID_BASE, - DOMAIN as DECONZ_DOMAIN, ) +from homeassistant.components.deconz.const import DOMAIN as DECONZ_DOMAIN from homeassistant.components.deconz.errors import AuthenticationRequired, CannotConnect -from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN -from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration -from tests.common import MockConfigEntry from tests.test_util.aiohttp import AiohttpClientMocker ENTRY1_HOST = "1.2.3.4" @@ -38,8 +31,9 @@ ENTRY2_UUID = "789ACE" async def setup_entry(hass, entry): """Test that setup entry works.""" - with patch.object(DeconzGateway, "async_setup", return_value=True), patch.object( - DeconzGateway, "async_update_device_registry", return_value=True + with ( + patch.object(DeconzHub, "async_setup", return_value=True), + patch.object(DeconzHub, "async_update_device_registry", return_value=True), ): assert await async_setup_entry(hass, entry) is True @@ -58,7 +52,7 @@ async def test_setup_entry_successful( async def test_setup_entry_fails_config_entry_not_ready(hass: HomeAssistant) -> None: """Failed authentication trigger a reauthentication flow.""" with patch( - "homeassistant.components.deconz.get_deconz_session", + "homeassistant.components.deconz.get_deconz_api", side_effect=CannotConnect, ): await setup_deconz_integration(hass) @@ -68,10 +62,13 @@ async def test_setup_entry_fails_config_entry_not_ready(hass: HomeAssistant) -> async def test_setup_entry_fails_trigger_reauth_flow(hass: HomeAssistant) -> None: """Failed authentication trigger a reauthentication flow.""" - with patch( - "homeassistant.components.deconz.get_deconz_session", - side_effect=AuthenticationRequired, - ), patch.object(hass.config_entries.flow, "async_init") as mock_flow_init: + with ( + patch( + "homeassistant.components.deconz.get_deconz_api", + side_effect=AuthenticationRequired, + ), + patch.object(hass.config_entries.flow, "async_init") as mock_flow_init, + ): await setup_deconz_integration(hass) mock_flow_init.assert_called_once() @@ -157,80 +154,3 @@ async def test_unload_entry_multiple_gateways_parallel( ) assert len(hass.data[DECONZ_DOMAIN]) == 0 - - -async def test_update_group_unique_id( - hass: HomeAssistant, entity_registry: er.EntityRegistry -) -> None: - """Test successful migration of entry data.""" - old_unique_id = "123" - new_unique_id = "1234" - entry = MockConfigEntry( - domain=DECONZ_DOMAIN, - unique_id=new_unique_id, - data={ - CONF_API_KEY: "1", - CONF_HOST: "2", - CONF_GROUP_ID_BASE: old_unique_id, - CONF_PORT: "3", - }, - ) - entry.add_to_hass(hass) - - # Create entity entry to migrate to new unique ID - entity_registry.async_get_or_create( - LIGHT_DOMAIN, - DECONZ_DOMAIN, - f"{old_unique_id}-OLD", - suggested_object_id="old", - config_entry=entry, - ) - # Create entity entry with new unique ID - entity_registry.async_get_or_create( - LIGHT_DOMAIN, - DECONZ_DOMAIN, - f"{new_unique_id}-NEW", - suggested_object_id="new", - config_entry=entry, - ) - - await async_update_group_unique_id(hass, entry) - - assert entry.data == {CONF_API_KEY: "1", CONF_HOST: "2", CONF_PORT: "3"} - assert ( - entity_registry.async_get(f"{LIGHT_DOMAIN}.old").unique_id - == f"{new_unique_id}-OLD" - ) - assert ( - entity_registry.async_get(f"{LIGHT_DOMAIN}.new").unique_id - == f"{new_unique_id}-NEW" - ) - - -async def test_update_group_unique_id_no_legacy_group_id( - hass: HomeAssistant, entity_registry: er.EntityRegistry -) -> None: - """Test migration doesn't trigger without old legacy group id in entry data.""" - old_unique_id = "123" - new_unique_id = "1234" - entry = MockConfigEntry( - domain=DECONZ_DOMAIN, - unique_id=new_unique_id, - data={}, - ) - - # Create entity entry to migrate to new unique ID - entity_registry.async_get_or_create( - LIGHT_DOMAIN, - DECONZ_DOMAIN, - f"{old_unique_id}-OLD", - suggested_object_id="old", - config_entry=entry, - ) - - await async_update_group_unique_id(hass, entry) - - assert ( - entity_registry.async_get(f"{LIGHT_DOMAIN}.old").unique_id - == f"{old_unique_id}-OLD" - ) diff --git a/tests/components/deconz/test_light.py b/tests/components/deconz/test_light.py index 07e284d65f2..5144f222484 100644 --- a/tests/components/deconz/test_light.py +++ b/tests/components/deconz/test_light.py @@ -1,4 +1,5 @@ """deCONZ light platform tests.""" + from unittest.mock import patch import pytest @@ -1321,12 +1322,10 @@ async def test_non_color_light_reports_color( await mock_deconz_websocket(data=event_changed_light) await hass.async_block_till_done() - assert hass.states.get("light.group").attributes[ATTR_COLOR_MODE] == ColorMode.XY - # Bug is fixed if we reach this point - # device won't have neither color temp nor color - with pytest.raises(AssertionError): - assert hass.states.get("light.group").attributes.get(ATTR_COLOR_TEMP) is None - assert hass.states.get("light.group").attributes.get(ATTR_HS_COLOR) is None + group = hass.states.get("light.group") + assert group.attributes[ATTR_COLOR_MODE] == ColorMode.XY + assert group.attributes[ATTR_HS_COLOR] == (40.571, 41.176) + assert group.attributes.get(ATTR_COLOR_TEMP) is None async def test_verify_group_supported_features( diff --git a/tests/components/deconz/test_lock.py b/tests/components/deconz/test_lock.py index 16879d48631..03d14802083 100644 --- a/tests/components/deconz/test_lock.py +++ b/tests/components/deconz/test_lock.py @@ -1,4 +1,5 @@ """deCONZ lock platform tests.""" + from unittest.mock import patch from homeassistant.components.lock import ( diff --git a/tests/components/deconz/test_logbook.py b/tests/components/deconz/test_logbook.py index 4d2043923bd..5940d2e8e34 100644 --- a/tests/components/deconz/test_logbook.py +++ b/tests/components/deconz/test_logbook.py @@ -1,4 +1,5 @@ """The tests for deCONZ logbook.""" + from unittest.mock import patch from homeassistant.components.deconz.const import CONF_GESTURE, DOMAIN as DECONZ_DOMAIN @@ -74,6 +75,7 @@ async def test_humanifying_deconz_alarm_event( hass.config.components.add("recorder") assert await async_setup_component(hass, "logbook", {}) + await hass.async_block_till_done() events = mock_humanify( hass, @@ -183,6 +185,7 @@ async def test_humanifying_deconz_event( hass.config.components.add("recorder") assert await async_setup_component(hass, "logbook", {}) + await hass.async_block_till_done() events = mock_humanify( hass, diff --git a/tests/components/deconz/test_number.py b/tests/components/deconz/test_number.py index 17cbc2917ec..3f86182e032 100644 --- a/tests/components/deconz/test_number.py +++ b/tests/components/deconz/test_number.py @@ -1,9 +1,9 @@ """deCONZ number platform tests.""" + from unittest.mock import patch import pytest -from homeassistant.components.deconz.const import DOMAIN as DECONZ_DOMAIN from homeassistant.components.number import ( ATTR_VALUE, DOMAIN as NUMBER_DOMAIN, @@ -49,7 +49,6 @@ TEST_DATA = [ "device_count": 3, "entity_id": "number.presence_sensor_delay", "unique_id": "00:00:00:00:00:00:00:00-00-delay", - "old_unique_id": "00:00:00:00:00:00:00:00-delay", "state": "0", "entity_category": EntityCategory.CONFIG, "attributes": { @@ -119,15 +118,6 @@ async def test_number_entities( ) -> None: """Test successful creation of number entities.""" - # Create entity entry to migrate to new unique ID - if "old_unique_id" in expected: - entity_registry.async_get_or_create( - NUMBER_DOMAIN, - DECONZ_DOMAIN, - expected["old_unique_id"], - suggested_object_id=expected["entity_id"].replace("number.", ""), - ) - with patch.dict(DECONZ_WEB_REQUEST, {"sensors": {"0": sensor_data}}): config_entry = await setup_deconz_integration(hass, aioclient_mock) diff --git a/tests/components/deconz/test_scene.py b/tests/components/deconz/test_scene.py index 7d16f0bd513..2bace605db5 100644 --- a/tests/components/deconz/test_scene.py +++ b/tests/components/deconz/test_scene.py @@ -1,4 +1,5 @@ """deCONZ scene platform tests.""" + from unittest.mock import patch import pytest diff --git a/tests/components/deconz/test_select.py b/tests/components/deconz/test_select.py index 7b7a9c86168..fb8f41293a2 100644 --- a/tests/components/deconz/test_select.py +++ b/tests/components/deconz/test_select.py @@ -1,4 +1,5 @@ """deCONZ select platform tests.""" + from unittest.mock import patch from pydeconz.models.sensor.presence import ( diff --git a/tests/components/deconz/test_sensor.py b/tests/components/deconz/test_sensor.py index 38d68d135b6..4950928f2e6 100644 --- a/tests/components/deconz/test_sensor.py +++ b/tests/components/deconz/test_sensor.py @@ -1,13 +1,11 @@ """deCONZ sensor platform tests.""" + from datetime import timedelta from unittest.mock import patch import pytest -from homeassistant.components.deconz.const import ( - CONF_ALLOW_CLIP_SENSOR, - DOMAIN as DECONZ_DOMAIN, -) +from homeassistant.components.deconz.const import CONF_ALLOW_CLIP_SENSOR from homeassistant.components.sensor import ( DOMAIN as SENSOR_DOMAIN, SensorDeviceClass, @@ -67,7 +65,6 @@ TEST_DATA = [ "device_count": 3, "entity_id": "sensor.bosch_air_quality_sensor", "unique_id": "00:12:4b:00:14:4d:00:07-02-fdef-air_quality", - "old_unique_id": "00:12:4b:00:14:4d:00:07-02-fdef", "state": "poor", "entity_category": None, "device_class": None, @@ -105,7 +102,6 @@ TEST_DATA = [ "device_count": 3, "entity_id": "sensor.bosch_air_quality_sensor_ppb", "unique_id": "00:12:4b:00:14:4d:00:07-02-fdef-air_quality_ppb", - "old_unique_id": "00:12:4b:00:14:4d:00:07-ppb", "state": "809", "entity_category": None, "device_class": None, @@ -264,7 +260,6 @@ TEST_DATA = [ "device_count": 3, "entity_id": "sensor.fyrtur_block_out_roller_blind_battery", "unique_id": "00:0d:6f:ff:fe:01:23:45-01-0001-battery", - "old_unique_id": "00:0d:6f:ff:fe:01:23:45-battery", "state": "100", "entity_category": EntityCategory.DIAGNOSTIC, "device_class": SensorDeviceClass.BATTERY, @@ -301,7 +296,6 @@ TEST_DATA = [ "device_count": 3, "entity_id": "sensor.consumption_15", "unique_id": "00:0d:6f:00:0b:7a:64:29-01-0702-consumption", - "old_unique_id": "00:0d:6f:00:0b:7a:64:29-01-0702", "state": "11.342", "entity_category": None, "device_class": SensorDeviceClass.ENERGY, @@ -383,7 +377,6 @@ TEST_DATA = [ "device_count": 2, "entity_id": "sensor.fsm_state_motion_stair", "unique_id": "fsm-state-1520195376277-status", - "old_unique_id": "fsm-state-1520195376277", "state": "0", "entity_category": None, "device_class": None, @@ -422,7 +415,6 @@ TEST_DATA = [ "device_count": 3, "entity_id": "sensor.mi_temperature_1", "unique_id": "00:15:8d:00:02:45:dc:53-01-0405-humidity", - "old_unique_id": "00:15:8d:00:02:45:dc:53-01-0405", "state": "35.55", "entity_category": None, "device_class": SensorDeviceClass.HUMIDITY, @@ -512,7 +504,6 @@ TEST_DATA = [ "device_count": 3, "entity_id": "sensor.motion_sensor_4", "unique_id": "00:17:88:01:03:28:8c:9b-02-0400-light_level", - "old_unique_id": "00:17:88:01:03:28:8c:9b-02-0400", "state": "5.0", "entity_category": None, "device_class": SensorDeviceClass.ILLUMINANCE, @@ -604,7 +595,6 @@ TEST_DATA = [ "device_count": 3, "entity_id": "sensor.power_16", "unique_id": "00:0d:6f:00:0b:7a:64:29-01-0b04-power", - "old_unique_id": "00:0d:6f:00:0b:7a:64:29-01-0b04", "state": "64", "entity_category": None, "device_class": SensorDeviceClass.POWER, @@ -647,7 +637,6 @@ TEST_DATA = [ "device_count": 3, "entity_id": "sensor.mi_temperature_1", "unique_id": "00:15:8d:00:02:45:dc:53-01-0403-pressure", - "old_unique_id": "00:15:8d:00:02:45:dc:53-01-0403", "state": "1010", "entity_category": None, "device_class": SensorDeviceClass.PRESSURE, @@ -689,7 +678,6 @@ TEST_DATA = [ "device_count": 3, "entity_id": "sensor.mi_temperature_1", "unique_id": "00:15:8d:00:02:45:dc:53-01-0402-temperature", - "old_unique_id": "00:15:8d:00:02:45:dc:53-01-0402", "state": "21.82", "entity_category": None, "device_class": SensorDeviceClass.TEMPERATURE, @@ -736,7 +724,6 @@ TEST_DATA = [ "device_count": 3, "entity_id": "sensor.etrv_sejour", "unique_id": "cc:cc:cc:ff:fe:38:4d:b3-01-000a-last_set", - "old_unique_id": "cc:cc:cc:ff:fe:38:4d:b3-01-000a", "state": "2020-11-19T08:07:08+00:00", "entity_category": None, "device_class": SensorDeviceClass.TIMESTAMP, @@ -776,7 +763,6 @@ TEST_DATA = [ "device_count": 3, "entity_id": "sensor.alarm_10_temperature", "unique_id": "00:15:8d:00:02:b5:d1:80-01-0500-internal_temperature", - "old_unique_id": "00:15:8d:00:02:b5:d1:80-temperature", "state": "26.0", "entity_category": None, "device_class": SensorDeviceClass.TEMPERATURE, @@ -818,7 +804,6 @@ TEST_DATA = [ "device_count": 3, "entity_id": "sensor.dimmer_switch_3_battery", "unique_id": "00:17:88:01:02:0e:32:a3-02-fc00-battery", - "old_unique_id": "00:17:88:01:02:0e:32:a3-battery", "state": "90", "entity_category": EntityCategory.DIAGNOSTIC, "device_class": SensorDeviceClass.BATTERY, @@ -850,15 +835,6 @@ async def test_sensors( ) -> None: """Test successful creation of sensor entities.""" - # Create entity entry to migrate to new unique ID - if "old_unique_id" in expected: - entity_registry.async_get_or_create( - SENSOR_DOMAIN, - DECONZ_DOMAIN, - expected["old_unique_id"], - suggested_object_id=expected["entity_id"].replace("sensor.", ""), - ) - with patch.dict(DECONZ_WEB_REQUEST, {"sensors": {"1": sensor_data}}): config_entry = await setup_deconz_integration( hass, aioclient_mock, options={CONF_ALLOW_CLIP_SENSOR: True} diff --git a/tests/components/deconz/test_services.py b/tests/components/deconz/test_services.py index ade7aba2346..7cf55ae75c3 100644 --- a/tests/components/deconz/test_services.py +++ b/tests/components/deconz/test_services.py @@ -1,4 +1,5 @@ """deCONZ service tests.""" + from unittest.mock import patch import pytest diff --git a/tests/components/deconz/test_siren.py b/tests/components/deconz/test_siren.py index 2dfaa5090de..62ed1b732b8 100644 --- a/tests/components/deconz/test_siren.py +++ b/tests/components/deconz/test_siren.py @@ -1,4 +1,5 @@ """deCONZ switch platform tests.""" + from unittest.mock import patch from homeassistant.components.siren import ATTR_DURATION, DOMAIN as SIREN_DOMAIN diff --git a/tests/components/deconz/test_switch.py b/tests/components/deconz/test_switch.py index 31555a71011..9ef2382a2e2 100644 --- a/tests/components/deconz/test_switch.py +++ b/tests/components/deconz/test_switch.py @@ -1,4 +1,5 @@ """deCONZ switch platform tests.""" + from unittest.mock import patch from homeassistant.components.deconz.const import DOMAIN as DECONZ_DOMAIN diff --git a/tests/components/default_config/test_init.py b/tests/components/default_config/test_init.py index 20029fe3cdc..222b2b14673 100644 --- a/tests/components/default_config/test_init.py +++ b/tests/components/default_config/test_init.py @@ -1,4 +1,5 @@ """Test the default_config init.""" + from unittest.mock import patch import pytest @@ -17,9 +18,11 @@ def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None: @pytest.fixture(autouse=True) def mock_ssdp(): """Mock ssdp.""" - with patch("homeassistant.components.ssdp.Scanner.async_scan"), patch( - "homeassistant.components.ssdp.Server.async_start" - ), patch("homeassistant.components.ssdp.Server.async_stop"): + with ( + patch("homeassistant.components.ssdp.Scanner.async_scan"), + patch("homeassistant.components.ssdp.Server.async_start"), + patch("homeassistant.components.ssdp.Server.async_stop"), + ): yield diff --git a/tests/components/deluge/test_config_flow.py b/tests/components/deluge/test_config_flow.py index 12753f74caf..1e7cecd8850 100644 --- a/tests/components/deluge/test_config_flow.py +++ b/tests/components/deluge/test_config_flow.py @@ -1,4 +1,5 @@ """Test Deluge config flow.""" + from unittest.mock import patch import pytest @@ -17,8 +18,9 @@ from tests.common import MockConfigEntry @pytest.fixture(name="api") def mock_deluge_api(): """Mock an api.""" - with patch("deluge_client.client.DelugeRPCClient.connect"), patch( - "deluge_client.client.DelugeRPCClient._create_socket" + with ( + patch("deluge_client.client.DelugeRPCClient.connect"), + patch("deluge_client.client.DelugeRPCClient._create_socket"), ): yield @@ -26,19 +28,23 @@ def mock_deluge_api(): @pytest.fixture(name="conn_error") def mock_api_connection_error(): """Mock an api.""" - with patch( - "deluge_client.client.DelugeRPCClient.connect", - side_effect=ConnectionRefusedError("111: Connection refused"), - ), patch("deluge_client.client.DelugeRPCClient._create_socket"): + with ( + patch( + "deluge_client.client.DelugeRPCClient.connect", + side_effect=ConnectionRefusedError("111: Connection refused"), + ), + patch("deluge_client.client.DelugeRPCClient._create_socket"), + ): yield @pytest.fixture(name="unknown_error") def mock_api_unknown_error(): """Mock an api.""" - with patch( - "deluge_client.client.DelugeRPCClient.connect", side_effect=Exception - ), patch("deluge_client.client.DelugeRPCClient._create_socket"): + with ( + patch("deluge_client.client.DelugeRPCClient.connect", side_effect=Exception), + patch("deluge_client.client.DelugeRPCClient._create_socket"), + ): yield diff --git a/tests/components/demo/conftest.py b/tests/components/demo/conftest.py index 0e54587ba4e..731a33360d7 100644 --- a/tests/components/demo/conftest.py +++ b/tests/components/demo/conftest.py @@ -1,4 +1,5 @@ """demo conftest.""" + from unittest.mock import patch import pytest diff --git a/tests/components/demo/test_camera.py b/tests/components/demo/test_camera.py index 132e7bdb096..ea115e72f72 100644 --- a/tests/components/demo/test_camera.py +++ b/tests/components/demo/test_camera.py @@ -1,4 +1,5 @@ """The tests for local file camera component.""" + from unittest.mock import patch import pytest diff --git a/tests/components/demo/test_climate.py b/tests/components/demo/test_climate.py index 18992c0d0f4..ff18f9e6a4e 100644 --- a/tests/components/demo/test_climate.py +++ b/tests/components/demo/test_climate.py @@ -1,11 +1,11 @@ """The tests for the demo climate component.""" + from unittest.mock import patch import pytest import voluptuous as vol from homeassistant.components.climate import ( - ATTR_AUX_HEAT, ATTR_CURRENT_HUMIDITY, ATTR_CURRENT_TEMPERATURE, ATTR_FAN_MODE, @@ -24,7 +24,6 @@ from homeassistant.components.climate import ( DOMAIN, PRESET_AWAY, PRESET_ECO, - SERVICE_SET_AUX_HEAT, SERVICE_SET_FAN_MODE, SERVICE_SET_HUMIDITY, SERVICE_SET_HVAC_MODE, @@ -39,8 +38,6 @@ from homeassistant.const import ( ATTR_TEMPERATURE, SERVICE_TURN_OFF, SERVICE_TURN_ON, - STATE_OFF, - STATE_ON, Platform, ) from homeassistant.core import HomeAssistant @@ -77,10 +74,9 @@ def test_setup_params(hass: HomeAssistant) -> None: assert state.attributes.get(ATTR_TEMPERATURE) == 21 assert state.attributes.get(ATTR_CURRENT_TEMPERATURE) == 22 assert state.attributes.get(ATTR_FAN_MODE) == "on_high" - assert state.attributes.get(ATTR_HUMIDITY) == 67 - assert state.attributes.get(ATTR_CURRENT_HUMIDITY) == 54 + assert state.attributes.get(ATTR_HUMIDITY) == 67.4 + assert state.attributes.get(ATTR_CURRENT_HUMIDITY) == 54.2 assert state.attributes.get(ATTR_SWING_MODE) == "off" - assert state.attributes.get(ATTR_AUX_HEAT) == STATE_OFF assert state.attributes.get(ATTR_HVAC_MODES) == [ HVACMode.OFF, HVACMode.HEAT, @@ -223,7 +219,7 @@ async def test_set_temp_with_hvac_mode(hass: HomeAssistant) -> None: async def test_set_target_humidity_bad_attr(hass: HomeAssistant) -> None: """Test setting the target humidity without required attribute.""" state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get(ATTR_HUMIDITY) == 67 + assert state.attributes.get(ATTR_HUMIDITY) == 67.4 with pytest.raises(vol.Invalid): await hass.services.async_call( @@ -234,13 +230,13 @@ async def test_set_target_humidity_bad_attr(hass: HomeAssistant) -> None: ) state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get(ATTR_HUMIDITY) == 67 + assert state.attributes.get(ATTR_HUMIDITY) == 67.4 async def test_set_target_humidity(hass: HomeAssistant) -> None: """Test the setting of the target humidity.""" state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get(ATTR_HUMIDITY) == 67 + assert state.attributes.get(ATTR_HUMIDITY) == 67.4 await hass.services.async_call( DOMAIN, @@ -383,49 +379,6 @@ async def test_set_hold_mode_eco(hass: HomeAssistant) -> None: assert state.attributes.get(ATTR_PRESET_MODE) == PRESET_ECO -async def test_set_aux_heat_bad_attr(hass: HomeAssistant) -> None: - """Test setting the auxiliary heater without required attribute.""" - state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get(ATTR_AUX_HEAT) == STATE_OFF - - with pytest.raises(vol.Invalid): - await hass.services.async_call( - DOMAIN, - SERVICE_SET_AUX_HEAT, - {ATTR_ENTITY_ID: ENTITY_CLIMATE, ATTR_AUX_HEAT: None}, - blocking=True, - ) - - state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get(ATTR_AUX_HEAT) == STATE_OFF - - -async def test_set_aux_heat_on(hass: HomeAssistant) -> None: - """Test setting the axillary heater on/true.""" - await hass.services.async_call( - DOMAIN, - SERVICE_SET_AUX_HEAT, - {ATTR_ENTITY_ID: ENTITY_CLIMATE, ATTR_AUX_HEAT: True}, - blocking=True, - ) - - state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get(ATTR_AUX_HEAT) == STATE_ON - - -async def test_set_aux_heat_off(hass: HomeAssistant) -> None: - """Test setting the auxiliary heater off/false.""" - await hass.services.async_call( - DOMAIN, - SERVICE_SET_AUX_HEAT, - {ATTR_ENTITY_ID: ENTITY_CLIMATE, ATTR_AUX_HEAT: False}, - blocking=True, - ) - - state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get(ATTR_AUX_HEAT) == STATE_OFF - - async def test_turn_on(hass: HomeAssistant) -> None: """Test turn on device.""" await hass.services.async_call( diff --git a/tests/components/demo/test_cover.py b/tests/components/demo/test_cover.py index 3a8fbb02f80..9ea743a0a01 100644 --- a/tests/components/demo/test_cover.py +++ b/tests/components/demo/test_cover.py @@ -1,4 +1,5 @@ """The tests for the Demo cover platform.""" + from datetime import timedelta from unittest.mock import patch diff --git a/tests/components/demo/test_date.py b/tests/components/demo/test_date.py index d0208c8e6dd..5e0fc2c29cd 100644 --- a/tests/components/demo/test_date.py +++ b/tests/components/demo/test_date.py @@ -1,4 +1,5 @@ """The tests for the demo date component.""" + from unittest.mock import patch import pytest diff --git a/tests/components/demo/test_datetime.py b/tests/components/demo/test_datetime.py index 41ed6969df3..c1f88d7686b 100644 --- a/tests/components/demo/test_datetime.py +++ b/tests/components/demo/test_datetime.py @@ -1,4 +1,5 @@ """The tests for the demo datetime component.""" + from unittest.mock import patch import pytest diff --git a/tests/components/demo/test_fan.py b/tests/components/demo/test_fan.py index a3f607aee76..bd42ae3a953 100644 --- a/tests/components/demo/test_fan.py +++ b/tests/components/demo/test_fan.py @@ -1,4 +1,5 @@ """Test cases around the demo fan platform.""" + from unittest.mock import patch import pytest @@ -24,13 +25,12 @@ from homeassistant.setup import async_setup_component FULL_FAN_ENTITY_IDS = ["fan.living_room_fan", "fan.percentage_full_fan"] FANS_WITH_PRESET_MODE_ONLY = ["fan.preset_only_limited_fan"] -LIMITED_AND_FULL_FAN_ENTITY_IDS = FULL_FAN_ENTITY_IDS + [ +LIMITED_AND_FULL_FAN_ENTITY_IDS = [ + *FULL_FAN_ENTITY_IDS, "fan.ceiling_fan", "fan.percentage_limited_fan", ] -FANS_WITH_PRESET_MODES = FULL_FAN_ENTITY_IDS + [ - "fan.percentage_limited_fan", -] +FANS_WITH_PRESET_MODES = [*FULL_FAN_ENTITY_IDS, "fan.percentage_limited_fan"] PERCENTAGE_MODEL_FANS = ["fan.percentage_full_fan", "fan.percentage_limited_fan"] diff --git a/tests/components/demo/test_geo_location.py b/tests/components/demo/test_geo_location.py index 8cd176934bc..d3c2937d12b 100644 --- a/tests/components/demo/test_geo_location.py +++ b/tests/components/demo/test_geo_location.py @@ -1,4 +1,5 @@ """The tests for the demo platform.""" + from freezegun import freeze_time from homeassistant.components import geo_location @@ -49,7 +50,7 @@ async def test_setup_platform(hass: HomeAssistant, disable_platforms) -> None: # Update (replaces 1 device). async_fire_time_changed(hass, utcnow + DEFAULT_UPDATE_INTERVAL) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) # Get all states again, ensure that the number of states is still # the same, but the lists are different. all_states_updated = [ diff --git a/tests/components/demo/test_humidifier.py b/tests/components/demo/test_humidifier.py index 97647f0a90f..0f0fcaf43fd 100644 --- a/tests/components/demo/test_humidifier.py +++ b/tests/components/demo/test_humidifier.py @@ -57,8 +57,8 @@ def test_setup_params(hass: HomeAssistant) -> None: """Test the initial parameters.""" state = hass.states.get(ENTITY_DEHUMIDIFIER) assert state.state == STATE_ON - assert state.attributes.get(ATTR_HUMIDITY) == 54 - assert state.attributes.get(ATTR_CURRENT_HUMIDITY) == 59 + assert state.attributes.get(ATTR_HUMIDITY) == 54.2 + assert state.attributes.get(ATTR_CURRENT_HUMIDITY) == 59.4 assert state.attributes.get(ATTR_ACTION) == "drying" @@ -72,7 +72,7 @@ def test_default_setup_params(hass: HomeAssistant) -> None: async def test_set_target_humidity_bad_attr(hass: HomeAssistant) -> None: """Test setting the target humidity without required attribute.""" state = hass.states.get(ENTITY_DEHUMIDIFIER) - assert state.attributes.get(ATTR_HUMIDITY) == 54 + assert state.attributes.get(ATTR_HUMIDITY) == 54.2 with pytest.raises(vol.Invalid): await hass.services.async_call( @@ -84,13 +84,13 @@ async def test_set_target_humidity_bad_attr(hass: HomeAssistant) -> None: await hass.async_block_till_done() state = hass.states.get(ENTITY_DEHUMIDIFIER) - assert state.attributes.get(ATTR_HUMIDITY) == 54 + assert state.attributes.get(ATTR_HUMIDITY) == 54.2 async def test_set_target_humidity(hass: HomeAssistant) -> None: """Test the setting of the target humidity.""" state = hass.states.get(ENTITY_DEHUMIDIFIER) - assert state.attributes.get(ATTR_HUMIDITY) == 54 + assert state.attributes.get(ATTR_HUMIDITY) == 54.2 await hass.services.async_call( DOMAIN, diff --git a/tests/components/demo/test_init.py b/tests/components/demo/test_init.py index fdf72c7fd34..05532d7503b 100644 --- a/tests/components/demo/test_init.py +++ b/tests/components/demo/test_init.py @@ -1,4 +1,5 @@ """The tests for the Demo component.""" + import json from unittest.mock import patch diff --git a/tests/components/demo/test_light.py b/tests/components/demo/test_light.py index 90fa26885dc..b67acf3f60f 100644 --- a/tests/components/demo/test_light.py +++ b/tests/components/demo/test_light.py @@ -1,4 +1,5 @@ """The tests for the demo light component.""" + from unittest.mock import patch import pytest diff --git a/tests/components/demo/test_lock.py b/tests/components/demo/test_lock.py index f72f5b01c19..634eee44385 100644 --- a/tests/components/demo/test_lock.py +++ b/tests/components/demo/test_lock.py @@ -1,4 +1,5 @@ """The tests for the Demo lock platform.""" + from unittest.mock import patch import pytest diff --git a/tests/components/demo/test_media_player.py b/tests/components/demo/test_media_player.py index b1bd77a74a1..6bc4c7a980b 100644 --- a/tests/components/demo/test_media_player.py +++ b/tests/components/demo/test_media_player.py @@ -1,4 +1,5 @@ """The tests for the Demo Media player platform.""" + from http import HTTPStatus from unittest.mock import patch diff --git a/tests/components/demo/test_notify.py b/tests/components/demo/test_notify.py index 96ffcdec96b..54eadc3bd91 100644 --- a/tests/components/demo/test_notify.py +++ b/tests/components/demo/test_notify.py @@ -1,4 +1,5 @@ """The tests for the notify demo platform.""" + import logging from unittest.mock import patch diff --git a/tests/components/demo/test_number.py b/tests/components/demo/test_number.py index f444b2f4831..3c41b98a3fa 100644 --- a/tests/components/demo/test_number.py +++ b/tests/components/demo/test_number.py @@ -1,4 +1,5 @@ """The tests for the demo number component.""" + from unittest.mock import patch import pytest diff --git a/tests/components/demo/test_remote.py b/tests/components/demo/test_remote.py index 5fafffae372..e2a82248fdf 100644 --- a/tests/components/demo/test_remote.py +++ b/tests/components/demo/test_remote.py @@ -1,4 +1,5 @@ """The tests for the demo remote component.""" + from unittest.mock import patch import pytest diff --git a/tests/components/demo/test_select.py b/tests/components/demo/test_select.py index 013a9900a83..f9805f44866 100644 --- a/tests/components/demo/test_select.py +++ b/tests/components/demo/test_select.py @@ -1,4 +1,5 @@ """The tests for the demo select component.""" + from unittest.mock import patch import pytest diff --git a/tests/components/demo/test_sensor.py b/tests/components/demo/test_sensor.py index 0fbe8f3fa7f..b017035caf1 100644 --- a/tests/components/demo/test_sensor.py +++ b/tests/components/demo/test_sensor.py @@ -1,4 +1,5 @@ """The tests for the demo sensor component.""" + from datetime import timedelta from unittest.mock import patch @@ -25,7 +26,7 @@ async def sensor_only() -> None: yield -@pytest.mark.parametrize(("entity_id", "delta"), (("sensor.total_energy_kwh", 0.5),)) +@pytest.mark.parametrize(("entity_id", "delta"), [("sensor.total_energy_kwh", 0.5)]) async def test_energy_sensor( hass: HomeAssistant, entity_id, delta, freezer: FrozenDateTimeFactory ) -> None: @@ -46,7 +47,7 @@ async def test_energy_sensor( assert state.state == str(delta) -@pytest.mark.parametrize(("entity_id", "delta"), (("sensor.total_energy_kwh", 0.5),)) +@pytest.mark.parametrize(("entity_id", "delta"), [("sensor.total_energy_kwh", 0.5)]) async def test_restore_state( hass: HomeAssistant, entity_id, delta, freezer: FrozenDateTimeFactory ) -> None: diff --git a/tests/components/demo/test_siren.py b/tests/components/demo/test_siren.py index 1434248599c..e21cd96efc9 100644 --- a/tests/components/demo/test_siren.py +++ b/tests/components/demo/test_siren.py @@ -1,4 +1,5 @@ """The tests for the demo siren component.""" + from unittest.mock import call, patch import pytest diff --git a/tests/components/demo/test_stt.py b/tests/components/demo/test_stt.py index 6ce25135ae0..dccdddd84e8 100644 --- a/tests/components/demo/test_stt.py +++ b/tests/components/demo/test_stt.py @@ -1,4 +1,5 @@ """The tests for the demo stt component.""" + from http import HTTPStatus from unittest.mock import patch diff --git a/tests/components/demo/test_switch.py b/tests/components/demo/test_switch.py index 95963ba0cbd..d8c3284875e 100644 --- a/tests/components/demo/test_switch.py +++ b/tests/components/demo/test_switch.py @@ -1,4 +1,5 @@ """The tests for the demo switch component.""" + from unittest.mock import patch import pytest diff --git a/tests/components/demo/test_text.py b/tests/components/demo/test_text.py index 2b8d8d122a3..faf611d9875 100644 --- a/tests/components/demo/test_text.py +++ b/tests/components/demo/test_text.py @@ -1,4 +1,5 @@ """The tests for the demo text component.""" + from unittest.mock import patch import pytest diff --git a/tests/components/demo/test_time.py b/tests/components/demo/test_time.py index efa62c1436b..8ef093a38f3 100644 --- a/tests/components/demo/test_time.py +++ b/tests/components/demo/test_time.py @@ -1,4 +1,5 @@ """The tests for the demo time component.""" + from unittest.mock import patch import pytest diff --git a/tests/components/demo/test_update.py b/tests/components/demo/test_update.py index a645c45019c..d8af9c21c75 100644 --- a/tests/components/demo/test_update.py +++ b/tests/components/demo/test_update.py @@ -1,4 +1,5 @@ """The tests for the demo update platform.""" + from unittest.mock import patch import pytest @@ -173,10 +174,13 @@ async def test_update_with_progress_raising(hass: HomeAssistant) -> None: callback(lambda event: events.append(event)), ) - with patch( - "homeassistant.components.demo.update._fake_install", - side_effect=[None, None, None, None, RuntimeError], - ) as fake_sleep, pytest.raises(RuntimeError): + with ( + patch( + "homeassistant.components.demo.update._fake_install", + side_effect=[None, None, None, None, RuntimeError], + ) as fake_sleep, + pytest.raises(RuntimeError), + ): await hass.services.async_call( DOMAIN, SERVICE_INSTALL, diff --git a/tests/components/demo/test_vacuum.py b/tests/components/demo/test_vacuum.py index 987bc6b9384..e70f0144e6a 100644 --- a/tests/components/demo/test_vacuum.py +++ b/tests/components/demo/test_vacuum.py @@ -1,4 +1,5 @@ """The tests for the Demo vacuum platform.""" + from datetime import timedelta from unittest.mock import patch diff --git a/tests/components/demo/test_water_heater.py b/tests/components/demo/test_water_heater.py index 6b133297e34..48859610d39 100644 --- a/tests/components/demo/test_water_heater.py +++ b/tests/components/demo/test_water_heater.py @@ -1,4 +1,5 @@ """The tests for the demo water_heater component.""" + from unittest.mock import patch import pytest diff --git a/tests/components/demo/test_weather.py b/tests/components/demo/test_weather.py index bb91535192c..403b8b1d346 100644 --- a/tests/components/demo/test_weather.py +++ b/tests/components/demo/test_weather.py @@ -1,4 +1,5 @@ """The tests for the demo weather component.""" + import datetime from typing import Any from unittest.mock import patch diff --git a/tests/components/denonavr/test_config_flow.py b/tests/components/denonavr/test_config_flow.py index a0fb908d920..5f5a5c8f17c 100644 --- a/tests/components/denonavr/test_config_flow.py +++ b/tests/components/denonavr/test_config_flow.py @@ -1,4 +1,5 @@ """Test the DenonAVR config flow.""" + from unittest.mock import patch import pytest @@ -40,33 +41,43 @@ TEST_DISCOVER_2_RECEIVER = [{CONF_HOST: TEST_HOST}, {CONF_HOST: TEST_HOST2}] @pytest.fixture(name="denonavr_connect", autouse=True) def denonavr_connect_fixture(): """Mock denonavr connection and entry setup.""" - with patch( - "homeassistant.components.denonavr.receiver.DenonAVR.async_setup", - return_value=None, - ), patch( - "homeassistant.components.denonavr.receiver.DenonAVR.async_update", - return_value=None, - ), patch( - "homeassistant.components.denonavr.receiver.DenonAVR.support_sound_mode", - return_value=True, - ), patch( - "homeassistant.components.denonavr.receiver.DenonAVR.name", - TEST_NAME, - ), patch( - "homeassistant.components.denonavr.receiver.DenonAVR.model_name", - TEST_MODEL, - ), patch( - "homeassistant.components.denonavr.receiver.DenonAVR.serial_number", - TEST_SERIALNUMBER, - ), patch( - "homeassistant.components.denonavr.receiver.DenonAVR.manufacturer", - TEST_MANUFACTURER, - ), patch( - "homeassistant.components.denonavr.receiver.DenonAVR.receiver_type", - TEST_RECEIVER_TYPE, - ), patch( - "homeassistant.components.denonavr.async_setup_entry", - return_value=True, + with ( + patch( + "homeassistant.components.denonavr.receiver.DenonAVR.async_setup", + return_value=None, + ), + patch( + "homeassistant.components.denonavr.receiver.DenonAVR.async_update", + return_value=None, + ), + patch( + "homeassistant.components.denonavr.receiver.DenonAVR.support_sound_mode", + return_value=True, + ), + patch( + "homeassistant.components.denonavr.receiver.DenonAVR.name", + TEST_NAME, + ), + patch( + "homeassistant.components.denonavr.receiver.DenonAVR.model_name", + TEST_MODEL, + ), + patch( + "homeassistant.components.denonavr.receiver.DenonAVR.serial_number", + TEST_SERIALNUMBER, + ), + patch( + "homeassistant.components.denonavr.receiver.DenonAVR.manufacturer", + TEST_MANUFACTURER, + ), + patch( + "homeassistant.components.denonavr.receiver.DenonAVR.receiver_type", + TEST_RECEIVER_TYPE, + ), + patch( + "homeassistant.components.denonavr.async_setup_entry", + return_value=True, + ), ): yield @@ -251,12 +262,15 @@ async def test_config_flow_manual_host_connection_error(hass: HomeAssistant) -> assert result["step_id"] == "user" assert result["errors"] == {} - with patch( - "homeassistant.components.denonavr.receiver.DenonAVR.async_setup", - side_effect=AvrTimoutError("Timeout", "async_setup"), - ), patch( - "homeassistant.components.denonavr.receiver.DenonAVR.receiver_type", - None, + with ( + patch( + "homeassistant.components.denonavr.receiver.DenonAVR.async_setup", + side_effect=AvrTimoutError("Timeout", "async_setup"), + ), + patch( + "homeassistant.components.denonavr.receiver.DenonAVR.receiver_type", + None, + ), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], diff --git a/tests/components/denonavr/test_media_player.py b/tests/components/denonavr/test_media_player.py index 85cbf91d850..c294c449518 100644 --- a/tests/components/denonavr/test_media_player.py +++ b/tests/components/denonavr/test_media_player.py @@ -1,4 +1,5 @@ """The tests for the denonavr media player platform.""" + from unittest.mock import patch import pytest @@ -40,11 +41,12 @@ ENTITY_ID = f"{media_player.DOMAIN}.{TEST_NAME}" @pytest.fixture(name="client") def client_fixture(): """Patch of client library for tests.""" - with patch( - "homeassistant.components.denonavr.receiver.DenonAVR", - autospec=True, - ) as mock_client_class, patch( - "homeassistant.components.denonavr.config_flow.denonavr.async_discover" + with ( + patch( + "homeassistant.components.denonavr.receiver.DenonAVR", + autospec=True, + ) as mock_client_class, + patch("homeassistant.components.denonavr.config_flow.denonavr.async_discover"), ): mock_client_class.return_value.name = TEST_NAME mock_client_class.return_value.model_name = TEST_MODEL diff --git a/tests/components/derivative/test_config_flow.py b/tests/components/derivative/test_config_flow.py index bc440723df2..9002a201f85 100644 --- a/tests/components/derivative/test_config_flow.py +++ b/tests/components/derivative/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Derivative config flow.""" + from unittest.mock import patch import pytest @@ -11,7 +12,7 @@ from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry -@pytest.mark.parametrize("platform", ("sensor",)) +@pytest.mark.parametrize("platform", ["sensor"]) async def test_config_flow(hass: HomeAssistant, platform) -> None: """Test the config flow.""" input_sensor_entity_id = "sensor.input" @@ -73,7 +74,7 @@ def get_suggested(schema, key): raise Exception -@pytest.mark.parametrize("platform", ("sensor",)) +@pytest.mark.parametrize("platform", ["sensor"]) async def test_options(hass: HomeAssistant, platform) -> None: """Test reconfiguring.""" # Setup the config entry diff --git a/tests/components/derivative/test_init.py b/tests/components/derivative/test_init.py index eab8ca67be7..34fe385032b 100644 --- a/tests/components/derivative/test_init.py +++ b/tests/components/derivative/test_init.py @@ -1,4 +1,5 @@ """Test the Derivative integration.""" + import pytest from homeassistant.components.derivative.const import DOMAIN @@ -8,7 +9,7 @@ from homeassistant.helpers import entity_registry as er from tests.common import MockConfigEntry -@pytest.mark.parametrize("platform", ("sensor",)) +@pytest.mark.parametrize("platform", ["sensor"]) async def test_setup_and_remove_config_entry( hass: HomeAssistant, entity_registry: er.EntityRegistry, diff --git a/tests/components/derivative/test_sensor.py b/tests/components/derivative/test_sensor.py index 4d954fcbb43..e4f57437d24 100644 --- a/tests/components/derivative/test_sensor.py +++ b/tests/components/derivative/test_sensor.py @@ -1,4 +1,5 @@ """The tests for the derivative sensor platform.""" + from datetime import timedelta from math import sin import random @@ -237,11 +238,7 @@ async def test_double_signal_after_delay(hass: HomeAssistant) -> None: # The old algorithm would produce extreme values if, after a delay longer than the time window # there would be two signals, a large spike would be produced. Check explicitly for this situation time_window = 60 - times = [*range(time_window * 10)] - times = times + [ - time_window * 20, - time_window * 20 + 0.01, - ] + times = [*range(time_window * 10), time_window * 20, time_window * 20 + 0.01] # just apply sine as some sort of temperature change and make sure the change after the delay is very small temperature_values = [sin(x) for x in times] diff --git a/tests/components/devialet/test_config_flow.py b/tests/components/devialet/test_config_flow.py index 0bacc558b74..05174b50f0d 100644 --- a/tests/components/devialet/test_config_flow.py +++ b/tests/components/devialet/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Devialet config flow.""" + from unittest.mock import patch from aiohttp import ClientError as HTTPClientError diff --git a/tests/components/devialet/test_diagnostics.py b/tests/components/devialet/test_diagnostics.py index 82600de7cf5..97c23efe713 100644 --- a/tests/components/devialet/test_diagnostics.py +++ b/tests/components/devialet/test_diagnostics.py @@ -1,4 +1,5 @@ """Test the Devialet diagnostics.""" + import json from homeassistant.core import HomeAssistant diff --git a/tests/components/devialet/test_init.py b/tests/components/devialet/test_init.py index 86d383e91d8..a87e8ac05c3 100644 --- a/tests/components/devialet/test_init.py +++ b/tests/components/devialet/test_init.py @@ -1,4 +1,5 @@ """Test the Devialet init.""" + from homeassistant.components.devialet.const import DOMAIN from homeassistant.components.media_player import DOMAIN as MP_DOMAIN, MediaPlayerState from homeassistant.config_entries import ConfigEntryState diff --git a/tests/components/devialet/test_media_player.py b/tests/components/devialet/test_media_player.py index 56381bf6de4..4e8f9b1dc03 100644 --- a/tests/components/devialet/test_media_player.py +++ b/tests/components/devialet/test_media_player.py @@ -1,4 +1,5 @@ """Test the Devialet init.""" + from unittest.mock import PropertyMock, patch from devialet import DevialetApi diff --git a/tests/components/device_automation/test_init.py b/tests/components/device_automation/test_init.py index 9a7d54fb690..1a4488e43cd 100644 --- a/tests/components/device_automation/test_init.py +++ b/tests/components/device_automation/test_init.py @@ -1,4 +1,5 @@ """The test for light device automation.""" + from unittest.mock import AsyncMock, Mock, patch import attr @@ -1275,11 +1276,9 @@ BAD_AUTOMATIONS = [ ), ] -BAD_TRIGGERS = BAD_CONDITIONS = BAD_AUTOMATIONS + [ - ( - {"domain": "light"}, - "required key not provided @ data{path}['device_id']", - ) +BAD_TRIGGERS = BAD_CONDITIONS = [ + *BAD_AUTOMATIONS, + ({"domain": "light"}, "required key not provided @ data{path}['device_id']"), ] @@ -1728,10 +1727,13 @@ async def test_async_get_device_automations_platform_reraises_exceptions( ) -> None: """Test InvalidDeviceAutomationConfig is raised when async_get_integration_with_requirements fails.""" await async_setup_component(hass, "device_automation", {}) - with patch( - "homeassistant.components.device_automation.async_get_integration_with_requirements", - side_effect=exc, - ), pytest.raises(InvalidDeviceAutomationConfig): + with ( + patch( + "homeassistant.components.device_automation.async_get_integration_with_requirements", + side_effect=exc, + ), + pytest.raises(InvalidDeviceAutomationConfig), + ): await device_automation.async_get_device_automation_platform( hass, "test", device_automation.DeviceAutomationType.TRIGGER ) diff --git a/tests/components/device_automation/test_toggle_entity.py b/tests/components/device_automation/test_toggle_entity.py index 30c9e5b542e..59d316545fa 100644 --- a/tests/components/device_automation/test_toggle_entity.py +++ b/tests/components/device_automation/test_toggle_entity.py @@ -1,4 +1,5 @@ """The test for device automation toggle entity helpers.""" + from datetime import timedelta import pytest @@ -213,6 +214,7 @@ async def test_if_fires_on_state_change_with_for( await hass.async_block_till_done() assert len(calls) == 1 await hass.async_block_till_done() - assert calls[0].data["some"] == "turn_off device - {} - on - off - 0:00:05".format( - entry.entity_id + assert ( + calls[0].data["some"] + == f"turn_off device - {entry.entity_id} - on - off - 0:00:05" ) diff --git a/tests/components/device_sun_light_trigger/test_init.py b/tests/components/device_sun_light_trigger/test_init.py index 3831d247ed4..570708cec79 100644 --- a/tests/components/device_sun_light_trigger/test_init.py +++ b/tests/components/device_sun_light_trigger/test_init.py @@ -1,4 +1,5 @@ """The tests device sun light trigger component.""" + from datetime import datetime from unittest.mock import patch diff --git a/tests/components/device_tracker/common.py b/tests/components/device_tracker/common.py index dfb3f9e8462..973eb7d8820 100644 --- a/tests/components/device_tracker/common.py +++ b/tests/components/device_tracker/common.py @@ -3,6 +3,7 @@ All containing methods are legacy helpers that should not be used by new components. Instead call the service directly. """ + from homeassistant.components.device_tracker import ( ATTR_ATTRIBUTES, ATTR_BATTERY, @@ -49,4 +50,4 @@ def async_see( } if attributes: data[ATTR_ATTRIBUTES] = attributes - hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_SEE, data)) + hass.async_create_task(hass.services.async_call(DOMAIN, SERVICE_SEE, data)) diff --git a/tests/components/device_tracker/test_config_entry.py b/tests/components/device_tracker/test_config_entry.py index ba258af068e..d8236c697c3 100644 --- a/tests/components/device_tracker/test_config_entry.py +++ b/tests/components/device_tracker/test_config_entry.py @@ -1,4 +1,5 @@ """Test Device Tracker config entry things.""" + from collections.abc import Generator from typing import Any diff --git a/tests/components/device_tracker/test_device_condition.py b/tests/components/device_tracker/test_device_condition.py index f550b803fda..431840d2f57 100644 --- a/tests/components/device_tracker/test_device_condition.py +++ b/tests/components/device_tracker/test_device_condition.py @@ -1,4 +1,5 @@ """The tests for Device tracker device conditions.""" + import pytest from pytest_unordered import unordered @@ -63,12 +64,12 @@ async def test_get_conditions( @pytest.mark.parametrize( ("hidden_by", "entity_category"), - ( + [ (RegistryEntryHider.INTEGRATION, None), (RegistryEntryHider.USER, None), (None, EntityCategory.CONFIG), (None, EntityCategory.DIAGNOSTIC), - ), + ], ) async def test_get_conditions_hidden_auxiliary( hass: HomeAssistant, diff --git a/tests/components/device_tracker/test_device_trigger.py b/tests/components/device_tracker/test_device_trigger.py index 3e19570ebcb..1bbe2394d8e 100644 --- a/tests/components/device_tracker/test_device_trigger.py +++ b/tests/components/device_tracker/test_device_trigger.py @@ -1,4 +1,5 @@ """The tests for Device Tracker device triggers.""" + import pytest from pytest_unordered import unordered import voluptuous_serialize @@ -95,12 +96,12 @@ async def test_get_triggers( @pytest.mark.parametrize( ("hidden_by", "entity_category"), - ( + [ (RegistryEntryHider.INTEGRATION, None), (RegistryEntryHider.USER, None), (None, EntityCategory.CONFIG), (None, EntityCategory.DIAGNOSTIC), - ), + ], ) async def test_get_triggers_hidden_auxiliary( hass: HomeAssistant, diff --git a/tests/components/device_tracker/test_init.py b/tests/components/device_tracker/test_init.py index eb8fde8f0e2..3b95fc9582c 100644 --- a/tests/components/device_tracker/test_init.py +++ b/tests/components/device_tracker/test_init.py @@ -1,4 +1,5 @@ """The tests for the device tracker component.""" + from datetime import datetime, timedelta import json import logging @@ -116,10 +117,21 @@ async def test_reading_yaml_config( await hass.async_add_executor_job( legacy.update_config, yaml_devices, dev_id, device ) - assert await async_setup_component(hass, device_tracker.DOMAIN, TEST_PLATFORM) - config = (await legacy.async_load_config(yaml_devices, hass, device.consider_home))[ - 0 - ] + loaded_config = None + original_async_load_config = legacy.async_load_config + + async def capture_load_config(*args, **kwargs): + nonlocal loaded_config + loaded_config = await original_async_load_config(*args, **kwargs) + return loaded_config + + with patch( + "homeassistant.components.device_tracker.legacy.async_load_config", + capture_load_config, + ): + assert await async_setup_component(hass, device_tracker.DOMAIN, TEST_PLATFORM) + await hass.async_block_till_done() + config = loaded_config[0] assert device.dev_id == config.dev_id assert device.track == config.track assert device.mac == config.mac @@ -222,6 +234,9 @@ async def test_discover_platform( ) -> None: """Test discovery of device_tracker demo platform.""" await async_setup_component(hass, "homeassistant", {}) + await async_setup_component(hass, device_tracker.DOMAIN, {}) + # async_block_till_done is intentionally missing here so we + # can verify async_load_platform still works without it with patch("homeassistant.components.device_tracker.legacy.update_config"): await discovery.async_load_platform( hass, device_tracker.DOMAIN, "demo", {"test_key": "test_val"}, {"bla": {}} @@ -237,6 +252,31 @@ async def test_discover_platform( ) +async def test_discover_platform_missing_platform( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test discovery of device_tracker missing platform.""" + await async_setup_component(hass, "homeassistant", {}) + await async_setup_component(hass, device_tracker.DOMAIN, {}) + # async_block_till_done is intentionally missing here so we + # can verify async_load_platform still works without it + with patch("homeassistant.components.device_tracker.legacy.update_config"): + await discovery.async_load_platform( + hass, + device_tracker.DOMAIN, + "its_not_there", + {"test_key": "test_val"}, + {"bla": {}}, + ) + await hass.async_block_till_done() + assert device_tracker.DOMAIN in hass.config.components + assert ( + "Unable to prepare setup for platform 'its_not_there.device_tracker'" + in caplog.text + ) + # This test should not generate an unhandled exception + + async def test_update_stale( hass: HomeAssistant, mock_device_tracker_conf: list[legacy.Device], @@ -252,10 +292,13 @@ async def test_update_stale( register_time = datetime(now.year + 1, 9, 15, 23, tzinfo=dt_util.UTC) scan_time = datetime(now.year + 1, 9, 15, 23, 1, tzinfo=dt_util.UTC) - with patch( - "homeassistant.components.device_tracker.legacy.dt_util.utcnow", - return_value=register_time, - ), assert_setup_component(1, device_tracker.DOMAIN): + with ( + patch( + "homeassistant.components.device_tracker.legacy.dt_util.utcnow", + return_value=register_time, + ), + assert_setup_component(1, device_tracker.DOMAIN), + ): assert await async_setup_component( hass, device_tracker.DOMAIN, @@ -309,6 +352,7 @@ async def test_entity_attributes( with assert_setup_component(1, device_tracker.DOMAIN): assert await async_setup_component(hass, device_tracker.DOMAIN, TEST_PLATFORM) + await hass.async_block_till_done() attrs = hass.states.get(entity_id).attributes @@ -324,6 +368,7 @@ async def test_see_service( """Test the see service with a unicode dev_id and NO MAC.""" with assert_setup_component(1, device_tracker.DOMAIN): assert await async_setup_component(hass, device_tracker.DOMAIN, TEST_PLATFORM) + await hass.async_block_till_done() params = { "dev_id": "some_device", "host_name": "example.com", @@ -359,6 +404,7 @@ async def test_see_service_guard_config_entry( mock_registry(hass, {entity_id: mock_entry}) devices = mock_device_tracker_conf assert await async_setup_component(hass, device_tracker.DOMAIN, TEST_PLATFORM) + await hass.async_block_till_done() params = {"dev_id": dev_id, "gps": [0.3, 0.8]} common.async_see(hass, **params) @@ -375,6 +421,7 @@ async def test_new_device_event_fired( """Test that the device tracker will fire an event.""" with assert_setup_component(1, device_tracker.DOMAIN): assert await async_setup_component(hass, device_tracker.DOMAIN, TEST_PLATFORM) + await hass.async_block_till_done() test_events = [] @callback @@ -410,6 +457,7 @@ async def test_duplicate_yaml_keys( devices = mock_device_tracker_conf with assert_setup_component(1, device_tracker.DOMAIN): assert await async_setup_component(hass, device_tracker.DOMAIN, TEST_PLATFORM) + await hass.async_block_till_done() common.async_see(hass, "mac_1", host_name="hello") common.async_see(hass, "mac_2", host_name="hello") @@ -429,6 +477,7 @@ async def test_invalid_dev_id( devices = mock_device_tracker_conf with assert_setup_component(1, device_tracker.DOMAIN): assert await async_setup_component(hass, device_tracker.DOMAIN, TEST_PLATFORM) + await hass.async_block_till_done() common.async_see(hass, dev_id="hello-world") await hass.async_block_till_done() @@ -441,6 +490,7 @@ async def test_see_state( ) -> None: """Test device tracker see records state correctly.""" assert await async_setup_component(hass, device_tracker.DOMAIN, TEST_PLATFORM) + await hass.async_block_till_done() params = { "mac": "AA:BB:CC:DD:EE:FF", @@ -495,15 +545,19 @@ async def test_see_passive_zone_state( } await async_setup_component(hass, zone.DOMAIN, {"zone": zone_info}) + await hass.async_block_till_done() scanner = getattr(hass.components, "test.device_tracker").SCANNER scanner.reset() scanner.come_home("dev1") - with patch( - "homeassistant.components.device_tracker.legacy.dt_util.utcnow", - return_value=register_time, - ), assert_setup_component(1, device_tracker.DOMAIN): + with ( + patch( + "homeassistant.components.device_tracker.legacy.dt_util.utcnow", + return_value=register_time, + ), + assert_setup_component(1, device_tracker.DOMAIN), + ): assert await async_setup_component( hass, device_tracker.DOMAIN, @@ -590,6 +644,7 @@ async def test_async_added_to_hass(hass: HomeAssistant) -> None: files = {path: "jk:\n name: JK Phone\n track: True"} with patch_yaml_files(files): assert await async_setup_component(hass, device_tracker.DOMAIN, {}) + await hass.async_block_till_done() state = hass.states.get("device_tracker.jk") assert state @@ -605,6 +660,7 @@ async def test_bad_platform(hass: HomeAssistant) -> None: config = {"device_tracker": [{"platform": "bad_platform"}]} with assert_setup_component(0, device_tracker.DOMAIN): assert await async_setup_component(hass, device_tracker.DOMAIN, config) + await hass.async_block_till_done() assert f"bad_platform.{device_tracker.DOMAIN}" not in hass.config.components diff --git a/tests/components/device_tracker/test_legacy.py b/tests/components/device_tracker/test_legacy.py index d7a2f33c23b..dba069c410b 100644 --- a/tests/components/device_tracker/test_legacy.py +++ b/tests/components/device_tracker/test_legacy.py @@ -1,4 +1,5 @@ """Tests for the legacy device tracker component.""" + from unittest.mock import mock_open, patch from homeassistant.components.device_tracker import legacy @@ -29,8 +30,9 @@ def test_remove_device_from_config(hass: HomeAssistant): mopen = mock_open() files = {legacy.YAML_DEVICES: dump(yaml_devices)} - with patch_yaml_files(files, True), patch( - "homeassistant.components.device_tracker.legacy.open", mopen + with ( + patch_yaml_files(files, True), + patch("homeassistant.components.device_tracker.legacy.open", mopen), ): legacy.remove_device_from_config(hass, "test") diff --git a/tests/components/devolo_home_control/__init__.py b/tests/components/devolo_home_control/__init__.py index a7217b0d530..f0e18eaf1a2 100644 --- a/tests/components/devolo_home_control/__init__.py +++ b/tests/components/devolo_home_control/__init__.py @@ -1,4 +1,5 @@ """Tests for the devolo_home_control integration.""" + from homeassistant.components.devolo_home_control.const import DOMAIN from homeassistant.core import HomeAssistant diff --git a/tests/components/devolo_home_control/conftest.py b/tests/components/devolo_home_control/conftest.py index 786652c7753..6ce9b73ff83 100644 --- a/tests/components/devolo_home_control/conftest.py +++ b/tests/components/devolo_home_control/conftest.py @@ -23,15 +23,19 @@ def patch_mydevolo( credentials_valid: bool, maintenance: bool ) -> Generator[None, None, None]: """Fixture to patch mydevolo into a desired state.""" - with patch( - "homeassistant.components.devolo_home_control.Mydevolo.credentials_valid", - return_value=credentials_valid, - ), patch( - "homeassistant.components.devolo_home_control.Mydevolo.maintenance", - return_value=maintenance, - ), patch( - "homeassistant.components.devolo_home_control.Mydevolo.get_gateway_ids", - return_value=["1400000000000001", "1400000000000002"], + with ( + patch( + "homeassistant.components.devolo_home_control.Mydevolo.credentials_valid", + return_value=credentials_valid, + ), + patch( + "homeassistant.components.devolo_home_control.Mydevolo.maintenance", + return_value=maintenance, + ), + patch( + "homeassistant.components.devolo_home_control.Mydevolo.get_gateway_ids", + return_value=["1400000000000001", "1400000000000002"], + ), ): yield diff --git a/tests/components/devolo_home_control/snapshots/test_binary_sensor.ambr b/tests/components/devolo_home_control/snapshots/test_binary_sensor.ambr index 851c1da4c62..0980a550c7b 100644 --- a/tests/components/devolo_home_control/snapshots/test_binary_sensor.ambr +++ b/tests/components/devolo_home_control/snapshots/test_binary_sensor.ambr @@ -8,6 +8,7 @@ 'context': , 'entity_id': 'binary_sensor.test_door', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }) @@ -54,6 +55,7 @@ 'context': , 'entity_id': 'binary_sensor.test_overload', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }) @@ -99,6 +101,7 @@ 'context': , 'entity_id': 'binary_sensor.test_button_1', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }) diff --git a/tests/components/devolo_home_control/snapshots/test_climate.ambr b/tests/components/devolo_home_control/snapshots/test_climate.ambr index bb6bd7d0f40..be7d6f78142 100644 --- a/tests/components/devolo_home_control/snapshots/test_climate.ambr +++ b/tests/components/devolo_home_control/snapshots/test_climate.ambr @@ -16,6 +16,7 @@ 'context': , 'entity_id': 'climate.test', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'heat', }) diff --git a/tests/components/devolo_home_control/snapshots/test_cover.ambr b/tests/components/devolo_home_control/snapshots/test_cover.ambr index 34a149c10c2..7d88d42d5c2 100644 --- a/tests/components/devolo_home_control/snapshots/test_cover.ambr +++ b/tests/components/devolo_home_control/snapshots/test_cover.ambr @@ -10,6 +10,7 @@ 'context': , 'entity_id': 'cover.test', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'open', }) diff --git a/tests/components/devolo_home_control/snapshots/test_light.ambr b/tests/components/devolo_home_control/snapshots/test_light.ambr index 7b156dd894b..959656b52a4 100644 --- a/tests/components/devolo_home_control/snapshots/test_light.ambr +++ b/tests/components/devolo_home_control/snapshots/test_light.ambr @@ -13,6 +13,7 @@ 'context': , 'entity_id': 'light.test', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }) @@ -68,6 +69,7 @@ 'context': , 'entity_id': 'light.test', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }) diff --git a/tests/components/devolo_home_control/snapshots/test_sensor.ambr b/tests/components/devolo_home_control/snapshots/test_sensor.ambr index 1f83877f14b..7f67c70f6ac 100644 --- a/tests/components/devolo_home_control/snapshots/test_sensor.ambr +++ b/tests/components/devolo_home_control/snapshots/test_sensor.ambr @@ -10,6 +10,7 @@ 'context': , 'entity_id': 'sensor.test_battery_level', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '25', }) @@ -60,6 +61,7 @@ 'context': , 'entity_id': 'sensor.test_current_consumption', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0.0', }) @@ -110,6 +112,7 @@ 'context': , 'entity_id': 'sensor.test_total_consumption', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0.0', }) @@ -160,6 +163,7 @@ 'context': , 'entity_id': 'sensor.test_temperature', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '20', }) diff --git a/tests/components/devolo_home_control/snapshots/test_siren.ambr b/tests/components/devolo_home_control/snapshots/test_siren.ambr index 82422e7f37f..5c94674998c 100644 --- a/tests/components/devolo_home_control/snapshots/test_siren.ambr +++ b/tests/components/devolo_home_control/snapshots/test_siren.ambr @@ -11,6 +11,7 @@ 'context': , 'entity_id': 'siren.test', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }) @@ -64,6 +65,7 @@ 'context': , 'entity_id': 'siren.test', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }) @@ -117,6 +119,7 @@ 'context': , 'entity_id': 'siren.test', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }) diff --git a/tests/components/devolo_home_control/snapshots/test_switch.ambr b/tests/components/devolo_home_control/snapshots/test_switch.ambr index e756fc26c9a..3e2f6f705d3 100644 --- a/tests/components/devolo_home_control/snapshots/test_switch.ambr +++ b/tests/components/devolo_home_control/snapshots/test_switch.ambr @@ -7,6 +7,7 @@ 'context': , 'entity_id': 'switch.test', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }) diff --git a/tests/components/devolo_home_control/test_binary_sensor.py b/tests/components/devolo_home_control/test_binary_sensor.py index ffb1794f006..e809c94c129 100644 --- a/tests/components/devolo_home_control/test_binary_sensor.py +++ b/tests/components/devolo_home_control/test_binary_sensor.py @@ -1,4 +1,5 @@ """Tests for the devolo Home Control binary sensors.""" + from unittest.mock import patch import pytest diff --git a/tests/components/devolo_home_control/test_climate.py b/tests/components/devolo_home_control/test_climate.py index 11d5b01ec5a..953ff835b89 100644 --- a/tests/components/devolo_home_control/test_climate.py +++ b/tests/components/devolo_home_control/test_climate.py @@ -1,4 +1,5 @@ """Tests for the devolo Home Control climate.""" + from unittest.mock import patch from syrupy.assertion import SnapshotAssertion diff --git a/tests/components/devolo_home_control/test_config_flow.py b/tests/components/devolo_home_control/test_config_flow.py index 1c04f0c83a6..1aa8e7f829d 100644 --- a/tests/components/devolo_home_control/test_config_flow.py +++ b/tests/components/devolo_home_control/test_config_flow.py @@ -1,4 +1,5 @@ """Test the devolo_home_control config flow.""" + from unittest.mock import patch import pytest @@ -74,12 +75,15 @@ async def test_form_advanced_options(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.FORM assert result["errors"] == {} - with patch( - "homeassistant.components.devolo_home_control.async_setup_entry", - return_value=True, - ) as mock_setup_entry, patch( - "homeassistant.components.devolo_home_control.Mydevolo.uuid", - return_value="123456", + with ( + patch( + "homeassistant.components.devolo_home_control.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + patch( + "homeassistant.components.devolo_home_control.Mydevolo.uuid", + return_value="123456", + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -178,12 +182,15 @@ async def test_form_reauth(hass: HomeAssistant) -> None: assert result["step_id"] == "reauth_confirm" assert result["type"] == FlowResultType.FORM - with patch( - "homeassistant.components.devolo_home_control.async_setup_entry", - return_value=True, - ) as mock_setup_entry, patch( - "homeassistant.components.devolo_home_control.Mydevolo.uuid", - return_value="123456", + with ( + patch( + "homeassistant.components.devolo_home_control.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + patch( + "homeassistant.components.devolo_home_control.Mydevolo.uuid", + return_value="123456", + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -241,12 +248,15 @@ async def test_form_uuid_change_reauth(hass: HomeAssistant) -> None: assert result["step_id"] == "reauth_confirm" assert result["type"] == FlowResultType.FORM - with patch( - "homeassistant.components.devolo_home_control.async_setup_entry", - return_value=True, - ), patch( - "homeassistant.components.devolo_home_control.Mydevolo.uuid", - return_value="789123", + with ( + patch( + "homeassistant.components.devolo_home_control.async_setup_entry", + return_value=True, + ), + patch( + "homeassistant.components.devolo_home_control.Mydevolo.uuid", + return_value="789123", + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -260,12 +270,15 @@ async def test_form_uuid_change_reauth(hass: HomeAssistant) -> None: async def _setup(hass: HomeAssistant, result: FlowResult) -> None: """Finish configuration steps.""" - with patch( - "homeassistant.components.devolo_home_control.async_setup_entry", - return_value=True, - ) as mock_setup_entry, patch( - "homeassistant.components.devolo_home_control.Mydevolo.uuid", - return_value="123456", + with ( + patch( + "homeassistant.components.devolo_home_control.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + patch( + "homeassistant.components.devolo_home_control.Mydevolo.uuid", + return_value="123456", + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], diff --git a/tests/components/devolo_home_control/test_cover.py b/tests/components/devolo_home_control/test_cover.py index 54f0cc34222..c21dabadb1a 100644 --- a/tests/components/devolo_home_control/test_cover.py +++ b/tests/components/devolo_home_control/test_cover.py @@ -1,4 +1,5 @@ """Tests for the devolo Home Control cover platform.""" + from unittest.mock import patch from syrupy.assertion import SnapshotAssertion diff --git a/tests/components/devolo_home_control/test_diagnostics.py b/tests/components/devolo_home_control/test_diagnostics.py index ad267c4c52e..e31bc360845 100644 --- a/tests/components/devolo_home_control/test_diagnostics.py +++ b/tests/components/devolo_home_control/test_diagnostics.py @@ -1,4 +1,5 @@ """Tests for the devolo Home Control diagnostics.""" + from __future__ import annotations from unittest.mock import patch diff --git a/tests/components/devolo_home_control/test_init.py b/tests/components/devolo_home_control/test_init.py index cb4c87aebdc..250a31843eb 100644 --- a/tests/components/devolo_home_control/test_init.py +++ b/tests/components/devolo_home_control/test_init.py @@ -1,4 +1,5 @@ """Tests for the devolo Home Control integration.""" + from unittest.mock import patch from devolo_home_control_api.exceptions.gateway import GatewayOfflineError diff --git a/tests/components/devolo_home_control/test_light.py b/tests/components/devolo_home_control/test_light.py index 8cb31dde8cc..f72136ee287 100644 --- a/tests/components/devolo_home_control/test_light.py +++ b/tests/components/devolo_home_control/test_light.py @@ -1,4 +1,5 @@ """Tests for the devolo Home Control light platform.""" + from unittest.mock import patch from syrupy.assertion import SnapshotAssertion diff --git a/tests/components/devolo_home_control/test_sensor.py b/tests/components/devolo_home_control/test_sensor.py index afc4289fccf..62023982e81 100644 --- a/tests/components/devolo_home_control/test_sensor.py +++ b/tests/components/devolo_home_control/test_sensor.py @@ -1,4 +1,5 @@ """Tests for the devolo Home Control sensor platform.""" + from unittest.mock import patch from syrupy.assertion import SnapshotAssertion diff --git a/tests/components/devolo_home_control/test_siren.py b/tests/components/devolo_home_control/test_siren.py index 5e72f771112..037d7b5021f 100644 --- a/tests/components/devolo_home_control/test_siren.py +++ b/tests/components/devolo_home_control/test_siren.py @@ -1,4 +1,5 @@ """Tests for the devolo Home Control binary sensors.""" + from unittest.mock import patch import pytest diff --git a/tests/components/devolo_home_control/test_switch.py b/tests/components/devolo_home_control/test_switch.py index 9216768b9c3..86f93bfddf6 100644 --- a/tests/components/devolo_home_control/test_switch.py +++ b/tests/components/devolo_home_control/test_switch.py @@ -1,4 +1,5 @@ """Tests for the devolo Home Control switch platform.""" + from unittest.mock import patch from syrupy.assertion import SnapshotAssertion diff --git a/tests/components/devolo_home_network/__init__.py b/tests/components/devolo_home_network/__init__.py index ac6a960fd8f..05ccbca0c56 100644 --- a/tests/components/devolo_home_network/__init__.py +++ b/tests/components/devolo_home_network/__init__.py @@ -1,4 +1,5 @@ """Tests for the devolo Home Network integration.""" + from homeassistant.components.devolo_home_network.const import DOMAIN from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD from homeassistant.core import HomeAssistant diff --git a/tests/components/devolo_home_network/conftest.py b/tests/components/devolo_home_network/conftest.py index 9f30ca74cb0..f6a6e233b6d 100644 --- a/tests/components/devolo_home_network/conftest.py +++ b/tests/components/devolo_home_network/conftest.py @@ -1,4 +1,5 @@ """Fixtures for tests.""" + from itertools import cycle from unittest.mock import patch diff --git a/tests/components/devolo_home_network/mock.py b/tests/components/devolo_home_network/mock.py index 612df4da2e0..4b999667e53 100644 --- a/tests/components/devolo_home_network/mock.py +++ b/tests/components/devolo_home_network/mock.py @@ -1,4 +1,5 @@ """Mock of a devolo Home Network device.""" + from __future__ import annotations from unittest.mock import AsyncMock diff --git a/tests/components/devolo_home_network/snapshots/test_binary_sensor.ambr b/tests/components/devolo_home_network/snapshots/test_binary_sensor.ambr index 7545fff55f1..c0df0d5d5a5 100644 --- a/tests/components/devolo_home_network/snapshots/test_binary_sensor.ambr +++ b/tests/components/devolo_home_network/snapshots/test_binary_sensor.ambr @@ -8,6 +8,7 @@ 'context': , 'entity_id': 'binary_sensor.mock_title_connected_to_router', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }) diff --git a/tests/components/devolo_home_network/snapshots/test_button.ambr b/tests/components/devolo_home_network/snapshots/test_button.ambr index 26be12805ce..3e8e4ae2bb3 100644 --- a/tests/components/devolo_home_network/snapshots/test_button.ambr +++ b/tests/components/devolo_home_network/snapshots/test_button.ambr @@ -51,6 +51,7 @@ 'context': , 'entity_id': 'button.mock_title_identify_device_with_a_blinking_led', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }) @@ -140,6 +141,7 @@ 'context': , 'entity_id': 'button.mock_title_restart_device', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }) @@ -228,6 +230,7 @@ 'context': , 'entity_id': 'button.mock_title_start_plc_pairing', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }) @@ -316,6 +319,7 @@ 'context': , 'entity_id': 'button.mock_title_start_wps', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }) diff --git a/tests/components/devolo_home_network/snapshots/test_device_tracker.ambr b/tests/components/devolo_home_network/snapshots/test_device_tracker.ambr index d438aca6a4a..9df6b168f9f 100644 --- a/tests/components/devolo_home_network/snapshots/test_device_tracker.ambr +++ b/tests/components/devolo_home_network/snapshots/test_device_tracker.ambr @@ -11,6 +11,7 @@ 'context': , 'entity_id': 'device_tracker.devolo_home_network_1234567890_aa_bb_cc_dd_ee_ff', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'home', }) diff --git a/tests/components/devolo_home_network/snapshots/test_sensor.ambr b/tests/components/devolo_home_network/snapshots/test_sensor.ambr index f961104a14f..fc173da8294 100644 --- a/tests/components/devolo_home_network/snapshots/test_sensor.ambr +++ b/tests/components/devolo_home_network/snapshots/test_sensor.ambr @@ -7,6 +7,7 @@ 'context': , 'entity_id': 'sensor.mock_title_connected_plc_devices', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '1', }) @@ -53,6 +54,7 @@ 'context': , 'entity_id': 'sensor.mock_title_connected_wifi_clients', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '1', }) @@ -100,6 +102,7 @@ 'context': , 'entity_id': 'sensor.mock_title_neighboring_wifi_networks', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '1', }) @@ -147,6 +150,7 @@ 'context': , 'entity_id': 'sensor.mock_title_plc_downlink_phy_rate_test2', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '100.0', }) @@ -197,6 +201,7 @@ 'context': , 'entity_id': 'sensor.mock_title_plc_downlink_phy_rate_test2', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '100.0', }) diff --git a/tests/components/devolo_home_network/snapshots/test_switch.ambr b/tests/components/devolo_home_network/snapshots/test_switch.ambr index 760e240fee3..09b56efc784 100644 --- a/tests/components/devolo_home_network/snapshots/test_switch.ambr +++ b/tests/components/devolo_home_network/snapshots/test_switch.ambr @@ -93,6 +93,7 @@ 'context': , 'entity_id': 'switch.mock_title_enable_guest_wifi', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }) @@ -138,6 +139,7 @@ 'context': , 'entity_id': 'switch.mock_title_enable_leds', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }) diff --git a/tests/components/devolo_home_network/snapshots/test_update.ambr b/tests/components/devolo_home_network/snapshots/test_update.ambr index 106b8f9885c..83ca84c82e8 100644 --- a/tests/components/devolo_home_network/snapshots/test_update.ambr +++ b/tests/components/devolo_home_network/snapshots/test_update.ambr @@ -18,6 +18,7 @@ 'context': , 'entity_id': 'update.mock_title_firmware', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }) diff --git a/tests/components/devolo_home_network/test_binary_sensor.py b/tests/components/devolo_home_network/test_binary_sensor.py index 17d95fc51a3..3e4bf8471c1 100644 --- a/tests/components/devolo_home_network/test_binary_sensor.py +++ b/tests/components/devolo_home_network/test_binary_sensor.py @@ -1,4 +1,5 @@ """Tests for the devolo Home Network sensors.""" + from unittest.mock import AsyncMock from devolo_plc_api.exceptions.device import DeviceUnavailable diff --git a/tests/components/devolo_home_network/test_button.py b/tests/components/devolo_home_network/test_button.py index 41820210dee..1097c0271cb 100644 --- a/tests/components/devolo_home_network/test_button.py +++ b/tests/components/devolo_home_network/test_button.py @@ -1,4 +1,5 @@ """Tests for the devolo Home Network buttons.""" + from unittest.mock import AsyncMock from devolo_plc_api.exceptions.device import DevicePasswordProtected, DeviceUnavailable @@ -39,26 +40,26 @@ async def test_button_setup(hass: HomeAssistant) -> None: @pytest.mark.parametrize( ("name", "api_name", "trigger_method"), [ - [ + ( "identify_device_with_a_blinking_led", "plcnet", "async_identify_device_start", - ], - [ + ), + ( "start_plc_pairing", "plcnet", "async_pair_device", - ], - [ + ), + ( "restart_device", "device", "async_restart", - ], - [ + ), + ( "start_wps", "device", "async_start_wps", - ], + ), ], ) @pytest.mark.freeze_time("2023-01-13 12:00:00+00:00") diff --git a/tests/components/devolo_home_network/test_config_flow.py b/tests/components/devolo_home_network/test_config_flow.py index 9050181cc8f..5d23037df54 100644 --- a/tests/components/devolo_home_network/test_config_flow.py +++ b/tests/components/devolo_home_network/test_config_flow.py @@ -1,4 +1,5 @@ """Test the devolo Home Network config flow.""" + from __future__ import annotations from typing import Any @@ -63,7 +64,7 @@ async def test_form(hass: HomeAssistant, info: dict[str, Any]) -> None: @pytest.mark.parametrize( ("exception_type", "expected_error"), - [[DeviceNotFound(IP), "cannot_connect"], [Exception, "unknown"]], + [(DeviceNotFound(IP), "cannot_connect"), (Exception, "unknown")], ) async def test_form_error(hass: HomeAssistant, exception_type, expected_error) -> None: """Test we handle errors.""" diff --git a/tests/components/devolo_home_network/test_device_tracker.py b/tests/components/devolo_home_network/test_device_tracker.py index 8f58b1154de..1cce11c36f9 100644 --- a/tests/components/devolo_home_network/test_device_tracker.py +++ b/tests/components/devolo_home_network/test_device_tracker.py @@ -1,4 +1,5 @@ """Tests for the devolo Home Network device tracker.""" + from unittest.mock import AsyncMock from devolo_plc_api.exceptions.device import DeviceUnavailable diff --git a/tests/components/devolo_home_network/test_diagnostics.py b/tests/components/devolo_home_network/test_diagnostics.py index 0248d755e22..75794250908 100644 --- a/tests/components/devolo_home_network/test_diagnostics.py +++ b/tests/components/devolo_home_network/test_diagnostics.py @@ -1,4 +1,5 @@ """Tests for the devolo Home Network diagnostics.""" + from __future__ import annotations import pytest diff --git a/tests/components/devolo_home_network/test_image.py b/tests/components/devolo_home_network/test_image.py index ef7c4b2bbba..0ca3936e1ac 100644 --- a/tests/components/devolo_home_network/test_image.py +++ b/tests/components/devolo_home_network/test_image.py @@ -1,4 +1,5 @@ """Tests for the devolo Home Network images.""" + from http import HTTPStatus from unittest.mock import AsyncMock diff --git a/tests/components/devolo_home_network/test_init.py b/tests/components/devolo_home_network/test_init.py index e34af0dcbaf..c4a02f9e375 100644 --- a/tests/components/devolo_home_network/test_init.py +++ b/tests/components/devolo_home_network/test_init.py @@ -1,4 +1,5 @@ """Test the devolo Home Network integration setup.""" + from unittest.mock import patch from devolo_plc_api.exceptions.device import DeviceNotFound @@ -52,10 +53,13 @@ async def test_setup_without_password(hass: HomeAssistant) -> None: } entry = MockConfigEntry(domain=DOMAIN, data=config) entry.add_to_hass(hass) - with patch( - "homeassistant.config_entries.ConfigEntries.async_forward_entry_setup", - return_value=True, - ), patch("homeassistant.core.EventBus.async_listen_once"): + with ( + patch( + "homeassistant.config_entries.ConfigEntries.async_forward_entry_setup", + return_value=True, + ), + patch("homeassistant.core.EventBus.async_listen_once"), + ): assert await hass.config_entries.async_setup(entry.entry_id) assert entry.state is ConfigEntryState.LOADED @@ -94,15 +98,15 @@ async def test_hass_stop(hass: HomeAssistant, mock_device: MockDevice) -> None: @pytest.mark.parametrize( ("device", "expected_platforms"), [ - [ + ( "mock_device", (BINARY_SENSOR, BUTTON, DEVICE_TRACKER, IMAGE, SENSOR, SWITCH, UPDATE), - ], - [ + ), + ( "mock_repeater_device", (BUTTON, DEVICE_TRACKER, IMAGE, SENSOR, SWITCH, UPDATE), - ], - ["mock_nonwifi_device", (BINARY_SENSOR, BUTTON, SENSOR, SWITCH, UPDATE)], + ), + ("mock_nonwifi_device", (BINARY_SENSOR, BUTTON, SENSOR, SWITCH, UPDATE)), ], ) async def test_platforms( diff --git a/tests/components/devolo_home_network/test_sensor.py b/tests/components/devolo_home_network/test_sensor.py index e6f02033425..5b5e05a40d1 100644 --- a/tests/components/devolo_home_network/test_sensor.py +++ b/tests/components/devolo_home_network/test_sensor.py @@ -1,4 +1,5 @@ """Tests for the devolo Home Network sensors.""" + from datetime import timedelta from unittest.mock import AsyncMock @@ -65,21 +66,21 @@ async def test_sensor_setup(hass: HomeAssistant) -> None: @pytest.mark.parametrize( ("name", "get_method", "interval"), [ - [ + ( "connected_wifi_clients", "async_get_wifi_connected_station", SHORT_UPDATE_INTERVAL, - ], - [ + ), + ( "neighboring_wifi_networks", "async_get_wifi_neighbor_access_points", LONG_UPDATE_INTERVAL, - ], - [ + ), + ( "connected_plc_devices", "async_get_network_overview", LONG_UPDATE_INTERVAL, - ], + ), ], ) @pytest.mark.usefixtures("entity_registry_enabled_by_default") diff --git a/tests/components/devolo_home_network/test_switch.py b/tests/components/devolo_home_network/test_switch.py index c77a77e87de..0fe5bea5c52 100644 --- a/tests/components/devolo_home_network/test_switch.py +++ b/tests/components/devolo_home_network/test_switch.py @@ -1,4 +1,5 @@ """Tests for the devolo Home Network switch.""" + from datetime import timedelta from unittest.mock import AsyncMock, patch @@ -246,8 +247,8 @@ async def test_update_enable_leds( @pytest.mark.parametrize( ("name", "get_method", "update_interval"), [ - ["enable_guest_wifi", "async_get_wifi_guest_access", SHORT_UPDATE_INTERVAL], - ["enable_leds", "async_get_led_setting", SHORT_UPDATE_INTERVAL], + ("enable_guest_wifi", "async_get_wifi_guest_access", SHORT_UPDATE_INTERVAL), + ("enable_leds", "async_get_led_setting", SHORT_UPDATE_INTERVAL), ], ) async def test_device_failure( @@ -283,8 +284,8 @@ async def test_device_failure( @pytest.mark.parametrize( ("name", "set_method"), [ - ["enable_guest_wifi", "async_set_wifi_guest_access"], - ["enable_leds", "async_set_led_setting"], + ("enable_guest_wifi", "async_set_wifi_guest_access"), + ("enable_leds", "async_set_led_setting"), ], ) async def test_auth_failed( diff --git a/tests/components/devolo_home_network/test_update.py b/tests/components/devolo_home_network/test_update.py index d80e9133a0a..7f70524fa5b 100644 --- a/tests/components/devolo_home_network/test_update.py +++ b/tests/components/devolo_home_network/test_update.py @@ -1,4 +1,5 @@ """Tests for the devolo Home Network update.""" + from devolo_plc_api.device_api import UPDATE_NOT_AVAILABLE, UpdateFirmwareCheck from devolo_plc_api.exceptions.device import DevicePasswordProtected, DeviceUnavailable from freezegun.api import FrozenDateTimeFactory diff --git a/tests/components/dexcom/__init__.py b/tests/components/dexcom/__init__.py index acf03725d32..e9ca303765b 100644 --- a/tests/components/dexcom/__init__.py +++ b/tests/components/dexcom/__init__.py @@ -28,12 +28,15 @@ async def init_integration(hass) -> MockConfigEntry: data=CONFIG, options=None, ) - with patch( - "homeassistant.components.dexcom.Dexcom.get_current_glucose_reading", - return_value=GLUCOSE_READING, - ), patch( - "homeassistant.components.dexcom.Dexcom.create_session", - return_value="test_session_id", + with ( + patch( + "homeassistant.components.dexcom.Dexcom.get_current_glucose_reading", + return_value=GLUCOSE_READING, + ), + patch( + "homeassistant.components.dexcom.Dexcom.create_session", + return_value="test_session_id", + ), ): entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) diff --git a/tests/components/dexcom/test_config_flow.py b/tests/components/dexcom/test_config_flow.py index 80ca65eabc9..f87f365a7e6 100644 --- a/tests/components/dexcom/test_config_flow.py +++ b/tests/components/dexcom/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Dexcom config flow.""" + from unittest.mock import patch from pydexcom import AccountError, SessionError @@ -22,13 +23,16 @@ async def test_form(hass: HomeAssistant) -> None: assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} - with patch( - "homeassistant.components.dexcom.config_flow.Dexcom.create_session", - return_value="test_session_id", - ), patch( - "homeassistant.components.dexcom.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.dexcom.config_flow.Dexcom.create_session", + return_value="test_session_id", + ), + patch( + "homeassistant.components.dexcom.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], CONFIG, diff --git a/tests/components/dexcom/test_init.py b/tests/components/dexcom/test_init.py index 019ea8a58bd..f43d39626b0 100644 --- a/tests/components/dexcom/test_init.py +++ b/tests/components/dexcom/test_init.py @@ -1,4 +1,5 @@ """Test the Dexcom config flow.""" + from unittest.mock import patch from pydexcom import AccountError, SessionError diff --git a/tests/components/dexcom/test_sensor.py b/tests/components/dexcom/test_sensor.py index a211f0606f3..1b7f0b026ab 100644 --- a/tests/components/dexcom/test_sensor.py +++ b/tests/components/dexcom/test_sensor.py @@ -1,4 +1,5 @@ """The sensor tests for the griddy platform.""" + from unittest.mock import patch from pydexcom import SessionError @@ -68,12 +69,15 @@ async def test_sensors_options_changed(hass: HomeAssistant) -> None: test_username_glucose_trend = hass.states.get("sensor.test_username_glucose_trend") assert test_username_glucose_trend.state == GLUCOSE_READING.trend_description - with patch( - "homeassistant.components.dexcom.Dexcom.get_current_glucose_reading", - return_value=GLUCOSE_READING, - ), patch( - "homeassistant.components.dexcom.Dexcom.create_session", - return_value="test_session_id", + with ( + patch( + "homeassistant.components.dexcom.Dexcom.get_current_glucose_reading", + return_value=GLUCOSE_READING, + ), + patch( + "homeassistant.components.dexcom.Dexcom.create_session", + return_value="test_session_id", + ), ): hass.config_entries.async_update_entry( entry=entry, diff --git a/tests/components/dhcp/test_init.py b/tests/components/dhcp/test_init.py index 487435ef3f5..7c652c8ea3e 100644 --- a/tests/components/dhcp/test_init.py +++ b/tests/components/dhcp/test_init.py @@ -1,4 +1,5 @@ """Test the DHCP discovery integration.""" + from collections.abc import Awaitable, Callable import datetime import threading @@ -144,8 +145,8 @@ async def _async_get_handle_dhcp_packet( {}, integration_matchers, ) - with patch("aiodhcpwatcher.start"): - dhcp_watcher.async_start() + with patch("aiodhcpwatcher.async_start"): + await dhcp_watcher.async_start() def _async_handle_dhcp_request(request: aiodhcpwatcher.DHCPRequest) -> None: dhcp_watcher._async_process_dhcp_request(request) @@ -534,11 +535,13 @@ async def test_setup_and_stop(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - with patch.object( - interfaces, - "resolve_iface", - ) as resolve_iface_call, patch("scapy.arch.common.compile_filter"), patch( - "homeassistant.components.dhcp.DiscoverHosts.async_discover" + with ( + patch.object( + interfaces, + "resolve_iface", + ) as resolve_iface_call, + patch("scapy.arch.common.compile_filter"), + patch("homeassistant.components.dhcp.DiscoverHosts.async_discover"), ): hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -563,11 +566,15 @@ async def test_setup_fails_as_root( wait_event = threading.Event() - with patch("os.geteuid", return_value=0), patch.object( - interfaces, - "resolve_iface", - side_effect=Scapy_Exception, - ), patch("homeassistant.components.dhcp.DiscoverHosts.async_discover"): + with ( + patch("os.geteuid", return_value=0), + patch.object( + interfaces, + "resolve_iface", + side_effect=Scapy_Exception, + ), + patch("homeassistant.components.dhcp.DiscoverHosts.async_discover"), + ): hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -589,13 +596,16 @@ async def test_setup_fails_non_root( ) await hass.async_block_till_done() - with patch("os.geteuid", return_value=10), patch( - "scapy.arch.common.compile_filter" - ), patch.object( - interfaces, - "resolve_iface", - side_effect=Scapy_Exception, - ), patch("homeassistant.components.dhcp.DiscoverHosts.async_discover"): + with ( + patch("os.geteuid", return_value=10), + patch("scapy.arch.common.compile_filter"), + patch.object( + interfaces, + "resolve_iface", + side_effect=Scapy_Exception, + ), + patch("homeassistant.components.dhcp.DiscoverHosts.async_discover"), + ): hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) @@ -616,14 +626,16 @@ async def test_setup_fails_with_broken_libpcap( ) await hass.async_block_till_done() - with patch( - "scapy.arch.common.compile_filter", - side_effect=ImportError, - ) as compile_filter, patch.object( - interfaces, - "resolve_iface", - ) as resolve_iface_call, patch( - "homeassistant.components.dhcp.DiscoverHosts.async_discover" + with ( + patch( + "scapy.arch.common.compile_filter", + side_effect=ImportError, + ) as compile_filter, + patch.object( + interfaces, + "resolve_iface", + ) as resolve_iface_call, + patch("homeassistant.components.dhcp.DiscoverHosts.async_discover"), ): hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -966,15 +978,18 @@ async def test_device_tracker_ignore_self_assigned_ips_before_start( async def test_aiodiscover_finds_new_hosts(hass: HomeAssistant) -> None: """Test aiodiscover finds new host.""" - with patch.object(hass.config_entries.flow, "async_init") as mock_init, patch( - "homeassistant.components.dhcp.DiscoverHosts.async_discover", - return_value=[ - { - dhcp.DISCOVERY_IP_ADDRESS: "192.168.210.56", - dhcp.DISCOVERY_HOSTNAME: "connect", - dhcp.DISCOVERY_MAC_ADDRESS: "b8b7f16db533", - } - ], + with ( + patch.object(hass.config_entries.flow, "async_init") as mock_init, + patch( + "homeassistant.components.dhcp.DiscoverHosts.async_discover", + return_value=[ + { + dhcp.DISCOVERY_IP_ADDRESS: "192.168.210.56", + dhcp.DISCOVERY_HOSTNAME: "connect", + dhcp.DISCOVERY_MAC_ADDRESS: "b8b7f16db533", + } + ], + ), ): device_tracker_watcher = dhcp.NetworkWatcher( hass, @@ -1015,25 +1030,28 @@ async def test_aiodiscover_does_not_call_again_on_shorter_hostname( additional discovery where the hostname is longer and then reject shorter ones. """ - with patch.object(hass.config_entries.flow, "async_init") as mock_init, patch( - "homeassistant.components.dhcp.DiscoverHosts.async_discover", - return_value=[ - { - dhcp.DISCOVERY_IP_ADDRESS: "192.168.210.56", - dhcp.DISCOVERY_HOSTNAME: "irobot-abc", - dhcp.DISCOVERY_MAC_ADDRESS: "b8b7f16db533", - }, - { - dhcp.DISCOVERY_IP_ADDRESS: "192.168.210.56", - dhcp.DISCOVERY_HOSTNAME: "irobot-abcdef", - dhcp.DISCOVERY_MAC_ADDRESS: "b8b7f16db533", - }, - { - dhcp.DISCOVERY_IP_ADDRESS: "192.168.210.56", - dhcp.DISCOVERY_HOSTNAME: "irobot-abc", - dhcp.DISCOVERY_MAC_ADDRESS: "b8b7f16db533", - }, - ], + with ( + patch.object(hass.config_entries.flow, "async_init") as mock_init, + patch( + "homeassistant.components.dhcp.DiscoverHosts.async_discover", + return_value=[ + { + dhcp.DISCOVERY_IP_ADDRESS: "192.168.210.56", + dhcp.DISCOVERY_HOSTNAME: "irobot-abc", + dhcp.DISCOVERY_MAC_ADDRESS: "b8b7f16db533", + }, + { + dhcp.DISCOVERY_IP_ADDRESS: "192.168.210.56", + dhcp.DISCOVERY_HOSTNAME: "irobot-abcdef", + dhcp.DISCOVERY_MAC_ADDRESS: "b8b7f16db533", + }, + { + dhcp.DISCOVERY_IP_ADDRESS: "192.168.210.56", + dhcp.DISCOVERY_HOSTNAME: "irobot-abc", + dhcp.DISCOVERY_MAC_ADDRESS: "b8b7f16db533", + }, + ], + ), ): device_tracker_watcher = dhcp.NetworkWatcher( hass, @@ -1076,9 +1094,12 @@ async def test_aiodiscover_does_not_call_again_on_shorter_hostname( async def test_aiodiscover_finds_new_hosts_after_interval(hass: HomeAssistant) -> None: """Test aiodiscover finds new host after interval.""" - with patch.object(hass.config_entries.flow, "async_init") as mock_init, patch( - "homeassistant.components.dhcp.DiscoverHosts.async_discover", - return_value=[], + with ( + patch.object(hass.config_entries.flow, "async_init") as mock_init, + patch( + "homeassistant.components.dhcp.DiscoverHosts.async_discover", + return_value=[], + ), ): device_tracker_watcher = dhcp.NetworkWatcher( hass, @@ -1098,15 +1119,18 @@ async def test_aiodiscover_finds_new_hosts_after_interval(hass: HomeAssistant) - assert len(mock_init.mock_calls) == 0 - with patch.object(hass.config_entries.flow, "async_init") as mock_init, patch( - "homeassistant.components.dhcp.DiscoverHosts.async_discover", - return_value=[ - { - dhcp.DISCOVERY_IP_ADDRESS: "192.168.210.56", - dhcp.DISCOVERY_HOSTNAME: "connect", - dhcp.DISCOVERY_MAC_ADDRESS: "b8b7f16db533", - } - ], + with ( + patch.object(hass.config_entries.flow, "async_init") as mock_init, + patch( + "homeassistant.components.dhcp.DiscoverHosts.async_discover", + return_value=[ + { + dhcp.DISCOVERY_IP_ADDRESS: "192.168.210.56", + dhcp.DISCOVERY_HOSTNAME: "connect", + dhcp.DISCOVERY_MAC_ADDRESS: "b8b7f16db533", + } + ], + ), ): async_fire_time_changed(hass, dt_util.utcnow() + datetime.timedelta(minutes=65)) await hass.async_block_till_done() diff --git a/tests/components/diagnostics/__init__.py b/tests/components/diagnostics/__init__.py index 5c81917b22c..81d62e7c2fe 100644 --- a/tests/components/diagnostics/__init__.py +++ b/tests/components/diagnostics/__init__.py @@ -1,4 +1,5 @@ """Tests for the Diagnostics integration.""" + from http import HTTPStatus from typing import cast diff --git a/tests/components/diagnostics/test_init.py b/tests/components/diagnostics/test_init.py index bc208e5f0f0..3303e51a5a5 100644 --- a/tests/components/diagnostics/test_init.py +++ b/tests/components/diagnostics/test_init.py @@ -1,4 +1,5 @@ """Test the Diagnostics integration.""" + from http import HTTPStatus from unittest.mock import AsyncMock, Mock diff --git a/tests/components/diagnostics/test_util.py b/tests/components/diagnostics/test_util.py index 87e29f31cc8..3781b980e97 100644 --- a/tests/components/diagnostics/test_util.py +++ b/tests/components/diagnostics/test_util.py @@ -1,4 +1,5 @@ """Test Diagnostics utils.""" + from homeassistant.components.diagnostics import REDACTED, async_redact_data diff --git a/tests/components/dialogflow/test_init.py b/tests/components/dialogflow/test_init.py index 3e90856fc12..7f5fe79d146 100644 --- a/tests/components/dialogflow/test_init.py +++ b/tests/components/dialogflow/test_init.py @@ -1,4 +1,5 @@ """The tests for the Dialogflow component.""" + import copy from http import HTTPStatus import json diff --git a/tests/components/directv/__init__.py b/tests/components/directv/__init__.py index 584a7c95509..ae22e280000 100644 --- a/tests/components/directv/__init__.py +++ b/tests/components/directv/__init__.py @@ -1,4 +1,5 @@ """Tests for the DirecTV component.""" + from http import HTTPStatus from homeassistant.components import ssdp diff --git a/tests/components/directv/test_config_flow.py b/tests/components/directv/test_config_flow.py index 8017dc290c9..569e165a0a6 100644 --- a/tests/components/directv/test_config_flow.py +++ b/tests/components/directv/test_config_flow.py @@ -1,4 +1,5 @@ """Test the DirecTV config flow.""" + import dataclasses from unittest.mock import patch diff --git a/tests/components/directv/test_init.py b/tests/components/directv/test_init.py index dd7b5b09f9f..4bfe8e2121f 100644 --- a/tests/components/directv/test_init.py +++ b/tests/components/directv/test_init.py @@ -1,4 +1,5 @@ """Tests for the DirecTV integration.""" + from homeassistant.components.directv.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant diff --git a/tests/components/directv/test_media_player.py b/tests/components/directv/test_media_player.py index 55dd2e758dc..33eb35ed268 100644 --- a/tests/components/directv/test_media_player.py +++ b/tests/components/directv/test_media_player.py @@ -1,4 +1,5 @@ """The tests for the DirecTV Media player platform.""" + from __future__ import annotations from datetime import datetime, timedelta diff --git a/tests/components/directv/test_remote.py b/tests/components/directv/test_remote.py index 9d326903933..a1a04967482 100644 --- a/tests/components/directv/test_remote.py +++ b/tests/components/directv/test_remote.py @@ -1,4 +1,5 @@ """The tests for the DirecTV remote platform.""" + from unittest.mock import patch from homeassistant.components.remote import ( diff --git a/tests/components/discord/conftest.py b/tests/components/discord/conftest.py index c98944fdc85..128869d0b80 100644 --- a/tests/components/discord/conftest.py +++ b/tests/components/discord/conftest.py @@ -1,4 +1,5 @@ """Discord notification test helpers.""" + from http import HTTPStatus import pytest diff --git a/tests/components/discord/test_config_flow.py b/tests/components/discord/test_config_flow.py index b6504e851ca..ba1909c48c8 100644 --- a/tests/components/discord/test_config_flow.py +++ b/tests/components/discord/test_config_flow.py @@ -1,4 +1,5 @@ """Test Discord config flow.""" + import nextcord from homeassistant import config_entries, data_entry_flow diff --git a/tests/components/discord/test_notify.py b/tests/components/discord/test_notify.py index 810898cdf73..011698867a8 100644 --- a/tests/components/discord/test_notify.py +++ b/tests/components/discord/test_notify.py @@ -1,4 +1,5 @@ """Test Discord notify.""" + import logging import pytest diff --git a/tests/components/discovergy/conftest.py b/tests/components/discovergy/conftest.py index 819a1cbb72a..d3ab3b831f0 100644 --- a/tests/components/discovergy/conftest.py +++ b/tests/components/discovergy/conftest.py @@ -1,4 +1,5 @@ """Fixtures for Discovergy integration tests.""" + from collections.abc import Generator from unittest.mock import AsyncMock, patch @@ -26,12 +27,15 @@ def _meter_last_reading(meter_id: str) -> Reading: @pytest.fixture(name="discovergy") def mock_discovergy() -> Generator[AsyncMock, None, None]: """Mock the pydiscovergy client.""" - with patch( - "homeassistant.components.discovergy.Discovergy", - autospec=True, - ) as mock_discovergy, patch( - "homeassistant.components.discovergy.config_flow.Discovergy", - new=mock_discovergy, + with ( + patch( + "homeassistant.components.discovergy.Discovergy", + autospec=True, + ) as mock_discovergy, + patch( + "homeassistant.components.discovergy.config_flow.Discovergy", + new=mock_discovergy, + ), ): mock = mock_discovergy.return_value mock.meters.return_value = GET_METERS diff --git a/tests/components/discovergy/const.py b/tests/components/discovergy/const.py index 3c9f696fb02..700d001814d 100644 --- a/tests/components/discovergy/const.py +++ b/tests/components/discovergy/const.py @@ -1,4 +1,5 @@ """Constants for Discovergy integration tests.""" + import datetime from pydiscovergy.models import Location, Meter, Reading diff --git a/tests/components/discovergy/snapshots/test_sensor.ambr b/tests/components/discovergy/snapshots/test_sensor.ambr index a107c44840d..b4831d81bda 100644 --- a/tests/components/discovergy/snapshots/test_sensor.ambr +++ b/tests/components/discovergy/snapshots/test_sensor.ambr @@ -84,6 +84,7 @@ 'context': , 'entity_id': 'sensor.electricity_teststrasse_1_total_consumption', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '11934.8699715', }) @@ -137,6 +138,7 @@ 'context': , 'entity_id': 'sensor.electricity_teststrasse_1_total_power', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0.0', }) @@ -226,6 +228,7 @@ 'context': , 'entity_id': 'sensor.gas_teststrasse_1_total_gas_consumption', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '21064.8', }) diff --git a/tests/components/discovergy/test_config_flow.py b/tests/components/discovergy/test_config_flow.py index 7c257f814c4..b8da429d881 100644 --- a/tests/components/discovergy/test_config_flow.py +++ b/tests/components/discovergy/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Discovergy config flow.""" + from unittest.mock import AsyncMock, patch from pydiscovergy.error import DiscovergyClientError, HTTPError, InvalidLogin diff --git a/tests/components/discovergy/test_diagnostics.py b/tests/components/discovergy/test_diagnostics.py index f2db5fb854d..5c231c3d221 100644 --- a/tests/components/discovergy/test_diagnostics.py +++ b/tests/components/discovergy/test_diagnostics.py @@ -1,4 +1,5 @@ """Test Discovergy diagnostics.""" + import pytest from syrupy import SnapshotAssertion diff --git a/tests/components/discovergy/test_init.py b/tests/components/discovergy/test_init.py index ac8f79540f5..adcf0545d7f 100644 --- a/tests/components/discovergy/test_init.py +++ b/tests/components/discovergy/test_init.py @@ -1,4 +1,5 @@ """Test Discovergy component setup.""" + from unittest.mock import AsyncMock from pydiscovergy.error import DiscovergyClientError, HTTPError, InvalidLogin diff --git a/tests/components/discovergy/test_sensor.py b/tests/components/discovergy/test_sensor.py index aba8229acf5..814efb1ba57 100644 --- a/tests/components/discovergy/test_sensor.py +++ b/tests/components/discovergy/test_sensor.py @@ -1,4 +1,5 @@ """Tests Discovergy sensor component.""" + from datetime import timedelta from unittest.mock import AsyncMock diff --git a/tests/components/discovergy/test_system_health.py b/tests/components/discovergy/test_system_health.py index 91025b06dd7..af8d9dde5b4 100644 --- a/tests/components/discovergy/test_system_health.py +++ b/tests/components/discovergy/test_system_health.py @@ -1,4 +1,5 @@ """Test Discovergy system health.""" + import asyncio from aiohttp import ClientError @@ -6,6 +7,7 @@ from pydiscovergy.const import API_BASE from homeassistant.components.discovergy.const import DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.loader import async_get_integration from homeassistant.setup import async_setup_component from tests.common import get_system_health_info @@ -17,8 +19,11 @@ async def test_discovergy_system_health( ) -> None: """Test Discovergy system health.""" aioclient_mock.get(API_BASE, text="") + integration = await async_get_integration(hass, DOMAIN) + await integration.async_get_component() hass.config.components.add(DOMAIN) assert await async_setup_component(hass, "system_health", {}) + await hass.async_block_till_done() info = await get_system_health_info(hass, DOMAIN) @@ -34,8 +39,11 @@ async def test_discovergy_system_health_fail( ) -> None: """Test Discovergy system health.""" aioclient_mock.get(API_BASE, exc=ClientError) + integration = await async_get_integration(hass, DOMAIN) + await integration.async_get_component() hass.config.components.add(DOMAIN) assert await async_setup_component(hass, "system_health", {}) + await hass.async_block_till_done() info = await get_system_health_info(hass, DOMAIN) diff --git a/tests/components/dlink/test_config_flow.py b/tests/components/dlink/test_config_flow.py index a778a4ccb22..01e61f7a8fa 100644 --- a/tests/components/dlink/test_config_flow.py +++ b/tests/components/dlink/test_config_flow.py @@ -1,4 +1,5 @@ """Test D-Link Smart Plug config flow.""" + from unittest.mock import MagicMock, patch from homeassistant import data_entry_flow diff --git a/tests/components/dlink/test_init.py b/tests/components/dlink/test_init.py index 4725d0cd3e8..484927340fa 100644 --- a/tests/components/dlink/test_init.py +++ b/tests/components/dlink/test_init.py @@ -1,4 +1,5 @@ """Test D-Link Smart Plug setup.""" + from unittest.mock import MagicMock from homeassistant.components.dlink.const import DOMAIN diff --git a/tests/components/dlink/test_switch.py b/tests/components/dlink/test_switch.py index 845e8dfe85a..d070158d9fb 100644 --- a/tests/components/dlink/test_switch.py +++ b/tests/components/dlink/test_switch.py @@ -1,4 +1,5 @@ """Switch tests for the D-Link Smart Plug integration.""" + from unittest.mock import patch from homeassistant.components.dlink import DOMAIN diff --git a/tests/components/dlna_dmr/conftest.py b/tests/components/dlna_dmr/conftest.py index 9e9bcbf3056..bb47a468dc4 100644 --- a/tests/components/dlna_dmr/conftest.py +++ b/tests/components/dlna_dmr/conftest.py @@ -1,4 +1,5 @@ """Fixtures for DLNA tests.""" + from __future__ import annotations from collections.abc import Iterable diff --git a/tests/components/dlna_dmr/test_config_flow.py b/tests/components/dlna_dmr/test_config_flow.py index d9b1d60708b..32cfd8ad5a9 100644 --- a/tests/components/dlna_dmr/test_config_flow.py +++ b/tests/components/dlna_dmr/test_config_flow.py @@ -1,4 +1,5 @@ """Test the DLNA config flow.""" + from __future__ import annotations from collections.abc import Iterable @@ -586,9 +587,9 @@ async def test_ssdp_ignore_device(hass: HomeAssistant) -> None: discovery = dataclasses.replace(MOCK_DISCOVERY) discovery.upnp = dict(discovery.upnp) - discovery.upnp[ - ssdp.ATTR_UPNP_DEVICE_TYPE - ] = "urn:schemas-upnp-org:device:ZonePlayer:1" + discovery.upnp[ssdp.ATTR_UPNP_DEVICE_TYPE] = ( + "urn:schemas-upnp-org:device:ZonePlayer:1" + ) result = await hass.config_entries.flow.async_init( DLNA_DOMAIN, context={"source": config_entries.SOURCE_SSDP}, diff --git a/tests/components/dlna_dmr/test_data.py b/tests/components/dlna_dmr/test_data.py index 06d5e01558c..57652747ffd 100644 --- a/tests/components/dlna_dmr/test_data.py +++ b/tests/components/dlna_dmr/test_data.py @@ -1,4 +1,5 @@ """Tests for the DLNA DMR data module.""" + from __future__ import annotations from collections.abc import Iterable diff --git a/tests/components/dlna_dmr/test_media_player.py b/tests/components/dlna_dmr/test_media_player.py index 1da970c4c92..9ead49f0955 100644 --- a/tests/components/dlna_dmr/test_media_player.py +++ b/tests/components/dlna_dmr/test_media_player.py @@ -1,4 +1,5 @@ """Tests for the DLNA DMR media_player module.""" + from __future__ import annotations import asyncio @@ -265,14 +266,13 @@ async def test_setup_entry_no_options( domain_data_mock.async_release_event_notifier.assert_awaited_once() dmr_device_mock.async_unsubscribe_services.assert_awaited_once() assert dmr_device_mock.on_event is None - mock_state = hass.states.get(mock_entity_id) - assert mock_state is not None - assert mock_state.state == ha_const.STATE_UNAVAILABLE + # Entity should be removed by the cleanup + assert hass.states.get(mock_entity_id) is None @pytest.mark.parametrize( "core_state", - (CoreState.not_running, CoreState.running), + [CoreState.not_running, CoreState.running], ) async def test_setup_entry_with_options( hass: HomeAssistant, @@ -344,9 +344,8 @@ async def test_setup_entry_with_options( domain_data_mock.async_release_event_notifier.assert_awaited_once() dmr_device_mock.async_unsubscribe_services.assert_awaited_once() assert dmr_device_mock.on_event is None - mock_state = hass.states.get(mock_entity_id) - assert mock_state is not None - assert mock_state.state == ha_const.STATE_UNAVAILABLE + # Entity should be removed by the cleanup + assert hass.states.get(mock_entity_id) is None async def test_setup_entry_mac_address( @@ -1010,6 +1009,7 @@ async def test_shuffle_repeat_modes( dmr_device_mock.async_set_play_mode.reset_mock() dmr_device_mock.play_mode = PlayMode.RANDOM dmr_device_mock.valid_play_modes = {PlayMode.SHUFFLE, PlayMode.RANDOM} + await get_attrs(hass, mock_entity_id) await hass.services.async_call( MP_DOMAIN, ha_const.SERVICE_SHUFFLE_SET, @@ -1023,6 +1023,7 @@ async def test_shuffle_repeat_modes( dmr_device_mock.async_set_play_mode.reset_mock() dmr_device_mock.play_mode = PlayMode.RANDOM dmr_device_mock.valid_play_modes = {PlayMode.REPEAT_ONE, PlayMode.REPEAT_ALL} + await get_attrs(hass, mock_entity_id) await hass.services.async_call( MP_DOMAIN, ha_const.SERVICE_REPEAT_SET, @@ -1262,7 +1263,7 @@ async def test_playback_update_state( @pytest.mark.parametrize( "core_state", - (CoreState.not_running, CoreState.running), + [CoreState.not_running, CoreState.running], ) async def test_unavailable_device( hass: HomeAssistant, @@ -1383,15 +1384,13 @@ async def test_unavailable_device( # Check event notifiers are not released domain_data_mock.async_release_event_notifier.assert_not_called() - # Confirm the entity is still unavailable - mock_state = hass.states.get(mock_entity_id) - assert mock_state is not None - assert mock_state.state == ha_const.STATE_UNAVAILABLE + # Entity should be removed by the cleanup + assert hass.states.get(mock_entity_id) is None @pytest.mark.parametrize( "core_state", - (CoreState.not_running, CoreState.running), + [CoreState.not_running, CoreState.running], ) async def test_become_available( hass: HomeAssistant, @@ -1424,7 +1423,7 @@ async def test_become_available( domain_data_mock.upnp_factory.async_create_device.reset_mock() # Send an SSDP notification from the now alive device - ssdp_callback = ssdp_scanner_mock.async_register_callback.call_args.args[0] + ssdp_callback = ssdp_scanner_mock.async_register_callback.call_args.args[0].target await ssdp_callback( ssdp.SsdpServiceInfo( ssdp_usn=MOCK_DEVICE_USN, @@ -1476,14 +1475,13 @@ async def test_become_available( domain_data_mock.async_release_event_notifier.assert_awaited_once() dmr_device_mock.async_unsubscribe_services.assert_awaited_once() assert dmr_device_mock.on_event is None - mock_state = hass.states.get(mock_entity_id) - assert mock_state is not None - assert mock_state.state == ha_const.STATE_UNAVAILABLE + # Entity should be removed by the cleanup + assert hass.states.get(mock_entity_id) is None @pytest.mark.parametrize( "core_state", - (CoreState.not_running, CoreState.running), + [CoreState.not_running, CoreState.running], ) async def test_alive_but_gone( hass: HomeAssistant, @@ -1497,7 +1495,7 @@ async def test_alive_but_gone( domain_data_mock.upnp_factory.async_create_device.side_effect = UpnpError # Send an SSDP notification from the still missing device - ssdp_callback = ssdp_scanner_mock.async_register_callback.call_args.args[0] + ssdp_callback = ssdp_scanner_mock.async_register_callback.call_args.args[0].target await ssdp_callback( ssdp.SsdpServiceInfo( ssdp_usn=MOCK_DEVICE_USN, @@ -1610,7 +1608,7 @@ async def test_multiple_ssdp_alive( ) # Send two SSDP notifications with the new device URL - ssdp_callback = ssdp_scanner_mock.async_register_callback.call_args.args[0] + ssdp_callback = ssdp_scanner_mock.async_register_callback.call_args.args[0].target await ssdp_callback( ssdp.SsdpServiceInfo( ssdp_usn=MOCK_DEVICE_USN, @@ -1650,7 +1648,7 @@ async def test_ssdp_byebye( ) -> None: """Test device is disconnected when byebye is received.""" # First byebye will cause a disconnect - ssdp_callback = ssdp_scanner_mock.async_register_callback.call_args.args[0] + ssdp_callback = ssdp_scanner_mock.async_register_callback.call_args.args[0].target await ssdp_callback( ssdp.SsdpServiceInfo( ssdp_usn=MOCK_DEVICE_USN, @@ -1702,7 +1700,7 @@ async def test_ssdp_update_seen_bootid( domain_data_mock.upnp_factory.async_create_device.side_effect = None # Send SSDP alive with boot ID - ssdp_callback = ssdp_scanner_mock.async_register_callback.call_args.args[0] + ssdp_callback = ssdp_scanner_mock.async_register_callback.call_args.args[0].target await ssdp_callback( ssdp.SsdpServiceInfo( ssdp_usn=MOCK_DEVICE_USN, @@ -1829,7 +1827,7 @@ async def test_ssdp_update_missed_bootid( domain_data_mock.upnp_factory.async_create_device.side_effect = None # Send SSDP alive with boot ID - ssdp_callback = ssdp_scanner_mock.async_register_callback.call_args.args[0] + ssdp_callback = ssdp_scanner_mock.async_register_callback.call_args.args[0].target await ssdp_callback( ssdp.SsdpServiceInfo( ssdp_usn=MOCK_DEVICE_USN, @@ -1906,7 +1904,7 @@ async def test_ssdp_bootid( domain_data_mock.upnp_factory.async_create_device.side_effect = None # Send SSDP alive with boot ID - ssdp_callback = ssdp_scanner_mock.async_register_callback.call_args.args[0] + ssdp_callback = ssdp_scanner_mock.async_register_callback.call_args.args[0].target await ssdp_callback( ssdp.SsdpServiceInfo( ssdp_usn=MOCK_DEVICE_USN, @@ -2333,7 +2331,7 @@ async def test_config_update_mac_address( @pytest.mark.parametrize( "core_state", - (CoreState.not_running, CoreState.running), + [CoreState.not_running, CoreState.running], ) async def test_connections_restored( hass: HomeAssistant, @@ -2366,7 +2364,7 @@ async def test_connections_restored( domain_data_mock.upnp_factory.async_create_device.reset_mock() # Send an SSDP notification from the now alive device - ssdp_callback = ssdp_scanner_mock.async_register_callback.call_args.args[0] + ssdp_callback = ssdp_scanner_mock.async_register_callback.call_args.args[0].target await ssdp_callback( ssdp.SsdpServiceInfo( ssdp_usn=MOCK_DEVICE_USN, diff --git a/tests/components/dlna_dms/conftest.py b/tests/components/dlna_dms/conftest.py index 5b785fb4ba5..eacf03e9da7 100644 --- a/tests/components/dlna_dms/conftest.py +++ b/tests/components/dlna_dms/conftest.py @@ -1,4 +1,5 @@ """Fixtures for DLNA DMS tests.""" + from __future__ import annotations from collections.abc import AsyncIterable, Iterable diff --git a/tests/components/dlna_dms/test_config_flow.py b/tests/components/dlna_dms/test_config_flow.py index c8c2998458f..8a2bda611a7 100644 --- a/tests/components/dlna_dms/test_config_flow.py +++ b/tests/components/dlna_dms/test_config_flow.py @@ -1,4 +1,5 @@ """Test the DLNA DMS config flow.""" + from __future__ import annotations from collections.abc import Iterable diff --git a/tests/components/dlna_dms/test_device_availability.py b/tests/components/dlna_dms/test_device_availability.py index a3ec5326f00..c1ad3c91a7b 100644 --- a/tests/components/dlna_dms/test_device_availability.py +++ b/tests/components/dlna_dms/test_device_availability.py @@ -1,4 +1,5 @@ """Test how the DmsDeviceSource handles available and unavailable devices.""" + from __future__ import annotations import asyncio @@ -176,7 +177,7 @@ async def test_become_available( upnp_factory_mock.async_create_device.reset_mock() # Send an SSDP notification from the now alive device - ssdp_callback = ssdp_scanner_mock.async_register_callback.call_args.args[0] + ssdp_callback = ssdp_scanner_mock.async_register_callback.call_args.args[0].target await ssdp_callback( ssdp.SsdpServiceInfo( ssdp_usn=MOCK_DEVICE_USN, @@ -204,7 +205,7 @@ async def test_alive_but_gone( upnp_factory_mock.async_create_device.side_effect = UpnpError # Send an SSDP notification from the still missing device - ssdp_callback = ssdp_scanner_mock.async_register_callback.call_args.args[0] + ssdp_callback = ssdp_scanner_mock.async_register_callback.call_args.args[0].target await ssdp_callback( ssdp.SsdpServiceInfo( ssdp_usn=MOCK_DEVICE_USN, @@ -307,7 +308,7 @@ async def test_multiple_ssdp_alive( upnp_factory_mock.async_create_device.side_effect = create_device_delayed # Send two SSDP notifications with the new device URL - ssdp_callback = ssdp_scanner_mock.async_register_callback.call_args.args[0] + ssdp_callback = ssdp_scanner_mock.async_register_callback.call_args.args[0].target await ssdp_callback( ssdp.SsdpServiceInfo( ssdp_usn=MOCK_DEVICE_USN, @@ -342,7 +343,7 @@ async def test_ssdp_byebye( ) -> None: """Test device is disconnected when byebye is received.""" # First byebye will cause a disconnect - ssdp_callback = ssdp_scanner_mock.async_register_callback.call_args.args[0] + ssdp_callback = ssdp_scanner_mock.async_register_callback.call_args.args[0].target await ssdp_callback( ssdp.SsdpServiceInfo( ssdp_usn=MOCK_DEVICE_USN, @@ -385,7 +386,7 @@ async def test_ssdp_update_seen_bootid( upnp_factory_mock.async_create_device.side_effect = None # Send SSDP alive with boot ID - ssdp_callback = ssdp_scanner_mock.async_register_callback.call_args.args[0] + ssdp_callback = ssdp_scanner_mock.async_register_callback.call_args.args[0].target await ssdp_callback( ssdp.SsdpServiceInfo( ssdp_usn=MOCK_DEVICE_USN, @@ -497,7 +498,7 @@ async def test_ssdp_update_missed_bootid( upnp_factory_mock.async_create_device.side_effect = None # Send SSDP alive with boot ID - ssdp_callback = ssdp_scanner_mock.async_register_callback.call_args.args[0] + ssdp_callback = ssdp_scanner_mock.async_register_callback.call_args.args[0].target await ssdp_callback( ssdp.SsdpServiceInfo( ssdp_usn=MOCK_DEVICE_USN, @@ -567,7 +568,7 @@ async def test_ssdp_bootid( upnp_factory_mock.async_create_device.reset_mock() # Send SSDP alive with boot ID - ssdp_callback = ssdp_scanner_mock.async_register_callback.call_args.args[0] + ssdp_callback = ssdp_scanner_mock.async_register_callback.call_args.args[0].target await ssdp_callback( ssdp.SsdpServiceInfo( ssdp_usn=MOCK_DEVICE_USN, diff --git a/tests/components/dlna_dms/test_dms_device_source.py b/tests/components/dlna_dms/test_dms_device_source.py index 622a3b8a4f9..47bd7b0b39b 100644 --- a/tests/components/dlna_dms/test_dms_device_source.py +++ b/tests/components/dlna_dms/test_dms_device_source.py @@ -1,4 +1,5 @@ """Test the browse and resolve methods of DmsDeviceSource.""" + from __future__ import annotations from typing import Final, Union @@ -66,7 +67,7 @@ async def test_catch_request_error_unavailable( ) -> None: """Test the device is checked for availability before trying requests.""" # DmsDevice notifies of disconnect via SSDP - ssdp_callback = ssdp_scanner_mock.async_register_callback.call_args.args[0] + ssdp_callback = ssdp_scanner_mock.async_register_callback.call_args.args[0].target await ssdp_callback( ssdp.SsdpServiceInfo( ssdp_usn=MOCK_DEVICE_USN, diff --git a/tests/components/dlna_dms/test_media_source.py b/tests/components/dlna_dms/test_media_source.py index 35f34d0689b..641232e356a 100644 --- a/tests/components/dlna_dms/test_media_source.py +++ b/tests/components/dlna_dms/test_media_source.py @@ -1,4 +1,5 @@ """Tests for dlna_dms.media_source, mostly testing DmsMediaSource.""" + from unittest.mock import ANY, Mock from async_upnp_client.exceptions import UpnpError diff --git a/tests/components/dnsip/__init__.py b/tests/components/dnsip/__init__.py index 1a465b59ab6..d98de181892 100644 --- a/tests/components/dnsip/__init__.py +++ b/tests/components/dnsip/__init__.py @@ -1,4 +1,5 @@ """Tests for the dnsip integration.""" + from __future__ import annotations diff --git a/tests/components/dnsip/test_config_flow.py b/tests/components/dnsip/test_config_flow.py index 7e219326ee9..5bfa1539d44 100644 --- a/tests/components/dnsip/test_config_flow.py +++ b/tests/components/dnsip/test_config_flow.py @@ -1,4 +1,5 @@ """Test the dnsip config flow.""" + from __future__ import annotations from unittest.mock import patch @@ -35,13 +36,16 @@ async def test_form(hass: HomeAssistant) -> None: assert result["data_schema"] == DATA_SCHEMA assert result["errors"] == {} - with patch( - "homeassistant.components.dnsip.config_flow.aiodns.DNSResolver", - return_value=RetrieveDNS(), - ), patch( - "homeassistant.components.dnsip.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.dnsip.config_flow.aiodns.DNSResolver", + return_value=RetrieveDNS(), + ), + patch( + "homeassistant.components.dnsip.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -75,13 +79,16 @@ async def test_form_adv(hass: HomeAssistant) -> None: assert result["data_schema"] == DATA_SCHEMA_ADV - with patch( - "homeassistant.components.dnsip.config_flow.aiodns.DNSResolver", - return_value=RetrieveDNS(), - ), patch( - "homeassistant.components.dnsip.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.dnsip.config_flow.aiodns.DNSResolver", + return_value=RetrieveDNS(), + ), + patch( + "homeassistant.components.dnsip.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -153,12 +160,15 @@ async def test_flow_already_exist(hass: HomeAssistant) -> None: ) dns_mock = RetrieveDNS() - with patch( - "homeassistant.components.dnsip.async_setup_entry", - return_value=True, - ), patch( - "homeassistant.components.dnsip.config_flow.aiodns.DNSResolver", - return_value=dns_mock, + with ( + patch( + "homeassistant.components.dnsip.async_setup_entry", + return_value=True, + ), + patch( + "homeassistant.components.dnsip.config_flow.aiodns.DNSResolver", + return_value=dns_mock, + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -220,6 +230,61 @@ async def test_options_flow(hass: HomeAssistant) -> None: assert entry.state == config_entries.ConfigEntryState.LOADED +async def test_options_flow_empty_return(hass: HomeAssistant) -> None: + """Test options config flow with empty return from user.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="12345", + data={ + CONF_HOSTNAME: "home-assistant.io", + CONF_NAME: "home-assistant.io", + CONF_IPV4: True, + CONF_IPV6: False, + }, + options={ + CONF_RESOLVER: "8.8.8.8", + CONF_RESOLVER_IPV6: "2620:119:53::1", + }, + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.dnsip.config_flow.aiodns.DNSResolver", + return_value=RetrieveDNS(), + ): + assert 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" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={}, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["data"] == { + "resolver": "208.67.222.222", + "resolver_ipv6": "2620:119:53::53", + } + + entry = hass.config_entries.async_get_entry(entry.entry_id) + assert entry.data == { + "hostname": "home-assistant.io", + "ipv4": True, + "ipv6": False, + "name": "home-assistant.io", + } + assert entry.options == { + "resolver": "208.67.222.222", + "resolver_ipv6": "2620:119:53::53", + } + + @pytest.mark.parametrize( "p_input", [ diff --git a/tests/components/dnsip/test_init.py b/tests/components/dnsip/test_init.py index 2869f13ca87..37595444c44 100644 --- a/tests/components/dnsip/test_init.py +++ b/tests/components/dnsip/test_init.py @@ -1,4 +1,5 @@ """Test for DNS IP component Init.""" + from __future__ import annotations from unittest.mock import patch diff --git a/tests/components/dnsip/test_sensor.py b/tests/components/dnsip/test_sensor.py index 6fd24ad9b13..e1353d83268 100644 --- a/tests/components/dnsip/test_sensor.py +++ b/tests/components/dnsip/test_sensor.py @@ -1,4 +1,5 @@ """The test for the DNS IP sensor platform.""" + from __future__ import annotations from datetime import timedelta diff --git a/tests/components/doorbird/test_config_flow.py b/tests/components/doorbird/test_config_flow.py index 7ad7fbe07ac..4939bada6f8 100644 --- a/tests/components/doorbird/test_config_flow.py +++ b/tests/components/doorbird/test_config_flow.py @@ -1,4 +1,5 @@ """Test the DoorBird config flow.""" + from ipaddress import ip_address from unittest.mock import MagicMock, Mock, patch @@ -51,15 +52,19 @@ async def test_user_form(hass: HomeAssistant) -> None: doorbirdapi = _get_mock_doorbirdapi_return_values( ready=[True], info={"WIFI_MAC_ADDR": "macaddr"} ) - with patch( - "homeassistant.components.doorbird.config_flow.DoorBird", - return_value=doorbirdapi, - ), patch( - "homeassistant.components.doorbird.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.doorbird.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.doorbird.config_flow.DoorBird", + return_value=doorbirdapi, + ), + patch( + "homeassistant.components.doorbird.async_setup", return_value=True + ) as mock_setup, + patch( + "homeassistant.components.doorbird.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], VALID_CONFIG, @@ -194,15 +199,20 @@ async def test_form_zeroconf_correct_oui(hass: HomeAssistant) -> None: assert result["step_id"] == "user" assert result["errors"] == {} - with patch( - "homeassistant.components.doorbird.config_flow.DoorBird", - return_value=doorbirdapi, - ), patch("homeassistant.components.logbook.async_setup", return_value=True), patch( - "homeassistant.components.doorbird.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.doorbird.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.doorbird.config_flow.DoorBird", + return_value=doorbirdapi, + ), + patch("homeassistant.components.logbook.async_setup", return_value=True), + patch( + "homeassistant.components.doorbird.async_setup", return_value=True + ) as mock_setup, + patch( + "homeassistant.components.doorbird.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], VALID_CONFIG ) diff --git a/tests/components/dormakaba_dkey/test_config_flow.py b/tests/components/dormakaba_dkey/test_config_flow.py index 8c0156e221b..d29e176bb7e 100644 --- a/tests/components/dormakaba_dkey/test_config_flow.py +++ b/tests/components/dormakaba_dkey/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Dormakaba dKey config flow.""" + from unittest.mock import patch from bleak.exc import BleakError @@ -164,13 +165,16 @@ async def test_bluetooth_step_success(hass: HomeAssistant) -> None: async def _test_common_success(hass: HomeAssistant, result: FlowResult) -> None: """Test bluetooth and user flow success paths.""" - with patch( - "homeassistant.components.dormakaba_dkey.config_flow.DKEYLock.associate", - return_value=AssociationData(b"1234", b"AABBCCDD"), - ) as mock_associate, patch( - "homeassistant.components.dormakaba_dkey.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.dormakaba_dkey.config_flow.DKEYLock.associate", + return_value=AssociationData(b"1234", b"AABBCCDD"), + ) as mock_associate, + patch( + "homeassistant.components.dormakaba_dkey.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"activation_code": "1234-1234"} ) @@ -221,10 +225,10 @@ async def test_bluetooth_step_already_in_progress(hass: HomeAssistant) -> None: @pytest.mark.parametrize( ("exc", "error"), - ( + [ (BleakError, "cannot_connect"), (Exception, "unknown"), - ), + ], ) async def test_bluetooth_step_cannot_connect(hass: HomeAssistant, exc, error) -> None: """Test bluetooth step and we cannot connect.""" @@ -260,10 +264,10 @@ async def test_bluetooth_step_cannot_connect(hass: HomeAssistant, exc, error) -> @pytest.mark.parametrize( ("exc", "error"), - ( + [ (dkey_errors.InvalidActivationCode, "invalid_code"), (dkey_errors.WrongActivationCode, "wrong_code"), - ), + ], ) async def test_bluetooth_step_cannot_associate(hass: HomeAssistant, exc, error) -> None: """Test bluetooth step and we cannot associate.""" @@ -342,12 +346,15 @@ async def test_reauth(hass: HomeAssistant) -> None: assert result["step_id"] == "associate" assert result["errors"] is None - with patch( - "homeassistant.components.dormakaba_dkey.config_flow.DKEYLock.associate", - return_value=AssociationData(b"1234", b"AABBCCDD"), - ) as mock_associate, patch( - "homeassistant.components.dormakaba_dkey.async_setup_entry", - return_value=True, + with ( + patch( + "homeassistant.components.dormakaba_dkey.config_flow.DKEYLock.associate", + return_value=AssociationData(b"1234", b"AABBCCDD"), + ) as mock_associate, + patch( + "homeassistant.components.dormakaba_dkey.async_setup_entry", + return_value=True, + ), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"activation_code": "1234-1234"} diff --git a/tests/components/downloader/__init__.py b/tests/components/downloader/__init__.py new file mode 100644 index 00000000000..abf11631bb9 --- /dev/null +++ b/tests/components/downloader/__init__.py @@ -0,0 +1 @@ +"""Tests for the downloader component.""" diff --git a/tests/components/downloader/test_config_flow.py b/tests/components/downloader/test_config_flow.py new file mode 100644 index 00000000000..5e75a9b33ba --- /dev/null +++ b/tests/components/downloader/test_config_flow.py @@ -0,0 +1,101 @@ +"""Test the Downloader config flow.""" + +from unittest.mock import patch + +import pytest + +from homeassistant import config_entries +from homeassistant.components.downloader.config_flow import DirectoryDoesNotExist +from homeassistant.components.downloader.const import CONF_DOWNLOAD_DIR, DOMAIN +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from tests.common import MockConfigEntry + +CONFIG = {CONF_DOWNLOAD_DIR: "download_dir"} + + +async def test_user_form(hass: HomeAssistant) -> None: + """Test the full user configuration flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + with patch( + "homeassistant.components.downloader.async_setup_entry", + return_value=True, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=CONFIG, + ) + assert result["type"] == FlowResultType.FORM + + with patch( + "homeassistant.components.downloader.config_flow.DownloaderConfigFlow._validate_input", + side_effect=DirectoryDoesNotExist, + ): + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": "cannot_connect"} + + with ( + patch( + "homeassistant.components.downloader.async_setup_entry", return_value=True + ), + patch( + "homeassistant.components.downloader.config_flow.DownloaderConfigFlow._validate_input", + return_value=None, + ), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=CONFIG, + ) + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "Downloader" + assert result["data"] == {"download_dir": "download_dir"} + + +@pytest.mark.parametrize("source", [SOURCE_USER, SOURCE_IMPORT]) +async def test_single_instance_allowed( + hass: HomeAssistant, + source: str, +) -> None: + """Test we abort if already setup.""" + mock_config_entry = MockConfigEntry(domain=DOMAIN) + + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": source} + ) + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "single_instance_allowed" + + +async def test_import_flow_success(hass: HomeAssistant) -> None: + """Test import flow.""" + with ( + patch( + "homeassistant.components.downloader.async_setup_entry", return_value=True + ), + patch( + "homeassistant.components.downloader.config_flow.DownloaderConfigFlow._validate_input", + return_value=None, + ), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={}, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "Downloader" + assert result["data"] == {} + assert result["options"] == {} diff --git a/tests/components/dremel_3d_printer/conftest.py b/tests/components/dremel_3d_printer/conftest.py index 8df59a2e64a..0284d8baebf 100644 --- a/tests/components/dremel_3d_printer/conftest.py +++ b/tests/components/dremel_3d_printer/conftest.py @@ -1,4 +1,5 @@ """Configure tests for the Dremel 3D Printer integration.""" + from http import HTTPStatus from unittest.mock import patch diff --git a/tests/components/dremel_3d_printer/test_binary_sensor.py b/tests/components/dremel_3d_printer/test_binary_sensor.py index 081cc7a02fb..6581b6ff13d 100644 --- a/tests/components/dremel_3d_printer/test_binary_sensor.py +++ b/tests/components/dremel_3d_printer/test_binary_sensor.py @@ -1,4 +1,5 @@ """Binary sensor tests for the Dremel 3D Printer integration.""" + from unittest.mock import AsyncMock from homeassistant.components.binary_sensor import BinarySensorDeviceClass diff --git a/tests/components/dremel_3d_printer/test_button.py b/tests/components/dremel_3d_printer/test_button.py index 00102b3306b..48b39b09cf1 100644 --- a/tests/components/dremel_3d_printer/test_button.py +++ b/tests/components/dremel_3d_printer/test_button.py @@ -1,4 +1,5 @@ """Button tests for the Dremel 3D Printer integration.""" + from unittest.mock import AsyncMock, patch import pytest @@ -15,11 +16,11 @@ from tests.common import MockConfigEntry @pytest.mark.parametrize( ("button", "function"), - ( + [ ("cancel", "stop"), ("pause", "pause"), ("resume", "resume"), - ), + ], ) async def test_buttons( hass: HomeAssistant, @@ -43,10 +44,13 @@ async def test_buttons( ) assert mock.call_count == 1 - with patch( - f"homeassistant.components.dremel_3d_printer.Dremel3DPrinter.{function}_print", - side_effect=RuntimeError, - ) as mock, pytest.raises(HomeAssistantError): + with ( + patch( + f"homeassistant.components.dremel_3d_printer.Dremel3DPrinter.{function}_print", + side_effect=RuntimeError, + ) as mock, + pytest.raises(HomeAssistantError), + ): await hass.services.async_call( BUTTON_DOMAIN, SERVICE_PRESS, diff --git a/tests/components/dremel_3d_printer/test_config_flow.py b/tests/components/dremel_3d_printer/test_config_flow.py index e968e0af491..938068aa9b0 100644 --- a/tests/components/dremel_3d_printer/test_config_flow.py +++ b/tests/components/dremel_3d_printer/test_config_flow.py @@ -1,4 +1,5 @@ """Test Dremel 3D Printer config flow.""" + from unittest.mock import patch from requests.exceptions import ConnectTimeout diff --git a/tests/components/dremel_3d_printer/test_init.py b/tests/components/dremel_3d_printer/test_init.py index fa41b74a5d2..8216054587d 100644 --- a/tests/components/dremel_3d_printer/test_init.py +++ b/tests/components/dremel_3d_printer/test_init.py @@ -1,4 +1,5 @@ """Test Dremel 3D Printer integration.""" + from datetime import timedelta from unittest.mock import patch diff --git a/tests/components/dremel_3d_printer/test_sensor.py b/tests/components/dremel_3d_printer/test_sensor.py index 49d66fe1e61..c1e3a9bc14b 100644 --- a/tests/components/dremel_3d_printer/test_sensor.py +++ b/tests/components/dremel_3d_printer/test_sensor.py @@ -1,4 +1,5 @@ """Sensor tests for the Dremel 3D Printer integration.""" + from datetime import datetime from unittest.mock import AsyncMock diff --git a/tests/components/drop_connect/snapshots/test_binary_sensor.ambr b/tests/components/drop_connect/snapshots/test_binary_sensor.ambr index 8f39e76538c..c42cdb8cde1 100644 --- a/tests/components/drop_connect/snapshots/test_binary_sensor.ambr +++ b/tests/components/drop_connect/snapshots/test_binary_sensor.ambr @@ -41,6 +41,7 @@ 'context': , 'entity_id': 'binary_sensor.hub_drop_1_c0ffee_leak_detected', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }) @@ -86,6 +87,7 @@ 'context': , 'entity_id': 'binary_sensor.hub_drop_1_c0ffee_notification_unread', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }) @@ -132,6 +134,7 @@ 'context': , 'entity_id': 'binary_sensor.leak_detector_leak_detected', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }) @@ -178,6 +181,7 @@ 'context': , 'entity_id': 'binary_sensor.protection_valve_leak_detected', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }) @@ -224,6 +228,7 @@ 'context': , 'entity_id': 'binary_sensor.pump_controller_leak_detected', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }) @@ -269,6 +274,7 @@ 'context': , 'entity_id': 'binary_sensor.pump_controller_pump_status', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }) @@ -315,6 +321,7 @@ 'context': , 'entity_id': 'binary_sensor.ro_filter_leak_detected', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }) @@ -360,6 +367,7 @@ 'context': , 'entity_id': 'binary_sensor.softener_reserve_capacity_in_use', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }) diff --git a/tests/components/drop_connect/test_config_flow.py b/tests/components/drop_connect/test_config_flow.py index fb727d2c7fd..180b6fef860 100644 --- a/tests/components/drop_connect/test_config_flow.py +++ b/tests/components/drop_connect/test_config_flow.py @@ -1,4 +1,5 @@ """Test config flow.""" + from homeassistant import config_entries from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType diff --git a/tests/components/dsmr/conftest.py b/tests/components/dsmr/conftest.py index 01aff5ae48e..05881d9c877 100644 --- a/tests/components/dsmr/conftest.py +++ b/tests/components/dsmr/conftest.py @@ -1,4 +1,5 @@ """Common test tools.""" + import asyncio from unittest.mock import MagicMock, patch @@ -29,11 +30,15 @@ async def dsmr_connection_fixture(hass): connection_factory = MagicMock(wraps=connection_factory) - with patch( - "homeassistant.components.dsmr.sensor.create_dsmr_reader", connection_factory - ), patch( - "homeassistant.components.dsmr.sensor.create_tcp_dsmr_reader", - connection_factory, + with ( + patch( + "homeassistant.components.dsmr.sensor.create_dsmr_reader", + connection_factory, + ), + patch( + "homeassistant.components.dsmr.sensor.create_tcp_dsmr_reader", + connection_factory, + ), ): yield (connection_factory, transport, protocol) @@ -51,12 +56,15 @@ async def rfxtrx_dsmr_connection_fixture(hass): connection_factory = MagicMock(wraps=connection_factory) - with patch( - "homeassistant.components.dsmr.sensor.create_rfxtrx_dsmr_reader", - connection_factory, - ), patch( - "homeassistant.components.dsmr.sensor.create_rfxtrx_tcp_dsmr_reader", - connection_factory, + with ( + patch( + "homeassistant.components.dsmr.sensor.create_rfxtrx_dsmr_reader", + connection_factory, + ), + patch( + "homeassistant.components.dsmr.sensor.create_rfxtrx_tcp_dsmr_reader", + connection_factory, + ), ): yield (connection_factory, transport, protocol) @@ -129,12 +137,15 @@ async def dsmr_connection_send_validate_fixture(hass): protocol.wait_closed = wait_closed - with patch( - "homeassistant.components.dsmr.config_flow.create_dsmr_reader", - connection_factory, - ), patch( - "homeassistant.components.dsmr.config_flow.create_tcp_dsmr_reader", - connection_factory, + with ( + patch( + "homeassistant.components.dsmr.config_flow.create_dsmr_reader", + connection_factory, + ), + patch( + "homeassistant.components.dsmr.config_flow.create_tcp_dsmr_reader", + connection_factory, + ), ): yield (connection_factory, transport, protocol) @@ -175,11 +186,14 @@ async def rfxtrx_dsmr_connection_send_validate_fixture(hass): protocol.wait_closed = wait_closed - with patch( - "homeassistant.components.dsmr.config_flow.create_rfxtrx_dsmr_reader", - connection_factory, - ), patch( - "homeassistant.components.dsmr.config_flow.create_rfxtrx_tcp_dsmr_reader", - connection_factory, + with ( + patch( + "homeassistant.components.dsmr.config_flow.create_rfxtrx_dsmr_reader", + connection_factory, + ), + patch( + "homeassistant.components.dsmr.config_flow.create_rfxtrx_tcp_dsmr_reader", + connection_factory, + ), ): yield (connection_factory, transport, protocol) diff --git a/tests/components/dsmr/test_config_flow.py b/tests/components/dsmr/test_config_flow.py index 2d44b67e870..687c6b4a3bc 100644 --- a/tests/components/dsmr/test_config_flow.py +++ b/tests/components/dsmr/test_config_flow.py @@ -1,4 +1,5 @@ """Test the DSMR config flow.""" + from itertools import chain, repeat import os from typing import Any @@ -494,9 +495,10 @@ async def test_options_flow(hass: HomeAssistant) -> None: }, ) - with patch( - "homeassistant.components.dsmr.async_setup_entry", return_value=True - ), patch("homeassistant.components.dsmr.async_unload_entry", return_value=True): + 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.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 index b42f26f4ccc..11487b6b87b 100644 --- a/tests/components/dsmr/test_init.py +++ b/tests/components/dsmr/test_init.py @@ -1,4 +1,5 @@ """Tests for the DSMR integration.""" + from unittest.mock import MagicMock import pytest @@ -77,7 +78,6 @@ from tests.common import MockConfigEntry ("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"), diff --git a/tests/components/dsmr/test_mbus_migration.py b/tests/components/dsmr/test_mbus_migration.py index 5e31fa7a82e..95def2f66cf 100644 --- a/tests/components/dsmr/test_mbus_migration.py +++ b/tests/components/dsmr/test_mbus_migration.py @@ -1,4 +1,5 @@ """Tests for the DSMR integration.""" + import datetime from decimal import Decimal diff --git a/tests/components/dsmr/test_sensor.py b/tests/components/dsmr/test_sensor.py index cd3e60c7aeb..f63422e0543 100644 --- a/tests/components/dsmr/test_sensor.py +++ b/tests/components/dsmr/test_sensor.py @@ -4,6 +4,7 @@ Tests setup of the DSMR component and ensure incoming telegrams cause Entity to be updated with new values. """ + import asyncio import datetime from decimal import Decimal diff --git a/tests/components/dsmr_reader/test_config_flow.py b/tests/components/dsmr_reader/test_config_flow.py index 42d18d866a9..cc605eaa49c 100644 --- a/tests/components/dsmr_reader/test_config_flow.py +++ b/tests/components/dsmr_reader/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for the config flow.""" + from homeassistant.components.dsmr_reader.const import DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.core import HomeAssistant diff --git a/tests/components/dte_energy_bridge/test_sensor.py b/tests/components/dte_energy_bridge/test_sensor.py index f62520701bd..244bec4e270 100644 --- a/tests/components/dte_energy_bridge/test_sensor.py +++ b/tests/components/dte_energy_bridge/test_sensor.py @@ -1,4 +1,5 @@ """The tests for the DTE Energy Bridge.""" + import requests_mock from homeassistant.core import HomeAssistant diff --git a/tests/components/duckdns/test_init.py b/tests/components/duckdns/test_init.py index c890fa78e2f..d019861af1b 100644 --- a/tests/components/duckdns/test_init.py +++ b/tests/components/duckdns/test_init.py @@ -1,4 +1,5 @@ """Test the DuckDNS component.""" + from datetime import timedelta import logging diff --git a/tests/components/dunehd/test_config_flow.py b/tests/components/dunehd/test_config_flow.py index 18f00281fc7..bf3137e0204 100644 --- a/tests/components/dunehd/test_config_flow.py +++ b/tests/components/dunehd/test_config_flow.py @@ -1,4 +1,5 @@ """Define tests for the Dune HD config flow.""" + from unittest.mock import patch from homeassistant import data_entry_flow @@ -68,8 +69,9 @@ async def test_duplicate_error(hass: HomeAssistant) -> None: async def test_create_entry(hass: HomeAssistant) -> None: """Test that the user step works.""" - with patch("homeassistant.components.dunehd.async_setup_entry"), patch( - "pdunehd.DuneHDPlayer.update_state", return_value=DUNEHD_STATE + with ( + patch("homeassistant.components.dunehd.async_setup_entry"), + patch("pdunehd.DuneHDPlayer.update_state", return_value=DUNEHD_STATE), ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONFIG_HOSTNAME @@ -82,8 +84,9 @@ async def test_create_entry(hass: HomeAssistant) -> None: async def test_create_entry_with_ipv6_address(hass: HomeAssistant) -> None: """Test that the user step works with device IPv6 address..""" - with patch("homeassistant.components.dunehd.async_setup_entry"), patch( - "pdunehd.DuneHDPlayer.update_state", return_value=DUNEHD_STATE + with ( + patch("homeassistant.components.dunehd.async_setup_entry"), + patch("pdunehd.DuneHDPlayer.update_state", return_value=DUNEHD_STATE), ): result = await hass.config_entries.flow.async_init( DOMAIN, diff --git a/tests/components/duotecno/conftest.py b/tests/components/duotecno/conftest.py index 82c3e0c7f44..c79210bdfe0 100644 --- a/tests/components/duotecno/conftest.py +++ b/tests/components/duotecno/conftest.py @@ -1,4 +1,5 @@ """Common fixtures for the duotecno tests.""" + from collections.abc import Generator from unittest.mock import AsyncMock, patch diff --git a/tests/components/duotecno/test_config_flow.py b/tests/components/duotecno/test_config_flow.py index a02fea8008c..b62b6e90801 100644 --- a/tests/components/duotecno/test_config_flow.py +++ b/tests/components/duotecno/test_config_flow.py @@ -1,4 +1,5 @@ """Test the duotecno config flow.""" + from unittest.mock import AsyncMock, patch from duotecno.exceptions import InvalidPassword diff --git a/tests/components/dynalite/common.py b/tests/components/dynalite/common.py index 355a1285a56..91458b0aaff 100644 --- a/tests/components/dynalite/common.py +++ b/tests/components/dynalite/common.py @@ -1,4 +1,5 @@ """Common functions for tests.""" + from unittest.mock import AsyncMock, Mock, call, patch from homeassistant.components import dynalite diff --git a/tests/components/dynalite/conftest.py b/tests/components/dynalite/conftest.py index 59f109e7e47..4d193cbb38b 100644 --- a/tests/components/dynalite/conftest.py +++ b/tests/components/dynalite/conftest.py @@ -1,2 +1,3 @@ """dynalite conftest.""" + from tests.components.light.conftest import mock_light_profiles # noqa: F401 diff --git a/tests/components/dynalite/test_bridge.py b/tests/components/dynalite/test_bridge.py index efadfd0b3f8..b0517b89031 100644 --- a/tests/components/dynalite/test_bridge.py +++ b/tests/components/dynalite/test_bridge.py @@ -1,4 +1,5 @@ """Test Dynalite bridge.""" + from unittest.mock import AsyncMock, Mock, patch from dynalite_devices_lib.dynalite_devices import ( diff --git a/tests/components/dynalite/test_config_flow.py b/tests/components/dynalite/test_config_flow.py index f337c7c3e74..724cb616deb 100644 --- a/tests/components/dynalite/test_config_flow.py +++ b/tests/components/dynalite/test_config_flow.py @@ -1,4 +1,5 @@ """Test Dynalite config flow.""" + from unittest.mock import AsyncMock, patch import pytest diff --git a/tests/components/dynalite/test_cover.py b/tests/components/dynalite/test_cover.py index 8ee205741e4..c43d349d184 100644 --- a/tests/components/dynalite/test_cover.py +++ b/tests/components/dynalite/test_cover.py @@ -1,4 +1,5 @@ """Test Dynalite cover.""" + from unittest.mock import Mock from dynalite_devices_lib.cover import DynaliteTimeCoverWithTiltDevice diff --git a/tests/components/dynalite/test_init.py b/tests/components/dynalite/test_init.py index a95ef72f61f..2c15c41e40b 100644 --- a/tests/components/dynalite/test_init.py +++ b/tests/components/dynalite/test_init.py @@ -1,4 +1,5 @@ """Test Dynalite __init__.""" + from unittest.mock import call, patch import pytest @@ -84,13 +85,16 @@ async def test_async_setup(hass: HomeAssistant) -> None: async def test_service_request_area_preset(hass: HomeAssistant) -> None: """Test requesting and area preset via service call.""" - with patch( - "homeassistant.components.dynalite.bridge.DynaliteDevices.async_setup", - return_value=True, - ), patch( - "dynalite_devices_lib.dynalite.Dynalite.request_area_preset", - return_value=True, - ) as mock_req_area_pres: + with ( + patch( + "homeassistant.components.dynalite.bridge.DynaliteDevices.async_setup", + return_value=True, + ), + patch( + "dynalite_devices_lib.dynalite.Dynalite.request_area_preset", + return_value=True, + ) as mock_req_area_pres, + ): assert await async_setup_component( hass, dynalite.DOMAIN, @@ -156,13 +160,16 @@ async def test_service_request_area_preset(hass: HomeAssistant) -> None: async def test_service_request_channel_level(hass: HomeAssistant) -> None: """Test requesting the level of a channel via service call.""" - with patch( - "homeassistant.components.dynalite.bridge.DynaliteDevices.async_setup", - return_value=True, - ), patch( - "dynalite_devices_lib.dynalite.Dynalite.request_channel_level", - return_value=True, - ) as mock_req_chan_lvl: + with ( + patch( + "homeassistant.components.dynalite.bridge.DynaliteDevices.async_setup", + return_value=True, + ), + patch( + "dynalite_devices_lib.dynalite.Dynalite.request_channel_level", + return_value=True, + ) as mock_req_chan_lvl, + ): assert await async_setup_component( hass, dynalite.DOMAIN, diff --git a/tests/components/dynalite/test_light.py b/tests/components/dynalite/test_light.py index a17c336efb0..901544cdf27 100644 --- a/tests/components/dynalite/test_light.py +++ b/tests/components/dynalite/test_light.py @@ -1,4 +1,5 @@ """Test Dynalite light.""" + from unittest.mock import Mock, PropertyMock from dynalite_devices_lib.light import DynaliteChannelLightDevice diff --git a/tests/components/dynalite/test_panel.py b/tests/components/dynalite/test_panel.py index a0acad54551..a1cd9749eb5 100644 --- a/tests/components/dynalite/test_panel.py +++ b/tests/components/dynalite/test_panel.py @@ -1,6 +1,5 @@ """Test websocket commands for the panel.""" - from unittest.mock import patch from homeassistant.components import dynalite diff --git a/tests/components/dynalite/test_switch.py b/tests/components/dynalite/test_switch.py index 9871c0817d3..97fb14f2840 100644 --- a/tests/components/dynalite/test_switch.py +++ b/tests/components/dynalite/test_switch.py @@ -1,4 +1,5 @@ """Test Dynalite switch.""" + from unittest.mock import Mock from dynalite_devices_lib.switch import DynalitePresetSwitchDevice diff --git a/tests/components/eafm/test_config_flow.py b/tests/components/eafm/test_config_flow.py index 208f406d8b9..e8f86154e67 100644 --- a/tests/components/eafm/test_config_flow.py +++ b/tests/components/eafm/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for eafm config flow.""" + from unittest.mock import patch import pytest diff --git a/tests/components/eafm/test_sensor.py b/tests/components/eafm/test_sensor.py index 04659a79d1c..380e1df5f37 100644 --- a/tests/components/eafm/test_sensor.py +++ b/tests/components/eafm/test_sensor.py @@ -1,4 +1,5 @@ """Tests for polling measures.""" + import datetime import aiohttp diff --git a/tests/components/easyenergy/conftest.py b/tests/components/easyenergy/conftest.py index 442221c0147..dd8abae4d4a 100644 --- a/tests/components/easyenergy/conftest.py +++ b/tests/components/easyenergy/conftest.py @@ -1,4 +1,5 @@ """Fixtures for easyEnergy integration tests.""" + from collections.abc import Generator import json from unittest.mock import AsyncMock, MagicMock, patch diff --git a/tests/components/easyenergy/test_config_flow.py b/tests/components/easyenergy/test_config_flow.py index 30d4924db8c..4e76d48b663 100644 --- a/tests/components/easyenergy/test_config_flow.py +++ b/tests/components/easyenergy/test_config_flow.py @@ -1,4 +1,5 @@ """Test the easyEnergy config flow.""" + from unittest.mock import MagicMock from homeassistant.components.easyenergy.const import DOMAIN diff --git a/tests/components/easyenergy/test_diagnostics.py b/tests/components/easyenergy/test_diagnostics.py index f76821cf265..d0eb9de3b00 100644 --- a/tests/components/easyenergy/test_diagnostics.py +++ b/tests/components/easyenergy/test_diagnostics.py @@ -1,4 +1,5 @@ """Tests for the diagnostics data provided by the easyEnergy integration.""" + from unittest.mock import MagicMock from easyenergy import EasyEnergyNoDataError diff --git a/tests/components/easyenergy/test_init.py b/tests/components/easyenergy/test_init.py index ed12d805e7a..74293049fd1 100644 --- a/tests/components/easyenergy/test_init.py +++ b/tests/components/easyenergy/test_init.py @@ -1,4 +1,5 @@ """Tests for the easyEnergy integration.""" + from unittest.mock import MagicMock, patch from easyenergy import EasyEnergyConnectionError diff --git a/tests/components/ecobee/__init__.py b/tests/components/ecobee/__init__.py index 3dba80090d4..52c6fcc6a4e 100644 --- a/tests/components/ecobee/__init__.py +++ b/tests/components/ecobee/__init__.py @@ -39,8 +39,10 @@ GENERIC_THERMOSTAT_INFO = { "running": True, "type": "hold", "holdClimateRef": "away", - "endDate": "2022-01-01 10:00:00", - "startDate": "2022-02-02 11:00:00", + "startDate": "2022-02-02", + "startTime": "11:00:00", + "endDate": "2022-01-01", + "endTime": "10:00:00", } ], "remoteSensors": [ @@ -99,8 +101,10 @@ GENERIC_THERMOSTAT_INFO_WITH_HEATPUMP = { "running": True, "type": "hold", "holdClimateRef": "away", - "endDate": "2022-01-01 10:00:00", - "startDate": "2022-02-02 11:00:00", + "startDate": "2022-02-02", + "startTime": "11:00:00", + "endDate": "2022-01-01", + "endTime": "10:00:00", } ], "remoteSensors": [ diff --git a/tests/components/ecobee/common.py b/tests/components/ecobee/common.py index ff9bc1b61fd..60f17c3618d 100644 --- a/tests/components/ecobee/common.py +++ b/tests/components/ecobee/common.py @@ -1,4 +1,5 @@ """Common methods used across tests for Ecobee.""" + from unittest.mock import patch from homeassistant.components.ecobee.const import CONF_REFRESH_TOKEN, DOMAIN diff --git a/tests/components/ecobee/conftest.py b/tests/components/ecobee/conftest.py index 05700fa2e20..952c2f3fba3 100644 --- a/tests/components/ecobee/conftest.py +++ b/tests/components/ecobee/conftest.py @@ -1,4 +1,5 @@ """Fixtures for tests.""" + from unittest.mock import MagicMock, patch import pytest diff --git a/tests/components/ecobee/fixtures/ecobee-data.json b/tests/components/ecobee/fixtures/ecobee-data.json index 9fe19af35c6..d9406c20c3b 100644 --- a/tests/components/ecobee/fixtures/ecobee-data.json +++ b/tests/components/ecobee/fixtures/ecobee-data.json @@ -42,8 +42,10 @@ "running": true, "type": "hold", "holdClimateRef": "away", - "endDate": "2022-01-01 10:00:00", - "startDate": "2022-02-02 11:00:00" + "startDate": "2022-02-02", + "startTime": "11:00:00", + "endDate": "2022-01-01", + "endTime": "10:00:00" } ], "remoteSensors": [ @@ -110,8 +112,10 @@ "running": true, "type": "hold", "holdClimateRef": "away", - "endDate": "2022-01-01 10:00:00", - "startDate": "2022-02-02 11:00:00" + "startDate": "2022-02-02", + "startTime": "11:00:00", + "endDate": "2022-01-01", + "endTime": "10:00:00" } ], "remoteSensors": [ diff --git a/tests/components/ecobee/test_climate.py b/tests/components/ecobee/test_climate.py index 642e4830016..0ec4f9cee68 100644 --- a/tests/components/ecobee/test_climate.py +++ b/tests/components/ecobee/test_climate.py @@ -1,4 +1,5 @@ """The test for the Ecobee thermostat module.""" + import copy from http import HTTPStatus from unittest import mock @@ -8,7 +9,11 @@ import pytest from homeassistant.components import climate from homeassistant.components.climate import ClimateEntityFeature -from homeassistant.components.ecobee.climate import ECOBEE_AUX_HEAT_ONLY, Thermostat +from homeassistant.components.ecobee.climate import ( + ECOBEE_AUX_HEAT_ONLY, + PRESET_AWAY_INDEFINITELY, + Thermostat, +) import homeassistant.const as const from homeassistant.const import ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, STATE_OFF from homeassistant.core import HomeAssistant @@ -30,6 +35,7 @@ def ecobee_fixture(): "climates": [ {"name": "Climate1", "climateRef": "c1"}, {"name": "Climate2", "climateRef": "c2"}, + {"name": "Away", "climateRef": "away"}, ], "currentClimateRef": "c1", }, @@ -55,9 +61,11 @@ def ecobee_fixture(): "name": "Event1", "running": True, "type": "hold", - "holdClimateRef": "away", - "endDate": "2017-01-01 10:00:00", - "startDate": "2017-02-02 11:00:00", + "holdClimateRef": "c1", + "startDate": "2017-02-02", + "startTime": "11:00:00", + "endDate": "2017-01-01", + "endTime": "10:00:00", } ], } @@ -427,3 +435,30 @@ async def test_turn_aux_heat_off(hass: HomeAssistant, mock_ecobee: MagicMock) -> ) assert mock_ecobee.set_hvac_mode.call_count == 1 assert mock_ecobee.set_hvac_mode.call_args == mock.call(0, "auto") + + +async def test_preset_indefinite_away(ecobee_fixture, thermostat) -> None: + """Test indefinite away showing correctly, and not as temporary away.""" + ecobee_fixture["program"]["currentClimateRef"] = "away" + ecobee_fixture["events"][0]["holdClimateRef"] = "away" + assert thermostat.preset_mode == "Away" + + ecobee_fixture["events"][0]["endDate"] = "2999-01-01" + assert thermostat.preset_mode == PRESET_AWAY_INDEFINITELY + + +async def test_set_preset_mode(ecobee_fixture, thermostat, data) -> None: + """Test set preset mode.""" + # Set a preset provided by ecobee. + data.reset_mock() + thermostat.set_preset_mode("Climate2") + data.ecobee.set_climate_hold.assert_has_calls( + [mock.call(1, "c2", thermostat.hold_preference(), thermostat.hold_hours())] + ) + + # Set the indefinite away preset provided by this integration. + data.reset_mock() + thermostat.set_preset_mode(PRESET_AWAY_INDEFINITELY) + data.ecobee.set_climate_hold.assert_has_calls( + [mock.call(1, "away", "indefinite", thermostat.hold_hours())] + ) diff --git a/tests/components/ecobee/test_config_flow.py b/tests/components/ecobee/test_config_flow.py index a0f34e3cd21..91d9f848ffd 100644 --- a/tests/components/ecobee/test_config_flow.py +++ b/tests/components/ecobee/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for the ecobee config flow.""" + from unittest.mock import patch from pyecobee import ECOBEE_API_KEY, ECOBEE_REFRESH_TOKEN @@ -143,10 +144,13 @@ async def test_import_flow_triggered_with_ecobee_conf_and_valid_data_and_valid_t MOCK_ECOBEE_CONF = {ECOBEE_API_KEY: None, ECOBEE_REFRESH_TOKEN: None} - with patch( - "homeassistant.components.ecobee.config_flow.load_json_object", - return_value=MOCK_ECOBEE_CONF, - ), patch("homeassistant.components.ecobee.config_flow.Ecobee") as mock_ecobee: + with ( + patch( + "homeassistant.components.ecobee.config_flow.load_json_object", + return_value=MOCK_ECOBEE_CONF, + ), + patch("homeassistant.components.ecobee.config_flow.Ecobee") as mock_ecobee, + ): mock_ecobee = mock_ecobee.return_value mock_ecobee.refresh_tokens.return_value = True mock_ecobee.api_key = "test-api-key" @@ -172,10 +176,13 @@ async def test_import_flow_triggered_with_ecobee_conf_and_invalid_data( MOCK_ECOBEE_CONF = {} - with patch( - "homeassistant.components.ecobee.config_flow.load_json_object", - return_value=MOCK_ECOBEE_CONF, - ), patch.object(flow, "async_step_user") as mock_async_step_user: + with ( + patch( + "homeassistant.components.ecobee.config_flow.load_json_object", + return_value=MOCK_ECOBEE_CONF, + ), + patch.object(flow, "async_step_user") as mock_async_step_user, + ): await flow.async_step_import(import_data=None) mock_async_step_user.assert_called_once_with( @@ -193,12 +200,14 @@ async def test_import_flow_triggered_with_ecobee_conf_and_valid_data_and_stale_t MOCK_ECOBEE_CONF = {ECOBEE_API_KEY: None, ECOBEE_REFRESH_TOKEN: None} - with patch( - "homeassistant.components.ecobee.config_flow.load_json_object", - return_value=MOCK_ECOBEE_CONF, - ), patch( - "homeassistant.components.ecobee.config_flow.Ecobee" - ) as mock_ecobee, patch.object(flow, "async_step_user") as mock_async_step_user: + with ( + patch( + "homeassistant.components.ecobee.config_flow.load_json_object", + return_value=MOCK_ECOBEE_CONF, + ), + patch("homeassistant.components.ecobee.config_flow.Ecobee") as mock_ecobee, + patch.object(flow, "async_step_user") as mock_async_step_user, + ): mock_ecobee = mock_ecobee.return_value mock_ecobee.refresh_tokens.return_value = False diff --git a/tests/components/ecobee/test_humidifier.py b/tests/components/ecobee/test_humidifier.py index 734c94c8752..36b52c9c357 100644 --- a/tests/components/ecobee/test_humidifier.py +++ b/tests/components/ecobee/test_humidifier.py @@ -1,4 +1,5 @@ """The test for the ecobee thermostat humidifier module.""" + from unittest.mock import patch import pytest diff --git a/tests/components/ecobee/test_number.py b/tests/components/ecobee/test_number.py index 20974a3b55e..da5c8135a05 100644 --- a/tests/components/ecobee/test_number.py +++ b/tests/components/ecobee/test_number.py @@ -1,4 +1,5 @@ """The test for the ecobee thermostat number module.""" + from unittest.mock import patch from homeassistant.components.number import ATTR_VALUE, DOMAIN, SERVICE_SET_VALUE diff --git a/tests/components/ecobee/test_util.py b/tests/components/ecobee/test_util.py index 368721f71c0..88032362af0 100644 --- a/tests/components/ecobee/test_util.py +++ b/tests/components/ecobee/test_util.py @@ -1,4 +1,5 @@ """Tests for the ecobee.util module.""" + import pytest import voluptuous as vol diff --git a/tests/components/ecoforest/conftest.py b/tests/components/ecoforest/conftest.py index 09860546c15..79d1ea7f77b 100644 --- a/tests/components/ecoforest/conftest.py +++ b/tests/components/ecoforest/conftest.py @@ -1,4 +1,5 @@ """Common fixtures for the Ecoforest tests.""" + from collections.abc import Generator from unittest.mock import AsyncMock, Mock, patch diff --git a/tests/components/ecoforest/test_config_flow.py b/tests/components/ecoforest/test_config_flow.py index 302cbe76fa9..95c63a2515d 100644 --- a/tests/components/ecoforest/test_config_flow.py +++ b/tests/components/ecoforest/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Ecoforest config flow.""" + from unittest.mock import AsyncMock, patch from pyecoforest.exceptions import EcoforestAuthenticationRequired diff --git a/tests/components/econet/test_config_flow.py b/tests/components/econet/test_config_flow.py index 3444cc83834..7647b77e0a6 100644 --- a/tests/components/econet/test_config_flow.py +++ b/tests/components/econet/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for the Econet component.""" + from unittest.mock import patch from pyeconet.api import EcoNetApiInterface @@ -22,10 +23,13 @@ async def test_bad_credentials(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" - with patch( - "pyeconet.EcoNetApiInterface.login", - side_effect=InvalidCredentialsError(), - ), patch("homeassistant.components.econet.async_setup_entry", return_value=True): + with ( + patch( + "pyeconet.EcoNetApiInterface.login", + side_effect=InvalidCredentialsError(), + ), + patch("homeassistant.components.econet.async_setup_entry", return_value=True), + ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={ @@ -50,10 +54,13 @@ async def test_generic_error_from_library(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" - with patch( - "pyeconet.EcoNetApiInterface.login", - side_effect=PyeconetError(), - ), patch("homeassistant.components.econet.async_setup_entry", return_value=True): + with ( + patch( + "pyeconet.EcoNetApiInterface.login", + side_effect=PyeconetError(), + ), + patch("homeassistant.components.econet.async_setup_entry", return_value=True), + ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={ @@ -78,10 +85,13 @@ async def test_auth_worked(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" - with patch( - "pyeconet.EcoNetApiInterface.login", - return_value=EcoNetApiInterface, - ), patch("homeassistant.components.econet.async_setup_entry", return_value=True): + with ( + patch( + "pyeconet.EcoNetApiInterface.login", + return_value=EcoNetApiInterface, + ), + patch("homeassistant.components.econet.async_setup_entry", return_value=True), + ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={ @@ -113,10 +123,13 @@ async def test_already_configured(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" - with patch( - "pyeconet.EcoNetApiInterface.login", - return_value=EcoNetApiInterface, - ), patch("homeassistant.components.econet.async_setup_entry", return_value=True): + with ( + patch( + "pyeconet.EcoNetApiInterface.login", + return_value=EcoNetApiInterface, + ), + patch("homeassistant.components.econet.async_setup_entry", return_value=True), + ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={ diff --git a/tests/components/ecovacs/conftest.py b/tests/components/ecovacs/conftest.py index 31d7246e6bc..1a313957c3e 100644 --- a/tests/components/ecovacs/conftest.py +++ b/tests/components/ecovacs/conftest.py @@ -1,4 +1,5 @@ """Common fixtures for the Ecovacs tests.""" + from collections.abc import Generator from typing import Any from unittest.mock import AsyncMock, Mock, patch @@ -54,12 +55,15 @@ def device_fixture() -> str: @pytest.fixture def mock_authenticator(device_fixture: str) -> Generator[Mock, None, None]: """Mock the authenticator.""" - with patch( - "homeassistant.components.ecovacs.controller.Authenticator", - autospec=True, - ) as mock, patch( - "homeassistant.components.ecovacs.config_flow.Authenticator", - new=mock, + with ( + patch( + "homeassistant.components.ecovacs.controller.Authenticator", + autospec=True, + ) as mock, + patch( + "homeassistant.components.ecovacs.config_flow.Authenticator", + new=mock, + ), ): authenticator = mock.return_value authenticator.authenticate.return_value = Credentials("token", "user_id", 0) @@ -96,12 +100,15 @@ def mock_authenticator_authenticate(mock_authenticator: Mock) -> AsyncMock: @pytest.fixture def mock_mqtt_client(mock_authenticator: Mock) -> Mock: """Mock the MQTT client.""" - with patch( - "homeassistant.components.ecovacs.controller.MqttClient", - autospec=True, - ) as mock, patch( - "homeassistant.components.ecovacs.config_flow.MqttClient", - new=mock, + with ( + patch( + "homeassistant.components.ecovacs.controller.MqttClient", + autospec=True, + ) as mock, + patch( + "homeassistant.components.ecovacs.config_flow.MqttClient", + new=mock, + ), ): client = mock.return_value client._authenticator = mock_authenticator diff --git a/tests/components/ecovacs/const.py b/tests/components/ecovacs/const.py index 237c7fa5c85..89d2e2a8166 100644 --- a/tests/components/ecovacs/const.py +++ b/tests/components/ecovacs/const.py @@ -1,6 +1,5 @@ """Test ecovacs constants.""" - from homeassistant.components.ecovacs.const import ( CONF_CONTINENT, CONF_OVERRIDE_MQTT_URL, diff --git a/tests/components/ecovacs/snapshots/test_binary_sensor.ambr b/tests/components/ecovacs/snapshots/test_binary_sensor.ambr index 564323e91b2..62b356e379d 100644 --- a/tests/components/ecovacs/snapshots/test_binary_sensor.ambr +++ b/tests/components/ecovacs/snapshots/test_binary_sensor.ambr @@ -33,35 +33,15 @@ }) # --- # name: test_mop_attached[binary_sensor.ozmo_950_mop_attached-state] - EntityRegistryEntrySnapshot({ - 'aliases': set({ + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Ozmo 950 Mop attached', }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': , + 'context': , 'entity_id': 'binary_sensor.ozmo_950_mop_attached', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Mop attached', - 'platform': 'ecovacs', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'water_mop_attached', - 'unique_id': 'E1234567890000000001_water_mop_attached', - 'unit_of_measurement': None, + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', }) # --- diff --git a/tests/components/ecovacs/snapshots/test_button.ambr b/tests/components/ecovacs/snapshots/test_button.ambr index ce28500cb52..816551f7e6a 100644 --- a/tests/components/ecovacs/snapshots/test_button.ambr +++ b/tests/components/ecovacs/snapshots/test_button.ambr @@ -40,6 +40,7 @@ 'context': , 'entity_id': 'button.ozmo_950_relocate', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '2024-01-01T00:00:00+00:00', }) @@ -85,6 +86,7 @@ 'context': , 'entity_id': 'button.ozmo_950_reset_filter_lifespan', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '2024-01-01T00:00:00+00:00', }) @@ -130,6 +132,7 @@ 'context': , 'entity_id': 'button.ozmo_950_reset_main_brush_lifespan', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '2024-01-01T00:00:00+00:00', }) @@ -175,6 +178,7 @@ 'context': , 'entity_id': 'button.ozmo_950_reset_side_brushes_lifespan', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '2024-01-01T00:00:00+00:00', }) diff --git a/tests/components/ecovacs/snapshots/test_event.ambr b/tests/components/ecovacs/snapshots/test_event.ambr new file mode 100644 index 00000000000..8f433560cd1 --- /dev/null +++ b/tests/components/ecovacs/snapshots/test_event.ambr @@ -0,0 +1,59 @@ +# serializer version: 1 +# name: test_last_job[event.ozmo_950_last_job-entity_entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'event_types': list([ + 'finished', + 'finished_with_warnings', + 'manually_stopped', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'event', + 'entity_category': , + 'entity_id': 'event.ozmo_950_last_job', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Last job', + 'platform': 'ecovacs', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'last_job', + 'unique_id': 'E1234567890000000001_stats_report', + 'unit_of_measurement': None, + }) +# --- +# name: test_last_job[event.ozmo_950_last_job-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'event_type': 'finished', + 'event_types': list([ + 'finished', + 'finished_with_warnings', + 'manually_stopped', + ]), + 'friendly_name': 'Ozmo 950 Last job', + }), + 'context': , + 'entity_id': 'event.ozmo_950_last_job', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2024-03-20T00:00:00.000+00:00', + }) +# --- diff --git a/tests/components/ecovacs/snapshots/test_lawn_mower.ambr b/tests/components/ecovacs/snapshots/test_lawn_mower.ambr index 9446bb805ac..29c710a5cb7 100644 --- a/tests/components/ecovacs/snapshots/test_lawn_mower.ambr +++ b/tests/components/ecovacs/snapshots/test_lawn_mower.ambr @@ -27,7 +27,7 @@ 'platform': 'ecovacs', 'previous_unique_id': None, 'supported_features': , - 'translation_key': 'mower', + 'translation_key': None, 'unique_id': '8516fbb1-17f1-4194-0000000_mower', 'unit_of_measurement': None, }) @@ -60,7 +60,7 @@ 'platform': 'ecovacs', 'previous_unique_id': None, 'supported_features': , - 'translation_key': 'mower', + 'translation_key': None, 'unique_id': '8516fbb1-17f1-4194-0000000_mower', 'unit_of_measurement': None, }) diff --git a/tests/components/ecovacs/snapshots/test_number.ambr b/tests/components/ecovacs/snapshots/test_number.ambr index be4234b86ec..da8406491b4 100644 --- a/tests/components/ecovacs/snapshots/test_number.ambr +++ b/tests/components/ecovacs/snapshots/test_number.ambr @@ -49,6 +49,7 @@ 'context': , 'entity_id': 'number.ozmo_950_volume', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '5', }) diff --git a/tests/components/ecovacs/snapshots/test_select.ambr b/tests/components/ecovacs/snapshots/test_select.ambr index c9fb352da27..125e7f0cee8 100644 --- a/tests/components/ecovacs/snapshots/test_select.ambr +++ b/tests/components/ecovacs/snapshots/test_select.ambr @@ -53,6 +53,7 @@ 'context': , 'entity_id': 'select.ozmo_950_water_flow_level', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'ultrahigh', }) diff --git a/tests/components/ecovacs/snapshots/test_sensor.ambr b/tests/components/ecovacs/snapshots/test_sensor.ambr index 58961db3c25..b35310158f2 100644 --- a/tests/components/ecovacs/snapshots/test_sensor.ambr +++ b/tests/components/ecovacs/snapshots/test_sensor.ambr @@ -41,6 +41,7 @@ 'context': , 'entity_id': 'sensor.ozmo_950_area_cleaned', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '10', }) @@ -88,6 +89,7 @@ 'context': , 'entity_id': 'sensor.ozmo_950_battery', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '100', }) @@ -138,6 +140,7 @@ 'context': , 'entity_id': 'sensor.ozmo_950_cleaning_duration', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '5.0', }) @@ -184,6 +187,7 @@ 'context': , 'entity_id': 'sensor.ozmo_950_error', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0', }) @@ -230,6 +234,7 @@ 'context': , 'entity_id': 'sensor.ozmo_950_filter_lifespan', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '56', }) @@ -275,6 +280,7 @@ 'context': , 'entity_id': 'sensor.ozmo_950_ip_address', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '192.168.0.10', }) @@ -321,6 +327,7 @@ 'context': , 'entity_id': 'sensor.ozmo_950_main_brush_lifespan', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '80', }) @@ -367,6 +374,7 @@ 'context': , 'entity_id': 'sensor.ozmo_950_side_brushes_lifespan', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '40', }) @@ -416,6 +424,7 @@ 'context': , 'entity_id': 'sensor.ozmo_950_total_area_cleaned', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '60', }) @@ -469,6 +478,7 @@ 'context': , 'entity_id': 'sensor.ozmo_950_total_cleaning_duration', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '40.000', }) @@ -517,6 +527,7 @@ 'context': , 'entity_id': 'sensor.ozmo_950_total_cleanings', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '123', }) @@ -562,6 +573,7 @@ 'context': , 'entity_id': 'sensor.ozmo_950_wi_fi_rssi', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '-62', }) @@ -607,6 +619,7 @@ 'context': , 'entity_id': 'sensor.ozmo_950_wi_fi_ssid', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'Testnetwork', }) diff --git a/tests/components/ecovacs/snapshots/test_switch.ambr b/tests/components/ecovacs/snapshots/test_switch.ambr index ecb5bc0dbe5..59e891bea5e 100644 --- a/tests/components/ecovacs/snapshots/test_switch.ambr +++ b/tests/components/ecovacs/snapshots/test_switch.ambr @@ -40,6 +40,7 @@ 'context': , 'entity_id': 'switch.goat_g1_advanced_mode', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }) @@ -85,6 +86,7 @@ 'context': , 'entity_id': 'switch.goat_g1_border_switch', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }) @@ -130,6 +132,7 @@ 'context': , 'entity_id': 'switch.goat_g1_child_lock', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }) @@ -175,6 +178,7 @@ 'context': , 'entity_id': 'switch.goat_g1_cross_map_border_warning', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }) @@ -220,6 +224,7 @@ 'context': , 'entity_id': 'switch.goat_g1_move_up_warning', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }) @@ -265,6 +270,7 @@ 'context': , 'entity_id': 'switch.goat_g1_safe_protect', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }) @@ -310,6 +316,7 @@ 'context': , 'entity_id': 'switch.goat_g1_true_detect', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }) @@ -355,6 +362,7 @@ 'context': , 'entity_id': 'switch.ozmo_950_advanced_mode', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }) @@ -400,6 +408,7 @@ 'context': , 'entity_id': 'switch.ozmo_950_carpet_auto_boost_suction', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }) @@ -445,6 +454,7 @@ 'context': , 'entity_id': 'switch.ozmo_950_continuous_cleaning', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }) diff --git a/tests/components/ecovacs/test_binary_sensor.py b/tests/components/ecovacs/test_binary_sensor.py index 2ca0100be31..697e57c6def 100644 --- a/tests/components/ecovacs/test_binary_sensor.py +++ b/tests/components/ecovacs/test_binary_sensor.py @@ -49,7 +49,7 @@ async def test_mop_attached( ) assert (state := hass.states.get(state.entity_id)) - assert entity_entry == snapshot(name=f"{entity_id}-state") + assert state == snapshot(name=f"{entity_id}-state") await notify_and_wait( hass, event_bus, WaterInfoEvent(WaterAmount.HIGH, mop_attached=False) diff --git a/tests/components/ecovacs/test_config_flow.py b/tests/components/ecovacs/test_config_flow.py index 5e02ec7dede..6bd30c3a201 100644 --- a/tests/components/ecovacs/test_config_flow.py +++ b/tests/components/ecovacs/test_config_flow.py @@ -1,4 +1,5 @@ """Test Ecovacs config flow.""" + from collections.abc import Awaitable, Callable import ssl from typing import Any diff --git a/tests/components/ecovacs/test_event.py b/tests/components/ecovacs/test_event.py new file mode 100644 index 00000000000..0e7adaad954 --- /dev/null +++ b/tests/components/ecovacs/test_event.py @@ -0,0 +1,97 @@ +"""Tests for Ecovacs event entities.""" + +from datetime import timedelta + +from deebot_client.capabilities import Capabilities +from deebot_client.events import CleanJobStatus, ReportStatsEvent +from freezegun.api import FrozenDateTimeFactory +import pytest +from syrupy import SnapshotAssertion + +from homeassistant.components.ecovacs.const import DOMAIN +from homeassistant.components.ecovacs.controller import EcovacsController +from homeassistant.components.event.const import ATTR_EVENT_TYPE +from homeassistant.const import STATE_UNKNOWN, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er + +from .util import notify_and_wait + +pytestmark = [pytest.mark.usefixtures("init_integration")] + + +@pytest.fixture +def platforms() -> Platform | list[Platform]: + """Platforms, which should be loaded during the test.""" + return Platform.EVENT + + +async def test_last_job( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, + freezer: FrozenDateTimeFactory, + controller: EcovacsController, +) -> None: + """Test last job event entity.""" + freezer.move_to("2024-03-20T00:00:00+00:00") + entity_id = "event.ozmo_950_last_job" + assert (state := hass.states.get(entity_id)) + assert state.state == STATE_UNKNOWN + + assert (entity_entry := entity_registry.async_get(state.entity_id)) + assert entity_entry == snapshot(name=f"{entity_id}-entity_entry") + assert entity_entry.device_id + + device = next(controller.devices(Capabilities)) + + assert (device_entry := device_registry.async_get(entity_entry.device_id)) + assert device_entry.identifiers == {(DOMAIN, device.device_info["did"])} + + event_bus = device.events + await notify_and_wait( + hass, + event_bus, + ReportStatsEvent(10, 5, "spotArea", "1", CleanJobStatus.FINISHED, [1, 2]), + ) + + assert (state := hass.states.get(state.entity_id)) + assert state == snapshot(name=f"{entity_id}-state") + + freezer.tick(timedelta(minutes=5)) + await notify_and_wait( + hass, + event_bus, + ReportStatsEvent( + 100, 50, "spotArea", "2", CleanJobStatus.FINISHED_WITH_WARNINGS, [2, 3] + ), + ) + + assert (state := hass.states.get(state.entity_id)) + assert state.state == "2024-03-20T00:05:00.000+00:00" + assert state.attributes[ATTR_EVENT_TYPE] == "finished_with_warnings" + + freezer.tick(timedelta(minutes=5)) + await notify_and_wait( + hass, + event_bus, + ReportStatsEvent(0, 1, "spotArea", "3", CleanJobStatus.MANUAL_STOPPED, [1]), + ) + + assert (state := hass.states.get(state.entity_id)) + assert state.state == "2024-03-20T00:10:00.000+00:00" + assert state.attributes[ATTR_EVENT_TYPE] == "manually_stopped" + + freezer.tick(timedelta(minutes=5)) + for status in (CleanJobStatus.NO_STATUS, CleanJobStatus.CLEANING): + # we should not trigger on these statuses + await notify_and_wait( + hass, + event_bus, + ReportStatsEvent(12, 11, "spotArea", "4", status, [1, 2, 3]), + ) + + assert (state := hass.states.get(state.entity_id)) + assert state.state == "2024-03-20T00:10:00.000+00:00" + assert state.attributes[ATTR_EVENT_TYPE] == "manually_stopped" diff --git a/tests/components/ecovacs/test_init.py b/tests/components/ecovacs/test_init.py index 4cad3e74ae0..7780b86d714 100644 --- a/tests/components/ecovacs/test_init.py +++ b/tests/components/ecovacs/test_init.py @@ -1,4 +1,5 @@ """Test init of ecovacs.""" + from typing import Any from unittest.mock import AsyncMock, Mock, patch @@ -120,8 +121,8 @@ async def test_devices_in_dr( @pytest.mark.parametrize( ("device_fixture", "entities"), [ - ("yna5x1", 25), - ("5xu9h3", 19), + ("yna5x1", 26), + ("5xu9h3", 20), ], ) async def test_all_entities_loaded( diff --git a/tests/components/ecovacs/util.py b/tests/components/ecovacs/util.py index 73762128202..c587494f5e2 100644 --- a/tests/components/ecovacs/util.py +++ b/tests/components/ecovacs/util.py @@ -1,4 +1,5 @@ """Ecovacs test util.""" + import asyncio from deebot_client.event_bus import EventBus diff --git a/tests/components/ecowitt/test_config_flow.py b/tests/components/ecowitt/test_config_flow.py index c09fb951b11..24a45e2d31b 100644 --- a/tests/components/ecowitt/test_config_flow.py +++ b/tests/components/ecowitt/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Ecowitt Weather Station config flow.""" + from unittest.mock import patch from homeassistant import config_entries diff --git a/tests/components/efergy/__init__.py b/tests/components/efergy/__init__.py index 3780bcb5494..d763aaa2fb6 100644 --- a/tests/components/efergy/__init__.py +++ b/tests/components/efergy/__init__.py @@ -1,4 +1,5 @@ """Tests for Efergy integration.""" + from unittest.mock import AsyncMock, patch from pyefergy import exceptions diff --git a/tests/components/efergy/test_config_flow.py b/tests/components/efergy/test_config_flow.py index ccc826616ff..3a7529da395 100644 --- a/tests/components/efergy/test_config_flow.py +++ b/tests/components/efergy/test_config_flow.py @@ -1,4 +1,5 @@ """Test Efergy config flow.""" + from unittest.mock import patch from pyefergy import exceptions diff --git a/tests/components/efergy/test_init.py b/tests/components/efergy/test_init.py index df6d6a7b112..5c72e1a5cfd 100644 --- a/tests/components/efergy/test_init.py +++ b/tests/components/efergy/test_init.py @@ -1,4 +1,5 @@ """Test Efergy integration.""" + from pyefergy import exceptions from homeassistant.components.efergy.const import DEFAULT_NAME, DOMAIN diff --git a/tests/components/efergy/test_sensor.py b/tests/components/efergy/test_sensor.py index afeb5f6e382..d7ab3101900 100644 --- a/tests/components/efergy/test_sensor.py +++ b/tests/components/efergy/test_sensor.py @@ -1,4 +1,5 @@ """The tests for Efergy sensor platform.""" + from datetime import timedelta import pytest @@ -128,11 +129,11 @@ async def test_failed_update_and_reconnection( await mock_responses(hass, aioclient_mock, error=True) next_update = dt_util.utcnow() + timedelta(seconds=30) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get("sensor.efergy_power_usage").state == STATE_UNAVAILABLE aioclient_mock.clear_requests() await mock_responses(hass, aioclient_mock) next_update = dt_util.utcnow() + timedelta(seconds=30) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get("sensor.efergy_power_usage").state == "1580" diff --git a/tests/components/electrasmart/test_config_flow.py b/tests/components/electrasmart/test_config_flow.py index 929259a0ccf..957c140862f 100644 --- a/tests/components/electrasmart/test_config_flow.py +++ b/tests/components/electrasmart/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Electra Smart config flow.""" + from json import loads from unittest.mock import patch @@ -48,15 +49,19 @@ async def test_one_time_password(hass: HomeAssistant): mock_generate_token = loads(load_fixture("generate_token_response.json", DOMAIN)) mock_otp_response = loads(load_fixture("otp_response.json", DOMAIN)) - with patch( - "electrasmart.api.ElectraAPI.generate_new_token", - return_value=mock_generate_token, - ), patch( - "electrasmart.api.ElectraAPI.validate_one_time_password", - return_value=mock_otp_response, - ), patch( - "electrasmart.api.ElectraAPI.fetch_devices", - return_value=[], + with ( + patch( + "electrasmart.api.ElectraAPI.generate_new_token", + return_value=mock_generate_token, + ), + patch( + "electrasmart.api.ElectraAPI.validate_one_time_password", + return_value=mock_otp_response, + ), + patch( + "electrasmart.api.ElectraAPI.fetch_devices", + return_value=[], + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -74,12 +79,15 @@ async def test_one_time_password(hass: HomeAssistant): async def test_one_time_password_api_error(hass: HomeAssistant): """Test one time password.""" mock_generate_token = loads(load_fixture("generate_token_response.json", DOMAIN)) - with patch( - "electrasmart.api.ElectraAPI.generate_new_token", - return_value=mock_generate_token, - ), patch( - "electrasmart.api.ElectraAPI.validate_one_time_password", - side_effect=ElectraApiError, + with ( + patch( + "electrasmart.api.ElectraAPI.generate_new_token", + return_value=mock_generate_token, + ), + patch( + "electrasmart.api.ElectraAPI.validate_one_time_password", + side_effect=ElectraApiError, + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -143,12 +151,15 @@ async def test_invalid_auth(hass: HomeAssistant): ) mock_invalid_otp_response = loads(load_fixture("invalid_otp_response.json", DOMAIN)) - with patch( - "electrasmart.api.ElectraAPI.generate_new_token", - return_value=mock_generate_token_response, - ), patch( - "electrasmart.api.ElectraAPI.validate_one_time_password", - return_value=mock_invalid_otp_response, + with ( + patch( + "electrasmart.api.ElectraAPI.generate_new_token", + return_value=mock_generate_token_response, + ), + patch( + "electrasmart.api.ElectraAPI.validate_one_time_password", + return_value=mock_invalid_otp_response, + ), ): # test with required result = await hass.config_entries.flow.async_init( diff --git a/tests/components/electric_kiwi/conftest.py b/tests/components/electric_kiwi/conftest.py index 684fef24240..0a1d32f0ec0 100644 --- a/tests/components/electric_kiwi/conftest.py +++ b/tests/components/electric_kiwi/conftest.py @@ -1,4 +1,5 @@ """Define fixtures for electric kiwi tests.""" + from __future__ import annotations from collections.abc import Awaitable, Callable, Generator diff --git a/tests/components/electric_kiwi/test_config_flow.py b/tests/components/electric_kiwi/test_config_flow.py index 1199c3e555a..d91936eeebf 100644 --- a/tests/components/electric_kiwi/test_config_flow.py +++ b/tests/components/electric_kiwi/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Electric Kiwi config flow.""" + from __future__ import annotations from http import HTTPStatus diff --git a/tests/components/electric_kiwi/test_sensor.py b/tests/components/electric_kiwi/test_sensor.py index 4961f5fdcd4..f91e4d9c58c 100644 --- a/tests/components/electric_kiwi/test_sensor.py +++ b/tests/components/electric_kiwi/test_sensor.py @@ -1,6 +1,5 @@ """The tests for Electric Kiwi sensors.""" - from datetime import UTC, datetime from unittest.mock import AsyncMock, Mock diff --git a/tests/components/elgato/conftest.py b/tests/components/elgato/conftest.py index e8be6a4810b..5a783c509c2 100644 --- a/tests/components/elgato/conftest.py +++ b/tests/components/elgato/conftest.py @@ -1,4 +1,5 @@ """Fixtures for Elgato integration tests.""" + from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch @@ -64,10 +65,11 @@ def mock_elgato( device_fixtures: str, state_variant: str ) -> Generator[None, MagicMock, None]: """Return a mocked Elgato client.""" - with patch( - "homeassistant.components.elgato.coordinator.Elgato", autospec=True - ) as elgato_mock, patch( - "homeassistant.components.elgato.config_flow.Elgato", new=elgato_mock + with ( + patch( + "homeassistant.components.elgato.coordinator.Elgato", autospec=True + ) as elgato_mock, + patch("homeassistant.components.elgato.config_flow.Elgato", new=elgato_mock), ): elgato = elgato_mock.return_value elgato.info.return_value = Info.from_json( diff --git a/tests/components/elgato/snapshots/test_button.ambr b/tests/components/elgato/snapshots/test_button.ambr index 36fcfb11801..e7477540f46 100644 --- a/tests/components/elgato/snapshots/test_button.ambr +++ b/tests/components/elgato/snapshots/test_button.ambr @@ -8,6 +8,7 @@ 'context': , 'entity_id': 'button.frenck_identify', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }) @@ -88,6 +89,7 @@ 'context': , 'entity_id': 'button.frenck_restart', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }) diff --git a/tests/components/elgato/snapshots/test_light.ambr b/tests/components/elgato/snapshots/test_light.ambr index a65f5c46b94..6ef773a7304 100644 --- a/tests/components/elgato/snapshots/test_light.ambr +++ b/tests/components/elgato/snapshots/test_light.ambr @@ -32,6 +32,7 @@ 'context': , 'entity_id': 'light.frenck', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }) @@ -145,6 +146,7 @@ 'context': , 'entity_id': 'light.frenck', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }) @@ -259,6 +261,7 @@ 'context': , 'entity_id': 'light.frenck', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }) diff --git a/tests/components/elgato/snapshots/test_sensor.ambr b/tests/components/elgato/snapshots/test_sensor.ambr index 0c6e593a2bc..2b52d6b9f23 100644 --- a/tests/components/elgato/snapshots/test_sensor.ambr +++ b/tests/components/elgato/snapshots/test_sensor.ambr @@ -10,6 +10,7 @@ 'context': , 'entity_id': 'sensor.frenck_battery', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '78.57', }) @@ -97,6 +98,7 @@ 'context': , 'entity_id': 'sensor.frenck_battery_voltage', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '3.86', }) @@ -187,6 +189,7 @@ 'context': , 'entity_id': 'sensor.frenck_charging_current', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '3.008', }) @@ -277,6 +280,7 @@ 'context': , 'entity_id': 'sensor.frenck_charging_power', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '12.66', }) @@ -364,6 +368,7 @@ 'context': , 'entity_id': 'sensor.frenck_charging_voltage', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '4.208', }) diff --git a/tests/components/elgato/snapshots/test_switch.ambr b/tests/components/elgato/snapshots/test_switch.ambr index 962563599f6..41f3a8f3aaf 100644 --- a/tests/components/elgato/snapshots/test_switch.ambr +++ b/tests/components/elgato/snapshots/test_switch.ambr @@ -3,11 +3,11 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Frenck Energy saving', - 'icon': 'mdi:leaf', }), 'context': , 'entity_id': 'switch.frenck_energy_saving', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }) @@ -35,7 +35,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:leaf', + 'original_icon': None, 'original_name': 'Energy saving', 'platform': 'elgato', 'previous_unique_id': None, @@ -83,11 +83,11 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Frenck Studio mode', - 'icon': 'mdi:battery-off-outline', }), 'context': , 'entity_id': 'switch.frenck_studio_mode', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }) @@ -115,7 +115,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:battery-off-outline', + 'original_icon': None, 'original_name': 'Studio mode', 'platform': 'elgato', 'previous_unique_id': None, diff --git a/tests/components/elgato/test_button.py b/tests/components/elgato/test_button.py index bd6c9a1bfe5..ab2169b623e 100644 --- a/tests/components/elgato/test_button.py +++ b/tests/components/elgato/test_button.py @@ -1,4 +1,5 @@ """Tests for the Elgato Light button platform.""" + from unittest.mock import MagicMock from elgato import ElgatoError diff --git a/tests/components/elgato/test_config_flow.py b/tests/components/elgato/test_config_flow.py index bfae6fc9a17..def12307107 100644 --- a/tests/components/elgato/test_config_flow.py +++ b/tests/components/elgato/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for the Elgato Key Light config flow.""" + from ipaddress import ip_address from unittest.mock import AsyncMock, MagicMock diff --git a/tests/components/elgato/test_init.py b/tests/components/elgato/test_init.py index 1b566ef8ab2..a4ccb302461 100644 --- a/tests/components/elgato/test_init.py +++ b/tests/components/elgato/test_init.py @@ -1,4 +1,5 @@ """Tests for the Elgato Key Light integration.""" + from unittest.mock import MagicMock from elgato import ElgatoConnectionError diff --git a/tests/components/elgato/test_light.py b/tests/components/elgato/test_light.py index 8a3d3382a5f..40c0232c2b3 100644 --- a/tests/components/elgato/test_light.py +++ b/tests/components/elgato/test_light.py @@ -1,4 +1,5 @@ """Tests for the Elgato Key Light light platform.""" + from unittest.mock import MagicMock from elgato import ElgatoError diff --git a/tests/components/elgato/test_sensor.py b/tests/components/elgato/test_sensor.py index f53e2e43156..1ad37d938e7 100644 --- a/tests/components/elgato/test_sensor.py +++ b/tests/components/elgato/test_sensor.py @@ -1,4 +1,5 @@ """Tests for the Elgato sensor platform.""" + import pytest from syrupy.assertion import SnapshotAssertion diff --git a/tests/components/elgato/test_switch.py b/tests/components/elgato/test_switch.py index 336c2a9376b..fc6dbfb1828 100644 --- a/tests/components/elgato/test_switch.py +++ b/tests/components/elgato/test_switch.py @@ -1,4 +1,5 @@ """Tests for the Elgato switch platform.""" + from unittest.mock import MagicMock from elgato import ElgatoError diff --git a/tests/components/elkm1/__init__.py b/tests/components/elkm1/__init__.py index cb34f78360a..bec83002f0f 100644 --- a/tests/components/elkm1/__init__.py +++ b/tests/components/elkm1/__init__.py @@ -50,12 +50,15 @@ def _patch_elk(elk=None): @contextmanager def _patcher(): - with patch( - "homeassistant.components.elkm1.config_flow.Elk", - new=_elk, - ), patch( - "homeassistant.components.elkm1.config_flow.Elk", - new=_elk, + with ( + patch( + "homeassistant.components.elkm1.config_flow.Elk", + new=_elk, + ), + patch( + "homeassistant.components.elkm1.config_flow.Elk", + new=_elk, + ), ): yield diff --git a/tests/components/elkm1/test_config_flow.py b/tests/components/elkm1/test_config_flow.py index 0295781dc5f..592efc16b5e 100644 --- a/tests/components/elkm1/test_config_flow.py +++ b/tests/components/elkm1/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Elk-M1 Control config flow.""" + from dataclasses import asdict from unittest.mock import patch @@ -71,12 +72,17 @@ async def test_form_user_with_secure_elk_no_discovery(hass: HomeAssistant) -> No mocked_elk = mock_elk(invalid_auth=False, sync_complete=True) - with _patch_discovery(no_device=True), _patch_elk(elk=mocked_elk), patch( - "homeassistant.components.elkm1.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.elkm1.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + _patch_discovery(no_device=True), + _patch_elk(elk=mocked_elk), + patch( + "homeassistant.components.elkm1.async_setup", return_value=True + ) as mock_setup, + patch( + "homeassistant.components.elkm1.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -123,12 +129,17 @@ async def test_form_user_with_insecure_elk_skip_discovery(hass: HomeAssistant) - mocked_elk = mock_elk(invalid_auth=False, sync_complete=True) - with _patch_discovery(), _patch_elk(elk=mocked_elk), patch( - "homeassistant.components.elkm1.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.elkm1.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + _patch_discovery(), + _patch_elk(elk=mocked_elk), + patch( + "homeassistant.components.elkm1.async_setup", return_value=True + ) as mock_setup, + patch( + "homeassistant.components.elkm1.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -175,12 +186,17 @@ async def test_form_user_with_insecure_elk_no_discovery(hass: HomeAssistant) -> mocked_elk = mock_elk(invalid_auth=False, sync_complete=True) - with _patch_discovery(no_device=True), _patch_elk(elk=mocked_elk), patch( - "homeassistant.components.elkm1.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.elkm1.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + _patch_discovery(no_device=True), + _patch_elk(elk=mocked_elk), + patch( + "homeassistant.components.elkm1.async_setup", return_value=True + ) as mock_setup, + patch( + "homeassistant.components.elkm1.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -227,12 +243,15 @@ async def test_form_user_with_insecure_elk_times_out(hass: HomeAssistant) -> Non mocked_elk = mock_elk(invalid_auth=False, sync_complete=False) - with patch( - "homeassistant.components.elkm1.config_flow.VALIDATE_TIMEOUT", - 0, - ), patch( - "homeassistant.components.elkm1.config_flow.LOGIN_TIMEOUT", 0 - ), _patch_discovery(), _patch_elk(elk=mocked_elk): + with ( + patch( + "homeassistant.components.elkm1.config_flow.VALIDATE_TIMEOUT", + 0, + ), + patch("homeassistant.components.elkm1.config_flow.LOGIN_TIMEOUT", 0), + _patch_discovery(), + _patch_elk(elk=mocked_elk), + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -311,12 +330,17 @@ async def test_form_user_with_secure_elk_with_discovery(hass: HomeAssistant) -> ) await hass.async_block_till_done() - with _patch_discovery(), _patch_elk(elk=mocked_elk), patch( - "homeassistant.components.elkm1.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.elkm1.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + _patch_discovery(), + _patch_elk(elk=mocked_elk), + patch( + "homeassistant.components.elkm1.async_setup", return_value=True + ) as mock_setup, + patch( + "homeassistant.components.elkm1.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], { @@ -364,12 +388,17 @@ async def test_form_user_with_secure_elk_with_discovery_pick_manual( ) await hass.async_block_till_done() - with _patch_discovery(), _patch_elk(elk=mocked_elk), patch( - "homeassistant.components.elkm1.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.elkm1.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + _patch_discovery(), + _patch_elk(elk=mocked_elk), + patch( + "homeassistant.components.elkm1.async_setup", return_value=True + ) as mock_setup, + patch( + "homeassistant.components.elkm1.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], { @@ -420,12 +449,17 @@ async def test_form_user_with_secure_elk_with_discovery_pick_manual_direct_disco ) await hass.async_block_till_done() - with _patch_discovery(), _patch_elk(elk=mocked_elk), patch( - "homeassistant.components.elkm1.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.elkm1.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + _patch_discovery(), + _patch_elk(elk=mocked_elk), + patch( + "homeassistant.components.elkm1.async_setup", return_value=True + ) as mock_setup, + patch( + "homeassistant.components.elkm1.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], { @@ -467,12 +501,17 @@ async def test_form_user_with_tls_elk_no_discovery(hass: HomeAssistant) -> None: mocked_elk = mock_elk(invalid_auth=False, sync_complete=True) - with _patch_discovery(no_device=True), _patch_elk(elk=mocked_elk), patch( - "homeassistant.components.elkm1.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.elkm1.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + _patch_discovery(no_device=True), + _patch_elk(elk=mocked_elk), + patch( + "homeassistant.components.elkm1.async_setup", return_value=True + ) as mock_setup, + patch( + "homeassistant.components.elkm1.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -513,12 +552,17 @@ async def test_form_user_with_non_secure_elk_no_discovery(hass: HomeAssistant) - mocked_elk = mock_elk(invalid_auth=None, sync_complete=True) - with _patch_discovery(no_device=True), _patch_elk(elk=mocked_elk), patch( - "homeassistant.components.elkm1.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.elkm1.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + _patch_discovery(no_device=True), + _patch_elk(elk=mocked_elk), + patch( + "homeassistant.components.elkm1.async_setup", return_value=True + ) as mock_setup, + patch( + "homeassistant.components.elkm1.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -557,12 +601,17 @@ async def test_form_user_with_serial_elk_no_discovery(hass: HomeAssistant) -> No mocked_elk = mock_elk(invalid_auth=None, sync_complete=True) - with _patch_discovery(no_device=True), _patch_elk(elk=mocked_elk), patch( - "homeassistant.components.elkm1.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.elkm1.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + _patch_discovery(no_device=True), + _patch_elk(elk=mocked_elk), + patch( + "homeassistant.components.elkm1.async_setup", return_value=True + ) as mock_setup, + patch( + "homeassistant.components.elkm1.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -595,12 +644,17 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: mocked_elk = mock_elk(invalid_auth=None, sync_complete=None) - with _patch_discovery(no_device=True), _patch_elk(elk=mocked_elk), patch( - "homeassistant.components.elkm1.config_flow.VALIDATE_TIMEOUT", - 0, - ), patch( - "homeassistant.components.elkm1.config_flow.LOGIN_TIMEOUT", - 0, + with ( + _patch_discovery(no_device=True), + _patch_elk(elk=mocked_elk), + patch( + "homeassistant.components.elkm1.config_flow.VALIDATE_TIMEOUT", + 0, + ), + patch( + "homeassistant.components.elkm1.config_flow.LOGIN_TIMEOUT", + 0, + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -626,12 +680,17 @@ async def test_unknown_exception(hass: HomeAssistant) -> None: mocked_elk = mock_elk(invalid_auth=None, sync_complete=None, exception=OSError) - with _patch_discovery(no_device=True), _patch_elk(elk=mocked_elk), patch( - "homeassistant.components.elkm1.config_flow.VALIDATE_TIMEOUT", - 0, - ), patch( - "homeassistant.components.elkm1.config_flow.LOGIN_TIMEOUT", - 0, + with ( + _patch_discovery(no_device=True), + _patch_elk(elk=mocked_elk), + patch( + "homeassistant.components.elkm1.config_flow.VALIDATE_TIMEOUT", + 0, + ), + patch( + "homeassistant.components.elkm1.config_flow.LOGIN_TIMEOUT", + 0, + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -706,12 +765,17 @@ async def test_form_import(hass: HomeAssistant) -> None: """Test we get the form with import source.""" mocked_elk = mock_elk(invalid_auth=False, sync_complete=True) - with _patch_discovery(no_device=True), _patch_elk(elk=mocked_elk), patch( - "homeassistant.components.elkm1.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.elkm1.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + _patch_discovery(no_device=True), + _patch_elk(elk=mocked_elk), + patch( + "homeassistant.components.elkm1.async_setup", return_value=True + ) as mock_setup, + patch( + "homeassistant.components.elkm1.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}, @@ -771,12 +835,17 @@ async def test_form_import_device_discovered(hass: HomeAssistant) -> None: """Test we can import with discovery.""" mocked_elk = mock_elk(invalid_auth=False, sync_complete=True) - with _patch_discovery(), _patch_elk(elk=mocked_elk), patch( - "homeassistant.components.elkm1.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.elkm1.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + _patch_discovery(), + _patch_elk(elk=mocked_elk), + patch( + "homeassistant.components.elkm1.async_setup", return_value=True + ) as mock_setup, + patch( + "homeassistant.components.elkm1.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}, @@ -836,12 +905,17 @@ async def test_form_import_non_secure_device_discovered(hass: HomeAssistant) -> """Test we can import non-secure with discovery.""" mocked_elk = mock_elk(invalid_auth=False, sync_complete=True) - with _patch_discovery(), _patch_elk(elk=mocked_elk), patch( - "homeassistant.components.elkm1.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.elkm1.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + _patch_discovery(), + _patch_elk(elk=mocked_elk), + patch( + "homeassistant.components.elkm1.async_setup", return_value=True + ) as mock_setup, + patch( + "homeassistant.components.elkm1.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}, @@ -875,12 +949,17 @@ async def test_form_import_non_secure_non_stanadard_port_device_discovered( """Test we can import non-secure non standard port with discovery.""" mocked_elk = mock_elk(invalid_auth=False, sync_complete=True) - with _patch_discovery(), _patch_elk(elk=mocked_elk), patch( - "homeassistant.components.elkm1.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.elkm1.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + _patch_discovery(), + _patch_elk(elk=mocked_elk), + patch( + "homeassistant.components.elkm1.async_setup", return_value=True + ) as mock_setup, + patch( + "homeassistant.components.elkm1.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}, @@ -1090,12 +1169,17 @@ async def test_discovered_by_discovery(hass: HomeAssistant) -> None: mocked_elk = mock_elk(invalid_auth=False, sync_complete=True) - with _patch_discovery(), _patch_elk(elk=mocked_elk), patch( - "homeassistant.components.elkm1.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.elkm1.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + _patch_discovery(), + _patch_elk(elk=mocked_elk), + patch( + "homeassistant.components.elkm1.async_setup", return_value=True + ) as mock_setup, + patch( + "homeassistant.components.elkm1.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -1135,12 +1219,17 @@ async def test_discovered_by_discovery_non_standard_port(hass: HomeAssistant) -> mocked_elk = mock_elk(invalid_auth=False, sync_complete=True) - with _patch_discovery(), _patch_elk(elk=mocked_elk), patch( - "homeassistant.components.elkm1.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.elkm1.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + _patch_discovery(), + _patch_elk(elk=mocked_elk), + patch( + "homeassistant.components.elkm1.async_setup", return_value=True + ) as mock_setup, + patch( + "homeassistant.components.elkm1.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -1201,12 +1290,17 @@ async def test_discovered_by_dhcp_udp_responds(hass: HomeAssistant) -> None: mocked_elk = mock_elk(invalid_auth=False, sync_complete=True) - with _patch_discovery(), _patch_elk(elk=mocked_elk), patch( - "homeassistant.components.elkm1.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.elkm1.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + _patch_discovery(), + _patch_elk(elk=mocked_elk), + patch( + "homeassistant.components.elkm1.async_setup", return_value=True + ) as mock_setup, + patch( + "homeassistant.components.elkm1.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -1246,14 +1340,17 @@ async def test_discovered_by_dhcp_udp_responds_with_nonsecure_port( mocked_elk = mock_elk(invalid_auth=False, sync_complete=True) - with _patch_discovery(device=ELK_NON_SECURE_DISCOVERY), _patch_elk( - elk=mocked_elk - ), patch( - "homeassistant.components.elkm1.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.elkm1.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + _patch_discovery(device=ELK_NON_SECURE_DISCOVERY), + _patch_elk(elk=mocked_elk), + patch( + "homeassistant.components.elkm1.async_setup", return_value=True + ) as mock_setup, + patch( + "homeassistant.components.elkm1.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -1298,12 +1395,17 @@ async def test_discovered_by_dhcp_udp_responds_existing_config_entry( mocked_elk = mock_elk(invalid_auth=False, sync_complete=True) - with _patch_discovery(), _patch_elk(elk=mocked_elk), patch( - "homeassistant.components.elkm1.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.elkm1.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + _patch_discovery(), + _patch_elk(elk=mocked_elk), + patch( + "homeassistant.components.elkm1.async_setup", return_value=True + ) as mock_setup, + patch( + "homeassistant.components.elkm1.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"username": "test-username", "password": "test-password"}, @@ -1361,12 +1463,17 @@ async def test_multiple_instances_with_discovery(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - with _patch_discovery(device=elk_discovery_1), _patch_elk(elk=mocked_elk), patch( - "homeassistant.components.elkm1.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.elkm1.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + _patch_discovery(device=elk_discovery_1), + _patch_elk(elk=mocked_elk), + patch( + "homeassistant.components.elkm1.async_setup", return_value=True + ) as mock_setup, + patch( + "homeassistant.components.elkm1.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], { @@ -1408,10 +1515,14 @@ async def test_multiple_instances_with_discovery(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - with _patch_discovery(device=elk_discovery_2), _patch_elk(elk=mocked_elk), patch( - "homeassistant.components.elkm1.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + _patch_discovery(device=elk_discovery_2), + _patch_elk(elk=mocked_elk), + patch( + "homeassistant.components.elkm1.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], { @@ -1446,10 +1557,14 @@ async def test_multiple_instances_with_discovery(hass: HomeAssistant) -> None: mocked_elk = mock_elk(invalid_auth=None, sync_complete=True) - with _patch_discovery(no_device=True), _patch_elk(elk=mocked_elk), patch( - "homeassistant.components.elkm1.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + _patch_discovery(no_device=True), + _patch_elk(elk=mocked_elk), + patch( + "homeassistant.components.elkm1.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -1500,12 +1615,17 @@ async def test_multiple_instances_with_tls_v12(hass: HomeAssistant) -> None: assert result2["type"] == "form" assert not result["errors"] assert result2["step_id"] == "discovered_connection" - with _patch_discovery(device=elk_discovery_1), _patch_elk(elk=mocked_elk), patch( - "homeassistant.components.elkm1.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.elkm1.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + _patch_discovery(device=elk_discovery_1), + _patch_elk(elk=mocked_elk), + patch( + "homeassistant.components.elkm1.async_setup", return_value=True + ) as mock_setup, + patch( + "homeassistant.components.elkm1.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], { @@ -1548,10 +1668,14 @@ async def test_multiple_instances_with_tls_v12(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - with _patch_discovery(device=elk_discovery_2), _patch_elk(elk=mocked_elk), patch( - "homeassistant.components.elkm1.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + _patch_discovery(device=elk_discovery_2), + _patch_elk(elk=mocked_elk), + patch( + "homeassistant.components.elkm1.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], { @@ -1587,10 +1711,14 @@ async def test_multiple_instances_with_tls_v12(hass: HomeAssistant) -> None: mocked_elk = mock_elk(invalid_auth=False, sync_complete=True) - with _patch_discovery(no_device=True), _patch_elk(elk=mocked_elk), patch( - "homeassistant.components.elkm1.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + _patch_discovery(no_device=True), + _patch_elk(elk=mocked_elk), + patch( + "homeassistant.components.elkm1.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { diff --git a/tests/components/elkm1/test_logbook.py b/tests/components/elkm1/test_logbook.py index 90e33b3911e..35977ec98f0 100644 --- a/tests/components/elkm1/test_logbook.py +++ b/tests/components/elkm1/test_logbook.py @@ -1,4 +1,5 @@ """The tests for elkm1 logbook.""" + from homeassistant.components.elkm1.const import ( ATTR_KEY, ATTR_KEY_NAME, diff --git a/tests/components/elmax/__init__.py b/tests/components/elmax/__init__.py index cf1bce356c7..1434c831df3 100644 --- a/tests/components/elmax/__init__.py +++ b/tests/components/elmax/__init__.py @@ -1,5 +1,7 @@ """Tests for the Elmax component.""" +from tests.common import load_fixture + MOCK_USER_JWT = ( "JWT eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" ".eyJfaWQiOiIxYjExYmIxMWJiYjExMTExYjFiMTFiMWIiLCJlbWFpbCI6InRoaXMuaXNAdGVzdC5jb20iLCJyb2xlIjoid" @@ -12,4 +14,11 @@ MOCK_USER_ID = "1b11bb11bbb11111b1b11b1b" MOCK_PANEL_ID = "2db3dae30b9102de4d078706f94d0708" MOCK_PANEL_NAME = "Test Panel Name" MOCK_PANEL_PIN = "000000" +MOCK_WRONG_PANEL_PIN = "000000" MOCK_PASSWORD = "password" +MOCK_DIRECT_HOST = "1.1.1.1" +MOCK_DIRECT_HOST_CHANGED = "2.2.2.2" +MOCK_DIRECT_PORT = 443 +MOCK_DIRECT_SSL = True +MOCK_DIRECT_CERT = load_fixture("direct/cert.pem", "elmax") +MOCK_DIRECT_FOLLOW_MDNS = True diff --git a/tests/components/elmax/conftest.py b/tests/components/elmax/conftest.py index 70e3af76702..e69f52f4cad 100644 --- a/tests/components/elmax/conftest.py +++ b/tests/components/elmax/conftest.py @@ -1,5 +1,7 @@ """Configuration for Elmax tests.""" + import json +from unittest.mock import patch from elmax_api.constants import ( BASE_URL, @@ -11,25 +13,35 @@ from httpx import Response import pytest import respx -from . import MOCK_PANEL_ID, MOCK_PANEL_PIN +from . import ( + MOCK_DIRECT_HOST, + MOCK_DIRECT_PORT, + MOCK_DIRECT_SSL, + MOCK_PANEL_ID, + MOCK_PANEL_PIN, +) from tests.common import load_fixture +MOCK_DIRECT_BASE_URI = ( + f"{'https' if MOCK_DIRECT_SSL else 'http'}://{MOCK_DIRECT_HOST}:{MOCK_DIRECT_PORT}" +) + @pytest.fixture(autouse=True) -def httpx_mock_fixture(requests_mock): - """Configure httpx fixture.""" +def httpx_mock_cloud_fixture(requests_mock): + """Configure httpx fixture for cloud API communication.""" with respx.mock(base_url=BASE_URL, assert_all_called=False) as respx_mock: # Mock Login POST. login_route = respx_mock.post(f"/{ENDPOINT_LOGIN}", name="login") login_route.return_value = Response( - 200, json=json.loads(load_fixture("login.json", "elmax")) + 200, json=json.loads(load_fixture("cloud/login.json", "elmax")) ) # Mock Device list GET. list_devices_route = respx_mock.get(f"/{ENDPOINT_DEVICES}", name="list_devices") list_devices_route.return_value = Response( - 200, json=json.loads(load_fixture("list_devices.json", "elmax")) + 200, json=json.loads(load_fixture("cloud/list_devices.json", "elmax")) ) # Mock Panel GET. @@ -37,7 +49,40 @@ def httpx_mock_fixture(requests_mock): f"/{ENDPOINT_DISCOVERY}/{MOCK_PANEL_ID}/{MOCK_PANEL_PIN}", name="get_panel" ) get_panel_route.return_value = Response( - 200, json=json.loads(load_fixture("get_panel.json", "elmax")) + 200, json=json.loads(load_fixture("cloud/get_panel.json", "elmax")) ) yield respx_mock + + +@pytest.fixture(autouse=True) +def httpx_mock_direct_fixture(requests_mock): + """Configure httpx fixture for direct Panel-API communication.""" + with respx.mock( + base_url=MOCK_DIRECT_BASE_URI, assert_all_called=False + ) as respx_mock: + # Mock Login POST. + login_route = respx_mock.post(f"/api/v2/{ENDPOINT_LOGIN}", name="login") + login_route.return_value = Response( + 200, json=json.loads(load_fixture("direct/login.json", "elmax")) + ) + + # Mock Device list GET. + list_devices_route = respx_mock.get( + f"/api/v2/{ENDPOINT_DISCOVERY}", name="discovery_panel" + ) + list_devices_route.return_value = Response( + 200, json=json.loads(load_fixture("direct/discovery_panel.json", "elmax")) + ) + + yield respx_mock + + +@pytest.fixture(autouse=True) +def elmax_mock_direct_cert(requests_mock): + """Patch elmax library to return a specific PEM for SSL communication.""" + with patch( + "elmax_api.http.GenericElmax.retrieve_server_certificate", + return_value=load_fixture("direct/cert.pem", "elmax"), + ) as patched_ssl_get_cert: + yield patched_ssl_get_cert diff --git a/tests/components/elmax/fixtures/cloud/get_panel.json b/tests/components/elmax/fixtures/cloud/get_panel.json new file mode 100644 index 00000000000..b97ab3b6c30 --- /dev/null +++ b/tests/components/elmax/fixtures/cloud/get_panel.json @@ -0,0 +1,126 @@ +{ + "release": 11.7, + "tappFeature": true, + "sceneFeature": true, + "zone": [ + { + "endpointId": "2db3dae30b9102de4d078706f94d0708-zona-0", + "visibile": true, + "indice": 0, + "nome": "Feed zone 0", + "aperta": false, + "esclusa": false + }, + { + "endpointId": "2db3dae30b9102de4d078706f94d0708-zona-1", + "visibile": true, + "indice": 1, + "nome": "Feed Zone 1", + "aperta": false, + "esclusa": false + }, + { + "endpointId": "2db3dae30b9102de4d078706f94d0708-zona-2", + "visibile": true, + "indice": 2, + "nome": "Feed Zone 2", + "aperta": false, + "esclusa": false + } + ], + "uscite": [ + { + "endpointId": "2db3dae30b9102de4d078706f94d0708-uscita-0", + "visibile": true, + "indice": 0, + "nome": "Actuator 0", + "aperta": false + }, + { + "endpointId": "2db3dae30b9102de4d078706f94d0708-uscita-1", + "visibile": true, + "indice": 1, + "nome": "Actuator 1", + "aperta": false + }, + { + "endpointId": "2db3dae30b9102de4d078706f94d0708-uscita-2", + "visibile": true, + "indice": 2, + "nome": "Actuator 2", + "aperta": true + } + ], + "aree": [ + { + "endpointId": "2db3dae30b9102de4d078706f94d0708-area-0", + "visibile": true, + "indice": 0, + "nome": "AREA 0", + "statiDisponibili": [0, 1, 2, 3, 4], + "statiSessioneDisponibili": [0, 1, 2, 3], + "stato": 0, + "statoSessione": 0 + }, + { + "endpointId": "2db3dae30b9102de4d078706f94d0708-area-1", + "visibile": true, + "indice": 1, + "nome": "AREA 1", + "statiDisponibili": [0, 1, 2, 3, 4], + "statiSessioneDisponibili": [0, 1, 2, 3], + "stato": 0, + "statoSessione": 0 + }, + { + "endpointId": "2db3dae30b9102de4d078706f94d0708-area-2", + "visibile": false, + "indice": 2, + "nome": "AREA 2", + "statiDisponibili": [0, 1, 2, 3, 4], + "statiSessioneDisponibili": [0, 1, 2, 3], + "stato": 0, + "statoSessione": 0 + } + ], + "tapparelle": [ + { + "endpointId": "2db3dae30b9102de4d078706f94d0708-tapparella-0", + "visibile": true, + "indice": 0, + "stato": "stop", + "posizione": 100, + "nome": "Cover 0" + } + ], + "gruppi": [ + { + "endpointId": "2db3dae30b9102de4d078706f94d0708-gruppo-0", + "visibile": true, + "indice": 0, + "nome": "Group 0" + }, + { + "endpointId": "2db3dae30b9102de4d078706f94d0708-gruppo-1", + "visibile": false, + "indice": 1, + "nome": "Group 1" + } + ], + "scenari": [ + { + "endpointId": "2db3dae30b9102de4d078706f94d0708-scenario-0", + "visibile": true, + "indice": 0, + "nome": "Automation 0" + }, + { + "endpointId": "2db3dae30b9102de4d078706f94d0708-scenario-2", + "visibile": true, + "indice": 2, + "nome": "Automation 2" + } + ], + "utente": "this.is@test.com", + "centrale": "2db3dae30b9102de4d078706f94d0708" +} diff --git a/tests/components/elmax/fixtures/cloud/list_devices.json b/tests/components/elmax/fixtures/cloud/list_devices.json new file mode 100644 index 00000000000..9a3091f371d --- /dev/null +++ b/tests/components/elmax/fixtures/cloud/list_devices.json @@ -0,0 +1,12 @@ +[ + { + "centrale_online": true, + "hash": "2db3dae30b9102de4d078706f94d0708", + "username": [{ "name": "this.is@test.com", "label": "Test Panel Name" }] + }, + { + "centrale_online": true, + "hash": "d8e8fca2dc0f896fd7cb4cb0031ba249", + "username": [{ "name": "this.is@test.com", "label": "Test Panel Name" }] + } +] diff --git a/tests/components/elmax/fixtures/cloud/login.json b/tests/components/elmax/fixtures/cloud/login.json new file mode 100644 index 00000000000..87b1af3f295 --- /dev/null +++ b/tests/components/elmax/fixtures/cloud/login.json @@ -0,0 +1,8 @@ +{ + "token": "JWT eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiIxYjExYmIxMWJiYjExMTExYjFiMTFiMWIiLCJlbWFpbCI6InRoaXMuaXNAdGVzdC5jb20iLCJyb2xlIjoidXNlciIsImlhdCI6MTYzNjE5OTk5OCwiZXhwIjoxNjM2MjM1OTk4fQ.1C7lXuKyX1HEGOfMxNwxJ2n-CjoW4rwvNRITQxLICv0", + "user": { + "_id": "1b11bb11bbb11111b1b11b1b", + "email": "this.is@test.com", + "role": "user" + } +} diff --git a/tests/components/elmax/fixtures/direct/cert.pem b/tests/components/elmax/fixtures/direct/cert.pem new file mode 100644 index 00000000000..f91abbf791c --- /dev/null +++ b/tests/components/elmax/fixtures/direct/cert.pem @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDozCCAougAwIBAgIUQw+nCBfAnisI86E/KS24OpJl6oQwDQYJKoZIhvcNAQEL +BQAwYTELMAkGA1UEBhMCSVQxDzANBgNVBAgMBk1pbGFubzEPMA0GA1UEBwwGTWls +YW5vMRcwFQYDVQQKDA5BbGJlcnRvR2VuaW9sYTEXMBUGA1UEAwwOYWxiZXJ0b2dl +bmlvbGEwHhcNMjIxMTEyMTc1MzE0WhcNMjMxMTEyMTc1MzE0WjBhMQswCQYDVQQG +EwJJVDEPMA0GA1UECAwGTWlsYW5vMQ8wDQYDVQQHDAZNaWxhbm8xFzAVBgNVBAoM +DkFsYmVydG9HZW5pb2xhMRcwFQYDVQQDDA5hbGJlcnRvZ2VuaW9sYTCCASIwDQYJ +KoZIhvcNAQEBBQADggEPADCCAQoCggEBAKKlyvbpb3IqrKsmNe3WLscXWOm1zWuZ +OECukWl9NFep1v7H2VgH8Vc5/Lz8GyFIK6f4auq3E8Cv3AuDH3Z9q8sxN4E4vh6Q +zkFqyBa4yAXyaN6AT/ZgmZTbd4KUg+AHecGeDdedbDRc7s8bPDILcQM/S49RSnnS +IYivDnf3uByCPjvU2+/JRUvrB+rlL3tvUt/H+In8uRd01cBAx3GQLN/eqmkgUhiy +/eI3r7g5goxyCZpy6uyQEfN1CYWOpoIdL9rAEwwvrT+zK7iPqxCN3N0xQNvVpNzE +ifTONUPq+JPxO0SIP3Ro7rSeNSoe1O309qb7kpi5G/Zt7u3nRoiL1zUCAwEAAaNT +MFEwHQYDVR0OBBYEFIceFAyZ62kgZZqVJ4cLQCMbproRMB8GA1UdIwQYMBaAFIce +FAyZ62kgZZqVJ4cLQCMbproRMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL +BQADggEBACHt8GY2FAruQ4Xa1JIMFHzGsqW8OMg6M1Ntrclu7MmST7T35VyCzBfB +QUwasn8bp/XHTZSmJB8RndYRKslATHSMyWhqjGCBNoSI8ljJgn9YLopRnAwEKQ6P +sqRl6/uMhwC587nIG1GJGWx2SbxrqoTcy39QmK0vHZfCVctzREZLaWGI2rtiQkVZ +mt3A0aQck+KQPlCIac14Z6lZJLJ4wSq6x7gjQAFA9uQfbnTaerOGgDTnhwsIrON3 +TkSZ0dFgvt3lnbpO7fa8nPhdKFgzWZzymAjv3vsAxiboDE3LPn9BulxEkC+IFf5Q +6n+BK6Ogu16GZX4zUKeVdF089opux64= +-----END CERTIFICATE----- diff --git a/tests/components/elmax/fixtures/direct/discovery_panel.json b/tests/components/elmax/fixtures/direct/discovery_panel.json new file mode 100644 index 00000000000..423cb64052a --- /dev/null +++ b/tests/components/elmax/fixtures/direct/discovery_panel.json @@ -0,0 +1,148 @@ +{ + "release": "PHANTOM64PRO_GSM 11.9.844", + "tappFeature": true, + "sceneFeature": true, + "zone": [ + { + "endpointId": "13762559c53cd093171-zona-0", + "visibile": true, + "indice": 0, + "aperta": true, + "esclusa": false, + "nome": "ZONA 01" + }, + { + "endpointId": "13762559c53cd093171-zona-1", + "visibile": true, + "indice": 1, + "aperta": true, + "esclusa": false, + "nome": "ZONA 02e" + }, + { + "endpointId": "13762559c53cd093171-zona-2", + "visibile": true, + "indice": 2, + "aperta": true, + "esclusa": false, + "nome": "ZONA 03a" + }, + { + "endpointId": "13762559c53cd093171-zona-3", + "visibile": true, + "indice": 3, + "aperta": true, + "esclusa": false, + "nome": "ZONA 04" + }, + { + "endpointId": "13762559c53cd093171-zona-4", + "visibile": true, + "indice": 4, + "aperta": true, + "esclusa": false, + "nome": "ZONA 05" + }, + { + "endpointId": "13762559c53cd093171-zona-5", + "visibile": true, + "indice": 5, + "aperta": true, + "esclusa": false, + "nome": "ZONA 06" + }, + { + "endpointId": "13762559c53cd093171-zona-6", + "visibile": true, + "indice": 6, + "aperta": true, + "esclusa": false, + "nome": "ZONA 07" + }, + { + "endpointId": "13762559c53cd093171-zona-7", + "visibile": true, + "indice": 7, + "aperta": true, + "esclusa": false, + "nome": "ZONA 08" + } + ], + "uscite": [ + { + "endpointId": "13762559c53cd093171-uscita-1", + "visibile": true, + "indice": 1, + "aperta": true, + "nome": "USCITA 02" + } + ], + "aree": [ + { + "endpointId": "13762559c53cd093171-area-0", + "visibile": true, + "indice": 0, + "statiDisponibili": [0, 1, 2, 3, 4], + "statiSessioneDisponibili": [0, 1, 2, 3], + "stato": 0, + "statoSessione": 1, + "zoneBmask": "0700000000000000", + "nome": "AREA 1" + }, + { + "endpointId": "13762559c53cd093171-area-1", + "visibile": true, + "indice": 1, + "statiDisponibili": [0, 1, 2, 3, 4], + "statiSessioneDisponibili": [0, 1, 2, 3], + "stato": 0, + "statoSessione": 1, + "zoneBmask": "3800000000000000", + "nome": "AREA 2" + }, + { + "endpointId": "13762559c53cd093171-area-2", + "visibile": true, + "indice": 2, + "statiDisponibili": [0, 1, 2, 3, 4], + "statiSessioneDisponibili": [0, 1, 2, 3], + "stato": 0, + "statoSessione": 1, + "zoneBmask": "C000000000000000", + "nome": "AREA 3" + } + ], + "tapparelle": [ + { + "endpointId": "13762559c53cd093171-tapparella-0", + "visibile": true, + "indice": 0, + "stato": "stop", + "posizione": 0, + "nome": "ESPAN.DOM.01" + } + ], + "gruppi": [ + { + "endpointId": "13762559c53cd093171-gruppo-1", + "visibile": true, + "indice": 1, + "nome": "GRUPPOUSC02" + } + ], + "scenari": [ + { + "endpointId": "13762559c53cd093171-scenario-1", + "visibile": true, + "indice": 1, + "nome": "SCENARIO02" + }, + { + "endpointId": "13762559c53cd093171-scenario-2", + "visibile": true, + "indice": 2, + "nome": "SCENARIO03" + } + ], + "datetime": "19:16:44 23/10/2022" +} diff --git a/tests/components/elmax/fixtures/direct/login.json b/tests/components/elmax/fixtures/direct/login.json new file mode 100644 index 00000000000..5ca1e8cb1b8 --- /dev/null +++ b/tests/components/elmax/fixtures/direct/login.json @@ -0,0 +1,3 @@ +{ + "token": "JWT eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoidXNlciIsImNhcGFiaWxpdGllcyI6eyJ6b25lIjoiMTExMTExMTEwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMCIsInVzYyI6IjAxMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwIiwiaW5kaWNlIjowLCJhcmVlIjo3LCJjYW0iOjAsInRhcHAiOjEsImdydXBwaSI6Miwic2NlbmFyaSI6Nn0sImlhdCI6MTY2NjU0NDYzMywiZXhwIjoxNTY2NTQ4MjM0fQ.0N50aK8VrCBvVZuLf2AzLxH96PFES7gql69URKb50cA" +} diff --git a/tests/components/elmax/test_config_flow.py b/tests/components/elmax/test_config_flow.py index d2f8d9841d4..6782b3f9b7a 100644 --- a/tests/components/elmax/test_config_flow.py +++ b/tests/components/elmax/test_config_flow.py @@ -1,10 +1,19 @@ """Tests for the Elmax config flow.""" + from unittest.mock import patch from elmax_api.exceptions import ElmaxBadLoginError, ElmaxBadPinError, ElmaxNetworkError from homeassistant import config_entries, data_entry_flow +from homeassistant.components import zeroconf from homeassistant.components.elmax.const import ( + CONF_ELMAX_MODE, + CONF_ELMAX_MODE_CLOUD, + CONF_ELMAX_MODE_DIRECT, + CONF_ELMAX_MODE_DIRECT_HOST, + CONF_ELMAX_MODE_DIRECT_PORT, + CONF_ELMAX_MODE_DIRECT_SSL, + CONF_ELMAX_MODE_DIRECT_SSL_CERT, CONF_ELMAX_PANEL_ID, CONF_ELMAX_PANEL_NAME, CONF_ELMAX_PANEL_PIN, @@ -16,29 +25,122 @@ from homeassistant.config_entries import SOURCE_REAUTH from homeassistant.core import HomeAssistant from . import ( + MOCK_DIRECT_CERT, + MOCK_DIRECT_HOST, + MOCK_DIRECT_HOST_CHANGED, + MOCK_DIRECT_PORT, + MOCK_DIRECT_SSL, MOCK_PANEL_ID, MOCK_PANEL_NAME, MOCK_PANEL_PIN, MOCK_PASSWORD, MOCK_USERNAME, + MOCK_WRONG_PANEL_PIN, ) from tests.common import MockConfigEntry +MOCK_ZEROCONF_DISCOVERY_INFO = zeroconf.ZeroconfServiceInfo( + ip_address=MOCK_DIRECT_HOST, + ip_addresses=[MOCK_DIRECT_HOST], + hostname="VideoBox.local", + name="VideoBox", + port=443, + properties={ + "idl": MOCK_PANEL_ID, + "idr": MOCK_PANEL_ID, + "v1": "PHANTOM64PRO_GSM 11.9.844", + "v2": "4.9.13", + }, + type="_elmax-ssl._tcp", +) +MOCK_ZEROCONF_DISCOVERY_CHANGED_INFO = zeroconf.ZeroconfServiceInfo( + ip_address=MOCK_DIRECT_HOST_CHANGED, + ip_addresses=[MOCK_DIRECT_HOST_CHANGED], + hostname="VideoBox.local", + name="VideoBox", + port=443, + properties={ + "idl": MOCK_PANEL_ID, + "idr": MOCK_PANEL_ID, + "v1": "PHANTOM64PRO_GSM 11.9.844", + "v2": "4.9.13", + }, + type="_elmax-ssl._tcp", +) +MOCK_ZEROCONF_DISCOVERY_INFO_NOT_SUPPORTED = zeroconf.ZeroconfServiceInfo( + ip_address=MOCK_DIRECT_HOST, + ip_addresses=[MOCK_DIRECT_HOST], + hostname="VideoBox.local", + name="VideoBox", + port=443, + properties={ + "idl": MOCK_PANEL_ID, + "idr": MOCK_PANEL_ID, + "v1": "PHANTOM64PRO_GSM 11.9.844", + }, + type="_elmax-ssl._tcp", +) CONF_POLLING = "polling" -async def test_show_form(hass: HomeAssistant) -> None: +async def test_show_menu(hass: HomeAssistant) -> None: """Test that the form is served with no input.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "user" + assert result["type"] == data_entry_flow.FlowResultType.MENU + assert result["step_id"] == "choose_mode" -async def test_standard_setup(hass: HomeAssistant) -> None: - """Test the standard setup case.""" +async def test_direct_setup(hass: HomeAssistant) -> None: + """Test the standard direct setup case.""" + show_form_result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + with patch( + "homeassistant.components.elmax.async_setup_entry", + return_value=True, + ): + set_mode_result = await hass.config_entries.flow.async_configure( + show_form_result["flow_id"], + {"next_step_id": CONF_ELMAX_MODE_DIRECT}, + ) + result = await hass.config_entries.flow.async_configure( + set_mode_result["flow_id"], + { + CONF_ELMAX_MODE_DIRECT_HOST: MOCK_DIRECT_HOST, + CONF_ELMAX_MODE_DIRECT_PORT: MOCK_DIRECT_PORT, + CONF_ELMAX_MODE_DIRECT_SSL: MOCK_DIRECT_SSL, + CONF_ELMAX_PANEL_PIN: MOCK_PANEL_PIN, + }, + ) + await hass.async_block_till_done() + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + + +async def test_direct_show_form(hass: HomeAssistant) -> None: + """Test the standard direct show form case.""" + show_form_result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + with patch( + "homeassistant.components.elmax.async_setup_entry", + return_value=True, + ): + set_mode_result = await hass.config_entries.flow.async_configure( + show_form_result["flow_id"], + ) + result = await hass.config_entries.flow.async_configure( + set_mode_result["flow_id"], {"next_step_id": CONF_ELMAX_MODE_DIRECT} + ) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == CONF_ELMAX_MODE_DIRECT + assert result["errors"] is None + + +async def test_cloud_setup(hass: HomeAssistant) -> None: + """Test the standard cloud setup case.""" # Setup once. show_form_result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -47,6 +149,10 @@ async def test_standard_setup(hass: HomeAssistant) -> None: "homeassistant.components.elmax.async_setup_entry", return_value=True, ): + show_form_result = await hass.config_entries.flow.async_configure( + show_form_result["flow_id"], + {"next_step_id": CONF_ELMAX_MODE_CLOUD}, + ) login_result = await hass.config_entries.flow.async_configure( show_form_result["flow_id"], { @@ -65,7 +171,131 @@ async def test_standard_setup(hass: HomeAssistant) -> None: assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY -async def test_one_config_allowed(hass: HomeAssistant) -> None: +async def test_zeroconf_form_setup_api_not_supported(hass): + """Test the zeroconf setup case.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=MOCK_ZEROCONF_DISCOVERY_INFO_NOT_SUPPORTED, + ) + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "not_supported" + + +async def test_zeroconf_discovery(hass): + """Test discovery of Elmax local api panel.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=MOCK_ZEROCONF_DISCOVERY_INFO, + ) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "zeroconf_setup" + assert result["errors"] is None + + +async def test_zeroconf_setup_show_form(hass): + """Test discovery shows a form when activated.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=MOCK_ZEROCONF_DISCOVERY_INFO, + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + ) + + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "zeroconf_setup" + + +async def test_zeroconf_setup(hass): + """Test the successful creation of config entry via discovery flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=MOCK_ZEROCONF_DISCOVERY_INFO, + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_ELMAX_PANEL_PIN: MOCK_PANEL_PIN, + CONF_ELMAX_MODE_DIRECT_SSL: MOCK_DIRECT_SSL, + }, + ) + + await hass.async_block_till_done() + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + + +async def test_zeroconf_already_configured(hass): + """Ensure local discovery aborts when same panel is already added to ha.""" + MockConfigEntry( + domain=DOMAIN, + title=f"Elmax Direct ({MOCK_PANEL_ID})", + data={ + CONF_ELMAX_MODE: CONF_ELMAX_MODE_DIRECT, + CONF_ELMAX_MODE_DIRECT_HOST: MOCK_DIRECT_HOST, + CONF_ELMAX_MODE_DIRECT_PORT: MOCK_DIRECT_PORT, + CONF_ELMAX_MODE_DIRECT_SSL: MOCK_DIRECT_SSL, + CONF_ELMAX_PANEL_PIN: MOCK_PANEL_PIN, + CONF_ELMAX_PANEL_ID: MOCK_PANEL_ID, + CONF_ELMAX_MODE_DIRECT_SSL_CERT: MOCK_DIRECT_CERT, + }, + unique_id=MOCK_PANEL_ID, + ).add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=MOCK_ZEROCONF_DISCOVERY_INFO, + ) + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_zeroconf_panel_changed_ip(hass): + """Ensure local discovery updates the panel data when a the panel changes its IP.""" + # Simulate an entry already exists for ip MOCK_DIRECT_HOST. + config_entry = MockConfigEntry( + domain=DOMAIN, + title=f"Elmax Direct ({MOCK_PANEL_ID})", + data={ + CONF_ELMAX_MODE: CONF_ELMAX_MODE_DIRECT, + CONF_ELMAX_MODE_DIRECT_HOST: MOCK_DIRECT_HOST, + CONF_ELMAX_MODE_DIRECT_PORT: MOCK_DIRECT_PORT, + CONF_ELMAX_MODE_DIRECT_SSL: MOCK_DIRECT_SSL, + CONF_ELMAX_PANEL_PIN: MOCK_PANEL_PIN, + CONF_ELMAX_PANEL_ID: MOCK_PANEL_ID, + CONF_ELMAX_MODE_DIRECT_SSL_CERT: MOCK_DIRECT_CERT, + }, + unique_id=MOCK_PANEL_ID, + ) + config_entry.add_to_hass(hass) + + # Simulate a MDNS discovery finds the same panel with a different IP (MOCK_ZEROCONF_DISCOVERY_CHANGED_INFO). + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=MOCK_ZEROCONF_DISCOVERY_CHANGED_INFO, + ) + + # Expect we abort the configuration as "already configured" + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "already_configured" + + # Expect the panel ip has been updated. + assert ( + hass.config_entries.async_get_entry(config_entry.entry_id).data[ + CONF_ELMAX_MODE_DIRECT_HOST + ] + == MOCK_ZEROCONF_DISCOVERY_CHANGED_INFO.host + ) + + +async def test_one_config_allowed_cloud(hass: HomeAssistant) -> None: """Test that only one Elmax configuration is allowed for each panel.""" MockConfigEntry( domain=DOMAIN, @@ -82,8 +312,12 @@ async def test_one_config_allowed(hass: HomeAssistant) -> None: show_form_result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - login_result = await hass.config_entries.flow.async_configure( + user_result = await hass.config_entries.flow.async_configure( show_form_result["flow_id"], + {"next_step_id": CONF_ELMAX_MODE_CLOUD}, + ) + login_result = await hass.config_entries.flow.async_configure( + user_result["flow_id"], { CONF_ELMAX_USERNAME: MOCK_USERNAME, CONF_ELMAX_PASSWORD: MOCK_PASSWORD, @@ -100,7 +334,7 @@ async def test_one_config_allowed(hass: HomeAssistant) -> None: assert result["reason"] == "already_configured" -async def test_invalid_credentials(hass: HomeAssistant) -> None: +async def test_cloud_invalid_credentials(hass: HomeAssistant) -> None: """Test that invalid credentials throws an error.""" with patch( "elmax_api.http.Elmax.login", @@ -109,6 +343,10 @@ async def test_invalid_credentials(hass: HomeAssistant) -> None: show_form_result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) + show_form_result = await hass.config_entries.flow.async_configure( + show_form_result["flow_id"], + {"next_step_id": CONF_ELMAX_MODE_CLOUD}, + ) login_result = await hass.config_entries.flow.async_configure( show_form_result["flow_id"], { @@ -116,12 +354,12 @@ async def test_invalid_credentials(hass: HomeAssistant) -> None: CONF_ELMAX_PASSWORD: "incorrect_password", }, ) - assert login_result["step_id"] == "user" + assert login_result["step_id"] == CONF_ELMAX_MODE_CLOUD assert login_result["type"] == data_entry_flow.FlowResultType.FORM assert login_result["errors"] == {"base": "invalid_auth"} -async def test_connection_error(hass: HomeAssistant) -> None: +async def test_cloud_connection_error(hass: HomeAssistant) -> None: """Test other than invalid credentials throws an error.""" with patch( "elmax_api.http.Elmax.login", @@ -130,6 +368,10 @@ async def test_connection_error(hass: HomeAssistant) -> None: show_form_result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) + show_form_result = await hass.config_entries.flow.async_configure( + show_form_result["flow_id"], + {"next_step_id": CONF_ELMAX_MODE_CLOUD}, + ) login_result = await hass.config_entries.flow.async_configure( show_form_result["flow_id"], { @@ -137,11 +379,65 @@ async def test_connection_error(hass: HomeAssistant) -> None: CONF_ELMAX_PASSWORD: MOCK_PASSWORD, }, ) - assert login_result["step_id"] == "user" + assert login_result["step_id"] == CONF_ELMAX_MODE_CLOUD assert login_result["type"] == data_entry_flow.FlowResultType.FORM assert login_result["errors"] == {"base": "network_error"} +async def test_direct_connection_error(hass: HomeAssistant) -> None: + """Test network error while dealing with direct panel APIs.""" + with patch( + "elmax_api.http.ElmaxLocal.login", + side_effect=ElmaxNetworkError(), + ): + show_form_result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + set_mode_result = await hass.config_entries.flow.async_configure( + show_form_result["flow_id"], + {"next_step_id": CONF_ELMAX_MODE_DIRECT}, + ) + result = await hass.config_entries.flow.async_configure( + set_mode_result["flow_id"], + { + CONF_ELMAX_MODE_DIRECT_HOST: MOCK_DIRECT_HOST, + CONF_ELMAX_MODE_DIRECT_PORT: MOCK_DIRECT_PORT, + CONF_ELMAX_MODE_DIRECT_SSL: MOCK_DIRECT_SSL, + CONF_ELMAX_PANEL_PIN: MOCK_PANEL_PIN, + }, + ) + assert result["step_id"] == CONF_ELMAX_MODE_DIRECT + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["errors"] == {"base": "network_error"} + + +async def test_direct_wrong_panel_code(hass: HomeAssistant) -> None: + """Test wrong code being specified while dealing with direct panel APIs.""" + with patch( + "elmax_api.http.ElmaxLocal.login", + side_effect=ElmaxBadLoginError(), + ): + show_form_result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + set_mode_result = await hass.config_entries.flow.async_configure( + show_form_result["flow_id"], + {"next_step_id": CONF_ELMAX_MODE_DIRECT}, + ) + result = await hass.config_entries.flow.async_configure( + set_mode_result["flow_id"], + { + CONF_ELMAX_MODE_DIRECT_HOST: MOCK_DIRECT_HOST, + CONF_ELMAX_MODE_DIRECT_PORT: MOCK_DIRECT_PORT, + CONF_ELMAX_MODE_DIRECT_SSL: MOCK_DIRECT_SSL, + CONF_ELMAX_PANEL_PIN: MOCK_WRONG_PANEL_PIN, + }, + ) + assert result["step_id"] == CONF_ELMAX_MODE_DIRECT + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["errors"] == {"base": "invalid_auth"} + + async def test_unhandled_error(hass: HomeAssistant) -> None: """Test unhandled exceptions.""" with patch( @@ -151,6 +447,10 @@ async def test_unhandled_error(hass: HomeAssistant) -> None: show_form_result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) + show_form_result = await hass.config_entries.flow.async_configure( + show_form_result["flow_id"], + {"next_step_id": CONF_ELMAX_MODE_CLOUD}, + ) login_result = await hass.config_entries.flow.async_configure( show_form_result["flow_id"], { @@ -180,6 +480,10 @@ async def test_invalid_pin(hass: HomeAssistant) -> None: show_form_result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) + show_form_result = await hass.config_entries.flow.async_configure( + show_form_result["flow_id"], + {"next_step_id": CONF_ELMAX_MODE_CLOUD}, + ) login_result = await hass.config_entries.flow.async_configure( show_form_result["flow_id"], { @@ -209,6 +513,10 @@ async def test_no_online_panel(hass: HomeAssistant) -> None: show_form_result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) + show_form_result = await hass.config_entries.flow.async_configure( + show_form_result["flow_id"], + {"next_step_id": CONF_ELMAX_MODE_CLOUD}, + ) login_result = await hass.config_entries.flow.async_configure( show_form_result["flow_id"], { @@ -216,7 +524,7 @@ async def test_no_online_panel(hass: HomeAssistant) -> None: CONF_ELMAX_PASSWORD: MOCK_PASSWORD, }, ) - assert login_result["step_id"] == "user" + assert login_result["step_id"] == CONF_ELMAX_MODE_CLOUD assert login_result["type"] == data_entry_flow.FlowResultType.FORM assert login_result["errors"] == {"base": "no_panel_online"} diff --git a/tests/components/elvia/conftest.py b/tests/components/elvia/conftest.py index a2a10e67893..c8b98f18f3f 100644 --- a/tests/components/elvia/conftest.py +++ b/tests/components/elvia/conftest.py @@ -1,4 +1,5 @@ """Common fixtures for the Elvia tests.""" + from collections.abc import Generator from unittest.mock import AsyncMock, patch diff --git a/tests/components/elvia/test_config_flow.py b/tests/components/elvia/test_config_flow.py index 630aca4f16c..2fda29217c4 100644 --- a/tests/components/elvia/test_config_flow.py +++ b/tests/components/elvia/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Elvia config flow.""" + from unittest.mock import AsyncMock, patch from elvia import error as ElviaError @@ -197,12 +198,12 @@ async def test_abort_when_metering_point_id_exist( @pytest.mark.parametrize( ("side_effect", "base_error"), - ( + [ (ElviaError.ElviaException("Boom"), "unknown"), (ElviaError.AuthError("Boom", 403, {}, ""), "invalid_auth"), (ElviaError.ElviaServerException("Boom", 500, {}, ""), "unknown"), (ElviaError.ElviaClientException("Boom"), "unknown"), - ), + ], ) async def test_form_exceptions( recorder_mock: Recorder, diff --git a/tests/components/emonitor/test_config_flow.py b/tests/components/emonitor/test_config_flow.py index af4c1ac2a8a..07809a83d89 100644 --- a/tests/components/emonitor/test_config_flow.py +++ b/tests/components/emonitor/test_config_flow.py @@ -1,4 +1,5 @@ """Test the SiteSage Emonitor config flow.""" + from unittest.mock import MagicMock, patch from aioemonitor.monitor import EmonitorNetwork, EmonitorStatus @@ -34,13 +35,16 @@ async def test_form(hass: HomeAssistant) -> None: assert result["type"] == "form" assert result["errors"] == {} - with patch( - "homeassistant.components.emonitor.config_flow.Emonitor.async_get_status", - return_value=_mock_emonitor(), - ), patch( - "homeassistant.components.emonitor.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.emonitor.config_flow.Emonitor.async_get_status", + return_value=_mock_emonitor(), + ), + patch( + "homeassistant.components.emonitor.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -197,12 +201,15 @@ async def test_user_unique_id_already_exists(hass: HomeAssistant) -> None: assert result["type"] == "form" assert result["errors"] == {} - with patch( - "homeassistant.components.emonitor.config_flow.Emonitor.async_get_status", - return_value=_mock_emonitor(), - ), patch( - "homeassistant.components.emonitor.async_setup_entry", - return_value=True, + with ( + patch( + "homeassistant.components.emonitor.config_flow.Emonitor.async_get_status", + return_value=_mock_emonitor(), + ), + patch( + "homeassistant.components.emonitor.async_setup_entry", + return_value=True, + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], diff --git a/tests/components/emulated_hue/test_hue_api.py b/tests/components/emulated_hue/test_hue_api.py index 167562578f2..08974b36215 100644 --- a/tests/components/emulated_hue/test_hue_api.py +++ b/tests/components/emulated_hue/test_hue_api.py @@ -1,4 +1,5 @@ """The tests for the emulated Hue component.""" + import asyncio from datetime import timedelta from http import HTTPStatus diff --git a/tests/components/emulated_hue/test_init.py b/tests/components/emulated_hue/test_init.py index 9a872d66946..6bc99db6e60 100644 --- a/tests/components/emulated_hue/test_init.py +++ b/tests/components/emulated_hue/test_init.py @@ -1,4 +1,5 @@ """Test the Emulated Hue component.""" + from datetime import timedelta from typing import Any from unittest.mock import AsyncMock, Mock, patch @@ -12,7 +13,7 @@ from homeassistant.components.emulated_hue.config import ( Config, ) from homeassistant.components.emulated_hue.upnp import UPNPResponderProtocol -from homeassistant.const import EVENT_HOMEASSISTANT_START +from homeassistant.const import EVENT_HOMEASSISTANT_STARTED from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component from homeassistant.util import utcnow @@ -132,20 +133,22 @@ def test_config_alexa_entity_id_to_number() -> None: async def test_setup_works(hass: HomeAssistant) -> None: """Test setup works.""" hass.config.components.add("network") - with patch( - "homeassistant.components.emulated_hue.async_create_upnp_datagram_endpoint", - AsyncMock(), - ) as mock_create_upnp_datagram_endpoint, patch( - "homeassistant.components.emulated_hue.async_get_source_ip" - ), patch( - "homeassistant.components.emulated_hue.web.TCPSite", - return_value=Mock(spec_set=web.TCPSite), + with ( + patch( + "homeassistant.components.emulated_hue.async_create_upnp_datagram_endpoint", + AsyncMock(), + ) as mock_create_upnp_datagram_endpoint, + patch("homeassistant.components.emulated_hue.async_get_source_ip"), + patch( + "homeassistant.components.emulated_hue.web.TCPSite", + return_value=Mock(spec_set=web.TCPSite), + ), ): mock_create_upnp_datagram_endpoint.return_value = AsyncMock( spec=UPNPResponderProtocol ) assert await async_setup_component(hass, "emulated_hue", {}) - hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() assert len(mock_create_upnp_datagram_endpoint.mock_calls) == 1 diff --git a/tests/components/emulated_hue/test_upnp.py b/tests/components/emulated_hue/test_upnp.py index 138c7c49f93..f69bd1b0651 100644 --- a/tests/components/emulated_hue/test_upnp.py +++ b/tests/components/emulated_hue/test_upnp.py @@ -1,4 +1,5 @@ """The tests for the emulated Hue component.""" + from http import HTTPStatus import json import unittest diff --git a/tests/components/emulated_kasa/test_init.py b/tests/components/emulated_kasa/test_init.py index 5d294ec3bba..36eaa749f11 100644 --- a/tests/components/emulated_kasa/test_init.py +++ b/tests/components/emulated_kasa/test_init.py @@ -1,4 +1,5 @@ """Tests for emulated_kasa library bindings.""" + import math from unittest.mock import AsyncMock, Mock, patch diff --git a/tests/components/emulated_roku/test_binding.py b/tests/components/emulated_roku/test_binding.py index 1b598483f01..5bde72d2e4d 100644 --- a/tests/components/emulated_roku/test_binding.py +++ b/tests/components/emulated_roku/test_binding.py @@ -1,4 +1,5 @@ """Tests for emulated_roku library bindings.""" + from unittest.mock import AsyncMock, Mock, patch from homeassistant.components.emulated_roku.binding import ( diff --git a/tests/components/emulated_roku/test_config_flow.py b/tests/components/emulated_roku/test_config_flow.py index 3d620ef6954..700adbf0039 100644 --- a/tests/components/emulated_roku/test_config_flow.py +++ b/tests/components/emulated_roku/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for emulated_roku config flow.""" + from homeassistant import config_entries from homeassistant.components.emulated_roku import config_flow from homeassistant.core import HomeAssistant diff --git a/tests/components/emulated_roku/test_init.py b/tests/components/emulated_roku/test_init.py index 117f5954b61..00316c66425 100644 --- a/tests/components/emulated_roku/test_init.py +++ b/tests/components/emulated_roku/test_init.py @@ -1,4 +1,5 @@ """Test emulated_roku component setup process.""" + from unittest.mock import AsyncMock, Mock, patch from homeassistant.components import emulated_roku @@ -8,9 +9,12 @@ from homeassistant.setup import async_setup_component async def test_config_required_fields(hass: HomeAssistant, mock_get_source_ip) -> None: """Test that configuration is successful with required fields.""" - with patch.object(emulated_roku, "configured_servers", return_value=[]), patch( - "homeassistant.components.emulated_roku.binding.EmulatedRokuServer", - return_value=Mock(start=AsyncMock(), close=AsyncMock()), + with ( + patch.object(emulated_roku, "configured_servers", return_value=[]), + patch( + "homeassistant.components.emulated_roku.binding.EmulatedRokuServer", + return_value=Mock(start=AsyncMock(), close=AsyncMock()), + ), ): assert ( await async_setup_component( @@ -35,11 +39,14 @@ async def test_config_already_registered_not_configured( hass: HomeAssistant, mock_get_source_ip ) -> None: """Test that an already registered name causes the entry to be ignored.""" - with patch( - "homeassistant.components.emulated_roku.binding.EmulatedRokuServer", - return_value=Mock(start=AsyncMock(), close=AsyncMock()), - ) as instantiate, patch.object( - emulated_roku, "configured_servers", return_value=["Emulated Roku Test"] + with ( + patch( + "homeassistant.components.emulated_roku.binding.EmulatedRokuServer", + return_value=Mock(start=AsyncMock(), close=AsyncMock()), + ) as instantiate, + patch.object( + emulated_roku, "configured_servers", return_value=["Emulated Roku Test"] + ), ): assert ( await async_setup_component( diff --git a/tests/components/energy/test_sensor.py b/tests/components/energy/test_sensor.py index 522bbe5af06..192cf6abea4 100644 --- a/tests/components/energy/test_sensor.py +++ b/tests/components/energy/test_sensor.py @@ -1,4 +1,5 @@ """Test the Energy sensors.""" + import copy from datetime import timedelta from typing import Any @@ -424,7 +425,7 @@ async def test_cost_sensor_price_entity_total( hass.states.async_set( usage_sensor_entity_id, initial_energy, - {**energy_attributes, **{"last_reset": last_reset}}, + {**energy_attributes, "last_reset": last_reset}, ) hass.states.async_set("sensor.energy_price", "1") @@ -443,7 +444,7 @@ async def test_cost_sensor_price_entity_total( hass.states.async_set( usage_sensor_entity_id, "0", - {**energy_attributes, **{"last_reset": last_reset}}, + {**energy_attributes, "last_reset": last_reset}, ) await hass.async_block_till_done() @@ -465,7 +466,7 @@ async def test_cost_sensor_price_entity_total( hass.states.async_set( usage_sensor_entity_id, "10", - {**energy_attributes, **{"last_reset": last_reset}}, + {**energy_attributes, "last_reset": last_reset}, ) await hass.async_block_till_done() state = hass.states.get(cost_sensor_entity_id) @@ -492,7 +493,7 @@ async def test_cost_sensor_price_entity_total( hass.states.async_set( usage_sensor_entity_id, "14.5", - {**energy_attributes, **{"last_reset": last_reset}}, + {**energy_attributes, "last_reset": last_reset}, ) await hass.async_block_till_done() state = hass.states.get(cost_sensor_entity_id) @@ -510,7 +511,7 @@ async def test_cost_sensor_price_entity_total( hass.states.async_set( usage_sensor_entity_id, "14", - {**energy_attributes, **{"last_reset": last_reset}}, + {**energy_attributes, "last_reset": last_reset}, ) await hass.async_block_till_done() state = hass.states.get(cost_sensor_entity_id) @@ -523,7 +524,7 @@ async def test_cost_sensor_price_entity_total( hass.states.async_set( usage_sensor_entity_id, "4", - {**energy_attributes, **{"last_reset": last_reset}}, + {**energy_attributes, "last_reset": last_reset}, ) await hass.async_block_till_done() state = hass.states.get(cost_sensor_entity_id) @@ -536,7 +537,7 @@ async def test_cost_sensor_price_entity_total( hass.states.async_set( usage_sensor_entity_id, "10", - {**energy_attributes, **{"last_reset": last_reset}}, + {**energy_attributes, "last_reset": last_reset}, ) await hass.async_block_till_done() state = hass.states.get(cost_sensor_entity_id) @@ -987,7 +988,7 @@ async def test_cost_sensor_handle_late_price_sensor( @pytest.mark.parametrize( "unit", - (UnitOfVolume.CUBIC_FEET, UnitOfVolume.CUBIC_METERS), + [UnitOfVolume.CUBIC_FEET, UnitOfVolume.CUBIC_METERS], ) async def test_cost_sensor_handle_gas( setup_integration, hass: HomeAssistant, hass_storage: dict[str, Any], unit @@ -1085,12 +1086,12 @@ async def test_cost_sensor_handle_gas_kwh( @pytest.mark.parametrize( ("unit_system", "usage_unit", "growth"), - ( + [ # 1 cubic foot = 7.47 gl, 100 ft3 growth @ 0.5/ft3: (US_CUSTOMARY_SYSTEM, UnitOfVolume.CUBIC_FEET, 374.025974025974), (US_CUSTOMARY_SYSTEM, UnitOfVolume.GALLONS, 50.0), (METRIC_SYSTEM, UnitOfVolume.CUBIC_METERS, 50.0), - ), + ], ) async def test_cost_sensor_handle_water( setup_integration, diff --git a/tests/components/energy/test_validate.py b/tests/components/energy/test_validate.py index 3c4a28df11d..7a328e77d76 100644 --- a/tests/components/energy/test_validate.py +++ b/tests/components/energy/test_validate.py @@ -1,4 +1,5 @@ """Test that validation works.""" + from unittest.mock import patch import pytest @@ -689,7 +690,7 @@ async def test_validation_grid_auto_cost_entity_errors( @pytest.mark.parametrize( ("state", "unit", "expected"), - ( + [ ( "123,123.12", "$/kWh", @@ -710,7 +711,7 @@ async def test_validation_grid_auto_cost_entity_errors( }, }, ), - ), + ], ) async def test_validation_grid_price_errors( hass: HomeAssistant, mock_energy_manager, mock_get_metadata, state, unit, expected diff --git a/tests/components/energy/test_websocket_api.py b/tests/components/energy/test_websocket_api.py index f953d0e3a03..afb23e4e88a 100644 --- a/tests/components/energy/test_websocket_api.py +++ b/tests/components/energy/test_websocket_api.py @@ -1,4 +1,5 @@ """Test the Energy websocket API.""" + from typing import Any from unittest.mock import AsyncMock, Mock @@ -89,6 +90,7 @@ async def test_save_preferences( mock_energy_platform, ) -> None: """Test we can save preferences.""" + await hass.async_block_till_done() client = await hass_ws_client(hass) # Test saving default prefs is also valid. @@ -283,6 +285,7 @@ async def test_get_solar_forecast( entry.add_to_hass(hass) manager = await data.async_get_manager(hass) + manager.data = data.EnergyManager.default_preferences() manager.data["energy_sources"].append( { @@ -292,6 +295,7 @@ async def test_get_solar_forecast( } ) client = await hass_ws_client(hass) + await hass.async_block_till_done() await client.send_json({"id": 5, "type": "energy/solar_forecast"}) diff --git a/tests/components/energyzero/conftest.py b/tests/components/energyzero/conftest.py index 42b05eff444..2198e8c0c79 100644 --- a/tests/components/energyzero/conftest.py +++ b/tests/components/energyzero/conftest.py @@ -1,4 +1,5 @@ """Fixtures for EnergyZero integration tests.""" + from collections.abc import Generator import json from unittest.mock import AsyncMock, MagicMock, patch diff --git a/tests/components/energyzero/snapshots/test_sensor.ambr b/tests/components/energyzero/snapshots/test_sensor.ambr index 84a027b89f1..5ffa623fd87 100644 --- a/tests/components/energyzero/snapshots/test_sensor.ambr +++ b/tests/components/energyzero/snapshots/test_sensor.ambr @@ -466,6 +466,7 @@ 'context': , 'entity_id': 'sensor.energyzero_today_energy_average_price', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0.37', }) @@ -537,6 +538,7 @@ 'context': , 'entity_id': 'sensor.energyzero_today_energy_current_hour_price', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0.49', }) @@ -609,6 +611,7 @@ 'context': , 'entity_id': 'sensor.energyzero_today_energy_highest_price_time', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '2022-12-07T16:00:00+00:00', }) @@ -679,6 +682,7 @@ 'context': , 'entity_id': 'sensor.energyzero_today_energy_hours_priced_equal_or_lower', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '23', }) @@ -749,6 +753,7 @@ 'context': , 'entity_id': 'sensor.energyzero_today_energy_max_price', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0.55', }) @@ -820,6 +825,7 @@ 'context': , 'entity_id': 'sensor.energyzero_today_gas_current_hour_price', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '1.47', }) diff --git a/tests/components/energyzero/test_config_flow.py b/tests/components/energyzero/test_config_flow.py index 5f7b4925036..d16ea5cc8a8 100644 --- a/tests/components/energyzero/test_config_flow.py +++ b/tests/components/energyzero/test_config_flow.py @@ -1,4 +1,5 @@ """Test the EnergyZero config flow.""" + from unittest.mock import MagicMock from syrupy.assertion import SnapshotAssertion diff --git a/tests/components/energyzero/test_diagnostics.py b/tests/components/energyzero/test_diagnostics.py index db0821cc951..f4408ded05d 100644 --- a/tests/components/energyzero/test_diagnostics.py +++ b/tests/components/energyzero/test_diagnostics.py @@ -1,4 +1,5 @@ """Tests for the diagnostics data provided by the EnergyZero integration.""" + from unittest.mock import MagicMock from energyzero import EnergyZeroNoDataError diff --git a/tests/components/energyzero/test_init.py b/tests/components/energyzero/test_init.py index b7072108b35..287157026f4 100644 --- a/tests/components/energyzero/test_init.py +++ b/tests/components/energyzero/test_init.py @@ -1,4 +1,5 @@ """Tests for the EnergyZero integration.""" + from unittest.mock import MagicMock, patch from energyzero import EnergyZeroConnectionError diff --git a/tests/components/energyzero/test_sensor.py b/tests/components/energyzero/test_sensor.py index 6c7eec9d5d8..5c4700c21f1 100644 --- a/tests/components/energyzero/test_sensor.py +++ b/tests/components/energyzero/test_sensor.py @@ -1,4 +1,5 @@ """Tests for the sensors provided by the EnergyZero integration.""" + from unittest.mock import MagicMock from energyzero import EnergyZeroNoDataError diff --git a/tests/components/energyzero/test_services.py b/tests/components/energyzero/test_services.py index c0b54729e03..38929d7007a 100644 --- a/tests/components/energyzero/test_services.py +++ b/tests/components/energyzero/test_services.py @@ -1,5 +1,7 @@ """Tests for the services provided by the EnergyZero integration.""" +import re + import pytest from syrupy.assertion import SnapshotAssertion import voluptuous as vol @@ -101,7 +103,7 @@ def config_entry_data( "start": "incorrect date", }, ServiceValidationError, - "Invalid datetime provided.", + "Invalid date provided. Got incorrect date", ), ( {"config_entry": True}, @@ -110,7 +112,7 @@ def config_entry_data( "end": "incorrect date", }, ServiceValidationError, - "Invalid datetime provided.", + "Invalid date provided. Got incorrect date", ), ], indirect=["config_entry_data"], @@ -125,7 +127,7 @@ async def test_service_validation( ) -> None: """Test the EnergyZero Service validation.""" - with pytest.raises(error, match=error_message): + with pytest.raises(error) as exc: await hass.services.async_call( DOMAIN, service, @@ -133,6 +135,7 @@ async def test_service_validation( blocking=True, return_response=True, ) + assert re.match(error_message, str(exc.value)) @pytest.mark.usefixtures("init_integration") diff --git a/tests/components/enocean/test_config_flow.py b/tests/components/enocean/test_config_flow.py index ad13af0caf2..45a4e6e387f 100644 --- a/tests/components/enocean/test_config_flow.py +++ b/tests/components/enocean/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for EnOcean config flow.""" + from unittest.mock import Mock, patch from homeassistant import config_entries, data_entry_flow @@ -74,8 +75,9 @@ async def test_detection_flow_with_custom_path(hass: HomeAssistant) -> None: USER_PROVIDED_PATH = EnOceanFlowHandler.MANUAL_PATH_VALUE FAKE_DONGLE_PATH = "/fake/dongle" - with patch(DONGLE_VALIDATE_PATH_METHOD, Mock(return_value=True)), patch( - DONGLE_DETECT_METHOD, Mock(return_value=[FAKE_DONGLE_PATH]) + with ( + patch(DONGLE_VALIDATE_PATH_METHOD, Mock(return_value=True)), + patch(DONGLE_DETECT_METHOD, Mock(return_value=[FAKE_DONGLE_PATH])), ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -92,8 +94,9 @@ async def test_detection_flow_with_invalid_path(hass: HomeAssistant) -> None: USER_PROVIDED_PATH = "/invalid/path" FAKE_DONGLE_PATH = "/fake/dongle" - with patch(DONGLE_VALIDATE_PATH_METHOD, Mock(return_value=False)), patch( - DONGLE_DETECT_METHOD, Mock(return_value=[FAKE_DONGLE_PATH]) + with ( + patch(DONGLE_VALIDATE_PATH_METHOD, Mock(return_value=False)), + patch(DONGLE_DETECT_METHOD, Mock(return_value=[FAKE_DONGLE_PATH])), ): result = await hass.config_entries.flow.async_init( DOMAIN, diff --git a/tests/components/enphase_envoy/conftest.py b/tests/components/enphase_envoy/conftest.py index c042846cd0b..40d409aea8e 100644 --- a/tests/components/enphase_envoy/conftest.py +++ b/tests/components/enphase_envoy/conftest.py @@ -1,4 +1,5 @@ """Define test fixtures for Enphase Envoy.""" + from unittest.mock import AsyncMock, Mock, patch from pyenphase import ( @@ -54,15 +55,18 @@ def config_fixture(): @pytest.fixture(name="mock_envoy") -def mock_envoy_fixture(serial_number, mock_authenticate, mock_setup, mock_auth): +def mock_envoy_fixture( + serial_number, + mock_authenticate, + mock_setup, + mock_auth, +): """Define a mocked Envoy fixture.""" mock_envoy = Mock(spec=Envoy) mock_envoy.serial_number = serial_number mock_envoy.firmware = "7.1.2" mock_envoy.part_number = "123456789" - mock_envoy.envoy_model = ( - "Envoy, phases: 3, phase mode: three, net-consumption CT, production CT" - ) + mock_envoy.envoy_model = "Envoy, phases: 3, phase mode: three, net-consumption CT, production CT, storage CT" mock_envoy.authenticate = mock_authenticate mock_envoy.setup = mock_setup mock_envoy.auth = mock_auth @@ -77,9 +81,10 @@ def mock_envoy_fixture(serial_number, mock_authenticate, mock_setup, mock_auth): mock_envoy.phase_mode = EnvoyPhaseMode.THREE mock_envoy.phase_count = 3 mock_envoy.active_phase_count = 3 - mock_envoy.ct_meter_count = 2 + mock_envoy.ct_meter_count = 3 mock_envoy.consumption_meter_type = CtType.NET_CONSUMPTION mock_envoy.production_meter_type = CtType.PRODUCTION + mock_envoy.storage_meter_type = CtType.STORAGE mock_envoy.data = EnvoyData( system_consumption=EnvoySystemConsumption( watt_hours_last_7_days=1234, @@ -166,6 +171,21 @@ def mock_envoy_fixture(serial_number, mock_authenticate, mock_setup, mock_auth): metering_status=CtMeterStatus.NORMAL, status_flags=[], ), + ctmeter_storage=EnvoyMeterData( + eid="100000030", + timestamp=1708006120, + energy_delivered=31234, + energy_received=32345, + active_power=103, + power_factor=0.23, + voltage=113, + current=0.4, + frequency=50.3, + state=CtState.ENABLED, + measurement_type=CtType.STORAGE, + metering_status=CtMeterStatus.NORMAL, + status_flags=[], + ), ctmeter_production_phases={ PhaseNames.PHASE_1: EnvoyMeterData( eid="100000011", @@ -260,6 +280,53 @@ def mock_envoy_fixture(serial_number, mock_authenticate, mock_setup, mock_auth): status_flags=[], ), }, + ctmeter_storage_phases={ + PhaseNames.PHASE_1: EnvoyMeterData( + eid="100000031", + timestamp=1708006121, + energy_delivered=312341, + energy_received=323451, + active_power=22, + power_factor=0.32, + voltage=113, + current=0.4, + frequency=50.3, + state=CtState.ENABLED, + measurement_type=CtType.STORAGE, + metering_status=CtMeterStatus.NORMAL, + status_flags=[], + ), + PhaseNames.PHASE_2: EnvoyMeterData( + eid="100000032", + timestamp=1708006122, + energy_delivered=312342, + energy_received=323452, + active_power=33, + power_factor=0.23, + voltage=112, + current=0.3, + frequency=50.2, + state=CtState.ENABLED, + measurement_type=CtType.STORAGE, + metering_status=CtMeterStatus.NORMAL, + status_flags=[], + ), + PhaseNames.PHASE_3: EnvoyMeterData( + eid="100000033", + timestamp=1708006123, + energy_delivered=312343, + energy_received=323453, + active_power=53, + power_factor=0.24, + voltage=112, + current=0.3, + frequency=50.2, + state=CtState.ENABLED, + measurement_type=CtType.STORAGE, + metering_status=CtMeterStatus.NORMAL, + status_flags=[], + ), + }, inverters={ "1": EnvoyInverter( serial_number="1", @@ -277,12 +344,15 @@ def mock_envoy_fixture(serial_number, mock_authenticate, mock_setup, mock_auth): @pytest.fixture(name="setup_enphase_envoy") async def setup_enphase_envoy_fixture(hass, config, mock_envoy): """Define a fixture to set up Enphase Envoy.""" - with patch( - "homeassistant.components.enphase_envoy.config_flow.Envoy", - return_value=mock_envoy, - ), patch( - "homeassistant.components.enphase_envoy.Envoy", - return_value=mock_envoy, + with ( + patch( + "homeassistant.components.enphase_envoy.config_flow.Envoy", + return_value=mock_envoy, + ), + patch( + "homeassistant.components.enphase_envoy.Envoy", + return_value=mock_envoy, + ), ): assert await async_setup_component(hass, DOMAIN, config) await hass.async_block_till_done() diff --git a/tests/components/enphase_envoy/snapshots/test_diagnostics.ambr b/tests/components/enphase_envoy/snapshots/test_diagnostics.ambr index 9266ffcf94e..51cda1cc478 100644 --- a/tests/components/enphase_envoy/snapshots/test_diagnostics.ambr +++ b/tests/components/enphase_envoy/snapshots/test_diagnostics.ambr @@ -1,10 +1,7 @@ # serializer version: 1 # name: test_entry_diagnostics dict({ - 'data': dict({ - 'varies_by': 'firmware_version', - }), - 'entry': dict({ + 'config_entry': dict({ 'data': dict({ 'host': '1.1.1.1', 'name': '**REDACTED**', @@ -25,6 +22,4278 @@ 'unique_id': '**REDACTED**', 'version': 1, }), - 'envoy_firmware': '7.1.2', + 'envoy_entities_by_device': list([ + dict({ + 'device': dict({ + 'area_id': None, + 'config_entries': list([ + '45a36e55aaddb2007c5f6602e0c38e72', + ]), + 'configuration_url': None, + 'connections': list([ + ]), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': '<>56789', + 'identifiers': list([ + list([ + 'enphase_envoy', + '<>', + ]), + ]), + 'is_new': False, + 'labels': list([ + ]), + 'manufacturer': 'Enphase', + 'model': 'Envoy, phases: 3, phase mode: three, net-consumption CT, production CT, storage CT', + 'name': 'Envoy <>', + 'name_by_user': None, + 'serial_number': '<>', + 'suggested_area': None, + 'sw_version': '7.1.2', + }), + 'entities': list([ + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_current_power_production', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 3, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kW', + }), + }), + 'original_device_class': 'power', + 'original_icon': 'mdi:flash', + 'original_name': 'Current power production', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'current_power_production', + 'unique_id': '<>_production', + 'unit_of_measurement': 'kW', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'power', + 'friendly_name': 'Envoy <> Current power production', + 'icon': 'mdi:flash', + 'state_class': 'measurement', + 'unit_of_measurement': 'kW', + }), + 'entity_id': 'sensor.envoy_<>_current_power_production', + 'state': '1.234', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_energy_production_today', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Energy production today', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'daily_production', + 'unique_id': '<>_daily_production', + 'unit_of_measurement': 'kWh', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'energy', + 'friendly_name': 'Envoy <> Energy production today', + 'icon': 'mdi:flash', + 'state_class': 'total_increasing', + 'unit_of_measurement': 'kWh', + }), + 'entity_id': 'sensor.envoy_<>_energy_production_today', + 'state': '1.234', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_energy_production_last_seven_days', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Energy production last seven days', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'seven_days_production', + 'unique_id': '<>_seven_days_production', + 'unit_of_measurement': 'kWh', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'energy', + 'friendly_name': 'Envoy <> Energy production last seven days', + 'icon': 'mdi:flash', + 'unit_of_measurement': 'kWh', + }), + 'entity_id': 'sensor.envoy_<>_energy_production_last_seven_days', + 'state': '1.234', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_energy_production', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 3, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime energy production', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_production', + 'unique_id': '<>_lifetime_production', + 'unit_of_measurement': 'MWh', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'energy', + 'friendly_name': 'Envoy <> Lifetime energy production', + 'icon': 'mdi:flash', + 'state_class': 'total_increasing', + 'unit_of_measurement': 'MWh', + }), + 'entity_id': 'sensor.envoy_<>_lifetime_energy_production', + 'state': '0.00<>', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_current_power_consumption', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 3, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kW', + }), + }), + 'original_device_class': 'power', + 'original_icon': 'mdi:flash', + 'original_name': 'Current power consumption', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'current_power_consumption', + 'unique_id': '<>_consumption', + 'unit_of_measurement': 'kW', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'power', + 'friendly_name': 'Envoy <> Current power consumption', + 'icon': 'mdi:flash', + 'state_class': 'measurement', + 'unit_of_measurement': 'kW', + }), + 'entity_id': 'sensor.envoy_<>_current_power_consumption', + 'state': '1.234', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_energy_consumption_today', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Energy consumption today', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'daily_consumption', + 'unique_id': '<>_daily_consumption', + 'unit_of_measurement': 'kWh', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'energy', + 'friendly_name': 'Envoy <> Energy consumption today', + 'icon': 'mdi:flash', + 'state_class': 'total_increasing', + 'unit_of_measurement': 'kWh', + }), + 'entity_id': 'sensor.envoy_<>_energy_consumption_today', + 'state': '1.234', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_energy_consumption_last_seven_days', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Energy consumption last seven days', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'seven_days_consumption', + 'unique_id': '<>_seven_days_consumption', + 'unit_of_measurement': 'kWh', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'energy', + 'friendly_name': 'Envoy <> Energy consumption last seven days', + 'icon': 'mdi:flash', + 'unit_of_measurement': 'kWh', + }), + 'entity_id': 'sensor.envoy_<>_energy_consumption_last_seven_days', + 'state': '1.234', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_energy_consumption', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 3, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime energy consumption', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_consumption', + 'unique_id': '<>_lifetime_consumption', + 'unit_of_measurement': 'MWh', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'energy', + 'friendly_name': 'Envoy <> Lifetime energy consumption', + 'icon': 'mdi:flash', + 'state_class': 'total_increasing', + 'unit_of_measurement': 'MWh', + }), + 'entity_id': 'sensor.envoy_<>_lifetime_energy_consumption', + 'state': '0.00<>', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_current_power_production_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 3, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kW', + }), + }), + 'original_device_class': 'power', + 'original_icon': 'mdi:flash', + 'original_name': 'Current power production l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'current_power_production_phase', + 'unique_id': '<>_production_l1', + 'unit_of_measurement': 'kW', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'power', + 'friendly_name': 'Envoy <> Current power production l1', + 'icon': 'mdi:flash', + 'state_class': 'measurement', + 'unit_of_measurement': 'kW', + }), + 'entity_id': 'sensor.envoy_<>_current_power_production_l1', + 'state': '1.234', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_energy_production_today_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Energy production today l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'daily_production_phase', + 'unique_id': '<>_daily_production_l1', + 'unit_of_measurement': 'kWh', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'energy', + 'friendly_name': 'Envoy <> Energy production today l1', + 'icon': 'mdi:flash', + 'state_class': 'total_increasing', + 'unit_of_measurement': 'kWh', + }), + 'entity_id': 'sensor.envoy_<>_energy_production_today_l1', + 'state': '1.233', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_energy_production_last_seven_days_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Energy production last seven days l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'seven_days_production_phase', + 'unique_id': '<>_seven_days_production_l1', + 'unit_of_measurement': 'kWh', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'energy', + 'friendly_name': 'Envoy <> Energy production last seven days l1', + 'icon': 'mdi:flash', + 'unit_of_measurement': 'kWh', + }), + 'entity_id': 'sensor.envoy_<>_energy_production_last_seven_days_l1', + 'state': '1.231', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_energy_production_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 3, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime energy production l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_production_phase', + 'unique_id': '<>_lifetime_production_l1', + 'unit_of_measurement': 'MWh', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'energy', + 'friendly_name': 'Envoy <> Lifetime energy production l1', + 'icon': 'mdi:flash', + 'state_class': 'total_increasing', + 'unit_of_measurement': 'MWh', + }), + 'entity_id': 'sensor.envoy_<>_lifetime_energy_production_l1', + 'state': '0.001232', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_current_power_production_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 3, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kW', + }), + }), + 'original_device_class': 'power', + 'original_icon': 'mdi:flash', + 'original_name': 'Current power production l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'current_power_production_phase', + 'unique_id': '<>_production_l2', + 'unit_of_measurement': 'kW', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'power', + 'friendly_name': 'Envoy <> Current power production l2', + 'icon': 'mdi:flash', + 'state_class': 'measurement', + 'unit_of_measurement': 'kW', + }), + 'entity_id': 'sensor.envoy_<>_current_power_production_l2', + 'state': '2.234', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_energy_production_today_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Energy production today l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'daily_production_phase', + 'unique_id': '<>_daily_production_l2', + 'unit_of_measurement': 'kWh', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'energy', + 'friendly_name': 'Envoy <> Energy production today l2', + 'icon': 'mdi:flash', + 'state_class': 'total_increasing', + 'unit_of_measurement': 'kWh', + }), + 'entity_id': 'sensor.envoy_<>_energy_production_today_l2', + 'state': '2.233', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_energy_production_last_seven_days_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Energy production last seven days l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'seven_days_production_phase', + 'unique_id': '<>_seven_days_production_l2', + 'unit_of_measurement': 'kWh', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'energy', + 'friendly_name': 'Envoy <> Energy production last seven days l2', + 'icon': 'mdi:flash', + 'unit_of_measurement': 'kWh', + }), + 'entity_id': 'sensor.envoy_<>_energy_production_last_seven_days_l2', + 'state': '2.231', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_energy_production_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 3, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime energy production l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_production_phase', + 'unique_id': '<>_lifetime_production_l2', + 'unit_of_measurement': 'MWh', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'energy', + 'friendly_name': 'Envoy <> Lifetime energy production l2', + 'icon': 'mdi:flash', + 'state_class': 'total_increasing', + 'unit_of_measurement': 'MWh', + }), + 'entity_id': 'sensor.envoy_<>_lifetime_energy_production_l2', + 'state': '0.002232', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_current_power_production_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 3, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kW', + }), + }), + 'original_device_class': 'power', + 'original_icon': 'mdi:flash', + 'original_name': 'Current power production l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'current_power_production_phase', + 'unique_id': '<>_production_l3', + 'unit_of_measurement': 'kW', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'power', + 'friendly_name': 'Envoy <> Current power production l3', + 'icon': 'mdi:flash', + 'state_class': 'measurement', + 'unit_of_measurement': 'kW', + }), + 'entity_id': 'sensor.envoy_<>_current_power_production_l3', + 'state': '3.234', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_energy_production_today_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Energy production today l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'daily_production_phase', + 'unique_id': '<>_daily_production_l3', + 'unit_of_measurement': 'kWh', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'energy', + 'friendly_name': 'Envoy <> Energy production today l3', + 'icon': 'mdi:flash', + 'state_class': 'total_increasing', + 'unit_of_measurement': 'kWh', + }), + 'entity_id': 'sensor.envoy_<>_energy_production_today_l3', + 'state': '3.233', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_energy_production_last_seven_days_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Energy production last seven days l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'seven_days_production_phase', + 'unique_id': '<>_seven_days_production_l3', + 'unit_of_measurement': 'kWh', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'energy', + 'friendly_name': 'Envoy <> Energy production last seven days l3', + 'icon': 'mdi:flash', + 'unit_of_measurement': 'kWh', + }), + 'entity_id': 'sensor.envoy_<>_energy_production_last_seven_days_l3', + 'state': '3.231', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_energy_production_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 3, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime energy production l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_production_phase', + 'unique_id': '<>_lifetime_production_l3', + 'unit_of_measurement': 'MWh', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'energy', + 'friendly_name': 'Envoy <> Lifetime energy production l3', + 'icon': 'mdi:flash', + 'state_class': 'total_increasing', + 'unit_of_measurement': 'MWh', + }), + 'entity_id': 'sensor.envoy_<>_lifetime_energy_production_l3', + 'state': '0.003232', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_current_power_consumption_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 3, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kW', + }), + }), + 'original_device_class': 'power', + 'original_icon': 'mdi:flash', + 'original_name': 'Current power consumption l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'current_power_consumption_phase', + 'unique_id': '<>_consumption_l1', + 'unit_of_measurement': 'kW', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'power', + 'friendly_name': 'Envoy <> Current power consumption l1', + 'icon': 'mdi:flash', + 'state_class': 'measurement', + 'unit_of_measurement': 'kW', + }), + 'entity_id': 'sensor.envoy_<>_current_power_consumption_l1', + 'state': '1.324', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_energy_consumption_today_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Energy consumption today l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'daily_consumption_phase', + 'unique_id': '<>_daily_consumption_l1', + 'unit_of_measurement': 'kWh', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'energy', + 'friendly_name': 'Envoy <> Energy consumption today l1', + 'icon': 'mdi:flash', + 'state_class': 'total_increasing', + 'unit_of_measurement': 'kWh', + }), + 'entity_id': 'sensor.envoy_<>_energy_consumption_today_l1', + 'state': '1.323', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_energy_consumption_last_seven_days_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Energy consumption last seven days l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'seven_days_consumption_phase', + 'unique_id': '<>_seven_days_consumption_l1', + 'unit_of_measurement': 'kWh', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'energy', + 'friendly_name': 'Envoy <> Energy consumption last seven days l1', + 'icon': 'mdi:flash', + 'unit_of_measurement': 'kWh', + }), + 'entity_id': 'sensor.envoy_<>_energy_consumption_last_seven_days_l1', + 'state': '1.321', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_energy_consumption_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 3, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime energy consumption l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_consumption_phase', + 'unique_id': '<>_lifetime_consumption_l1', + 'unit_of_measurement': 'MWh', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'energy', + 'friendly_name': 'Envoy <> Lifetime energy consumption l1', + 'icon': 'mdi:flash', + 'state_class': 'total_increasing', + 'unit_of_measurement': 'MWh', + }), + 'entity_id': 'sensor.envoy_<>_lifetime_energy_consumption_l1', + 'state': '0.001322', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_current_power_consumption_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 3, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kW', + }), + }), + 'original_device_class': 'power', + 'original_icon': 'mdi:flash', + 'original_name': 'Current power consumption l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'current_power_consumption_phase', + 'unique_id': '<>_consumption_l2', + 'unit_of_measurement': 'kW', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'power', + 'friendly_name': 'Envoy <> Current power consumption l2', + 'icon': 'mdi:flash', + 'state_class': 'measurement', + 'unit_of_measurement': 'kW', + }), + 'entity_id': 'sensor.envoy_<>_current_power_consumption_l2', + 'state': '2.324', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_energy_consumption_today_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Energy consumption today l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'daily_consumption_phase', + 'unique_id': '<>_daily_consumption_l2', + 'unit_of_measurement': 'kWh', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'energy', + 'friendly_name': 'Envoy <> Energy consumption today l2', + 'icon': 'mdi:flash', + 'state_class': 'total_increasing', + 'unit_of_measurement': 'kWh', + }), + 'entity_id': 'sensor.envoy_<>_energy_consumption_today_l2', + 'state': '2.323', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_energy_consumption_last_seven_days_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Energy consumption last seven days l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'seven_days_consumption_phase', + 'unique_id': '<>_seven_days_consumption_l2', + 'unit_of_measurement': 'kWh', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'energy', + 'friendly_name': 'Envoy <> Energy consumption last seven days l2', + 'icon': 'mdi:flash', + 'unit_of_measurement': 'kWh', + }), + 'entity_id': 'sensor.envoy_<>_energy_consumption_last_seven_days_l2', + 'state': '2.321', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_energy_consumption_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 3, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime energy consumption l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_consumption_phase', + 'unique_id': '<>_lifetime_consumption_l2', + 'unit_of_measurement': 'MWh', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'energy', + 'friendly_name': 'Envoy <> Lifetime energy consumption l2', + 'icon': 'mdi:flash', + 'state_class': 'total_increasing', + 'unit_of_measurement': 'MWh', + }), + 'entity_id': 'sensor.envoy_<>_lifetime_energy_consumption_l2', + 'state': '0.002322', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_current_power_consumption_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 3, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kW', + }), + }), + 'original_device_class': 'power', + 'original_icon': 'mdi:flash', + 'original_name': 'Current power consumption l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'current_power_consumption_phase', + 'unique_id': '<>_consumption_l3', + 'unit_of_measurement': 'kW', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'power', + 'friendly_name': 'Envoy <> Current power consumption l3', + 'icon': 'mdi:flash', + 'state_class': 'measurement', + 'unit_of_measurement': 'kW', + }), + 'entity_id': 'sensor.envoy_<>_current_power_consumption_l3', + 'state': '3.324', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_energy_consumption_today_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Energy consumption today l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'daily_consumption_phase', + 'unique_id': '<>_daily_consumption_l3', + 'unit_of_measurement': 'kWh', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'energy', + 'friendly_name': 'Envoy <> Energy consumption today l3', + 'icon': 'mdi:flash', + 'state_class': 'total_increasing', + 'unit_of_measurement': 'kWh', + }), + 'entity_id': 'sensor.envoy_<>_energy_consumption_today_l3', + 'state': '3.323', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_energy_consumption_last_seven_days_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Energy consumption last seven days l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'seven_days_consumption_phase', + 'unique_id': '<>_seven_days_consumption_l3', + 'unit_of_measurement': 'kWh', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'energy', + 'friendly_name': 'Envoy <> Energy consumption last seven days l3', + 'icon': 'mdi:flash', + 'unit_of_measurement': 'kWh', + }), + 'entity_id': 'sensor.envoy_<>_energy_consumption_last_seven_days_l3', + 'state': '3.321', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_energy_consumption_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 3, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime energy consumption l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_consumption_phase', + 'unique_id': '<>_lifetime_consumption_l3', + 'unit_of_measurement': 'MWh', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'energy', + 'friendly_name': 'Envoy <> Lifetime energy consumption l3', + 'icon': 'mdi:flash', + 'state_class': 'total_increasing', + 'unit_of_measurement': 'MWh', + }), + 'entity_id': 'sensor.envoy_<>_lifetime_energy_consumption_l3', + 'state': '0.003322', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_net_energy_consumption', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 3, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime net energy consumption', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_net_consumption', + 'unique_id': '<>_lifetime_net_consumption', + 'unit_of_measurement': 'MWh', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'energy', + 'friendly_name': 'Envoy <> Lifetime net energy consumption', + 'icon': 'mdi:flash', + 'state_class': 'total_increasing', + 'unit_of_measurement': 'MWh', + }), + 'entity_id': 'sensor.envoy_<>_lifetime_net_energy_consumption', + 'state': '0.02<>', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_net_energy_production', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 3, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime net energy production', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_net_production', + 'unique_id': '<>_lifetime_net_production', + 'unit_of_measurement': 'MWh', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'energy', + 'friendly_name': 'Envoy <> Lifetime net energy production', + 'icon': 'mdi:flash', + 'state_class': 'total_increasing', + 'unit_of_measurement': 'MWh', + }), + 'entity_id': 'sensor.envoy_<>_lifetime_net_energy_production', + 'state': '0.022345', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_current_net_power_consumption', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 3, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kW', + }), + }), + 'original_device_class': 'power', + 'original_icon': 'mdi:flash', + 'original_name': 'Current net power consumption', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_consumption', + 'unique_id': '<>_net_consumption', + 'unit_of_measurement': 'kW', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'power', + 'friendly_name': 'Envoy <> Current net power consumption', + 'icon': 'mdi:flash', + 'state_class': 'measurement', + 'unit_of_measurement': 'kW', + }), + 'entity_id': 'sensor.envoy_<>_current_net_power_consumption', + 'state': '0.101', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_frequency_net_consumption_ct', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': 'frequency', + 'original_icon': 'mdi:flash', + 'original_name': 'Frequency net consumption CT', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_ct_frequency', + 'unique_id': '<>_frequency', + 'unit_of_measurement': 'Hz', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_voltage_net_consumption_ct', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'V', + }), + }), + 'original_device_class': 'voltage', + 'original_icon': 'mdi:flash', + 'original_name': 'Voltage net consumption CT', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_ct_voltage', + 'unique_id': '<>_voltage', + 'unit_of_measurement': 'V', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'normal', + 'not-metering', + 'check-wiring', + ]), + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_metering_status_net_consumption_ct', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': 'enum', + 'original_icon': 'mdi:flash', + 'original_name': 'Metering status net consumption CT', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_ct_metering_status', + 'unique_id': '<>_net_consumption_ct_metering_status', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_meter_status_flags_active_net_consumption_ct', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:flash', + 'original_name': 'Meter status flags active net consumption CT', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_ct_status_flags', + 'unique_id': '<>_net_consumption_ct_status_flags', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_net_energy_consumption_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime net energy consumption l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_net_consumption_phase', + 'unique_id': '<>_lifetime_net_consumption_l1', + 'unit_of_measurement': 'MWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_net_energy_production_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime net energy production l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_net_production_phase', + 'unique_id': '<>_lifetime_net_production_l1', + 'unit_of_measurement': 'MWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_current_net_power_consumption_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kW', + }), + }), + 'original_device_class': 'power', + 'original_icon': 'mdi:flash', + 'original_name': 'Current net power consumption l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_consumption_phase', + 'unique_id': '<>_net_consumption_l1', + 'unit_of_measurement': 'kW', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_frequency_net_consumption_ct_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': 'frequency', + 'original_icon': 'mdi:flash', + 'original_name': 'Frequency net consumption CT l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_ct_frequency_phase', + 'unique_id': '<>_frequency_l1', + 'unit_of_measurement': 'Hz', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_voltage_net_consumption_ct_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'V', + }), + }), + 'original_device_class': 'voltage', + 'original_icon': 'mdi:flash', + 'original_name': 'Voltage net consumption CT l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_ct_voltage_phase', + 'unique_id': '<>_voltage_l1', + 'unit_of_measurement': 'V', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'normal', + 'not-metering', + 'check-wiring', + ]), + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_metering_status_net_consumption_ct_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': 'enum', + 'original_icon': 'mdi:flash', + 'original_name': 'Metering status net consumption CT l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_ct_metering_status_phase', + 'unique_id': '<>_net_consumption_ct_metering_status_l1', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_meter_status_flags_active_net_consumption_ct_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:flash', + 'original_name': 'Meter status flags active net consumption CT l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_ct_status_flags_phase', + 'unique_id': '<>_net_consumption_ct_status_flags_l1', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_net_energy_consumption_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime net energy consumption l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_net_consumption_phase', + 'unique_id': '<>_lifetime_net_consumption_l2', + 'unit_of_measurement': 'MWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_net_energy_production_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime net energy production l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_net_production_phase', + 'unique_id': '<>_lifetime_net_production_l2', + 'unit_of_measurement': 'MWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_current_net_power_consumption_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kW', + }), + }), + 'original_device_class': 'power', + 'original_icon': 'mdi:flash', + 'original_name': 'Current net power consumption l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_consumption_phase', + 'unique_id': '<>_net_consumption_l2', + 'unit_of_measurement': 'kW', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_frequency_net_consumption_ct_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': 'frequency', + 'original_icon': 'mdi:flash', + 'original_name': 'Frequency net consumption CT l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_ct_frequency_phase', + 'unique_id': '<>_frequency_l2', + 'unit_of_measurement': 'Hz', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_voltage_net_consumption_ct_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'V', + }), + }), + 'original_device_class': 'voltage', + 'original_icon': 'mdi:flash', + 'original_name': 'Voltage net consumption CT l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_ct_voltage_phase', + 'unique_id': '<>_voltage_l2', + 'unit_of_measurement': 'V', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'normal', + 'not-metering', + 'check-wiring', + ]), + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_metering_status_net_consumption_ct_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': 'enum', + 'original_icon': 'mdi:flash', + 'original_name': 'Metering status net consumption CT l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_ct_metering_status_phase', + 'unique_id': '<>_net_consumption_ct_metering_status_l2', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_meter_status_flags_active_net_consumption_ct_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:flash', + 'original_name': 'Meter status flags active net consumption CT l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_ct_status_flags_phase', + 'unique_id': '<>_net_consumption_ct_status_flags_l2', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_net_energy_consumption_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime net energy consumption l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_net_consumption_phase', + 'unique_id': '<>_lifetime_net_consumption_l3', + 'unit_of_measurement': 'MWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_net_energy_production_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime net energy production l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_net_production_phase', + 'unique_id': '<>_lifetime_net_production_l3', + 'unit_of_measurement': 'MWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_current_net_power_consumption_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kW', + }), + }), + 'original_device_class': 'power', + 'original_icon': 'mdi:flash', + 'original_name': 'Current net power consumption l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_consumption_phase', + 'unique_id': '<>_net_consumption_l3', + 'unit_of_measurement': 'kW', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_frequency_net_consumption_ct_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': 'frequency', + 'original_icon': 'mdi:flash', + 'original_name': 'Frequency net consumption CT l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_ct_frequency_phase', + 'unique_id': '<>_frequency_l3', + 'unit_of_measurement': 'Hz', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_voltage_net_consumption_ct_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'V', + }), + }), + 'original_device_class': 'voltage', + 'original_icon': 'mdi:flash', + 'original_name': 'Voltage net consumption CT l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_ct_voltage_phase', + 'unique_id': '<>_voltage_l3', + 'unit_of_measurement': 'V', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'normal', + 'not-metering', + 'check-wiring', + ]), + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_metering_status_net_consumption_ct_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': 'enum', + 'original_icon': 'mdi:flash', + 'original_name': 'Metering status net consumption CT l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_ct_metering_status_phase', + 'unique_id': '<>_net_consumption_ct_metering_status_l3', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_meter_status_flags_active_net_consumption_ct_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:flash', + 'original_name': 'Meter status flags active net consumption CT l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'net_ct_status_flags_phase', + 'unique_id': '<>_net_consumption_ct_status_flags_l3', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'normal', + 'not-metering', + 'check-wiring', + ]), + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_metering_status_production_ct', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': 'enum', + 'original_icon': 'mdi:flash', + 'original_name': 'Metering status production CT', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'production_ct_metering_status', + 'unique_id': '<>_production_ct_metering_status', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_meter_status_flags_active_production_ct', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:flash', + 'original_name': 'Meter status flags active production CT', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'production_ct_status_flags', + 'unique_id': '<>_production_ct_status_flags', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'normal', + 'not-metering', + 'check-wiring', + ]), + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_metering_status_production_ct_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': 'enum', + 'original_icon': 'mdi:flash', + 'original_name': 'Metering status production CT l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'production_ct_metering_status_phase', + 'unique_id': '<>_production_ct_metering_status_l1', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_meter_status_flags_active_production_ct_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:flash', + 'original_name': 'Meter status flags active production CT l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'production_ct_status_flags_phase', + 'unique_id': '<>_production_ct_status_flags_l1', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'normal', + 'not-metering', + 'check-wiring', + ]), + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_metering_status_production_ct_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': 'enum', + 'original_icon': 'mdi:flash', + 'original_name': 'Metering status production CT l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'production_ct_metering_status_phase', + 'unique_id': '<>_production_ct_metering_status_l2', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_meter_status_flags_active_production_ct_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:flash', + 'original_name': 'Meter status flags active production CT l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'production_ct_status_flags_phase', + 'unique_id': '<>_production_ct_status_flags_l2', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'normal', + 'not-metering', + 'check-wiring', + ]), + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_metering_status_production_ct_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': 'enum', + 'original_icon': 'mdi:flash', + 'original_name': 'Metering status production CT l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'production_ct_metering_status_phase', + 'unique_id': '<>_production_ct_metering_status_l3', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_meter_status_flags_active_production_ct_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:flash', + 'original_name': 'Meter status flags active production CT l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'production_ct_status_flags_phase', + 'unique_id': '<>_production_ct_status_flags_l3', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_battery_energy_discharged', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 3, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime battery energy discharged', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_battery_discharged', + 'unique_id': '<>_lifetime_battery_discharged', + 'unit_of_measurement': 'MWh', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'energy', + 'friendly_name': 'Envoy <> Lifetime battery energy discharged', + 'icon': 'mdi:flash', + 'state_class': 'total_increasing', + 'unit_of_measurement': 'MWh', + }), + 'entity_id': 'sensor.envoy_<>_lifetime_battery_energy_discharged', + 'state': '0.03<>', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_battery_energy_charged', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 3, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime battery energy charged', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_battery_charged', + 'unique_id': '<>_lifetime_battery_charged', + 'unit_of_measurement': 'MWh', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'energy', + 'friendly_name': 'Envoy <> Lifetime battery energy charged', + 'icon': 'mdi:flash', + 'state_class': 'total_increasing', + 'unit_of_measurement': 'MWh', + }), + 'entity_id': 'sensor.envoy_<>_lifetime_battery_energy_charged', + 'state': '0.032345', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_current_battery_discharge', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 3, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kW', + }), + }), + 'original_device_class': 'power', + 'original_icon': 'mdi:flash', + 'original_name': 'Current battery discharge', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'battery_discharge', + 'unique_id': '<>_battery_discharge', + 'unit_of_measurement': 'kW', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'power', + 'friendly_name': 'Envoy <> Current battery discharge', + 'icon': 'mdi:flash', + 'state_class': 'measurement', + 'unit_of_measurement': 'kW', + }), + 'entity_id': 'sensor.envoy_<>_current_battery_discharge', + 'state': '0.103', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_voltage_storage_ct', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'V', + }), + }), + 'original_device_class': 'voltage', + 'original_icon': 'mdi:flash', + 'original_name': 'Voltage storage CT', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'storage_ct_voltage', + 'unique_id': '<>_storage_voltage', + 'unit_of_measurement': 'V', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'normal', + 'not-metering', + 'check-wiring', + ]), + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_metering_status_storage_ct', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': 'enum', + 'original_icon': 'mdi:flash', + 'original_name': 'Metering status storage CT', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'storage_ct_metering_status', + 'unique_id': '<>_storage_ct_metering_status', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_meter_status_flags_active_storage_ct', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:flash', + 'original_name': 'Meter status flags active storage CT', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'storage_ct_status_flags', + 'unique_id': '<>_storage_ct_status_flags', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_battery_energy_discharged_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime battery energy discharged l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_battery_discharged_phase', + 'unique_id': '<>_lifetime_battery_discharged_l1', + 'unit_of_measurement': 'MWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_battery_energy_charged_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime battery energy charged l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_battery_charged_phase', + 'unique_id': '<>_lifetime_battery_charged_l1', + 'unit_of_measurement': 'MWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_current_battery_discharge_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kW', + }), + }), + 'original_device_class': 'power', + 'original_icon': 'mdi:flash', + 'original_name': 'Current battery discharge l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'battery_discharge_phase', + 'unique_id': '<>_battery_discharge_l1', + 'unit_of_measurement': 'kW', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_voltage_storage_ct_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'V', + }), + }), + 'original_device_class': 'voltage', + 'original_icon': 'mdi:flash', + 'original_name': 'Voltage storage CT l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'storage_ct_voltage_phase', + 'unique_id': '<>_storage_voltage_l1', + 'unit_of_measurement': 'V', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'normal', + 'not-metering', + 'check-wiring', + ]), + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_metering_status_storage_ct_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': 'enum', + 'original_icon': 'mdi:flash', + 'original_name': 'Metering status storage CT l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'storage_ct_metering_status_phase', + 'unique_id': '<>_storage_ct_metering_status_l1', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_meter_status_flags_active_storage_ct_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:flash', + 'original_name': 'Meter status flags active storage CT l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'storage_ct_status_flags_phase', + 'unique_id': '<>_storage_ct_status_flags_l1', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_battery_energy_discharged_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime battery energy discharged l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_battery_discharged_phase', + 'unique_id': '<>_lifetime_battery_discharged_l2', + 'unit_of_measurement': 'MWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_battery_energy_charged_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime battery energy charged l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_battery_charged_phase', + 'unique_id': '<>_lifetime_battery_charged_l2', + 'unit_of_measurement': 'MWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_current_battery_discharge_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kW', + }), + }), + 'original_device_class': 'power', + 'original_icon': 'mdi:flash', + 'original_name': 'Current battery discharge l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'battery_discharge_phase', + 'unique_id': '<>_battery_discharge_l2', + 'unit_of_measurement': 'kW', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_voltage_storage_ct_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'V', + }), + }), + 'original_device_class': 'voltage', + 'original_icon': 'mdi:flash', + 'original_name': 'Voltage storage CT l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'storage_ct_voltage_phase', + 'unique_id': '<>_storage_voltage_l2', + 'unit_of_measurement': 'V', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'normal', + 'not-metering', + 'check-wiring', + ]), + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_metering_status_storage_ct_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': 'enum', + 'original_icon': 'mdi:flash', + 'original_name': 'Metering status storage CT l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'storage_ct_metering_status_phase', + 'unique_id': '<>_storage_ct_metering_status_l2', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_meter_status_flags_active_storage_ct_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:flash', + 'original_name': 'Meter status flags active storage CT l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'storage_ct_status_flags_phase', + 'unique_id': '<>_storage_ct_status_flags_l2', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_battery_energy_discharged_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime battery energy discharged l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_battery_discharged_phase', + 'unique_id': '<>_lifetime_battery_discharged_l3', + 'unit_of_measurement': 'MWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'total_increasing', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_lifetime_battery_energy_charged_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'MWh', + }), + }), + 'original_device_class': 'energy', + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime battery energy charged l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_battery_charged_phase', + 'unique_id': '<>_lifetime_battery_charged_l3', + 'unit_of_measurement': 'MWh', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_current_battery_discharge_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'kW', + }), + }), + 'original_device_class': 'power', + 'original_icon': 'mdi:flash', + 'original_name': 'Current battery discharge l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'battery_discharge_phase', + 'unique_id': '<>_battery_discharge_l3', + 'unit_of_measurement': 'kW', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_voltage_storage_ct_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': 'V', + }), + }), + 'original_device_class': 'voltage', + 'original_icon': 'mdi:flash', + 'original_name': 'Voltage storage CT l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'storage_ct_voltage_phase', + 'unique_id': '<>_storage_voltage_l3', + 'unit_of_measurement': 'V', + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'normal', + 'not-metering', + 'check-wiring', + ]), + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_metering_status_storage_ct_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': 'enum', + 'original_icon': 'mdi:flash', + 'original_name': 'Metering status storage CT l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'storage_ct_metering_status_phase', + 'unique_id': '<>_storage_ct_metering_status_l3', + 'unit_of_measurement': None, + }), + 'state': None, + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_<>_meter_status_flags_active_storage_ct_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:flash', + 'original_name': 'Meter status flags active storage CT l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'storage_ct_status_flags_phase', + 'unique_id': '<>_storage_ct_status_flags_l3', + 'unit_of_measurement': None, + }), + 'state': None, + }), + ]), + }), + dict({ + 'device': dict({ + 'area_id': None, + 'config_entries': list([ + '45a36e55aaddb2007c5f6602e0c38e72', + ]), + 'configuration_url': None, + 'connections': list([ + ]), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'identifiers': list([ + list([ + 'enphase_envoy', + '1', + ]), + ]), + 'is_new': False, + 'labels': list([ + ]), + 'manufacturer': 'Enphase', + 'model': 'Inverter', + 'name': 'Inverter 1', + 'name_by_user': None, + 'serial_number': None, + 'suggested_area': None, + 'sw_version': None, + }), + 'entities': list([ + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': 'measurement', + }), + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.inverter_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': 'power', + 'original_icon': 'mdi:flash', + 'original_name': None, + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1', + 'unit_of_measurement': 'W', + }), + 'state': dict({ + 'attributes': dict({ + 'device_class': 'power', + 'friendly_name': 'Inverter 1', + 'icon': 'mdi:flash', + 'state_class': 'measurement', + 'unit_of_measurement': 'W', + }), + 'entity_id': 'sensor.inverter_1', + 'state': '1', + }), + }), + dict({ + 'entity': dict({ + 'aliases': list([ + ]), + 'area_id': None, + 'capabilities': None, + 'categories': dict({ + }), + 'config_entry_id': '45a36e55aaddb2007c5f6602e0c38e72', + 'device_class': None, + 'disabled_by': 'integration', + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.inverter_1_last_reported', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'labels': list([ + ]), + 'name': None, + 'options': dict({ + }), + 'original_device_class': 'timestamp', + 'original_icon': 'mdi:flash', + 'original_name': 'Last reported', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'last_reported', + 'unique_id': '1_last_reported', + 'unit_of_measurement': None, + }), + 'state': None, + }), + ]), + }), + ]), + 'envoy_model_data': dict({ + 'ctmeter_consumption': dict({ + '__type': "", + 'repr': "EnvoyMeterData(eid='100000020', timestamp=1708006120, energy_delivered=21234, energy_received=22345, active_power=101, power_factor=0.21, voltage=112, current=0.3, frequency=50.2, state=, measurement_type=, metering_status=, status_flags=[])", + }), + 'ctmeter_consumption_phases': dict({ + 'L1': dict({ + '__type': "", + 'repr': "EnvoyMeterData(eid='100000021', timestamp=1708006121, energy_delivered=212341, energy_received=223451, active_power=21, power_factor=0.22, voltage=112, current=0.3, frequency=50.2, state=, measurement_type=, metering_status=, status_flags=[])", + }), + 'L2': dict({ + '__type': "", + 'repr': "EnvoyMeterData(eid='100000022', timestamp=1708006122, energy_delivered=212342, energy_received=223452, active_power=31, power_factor=0.23, voltage=112, current=0.3, frequency=50.2, state=, measurement_type=, metering_status=, status_flags=[])", + }), + 'L3': dict({ + '__type': "", + 'repr': "EnvoyMeterData(eid='100000023', timestamp=1708006123, energy_delivered=212343, energy_received=223453, active_power=51, power_factor=0.24, voltage=112, current=0.3, frequency=50.2, state=, measurement_type=, metering_status=, status_flags=[])", + }), + }), + 'ctmeter_production': dict({ + '__type': "", + 'repr': "EnvoyMeterData(eid='100000010', timestamp=1708006110, energy_delivered=11234, energy_received=12345, active_power=100, power_factor=0.11, voltage=111, current=0.2, frequency=50.1, state=, measurement_type=, metering_status=, status_flags=[, ])", + }), + 'ctmeter_production_phases': dict({ + 'L1': dict({ + '__type': "", + 'repr': "EnvoyMeterData(eid='100000011', timestamp=1708006111, energy_delivered=112341, energy_received=123451, active_power=20, power_factor=0.12, voltage=111, current=0.2, frequency=50.1, state=, measurement_type=, metering_status=, status_flags=[])", + }), + 'L2': dict({ + '__type': "", + 'repr': "EnvoyMeterData(eid='100000012', timestamp=1708006112, energy_delivered=112342, energy_received=123452, active_power=30, power_factor=0.13, voltage=111, current=0.2, frequency=50.1, state=, measurement_type=, metering_status=, status_flags=[])", + }), + 'L3': dict({ + '__type': "", + 'repr': "EnvoyMeterData(eid='100000013', timestamp=1708006113, energy_delivered=112343, energy_received=123453, active_power=50, power_factor=0.14, voltage=111, current=0.2, frequency=50.1, state=, measurement_type=, metering_status=, status_flags=[])", + }), + }), + 'ctmeter_storage': dict({ + '__type': "", + 'repr': "EnvoyMeterData(eid='100000030', timestamp=1708006120, energy_delivered=31234, energy_received=32345, active_power=103, power_factor=0.23, voltage=113, current=0.4, frequency=50.3, state=, measurement_type=, metering_status=, status_flags=[])", + }), + 'ctmeter_storage_phases': dict({ + 'L1': dict({ + '__type': "", + 'repr': "EnvoyMeterData(eid='100000031', timestamp=1708006121, energy_delivered=312341, energy_received=323451, active_power=22, power_factor=0.32, voltage=113, current=0.4, frequency=50.3, state=, measurement_type=, metering_status=, status_flags=[])", + }), + 'L2': dict({ + '__type': "", + 'repr': "EnvoyMeterData(eid='100000032', timestamp=1708006122, energy_delivered=312342, energy_received=323452, active_power=33, power_factor=0.23, voltage=112, current=0.3, frequency=50.2, state=, measurement_type=, metering_status=, status_flags=[])", + }), + 'L3': dict({ + '__type': "", + 'repr': "EnvoyMeterData(eid='100000033', timestamp=1708006123, energy_delivered=312343, energy_received=323453, active_power=53, power_factor=0.24, voltage=112, current=0.3, frequency=50.2, state=, measurement_type=, metering_status=, status_flags=[])", + }), + }), + 'dry_contact_settings': dict({ + }), + 'dry_contact_status': dict({ + }), + 'encharge_aggregate': None, + 'encharge_inventory': None, + 'encharge_power': None, + 'enpower': None, + 'inverters': dict({ + '1': dict({ + '__type': "", + 'repr': "EnvoyInverter(serial_number='1', last_report_date=1, last_report_watts=1, max_report_watts=1)", + }), + }), + 'system_consumption': dict({ + '__type': "", + 'repr': 'EnvoySystemConsumption(watt_hours_lifetime=1234, watt_hours_last_7_days=1234, watt_hours_today=1234, watts_now=1234)', + }), + 'system_consumption_phases': dict({ + 'L1': dict({ + '__type': "", + 'repr': 'EnvoySystemConsumption(watt_hours_lifetime=1322, watt_hours_last_7_days=1321, watt_hours_today=1323, watts_now=1324)', + }), + 'L2': dict({ + '__type': "", + 'repr': 'EnvoySystemConsumption(watt_hours_lifetime=2322, watt_hours_last_7_days=2321, watt_hours_today=2323, watts_now=2324)', + }), + 'L3': dict({ + '__type': "", + 'repr': 'EnvoySystemConsumption(watt_hours_lifetime=3322, watt_hours_last_7_days=3321, watt_hours_today=3323, watts_now=3324)', + }), + }), + 'system_production': dict({ + '__type': "", + 'repr': 'EnvoySystemProduction(watt_hours_lifetime=1234, watt_hours_last_7_days=1234, watt_hours_today=1234, watts_now=1234)', + }), + 'system_production_phases': dict({ + 'L1': dict({ + '__type': "", + 'repr': 'EnvoySystemProduction(watt_hours_lifetime=1232, watt_hours_last_7_days=1231, watt_hours_today=1233, watts_now=1234)', + }), + 'L2': dict({ + '__type': "", + 'repr': 'EnvoySystemProduction(watt_hours_lifetime=2232, watt_hours_last_7_days=2231, watt_hours_today=2233, watts_now=2234)', + }), + 'L3': dict({ + '__type': "", + 'repr': 'EnvoySystemProduction(watt_hours_lifetime=3232, watt_hours_last_7_days=3231, watt_hours_today=3233, watts_now=3234)', + }), + }), + 'tariff': None, + }), + 'envoy_properties': dict({ + 'active_phasecount': 3, + 'ct_consumption_meter': 'net-consumption', + 'ct_count': 3, + 'ct_production_meter': 'production', + 'ct_storage_meter': 'storage', + 'envoy_firmware': '7.1.2', + 'envoy_model': 'Envoy, phases: 3, phase mode: three, net-consumption CT, production CT, storage CT', + 'part_number': '123456789', + 'phase_count': 3, + 'phase_mode': 'three', + 'supported_features': list([ + 'INVERTERS', + 'METERING', + 'PRODUCTION', + 'THREEPHASE', + 'CTMETERS', + ]), + }), + 'raw_data': dict({ + 'varies_by': 'firmware_version', + }), }) # --- diff --git a/tests/components/enphase_envoy/snapshots/test_sensor.ambr b/tests/components/enphase_envoy/snapshots/test_sensor.ambr index c3fa2bf23f5..4024c43c655 100644 --- a/tests/components/enphase_envoy/snapshots/test_sensor.ambr +++ b/tests/components/enphase_envoy/snapshots/test_sensor.ambr @@ -2494,6 +2494,863 @@ 'unique_id': '1234_production_ct_status_flags_l3', 'unit_of_measurement': None, }), + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_1234_lifetime_battery_energy_discharged', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 3, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime battery energy discharged', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_battery_discharged', + 'unique_id': '1234_lifetime_battery_discharged', + 'unit_of_measurement': , + }), + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_1234_lifetime_battery_energy_charged', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 3, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime battery energy charged', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_battery_charged', + 'unique_id': '1234_lifetime_battery_charged', + 'unit_of_measurement': , + }), + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_1234_current_battery_discharge', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 3, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': 'mdi:flash', + 'original_name': 'Current battery discharge', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'battery_discharge', + 'unique_id': '1234_battery_discharge', + 'unit_of_measurement': , + }), + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': , + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_1234_voltage_storage_ct', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': 'mdi:flash', + 'original_name': 'Voltage storage CT', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'storage_ct_voltage', + 'unique_id': '1234_storage_voltage', + 'unit_of_measurement': , + }), + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + , + , + , + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': , + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_1234_metering_status_storage_ct', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:flash', + 'original_name': 'Metering status storage CT', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'storage_ct_metering_status', + 'unique_id': '1234_storage_ct_metering_status', + 'unit_of_measurement': None, + }), + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': , + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_1234_meter_status_flags_active_storage_ct', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:flash', + 'original_name': 'Meter status flags active storage CT', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'storage_ct_status_flags', + 'unique_id': '1234_storage_ct_status_flags', + 'unit_of_measurement': None, + }), + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': , + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_1234_lifetime_battery_energy_discharged_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime battery energy discharged l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_battery_discharged_phase', + 'unique_id': '1234_lifetime_battery_discharged_l1', + 'unit_of_measurement': , + }), + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': , + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_1234_lifetime_battery_energy_charged_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime battery energy charged l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_battery_charged_phase', + 'unique_id': '1234_lifetime_battery_charged_l1', + 'unit_of_measurement': , + }), + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': , + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_1234_current_battery_discharge_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': 'mdi:flash', + 'original_name': 'Current battery discharge l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'battery_discharge_phase', + 'unique_id': '1234_battery_discharge_l1', + 'unit_of_measurement': , + }), + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': , + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_1234_voltage_storage_ct_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': 'mdi:flash', + 'original_name': 'Voltage storage CT l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'storage_ct_voltage_phase', + 'unique_id': '1234_storage_voltage_l1', + 'unit_of_measurement': , + }), + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + , + , + , + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': , + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_1234_metering_status_storage_ct_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:flash', + 'original_name': 'Metering status storage CT l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'storage_ct_metering_status_phase', + 'unique_id': '1234_storage_ct_metering_status_l1', + 'unit_of_measurement': None, + }), + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': , + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_1234_meter_status_flags_active_storage_ct_l1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:flash', + 'original_name': 'Meter status flags active storage CT l1', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'storage_ct_status_flags_phase', + 'unique_id': '1234_storage_ct_status_flags_l1', + 'unit_of_measurement': None, + }), + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': , + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_1234_lifetime_battery_energy_discharged_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime battery energy discharged l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_battery_discharged_phase', + 'unique_id': '1234_lifetime_battery_discharged_l2', + 'unit_of_measurement': , + }), + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': , + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_1234_lifetime_battery_energy_charged_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime battery energy charged l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_battery_charged_phase', + 'unique_id': '1234_lifetime_battery_charged_l2', + 'unit_of_measurement': , + }), + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': , + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_1234_current_battery_discharge_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': 'mdi:flash', + 'original_name': 'Current battery discharge l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'battery_discharge_phase', + 'unique_id': '1234_battery_discharge_l2', + 'unit_of_measurement': , + }), + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': , + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_1234_voltage_storage_ct_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': 'mdi:flash', + 'original_name': 'Voltage storage CT l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'storage_ct_voltage_phase', + 'unique_id': '1234_storage_voltage_l2', + 'unit_of_measurement': , + }), + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + , + , + , + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': , + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_1234_metering_status_storage_ct_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:flash', + 'original_name': 'Metering status storage CT l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'storage_ct_metering_status_phase', + 'unique_id': '1234_storage_ct_metering_status_l2', + 'unit_of_measurement': None, + }), + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': , + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_1234_meter_status_flags_active_storage_ct_l2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:flash', + 'original_name': 'Meter status flags active storage CT l2', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'storage_ct_status_flags_phase', + 'unique_id': '1234_storage_ct_status_flags_l2', + 'unit_of_measurement': None, + }), + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': , + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_1234_lifetime_battery_energy_discharged_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime battery energy discharged l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_battery_discharged_phase', + 'unique_id': '1234_lifetime_battery_discharged_l3', + 'unit_of_measurement': , + }), + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': , + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_1234_lifetime_battery_energy_charged_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': 'mdi:flash', + 'original_name': 'Lifetime battery energy charged l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_battery_charged_phase', + 'unique_id': '1234_lifetime_battery_charged_l3', + 'unit_of_measurement': , + }), + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': , + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_1234_current_battery_discharge_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': 'mdi:flash', + 'original_name': 'Current battery discharge l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'battery_discharge_phase', + 'unique_id': '1234_battery_discharge_l3', + 'unit_of_measurement': , + }), + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': , + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_1234_voltage_storage_ct_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': 'mdi:flash', + 'original_name': 'Voltage storage CT l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'storage_ct_voltage_phase', + 'unique_id': '1234_storage_voltage_l3', + 'unit_of_measurement': , + }), + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + , + , + , + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': , + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_1234_metering_status_storage_ct_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:flash', + 'original_name': 'Metering status storage CT l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'storage_ct_metering_status_phase', + 'unique_id': '1234_storage_ct_metering_status_l3', + 'unit_of_measurement': None, + }), + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': , + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.envoy_1234_meter_status_flags_active_storage_ct_l3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:flash', + 'original_name': 'Meter status flags active storage CT l3', + 'platform': 'enphase_envoy', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'storage_ct_status_flags_phase', + 'unique_id': '1234_storage_ct_status_flags_l3', + 'unit_of_measurement': None, + }), EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2560,6 +3417,32 @@ }), ]) # --- +# name: test_sensor[sensor.envoy_1234_current_battery_discharge-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Envoy 1234 Current battery discharge', + 'icon': 'mdi:flash', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.envoy_1234_current_battery_discharge', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.103', + }) +# --- +# name: test_sensor[sensor.envoy_1234_current_battery_discharge_l1-state] + None +# --- +# name: test_sensor[sensor.envoy_1234_current_battery_discharge_l2-state] + None +# --- +# name: test_sensor[sensor.envoy_1234_current_battery_discharge_l3-state] + None +# --- # name: test_sensor[sensor.envoy_1234_current_net_power_consumption-state] StateSnapshot({ 'attributes': ReadOnlyDict({ @@ -2572,6 +3455,7 @@ 'context': , 'entity_id': 'sensor.envoy_1234_current_net_power_consumption', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0.101', }) @@ -2597,6 +3481,7 @@ 'context': , 'entity_id': 'sensor.envoy_1234_current_power_consumption', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '1.234', }) @@ -2613,6 +3498,7 @@ 'context': , 'entity_id': 'sensor.envoy_1234_current_power_consumption_l1', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '1.324', }) @@ -2629,6 +3515,7 @@ 'context': , 'entity_id': 'sensor.envoy_1234_current_power_consumption_l2', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '2.324', }) @@ -2645,6 +3532,7 @@ 'context': , 'entity_id': 'sensor.envoy_1234_current_power_consumption_l3', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '3.324', }) @@ -2661,6 +3549,7 @@ 'context': , 'entity_id': 'sensor.envoy_1234_current_power_production', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '1.234', }) @@ -2677,6 +3566,7 @@ 'context': , 'entity_id': 'sensor.envoy_1234_current_power_production_l1', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '1.234', }) @@ -2693,6 +3583,7 @@ 'context': , 'entity_id': 'sensor.envoy_1234_current_power_production_l2', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '2.234', }) @@ -2709,6 +3600,7 @@ 'context': , 'entity_id': 'sensor.envoy_1234_current_power_production_l3', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '3.234', }) @@ -2724,6 +3616,7 @@ 'context': , 'entity_id': 'sensor.envoy_1234_energy_consumption_last_seven_days', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '1.234', }) @@ -2739,6 +3632,7 @@ 'context': , 'entity_id': 'sensor.envoy_1234_energy_consumption_last_seven_days_l1', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '1.321', }) @@ -2754,6 +3648,7 @@ 'context': , 'entity_id': 'sensor.envoy_1234_energy_consumption_last_seven_days_l2', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '2.321', }) @@ -2769,6 +3664,7 @@ 'context': , 'entity_id': 'sensor.envoy_1234_energy_consumption_last_seven_days_l3', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '3.321', }) @@ -2785,6 +3681,7 @@ 'context': , 'entity_id': 'sensor.envoy_1234_energy_consumption_today', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '1.234', }) @@ -2801,6 +3698,7 @@ 'context': , 'entity_id': 'sensor.envoy_1234_energy_consumption_today_l1', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '1.323', }) @@ -2817,6 +3715,7 @@ 'context': , 'entity_id': 'sensor.envoy_1234_energy_consumption_today_l2', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '2.323', }) @@ -2833,6 +3732,7 @@ 'context': , 'entity_id': 'sensor.envoy_1234_energy_consumption_today_l3', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '3.323', }) @@ -2848,6 +3748,7 @@ 'context': , 'entity_id': 'sensor.envoy_1234_energy_production_last_seven_days', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '1.234', }) @@ -2863,6 +3764,7 @@ 'context': , 'entity_id': 'sensor.envoy_1234_energy_production_last_seven_days_l1', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '1.231', }) @@ -2878,6 +3780,7 @@ 'context': , 'entity_id': 'sensor.envoy_1234_energy_production_last_seven_days_l2', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '2.231', }) @@ -2893,6 +3796,7 @@ 'context': , 'entity_id': 'sensor.envoy_1234_energy_production_last_seven_days_l3', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '3.231', }) @@ -2909,6 +3813,7 @@ 'context': , 'entity_id': 'sensor.envoy_1234_energy_production_today', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '1.234', }) @@ -2925,6 +3830,7 @@ 'context': , 'entity_id': 'sensor.envoy_1234_energy_production_today_l1', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '1.233', }) @@ -2941,6 +3847,7 @@ 'context': , 'entity_id': 'sensor.envoy_1234_energy_production_today_l2', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '2.233', }) @@ -2957,6 +3864,7 @@ 'context': , 'entity_id': 'sensor.envoy_1234_energy_production_today_l3', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '3.233', }) @@ -2973,6 +3881,58 @@ # name: test_sensor[sensor.envoy_1234_frequency_net_consumption_ct_l3-state] None # --- +# name: test_sensor[sensor.envoy_1234_lifetime_battery_energy_charged-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Envoy 1234 Lifetime battery energy charged', + 'icon': 'mdi:flash', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.envoy_1234_lifetime_battery_energy_charged', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.032345', + }) +# --- +# name: test_sensor[sensor.envoy_1234_lifetime_battery_energy_charged_l1-state] + None +# --- +# name: test_sensor[sensor.envoy_1234_lifetime_battery_energy_charged_l2-state] + None +# --- +# name: test_sensor[sensor.envoy_1234_lifetime_battery_energy_charged_l3-state] + None +# --- +# name: test_sensor[sensor.envoy_1234_lifetime_battery_energy_discharged-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Envoy 1234 Lifetime battery energy discharged', + 'icon': 'mdi:flash', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.envoy_1234_lifetime_battery_energy_discharged', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.031234', + }) +# --- +# name: test_sensor[sensor.envoy_1234_lifetime_battery_energy_discharged_l1-state] + None +# --- +# name: test_sensor[sensor.envoy_1234_lifetime_battery_energy_discharged_l2-state] + None +# --- +# name: test_sensor[sensor.envoy_1234_lifetime_battery_energy_discharged_l3-state] + None +# --- # name: test_sensor[sensor.envoy_1234_lifetime_energy_consumption-state] StateSnapshot({ 'attributes': ReadOnlyDict({ @@ -2985,6 +3945,7 @@ 'context': , 'entity_id': 'sensor.envoy_1234_lifetime_energy_consumption', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0.001234', }) @@ -3001,6 +3962,7 @@ 'context': , 'entity_id': 'sensor.envoy_1234_lifetime_energy_consumption_l1', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0.001322', }) @@ -3017,6 +3979,7 @@ 'context': , 'entity_id': 'sensor.envoy_1234_lifetime_energy_consumption_l2', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0.002322', }) @@ -3033,6 +3996,7 @@ 'context': , 'entity_id': 'sensor.envoy_1234_lifetime_energy_consumption_l3', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0.003322', }) @@ -3049,6 +4013,7 @@ 'context': , 'entity_id': 'sensor.envoy_1234_lifetime_energy_production', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0.001234', }) @@ -3065,6 +4030,7 @@ 'context': , 'entity_id': 'sensor.envoy_1234_lifetime_energy_production_l1', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0.001232', }) @@ -3081,6 +4047,7 @@ 'context': , 'entity_id': 'sensor.envoy_1234_lifetime_energy_production_l2', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0.002232', }) @@ -3097,6 +4064,7 @@ 'context': , 'entity_id': 'sensor.envoy_1234_lifetime_energy_production_l3', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0.003232', }) @@ -3113,6 +4081,7 @@ 'context': , 'entity_id': 'sensor.envoy_1234_lifetime_net_energy_consumption', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0.021234', }) @@ -3138,6 +4107,7 @@ 'context': , 'entity_id': 'sensor.envoy_1234_lifetime_net_energy_production', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0.022345', }) @@ -3175,6 +4145,18 @@ # name: test_sensor[sensor.envoy_1234_meter_status_flags_active_production_ct_l3-state] None # --- +# name: test_sensor[sensor.envoy_1234_meter_status_flags_active_storage_ct-state] + None +# --- +# name: test_sensor[sensor.envoy_1234_meter_status_flags_active_storage_ct_l1-state] + None +# --- +# name: test_sensor[sensor.envoy_1234_meter_status_flags_active_storage_ct_l2-state] + None +# --- +# name: test_sensor[sensor.envoy_1234_meter_status_flags_active_storage_ct_l3-state] + None +# --- # name: test_sensor[sensor.envoy_1234_metering_status_net_consumption_ct-state] None # --- @@ -3202,6 +4184,18 @@ # name: test_sensor[sensor.envoy_1234_metering_status_production_ct_l3-state] None # --- +# name: test_sensor[sensor.envoy_1234_metering_status_storage_ct-state] + None +# --- +# name: test_sensor[sensor.envoy_1234_metering_status_storage_ct_l1-state] + None +# --- +# name: test_sensor[sensor.envoy_1234_metering_status_storage_ct_l2-state] + None +# --- +# name: test_sensor[sensor.envoy_1234_metering_status_storage_ct_l3-state] + None +# --- # name: test_sensor[sensor.envoy_1234_voltage_net_consumption_ct-state] None # --- @@ -3214,6 +4208,18 @@ # name: test_sensor[sensor.envoy_1234_voltage_net_consumption_ct_l3-state] None # --- +# name: test_sensor[sensor.envoy_1234_voltage_storage_ct-state] + None +# --- +# name: test_sensor[sensor.envoy_1234_voltage_storage_ct_l1-state] + None +# --- +# name: test_sensor[sensor.envoy_1234_voltage_storage_ct_l2-state] + None +# --- +# name: test_sensor[sensor.envoy_1234_voltage_storage_ct_l3-state] + None +# --- # name: test_sensor[sensor.inverter_1-state] StateSnapshot({ 'attributes': ReadOnlyDict({ @@ -3226,6 +4232,7 @@ 'context': , 'entity_id': 'sensor.inverter_1', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '1', }) diff --git a/tests/components/enphase_envoy/test_config_flow.py b/tests/components/enphase_envoy/test_config_flow.py index 6e274d0f7cc..9000cf92e0e 100644 --- a/tests/components/enphase_envoy/test_config_flow.py +++ b/tests/components/enphase_envoy/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Enphase Envoy config flow.""" + from ipaddress import ip_address from unittest.mock import AsyncMock @@ -28,6 +29,7 @@ async def test_form(hass: HomeAssistant, config, setup_enphase_envoy) -> None: "password": "test-password", }, ) + await hass.async_block_till_done() assert result2["type"] == "create_entry" assert result2["title"] == "Envoy 1234" assert result2["data"] == { @@ -57,6 +59,7 @@ async def test_user_no_serial_number( "password": "test-password", }, ) + await hass.async_block_till_done() assert result2["type"] == "create_entry" assert result2["title"] == "Envoy" assert result2["data"] == { @@ -86,6 +89,7 @@ async def test_user_fetching_serial_fails( "password": "test-password", }, ) + await hass.async_block_till_done() assert result2["type"] == "create_entry" assert result2["title"] == "Envoy" assert result2["data"] == { @@ -115,6 +119,7 @@ async def test_form_invalid_auth(hass: HomeAssistant, setup_enphase_envoy) -> No "password": "test-password", }, ) + await hass.async_block_till_done() assert result2["type"] == "form" assert result2["errors"] == {"base": "invalid_auth"} @@ -136,6 +141,7 @@ async def test_form_cannot_connect(hass: HomeAssistant, setup_enphase_envoy) -> "password": "test-password", }, ) + await hass.async_block_till_done() assert result2["type"] == "form" assert result2["errors"] == {"base": "cannot_connect"} @@ -157,6 +163,7 @@ async def test_form_unknown_error(hass: HomeAssistant, setup_enphase_envoy) -> N "password": "test-password", }, ) + await hass.async_block_till_done() assert result2["type"] == "form" assert result2["errors"] == {"base": "unknown"} @@ -199,6 +206,7 @@ async def test_zeroconf_pre_token_firmware( "password": "test-password", }, ) + await hass.async_block_till_done() assert result2["type"] == "create_entry" assert result2["title"] == "Envoy 1234" assert result2["result"].unique_id == "1234" @@ -239,6 +247,7 @@ async def test_zeroconf_token_firmware( "password": "test-password", }, ) + await hass.async_block_till_done() assert result2["type"] == "create_entry" assert result2["title"] == "Envoy 1234" assert result2["result"].unique_id == "1234" @@ -330,6 +339,7 @@ async def test_zeroconf_serial_already_exists( type="mock_type", ), ) + await hass.async_block_till_done() assert result["type"] == "abort" assert result["reason"] == "already_configured" @@ -353,6 +363,7 @@ async def test_zeroconf_serial_already_exists_ignores_ipv6( type="mock_type", ), ) + await hass.async_block_till_done() assert result["type"] == "abort" assert result["reason"] == "not_ipv4_address" @@ -377,6 +388,7 @@ async def test_zeroconf_host_already_exists( type="mock_type", ), ) + await hass.async_block_till_done() assert result["type"] == "abort" assert result["reason"] == "already_configured" @@ -401,6 +413,7 @@ async def test_reauth(hass: HomeAssistant, config_entry, setup_enphase_envoy) -> "password": "test-password", }, ) + await hass.async_block_till_done() assert result2["type"] == "abort" assert result2["reason"] == "reauth_successful" diff --git a/tests/components/enphase_envoy/test_diagnostics.py b/tests/components/enphase_envoy/test_diagnostics.py index c3659b2a9bb..a3b4f8e0f3c 100644 --- a/tests/components/enphase_envoy/test_diagnostics.py +++ b/tests/components/enphase_envoy/test_diagnostics.py @@ -1,21 +1,37 @@ """Test Enphase Envoy diagnostics.""" + from syrupy import SnapshotAssertion +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from tests.components.diagnostics import get_diagnostics_for_config_entry from tests.typing import ClientSessionGenerator +# Fields to exclude from snapshot as they change each run +TO_EXCLUDE = { + "id", + "device_id", + "via_device_id", + "last_updated", + "last_changed", + "last_reported", +} + + +def limit_diagnostic_attrs(prop, path) -> bool: + """Mark attributes to exclude from diagnostic snapshot.""" + return prop in TO_EXCLUDE + async def test_entry_diagnostics( hass: HomeAssistant, - config_entry, + config_entry: ConfigEntry, hass_client: ClientSessionGenerator, setup_enphase_envoy, snapshot: SnapshotAssertion, ) -> None: """Test config entry diagnostics.""" - assert ( - await get_diagnostics_for_config_entry(hass, hass_client, config_entry) - == snapshot - ) + assert await get_diagnostics_for_config_entry( + hass, hass_client, config_entry + ) == snapshot(exclude=limit_diagnostic_attrs) diff --git a/tests/components/enphase_envoy/test_sensor.py b/tests/components/enphase_envoy/test_sensor.py index 424ce2cecdd..3d6a0ec5757 100644 --- a/tests/components/enphase_envoy/test_sensor.py +++ b/tests/components/enphase_envoy/test_sensor.py @@ -1,4 +1,5 @@ """Test Enphase Envoy sensors.""" + from unittest.mock import patch import pytest @@ -16,15 +17,19 @@ from tests.common import MockConfigEntry @pytest.fixture(name="setup_enphase_envoy_sensor") async def setup_enphase_envoy_sensor_fixture(hass, config, mock_envoy): """Define a fixture to set up Enphase Envoy with sensor platform only.""" - with patch( - "homeassistant.components.enphase_envoy.config_flow.Envoy", - return_value=mock_envoy, - ), patch( - "homeassistant.components.enphase_envoy.Envoy", - return_value=mock_envoy, - ), patch( - "homeassistant.components.enphase_envoy.PLATFORMS", - [Platform.SENSOR], + with ( + patch( + "homeassistant.components.enphase_envoy.config_flow.Envoy", + return_value=mock_envoy, + ), + patch( + "homeassistant.components.enphase_envoy.Envoy", + return_value=mock_envoy, + ), + patch( + "homeassistant.components.enphase_envoy.PLATFORMS", + [Platform.SENSOR], + ), ): assert await async_setup_component(hass, DOMAIN, config) await hass.async_block_till_done() diff --git a/tests/components/environment_canada/test_config_flow.py b/tests/components/environment_canada/test_config_flow.py index b745ac02693..e9513644947 100644 --- a/tests/components/environment_canada/test_config_flow.py +++ b/tests/components/environment_canada/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Environment Canada (EC) config flow.""" + from unittest.mock import AsyncMock, MagicMock, Mock, patch import xml.etree.ElementTree as et @@ -50,9 +51,12 @@ def mocked_ec( async def test_create_entry(hass: HomeAssistant) -> None: """Test creating an entry.""" - with mocked_ec(), patch( - "homeassistant.components.environment_canada.async_setup_entry", - return_value=True, + with ( + mocked_ec(), + patch( + "homeassistant.components.environment_canada.async_setup_entry", + return_value=True, + ), ): flow = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -75,9 +79,12 @@ async def test_create_same_entry_twice(hass: HomeAssistant) -> None: ) entry.add_to_hass(hass) - with mocked_ec(), patch( - "homeassistant.components.environment_canada.async_setup_entry", - return_value=True, + with ( + mocked_ec(), + patch( + "homeassistant.components.environment_canada.async_setup_entry", + return_value=True, + ), ): flow = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -121,9 +128,12 @@ async def test_exception_handling(hass: HomeAssistant, error) -> None: async def test_lat_lon_not_specified(hass: HomeAssistant) -> None: """Test that the import step works when coordinates are not specified.""" - with mocked_ec(), patch( - "homeassistant.components.environment_canada.async_setup_entry", - return_value=True, + with ( + mocked_ec(), + patch( + "homeassistant.components.environment_canada.async_setup_entry", + return_value=True, + ), ): fake_config = dict(FAKE_CONFIG) del fake_config[CONF_LATITUDE] diff --git a/tests/components/environment_canada/test_diagnostics.py b/tests/components/environment_canada/test_diagnostics.py index fb1597e3622..8f800111d39 100644 --- a/tests/components/environment_canada/test_diagnostics.py +++ b/tests/components/environment_canada/test_diagnostics.py @@ -1,4 +1,5 @@ """Test Environment Canada diagnostics.""" + from datetime import UTC, datetime import json from unittest.mock import AsyncMock, MagicMock, patch @@ -51,17 +52,23 @@ async def init_integration(hass: HomeAssistant) -> MockConfigEntry: radar_mock.image = b"GIF..." radar_mock.timestamp = datetime(2022, 10, 4, tzinfo=UTC) - with patch( - "homeassistant.components.environment_canada.ECWeather", - return_value=weather_mock, - ), patch( - "homeassistant.components.environment_canada.ECAirQuality", - return_value=mock_ec(), - ), patch( - "homeassistant.components.environment_canada.ECRadar", return_value=radar_mock - ), patch( - "homeassistant.components.environment_canada.config_flow.ECWeather", - return_value=weather_mock, + with ( + patch( + "homeassistant.components.environment_canada.ECWeather", + return_value=weather_mock, + ), + patch( + "homeassistant.components.environment_canada.ECAirQuality", + return_value=mock_ec(), + ), + patch( + "homeassistant.components.environment_canada.ECRadar", + return_value=radar_mock, + ), + patch( + "homeassistant.components.environment_canada.config_flow.ECWeather", + return_value=weather_mock, + ), ): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/epion/conftest.py b/tests/components/epion/conftest.py index 2290d0d4c8f..51fed636ca1 100644 --- a/tests/components/epion/conftest.py +++ b/tests/components/epion/conftest.py @@ -14,12 +14,15 @@ def mock_epion(): "epion/get_current_one_device.json" ) mock_epion_api = MagicMock() - with patch( - "homeassistant.components.epion.config_flow.Epion", - return_value=mock_epion_api, - ) as mock_epion_api, patch( - "homeassistant.components.epion.Epion", - return_value=mock_epion_api, + with ( + patch( + "homeassistant.components.epion.config_flow.Epion", + return_value=mock_epion_api, + ) as mock_epion_api, + patch( + "homeassistant.components.epion.Epion", + return_value=mock_epion_api, + ), ): mock_epion_api.return_value.get_current.return_value = current_one_device_data yield mock_epion_api diff --git a/tests/components/epion/test_config_flow.py b/tests/components/epion/test_config_flow.py index d7329f64324..8d246ac4dd4 100644 --- a/tests/components/epion/test_config_flow.py +++ b/tests/components/epion/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for the Epion config flow.""" + from unittest.mock import MagicMock, patch from epion import EpionAuthenticationError, EpionConnectionError diff --git a/tests/components/epson/test_config_flow.py b/tests/components/epson/test_config_flow.py index be0267a4af8..c6ca921df0f 100644 --- a/tests/components/epson/test_config_flow.py +++ b/tests/components/epson/test_config_flow.py @@ -1,4 +1,5 @@ """Test the epson config flow.""" + from unittest.mock import patch from epson_projector.const import PWR_OFF_STATE @@ -19,16 +20,20 @@ async def test_form(hass: HomeAssistant) -> None: assert result["type"] == "form" assert result["errors"] == {} assert result["step_id"] == config_entries.SOURCE_USER - with patch( - "homeassistant.components.epson.Projector.get_power", - return_value="01", - ), patch( - "homeassistant.components.epson.Projector.get_serial_number", - return_value="12345", - ), patch( - "homeassistant.components.epson.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.epson.Projector.get_power", + return_value="01", + ), + patch( + "homeassistant.components.epson.Projector.get_serial_number", + return_value="12345", + ), + patch( + "homeassistant.components.epson.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_HOST: "1.1.1.1", CONF_NAME: "test-epson"}, diff --git a/tests/components/epson/test_media_player.py b/tests/components/epson/test_media_player.py index 874a12173d6..000071054f1 100644 --- a/tests/components/epson/test_media_player.py +++ b/tests/components/epson/test_media_player.py @@ -1,4 +1,5 @@ """Tests for the epson integration.""" + from datetime import timedelta from unittest.mock import patch @@ -33,12 +34,15 @@ async def test_set_unique_id( entity_entry = entity_registry.async_get("media_player.epson") assert entity_entry assert entity_entry.unique_id == entry.entry_id - with patch( - "homeassistant.components.epson.Projector.get_power", return_value="01" - ), patch( - "homeassistant.components.epson.Projector.get_serial_number", return_value="123" - ), patch( - "homeassistant.components.epson.Projector.get_property", + with ( + patch("homeassistant.components.epson.Projector.get_power", return_value="01"), + patch( + "homeassistant.components.epson.Projector.get_serial_number", + return_value="123", + ), + patch( + "homeassistant.components.epson.Projector.get_property", + ), ): freezer.tick(timedelta(seconds=30)) async_fire_time_changed(hass) diff --git a/tests/components/escea/test_config_flow.py b/tests/components/escea/test_config_flow.py index 0bb128f717e..7d467fc50a0 100644 --- a/tests/components/escea/test_config_flow.py +++ b/tests/components/escea/test_config_flow.py @@ -47,10 +47,11 @@ async def test_not_found( ) -> None: """Test not finding any Escea controllers.""" - with patch( - "homeassistant.components.escea.discovery.pescea_discovery_service" - ) as discovery_service, patch( - "homeassistant.components.escea.config_flow.TIMEOUT_DISCOVERY", 0 + with ( + patch( + "homeassistant.components.escea.discovery.pescea_discovery_service" + ) as discovery_service, + patch("homeassistant.components.escea.config_flow.TIMEOUT_DISCOVERY", 0), ): discovery_service.return_value = mock_discovery_service @@ -75,12 +76,15 @@ async def test_found( """Test finding an Escea controller.""" mock_discovery_service.controllers["test-uid"] = mock_controller - with patch( - "homeassistant.components.escea.async_setup_entry", - return_value=True, - ) as mock_setup, patch( - "homeassistant.components.escea.discovery.pescea_discovery_service" - ) as discovery_service: + with ( + patch( + "homeassistant.components.escea.async_setup_entry", + return_value=True, + ) as mock_setup, + patch( + "homeassistant.components.escea.discovery.pescea_discovery_service" + ) as discovery_service, + ): discovery_service.return_value = mock_discovery_service mock_discovery_service.start_discovery.side_effect = _mock_start_discovery( discovery_service, mock_controller diff --git a/tests/components/esphome/bluetooth/test_client.py b/tests/components/esphome/bluetooth/test_client.py index cd250bc1080..98993be37d0 100644 --- a/tests/components/esphome/bluetooth/test_client.py +++ b/tests/components/esphome/bluetooth/test_client.py @@ -1,4 +1,5 @@ """Tests for ESPHomeClient.""" + from __future__ import annotations from aioesphomeapi import APIClient, APIVersion, BluetoothProxyFeature, DeviceInfo @@ -54,4 +55,4 @@ async def test_client_usage_while_not_connected(client_data: ESPHomeClientData) with pytest.raises( BleakError, match=f"{ESP_NAME}.*{ESP_MAC_ADDRESS}.*not connected" ): - await client.write_gatt_char("test", b"test") is False + assert await client.write_gatt_char("test", b"test") is False diff --git a/tests/components/esphome/conftest.py b/tests/components/esphome/conftest.py index 86eb6ef3d77..e51fc663b59 100644 --- a/tests/components/esphome/conftest.py +++ b/tests/components/esphome/conftest.py @@ -1,4 +1,5 @@ """esphome session fixtures.""" + from __future__ import annotations import asyncio @@ -151,11 +152,13 @@ def mock_client(mock_device_info) -> APIClient: mock_client.address = "127.0.0.1" mock_client.api_version = APIVersion(99, 99) - with patch( - "homeassistant.components.esphome.manager.ReconnectLogic", - BaseMockReconnectLogic, - ), patch("homeassistant.components.esphome.APIClient", mock_client), patch( - "homeassistant.components.esphome.config_flow.APIClient", mock_client + with ( + patch( + "homeassistant.components.esphome.manager.ReconnectLogic", + BaseMockReconnectLogic, + ), + patch("homeassistant.components.esphome.APIClient", mock_client), + patch("homeassistant.components.esphome.config_flow.APIClient", mock_client), ): yield mock_client diff --git a/tests/components/esphome/test_alarm_control_panel.py b/tests/components/esphome/test_alarm_control_panel.py index e7409bdfae4..af717ac1b49 100644 --- a/tests/components/esphome/test_alarm_control_panel.py +++ b/tests/components/esphome/test_alarm_control_panel.py @@ -1,4 +1,5 @@ """Test ESPHome alarm_control_panels.""" + from unittest.mock import call from aioesphomeapi import ( diff --git a/tests/components/esphome/test_binary_sensor.py b/tests/components/esphome/test_binary_sensor.py index 209ea344328..3da8a54ff34 100644 --- a/tests/components/esphome/test_binary_sensor.py +++ b/tests/components/esphome/test_binary_sensor.py @@ -1,4 +1,5 @@ """Test ESPHome binary sensors.""" + from collections.abc import Awaitable, Callable from aioesphomeapi import ( @@ -44,7 +45,7 @@ async def test_assist_in_progress( @pytest.mark.parametrize( - "binary_state", ((True, STATE_ON), (False, STATE_OFF), (None, STATE_UNKNOWN)) + "binary_state", [(True, STATE_ON), (False, STATE_OFF), (None, STATE_UNKNOWN)] ) async def test_binary_sensor_generic_entity( hass: HomeAssistant, diff --git a/tests/components/esphome/test_button.py b/tests/components/esphome/test_button.py index 71406341175..8c120949caa 100644 --- a/tests/components/esphome/test_button.py +++ b/tests/components/esphome/test_button.py @@ -1,6 +1,5 @@ """Test ESPHome buttones.""" - from unittest.mock import call from aioesphomeapi import APIClient, ButtonInfo diff --git a/tests/components/esphome/test_camera.py b/tests/components/esphome/test_camera.py index 682ce369b57..c6a61cd18e8 100644 --- a/tests/components/esphome/test_camera.py +++ b/tests/components/esphome/test_camera.py @@ -1,4 +1,5 @@ """Test ESPHome cameras.""" + from collections.abc import Awaitable, Callable from aioesphomeapi import ( diff --git a/tests/components/esphome/test_climate.py b/tests/components/esphome/test_climate.py index dbdee826137..4ec7fee6447 100644 --- a/tests/components/esphome/test_climate.py +++ b/tests/components/esphome/test_climate.py @@ -1,6 +1,5 @@ """Test ESPHome climates.""" - import math from unittest.mock import call diff --git a/tests/components/esphome/test_config_flow.py b/tests/components/esphome/test_config_flow.py index 9b07f9b4ee0..e06b96356ae 100644 --- a/tests/components/esphome/test_config_flow.py +++ b/tests/components/esphome/test_config_flow.py @@ -1,4 +1,5 @@ """Test config flow.""" + from ipaddress import ip_address import json from unittest.mock import AsyncMock, MagicMock, patch diff --git a/tests/components/esphome/test_dashboard.py b/tests/components/esphome/test_dashboard.py index 11db8a73731..51b9b535caa 100644 --- a/tests/components/esphome/test_dashboard.py +++ b/tests/components/esphome/test_dashboard.py @@ -1,4 +1,5 @@ """Test ESPHome dashboard features.""" + from unittest.mock import patch from aioesphomeapi import DeviceInfo, InvalidAuthAPIError @@ -97,11 +98,14 @@ async def test_setup_dashboard_fails_when_already_setup( await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() - with patch.object( - dashboard.ESPHomeDashboardAPI, "get_devices", side_effect=TimeoutError - ) as mock_get_devices, patch( - "homeassistant.components.esphome.async_setup_entry", return_value=True - ) as mock_setup: + with ( + patch.object( + dashboard.ESPHomeDashboardAPI, "get_devices", side_effect=TimeoutError + ) as mock_get_devices, + patch( + "homeassistant.components.esphome.async_setup_entry", return_value=True + ) as mock_setup, + ): await dashboard.async_set_dashboard_info(hass, "test-slug", "test-host", 6052) await hass.async_block_till_done() @@ -165,12 +169,15 @@ async def test_new_dashboard_fix_reauth( await dashboard.async_get_dashboard(hass).async_refresh() - with patch( - "homeassistant.components.esphome.dashboard.ESPHomeDashboardAPI.get_encryption_key", - return_value=VALID_NOISE_PSK, - ) as mock_get_encryption_key, patch( - "homeassistant.components.esphome.async_setup_entry", return_value=True - ) as mock_setup: + with ( + patch( + "homeassistant.components.esphome.dashboard.ESPHomeDashboardAPI.get_encryption_key", + return_value=VALID_NOISE_PSK, + ) as mock_get_encryption_key, + patch( + "homeassistant.components.esphome.async_setup_entry", return_value=True + ) as mock_setup, + ): await dashboard.async_set_dashboard_info(hass, "test-slug", "test-host", 6052) await hass.async_block_till_done() diff --git a/tests/components/esphome/test_date.py b/tests/components/esphome/test_date.py new file mode 100644 index 00000000000..2deb92775fb --- /dev/null +++ b/tests/components/esphome/test_date.py @@ -0,0 +1,76 @@ +"""Test ESPHome dates.""" + +from unittest.mock import call + +from aioesphomeapi import APIClient, DateInfo, DateState + +from homeassistant.components.date import ( + ATTR_DATE, + DOMAIN as DATE_DOMAIN, + SERVICE_SET_VALUE, +) +from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN +from homeassistant.core import HomeAssistant + + +async def test_generic_date_entity( + hass: HomeAssistant, + mock_client: APIClient, + mock_generic_device_entry, +) -> None: + """Test a generic date entity.""" + entity_info = [ + DateInfo( + object_id="mydate", + key=1, + name="my date", + unique_id="my_date", + ) + ] + states = [DateState(key=1, year=2024, month=12, day=31)] + user_service = [] + await mock_generic_device_entry( + mock_client=mock_client, + entity_info=entity_info, + user_service=user_service, + states=states, + ) + state = hass.states.get("date.test_mydate") + assert state is not None + assert state.state == "2024-12-31" + + await hass.services.async_call( + DATE_DOMAIN, + SERVICE_SET_VALUE, + {ATTR_ENTITY_ID: "date.test_mydate", ATTR_DATE: "1999-01-01"}, + blocking=True, + ) + mock_client.date_command.assert_has_calls([call(1, 1999, 1, 1)]) + mock_client.date_command.reset_mock() + + +async def test_generic_date_missing_state( + hass: HomeAssistant, + mock_client: APIClient, + mock_generic_device_entry, +) -> None: + """Test a generic date entity with missing state.""" + entity_info = [ + DateInfo( + object_id="mydate", + key=1, + name="my date", + unique_id="my_date", + ) + ] + states = [DateState(key=1, missing_state=True)] + user_service = [] + await mock_generic_device_entry( + mock_client=mock_client, + entity_info=entity_info, + user_service=user_service, + states=states, + ) + state = hass.states.get("date.test_mydate") + assert state is not None + assert state.state == STATE_UNKNOWN diff --git a/tests/components/esphome/test_diagnostics.py b/tests/components/esphome/test_diagnostics.py index d528010af1b..0f2b18218ff 100644 --- a/tests/components/esphome/test_diagnostics.py +++ b/tests/components/esphome/test_diagnostics.py @@ -1,4 +1,5 @@ """Tests for the diagnostics data provided by the ESPHome integration.""" + from unittest.mock import ANY from syrupy import SnapshotAssertion diff --git a/tests/components/esphome/test_entity.py b/tests/components/esphome/test_entity.py index 609af788ceb..303d50f3103 100644 --- a/tests/components/esphome/test_entity.py +++ b/tests/components/esphome/test_entity.py @@ -1,4 +1,5 @@ """Test ESPHome binary sensors.""" + import asyncio from collections.abc import Awaitable, Callable from typing import Any @@ -22,13 +23,12 @@ from homeassistant.const import ( STATE_ON, STATE_UNAVAILABLE, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import Event, HomeAssistant, callback from homeassistant.helpers import entity_registry as er from homeassistant.helpers.event import ( EventStateChangedData, async_track_state_change_event, ) -from homeassistant.helpers.typing import EventType from .conftest import MockESPHomeDevice @@ -224,7 +224,7 @@ async def test_entities_removed_after_reload( on_future = hass.loop.create_future() @callback - def _async_wait_for_on(event: EventType[EventStateChangedData]) -> None: + def _async_wait_for_on(event: Event[EventStateChangedData]) -> None: if event.data["new_state"].state == STATE_ON: on_future.set_result(None) diff --git a/tests/components/esphome/test_fan.py b/tests/components/esphome/test_fan.py index 6f383dcb6ba..064b37b1ec1 100644 --- a/tests/components/esphome/test_fan.py +++ b/tests/components/esphome/test_fan.py @@ -1,6 +1,5 @@ """Test ESPHome fans.""" - from unittest.mock import call from aioesphomeapi import ( diff --git a/tests/components/esphome/test_init.py b/tests/components/esphome/test_init.py index 8e7e228e422..7e008cde212 100644 --- a/tests/components/esphome/test_init.py +++ b/tests/components/esphome/test_init.py @@ -1,6 +1,5 @@ """ESPHome set up tests.""" - from homeassistant.components.esphome import DOMAIN from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT from homeassistant.core import HomeAssistant diff --git a/tests/components/esphome/test_light.py b/tests/components/esphome/test_light.py index fc63508a836..2324c73b16f 100644 --- a/tests/components/esphome/test_light.py +++ b/tests/components/esphome/test_light.py @@ -1,6 +1,5 @@ """Test ESPHome lights.""" - from unittest.mock import call from aioesphomeapi import ( diff --git a/tests/components/esphome/test_lock.py b/tests/components/esphome/test_lock.py index 83312c85934..82c24b59a2c 100644 --- a/tests/components/esphome/test_lock.py +++ b/tests/components/esphome/test_lock.py @@ -1,6 +1,5 @@ """Test ESPHome locks.""" - from unittest.mock import call from aioesphomeapi import APIClient, LockCommand, LockEntityState, LockInfo, LockState diff --git a/tests/components/esphome/test_manager.py b/tests/components/esphome/test_manager.py index 424ab7338aa..55369e54b53 100644 --- a/tests/components/esphome/test_manager.py +++ b/tests/components/esphome/test_manager.py @@ -1,4 +1,5 @@ """Test ESPHome manager.""" + import asyncio from collections.abc import Awaitable, Callable from unittest.mock import AsyncMock, call diff --git a/tests/components/esphome/test_number.py b/tests/components/esphome/test_number.py index dc90d1c1098..557425052f3 100644 --- a/tests/components/esphome/test_number.py +++ b/tests/components/esphome/test_number.py @@ -1,14 +1,16 @@ """Test ESPHome numbers.""" import math -from unittest.mock import call +from unittest.mock import Mock, call from aioesphomeapi import ( APIClient, + APIConnectionError, NumberInfo, NumberMode as ESPHomeNumberMode, NumberState, ) +import pytest from homeassistant.components.number import ( ATTR_VALUE, @@ -17,6 +19,7 @@ from homeassistant.components.number import ( ) from homeassistant.const import ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT, STATE_UNKNOWN from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError async def test_generic_number_entity( @@ -122,3 +125,42 @@ async def test_generic_number_with_unit_of_measurement_as_empty_string( assert state is not None assert state.state == "42" assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes + + +async def test_generic_number_entity_set_when_disconnected( + hass: HomeAssistant, + mock_client: APIClient, + mock_generic_device_entry, +) -> None: + """Test a generic number entity.""" + entity_info = [ + NumberInfo( + object_id="mynumber", + key=1, + name="my number", + unique_id="my_number", + max_value=100, + min_value=0, + step=1, + unit_of_measurement="%", + ) + ] + states = [NumberState(key=1, state=50)] + user_service = [] + await mock_generic_device_entry( + mock_client=mock_client, + entity_info=entity_info, + user_service=user_service, + states=states, + ) + + mock_client.number_command = Mock(side_effect=APIConnectionError("Not connected")) + + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + {ATTR_ENTITY_ID: "number.test_mynumber", ATTR_VALUE: 20}, + blocking=True, + ) + mock_client.number_command.reset_mock() diff --git a/tests/components/esphome/test_select.py b/tests/components/esphome/test_select.py index 528483d4290..a433b1b0ab0 100644 --- a/tests/components/esphome/test_select.py +++ b/tests/components/esphome/test_select.py @@ -1,6 +1,5 @@ """Test ESPHome selects.""" - from unittest.mock import call from aioesphomeapi import APIClient, SelectInfo, SelectState diff --git a/tests/components/esphome/test_sensor.py b/tests/components/esphome/test_sensor.py index a824a4e3905..9f8e45ed64d 100644 --- a/tests/components/esphome/test_sensor.py +++ b/tests/components/esphome/test_sensor.py @@ -1,4 +1,5 @@ """Test ESPHome sensors.""" + from collections.abc import Awaitable, Callable import logging import math diff --git a/tests/components/esphome/test_switch.py b/tests/components/esphome/test_switch.py index cd60eb70edd..561ac0b369f 100644 --- a/tests/components/esphome/test_switch.py +++ b/tests/components/esphome/test_switch.py @@ -1,6 +1,5 @@ """Test ESPHome switches.""" - from unittest.mock import call from aioesphomeapi import APIClient, SwitchInfo, SwitchState diff --git a/tests/components/esphome/test_time.py b/tests/components/esphome/test_time.py new file mode 100644 index 00000000000..aaa18c77a47 --- /dev/null +++ b/tests/components/esphome/test_time.py @@ -0,0 +1,76 @@ +"""Test ESPHome times.""" + +from unittest.mock import call + +from aioesphomeapi import APIClient, TimeInfo, TimeState + +from homeassistant.components.time import ( + ATTR_TIME, + DOMAIN as TIME_DOMAIN, + SERVICE_SET_VALUE, +) +from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN +from homeassistant.core import HomeAssistant + + +async def test_generic_time_entity( + hass: HomeAssistant, + mock_client: APIClient, + mock_generic_device_entry, +) -> None: + """Test a generic time entity.""" + entity_info = [ + TimeInfo( + object_id="mytime", + key=1, + name="my time", + unique_id="my_time", + ) + ] + states = [TimeState(key=1, hour=12, minute=34, second=56)] + user_service = [] + await mock_generic_device_entry( + mock_client=mock_client, + entity_info=entity_info, + user_service=user_service, + states=states, + ) + state = hass.states.get("time.test_mytime") + assert state is not None + assert state.state == "12:34:56" + + await hass.services.async_call( + TIME_DOMAIN, + SERVICE_SET_VALUE, + {ATTR_ENTITY_ID: "time.test_mytime", ATTR_TIME: "01:23:45"}, + blocking=True, + ) + mock_client.time_command.assert_has_calls([call(1, 1, 23, 45)]) + mock_client.time_command.reset_mock() + + +async def test_generic_time_missing_state( + hass: HomeAssistant, + mock_client: APIClient, + mock_generic_device_entry, +) -> None: + """Test a generic time entity with missing state.""" + entity_info = [ + TimeInfo( + object_id="mytime", + key=1, + name="my time", + unique_id="my_time", + ) + ] + states = [TimeState(key=1, missing_state=True)] + user_service = [] + await mock_generic_device_entry( + mock_client=mock_client, + entity_info=entity_info, + user_service=user_service, + states=states, + ) + state = hass.states.get("time.test_mytime") + assert state is not None + assert state.state == STATE_UNKNOWN diff --git a/tests/components/esphome/test_update.py b/tests/components/esphome/test_update.py index 842480d9433..959ad12876d 100644 --- a/tests/components/esphome/test_update.py +++ b/tests/components/esphome/test_update.py @@ -1,4 +1,5 @@ """Test ESPHome update entities.""" + from collections.abc import Awaitable, Callable import dataclasses from unittest.mock import Mock, patch @@ -100,13 +101,17 @@ async def test_update_entity( return # Compile failed, don't try to upload - with patch( - "esphome_dashboard_api.ESPHomeDashboardAPI.compile", return_value=False - ) as mock_compile, patch( - "esphome_dashboard_api.ESPHomeDashboardAPI.upload", return_value=True - ) as mock_upload, pytest.raises( - HomeAssistantError, - match="compiling", + with ( + patch( + "esphome_dashboard_api.ESPHomeDashboardAPI.compile", return_value=False + ) as mock_compile, + patch( + "esphome_dashboard_api.ESPHomeDashboardAPI.upload", return_value=True + ) as mock_upload, + pytest.raises( + HomeAssistantError, + match="compiling", + ), ): await hass.services.async_call( "update", @@ -121,13 +126,17 @@ async def test_update_entity( assert len(mock_upload.mock_calls) == 0 # Compile success, upload fails - with patch( - "esphome_dashboard_api.ESPHomeDashboardAPI.compile", return_value=True - ) as mock_compile, patch( - "esphome_dashboard_api.ESPHomeDashboardAPI.upload", return_value=False - ) as mock_upload, pytest.raises( - HomeAssistantError, - match="OTA", + with ( + patch( + "esphome_dashboard_api.ESPHomeDashboardAPI.compile", return_value=True + ) as mock_compile, + patch( + "esphome_dashboard_api.ESPHomeDashboardAPI.upload", return_value=False + ) as mock_upload, + pytest.raises( + HomeAssistantError, + match="OTA", + ), ): await hass.services.async_call( "update", @@ -143,11 +152,14 @@ async def test_update_entity( assert mock_upload.mock_calls[0][1][0] == "test.yaml" # Everything works - with patch( - "esphome_dashboard_api.ESPHomeDashboardAPI.compile", return_value=True - ) as mock_compile, patch( - "esphome_dashboard_api.ESPHomeDashboardAPI.upload", return_value=True - ) as mock_upload: + with ( + patch( + "esphome_dashboard_api.ESPHomeDashboardAPI.compile", return_value=True + ) as mock_compile, + patch( + "esphome_dashboard_api.ESPHomeDashboardAPI.upload", return_value=True + ) as mock_upload, + ): await hass.services.async_call( "update", "install", @@ -207,15 +219,25 @@ async def test_update_static_info( @pytest.mark.parametrize( - "expected_disconnect_state", [(True, STATE_ON), (False, STATE_UNAVAILABLE)] + ("expected_disconnect", "expected_state", "has_deep_sleep"), + [ + (True, STATE_ON, False), + (False, STATE_UNAVAILABLE, False), + (True, STATE_ON, True), + (False, STATE_ON, True), + ], ) async def test_update_device_state_for_availability( hass: HomeAssistant, - stub_reconnect, - expected_disconnect_state: tuple[bool, str], - mock_config_entry, - mock_device_info, + expected_disconnect: bool, + expected_state: str, + has_deep_sleep: bool, mock_dashboard, + mock_client: APIClient, + mock_esphome_device: Callable[ + [APIClient, list[EntityInfo], list[UserService], list[EntityState]], + Awaitable[MockESPHomeDevice], + ], ) -> None: """Test ESPHome update entity changes availability with the device.""" mock_dashboard["configured"] = [ @@ -225,46 +247,21 @@ async def test_update_device_state_for_availability( }, ] await async_get_dashboard(hass).async_refresh() - - signal_device_updated = f"esphome_{mock_config_entry.entry_id}_on_device_update" - runtime_data = Mock( - available=True, - expected_disconnect=False, - device_info=mock_device_info, - signal_device_updated=signal_device_updated, + mock_device = await mock_esphome_device( + mock_client=mock_client, + entity_info=[], + user_service=[], + states=[], + device_info={"has_deep_sleep": has_deep_sleep}, ) - with patch( - "homeassistant.components.esphome.update.DomainData.get_entry_data", - return_value=runtime_data, - ): - assert await hass.config_entries.async_forward_entry_setup( - mock_config_entry, "update" - ) - - state = hass.states.get("update.none_firmware") + state = hass.states.get("update.test_firmware") assert state is not None - assert state.state == "on" - - expected_disconnect, expected_state = expected_disconnect_state - - runtime_data.available = False - runtime_data.expected_disconnect = expected_disconnect - async_dispatcher_send(hass, signal_device_updated) - - state = hass.states.get("update.none_firmware") + assert state.state == STATE_ON + await mock_device.mock_disconnect(expected_disconnect) + state = hass.states.get("update.test_firmware") assert state.state == expected_state - # Deep sleep devices should still be available - runtime_data.device_info = dataclasses.replace( - runtime_data.device_info, has_deep_sleep=True - ) - - async_dispatcher_send(hass, signal_device_updated) - - state = hass.states.get("update.none_firmware") - assert state.state == "on" - async def test_update_entity_dashboard_not_available_startup( hass: HomeAssistant, @@ -274,12 +271,15 @@ async def test_update_entity_dashboard_not_available_startup( mock_dashboard, ) -> None: """Test ESPHome update entity when dashboard is not available at startup.""" - with patch( - "homeassistant.components.esphome.update.DomainData.get_entry_data", - return_value=Mock(available=True, device_info=mock_device_info), - ), patch( - "esphome_dashboard_api.ESPHomeDashboardAPI.get_devices", - side_effect=TimeoutError, + with ( + patch( + "homeassistant.components.esphome.update.DomainData.get_entry_data", + return_value=Mock(available=True, device_info=mock_device_info), + ), + patch( + "esphome_dashboard_api.ESPHomeDashboardAPI.get_devices", + side_effect=TimeoutError, + ), ): await async_get_dashboard(hass).async_refresh() assert await hass.config_entries.async_forward_entry_setup( diff --git a/tests/components/eufylife_ble/__init__.py b/tests/components/eufylife_ble/__init__.py index 7fbeed9d798..f18d868efe1 100644 --- a/tests/components/eufylife_ble/__init__.py +++ b/tests/components/eufylife_ble/__init__.py @@ -1,6 +1,5 @@ """Tests for the EufyLife integration.""" - from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo NOT_EUFYLIFE_SERVICE_INFO = BluetoothServiceInfo( diff --git a/tests/components/eufylife_ble/test_config_flow.py b/tests/components/eufylife_ble/test_config_flow.py index 477aa53c12d..c3590077d93 100644 --- a/tests/components/eufylife_ble/test_config_flow.py +++ b/tests/components/eufylife_ble/test_config_flow.py @@ -1,4 +1,5 @@ """Test the EufyLife config flow.""" + from unittest.mock import patch from homeassistant import config_entries diff --git a/tests/components/event/conftest.py b/tests/components/event/conftest.py new file mode 100644 index 00000000000..2b4a2dbba45 --- /dev/null +++ b/tests/components/event/conftest.py @@ -0,0 +1,53 @@ +"""Fixtures for the event entity component tests.""" + +import logging + +import pytest + +from homeassistant.components.event import DOMAIN, EventEntity +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType + +from .const import TEST_DOMAIN + +from tests.common import MockEntity, MockPlatform, mock_platform + +_LOGGER = logging.getLogger(__name__) + + +class MockEventEntity(MockEntity, EventEntity): + """Mock EventEntity class.""" + + @property + def event_types(self) -> list[str]: + """Return a list of possible events.""" + return self._handle("event_types") + + +@pytest.fixture +async def mock_event_platform(hass: HomeAssistant) -> None: + """Mock the event entity platform.""" + + async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, + ) -> None: + """Set up test event platform.""" + async_add_entities( + [ + MockEventEntity( + name="doorbell", + unique_id="unique_doorbell", + event_types=["short_press", "long_press"], + ), + ] + ) + + mock_platform( + hass, + f"{TEST_DOMAIN}.{DOMAIN}", + MockPlatform(async_setup_platform=async_setup_platform), + ) diff --git a/tests/components/event/const.py b/tests/components/event/const.py new file mode 100644 index 00000000000..323f32fa04b --- /dev/null +++ b/tests/components/event/const.py @@ -0,0 +1,3 @@ +"""Constants for the event entity component tests.""" + +TEST_DOMAIN = "test" diff --git a/tests/components/event/test_init.py b/tests/components/event/test_init.py index b8ba5fb6a18..fd3cf0eaf9b 100644 --- a/tests/components/event/test_init.py +++ b/tests/components/event/test_init.py @@ -1,4 +1,5 @@ """The tests for the event integration.""" + from collections.abc import Generator from typing import Any @@ -21,6 +22,8 @@ from homeassistant.helpers.restore_state import STORAGE_KEY as RESTORE_STATE_KEY from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util +from .const import TEST_DOMAIN + from tests.common import ( MockConfigEntry, MockModule, @@ -33,8 +36,6 @@ from tests.common import ( mock_restore_cache_with_extra_data, ) -TEST_DOMAIN = "test" - async def test_event() -> None: """Test the event entity.""" @@ -48,7 +49,7 @@ async def test_event() -> None: # No event types defined, should raise with pytest.raises(AttributeError): - event.event_types + _ = event.event_types # Test retrieving data from entity description event.entity_description = EventEntityDescription( @@ -95,7 +96,7 @@ async def test_event() -> None: event._trigger_event("unknown_event") -@pytest.mark.usefixtures("enable_custom_integrations") +@pytest.mark.usefixtures("enable_custom_integrations", "mock_event_platform") async def test_restore_state(hass: HomeAssistant) -> None: """Test we restore state integration.""" mock_restore_cache_with_extra_data( @@ -127,9 +128,6 @@ async def test_restore_state(hass: HomeAssistant) -> None: ), ) - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) await hass.async_block_till_done() @@ -141,7 +139,7 @@ async def test_restore_state(hass: HomeAssistant) -> None: assert state.attributes["hello"] == "world" -@pytest.mark.usefixtures("enable_custom_integrations") +@pytest.mark.usefixtures("enable_custom_integrations", "mock_event_platform") async def test_invalid_extra_restore_state(hass: HomeAssistant) -> None: """Test we restore state integration.""" mock_restore_cache_with_extra_data( @@ -162,9 +160,6 @@ async def test_invalid_extra_restore_state(hass: HomeAssistant) -> None: ), ) - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) await hass.async_block_till_done() @@ -176,7 +171,7 @@ async def test_invalid_extra_restore_state(hass: HomeAssistant) -> None: assert "hello" not in state.attributes -@pytest.mark.usefixtures("enable_custom_integrations") +@pytest.mark.usefixtures("enable_custom_integrations", "mock_event_platform") async def test_no_extra_restore_state(hass: HomeAssistant) -> None: """Test we restore state integration.""" mock_restore_cache( @@ -197,9 +192,6 @@ async def test_no_extra_restore_state(hass: HomeAssistant) -> None: ), ) - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) await hass.async_block_till_done() @@ -211,7 +203,7 @@ async def test_no_extra_restore_state(hass: HomeAssistant) -> None: assert "hello" not in state.attributes -@pytest.mark.usefixtures("enable_custom_integrations") +@pytest.mark.usefixtures("enable_custom_integrations", "mock_event_platform") async def test_saving_state(hass: HomeAssistant, hass_storage: dict[str, Any]) -> None: """Test we restore state integration.""" restore_data = {"last_event_type": "double_press", "last_event_attributes": None} @@ -229,9 +221,6 @@ async def test_saving_state(hass: HomeAssistant, hass_storage: dict[str, Any]) - ), ) - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) await hass.async_block_till_done() diff --git a/tests/components/event/test_recorder.py b/tests/components/event/test_recorder.py index 133f7e173e3..f45846c9ecc 100644 --- a/tests/components/event/test_recorder.py +++ b/tests/components/event/test_recorder.py @@ -1,4 +1,5 @@ """The tests for event recorder.""" + from __future__ import annotations from unittest.mock import patch diff --git a/tests/components/everlights/conftest.py b/tests/components/everlights/conftest.py index 5009251b102..b9e398e3f64 100644 --- a/tests/components/everlights/conftest.py +++ b/tests/components/everlights/conftest.py @@ -1,2 +1,3 @@ """everlights conftest.""" + from tests.components.light.conftest import mock_light_profiles # noqa: F401 diff --git a/tests/components/everlights/test_light.py b/tests/components/everlights/test_light.py index fbcc967fdba..828b817b236 100644 --- a/tests/components/everlights/test_light.py +++ b/tests/components/everlights/test_light.py @@ -1,4 +1,5 @@ """The tests for the everlights component.""" + from homeassistant.components.everlights import light as everlights diff --git a/tests/components/evil_genius_labs/conftest.py b/tests/components/evil_genius_labs/conftest.py index a4f10fe97c4..49092da75c7 100644 --- a/tests/components/evil_genius_labs/conftest.py +++ b/tests/components/evil_genius_labs/conftest.py @@ -1,4 +1,5 @@ """Test helpers for Evil Genius Labs.""" + import json from unittest.mock import patch @@ -41,18 +42,23 @@ async def setup_evil_genius_labs( hass, config_entry, all_fixture, info_fixture, product_fixture, platforms ): """Test up Evil Genius Labs instance.""" - with patch( - "pyevilgenius.EvilGeniusDevice.get_all", - return_value=all_fixture, - ), patch( - "pyevilgenius.EvilGeniusDevice.get_info", - return_value=info_fixture, - ), patch( - "pyevilgenius.EvilGeniusDevice.get_product", - return_value=product_fixture, - ), patch( - "homeassistant.components.evil_genius_labs.PLATFORMS", - platforms, + with ( + patch( + "pyevilgenius.EvilGeniusDevice.get_all", + return_value=all_fixture, + ), + patch( + "pyevilgenius.EvilGeniusDevice.get_info", + return_value=info_fixture, + ), + patch( + "pyevilgenius.EvilGeniusDevice.get_product", + return_value=product_fixture, + ), + patch( + "homeassistant.components.evil_genius_labs.PLATFORMS", + platforms, + ), ): assert await async_setup_component(hass, "evil_genius_labs", {}) await hass.async_block_till_done() diff --git a/tests/components/evil_genius_labs/test_config_flow.py b/tests/components/evil_genius_labs/test_config_flow.py index 7826104b326..b6bdae940ba 100644 --- a/tests/components/evil_genius_labs/test_config_flow.py +++ b/tests/components/evil_genius_labs/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Evil Genius Labs config flow.""" + from unittest.mock import patch import aiohttp @@ -20,19 +21,24 @@ async def test_form( assert result["type"] == FlowResultType.FORM assert result["errors"] is None - with patch( - "pyevilgenius.EvilGeniusDevice.get_all", - return_value=all_fixture, - ), patch( - "pyevilgenius.EvilGeniusDevice.get_info", - return_value=info_fixture, - ), patch( - "pyevilgenius.EvilGeniusDevice.get_product", - return_value=product_fixture, - ), patch( - "homeassistant.components.evil_genius_labs.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "pyevilgenius.EvilGeniusDevice.get_all", + return_value=all_fixture, + ), + patch( + "pyevilgenius.EvilGeniusDevice.get_info", + return_value=info_fixture, + ), + patch( + "pyevilgenius.EvilGeniusDevice.get_product", + return_value=product_fixture, + ), + patch( + "homeassistant.components.evil_genius_labs.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { diff --git a/tests/components/evil_genius_labs/test_diagnostics.py b/tests/components/evil_genius_labs/test_diagnostics.py index 2435244d3c5..0ad7f6e27f3 100644 --- a/tests/components/evil_genius_labs/test_diagnostics.py +++ b/tests/components/evil_genius_labs/test_diagnostics.py @@ -1,4 +1,5 @@ """Test evil genius labs diagnostics.""" + import pytest from homeassistant.components.diagnostics import REDACTED diff --git a/tests/components/evil_genius_labs/test_init.py b/tests/components/evil_genius_labs/test_init.py index 85055c0a73c..71b8a6164a6 100644 --- a/tests/components/evil_genius_labs/test_init.py +++ b/tests/components/evil_genius_labs/test_init.py @@ -1,4 +1,5 @@ """Test evil genius labs init.""" + import pytest from homeassistant import config_entries diff --git a/tests/components/evil_genius_labs/test_light.py b/tests/components/evil_genius_labs/test_light.py index 5ff7f06804a..9b93e8f66cf 100644 --- a/tests/components/evil_genius_labs/test_light.py +++ b/tests/components/evil_genius_labs/test_light.py @@ -1,4 +1,5 @@ """Test Evil Genius Labs light.""" + from unittest.mock import patch import pytest @@ -28,11 +29,10 @@ async def test_works(hass: HomeAssistant, setup_evil_genius_labs) -> None: @pytest.mark.parametrize("platforms", [("light",)]) async def test_turn_on_color(hass: HomeAssistant, setup_evil_genius_labs) -> None: """Test turning on with a color.""" - with patch( - "pyevilgenius.EvilGeniusDevice.set_path_value" - ) as mock_set_path_value, patch( - "pyevilgenius.EvilGeniusDevice.set_rgb_color" - ) as mock_set_rgb_color: + with ( + patch("pyevilgenius.EvilGeniusDevice.set_path_value") as mock_set_path_value, + patch("pyevilgenius.EvilGeniusDevice.set_rgb_color") as mock_set_rgb_color, + ): await hass.services.async_call( "light", "turn_on", @@ -45,11 +45,11 @@ async def test_turn_on_color(hass: HomeAssistant, setup_evil_genius_labs) -> Non ) assert len(mock_set_path_value.mock_calls) == 2 - mock_set_path_value.mock_calls[0][1] == ("brightness", 100) - mock_set_path_value.mock_calls[1][1] == ("power", 1) + assert mock_set_path_value.mock_calls[0][1] == ("brightness", 100) + assert mock_set_path_value.mock_calls[1][1] == ("power", 1) assert len(mock_set_rgb_color.mock_calls) == 1 - mock_set_rgb_color.mock_calls[0][1] == (10, 20, 30) + assert mock_set_rgb_color.mock_calls[0][1] == (10, 20, 30) @pytest.mark.parametrize("platforms", [("light",)]) @@ -67,8 +67,8 @@ async def test_turn_on_effect(hass: HomeAssistant, setup_evil_genius_labs) -> No ) assert len(mock_set_path_value.mock_calls) == 2 - mock_set_path_value.mock_calls[0][1] == ("pattern", 4) - mock_set_path_value.mock_calls[1][1] == ("power", 1) + assert mock_set_path_value.mock_calls[0][1] == ("pattern", 4) + assert mock_set_path_value.mock_calls[1][1] == ("power", 1) @pytest.mark.parametrize("platforms", [("light",)]) @@ -85,4 +85,4 @@ async def test_turn_off(hass: HomeAssistant, setup_evil_genius_labs) -> None: ) assert len(mock_set_path_value.mock_calls) == 1 - mock_set_path_value.mock_calls[0][1] == ("power", 0) + assert mock_set_path_value.mock_calls[0][1] == ("power", 0) diff --git a/tests/components/ezviz/__init__.py b/tests/components/ezviz/__init__.py index 768fc30cc81..7872cf37b68 100644 --- a/tests/components/ezviz/__init__.py +++ b/tests/components/ezviz/__init__.py @@ -1,4 +1,5 @@ """Tests for the EZVIZ integration.""" + from unittest.mock import patch from homeassistant.components.ezviz.const import ( diff --git a/tests/components/ezviz/conftest.py b/tests/components/ezviz/conftest.py index e89e375fb5e..10fd0406a1c 100644 --- a/tests/components/ezviz/conftest.py +++ b/tests/components/ezviz/conftest.py @@ -1,4 +1,5 @@ """Define pytest.fixtures available for all tests.""" + from unittest.mock import MagicMock, patch from pyezviz import EzvizClient @@ -21,9 +22,12 @@ def mock_ffmpeg(hass): @pytest.fixture def ezviz_test_rtsp_config_flow(hass): """Mock the EzvizApi for easier testing.""" - with patch.object(TestRTSPAuth, "main", return_value=True), patch( - "homeassistant.components.ezviz.config_flow.TestRTSPAuth" - ) as mock_ezviz_test_rtsp: + with ( + patch.object(TestRTSPAuth, "main", return_value=True), + patch( + "homeassistant.components.ezviz.config_flow.TestRTSPAuth" + ) as mock_ezviz_test_rtsp, + ): instance = mock_ezviz_test_rtsp.return_value = TestRTSPAuth( "test-ip", "test-username", @@ -38,9 +42,10 @@ def ezviz_test_rtsp_config_flow(hass): @pytest.fixture def ezviz_config_flow(hass): """Mock the EzvizAPI for easier config flow testing.""" - with patch.object(EzvizClient, "login", return_value=True), patch( - "homeassistant.components.ezviz.config_flow.EzvizClient" - ) as mock_ezviz: + with ( + patch.object(EzvizClient, "login", return_value=True), + patch("homeassistant.components.ezviz.config_flow.EzvizClient") as mock_ezviz, + ): instance = mock_ezviz.return_value = EzvizClient( "test-username", "test-password", diff --git a/tests/components/ezviz/test_config_flow.py b/tests/components/ezviz/test_config_flow.py index d3bd2a1bed6..c99c9c0fe9e 100644 --- a/tests/components/ezviz/test_config_flow.py +++ b/tests/components/ezviz/test_config_flow.py @@ -1,4 +1,5 @@ """Test the EZVIZ config flow.""" + from unittest.mock import patch from pyezviz.exceptions import ( diff --git a/tests/components/faa_delays/test_config_flow.py b/tests/components/faa_delays/test_config_flow.py index 5fb1b9cfcd2..92a8929afbf 100644 --- a/tests/components/faa_delays/test_config_flow.py +++ b/tests/components/faa_delays/test_config_flow.py @@ -1,4 +1,5 @@ """Test the FAA Delays config flow.""" + from unittest.mock import patch from aiohttp import ClientConnectionError @@ -27,10 +28,13 @@ async def test_form(hass: HomeAssistant) -> None: assert result["type"] == "form" assert result["errors"] == {} - with patch.object(faadelays.Airport, "update", new=mock_valid_airport), patch( - "homeassistant.components.faa_delays.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch.object(faadelays.Airport, "update", new=mock_valid_airport), + patch( + "homeassistant.components.faa_delays.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { diff --git a/tests/components/facebook/test_notify.py b/tests/components/facebook/test_notify.py index 152a516943b..bbaa1f12516 100644 --- a/tests/components/facebook/test_notify.py +++ b/tests/components/facebook/test_notify.py @@ -1,4 +1,5 @@ """The test for the Facebook notify module.""" + from http import HTTPStatus import pytest diff --git a/tests/components/fail2ban/test_sensor.py b/tests/components/fail2ban/test_sensor.py index 5906ac947fe..713edb72444 100644 --- a/tests/components/fail2ban/test_sensor.py +++ b/tests/components/fail2ban/test_sensor.py @@ -1,4 +1,5 @@ """The tests for local file sensor platform.""" + from unittest.mock import Mock, mock_open, patch from homeassistant.components.fail2ban.sensor import ( diff --git a/tests/components/fan/common.py b/tests/components/fan/common.py index 58264d80817..7955a91bc0a 100644 --- a/tests/components/fan/common.py +++ b/tests/components/fan/common.py @@ -3,6 +3,7 @@ All containing methods are legacy helpers that should not be used by new components. Instead call the service directly. """ + from homeassistant.components.fan import ( ATTR_DIRECTION, ATTR_OSCILLATING, @@ -43,6 +44,7 @@ async def async_turn_on( } await hass.services.async_call(DOMAIN, SERVICE_TURN_ON, data, blocking=True) + await hass.async_block_till_done() async def async_turn_off(hass, entity_id=ENTITY_MATCH_ALL) -> None: @@ -50,6 +52,7 @@ async def async_turn_off(hass, entity_id=ENTITY_MATCH_ALL) -> None: data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} await hass.services.async_call(DOMAIN, SERVICE_TURN_OFF, data, blocking=True) + await hass.async_block_till_done() async def async_oscillate( @@ -66,6 +69,7 @@ async def async_oscillate( } await hass.services.async_call(DOMAIN, SERVICE_OSCILLATE, data, blocking=True) + await hass.async_block_till_done() async def async_set_preset_mode( @@ -79,6 +83,7 @@ async def async_set_preset_mode( } await hass.services.async_call(DOMAIN, SERVICE_SET_PRESET_MODE, data, blocking=True) + await hass.async_block_till_done() async def async_set_percentage( @@ -92,6 +97,7 @@ async def async_set_percentage( } await hass.services.async_call(DOMAIN, SERVICE_SET_PERCENTAGE, data, blocking=True) + await hass.async_block_till_done() async def async_increase_speed( @@ -108,6 +114,7 @@ async def async_increase_speed( } await hass.services.async_call(DOMAIN, SERVICE_INCREASE_SPEED, data, blocking=True) + await hass.async_block_till_done() async def async_decrease_speed( @@ -124,6 +131,7 @@ async def async_decrease_speed( } await hass.services.async_call(DOMAIN, SERVICE_DECREASE_SPEED, data, blocking=True) + await hass.async_block_till_done() async def async_set_direction( @@ -137,3 +145,4 @@ async def async_set_direction( } await hass.services.async_call(DOMAIN, SERVICE_SET_DIRECTION, data, blocking=True) + await hass.async_block_till_done() diff --git a/tests/components/fan/test_device_action.py b/tests/components/fan/test_device_action.py index b8756d9ace5..c08e0617700 100644 --- a/tests/components/fan/test_device_action.py +++ b/tests/components/fan/test_device_action.py @@ -1,4 +1,5 @@ """The tests for Fan device actions.""" + import pytest from pytest_unordered import unordered @@ -57,12 +58,12 @@ async def test_get_actions( @pytest.mark.parametrize( ("hidden_by", "entity_category"), - ( + [ (RegistryEntryHider.INTEGRATION, None), (RegistryEntryHider.USER, None), (None, EntityCategory.CONFIG), (None, EntityCategory.DIAGNOSTIC), - ), + ], ) async def test_get_actions_hidden_auxiliary( hass: HomeAssistant, diff --git a/tests/components/fan/test_device_condition.py b/tests/components/fan/test_device_condition.py index 1ee168f28ab..afd237d1974 100644 --- a/tests/components/fan/test_device_condition.py +++ b/tests/components/fan/test_device_condition.py @@ -1,4 +1,5 @@ """The tests for Fan device conditions.""" + import pytest from pytest_unordered import unordered @@ -63,12 +64,12 @@ async def test_get_conditions( @pytest.mark.parametrize( ("hidden_by", "entity_category"), - ( + [ (RegistryEntryHider.INTEGRATION, None), (RegistryEntryHider.USER, None), (None, EntityCategory.CONFIG), (None, EntityCategory.DIAGNOSTIC), - ), + ], ) async def test_get_conditions_hidden_auxiliary( hass: HomeAssistant, diff --git a/tests/components/fan/test_device_trigger.py b/tests/components/fan/test_device_trigger.py index 8ac5e79ba5b..92b6443f241 100644 --- a/tests/components/fan/test_device_trigger.py +++ b/tests/components/fan/test_device_trigger.py @@ -1,4 +1,5 @@ """The tests for Fan device triggers.""" + from datetime import timedelta import pytest @@ -68,12 +69,12 @@ async def test_get_triggers( @pytest.mark.parametrize( ("hidden_by", "entity_category"), - ( + [ (RegistryEntryHider.INTEGRATION, None), (RegistryEntryHider.USER, None), (None, EntityCategory.CONFIG), (None, EntityCategory.DIAGNOSTIC), - ), + ], ) async def test_get_triggers_hidden_auxiliary( hass: HomeAssistant, diff --git a/tests/components/fan/test_init.py b/tests/components/fan/test_init.py index 1beea47c6fa..911954d1ecd 100644 --- a/tests/components/fan/test_init.py +++ b/tests/components/fan/test_init.py @@ -1,4 +1,5 @@ """Tests for fan platforms.""" + import pytest from homeassistant.components import fan diff --git a/tests/components/fan/test_recorder.py b/tests/components/fan/test_recorder.py index d75e1e18511..4f1499765b8 100644 --- a/tests/components/fan/test_recorder.py +++ b/tests/components/fan/test_recorder.py @@ -1,4 +1,5 @@ """The tests for fan recorder.""" + from __future__ import annotations from datetime import timedelta diff --git a/tests/components/fan/test_reproduce_state.py b/tests/components/fan/test_reproduce_state.py index 3d9ea90e735..e1904b34769 100644 --- a/tests/components/fan/test_reproduce_state.py +++ b/tests/components/fan/test_reproduce_state.py @@ -1,4 +1,5 @@ """Test reproduce state for Fan.""" + import pytest from homeassistant.components.fan import ( diff --git a/tests/components/fan/test_significant_change.py b/tests/components/fan/test_significant_change.py index 764abb6e8ee..9b940e33138 100644 --- a/tests/components/fan/test_significant_change.py +++ b/tests/components/fan/test_significant_change.py @@ -1,4 +1,5 @@ """Test the Fan significant change platform.""" + import pytest from homeassistant.components.fan import ( diff --git a/tests/components/fastdotcom/snapshots/test_diagnostics.ambr b/tests/components/fastdotcom/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..d2b4ca6c55a --- /dev/null +++ b/tests/components/fastdotcom/snapshots/test_diagnostics.ambr @@ -0,0 +1,6 @@ +# serializer version: 1 +# name: test_get_config_entry_diagnostics + dict({ + 'coordinator_data': 50.3, + }) +# --- diff --git a/tests/components/fastdotcom/test_config_flow.py b/tests/components/fastdotcom/test_config_flow.py index 17e75935dae..d2122f4fe61 100644 --- a/tests/components/fastdotcom/test_config_flow.py +++ b/tests/components/fastdotcom/test_config_flow.py @@ -1,4 +1,5 @@ """Test for the Fast.com config flow.""" + from unittest.mock import patch import pytest diff --git a/tests/components/fastdotcom/test_coordinator.py b/tests/components/fastdotcom/test_coordinator.py index f51f0254714..5c8cb17c736 100644 --- a/tests/components/fastdotcom/test_coordinator.py +++ b/tests/components/fastdotcom/test_coordinator.py @@ -1,4 +1,5 @@ """Test the FastdotcomDataUpdateCoordindator.""" + from datetime import timedelta from unittest.mock import patch diff --git a/tests/components/fastdotcom/test_diagnostics.py b/tests/components/fastdotcom/test_diagnostics.py new file mode 100644 index 00000000000..7ea644665c7 --- /dev/null +++ b/tests/components/fastdotcom/test_diagnostics.py @@ -0,0 +1,43 @@ +"""Test the Fast.com component diagnostics.""" + +from unittest.mock import patch + +from syrupy import SnapshotAssertion + +from homeassistant.components.fastdotcom.const import DEFAULT_NAME, DOMAIN +from homeassistant.config_entries import SOURCE_USER +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry +from tests.components.diagnostics import get_diagnostics_for_config_entry +from tests.typing import ClientSessionGenerator + + +async def test_get_config_entry_diagnostics( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + snapshot: SnapshotAssertion, +) -> None: + """Test if get_config_entry_diagnostics returns the correct data.""" + config_entry = MockConfigEntry( + version=1, + domain=DOMAIN, + title=DEFAULT_NAME, + source=SOURCE_USER, + options={}, + entry_id="TEST_ENTRY_ID", + unique_id="UNIQUE_TEST_ID", + minor_version=1, + ) + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.fastdotcom.coordinator.fast_com", return_value=50.3 + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert ( + await get_diagnostics_for_config_entry(hass, hass_client, config_entry) + == snapshot + ) diff --git a/tests/components/fastdotcom/test_init.py b/tests/components/fastdotcom/test_init.py index 547d574b25a..12e3902d874 100644 --- a/tests/components/fastdotcom/test_init.py +++ b/tests/components/fastdotcom/test_init.py @@ -1,4 +1,5 @@ """Test for Sensibo component Init.""" + from __future__ import annotations from unittest.mock import patch diff --git a/tests/components/fastdotcom/test_sensor.py b/tests/components/fastdotcom/test_sensor.py index 47826bf35cf..8f5a81e5205 100644 --- a/tests/components/fastdotcom/test_sensor.py +++ b/tests/components/fastdotcom/test_sensor.py @@ -1,4 +1,5 @@ """Test the FastdotcomDataUpdateCoordindator.""" + from unittest.mock import patch from freezegun.api import FrozenDateTimeFactory diff --git a/tests/components/fastdotcom/test_service.py b/tests/components/fastdotcom/test_service.py index 2f919bc8a84..8747beb6245 100644 --- a/tests/components/fastdotcom/test_service.py +++ b/tests/components/fastdotcom/test_service.py @@ -1,4 +1,5 @@ """Test Fastdotcom service.""" + from unittest.mock import patch import pytest diff --git a/tests/fixtures/feedreader.xml b/tests/components/feedreader/fixtures/feedreader.xml similarity index 100% rename from tests/fixtures/feedreader.xml rename to tests/components/feedreader/fixtures/feedreader.xml diff --git a/tests/fixtures/feedreader1.xml b/tests/components/feedreader/fixtures/feedreader1.xml similarity index 100% rename from tests/fixtures/feedreader1.xml rename to tests/components/feedreader/fixtures/feedreader1.xml diff --git a/tests/fixtures/feedreader2.xml b/tests/components/feedreader/fixtures/feedreader2.xml similarity index 100% rename from tests/fixtures/feedreader2.xml rename to tests/components/feedreader/fixtures/feedreader2.xml diff --git a/tests/fixtures/feedreader3.xml b/tests/components/feedreader/fixtures/feedreader3.xml similarity index 100% rename from tests/fixtures/feedreader3.xml rename to tests/components/feedreader/fixtures/feedreader3.xml diff --git a/tests/fixtures/feedreader4.xml b/tests/components/feedreader/fixtures/feedreader4.xml similarity index 100% rename from tests/fixtures/feedreader4.xml rename to tests/components/feedreader/fixtures/feedreader4.xml diff --git a/tests/fixtures/feedreader5.xml b/tests/components/feedreader/fixtures/feedreader5.xml similarity index 100% rename from tests/fixtures/feedreader5.xml rename to tests/components/feedreader/fixtures/feedreader5.xml diff --git a/tests/fixtures/feedreader6.xml b/tests/components/feedreader/fixtures/feedreader6.xml similarity index 100% rename from tests/fixtures/feedreader6.xml rename to tests/components/feedreader/fixtures/feedreader6.xml diff --git a/tests/components/feedreader/test_init.py b/tests/components/feedreader/test_init.py index cd906940931..f836d233670 100644 --- a/tests/components/feedreader/test_init.py +++ b/tests/components/feedreader/test_init.py @@ -1,4 +1,5 @@ """The tests for the feedreader component.""" + from collections.abc import Generator from datetime import datetime, timedelta import pickle @@ -14,6 +15,7 @@ from homeassistant.components.feedreader import ( CONF_MAX_ENTRIES, CONF_URLS, DEFAULT_SCAN_INTERVAL, + DOMAIN, EVENT_FEEDREADER, ) from homeassistant.const import CONF_SCAN_INTERVAL, EVENT_HOMEASSISTANT_START @@ -33,7 +35,7 @@ VALID_CONFIG_5 = {feedreader.DOMAIN: {CONF_URLS: [URL], CONF_MAX_ENTRIES: 1}} def load_fixture_bytes(src: str) -> bytes: """Return byte stream of fixture.""" - feed_data = load_fixture(src) + feed_data = load_fixture(src, DOMAIN) raw = bytes(feed_data, "utf-8") return raw @@ -196,10 +198,13 @@ async def test_storage_data_writing( """Test writing to storage.""" storage_data: dict[str, str] = {URL: "2018-04-30T05:10:00+00:00"} - with patch( - "feedparser.http.get", - return_value=feed_one_event, - ), patch("homeassistant.components.feedreader.DELAY_SAVE", new=0): + with ( + patch( + "feedparser.http.get", + return_value=feed_one_event, + ), + patch("homeassistant.components.feedreader.DELAY_SAVE", new=0), + ): assert await async_setup_component(hass, feedreader.DOMAIN, VALID_CONFIG_2) hass.bus.async_fire(EVENT_HOMEASSISTANT_START) @@ -295,13 +300,16 @@ async def test_feed_identical_timestamps( hass: HomeAssistant, events, feed_identically_timed_events ) -> None: """Test feed with 2 entries with identical timestamps.""" - with patch( - "feedparser.http.get", - return_value=feed_identically_timed_events, - ), patch( - "homeassistant.components.feedreader.StoredData.get_timestamp", - return_value=gmtime( - datetime.fromisoformat("1970-01-01T00:00:00.0+0000").timestamp() + with ( + patch( + "feedparser.http.get", + return_value=feed_identically_timed_events, + ), + patch( + "homeassistant.components.feedreader.StoredData.get_timestamp", + return_value=gmtime( + datetime.fromisoformat("1970-01-01T00:00:00.0+0000").timestamp() + ), ), ): assert await async_setup_component(hass, feedreader.DOMAIN, VALID_CONFIG_2) @@ -369,14 +377,14 @@ async def test_feed_updates( # Change time and fetch more entries future = dt_util.utcnow() + timedelta(hours=1, seconds=1) async_fire_time_changed(hass, future) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert len(events) == 2 # Change time but no new entries future = dt_util.utcnow() + timedelta(hours=2, seconds=2) async_fire_time_changed(hass, future) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert len(events) == 2 diff --git a/tests/components/ffmpeg/test_binary_sensor.py b/tests/components/ffmpeg/test_binary_sensor.py index 6eec115d6f0..8b1a5115f86 100644 --- a/tests/components/ffmpeg/test_binary_sensor.py +++ b/tests/components/ffmpeg/test_binary_sensor.py @@ -1,4 +1,5 @@ """The tests for Home Assistant ffmpeg binary sensor.""" + from unittest.mock import AsyncMock, patch from homeassistant.const import EVENT_HOMEASSISTANT_START @@ -64,7 +65,7 @@ async def test_noise_setup_component_start_callback(mock_ffmpeg, hass: HomeAssis entity = hass.states.get("binary_sensor.ffmpeg_noise") assert entity.state == "off" - hass.async_add_job(mock_ffmpeg.call_args[0][1], True) + mock_ffmpeg.call_args[0][1](True) await hass.async_block_till_done() entity = hass.states.get("binary_sensor.ffmpeg_noise") @@ -120,7 +121,7 @@ async def test_motion_setup_component_start_callback(mock_ffmpeg, hass: HomeAssi entity = hass.states.get("binary_sensor.ffmpeg_motion") assert entity.state == "off" - hass.async_add_job(mock_ffmpeg.call_args[0][1], True) + mock_ffmpeg.call_args[0][1](True) await hass.async_block_till_done() entity = hass.states.get("binary_sensor.ffmpeg_motion") diff --git a/tests/components/ffmpeg/test_init.py b/tests/components/ffmpeg/test_init.py index 452a8188596..60d24baa302 100644 --- a/tests/components/ffmpeg/test_init.py +++ b/tests/components/ffmpeg/test_init.py @@ -1,4 +1,5 @@ """The tests for Home Assistant ffmpeg.""" + from unittest.mock import AsyncMock, MagicMock, Mock, call, patch from homeassistant.components import ffmpeg @@ -7,6 +8,7 @@ from homeassistant.components.ffmpeg import ( SERVICE_RESTART, SERVICE_START, SERVICE_STOP, + get_ffmpeg_manager, ) from homeassistant.const import ( ATTR_ENTITY_ID, @@ -26,7 +28,7 @@ def async_start(hass, entity_id=None): This is a legacy helper method. Do not use it for new tests. """ data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_START, data)) + hass.async_create_task(hass.services.async_call(DOMAIN, SERVICE_START, data)) @callback @@ -36,7 +38,7 @@ def async_stop(hass, entity_id=None): This is a legacy helper method. Do not use it for new tests. """ data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_STOP, data)) + hass.async_create_task(hass.services.async_call(DOMAIN, SERVICE_STOP, data)) @callback @@ -46,7 +48,7 @@ def async_restart(hass, entity_id=None): This is a legacy helper method. Do not use it for new tests. """ data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_RESTART, data)) + hass.async_create_task(hass.services.async_call(DOMAIN, SERVICE_RESTART, data)) class MockFFmpegDev(ffmpeg.FFmpegBase): @@ -250,3 +252,45 @@ async def test_async_get_image_with_extra_cmd_width_height(hass: HomeAssistant) assert get_image_mock.call_args_list == [ call("rtsp://fake", output_format="mjpeg", extra_cmd="-vf any -s 640x480") ] + + +async def test_modern_ffmpeg( + hass: HomeAssistant, +) -> None: + """Test modern ffmpeg uses the new ffmpeg content type.""" + with assert_setup_component(1): + await async_setup_component(hass, ffmpeg.DOMAIN, {ffmpeg.DOMAIN: {}}) + + manager = get_ffmpeg_manager(hass) + assert "ffmpeg" in manager.ffmpeg_stream_content_type + + +async def test_legacy_ffmpeg( + hass: HomeAssistant, +) -> None: + """Test legacy ffmpeg uses the old ffserver content type.""" + with ( + assert_setup_component(1), + patch( + "homeassistant.components.ffmpeg.FFVersion.get_version", return_value="3.0" + ), + patch("homeassistant.components.ffmpeg.is_official_image", return_value=False), + ): + await async_setup_component(hass, ffmpeg.DOMAIN, {ffmpeg.DOMAIN: {}}) + + manager = get_ffmpeg_manager(hass) + assert "ffserver" in manager.ffmpeg_stream_content_type + + +async def test_ffmpeg_using_official_image( + hass: HomeAssistant, +) -> None: + """Test ffmpeg using official image is the new ffmpeg content type.""" + with ( + assert_setup_component(1), + patch("homeassistant.components.ffmpeg.is_official_image", return_value=True), + ): + await async_setup_component(hass, ffmpeg.DOMAIN, {ffmpeg.DOMAIN: {}}) + + manager = get_ffmpeg_manager(hass) + assert "ffmpeg" in manager.ffmpeg_stream_content_type diff --git a/tests/components/fibaro/conftest.py b/tests/components/fibaro/conftest.py index d86817814b1..345668c23bd 100644 --- a/tests/components/fibaro/conftest.py +++ b/tests/components/fibaro/conftest.py @@ -1,4 +1,5 @@ """Test helpers.""" + from collections.abc import Generator from unittest.mock import AsyncMock, Mock, patch diff --git a/tests/components/fibaro/test_config_flow.py b/tests/components/fibaro/test_config_flow.py index 42d20f902c0..2c7d05b87a3 100644 --- a/tests/components/fibaro/test_config_flow.py +++ b/tests/components/fibaro/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Fibaro config flow.""" + from unittest.mock import Mock import pytest diff --git a/tests/components/fibaro/test_scene.py b/tests/components/fibaro/test_scene.py index e07face3ac0..667f8236a31 100644 --- a/tests/components/fibaro/test_scene.py +++ b/tests/components/fibaro/test_scene.py @@ -1,4 +1,5 @@ """Test the Fibaro scene platform.""" + from unittest.mock import Mock from homeassistant.components.scene import DOMAIN as SCENE_DOMAIN diff --git a/tests/components/fido/test_sensor.py b/tests/components/fido/test_sensor.py index 81ae54174ca..a067f060af8 100644 --- a/tests/components/fido/test_sensor.py +++ b/tests/components/fido/test_sensor.py @@ -1,4 +1,5 @@ """The test for the fido sensor platform.""" + import logging from unittest.mock import MagicMock, patch diff --git a/tests/components/file/test_notify.py b/tests/components/file/test_notify.py index 9cde648d27c..3077d71bdde 100644 --- a/tests/components/file/test_notify.py +++ b/tests/components/file/test_notify.py @@ -1,4 +1,5 @@ """The tests for the notify file platform.""" + import os from unittest.mock import call, mock_open, patch @@ -19,6 +20,7 @@ async def test_bad_config(hass: HomeAssistant) -> None: config = {notify.DOMAIN: {"name": "test", "platform": "file"}} with assert_setup_component(0) as handle_config: assert await async_setup_component(hass, notify.DOMAIN, config) + await hass.async_block_till_done() assert not handle_config[notify.DOMAIN] @@ -48,14 +50,16 @@ async def test_notify_file( } }, ) + await hass.async_block_till_done() assert handle_config[notify.DOMAIN] freezer.move_to(dt_util.utcnow()) m_open = mock_open() - with patch("homeassistant.components.file.notify.open", m_open, create=True), patch( - "homeassistant.components.file.notify.os.stat" - ) as mock_st: + with ( + patch("homeassistant.components.file.notify.open", m_open, create=True), + patch("homeassistant.components.file.notify.os.stat") as mock_st, + ): mock_st.return_value.st_size = 0 title = ( f"{ATTR_TITLE_DEFAULT} notifications " diff --git a/tests/components/file/test_sensor.py b/tests/components/file/test_sensor.py index e01d90b8cc3..8acdc324209 100644 --- a/tests/components/file/test_sensor.py +++ b/tests/components/file/test_sensor.py @@ -1,4 +1,5 @@ """The tests for local file sensor platform.""" + from unittest.mock import Mock, patch from homeassistant.const import STATE_UNKNOWN diff --git a/tests/components/file_upload/conftest.py b/tests/components/file_upload/conftest.py index ab9965c1914..00423ad21e6 100644 --- a/tests/components/file_upload/conftest.py +++ b/tests/components/file_upload/conftest.py @@ -1,4 +1,5 @@ """Fixtures for FileUpload integration.""" + from io import StringIO import pytest diff --git a/tests/components/file_upload/test_init.py b/tests/components/file_upload/test_init.py index e9dfbdb4bd6..1ef238cafd0 100644 --- a/tests/components/file_upload/test_init.py +++ b/tests/components/file_upload/test_init.py @@ -1,4 +1,6 @@ """Test the File Upload integration.""" + +from contextlib import contextmanager from pathlib import Path from random import getrandbits from unittest.mock import patch @@ -19,11 +21,14 @@ async def uploaded_file_dir(hass: HomeAssistant, hass_client) -> Path: assert await async_setup_component(hass, "file_upload", {}) client = await hass_client() - with patch( - # Patch temp dir name to avoid tests fail running in parallel - "homeassistant.components.file_upload.TEMP_DIR_NAME", - file_upload.TEMP_DIR_NAME + f"-{getrandbits(10):03x}", - ), TEST_IMAGE.open("rb") as fp: + with ( + patch( + # Patch temp dir name to avoid tests fail running in parallel + "homeassistant.components.file_upload.TEMP_DIR_NAME", + file_upload.TEMP_DIR_NAME + f"-{getrandbits(10):03x}", + ), + TEST_IMAGE.open("rb") as fp, + ): res = await client.post("/api/file_upload", data={"file": fp}) assert res.status == 200 @@ -78,14 +83,17 @@ async def test_upload_large_file( assert await async_setup_component(hass, "file_upload", {}) client = await hass_client() - with patch( - # Patch temp dir name to avoid tests fail running in parallel - "homeassistant.components.file_upload.TEMP_DIR_NAME", - file_upload.TEMP_DIR_NAME + f"-{getrandbits(10):03x}", - ), patch( - # Patch one megabyte to 8 bytes to prevent having to use big files in tests - "homeassistant.components.file_upload.ONE_MEGABYTE", - 8, + with ( + patch( + # Patch temp dir name to avoid tests fail running in parallel + "homeassistant.components.file_upload.TEMP_DIR_NAME", + file_upload.TEMP_DIR_NAME + f"-{getrandbits(10):03x}", + ), + patch( + # Patch one megabyte to 8 bytes to prevent having to use big files in tests + "homeassistant.components.file_upload.ONE_MEGABYTE", + 8, + ), ): res = await client.post("/api/file_upload", data={"file": large_file_io}) @@ -117,3 +125,45 @@ async def test_upload_with_wrong_key_fails( res = await client.post("/api/file_upload", data={"wrong_key": large_file_io}) assert res.status == 400 + + +async def test_upload_large_file_fails( + hass: HomeAssistant, hass_client: ClientSessionGenerator, large_file_io +) -> None: + """Test uploading large file.""" + assert await async_setup_component(hass, "file_upload", {}) + client = await hass_client() + + @contextmanager + def _mock_open(*args, **kwargs): + yield MockPathOpen() + + class MockPathOpen: + def __init__(self, *args, **kwargs) -> None: + pass + + def write(self, data: bytes) -> None: + raise OSError("Boom") + + with ( + patch( + # Patch temp dir name to avoid tests fail running in parallel + "homeassistant.components.file_upload.TEMP_DIR_NAME", + file_upload.TEMP_DIR_NAME + f"-{getrandbits(10):03x}", + ), + patch( + # Patch one megabyte to 8 bytes to prevent having to use big files in tests + "homeassistant.components.file_upload.ONE_MEGABYTE", + 8, + ), + patch( + "homeassistant.components.file_upload.Path.open", return_value=_mock_open() + ), + ): + res = await client.post("/api/file_upload", data={"file": large_file_io}) + + assert res.status == 500 + + response = await res.content.read() + + assert b"Boom" in response diff --git a/tests/components/filesize/conftest.py b/tests/components/filesize/conftest.py index ac36ab687f4..81aea2aee54 100644 --- a/tests/components/filesize/conftest.py +++ b/tests/components/filesize/conftest.py @@ -1,4 +1,5 @@ """Fixtures for Filesize integration tests.""" + from __future__ import annotations from collections.abc import Generator diff --git a/tests/components/filesize/test_config_flow.py b/tests/components/filesize/test_config_flow.py index 27dec438168..38209a3014e 100644 --- a/tests/components/filesize/test_config_flow.py +++ b/tests/components/filesize/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for the Filesize config flow.""" + from pathlib import Path from unittest.mock import patch diff --git a/tests/components/filesize/test_init.py b/tests/components/filesize/test_init.py index c580bb7da77..7f1672176dc 100644 --- a/tests/components/filesize/test_init.py +++ b/tests/components/filesize/test_init.py @@ -1,4 +1,5 @@ """Tests for the Filesize integration.""" + from pathlib import Path from homeassistant.components.filesize.const import DOMAIN diff --git a/tests/components/filesize/test_sensor.py b/tests/components/filesize/test_sensor.py index 20354df13bd..880563f0ad8 100644 --- a/tests/components/filesize/test_sensor.py +++ b/tests/components/filesize/test_sensor.py @@ -1,4 +1,5 @@ """The tests for the filesize sensor.""" + import os from pathlib import Path diff --git a/tests/components/filter/test_sensor.py b/tests/components/filter/test_sensor.py index ffb306a23c1..67370bbcedc 100644 --- a/tests/components/filter/test_sensor.py +++ b/tests/components/filter/test_sensor.py @@ -1,4 +1,5 @@ """The test for the data filter sensor platform.""" + from datetime import timedelta from unittest.mock import patch @@ -91,7 +92,7 @@ async def test_chain( assert state.state == "18.05" -@pytest.mark.parametrize("missing", (True, False)) +@pytest.mark.parametrize("missing", [True, False]) async def test_chain_history( recorder_mock: Recorder, hass: HomeAssistant, @@ -129,12 +130,15 @@ async def test_chain_history( ] } - with patch( - "homeassistant.components.recorder.history.state_changes_during_period", - return_value=fake_states, - ), patch( - "homeassistant.components.recorder.history.get_last_state_changes", - return_value=fake_states, + with ( + patch( + "homeassistant.components.recorder.history.state_changes_during_period", + return_value=fake_states, + ), + patch( + "homeassistant.components.recorder.history.get_last_state_changes", + return_value=fake_states, + ), ): with assert_setup_component(1, "sensor"): assert await async_setup_component(hass, "sensor", config) @@ -233,12 +237,15 @@ async def test_history_time(recorder_mock: Recorder, hass: HomeAssistant) -> Non State("sensor.test_monitored", "18.2", last_changed=t_2), ] } - with patch( - "homeassistant.components.recorder.history.state_changes_during_period", - return_value=fake_states, - ), patch( - "homeassistant.components.recorder.history.get_last_state_changes", - return_value=fake_states, + with ( + patch( + "homeassistant.components.recorder.history.state_changes_during_period", + return_value=fake_states, + ), + patch( + "homeassistant.components.recorder.history.get_last_state_changes", + return_value=fake_states, + ), ): with assert_setup_component(1, "sensor"): assert await async_setup_component(hass, "sensor", config) @@ -383,7 +390,7 @@ def test_initial_outlier(values: list[State]) -> None: """Test issue #13363.""" filt = OutlierFilter(window_size=3, precision=2, entity=None, radius=4.0) out = State("sensor.test_monitored", "4000") - for state in [out] + values: + for state in [out, *values]: filtered = filt.filter_state(state) assert filtered.state == 21 @@ -392,7 +399,7 @@ def test_unknown_state_outlier(values: list[State]) -> None: """Test issue #32395.""" filt = OutlierFilter(window_size=3, precision=2, entity=None, radius=4.0) out = State("sensor.test_monitored", "unknown") - for state in [out] + values + [out]: + for state in [out, *values, out]: try: filtered = filt.filter_state(state) except ValueError: @@ -412,7 +419,7 @@ def test_lowpass(values: list[State]) -> None: """Test if lowpass filter works.""" filt = LowPassFilter(window_size=10, precision=2, entity=None, time_constant=10) out = State("sensor.test_monitored", "unknown") - for state in [out] + values + [out]: + for state in [out, *values, out]: try: filtered = filt.filter_state(state) except ValueError: diff --git a/tests/components/fireservicerota/test_config_flow.py b/tests/components/fireservicerota/test_config_flow.py index ace0532f1f7..e2bf5911089 100644 --- a/tests/components/fireservicerota/test_config_flow.py +++ b/tests/components/fireservicerota/test_config_flow.py @@ -1,4 +1,5 @@ """Test the FireServiceRota config flow.""" + from unittest.mock import patch from pyfireservicerota import InvalidAuthError @@ -76,12 +77,15 @@ async def test_invalid_credentials(hass: HomeAssistant) -> None: async def test_step_user(hass: HomeAssistant) -> None: """Test the start of the config flow.""" - with patch( - "homeassistant.components.fireservicerota.config_flow.FireServiceRota" - ) as mock_fsr, patch( - "homeassistant.components.fireservicerota.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.fireservicerota.config_flow.FireServiceRota" + ) as mock_fsr, + patch( + "homeassistant.components.fireservicerota.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): mock_fireservicerota = mock_fsr.return_value mock_fireservicerota.request_tokens.return_value = MOCK_TOKEN_INFO @@ -133,11 +137,14 @@ async def test_reauth(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert result["type"] == data_entry_flow.FlowResultType.FORM - with patch( - "homeassistant.components.fireservicerota.config_flow.FireServiceRota" - ) as mock_fsr, patch( - "homeassistant.components.fireservicerota.async_setup_entry", - return_value=True, + with ( + patch( + "homeassistant.components.fireservicerota.config_flow.FireServiceRota" + ) as mock_fsr, + patch( + "homeassistant.components.fireservicerota.async_setup_entry", + return_value=True, + ), ): mock_fireservicerota = mock_fsr.return_value mock_fireservicerota.request_tokens.return_value = MOCK_TOKEN_INFO diff --git a/tests/components/firmata/test_config_flow.py b/tests/components/firmata/test_config_flow.py index 474455fc164..a3c8ca7e728 100644 --- a/tests/components/firmata/test_config_flow.py +++ b/tests/components/firmata/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Firmata config flow.""" + from unittest.mock import patch from pymata_express.pymata_express_serial import serial @@ -63,13 +64,15 @@ async def test_import_cannot_connect_serial_timeout(hass: HomeAssistant) -> None async def test_import(hass: HomeAssistant) -> None: """Test we create an entry from config.""" - with patch( - "homeassistant.components.firmata.board.PymataExpress", autospec=True - ), patch( - "homeassistant.components.firmata.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.firmata.async_setup_entry", return_value=True - ) as mock_setup_entry: + with ( + patch("homeassistant.components.firmata.board.PymataExpress", autospec=True), + patch( + "homeassistant.components.firmata.async_setup", return_value=True + ) as mock_setup, + patch( + "homeassistant.components.firmata.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}, diff --git a/tests/components/fitbit/conftest.py b/tests/components/fitbit/conftest.py index a076be7f63d..a4bfed43cba 100644 --- a/tests/components/fitbit/conftest.py +++ b/tests/components/fitbit/conftest.py @@ -125,12 +125,15 @@ def mock_fitbit_config_setup( ) -> Generator[None, None, None]: """Fixture to mock out fitbit.conf file data loading and persistence.""" has_config = fitbit_config_yaml is not None - with patch( - "homeassistant.components.fitbit.sensor.os.path.isfile", - return_value=has_config, - ), patch( - "homeassistant.components.fitbit.sensor.load_json_object", - return_value=fitbit_config_yaml, + with ( + patch( + "homeassistant.components.fitbit.sensor.os.path.isfile", + return_value=has_config, + ), + patch( + "homeassistant.components.fitbit.sensor.load_json_object", + return_value=fitbit_config_yaml, + ), ): yield diff --git a/tests/components/fitbit/test_sensor.py b/tests/components/fitbit/test_sensor.py index 59405e3ea91..9443d0500eb 100644 --- a/tests/components/fitbit/test_sensor.py +++ b/tests/components/fitbit/test_sensor.py @@ -1,6 +1,5 @@ """Tests for the fitbit sensor platform.""" - from collections.abc import Awaitable, Callable from http import HTTPStatus from typing import Any diff --git a/tests/components/fivem/test_config_flow.py b/tests/components/fivem/test_config_flow.py index 121b416a110..174078c5420 100644 --- a/tests/components/fivem/test_config_flow.py +++ b/tests/components/fivem/test_config_flow.py @@ -1,4 +1,5 @@ """Test the FiveM config flow.""" + from unittest.mock import patch from fivem import FiveMServerOfflineError @@ -66,13 +67,16 @@ async def test_form(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.FORM assert result["errors"] is None - with patch( - "fivem.fivem.FiveM.get_info_raw", - return_value=_mock_fivem_info_success(), - ), patch( - "homeassistant.components.fivem.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "fivem.fivem.FiveM.get_info_raw", + return_value=_mock_fivem_info_success(), + ), + patch( + "homeassistant.components.fivem.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], USER_INPUT, diff --git a/tests/components/fjaraskupan/__init__.py b/tests/components/fjaraskupan/__init__.py index 5025fbeaf06..a55d7ea84c0 100644 --- a/tests/components/fjaraskupan/__init__.py +++ b/tests/components/fjaraskupan/__init__.py @@ -1,6 +1,5 @@ """Tests for the Fjäråskupan integration.""" - from homeassistant.components.bluetooth import BluetoothServiceInfoBleak from tests.components.bluetooth import generate_advertisement_data, generate_ble_device diff --git a/tests/components/fjaraskupan/conftest.py b/tests/components/fjaraskupan/conftest.py index 46ff5ae167a..85493157a3c 100644 --- a/tests/components/fjaraskupan/conftest.py +++ b/tests/components/fjaraskupan/conftest.py @@ -1,4 +1,5 @@ """Standard fixtures for the Fjäråskupan integration.""" + from __future__ import annotations import pytest diff --git a/tests/components/fjaraskupan/test_config_flow.py b/tests/components/fjaraskupan/test_config_flow.py index 13dc643a2a1..b6da1fcf5b5 100644 --- a/tests/components/fjaraskupan/test_config_flow.py +++ b/tests/components/fjaraskupan/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Fjäråskupan config flow.""" + from __future__ import annotations from unittest.mock import patch diff --git a/tests/components/flexit_bacnet/__init__.py b/tests/components/flexit_bacnet/__init__.py index e934f3c7e5f..c43f55dc33b 100644 --- a/tests/components/flexit_bacnet/__init__.py +++ b/tests/components/flexit_bacnet/__init__.py @@ -1,4 +1,5 @@ """Tests for the Flexit Nordic (BACnet) integration.""" + from unittest.mock import patch from homeassistant.const import Platform diff --git a/tests/components/flexit_bacnet/conftest.py b/tests/components/flexit_bacnet/conftest.py index cfcecebd19b..fb4b2237833 100644 --- a/tests/components/flexit_bacnet/conftest.py +++ b/tests/components/flexit_bacnet/conftest.py @@ -1,4 +1,5 @@ """Configuration for Flexit Nordic (BACnet) tests.""" + from collections.abc import Generator from unittest.mock import AsyncMock, patch @@ -31,12 +32,15 @@ async def flow_id(hass: HomeAssistant) -> str: def mock_flexit_bacnet() -> Generator[AsyncMock, None, None]: """Mock data from the device.""" flexit_bacnet = AsyncMock(spec=FlexitBACnet) - with patch( - "homeassistant.components.flexit_bacnet.config_flow.FlexitBACnet", - return_value=flexit_bacnet, - ), patch( - "homeassistant.components.flexit_bacnet.coordinator.FlexitBACnet", - return_value=flexit_bacnet, + with ( + patch( + "homeassistant.components.flexit_bacnet.config_flow.FlexitBACnet", + return_value=flexit_bacnet, + ), + patch( + "homeassistant.components.flexit_bacnet.coordinator.FlexitBACnet", + return_value=flexit_bacnet, + ), ): flexit_bacnet.serial_number = "0000-0001" flexit_bacnet.device_name = "Device Name" diff --git a/tests/components/flexit_bacnet/snapshots/test_binary_sensor.ambr b/tests/components/flexit_bacnet/snapshots/test_binary_sensor.ambr index 6d3ef4c1940..f983d834927 100644 --- a/tests/components/flexit_bacnet/snapshots/test_binary_sensor.ambr +++ b/tests/components/flexit_bacnet/snapshots/test_binary_sensor.ambr @@ -41,6 +41,7 @@ 'context': , 'entity_id': 'binary_sensor.device_name_air_filter_polluted', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }) diff --git a/tests/components/flexit_bacnet/snapshots/test_climate.ambr b/tests/components/flexit_bacnet/snapshots/test_climate.ambr index 6d5ef2251b8..551c5363e98 100644 --- a/tests/components/flexit_bacnet/snapshots/test_climate.ambr +++ b/tests/components/flexit_bacnet/snapshots/test_climate.ambr @@ -24,6 +24,7 @@ 'context': , 'entity_id': 'climate.device_name', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'fan_only', }) diff --git a/tests/components/flexit_bacnet/snapshots/test_number.ambr b/tests/components/flexit_bacnet/snapshots/test_number.ambr index 1318166fb87..008046bf512 100644 --- a/tests/components/flexit_bacnet/snapshots/test_number.ambr +++ b/tests/components/flexit_bacnet/snapshots/test_number.ambr @@ -51,6 +51,7 @@ 'context': , 'entity_id': 'number.device_name_away_extract_fan_setpoint', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '30', }) @@ -107,6 +108,7 @@ 'context': , 'entity_id': 'number.device_name_away_supply_fan_setpoint', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '40', }) @@ -163,6 +165,7 @@ 'context': , 'entity_id': 'number.device_name_cooker_hood_extract_fan_setpoint', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '90', }) @@ -219,6 +222,7 @@ 'context': , 'entity_id': 'number.device_name_cooker_hood_supply_fan_setpoint', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '100', }) @@ -275,6 +279,7 @@ 'context': , 'entity_id': 'number.device_name_fireplace_extract_fan_setpoint', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '10', }) @@ -331,6 +336,7 @@ 'context': , 'entity_id': 'number.device_name_fireplace_supply_fan_setpoint', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '20', }) @@ -387,6 +393,7 @@ 'context': , 'entity_id': 'number.device_name_high_extract_fan_setpoint', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '70', }) @@ -443,6 +450,7 @@ 'context': , 'entity_id': 'number.device_name_high_supply_fan_setpoint', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '80', }) @@ -499,6 +507,7 @@ 'context': , 'entity_id': 'number.device_name_home_extract_fan_setpoint', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '50', }) @@ -555,6 +564,7 @@ 'context': , 'entity_id': 'number.device_name_home_supply_fan_setpoint', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '60', }) diff --git a/tests/components/flexit_bacnet/snapshots/test_sensor.ambr b/tests/components/flexit_bacnet/snapshots/test_sensor.ambr index 55d1c525be6..2c65bd53a6e 100644 --- a/tests/components/flexit_bacnet/snapshots/test_sensor.ambr +++ b/tests/components/flexit_bacnet/snapshots/test_sensor.ambr @@ -47,6 +47,7 @@ 'context': , 'entity_id': 'sensor.device_name_air_filter_operating_time', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '8820.0', }) @@ -97,6 +98,7 @@ 'context': , 'entity_id': 'sensor.device_name_electric_heater_power', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0.396365851163864', }) @@ -146,6 +148,7 @@ 'context': , 'entity_id': 'sensor.device_name_exhaust_air_fan', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '2606', }) @@ -195,6 +198,7 @@ 'context': , 'entity_id': 'sensor.device_name_exhaust_air_fan_control_signal', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '70', }) @@ -242,6 +246,7 @@ 'context': , 'entity_id': 'sensor.device_name_exhaust_air_temperature', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '-3.3', }) @@ -289,6 +294,7 @@ 'context': , 'entity_id': 'sensor.device_name_extract_air_temperature', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '19.0', }) @@ -342,6 +348,7 @@ 'context': , 'entity_id': 'sensor.device_name_fireplace_ventilation_remaining_duration', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '10.0', }) @@ -391,6 +398,7 @@ 'context': , 'entity_id': 'sensor.device_name_heat_exchanger_efficiency', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '81', }) @@ -440,6 +448,7 @@ 'context': , 'entity_id': 'sensor.device_name_heat_exchanger_speed', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '100', }) @@ -487,6 +496,7 @@ 'context': , 'entity_id': 'sensor.device_name_outside_air_temperature', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '-8.6', }) @@ -540,6 +550,7 @@ 'context': , 'entity_id': 'sensor.device_name_rapid_ventilation_remaining_duration', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '30.0', }) @@ -587,6 +598,7 @@ 'context': , 'entity_id': 'sensor.device_name_room_temperature', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '19.0', }) @@ -636,6 +648,7 @@ 'context': , 'entity_id': 'sensor.device_name_supply_air_fan', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '2784', }) @@ -685,6 +698,7 @@ 'context': , 'entity_id': 'sensor.device_name_supply_air_fan_control_signal', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '74', }) @@ -732,6 +746,7 @@ 'context': , 'entity_id': 'sensor.device_name_supply_air_temperature', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '19.1', }) diff --git a/tests/components/flexit_bacnet/snapshots/test_switch.ambr b/tests/components/flexit_bacnet/snapshots/test_switch.ambr index 5a7350f9728..d054608f1f7 100644 --- a/tests/components/flexit_bacnet/snapshots/test_switch.ambr +++ b/tests/components/flexit_bacnet/snapshots/test_switch.ambr @@ -41,6 +41,7 @@ 'context': , 'entity_id': 'switch.device_name_electric_heater', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }) @@ -54,6 +55,7 @@ 'context': , 'entity_id': 'switch.device_name_electric_heater', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }) diff --git a/tests/components/flexit_bacnet/test_binary_sensor.py b/tests/components/flexit_bacnet/test_binary_sensor.py index df363086f63..649eebaec2c 100644 --- a/tests/components/flexit_bacnet/test_binary_sensor.py +++ b/tests/components/flexit_bacnet/test_binary_sensor.py @@ -1,4 +1,5 @@ """Tests for the Flexit Nordic (BACnet) binary sensor entities.""" + from unittest.mock import AsyncMock from syrupy.assertion import SnapshotAssertion diff --git a/tests/components/flexit_bacnet/test_climate.py b/tests/components/flexit_bacnet/test_climate.py index 077aee019e7..6c88e6e69d2 100644 --- a/tests/components/flexit_bacnet/test_climate.py +++ b/tests/components/flexit_bacnet/test_climate.py @@ -1,4 +1,5 @@ """Tests for the Flexit Nordic (BACnet) climate entity.""" + from unittest.mock import AsyncMock from syrupy.assertion import SnapshotAssertion diff --git a/tests/components/flexit_bacnet/test_config_flow.py b/tests/components/flexit_bacnet/test_config_flow.py index 860d25e4b75..7d864a80c2d 100644 --- a/tests/components/flexit_bacnet/test_config_flow.py +++ b/tests/components/flexit_bacnet/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Flexit Nordic (BACnet) config flow.""" + import asyncio.exceptions from flexit_bacnet import DecodingError diff --git a/tests/components/flexit_bacnet/test_init.py b/tests/components/flexit_bacnet/test_init.py index 71f79f54302..0741120c1ad 100644 --- a/tests/components/flexit_bacnet/test_init.py +++ b/tests/components/flexit_bacnet/test_init.py @@ -1,4 +1,5 @@ """Tests for the Flexit Nordic (BACnet) __init__.""" + from flexit_bacnet import DecodingError from homeassistant.components.flexit_bacnet.const import DOMAIN diff --git a/tests/components/flexit_bacnet/test_number.py b/tests/components/flexit_bacnet/test_number.py index 6f23ee11866..2aa3c9abcff 100644 --- a/tests/components/flexit_bacnet/test_number.py +++ b/tests/components/flexit_bacnet/test_number.py @@ -1,4 +1,5 @@ """Tests for the Flexit Nordic (BACnet) number entities.""" + from unittest.mock import AsyncMock from flexit_bacnet import DecodingError diff --git a/tests/components/flexit_bacnet/test_sensor.py b/tests/components/flexit_bacnet/test_sensor.py index 2285b4c8692..460f2cf5728 100644 --- a/tests/components/flexit_bacnet/test_sensor.py +++ b/tests/components/flexit_bacnet/test_sensor.py @@ -1,4 +1,5 @@ """Tests for the Flexit Nordic (BACnet) sensor entities.""" + from unittest.mock import AsyncMock from syrupy.assertion import SnapshotAssertion diff --git a/tests/components/flexit_bacnet/test_switch.py b/tests/components/flexit_bacnet/test_switch.py index 7c08fc2a024..19c7dfc804e 100644 --- a/tests/components/flexit_bacnet/test_switch.py +++ b/tests/components/flexit_bacnet/test_switch.py @@ -1,4 +1,5 @@ """Tests for the Flexit Nordic (BACnet) switch entities.""" + from unittest.mock import AsyncMock from flexit_bacnet import DecodingError diff --git a/tests/components/flic/test_binary_sensor.py b/tests/components/flic/test_binary_sensor.py index 2fa703348f9..d2584e4f5a2 100644 --- a/tests/components/flic/test_binary_sensor.py +++ b/tests/components/flic/test_binary_sensor.py @@ -1,4 +1,5 @@ """Tests for Flic button integration.""" + from unittest import mock from homeassistant.core import HomeAssistant diff --git a/tests/components/flick_electric/test_config_flow.py b/tests/components/flick_electric/test_config_flow.py index bd77f1b6002..9635f3a1526 100644 --- a/tests/components/flick_electric/test_config_flow.py +++ b/tests/components/flick_electric/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Flick Electric config flow.""" + from unittest.mock import patch from pyflick.authentication import AuthException @@ -30,13 +31,16 @@ async def test_form(hass: HomeAssistant) -> None: assert result["type"] == "form" assert result["errors"] == {} - with patch( - "homeassistant.components.flick_electric.config_flow.SimpleFlickAuth.async_get_access_token", - return_value="123456789abcdef", - ), patch( - "homeassistant.components.flick_electric.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.flick_electric.config_flow.SimpleFlickAuth.async_get_access_token", + return_value="123456789abcdef", + ), + patch( + "homeassistant.components.flick_electric.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], CONF, diff --git a/tests/components/flipr/test_binary_sensor.py b/tests/components/flipr/test_binary_sensor.py index fa938521d3b..971b5b046b3 100644 --- a/tests/components/flipr/test_binary_sensor.py +++ b/tests/components/flipr/test_binary_sensor.py @@ -1,4 +1,5 @@ """Test the Flipr binary sensor.""" + from datetime import datetime from unittest.mock import patch diff --git a/tests/components/flipr/test_config_flow.py b/tests/components/flipr/test_config_flow.py index 08fb71da1e4..4ee6d85cead 100644 --- a/tests/components/flipr/test_config_flow.py +++ b/tests/components/flipr/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Flipr config flow.""" + from unittest.mock import patch import pytest diff --git a/tests/components/flipr/test_init.py b/tests/components/flipr/test_init.py index c1c5c0086e7..8300ac185ba 100644 --- a/tests/components/flipr/test_init.py +++ b/tests/components/flipr/test_init.py @@ -1,4 +1,5 @@ """Tests for init methods.""" + from unittest.mock import patch from homeassistant.components.flipr.const import CONF_FLIPR_ID, DOMAIN diff --git a/tests/components/flipr/test_sensor.py b/tests/components/flipr/test_sensor.py index 54684722802..31eb075469d 100644 --- a/tests/components/flipr/test_sensor.py +++ b/tests/components/flipr/test_sensor.py @@ -1,4 +1,5 @@ """Test the Flipr sensor.""" + from datetime import datetime from unittest.mock import patch @@ -7,7 +8,6 @@ from flipr_api.exceptions import FliprError from homeassistant.components.flipr.const import CONF_FLIPR_ID, DOMAIN from homeassistant.components.sensor import ATTR_STATE_CLASS, SensorStateClass from homeassistant.const import ( - ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, CONF_EMAIL, CONF_PASSWORD, @@ -61,42 +61,36 @@ async def test_sensors(hass: HomeAssistant, entity_registry: er.EntityRegistry) state = hass.states.get("sensor.flipr_myfliprid_ph") assert state - assert state.attributes.get(ATTR_ICON) == "mdi:pool" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert state.state == "7.03" state = hass.states.get("sensor.flipr_myfliprid_water_temperature") assert state - assert state.attributes.get(ATTR_ICON) is None assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert state.state == "10.5" state = hass.states.get("sensor.flipr_myfliprid_last_measured") assert state - assert state.attributes.get(ATTR_ICON) is None assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None assert state.attributes.get(ATTR_STATE_CLASS) is None assert state.state == "2021-02-15T09:10:32+00:00" state = hass.states.get("sensor.flipr_myfliprid_red_ox") assert state - assert state.attributes.get(ATTR_ICON) == "mdi:pool" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "mV" assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert state.state == "657.58" state = hass.states.get("sensor.flipr_myfliprid_chlorine") assert state - assert state.attributes.get(ATTR_ICON) == "mdi:pool" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "mV" assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert state.state == "0.23654886" state = hass.states.get("sensor.flipr_myfliprid_battery") assert state - assert state.attributes.get(ATTR_ICON) is None assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert state.state == "95.0" diff --git a/tests/components/flo/common.py b/tests/components/flo/common.py index d4018aae090..9ec1834b0c2 100644 --- a/tests/components/flo/common.py +++ b/tests/components/flo/common.py @@ -1,4 +1,5 @@ """Define common test utilities.""" + TEST_ACCOUNT_ID = "aabbccdd" TEST_DEVICE_ID = "98765" TEST_EMAIL_ADDRESS = "email@address.com" diff --git a/tests/components/flo/conftest.py b/tests/components/flo/conftest.py index 1484b10eae2..3cd666b7462 100644 --- a/tests/components/flo/conftest.py +++ b/tests/components/flo/conftest.py @@ -1,4 +1,5 @@ """Define fixtures available for all tests.""" + from http import HTTPStatus import json import time diff --git a/tests/components/flo/test_binary_sensor.py b/tests/components/flo/test_binary_sensor.py index b94f182f3bf..d3032cde1b5 100644 --- a/tests/components/flo/test_binary_sensor.py +++ b/tests/components/flo/test_binary_sensor.py @@ -1,4 +1,5 @@ """Test Flo by Moen binary sensor entities.""" + from homeassistant.components.flo.const import DOMAIN as FLO_DOMAIN from homeassistant.const import ( ATTR_FRIENDLY_NAME, diff --git a/tests/components/flo/test_config_flow.py b/tests/components/flo/test_config_flow.py index 703689e7c36..f5a730a2056 100644 --- a/tests/components/flo/test_config_flow.py +++ b/tests/components/flo/test_config_flow.py @@ -1,4 +1,5 @@ """Test the flo config flow.""" + from http import HTTPStatus import json import time diff --git a/tests/components/flo/test_device.py b/tests/components/flo/test_device.py index 34abfef0e72..c1c9222c723 100644 --- a/tests/components/flo/test_device.py +++ b/tests/components/flo/test_device.py @@ -1,4 +1,5 @@ """Define tests for device-related endpoints.""" + from datetime import timedelta from unittest.mock import patch diff --git a/tests/components/flo/test_init.py b/tests/components/flo/test_init.py index 44f31bdb738..599a91b80fb 100644 --- a/tests/components/flo/test_init.py +++ b/tests/components/flo/test_init.py @@ -1,4 +1,5 @@ """Test init.""" + from homeassistant.components.flo.const import DOMAIN as FLO_DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant diff --git a/tests/components/flo/test_sensor.py b/tests/components/flo/test_sensor.py index 14a36b9e032..5fe388c62e1 100644 --- a/tests/components/flo/test_sensor.py +++ b/tests/components/flo/test_sensor.py @@ -1,4 +1,5 @@ """Test Flo by Moen sensor entities.""" + from homeassistant.components.flo.const import DOMAIN as FLO_DOMAIN from homeassistant.components.sensor import ATTR_STATE_CLASS, SensorStateClass from homeassistant.const import ATTR_ENTITY_ID, CONF_PASSWORD, CONF_USERNAME diff --git a/tests/components/flo/test_services.py b/tests/components/flo/test_services.py index d38c5c45031..c65aa7937ee 100644 --- a/tests/components/flo/test_services.py +++ b/tests/components/flo/test_services.py @@ -1,4 +1,5 @@ """Test the services for the Flo by Moen integration.""" + import pytest from voluptuous.error import MultipleInvalid diff --git a/tests/components/flo/test_switch.py b/tests/components/flo/test_switch.py index 93996a35a98..85f7ea0f317 100644 --- a/tests/components/flo/test_switch.py +++ b/tests/components/flo/test_switch.py @@ -1,4 +1,5 @@ """Tests for the switch domain for Flo by Moen.""" + from homeassistant.components.flo.const import DOMAIN as FLO_DOMAIN from homeassistant.components.switch import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, STATE_OFF, STATE_ON diff --git a/tests/components/flume/test_config_flow.py b/tests/components/flume/test_config_flow.py index f5315e07043..8fa66c03258 100644 --- a/tests/components/flume/test_config_flow.py +++ b/tests/components/flume/test_config_flow.py @@ -1,4 +1,5 @@ """Test the flume config flow.""" + from unittest.mock import MagicMock, patch import requests.exceptions @@ -33,16 +34,20 @@ async def test_form(hass: HomeAssistant) -> None: mock_flume_device_list = _get_mocked_flume_device_list() - with patch( - "homeassistant.components.flume.config_flow.FlumeAuth", - return_value=True, - ), patch( - "homeassistant.components.flume.config_flow.FlumeDeviceList", - return_value=mock_flume_device_list, - ), patch( - "homeassistant.components.flume.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.flume.config_flow.FlumeAuth", + return_value=True, + ), + patch( + "homeassistant.components.flume.config_flow.FlumeDeviceList", + return_value=mock_flume_device_list, + ), + patch( + "homeassistant.components.flume.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -71,12 +76,15 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch( - "homeassistant.components.flume.config_flow.FlumeAuth", - return_value=True, - ), patch( - "homeassistant.components.flume.config_flow.FlumeDeviceList", - side_effect=Exception, + with ( + patch( + "homeassistant.components.flume.config_flow.FlumeAuth", + return_value=True, + ), + patch( + "homeassistant.components.flume.config_flow.FlumeDeviceList", + side_effect=Exception, + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -97,12 +105,15 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch( - "homeassistant.components.flume.config_flow.FlumeAuth", - return_value=True, - ), patch( - "homeassistant.components.flume.config_flow.FlumeDeviceList", - side_effect=requests.exceptions.ConnectionError(), + with ( + patch( + "homeassistant.components.flume.config_flow.FlumeAuth", + return_value=True, + ), + patch( + "homeassistant.components.flume.config_flow.FlumeDeviceList", + side_effect=requests.exceptions.ConnectionError(), + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -139,12 +150,15 @@ async def test_reauth(hass: HomeAssistant) -> None: assert result["type"] == "form" assert result["step_id"] == "reauth_confirm" - with patch( - "homeassistant.components.flume.config_flow.FlumeAuth", - return_value=True, - ), patch( - "homeassistant.components.flume.config_flow.FlumeDeviceList", - side_effect=Exception, + with ( + patch( + "homeassistant.components.flume.config_flow.FlumeAuth", + return_value=True, + ), + patch( + "homeassistant.components.flume.config_flow.FlumeDeviceList", + side_effect=Exception, + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -156,12 +170,15 @@ async def test_reauth(hass: HomeAssistant) -> None: assert result2["type"] == "form" assert result2["errors"] == {"password": "invalid_auth"} - with patch( - "homeassistant.components.flume.config_flow.FlumeAuth", - return_value=True, - ), patch( - "homeassistant.components.flume.config_flow.FlumeDeviceList", - side_effect=requests.exceptions.ConnectionError(), + with ( + patch( + "homeassistant.components.flume.config_flow.FlumeAuth", + return_value=True, + ), + patch( + "homeassistant.components.flume.config_flow.FlumeDeviceList", + side_effect=requests.exceptions.ConnectionError(), + ), ): result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], @@ -175,16 +192,20 @@ async def test_reauth(hass: HomeAssistant) -> None: mock_flume_device_list = _get_mocked_flume_device_list() - with patch( - "homeassistant.components.flume.config_flow.FlumeAuth", - return_value=True, - ), patch( - "homeassistant.components.flume.config_flow.FlumeDeviceList", - return_value=mock_flume_device_list, - ), patch( - "homeassistant.components.flume.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.flume.config_flow.FlumeAuth", + return_value=True, + ), + patch( + "homeassistant.components.flume.config_flow.FlumeDeviceList", + return_value=mock_flume_device_list, + ), + patch( + "homeassistant.components.flume.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result4 = await hass.config_entries.flow.async_configure( result3["flow_id"], { diff --git a/tests/components/flux/test_switch.py b/tests/components/flux/test_switch.py index ed8a4756031..a3eeec10fa5 100644 --- a/tests/components/flux/test_switch.py +++ b/tests/components/flux/test_switch.py @@ -1,4 +1,5 @@ """The tests for the Flux switch platform.""" + from unittest.mock import patch from freezegun import freeze_time @@ -21,7 +22,9 @@ from tests.common import ( async_fire_time_changed, async_mock_service, mock_restore_cache, + setup_test_component_platform, ) +from tests.components.light.common import MockLight @pytest.fixture(autouse=True) @@ -135,17 +138,18 @@ async def test_invalid_config_no_lights(hass: HomeAssistant) -> None: async def test_flux_when_switch_is_off( - hass: HomeAssistant, enable_custom_integrations: None + hass: HomeAssistant, + mock_light_entities: list[MockLight], ) -> None: """Test the flux switch when it is off.""" - platform = getattr(hass.components, "test.light") - platform.init() + setup_test_component_platform(hass, light.DOMAIN, mock_light_entities) + assert await async_setup_component( hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) await hass.async_block_till_done() - ent1 = platform.ENTITIES[0] + ent1 = mock_light_entities[0] # Verify initial state of light state = hass.states.get(ent1.entity_id) @@ -162,9 +166,12 @@ async def test_flux_when_switch_is_off( return sunrise_time return sunset_time - with freeze_time(test_time), patch( - "homeassistant.components.flux.switch.get_astral_event_date", - side_effect=event_date, + with ( + freeze_time(test_time), + patch( + "homeassistant.components.flux.switch.get_astral_event_date", + side_effect=event_date, + ), ): turn_on_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON) assert await async_setup_component( @@ -186,17 +193,18 @@ async def test_flux_when_switch_is_off( async def test_flux_before_sunrise( - hass: HomeAssistant, enable_custom_integrations: None + hass: HomeAssistant, + mock_light_entities: list[MockLight], ) -> None: """Test the flux switch before sunrise.""" - platform = getattr(hass.components, "test.light") - platform.init() + setup_test_component_platform(hass, light.DOMAIN, mock_light_entities) + assert await async_setup_component( hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) await hass.async_block_till_done() - ent1 = platform.ENTITIES[0] + ent1 = mock_light_entities[0] # Verify initial state of light state = hass.states.get(ent1.entity_id) @@ -214,9 +222,12 @@ async def test_flux_before_sunrise( return sunset_time await hass.async_block_till_done() - with freeze_time(test_time), patch( - "homeassistant.components.flux.switch.get_astral_event_date", - side_effect=event_date, + with ( + freeze_time(test_time), + patch( + "homeassistant.components.flux.switch.get_astral_event_date", + side_effect=event_date, + ), ): assert await async_setup_component( hass, @@ -245,17 +256,18 @@ async def test_flux_before_sunrise( async def test_flux_before_sunrise_known_location( - hass: HomeAssistant, enable_custom_integrations: None + hass: HomeAssistant, + mock_light_entities: list[MockLight], ) -> None: """Test the flux switch before sunrise.""" - platform = getattr(hass.components, "test.light") - platform.init() + setup_test_component_platform(hass, light.DOMAIN, mock_light_entities) + assert await async_setup_component( hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) await hass.async_block_till_done() - ent1 = platform.ENTITIES[0] + ent1 = mock_light_entities[0] # Verify initial state of light state = hass.states.get(ent1.entity_id) @@ -303,17 +315,18 @@ async def test_flux_before_sunrise_known_location( async def test_flux_after_sunrise_before_sunset( - hass: HomeAssistant, enable_custom_integrations: None + hass: HomeAssistant, + mock_light_entities: list[MockLight], ) -> None: """Test the flux switch after sunrise and before sunset.""" - platform = getattr(hass.components, "test.light") - platform.init() + setup_test_component_platform(hass, light.DOMAIN, mock_light_entities) + assert await async_setup_component( hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) await hass.async_block_till_done() - ent1 = platform.ENTITIES[0] + ent1 = mock_light_entities[0] # Verify initial state of light state = hass.states.get(ent1.entity_id) @@ -330,9 +343,12 @@ async def test_flux_after_sunrise_before_sunset( return sunrise_time return sunset_time - with freeze_time(test_time), patch( - "homeassistant.components.flux.switch.get_astral_event_date", - side_effect=event_date, + with ( + freeze_time(test_time), + patch( + "homeassistant.components.flux.switch.get_astral_event_date", + side_effect=event_date, + ), ): assert await async_setup_component( hass, @@ -361,17 +377,18 @@ async def test_flux_after_sunrise_before_sunset( async def test_flux_after_sunset_before_stop( - hass: HomeAssistant, enable_custom_integrations: None + hass: HomeAssistant, + mock_light_entities: list[MockLight], ) -> None: """Test the flux switch after sunset and before stop.""" - platform = getattr(hass.components, "test.light") - platform.init() + setup_test_component_platform(hass, light.DOMAIN, mock_light_entities) + assert await async_setup_component( hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) await hass.async_block_till_done() - ent1 = platform.ENTITIES[0] + ent1 = mock_light_entities[0] # Verify initial state of light state = hass.states.get(ent1.entity_id) @@ -388,9 +405,12 @@ async def test_flux_after_sunset_before_stop( return sunrise_time return sunset_time - with freeze_time(test_time), patch( - "homeassistant.components.flux.switch.get_astral_event_date", - side_effect=event_date, + with ( + freeze_time(test_time), + patch( + "homeassistant.components.flux.switch.get_astral_event_date", + side_effect=event_date, + ), ): assert await async_setup_component( hass, @@ -420,17 +440,18 @@ async def test_flux_after_sunset_before_stop( async def test_flux_after_stop_before_sunrise( - hass: HomeAssistant, enable_custom_integrations: None + hass: HomeAssistant, + mock_light_entities: list[MockLight], ) -> None: """Test the flux switch after stop and before sunrise.""" - platform = getattr(hass.components, "test.light") - platform.init() + setup_test_component_platform(hass, light.DOMAIN, mock_light_entities) + assert await async_setup_component( hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) await hass.async_block_till_done() - ent1 = platform.ENTITIES[0] + ent1 = mock_light_entities[0] # Verify initial state of light state = hass.states.get(ent1.entity_id) @@ -447,9 +468,12 @@ async def test_flux_after_stop_before_sunrise( return sunrise_time return sunset_time - with freeze_time(test_time), patch( - "homeassistant.components.flux.switch.get_astral_event_date", - side_effect=event_date, + with ( + freeze_time(test_time), + patch( + "homeassistant.components.flux.switch.get_astral_event_date", + side_effect=event_date, + ), ): assert await async_setup_component( hass, @@ -478,17 +502,18 @@ async def test_flux_after_stop_before_sunrise( async def test_flux_with_custom_start_stop_times( - hass: HomeAssistant, enable_custom_integrations: None + hass: HomeAssistant, + mock_light_entities: list[MockLight], ) -> None: """Test the flux with custom start and stop times.""" - platform = getattr(hass.components, "test.light") - platform.init() + setup_test_component_platform(hass, light.DOMAIN, mock_light_entities) + assert await async_setup_component( hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) await hass.async_block_till_done() - ent1 = platform.ENTITIES[0] + ent1 = mock_light_entities[0] # Verify initial state of light state = hass.states.get(ent1.entity_id) @@ -505,9 +530,12 @@ async def test_flux_with_custom_start_stop_times( return sunrise_time return sunset_time - with freeze_time(test_time), patch( - "homeassistant.components.flux.switch.get_astral_event_date", - side_effect=event_date, + with ( + freeze_time(test_time), + patch( + "homeassistant.components.flux.switch.get_astral_event_date", + side_effect=event_date, + ), ): assert await async_setup_component( hass, @@ -538,20 +566,21 @@ async def test_flux_with_custom_start_stop_times( async def test_flux_before_sunrise_stop_next_day( - hass: HomeAssistant, enable_custom_integrations: None + hass: HomeAssistant, + mock_light_entities: list[MockLight], ) -> None: """Test the flux switch before sunrise. This test has the stop_time on the next day (after midnight). """ - platform = getattr(hass.components, "test.light") - platform.init() + setup_test_component_platform(hass, light.DOMAIN, mock_light_entities) + assert await async_setup_component( hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) await hass.async_block_till_done() - ent1 = platform.ENTITIES[0] + ent1 = mock_light_entities[0] # Verify initial state of light state = hass.states.get(ent1.entity_id) @@ -568,9 +597,12 @@ async def test_flux_before_sunrise_stop_next_day( return sunrise_time return sunset_time - with freeze_time(test_time), patch( - "homeassistant.components.flux.switch.get_astral_event_date", - side_effect=event_date, + with ( + freeze_time(test_time), + patch( + "homeassistant.components.flux.switch.get_astral_event_date", + side_effect=event_date, + ), ): assert await async_setup_component( hass, @@ -600,20 +632,21 @@ async def test_flux_before_sunrise_stop_next_day( async def test_flux_after_sunrise_before_sunset_stop_next_day( - hass: HomeAssistant, enable_custom_integrations: None + hass: HomeAssistant, + mock_light_entities: list[MockLight], ) -> None: """Test the flux switch after sunrise and before sunset. This test has the stop_time on the next day (after midnight). """ - platform = getattr(hass.components, "test.light") - platform.init() + setup_test_component_platform(hass, light.DOMAIN, mock_light_entities) + assert await async_setup_component( hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) await hass.async_block_till_done() - ent1 = platform.ENTITIES[0] + ent1 = mock_light_entities[0] # Verify initial state of light state = hass.states.get(ent1.entity_id) @@ -630,9 +663,12 @@ async def test_flux_after_sunrise_before_sunset_stop_next_day( return sunrise_time return sunset_time - with freeze_time(test_time), patch( - "homeassistant.components.flux.switch.get_astral_event_date", - side_effect=event_date, + with ( + freeze_time(test_time), + patch( + "homeassistant.components.flux.switch.get_astral_event_date", + side_effect=event_date, + ), ): assert await async_setup_component( hass, @@ -663,20 +699,22 @@ async def test_flux_after_sunrise_before_sunset_stop_next_day( @pytest.mark.parametrize("x", [0, 1]) async def test_flux_after_sunset_before_midnight_stop_next_day( - hass: HomeAssistant, x, enable_custom_integrations: None + hass: HomeAssistant, + x, + mock_light_entities: list[MockLight], ) -> None: """Test the flux switch after sunset and before stop. This test has the stop_time on the next day (after midnight). """ - platform = getattr(hass.components, "test.light") - platform.init() + setup_test_component_platform(hass, light.DOMAIN, mock_light_entities) + assert await async_setup_component( hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) await hass.async_block_till_done() - ent1 = platform.ENTITIES[0] + ent1 = mock_light_entities[0] # Verify initial state of light state = hass.states.get(ent1.entity_id) @@ -693,9 +731,12 @@ async def test_flux_after_sunset_before_midnight_stop_next_day( return sunrise_time return sunset_time - with freeze_time(test_time), patch( - "homeassistant.components.flux.switch.get_astral_event_date", - side_effect=event_date, + with ( + freeze_time(test_time), + patch( + "homeassistant.components.flux.switch.get_astral_event_date", + side_effect=event_date, + ), ): assert await async_setup_component( hass, @@ -725,20 +766,21 @@ async def test_flux_after_sunset_before_midnight_stop_next_day( async def test_flux_after_sunset_after_midnight_stop_next_day( - hass: HomeAssistant, enable_custom_integrations: None + hass: HomeAssistant, + mock_light_entities: list[MockLight], ) -> None: """Test the flux switch after sunset and before stop. This test has the stop_time on the next day (after midnight). """ - platform = getattr(hass.components, "test.light") - platform.init() + setup_test_component_platform(hass, light.DOMAIN, mock_light_entities) + assert await async_setup_component( hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) await hass.async_block_till_done() - ent1 = platform.ENTITIES[0] + ent1 = mock_light_entities[0] # Verify initial state of light state = hass.states.get(ent1.entity_id) @@ -755,9 +797,12 @@ async def test_flux_after_sunset_after_midnight_stop_next_day( return sunrise_time return sunset_time - with freeze_time(test_time), patch( - "homeassistant.components.flux.switch.get_astral_event_date", - side_effect=event_date, + with ( + freeze_time(test_time), + patch( + "homeassistant.components.flux.switch.get_astral_event_date", + side_effect=event_date, + ), ): assert await async_setup_component( hass, @@ -787,20 +832,21 @@ async def test_flux_after_sunset_after_midnight_stop_next_day( async def test_flux_after_stop_before_sunrise_stop_next_day( - hass: HomeAssistant, enable_custom_integrations: None + hass: HomeAssistant, + mock_light_entities: list[MockLight], ) -> None: """Test the flux switch after stop and before sunrise. This test has the stop_time on the next day (after midnight). """ - platform = getattr(hass.components, "test.light") - platform.init() + setup_test_component_platform(hass, light.DOMAIN, mock_light_entities) + assert await async_setup_component( hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) await hass.async_block_till_done() - ent1 = platform.ENTITIES[0] + ent1 = mock_light_entities[0] # Verify initial state of light state = hass.states.get(ent1.entity_id) @@ -817,9 +863,12 @@ async def test_flux_after_stop_before_sunrise_stop_next_day( return sunrise_time return sunset_time - with freeze_time(test_time), patch( - "homeassistant.components.flux.switch.get_astral_event_date", - side_effect=event_date, + with ( + freeze_time(test_time), + patch( + "homeassistant.components.flux.switch.get_astral_event_date", + side_effect=event_date, + ), ): assert await async_setup_component( hass, @@ -849,17 +898,18 @@ async def test_flux_after_stop_before_sunrise_stop_next_day( async def test_flux_with_custom_colortemps( - hass: HomeAssistant, enable_custom_integrations: None + hass: HomeAssistant, + mock_light_entities: list[MockLight], ) -> None: """Test the flux with custom start and stop colortemps.""" - platform = getattr(hass.components, "test.light") - platform.init() + setup_test_component_platform(hass, light.DOMAIN, mock_light_entities) + assert await async_setup_component( hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) await hass.async_block_till_done() - ent1 = platform.ENTITIES[0] + ent1 = mock_light_entities[0] # Verify initial state of light state = hass.states.get(ent1.entity_id) @@ -876,9 +926,12 @@ async def test_flux_with_custom_colortemps( return sunrise_time return sunset_time - with freeze_time(test_time), patch( - "homeassistant.components.flux.switch.get_astral_event_date", - side_effect=event_date, + with ( + freeze_time(test_time), + patch( + "homeassistant.components.flux.switch.get_astral_event_date", + side_effect=event_date, + ), ): assert await async_setup_component( hass, @@ -910,17 +963,18 @@ async def test_flux_with_custom_colortemps( async def test_flux_with_custom_brightness( - hass: HomeAssistant, enable_custom_integrations: None + hass: HomeAssistant, + mock_light_entities: list[MockLight], ) -> None: """Test the flux with custom start and stop colortemps.""" - platform = getattr(hass.components, "test.light") - platform.init() + setup_test_component_platform(hass, light.DOMAIN, mock_light_entities) + assert await async_setup_component( hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) await hass.async_block_till_done() - ent1 = platform.ENTITIES[0] + ent1 = mock_light_entities[0] # Verify initial state of light state = hass.states.get(ent1.entity_id) @@ -937,9 +991,12 @@ async def test_flux_with_custom_brightness( return sunrise_time return sunset_time - with freeze_time(test_time), patch( - "homeassistant.components.flux.switch.get_astral_event_date", - side_effect=event_date, + with ( + freeze_time(test_time), + patch( + "homeassistant.components.flux.switch.get_astral_event_date", + side_effect=event_date, + ), ): assert await async_setup_component( hass, @@ -970,17 +1027,18 @@ async def test_flux_with_custom_brightness( async def test_flux_with_multiple_lights( - hass: HomeAssistant, enable_custom_integrations: None + hass: HomeAssistant, + mock_light_entities: list[MockLight], ) -> None: """Test the flux switch with multiple light entities.""" - platform = getattr(hass.components, "test.light") - platform.init() + setup_test_component_platform(hass, light.DOMAIN, mock_light_entities) + assert await async_setup_component( hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) await hass.async_block_till_done() - ent1, ent2, ent3 = platform.ENTITIES + ent1, ent2, ent3 = mock_light_entities await hass.services.async_call( light.DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ent2.entity_id}, blocking=True @@ -1014,9 +1072,12 @@ async def test_flux_with_multiple_lights( return sunrise_time return sunset_time - with freeze_time(test_time), patch( - "homeassistant.components.flux.switch.get_astral_event_date", - side_effect=event_date, + with ( + freeze_time(test_time), + patch( + "homeassistant.components.flux.switch.get_astral_event_date", + side_effect=event_date, + ), ): assert await async_setup_component( hass, @@ -1051,17 +1112,18 @@ async def test_flux_with_multiple_lights( async def test_flux_with_mired( - hass: HomeAssistant, enable_custom_integrations: None + hass: HomeAssistant, + mock_light_entities: list[MockLight], ) -> None: """Test the flux switch´s mode mired.""" - platform = getattr(hass.components, "test.light") - platform.init() + setup_test_component_platform(hass, light.DOMAIN, mock_light_entities) + assert await async_setup_component( hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) await hass.async_block_till_done() - ent1 = platform.ENTITIES[0] + ent1 = mock_light_entities[0] # Verify initial state of light state = hass.states.get(ent1.entity_id) @@ -1077,9 +1139,12 @@ async def test_flux_with_mired( return sunrise_time return sunset_time - with freeze_time(test_time), patch( - "homeassistant.components.flux.switch.get_astral_event_date", - side_effect=event_date, + with ( + freeze_time(test_time), + patch( + "homeassistant.components.flux.switch.get_astral_event_date", + side_effect=event_date, + ), ): assert await async_setup_component( hass, @@ -1108,17 +1173,18 @@ async def test_flux_with_mired( async def test_flux_with_rgb( - hass: HomeAssistant, enable_custom_integrations: None + hass: HomeAssistant, + mock_light_entities: list[MockLight], ) -> None: """Test the flux switch´s mode rgb.""" - platform = getattr(hass.components, "test.light") - platform.init() + setup_test_component_platform(hass, light.DOMAIN, mock_light_entities) + assert await async_setup_component( hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) await hass.async_block_till_done() - ent1 = platform.ENTITIES[0] + ent1 = mock_light_entities[0] # Verify initial state of light state = hass.states.get(ent1.entity_id) @@ -1134,9 +1200,12 @@ async def test_flux_with_rgb( return sunrise_time return sunset_time - with freeze_time(test_time), patch( - "homeassistant.components.flux.switch.get_astral_event_date", - side_effect=event_date, + with ( + freeze_time(test_time), + patch( + "homeassistant.components.flux.switch.get_astral_event_date", + side_effect=event_date, + ), ): assert await async_setup_component( hass, diff --git a/tests/components/flux_led/__init__.py b/tests/components/flux_led/__init__.py index 80f072328d6..d1cb892d548 100644 --- a/tests/components/flux_led/__init__.py +++ b/tests/components/flux_led/__init__.py @@ -1,4 +1,5 @@ """Tests for the flux_led integration.""" + from __future__ import annotations import asyncio @@ -240,12 +241,15 @@ def _patch_discovery(device=None, no_device=False): @contextmanager def _patcher(): - with patch( - "homeassistant.components.flux_led.discovery.AIOBulbScanner.async_scan", - new=_discovery, - ), patch( - "homeassistant.components.flux_led.discovery.AIOBulbScanner.getBulbInfo", - return_value=[] if no_device else [device or FLUX_DISCOVERY], + with ( + patch( + "homeassistant.components.flux_led.discovery.AIOBulbScanner.async_scan", + new=_discovery, + ), + patch( + "homeassistant.components.flux_led.discovery.AIOBulbScanner.getBulbInfo", + return_value=[] if no_device else [device or FLUX_DISCOVERY], + ), ): yield diff --git a/tests/components/flux_led/test_button.py b/tests/components/flux_led/test_button.py index 010b7f329ed..105fc57c085 100644 --- a/tests/components/flux_led/test_button.py +++ b/tests/components/flux_led/test_button.py @@ -1,4 +1,5 @@ """Tests for button platform.""" + from homeassistant.components import flux_led from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN from homeassistant.components.flux_led.const import DOMAIN diff --git a/tests/components/flux_led/test_config_flow.py b/tests/components/flux_led/test_config_flow.py index 008661303f1..63a7a671871 100644 --- a/tests/components/flux_led/test_config_flow.py +++ b/tests/components/flux_led/test_config_flow.py @@ -1,4 +1,5 @@ """Define tests for the Flux LED/Magic Home config flow.""" + from __future__ import annotations from unittest.mock import patch @@ -78,11 +79,12 @@ async def test_discovery(hass: HomeAssistant) -> None: assert result2["step_id"] == "pick_device" assert not result2["errors"] - with _patch_discovery(), _patch_wifibulb(), patch( - f"{MODULE}.async_setup", return_value=True - ) as mock_setup, patch( - f"{MODULE}.async_setup_entry", return_value=True - ) as mock_setup_entry: + with ( + _patch_discovery(), + _patch_wifibulb(), + 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: MAC_ADDRESS}, @@ -152,11 +154,12 @@ async def test_discovery_legacy(hass: HomeAssistant) -> None: assert result2["step_id"] == "pick_device" assert not result2["errors"] - with _patch_discovery(), _patch_wifibulb(), patch( - f"{MODULE}.async_setup", return_value=True - ) as mock_setup, patch( - f"{MODULE}.async_setup_entry", return_value=True - ) as mock_setup_entry: + with ( + _patch_discovery(), + _patch_wifibulb(), + 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: MAC_ADDRESS}, @@ -238,9 +241,11 @@ async def test_discovery_with_existing_device_present(hass: HomeAssistant) -> No assert result2["step_id"] == "pick_device" assert not result2["errors"] - with _patch_discovery(), _patch_wifibulb(), patch( - f"{MODULE}.async_setup_entry", return_value=True - ) as mock_setup_entry: + with ( + _patch_discovery(), + _patch_wifibulb(), + 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: MAC_ADDRESS} ) @@ -312,9 +317,12 @@ async def test_manual_working_discovery(hass: HomeAssistant) -> None: assert result2["errors"] == {"base": "cannot_connect"} # Success - with _patch_discovery(), _patch_wifibulb(), patch( - f"{MODULE}.async_setup", return_value=True - ), patch(f"{MODULE}.async_setup_entry", return_value=True): + with ( + _patch_discovery(), + _patch_wifibulb(), + 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} ) @@ -356,9 +364,12 @@ async def test_manual_no_discovery_data(hass: HomeAssistant) -> None: assert result["step_id"] == "user" assert not result["errors"] - with _patch_discovery(no_device=True), _patch_wifibulb(), patch( - f"{MODULE}.async_setup", return_value=True - ), patch(f"{MODULE}.async_setup_entry", return_value=True): + with ( + _patch_discovery(no_device=True), + _patch_wifibulb(), + 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} ) @@ -424,11 +435,14 @@ async def test_discovered_by_discovery(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.FORM assert result["errors"] is None - with _patch_discovery(), _patch_wifibulb(), 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: + with ( + _patch_discovery(), + _patch_wifibulb(), + 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() @@ -460,11 +474,14 @@ async def test_discovered_by_dhcp_udp_responds(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.FORM assert result["errors"] is None - with _patch_discovery(), _patch_wifibulb(), 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: + with ( + _patch_discovery(), + _patch_wifibulb(), + 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() @@ -496,11 +513,14 @@ async def test_discovered_by_dhcp_no_udp_response(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.FORM assert result["errors"] is None - with _patch_discovery(no_device=True), _patch_wifibulb(), 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: + with ( + _patch_discovery(no_device=True), + _patch_wifibulb(), + 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() @@ -528,11 +548,14 @@ async def test_discovered_by_dhcp_partial_udp_response_fallback_tcp( assert result["type"] == FlowResultType.FORM assert result["errors"] is None - with _patch_discovery(device=FLUX_DISCOVERY_PARTIAL), _patch_wifibulb(), 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: + with ( + _patch_discovery(device=FLUX_DISCOVERY_PARTIAL), + _patch_wifibulb(), + 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() diff --git a/tests/components/flux_led/test_diagnostics.py b/tests/components/flux_led/test_diagnostics.py index 4bc53440632..b539d5252e5 100644 --- a/tests/components/flux_led/test_diagnostics.py +++ b/tests/components/flux_led/test_diagnostics.py @@ -1,4 +1,5 @@ """Test flux_led diagnostics.""" + from homeassistant.components.flux_led.const import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component diff --git a/tests/components/flux_led/test_init.py b/tests/components/flux_led/test_init.py index d75644c7599..a42ba5dff37 100644 --- a/tests/components/flux_led/test_init.py +++ b/tests/components/flux_led/test_init.py @@ -1,4 +1,5 @@ """Tests for the flux_led component.""" + from __future__ import annotations from datetime import timedelta @@ -46,11 +47,14 @@ from tests.common import MockConfigEntry, async_fire_time_changed @pytest.mark.usefixtures("mock_single_broadcast_address") async def test_configuring_flux_led_causes_discovery(hass: HomeAssistant) -> None: """Test that specifying empty config does discovery.""" - with patch( - "homeassistant.components.flux_led.discovery.AIOBulbScanner.async_scan" - ) as scan, patch( - "homeassistant.components.flux_led.discovery.AIOBulbScanner.getBulbInfo" - ) as discover: + with ( + patch( + "homeassistant.components.flux_led.discovery.AIOBulbScanner.async_scan" + ) as scan, + patch( + "homeassistant.components.flux_led.discovery.AIOBulbScanner.getBulbInfo" + ) as discover, + ): discover.return_value = [FLUX_DISCOVERY] await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() @@ -67,11 +71,14 @@ async def test_configuring_flux_led_causes_discovery_multiple_addresses( hass: HomeAssistant, ) -> None: """Test that specifying empty config does discovery.""" - with patch( - "homeassistant.components.flux_led.discovery.AIOBulbScanner.async_scan" - ) as scan, patch( - "homeassistant.components.flux_led.discovery.AIOBulbScanner.getBulbInfo" - ) as discover: + with ( + patch( + "homeassistant.components.flux_led.discovery.AIOBulbScanner.async_scan" + ) as scan, + patch( + "homeassistant.components.flux_led.discovery.AIOBulbScanner.getBulbInfo" + ) as discover, + ): discover.return_value = [FLUX_DISCOVERY] await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() @@ -199,13 +206,17 @@ async def test_config_entry_fills_unique_id_with_directed_discovery( nonlocal last_address return [discovery] if last_address == IP_ADDRESS else [] - with patch( - "homeassistant.components.flux_led.discovery.AIOBulbScanner.async_scan", - new=_discovery, - ), patch( - "homeassistant.components.flux_led.discovery.AIOBulbScanner.getBulbInfo", - new=_mock_getBulbInfo, - ), _patch_wifibulb(): + with ( + patch( + "homeassistant.components.flux_led.discovery.AIOBulbScanner.async_scan", + new=_discovery, + ), + patch( + "homeassistant.components.flux_led.discovery.AIOBulbScanner.getBulbInfo", + new=_mock_getBulbInfo, + ), + _patch_wifibulb(), + ): await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() assert config_entry.state == ConfigEntryState.LOADED diff --git a/tests/components/flux_led/test_light.py b/tests/components/flux_led/test_light.py index 6ddb9e1687f..f5a7b310202 100644 --- a/tests/components/flux_led/test_light.py +++ b/tests/components/flux_led/test_light.py @@ -1,4 +1,5 @@ """Tests for light platform.""" + from datetime import timedelta from unittest.mock import AsyncMock, Mock diff --git a/tests/components/flux_led/test_number.py b/tests/components/flux_led/test_number.py index 83bd0d1d517..455bad05029 100644 --- a/tests/components/flux_led/test_number.py +++ b/tests/components/flux_led/test_number.py @@ -1,7 +1,6 @@ """Tests for the flux_led number platform.""" - -from unittest.mock import patch +from datetime import timedelta from flux_led.const import COLOR_MODE_RGB as FLUX_COLOR_MODE_RGB import pytest @@ -25,6 +24,7 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component +import homeassistant.util.dt as dt_util from . import ( DEFAULT_ENTRY_TITLE, @@ -38,7 +38,7 @@ from . import ( async_mock_effect_speed, ) -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_fire_time_changed async def test_effects_speed_unique_id( @@ -269,9 +269,7 @@ async def test_addressable_light_pixel_config(hass: HomeAssistant) -> None: ) # Original addressable model bulb.color_modes = {FLUX_COLOR_MODE_RGB} bulb.color_mode = FLUX_COLOR_MODE_RGB - with patch.object( - flux_number, "DEBOUNCE_TIME", 0 - ), _patch_discovery(), _patch_wifibulb(device=bulb): + with _patch_discovery(), _patch_wifibulb(device=bulb): await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() @@ -292,6 +290,7 @@ async def test_addressable_light_pixel_config(hass: HomeAssistant) -> None: music_segments_entity_id = "number.bulb_rgbcw_ddeeff_music_segments" state = hass.states.get(music_segments_entity_id) assert state.state == "4" + await hass.async_block_till_done(wait_background_tasks=True) with pytest.raises(ValueError): await hass.services.async_call( @@ -307,7 +306,11 @@ async def test_addressable_light_pixel_config(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: pixels_per_segment_entity_id, ATTR_VALUE: 100}, blocking=True, ) - await hass.async_block_till_done() + async_fire_time_changed( + hass, dt_util.utcnow() + timedelta(seconds=flux_number.DEBOUNCE_TIME) + ) + await hass.async_block_till_done(wait_background_tasks=True) + bulb.async_set_device_config.assert_called_with(pixels_per_segment=100) bulb.async_set_device_config.reset_mock() @@ -325,7 +328,10 @@ async def test_addressable_light_pixel_config(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: music_pixels_per_segment_entity_id, ATTR_VALUE: 100}, blocking=True, ) - await hass.async_block_till_done() + async_fire_time_changed( + hass, dt_util.utcnow() + timedelta(seconds=flux_number.DEBOUNCE_TIME) + ) + await hass.async_block_till_done(wait_background_tasks=True) bulb.async_set_device_config.assert_called_with(music_pixels_per_segment=100) bulb.async_set_device_config.reset_mock() @@ -343,7 +349,10 @@ async def test_addressable_light_pixel_config(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: segments_entity_id, ATTR_VALUE: 5}, blocking=True, ) - await hass.async_block_till_done() + async_fire_time_changed( + hass, dt_util.utcnow() + timedelta(seconds=flux_number.DEBOUNCE_TIME) + ) + await hass.async_block_till_done(wait_background_tasks=True) bulb.async_set_device_config.assert_called_with(segments=5) bulb.async_set_device_config.reset_mock() @@ -361,7 +370,10 @@ async def test_addressable_light_pixel_config(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: music_segments_entity_id, ATTR_VALUE: 5}, blocking=True, ) - await hass.async_block_till_done() + async_fire_time_changed( + hass, dt_util.utcnow() + timedelta(seconds=flux_number.DEBOUNCE_TIME) + ) + await hass.async_block_till_done(wait_background_tasks=True) bulb.async_set_device_config.assert_called_with(music_segments=5) bulb.async_set_device_config.reset_mock() @@ -386,9 +398,7 @@ async def test_addressable_light_pixel_config_music_disabled( ) # Original addressable model bulb.color_modes = {FLUX_COLOR_MODE_RGB} bulb.color_mode = FLUX_COLOR_MODE_RGB - with patch.object( - flux_number, "DEBOUNCE_TIME", 0 - ), _patch_discovery(), _patch_wifibulb(device=bulb): + with _patch_discovery(), _patch_wifibulb(device=bulb): await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() diff --git a/tests/components/flux_led/test_select.py b/tests/components/flux_led/test_select.py index 1cdbb9369ab..76512d9dc4c 100644 --- a/tests/components/flux_led/test_select.py +++ b/tests/components/flux_led/test_select.py @@ -1,4 +1,5 @@ """Tests for select platform.""" + from unittest.mock import patch from flux_led.const import ( diff --git a/tests/components/flux_led/test_sensor.py b/tests/components/flux_led/test_sensor.py index b06a6330fde..49f291a916a 100644 --- a/tests/components/flux_led/test_sensor.py +++ b/tests/components/flux_led/test_sensor.py @@ -1,4 +1,5 @@ """Tests for flux_led sensor platform.""" + from homeassistant.components import flux_led from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component diff --git a/tests/components/flux_led/test_switch.py b/tests/components/flux_led/test_switch.py index 5d025a4cab0..ac0aec66c23 100644 --- a/tests/components/flux_led/test_switch.py +++ b/tests/components/flux_led/test_switch.py @@ -1,4 +1,5 @@ """Tests for switch platform.""" + from flux_led.const import MODE_MUSIC from homeassistant.components import flux_led diff --git a/tests/components/folder/test_sensor.py b/tests/components/folder/test_sensor.py index c45fe2ae5f5..ad0969c6a0f 100644 --- a/tests/components/folder/test_sensor.py +++ b/tests/components/folder/test_sensor.py @@ -1,4 +1,5 @@ """The tests for the folder sensor.""" + import os from homeassistant.components.folder.sensor import CONF_FOLDER_PATHS diff --git a/tests/components/folder_watcher/test_init.py b/tests/components/folder_watcher/test_init.py index b09e060725f..2e9eb99f678 100644 --- a/tests/components/folder_watcher/test_init.py +++ b/tests/components/folder_watcher/test_init.py @@ -1,4 +1,5 @@ """The tests for the folder_watcher component.""" + import os from types import SimpleNamespace from unittest.mock import Mock, patch diff --git a/tests/components/foobot/test_sensor.py b/tests/components/foobot/test_sensor.py index 27e56816176..d8beae3b77b 100644 --- a/tests/components/foobot/test_sensor.py +++ b/tests/components/foobot/test_sensor.py @@ -1,4 +1,5 @@ """The tests for the Foobot sensor platform.""" + from http import HTTPStatus import re from unittest.mock import MagicMock diff --git a/tests/components/forecast_solar/test_config_flow.py b/tests/components/forecast_solar/test_config_flow.py index 06aeb94542e..015bd809b20 100644 --- a/tests/components/forecast_solar/test_config_flow.py +++ b/tests/components/forecast_solar/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Forecast.Solar config flow.""" + from unittest.mock import AsyncMock from homeassistant.components.forecast_solar.const import ( diff --git a/tests/components/forecast_solar/test_diagnostics.py b/tests/components/forecast_solar/test_diagnostics.py index e72f2d7d9dc..0e80fba7647 100644 --- a/tests/components/forecast_solar/test_diagnostics.py +++ b/tests/components/forecast_solar/test_diagnostics.py @@ -1,4 +1,5 @@ """Tests for the diagnostics data provided by the Forecast.Solar integration.""" + from syrupy import SnapshotAssertion from homeassistant.core import HomeAssistant diff --git a/tests/components/forecast_solar/test_energy.py b/tests/components/forecast_solar/test_energy.py index 7d3a853b8a7..246ed866506 100644 --- a/tests/components/forecast_solar/test_energy.py +++ b/tests/components/forecast_solar/test_energy.py @@ -1,4 +1,5 @@ """Test forecast solar energy platform.""" + from datetime import UTC, datetime from unittest.mock import MagicMock diff --git a/tests/components/forecast_solar/test_init.py b/tests/components/forecast_solar/test_init.py index 25dcb41c976..b581888547d 100644 --- a/tests/components/forecast_solar/test_init.py +++ b/tests/components/forecast_solar/test_init.py @@ -1,4 +1,5 @@ """Tests for the Forecast.Solar integration.""" + from unittest.mock import MagicMock, patch from forecast_solar import ForecastSolarConnectionError diff --git a/tests/components/forecast_solar/test_sensor.py b/tests/components/forecast_solar/test_sensor.py index 8faec950eb7..f78ca894acb 100644 --- a/tests/components/forecast_solar/test_sensor.py +++ b/tests/components/forecast_solar/test_sensor.py @@ -1,4 +1,5 @@ """Tests for the sensors provided by the Forecast.Solar integration.""" + from unittest.mock import MagicMock import pytest @@ -166,11 +167,11 @@ async def test_sensors( @pytest.mark.parametrize( "entity_id", - ( + [ "sensor.power_production_next_12hours", "sensor.power_production_next_24hours", "sensor.power_production_next_hour", - ), + ], ) async def test_disabled_by_default( hass: HomeAssistant, diff --git a/tests/components/forked_daapd/test_browse_media.py b/tests/components/forked_daapd/test_browse_media.py index 4d15f083591..29923c9f9e9 100644 --- a/tests/components/forked_daapd/test_browse_media.py +++ b/tests/components/forked_daapd/test_browse_media.py @@ -1,4 +1,5 @@ """Media browsing tests for the forked_daapd media player platform.""" + from http import HTTPStatus from unittest.mock import patch diff --git a/tests/components/forked_daapd/test_config_flow.py b/tests/components/forked_daapd/test_config_flow.py index 080e47acc3e..a7f0dc3f603 100644 --- a/tests/components/forked_daapd/test_config_flow.py +++ b/tests/components/forked_daapd/test_config_flow.py @@ -1,4 +1,5 @@ """The config flow tests for the forked_daapd media player platform.""" + from ipaddress import ip_address from unittest.mock import AsyncMock, MagicMock, patch @@ -68,13 +69,16 @@ async def test_show_form(hass: HomeAssistant) -> None: async def test_config_flow(hass: HomeAssistant, config_entry) -> None: """Test that the user step works.""" - with patch( - "homeassistant.components.forked_daapd.config_flow.ForkedDaapdAPI.test_connection", - new=AsyncMock(), - ) as mock_test_connection, patch( - "homeassistant.components.forked_daapd.media_player.ForkedDaapdAPI.get_request", - autospec=True, - ) as mock_get_request: + with ( + patch( + "homeassistant.components.forked_daapd.config_flow.ForkedDaapdAPI.test_connection", + new=AsyncMock(), + ) as mock_test_connection, + patch( + "homeassistant.components.forked_daapd.media_player.ForkedDaapdAPI.get_request", + autospec=True, + ) as mock_get_request, + ): mock_get_request.return_value = SAMPLE_CONFIG mock_test_connection.return_value = ["ok", "My Music on myhost"] config_data = config_entry.data diff --git a/tests/components/forked_daapd/test_media_player.py b/tests/components/forked_daapd/test_media_player.py index ea9cf557570..19488666be7 100644 --- a/tests/components/forked_daapd/test_media_player.py +++ b/tests/components/forked_daapd/test_media_player.py @@ -1,4 +1,5 @@ """The media player tests for the forked_daapd media player platform.""" + from unittest.mock import patch import pytest diff --git a/tests/components/foscam/test_config_flow.py b/tests/components/foscam/test_config_flow.py index 49bffe491af..64ad2b946da 100644 --- a/tests/components/foscam/test_config_flow.py +++ b/tests/components/foscam/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Foscam config flow.""" + from unittest.mock import patch from libpyfoscam.foscam import ( @@ -84,12 +85,15 @@ async def test_user_valid(hass: HomeAssistant) -> None: assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} - with patch( - "homeassistant.components.foscam.config_flow.FoscamCamera", - ) as mock_foscam_camera, patch( - "homeassistant.components.foscam.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.foscam.config_flow.FoscamCamera", + ) as mock_foscam_camera, + patch( + "homeassistant.components.foscam.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): setup_mock_foscam_camera(mock_foscam_camera) result = await hass.config_entries.flow.async_configure( diff --git a/tests/components/freebox/common.py b/tests/components/freebox/common.py index 9f7dfd8f92a..31ee2a474a5 100644 --- a/tests/components/freebox/common.py +++ b/tests/components/freebox/common.py @@ -1,4 +1,5 @@ """Common methods used across tests for Freebox.""" + from unittest.mock import patch from homeassistant.components.freebox.const import DOMAIN diff --git a/tests/components/freebox/conftest.py b/tests/components/freebox/conftest.py index 6042248561c..cf520043755 100644 --- a/tests/components/freebox/conftest.py +++ b/tests/components/freebox/conftest.py @@ -1,4 +1,5 @@ """Test helpers for Freebox.""" + import json from unittest.mock import AsyncMock, PropertyMock, patch @@ -28,8 +29,9 @@ from tests.common import MockConfigEntry @pytest.fixture(autouse=True) def mock_path(): """Mock path lib.""" - with patch("homeassistant.components.freebox.router.Path"), patch( - "homeassistant.components.freebox.router.os.makedirs" + with ( + patch("homeassistant.components.freebox.router.Path"), + patch("homeassistant.components.freebox.router.os.makedirs"), ): yield diff --git a/tests/components/freebox/const.py b/tests/components/freebox/const.py index ae07b39c5e8..5211b793918 100644 --- a/tests/components/freebox/const.py +++ b/tests/components/freebox/const.py @@ -1,6 +1,5 @@ """Test constants.""" - from tests.common import load_json_array_fixture, load_json_object_fixture MOCK_HOST = "myrouter.freeboxos.fr" diff --git a/tests/components/freebox/test_alarm_control_panel.py b/tests/components/freebox/test_alarm_control_panel.py index 44286f18b87..e4ee8f63b2c 100644 --- a/tests/components/freebox/test_alarm_control_panel.py +++ b/tests/components/freebox/test_alarm_control_panel.py @@ -1,4 +1,5 @@ """Tests for the Freebox alarms.""" + from copy import deepcopy from unittest.mock import Mock diff --git a/tests/components/freebox/test_binary_sensor.py b/tests/components/freebox/test_binary_sensor.py index ee07af786be..4950ef27e5f 100644 --- a/tests/components/freebox/test_binary_sensor.py +++ b/tests/components/freebox/test_binary_sensor.py @@ -1,4 +1,5 @@ """Tests for the Freebox binary sensors.""" + from copy import deepcopy from unittest.mock import Mock diff --git a/tests/components/freebox/test_button.py b/tests/components/freebox/test_button.py index 209ab1e9fc2..e2e1c9714a5 100644 --- a/tests/components/freebox/test_button.py +++ b/tests/components/freebox/test_button.py @@ -1,4 +1,5 @@ """Tests for the Freebox buttons.""" + from unittest.mock import ANY, AsyncMock, Mock, patch from pytest_unordered import unordered diff --git a/tests/components/freebox/test_config_flow.py b/tests/components/freebox/test_config_flow.py index c19b3c3f3b2..a7dff79ecfb 100644 --- a/tests/components/freebox/test_config_flow.py +++ b/tests/components/freebox/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for the Freebox config flow.""" + from ipaddress import ip_address from unittest.mock import Mock, patch diff --git a/tests/components/freebox/test_device_tracker.py b/tests/components/freebox/test_device_tracker.py index 6d4ca5fb7ee..405166d6ba2 100644 --- a/tests/components/freebox/test_device_tracker.py +++ b/tests/components/freebox/test_device_tracker.py @@ -1,4 +1,5 @@ """Tests for the Freebox device trackers.""" + from unittest.mock import Mock from freezegun.api import FrozenDateTimeFactory diff --git a/tests/components/freebox/test_init.py b/tests/components/freebox/test_init.py index 9064727fb7f..4be58f247cd 100644 --- a/tests/components/freebox/test_init.py +++ b/tests/components/freebox/test_init.py @@ -1,4 +1,5 @@ """Tests for the Freebox init.""" + from unittest.mock import ANY, Mock, patch from pytest_unordered import unordered diff --git a/tests/components/freebox/test_router.py b/tests/components/freebox/test_router.py index 88cf56de2bb..623f595e1ad 100644 --- a/tests/components/freebox/test_router.py +++ b/tests/components/freebox/test_router.py @@ -1,4 +1,5 @@ """Tests for the Freebox utility methods.""" + import json from unittest.mock import Mock diff --git a/tests/components/freebox/test_sensor.py b/tests/components/freebox/test_sensor.py index 0abdc55b92c..834bafa0e64 100644 --- a/tests/components/freebox/test_sensor.py +++ b/tests/components/freebox/test_sensor.py @@ -1,4 +1,5 @@ """Tests for the Freebox sensors.""" + from copy import deepcopy from unittest.mock import Mock diff --git a/tests/components/freedns/test_init.py b/tests/components/freedns/test_init.py index 9163a8faf2d..bdb60933a19 100644 --- a/tests/components/freedns/test_init.py +++ b/tests/components/freedns/test_init.py @@ -1,4 +1,5 @@ """Test the FreeDNS component.""" + import pytest from homeassistant.components import freedns diff --git a/tests/components/freedompro/conftest.py b/tests/components/freedompro/conftest.py index 30e7968e2fe..27e6c767223 100644 --- a/tests/components/freedompro/conftest.py +++ b/tests/components/freedompro/conftest.py @@ -1,4 +1,5 @@ """Fixtures for Freedompro integration tests.""" + from __future__ import annotations from collections.abc import Generator @@ -27,15 +28,18 @@ def mock_setup_entry() -> Generator[AsyncMock, None, None]: @pytest.fixture(autouse=True) def mock_freedompro(): """Mock freedompro get_list and get_states.""" - with patch( - "homeassistant.components.freedompro.coordinator.get_list", - return_value={ - "state": True, - "devices": DEVICES, - }, - ), patch( - "homeassistant.components.freedompro.coordinator.get_states", - return_value=DEVICES_STATE, + with ( + patch( + "homeassistant.components.freedompro.coordinator.get_list", + return_value={ + "state": True, + "devices": DEVICES, + }, + ), + patch( + "homeassistant.components.freedompro.coordinator.get_states", + return_value=DEVICES_STATE, + ), ): yield @@ -71,15 +75,18 @@ async def init_integration_no_state(hass) -> MockConfigEntry: }, ) - with patch( - "homeassistant.components.freedompro.coordinator.get_list", - return_value={ - "state": True, - "devices": DEVICES, - }, - ), patch( - "homeassistant.components.freedompro.coordinator.get_states", - return_value=[], + with ( + patch( + "homeassistant.components.freedompro.coordinator.get_list", + return_value={ + "state": True, + "devices": DEVICES, + }, + ), + patch( + "homeassistant.components.freedompro.coordinator.get_states", + return_value=[], + ), ): entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) diff --git a/tests/components/freedompro/test_binary_sensor.py b/tests/components/freedompro/test_binary_sensor.py index 84e421a8653..f3bfeb68c6c 100644 --- a/tests/components/freedompro/test_binary_sensor.py +++ b/tests/components/freedompro/test_binary_sensor.py @@ -1,4 +1,5 @@ """Tests for the Freedompro binary sensor.""" + from datetime import timedelta from unittest.mock import patch @@ -11,7 +12,7 @@ from homeassistant.util.dt import utcnow from .conftest import get_states_response_for_uid -from tests.common import async_fire_time_changed +from tests.common import MockConfigEntry, async_fire_time_changed @pytest.mark.parametrize( @@ -47,14 +48,13 @@ async def test_binary_sensor_get_state( hass: HomeAssistant, entity_registry: er.EntityRegistry, device_registry: dr.DeviceRegistry, - init_integration, + init_integration: MockConfigEntry, entity_id: str, uid: str, name: str, model: str, ) -> None: """Test states of the binary_sensor.""" - init_integration device = device_registry.async_get_device(identifiers={("freedompro", uid)}) assert device is not None diff --git a/tests/components/freedompro/test_climate.py b/tests/components/freedompro/test_climate.py index 581c6d05448..9a8f0c5030c 100644 --- a/tests/components/freedompro/test_climate.py +++ b/tests/components/freedompro/test_climate.py @@ -1,4 +1,5 @@ """Tests for the Freedompro climate.""" + from datetime import timedelta from unittest.mock import ANY, patch @@ -23,7 +24,7 @@ from homeassistant.util.dt import utcnow from .conftest import get_states_response_for_uid -from tests.common import async_fire_time_changed +from tests.common import MockConfigEntry, async_fire_time_changed uid = "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*TWMYQKL3UVED4HSIIB9GXJWJZBQCXG-9VE-N2IUAIWI" @@ -32,7 +33,7 @@ async def test_climate_get_state( hass: HomeAssistant, entity_registry: er.EntityRegistry, device_registry: dr.DeviceRegistry, - init_integration, + init_integration: MockConfigEntry, ) -> None: """Test states of the climate.""" device = device_registry.async_get_device(identifiers={("freedompro", uid)}) @@ -87,10 +88,11 @@ async def test_climate_get_state( async def test_climate_set_off( - hass: HomeAssistant, entity_registry: er.EntityRegistry, init_integration + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + init_integration: MockConfigEntry, ) -> None: """Test set off climate.""" - init_integration entity_id = "climate.thermostat" state = hass.states.get(entity_id) @@ -118,10 +120,11 @@ async def test_climate_set_off( async def test_climate_set_unsupported_hvac_mode( - hass: HomeAssistant, entity_registry: er.EntityRegistry, init_integration + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + init_integration: MockConfigEntry, ) -> None: """Test set unsupported hvac mode climate.""" - init_integration entity_id = "climate.thermostat" state = hass.states.get(entity_id) @@ -142,10 +145,11 @@ async def test_climate_set_unsupported_hvac_mode( async def test_climate_set_temperature( - hass: HomeAssistant, entity_registry: er.EntityRegistry, init_integration + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + init_integration: MockConfigEntry, ) -> None: """Test set temperature climate.""" - init_integration entity_id = "climate.thermostat" state = hass.states.get(entity_id) @@ -188,10 +192,11 @@ async def test_climate_set_temperature( async def test_climate_set_temperature_unsupported_hvac_mode( - hass: HomeAssistant, entity_registry: er.EntityRegistry, init_integration + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + init_integration: MockConfigEntry, ) -> None: """Test set temperature climate unsupported hvac mode.""" - init_integration entity_id = "climate.thermostat" state = hass.states.get(entity_id) diff --git a/tests/components/freedompro/test_config_flow.py b/tests/components/freedompro/test_config_flow.py index cab8605d865..a0063f72557 100644 --- a/tests/components/freedompro/test_config_flow.py +++ b/tests/components/freedompro/test_config_flow.py @@ -1,4 +1,5 @@ """Define tests for the Freedompro config flow.""" + from unittest.mock import patch import pytest diff --git a/tests/components/freedompro/test_cover.py b/tests/components/freedompro/test_cover.py index a4c837194fe..ba48da1d1d4 100644 --- a/tests/components/freedompro/test_cover.py +++ b/tests/components/freedompro/test_cover.py @@ -1,4 +1,5 @@ """Tests for the Freedompro cover.""" + from datetime import timedelta from unittest.mock import ANY, patch @@ -20,7 +21,7 @@ from homeassistant.util.dt import utcnow from .conftest import get_states_response_for_uid -from tests.common import async_fire_time_changed +from tests.common import MockConfigEntry, async_fire_time_changed @pytest.mark.parametrize( @@ -38,14 +39,13 @@ async def test_cover_get_state( hass: HomeAssistant, entity_registry: er.EntityRegistry, device_registry: dr.DeviceRegistry, - init_integration, + init_integration: MockConfigEntry, entity_id: str, uid: str, name: str, model: str, ) -> None: """Test states of the cover.""" - init_integration device = device_registry.async_get_device(identifiers={("freedompro", uid)}) assert device is not None @@ -97,14 +97,13 @@ async def test_cover_get_state( async def test_cover_set_position( hass: HomeAssistant, entity_registry: er.EntityRegistry, - init_integration, + init_integration: MockConfigEntry, entity_id: str, uid: str, name: str, model: str, ) -> None: """Test set position of the cover.""" - init_integration state = hass.states.get(entity_id) assert state @@ -152,14 +151,13 @@ async def test_cover_set_position( async def test_cover_close( hass: HomeAssistant, entity_registry: er.EntityRegistry, - init_integration, + init_integration: MockConfigEntry, entity_id: str, uid: str, name: str, model: str, ) -> None: """Test close cover.""" - init_integration states_response = get_states_response_for_uid(uid) states_response[0]["state"]["position"] = 100 @@ -215,14 +213,13 @@ async def test_cover_close( async def test_cover_open( hass: HomeAssistant, entity_registry: er.EntityRegistry, - init_integration, + init_integration: MockConfigEntry, entity_id: str, uid: str, name: str, model: str, ) -> None: """Test open cover.""" - init_integration state = hass.states.get(entity_id) assert state diff --git a/tests/components/freedompro/test_fan.py b/tests/components/freedompro/test_fan.py index 80b1e5613eb..fd1a7fb4399 100644 --- a/tests/components/freedompro/test_fan.py +++ b/tests/components/freedompro/test_fan.py @@ -1,4 +1,5 @@ """Tests for the Freedompro fan.""" + from datetime import timedelta from unittest.mock import ANY, patch @@ -16,7 +17,7 @@ from homeassistant.util.dt import utcnow from .conftest import get_states_response_for_uid -from tests.common import async_fire_time_changed +from tests.common import MockConfigEntry, async_fire_time_changed uid = "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*ILYH1E3DWZOVMNEUIMDYMNLOW-LFRQFDPWWJOVHVDOS" @@ -25,10 +26,9 @@ async def test_fan_get_state( hass: HomeAssistant, entity_registry: er.EntityRegistry, device_registry: dr.DeviceRegistry, - init_integration, + init_integration: MockConfigEntry, ) -> None: """Test states of the fan.""" - init_integration device = device_registry.async_get_device(identifiers={("freedompro", uid)}) assert device is not None @@ -71,10 +71,11 @@ async def test_fan_get_state( async def test_fan_set_off( - hass: HomeAssistant, entity_registry: er.EntityRegistry, init_integration + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + init_integration: MockConfigEntry, ) -> None: """Test turn off the fan.""" - init_integration entity_id = "fan.bedroom" @@ -125,10 +126,11 @@ async def test_fan_set_off( async def test_fan_set_on( - hass: HomeAssistant, entity_registry: er.EntityRegistry, init_integration + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + init_integration: MockConfigEntry, ) -> None: """Test turn on the fan.""" - init_integration entity_id = "fan.bedroom" state = hass.states.get(entity_id) @@ -166,10 +168,11 @@ async def test_fan_set_on( async def test_fan_set_percent( - hass: HomeAssistant, entity_registry: er.EntityRegistry, init_integration + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + init_integration: MockConfigEntry, ) -> None: """Test turn on the fan.""" - init_integration entity_id = "fan.bedroom" state = hass.states.get(entity_id) diff --git a/tests/components/freedompro/test_init.py b/tests/components/freedompro/test_init.py index 5e1b050a4e7..58f9c493582 100644 --- a/tests/components/freedompro/test_init.py +++ b/tests/components/freedompro/test_init.py @@ -1,4 +1,5 @@ """Freedompro component tests.""" + import logging from unittest.mock import patch @@ -13,7 +14,9 @@ LOGGER = logging.getLogger(__name__) ENTITY_ID = f"{DOMAIN}.fake_name" -async def test_async_setup_entry(hass: HomeAssistant, init_integration) -> None: +async def test_async_setup_entry( + hass: HomeAssistant, init_integration: MockConfigEntry +) -> None: """Test a successful setup entry.""" entry = init_integration assert entry is not None @@ -43,7 +46,9 @@ async def test_config_not_ready(hass: HomeAssistant) -> None: assert entry.state == ConfigEntryState.SETUP_RETRY -async def test_unload_entry(hass: HomeAssistant, init_integration) -> None: +async def test_unload_entry( + hass: HomeAssistant, init_integration: MockConfigEntry +) -> None: """Test successful unload of entry.""" entry = init_integration diff --git a/tests/components/freedompro/test_light.py b/tests/components/freedompro/test_light.py index 53cb59d5646..05439adf764 100644 --- a/tests/components/freedompro/test_light.py +++ b/tests/components/freedompro/test_light.py @@ -1,4 +1,5 @@ """Tests for the Freedompro light.""" + from unittest.mock import patch import pytest @@ -13,6 +14,8 @@ from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, STATE_OFF, STA from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er +from tests.common import MockConfigEntry + @pytest.fixture(autouse=True) def mock_freedompro_put_state(): @@ -22,10 +25,11 @@ def mock_freedompro_put_state(): async def test_light_get_state( - hass: HomeAssistant, entity_registry: er.EntityRegistry, init_integration + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + init_integration: MockConfigEntry, ) -> None: """Test states of the light.""" - init_integration entity_id = "light.lightbulb" state = hass.states.get(entity_id) @@ -42,10 +46,11 @@ async def test_light_get_state( async def test_light_set_on( - hass: HomeAssistant, entity_registry: er.EntityRegistry, init_integration + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + init_integration: MockConfigEntry, ) -> None: """Test set on of the light.""" - init_integration entity_id = "light.lightbulb" state = hass.states.get(entity_id) @@ -73,10 +78,11 @@ async def test_light_set_on( async def test_light_set_off( - hass: HomeAssistant, entity_registry: er.EntityRegistry, init_integration + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + init_integration: MockConfigEntry, ) -> None: """Test set off of the light.""" - init_integration entity_id = "light.bedroomlight" state = hass.states.get(entity_id) @@ -104,10 +110,11 @@ async def test_light_set_off( async def test_light_set_brightness( - hass: HomeAssistant, entity_registry: er.EntityRegistry, init_integration + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + init_integration: MockConfigEntry, ) -> None: """Test set brightness of the light.""" - init_integration entity_id = "light.lightbulb" state = hass.states.get(entity_id) @@ -136,10 +143,11 @@ async def test_light_set_brightness( async def test_light_set_hue( - hass: HomeAssistant, entity_registry: er.EntityRegistry, init_integration + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + init_integration: MockConfigEntry, ) -> None: """Test set brightness of the light.""" - init_integration entity_id = "light.lightbulb" state = hass.states.get(entity_id) diff --git a/tests/components/freedompro/test_lock.py b/tests/components/freedompro/test_lock.py index 37145d6fe95..94f5609ee47 100644 --- a/tests/components/freedompro/test_lock.py +++ b/tests/components/freedompro/test_lock.py @@ -1,4 +1,5 @@ """Tests for the Freedompro lock.""" + from datetime import timedelta from unittest.mock import ANY, patch @@ -15,7 +16,7 @@ from homeassistant.util.dt import utcnow from .conftest import get_states_response_for_uid -from tests.common import async_fire_time_changed +from tests.common import MockConfigEntry, async_fire_time_changed uid = "2WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*2VAS3HTWINNZ5N6HVEIPDJ6NX85P2-AM-GSYWUCNPU0" @@ -24,10 +25,9 @@ async def test_lock_get_state( hass: HomeAssistant, entity_registry: er.EntityRegistry, device_registry: dr.DeviceRegistry, - init_integration, + init_integration: MockConfigEntry, ) -> None: """Test states of the lock.""" - init_integration device = device_registry.async_get_device(identifiers={("freedompro", uid)}) assert device is not None @@ -67,10 +67,11 @@ async def test_lock_get_state( async def test_lock_set_unlock( - hass: HomeAssistant, entity_registry: er.EntityRegistry, init_integration + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + init_integration: MockConfigEntry, ) -> None: """Test set on of the lock.""" - init_integration entity_id = "lock.lock" @@ -116,10 +117,11 @@ async def test_lock_set_unlock( async def test_lock_set_lock( - hass: HomeAssistant, entity_registry: er.EntityRegistry, init_integration + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + init_integration: MockConfigEntry, ) -> None: """Test set on of the lock.""" - init_integration entity_id = "lock.lock" state = hass.states.get(entity_id) diff --git a/tests/components/freedompro/test_sensor.py b/tests/components/freedompro/test_sensor.py index c06ce5b0794..c9075299238 100644 --- a/tests/components/freedompro/test_sensor.py +++ b/tests/components/freedompro/test_sensor.py @@ -1,4 +1,5 @@ """Tests for the Freedompro sensor.""" + from datetime import timedelta from unittest.mock import patch @@ -10,7 +11,7 @@ from homeassistant.util.dt import utcnow from .conftest import get_states_response_for_uid -from tests.common import async_fire_time_changed +from tests.common import MockConfigEntry, async_fire_time_changed @pytest.mark.parametrize( @@ -36,13 +37,12 @@ from tests.common import async_fire_time_changed async def test_sensor_get_state( hass: HomeAssistant, entity_registry: er.EntityRegistry, - init_integration, + init_integration: MockConfigEntry, entity_id: str, uid: str, name: str, ) -> None: """Test states of the sensor.""" - init_integration state = hass.states.get(entity_id) assert state diff --git a/tests/components/freedompro/test_switch.py b/tests/components/freedompro/test_switch.py index 7d72a87a7b5..831218550a4 100644 --- a/tests/components/freedompro/test_switch.py +++ b/tests/components/freedompro/test_switch.py @@ -1,4 +1,5 @@ """Tests for the Freedompro switch.""" + from datetime import timedelta from unittest.mock import ANY, patch @@ -11,16 +12,17 @@ from homeassistant.util.dt import utcnow from .conftest import get_states_response_for_uid -from tests.common import async_fire_time_changed +from tests.common import MockConfigEntry, async_fire_time_changed uid = "3WRRJR6RCZQZSND8VP0YTO3YXCSOFPKBMW8T51TU-LQ*1JKU1MVWHQL-Z9SCUS85VFXMRGNDCDNDDUVVDKBU31W" async def test_switch_get_state( - hass: HomeAssistant, entity_registry: er.EntityRegistry, init_integration + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + init_integration: MockConfigEntry, ) -> None: """Test states of the switch.""" - init_integration entity_id = "switch.irrigation_switch" state = hass.states.get(entity_id) @@ -53,10 +55,11 @@ async def test_switch_get_state( async def test_switch_set_off( - hass: HomeAssistant, entity_registry: er.EntityRegistry, init_integration + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + init_integration: MockConfigEntry, ) -> None: """Test set off of the switch.""" - init_integration entity_id = "switch.irrigation_switch" @@ -104,10 +107,11 @@ async def test_switch_set_off( async def test_switch_set_on( - hass: HomeAssistant, entity_registry: er.EntityRegistry, init_integration + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + init_integration: MockConfigEntry, ) -> None: """Test set on of the switch.""" - init_integration entity_id = "switch.irrigation_switch" state = hass.states.get(entity_id) diff --git a/tests/components/fritz/conftest.py b/tests/components/fritz/conftest.py index 08dce14f18d..2e26f67c1eb 100644 --- a/tests/components/fritz/conftest.py +++ b/tests/components/fritz/conftest.py @@ -1,4 +1,5 @@ """Common stuff for Fritz!Tools tests.""" + import logging from unittest.mock import MagicMock, patch diff --git a/tests/components/fritz/const.py b/tests/components/fritz/const.py index d39cb21beea..30c9f9be174 100644 --- a/tests/components/fritz/const.py +++ b/tests/components/fritz/const.py @@ -1,4 +1,5 @@ """Common stuff for Fritz!Tools tests.""" + from homeassistant.components import ssdp from homeassistant.components.fritz.const import DOMAIN from homeassistant.components.ssdp import ATTR_UPNP_FRIENDLY_NAME, ATTR_UPNP_UDN diff --git a/tests/components/fritz/test_button.py b/tests/components/fritz/test_button.py index e375cd4f047..106fb7f9bef 100644 --- a/tests/components/fritz/test_button.py +++ b/tests/components/fritz/test_button.py @@ -1,4 +1,5 @@ """Tests for Fritz!Tools button platform.""" + from unittest.mock import patch import pytest diff --git a/tests/components/fritz/test_config_flow.py b/tests/components/fritz/test_config_flow.py index 5b87d897dd9..fbd9886f468 100644 --- a/tests/components/fritz/test_config_flow.py +++ b/tests/components/fritz/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for Fritz!Tools config flow.""" + import dataclasses from unittest.mock import patch @@ -41,21 +42,26 @@ from tests.common import MockConfigEntry async def test_user(hass: HomeAssistant, fc_class_mock, mock_get_source_ip) -> None: """Test starting a flow by user.""" - with patch( - "homeassistant.components.fritz.config_flow.FritzConnection", - side_effect=fc_class_mock, - ), patch( - "homeassistant.components.fritz.common.FritzBoxTools._update_device_info", - return_value=MOCK_FIRMWARE_INFO, - ), patch( - "homeassistant.components.fritz.async_setup_entry" - ) as mock_setup_entry, patch( - "requests.get", - ) as mock_request_get, patch( - "requests.post", - ) as mock_request_post, patch( - "homeassistant.components.fritz.config_flow.socket.gethostbyname", - return_value=MOCK_IPS["fritz.box"], + with ( + patch( + "homeassistant.components.fritz.config_flow.FritzConnection", + side_effect=fc_class_mock, + ), + patch( + "homeassistant.components.fritz.common.FritzBoxTools._update_device_info", + return_value=MOCK_FIRMWARE_INFO, + ), + patch("homeassistant.components.fritz.async_setup_entry") as mock_setup_entry, + patch( + "requests.get", + ) as mock_request_get, + patch( + "requests.post", + ) as mock_request_post, + patch( + "homeassistant.components.fritz.config_flow.socket.gethostbyname", + return_value=MOCK_IPS["fritz.box"], + ), ): mock_request_get.return_value.status_code = 200 mock_request_get.return_value.content = MOCK_REQUEST @@ -93,19 +99,25 @@ async def test_user_already_configured( mock_config = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) mock_config.add_to_hass(hass) - with patch( - "homeassistant.components.fritz.config_flow.FritzConnection", - side_effect=fc_class_mock, - ), patch( - "homeassistant.components.fritz.common.FritzBoxTools._update_device_info", - return_value=MOCK_FIRMWARE_INFO, - ), patch( - "requests.get", - ) as mock_request_get, patch( - "requests.post", - ) as mock_request_post, patch( - "homeassistant.components.fritz.config_flow.socket.gethostbyname", - return_value=MOCK_IPS["fritz.box"], + with ( + patch( + "homeassistant.components.fritz.config_flow.FritzConnection", + side_effect=fc_class_mock, + ), + patch( + "homeassistant.components.fritz.common.FritzBoxTools._update_device_info", + return_value=MOCK_FIRMWARE_INFO, + ), + patch( + "requests.get", + ) as mock_request_get, + patch( + "requests.post", + ) as mock_request_post, + patch( + "homeassistant.components.fritz.config_flow.socket.gethostbyname", + return_value=MOCK_IPS["fritz.box"], + ), ): mock_request_get.return_value.status_code = 200 mock_request_get.return_value.content = MOCK_REQUEST @@ -206,19 +218,25 @@ async def test_reauth_successful( mock_config = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) mock_config.add_to_hass(hass) - with patch( - "homeassistant.components.fritz.config_flow.FritzConnection", - side_effect=fc_class_mock, - ), patch( - "homeassistant.components.fritz.common.FritzBoxTools._update_device_info", - return_value=MOCK_FIRMWARE_INFO, - ), patch( - "homeassistant.components.fritz.async_setup_entry", - ) as mock_setup_entry, patch( - "requests.get", - ) as mock_request_get, patch( - "requests.post", - ) as mock_request_post: + with ( + patch( + "homeassistant.components.fritz.config_flow.FritzConnection", + side_effect=fc_class_mock, + ), + patch( + "homeassistant.components.fritz.common.FritzBoxTools._update_device_info", + return_value=MOCK_FIRMWARE_INFO, + ), + patch( + "homeassistant.components.fritz.async_setup_entry", + ) as mock_setup_entry, + patch( + "requests.get", + ) as mock_request_get, + patch( + "requests.post", + ) as mock_request_post, + ): mock_request_get.return_value.status_code = 200 mock_request_get.return_value.content = MOCK_REQUEST mock_request_post.return_value.status_code = 200 @@ -301,12 +319,15 @@ async def test_ssdp_already_configured( ) mock_config.add_to_hass(hass) - with patch( - "homeassistant.components.fritz.config_flow.FritzConnection", - side_effect=fc_class_mock, - ), patch( - "homeassistant.components.fritz.config_flow.socket.gethostbyname", - return_value=MOCK_IPS["fritz.box"], + with ( + patch( + "homeassistant.components.fritz.config_flow.FritzConnection", + side_effect=fc_class_mock, + ), + patch( + "homeassistant.components.fritz.config_flow.socket.gethostbyname", + return_value=MOCK_IPS["fritz.box"], + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA @@ -327,12 +348,15 @@ async def test_ssdp_already_configured_host( ) mock_config.add_to_hass(hass) - with patch( - "homeassistant.components.fritz.config_flow.FritzConnection", - side_effect=fc_class_mock, - ), patch( - "homeassistant.components.fritz.config_flow.socket.gethostbyname", - return_value=MOCK_IPS["fritz.box"], + with ( + patch( + "homeassistant.components.fritz.config_flow.FritzConnection", + side_effect=fc_class_mock, + ), + patch( + "homeassistant.components.fritz.config_flow.socket.gethostbyname", + return_value=MOCK_IPS["fritz.box"], + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA @@ -353,12 +377,15 @@ async def test_ssdp_already_configured_host_uuid( ) mock_config.add_to_hass(hass) - with patch( - "homeassistant.components.fritz.config_flow.FritzConnection", - side_effect=fc_class_mock, - ), patch( - "homeassistant.components.fritz.config_flow.socket.gethostbyname", - return_value=MOCK_IPS["fritz.box"], + with ( + patch( + "homeassistant.components.fritz.config_flow.FritzConnection", + side_effect=fc_class_mock, + ), + patch( + "homeassistant.components.fritz.config_flow.socket.gethostbyname", + return_value=MOCK_IPS["fritz.box"], + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA @@ -393,17 +420,19 @@ async def test_ssdp_already_in_progress_host( async def test_ssdp(hass: HomeAssistant, fc_class_mock, mock_get_source_ip) -> None: """Test starting a flow from discovery.""" - with patch( - "homeassistant.components.fritz.config_flow.FritzConnection", - side_effect=fc_class_mock, - ), patch( - "homeassistant.components.fritz.common.FritzBoxTools._update_device_info", - return_value=MOCK_FIRMWARE_INFO, - ), patch( - "homeassistant.components.fritz.async_setup_entry" - ) as mock_setup_entry, patch("requests.get") as mock_request_get, patch( - "requests.post" - ) as mock_request_post: + with ( + patch( + "homeassistant.components.fritz.config_flow.FritzConnection", + side_effect=fc_class_mock, + ), + patch( + "homeassistant.components.fritz.common.FritzBoxTools._update_device_info", + return_value=MOCK_FIRMWARE_INFO, + ), + patch("homeassistant.components.fritz.async_setup_entry") as mock_setup_entry, + patch("requests.get") as mock_request_get, + patch("requests.post") as mock_request_post, + ): mock_request_get.return_value.status_code = 200 mock_request_get.return_value.content = MOCK_REQUEST mock_request_post.return_value.status_code = 200 diff --git a/tests/components/fritz/test_diagnostics.py b/tests/components/fritz/test_diagnostics.py index 9f50a598039..9dc50cc3378 100644 --- a/tests/components/fritz/test_diagnostics.py +++ b/tests/components/fritz/test_diagnostics.py @@ -1,4 +1,5 @@ """Tests for Fritz!Tools diagnostics platform.""" + from __future__ import annotations from homeassistant.components.diagnostics import REDACTED diff --git a/tests/components/fritz/test_image.py b/tests/components/fritz/test_image.py index bd75676da6c..5d6b9265760 100644 --- a/tests/components/fritz/test_image.py +++ b/tests/components/fritz/test_image.py @@ -1,4 +1,5 @@ """Tests for Fritz!Tools image platform.""" + from datetime import timedelta from http import HTTPStatus from unittest.mock import patch @@ -198,7 +199,7 @@ async def test_image_update_unavailable( # fritzbox becomes unavailable fc_class_mock().call_action_side_effect(ReadTimeout) async_fire_time_changed(hass, utcnow() + timedelta(seconds=60)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get("image.mock_title_guestwifi") assert state.state == STATE_UNKNOWN @@ -206,7 +207,7 @@ async def test_image_update_unavailable( # fritzbox is available again fc_class_mock().call_action_side_effect(None) async_fire_time_changed(hass, utcnow() + timedelta(seconds=60)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get("image.mock_title_guestwifi") assert state.state != STATE_UNKNOWN diff --git a/tests/components/fritz/test_init.py b/tests/components/fritz/test_init.py index 2e230eb61a6..0a525192778 100644 --- a/tests/components/fritz/test_init.py +++ b/tests/components/fritz/test_init.py @@ -1,4 +1,5 @@ """Tests for Fritz!Tools.""" + from unittest.mock import patch import pytest diff --git a/tests/components/fritz/test_sensor.py b/tests/components/fritz/test_sensor.py index 9265d1337f0..37116e66719 100644 --- a/tests/components/fritz/test_sensor.py +++ b/tests/components/fritz/test_sensor.py @@ -1,4 +1,5 @@ """Tests for Fritz!Tools sensor platform.""" + from __future__ import annotations from datetime import timedelta @@ -99,7 +100,7 @@ SENSOR_STATES: dict[str, dict[str, Any]] = { async def test_sensor_setup(hass: HomeAssistant, fc_class_mock, fh_class_mock) -> None: - """Test setup of Fritz!Tools sesnors.""" + """Test setup of Fritz!Tools sensors.""" entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) entry.add_to_hass(hass) @@ -123,7 +124,7 @@ async def test_sensor_setup(hass: HomeAssistant, fc_class_mock, fh_class_mock) - async def test_sensor_update_fail( hass: HomeAssistant, fc_class_mock, fh_class_mock ) -> None: - """Test failed update of Fritz!Tools sesnors.""" + """Test failed update of Fritz!Tools sensors.""" entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) entry.add_to_hass(hass) @@ -133,7 +134,7 @@ async def test_sensor_update_fail( fc_class_mock().call_action_side_effect(FritzConnectionException) async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=300)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) sensors = hass.states.async_all(SENSOR_DOMAIN) for sensor in sensors: diff --git a/tests/components/fritz/test_switch.py b/tests/components/fritz/test_switch.py index c8288f0143f..722f16fa0de 100644 --- a/tests/components/fritz/test_switch.py +++ b/tests/components/fritz/test_switch.py @@ -1,4 +1,5 @@ """Tests for Fritz!Tools switch platform.""" + from __future__ import annotations import pytest diff --git a/tests/components/fritzbox/__init__.py b/tests/components/fritzbox/__init__.py index 1faf37c84ee..8d366e39f6d 100644 --- a/tests/components/fritzbox/__init__.py +++ b/tests/components/fritzbox/__init__.py @@ -1,4 +1,5 @@ """Tests for the AVM Fritz!Box integration.""" + from __future__ import annotations from typing import Any diff --git a/tests/components/fritzbox/conftest.py b/tests/components/fritzbox/conftest.py index 1fbaf48de4b..836a8bc127f 100644 --- a/tests/components/fritzbox/conftest.py +++ b/tests/components/fritzbox/conftest.py @@ -1,4 +1,5 @@ """Fixtures for the AVM Fritz!Box integration.""" + from unittest.mock import Mock, patch import pytest @@ -7,8 +8,9 @@ import pytest @pytest.fixture(name="fritz") def fritz_fixture() -> Mock: """Patch libraries.""" - with patch("homeassistant.components.fritzbox.Fritzhome") as fritz, patch( - "homeassistant.components.fritzbox.config_flow.Fritzhome" + with ( + patch("homeassistant.components.fritzbox.Fritzhome") as fritz, + patch("homeassistant.components.fritzbox.config_flow.Fritzhome"), ): fritz.return_value.get_prefixed_host.return_value = "http://1.2.3.4" yield fritz diff --git a/tests/components/fritzbox/const.py b/tests/components/fritzbox/const.py index 1725d974c6f..c2895de6b8e 100644 --- a/tests/components/fritzbox/const.py +++ b/tests/components/fritzbox/const.py @@ -1,4 +1,5 @@ """Constants for fritzbox tests.""" + from homeassistant.components.fritzbox.const import DOMAIN from homeassistant.const import CONF_DEVICES, CONF_HOST, CONF_PASSWORD, CONF_USERNAME diff --git a/tests/components/fritzbox/test_binary_sensor.py b/tests/components/fritzbox/test_binary_sensor.py index 983516bb9c0..3e1a2691f67 100644 --- a/tests/components/fritzbox/test_binary_sensor.py +++ b/tests/components/fritzbox/test_binary_sensor.py @@ -1,4 +1,5 @@ """Tests for AVM Fritz!Box binary sensor component.""" + from datetime import timedelta from unittest import mock from unittest.mock import Mock @@ -103,7 +104,7 @@ async def test_update(hass: HomeAssistant, fritz: Mock) -> None: next_update = dt_util.utcnow() + timedelta(seconds=200) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert fritz().update_devices.call_count == 2 assert fritz().login.call_count == 1 @@ -122,7 +123,7 @@ async def test_update_error(hass: HomeAssistant, fritz: Mock) -> None: next_update = dt_util.utcnow() + timedelta(seconds=200) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert fritz().update_devices.call_count == 2 assert fritz().login.call_count == 1 @@ -145,7 +146,7 @@ async def test_discover_new_device(hass: HomeAssistant, fritz: Mock) -> None: next_update = dt_util.utcnow() + timedelta(seconds=200) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(f"{DOMAIN}.new_device_alarm") assert state diff --git a/tests/components/fritzbox/test_button.py b/tests/components/fritzbox/test_button.py index 8c0bbec573e..89e8d8357dd 100644 --- a/tests/components/fritzbox/test_button.py +++ b/tests/components/fritzbox/test_button.py @@ -1,4 +1,5 @@ """Tests for AVM Fritz!Box templates.""" + from datetime import timedelta from unittest.mock import Mock @@ -64,7 +65,7 @@ async def test_discover_new_device(hass: HomeAssistant, fritz: Mock) -> None: next_update = dt_util.utcnow() + timedelta(seconds=200) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(f"{DOMAIN}.new_template") assert state diff --git a/tests/components/fritzbox/test_climate.py b/tests/components/fritzbox/test_climate.py index a14c53d6529..073a67f22c1 100644 --- a/tests/components/fritzbox/test_climate.py +++ b/tests/components/fritzbox/test_climate.py @@ -1,4 +1,5 @@ """Tests for AVM Fritz!Box climate component.""" + from datetime import timedelta from unittest.mock import Mock, call @@ -144,7 +145,7 @@ async def test_setup(hass: HomeAssistant, fritz: Mock) -> None: next_update = dt_util.utcnow() + timedelta(seconds=200) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(f"{SENSOR_DOMAIN}.{CONF_FAKE_NAME}_next_scheduled_preset") assert state @@ -202,7 +203,7 @@ async def test_update(hass: HomeAssistant, fritz: Mock) -> None: next_update = dt_util.utcnow() + timedelta(seconds=200) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(ENTITY_ID) assert fritz().update_devices.call_count == 2 @@ -242,7 +243,7 @@ async def test_update_error(hass: HomeAssistant, fritz: Mock) -> None: next_update = dt_util.utcnow() + timedelta(seconds=200) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert fritz().update_devices.call_count == 4 assert fritz().login.call_count == 4 @@ -385,7 +386,7 @@ async def test_preset_mode_update(hass: HomeAssistant, fritz: Mock) -> None: next_update = dt_util.utcnow() + timedelta(seconds=200) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(ENTITY_ID) assert fritz().update_devices.call_count == 2 @@ -396,7 +397,7 @@ async def test_preset_mode_update(hass: HomeAssistant, fritz: Mock) -> None: next_update = dt_util.utcnow() + timedelta(seconds=200) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(ENTITY_ID) assert fritz().update_devices.call_count == 3 @@ -421,7 +422,7 @@ async def test_discover_new_device(hass: HomeAssistant, fritz: Mock) -> None: next_update = dt_util.utcnow() + timedelta(seconds=200) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(f"{DOMAIN}.new_climate") assert state diff --git a/tests/components/fritzbox/test_config_flow.py b/tests/components/fritzbox/test_config_flow.py index 674beda13ee..690082085f8 100644 --- a/tests/components/fritzbox/test_config_flow.py +++ b/tests/components/fritzbox/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for AVM Fritz!Box config flow.""" + import dataclasses from unittest import mock from unittest.mock import Mock, patch @@ -55,9 +56,10 @@ MOCK_SSDP_DATA = { @pytest.fixture(name="fritz") def fritz_fixture() -> Mock: """Patch libraries.""" - with patch("homeassistant.components.fritzbox.async_setup_entry"), patch( - "homeassistant.components.fritzbox.config_flow.Fritzhome" - ) as fritz: + with ( + patch("homeassistant.components.fritzbox.async_setup_entry"), + patch("homeassistant.components.fritzbox.config_flow.Fritzhome") as fritz, + ): yield fritz diff --git a/tests/components/fritzbox/test_cover.py b/tests/components/fritzbox/test_cover.py index e3a6d786abf..6c301fc8f46 100644 --- a/tests/components/fritzbox/test_cover.py +++ b/tests/components/fritzbox/test_cover.py @@ -1,4 +1,5 @@ """Tests for AVM Fritz!Box switch component.""" + from datetime import timedelta from unittest.mock import Mock, call @@ -107,7 +108,7 @@ async def test_discover_new_device(hass: HomeAssistant, fritz: Mock) -> None: next_update = dt_util.utcnow() + timedelta(seconds=200) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(f"{DOMAIN}.new_climate") assert state diff --git a/tests/components/fritzbox/test_diagnostics.py b/tests/components/fritzbox/test_diagnostics.py index ec1bcce3979..38aaa623080 100644 --- a/tests/components/fritzbox/test_diagnostics.py +++ b/tests/components/fritzbox/test_diagnostics.py @@ -1,4 +1,5 @@ """Tests for the AVM Fritz!Box integration.""" + from __future__ import annotations from unittest.mock import Mock diff --git a/tests/components/fritzbox/test_init.py b/tests/components/fritzbox/test_init.py index b8273204325..4ee351f7914 100644 --- a/tests/components/fritzbox/test_init.py +++ b/tests/components/fritzbox/test_init.py @@ -1,4 +1,5 @@ """Tests for the AVM Fritz!Box integration.""" + from __future__ import annotations from unittest.mock import Mock, call, patch diff --git a/tests/components/fritzbox/test_light.py b/tests/components/fritzbox/test_light.py index 858b564cd18..45920c7c3ee 100644 --- a/tests/components/fritzbox/test_light.py +++ b/tests/components/fritzbox/test_light.py @@ -1,4 +1,5 @@ """Tests for AVM Fritz!Box light component.""" + from datetime import timedelta from unittest.mock import Mock, call @@ -236,7 +237,7 @@ async def test_update(hass: HomeAssistant, fritz: Mock) -> None: next_update = dt_util.utcnow() + timedelta(seconds=200) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert fritz().update_devices.call_count == 2 assert fritz().login.call_count == 1 @@ -258,7 +259,7 @@ async def test_update_error(hass: HomeAssistant, fritz: Mock) -> None: next_update = dt_util.utcnow() + timedelta(seconds=200) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert fritz().update_devices.call_count == 4 assert fritz().login.call_count == 4 @@ -293,7 +294,7 @@ async def test_discover_new_device(hass: HomeAssistant, fritz: Mock) -> None: next_update = dt_util.utcnow() + timedelta(seconds=200) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(f"{DOMAIN}.new_light") assert state diff --git a/tests/components/fritzbox/test_sensor.py b/tests/components/fritzbox/test_sensor.py index 9fe25d02ed0..63d0b67d7f4 100644 --- a/tests/components/fritzbox/test_sensor.py +++ b/tests/components/fritzbox/test_sensor.py @@ -1,4 +1,5 @@ """Tests for AVM Fritz!Box sensor component.""" + from datetime import timedelta from unittest.mock import Mock @@ -86,7 +87,7 @@ async def test_update(hass: HomeAssistant, fritz: Mock) -> None: next_update = dt_util.utcnow() + timedelta(seconds=200) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert fritz().update_devices.call_count == 2 assert fritz().login.call_count == 1 @@ -104,7 +105,7 @@ async def test_update_error(hass: HomeAssistant, fritz: Mock) -> None: next_update = dt_util.utcnow() + timedelta(seconds=200) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert fritz().update_devices.call_count == 4 assert fritz().login.call_count == 4 @@ -127,7 +128,7 @@ async def test_discover_new_device(hass: HomeAssistant, fritz: Mock) -> None: next_update = dt_util.utcnow() + timedelta(seconds=200) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(f"{DOMAIN}.new_device_temperature") assert state diff --git a/tests/components/fritzbox/test_switch.py b/tests/components/fritzbox/test_switch.py index aefe21e3ffc..417b355b396 100644 --- a/tests/components/fritzbox/test_switch.py +++ b/tests/components/fritzbox/test_switch.py @@ -1,4 +1,5 @@ """Tests for AVM Fritz!Box switch component.""" + from datetime import timedelta from unittest.mock import Mock @@ -150,7 +151,7 @@ async def test_update(hass: HomeAssistant, fritz: Mock) -> None: next_update = dt_util.utcnow() + timedelta(seconds=200) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert fritz().update_devices.call_count == 2 assert fritz().login.call_count == 1 @@ -168,7 +169,7 @@ async def test_update_error(hass: HomeAssistant, fritz: Mock) -> None: next_update = dt_util.utcnow() + timedelta(seconds=200) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert fritz().update_devices.call_count == 4 assert fritz().login.call_count == 4 @@ -206,7 +207,7 @@ async def test_discover_new_device(hass: HomeAssistant, fritz: Mock) -> None: next_update = dt_util.utcnow() + timedelta(seconds=200) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(f"{DOMAIN}.new_switch") assert state diff --git a/tests/components/fritzbox_callmonitor/test_config_flow.py b/tests/components/fritzbox_callmonitor/test_config_flow.py index 386151c31b1..33e2d8fb125 100644 --- a/tests/components/fritzbox_callmonitor/test_config_flow.py +++ b/tests/components/fritzbox_callmonitor/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for fritzbox_callmonitor config flow.""" + from __future__ import annotations from unittest.mock import PropertyMock @@ -92,30 +93,38 @@ async def test_setup_one_phonebook(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" - with patch( - "homeassistant.components.fritzbox_callmonitor.base.FritzPhonebook.__init__", - return_value=None, - ), patch( - "homeassistant.components.fritzbox_callmonitor.base.FritzPhonebook.phonebook_ids", - new_callable=PropertyMock, - return_value=[0], - ), patch( - "homeassistant.components.fritzbox_callmonitor.base.FritzPhonebook.phonebook_info", - return_value=MOCK_PHONEBOOK_INFO_1, - ), patch( - "homeassistant.components.fritzbox_callmonitor.base.FritzPhonebook.modelname", - return_value=MOCK_PHONEBOOK_NAME_1, - ), patch( - "homeassistant.components.fritzbox_callmonitor.config_flow.FritzConnection.__init__", - return_value=None, - ), patch( - "homeassistant.components.fritzbox_callmonitor.config_flow.FritzConnection.updatecheck", - new_callable=PropertyMock, - return_value=MOCK_DEVICE_INFO, - ), patch( - "homeassistant.components.fritzbox_callmonitor.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.fritzbox_callmonitor.base.FritzPhonebook.__init__", + return_value=None, + ), + patch( + "homeassistant.components.fritzbox_callmonitor.base.FritzPhonebook.phonebook_ids", + new_callable=PropertyMock, + return_value=[0], + ), + patch( + "homeassistant.components.fritzbox_callmonitor.base.FritzPhonebook.phonebook_info", + return_value=MOCK_PHONEBOOK_INFO_1, + ), + patch( + "homeassistant.components.fritzbox_callmonitor.base.FritzPhonebook.modelname", + return_value=MOCK_PHONEBOOK_NAME_1, + ), + patch( + "homeassistant.components.fritzbox_callmonitor.config_flow.FritzConnection.__init__", + return_value=None, + ), + patch( + "homeassistant.components.fritzbox_callmonitor.config_flow.FritzConnection.updatecheck", + new_callable=PropertyMock, + return_value=MOCK_DEVICE_INFO, + ), + patch( + "homeassistant.components.fritzbox_callmonitor.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_DATA ) @@ -135,23 +144,29 @@ async def test_setup_multiple_phonebooks(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" - with patch( - "homeassistant.components.fritzbox_callmonitor.base.FritzPhonebook.__init__", - return_value=None, - ), patch( - "homeassistant.components.fritzbox_callmonitor.base.FritzPhonebook.phonebook_ids", - new_callable=PropertyMock, - return_value=[0, 1], - ), patch( - "homeassistant.components.fritzbox_callmonitor.config_flow.FritzConnection.__init__", - return_value=None, - ), patch( - "homeassistant.components.fritzbox_callmonitor.config_flow.FritzConnection.updatecheck", - new_callable=PropertyMock, - return_value=MOCK_DEVICE_INFO, - ), patch( - "homeassistant.components.fritzbox_callmonitor.base.FritzPhonebook.phonebook_info", - side_effect=[MOCK_PHONEBOOK_INFO_1, MOCK_PHONEBOOK_INFO_2], + with ( + patch( + "homeassistant.components.fritzbox_callmonitor.base.FritzPhonebook.__init__", + return_value=None, + ), + patch( + "homeassistant.components.fritzbox_callmonitor.base.FritzPhonebook.phonebook_ids", + new_callable=PropertyMock, + return_value=[0, 1], + ), + patch( + "homeassistant.components.fritzbox_callmonitor.config_flow.FritzConnection.__init__", + return_value=None, + ), + patch( + "homeassistant.components.fritzbox_callmonitor.config_flow.FritzConnection.updatecheck", + new_callable=PropertyMock, + return_value=MOCK_DEVICE_INFO, + ), + patch( + "homeassistant.components.fritzbox_callmonitor.base.FritzPhonebook.phonebook_info", + side_effect=[MOCK_PHONEBOOK_INFO_1, MOCK_PHONEBOOK_INFO_2], + ), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_DATA @@ -161,13 +176,16 @@ async def test_setup_multiple_phonebooks(hass: HomeAssistant) -> None: assert result["step_id"] == "phonebook" assert result["errors"] == {} - with patch( - "homeassistant.components.fritzbox_callmonitor.base.FritzPhonebook.modelname", - return_value=MOCK_PHONEBOOK_NAME_1, - ), patch( - "homeassistant.components.fritzbox_callmonitor.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.fritzbox_callmonitor.base.FritzPhonebook.modelname", + return_value=MOCK_PHONEBOOK_NAME_1, + ), + patch( + "homeassistant.components.fritzbox_callmonitor.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_PHONEBOOK: MOCK_PHONEBOOK_NAME_2}, diff --git a/tests/components/fronius/__init__.py b/tests/components/fronius/__init__.py index 2e053f7ccc5..3757abab928 100644 --- a/tests/components/fronius/__init__.py +++ b/tests/components/fronius/__init__.py @@ -1,4 +1,5 @@ """Tests for the Fronius integration.""" + from __future__ import annotations from collections.abc import Callable @@ -120,7 +121,7 @@ async def enable_all_entities(hass, freezer, config_entry_id, time_till_next_upd for entry in entities if entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION ]: - registry.async_update_entity(entry.entity_id, **{"disabled_by": None}) + registry.async_update_entity(entry.entity_id, disabled_by=None) await hass.async_block_till_done() freezer.tick(time_till_next_update) async_fire_time_changed(hass) diff --git a/tests/components/fronius/test_config_flow.py b/tests/components/fronius/test_config_flow.py index 928bca0eb94..c09baeb2d22 100644 --- a/tests/components/fronius/test_config_flow.py +++ b/tests/components/fronius/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Fronius config flow.""" + from unittest.mock import patch from pyfronius import FroniusError @@ -51,13 +52,16 @@ async def test_form_with_logger(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.FORM assert result["errors"] is None - with patch( - "pyfronius.Fronius.current_logger_info", - return_value=LOGGER_INFO_RETURN_VALUE, - ), patch( - "homeassistant.components.fronius.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "pyfronius.Fronius.current_logger_info", + return_value=LOGGER_INFO_RETURN_VALUE, + ), + patch( + "homeassistant.components.fronius.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -83,16 +87,20 @@ async def test_form_with_inverter(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.FORM assert result["errors"] is None - with patch( - "pyfronius.Fronius.current_logger_info", - side_effect=FroniusError, - ), patch( - "pyfronius.Fronius.inverter_info", - return_value=INVERTER_INFO_RETURN_VALUE, - ), patch( - "homeassistant.components.fronius.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "pyfronius.Fronius.current_logger_info", + side_effect=FroniusError, + ), + patch( + "pyfronius.Fronius.inverter_info", + return_value=INVERTER_INFO_RETURN_VALUE, + ), + patch( + "homeassistant.components.fronius.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -116,12 +124,15 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch( - "pyfronius.Fronius.current_logger_info", - side_effect=FroniusError, - ), patch( - "pyfronius.Fronius.inverter_info", - side_effect=FroniusError, + with ( + patch( + "pyfronius.Fronius.current_logger_info", + side_effect=FroniusError, + ), + patch( + "pyfronius.Fronius.inverter_info", + side_effect=FroniusError, + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -140,12 +151,15 @@ async def test_form_no_device(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch( - "pyfronius.Fronius.current_logger_info", - side_effect=FroniusError, - ), patch( - "pyfronius.Fronius.inverter_info", - return_value={"inverters": []}, + with ( + patch( + "pyfronius.Fronius.current_logger_info", + side_effect=FroniusError, + ), + patch( + "pyfronius.Fronius.inverter_info", + return_value={"inverters": []}, + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -259,11 +273,12 @@ async def test_form_updates_host( async def test_dhcp(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker) -> None: """Test starting a flow from discovery.""" - with patch( - "homeassistant.components.fronius.config_flow.DHCP_REQUEST_DELAY", 0 - ), patch( - "pyfronius.Fronius.current_logger_info", - return_value=LOGGER_INFO_RETURN_VALUE, + with ( + patch("homeassistant.components.fronius.config_flow.DHCP_REQUEST_DELAY", 0), + patch( + "pyfronius.Fronius.current_logger_info", + return_value=LOGGER_INFO_RETURN_VALUE, + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=MOCK_DHCP_DATA @@ -307,14 +322,16 @@ async def test_dhcp_invalid( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test starting a flow from discovery.""" - with patch( - "homeassistant.components.fronius.config_flow.DHCP_REQUEST_DELAY", 0 - ), patch( - "pyfronius.Fronius.current_logger_info", - side_effect=FroniusError, - ), patch( - "pyfronius.Fronius.inverter_info", - side_effect=FroniusError, + with ( + patch("homeassistant.components.fronius.config_flow.DHCP_REQUEST_DELAY", 0), + patch( + "pyfronius.Fronius.current_logger_info", + side_effect=FroniusError, + ), + patch( + "pyfronius.Fronius.inverter_info", + side_effect=FroniusError, + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=MOCK_DHCP_DATA diff --git a/tests/components/fronius/test_coordinator.py b/tests/components/fronius/test_coordinator.py index d4f42fadb06..13a08bbe70e 100644 --- a/tests/components/fronius/test_coordinator.py +++ b/tests/components/fronius/test_coordinator.py @@ -1,4 +1,5 @@ """Test the Fronius update coordinators.""" + from unittest.mock import patch from freezegun.api import FrozenDateTimeFactory diff --git a/tests/components/fronius/test_init.py b/tests/components/fronius/test_init.py index f8d86bac26a..282b2c3fa76 100644 --- a/tests/components/fronius/test_init.py +++ b/tests/components/fronius/test_init.py @@ -1,4 +1,5 @@ """Test the Fronius integration.""" + from datetime import timedelta from unittest.mock import patch diff --git a/tests/components/fronius/test_sensor.py b/tests/components/fronius/test_sensor.py index a8f48ce2e88..f5e77660271 100644 --- a/tests/components/fronius/test_sensor.py +++ b/tests/components/fronius/test_sensor.py @@ -1,4 +1,5 @@ """Tests for the Fronius sensor platform.""" + from freezegun.api import FrozenDateTimeFactory import pytest diff --git a/tests/components/frontend/test_init.py b/tests/components/frontend/test_init.py index f04d4a9bc52..d715eb8859d 100644 --- a/tests/components/frontend/test_init.py +++ b/tests/components/frontend/test_init.py @@ -1,4 +1,5 @@ """The tests for Home Assistant frontend.""" + from http import HTTPStatus import re from typing import Any @@ -15,6 +16,8 @@ from homeassistant.components.frontend import ( DOMAIN, EVENT_PANELS_UPDATED, THEMES_STORAGE_KEY, + async_register_built_in_panel, + async_remove_panel, ) from homeassistant.components.websocket_api.const import TYPE_RESULT from homeassistant.core import HomeAssistant @@ -416,8 +419,8 @@ async def test_get_panels( resp = await mock_http_client.get("/map") assert resp.status == HTTPStatus.NOT_FOUND - hass.components.frontend.async_register_built_in_panel( - "map", "Map", "mdi:tooltip-account", require_admin=True + async_register_built_in_panel( + hass, "map", "Map", "mdi:tooltip-account", require_admin=True ) resp = await mock_http_client.get("/map") @@ -439,7 +442,7 @@ async def test_get_panels( assert msg["result"]["map"]["title"] == "Map" assert msg["result"]["map"]["require_admin"] is True - hass.components.frontend.async_remove_panel("map") + async_remove_panel(hass, "map") resp = await mock_http_client.get("/map") assert resp.status == HTTPStatus.NOT_FOUND @@ -453,12 +456,10 @@ async def test_get_panels_non_admin( """Test get_panels command.""" hass_admin_user.groups = [] - hass.components.frontend.async_register_built_in_panel( - "map", "Map", "mdi:tooltip-account", require_admin=True - ) - hass.components.frontend.async_register_built_in_panel( - "history", "History", "mdi:history" + async_register_built_in_panel( + hass, "map", "Map", "mdi:tooltip-account", require_admin=True ) + async_register_built_in_panel(hass, "history", "History", "mdi:history") await ws_client.send_json({"id": 5, "type": "get_panels"}) diff --git a/tests/components/frontend/test_storage.py b/tests/components/frontend/test_storage.py index 0260907ab2e..8b97fa9ee04 100644 --- a/tests/components/frontend/test_storage.py +++ b/tests/components/frontend/test_storage.py @@ -1,4 +1,5 @@ """The tests for frontend storage.""" + from typing import Any import pytest diff --git a/tests/components/frontier_silicon/conftest.py b/tests/components/frontier_silicon/conftest.py index 1def9b160b2..65a5ede5b26 100644 --- a/tests/components/frontier_silicon/conftest.py +++ b/tests/components/frontier_silicon/conftest.py @@ -1,4 +1,5 @@ """Configuration for frontier_silicon tests.""" + from collections.abc import Generator from unittest.mock import AsyncMock, patch diff --git a/tests/components/frontier_silicon/test_config_flow.py b/tests/components/frontier_silicon/test_config_flow.py index bedac792f02..6a5e62f7dce 100644 --- a/tests/components/frontier_silicon/test_config_flow.py +++ b/tests/components/frontier_silicon/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Frontier Silicon config flow.""" + from unittest.mock import AsyncMock, patch from afsapi import ConnectionError, InvalidPinException, NotImplementedException diff --git a/tests/components/fully_kiosk/conftest.py b/tests/components/fully_kiosk/conftest.py index e409a0a3787..ff732d0e223 100644 --- a/tests/components/fully_kiosk/conftest.py +++ b/tests/components/fully_kiosk/conftest.py @@ -1,4 +1,5 @@ """Fixtures for the Fully Kiosk Browser integration tests.""" + from __future__ import annotations from collections.abc import Generator diff --git a/tests/components/fully_kiosk/test_binary_sensor.py b/tests/components/fully_kiosk/test_binary_sensor.py index cc003199f26..70ae2d15b61 100644 --- a/tests/components/fully_kiosk/test_binary_sensor.py +++ b/tests/components/fully_kiosk/test_binary_sensor.py @@ -1,4 +1,5 @@ """Test the Fully Kiosk Browser binary sensors.""" + from unittest.mock import MagicMock from freezegun.api import FrozenDateTimeFactory @@ -78,7 +79,7 @@ async def test_binary_sensors( mock_fully_kiosk.getDeviceInfo.return_value = {} freezer.tick(UPDATE_INTERVAL) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get("binary_sensor.amazon_fire_plugged_in") assert state @@ -88,7 +89,7 @@ async def test_binary_sensors( mock_fully_kiosk.getDeviceInfo.side_effect = FullyKioskError("error", "status") freezer.tick(UPDATE_INTERVAL) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get("binary_sensor.amazon_fire_plugged_in") assert state diff --git a/tests/components/fully_kiosk/test_button.py b/tests/components/fully_kiosk/test_button.py index f04935aed0e..9bd4c3a897c 100644 --- a/tests/components/fully_kiosk/test_button.py +++ b/tests/components/fully_kiosk/test_button.py @@ -1,4 +1,5 @@ """Test the Fully Kiosk Browser buttons.""" + from unittest.mock import MagicMock import homeassistant.components.button as button diff --git a/tests/components/fully_kiosk/test_diagnostics.py b/tests/components/fully_kiosk/test_diagnostics.py index e48867739e8..31050dad1e0 100644 --- a/tests/components/fully_kiosk/test_diagnostics.py +++ b/tests/components/fully_kiosk/test_diagnostics.py @@ -1,4 +1,5 @@ """Test the Fully Kiosk Browser diagnostics.""" + from unittest.mock import MagicMock from homeassistant.components.diagnostics import REDACTED diff --git a/tests/components/fully_kiosk/test_init.py b/tests/components/fully_kiosk/test_init.py index e74da6434cd..f3fb945c8f0 100644 --- a/tests/components/fully_kiosk/test_init.py +++ b/tests/components/fully_kiosk/test_init.py @@ -1,4 +1,5 @@ """Tests for the Fully Kiosk Browser integration.""" + import json from unittest.mock import MagicMock, patch diff --git a/tests/components/fully_kiosk/test_media_player.py b/tests/components/fully_kiosk/test_media_player.py index 4cae64e641e..b8719a578aa 100644 --- a/tests/components/fully_kiosk/test_media_player.py +++ b/tests/components/fully_kiosk/test_media_player.py @@ -1,4 +1,5 @@ """Test the Fully Kiosk Browser media player.""" + from unittest.mock import MagicMock, Mock, patch from homeassistant.components.fully_kiosk.const import DOMAIN, MEDIA_SUPPORT_FULLYKIOSK diff --git a/tests/components/fully_kiosk/test_number.py b/tests/components/fully_kiosk/test_number.py index 286ca7fc0cb..b4ac50cb076 100644 --- a/tests/components/fully_kiosk/test_number.py +++ b/tests/components/fully_kiosk/test_number.py @@ -1,4 +1,5 @@ """Test the Fully Kiosk Browser number entities.""" + from unittest.mock import MagicMock from homeassistant.components.fully_kiosk.const import DOMAIN, UPDATE_INTERVAL @@ -52,7 +53,7 @@ async def test_numbers( # Test invalid numeric data mock_fully_kiosk.getSettings.return_value = {"screenBrightness": "invalid"} async_fire_time_changed(hass, dt_util.utcnow() + UPDATE_INTERVAL) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get("number.amazon_fire_screen_brightness") assert state @@ -61,7 +62,7 @@ async def test_numbers( # Test unknown/missing data mock_fully_kiosk.getSettings.return_value = {} async_fire_time_changed(hass, dt_util.utcnow() + UPDATE_INTERVAL) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get("number.amazon_fire_screensaver_timer") assert state diff --git a/tests/components/fully_kiosk/test_sensor.py b/tests/components/fully_kiosk/test_sensor.py index 40912f0f568..ebf01f5e3d7 100644 --- a/tests/components/fully_kiosk/test_sensor.py +++ b/tests/components/fully_kiosk/test_sensor.py @@ -1,4 +1,5 @@ """Test the Fully Kiosk Browser sensors.""" + from unittest.mock import MagicMock from freezegun.api import FrozenDateTimeFactory @@ -144,7 +145,7 @@ async def test_sensors_sensors( mock_fully_kiosk.getDeviceInfo.return_value = {} freezer.tick(UPDATE_INTERVAL) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get("sensor.amazon_fire_internal_storage_free_space") assert state @@ -154,7 +155,7 @@ async def test_sensors_sensors( mock_fully_kiosk.getDeviceInfo.side_effect = FullyKioskError("error", "status") freezer.tick(UPDATE_INTERVAL) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get("sensor.amazon_fire_internal_storage_free_space") assert state @@ -180,7 +181,7 @@ async def test_url_sensor_truncating( "currentPage": long_url, } async_fire_time_changed(hass, dt_util.utcnow() + UPDATE_INTERVAL) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get("sensor.amazon_fire_current_page") assert state diff --git a/tests/components/fully_kiosk/test_services.py b/tests/components/fully_kiosk/test_services.py index 6622b400da1..eaf00d74a91 100644 --- a/tests/components/fully_kiosk/test_services.py +++ b/tests/components/fully_kiosk/test_services.py @@ -1,4 +1,5 @@ """Test Fully Kiosk Browser services.""" + from unittest.mock import MagicMock import pytest diff --git a/tests/components/fully_kiosk/test_switch.py b/tests/components/fully_kiosk/test_switch.py index 3c0874384c2..03ac00ef677 100644 --- a/tests/components/fully_kiosk/test_switch.py +++ b/tests/components/fully_kiosk/test_switch.py @@ -1,4 +1,5 @@ """Test the Fully Kiosk Browser switches.""" + from unittest.mock import MagicMock from homeassistant.components.fully_kiosk.const import DOMAIN diff --git a/tests/components/fyta/__init__.py b/tests/components/fyta/__init__.py new file mode 100644 index 00000000000..cdc2cf63b0d --- /dev/null +++ b/tests/components/fyta/__init__.py @@ -0,0 +1 @@ +"""Tests for the Fyta integration.""" diff --git a/tests/components/fyta/conftest.py b/tests/components/fyta/conftest.py new file mode 100644 index 00000000000..e35012a02e8 --- /dev/null +++ b/tests/components/fyta/conftest.py @@ -0,0 +1,33 @@ +"""Test helpers.""" + +from collections.abc import Generator +from unittest.mock import AsyncMock, patch + +import pytest + +from .test_config_flow import ACCESS_TOKEN, EXPIRATION + + +@pytest.fixture +def mock_fyta(): + """Build a fixture for the Fyta API that connects successfully and returns one device.""" + + mock_fyta_api = AsyncMock() + with patch( + "homeassistant.components.fyta.config_flow.FytaConnector", + return_value=mock_fyta_api, + ) as mock_fyta_api: + mock_fyta_api.return_value.login.return_value = { + "access_token": ACCESS_TOKEN, + "expiration": EXPIRATION, + } + yield mock_fyta_api + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock, None, None]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.fyta.async_setup_entry", return_value=True + ) as mock_setup_entry: + yield mock_setup_entry diff --git a/tests/components/fyta/test_config_flow.py b/tests/components/fyta/test_config_flow.py new file mode 100644 index 00000000000..b21be5abb90 --- /dev/null +++ b/tests/components/fyta/test_config_flow.py @@ -0,0 +1,122 @@ +"""Test the fyta config flow.""" + +from datetime import datetime +from unittest.mock import AsyncMock + +from fyta_cli.fyta_exceptions import ( + FytaAuthentificationError, + FytaConnectionError, + FytaPasswordError, +) +import pytest + +from homeassistant import config_entries, data_entry_flow +from homeassistant.components.fyta.const import DOMAIN +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + +USERNAME = "fyta_user" +PASSWORD = "fyta_pass" +ACCESS_TOKEN = "123xyz" +EXPIRATION = datetime.now() + + +async def test_user_flow( + hass: HomeAssistant, mock_fyta: AsyncMock, mock_setup_entry +) -> None: + """Test we get the form.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["errors"] == {} + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD} + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == USERNAME + assert result2["data"] == {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD} + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.parametrize( + ("exception", "error"), + [ + (FytaConnectionError, {"base": "cannot_connect"}), + (FytaAuthentificationError, {"base": "invalid_auth"}), + (FytaPasswordError, {"base": "invalid_auth", CONF_PASSWORD: "password_error"}), + (Exception, {"base": "unknown"}), + ], +) +async def test_form_exceptions( + hass: HomeAssistant, + exception: Exception, + error: dict[str, str], + mock_fyta: AsyncMock, + mock_setup_entry, +) -> None: + """Test we can handle Form exceptions.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + mock_fyta.return_value.login.side_effect = exception + + # tests with connection error + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD} + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == error + + mock_fyta.return_value.login.side_effect = None + + # tests with all information provided + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD} + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["title"] == USERNAME + assert result["data"][CONF_USERNAME] == USERNAME + assert result["data"][CONF_PASSWORD] == PASSWORD + + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_duplicate_entry(hass: HomeAssistant, mock_fyta: AsyncMock) -> None: + """Test duplicate setup handling.""" + entry = MockConfigEntry( + domain=DOMAIN, + title=USERNAME, + data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "already_configured" diff --git a/tests/components/garages_amsterdam/test_config_flow.py b/tests/components/garages_amsterdam/test_config_flow.py index ebafe3d566a..ae735d71e55 100644 --- a/tests/components/garages_amsterdam/test_config_flow.py +++ b/tests/components/garages_amsterdam/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Garages Amsterdam config flow.""" + from http import HTTPStatus from unittest.mock import patch diff --git a/tests/components/gardena_bluetooth/conftest.py b/tests/components/gardena_bluetooth/conftest.py index 9395d8570e6..af882e35751 100644 --- a/tests/components/gardena_bluetooth/conftest.py +++ b/tests/components/gardena_bluetooth/conftest.py @@ -1,4 +1,5 @@ """Common fixtures for the Gardena Bluetooth tests.""" + from collections.abc import Awaitable, Callable, Generator from typing import Any from unittest.mock import AsyncMock, Mock, patch @@ -100,10 +101,13 @@ def mock_client( client.read_char_raw.side_effect = _read_char_raw client.get_all_characteristics_uuid.side_effect = _all_char - with patch( - "homeassistant.components.gardena_bluetooth.config_flow.Client", - return_value=client, - ), patch("homeassistant.components.gardena_bluetooth.Client", return_value=client): + with ( + patch( + "homeassistant.components.gardena_bluetooth.config_flow.Client", + return_value=client, + ), + patch("homeassistant.components.gardena_bluetooth.Client", return_value=client), + ): yield client diff --git a/tests/components/gardena_bluetooth/snapshots/test_binary_sensor.ambr b/tests/components/gardena_bluetooth/snapshots/test_binary_sensor.ambr index 8a2600dcbb1..0ce39de5894 100644 --- a/tests/components/gardena_bluetooth/snapshots/test_binary_sensor.ambr +++ b/tests/components/gardena_bluetooth/snapshots/test_binary_sensor.ambr @@ -8,6 +8,7 @@ 'context': , 'entity_id': 'binary_sensor.mock_title_valve_connection', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }) @@ -21,6 +22,7 @@ 'context': , 'entity_id': 'binary_sensor.mock_title_valve_connection', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }) diff --git a/tests/components/gardena_bluetooth/snapshots/test_button.ambr b/tests/components/gardena_bluetooth/snapshots/test_button.ambr index b9cdca0e03c..c1ac96f0809 100644 --- a/tests/components/gardena_bluetooth/snapshots/test_button.ambr +++ b/tests/components/gardena_bluetooth/snapshots/test_button.ambr @@ -7,6 +7,7 @@ 'context': , 'entity_id': 'button.mock_title_factory_reset', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }) @@ -19,6 +20,7 @@ 'context': , 'entity_id': 'button.mock_title_factory_reset', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }) diff --git a/tests/components/gardena_bluetooth/snapshots/test_number.ambr b/tests/components/gardena_bluetooth/snapshots/test_number.ambr index 0b39525dc82..c89ead450d2 100644 --- a/tests/components/gardena_bluetooth/snapshots/test_number.ambr +++ b/tests/components/gardena_bluetooth/snapshots/test_number.ambr @@ -12,6 +12,7 @@ 'context': , 'entity_id': 'number.mock_title_remaining_open_time', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0.0', }) @@ -29,6 +30,7 @@ 'context': , 'entity_id': 'number.mock_title_manual_watering_time', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0.0', }) @@ -46,6 +48,7 @@ 'context': , 'entity_id': 'number.mock_title_remaining_open_time', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unavailable', }) @@ -63,6 +66,7 @@ 'context': , 'entity_id': 'number.mock_title_manual_watering_time', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unavailable', }) @@ -80,6 +84,7 @@ 'context': , 'entity_id': 'number.mock_title_sensor_threshold', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unavailable', }) @@ -97,6 +102,7 @@ 'context': , 'entity_id': 'number.mock_title_sensor_threshold', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '45.0', }) @@ -114,6 +120,7 @@ 'context': , 'entity_id': 'number.mock_title_remaining_open_time', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '100.0', }) @@ -131,6 +138,7 @@ 'context': , 'entity_id': 'number.mock_title_remaining_open_time', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '10.0', }) @@ -148,6 +156,7 @@ 'context': , 'entity_id': 'number.mock_title_remaining_open_time', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }) @@ -165,6 +174,7 @@ 'context': , 'entity_id': 'number.mock_title_remaining_open_time', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unavailable', }) @@ -182,6 +192,7 @@ 'context': , 'entity_id': 'number.mock_title_open_for', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }) @@ -199,6 +210,7 @@ 'context': , 'entity_id': 'number.mock_title_manual_watering_time', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '100.0', }) @@ -216,6 +228,7 @@ 'context': , 'entity_id': 'number.mock_title_manual_watering_time', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '10.0', }) diff --git a/tests/components/gardena_bluetooth/snapshots/test_sensor.ambr b/tests/components/gardena_bluetooth/snapshots/test_sensor.ambr index 1c33e8ebab9..3739fc19fd1 100644 --- a/tests/components/gardena_bluetooth/snapshots/test_sensor.ambr +++ b/tests/components/gardena_bluetooth/snapshots/test_sensor.ambr @@ -10,6 +10,7 @@ 'context': , 'entity_id': 'sensor.mock_title_sensor_battery', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unavailable', }) @@ -25,6 +26,7 @@ 'context': , 'entity_id': 'sensor.mock_title_sensor_battery', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '45', }) @@ -38,6 +40,7 @@ 'context': , 'entity_id': 'sensor.mock_title_valve_closing', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '2023-01-01T01:01:40+00:00', }) @@ -51,6 +54,7 @@ 'context': , 'entity_id': 'sensor.mock_title_valve_closing', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '2023-01-01T01:01:10+00:00', }) @@ -64,6 +68,7 @@ 'context': , 'entity_id': 'sensor.mock_title_valve_closing', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unavailable', }) @@ -79,6 +84,7 @@ 'context': , 'entity_id': 'sensor.mock_title_battery', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '100', }) @@ -94,6 +100,7 @@ 'context': , 'entity_id': 'sensor.mock_title_battery', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '10', }) diff --git a/tests/components/gardena_bluetooth/snapshots/test_switch.ambr b/tests/components/gardena_bluetooth/snapshots/test_switch.ambr index 37dae0bff75..3b55aade60f 100644 --- a/tests/components/gardena_bluetooth/snapshots/test_switch.ambr +++ b/tests/components/gardena_bluetooth/snapshots/test_switch.ambr @@ -7,6 +7,7 @@ 'context': , 'entity_id': 'switch.mock_title_open', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }) @@ -19,6 +20,7 @@ 'context': , 'entity_id': 'switch.mock_title_open', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }) diff --git a/tests/components/gardena_bluetooth/test_binary_sensor.py b/tests/components/gardena_bluetooth/test_binary_sensor.py index d12f825b1a7..97ba69ba239 100644 --- a/tests/components/gardena_bluetooth/test_binary_sensor.py +++ b/tests/components/gardena_bluetooth/test_binary_sensor.py @@ -1,6 +1,5 @@ """Test Gardena Bluetooth binary sensor.""" - from collections.abc import Awaitable, Callable from gardena_bluetooth.const import Valve diff --git a/tests/components/gardena_bluetooth/test_button.py b/tests/components/gardena_bluetooth/test_button.py index 480f0c3572e..685afd8c337 100644 --- a/tests/components/gardena_bluetooth/test_button.py +++ b/tests/components/gardena_bluetooth/test_button.py @@ -1,6 +1,5 @@ """Test Gardena Bluetooth sensor.""" - from collections.abc import Awaitable, Callable from unittest.mock import Mock, call diff --git a/tests/components/gardena_bluetooth/test_config_flow.py b/tests/components/gardena_bluetooth/test_config_flow.py index bcbd27e50a7..7707a13180f 100644 --- a/tests/components/gardena_bluetooth/test_config_flow.py +++ b/tests/components/gardena_bluetooth/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Gardena Bluetooth config flow.""" + from unittest.mock import Mock from gardena_bluetooth.exceptions import CharacteristicNotFound diff --git a/tests/components/gardena_bluetooth/test_number.py b/tests/components/gardena_bluetooth/test_number.py index ce2d19b8c63..4c053fca0fa 100644 --- a/tests/components/gardena_bluetooth/test_number.py +++ b/tests/components/gardena_bluetooth/test_number.py @@ -1,6 +1,5 @@ """Test Gardena Bluetooth sensor.""" - from collections.abc import Awaitable, Callable from typing import Any from unittest.mock import Mock, call @@ -131,12 +130,12 @@ async def test_bluetooth_error_unavailable( ) -> None: """Verify that a connectivity error makes all entities unavailable.""" - mock_read_char_raw[ - Valve.manual_watering_time.uuid - ] = Valve.manual_watering_time.encode(0) - mock_read_char_raw[ - Valve.remaining_open_time.uuid - ] = Valve.remaining_open_time.encode(0) + mock_read_char_raw[Valve.manual_watering_time.uuid] = ( + Valve.manual_watering_time.encode(0) + ) + mock_read_char_raw[Valve.remaining_open_time.uuid] = ( + Valve.remaining_open_time.encode(0) + ) await setup_entry(hass, mock_entry, [Platform.NUMBER]) assert hass.states.get("number.mock_title_remaining_open_time") == snapshot diff --git a/tests/components/gardena_bluetooth/test_sensor.py b/tests/components/gardena_bluetooth/test_sensor.py index dc0d0cb4809..e794934d028 100644 --- a/tests/components/gardena_bluetooth/test_sensor.py +++ b/tests/components/gardena_bluetooth/test_sensor.py @@ -1,4 +1,5 @@ """Test Gardena Bluetooth sensor.""" + from collections.abc import Awaitable, Callable from gardena_bluetooth.const import Battery, Sensor, Valve diff --git a/tests/components/gardena_bluetooth/test_switch.py b/tests/components/gardena_bluetooth/test_switch.py index 40e8c148335..4a29f8f91ae 100644 --- a/tests/components/gardena_bluetooth/test_switch.py +++ b/tests/components/gardena_bluetooth/test_switch.py @@ -1,6 +1,5 @@ """Test Gardena Bluetooth sensor.""" - from collections.abc import Awaitable, Callable from unittest.mock import Mock, call @@ -26,12 +25,12 @@ from tests.common import MockConfigEntry def mock_switch_chars(mock_read_char_raw): """Mock data on device.""" mock_read_char_raw[Valve.state.uuid] = b"\x00" - mock_read_char_raw[ - Valve.remaining_open_time.uuid - ] = Valve.remaining_open_time.encode(0) - mock_read_char_raw[ - Valve.manual_watering_time.uuid - ] = Valve.manual_watering_time.encode(1000) + mock_read_char_raw[Valve.remaining_open_time.uuid] = ( + Valve.remaining_open_time.encode(0) + ) + mock_read_char_raw[Valve.manual_watering_time.uuid] = ( + Valve.manual_watering_time.encode(1000) + ) return mock_read_char_raw diff --git a/tests/components/gdacs/__init__.py b/tests/components/gdacs/__init__.py index 6e61b86dbb7..01e13cca900 100644 --- a/tests/components/gdacs/__init__.py +++ b/tests/components/gdacs/__init__.py @@ -1,4 +1,5 @@ """Tests for the GDACS component.""" + from unittest.mock import MagicMock diff --git a/tests/components/gdacs/conftest.py b/tests/components/gdacs/conftest.py index ee82a3131b1..9d9a91aa407 100644 --- a/tests/components/gdacs/conftest.py +++ b/tests/components/gdacs/conftest.py @@ -1,4 +1,5 @@ """Configuration for GDACS tests.""" + import pytest from homeassistant.components.gdacs import CONF_CATEGORIES, DOMAIN diff --git a/tests/components/gdacs/test_config_flow.py b/tests/components/gdacs/test_config_flow.py index ad673815ace..71e5dfdb5d5 100644 --- a/tests/components/gdacs/test_config_flow.py +++ b/tests/components/gdacs/test_config_flow.py @@ -1,4 +1,5 @@ """Define tests for the GDACS config flow.""" + from unittest.mock import patch import pytest diff --git a/tests/components/gdacs/test_geo_location.py b/tests/components/gdacs/test_geo_location.py index c7f5a0f72e3..4ea28bd8fd3 100644 --- a/tests/components/gdacs/test_geo_location.py +++ b/tests/components/gdacs/test_geo_location.py @@ -1,4 +1,5 @@ """The tests for the GDACS Feed integration.""" + import datetime from unittest.mock import patch @@ -92,9 +93,10 @@ async def test_setup( # Patching 'utcnow' to gain more control over the timed update. utcnow = dt_util.utcnow() - with freeze_time(utcnow), patch( - "aio_georss_client.feed.GeoRssFeed.update" - ) as mock_feed_update: + with ( + freeze_time(utcnow), + patch("aio_georss_client.feed.GeoRssFeed.update") as mock_feed_update, + ): mock_feed_update.return_value = "OK", [mock_entry_1, mock_entry_2, mock_entry_3] config_entry.add_to_hass(hass) @@ -226,10 +228,10 @@ async def test_setup_imperial( # Patching 'utcnow' to gain more control over the timed update. utcnow = dt_util.utcnow() - with freeze_time(utcnow), patch( - "aio_georss_client.feed.GeoRssFeed.update" - ) as mock_feed_update, patch( - "aio_georss_client.feed.GeoRssFeed.last_timestamp", create=True + with ( + freeze_time(utcnow), + patch("aio_georss_client.feed.GeoRssFeed.update") as mock_feed_update, + patch("aio_georss_client.feed.GeoRssFeed.last_timestamp", create=True), ): mock_feed_update.return_value = "OK", [mock_entry_1] config_entry.add_to_hass(hass) diff --git a/tests/components/gdacs/test_init.py b/tests/components/gdacs/test_init.py index 6fc721261a7..1da4b0d9b9f 100644 --- a/tests/components/gdacs/test_init.py +++ b/tests/components/gdacs/test_init.py @@ -1,4 +1,5 @@ """Define tests for the GDACS general setup.""" + from unittest.mock import patch from homeassistant.components.gdacs import DOMAIN, FEED diff --git a/tests/components/gdacs/test_sensor.py b/tests/components/gdacs/test_sensor.py index f40756235e1..87b66295006 100644 --- a/tests/components/gdacs/test_sensor.py +++ b/tests/components/gdacs/test_sensor.py @@ -1,4 +1,5 @@ """The tests for the GDACS Feed integration.""" + from unittest.mock import patch from freezegun import freeze_time @@ -56,9 +57,10 @@ async def test_setup(hass: HomeAssistant) -> None: # Patching 'utcnow' to gain more control over the timed update. utcnow = dt_util.utcnow() - with freeze_time(utcnow), patch( - "aio_georss_client.feed.GeoRssFeed.update" - ) as mock_feed_update: + with ( + freeze_time(utcnow), + patch("aio_georss_client.feed.GeoRssFeed.update") as mock_feed_update, + ): mock_feed_update.return_value = "OK", [mock_entry_1, mock_entry_2, mock_entry_3] latitude = 32.87336 longitude = -117.22743 diff --git a/tests/components/generic/test_camera.py b/tests/components/generic/test_camera.py index 608b8666027..e359ddaca9d 100644 --- a/tests/components/generic/test_camera.py +++ b/tests/components/generic/test_camera.py @@ -1,4 +1,5 @@ """The tests for generic camera component.""" + import asyncio from datetime import timedelta from http import HTTPStatus @@ -272,8 +273,9 @@ async def test_limit_refetch( hass.states.async_set("sensor.temp", "5") - with pytest.raises(aiohttp.ServerTimeoutError), patch( - "asyncio.timeout", side_effect=TimeoutError() + with ( + pytest.raises(aiohttp.ServerTimeoutError), + patch("asyncio.timeout", side_effect=TimeoutError()), ): resp = await client.get("/api/camera_proxy/camera.config_test") diff --git a/tests/components/generic/test_config_flow.py b/tests/components/generic/test_config_flow.py index 86bd552bcf3..d9b3c848eb6 100644 --- a/tests/components/generic/test_config_flow.py +++ b/tests/components/generic/test_config_flow.py @@ -1,4 +1,5 @@ """Test The generic (IP Camera) config flow.""" + import contextlib import errno from http import HTTPStatus @@ -73,9 +74,12 @@ async def test_form( """Test the form with a normal set of settings.""" respx.get("http://127.0.0.1/testurl/1").respond(stream=fakeimgbytes_png) - with mock_create_stream as mock_setup, patch( - "homeassistant.components.generic.async_setup_entry", return_value=True - ) as mock_setup_entry: + with ( + mock_create_stream as mock_setup, + patch( + "homeassistant.components.generic.async_setup_entry", return_value=True + ) as mock_setup_entry, + ): result1 = await hass.config_entries.flow.async_configure( user_flow["flow_id"], TESTDATA, @@ -186,10 +190,13 @@ async def test_form_still_preview_cam_off( hass_client: ClientSessionGenerator, ) -> None: """Test camera errors are triggered during preview.""" - with patch( - "homeassistant.components.generic.camera.GenericCamera.is_on", - new_callable=PropertyMock(return_value=False), - ), mock_create_stream: + with ( + patch( + "homeassistant.components.generic.camera.GenericCamera.is_on", + new_callable=PropertyMock(return_value=False), + ), + mock_create_stream, + ): result1 = await hass.config_entries.flow.async_configure( user_flow["flow_id"], TESTDATA, @@ -357,8 +364,9 @@ async def test_form_rtsp_mode( data = TESTDATA.copy() data[CONF_RTSP_TRANSPORT] = "tcp" data[CONF_STREAM_SOURCE] = "rtsp://127.0.0.1/testurl/2" - with mock_create_stream as mock_setup, patch( - "homeassistant.components.generic.async_setup_entry", return_value=True + with ( + mock_create_stream as mock_setup, + patch("homeassistant.components.generic.async_setup_entry", return_value=True), ): result1 = await hass.config_entries.flow.async_configure( user_flow["flow_id"], data @@ -448,12 +456,42 @@ async def test_form_still_and_stream_not_provided( @respx.mock -async def test_form_image_timeout( - hass: HomeAssistant, user_flow, mock_create_stream +@pytest.mark.parametrize( + ("side_effect", "expected_message"), + [ + (httpx.TimeoutException, {"still_image_url": "unable_still_load"}), + ( + httpx.HTTPStatusError("", request=None, response=httpx.Response(401)), + {"still_image_url": "unable_still_load_auth"}, + ), + ( + httpx.HTTPStatusError("", request=None, response=httpx.Response(403)), + {"still_image_url": "unable_still_load_auth"}, + ), + ( + httpx.HTTPStatusError("", request=None, response=httpx.Response(404)), + {"still_image_url": "unable_still_load_not_found"}, + ), + ( + httpx.HTTPStatusError("", request=None, response=httpx.Response(500)), + {"still_image_url": "unable_still_load_server_error"}, + ), + ( + httpx.HTTPStatusError("", request=None, response=httpx.Response(503)), + {"still_image_url": "unable_still_load_server_error"}, + ), + ( # Errors without specific handler should show the general message. + httpx.HTTPStatusError("", request=None, response=httpx.Response(507)), + {"still_image_url": "unable_still_load"}, + ), + ], +) +async def test_form_image_http_exceptions( + side_effect, expected_message, hass: HomeAssistant, user_flow, mock_create_stream ) -> None: - """Test we handle invalid image timeout.""" + """Test we handle image http exceptions.""" respx.get("http://127.0.0.1/testurl/1").side_effect = [ - httpx.TimeoutException, + side_effect, ] with mock_create_stream: @@ -464,7 +502,7 @@ async def test_form_image_timeout( await hass.async_block_till_done() assert result2["type"] == "form" - assert result2["errors"] == {"still_image_url": "unable_still_load"} + assert result2["errors"] == expected_message @respx.mock @@ -498,7 +536,7 @@ async def test_form_stream_invalidimage2( await hass.async_block_till_done() assert result2["type"] == "form" - assert result2["errors"] == {"still_image_url": "unable_still_load"} + assert result2["errors"] == {"still_image_url": "unable_still_load_no_image"} @respx.mock @@ -609,10 +647,13 @@ async def test_form_stream_io_error( @respx.mock async def test_form_oserror(hass: HomeAssistant, fakeimg_png, user_flow) -> None: """Test we handle OS error when setting up stream.""" - with patch( - "homeassistant.components.generic.config_flow.create_stream", - side_effect=OSError("Some other OSError"), - ), pytest.raises(OSError): + with ( + patch( + "homeassistant.components.generic.config_flow.create_stream", + side_effect=OSError("Some other OSError"), + ), + pytest.raises(OSError), + ): await hass.config_entries.flow.async_configure( user_flow["flow_id"], TESTDATA, @@ -847,9 +888,10 @@ async def test_use_wallclock_as_timestamps_option( ) assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" - with patch( - "homeassistant.components.generic.async_setup_entry", return_value=True - ), mock_create_stream: + with ( + patch("homeassistant.components.generic.async_setup_entry", return_value=True), + mock_create_stream, + ): result2 = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_USE_WALLCLOCK_AS_TIMESTAMPS: True, **TESTDATA}, @@ -861,9 +903,10 @@ async def test_use_wallclock_as_timestamps_option( ) assert result3["type"] == FlowResultType.FORM assert result3["step_id"] == "init" - with patch( - "homeassistant.components.generic.async_setup_entry", return_value=True - ), mock_create_stream: + with ( + patch("homeassistant.components.generic.async_setup_entry", return_value=True), + mock_create_stream, + ): result4 = await hass.config_entries.options.async_configure( result3["flow_id"], user_input={CONF_USE_WALLCLOCK_AS_TIMESTAMPS: True, **TESTDATA}, diff --git a/tests/components/generic/test_diagnostics.py b/tests/components/generic/test_diagnostics.py index 99dc3e22a0b..f68c3ba4bc6 100644 --- a/tests/components/generic/test_diagnostics.py +++ b/tests/components/generic/test_diagnostics.py @@ -1,4 +1,5 @@ """Test generic (IP camera) diagnostics.""" + import pytest from homeassistant.components.diagnostics import REDACTED diff --git a/tests/components/generic_hygrostat/test_humidifier.py b/tests/components/generic_hygrostat/test_humidifier.py index e1a33a19834..fdad20f5b2d 100644 --- a/tests/components/generic_hygrostat/test_humidifier.py +++ b/tests/components/generic_hygrostat/test_humidifier.py @@ -1,4 +1,5 @@ """The tests for the generic_hygrostat.""" + import datetime from freezegun import freeze_time diff --git a/tests/components/generic_thermostat/test_climate.py b/tests/components/generic_thermostat/test_climate.py index e181b14efb0..fdcad219d93 100644 --- a/tests/components/generic_thermostat/test_climate.py +++ b/tests/components/generic_thermostat/test_climate.py @@ -1,4 +1,5 @@ """The tests for the generic_thermostat.""" + import datetime from unittest.mock import patch @@ -754,8 +755,9 @@ async def test_no_state_change_when_operation_mode_off_2( assert len(calls) == 0 -@pytest.fixture -async def setup_comp_4(hass): +async def _setup_thermostat_with_min_cycle_duration( + hass: HomeAssistant, ac_mode: bool, initial_hvac_mode: HVACMode +): """Initialize components.""" hass.config.temperature_unit = UnitOfTemperature.CELSIUS assert await async_setup_component( @@ -769,102 +771,141 @@ async def setup_comp_4(hass): "hot_tolerance": 0.3, "heater": ENT_SWITCH, "target_sensor": ENT_SENSOR, - "ac_mode": True, + "ac_mode": ac_mode, "min_cycle_duration": datetime.timedelta(minutes=10), - "initial_hvac_mode": HVACMode.COOL, + "initial_hvac_mode": initial_hvac_mode, } }, ) await hass.async_block_till_done() -async def test_temp_change_ac_trigger_on_not_long_enough( - hass: HomeAssistant, setup_comp_4 +@pytest.mark.parametrize( + ( + "ac_mode", + "initial_hvac_mode", + "initial_switch_state", + "sensor_temperature", + "target_temperature", + ), + [ + (True, HVACMode.COOL, False, 30, 25), + (True, HVACMode.COOL, True, 25, 30), + (False, HVACMode.HEAT, True, 25, 30), + (False, HVACMode.HEAT, False, 30, 25), + ], +) +async def test_heating_cooling_switch_does_not_toggle_when_within_min_cycle_duration( + hass: HomeAssistant, + ac_mode: bool, + initial_hvac_mode: HVACMode, + initial_switch_state: bool, + sensor_temperature: int, + target_temperature: int, ) -> None: - """Test if temperature change turn ac on.""" - calls = _setup_switch(hass, False) - await common.async_set_temperature(hass, 25) - _setup_sensor(hass, 30) + """Test if heating/cooling does not toggle when inside minimum cycle.""" + # Given + await _setup_thermostat_with_min_cycle_duration(hass, ac_mode, initial_hvac_mode) + calls = _setup_switch(hass, initial_switch_state) + _setup_sensor(hass, sensor_temperature) + + # When + await common.async_set_temperature(hass, target_temperature) await hass.async_block_till_done() + + # Then assert len(calls) == 0 -async def test_temp_change_ac_trigger_on_long_enough( - hass: HomeAssistant, setup_comp_4 +@pytest.mark.parametrize( + ( + "ac_mode", + "initial_hvac_mode", + "initial_switch_state", + "sensor_temperature", + "target_temperature", + "expected_triggered_service_call", + ), + [ + (True, HVACMode.COOL, False, 30, 25, SERVICE_TURN_ON), + (True, HVACMode.COOL, True, 25, 30, SERVICE_TURN_OFF), + (False, HVACMode.HEAT, False, 25, 30, SERVICE_TURN_ON), + (False, HVACMode.HEAT, True, 30, 25, SERVICE_TURN_OFF), + ], +) +async def test_heating_cooling_switch_toggles_when_outside_min_cycle_duration( + hass: HomeAssistant, + ac_mode: bool, + initial_hvac_mode: HVACMode, + initial_switch_state: bool, + sensor_temperature: int, + target_temperature: int, + expected_triggered_service_call: str, ) -> None: - """Test if temperature change turn ac on.""" + """Test if heating/cooling toggles when outside minimum cycle.""" + # Given + await _setup_thermostat_with_min_cycle_duration(hass, ac_mode, initial_hvac_mode) fake_changed = datetime.datetime(1970, 11, 11, 11, 11, 11, tzinfo=dt_util.UTC) with freeze_time(fake_changed): - calls = _setup_switch(hass, False) - await common.async_set_temperature(hass, 25) - _setup_sensor(hass, 30) + calls = _setup_switch(hass, initial_switch_state) + _setup_sensor(hass, sensor_temperature) + + # When + await common.async_set_temperature(hass, target_temperature) await hass.async_block_till_done() + + # Then assert len(calls) == 1 call = calls[0] assert call.domain == HASS_DOMAIN - assert call.service == SERVICE_TURN_ON + assert call.service == expected_triggered_service_call assert call.data["entity_id"] == ENT_SWITCH -async def test_temp_change_ac_trigger_off_not_long_enough( - hass: HomeAssistant, setup_comp_4 +@pytest.mark.parametrize( + ( + "ac_mode", + "initial_hvac_mode", + "initial_switch_state", + "sensor_temperature", + "target_temperature", + "changed_hvac_mode", + "expected_triggered_service_call", + ), + [ + (True, HVACMode.COOL, False, 30, 25, HVACMode.HEAT, SERVICE_TURN_ON), + (True, HVACMode.COOL, True, 25, 30, HVACMode.OFF, SERVICE_TURN_OFF), + (False, HVACMode.HEAT, False, 25, 30, HVACMode.HEAT, SERVICE_TURN_ON), + (False, HVACMode.HEAT, True, 30, 25, HVACMode.OFF, SERVICE_TURN_OFF), + ], +) +async def test_hvac_mode_change_toggles_heating_cooling_switch_even_when_within_min_cycle_duration( + hass: HomeAssistant, + ac_mode: bool, + initial_hvac_mode: HVACMode, + initial_switch_state: bool, + sensor_temperature: int, + target_temperature: int, + changed_hvac_mode: HVACMode, + expected_triggered_service_call: str, ) -> None: - """Test if temperature change turn ac on.""" - calls = _setup_switch(hass, True) - await common.async_set_temperature(hass, 30) - _setup_sensor(hass, 25) + """Test if mode change toggles heating/cooling despite minimum cycle.""" + # Given + await _setup_thermostat_with_min_cycle_duration(hass, ac_mode, initial_hvac_mode) + calls = _setup_switch(hass, initial_switch_state) + _setup_sensor(hass, sensor_temperature) + + # When + await common.async_set_temperature(hass, target_temperature) await hass.async_block_till_done() + + # Then assert len(calls) == 0 - - -async def test_temp_change_ac_trigger_off_long_enough( - hass: HomeAssistant, setup_comp_4 -) -> None: - """Test if temperature change turn ac on.""" - fake_changed = datetime.datetime(1970, 11, 11, 11, 11, 11, tzinfo=dt_util.UTC) - with freeze_time(fake_changed): - calls = _setup_switch(hass, True) - await common.async_set_temperature(hass, 30) - _setup_sensor(hass, 25) - await hass.async_block_till_done() - assert len(calls) == 1 - call = calls[0] - assert call.domain == HASS_DOMAIN - assert call.service == SERVICE_TURN_OFF - assert call.data["entity_id"] == ENT_SWITCH - - -async def test_mode_change_ac_trigger_off_not_long_enough( - hass: HomeAssistant, setup_comp_4 -) -> None: - """Test if mode change turns ac off despite minimum cycle.""" - calls = _setup_switch(hass, True) - await common.async_set_temperature(hass, 30) - _setup_sensor(hass, 25) - await hass.async_block_till_done() - assert len(calls) == 0 - await common.async_set_hvac_mode(hass, HVACMode.OFF) + await common.async_set_hvac_mode(hass, changed_hvac_mode) assert len(calls) == 1 call = calls[0] assert call.domain == "homeassistant" - assert call.service == SERVICE_TURN_OFF - assert call.data["entity_id"] == ENT_SWITCH - - -async def test_mode_change_ac_trigger_on_not_long_enough( - hass: HomeAssistant, setup_comp_4 -) -> None: - """Test if mode change turns ac on despite minimum cycle.""" - calls = _setup_switch(hass, False) - await common.async_set_temperature(hass, 25) - _setup_sensor(hass, 30) - await hass.async_block_till_done() - assert len(calls) == 0 - await common.async_set_hvac_mode(hass, HVACMode.HEAT) - assert len(calls) == 1 - call = calls[0] - assert call.domain == "homeassistant" - assert call.service == SERVICE_TURN_ON + assert call.service == expected_triggered_service_call assert call.data["entity_id"] == ENT_SWITCH @@ -982,119 +1023,6 @@ async def test_mode_change_ac_trigger_on_not_long_enough_2( assert call.data["entity_id"] == ENT_SWITCH -@pytest.fixture -async def setup_comp_6(hass): - """Initialize components.""" - hass.config.temperature_unit = UnitOfTemperature.CELSIUS - assert await async_setup_component( - hass, - DOMAIN, - { - "climate": { - "platform": "generic_thermostat", - "name": "test", - "cold_tolerance": 0.3, - "hot_tolerance": 0.3, - "heater": ENT_SWITCH, - "target_sensor": ENT_SENSOR, - "min_cycle_duration": datetime.timedelta(minutes=10), - "initial_hvac_mode": HVACMode.HEAT, - } - }, - ) - await hass.async_block_till_done() - - -async def test_temp_change_heater_trigger_off_not_long_enough( - hass: HomeAssistant, setup_comp_6 -) -> None: - """Test if temp change doesn't turn heater off because of time.""" - calls = _setup_switch(hass, True) - await common.async_set_temperature(hass, 25) - _setup_sensor(hass, 30) - await hass.async_block_till_done() - assert len(calls) == 0 - - -async def test_temp_change_heater_trigger_on_not_long_enough( - hass: HomeAssistant, setup_comp_6 -) -> None: - """Test if temp change doesn't turn heater on because of time.""" - calls = _setup_switch(hass, False) - await common.async_set_temperature(hass, 30) - _setup_sensor(hass, 25) - await hass.async_block_till_done() - assert len(calls) == 0 - - -async def test_temp_change_heater_trigger_on_long_enough( - hass: HomeAssistant, setup_comp_6 -) -> None: - """Test if temperature change turn heater on after min cycle.""" - fake_changed = datetime.datetime(1970, 11, 11, 11, 11, 11, tzinfo=dt_util.UTC) - with freeze_time(fake_changed): - calls = _setup_switch(hass, False) - await common.async_set_temperature(hass, 30) - _setup_sensor(hass, 25) - await hass.async_block_till_done() - assert len(calls) == 1 - call = calls[0] - assert call.domain == HASS_DOMAIN - assert call.service == SERVICE_TURN_ON - assert call.data["entity_id"] == ENT_SWITCH - - -async def test_temp_change_heater_trigger_off_long_enough( - hass: HomeAssistant, setup_comp_6 -) -> None: - """Test if temperature change turn heater off after min cycle.""" - fake_changed = datetime.datetime(1970, 11, 11, 11, 11, 11, tzinfo=dt_util.UTC) - with freeze_time(fake_changed): - calls = _setup_switch(hass, True) - await common.async_set_temperature(hass, 25) - _setup_sensor(hass, 30) - await hass.async_block_till_done() - assert len(calls) == 1 - call = calls[0] - assert call.domain == HASS_DOMAIN - assert call.service == SERVICE_TURN_OFF - assert call.data["entity_id"] == ENT_SWITCH - - -async def test_mode_change_heater_trigger_off_not_long_enough( - hass: HomeAssistant, setup_comp_6 -) -> None: - """Test if mode change turns heater off despite minimum cycle.""" - calls = _setup_switch(hass, True) - await common.async_set_temperature(hass, 25) - _setup_sensor(hass, 30) - await hass.async_block_till_done() - assert len(calls) == 0 - await common.async_set_hvac_mode(hass, HVACMode.OFF) - assert len(calls) == 1 - call = calls[0] - assert call.domain == "homeassistant" - assert call.service == SERVICE_TURN_OFF - assert call.data["entity_id"] == ENT_SWITCH - - -async def test_mode_change_heater_trigger_on_not_long_enough( - hass: HomeAssistant, setup_comp_6 -) -> None: - """Test if mode change turns heater on despite minimum cycle.""" - calls = _setup_switch(hass, False) - await common.async_set_temperature(hass, 30) - _setup_sensor(hass, 25) - await hass.async_block_till_done() - assert len(calls) == 0 - await common.async_set_hvac_mode(hass, HVACMode.HEAT) - assert len(calls) == 1 - call = calls[0] - assert call.domain == "homeassistant" - assert call.service == SERVICE_TURN_ON - assert call.data["entity_id"] == ENT_SWITCH - - @pytest.fixture async def setup_comp_7(hass): """Initialize components.""" diff --git a/tests/components/geo_json_events/__init__.py b/tests/components/geo_json_events/__init__.py index 7d7148b3c20..18fbba47b6e 100644 --- a/tests/components/geo_json_events/__init__.py +++ b/tests/components/geo_json_events/__init__.py @@ -1,4 +1,5 @@ """Tests for the geo_json_events component.""" + from typing import Any from unittest.mock import MagicMock diff --git a/tests/components/geo_json_events/conftest.py b/tests/components/geo_json_events/conftest.py index db0ac38fe47..80e06f4880c 100644 --- a/tests/components/geo_json_events/conftest.py +++ b/tests/components/geo_json_events/conftest.py @@ -1,4 +1,5 @@ """Configuration for GeoJSON Events tests.""" + from collections.abc import Generator from unittest.mock import AsyncMock, patch diff --git a/tests/components/geo_json_events/test_geo_location.py b/tests/components/geo_json_events/test_geo_location.py index 2f3b12ed554..365c4ca27bc 100644 --- a/tests/components/geo_json_events/test_geo_location.py +++ b/tests/components/geo_json_events/test_geo_location.py @@ -1,4 +1,5 @@ """The tests for the geojson platform.""" + from datetime import timedelta from unittest.mock import patch @@ -65,9 +66,10 @@ async def test_entity_lifecycle( mock_entry_4 = _generate_mock_feed_entry("4567", "Title 4", 12.5, (-31.3, 150.3)) utcnow = dt_util.utcnow() - with freeze_time(utcnow), patch( - "aio_geojson_client.feed.GeoJsonFeed.update" - ) as mock_feed_update: + with ( + freeze_time(utcnow), + patch("aio_geojson_client.feed.GeoJsonFeed.update") as mock_feed_update, + ): mock_feed_update.return_value = "OK", [mock_entry_1, mock_entry_2, mock_entry_3] # Load config entry. diff --git a/tests/components/geo_json_events/test_init.py b/tests/components/geo_json_events/test_init.py index bc803b3e8d8..278586ba2e3 100644 --- a/tests/components/geo_json_events/test_init.py +++ b/tests/components/geo_json_events/test_init.py @@ -1,4 +1,5 @@ """Define tests for the GeoJSON Events general setup.""" + from unittest.mock import patch from homeassistant.components.geo_json_events.const import DOMAIN diff --git a/tests/components/geo_location/test_init.py b/tests/components/geo_location/test_init.py index 0861cd7cba1..747135def9b 100644 --- a/tests/components/geo_location/test_init.py +++ b/tests/components/geo_location/test_init.py @@ -1,4 +1,5 @@ """The tests for the geolocation component.""" + import pytest from homeassistant.components import geo_location diff --git a/tests/components/geo_location/test_trigger.py b/tests/components/geo_location/test_trigger.py index a5e0f99c5c2..85461d60aac 100644 --- a/tests/components/geo_location/test_trigger.py +++ b/tests/components/geo_location/test_trigger.py @@ -1,4 +1,5 @@ """The tests for the geolocation trigger.""" + import logging import pytest diff --git a/tests/components/geo_rss_events/test_sensor.py b/tests/components/geo_rss_events/test_sensor.py index c86ef393875..d19262c3339 100644 --- a/tests/components/geo_rss_events/test_sensor.py +++ b/tests/components/geo_rss_events/test_sensor.py @@ -1,4 +1,5 @@ """The test for the geo rss events sensor platform.""" + from unittest.mock import MagicMock, patch from freezegun.api import FrozenDateTimeFactory @@ -98,7 +99,7 @@ async def test_setup( # so no changes to entities. mock_feed.return_value.update.return_value = "OK_NO_DATA", None async_fire_time_changed(hass, utcnow + geo_rss_events.SCAN_INTERVAL) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) all_states = hass.states.async_all() assert len(all_states) == 1 @@ -108,7 +109,7 @@ async def test_setup( # Simulate an update - empty data, removes all entities mock_feed.return_value.update.return_value = "ERROR", None async_fire_time_changed(hass, utcnow + 2 * geo_rss_events.SCAN_INTERVAL) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) all_states = hass.states.async_all() assert len(all_states) == 1 diff --git a/tests/components/geocaching/conftest.py b/tests/components/geocaching/conftest.py index f59f428118e..68041672efb 100644 --- a/tests/components/geocaching/conftest.py +++ b/tests/components/geocaching/conftest.py @@ -1,4 +1,5 @@ """Fixtures for the Geocaching integration tests.""" + from __future__ import annotations from collections.abc import Generator diff --git a/tests/components/geocaching/test_config_flow.py b/tests/components/geocaching/test_config_flow.py index 1b0d32278a6..15f7ee0972f 100644 --- a/tests/components/geocaching/test_config_flow.py +++ b/tests/components/geocaching/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Geocaching config flow.""" + from http import HTTPStatus from unittest.mock import MagicMock diff --git a/tests/components/geofency/test_init.py b/tests/components/geofency/test_init.py index 2ab2d9cc8bb..d5d77c1387a 100644 --- a/tests/components/geofency/test_init.py +++ b/tests/components/geofency/test_init.py @@ -1,4 +1,5 @@ """The tests for the Geofency device tracker platform.""" + from http import HTTPStatus from unittest.mock import patch diff --git a/tests/components/geonetnz_quakes/__init__.py b/tests/components/geonetnz_quakes/__init__.py index 424c6372ea8..90375079daa 100644 --- a/tests/components/geonetnz_quakes/__init__.py +++ b/tests/components/geonetnz_quakes/__init__.py @@ -1,4 +1,5 @@ """Tests for the geonetnz_quakes component.""" + from unittest.mock import MagicMock diff --git a/tests/components/geonetnz_quakes/conftest.py b/tests/components/geonetnz_quakes/conftest.py index 7715b17796b..c93d4a2e50c 100644 --- a/tests/components/geonetnz_quakes/conftest.py +++ b/tests/components/geonetnz_quakes/conftest.py @@ -1,4 +1,5 @@ """Configuration for GeoNet NZ Quakes tests.""" + import pytest from homeassistant.components.geonetnz_quakes import ( diff --git a/tests/components/geonetnz_quakes/test_config_flow.py b/tests/components/geonetnz_quakes/test_config_flow.py index 4a59386fc35..d4b406cf054 100644 --- a/tests/components/geonetnz_quakes/test_config_flow.py +++ b/tests/components/geonetnz_quakes/test_config_flow.py @@ -1,4 +1,5 @@ """Define tests for the GeoNet NZ Quakes config flow.""" + from datetime import timedelta from unittest.mock import patch @@ -51,9 +52,15 @@ async def test_step_import(hass: HomeAssistant) -> None: CONF_MINIMUM_MAGNITUDE: 2.5, } - with patch( - "homeassistant.components.geonetnz_quakes.async_setup_entry", return_value=True - ), patch("homeassistant.components.geonetnz_quakes.async_setup", return_value=True): + with ( + patch( + "homeassistant.components.geonetnz_quakes.async_setup_entry", + return_value=True, + ), + patch( + "homeassistant.components.geonetnz_quakes.async_setup", return_value=True + ), + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=conf ) @@ -76,9 +83,15 @@ async def test_step_user(hass: HomeAssistant) -> None: hass.config.longitude = 174.7 conf = {CONF_RADIUS: 25, CONF_MMI: 4} - with patch( - "homeassistant.components.geonetnz_quakes.async_setup_entry", return_value=True - ), patch("homeassistant.components.geonetnz_quakes.async_setup", return_value=True): + with ( + patch( + "homeassistant.components.geonetnz_quakes.async_setup_entry", + return_value=True, + ), + patch( + "homeassistant.components.geonetnz_quakes.async_setup", return_value=True + ), + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=conf ) diff --git a/tests/components/geonetnz_quakes/test_geo_location.py b/tests/components/geonetnz_quakes/test_geo_location.py index afc6ada75cd..163bca775c9 100644 --- a/tests/components/geonetnz_quakes/test_geo_location.py +++ b/tests/components/geonetnz_quakes/test_geo_location.py @@ -1,4 +1,5 @@ """The tests for the GeoNet NZ Quakes Feed integration.""" + import datetime from unittest.mock import patch @@ -182,8 +183,9 @@ async def test_setup_imperial( # Patching 'utcnow' to gain more control over the timed update. freezer.move_to(dt_util.utcnow()) - with patch("aio_geojson_client.feed.GeoJsonFeed.update") as mock_feed_update, patch( - "aio_geojson_client.feed.GeoJsonFeed.last_timestamp", create=True + with ( + patch("aio_geojson_client.feed.GeoJsonFeed.update") as mock_feed_update, + patch("aio_geojson_client.feed.GeoJsonFeed.last_timestamp", create=True), ): mock_feed_update.return_value = "OK", [mock_entry_1] assert await async_setup_component(hass, geonetnz_quakes.DOMAIN, CONFIG) diff --git a/tests/components/geonetnz_quakes/test_init.py b/tests/components/geonetnz_quakes/test_init.py index 043d0ff6209..6730fa53ece 100644 --- a/tests/components/geonetnz_quakes/test_init.py +++ b/tests/components/geonetnz_quakes/test_init.py @@ -1,4 +1,5 @@ """Define tests for the GeoNet NZ Quakes general setup.""" + from unittest.mock import patch from homeassistant.components.geonetnz_quakes import DOMAIN, FEED diff --git a/tests/components/geonetnz_quakes/test_sensor.py b/tests/components/geonetnz_quakes/test_sensor.py index 27f67dad322..82143baa374 100644 --- a/tests/components/geonetnz_quakes/test_sensor.py +++ b/tests/components/geonetnz_quakes/test_sensor.py @@ -1,4 +1,5 @@ """The tests for the GeoNet NZ Quakes Feed integration.""" + import datetime from unittest.mock import patch @@ -57,9 +58,10 @@ async def test_setup(hass: HomeAssistant) -> None: # Patching 'utcnow' to gain more control over the timed update. utcnow = dt_util.utcnow() - with freeze_time(utcnow), patch( - "aio_geojson_client.feed.GeoJsonFeed.update" - ) as mock_feed_update: + with ( + freeze_time(utcnow), + patch("aio_geojson_client.feed.GeoJsonFeed.update") as mock_feed_update, + ): mock_feed_update.return_value = "OK", [mock_entry_1, mock_entry_2, mock_entry_3] assert await async_setup_component(hass, geonetnz_quakes.DOMAIN, CONFIG) # Artificially trigger update and collect events. diff --git a/tests/components/geonetnz_volcano/__init__.py b/tests/components/geonetnz_volcano/__init__.py index 708b69e0031..b8a36a124a3 100644 --- a/tests/components/geonetnz_volcano/__init__.py +++ b/tests/components/geonetnz_volcano/__init__.py @@ -1,4 +1,5 @@ """The tests for the GeoNet NZ Volcano Feed integration.""" + from unittest.mock import MagicMock diff --git a/tests/components/geonetnz_volcano/conftest.py b/tests/components/geonetnz_volcano/conftest.py index 33a299eeb79..3df67e91c95 100644 --- a/tests/components/geonetnz_volcano/conftest.py +++ b/tests/components/geonetnz_volcano/conftest.py @@ -1,4 +1,5 @@ """Configuration for GeoNet NZ Volcano tests.""" + import pytest from homeassistant.components.geonetnz_volcano import DOMAIN diff --git a/tests/components/geonetnz_volcano/test_config_flow.py b/tests/components/geonetnz_volcano/test_config_flow.py index 7583bc29a43..e314896dd6b 100644 --- a/tests/components/geonetnz_volcano/test_config_flow.py +++ b/tests/components/geonetnz_volcano/test_config_flow.py @@ -1,4 +1,5 @@ """Define tests for the GeoNet NZ Volcano config flow.""" + from datetime import timedelta from unittest.mock import patch @@ -50,10 +51,14 @@ async def test_step_import(hass: HomeAssistant) -> None: flow = config_flow.GeonetnzVolcanoFlowHandler() flow.hass = hass - with patch( - "homeassistant.components.geonetnz_volcano.async_setup_entry", return_value=True - ), patch( - "homeassistant.components.geonetnz_volcano.async_setup", return_value=True + with ( + patch( + "homeassistant.components.geonetnz_volcano.async_setup_entry", + return_value=True, + ), + patch( + "homeassistant.components.geonetnz_volcano.async_setup", return_value=True + ), ): result = await flow.async_step_import(import_config=conf) assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY @@ -76,10 +81,14 @@ async def test_step_user(hass: HomeAssistant) -> None: flow = config_flow.GeonetnzVolcanoFlowHandler() flow.hass = hass - with patch( - "homeassistant.components.geonetnz_volcano.async_setup_entry", return_value=True - ), patch( - "homeassistant.components.geonetnz_volcano.async_setup", return_value=True + with ( + patch( + "homeassistant.components.geonetnz_volcano.async_setup_entry", + return_value=True, + ), + patch( + "homeassistant.components.geonetnz_volcano.async_setup", return_value=True + ), ): result = await flow.async_step_user(user_input=conf) assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY diff --git a/tests/components/geonetnz_volcano/test_init.py b/tests/components/geonetnz_volcano/test_init.py index 64e7ddc3eba..fe113434dc6 100644 --- a/tests/components/geonetnz_volcano/test_init.py +++ b/tests/components/geonetnz_volcano/test_init.py @@ -1,4 +1,5 @@ """Define tests for the GeoNet NZ Volcano general setup.""" + from unittest.mock import AsyncMock, patch from homeassistant.components.geonetnz_volcano import DOMAIN, FEED diff --git a/tests/components/geonetnz_volcano/test_sensor.py b/tests/components/geonetnz_volcano/test_sensor.py index 4d11ff0673c..d6ebbcd6582 100644 --- a/tests/components/geonetnz_volcano/test_sensor.py +++ b/tests/components/geonetnz_volcano/test_sensor.py @@ -1,4 +1,5 @@ """The tests for the GeoNet NZ Volcano Feed integration.""" + from unittest.mock import AsyncMock, patch from freezegun import freeze_time @@ -53,9 +54,12 @@ async def test_setup(hass: HomeAssistant) -> None: # Patching 'utcnow' to gain more control over the timed update. utcnow = dt_util.utcnow() - with freeze_time(utcnow), patch( - "aio_geojson_client.feed.GeoJsonFeed.update", new_callable=AsyncMock - ) as mock_feed_update: + with ( + freeze_time(utcnow), + patch( + "aio_geojson_client.feed.GeoJsonFeed.update", new_callable=AsyncMock + ) as mock_feed_update, + ): mock_feed_update.return_value = "OK", [mock_entry_1, mock_entry_2, mock_entry_3] assert await async_setup_component(hass, geonetnz_volcano.DOMAIN, CONFIG) # Artificially trigger update and collect events. @@ -160,11 +164,12 @@ async def test_setup_imperial( # Patching 'utcnow' to gain more control over the timed update. freezer.move_to(dt_util.utcnow()) - with patch( - "aio_geojson_client.feed.GeoJsonFeed.update", new_callable=AsyncMock - ) as mock_feed_update, patch( - "aio_geojson_client.feed.GeoJsonFeed.__init__" - ) as mock_feed_init: + with ( + patch( + "aio_geojson_client.feed.GeoJsonFeed.update", new_callable=AsyncMock + ) as mock_feed_update, + patch("aio_geojson_client.feed.GeoJsonFeed.__init__") as mock_feed_init, + ): mock_feed_update.return_value = "OK", [mock_entry_1] assert await async_setup_component(hass, geonetnz_volcano.DOMAIN, CONFIG) # Artificially trigger update and collect events. diff --git a/tests/components/gios/__init__.py b/tests/components/gios/__init__.py index 4e69420f66e..d5c43c8acc0 100644 --- a/tests/components/gios/__init__.py +++ b/tests/components/gios/__init__.py @@ -1,4 +1,5 @@ """Tests for GIOS.""" + import json from unittest.mock import patch @@ -34,17 +35,22 @@ async def init_integration( if invalid_indexes: indexes = {} - with patch( - "homeassistant.components.gios.Gios._get_stations", return_value=STATIONS - ), patch( - "homeassistant.components.gios.Gios._get_station", - return_value=station, - ), patch( - "homeassistant.components.gios.Gios._get_all_sensors", - return_value=sensors, - ), patch( - "homeassistant.components.gios.Gios._get_indexes", - return_value=indexes, + with ( + patch( + "homeassistant.components.gios.Gios._get_stations", return_value=STATIONS + ), + patch( + "homeassistant.components.gios.Gios._get_station", + return_value=station, + ), + patch( + "homeassistant.components.gios.Gios._get_all_sensors", + return_value=sensors, + ), + patch( + "homeassistant.components.gios.Gios._get_indexes", + return_value=indexes, + ), ): entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) diff --git a/tests/components/gios/test_config_flow.py b/tests/components/gios/test_config_flow.py index efe46be9b8d..4471cfa64ec 100644 --- a/tests/components/gios/test_config_flow.py +++ b/tests/components/gios/test_config_flow.py @@ -1,4 +1,5 @@ """Define tests for the GIOS config flow.""" + import json from unittest.mock import patch @@ -49,14 +50,18 @@ async def test_invalid_station_id(hass: HomeAssistant) -> None: async def test_invalid_sensor_data(hass: HomeAssistant) -> None: """Test that errors are shown when sensor data is invalid.""" - with patch( - "homeassistant.components.gios.Gios._get_stations", return_value=STATIONS - ), patch( - "homeassistant.components.gios.Gios._get_station", - return_value=json.loads(load_fixture("gios/station.json")), - ), patch( - "homeassistant.components.gios.Gios._get_sensor", - return_value={}, + with ( + patch( + "homeassistant.components.gios.Gios._get_stations", return_value=STATIONS + ), + patch( + "homeassistant.components.gios.Gios._get_station", + return_value=json.loads(load_fixture("gios/station.json")), + ), + patch( + "homeassistant.components.gios.Gios._get_sensor", + return_value={}, + ), ): flow = config_flow.GiosFlowHandler() flow.hass = hass @@ -83,18 +88,23 @@ async def test_cannot_connect(hass: HomeAssistant) -> None: async def test_create_entry(hass: HomeAssistant) -> None: """Test that the user step works.""" - with patch( - "homeassistant.components.gios.Gios._get_stations", - return_value=STATIONS, - ), patch( - "homeassistant.components.gios.Gios._get_station", - return_value=json.loads(load_fixture("gios/station.json")), - ), patch( - "homeassistant.components.gios.Gios._get_all_sensors", - return_value=json.loads(load_fixture("gios/sensors.json")), - ), patch( - "homeassistant.components.gios.Gios._get_indexes", - return_value=json.loads(load_fixture("gios/indexes.json")), + with ( + patch( + "homeassistant.components.gios.Gios._get_stations", + return_value=STATIONS, + ), + patch( + "homeassistant.components.gios.Gios._get_station", + return_value=json.loads(load_fixture("gios/station.json")), + ), + patch( + "homeassistant.components.gios.Gios._get_all_sensors", + return_value=json.loads(load_fixture("gios/sensors.json")), + ), + patch( + "homeassistant.components.gios.Gios._get_indexes", + return_value=json.loads(load_fixture("gios/indexes.json")), + ), ): flow = config_flow.GiosFlowHandler() flow.hass = hass diff --git a/tests/components/gios/test_init.py b/tests/components/gios/test_init.py index d20aecad3df..e5f3454bcd9 100644 --- a/tests/components/gios/test_init.py +++ b/tests/components/gios/test_init.py @@ -1,4 +1,5 @@ """Test init of GIOS integration.""" + import json from unittest.mock import patch @@ -74,15 +75,20 @@ async def test_migrate_device_and_config_entry( station = json.loads(load_fixture("gios/station.json")) sensors = json.loads(load_fixture("gios/sensors.json")) - with patch( - "homeassistant.components.gios.Gios._get_stations", return_value=STATIONS - ), patch( - "homeassistant.components.gios.Gios._get_station", - return_value=station, - ), patch( - "homeassistant.components.gios.Gios._get_all_sensors", - return_value=sensors, - ), patch("homeassistant.components.gios.Gios._get_indexes", return_value=indexes): + with ( + patch( + "homeassistant.components.gios.Gios._get_stations", return_value=STATIONS + ), + patch( + "homeassistant.components.gios.Gios._get_station", + return_value=station, + ), + patch( + "homeassistant.components.gios.Gios._get_all_sensors", + return_value=sensors, + ), + patch("homeassistant.components.gios.Gios._get_indexes", return_value=indexes), + ): config_entry.add_to_hass(hass) device_entry = device_registry.async_get_or_create( diff --git a/tests/components/gios/test_sensor.py b/tests/components/gios/test_sensor.py index 6714882ad3f..60e8722ba24 100644 --- a/tests/components/gios/test_sensor.py +++ b/tests/components/gios/test_sensor.py @@ -293,12 +293,15 @@ async def test_availability(hass: HomeAssistant) -> None: incomplete_sensors = deepcopy(sensors) incomplete_sensors["pm2.5"] = {} future = utcnow() + timedelta(minutes=120) - with patch( - "homeassistant.components.gios.Gios._get_all_sensors", - return_value=incomplete_sensors, - ), patch( - "homeassistant.components.gios.Gios._get_indexes", - return_value={}, + with ( + patch( + "homeassistant.components.gios.Gios._get_all_sensors", + return_value=incomplete_sensors, + ), + patch( + "homeassistant.components.gios.Gios._get_indexes", + return_value={}, + ), ): async_fire_time_changed(hass, future) await hass.async_block_till_done() @@ -319,11 +322,14 @@ async def test_availability(hass: HomeAssistant) -> None: assert state.state == STATE_UNAVAILABLE future = utcnow() + timedelta(minutes=180) - with patch( - "homeassistant.components.gios.Gios._get_all_sensors", return_value=sensors - ), patch( - "homeassistant.components.gios.Gios._get_indexes", - return_value=indexes, + with ( + patch( + "homeassistant.components.gios.Gios._get_all_sensors", return_value=sensors + ), + patch( + "homeassistant.components.gios.Gios._get_indexes", + return_value=indexes, + ), ): async_fire_time_changed(hass, future) await hass.async_block_till_done() diff --git a/tests/components/gios/test_system_health.py b/tests/components/gios/test_system_health.py index 11af7bad8f3..896f3cca04a 100644 --- a/tests/components/gios/test_system_health.py +++ b/tests/components/gios/test_system_health.py @@ -1,10 +1,12 @@ """Test GIOS system health.""" + import asyncio from aiohttp import ClientError from homeassistant.components.gios.const import DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.loader import async_get_integration from homeassistant.setup import async_setup_component from tests.common import get_system_health_info @@ -16,8 +18,11 @@ async def test_gios_system_health( ) -> None: """Test GIOS system health.""" aioclient_mock.get("http://api.gios.gov.pl/", text="") + integration = await async_get_integration(hass, DOMAIN) + await integration.async_get_component() hass.config.components.add(DOMAIN) assert await async_setup_component(hass, "system_health", {}) + await hass.async_block_till_done() info = await get_system_health_info(hass, DOMAIN) @@ -33,8 +38,11 @@ async def test_gios_system_health_fail( ) -> None: """Test GIOS system health.""" aioclient_mock.get("http://api.gios.gov.pl/", exc=ClientError) + integration = await async_get_integration(hass, DOMAIN) + await integration.async_get_component() hass.config.components.add(DOMAIN) assert await async_setup_component(hass, "system_health", {}) + await hass.async_block_till_done() info = await get_system_health_info(hass, DOMAIN) diff --git a/tests/components/github/common.py b/tests/components/github/common.py index 223722dccd8..d850ce1bba8 100644 --- a/tests/components/github/common.py +++ b/tests/components/github/common.py @@ -1,4 +1,5 @@ """Common helpers for GitHub integration tests.""" + from __future__ import annotations import json diff --git a/tests/components/github/conftest.py b/tests/components/github/conftest.py index b0b6f243fa0..2951a58702a 100644 --- a/tests/components/github/conftest.py +++ b/tests/components/github/conftest.py @@ -1,4 +1,5 @@ """conftest for the GitHub integration.""" + from collections.abc import Generator from unittest.mock import patch diff --git a/tests/components/github/test_config_flow.py b/tests/components/github/test_config_flow.py index 4f805cf43fc..c715889b7dc 100644 --- a/tests/components/github/test_config_flow.py +++ b/tests/components/github/test_config_flow.py @@ -1,4 +1,5 @@ """Test the GitHub config flow.""" + from unittest.mock import AsyncMock, MagicMock, patch from aiogithubapi import GitHubException diff --git a/tests/components/github/test_init.py b/tests/components/github/test_init.py index 03cc2e92720..c97a940b05c 100644 --- a/tests/components/github/test_init.py +++ b/tests/components/github/test_init.py @@ -1,9 +1,10 @@ """Test the GitHub init file.""" + import pytest from homeassistant.components.github import CONF_REPOSITORIES from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry as dr +from homeassistant.helpers import device_registry as dr, entity_registry as er, icon from .common import setup_github_integration @@ -20,7 +21,7 @@ async def test_device_registry_cleanup( aioclient_mock: AiohttpClientMocker, caplog: pytest.LogCaptureFixture, ) -> None: - """Test that we remove untracked repositories from the decvice registry.""" + """Test that we remove untracked repositories from the device registry.""" mock_config_entry.add_to_hass(hass) hass.config_entries.async_update_entry( mock_config_entry, @@ -112,3 +113,22 @@ async def test_subscription_setup_polling_disabled( "https://api.github.com/repos/home-assistant/core/events" in x[1] for x in aioclient_mock.mock_calls ) + + +# This tests needs to be adjusted to remove lingering tasks +@pytest.mark.parametrize("expected_lingering_tasks", [True]) +async def test_sensor_icons( + hass: HomeAssistant, + init_integration: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test to ensure that all sensor entities have an icon definition.""" + entities = er.async_entries_for_config_entry( + entity_registry, + config_entry_id=init_integration.entry_id, + ) + + icons = await icon.async_get_icons(hass, "entity", integrations=["github"]) + for entity in entities: + assert entity.translation_key is not None + assert icons["github"]["sensor"][entity.translation_key] is not None diff --git a/tests/components/github/test_sensor.py b/tests/components/github/test_sensor.py index f81f59e88c9..b0eaed3ae0e 100644 --- a/tests/components/github/test_sensor.py +++ b/tests/components/github/test_sensor.py @@ -1,4 +1,5 @@ """Test GitHub sensor.""" + import json import pytest diff --git a/tests/components/glances/conftest.py b/tests/components/glances/conftest.py index 9f4590ab5e0..339136f44e8 100644 --- a/tests/components/glances/conftest.py +++ b/tests/components/glances/conftest.py @@ -1,4 +1,5 @@ """Conftest for speedtestdotnet.""" + from unittest.mock import AsyncMock, patch import pytest diff --git a/tests/components/glances/snapshots/test_sensor.ambr b/tests/components/glances/snapshots/test_sensor.ambr index a9d3436aac8..23242f66071 100644 --- a/tests/components/glances/snapshots/test_sensor.ambr +++ b/tests/components/glances/snapshots/test_sensor.ambr @@ -43,6 +43,7 @@ 'context': , 'entity_id': 'sensor.0_0_0_0_containers_active', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '2', }) @@ -92,6 +93,7 @@ 'context': , 'entity_id': 'sensor.0_0_0_0_containers_cpu_usage', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '77.2', }) @@ -142,6 +144,7 @@ 'context': , 'entity_id': 'sensor.0_0_0_0_containers_memory_used', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '1149.6', }) @@ -192,6 +195,7 @@ 'context': , 'entity_id': 'sensor.0_0_0_0_cpu_thermal_1_temperature', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '59', }) @@ -242,6 +246,7 @@ 'context': , 'entity_id': 'sensor.0_0_0_0_err_temp_temperature', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unavailable', }) @@ -290,6 +295,7 @@ 'context': , 'entity_id': 'sensor.0_0_0_0_md1_available', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '2', }) @@ -338,6 +344,7 @@ 'context': , 'entity_id': 'sensor.0_0_0_0_md1_used', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '2', }) @@ -386,6 +393,7 @@ 'context': , 'entity_id': 'sensor.0_0_0_0_md3_available', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '2', }) @@ -434,6 +442,7 @@ 'context': , 'entity_id': 'sensor.0_0_0_0_md3_used', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '2', }) @@ -484,6 +493,7 @@ 'context': , 'entity_id': 'sensor.0_0_0_0_media_disk_free', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '426.5', }) @@ -533,6 +543,7 @@ 'context': , 'entity_id': 'sensor.0_0_0_0_media_disk_usage', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '6.7', }) @@ -583,6 +594,7 @@ 'context': , 'entity_id': 'sensor.0_0_0_0_media_disk_used', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '30.7', }) @@ -633,6 +645,7 @@ 'context': , 'entity_id': 'sensor.0_0_0_0_memory_free', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '2745.0', }) @@ -682,6 +695,7 @@ 'context': , 'entity_id': 'sensor.0_0_0_0_memory_usage', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '27.6', }) @@ -732,6 +746,7 @@ 'context': , 'entity_id': 'sensor.0_0_0_0_memory_use', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '1047.1', }) @@ -782,6 +797,7 @@ 'context': , 'entity_id': 'sensor.0_0_0_0_na_temp_temperature', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unavailable', }) @@ -832,6 +848,7 @@ 'context': , 'entity_id': 'sensor.0_0_0_0_ssl_disk_free', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '426.5', }) @@ -881,6 +898,7 @@ 'context': , 'entity_id': 'sensor.0_0_0_0_ssl_disk_usage', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '6.7', }) @@ -931,6 +949,7 @@ 'context': , 'entity_id': 'sensor.0_0_0_0_ssl_disk_used', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '30.7', }) diff --git a/tests/components/glances/test_config_flow.py b/tests/components/glances/test_config_flow.py index 8d590317c61..09dc638bb53 100644 --- a/tests/components/glances/test_config_flow.py +++ b/tests/components/glances/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for Glances config flow.""" + from unittest.mock import MagicMock from glances_api.exceptions import ( diff --git a/tests/components/glances/test_init.py b/tests/components/glances/test_init.py index 764426c6276..aa861dc5518 100644 --- a/tests/components/glances/test_init.py +++ b/tests/components/glances/test_init.py @@ -1,4 +1,5 @@ """Tests for Glances integration.""" + from unittest.mock import AsyncMock, MagicMock from glances_api.exceptions import ( diff --git a/tests/components/glances/test_sensor.py b/tests/components/glances/test_sensor.py index aeef1de0b09..ebe8b75b618 100644 --- a/tests/components/glances/test_sensor.py +++ b/tests/components/glances/test_sensor.py @@ -1,4 +1,5 @@ """Tests for glances sensors.""" + from syrupy import SnapshotAssertion from homeassistant.components.glances.const import DOMAIN diff --git a/tests/components/goalzero/__init__.py b/tests/components/goalzero/__init__.py index cde970f67a3..d2e990ca122 100644 --- a/tests/components/goalzero/__init__.py +++ b/tests/components/goalzero/__init__.py @@ -1,4 +1,5 @@ """Tests for the Goal Zero Yeti integration.""" + from unittest.mock import AsyncMock, patch from homeassistant.components import dhcp diff --git a/tests/components/goalzero/test_binary_sensor.py b/tests/components/goalzero/test_binary_sensor.py index 0a26d2adc2f..0e169fc9de0 100644 --- a/tests/components/goalzero/test_binary_sensor.py +++ b/tests/components/goalzero/test_binary_sensor.py @@ -1,4 +1,5 @@ """Binary sensor tests for the Goalzero integration.""" + from homeassistant.components.binary_sensor import BinarySensorDeviceClass from homeassistant.components.goalzero.const import DEFAULT_NAME from homeassistant.const import ATTR_DEVICE_CLASS, STATE_OFF, STATE_ON diff --git a/tests/components/goalzero/test_config_flow.py b/tests/components/goalzero/test_config_flow.py index 6d02730a572..7e57312c5b6 100644 --- a/tests/components/goalzero/test_config_flow.py +++ b/tests/components/goalzero/test_config_flow.py @@ -1,4 +1,5 @@ """Test Goal Zero Yeti config flow.""" + from unittest.mock import patch from goalzero import exceptions diff --git a/tests/components/goalzero/test_init.py b/tests/components/goalzero/test_init.py index 3a277d4cb53..1390561785e 100644 --- a/tests/components/goalzero/test_init.py +++ b/tests/components/goalzero/test_init.py @@ -1,4 +1,5 @@ """Test Goal Zero integration.""" + from datetime import timedelta from unittest.mock import patch diff --git a/tests/components/goalzero/test_switch.py b/tests/components/goalzero/test_switch.py index d97a4b9a3fd..de2e6035a12 100644 --- a/tests/components/goalzero/test_switch.py +++ b/tests/components/goalzero/test_switch.py @@ -1,4 +1,5 @@ """Switch tests for the Goalzero integration.""" + from homeassistant.components.goalzero.const import DEFAULT_NAME from homeassistant.components.switch import DOMAIN from homeassistant.const import ( diff --git a/tests/components/gogogate2/test_config_flow.py b/tests/components/gogogate2/test_config_flow.py index 6de04125783..a88dbd45116 100644 --- a/tests/components/gogogate2/test_config_flow.py +++ b/tests/components/gogogate2/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for the GogoGate2 component.""" + from ipaddress import ip_address from unittest.mock import MagicMock, patch diff --git a/tests/components/gogogate2/test_cover.py b/tests/components/gogogate2/test_cover.py index ca6509d53b9..001212fa17b 100644 --- a/tests/components/gogogate2/test_cover.py +++ b/tests/components/gogogate2/test_cover.py @@ -1,4 +1,5 @@ """Tests for the GogoGate2 component.""" + from datetime import timedelta from unittest.mock import MagicMock, patch diff --git a/tests/components/gogogate2/test_init.py b/tests/components/gogogate2/test_init.py index 5c0755bb91b..f7e58296a43 100644 --- a/tests/components/gogogate2/test_init.py +++ b/tests/components/gogogate2/test_init.py @@ -1,4 +1,5 @@ """Tests for the GogoGate2 component.""" + from unittest.mock import MagicMock, patch from ismartgate import GogoGate2Api @@ -91,8 +92,11 @@ async def test_api_failure_on_startup(hass: HomeAssistant) -> None: ) config_entry.add_to_hass(hass) - with patch( - "homeassistant.components.gogogate2.common.ISmartGateApi.async_info", - side_effect=TimeoutError, - ), pytest.raises(ConfigEntryNotReady): + with ( + patch( + "homeassistant.components.gogogate2.common.ISmartGateApi.async_info", + side_effect=TimeoutError, + ), + pytest.raises(ConfigEntryNotReady), + ): await async_setup_entry(hass, config_entry) diff --git a/tests/components/gogogate2/test_sensor.py b/tests/components/gogogate2/test_sensor.py index 8df88b2b4b7..610d9eda34f 100644 --- a/tests/components/gogogate2/test_sensor.py +++ b/tests/components/gogogate2/test_sensor.py @@ -1,4 +1,5 @@ """Tests for the GogoGate2 component.""" + from datetime import timedelta from unittest.mock import MagicMock, patch diff --git a/tests/components/goodwe/conftest.py b/tests/components/goodwe/conftest.py index cabb0f6ea10..0b4ce67d121 100644 --- a/tests/components/goodwe/conftest.py +++ b/tests/components/goodwe/conftest.py @@ -1,4 +1,5 @@ """Fixtures for the Aladdin Connect integration tests.""" + from unittest.mock import AsyncMock, MagicMock from goodwe import Inverter diff --git a/tests/components/goodwe/test_config_flow.py b/tests/components/goodwe/test_config_flow.py index c9bf5f1e9ff..0d0a1249ea1 100644 --- a/tests/components/goodwe/test_config_flow.py +++ b/tests/components/goodwe/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Goodwe config flow.""" + from unittest.mock import AsyncMock, patch from goodwe import InverterError @@ -35,12 +36,15 @@ async def test_manual_setup(hass: HomeAssistant) -> None: assert result["step_id"] == "user" assert not result["errors"] - with patch( - "homeassistant.components.goodwe.config_flow.connect", - return_value=mock_inverter(), - ), patch( - "homeassistant.components.goodwe.async_setup_entry", return_value=True - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.goodwe.config_flow.connect", + return_value=mock_inverter(), + ), + patch( + "homeassistant.components.goodwe.async_setup_entry", return_value=True + ) as mock_setup_entry, + ): result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_HOST: TEST_HOST} ) @@ -68,10 +72,13 @@ async def test_manual_setup_already_exists(hass: HomeAssistant) -> None: assert result["step_id"] == "user" assert not result["errors"] - with patch( - "homeassistant.components.goodwe.config_flow.connect", - return_value=mock_inverter(), - ), patch("homeassistant.components.goodwe.async_setup_entry", return_value=True): + with ( + patch( + "homeassistant.components.goodwe.config_flow.connect", + return_value=mock_inverter(), + ), + patch("homeassistant.components.goodwe.async_setup_entry", return_value=True), + ): result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_HOST: TEST_HOST} ) diff --git a/tests/components/goodwe/test_diagnostics.py b/tests/components/goodwe/test_diagnostics.py index edda2ed2cb7..21917265811 100644 --- a/tests/components/goodwe/test_diagnostics.py +++ b/tests/components/goodwe/test_diagnostics.py @@ -1,4 +1,5 @@ """Test the CO2Signal diagnostics.""" + from unittest.mock import MagicMock, patch from syrupy import SnapshotAssertion diff --git a/tests/components/google/conftest.py b/tests/components/google/conftest.py index 97d918c2e01..989e6690630 100644 --- a/tests/components/google/conftest.py +++ b/tests/components/google/conftest.py @@ -1,4 +1,5 @@ """Test configuration and mocks for the google integration.""" + from __future__ import annotations from collections.abc import Awaitable, Callable, Generator diff --git a/tests/components/google/test_calendar.py b/tests/components/google/test_calendar.py index 55a9f814a63..3946e432497 100644 --- a/tests/components/google/test_calendar.py +++ b/tests/components/google/test_calendar.py @@ -1,4 +1,5 @@ """The tests for the google calendar platform.""" + from __future__ import annotations from collections.abc import Awaitable, Callable diff --git a/tests/components/google/test_diagnostics.py b/tests/components/google/test_diagnostics.py index 32c61d0f945..32ed2ab3224 100644 --- a/tests/components/google/test_diagnostics.py +++ b/tests/components/google/test_diagnostics.py @@ -1,4 +1,5 @@ """Tests for diagnostics platform of google calendar.""" + from collections.abc import Callable import time from typing import Any diff --git a/tests/components/google/test_init.py b/tests/components/google/test_init.py index 26a5cb2e192..319f6be5012 100644 --- a/tests/components/google/test_init.py +++ b/tests/components/google/test_init.py @@ -1,4 +1,5 @@ """The tests for the Google Calendar component.""" + from __future__ import annotations from collections.abc import Awaitable, Callable @@ -815,7 +816,7 @@ async def test_calendar_yaml_update( assert await component_setup() mock_calendars_yaml().read.assert_called() - mock_calendars_yaml().write.called is expect_write_calls + assert mock_calendars_yaml().write.called is expect_write_calls state = hass.states.get(TEST_API_ENTITY) assert state diff --git a/tests/components/google_assistant/__init__.py b/tests/components/google_assistant/__init__.py index e24e6b740d3..73dc109f7e6 100644 --- a/tests/components/google_assistant/__init__.py +++ b/tests/components/google_assistant/__init__.py @@ -1,4 +1,5 @@ """Tests for the Google Assistant integration.""" + from unittest.mock import MagicMock from homeassistant.components.google_assistant import helpers, http diff --git a/tests/components/google_assistant/test_button.py b/tests/components/google_assistant/test_button.py index d3c5665b945..11ca77bf733 100644 --- a/tests/components/google_assistant/test_button.py +++ b/tests/components/google_assistant/test_button.py @@ -1,4 +1,5 @@ """Test buttons.""" + from unittest.mock import patch import pytest diff --git a/tests/components/google_assistant/test_data_redaction.py b/tests/components/google_assistant/test_data_redaction.py index 86b15782fe5..d650a223e15 100644 --- a/tests/components/google_assistant/test_data_redaction.py +++ b/tests/components/google_assistant/test_data_redaction.py @@ -1,4 +1,5 @@ """Test data redaction helpers.""" + import json from homeassistant.components.google_assistant.data_redaction import async_redact_msg diff --git a/tests/components/google_assistant/test_diagnostics.py b/tests/components/google_assistant/test_diagnostics.py index df8221b5053..26d91ce7920 100644 --- a/tests/components/google_assistant/test_diagnostics.py +++ b/tests/components/google_assistant/test_diagnostics.py @@ -1,4 +1,5 @@ """Test diagnostics.""" + from unittest.mock import patch import pytest @@ -34,16 +35,17 @@ async def test_diagnostics( ) -> None: """Test diagnostics v1.""" + await async_setup_component(hass, "homeassistant", {}) await setup.async_setup_component( hass, switch.DOMAIN, {"switch": [{"platform": "demo"}]} ) - await async_setup_component(hass, "homeassistant", {}) await async_setup_component( hass, ga.DOMAIN, {"google_assistant": DUMMY_CONFIG}, ) + await hass.async_block_till_done() config_entry = hass.config_entries.async_entries("google_assistant")[0] assert await get_diagnostics_for_config_entry( diff --git a/tests/components/google_assistant/test_google_assistant.py b/tests/components/google_assistant/test_google_assistant.py index 4fb6f50a5e6..4198e648b53 100644 --- a/tests/components/google_assistant/test_google_assistant.py +++ b/tests/components/google_assistant/test_google_assistant.py @@ -1,4 +1,5 @@ """The tests for the Google Assistant component.""" + from http import HTTPStatus import json from unittest.mock import patch @@ -253,7 +254,7 @@ async def test_query_climate_request( "thermostatTemperatureSetpoint": 21, "thermostatTemperatureAmbient": 22, "thermostatMode": "cool", - "thermostatHumidityAmbient": 54, + "thermostatHumidityAmbient": 54.2, "currentFanSpeedSetting": "on_high", } @@ -317,7 +318,7 @@ async def test_query_climate_request_f( "thermostatTemperatureSetpoint": -6.1, "thermostatTemperatureAmbient": -5.6, "thermostatMode": "cool", - "thermostatHumidityAmbient": 54, + "thermostatHumidityAmbient": 54.2, "currentFanSpeedSetting": "on_high", } hass_fixture.config.units.temperature_unit = UnitOfTemperature.CELSIUS @@ -362,8 +363,8 @@ async def test_query_humidifier_request( assert devices["humidifier.dehumidifier"] == { "on": True, "online": True, - "humiditySetpointPercent": 54, - "humidityAmbientPercent": 59, + "humiditySetpointPercent": 54.2, + "humidityAmbientPercent": 59.4, } assert devices["humidifier.hygrostat"] == { "on": True, diff --git a/tests/components/google_assistant/test_helpers.py b/tests/components/google_assistant/test_helpers.py index 1de1799358f..3f7fd91fed2 100644 --- a/tests/components/google_assistant/test_helpers.py +++ b/tests/components/google_assistant/test_helpers.py @@ -1,4 +1,5 @@ """Test Google Assistant helpers.""" + from datetime import timedelta from http import HTTPStatus from unittest.mock import Mock, call, patch @@ -403,7 +404,7 @@ async def test_config_local_sdk_allow_min_version( ) not in caplog.text -@pytest.mark.parametrize("version", (None, "2.1.4")) +@pytest.mark.parametrize("version", [None, "2.1.4"]) async def test_config_local_sdk_warn_version( hass: HomeAssistant, hass_client: ClientSessionGenerator, diff --git a/tests/components/google_assistant/test_http.py b/tests/components/google_assistant/test_http.py index 6f2d61d03ae..1dac75875a6 100644 --- a/tests/components/google_assistant/test_http.py +++ b/tests/components/google_assistant/test_http.py @@ -1,4 +1,5 @@ """Test Google http services.""" + from datetime import UTC, datetime, timedelta from http import HTTPStatus import json @@ -98,13 +99,17 @@ async def test_update_access_token(hass: HomeAssistant) -> None: await config.async_initialize() base_time = datetime(2019, 10, 14, tzinfo=UTC) - with patch( - "homeassistant.components.google_assistant.http._get_homegraph_token" - ) as mock_get_token, patch( - "homeassistant.components.google_assistant.http._get_homegraph_jwt" - ) as mock_get_jwt, patch( - "homeassistant.core.dt_util.utcnow", - ) as mock_utcnow: + with ( + patch( + "homeassistant.components.google_assistant.http._get_homegraph_token" + ) as mock_get_token, + patch( + "homeassistant.components.google_assistant.http._get_homegraph_jwt" + ) as mock_get_jwt, + patch( + "homeassistant.core.dt_util.utcnow", + ) as mock_utcnow, + ): mock_utcnow.return_value = base_time mock_get_jwt.return_value = jwt mock_get_token.return_value = MOCK_TOKEN diff --git a/tests/components/google_assistant/test_init.py b/tests/components/google_assistant/test_init.py index f33cf4354e3..270455d4f76 100644 --- a/tests/components/google_assistant/test_init.py +++ b/tests/components/google_assistant/test_init.py @@ -1,4 +1,5 @@ """The tests for google-assistant init.""" + from http import HTTPStatus from homeassistant.components import google_assistant as ga diff --git a/tests/components/google_assistant/test_logbook.py b/tests/components/google_assistant/test_logbook.py index 58b747a6f07..ca5b011dddc 100644 --- a/tests/components/google_assistant/test_logbook.py +++ b/tests/components/google_assistant/test_logbook.py @@ -1,4 +1,5 @@ """The tests for Google Assistant logbook.""" + from homeassistant.components.google_assistant.const import ( DOMAIN, EVENT_COMMAND_RECEIVED, @@ -18,6 +19,7 @@ async def test_humanify_command_received(hass: HomeAssistant) -> None: hass.config.components.add("frontend") hass.config.components.add("google_assistant") assert await async_setup_component(hass, "logbook", {}) + await hass.async_block_till_done() hass.states.async_set( "light.kitchen", "on", {ATTR_FRIENDLY_NAME: "The Kitchen Lights"} diff --git a/tests/components/google_assistant/test_report_state.py b/tests/components/google_assistant/test_report_state.py index 29ac7c3b48d..758ebf63db9 100644 --- a/tests/components/google_assistant/test_report_state.py +++ b/tests/components/google_assistant/test_report_state.py @@ -1,4 +1,5 @@ """Test Google report state.""" + from datetime import datetime, timedelta from http import HTTPStatus from unittest.mock import AsyncMock, patch @@ -26,9 +27,12 @@ async def test_report_state( "event.doorbell", "unknown", attributes={"device_class": "doorbell"} ) - with patch.object( - BASIC_CONFIG, "async_report_state_all", AsyncMock() - ) as mock_report, patch.object(report_state, "INITIAL_REPORT_DELAY", 0): + with ( + patch.object( + BASIC_CONFIG, "async_report_state_all", AsyncMock() + ) as mock_report, + patch.object(report_state, "INITIAL_REPORT_DELAY", 0), + ): unsub = report_state.async_enable_report_state(hass, BASIC_CONFIG) async_fire_time_changed(hass, utcnow()) @@ -73,10 +77,15 @@ async def test_report_state( } # Test that if serialize returns same value, we don't send - with patch( - "homeassistant.components.google_assistant.helpers.GoogleEntity.query_serialize", - return_value={"same": "info"}, - ), patch.object(BASIC_CONFIG, "async_report_state_all", AsyncMock()) as mock_report: + with ( + patch( + "homeassistant.components.google_assistant.helpers.GoogleEntity.query_serialize", + return_value={"same": "info"}, + ), + patch.object( + BASIC_CONFIG, "async_report_state_all", AsyncMock() + ) as mock_report, + ): # New state, so reported hass.states.async_set("light.double_report", "on") await hass.async_block_till_done() @@ -106,11 +115,14 @@ async def test_report_state( assert len(mock_report.mock_calls) == 0 # Test that entities that we can't query don't report a state - with patch.object( - BASIC_CONFIG, "async_report_state_all", AsyncMock() - ) as mock_report, patch( - "homeassistant.components.google_assistant.helpers.GoogleEntity.query_serialize", - side_effect=error.SmartHomeError("mock-error", "mock-msg"), + with ( + patch.object( + BASIC_CONFIG, "async_report_state_all", AsyncMock() + ) as mock_report, + patch( + "homeassistant.components.google_assistant.helpers.GoogleEntity.query_serialize", + side_effect=error.SmartHomeError("mock-error", "mock-msg"), + ), ): hass.states.async_set("light.kitchen", "off") async_fire_time_changed( @@ -147,9 +159,10 @@ async def test_report_notifications( "event.doorbell", "unknown", attributes={"device_class": "doorbell"} ) - with patch.object( - config, "async_report_state_all", AsyncMock() - ) as mock_report, patch.object(report_state, "INITIAL_REPORT_DELAY", 0): + with ( + patch.object(config, "async_report_state_all", AsyncMock()) as mock_report, + patch.object(report_state, "INITIAL_REPORT_DELAY", 0), + ): report_state.async_enable_report_state(hass, config) async_fire_time_changed( @@ -242,9 +255,12 @@ async def test_report_notifications( # Test disconnecting agent user caplog.clear() - with patch.object( - config, "async_report_state", return_value=HTTPStatus.NOT_FOUND - ) as mock_report_state, patch.object(config, "async_disconnect_agent_user"): + with ( + patch.object( + config, "async_report_state", return_value=HTTPStatus.NOT_FOUND + ) as mock_report_state, + patch.object(config, "async_disconnect_agent_user"), + ): event_time = datetime.fromisoformat("2023-08-01T01:03:57+00:00") epoc_event_time = event_time.timestamp() hass.states.async_set( diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index 587d89a1d8a..04ceafb004a 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -1,4 +1,5 @@ """Test Google Smart Home.""" + import asyncio from types import SimpleNamespace from unittest.mock import ANY, patch diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index c3b60a32850..0ed4d960edc 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -1,4 +1,5 @@ """Tests for the Google Assistant traits.""" + from datetime import datetime, timedelta from typing import Any from unittest.mock import ANY, patch @@ -3305,11 +3306,11 @@ async def test_openclose_cover_valve_no_position( @pytest.mark.parametrize( "device_class", - ( + [ cover.CoverDeviceClass.DOOR, cover.CoverDeviceClass.GARAGE, cover.CoverDeviceClass.GATE, - ), + ], ) async def test_openclose_cover_secure(hass: HomeAssistant, device_class) -> None: """Test OpenClose trait support for cover domain.""" @@ -3371,13 +3372,13 @@ async def test_openclose_cover_secure(hass: HomeAssistant, device_class) -> None @pytest.mark.parametrize( "device_class", - ( + [ binary_sensor.BinarySensorDeviceClass.DOOR, binary_sensor.BinarySensorDeviceClass.GARAGE_DOOR, binary_sensor.BinarySensorDeviceClass.LOCK, binary_sensor.BinarySensorDeviceClass.OPENING, binary_sensor.BinarySensorDeviceClass.WINDOW, - ), + ], ) async def test_openclose_binary_sensor(hass: HomeAssistant, device_class) -> None: """Test OpenClose trait support for binary_sensor domain.""" @@ -3815,7 +3816,7 @@ async def test_transport_control( @pytest.mark.parametrize( "state", - ( + [ STATE_OFF, STATE_IDLE, STATE_PLAYING, @@ -3824,7 +3825,7 @@ async def test_transport_control( STATE_STANDBY, STATE_UNAVAILABLE, STATE_UNKNOWN, - ), + ], ) async def test_media_state(hass: HomeAssistant, state) -> None: """Test the MediaStateTrait.""" diff --git a/tests/components/google_assistant_sdk/conftest.py b/tests/components/google_assistant_sdk/conftest.py index c994a8b12e3..6922b078574 100644 --- a/tests/components/google_assistant_sdk/conftest.py +++ b/tests/components/google_assistant_sdk/conftest.py @@ -1,4 +1,5 @@ """PyTest fixtures and test helpers.""" + from collections.abc import Awaitable, Callable, Coroutine import time from typing import Any diff --git a/tests/components/google_assistant_sdk/test_config_flow.py b/tests/components/google_assistant_sdk/test_config_flow.py index c65477b18b1..49e849398af 100644 --- a/tests/components/google_assistant_sdk/test_config_flow.py +++ b/tests/components/google_assistant_sdk/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Google Assistant SDK config flow.""" + from unittest.mock import patch from homeassistant import config_entries diff --git a/tests/components/google_assistant_sdk/test_helpers.py b/tests/components/google_assistant_sdk/test_helpers.py index 03a04097d67..1090eb9da45 100644 --- a/tests/components/google_assistant_sdk/test_helpers.py +++ b/tests/components/google_assistant_sdk/test_helpers.py @@ -1,4 +1,5 @@ """Test the Google Assistant SDK helpers.""" + from homeassistant.components.google_assistant_sdk.const import SUPPORTED_LANGUAGE_CODES from homeassistant.components.google_assistant_sdk.helpers import ( DEFAULT_LANGUAGE_CODES, diff --git a/tests/components/google_assistant_sdk/test_init.py b/tests/components/google_assistant_sdk/test_init.py index 5aa68093627..2d930599c24 100644 --- a/tests/components/google_assistant_sdk/test_init.py +++ b/tests/components/google_assistant_sdk/test_init.py @@ -1,4 +1,5 @@ """Tests for Google Assistant SDK.""" + from datetime import timedelta import http import time diff --git a/tests/components/google_assistant_sdk/test_notify.py b/tests/components/google_assistant_sdk/test_notify.py index 3320bb944b2..0ffdc3c5660 100644 --- a/tests/components/google_assistant_sdk/test_notify.py +++ b/tests/components/google_assistant_sdk/test_notify.py @@ -1,4 +1,5 @@ """Tests for the Google Assistant notify.""" + from unittest.mock import call, patch import pytest diff --git a/tests/components/google_domains/test_init.py b/tests/components/google_domains/test_init.py index 12f5e509736..a682d4ad090 100644 --- a/tests/components/google_domains/test_init.py +++ b/tests/components/google_domains/test_init.py @@ -1,4 +1,5 @@ """Test the Google Domains component.""" + from datetime import timedelta import pytest diff --git a/tests/components/google_generative_ai_conversation/conftest.py b/tests/components/google_generative_ai_conversation/conftest.py index 0a45a991bf8..66dfd980cf3 100644 --- a/tests/components/google_generative_ai_conversation/conftest.py +++ b/tests/components/google_generative_ai_conversation/conftest.py @@ -1,4 +1,5 @@ """Tests helpers.""" + from unittest.mock import patch import pytest diff --git a/tests/components/google_generative_ai_conversation/test_config_flow.py b/tests/components/google_generative_ai_conversation/test_config_flow.py index 4a2478c5a7a..6ae42a350e6 100644 --- a/tests/components/google_generative_ai_conversation/test_config_flow.py +++ b/tests/components/google_generative_ai_conversation/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Google Generative AI Conversation config flow.""" + from unittest.mock import patch from google.api_core.exceptions import ClientError @@ -38,12 +39,15 @@ async def test_form(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.FORM assert result["errors"] is None - with patch( - "homeassistant.components.google_generative_ai_conversation.config_flow.genai.list_models", - ), patch( - "homeassistant.components.google_generative_ai_conversation.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.google_generative_ai_conversation.config_flow.genai.list_models", + ), + patch( + "homeassistant.components.google_generative_ai_conversation.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { diff --git a/tests/components/google_generative_ai_conversation/test_init.py b/tests/components/google_generative_ai_conversation/test_init.py index eee00fadfac..b77fa14b4cf 100644 --- a/tests/components/google_generative_ai_conversation/test_init.py +++ b/tests/components/google_generative_ai_conversation/test_init.py @@ -1,4 +1,5 @@ """Tests for the Google Generative AI Conversation integration.""" + from unittest.mock import AsyncMock, MagicMock, patch from google.api_core.exceptions import ClientError @@ -129,9 +130,12 @@ async def test_template_error( "prompt": "talk like a {% if True %}smarthome{% else %}pirate please.", }, ) - with patch( - "google.generativeai.get_model", - ), patch("google.generativeai.GenerativeModel"): + with ( + patch( + "google.generativeai.get_model", + ), + patch("google.generativeai.GenerativeModel"), + ): await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() result = await conversation.async_converse( @@ -197,11 +201,14 @@ async def test_generate_content_service_with_image( "A mail carrier is at your front door delivering a package" ) - with patch("google.generativeai.GenerativeModel") as mock_model, patch( - "homeassistant.components.google_generative_ai_conversation.Path.read_bytes", - return_value=b"image bytes", - ), patch("pathlib.Path.exists", return_value=True), patch.object( - hass.config, "is_allowed_path", return_value=True + with ( + patch("google.generativeai.GenerativeModel") as mock_model, + patch( + "homeassistant.components.google_generative_ai_conversation.Path.read_bytes", + return_value=b"image bytes", + ), + patch("pathlib.Path.exists", return_value=True), + patch.object(hass.config, "is_allowed_path", return_value=True), ): mock_response = MagicMock() mock_response.text = stubbed_generated_content @@ -231,8 +238,11 @@ async def test_generate_content_service_error( mock_config_entry: MockConfigEntry, ) -> None: """Test generate content service handles errors.""" - with patch("google.generativeai.GenerativeModel") as mock_model, pytest.raises( - HomeAssistantError, match="Error generating content: None reason" + with ( + patch("google.generativeai.GenerativeModel") as mock_model, + pytest.raises( + HomeAssistantError, match="Error generating content: None reason" + ), ): mock_model.return_value.generate_content_async = AsyncMock( side_effect=ClientError("reason") @@ -253,14 +263,16 @@ async def test_generate_content_service_with_image_not_allowed_path( snapshot: SnapshotAssertion, ) -> None: """Test generate content service with an image in a not allowed path.""" - with patch("pathlib.Path.exists", return_value=True), patch.object( - hass.config, "is_allowed_path", return_value=False - ), pytest.raises( - HomeAssistantError, - match=( - "Cannot read `doorbell_snapshot.jpg`, no access to path; " - "`allowlist_external_dirs` may need to be adjusted in " - "`configuration.yaml`" + with ( + patch("pathlib.Path.exists", return_value=True), + patch.object(hass.config, "is_allowed_path", return_value=False), + pytest.raises( + HomeAssistantError, + match=( + "Cannot read `doorbell_snapshot.jpg`, no access to path; " + "`allowlist_external_dirs` may need to be adjusted in " + "`configuration.yaml`" + ), ), ): await hass.services.async_call( @@ -282,10 +294,13 @@ async def test_generate_content_service_with_image_not_exists( snapshot: SnapshotAssertion, ) -> None: """Test generate content service with an image that does not exist.""" - with patch("pathlib.Path.exists", return_value=True), patch.object( - hass.config, "is_allowed_path", return_value=True - ), patch("pathlib.Path.exists", return_value=False), pytest.raises( - HomeAssistantError, match="`doorbell_snapshot.jpg` does not exist" + with ( + patch("pathlib.Path.exists", return_value=True), + patch.object(hass.config, "is_allowed_path", return_value=True), + patch("pathlib.Path.exists", return_value=False), + pytest.raises( + HomeAssistantError, match="`doorbell_snapshot.jpg` does not exist" + ), ): await hass.services.async_call( "google_generative_ai_conversation", @@ -306,10 +321,13 @@ async def test_generate_content_service_with_non_image( snapshot: SnapshotAssertion, ) -> None: """Test generate content service with a non image.""" - with patch("pathlib.Path.exists", return_value=True), patch.object( - hass.config, "is_allowed_path", return_value=True - ), patch("pathlib.Path.exists", return_value=True), pytest.raises( - HomeAssistantError, match="`doorbell_snapshot.mp4` is not an image" + with ( + patch("pathlib.Path.exists", return_value=True), + patch.object(hass.config, "is_allowed_path", return_value=True), + patch("pathlib.Path.exists", return_value=True), + pytest.raises( + HomeAssistantError, match="`doorbell_snapshot.mp4` is not an image" + ), ): await hass.services.async_call( "google_generative_ai_conversation", diff --git a/tests/components/google_mail/conftest.py b/tests/components/google_mail/conftest.py index c3318b37f0f..947d5fe2fb1 100644 --- a/tests/components/google_mail/conftest.py +++ b/tests/components/google_mail/conftest.py @@ -1,4 +1,5 @@ """Configure tests for the Google Mail integration.""" + from collections.abc import Awaitable, Callable, Coroutine import time from typing import Any diff --git a/tests/components/google_mail/test_config_flow.py b/tests/components/google_mail/test_config_flow.py index dbf06c26205..62db6603988 100644 --- a/tests/components/google_mail/test_config_flow.py +++ b/tests/components/google_mail/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Google Mail config flow.""" + from unittest.mock import patch from httplib2 import Response @@ -45,13 +46,16 @@ async def test_full_flow( assert resp.status == 200 assert resp.headers["content-type"] == "text/html; charset=utf-8" - with patch( - "homeassistant.components.google_mail.async_setup_entry", return_value=True - ) as mock_setup, patch( - "httplib2.Http.request", - return_value=( - Response({}), - bytes(load_fixture("google_mail/get_profile.json"), encoding="UTF-8"), + with ( + patch( + "homeassistant.components.google_mail.async_setup_entry", return_value=True + ) as mock_setup, + patch( + "httplib2.Http.request", + return_value=( + Response({}), + bytes(load_fixture("google_mail/get_profile.json"), encoding="UTF-8"), + ), ), ): result = await hass.config_entries.flow.async_configure(result["flow_id"]) @@ -140,13 +144,16 @@ async def test_reauth( }, ) - with patch( - "homeassistant.components.google_mail.async_setup_entry", return_value=True - ) as mock_setup, patch( - "httplib2.Http.request", - return_value=( - Response({}), - bytes(load_fixture(f"google_mail/{fixture}.json"), encoding="UTF-8"), + with ( + patch( + "homeassistant.components.google_mail.async_setup_entry", return_value=True + ) as mock_setup, + patch( + "httplib2.Http.request", + return_value=( + Response({}), + bytes(load_fixture(f"google_mail/{fixture}.json"), encoding="UTF-8"), + ), ), ): result = await hass.config_entries.flow.async_configure(result["flow_id"]) diff --git a/tests/components/google_mail/test_init.py b/tests/components/google_mail/test_init.py index ef2f1475dad..6e7def716a9 100644 --- a/tests/components/google_mail/test_init.py +++ b/tests/components/google_mail/test_init.py @@ -1,4 +1,5 @@ """Tests for Google Mail.""" + import http import time from unittest.mock import patch diff --git a/tests/components/google_mail/test_notify.py b/tests/components/google_mail/test_notify.py index 1e9a174d81f..7373047b46e 100644 --- a/tests/components/google_mail/test_notify.py +++ b/tests/components/google_mail/test_notify.py @@ -1,4 +1,5 @@ """Notify tests for the Google Mail integration.""" + from unittest.mock import patch import pytest diff --git a/tests/components/google_mail/test_sensor.py b/tests/components/google_mail/test_sensor.py index 248622d3157..6f2f1a4ec32 100644 --- a/tests/components/google_mail/test_sensor.py +++ b/tests/components/google_mail/test_sensor.py @@ -1,4 +1,5 @@ """Sensor tests for the Google Mail integration.""" + from datetime import timedelta from unittest.mock import patch @@ -45,7 +46,7 @@ async def test_sensors( ): next_update = dt_util.utcnow() + timedelta(minutes=15) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(SENSOR) assert state.state == result @@ -60,7 +61,7 @@ async def test_sensor_reauth_trigger( with patch(TOKEN, side_effect=RefreshError): next_update = dt_util.utcnow() + timedelta(minutes=15) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) flows = hass.config_entries.flow.async_progress() diff --git a/tests/components/google_mail/test_services.py b/tests/components/google_mail/test_services.py index caa0d887dec..c8679de75e4 100644 --- a/tests/components/google_mail/test_services.py +++ b/tests/components/google_mail/test_services.py @@ -1,4 +1,5 @@ """Services tests for the Google Mail integration.""" + from unittest.mock import patch from aiohttp.client_exceptions import ClientResponseError @@ -61,10 +62,10 @@ async def test_set_vacation( @pytest.mark.parametrize( ("side_effect"), - ( + [ (RefreshError,), (ClientResponseError("", (), status=400),), - ), + ], ) async def test_reauth_trigger( hass: HomeAssistant, diff --git a/tests/components/google_pubsub/test_init.py b/tests/components/google_pubsub/test_init.py index 0a1d4741268..e397ab2c403 100644 --- a/tests/components/google_pubsub/test_init.py +++ b/tests/components/google_pubsub/test_init.py @@ -1,4 +1,5 @@ """The tests for the Google Pub/Sub component.""" + from dataclasses import dataclass from datetime import datetime import os diff --git a/tests/components/google_sheets/test_config_flow.py b/tests/components/google_sheets/test_config_flow.py index c2a12d65a97..edf4580485f 100644 --- a/tests/components/google_sheets/test_config_flow.py +++ b/tests/components/google_sheets/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Google Sheets config flow.""" + from collections.abc import Generator from unittest.mock import Mock, patch diff --git a/tests/components/google_sheets/test_init.py b/tests/components/google_sheets/test_init.py index 8f7ce7603e8..f474e44e925 100644 --- a/tests/components/google_sheets/test_init.py +++ b/tests/components/google_sheets/test_init.py @@ -229,9 +229,12 @@ async def test_append_sheet_api_error( response = Response() response.status_code = 503 - with pytest.raises(HomeAssistantError), patch( - "homeassistant.components.google_sheets.Client.request", - side_effect=APIError(response), + with ( + pytest.raises(HomeAssistantError), + patch( + "homeassistant.components.google_sheets.Client.request", + side_effect=APIError(response), + ), ): await hass.services.async_call( DOMAIN, diff --git a/tests/components/google_tasks/conftest.py b/tests/components/google_tasks/conftest.py index 60387889aad..87ddb2ed81d 100644 --- a/tests/components/google_tasks/conftest.py +++ b/tests/components/google_tasks/conftest.py @@ -1,6 +1,5 @@ """Test fixtures for Google Tasks.""" - from collections.abc import Awaitable, Callable import time from typing import Any diff --git a/tests/components/google_tasks/test_config_flow.py b/tests/components/google_tasks/test_config_flow.py index e92da605697..d7ad21292fc 100644 --- a/tests/components/google_tasks/test_config_flow.py +++ b/tests/components/google_tasks/test_config_flow.py @@ -63,9 +63,12 @@ async def test_full_flow( }, ) - with patch( - "homeassistant.components.google_tasks.async_setup_entry", return_value=True - ) as mock_setup, patch("homeassistant.components.google_tasks.config_flow.build"): + with ( + patch( + "homeassistant.components.google_tasks.async_setup_entry", return_value=True + ) as mock_setup, + patch("homeassistant.components.google_tasks.config_flow.build"), + ): result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == FlowResultType.CREATE_ENTRY assert len(hass.config_entries.async_entries(DOMAIN)) == 1 diff --git a/tests/components/google_tasks/test_init.py b/tests/components/google_tasks/test_init.py index b486942f70a..061bf549748 100644 --- a/tests/components/google_tasks/test_init.py +++ b/tests/components/google_tasks/test_init.py @@ -1,4 +1,5 @@ """Tests for Google Tasks.""" + from collections.abc import Awaitable, Callable import http import time diff --git a/tests/components/google_tasks/test_todo.py b/tests/components/google_tasks/test_todo.py index ee1b1e4cfd4..83d419439d7 100644 --- a/tests/components/google_tasks/test_todo.py +++ b/tests/components/google_tasks/test_todo.py @@ -1,6 +1,5 @@ """Tests for Google Tasks todo platform.""" - from collections.abc import Awaitable, Callable from http import HTTPStatus import json diff --git a/tests/components/google_translate/conftest.py b/tests/components/google_translate/conftest.py index 34132fc5c1d..3600fae3841 100644 --- a/tests/components/google_translate/conftest.py +++ b/tests/components/google_translate/conftest.py @@ -1,4 +1,5 @@ """Common fixtures for the Google Translate text-to-speech tests.""" + from collections.abc import Generator from unittest.mock import AsyncMock, patch diff --git a/tests/components/google_translate/test_config_flow.py b/tests/components/google_translate/test_config_flow.py index e9a41e8eea6..a4104fc0908 100644 --- a/tests/components/google_translate/test_config_flow.py +++ b/tests/components/google_translate/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Google Translate text-to-speech config flow.""" + from unittest.mock import AsyncMock import pytest diff --git a/tests/components/google_translate/test_tts.py b/tests/components/google_translate/test_tts.py index fd1ddd8a4f2..1df609b0db4 100644 --- a/tests/components/google_translate/test_tts.py +++ b/tests/components/google_translate/test_tts.py @@ -1,4 +1,5 @@ """The tests for the Google speech platform.""" + from __future__ import annotations from collections.abc import Generator diff --git a/tests/components/google_travel_time/conftest.py b/tests/components/google_travel_time/conftest.py index cef8dfeb65c..141b40eff29 100644 --- a/tests/components/google_travel_time/conftest.py +++ b/tests/components/google_travel_time/conftest.py @@ -1,4 +1,5 @@ """Fixtures for Google Time Travel tests.""" + from unittest.mock import patch from googlemaps.exceptions import ApiError, Timeout, TransportError @@ -47,9 +48,12 @@ def bypass_platform_setup_fixture(): @pytest.fixture(name="validate_config_entry") def validate_config_entry_fixture(): """Return valid config entry.""" - with patch("homeassistant.components.google_travel_time.helpers.Client"), patch( - "homeassistant.components.google_travel_time.helpers.distance_matrix" - ) as distance_matrix_mock: + with ( + patch("homeassistant.components.google_travel_time.helpers.Client"), + patch( + "homeassistant.components.google_travel_time.helpers.distance_matrix" + ) as distance_matrix_mock, + ): distance_matrix_mock.return_value = None yield distance_matrix_mock diff --git a/tests/components/google_travel_time/const.py b/tests/components/google_travel_time/const.py index 844766ceffa..77e99ffbf68 100644 --- a/tests/components/google_travel_time/const.py +++ b/tests/components/google_travel_time/const.py @@ -1,6 +1,5 @@ """Constants for google_travel_time tests.""" - from homeassistant.components.google_travel_time.const import ( CONF_DESTINATION, CONF_ORIGIN, diff --git a/tests/components/google_travel_time/test_config_flow.py b/tests/components/google_travel_time/test_config_flow.py index b701fcb2143..24e9cb1297a 100644 --- a/tests/components/google_travel_time/test_config_flow.py +++ b/tests/components/google_travel_time/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Google Maps Travel Time config flow.""" + import pytest from homeassistant import config_entries, data_entry_flow diff --git a/tests/components/google_travel_time/test_sensor.py b/tests/components/google_travel_time/test_sensor.py index bb1ba722540..a7fb263d4c9 100644 --- a/tests/components/google_travel_time/test_sensor.py +++ b/tests/components/google_travel_time/test_sensor.py @@ -26,9 +26,12 @@ from tests.common import MockConfigEntry @pytest.fixture(name="mock_update") def mock_update_fixture(): """Mock an update to the sensor.""" - with patch("homeassistant.components.google_travel_time.sensor.Client"), patch( - "homeassistant.components.google_travel_time.sensor.distance_matrix" - ) as distance_matrix_mock: + with ( + patch("homeassistant.components.google_travel_time.sensor.Client"), + patch( + "homeassistant.components.google_travel_time.sensor.distance_matrix" + ) as distance_matrix_mock, + ): distance_matrix_mock.return_value = { "rows": [ { @@ -224,9 +227,12 @@ async def test_sensor_unit_system( entry_id="test", ) config_entry.add_to_hass(hass) - with patch("homeassistant.components.google_travel_time.sensor.Client"), patch( - "homeassistant.components.google_travel_time.sensor.distance_matrix" - ) as distance_matrix_mock: + with ( + patch("homeassistant.components.google_travel_time.sensor.Client"), + patch( + "homeassistant.components.google_travel_time.sensor.distance_matrix" + ) as distance_matrix_mock, + ): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/google_wifi/test_sensor.py b/tests/components/google_wifi/test_sensor.py index 3eceac64904..fcc5603fdc5 100644 --- a/tests/components/google_wifi/test_sensor.py +++ b/tests/components/google_wifi/test_sensor.py @@ -1,4 +1,5 @@ """The tests for the Google Wifi platform.""" + from datetime import datetime, timedelta from http import HTTPStatus from unittest.mock import Mock, patch diff --git a/tests/components/govee_ble/__init__.py b/tests/components/govee_ble/__init__.py index c093a6dddb5..60930d1dd0e 100644 --- a/tests/components/govee_ble/__init__.py +++ b/tests/components/govee_ble/__init__.py @@ -1,6 +1,5 @@ """Tests for the Govee BLE integration.""" - from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo NOT_GOVEE_SERVICE_INFO = BluetoothServiceInfo( diff --git a/tests/components/govee_ble/test_config_flow.py b/tests/components/govee_ble/test_config_flow.py index fee37a0a886..4b498b2618a 100644 --- a/tests/components/govee_ble/test_config_flow.py +++ b/tests/components/govee_ble/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Govee config flow.""" + from unittest.mock import patch from homeassistant import config_entries diff --git a/tests/components/govee_ble/test_sensor.py b/tests/components/govee_ble/test_sensor.py index 55f3d293096..caed4a5c469 100644 --- a/tests/components/govee_ble/test_sensor.py +++ b/tests/components/govee_ble/test_sensor.py @@ -1,4 +1,5 @@ """Test the Govee BLE sensors.""" + from datetime import timedelta import time @@ -113,9 +114,12 @@ async def test_gvh5178_multi_sensor(hass: HomeAssistant) -> None: # Fastforward time without BLE advertisements monotonic_now = start_monotonic + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1 - with patch_bluetooth_time( - monotonic_now, - ), patch_all_discovered_devices([]): + with ( + patch_bluetooth_time( + monotonic_now, + ), + patch_all_discovered_devices([]), + ): async_fire_time_changed( hass, dt_util.utcnow() @@ -139,9 +143,12 @@ async def test_gvh5178_multi_sensor(hass: HomeAssistant) -> None: assert primary_temp_sensor.state == "1.0" # Fastforward time without BLE advertisements - with patch_bluetooth_time( - monotonic_now, - ), patch_all_discovered_devices([]): + with ( + patch_bluetooth_time( + monotonic_now, + ), + patch_all_discovered_devices([]), + ): async_fire_time_changed( hass, dt_util.utcnow() diff --git a/tests/components/govee_light_local/conftest.py b/tests/components/govee_light_local/conftest.py index 2b3690f7011..5976d3c1b74 100644 --- a/tests/components/govee_light_local/conftest.py +++ b/tests/components/govee_light_local/conftest.py @@ -1,4 +1,5 @@ """Tests configuration for Govee Local API.""" + from collections.abc import Generator from unittest.mock import AsyncMock, patch diff --git a/tests/components/govee_light_local/test_config_flow.py b/tests/components/govee_light_local/test_config_flow.py index 7753b40c29c..79baef33969 100644 --- a/tests/components/govee_light_local/test_config_flow.py +++ b/tests/components/govee_light_local/test_config_flow.py @@ -1,4 +1,5 @@ """Test Govee light local config flow.""" + from unittest.mock import AsyncMock, patch from govee_local_api import GoveeDevice @@ -17,9 +18,15 @@ async def test_creating_entry_has_no_devices( mock_govee_api.devices = [] - with patch( - "homeassistant.components.govee_light_local.config_flow.GoveeController", - return_value=mock_govee_api, + with ( + patch( + "homeassistant.components.govee_light_local.config_flow.GoveeController", + return_value=mock_govee_api, + ), + patch( + "homeassistant.components.govee_light_local.config_flow.DISCOVERY_TIMEOUT", + 0, + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} diff --git a/tests/components/govee_light_local/test_light.py b/tests/components/govee_light_local/test_light.py index 66f471df267..3bc9da77fe5 100644 --- a/tests/components/govee_light_local/test_light.py +++ b/tests/components/govee_light_local/test_light.py @@ -131,8 +131,8 @@ async def test_light_setup_retry( entry.add_to_hass(hass) with patch( - "homeassistant.components.govee_light_local.asyncio.timeout", - side_effect=TimeoutError, + "homeassistant.components.govee_light_local.DISCOVERY_TIMEOUT", + 0, ): await hass.config_entries.async_setup(entry.entry_id) assert entry.state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/gpsd/conftest.py b/tests/components/gpsd/conftest.py index c2bd2b8564a..71bb3aa61bf 100644 --- a/tests/components/gpsd/conftest.py +++ b/tests/components/gpsd/conftest.py @@ -1,4 +1,5 @@ """Common fixtures for the GPSD tests.""" + from collections.abc import Generator from unittest.mock import AsyncMock, patch diff --git a/tests/components/gpsd/test_config_flow.py b/tests/components/gpsd/test_config_flow.py index 0b0465b026d..81d6681dabd 100644 --- a/tests/components/gpsd/test_config_flow.py +++ b/tests/components/gpsd/test_config_flow.py @@ -1,4 +1,5 @@ """Test the GPSD config flow.""" + from unittest.mock import AsyncMock, patch from gps3.agps3threaded import GPSD_PORT as DEFAULT_PORT diff --git a/tests/components/gpslogger/test_init.py b/tests/components/gpslogger/test_init.py index 3873695033e..cfe9d050c69 100644 --- a/tests/components/gpslogger/test_init.py +++ b/tests/components/gpslogger/test_init.py @@ -1,4 +1,5 @@ """The tests the for GPSLogger device tracker platform.""" + from http import HTTPStatus from unittest.mock import patch diff --git a/tests/components/graphite/test_init.py b/tests/components/graphite/test_init.py index fec75bf5fcc..738f8eda333 100644 --- a/tests/components/graphite/test_init.py +++ b/tests/components/graphite/test_init.py @@ -1,4 +1,5 @@ """The tests for the Graphite component.""" + import socket from unittest import mock from unittest.mock import patch diff --git a/tests/components/gree/common.py b/tests/components/gree/common.py index aa88688486c..97656596ce6 100644 --- a/tests/components/gree/common.py +++ b/tests/components/gree/common.py @@ -1,4 +1,5 @@ """Common helpers for gree test cases.""" + import asyncio import logging from unittest.mock import AsyncMock, Mock diff --git a/tests/components/gree/conftest.py b/tests/components/gree/conftest.py index 8ef5f7bb38f..18113e6530c 100644 --- a/tests/components/gree/conftest.py +++ b/tests/components/gree/conftest.py @@ -1,4 +1,5 @@ """Pytest module configuration.""" + from collections.abc import Generator from unittest.mock import AsyncMock, patch diff --git a/tests/components/gree/snapshots/test_climate.ambr b/tests/components/gree/snapshots/test_climate.ambr index e5d733bc6f0..4f62be5cded 100644 --- a/tests/components/gree/snapshots/test_climate.ambr +++ b/tests/components/gree/snapshots/test_climate.ambr @@ -46,6 +46,7 @@ 'context': , 'entity_id': 'climate.fake_device_1', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }), diff --git a/tests/components/gree/snapshots/test_switch.ambr b/tests/components/gree/snapshots/test_switch.ambr index 988771cd517..71c6d3ea71d 100644 --- a/tests/components/gree/snapshots/test_switch.ambr +++ b/tests/components/gree/snapshots/test_switch.ambr @@ -9,6 +9,7 @@ 'context': , 'entity_id': 'switch.fake_device_1_panel_light', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }), @@ -20,6 +21,7 @@ 'context': , 'entity_id': 'switch.fake_device_1_quiet', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }), @@ -31,6 +33,7 @@ 'context': , 'entity_id': 'switch.fake_device_1_fresh_air', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }), @@ -42,6 +45,7 @@ 'context': , 'entity_id': 'switch.fake_device_1_xfan', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }), @@ -53,6 +57,7 @@ 'context': , 'entity_id': 'switch.fake_device_1_health_mode', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }), diff --git a/tests/components/gree/test_bridge.py b/tests/components/gree/test_bridge.py index f40ab6525d4..37b0b0dc15e 100644 --- a/tests/components/gree/test_bridge.py +++ b/tests/components/gree/test_bridge.py @@ -1,4 +1,5 @@ """Tests for gree component.""" + from datetime import timedelta from freezegun.api import FrozenDateTimeFactory diff --git a/tests/components/gree/test_climate.py b/tests/components/gree/test_climate.py index 5b261fa266b..0bd767e4f35 100644 --- a/tests/components/gree/test_climate.py +++ b/tests/components/gree/test_climate.py @@ -1,4 +1,5 @@ """Tests for gree component.""" + from datetime import timedelta from unittest.mock import DEFAULT as DEFAULT_MOCK, AsyncMock, patch @@ -513,7 +514,7 @@ async def test_update_target_temperature( @pytest.mark.parametrize( - "preset", (PRESET_AWAY, PRESET_ECO, PRESET_SLEEP, PRESET_BOOST, PRESET_NONE) + "preset", [PRESET_AWAY, PRESET_ECO, PRESET_SLEEP, PRESET_BOOST, PRESET_NONE] ) async def test_send_preset_mode( hass: HomeAssistant, discovery, device, mock_now, preset @@ -553,7 +554,7 @@ async def test_send_invalid_preset_mode( @pytest.mark.parametrize( - "preset", (PRESET_AWAY, PRESET_ECO, PRESET_SLEEP, PRESET_BOOST, PRESET_NONE) + "preset", [PRESET_AWAY, PRESET_ECO, PRESET_SLEEP, PRESET_BOOST, PRESET_NONE] ) async def test_send_preset_mode_device_timeout( hass: HomeAssistant, discovery, device, mock_now, preset @@ -576,7 +577,7 @@ async def test_send_preset_mode_device_timeout( @pytest.mark.parametrize( - "preset", (PRESET_AWAY, PRESET_ECO, PRESET_SLEEP, PRESET_BOOST, PRESET_NONE) + "preset", [PRESET_AWAY, PRESET_ECO, PRESET_SLEEP, PRESET_BOOST, PRESET_NONE] ) async def test_update_preset_mode( hass: HomeAssistant, discovery, device, mock_now, preset @@ -596,14 +597,14 @@ async def test_update_preset_mode( @pytest.mark.parametrize( "hvac_mode", - ( + [ HVACMode.OFF, HVACMode.AUTO, HVACMode.COOL, HVACMode.DRY, HVACMode.FAN_ONLY, HVACMode.HEAT, - ), + ], ) async def test_send_hvac_mode( hass: HomeAssistant, discovery, device, mock_now, hvac_mode @@ -625,7 +626,7 @@ async def test_send_hvac_mode( @pytest.mark.parametrize( "hvac_mode", - (HVACMode.AUTO, HVACMode.COOL, HVACMode.DRY, HVACMode.FAN_ONLY, HVACMode.HEAT), + [HVACMode.AUTO, HVACMode.COOL, HVACMode.DRY, HVACMode.FAN_ONLY, HVACMode.HEAT], ) async def test_send_hvac_mode_device_timeout( hass: HomeAssistant, discovery, device, mock_now, hvac_mode @@ -649,14 +650,14 @@ async def test_send_hvac_mode_device_timeout( @pytest.mark.parametrize( "hvac_mode", - ( + [ HVACMode.OFF, HVACMode.AUTO, HVACMode.COOL, HVACMode.DRY, HVACMode.FAN_ONLY, HVACMode.HEAT, - ), + ], ) async def test_update_hvac_mode( hass: HomeAssistant, discovery, device, mock_now, hvac_mode @@ -674,7 +675,7 @@ async def test_update_hvac_mode( @pytest.mark.parametrize( "fan_mode", - (FAN_AUTO, FAN_LOW, FAN_MEDIUM_LOW, FAN_MEDIUM, FAN_MEDIUM_HIGH, FAN_HIGH), + [FAN_AUTO, FAN_LOW, FAN_MEDIUM_LOW, FAN_MEDIUM, FAN_MEDIUM_HIGH, FAN_HIGH], ) async def test_send_fan_mode( hass: HomeAssistant, discovery, device, mock_now, fan_mode @@ -715,7 +716,7 @@ async def test_send_invalid_fan_mode( @pytest.mark.parametrize( "fan_mode", - (FAN_AUTO, FAN_LOW, FAN_MEDIUM_LOW, FAN_MEDIUM, FAN_MEDIUM_HIGH, FAN_HIGH), + [FAN_AUTO, FAN_LOW, FAN_MEDIUM_LOW, FAN_MEDIUM, FAN_MEDIUM_HIGH, FAN_HIGH], ) async def test_send_fan_mode_device_timeout( hass: HomeAssistant, discovery, device, mock_now, fan_mode @@ -739,7 +740,7 @@ async def test_send_fan_mode_device_timeout( @pytest.mark.parametrize( "fan_mode", - (FAN_AUTO, FAN_LOW, FAN_MEDIUM_LOW, FAN_MEDIUM, FAN_MEDIUM_HIGH, FAN_HIGH), + [FAN_AUTO, FAN_LOW, FAN_MEDIUM_LOW, FAN_MEDIUM, FAN_MEDIUM_HIGH, FAN_HIGH], ) async def test_update_fan_mode( hass: HomeAssistant, discovery, device, mock_now, fan_mode @@ -755,7 +756,7 @@ async def test_update_fan_mode( @pytest.mark.parametrize( - "swing_mode", (SWING_OFF, SWING_BOTH, SWING_VERTICAL, SWING_HORIZONTAL) + "swing_mode", [SWING_OFF, SWING_BOTH, SWING_VERTICAL, SWING_HORIZONTAL] ) async def test_send_swing_mode( hass: HomeAssistant, discovery, device, mock_now, swing_mode @@ -795,7 +796,7 @@ async def test_send_invalid_swing_mode( @pytest.mark.parametrize( - "swing_mode", (SWING_OFF, SWING_BOTH, SWING_VERTICAL, SWING_HORIZONTAL) + "swing_mode", [SWING_OFF, SWING_BOTH, SWING_VERTICAL, SWING_HORIZONTAL] ) async def test_send_swing_mode_device_timeout( hass: HomeAssistant, discovery, device, mock_now, swing_mode @@ -818,7 +819,7 @@ async def test_send_swing_mode_device_timeout( @pytest.mark.parametrize( - "swing_mode", (SWING_OFF, SWING_BOTH, SWING_VERTICAL, SWING_HORIZONTAL) + "swing_mode", [SWING_OFF, SWING_BOTH, SWING_VERTICAL, SWING_HORIZONTAL] ) async def test_update_swing_mode( hass: HomeAssistant, discovery, device, mock_now, swing_mode diff --git a/tests/components/gree/test_config_flow.py b/tests/components/gree/test_config_flow.py index d4a922be449..7127af6b913 100644 --- a/tests/components/gree/test_config_flow.py +++ b/tests/components/gree/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for the Gree Integration.""" + from unittest.mock import AsyncMock, patch import pytest diff --git a/tests/components/gree/test_init.py b/tests/components/gree/test_init.py index 240b60e30d7..026660cf2d1 100644 --- a/tests/components/gree/test_init.py +++ b/tests/components/gree/test_init.py @@ -1,4 +1,5 @@ """Tests for the Gree Integration.""" + from unittest.mock import patch from homeassistant.components.gree.const import DOMAIN as GREE_DOMAIN @@ -14,13 +15,16 @@ async def test_setup_simple(hass: HomeAssistant) -> None: entry = MockConfigEntry(domain=GREE_DOMAIN) entry.add_to_hass(hass) - with patch( - "homeassistant.components.gree.climate.async_setup_entry", - return_value=True, - ) as climate_setup, patch( - "homeassistant.components.gree.switch.async_setup_entry", - return_value=True, - ) as switch_setup: + with ( + patch( + "homeassistant.components.gree.climate.async_setup_entry", + return_value=True, + ) as climate_setup, + patch( + "homeassistant.components.gree.switch.async_setup_entry", + return_value=True, + ) as switch_setup, + ): assert await async_setup_component(hass, GREE_DOMAIN, {}) await hass.async_block_till_done() diff --git a/tests/components/gree/test_switch.py b/tests/components/gree/test_switch.py index d8160d99040..9c465a9f297 100644 --- a/tests/components/gree/test_switch.py +++ b/tests/components/gree/test_switch.py @@ -1,4 +1,5 @@ """Tests for gree component.""" + from unittest.mock import patch from greeclimate.exceptions import DeviceTimeoutError diff --git a/tests/components/greeneye_monitor/common.py b/tests/components/greeneye_monitor/common.py index e9285647f4d..e4eeca47e60 100644 --- a/tests/components/greeneye_monitor/common.py +++ b/tests/components/greeneye_monitor/common.py @@ -1,4 +1,5 @@ """Common helpers for greeneye_monitor tests.""" + from __future__ import annotations from typing import Any @@ -235,9 +236,9 @@ def mock_monitor(serial_number: int) -> MagicMock: monitor = mock_with_listeners() monitor.serial_number = serial_number monitor.voltage_sensor = mock_voltage_sensor() - monitor.pulse_counters = [mock_pulse_counter() for i in range(0, 4)] - monitor.temperature_sensors = [mock_temperature_sensor() for i in range(0, 8)] - monitor.channels = [mock_channel() for i in range(0, 32)] + monitor.pulse_counters = [mock_pulse_counter() for i in range(4)] + monitor.temperature_sensors = [mock_temperature_sensor() for i in range(8)] + monitor.channels = [mock_channel() for i in range(32)] return monitor diff --git a/tests/components/greeneye_monitor/conftest.py b/tests/components/greeneye_monitor/conftest.py index 70b337430c5..d09d31d1db8 100644 --- a/tests/components/greeneye_monitor/conftest.py +++ b/tests/components/greeneye_monitor/conftest.py @@ -1,4 +1,5 @@ """Common fixtures for testing greeneye_monitor.""" + from typing import Any from unittest.mock import AsyncMock, MagicMock, patch diff --git a/tests/components/greeneye_monitor/test_sensor.py b/tests/components/greeneye_monitor/test_sensor.py index f739b8a64ca..35d515a4877 100644 --- a/tests/components/greeneye_monitor/test_sensor.py +++ b/tests/components/greeneye_monitor/test_sensor.py @@ -1,4 +1,5 @@ """Tests for greeneye_monitor sensors.""" + from unittest.mock import AsyncMock from homeassistant.components.greeneye_monitor.sensor import ( diff --git a/tests/components/group/common.py b/tests/components/group/common.py index b2c35703e6c..395fc990930 100644 --- a/tests/components/group/common.py +++ b/tests/components/group/common.py @@ -3,6 +3,7 @@ All containing methods are legacy helpers that should not be used by new components. Instead call the service directly. """ + from homeassistant.components.group import ( ATTR_ADD_ENTITIES, ATTR_ENTITIES, @@ -26,7 +27,7 @@ def reload(hass): @bind_hass def async_reload(hass): """Reload the automation from config.""" - hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_RELOAD)) + hass.async_create_task(hass.services.async_call(DOMAIN, SERVICE_RELOAD)) @bind_hass @@ -73,7 +74,7 @@ def async_set_group( if value is not None } - hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_SET, data)) + hass.async_create_task(hass.services.async_call(DOMAIN, SERVICE_SET, data)) @callback @@ -81,4 +82,4 @@ def async_set_group( def async_remove(hass, object_id): """Remove a user group.""" data = {ATTR_OBJECT_ID: object_id} - hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_REMOVE, data)) + hass.async_create_task(hass.services.async_call(DOMAIN, SERVICE_REMOVE, data)) diff --git a/tests/components/group/conftest.py b/tests/components/group/conftest.py index 3aefbfacdf8..5d332f8d904 100644 --- a/tests/components/group/conftest.py +++ b/tests/components/group/conftest.py @@ -1,4 +1,5 @@ """group conftest.""" + import pytest from homeassistant.core import HomeAssistant diff --git a/tests/components/group/test_binary_sensor.py b/tests/components/group/test_binary_sensor.py index 10c1d58d3d2..2e2b5f3bd21 100644 --- a/tests/components/group/test_binary_sensor.py +++ b/tests/components/group/test_binary_sensor.py @@ -1,4 +1,5 @@ """The tests for the Group Binary Sensor platform.""" + from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.group import DOMAIN from homeassistant.const import ( diff --git a/tests/components/group/test_config_flow.py b/tests/components/group/test_config_flow.py index 9db70ca80d1..623bcb5c1ee 100644 --- a/tests/components/group/test_config_flow.py +++ b/tests/components/group/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Switch config flow.""" + from typing import Any from unittest.mock import patch @@ -25,7 +26,7 @@ from tests.typing import WebSocketGenerator "extra_options", "extra_attrs", ), - ( + [ ("binary_sensor", "on", "on", {}, {}, {"all": False}, {}), ("binary_sensor", "on", "on", {}, {"all": True}, {"all": True}, {}), ("cover", "open", "open", {}, {}, {}, {}), @@ -55,7 +56,7 @@ from tests.typing import WebSocketGenerator {}, ), ("switch", "on", "on", {}, {}, {}, {}), - ), + ], ) async def test_config_flow( hass: HomeAssistant, @@ -128,11 +129,11 @@ async def test_config_flow( @pytest.mark.parametrize( - ("hide_members", "hidden_by"), ((False, None), (True, "integration")) + ("hide_members", "hidden_by"), [(False, None), (True, "integration")] ) @pytest.mark.parametrize( ("group_type", "extra_input"), - ( + [ ("binary_sensor", {"all": False}), ("cover", {}), ("event", {}), @@ -141,7 +142,7 @@ async def test_config_flow( ("lock", {}), ("media_player", {}), ("switch", {}), - ), + ], ) async def test_config_flow_hides_members( hass: HomeAssistant, @@ -209,7 +210,7 @@ def get_suggested(schema, key): @pytest.mark.parametrize( ("group_type", "member_state", "extra_options", "options_options"), - ( + [ ("binary_sensor", "on", {"all": False}, {}), ("cover", "open", {}, {}), ("event", "2021-01-01T23:59:59.123+00:00", {}, {}), @@ -224,7 +225,7 @@ def get_suggested(schema, key): {"ignore_non_numeric": False, "type": "sum"}, ), ("switch", "on", {"all": False}, {}), - ), + ], ) async def test_options( hass: HomeAssistant, group_type, member_state, extra_options, options_options @@ -315,7 +316,7 @@ async def test_options( @pytest.mark.parametrize( ("group_type", "extra_options", "extra_options_after", "advanced"), - ( + [ ("light", {"all": False}, {"all": False}, False), ("light", {"all": True}, {"all": True}, False), ("light", {"all": False}, {"all": False}, True), @@ -324,7 +325,7 @@ async def test_options( ("switch", {"all": True}, {"all": True}, False), ("switch", {"all": False}, {"all": False}, True), ("switch", {"all": True}, {"all": False}, True), - ), + ], ) async def test_all_options( hass: HomeAssistant, group_type, extra_options, extra_options_after, advanced @@ -386,14 +387,14 @@ async def test_all_options( @pytest.mark.parametrize( ("hide_members", "hidden_by_initial", "hidden_by"), - ( + [ (False, er.RegistryEntryHider.INTEGRATION, None), (True, None, er.RegistryEntryHider.INTEGRATION), - ), + ], ) @pytest.mark.parametrize( ("group_type", "extra_input"), - ( + [ ("binary_sensor", {"all": False}), ("cover", {}), ("event", {}), @@ -402,7 +403,7 @@ async def test_all_options( ("lock", {}), ("media_player", {}), ("switch", {}), - ), + ], ) async def test_options_flow_hides_members( hass: HomeAssistant, diff --git a/tests/components/group/test_cover.py b/tests/components/group/test_cover.py index d0eb3788763..5b5d8fa873c 100644 --- a/tests/components/group/test_cover.py +++ b/tests/components/group/test_cover.py @@ -1,4 +1,5 @@ """The tests for the group cover platform.""" + import asyncio from datetime import timedelta diff --git a/tests/components/group/test_fan.py b/tests/components/group/test_fan.py index ecc21ab0cd2..6aa6fc2933d 100644 --- a/tests/components/group/test_fan.py +++ b/tests/components/group/test_fan.py @@ -1,4 +1,5 @@ """The tests for the group fan platform.""" + import asyncio from unittest.mock import patch diff --git a/tests/components/group/test_init.py b/tests/components/group/test_init.py index cb5143d5a12..45846123a80 100644 --- a/tests/components/group/test_init.py +++ b/tests/components/group/test_init.py @@ -1,4 +1,5 @@ """The tests for the Group components.""" + from __future__ import annotations from collections import OrderedDict @@ -553,6 +554,7 @@ async def test_group_updated_after_device_tracker_zone_change( assert await async_setup_component(hass, "group", {}) assert await async_setup_component(hass, "device_tracker", {}) + await hass.async_block_till_done() await group.Group.async_create_group( hass, @@ -758,6 +760,7 @@ async def test_service_group_services_add_remove_entities(hass: HomeAssistant) - assert await async_setup_component(hass, "person", {}) with assert_setup_component(0, "group"): await async_setup_component(hass, "group", {"group": {}}) + await hass.async_block_till_done() assert hass.services.has_service("group", group.SERVICE_SET) @@ -1229,6 +1232,8 @@ async def test_group_vacuum_on(hass: HomeAssistant) -> None: async def test_device_tracker_not_home(hass: HomeAssistant) -> None: """Test group of device_tracker not_home.""" + await async_setup_component(hass, "device_tracker", {}) + await hass.async_block_till_done() hass.states.async_set("device_tracker.one", "not_home") hass.states.async_set("device_tracker.two", "not_home") hass.states.async_set("device_tracker.three", "not_home") @@ -1621,7 +1626,7 @@ async def test_plant_group(hass: HomeAssistant) -> None: @pytest.mark.parametrize( ("group_type", "member_state", "extra_options"), - ( + [ ("binary_sensor", "on", {"all": False}), ("cover", "open", {}), ("fan", "on", {}), @@ -1637,7 +1642,7 @@ async def test_plant_group(hass: HomeAssistant) -> None: "state_class": "measurement", }, ), - ), + ], ) async def test_setup_and_remove_config_entry( hass: HomeAssistant, @@ -1684,24 +1689,24 @@ async def test_setup_and_remove_config_entry( @pytest.mark.parametrize( ("hide_members", "hidden_by_initial", "hidden_by"), - ( + [ (False, er.RegistryEntryHider.INTEGRATION, er.RegistryEntryHider.INTEGRATION), (False, None, None), (False, er.RegistryEntryHider.USER, er.RegistryEntryHider.USER), (True, er.RegistryEntryHider.INTEGRATION, None), (True, None, None), (True, er.RegistryEntryHider.USER, er.RegistryEntryHider.USER), - ), + ], ) @pytest.mark.parametrize( ("group_type", "extra_options"), - ( + [ ("binary_sensor", {"all": False}), ("cover", {}), ("fan", {}), ("light", {"all": False}), ("media_player", {}), - ), + ], ) async def test_unhide_members_on_remove( hass: HomeAssistant, diff --git a/tests/components/group/test_light.py b/tests/components/group/test_light.py index 63f21456066..af8556b5450 100644 --- a/tests/components/group/test_light.py +++ b/tests/components/group/test_light.py @@ -1,4 +1,5 @@ """The tests for the Group Light platform.""" + import asyncio from unittest.mock import MagicMock, patch @@ -43,7 +44,12 @@ from homeassistant.core import Event, HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component -from tests.common import async_capture_events, get_fixture_path +from tests.common import ( + async_capture_events, + get_fixture_path, + setup_test_component_platform, +) +from tests.components.light.common import MockLight async def test_default_state( @@ -259,22 +265,20 @@ async def test_state_reporting_all(hass: HomeAssistant) -> None: assert hass.states.get("light.light_group").state == STATE_UNAVAILABLE -async def test_brightness( - hass: HomeAssistant, enable_custom_integrations: None -) -> None: +async def test_brightness(hass: HomeAssistant) -> None: """Test brightness reporting.""" - platform = getattr(hass.components, "test.light") - platform.init(empty=True) + entities = [ + MockLight("test1", STATE_ON), + MockLight("test2", STATE_OFF), + ] + setup_test_component_platform(hass, LIGHT_DOMAIN, entities) - platform.ENTITIES.append(platform.MockLight("test1", STATE_ON)) - platform.ENTITIES.append(platform.MockLight("test2", STATE_OFF)) - - entity0 = platform.ENTITIES[0] + entity0 = entities[0] entity0.supported_color_modes = {ColorMode.BRIGHTNESS} entity0.color_mode = ColorMode.BRIGHTNESS entity0.brightness = 255 - entity1 = platform.ENTITIES[1] + entity1 = entities[1] entity1.supported_color_modes = {ColorMode.BRIGHTNESS} entity1.color_mode = ColorMode.BRIGHTNESS @@ -332,21 +336,21 @@ async def test_brightness( assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == ["brightness"] -async def test_color_hs(hass: HomeAssistant, enable_custom_integrations: None) -> None: +async def test_color_hs(hass: HomeAssistant) -> None: """Test hs color reporting.""" - platform = getattr(hass.components, "test.light") - platform.init(empty=True) + entities = [ + MockLight("test1", STATE_ON), + MockLight("test2", STATE_OFF), + ] + setup_test_component_platform(hass, LIGHT_DOMAIN, entities) - platform.ENTITIES.append(platform.MockLight("test1", STATE_ON)) - platform.ENTITIES.append(platform.MockLight("test2", STATE_OFF)) - - entity0 = platform.ENTITIES[0] + entity0 = entities[0] entity0.supported_color_modes = {ColorMode.HS} entity0.color_mode = ColorMode.HS entity0.brightness = 255 entity0.hs_color = (0, 100) - entity1 = platform.ENTITIES[1] + entity1 = entities[1] entity1.supported_color_modes = {ColorMode.HS} entity1.color_mode = ColorMode.HS @@ -402,21 +406,21 @@ async def test_color_hs(hass: HomeAssistant, enable_custom_integrations: None) - assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 -async def test_color_rgb(hass: HomeAssistant, enable_custom_integrations: None) -> None: +async def test_color_rgb(hass: HomeAssistant) -> None: """Test rgbw color reporting.""" - platform = getattr(hass.components, "test.light") - platform.init(empty=True) + entities = [ + MockLight("test1", STATE_ON), + MockLight("test2", STATE_OFF), + ] + setup_test_component_platform(hass, LIGHT_DOMAIN, entities) - platform.ENTITIES.append(platform.MockLight("test1", STATE_ON)) - platform.ENTITIES.append(platform.MockLight("test2", STATE_OFF)) - - entity0 = platform.ENTITIES[0] + entity0 = entities[0] entity0.supported_color_modes = {ColorMode.RGB} entity0.color_mode = ColorMode.RGB entity0.brightness = 255 entity0.rgb_color = (0, 64, 128) - entity1 = platform.ENTITIES[1] + entity1 = entities[1] entity1.supported_color_modes = {ColorMode.RGB} entity1.color_mode = ColorMode.RGB entity1.brightness = 255 @@ -474,23 +478,21 @@ async def test_color_rgb(hass: HomeAssistant, enable_custom_integrations: None) assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 -async def test_color_rgbw( - hass: HomeAssistant, enable_custom_integrations: None -) -> None: +async def test_color_rgbw(hass: HomeAssistant) -> None: """Test rgbw color reporting.""" - platform = getattr(hass.components, "test.light") - platform.init(empty=True) + entities = [ + MockLight("test1", STATE_ON), + MockLight("test2", STATE_OFF), + ] + setup_test_component_platform(hass, LIGHT_DOMAIN, entities) - platform.ENTITIES.append(platform.MockLight("test1", STATE_ON)) - platform.ENTITIES.append(platform.MockLight("test2", STATE_OFF)) - - entity0 = platform.ENTITIES[0] + entity0 = entities[0] entity0.supported_color_modes = {ColorMode.RGBW} entity0.color_mode = ColorMode.RGBW entity0.brightness = 255 entity0.rgbw_color = (0, 64, 128, 255) - entity1 = platform.ENTITIES[1] + entity1 = entities[1] entity1.supported_color_modes = {ColorMode.RGBW} entity1.color_mode = ColorMode.RGBW entity1.brightness = 255 @@ -548,23 +550,21 @@ async def test_color_rgbw( assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 -async def test_color_rgbww( - hass: HomeAssistant, enable_custom_integrations: None -) -> None: +async def test_color_rgbww(hass: HomeAssistant) -> None: """Test rgbww color reporting.""" - platform = getattr(hass.components, "test.light") - platform.init(empty=True) + entities = [ + MockLight("test1", STATE_ON), + MockLight("test2", STATE_OFF), + ] + setup_test_component_platform(hass, LIGHT_DOMAIN, entities) - platform.ENTITIES.append(platform.MockLight("test1", STATE_ON)) - platform.ENTITIES.append(platform.MockLight("test2", STATE_OFF)) - - entity0 = platform.ENTITIES[0] + entity0 = entities[0] entity0.supported_color_modes = {ColorMode.RGBWW} entity0.color_mode = ColorMode.RGBWW entity0.brightness = 255 entity0.rgbww_color = (0, 32, 64, 128, 255) - entity1 = platform.ENTITIES[1] + entity1 = entities[1] entity1.supported_color_modes = {ColorMode.RGBWW} entity1.color_mode = ColorMode.RGBWW entity1.brightness = 255 @@ -622,20 +622,20 @@ async def test_color_rgbww( assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 -async def test_white(hass: HomeAssistant, enable_custom_integrations: None) -> None: +async def test_white(hass: HomeAssistant) -> None: """Test white reporting.""" - platform = getattr(hass.components, "test.light") - platform.init(empty=True) + entities = [ + MockLight("test1", STATE_ON), + MockLight("test2", STATE_ON), + ] + setup_test_component_platform(hass, LIGHT_DOMAIN, entities) - platform.ENTITIES.append(platform.MockLight("test1", STATE_ON)) - platform.ENTITIES.append(platform.MockLight("test2", STATE_ON)) - - entity0 = platform.ENTITIES[0] + entity0 = entities[0] entity0.supported_color_modes = {ColorMode.HS, ColorMode.WHITE} entity0.color_mode = ColorMode.WHITE entity0.brightness = 255 - entity1 = platform.ENTITIES[1] + entity1 = entities[1] entity1.supported_color_modes = {ColorMode.HS, ColorMode.WHITE} entity1.color_mode = ColorMode.WHITE entity1.brightness = 128 @@ -679,23 +679,21 @@ async def test_white(hass: HomeAssistant, enable_custom_integrations: None) -> N assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == ["hs", "white"] -async def test_color_temp( - hass: HomeAssistant, enable_custom_integrations: None -) -> None: +async def test_color_temp(hass: HomeAssistant) -> None: """Test color temp reporting.""" - platform = getattr(hass.components, "test.light") - platform.init(empty=True) + entities = [ + MockLight("test1", STATE_ON), + MockLight("test2", STATE_OFF), + ] + setup_test_component_platform(hass, LIGHT_DOMAIN, entities) - platform.ENTITIES.append(platform.MockLight("test1", STATE_ON)) - platform.ENTITIES.append(platform.MockLight("test2", STATE_OFF)) - - entity0 = platform.ENTITIES[0] + entity0 = entities[0] entity0.supported_color_modes = {ColorMode.COLOR_TEMP} entity0.color_mode = ColorMode.COLOR_TEMP entity0.brightness = 255 entity0.color_temp_kelvin = 2 - entity1 = platform.ENTITIES[1] + entity1 = entities[1] entity1.supported_color_modes = {ColorMode.COLOR_TEMP} entity1.color_mode = ColorMode.COLOR_TEMP @@ -750,26 +748,24 @@ async def test_color_temp( assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == ["color_temp"] -async def test_emulated_color_temp_group( - hass: HomeAssistant, enable_custom_integrations: None -) -> None: +async def test_emulated_color_temp_group(hass: HomeAssistant) -> None: """Test emulated color temperature in a group.""" - platform = getattr(hass.components, "test.light") - platform.init(empty=True) + entities = [ + MockLight("test1", STATE_ON), + MockLight("test2", STATE_OFF), + MockLight("test3", STATE_OFF), + ] + setup_test_component_platform(hass, LIGHT_DOMAIN, entities) - platform.ENTITIES.append(platform.MockLight("test1", STATE_ON)) - platform.ENTITIES.append(platform.MockLight("test2", STATE_OFF)) - platform.ENTITIES.append(platform.MockLight("test3", STATE_OFF)) - - entity0 = platform.ENTITIES[0] + entity0 = entities[0] entity0.supported_color_modes = {ColorMode.COLOR_TEMP} entity0.color_mode = ColorMode.COLOR_TEMP - entity1 = platform.ENTITIES[1] + entity1 = entities[1] entity1.supported_color_modes = {ColorMode.COLOR_TEMP, ColorMode.HS} entity1.color_mode = ColorMode.COLOR_TEMP - entity2 = platform.ENTITIES[2] + entity2 = entities[2] entity2.supported_color_modes = {ColorMode.HS} entity2.color_mode = ColorMode.HS @@ -816,27 +812,25 @@ async def test_emulated_color_temp_group( assert state.attributes[ATTR_HS_COLOR] == (27.001, 19.243) -async def test_min_max_mireds( - hass: HomeAssistant, enable_custom_integrations: None -) -> None: +async def test_min_max_mireds(hass: HomeAssistant) -> None: """Test min/max mireds reporting. min/max mireds is reported both when light is on and off """ - platform = getattr(hass.components, "test.light") - platform.init(empty=True) + entities = [ + MockLight("test1", STATE_ON), + MockLight("test2", STATE_OFF), + ] + setup_test_component_platform(hass, LIGHT_DOMAIN, entities) - platform.ENTITIES.append(platform.MockLight("test1", STATE_ON)) - platform.ENTITIES.append(platform.MockLight("test2", STATE_OFF)) - - entity0 = platform.ENTITIES[0] + entity0 = entities[0] entity0.supported_color_modes = {ColorMode.COLOR_TEMP} entity0.color_mode = ColorMode.COLOR_TEMP entity0.color_temp_kelvin = 2 entity0._attr_min_color_temp_kelvin = 2 entity0._attr_max_color_temp_kelvin = 5 - entity1 = platform.ENTITIES[1] + entity1 = entities[1] entity1.supported_color_modes = {ColorMode.COLOR_TEMP} entity1.color_mode = ColorMode.COLOR_TEMP entity1._attr_min_color_temp_kelvin = 1 @@ -997,26 +991,24 @@ async def test_effect(hass: HomeAssistant) -> None: assert state.attributes[ATTR_EFFECT] == "Random" -async def test_supported_color_modes( - hass: HomeAssistant, enable_custom_integrations: None -) -> None: +async def test_supported_color_modes(hass: HomeAssistant) -> None: """Test supported_color_modes reporting.""" - platform = getattr(hass.components, "test.light") - platform.init(empty=True) + entities = [ + MockLight("test1", STATE_ON), + MockLight("test2", STATE_OFF), + MockLight("test3", STATE_OFF), + ] + setup_test_component_platform(hass, LIGHT_DOMAIN, entities) - platform.ENTITIES.append(platform.MockLight("test1", STATE_ON)) - platform.ENTITIES.append(platform.MockLight("test2", STATE_OFF)) - platform.ENTITIES.append(platform.MockLight("test3", STATE_OFF)) - - entity0 = platform.ENTITIES[0] + entity0 = entities[0] entity0.supported_color_modes = {ColorMode.COLOR_TEMP, ColorMode.HS} entity0.color_mode = ColorMode.UNKNOWN - entity1 = platform.ENTITIES[1] + entity1 = entities[1] entity1.supported_color_modes = {ColorMode.RGBW, ColorMode.RGBWW} entity1.color_mode = ColorMode.UNKNOWN - entity2 = platform.ENTITIES[2] + entity2 = entities[2] entity2.supported_color_modes = {ColorMode.BRIGHTNESS} entity2.color_mode = ColorMode.UNKNOWN @@ -1047,26 +1039,24 @@ async def test_supported_color_modes( } -async def test_color_mode( - hass: HomeAssistant, enable_custom_integrations: None -) -> None: +async def test_color_mode(hass: HomeAssistant) -> None: """Test color_mode reporting.""" - platform = getattr(hass.components, "test.light") - platform.init(empty=True) + entities = [ + MockLight("test1", STATE_ON), + MockLight("test2", STATE_OFF), + MockLight("test3", STATE_OFF), + ] + setup_test_component_platform(hass, LIGHT_DOMAIN, entities) - platform.ENTITIES.append(platform.MockLight("test1", STATE_ON)) - platform.ENTITIES.append(platform.MockLight("test2", STATE_OFF)) - platform.ENTITIES.append(platform.MockLight("test3", STATE_OFF)) - - entity0 = platform.ENTITIES[0] + entity0 = entities[0] entity0.supported_color_modes = {ColorMode.COLOR_TEMP, ColorMode.HS} entity0.color_mode = ColorMode.COLOR_TEMP - entity1 = platform.ENTITIES[1] + entity1 = entities[1] entity1.supported_color_modes = {ColorMode.COLOR_TEMP, ColorMode.HS} entity1.color_mode = ColorMode.COLOR_TEMP - entity2 = platform.ENTITIES[2] + entity2 = entities[2] entity2.supported_color_modes = {ColorMode.COLOR_TEMP, ColorMode.HS} entity2.color_mode = ColorMode.HS @@ -1122,41 +1112,39 @@ async def test_color_mode( assert state.attributes[ATTR_COLOR_MODE] == ColorMode.HS -async def test_color_mode2( - hass: HomeAssistant, enable_custom_integrations: None -) -> None: +async def test_color_mode2(hass: HomeAssistant) -> None: """Test onoff color_mode and brightness are given lowest priority.""" - platform = getattr(hass.components, "test.light") - platform.init(empty=True) + entities = [ + MockLight("test1", STATE_ON), + MockLight("test2", STATE_ON), + MockLight("test3", STATE_ON), + MockLight("test4", STATE_ON), + MockLight("test5", STATE_ON), + MockLight("test6", STATE_ON), + ] + setup_test_component_platform(hass, LIGHT_DOMAIN, entities) - platform.ENTITIES.append(platform.MockLight("test1", STATE_ON)) - platform.ENTITIES.append(platform.MockLight("test2", STATE_ON)) - platform.ENTITIES.append(platform.MockLight("test3", STATE_ON)) - platform.ENTITIES.append(platform.MockLight("test4", STATE_ON)) - platform.ENTITIES.append(platform.MockLight("test5", STATE_ON)) - platform.ENTITIES.append(platform.MockLight("test6", STATE_ON)) - - entity = platform.ENTITIES[0] + entity = entities[0] entity.supported_color_modes = {ColorMode.COLOR_TEMP} entity.color_mode = ColorMode.COLOR_TEMP - entity = platform.ENTITIES[1] + entity = entities[1] entity.supported_color_modes = {ColorMode.BRIGHTNESS} entity.color_mode = ColorMode.BRIGHTNESS - entity = platform.ENTITIES[2] + entity = entities[2] entity.supported_color_modes = {ColorMode.BRIGHTNESS} entity.color_mode = ColorMode.BRIGHTNESS - entity = platform.ENTITIES[3] + entity = entities[3] entity.supported_color_modes = {ColorMode.ONOFF} entity.color_mode = ColorMode.ONOFF - entity = platform.ENTITIES[4] + entity = entities[4] entity.supported_color_modes = {ColorMode.ONOFF} entity.color_mode = ColorMode.ONOFF - entity = platform.ENTITIES[5] + entity = entities[5] entity.supported_color_modes = {ColorMode.ONOFF} entity.color_mode = ColorMode.ONOFF @@ -1247,29 +1235,30 @@ async def test_supported_features(hass: HomeAssistant) -> None: @pytest.mark.parametrize("supported_color_modes", [ColorMode.HS, ColorMode.RGB]) async def test_service_calls( - hass: HomeAssistant, enable_custom_integrations: None, supported_color_modes + hass: HomeAssistant, + supported_color_modes, ) -> None: """Test service calls.""" - platform = getattr(hass.components, "test.light") - platform.init(empty=True) + entities = [ + MockLight("bed_light", STATE_ON), + MockLight("ceiling_lights", STATE_OFF), + MockLight("kitchen_lights", STATE_OFF), + ] + setup_test_component_platform(hass, LIGHT_DOMAIN, entities) - platform.ENTITIES.append(platform.MockLight("bed_light", STATE_ON)) - platform.ENTITIES.append(platform.MockLight("ceiling_lights", STATE_OFF)) - platform.ENTITIES.append(platform.MockLight("kitchen_lights", STATE_OFF)) - - entity0 = platform.ENTITIES[0] + entity0 = entities[0] entity0.supported_color_modes = {supported_color_modes} entity0.color_mode = supported_color_modes entity0.brightness = 255 entity0.rgb_color = (0, 64, 128) - entity1 = platform.ENTITIES[1] + entity1 = entities[1] entity1.supported_color_modes = {supported_color_modes} entity1.color_mode = supported_color_modes entity1.brightness = 255 entity1.rgb_color = (255, 128, 64) - entity2 = platform.ENTITIES[2] + entity2 = entities[2] entity2.supported_color_modes = {supported_color_modes} entity2.color_mode = supported_color_modes entity2.brightness = 255 diff --git a/tests/components/group/test_media_player.py b/tests/components/group/test_media_player.py index 9f36693d9ef..451aae200b3 100644 --- a/tests/components/group/test_media_player.py +++ b/tests/components/group/test_media_player.py @@ -1,4 +1,5 @@ """The tests for the Media group platform.""" + import asyncio from unittest.mock import Mock, patch diff --git a/tests/components/group/test_notify.py b/tests/components/group/test_notify.py index 77569c80f0f..52d049431d8 100644 --- a/tests/components/group/test_notify.py +++ b/tests/components/group/test_notify.py @@ -1,4 +1,5 @@ """The tests for the notify.group platform.""" + from unittest.mock import MagicMock, patch from homeassistant import config as hass_config diff --git a/tests/components/group/test_recorder.py b/tests/components/group/test_recorder.py index 3ca965ec998..2b0cab6c6f7 100644 --- a/tests/components/group/test_recorder.py +++ b/tests/components/group/test_recorder.py @@ -1,4 +1,5 @@ """The tests for group recorder.""" + from __future__ import annotations from datetime import timedelta diff --git a/tests/components/group/test_reproduce_state.py b/tests/components/group/test_reproduce_state.py index ea9c6e9d43d..ff834bc87d2 100644 --- a/tests/components/group/test_reproduce_state.py +++ b/tests/components/group/test_reproduce_state.py @@ -1,4 +1,5 @@ """The tests for reproduction of state.""" + from asyncio import Future from unittest.mock import ANY, patch diff --git a/tests/components/group/test_switch.py b/tests/components/group/test_switch.py index 86f6eb43ed9..32b21fcb0d7 100644 --- a/tests/components/group/test_switch.py +++ b/tests/components/group/test_switch.py @@ -1,4 +1,5 @@ """The tests for the Group Switch platform.""" + import asyncio from unittest.mock import patch diff --git a/tests/components/growatt_server/test_config_flow.py b/tests/components/growatt_server/test_config_flow.py index 8455495165a..57777a57783 100644 --- a/tests/components/growatt_server/test_config_flow.py +++ b/tests/components/growatt_server/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for the Growatt server config flow.""" + from copy import deepcopy from unittest.mock import patch @@ -83,9 +84,10 @@ async def test_no_plants_on_account(hass: HomeAssistant) -> None: plant_list = deepcopy(GROWATT_PLANT_LIST_RESPONSE) plant_list["data"] = [] - with patch( - "growattServer.GrowattApi.login", return_value=GROWATT_LOGIN_RESPONSE - ), patch("growattServer.GrowattApi.plant_list", return_value=plant_list): + with ( + patch("growattServer.GrowattApi.login", return_value=GROWATT_LOGIN_RESPONSE), + patch("growattServer.GrowattApi.plant_list", return_value=plant_list), + ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input ) @@ -103,10 +105,13 @@ async def test_multiple_plant_ids(hass: HomeAssistant) -> None: plant_list = deepcopy(GROWATT_PLANT_LIST_RESPONSE) plant_list["data"].append(plant_list["data"][0]) - with patch( - "growattServer.GrowattApi.login", return_value=GROWATT_LOGIN_RESPONSE - ), patch("growattServer.GrowattApi.plant_list", return_value=plant_list), patch( - "homeassistant.components.growatt_server.async_setup_entry", return_value=True + with ( + patch("growattServer.GrowattApi.login", return_value=GROWATT_LOGIN_RESPONSE), + patch("growattServer.GrowattApi.plant_list", return_value=plant_list), + patch( + "homeassistant.components.growatt_server.async_setup_entry", + return_value=True, + ), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input @@ -133,13 +138,16 @@ async def test_one_plant_on_account(hass: HomeAssistant) -> None: ) user_input = FIXTURE_USER_INPUT.copy() - with patch( - "growattServer.GrowattApi.login", return_value=GROWATT_LOGIN_RESPONSE - ), patch( - "growattServer.GrowattApi.plant_list", - return_value=GROWATT_PLANT_LIST_RESPONSE, - ), patch( - "homeassistant.components.growatt_server.async_setup_entry", return_value=True + with ( + patch("growattServer.GrowattApi.login", return_value=GROWATT_LOGIN_RESPONSE), + patch( + "growattServer.GrowattApi.plant_list", + return_value=GROWATT_PLANT_LIST_RESPONSE, + ), + patch( + "homeassistant.components.growatt_server.async_setup_entry", + return_value=True, + ), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input @@ -160,11 +168,12 @@ async def test_existing_plant_configured(hass: HomeAssistant) -> None: ) user_input = FIXTURE_USER_INPUT.copy() - with patch( - "growattServer.GrowattApi.login", return_value=GROWATT_LOGIN_RESPONSE - ), patch( - "growattServer.GrowattApi.plant_list", - return_value=GROWATT_PLANT_LIST_RESPONSE, + with ( + patch("growattServer.GrowattApi.login", return_value=GROWATT_LOGIN_RESPONSE), + patch( + "growattServer.GrowattApi.plant_list", + return_value=GROWATT_PLANT_LIST_RESPONSE, + ), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input diff --git a/tests/components/guardian/conftest.py b/tests/components/guardian/conftest.py index f2cde0a553d..df517aba603 100644 --- a/tests/components/guardian/conftest.py +++ b/tests/components/guardian/conftest.py @@ -1,4 +1,5 @@ """Define fixtures for Elexa Guardian tests.""" + from collections.abc import Generator import json from unittest.mock import AsyncMock, patch @@ -106,35 +107,47 @@ async def setup_guardian_fixture( data_wifi_status, ): """Define a fixture to set up Guardian.""" - with patch("aioguardian.client.Client.connect"), patch( - "aioguardian.commands.sensor.SensorCommands.pair_dump", - return_value=data_sensor_pair_dump, - ), patch( - "aioguardian.commands.sensor.SensorCommands.pair_sensor", - return_value=data_sensor_pair_sensor, - ), patch( - "aioguardian.commands.sensor.SensorCommands.paired_sensor_status", - return_value=data_sensor_paired_sensor_status, - ), patch( - "aioguardian.commands.system.SystemCommands.diagnostics", - return_value=data_system_diagnostics, - ), patch( - "aioguardian.commands.system.SystemCommands.onboard_sensor_status", - return_value=data_system_onboard_sensor_status, - ), patch( - "aioguardian.commands.system.SystemCommands.ping", - return_value=data_system_ping, - ), patch( - "aioguardian.commands.valve.ValveCommands.status", - return_value=data_valve_status, - ), patch( - "aioguardian.commands.wifi.WiFiCommands.status", - return_value=data_wifi_status, - ), patch( - "aioguardian.client.Client.disconnect", - ), patch( - "homeassistant.components.guardian.PLATFORMS", - [], + with ( + patch("aioguardian.client.Client.connect"), + patch( + "aioguardian.commands.sensor.SensorCommands.pair_dump", + return_value=data_sensor_pair_dump, + ), + patch( + "aioguardian.commands.sensor.SensorCommands.pair_sensor", + return_value=data_sensor_pair_sensor, + ), + patch( + "aioguardian.commands.sensor.SensorCommands.paired_sensor_status", + return_value=data_sensor_paired_sensor_status, + ), + patch( + "aioguardian.commands.system.SystemCommands.diagnostics", + return_value=data_system_diagnostics, + ), + patch( + "aioguardian.commands.system.SystemCommands.onboard_sensor_status", + return_value=data_system_onboard_sensor_status, + ), + patch( + "aioguardian.commands.system.SystemCommands.ping", + return_value=data_system_ping, + ), + patch( + "aioguardian.commands.valve.ValveCommands.status", + return_value=data_valve_status, + ), + patch( + "aioguardian.commands.wifi.WiFiCommands.status", + return_value=data_wifi_status, + ), + patch( + "aioguardian.client.Client.disconnect", + ), + patch( + "homeassistant.components.guardian.PLATFORMS", + [], + ), ): assert await async_setup_component(hass, DOMAIN, config) await hass.async_block_till_done() diff --git a/tests/components/guardian/test_config_flow.py b/tests/components/guardian/test_config_flow.py index e52d14fb6a0..3922b196e4b 100644 --- a/tests/components/guardian/test_config_flow.py +++ b/tests/components/guardian/test_config_flow.py @@ -1,4 +1,5 @@ """Define tests for the Elexa Guardian config flow.""" + from ipaddress import ip_address from unittest.mock import patch diff --git a/tests/components/guardian/test_diagnostics.py b/tests/components/guardian/test_diagnostics.py index ec288461661..02b620b8e01 100644 --- a/tests/components/guardian/test_diagnostics.py +++ b/tests/components/guardian/test_diagnostics.py @@ -1,4 +1,5 @@ """Test Guardian diagnostics.""" + from homeassistant.components.diagnostics import REDACTED from homeassistant.components.guardian import DOMAIN, GuardianData from homeassistant.core import HomeAssistant diff --git a/tests/components/habitica/test_config_flow.py b/tests/components/habitica/test_config_flow.py index 83202078dfe..fe5ddcacdea 100644 --- a/tests/components/habitica/test_config_flow.py +++ b/tests/components/habitica/test_config_flow.py @@ -1,4 +1,5 @@ """Test the habitica config flow.""" + from unittest.mock import AsyncMock, MagicMock, patch from aiohttp import ClientResponseError @@ -22,15 +23,19 @@ async def test_form(hass: HomeAssistant) -> None: mock_obj = MagicMock() mock_obj.user.get = AsyncMock() - with patch( - "homeassistant.components.habitica.config_flow.HabitipyAsync", - return_value=mock_obj, - ), patch( - "homeassistant.components.habitica.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.habitica.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.habitica.config_flow.HabitipyAsync", + return_value=mock_obj, + ), + patch( + "homeassistant.components.habitica.async_setup", return_value=True + ) as mock_setup, + patch( + "homeassistant.components.habitica.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"api_user": "test-api-user", "api_key": "test-api-key"}, diff --git a/tests/components/habitica/test_init.py b/tests/components/habitica/test_init.py index 91fa6f90e9f..9168e29f2d5 100644 --- a/tests/components/habitica/test_init.py +++ b/tests/components/habitica/test_init.py @@ -1,4 +1,5 @@ """Test the habitica module.""" + from http import HTTPStatus import pytest diff --git a/tests/components/hardkernel/test_config_flow.py b/tests/components/hardkernel/test_config_flow.py index 309c796fcc3..1965e2af8c7 100644 --- a/tests/components/hardkernel/test_config_flow.py +++ b/tests/components/hardkernel/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Hardkernel config flow.""" + from unittest.mock import patch from homeassistant.components.hardkernel.const import DOMAIN diff --git a/tests/components/hardkernel/test_hardware.py b/tests/components/hardkernel/test_hardware.py index ee2299f383c..8b57fc24d00 100644 --- a/tests/components/hardkernel/test_hardware.py +++ b/tests/components/hardkernel/test_hardware.py @@ -1,4 +1,5 @@ """Test the Hardkernel hardware platform.""" + from unittest.mock import patch import pytest diff --git a/tests/components/hardkernel/test_init.py b/tests/components/hardkernel/test_init.py index 98f4c08cc80..90717054ead 100644 --- a/tests/components/hardkernel/test_init.py +++ b/tests/components/hardkernel/test_init.py @@ -1,4 +1,5 @@ """Test the Hardkernel integration.""" + from unittest.mock import patch from homeassistant.components.hardkernel.const import DOMAIN diff --git a/tests/components/hardware/test_websocket_api.py b/tests/components/hardware/test_websocket_api.py index 98fa00486ff..e8099069a9c 100644 --- a/tests/components/hardware/test_websocket_api.py +++ b/tests/components/hardware/test_websocket_api.py @@ -1,4 +1,5 @@ """Test the hardware websocket API.""" + from collections import namedtuple import datetime from unittest.mock import patch @@ -63,14 +64,17 @@ async def test_system_status_subscription( VirtualMem = namedtuple("VirtualMemory", ["available", "percent", "total"]) vmem = VirtualMem(10 * 1024**2, 50, 30 * 1024**2) - with patch.object( - mock_psutil.psutil, - "cpu_percent", - return_value=123, - ), patch.object( - mock_psutil.psutil, - "virtual_memory", - return_value=vmem, + with ( + patch.object( + mock_psutil.psutil, + "cpu_percent", + return_value=123, + ), + patch.object( + mock_psutil.psutil, + "virtual_memory", + return_value=vmem, + ), ): freezer.tick(TEST_TIME_ADVANCE_INTERVAL) await hass.async_block_till_done() @@ -90,9 +94,10 @@ async def test_system_status_subscription( response = await client.receive_json() assert response["success"] - with patch.object(mock_psutil.psutil, "cpu_percent") as cpu_mock, patch.object( - mock_psutil.psutil, "virtual_memory" - ) as vmem_mock: + with ( + patch.object(mock_psutil.psutil, "cpu_percent") as cpu_mock, + patch.object(mock_psutil.psutil, "virtual_memory") as vmem_mock, + ): freezer.tick(TEST_TIME_ADVANCE_INTERVAL) await hass.async_block_till_done() cpu_mock.assert_not_called() diff --git a/tests/components/harmony/conftest.py b/tests/components/harmony/conftest.py index 9b335d18183..97449749667 100644 --- a/tests/components/harmony/conftest.py +++ b/tests/components/harmony/conftest.py @@ -1,12 +1,21 @@ """Fixtures for harmony tests.""" + from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch from aioharmony.const import ClientCallbackType import pytest -from homeassistant.components.harmony.const import ACTIVITY_POWER_OFF +from homeassistant.components.harmony.const import ACTIVITY_POWER_OFF, DOMAIN +from homeassistant.const import CONF_HOST, CONF_NAME -from .const import NILE_TV_ACTIVITY_ID, PLAY_MUSIC_ACTIVITY_ID, WATCH_TV_ACTIVITY_ID +from .const import ( + HUB_NAME, + NILE_TV_ACTIVITY_ID, + PLAY_MUSIC_ACTIVITY_ID, + WATCH_TV_ACTIVITY_ID, +) + +from tests.common import MockConfigEntry ACTIVITIES_TO_IDS = { ACTIVITY_POWER_OFF: -1, @@ -165,3 +174,13 @@ def mock_write_config(): "homeassistant.components.harmony.remote.HarmonyRemote.write_config_file", ) as mock: yield mock + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Return mock config entry.""" + return MockConfigEntry( + domain=DOMAIN, + unique_id="123", + data={CONF_HOST: "192.0.2.0", CONF_NAME: HUB_NAME}, + ) diff --git a/tests/components/harmony/test_config_flow.py b/tests/components/harmony/test_config_flow.py index b1c9d5649bb..c2daa98728b 100644 --- a/tests/components/harmony/test_config_flow.py +++ b/tests/components/harmony/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Logitech Harmony Hub config flow.""" + from unittest.mock import AsyncMock, MagicMock, patch import aiohttp @@ -31,13 +32,16 @@ async def test_user_form(hass: HomeAssistant) -> None: assert result["errors"] == {} harmonyapi = _get_mock_harmonyapi(connect=True) - with patch( - "homeassistant.components.harmony.util.HarmonyAPI", - return_value=harmonyapi, - ), patch( - "homeassistant.components.harmony.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.harmony.util.HarmonyAPI", + return_value=harmonyapi, + ), + patch( + "homeassistant.components.harmony.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "1.2.3.4", "name": "friend"}, @@ -83,13 +87,16 @@ async def test_form_ssdp(hass: HomeAssistant) -> None: harmonyapi = _get_mock_harmonyapi(connect=True) - with patch( - "homeassistant.components.harmony.util.HarmonyAPI", - return_value=harmonyapi, - ), patch( - "homeassistant.components.harmony.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.harmony.util.HarmonyAPI", + return_value=harmonyapi, + ), + patch( + "homeassistant.components.harmony.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {}, diff --git a/tests/components/harmony/test_init.py b/tests/components/harmony/test_init.py index f718cee109e..971983fc3b6 100644 --- a/tests/components/harmony/test_init.py +++ b/tests/components/harmony/test_init.py @@ -1,4 +1,5 @@ """Test init of Logitch Harmony Hub integration.""" + from homeassistant.components.harmony.const import DOMAIN from homeassistant.const import CONF_HOST, CONF_NAME from homeassistant.core import HomeAssistant diff --git a/tests/components/harmony/test_remote.py b/tests/components/harmony/test_remote.py index b3f0d695c75..c0ec2235b84 100644 --- a/tests/components/harmony/test_remote.py +++ b/tests/components/harmony/test_remote.py @@ -1,4 +1,5 @@ """Test the Logitech Harmony Hub remote.""" + from datetime import timedelta from aioharmony.const import SendCommandDevice @@ -42,15 +43,16 @@ STOP_COMMAND = "Stop" async def test_connection_state_changes( - harmony_client, mock_hc, hass: HomeAssistant, mock_write_config + harmony_client, + mock_hc, + hass: HomeAssistant, + mock_write_config, + mock_config_entry: MockConfigEntry, ) -> None: """Ensure connection changes are reflected in the remote state.""" - entry = MockConfigEntry( - domain=DOMAIN, data={CONF_HOST: "192.0.2.0", CONF_NAME: HUB_NAME} - ) - entry.add_to_hass(hass) - await hass.config_entries.async_setup(entry.entry_id) + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() # mocks start with current activity == Watch TV @@ -81,14 +83,13 @@ async def test_connection_state_changes( assert hass.states.is_state(ENTITY_REMOTE, STATE_ON) -async def test_remote_toggles(mock_hc, hass: HomeAssistant, mock_write_config) -> None: +async def test_remote_toggles( + mock_hc, hass: HomeAssistant, mock_write_config, mock_config_entry: MockConfigEntry +) -> None: """Ensure calls to the remote also updates the switches.""" - entry = MockConfigEntry( - domain=DOMAIN, data={CONF_HOST: "192.0.2.0", CONF_NAME: HUB_NAME} - ) - entry.add_to_hass(hass) - await hass.config_entries.async_setup(entry.entry_id) + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() # mocks start remote with Watch TV default activity @@ -150,15 +151,16 @@ async def test_remote_toggles(mock_hc, hass: HomeAssistant, mock_write_config) - async def test_async_send_command( - mock_hc, harmony_client, hass: HomeAssistant, mock_write_config + mock_hc, + harmony_client, + hass: HomeAssistant, + mock_write_config, + mock_config_entry: MockConfigEntry, ) -> None: """Ensure calls to send remote commands properly propagate to devices.""" - entry = MockConfigEntry( - domain=DOMAIN, data={CONF_HOST: "192.0.2.0", CONF_NAME: HUB_NAME} - ) - entry.add_to_hass(hass) - await hass.config_entries.async_setup(entry.entry_id) + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() send_commands_mock = harmony_client.send_commands @@ -284,11 +286,16 @@ async def test_async_send_command( async def test_async_send_command_custom_delay( - mock_hc, harmony_client, hass: HomeAssistant, mock_write_config + mock_hc, + harmony_client, + hass: HomeAssistant, + mock_write_config, + mock_config_entry: MockConfigEntry, ) -> None: """Ensure calls to send remote commands properly propagate to devices with custom delays.""" entry = MockConfigEntry( domain=DOMAIN, + unique_id="123", data={ CONF_HOST: "192.0.2.0", CONF_NAME: HUB_NAME, @@ -326,15 +333,16 @@ async def test_async_send_command_custom_delay( async def test_change_channel( - mock_hc, harmony_client, hass: HomeAssistant, mock_write_config + mock_hc, + harmony_client, + hass: HomeAssistant, + mock_write_config, + mock_config_entry: MockConfigEntry, ) -> None: """Test change channel commands.""" - entry = MockConfigEntry( - domain=DOMAIN, data={CONF_HOST: "192.0.2.0", CONF_NAME: HUB_NAME} - ) - entry.add_to_hass(hass) - await hass.config_entries.async_setup(entry.entry_id) + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() change_channel_mock = harmony_client.change_channel @@ -352,15 +360,16 @@ async def test_change_channel( async def test_sync( - mock_hc, harmony_client, mock_write_config, hass: HomeAssistant + mock_hc, + harmony_client, + mock_write_config, + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, ) -> None: """Test the sync command.""" - entry = MockConfigEntry( - domain=DOMAIN, data={CONF_HOST: "192.0.2.0", CONF_NAME: HUB_NAME} - ) - entry.add_to_hass(hass) - await hass.config_entries.async_setup(entry.entry_id) + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() sync_mock = harmony_client.sync diff --git a/tests/components/harmony/test_select.py b/tests/components/harmony/test_select.py index 0b775a84d08..2568feb1412 100644 --- a/tests/components/harmony/test_select.py +++ b/tests/components/harmony/test_select.py @@ -1,38 +1,32 @@ """Test the Logitech Harmony Hub activity select.""" + from datetime import timedelta -from homeassistant.components.harmony.const import DOMAIN from homeassistant.components.select import ( ATTR_OPTION, DOMAIN as SELECT_DOMAIN, SERVICE_SELECT_OPTION, ) -from homeassistant.const import ( - ATTR_ENTITY_ID, - CONF_HOST, - CONF_NAME, - STATE_OFF, - STATE_ON, - STATE_UNAVAILABLE, -) +from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.util import utcnow -from .const import ENTITY_REMOTE, ENTITY_SELECT, HUB_NAME +from .const import ENTITY_REMOTE, ENTITY_SELECT from tests.common import MockConfigEntry, async_fire_time_changed async def test_connection_state_changes( - harmony_client, mock_hc, hass: HomeAssistant, mock_write_config + harmony_client, + mock_hc, + hass: HomeAssistant, + mock_write_config, + mock_config_entry: MockConfigEntry, ) -> None: """Ensure connection changes are reflected in the switch states.""" - entry = MockConfigEntry( - domain=DOMAIN, data={CONF_HOST: "192.0.2.0", CONF_NAME: HUB_NAME} - ) - entry.add_to_hass(hass) - await hass.config_entries.async_setup(entry.entry_id) + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() # mocks start with current activity == Watch TV @@ -55,14 +49,13 @@ async def test_connection_state_changes( assert hass.states.is_state(ENTITY_SELECT, "Watch TV") -async def test_options(mock_hc, hass: HomeAssistant, mock_write_config) -> None: +async def test_options( + mock_hc, hass: HomeAssistant, mock_write_config, mock_config_entry: MockConfigEntry +) -> None: """Ensure calls to the switch modify the harmony state.""" - entry = MockConfigEntry( - domain=DOMAIN, data={CONF_HOST: "192.0.2.0", CONF_NAME: HUB_NAME} - ) - entry.add_to_hass(hass) - await hass.config_entries.async_setup(entry.entry_id) + 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 we have all options @@ -75,14 +68,12 @@ async def test_options(mock_hc, hass: HomeAssistant, mock_write_config) -> None: ] -async def test_select_option(mock_hc, hass: HomeAssistant, mock_write_config) -> None: +async def test_select_option( + mock_hc, hass: HomeAssistant, mock_write_config, mock_config_entry: MockConfigEntry +) -> None: """Ensure calls to the switch modify the harmony state.""" - entry = MockConfigEntry( - domain=DOMAIN, data={CONF_HOST: "192.0.2.0", CONF_NAME: HUB_NAME} - ) - - entry.add_to_hass(hass) - await hass.config_entries.async_setup(entry.entry_id) + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() # mocks start with current activity == Watch TV diff --git a/tests/components/harmony/test_subscriber.py b/tests/components/harmony/test_subscriber.py index bdc57385852..f1d1866a044 100644 --- a/tests/components/harmony/test_subscriber.py +++ b/tests/components/harmony/test_subscriber.py @@ -1,4 +1,5 @@ """Test the HarmonySubscriberMixin class.""" + import asyncio from unittest.mock import AsyncMock, MagicMock @@ -6,7 +7,7 @@ from homeassistant.components.harmony.subscriber import ( HarmonyCallback, HarmonySubscriberMixin, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import HassJob, HomeAssistant _NO_PARAM_CALLBACKS = { "connected": "_connected", @@ -47,18 +48,18 @@ async def test_async_callbacks(hass: HomeAssistant) -> None: """Ensure we handle async callbacks.""" subscriber = HarmonySubscriberMixin(hass) - callbacks = {k: AsyncMock() for k in _ALL_CALLBACK_NAMES} + callbacks = {k: HassJob(AsyncMock()) for k in _ALL_CALLBACK_NAMES} subscriber.async_subscribe(HarmonyCallback(**callbacks)) _call_all_callbacks(subscriber) await hass.async_block_till_done() for callback_name in _NO_PARAM_CALLBACKS: callback_mock = callbacks[callback_name] - callback_mock.assert_awaited_once() + callback_mock.target.assert_awaited_once() for callback_name in _ACTIVITY_CALLBACKS: callback_mock = callbacks[callback_name] - callback_mock.assert_awaited_once_with(_ACTIVITY_TUPLE) + callback_mock.target.assert_awaited_once_with(_ACTIVITY_TUPLE) async def test_long_async_callbacks(hass: HomeAssistant) -> None: @@ -76,8 +77,8 @@ async def test_long_async_callbacks(hass: HomeAssistant) -> None: async def notifies_when_called(): notifier_event_two.set() - callbacks_one = {k: blocks_until_notified for k in _ALL_CALLBACK_NAMES} - callbacks_two = {k: notifies_when_called for k in _ALL_CALLBACK_NAMES} + callbacks_one = {k: HassJob(blocks_until_notified) for k in _ALL_CALLBACK_NAMES} + callbacks_two = {k: HassJob(notifies_when_called) for k in _ALL_CALLBACK_NAMES} subscriber.async_subscribe(HarmonyCallback(**callbacks_one)) subscriber.async_subscribe(HarmonyCallback(**callbacks_two)) @@ -91,29 +92,29 @@ async def test_callbacks(hass: HomeAssistant) -> None: """Ensure we handle non-async callbacks.""" subscriber = HarmonySubscriberMixin(hass) - callbacks = {k: MagicMock() for k in _ALL_CALLBACK_NAMES} + callbacks = {k: HassJob(MagicMock()) for k in _ALL_CALLBACK_NAMES} subscriber.async_subscribe(HarmonyCallback(**callbacks)) _call_all_callbacks(subscriber) await hass.async_block_till_done() for callback_name in _NO_PARAM_CALLBACKS: callback_mock = callbacks[callback_name] - callback_mock.assert_called_once() + callback_mock.target.assert_called_once() for callback_name in _ACTIVITY_CALLBACKS: callback_mock = callbacks[callback_name] - callback_mock.assert_called_once_with(_ACTIVITY_TUPLE) + callback_mock.target.assert_called_once_with(_ACTIVITY_TUPLE) async def test_subscribe_unsubscribe(hass: HomeAssistant) -> None: """Ensure we handle subscriptions and unsubscriptions correctly.""" subscriber = HarmonySubscriberMixin(hass) - callback_one = {k: MagicMock() for k in _ALL_CALLBACK_NAMES} + callback_one = {k: HassJob(MagicMock()) for k in _ALL_CALLBACK_NAMES} unsub_one = subscriber.async_subscribe(HarmonyCallback(**callback_one)) - callback_two = {k: MagicMock() for k in _ALL_CALLBACK_NAMES} + callback_two = {k: HassJob(MagicMock()) for k in _ALL_CALLBACK_NAMES} _ = subscriber.async_subscribe(HarmonyCallback(**callback_two)) - callback_three = {k: MagicMock() for k in _ALL_CALLBACK_NAMES} + callback_three = {k: HassJob(MagicMock()) for k in _ALL_CALLBACK_NAMES} unsub_three = subscriber.async_subscribe(HarmonyCallback(**callback_three)) unsub_one() @@ -123,14 +124,14 @@ async def test_subscribe_unsubscribe(hass: HomeAssistant) -> None: await hass.async_block_till_done() for callback_name in _NO_PARAM_CALLBACKS: - callback_one[callback_name].assert_not_called() - callback_two[callback_name].assert_called_once() - callback_three[callback_name].assert_not_called() + callback_one[callback_name].target.assert_not_called() + callback_two[callback_name].target.assert_called_once() + callback_three[callback_name].target.assert_not_called() for callback_name in _ACTIVITY_CALLBACKS: - callback_one[callback_name].assert_not_called() - callback_two[callback_name].assert_called_once_with(_ACTIVITY_TUPLE) - callback_three[callback_name].assert_not_called() + callback_one[callback_name].target.assert_not_called() + callback_two[callback_name].target.assert_called_once_with(_ACTIVITY_TUPLE) + callback_three[callback_name].target.assert_not_called() def _call_all_callbacks(subscriber): diff --git a/tests/components/harmony/test_switch.py b/tests/components/harmony/test_switch.py index f843ab4deca..01f9287ae57 100644 --- a/tests/components/harmony/test_switch.py +++ b/tests/components/harmony/test_switch.py @@ -1,4 +1,5 @@ """Test the Logitech Harmony Hub activity switches.""" + from datetime import timedelta from homeassistant.components import automation, script @@ -89,21 +90,22 @@ async def test_connection_state_changes( async def test_switch_toggles( - mock_hc, hass: HomeAssistant, mock_write_config, entity_registry: er.EntityRegistry + mock_hc, + hass: HomeAssistant, + mock_write_config, + entity_registry: er.EntityRegistry, + mock_config_entry: MockConfigEntry, ) -> None: """Ensure calls to the switch modify the harmony state.""" - entry = MockConfigEntry( - domain=DOMAIN, data={CONF_HOST: "192.0.2.0", CONF_NAME: HUB_NAME} - ) - entry.add_to_hass(hass) - await hass.config_entries.async_setup(entry.entry_id) + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() # enable switch entities entity_registry.async_update_entity(ENTITY_WATCH_TV, disabled_by=None) entity_registry.async_update_entity(ENTITY_PLAY_MUSIC, disabled_by=None) - await hass.config_entries.async_reload(entry.entry_id) + await hass.config_entries.async_reload(mock_config_entry.entry_id) await hass.async_block_till_done() # mocks start with current activity == Watch TV diff --git a/tests/components/hassio/__init__.py b/tests/components/hassio/__init__.py index 76aecd64098..e5669f777d2 100644 --- a/tests/components/hassio/__init__.py +++ b/tests/components/hassio/__init__.py @@ -1,2 +1,3 @@ """Tests for Hass.io component.""" + SUPERVISOR_TOKEN = "123456" diff --git a/tests/components/hassio/conftest.py b/tests/components/hassio/conftest.py index e54fdcafd1d..21eeedb89ad 100644 --- a/tests/components/hassio/conftest.py +++ b/tests/components/hassio/conftest.py @@ -1,4 +1,5 @@ """Fixtures for Hass.io.""" + import os import re from unittest.mock import Mock, patch @@ -28,12 +29,17 @@ def disable_security_filter(): @pytest.fixture def hassio_env(): """Fixture to inject hassio env.""" - with patch.dict(os.environ, {"SUPERVISOR": "127.0.0.1"}), patch( - "homeassistant.components.hassio.HassIO.is_connected", - return_value={"result": "ok", "data": {}}, - ), patch.dict(os.environ, {"SUPERVISOR_TOKEN": SUPERVISOR_TOKEN}), patch( - "homeassistant.components.hassio.HassIO.get_info", - Mock(side_effect=HassioAPIError()), + with ( + patch.dict(os.environ, {"SUPERVISOR": "127.0.0.1"}), + patch( + "homeassistant.components.hassio.HassIO.is_connected", + return_value={"result": "ok", "data": {}}, + ), + patch.dict(os.environ, {"SUPERVISOR_TOKEN": SUPERVISOR_TOKEN}), + patch( + "homeassistant.components.hassio.HassIO.get_info", + Mock(side_effect=HassioAPIError()), + ), ): yield @@ -41,22 +47,29 @@ def hassio_env(): @pytest.fixture def hassio_stubs(hassio_env, hass, hass_client, aioclient_mock): """Create mock hassio http client.""" - with patch( - "homeassistant.components.hassio.HassIO.update_hass_api", - return_value={"result": "ok"}, - ) as hass_api, patch( - "homeassistant.components.hassio.HassIO.update_hass_timezone", - return_value={"result": "ok"}, - ), patch( - "homeassistant.components.hassio.HassIO.get_info", - side_effect=HassioAPIError(), - ), patch( - "homeassistant.components.hassio.HassIO.get_ingress_panels", - return_value={"panels": []}, - ), patch( - "homeassistant.components.hassio.issues.SupervisorIssues.setup", - ), patch( - "homeassistant.components.hassio.HassIO.refresh_updates", + with ( + patch( + "homeassistant.components.hassio.HassIO.update_hass_api", + return_value={"result": "ok"}, + ) as hass_api, + patch( + "homeassistant.components.hassio.HassIO.update_hass_timezone", + return_value={"result": "ok"}, + ), + patch( + "homeassistant.components.hassio.HassIO.get_info", + side_effect=HassioAPIError(), + ), + patch( + "homeassistant.components.hassio.HassIO.get_ingress_panels", + return_value={"panels": []}, + ), + patch( + "homeassistant.components.hassio.issues.SupervisorIssues.setup", + ), + patch( + "homeassistant.components.hassio.HassIO.refresh_updates", + ), ): hass.set_state(CoreState.starting) hass.loop.run_until_complete(async_setup_component(hass, "hassio", {})) diff --git a/tests/components/hassio/test_addon_manager.py b/tests/components/hassio/test_addon_manager.py index 57a6949c56d..f846de007ef 100644 --- a/tests/components/hassio/test_addon_manager.py +++ b/tests/components/hassio/test_addon_manager.py @@ -1,4 +1,5 @@ """Test the addon manager.""" + from __future__ import annotations import asyncio diff --git a/tests/components/hassio/test_addon_panel.py b/tests/components/hassio/test_addon_panel.py index 4abc4b16c9f..9b1735287c6 100644 --- a/tests/components/hassio/test_addon_panel.py +++ b/tests/components/hassio/test_addon_panel.py @@ -1,4 +1,5 @@ """Test add-on panel.""" + from http import HTTPStatus from unittest.mock import patch diff --git a/tests/components/hassio/test_auth.py b/tests/components/hassio/test_auth.py index b58d43f87ed..175d9061d56 100644 --- a/tests/components/hassio/test_auth.py +++ b/tests/components/hassio/test_auth.py @@ -1,4 +1,5 @@ """The tests for the hassio component.""" + from http import HTTPStatus from unittest.mock import Mock, patch diff --git a/tests/components/hassio/test_binary_sensor.py b/tests/components/hassio/test_binary_sensor.py index 854a6782b1a..d502d6ea730 100644 --- a/tests/components/hassio/test_binary_sensor.py +++ b/tests/components/hassio/test_binary_sensor.py @@ -1,4 +1,5 @@ """The tests for the hassio binary sensors.""" + import os from unittest.mock import patch diff --git a/tests/components/hassio/test_config_flow.py b/tests/components/hassio/test_config_flow.py index 80b403a333b..1c56f4e25f5 100644 --- a/tests/components/hassio/test_config_flow.py +++ b/tests/components/hassio/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Home Assistant Supervisor config flow.""" + from unittest.mock import patch from homeassistant.components.hassio import DOMAIN @@ -8,12 +9,15 @@ from homeassistant.core import HomeAssistant async def test_config_flow(hass: HomeAssistant) -> None: """Test we get the form.""" - with patch( - "homeassistant.components.hassio.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.hassio.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.hassio.async_setup", return_value=True + ) as mock_setup, + patch( + "homeassistant.components.hassio.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": "system"} ) diff --git a/tests/components/hassio/test_discovery.py b/tests/components/hassio/test_discovery.py index 0923967a480..0783ee77932 100644 --- a/tests/components/hassio/test_discovery.py +++ b/tests/components/hassio/test_discovery.py @@ -1,4 +1,5 @@ """Test config flow.""" + from http import HTTPStatus from unittest.mock import AsyncMock, Mock, patch @@ -124,12 +125,15 @@ async def test_hassio_discovery_startup_done( json={"result": "ok", "data": {"name": "Mosquitto Test"}}, ) - with patch( - "homeassistant.components.hassio.HassIO.update_hass_api", - return_value={"result": "ok"}, - ), patch( - "homeassistant.components.hassio.HassIO.get_info", - Mock(side_effect=HassioAPIError()), + with ( + patch( + "homeassistant.components.hassio.HassIO.update_hass_api", + return_value={"result": "ok"}, + ), + patch( + "homeassistant.components.hassio.HassIO.get_info", + Mock(side_effect=HassioAPIError()), + ), ): await hass.async_start() await async_setup_component(hass, "hassio", {}) diff --git a/tests/components/hassio/test_handler.py b/tests/components/hassio/test_handler.py index 06c726360d9..337a0dd864f 100644 --- a/tests/components/hassio/test_handler.py +++ b/tests/components/hassio/test_handler.py @@ -1,4 +1,5 @@ """The tests for the hassio component.""" + from __future__ import annotations from typing import Any, Literal @@ -314,9 +315,9 @@ async def test_api_ingress_panels( @pytest.mark.parametrize( ("api_call", "method", "payload"), [ - ["retrieve_discovery_messages", "GET", None], - ["refresh_updates", "POST", None], - ["update_diagnostics", "POST", True], + ("retrieve_discovery_messages", "GET", None), + ("refresh_updates", "POST", None), + ("update_diagnostics", "POST", True), ], ) async def test_api_headers( diff --git a/tests/components/hassio/test_http.py b/tests/components/hassio/test_http.py index 4e1e7436a58..55d4d8b0365 100644 --- a/tests/components/hassio/test_http.py +++ b/tests/components/hassio/test_http.py @@ -1,4 +1,5 @@ """The tests for the hassio component.""" + from http import HTTPStatus from unittest.mock import patch diff --git a/tests/components/hassio/test_ingress.py b/tests/components/hassio/test_ingress.py index 791337079f0..27e99f7f596 100644 --- a/tests/components/hassio/test_ingress.py +++ b/tests/components/hassio/test_ingress.py @@ -1,4 +1,5 @@ """The tests for the hassio component.""" + from http import HTTPStatus from unittest.mock import MagicMock, patch @@ -309,7 +310,7 @@ async def test_ingress_missing_peername( aioclient_mock: AiohttpClientMocker, caplog: pytest.LogCaptureFixture, ) -> None: - """Test hadnling of missing peername.""" + """Test handling of missing peername.""" aioclient_mock.get( "http://127.0.0.1/ingress/lorem/ipsum", text="test", diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index 7d0943b3677..da49b8d9f16 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -1,4 +1,5 @@ """The tests for the hassio component.""" + from datetime import timedelta import os from typing import Any @@ -15,7 +16,9 @@ from homeassistant.components.hassio import ( DOMAIN, STORAGE_KEY, async_get_addon_store_info, + get_core_info, hostname_from_addon_slug, + is_hassio, ) from homeassistant.components.hassio.const import REQUEST_REFRESH_DELAY from homeassistant.components.hassio.handler import HassioAPIError @@ -246,8 +249,8 @@ async def test_setup_api_ping( assert result assert aioclient_mock.call_count == 19 - assert hass.components.hassio.get_core_info()["version_latest"] == "1.0.0" - assert hass.components.hassio.is_hassio() + assert get_core_info(hass)["version_latest"] == "1.0.0" + assert is_hassio(hass) async def test_setup_api_panel( @@ -436,8 +439,9 @@ async def test_setup_hassio_no_additional_data( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test setup with API push default data.""" - with patch.dict(os.environ, MOCK_ENVIRON), patch.dict( - os.environ, {"SUPERVISOR_TOKEN": "123456"} + with ( + patch.dict(os.environ, MOCK_ENVIRON), + patch.dict(os.environ, {"SUPERVISOR_TOKEN": "123456"}), ): result = await async_setup_component(hass, "hassio", {"hassio": {}}) await hass.async_block_till_done() @@ -458,14 +462,17 @@ async def test_warn_when_cannot_connect( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Fail warn when we cannot connect.""" - with patch.dict(os.environ, MOCK_ENVIRON), patch( - "homeassistant.components.hassio.HassIO.is_connected", - return_value=None, + with ( + patch.dict(os.environ, MOCK_ENVIRON), + patch( + "homeassistant.components.hassio.HassIO.is_connected", + return_value=None, + ), ): result = await async_setup_component(hass, "hassio", {}) assert result - assert hass.components.hassio.is_hassio() + assert is_hassio(hass) assert "Not connected with the supervisor / system too busy!" in caplog.text @@ -493,9 +500,12 @@ async def test_service_calls( caplog: pytest.LogCaptureFixture, ) -> None: """Call service and check the API calls behind that.""" - with patch.dict(os.environ, MOCK_ENVIRON), patch( - "homeassistant.components.hassio.HassIO.is_connected", - return_value=None, + with ( + patch.dict(os.environ, MOCK_ENVIRON), + patch( + "homeassistant.components.hassio.HassIO.is_connected", + return_value=None, + ), ): assert await async_setup_component(hass, "hassio", {}) await hass.async_block_till_done() @@ -638,9 +648,12 @@ async def test_invalid_service_calls( aioclient_mock: AiohttpClientMocker, ) -> None: """Call service with invalid input and check that it raises.""" - with patch.dict(os.environ, MOCK_ENVIRON), patch( - "homeassistant.components.hassio.HassIO.is_connected", - return_value=None, + with ( + patch.dict(os.environ, MOCK_ENVIRON), + patch( + "homeassistant.components.hassio.HassIO.is_connected", + return_value=None, + ), ): assert await async_setup_component(hass, "hassio", {}) await hass.async_block_till_done() @@ -677,12 +690,16 @@ async def test_addon_service_call_with_complex_slug( }, ], } - with patch.dict(os.environ, MOCK_ENVIRON), patch( - "homeassistant.components.hassio.HassIO.is_connected", - return_value=None, - ), patch( - "homeassistant.components.hassio.HassIO.get_supervisor_info", - return_value=supervisor_mock_data, + with ( + patch.dict(os.environ, MOCK_ENVIRON), + patch( + "homeassistant.components.hassio.HassIO.is_connected", + return_value=None, + ), + patch( + "homeassistant.components.hassio.HassIO.get_supervisor_info", + return_value=supervisor_mock_data, + ), ): assert await async_setup_component(hass, "hassio", {}) await hass.async_block_till_done() @@ -787,12 +804,16 @@ async def test_device_registry_calls(hass: HomeAssistant) -> None: "version_latest": "5.12", } - with patch.dict(os.environ, MOCK_ENVIRON), patch( - "homeassistant.components.hassio.HassIO.get_supervisor_info", - return_value=supervisor_mock_data, - ), patch( - "homeassistant.components.hassio.HassIO.get_os_info", - return_value=os_mock_data, + with ( + patch.dict(os.environ, MOCK_ENVIRON), + patch( + "homeassistant.components.hassio.HassIO.get_supervisor_info", + return_value=supervisor_mock_data, + ), + patch( + "homeassistant.components.hassio.HassIO.get_os_info", + return_value=os_mock_data, + ), ): config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN) config_entry.add_to_hass(hass) @@ -820,19 +841,22 @@ async def test_device_registry_calls(hass: HomeAssistant) -> None: } # Test that when addon is removed, next update will remove the add-on and subsequent updates won't - with patch( - "homeassistant.components.hassio.HassIO.get_supervisor_info", - return_value=supervisor_mock_data, - ), patch( - "homeassistant.components.hassio.HassIO.get_os_info", - return_value=os_mock_data, + with ( + patch( + "homeassistant.components.hassio.HassIO.get_supervisor_info", + return_value=supervisor_mock_data, + ), + patch( + "homeassistant.components.hassio.HassIO.get_os_info", + return_value=os_mock_data, + ), ): async_fire_time_changed(hass, dt_util.now() + timedelta(hours=1)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert len(dev_reg.devices) == 5 async_fire_time_changed(hass, dt_util.now() + timedelta(hours=2)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert len(dev_reg.devices) == 5 supervisor_mock_data = { @@ -867,19 +891,23 @@ async def test_device_registry_calls(hass: HomeAssistant) -> None: # Test that when addon is added, next update will reload the entry so we register # a new device - with patch( - "homeassistant.components.hassio.HassIO.get_supervisor_info", - return_value=supervisor_mock_data, - ), patch( - "homeassistant.components.hassio.HassIO.get_os_info", - return_value=os_mock_data, - ), patch( - "homeassistant.components.hassio.HassIO.get_info", - return_value={ - "supervisor": "222", - "homeassistant": "0.110.0", - "hassos": None, - }, + with ( + patch( + "homeassistant.components.hassio.HassIO.get_supervisor_info", + return_value=supervisor_mock_data, + ), + patch( + "homeassistant.components.hassio.HassIO.get_os_info", + return_value=os_mock_data, + ), + patch( + "homeassistant.components.hassio.HassIO.get_info", + return_value={ + "supervisor": "222", + "homeassistant": "0.110.0", + "hassos": None, + }, + ), ): async_fire_time_changed(hass, dt_util.now() + timedelta(hours=3)) await hass.async_block_till_done() @@ -891,9 +919,12 @@ async def test_coordinator_updates( ) -> None: """Test coordinator updates.""" await async_setup_component(hass, "homeassistant", {}) - with patch.dict(os.environ, MOCK_ENVIRON), patch( - "homeassistant.components.hassio.HassIO.refresh_updates" - ) as refresh_updates_mock: + with ( + patch.dict(os.environ, MOCK_ENVIRON), + patch( + "homeassistant.components.hassio.HassIO.refresh_updates" + ) as refresh_updates_mock, + ): config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN) config_entry.add_to_hass(hass) assert await hass.config_entries.async_setup(config_entry.entry_id) @@ -965,9 +996,12 @@ async def test_coordinator_updates_stats_entities_enabled( ) -> None: """Test coordinator updates with stats entities enabled.""" await async_setup_component(hass, "homeassistant", {}) - with patch.dict(os.environ, MOCK_ENVIRON), patch( - "homeassistant.components.hassio.HassIO.refresh_updates" - ) as refresh_updates_mock: + with ( + patch.dict(os.environ, MOCK_ENVIRON), + patch( + "homeassistant.components.hassio.HassIO.refresh_updates" + ) as refresh_updates_mock, + ): config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN) config_entry.add_to_hass(hass) assert await hass.config_entries.async_setup(config_entry.entry_id) @@ -1056,10 +1090,13 @@ async def test_setup_hardware_integration( ) -> None: """Test setup initiates hardware integration.""" - with patch.dict(os.environ, MOCK_ENVIRON), patch( - f"homeassistant.components.{integration}.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch.dict(os.environ, MOCK_ENVIRON), + patch( + f"homeassistant.components.{integration}.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result = await async_setup_component(hass, "hassio", {"hassio": {}}) await hass.async_block_till_done() diff --git a/tests/components/hassio/test_issues.py b/tests/components/hassio/test_issues.py index b5a852710fe..2da9d30549d 100644 --- a/tests/components/hassio/test_issues.py +++ b/tests/components/hassio/test_issues.py @@ -1,12 +1,14 @@ """Test issues from supervisor issues.""" + from __future__ import annotations -import asyncio +from datetime import timedelta from http import HTTPStatus import os from typing import Any from unittest.mock import ANY, patch +from freezegun.api import FrozenDateTimeFactory import pytest from homeassistant.components.repairs import DOMAIN as REPAIRS_DOMAIN @@ -535,6 +537,7 @@ async def test_supervisor_issues_initial_failure( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, hass_ws_client: WebSocketGenerator, + freezer: FrozenDateTimeFactory, ) -> None: """Test issues manager retries after initial update failure.""" responses = [ @@ -589,6 +592,7 @@ async def test_supervisor_issues_initial_failure( with patch("homeassistant.components.hassio.issues.REQUEST_REFRESH_DELAY", new=0.1): result = await async_setup_component(hass, "hassio", {}) + await hass.async_block_till_done() assert result client = await hass_ws_client(hass) @@ -598,7 +602,8 @@ async def test_supervisor_issues_initial_failure( assert msg["success"] assert len(msg["result"]["issues"]) == 0 - await asyncio.sleep(0.1) + freezer.tick(timedelta(milliseconds=200)) + await hass.async_block_till_done() await client.send_json({"id": 2, "type": "repairs/list_issues"}) msg = await client.receive_json() assert msg["success"] diff --git a/tests/components/hassio/test_repairs.py b/tests/components/hassio/test_repairs.py index 97a13fe1e5d..d387968da46 100644 --- a/tests/components/hassio/test_repairs.py +++ b/tests/components/hassio/test_repairs.py @@ -94,13 +94,11 @@ async def test_supervisor_issue_repair_flow( flow_id = data["flow_id"] assert data == { - "version": 1, "type": "create_entry", "flow_id": flow_id, "handler": "hassio", "description": None, "description_placeholders": None, - "minor_version": 1, } assert not issue_registry.async_get_issue(domain="hassio", issue_id="1234") @@ -190,13 +188,11 @@ async def test_supervisor_issue_repair_flow_with_multiple_suggestions( flow_id = data["flow_id"] assert data == { - "version": 1, "type": "create_entry", "flow_id": flow_id, "handler": "hassio", "description": None, "description_placeholders": None, - "minor_version": 1, } assert not issue_registry.async_get_issue(domain="hassio", issue_id="1234") @@ -305,13 +301,11 @@ async def test_supervisor_issue_repair_flow_with_multiple_suggestions_and_confir flow_id = data["flow_id"] assert data == { - "version": 1, "type": "create_entry", "flow_id": flow_id, "handler": "hassio", "description": None, "description_placeholders": None, - "minor_version": 1, } assert not issue_registry.async_get_issue(domain="hassio", issue_id="1234") @@ -386,13 +380,11 @@ async def test_supervisor_issue_repair_flow_skip_confirmation( flow_id = data["flow_id"] assert data == { - "version": 1, "type": "create_entry", "flow_id": flow_id, "handler": "hassio", "description": None, "description_placeholders": None, - "minor_version": 1, } assert not issue_registry.async_get_issue(domain="hassio", issue_id="1234") @@ -558,13 +550,11 @@ async def test_mount_failed_repair_flow( flow_id = data["flow_id"] assert data == { - "version": 1, "type": "create_entry", "flow_id": flow_id, "handler": "hassio", "description": None, "description_placeholders": None, - "minor_version": 1, } assert not issue_registry.async_get_issue(domain="hassio", issue_id="1234") @@ -670,13 +660,11 @@ async def test_supervisor_issue_docker_config_repair_flow( flow_id = data["flow_id"] assert data == { - "version": 1, "type": "create_entry", "flow_id": flow_id, "handler": "hassio", "description": None, "description_placeholders": None, - "minor_version": 1, } assert not issue_registry.async_get_issue(domain="hassio", issue_id="1234") diff --git a/tests/components/hassio/test_sensor.py b/tests/components/hassio/test_sensor.py index fbc6f08a1f5..82ac3eccdf5 100644 --- a/tests/components/hassio/test_sensor.py +++ b/tests/components/hassio/test_sensor.py @@ -1,16 +1,20 @@ """The tests for the hassio sensors.""" + from datetime import timedelta import os from unittest.mock import patch +from freezegun.api import FrozenDateTimeFactory import pytest +from homeassistant import config_entries from homeassistant.components.hassio import ( DOMAIN, HASSIO_UPDATE_INTERVAL, HassioAPIError, ) from homeassistant.components.hassio.const import REQUEST_REFRESH_DELAY +from homeassistant.const import STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component @@ -264,6 +268,7 @@ async def test_sensor( ("sensor.test_memory_percent", "4.59"), ], ) +@patch.dict(os.environ, MOCK_ENVIRON) async def test_stats_addon_sensor( hass: HomeAssistant, entity_id, @@ -271,18 +276,17 @@ async def test_stats_addon_sensor( aioclient_mock: AiohttpClientMocker, entity_registry: er.EntityRegistry, caplog: pytest.LogCaptureFixture, + freezer: FrozenDateTimeFactory, ) -> None: """Test stats addons sensor.""" config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN) config_entry.add_to_hass(hass) - with patch.dict(os.environ, MOCK_ENVIRON): - result = await async_setup_component( - hass, - "hassio", - {"http": {"server_port": 9999, "server_host": "127.0.0.1"}, "hassio": {}}, - ) - assert result + assert await async_setup_component( + hass, + "hassio", + {"http": {"server_port": 9999, "server_host": "127.0.0.1"}, "hassio": {}}, + ) await hass.async_block_till_done() # Verify that the entity is disabled by default. @@ -292,10 +296,9 @@ async def test_stats_addon_sensor( _install_default_mocks(aioclient_mock) _install_test_addon_stats_failure_mock(aioclient_mock) - async_fire_time_changed( - hass, dt_util.utcnow() + HASSIO_UPDATE_INTERVAL + timedelta(seconds=1) - ) - await hass.async_block_till_done() + freezer.tick(HASSIO_UPDATE_INTERVAL + timedelta(seconds=1)) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) assert "Could not fetch stats" not in caplog.text @@ -303,22 +306,31 @@ async def test_stats_addon_sensor( _install_default_mocks(aioclient_mock) _install_test_addon_stats_mock(aioclient_mock) - async_fire_time_changed( - hass, dt_util.utcnow() + HASSIO_UPDATE_INTERVAL + timedelta(seconds=1) - ) - await hass.async_block_till_done() + freezer.tick(HASSIO_UPDATE_INTERVAL + timedelta(seconds=1)) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) - # Enable the entity. + assert "Could not fetch stats" not in caplog.text + + # Enable the entity and wait for the reload to complete. entity_registry.async_update_entity(entity_id, disabled_by=None) - await hass.config_entries.async_reload(config_entry.entry_id) - await hass.async_block_till_done() + freezer.tick(config_entries.RELOAD_AFTER_UPDATE_DELAY) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + assert config_entry.state is config_entries.ConfigEntryState.LOADED + # Verify the entity is still enabled + assert entity_registry.async_get(entity_id).disabled_by is None - # There is a REQUEST_REFRESH_DELAYs cooldown on the debouncer - async_fire_time_changed( - hass, dt_util.now() + timedelta(seconds=REQUEST_REFRESH_DELAY) - ) - await hass.async_block_till_done() + # The config entry just reloaded, so we need to wait for the next update + freezer.tick(HASSIO_UPDATE_INTERVAL + timedelta(seconds=1)) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + assert hass.states.get(entity_id) is not None + + freezer.tick(HASSIO_UPDATE_INTERVAL + timedelta(seconds=1)) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) # Verify that the entity have the expected state. state = hass.states.get(entity_id) assert state.state == expected @@ -327,9 +339,10 @@ async def test_stats_addon_sensor( _install_default_mocks(aioclient_mock) _install_test_addon_stats_failure_mock(aioclient_mock) - async_fire_time_changed( - hass, dt_util.utcnow() + HASSIO_UPDATE_INTERVAL + timedelta(seconds=1) - ) - await hass.async_block_till_done() + freezer.tick(HASSIO_UPDATE_INTERVAL + timedelta(seconds=1)) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + state = hass.states.get(entity_id) + assert state.state == STATE_UNAVAILABLE assert "Could not fetch stats" in caplog.text diff --git a/tests/components/hassio/test_system_health.py b/tests/components/hassio/test_system_health.py index 7a01c9444ab..873365aa3a0 100644 --- a/tests/components/hassio/test_system_health.py +++ b/tests/components/hassio/test_system_health.py @@ -1,4 +1,5 @@ """Test hassio system health.""" + import asyncio import os from unittest.mock import patch @@ -28,8 +29,8 @@ async def test_hassio_system_health( ) hass.config.components.add("hassio") - with patch.dict(os.environ, MOCK_ENVIRON): - assert await async_setup_component(hass, "system_health", {}) + assert await async_setup_component(hass, "system_health", {}) + await hass.async_block_till_done() hass.data["hassio_info"] = { "channel": "stable", @@ -50,7 +51,8 @@ async def test_hassio_system_health( "addons": [{"name": "Awesome Addon", "version": "1.0.0"}], } - info = await get_system_health_info(hass, "hassio") + with patch.dict(os.environ, MOCK_ENVIRON): + info = await get_system_health_info(hass, "hassio") for key, val in info.items(): if asyncio.iscoroutine(val): @@ -87,8 +89,8 @@ async def test_hassio_system_health_with_issues( ) hass.config.components.add("hassio") - with patch.dict(os.environ, MOCK_ENVIRON): - assert await async_setup_component(hass, "system_health", {}) + assert await async_setup_component(hass, "system_health", {}) + await hass.async_block_till_done() hass.data["hassio_info"] = {"channel": "stable"} hass.data["hassio_host_info"] = {} @@ -98,7 +100,8 @@ async def test_hassio_system_health_with_issues( "supported": False, } - info = await get_system_health_info(hass, "hassio") + with patch.dict(os.environ, MOCK_ENVIRON): + info = await get_system_health_info(hass, "hassio") for key, val in info.items(): if asyncio.iscoroutine(val): diff --git a/tests/components/hassio/test_update.py b/tests/components/hassio/test_update.py index 42918b02266..f6b61aeedab 100644 --- a/tests/components/hassio/test_update.py +++ b/tests/components/hassio/test_update.py @@ -1,4 +1,5 @@ """The tests for the hassio update entities.""" + from datetime import timedelta import os from unittest.mock import patch @@ -469,9 +470,12 @@ async def test_release_notes_between_versions( config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN) config_entry.add_to_hass(hass) - with patch.dict(os.environ, MOCK_ENVIRON), patch( - "homeassistant.components.hassio.get_addons_changelogs", - return_value={"test": "# 2.0.1\nNew updates\n# 2.0.0\nOld updates"}, + with ( + patch.dict(os.environ, MOCK_ENVIRON), + patch( + "homeassistant.components.hassio.data.get_addons_changelogs", + return_value={"test": "# 2.0.1\nNew updates\n# 2.0.0\nOld updates"}, + ), ): result = await async_setup_component( hass, @@ -505,9 +509,12 @@ async def test_release_notes_full( config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN) config_entry.add_to_hass(hass) - with patch.dict(os.environ, MOCK_ENVIRON), patch( - "homeassistant.components.hassio.get_addons_changelogs", - return_value={"test": "# 2.0.0\nNew updates\n# 2.0.0\nOld updates"}, + with ( + patch.dict(os.environ, MOCK_ENVIRON), + patch( + "homeassistant.components.hassio.data.get_addons_changelogs", + return_value={"test": "# 2.0.0\nNew updates\n# 2.0.0\nOld updates"}, + ), ): result = await async_setup_component( hass, @@ -541,9 +548,12 @@ async def test_not_release_notes( config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN) config_entry.add_to_hass(hass) - with patch.dict(os.environ, MOCK_ENVIRON), patch( - "homeassistant.components.hassio.get_addons_changelogs", - return_value={"test": None}, + with ( + patch.dict(os.environ, MOCK_ENVIRON), + patch( + "homeassistant.components.hassio.data.get_addons_changelogs", + return_value={"test": None}, + ), ): result = await async_setup_component( hass, @@ -569,13 +579,16 @@ async def test_not_release_notes( async def test_no_os_entity(hass: HomeAssistant) -> None: """Test handling where there is no os entity.""" - with patch.dict(os.environ, MOCK_ENVIRON), patch( - "homeassistant.components.hassio.HassIO.get_info", - return_value={ - "supervisor": "222", - "homeassistant": "0.110.0", - "hassos": None, - }, + with ( + patch.dict(os.environ, MOCK_ENVIRON), + patch( + "homeassistant.components.hassio.HassIO.get_info", + return_value={ + "supervisor": "222", + "homeassistant": "0.110.0", + "hassos": None, + }, + ), ): result = await async_setup_component( hass, @@ -593,15 +606,20 @@ async def test_setting_up_core_update_when_addon_fails( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test setting up core update when single addon fails.""" - with patch.dict(os.environ, MOCK_ENVIRON), patch( - "homeassistant.components.hassio.HassIO.get_addon_stats", - side_effect=HassioAPIError("add-on is not running"), - ), patch( - "homeassistant.components.hassio.HassIO.get_addon_changelog", - side_effect=HassioAPIError("add-on is not running"), - ), patch( - "homeassistant.components.hassio.HassIO.get_addon_info", - side_effect=HassioAPIError("add-on is not running"), + with ( + patch.dict(os.environ, MOCK_ENVIRON), + patch( + "homeassistant.components.hassio.HassIO.get_addon_stats", + side_effect=HassioAPIError("add-on is not running"), + ), + patch( + "homeassistant.components.hassio.HassIO.get_addon_changelog", + side_effect=HassioAPIError("add-on is not running"), + ), + patch( + "homeassistant.components.hassio.HassIO.get_addon_info", + side_effect=HassioAPIError("add-on is not running"), + ), ): result = await async_setup_component( hass, diff --git a/tests/components/hassio/test_websocket_api.py b/tests/components/hassio/test_websocket_api.py index ee17ad62e74..67252a0bc83 100644 --- a/tests/components/hassio/test_websocket_api.py +++ b/tests/components/hassio/test_websocket_api.py @@ -1,4 +1,5 @@ """Test websocket API.""" + import pytest from homeassistant.components.hassio.const import ( diff --git a/tests/components/hddtemp/test_sensor.py b/tests/components/hddtemp/test_sensor.py index 70a9910595c..eac6d4c4053 100644 --- a/tests/components/hddtemp/test_sensor.py +++ b/tests/components/hddtemp/test_sensor.py @@ -1,4 +1,5 @@ """The tests for the hddtemp platform.""" + import socket from unittest.mock import patch @@ -66,9 +67,9 @@ class TelnetMock: self.timeout = timeout self.sample_data = bytes( "|/dev/sda1|WDC WD30EZRX-12DC0B0|29|C|" - + "|/dev/sdb1|WDC WD15EADS-11P7B2|32|C|" - + "|/dev/sdc1|WDC WD20EARX-22MMMB0|29|C|" - + "|/dev/sdd1|WDC WD15EARS-00Z5B1|89|F|", + "|/dev/sdb1|WDC WD15EADS-11P7B2|32|C|" + "|/dev/sdc1|WDC WD20EARX-22MMMB0|29|C|" + "|/dev/sdd1|WDC WD15EARS-00Z5B1|89|F|", "ascii", ) diff --git a/tests/components/hdmi_cec/__init__.py b/tests/components/hdmi_cec/__init__.py index c131bf96b41..31e09489d4a 100644 --- a/tests/components/hdmi_cec/__init__.py +++ b/tests/components/hdmi_cec/__init__.py @@ -1,4 +1,5 @@ """Tests for the HDMI-CEC component.""" + from unittest.mock import AsyncMock, Mock from homeassistant.components.hdmi_cec import KeyPressCommand, KeyReleaseCommand diff --git a/tests/components/hdmi_cec/conftest.py b/tests/components/hdmi_cec/conftest.py index c5f82c04e19..0756ea639b7 100644 --- a/tests/components/hdmi_cec/conftest.py +++ b/tests/components/hdmi_cec/conftest.py @@ -1,4 +1,5 @@ """Tests for the HDMI-CEC component.""" + from unittest.mock import patch import pytest diff --git a/tests/components/hdmi_cec/test_init.py b/tests/components/hdmi_cec/test_init.py index 7fb921225f3..b8cbf1ea8cd 100644 --- a/tests/components/hdmi_cec/test_init.py +++ b/tests/components/hdmi_cec/test_init.py @@ -1,4 +1,5 @@ """Tests for the HDMI-CEC component.""" + from datetime import timedelta from unittest.mock import ANY, PropertyMock, call, patch diff --git a/tests/components/hdmi_cec/test_media_player.py b/tests/components/hdmi_cec/test_media_player.py index 0a2f30c691a..4c2c5f42e6e 100644 --- a/tests/components/hdmi_cec/test_media_player.py +++ b/tests/components/hdmi_cec/test_media_player.py @@ -1,4 +1,5 @@ """Tests for the HDMI-CEC media player platform.""" + from pycec.const import ( DEVICE_TYPE_NAMES, KEY_BACKWARD, diff --git a/tests/components/hdmi_cec/test_switch.py b/tests/components/hdmi_cec/test_switch.py index ccb10a2f492..d54d6cc103b 100644 --- a/tests/components/hdmi_cec/test_switch.py +++ b/tests/components/hdmi_cec/test_switch.py @@ -1,4 +1,5 @@ """Tests for the HDMI-CEC switch platform.""" + from pycec.const import POWER_OFF, POWER_ON, STATUS_PLAY, STATUS_STILL, STATUS_STOP from pycec.network import PhysicalAddress import pytest diff --git a/tests/components/heos/conftest.py b/tests/components/heos/conftest.py index ee3847604c9..a12f4c610ad 100644 --- a/tests/components/heos/conftest.py +++ b/tests/components/heos/conftest.py @@ -1,4 +1,5 @@ """Configuration for HEOS tests.""" + from __future__ import annotations from collections.abc import Sequence @@ -52,8 +53,9 @@ def controller_fixture( mock_heos.create_group.return_value = None mock = Mock(return_value=mock_heos) - with patch("homeassistant.components.heos.Heos", new=mock), patch( - "homeassistant.components.heos.config_flow.Heos", new=mock + with ( + patch("homeassistant.components.heos.Heos", new=mock), + patch("homeassistant.components.heos.config_flow.Heos", new=mock), ): yield mock_heos diff --git a/tests/components/heos/test_config_flow.py b/tests/components/heos/test_config_flow.py index bfb6b03c898..7f0cd6cbd5a 100644 --- a/tests/components/heos/test_config_flow.py +++ b/tests/components/heos/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for the Heos config flow module.""" + from unittest.mock import patch from urllib.parse import urlparse diff --git a/tests/components/heos/test_init.py b/tests/components/heos/test_init.py index 761ab45dabc..fd453c70ebf 100644 --- a/tests/components/heos/test_init.py +++ b/tests/components/heos/test_init.py @@ -1,4 +1,5 @@ """Tests for the init module.""" + import asyncio from unittest.mock import Mock, patch @@ -79,7 +80,9 @@ async def test_async_setup_entry_loads_platforms( ) -> None: """Test load connects to heos, retrieves players, and loads platforms.""" config_entry.add_to_hass(hass) - with patch.object(hass.config_entries, "async_forward_entry_setup") as forward_mock: + with patch.object( + hass.config_entries, "async_forward_entry_setups" + ) as forward_mock: assert await async_setup_entry(hass, config_entry) # Assert platforms loaded await hass.async_block_till_done() @@ -106,7 +109,9 @@ async def test_async_setup_entry_not_signed_in_loads_platforms( config_entry.add_to_hass(hass) controller.is_signed_in = False controller.signed_in_username = None - with patch.object(hass.config_entries, "async_forward_entry_setup") as forward_mock: + with patch.object( + hass.config_entries, "async_forward_entry_setups" + ) as forward_mock: assert await async_setup_entry(hass, config_entry) # Assert platforms loaded await hass.async_block_till_done() diff --git a/tests/components/heos/test_media_player.py b/tests/components/heos/test_media_player.py index 70d96a0d5cb..99d09cfb7b1 100644 --- a/tests/components/heos/test_media_player.py +++ b/tests/components/heos/test_media_player.py @@ -1,4 +1,5 @@ """Tests for the Heos Media Player platform.""" + import asyncio from pyheos import CommandFailedError, const diff --git a/tests/components/heos/test_services.py b/tests/components/heos/test_services.py index 88eda68abb6..2d812eb83ab 100644 --- a/tests/components/heos/test_services.py +++ b/tests/components/heos/test_services.py @@ -1,4 +1,5 @@ """Tests for the services module.""" + from pyheos import CommandFailedError, HeosError, const import pytest diff --git a/tests/components/here_travel_time/conftest.py b/tests/components/here_travel_time/conftest.py index dff91a4e1fb..f318016315a 100644 --- a/tests/components/here_travel_time/conftest.py +++ b/tests/components/here_travel_time/conftest.py @@ -1,4 +1,5 @@ """Fixtures for HERE Travel Time tests.""" + import json from unittest.mock import patch @@ -19,35 +20,40 @@ BIKE_RESPONSE = json.loads(load_fixture("here_travel_time/bike_response.json")) @pytest.fixture(name="valid_response") def valid_response_fixture(): """Return valid api response.""" - with patch( - "here_transit.HERETransitApi.route", return_value=TRANSIT_RESPONSE - ), patch( - "here_routing.HERERoutingApi.route", - return_value=RESPONSE, - ) as mock: + with ( + patch("here_transit.HERETransitApi.route", return_value=TRANSIT_RESPONSE), + patch( + "here_routing.HERERoutingApi.route", + return_value=RESPONSE, + ) as mock, + ): yield mock @pytest.fixture(name="bike_response") def bike_response_fixture(): """Return valid api response.""" - with patch( - "here_transit.HERETransitApi.route", return_value=TRANSIT_RESPONSE - ), patch( - "here_routing.HERERoutingApi.route", - return_value=BIKE_RESPONSE, - ) as mock: + with ( + patch("here_transit.HERETransitApi.route", return_value=TRANSIT_RESPONSE), + patch( + "here_routing.HERERoutingApi.route", + return_value=BIKE_RESPONSE, + ) as mock, + ): yield mock @pytest.fixture(name="no_attribution_response") def no_attribution_response_fixture(): """Return valid api response without attribution.""" - with patch( - "here_transit.HERETransitApi.route", - return_value=NO_ATTRIBUTION_TRANSIT_RESPONSE, - ), patch( - "here_routing.HERERoutingApi.route", - return_value=RESPONSE, - ) as mock: + with ( + patch( + "here_transit.HERETransitApi.route", + return_value=NO_ATTRIBUTION_TRANSIT_RESPONSE, + ), + patch( + "here_routing.HERERoutingApi.route", + return_value=RESPONSE, + ) as mock, + ): yield mock diff --git a/tests/components/here_travel_time/test_config_flow.py b/tests/components/here_travel_time/test_config_flow.py index 42add4192e5..51b12978856 100644 --- a/tests/components/here_travel_time/test_config_flow.py +++ b/tests/components/here_travel_time/test_config_flow.py @@ -1,4 +1,5 @@ """Test the HERE Travel Time config flow.""" + from unittest.mock import patch from here_routing import HERERoutingError, HERERoutingUnauthorizedError @@ -115,7 +116,7 @@ async def origin_step_result_fixture( @pytest.mark.parametrize( "menu_options", - (["origin_coordinates", "origin_entity"],), + [["origin_coordinates", "origin_entity"]], ) @pytest.mark.usefixtures("valid_response") async def test_step_user(hass: HomeAssistant, menu_options) -> None: diff --git a/tests/components/here_travel_time/test_sensor.py b/tests/components/here_travel_time/test_sensor.py index 21580c48f33..0231ac6428f 100644 --- a/tests/components/here_travel_time/test_sensor.py +++ b/tests/components/here_travel_time/test_sensor.py @@ -1,4 +1,5 @@ """The test for the HERE Travel Time sensor platform.""" + from datetime import timedelta from unittest.mock import MagicMock, patch @@ -59,7 +60,7 @@ from homeassistant.const import ( CONF_API_KEY, CONF_MODE, CONF_NAME, - EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STARTED, UnitOfLength, UnitOfTime, ) @@ -121,6 +122,7 @@ async def test_sensor( departure_time, ) -> None: """Test that sensor works.""" + hass.set_state(CoreState.not_running) entry = MockConfigEntry( domain=DOMAIN, unique_id="0123456789", @@ -142,7 +144,7 @@ async def test_sensor( entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() duration = hass.states.get("sensor.test_duration") @@ -200,7 +202,7 @@ async def test_circular_ref( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() assert "No coordinates found for test.first" in caplog.text @@ -209,6 +211,7 @@ async def test_circular_ref( @pytest.mark.usefixtures("valid_response") async def test_public_transport(hass: HomeAssistant) -> None: """Test that public transport mode is handled.""" + hass.set_state(CoreState.not_running) entry = MockConfigEntry( domain=DOMAIN, unique_id="0123456789", @@ -231,7 +234,7 @@ async def test_public_transport(hass: HomeAssistant) -> None: await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() assert ( @@ -262,7 +265,7 @@ async def test_no_attribution_response(hass: HomeAssistant) -> None: await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() assert ( @@ -272,6 +275,7 @@ async def test_no_attribution_response(hass: HomeAssistant) -> None: async def test_entity_ids(hass: HomeAssistant, valid_response: MagicMock) -> None: """Test that origin/destination supplied by entities works.""" + hass.set_state(CoreState.not_running) zone_config = { "zone": [ { @@ -308,7 +312,7 @@ async def test_entity_ids(hass: HomeAssistant, valid_response: MagicMock) -> Non await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() assert hass.states.get("sensor.test_distance").state == "13.682" @@ -347,7 +351,7 @@ async def test_destination_entity_not_found( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() assert "Could not find entity device_tracker.test" in caplog.text @@ -375,7 +379,7 @@ async def test_origin_entity_not_found( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() assert "Could not find entity device_tracker.test" in caplog.text @@ -407,7 +411,7 @@ async def test_invalid_destination_entity_state( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() assert ( @@ -441,7 +445,7 @@ async def test_invalid_origin_entity_state( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() assert ( @@ -476,7 +480,7 @@ async def test_route_not_found( entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() assert "Route calculation failed: Couldn't find a route." in caplog.text @@ -634,6 +638,7 @@ async def test_transit_errors( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, exception, expected_message ) -> None: """Test that transit errors are correctly handled.""" + hass.set_state(CoreState.not_running) with patch( "here_transit.HERETransitApi.route", side_effect=exception(), @@ -655,7 +660,7 @@ async def test_transit_errors( entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() assert expected_message in caplog.text @@ -667,6 +672,7 @@ async def test_routing_rate_limit( freezer: FrozenDateTimeFactory, ) -> None: """Test that rate limiting is applied when encountering HTTP 429.""" + hass.set_state(CoreState.not_running) with patch( "here_routing.HERERoutingApi.route", return_value=RESPONSE, @@ -680,7 +686,7 @@ async def test_routing_rate_limit( entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() assert hass.states.get("sensor.test_distance").state == "13.682" @@ -715,6 +721,7 @@ async def test_transit_rate_limit( freezer: FrozenDateTimeFactory, ) -> None: """Test that rate limiting is applied when encountering HTTP 429.""" + hass.set_state(CoreState.not_running) with patch( "here_transit.HERETransitApi.route", return_value=TRANSIT_RESPONSE, @@ -736,7 +743,7 @@ async def test_transit_rate_limit( entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() assert hass.states.get("sensor.test_distance").state == "1.883" @@ -770,6 +777,7 @@ async def test_multiple_sections( hass: HomeAssistant, ) -> None: """Test that multiple sections are handled correctly.""" + hass.set_state(CoreState.not_running) entry = MockConfigEntry( domain=DOMAIN, unique_id="0123456789", @@ -787,7 +795,7 @@ async def test_multiple_sections( entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() duration = hass.states.get("sensor.test_duration") diff --git a/tests/components/hisense_aehw4a1/test_init.py b/tests/components/hisense_aehw4a1/test_init.py index 3cf89173f20..1abac3421a6 100644 --- a/tests/components/hisense_aehw4a1/test_init.py +++ b/tests/components/hisense_aehw4a1/test_init.py @@ -1,4 +1,5 @@ """Tests for the Hisense AEH-W4A1 init file.""" + from unittest.mock import patch from pyaehw4a1 import exceptions @@ -11,13 +12,16 @@ from homeassistant.setup import async_setup_component async def test_creating_entry_sets_up_climate_discovery(hass: HomeAssistant) -> None: """Test setting up Hisense AEH-W4A1 loads the climate component.""" - with patch( - "homeassistant.components.hisense_aehw4a1.config_flow.AehW4a1.discovery", - return_value=["1.2.3.4"], - ), patch( - "homeassistant.components.hisense_aehw4a1.climate.async_setup_entry", - return_value=True, - ) as mock_setup: + with ( + patch( + "homeassistant.components.hisense_aehw4a1.config_flow.AehW4a1.discovery", + return_value=["1.2.3.4"], + ), + patch( + "homeassistant.components.hisense_aehw4a1.climate.async_setup_entry", + return_value=True, + ) as mock_setup, + ): result = await hass.config_entries.flow.async_init( hisense_aehw4a1.DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -35,13 +39,16 @@ async def test_creating_entry_sets_up_climate_discovery(hass: HomeAssistant) -> async def test_configuring_hisense_w4a1_create_entry(hass: HomeAssistant) -> None: """Test that specifying config will create an entry.""" - with patch( - "homeassistant.components.hisense_aehw4a1.config_flow.AehW4a1.check", - return_value=True, - ), patch( - "homeassistant.components.hisense_aehw4a1.async_setup_entry", - return_value=True, - ) as mock_setup: + with ( + patch( + "homeassistant.components.hisense_aehw4a1.config_flow.AehW4a1.check", + return_value=True, + ), + patch( + "homeassistant.components.hisense_aehw4a1.async_setup_entry", + return_value=True, + ) as mock_setup, + ): await async_setup_component( hass, hisense_aehw4a1.DOMAIN, @@ -56,13 +63,16 @@ async def test_configuring_hisense_w4a1_not_creates_entry_for_device_not_found( hass: HomeAssistant, ) -> None: """Test that specifying config will not create an entry.""" - with patch( - "homeassistant.components.hisense_aehw4a1.config_flow.AehW4a1.check", - side_effect=exceptions.ConnectionError, - ), patch( - "homeassistant.components.hisense_aehw4a1.async_setup_entry", - return_value=True, - ) as mock_setup: + with ( + patch( + "homeassistant.components.hisense_aehw4a1.config_flow.AehW4a1.check", + side_effect=exceptions.ConnectionError, + ), + patch( + "homeassistant.components.hisense_aehw4a1.async_setup_entry", + return_value=True, + ) as mock_setup, + ): await async_setup_component( hass, hisense_aehw4a1.DOMAIN, diff --git a/tests/components/history/conftest.py b/tests/components/history/conftest.py index 86d8d2a9421..0ce6a190f55 100644 --- a/tests/components/history/conftest.py +++ b/tests/components/history/conftest.py @@ -1,4 +1,5 @@ """Fixtures for history tests.""" + import pytest from homeassistant.components import history diff --git a/tests/components/history/test_init.py b/tests/components/history/test_init.py index 356fbb86b01..13574bb2bb2 100644 --- a/tests/components/history/test_init.py +++ b/tests/components/history/test_init.py @@ -1,4 +1,5 @@ """The tests the History component.""" + from datetime import timedelta from http import HTTPStatus import json @@ -240,9 +241,7 @@ def test_get_significant_states_only(hass_history) -> None: return hass.states.get(entity_id) start = dt_util.utcnow() - timedelta(minutes=4) - points = [] - for i in range(1, 4): - points.append(start + timedelta(minutes=i)) + points = [start + timedelta(minutes=i) for i in range(1, 4)] states = [] with freeze_time(start) as freezer: diff --git a/tests/components/history/test_init_db_schema_30.py b/tests/components/history/test_init_db_schema_30.py index caf151cafe7..0bbd913ce2b 100644 --- a/tests/components/history/test_init_db_schema_30.py +++ b/tests/components/history/test_init_db_schema_30.py @@ -1,4 +1,5 @@ """The tests the History component.""" + from __future__ import annotations from datetime import timedelta @@ -256,9 +257,7 @@ def test_get_significant_states_only(legacy_hass_history) -> None: return hass.states.get(entity_id) start = dt_util.utcnow() - timedelta(minutes=4) - points = [] - for i in range(1, 4): - points.append(start + timedelta(minutes=i)) + points = [start + timedelta(minutes=i) for i in range(1, 4)] states = [] with freeze_time(start) as freezer: diff --git a/tests/components/history/test_websocket_api.py b/tests/components/history/test_websocket_api.py index 9ba47303e53..70e2eb9470a 100644 --- a/tests/components/history/test_websocket_api.py +++ b/tests/components/history/test_websocket_api.py @@ -1,4 +1,5 @@ """The tests the History component websocket_api.""" + import asyncio from datetime import timedelta from unittest.mock import patch @@ -1331,7 +1332,7 @@ async def test_history_stream_live_with_future_end_time( ) == listeners_without_writes(init_listeners) -@pytest.mark.parametrize("include_start_time_state", (True, False)) +@pytest.mark.parametrize("include_start_time_state", [True, False]) async def test_history_stream_before_history_starts( recorder_mock: Recorder, hass: HomeAssistant, @@ -1834,7 +1835,6 @@ async def test_history_stream_historical_only_with_start_time_state_past( await async_setup_component(hass, "sensor", {}) hass.states.async_set("sensor.one", "first", attributes={"any": "attr"}) - hass.states.get("sensor.one").last_updated await async_recorder_block_till_done(hass) await asyncio.sleep(0.00002) diff --git a/tests/components/history_stats/test_sensor.py b/tests/components/history_stats/test_sensor.py index c421a1b8c5c..1982ec12188 100644 --- a/tests/components/history_stats/test_sensor.py +++ b/tests/components/history_stats/test_sensor.py @@ -1,4 +1,5 @@ """The test for the History Statistics sensor platform.""" + from datetime import datetime, timedelta from unittest.mock import patch @@ -610,10 +611,13 @@ async def test_async_start_from_history_and_switch_to_watching_state_changes_sin ] } - with patch( - "homeassistant.components.recorder.history.state_changes_during_period", - _fake_states, - ), freeze_time(start_time): + with ( + patch( + "homeassistant.components.recorder.history.state_changes_during_period", + _fake_states, + ), + freeze_time(start_time), + ): await async_setup_component( hass, "sensor", @@ -708,10 +712,13 @@ async def test_async_start_from_history_and_switch_to_watching_state_changes_sin ] } - with patch( - "homeassistant.components.recorder.history.state_changes_during_period", - _fake_states, - ), freeze_time(start_time): + with ( + patch( + "homeassistant.components.recorder.history.state_changes_during_period", + _fake_states, + ), + freeze_time(start_time), + ): await async_setup_component( hass, "sensor", @@ -822,10 +829,13 @@ async def test_async_start_from_history_and_switch_to_watching_state_changes_mul ] } - with patch( - "homeassistant.components.recorder.history.state_changes_during_period", - _fake_states, - ), freeze_time(start_time): + with ( + patch( + "homeassistant.components.recorder.history.state_changes_during_period", + _fake_states, + ), + freeze_time(start_time), + ): await async_setup_component( hass, "sensor", @@ -1002,7 +1012,7 @@ async def test_does_not_work_into_the_future( one_hour_in = start_time + timedelta(minutes=60) with freeze_time(one_hour_in): async_fire_time_changed(hass, one_hour_in) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get("sensor.sensor1").state == STATE_UNKNOWN assert hass.states.get("sensor.sensor2").state == STATE_UNKNOWN @@ -1012,7 +1022,7 @@ async def test_does_not_work_into_the_future( hass.states.async_set("binary_sensor.state", "off") await hass.async_block_till_done() async_fire_time_changed(hass, turn_off_time) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get("sensor.sensor1").state == STATE_UNKNOWN assert hass.states.get("sensor.sensor2").state == STATE_UNKNOWN @@ -1020,7 +1030,7 @@ async def test_does_not_work_into_the_future( turn_back_on_time = start_time + timedelta(minutes=105) with freeze_time(turn_back_on_time): async_fire_time_changed(hass, turn_back_on_time) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get("sensor.sensor1").state == STATE_UNKNOWN assert hass.states.get("sensor.sensor2").state == STATE_UNKNOWN @@ -1035,7 +1045,7 @@ async def test_does_not_work_into_the_future( end_time = start_time + timedelta(minutes=120) with freeze_time(end_time): async_fire_time_changed(hass, end_time) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get("sensor.sensor1").state == STATE_UNKNOWN assert hass.states.get("sensor.sensor2").state == STATE_UNKNOWN @@ -1043,18 +1053,21 @@ async def test_does_not_work_into_the_future( in_the_window = start_time + timedelta(hours=23, minutes=5) with freeze_time(in_the_window): async_fire_time_changed(hass, in_the_window) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get("sensor.sensor1").state == "0.08" assert hass.states.get("sensor.sensor2").state == "0.0833333333333333" past_the_window = start_time + timedelta(hours=25) - with patch( - "homeassistant.components.recorder.history.state_changes_during_period", - return_value=[], - ), freeze_time(past_the_window): + with ( + patch( + "homeassistant.components.recorder.history.state_changes_during_period", + return_value=[], + ), + freeze_time(past_the_window), + ): async_fire_time_changed(hass, past_the_window) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get("sensor.sensor1").state == STATE_UNKNOWN @@ -1071,22 +1084,28 @@ async def test_does_not_work_into_the_future( } past_the_window_with_data = start_time + timedelta(hours=26) - with patch( - "homeassistant.components.recorder.history.state_changes_during_period", - _fake_off_states, - ), freeze_time(past_the_window_with_data): + with ( + patch( + "homeassistant.components.recorder.history.state_changes_during_period", + _fake_off_states, + ), + freeze_time(past_the_window_with_data), + ): async_fire_time_changed(hass, past_the_window_with_data) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get("sensor.sensor1").state == STATE_UNKNOWN at_the_next_window_with_data = start_time + timedelta(days=1, hours=23) - with patch( - "homeassistant.components.recorder.history.state_changes_during_period", - _fake_off_states, - ), freeze_time(at_the_next_window_with_data): + with ( + patch( + "homeassistant.components.recorder.history.state_changes_during_period", + _fake_off_states, + ), + freeze_time(at_the_next_window_with_data), + ): async_fire_time_changed(hass, at_the_next_window_with_data) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get("sensor.sensor1").state == "0.0" @@ -1203,10 +1222,13 @@ async def test_measure_sliding_window( ) await hass.async_block_till_done() - with patch( - "homeassistant.components.recorder.history.state_changes_during_period", - _fake_states, - ), freeze_time(start_time): + with ( + patch( + "homeassistant.components.recorder.history.state_changes_during_period", + _fake_states, + ), + freeze_time(start_time), + ): for i in range(1, 5): await async_update_entity(hass, f"sensor.sensor{i}") await hass.async_block_till_done() @@ -1217,10 +1239,13 @@ async def test_measure_sliding_window( assert hass.states.get("sensor.sensor4").state == "41.7" past_next_update = start_time + timedelta(minutes=30) - with patch( - "homeassistant.components.recorder.history.state_changes_during_period", - _fake_states, - ), freeze_time(past_next_update): + with ( + patch( + "homeassistant.components.recorder.history.state_changes_during_period", + _fake_states, + ), + freeze_time(past_next_update), + ): async_fire_time_changed(hass, past_next_update) await hass.async_block_till_done() @@ -1252,10 +1277,13 @@ async def test_measure_from_end_going_backwards( ] } - with patch( - "homeassistant.components.recorder.history.state_changes_during_period", - _fake_states, - ), freeze_time(start_time): + with ( + patch( + "homeassistant.components.recorder.history.state_changes_during_period", + _fake_states, + ), + freeze_time(start_time), + ): await async_setup_component( hass, "sensor", @@ -1312,10 +1340,13 @@ async def test_measure_from_end_going_backwards( assert hass.states.get("sensor.sensor4").state == "83.3" past_next_update = start_time + timedelta(minutes=30) - with patch( - "homeassistant.components.recorder.history.state_changes_during_period", - _fake_states, - ), freeze_time(past_next_update): + with ( + patch( + "homeassistant.components.recorder.history.state_changes_during_period", + _fake_states, + ), + freeze_time(past_next_update), + ): async_fire_time_changed(hass, past_next_update) await hass.async_block_till_done() @@ -1445,9 +1476,12 @@ async def test_end_time_with_microseconds_zeroed( ] } - with freeze_time(time_200), patch( - "homeassistant.components.recorder.history.state_changes_during_period", - _fake_states, + with ( + freeze_time(time_200), + patch( + "homeassistant.components.recorder.history.state_changes_during_period", + _fake_states, + ), ): await async_setup_component( hass, @@ -1486,7 +1520,7 @@ async def test_end_time_with_microseconds_zeroed( ) async_fire_time_changed(hass, time_200) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get("sensor.heatpump_compressor_today").state == "1.83" assert ( hass.states.get("sensor.heatpump_compressor_today2").state @@ -1498,7 +1532,7 @@ async def test_end_time_with_microseconds_zeroed( time_400 = start_of_today + timedelta(hours=4) with freeze_time(time_400): async_fire_time_changed(hass, time_400) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get("sensor.heatpump_compressor_today").state == "1.83" assert ( hass.states.get("sensor.heatpump_compressor_today2").state @@ -1509,7 +1543,7 @@ async def test_end_time_with_microseconds_zeroed( time_600 = start_of_today + timedelta(hours=6) with freeze_time(time_600): async_fire_time_changed(hass, time_600) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get("sensor.heatpump_compressor_today").state == "3.83" assert ( hass.states.get("sensor.heatpump_compressor_today2").state @@ -1524,7 +1558,7 @@ async def test_end_time_with_microseconds_zeroed( with freeze_time(rolled_to_next_day): async_fire_time_changed(hass, rolled_to_next_day) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get("sensor.heatpump_compressor_today").state == "0.0" assert hass.states.get("sensor.heatpump_compressor_today2").state == "0.0" @@ -1533,7 +1567,7 @@ async def test_end_time_with_microseconds_zeroed( ) with freeze_time(rolled_to_next_day_plus_12): async_fire_time_changed(hass, rolled_to_next_day_plus_12) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get("sensor.heatpump_compressor_today").state == "12.0" assert hass.states.get("sensor.heatpump_compressor_today2").state == "12.0" @@ -1542,7 +1576,7 @@ async def test_end_time_with_microseconds_zeroed( ) with freeze_time(rolled_to_next_day_plus_14): async_fire_time_changed(hass, rolled_to_next_day_plus_14) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get("sensor.heatpump_compressor_today").state == "14.0" assert hass.states.get("sensor.heatpump_compressor_today2").state == "14.0" @@ -1553,12 +1587,12 @@ async def test_end_time_with_microseconds_zeroed( hass.states.async_set("binary_sensor.heatpump_compressor_state", "off") await async_wait_recording_done(hass) async_fire_time_changed(hass, rolled_to_next_day_plus_16_860000) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) rolled_to_next_day_plus_18 = start_of_today + timedelta(days=1, hours=18) with freeze_time(rolled_to_next_day_plus_18): async_fire_time_changed(hass, rolled_to_next_day_plus_18) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get("sensor.heatpump_compressor_today").state == "16.0" assert ( hass.states.get("sensor.heatpump_compressor_today2").state @@ -1636,10 +1670,13 @@ async def test_history_stats_handles_floored_timestamps( ] } - with patch( - "homeassistant.components.recorder.history.state_changes_during_period", - _fake_states, - ), freeze_time(start_time): + with ( + patch( + "homeassistant.components.recorder.history.state_changes_during_period", + _fake_states, + ), + freeze_time(start_time), + ): await async_setup_component( hass, "sensor", diff --git a/tests/components/hive/test_config_flow.py b/tests/components/hive/test_config_flow.py index 69969b4ac0d..b1553b2c485 100644 --- a/tests/components/hive/test_config_flow.py +++ b/tests/components/hive/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Hive config flow.""" + from unittest.mock import patch from apyhiveapi.helper import hive_exceptions @@ -26,21 +27,25 @@ MFA_INVALID_CODE = "HIVE" async def test_import_flow(hass: HomeAssistant) -> None: """Check import flow.""" - with patch( - "homeassistant.components.hive.config_flow.Auth.login", - return_value={ - "ChallengeName": "SUCCESS", - "AuthenticationResult": { - "RefreshToken": "mock-refresh-token", - "AccessToken": "mock-access-token", + with ( + patch( + "homeassistant.components.hive.config_flow.Auth.login", + return_value={ + "ChallengeName": "SUCCESS", + "AuthenticationResult": { + "RefreshToken": "mock-refresh-token", + "AccessToken": "mock-access-token", + }, }, - }, - ), patch( - "homeassistant.components.hive.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.hive.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + ), + patch( + "homeassistant.components.hive.async_setup", return_value=True + ) as mock_setup, + patch( + "homeassistant.components.hive.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}, @@ -74,21 +79,25 @@ async def test_user_flow(hass: HomeAssistant) -> None: assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} - with patch( - "homeassistant.components.hive.config_flow.Auth.login", - return_value={ - "ChallengeName": "SUCCESS", - "AuthenticationResult": { - "RefreshToken": "mock-refresh-token", - "AccessToken": "mock-access-token", + with ( + patch( + "homeassistant.components.hive.config_flow.Auth.login", + return_value={ + "ChallengeName": "SUCCESS", + "AuthenticationResult": { + "RefreshToken": "mock-refresh-token", + "AccessToken": "mock-access-token", + }, }, - }, - ), patch( - "homeassistant.components.hive.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.hive.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + ), + patch( + "homeassistant.components.hive.async_setup", return_value=True + ) as mock_setup, + patch( + "homeassistant.components.hive.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, @@ -162,22 +171,27 @@ async def test_user_flow_2fa(hass: HomeAssistant) -> None: assert result3["step_id"] == "configuration" assert result3["errors"] == {} - with patch( - "homeassistant.components.hive.config_flow.Auth.device_registration", - return_value=True, - ), patch( - "homeassistant.components.hive.config_flow.Auth.get_device_data", - return_value=[ - "mock-device-group-key", - "mock-device-key", - "mock-device-password", - ], - ), patch( - "homeassistant.components.hive.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.hive.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.hive.config_flow.Auth.device_registration", + return_value=True, + ), + patch( + "homeassistant.components.hive.config_flow.Auth.get_device_data", + return_value=[ + "mock-device-group-key", + "mock-device-key", + "mock-device-password", + ], + ), + patch( + "homeassistant.components.hive.async_setup", return_value=True + ) as mock_setup, + patch( + "homeassistant.components.hive.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result4 = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -316,19 +330,22 @@ async def test_reauth_2fa_flow(hass: HomeAssistant) -> None: }, ) - with patch( - "homeassistant.components.hive.config_flow.Auth.sms_2fa", - return_value={ - "ChallengeName": "SUCCESS", - "AuthenticationResult": { - "RefreshToken": "mock-refresh-token", - "AccessToken": "mock-access-token", + with ( + patch( + "homeassistant.components.hive.config_flow.Auth.sms_2fa", + return_value={ + "ChallengeName": "SUCCESS", + "AuthenticationResult": { + "RefreshToken": "mock-refresh-token", + "AccessToken": "mock-access-token", + }, }, - }, - ), patch( - "homeassistant.components.hive.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + ), + patch( + "homeassistant.components.hive.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], { @@ -445,22 +462,27 @@ async def test_user_flow_2fa_send_new_code(hass: HomeAssistant) -> None: assert result4["step_id"] == "configuration" assert result4["errors"] == {} - with patch( - "homeassistant.components.hive.config_flow.Auth.device_registration", - return_value=True, - ), patch( - "homeassistant.components.hive.config_flow.Auth.get_device_data", - return_value=[ - "mock-device-group-key", - "mock-device-key", - "mock-device-password", - ], - ), patch( - "homeassistant.components.hive.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.hive.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.hive.config_flow.Auth.device_registration", + return_value=True, + ), + patch( + "homeassistant.components.hive.config_flow.Auth.get_device_data", + return_value=[ + "mock-device-group-key", + "mock-device-key", + "mock-device-password", + ], + ), + patch( + "homeassistant.components.hive.async_setup", return_value=True + ) as mock_setup, + patch( + "homeassistant.components.hive.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result5 = await hass.config_entries.flow.async_configure( result4["flow_id"], {CONF_DEVICE_NAME: DEVICE_NAME} ) @@ -717,16 +739,19 @@ async def test_user_flow_2fa_unknown_error(hass: HomeAssistant) -> None: assert result3["step_id"] == "configuration" assert result3["errors"] == {} - with patch( - "homeassistant.components.hive.config_flow.Auth.device_registration", - return_value=True, - ), patch( - "homeassistant.components.hive.config_flow.Auth.get_device_data", - return_value=[ - "mock-device-group-key", - "mock-device-key", - "mock-device-password", - ], + with ( + patch( + "homeassistant.components.hive.config_flow.Auth.device_registration", + return_value=True, + ), + patch( + "homeassistant.components.hive.config_flow.Auth.get_device_data", + return_value=[ + "mock-device-group-key", + "mock-device-key", + "mock-device-password", + ], + ), ): result4 = await hass.config_entries.flow.async_configure( result["flow_id"], diff --git a/tests/components/hko/conftest.py b/tests/components/hko/conftest.py index fd2181ddfc9..853eca6507b 100644 --- a/tests/components/hko/conftest.py +++ b/tests/components/hko/conftest.py @@ -1,4 +1,5 @@ """Configure py.test.""" + import json from unittest.mock import patch diff --git a/tests/components/hlk_sw16/test_config_flow.py b/tests/components/hlk_sw16/test_config_flow.py index d390bcc6c79..e4770343114 100644 --- a/tests/components/hlk_sw16/test_config_flow.py +++ b/tests/components/hlk_sw16/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Hi-Link HLK-SW16 config flow.""" + import asyncio from unittest.mock import patch @@ -64,15 +65,19 @@ async def test_form(hass: HomeAssistant) -> None: mock_hlk_sw16_connection = await create_mock_hlk_sw16_connection(False) - with patch( - "homeassistant.components.hlk_sw16.config_flow.create_hlk_sw16_connection", - return_value=mock_hlk_sw16_connection, - ), patch( - "homeassistant.components.hlk_sw16.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.hlk_sw16.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.hlk_sw16.config_flow.create_hlk_sw16_connection", + return_value=mock_hlk_sw16_connection, + ), + patch( + "homeassistant.components.hlk_sw16.async_setup", return_value=True + ) as mock_setup, + patch( + "homeassistant.components.hlk_sw16.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], conf, @@ -125,15 +130,19 @@ async def test_import(hass: HomeAssistant) -> None: mock_hlk_sw16_connection = await create_mock_hlk_sw16_connection(False) - with patch( - "homeassistant.components.hlk_sw16.config_flow.connect_client", - return_value=mock_hlk_sw16_connection, - ), patch( - "homeassistant.components.hlk_sw16.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.hlk_sw16.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.hlk_sw16.config_flow.connect_client", + return_value=mock_hlk_sw16_connection, + ), + patch( + "homeassistant.components.hlk_sw16.async_setup", return_value=True + ) as mock_setup, + patch( + "homeassistant.components.hlk_sw16.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], conf, diff --git a/tests/components/holiday/conftest.py b/tests/components/holiday/conftest.py index d9b0d1a5788..92f46c8b238 100644 --- a/tests/components/holiday/conftest.py +++ b/tests/components/holiday/conftest.py @@ -1,4 +1,5 @@ """Common fixtures for the Holiday tests.""" + from collections.abc import Generator from unittest.mock import AsyncMock, patch diff --git a/tests/components/holiday/test_calendar.py b/tests/components/holiday/test_calendar.py index df0ce6d50d5..b5067a467ed 100644 --- a/tests/components/holiday/test_calendar.py +++ b/tests/components/holiday/test_calendar.py @@ -1,4 +1,5 @@ """Tests for calendar platform of Holiday integration.""" + from datetime import datetime, timedelta from freezegun.api import FrozenDateTimeFactory diff --git a/tests/components/holiday/test_config_flow.py b/tests/components/holiday/test_config_flow.py index 7dce6131616..44a72f58404 100644 --- a/tests/components/holiday/test_config_flow.py +++ b/tests/components/holiday/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Holiday config flow.""" + from unittest.mock import AsyncMock import pytest @@ -218,3 +219,114 @@ async def test_form_babel_replace_dash_with_underscore(hass: HomeAssistant) -> N "country": "DE", "province": "BW", } + + +async def test_reconfigure(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: + """Test reconfigure flow.""" + entry = MockConfigEntry( + domain=DOMAIN, + title="Germany, BW", + data={"country": "DE", "province": "BW"}, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_RECONFIGURE, + "entry_id": entry.entry_id, + }, + ) + assert result["type"] == FlowResultType.FORM + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_PROVINCE: "NW", + }, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + entry = hass.config_entries.async_get_entry(entry.entry_id) + assert entry.title == "Germany, NW" + assert entry.data == {"country": "DE", "province": "NW"} + + +async def test_reconfigure_incorrect_language( + hass: HomeAssistant, mock_setup_entry: AsyncMock +) -> None: + """Test reconfigure flow default to English.""" + hass.config.language = "en-XX" + + entry = MockConfigEntry( + domain=DOMAIN, + title="Germany, BW", + data={"country": "DE", "province": "BW"}, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_RECONFIGURE, + "entry_id": entry.entry_id, + }, + ) + assert result["type"] == FlowResultType.FORM + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_PROVINCE: "NW", + }, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + entry = hass.config_entries.async_get_entry(entry.entry_id) + assert entry.title == "Germany, NW" + assert entry.data == {"country": "DE", "province": "NW"} + + +async def test_reconfigure_entry_exists( + hass: HomeAssistant, mock_setup_entry: AsyncMock +) -> None: + """Test reconfigure flow stops if other entry already exist.""" + entry = MockConfigEntry( + domain=DOMAIN, + title="Germany, BW", + data={"country": "DE", "province": "BW"}, + ) + entry.add_to_hass(hass) + entry2 = MockConfigEntry( + domain=DOMAIN, + title="Germany, NW", + data={"country": "DE", "province": "NW"}, + ) + entry2.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_RECONFIGURE, + "entry_id": entry.entry_id, + }, + ) + assert result["type"] == FlowResultType.FORM + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_PROVINCE: "NW", + }, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + entry = hass.config_entries.async_get_entry(entry.entry_id) + assert entry.title == "Germany, BW" + assert entry.data == {"country": "DE", "province": "BW"} diff --git a/tests/components/home_connect/test_config_flow.py b/tests/components/home_connect/test_config_flow.py index 209100c71b2..74ca918889d 100644 --- a/tests/components/home_connect/test_config_flow.py +++ b/tests/components/home_connect/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Home Connect config flow.""" + from http import HTTPStatus from unittest.mock import patch diff --git a/tests/components/homeassistant/test_exposed_entities.py b/tests/components/homeassistant/test_exposed_entities.py index fd09bcee45a..e20fcb69d00 100644 --- a/tests/components/homeassistant/test_exposed_entities.py +++ b/tests/components/homeassistant/test_exposed_entities.py @@ -1,4 +1,5 @@ """Test Home Assistant exposed entities helper.""" + import pytest from syrupy.assertion import SnapshotAssertion diff --git a/tests/components/homeassistant/test_init.py b/tests/components/homeassistant/test_init.py index 09a9ffc58b8..84319df2888 100644 --- a/tests/components/homeassistant/test_init.py +++ b/tests/components/homeassistant/test_init.py @@ -1,4 +1,5 @@ """The tests for Core components.""" + import asyncio import unittest from unittest.mock import Mock, patch @@ -135,10 +136,11 @@ class TestComponentsCore(unittest.TestCase): def test_is_on(self): """Test is_on method.""" - assert comps.is_on(self.hass, "light.Bowl") - assert not comps.is_on(self.hass, "light.Ceiling") - assert comps.is_on(self.hass) - assert not comps.is_on(self.hass, "non_existing.entity") + with pytest.raises( + RuntimeError, + match="Detected code that uses homeassistant.components.is_on. This is deprecated and will stop working", + ): + assert comps.is_on(self.hass, "light.Bowl") def test_turn_on_without_entities(self): """Test turn_on method without entities.""" @@ -471,10 +473,13 @@ async def test_raises_when_db_upgrade_in_progress( """Test an exception is raised when the database migration is in progress.""" await async_setup_component(hass, "homeassistant", {}) - with pytest.raises(HomeAssistantError), patch( - "homeassistant.helpers.recorder.async_migration_in_progress", - return_value=True, - ) as mock_async_migration_in_progress: + with ( + pytest.raises(HomeAssistantError), + patch( + "homeassistant.helpers.recorder.async_migration_in_progress", + return_value=True, + ) as mock_async_migration_in_progress, + ): await hass.services.async_call( "homeassistant", service, @@ -486,11 +491,12 @@ async def test_raises_when_db_upgrade_in_progress( assert mock_async_migration_in_progress.called caplog.clear() - with patch( - "homeassistant.helpers.recorder.async_migration_in_progress", - return_value=False, - ) as mock_async_migration_in_progress, patch( - "homeassistant.config.async_check_ha_config_file", return_value=None + with ( + patch( + "homeassistant.helpers.recorder.async_migration_in_progress", + return_value=False, + ) as mock_async_migration_in_progress, + patch("homeassistant.config.async_check_ha_config_file", return_value=None), ): await hass.services.async_call( "homeassistant", @@ -509,12 +515,16 @@ async def test_raises_when_config_is_invalid( """Test an exception is raised when the configuration is invalid.""" await async_setup_component(hass, "homeassistant", {}) - with pytest.raises(HomeAssistantError), patch( - "homeassistant.helpers.recorder.async_migration_in_progress", - return_value=False, - ), patch( - "homeassistant.config.async_check_ha_config_file", return_value=["Error 1"] - ) as mock_async_check_ha_config_file: + with ( + pytest.raises(HomeAssistantError), + patch( + "homeassistant.helpers.recorder.async_migration_in_progress", + return_value=False, + ), + patch( + "homeassistant.config.async_check_ha_config_file", return_value=["Error 1"] + ) as mock_async_check_ha_config_file, + ): await hass.services.async_call( "homeassistant", SERVICE_HOMEASSISTANT_RESTART, @@ -527,12 +537,15 @@ async def test_raises_when_config_is_invalid( assert mock_async_check_ha_config_file.called caplog.clear() - with patch( - "homeassistant.helpers.recorder.async_migration_in_progress", - return_value=False, - ), patch( - "homeassistant.config.async_check_ha_config_file", return_value=None - ) as mock_async_check_ha_config_file: + with ( + patch( + "homeassistant.helpers.recorder.async_migration_in_progress", + return_value=False, + ), + patch( + "homeassistant.config.async_check_ha_config_file", return_value=None + ) as mock_async_check_ha_config_file, + ): await hass.services.async_call( "homeassistant", SERVICE_HOMEASSISTANT_RESTART, @@ -551,13 +564,15 @@ async def test_restart_homeassistant( ) -> None: """Test we can restart when there is no configuration error.""" await async_setup_component(hass, "homeassistant", {}) - with patch( - "homeassistant.config.async_check_ha_config_file", return_value=None - ) as mock_check, patch( - "homeassistant.config.async_enable_safe_mode" - ) as mock_safe_mode, patch( - "homeassistant.core.HomeAssistant.async_stop", return_value=None - ) as mock_restart: + with ( + patch( + "homeassistant.config.async_check_ha_config_file", return_value=None + ) as mock_check, + patch("homeassistant.config.async_enable_safe_mode") as mock_safe_mode, + patch( + "homeassistant.core.HomeAssistant.async_stop", return_value=None + ) as mock_restart, + ): await hass.services.async_call( "homeassistant", SERVICE_HOMEASSISTANT_RESTART, @@ -573,11 +588,14 @@ async def test_restart_homeassistant( async def test_stop_homeassistant(hass: HomeAssistant) -> None: """Test we can stop when there is a configuration error.""" await async_setup_component(hass, "homeassistant", {}) - with patch( - "homeassistant.config.async_check_ha_config_file", return_value=None - ) as mock_check, patch( - "homeassistant.core.HomeAssistant.async_stop", return_value=None - ) as mock_restart: + with ( + patch( + "homeassistant.config.async_check_ha_config_file", return_value=None + ) as mock_check, + patch( + "homeassistant.core.HomeAssistant.async_stop", return_value=None + ) as mock_restart, + ): await hass.services.async_call( "homeassistant", SERVICE_HOMEASSISTANT_STOP, @@ -649,16 +667,19 @@ async def test_reload_all( assert len(core_config) == 1 assert len(themes) == 1 - with pytest.raises( - HomeAssistantError, - match=( - "Cannot quick reload all YAML configurations because the configuration is " - "not valid: Oh no, drama!" + with ( + pytest.raises( + HomeAssistantError, + match=( + "Cannot quick reload all YAML configurations because the configuration is " + "not valid: Oh no, drama!" + ), ), - ), patch( - "homeassistant.config.async_check_ha_config_file", - return_value="Oh no, drama!", - ) as mock_async_check_ha_config_file: + patch( + "homeassistant.config.async_check_ha_config_file", + return_value="Oh no, drama!", + ) as mock_async_check_ha_config_file, + ): await hass.services.async_call( "homeassistant", SERVICE_RELOAD_ALL, diff --git a/tests/components/homeassistant/test_scene.py b/tests/components/homeassistant/test_scene.py index d754c67ad49..a1a532db162 100644 --- a/tests/components/homeassistant/test_scene.py +++ b/tests/components/homeassistant/test_scene.py @@ -1,4 +1,5 @@ """Test Home Assistant scenes.""" + from unittest.mock import patch import pytest @@ -17,6 +18,7 @@ from tests.common import async_capture_events, async_mock_service async def test_reload_config_service(hass: HomeAssistant) -> None: """Test the reload config service.""" assert await async_setup_component(hass, "scene", {}) + await hass.async_block_till_done() test_reloaded_event = async_capture_events(hass, EVENT_SCENE_RELOADED) @@ -174,6 +176,7 @@ async def test_delete_service( "scene", {"scene": {"name": "hallo_2", "entities": {"light.kitchen": "on"}}}, ) + await hass.async_block_till_done() await hass.services.async_call( "scene", diff --git a/tests/components/homeassistant/triggers/test_event.py b/tests/components/homeassistant/triggers/test_event.py index d996cd74da7..a0c1f6cb45d 100644 --- a/tests/components/homeassistant/triggers/test_event.py +++ b/tests/components/homeassistant/triggers/test_event.py @@ -1,4 +1,5 @@ """The tests for the Event automation.""" + import pytest import homeassistant.components.automation as automation @@ -504,3 +505,66 @@ async def test_event_data_with_list(hass: HomeAssistant, calls) -> None: hass.bus.async_fire("test_event", {"some_attr": [1, 2, 3]}) await hass.async_block_till_done() assert len(calls) == 1 + + +@pytest.mark.parametrize( + "event_type", ["state_reported", ["test_event", "state_reported"]] +) +async def test_state_reported_event( + hass: HomeAssistant, calls, caplog, event_type: list[str] +) -> None: + """Test triggering on state reported event.""" + context = Context() + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "trigger": {"platform": "event", "event_type": event_type}, + "action": { + "service": "test.automation", + "data_template": {"id": "{{ trigger.id}}"}, + }, + } + }, + ) + + hass.bus.async_fire("test_event", context=context) + await hass.async_block_till_done() + assert len(calls) == 0 + assert ( + "Unnamed automation failed to setup triggers and has been disabled: Can't " + "listen to state_reported in event trigger for dictionary value @ " + "data['event_type']. Got None" in caplog.text + ) + + +async def test_templated_state_reported_event( + hass: HomeAssistant, calls, caplog +) -> None: + """Test triggering on state reported event.""" + context = Context() + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "trigger_variables": {"event_type": "state_reported"}, + "trigger": {"platform": "event", "event_type": "{{event_type}}"}, + "action": { + "service": "test.automation", + "data_template": {"id": "{{ trigger.id}}"}, + }, + } + }, + ) + + hass.bus.async_fire("test_event", context=context) + await hass.async_block_till_done() + assert len(calls) == 0 + assert ( + "Got error 'Can't listen to state_reported in event trigger' " + "when setting up triggers for automation 0" in caplog.text + ) diff --git a/tests/components/homeassistant/triggers/test_homeassistant.py b/tests/components/homeassistant/triggers/test_homeassistant.py index 9a202bc99a1..ebe90415018 100644 --- a/tests/components/homeassistant/triggers/test_homeassistant.py +++ b/tests/components/homeassistant/triggers/test_homeassistant.py @@ -1,4 +1,5 @@ """The tests for the Event automation.""" + from unittest.mock import patch import pytest diff --git a/tests/components/homeassistant/triggers/test_numeric_state.py b/tests/components/homeassistant/triggers/test_numeric_state.py index 92c8aac3eba..65c2863d0d7 100644 --- a/tests/components/homeassistant/triggers/test_numeric_state.py +++ b/tests/components/homeassistant/triggers/test_numeric_state.py @@ -1,4 +1,5 @@ """The tests for numeric state automation.""" + from datetime import timedelta import logging from unittest.mock import patch @@ -59,7 +60,7 @@ async def setup_comp(hass): @pytest.mark.parametrize( - "below", (10, "input_number.value_10", "number.value_10", "sensor.value_10") + "below", [10, "input_number.value_10", "number.value_10", "sensor.value_10"] ) async def test_if_not_fires_on_entity_removal( hass: HomeAssistant, calls, below @@ -89,7 +90,7 @@ async def test_if_not_fires_on_entity_removal( @pytest.mark.parametrize( - "below", (10, "input_number.value_10", "number.value_10", "sensor.value_10") + "below", [10, "input_number.value_10", "number.value_10", "sensor.value_10"] ) async def test_if_fires_on_entity_change_below( hass: HomeAssistant, calls, below @@ -138,7 +139,7 @@ async def test_if_fires_on_entity_change_below( @pytest.mark.parametrize( - "below", (10, "input_number.value_10", "number.value_10", "sensor.value_10") + "below", [10, "input_number.value_10", "number.value_10", "sensor.value_10"] ) async def test_if_fires_on_entity_change_below_uuid( hass: HomeAssistant, entity_registry: er.EntityRegistry, calls, below @@ -192,7 +193,7 @@ async def test_if_fires_on_entity_change_below_uuid( @pytest.mark.parametrize( - "below", (10, "input_number.value_10", "number.value_10", "sensor.value_10") + "below", [10, "input_number.value_10", "number.value_10", "sensor.value_10"] ) async def test_if_fires_on_entity_change_over_to_below( hass: HomeAssistant, calls, below @@ -223,7 +224,7 @@ async def test_if_fires_on_entity_change_over_to_below( @pytest.mark.parametrize( - "below", (10, "input_number.value_10", "number.value_10", "sensor.value_10") + "below", [10, "input_number.value_10", "number.value_10", "sensor.value_10"] ) async def test_if_fires_on_entities_change_over_to_below( hass: HomeAssistant, calls, below @@ -258,7 +259,7 @@ async def test_if_fires_on_entities_change_over_to_below( @pytest.mark.parametrize( - "below", (10, "input_number.value_10", "number.value_10", "sensor.value_10") + "below", [10, "input_number.value_10", "number.value_10", "sensor.value_10"] ) async def test_if_not_fires_on_entity_change_below_to_below( hass: HomeAssistant, calls, below @@ -301,7 +302,7 @@ async def test_if_not_fires_on_entity_change_below_to_below( @pytest.mark.parametrize( - "below", (10, "input_number.value_10", "number.value_10", "sensor.value_10") + "below", [10, "input_number.value_10", "number.value_10", "sensor.value_10"] ) async def test_if_not_below_fires_on_entity_change_to_equal( hass: HomeAssistant, calls, below @@ -332,7 +333,7 @@ async def test_if_not_below_fires_on_entity_change_to_equal( @pytest.mark.parametrize( - "below", (10, "input_number.value_10", "number.value_10", "sensor.value_10") + "below", [10, "input_number.value_10", "number.value_10", "sensor.value_10"] ) async def test_if_not_fires_on_initial_entity_below( hass: HomeAssistant, calls, below @@ -363,7 +364,7 @@ async def test_if_not_fires_on_initial_entity_below( @pytest.mark.parametrize( - "above", (10, "input_number.value_10", "number.value_10", "sensor.value_10") + "above", [10, "input_number.value_10", "number.value_10", "sensor.value_10"] ) async def test_if_not_fires_on_initial_entity_above( hass: HomeAssistant, calls, above @@ -394,7 +395,7 @@ async def test_if_not_fires_on_initial_entity_above( @pytest.mark.parametrize( - "above", (10, "input_number.value_10", "number.value_10", "sensor.value_10") + "above", [10, "input_number.value_10", "number.value_10", "sensor.value_10"] ) async def test_if_fires_on_entity_change_above( hass: HomeAssistant, calls, above @@ -447,7 +448,7 @@ async def test_if_fires_on_entity_unavailable_at_startup( assert len(calls) == 0 -@pytest.mark.parametrize("above", (10, "input_number.value_10")) +@pytest.mark.parametrize("above", [10, "input_number.value_10"]) async def test_if_fires_on_entity_change_below_to_above( hass: HomeAssistant, calls, above ) -> None: @@ -477,7 +478,7 @@ async def test_if_fires_on_entity_change_below_to_above( assert len(calls) == 1 -@pytest.mark.parametrize("above", (10, "input_number.value_10")) +@pytest.mark.parametrize("above", [10, "input_number.value_10"]) async def test_if_not_fires_on_entity_change_above_to_above( hass: HomeAssistant, calls, above ) -> None: @@ -512,7 +513,7 @@ async def test_if_not_fires_on_entity_change_above_to_above( assert len(calls) == 1 -@pytest.mark.parametrize("above", (10, "input_number.value_10")) +@pytest.mark.parametrize("above", [10, "input_number.value_10"]) async def test_if_not_above_fires_on_entity_change_to_equal( hass: HomeAssistant, calls, above ) -> None: @@ -544,12 +545,12 @@ async def test_if_not_above_fires_on_entity_change_to_equal( @pytest.mark.parametrize( ("above", "below"), - ( + [ (5, 10), (5, "input_number.value_10"), ("input_number.value_5", 10), ("input_number.value_5", "input_number.value_10"), - ), + ], ) async def test_if_fires_on_entity_change_below_range( hass: HomeAssistant, calls, above, below @@ -581,12 +582,12 @@ async def test_if_fires_on_entity_change_below_range( @pytest.mark.parametrize( ("above", "below"), - ( + [ (5, 10), (5, "input_number.value_10"), ("input_number.value_5", 10), ("input_number.value_5", "input_number.value_10"), - ), + ], ) async def test_if_fires_on_entity_change_below_above_range( hass: HomeAssistant, calls, above, below @@ -615,12 +616,12 @@ async def test_if_fires_on_entity_change_below_above_range( @pytest.mark.parametrize( ("above", "below"), - ( + [ (5, 10), (5, "input_number.value_10"), ("input_number.value_5", 10), ("input_number.value_5", "input_number.value_10"), - ), + ], ) async def test_if_fires_on_entity_change_over_to_below_range( hass: HomeAssistant, calls, above, below @@ -653,12 +654,12 @@ async def test_if_fires_on_entity_change_over_to_below_range( @pytest.mark.parametrize( ("above", "below"), - ( + [ (5, 10), (5, "input_number.value_10"), ("input_number.value_5", 10), ("input_number.value_5", "input_number.value_10"), - ), + ], ) async def test_if_fires_on_entity_change_over_to_below_above_range( hass: HomeAssistant, calls, above, below @@ -689,7 +690,7 @@ async def test_if_fires_on_entity_change_over_to_below_above_range( assert len(calls) == 0 -@pytest.mark.parametrize("below", (100, "input_number.value_100")) +@pytest.mark.parametrize("below", [100, "input_number.value_100"]) async def test_if_not_fires_if_entity_not_match( hass: HomeAssistant, calls, below ) -> None: @@ -744,7 +745,7 @@ async def test_if_not_fires_and_warns_if_below_entity_unknown( assert caplog.record_tuples[0][1] == logging.WARNING -@pytest.mark.parametrize("below", (10, "input_number.value_10")) +@pytest.mark.parametrize("below", [10, "input_number.value_10"]) async def test_if_fires_on_entity_change_below_with_attribute( hass: HomeAssistant, calls, below ) -> None: @@ -772,7 +773,7 @@ async def test_if_fires_on_entity_change_below_with_attribute( assert len(calls) == 1 -@pytest.mark.parametrize("below", (10, "input_number.value_10")) +@pytest.mark.parametrize("below", [10, "input_number.value_10"]) async def test_if_not_fires_on_entity_change_not_below_with_attribute( hass: HomeAssistant, calls, below ) -> None: @@ -797,7 +798,7 @@ async def test_if_not_fires_on_entity_change_not_below_with_attribute( assert len(calls) == 0 -@pytest.mark.parametrize("below", (10, "input_number.value_10")) +@pytest.mark.parametrize("below", [10, "input_number.value_10"]) async def test_if_fires_on_attribute_change_with_attribute_below( hass: HomeAssistant, calls, below ) -> None: @@ -826,7 +827,7 @@ async def test_if_fires_on_attribute_change_with_attribute_below( assert len(calls) == 1 -@pytest.mark.parametrize("below", (10, "input_number.value_10")) +@pytest.mark.parametrize("below", [10, "input_number.value_10"]) async def test_if_not_fires_on_attribute_change_with_attribute_not_below( hass: HomeAssistant, calls, below ) -> None: @@ -852,7 +853,7 @@ async def test_if_not_fires_on_attribute_change_with_attribute_not_below( assert len(calls) == 0 -@pytest.mark.parametrize("below", (10, "input_number.value_10")) +@pytest.mark.parametrize("below", [10, "input_number.value_10"]) async def test_if_not_fires_on_entity_change_with_attribute_below( hass: HomeAssistant, calls, below ) -> None: @@ -878,7 +879,7 @@ async def test_if_not_fires_on_entity_change_with_attribute_below( assert len(calls) == 0 -@pytest.mark.parametrize("below", (10, "input_number.value_10")) +@pytest.mark.parametrize("below", [10, "input_number.value_10"]) async def test_if_not_fires_on_entity_change_with_not_attribute_below( hass: HomeAssistant, calls, below ) -> None: @@ -904,7 +905,7 @@ async def test_if_not_fires_on_entity_change_with_not_attribute_below( assert len(calls) == 0 -@pytest.mark.parametrize("below", (10, "input_number.value_10")) +@pytest.mark.parametrize("below", [10, "input_number.value_10"]) async def test_fires_on_attr_change_with_attribute_below_and_multiple_attr( hass: HomeAssistant, calls, below ) -> None: @@ -936,7 +937,7 @@ async def test_fires_on_attr_change_with_attribute_below_and_multiple_attr( assert len(calls) == 1 -@pytest.mark.parametrize("below", (10, "input_number.value_10")) +@pytest.mark.parametrize("below", [10, "input_number.value_10"]) async def test_template_list(hass: HomeAssistant, calls, below) -> None: """Test template list.""" hass.states.async_set("test.entity", "entity", {"test_attribute": [11, 15, 11]}) @@ -962,7 +963,7 @@ async def test_template_list(hass: HomeAssistant, calls, below) -> None: assert len(calls) == 1 -@pytest.mark.parametrize("below", (10.0, "input_number.value_10")) +@pytest.mark.parametrize("below", [10.0, "input_number.value_10"]) async def test_template_string(hass: HomeAssistant, calls, below) -> None: """Test template string.""" assert await async_setup_component( @@ -1035,12 +1036,12 @@ async def test_not_fires_on_attr_change_with_attr_not_below_multiple_attr( @pytest.mark.parametrize( ("above", "below"), - ( + [ (8, 12), (8, "input_number.value_12"), ("input_number.value_8", 12), ("input_number.value_8", "input_number.value_12"), - ), + ], ) async def test_if_action(hass: HomeAssistant, calls, above, below) -> None: """Test if action.""" @@ -1083,12 +1084,12 @@ async def test_if_action(hass: HomeAssistant, calls, above, below) -> None: @pytest.mark.parametrize( ("above", "below"), - ( + [ (8, 12), (8, "input_number.value_12"), ("input_number.value_8", 12), ("input_number.value_8", "input_number.value_12"), - ), + ], ) async def test_if_fails_setup_bad_for(hass: HomeAssistant, calls, above, below) -> None: """Test for setup failure for bad for.""" @@ -1139,12 +1140,12 @@ async def test_if_fails_setup_for_without_above_below( @pytest.mark.parametrize( ("above", "below"), - ( + [ (8, 12), (8, "input_number.value_12"), ("input_number.value_8", 12), ("input_number.value_8", "input_number.value_12"), - ), + ], ) async def test_if_not_fires_on_entity_change_with_for( hass: HomeAssistant, freezer: FrozenDateTimeFactory, calls, above, below @@ -1179,12 +1180,12 @@ async def test_if_not_fires_on_entity_change_with_for( @pytest.mark.parametrize( ("above", "below"), - ( + [ (8, 12), (8, "input_number.value_12"), ("input_number.value_8", 12), ("input_number.value_8", "input_number.value_12"), - ), + ], ) async def test_if_not_fires_on_entities_change_with_for_after_stop( hass: HomeAssistant, calls, above, below @@ -1206,7 +1207,10 @@ async def test_if_not_fires_on_entities_change_with_for_after_stop( "below": below, "for": {"seconds": 5}, }, - "action": {"service": "test.automation"}, + "action": [ + {"delay": "0.0001"}, + {"service": "test.automation"}, + ], } }, ) @@ -1237,12 +1241,12 @@ async def test_if_not_fires_on_entities_change_with_for_after_stop( @pytest.mark.parametrize( ("above", "below"), - ( + [ (8, 12), (8, "input_number.value_12"), ("input_number.value_8", 12), ("input_number.value_8", "input_number.value_12"), - ), + ], ) async def test_if_fires_on_entity_change_with_for_attribute_change( hass: HomeAssistant, freezer: FrozenDateTimeFactory, calls, above, below @@ -1283,12 +1287,12 @@ async def test_if_fires_on_entity_change_with_for_attribute_change( @pytest.mark.parametrize( ("above", "below"), - ( + [ (8, 12), (8, "input_number.value_12"), ("input_number.value_8", 12), ("input_number.value_8", "input_number.value_12"), - ), + ], ) async def test_if_fires_on_entity_change_with_for( hass: HomeAssistant, calls, above, below @@ -1321,7 +1325,7 @@ async def test_if_fires_on_entity_change_with_for( assert len(calls) == 1 -@pytest.mark.parametrize("above", (10, "input_number.value_10")) +@pytest.mark.parametrize("above", [10, "input_number.value_10"]) async def test_wait_template_with_trigger(hass: HomeAssistant, calls, above) -> None: """Test using wait template with 'trigger.entity_id'.""" hass.states.async_set("test.entity", "0") @@ -1364,12 +1368,12 @@ async def test_wait_template_with_trigger(hass: HomeAssistant, calls, above) -> @pytest.mark.parametrize( ("above", "below"), - ( + [ (8, 12), (8, "input_number.value_12"), ("input_number.value_8", 12), ("input_number.value_8", "input_number.value_12"), - ), + ], ) async def test_if_fires_on_entities_change_no_overlap( hass: HomeAssistant, freezer: FrozenDateTimeFactory, calls, above, below @@ -1419,12 +1423,12 @@ async def test_if_fires_on_entities_change_no_overlap( @pytest.mark.parametrize( ("above", "below"), - ( + [ (8, 12), (8, "input_number.value_12"), ("input_number.value_8", 12), ("input_number.value_8", "input_number.value_12"), - ), + ], ) async def test_if_fires_on_entities_change_overlap( hass: HomeAssistant, freezer: FrozenDateTimeFactory, calls, above, below @@ -1485,12 +1489,12 @@ async def test_if_fires_on_entities_change_overlap( @pytest.mark.parametrize( ("above", "below"), - ( + [ (8, 12), (8, "input_number.value_12"), ("input_number.value_8", 12), ("input_number.value_8", "input_number.value_12"), - ), + ], ) async def test_if_fires_on_change_with_for_template_1( hass: HomeAssistant, calls, above, below @@ -1526,12 +1530,12 @@ async def test_if_fires_on_change_with_for_template_1( @pytest.mark.parametrize( ("above", "below"), - ( + [ (8, 12), (8, "input_number.value_12"), ("input_number.value_8", 12), ("input_number.value_8", "input_number.value_12"), - ), + ], ) async def test_if_fires_on_change_with_for_template_2( hass: HomeAssistant, calls, above, below @@ -1567,12 +1571,12 @@ async def test_if_fires_on_change_with_for_template_2( @pytest.mark.parametrize( ("above", "below"), - ( + [ (8, 12), (8, "input_number.value_12"), ("input_number.value_8", 12), ("input_number.value_8", "input_number.value_12"), - ), + ], ) async def test_if_fires_on_change_with_for_template_3( hass: HomeAssistant, calls, above, below @@ -1646,12 +1650,12 @@ async def test_if_not_fires_on_error_with_for_template( @pytest.mark.parametrize( ("above", "below"), - ( + [ (8, 12), (8, "input_number.value_12"), ("input_number.value_8", 12), ("input_number.value_8", "input_number.value_12"), - ), + ], ) async def test_invalid_for_template(hass: HomeAssistant, calls, above, below) -> None: """Test for invalid for template.""" @@ -1683,12 +1687,12 @@ async def test_invalid_for_template(hass: HomeAssistant, calls, above, below) -> @pytest.mark.parametrize( ("above", "below"), - ( + [ (8, 12), (8, "input_number.value_12"), ("input_number.value_8", 12), ("input_number.value_8", "input_number.value_12"), - ), + ], ) async def test_if_fires_on_entities_change_overlap_for_template( hass: HomeAssistant, freezer: FrozenDateTimeFactory, calls, above, below @@ -1784,7 +1788,7 @@ async def test_schema_unacceptable_entities(hass: HomeAssistant) -> None: ) -@pytest.mark.parametrize("above", (3, "input_number.value_3")) +@pytest.mark.parametrize("above", [3, "input_number.value_3"]) async def test_attribute_if_fires_on_entity_change_with_both_filters( hass: HomeAssistant, calls, above ) -> None: @@ -1813,7 +1817,7 @@ async def test_attribute_if_fires_on_entity_change_with_both_filters( assert len(calls) == 1 -@pytest.mark.parametrize("above", (3, "input_number.value_3")) +@pytest.mark.parametrize("above", [3, "input_number.value_3"]) async def test_attribute_if_not_fires_on_entities_change_with_for_after_stop( hass: HomeAssistant, calls, above ) -> None: @@ -1832,7 +1836,10 @@ async def test_attribute_if_not_fires_on_entities_change_with_for_after_stop( "attribute": "test-measurement", "for": 5, }, - "action": {"service": "test.automation"}, + "action": [ + {"delay": "0.0001"}, + {"service": "test.automation"}, + ], } }, ) @@ -1848,7 +1855,7 @@ async def test_attribute_if_not_fires_on_entities_change_with_for_after_stop( @pytest.mark.parametrize( ("above", "below"), - ((8, 12),), + [(8, 12)], ) async def test_variables_priority( hass: HomeAssistant, freezer: FrozenDateTimeFactory, calls, above, below @@ -1905,7 +1912,7 @@ async def test_variables_priority( assert calls[0].data["some"] == "test.entity_1 - 0:00:05" -@pytest.mark.parametrize("multiplier", (1, 5)) +@pytest.mark.parametrize("multiplier", [1, 5]) async def test_template_variable(hass: HomeAssistant, calls, multiplier) -> None: """Test template variable.""" hass.states.async_set("test.entity", "entity", {"test_attribute": [11, 15, 11]}) diff --git a/tests/components/homeassistant/triggers/test_state.py b/tests/components/homeassistant/triggers/test_state.py index a8f001ff5e0..9d1d60031e0 100644 --- a/tests/components/homeassistant/triggers/test_state.py +++ b/tests/components/homeassistant/triggers/test_state.py @@ -1,4 +1,5 @@ """The test for state automation.""" + from datetime import timedelta from unittest.mock import patch @@ -665,7 +666,10 @@ async def test_if_not_fires_on_entities_change_with_for_after_stop( "to": "world", "for": {"seconds": 5}, }, - "action": {"service": "test.automation"}, + "action": [ + {"delay": "0.0001"}, + {"service": "test.automation"}, + ], } }, ) @@ -1623,7 +1627,10 @@ async def test_attribute_if_not_fires_on_entities_change_with_for_after_stop( "attribute": "name", "for": 5, }, - "action": {"service": "test.automation"}, + "action": [ + {"delay": "0.0001"}, + {"service": "test.automation"}, + ], } }, ) diff --git a/tests/components/homeassistant/triggers/test_time.py b/tests/components/homeassistant/triggers/test_time.py index ab5eb383f96..340b2839ab1 100644 --- a/tests/components/homeassistant/triggers/test_time.py +++ b/tests/components/homeassistant/triggers/test_time.py @@ -1,4 +1,5 @@ """The tests for the time automation.""" + from datetime import timedelta from unittest.mock import Mock, patch diff --git a/tests/components/homeassistant/triggers/test_time_pattern.py b/tests/components/homeassistant/triggers/test_time_pattern.py index e505dd4f3f5..2d814813ed4 100644 --- a/tests/components/homeassistant/triggers/test_time_pattern.py +++ b/tests/components/homeassistant/triggers/test_time_pattern.py @@ -1,4 +1,5 @@ """The tests for the time_pattern automation.""" + from datetime import timedelta from freezegun.api import FrozenDateTimeFactory diff --git a/tests/components/homeassistant_alerts/test_init.py b/tests/components/homeassistant_alerts/test_init.py index c772c088505..761eb5dec13 100644 --- a/tests/components/homeassistant_alerts/test_init.py +++ b/tests/components/homeassistant_alerts/test_init.py @@ -1,4 +1,5 @@ """Test creating repairs from alerts.""" + from __future__ import annotations from datetime import timedelta @@ -41,7 +42,7 @@ async def setup_repairs(hass): @pytest.mark.parametrize( ("ha_version", "supervisor_info", "expected_alerts"), - ( + [ ( "2022.7.0", {"version": "2022.11.0"}, @@ -92,7 +93,7 @@ async def setup_repairs(hass): ("sochain", "sochain"), ], ), - ), + ], ) async def test_alerts( hass: HomeAssistant, @@ -131,15 +132,19 @@ async def test_alerts( if supervisor_info is not None: hass.config.components.add("hassio") - with patch( - "homeassistant.components.homeassistant_alerts.__version__", - ha_version, - ), patch( - "homeassistant.components.homeassistant_alerts.is_hassio", - return_value=supervisor_info is not None, - ), patch( - "homeassistant.components.homeassistant_alerts.get_supervisor_info", - return_value=supervisor_info, + with ( + patch( + "homeassistant.components.homeassistant_alerts.__version__", + ha_version, + ), + patch( + "homeassistant.components.homeassistant_alerts.is_hassio", + return_value=supervisor_info is not None, + ), + patch( + "homeassistant.components.homeassistant_alerts.get_supervisor_info", + return_value=supervisor_info, + ), ): assert await async_setup_component(hass, DOMAIN, {}) @@ -181,7 +186,7 @@ async def test_alerts( "initial_alerts", "late_alerts", ), - ( + [ ( "2022.7.0", {"version": "2022.11.0"}, @@ -281,7 +286,7 @@ async def test_alerts( ("sochain", "sochain"), ], ), - ), + ], ) async def test_alerts_refreshed_on_component_load( hass: HomeAssistant, @@ -310,55 +315,63 @@ async def test_alerts_refreshed_on_component_load( for domain in initial_components: hass.config.components.add(domain) - with patch( - "homeassistant.components.homeassistant_alerts.__version__", - ha_version, - ), patch( - "homeassistant.components.homeassistant_alerts.is_hassio", - return_value=supervisor_info is not None, - ), patch( - "homeassistant.components.homeassistant_alerts.get_supervisor_info", - return_value=supervisor_info, + with ( + patch( + "homeassistant.components.homeassistant_alerts.__version__", + ha_version, + ), + patch( + "homeassistant.components.homeassistant_alerts.is_hassio", + return_value=supervisor_info is not None, + ), + patch( + "homeassistant.components.homeassistant_alerts.get_supervisor_info", + return_value=supervisor_info, + ), ): assert await async_setup_component(hass, DOMAIN, {}) - client = await hass_ws_client(hass) + 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}.markdown_{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 initial_alerts - ] - } + 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}.markdown_{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 initial_alerts + ] + } - with patch( - "homeassistant.components.homeassistant_alerts.__version__", - ha_version, - ), patch( - "homeassistant.components.homeassistant_alerts.is_hassio", - return_value=supervisor_info is not None, - ), patch( - "homeassistant.components.homeassistant_alerts.get_supervisor_info", - return_value=supervisor_info, + with ( + patch( + "homeassistant.components.homeassistant_alerts.__version__", + ha_version, + ), + patch( + "homeassistant.components.homeassistant_alerts.is_hassio", + return_value=supervisor_info is not None, + ), + patch( + "homeassistant.components.homeassistant_alerts.get_supervisor_info", + return_value=supervisor_info, + ), ): # Fake component_loaded events and wait for debounce for domain in late_components: @@ -367,38 +380,38 @@ async def test_alerts_refreshed_on_component_load( freezer.tick(COMPONENT_LOADED_COOLDOWN + 1) await hass.async_block_till_done() - client = await hass_ws_client(hass) + client = await hass_ws_client(hass) - 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": None, - "created": ANY, - "dismissed_version": None, - "domain": "homeassistant_alerts", - "ignored": False, - "is_fixable": False, - "issue_id": f"{alert}.markdown_{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 late_alerts - ] - } + 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": None, + "created": ANY, + "dismissed_version": None, + "domain": "homeassistant_alerts", + "ignored": False, + "is_fixable": False, + "issue_id": f"{alert}.markdown_{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 late_alerts + ] + } @pytest.mark.parametrize( ("ha_version", "fixture", "expected_alerts"), - ( + [ ( "2022.7.0", "alerts_no_integrations.json", @@ -414,7 +427,7 @@ async def test_alerts_refreshed_on_component_load( ("hikvision", "hikvision"), ], ), - ), + ], ) async def test_bad_alerts( hass: HomeAssistant, @@ -502,7 +515,7 @@ async def test_no_alerts( @pytest.mark.parametrize( ("ha_version", "fixture_1", "expected_alerts_1", "fixture_2", "expected_alerts_2"), - ( + [ ( "2022.7.0", "alerts_1.json", @@ -563,7 +576,7 @@ async def test_no_alerts( ("sochain", "sochain"), ], ), - ), + ], ) async def test_alerts_change( hass: HomeAssistant, diff --git a/tests/components/homeassistant_green/test_config_flow.py b/tests/components/homeassistant_green/test_config_flow.py index cfac774f77e..f0bf15aaa53 100644 --- a/tests/components/homeassistant_green/test_config_flow.py +++ b/tests/components/homeassistant_green/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Home Assistant Green config flow.""" + from unittest.mock import patch import pytest diff --git a/tests/components/homeassistant_green/test_hardware.py b/tests/components/homeassistant_green/test_hardware.py index c9f958b882c..ab91514b297 100644 --- a/tests/components/homeassistant_green/test_hardware.py +++ b/tests/components/homeassistant_green/test_hardware.py @@ -1,4 +1,5 @@ """Test the Home Assistant Green hardware platform.""" + from unittest.mock import patch import pytest diff --git a/tests/components/homeassistant_green/test_init.py b/tests/components/homeassistant_green/test_init.py index b183a332f50..0efb449137a 100644 --- a/tests/components/homeassistant_green/test_init.py +++ b/tests/components/homeassistant_green/test_init.py @@ -1,4 +1,5 @@ """Test the Home Assistant Green integration.""" + from unittest.mock import patch from homeassistant.components.hassio import DOMAIN as HASSIO_DOMAIN diff --git a/tests/components/homeassistant_hardware/conftest.py b/tests/components/homeassistant_hardware/conftest.py index 02b468e558e..ae9ee6e1d2e 100644 --- a/tests/components/homeassistant_hardware/conftest.py +++ b/tests/components/homeassistant_hardware/conftest.py @@ -1,4 +1,5 @@ """Test fixtures for the Home Assistant Hardware integration.""" + from collections.abc import Generator from typing import Any from unittest.mock import AsyncMock, MagicMock, patch @@ -20,14 +21,19 @@ def mock_zha_config_flow_setup() -> Generator[None, None, None]: MagicMock() ) - with patch( - "bellows.zigbee.application.ControllerApplication.probe", side_effect=mock_probe - ), patch( - "homeassistant.components.zha.radio_manager.ZhaRadioManager.connect_zigpy_app", - return_value=mock_connect_app, - ), patch( - "homeassistant.components.zha.async_setup_entry", - return_value=True, + with ( + patch( + "bellows.zigbee.application.ControllerApplication.probe", + side_effect=mock_probe, + ), + patch( + "homeassistant.components.zha.radio_manager.ZhaRadioManager.connect_zigpy_app", + return_value=mock_connect_app, + ), + patch( + "homeassistant.components.zha.async_setup_entry", + return_value=True, + ), ): yield diff --git a/tests/components/homeassistant_hardware/test_silabs_multiprotocol_addon.py b/tests/components/homeassistant_hardware/test_silabs_multiprotocol_addon.py index 43fcd69e4db..82b6fd0c092 100644 --- a/tests/components/homeassistant_hardware/test_silabs_multiprotocol_addon.py +++ b/tests/components/homeassistant_hardware/test_silabs_multiprotocol_addon.py @@ -1,4 +1,5 @@ """Test the Home Assistant Hardware silabs multiprotocol addon manager.""" + from __future__ import annotations from collections.abc import Generator @@ -1609,13 +1610,16 @@ async def test_active_plaforms( async def test_check_multi_pan_addon_no_hassio(hass: HomeAssistant) -> None: """Test `check_multi_pan_addon` without hassio.""" - with patch( - "homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.is_hassio", - return_value=False, - ), patch( - "homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.get_multiprotocol_addon_manager", - autospec=True, - ) as mock_get_addon_manager: + with ( + patch( + "homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.is_hassio", + return_value=False, + ), + patch( + "homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.get_multiprotocol_addon_manager", + autospec=True, + ) as mock_get_addon_manager, + ): await silabs_multiprotocol_addon.check_multi_pan_addon(hass) mock_get_addon_manager.assert_not_called() @@ -1718,9 +1722,12 @@ async def test_multi_pan_addon_using_device_not_running( "state": "not_running", } - await silabs_multiprotocol_addon.multi_pan_addon_using_device( - hass, "/dev/ttyAMA1" - ) is False + assert ( + await silabs_multiprotocol_addon.multi_pan_addon_using_device( + hass, "/dev/ttyAMA1" + ) + is False + ) @pytest.mark.parametrize( @@ -1749,6 +1756,9 @@ async def test_multi_pan_addon_using_device( "state": "running", } - await silabs_multiprotocol_addon.multi_pan_addon_using_device( - hass, "/dev/ttyAMA1" - ) is expected_result + assert ( + await silabs_multiprotocol_addon.multi_pan_addon_using_device( + hass, "/dev/ttyAMA1" + ) + is expected_result + ) diff --git a/tests/components/homeassistant_sky_connect/conftest.py b/tests/components/homeassistant_sky_connect/conftest.py index 90dbe5af384..de8576e2a0a 100644 --- a/tests/components/homeassistant_sky_connect/conftest.py +++ b/tests/components/homeassistant_sky_connect/conftest.py @@ -1,4 +1,5 @@ """Test fixtures for the Home Assistant SkyConnect integration.""" + from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch @@ -24,12 +25,15 @@ def mock_zha(): MagicMock() ) - with patch( - "homeassistant.components.zha.radio_manager.ZhaRadioManager.connect_zigpy_app", - return_value=mock_connect_app, - ), patch( - "homeassistant.components.zha.async_setup_entry", - return_value=True, + with ( + patch( + "homeassistant.components.zha.radio_manager.ZhaRadioManager.connect_zigpy_app", + return_value=mock_connect_app, + ), + patch( + "homeassistant.components.zha.async_setup_entry", + return_value=True, + ), ): yield diff --git a/tests/components/homeassistant_sky_connect/test_config_flow.py b/tests/components/homeassistant_sky_connect/test_config_flow.py index 36f0a259b7f..957a407cc0e 100644 --- a/tests/components/homeassistant_sky_connect/test_config_flow.py +++ b/tests/components/homeassistant_sky_connect/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Home Assistant SkyConnect config flow.""" + from collections.abc import Generator import copy from unittest.mock import Mock, patch @@ -18,13 +19,22 @@ from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry, MockModule, mock_integration -USB_DATA = usb.UsbServiceInfo( - device="bla_device", - vid="bla_vid", - pid="bla_pid", - serial_number="bla_serial_number", - manufacturer="bla_manufacturer", - description="bla_description", +USB_DATA_SKY = usb.UsbServiceInfo( + device="/dev/serial/by-id/usb-Nabu_Casa_SkyConnect_v1.0_9e2adbd75b8beb119fe564a0f320645d-if00-port0", + vid="10C4", + pid="EA60", + serial_number="9e2adbd75b8beb119fe564a0f320645d", + manufacturer="Nabu Casa", + description="SkyConnect v1.0", +) + +USB_DATA_ZBT1 = usb.UsbServiceInfo( + device="/dev/serial/by-id/usb-Nabu_Casa_Home_Assistant_Connect_ZBT-1_9e2adbd75b8beb119fe564a0f320645d-if00-port0", + vid="10C4", + pid="EA60", + serial_number="9e2adbd75b8beb119fe564a0f320645d", + manufacturer="Nabu Casa", + description="Home Assistant Connect ZBT-1", ) @@ -37,27 +47,36 @@ def config_flow_handler(hass: HomeAssistant) -> Generator[None, None, None]: yield -async def test_config_flow(hass: HomeAssistant) -> None: - """Test the config flow.""" +@pytest.mark.parametrize( + ("usb_data", "title"), + [ + (USB_DATA_SKY, "Home Assistant SkyConnect"), + (USB_DATA_ZBT1, "Home Assistant Connect ZBT-1"), + ], +) +async def test_config_flow( + usb_data: usb.UsbServiceInfo, title: str, hass: HomeAssistant +) -> None: + """Test the config flow for SkyConnect.""" with patch( "homeassistant.components.homeassistant_sky_connect.async_setup_entry", return_value=True, ) as mock_setup_entry: result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "usb"}, data=USB_DATA + DOMAIN, context={"source": "usb"}, data=usb_data ) expected_data = { - "device": USB_DATA.device, - "vid": USB_DATA.vid, - "pid": USB_DATA.pid, - "serial_number": USB_DATA.serial_number, - "manufacturer": USB_DATA.manufacturer, - "description": USB_DATA.description, + "device": usb_data.device, + "vid": usb_data.vid, + "pid": usb_data.pid, + "serial_number": usb_data.serial_number, + "manufacturer": usb_data.manufacturer, + "description": usb_data.description, } assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["title"] == "Home Assistant SkyConnect" + assert result["title"] == title assert result["data"] == expected_data assert result["options"] == {} assert len(mock_setup_entry.mock_calls) == 1 @@ -65,51 +84,35 @@ async def test_config_flow(hass: HomeAssistant) -> None: config_entry = hass.config_entries.async_entries(DOMAIN)[0] assert config_entry.data == expected_data assert config_entry.options == {} - assert config_entry.title == "Home Assistant SkyConnect" + assert config_entry.title == title assert ( config_entry.unique_id - == f"{USB_DATA.vid}:{USB_DATA.pid}_{USB_DATA.serial_number}_{USB_DATA.manufacturer}_{USB_DATA.description}" + == f"{usb_data.vid}:{usb_data.pid}_{usb_data.serial_number}_{usb_data.manufacturer}_{usb_data.description}" ) -async def test_config_flow_unique_id(hass: HomeAssistant) -> None: - """Test only a single entry is allowed for a dongle.""" - # Setup an existing config entry - config_entry = MockConfigEntry( - data={}, - domain=DOMAIN, - options={}, - title="Home Assistant SkyConnect", - unique_id=f"{USB_DATA.vid}:{USB_DATA.pid}_{USB_DATA.serial_number}_{USB_DATA.manufacturer}_{USB_DATA.description}", - ) - config_entry.add_to_hass(hass) - - with patch( - "homeassistant.components.homeassistant_sky_connect.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "usb"}, data=USB_DATA - ) - - assert result["type"] == FlowResultType.ABORT - assert result["reason"] == "already_configured" - mock_setup_entry.assert_not_called() - - -async def test_config_flow_multiple_entries(hass: HomeAssistant) -> None: +@pytest.mark.parametrize( + ("usb_data", "title"), + [ + (USB_DATA_SKY, "Home Assistant SkyConnect"), + (USB_DATA_ZBT1, "Home Assistant Connect ZBT-1"), + ], +) +async def test_config_flow_multiple_entries( + usb_data: usb.UsbServiceInfo, title: str, hass: HomeAssistant +) -> None: """Test multiple entries are allowed.""" # Setup an existing config entry config_entry = MockConfigEntry( data={}, domain=DOMAIN, options={}, - title="Home Assistant SkyConnect", - unique_id=f"{USB_DATA.vid}:{USB_DATA.pid}_{USB_DATA.serial_number}_{USB_DATA.manufacturer}_{USB_DATA.description}", + title=title, + unique_id=f"{usb_data.vid}:{usb_data.pid}_{usb_data.serial_number}_{usb_data.manufacturer}_{usb_data.description}", ) config_entry.add_to_hass(hass) - usb_data = copy.copy(USB_DATA) + usb_data = copy.copy(usb_data) usb_data.serial_number = "bla_serial_number_2" with patch( @@ -123,19 +126,28 @@ async def test_config_flow_multiple_entries(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.CREATE_ENTRY -async def test_config_flow_update_device(hass: HomeAssistant) -> None: +@pytest.mark.parametrize( + ("usb_data", "title"), + [ + (USB_DATA_SKY, "Home Assistant SkyConnect"), + (USB_DATA_ZBT1, "Home Assistant Connect ZBT-1"), + ], +) +async def test_config_flow_update_device( + usb_data: usb.UsbServiceInfo, title: str, hass: HomeAssistant +) -> None: """Test updating device path.""" # Setup an existing config entry config_entry = MockConfigEntry( data={}, domain=DOMAIN, options={}, - title="Home Assistant SkyConnect", - unique_id=f"{USB_DATA.vid}:{USB_DATA.pid}_{USB_DATA.serial_number}_{USB_DATA.manufacturer}_{USB_DATA.description}", + title=title, + unique_id=f"{usb_data.vid}:{usb_data.pid}_{usb_data.serial_number}_{usb_data.manufacturer}_{usb_data.description}", ) config_entry.add_to_hass(hass) - usb_data = copy.copy(USB_DATA) + usb_data = copy.copy(usb_data) usb_data.device = "bla_device_2" with patch( @@ -145,13 +157,16 @@ async def test_config_flow_update_device(hass: HomeAssistant) -> None: assert await hass.config_entries.async_setup(config_entry.entry_id) assert len(mock_setup_entry.mock_calls) == 1 - with patch( - "homeassistant.components.homeassistant_sky_connect.async_setup_entry", - return_value=True, - ) as mock_setup_entry, patch( - "homeassistant.components.homeassistant_sky_connect.async_unload_entry", - wraps=homeassistant_sky_connect.async_unload_entry, - ) as mock_unload_entry: + with ( + patch( + "homeassistant.components.homeassistant_sky_connect.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + patch( + "homeassistant.components.homeassistant_sky_connect.async_unload_entry", + wraps=homeassistant_sky_connect.async_unload_entry, + ) as mock_unload_entry, + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": "usb"}, data=usb_data ) @@ -163,7 +178,16 @@ async def test_config_flow_update_device(hass: HomeAssistant) -> None: assert len(mock_unload_entry.mock_calls) == 1 +@pytest.mark.parametrize( + ("usb_data", "title"), + [ + (USB_DATA_SKY, "Home Assistant SkyConnect"), + (USB_DATA_ZBT1, "Home Assistant ZBT-1"), + ], +) async def test_option_flow_install_multi_pan_addon( + usb_data: usb.UsbServiceInfo, + title: str, hass: HomeAssistant, addon_store_info, addon_info, @@ -178,17 +202,17 @@ async def test_option_flow_install_multi_pan_addon( # Setup the config entry config_entry = MockConfigEntry( data={ - "device": USB_DATA.device, - "vid": USB_DATA.vid, - "pid": USB_DATA.pid, - "serial_number": USB_DATA.serial_number, - "manufacturer": USB_DATA.manufacturer, - "description": USB_DATA.description, + "device": usb_data.device, + "vid": usb_data.vid, + "pid": usb_data.pid, + "serial_number": usb_data.serial_number, + "manufacturer": usb_data.manufacturer, + "description": usb_data.description, }, domain=DOMAIN, options={}, - title="Home Assistant SkyConnect", - unique_id=f"{USB_DATA.vid}:{USB_DATA.pid}_{USB_DATA.serial_number}_{USB_DATA.manufacturer}_{USB_DATA.description}", + title=title, + unique_id=f"{usb_data.vid}:{usb_data.pid}_{usb_data.serial_number}_{usb_data.manufacturer}_{usb_data.description}", ) config_entry.add_to_hass(hass) @@ -222,7 +246,7 @@ async def test_option_flow_install_multi_pan_addon( { "options": { "autoflash_firmware": True, - "device": "bla_device", + "device": usb_data.device, "baudrate": "115200", "flow_control": True, } @@ -250,11 +274,20 @@ def mock_detect_radio_type(radio_type=RadioType.ezsp, ret=True): return detect +@pytest.mark.parametrize( + ("usb_data", "title"), + [ + (USB_DATA_SKY, "Home Assistant SkyConnect"), + (USB_DATA_ZBT1, "Home Assistant Connect ZBT-1"), + ], +) @patch( "homeassistant.components.zha.radio_manager.ZhaRadioManager.detect_radio_type", mock_detect_radio_type(), ) async def test_option_flow_install_multi_pan_addon_zha( + usb_data: usb.UsbServiceInfo, + title: str, hass: HomeAssistant, addon_store_info, addon_info, @@ -269,22 +302,22 @@ async def test_option_flow_install_multi_pan_addon_zha( # Setup the config entry config_entry = MockConfigEntry( data={ - "device": USB_DATA.device, - "vid": USB_DATA.vid, - "pid": USB_DATA.pid, - "serial_number": USB_DATA.serial_number, - "manufacturer": USB_DATA.manufacturer, - "description": USB_DATA.description, + "device": usb_data.device, + "vid": usb_data.vid, + "pid": usb_data.pid, + "serial_number": usb_data.serial_number, + "manufacturer": usb_data.manufacturer, + "description": usb_data.description, }, domain=DOMAIN, options={}, - title="Home Assistant SkyConnect", - unique_id=f"{USB_DATA.vid}:{USB_DATA.pid}_{USB_DATA.serial_number}_{USB_DATA.manufacturer}_{USB_DATA.description}", + title=title, + unique_id=f"{usb_data.vid}:{usb_data.pid}_{usb_data.serial_number}_{usb_data.manufacturer}_{usb_data.description}", ) config_entry.add_to_hass(hass) zha_config_entry = MockConfigEntry( - data={"device": {"path": "bla_device"}, "radio_type": "ezsp"}, + data={"device": {"path": usb_data.device}, "radio_type": "ezsp"}, domain=ZHA_DOMAIN, options={}, title="Yellow", @@ -321,7 +354,7 @@ async def test_option_flow_install_multi_pan_addon_zha( { "options": { "autoflash_firmware": True, - "device": "bla_device", + "device": usb_data.device, "baudrate": "115200", "flow_control": True, } diff --git a/tests/components/homeassistant_sky_connect/test_const.py b/tests/components/homeassistant_sky_connect/test_const.py new file mode 100644 index 00000000000..24a39270061 --- /dev/null +++ b/tests/components/homeassistant_sky_connect/test_const.py @@ -0,0 +1,27 @@ +"""Test the Home Assistant SkyConnect constants.""" + +import pytest + +from homeassistant.components.homeassistant_sky_connect.const import HardwareVariant + + +@pytest.mark.parametrize( + ("usb_product_name", "expected_variant"), + [ + ("SkyConnect v1.0", HardwareVariant.SKYCONNECT), + ("Home Assistant Connect ZBT-1", HardwareVariant.CONNECT_ZBT1), + ], +) +def test_hardware_variant( + usb_product_name: str, expected_variant: HardwareVariant +) -> None: + """Test hardware variant parsing.""" + assert HardwareVariant.from_usb_product_name(usb_product_name) == expected_variant + + +def test_hardware_variant_invalid(): + """Test hardware variant parsing with an invalid product.""" + with pytest.raises( + ValueError, match=r"^Unknown SkyConnect product name: Some other product$" + ): + HardwareVariant.from_usb_product_name("Some other product") diff --git a/tests/components/homeassistant_sky_connect/test_hardware.py b/tests/components/homeassistant_sky_connect/test_hardware.py index ca9a7887040..6b283378045 100644 --- a/tests/components/homeassistant_sky_connect/test_hardware.py +++ b/tests/components/homeassistant_sky_connect/test_hardware.py @@ -1,4 +1,5 @@ """Test the Home Assistant SkyConnect hardware platform.""" + from unittest.mock import patch from homeassistant.components.homeassistant_sky_connect.const import DOMAIN @@ -9,21 +10,21 @@ from tests.common import MockConfigEntry from tests.typing import WebSocketGenerator CONFIG_ENTRY_DATA = { - "device": "bla_device", - "vid": "bla_vid", - "pid": "bla_pid", - "serial_number": "bla_serial_number", - "manufacturer": "bla_manufacturer", - "description": "bla_description", + "device": "/dev/serial/by-id/usb-Nabu_Casa_SkyConnect_v1.0_9e2adbd75b8beb119fe564a0f320645d-if00-port0", + "vid": "10C4", + "pid": "EA60", + "serial_number": "9e2adbd75b8beb119fe564a0f320645d", + "manufacturer": "Nabu Casa", + "description": "SkyConnect v1.0", } CONFIG_ENTRY_DATA_2 = { - "device": "bla_device_2", - "vid": "bla_vid_2", - "pid": "bla_pid_2", - "serial_number": "bla_serial_number_2", - "manufacturer": "bla_manufacturer_2", - "description": "bla_description_2", + "device": "/dev/serial/by-id/usb-Nabu_Casa_Home_Assistant_Connect_ZBT-1_9e2adbd75b8beb119fe564a0f320645d-if00-port0", + "vid": "10C4", + "pid": "EA60", + "serial_number": "9e2adbd75b8beb119fe564a0f320645d", + "manufacturer": "Nabu Casa", + "description": "Home Assistant Connect ZBT-1", } @@ -47,7 +48,7 @@ async def test_hardware_info( data=CONFIG_ENTRY_DATA_2, domain=DOMAIN, options={}, - title="Home Assistant SkyConnect", + title="Home Assistant Connect ZBT-1", unique_id="unique_2", ) config_entry_2.add_to_hass(hass) @@ -71,11 +72,11 @@ async def test_hardware_info( "board": None, "config_entries": [config_entry.entry_id], "dongle": { - "vid": "bla_vid", - "pid": "bla_pid", - "serial_number": "bla_serial_number", - "manufacturer": "bla_manufacturer", - "description": "bla_description", + "vid": "10C4", + "pid": "EA60", + "serial_number": "9e2adbd75b8beb119fe564a0f320645d", + "manufacturer": "Nabu Casa", + "description": "SkyConnect v1.0", }, "name": "Home Assistant SkyConnect", "url": "https://skyconnect.home-assistant.io/documentation/", @@ -84,13 +85,13 @@ async def test_hardware_info( "board": None, "config_entries": [config_entry_2.entry_id], "dongle": { - "vid": "bla_vid_2", - "pid": "bla_pid_2", - "serial_number": "bla_serial_number_2", - "manufacturer": "bla_manufacturer_2", - "description": "bla_description_2", + "vid": "10C4", + "pid": "EA60", + "serial_number": "9e2adbd75b8beb119fe564a0f320645d", + "manufacturer": "Nabu Casa", + "description": "Home Assistant Connect ZBT-1", }, - "name": "Home Assistant SkyConnect", + "name": "Home Assistant Connect ZBT-1", "url": "https://skyconnect.home-assistant.io/documentation/", }, ] diff --git a/tests/components/homeassistant_sky_connect/test_init.py b/tests/components/homeassistant_sky_connect/test_init.py index 11961c09a2d..a1fa4a5c743 100644 --- a/tests/components/homeassistant_sky_connect/test_init.py +++ b/tests/components/homeassistant_sky_connect/test_init.py @@ -1,4 +1,5 @@ """Test the Home Assistant SkyConnect integration.""" + from collections.abc import Generator from typing import Any from unittest.mock import MagicMock, Mock, patch @@ -42,17 +43,21 @@ def mock_zha_config_flow_setup() -> Generator[None, None, None]: mock_connect_app = MagicMock() mock_connect_app.__aenter__.return_value.backups.backups = [] - with patch( - "bellows.zigbee.application.ControllerApplication.probe", side_effect=mock_probe - ), patch( - "homeassistant.components.zha.radio_manager.ZhaRadioManager.connect_zigpy_app", - return_value=mock_connect_app, + with ( + patch( + "bellows.zigbee.application.ControllerApplication.probe", + side_effect=mock_probe, + ), + patch( + "homeassistant.components.zha.radio_manager.ZhaRadioManager.connect_zigpy_app", + return_value=mock_connect_app, + ), ): yield @pytest.mark.parametrize( - ("onboarded", "num_entries", "num_flows"), ((False, 1, 0), (True, 0, 1)) + ("onboarded", "num_entries", "num_flows"), [(False, 1, 0), (True, 0, 1)] ) async def test_setup_entry( mock_zha_config_flow_setup, @@ -74,11 +79,15 @@ async def test_setup_entry( title="Home Assistant SkyConnect", ) config_entry.add_to_hass(hass) - with patch( - "homeassistant.components.homeassistant_sky_connect.usb.async_is_plugged_in", - return_value=True, - ) as mock_is_plugged_in, patch( - "homeassistant.components.onboarding.async_is_onboarded", return_value=onboarded + with ( + patch( + "homeassistant.components.homeassistant_sky_connect.usb.async_is_plugged_in", + return_value=True, + ) as mock_is_plugged_in, + patch( + "homeassistant.components.onboarding.async_is_onboarded", + return_value=onboarded, + ), ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -122,11 +131,14 @@ async def test_setup_zha( title="Home Assistant SkyConnect", ) config_entry.add_to_hass(hass) - with patch( - "homeassistant.components.homeassistant_sky_connect.usb.async_is_plugged_in", - return_value=True, - ) as mock_is_plugged_in, patch( - "homeassistant.components.onboarding.async_is_onboarded", return_value=False + with ( + patch( + "homeassistant.components.homeassistant_sky_connect.usb.async_is_plugged_in", + return_value=True, + ) as mock_is_plugged_in, + patch( + "homeassistant.components.onboarding.async_is_onboarded", return_value=False + ), ): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -173,14 +185,18 @@ async def test_setup_zha_multipan( title="Home Assistant SkyConnect", ) config_entry.add_to_hass(hass) - with patch( - "homeassistant.components.homeassistant_sky_connect.usb.async_is_plugged_in", - return_value=True, - ) as mock_is_plugged_in, patch( - "homeassistant.components.onboarding.async_is_onboarded", return_value=False - ), patch( - "homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.is_hassio", - side_effect=Mock(return_value=True), + with ( + patch( + "homeassistant.components.homeassistant_sky_connect.usb.async_is_plugged_in", + return_value=True, + ) as mock_is_plugged_in, + patch( + "homeassistant.components.onboarding.async_is_onboarded", return_value=False + ), + patch( + "homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.is_hassio", + side_effect=Mock(return_value=True), + ), ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -227,14 +243,18 @@ async def test_setup_zha_multipan_other_device( title="Home Assistant Yellow", ) config_entry.add_to_hass(hass) - with patch( - "homeassistant.components.homeassistant_sky_connect.usb.async_is_plugged_in", - return_value=True, - ) as mock_is_plugged_in, patch( - "homeassistant.components.onboarding.async_is_onboarded", return_value=False - ), patch( - "homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.is_hassio", - side_effect=Mock(return_value=True), + with ( + patch( + "homeassistant.components.homeassistant_sky_connect.usb.async_is_plugged_in", + return_value=True, + ) as mock_is_plugged_in, + patch( + "homeassistant.components.onboarding.async_is_onboarded", return_value=False + ), + patch( + "homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.is_hassio", + side_effect=Mock(return_value=True), + ), ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -305,14 +325,18 @@ async def test_setup_entry_addon_info_fails( title="Home Assistant SkyConnect", ) config_entry.add_to_hass(hass) - with patch( - "homeassistant.components.homeassistant_sky_connect.usb.async_is_plugged_in", - return_value=True, - ), patch( - "homeassistant.components.onboarding.async_is_onboarded", return_value=False - ), patch( - "homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.is_hassio", - side_effect=Mock(return_value=True), + with ( + patch( + "homeassistant.components.homeassistant_sky_connect.usb.async_is_plugged_in", + return_value=True, + ), + patch( + "homeassistant.components.onboarding.async_is_onboarded", return_value=False + ), + patch( + "homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.is_hassio", + side_effect=Mock(return_value=True), + ), ): assert not await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -334,14 +358,18 @@ async def test_setup_entry_addon_not_running( title="Home Assistant SkyConnect", ) config_entry.add_to_hass(hass) - with patch( - "homeassistant.components.homeassistant_sky_connect.usb.async_is_plugged_in", - return_value=True, - ), patch( - "homeassistant.components.onboarding.async_is_onboarded", return_value=False - ), patch( - "homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.is_hassio", - side_effect=Mock(return_value=True), + with ( + patch( + "homeassistant.components.homeassistant_sky_connect.usb.async_is_plugged_in", + return_value=True, + ), + patch( + "homeassistant.components.onboarding.async_is_onboarded", return_value=False + ), + patch( + "homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.is_hassio", + side_effect=Mock(return_value=True), + ), ): assert not await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/homeassistant_yellow/conftest.py b/tests/components/homeassistant_yellow/conftest.py index a7d66d659f0..070047648fc 100644 --- a/tests/components/homeassistant_yellow/conftest.py +++ b/tests/components/homeassistant_yellow/conftest.py @@ -1,4 +1,5 @@ """Test fixtures for the Home Assistant Yellow integration.""" + from collections.abc import Generator from typing import Any from unittest.mock import AsyncMock, MagicMock, patch @@ -20,14 +21,19 @@ def mock_zha_config_flow_setup() -> Generator[None, None, None]: MagicMock() ) - with patch( - "bellows.zigbee.application.ControllerApplication.probe", side_effect=mock_probe - ), patch( - "homeassistant.components.zha.radio_manager.ZhaRadioManager.connect_zigpy_app", - return_value=mock_connect_app, - ), patch( - "homeassistant.components.zha.async_setup_entry", - return_value=True, + with ( + patch( + "bellows.zigbee.application.ControllerApplication.probe", + side_effect=mock_probe, + ), + patch( + "homeassistant.components.zha.radio_manager.ZhaRadioManager.connect_zigpy_app", + return_value=mock_connect_app, + ), + patch( + "homeassistant.components.zha.async_setup_entry", + return_value=True, + ), ): yield diff --git a/tests/components/homeassistant_yellow/test_config_flow.py b/tests/components/homeassistant_yellow/test_config_flow.py index 1b80610953f..821621d5e57 100644 --- a/tests/components/homeassistant_yellow/test_config_flow.py +++ b/tests/components/homeassistant_yellow/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Home Assistant Yellow config flow.""" + from collections.abc import Generator from unittest.mock import Mock, patch diff --git a/tests/components/homeassistant_yellow/test_hardware.py b/tests/components/homeassistant_yellow/test_hardware.py index b7843e75dcf..9d43b341abf 100644 --- a/tests/components/homeassistant_yellow/test_hardware.py +++ b/tests/components/homeassistant_yellow/test_hardware.py @@ -1,4 +1,5 @@ """Test the Home Assistant Yellow hardware platform.""" + from unittest.mock import patch import pytest diff --git a/tests/components/homeassistant_yellow/test_init.py b/tests/components/homeassistant_yellow/test_init.py index f042a7bf54d..e94dbbc1438 100644 --- a/tests/components/homeassistant_yellow/test_init.py +++ b/tests/components/homeassistant_yellow/test_init.py @@ -1,4 +1,5 @@ """Test the Home Assistant Yellow integration.""" + from unittest.mock import patch import pytest @@ -15,7 +16,7 @@ from tests.common import MockConfigEntry, MockModule, mock_integration @pytest.mark.parametrize( - ("onboarded", "num_entries", "num_flows"), ((False, 1, 0), (True, 0, 1)) + ("onboarded", "num_entries", "num_flows"), [(False, 1, 0), (True, 0, 1)] ) async def test_setup_entry( hass: HomeAssistant, onboarded, num_entries, num_flows, addon_store_info @@ -32,11 +33,15 @@ async def test_setup_entry( title="Home Assistant Yellow", ) config_entry.add_to_hass(hass) - with patch( - "homeassistant.components.homeassistant_yellow.get_os_info", - return_value={"board": "yellow"}, - ) as mock_get_os_info, patch( - "homeassistant.components.onboarding.async_is_onboarded", return_value=onboarded + with ( + patch( + "homeassistant.components.homeassistant_yellow.get_os_info", + return_value={"board": "yellow"}, + ) as mock_get_os_info, + patch( + "homeassistant.components.onboarding.async_is_onboarded", + return_value=onboarded, + ), ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -75,11 +80,14 @@ async def test_setup_zha(hass: HomeAssistant, addon_store_info) -> None: title="Home Assistant Yellow", ) config_entry.add_to_hass(hass) - with patch( - "homeassistant.components.homeassistant_yellow.get_os_info", - return_value={"board": "yellow"}, - ) as mock_get_os_info, patch( - "homeassistant.components.onboarding.async_is_onboarded", return_value=False + with ( + patch( + "homeassistant.components.homeassistant_yellow.get_os_info", + return_value={"board": "yellow"}, + ) as mock_get_os_info, + patch( + "homeassistant.components.onboarding.async_is_onboarded", return_value=False + ), ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -126,11 +134,14 @@ async def test_setup_zha_multipan( title="Home Assistant Yellow", ) config_entry.add_to_hass(hass) - with patch( - "homeassistant.components.homeassistant_yellow.get_os_info", - return_value={"board": "yellow"}, - ) as mock_get_os_info, patch( - "homeassistant.components.onboarding.async_is_onboarded", return_value=False + with ( + patch( + "homeassistant.components.homeassistant_yellow.get_os_info", + return_value={"board": "yellow"}, + ) as mock_get_os_info, + patch( + "homeassistant.components.onboarding.async_is_onboarded", return_value=False + ), ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -177,11 +188,14 @@ async def test_setup_zha_multipan_other_device( title="Home Assistant Yellow", ) config_entry.add_to_hass(hass) - with patch( - "homeassistant.components.homeassistant_yellow.get_os_info", - return_value={"board": "yellow"}, - ) as mock_get_os_info, patch( - "homeassistant.components.onboarding.async_is_onboarded", return_value=False + with ( + patch( + "homeassistant.components.homeassistant_yellow.get_os_info", + return_value={"board": "yellow"}, + ) as mock_get_os_info, + patch( + "homeassistant.components.onboarding.async_is_onboarded", return_value=False + ), ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -299,11 +313,14 @@ async def test_setup_entry_addon_info_fails( title="Home Assistant Yellow", ) config_entry.add_to_hass(hass) - with patch( - "homeassistant.components.homeassistant_yellow.get_os_info", - return_value={"board": "yellow"}, - ), patch( - "homeassistant.components.onboarding.async_is_onboarded", return_value=False + with ( + patch( + "homeassistant.components.homeassistant_yellow.get_os_info", + return_value={"board": "yellow"}, + ), + patch( + "homeassistant.components.onboarding.async_is_onboarded", return_value=False + ), ): assert not await hass.config_entries.async_setup(config_entry.entry_id) @@ -326,11 +343,14 @@ async def test_setup_entry_addon_not_running( title="Home Assistant Yellow", ) config_entry.add_to_hass(hass) - with patch( - "homeassistant.components.homeassistant_yellow.get_os_info", - return_value={"board": "yellow"}, - ), patch( - "homeassistant.components.onboarding.async_is_onboarded", return_value=False + with ( + patch( + "homeassistant.components.homeassistant_yellow.get_os_info", + return_value={"board": "yellow"}, + ), + patch( + "homeassistant.components.onboarding.async_is_onboarded", return_value=False + ), ): assert not await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/homekit/conftest.py b/tests/components/homekit/conftest.py index 8c6d4328065..fcbeafa3b60 100644 --- a/tests/components/homekit/conftest.py +++ b/tests/components/homekit/conftest.py @@ -1,4 +1,5 @@ """HomeKit session fixtures.""" + from contextlib import suppress import os from unittest.mock import patch @@ -26,12 +27,14 @@ def run_driver(hass, event_loop, iid_storage): This mock does not mock async_stop, so the driver will not be stopped """ - with patch("pyhap.accessory_driver.AsyncZeroconf"), patch( - "pyhap.accessory_driver.AccessoryEncoder" - ), patch("pyhap.accessory_driver.HAPServer"), patch( - "pyhap.accessory_driver.AccessoryDriver.publish" - ), patch( - "pyhap.accessory_driver.AccessoryDriver.persist", + with ( + patch("pyhap.accessory_driver.AsyncZeroconf"), + patch("pyhap.accessory_driver.AccessoryEncoder"), + patch("pyhap.accessory_driver.HAPServer"), + patch("pyhap.accessory_driver.AccessoryDriver.publish"), + patch( + "pyhap.accessory_driver.AccessoryDriver.persist", + ), ): yield HomeDriver( hass, @@ -48,14 +51,17 @@ def run_driver(hass, event_loop, iid_storage): @pytest.fixture def hk_driver(hass, event_loop, iid_storage): """Return a custom AccessoryDriver instance for HomeKit accessory init.""" - with patch("pyhap.accessory_driver.AsyncZeroconf"), patch( - "pyhap.accessory_driver.AccessoryEncoder" - ), patch("pyhap.accessory_driver.HAPServer.async_stop"), patch( - "pyhap.accessory_driver.HAPServer.async_start" - ), patch( - "pyhap.accessory_driver.AccessoryDriver.publish", - ), patch( - "pyhap.accessory_driver.AccessoryDriver.persist", + with ( + patch("pyhap.accessory_driver.AsyncZeroconf"), + patch("pyhap.accessory_driver.AccessoryEncoder"), + patch("pyhap.accessory_driver.HAPServer.async_stop"), + patch("pyhap.accessory_driver.HAPServer.async_start"), + patch( + "pyhap.accessory_driver.AccessoryDriver.publish", + ), + patch( + "pyhap.accessory_driver.AccessoryDriver.persist", + ), ): yield HomeDriver( hass, @@ -72,18 +78,23 @@ def hk_driver(hass, event_loop, iid_storage): @pytest.fixture def mock_hap(hass, event_loop, iid_storage, mock_zeroconf): """Return a custom AccessoryDriver instance for HomeKit accessory init.""" - with patch("pyhap.accessory_driver.AsyncZeroconf"), patch( - "pyhap.accessory_driver.AccessoryEncoder" - ), patch("pyhap.accessory_driver.HAPServer.async_stop"), patch( - "pyhap.accessory_driver.HAPServer.async_start" - ), patch( - "pyhap.accessory_driver.AccessoryDriver.publish", - ), patch( - "pyhap.accessory_driver.AccessoryDriver.async_start", - ), patch( - "pyhap.accessory_driver.AccessoryDriver.async_stop", - ), patch( - "pyhap.accessory_driver.AccessoryDriver.persist", + with ( + patch("pyhap.accessory_driver.AsyncZeroconf"), + patch("pyhap.accessory_driver.AccessoryEncoder"), + patch("pyhap.accessory_driver.HAPServer.async_stop"), + patch("pyhap.accessory_driver.HAPServer.async_start"), + patch( + "pyhap.accessory_driver.AccessoryDriver.publish", + ), + patch( + "pyhap.accessory_driver.AccessoryDriver.async_start", + ), + patch( + "pyhap.accessory_driver.AccessoryDriver.async_stop", + ), + patch( + "pyhap.accessory_driver.AccessoryDriver.persist", + ), ): yield HomeDriver( hass, diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py index 4b1f315c0b6..11a2675382a 100644 --- a/tests/components/homekit/test_accessories.py +++ b/tests/components/homekit/test_accessories.py @@ -2,6 +2,7 @@ This includes tests for all mock object types. """ + from unittest.mock import Mock, patch import pytest @@ -27,6 +28,7 @@ from homeassistant.components.homekit.const import ( CONF_LINKED_BATTERY_CHARGING_SENSOR, CONF_LINKED_BATTERY_SENSOR, CONF_LOW_BATTERY_THRESHOLD, + EMPTY_MAC, MANUFACTURER, SERV_ACCESSORY_INFO, ) @@ -746,24 +748,32 @@ def test_home_driver(iid_storage) -> None: persist_file=path, ) - mock_driver.assert_called_with(address=ip_address, port=port, persist_file=path) + mock_driver.assert_called_with( + address=ip_address, port=port, persist_file=path, mac=EMPTY_MAC + ) driver.state = Mock(pincode=pin, paired=False) xhm_uri_mock = Mock(return_value="X-HM://0") driver.accessory = Mock(display_name="any", xhm_uri=xhm_uri_mock) # pair - with patch("pyhap.accessory_driver.AccessoryDriver.pair") as mock_pair, patch( - "homeassistant.components.homekit.accessories.async_dismiss_setup_message" - ) as mock_dissmiss_msg: + with ( + patch("pyhap.accessory_driver.AccessoryDriver.pair") as mock_pair, + patch( + "homeassistant.components.homekit.accessories.async_dismiss_setup_message" + ) as mock_dissmiss_msg, + ): driver.pair("client_uuid", "client_public", b"1") mock_pair.assert_called_with("client_uuid", "client_public", b"1") mock_dissmiss_msg.assert_called_with("hass", "entry_id") # unpair - with patch("pyhap.accessory_driver.AccessoryDriver.unpair") as mock_unpair, patch( - "homeassistant.components.homekit.accessories.async_show_setup_message" - ) as mock_show_msg: + with ( + patch("pyhap.accessory_driver.AccessoryDriver.unpair") as mock_unpair, + patch( + "homeassistant.components.homekit.accessories.async_show_setup_message" + ) as mock_show_msg, + ): driver.unpair("client_uuid") mock_unpair.assert_called_with("client_uuid") diff --git a/tests/components/homekit/test_aidmanager.py b/tests/components/homekit/test_aidmanager.py index 64c5cd9cc74..6dbac422f07 100644 --- a/tests/components/homekit/test_aidmanager.py +++ b/tests/components/homekit/test_aidmanager.py @@ -1,4 +1,5 @@ """Tests for the HomeKit AID manager.""" + import os from unittest.mock import patch @@ -48,7 +49,7 @@ async def test_aid_generation( aid_storage = AccessoryAidStorage(hass, config_entry) await aid_storage.async_initialize() - for _ in range(0, 2): + for _ in range(2): assert ( aid_storage.get_or_allocate_aid_for_entity_id(light_ent.entity_id) == 1953095294 @@ -71,7 +72,7 @@ async def test_aid_generation( aid_storage.delete_aid(get_system_unique_id(remote_ent, remote_ent.unique_id)) aid_storage.delete_aid("non-existent-one") - for _ in range(0, 2): + for _ in range(2): assert ( aid_storage.get_or_allocate_aid_for_entity_id(light_ent.entity_id) == 1953095294 @@ -111,7 +112,7 @@ async def test_no_aid_collision( seen_aids = set() - for unique_id in range(0, 202): + for unique_id in range(202): ent = entity_registry.async_get_or_create( "light", "device", unique_id, device_id=device_entry.id ) @@ -140,7 +141,7 @@ async def test_aid_generation_no_unique_ids_handles_collision( seen_aids = set() collisions = [] - for light_id in range(0, 220): + for light_id in range(220): entity_id = f"light.light{light_id}" hass.states.async_set(entity_id, "on") expected_aid = fnv1a_32(entity_id.encode("utf-8")) diff --git a/tests/components/homekit/test_config_flow.py b/tests/components/homekit/test_config_flow.py index 6dff9ef896e..b3b8a70b1a1 100644 --- a/tests/components/homekit/test_config_flow.py +++ b/tests/components/homekit/test_config_flow.py @@ -1,5 +1,6 @@ """Test the HomeKit config flow.""" -from unittest.mock import patch + +from unittest.mock import AsyncMock, Mock, patch import pytest import voluptuous as vol @@ -59,15 +60,19 @@ async def test_setup_in_bridge_mode(hass: HomeAssistant, mock_get_source_ip) -> assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "pairing" - with patch( - "homeassistant.components.homekit.config_flow.async_find_next_available_port", - return_value=12345, - ), patch( - "homeassistant.components.homekit.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.homekit.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.homekit.config_flow.async_find_next_available_port", + return_value=12345, + ), + patch( + "homeassistant.components.homekit.async_setup", return_value=True + ) as mock_setup, + patch( + "homeassistant.components.homekit.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], {}, @@ -117,15 +122,19 @@ async def test_setup_in_bridge_mode_name_taken( assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "pairing" - with patch( - "homeassistant.components.homekit.config_flow.async_find_next_available_port", - return_value=12345, - ), patch( - "homeassistant.components.homekit.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.homekit.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.homekit.config_flow.async_find_next_available_port", + return_value=12345, + ), + patch( + "homeassistant.components.homekit.async_setup", return_value=True + ) as mock_setup, + patch( + "homeassistant.components.homekit.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], {}, @@ -199,15 +208,19 @@ async def test_setup_creates_entries_for_accessory_mode_devices( assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "pairing" - with patch( - "homeassistant.components.homekit.config_flow.async_find_next_available_port", - return_value=12345, - ), patch( - "homeassistant.components.homekit.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.homekit.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.homekit.config_flow.async_find_next_available_port", + return_value=12345, + ), + patch( + "homeassistant.components.homekit.async_setup", return_value=True + ) as mock_setup, + patch( + "homeassistant.components.homekit.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], {}, @@ -262,12 +275,15 @@ async def test_import(hass: HomeAssistant, mock_get_source_ip) -> None: assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "port_name_in_use" - with patch( - "homeassistant.components.homekit.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.homekit.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.homekit.async_setup", return_value=True + ) as mock_setup, + patch( + "homeassistant.components.homekit.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, @@ -427,62 +443,68 @@ async def test_options_flow_devices( demo_config_entry = MockConfigEntry(domain="domain") demo_config_entry.add_to_hass(hass) - assert await async_setup_component(hass, "homeassistant", {}) - assert await async_setup_component(hass, "demo", {"demo": {}}) - assert await async_setup_component(hass, "homekit", {"homekit": {}}) + with patch("homeassistant.components.homekit.HomeKit") as mock_homekit: + mock_homekit.return_value = homekit = Mock() + type(homekit).async_start = AsyncMock() + assert await async_setup_component(hass, "homekit", {"homekit": {}}) + assert await async_setup_component(hass, "homeassistant", {}) + assert await async_setup_component(hass, "demo", {"demo": {}}) + assert await async_setup_component(hass, "homekit", {"homekit": {}}) - hass.states.async_set("climate.old", "off") - await hass.async_block_till_done() + hass.states.async_set("climate.old", "off") + await hass.async_block_till_done() - result = await hass.config_entries.options.async_init( - config_entry.entry_id, context={"show_advanced_options": True} - ) - - 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={ - "domains": ["fan", "vacuum", "climate"], - "include_exclude_mode": "exclude", - }, - ) - - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "exclude" - - entry = entity_registry.async_get("light.ceiling_lights") - assert entry is not None - device_id = entry.device_id - - result2 = await hass.config_entries.options.async_configure( - result["flow_id"], - user_input={ - "entities": ["climate.old"], - }, - ) - - with patch("homeassistant.components.homekit.async_setup_entry", return_value=True): - result3 = await hass.config_entries.options.async_configure( - result2["flow_id"], - user_input={"devices": [device_id]}, + result = await hass.config_entries.options.async_init( + config_entry.entry_id, context={"show_advanced_options": True} ) - assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - assert config_entry.options == { - "devices": [device_id], - "mode": "bridge", - "filter": { - "exclude_domains": [], - "exclude_entities": ["climate.old"], - "include_domains": ["fan", "vacuum", "climate"], - "include_entities": [], - }, - } + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "init" - await hass.async_block_till_done() - await hass.config_entries.async_unload(config_entry.entry_id) + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "domains": ["fan", "vacuum", "climate"], + "include_exclude_mode": "exclude", + }, + ) + + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "exclude" + + entry = entity_registry.async_get("light.ceiling_lights") + assert entry is not None + device_id = entry.device_id + + result2 = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "entities": ["climate.old"], + }, + ) + + with patch( + "homeassistant.components.homekit.async_setup_entry", return_value=True + ): + result3 = await hass.config_entries.options.async_configure( + result2["flow_id"], + user_input={"devices": [device_id]}, + ) + + assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert config_entry.options == { + "devices": [device_id], + "mode": "bridge", + "filter": { + "exclude_domains": [], + "exclude_entities": ["climate.old"], + "include_domains": ["fan", "vacuum", "climate"], + "include_entities": [], + }, + } + + await hass.async_block_till_done() + await hass.config_entries.async_unload(config_entry.entry_id) @patch(f"{PATH_HOMEKIT}.async_port_is_available", return_value=True) @@ -513,49 +535,53 @@ async def test_options_flow_devices_preserved_when_advanced_off( demo_config_entry = MockConfigEntry(domain="domain") demo_config_entry.add_to_hass(hass) - assert await async_setup_component(hass, "homekit", {"homekit": {}}) + with patch("homeassistant.components.homekit.HomeKit") as mock_homekit: + mock_homekit.return_value = homekit = Mock() + type(homekit).async_start = AsyncMock() + assert await async_setup_component(hass, "homekit", {"homekit": {}}) - hass.states.async_set("climate.old", "off") - await hass.async_block_till_done() + hass.states.async_set("climate.old", "off") + await hass.async_block_till_done() - result = await hass.config_entries.options.async_init( - config_entry.entry_id, context={"show_advanced_options": False} - ) + result = await hass.config_entries.options.async_init( + config_entry.entry_id, context={"show_advanced_options": False} + ) - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "init" + 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={ - "domains": ["fan", "vacuum", "climate"], - "include_exclude_mode": "exclude", - }, - ) + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "domains": ["fan", "vacuum", "climate"], + "include_exclude_mode": "exclude", + }, + ) - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "exclude" + 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"], - }, - ) + result2 = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "entities": ["climate.old"], + }, + ) - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - assert config_entry.options == { - "devices": ["1fabcabcabcabcabcabcabcabcabc"], - "mode": "bridge", - "filter": { - "exclude_domains": [], - "exclude_entities": ["climate.old"], - "include_domains": ["fan", "vacuum", "climate"], - "include_entities": [], - }, - } - await hass.async_block_till_done() - await hass.config_entries.async_unload(config_entry.entry_id) + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert config_entry.options == { + "devices": ["1fabcabcabcabcabcabcabcabcabc"], + "mode": "bridge", + "filter": { + "exclude_domains": [], + "exclude_entities": ["climate.old"], + "include_domains": ["fan", "vacuum", "climate"], + "include_entities": [], + }, + } + await hass.async_block_till_done() + await hass.config_entries.async_unload(config_entry.entry_id) + await hass.async_block_till_done() async def test_options_flow_include_mode_with_non_existant_entity( @@ -1275,13 +1301,16 @@ async def test_converting_bridge_to_accessory_mode( # We need to actually setup the config entry or the data # will not get migrated to options - with patch( - "homeassistant.components.homekit.config_flow.async_find_next_available_port", - return_value=12345, - ), patch( - "homeassistant.components.homekit.HomeKit.async_start", - return_value=True, - ) as mock_async_start: + with ( + patch( + "homeassistant.components.homekit.config_flow.async_find_next_available_port", + return_value=12345, + ), + patch( + "homeassistant.components.homekit.HomeKit.async_start", + return_value=True, + ) as mock_async_start, + ): result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], {}, @@ -1337,11 +1366,12 @@ async def test_converting_bridge_to_accessory_mode( assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "cameras" - with patch( - "homeassistant.components.homekit.async_setup_entry", - return_value=True, - ) as mock_setup_entry, patch( - "homeassistant.components.homekit.async_port_is_available" + with ( + patch( + "homeassistant.components.homekit.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + patch("homeassistant.components.homekit.async_port_is_available"), ): result3 = await hass.config_entries.options.async_configure( result2["flow_id"], diff --git a/tests/components/homekit/test_diagnostics.py b/tests/components/homekit/test_diagnostics.py index 2f18c7a5a89..9fe4fc6fcc7 100644 --- a/tests/components/homekit/test_diagnostics.py +++ b/tests/components/homekit/test_diagnostics.py @@ -1,4 +1,5 @@ """Test homekit diagnostics.""" + from unittest.mock import ANY, MagicMock, patch from homeassistant.components.homekit.const import ( @@ -142,9 +143,11 @@ async def test_config_entry_running( "status": 1, } - with patch("pyhap.accessory_driver.AccessoryDriver.async_start"), patch( - "homeassistant.components.homekit.HomeKit.async_stop" - ), patch("homeassistant.components.homekit.async_port_is_available"): + with ( + patch("pyhap.accessory_driver.AccessoryDriver.async_start"), + patch("homeassistant.components.homekit.HomeKit.async_stop"), + patch("homeassistant.components.homekit.async_port_is_available"), + ): assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() @@ -263,6 +266,7 @@ async def test_config_entry_accessory( "context": {"id": ANY, "parent_id": None, "user_id": None}, "entity_id": "light.demo", "last_changed": ANY, + "last_reported": ANY, "last_updated": ANY, "state": "on", }, @@ -301,9 +305,11 @@ async def test_config_entry_accessory( }, "status": 1, } - with patch("pyhap.accessory_driver.AccessoryDriver.async_start"), patch( - "homeassistant.components.homekit.HomeKit.async_stop" - ), patch("homeassistant.components.homekit.async_port_is_available"): + with ( + patch("pyhap.accessory_driver.AccessoryDriver.async_start"), + patch("homeassistant.components.homekit.HomeKit.async_stop"), + patch("homeassistant.components.homekit.async_port_is_available"), + ): assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() @@ -620,8 +626,10 @@ async def test_config_entry_with_trigger_accessory( "pairing_id": ANY, "status": 1, } - with patch("pyhap.accessory_driver.AccessoryDriver.async_start"), patch( - "homeassistant.components.homekit.HomeKit.async_stop" - ), patch("homeassistant.components.homekit.async_port_is_available"): + with ( + patch("pyhap.accessory_driver.AccessoryDriver.async_start"), + patch("homeassistant.components.homekit.HomeKit.async_stop"), + patch("homeassistant.components.homekit.async_port_is_available"), + ): assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index 179a0ce467f..02a39ed9258 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -1,4 +1,5 @@ """Package to test the get_accessory method.""" + from unittest.mock import Mock, patch import pytest diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index 158cb29239c..e0f0786f15d 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -1,4 +1,5 @@ """Tests for the HomeKit component.""" + from __future__ import annotations import asyncio @@ -80,6 +81,8 @@ from tests.common import MockConfigEntry, get_fixture_path IP_ADDRESS = "127.0.0.1" +DEFAULT_LISTEN = ["0.0.0.0", "::"] + def generate_filter( include_domains, @@ -161,8 +164,12 @@ async def test_setup_min(hass: HomeAssistant, mock_async_zeroconf: None) -> None ) entry.add_to_hass(hass) - with patch(f"{PATH_HOMEKIT}.HomeKit") as mock_homekit, patch( - "homeassistant.components.network.async_get_source_ip", return_value="1.2.3.4" + with ( + patch(f"{PATH_HOMEKIT}.HomeKit") as mock_homekit, + patch( + "homeassistant.components.network.async_get_source_ip", + return_value="1.2.3.4", + ), ): mock_homekit.return_value = homekit = Mock() type(homekit).async_start = AsyncMock() @@ -173,7 +180,7 @@ async def test_setup_min(hass: HomeAssistant, mock_async_zeroconf: None) -> None hass, BRIDGE_NAME, DEFAULT_PORT, - [None], + DEFAULT_LISTEN, ANY, ANY, {}, @@ -203,8 +210,12 @@ async def test_removing_entry( ) entry.add_to_hass(hass) - with patch(f"{PATH_HOMEKIT}.HomeKit") as mock_homekit, patch( - "homeassistant.components.network.async_get_source_ip", return_value="1.2.3.4" + with ( + patch(f"{PATH_HOMEKIT}.HomeKit") as mock_homekit, + patch( + "homeassistant.components.network.async_get_source_ip", + return_value="1.2.3.4", + ), ): mock_homekit.return_value = homekit = Mock() type(homekit).async_start = AsyncMock() @@ -215,7 +226,7 @@ async def test_removing_entry( hass, BRIDGE_NAME, DEFAULT_PORT, - [None], + DEFAULT_LISTEN, ANY, ANY, {}, @@ -344,7 +355,7 @@ async def test_homekit_with_single_advertise_ips( ) entry.add_to_hass(hass) with patch(f"{PATH_HOMEKIT}.HomeDriver", return_value=hk_driver) as mock_driver: - mock_driver.async_start = AsyncMock() + hk_driver.async_start = AsyncMock() await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -354,7 +365,7 @@ async def test_homekit_with_single_advertise_ips( ANY, entry.title, loop=hass.loop, - address=[None], + address=DEFAULT_LISTEN, port=ANY, persist_file=ANY, advertised_address="1.3.4.4", @@ -383,7 +394,7 @@ async def test_homekit_with_many_advertise_ips( ) entry.add_to_hass(hass) with patch(f"{PATH_HOMEKIT}.HomeDriver", return_value=hk_driver) as mock_driver: - mock_driver.async_start = AsyncMock() + hk_driver.async_start = AsyncMock() await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -393,7 +404,7 @@ async def test_homekit_with_many_advertise_ips( ANY, entry.title, loop=hass.loop, - address=[None], + address=DEFAULT_LISTEN, port=ANY, persist_file=ANY, advertised_address=["1.3.4.4", "4.3.2.2"], @@ -726,11 +737,11 @@ async def test_homekit_start( hass.states.async_set("light.demo2", "on") state = hass.states.async_all()[0] - with patch(f"{PATH_HOMEKIT}.HomeKit.add_bridge_accessory") as mock_add_acc, patch( - f"{PATH_HOMEKIT}.async_show_setup_message" - ) as mock_setup_msg, patch( - "pyhap.accessory_driver.AccessoryDriver.async_start" - ) as hk_driver_start: + with ( + patch(f"{PATH_HOMEKIT}.HomeKit.add_bridge_accessory") as mock_add_acc, + patch(f"{PATH_HOMEKIT}.async_show_setup_message") as mock_setup_msg, + patch("pyhap.accessory_driver.AccessoryDriver.async_start") as hk_driver_start, + ): await homekit.async_start() await hass.async_block_till_done() @@ -758,13 +769,19 @@ async def test_homekit_start( # Start again to make sure the registry entry is kept homekit.status = STATUS_READY - with patch(f"{PATH_HOMEKIT}.HomeKit.add_bridge_accessory") as mock_add_acc, patch( - f"{PATH_HOMEKIT}.async_show_setup_message" - ) as mock_setup_msg, patch( - "pyhap.accessory_driver.AccessoryDriver.async_start" - ) as hk_driver_start: + with ( + patch(f"{PATH_HOMEKIT}.HomeKit.add_bridge_accessory") as mock_add_acc, + patch(f"{PATH_HOMEKIT}.async_show_setup_message") as mock_setup_msg, + patch("pyhap.accessory_driver.AccessoryDriver.async_start") as hk_driver_start, + patch("pyhap.accessory_driver.AccessoryDriver.load") as load_mock, + patch("pyhap.accessory_driver.AccessoryDriver.persist") as persist_mock, + patch(f"{PATH_HOMEKIT}.os.path.exists", return_value=True), + ): + await homekit.async_stop() await homekit.async_start() + assert load_mock.called + assert not persist_mock.called device = device_registry.async_get_device( identifiers={(DOMAIN, entry.entry_id, BRIDGE_SERIAL_NUMBER)} ) @@ -797,11 +814,11 @@ async def test_homekit_start_with_a_broken_accessory( hass.states.async_set("light.demo", "on") hass.states.async_set("light.broken", "on") - with patch(f"{PATH_HOMEKIT}.get_accessory", side_effect=Exception), patch( - f"{PATH_HOMEKIT}.async_show_setup_message" - ) as mock_setup_msg, patch( - "pyhap.accessory_driver.AccessoryDriver.async_start" - ) as hk_driver_start: + with ( + patch(f"{PATH_HOMEKIT}.get_accessory", side_effect=Exception), + patch(f"{PATH_HOMEKIT}.async_show_setup_message") as mock_setup_msg, + patch("pyhap.accessory_driver.AccessoryDriver.async_start") as hk_driver_start, + ): await homekit.async_start() await hass.async_block_till_done() @@ -843,9 +860,10 @@ async def test_homekit_start_with_a_device( homekit.driver = hk_driver homekit.aid_storage = MagicMock() - with patch(f"{PATH_HOMEKIT}.get_accessory", side_effect=Exception), patch( - f"{PATH_HOMEKIT}.async_show_setup_message" - ) as mock_setup_msg: + with ( + patch(f"{PATH_HOMEKIT}.get_accessory", side_effect=Exception), + patch(f"{PATH_HOMEKIT}.async_show_setup_message") as mock_setup_msg, + ): await homekit.async_start() await hass.async_block_till_done() @@ -902,12 +920,12 @@ async def test_homekit_reset_accessories( hass.states.async_set("light.demo", "on") homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_BRIDGE) - with patch(f"{PATH_HOMEKIT}.HomeKit", return_value=homekit), patch( - "pyhap.accessory_driver.AccessoryDriver.config_changed" - ), patch("pyhap.accessory_driver.AccessoryDriver.async_start"), patch( - f"{PATH_HOMEKIT}.accessories.HomeAccessory.run" - ) as mock_run_accessory, patch.object( - homekit_base, "_HOMEKIT_CONFIG_UPDATE_TIME", 0 + with ( + patch(f"{PATH_HOMEKIT}.HomeKit", return_value=homekit), + patch("pyhap.accessory_driver.AccessoryDriver.config_changed"), + patch("pyhap.accessory_driver.AccessoryDriver.async_start"), + patch(f"{PATH_HOMEKIT}.accessories.HomeAccessory.run") as mock_run_accessory, + patch.object(homekit_base, "_HOMEKIT_CONFIG_UPDATE_TIME", 0), ): await async_init_entry(hass, entry) @@ -1054,8 +1072,9 @@ async def test_homekit_unpair( hass.states.async_set("light.demo", "on") homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_BRIDGE) - with patch(f"{PATH_HOMEKIT}.HomeKit", return_value=homekit), patch( - "pyhap.accessory_driver.AccessoryDriver.async_start" + with ( + patch(f"{PATH_HOMEKIT}.HomeKit", return_value=homekit), + patch("pyhap.accessory_driver.AccessoryDriver.async_start"), ): await async_init_entry(hass, entry) @@ -1103,8 +1122,9 @@ async def test_homekit_unpair_missing_device_id( hass.states.async_set("light.demo", "on") homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_BRIDGE) - with patch(f"{PATH_HOMEKIT}.HomeKit", return_value=homekit), patch( - "pyhap.accessory_driver.AccessoryDriver.async_start" + with ( + patch(f"{PATH_HOMEKIT}.HomeKit", return_value=homekit), + patch("pyhap.accessory_driver.AccessoryDriver.async_start"), ): await async_init_entry(hass, entry) @@ -1148,8 +1168,9 @@ async def test_homekit_unpair_not_homekit_device( hass.states.async_set("light.demo", "on") homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_BRIDGE) - with patch(f"{PATH_HOMEKIT}.HomeKit", return_value=homekit), patch( - "pyhap.accessory_driver.AccessoryDriver.async_start" + with ( + patch(f"{PATH_HOMEKIT}.HomeKit", return_value=homekit), + patch("pyhap.accessory_driver.AccessoryDriver.async_start"), ): await async_init_entry(hass, entry) @@ -1196,13 +1217,15 @@ async def test_homekit_reset_accessories_not_supported( hass.states.async_set("not_supported.demo", "on") homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_BRIDGE) - with patch(f"{PATH_HOMEKIT}.HomeKit", return_value=homekit), patch( - "pyhap.accessory.Bridge.add_accessory" - ) as mock_add_accessory, patch( - "pyhap.accessory_driver.AccessoryDriver.async_update_advertisement" - ) as hk_driver_async_update_advertisement, patch( - "pyhap.accessory_driver.AccessoryDriver.async_start" - ), patch.object(homekit_base, "_HOMEKIT_CONFIG_UPDATE_TIME", 0): + with ( + patch(f"{PATH_HOMEKIT}.HomeKit", return_value=homekit), + patch("pyhap.accessory.Bridge.add_accessory") as mock_add_accessory, + patch( + "pyhap.accessory_driver.AccessoryDriver.async_update_advertisement" + ) as hk_driver_async_update_advertisement, + patch("pyhap.accessory_driver.AccessoryDriver.async_start"), + patch.object(homekit_base, "_HOMEKIT_CONFIG_UPDATE_TIME", 0), + ): await async_init_entry(hass, entry) acc_mock = MagicMock() @@ -1239,13 +1262,15 @@ async def test_homekit_reset_accessories_state_missing( entity_id = "light.demo" homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_BRIDGE) - with patch(f"{PATH_HOMEKIT}.HomeKit", return_value=homekit), patch( - "pyhap.accessory.Bridge.add_accessory" - ) as mock_add_accessory, patch( - "pyhap.accessory_driver.AccessoryDriver.config_changed" - ) as hk_driver_config_changed, patch( - "pyhap.accessory_driver.AccessoryDriver.async_start" - ), patch.object(homekit_base, "_HOMEKIT_CONFIG_UPDATE_TIME", 0): + with ( + patch(f"{PATH_HOMEKIT}.HomeKit", return_value=homekit), + patch("pyhap.accessory.Bridge.add_accessory") as mock_add_accessory, + patch( + "pyhap.accessory_driver.AccessoryDriver.config_changed" + ) as hk_driver_config_changed, + patch("pyhap.accessory_driver.AccessoryDriver.async_start"), + patch.object(homekit_base, "_HOMEKIT_CONFIG_UPDATE_TIME", 0), + ): await async_init_entry(hass, entry) acc_mock = MagicMock() @@ -1281,20 +1306,22 @@ async def test_homekit_reset_accessories_not_bridged( entity_id = "light.demo" homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_BRIDGE) - with patch(f"{PATH_HOMEKIT}.HomeKit", return_value=homekit), patch( - "pyhap.accessory.Bridge.add_accessory" - ) as mock_add_accessory, patch( - "pyhap.accessory_driver.AccessoryDriver.async_update_advertisement" - ) as hk_driver_async_update_advertisement, patch( - "pyhap.accessory_driver.AccessoryDriver.async_start" - ), patch.object(homekit_base, "_HOMEKIT_CONFIG_UPDATE_TIME", 0): + with ( + patch(f"{PATH_HOMEKIT}.HomeKit", return_value=homekit), + patch("pyhap.accessory.Bridge.add_accessory") as mock_add_accessory, + patch( + "pyhap.accessory_driver.AccessoryDriver.async_update_advertisement" + ) as hk_driver_async_update_advertisement, + patch("pyhap.accessory_driver.AccessoryDriver.async_start"), + patch.object(homekit_base, "_HOMEKIT_CONFIG_UPDATE_TIME", 0), + ): await async_init_entry(hass, entry) assert hk_driver_async_update_advertisement.call_count == 0 acc_mock = MagicMock() acc_mock.entity_id = entity_id acc_mock.stop = AsyncMock() - acc_mock.to_HAP = lambda: {} + acc_mock.to_HAP = dict aid = homekit.aid_storage.get_or_allocate_aid_for_entity_id(entity_id) homekit.bridge.accessories = {aid: acc_mock} @@ -1327,13 +1354,16 @@ async def test_homekit_reset_single_accessory( hass.states.async_set("light.demo", "on") homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_ACCESSORY) - with patch(f"{PATH_HOMEKIT}.HomeKit", return_value=homekit), patch( - "pyhap.accessory_driver.AccessoryDriver.async_update_advertisement" - ) as hk_driver_async_update_advertisement, patch( - "pyhap.accessory_driver.AccessoryDriver.async_start" - ), patch( - f"{PATH_HOMEKIT}.accessories.HomeAccessory.run", - ) as mock_run: + with ( + patch(f"{PATH_HOMEKIT}.HomeKit", return_value=homekit), + patch( + "pyhap.accessory_driver.AccessoryDriver.async_update_advertisement" + ) as hk_driver_async_update_advertisement, + patch("pyhap.accessory_driver.AccessoryDriver.async_start"), + patch( + f"{PATH_HOMEKIT}.accessories.HomeAccessory.run", + ) as mock_run, + ): await async_init_entry(hass, entry) homekit.status = STATUS_RUNNING homekit.driver.aio_stop_event = MagicMock() @@ -1363,10 +1393,12 @@ async def test_homekit_reset_single_accessory_unsupported( hass.states.async_set("not_supported.demo", "on") homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_ACCESSORY) - with patch(f"{PATH_HOMEKIT}.HomeKit", return_value=homekit), patch( - "pyhap.accessory_driver.AccessoryDriver.config_changed" - ) as hk_driver_config_changed, patch( - "pyhap.accessory_driver.AccessoryDriver.async_start" + with ( + patch(f"{PATH_HOMEKIT}.HomeKit", return_value=homekit), + patch( + "pyhap.accessory_driver.AccessoryDriver.config_changed" + ) as hk_driver_config_changed, + patch("pyhap.accessory_driver.AccessoryDriver.async_start"), ): await async_init_entry(hass, entry) @@ -1401,10 +1433,12 @@ async def test_homekit_reset_single_accessory_state_missing( entity_id = "light.demo" homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_ACCESSORY) - with patch(f"{PATH_HOMEKIT}.HomeKit", return_value=homekit), patch( - "pyhap.accessory_driver.AccessoryDriver.config_changed" - ) as hk_driver_config_changed, patch( - "pyhap.accessory_driver.AccessoryDriver.async_start" + with ( + patch(f"{PATH_HOMEKIT}.HomeKit", return_value=homekit), + patch( + "pyhap.accessory_driver.AccessoryDriver.config_changed" + ) as hk_driver_config_changed, + patch("pyhap.accessory_driver.AccessoryDriver.async_start"), ): await async_init_entry(hass, entry) @@ -1439,10 +1473,12 @@ async def test_homekit_reset_single_accessory_no_match( entity_id = "light.demo" homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_ACCESSORY) - with patch(f"{PATH_HOMEKIT}.HomeKit", return_value=homekit), patch( - "pyhap.accessory_driver.AccessoryDriver.config_changed" - ) as hk_driver_config_changed, patch( - "pyhap.accessory_driver.AccessoryDriver.async_start" + with ( + patch(f"{PATH_HOMEKIT}.HomeKit", return_value=homekit), + patch( + "pyhap.accessory_driver.AccessoryDriver.config_changed" + ) as hk_driver_config_changed, + patch("pyhap.accessory_driver.AccessoryDriver.async_start"), ): await async_init_entry(hass, entry) @@ -1492,9 +1528,11 @@ async def test_homekit_too_many_accessories( hass.states.async_set("light.demo2", "on") hass.states.async_set("light.demo3", "on") - with patch("pyhap.accessory_driver.AccessoryDriver.async_start"), patch( - f"{PATH_HOMEKIT}.async_show_setup_message" - ), patch(f"{PATH_HOMEKIT}.HomeBridge", _mock_bridge): + with ( + patch("pyhap.accessory_driver.AccessoryDriver.async_start"), + patch(f"{PATH_HOMEKIT}.async_show_setup_message"), + patch(f"{PATH_HOMEKIT}.HomeBridge", _mock_bridge), + ): await homekit.async_start() await hass.async_block_till_done() assert "would exceed" in caplog.text @@ -1554,9 +1592,11 @@ async def test_homekit_finds_linked_batteries( ) hass.states.async_set(light.entity_id, STATE_ON) - with patch(f"{PATH_HOMEKIT}.async_show_setup_message"), patch( - f"{PATH_HOMEKIT}.get_accessory" - ) as mock_get_acc, patch("pyhap.accessory_driver.AccessoryDriver.async_start"): + with ( + patch(f"{PATH_HOMEKIT}.async_show_setup_message"), + patch(f"{PATH_HOMEKIT}.get_accessory") as mock_get_acc, + patch("pyhap.accessory_driver.AccessoryDriver.async_start"), + ): await homekit.async_start() await hass.async_block_till_done() @@ -1628,10 +1668,11 @@ async def test_homekit_async_get_integration_fails( ) hass.states.async_set(light.entity_id, STATE_ON) - with patch.object(homekit.bridge, "add_accessory"), patch( - f"{PATH_HOMEKIT}.async_show_setup_message" - ), patch(f"{PATH_HOMEKIT}.get_accessory") as mock_get_acc, patch( - "pyhap.accessory_driver.AccessoryDriver.async_start" + with ( + patch.object(homekit.bridge, "add_accessory"), + patch(f"{PATH_HOMEKIT}.async_show_setup_message"), + patch(f"{PATH_HOMEKIT}.get_accessory") as mock_get_acc, + patch("pyhap.accessory_driver.AccessoryDriver.async_start"), ): await homekit.async_start() await hass.async_block_till_done() @@ -1664,8 +1705,12 @@ async def test_yaml_updates_update_config_entry_for_name( ) entry.add_to_hass(hass) - with patch(f"{PATH_HOMEKIT}.HomeKit") as mock_homekit, patch( - "homeassistant.components.network.async_get_source_ip", return_value="1.2.3.4" + with ( + patch(f"{PATH_HOMEKIT}.HomeKit") as mock_homekit, + patch( + "homeassistant.components.network.async_get_source_ip", + return_value="1.2.3.4", + ), ): mock_homekit.return_value = homekit = Mock() type(homekit).async_start = AsyncMock() @@ -1678,7 +1723,7 @@ async def test_yaml_updates_update_config_entry_for_name( hass, BRIDGE_NAME, 12345, - [None], + DEFAULT_LISTEN, ANY, ANY, {}, @@ -1709,8 +1754,12 @@ async def test_yaml_can_link_with_default_name( ) entry.add_to_hass(hass) - with patch(f"{PATH_HOMEKIT}.HomeKit") as mock_homekit, patch( - "homeassistant.components.network.async_get_source_ip", return_value="1.2.3.4" + with ( + patch(f"{PATH_HOMEKIT}.HomeKit") as mock_homekit, + patch( + "homeassistant.components.network.async_get_source_ip", + return_value="1.2.3.4", + ), ): mock_homekit.return_value = homekit = Mock() type(homekit).async_start = AsyncMock() @@ -1752,8 +1801,12 @@ async def test_yaml_can_link_with_port( options={}, ) entry3.add_to_hass(hass) - with patch(f"{PATH_HOMEKIT}.HomeKit") as mock_homekit, patch( - "homeassistant.components.network.async_get_source_ip", return_value="1.2.3.4" + with ( + patch(f"{PATH_HOMEKIT}.HomeKit") as mock_homekit, + patch( + "homeassistant.components.network.async_get_source_ip", + return_value="1.2.3.4", + ), ): mock_homekit.return_value = homekit = Mock() type(homekit).async_start = AsyncMock() @@ -1789,9 +1842,11 @@ async def test_homekit_uses_system_zeroconf( assert await async_setup_component(hass, "zeroconf", {"zeroconf": {}}) system_async_zc = await zeroconf.async_get_async_instance(hass) - with patch("pyhap.accessory_driver.AccessoryDriver.async_start"), patch( - f"{PATH_HOMEKIT}.HomeKit.async_stop" - ), patch(f"{PATH_HOMEKIT}.async_port_is_available"): + with ( + patch("pyhap.accessory_driver.AccessoryDriver.async_start"), + patch(f"{PATH_HOMEKIT}.HomeKit.async_stop"), + patch(f"{PATH_HOMEKIT}.async_port_is_available"), + ): entry.add_to_hass(hass) assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -1871,9 +1926,11 @@ async def test_homekit_ignored_missing_devices( hass.states.async_set(light.entity_id, STATE_ON) hass.states.async_set("light.two", STATE_ON) - with patch(f"{PATH_HOMEKIT}.get_accessory") as mock_get_acc, patch( - f"{PATH_HOMEKIT}.HomeBridge", return_value=homekit.bridge - ), patch("pyhap.accessory_driver.AccessoryDriver.async_start"): + with ( + patch(f"{PATH_HOMEKIT}.get_accessory") as mock_get_acc, + patch(f"{PATH_HOMEKIT}.HomeBridge", return_value=homekit.bridge), + patch("pyhap.accessory_driver.AccessoryDriver.async_start"), + ): await homekit.async_start() await hass.async_block_till_done() @@ -1933,10 +1990,11 @@ async def test_homekit_finds_linked_motion_sensors( ) hass.states.async_set(camera.entity_id, STATE_ON) - with patch.object(homekit.bridge, "add_accessory"), patch( - f"{PATH_HOMEKIT}.async_show_setup_message" - ), patch(f"{PATH_HOMEKIT}.get_accessory") as mock_get_acc, patch( - "pyhap.accessory_driver.AccessoryDriver.async_start" + with ( + patch.object(homekit.bridge, "add_accessory"), + patch(f"{PATH_HOMEKIT}.async_show_setup_message"), + patch(f"{PATH_HOMEKIT}.get_accessory") as mock_get_acc, + patch("pyhap.accessory_driver.AccessoryDriver.async_start"), ): await homekit.async_start() await hass.async_block_till_done() @@ -2002,10 +2060,11 @@ async def test_homekit_finds_linked_humidity_sensors( ) hass.states.async_set(humidifier.entity_id, STATE_ON) - with patch.object(homekit.bridge, "add_accessory"), patch( - f"{PATH_HOMEKIT}.async_show_setup_message" - ), patch(f"{PATH_HOMEKIT}.get_accessory") as mock_get_acc, patch( - "pyhap.accessory_driver.AccessoryDriver.async_start" + with ( + patch.object(homekit.bridge, "add_accessory"), + patch(f"{PATH_HOMEKIT}.async_show_setup_message"), + patch(f"{PATH_HOMEKIT}.get_accessory") as mock_get_acc, + patch("pyhap.accessory_driver.AccessoryDriver.async_start"), ): await homekit.async_start() await hass.async_block_till_done() @@ -2036,10 +2095,15 @@ async def test_reload(hass: HomeAssistant, mock_async_zeroconf: None) -> None: ) entry.add_to_hass(hass) - with patch(f"{PATH_HOMEKIT}.HomeKit") as mock_homekit, patch( - "homeassistant.components.network.async_get_source_ip", return_value="1.2.3.4" + with ( + patch(f"{PATH_HOMEKIT}.HomeKit") as mock_homekit, + patch( + "homeassistant.components.network.async_get_source_ip", + return_value="1.2.3.4", + ), ): mock_homekit.return_value = homekit = Mock() + type(homekit).async_start = AsyncMock() assert await async_setup_component( hass, "homekit", {"homekit": {CONF_NAME: "reloadable", CONF_PORT: 12345}} ) @@ -2049,7 +2113,7 @@ async def test_reload(hass: HomeAssistant, mock_async_zeroconf: None) -> None: hass, "reloadable", 12345, - [None], + DEFAULT_LISTEN, ANY, False, {}, @@ -2060,18 +2124,24 @@ async def test_reload(hass: HomeAssistant, mock_async_zeroconf: None) -> None: devices=[], ) yaml_path = get_fixture_path("configuration.yaml", "homekit") - with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path), patch( - f"{PATH_HOMEKIT}.HomeKit" - ) as mock_homekit2, patch.object(homekit.bridge, "add_accessory"), patch( - f"{PATH_HOMEKIT}.async_show_setup_message" - ), patch( - f"{PATH_HOMEKIT}.get_accessory", - ), patch( - "pyhap.accessory_driver.AccessoryDriver.async_start", - ), patch( - "homeassistant.components.network.async_get_source_ip", return_value="1.2.3.4" + with ( + patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path), + patch(f"{PATH_HOMEKIT}.HomeKit") as mock_homekit2, + patch(f"{PATH_HOMEKIT}.async_show_setup_message"), + patch( + f"{PATH_HOMEKIT}.get_accessory", + ), + patch(f"{PATH_HOMEKIT}.async_port_is_available", return_value=True), + patch( + "pyhap.accessory_driver.AccessoryDriver.async_start", + ), + patch( + "homeassistant.components.network.async_get_source_ip", + return_value="1.2.3.4", + ), ): mock_homekit2.return_value = homekit = Mock() + type(homekit).async_start = AsyncMock() await hass.services.async_call( "homekit", SERVICE_RELOAD, @@ -2084,7 +2154,7 @@ async def test_reload(hass: HomeAssistant, mock_async_zeroconf: None) -> None: hass, "reloadable", 45678, - [None], + DEFAULT_LISTEN, ANY, False, {}, @@ -2114,11 +2184,11 @@ async def test_homekit_start_in_accessory_mode( hass.states.async_set("light.demo", "on") - with patch(f"{PATH_HOMEKIT}.HomeKit.add_bridge_accessory") as mock_add_acc, patch( - f"{PATH_HOMEKIT}.async_show_setup_message" - ) as mock_setup_msg, patch( - "pyhap.accessory_driver.AccessoryDriver.async_start" - ) as hk_driver_start: + with ( + patch(f"{PATH_HOMEKIT}.HomeKit.add_bridge_accessory") as mock_add_acc, + patch(f"{PATH_HOMEKIT}.async_show_setup_message") as mock_setup_msg, + patch("pyhap.accessory_driver.AccessoryDriver.async_start") as hk_driver_start, + ): await homekit.async_start() await hass.async_block_till_done() @@ -2159,11 +2229,11 @@ async def test_homekit_start_in_accessory_mode_unsupported_entity( hass.states.async_set("notsupported.demo", "on") - with patch(f"{PATH_HOMEKIT}.HomeKit.add_bridge_accessory") as mock_add_acc, patch( - f"{PATH_HOMEKIT}.async_show_setup_message" - ) as mock_setup_msg, patch( - "pyhap.accessory_driver.AccessoryDriver.async_start" - ) as hk_driver_start: + with ( + patch(f"{PATH_HOMEKIT}.HomeKit.add_bridge_accessory") as mock_add_acc, + patch(f"{PATH_HOMEKIT}.async_show_setup_message") as mock_setup_msg, + patch("pyhap.accessory_driver.AccessoryDriver.async_start") as hk_driver_start, + ): await homekit.async_start() await hass.async_block_till_done() @@ -2191,9 +2261,11 @@ async def test_homekit_start_in_accessory_mode_missing_entity( homekit.driver = hk_driver homekit.driver.accessory = Accessory(hk_driver, "any") - with patch(f"{PATH_HOMEKIT}.HomeKit.add_bridge_accessory") as mock_add_acc, patch( - f"{PATH_HOMEKIT}.async_show_setup_message" - ), patch("pyhap.accessory_driver.AccessoryDriver.async_start"): + with ( + patch(f"{PATH_HOMEKIT}.HomeKit.add_bridge_accessory") as mock_add_acc, + patch(f"{PATH_HOMEKIT}.async_show_setup_message"), + patch("pyhap.accessory_driver.AccessoryDriver.async_start"), + ): await homekit.async_start() await hass.async_block_till_done() @@ -2218,9 +2290,13 @@ async def test_wait_for_port_to_free( ) entry.add_to_hass(hass) - with patch("pyhap.accessory_driver.AccessoryDriver.async_start"), patch( - f"{PATH_HOMEKIT}.HomeKit.async_stop" - ), patch(f"{PATH_HOMEKIT}.async_port_is_available", return_value=True) as port_mock: + with ( + patch("pyhap.accessory_driver.AccessoryDriver.async_start"), + patch(f"{PATH_HOMEKIT}.HomeKit.async_stop"), + patch( + f"{PATH_HOMEKIT}.async_port_is_available", return_value=True + ) as port_mock, + ): 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) @@ -2228,11 +2304,14 @@ async def test_wait_for_port_to_free( assert "Waiting for the HomeKit server to shutdown" not in caplog.text assert port_mock.called - with patch("pyhap.accessory_driver.AccessoryDriver.async_start"), patch( - f"{PATH_HOMEKIT}.HomeKit.async_stop" - ), patch.object(homekit_base, "PORT_CLEANUP_CHECK_INTERVAL_SECS", 0), patch( - f"{PATH_HOMEKIT}.async_port_is_available", return_value=False - ) as port_mock: + with ( + patch("pyhap.accessory_driver.AccessoryDriver.async_start"), + patch(f"{PATH_HOMEKIT}.HomeKit.async_stop"), + patch.object(homekit_base, "PORT_CLEANUP_CHECK_INTERVAL_SECS", 0), + patch( + f"{PATH_HOMEKIT}.async_port_is_available", return_value=False + ) as port_mock, + ): 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) diff --git a/tests/components/homekit/test_iidmanager.py b/tests/components/homekit/test_iidmanager.py index 386e8cf8f11..39d2dda8237 100644 --- a/tests/components/homekit/test_iidmanager.py +++ b/tests/components/homekit/test_iidmanager.py @@ -1,4 +1,5 @@ """Tests for the HomeKit IID manager.""" + from typing import Any from uuid import UUID diff --git a/tests/components/homekit/test_init.py b/tests/components/homekit/test_init.py index 5a1d42352fe..e8fb7e1d92e 100644 --- a/tests/components/homekit/test_init.py +++ b/tests/components/homekit/test_init.py @@ -1,5 +1,6 @@ """Test HomeKit initialization.""" -from unittest.mock import patch + +from unittest.mock import AsyncMock, Mock, patch import pytest @@ -30,9 +31,12 @@ async def test_humanify_homekit_changed_event( ) -> None: """Test humanifying HomeKit changed event.""" hass.config.components.add("recorder") - with patch("homeassistant.components.homekit.HomeKit"): + with patch("homeassistant.components.homekit.HomeKit") as mock_homekit: + mock_homekit.return_value = homekit = Mock() + type(homekit).async_start = AsyncMock() assert await async_setup_component(hass, "homekit", {"homekit": {}}) assert await async_setup_component(hass, "logbook", {}) + await hass.async_block_till_done() event1, event2 = mock_humanify( hass, @@ -110,9 +114,13 @@ async def test_bridge_with_triggers( ) entry.add_to_hass(hass) - with patch( - "homeassistant.components.network.async_get_source_ip", return_value="1.2.3.4" - ), patch(f"{PATH_HOMEKIT}.async_port_is_available", return_value=True): + with ( + patch( + "homeassistant.components.network.async_get_source_ip", + return_value="1.2.3.4", + ), + patch(f"{PATH_HOMEKIT}.async_port_is_available", return_value=True), + ): assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) diff --git a/tests/components/homekit/test_type_cameras.py b/tests/components/homekit/test_type_cameras.py index db2e6c6e8ea..184ce1b6521 100644 --- a/tests/components/homekit/test_type_cameras.py +++ b/tests/components/homekit/test_type_cameras.py @@ -1,4 +1,5 @@ """Test different accessory types: Camera.""" + import asyncio from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch from uuid import UUID @@ -172,12 +173,15 @@ async def test_camera_stream_source_configured( working_ffmpeg = _get_working_mock_ffmpeg() session_info = acc.sessions[MOCK_START_STREAM_SESSION_UUID] - with patch( - "homeassistant.components.demo.camera.DemoCamera.stream_source", - return_value=None, - ), patch( - "homeassistant.components.homekit.type_cameras.HAFFmpeg", - return_value=working_ffmpeg, + with ( + patch( + "homeassistant.components.demo.camera.DemoCamera.stream_source", + return_value=None, + ), + patch( + "homeassistant.components.homekit.type_cameras.HAFFmpeg", + return_value=working_ffmpeg, + ), ): await _async_start_streaming(hass, acc) await _async_stop_all_streams(hass, acc) @@ -207,12 +211,15 @@ async def test_camera_stream_source_configured( working_ffmpeg = _get_working_mock_ffmpeg() session_info = acc.sessions[MOCK_START_STREAM_SESSION_UUID] - with patch( - "homeassistant.components.demo.camera.DemoCamera.stream_source", - return_value="rtsp://example.local", - ), patch( - "homeassistant.components.homekit.type_cameras.HAFFmpeg", - return_value=working_ffmpeg, + with ( + patch( + "homeassistant.components.demo.camera.DemoCamera.stream_source", + return_value="rtsp://example.local", + ), + patch( + "homeassistant.components.homekit.type_cameras.HAFFmpeg", + return_value=working_ffmpeg, + ), ): await _async_start_streaming(hass, acc) await _async_stop_all_streams(hass, acc) @@ -285,12 +292,15 @@ async def test_camera_stream_source_configured_with_failing_ffmpeg( await _async_setup_endpoints(hass, acc) - with patch( - "homeassistant.components.demo.camera.DemoCamera.stream_source", - return_value="rtsp://example.local", - ), patch( - "homeassistant.components.homekit.type_cameras.HAFFmpeg", - return_value=_get_failing_mock_ffmpeg(), + with ( + patch( + "homeassistant.components.demo.camera.DemoCamera.stream_source", + return_value="rtsp://example.local", + ), + patch( + "homeassistant.components.homekit.type_cameras.HAFFmpeg", + return_value=_get_failing_mock_ffmpeg(), + ), ): await _async_start_streaming(hass, acc) await _async_stop_all_streams(hass, acc) @@ -329,12 +339,15 @@ async def test_camera_stream_source_found( working_ffmpeg = _get_working_mock_ffmpeg() session_info = acc.sessions[MOCK_START_STREAM_SESSION_UUID] - with patch( - "homeassistant.components.demo.camera.DemoCamera.stream_source", - return_value="rtsp://example.local", - ), patch( - "homeassistant.components.homekit.type_cameras.HAFFmpeg", - return_value=working_ffmpeg, + with ( + patch( + "homeassistant.components.demo.camera.DemoCamera.stream_source", + return_value="rtsp://example.local", + ), + patch( + "homeassistant.components.homekit.type_cameras.HAFFmpeg", + return_value=working_ffmpeg, + ), ): await _async_start_streaming(hass, acc) await _async_stop_all_streams(hass, acc) @@ -360,12 +373,15 @@ async def test_camera_stream_source_found( working_ffmpeg = _get_working_mock_ffmpeg() session_info = acc.sessions[MOCK_START_STREAM_SESSION_UUID] - with patch( - "homeassistant.components.demo.camera.DemoCamera.stream_source", - return_value="rtsp://example2.local", - ), patch( - "homeassistant.components.homekit.type_cameras.HAFFmpeg", - return_value=working_ffmpeg, + with ( + patch( + "homeassistant.components.demo.camera.DemoCamera.stream_source", + return_value="rtsp://example2.local", + ), + patch( + "homeassistant.components.homekit.type_cameras.HAFFmpeg", + return_value=working_ffmpeg, + ), ): await _async_start_streaming(hass, acc) await _async_stop_all_streams(hass, acc) @@ -409,12 +425,15 @@ async def test_camera_stream_source_fails( await _async_setup_endpoints(hass, acc) - with patch( - "homeassistant.components.demo.camera.DemoCamera.stream_source", - side_effect=OSError, - ), patch( - "homeassistant.components.homekit.type_cameras.HAFFmpeg", - return_value=_get_working_mock_ffmpeg(), + with ( + patch( + "homeassistant.components.demo.camera.DemoCamera.stream_source", + side_effect=OSError, + ), + patch( + "homeassistant.components.homekit.type_cameras.HAFFmpeg", + return_value=_get_working_mock_ffmpeg(), + ), ): await _async_start_streaming(hass, acc) await _async_stop_all_streams(hass, acc) @@ -492,12 +511,15 @@ async def test_camera_stream_source_configured_and_copy_codec( working_ffmpeg = _get_working_mock_ffmpeg() - with patch( - "homeassistant.components.demo.camera.DemoCamera.stream_source", - return_value=None, - ), patch( - "homeassistant.components.homekit.type_cameras.HAFFmpeg", - return_value=working_ffmpeg, + with ( + patch( + "homeassistant.components.demo.camera.DemoCamera.stream_source", + return_value=None, + ), + patch( + "homeassistant.components.homekit.type_cameras.HAFFmpeg", + return_value=working_ffmpeg, + ), ): await _async_start_streaming(hass, acc) await _async_reconfigure_stream(hass, acc, session_info, {}) @@ -565,12 +587,15 @@ async def test_camera_stream_source_configured_and_override_profile_names( working_ffmpeg = _get_working_mock_ffmpeg() - with patch( - "homeassistant.components.demo.camera.DemoCamera.stream_source", - return_value=None, - ), patch( - "homeassistant.components.homekit.type_cameras.HAFFmpeg", - return_value=working_ffmpeg, + with ( + patch( + "homeassistant.components.demo.camera.DemoCamera.stream_source", + return_value=None, + ), + patch( + "homeassistant.components.homekit.type_cameras.HAFFmpeg", + return_value=working_ffmpeg, + ), ): await _async_start_streaming(hass, acc) await _async_reconfigure_stream(hass, acc, session_info, {}) @@ -637,12 +662,15 @@ async def test_camera_streaming_fails_after_starting_ffmpeg( ffmpeg_with_invalid_pid = _get_exits_after_startup_mock_ffmpeg() - with patch( - "homeassistant.components.demo.camera.DemoCamera.stream_source", - return_value=None, - ), patch( - "homeassistant.components.homekit.type_cameras.HAFFmpeg", - return_value=ffmpeg_with_invalid_pid, + with ( + patch( + "homeassistant.components.demo.camera.DemoCamera.stream_source", + return_value=None, + ), + patch( + "homeassistant.components.homekit.type_cameras.HAFFmpeg", + return_value=ffmpeg_with_invalid_pid, + ), ): await _async_start_streaming(hass, acc) await _async_reconfigure_stream(hass, acc, session_info, {}) diff --git a/tests/components/homekit/test_type_covers.py b/tests/components/homekit/test_type_covers.py index e0c016264f2..6efd9118092 100644 --- a/tests/components/homekit/test_type_covers.py +++ b/tests/components/homekit/test_type_covers.py @@ -1,4 +1,5 @@ """Test different accessory types: Covers.""" + from homeassistant.components.cover import ( ATTR_CURRENT_POSITION, ATTR_CURRENT_TILT_POSITION, diff --git a/tests/components/homekit/test_type_fans.py b/tests/components/homekit/test_type_fans.py index cb47b320c04..d971b8c06d2 100644 --- a/tests/components/homekit/test_type_fans.py +++ b/tests/components/homekit/test_type_fans.py @@ -1,4 +1,5 @@ """Test different accessory types: Fans.""" + from pyhap.const import HAP_REPR_AID, HAP_REPR_CHARS, HAP_REPR_IID, HAP_REPR_VALUE from homeassistant.components.fan import ( diff --git a/tests/components/homekit/test_type_humidifiers.py b/tests/components/homekit/test_type_humidifiers.py index 8ac748db278..fdd01e05a91 100644 --- a/tests/components/homekit/test_type_humidifiers.py +++ b/tests/components/homekit/test_type_humidifiers.py @@ -1,4 +1,5 @@ """Test different accessory types: HumidifierDehumidifier.""" + from pyhap.accessory_driver import AccessoryDriver from pyhap.const import ( CATEGORY_HUMIDIFIER, diff --git a/tests/components/homekit/test_type_lights.py b/tests/components/homekit/test_type_lights.py index 3bd3f1fb824..8d2978fb0bd 100644 --- a/tests/components/homekit/test_type_lights.py +++ b/tests/components/homekit/test_type_lights.py @@ -1,4 +1,5 @@ """Test different accessory types: Lights.""" + from datetime import timedelta from pyhap.const import HAP_REPR_AID, HAP_REPR_CHARS, HAP_REPR_IID, HAP_REPR_VALUE @@ -614,7 +615,7 @@ async def test_light_restore( @pytest.mark.parametrize( ("supported_color_modes", "state_props", "turn_on_props_with_brightness"), [ - [ + ( [ColorMode.COLOR_TEMP, ColorMode.RGBW], { ATTR_RGBW_COLOR: (128, 50, 0, 255), @@ -624,8 +625,8 @@ async def test_light_restore( ATTR_COLOR_MODE: ColorMode.RGBW, }, {ATTR_HS_COLOR: (145, 75), ATTR_BRIGHTNESS_PCT: 25}, - ], - [ + ), + ( [ColorMode.COLOR_TEMP, ColorMode.RGBWW], { ATTR_RGBWW_COLOR: (128, 50, 0, 255, 255), @@ -635,7 +636,7 @@ async def test_light_restore( ATTR_COLOR_MODE: ColorMode.RGBWW, }, {ATTR_HS_COLOR: (145, 75), ATTR_BRIGHTNESS_PCT: 25}, - ], + ), ], ) async def test_light_rgb_with_color_temp( @@ -734,7 +735,7 @@ async def test_light_rgb_with_color_temp( @pytest.mark.parametrize( ("supported_color_modes", "state_props", "turn_on_props_with_brightness"), [ - [ + ( [ColorMode.RGBW], { ATTR_RGBW_COLOR: (128, 50, 0, 255), @@ -744,8 +745,8 @@ async def test_light_rgb_with_color_temp( ATTR_COLOR_MODE: ColorMode.RGBW, }, {ATTR_RGBW_COLOR: (0, 0, 0, 191)}, - ], - [ + ), + ( [ColorMode.RGBWW], { ATTR_RGBWW_COLOR: (128, 50, 0, 255, 255), @@ -755,7 +756,7 @@ async def test_light_rgb_with_color_temp( ATTR_COLOR_MODE: ColorMode.RGBWW, }, {ATTR_RGBWW_COLOR: (0, 0, 0, 165, 26)}, - ], + ), ], ) async def test_light_rgbwx_with_color_temp_and_brightness( @@ -931,7 +932,7 @@ async def test_light_rgb_or_w_lights( @pytest.mark.parametrize( ("supported_color_modes", "state_props"), [ - [ + ( [ColorMode.COLOR_TEMP, ColorMode.RGBW], { ATTR_RGBW_COLOR: (128, 50, 0, 255), @@ -940,8 +941,8 @@ async def test_light_rgb_or_w_lights( ATTR_BRIGHTNESS: 255, ATTR_COLOR_MODE: ColorMode.RGBW, }, - ], - [ + ), + ( [ColorMode.COLOR_TEMP, ColorMode.RGBWW], { ATTR_RGBWW_COLOR: (128, 50, 0, 255, 255), @@ -950,7 +951,7 @@ async def test_light_rgb_or_w_lights( ATTR_BRIGHTNESS: 255, ATTR_COLOR_MODE: ColorMode.RGBWW, }, - ], + ), ], ) async def test_light_rgb_with_white_switch_to_temp( @@ -1375,13 +1376,13 @@ async def test_light_min_max_mireds(hass: HomeAssistant, hk_driver, events) -> N ATTR_SUPPORTED_COLOR_MODES: [ColorMode.COLOR_TEMP], ATTR_BRIGHTNESS: 255, ATTR_MAX_MIREDS: 500.5, - ATTR_MIN_MIREDS: 100.5, + ATTR_MIN_MIREDS: 153.5, }, ) await hass.async_block_till_done() acc = Light(hass, hk_driver, "Light", entity_id, 1, None) - acc.char_color_temp.properties["maxValue"] == 500 - acc.char_color_temp.properties["minValue"] == 100 + assert acc.char_color_temp.properties["maxValue"] == 500 + assert acc.char_color_temp.properties["minValue"] == 153 async def test_light_set_brightness_and_color_temp( diff --git a/tests/components/homekit/test_type_locks.py b/tests/components/homekit/test_type_locks.py index 3a9f7d93d88..4d83fe41f48 100644 --- a/tests/components/homekit/test_type_locks.py +++ b/tests/components/homekit/test_type_locks.py @@ -1,4 +1,5 @@ """Test different accessory types: Locks.""" + import pytest from homeassistant.components.homekit.const import ATTR_VALUE diff --git a/tests/components/homekit/test_type_media_players.py b/tests/components/homekit/test_type_media_players.py index 33eb1e6c4ee..b17f16231af 100644 --- a/tests/components/homekit/test_type_media_players.py +++ b/tests/components/homekit/test_type_media_players.py @@ -1,4 +1,5 @@ """Test different accessory types: Media Players.""" + import pytest from homeassistant.components.homekit.accessories import HomeDriver diff --git a/tests/components/homekit/test_type_remote.py b/tests/components/homekit/test_type_remote.py index 7c66c20f17e..988950c64a8 100644 --- a/tests/components/homekit/test_type_remote.py +++ b/tests/components/homekit/test_type_remote.py @@ -1,4 +1,5 @@ """Test different accessory types: Remotes.""" + from unittest.mock import patch import pytest diff --git a/tests/components/homekit/test_type_security_systems.py b/tests/components/homekit/test_type_security_systems.py index b71e01dd280..18434a345ce 100644 --- a/tests/components/homekit/test_type_security_systems.py +++ b/tests/components/homekit/test_type_security_systems.py @@ -1,4 +1,5 @@ """Test different accessory types: Security Systems.""" + from pyhap.loader import get_loader import pytest diff --git a/tests/components/homekit/test_type_sensors.py b/tests/components/homekit/test_type_sensors.py index cba0bc54243..ac086b8100e 100644 --- a/tests/components/homekit/test_type_sensors.py +++ b/tests/components/homekit/test_type_sensors.py @@ -1,4 +1,5 @@ """Test different accessory types: Sensors.""" + from unittest.mock import patch from homeassistant.components.binary_sensor import BinarySensorDeviceClass diff --git a/tests/components/homekit/test_type_switches.py b/tests/components/homekit/test_type_switches.py index 863515c31d7..27937babc57 100644 --- a/tests/components/homekit/test_type_switches.py +++ b/tests/components/homekit/test_type_switches.py @@ -1,4 +1,5 @@ """Test different accessory types: Switches.""" + from datetime import timedelta import pytest diff --git a/tests/components/homekit/test_type_thermostats.py b/tests/components/homekit/test_type_thermostats.py index e827573363d..ca2a02cb440 100644 --- a/tests/components/homekit/test_type_thermostats.py +++ b/tests/components/homekit/test_type_thermostats.py @@ -1,4 +1,5 @@ """Test different accessory types: Thermostats.""" + from unittest.mock import patch from pyhap.characteristic import Characteristic @@ -1958,7 +1959,7 @@ async def test_thermostat_with_no_off_after_recheck( async def test_thermostat_with_temp_clamps( hass: HomeAssistant, hk_driver, events ) -> None: - """Test that tempatures are clamped to valid values to prevent homekit crash.""" + """Test that temperatures are clamped to valid values to prevent homekit crash.""" entity_id = "climate.test" base_attrs = { ATTR_SUPPORTED_FEATURES: ClimateEntityFeature.TARGET_TEMPERATURE diff --git a/tests/components/homekit/test_type_triggers.py b/tests/components/homekit/test_type_triggers.py index 33ce01678a3..7471e0bff1c 100644 --- a/tests/components/homekit/test_type_triggers.py +++ b/tests/components/homekit/test_type_triggers.py @@ -1,4 +1,5 @@ """Test different accessory types: Triggers (Programmable Switches).""" + from unittest.mock import MagicMock from homeassistant.components.device_automation import DeviceAutomationType diff --git a/tests/components/homekit/test_util.py b/tests/components/homekit/test_util.py index 60ee2a4d8e8..c419f7c19e7 100644 --- a/tests/components/homekit/test_util.py +++ b/tests/components/homekit/test_util.py @@ -1,4 +1,5 @@ """Test HomeKit util module.""" + from unittest.mock import MagicMock, Mock, patch import pytest @@ -341,9 +342,12 @@ async def test_port_is_available_skips_existing_entries(hass: HomeAssistant) -> ): assert async_port_is_available(next_port) - with pytest.raises(OSError), patch( - "homeassistant.components.homekit.util.socket.socket", - return_value=_mock_socket(10), + with ( + pytest.raises(OSError), + patch( + "homeassistant.components.homekit.util.socket.socket", + return_value=_mock_socket(10), + ), ): async_find_next_available_port(hass, 65530) diff --git a/tests/components/homekit_controller/common.py b/tests/components/homekit_controller/common.py index a5219fe7018..39466cc51e4 100644 --- a/tests/components/homekit_controller/common.py +++ b/tests/components/homekit_controller/common.py @@ -1,4 +1,5 @@ """Code to support homekit_controller tests.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/tests/components/homekit_controller/conftest.py b/tests/components/homekit_controller/conftest.py index 904b752205e..ae2ca721cfa 100644 --- a/tests/components/homekit_controller/conftest.py +++ b/tests/components/homekit_controller/conftest.py @@ -1,4 +1,5 @@ """HomeKit controller session fixtures.""" + import datetime import unittest.mock diff --git a/tests/components/homekit_controller/snapshots/test_init.ambr b/tests/components/homekit_controller/snapshots/test_init.ambr index 94e7dd7bc17..10f62920d8e 100644 --- a/tests/components/homekit_controller/snapshots/test_init.ambr +++ b/tests/components/homekit_controller/snapshots/test_init.ambr @@ -37,6 +37,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -78,6 +80,8 @@ 'capabilities': dict({ 'preset_modes': None, }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -126,6 +130,8 @@ 'manual', ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -174,6 +180,8 @@ 'purifying', ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -220,6 +228,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -262,6 +272,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -304,6 +316,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -354,6 +368,8 @@ 'sleepy', ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -411,6 +427,8 @@ 'router', ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -459,6 +477,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -497,6 +517,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -535,6 +557,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -609,6 +633,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -680,6 +706,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -719,6 +747,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -758,6 +788,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -799,6 +831,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -841,6 +875,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -911,6 +947,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -950,6 +988,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -989,6 +1029,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -1030,6 +1072,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -1072,6 +1116,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -1142,6 +1188,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -1181,6 +1229,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -1220,6 +1270,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -1261,6 +1313,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -1303,6 +1357,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -1377,6 +1433,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -1419,6 +1477,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -1463,6 +1523,8 @@ 'mode': , 'step': 1, }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -1505,6 +1567,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -1575,6 +1639,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -1614,6 +1680,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -1655,6 +1723,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -1733,6 +1803,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -1775,6 +1847,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -1823,6 +1897,8 @@ , ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -1882,6 +1958,8 @@ 'mode': , 'step': 1, }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -1924,6 +2002,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -1998,6 +2078,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -2039,6 +2121,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -2117,6 +2201,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -2156,6 +2242,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -2195,6 +2283,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -2243,6 +2333,8 @@ , ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -2299,6 +2391,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -2341,6 +2435,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -2385,6 +2481,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -2428,6 +2526,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -2469,6 +2569,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -2507,6 +2609,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -2581,6 +2685,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -2622,6 +2728,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -2665,6 +2773,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -2708,6 +2818,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -2751,6 +2863,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -2794,6 +2908,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -2837,6 +2953,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -2878,6 +2996,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -2917,6 +3037,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -2992,6 +3114,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -3031,6 +3155,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -3072,6 +3198,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -3145,6 +3273,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -3184,6 +3314,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -3223,6 +3355,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -3261,6 +3395,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -3311,6 +3447,8 @@ 'min_humidity': 20, 'min_temp': 7.2, }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -3373,6 +3511,8 @@ 'away', ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -3421,6 +3561,8 @@ 'fahrenheit', ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -3465,6 +3607,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -3508,6 +3652,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -3581,6 +3727,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -3620,6 +3768,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -3661,6 +3811,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -3734,6 +3886,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -3773,6 +3927,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -3814,6 +3970,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -3891,6 +4049,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -3930,6 +4090,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -3969,6 +4131,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -4007,6 +4171,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -4057,6 +4223,8 @@ 'min_humidity': 20, 'min_temp': 7.2, }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -4119,6 +4287,8 @@ 'away', ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -4167,6 +4337,8 @@ 'fahrenheit', ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -4211,6 +4383,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -4254,6 +4428,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -4331,6 +4507,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -4370,6 +4548,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -4441,6 +4621,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -4491,6 +4673,8 @@ 'min_humidity': 20, 'min_temp': 7.2, }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -4552,6 +4736,8 @@ 'fahrenheit', ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -4596,6 +4782,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -4639,6 +4827,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -4712,6 +4902,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -4751,6 +4943,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -4792,6 +4986,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -4865,6 +5061,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -4904,6 +5102,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -4945,6 +5145,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -5022,6 +5224,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -5061,6 +5265,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -5100,6 +5306,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -5138,6 +5346,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -5192,6 +5402,8 @@ 'min_humidity': 20, 'min_temp': 7.2, }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -5259,6 +5471,8 @@ 'away', ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -5307,6 +5521,8 @@ 'fahrenheit', ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -5351,6 +5567,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -5394,6 +5612,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -5471,6 +5691,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -5510,6 +5732,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -5549,6 +5773,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -5590,6 +5816,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -5633,6 +5861,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -5674,6 +5904,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -5748,6 +5980,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -5792,6 +6026,8 @@ 'mode': , 'step': 1, }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -5839,6 +6075,8 @@ 'fahrenheit', ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -5883,6 +6121,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -5926,6 +6166,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -5970,6 +6212,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -6013,6 +6257,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -6090,6 +6336,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -6131,6 +6379,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -6174,6 +6424,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -6217,6 +6469,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -6260,6 +6514,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -6301,6 +6557,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -6340,6 +6598,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -6414,6 +6674,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -6453,6 +6715,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -6491,6 +6755,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -6532,6 +6798,8 @@ 'capabilities': dict({ 'preset_modes': None, }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -6607,6 +6875,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -6646,6 +6916,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -6720,6 +6992,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -6759,6 +7033,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -6801,6 +7077,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -6875,6 +7153,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -6946,6 +7226,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -6985,6 +7267,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -7027,6 +7311,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -7105,6 +7391,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -7146,6 +7434,8 @@ 'capabilities': dict({ 'preset_modes': None, }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -7221,6 +7511,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -7292,6 +7584,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -7333,6 +7627,8 @@ 'capabilities': dict({ 'preset_modes': None, }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -7413,6 +7709,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -7462,6 +7760,8 @@ 'min_temp': 7, 'target_temp_step': 1.0, }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -7514,6 +7814,8 @@ 'capabilities': dict({ 'preset_modes': None, }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -7563,6 +7865,8 @@ 'fahrenheit', ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -7607,6 +7911,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -7650,6 +7956,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -7723,6 +8031,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -7798,6 +8108,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -7869,6 +8181,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -7912,6 +8226,8 @@ , ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -7958,6 +8274,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -8036,6 +8354,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -8075,6 +8395,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -8117,6 +8439,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -8191,6 +8515,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -8262,6 +8588,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -8301,6 +8629,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -8343,6 +8673,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -8421,6 +8753,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -8462,6 +8796,8 @@ 'capabilities': dict({ 'preset_modes': None, }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -8537,6 +8873,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -8608,6 +8946,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -8649,6 +8989,8 @@ 'capabilities': dict({ 'preset_modes': None, }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -8730,6 +9072,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -8801,6 +9145,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -8842,6 +9188,8 @@ 'capabilities': dict({ 'preset_modes': None, }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -8923,6 +9271,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -8976,6 +9326,8 @@ ]), 'target_temp_step': 1.0, }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -9033,6 +9385,8 @@ 'capabilities': dict({ 'preset_modes': None, }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -9082,6 +9436,8 @@ 'fahrenheit', ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -9126,6 +9482,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -9169,6 +9527,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -9242,6 +9602,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -9317,6 +9679,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -9388,6 +9752,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -9434,6 +9800,8 @@ 'max_humidity': 100, 'min_humidity': 0, }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -9485,6 +9853,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -9562,6 +9932,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -9633,6 +10005,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -9679,6 +10053,8 @@ 'max_humidity': 80, 'min_humidity': 20, }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -9730,6 +10106,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -9807,6 +10185,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -9878,6 +10258,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -9926,6 +10308,8 @@ , ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -9982,6 +10366,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -10060,6 +10446,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -10115,6 +10503,8 @@ 'min_temp': 18, 'target_temp_step': 0.5, }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -10175,6 +10565,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -10252,6 +10644,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -10299,6 +10693,8 @@ , ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -10384,6 +10780,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -10431,6 +10829,8 @@ , ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -10516,6 +10916,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -10563,6 +10965,8 @@ , ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -10648,6 +11052,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -10695,6 +11101,8 @@ , ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -10780,6 +11188,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -10827,6 +11237,8 @@ , ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -10922,6 +11334,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -10969,6 +11383,8 @@ , ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -11064,6 +11480,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -11107,6 +11525,8 @@ 'single_press', ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -11154,6 +11574,8 @@ 'single_press', ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -11201,6 +11623,8 @@ 'single_press', ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -11248,6 +11672,8 @@ 'single_press', ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -11293,6 +11719,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -11367,6 +11795,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -11410,6 +11840,8 @@ , ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -11486,6 +11918,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -11529,6 +11963,8 @@ , ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -11605,6 +12041,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -11648,6 +12086,8 @@ , ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -11724,6 +12164,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -11767,6 +12209,8 @@ , ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -11843,6 +12287,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -11886,6 +12332,8 @@ , ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -11962,6 +12410,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -12005,6 +12455,8 @@ , ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -12081,6 +12533,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -12124,6 +12578,8 @@ , ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -12200,6 +12656,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -12275,6 +12733,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -12323,6 +12783,8 @@ , ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -12413,6 +12875,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -12454,6 +12918,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -12495,6 +12961,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -12570,6 +13038,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -12611,6 +13081,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -12652,6 +13124,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -12690,6 +13164,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -12764,6 +13240,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -12812,6 +13290,8 @@ 'max_temp': 37, 'min_temp': 4.5, }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -12870,6 +13350,8 @@ 'fahrenheit', ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -12914,6 +13396,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -12957,6 +13441,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -13034,6 +13520,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -13083,6 +13571,8 @@ 'HDMI 4', ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -13133,6 +13623,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -13207,6 +13699,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -13248,6 +13742,8 @@ 'capabilities': dict({ 'preset_modes': None, }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -13323,6 +13819,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -13398,6 +13896,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -13437,6 +13937,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -13475,6 +13977,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -13513,6 +14017,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -13551,6 +14057,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -13589,6 +14097,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -13663,6 +14173,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -13706,6 +14218,8 @@ , ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -13786,6 +14300,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -13834,6 +14350,8 @@ 'max_temp': 35, 'min_temp': 7, }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -13889,6 +14407,8 @@ , ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -13938,6 +14458,8 @@ 'fahrenheit', ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -13982,6 +14504,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -14025,6 +14549,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -14102,6 +14628,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -14150,6 +14678,8 @@ , ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -14223,6 +14753,8 @@ 'sleepy', ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -14280,6 +14812,8 @@ 'router', ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -14364,6 +14898,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -14403,6 +14939,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -14442,6 +14980,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -14487,6 +15027,8 @@ 'long_press', ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -14532,6 +15074,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -14570,6 +15114,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -14644,6 +15190,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -14683,6 +15231,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -14722,6 +15272,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -14797,6 +15349,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -14838,6 +15392,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -14880,6 +15436,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -14923,6 +15481,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -14966,6 +15526,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -15009,6 +15571,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -15086,6 +15650,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -15125,6 +15691,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -15166,6 +15734,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -15207,6 +15777,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -15248,6 +15820,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -15289,6 +15863,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -15330,6 +15906,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -15371,6 +15949,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -15412,6 +15992,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -15489,6 +16071,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -15528,6 +16112,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -15570,6 +16156,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -15644,6 +16232,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -15715,6 +16305,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -15754,6 +16346,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -15796,6 +16390,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -15874,6 +16470,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -15913,6 +16511,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -15955,6 +16555,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -16029,6 +16631,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -16068,6 +16672,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -16110,6 +16716,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -16184,6 +16792,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -16223,6 +16833,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -16265,6 +16877,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -16339,6 +16953,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -16410,6 +17026,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -16449,6 +17067,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -16491,6 +17111,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -16569,6 +17191,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -16608,6 +17232,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -16683,6 +17309,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -16724,6 +17352,8 @@ 'capabilities': dict({ 'preset_modes': None, }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -16772,6 +17402,8 @@ , ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -16852,6 +17484,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -16923,6 +17557,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -16964,6 +17600,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -17007,6 +17645,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -17050,6 +17690,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -17123,6 +17765,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -17162,6 +17806,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -17239,6 +17885,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -17285,6 +17933,8 @@ 'max_humidity': 100, 'min_humidity': 0, }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -17343,6 +17993,8 @@ , ]), }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -17412,6 +18064,8 @@ 'mode': , 'step': 1, }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -17456,6 +18110,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -17533,6 +18189,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -17574,6 +18232,8 @@ 'capabilities': dict({ 'state_class': , }), + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, @@ -17615,6 +18275,8 @@ ]), 'area_id': None, 'capabilities': None, + 'categories': dict({ + }), 'config_entry_id': 'TestData', 'device_class': None, 'disabled_by': None, diff --git a/tests/components/homekit_controller/specific_devices/test_anker_eufycam.py b/tests/components/homekit_controller/specific_devices/test_anker_eufycam.py index 603036c00fd..43945af2fbf 100644 --- a/tests/components/homekit_controller/specific_devices/test_anker_eufycam.py +++ b/tests/components/homekit_controller/specific_devices/test_anker_eufycam.py @@ -1,4 +1,5 @@ """Test against characteristics captured from a eufycam.""" + from homeassistant.core import HomeAssistant from ..common import ( 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 338f2bc6e9f..b6848000943 100644 --- a/tests/components/homekit_controller/specific_devices/test_aqara_switch.py +++ b/tests/components/homekit_controller/specific_devices/test_aqara_switch.py @@ -5,6 +5,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, EntityCategory from homeassistant.core import HomeAssistant diff --git a/tests/components/homekit_controller/specific_devices/test_connectsense.py b/tests/components/homekit_controller/specific_devices/test_connectsense.py index 94a91bb0417..b3190c510fd 100644 --- a/tests/components/homekit_controller/specific_devices/test_connectsense.py +++ b/tests/components/homekit_controller/specific_devices/test_connectsense.py @@ -1,4 +1,5 @@ """Make sure that ConnectSense Smart Outlet2 / In-Wall Outlet is enumerated properly.""" + from homeassistant.components.sensor import SensorStateClass from homeassistant.const import UnitOfElectricCurrent, UnitOfEnergy, UnitOfPower from homeassistant.core import HomeAssistant diff --git a/tests/components/homekit_controller/specific_devices/test_cover_that_changes_features.py b/tests/components/homekit_controller/specific_devices/test_cover_that_changes_features.py index 87948c92214..2833346288a 100644 --- a/tests/components/homekit_controller/specific_devices/test_cover_that_changes_features.py +++ b/tests/components/homekit_controller/specific_devices/test_cover_that_changes_features.py @@ -1,6 +1,5 @@ """Test for a Home Assistant bridge that changes cover features at runtime.""" - from homeassistant.components.cover import CoverEntityFeature from homeassistant.const import ATTR_SUPPORTED_FEATURES from homeassistant.core import HomeAssistant diff --git a/tests/components/homekit_controller/specific_devices/test_ecobee3.py b/tests/components/homekit_controller/specific_devices/test_ecobee3.py index 99ece418c7b..3f93ca1a896 100644 --- a/tests/components/homekit_controller/specific_devices/test_ecobee3.py +++ b/tests/components/homekit_controller/specific_devices/test_ecobee3.py @@ -2,6 +2,7 @@ https://github.com/home-assistant/core/issues/15336 """ + from typing import Any from unittest import mock diff --git a/tests/components/homekit_controller/specific_devices/test_fan_that_changes_features.py b/tests/components/homekit_controller/specific_devices/test_fan_that_changes_features.py index 9921808c371..aea53e74d46 100644 --- a/tests/components/homekit_controller/specific_devices/test_fan_that_changes_features.py +++ b/tests/components/homekit_controller/specific_devices/test_fan_that_changes_features.py @@ -1,6 +1,5 @@ """Test for a Home Assistant bridge that changes fan features at runtime.""" - from homeassistant.components.fan import FanEntityFeature from homeassistant.const import ATTR_SUPPORTED_FEATURES from homeassistant.core import HomeAssistant diff --git a/tests/components/homekit_controller/specific_devices/test_heater_cooler_that_changes_features.py b/tests/components/homekit_controller/specific_devices/test_heater_cooler_that_changes_features.py index 5d0f63b07ff..e98bed4b0de 100644 --- a/tests/components/homekit_controller/specific_devices/test_heater_cooler_that_changes_features.py +++ b/tests/components/homekit_controller/specific_devices/test_heater_cooler_that_changes_features.py @@ -1,6 +1,5 @@ """Test for a Home Assistant bridge that changes climate features at runtime.""" - from homeassistant.components.climate import ATTR_SWING_MODES, ClimateEntityFeature from homeassistant.const import ATTR_SUPPORTED_FEATURES from homeassistant.core import HomeAssistant 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 f1f8b690384..61c4fd1d1da 100644 --- a/tests/components/homekit_controller/specific_devices/test_hue_bridge.py +++ b/tests/components/homekit_controller/specific_devices/test_hue_bridge.py @@ -1,4 +1,5 @@ """Tests for handling accessories on a Hue bridge via HomeKit.""" + from homeassistant.components.sensor import SensorStateClass from homeassistant.const import PERCENTAGE, EntityCategory from homeassistant.core import HomeAssistant diff --git a/tests/components/homekit_controller/specific_devices/test_humidifier_that_changes_value_range.py b/tests/components/homekit_controller/specific_devices/test_humidifier_that_changes_value_range.py index 518bcbbef38..2235b35a9a9 100644 --- a/tests/components/homekit_controller/specific_devices/test_humidifier_that_changes_value_range.py +++ b/tests/components/homekit_controller/specific_devices/test_humidifier_that_changes_value_range.py @@ -1,6 +1,5 @@ """Test for a Home Assistant bridge that changes humidifier min/max at runtime.""" - from homeassistant.components.humidifier import ATTR_MAX_HUMIDITY, ATTR_MIN_HUMIDITY from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er 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 baee3082106..9c6e5a6687a 100644 --- a/tests/components/homekit_controller/specific_devices/test_koogeek_ls1.py +++ b/tests/components/homekit_controller/specific_devices/test_koogeek_ls1.py @@ -1,4 +1,5 @@ """Make sure that existing Koogeek LS1 support isn't broken.""" + from datetime import timedelta from unittest import mock 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 7114d138039..0063bfc7f5b 100644 --- a/tests/components/homekit_controller/specific_devices/test_koogeek_sw2.py +++ b/tests/components/homekit_controller/specific_devices/test_koogeek_sw2.py @@ -4,6 +4,7 @@ This Koogeek device has a custom power sensor that extra handling. It should have 2 entities - the actual switch and a sensor for power usage. """ + from homeassistant.components.sensor import SensorStateClass from homeassistant.const import UnitOfPower from homeassistant.core import HomeAssistant diff --git a/tests/components/homekit_controller/specific_devices/test_light_that_changes_features.py b/tests/components/homekit_controller/specific_devices/test_light_that_changes_features.py index 4e62c75d8f2..d99c1fb2dba 100644 --- a/tests/components/homekit_controller/specific_devices/test_light_that_changes_features.py +++ b/tests/components/homekit_controller/specific_devices/test_light_that_changes_features.py @@ -1,6 +1,5 @@ """Test for a Home Assistant bridge that changes light features at runtime.""" - from homeassistant.components.light import ATTR_SUPPORTED_COLOR_MODES, ColorMode from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er diff --git a/tests/components/homekit_controller/specific_devices/test_netamo_doorbell.py b/tests/components/homekit_controller/specific_devices/test_netamo_doorbell.py index d65baf93884..9b85a111fc7 100644 --- a/tests/components/homekit_controller/specific_devices/test_netamo_doorbell.py +++ b/tests/components/homekit_controller/specific_devices/test_netamo_doorbell.py @@ -2,6 +2,7 @@ https://github.com/home-assistant/core/issues/44596 """ + from homeassistant.core import HomeAssistant from ..common import ( 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 b42a7652c1c..9bb06486e18 100644 --- a/tests/components/homekit_controller/specific_devices/test_vocolinc_vp3.py +++ b/tests/components/homekit_controller/specific_devices/test_vocolinc_vp3.py @@ -1,4 +1,5 @@ """Make sure that existing VOCOlinc VP3 support isn't broken.""" + from homeassistant.components.sensor import SensorStateClass from homeassistant.const import UnitOfPower from homeassistant.core import HomeAssistant diff --git a/tests/components/homekit_controller/test_alarm_control_panel.py b/tests/components/homekit_controller/test_alarm_control_panel.py index 19991d7cc13..a660e29ca17 100644 --- a/tests/components/homekit_controller/test_alarm_control_panel.py +++ b/tests/components/homekit_controller/test_alarm_control_panel.py @@ -1,4 +1,5 @@ """Basic checks for HomeKitalarm_control_panel.""" + from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.services import ServicesTypes diff --git a/tests/components/homekit_controller/test_binary_sensor.py b/tests/components/homekit_controller/test_binary_sensor.py index 92c303cab45..3d4486bb38d 100644 --- a/tests/components/homekit_controller/test_binary_sensor.py +++ b/tests/components/homekit_controller/test_binary_sensor.py @@ -1,4 +1,5 @@ """Basic checks for HomeKit motion sensors and contact sensors.""" + from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.services import ServicesTypes diff --git a/tests/components/homekit_controller/test_button.py b/tests/components/homekit_controller/test_button.py index 57592fb7a27..0d76ac98fbe 100644 --- a/tests/components/homekit_controller/test_button.py +++ b/tests/components/homekit_controller/test_button.py @@ -1,4 +1,5 @@ """Basic checks for HomeKit button.""" + from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.services import ServicesTypes diff --git a/tests/components/homekit_controller/test_camera.py b/tests/components/homekit_controller/test_camera.py index f74f2e62772..de64ee95d74 100644 --- a/tests/components/homekit_controller/test_camera.py +++ b/tests/components/homekit_controller/test_camera.py @@ -1,4 +1,5 @@ """Basic checks for HomeKit cameras.""" + import base64 from aiohomekit.model.services import ServicesTypes diff --git a/tests/components/homekit_controller/test_climate.py b/tests/components/homekit_controller/test_climate.py index e4fe754013a..5470c669700 100644 --- a/tests/components/homekit_controller/test_climate.py +++ b/tests/components/homekit_controller/test_climate.py @@ -1,4 +1,5 @@ """Basic checks for HomeKitclimate.""" + from aiohomekit.model.characteristics import ( ActivationStateValues, CharacteristicsTypes, diff --git a/tests/components/homekit_controller/test_config_flow.py b/tests/components/homekit_controller/test_config_flow.py index 2b375566b5b..6b658e9eef4 100644 --- a/tests/components/homekit_controller/test_config_flow.py +++ b/tests/components/homekit_controller/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for homekit_controller config flow.""" + import asyncio from ipaddress import ip_address import unittest.mock diff --git a/tests/components/homekit_controller/test_connection.py b/tests/components/homekit_controller/test_connection.py index d8382bdde86..0a77509d675 100644 --- a/tests/components/homekit_controller/test_connection.py +++ b/tests/components/homekit_controller/test_connection.py @@ -273,6 +273,7 @@ async def test_thread_provision( }, blocking=True, ) + await hass.async_block_till_done(wait_background_tasks=True) assert config_entry.data["Connection"] == "CoAP" diff --git a/tests/components/homekit_controller/test_cover.py b/tests/components/homekit_controller/test_cover.py index 7d004a8a428..671e9779d30 100644 --- a/tests/components/homekit_controller/test_cover.py +++ b/tests/components/homekit_controller/test_cover.py @@ -1,4 +1,5 @@ """Basic checks for HomeKitalarm_control_panel.""" + from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.services import ServicesTypes diff --git a/tests/components/homekit_controller/test_device_trigger.py b/tests/components/homekit_controller/test_device_trigger.py index 2f66a1eea26..40f565ec88b 100644 --- a/tests/components/homekit_controller/test_device_trigger.py +++ b/tests/components/homekit_controller/test_device_trigger.py @@ -1,4 +1,5 @@ """Test homekit_controller stateless triggers.""" + from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.services import ServicesTypes import pytest @@ -115,18 +116,18 @@ async def test_enumerate_remote( }, ] - for button in ("button1", "button2", "button3", "button4"): - for subtype in ("single_press", "double_press", "long_press"): - expected.append( - { - "device_id": device.id, - "domain": "homekit_controller", - "platform": "device", - "type": button, - "subtype": subtype, - "metadata": {}, - } - ) + expected.extend( + { + "device_id": device.id, + "domain": "homekit_controller", + "platform": "device", + "type": button, + "subtype": subtype, + "metadata": {}, + } + for button in ("button1", "button2", "button3", "button4") + for subtype in ("single_press", "double_press", "long_press") + ) triggers = await async_get_device_automations( hass, DeviceAutomationType.TRIGGER, device.id @@ -166,17 +167,17 @@ async def test_enumerate_button( }, ] - for subtype in ("single_press", "double_press", "long_press"): - expected.append( - { - "device_id": device.id, - "domain": "homekit_controller", - "platform": "device", - "type": "button1", - "subtype": subtype, - "metadata": {}, - } - ) + expected.extend( + { + "device_id": device.id, + "domain": "homekit_controller", + "platform": "device", + "type": "button1", + "subtype": subtype, + "metadata": {}, + } + for subtype in ("single_press", "double_press", "long_press") + ) triggers = await async_get_device_automations( hass, DeviceAutomationType.TRIGGER, device.id @@ -216,17 +217,17 @@ async def test_enumerate_doorbell( }, ] - for subtype in ("single_press", "double_press", "long_press"): - expected.append( - { - "device_id": device.id, - "domain": "homekit_controller", - "platform": "device", - "type": "doorbell", - "subtype": subtype, - "metadata": {}, - } - ) + expected.extend( + { + "device_id": device.id, + "domain": "homekit_controller", + "platform": "device", + "type": "doorbell", + "subtype": subtype, + "metadata": {}, + } + for subtype in ("single_press", "double_press", "long_press") + ) triggers = await async_get_device_automations( hass, DeviceAutomationType.TRIGGER, device.id diff --git a/tests/components/homekit_controller/test_diagnostics.py b/tests/components/homekit_controller/test_diagnostics.py index c0a9ebbb8d4..f79c875385d 100644 --- a/tests/components/homekit_controller/test_diagnostics.py +++ b/tests/components/homekit_controller/test_diagnostics.py @@ -27,7 +27,9 @@ async def test_config_entry( diag = await get_diagnostics_for_config_entry(hass, hass_client, config_entry) - assert diag == snapshot(exclude=props("last_updated", "last_changed")) + assert diag == snapshot( + exclude=props("last_changed", "last_reported", "last_updated") + ) async def test_device( @@ -45,4 +47,6 @@ async def test_device( diag = await get_diagnostics_for_device(hass, hass_client, config_entry, device) - assert diag == snapshot(exclude=props("last_updated", "last_changed")) + assert diag == snapshot( + exclude=props("last_changed", "last_reported", "last_updated") + ) diff --git a/tests/components/homekit_controller/test_event.py b/tests/components/homekit_controller/test_event.py index a836fb1c669..e139b49982a 100644 --- a/tests/components/homekit_controller/test_event.py +++ b/tests/components/homekit_controller/test_event.py @@ -1,4 +1,5 @@ """Test homekit_controller stateless triggers.""" + from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.services import ServicesTypes diff --git a/tests/components/homekit_controller/test_fan.py b/tests/components/homekit_controller/test_fan.py index 938f09c453e..428d3ab7d50 100644 --- a/tests/components/homekit_controller/test_fan.py +++ b/tests/components/homekit_controller/test_fan.py @@ -1,4 +1,5 @@ """Basic checks for HomeKit fans.""" + from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.services import ServicesTypes diff --git a/tests/components/homekit_controller/test_humidifier.py b/tests/components/homekit_controller/test_humidifier.py index 1a1db53d8dd..60c74be3949 100644 --- a/tests/components/homekit_controller/test_humidifier.py +++ b/tests/components/homekit_controller/test_humidifier.py @@ -1,4 +1,5 @@ """Basic checks for HomeKit Humidifier/Dehumidifier.""" + from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.services import ServicesTypes diff --git a/tests/components/homekit_controller/test_init.py b/tests/components/homekit_controller/test_init.py index 57d206a6025..ec3e6216288 100644 --- a/tests/components/homekit_controller/test_init.py +++ b/tests/components/homekit_controller/test_init.py @@ -1,4 +1,5 @@ """Tests for homekit_controller init.""" + from datetime import timedelta import pathlib from unittest.mock import patch @@ -264,6 +265,7 @@ async def test_snapshots( state_dict = dict(state.as_dict()) state_dict.pop("context", None) state_dict.pop("last_changed", None) + state_dict.pop("last_reported", None) state_dict.pop("last_updated", None) state_dict["attributes"] = dict(state_dict["attributes"]) diff --git a/tests/components/homekit_controller/test_light.py b/tests/components/homekit_controller/test_light.py index c7f168b2abe..606a9e75eb1 100644 --- a/tests/components/homekit_controller/test_light.py +++ b/tests/components/homekit_controller/test_light.py @@ -1,4 +1,5 @@ """Basic checks for HomeKitSwitch.""" + from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.services import ServicesTypes diff --git a/tests/components/homekit_controller/test_lock.py b/tests/components/homekit_controller/test_lock.py index 9aacda81683..db248b82b1a 100644 --- a/tests/components/homekit_controller/test_lock.py +++ b/tests/components/homekit_controller/test_lock.py @@ -1,4 +1,5 @@ """Basic checks for HomeKitLock.""" + from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.services import ServicesTypes diff --git a/tests/components/homekit_controller/test_media_player.py b/tests/components/homekit_controller/test_media_player.py index 1573fccea02..62a042ff7b9 100644 --- a/tests/components/homekit_controller/test_media_player.py +++ b/tests/components/homekit_controller/test_media_player.py @@ -1,4 +1,5 @@ """Basic checks for HomeKit motion sensors and contact sensors.""" + from aiohomekit.model.characteristics import ( CharacteristicPermissions, CharacteristicsTypes, diff --git a/tests/components/homekit_controller/test_number.py b/tests/components/homekit_controller/test_number.py index d35df281eab..96e2cbe8d4d 100644 --- a/tests/components/homekit_controller/test_number.py +++ b/tests/components/homekit_controller/test_number.py @@ -1,4 +1,5 @@ """Basic checks for HomeKit sensor.""" + from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.services import ServicesTypes diff --git a/tests/components/homekit_controller/test_select.py b/tests/components/homekit_controller/test_select.py index baae2cf8219..b00206e1b0d 100644 --- a/tests/components/homekit_controller/test_select.py +++ b/tests/components/homekit_controller/test_select.py @@ -1,4 +1,5 @@ """Basic checks for HomeKit select entities.""" + from aiohomekit.model import Accessory from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.characteristics.const import TemperatureDisplayUnits diff --git a/tests/components/homekit_controller/test_sensor.py b/tests/components/homekit_controller/test_sensor.py index 3134605125e..8634b33fe3b 100644 --- a/tests/components/homekit_controller/test_sensor.py +++ b/tests/components/homekit_controller/test_sensor.py @@ -1,4 +1,5 @@ """Basic checks for HomeKit sensor.""" + from unittest.mock import patch from aiohomekit.model import Transport diff --git a/tests/components/homekit_controller/test_storage.py b/tests/components/homekit_controller/test_storage.py index afab63983e2..9523dc9abb7 100644 --- a/tests/components/homekit_controller/test_storage.py +++ b/tests/components/homekit_controller/test_storage.py @@ -1,4 +1,5 @@ """Basic checks for entity map storage.""" + from typing import Any from aiohomekit.model.characteristics import CharacteristicsTypes diff --git a/tests/components/homekit_controller/test_switch.py b/tests/components/homekit_controller/test_switch.py index 5b6a77b75c9..8a6b2a65e88 100644 --- a/tests/components/homekit_controller/test_switch.py +++ b/tests/components/homekit_controller/test_switch.py @@ -1,4 +1,5 @@ """Basic checks for HomeKitSwitch.""" + from aiohomekit.model.characteristics import ( CharacteristicsTypes, InUseValues, diff --git a/tests/components/homekit_controller/test_utils.py b/tests/components/homekit_controller/test_utils.py index 57dd98669fb..703cf288f63 100644 --- a/tests/components/homekit_controller/test_utils.py +++ b/tests/components/homekit_controller/test_utils.py @@ -1,4 +1,5 @@ """Checks for basic helper utils.""" + from homeassistant.components.homekit_controller.utils import unique_id_to_iids diff --git a/tests/components/homematic/test_notify.py b/tests/components/homematic/test_notify.py index bccf5884d35..33c9b0f359e 100644 --- a/tests/components/homematic/test_notify.py +++ b/tests/components/homematic/test_notify.py @@ -1,4 +1,5 @@ """The tests for the Homematic notification platform.""" + import homeassistant.components.notify as notify_comp from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component diff --git a/tests/components/homematicip_cloud/conftest.py b/tests/components/homematicip_cloud/conftest.py index c033670efa6..88298521f75 100644 --- a/tests/components/homematicip_cloud/conftest.py +++ b/tests/components/homematicip_cloud/conftest.py @@ -1,4 +1,5 @@ """Initializer helpers for HomematicIP fake server.""" + from unittest.mock import AsyncMock, MagicMock, Mock, patch from homematicip.aio.auth import AsyncAuth @@ -144,12 +145,15 @@ def simple_mock_home_fixture(): def mock_connection_init_fixture(): """Return a simple mocked connection.""" - with patch( - "homeassistant.components.homematicip_cloud.hap.AsyncHome.init", - return_value=None, - ), patch( - "homeassistant.components.homematicip_cloud.hap.AsyncAuth.init", - return_value=None, + with ( + patch( + "homeassistant.components.homematicip_cloud.hap.AsyncHome.init", + return_value=None, + ), + patch( + "homeassistant.components.homematicip_cloud.hap.AsyncAuth.init", + return_value=None, + ), ): yield diff --git a/tests/fixtures/homematicip_cloud.json b/tests/components/homematicip_cloud/fixtures/homematicip_cloud.json similarity index 100% rename from tests/fixtures/homematicip_cloud.json rename to tests/components/homematicip_cloud/fixtures/homematicip_cloud.json diff --git a/tests/components/homematicip_cloud/helper.py b/tests/components/homematicip_cloud/helper.py index 7b4071e9808..4632b9107af 100644 --- a/tests/components/homematicip_cloud/helper.py +++ b/tests/components/homematicip_cloud/helper.py @@ -1,4 +1,5 @@ """Helper for HomematicIP Cloud Tests.""" + import json from unittest.mock import Mock, patch @@ -27,8 +28,7 @@ from tests.common import load_fixture HAPID = "3014F7110000000000000001" HAPPIN = "5678" AUTH_TOKEN = "1234" -HOME_JSON = "homematicip_cloud.json" -FIXTURE_DATA = load_fixture(HOME_JSON) +FIXTURE_DATA = load_fixture("homematicip_cloud.json", "homematicip_cloud") def get_and_check_entity_basics(hass, mock_hap, entity_id, entity_name, device_model): @@ -171,15 +171,9 @@ class HomeTemplate(Home): def _generate_mocks(self): """Generate mocks for groups and devices.""" - mock_devices = [] - for device in self.devices: - mock_devices.append(_get_mock(device)) - self.devices = mock_devices + self.devices = [_get_mock(device) for device in self.devices] - mock_groups = [] - for group in self.groups: - mock_groups.append(_get_mock(group)) - self.groups = mock_groups + self.groups = [_get_mock(group) for group in self.groups] def download_configuration(self): """Return the initial json config.""" diff --git a/tests/components/homematicip_cloud/test_alarm_control_panel.py b/tests/components/homematicip_cloud/test_alarm_control_panel.py index ddd32b7dc64..05d7963cea8 100644 --- a/tests/components/homematicip_cloud/test_alarm_control_panel.py +++ b/tests/components/homematicip_cloud/test_alarm_control_panel.py @@ -1,4 +1,5 @@ """Tests for HomematicIP Cloud alarm control panel.""" + from homeassistant.components.alarm_control_panel import ( DOMAIN as ALARM_CONTROL_PANEL_DOMAIN, ) diff --git a/tests/components/homematicip_cloud/test_binary_sensor.py b/tests/components/homematicip_cloud/test_binary_sensor.py index 07fcccc7479..54f8e2141d2 100644 --- a/tests/components/homematicip_cloud/test_binary_sensor.py +++ b/tests/components/homematicip_cloud/test_binary_sensor.py @@ -1,4 +1,5 @@ """Tests for HomematicIP Cloud binary sensor.""" + from homematicip.base.enums import SmokeDetectorAlarmType, WindowState from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN diff --git a/tests/components/homematicip_cloud/test_climate.py b/tests/components/homematicip_cloud/test_climate.py index 20193d91239..9ede89859dc 100644 --- a/tests/components/homematicip_cloud/test_climate.py +++ b/tests/components/homematicip_cloud/test_climate.py @@ -1,4 +1,5 @@ """Tests for HomematicIP Cloud climate.""" + import datetime from homematicip.base.enums import AbsenceType diff --git a/tests/components/homematicip_cloud/test_config_flow.py b/tests/components/homematicip_cloud/test_config_flow.py index 44f923b33df..4b0d1c26b8f 100644 --- a/tests/components/homematicip_cloud/test_config_flow.py +++ b/tests/components/homematicip_cloud/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for HomematicIP Cloud config flow.""" + from unittest.mock import patch from homeassistant import config_entries @@ -21,12 +22,15 @@ IMPORT_CONFIG = {HMIPC_HAPID: "ABC123", HMIPC_AUTHTOKEN: "123", HMIPC_NAME: "hmi async def test_flow_works(hass: HomeAssistant, simple_mock_home) -> None: """Test config flow.""" - with patch( - "homeassistant.components.homematicip_cloud.hap.HomematicipAuth.async_checkbutton", - return_value=False, - ), patch( - "homeassistant.components.homematicip_cloud.hap.HomematicipAuth.get_auth", - return_value=True, + with ( + patch( + "homeassistant.components.homematicip_cloud.hap.HomematicipAuth.async_checkbutton", + return_value=False, + ), + patch( + "homeassistant.components.homematicip_cloud.hap.HomematicipAuth.get_auth", + return_value=True, + ), ): result = await hass.config_entries.flow.async_init( HMIPC_DOMAIN, @@ -45,17 +49,22 @@ async def test_flow_works(hass: HomeAssistant, simple_mock_home) -> None: ) assert flow["context"]["unique_id"] == "ABC123" - with patch( - "homeassistant.components.homematicip_cloud.hap.HomematicipAuth.async_checkbutton", - return_value=True, - ), patch( - "homeassistant.components.homematicip_cloud.hap.HomematicipAuth.async_setup", - return_value=True, - ), patch( - "homeassistant.components.homematicip_cloud.hap.HomematicipAuth.async_register", - return_value=True, - ), patch( - "homeassistant.components.homematicip_cloud.hap.HomematicipHAP.async_connect", + with ( + patch( + "homeassistant.components.homematicip_cloud.hap.HomematicipAuth.async_checkbutton", + return_value=True, + ), + patch( + "homeassistant.components.homematicip_cloud.hap.HomematicipAuth.async_setup", + return_value=True, + ), + patch( + "homeassistant.components.homematicip_cloud.hap.HomematicipAuth.async_register", + return_value=True, + ), + patch( + "homeassistant.components.homematicip_cloud.hap.HomematicipHAP.async_connect", + ), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} @@ -85,15 +94,19 @@ async def test_flow_init_connection_error(hass: HomeAssistant) -> None: async def test_flow_link_connection_error(hass: HomeAssistant) -> None: """Test config flow client registration connection error.""" - with patch( - "homeassistant.components.homematicip_cloud.hap.HomematicipAuth.async_checkbutton", - return_value=True, - ), patch( - "homeassistant.components.homematicip_cloud.hap.HomematicipAuth.async_setup", - return_value=True, - ), patch( - "homeassistant.components.homematicip_cloud.hap.HomematicipAuth.async_register", - return_value=False, + with ( + patch( + "homeassistant.components.homematicip_cloud.hap.HomematicipAuth.async_checkbutton", + return_value=True, + ), + patch( + "homeassistant.components.homematicip_cloud.hap.HomematicipAuth.async_setup", + return_value=True, + ), + patch( + "homeassistant.components.homematicip_cloud.hap.HomematicipAuth.async_register", + return_value=False, + ), ): result = await hass.config_entries.flow.async_init( HMIPC_DOMAIN, @@ -107,12 +120,15 @@ async def test_flow_link_connection_error(hass: HomeAssistant) -> None: async def test_flow_link_press_button(hass: HomeAssistant) -> None: """Test config flow ask for pressing the blue button.""" - with patch( - "homeassistant.components.homematicip_cloud.hap.HomematicipAuth.async_checkbutton", - return_value=False, - ), patch( - "homeassistant.components.homematicip_cloud.hap.HomematicipAuth.async_setup", - return_value=True, + with ( + patch( + "homeassistant.components.homematicip_cloud.hap.HomematicipAuth.async_checkbutton", + return_value=False, + ), + patch( + "homeassistant.components.homematicip_cloud.hap.HomematicipAuth.async_setup", + return_value=True, + ), ): result = await hass.config_entries.flow.async_init( HMIPC_DOMAIN, @@ -154,17 +170,22 @@ async def test_init_already_configured(hass: HomeAssistant) -> None: async def test_import_config(hass: HomeAssistant, simple_mock_home) -> None: """Test importing a host with an existing config file.""" - with patch( - "homeassistant.components.homematicip_cloud.hap.HomematicipAuth.async_checkbutton", - return_value=True, - ), patch( - "homeassistant.components.homematicip_cloud.hap.HomematicipAuth.async_setup", - return_value=True, - ), patch( - "homeassistant.components.homematicip_cloud.hap.HomematicipAuth.async_register", - return_value=True, - ), patch( - "homeassistant.components.homematicip_cloud.hap.HomematicipHAP.async_connect", + with ( + patch( + "homeassistant.components.homematicip_cloud.hap.HomematicipAuth.async_checkbutton", + return_value=True, + ), + patch( + "homeassistant.components.homematicip_cloud.hap.HomematicipAuth.async_setup", + return_value=True, + ), + patch( + "homeassistant.components.homematicip_cloud.hap.HomematicipAuth.async_register", + return_value=True, + ), + patch( + "homeassistant.components.homematicip_cloud.hap.HomematicipHAP.async_connect", + ), ): result = await hass.config_entries.flow.async_init( HMIPC_DOMAIN, @@ -181,15 +202,19 @@ async def test_import_config(hass: HomeAssistant, simple_mock_home) -> None: async def test_import_existing_config(hass: HomeAssistant) -> None: """Test abort of an existing accesspoint from config.""" MockConfigEntry(domain=HMIPC_DOMAIN, unique_id="ABC123").add_to_hass(hass) - with patch( - "homeassistant.components.homematicip_cloud.hap.HomematicipAuth.async_checkbutton", - return_value=True, - ), patch( - "homeassistant.components.homematicip_cloud.hap.HomematicipAuth.async_setup", - return_value=True, - ), patch( - "homeassistant.components.homematicip_cloud.hap.HomematicipAuth.async_register", - return_value=True, + with ( + patch( + "homeassistant.components.homematicip_cloud.hap.HomematicipAuth.async_checkbutton", + return_value=True, + ), + patch( + "homeassistant.components.homematicip_cloud.hap.HomematicipAuth.async_setup", + return_value=True, + ), + patch( + "homeassistant.components.homematicip_cloud.hap.HomematicipAuth.async_register", + return_value=True, + ), ): result = await hass.config_entries.flow.async_init( HMIPC_DOMAIN, diff --git a/tests/components/homematicip_cloud/test_cover.py b/tests/components/homematicip_cloud/test_cover.py index 2c80c4e41a1..ee126dff936 100644 --- a/tests/components/homematicip_cloud/test_cover.py +++ b/tests/components/homematicip_cloud/test_cover.py @@ -1,4 +1,5 @@ """Tests for HomematicIP Cloud cover.""" + from homematicip.base.enums import DoorCommand, DoorState from homeassistant.components.cover import ( diff --git a/tests/components/homematicip_cloud/test_device.py b/tests/components/homematicip_cloud/test_device.py index a535d0ed6f0..9fc1f518c64 100644 --- a/tests/components/homematicip_cloud/test_device.py +++ b/tests/components/homematicip_cloud/test_device.py @@ -1,4 +1,5 @@ """Common tests for HomematicIP devices.""" + from unittest.mock import patch from homematicip.base.enums import EventType @@ -96,13 +97,16 @@ async def test_hmip_add_device( assert len(mock_hap.hmip_device_by_entity_id) == pre_mapping_count - 3 reloaded_hap = HomematicipHAP(hass, hmip_config_entry) - with patch( - "homeassistant.components.homematicip_cloud.HomematicipHAP", - return_value=reloaded_hap, - ), patch.object(reloaded_hap, "async_connect"), patch.object( - reloaded_hap, "get_hap", return_value=mock_hap.home - ), patch( - "homeassistant.components.homematicip_cloud.hap.asyncio.sleep", + with ( + patch( + "homeassistant.components.homematicip_cloud.HomematicipHAP", + return_value=reloaded_hap, + ), + patch.object(reloaded_hap, "async_connect"), + patch.object(reloaded_hap, "get_hap", return_value=mock_hap.home), + patch( + "homeassistant.components.homematicip_cloud.hap.asyncio.sleep", + ), ): mock_hap.home.fire_create_event(event_type=EventType.DEVICE_ADDED) await hass.async_block_till_done() diff --git a/tests/components/homematicip_cloud/test_hap.py b/tests/components/homematicip_cloud/test_hap.py index 0d950968191..cddade7cec5 100644 --- a/tests/components/homematicip_cloud/test_hap.py +++ b/tests/components/homematicip_cloud/test_hap.py @@ -1,4 +1,5 @@ """Test HomematicIP Cloud accesspoint.""" + from unittest.mock import Mock, patch from homematicip.aio.auth import AsyncAuth @@ -48,13 +49,13 @@ async def test_auth_auth_check_and_register(hass: HomeAssistant) -> None: hmip_auth = HomematicipAuth(hass, config) hmip_auth.auth = Mock(spec=AsyncAuth) - with patch.object( - hmip_auth.auth, "isRequestAcknowledged", return_value=True - ), patch.object( - hmip_auth.auth, "requestAuthToken", return_value="ABC" - ), patch.object( - hmip_auth.auth, - "confirmAuthToken", + with ( + patch.object(hmip_auth.auth, "isRequestAcknowledged", return_value=True), + patch.object(hmip_auth.auth, "requestAuthToken", return_value="ABC"), + patch.object( + hmip_auth.auth, + "confirmAuthToken", + ), ): assert await hmip_auth.async_checkbutton() assert await hmip_auth.async_register() == "ABC" @@ -65,10 +66,13 @@ async def test_auth_auth_check_and_register_with_exception(hass: HomeAssistant) config = {HMIPC_HAPID: "ABC123", HMIPC_PIN: "123", HMIPC_NAME: "hmip"} hmip_auth = HomematicipAuth(hass, config) hmip_auth.auth = Mock(spec=AsyncAuth) - with patch.object( - hmip_auth.auth, "isRequestAcknowledged", side_effect=HmipConnectionError - ), patch.object( - hmip_auth.auth, "requestAuthToken", side_effect=HmipConnectionError + with ( + patch.object( + hmip_auth.auth, "isRequestAcknowledged", side_effect=HmipConnectionError + ), + patch.object( + hmip_auth.auth, "requestAuthToken", side_effect=HmipConnectionError + ), ): assert not await hmip_auth.async_checkbutton() assert await hmip_auth.async_register() is False @@ -95,12 +99,13 @@ async def test_hap_setup_connection_error() -> None: entry = Mock() entry.data = {HMIPC_HAPID: "ABC123", HMIPC_AUTHTOKEN: "123", HMIPC_NAME: "hmip"} hap = HomematicipHAP(hass, entry) - with patch.object(hap, "get_hap", side_effect=HmipcConnectionError), pytest.raises( - ConfigEntryNotReady + with ( + patch.object(hap, "get_hap", side_effect=HmipcConnectionError), + pytest.raises(ConfigEntryNotReady), ): assert not await hap.async_setup() - assert not hass.async_add_job.mock_calls + assert not hass.async_add_hass_job.mock_calls assert not hass.config_entries.flow.async_init.mock_calls @@ -145,10 +150,13 @@ async def test_hap_create_exception( ): assert not await hap.async_setup() - with patch( - "homeassistant.components.homematicip_cloud.hap.AsyncHome.get_current_state", - side_effect=HmipConnectionError, - ), pytest.raises(ConfigEntryNotReady): + with ( + patch( + "homeassistant.components.homematicip_cloud.hap.AsyncHome.get_current_state", + side_effect=HmipConnectionError, + ), + pytest.raises(ConfigEntryNotReady), + ): await hap.async_setup() diff --git a/tests/components/homematicip_cloud/test_init.py b/tests/components/homematicip_cloud/test_init.py index 5b9472d329b..9303a755e89 100644 --- a/tests/components/homematicip_cloud/test_init.py +++ b/tests/components/homematicip_cloud/test_init.py @@ -1,4 +1,5 @@ """Test HomematicIP Cloud setup process.""" + from unittest.mock import AsyncMock, Mock, patch from homematicip.base.base_connection import HmipConnectionError @@ -120,11 +121,14 @@ async def test_load_entry_fails_due_to_generic_exception( """Test load entry fails due to generic exception.""" hmip_config_entry.add_to_hass(hass) - with patch( - "homeassistant.components.homematicip_cloud.hap.AsyncHome.get_current_state", - side_effect=Exception, - ), patch( - "homematicip.aio.connection.AsyncConnection.init", + with ( + patch( + "homeassistant.components.homematicip_cloud.hap.AsyncHome.get_current_state", + side_effect=Exception, + ), + patch( + "homematicip.aio.connection.AsyncConnection.init", + ), ): assert await async_setup_component(hass, HMIPC_DOMAIN, {}) diff --git a/tests/components/homematicip_cloud/test_light.py b/tests/components/homematicip_cloud/test_light.py index 517978e74c0..18f002a5dbc 100644 --- a/tests/components/homematicip_cloud/test_light.py +++ b/tests/components/homematicip_cloud/test_light.py @@ -1,4 +1,5 @@ """Tests for HomematicIP Cloud light.""" + from homematicip.base.enums import RGBColorState from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN diff --git a/tests/components/homematicip_cloud/test_lock.py b/tests/components/homematicip_cloud/test_lock.py index 61457fd5119..f49ad42b013 100644 --- a/tests/components/homematicip_cloud/test_lock.py +++ b/tests/components/homematicip_cloud/test_lock.py @@ -1,4 +1,5 @@ """Tests for HomematicIP Cloud locks.""" + from unittest.mock import patch from homematicip.base.enums import LockState, MotorState diff --git a/tests/components/homematicip_cloud/test_sensor.py b/tests/components/homematicip_cloud/test_sensor.py index 908a881878b..3089bb062e5 100644 --- a/tests/components/homematicip_cloud/test_sensor.py +++ b/tests/components/homematicip_cloud/test_sensor.py @@ -1,4 +1,5 @@ """Tests for HomematicIP Cloud sensor.""" + from homematicip.base.enums import ValveState from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN diff --git a/tests/components/homematicip_cloud/test_switch.py b/tests/components/homematicip_cloud/test_switch.py index d8f58159e06..a249c52393d 100644 --- a/tests/components/homematicip_cloud/test_switch.py +++ b/tests/components/homematicip_cloud/test_switch.py @@ -1,4 +1,5 @@ """Tests for HomematicIP Cloud switch.""" + from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN from homeassistant.components.homematicip_cloud.generic_entity import ( ATTR_GROUP_MEMBER_UNREACHABLE, diff --git a/tests/components/homematicip_cloud/test_weather.py b/tests/components/homematicip_cloud/test_weather.py index b0cd0bde923..44005afd511 100644 --- a/tests/components/homematicip_cloud/test_weather.py +++ b/tests/components/homematicip_cloud/test_weather.py @@ -1,4 +1,5 @@ """Tests for HomematicIP Cloud weather.""" + from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN from homeassistant.components.weather import ( ATTR_WEATHER_HUMIDITY, diff --git a/tests/components/homewizard/conftest.py b/tests/components/homewizard/conftest.py index 0c24d9daebe..bc661da390d 100644 --- a/tests/components/homewizard/conftest.py +++ b/tests/components/homewizard/conftest.py @@ -1,4 +1,5 @@ """Fixtures for HomeWizard integration tests.""" + from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch @@ -24,12 +25,15 @@ def mock_homewizardenergy( device_fixture: str, ) -> MagicMock: """Return a mock bridge.""" - with patch( - "homeassistant.components.homewizard.coordinator.HomeWizardEnergy", - autospec=True, - ) as homewizard, patch( - "homeassistant.components.homewizard.config_flow.HomeWizardEnergy", - new=homewizard, + with ( + patch( + "homeassistant.components.homewizard.coordinator.HomeWizardEnergy", + autospec=True, + ) as homewizard, + patch( + "homeassistant.components.homewizard.config_flow.HomeWizardEnergy", + new=homewizard, + ), ): client = homewizard.return_value diff --git a/tests/components/homewizard/fixtures/HWE-SKT/data.json b/tests/components/homewizard/fixtures/HWE-SKT-11/data.json similarity index 100% rename from tests/components/homewizard/fixtures/HWE-SKT/data.json rename to tests/components/homewizard/fixtures/HWE-SKT-11/data.json diff --git a/tests/components/homewizard/fixtures/HWE-SKT/device.json b/tests/components/homewizard/fixtures/HWE-SKT-11/device.json similarity index 100% rename from tests/components/homewizard/fixtures/HWE-SKT/device.json rename to tests/components/homewizard/fixtures/HWE-SKT-11/device.json diff --git a/tests/components/homewizard/fixtures/HWE-SKT/state.json b/tests/components/homewizard/fixtures/HWE-SKT-11/state.json similarity index 100% rename from tests/components/homewizard/fixtures/HWE-SKT/state.json rename to tests/components/homewizard/fixtures/HWE-SKT-11/state.json diff --git a/tests/components/homewizard/fixtures/HWE-SKT/system.json b/tests/components/homewizard/fixtures/HWE-SKT-11/system.json similarity index 100% rename from tests/components/homewizard/fixtures/HWE-SKT/system.json rename to tests/components/homewizard/fixtures/HWE-SKT-11/system.json diff --git a/tests/components/homewizard/fixtures/HWE-SKT-21/data.json b/tests/components/homewizard/fixtures/HWE-SKT-21/data.json new file mode 100644 index 00000000000..5b68ae0090a --- /dev/null +++ b/tests/components/homewizard/fixtures/HWE-SKT-21/data.json @@ -0,0 +1,16 @@ +{ + "wifi_ssid": "My Wi-Fi", + "wifi_strength": 100, + "total_power_import_kwh": 30.511, + "total_power_import_t1_kwh": 30.511, + "total_power_export_kwh": 85.951, + "total_power_export_t1_kwh": 85.951, + "active_power_w": 543.312, + "active_power_l1_w": 543.312, + "active_voltage_v": 231.539, + "active_current_a": 2.346, + "active_reactive_power_var": 123.456, + "active_apparent_power_va": 666.768, + "active_power_factor": 0.81688, + "active_frequency_hz": 50.005 +} diff --git a/tests/components/homewizard/fixtures/HWE-SKT-21/device.json b/tests/components/homewizard/fixtures/HWE-SKT-21/device.json new file mode 100644 index 00000000000..69b5947351f --- /dev/null +++ b/tests/components/homewizard/fixtures/HWE-SKT-21/device.json @@ -0,0 +1,7 @@ +{ + "product_type": "HWE-SKT", + "product_name": "Energy Socket", + "serial": "3c39e7aabbcc", + "firmware_version": "4.07", + "api_version": "v1" +} diff --git a/tests/components/homewizard/fixtures/HWE-SKT-21/state.json b/tests/components/homewizard/fixtures/HWE-SKT-21/state.json new file mode 100644 index 00000000000..bbc0242ed58 --- /dev/null +++ b/tests/components/homewizard/fixtures/HWE-SKT-21/state.json @@ -0,0 +1,5 @@ +{ + "power_on": true, + "switch_lock": false, + "brightness": 255 +} diff --git a/tests/components/homewizard/fixtures/HWE-SKT-21/system.json b/tests/components/homewizard/fixtures/HWE-SKT-21/system.json new file mode 100644 index 00000000000..362491b3519 --- /dev/null +++ b/tests/components/homewizard/fixtures/HWE-SKT-21/system.json @@ -0,0 +1,3 @@ +{ + "cloud_enabled": true +} diff --git a/tests/components/homewizard/fixtures/HWE-WTR/system.json b/tests/components/homewizard/fixtures/HWE-WTR/system.json new file mode 100644 index 00000000000..362491b3519 --- /dev/null +++ b/tests/components/homewizard/fixtures/HWE-WTR/system.json @@ -0,0 +1,3 @@ +{ + "cloud_enabled": true +} diff --git a/tests/components/homewizard/snapshots/test_button.ambr b/tests/components/homewizard/snapshots/test_button.ambr index 4bcda8f38ac..5ab108d344c 100644 --- a/tests/components/homewizard/snapshots/test_button.ambr +++ b/tests/components/homewizard/snapshots/test_button.ambr @@ -8,6 +8,7 @@ 'context': , 'entity_id': 'button.device_identify', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }) diff --git a/tests/components/homewizard/snapshots/test_diagnostics.ambr b/tests/components/homewizard/snapshots/test_diagnostics.ambr index 9e3a468d58f..ed744083373 100644 --- a/tests/components/homewizard/snapshots/test_diagnostics.ambr +++ b/tests/components/homewizard/snapshots/test_diagnostics.ambr @@ -299,7 +299,7 @@ }), }) # --- -# name: test_diagnostics[HWE-SKT] +# name: test_diagnostics[HWE-SKT-11] dict({ 'data': dict({ 'data': dict({ @@ -386,6 +386,93 @@ }), }) # --- +# name: test_diagnostics[HWE-SKT-21] + dict({ + 'data': dict({ + 'data': dict({ + 'active_apparent_power_l1_va': None, + 'active_apparent_power_l2_va': None, + 'active_apparent_power_l3_va': None, + 'active_apparent_power_va': 666.768, + 'active_current_a': 2.346, + 'active_current_l1_a': None, + 'active_current_l2_a': None, + 'active_current_l3_a': None, + 'active_frequency_hz': 50.005, + 'active_liter_lpm': None, + 'active_power_average_w': None, + 'active_power_factor': 0.81688, + 'active_power_factor_l1': None, + 'active_power_factor_l2': None, + 'active_power_factor_l3': None, + 'active_power_l1_w': 543.312, + 'active_power_l2_w': None, + 'active_power_l3_w': None, + 'active_power_w': 543.312, + 'active_reactive_power_l1_var': None, + 'active_reactive_power_l2_var': None, + 'active_reactive_power_l3_var': None, + 'active_reactive_power_var': 123.456, + 'active_tariff': None, + 'active_voltage_l1_v': None, + 'active_voltage_l2_v': None, + 'active_voltage_l3_v': None, + 'active_voltage_v': 231.539, + 'any_power_fail_count': None, + 'external_devices': None, + 'gas_timestamp': None, + 'gas_unique_id': None, + 'long_power_fail_count': None, + 'meter_model': None, + 'monthly_power_peak_timestamp': None, + 'monthly_power_peak_w': None, + 'smr_version': None, + 'total_energy_export_kwh': 85.951, + 'total_energy_export_t1_kwh': 85.951, + 'total_energy_export_t2_kwh': None, + 'total_energy_export_t3_kwh': None, + 'total_energy_export_t4_kwh': None, + 'total_energy_import_kwh': 30.511, + 'total_energy_import_t1_kwh': 30.511, + 'total_energy_import_t2_kwh': None, + 'total_energy_import_t3_kwh': None, + 'total_energy_import_t4_kwh': None, + 'total_gas_m3': None, + 'total_liter_m3': None, + 'unique_meter_id': None, + 'voltage_sag_l1_count': None, + 'voltage_sag_l2_count': None, + 'voltage_sag_l3_count': None, + 'voltage_swell_l1_count': None, + 'voltage_swell_l2_count': None, + 'voltage_swell_l3_count': None, + 'wifi_ssid': '**REDACTED**', + 'wifi_strength': 100, + }), + 'device': dict({ + 'api_version': 'v1', + 'firmware_version': '4.07', + 'product_name': 'Energy Socket', + 'product_type': 'HWE-SKT', + 'serial': '**REDACTED**', + }), + 'state': dict({ + 'brightness': 255, + 'power_on': True, + 'switch_lock': False, + }), + 'system': dict({ + 'cloud_enabled': True, + }), + }), + 'entry': dict({ + 'ip_address': '**REDACTED**', + 'product_name': 'Product name', + 'product_type': 'product_type', + 'serial': '**REDACTED**', + }), + }) +# --- # name: test_diagnostics[HWE-WTR] dict({ 'data': dict({ @@ -457,7 +544,9 @@ 'serial': '**REDACTED**', }), 'state': None, - 'system': None, + 'system': dict({ + 'cloud_enabled': True, + }), }), 'entry': dict({ 'ip_address': '**REDACTED**', diff --git a/tests/components/homewizard/snapshots/test_number.ambr b/tests/components/homewizard/snapshots/test_number.ambr index 4ac6525dd72..a9c9e45098d 100644 --- a/tests/components/homewizard/snapshots/test_number.ambr +++ b/tests/components/homewizard/snapshots/test_number.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_number_entities[HWE-SKT] +# name: test_number_entities[HWE-SKT-11] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Device Status light brightness', @@ -12,11 +12,12 @@ 'context': , 'entity_id': 'number.device_status_light_brightness', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '100.0', }) # --- -# name: test_number_entities[HWE-SKT].1 +# name: test_number_entities[HWE-SKT-11].1 EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -54,7 +55,7 @@ 'unit_of_measurement': '%', }) # --- -# name: test_number_entities[HWE-SKT].2 +# name: test_number_entities[HWE-SKT-11].2 DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , @@ -88,3 +89,93 @@ 'via_device_id': None, }) # --- +# name: test_number_entities[HWE-SKT-21] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Device Status light brightness', + 'max': 100.0, + 'min': 0.0, + 'mode': , + 'step': 1.0, + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'number.device_status_light_brightness', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '100.0', + }) +# --- +# name: test_number_entities[HWE-SKT-21].1 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 100.0, + 'min': 0.0, + 'mode': , + 'step': 1.0, + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.device_status_light_brightness', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Status light brightness', + 'platform': 'homewizard', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'status_light_brightness', + 'unique_id': 'aabbccddeeff_status_light_brightness', + 'unit_of_measurement': '%', + }) +# --- +# name: test_number_entities[HWE-SKT-21].2 + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'configuration_url': None, + 'connections': set({ + tuple( + 'mac', + '3c:39:e7:aa:bb:cc', + ), + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'homewizard', + '3c39e7aabbcc', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'HomeWizard', + 'model': 'HWE-SKT', + 'name': 'Device', + 'name_by_user': None, + 'serial_number': None, + 'suggested_area': None, + 'sw_version': '4.07', + 'via_device_id': None, + }) +# --- diff --git a/tests/components/homewizard/snapshots/test_sensor.ambr b/tests/components/homewizard/snapshots/test_sensor.ambr index 967b4e5fda7..0503085b7e6 100644 --- a/tests/components/homewizard/snapshots/test_sensor.ambr +++ b/tests/components/homewizard/snapshots/test_sensor.ambr @@ -1,190 +1,4 @@ # serializer version: 1 -# name: test_gas_meter_migrated[HWE-P1-unique_ids0][sensor.homewizard_aabbccddeeff_gas_unique_id:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.homewizard_aabbccddeeff_gas_unique_id', - 'has_entity_name': False, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': None, - 'platform': 'homewizard', - 'previous_unique_id': 'aabbccddeeff_gas_unique_id', - 'supported_features': 0, - 'translation_key': None, - 'unique_id': 'homewizard_01FFEEDDCCBBAA99887766554433221100_meter_identifier', - 'unit_of_measurement': None, - }) -# --- -# name: test_gas_meter_migrated[HWE-P1-unique_ids0][sensor.homewizard_aabbccddeeff_total_gas_m3:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.homewizard_aabbccddeeff_total_gas_m3', - 'has_entity_name': False, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': None, - 'platform': 'homewizard', - 'previous_unique_id': 'aabbccddeeff_total_gas_m3', - 'supported_features': 0, - 'translation_key': None, - 'unique_id': 'homewizard_01FFEEDDCCBBAA99887766554433221100', - 'unit_of_measurement': None, - }) -# --- -# name: test_gas_meter_migrated[aabbccddeeff_gas_unique_id][sensor.homewizard_a:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.homewizard_a', - 'has_entity_name': False, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': None, - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': 'a', - 'unit_of_measurement': None, - }) -# --- -# name: test_gas_meter_migrated[aabbccddeeff_gas_unique_id][sensor.homewizard_aabbccddeeff_gas_unique_id:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.homewizard_aabbccddeeff_gas_unique_id', - 'has_entity_name': False, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': None, - 'platform': 'homewizard', - 'previous_unique_id': 'aabbccddeeff_gas_unique_id', - 'supported_features': 0, - 'translation_key': None, - 'unique_id': 'homewizard_01FFEEDDCCBBAA99887766554433221100_meter_identifier', - 'unit_of_measurement': None, - }) -# --- -# name: test_gas_meter_migrated[aabbccddeeff_total_gas_m3][sensor.homewizard_a:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.homewizard_a', - 'has_entity_name': False, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': None, - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': 'a', - 'unit_of_measurement': None, - }) -# --- -# name: test_gas_meter_migrated[aabbccddeeff_total_gas_m3][sensor.homewizard_aabbccddeeff_total_gas_m3:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.homewizard_aabbccddeeff_total_gas_m3', - 'has_entity_name': False, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': None, - 'platform': 'homewizard', - 'previous_unique_id': 'aabbccddeeff_total_gas_m3', - 'supported_features': 0, - 'translation_key': None, - 'unique_id': 'homewizard_01FFEEDDCCBBAA99887766554433221100', - 'unit_of_measurement': None, - }) -# --- # name: test_gas_meter_migrated[sensor.homewizard_aabbccddeeff_total_gas_m3:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -218,7 +32,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_sensors[HWE-KWH1-entity_ids6][sensor.device_apparent_power:device-registry] +# name: test_sensors[HWE-KWH1-entity_ids7][sensor.device_apparent_power:device-registry] DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , @@ -252,7 +66,7 @@ 'via_device_id': None, }) # --- -# name: test_sensors[HWE-KWH1-entity_ids6][sensor.device_apparent_power:entity-registry] +# name: test_sensors[HWE-KWH1-entity_ids7][sensor.device_apparent_power:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -287,7 +101,7 @@ 'unit_of_measurement': , }) # --- -# name: test_sensors[HWE-KWH1-entity_ids6][sensor.device_apparent_power:state] +# name: test_sensors[HWE-KWH1-entity_ids7][sensor.device_apparent_power:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'apparent_power', @@ -298,11 +112,12 @@ 'context': , 'entity_id': 'sensor.device_apparent_power', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '74.052', }) # --- -# name: test_sensors[HWE-KWH1-entity_ids6][sensor.device_current:device-registry] +# name: test_sensors[HWE-KWH1-entity_ids7][sensor.device_current:device-registry] DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , @@ -336,7 +151,7 @@ 'via_device_id': None, }) # --- -# name: test_sensors[HWE-KWH1-entity_ids6][sensor.device_current:entity-registry] +# name: test_sensors[HWE-KWH1-entity_ids7][sensor.device_current:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -371,7 +186,7 @@ 'unit_of_measurement': , }) # --- -# name: test_sensors[HWE-KWH1-entity_ids6][sensor.device_current:state] +# name: test_sensors[HWE-KWH1-entity_ids7][sensor.device_current:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'current', @@ -382,11 +197,12 @@ 'context': , 'entity_id': 'sensor.device_current', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0.273', }) # --- -# name: test_sensors[HWE-KWH1-entity_ids6][sensor.device_energy_export:device-registry] +# name: test_sensors[HWE-KWH1-entity_ids7][sensor.device_energy_export:device-registry] DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , @@ -420,7 +236,7 @@ 'via_device_id': None, }) # --- -# name: test_sensors[HWE-KWH1-entity_ids6][sensor.device_energy_export:entity-registry] +# name: test_sensors[HWE-KWH1-entity_ids7][sensor.device_energy_export:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -455,7 +271,7 @@ 'unit_of_measurement': , }) # --- -# name: test_sensors[HWE-KWH1-entity_ids6][sensor.device_energy_export:state] +# name: test_sensors[HWE-KWH1-entity_ids7][sensor.device_energy_export:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'energy', @@ -466,11 +282,12 @@ 'context': , 'entity_id': 'sensor.device_energy_export', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '255.551', }) # --- -# name: test_sensors[HWE-KWH1-entity_ids6][sensor.device_energy_import:device-registry] +# name: test_sensors[HWE-KWH1-entity_ids7][sensor.device_energy_import:device-registry] DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , @@ -504,7 +321,7 @@ 'via_device_id': None, }) # --- -# name: test_sensors[HWE-KWH1-entity_ids6][sensor.device_energy_import:entity-registry] +# name: test_sensors[HWE-KWH1-entity_ids7][sensor.device_energy_import:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -539,7 +356,7 @@ 'unit_of_measurement': , }) # --- -# name: test_sensors[HWE-KWH1-entity_ids6][sensor.device_energy_import:state] +# name: test_sensors[HWE-KWH1-entity_ids7][sensor.device_energy_import:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'energy', @@ -550,11 +367,12 @@ 'context': , 'entity_id': 'sensor.device_energy_import', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '2.705', }) # --- -# name: test_sensors[HWE-KWH1-entity_ids6][sensor.device_frequency:device-registry] +# name: test_sensors[HWE-KWH1-entity_ids7][sensor.device_frequency:device-registry] DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , @@ -588,7 +406,7 @@ 'via_device_id': None, }) # --- -# name: test_sensors[HWE-KWH1-entity_ids6][sensor.device_frequency:entity-registry] +# name: test_sensors[HWE-KWH1-entity_ids7][sensor.device_frequency:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -623,7 +441,7 @@ 'unit_of_measurement': , }) # --- -# name: test_sensors[HWE-KWH1-entity_ids6][sensor.device_frequency:state] +# name: test_sensors[HWE-KWH1-entity_ids7][sensor.device_frequency:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'frequency', @@ -634,11 +452,12 @@ 'context': , 'entity_id': 'sensor.device_frequency', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '50', }) # --- -# name: test_sensors[HWE-KWH1-entity_ids6][sensor.device_power:device-registry] +# name: test_sensors[HWE-KWH1-entity_ids7][sensor.device_power:device-registry] DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , @@ -672,7 +491,7 @@ 'via_device_id': None, }) # --- -# name: test_sensors[HWE-KWH1-entity_ids6][sensor.device_power:entity-registry] +# name: test_sensors[HWE-KWH1-entity_ids7][sensor.device_power:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -710,7 +529,7 @@ 'unit_of_measurement': , }) # --- -# name: test_sensors[HWE-KWH1-entity_ids6][sensor.device_power:state] +# name: test_sensors[HWE-KWH1-entity_ids7][sensor.device_power:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'power', @@ -721,11 +540,12 @@ 'context': , 'entity_id': 'sensor.device_power', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '-1058.296', }) # --- -# name: test_sensors[HWE-KWH1-entity_ids6][sensor.device_power_factor:device-registry] +# name: test_sensors[HWE-KWH1-entity_ids7][sensor.device_power_factor:device-registry] DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , @@ -759,7 +579,7 @@ 'via_device_id': None, }) # --- -# name: test_sensors[HWE-KWH1-entity_ids6][sensor.device_power_factor:entity-registry] +# name: test_sensors[HWE-KWH1-entity_ids7][sensor.device_power_factor:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -794,7 +614,7 @@ 'unit_of_measurement': '%', }) # --- -# name: test_sensors[HWE-KWH1-entity_ids6][sensor.device_power_factor:state] +# name: test_sensors[HWE-KWH1-entity_ids7][sensor.device_power_factor:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'power_factor', @@ -805,11 +625,12 @@ 'context': , 'entity_id': 'sensor.device_power_factor', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '61.1', }) # --- -# name: test_sensors[HWE-KWH1-entity_ids6][sensor.device_reactive_power:device-registry] +# name: test_sensors[HWE-KWH1-entity_ids7][sensor.device_reactive_power:device-registry] DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , @@ -843,7 +664,7 @@ 'via_device_id': None, }) # --- -# name: test_sensors[HWE-KWH1-entity_ids6][sensor.device_reactive_power:entity-registry] +# name: test_sensors[HWE-KWH1-entity_ids7][sensor.device_reactive_power:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -878,7 +699,7 @@ 'unit_of_measurement': 'var', }) # --- -# name: test_sensors[HWE-KWH1-entity_ids6][sensor.device_reactive_power:state] +# name: test_sensors[HWE-KWH1-entity_ids7][sensor.device_reactive_power:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'reactive_power', @@ -889,11 +710,12 @@ 'context': , 'entity_id': 'sensor.device_reactive_power', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '-58.612', }) # --- -# name: test_sensors[HWE-KWH1-entity_ids6][sensor.device_voltage:device-registry] +# name: test_sensors[HWE-KWH1-entity_ids7][sensor.device_voltage:device-registry] DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , @@ -927,7 +749,7 @@ 'via_device_id': None, }) # --- -# name: test_sensors[HWE-KWH1-entity_ids6][sensor.device_voltage:entity-registry] +# name: test_sensors[HWE-KWH1-entity_ids7][sensor.device_voltage:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -962,7 +784,7 @@ 'unit_of_measurement': , }) # --- -# name: test_sensors[HWE-KWH1-entity_ids6][sensor.device_voltage:state] +# name: test_sensors[HWE-KWH1-entity_ids7][sensor.device_voltage:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'voltage', @@ -973,11 +795,12 @@ 'context': , 'entity_id': 'sensor.device_voltage', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '228.472', }) # --- -# name: test_sensors[HWE-KWH1-entity_ids6][sensor.device_wi_fi_ssid:device-registry] +# name: test_sensors[HWE-KWH1-entity_ids7][sensor.device_wi_fi_ssid:device-registry] DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , @@ -1011,7 +834,7 @@ 'via_device_id': None, }) # --- -# name: test_sensors[HWE-KWH1-entity_ids6][sensor.device_wi_fi_ssid:entity-registry] +# name: test_sensors[HWE-KWH1-entity_ids7][sensor.device_wi_fi_ssid:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1044,7 +867,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_sensors[HWE-KWH1-entity_ids6][sensor.device_wi_fi_ssid:state] +# name: test_sensors[HWE-KWH1-entity_ids7][sensor.device_wi_fi_ssid:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Device Wi-Fi SSID', @@ -1052,11 +875,12 @@ 'context': , 'entity_id': 'sensor.device_wi_fi_ssid', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'My Wi-Fi', }) # --- -# name: test_sensors[HWE-KWH1-entity_ids6][sensor.device_wi_fi_strength:device-registry] +# name: test_sensors[HWE-KWH1-entity_ids7][sensor.device_wi_fi_strength:device-registry] DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , @@ -1090,7 +914,7 @@ 'via_device_id': None, }) # --- -# name: test_sensors[HWE-KWH1-entity_ids6][sensor.device_wi_fi_strength:entity-registry] +# name: test_sensors[HWE-KWH1-entity_ids7][sensor.device_wi_fi_strength:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1125,7 +949,7 @@ 'unit_of_measurement': '%', }) # --- -# name: test_sensors[HWE-KWH1-entity_ids6][sensor.device_wi_fi_strength:state] +# name: test_sensors[HWE-KWH1-entity_ids7][sensor.device_wi_fi_strength:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Device Wi-Fi strength', @@ -1135,11 +959,12 @@ 'context': , 'entity_id': 'sensor.device_wi_fi_strength', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '92', }) # --- -# name: test_sensors[HWE-KWH3-entity_ids7][sensor.device_apparent_power:device-registry] +# name: test_sensors[HWE-KWH3-entity_ids8][sensor.device_apparent_power:device-registry] DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , @@ -1173,7 +998,7 @@ 'via_device_id': None, }) # --- -# name: test_sensors[HWE-KWH3-entity_ids7][sensor.device_apparent_power:entity-registry] +# name: test_sensors[HWE-KWH3-entity_ids8][sensor.device_apparent_power:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1208,7 +1033,7 @@ 'unit_of_measurement': , }) # --- -# name: test_sensors[HWE-KWH3-entity_ids7][sensor.device_apparent_power:state] +# name: test_sensors[HWE-KWH3-entity_ids8][sensor.device_apparent_power:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'apparent_power', @@ -1219,11 +1044,12 @@ 'context': , 'entity_id': 'sensor.device_apparent_power', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '7112.293', }) # --- -# name: test_sensors[HWE-KWH3-entity_ids7][sensor.device_apparent_power_phase_1:device-registry] +# name: test_sensors[HWE-KWH3-entity_ids8][sensor.device_apparent_power_phase_1:device-registry] DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , @@ -1257,7 +1083,7 @@ 'via_device_id': None, }) # --- -# name: test_sensors[HWE-KWH3-entity_ids7][sensor.device_apparent_power_phase_1:entity-registry] +# name: test_sensors[HWE-KWH3-entity_ids8][sensor.device_apparent_power_phase_1:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1292,7 +1118,7 @@ 'unit_of_measurement': , }) # --- -# name: test_sensors[HWE-KWH3-entity_ids7][sensor.device_apparent_power_phase_1:state] +# name: test_sensors[HWE-KWH3-entity_ids8][sensor.device_apparent_power_phase_1:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'apparent_power', @@ -1303,11 +1129,12 @@ 'context': , 'entity_id': 'sensor.device_apparent_power_phase_1', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0', }) # --- -# name: test_sensors[HWE-KWH3-entity_ids7][sensor.device_apparent_power_phase_2:device-registry] +# name: test_sensors[HWE-KWH3-entity_ids8][sensor.device_apparent_power_phase_2:device-registry] DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , @@ -1341,7 +1168,7 @@ 'via_device_id': None, }) # --- -# name: test_sensors[HWE-KWH3-entity_ids7][sensor.device_apparent_power_phase_2:entity-registry] +# name: test_sensors[HWE-KWH3-entity_ids8][sensor.device_apparent_power_phase_2:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1376,7 +1203,7 @@ 'unit_of_measurement': , }) # --- -# name: test_sensors[HWE-KWH3-entity_ids7][sensor.device_apparent_power_phase_2:state] +# name: test_sensors[HWE-KWH3-entity_ids8][sensor.device_apparent_power_phase_2:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'apparent_power', @@ -1387,11 +1214,12 @@ 'context': , 'entity_id': 'sensor.device_apparent_power_phase_2', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '3548.879', }) # --- -# name: test_sensors[HWE-KWH3-entity_ids7][sensor.device_apparent_power_phase_3:device-registry] +# name: test_sensors[HWE-KWH3-entity_ids8][sensor.device_apparent_power_phase_3:device-registry] DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , @@ -1425,7 +1253,7 @@ 'via_device_id': None, }) # --- -# name: test_sensors[HWE-KWH3-entity_ids7][sensor.device_apparent_power_phase_3:entity-registry] +# name: test_sensors[HWE-KWH3-entity_ids8][sensor.device_apparent_power_phase_3:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1460,7 +1288,7 @@ 'unit_of_measurement': , }) # --- -# name: test_sensors[HWE-KWH3-entity_ids7][sensor.device_apparent_power_phase_3:state] +# name: test_sensors[HWE-KWH3-entity_ids8][sensor.device_apparent_power_phase_3:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'apparent_power', @@ -1471,11 +1299,12 @@ 'context': , 'entity_id': 'sensor.device_apparent_power_phase_3', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '3563.414', }) # --- -# name: test_sensors[HWE-KWH3-entity_ids7][sensor.device_current:device-registry] +# name: test_sensors[HWE-KWH3-entity_ids8][sensor.device_current:device-registry] DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , @@ -1509,7 +1338,7 @@ 'via_device_id': None, }) # --- -# name: test_sensors[HWE-KWH3-entity_ids7][sensor.device_current:entity-registry] +# name: test_sensors[HWE-KWH3-entity_ids8][sensor.device_current:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1544,7 +1373,7 @@ 'unit_of_measurement': , }) # --- -# name: test_sensors[HWE-KWH3-entity_ids7][sensor.device_current:state] +# name: test_sensors[HWE-KWH3-entity_ids8][sensor.device_current:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'current', @@ -1555,11 +1384,12 @@ 'context': , 'entity_id': 'sensor.device_current', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '30.999', }) # --- -# name: test_sensors[HWE-KWH3-entity_ids7][sensor.device_current_phase_1:device-registry] +# name: test_sensors[HWE-KWH3-entity_ids8][sensor.device_current_phase_1:device-registry] DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , @@ -1593,7 +1423,7 @@ 'via_device_id': None, }) # --- -# name: test_sensors[HWE-KWH3-entity_ids7][sensor.device_current_phase_1:entity-registry] +# name: test_sensors[HWE-KWH3-entity_ids8][sensor.device_current_phase_1:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1628,7 +1458,7 @@ 'unit_of_measurement': , }) # --- -# name: test_sensors[HWE-KWH3-entity_ids7][sensor.device_current_phase_1:state] +# name: test_sensors[HWE-KWH3-entity_ids8][sensor.device_current_phase_1:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'current', @@ -1639,11 +1469,12 @@ 'context': , 'entity_id': 'sensor.device_current_phase_1', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0', }) # --- -# name: test_sensors[HWE-KWH3-entity_ids7][sensor.device_current_phase_2:device-registry] +# name: test_sensors[HWE-KWH3-entity_ids8][sensor.device_current_phase_2:device-registry] DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , @@ -1677,7 +1508,7 @@ 'via_device_id': None, }) # --- -# name: test_sensors[HWE-KWH3-entity_ids7][sensor.device_current_phase_2:entity-registry] +# name: test_sensors[HWE-KWH3-entity_ids8][sensor.device_current_phase_2:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1712,7 +1543,7 @@ 'unit_of_measurement': , }) # --- -# name: test_sensors[HWE-KWH3-entity_ids7][sensor.device_current_phase_2:state] +# name: test_sensors[HWE-KWH3-entity_ids8][sensor.device_current_phase_2:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'current', @@ -1723,11 +1554,12 @@ 'context': , 'entity_id': 'sensor.device_current_phase_2', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '15.521', }) # --- -# name: test_sensors[HWE-KWH3-entity_ids7][sensor.device_current_phase_3:device-registry] +# name: test_sensors[HWE-KWH3-entity_ids8][sensor.device_current_phase_3:device-registry] DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , @@ -1761,7 +1593,7 @@ 'via_device_id': None, }) # --- -# name: test_sensors[HWE-KWH3-entity_ids7][sensor.device_current_phase_3:entity-registry] +# name: test_sensors[HWE-KWH3-entity_ids8][sensor.device_current_phase_3:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1796,7 +1628,7 @@ 'unit_of_measurement': , }) # --- -# name: test_sensors[HWE-KWH3-entity_ids7][sensor.device_current_phase_3:state] +# name: test_sensors[HWE-KWH3-entity_ids8][sensor.device_current_phase_3:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'current', @@ -1807,11 +1639,12 @@ 'context': , 'entity_id': 'sensor.device_current_phase_3', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '15.477', }) # --- -# name: test_sensors[HWE-KWH3-entity_ids7][sensor.device_energy_export:device-registry] +# name: test_sensors[HWE-KWH3-entity_ids8][sensor.device_energy_export:device-registry] DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , @@ -1845,7 +1678,7 @@ 'via_device_id': None, }) # --- -# name: test_sensors[HWE-KWH3-entity_ids7][sensor.device_energy_export:entity-registry] +# name: test_sensors[HWE-KWH3-entity_ids8][sensor.device_energy_export:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1880,7 +1713,7 @@ 'unit_of_measurement': , }) # --- -# name: test_sensors[HWE-KWH3-entity_ids7][sensor.device_energy_export:state] +# name: test_sensors[HWE-KWH3-entity_ids8][sensor.device_energy_export:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'energy', @@ -1891,11 +1724,12 @@ 'context': , 'entity_id': 'sensor.device_energy_export', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0.523', }) # --- -# name: test_sensors[HWE-KWH3-entity_ids7][sensor.device_energy_import:device-registry] +# name: test_sensors[HWE-KWH3-entity_ids8][sensor.device_energy_import:device-registry] DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , @@ -1929,7 +1763,7 @@ 'via_device_id': None, }) # --- -# name: test_sensors[HWE-KWH3-entity_ids7][sensor.device_energy_import:entity-registry] +# name: test_sensors[HWE-KWH3-entity_ids8][sensor.device_energy_import:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1964,7 +1798,7 @@ 'unit_of_measurement': , }) # --- -# name: test_sensors[HWE-KWH3-entity_ids7][sensor.device_energy_import:state] +# name: test_sensors[HWE-KWH3-entity_ids8][sensor.device_energy_import:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'energy', @@ -1975,11 +1809,12 @@ 'context': , 'entity_id': 'sensor.device_energy_import', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0.101', }) # --- -# name: test_sensors[HWE-KWH3-entity_ids7][sensor.device_frequency:device-registry] +# name: test_sensors[HWE-KWH3-entity_ids8][sensor.device_frequency:device-registry] DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , @@ -2013,7 +1848,7 @@ 'via_device_id': None, }) # --- -# name: test_sensors[HWE-KWH3-entity_ids7][sensor.device_frequency:entity-registry] +# name: test_sensors[HWE-KWH3-entity_ids8][sensor.device_frequency:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2048,7 +1883,7 @@ 'unit_of_measurement': , }) # --- -# name: test_sensors[HWE-KWH3-entity_ids7][sensor.device_frequency:state] +# name: test_sensors[HWE-KWH3-entity_ids8][sensor.device_frequency:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'frequency', @@ -2059,11 +1894,12 @@ 'context': , 'entity_id': 'sensor.device_frequency', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '49.926', }) # --- -# name: test_sensors[HWE-KWH3-entity_ids7][sensor.device_power:device-registry] +# name: test_sensors[HWE-KWH3-entity_ids8][sensor.device_power:device-registry] DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , @@ -2097,7 +1933,7 @@ 'via_device_id': None, }) # --- -# name: test_sensors[HWE-KWH3-entity_ids7][sensor.device_power:entity-registry] +# name: test_sensors[HWE-KWH3-entity_ids8][sensor.device_power:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2135,7 +1971,7 @@ 'unit_of_measurement': , }) # --- -# name: test_sensors[HWE-KWH3-entity_ids7][sensor.device_power:state] +# name: test_sensors[HWE-KWH3-entity_ids8][sensor.device_power:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'power', @@ -2146,11 +1982,12 @@ 'context': , 'entity_id': 'sensor.device_power', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '-900.194', }) # --- -# name: test_sensors[HWE-KWH3-entity_ids7][sensor.device_power_factor_phase_1:device-registry] +# name: test_sensors[HWE-KWH3-entity_ids8][sensor.device_power_factor_phase_1:device-registry] DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , @@ -2184,7 +2021,7 @@ 'via_device_id': None, }) # --- -# name: test_sensors[HWE-KWH3-entity_ids7][sensor.device_power_factor_phase_1:entity-registry] +# name: test_sensors[HWE-KWH3-entity_ids8][sensor.device_power_factor_phase_1:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2219,7 +2056,7 @@ 'unit_of_measurement': '%', }) # --- -# name: test_sensors[HWE-KWH3-entity_ids7][sensor.device_power_factor_phase_1:state] +# name: test_sensors[HWE-KWH3-entity_ids8][sensor.device_power_factor_phase_1:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'power_factor', @@ -2230,11 +2067,12 @@ 'context': , 'entity_id': 'sensor.device_power_factor_phase_1', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '100', }) # --- -# name: test_sensors[HWE-KWH3-entity_ids7][sensor.device_power_factor_phase_2:device-registry] +# name: test_sensors[HWE-KWH3-entity_ids8][sensor.device_power_factor_phase_2:device-registry] DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , @@ -2268,7 +2106,7 @@ 'via_device_id': None, }) # --- -# name: test_sensors[HWE-KWH3-entity_ids7][sensor.device_power_factor_phase_2:entity-registry] +# name: test_sensors[HWE-KWH3-entity_ids8][sensor.device_power_factor_phase_2:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2303,7 +2141,7 @@ 'unit_of_measurement': '%', }) # --- -# name: test_sensors[HWE-KWH3-entity_ids7][sensor.device_power_factor_phase_2:state] +# name: test_sensors[HWE-KWH3-entity_ids8][sensor.device_power_factor_phase_2:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'power_factor', @@ -2314,11 +2152,12 @@ 'context': , 'entity_id': 'sensor.device_power_factor_phase_2', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '99.9', }) # --- -# name: test_sensors[HWE-KWH3-entity_ids7][sensor.device_power_factor_phase_3:device-registry] +# name: test_sensors[HWE-KWH3-entity_ids8][sensor.device_power_factor_phase_3:device-registry] DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , @@ -2352,7 +2191,7 @@ 'via_device_id': None, }) # --- -# name: test_sensors[HWE-KWH3-entity_ids7][sensor.device_power_factor_phase_3:entity-registry] +# name: test_sensors[HWE-KWH3-entity_ids8][sensor.device_power_factor_phase_3:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2387,7 +2226,7 @@ 'unit_of_measurement': '%', }) # --- -# name: test_sensors[HWE-KWH3-entity_ids7][sensor.device_power_factor_phase_3:state] +# name: test_sensors[HWE-KWH3-entity_ids8][sensor.device_power_factor_phase_3:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'power_factor', @@ -2398,11 +2237,12 @@ 'context': , 'entity_id': 'sensor.device_power_factor_phase_3', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '99.7', }) # --- -# name: test_sensors[HWE-KWH3-entity_ids7][sensor.device_power_phase_1:device-registry] +# name: test_sensors[HWE-KWH3-entity_ids8][sensor.device_power_phase_1:device-registry] DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , @@ -2436,7 +2276,7 @@ 'via_device_id': None, }) # --- -# name: test_sensors[HWE-KWH3-entity_ids7][sensor.device_power_phase_1:entity-registry] +# name: test_sensors[HWE-KWH3-entity_ids8][sensor.device_power_phase_1:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2474,7 +2314,7 @@ 'unit_of_measurement': , }) # --- -# name: test_sensors[HWE-KWH3-entity_ids7][sensor.device_power_phase_1:state] +# name: test_sensors[HWE-KWH3-entity_ids8][sensor.device_power_phase_1:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'power', @@ -2485,11 +2325,12 @@ 'context': , 'entity_id': 'sensor.device_power_phase_1', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '-1058.296', }) # --- -# name: test_sensors[HWE-KWH3-entity_ids7][sensor.device_power_phase_2:device-registry] +# name: test_sensors[HWE-KWH3-entity_ids8][sensor.device_power_phase_2:device-registry] DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , @@ -2523,7 +2364,7 @@ 'via_device_id': None, }) # --- -# name: test_sensors[HWE-KWH3-entity_ids7][sensor.device_power_phase_2:entity-registry] +# name: test_sensors[HWE-KWH3-entity_ids8][sensor.device_power_phase_2:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2561,7 +2402,7 @@ 'unit_of_measurement': , }) # --- -# name: test_sensors[HWE-KWH3-entity_ids7][sensor.device_power_phase_2:state] +# name: test_sensors[HWE-KWH3-entity_ids8][sensor.device_power_phase_2:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'power', @@ -2572,11 +2413,12 @@ 'context': , 'entity_id': 'sensor.device_power_phase_2', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '158.102', }) # --- -# name: test_sensors[HWE-KWH3-entity_ids7][sensor.device_power_phase_3:device-registry] +# name: test_sensors[HWE-KWH3-entity_ids8][sensor.device_power_phase_3:device-registry] DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , @@ -2610,7 +2452,7 @@ 'via_device_id': None, }) # --- -# name: test_sensors[HWE-KWH3-entity_ids7][sensor.device_power_phase_3:entity-registry] +# name: test_sensors[HWE-KWH3-entity_ids8][sensor.device_power_phase_3:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2648,7 +2490,7 @@ 'unit_of_measurement': , }) # --- -# name: test_sensors[HWE-KWH3-entity_ids7][sensor.device_power_phase_3:state] +# name: test_sensors[HWE-KWH3-entity_ids8][sensor.device_power_phase_3:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'power', @@ -2659,11 +2501,12 @@ 'context': , 'entity_id': 'sensor.device_power_phase_3', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0.0', }) # --- -# name: test_sensors[HWE-KWH3-entity_ids7][sensor.device_reactive_power:device-registry] +# name: test_sensors[HWE-KWH3-entity_ids8][sensor.device_reactive_power:device-registry] DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , @@ -2697,7 +2540,7 @@ 'via_device_id': None, }) # --- -# name: test_sensors[HWE-KWH3-entity_ids7][sensor.device_reactive_power:entity-registry] +# name: test_sensors[HWE-KWH3-entity_ids8][sensor.device_reactive_power:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2732,7 +2575,7 @@ 'unit_of_measurement': 'var', }) # --- -# name: test_sensors[HWE-KWH3-entity_ids7][sensor.device_reactive_power:state] +# name: test_sensors[HWE-KWH3-entity_ids8][sensor.device_reactive_power:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'reactive_power', @@ -2743,11 +2586,12 @@ 'context': , 'entity_id': 'sensor.device_reactive_power', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '-429.025', }) # --- -# name: test_sensors[HWE-KWH3-entity_ids7][sensor.device_reactive_power_phase_1:device-registry] +# name: test_sensors[HWE-KWH3-entity_ids8][sensor.device_reactive_power_phase_1:device-registry] DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , @@ -2781,7 +2625,7 @@ 'via_device_id': None, }) # --- -# name: test_sensors[HWE-KWH3-entity_ids7][sensor.device_reactive_power_phase_1:entity-registry] +# name: test_sensors[HWE-KWH3-entity_ids8][sensor.device_reactive_power_phase_1:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2816,7 +2660,7 @@ 'unit_of_measurement': 'var', }) # --- -# name: test_sensors[HWE-KWH3-entity_ids7][sensor.device_reactive_power_phase_1:state] +# name: test_sensors[HWE-KWH3-entity_ids8][sensor.device_reactive_power_phase_1:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'reactive_power', @@ -2827,11 +2671,12 @@ 'context': , 'entity_id': 'sensor.device_reactive_power_phase_1', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0', }) # --- -# name: test_sensors[HWE-KWH3-entity_ids7][sensor.device_reactive_power_phase_2:device-registry] +# name: test_sensors[HWE-KWH3-entity_ids8][sensor.device_reactive_power_phase_2:device-registry] DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , @@ -2865,7 +2710,7 @@ 'via_device_id': None, }) # --- -# name: test_sensors[HWE-KWH3-entity_ids7][sensor.device_reactive_power_phase_2:entity-registry] +# name: test_sensors[HWE-KWH3-entity_ids8][sensor.device_reactive_power_phase_2:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2900,7 +2745,7 @@ 'unit_of_measurement': 'var', }) # --- -# name: test_sensors[HWE-KWH3-entity_ids7][sensor.device_reactive_power_phase_2:state] +# name: test_sensors[HWE-KWH3-entity_ids8][sensor.device_reactive_power_phase_2:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'reactive_power', @@ -2911,11 +2756,12 @@ 'context': , 'entity_id': 'sensor.device_reactive_power_phase_2', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '-166.675', }) # --- -# name: test_sensors[HWE-KWH3-entity_ids7][sensor.device_reactive_power_phase_3:device-registry] +# name: test_sensors[HWE-KWH3-entity_ids8][sensor.device_reactive_power_phase_3:device-registry] DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , @@ -2949,7 +2795,7 @@ 'via_device_id': None, }) # --- -# name: test_sensors[HWE-KWH3-entity_ids7][sensor.device_reactive_power_phase_3:entity-registry] +# name: test_sensors[HWE-KWH3-entity_ids8][sensor.device_reactive_power_phase_3:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2984,7 +2830,7 @@ 'unit_of_measurement': 'var', }) # --- -# name: test_sensors[HWE-KWH3-entity_ids7][sensor.device_reactive_power_phase_3:state] +# name: test_sensors[HWE-KWH3-entity_ids8][sensor.device_reactive_power_phase_3:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'reactive_power', @@ -2995,11 +2841,12 @@ 'context': , 'entity_id': 'sensor.device_reactive_power_phase_3', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '-262.35', }) # --- -# name: test_sensors[HWE-KWH3-entity_ids7][sensor.device_voltage_phase_1:device-registry] +# name: test_sensors[HWE-KWH3-entity_ids8][sensor.device_voltage_phase_1:device-registry] DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , @@ -3033,7 +2880,7 @@ 'via_device_id': None, }) # --- -# name: test_sensors[HWE-KWH3-entity_ids7][sensor.device_voltage_phase_1:entity-registry] +# name: test_sensors[HWE-KWH3-entity_ids8][sensor.device_voltage_phase_1:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -3068,7 +2915,7 @@ 'unit_of_measurement': , }) # --- -# name: test_sensors[HWE-KWH3-entity_ids7][sensor.device_voltage_phase_1:state] +# name: test_sensors[HWE-KWH3-entity_ids8][sensor.device_voltage_phase_1:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'voltage', @@ -3079,11 +2926,12 @@ 'context': , 'entity_id': 'sensor.device_voltage_phase_1', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '230.751', }) # --- -# name: test_sensors[HWE-KWH3-entity_ids7][sensor.device_voltage_phase_2:device-registry] +# name: test_sensors[HWE-KWH3-entity_ids8][sensor.device_voltage_phase_2:device-registry] DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , @@ -3117,7 +2965,7 @@ 'via_device_id': None, }) # --- -# name: test_sensors[HWE-KWH3-entity_ids7][sensor.device_voltage_phase_2:entity-registry] +# name: test_sensors[HWE-KWH3-entity_ids8][sensor.device_voltage_phase_2:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -3152,7 +3000,7 @@ 'unit_of_measurement': , }) # --- -# name: test_sensors[HWE-KWH3-entity_ids7][sensor.device_voltage_phase_2:state] +# name: test_sensors[HWE-KWH3-entity_ids8][sensor.device_voltage_phase_2:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'voltage', @@ -3163,11 +3011,12 @@ 'context': , 'entity_id': 'sensor.device_voltage_phase_2', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '228.391', }) # --- -# name: test_sensors[HWE-KWH3-entity_ids7][sensor.device_voltage_phase_3:device-registry] +# name: test_sensors[HWE-KWH3-entity_ids8][sensor.device_voltage_phase_3:device-registry] DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , @@ -3201,7 +3050,7 @@ 'via_device_id': None, }) # --- -# name: test_sensors[HWE-KWH3-entity_ids7][sensor.device_voltage_phase_3:entity-registry] +# name: test_sensors[HWE-KWH3-entity_ids8][sensor.device_voltage_phase_3:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -3236,7 +3085,7 @@ 'unit_of_measurement': , }) # --- -# name: test_sensors[HWE-KWH3-entity_ids7][sensor.device_voltage_phase_3:state] +# name: test_sensors[HWE-KWH3-entity_ids8][sensor.device_voltage_phase_3:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'voltage', @@ -3247,11 +3096,12 @@ 'context': , 'entity_id': 'sensor.device_voltage_phase_3', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '229.612', }) # --- -# name: test_sensors[HWE-KWH3-entity_ids7][sensor.device_wi_fi_ssid:device-registry] +# name: test_sensors[HWE-KWH3-entity_ids8][sensor.device_wi_fi_ssid:device-registry] DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , @@ -3285,7 +3135,7 @@ 'via_device_id': None, }) # --- -# name: test_sensors[HWE-KWH3-entity_ids7][sensor.device_wi_fi_ssid:entity-registry] +# name: test_sensors[HWE-KWH3-entity_ids8][sensor.device_wi_fi_ssid:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -3318,7 +3168,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_sensors[HWE-KWH3-entity_ids7][sensor.device_wi_fi_ssid:state] +# name: test_sensors[HWE-KWH3-entity_ids8][sensor.device_wi_fi_ssid:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Device Wi-Fi SSID', @@ -3326,11 +3176,12 @@ 'context': , 'entity_id': 'sensor.device_wi_fi_ssid', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'My Wi-Fi', }) # --- -# name: test_sensors[HWE-KWH3-entity_ids7][sensor.device_wi_fi_strength:device-registry] +# name: test_sensors[HWE-KWH3-entity_ids8][sensor.device_wi_fi_strength:device-registry] DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , @@ -3364,7 +3215,7 @@ 'via_device_id': None, }) # --- -# name: test_sensors[HWE-KWH3-entity_ids7][sensor.device_wi_fi_strength:entity-registry] +# name: test_sensors[HWE-KWH3-entity_ids8][sensor.device_wi_fi_strength:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -3399,7 +3250,7 @@ 'unit_of_measurement': '%', }) # --- -# name: test_sensors[HWE-KWH3-entity_ids7][sensor.device_wi_fi_strength:state] +# name: test_sensors[HWE-KWH3-entity_ids8][sensor.device_wi_fi_strength:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Device Wi-Fi strength', @@ -3409,1147 +3260,11 @@ 'context': , 'entity_id': 'sensor.device_wi_fi_strength', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '92', }) # --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.device_active_average_demand:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'HWE-P1', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '4.19', - 'via_device_id': None, - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.device_active_average_demand:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_active_average_demand', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Active average demand', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'active_power_average_w', - 'unique_id': 'aabbccddeeff_active_power_average_w', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.device_active_average_demand:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'power', - 'friendly_name': 'Device Active average demand', - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.device_active_average_demand', - 'last_changed': , - 'last_updated': , - 'state': '123.0', - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.device_active_current_phase_1:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'HWE-P1', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '4.19', - 'via_device_id': None, - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.device_active_current_phase_1:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_active_current_phase_1', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Active current phase 1', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'active_current_phase_a', - 'unique_id': 'aabbccddeeff_active_current_l1_a', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.device_active_current_phase_1:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'current', - 'friendly_name': 'Device Active current phase 1', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.device_active_current_phase_1', - 'last_changed': , - 'last_updated': , - 'state': '-4', - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.device_active_current_phase_2:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'HWE-P1', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '4.19', - 'via_device_id': None, - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.device_active_current_phase_2:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_active_current_phase_2', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Active current phase 2', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'active_current_phase_a', - 'unique_id': 'aabbccddeeff_active_current_l2_a', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.device_active_current_phase_2:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'current', - 'friendly_name': 'Device Active current phase 2', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.device_active_current_phase_2', - 'last_changed': , - 'last_updated': , - 'state': '2', - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.device_active_current_phase_3:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'HWE-P1', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '4.19', - 'via_device_id': None, - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.device_active_current_phase_3:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_active_current_phase_3', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Active current phase 3', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'active_current_phase_a', - 'unique_id': 'aabbccddeeff_active_current_l3_a', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.device_active_current_phase_3:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'current', - 'friendly_name': 'Device Active current phase 3', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.device_active_current_phase_3', - 'last_changed': , - 'last_updated': , - 'state': '0', - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.device_active_frequency:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'HWE-P1', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '4.19', - 'via_device_id': None, - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.device_active_frequency:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_active_frequency', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Active frequency', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'active_frequency_hz', - 'unique_id': 'aabbccddeeff_active_frequency_hz', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.device_active_frequency:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'frequency', - 'friendly_name': 'Device Active frequency', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.device_active_frequency', - 'last_changed': , - 'last_updated': , - 'state': '50', - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.device_active_power:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'HWE-P1', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '4.19', - 'via_device_id': None, - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.device_active_power:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_active_power', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 0, - }), - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Active power', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'active_power_w', - 'unique_id': 'aabbccddeeff_active_power_w', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.device_active_power:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'power', - 'friendly_name': 'Device Active power', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.device_active_power', - 'last_changed': , - 'last_updated': , - 'state': '-123', - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.device_active_power_phase_1:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'HWE-P1', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '4.19', - 'via_device_id': None, - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.device_active_power_phase_1:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_active_power_phase_1', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 0, - }), - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Active power phase 1', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'active_power_phase_w', - 'unique_id': 'aabbccddeeff_active_power_l1_w', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.device_active_power_phase_1:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'power', - 'friendly_name': 'Device Active power phase 1', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.device_active_power_phase_1', - 'last_changed': , - 'last_updated': , - 'state': '-123', - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.device_active_power_phase_2:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'HWE-P1', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '4.19', - 'via_device_id': None, - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.device_active_power_phase_2:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_active_power_phase_2', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 0, - }), - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Active power phase 2', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'active_power_phase_w', - 'unique_id': 'aabbccddeeff_active_power_l2_w', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.device_active_power_phase_2:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'power', - 'friendly_name': 'Device Active power phase 2', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.device_active_power_phase_2', - 'last_changed': , - 'last_updated': , - 'state': '456', - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.device_active_power_phase_3:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'HWE-P1', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '4.19', - 'via_device_id': None, - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.device_active_power_phase_3:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_active_power_phase_3', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 0, - }), - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Active power phase 3', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'active_power_phase_w', - 'unique_id': 'aabbccddeeff_active_power_l3_w', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.device_active_power_phase_3:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'power', - 'friendly_name': 'Device Active power phase 3', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.device_active_power_phase_3', - 'last_changed': , - 'last_updated': , - 'state': '123.456', - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.device_active_tariff:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'HWE-P1', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '4.19', - 'via_device_id': None, - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.device_active_tariff:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'options': list([ - '1', - '2', - '3', - '4', - ]), - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_active_tariff', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Active tariff', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'active_tariff', - 'unique_id': 'aabbccddeeff_active_tariff', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.device_active_tariff:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'enum', - 'friendly_name': 'Device Active tariff', - 'options': list([ - '1', - '2', - '3', - '4', - ]), - }), - 'context': , - 'entity_id': 'sensor.device_active_tariff', - 'last_changed': , - 'last_updated': , - 'state': '2', - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.device_active_voltage_phase_1:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'HWE-P1', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '4.19', - 'via_device_id': None, - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.device_active_voltage_phase_1:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_active_voltage_phase_1', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Active voltage phase 1', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'active_voltage_phase_v', - 'unique_id': 'aabbccddeeff_active_voltage_l1_v', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.device_active_voltage_phase_1:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'voltage', - 'friendly_name': 'Device Active voltage phase 1', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.device_active_voltage_phase_1', - 'last_changed': , - 'last_updated': , - 'state': '230.111', - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.device_active_voltage_phase_2:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'HWE-P1', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '4.19', - 'via_device_id': None, - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.device_active_voltage_phase_2:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_active_voltage_phase_2', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Active voltage phase 2', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'active_voltage_phase_v', - 'unique_id': 'aabbccddeeff_active_voltage_l2_v', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.device_active_voltage_phase_2:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'voltage', - 'friendly_name': 'Device Active voltage phase 2', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.device_active_voltage_phase_2', - 'last_changed': , - 'last_updated': , - 'state': '230.222', - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.device_active_voltage_phase_3:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'HWE-P1', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '4.19', - 'via_device_id': None, - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.device_active_voltage_phase_3:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_active_voltage_phase_3', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Active voltage phase 3', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'active_voltage_phase_v', - 'unique_id': 'aabbccddeeff_active_voltage_l3_v', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.device_active_voltage_phase_3:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'voltage', - 'friendly_name': 'Device Active voltage phase 3', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.device_active_voltage_phase_3', - 'last_changed': , - 'last_updated': , - 'state': '230.333', - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.device_active_water_usage:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'HWE-P1', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '4.19', - 'via_device_id': None, - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.device_active_water_usage:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_active_water_usage', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Active water usage', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'active_liter_lpm', - 'unique_id': 'aabbccddeeff_active_liter_lpm', - 'unit_of_measurement': 'l/min', - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.device_active_water_usage:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Device Active water usage', - 'state_class': , - 'unit_of_measurement': 'l/min', - }), - 'context': , - 'entity_id': 'sensor.device_active_water_usage', - 'last_changed': , - 'last_updated': , - 'state': '12.345', - }) -# --- # name: test_sensors[HWE-P1-entity_ids0][sensor.device_average_demand:device-registry] DeviceRegistryEntrySnapshot({ 'area_id': None, @@ -4627,6 +3342,7 @@ 'context': , 'entity_id': 'sensor.device_average_demand', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '123.0', }) @@ -4711,6 +3427,7 @@ 'context': , 'entity_id': 'sensor.device_current_phase_1', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '-4', }) @@ -4795,6 +3512,7 @@ 'context': , 'entity_id': 'sensor.device_current_phase_2', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '2', }) @@ -4879,6 +3597,7 @@ 'context': , 'entity_id': 'sensor.device_current_phase_3', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0', }) @@ -4958,6 +3677,7 @@ 'context': , 'entity_id': 'sensor.device_dsmr_version', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '50', }) @@ -5042,6 +3762,7 @@ 'context': , 'entity_id': 'sensor.device_energy_export', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '13086.777', }) @@ -5126,6 +3847,7 @@ 'context': , 'entity_id': 'sensor.device_energy_export_tariff_1', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '4321.333', }) @@ -5210,6 +3932,7 @@ 'context': , 'entity_id': 'sensor.device_energy_export_tariff_2', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '8765.444', }) @@ -5294,6 +4017,7 @@ 'context': , 'entity_id': 'sensor.device_energy_export_tariff_3', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '8765.444', }) @@ -5378,6 +4102,7 @@ 'context': , 'entity_id': 'sensor.device_energy_export_tariff_4', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '8765.444', }) @@ -5462,6 +4187,7 @@ 'context': , 'entity_id': 'sensor.device_energy_import', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '13779.338', }) @@ -5546,6 +4272,7 @@ 'context': , 'entity_id': 'sensor.device_energy_import_tariff_1', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '10830.511', }) @@ -5630,6 +4357,7 @@ 'context': , 'entity_id': 'sensor.device_energy_import_tariff_2', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '2948.827', }) @@ -5714,6 +4442,7 @@ 'context': , 'entity_id': 'sensor.device_energy_import_tariff_3', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '2948.827', }) @@ -5798,6 +4527,7 @@ 'context': , 'entity_id': 'sensor.device_energy_import_tariff_4', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '2948.827', }) @@ -5882,85 +4612,11 @@ 'context': , 'entity_id': 'sensor.device_frequency', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '50', }) # --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.device_gas_meter_identifier:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'HWE-P1', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '4.19', - 'via_device_id': None, - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.device_gas_meter_identifier:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.device_gas_meter_identifier', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Gas meter identifier', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'gas_unique_id', - 'unique_id': 'aabbccddeeff_gas_unique_id', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.device_gas_meter_identifier:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Device Gas meter identifier', - }), - 'context': , - 'entity_id': 'sensor.device_gas_meter_identifier', - 'last_changed': , - 'last_updated': , - 'state': '01FFEEDDCCBBAA99887766554433221100', - }) -# --- # name: test_sensors[HWE-P1-entity_ids0][sensor.device_long_power_failures_detected:device-registry] DeviceRegistryEntrySnapshot({ 'area_id': None, @@ -6036,6 +4692,7 @@ 'context': , 'entity_id': 'sensor.device_long_power_failures_detected', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '5', }) @@ -6117,6 +4774,7 @@ 'context': , 'entity_id': 'sensor.device_peak_demand_current_month', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '1111.0', }) @@ -6204,6 +4862,7 @@ 'context': , 'entity_id': 'sensor.device_power', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '-123', }) @@ -6283,6 +4942,7 @@ 'context': , 'entity_id': 'sensor.device_power_failures_detected', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '4', }) @@ -6370,6 +5030,7 @@ 'context': , 'entity_id': 'sensor.device_power_phase_1', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '-123', }) @@ -6457,6 +5118,7 @@ 'context': , 'entity_id': 'sensor.device_power_phase_2', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '456', }) @@ -6544,6 +5206,7 @@ 'context': , 'entity_id': 'sensor.device_power_phase_3', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '123.456', }) @@ -6623,6 +5286,7 @@ 'context': , 'entity_id': 'sensor.device_smart_meter_identifier', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '00112233445566778899AABBCCDDEEFF', }) @@ -6702,6 +5366,7 @@ 'context': , 'entity_id': 'sensor.device_smart_meter_model', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'ISKRA 2M550T-101', }) @@ -6795,890 +5460,11 @@ 'context': , 'entity_id': 'sensor.device_tariff', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '2', }) # --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.device_total_energy_export:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'HWE-P1', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '4.19', - 'via_device_id': None, - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.device_total_energy_export:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_total_energy_export', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Total energy export', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'total_energy_export_kwh', - 'unique_id': 'aabbccddeeff_total_power_export_kwh', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.device_total_energy_export:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'Device Total energy export', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.device_total_energy_export', - 'last_changed': , - 'last_updated': , - 'state': '13086.777', - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.device_total_energy_export_tariff_1:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'HWE-P1', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '4.19', - 'via_device_id': None, - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.device_total_energy_export_tariff_1:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_total_energy_export_tariff_1', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Total energy export tariff 1', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'total_energy_export_tariff_kwh', - 'unique_id': 'aabbccddeeff_total_power_export_t1_kwh', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.device_total_energy_export_tariff_1:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'Device Total energy export tariff 1', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.device_total_energy_export_tariff_1', - 'last_changed': , - 'last_updated': , - 'state': '4321.333', - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.device_total_energy_export_tariff_2:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'HWE-P1', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '4.19', - 'via_device_id': None, - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.device_total_energy_export_tariff_2:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_total_energy_export_tariff_2', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Total energy export tariff 2', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'total_energy_export_tariff_kwh', - 'unique_id': 'aabbccddeeff_total_power_export_t2_kwh', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.device_total_energy_export_tariff_2:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'Device Total energy export tariff 2', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.device_total_energy_export_tariff_2', - 'last_changed': , - 'last_updated': , - 'state': '8765.444', - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.device_total_energy_export_tariff_3:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'HWE-P1', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '4.19', - 'via_device_id': None, - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.device_total_energy_export_tariff_3:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_total_energy_export_tariff_3', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Total energy export tariff 3', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'total_energy_export_tariff_kwh', - 'unique_id': 'aabbccddeeff_total_power_export_t3_kwh', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.device_total_energy_export_tariff_3:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'Device Total energy export tariff 3', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.device_total_energy_export_tariff_3', - 'last_changed': , - 'last_updated': , - 'state': '8765.444', - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.device_total_energy_export_tariff_4:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'HWE-P1', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '4.19', - 'via_device_id': None, - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.device_total_energy_export_tariff_4:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_total_energy_export_tariff_4', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Total energy export tariff 4', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'total_energy_export_tariff_kwh', - 'unique_id': 'aabbccddeeff_total_power_export_t4_kwh', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.device_total_energy_export_tariff_4:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'Device Total energy export tariff 4', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.device_total_energy_export_tariff_4', - 'last_changed': , - 'last_updated': , - 'state': '8765.444', - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.device_total_energy_import:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'HWE-P1', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '4.19', - 'via_device_id': None, - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.device_total_energy_import:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_total_energy_import', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Total energy import', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'total_energy_import_kwh', - 'unique_id': 'aabbccddeeff_total_power_import_kwh', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.device_total_energy_import:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'Device Total energy import', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.device_total_energy_import', - 'last_changed': , - 'last_updated': , - 'state': '13779.338', - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.device_total_energy_import_tariff_1:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'HWE-P1', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '4.19', - 'via_device_id': None, - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.device_total_energy_import_tariff_1:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_total_energy_import_tariff_1', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Total energy import tariff 1', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'total_energy_import_tariff_kwh', - 'unique_id': 'aabbccddeeff_total_power_import_t1_kwh', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.device_total_energy_import_tariff_1:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'Device Total energy import tariff 1', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.device_total_energy_import_tariff_1', - 'last_changed': , - 'last_updated': , - 'state': '10830.511', - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.device_total_energy_import_tariff_2:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'HWE-P1', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '4.19', - 'via_device_id': None, - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.device_total_energy_import_tariff_2:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_total_energy_import_tariff_2', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Total energy import tariff 2', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'total_energy_import_tariff_kwh', - 'unique_id': 'aabbccddeeff_total_power_import_t2_kwh', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.device_total_energy_import_tariff_2:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'Device Total energy import tariff 2', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.device_total_energy_import_tariff_2', - 'last_changed': , - 'last_updated': , - 'state': '2948.827', - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.device_total_energy_import_tariff_3:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'HWE-P1', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '4.19', - 'via_device_id': None, - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.device_total_energy_import_tariff_3:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_total_energy_import_tariff_3', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Total energy import tariff 3', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'total_energy_import_tariff_kwh', - 'unique_id': 'aabbccddeeff_total_power_import_t3_kwh', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.device_total_energy_import_tariff_3:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'Device Total energy import tariff 3', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.device_total_energy_import_tariff_3', - 'last_changed': , - 'last_updated': , - 'state': '2948.827', - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.device_total_energy_import_tariff_4:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'HWE-P1', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '4.19', - 'via_device_id': None, - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.device_total_energy_import_tariff_4:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_total_energy_import_tariff_4', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Total energy import tariff 4', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'total_energy_import_tariff_kwh', - 'unique_id': 'aabbccddeeff_total_power_import_t4_kwh', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.device_total_energy_import_tariff_4:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'Device Total energy import tariff 4', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.device_total_energy_import_tariff_4', - 'last_changed': , - 'last_updated': , - 'state': '2948.827', - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.device_total_gas:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'HWE-P1', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '4.19', - 'via_device_id': None, - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.device_total_gas:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_total_gas', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Total gas', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'total_gas_m3', - 'unique_id': 'aabbccddeeff_total_gas_m3', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.device_total_gas:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'gas', - 'friendly_name': 'Device Total gas', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.device_total_gas', - 'last_changed': , - 'last_updated': , - 'state': '1122.333', - }) -# --- # name: test_sensors[HWE-P1-entity_ids0][sensor.device_total_water_usage:device-registry] DeviceRegistryEntrySnapshot({ 'area_id': None, @@ -7759,6 +5545,7 @@ 'context': , 'entity_id': 'sensor.device_total_water_usage', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '1234.567', }) @@ -7843,6 +5630,7 @@ 'context': , 'entity_id': 'sensor.device_voltage_phase_1', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '230.111', }) @@ -7927,6 +5715,7 @@ 'context': , 'entity_id': 'sensor.device_voltage_phase_2', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '230.222', }) @@ -8011,6 +5800,7 @@ 'context': , 'entity_id': 'sensor.device_voltage_phase_3', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '230.333', }) @@ -8090,6 +5880,7 @@ 'context': , 'entity_id': 'sensor.device_voltage_sags_detected_phase_1', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '1', }) @@ -8169,6 +5960,7 @@ 'context': , 'entity_id': 'sensor.device_voltage_sags_detected_phase_2', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '2', }) @@ -8248,6 +6040,7 @@ 'context': , 'entity_id': 'sensor.device_voltage_sags_detected_phase_3', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '3', }) @@ -8327,6 +6120,7 @@ 'context': , 'entity_id': 'sensor.device_voltage_swells_detected_phase_1', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '4', }) @@ -8406,6 +6200,7 @@ 'context': , 'entity_id': 'sensor.device_voltage_swells_detected_phase_2', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '5', }) @@ -8485,6 +6280,7 @@ 'context': , 'entity_id': 'sensor.device_voltage_swells_detected_phase_3', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '6', }) @@ -8568,6 +6364,7 @@ 'context': , 'entity_id': 'sensor.device_water_usage', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '12.345', }) @@ -8647,6 +6444,7 @@ 'context': , 'entity_id': 'sensor.device_wi_fi_ssid', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'My Wi-Fi', }) @@ -8730,82 +6528,11 @@ 'context': , 'entity_id': 'sensor.device_wi_fi_strength', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '100', }) # --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.gas_meter:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - 'G001', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'HWE-P1', - 'name': 'Gas meter', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': , - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.gas_meter:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.gas_meter', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': 'mdi:alphabetical-variant', - 'original_name': None, - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'meter_identifier', - 'unique_id': 'homewizard_G001_meter_identifier', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.gas_meter:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Gas meter', - 'icon': 'mdi:alphabetical-variant', - }), - 'context': , - 'entity_id': 'sensor.gas_meter', - 'last_changed': , - 'last_updated': , - 'state': 'G001', - }) -# --- # name: test_sensors[HWE-P1-entity_ids0][sensor.gas_meter_gas:device-registry] DeviceRegistryEntrySnapshot({ 'area_id': None, @@ -8882,158 +6609,11 @@ 'context': , 'entity_id': 'sensor.gas_meter_gas', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '111.111', }) # --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.gas_meter_total_gas:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - 'G001', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'HWE-P1', - 'name': 'Gas meter', - 'name_by_user': None, - 'serial_number': 'G001', - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': , - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.gas_meter_total_gas:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.gas_meter_total_gas', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Total gas', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'total_gas_m3', - 'unique_id': 'homewizard_G001', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.gas_meter_total_gas:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'gas', - 'friendly_name': 'Gas meter Total gas', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.gas_meter_total_gas', - 'last_changed': , - 'last_updated': , - 'state': '111.111', - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.heat_meter:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - 'H001', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'HWE-P1', - 'name': 'Heat meter', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': , - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.heat_meter:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.heat_meter', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': 'mdi:alphabetical-variant', - 'original_name': None, - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'meter_identifier', - 'unique_id': 'homewizard_H001_meter_identifier', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.heat_meter:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Heat meter', - 'icon': 'mdi:alphabetical-variant', - }), - 'context': , - 'entity_id': 'sensor.heat_meter', - 'last_changed': , - 'last_updated': , - 'state': 'H001', - }) -# --- # name: test_sensors[HWE-P1-entity_ids0][sensor.heat_meter_energy:device-registry] DeviceRegistryEntrySnapshot({ 'area_id': None, @@ -9110,158 +6690,11 @@ 'context': , 'entity_id': 'sensor.heat_meter_energy', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '444.444', }) # --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.heat_meter_total_heat_energy:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - 'H001', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'HWE-P1', - 'name': 'Heat meter', - 'name_by_user': None, - 'serial_number': 'H001', - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': , - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.heat_meter_total_heat_energy:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.heat_meter_total_heat_energy', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Total heat energy', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'total_energy_gj', - 'unique_id': 'homewizard_H001', - 'unit_of_measurement': 'GJ', - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.heat_meter_total_heat_energy:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'Heat meter Total heat energy', - 'state_class': , - 'unit_of_measurement': 'GJ', - }), - 'context': , - 'entity_id': 'sensor.heat_meter_total_heat_energy', - 'last_changed': , - 'last_updated': , - 'state': '444.444', - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.inlet_heat_meter:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - 'IH001', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'HWE-P1', - 'name': 'Inlet heat meter', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': , - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.inlet_heat_meter:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.inlet_heat_meter', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': 'mdi:alphabetical-variant', - 'original_name': None, - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'meter_identifier', - 'unique_id': 'homewizard_IH001_meter_identifier', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.inlet_heat_meter:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Inlet heat meter', - 'icon': 'mdi:alphabetical-variant', - }), - 'context': , - 'entity_id': 'sensor.inlet_heat_meter', - 'last_changed': , - 'last_updated': , - 'state': 'IH001', - }) -# --- # name: test_sensors[HWE-P1-entity_ids0][sensor.inlet_heat_meter_none:device-registry] DeviceRegistryEntrySnapshot({ 'area_id': None, @@ -9337,233 +6770,11 @@ 'context': , 'entity_id': 'sensor.inlet_heat_meter_none', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '555.555', }) # --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.inlet_heat_meter_total_heat_energy:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - 'IH001', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'HWE-P1', - 'name': 'Inlet heat meter', - 'name_by_user': None, - 'serial_number': 'IH001', - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': , - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.inlet_heat_meter_total_heat_energy:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.inlet_heat_meter_total_heat_energy', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Total heat energy', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'total_energy_gj', - 'unique_id': 'homewizard_IH001', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.inlet_heat_meter_total_heat_energy:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Inlet heat meter Total heat energy', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.inlet_heat_meter_total_heat_energy', - 'last_changed': , - 'last_updated': , - 'state': '555.555', - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.warm_water_meter:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - 'WW001', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'HWE-P1', - 'name': 'Warm water meter', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': , - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.warm_water_meter:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.warm_water_meter', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': 'mdi:alphabetical-variant', - 'original_name': None, - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'meter_identifier', - 'unique_id': 'homewizard_WW001_meter_identifier', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.warm_water_meter:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Warm water meter', - 'icon': 'mdi:alphabetical-variant', - }), - 'context': , - 'entity_id': 'sensor.warm_water_meter', - 'last_changed': , - 'last_updated': , - 'state': 'WW001', - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.warm_water_meter_total_water_usage:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - 'WW001', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'HWE-P1', - 'name': 'Warm water meter', - 'name_by_user': None, - 'serial_number': 'WW001', - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': , - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.warm_water_meter_total_water_usage:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.warm_water_meter_total_water_usage', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Total water usage', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'total_liter_m3', - 'unique_id': 'homewizard_WW001', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.warm_water_meter_total_water_usage:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'water', - 'friendly_name': 'Warm water meter Total water usage', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.warm_water_meter_total_water_usage', - 'last_changed': , - 'last_updated': , - 'state': '333.333', - }) -# --- # name: test_sensors[HWE-P1-entity_ids0][sensor.warm_water_meter_water:device-registry] DeviceRegistryEntrySnapshot({ 'area_id': None, @@ -9640,158 +6851,11 @@ 'context': , 'entity_id': 'sensor.warm_water_meter_water', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '333.333', }) # --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.water_meter:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - 'W001', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'HWE-P1', - 'name': 'Water meter', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': , - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.water_meter:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.water_meter', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': 'mdi:alphabetical-variant', - 'original_name': None, - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'meter_identifier', - 'unique_id': 'homewizard_W001_meter_identifier', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.water_meter:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Water meter', - 'icon': 'mdi:alphabetical-variant', - }), - 'context': , - 'entity_id': 'sensor.water_meter', - 'last_changed': , - 'last_updated': , - 'state': 'W001', - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.water_meter_total_water_usage:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - 'W001', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'HWE-P1', - 'name': 'Water meter', - 'name_by_user': None, - 'serial_number': 'W001', - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': , - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.water_meter_total_water_usage:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.water_meter_total_water_usage', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Total water usage', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'total_liter_m3', - 'unique_id': 'homewizard_W001', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[HWE-P1-entity_ids0][sensor.water_meter_total_water_usage:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'water', - 'friendly_name': 'Water meter Total water usage', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.water_meter_total_water_usage', - 'last_changed': , - 'last_updated': , - 'state': '222.222', - }) -# --- # name: test_sensors[HWE-P1-entity_ids0][sensor.water_meter_water:device-registry] DeviceRegistryEntrySnapshot({ 'area_id': None, @@ -9868,1058 +6932,11 @@ 'context': , 'entity_id': 'sensor.water_meter_water', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '222.222', }) # --- -# name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_active_average_demand:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'HWE-P1', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '4.19', - 'via_device_id': None, - }) -# --- -# name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_active_average_demand:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_active_average_demand', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Active average demand', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'active_power_average_w', - 'unique_id': 'aabbccddeeff_active_power_average_w', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_active_average_demand:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'power', - 'friendly_name': 'Device Active average demand', - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.device_active_average_demand', - 'last_changed': , - 'last_updated': , - 'state': '0', - }) -# --- -# name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_active_current_phase_1:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'HWE-P1', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '4.19', - 'via_device_id': None, - }) -# --- -# name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_active_current_phase_1:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_active_current_phase_1', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Active current phase 1', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'active_current_phase_a', - 'unique_id': 'aabbccddeeff_active_current_l1_a', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_active_current_phase_1:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'current', - 'friendly_name': 'Device Active current phase 1', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.device_active_current_phase_1', - 'last_changed': , - 'last_updated': , - 'state': '0', - }) -# --- -# name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_active_current_phase_2:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'HWE-P1', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '4.19', - 'via_device_id': None, - }) -# --- -# name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_active_current_phase_2:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_active_current_phase_2', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Active current phase 2', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'active_current_phase_a', - 'unique_id': 'aabbccddeeff_active_current_l2_a', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_active_current_phase_2:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'current', - 'friendly_name': 'Device Active current phase 2', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.device_active_current_phase_2', - 'last_changed': , - 'last_updated': , - 'state': '0', - }) -# --- -# name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_active_current_phase_3:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'HWE-P1', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '4.19', - 'via_device_id': None, - }) -# --- -# name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_active_current_phase_3:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_active_current_phase_3', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Active current phase 3', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'active_current_phase_a', - 'unique_id': 'aabbccddeeff_active_current_l3_a', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_active_current_phase_3:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'current', - 'friendly_name': 'Device Active current phase 3', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.device_active_current_phase_3', - 'last_changed': , - 'last_updated': , - 'state': '0', - }) -# --- -# name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_active_frequency:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'HWE-P1', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '4.19', - 'via_device_id': None, - }) -# --- -# name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_active_frequency:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_active_frequency', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Active frequency', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'active_frequency_hz', - 'unique_id': 'aabbccddeeff_active_frequency_hz', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_active_frequency:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'frequency', - 'friendly_name': 'Device Active frequency', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.device_active_frequency', - 'last_changed': , - 'last_updated': , - 'state': '0', - }) -# --- -# name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_active_power:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'HWE-P1', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '4.19', - 'via_device_id': None, - }) -# --- -# name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_active_power:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_active_power', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 0, - }), - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Active power', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'active_power_w', - 'unique_id': 'aabbccddeeff_active_power_w', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_active_power:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'power', - 'friendly_name': 'Device Active power', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.device_active_power', - 'last_changed': , - 'last_updated': , - 'state': '0.0', - }) -# --- -# name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_active_power_phase_1:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'HWE-P1', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '4.19', - 'via_device_id': None, - }) -# --- -# name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_active_power_phase_1:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_active_power_phase_1', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 0, - }), - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Active power phase 1', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'active_power_phase_w', - 'unique_id': 'aabbccddeeff_active_power_l1_w', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_active_power_phase_1:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'power', - 'friendly_name': 'Device Active power phase 1', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.device_active_power_phase_1', - 'last_changed': , - 'last_updated': , - 'state': '0.0', - }) -# --- -# name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_active_power_phase_2:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'HWE-P1', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '4.19', - 'via_device_id': None, - }) -# --- -# name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_active_power_phase_2:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_active_power_phase_2', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 0, - }), - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Active power phase 2', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'active_power_phase_w', - 'unique_id': 'aabbccddeeff_active_power_l2_w', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_active_power_phase_2:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'power', - 'friendly_name': 'Device Active power phase 2', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.device_active_power_phase_2', - 'last_changed': , - 'last_updated': , - 'state': '0.0', - }) -# --- -# name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_active_power_phase_3:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'HWE-P1', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '4.19', - 'via_device_id': None, - }) -# --- -# name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_active_power_phase_3:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_active_power_phase_3', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 0, - }), - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Active power phase 3', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'active_power_phase_w', - 'unique_id': 'aabbccddeeff_active_power_l3_w', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_active_power_phase_3:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'power', - 'friendly_name': 'Device Active power phase 3', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.device_active_power_phase_3', - 'last_changed': , - 'last_updated': , - 'state': '0.0', - }) -# --- -# name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_active_voltage_phase_1:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'HWE-P1', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '4.19', - 'via_device_id': None, - }) -# --- -# name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_active_voltage_phase_1:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_active_voltage_phase_1', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Active voltage phase 1', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'active_voltage_phase_v', - 'unique_id': 'aabbccddeeff_active_voltage_l1_v', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_active_voltage_phase_1:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'voltage', - 'friendly_name': 'Device Active voltage phase 1', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.device_active_voltage_phase_1', - 'last_changed': , - 'last_updated': , - 'state': '0.0', - }) -# --- -# name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_active_voltage_phase_2:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'HWE-P1', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '4.19', - 'via_device_id': None, - }) -# --- -# name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_active_voltage_phase_2:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_active_voltage_phase_2', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Active voltage phase 2', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'active_voltage_phase_v', - 'unique_id': 'aabbccddeeff_active_voltage_l2_v', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_active_voltage_phase_2:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'voltage', - 'friendly_name': 'Device Active voltage phase 2', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.device_active_voltage_phase_2', - 'last_changed': , - 'last_updated': , - 'state': '0.0', - }) -# --- -# name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_active_voltage_phase_3:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'HWE-P1', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '4.19', - 'via_device_id': None, - }) -# --- -# name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_active_voltage_phase_3:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_active_voltage_phase_3', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Active voltage phase 3', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'active_voltage_phase_v', - 'unique_id': 'aabbccddeeff_active_voltage_l3_v', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_active_voltage_phase_3:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'voltage', - 'friendly_name': 'Device Active voltage phase 3', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.device_active_voltage_phase_3', - 'last_changed': , - 'last_updated': , - 'state': '0.0', - }) -# --- -# name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_active_water_usage:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'HWE-P1', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '4.19', - 'via_device_id': None, - }) -# --- -# name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_active_water_usage:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_active_water_usage', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Active water usage', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'active_liter_lpm', - 'unique_id': 'aabbccddeeff_active_liter_lpm', - 'unit_of_measurement': 'l/min', - }) -# --- -# name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_active_water_usage:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Device Active water usage', - 'state_class': , - 'unit_of_measurement': 'l/min', - }), - 'context': , - 'entity_id': 'sensor.device_active_water_usage', - 'last_changed': , - 'last_updated': , - 'state': '0.0', - }) -# --- # name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_average_demand:device-registry] DeviceRegistryEntrySnapshot({ 'area_id': None, @@ -10997,6 +7014,7 @@ 'context': , 'entity_id': 'sensor.device_average_demand', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0', }) @@ -11081,6 +7099,7 @@ 'context': , 'entity_id': 'sensor.device_current_phase_1', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0', }) @@ -11165,6 +7184,7 @@ 'context': , 'entity_id': 'sensor.device_current_phase_2', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0', }) @@ -11249,6 +7269,7 @@ 'context': , 'entity_id': 'sensor.device_current_phase_3', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0', }) @@ -11333,6 +7354,7 @@ 'context': , 'entity_id': 'sensor.device_energy_export', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0.0', }) @@ -11417,6 +7439,7 @@ 'context': , 'entity_id': 'sensor.device_energy_export_tariff_1', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0.0', }) @@ -11501,6 +7524,7 @@ 'context': , 'entity_id': 'sensor.device_energy_export_tariff_2', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0.0', }) @@ -11585,6 +7609,7 @@ 'context': , 'entity_id': 'sensor.device_energy_export_tariff_3', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0.0', }) @@ -11669,6 +7694,7 @@ 'context': , 'entity_id': 'sensor.device_energy_export_tariff_4', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0.0', }) @@ -11753,6 +7779,7 @@ 'context': , 'entity_id': 'sensor.device_energy_import', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0.0', }) @@ -11837,6 +7864,7 @@ 'context': , 'entity_id': 'sensor.device_energy_import_tariff_1', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0.0', }) @@ -11921,6 +7949,7 @@ 'context': , 'entity_id': 'sensor.device_energy_import_tariff_2', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0.0', }) @@ -12005,6 +8034,7 @@ 'context': , 'entity_id': 'sensor.device_energy_import_tariff_3', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0.0', }) @@ -12089,6 +8119,7 @@ 'context': , 'entity_id': 'sensor.device_energy_import_tariff_4', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0.0', }) @@ -12173,6 +8204,7 @@ 'context': , 'entity_id': 'sensor.device_frequency', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0', }) @@ -12252,87 +8284,11 @@ 'context': , 'entity_id': 'sensor.device_long_power_failures_detected', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0', }) # --- -# name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_peak_demand_current_month:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'HWE-P1', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '4.19', - 'via_device_id': None, - }) -# --- -# name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_peak_demand_current_month:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_peak_demand_current_month', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Peak demand current month', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'monthly_power_peak_w', - 'unique_id': 'aabbccddeeff_monthly_power_peak_w', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_peak_demand_current_month:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'power', - 'friendly_name': 'Device Peak demand current month', - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.device_peak_demand_current_month', - 'last_changed': , - 'last_updated': , - 'state': '0.0', - }) -# --- # name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_power:device-registry] DeviceRegistryEntrySnapshot({ 'area_id': None, @@ -12416,6 +8372,7 @@ 'context': , 'entity_id': 'sensor.device_power', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0.0', }) @@ -12495,6 +8452,7 @@ 'context': , 'entity_id': 'sensor.device_power_failures_detected', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0', }) @@ -12582,6 +8540,7 @@ 'context': , 'entity_id': 'sensor.device_power_phase_1', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0.0', }) @@ -12669,6 +8628,7 @@ 'context': , 'entity_id': 'sensor.device_power_phase_2', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0.0', }) @@ -12756,886 +8716,7 @@ 'context': , 'entity_id': 'sensor.device_power_phase_3', 'last_changed': , - 'last_updated': , - 'state': '0.0', - }) -# --- -# name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_total_energy_export:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'HWE-P1', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '4.19', - 'via_device_id': None, - }) -# --- -# name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_total_energy_export:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_total_energy_export', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Total energy export', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'total_energy_export_kwh', - 'unique_id': 'aabbccddeeff_total_power_export_kwh', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_total_energy_export:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'Device Total energy export', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.device_total_energy_export', - 'last_changed': , - 'last_updated': , - 'state': '0.0', - }) -# --- -# name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_total_energy_export_tariff_1:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'HWE-P1', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '4.19', - 'via_device_id': None, - }) -# --- -# name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_total_energy_export_tariff_1:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_total_energy_export_tariff_1', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Total energy export tariff 1', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'total_energy_export_tariff_kwh', - 'unique_id': 'aabbccddeeff_total_power_export_t1_kwh', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_total_energy_export_tariff_1:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'Device Total energy export tariff 1', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.device_total_energy_export_tariff_1', - 'last_changed': , - 'last_updated': , - 'state': '0.0', - }) -# --- -# name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_total_energy_export_tariff_2:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'HWE-P1', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '4.19', - 'via_device_id': None, - }) -# --- -# name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_total_energy_export_tariff_2:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_total_energy_export_tariff_2', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Total energy export tariff 2', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'total_energy_export_tariff_kwh', - 'unique_id': 'aabbccddeeff_total_power_export_t2_kwh', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_total_energy_export_tariff_2:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'Device Total energy export tariff 2', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.device_total_energy_export_tariff_2', - 'last_changed': , - 'last_updated': , - 'state': '0.0', - }) -# --- -# name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_total_energy_export_tariff_3:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'HWE-P1', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '4.19', - 'via_device_id': None, - }) -# --- -# name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_total_energy_export_tariff_3:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_total_energy_export_tariff_3', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Total energy export tariff 3', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'total_energy_export_tariff_kwh', - 'unique_id': 'aabbccddeeff_total_power_export_t3_kwh', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_total_energy_export_tariff_3:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'Device Total energy export tariff 3', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.device_total_energy_export_tariff_3', - 'last_changed': , - 'last_updated': , - 'state': '0.0', - }) -# --- -# name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_total_energy_export_tariff_4:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'HWE-P1', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '4.19', - 'via_device_id': None, - }) -# --- -# name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_total_energy_export_tariff_4:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_total_energy_export_tariff_4', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Total energy export tariff 4', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'total_energy_export_tariff_kwh', - 'unique_id': 'aabbccddeeff_total_power_export_t4_kwh', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_total_energy_export_tariff_4:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'Device Total energy export tariff 4', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.device_total_energy_export_tariff_4', - 'last_changed': , - 'last_updated': , - 'state': '0.0', - }) -# --- -# name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_total_energy_import:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'HWE-P1', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '4.19', - 'via_device_id': None, - }) -# --- -# name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_total_energy_import:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_total_energy_import', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Total energy import', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'total_energy_import_kwh', - 'unique_id': 'aabbccddeeff_total_power_import_kwh', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_total_energy_import:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'Device Total energy import', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.device_total_energy_import', - 'last_changed': , - 'last_updated': , - 'state': '0.0', - }) -# --- -# name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_total_energy_import_tariff_1:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'HWE-P1', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '4.19', - 'via_device_id': None, - }) -# --- -# name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_total_energy_import_tariff_1:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_total_energy_import_tariff_1', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Total energy import tariff 1', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'total_energy_import_tariff_kwh', - 'unique_id': 'aabbccddeeff_total_power_import_t1_kwh', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_total_energy_import_tariff_1:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'Device Total energy import tariff 1', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.device_total_energy_import_tariff_1', - 'last_changed': , - 'last_updated': , - 'state': '0.0', - }) -# --- -# name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_total_energy_import_tariff_2:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'HWE-P1', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '4.19', - 'via_device_id': None, - }) -# --- -# name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_total_energy_import_tariff_2:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_total_energy_import_tariff_2', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Total energy import tariff 2', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'total_energy_import_tariff_kwh', - 'unique_id': 'aabbccddeeff_total_power_import_t2_kwh', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_total_energy_import_tariff_2:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'Device Total energy import tariff 2', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.device_total_energy_import_tariff_2', - 'last_changed': , - 'last_updated': , - 'state': '0.0', - }) -# --- -# name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_total_energy_import_tariff_3:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'HWE-P1', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '4.19', - 'via_device_id': None, - }) -# --- -# name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_total_energy_import_tariff_3:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_total_energy_import_tariff_3', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Total energy import tariff 3', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'total_energy_import_tariff_kwh', - 'unique_id': 'aabbccddeeff_total_power_import_t3_kwh', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_total_energy_import_tariff_3:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'Device Total energy import tariff 3', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.device_total_energy_import_tariff_3', - 'last_changed': , - 'last_updated': , - 'state': '0.0', - }) -# --- -# name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_total_energy_import_tariff_4:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'HWE-P1', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '4.19', - 'via_device_id': None, - }) -# --- -# name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_total_energy_import_tariff_4:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_total_energy_import_tariff_4', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Total energy import tariff 4', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'total_energy_import_tariff_kwh', - 'unique_id': 'aabbccddeeff_total_power_import_t4_kwh', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_total_energy_import_tariff_4:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'Device Total energy import tariff 4', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.device_total_energy_import_tariff_4', - 'last_changed': , - 'last_updated': , - 'state': '0.0', - }) -# --- -# name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_total_gas:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'HWE-P1', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '4.19', - 'via_device_id': None, - }) -# --- -# name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_total_gas:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_total_gas', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Total gas', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'total_gas_m3', - 'unique_id': 'aabbccddeeff_total_gas_m3', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_total_gas:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'gas', - 'friendly_name': 'Device Total gas', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.device_total_gas', - 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0.0', }) @@ -13720,6 +8801,7 @@ 'context': , 'entity_id': 'sensor.device_total_water_usage', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0.0', }) @@ -13804,6 +8886,7 @@ 'context': , 'entity_id': 'sensor.device_voltage_phase_1', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0.0', }) @@ -13888,6 +8971,7 @@ 'context': , 'entity_id': 'sensor.device_voltage_phase_2', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0.0', }) @@ -13972,6 +9056,7 @@ 'context': , 'entity_id': 'sensor.device_voltage_phase_3', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0.0', }) @@ -14051,6 +9136,7 @@ 'context': , 'entity_id': 'sensor.device_voltage_sags_detected_phase_1', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0', }) @@ -14130,6 +9216,7 @@ 'context': , 'entity_id': 'sensor.device_voltage_sags_detected_phase_2', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0', }) @@ -14209,6 +9296,7 @@ 'context': , 'entity_id': 'sensor.device_voltage_sags_detected_phase_3', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0', }) @@ -14288,6 +9376,7 @@ 'context': , 'entity_id': 'sensor.device_voltage_swells_detected_phase_1', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0', }) @@ -14367,6 +9456,7 @@ 'context': , 'entity_id': 'sensor.device_voltage_swells_detected_phase_2', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0', }) @@ -14446,6 +9536,7 @@ 'context': , 'entity_id': 'sensor.device_voltage_swells_detected_phase_3', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0', }) @@ -14529,177 +9620,12 @@ 'context': , 'entity_id': 'sensor.device_water_usage', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0.0', }) # --- -# name: test_sensors[HWE-SKT-entity_ids2][sensor.device_active_power:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'HWE-SKT', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '3.03', - 'via_device_id': None, - }) -# --- -# name: test_sensors[HWE-SKT-entity_ids2][sensor.device_active_power:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_active_power', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 0, - }), - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Active power', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'active_power_w', - 'unique_id': 'aabbccddeeff_active_power_w', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[HWE-SKT-entity_ids2][sensor.device_active_power:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'power', - 'friendly_name': 'Device Active power', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.device_active_power', - 'last_changed': , - 'last_updated': , - 'state': '1457.277', - }) -# --- -# name: test_sensors[HWE-SKT-entity_ids2][sensor.device_active_power_phase_1:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'HWE-SKT', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '3.03', - 'via_device_id': None, - }) -# --- -# name: test_sensors[HWE-SKT-entity_ids2][sensor.device_active_power_phase_1:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_active_power_phase_1', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 0, - }), - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Active power phase 1', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'active_power_phase_w', - 'unique_id': 'aabbccddeeff_active_power_l1_w', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[HWE-SKT-entity_ids2][sensor.device_active_power_phase_1:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'power', - 'friendly_name': 'Device Active power phase 1', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.device_active_power_phase_1', - 'last_changed': , - 'last_updated': , - 'state': '1457.277', - }) -# --- -# name: test_sensors[HWE-SKT-entity_ids2][sensor.device_energy_export:device-registry] +# name: test_sensors[HWE-SKT-11-entity_ids2][sensor.device_energy_export:device-registry] DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , @@ -14733,7 +9659,7 @@ 'via_device_id': None, }) # --- -# name: test_sensors[HWE-SKT-entity_ids2][sensor.device_energy_export:entity-registry] +# name: test_sensors[HWE-SKT-11-entity_ids2][sensor.device_energy_export:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -14768,7 +9694,7 @@ 'unit_of_measurement': , }) # --- -# name: test_sensors[HWE-SKT-entity_ids2][sensor.device_energy_export:state] +# name: test_sensors[HWE-SKT-11-entity_ids2][sensor.device_energy_export:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'energy', @@ -14779,11 +9705,12 @@ 'context': , 'entity_id': 'sensor.device_energy_export', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0', }) # --- -# name: test_sensors[HWE-SKT-entity_ids2][sensor.device_energy_import:device-registry] +# name: test_sensors[HWE-SKT-11-entity_ids2][sensor.device_energy_import:device-registry] DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , @@ -14817,7 +9744,7 @@ 'via_device_id': None, }) # --- -# name: test_sensors[HWE-SKT-entity_ids2][sensor.device_energy_import:entity-registry] +# name: test_sensors[HWE-SKT-11-entity_ids2][sensor.device_energy_import:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -14852,7 +9779,7 @@ 'unit_of_measurement': , }) # --- -# name: test_sensors[HWE-SKT-entity_ids2][sensor.device_energy_import:state] +# name: test_sensors[HWE-SKT-11-entity_ids2][sensor.device_energy_import:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'energy', @@ -14863,11 +9790,12 @@ 'context': , 'entity_id': 'sensor.device_energy_import', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '63.651', }) # --- -# name: test_sensors[HWE-SKT-entity_ids2][sensor.device_power:device-registry] +# name: test_sensors[HWE-SKT-11-entity_ids2][sensor.device_power:device-registry] DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , @@ -14901,7 +9829,7 @@ 'via_device_id': None, }) # --- -# name: test_sensors[HWE-SKT-entity_ids2][sensor.device_power:entity-registry] +# name: test_sensors[HWE-SKT-11-entity_ids2][sensor.device_power:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -14939,7 +9867,7 @@ 'unit_of_measurement': , }) # --- -# name: test_sensors[HWE-SKT-entity_ids2][sensor.device_power:state] +# name: test_sensors[HWE-SKT-11-entity_ids2][sensor.device_power:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'power', @@ -14950,11 +9878,12 @@ 'context': , 'entity_id': 'sensor.device_power', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '1457.277', }) # --- -# name: test_sensors[HWE-SKT-entity_ids2][sensor.device_power_phase_1:device-registry] +# name: test_sensors[HWE-SKT-11-entity_ids2][sensor.device_power_phase_1:device-registry] DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , @@ -14988,7 +9917,7 @@ 'via_device_id': None, }) # --- -# name: test_sensors[HWE-SKT-entity_ids2][sensor.device_power_phase_1:entity-registry] +# name: test_sensors[HWE-SKT-11-entity_ids2][sensor.device_power_phase_1:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -15026,7 +9955,7 @@ 'unit_of_measurement': , }) # --- -# name: test_sensors[HWE-SKT-entity_ids2][sensor.device_power_phase_1:state] +# name: test_sensors[HWE-SKT-11-entity_ids2][sensor.device_power_phase_1:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'power', @@ -15037,171 +9966,12 @@ 'context': , 'entity_id': 'sensor.device_power_phase_1', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '1457.277', }) # --- -# name: test_sensors[HWE-SKT-entity_ids2][sensor.device_total_energy_export:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'HWE-SKT', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '3.03', - 'via_device_id': None, - }) -# --- -# name: test_sensors[HWE-SKT-entity_ids2][sensor.device_total_energy_export:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_total_energy_export', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Total energy export', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'total_energy_export_kwh', - 'unique_id': 'aabbccddeeff_total_power_export_kwh', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[HWE-SKT-entity_ids2][sensor.device_total_energy_export:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'Device Total energy export', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.device_total_energy_export', - 'last_changed': , - 'last_updated': , - 'state': '0', - }) -# --- -# name: test_sensors[HWE-SKT-entity_ids2][sensor.device_total_energy_import:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'HWE-SKT', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '3.03', - 'via_device_id': None, - }) -# --- -# name: test_sensors[HWE-SKT-entity_ids2][sensor.device_total_energy_import:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_total_energy_import', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Total energy import', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'total_energy_import_kwh', - 'unique_id': 'aabbccddeeff_total_power_import_kwh', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[HWE-SKT-entity_ids2][sensor.device_total_energy_import:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'Device Total energy import', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.device_total_energy_import', - 'last_changed': , - 'last_updated': , - 'state': '63.651', - }) -# --- -# name: test_sensors[HWE-SKT-entity_ids2][sensor.device_wi_fi_ssid:device-registry] +# name: test_sensors[HWE-SKT-11-entity_ids2][sensor.device_wi_fi_ssid:device-registry] DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , @@ -15235,7 +10005,7 @@ 'via_device_id': None, }) # --- -# name: test_sensors[HWE-SKT-entity_ids2][sensor.device_wi_fi_ssid:entity-registry] +# name: test_sensors[HWE-SKT-11-entity_ids2][sensor.device_wi_fi_ssid:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -15268,7 +10038,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_sensors[HWE-SKT-entity_ids2][sensor.device_wi_fi_ssid:state] +# name: test_sensors[HWE-SKT-11-entity_ids2][sensor.device_wi_fi_ssid:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Device Wi-Fi SSID', @@ -15276,11 +10046,12 @@ 'context': , 'entity_id': 'sensor.device_wi_fi_ssid', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'My Wi-Fi', }) # --- -# name: test_sensors[HWE-SKT-entity_ids2][sensor.device_wi_fi_strength:device-registry] +# name: test_sensors[HWE-SKT-11-entity_ids2][sensor.device_wi_fi_strength:device-registry] DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , @@ -15314,7 +10085,7 @@ 'via_device_id': None, }) # --- -# name: test_sensors[HWE-SKT-entity_ids2][sensor.device_wi_fi_strength:entity-registry] +# name: test_sensors[HWE-SKT-11-entity_ids2][sensor.device_wi_fi_strength:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -15349,7 +10120,7 @@ 'unit_of_measurement': '%', }) # --- -# name: test_sensors[HWE-SKT-entity_ids2][sensor.device_wi_fi_strength:state] +# name: test_sensors[HWE-SKT-11-entity_ids2][sensor.device_wi_fi_strength:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Device Wi-Fi strength', @@ -15359,90 +10130,12 @@ 'context': , 'entity_id': 'sensor.device_wi_fi_strength', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '94', }) # --- -# name: test_sensors[HWE-WTR-entity_ids3][sensor.device_active_water_usage:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'HWE-WTR', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '2.03', - 'via_device_id': None, - }) -# --- -# name: test_sensors[HWE-WTR-entity_ids3][sensor.device_active_water_usage:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_active_water_usage', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Active water usage', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'active_liter_lpm', - 'unique_id': 'aabbccddeeff_active_liter_lpm', - 'unit_of_measurement': 'l/min', - }) -# --- -# name: test_sensors[HWE-WTR-entity_ids3][sensor.device_active_water_usage:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Device Active water usage', - 'state_class': , - 'unit_of_measurement': 'l/min', - }), - 'context': , - 'entity_id': 'sensor.device_active_water_usage', - 'last_changed': , - 'last_updated': , - 'state': '0', - }) -# --- -# name: test_sensors[HWE-WTR-entity_ids3][sensor.device_total_water_usage:device-registry] +# name: test_sensors[HWE-SKT-21-entity_ids3][sensor.device_apparent_power:device-registry] DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , @@ -15467,992 +10160,16 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'HWE-WTR', + 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, 'serial_number': None, 'suggested_area': None, - 'sw_version': '2.03', + 'sw_version': '4.07', 'via_device_id': None, }) # --- -# name: test_sensors[HWE-WTR-entity_ids3][sensor.device_total_water_usage:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_total_water_usage', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Total water usage', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'total_liter_m3', - 'unique_id': 'aabbccddeeff_total_liter_m3', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[HWE-WTR-entity_ids3][sensor.device_total_water_usage:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'water', - 'friendly_name': 'Device Total water usage', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.device_total_water_usage', - 'last_changed': , - 'last_updated': , - 'state': '17.014', - }) -# --- -# name: test_sensors[HWE-WTR-entity_ids3][sensor.device_water_usage:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': 'HomeWizard', - 'model': 'HWE-WTR', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '2.03', - 'via_device_id': None, - }) -# --- -# name: test_sensors[HWE-WTR-entity_ids3][sensor.device_water_usage:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_water_usage', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Water usage', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'active_liter_lpm', - 'unique_id': 'aabbccddeeff_active_liter_lpm', - 'unit_of_measurement': 'l/min', - }) -# --- -# name: test_sensors[HWE-WTR-entity_ids3][sensor.device_water_usage:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Device Water usage', - 'state_class': , - 'unit_of_measurement': 'l/min', - }), - 'context': , - 'entity_id': 'sensor.device_water_usage', - 'last_changed': , - 'last_updated': , - 'state': '0', - }) -# --- -# name: test_sensors[HWE-WTR-entity_ids3][sensor.device_wi_fi_ssid:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': 'HomeWizard', - 'model': 'HWE-WTR', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '2.03', - 'via_device_id': None, - }) -# --- -# name: test_sensors[HWE-WTR-entity_ids3][sensor.device_wi_fi_ssid:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.device_wi_fi_ssid', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Wi-Fi SSID', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'wifi_ssid', - 'unique_id': 'aabbccddeeff_wifi_ssid', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensors[HWE-WTR-entity_ids3][sensor.device_wi_fi_ssid:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Device Wi-Fi SSID', - }), - 'context': , - 'entity_id': 'sensor.device_wi_fi_ssid', - 'last_changed': , - 'last_updated': , - 'state': 'My Wi-Fi', - }) -# --- -# name: test_sensors[HWE-WTR-entity_ids3][sensor.device_wi_fi_strength:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': 'HomeWizard', - 'model': 'HWE-WTR', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '2.03', - 'via_device_id': None, - }) -# --- -# name: test_sensors[HWE-WTR-entity_ids3][sensor.device_wi_fi_strength:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.device_wi_fi_strength', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Wi-Fi strength', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'wifi_strength', - 'unique_id': 'aabbccddeeff_wifi_strength', - 'unit_of_measurement': '%', - }) -# --- -# name: test_sensors[HWE-WTR-entity_ids3][sensor.device_wi_fi_strength:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Device Wi-Fi strength', - 'state_class': , - 'unit_of_measurement': '%', - }), - 'context': , - 'entity_id': 'sensor.device_wi_fi_strength', - 'last_changed': , - 'last_updated': , - 'state': '84', - }) -# --- -# name: test_sensors[SDM230-entity_ids4][sensor.device_active_apparent_power:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'SDM230-wifi', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '3.06', - 'via_device_id': None, - }) -# --- -# name: test_sensors[SDM230-entity_ids4][sensor.device_active_apparent_power:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_active_apparent_power', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Active apparent power', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'active_apparent_power_va', - 'unique_id': 'aabbccddeeff_active_apparent_power_va', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[SDM230-entity_ids4][sensor.device_active_apparent_power:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'apparent_power', - 'friendly_name': 'Device Active apparent power', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.device_active_apparent_power', - 'last_changed': , - 'last_updated': , - 'state': '74.052', - }) -# --- -# name: test_sensors[SDM230-entity_ids4][sensor.device_active_current:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'SDM230-wifi', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '3.06', - 'via_device_id': None, - }) -# --- -# name: test_sensors[SDM230-entity_ids4][sensor.device_active_current:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_active_current', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Active current', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'active_current_a', - 'unique_id': 'aabbccddeeff_active_current_a', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[SDM230-entity_ids4][sensor.device_active_current:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'current', - 'friendly_name': 'Device Active current', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.device_active_current', - 'last_changed': , - 'last_updated': , - 'state': '0.273', - }) -# --- -# name: test_sensors[SDM230-entity_ids4][sensor.device_active_frequency:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'SDM230-wifi', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '3.06', - 'via_device_id': None, - }) -# --- -# name: test_sensors[SDM230-entity_ids4][sensor.device_active_frequency:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_active_frequency', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Active frequency', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'active_frequency_hz', - 'unique_id': 'aabbccddeeff_active_frequency_hz', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[SDM230-entity_ids4][sensor.device_active_frequency:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'frequency', - 'friendly_name': 'Device Active frequency', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.device_active_frequency', - 'last_changed': , - 'last_updated': , - 'state': '50', - }) -# --- -# name: test_sensors[SDM230-entity_ids4][sensor.device_active_power:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'SDM230-wifi', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '3.06', - 'via_device_id': None, - }) -# --- -# name: test_sensors[SDM230-entity_ids4][sensor.device_active_power:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_active_power', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 0, - }), - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Active power', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'active_power_w', - 'unique_id': 'aabbccddeeff_active_power_w', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[SDM230-entity_ids4][sensor.device_active_power:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'power', - 'friendly_name': 'Device Active power', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.device_active_power', - 'last_changed': , - 'last_updated': , - 'state': '-1058.296', - }) -# --- -# name: test_sensors[SDM230-entity_ids4][sensor.device_active_power_factor:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'SDM230-wifi', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '3.06', - 'via_device_id': None, - }) -# --- -# name: test_sensors[SDM230-entity_ids4][sensor.device_active_power_factor:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_active_power_factor', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Active power factor', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'active_power_factor', - 'unique_id': 'aabbccddeeff_active_power_factor', - 'unit_of_measurement': '%', - }) -# --- -# name: test_sensors[SDM230-entity_ids4][sensor.device_active_power_factor:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'power_factor', - 'friendly_name': 'Device Active power factor', - 'state_class': , - 'unit_of_measurement': '%', - }), - 'context': , - 'entity_id': 'sensor.device_active_power_factor', - 'last_changed': , - 'last_updated': , - 'state': '61.1', - }) -# --- -# name: test_sensors[SDM230-entity_ids4][sensor.device_active_power_phase_1:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'SDM230-wifi', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '3.06', - 'via_device_id': None, - }) -# --- -# name: test_sensors[SDM230-entity_ids4][sensor.device_active_power_phase_1:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_active_power_phase_1', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 0, - }), - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Active power phase 1', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'active_power_phase_w', - 'unique_id': 'aabbccddeeff_active_power_l1_w', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[SDM230-entity_ids4][sensor.device_active_power_phase_1:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'power', - 'friendly_name': 'Device Active power phase 1', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.device_active_power_phase_1', - 'last_changed': , - 'last_updated': , - 'state': '-1058.296', - }) -# --- -# name: test_sensors[SDM230-entity_ids4][sensor.device_active_reactive_power:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'SDM230-wifi', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '3.06', - 'via_device_id': None, - }) -# --- -# name: test_sensors[SDM230-entity_ids4][sensor.device_active_reactive_power:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_power', - 'entity_id': 'sensor.device_active_reactive_power', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Active reactive power', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'active_reactive_power_var', - 'unique_id': 'aabbccddeeff_active_reactive_power_var', - 'unit_of_measurement': 'var', - }) -# --- -# name: test_sensors[SDM230-entity_ids4][sensor.device_active_reactive_power:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'reactive_power', - 'friendly_name': 'Device Active reactive power', - 'state_class': , - 'unit_of_measurement': 'var', - }), - 'context': , - 'entity_id': 'sensor.device_active_reactive_power', - 'last_changed': , - 'last_updated': , - 'state': '-58.612', - }) -# --- -# name: test_sensors[SDM230-entity_ids4][sensor.device_active_voltage:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'SDM230-wifi', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '3.06', - 'via_device_id': None, - }) -# --- -# name: test_sensors[SDM230-entity_ids4][sensor.device_active_voltage:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_active_voltage', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Power', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': 'aabbccddeeff_active_power_w', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[SDM230-entity_ids4][sensor.device_active_voltage:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'voltage', - 'friendly_name': 'Device Active voltage', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.device_active_voltage', - 'last_changed': , - 'last_updated': , - 'state': '228.472', - }) -# --- -# name: test_sensors[SDM230-entity_ids4][sensor.device_apparent_power:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': 'HomeWizard', - 'model': 'SDM230-wifi', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '3.06', - 'via_device_id': None, - }) -# --- -# name: test_sensors[SDM230-entity_ids4][sensor.device_apparent_power:entity-registry] +# name: test_sensors[HWE-SKT-21-entity_ids3][sensor.device_apparent_power:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -16487,7 +10204,7 @@ 'unit_of_measurement': , }) # --- -# name: test_sensors[SDM230-entity_ids4][sensor.device_apparent_power:state] +# name: test_sensors[HWE-SKT-21-entity_ids3][sensor.device_apparent_power:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'apparent_power', @@ -16498,11 +10215,12 @@ 'context': , 'entity_id': 'sensor.device_apparent_power', 'last_changed': , + 'last_reported': , 'last_updated': , - 'state': '74.052', + 'state': '666.768', }) # --- -# name: test_sensors[SDM230-entity_ids4][sensor.device_current:device-registry] +# name: test_sensors[HWE-SKT-21-entity_ids3][sensor.device_current:device-registry] DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , @@ -16527,16 +10245,16 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'SDM230-wifi', + 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, 'serial_number': None, 'suggested_area': None, - 'sw_version': '3.06', + 'sw_version': '4.07', 'via_device_id': None, }) # --- -# name: test_sensors[SDM230-entity_ids4][sensor.device_current:entity-registry] +# name: test_sensors[HWE-SKT-21-entity_ids3][sensor.device_current:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -16571,7 +10289,7 @@ 'unit_of_measurement': , }) # --- -# name: test_sensors[SDM230-entity_ids4][sensor.device_current:state] +# name: test_sensors[HWE-SKT-21-entity_ids3][sensor.device_current:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'current', @@ -16582,11 +10300,12 @@ 'context': , 'entity_id': 'sensor.device_current', 'last_changed': , + 'last_reported': , 'last_updated': , - 'state': '0.273', + 'state': '2.346', }) # --- -# name: test_sensors[SDM230-entity_ids4][sensor.device_energy_export:device-registry] +# name: test_sensors[HWE-SKT-21-entity_ids3][sensor.device_energy_export:device-registry] DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , @@ -16611,16 +10330,16 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'SDM230-wifi', + 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, 'serial_number': None, 'suggested_area': None, - 'sw_version': '3.06', + 'sw_version': '4.07', 'via_device_id': None, }) # --- -# name: test_sensors[SDM230-entity_ids4][sensor.device_energy_export:entity-registry] +# name: test_sensors[HWE-SKT-21-entity_ids3][sensor.device_energy_export:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -16655,7 +10374,7 @@ 'unit_of_measurement': , }) # --- -# name: test_sensors[SDM230-entity_ids4][sensor.device_energy_export:state] +# name: test_sensors[HWE-SKT-21-entity_ids3][sensor.device_energy_export:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'energy', @@ -16666,11 +10385,12 @@ 'context': , 'entity_id': 'sensor.device_energy_export', 'last_changed': , + 'last_reported': , 'last_updated': , - 'state': '255.551', + 'state': '85.951', }) # --- -# name: test_sensors[SDM230-entity_ids4][sensor.device_energy_import:device-registry] +# name: test_sensors[HWE-SKT-21-entity_ids3][sensor.device_energy_import:device-registry] DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , @@ -16695,16 +10415,16 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'SDM230-wifi', + 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, 'serial_number': None, 'suggested_area': None, - 'sw_version': '3.06', + 'sw_version': '4.07', 'via_device_id': None, }) # --- -# name: test_sensors[SDM230-entity_ids4][sensor.device_energy_import:entity-registry] +# name: test_sensors[HWE-SKT-21-entity_ids3][sensor.device_energy_import:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -16739,7 +10459,7 @@ 'unit_of_measurement': , }) # --- -# name: test_sensors[SDM230-entity_ids4][sensor.device_energy_import:state] +# name: test_sensors[HWE-SKT-21-entity_ids3][sensor.device_energy_import:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'energy', @@ -16750,11 +10470,12 @@ 'context': , 'entity_id': 'sensor.device_energy_import', 'last_changed': , + 'last_reported': , 'last_updated': , - 'state': '2.705', + 'state': '30.511', }) # --- -# name: test_sensors[SDM230-entity_ids4][sensor.device_frequency:device-registry] +# name: test_sensors[HWE-SKT-21-entity_ids3][sensor.device_frequency:device-registry] DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , @@ -16779,16 +10500,16 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'SDM230-wifi', + 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, 'serial_number': None, 'suggested_area': None, - 'sw_version': '3.06', + 'sw_version': '4.07', 'via_device_id': None, }) # --- -# name: test_sensors[SDM230-entity_ids4][sensor.device_frequency:entity-registry] +# name: test_sensors[HWE-SKT-21-entity_ids3][sensor.device_frequency:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -16823,7 +10544,7 @@ 'unit_of_measurement': , }) # --- -# name: test_sensors[SDM230-entity_ids4][sensor.device_frequency:state] +# name: test_sensors[HWE-SKT-21-entity_ids3][sensor.device_frequency:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'frequency', @@ -16834,11 +10555,12 @@ 'context': , 'entity_id': 'sensor.device_frequency', 'last_changed': , + 'last_reported': , 'last_updated': , - 'state': '50', + 'state': '50.005', }) # --- -# name: test_sensors[SDM230-entity_ids4][sensor.device_power:device-registry] +# name: test_sensors[HWE-SKT-21-entity_ids3][sensor.device_power:device-registry] DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , @@ -16863,16 +10585,16 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'SDM230-wifi', + 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, 'serial_number': None, 'suggested_area': None, - 'sw_version': '3.06', + 'sw_version': '4.07', 'via_device_id': None, }) # --- -# name: test_sensors[SDM230-entity_ids4][sensor.device_power:entity-registry] +# name: test_sensors[HWE-SKT-21-entity_ids3][sensor.device_power:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -16910,7 +10632,7 @@ 'unit_of_measurement': , }) # --- -# name: test_sensors[SDM230-entity_ids4][sensor.device_power:state] +# name: test_sensors[HWE-SKT-21-entity_ids3][sensor.device_power:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'power', @@ -16921,11 +10643,12 @@ 'context': , 'entity_id': 'sensor.device_power', 'last_changed': , + 'last_reported': , 'last_updated': , - 'state': '-1058.296', + 'state': '543.312', }) # --- -# name: test_sensors[SDM230-entity_ids4][sensor.device_power_factor:device-registry] +# name: test_sensors[HWE-SKT-21-entity_ids3][sensor.device_power_factor:device-registry] DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , @@ -16950,16 +10673,16 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'SDM230-wifi', + 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, 'serial_number': None, 'suggested_area': None, - 'sw_version': '3.06', + 'sw_version': '4.07', 'via_device_id': None, }) # --- -# name: test_sensors[SDM230-entity_ids4][sensor.device_power_factor:entity-registry] +# name: test_sensors[HWE-SKT-21-entity_ids3][sensor.device_power_factor:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -16994,7 +10717,7 @@ 'unit_of_measurement': '%', }) # --- -# name: test_sensors[SDM230-entity_ids4][sensor.device_power_factor:state] +# name: test_sensors[HWE-SKT-21-entity_ids3][sensor.device_power_factor:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'power_factor', @@ -17005,94 +10728,12 @@ 'context': , 'entity_id': 'sensor.device_power_factor', 'last_changed': , + 'last_reported': , 'last_updated': , - 'state': '61.1', + 'state': '81.688', }) # --- -# name: test_sensors[SDM230-entity_ids4][sensor.device_power_phase_1:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'SDM230-wifi', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '3.06', - 'via_device_id': None, - }) -# --- -# name: test_sensors[SDM230-entity_ids4][sensor.device_power_phase_1:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_power_phase_1', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 0, - }), - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Power phase 1', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'active_power_phase_w', - 'unique_id': 'aabbccddeeff_active_power_l1_w', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[SDM230-entity_ids4][sensor.device_power_phase_1:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'power', - 'friendly_name': 'Device Power phase 1', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.device_power_phase_1', - 'last_changed': , - 'last_updated': , - 'state': '-1058.296', - }) -# --- -# name: test_sensors[SDM230-entity_ids4][sensor.device_reactive_power:device-registry] +# name: test_sensors[HWE-SKT-21-entity_ids3][sensor.device_power_phase_1:device-registry] DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , @@ -17117,16 +10758,104 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'SDM230-wifi', + 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, 'serial_number': None, 'suggested_area': None, - 'sw_version': '3.06', + 'sw_version': '4.07', 'via_device_id': None, }) # --- -# name: test_sensors[SDM230-entity_ids4][sensor.device_reactive_power:entity-registry] +# name: test_sensors[HWE-SKT-21-entity_ids3][sensor.device_power_phase_1:entity-registry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.device_power_phase_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power phase 1', + 'platform': 'homewizard', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'active_power_phase_w', + 'unique_id': 'aabbccddeeff_active_power_l1_w', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[HWE-SKT-21-entity_ids3][sensor.device_power_phase_1:state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Device Power phase 1', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.device_power_phase_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '543.312', + }) +# --- +# name: test_sensors[HWE-SKT-21-entity_ids3][sensor.device_reactive_power:device-registry] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'configuration_url': None, + 'connections': set({ + tuple( + 'mac', + '3c:39:e7:aa:bb:cc', + ), + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'homewizard', + '3c39e7aabbcc', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'HomeWizard', + 'model': 'HWE-SKT', + 'name': 'Device', + 'name_by_user': None, + 'serial_number': None, + 'suggested_area': None, + 'sw_version': '4.07', + 'via_device_id': None, + }) +# --- +# name: test_sensors[HWE-SKT-21-entity_ids3][sensor.device_reactive_power:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -17161,7 +10890,7 @@ 'unit_of_measurement': 'var', }) # --- -# name: test_sensors[SDM230-entity_ids4][sensor.device_reactive_power:state] +# name: test_sensors[HWE-SKT-21-entity_ids3][sensor.device_reactive_power:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'reactive_power', @@ -17172,171 +10901,12 @@ 'context': , 'entity_id': 'sensor.device_reactive_power', 'last_changed': , + 'last_reported': , 'last_updated': , - 'state': '-58.612', + 'state': '123.456', }) # --- -# name: test_sensors[SDM230-entity_ids4][sensor.device_total_energy_export:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'SDM230-wifi', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '3.06', - 'via_device_id': None, - }) -# --- -# name: test_sensors[SDM230-entity_ids4][sensor.device_total_energy_export:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_total_energy_export', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Total energy export', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'total_energy_export_kwh', - 'unique_id': 'aabbccddeeff_total_power_export_kwh', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[SDM230-entity_ids4][sensor.device_total_energy_export:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'Device Total energy export', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.device_total_energy_export', - 'last_changed': , - 'last_updated': , - 'state': '255.551', - }) -# --- -# name: test_sensors[SDM230-entity_ids4][sensor.device_total_energy_import:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'SDM230-wifi', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '3.06', - 'via_device_id': None, - }) -# --- -# name: test_sensors[SDM230-entity_ids4][sensor.device_total_energy_import:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_total_energy_import', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Total energy import', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'total_energy_import_kwh', - 'unique_id': 'aabbccddeeff_total_power_import_kwh', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[SDM230-entity_ids4][sensor.device_total_energy_import:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'Device Total energy import', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.device_total_energy_import', - 'last_changed': , - 'last_updated': , - 'state': '2.705', - }) -# --- -# name: test_sensors[SDM230-entity_ids4][sensor.device_voltage:device-registry] +# name: test_sensors[HWE-SKT-21-entity_ids3][sensor.device_voltage:device-registry] DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , @@ -17361,16 +10931,16 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'SDM230-wifi', + 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, 'serial_number': None, 'suggested_area': None, - 'sw_version': '3.06', + 'sw_version': '4.07', 'via_device_id': None, }) # --- -# name: test_sensors[SDM230-entity_ids4][sensor.device_voltage:entity-registry] +# name: test_sensors[HWE-SKT-21-entity_ids3][sensor.device_voltage:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -17405,7 +10975,7 @@ 'unit_of_measurement': , }) # --- -# name: test_sensors[SDM230-entity_ids4][sensor.device_voltage:state] +# name: test_sensors[HWE-SKT-21-entity_ids3][sensor.device_voltage:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'voltage', @@ -17416,11 +10986,12 @@ 'context': , 'entity_id': 'sensor.device_voltage', 'last_changed': , + 'last_reported': , 'last_updated': , - 'state': '228.472', + 'state': '231.539', }) # --- -# name: test_sensors[SDM230-entity_ids4][sensor.device_wi_fi_ssid:device-registry] +# name: test_sensors[HWE-SKT-21-entity_ids3][sensor.device_wi_fi_ssid:device-registry] DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , @@ -17445,16 +11016,16 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'SDM230-wifi', + 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, 'serial_number': None, 'suggested_area': None, - 'sw_version': '3.06', + 'sw_version': '4.07', 'via_device_id': None, }) # --- -# name: test_sensors[SDM230-entity_ids4][sensor.device_wi_fi_ssid:entity-registry] +# name: test_sensors[HWE-SKT-21-entity_ids3][sensor.device_wi_fi_ssid:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -17487,7 +11058,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_sensors[SDM230-entity_ids4][sensor.device_wi_fi_ssid:state] +# name: test_sensors[HWE-SKT-21-entity_ids3][sensor.device_wi_fi_ssid:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Device Wi-Fi SSID', @@ -17495,11 +11066,12 @@ 'context': , 'entity_id': 'sensor.device_wi_fi_ssid', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'My Wi-Fi', }) # --- -# name: test_sensors[SDM230-entity_ids4][sensor.device_wi_fi_strength:device-registry] +# name: test_sensors[HWE-SKT-21-entity_ids3][sensor.device_wi_fi_strength:device-registry] DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , @@ -17524,16 +11096,16 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'SDM230-wifi', + 'model': 'HWE-SKT', 'name': 'Device', 'name_by_user': None, 'serial_number': None, 'suggested_area': None, - 'sw_version': '3.06', + 'sw_version': '4.07', 'via_device_id': None, }) # --- -# name: test_sensors[SDM230-entity_ids4][sensor.device_wi_fi_strength:entity-registry] +# name: test_sensors[HWE-SKT-21-entity_ids3][sensor.device_wi_fi_strength:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -17568,7 +11140,7 @@ 'unit_of_measurement': '%', }) # --- -# name: test_sensors[SDM230-entity_ids4][sensor.device_wi_fi_strength:state] +# name: test_sensors[HWE-SKT-21-entity_ids3][sensor.device_wi_fi_strength:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Device Wi-Fi strength', @@ -17578,1866 +11150,12 @@ 'context': , 'entity_id': 'sensor.device_wi_fi_strength', 'last_changed': , - 'last_updated': , - 'state': '92', - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_active_apparent_power:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'SDM630-wifi', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '3.06', - 'via_device_id': None, - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_active_apparent_power:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_active_apparent_power', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Active apparent power', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'active_apparent_power_va', - 'unique_id': 'aabbccddeeff_active_apparent_power_va', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_active_apparent_power:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'apparent_power', - 'friendly_name': 'Device Active apparent power', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.device_active_apparent_power', - 'last_changed': , - 'last_updated': , - 'state': '7112.293', - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_active_apparent_power_phase_1:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'SDM630-wifi', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '3.06', - 'via_device_id': None, - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_active_apparent_power_phase_1:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_active_apparent_power_phase_1', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Active apparent power phase 1', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'active_apparent_power_phase_va', - 'unique_id': 'aabbccddeeff_active_apparent_power_l1_va', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_active_apparent_power_phase_1:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'apparent_power', - 'friendly_name': 'Device Active apparent power phase 1', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.device_active_apparent_power_phase_1', - 'last_changed': , - 'last_updated': , - 'state': '0', - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_active_apparent_power_phase_2:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'SDM630-wifi', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '3.06', - 'via_device_id': None, - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_active_apparent_power_phase_2:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_active_apparent_power_phase_2', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Active apparent power phase 2', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'active_apparent_power_phase_va', - 'unique_id': 'aabbccddeeff_active_apparent_power_l2_va', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_active_apparent_power_phase_2:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'apparent_power', - 'friendly_name': 'Device Active apparent power phase 2', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.device_active_apparent_power_phase_2', - 'last_changed': , - 'last_updated': , - 'state': '3548.879', - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_active_apparent_power_phase_3:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'SDM630-wifi', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '3.06', - 'via_device_id': None, - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_active_apparent_power_phase_3:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_active_apparent_power_phase_3', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Active apparent power phase 3', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'active_apparent_power_phase_va', - 'unique_id': 'aabbccddeeff_active_apparent_power_l3_va', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_active_apparent_power_phase_3:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'apparent_power', - 'friendly_name': 'Device Active apparent power phase 3', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.device_active_apparent_power_phase_3', - 'last_changed': , - 'last_updated': , - 'state': '3563.414', - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_active_current:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'SDM630-wifi', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '3.06', - 'via_device_id': None, - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_active_current:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_active_current', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Active current', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'active_current_a', - 'unique_id': 'aabbccddeeff_active_current_a', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_active_current:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'current', - 'friendly_name': 'Device Active current', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.device_active_current', - 'last_changed': , - 'last_updated': , - 'state': '30.999', - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_active_current_phase_1:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'SDM630-wifi', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '3.06', - 'via_device_id': None, - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_active_current_phase_1:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_active_current_phase_1', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Active current phase 1', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'active_current_phase_a', - 'unique_id': 'aabbccddeeff_active_current_l1_a', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_active_current_phase_1:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'current', - 'friendly_name': 'Device Active current phase 1', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.device_active_current_phase_1', - 'last_changed': , - 'last_updated': , - 'state': '0', - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_active_current_phase_2:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'SDM630-wifi', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '3.06', - 'via_device_id': None, - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_active_current_phase_2:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_active_current_phase_2', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Active current phase 2', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'active_current_phase_a', - 'unique_id': 'aabbccddeeff_active_current_l2_a', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_active_current_phase_2:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'current', - 'friendly_name': 'Device Active current phase 2', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.device_active_current_phase_2', - 'last_changed': , - 'last_updated': , - 'state': '15.521', - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_active_current_phase_3:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'SDM630-wifi', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '3.06', - 'via_device_id': None, - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_active_current_phase_3:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_active_current_phase_3', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Active current phase 3', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'active_current_phase_a', - 'unique_id': 'aabbccddeeff_active_current_l3_a', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_active_current_phase_3:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'current', - 'friendly_name': 'Device Active current phase 3', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.device_active_current_phase_3', - 'last_changed': , - 'last_updated': , - 'state': '15.477', - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_active_frequency:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'SDM630-wifi', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '3.06', - 'via_device_id': None, - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_active_frequency:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_active_frequency', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Active frequency', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'active_frequency_hz', - 'unique_id': 'aabbccddeeff_active_frequency_hz', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_active_frequency:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'frequency', - 'friendly_name': 'Device Active frequency', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.device_active_frequency', - 'last_changed': , - 'last_updated': , - 'state': '49.926', - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_active_power:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'SDM630-wifi', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '3.06', - 'via_device_id': None, - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_active_power:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_active_power', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 0, - }), - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Active power', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'active_power_w', - 'unique_id': 'aabbccddeeff_active_power_w', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_active_power:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'power', - 'friendly_name': 'Device Active power', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.device_active_power', - 'last_changed': , - 'last_updated': , - 'state': '-900.194', - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_active_power_factor_phase_1:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'SDM630-wifi', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '3.06', - 'via_device_id': None, - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_active_power_factor_phase_1:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_active_power_factor_phase_1', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Active power factor phase 1', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'active_power_factor_phase', - 'unique_id': 'aabbccddeeff_active_power_factor_l1', - 'unit_of_measurement': '%', - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_active_power_factor_phase_1:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'power_factor', - 'friendly_name': 'Device Active power factor phase 1', - 'state_class': , - 'unit_of_measurement': '%', - }), - 'context': , - 'entity_id': 'sensor.device_active_power_factor_phase_1', - 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '100', }) # --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_active_power_factor_phase_2:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'SDM630-wifi', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '3.06', - 'via_device_id': None, - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_active_power_factor_phase_2:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_active_power_factor_phase_2', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Active power factor phase 2', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'active_power_factor_phase', - 'unique_id': 'aabbccddeeff_active_power_factor_l2', - 'unit_of_measurement': '%', - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_active_power_factor_phase_2:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'power_factor', - 'friendly_name': 'Device Active power factor phase 2', - 'state_class': , - 'unit_of_measurement': '%', - }), - 'context': , - 'entity_id': 'sensor.device_active_power_factor_phase_2', - 'last_changed': , - 'last_updated': , - 'state': '99.9', - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_active_power_factor_phase_3:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'SDM630-wifi', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '3.06', - 'via_device_id': None, - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_active_power_factor_phase_3:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_active_power_factor_phase_3', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Active power factor phase 3', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'active_power_factor_phase', - 'unique_id': 'aabbccddeeff_active_power_factor_l3', - 'unit_of_measurement': '%', - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_active_power_factor_phase_3:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'power_factor', - 'friendly_name': 'Device Active power factor phase 3', - 'state_class': , - 'unit_of_measurement': '%', - }), - 'context': , - 'entity_id': 'sensor.device_active_power_factor_phase_3', - 'last_changed': , - 'last_updated': , - 'state': '99.7', - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_active_power_phase_1:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'SDM630-wifi', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '3.06', - 'via_device_id': None, - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_active_power_phase_1:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_active_power_phase_1', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 0, - }), - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Active power phase 1', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'active_power_phase_w', - 'unique_id': 'aabbccddeeff_active_power_l1_w', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_active_power_phase_1:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'power', - 'friendly_name': 'Device Active power phase 1', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.device_active_power_phase_1', - 'last_changed': , - 'last_updated': , - 'state': '-1058.296', - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_active_power_phase_2:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'SDM630-wifi', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '3.06', - 'via_device_id': None, - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_active_power_phase_2:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_active_power_phase_2', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 0, - }), - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Active power phase 2', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'active_power_phase_w', - 'unique_id': 'aabbccddeeff_active_power_l2_w', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_active_power_phase_2:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'power', - 'friendly_name': 'Device Active power phase 2', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.device_active_power_phase_2', - 'last_changed': , - 'last_updated': , - 'state': '158.102', - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_active_power_phase_3:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'SDM630-wifi', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '3.06', - 'via_device_id': None, - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_active_power_phase_3:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_active_power_phase_3', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 0, - }), - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Active power phase 3', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'active_power_phase_w', - 'unique_id': 'aabbccddeeff_active_power_l3_w', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_active_power_phase_3:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'power', - 'friendly_name': 'Device Active power phase 3', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.device_active_power_phase_3', - 'last_changed': , - 'last_updated': , - 'state': '0.0', - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_active_reactive_power:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'SDM630-wifi', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '3.06', - 'via_device_id': None, - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_active_reactive_power:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_power', - 'entity_id': 'sensor.device_active_reactive_power', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Active reactive power', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'active_reactive_power_var', - 'unique_id': 'aabbccddeeff_active_reactive_power_var', - 'unit_of_measurement': 'var', - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_active_reactive_power:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'reactive_power', - 'friendly_name': 'Device Active reactive power', - 'state_class': , - 'unit_of_measurement': 'var', - }), - 'context': , - 'entity_id': 'sensor.device_active_reactive_power', - 'last_changed': , - 'last_updated': , - 'state': '-429.025', - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_active_reactive_power_phase_1:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'SDM630-wifi', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '3.06', - 'via_device_id': None, - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_active_reactive_power_phase_1:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_active_reactive_power_phase_1', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Active reactive power phase 1', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'active_reactive_power_phase_var', - 'unique_id': 'aabbccddeeff_active_reactive_power_l1_var', - 'unit_of_measurement': 'var', - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_active_reactive_power_phase_1:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'reactive_power', - 'friendly_name': 'Device Active reactive power phase 1', - 'state_class': , - 'unit_of_measurement': 'var', - }), - 'context': , - 'entity_id': 'sensor.device_active_reactive_power_phase_1', - 'last_changed': , - 'last_updated': , - 'state': '0', - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_active_reactive_power_phase_2:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'SDM630-wifi', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '3.06', - 'via_device_id': None, - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_active_reactive_power_phase_2:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_active_reactive_power_phase_2', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Active reactive power phase 2', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'active_reactive_power_phase_var', - 'unique_id': 'aabbccddeeff_active_reactive_power_l2_var', - 'unit_of_measurement': 'var', - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_active_reactive_power_phase_2:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'reactive_power', - 'friendly_name': 'Device Active reactive power phase 2', - 'state_class': , - 'unit_of_measurement': 'var', - }), - 'context': , - 'entity_id': 'sensor.device_active_reactive_power_phase_2', - 'last_changed': , - 'last_updated': , - 'state': '-166.675', - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_active_reactive_power_phase_3:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'SDM630-wifi', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '3.06', - 'via_device_id': None, - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_active_reactive_power_phase_3:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_active_reactive_power_phase_3', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Active reactive power phase 3', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'active_reactive_power_phase_var', - 'unique_id': 'aabbccddeeff_active_reactive_power_l3_var', - 'unit_of_measurement': 'var', - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_active_reactive_power_phase_3:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'reactive_power', - 'friendly_name': 'Device Active reactive power phase 3', - 'state_class': , - 'unit_of_measurement': 'var', - }), - 'context': , - 'entity_id': 'sensor.device_active_reactive_power_phase_3', - 'last_changed': , - 'last_updated': , - 'state': '-262.35', - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_active_voltage_phase_1:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'SDM630-wifi', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '3.06', - 'via_device_id': None, - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_active_voltage_phase_1:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_active_voltage_phase_1', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Power', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': 'aabbccddeeff_active_power_w', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_active_voltage_phase_1:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'voltage', - 'friendly_name': 'Device Active voltage phase 1', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.device_active_voltage_phase_1', - 'last_changed': , - 'last_updated': , - 'state': '230.751', - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_active_voltage_phase_2:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'SDM630-wifi', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '3.06', - 'via_device_id': None, - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_active_voltage_phase_2:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_power_phase_1', - 'entity_id': 'sensor.device_active_voltage_phase_2', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Power phase 1', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'active_power_phase_w', - 'unique_id': 'aabbccddeeff_active_power_l1_w', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_active_voltage_phase_2:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'voltage', - 'friendly_name': 'Device Active voltage phase 2', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.device_active_voltage_phase_2', - 'last_changed': , - 'last_updated': , - 'state': '228.391', - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_active_voltage_phase_3:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'SDM630-wifi', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '3.06', - 'via_device_id': None, - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_active_voltage_phase_3:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_power_phase_2', - 'entity_id': 'sensor.device_active_voltage_phase_3', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Power phase 2', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'active_power_phase_w', - 'unique_id': 'aabbccddeeff_active_power_l2_w', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_active_voltage_phase_3:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'voltage', - 'friendly_name': 'Device Active voltage phase 3', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.device_active_voltage_phase_3', - 'last_changed': , - 'last_updated': , - 'state': '229.612', - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_apparent_power:device-registry] +# name: test_sensors[HWE-WTR-entity_ids4][sensor.device_total_water_usage:device-registry] DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , @@ -19462,7 +11180,340 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'SDM630-wifi', + 'model': 'HWE-WTR', + 'name': 'Device', + 'name_by_user': None, + 'serial_number': None, + 'suggested_area': None, + 'sw_version': '2.03', + 'via_device_id': None, + }) +# --- +# name: test_sensors[HWE-WTR-entity_ids4][sensor.device_total_water_usage:entity-registry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.device_total_water_usage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Total water usage', + 'platform': 'homewizard', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'total_liter_m3', + 'unique_id': 'aabbccddeeff_total_liter_m3', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[HWE-WTR-entity_ids4][sensor.device_total_water_usage:state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'water', + 'friendly_name': 'Device Total water usage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.device_total_water_usage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '17.014', + }) +# --- +# name: test_sensors[HWE-WTR-entity_ids4][sensor.device_water_usage:device-registry] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'configuration_url': None, + 'connections': set({ + tuple( + 'mac', + '3c:39:e7:aa:bb:cc', + ), + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'homewizard', + '3c39e7aabbcc', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'HomeWizard', + 'model': 'HWE-WTR', + 'name': 'Device', + 'name_by_user': None, + 'serial_number': None, + 'suggested_area': None, + 'sw_version': '2.03', + 'via_device_id': None, + }) +# --- +# name: test_sensors[HWE-WTR-entity_ids4][sensor.device_water_usage:entity-registry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.device_water_usage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Water usage', + 'platform': 'homewizard', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'active_liter_lpm', + 'unique_id': 'aabbccddeeff_active_liter_lpm', + 'unit_of_measurement': 'l/min', + }) +# --- +# name: test_sensors[HWE-WTR-entity_ids4][sensor.device_water_usage:state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Device Water usage', + 'state_class': , + 'unit_of_measurement': 'l/min', + }), + 'context': , + 'entity_id': 'sensor.device_water_usage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensors[HWE-WTR-entity_ids4][sensor.device_wi_fi_ssid:device-registry] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'configuration_url': None, + 'connections': set({ + tuple( + 'mac', + '3c:39:e7:aa:bb:cc', + ), + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'homewizard', + '3c39e7aabbcc', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'HomeWizard', + 'model': 'HWE-WTR', + 'name': 'Device', + 'name_by_user': None, + 'serial_number': None, + 'suggested_area': None, + 'sw_version': '2.03', + 'via_device_id': None, + }) +# --- +# name: test_sensors[HWE-WTR-entity_ids4][sensor.device_wi_fi_ssid:entity-registry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.device_wi_fi_ssid', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Wi-Fi SSID', + 'platform': 'homewizard', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wifi_ssid', + 'unique_id': 'aabbccddeeff_wifi_ssid', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[HWE-WTR-entity_ids4][sensor.device_wi_fi_ssid:state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Device Wi-Fi SSID', + }), + 'context': , + 'entity_id': 'sensor.device_wi_fi_ssid', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'My Wi-Fi', + }) +# --- +# name: test_sensors[HWE-WTR-entity_ids4][sensor.device_wi_fi_strength:device-registry] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'configuration_url': None, + 'connections': set({ + tuple( + 'mac', + '3c:39:e7:aa:bb:cc', + ), + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'homewizard', + '3c39e7aabbcc', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'HomeWizard', + 'model': 'HWE-WTR', + 'name': 'Device', + 'name_by_user': None, + 'serial_number': None, + 'suggested_area': None, + 'sw_version': '2.03', + 'via_device_id': None, + }) +# --- +# name: test_sensors[HWE-WTR-entity_ids4][sensor.device_wi_fi_strength:entity-registry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.device_wi_fi_strength', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Wi-Fi strength', + 'platform': 'homewizard', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wifi_strength', + 'unique_id': 'aabbccddeeff_wifi_strength', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[HWE-WTR-entity_ids4][sensor.device_wi_fi_strength:state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Device Wi-Fi strength', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.device_wi_fi_strength', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '84', + }) +# --- +# name: test_sensors[SDM230-entity_ids5][sensor.device_apparent_power:device-registry] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'configuration_url': None, + 'connections': set({ + tuple( + 'mac', + '3c:39:e7:aa:bb:cc', + ), + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'homewizard', + '3c39e7aabbcc', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'HomeWizard', + 'model': 'SDM230-wifi', 'name': 'Device', 'name_by_user': None, 'serial_number': None, @@ -19471,7 +11522,7 @@ 'via_device_id': None, }) # --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_apparent_power:entity-registry] +# name: test_sensors[SDM230-entity_ids5][sensor.device_apparent_power:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -19506,7 +11557,7 @@ 'unit_of_measurement': , }) # --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_apparent_power:state] +# name: test_sensors[SDM230-entity_ids5][sensor.device_apparent_power:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'apparent_power', @@ -19517,11 +11568,12 @@ 'context': , 'entity_id': 'sensor.device_apparent_power', 'last_changed': , + 'last_reported': , 'last_updated': , - 'state': '7112.293', + 'state': '74.052', }) # --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_apparent_power_phase_1:device-registry] +# name: test_sensors[SDM230-entity_ids5][sensor.device_current:device-registry] DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , @@ -19546,7 +11598,7 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'SDM630-wifi', + 'model': 'SDM230-wifi', 'name': 'Device', 'name_by_user': None, 'serial_number': None, @@ -19555,259 +11607,7 @@ 'via_device_id': None, }) # --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_apparent_power_phase_1:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_apparent_power_phase_1', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Apparent power phase 1', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'active_apparent_power_phase_va', - 'unique_id': 'aabbccddeeff_active_apparent_power_l1_va', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_apparent_power_phase_1:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'apparent_power', - 'friendly_name': 'Device Apparent power phase 1', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.device_apparent_power_phase_1', - 'last_changed': , - 'last_updated': , - 'state': '0', - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_apparent_power_phase_2:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': 'HomeWizard', - 'model': 'SDM630-wifi', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '3.06', - 'via_device_id': None, - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_apparent_power_phase_2:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_apparent_power_phase_2', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Apparent power phase 2', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'active_apparent_power_phase_va', - 'unique_id': 'aabbccddeeff_active_apparent_power_l2_va', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_apparent_power_phase_2:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'apparent_power', - 'friendly_name': 'Device Apparent power phase 2', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.device_apparent_power_phase_2', - 'last_changed': , - 'last_updated': , - 'state': '3548.879', - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_apparent_power_phase_3:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': 'HomeWizard', - 'model': 'SDM630-wifi', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '3.06', - 'via_device_id': None, - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_apparent_power_phase_3:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_apparent_power_phase_3', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Apparent power phase 3', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'active_apparent_power_phase_va', - 'unique_id': 'aabbccddeeff_active_apparent_power_l3_va', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_apparent_power_phase_3:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'apparent_power', - 'friendly_name': 'Device Apparent power phase 3', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.device_apparent_power_phase_3', - 'last_changed': , - 'last_updated': , - 'state': '3563.414', - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_current:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': 'HomeWizard', - 'model': 'SDM630-wifi', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '3.06', - 'via_device_id': None, - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_current:entity-registry] +# name: test_sensors[SDM230-entity_ids5][sensor.device_current:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -19842,7 +11642,7 @@ 'unit_of_measurement': , }) # --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_current:state] +# name: test_sensors[SDM230-entity_ids5][sensor.device_current:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'current', @@ -19853,11 +11653,12 @@ 'context': , 'entity_id': 'sensor.device_current', 'last_changed': , + 'last_reported': , 'last_updated': , - 'state': '30.999', + 'state': '0.273', }) # --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_current_phase_1:device-registry] +# name: test_sensors[SDM230-entity_ids5][sensor.device_energy_export:device-registry] DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , @@ -19882,7 +11683,7 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'SDM630-wifi', + 'model': 'SDM230-wifi', 'name': 'Device', 'name_by_user': None, 'serial_number': None, @@ -19891,259 +11692,7 @@ 'via_device_id': None, }) # --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_current_phase_1:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_current_phase_1', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Current phase 1', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'active_current_phase_a', - 'unique_id': 'aabbccddeeff_active_current_l1_a', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_current_phase_1:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'current', - 'friendly_name': 'Device Current phase 1', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.device_current_phase_1', - 'last_changed': , - 'last_updated': , - 'state': '0', - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_current_phase_2:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': 'HomeWizard', - 'model': 'SDM630-wifi', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '3.06', - 'via_device_id': None, - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_current_phase_2:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_current_phase_2', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Current phase 2', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'active_current_phase_a', - 'unique_id': 'aabbccddeeff_active_current_l2_a', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_current_phase_2:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'current', - 'friendly_name': 'Device Current phase 2', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.device_current_phase_2', - 'last_changed': , - 'last_updated': , - 'state': '15.521', - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_current_phase_3:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': 'HomeWizard', - 'model': 'SDM630-wifi', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '3.06', - 'via_device_id': None, - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_current_phase_3:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_current_phase_3', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Current phase 3', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'active_current_phase_a', - 'unique_id': 'aabbccddeeff_active_current_l3_a', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_current_phase_3:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'current', - 'friendly_name': 'Device Current phase 3', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.device_current_phase_3', - 'last_changed': , - 'last_updated': , - 'state': '15.477', - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_energy_export:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': 'HomeWizard', - 'model': 'SDM630-wifi', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '3.06', - 'via_device_id': None, - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_energy_export:entity-registry] +# name: test_sensors[SDM230-entity_ids5][sensor.device_energy_export:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -20178,7 +11727,7 @@ 'unit_of_measurement': , }) # --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_energy_export:state] +# name: test_sensors[SDM230-entity_ids5][sensor.device_energy_export:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'energy', @@ -20189,11 +11738,12 @@ 'context': , 'entity_id': 'sensor.device_energy_export', 'last_changed': , + 'last_reported': , 'last_updated': , - 'state': '0.523', + 'state': '255.551', }) # --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_energy_import:device-registry] +# name: test_sensors[SDM230-entity_ids5][sensor.device_energy_import:device-registry] DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , @@ -20218,7 +11768,7 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'SDM630-wifi', + 'model': 'SDM230-wifi', 'name': 'Device', 'name_by_user': None, 'serial_number': None, @@ -20227,7 +11777,7 @@ 'via_device_id': None, }) # --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_energy_import:entity-registry] +# name: test_sensors[SDM230-entity_ids5][sensor.device_energy_import:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -20262,7 +11812,7 @@ 'unit_of_measurement': , }) # --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_energy_import:state] +# name: test_sensors[SDM230-entity_ids5][sensor.device_energy_import:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'energy', @@ -20273,11 +11823,12 @@ 'context': , 'entity_id': 'sensor.device_energy_import', 'last_changed': , + 'last_reported': , 'last_updated': , - 'state': '0.101', + 'state': '2.705', }) # --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_frequency:device-registry] +# name: test_sensors[SDM230-entity_ids5][sensor.device_frequency:device-registry] DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , @@ -20302,7 +11853,7 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'SDM630-wifi', + 'model': 'SDM230-wifi', 'name': 'Device', 'name_by_user': None, 'serial_number': None, @@ -20311,7 +11862,7 @@ 'via_device_id': None, }) # --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_frequency:entity-registry] +# name: test_sensors[SDM230-entity_ids5][sensor.device_frequency:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -20346,7 +11897,7 @@ 'unit_of_measurement': , }) # --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_frequency:state] +# name: test_sensors[SDM230-entity_ids5][sensor.device_frequency:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'frequency', @@ -20357,11 +11908,12 @@ 'context': , 'entity_id': 'sensor.device_frequency', 'last_changed': , + 'last_reported': , 'last_updated': , - 'state': '49.926', + 'state': '50', }) # --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_power:device-registry] +# name: test_sensors[SDM230-entity_ids5][sensor.device_power:device-registry] DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , @@ -20386,7 +11938,7 @@ 'labels': set({ }), 'manufacturer': 'HomeWizard', - 'model': 'SDM630-wifi', + 'model': 'SDM230-wifi', 'name': 'Device', 'name_by_user': None, 'serial_number': None, @@ -20395,7 +11947,7 @@ 'via_device_id': None, }) # --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_power:entity-registry] +# name: test_sensors[SDM230-entity_ids5][sensor.device_power:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -20433,7 +11985,7 @@ 'unit_of_measurement': , }) # --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_power:state] +# name: test_sensors[SDM230-entity_ids5][sensor.device_power:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'power', @@ -20444,11 +11996,431 @@ 'context': , 'entity_id': 'sensor.device_power', 'last_changed': , + 'last_reported': , 'last_updated': , - 'state': '-900.194', + 'state': '-1058.296', }) # --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_power_factor_phase_1:device-registry] +# name: test_sensors[SDM230-entity_ids5][sensor.device_power_factor:device-registry] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'configuration_url': None, + 'connections': set({ + tuple( + 'mac', + '3c:39:e7:aa:bb:cc', + ), + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'homewizard', + '3c39e7aabbcc', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'HomeWizard', + 'model': 'SDM230-wifi', + 'name': 'Device', + 'name_by_user': None, + 'serial_number': None, + 'suggested_area': None, + 'sw_version': '3.06', + 'via_device_id': None, + }) +# --- +# name: test_sensors[SDM230-entity_ids5][sensor.device_power_factor:entity-registry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.device_power_factor', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power factor', + 'platform': 'homewizard', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'aabbccddeeff_active_power_factor', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[SDM230-entity_ids5][sensor.device_power_factor:state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power_factor', + 'friendly_name': 'Device Power factor', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.device_power_factor', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '61.1', + }) +# --- +# name: test_sensors[SDM230-entity_ids5][sensor.device_reactive_power:device-registry] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'configuration_url': None, + 'connections': set({ + tuple( + 'mac', + '3c:39:e7:aa:bb:cc', + ), + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'homewizard', + '3c39e7aabbcc', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'HomeWizard', + 'model': 'SDM230-wifi', + 'name': 'Device', + 'name_by_user': None, + 'serial_number': None, + 'suggested_area': None, + 'sw_version': '3.06', + 'via_device_id': None, + }) +# --- +# name: test_sensors[SDM230-entity_ids5][sensor.device_reactive_power:entity-registry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.device_reactive_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Reactive power', + 'platform': 'homewizard', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'aabbccddeeff_active_reactive_power_var', + 'unit_of_measurement': 'var', + }) +# --- +# name: test_sensors[SDM230-entity_ids5][sensor.device_reactive_power:state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'reactive_power', + 'friendly_name': 'Device Reactive power', + 'state_class': , + 'unit_of_measurement': 'var', + }), + 'context': , + 'entity_id': 'sensor.device_reactive_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '-58.612', + }) +# --- +# name: test_sensors[SDM230-entity_ids5][sensor.device_voltage:device-registry] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'configuration_url': None, + 'connections': set({ + tuple( + 'mac', + '3c:39:e7:aa:bb:cc', + ), + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'homewizard', + '3c39e7aabbcc', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'HomeWizard', + 'model': 'SDM230-wifi', + 'name': 'Device', + 'name_by_user': None, + 'serial_number': None, + 'suggested_area': None, + 'sw_version': '3.06', + 'via_device_id': None, + }) +# --- +# name: test_sensors[SDM230-entity_ids5][sensor.device_voltage:entity-registry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.device_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Voltage', + 'platform': 'homewizard', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'aabbccddeeff_active_voltage_v', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[SDM230-entity_ids5][sensor.device_voltage:state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'Device Voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.device_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '228.472', + }) +# --- +# name: test_sensors[SDM230-entity_ids5][sensor.device_wi_fi_ssid:device-registry] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'configuration_url': None, + 'connections': set({ + tuple( + 'mac', + '3c:39:e7:aa:bb:cc', + ), + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'homewizard', + '3c39e7aabbcc', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'HomeWizard', + 'model': 'SDM230-wifi', + 'name': 'Device', + 'name_by_user': None, + 'serial_number': None, + 'suggested_area': None, + 'sw_version': '3.06', + 'via_device_id': None, + }) +# --- +# name: test_sensors[SDM230-entity_ids5][sensor.device_wi_fi_ssid:entity-registry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.device_wi_fi_ssid', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Wi-Fi SSID', + 'platform': 'homewizard', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wifi_ssid', + 'unique_id': 'aabbccddeeff_wifi_ssid', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[SDM230-entity_ids5][sensor.device_wi_fi_ssid:state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Device Wi-Fi SSID', + }), + 'context': , + 'entity_id': 'sensor.device_wi_fi_ssid', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'My Wi-Fi', + }) +# --- +# name: test_sensors[SDM230-entity_ids5][sensor.device_wi_fi_strength:device-registry] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'configuration_url': None, + 'connections': set({ + tuple( + 'mac', + '3c:39:e7:aa:bb:cc', + ), + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'homewizard', + '3c39e7aabbcc', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'HomeWizard', + 'model': 'SDM230-wifi', + 'name': 'Device', + 'name_by_user': None, + 'serial_number': None, + 'suggested_area': None, + 'sw_version': '3.06', + 'via_device_id': None, + }) +# --- +# name: test_sensors[SDM230-entity_ids5][sensor.device_wi_fi_strength:entity-registry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.device_wi_fi_strength', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Wi-Fi strength', + 'platform': 'homewizard', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wifi_strength', + 'unique_id': 'aabbccddeeff_wifi_strength', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[SDM230-entity_ids5][sensor.device_wi_fi_strength:state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Device Wi-Fi strength', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.device_wi_fi_strength', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '92', + }) +# --- +# name: test_sensors[SDM630-entity_ids6][sensor.device_apparent_power:device-registry] DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , @@ -20482,7 +12454,1030 @@ 'via_device_id': None, }) # --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_power_factor_phase_1:entity-registry] +# name: test_sensors[SDM630-entity_ids6][sensor.device_apparent_power:entity-registry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.device_apparent_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Apparent power', + 'platform': 'homewizard', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'aabbccddeeff_active_apparent_power_va', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[SDM630-entity_ids6][sensor.device_apparent_power:state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'apparent_power', + 'friendly_name': 'Device Apparent power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.device_apparent_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '7112.293', + }) +# --- +# name: test_sensors[SDM630-entity_ids6][sensor.device_apparent_power_phase_1:device-registry] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'configuration_url': None, + 'connections': set({ + tuple( + 'mac', + '3c:39:e7:aa:bb:cc', + ), + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'homewizard', + '3c39e7aabbcc', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'HomeWizard', + 'model': 'SDM630-wifi', + 'name': 'Device', + 'name_by_user': None, + 'serial_number': None, + 'suggested_area': None, + 'sw_version': '3.06', + 'via_device_id': None, + }) +# --- +# name: test_sensors[SDM630-entity_ids6][sensor.device_apparent_power_phase_1:entity-registry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.device_apparent_power_phase_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Apparent power phase 1', + 'platform': 'homewizard', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'active_apparent_power_phase_va', + 'unique_id': 'aabbccddeeff_active_apparent_power_l1_va', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[SDM630-entity_ids6][sensor.device_apparent_power_phase_1:state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'apparent_power', + 'friendly_name': 'Device Apparent power phase 1', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.device_apparent_power_phase_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensors[SDM630-entity_ids6][sensor.device_apparent_power_phase_2:device-registry] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'configuration_url': None, + 'connections': set({ + tuple( + 'mac', + '3c:39:e7:aa:bb:cc', + ), + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'homewizard', + '3c39e7aabbcc', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'HomeWizard', + 'model': 'SDM630-wifi', + 'name': 'Device', + 'name_by_user': None, + 'serial_number': None, + 'suggested_area': None, + 'sw_version': '3.06', + 'via_device_id': None, + }) +# --- +# name: test_sensors[SDM630-entity_ids6][sensor.device_apparent_power_phase_2:entity-registry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.device_apparent_power_phase_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Apparent power phase 2', + 'platform': 'homewizard', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'active_apparent_power_phase_va', + 'unique_id': 'aabbccddeeff_active_apparent_power_l2_va', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[SDM630-entity_ids6][sensor.device_apparent_power_phase_2:state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'apparent_power', + 'friendly_name': 'Device Apparent power phase 2', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.device_apparent_power_phase_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '3548.879', + }) +# --- +# name: test_sensors[SDM630-entity_ids6][sensor.device_apparent_power_phase_3:device-registry] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'configuration_url': None, + 'connections': set({ + tuple( + 'mac', + '3c:39:e7:aa:bb:cc', + ), + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'homewizard', + '3c39e7aabbcc', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'HomeWizard', + 'model': 'SDM630-wifi', + 'name': 'Device', + 'name_by_user': None, + 'serial_number': None, + 'suggested_area': None, + 'sw_version': '3.06', + 'via_device_id': None, + }) +# --- +# name: test_sensors[SDM630-entity_ids6][sensor.device_apparent_power_phase_3:entity-registry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.device_apparent_power_phase_3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Apparent power phase 3', + 'platform': 'homewizard', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'active_apparent_power_phase_va', + 'unique_id': 'aabbccddeeff_active_apparent_power_l3_va', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[SDM630-entity_ids6][sensor.device_apparent_power_phase_3:state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'apparent_power', + 'friendly_name': 'Device Apparent power phase 3', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.device_apparent_power_phase_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '3563.414', + }) +# --- +# name: test_sensors[SDM630-entity_ids6][sensor.device_current:device-registry] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'configuration_url': None, + 'connections': set({ + tuple( + 'mac', + '3c:39:e7:aa:bb:cc', + ), + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'homewizard', + '3c39e7aabbcc', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'HomeWizard', + 'model': 'SDM630-wifi', + 'name': 'Device', + 'name_by_user': None, + 'serial_number': None, + 'suggested_area': None, + 'sw_version': '3.06', + 'via_device_id': None, + }) +# --- +# name: test_sensors[SDM630-entity_ids6][sensor.device_current:entity-registry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.device_current', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Current', + 'platform': 'homewizard', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'aabbccddeeff_active_current_a', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[SDM630-entity_ids6][sensor.device_current:state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'Device Current', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.device_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '30.999', + }) +# --- +# name: test_sensors[SDM630-entity_ids6][sensor.device_current_phase_1:device-registry] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'configuration_url': None, + 'connections': set({ + tuple( + 'mac', + '3c:39:e7:aa:bb:cc', + ), + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'homewizard', + '3c39e7aabbcc', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'HomeWizard', + 'model': 'SDM630-wifi', + 'name': 'Device', + 'name_by_user': None, + 'serial_number': None, + 'suggested_area': None, + 'sw_version': '3.06', + 'via_device_id': None, + }) +# --- +# name: test_sensors[SDM630-entity_ids6][sensor.device_current_phase_1:entity-registry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.device_current_phase_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Current phase 1', + 'platform': 'homewizard', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'active_current_phase_a', + 'unique_id': 'aabbccddeeff_active_current_l1_a', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[SDM630-entity_ids6][sensor.device_current_phase_1:state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'Device Current phase 1', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.device_current_phase_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensors[SDM630-entity_ids6][sensor.device_current_phase_2:device-registry] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'configuration_url': None, + 'connections': set({ + tuple( + 'mac', + '3c:39:e7:aa:bb:cc', + ), + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'homewizard', + '3c39e7aabbcc', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'HomeWizard', + 'model': 'SDM630-wifi', + 'name': 'Device', + 'name_by_user': None, + 'serial_number': None, + 'suggested_area': None, + 'sw_version': '3.06', + 'via_device_id': None, + }) +# --- +# name: test_sensors[SDM630-entity_ids6][sensor.device_current_phase_2:entity-registry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.device_current_phase_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Current phase 2', + 'platform': 'homewizard', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'active_current_phase_a', + 'unique_id': 'aabbccddeeff_active_current_l2_a', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[SDM630-entity_ids6][sensor.device_current_phase_2:state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'Device Current phase 2', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.device_current_phase_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '15.521', + }) +# --- +# name: test_sensors[SDM630-entity_ids6][sensor.device_current_phase_3:device-registry] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'configuration_url': None, + 'connections': set({ + tuple( + 'mac', + '3c:39:e7:aa:bb:cc', + ), + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'homewizard', + '3c39e7aabbcc', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'HomeWizard', + 'model': 'SDM630-wifi', + 'name': 'Device', + 'name_by_user': None, + 'serial_number': None, + 'suggested_area': None, + 'sw_version': '3.06', + 'via_device_id': None, + }) +# --- +# name: test_sensors[SDM630-entity_ids6][sensor.device_current_phase_3:entity-registry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.device_current_phase_3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Current phase 3', + 'platform': 'homewizard', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'active_current_phase_a', + 'unique_id': 'aabbccddeeff_active_current_l3_a', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[SDM630-entity_ids6][sensor.device_current_phase_3:state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'Device Current phase 3', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.device_current_phase_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '15.477', + }) +# --- +# name: test_sensors[SDM630-entity_ids6][sensor.device_energy_export:device-registry] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'configuration_url': None, + 'connections': set({ + tuple( + 'mac', + '3c:39:e7:aa:bb:cc', + ), + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'homewizard', + '3c39e7aabbcc', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'HomeWizard', + 'model': 'SDM630-wifi', + 'name': 'Device', + 'name_by_user': None, + 'serial_number': None, + 'suggested_area': None, + 'sw_version': '3.06', + 'via_device_id': None, + }) +# --- +# name: test_sensors[SDM630-entity_ids6][sensor.device_energy_export:entity-registry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.device_energy_export', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Energy export', + 'platform': 'homewizard', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'total_energy_export_kwh', + 'unique_id': 'aabbccddeeff_total_power_export_kwh', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[SDM630-entity_ids6][sensor.device_energy_export:state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Device Energy export', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.device_energy_export', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.523', + }) +# --- +# name: test_sensors[SDM630-entity_ids6][sensor.device_energy_import:device-registry] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'configuration_url': None, + 'connections': set({ + tuple( + 'mac', + '3c:39:e7:aa:bb:cc', + ), + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'homewizard', + '3c39e7aabbcc', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'HomeWizard', + 'model': 'SDM630-wifi', + 'name': 'Device', + 'name_by_user': None, + 'serial_number': None, + 'suggested_area': None, + 'sw_version': '3.06', + 'via_device_id': None, + }) +# --- +# name: test_sensors[SDM630-entity_ids6][sensor.device_energy_import:entity-registry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.device_energy_import', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Energy import', + 'platform': 'homewizard', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'total_energy_import_kwh', + 'unique_id': 'aabbccddeeff_total_power_import_kwh', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[SDM630-entity_ids6][sensor.device_energy_import:state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Device Energy import', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.device_energy_import', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.101', + }) +# --- +# name: test_sensors[SDM630-entity_ids6][sensor.device_frequency:device-registry] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'configuration_url': None, + 'connections': set({ + tuple( + 'mac', + '3c:39:e7:aa:bb:cc', + ), + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'homewizard', + '3c39e7aabbcc', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'HomeWizard', + 'model': 'SDM630-wifi', + 'name': 'Device', + 'name_by_user': None, + 'serial_number': None, + 'suggested_area': None, + 'sw_version': '3.06', + 'via_device_id': None, + }) +# --- +# name: test_sensors[SDM630-entity_ids6][sensor.device_frequency:entity-registry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.device_frequency', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Frequency', + 'platform': 'homewizard', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'aabbccddeeff_active_frequency_hz', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[SDM630-entity_ids6][sensor.device_frequency:state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'frequency', + 'friendly_name': 'Device Frequency', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.device_frequency', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '49.926', + }) +# --- +# name: test_sensors[SDM630-entity_ids6][sensor.device_power:device-registry] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'configuration_url': None, + 'connections': set({ + tuple( + 'mac', + '3c:39:e7:aa:bb:cc', + ), + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'homewizard', + '3c39e7aabbcc', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'HomeWizard', + 'model': 'SDM630-wifi', + 'name': 'Device', + 'name_by_user': None, + 'serial_number': None, + 'suggested_area': None, + 'sw_version': '3.06', + 'via_device_id': None, + }) +# --- +# name: test_sensors[SDM630-entity_ids6][sensor.device_power:entity-registry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.device_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power', + 'platform': 'homewizard', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'aabbccddeeff_active_power_w', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[SDM630-entity_ids6][sensor.device_power:state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Device Power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.device_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '-900.194', + }) +# --- +# name: test_sensors[SDM630-entity_ids6][sensor.device_power_factor_phase_1:device-registry] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'configuration_url': None, + 'connections': set({ + tuple( + 'mac', + '3c:39:e7:aa:bb:cc', + ), + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'homewizard', + '3c39e7aabbcc', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'HomeWizard', + 'model': 'SDM630-wifi', + 'name': 'Device', + 'name_by_user': None, + 'serial_number': None, + 'suggested_area': None, + 'sw_version': '3.06', + 'via_device_id': None, + }) +# --- +# name: test_sensors[SDM630-entity_ids6][sensor.device_power_factor_phase_1:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -20517,7 +13512,7 @@ 'unit_of_measurement': '%', }) # --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_power_factor_phase_1:state] +# name: test_sensors[SDM630-entity_ids6][sensor.device_power_factor_phase_1:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'power_factor', @@ -20528,11 +13523,12 @@ 'context': , 'entity_id': 'sensor.device_power_factor_phase_1', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '100', }) # --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_power_factor_phase_2:device-registry] +# name: test_sensors[SDM630-entity_ids6][sensor.device_power_factor_phase_2:device-registry] DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , @@ -20566,7 +13562,7 @@ 'via_device_id': None, }) # --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_power_factor_phase_2:entity-registry] +# name: test_sensors[SDM630-entity_ids6][sensor.device_power_factor_phase_2:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -20601,7 +13597,7 @@ 'unit_of_measurement': '%', }) # --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_power_factor_phase_2:state] +# name: test_sensors[SDM630-entity_ids6][sensor.device_power_factor_phase_2:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'power_factor', @@ -20612,11 +13608,12 @@ 'context': , 'entity_id': 'sensor.device_power_factor_phase_2', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '99.9', }) # --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_power_factor_phase_3:device-registry] +# name: test_sensors[SDM630-entity_ids6][sensor.device_power_factor_phase_3:device-registry] DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , @@ -20650,7 +13647,7 @@ 'via_device_id': None, }) # --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_power_factor_phase_3:entity-registry] +# name: test_sensors[SDM630-entity_ids6][sensor.device_power_factor_phase_3:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -20685,7 +13682,7 @@ 'unit_of_measurement': '%', }) # --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_power_factor_phase_3:state] +# name: test_sensors[SDM630-entity_ids6][sensor.device_power_factor_phase_3:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'power_factor', @@ -20696,11 +13693,12 @@ 'context': , 'entity_id': 'sensor.device_power_factor_phase_3', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '99.7', }) # --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_power_phase_1:device-registry] +# name: test_sensors[SDM630-entity_ids6][sensor.device_power_phase_1:device-registry] DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , @@ -20734,7 +13732,7 @@ 'via_device_id': None, }) # --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_power_phase_1:entity-registry] +# name: test_sensors[SDM630-entity_ids6][sensor.device_power_phase_1:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -20772,7 +13770,7 @@ 'unit_of_measurement': , }) # --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_power_phase_1:state] +# name: test_sensors[SDM630-entity_ids6][sensor.device_power_phase_1:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'power', @@ -20783,11 +13781,12 @@ 'context': , 'entity_id': 'sensor.device_power_phase_1', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '-1058.296', }) # --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_power_phase_2:device-registry] +# name: test_sensors[SDM630-entity_ids6][sensor.device_power_phase_2:device-registry] DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , @@ -20821,7 +13820,7 @@ 'via_device_id': None, }) # --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_power_phase_2:entity-registry] +# name: test_sensors[SDM630-entity_ids6][sensor.device_power_phase_2:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -20859,7 +13858,7 @@ 'unit_of_measurement': , }) # --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_power_phase_2:state] +# name: test_sensors[SDM630-entity_ids6][sensor.device_power_phase_2:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'power', @@ -20870,11 +13869,12 @@ 'context': , 'entity_id': 'sensor.device_power_phase_2', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '158.102', }) # --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_power_phase_3:device-registry] +# name: test_sensors[SDM630-entity_ids6][sensor.device_power_phase_3:device-registry] DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , @@ -20908,7 +13908,7 @@ 'via_device_id': None, }) # --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_power_phase_3:entity-registry] +# name: test_sensors[SDM630-entity_ids6][sensor.device_power_phase_3:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -20946,7 +13946,7 @@ 'unit_of_measurement': , }) # --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_power_phase_3:state] +# name: test_sensors[SDM630-entity_ids6][sensor.device_power_phase_3:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'power', @@ -20957,11 +13957,12 @@ 'context': , 'entity_id': 'sensor.device_power_phase_3', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0.0', }) # --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_reactive_power:device-registry] +# name: test_sensors[SDM630-entity_ids6][sensor.device_reactive_power:device-registry] DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , @@ -20995,7 +13996,7 @@ 'via_device_id': None, }) # --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_reactive_power:entity-registry] +# name: test_sensors[SDM630-entity_ids6][sensor.device_reactive_power:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -21030,7 +14031,7 @@ 'unit_of_measurement': 'var', }) # --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_reactive_power:state] +# name: test_sensors[SDM630-entity_ids6][sensor.device_reactive_power:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'reactive_power', @@ -21041,11 +14042,12 @@ 'context': , 'entity_id': 'sensor.device_reactive_power', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '-429.025', }) # --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_reactive_power_phase_1:device-registry] +# name: test_sensors[SDM630-entity_ids6][sensor.device_reactive_power_phase_1:device-registry] DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , @@ -21079,7 +14081,7 @@ 'via_device_id': None, }) # --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_reactive_power_phase_1:entity-registry] +# name: test_sensors[SDM630-entity_ids6][sensor.device_reactive_power_phase_1:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -21114,7 +14116,7 @@ 'unit_of_measurement': 'var', }) # --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_reactive_power_phase_1:state] +# name: test_sensors[SDM630-entity_ids6][sensor.device_reactive_power_phase_1:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'reactive_power', @@ -21125,11 +14127,12 @@ 'context': , 'entity_id': 'sensor.device_reactive_power_phase_1', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0', }) # --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_reactive_power_phase_2:device-registry] +# name: test_sensors[SDM630-entity_ids6][sensor.device_reactive_power_phase_2:device-registry] DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , @@ -21163,7 +14166,7 @@ 'via_device_id': None, }) # --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_reactive_power_phase_2:entity-registry] +# name: test_sensors[SDM630-entity_ids6][sensor.device_reactive_power_phase_2:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -21198,7 +14201,7 @@ 'unit_of_measurement': 'var', }) # --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_reactive_power_phase_2:state] +# name: test_sensors[SDM630-entity_ids6][sensor.device_reactive_power_phase_2:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'reactive_power', @@ -21209,11 +14212,12 @@ 'context': , 'entity_id': 'sensor.device_reactive_power_phase_2', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '-166.675', }) # --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_reactive_power_phase_3:device-registry] +# name: test_sensors[SDM630-entity_ids6][sensor.device_reactive_power_phase_3:device-registry] DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , @@ -21247,7 +14251,7 @@ 'via_device_id': None, }) # --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_reactive_power_phase_3:entity-registry] +# name: test_sensors[SDM630-entity_ids6][sensor.device_reactive_power_phase_3:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -21282,7 +14286,7 @@ 'unit_of_measurement': 'var', }) # --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_reactive_power_phase_3:state] +# name: test_sensors[SDM630-entity_ids6][sensor.device_reactive_power_phase_3:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'reactive_power', @@ -21293,171 +14297,12 @@ 'context': , 'entity_id': 'sensor.device_reactive_power_phase_3', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '-262.35', }) # --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_total_energy_export:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'SDM630-wifi', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '3.06', - 'via_device_id': None, - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_total_energy_export:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_total_energy_export', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Total energy export', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'total_energy_export_kwh', - 'unique_id': 'aabbccddeeff_total_power_export_kwh', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_total_energy_export:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'Device Total energy export', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.device_total_energy_export', - 'last_changed': , - 'last_updated': , - 'state': '0.523', - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_total_energy_import:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - '3c:39:e7:aa:bb:cc', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - '3c39e7aabbcc', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'SDM630-wifi', - 'name': 'Device', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': '3.06', - 'via_device_id': None, - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_total_energy_import:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.device_total_energy_import', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Total energy import', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'total_energy_import_kwh', - 'unique_id': 'aabbccddeeff_total_power_import_kwh', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_total_energy_import:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'Device Total energy import', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.device_total_energy_import', - 'last_changed': , - 'last_updated': , - 'state': '0.101', - }) -# --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_voltage_phase_1:device-registry] +# name: test_sensors[SDM630-entity_ids6][sensor.device_voltage_phase_1:device-registry] DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , @@ -21491,7 +14336,7 @@ 'via_device_id': None, }) # --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_voltage_phase_1:entity-registry] +# name: test_sensors[SDM630-entity_ids6][sensor.device_voltage_phase_1:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -21526,7 +14371,7 @@ 'unit_of_measurement': , }) # --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_voltage_phase_1:state] +# name: test_sensors[SDM630-entity_ids6][sensor.device_voltage_phase_1:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'voltage', @@ -21537,11 +14382,12 @@ 'context': , 'entity_id': 'sensor.device_voltage_phase_1', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '230.751', }) # --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_voltage_phase_2:device-registry] +# name: test_sensors[SDM630-entity_ids6][sensor.device_voltage_phase_2:device-registry] DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , @@ -21575,7 +14421,7 @@ 'via_device_id': None, }) # --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_voltage_phase_2:entity-registry] +# name: test_sensors[SDM630-entity_ids6][sensor.device_voltage_phase_2:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -21610,7 +14456,7 @@ 'unit_of_measurement': , }) # --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_voltage_phase_2:state] +# name: test_sensors[SDM630-entity_ids6][sensor.device_voltage_phase_2:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'voltage', @@ -21621,11 +14467,12 @@ 'context': , 'entity_id': 'sensor.device_voltage_phase_2', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '228.391', }) # --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_voltage_phase_3:device-registry] +# name: test_sensors[SDM630-entity_ids6][sensor.device_voltage_phase_3:device-registry] DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , @@ -21659,7 +14506,7 @@ 'via_device_id': None, }) # --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_voltage_phase_3:entity-registry] +# name: test_sensors[SDM630-entity_ids6][sensor.device_voltage_phase_3:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -21694,7 +14541,7 @@ 'unit_of_measurement': , }) # --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_voltage_phase_3:state] +# name: test_sensors[SDM630-entity_ids6][sensor.device_voltage_phase_3:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'voltage', @@ -21705,11 +14552,12 @@ 'context': , 'entity_id': 'sensor.device_voltage_phase_3', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '229.612', }) # --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_wi_fi_ssid:device-registry] +# name: test_sensors[SDM630-entity_ids6][sensor.device_wi_fi_ssid:device-registry] DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , @@ -21743,7 +14591,7 @@ 'via_device_id': None, }) # --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_wi_fi_ssid:entity-registry] +# name: test_sensors[SDM630-entity_ids6][sensor.device_wi_fi_ssid:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -21776,7 +14624,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_wi_fi_ssid:state] +# name: test_sensors[SDM630-entity_ids6][sensor.device_wi_fi_ssid:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Device Wi-Fi SSID', @@ -21784,11 +14632,12 @@ 'context': , 'entity_id': 'sensor.device_wi_fi_ssid', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'My Wi-Fi', }) # --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_wi_fi_strength:device-registry] +# name: test_sensors[SDM630-entity_ids6][sensor.device_wi_fi_strength:device-registry] DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , @@ -21822,7 +14671,7 @@ 'via_device_id': None, }) # --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_wi_fi_strength:entity-registry] +# name: test_sensors[SDM630-entity_ids6][sensor.device_wi_fi_strength:entity-registry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -21857,7 +14706,7 @@ 'unit_of_measurement': '%', }) # --- -# name: test_sensors[SDM630-entity_ids5][sensor.device_wi_fi_strength:state] +# name: test_sensors[SDM630-entity_ids6][sensor.device_wi_fi_strength:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Device Wi-Fi strength', @@ -21867,893 +14716,8 @@ 'context': , 'entity_id': 'sensor.device_wi_fi_strength', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '92', }) # --- -# name: test_sensors_external_devices[HWE-P1-entity_ids0][sensor.gas_meter:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - 'G001', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'HWE-P1', - 'name': 'Gas meter', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': , - }) -# --- -# name: test_sensors_external_devices[HWE-P1-entity_ids0][sensor.gas_meter:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.gas_meter', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': 'mdi:alphabetical-variant', - 'original_name': None, - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'meter_identifier', - 'unique_id': 'homewizard_G001_meter_identifier', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensors_external_devices[HWE-P1-entity_ids0][sensor.gas_meter:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Gas meter', - 'icon': 'mdi:alphabetical-variant', - }), - 'context': , - 'entity_id': 'sensor.gas_meter', - 'last_changed': , - 'last_updated': , - 'state': 'G001', - }) -# --- -# name: test_sensors_external_devices[HWE-P1-entity_ids0][sensor.gas_meter_2:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - 'unknown_unit', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'HWE-P1', - 'name': 'Gas meter', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': , - }) -# --- -# name: test_sensors_external_devices[HWE-P1-entity_ids0][sensor.gas_meter_2:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.gas_meter_2', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': 'mdi:alphabetical-variant', - 'original_name': None, - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'meter_identifier', - 'unique_id': 'homewizard_unknown_unit_meter_identifier', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensors_external_devices[HWE-P1-entity_ids0][sensor.gas_meter_2:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Gas meter', - 'icon': 'mdi:alphabetical-variant', - }), - 'context': , - 'entity_id': 'sensor.gas_meter_2', - 'last_changed': , - 'last_updated': , - 'state': 'unknown_unit', - }) -# --- -# name: test_sensors_external_devices[HWE-P1-entity_ids0][sensor.gas_meter_total_gas:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - 'G001', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'HWE-P1', - 'name': 'Gas meter', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': , - }) -# --- -# name: test_sensors_external_devices[HWE-P1-entity_ids0][sensor.gas_meter_total_gas:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.gas_meter_total_gas', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Total gas', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'total_gas_m3', - 'unique_id': 'homewizard_G001', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors_external_devices[HWE-P1-entity_ids0][sensor.gas_meter_total_gas:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'gas', - 'friendly_name': 'Gas meter Total gas', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.gas_meter_total_gas', - 'last_changed': , - 'last_updated': , - 'state': '111.111', - }) -# --- -# name: test_sensors_external_devices[HWE-P1-entity_ids0][sensor.gas_meter_total_gas_2:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - 'unknown_unit', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'HWE-P1', - 'name': 'Gas meter', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': , - }) -# --- -# name: test_sensors_external_devices[HWE-P1-entity_ids0][sensor.gas_meter_total_gas_2:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.gas_meter_total_gas_2', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Total gas', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'total_gas_m3', - 'unique_id': 'homewizard_unknown_unit', - 'unit_of_measurement': 'cats', - }) -# --- -# name: test_sensors_external_devices[HWE-P1-entity_ids0][sensor.gas_meter_total_gas_2:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Gas meter Total gas', - 'state_class': , - 'unit_of_measurement': 'cats', - }), - 'context': , - 'entity_id': 'sensor.gas_meter_total_gas_2', - 'last_changed': , - 'last_updated': , - 'state': '666.666', - }) -# --- -# name: test_sensors_external_devices[HWE-P1-entity_ids0][sensor.heat_meter:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - 'H001', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'HWE-P1', - 'name': 'Heat meter', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': , - }) -# --- -# name: test_sensors_external_devices[HWE-P1-entity_ids0][sensor.heat_meter:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.heat_meter', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': 'mdi:alphabetical-variant', - 'original_name': None, - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'meter_identifier', - 'unique_id': 'homewizard_H001_meter_identifier', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensors_external_devices[HWE-P1-entity_ids0][sensor.heat_meter:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Heat meter', - 'icon': 'mdi:alphabetical-variant', - }), - 'context': , - 'entity_id': 'sensor.heat_meter', - 'last_changed': , - 'last_updated': , - 'state': 'H001', - }) -# --- -# name: test_sensors_external_devices[HWE-P1-entity_ids0][sensor.heat_meter_total_heat_energy:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - 'H001', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'HWE-P1', - 'name': 'Heat meter', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': , - }) -# --- -# name: test_sensors_external_devices[HWE-P1-entity_ids0][sensor.heat_meter_total_heat_energy:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.heat_meter_total_heat_energy', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Total heat energy', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'total_energy_gj', - 'unique_id': 'homewizard_H001', - 'unit_of_measurement': 'GJ', - }) -# --- -# name: test_sensors_external_devices[HWE-P1-entity_ids0][sensor.heat_meter_total_heat_energy:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'energy', - 'friendly_name': 'Heat meter Total heat energy', - 'state_class': , - 'unit_of_measurement': 'GJ', - }), - 'context': , - 'entity_id': 'sensor.heat_meter_total_heat_energy', - 'last_changed': , - 'last_updated': , - 'state': '444.444', - }) -# --- -# name: test_sensors_external_devices[HWE-P1-entity_ids0][sensor.inlet_heat_meter:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - 'IH001', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'HWE-P1', - 'name': 'Inlet heat meter', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': , - }) -# --- -# name: test_sensors_external_devices[HWE-P1-entity_ids0][sensor.inlet_heat_meter:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.inlet_heat_meter', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': 'mdi:alphabetical-variant', - 'original_name': None, - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'meter_identifier', - 'unique_id': 'homewizard_IH001_meter_identifier', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensors_external_devices[HWE-P1-entity_ids0][sensor.inlet_heat_meter:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Inlet heat meter', - 'icon': 'mdi:alphabetical-variant', - }), - 'context': , - 'entity_id': 'sensor.inlet_heat_meter', - 'last_changed': , - 'last_updated': , - 'state': 'IH001', - }) -# --- -# name: test_sensors_external_devices[HWE-P1-entity_ids0][sensor.inlet_heat_meter_total_heat_energy:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - 'IH001', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'HWE-P1', - 'name': 'Inlet heat meter', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': , - }) -# --- -# name: test_sensors_external_devices[HWE-P1-entity_ids0][sensor.inlet_heat_meter_total_heat_energy:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.inlet_heat_meter_total_heat_energy', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Total heat energy', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'total_energy_gj', - 'unique_id': 'homewizard_IH001', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors_external_devices[HWE-P1-entity_ids0][sensor.inlet_heat_meter_total_heat_energy:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Inlet heat meter Total heat energy', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.inlet_heat_meter_total_heat_energy', - 'last_changed': , - 'last_updated': , - 'state': '555.555', - }) -# --- -# name: test_sensors_external_devices[HWE-P1-entity_ids0][sensor.warm_water_meter:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - 'WW001', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'HWE-P1', - 'name': 'Warm water meter', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': , - }) -# --- -# name: test_sensors_external_devices[HWE-P1-entity_ids0][sensor.warm_water_meter:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.warm_water_meter', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': 'mdi:alphabetical-variant', - 'original_name': None, - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'meter_identifier', - 'unique_id': 'homewizard_WW001_meter_identifier', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensors_external_devices[HWE-P1-entity_ids0][sensor.warm_water_meter:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Warm water meter', - 'icon': 'mdi:alphabetical-variant', - }), - 'context': , - 'entity_id': 'sensor.warm_water_meter', - 'last_changed': , - 'last_updated': , - 'state': 'WW001', - }) -# --- -# name: test_sensors_external_devices[HWE-P1-entity_ids0][sensor.warm_water_meter_total_water_usage:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - 'WW001', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'HWE-P1', - 'name': 'Warm water meter', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': , - }) -# --- -# name: test_sensors_external_devices[HWE-P1-entity_ids0][sensor.warm_water_meter_total_water_usage:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.warm_water_meter_total_water_usage', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Total water usage', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'total_liter_m3', - 'unique_id': 'homewizard_WW001', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors_external_devices[HWE-P1-entity_ids0][sensor.warm_water_meter_total_water_usage:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'water', - 'friendly_name': 'Warm water meter Total water usage', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.warm_water_meter_total_water_usage', - 'last_changed': , - 'last_updated': , - 'state': '333.333', - }) -# --- -# name: test_sensors_external_devices[HWE-P1-entity_ids0][sensor.water_meter:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - 'W001', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'HWE-P1', - 'name': 'Water meter', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': , - }) -# --- -# name: test_sensors_external_devices[HWE-P1-entity_ids0][sensor.water_meter:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.water_meter', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': 'mdi:alphabetical-variant', - 'original_name': None, - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'meter_identifier', - 'unique_id': 'homewizard_W001_meter_identifier', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensors_external_devices[HWE-P1-entity_ids0][sensor.water_meter:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Water meter', - 'icon': 'mdi:alphabetical-variant', - }), - 'context': , - 'entity_id': 'sensor.water_meter', - 'last_changed': , - 'last_updated': , - 'state': 'W001', - }) -# --- -# name: test_sensors_external_devices[HWE-P1-entity_ids0][sensor.water_meter_total_water_usage:device-registry] - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'configuration_url': None, - 'connections': set({ - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'homewizard', - 'W001', - ), - }), - 'is_new': False, - 'manufacturer': 'HomeWizard', - 'model': 'HWE-P1', - 'name': 'Water meter', - 'name_by_user': None, - 'serial_number': None, - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': , - }) -# --- -# name: test_sensors_external_devices[HWE-P1-entity_ids0][sensor.water_meter_total_water_usage:entity-registry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.water_meter_total_water_usage', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Total water usage', - 'platform': 'homewizard', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'total_liter_m3', - 'unique_id': 'homewizard_W001', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors_external_devices[HWE-P1-entity_ids0][sensor.water_meter_total_water_usage:state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'water', - 'friendly_name': 'Water meter Total water usage', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.water_meter_total_water_usage', - 'last_changed': , - 'last_updated': , - 'state': '222.222', - }) -# --- diff --git a/tests/components/homewizard/snapshots/test_switch.ambr b/tests/components/homewizard/snapshots/test_switch.ambr index 55d327c2244..99a5bcab6cb 100644 --- a/tests/components/homewizard/snapshots/test_switch.ambr +++ b/tests/components/homewizard/snapshots/test_switch.ambr @@ -7,6 +7,7 @@ 'context': , 'entity_id': 'switch.device_cloud_connection', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }) @@ -86,6 +87,7 @@ 'context': , 'entity_id': 'switch.device_cloud_connection', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }) @@ -157,7 +159,7 @@ 'via_device_id': None, }) # --- -# name: test_switch_entities[HWE-SKT-switch.device-state_set-power_on] +# name: test_switch_entities[HWE-SKT-11-switch.device-state_set-power_on] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'outlet', @@ -166,11 +168,12 @@ 'context': , 'entity_id': 'switch.device', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }) # --- -# name: test_switch_entities[HWE-SKT-switch.device-state_set-power_on].1 +# name: test_switch_entities[HWE-SKT-11-switch.device-state_set-power_on].1 EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -203,7 +206,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_switch_entities[HWE-SKT-switch.device-state_set-power_on].2 +# name: test_switch_entities[HWE-SKT-11-switch.device-state_set-power_on].2 DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , @@ -237,7 +240,7 @@ 'via_device_id': None, }) # --- -# name: test_switch_entities[HWE-SKT-switch.device_cloud_connection-system_set-cloud_enabled] +# name: test_switch_entities[HWE-SKT-11-switch.device_cloud_connection-system_set-cloud_enabled] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Device Cloud connection', @@ -245,11 +248,12 @@ 'context': , 'entity_id': 'switch.device_cloud_connection', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }) # --- -# name: test_switch_entities[HWE-SKT-switch.device_cloud_connection-system_set-cloud_enabled].1 +# name: test_switch_entities[HWE-SKT-11-switch.device_cloud_connection-system_set-cloud_enabled].1 EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -282,7 +286,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_switch_entities[HWE-SKT-switch.device_cloud_connection-system_set-cloud_enabled].2 +# name: test_switch_entities[HWE-SKT-11-switch.device_cloud_connection-system_set-cloud_enabled].2 DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , @@ -316,7 +320,7 @@ 'via_device_id': None, }) # --- -# name: test_switch_entities[HWE-SKT-switch.device_switch_lock-state_set-switch_lock] +# name: test_switch_entities[HWE-SKT-11-switch.device_switch_lock-state_set-switch_lock] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Device Switch lock', @@ -324,11 +328,12 @@ 'context': , 'entity_id': 'switch.device_switch_lock', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }) # --- -# name: test_switch_entities[HWE-SKT-switch.device_switch_lock-state_set-switch_lock].1 +# name: test_switch_entities[HWE-SKT-11-switch.device_switch_lock-state_set-switch_lock].1 EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -361,7 +366,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_switch_entities[HWE-SKT-switch.device_switch_lock-state_set-switch_lock].2 +# name: test_switch_entities[HWE-SKT-11-switch.device_switch_lock-state_set-switch_lock].2 DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , @@ -395,6 +400,327 @@ 'via_device_id': None, }) # --- +# name: test_switch_entities[HWE-SKT-21-switch.device-state_set-power_on] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'Device', + }), + 'context': , + 'entity_id': 'switch.device', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch_entities[HWE-SKT-21-switch.device-state_set-power_on].1 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.device', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': None, + 'platform': 'homewizard', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'aabbccddeeff_power_on', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch_entities[HWE-SKT-21-switch.device-state_set-power_on].2 + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'configuration_url': None, + 'connections': set({ + tuple( + 'mac', + '3c:39:e7:aa:bb:cc', + ), + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'homewizard', + '3c39e7aabbcc', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'HomeWizard', + 'model': 'HWE-SKT', + 'name': 'Device', + 'name_by_user': None, + 'serial_number': None, + 'suggested_area': None, + 'sw_version': '4.07', + 'via_device_id': None, + }) +# --- +# name: test_switch_entities[HWE-SKT-21-switch.device_cloud_connection-system_set-cloud_enabled] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Device Cloud connection', + }), + 'context': , + 'entity_id': 'switch.device_cloud_connection', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch_entities[HWE-SKT-21-switch.device_cloud_connection-system_set-cloud_enabled].1 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.device_cloud_connection', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Cloud connection', + 'platform': 'homewizard', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'cloud_connection', + 'unique_id': 'aabbccddeeff_cloud_connection', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch_entities[HWE-SKT-21-switch.device_cloud_connection-system_set-cloud_enabled].2 + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'configuration_url': None, + 'connections': set({ + tuple( + 'mac', + '3c:39:e7:aa:bb:cc', + ), + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'homewizard', + '3c39e7aabbcc', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'HomeWizard', + 'model': 'HWE-SKT', + 'name': 'Device', + 'name_by_user': None, + 'serial_number': None, + 'suggested_area': None, + 'sw_version': '4.07', + 'via_device_id': None, + }) +# --- +# name: test_switch_entities[HWE-SKT-21-switch.device_switch_lock-state_set-switch_lock] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Device Switch lock', + }), + 'context': , + 'entity_id': 'switch.device_switch_lock', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_switch_entities[HWE-SKT-21-switch.device_switch_lock-state_set-switch_lock].1 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.device_switch_lock', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Switch lock', + 'platform': 'homewizard', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'switch_lock', + 'unique_id': 'aabbccddeeff_switch_lock', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch_entities[HWE-SKT-21-switch.device_switch_lock-state_set-switch_lock].2 + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'configuration_url': None, + 'connections': set({ + tuple( + 'mac', + '3c:39:e7:aa:bb:cc', + ), + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'homewizard', + '3c39e7aabbcc', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'HomeWizard', + 'model': 'HWE-SKT', + 'name': 'Device', + 'name_by_user': None, + 'serial_number': None, + 'suggested_area': None, + 'sw_version': '4.07', + 'via_device_id': None, + }) +# --- +# name: test_switch_entities[HWE-WTR-switch.device_cloud_connection-system_set-cloud_enabled] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Device Cloud connection', + }), + 'context': , + 'entity_id': 'switch.device_cloud_connection', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch_entities[HWE-WTR-switch.device_cloud_connection-system_set-cloud_enabled].1 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.device_cloud_connection', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Cloud connection', + 'platform': 'homewizard', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'cloud_connection', + 'unique_id': 'aabbccddeeff_cloud_connection', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch_entities[HWE-WTR-switch.device_cloud_connection-system_set-cloud_enabled].2 + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'configuration_url': None, + 'connections': set({ + tuple( + 'mac', + '3c:39:e7:aa:bb:cc', + ), + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'homewizard', + '3c39e7aabbcc', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'HomeWizard', + 'model': 'HWE-WTR', + 'name': 'Device', + 'name_by_user': None, + 'serial_number': None, + 'suggested_area': None, + 'sw_version': '2.03', + 'via_device_id': None, + }) +# --- # name: test_switch_entities[SDM230-switch.device_cloud_connection-system_set-cloud_enabled] StateSnapshot({ 'attributes': ReadOnlyDict({ @@ -403,6 +729,7 @@ 'context': , 'entity_id': 'switch.device_cloud_connection', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }) @@ -482,6 +809,7 @@ 'context': , 'entity_id': 'switch.device_cloud_connection', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }) diff --git a/tests/components/homewizard/test_button.py b/tests/components/homewizard/test_button.py index b73a194c5ae..928e6f21901 100644 --- a/tests/components/homewizard/test_button.py +++ b/tests/components/homewizard/test_button.py @@ -1,4 +1,5 @@ """Test the identify button for HomeWizard.""" + from unittest.mock import MagicMock from homewizard_energy.errors import DisabledError, RequestError @@ -17,9 +18,7 @@ pytestmark = [ ] -@pytest.mark.parametrize( - "device_fixture", ["HWE-WTR", "SDM230", "SDM630", "HWE-KWH1", "HWE-KWH3"] -) +@pytest.mark.parametrize("device_fixture", ["SDM230", "SDM630", "HWE-KWH1", "HWE-KWH3"]) async def test_identify_button_entity_not_loaded_when_not_available( hass: HomeAssistant, ) -> None: diff --git a/tests/components/homewizard/test_config_flow.py b/tests/components/homewizard/test_config_flow.py index 5e71826b28d..f0776877aec 100644 --- a/tests/components/homewizard/test_config_flow.py +++ b/tests/components/homewizard/test_config_flow.py @@ -1,4 +1,5 @@ """Test the homewizard config flow.""" + from ipaddress import ip_address from unittest.mock import AsyncMock, MagicMock @@ -289,6 +290,7 @@ async def test_error_flow( assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": reason} + assert result["data_schema"]({}) == {CONF_IP_ADDRESS: "127.0.0.1"} # Recover from error mock_homewizardenergy.device.side_effect = None diff --git a/tests/components/homewizard/test_diagnostics.py b/tests/components/homewizard/test_diagnostics.py index 8356c94d164..e3d7f4e6da9 100644 --- a/tests/components/homewizard/test_diagnostics.py +++ b/tests/components/homewizard/test_diagnostics.py @@ -14,7 +14,8 @@ from tests.typing import ClientSessionGenerator "device_fixture", [ "HWE-P1", - "HWE-SKT", + "HWE-SKT-11", + "HWE-SKT-21", "HWE-WTR", "SDM230", "SDM630", diff --git a/tests/components/homewizard/test_init.py b/tests/components/homewizard/test_init.py index e777b2d43c6..438df8ab869 100644 --- a/tests/components/homewizard/test_init.py +++ b/tests/components/homewizard/test_init.py @@ -1,4 +1,5 @@ """Tests for the homewizard component.""" + from unittest.mock import MagicMock from homewizard_energy.errors import DisabledError, HomeWizardEnergyException @@ -125,12 +126,22 @@ async def test_load_handles_homewizardenergy_exception( ("device_fixture", "old_unique_id", "new_unique_id"), [ ( - "HWE-SKT", + "HWE-SKT-11", "aabbccddeeff_total_power_import_t1_kwh", "aabbccddeeff_total_power_import_kwh", ), ( - "HWE-SKT", + "HWE-SKT-11", + "aabbccddeeff_total_power_export_t1_kwh", + "aabbccddeeff_total_power_export_kwh", + ), + ( + "HWE-SKT-21", + "aabbccddeeff_total_power_import_t1_kwh", + "aabbccddeeff_total_power_import_kwh", + ), + ( + "HWE-SKT-21", "aabbccddeeff_total_power_export_t1_kwh", "aabbccddeeff_total_power_export_kwh", ), @@ -169,12 +180,22 @@ async def test_sensor_migration( ("device_fixture", "old_unique_id", "new_unique_id"), [ ( - "HWE-SKT", + "HWE-SKT-11", "aabbccddeeff_total_power_import_t1_kwh", "aabbccddeeff_total_power_import_kwh", ), ( - "HWE-SKT", + "HWE-SKT-11", + "aabbccddeeff_total_power_export_t1_kwh", + "aabbccddeeff_total_power_export_kwh", + ), + ( + "HWE-SKT-21", + "aabbccddeeff_total_power_import_t1_kwh", + "aabbccddeeff_total_power_import_kwh", + ), + ( + "HWE-SKT-21", "aabbccddeeff_total_power_export_t1_kwh", "aabbccddeeff_total_power_export_kwh", ), diff --git a/tests/components/homewizard/test_number.py b/tests/components/homewizard/test_number.py index a7fb2834bd3..ff27fb1b257 100644 --- a/tests/components/homewizard/test_number.py +++ b/tests/components/homewizard/test_number.py @@ -1,4 +1,5 @@ """Test the number entity for HomeWizard.""" + from unittest.mock import MagicMock from homewizard_energy.errors import DisabledError, RequestError @@ -21,7 +22,7 @@ pytestmark = [ ] -@pytest.mark.parametrize("device_fixture", ["HWE-SKT"]) +@pytest.mark.parametrize("device_fixture", ["HWE-SKT-11", "HWE-SKT-21"]) async def test_number_entities( hass: HomeAssistant, device_registry: dr.DeviceRegistry, diff --git a/tests/components/homewizard/test_sensor.py b/tests/components/homewizard/test_sensor.py index 243e8f542e2..5a1b25c69bb 100644 --- a/tests/components/homewizard/test_sensor.py +++ b/tests/components/homewizard/test_sensor.py @@ -2,7 +2,7 @@ from unittest.mock import MagicMock -from homewizard_energy.errors import DisabledError, RequestError +from homewizard_energy.errors import RequestError from homewizard_energy.models import Data import pytest from syrupy.assertion import SnapshotAssertion @@ -112,7 +112,7 @@ pytestmark = [ ], ), ( - "HWE-SKT", + "HWE-SKT-11", [ "sensor.device_energy_export", "sensor.device_energy_import", @@ -122,6 +122,23 @@ pytestmark = [ "sensor.device_wi_fi_strength", ], ), + ( + "HWE-SKT-21", + [ + "sensor.device_apparent_power", + "sensor.device_current", + "sensor.device_energy_export", + "sensor.device_energy_import", + "sensor.device_frequency", + "sensor.device_power_factor", + "sensor.device_power_phase_1", + "sensor.device_power", + "sensor.device_reactive_power", + "sensor.device_voltage", + "sensor.device_wi_fi_ssid", + "sensor.device_wi_fi_strength", + ], + ), ( "HWE-WTR", [ @@ -276,7 +293,13 @@ async def test_sensors( ], ), ( - "HWE-SKT", + "HWE-SKT-11", + [ + "sensor.device_wi_fi_strength", + ], + ), + ( + "HWE-SKT-21", [ "sensor.device_wi_fi_strength", ], @@ -375,7 +398,7 @@ async def test_disabled_by_default_sensors( assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION -@pytest.mark.parametrize("exception", [RequestError, DisabledError]) +@pytest.mark.parametrize("exception", [RequestError]) async def test_sensors_unreachable( hass: HomeAssistant, mock_homewizardenergy: MagicMock, @@ -413,7 +436,7 @@ async def test_external_sensors_unreachable( ("device_fixture", "entity_ids"), [ ( - "HWE-SKT", + "HWE-SKT-11", [ "sensor.device_apparent_power_phase_1", "sensor.device_apparent_power_phase_2", @@ -464,6 +487,52 @@ async def test_external_sensors_unreachable( "sensor.device_water_usage", ], ), + ( + "HWE-SKT-21", + [ + "sensor.device_apparent_power_phase_1", + "sensor.device_apparent_power_phase_2", + "sensor.device_apparent_power_phase_3", + "sensor.device_average_demand", + "sensor.device_current_phase_1", + "sensor.device_current_phase_2", + "sensor.device_current_phase_3", + "sensor.device_dsmr_version", + "sensor.device_energy_export_tariff_1", + "sensor.device_energy_export_tariff_2", + "sensor.device_energy_export_tariff_3", + "sensor.device_energy_export_tariff_4", + "sensor.device_energy_import_tariff_1", + "sensor.device_energy_import_tariff_2", + "sensor.device_energy_import_tariff_3", + "sensor.device_energy_import_tariff_4", + "sensor.device_long_power_failures_detected", + "sensor.device_peak_demand_current_month", + "sensor.device_power_factor_phase_1", + "sensor.device_power_factor_phase_2", + "sensor.device_power_factor_phase_3", + "sensor.device_power_failures_detected", + "sensor.device_power_phase_2", + "sensor.device_power_phase_3", + "sensor.device_reactive_power_phase_1", + "sensor.device_reactive_power_phase_2", + "sensor.device_reactive_power_phase_3", + "sensor.device_smart_meter_identifier", + "sensor.device_smart_meter_model", + "sensor.device_tariff", + "sensor.device_total_water_usage", + "sensor.device_voltage_phase_1", + "sensor.device_voltage_phase_2", + "sensor.device_voltage_phase_3", + "sensor.device_voltage_sags_detected_phase_1", + "sensor.device_voltage_sags_detected_phase_2", + "sensor.device_voltage_sags_detected_phase_3", + "sensor.device_voltage_swells_detected_phase_1", + "sensor.device_voltage_swells_detected_phase_2", + "sensor.device_voltage_swells_detected_phase_3", + "sensor.device_water_usage", + ], + ), ( "HWE-WTR", [ diff --git a/tests/components/homewizard/test_switch.py b/tests/components/homewizard/test_switch.py index bfc23264340..b9e812620e8 100644 --- a/tests/components/homewizard/test_switch.py +++ b/tests/components/homewizard/test_switch.py @@ -1,4 +1,5 @@ """Test the switch entity for HomeWizard.""" + from unittest.mock import MagicMock from homewizard_energy import UnsupportedError @@ -41,7 +42,6 @@ pytestmark = [ [ "switch.device", "switch.device_switch_lock", - "switch.device_cloud_connection", ], ), ( @@ -86,9 +86,13 @@ async def test_entities_not_created_for_device( @pytest.mark.parametrize( ("device_fixture", "entity_id", "method", "parameter"), [ - ("HWE-SKT", "switch.device", "state_set", "power_on"), - ("HWE-SKT", "switch.device_switch_lock", "state_set", "switch_lock"), - ("HWE-SKT", "switch.device_cloud_connection", "system_set", "cloud_enabled"), + ("HWE-SKT-11", "switch.device", "state_set", "power_on"), + ("HWE-SKT-11", "switch.device_switch_lock", "state_set", "switch_lock"), + ("HWE-SKT-11", "switch.device_cloud_connection", "system_set", "cloud_enabled"), + ("HWE-SKT-21", "switch.device", "state_set", "power_on"), + ("HWE-SKT-21", "switch.device_switch_lock", "state_set", "switch_lock"), + ("HWE-SKT-21", "switch.device_cloud_connection", "system_set", "cloud_enabled"), + ("HWE-WTR", "switch.device_cloud_connection", "system_set", "cloud_enabled"), ("SDM230", "switch.device_cloud_connection", "system_set", "cloud_enabled"), ("SDM630", "switch.device_cloud_connection", "system_set", "cloud_enabled"), ("HWE-KWH1", "switch.device_cloud_connection", "system_set", "cloud_enabled"), @@ -191,8 +195,8 @@ async def test_switch_entities( ) -@pytest.mark.parametrize("device_fixture", ["HWE-SKT"]) -@pytest.mark.parametrize("exception", [RequestError, DisabledError, UnsupportedError]) +@pytest.mark.parametrize("device_fixture", ["HWE-SKT-11", "HWE-SKT-21"]) +@pytest.mark.parametrize("exception", [RequestError, UnsupportedError]) @pytest.mark.parametrize( ("entity_id", "method"), [ diff --git a/tests/components/homeworks/__init__.py b/tests/components/homeworks/__init__.py new file mode 100644 index 00000000000..6cb38e6ff81 --- /dev/null +++ b/tests/components/homeworks/__init__.py @@ -0,0 +1 @@ +"""Tests for the Lutron Homeworks Series 4 and 8 integration.""" diff --git a/tests/components/homeworks/conftest.py b/tests/components/homeworks/conftest.py new file mode 100644 index 00000000000..ccff56ae3d1 --- /dev/null +++ b/tests/components/homeworks/conftest.py @@ -0,0 +1,111 @@ +"""Common fixtures for the Lutron Homeworks Series 4 and 8 tests.""" + +from collections.abc import Generator +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + +from homeassistant.components.homeworks.const import ( + CONF_ADDR, + CONF_BUTTONS, + CONF_CONTROLLER_ID, + CONF_DIMMERS, + CONF_KEYPADS, + CONF_LED, + CONF_NUMBER, + CONF_RATE, + CONF_RELEASE_DELAY, + DOMAIN, +) +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT + +from tests.common import MockConfigEntry + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Return the default mocked config entry.""" + return MockConfigEntry( + title="Lutron Homeworks", + domain=DOMAIN, + data={}, + options={ + CONF_CONTROLLER_ID: "main_controller", + CONF_HOST: "192.168.0.1", + CONF_PORT: 1234, + CONF_DIMMERS: [ + { + CONF_ADDR: "[02:08:01:01]", + CONF_NAME: "Foyer Sconces", + CONF_RATE: 1.0, + } + ], + CONF_KEYPADS: [ + { + CONF_ADDR: "[02:08:02:01]", + CONF_NAME: "Foyer Keypad", + CONF_BUTTONS: [ + { + CONF_NAME: "Morning", + CONF_NUMBER: 1, + CONF_LED: True, + CONF_RELEASE_DELAY: None, + }, + { + CONF_NAME: "Relax", + CONF_NUMBER: 2, + CONF_LED: True, + CONF_RELEASE_DELAY: None, + }, + { + CONF_NAME: "Dim up", + CONF_NUMBER: 3, + CONF_LED: False, + CONF_RELEASE_DELAY: 0.2, + }, + ], + } + ], + }, + ) + + +@pytest.fixture +def mock_empty_config_entry() -> MockConfigEntry: + """Return a mocked config entry with no keypads or dimmers.""" + return MockConfigEntry( + title="Lutron Homeworks", + domain=DOMAIN, + data={}, + options={ + CONF_CONTROLLER_ID: "main_controller", + CONF_HOST: "192.168.0.1", + CONF_PORT: 1234, + CONF_DIMMERS: [], + CONF_KEYPADS: [], + }, + ) + + +@pytest.fixture +def mock_homeworks() -> Generator[None, MagicMock, None]: + """Return a mocked Homeworks client.""" + with ( + patch( + "homeassistant.components.homeworks.Homeworks", autospec=True + ) as homeworks_mock, + patch( + "homeassistant.components.homeworks.config_flow.Homeworks", + new=homeworks_mock, + ), + ): + yield homeworks_mock + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock, None, None]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.homeworks.async_setup_entry", return_value=True + ) as mock_setup_entry: + yield mock_setup_entry diff --git a/tests/components/homeworks/snapshots/test_binary_sensor.ambr b/tests/components/homeworks/snapshots/test_binary_sensor.ambr new file mode 100644 index 00000000000..c301347d05d --- /dev/null +++ b/tests/components/homeworks/snapshots/test_binary_sensor.ambr @@ -0,0 +1,43 @@ +# serializer version: 1 +# name: test_binary_sensor_attributes_state_update + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Foyer Keypad Morning', + 'homeworks_address': '[02:08:02:01]', + }), + 'context': , + 'entity_id': 'binary_sensor.foyer_keypad_morning', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_binary_sensor_attributes_state_update.1 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Foyer Keypad Morning', + 'homeworks_address': '[02:08:02:01]', + }), + 'context': , + 'entity_id': 'binary_sensor.foyer_keypad_morning', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensor_attributes_state_update.2 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Foyer Keypad Morning', + 'homeworks_address': '[02:08:02:01]', + }), + 'context': , + 'entity_id': 'binary_sensor.foyer_keypad_morning', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- diff --git a/tests/components/homeworks/snapshots/test_light.ambr b/tests/components/homeworks/snapshots/test_light.ambr new file mode 100644 index 00000000000..7117a9d9b48 --- /dev/null +++ b/tests/components/homeworks/snapshots/test_light.ambr @@ -0,0 +1,41 @@ +# serializer version: 1 +# name: test_light_attributes_state_update + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': None, + 'color_mode': None, + 'friendly_name': 'Foyer Sconces', + 'homeworks_address': '[02:08:01:01]', + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + }), + 'context': , + 'entity_id': 'light.foyer_sconces', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_light_attributes_state_update.1 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': 127, + 'color_mode': , + 'friendly_name': 'Foyer Sconces', + 'homeworks_address': '[02:08:01:01]', + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + }), + 'context': , + 'entity_id': 'light.foyer_sconces', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- diff --git a/tests/components/homeworks/test_binary_sensor.py b/tests/components/homeworks/test_binary_sensor.py new file mode 100644 index 00000000000..0b21ae3b773 --- /dev/null +++ b/tests/components/homeworks/test_binary_sensor.py @@ -0,0 +1,70 @@ +"""Tests for the Lutron Homeworks Series 4 and 8 binary sensor.""" + +from unittest.mock import ANY, MagicMock + +from freezegun.api import FrozenDateTimeFactory +from pyhomeworks.pyhomeworks import HW_KEYPAD_LED_CHANGED +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN +from homeassistant.components.homeworks import KEYPAD_LEDSTATE_POLL_COOLDOWN +from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNKNOWN +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry, async_fire_time_changed + + +async def test_binary_sensor_attributes_state_update( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_homeworks: MagicMock, + freezer: FrozenDateTimeFactory, + snapshot: SnapshotAssertion, +) -> None: + """Test Homeworks binary sensor state changes.""" + entity_id = "binary_sensor.foyer_keypad_morning" + mock_controller = MagicMock() + mock_homeworks.return_value = mock_controller + + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + mock_homeworks.assert_called_once_with("192.168.0.1", 1234, ANY) + hw_callback = mock_homeworks.mock_calls[0][1][2] + + assert entity_id in hass.states.async_entity_ids(BINARY_SENSOR_DOMAIN) + + state = hass.states.get(entity_id) + assert state.state == STATE_UNKNOWN + assert state == snapshot + + freezer.tick(KEYPAD_LEDSTATE_POLL_COOLDOWN + 1) + async_fire_time_changed(hass) + await hass.async_block_till_done() + assert len(mock_controller._send.mock_calls) == 1 + assert mock_controller._send.mock_calls[0][1] == ("RKLS, [02:08:02:01]",) + + hw_callback( + HW_KEYPAD_LED_CHANGED, + [ + "[02:08:02:01]", + [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + ], + ) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == STATE_ON + assert state == snapshot + + hw_callback( + HW_KEYPAD_LED_CHANGED, + [ + "[02:08:02:01]", + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + ], + ) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == STATE_OFF + assert state == snapshot diff --git a/tests/components/homeworks/test_button.py b/tests/components/homeworks/test_button.py new file mode 100644 index 00000000000..1bf4f93e49a --- /dev/null +++ b/tests/components/homeworks/test_button.py @@ -0,0 +1,58 @@ +"""Tests for the Lutron Homeworks Series 4 and 8 button.""" + +from unittest.mock import MagicMock + +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def test_button_service_calls( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_homeworks: MagicMock, +) -> None: + """Test Homeworks button service call.""" + entity_id = "button.foyer_keypad_morning" + mock_controller = MagicMock() + mock_homeworks.return_value = mock_controller + + 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 entity_id in hass.states.async_entity_ids(BUTTON_DOMAIN) + + mock_controller._send.reset_mock() + await hass.services.async_call( + BUTTON_DOMAIN, SERVICE_PRESS, {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + assert len(mock_controller._send.mock_calls) == 1 + assert mock_controller._send.mock_calls[0][1] == ("KBP, [02:08:02:01], 1",) + + +async def test_button_service_calls_delay( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_homeworks: MagicMock, +) -> None: + """Test Homeworks button service call.""" + entity_id = "button.foyer_keypad_dim_up" + mock_controller = MagicMock() + mock_homeworks.return_value = mock_controller + + 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 entity_id in hass.states.async_entity_ids(BUTTON_DOMAIN) + + mock_controller._send.reset_mock() + await hass.services.async_call( + BUTTON_DOMAIN, SERVICE_PRESS, {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + assert len(mock_controller._send.mock_calls) == 2 + assert mock_controller._send.mock_calls[0][1] == ("KBP, [02:08:02:01], 3",) + assert mock_controller._send.mock_calls[1][1] == ("KBR, [02:08:02:01], 3",) diff --git a/tests/components/homeworks/test_config_flow.py b/tests/components/homeworks/test_config_flow.py new file mode 100644 index 00000000000..4bdb5938f1c --- /dev/null +++ b/tests/components/homeworks/test_config_flow.py @@ -0,0 +1,1099 @@ +"""Test Lutron Homeworks Series 4 and 8 config flow.""" + +from unittest.mock import ANY, MagicMock + +import pytest +from pytest_unordered import unordered + +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN +from homeassistant.components.homeworks.const import ( + CONF_ADDR, + CONF_BUTTONS, + CONF_DIMMERS, + CONF_INDEX, + CONF_KEYPADS, + CONF_LED, + CONF_NUMBER, + CONF_RATE, + CONF_RELEASE_DELAY, + DOMAIN, +) +from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_RECONFIGURE, SOURCE_USER +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers import entity_registry as er, issue_registry as ir + +from tests.common import MockConfigEntry + + +async def test_user_flow( + hass: HomeAssistant, mock_homeworks: MagicMock, mock_setup_entry +) -> None: + """Test the user configuration flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) + + mock_controller = MagicMock() + mock_homeworks.return_value = mock_controller + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: "192.168.0.1", + CONF_NAME: "Main controller", + CONF_PORT: 1234, + }, + ) + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "Main controller" + assert result["data"] == {} + assert result["options"] == { + "controller_id": "main_controller", + "dimmers": [], + "host": "192.168.0.1", + "keypads": [], + "port": 1234, + } + mock_homeworks.assert_called_once_with("192.168.0.1", 1234, ANY) + mock_controller.close.assert_called_once_with() + mock_controller.join.assert_called_once_with() + + +async def test_user_flow_already_exists( + hass: HomeAssistant, mock_empty_config_entry: MockConfigEntry, mock_setup_entry +) -> None: + """Test the user configuration flow.""" + mock_empty_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: "192.168.0.1", + CONF_NAME: "Main controller", + CONF_PORT: 1234, + }, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": "duplicated_host_port"} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: "192.168.0.2", + CONF_NAME: "Main controller", + CONF_PORT: 1234, + }, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": "duplicated_controller_id"} + + +@pytest.mark.parametrize( + ("side_effect", "error"), + [(ConnectionError, "connection_error"), (Exception, "unknown_error")], +) +async def test_user_flow_cannot_connect( + hass: HomeAssistant, + mock_homeworks: MagicMock, + mock_setup_entry, + side_effect: type[Exception], + error: str, +) -> None: + """Test handling invalid connection.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) + + mock_homeworks.side_effect = side_effect + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: "192.168.0.1", + CONF_NAME: "Main controller", + CONF_PORT: 1234, + }, + ) + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {"base": error} + assert result["step_id"] == "user" + + +async def test_import_flow( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + issue_registry: ir.IssueRegistry, + mock_homeworks: MagicMock, + mock_setup_entry, +) -> None: + """Test importing yaml config.""" + entry = entity_registry.async_get_or_create( + LIGHT_DOMAIN, DOMAIN, "homeworks.[02:08:01:01]" + ) + + mock_controller = MagicMock() + mock_homeworks.return_value = mock_controller + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={ + CONF_HOST: "192.168.0.1", + CONF_PORT: 1234, + CONF_DIMMERS: [ + { + CONF_ADDR: "[02:08:01:01]", + CONF_NAME: "Foyer Sconces", + CONF_RATE: 1.0, + } + ], + CONF_KEYPADS: [ + { + CONF_ADDR: "[02:08:02:01]", + CONF_NAME: "Foyer Keypad", + CONF_BUTTONS: [ + { + CONF_NAME: "Morning", + CONF_NUMBER: 1, + CONF_LED: True, + CONF_RELEASE_DELAY: None, + }, + { + CONF_NAME: "Relax", + CONF_NUMBER: 2, + CONF_LED: True, + CONF_RELEASE_DELAY: None, + }, + { + CONF_NAME: "Dim up", + CONF_NUMBER: 3, + CONF_LED: False, + CONF_RELEASE_DELAY: 0.2, + }, + ], + } + ], + }, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "import_controller_name" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_NAME: "Main controller"} + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "import_finish" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "Main controller" + assert result["data"] == {} + assert result["options"] == { + "controller_id": "main_controller", + "dimmers": [{"addr": "[02:08:01:01]", "name": "Foyer Sconces", "rate": 1.0}], + "host": "192.168.0.1", + "keypads": [ + { + "addr": "[02:08:02:01]", + "buttons": [ + { + "led": True, + "name": "Morning", + "number": 1, + "release_delay": None, + }, + {"led": True, "name": "Relax", "number": 2, "release_delay": None}, + {"led": False, "name": "Dim up", "number": 3, "release_delay": 0.2}, + ], + "name": "Foyer Keypad", + } + ], + "port": 1234, + } + assert len(issue_registry.issues) == 0 + + # Check unique ID is updated in entity registry + entry = entity_registry.async_get(entry.id) + assert entry.unique_id == "homeworks.main_controller.[02:08:01:01].0" + + +async def test_import_flow_already_exists( + hass: HomeAssistant, + issue_registry: ir.IssueRegistry, + mock_empty_config_entry: MockConfigEntry, +) -> None: + """Test importing yaml config where entry already exists.""" + mock_empty_config_entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={"host": "192.168.0.1", "port": 1234, CONF_DIMMERS: [], CONF_KEYPADS: []}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + assert len(issue_registry.issues) == 1 + + +async def test_import_flow_controller_id_exists( + hass: HomeAssistant, mock_empty_config_entry: MockConfigEntry +) -> None: + """Test importing yaml config where entry already exists.""" + mock_empty_config_entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={"host": "192.168.0.2", "port": 1234, CONF_DIMMERS: [], CONF_KEYPADS: []}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "import_controller_name" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_NAME: "Main controller"} + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "import_controller_name" + assert result["errors"] == {"base": "duplicated_controller_id"} + + +async def test_reconfigure_flow( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_homeworks: MagicMock +) -> None: + """Test reconfigure flow.""" + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_RECONFIGURE, "entry_id": mock_config_entry.entry_id}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "reconfigure" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: "192.168.0.2", + CONF_PORT: 1234, + }, + ) + await hass.async_block_till_done() + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + assert mock_config_entry.options == { + "controller_id": "main_controller", + "dimmers": [ + {"addr": "[02:08:01:01]", "name": "Foyer Sconces", "rate": 1.0}, + ], + "host": "192.168.0.2", + "keypads": [ + { + "addr": "[02:08:02:01]", + "buttons": [ + { + "led": True, + "name": "Morning", + "number": 1, + "release_delay": None, + }, + {"led": True, "name": "Relax", "number": 2, "release_delay": None}, + {"led": False, "name": "Dim up", "number": 3, "release_delay": 0.2}, + ], + "name": "Foyer Keypad", + }, + ], + "port": 1234, + } + + +async def test_reconfigure_flow_flow_duplicate( + hass: HomeAssistant, mock_homeworks: MagicMock +) -> None: + """Test reconfigure flow.""" + entry1 = MockConfigEntry( + domain=DOMAIN, + data={}, + options={ + "controller_id": "controller_1", + "host": "192.168.0.1", + "port": 1234, + }, + ) + entry1.add_to_hass(hass) + entry2 = MockConfigEntry( + domain=DOMAIN, + data={}, + options={ + "controller_id": "controller_2", + "host": "192.168.0.2", + "port": 1234, + }, + ) + entry2.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_RECONFIGURE, "entry_id": entry1.entry_id}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "reconfigure" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: "192.168.0.2", + CONF_PORT: 1234, + }, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "reconfigure" + assert result["errors"] == {"base": "duplicated_host_port"} + + +async def test_reconfigure_flow_flow_no_change( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_homeworks: MagicMock +) -> None: + """Test reconfigure flow.""" + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_RECONFIGURE, "entry_id": mock_config_entry.entry_id}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "reconfigure" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: "192.168.0.1", + CONF_PORT: 1234, + }, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + assert mock_config_entry.options == { + "controller_id": "main_controller", + "dimmers": [ + {"addr": "[02:08:01:01]", "name": "Foyer Sconces", "rate": 1.0}, + ], + "host": "192.168.0.1", + "keypads": [ + { + "addr": "[02:08:02:01]", + "buttons": [ + { + "led": True, + "name": "Morning", + "number": 1, + "release_delay": None, + }, + {"led": True, "name": "Relax", "number": 2, "release_delay": None}, + {"led": False, "name": "Dim up", "number": 3, "release_delay": 0.2}, + ], + "name": "Foyer Keypad", + } + ], + "port": 1234, + } + + +async def test_options_add_light_flow( + hass: HomeAssistant, + mock_empty_config_entry: MockConfigEntry, + mock_homeworks: MagicMock, +) -> None: + """Test options flow to add a light.""" + mock_empty_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_empty_config_entry.entry_id) + await hass.async_block_till_done() + assert hass.states.async_entity_ids("light") == unordered([]) + + result = await hass.config_entries.options.async_init( + mock_empty_config_entry.entry_id + ) + assert result["type"] == FlowResultType.MENU + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + {"next_step_id": "add_light"}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "add_light" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_ADDR: "[02:08:01:02]", + CONF_NAME: "Foyer Downlights", + CONF_RATE: 2.0, + }, + ) + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["data"] == { + "controller_id": "main_controller", + "dimmers": [ + {"addr": "[02:08:01:02]", "name": "Foyer Downlights", "rate": 2.0}, + ], + "host": "192.168.0.1", + "keypads": [], + "port": 1234, + } + + await hass.async_block_till_done() + + # Check the entry was updated with the new entity + assert hass.states.async_entity_ids("light") == unordered( + ["light.foyer_downlights"] + ) + + +async def test_options_add_remove_light_flow( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_homeworks: MagicMock +) -> None: + """Test options flow to add and remove a light.""" + 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 hass.states.async_entity_ids("light") == unordered(["light.foyer_sconces"]) + + result = await hass.config_entries.options.async_init(mock_config_entry.entry_id) + assert result["type"] == FlowResultType.MENU + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + {"next_step_id": "add_light"}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "add_light" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_ADDR: "[02:08:01:02]", + CONF_NAME: "Foyer Downlights", + CONF_RATE: 2.0, + }, + ) + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["data"] == { + "controller_id": "main_controller", + "dimmers": [ + {"addr": "[02:08:01:01]", "name": "Foyer Sconces", "rate": 1.0}, + {"addr": "[02:08:01:02]", "name": "Foyer Downlights", "rate": 2.0}, + ], + "host": "192.168.0.1", + "keypads": [ + { + "addr": "[02:08:02:01]", + "buttons": [ + { + "led": True, + "name": "Morning", + "number": 1, + "release_delay": None, + }, + {"led": True, "name": "Relax", "number": 2, "release_delay": None}, + {"led": False, "name": "Dim up", "number": 3, "release_delay": 0.2}, + ], + "name": "Foyer Keypad", + } + ], + "port": 1234, + } + + await hass.async_block_till_done() + + # Check the entry was updated with the new entity + assert hass.states.async_entity_ids("light") == unordered( + ["light.foyer_sconces", "light.foyer_downlights"] + ) + + # Now remove the original light + result = await hass.config_entries.options.async_init(mock_config_entry.entry_id) + assert result["type"] == FlowResultType.MENU + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + {"next_step_id": "remove_light"}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "remove_light" + assert result["data_schema"].schema["index"].options == { + "0": "Foyer Sconces ([02:08:01:01])", + "1": "Foyer Downlights ([02:08:01:02])", + } + + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={CONF_INDEX: ["0"]} + ) + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["data"] == { + "controller_id": "main_controller", + "dimmers": [ + {"addr": "[02:08:01:02]", "name": "Foyer Downlights", "rate": 2.0}, + ], + "host": "192.168.0.1", + "keypads": [ + { + "addr": "[02:08:02:01]", + "buttons": [ + { + "led": True, + "name": "Morning", + "number": 1, + "release_delay": None, + }, + {"led": True, "name": "Relax", "number": 2, "release_delay": None}, + {"led": False, "name": "Dim up", "number": 3, "release_delay": 0.2}, + ], + "name": "Foyer Keypad", + } + ], + "port": 1234, + } + + await hass.async_block_till_done() + + # Check the original entity was removed, with only the new entity left + assert hass.states.async_entity_ids("light") == unordered( + ["light.foyer_downlights"] + ) + + +async def test_options_add_remove_keypad_flow( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_homeworks: MagicMock +) -> None: + """Test options flow to add and remove a keypad.""" + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_init(mock_config_entry.entry_id) + assert result["type"] == FlowResultType.MENU + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + {"next_step_id": "add_keypad"}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "add_keypad" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_ADDR: "[02:08:03:01]", + CONF_NAME: "Hall Keypad", + }, + ) + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["data"] == { + "controller_id": "main_controller", + "dimmers": [ + {"addr": "[02:08:01:01]", "name": "Foyer Sconces", "rate": 1.0}, + ], + "host": "192.168.0.1", + "keypads": [ + { + "addr": "[02:08:02:01]", + "buttons": [ + { + "led": True, + "name": "Morning", + "number": 1, + "release_delay": None, + }, + {"led": True, "name": "Relax", "number": 2, "release_delay": None}, + {"led": False, "name": "Dim up", "number": 3, "release_delay": 0.2}, + ], + "name": "Foyer Keypad", + }, + {"addr": "[02:08:03:01]", "buttons": [], "name": "Hall Keypad"}, + ], + "port": 1234, + } + + await hass.async_block_till_done() + + # Now remove the original keypad + result = await hass.config_entries.options.async_init(mock_config_entry.entry_id) + assert result["type"] == FlowResultType.MENU + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + {"next_step_id": "remove_keypad"}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "remove_keypad" + assert result["data_schema"].schema["index"].options == { + "0": "Foyer Keypad ([02:08:02:01])", + "1": "Hall Keypad ([02:08:03:01])", + } + + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={CONF_INDEX: ["0"]} + ) + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["data"] == { + "controller_id": "main_controller", + "dimmers": [ + {"addr": "[02:08:01:01]", "name": "Foyer Sconces", "rate": 1.0}, + ], + "host": "192.168.0.1", + "keypads": [{"addr": "[02:08:03:01]", "buttons": [], "name": "Hall Keypad"}], + "port": 1234, + } + await hass.async_block_till_done() + + +async def test_options_add_keypad_with_error( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_homeworks: MagicMock +) -> None: + """Test options flow to add and remove a keypad.""" + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_init(mock_config_entry.entry_id) + assert result["type"] == FlowResultType.MENU + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + {"next_step_id": "add_keypad"}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "add_keypad" + + # Try an invalid address + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_ADDR: "[02:08:03:01", + CONF_NAME: "Hall Keypad", + }, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "add_keypad" + assert result["errors"] == {"base": "invalid_addr"} + + # Try an address claimed by another keypad + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_ADDR: "[02:08:02:01]", + CONF_NAME: "Hall Keypad", + }, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "add_keypad" + assert result["errors"] == {"base": "duplicated_addr"} + + # Try an address claimed by a light + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_ADDR: "[02:08:01:01]", + CONF_NAME: "Hall Keypad", + }, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "add_keypad" + assert result["errors"] == {"base": "duplicated_addr"} + + +async def test_options_edit_light_no_lights_flow( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_homeworks: MagicMock +) -> None: + """Test options flow to edit a light.""" + 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 hass.states.async_entity_ids("light") == unordered(["light.foyer_sconces"]) + + result = await hass.config_entries.options.async_init(mock_config_entry.entry_id) + assert result["type"] == FlowResultType.MENU + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], {"next_step_id": "select_edit_light"} + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "select_edit_light" + assert result["data_schema"].schema["index"].container == { + "0": "Foyer Sconces ([02:08:01:01])" + } + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + {"index": "0"}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "edit_light" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={CONF_RATE: 3.0} + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["data"] == { + "controller_id": "main_controller", + "dimmers": [{"addr": "[02:08:01:01]", "name": "Foyer Sconces", "rate": 3.0}], + "host": "192.168.0.1", + "keypads": [ + { + "addr": "[02:08:02:01]", + "buttons": [ + { + "led": True, + "name": "Morning", + "number": 1, + "release_delay": None, + }, + {"led": True, "name": "Relax", "number": 2, "release_delay": None}, + {"led": False, "name": "Dim up", "number": 3, "release_delay": 0.2}, + ], + "name": "Foyer Keypad", + } + ], + "port": 1234, + } + + await hass.async_block_till_done() + + # Check the entity was updated + assert len(hass.states.async_entity_ids("light")) == 1 + + +async def test_options_edit_light_flow_empty( + hass: HomeAssistant, + mock_empty_config_entry: MockConfigEntry, + mock_homeworks: MagicMock, +) -> None: + """Test options flow to edit a light.""" + mock_empty_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_empty_config_entry.entry_id) + await hass.async_block_till_done() + assert hass.states.async_entity_ids("light") == unordered([]) + + result = await hass.config_entries.options.async_init( + mock_empty_config_entry.entry_id + ) + assert result["type"] == FlowResultType.MENU + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], {"next_step_id": "select_edit_light"} + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "select_edit_light" + assert result["data_schema"].schema["index"].container == {} + + +async def test_options_add_button_flow( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_homeworks: MagicMock +) -> None: + """Test options flow to add a button.""" + 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 len(hass.states.async_entity_ids(BINARY_SENSOR_DOMAIN)) == 2 + assert len(hass.states.async_entity_ids(BUTTON_DOMAIN)) == 3 + + result = await hass.config_entries.options.async_init(mock_config_entry.entry_id) + assert result["type"] == FlowResultType.MENU + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], {"next_step_id": "select_edit_keypad"} + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "select_edit_keypad" + assert result["data_schema"].schema["index"].container == { + "0": "Foyer Keypad ([02:08:02:01])" + } + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + {"index": "0"}, + ) + assert result["type"] == FlowResultType.MENU + assert result["step_id"] == "edit_keypad" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], {"next_step_id": "add_button"} + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "add_button" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_NAME: "Dim down", + CONF_NUMBER: 4, + CONF_RELEASE_DELAY: 0.2, + CONF_LED: True, + }, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["data"] == { + "controller_id": "main_controller", + "dimmers": [{"addr": "[02:08:01:01]", "name": "Foyer Sconces", "rate": 1.0}], + "host": "192.168.0.1", + "keypads": [ + { + "addr": "[02:08:02:01]", + "buttons": [ + { + "led": True, + "name": "Morning", + "number": 1, + "release_delay": None, + }, + {"led": True, "name": "Relax", "number": 2, "release_delay": None}, + {"led": False, "name": "Dim up", "number": 3, "release_delay": 0.2}, + { + "led": True, + "name": "Dim down", + "number": 4, + "release_delay": 0.2, + }, + ], + "name": "Foyer Keypad", + } + ], + "port": 1234, + } + + await hass.async_block_till_done() + + # Check the new entities were added + assert len(hass.states.async_entity_ids(BINARY_SENSOR_DOMAIN)) == 3 + assert len(hass.states.async_entity_ids(BUTTON_DOMAIN)) == 4 + + +async def test_options_add_button_flow_duplicate( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_homeworks: MagicMock +) -> None: + """Test options flow to add a button.""" + 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 len(hass.states.async_entity_ids(BINARY_SENSOR_DOMAIN)) == 2 + assert len(hass.states.async_entity_ids(BUTTON_DOMAIN)) == 3 + + result = await hass.config_entries.options.async_init(mock_config_entry.entry_id) + assert result["type"] == FlowResultType.MENU + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], {"next_step_id": "select_edit_keypad"} + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "select_edit_keypad" + assert result["data_schema"].schema["index"].container == { + "0": "Foyer Keypad ([02:08:02:01])" + } + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + {"index": "0"}, + ) + assert result["type"] == FlowResultType.MENU + assert result["step_id"] == "edit_keypad" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], {"next_step_id": "add_button"} + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "add_button" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_NAME: "Dim down", + CONF_NUMBER: 1, + CONF_RELEASE_DELAY: 0.2, + }, + ) + + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {"base": "duplicated_number"} + + +async def test_options_edit_button_flow( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_homeworks: MagicMock +) -> None: + """Test options flow to add a button.""" + 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 len(hass.states.async_entity_ids(BINARY_SENSOR_DOMAIN)) == 2 + assert len(hass.states.async_entity_ids(BUTTON_DOMAIN)) == 3 + + result = await hass.config_entries.options.async_init(mock_config_entry.entry_id) + assert result["type"] == FlowResultType.MENU + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], {"next_step_id": "select_edit_keypad"} + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "select_edit_keypad" + assert result["data_schema"].schema["index"].container == { + "0": "Foyer Keypad ([02:08:02:01])" + } + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + {"index": "0"}, + ) + assert result["type"] == FlowResultType.MENU + assert result["step_id"] == "edit_keypad" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], {"next_step_id": "select_edit_button"} + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "select_edit_button" + assert result["data_schema"].schema["index"].container == { + "0": "Morning (1)", + "1": "Relax (2)", + "2": "Dim up (3)", + } + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + {"index": "0"}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "edit_button" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_RELEASE_DELAY: 0, + CONF_LED: False, + }, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["data"] == { + "controller_id": "main_controller", + "dimmers": [{"addr": "[02:08:01:01]", "name": "Foyer Sconces", "rate": 1.0}], + "host": "192.168.0.1", + "keypads": [ + { + "addr": "[02:08:02:01]", + "buttons": [ + { + "led": False, + "name": "Morning", + "number": 1, + "release_delay": 0.0, + }, + {"led": True, "name": "Relax", "number": 2, "release_delay": None}, + {"led": False, "name": "Dim up", "number": 3, "release_delay": 0.2}, + ], + "name": "Foyer Keypad", + } + ], + "port": 1234, + } + + await hass.async_block_till_done() + + # Check the new entities were added + assert len(hass.states.async_entity_ids(BINARY_SENSOR_DOMAIN)) == 2 + assert len(hass.states.async_entity_ids(BUTTON_DOMAIN)) == 3 + + +async def test_options_remove_button_flow( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_homeworks: MagicMock +) -> None: + """Test options flow to remove a button.""" + 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 len(hass.states.async_entity_ids(BUTTON_DOMAIN)) == 3 + + result = await hass.config_entries.options.async_init(mock_config_entry.entry_id) + assert result["type"] == FlowResultType.MENU + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], {"next_step_id": "select_edit_keypad"} + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "select_edit_keypad" + assert result["data_schema"].schema["index"].container == { + "0": "Foyer Keypad ([02:08:02:01])" + } + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + {"index": "0"}, + ) + assert result["type"] == FlowResultType.MENU + assert result["step_id"] == "edit_keypad" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], {"next_step_id": "remove_button"} + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "remove_button" + assert result["data_schema"].schema["index"].options == { + "0": "Morning (1)", + "1": "Relax (2)", + "2": "Dim up (3)", + } + + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={CONF_INDEX: ["0"]} + ) + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["data"] == { + "controller_id": "main_controller", + "dimmers": [{"addr": "[02:08:01:01]", "name": "Foyer Sconces", "rate": 1.0}], + "host": "192.168.0.1", + "keypads": [ + { + "addr": "[02:08:02:01]", + "buttons": [ + {"led": True, "name": "Relax", "number": 2, "release_delay": None}, + {"led": False, "name": "Dim up", "number": 3, "release_delay": 0.2}, + ], + "name": "Foyer Keypad", + } + ], + "port": 1234, + } + + await hass.async_block_till_done() + + # Check the entities were removed + assert len(hass.states.async_entity_ids(BINARY_SENSOR_DOMAIN)) == 1 + assert len(hass.states.async_entity_ids(BUTTON_DOMAIN)) == 2 diff --git a/tests/components/homeworks/test_init.py b/tests/components/homeworks/test_init.py new file mode 100644 index 00000000000..566e0b4beb4 --- /dev/null +++ b/tests/components/homeworks/test_init.py @@ -0,0 +1,116 @@ +"""Tests for the Lutron Homeworks Series 4 and 8 integration.""" + +from unittest.mock import ANY, MagicMock + +from pyhomeworks.pyhomeworks import HW_BUTTON_PRESSED, HW_BUTTON_RELEASED + +from homeassistant.components.homeworks import EVENT_BUTTON_PRESS, EVENT_BUTTON_RELEASE +from homeassistant.components.homeworks.const import CONF_DIMMERS, CONF_KEYPADS, DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry, async_capture_events + + +async def test_import( + hass: HomeAssistant, + mock_homeworks: MagicMock, +) -> None: + """Test the Homeworks YAML import.""" + await async_setup_component( + hass, + DOMAIN, + { + DOMAIN: { + CONF_HOST: "192.168.0.1", + CONF_PORT: 1234, + CONF_DIMMERS: [], + CONF_KEYPADS: [], + } + }, + ) + await hass.async_block_till_done() + + assert len(hass.config_entries.flow.async_progress()) == 1 + assert hass.config_entries.flow.async_progress()[0]["context"]["source"] == "import" + + +async def test_load_unload_config_entry( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_homeworks: MagicMock, +) -> None: + """Test the Homeworks configuration entry loading/unloading.""" + 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 + mock_homeworks.assert_called_once_with("192.168.0.1", 1234, ANY) + + 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 + + +async def test_config_entry_not_ready( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_homeworks: MagicMock, +) -> None: + """Test the Homeworks configuration entry not ready.""" + mock_homeworks.side_effect = ConnectionError + + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + mock_homeworks.assert_called_once() + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_keypad_events( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_homeworks: MagicMock, +) -> None: + """Test Homeworks keypad events.""" + release_events = async_capture_events(hass, EVENT_BUTTON_RELEASE) + press_events = async_capture_events(hass, EVENT_BUTTON_PRESS) + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + mock_homeworks.assert_called_once_with("192.168.0.1", 1234, ANY) + hw_callback = mock_homeworks.mock_calls[0][1][2] + + hw_callback(HW_BUTTON_PRESSED, ["[02:08:02:01]", 1]) + await hass.async_block_till_done() + assert len(press_events) == 1 + assert len(release_events) == 0 + assert press_events[0].data == { + "id": "foyer_keypad", + "name": "Foyer Keypad", + "button": 1, + } + assert press_events[0].event_type == "homeworks_button_press" + + hw_callback(HW_BUTTON_RELEASED, ["[02:08:02:01]", 1]) + await hass.async_block_till_done() + assert len(press_events) == 1 + assert len(release_events) == 1 + assert release_events[0].data == { + "id": "foyer_keypad", + "name": "Foyer Keypad", + "button": 1, + } + assert release_events[0].event_type == "homeworks_button_release" + + hw_callback("unsupported", ["[02:08:02:01]", 1]) + await hass.async_block_till_done() + assert len(press_events) == 1 + assert len(release_events) == 1 diff --git a/tests/components/homeworks/test_light.py b/tests/components/homeworks/test_light.py new file mode 100644 index 00000000000..a5d94f736d5 --- /dev/null +++ b/tests/components/homeworks/test_light.py @@ -0,0 +1,125 @@ +"""Tests for the Lutron Homeworks Series 4 and 8 light.""" + +from unittest.mock import ANY, MagicMock + +from pyhomeworks.pyhomeworks import HW_LIGHT_CHANGED +import pytest +from pytest_unordered import unordered +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.light import ATTR_BRIGHTNESS, DOMAIN as LIGHT_DOMAIN +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, +) +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def test_light_attributes_state_update( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_homeworks: MagicMock, + snapshot: SnapshotAssertion, +) -> None: + """Test Homeworks light state changes.""" + entity_id = "light.foyer_sconces" + mock_controller = MagicMock() + mock_homeworks.return_value = mock_controller + + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + mock_homeworks.assert_called_once_with("192.168.0.1", 1234, ANY) + hw_callback = mock_homeworks.mock_calls[0][1][2] + + assert len(mock_controller.request_dimmer_level.mock_calls) == 1 + assert mock_controller.request_dimmer_level.mock_calls[0][1] == ("[02:08:01:01]",) + + assert hass.states.async_entity_ids("light") == unordered([entity_id]) + + state = hass.states.get(entity_id) + assert state.state == STATE_OFF + assert state == snapshot + + hw_callback(HW_LIGHT_CHANGED, ["[02:08:01:01]", 50]) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == STATE_ON + assert state == snapshot + + +async def test_light_service_calls( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_homeworks: MagicMock, +) -> None: + """Test Homeworks light service call.""" + entity_id = "light.foyer_sconces" + mock_controller = MagicMock() + mock_homeworks.return_value = mock_controller + + 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 hass.states.async_entity_ids("light") == unordered([entity_id]) + + await hass.services.async_call( + LIGHT_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + mock_controller.fade_dim.assert_called_with(0.0, 1.0, 0, "[02:08:01:01]") + + # The light's brightness is unknown, turning it on should set it to max + await hass.services.async_call( + LIGHT_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + mock_controller.fade_dim.assert_called_with(100.0, 1.0, 0, "[02:08:01:01]") + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 127}, + blocking=True, + ) + mock_controller.fade_dim.assert_called_with( + pytest.approx(49.8, abs=0.1), 1.0, 0, "[02:08:01:01]" + ) + + +async def test_light_restore_brightness( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_homeworks: MagicMock, +) -> None: + """Test Homeworks light service call.""" + entity_id = "light.foyer_sconces" + mock_controller = MagicMock() + mock_homeworks.return_value = mock_controller + + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + mock_homeworks.assert_called_once_with("192.168.0.1", 1234, ANY) + hw_callback = mock_homeworks.mock_calls[0][1][2] + + assert hass.states.async_entity_ids("light") == unordered([entity_id]) + + hw_callback(HW_LIGHT_CHANGED, ["[02:08:01:01]", 50]) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == STATE_ON + assert state.attributes[ATTR_BRIGHTNESS] == 127 + + await hass.services.async_call( + LIGHT_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + mock_controller.fade_dim.assert_called_with( + pytest.approx(49.8, abs=0.1), 1.0, 0, "[02:08:01:01]" + ) diff --git a/tests/components/honeywell/__init__.py b/tests/components/honeywell/__init__.py index 6299097b104..98fcaa551bf 100644 --- a/tests/components/honeywell/__init__.py +++ b/tests/components/honeywell/__init__.py @@ -1,4 +1,5 @@ """Tests for honeywell component.""" + from unittest.mock import MagicMock from homeassistant.core import HomeAssistant diff --git a/tests/components/honeywell/test_climate.py b/tests/components/honeywell/test_climate.py index 743689da43d..751ba8aa288 100644 --- a/tests/components/honeywell/test_climate.py +++ b/tests/components/honeywell/test_climate.py @@ -1,4 +1,5 @@ """Test the Whirlpool Sixth Sense climate domain.""" + import datetime from unittest.mock import MagicMock diff --git a/tests/components/honeywell/test_config_flow.py b/tests/components/honeywell/test_config_flow.py index b76eb1bf1e4..a978a14daa1 100644 --- a/tests/components/honeywell/test_config_flow.py +++ b/tests/components/honeywell/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for honeywell config flow.""" + from unittest.mock import MagicMock, patch import aiosomecomfort diff --git a/tests/components/honeywell/test_diagnostics.py b/tests/components/honeywell/test_diagnostics.py index aafc50d5545..b180bf0e5bc 100644 --- a/tests/components/honeywell/test_diagnostics.py +++ b/tests/components/honeywell/test_diagnostics.py @@ -1,4 +1,5 @@ """Test Honeywell diagnostics.""" + from unittest.mock import MagicMock from syrupy import SnapshotAssertion diff --git a/tests/components/honeywell/test_init.py b/tests/components/honeywell/test_init.py index 8be7cfeb61a..d27428fcf65 100644 --- a/tests/components/honeywell/test_init.py +++ b/tests/components/honeywell/test_init.py @@ -1,4 +1,5 @@ """Test honeywell setup process.""" + from unittest.mock import MagicMock, create_autospec, patch import aiosomecomfort diff --git a/tests/components/honeywell/test_sensor.py b/tests/components/honeywell/test_sensor.py index b286132a40f..ed46fd4cdd2 100644 --- a/tests/components/honeywell/test_sensor.py +++ b/tests/components/honeywell/test_sensor.py @@ -1,4 +1,5 @@ """Test honeywell sensor.""" + from aiosomecomfort.device import Device from aiosomecomfort.location import Location import pytest @@ -19,9 +20,9 @@ async def test_outdoor_sensor( ) -> None: """Test outdoor temperature sensor.""" device_with_outdoor_sensor.temperature_unit = unit - location.devices_by_id[ - device_with_outdoor_sensor.deviceid - ] = device_with_outdoor_sensor + location.devices_by_id[device_with_outdoor_sensor.deviceid] = ( + device_with_outdoor_sensor + ) config_entry.add_to_hass(hass) await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/html5/test_notify.py b/tests/components/html5/test_notify.py index edaa6b56897..6763708cc38 100644 --- a/tests/components/html5/test_notify.py +++ b/tests/components/html5/test_notify.py @@ -1,4 +1,5 @@ """Test HTML5 notify platform.""" + from http import HTTPStatus import json from unittest.mock import MagicMock, mock_open, patch @@ -16,9 +17,11 @@ CONFIG_FILE = "file.conf" VAPID_CONF = { "platform": "html5", - "vapid_pub_key": "BJMA2gDZEkHaXRhf1fhY_" - + "QbKbhVIHlSJXI0bFyo0eJXnUPOjdgycCAbj-2bMKMKNKs" - + "_rM8JoSnyKGCXAY2dbONI", + "vapid_pub_key": ( + "BJMA2gDZEkHaXRhf1fhY_" + "QbKbhVIHlSJXI0bFyo0eJXnUPOjdgycCAbj-2bMKMKNKs" + "_rM8JoSnyKGCXAY2dbONI" + ), "vapid_prv_key": "ZwPgwKpESGuGLMZYU39vKgrekrWzCijo-LsBM3CZ9-c", "vapid_email": "someone@example.com", } diff --git a/tests/components/http/__init__.py b/tests/components/http/__init__.py index cd1d5916ab8..931af50cbc6 100644 --- a/tests/components/http/__init__.py +++ b/tests/components/http/__init__.py @@ -1,3 +1,4 @@ """Tests for the HTTP component.""" + # Relic from the past. Kept here so we can run negative tests. HTTP_HEADER_HA_AUTH = "X-HA-access" diff --git a/tests/components/http/conftest.py b/tests/components/http/conftest.py index ed2c78bafd7..60b1b73ff83 100644 --- a/tests/components/http/conftest.py +++ b/tests/components/http/conftest.py @@ -1,4 +1,5 @@ """Test configuration for http.""" + import pytest diff --git a/tests/components/http/test_auth.py b/tests/components/http/test_auth.py index ab56dca5580..de6f323bc8a 100644 --- a/tests/components/http/test_auth.py +++ b/tests/components/http/test_auth.py @@ -1,4 +1,5 @@ """The tests for the Home Assistant HTTP component.""" + from datetime import timedelta from http import HTTPStatus from ipaddress import ip_network @@ -17,6 +18,7 @@ from homeassistant.auth.providers.legacy_api_password import ( LegacyApiPasswordAuthProvider, ) from homeassistant.components import websocket_api +from homeassistant.components.http import KEY_HASS from homeassistant.components.http.auth import ( CONTENT_USER_NAME, DATA_SIGN_SECRET, @@ -78,7 +80,7 @@ async def get_legacy_user(auth): def app(hass): """Fixture to set up a web.Application.""" app = web.Application() - app["hass"] = hass + app[KEY_HASS] = hass app.router.add_get("/", mock_handler) async_setup_forwarded(app, True, []) return app @@ -88,7 +90,7 @@ def app(hass): def app2(hass): """Fixture to set up a web.Application without real_ip middleware.""" app = web.Application() - app["hass"] = hass + app[KEY_HASS] = hass app.router.add_get("/", mock_handler) return app diff --git a/tests/components/http/test_ban.py b/tests/components/http/test_ban.py index e38a9c97071..a10aa740268 100644 --- a/tests/components/http/test_ban.py +++ b/tests/components/http/test_ban.py @@ -1,4 +1,5 @@ """The tests for the Home Assistant HTTP component.""" + from http import HTTPStatus from ipaddress import ip_address import os @@ -10,12 +11,11 @@ from aiohttp.web_middlewares import middleware import pytest import homeassistant.components.http as http -from homeassistant.components.http import KEY_AUTHENTICATED +from homeassistant.components.http import KEY_AUTHENTICATED, KEY_HASS from homeassistant.components.http.ban import ( IP_BANS_FILE, KEY_BAN_MANAGER, KEY_FAILED_LOGIN_ATTEMPTS, - IpBanManager, process_success_login, setup_bans, ) @@ -30,16 +30,20 @@ from tests.typing import ClientSessionGenerator SUPERVISOR_IP = "1.2.3.4" BANNED_IPS = ["200.201.202.203", "100.64.0.2"] -BANNED_IPS_WITH_SUPERVISOR = BANNED_IPS + [SUPERVISOR_IP] +BANNED_IPS_WITH_SUPERVISOR = [*BANNED_IPS, SUPERVISOR_IP] @pytest.fixture(name="hassio_env") def hassio_env_fixture(): """Fixture to inject hassio env.""" - with patch.dict(os.environ, {"SUPERVISOR": "127.0.0.1"}), patch( - "homeassistant.components.hassio.HassIO.is_connected", - return_value={"result": "ok", "data": {}}, - ), patch.dict(os.environ, {"SUPERVISOR_TOKEN": "123456"}): + with ( + patch.dict(os.environ, {"SUPERVISOR": "127.0.0.1"}), + patch( + "homeassistant.components.hassio.HassIO.is_connected", + return_value={"result": "ok", "data": {}}, + ), + patch.dict(os.environ, {"SUPERVISOR_TOKEN": "123456"}), + ): yield @@ -58,7 +62,7 @@ async def test_access_from_banned_ip( ) -> None: """Test accessing to server from banned IP. Both trusted and not.""" app = web.Application() - app["hass"] = hass + app[KEY_HASS] = hass setup_bans(hass, app, 5) set_real_ip = mock_real_ip(app) @@ -87,7 +91,7 @@ async def test_access_from_banned_ip_with_partially_broken_yaml_file( still load the bans. """ app = web.Application() - app["hass"] = hass + app[KEY_HASS] = hass setup_bans(hass, app, 5) set_real_ip = mock_real_ip(app) @@ -118,7 +122,7 @@ async def test_no_ip_bans_file( ) -> None: """Test no ip bans file.""" app = web.Application() - app["hass"] = hass + app[KEY_HASS] = hass setup_bans(hass, app, 5) set_real_ip = mock_real_ip(app) @@ -138,7 +142,7 @@ async def test_failure_loading_ip_bans_file( ) -> None: """Test failure loading ip bans file.""" app = web.Application() - app["hass"] = hass + app[KEY_HASS] = hass setup_bans(hass, app, 5) set_real_ip = mock_real_ip(app) @@ -160,7 +164,7 @@ async def test_ip_ban_manager_never_started( ) -> None: """Test we handle the ip ban manager not being started.""" app = web.Application() - app["hass"] = hass + app[KEY_HASS] = hass setup_bans(hass, app, 5) set_real_ip = mock_real_ip(app) @@ -199,7 +203,7 @@ async def test_access_from_supervisor_ip( ) -> None: """Test accessing to server from supervisor IP.""" app = web.Application() - app["hass"] = hass + app[KEY_HASS] = hass async def unauth_handler(request): """Return a mock web response.""" @@ -215,7 +219,7 @@ async def test_access_from_supervisor_ip( ): client = await aiohttp_client(app) - manager: IpBanManager = app[KEY_BAN_MANAGER] + manager = app[KEY_BAN_MANAGER] with patch( "homeassistant.components.hassio.HassIO.get_resolution_info", @@ -231,8 +235,9 @@ async def test_access_from_supervisor_ip( m_open = mock_open() - with patch.dict(os.environ, {"SUPERVISOR": SUPERVISOR_IP}), patch( - "homeassistant.components.http.ban.open", m_open, create=True + with ( + patch.dict(os.environ, {"SUPERVISOR": SUPERVISOR_IP}), + patch("homeassistant.components.http.ban.open", m_open, create=True), ): resp = await client.get("/") assert resp.status == HTTPStatus.UNAUTHORIZED @@ -270,7 +275,7 @@ async def test_ip_bans_file_creation( ) -> None: """Testing if banned IP file created.""" app = web.Application() - app["hass"] = hass + app[KEY_HASS] = hass async def unauth_handler(request): """Return a mock web response.""" @@ -288,7 +293,7 @@ async def test_ip_bans_file_creation( ): client = await aiohttp_client(app) - manager: IpBanManager = app[KEY_BAN_MANAGER] + manager = app[KEY_BAN_MANAGER] m_open = mock_open() with patch("homeassistant.components.http.ban.open", m_open, create=True): @@ -326,7 +331,7 @@ async def test_failed_login_attempts_counter( ) -> None: """Testing if failed login attempts counter increased.""" app = web.Application() - app["hass"] = hass + app[KEY_HASS] = hass async def auth_handler(request): """Return 200 status code.""" @@ -398,7 +403,7 @@ async def test_single_ban_file_entry( ) -> None: """Test that only one item is added to ban file.""" app = web.Application() - app["hass"] = hass + app[KEY_HASS] = hass async def unauth_handler(request): """Return a mock web response.""" @@ -408,7 +413,7 @@ async def test_single_ban_file_entry( setup_bans(hass, app, 2) mock_real_ip(app)("200.201.202.204") - manager: IpBanManager = app[KEY_BAN_MANAGER] + manager = app[KEY_BAN_MANAGER] m_open = mock_open() with patch("homeassistant.components.http.ban.open", m_open, create=True): diff --git a/tests/components/http/test_cors.py b/tests/components/http/test_cors.py index 738579ea190..c4fd101f733 100644 --- a/tests/components/http/test_cors.py +++ b/tests/components/http/test_cors.py @@ -1,4 +1,5 @@ """Test cors for the HTTP component.""" + from http import HTTPStatus from pathlib import Path from unittest.mock import patch @@ -17,6 +18,7 @@ import pytest from homeassistant.components.http.cors import setup_cors from homeassistant.components.http.view import HomeAssistantView from homeassistant.core import HomeAssistant +from homeassistant.helpers.http import KEY_ALLOW_CONFIGRED_CORS from homeassistant.setup import async_setup_component from . import HTTP_HEADER_HA_AUTH @@ -56,7 +58,7 @@ def client(event_loop, aiohttp_client): """Fixture to set up a web.Application.""" app = web.Application() setup_cors(app, [TRUSTED_ORIGIN]) - app["allow_configured_cors"](app.router.add_get("/", mock_handler)) + app[KEY_ALLOW_CONFIGRED_CORS](app.router.add_get("/", mock_handler)) return event_loop.run_until_complete(aiohttp_client(app)) diff --git a/tests/components/http/test_data_validator.py b/tests/components/http/test_data_validator.py index ecff4370999..af55e0b8597 100644 --- a/tests/components/http/test_data_validator.py +++ b/tests/components/http/test_data_validator.py @@ -1,12 +1,14 @@ """Test data validator decorator.""" + from http import HTTPStatus from unittest.mock import Mock from aiohttp import web import voluptuous as vol -from homeassistant.components.http import HomeAssistantView +from homeassistant.components.http import KEY_HASS, HomeAssistantView from homeassistant.components.http.data_validator import RequestDataValidator +from homeassistant.helpers.http import KEY_ALLOW_CONFIGRED_CORS from tests.typing import ClientSessionGenerator @@ -14,8 +16,8 @@ from tests.typing import ClientSessionGenerator async def get_client(aiohttp_client, validator): """Generate a client that hits a view decorated with validator.""" app = web.Application() - app["hass"] = Mock(is_stopping=False) - app["allow_configured_cors"] = lambda _: None + app[KEY_HASS] = Mock(is_stopping=False) + app[KEY_ALLOW_CONFIGRED_CORS] = lambda _: None class TestView(HomeAssistantView): url = "/" @@ -27,7 +29,7 @@ async def get_client(aiohttp_client, validator): """Test method.""" return b"" - TestView().register(app["hass"], app, app.router) + TestView().register(app[KEY_HASS], app, app.router) client = await aiohttp_client(app) return client diff --git a/tests/components/http/test_forwarded.py b/tests/components/http/test_forwarded.py index 421dbaf2dfc..ce9b8198377 100644 --- a/tests/components/http/test_forwarded.py +++ b/tests/components/http/test_forwarded.py @@ -1,4 +1,5 @@ """Test real forwarded middleware.""" + from http import HTTPStatus from ipaddress import ip_network from unittest.mock import Mock, patch diff --git a/tests/components/http/test_headers.py b/tests/components/http/test_headers.py index 16b897b9f99..41c974b5239 100644 --- a/tests/components/http/test_headers.py +++ b/tests/components/http/test_headers.py @@ -1,4 +1,5 @@ """Test headers middleware.""" + from http import HTTPStatus from aiohttp import web diff --git a/tests/components/http/test_init.py b/tests/components/http/test_init.py index 97e39811cd8..98e97d0fe57 100644 --- a/tests/components/http/test_init.py +++ b/tests/components/http/test_init.py @@ -1,4 +1,5 @@ """The tests for the Home Assistant HTTP component.""" + import asyncio from datetime import timedelta from http import HTTPStatus @@ -14,6 +15,7 @@ from homeassistant.auth.providers.legacy_api_password import ( ) import homeassistant.components.http as http from homeassistant.core import HomeAssistant +from homeassistant.helpers.http import KEY_HASS from homeassistant.helpers.network import NoURLAvailableError from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util @@ -97,6 +99,15 @@ async def test_registering_view_while_running( hass.http.register_view(TestView) +async def test_homeassistant_assigned_to_app(hass: HomeAssistant) -> None: + """Test HomeAssistant instance is assigned to HomeAssistantApp.""" + assert await async_setup_component(hass, "api", {"http": {}}) + await hass.async_start() + assert hass.http.app[KEY_HASS] == hass + assert hass.http.app["hass"] == hass # For backwards compatibility + await hass.async_stop() + + async def test_not_log_password( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, @@ -162,10 +173,13 @@ async def test_ssl_profile_defaults_modern(hass: HomeAssistant, tmp_path: Path) _setup_empty_ssl_pem_files, tmp_path ) - with patch("ssl.SSLContext.load_cert_chain"), patch( - "homeassistant.util.ssl.server_context_modern", - side_effect=server_context_modern, - ) as mock_context: + with ( + patch("ssl.SSLContext.load_cert_chain"), + patch( + "homeassistant.util.ssl.server_context_modern", + side_effect=server_context_modern, + ) as mock_context, + ): assert ( await async_setup_component( hass, @@ -189,10 +203,13 @@ async def test_ssl_profile_change_intermediate( _setup_empty_ssl_pem_files, tmp_path ) - with patch("ssl.SSLContext.load_cert_chain"), patch( - "homeassistant.util.ssl.server_context_intermediate", - side_effect=server_context_intermediate, - ) as mock_context: + with ( + patch("ssl.SSLContext.load_cert_chain"), + patch( + "homeassistant.util.ssl.server_context_intermediate", + side_effect=server_context_intermediate, + ) as mock_context, + ): assert ( await async_setup_component( hass, @@ -220,10 +237,13 @@ async def test_ssl_profile_change_modern(hass: HomeAssistant, tmp_path: Path) -> _setup_empty_ssl_pem_files, tmp_path ) - with patch("ssl.SSLContext.load_cert_chain"), patch( - "homeassistant.util.ssl.server_context_modern", - side_effect=server_context_modern, - ) as mock_context: + with ( + patch("ssl.SSLContext.load_cert_chain"), + patch( + "homeassistant.util.ssl.server_context_modern", + side_effect=server_context_modern, + ) as mock_context, + ): assert ( await async_setup_component( hass, @@ -250,12 +270,14 @@ async def test_peer_cert(hass: HomeAssistant, tmp_path: Path) -> None: _setup_empty_ssl_pem_files, tmp_path ) - with patch("ssl.SSLContext.load_cert_chain"), patch( - "ssl.SSLContext.load_verify_locations" - ) as mock_load_verify_locations, patch( - "homeassistant.util.ssl.server_context_modern", - side_effect=server_context_modern, - ) as mock_context: + with ( + patch("ssl.SSLContext.load_cert_chain"), + patch("ssl.SSLContext.load_verify_locations") as mock_load_verify_locations, + patch( + "homeassistant.util.ssl.server_context_modern", + side_effect=server_context_modern, + ) as mock_context, + ): assert ( await async_setup_component( hass, @@ -459,7 +481,7 @@ async def test_storing_config( async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=200)) await hass.async_block_till_done() - restored = await hass.components.http.async_get_last_config() + restored = await http.async_get_last_config(hass) restored["trusted_proxies"][0] = ip_network(restored["trusted_proxies"][0]) assert restored == http.HTTP_SCHEMA(config) diff --git a/tests/components/http/test_request_context.py b/tests/components/http/test_request_context.py index 6e891be1799..076780a9685 100644 --- a/tests/components/http/test_request_context.py +++ b/tests/components/http/test_request_context.py @@ -1,4 +1,5 @@ """Test request context middleware.""" + from contextvars import ContextVar from http import HTTPStatus diff --git a/tests/components/http/test_security_filter.py b/tests/components/http/test_security_filter.py index 0cd85b48b06..c5b928b426f 100644 --- a/tests/components/http/test_security_filter.py +++ b/tests/components/http/test_security_filter.py @@ -1,4 +1,5 @@ """Test security filter middleware.""" + import asyncio from http import HTTPStatus diff --git a/tests/components/http/test_static.py b/tests/components/http/test_static.py index b11d54defd6..e3cf2f50c15 100644 --- a/tests/components/http/test_static.py +++ b/tests/components/http/test_static.py @@ -1,6 +1,5 @@ """The tests for http static files.""" - from pathlib import Path from aiohttp.test_utils import TestClient @@ -9,6 +8,7 @@ import pytest from homeassistant.components.http.static import CachingStaticResource, _get_file_path from homeassistant.core import EVENT_HOMEASSISTANT_START, HomeAssistant +from homeassistant.helpers.http import KEY_ALLOW_CONFIGRED_CORS from homeassistant.setup import async_setup_component from tests.typing import ClientSessionGenerator @@ -30,11 +30,11 @@ async def mock_http_client(hass: HomeAssistant, aiohttp_client: ClientSessionGen @pytest.mark.parametrize( ("url", "canonical_url"), - ( + [ ("//a", "//a"), ("///a", "///a"), ("/c:\\a\\b", "/c:%5Ca%5Cb"), - ), + ], ) async def test_static_path_blocks_anchors( hass: HomeAssistant, @@ -49,7 +49,7 @@ async def test_static_path_blocks_anchors( resource = CachingStaticResource(url, str(tmp_path)) assert resource.canonical == canonical_url app.router.register_resource(resource) - app["allow_configured_cors"](resource) + app[KEY_ALLOW_CONFIGRED_CORS](resource) resp = await mock_http_client.get(canonical_url, allow_redirects=False) assert resp.status == 403 diff --git a/tests/components/http/test_view.py b/tests/components/http/test_view.py index e52413d5225..e3bb3ac303b 100644 --- a/tests/components/http/test_view.py +++ b/tests/components/http/test_view.py @@ -1,4 +1,5 @@ """Tests for Home Assistant View.""" + from decimal import Decimal from http import HTTPStatus import json @@ -12,6 +13,7 @@ from aiohttp.web_exceptions import ( import pytest import voluptuous as vol +from homeassistant.components.http import KEY_HASS from homeassistant.components.http.view import ( HomeAssistantView, request_handler_factory, @@ -22,13 +24,13 @@ from homeassistant.exceptions import ServiceNotFound, Unauthorized @pytest.fixture def mock_request() -> Mock: """Mock a request.""" - return Mock(app={"hass": Mock(is_stopping=False)}, match_info={}) + return Mock(app={KEY_HASS: Mock(is_stopping=False)}, match_info={}) @pytest.fixture def mock_request_with_stopping() -> Mock: """Mock a request.""" - return Mock(app={"hass": Mock(is_stopping=True)}, match_info={}) + return Mock(app={KEY_HASS: Mock(is_stopping=True)}, match_info={}) async def test_invalid_json(caplog: pytest.LogCaptureFixture) -> None: @@ -52,7 +54,7 @@ async def test_handling_unauthorized(mock_request: Mock) -> None: """Test handling unauth exceptions.""" with pytest.raises(HTTPUnauthorized): await request_handler_factory( - mock_request.app["hass"], + mock_request.app[KEY_HASS], Mock(requires_auth=False), AsyncMock(side_effect=Unauthorized), )(mock_request) @@ -62,7 +64,7 @@ async def test_handling_invalid_data(mock_request: Mock) -> None: """Test handling unauth exceptions.""" with pytest.raises(HTTPBadRequest): await request_handler_factory( - mock_request.app["hass"], + mock_request.app[KEY_HASS], Mock(requires_auth=False), AsyncMock(side_effect=vol.Invalid("yo")), )(mock_request) @@ -72,7 +74,7 @@ async def test_handling_service_not_found(mock_request: Mock) -> None: """Test handling unauth exceptions.""" with pytest.raises(HTTPInternalServerError): await request_handler_factory( - mock_request.app["hass"], + mock_request.app[KEY_HASS], Mock(requires_auth=False), AsyncMock(side_effect=ServiceNotFound("test", "test")), )(mock_request) @@ -81,7 +83,7 @@ async def test_handling_service_not_found(mock_request: Mock) -> None: async def test_not_running(mock_request_with_stopping: Mock) -> None: """Test we get a 503 when not running.""" response = await request_handler_factory( - mock_request_with_stopping.app["hass"], + mock_request_with_stopping.app[KEY_HASS], Mock(requires_auth=False), AsyncMock(side_effect=Unauthorized), )(mock_request_with_stopping) @@ -92,7 +94,7 @@ async def test_invalid_handler(mock_request: Mock) -> None: """Test an invalid handler.""" with pytest.raises(TypeError): await request_handler_factory( - mock_request.app["hass"], + mock_request.app[KEY_HASS], Mock(requires_auth=False), AsyncMock(return_value=["not valid"]), )(mock_request) diff --git a/tests/components/huawei_lte/test_button.py b/tests/components/huawei_lte/test_button.py index 982fba166c3..c99c08c436c 100644 --- a/tests/components/huawei_lte/test_button.py +++ b/tests/components/huawei_lte/test_button.py @@ -1,4 +1,5 @@ """Tests for the Huawei LTE switches.""" + from unittest.mock import MagicMock, patch from huawei_lte_api.enums.device import ControlModeEnum diff --git a/tests/components/huawei_lte/test_config_flow.py b/tests/components/huawei_lte/test_config_flow.py index e358920b07b..f8ddaa42ac1 100644 --- a/tests/components/huawei_lte/test_config_flow.py +++ b/tests/components/huawei_lte/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for the Huawei LTE config flow.""" + from typing import Any from unittest.mock import patch from urllib.parse import urlparse, urlunparse @@ -101,7 +102,7 @@ async def test_already_configured( @pytest.mark.parametrize( ("exception", "errors", "data_patch"), - ( + [ (ConnectionError(), {CONF_URL: "unknown"}, {}), (requests.exceptions.SSLError(), {CONF_URL: "ssl_error_try_plain"}, {}), ( @@ -109,7 +110,7 @@ async def test_already_configured( {CONF_URL: "ssl_error_try_unverified"}, {CONF_VERIFY_SSL: True}, ), - ), + ], ) async def test_connection_errors( hass: HomeAssistant, @@ -157,7 +158,7 @@ def login_requests_mock(requests_mock): @pytest.mark.parametrize( ("request_outcome", "fixture_override", "errors"), - ( + [ ( { "text": f"{LoginErrorEnum.USERNAME_WRONG}", @@ -201,7 +202,7 @@ def login_requests_mock(requests_mock): {}, {CONF_URL: "connection_timeout"}, ), - ), + ], ) async def test_login_error( hass: HomeAssistant, login_requests_mock, request_outcome, fixture_override, errors @@ -223,7 +224,7 @@ async def test_login_error( assert result["errors"] == errors -@pytest.mark.parametrize("scheme", ("http", "https")) +@pytest.mark.parametrize("scheme", ["http", "https"]) async def test_success(hass: HomeAssistant, login_requests_mock, scheme: str) -> None: """Test successful flow provides entry creation data.""" user_input = { @@ -238,8 +239,9 @@ async def test_success(hass: HomeAssistant, login_requests_mock, scheme: str) -> f"{user_input[CONF_URL]}api/user/login", text="OK", ) - with patch("homeassistant.components.huawei_lte.async_setup"), patch( - "homeassistant.components.huawei_lte.async_setup_entry" + with ( + patch("homeassistant.components.huawei_lte.async_setup"), + patch("homeassistant.components.huawei_lte.async_setup_entry"), ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -256,7 +258,7 @@ async def test_success(hass: HomeAssistant, login_requests_mock, scheme: str) -> @pytest.mark.parametrize( ("requests_mock_request_kwargs", "upnp_data", "expected_result"), - ( + [ ( { "method": ANY, @@ -303,7 +305,7 @@ async def test_success(hass: HomeAssistant, login_requests_mock, scheme: str) -> "reason": "unsupported_device", }, ), - ), + ], ) async def test_ssdp( hass: HomeAssistant, @@ -345,7 +347,7 @@ async def test_ssdp( @pytest.mark.parametrize( ("login_response_text", "expected_result", "expected_entry_data"), - ( + [ ( "OK", { @@ -363,7 +365,7 @@ async def test_ssdp( }, {**FIXTURE_USER_INPUT, CONF_PASSWORD: "invalid-password"}, ), - ), + ], ) async def test_reauth( hass: HomeAssistant, diff --git a/tests/components/huawei_lte/test_device_tracker.py b/tests/components/huawei_lte/test_device_tracker.py index 094fd6e88b7..56eb594fca0 100644 --- a/tests/components/huawei_lte/test_device_tracker.py +++ b/tests/components/huawei_lte/test_device_tracker.py @@ -7,13 +7,13 @@ from homeassistant.components.huawei_lte import device_tracker @pytest.mark.parametrize( ("value", "expected"), - ( + [ ("HTTP", "http"), ("ID", "id"), ("IPAddress", "ip_address"), ("HTTPResponse", "http_response"), ("foo_bar", "foo_bar"), - ), + ], ) def test_better_snakecase(value, expected) -> None: """Test that better snakecase works better.""" diff --git a/tests/components/huawei_lte/test_select.py b/tests/components/huawei_lte/test_select.py index c3f6ded65b6..f6c8d34c4a0 100644 --- a/tests/components/huawei_lte/test_select.py +++ b/tests/components/huawei_lte/test_select.py @@ -1,4 +1,5 @@ """Tests for the Huawei LTE selects.""" + from unittest.mock import MagicMock, patch from huawei_lte_api.enums.net import LTEBandEnum, NetworkBandEnum, NetworkModeEnum diff --git a/tests/components/huawei_lte/test_sensor.py b/tests/components/huawei_lte/test_sensor.py index 74f8b7c7b49..4d5acaf2d31 100644 --- a/tests/components/huawei_lte/test_sensor.py +++ b/tests/components/huawei_lte/test_sensor.py @@ -11,11 +11,11 @@ from homeassistant.const import ( @pytest.mark.parametrize( ("value", "expected"), - ( + [ ("-71 dBm", (-71, SIGNAL_STRENGTH_DECIBELS_MILLIWATT)), ("15dB", (15, SIGNAL_STRENGTH_DECIBELS)), (">=-51dBm", (-51, SIGNAL_STRENGTH_DECIBELS_MILLIWATT)), - ), + ], ) def test_format_default(value, expected) -> None: """Test that default formatter copes with expected values.""" diff --git a/tests/components/huawei_lte/test_switches.py b/tests/components/huawei_lte/test_switches.py index acaffdbd0ba..288416c8c99 100644 --- a/tests/components/huawei_lte/test_switches.py +++ b/tests/components/huawei_lte/test_switches.py @@ -1,4 +1,5 @@ """Tests for the Huawei LTE switches.""" + from unittest.mock import MagicMock, patch from homeassistant.components.huawei_lte.const import DOMAIN diff --git a/tests/components/hue/conftest.py b/tests/components/hue/conftest.py index 3350ea15185..f87faf6294b 100644 --- a/tests/components/hue/conftest.py +++ b/tests/components/hue/conftest.py @@ -1,4 +1,5 @@ """Test helpers for Hue.""" + import asyncio from collections import deque import json diff --git a/tests/components/hue/test_binary_sensor.py b/tests/components/hue/test_binary_sensor.py index ab6f4ab0581..8f299a4b6a6 100644 --- a/tests/components/hue/test_binary_sensor.py +++ b/tests/components/hue/test_binary_sensor.py @@ -1,4 +1,5 @@ """Philips Hue binary_sensor platform tests for V2 bridge/api.""" + from homeassistant.core import HomeAssistant from .conftest import setup_platform diff --git a/tests/components/hue/test_bridge.py b/tests/components/hue/test_bridge.py index 28aa8626c42..5d103e47870 100644 --- a/tests/components/hue/test_bridge.py +++ b/tests/components/hue/test_bridge.py @@ -1,6 +1,7 @@ """Test Hue bridge.""" + import asyncio -from unittest.mock import AsyncMock, Mock, patch +from unittest.mock import patch from aiohttp import client_exceptions from aiohue.errors import Unauthorized @@ -12,47 +13,56 @@ from homeassistant.components.hue import bridge from homeassistant.components.hue.const import ( CONF_ALLOW_HUE_GROUPS, CONF_ALLOW_UNREACHABLE, + DOMAIN, ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady +from tests.common import MockConfigEntry + async def test_bridge_setup_v1(hass: HomeAssistant, mock_api_v1) -> None: """Test a successful setup for V1 bridge.""" - config_entry = Mock() - config_entry.data = {"host": "1.2.3.4", "api_key": "mock-api-key", "api_version": 1} - config_entry.options = {CONF_ALLOW_HUE_GROUPS: False, CONF_ALLOW_UNREACHABLE: False} + config_entry = MockConfigEntry( + domain=DOMAIN, + data={"host": "1.2.3.4", "api_key": "mock-api-key", "api_version": 1}, + options={CONF_ALLOW_HUE_GROUPS: False, CONF_ALLOW_UNREACHABLE: False}, + ) - with patch.object(bridge, "HueBridgeV1", return_value=mock_api_v1), patch.object( - hass.config_entries, "async_forward_entry_setup" - ) as mock_forward: + with ( + patch.object(bridge, "HueBridgeV1", return_value=mock_api_v1), + patch.object(hass.config_entries, "async_forward_entry_setups") as mock_forward, + ): hue_bridge = bridge.HueBridge(hass, config_entry) assert await hue_bridge.async_initialize_bridge() is True assert hue_bridge.api is mock_api_v1 assert isinstance(hue_bridge.api, HueBridgeV1) assert hue_bridge.api_version == 1 - assert len(mock_forward.mock_calls) == 3 - forward_entries = {c[1][1] for c in mock_forward.mock_calls} + assert len(mock_forward.mock_calls) == 1 + forward_entries = set(mock_forward.mock_calls[0][1][1]) assert forward_entries == {"light", "binary_sensor", "sensor"} async def test_bridge_setup_v2(hass: HomeAssistant, mock_api_v2) -> None: """Test a successful setup for V2 bridge.""" - config_entry = Mock() - config_entry.data = {"host": "1.2.3.4", "api_key": "mock-api-key", "api_version": 2} + config_entry = MockConfigEntry( + domain=DOMAIN, + data={"host": "1.2.3.4", "api_key": "mock-api-key", "api_version": 2}, + ) - with patch.object(bridge, "HueBridgeV2", return_value=mock_api_v2), patch.object( - hass.config_entries, "async_forward_entry_setup" - ) as mock_forward: + with ( + patch.object(bridge, "HueBridgeV2", return_value=mock_api_v2), + patch.object(hass.config_entries, "async_forward_entry_setups") as mock_forward, + ): hue_bridge = bridge.HueBridge(hass, config_entry) assert await hue_bridge.async_initialize_bridge() is True assert hue_bridge.api is mock_api_v2 assert isinstance(hue_bridge.api, HueBridgeV2) assert hue_bridge.api_version == 2 - assert len(mock_forward.mock_calls) == 6 - forward_entries = {c[1][1] for c in mock_forward.mock_calls} + assert len(mock_forward.mock_calls) == 1 + forward_entries = set(mock_forward.mock_calls[0][1][1]) assert forward_entries == { "light", "binary_sensor", @@ -65,14 +75,17 @@ async def test_bridge_setup_v2(hass: HomeAssistant, mock_api_v2) -> None: async def test_bridge_setup_invalid_api_key(hass: HomeAssistant) -> None: """Test we start config flow if username is no longer whitelisted.""" - entry = Mock() - entry.data = {"host": "1.2.3.4", "api_key": "mock-api-key", "api_version": 1} - entry.options = {CONF_ALLOW_HUE_GROUPS: False, CONF_ALLOW_UNREACHABLE: False} + entry = MockConfigEntry( + domain=DOMAIN, + data={"host": "1.2.3.4", "api_key": "mock-api-key", "api_version": 1}, + options={CONF_ALLOW_HUE_GROUPS: False, CONF_ALLOW_UNREACHABLE: False}, + ) hue_bridge = bridge.HueBridge(hass, entry) - with patch.object( - hue_bridge.api, "initialize", side_effect=Unauthorized - ), patch.object(hass.config_entries.flow, "async_init") as mock_init: + with ( + patch.object(hue_bridge.api, "initialize", side_effect=Unauthorized), + patch.object(hass.config_entries.flow, "async_init") as mock_init, + ): assert await hue_bridge.async_initialize_bridge() is False assert len(mock_init.mock_calls) == 1 @@ -81,35 +94,43 @@ async def test_bridge_setup_invalid_api_key(hass: HomeAssistant) -> None: async def test_bridge_setup_timeout(hass: HomeAssistant) -> None: """Test we retry to connect if we cannot connect.""" - entry = Mock() - entry.data = {"host": "1.2.3.4", "api_key": "mock-api-key", "api_version": 1} - entry.options = {CONF_ALLOW_HUE_GROUPS: False, CONF_ALLOW_UNREACHABLE: False} + entry = MockConfigEntry( + domain=DOMAIN, + data={"host": "1.2.3.4", "api_key": "mock-api-key", "api_version": 1}, + options={CONF_ALLOW_HUE_GROUPS: False, CONF_ALLOW_UNREACHABLE: False}, + ) hue_bridge = bridge.HueBridge(hass, entry) - with patch.object( - hue_bridge.api, - "initialize", - side_effect=client_exceptions.ServerDisconnectedError, - ), pytest.raises(ConfigEntryNotReady): + with ( + patch.object( + hue_bridge.api, + "initialize", + side_effect=client_exceptions.ServerDisconnectedError, + ), + pytest.raises(ConfigEntryNotReady), + ): await hue_bridge.async_initialize_bridge() async def test_reset_unloads_entry_if_setup(hass: HomeAssistant, mock_api_v1) -> None: """Test calling reset while the entry has been setup.""" - config_entry = Mock() - config_entry.data = {"host": "1.2.3.4", "api_key": "mock-api-key", "api_version": 1} - config_entry.options = {CONF_ALLOW_HUE_GROUPS: False, CONF_ALLOW_UNREACHABLE: False} + config_entry = MockConfigEntry( + domain=DOMAIN, + data={"host": "1.2.3.4", "api_key": "mock-api-key", "api_version": 1}, + options={CONF_ALLOW_HUE_GROUPS: False, CONF_ALLOW_UNREACHABLE: False}, + ) - with patch.object(bridge, "HueBridgeV1", return_value=mock_api_v1), patch.object( - hass.config_entries, "async_forward_entry_setup" - ) as mock_forward: + with ( + patch.object(bridge, "HueBridgeV1", return_value=mock_api_v1), + patch.object(hass.config_entries, "async_forward_entry_setups") as mock_forward, + ): hue_bridge = bridge.HueBridge(hass, config_entry) assert await hue_bridge.async_initialize_bridge() is True await asyncio.sleep(0) assert len(hass.services.async_services()) == 0 - assert len(mock_forward.mock_calls) == 3 + assert len(mock_forward.mock_calls) == 1 with patch.object( hass.config_entries, "async_forward_entry_unload", return_value=True @@ -122,9 +143,11 @@ async def test_reset_unloads_entry_if_setup(hass: HomeAssistant, mock_api_v1) -> async def test_handle_unauthorized(hass: HomeAssistant, mock_api_v1) -> None: """Test handling an unauthorized error on update.""" - config_entry = Mock(async_setup=AsyncMock()) - config_entry.data = {"host": "1.2.3.4", "api_key": "mock-api-key", "api_version": 1} - config_entry.options = {CONF_ALLOW_HUE_GROUPS: False, CONF_ALLOW_UNREACHABLE: False} + config_entry = MockConfigEntry( + domain=DOMAIN, + data={"host": "1.2.3.4", "api_key": "mock-api-key", "api_version": 1}, + options={CONF_ALLOW_HUE_GROUPS: False, CONF_ALLOW_UNREACHABLE: False}, + ) with patch.object(bridge, "HueBridgeV1", return_value=mock_api_v1): hue_bridge = bridge.HueBridge(hass, config_entry) diff --git a/tests/components/hue/test_config_flow.py b/tests/components/hue/test_config_flow.py index 30d2a8c0b42..74cceb03aba 100644 --- a/tests/components/hue/test_config_flow.py +++ b/tests/components/hue/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for Philips Hue config flow.""" + from ipaddress import ip_address from unittest.mock import Mock, patch @@ -125,8 +126,9 @@ async def test_manual_flow_works(hass: HomeAssistant) -> None: assert result["type"] == "form" assert result["step_id"] == "link" - with patch.object(config_flow, "create_app_key", return_value="123456789"), patch( - "homeassistant.components.hue.async_unload_entry", return_value=True + with ( + patch.object(config_flow, "create_app_key", return_value="123456789"), + patch("homeassistant.components.hue.async_unload_entry", return_value=True), ): result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) @@ -259,8 +261,8 @@ async def test_flow_timeout_discovery(hass: HomeAssistant) -> None: const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "abort" - assert result["reason"] == "discover_timeout" + assert result["type"] == "form" + assert result["step_id"] == "manual" async def test_flow_link_unknown_error(hass: HomeAssistant) -> None: @@ -386,10 +388,13 @@ async def test_creating_entry_removes_entries_for_same_host_or_bridge( assert result["type"] == "form" assert result["step_id"] == "link" - with patch( - "homeassistant.components.hue.config_flow.create_app_key", - return_value="123456789", - ), patch("homeassistant.components.hue.async_unload_entry", return_value=True): + with ( + patch( + "homeassistant.components.hue.config_flow.create_app_key", + return_value="123456789", + ), + patch("homeassistant.components.hue.async_unload_entry", return_value=True), + ): result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) assert result["type"] == "create_entry" diff --git a/tests/components/hue/test_device_trigger_v1.py b/tests/components/hue/test_device_trigger_v1.py index 3be150f0269..b12c3cce584 100644 --- a/tests/components/hue/test_device_trigger_v1.py +++ b/tests/components/hue/test_device_trigger_v1.py @@ -1,4 +1,5 @@ """The tests for Philips Hue device triggers for V1 bridge.""" + from pytest_unordered import unordered from homeassistant.components import automation, hue diff --git a/tests/components/hue/test_device_trigger_v2.py b/tests/components/hue/test_device_trigger_v2.py index e79fce7ab13..0a89b3263c7 100644 --- a/tests/components/hue/test_device_trigger_v2.py +++ b/tests/components/hue/test_device_trigger_v2.py @@ -1,4 +1,5 @@ """The tests for Philips Hue device triggers for V2 bridge.""" + from aiohue.v2.models.button import ButtonEvent from pytest_unordered import unordered diff --git a/tests/components/hue/test_diagnostics.py b/tests/components/hue/test_diagnostics.py index 766d3fe321c..7e64ba1ad93 100644 --- a/tests/components/hue/test_diagnostics.py +++ b/tests/components/hue/test_diagnostics.py @@ -1,4 +1,5 @@ """Test Hue diagnostics.""" + from homeassistant.core import HomeAssistant from .conftest import setup_platform diff --git a/tests/components/hue/test_event.py b/tests/components/hue/test_event.py index 9953bb11796..b33509543e9 100644 --- a/tests/components/hue/test_event.py +++ b/tests/components/hue/test_event.py @@ -1,4 +1,5 @@ """Philips Hue Event platform tests for V2 bridge/api.""" + from homeassistant.components.event import ATTR_EVENT_TYPE, ATTR_EVENT_TYPES from homeassistant.core import HomeAssistant diff --git a/tests/components/hue/test_init.py b/tests/components/hue/test_init.py index bdca6ee135c..5ce0d78ead9 100644 --- a/tests/components/hue/test_init.py +++ b/tests/components/hue/test_init.py @@ -1,4 +1,5 @@ """Test Hue setup process.""" + from unittest.mock import AsyncMock, Mock, patch import aiohue.v2 as aiohue_v2 @@ -147,15 +148,18 @@ async def test_security_vuln_check(hass: HomeAssistant) -> None: ) config.name = "Hue" - with patch.object(hue.migration, "is_v2_bridge", return_value=False), patch.object( - hue, - "HueBridge", - Mock( - return_value=Mock( - async_initialize_bridge=AsyncMock(return_value=True), - api=Mock(config=config), - api_version=1, - ) + with ( + patch.object(hue.migration, "is_v2_bridge", return_value=False), + patch.object( + hue, + "HueBridge", + Mock( + return_value=Mock( + async_initialize_bridge=AsyncMock(return_value=True), + api=Mock(config=config), + api_version=1, + ) + ), ), ): assert await async_setup_component(hass, "hue", {}) diff --git a/tests/components/hue/test_light_v1.py b/tests/components/hue/test_light_v1.py index 2e67eb6d0e1..9a74d9cd994 100644 --- a/tests/components/hue/test_light_v1.py +++ b/tests/components/hue/test_light_v1.py @@ -1,4 +1,5 @@ """Philips Hue lights platform tests.""" + from unittest.mock import Mock import aiohue @@ -412,6 +413,8 @@ async def test_group_removed(hass: HomeAssistant, mock_bridge_v1) -> None: await hass.services.async_call( "light", "turn_on", {"entity_id": "light.group_1"}, blocking=True ) + # Wait for the group to be updated + await hass.async_block_till_done() # 2x group update, 1x light update, 1 turn on request assert len(mock_bridge_v1.mock_requests) == 4 @@ -439,6 +442,8 @@ async def test_light_removed(hass: HomeAssistant, mock_bridge_v1) -> None: await hass.services.async_call( "light", "turn_on", {"entity_id": "light.hue_lamp_1"}, blocking=True ) + # Wait for the light to be updated + await hass.async_block_till_done() # 2x light update, 1 group update, 1 turn on request assert len(mock_bridge_v1.mock_requests) == 4 diff --git a/tests/components/hue/test_light_v2.py b/tests/components/hue/test_light_v2.py index 0c79933a246..d8d0f4b6e66 100644 --- a/tests/components/hue/test_light_v2.py +++ b/tests/components/hue/test_light_v2.py @@ -1,4 +1,5 @@ """Philips Hue lights platform tests for V2 bridge/api.""" + from homeassistant.components.light import ColorMode from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -204,6 +205,28 @@ async def test_light_turn_on_service( ) assert mock_bridge_v2.mock_requests[8]["json"]["timed_effects"]["duration"] == 6000 + # test enabling effect should ignore color temperature + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": test_light_id, "effect": "candle", "color_temp": 500}, + blocking=True, + ) + assert len(mock_bridge_v2.mock_requests) == 10 + assert mock_bridge_v2.mock_requests[9]["json"]["effects"]["effect"] == "candle" + assert "color_temperature" not in mock_bridge_v2.mock_requests[9]["json"] + + # test enabling effect should ignore xy color + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": test_light_id, "effect": "candle", "xy_color": [0.123, 0.123]}, + blocking=True, + ) + assert len(mock_bridge_v2.mock_requests) == 11 + assert mock_bridge_v2.mock_requests[10]["json"]["effects"]["effect"] == "candle" + assert "xy_color" not in mock_bridge_v2.mock_requests[9]["json"] + async def test_light_turn_off_service( hass: HomeAssistant, mock_bridge_v2, v2_resources_test_data @@ -527,7 +550,7 @@ async def test_grouped_lights( # PUT request should have been sent to ALL group lights with correct params assert len(mock_bridge_v2.mock_requests) == 3 - for index in range(0, 3): + for index in range(3): assert ( mock_bridge_v2.mock_requests[index]["json"]["identify"]["action"] == "identify" @@ -565,7 +588,7 @@ async def test_grouped_lights( # PUT request should have been sent to ALL group lights with correct params assert len(mock_bridge_v2.mock_requests) == 3 - for index in range(0, 3): + for index in range(3): assert ( mock_bridge_v2.mock_requests[index]["json"]["identify"]["action"] == "identify" diff --git a/tests/components/hue/test_migration.py b/tests/components/hue/test_migration.py index 0a6c24a5756..adcc582a314 100644 --- a/tests/components/hue/test_migration.py +++ b/tests/components/hue/test_migration.py @@ -1,4 +1,5 @@ """Test Hue migration logic.""" + from unittest.mock import patch from homeassistant.components import hue @@ -32,9 +33,10 @@ async def test_auto_switchover(hass: HomeAssistant) -> None: ) config_entry.add_to_hass(hass) - with patch.object(hue.migration, "is_v2_bridge", retun_value=True), patch.object( - hue.migration, "handle_v2_migration" - ) as mock_mig: + with ( + patch.object(hue.migration, "is_v2_bridge", retun_value=True), + patch.object(hue.migration, "handle_v2_migration") as mock_mig, + ): await hue.migration.check_migration(hass, config_entry) assert len(mock_mig.mock_calls) == 1 # the api version should now be version 2 diff --git a/tests/components/hue/test_scene.py b/tests/components/hue/test_scene.py index ad2d11ff6b6..5e2fd939087 100644 --- a/tests/components/hue/test_scene.py +++ b/tests/components/hue/test_scene.py @@ -1,4 +1,5 @@ """Philips Hue scene platform tests for V2 bridge/api.""" + from homeassistant.const import STATE_UNKNOWN from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er diff --git a/tests/components/hue/test_sensor_v1.py b/tests/components/hue/test_sensor_v1.py index df8c45119df..6e620ded365 100644 --- a/tests/components/hue/test_sensor_v1.py +++ b/tests/components/hue/test_sensor_v1.py @@ -1,4 +1,5 @@ """Philips Hue sensors platform tests.""" + from unittest.mock import Mock import aiohue diff --git a/tests/components/hue/test_sensor_v2.py b/tests/components/hue/test_sensor_v2.py index b8793c99d6c..4c1f8defc95 100644 --- a/tests/components/hue/test_sensor_v2.py +++ b/tests/components/hue/test_sensor_v2.py @@ -1,4 +1,5 @@ """Philips Hue sensor platform tests for V2 bridge/api.""" + from homeassistant.components import hue from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -85,7 +86,7 @@ async def test_enable_sensor( # enable the entity updated_entry = entity_registry.async_update_entity( - entity_entry.entity_id, **{"disabled_by": None} + entity_entry.entity_id, disabled_by=None ) assert updated_entry != entity_entry assert updated_entry.disabled is False diff --git a/tests/components/hue/test_services.py b/tests/components/hue/test_services.py index ec1c1154d75..8139bfa034c 100644 --- a/tests/components/hue/test_services.py +++ b/tests/components/hue/test_services.py @@ -1,4 +1,5 @@ """Test Hue services.""" + from unittest.mock import patch from homeassistant import config_entries @@ -61,8 +62,9 @@ async def test_hue_activate_scene(hass: HomeAssistant, mock_api_v1) -> None: mock_api_v1.mock_group_responses.append(GROUP_RESPONSE) mock_api_v1.mock_scene_responses.append(SCENE_RESPONSE) - with patch.object(bridge, "HueBridgeV1", return_value=mock_api_v1), patch.object( - hass.config_entries, "async_forward_entry_setup" + with ( + patch.object(bridge, "HueBridgeV1", return_value=mock_api_v1), + patch.object(hass.config_entries, "async_forward_entry_setups"), ): hue_bridge = bridge.HueBridge(hass, config_entry) assert await hue_bridge.async_initialize_bridge() is True @@ -98,8 +100,9 @@ async def test_hue_activate_scene_transition(hass: HomeAssistant, mock_api_v1) - mock_api_v1.mock_group_responses.append(GROUP_RESPONSE) mock_api_v1.mock_scene_responses.append(SCENE_RESPONSE) - with patch.object(bridge, "HueBridgeV1", return_value=mock_api_v1), patch.object( - hass.config_entries, "async_forward_entry_setup" + with ( + patch.object(bridge, "HueBridgeV1", return_value=mock_api_v1), + patch.object(hass.config_entries, "async_forward_entry_setups"), ): hue_bridge = bridge.HueBridge(hass, config_entry) assert await hue_bridge.async_initialize_bridge() is True @@ -137,8 +140,9 @@ async def test_hue_activate_scene_group_not_found( mock_api_v1.mock_group_responses.append({}) mock_api_v1.mock_scene_responses.append(SCENE_RESPONSE) - with patch.object(bridge, "HueBridgeV1", return_value=mock_api_v1), patch.object( - hass.config_entries, "async_forward_entry_setup" + with ( + patch.object(bridge, "HueBridgeV1", return_value=mock_api_v1), + patch.object(hass.config_entries, "async_forward_entry_setups"), ): hue_bridge = bridge.HueBridge(hass, config_entry) assert await hue_bridge.async_initialize_bridge() is True @@ -171,8 +175,9 @@ async def test_hue_activate_scene_scene_not_found( mock_api_v1.mock_group_responses.append(GROUP_RESPONSE) mock_api_v1.mock_scene_responses.append({}) - with patch.object(bridge, "HueBridgeV1", return_value=mock_api_v1), patch.object( - hass.config_entries, "async_forward_entry_setup" + with ( + patch.object(bridge, "HueBridgeV1", return_value=mock_api_v1), + patch.object(hass.config_entries, "async_forward_entry_setups"), ): hue_bridge = bridge.HueBridge(hass, config_entry) assert await hue_bridge.async_initialize_bridge() is True diff --git a/tests/components/hue/test_switch.py b/tests/components/hue/test_switch.py index c3384ae1e44..2e25dd715c1 100644 --- a/tests/components/hue/test_switch.py +++ b/tests/components/hue/test_switch.py @@ -1,4 +1,5 @@ """Philips Hue switch platform tests for V2 bridge/api.""" + from homeassistant.core import HomeAssistant from .conftest import setup_platform diff --git a/tests/components/huisbaasje/test_config_flow.py b/tests/components/huisbaasje/test_config_flow.py index 87b605473f4..0c65d425d4d 100644 --- a/tests/components/huisbaasje/test_config_flow.py +++ b/tests/components/huisbaasje/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Huisbaasje config flow.""" + from unittest.mock import patch from energyflip import ( @@ -23,17 +24,22 @@ async def test_form(hass: HomeAssistant) -> None: assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} - with patch( - "energyflip.EnergyFlip.authenticate", return_value=None - ) as mock_authenticate, patch( - "energyflip.EnergyFlip.customer_overview", return_value=None - ) as mock_customer_overview, patch( - "energyflip.EnergyFlip.get_user_id", - return_value="test-id", - ) as mock_get_user_id, patch( - "homeassistant.components.huisbaasje.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "energyflip.EnergyFlip.authenticate", return_value=None + ) as mock_authenticate, + patch( + "energyflip.EnergyFlip.customer_overview", return_value=None + ) as mock_customer_overview, + patch( + "energyflip.EnergyFlip.get_user_id", + return_value="test-id", + ) as mock_get_user_id, + patch( + "homeassistant.components.huisbaasje.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): form_result = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -128,9 +134,12 @@ async def test_form_customer_overview_cannot_connect(hass: HomeAssistant) -> Non DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch("energyflip.EnergyFlip.authenticate", return_value=None), patch( - "energyflip.EnergyFlip.customer_overview", - side_effect=EnergyFlipConnectionException, + with ( + patch("energyflip.EnergyFlip.authenticate", return_value=None), + patch( + "energyflip.EnergyFlip.customer_overview", + side_effect=EnergyFlipConnectionException, + ), ): form_result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -150,9 +159,12 @@ async def test_form_customer_overview_authentication_error(hass: HomeAssistant) DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch("energyflip.EnergyFlip.authenticate", return_value=None), patch( - "energyflip.EnergyFlip.customer_overview", - side_effect=EnergyFlipUnauthenticatedException, + with ( + patch("energyflip.EnergyFlip.authenticate", return_value=None), + patch( + "energyflip.EnergyFlip.customer_overview", + side_effect=EnergyFlipUnauthenticatedException, + ), ): form_result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -172,9 +184,12 @@ async def test_form_customer_overview_unknown_error(hass: HomeAssistant) -> None DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch("energyflip.EnergyFlip.authenticate", return_value=None), patch( - "energyflip.EnergyFlip.customer_overview", - side_effect=Exception, + with ( + patch("energyflip.EnergyFlip.authenticate", return_value=None), + patch( + "energyflip.EnergyFlip.customer_overview", + side_effect=Exception, + ), ): form_result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -205,14 +220,17 @@ async def test_form_entry_exists(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch("energyflip.EnergyFlip.authenticate", return_value=None), patch( - "energyflip.EnergyFlip.customer_overview", return_value=None - ), patch( - "energyflip.EnergyFlip.get_user_id", - return_value="test-id", - ), patch( - "homeassistant.components.huisbaasje.async_setup_entry", - return_value=True, + with ( + patch("energyflip.EnergyFlip.authenticate", return_value=None), + patch("energyflip.EnergyFlip.customer_overview", return_value=None), + patch( + "energyflip.EnergyFlip.get_user_id", + return_value="test-id", + ), + patch( + "homeassistant.components.huisbaasje.async_setup_entry", + return_value=True, + ), ): form_result = await hass.config_entries.flow.async_configure( result["flow_id"], diff --git a/tests/components/huisbaasje/test_data.py b/tests/components/huisbaasje/test_data.py index e14976443f3..181dcd7640e 100644 --- a/tests/components/huisbaasje/test_data.py +++ b/tests/components/huisbaasje/test_data.py @@ -1,4 +1,5 @@ """Test data for the tests of the Huisbaasje integration.""" + MOCK_CURRENT_MEASUREMENTS = { "electricity": { "measurement": { diff --git a/tests/components/huisbaasje/test_init.py b/tests/components/huisbaasje/test_init.py index 2047be40367..5f1bcb0094d 100644 --- a/tests/components/huisbaasje/test_init.py +++ b/tests/components/huisbaasje/test_init.py @@ -1,4 +1,5 @@ """Test cases for the initialisation of the Huisbaasje integration.""" + from unittest.mock import patch from energyflip import EnergyFlipException @@ -23,14 +24,18 @@ async def test_setup(hass: HomeAssistant) -> None: async def test_setup_entry(hass: HomeAssistant) -> None: """Test for successfully setting a config entry.""" - with patch( - "energyflip.EnergyFlip.authenticate", return_value=None - ) as mock_authenticate, patch( - "energyflip.EnergyFlip.is_authenticated", return_value=True - ) as mock_is_authenticated, patch( - "energyflip.EnergyFlip.current_measurements", - return_value=MOCK_CURRENT_MEASUREMENTS, - ) as mock_current_measurements: + with ( + patch( + "energyflip.EnergyFlip.authenticate", return_value=None + ) as mock_authenticate, + patch( + "energyflip.EnergyFlip.is_authenticated", return_value=True + ) as mock_is_authenticated, + patch( + "energyflip.EnergyFlip.current_measurements", + return_value=MOCK_CURRENT_MEASUREMENTS, + ) as mock_current_measurements, + ): hass.config.components.add(huisbaasje.DOMAIN) config_entry = MockConfigEntry( version=1, @@ -102,14 +107,18 @@ async def test_setup_entry_error(hass: HomeAssistant) -> None: async def test_unload_entry(hass: HomeAssistant) -> None: """Test for successfully unloading the config entry.""" - with patch( - "energyflip.EnergyFlip.authenticate", return_value=None - ) as mock_authenticate, patch( - "energyflip.EnergyFlip.is_authenticated", return_value=True - ) as mock_is_authenticated, patch( - "energyflip.EnergyFlip.current_measurements", - return_value=MOCK_CURRENT_MEASUREMENTS, - ) as mock_current_measurements: + with ( + patch( + "energyflip.EnergyFlip.authenticate", return_value=None + ) as mock_authenticate, + patch( + "energyflip.EnergyFlip.is_authenticated", return_value=True + ) as mock_is_authenticated, + patch( + "energyflip.EnergyFlip.current_measurements", + return_value=MOCK_CURRENT_MEASUREMENTS, + ) as mock_current_measurements, + ): hass.config.components.add(huisbaasje.DOMAIN) config_entry = MockConfigEntry( version=1, diff --git a/tests/components/huisbaasje/test_sensor.py b/tests/components/huisbaasje/test_sensor.py index 570c5656051..02a05c78763 100644 --- a/tests/components/huisbaasje/test_sensor.py +++ b/tests/components/huisbaasje/test_sensor.py @@ -1,4 +1,5 @@ """Test cases for the sensors of the Huisbaasje integration.""" + from unittest.mock import patch from homeassistant.components import huisbaasje @@ -27,14 +28,18 @@ from tests.common import MockConfigEntry async def test_setup_entry(hass: HomeAssistant) -> None: """Test for successfully loading sensor states.""" - with patch( - "energyflip.EnergyFlip.authenticate", return_value=None - ) as mock_authenticate, patch( - "energyflip.EnergyFlip.is_authenticated", return_value=True - ) as mock_is_authenticated, patch( - "energyflip.EnergyFlip.current_measurements", - return_value=MOCK_CURRENT_MEASUREMENTS, - ) as mock_current_measurements: + with ( + patch( + "energyflip.EnergyFlip.authenticate", return_value=None + ) as mock_authenticate, + patch( + "energyflip.EnergyFlip.is_authenticated", return_value=True + ) as mock_is_authenticated, + patch( + "energyflip.EnergyFlip.current_measurements", + return_value=MOCK_CURRENT_MEASUREMENTS, + ) as mock_current_measurements, + ): hass.config.components.add(huisbaasje.DOMAIN) config_entry = MockConfigEntry( version=1, @@ -314,14 +319,18 @@ async def test_setup_entry(hass: HomeAssistant) -> None: async def test_setup_entry_absent_measurement(hass: HomeAssistant) -> None: """Test for successfully loading sensor states when response does not contain all measurements.""" - with patch( - "energyflip.EnergyFlip.authenticate", return_value=None - ) as mock_authenticate, patch( - "energyflip.EnergyFlip.is_authenticated", return_value=True - ) as mock_is_authenticated, patch( - "energyflip.EnergyFlip.current_measurements", - return_value=MOCK_LIMITED_CURRENT_MEASUREMENTS, - ) as mock_current_measurements: + with ( + patch( + "energyflip.EnergyFlip.authenticate", return_value=None + ) as mock_authenticate, + patch( + "energyflip.EnergyFlip.is_authenticated", return_value=True + ) as mock_is_authenticated, + patch( + "energyflip.EnergyFlip.current_measurements", + return_value=MOCK_LIMITED_CURRENT_MEASUREMENTS, + ) as mock_current_measurements, + ): hass.config.components.add(huisbaasje.DOMAIN) config_entry = MockConfigEntry( version=1, diff --git a/tests/components/humidifier/test_device_action.py b/tests/components/humidifier/test_device_action.py index ff508bd3a67..13c41fd8369 100644 --- a/tests/components/humidifier/test_device_action.py +++ b/tests/components/humidifier/test_device_action.py @@ -1,4 +1,5 @@ """The tests for Humidifier device actions.""" + import pytest from pytest_unordered import unordered import voluptuous_serialize @@ -94,12 +95,12 @@ async def test_get_actions( @pytest.mark.parametrize( ("hidden_by", "entity_category"), - ( + [ (RegistryEntryHider.INTEGRATION, None), (RegistryEntryHider.USER, None), (None, EntityCategory.CONFIG), (None, EntityCategory.DIAGNOSTIC), - ), + ], ) async def test_get_actions_hidden_auxiliary( hass: HomeAssistant, diff --git a/tests/components/humidifier/test_device_condition.py b/tests/components/humidifier/test_device_condition.py index 224c69b9fb5..fa17d1bb732 100644 --- a/tests/components/humidifier/test_device_condition.py +++ b/tests/components/humidifier/test_device_condition.py @@ -1,4 +1,5 @@ """The tests for Humidifier device conditions.""" + import pytest from pytest_unordered import unordered import voluptuous_serialize @@ -102,12 +103,12 @@ async def test_get_conditions( @pytest.mark.parametrize( ("hidden_by", "entity_category"), - ( + [ (RegistryEntryHider.INTEGRATION, None), (RegistryEntryHider.USER, None), (None, EntityCategory.CONFIG), (None, EntityCategory.DIAGNOSTIC), - ), + ], ) async def test_get_conditions_hidden_auxiliary( hass: HomeAssistant, diff --git a/tests/components/humidifier/test_device_trigger.py b/tests/components/humidifier/test_device_trigger.py index 34067d96ff2..3e05f6b02d1 100644 --- a/tests/components/humidifier/test_device_trigger.py +++ b/tests/components/humidifier/test_device_trigger.py @@ -1,4 +1,5 @@ """The tests for Humidifier device triggers.""" + import datetime import pytest @@ -102,12 +103,12 @@ async def test_get_triggers( @pytest.mark.parametrize( ("hidden_by", "entity_category"), - ( + [ (RegistryEntryHider.INTEGRATION, None), (RegistryEntryHider.USER, None), (None, EntityCategory.CONFIG), (None, EntityCategory.DIAGNOSTIC), - ), + ], ) async def test_get_triggers_hidden_auxiliary( hass: HomeAssistant, diff --git a/tests/components/humidifier/test_init.py b/tests/components/humidifier/test_init.py index 3ef3fca8589..b90e7084dd1 100644 --- a/tests/components/humidifier/test_init.py +++ b/tests/components/humidifier/test_init.py @@ -1,4 +1,5 @@ """The tests for the humidifier component.""" + from enum import Enum from types import ModuleType from unittest.mock import MagicMock @@ -48,10 +49,7 @@ async def test_sync_turn_off(hass: HomeAssistant) -> None: def _create_tuples(enum: Enum, constant_prefix: str) -> list[tuple[Enum, str]]: - result = [] - for enum in enum: - result.append((enum, constant_prefix)) - return result + return [(enum_field, constant_prefix) for enum_field in enum] @pytest.mark.parametrize( diff --git a/tests/components/humidifier/test_intent.py b/tests/components/humidifier/test_intent.py index d8c9f199f57..936369f8aa7 100644 --- a/tests/components/humidifier/test_intent.py +++ b/tests/components/humidifier/test_intent.py @@ -1,4 +1,5 @@ """Tests for the humidifier intents.""" + import pytest from homeassistant.components.humidifier import ( @@ -174,21 +175,18 @@ async def test_intent_set_mode_tests_feature(hass: HomeAssistant) -> None: mode_calls = async_mock_service(hass, DOMAIN, SERVICE_SET_MODE) await intent.async_setup_intents(hass) - try: + with pytest.raises(IntentHandleError) as excinfo: await async_handle( hass, "test", intent.INTENT_MODE, {"name": {"value": "Bedroom humidifier"}, "mode": {"value": "away"}}, ) - pytest.fail("handling intent should have raised") - except IntentHandleError as err: - assert str(err) == "Entity bedroom humidifier does not support modes" - + assert str(excinfo.value) == "Entity bedroom humidifier does not support modes" assert len(mode_calls) == 0 -@pytest.mark.parametrize("available_modes", (["home", "away"], None)) +@pytest.mark.parametrize("available_modes", [["home", "away"], None]) async def test_intent_set_unknown_mode( hass: HomeAssistant, available_modes: list[str] | None ) -> None: @@ -206,15 +204,12 @@ async def test_intent_set_unknown_mode( mode_calls = async_mock_service(hass, DOMAIN, SERVICE_SET_MODE) await intent.async_setup_intents(hass) - try: + with pytest.raises(IntentHandleError) as excinfo: await async_handle( hass, "test", intent.INTENT_MODE, {"name": {"value": "Bedroom humidifier"}, "mode": {"value": "eco"}}, ) - pytest.fail("handling intent should have raised") - except IntentHandleError as err: - assert str(err) == "Entity bedroom humidifier does not support eco mode" - + assert str(excinfo.value) == "Entity bedroom humidifier does not support eco mode" assert len(mode_calls) == 0 diff --git a/tests/components/humidifier/test_recorder.py b/tests/components/humidifier/test_recorder.py index 0a38ff05080..733f505d0ab 100644 --- a/tests/components/humidifier/test_recorder.py +++ b/tests/components/humidifier/test_recorder.py @@ -1,4 +1,5 @@ """The tests for humidifier recorder.""" + from __future__ import annotations from datetime import timedelta @@ -12,7 +13,7 @@ from homeassistant.components.humidifier import ( from homeassistant.components.recorder import Recorder from homeassistant.components.recorder.history import get_significant_states from homeassistant.const import ATTR_FRIENDLY_NAME -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, split_entity_id from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util @@ -35,9 +36,13 @@ async def test_exclude_attributes(recorder_mock: Recorder, hass: HomeAssistant) get_significant_states, hass, now, None, hass.states.async_entity_ids() ) assert len(states) >= 1 - for entity_states in states.values(): - for state in entity_states: - assert ATTR_MIN_HUMIDITY not in state.attributes - assert ATTR_MAX_HUMIDITY not in state.attributes - assert ATTR_AVAILABLE_MODES not in state.attributes - assert ATTR_FRIENDLY_NAME in state.attributes + for state in ( + state + for entity_states in states.values() + for state in entity_states + if split_entity_id(state.entity_id)[0] == humidifier.DOMAIN + ): + assert ATTR_MIN_HUMIDITY not in state.attributes + assert ATTR_MAX_HUMIDITY not in state.attributes + assert ATTR_AVAILABLE_MODES not in state.attributes + assert ATTR_FRIENDLY_NAME in state.attributes diff --git a/tests/components/humidifier/test_reproduce_state.py b/tests/components/humidifier/test_reproduce_state.py index cedb0284104..cab3a11e609 100644 --- a/tests/components/humidifier/test_reproduce_state.py +++ b/tests/components/humidifier/test_reproduce_state.py @@ -1,4 +1,5 @@ """The tests for reproduction of state.""" + import pytest from homeassistant.components.humidifier.const import ( diff --git a/tests/components/humidifier/test_significant_change.py b/tests/components/humidifier/test_significant_change.py index 3d1b2a7e1ab..93e22d7ddaa 100644 --- a/tests/components/humidifier/test_significant_change.py +++ b/tests/components/humidifier/test_significant_change.py @@ -1,4 +1,5 @@ """Test the Humidifier significant change platform.""" + import pytest from homeassistant.components.humidifier import ( diff --git a/tests/components/hunterdouglas_powerview/conftest.py b/tests/components/hunterdouglas_powerview/conftest.py index be7dec42dea..e55e252f670 100644 --- a/tests/components/hunterdouglas_powerview/conftest.py +++ b/tests/components/hunterdouglas_powerview/conftest.py @@ -31,30 +31,39 @@ def mock_hunterdouglas_hub( shades_json: str, ) -> Generator[MagicMock, None, None]: """Return a mocked Powerview Hub with all data populated.""" - with patch( - "homeassistant.components.hunterdouglas_powerview.Hub.request_raw_data", - return_value=load_json_object_fixture(device_json, DOMAIN), - ), patch( - "homeassistant.components.hunterdouglas_powerview.Hub.request_home_data", - return_value=load_json_object_fixture(home_json, DOMAIN), - ), patch( - "homeassistant.components.hunterdouglas_powerview.Hub.request_raw_firmware", - return_value=load_json_object_fixture(firmware_json, DOMAIN), - ), patch( - "homeassistant.components.hunterdouglas_powerview.Rooms.get_resources", - return_value=load_json_value_fixture(rooms_json, DOMAIN), - ), patch( - "homeassistant.components.hunterdouglas_powerview.Scenes.get_resources", - return_value=load_json_value_fixture(scenes_json, DOMAIN), - ), patch( - "homeassistant.components.hunterdouglas_powerview.Shades.get_resources", - return_value=load_json_value_fixture(shades_json, DOMAIN), - ), patch( - "homeassistant.components.hunterdouglas_powerview.cover.BaseShade.refresh", - ), patch( - "homeassistant.components.hunterdouglas_powerview.cover.BaseShade.current_position", - new_callable=PropertyMock, - return_value=ShadePosition(primary=0, secondary=0, tilt=0, velocity=0), + with ( + patch( + "homeassistant.components.hunterdouglas_powerview.Hub.request_raw_data", + return_value=load_json_object_fixture(device_json, DOMAIN), + ), + patch( + "homeassistant.components.hunterdouglas_powerview.Hub.request_home_data", + return_value=load_json_object_fixture(home_json, DOMAIN), + ), + patch( + "homeassistant.components.hunterdouglas_powerview.Hub.request_raw_firmware", + return_value=load_json_object_fixture(firmware_json, DOMAIN), + ), + patch( + "homeassistant.components.hunterdouglas_powerview.Rooms.get_resources", + return_value=load_json_value_fixture(rooms_json, DOMAIN), + ), + patch( + "homeassistant.components.hunterdouglas_powerview.Scenes.get_resources", + return_value=load_json_value_fixture(scenes_json, DOMAIN), + ), + patch( + "homeassistant.components.hunterdouglas_powerview.Shades.get_resources", + return_value=load_json_value_fixture(shades_json, DOMAIN), + ), + patch( + "homeassistant.components.hunterdouglas_powerview.cover.BaseShade.refresh", + ), + patch( + "homeassistant.components.hunterdouglas_powerview.cover.BaseShade.current_position", + new_callable=PropertyMock, + return_value=ShadePosition(primary=0, secondary=0, tilt=0, velocity=0), + ), ): yield diff --git a/tests/components/hunterdouglas_powerview/fixtures/gen2/shades.json b/tests/components/hunterdouglas_powerview/fixtures/gen2/shades.json index 6852c37d883..4ed5e3fc313 100644 --- a/tests/components/hunterdouglas_powerview/fixtures/gen2/shades.json +++ b/tests/components/hunterdouglas_powerview/fixtures/gen2/shades.json @@ -67,7 +67,7 @@ "posKind1": 1, "position1": 0, "posKind2": 3, - "position3": 0 + "position2": 0 }, "name_unicode": "Family Centre", "shade_api_class": "ShadeBottomUpTiltOnClosed180" @@ -102,7 +102,7 @@ "posKind1": 1, "position1": 0, "posKind2": 3, - "position3": 0 + "position2": 0 }, "name_unicode": "Family Right", "shade_api_class": "ShadeBottomUpTiltOnClosed90" @@ -137,7 +137,7 @@ "posKind1": 1, "position1": 0, "posKind2": 3, - "position3": 0 + "position2": 0 }, "name_unicode": "Bed 2", "shade_api_class": "ShadeBottomUpTiltAnywhere" diff --git a/tests/components/hunterdouglas_powerview/fixtures/gen3/home/shades.json b/tests/components/hunterdouglas_powerview/fixtures/gen3/home/shades.json index ef9fdbd5c61..5db6065843f 100644 --- a/tests/components/hunterdouglas_powerview/fixtures/gen3/home/shades.json +++ b/tests/components/hunterdouglas_powerview/fixtures/gen3/home/shades.json @@ -310,5 +310,31 @@ "bleName": "R23:E63C", "shadeGroupIds": [], "shade_api_class": "ShadeDualOverlappedTilt180" + }, + { + "batteryStatus": null, + "bleName": "AUR:881C", + "capabilities": 11, + "firmware": { + "build": 400, + "revision": 3, + "subRevision": 0 + }, + "id": 109, + "motion": null, + "name": "Q2VudGVy", + "positions": { + "light": null, + "primary": null, + "secondary": null, + "tilt": null, + "velocity": null + }, + "powerType": 12, + "ptName": "Center", + "roomId": 9, + "shadeGroupIds": [], + "type": 95, + "shade_api_class": "ShadeDualOverlappedIlluminated" } ] diff --git a/tests/components/hunterdouglas_powerview/test_config_flow.py b/tests/components/hunterdouglas_powerview/test_config_flow.py index 2eaf194ee00..ac4f6368f38 100644 --- a/tests/components/hunterdouglas_powerview/test_config_flow.py +++ b/tests/components/hunterdouglas_powerview/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Hunter Douglas Powerview config flow.""" + from unittest.mock import MagicMock, patch import pytest @@ -241,12 +242,15 @@ async def test_form_no_data( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch( - "homeassistant.components.hunterdouglas_powerview.Hub.request_raw_data", - return_value={}, - ), patch( - "homeassistant.components.hunterdouglas_powerview.Hub.request_home_data", - return_value={}, + with ( + patch( + "homeassistant.components.hunterdouglas_powerview.Hub.request_raw_data", + return_value={}, + ), + patch( + "homeassistant.components.hunterdouglas_powerview.Hub.request_home_data", + return_value={}, + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], diff --git a/tests/components/hunterdouglas_powerview/test_scene.py b/tests/components/hunterdouglas_powerview/test_scene.py index 5f24bbc36ea..9628805d0e8 100644 --- a/tests/components/hunterdouglas_powerview/test_scene.py +++ b/tests/components/hunterdouglas_powerview/test_scene.py @@ -1,4 +1,5 @@ """Test the Hunter Douglas Powerview scene platform.""" + from unittest.mock import patch import pytest diff --git a/tests/components/husqvarna_automower/__init__.py b/tests/components/husqvarna_automower/__init__.py index 069fa0d7372..8c51d69ba3d 100644 --- a/tests/components/husqvarna_automower/__init__.py +++ b/tests/components/husqvarna_automower/__init__.py @@ -1,4 +1,5 @@ """Tests for the Husqvarna Automower integration.""" + from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry diff --git a/tests/components/husqvarna_automower/conftest.py b/tests/components/husqvarna_automower/conftest.py index 3194f1b3188..5d7cb43698b 100644 --- a/tests/components/husqvarna_automower/conftest.py +++ b/tests/components/husqvarna_automower/conftest.py @@ -1,4 +1,5 @@ """Test helpers for Husqvarna Automower.""" + from collections.abc import Generator import time from unittest.mock import AsyncMock, patch diff --git a/tests/components/husqvarna_automower/const.py b/tests/components/husqvarna_automower/const.py index a8b018fa839..dc5893c6749 100644 --- a/tests/components/husqvarna_automower/const.py +++ b/tests/components/husqvarna_automower/const.py @@ -1,4 +1,5 @@ """Constants for Husqvarna Automower tests.""" + CLIENT_ID = "1234" CLIENT_SECRET = "5678" TEST_MOWER_ID = "c7233734-b219-4287-a173-08e3643f89f0" diff --git a/tests/components/husqvarna_automower/snapshots/test_binary_sensor.ambr b/tests/components/husqvarna_automower/snapshots/test_binary_sensor.ambr new file mode 100644 index 00000000000..d677f504390 --- /dev/null +++ b/tests/components/husqvarna_automower/snapshots/test_binary_sensor.ambr @@ -0,0 +1,276 @@ +# serializer version: 1 +# name: test_sensor[binary_sensor.test_mower_1_charging-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.test_mower_1_charging', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Charging', + 'platform': 'husqvarna_automower', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'c7233734-b219-4287-a173-08e3643f89f0_battery_charging', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[binary_sensor.test_mower_1_charging-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery_charging', + 'friendly_name': 'Test Mower 1 Charging', + }), + 'context': , + 'entity_id': 'binary_sensor.test_mower_1_charging', + 'last_changed': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_sensor[binary_sensor.test_mower_1_leaving_dock-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.test_mower_1_leaving_dock', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Leaving dock', + 'platform': 'husqvarna_automower', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'leaving_dock', + 'unique_id': 'c7233734-b219-4287-a173-08e3643f89f0_leaving_dock', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[binary_sensor.test_mower_1_leaving_dock-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Mower 1 Leaving dock', + }), + 'context': , + 'entity_id': 'binary_sensor.test_mower_1_leaving_dock', + 'last_changed': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_sensor[binary_sensor.test_mower_1_returning_to_dock-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.test_mower_1_returning_to_dock', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Returning to dock', + 'platform': 'husqvarna_automower', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'returning_to_dock', + 'unique_id': 'c7233734-b219-4287-a173-08e3643f89f0_returning_to_dock', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[binary_sensor.test_mower_1_returning_to_dock-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Mower 1 Returning to dock', + }), + 'context': , + 'entity_id': 'binary_sensor.test_mower_1_returning_to_dock', + 'last_changed': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_snapshot_binary_sensor[binary_sensor.test_mower_1_charging-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.test_mower_1_charging', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Charging', + 'platform': 'husqvarna_automower', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'c7233734-b219-4287-a173-08e3643f89f0_battery_charging', + 'unit_of_measurement': None, + }) +# --- +# name: test_snapshot_binary_sensor[binary_sensor.test_mower_1_charging-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery_charging', + 'friendly_name': 'Test Mower 1 Charging', + }), + 'context': , + 'entity_id': 'binary_sensor.test_mower_1_charging', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_snapshot_binary_sensor[binary_sensor.test_mower_1_leaving_dock-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.test_mower_1_leaving_dock', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Leaving dock', + 'platform': 'husqvarna_automower', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'leaving_dock', + 'unique_id': 'c7233734-b219-4287-a173-08e3643f89f0_leaving_dock', + 'unit_of_measurement': None, + }) +# --- +# name: test_snapshot_binary_sensor[binary_sensor.test_mower_1_leaving_dock-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Mower 1 Leaving dock', + }), + 'context': , + 'entity_id': 'binary_sensor.test_mower_1_leaving_dock', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_snapshot_binary_sensor[binary_sensor.test_mower_1_returning_to_dock-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.test_mower_1_returning_to_dock', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Returning to dock', + 'platform': 'husqvarna_automower', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'returning_to_dock', + 'unique_id': 'c7233734-b219-4287-a173-08e3643f89f0_returning_to_dock', + 'unit_of_measurement': None, + }) +# --- +# name: test_snapshot_binary_sensor[binary_sensor.test_mower_1_returning_to_dock-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Mower 1 Returning to dock', + }), + 'context': , + 'entity_id': 'binary_sensor.test_mower_1_returning_to_dock', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- diff --git a/tests/components/husqvarna_automower/snapshots/test_device_tracker.ambr b/tests/components/husqvarna_automower/snapshots/test_device_tracker.ambr new file mode 100644 index 00000000000..156eee9b8df --- /dev/null +++ b/tests/components/husqvarna_automower/snapshots/test_device_tracker.ambr @@ -0,0 +1,51 @@ +# serializer version: 1 +# name: test_device_tracker_snapshot[device_tracker.test_mower_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'device_tracker', + 'entity_category': , + 'entity_id': 'device_tracker.test_mower_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'husqvarna_automower', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'c7233734-b219-4287-a173-08e3643f89f0', + 'unit_of_measurement': None, + }) +# --- +# name: test_device_tracker_snapshot[device_tracker.test_mower_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Mower 1', + 'gps_accuracy': 0, + 'latitude': 35.5402913, + 'longitude': -82.5527055, + 'source_type': , + }), + 'context': , + 'entity_id': 'device_tracker.test_mower_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'not_home', + }) +# --- diff --git a/tests/components/husqvarna_automower/snapshots/test_diagnostics.ambr b/tests/components/husqvarna_automower/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..aea65005fc4 --- /dev/null +++ b/tests/components/husqvarna_automower/snapshots/test_diagnostics.ambr @@ -0,0 +1,129 @@ +# serializer version: 1 +# name: test_device_diagnostics + dict({ + 'battery': dict({ + 'battery_percent': 100, + }), + 'calendar': dict({ + 'tasks': list([ + dict({ + 'duration': 300, + 'friday': True, + 'monday': True, + 'saturday': False, + 'start': 1140, + 'sunday': False, + 'thursday': False, + 'tuesday': False, + 'wednesday': True, + 'work_area_id': None, + }), + dict({ + 'duration': 480, + 'friday': False, + 'monday': False, + 'saturday': True, + 'start': 0, + 'sunday': False, + 'thursday': True, + 'tuesday': True, + 'wednesday': False, + 'work_area_id': None, + }), + ]), + }), + 'capabilities': dict({ + 'headlights': True, + 'position': True, + 'stay_out_zones': False, + 'work_areas': False, + }), + 'cutting_height': 4, + 'headlight': dict({ + 'mode': 'EVENING_ONLY', + }), + 'metadata': dict({ + 'connected': True, + 'status_dateteime': '2023-10-18T22:58:52.683000+00:00', + }), + 'mower': dict({ + 'activity': 'PARKED_IN_CS', + 'error_code': 0, + 'error_datetime': None, + 'error_key': None, + 'mode': 'MAIN_AREA', + 'state': 'RESTRICTED', + }), + 'planner': dict({ + 'next_start_datetime': '2023-06-05T19:00:00', + 'override': dict({ + 'action': 'NOT_ACTIVE', + }), + 'restricted_reason': 'WEEK_SCHEDULE', + }), + 'positions': '**REDACTED**', + 'statistics': dict({ + 'cutting_blade_usage_time': 123, + 'number_of_charging_cycles': 1380, + 'number_of_collisions': 11396, + 'total_charging_time': 4334400, + 'total_cutting_time': 4194000, + 'total_drive_distance': 1780272, + 'total_running_time': 4564800, + 'total_searching_time': 370800, + }), + 'stay_out_zones': dict({ + 'dirty': False, + 'zones': dict({ + '81C6EEA2-D139-4FEA-B134-F22A6B3EA403': dict({ + 'enabled': True, + 'name': 'Springflowers', + }), + }), + }), + 'system': dict({ + 'model': '450XH-TEST', + 'name': 'Test Mower 1', + 'serial_number': 123, + }), + 'work_areas': dict({ + '0': dict({ + 'cutting_height': 50, + 'name': None, + }), + '123456': dict({ + 'cutting_height': 50, + 'name': 'Front lawn', + }), + }), + }) +# --- +# name: test_entry_diagnostics + dict({ + 'data': dict({ + 'auth_implementation': 'husqvarna_automower', + 'token': dict({ + 'access_token': '**REDACTED**', + 'expires_at': 1709208000.0, + 'expires_in': 86399, + 'provider': 'husqvarna', + 'refresh_token': '**REDACTED**', + 'scope': 'iam:read amc:api', + 'token_type': 'Bearer', + 'user_id': '123', + }), + }), + 'disabled_by': None, + 'domain': 'husqvarna_automower', + 'entry_id': 'automower_test', + 'minor_version': 1, + 'options': dict({ + }), + 'pref_disable_new_entities': False, + 'pref_disable_polling': False, + 'source': 'user', + 'title': 'Husqvarna Automower of Erika Mustermann', + 'unique_id': '123', + 'version': 1, + }) +# --- diff --git a/tests/components/husqvarna_automower/snapshots/test_init.ambr b/tests/components/husqvarna_automower/snapshots/test_init.ambr new file mode 100644 index 00000000000..c3a7191b4b9 --- /dev/null +++ b/tests/components/husqvarna_automower/snapshots/test_init.ambr @@ -0,0 +1,31 @@ +# serializer version: 1 +# name: test_device_info + DeviceRegistryEntrySnapshot({ + 'area_id': 'garden', + 'config_entries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'husqvarna_automower', + 'c7233734-b219-4287-a173-08e3643f89f0', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'Husqvarna', + 'model': '450XH-TEST', + 'name': 'Test Mower 1', + 'name_by_user': None, + 'serial_number': 123, + 'suggested_area': 'Garden', + 'sw_version': None, + 'via_device_id': None, + }) +# --- diff --git a/tests/components/husqvarna_automower/snapshots/test_sensor.ambr b/tests/components/husqvarna_automower/snapshots/test_sensor.ambr index f03624627bf..ce81098f753 100644 --- a/tests/components/husqvarna_automower/snapshots/test_sensor.ambr +++ b/tests/components/husqvarna_automower/snapshots/test_sensor.ambr @@ -45,6 +45,7 @@ 'context': , 'entity_id': 'sensor.test_mower_1_battery', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '100', }) @@ -98,6 +99,7 @@ 'context': , 'entity_id': 'sensor.test_mower_1_cutting_blade_usage_time', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0.034', }) @@ -159,6 +161,7 @@ 'context': , 'entity_id': 'sensor.test_mower_1_mode', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'main_area', }) @@ -205,8 +208,9 @@ 'context': , 'entity_id': 'sensor.test_mower_1_next_start', 'last_changed': , + 'last_reported': , 'last_updated': , - 'state': '2023-06-05T19:00:00+00:00', + 'state': '2023-06-06T02:00:00+00:00', }) # --- # name: test_sensor[sensor.test_mower_1_number_of_charging_cycles-entry] @@ -234,7 +238,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:battery-sync-outline', + 'original_icon': None, 'original_name': 'Number of charging cycles', 'platform': 'husqvarna_automower', 'previous_unique_id': None, @@ -248,12 +252,12 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Test Mower 1 Number of charging cycles', - 'icon': 'mdi:battery-sync-outline', 'state_class': , }), 'context': , 'entity_id': 'sensor.test_mower_1_number_of_charging_cycles', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '1380', }) @@ -283,7 +287,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:counter', + 'original_icon': None, 'original_name': 'Number of collisions', 'platform': 'husqvarna_automower', 'previous_unique_id': None, @@ -297,12 +301,12 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Test Mower 1 Number of collisions', - 'icon': 'mdi:counter', 'state_class': , }), 'context': , 'entity_id': 'sensor.test_mower_1_number_of_collisions', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '11396', }) @@ -356,6 +360,7 @@ 'context': , 'entity_id': 'sensor.test_mower_1_total_charging_time', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '1204.000', }) @@ -409,6 +414,7 @@ 'context': , 'entity_id': 'sensor.test_mower_1_total_cutting_time', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '1165.000', }) @@ -462,6 +468,7 @@ 'context': , 'entity_id': 'sensor.test_mower_1_total_drive_distance', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '1780.272', }) @@ -515,6 +522,7 @@ 'context': , 'entity_id': 'sensor.test_mower_1_total_running_time', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '1268.000', }) @@ -568,6 +576,7 @@ 'context': , 'entity_id': 'sensor.test_mower_1_total_searching_time', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '103.000', }) diff --git a/tests/components/husqvarna_automower/snapshots/test_switch.ambr b/tests/components/husqvarna_automower/snapshots/test_switch.ambr index b1629a4cf99..c54997fcf06 100644 --- a/tests/components/husqvarna_automower/snapshots/test_switch.ambr +++ b/tests/components/husqvarna_automower/snapshots/test_switch.ambr @@ -40,6 +40,7 @@ 'context': , 'entity_id': 'switch.test_mower_1_enable_schedule', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }) diff --git a/tests/components/husqvarna_automower/test_binary_sensor.py b/tests/components/husqvarna_automower/test_binary_sensor.py new file mode 100644 index 00000000000..425636ba915 --- /dev/null +++ b/tests/components/husqvarna_automower/test_binary_sensor.py @@ -0,0 +1,83 @@ +"""Tests for binary sensor platform.""" + +from datetime import timedelta +from unittest.mock import AsyncMock, patch + +from aioautomower.model import MowerActivities +from aioautomower.utils import mower_list_to_dictionary_dataclass +from freezegun.api import FrozenDateTimeFactory +from syrupy import SnapshotAssertion + +from homeassistant.components.husqvarna_automower.const import DOMAIN +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import setup_integration +from .const import TEST_MOWER_ID + +from tests.common import ( + MockConfigEntry, + async_fire_time_changed, + load_json_value_fixture, +) + + +async def test_binary_sensor_states( + hass: HomeAssistant, + mock_automower_client: AsyncMock, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test binary sensor states.""" + values = mower_list_to_dictionary_dataclass( + load_json_value_fixture("mower.json", DOMAIN) + ) + await setup_integration(hass, mock_config_entry) + state = hass.states.get("binary_sensor.test_mower_1_charging") + assert state is not None + assert state.state == "off" + state = hass.states.get("binary_sensor.test_mower_1_leaving_dock") + assert state is not None + assert state.state == "off" + state = hass.states.get("binary_sensor.test_mower_1_returning_to_dock") + assert state is not None + assert state.state == "off" + + for activity, entity in [ + (MowerActivities.CHARGING, "test_mower_1_charging"), + (MowerActivities.LEAVING, "test_mower_1_leaving_dock"), + (MowerActivities.GOING_HOME, "test_mower_1_returning_to_dock"), + ]: + values[TEST_MOWER_ID].mower.activity = activity + mock_automower_client.get_status.return_value = values + freezer.tick(timedelta(minutes=5)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + state = hass.states.get(f"binary_sensor.{entity}") + assert state.state == "on" + + +async def test_snapshot_binary_sensor( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + mock_automower_client: AsyncMock, + mock_config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, +) -> None: + """Test states of the binary sensors.""" + with patch( + "homeassistant.components.husqvarna_automower.PLATFORMS", + [Platform.BINARY_SENSOR], + ): + await setup_integration(hass, mock_config_entry) + entity_entries = er.async_entries_for_config_entry( + entity_registry, mock_config_entry.entry_id + ) + + assert entity_entries + for entity_entry in entity_entries: + assert hass.states.get(entity_entry.entity_id) == snapshot( + name=f"{entity_entry.entity_id}-state" + ) + assert entity_entry == snapshot(name=f"{entity_entry.entity_id}-entry") diff --git a/tests/components/husqvarna_automower/test_config_flow.py b/tests/components/husqvarna_automower/test_config_flow.py index fcf9fbffa0c..e22ab7718ec 100644 --- a/tests/components/husqvarna_automower/test_config_flow.py +++ b/tests/components/husqvarna_automower/test_config_flow.py @@ -127,3 +127,148 @@ async def test_config_non_unique_profile( result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" + + +async def test_reauth( + hass: HomeAssistant, + hass_client_no_auth: ClientSessionGenerator, + aioclient_mock: AiohttpClientMocker, + mock_config_entry: MockConfigEntry, + current_request_with_host: None, + mock_automower_client: AsyncMock, + jwt, +) -> None: + """Test the reauthentication case updates the existing config entry.""" + + mock_config_entry.add_to_hass(hass) + + mock_config_entry.async_start_reauth(hass) + await hass.async_block_till_done() + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + result = flows[0] + assert result["step_id"] == "reauth_confirm" + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": "https://example.com/auth/external/callback", + }, + ) + assert result["url"] == ( + f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" + "&redirect_uri=https://example.com/auth/external/callback" + f"&state={state}" + ) + client = await hass_client_no_auth() + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == 200 + assert resp.headers["content-type"] == "text/html; charset=utf-8" + + aioclient_mock.post( + OAUTH2_TOKEN, + json={ + "access_token": "mock-updated-token", + "scope": "iam:read amc:api", + "expires_in": 86399, + "refresh_token": "mock-refresh-token", + "provider": "husqvarna", + "user_id": USER_ID, + "token_type": "Bearer", + "expires_at": 1697753347, + }, + ) + + with patch( + "homeassistant.components.husqvarna_automower.async_setup_entry", + return_value=True, + ): + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + + assert result.get("type") == "abort" + assert result.get("reason") == "reauth_successful" + + assert mock_config_entry.unique_id == USER_ID + assert "token" in mock_config_entry.data + # Verify access token is refreshed + assert mock_config_entry.data["token"].get("access_token") == "mock-updated-token" + assert mock_config_entry.data["token"].get("refresh_token") == "mock-refresh-token" + + +async def test_reauth_wrong_account( + hass: HomeAssistant, + hass_client_no_auth: ClientSessionGenerator, + aioclient_mock: AiohttpClientMocker, + mock_config_entry: MockConfigEntry, + current_request_with_host: None, + mock_automower_client: AsyncMock, + jwt, +) -> None: + """Test the reauthentication aborts, if user tries to reauthenticate with another account.""" + + mock_config_entry.add_to_hass(hass) + + mock_config_entry.async_start_reauth(hass) + await hass.async_block_till_done() + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + result = flows[0] + assert result["step_id"] == "reauth_confirm" + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": "https://example.com/auth/external/callback", + }, + ) + assert result["url"] == ( + f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" + "&redirect_uri=https://example.com/auth/external/callback" + f"&state={state}" + ) + client = await hass_client_no_auth() + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == 200 + assert resp.headers["content-type"] == "text/html; charset=utf-8" + + aioclient_mock.post( + OAUTH2_TOKEN, + json={ + "access_token": "mock-updated-token", + "scope": "iam:read amc:api", + "expires_in": 86399, + "refresh_token": "mock-refresh-token", + "provider": "husqvarna", + "user_id": "wrong-user-id", + "token_type": "Bearer", + "expires_at": 1697753347, + }, + ) + + with patch( + "homeassistant.components.husqvarna_automower.async_setup_entry", + return_value=True, + ): + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + + assert result.get("type") == "abort" + assert result.get("reason") == "wrong_account" + + assert mock_config_entry.unique_id == USER_ID + assert "token" in mock_config_entry.data + # Verify access token is like before + assert mock_config_entry.data["token"].get("access_token") == jwt + assert ( + mock_config_entry.data["token"].get("refresh_token") + == "3012bc9f-7a65-4240-b817-9154ffdcc30f" + ) diff --git a/tests/components/husqvarna_automower/test_device_tracker.py b/tests/components/husqvarna_automower/test_device_tracker.py new file mode 100644 index 00000000000..d9cab0d5074 --- /dev/null +++ b/tests/components/husqvarna_automower/test_device_tracker.py @@ -0,0 +1,38 @@ +"""Tests for the device tracker platform.""" + +from unittest.mock import AsyncMock, patch + +from syrupy import SnapshotAssertion + +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import setup_integration + +from tests.common import MockConfigEntry + + +async def test_device_tracker_snapshot( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + mock_automower_client: AsyncMock, + mock_config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, +) -> None: + """Test device tracker with a snapshot.""" + with patch( + "homeassistant.components.husqvarna_automower.PLATFORMS", + [Platform.DEVICE_TRACKER], + ): + await setup_integration(hass, mock_config_entry) + entity_entries = er.async_entries_for_config_entry( + entity_registry, mock_config_entry.entry_id + ) + + assert entity_entries + for entity_entry in entity_entries: + assert hass.states.get(entity_entry.entity_id) == snapshot( + name=f"{entity_entry.entity_id}-state" + ) + assert entity_entry == snapshot(name=f"{entity_entry.entity_id}-entry") diff --git a/tests/components/husqvarna_automower/test_diagnostics.py b/tests/components/husqvarna_automower/test_diagnostics.py new file mode 100644 index 00000000000..c19345e507e --- /dev/null +++ b/tests/components/husqvarna_automower/test_diagnostics.py @@ -0,0 +1,62 @@ +"""Test the Husqvarna Automower Diagnostics.""" + +import datetime +from unittest.mock import AsyncMock + +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.husqvarna_automower.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr + +from .const import TEST_MOWER_ID + +from tests.common import MockConfigEntry +from tests.components.diagnostics import ( + get_diagnostics_for_config_entry, + get_diagnostics_for_device, +) +from tests.typing import ClientSessionGenerator + + +@pytest.mark.freeze_time(datetime.datetime(2024, 2, 29, 11, tzinfo=datetime.UTC)) +async def test_entry_diagnostics( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + snapshot: SnapshotAssertion, + mock_automower_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test config entry diagnostics.""" + + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + result = await get_diagnostics_for_config_entry( + hass, hass_client, mock_config_entry + ) + assert result == snapshot + + +async def test_device_diagnostics( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + snapshot: SnapshotAssertion, + mock_automower_client: AsyncMock, + mock_config_entry: MockConfigEntry, + device_registry: dr.DeviceRegistry, +) -> None: + """Test select platform.""" + + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + reg_device = device_registry.async_get_device( + identifiers={(DOMAIN, TEST_MOWER_ID)}, + ) + assert reg_device is not None + result = await get_diagnostics_for_device( + hass, hass_client, mock_config_entry, reg_device + ) + assert result == snapshot diff --git a/tests/components/husqvarna_automower/test_init.py b/tests/components/husqvarna_automower/test_init.py index c11e4ac4cc7..3c97a3b2668 100644 --- a/tests/components/husqvarna_automower/test_init.py +++ b/tests/components/husqvarna_automower/test_init.py @@ -1,4 +1,5 @@ """Tests for init module.""" + from datetime import timedelta import http import time @@ -7,12 +8,15 @@ from unittest.mock import AsyncMock from aioautomower.exceptions import ApiException, HusqvarnaWSServerHandshakeError from freezegun.api import FrozenDateTimeFactory import pytest +from syrupy.assertion import SnapshotAssertion from homeassistant.components.husqvarna_automower.const import DOMAIN, OAUTH2_TOKEN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr from . import setup_integration +from .const import TEST_MOWER_ID from tests.common import MockConfigEntry, async_fire_time_changed from tests.test_util.aiohttp import AiohttpClientMocker @@ -41,7 +45,7 @@ async def test_load_unload_entry( ( time.time() - 3600, http.HTTPStatus.UNAUTHORIZED, - ConfigEntryState.SETUP_RETRY, # Will trigger reauth in the future + ConfigEntryState.SETUP_ERROR, ), ( time.time() - 3600, @@ -108,3 +112,21 @@ async def test_websocket_not_available( assert mock_automower_client.auth.websocket_connect.call_count == 2 assert mock_automower_client.start_listening.call_count == 2 assert mock_config_entry.state == ConfigEntryState.LOADED + + +async def test_device_info( + hass: HomeAssistant, + mock_automower_client: AsyncMock, + mock_config_entry: MockConfigEntry, + device_registry: dr.DeviceRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test select platform.""" + + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + reg_device = device_registry.async_get_device( + identifiers={(DOMAIN, TEST_MOWER_ID)}, + ) + assert reg_device == snapshot diff --git a/tests/components/husqvarna_automower/test_lawn_mower.py b/tests/components/husqvarna_automower/test_lawn_mower.py index 8c444913641..6e491fd4a28 100644 --- a/tests/components/husqvarna_automower/test_lawn_mower.py +++ b/tests/components/husqvarna_automower/test_lawn_mower.py @@ -1,4 +1,5 @@ """Tests for lawn_mower module.""" + from datetime import timedelta from unittest.mock import AsyncMock diff --git a/tests/components/husqvarna_automower/test_select.py b/tests/components/husqvarna_automower/test_select.py new file mode 100644 index 00000000000..4283c7d3797 --- /dev/null +++ b/tests/components/husqvarna_automower/test_select.py @@ -0,0 +1,102 @@ +"""Tests for select platform.""" + +from datetime import timedelta +from unittest.mock import AsyncMock + +from aioautomower.exceptions import ApiException +from aioautomower.model import HeadlightModes +from aioautomower.utils import mower_list_to_dictionary_dataclass +from freezegun.api import FrozenDateTimeFactory +import pytest + +from homeassistant.components.husqvarna_automower.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError + +from . import setup_integration +from .const import TEST_MOWER_ID + +from tests.common import ( + MockConfigEntry, + async_fire_time_changed, + load_json_value_fixture, +) + + +async def test_select_states( + hass: HomeAssistant, + mock_automower_client: AsyncMock, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test states of headlight mode select.""" + values = mower_list_to_dictionary_dataclass( + load_json_value_fixture("mower.json", DOMAIN) + ) + await setup_integration(hass, mock_config_entry) + state = hass.states.get("select.test_mower_1_headlight_mode") + assert state is not None + assert state.state == "evening_only" + + for state, expected_state in [ + ( + HeadlightModes.ALWAYS_OFF, + "always_off", + ), + (HeadlightModes.ALWAYS_ON, "always_on"), + (HeadlightModes.EVENING_AND_NIGHT, "evening_and_night"), + ]: + values[TEST_MOWER_ID].headlight.mode = state + mock_automower_client.get_status.return_value = values + freezer.tick(timedelta(minutes=5)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + state = hass.states.get("select.test_mower_1_headlight_mode") + assert state.state == expected_state + + +@pytest.mark.parametrize( + ("service"), + [ + ("always_on"), + ("always_off"), + ("evening_only"), + ("evening_and_night"), + ], +) +async def test_select_commands( + hass: HomeAssistant, + service: str, + mock_automower_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test select commands for headlight mode.""" + await setup_integration(hass, mock_config_entry) + await hass.services.async_call( + domain="select", + service="select_option", + service_data={ + "entity_id": "select.test_mower_1_headlight_mode", + "option": service, + }, + blocking=True, + ) + mocked_method = mock_automower_client.set_headlight_mode + assert len(mocked_method.mock_calls) == 1 + + mocked_method.side_effect = ApiException("Test error") + with pytest.raises(HomeAssistantError) as exc_info: + await hass.services.async_call( + domain="select", + service="select_option", + service_data={ + "entity_id": "select.test_mower_1_headlight_mode", + "option": service, + }, + blocking=True, + ) + assert ( + str(exc_info.value) + == "Command couldn't be sent to the command queue: Test error" + ) + assert len(mocked_method.mock_calls) == 2 diff --git a/tests/components/husqvarna_automower/test_sensor.py b/tests/components/husqvarna_automower/test_sensor.py index feae870478e..bc464b2ce78 100644 --- a/tests/components/husqvarna_automower/test_sensor.py +++ b/tests/components/husqvarna_automower/test_sensor.py @@ -1,4 +1,5 @@ """Tests for sensor platform.""" + from datetime import timedelta from unittest.mock import AsyncMock, patch diff --git a/tests/components/husqvarna_automower/test_switch.py b/tests/components/husqvarna_automower/test_switch.py index c4a73fec641..22137a35323 100644 --- a/tests/components/husqvarna_automower/test_switch.py +++ b/tests/components/husqvarna_automower/test_switch.py @@ -1,4 +1,5 @@ """Tests for switch platform.""" + from datetime import timedelta from unittest.mock import AsyncMock, patch diff --git a/tests/components/huum/test_config_flow.py b/tests/components/huum/test_config_flow.py index 7163521b446..219783079e3 100644 --- a/tests/components/huum/test_config_flow.py +++ b/tests/components/huum/test_config_flow.py @@ -1,4 +1,5 @@ """Test the huum config flow.""" + from unittest.mock import patch from huum.exceptions import Forbidden @@ -25,13 +26,16 @@ async def test_form(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.FORM assert result["errors"] == {} - with patch( - "homeassistant.components.huum.config_flow.Huum.status", - return_value=True, - ), patch( - "homeassistant.components.huum.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.huum.config_flow.Huum.status", + return_value=True, + ), + patch( + "homeassistant.components.huum.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -67,12 +71,15 @@ async def test_signup_flow_already_set_up(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch( - "homeassistant.components.huum.config_flow.Huum.status", - return_value=True, - ), patch( - "homeassistant.components.huum.async_setup_entry", - return_value=True, + with ( + patch( + "homeassistant.components.huum.config_flow.Huum.status", + return_value=True, + ), + patch( + "homeassistant.components.huum.async_setup_entry", + return_value=True, + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -118,12 +125,15 @@ async def test_huum_errors( assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": error_base} - with patch( - "homeassistant.components.huum.config_flow.Huum.status", - return_value=True, - ), patch( - "homeassistant.components.huum.async_setup_entry", - return_value=True, + with ( + patch( + "homeassistant.components.huum.config_flow.Huum.status", + return_value=True, + ), + patch( + "homeassistant.components.huum.async_setup_entry", + return_value=True, + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], diff --git a/tests/components/hvv_departures/test_config_flow.py b/tests/components/hvv_departures/test_config_flow.py index 0d885febb3c..2712e1bbca9 100644 --- a/tests/components/hvv_departures/test_config_flow.py +++ b/tests/components/hvv_departures/test_config_flow.py @@ -1,4 +1,5 @@ """Test the HVV Departures config flow.""" + import json from unittest.mock import patch @@ -30,18 +31,23 @@ FIXTURE_DEPARTURE_LIST = json.loads(load_fixture("hvv_departures/departure_list. async def test_user_flow(hass: HomeAssistant) -> None: """Test that config flow works.""" - with patch( - "homeassistant.components.hvv_departures.hub.GTI.init", - return_value=FIXTURE_INIT, - ), patch( - "homeassistant.components.hvv_departures.hub.GTI.checkName", - return_value=FIXTURE_CHECK_NAME, - ), patch( - "homeassistant.components.hvv_departures.hub.GTI.stationInformation", - return_value=FIXTURE_STATION_INFORMATION, - ), patch( - "homeassistant.components.hvv_departures.async_setup_entry", - return_value=True, + with ( + patch( + "homeassistant.components.hvv_departures.hub.GTI.init", + return_value=FIXTURE_INIT, + ), + patch( + "homeassistant.components.hvv_departures.hub.GTI.checkName", + return_value=FIXTURE_CHECK_NAME, + ), + patch( + "homeassistant.components.hvv_departures.hub.GTI.stationInformation", + return_value=FIXTURE_STATION_INFORMATION, + ), + patch( + "homeassistant.components.hvv_departures.async_setup_entry", + return_value=True, + ), ): # step: user @@ -93,15 +99,19 @@ async def test_user_flow(hass: HomeAssistant) -> None: async def test_user_flow_no_results(hass: HomeAssistant) -> None: """Test that config flow works when there are no results.""" - with patch( - "homeassistant.components.hvv_departures.hub.GTI.init", - return_value=FIXTURE_INIT, - ), patch( - "homeassistant.components.hvv_departures.hub.GTI.checkName", - return_value={"returnCode": "OK", "results": []}, - ), patch( - "homeassistant.components.hvv_departures.async_setup_entry", - return_value=True, + with ( + patch( + "homeassistant.components.hvv_departures.hub.GTI.init", + return_value=FIXTURE_INIT, + ), + patch( + "homeassistant.components.hvv_departures.hub.GTI.checkName", + return_value={"returnCode": "OK", "results": []}, + ), + patch( + "homeassistant.components.hvv_departures.async_setup_entry", + return_value=True, + ), ): # step: user @@ -178,12 +188,15 @@ async def test_user_flow_cannot_connect(hass: HomeAssistant) -> None: async def test_user_flow_station(hass: HomeAssistant) -> None: """Test that config flow handles empty data on step station.""" - with patch( - "homeassistant.components.hvv_departures.hub.GTI.init", - return_value=True, - ), patch( - "homeassistant.components.hvv_departures.hub.GTI.checkName", - return_value={"returnCode": "OK", "results": []}, + with ( + patch( + "homeassistant.components.hvv_departures.hub.GTI.init", + return_value=True, + ), + patch( + "homeassistant.components.hvv_departures.hub.GTI.checkName", + return_value={"returnCode": "OK", "results": []}, + ), ): # step: user @@ -211,12 +224,15 @@ async def test_user_flow_station(hass: HomeAssistant) -> None: async def test_user_flow_station_select(hass: HomeAssistant) -> None: """Test that config flow handles empty data on step station_select.""" - with patch( - "homeassistant.components.hvv_departures.hub.GTI.init", - return_value=True, - ), patch( - "homeassistant.components.hvv_departures.hub.GTI.checkName", - return_value=FIXTURE_CHECK_NAME, + with ( + patch( + "homeassistant.components.hvv_departures.hub.GTI.init", + return_value=True, + ), + patch( + "homeassistant.components.hvv_departures.hub.GTI.checkName", + return_value=FIXTURE_CHECK_NAME, + ), ): result_user = await hass.config_entries.flow.async_init( DOMAIN, @@ -257,12 +273,16 @@ async def test_options_flow(hass: HomeAssistant) -> None: ) config_entry.add_to_hass(hass) - with patch("homeassistant.components.hvv_departures.PLATFORMS", new=[]), patch( - "homeassistant.components.hvv_departures.hub.GTI.init", - return_value=True, - ), patch( - "homeassistant.components.hvv_departures.hub.GTI.departureList", - return_value=FIXTURE_DEPARTURE_LIST, + with ( + patch("homeassistant.components.hvv_departures.PLATFORMS", new=[]), + patch( + "homeassistant.components.hvv_departures.hub.GTI.init", + return_value=True, + ), + patch( + "homeassistant.components.hvv_departures.hub.GTI.departureList", + return_value=FIXTURE_DEPARTURE_LIST, + ), ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -306,11 +326,15 @@ async def test_options_flow_invalid_auth(hass: HomeAssistant) -> None: ) config_entry.add_to_hass(hass) - with patch("homeassistant.components.hvv_departures.PLATFORMS", new=[]), patch( - "homeassistant.components.hvv_departures.hub.GTI.init", return_value=True - ), patch( - "homeassistant.components.hvv_departures.hub.GTI.departureList", - return_value=FIXTURE_DEPARTURE_LIST, + with ( + patch("homeassistant.components.hvv_departures.PLATFORMS", new=[]), + patch( + "homeassistant.components.hvv_departures.hub.GTI.init", return_value=True + ), + patch( + "homeassistant.components.hvv_departures.hub.GTI.departureList", + return_value=FIXTURE_DEPARTURE_LIST, + ), ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -345,11 +369,15 @@ async def test_options_flow_cannot_connect(hass: HomeAssistant) -> None: ) config_entry.add_to_hass(hass) - with patch("homeassistant.components.hvv_departures.PLATFORMS", new=[]), patch( - "homeassistant.components.hvv_departures.hub.GTI.init", return_value=True - ), patch( - "homeassistant.components.hvv_departures.hub.GTI.departureList", - return_value=FIXTURE_DEPARTURE_LIST, + with ( + patch("homeassistant.components.hvv_departures.PLATFORMS", new=[]), + patch( + "homeassistant.components.hvv_departures.hub.GTI.init", return_value=True + ), + patch( + "homeassistant.components.hvv_departures.hub.GTI.departureList", + return_value=FIXTURE_DEPARTURE_LIST, + ), ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/hyperion/__init__.py b/tests/components/hyperion/__init__.py index 3714e58479b..72aba96e81f 100644 --- a/tests/components/hyperion/__init__.py +++ b/tests/components/hyperion/__init__.py @@ -1,4 +1,5 @@ """Tests for the Hyperion component.""" + from __future__ import annotations from types import TracebackType diff --git a/tests/components/hyperion/conftest.py b/tests/components/hyperion/conftest.py index f971fa3c767..7cbaa07ec03 100644 --- a/tests/components/hyperion/conftest.py +++ b/tests/components/hyperion/conftest.py @@ -1,2 +1,3 @@ """hyperion conftest.""" + from tests.components.light.conftest import mock_light_profiles # noqa: F401 diff --git a/tests/components/hyperion/test_camera.py b/tests/components/hyperion/test_camera.py index e087b0fc1a5..0169759f328 100644 --- a/tests/components/hyperion/test_camera.py +++ b/tests/components/hyperion/test_camera.py @@ -1,4 +1,5 @@ """Tests for the Hyperion integration.""" + from __future__ import annotations import asyncio diff --git a/tests/components/hyperion/test_config_flow.py b/tests/components/hyperion/test_config_flow.py index 97b705ef731..86dc4c5c39d 100644 --- a/tests/components/hyperion/test_config_flow.py +++ b/tests/components/hyperion/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for the Hyperion config flow.""" + from __future__ import annotations import asyncio @@ -375,11 +376,15 @@ async def test_auth_create_token_approval_declined(hass: HomeAssistant) -> None: assert result["step_id"] == "auth" client.async_request_token = AsyncMock(return_value=TEST_REQUEST_TOKEN_FAIL) - with patch( - "homeassistant.components.hyperion.client.HyperionClient", return_value=client - ), patch( - "homeassistant.components.hyperion.config_flow.client.generate_random_auth_id", - return_value=TEST_AUTH_ID, + with ( + patch( + "homeassistant.components.hyperion.client.HyperionClient", + return_value=client, + ), + patch( + "homeassistant.components.hyperion.config_flow.client.generate_random_auth_id", + return_value=TEST_AUTH_ID, + ), ): result = await _configure_flow( hass, result, user_input={CONF_CREATE_TOKEN: True} @@ -439,11 +444,15 @@ async def test_auth_create_token_approval_declined_task_canceled( task_coro = arg return mock_task - with patch( - "homeassistant.components.hyperion.client.HyperionClient", return_value=client - ), patch( - "homeassistant.components.hyperion.config_flow.client.generate_random_auth_id", - return_value=TEST_AUTH_ID, + with ( + patch( + "homeassistant.components.hyperion.client.HyperionClient", + return_value=client, + ), + patch( + "homeassistant.components.hyperion.config_flow.client.generate_random_auth_id", + return_value=TEST_AUTH_ID, + ), ): result = await _configure_flow( hass, result, user_input={CONF_CREATE_TOKEN: True} @@ -481,11 +490,15 @@ async def test_auth_create_token_when_issued_token_fails( assert result["step_id"] == "auth" client.async_request_token = AsyncMock(return_value=TEST_REQUEST_TOKEN_SUCCESS) - with patch( - "homeassistant.components.hyperion.client.HyperionClient", return_value=client - ), patch( - "homeassistant.components.hyperion.config_flow.client.generate_random_auth_id", - return_value=TEST_AUTH_ID, + with ( + patch( + "homeassistant.components.hyperion.client.HyperionClient", + return_value=client, + ), + patch( + "homeassistant.components.hyperion.config_flow.client.generate_random_auth_id", + return_value=TEST_AUTH_ID, + ), ): result = await _configure_flow( hass, result, user_input={CONF_CREATE_TOKEN: True} @@ -525,11 +538,15 @@ async def test_auth_create_token_success(hass: HomeAssistant) -> None: assert result["step_id"] == "auth" client.async_request_token = AsyncMock(return_value=TEST_REQUEST_TOKEN_SUCCESS) - with patch( - "homeassistant.components.hyperion.client.HyperionClient", return_value=client - ), patch( - "homeassistant.components.hyperion.config_flow.client.generate_random_auth_id", - return_value=TEST_AUTH_ID, + with ( + patch( + "homeassistant.components.hyperion.client.HyperionClient", + return_value=client, + ), + patch( + "homeassistant.components.hyperion.config_flow.client.generate_random_auth_id", + return_value=TEST_AUTH_ID, + ), ): result = await _configure_flow( hass, result, user_input={CONF_CREATE_TOKEN: True} @@ -571,11 +588,15 @@ async def test_auth_create_token_success_but_login_fail( assert result["step_id"] == "auth" client.async_request_token = AsyncMock(return_value=TEST_REQUEST_TOKEN_SUCCESS) - with patch( - "homeassistant.components.hyperion.client.HyperionClient", return_value=client - ), patch( - "homeassistant.components.hyperion.config_flow.client.generate_random_auth_id", - return_value=TEST_AUTH_ID, + with ( + patch( + "homeassistant.components.hyperion.client.HyperionClient", + return_value=client, + ), + patch( + "homeassistant.components.hyperion.config_flow.client.generate_random_auth_id", + return_value=TEST_AUTH_ID, + ), ): result = await _configure_flow( hass, result, user_input={CONF_CREATE_TOKEN: True} @@ -683,11 +704,15 @@ async def test_ssdp_failure_bad_port_ui(hass: HomeAssistant) -> None: bad_data = dataclasses.replace(TEST_SSDP_SERVICE_INFO) bad_data.ssdp_location = f"http://{TEST_HOST}:not_a_port/description.xml" - with patch( - "homeassistant.components.hyperion.client.HyperionClient", return_value=client - ), patch( - "homeassistant.components.hyperion.config_flow.client.generate_random_auth_id", - return_value=TEST_AUTH_ID, + with ( + patch( + "homeassistant.components.hyperion.client.HyperionClient", + return_value=client, + ), + patch( + "homeassistant.components.hyperion.config_flow.client.generate_random_auth_id", + return_value=TEST_AUTH_ID, + ), ): result = await _init_flow(hass, source=SOURCE_SSDP, data=bad_data) @@ -829,9 +854,13 @@ async def test_reauth_success(hass: HomeAssistant) -> None: client = create_mock_client() client.async_is_auth_required = AsyncMock(return_value=TEST_AUTH_REQUIRED_RESP) - with patch( - "homeassistant.components.hyperion.client.HyperionClient", return_value=client - ), patch("homeassistant.components.hyperion.async_setup_entry", return_value=True): + with ( + patch( + "homeassistant.components.hyperion.client.HyperionClient", + return_value=client, + ), + patch("homeassistant.components.hyperion.async_setup_entry", return_value=True), + ): result = await _init_flow( hass, source=SOURCE_REAUTH, diff --git a/tests/components/hyperion/test_light.py b/tests/components/hyperion/test_light.py index 4715441a5de..e1e7711e702 100644 --- a/tests/components/hyperion/test_light.py +++ b/tests/components/hyperion/test_light.py @@ -1,4 +1,5 @@ """Tests for the Hyperion integration.""" + from __future__ import annotations from unittest.mock import AsyncMock, Mock, call, patch @@ -338,6 +339,7 @@ async def test_light_async_turn_on(hass: HomeAssistant) -> None: const.KEY_PRIORITY: TEST_PRIORITY, const.KEY_COMPONENTID: const.KEY_COMPONENTID_COLOR, const.KEY_VALUE: {const.KEY_RGB: (0, 255, 255)}, + const.KEY_OWNER: "System", } ] @@ -432,6 +434,7 @@ async def test_light_async_turn_on(hass: HomeAssistant) -> None: const.KEY_PRIORITY: TEST_PRIORITY, const.KEY_COMPONENTID: const.KEY_COMPONENTID_COLOR, const.KEY_VALUE: {const.KEY_RGB: (0, 0, 255)}, + const.KEY_OWNER: "System", } ] call_registered_callback(client, "priorities-update") @@ -564,6 +567,8 @@ async def test_light_async_updates_from_hyperion_client( const.KEY_PRIORITY: TEST_PRIORITY, const.KEY_COMPONENTID: const.KEY_COMPONENTID_EFFECT, const.KEY_OWNER: effect, + const.KEY_VISIBLE: True, + const.KEY_ORIGIN: "System", } ] @@ -581,6 +586,9 @@ async def test_light_async_updates_from_hyperion_client( const.KEY_PRIORITY: TEST_PRIORITY, const.KEY_COMPONENTID: const.KEY_COMPONENTID_COLOR, const.KEY_VALUE: {const.KEY_RGB: rgb}, + const.KEY_VISIBLE: True, + const.KEY_ORIGIN: "System", + const.KEY_OWNER: "System", } ] @@ -625,6 +633,9 @@ async def test_light_async_updates_from_hyperion_client( const.KEY_PRIORITY: TEST_PRIORITY, const.KEY_COMPONENTID: const.KEY_COMPONENTID_COLOR, const.KEY_VALUE: {const.KEY_RGB: rgb}, + const.KEY_VISIBLE: True, + const.KEY_ORIGIN: "System", + const.KEY_OWNER: "System", } ] call_registered_callback(client, "client-update", {"loaded-state": True}) @@ -645,6 +656,7 @@ async def test_full_state_loaded_on_start(hass: HomeAssistant) -> None: const.KEY_PRIORITY: TEST_PRIORITY, const.KEY_COMPONENTID: const.KEY_COMPONENTID_COLOR, const.KEY_VALUE: {const.KEY_RGB: (0, 100, 100)}, + const.KEY_OWNER: "System", } ] client.effects = [{const.KEY_NAME: "One"}, {const.KEY_NAME: "Two"}] @@ -701,9 +713,13 @@ async def test_setup_entry_no_token_reauth(hass: HomeAssistant) -> None: config_entry = add_test_config_entry(hass) client.async_is_auth_required = AsyncMock(return_value=TEST_AUTH_REQUIRED_RESP) - with patch( - "homeassistant.components.hyperion.client.HyperionClient", return_value=client - ), patch.object(hass.config_entries.flow, "async_init") as mock_flow_init: + with ( + patch( + "homeassistant.components.hyperion.client.HyperionClient", + return_value=client, + ), + patch.object(hass.config_entries.flow, "async_init") as mock_flow_init, + ): assert not await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() assert client.async_client_disconnect.called @@ -731,9 +747,13 @@ async def test_setup_entry_bad_token_reauth(hass: HomeAssistant) -> None: # Fail to log in. client.async_client_login = AsyncMock(return_value=False) - with patch( - "homeassistant.components.hyperion.client.HyperionClient", return_value=client - ), patch.object(hass.config_entries.flow, "async_init") as mock_flow_init: + with ( + patch( + "homeassistant.components.hyperion.client.HyperionClient", + return_value=client, + ), + patch.object(hass.config_entries.flow, "async_init") as mock_flow_init, + ): assert not await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() assert client.async_client_disconnect.called diff --git a/tests/components/hyperion/test_sensor.py b/tests/components/hyperion/test_sensor.py new file mode 100644 index 00000000000..65991b4b7e1 --- /dev/null +++ b/tests/components/hyperion/test_sensor.py @@ -0,0 +1,178 @@ +"""Tests for the Hyperion integration.""" + +from hyperion.const import ( + KEY_ACTIVE, + KEY_COMPONENTID, + KEY_ORIGIN, + KEY_OWNER, + KEY_PRIORITY, + KEY_RGB, + KEY_VALUE, + KEY_VISIBLE, +) + +from homeassistant.components.hyperion import get_hyperion_device_id +from homeassistant.components.hyperion.const import ( + DOMAIN, + HYPERION_MANUFACTURER_NAME, + HYPERION_MODEL_NAME, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.util import slugify + +from . import ( + TEST_CONFIG_ENTRY_ID, + TEST_INSTANCE, + TEST_INSTANCE_1, + TEST_SYSINFO_ID, + call_registered_callback, + create_mock_client, + setup_test_config_entry, +) + +TEST_COMPONENTS = [ + {"enabled": True, "name": "VISIBLE_PRIORITY"}, +] + +TEST_SENSOR_BASE_ENTITY_ID = "sensor.test_instance_1" +TEST_VISIBLE_EFFECT_SENSOR_ID = "sensor.test_instance_1_visible_priority" + + +async def test_sensor_has_correct_entities(hass: HomeAssistant) -> None: + """Test that the correct sensor entities are created.""" + client = create_mock_client() + client.components = TEST_COMPONENTS + await setup_test_config_entry(hass, hyperion_client=client) + + for component in TEST_COMPONENTS: + name = slugify(component["name"]) + entity_id = f"{TEST_SENSOR_BASE_ENTITY_ID}_{name}" + entity_state = hass.states.get(entity_id) + assert entity_state, f"Couldn't find entity: {entity_id}" + + +async def test_device_info(hass: HomeAssistant) -> None: + """Verify device information includes expected details.""" + client = create_mock_client() + client.components = TEST_COMPONENTS + await setup_test_config_entry(hass, hyperion_client=client) + + device_identifer = get_hyperion_device_id(TEST_SYSINFO_ID, TEST_INSTANCE) + device_registry = dr.async_get(hass) + + device = device_registry.async_get_device(identifiers={(DOMAIN, device_identifer)}) + assert device + assert device.config_entries == {TEST_CONFIG_ENTRY_ID} + assert device.identifiers == {(DOMAIN, device_identifer)} + assert device.manufacturer == HYPERION_MANUFACTURER_NAME + assert device.model == HYPERION_MODEL_NAME + assert device.name == TEST_INSTANCE_1["friendly_name"] + + entity_registry = er.async_get(hass) + entities_from_device = [ + entry.entity_id + for entry in er.async_entries_for_device(entity_registry, device.id) + ] + + for component in TEST_COMPONENTS: + name = slugify(component["name"]) + entity_id = TEST_SENSOR_BASE_ENTITY_ID + "_" + name + assert entity_id in entities_from_device + + +async def test_visible_effect_state_changes(hass: HomeAssistant) -> None: + """Verify that state changes are processed as expected for visible effect sensor.""" + client = create_mock_client() + client.components = TEST_COMPONENTS + await setup_test_config_entry(hass, hyperion_client=client) + + # Simulate a platform grabber effect state callback from Hyperion. + client.priorities = [ + { + KEY_ACTIVE: True, + KEY_COMPONENTID: "GRABBER", + KEY_ORIGIN: "System", + KEY_OWNER: "X11", + KEY_PRIORITY: 250, + KEY_VISIBLE: True, + } + ] + + call_registered_callback(client, "priorities-update") + entity_state = hass.states.get(TEST_VISIBLE_EFFECT_SENSOR_ID) + assert entity_state + assert entity_state.state == client.priorities[0][KEY_OWNER] + assert ( + entity_state.attributes["component_id"] == client.priorities[0][KEY_COMPONENTID] + ) + assert entity_state.attributes["origin"] == client.priorities[0][KEY_ORIGIN] + assert entity_state.attributes["priority"] == client.priorities[0][KEY_PRIORITY] + + # Simulate an effect state callback from Hyperion. + client.priorities = [ + { + KEY_ACTIVE: True, + KEY_COMPONENTID: "EFFECT", + KEY_ORIGIN: "System", + KEY_OWNER: "Warm mood blobs", + KEY_PRIORITY: 250, + KEY_VISIBLE: True, + } + ] + + call_registered_callback(client, "priorities-update") + entity_state = hass.states.get(TEST_VISIBLE_EFFECT_SENSOR_ID) + assert entity_state + assert entity_state.state == client.priorities[0][KEY_OWNER] + assert ( + entity_state.attributes["component_id"] == client.priorities[0][KEY_COMPONENTID] + ) + assert entity_state.attributes["origin"] == client.priorities[0][KEY_ORIGIN] + assert entity_state.attributes["priority"] == client.priorities[0][KEY_PRIORITY] + + # Simulate a USB Capture state callback from Hyperion. + client.priorities = [ + { + KEY_ACTIVE: True, + KEY_COMPONENTID: "V4L", + KEY_ORIGIN: "System", + KEY_OWNER: "V4L2", + KEY_PRIORITY: 250, + KEY_VISIBLE: True, + } + ] + + call_registered_callback(client, "priorities-update") + entity_state = hass.states.get(TEST_VISIBLE_EFFECT_SENSOR_ID) + assert entity_state + assert entity_state.state == client.priorities[0][KEY_OWNER] + assert ( + entity_state.attributes["component_id"] == client.priorities[0][KEY_COMPONENTID] + ) + assert entity_state.attributes["origin"] == client.priorities[0][KEY_ORIGIN] + assert entity_state.attributes["priority"] == client.priorities[0][KEY_PRIORITY] + + # Simulate a color effect state callback from Hyperion. + client.priorities = [ + { + KEY_ACTIVE: True, + KEY_COMPONENTID: "COLOR", + KEY_ORIGIN: "System", + KEY_OWNER: "System", + KEY_PRIORITY: 250, + KEY_VALUE: {KEY_RGB: [0, 0, 0]}, + KEY_VISIBLE: True, + } + ] + + call_registered_callback(client, "priorities-update") + entity_state = hass.states.get(TEST_VISIBLE_EFFECT_SENSOR_ID) + assert entity_state + assert entity_state.state == str(client.priorities[0][KEY_VALUE][KEY_RGB]) + assert ( + entity_state.attributes["component_id"] == client.priorities[0][KEY_COMPONENTID] + ) + assert entity_state.attributes["origin"] == client.priorities[0][KEY_ORIGIN] + assert entity_state.attributes["priority"] == client.priorities[0][KEY_PRIORITY] + assert entity_state.attributes["color"] == client.priorities[0][KEY_VALUE] diff --git a/tests/components/hyperion/test_switch.py b/tests/components/hyperion/test_switch.py index 79b9454e29f..da458820c81 100644 --- a/tests/components/hyperion/test_switch.py +++ b/tests/components/hyperion/test_switch.py @@ -1,4 +1,5 @@ """Tests for the Hyperion integration.""" + from datetime import timedelta from unittest.mock import AsyncMock, call, patch diff --git a/tests/components/ialarm/test_config_flow.py b/tests/components/ialarm/test_config_flow.py index c242f360f30..816f03efa9e 100644 --- a/tests/components/ialarm/test_config_flow.py +++ b/tests/components/ialarm/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Antifurto365 iAlarm config flow.""" + from unittest.mock import patch from homeassistant import config_entries, data_entry_flow @@ -22,16 +23,20 @@ async def test_form(hass: HomeAssistant) -> None: assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None - with patch( - "homeassistant.components.ialarm.config_flow.IAlarm.get_status", - return_value=1, - ), patch( - "homeassistant.components.ialarm.config_flow.IAlarm.get_mac", - return_value=TEST_MAC, - ), patch( - "homeassistant.components.ialarm.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.ialarm.config_flow.IAlarm.get_status", + return_value=1, + ), + patch( + "homeassistant.components.ialarm.config_flow.IAlarm.get_mac", + return_value=TEST_MAC, + ), + patch( + "homeassistant.components.ialarm.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], TEST_DATA ) diff --git a/tests/components/ialarm/test_init.py b/tests/components/ialarm/test_init.py index ba44ae7a080..3600ce62fcd 100644 --- a/tests/components/ialarm/test_init.py +++ b/tests/components/ialarm/test_init.py @@ -1,4 +1,5 @@ """Test the Antifurto365 iAlarm init.""" + from unittest.mock import Mock, patch from uuid import uuid4 diff --git a/tests/components/iaqualink/conftest.py b/tests/components/iaqualink/conftest.py index b4db99dbe40..c7e7373f4c2 100644 --- a/tests/components/iaqualink/conftest.py +++ b/tests/components/iaqualink/conftest.py @@ -1,4 +1,5 @@ """Configuration for iAqualink tests.""" + import random from unittest.mock import AsyncMock, PropertyMock, patch diff --git a/tests/components/iaqualink/test_config_flow.py b/tests/components/iaqualink/test_config_flow.py index 7c43cf5307c..64a09e218c3 100644 --- a/tests/components/iaqualink/test_config_flow.py +++ b/tests/components/iaqualink/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for iAqualink config flow.""" + from unittest.mock import patch from iaqualink.exception import ( diff --git a/tests/components/iaqualink/test_init.py b/tests/components/iaqualink/test_init.py index a1e3ee6ac35..d450ced1fd7 100644 --- a/tests/components/iaqualink/test_init.py +++ b/tests/components/iaqualink/test_init.py @@ -1,4 +1,5 @@ """Tests for iAqualink integration.""" + import logging from unittest.mock import AsyncMock, patch @@ -67,12 +68,15 @@ async def test_setup_systems_exception(hass: HomeAssistant, config_entry) -> Non """Test setup encountering an exception while retrieving systems.""" config_entry.add_to_hass(hass) - with patch( - "homeassistant.components.iaqualink.AqualinkClient.login", - return_value=None, - ), patch( - "homeassistant.components.iaqualink.AqualinkClient.get_systems", - side_effect=AqualinkServiceException, + with ( + patch( + "homeassistant.components.iaqualink.AqualinkClient.login", + return_value=None, + ), + patch( + "homeassistant.components.iaqualink.AqualinkClient.get_systems", + side_effect=AqualinkServiceException, + ), ): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -84,12 +88,15 @@ async def test_setup_no_systems_recognized(hass: HomeAssistant, config_entry) -> """Test setup ending in no systems recognized.""" config_entry.add_to_hass(hass) - with patch( - "homeassistant.components.iaqualink.AqualinkClient.login", - return_value=None, - ), patch( - "homeassistant.components.iaqualink.AqualinkClient.get_systems", - return_value={}, + with ( + patch( + "homeassistant.components.iaqualink.AqualinkClient.login", + return_value=None, + ), + patch( + "homeassistant.components.iaqualink.AqualinkClient.get_systems", + return_value={}, + ), ): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -106,16 +113,20 @@ async def test_setup_devices_exception( system = get_aqualink_system(client, cls=IaquaSystem) systems = {system.serial: system} - with patch( - "homeassistant.components.iaqualink.AqualinkClient.login", - return_value=None, - ), patch( - "homeassistant.components.iaqualink.AqualinkClient.get_systems", - return_value=systems, - ), patch.object( - system, - "get_devices", - ) as mock_get_devices: + with ( + patch( + "homeassistant.components.iaqualink.AqualinkClient.login", + return_value=None, + ), + patch( + "homeassistant.components.iaqualink.AqualinkClient.get_systems", + return_value=systems, + ), + patch.object( + system, + "get_devices", + ) as mock_get_devices, + ): mock_get_devices.side_effect = AqualinkServiceException await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -135,16 +146,20 @@ async def test_setup_all_good_no_recognized_devices( device = get_aqualink_device(system, name="dev_1") devices = {device.name: device} - with patch( - "homeassistant.components.iaqualink.AqualinkClient.login", - return_value=None, - ), patch( - "homeassistant.components.iaqualink.AqualinkClient.get_systems", - return_value=systems, - ), patch.object( - system, - "get_devices", - ) as mock_get_devices: + with ( + patch( + "homeassistant.components.iaqualink.AqualinkClient.login", + return_value=None, + ), + patch( + "homeassistant.components.iaqualink.AqualinkClient.get_systems", + return_value=systems, + ), + patch.object( + system, + "get_devices", + ) as mock_get_devices, + ): mock_get_devices.return_value = devices await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -183,12 +198,15 @@ async def test_setup_all_good_all_device_types( system.get_devices = AsyncMock(return_value=devices) - with patch( - "homeassistant.components.iaqualink.AqualinkClient.login", - return_value=None, - ), patch( - "homeassistant.components.iaqualink.AqualinkClient.get_systems", - return_value=systems, + with ( + patch( + "homeassistant.components.iaqualink.AqualinkClient.login", + return_value=None, + ), + patch( + "homeassistant.components.iaqualink.AqualinkClient.get_systems", + return_value=systems, + ), ): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -220,12 +238,15 @@ async def test_multiple_updates( caplog.set_level(logging.WARNING) - with patch( - "homeassistant.components.iaqualink.AqualinkClient.login", - return_value=None, - ), patch( - "homeassistant.components.iaqualink.AqualinkClient.get_systems", - return_value=systems, + with ( + patch( + "homeassistant.components.iaqualink.AqualinkClient.login", + return_value=None, + ), + patch( + "homeassistant.components.iaqualink.AqualinkClient.get_systems", + return_value=systems, + ), ): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -329,12 +350,15 @@ async def test_entity_assumed_and_available( system.get_devices = AsyncMock(return_value=devices) system.update = AsyncMock() - with patch( - "homeassistant.components.iaqualink.AqualinkClient.login", - return_value=None, - ), patch( - "homeassistant.components.iaqualink.AqualinkClient.get_systems", - return_value=systems, + with ( + patch( + "homeassistant.components.iaqualink.AqualinkClient.login", + return_value=None, + ), + patch( + "homeassistant.components.iaqualink.AqualinkClient.get_systems", + return_value=systems, + ), ): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/iaqualink/test_utils.py b/tests/components/iaqualink/test_utils.py index b6aa3002f0e..c803fb48b09 100644 --- a/tests/components/iaqualink/test_utils.py +++ b/tests/components/iaqualink/test_utils.py @@ -1,4 +1,5 @@ """Tests for iAqualink integration utility functions.""" + from iaqualink.exception import AqualinkServiceException import pytest diff --git a/tests/components/ibeacon/__init__.py b/tests/components/ibeacon/__init__.py index a18a90f6c3d..b4aa04fd0bb 100644 --- a/tests/components/ibeacon/__init__.py +++ b/tests/components/ibeacon/__init__.py @@ -1,4 +1,5 @@ """Tests for the ibeacon integration.""" + from typing import Any from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo diff --git a/tests/components/ibeacon/test_config_flow.py b/tests/components/ibeacon/test_config_flow.py index 2f79474dea7..3a3e1d90d91 100644 --- a/tests/components/ibeacon/test_config_flow.py +++ b/tests/components/ibeacon/test_config_flow.py @@ -1,4 +1,5 @@ """Test the ibeacon config flow.""" + from unittest.mock import patch from homeassistant import config_entries diff --git a/tests/components/ibeacon/test_coordinator.py b/tests/components/ibeacon/test_coordinator.py index 372907307a7..0880f745ec2 100644 --- a/tests/components/ibeacon/test_coordinator.py +++ b/tests/components/ibeacon/test_coordinator.py @@ -1,4 +1,5 @@ """Test the ibeacon sensors.""" + from datetime import timedelta import time @@ -235,9 +236,12 @@ async def test_default_name_allowlisted_restore_late(hass: HomeAssistant) -> Non # Fastforward time until the device is no longer advertised monotonic_now = start_monotonic + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1 - with patch_bluetooth_time( - monotonic_now, - ), patch_all_discovered_devices([]): + with ( + patch_bluetooth_time( + monotonic_now, + ), + patch_all_discovered_devices([]), + ): async_fire_time_changed( hass, dt_util.utcnow() diff --git a/tests/components/ibeacon/test_device_tracker.py b/tests/components/ibeacon/test_device_tracker.py index 3b8268cee60..77f8271370e 100644 --- a/tests/components/ibeacon/test_device_tracker.py +++ b/tests/components/ibeacon/test_device_tracker.py @@ -1,4 +1,5 @@ """Test the ibeacon device trackers.""" + from datetime import timedelta import time from unittest.mock import patch @@ -103,9 +104,12 @@ async def test_device_tracker_random_address(hass: HomeAssistant) -> None: assert tracker_attributes[ATTR_FRIENDLY_NAME] == "RandomAddress_1234" await hass.async_block_till_done() - with patch_all_discovered_devices([]), patch( - "homeassistant.components.ibeacon.coordinator.MONOTONIC_TIME", - return_value=start_time + UNAVAILABLE_TIMEOUT + 1, + with ( + patch_all_discovered_devices([]), + patch( + "homeassistant.components.ibeacon.coordinator.MONOTONIC_TIME", + return_value=start_time + UNAVAILABLE_TIMEOUT + 1, + ), ): async_fire_time_changed( hass, dt_util.utcnow() + timedelta(seconds=UNAVAILABLE_TIMEOUT) @@ -168,9 +172,12 @@ async def test_device_tracker_random_address_infrequent_changes( assert tracker_attributes[ATTR_FRIENDLY_NAME] == "RandomAddress_1234" await hass.async_block_till_done() - with patch_all_discovered_devices([]), patch( - "homeassistant.components.ibeacon.coordinator.MONOTONIC_TIME", - return_value=start_time + UNAVAILABLE_TIMEOUT + 1, + with ( + patch_all_discovered_devices([]), + patch( + "homeassistant.components.ibeacon.coordinator.MONOTONIC_TIME", + return_value=start_time + UNAVAILABLE_TIMEOUT + 1, + ), ): async_fire_time_changed( hass, dt_util.utcnow() + timedelta(seconds=UNAVAILABLE_TIMEOUT) @@ -195,9 +202,12 @@ async def test_device_tracker_random_address_infrequent_changes( ) device = async_ble_device_from_address(hass, "AA:BB:CC:DD:EE:14", False) - with patch_all_discovered_devices([device]), patch( - "homeassistant.components.ibeacon.coordinator.MONOTONIC_TIME", - return_value=start_time + UPDATE_INTERVAL.total_seconds() + 1, + with ( + patch_all_discovered_devices([device]), + patch( + "homeassistant.components.ibeacon.coordinator.MONOTONIC_TIME", + return_value=start_time + UPDATE_INTERVAL.total_seconds() + 1, + ), ): async_fire_time_changed(hass, dt_util.utcnow() + UPDATE_INTERVAL) await hass.async_block_till_done() @@ -233,9 +243,12 @@ async def test_device_tracker_random_address_infrequent_changes( == one_day_future ) - with patch_all_discovered_devices([device]), patch( - "homeassistant.components.ibeacon.coordinator.MONOTONIC_TIME", - return_value=start_time + UNAVAILABLE_TIMEOUT + 1, + with ( + patch_all_discovered_devices([device]), + patch( + "homeassistant.components.ibeacon.coordinator.MONOTONIC_TIME", + return_value=start_time + UNAVAILABLE_TIMEOUT + 1, + ), ): async_fire_time_changed( hass, dt_util.utcnow() + timedelta(seconds=UNAVAILABLE_TIMEOUT + 1) diff --git a/tests/components/ibeacon/test_init.py b/tests/components/ibeacon/test_init.py index b29cc3a4b2e..99c45b3dfe7 100644 --- a/tests/components/ibeacon/test_init.py +++ b/tests/components/ibeacon/test_init.py @@ -1,4 +1,5 @@ """Test the ibeacon init.""" + import pytest from homeassistant.components.ibeacon.const import DOMAIN diff --git a/tests/components/ibeacon/test_sensor.py b/tests/components/ibeacon/test_sensor.py index 30a50305d2d..fb6322162d4 100644 --- a/tests/components/ibeacon/test_sensor.py +++ b/tests/components/ibeacon/test_sensor.py @@ -1,4 +1,5 @@ """Test the ibeacon sensors.""" + from datetime import timedelta import pytest diff --git a/tests/components/icloud/conftest.py b/tests/components/icloud/conftest.py index 2437d05f575..e9d5c0e5620 100644 --- a/tests/components/icloud/conftest.py +++ b/tests/components/icloud/conftest.py @@ -1,4 +1,5 @@ """Configure iCloud tests.""" + from unittest.mock import patch import pytest diff --git a/tests/components/icloud/const.py b/tests/components/icloud/const.py index 459f18e17cc..463ae6a7da2 100644 --- a/tests/components/icloud/const.py +++ b/tests/components/icloud/const.py @@ -1,4 +1,5 @@ """Constants for the iCloud tests.""" + from homeassistant.components.icloud.const import ( CONF_GPS_ACCURACY_THRESHOLD, CONF_MAX_INTERVAL, diff --git a/tests/components/icloud/test_config_flow.py b/tests/components/icloud/test_config_flow.py index c9e3129f865..f13a0e14595 100644 --- a/tests/components/icloud/test_config_flow.py +++ b/tests/components/icloud/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for the iCloud config flow.""" + from unittest.mock import MagicMock, Mock, patch from pyicloud.exceptions import PyiCloudFailedLoginException diff --git a/tests/components/icloud/test_init.py b/tests/components/icloud/test_init.py index 60ab00a6262..4423fd598a4 100644 --- a/tests/components/icloud/test_init.py +++ b/tests/components/icloud/test_init.py @@ -1,4 +1,5 @@ """Tests for the iCloud config flow.""" + from unittest.mock import Mock, patch import pytest diff --git a/tests/components/idasen_desk/test_buttons.py b/tests/components/idasen_desk/test_buttons.py index d576b2fe580..47d37a90ece 100644 --- a/tests/components/idasen_desk/test_buttons.py +++ b/tests/components/idasen_desk/test_buttons.py @@ -1,4 +1,5 @@ """Test the IKEA Idasen Desk connection buttons.""" + from unittest.mock import MagicMock from homeassistant.core import HomeAssistant diff --git a/tests/components/idasen_desk/test_config_flow.py b/tests/components/idasen_desk/test_config_flow.py index ca585c65e4d..78eacfb6942 100644 --- a/tests/components/idasen_desk/test_config_flow.py +++ b/tests/components/idasen_desk/test_config_flow.py @@ -1,4 +1,5 @@ """Test the IKEA Idasen Desk config flow.""" + from unittest.mock import ANY, patch from bleak.exc import BleakError @@ -29,12 +30,14 @@ async def test_user_step_success(hass: HomeAssistant) -> None: assert result["step_id"] == "user" assert result["errors"] == {} - with patch("homeassistant.components.idasen_desk.config_flow.Desk.connect"), patch( - "homeassistant.components.idasen_desk.config_flow.Desk.disconnect" - ), patch( - "homeassistant.components.idasen_desk.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch("homeassistant.components.idasen_desk.config_flow.Desk.connect"), + patch("homeassistant.components.idasen_desk.config_flow.Desk.disconnect"), + patch( + "homeassistant.components.idasen_desk.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -102,10 +105,13 @@ async def test_user_step_cannot_connect( assert result["step_id"] == "user" assert result["errors"] == {} - with patch( - "homeassistant.components.idasen_desk.config_flow.Desk.connect", - side_effect=exception, - ), patch("homeassistant.components.idasen_desk.config_flow.Desk.disconnect"): + with ( + patch( + "homeassistant.components.idasen_desk.config_flow.Desk.connect", + side_effect=exception, + ), + patch("homeassistant.components.idasen_desk.config_flow.Desk.disconnect"), + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -118,12 +124,14 @@ async def test_user_step_cannot_connect( assert result2["step_id"] == "user" assert result2["errors"] == {"base": "cannot_connect"} - with patch("homeassistant.components.idasen_desk.config_flow.Desk.connect"), patch( - "homeassistant.components.idasen_desk.config_flow.Desk.disconnect" - ), patch( - "homeassistant.components.idasen_desk.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch("homeassistant.components.idasen_desk.config_flow.Desk.connect"), + patch("homeassistant.components.idasen_desk.config_flow.Desk.disconnect"), + patch( + "homeassistant.components.idasen_desk.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], { @@ -154,10 +162,13 @@ async def test_user_step_auth_failed(hass: HomeAssistant) -> None: assert result["step_id"] == "user" assert result["errors"] == {} - with patch( - "homeassistant.components.idasen_desk.config_flow.Desk.connect", - side_effect=AuthFailedError, - ), patch("homeassistant.components.idasen_desk.config_flow.Desk.disconnect"): + with ( + patch( + "homeassistant.components.idasen_desk.config_flow.Desk.connect", + side_effect=AuthFailedError, + ), + patch("homeassistant.components.idasen_desk.config_flow.Desk.disconnect"), + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -170,12 +181,14 @@ async def test_user_step_auth_failed(hass: HomeAssistant) -> None: assert result2["step_id"] == "user" assert result2["errors"] == {"base": "auth_failed"} - with patch("homeassistant.components.idasen_desk.config_flow.Desk.connect"), patch( - "homeassistant.components.idasen_desk.config_flow.Desk.disconnect" - ), patch( - "homeassistant.components.idasen_desk.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch("homeassistant.components.idasen_desk.config_flow.Desk.connect"), + patch("homeassistant.components.idasen_desk.config_flow.Desk.disconnect"), + patch( + "homeassistant.components.idasen_desk.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], { @@ -206,11 +219,14 @@ async def test_user_step_unknown_exception(hass: HomeAssistant) -> None: assert result["step_id"] == "user" assert result["errors"] == {} - with patch( - "homeassistant.components.idasen_desk.config_flow.Desk.connect", - side_effect=RuntimeError, - ), patch( - "homeassistant.components.idasen_desk.config_flow.Desk.disconnect", + with ( + patch( + "homeassistant.components.idasen_desk.config_flow.Desk.connect", + side_effect=RuntimeError, + ), + patch( + "homeassistant.components.idasen_desk.config_flow.Desk.disconnect", + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -224,14 +240,18 @@ async def test_user_step_unknown_exception(hass: HomeAssistant) -> None: assert result2["step_id"] == "user" assert result2["errors"] == {"base": "unknown"} - with patch( - "homeassistant.components.idasen_desk.config_flow.Desk.connect", - ), patch( - "homeassistant.components.idasen_desk.config_flow.Desk.disconnect", - ), patch( - "homeassistant.components.idasen_desk.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.idasen_desk.config_flow.Desk.connect", + ), + patch( + "homeassistant.components.idasen_desk.config_flow.Desk.disconnect", + ), + patch( + "homeassistant.components.idasen_desk.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], { @@ -260,14 +280,16 @@ async def test_bluetooth_step_success(hass: HomeAssistant) -> None: assert result["step_id"] == "user" assert result["errors"] == {} - with patch( - "homeassistant.components.idasen_desk.config_flow.Desk.connect" - ) as desk_connect, patch( - "homeassistant.components.idasen_desk.config_flow.Desk.disconnect" - ), patch( - "homeassistant.components.idasen_desk.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.idasen_desk.config_flow.Desk.connect" + ) as desk_connect, + patch("homeassistant.components.idasen_desk.config_flow.Desk.disconnect"), + patch( + "homeassistant.components.idasen_desk.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { diff --git a/tests/components/idasen_desk/test_cover.py b/tests/components/idasen_desk/test_cover.py index 4c8bf7806e0..3c18d604549 100644 --- a/tests/components/idasen_desk/test_cover.py +++ b/tests/components/idasen_desk/test_cover.py @@ -1,4 +1,5 @@ """Test the IKEA Idasen Desk cover.""" + from typing import Any from unittest.mock import MagicMock diff --git a/tests/components/idasen_desk/test_init.py b/tests/components/idasen_desk/test_init.py index cc8daaf98ea..5b8258c8d33 100644 --- a/tests/components/idasen_desk/test_init.py +++ b/tests/components/idasen_desk/test_init.py @@ -1,4 +1,5 @@ """Test the IKEA Idasen Desk init.""" + from unittest import mock from unittest.mock import AsyncMock, MagicMock diff --git a/tests/components/idasen_desk/test_sensors.py b/tests/components/idasen_desk/test_sensors.py index 23d7ac2447b..f56a45104eb 100644 --- a/tests/components/idasen_desk/test_sensors.py +++ b/tests/components/idasen_desk/test_sensors.py @@ -1,4 +1,5 @@ """Test the IKEA Idasen Desk sensors.""" + from unittest.mock import MagicMock from homeassistant.core import HomeAssistant diff --git a/tests/components/ifttt/test_init.py b/tests/components/ifttt/test_init.py index 8e38e683914..71bd2bc297f 100644 --- a/tests/components/ifttt/test_init.py +++ b/tests/components/ifttt/test_init.py @@ -1,4 +1,5 @@ """Test the init file of IFTTT.""" + from homeassistant import config_entries, data_entry_flow from homeassistant.components import ifttt from homeassistant.config import async_process_ha_core_config diff --git a/tests/components/ign_sismologia/test_geo_location.py b/tests/components/ign_sismologia/test_geo_location.py index fd3c34a506c..c26eae28086 100644 --- a/tests/components/ign_sismologia/test_geo_location.py +++ b/tests/components/ign_sismologia/test_geo_location.py @@ -1,4 +1,5 @@ """The tests for the IGN Sismologia (Earthquakes) Feed platform.""" + import datetime from unittest.mock import MagicMock, call, patch @@ -176,7 +177,7 @@ async def test_setup(hass: HomeAssistant, freezer: FrozenDateTimeFactory) -> Non [mock_entry_1, mock_entry_4, mock_entry_3], ) async_fire_time_changed(hass, utcnow + SCAN_INTERVAL) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) all_states = hass.states.async_all() assert len(all_states) == 3 @@ -185,7 +186,7 @@ async def test_setup(hass: HomeAssistant, freezer: FrozenDateTimeFactory) -> Non # so no changes to entities. mock_feed_update.return_value = "OK_NO_DATA", None async_fire_time_changed(hass, utcnow + 2 * SCAN_INTERVAL) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) all_states = hass.states.async_all() assert len(all_states) == 3 @@ -193,7 +194,7 @@ async def test_setup(hass: HomeAssistant, freezer: FrozenDateTimeFactory) -> Non # Simulate an update - empty data, removes all entities mock_feed_update.return_value = "ERROR", None async_fire_time_changed(hass, utcnow + 3 * SCAN_INTERVAL) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) all_states = hass.states.async_all() assert len(all_states) == 0 @@ -204,10 +205,13 @@ async def test_setup_with_custom_location(hass: HomeAssistant) -> None: # Set up some mock feed entries for this test. mock_entry_1 = _generate_mock_feed_entry("1234", "Title 1", 20.5, (38.1, -3.1)) - with patch( - "georss_ign_sismologia_client.feed_manager.IgnSismologiaFeed", - wraps=IgnSismologiaFeed, - ) as mock_feed, patch("georss_client.feed.GeoRssFeed.update") as mock_feed_update: + with ( + patch( + "georss_ign_sismologia_client.feed_manager.IgnSismologiaFeed", + wraps=IgnSismologiaFeed, + ) as mock_feed, + patch("georss_client.feed.GeoRssFeed.update") as mock_feed_update, + ): mock_feed_update.return_value = "OK", [mock_entry_1] with assert_setup_component(1, geo_location.DOMAIN): diff --git a/tests/components/image/conftest.py b/tests/components/image/conftest.py index 6c9deea852f..35c9f0a86af 100644 --- a/tests/components/image/conftest.py +++ b/tests/components/image/conftest.py @@ -1,4 +1,5 @@ """Test helpers for image.""" + from collections.abc import Generator import pytest diff --git a/tests/components/image/test_init.py b/tests/components/image/test_init.py index e68a58d7298..717e82a652d 100644 --- a/tests/components/image/test_init.py +++ b/tests/components/image/test_init.py @@ -1,10 +1,12 @@ """The tests for the image component.""" -import datetime + +from datetime import datetime from http import HTTPStatus import ssl from unittest.mock import MagicMock, patch from aiohttp import hdrs +from freezegun.api import FrozenDateTimeFactory import httpx import pytest import respx @@ -23,7 +25,12 @@ from .conftest import ( MockURLImageEntity, ) -from tests.common import MockModule, mock_integration, mock_platform +from tests.common import ( + MockModule, + async_fire_time_changed, + mock_integration, + mock_platform, +) from tests.typing import ClientSessionGenerator @@ -291,7 +298,9 @@ async def test_fetch_image_url_wrong_content_type( async def test_image_stream( - hass: HomeAssistant, hass_client: ClientSessionGenerator + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + freezer: FrozenDateTimeFactory, ) -> None: """Test image stream.""" @@ -305,22 +314,43 @@ async def test_image_stream( client = await hass_client() - with patch.object(mock_image, "async_image", return_value=b""): - resp = await client.get("/api/image_proxy_stream/image.test") - assert not resp.closed - assert resp.status == HTTPStatus.OK + close_future = hass.loop.create_future() + original_get_still_stream = image.async_get_still_stream - mock_image.image_last_updated = datetime.datetime.now() - mock_image.async_write_ha_state() - # Two blocks to ensure the frame is written - await hass.async_block_till_done() - await hass.async_block_till_done() + async def _wrap_async_get_still_stream(*args, **kwargs): + result = await original_get_still_stream(*args, **kwargs) + hass.loop.call_soon(close_future.set_result, None) + return result - with patch.object(mock_image, "async_image", return_value=None): - mock_image.image_last_updated = datetime.datetime.now() - mock_image.async_write_ha_state() - # Two blocks to ensure the frame is written - await hass.async_block_till_done() - await hass.async_block_till_done() + with patch( + "homeassistant.components.image.async_get_still_stream", + _wrap_async_get_still_stream, + ): + with patch.object(mock_image, "async_image", return_value=b""): + resp = await client.get("/api/image_proxy_stream/image.test") + assert not resp.closed + assert resp.status == HTTPStatus.OK - assert resp.closed + mock_image.image_last_updated = datetime.now() + mock_image.async_write_ha_state() + # Two blocks to ensure the frame is written + await hass.async_block_till_done() + await hass.async_block_till_done() + + with patch.object(mock_image, "async_image", return_value=b"") as mock: + # Simulate a "keep alive" frame + freezer.tick(55) + async_fire_time_changed(hass) + # Two blocks to ensure the frame is written + await hass.async_block_till_done() + await hass.async_block_till_done() + mock.assert_called_once() + + with patch.object(mock_image, "async_image", return_value=None): + freezer.tick(55) + async_fire_time_changed(hass) + # Two blocks to ensure the frame is written + await hass.async_block_till_done() + await hass.async_block_till_done() + + await close_future diff --git a/tests/components/image/test_recorder.py b/tests/components/image/test_recorder.py index f0ecc43e6dc..18258d3819a 100644 --- a/tests/components/image/test_recorder.py +++ b/tests/components/image/test_recorder.py @@ -1,4 +1,5 @@ """The tests for image recorder.""" + from __future__ import annotations from datetime import timedelta diff --git a/tests/components/image_processing/common.py b/tests/components/image_processing/common.py index 8522353d3f2..4b3a008c6cd 100644 --- a/tests/components/image_processing/common.py +++ b/tests/components/image_processing/common.py @@ -3,6 +3,7 @@ All containing methods are legacy helpers that should not be used by new components. Instead call the service directly. """ + from homeassistant.components.image_processing import DOMAIN, SERVICE_SCAN from homeassistant.const import ATTR_ENTITY_ID, ENTITY_MATCH_ALL from homeassistant.core import callback @@ -20,4 +21,4 @@ def scan(hass, entity_id=ENTITY_MATCH_ALL): def async_scan(hass, entity_id=ENTITY_MATCH_ALL): """Force process of all cameras or given entity.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_SCAN, data)) + hass.async_create_task(hass.services.async_call(DOMAIN, SERVICE_SCAN, data)) diff --git a/tests/components/image_processing/test_init.py b/tests/components/image_processing/test_init.py index 40b4c47a3c9..6415e4e2a4e 100644 --- a/tests/components/image_processing/test_init.py +++ b/tests/components/image_processing/test_init.py @@ -1,4 +1,5 @@ """The tests for the image_processing component.""" + from unittest.mock import PropertyMock, patch import pytest @@ -102,7 +103,7 @@ async def test_get_image_from_camera( @patch( - "homeassistant.components.camera.async_get_image", + "homeassistant.components.image_processing.async_get_image", side_effect=HomeAssistantError(), ) async def test_get_image_without_exists_camera( @@ -179,3 +180,22 @@ async def test_face_event_call_no_confidence( assert event_data[0]["confidence"] == 98.34 assert event_data[0]["gender"] == "male" assert event_data[0]["entity_id"] == "image_processing.demo_face" + + +async def test_update_missing_camera( + hass: HomeAssistant, + aiohttp_unused_port_factory, + enable_custom_integrations: None, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test when entity does not set camera.""" + await setup_image_processing(hass, aiohttp_unused_port_factory) + + with patch( + "custom_components.test.image_processing.TestImageProcessing.camera_entity", + new_callable=PropertyMock(return_value=None), + ): + common.async_scan(hass, entity_id="image_processing.test") + await hass.async_block_till_done() + + assert "No camera entity id was set by the image processing entity" in caplog.text diff --git a/tests/components/image_upload/__init__.py b/tests/components/image_upload/__init__.py index 94acd4a5485..34064e0d40a 100644 --- a/tests/components/image_upload/__init__.py +++ b/tests/components/image_upload/__init__.py @@ -1,4 +1,5 @@ """Tests for the Image Upload integration.""" + import pathlib TEST_IMAGE = pathlib.Path(__file__).parent / "logo.png" diff --git a/tests/components/image_upload/test_init.py b/tests/components/image_upload/test_init.py index 9f842d25b64..1117befc7fd 100644 --- a/tests/components/image_upload/test_init.py +++ b/tests/components/image_upload/test_init.py @@ -1,4 +1,5 @@ """Test that we can upload images.""" + import pathlib import tempfile from unittest.mock import patch @@ -26,8 +27,9 @@ async def test_upload_image( now = dt_util.utcnow() freezer.move_to(now) - with tempfile.TemporaryDirectory() as tempdir, patch.object( - hass.config, "path", return_value=tempdir + with ( + tempfile.TemporaryDirectory() as tempdir, + patch.object(hass.config, "path", return_value=tempdir), ): assert await async_setup_component(hass, "image_upload", {}) ws_client: ClientWebSocketResponse = await hass_ws_client() diff --git a/tests/components/imap/const.py b/tests/components/imap/const.py index 713261936c7..677eea7a473 100644 --- a/tests/components/imap/const.py +++ b/tests/components/imap/const.py @@ -1,6 +1,5 @@ """Constants for tests imap integration.""" - DATE_HEADER1 = b"Date: Fri, 24 Mar 2023 13:52:00 +0100\r\n" DATE_HEADER2 = b"Date: Fri, 24 Mar 2023 13:52:00 +0100 (CET)\r\n" DATE_HEADER3 = b"Date: 24 Mar 2023 13:52:00 +0100\r\n" @@ -102,7 +101,7 @@ TEST_CONTENT_HTML_BASE64 = ( TEST_CONTENT_MULTIPART = ( b"\r\nThis is a multi-part message in MIME format.\r\n" - + b"\r\n--Mark=_100584970350292485166\r\n" + b"\r\n--Mark=_100584970350292485166\r\n" + TEST_CONTENT_TEXT_PLAIN + b"\r\n--Mark=_100584970350292485166\r\n" + TEST_CONTENT_HTML @@ -111,7 +110,7 @@ TEST_CONTENT_MULTIPART = ( TEST_CONTENT_MULTIPART_BASE64 = ( b"\r\nThis is a multi-part message in MIME format.\r\n" - + b"\r\n--Mark=_100584970350292485166\r\n" + b"\r\n--Mark=_100584970350292485166\r\n" + TEST_CONTENT_TEXT_BASE64 + b"\r\n--Mark=_100584970350292485166\r\n" + TEST_CONTENT_HTML_BASE64 @@ -120,7 +119,7 @@ TEST_CONTENT_MULTIPART_BASE64 = ( TEST_CONTENT_MULTIPART_BASE64_INVALID = ( b"\r\nThis is a multi-part message in MIME format.\r\n" - + b"\r\n--Mark=_100584970350292485166\r\n" + b"\r\n--Mark=_100584970350292485166\r\n" + TEST_CONTENT_TEXT_BASE64_INVALID + b"\r\n--Mark=_100584970350292485166\r\n" + TEST_CONTENT_HTML_BASE64 diff --git a/tests/components/imap/test_config_flow.py b/tests/components/imap/test_config_flow.py index 177aba04950..9d9edae5b14 100644 --- a/tests/components/imap/test_config_flow.py +++ b/tests/components/imap/test_config_flow.py @@ -1,4 +1,5 @@ """Test the imap config flow.""" + import ssl from unittest.mock import AsyncMock, patch diff --git a/tests/components/imap/test_diagnostics.py b/tests/components/imap/test_diagnostics.py index 68b6831fa5b..79d51b73401 100644 --- a/tests/components/imap/test_diagnostics.py +++ b/tests/components/imap/test_diagnostics.py @@ -1,4 +1,5 @@ """Test IMAP diagnostics.""" + from datetime import timedelta from typing import Any from unittest.mock import MagicMock diff --git a/tests/components/imap/test_init.py b/tests/components/imap/test_init.py index 9c194bf08a0..8608963413a 100644 --- a/tests/components/imap/test_init.py +++ b/tests/components/imap/test_init.py @@ -1,4 +1,5 @@ """Test the imap entry initialization.""" + import asyncio from datetime import datetime, timedelta, timezone from typing import Any diff --git a/tests/components/improv_ble/test_config_flow.py b/tests/components/improv_ble/test_config_flow.py index d5e5e0c33ee..bafc32907ab 100644 --- a/tests/components/improv_ble/test_config_flow.py +++ b/tests/components/improv_ble/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Improv via BLE config flow.""" + from collections.abc import Callable from unittest.mock import patch @@ -252,13 +253,16 @@ async def _test_common_success( ) -> None: """Test bluetooth and user flow success paths.""" - with patch( - f"{IMPROV_BLE}.config_flow.ImprovBLEClient.need_authorization", - return_value=False, - ), patch( - f"{IMPROV_BLE}.config_flow.ImprovBLEClient.provision", - return_value=url, - ) as mock_provision: + with ( + patch( + f"{IMPROV_BLE}.config_flow.ImprovBLEClient.need_authorization", + return_value=False, + ), + patch( + f"{IMPROV_BLE}.config_flow.ImprovBLEClient.provision", + return_value=url, + ) as mock_provision, + ): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"ssid": "MyWIFI", "password": "secret"} ) @@ -304,13 +308,16 @@ async def _test_common_success_w_authorize( state_callback(State.AUTHORIZED) return lambda: None - with patch( - f"{IMPROV_BLE}.config_flow.ImprovBLEClient.need_authorization", - return_value=True, - ), patch( - f"{IMPROV_BLE}.config_flow.ImprovBLEClient.subscribe_state_updates", - side_effect=subscribe_state_updates, - ) as mock_subscribe_state_updates: + with ( + patch( + f"{IMPROV_BLE}.config_flow.ImprovBLEClient.need_authorization", + return_value=True, + ), + patch( + f"{IMPROV_BLE}.config_flow.ImprovBLEClient.subscribe_state_updates", + side_effect=subscribe_state_updates, + ) as mock_subscribe_state_updates, + ): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"ssid": "MyWIFI", "password": "secret"} ) @@ -320,13 +327,16 @@ async def _test_common_success_w_authorize( mock_subscribe_state_updates.assert_awaited_once() await hass.async_block_till_done() - with patch( - f"{IMPROV_BLE}.config_flow.ImprovBLEClient.need_authorization", - return_value=False, - ), patch( - f"{IMPROV_BLE}.config_flow.ImprovBLEClient.provision", - return_value="http://blabla.local", - ) as mock_provision: + with ( + patch( + f"{IMPROV_BLE}.config_flow.ImprovBLEClient.need_authorization", + return_value=False, + ), + patch( + f"{IMPROV_BLE}.config_flow.ImprovBLEClient.provision", + return_value="http://blabla.local", + ) as mock_provision, + ): result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == FlowResultType.SHOW_PROGRESS assert result["progress_action"] == "provisioning" @@ -362,11 +372,11 @@ async def test_bluetooth_step_already_in_progress(hass: HomeAssistant) -> None: @pytest.mark.parametrize( ("exc", "error"), - ( + [ (BleakError, "cannot_connect"), (Exception, "unknown"), (improv_ble_errors.CharacteristicMissingError, "characteristic_missing"), - ), + ], ) async def test_can_identify_fails(hass: HomeAssistant, exc, error) -> None: """Test bluetooth flow with error.""" @@ -397,11 +407,11 @@ async def test_can_identify_fails(hass: HomeAssistant, exc, error) -> None: @pytest.mark.parametrize( ("exc", "error"), - ( + [ (BleakError, "cannot_connect"), (Exception, "unknown"), (improv_ble_errors.CharacteristicMissingError, "characteristic_missing"), - ), + ], ) async def test_identify_fails(hass: HomeAssistant, exc, error) -> None: """Test bluetooth flow with error.""" @@ -440,11 +450,11 @@ async def test_identify_fails(hass: HomeAssistant, exc, error) -> None: @pytest.mark.parametrize( ("exc", "error"), - ( + [ (BleakError, "cannot_connect"), (Exception, "unknown"), (improv_ble_errors.CharacteristicMissingError, "characteristic_missing"), - ), + ], ) async def test_need_authorization_fails(hass: HomeAssistant, exc, error) -> None: """Test bluetooth flow with error.""" @@ -484,11 +494,11 @@ async def test_need_authorization_fails(hass: HomeAssistant, exc, error) -> None @pytest.mark.parametrize( ("exc", "error"), - ( + [ (BleakError, "cannot_connect"), (Exception, "unknown"), (improv_ble_errors.CharacteristicMissingError, "characteristic_missing"), - ), + ], ) async def test_authorize_fails(hass: HomeAssistant, exc, error) -> None: """Test bluetooth flow with error.""" @@ -516,12 +526,15 @@ async def test_authorize_fails(hass: HomeAssistant, exc, error) -> None: assert result["type"] == FlowResultType.FORM assert result["step_id"] == "provision" - with patch( - f"{IMPROV_BLE}.config_flow.ImprovBLEClient.need_authorization", - return_value=True, - ), patch( - f"{IMPROV_BLE}.config_flow.ImprovBLEClient.subscribe_state_updates", - side_effect=exc, + with ( + patch( + f"{IMPROV_BLE}.config_flow.ImprovBLEClient.need_authorization", + return_value=True, + ), + patch( + f"{IMPROV_BLE}.config_flow.ImprovBLEClient.subscribe_state_updates", + side_effect=exc, + ), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"ssid": "MyWIFI", "password": "secret"} @@ -556,12 +569,15 @@ async def _test_provision_error(hass: HomeAssistant, exc) -> None: assert result["type"] == FlowResultType.FORM assert result["step_id"] == "provision" - with patch( - f"{IMPROV_BLE}.config_flow.ImprovBLEClient.need_authorization", - return_value=False, - ), patch( - f"{IMPROV_BLE}.config_flow.ImprovBLEClient.provision", - side_effect=exc, + with ( + patch( + f"{IMPROV_BLE}.config_flow.ImprovBLEClient.need_authorization", + return_value=False, + ), + patch( + f"{IMPROV_BLE}.config_flow.ImprovBLEClient.provision", + side_effect=exc, + ), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"ssid": "MyWIFI", "password": "secret"} @@ -576,12 +592,12 @@ async def _test_provision_error(hass: HomeAssistant, exc) -> None: @pytest.mark.parametrize( ("exc", "error"), - ( + [ (BleakError, "cannot_connect"), (Exception, "unknown"), (improv_ble_errors.CharacteristicMissingError, "characteristic_missing"), (improv_ble_errors.ProvisioningFailed(Error.UNKNOWN_ERROR), "unknown"), - ), + ], ) async def test_provision_fails(hass: HomeAssistant, exc, error) -> None: """Test bluetooth flow with error.""" @@ -594,7 +610,7 @@ async def test_provision_fails(hass: HomeAssistant, exc, error) -> None: @pytest.mark.parametrize( ("exc", "error"), - ((improv_ble_errors.ProvisioningFailed(Error.NOT_AUTHORIZED), "unknown"),), + [(improv_ble_errors.ProvisioningFailed(Error.NOT_AUTHORIZED), "unknown")], ) async def test_provision_not_authorized(hass: HomeAssistant, exc, error) -> None: """Test bluetooth flow with error.""" @@ -618,12 +634,12 @@ async def test_provision_not_authorized(hass: HomeAssistant, exc, error) -> None @pytest.mark.parametrize( ("exc", "error"), - ( + [ ( improv_ble_errors.ProvisioningFailed(Error.UNABLE_TO_CONNECT), "unable_to_connect", ), - ), + ], ) async def test_provision_retry(hass: HomeAssistant, exc, error) -> None: """Test bluetooth flow with error.""" diff --git a/tests/components/influxdb/test_init.py b/tests/components/influxdb/test_init.py index b6d68714af5..41f14aa78e3 100644 --- a/tests/components/influxdb/test_init.py +++ b/tests/components/influxdb/test_init.py @@ -1,4 +1,5 @@ """The tests for the InfluxDB component.""" + from dataclasses import dataclass import datetime from http import HTTPStatus @@ -22,6 +23,15 @@ BASE_V2_CONFIG = { } +async def async_wait_for_queue_to_process(hass: HomeAssistant) -> None: + """Wait for the queue to be processed. + + In the future we should refactor this away to not have + to access hass.data directly. + """ + await hass.async_add_executor_job(hass.data[influxdb.DOMAIN].block_till_done) + + @dataclass class FilterTest: """Class for capturing a filter test.""" @@ -248,8 +258,9 @@ async def test_setup_config_ssl( config = {"influxdb": config_base.copy()} config["influxdb"].update(config_ext) - with patch("os.access", return_value=True), patch( - "os.path.isfile", return_value=True + with ( + patch("os.access", return_value=True), + patch("os.path.isfile", return_value=True), ): assert await async_setup_component(hass, influxdb.DOMAIN, config) await hass.async_block_till_done() @@ -407,7 +418,7 @@ async def test_event_listener( hass.states.async_set("fake.entity_id", in_, attrs) await hass.async_block_till_done() - hass.data[influxdb.DOMAIN].block_till_done() + await async_wait_for_queue_to_process(hass) write_api = get_write_api(mock_client) assert write_api.call_count == 1 @@ -454,7 +465,7 @@ async def test_event_listener_no_units( ] hass.states.async_set("fake.entity_id", 1, attrs) await hass.async_block_till_done() - hass.data[influxdb.DOMAIN].block_till_done() + await async_wait_for_queue_to_process(hass) write_api = get_write_api(mock_client) assert write_api.call_count == 1 @@ -497,7 +508,7 @@ async def test_event_listener_inf( ] hass.states.async_set("fake.entity_id", 8, attrs) await hass.async_block_till_done() - hass.data[influxdb.DOMAIN].block_till_done() + await async_wait_for_queue_to_process(hass) write_api = get_write_api(mock_client) assert write_api.call_count == 1 @@ -539,7 +550,7 @@ async def test_event_listener_states( ] hass.states.async_set("fake.entity_id", state_state) await hass.async_block_till_done() - hass.data[influxdb.DOMAIN].block_till_done() + await async_wait_for_queue_to_process(hass) write_api = get_write_api(mock_client) if state_state == 1: @@ -564,7 +575,7 @@ async def execute_filter_test(hass: HomeAssistant, tests, write_api, get_mock_ca ] hass.states.async_set(test.id, 1) await hass.async_block_till_done() - hass.data[influxdb.DOMAIN].block_till_done() + await async_wait_for_queue_to_process(hass) if test.should_pass: write_api.assert_called_once() @@ -927,7 +938,7 @@ async def test_event_listener_invalid_type( hass.states.async_set("fake.entity_id", in_, attrs) await hass.async_block_till_done() - hass.data[influxdb.DOMAIN].block_till_done() + await async_wait_for_queue_to_process(hass) write_api = get_write_api(mock_client) assert write_api.call_count == 1 @@ -970,7 +981,7 @@ async def test_event_listener_default_measurement( ] hass.states.async_set("fake.ok", 1) await hass.async_block_till_done() - hass.data[influxdb.DOMAIN].block_till_done() + await async_wait_for_queue_to_process(hass) write_api = get_write_api(mock_client) assert write_api.call_count == 1 @@ -1014,7 +1025,7 @@ async def test_event_listener_unit_of_measurement_field( ] hass.states.async_set("fake.entity_id", "foo", attrs) await hass.async_block_till_done() - hass.data[influxdb.DOMAIN].block_till_done() + await async_wait_for_queue_to_process(hass) write_api = get_write_api(mock_client) assert write_api.call_count == 1 @@ -1062,7 +1073,7 @@ async def test_event_listener_tags_attributes( ] hass.states.async_set("fake.something", 1, attrs) await hass.async_block_till_done() - hass.data[influxdb.DOMAIN].block_till_done() + await async_wait_for_queue_to_process(hass) write_api = get_write_api(mock_client) assert write_api.call_count == 1 @@ -1120,7 +1131,7 @@ async def test_event_listener_component_override_measurement( ] hass.states.async_set(f"{comp['domain']}.{comp['id']}", 1) await hass.async_block_till_done() - hass.data[influxdb.DOMAIN].block_till_done() + await async_wait_for_queue_to_process(hass) write_api = get_write_api(mock_client) assert write_api.call_count == 1 @@ -1186,7 +1197,7 @@ async def test_event_listener_component_measurement_attr( ] hass.states.async_set(f"{comp['domain']}.{comp['id']}", 1, comp["attrs"]) await hass.async_block_till_done() - hass.data[influxdb.DOMAIN].block_till_done() + await async_wait_for_queue_to_process(hass) write_api = get_write_api(mock_client) assert write_api.call_count == 1 @@ -1271,7 +1282,7 @@ async def test_event_listener_ignore_attributes( }, ) await hass.async_block_till_done() - hass.data[influxdb.DOMAIN].block_till_done() + await async_wait_for_queue_to_process(hass) write_api = get_write_api(mock_client) assert write_api.call_count == 1 @@ -1317,7 +1328,7 @@ async def test_event_listener_ignore_attributes_overlapping_entities( ] hass.states.async_set("sensor.fake", 1, {"ignore": 1}) await hass.async_block_till_done() - hass.data[influxdb.DOMAIN].block_till_done() + await async_wait_for_queue_to_process(hass) write_api = get_write_api(mock_client) assert write_api.call_count == 1 @@ -1357,7 +1368,7 @@ async def test_event_listener_scheduled_write( with patch.object(influxdb.time, "sleep") as mock_sleep: hass.states.async_set("entity.entity_id", 1) await hass.async_block_till_done() - hass.data[influxdb.DOMAIN].block_till_done() + await async_wait_for_queue_to_process(hass) assert mock_sleep.called assert write_api.call_count == 2 @@ -1366,7 +1377,7 @@ async def test_event_listener_scheduled_write( with patch.object(influxdb.time, "sleep") as mock_sleep: hass.states.async_set("entity.entity_id", "2") await hass.async_block_till_done() - hass.data[influxdb.DOMAIN].block_till_done() + await async_wait_for_queue_to_process(hass) assert not mock_sleep.called assert write_api.call_count == 3 @@ -1406,7 +1417,7 @@ async def test_event_listener_backlog_full( with patch("homeassistant.components.influxdb.time.monotonic", new=fast_monotonic): hass.states.async_set("entity.id", 1) await hass.async_block_till_done() - hass.data[influxdb.DOMAIN].block_till_done() + await async_wait_for_queue_to_process(hass) assert get_write_api(mock_client).call_count == 0 @@ -1444,7 +1455,7 @@ async def test_event_listener_attribute_name_conflict( ] hass.states.async_set("fake.something", 1, {"value": "value_str"}) await hass.async_block_till_done() - hass.data[influxdb.DOMAIN].block_till_done() + await async_wait_for_queue_to_process(hass) write_api = get_write_api(mock_client) assert write_api.call_count == 1 @@ -1566,7 +1577,8 @@ async def test_invalid_inputs_error( with patch(f"{INFLUX_PATH}.time.sleep") as sleep: hass.states.async_set("fake.something", 1) await hass.async_block_till_done() - hass.data[influxdb.DOMAIN].block_till_done() + await async_wait_for_queue_to_process(hass) + await hass.async_block_till_done() write_api.assert_called_once() assert ( @@ -1670,7 +1682,7 @@ async def test_precision( }, ) await hass.async_block_till_done() - hass.data[influxdb.DOMAIN].block_till_done() + await async_wait_for_queue_to_process(hass) write_api = get_write_api(mock_client) assert write_api.call_count == 1 diff --git a/tests/components/influxdb/test_sensor.py b/tests/components/influxdb/test_sensor.py index c1aaf88932e..395d33004a7 100644 --- a/tests/components/influxdb/test_sensor.py +++ b/tests/components/influxdb/test_sensor.py @@ -1,4 +1,5 @@ """The tests for the InfluxDB sensor.""" + from __future__ import annotations from dataclasses import dataclass @@ -92,9 +93,10 @@ def mock_client_fixture(request): @pytest.fixture(autouse=True, scope="module") def mock_client_close(): """Mock close method of clients at module scope.""" - with patch(f"{INFLUXDB_CLIENT_PATH}.close") as close_v1, patch( - f"{INFLUXDB_CLIENT_PATH}V2.close" - ) as close_v2: + with ( + patch(f"{INFLUXDB_CLIENT_PATH}.close") as close_v1, + patch(f"{INFLUXDB_CLIENT_PATH}V2.close") as close_v2, + ): yield (close_v1, close_v2) @@ -124,9 +126,7 @@ def _make_v2_resultset(*args): def _make_v2_buckets_resultset(): """Create a mock V2 'buckets()' resultset.""" - records = [] - for name in [DEFAULT_BUCKET, "bucket2"]: - records.append(Record({"name": name})) + records = [Record({"name": name}) for name in [DEFAULT_BUCKET, "bucket2"]] return [Table(records)] diff --git a/tests/components/inkbird/__init__.py b/tests/components/inkbird/__init__.py index 0a74e50ae0e..30ca369672c 100644 --- a/tests/components/inkbird/__init__.py +++ b/tests/components/inkbird/__init__.py @@ -1,6 +1,5 @@ """Tests for the INKBIRD integration.""" - from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo NOT_INKBIRD_SERVICE_INFO = BluetoothServiceInfo( diff --git a/tests/components/inkbird/test_config_flow.py b/tests/components/inkbird/test_config_flow.py index 2824009987c..ffb25ebd093 100644 --- a/tests/components/inkbird/test_config_flow.py +++ b/tests/components/inkbird/test_config_flow.py @@ -1,4 +1,5 @@ """Test the INKBIRD config flow.""" + from unittest.mock import patch from homeassistant import config_entries diff --git a/tests/components/inkbird/test_sensor.py b/tests/components/inkbird/test_sensor.py index 7585868485c..822136b9021 100644 --- a/tests/components/inkbird/test_sensor.py +++ b/tests/components/inkbird/test_sensor.py @@ -1,4 +1,5 @@ """Test the INKBIRD config flow.""" + 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 diff --git a/tests/components/input_boolean/test_init.py b/tests/components/input_boolean/test_init.py index 65d7fb93d19..2a616691e62 100644 --- a/tests/components/input_boolean/test_init.py +++ b/tests/components/input_boolean/test_init.py @@ -1,4 +1,5 @@ """The tests for the input_boolean component.""" + import logging from unittest.mock import patch diff --git a/tests/components/input_boolean/test_recorder.py b/tests/components/input_boolean/test_recorder.py index a59ae7b85c3..8f041d6c848 100644 --- a/tests/components/input_boolean/test_recorder.py +++ b/tests/components/input_boolean/test_recorder.py @@ -1,4 +1,5 @@ """The tests for recorder platform.""" + from __future__ import annotations from datetime import timedelta diff --git a/tests/components/input_boolean/test_reproduce_state.py b/tests/components/input_boolean/test_reproduce_state.py index 8d1a8827e61..b61f110d5b7 100644 --- a/tests/components/input_boolean/test_reproduce_state.py +++ b/tests/components/input_boolean/test_reproduce_state.py @@ -1,4 +1,5 @@ """Test reproduce state for input boolean.""" + from homeassistant.core import HomeAssistant, State from homeassistant.helpers.state import async_reproduce_state from homeassistant.setup import async_setup_component diff --git a/tests/components/input_button/test_init.py b/tests/components/input_button/test_init.py index 2f9b677e134..568d0076318 100644 --- a/tests/components/input_button/test_init.py +++ b/tests/components/input_button/test_init.py @@ -1,4 +1,5 @@ """The tests for the input_test component.""" + import logging from unittest.mock import patch diff --git a/tests/components/input_button/test_recorder.py b/tests/components/input_button/test_recorder.py index dd5f7530493..74023b73342 100644 --- a/tests/components/input_button/test_recorder.py +++ b/tests/components/input_button/test_recorder.py @@ -1,4 +1,5 @@ """The tests for recorder platform.""" + from __future__ import annotations from datetime import timedelta diff --git a/tests/components/input_datetime/test_init.py b/tests/components/input_datetime/test_init.py index 0a3f9b3ed6c..9d218e6d6ec 100644 --- a/tests/components/input_datetime/test_init.py +++ b/tests/components/input_datetime/test_init.py @@ -1,4 +1,5 @@ """Tests for the Input slider component.""" + import datetime from unittest.mock import patch diff --git a/tests/components/input_datetime/test_recorder.py b/tests/components/input_datetime/test_recorder.py index fe96b7cfb2d..d32e8ec3471 100644 --- a/tests/components/input_datetime/test_recorder.py +++ b/tests/components/input_datetime/test_recorder.py @@ -1,4 +1,5 @@ """The tests for recorder platform.""" + from __future__ import annotations from datetime import timedelta diff --git a/tests/components/input_datetime/test_reproduce_state.py b/tests/components/input_datetime/test_reproduce_state.py index b5f75b81068..323849bc882 100644 --- a/tests/components/input_datetime/test_reproduce_state.py +++ b/tests/components/input_datetime/test_reproduce_state.py @@ -1,4 +1,5 @@ """Test reproduce state for Input datetime.""" + import pytest from homeassistant.core import HomeAssistant, State diff --git a/tests/components/input_number/test_init.py b/tests/components/input_number/test_init.py index 305ff74b6bf..62b95fe16b3 100644 --- a/tests/components/input_number/test_init.py +++ b/tests/components/input_number/test_init.py @@ -1,4 +1,5 @@ """The tests for the Input number component.""" + from unittest.mock import patch import pytest diff --git a/tests/components/input_number/test_recorder.py b/tests/components/input_number/test_recorder.py index 4172d169deb..78f709511de 100644 --- a/tests/components/input_number/test_recorder.py +++ b/tests/components/input_number/test_recorder.py @@ -1,4 +1,5 @@ """The tests for recorder platform.""" + from __future__ import annotations from datetime import timedelta diff --git a/tests/components/input_number/test_reproduce_state.py b/tests/components/input_number/test_reproduce_state.py index 087b2fe1001..6c8c61a0b58 100644 --- a/tests/components/input_number/test_reproduce_state.py +++ b/tests/components/input_number/test_reproduce_state.py @@ -1,4 +1,5 @@ """Test reproduce state for Input number.""" + import pytest from homeassistant.core import HomeAssistant, State diff --git a/tests/components/input_select/test_init.py b/tests/components/input_select/test_init.py index 3978d0cf175..431f8b7d078 100644 --- a/tests/components/input_select/test_init.py +++ b/tests/components/input_select/test_init.py @@ -1,4 +1,5 @@ """The tests for the Input select component.""" + from unittest.mock import patch import pytest diff --git a/tests/components/input_select/test_recorder.py b/tests/components/input_select/test_recorder.py index f4ac98dfc39..b12fe57d431 100644 --- a/tests/components/input_select/test_recorder.py +++ b/tests/components/input_select/test_recorder.py @@ -1,4 +1,5 @@ """The tests for recorder platform.""" + from __future__ import annotations from datetime import timedelta diff --git a/tests/components/input_select/test_reproduce_state.py b/tests/components/input_select/test_reproduce_state.py index a00b6b02ade..13672ebc708 100644 --- a/tests/components/input_select/test_reproduce_state.py +++ b/tests/components/input_select/test_reproduce_state.py @@ -1,4 +1,5 @@ """Test reproduce state for Input select.""" + import pytest from homeassistant.core import HomeAssistant, State diff --git a/tests/components/input_text/test_init.py b/tests/components/input_text/test_init.py index c057407a644..d98ee4f7668 100644 --- a/tests/components/input_text/test_init.py +++ b/tests/components/input_text/test_init.py @@ -1,4 +1,5 @@ """The tests for the Input text component.""" + from unittest.mock import patch import pytest diff --git a/tests/components/input_text/test_recorder.py b/tests/components/input_text/test_recorder.py index 001f56a5a3e..a81160b32c7 100644 --- a/tests/components/input_text/test_recorder.py +++ b/tests/components/input_text/test_recorder.py @@ -1,4 +1,5 @@ """The tests for recorder platform.""" + from __future__ import annotations from datetime import timedelta diff --git a/tests/components/input_text/test_reproduce_state.py b/tests/components/input_text/test_reproduce_state.py index 877a784853a..88b8131000b 100644 --- a/tests/components/input_text/test_reproduce_state.py +++ b/tests/components/input_text/test_reproduce_state.py @@ -1,4 +1,5 @@ """Test reproduce state for Input text.""" + import pytest from homeassistant.core import HomeAssistant, State diff --git a/tests/components/insteon/const.py b/tests/components/insteon/const.py index 53db12acb04..c35db3b7092 100644 --- a/tests/components/insteon/const.py +++ b/tests/components/insteon/const.py @@ -1,4 +1,5 @@ """Constants used for Insteon test cases.""" + from homeassistant.components.insteon.const import ( CONF_CAT, CONF_DIM_STEPS, diff --git a/tests/components/insteon/test_api_aldb.py b/tests/components/insteon/test_api_aldb.py index 4bd299d3d05..4e0df12c6f1 100644 --- a/tests/components/insteon/test_api_aldb.py +++ b/tests/components/insteon/test_api_aldb.py @@ -1,4 +1,5 @@ """Test the Insteon All-Link Database APIs.""" + import json from unittest.mock import patch diff --git a/tests/components/insteon/test_api_device.py b/tests/components/insteon/test_api_device.py index 7485914026a..f3c67d479d0 100644 --- a/tests/components/insteon/test_api_device.py +++ b/tests/components/insteon/test_api_device.py @@ -1,4 +1,5 @@ """Test the device level APIs.""" + import asyncio from unittest.mock import patch diff --git a/tests/components/insteon/test_api_properties.py b/tests/components/insteon/test_api_properties.py index 850ccc85411..d2a388929b5 100644 --- a/tests/components/insteon/test_api_properties.py +++ b/tests/components/insteon/test_api_properties.py @@ -1,4 +1,5 @@ """Test the Insteon properties APIs.""" + import json from unittest.mock import AsyncMock, patch diff --git a/tests/components/insteon/test_api_scenes.py b/tests/components/insteon/test_api_scenes.py index cc9b11f4632..04fc74c89d1 100644 --- a/tests/components/insteon/test_api_scenes.py +++ b/tests/components/insteon/test_api_scenes.py @@ -1,4 +1,5 @@ """Test the Insteon Scenes APIs.""" + import json import os from unittest.mock import AsyncMock, patch @@ -100,9 +101,10 @@ async def test_save_scene( mock_add_or_update_scene = AsyncMock(return_value=(20, ResponseStatus.SUCCESS)) - with patch.object( - pyinsteon.managers.scene_manager, "devices", devices - ), patch.object(scenes, "async_add_or_update_scene", mock_add_or_update_scene): + with ( + patch.object(pyinsteon.managers.scene_manager, "devices", devices), + patch.object(scenes, "async_add_or_update_scene", mock_add_or_update_scene), + ): scene = await pyinsteon.managers.scene_manager.async_get_scene(20) scene["devices"]["1a1a1a"] = [] links = _scene_to_array(scene) @@ -131,9 +133,10 @@ async def test_save_new_scene( mock_add_or_update_scene = AsyncMock(return_value=(21, ResponseStatus.SUCCESS)) - with patch.object( - pyinsteon.managers.scene_manager, "devices", devices - ), patch.object(scenes, "async_add_or_update_scene", mock_add_or_update_scene): + with ( + patch.object(pyinsteon.managers.scene_manager, "devices", devices), + patch.object(scenes, "async_add_or_update_scene", mock_add_or_update_scene), + ): scene = await pyinsteon.managers.scene_manager.async_get_scene(20) scene["devices"]["1a1a1a"] = [] links = _scene_to_array(scene) @@ -162,9 +165,10 @@ async def test_save_scene_error( mock_add_or_update_scene = AsyncMock(return_value=(20, ResponseStatus.FAILURE)) - with patch.object( - pyinsteon.managers.scene_manager, "devices", devices - ), patch.object(scenes, "async_add_or_update_scene", mock_add_or_update_scene): + with ( + patch.object(pyinsteon.managers.scene_manager, "devices", devices), + patch.object(scenes, "async_add_or_update_scene", mock_add_or_update_scene), + ): scene = await pyinsteon.managers.scene_manager.async_get_scene(20) scene["devices"]["1a1a1a"] = [] links = _scene_to_array(scene) @@ -193,9 +197,10 @@ async def test_delete_scene( mock_delete_scene = AsyncMock(return_value=ResponseStatus.SUCCESS) - with patch.object( - pyinsteon.managers.scene_manager, "devices", devices - ), patch.object(scenes, "async_delete_scene", mock_delete_scene): + with ( + patch.object(pyinsteon.managers.scene_manager, "devices", devices), + patch.object(scenes, "async_delete_scene", mock_delete_scene), + ): await ws_client.send_json( { ID: 1, diff --git a/tests/components/insteon/test_config_flow.py b/tests/components/insteon/test_config_flow.py index aec60d45961..df7430bc254 100644 --- a/tests/components/insteon/test_config_flow.py +++ b/tests/components/insteon/test_config_flow.py @@ -1,4 +1,5 @@ """Test the config flow for the Insteon integration.""" + from unittest.mock import patch import pytest @@ -101,13 +102,17 @@ async def _init_form(hass, modem_type): async def _device_form(hass, flow_id, connection, user_input): """Test the PLM, Hub v1 or Hub v2 form.""" - with patch( - PATCH_CONNECTION, - new=connection, - ), patch(PATCH_ASYNC_SETUP, return_value=True) as mock_setup, patch( - PATCH_ASYNC_SETUP_ENTRY, - return_value=True, - ) as mock_setup_entry: + with ( + patch( + PATCH_CONNECTION, + new=connection, + ), + patch(PATCH_ASYNC_SETUP, return_value=True) as mock_setup, + patch( + PATCH_ASYNC_SETUP_ENTRY, + return_value=True, + ) as mock_setup_entry, + ): result = await hass.config_entries.flow.async_configure(flow_id, user_input) await hass.async_block_till_done() return result, mock_setup, mock_setup_entry @@ -310,10 +315,11 @@ async def _options_form( mock_devices = MockDevices(connected=True) await mock_devices.async_load() mock_devices.modem = mock_devices["AA.AA.AA"] - with patch(PATCH_CONNECTION, new=connection), patch( - PATCH_ASYNC_SETUP_ENTRY, return_value=True - ) as mock_setup_entry, patch(PATCH_DEVICES, mock_devices), patch( - PATCH_CONNECTION_CLOSE + with ( + patch(PATCH_CONNECTION, new=connection), + patch(PATCH_ASYNC_SETUP_ENTRY, return_value=True) as mock_setup_entry, + patch(PATCH_DEVICES, mock_devices), + patch(PATCH_CONNECTION_CLOSE), ): result = await hass.config_entries.options.async_configure(flow_id, user_input) return result, mock_setup_entry diff --git a/tests/components/insteon/test_init.py b/tests/components/insteon/test_init.py index f772eed2d26..a4e8da03345 100644 --- a/tests/components/insteon/test_init.py +++ b/tests/components/insteon/test_init.py @@ -1,4 +1,5 @@ """Test the init file for the Insteon component.""" + import asyncio from unittest.mock import patch @@ -31,10 +32,10 @@ async def test_setup_entry(hass: HomeAssistant) -> None: config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_INPUT_PLM) config_entry.add_to_hass(hass) - with patch.object( - insteon, "async_connect", new=mock_successful_connection - ), patch.object(insteon, "async_close") as mock_close, patch.object( - insteon, "devices", new=MockDevices() + with ( + patch.object(insteon, "async_connect", new=mock_successful_connection), + patch.object(insteon, "async_close") as mock_close, + patch.object(insteon, "devices", new=MockDevices()), ): assert await async_setup_component( hass, @@ -55,9 +56,10 @@ async def test_setup_entry_failed_connection( config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_INPUT_PLM) config_entry.add_to_hass(hass) - with patch.object( - insteon, "async_connect", new=mock_failed_connection - ), patch.object(insteon, "devices", new=MockDevices(connected=False)): + with ( + patch.object(insteon, "async_connect", new=mock_failed_connection), + patch.object(insteon, "devices", new=MockDevices(connected=False)), + ): assert await async_setup_component( hass, insteon.DOMAIN, @@ -71,13 +73,14 @@ async def test_import_frontend_dev_url(hass: HomeAssistant) -> None: config = {} config[DOMAIN] = {CONF_DEV_PATH: "/some/path"} - with patch.object( - insteon, "async_connect", new=mock_successful_connection - ), patch.object(insteon, "close_insteon_connection"), patch.object( - insteon, "devices", new=MockDevices() - ), patch( - PATCH_CONNECTION, - new=mock_successful_connection, + with ( + patch.object(insteon, "async_connect", new=mock_successful_connection), + patch.object(insteon, "close_insteon_connection"), + patch.object(insteon, "devices", new=MockDevices()), + patch( + PATCH_CONNECTION, + new=mock_successful_connection, + ), ): assert await async_setup_component( hass, diff --git a/tests/components/insteon/test_lock.py b/tests/components/insteon/test_lock.py index c100acae3ce..a782e006a62 100644 --- a/tests/components/insteon/test_lock.py +++ b/tests/components/insteon/test_lock.py @@ -1,4 +1,5 @@ """Tests for the Insteon lock.""" + from unittest.mock import patch import pytest @@ -42,14 +43,16 @@ def lock_platform_only(): @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, + 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 diff --git a/tests/components/integration/test_config_flow.py b/tests/components/integration/test_config_flow.py index c92cf70b0c2..4f811e98de2 100644 --- a/tests/components/integration/test_config_flow.py +++ b/tests/components/integration/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Integration - Riemann sum integral config flow.""" + from unittest.mock import patch import pytest @@ -11,7 +12,7 @@ from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry -@pytest.mark.parametrize("platform", ("sensor",)) +@pytest.mark.parametrize("platform", ["sensor"]) async def test_config_flow(hass: HomeAssistant, platform) -> None: """Test the config flow.""" input_sensor_entity_id = "sensor.input" @@ -73,7 +74,7 @@ def get_suggested(schema, key): raise Exception -@pytest.mark.parametrize("platform", ("sensor",)) +@pytest.mark.parametrize("platform", ["sensor"]) async def test_options(hass: HomeAssistant, platform) -> None: """Test reconfiguring.""" # Setup the config entry diff --git a/tests/components/integration/test_init.py b/tests/components/integration/test_init.py index 885c10277f8..f92a4a67585 100644 --- a/tests/components/integration/test_init.py +++ b/tests/components/integration/test_init.py @@ -1,4 +1,5 @@ """Test the Integration - Riemann sum integral integration.""" + import pytest from homeassistant.components.integration.const import DOMAIN @@ -8,7 +9,7 @@ from homeassistant.helpers import entity_registry as er from tests.common import MockConfigEntry -@pytest.mark.parametrize("platform", ("sensor",)) +@pytest.mark.parametrize("platform", ["sensor"]) async def test_setup_and_remove_config_entry( hass: HomeAssistant, entity_registry: er.EntityRegistry, diff --git a/tests/components/integration/test_sensor.py b/tests/components/integration/test_sensor.py index 8ef9caf4928..53763247bdf 100644 --- a/tests/components/integration/test_sensor.py +++ b/tests/components/integration/test_sensor.py @@ -1,4 +1,5 @@ """The tests for the integration sensor platform.""" + from datetime import timedelta from freezegun import freeze_time @@ -562,7 +563,7 @@ async def test_units(hass: HomeAssistant) -> None: # When source state goes to None / Unknown, expect an early exit without # changes to the state or unit_of_measurement - hass.states.async_set(entity_id, None, None) + hass.states.async_set(entity_id, None, {"unit_of_measurement": UnitOfPower.WATT}) await hass.async_block_till_done() new_state = hass.states.get("sensor.integration") diff --git a/tests/components/intellifire/conftest.py b/tests/components/intellifire/conftest.py index 7ad41cec4e5..fa7a48ef9ac 100644 --- a/tests/components/intellifire/conftest.py +++ b/tests/components/intellifire/conftest.py @@ -1,4 +1,5 @@ """Fixtures for IntelliFire integration tests.""" + from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, Mock, patch diff --git a/tests/components/intellifire/test_config_flow.py b/tests/components/intellifire/test_config_flow.py index 1bc8cc0e06a..7f6f509a3a3 100644 --- a/tests/components/intellifire/test_config_flow.py +++ b/tests/components/intellifire/test_config_flow.py @@ -1,4 +1,5 @@ """Test the IntelliFire config flow.""" + from unittest.mock import AsyncMock, MagicMock, patch from intellifire4py.exceptions import LoginException diff --git a/tests/components/intent_script/test_init.py b/tests/components/intent_script/test_init.py index fe694607def..14e5dd62d51 100644 --- a/tests/components/intent_script/test_init.py +++ b/tests/components/intent_script/test_init.py @@ -1,4 +1,5 @@ """Test intent_script component.""" + from unittest.mock import patch from homeassistant import config as hass_config diff --git a/tests/components/ios/test_init.py b/tests/components/ios/test_init.py index 9586bd3c011..afefec1530c 100644 --- a/tests/components/ios/test_init.py +++ b/tests/components/ios/test_init.py @@ -1,4 +1,5 @@ """Tests for the iOS init file.""" + from unittest.mock import patch import pytest diff --git a/tests/components/iotawatt/__init__.py b/tests/components/iotawatt/__init__.py index 5f66c145ad6..5233ce6fc83 100644 --- a/tests/components/iotawatt/__init__.py +++ b/tests/components/iotawatt/__init__.py @@ -1,4 +1,5 @@ """Tests for the IoTaWatt integration.""" + from iotawattpy.sensor import Sensor INPUT_SENSOR = Sensor( diff --git a/tests/components/iotawatt/conftest.py b/tests/components/iotawatt/conftest.py index f96201ba50e..f3a60e69021 100644 --- a/tests/components/iotawatt/conftest.py +++ b/tests/components/iotawatt/conftest.py @@ -1,4 +1,5 @@ """Test fixtures for IoTaWatt.""" + from unittest.mock import AsyncMock, patch import pytest diff --git a/tests/components/iotawatt/test_config_flow.py b/tests/components/iotawatt/test_config_flow.py index 06c9cce0da9..d4980ba978e 100644 --- a/tests/components/iotawatt/test_config_flow.py +++ b/tests/components/iotawatt/test_config_flow.py @@ -1,4 +1,5 @@ """Test the IoTawatt config flow.""" + from unittest.mock import patch import httpx @@ -18,13 +19,16 @@ async def test_form(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.FORM assert result["step_id"] == config_entries.SOURCE_USER - with patch( - "homeassistant.components.iotawatt.async_setup_entry", - return_value=True, - ), patch( - "homeassistant.components.iotawatt.config_flow.Iotawatt.connect", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.iotawatt.async_setup_entry", + return_value=True, + ), + patch( + "homeassistant.components.iotawatt.config_flow.Iotawatt.connect", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -79,12 +83,15 @@ async def test_form_auth(hass: HomeAssistant) -> None: assert result3["step_id"] == "auth" assert result3["errors"] == {"base": "invalid_auth"} - with patch( - "homeassistant.components.iotawatt.async_setup_entry", - return_value=True, - ) as mock_setup_entry, patch( - "homeassistant.components.iotawatt.config_flow.Iotawatt.connect", - return_value=True, + with ( + patch( + "homeassistant.components.iotawatt.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + patch( + "homeassistant.components.iotawatt.config_flow.Iotawatt.connect", + return_value=True, + ), ): result4 = await hass.config_entries.flow.async_configure( result["flow_id"], diff --git a/tests/components/iotawatt/test_init.py b/tests/components/iotawatt/test_init.py index cdf58a0a2d2..c185fec0e4d 100644 --- a/tests/components/iotawatt/test_init.py +++ b/tests/components/iotawatt/test_init.py @@ -1,4 +1,5 @@ """Test init.""" + import httpx from homeassistant.config_entries import ConfigEntryState diff --git a/tests/components/iotawatt/test_sensor.py b/tests/components/iotawatt/test_sensor.py index 5646115f59a..ecf2f97c67a 100644 --- a/tests/components/iotawatt/test_sensor.py +++ b/tests/components/iotawatt/test_sensor.py @@ -1,4 +1,5 @@ """Test setting up sensors.""" + from datetime import timedelta from freezegun.api import FrozenDateTimeFactory @@ -62,9 +63,9 @@ async def test_sensor_type_output( hass: HomeAssistant, freezer: FrozenDateTimeFactory, mock_iotawatt ) -> None: """Tests the sensor type of Output.""" - mock_iotawatt.getSensors.return_value["sensors"][ - "my_watthour_sensor_key" - ] = OUTPUT_SENSOR + mock_iotawatt.getSensors.return_value["sensors"]["my_watthour_sensor_key"] = ( + OUTPUT_SENSOR + ) assert await async_setup_component(hass, "iotawatt", {}) await hass.async_block_till_done() diff --git a/tests/components/ipma/__init__.py b/tests/components/ipma/__init__.py index 02a61f0b201..65cff43c8d4 100644 --- a/tests/components/ipma/__init__.py +++ b/tests/components/ipma/__init__.py @@ -1,4 +1,5 @@ """Tests for the IPMA component.""" + from collections import namedtuple from datetime import UTC, datetime diff --git a/tests/components/ipma/conftest.py b/tests/components/ipma/conftest.py index dda0e69d118..7f3e82a8819 100644 --- a/tests/components/ipma/conftest.py +++ b/tests/components/ipma/conftest.py @@ -1,36 +1,42 @@ """Define test fixtures for IPMA.""" +from unittest.mock import patch + import pytest from homeassistant.components.ipma import DOMAIN from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME +from homeassistant.core import HomeAssistant + +from . import MockLocation from tests.common import MockConfigEntry -@pytest.fixture(name="config_entry") -def config_entry_fixture(hass, config): +@pytest.fixture +def config_entry(hass): """Define a config entry fixture.""" entry = MockConfigEntry( domain=DOMAIN, - data=config, + data={ + CONF_NAME: "Home", + CONF_LATITUDE: 0, + CONF_LONGITUDE: 0, + }, ) entry.add_to_hass(hass) return entry -@pytest.fixture(name="config") -def config_fixture(): - """Define a config entry data fixture.""" - return { - CONF_NAME: "Home", - CONF_LATITUDE: 0, - CONF_LONGITUDE: 0, - } +@pytest.fixture +async def init_integration( + hass: HomeAssistant, config_entry: MockConfigEntry +) -> MockConfigEntry: + """Set up the IPMA integration for testing.""" + config_entry.add_to_hass(hass) + with patch("pyipma.location.Location.get", return_value=MockLocation()): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() -@pytest.fixture(name="setup_config_entry") -async def setup_config_entry_fixture(hass, config_entry): - """Define a fixture to set up ipma.""" - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + return config_entry diff --git a/tests/components/ipma/snapshots/test_diagnostics.ambr b/tests/components/ipma/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..c95364b6e4a --- /dev/null +++ b/tests/components/ipma/snapshots/test_diagnostics.ambr @@ -0,0 +1,60 @@ +# serializer version: 1 +# name: test_diagnostics + dict({ + 'current_weather': list([ + 0.0, + 71.0, + 1000.0, + 0.0, + 18.0, + 'NW', + 3.94, + ]), + 'location_information': dict({ + 'global_id_local': 1130600, + 'id_station': 1200545, + 'latitude': 0.0, + 'longitude': 0.0, + 'name': 'HomeTown', + 'station': 'HomeTown Station', + }), + 'weather_forecast': list([ + list([ + '7.7', + '2020-01-15T01:00:00+00:00', + 1, + '86.9', + 12.0, + None, + 80.0, + 10.6, + '2020-01-15T02:51:00', + list([ + 10, + 'Light rain', + 'Chuva fraca ou chuvisco', + ]), + 'S', + '32.7', + ]), + list([ + '5.7', + '2020-01-15T02:00:00+00:00', + 1, + '86.9', + 12.0, + None, + 80.0, + 10.6, + '2020-01-15T02:51:00', + list([ + 1, + 'Clear sky', + 'Céu limpo', + ]), + 'S', + '32.7', + ]), + ]), + }) +# --- diff --git a/tests/components/ipma/test_config_flow.py b/tests/components/ipma/test_config_flow.py index aff8af16bc3..e17c8d011a9 100644 --- a/tests/components/ipma/test_config_flow.py +++ b/tests/components/ipma/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for IPMA config flow.""" + from unittest.mock import patch from pyipma import IPMAException @@ -91,7 +92,7 @@ async def test_config_flow_failures(hass: HomeAssistant) -> None: } -async def test_flow_entry_already_exists(hass: HomeAssistant, config_entry) -> None: +async def test_flow_entry_already_exists(hass: HomeAssistant, init_integration) -> None: """Test user input for config_entry that already exists. Test when the form should show when user puts existing location diff --git a/tests/components/ipma/test_diagnostics.py b/tests/components/ipma/test_diagnostics.py new file mode 100644 index 00000000000..b7d421a2ee5 --- /dev/null +++ b/tests/components/ipma/test_diagnostics.py @@ -0,0 +1,22 @@ +"""Test IPMA diagnostics.""" + +from syrupy.assertion import SnapshotAssertion + +from homeassistant.core import HomeAssistant + +from tests.components.diagnostics import get_diagnostics_for_config_entry +from tests.typing import ClientSessionGenerator + + +async def test_diagnostics( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + init_integration, + snapshot: SnapshotAssertion, +) -> None: + """Test diagnostics.""" + + assert ( + await get_diagnostics_for_config_entry(hass, hass_client, init_integration) + == snapshot + ) diff --git a/tests/components/ipma/test_init.py b/tests/components/ipma/test_init.py index 54755a7ff08..7967b97dd23 100644 --- a/tests/components/ipma/test_init.py +++ b/tests/components/ipma/test_init.py @@ -1,4 +1,5 @@ """Test the IPMA integration.""" + from unittest.mock import patch from pyipma import IPMAException diff --git a/tests/components/ipma/test_system_health.py b/tests/components/ipma/test_system_health.py index 665ed193da1..5d532611969 100644 --- a/tests/components/ipma/test_system_health.py +++ b/tests/components/ipma/test_system_health.py @@ -1,4 +1,5 @@ """Test ipma system health.""" + import asyncio from homeassistant.components.ipma.system_health import IPMA_API_URL @@ -17,6 +18,7 @@ async def test_ipma_system_health( hass.config.components.add("ipma") assert await async_setup_component(hass, "system_health", {}) + await hass.async_block_till_done() info = await get_system_health_info(hass, "ipma") diff --git a/tests/components/ipma/test_weather.py b/tests/components/ipma/test_weather.py index 9e0262733a3..7150286e4f9 100644 --- a/tests/components/ipma/test_weather.py +++ b/tests/components/ipma/test_weather.py @@ -1,4 +1,5 @@ """The tests for the IPMA weather component.""" + import datetime from unittest.mock import patch @@ -8,14 +9,6 @@ from syrupy.assertion import SnapshotAssertion from homeassistant.components.ipma.const import MIN_TIME_BETWEEN_UPDATES from homeassistant.components.weather import ( - ATTR_FORECAST, - ATTR_FORECAST_CONDITION, - ATTR_FORECAST_PRECIPITATION_PROBABILITY, - ATTR_FORECAST_TEMP, - ATTR_FORECAST_TEMP_LOW, - ATTR_FORECAST_TIME, - ATTR_FORECAST_WIND_BEARING, - ATTR_FORECAST_WIND_SPEED, ATTR_WEATHER_HUMIDITY, ATTR_WEATHER_PRESSURE, ATTR_WEATHER_TEMPERATURE, @@ -83,53 +76,6 @@ async def test_setup_config_flow(hass: HomeAssistant) -> None: assert state.attributes.get("friendly_name") == "HomeTown" -async def test_daily_forecast(hass: HomeAssistant) -> None: - """Test for successfully getting daily forecast.""" - with patch( - "pyipma.location.Location.get", - return_value=MockLocation(), - ): - entry = MockConfigEntry(domain="ipma", data=TEST_CONFIG) - entry.add_to_hass(hass) - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - state = hass.states.get("weather.hometown") - assert state.state == "rainy" - - forecast = state.attributes.get(ATTR_FORECAST)[0] - assert forecast.get(ATTR_FORECAST_TIME) == datetime.datetime(2020, 1, 16, 0, 0, 0) - assert forecast.get(ATTR_FORECAST_CONDITION) == "rainy" - assert forecast.get(ATTR_FORECAST_TEMP) == 16.2 - assert forecast.get(ATTR_FORECAST_TEMP_LOW) == 10.6 - assert forecast.get(ATTR_FORECAST_PRECIPITATION_PROBABILITY) == "100.0" - assert forecast.get(ATTR_FORECAST_WIND_SPEED) == 10.0 - assert forecast.get(ATTR_FORECAST_WIND_BEARING) == "S" - - -@pytest.mark.freeze_time("2020-01-14 23:00:00") -async def test_hourly_forecast(hass: HomeAssistant) -> None: - """Test for successfully getting daily forecast.""" - with patch( - "pyipma.location.Location.get", - return_value=MockLocation(), - ): - entry = MockConfigEntry(domain="ipma", data=TEST_CONFIG_HOURLY) - entry.add_to_hass(hass) - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - state = hass.states.get("weather.hometown") - assert state.state == "rainy" - - forecast = state.attributes.get(ATTR_FORECAST)[0] - assert forecast.get(ATTR_FORECAST_CONDITION) == "rainy" - assert forecast.get(ATTR_FORECAST_TEMP) == 12.0 - assert forecast.get(ATTR_FORECAST_PRECIPITATION_PROBABILITY) == 80.0 - assert forecast.get(ATTR_FORECAST_WIND_SPEED) == 32.7 - assert forecast.get(ATTR_FORECAST_WIND_BEARING) == "S" - - async def test_failed_get_observation_forecast(hass: HomeAssistant) -> None: """Test for successfully setting up the IPMA platform.""" with patch( diff --git a/tests/components/ipp/conftest.py b/tests/components/ipp/conftest.py index de3f1e0e73c..f650b370200 100644 --- a/tests/components/ipp/conftest.py +++ b/tests/components/ipp/conftest.py @@ -1,4 +1,5 @@ """Fixtures for IPP integration tests.""" + from collections.abc import Generator import json from unittest.mock import AsyncMock, MagicMock, patch diff --git a/tests/components/ipp/test_config_flow.py b/tests/components/ipp/test_config_flow.py index 5dd6c1af5bf..1ea75ecf167 100644 --- a/tests/components/ipp/test_config_flow.py +++ b/tests/components/ipp/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for the IPP config flow.""" + import dataclasses from ipaddress import ip_address import json diff --git a/tests/components/ipp/test_init.py b/tests/components/ipp/test_init.py index f502c30068c..5742d47674d 100644 --- a/tests/components/ipp/test_init.py +++ b/tests/components/ipp/test_init.py @@ -1,4 +1,5 @@ """Tests for the IPP integration.""" + from unittest.mock import AsyncMock, MagicMock, patch from pyipp import IPPConnectionError diff --git a/tests/components/ipp/test_sensor.py b/tests/components/ipp/test_sensor.py index 52e8cdedf91..9f0079a4e40 100644 --- a/tests/components/ipp/test_sensor.py +++ b/tests/components/ipp/test_sensor.py @@ -1,15 +1,11 @@ """Tests for the IPP sensor platform.""" + from unittest.mock import AsyncMock import pytest from homeassistant.components.sensor import ATTR_OPTIONS -from homeassistant.const import ( - ATTR_ICON, - ATTR_UNIT_OF_MEASUREMENT, - PERCENTAGE, - EntityCategory, -) +from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, PERCENTAGE, EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -26,7 +22,6 @@ async def test_sensors( """Test the creation and values of the IPP sensors.""" state = hass.states.get("sensor.test_ha_1000_series") assert state - assert state.attributes.get(ATTR_ICON) == "mdi:printer" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None assert state.attributes.get(ATTR_OPTIONS) == ["idle", "printing", "stopped"] @@ -36,37 +31,31 @@ async def test_sensors( state = hass.states.get("sensor.test_ha_1000_series_black_ink") assert state - assert state.attributes.get(ATTR_ICON) == "mdi:water" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is PERCENTAGE assert state.state == "58" state = hass.states.get("sensor.test_ha_1000_series_photo_black_ink") assert state - assert state.attributes.get(ATTR_ICON) == "mdi:water" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is PERCENTAGE assert state.state == "98" state = hass.states.get("sensor.test_ha_1000_series_cyan_ink") assert state - assert state.attributes.get(ATTR_ICON) == "mdi:water" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is PERCENTAGE assert state.state == "91" state = hass.states.get("sensor.test_ha_1000_series_yellow_ink") assert state - assert state.attributes.get(ATTR_ICON) == "mdi:water" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is PERCENTAGE assert state.state == "95" state = hass.states.get("sensor.test_ha_1000_series_magenta_ink") assert state - assert state.attributes.get(ATTR_ICON) == "mdi:water" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is PERCENTAGE assert state.state == "73" state = hass.states.get("sensor.test_ha_1000_series_uptime") assert state - assert state.attributes.get(ATTR_ICON) == "mdi:clock-outline" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None assert state.state == "2019-11-11T09:10:02+00:00" diff --git a/tests/components/iqvia/conftest.py b/tests/components/iqvia/conftest.py index b24d473c7df..6fb14ca4d28 100644 --- a/tests/components/iqvia/conftest.py +++ b/tests/components/iqvia/conftest.py @@ -1,4 +1,5 @@ """Define test fixtures for IQVIA.""" + import json from unittest.mock import patch @@ -86,18 +87,17 @@ async def setup_iqvia_fixture( data_disease_index, ): """Define a fixture to set up IQVIA.""" - with patch( - "pyiqvia.allergens.Allergens.extended", return_value=data_allergy_forecast - ), patch( - "pyiqvia.allergens.Allergens.current", return_value=data_allergy_index - ), patch( - "pyiqvia.allergens.Allergens.outlook", return_value=data_allergy_outlook - ), patch( - "pyiqvia.asthma.Asthma.extended", return_value=data_asthma_forecast - ), patch("pyiqvia.asthma.Asthma.current", return_value=data_asthma_index), patch( - "pyiqvia.disease.Disease.extended", return_value=data_disease_forecast - ), patch("pyiqvia.disease.Disease.current", return_value=data_disease_index), patch( - "homeassistant.components.iqvia.PLATFORMS", [] + with ( + patch( + "pyiqvia.allergens.Allergens.extended", return_value=data_allergy_forecast + ), + patch("pyiqvia.allergens.Allergens.current", return_value=data_allergy_index), + patch("pyiqvia.allergens.Allergens.outlook", return_value=data_allergy_outlook), + patch("pyiqvia.asthma.Asthma.extended", return_value=data_asthma_forecast), + patch("pyiqvia.asthma.Asthma.current", return_value=data_asthma_index), + patch("pyiqvia.disease.Disease.extended", return_value=data_disease_forecast), + patch("pyiqvia.disease.Disease.current", return_value=data_disease_index), + patch("homeassistant.components.iqvia.PLATFORMS", []), ): assert await async_setup_component(hass, DOMAIN, config) await hass.async_block_till_done() diff --git a/tests/components/iqvia/test_config_flow.py b/tests/components/iqvia/test_config_flow.py index ead163b0269..a75eed8ecd0 100644 --- a/tests/components/iqvia/test_config_flow.py +++ b/tests/components/iqvia/test_config_flow.py @@ -1,4 +1,5 @@ """Define tests for the IQVIA config flow.""" + from homeassistant import data_entry_flow from homeassistant.components.iqvia import CONF_ZIP_CODE, DOMAIN from homeassistant.config_entries import SOURCE_USER diff --git a/tests/components/iqvia/test_diagnostics.py b/tests/components/iqvia/test_diagnostics.py index bde2af57447..7c445c9b3e4 100644 --- a/tests/components/iqvia/test_diagnostics.py +++ b/tests/components/iqvia/test_diagnostics.py @@ -1,4 +1,5 @@ """Test IQVIA diagnostics.""" + from syrupy import SnapshotAssertion from homeassistant.core import HomeAssistant diff --git a/tests/components/islamic_prayer_times/conftest.py b/tests/components/islamic_prayer_times/conftest.py index 63c6ad8414b..f1b4a8f675c 100644 --- a/tests/components/islamic_prayer_times/conftest.py +++ b/tests/components/islamic_prayer_times/conftest.py @@ -1,4 +1,5 @@ """Common fixtures for the islamic_prayer_times tests.""" + from collections.abc import Generator from unittest.mock import AsyncMock, patch diff --git a/tests/components/islamic_prayer_times/test_config_flow.py b/tests/components/islamic_prayer_times/test_config_flow.py index 0375c788b11..41a5c3df0ac 100644 --- a/tests/components/islamic_prayer_times/test_config_flow.py +++ b/tests/components/islamic_prayer_times/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for Islamic Prayer Times config flow.""" + from unittest.mock import patch from prayer_times_calculator import InvalidResponseError diff --git a/tests/components/islamic_prayer_times/test_init.py b/tests/components/islamic_prayer_times/test_init.py index 746abf27d43..3c7565a37ef 100644 --- a/tests/components/islamic_prayer_times/test_init.py +++ b/tests/components/islamic_prayer_times/test_init.py @@ -1,4 +1,5 @@ """Tests for Islamic Prayer Times init.""" + from datetime import timedelta from unittest.mock import patch @@ -88,10 +89,13 @@ async def test_options_listener(hass: HomeAssistant) -> None: entry = MockConfigEntry(domain=islamic_prayer_times.DOMAIN, data={}) entry.add_to_hass(hass) - with patch( - "prayer_times_calculator.PrayerTimesCalculator.fetch_prayer_times", - return_value=PRAYER_TIMES, - ) as mock_fetch_prayer_times, freeze_time(NOW): + with ( + patch( + "prayer_times_calculator.PrayerTimesCalculator.fetch_prayer_times", + return_value=PRAYER_TIMES, + ) as mock_fetch_prayer_times, + freeze_time(NOW), + ): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() assert mock_fetch_prayer_times.call_count == 1 @@ -108,10 +112,13 @@ async def test_update_failed(hass: HomeAssistant) -> None: entry = MockConfigEntry(domain=islamic_prayer_times.DOMAIN, data={}) entry.add_to_hass(hass) - with patch( - "prayer_times_calculator.PrayerTimesCalculator.fetch_prayer_times", - return_value=PRAYER_TIMES, - ), freeze_time(NOW): + with ( + patch( + "prayer_times_calculator.PrayerTimesCalculator.fetch_prayer_times", + return_value=PRAYER_TIMES, + ), + freeze_time(NOW), + ): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -175,10 +182,13 @@ async def test_migrate_unique_id( ) assert entity.unique_id == old_unique_id - with patch( - "prayer_times_calculator.PrayerTimesCalculator.fetch_prayer_times", - return_value=PRAYER_TIMES, - ), freeze_time(NOW): + with ( + patch( + "prayer_times_calculator.PrayerTimesCalculator.fetch_prayer_times", + return_value=PRAYER_TIMES, + ), + freeze_time(NOW), + ): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -195,10 +205,13 @@ async def test_migration_from_1_1_to_1_2(hass: HomeAssistant) -> None: ) entry.add_to_hass(hass) - with patch( - "prayer_times_calculator.PrayerTimesCalculator.fetch_prayer_times", - return_value=PRAYER_TIMES, - ), freeze_time(NOW): + with ( + patch( + "prayer_times_calculator.PrayerTimesCalculator.fetch_prayer_times", + return_value=PRAYER_TIMES, + ), + freeze_time(NOW), + ): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/islamic_prayer_times/test_sensor.py b/tests/components/islamic_prayer_times/test_sensor.py index 164ac8818fe..22629819e05 100644 --- a/tests/components/islamic_prayer_times/test_sensor.py +++ b/tests/components/islamic_prayer_times/test_sensor.py @@ -1,4 +1,5 @@ """The tests for the Islamic prayer times sensor platform.""" + from unittest.mock import patch from freezegun import freeze_time @@ -37,10 +38,13 @@ async def test_islamic_prayer_times_sensors( entry = MockConfigEntry(domain=DOMAIN, data={}) entry.add_to_hass(hass) - with patch( - "prayer_times_calculator.PrayerTimesCalculator.fetch_prayer_times", - return_value=PRAYER_TIMES, - ), freeze_time(NOW): + with ( + patch( + "prayer_times_calculator.PrayerTimesCalculator.fetch_prayer_times", + return_value=PRAYER_TIMES, + ), + freeze_time(NOW), + ): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() assert hass.states.get(sensor_name).state == PRAYER_TIMES[key] diff --git a/tests/components/iss/test_config_flow.py b/tests/components/iss/test_config_flow.py index fec4d9b192c..77a61eaa770 100644 --- a/tests/components/iss/test_config_flow.py +++ b/tests/components/iss/test_config_flow.py @@ -1,4 +1,5 @@ """Test iss config flow.""" + from unittest.mock import patch from homeassistant import data_entry_flow diff --git a/tests/components/isy994/test_config_flow.py b/tests/components/isy994/test_config_flow.py index 4a5bfb007f0..b29b1dbc775 100644 --- a/tests/components/isy994/test_config_flow.py +++ b/tests/components/isy994/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Universal Devices ISY/IoX config flow.""" + import re from unittest.mock import patch @@ -90,10 +91,13 @@ async def test_form(hass: HomeAssistant) -> None: assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} - with patch(PATCH_CONNECTION, return_value=MOCK_CONFIG_RESPONSE), patch( - PATCH_ASYNC_SETUP_ENTRY, - return_value=True, - ) as mock_setup_entry: + with ( + patch(PATCH_CONNECTION, return_value=MOCK_CONFIG_RESPONSE), + patch( + PATCH_ASYNC_SETUP_ENTRY, + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], MOCK_USER_INPUT, @@ -283,10 +287,13 @@ async def test_form_ssdp(hass: HomeAssistant) -> None: assert result["step_id"] == "user" assert result["errors"] == {} - with patch(PATCH_CONNECTION, return_value=MOCK_CONFIG_RESPONSE), patch( - PATCH_ASYNC_SETUP_ENTRY, - return_value=True, - ) as mock_setup_entry: + with ( + patch(PATCH_CONNECTION, return_value=MOCK_CONFIG_RESPONSE), + patch( + PATCH_ASYNC_SETUP_ENTRY, + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], MOCK_USER_INPUT, @@ -442,10 +449,13 @@ async def test_form_dhcp(hass: HomeAssistant) -> None: assert result["step_id"] == "user" assert result["errors"] == {} - with patch(PATCH_CONNECTION, return_value=MOCK_CONFIG_RESPONSE), patch( - PATCH_ASYNC_SETUP_ENTRY, - return_value=True, - ) as mock_setup_entry: + with ( + patch(PATCH_CONNECTION, return_value=MOCK_CONFIG_RESPONSE), + patch( + PATCH_ASYNC_SETUP_ENTRY, + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], MOCK_USER_INPUT, @@ -479,10 +489,13 @@ async def test_form_dhcp_with_polisy(hass: HomeAssistant) -> None: == "http://1.2.3.4:8080" ) - with patch(PATCH_CONNECTION, return_value=MOCK_CONFIG_RESPONSE), patch( - PATCH_ASYNC_SETUP_ENTRY, - return_value=True, - ) as mock_setup_entry: + with ( + patch(PATCH_CONNECTION, return_value=MOCK_CONFIG_RESPONSE), + patch( + PATCH_ASYNC_SETUP_ENTRY, + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], MOCK_IOX_USER_INPUT, @@ -516,10 +529,13 @@ async def test_form_dhcp_with_eisy(hass: HomeAssistant) -> None: == "http://1.2.3.4:8080" ) - with patch(PATCH_CONNECTION, return_value=MOCK_CONFIG_RESPONSE), patch( - PATCH_ASYNC_SETUP_ENTRY, - return_value=True, - ) as mock_setup_entry: + with ( + patch(PATCH_CONNECTION, return_value=MOCK_CONFIG_RESPONSE), + patch( + PATCH_ASYNC_SETUP_ENTRY, + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], MOCK_IOX_USER_INPUT, @@ -665,10 +681,13 @@ async def test_reauth(hass: HomeAssistant) -> None: assert result3["type"] == "form" assert result3["errors"] == {"base": "cannot_connect"} - with patch(PATCH_CONNECTION, return_value=MOCK_CONFIG_RESPONSE), patch( - "homeassistant.components.isy994.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch(PATCH_CONNECTION, return_value=MOCK_CONFIG_RESPONSE), + patch( + "homeassistant.components.isy994.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result4 = await hass.config_entries.flow.async_configure( result3["flow_id"], { diff --git a/tests/components/isy994/test_system_health.py b/tests/components/isy994/test_system_health.py index fdc0c634f8a..5f472189513 100644 --- a/tests/components/isy994/test_system_health.py +++ b/tests/components/isy994/test_system_health.py @@ -1,4 +1,5 @@ """Test ISY system health.""" + import asyncio from unittest.mock import Mock @@ -27,6 +28,7 @@ async def test_system_health( hass.config.components.add(DOMAIN) assert await async_setup_component(hass, "system_health", {}) + await hass.async_block_till_done() MockConfigEntry( domain=DOMAIN, @@ -66,6 +68,7 @@ async def test_system_health_failed_connect( hass.config.components.add(DOMAIN) assert await async_setup_component(hass, "system_health", {}) + await hass.async_block_till_done() MockConfigEntry( domain=DOMAIN, diff --git a/tests/components/izone/test_config_flow.py b/tests/components/izone/test_config_flow.py index 479c66c8b13..0988640d644 100644 --- a/tests/components/izone/test_config_flow.py +++ b/tests/components/izone/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for iZone.""" + from unittest.mock import Mock, patch import pytest @@ -30,12 +31,15 @@ def _mock_start_discovery(hass, mock_disco): async def test_not_found(hass: HomeAssistant, mock_disco) -> None: """Test not finding iZone controller.""" - with patch( - "homeassistant.components.izone.config_flow.async_start_discovery_service" - ) as start_disco, patch( - "homeassistant.components.izone.config_flow.async_stop_discovery_service", - return_value=None, - ) as stop_disco: + with ( + patch( + "homeassistant.components.izone.config_flow.async_start_discovery_service" + ) as start_disco, + patch( + "homeassistant.components.izone.config_flow.async_stop_discovery_service", + return_value=None, + ) as stop_disco, + ): start_disco.side_effect = _mock_start_discovery(hass, mock_disco) result = await hass.config_entries.flow.async_init( IZONE, context={"source": config_entries.SOURCE_USER} @@ -56,14 +60,18 @@ async def test_found(hass: HomeAssistant, mock_disco) -> None: """Test not finding iZone controller.""" mock_disco.pi_disco.controllers["blah"] = object() - with patch( - "homeassistant.components.izone.climate.async_setup_entry", - return_value=True, - ) as mock_setup, patch( - "homeassistant.components.izone.config_flow.async_start_discovery_service" - ) as start_disco, patch( - "homeassistant.components.izone.async_start_discovery_service", - return_value=None, + with ( + patch( + "homeassistant.components.izone.climate.async_setup_entry", + return_value=True, + ) as mock_setup, + patch( + "homeassistant.components.izone.config_flow.async_start_discovery_service" + ) as start_disco, + patch( + "homeassistant.components.izone.async_start_discovery_service", + return_value=None, + ), ): start_disco.side_effect = _mock_start_discovery(hass, mock_disco) result = await hass.config_entries.flow.async_init( diff --git a/tests/components/jellyfin/__init__.py b/tests/components/jellyfin/__init__.py index c1f7bbb2f35..7db0ba2d8a3 100644 --- a/tests/components/jellyfin/__init__.py +++ b/tests/components/jellyfin/__init__.py @@ -1,4 +1,5 @@ """Tests for the jellyfin integration.""" + import json from typing import Any diff --git a/tests/components/jellyfin/conftest.py b/tests/components/jellyfin/conftest.py index 671c9881ae0..ea46c669af7 100644 --- a/tests/components/jellyfin/conftest.py +++ b/tests/components/jellyfin/conftest.py @@ -1,4 +1,5 @@ """Fixtures for Jellyfin integration tests.""" + from __future__ import annotations from collections.abc import Generator diff --git a/tests/components/jellyfin/test_config_flow.py b/tests/components/jellyfin/test_config_flow.py index c59efd7efb9..23c530d7e4d 100644 --- a/tests/components/jellyfin/test_config_flow.py +++ b/tests/components/jellyfin/test_config_flow.py @@ -1,4 +1,5 @@ """Test the jellyfin config flow.""" + from unittest.mock import MagicMock import pytest diff --git a/tests/components/jellyfin/test_diagnostics.py b/tests/components/jellyfin/test_diagnostics.py index b56d864eaac..bd34e3a8e31 100644 --- a/tests/components/jellyfin/test_diagnostics.py +++ b/tests/components/jellyfin/test_diagnostics.py @@ -1,4 +1,5 @@ """Test Jellyfin diagnostics.""" + from syrupy import SnapshotAssertion from homeassistant.core import HomeAssistant diff --git a/tests/components/jellyfin/test_init.py b/tests/components/jellyfin/test_init.py index eb184592bb8..6e6a0f7219b 100644 --- a/tests/components/jellyfin/test_init.py +++ b/tests/components/jellyfin/test_init.py @@ -1,4 +1,5 @@ """Tests for the Jellyfin integration.""" + from unittest.mock import MagicMock from homeassistant.components.jellyfin.const import DOMAIN @@ -66,6 +67,7 @@ async def test_invalid_auth( mock_config_entry.add_to_hass(hass) assert not await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 diff --git a/tests/components/jellyfin/test_media_player.py b/tests/components/jellyfin/test_media_player.py index 00fe230b31f..3263639a32f 100644 --- a/tests/components/jellyfin/test_media_player.py +++ b/tests/components/jellyfin/test_media_player.py @@ -1,4 +1,5 @@ """Tests for the Jellyfin media_player platform.""" + from datetime import timedelta from unittest.mock import MagicMock diff --git a/tests/components/jellyfin/test_media_source.py b/tests/components/jellyfin/test_media_source.py index e87b3c15b0b..b8bbfea00d9 100644 --- a/tests/components/jellyfin/test_media_source.py +++ b/tests/components/jellyfin/test_media_source.py @@ -1,4 +1,5 @@ """Tests for the Jellyfin media_player platform.""" + from unittest.mock import MagicMock import pytest diff --git a/tests/components/jellyfin/test_sensor.py b/tests/components/jellyfin/test_sensor.py index e1377d81100..40a3e62a6c0 100644 --- a/tests/components/jellyfin/test_sensor.py +++ b/tests/components/jellyfin/test_sensor.py @@ -1,4 +1,5 @@ """Tests for the Jellyfin sensor platform.""" + from unittest.mock import MagicMock from homeassistant.components.jellyfin.const import DOMAIN diff --git a/tests/components/jewish_calendar/__init__.py b/tests/components/jewish_calendar/__init__.py index f92bfe7d71e..e1352f789ac 100644 --- a/tests/components/jewish_calendar/__init__.py +++ b/tests/components/jewish_calendar/__init__.py @@ -1,4 +1,5 @@ """Tests for the jewish_calendar component.""" + from collections import namedtuple from datetime import datetime diff --git a/tests/components/jewish_calendar/test_binary_sensor.py b/tests/components/jewish_calendar/test_binary_sensor.py index d14ae0faad2..bced831462a 100644 --- a/tests/components/jewish_calendar/test_binary_sensor.py +++ b/tests/components/jewish_calendar/test_binary_sensor.py @@ -1,4 +1,5 @@ """The tests for the Jewish calendar binary sensors.""" + from datetime import datetime as dt, timedelta import pytest diff --git a/tests/components/jewish_calendar/test_sensor.py b/tests/components/jewish_calendar/test_sensor.py index 0f2912e9de3..d9f43236965 100644 --- a/tests/components/jewish_calendar/test_sensor.py +++ b/tests/components/jewish_calendar/test_sensor.py @@ -1,4 +1,5 @@ """The tests for the Jewish calendar sensors.""" + from datetime import datetime as dt, timedelta import pytest diff --git a/tests/components/juicenet/test_config_flow.py b/tests/components/juicenet/test_config_flow.py index 6adc841862e..2a4be10b49b 100644 --- a/tests/components/juicenet/test_config_flow.py +++ b/tests/components/juicenet/test_config_flow.py @@ -1,4 +1,5 @@ """Test the JuiceNet config flow.""" + from unittest.mock import MagicMock, patch import aiohttp @@ -25,14 +26,18 @@ async def test_form(hass: HomeAssistant) -> None: assert result["type"] == "form" assert result["errors"] == {} - with patch( - "homeassistant.components.juicenet.config_flow.Api.get_devices", - return_value=MagicMock(), - ), patch( - "homeassistant.components.juicenet.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.juicenet.async_setup_entry", return_value=True - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.juicenet.config_flow.Api.get_devices", + return_value=MagicMock(), + ), + patch( + "homeassistant.components.juicenet.async_setup", return_value=True + ) as mock_setup, + patch( + "homeassistant.components.juicenet.async_setup_entry", return_value=True + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_ACCESS_TOKEN: "access_token"} ) @@ -102,14 +107,18 @@ async def test_form_catch_unknown_errors(hass: HomeAssistant) -> None: async def test_import(hass: HomeAssistant) -> None: """Test that import works as expected.""" - with patch( - "homeassistant.components.juicenet.config_flow.Api.get_devices", - return_value=MagicMock(), - ), patch( - "homeassistant.components.juicenet.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.juicenet.async_setup_entry", return_value=True - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.juicenet.config_flow.Api.get_devices", + return_value=MagicMock(), + ), + patch( + "homeassistant.components.juicenet.async_setup", return_value=True + ) as mock_setup, + patch( + "homeassistant.components.juicenet.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}, diff --git a/tests/components/justnimbus/test_config_flow.py b/tests/components/justnimbus/test_config_flow.py index 8db8dd09b23..b96b6c8aa5c 100644 --- a/tests/components/justnimbus/test_config_flow.py +++ b/tests/components/justnimbus/test_config_flow.py @@ -1,4 +1,5 @@ """Test the JustNimbus config flow.""" + from unittest.mock import patch from justnimbus.exceptions import InvalidClientID, JustNimbusError @@ -27,7 +28,7 @@ async def test_form(hass: HomeAssistant) -> None: @pytest.mark.parametrize( ("side_effect", "errors"), - ( + [ ( InvalidClientID(client_id="test_id"), {"base": "invalid_auth"}, @@ -40,7 +41,7 @@ async def test_form(hass: HomeAssistant) -> None: RuntimeError, {"base": "unknown"}, ), - ), + ], ) async def test_form_errors( hass: HomeAssistant, @@ -94,10 +95,13 @@ async def test_abort_already_configured(hass: HomeAssistant) -> None: async def _set_up_justnimbus(hass: HomeAssistant, flow_id: str) -> None: """Reusable successful setup of JustNimbus sensor.""" - with patch("justnimbus.JustNimbusClient.get_data"), patch( - "homeassistant.components.justnimbus.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch("justnimbus.JustNimbusClient.get_data"), + patch( + "homeassistant.components.justnimbus.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( flow_id=flow_id, user_input=FIXTURE_USER_INPUT, diff --git a/tests/components/justnimbus/test_init.py b/tests/components/justnimbus/test_init.py index 223e36d2bbc..fb4a40acb9b 100644 --- a/tests/components/justnimbus/test_init.py +++ b/tests/components/justnimbus/test_init.py @@ -1,4 +1,5 @@ """Tests for JustNimbus initialization.""" + from homeassistant.components.justnimbus.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant diff --git a/tests/components/jvc_projector/conftest.py b/tests/components/jvc_projector/conftest.py index 091aad9e849..10603e8ae39 100644 --- a/tests/components/jvc_projector/conftest.py +++ b/tests/components/jvc_projector/conftest.py @@ -27,7 +27,7 @@ def fixture_mock_device(request) -> Generator[None, AsyncMock, None]: device.port = MOCK_PORT device.mac = MOCK_MAC device.model = MOCK_MODEL - device.get_state.return_value = {"power": "standby"} + device.get_state.return_value = {"power": "standby", "input": "hdmi1"} yield device diff --git a/tests/components/jvc_projector/test_coordinator.py b/tests/components/jvc_projector/test_coordinator.py index cfda3728eb0..24297348653 100644 --- a/tests/components/jvc_projector/test_coordinator.py +++ b/tests/components/jvc_projector/test_coordinator.py @@ -23,7 +23,7 @@ async def test_coordinator_update( mock_integration: MockConfigEntry, ) -> None: """Test coordinator update runs.""" - mock_device.get_state.return_value = {"power": "standby"} + mock_device.get_state.return_value = {"power": "standby", "input": "hdmi1"} async_fire_time_changed( hass, utcnow() + timedelta(seconds=INTERVAL_SLOW.seconds + 1) ) @@ -65,7 +65,7 @@ async def test_coordinator_device_on( mock_config_entry: MockConfigEntry, ) -> None: """Test coordinator changes update interval when device is on.""" - mock_device.get_state.return_value = {"power": "on"} + mock_device.get_state.return_value = {"power": "on", "input": "hdmi1"} mock_config_entry.add_to_hass(hass) await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/jvc_projector/test_sensor.py b/tests/components/jvc_projector/test_sensor.py new file mode 100644 index 00000000000..1827363e5ad --- /dev/null +++ b/tests/components/jvc_projector/test_sensor.py @@ -0,0 +1,24 @@ +"""Tests for the JVC Projector binary sensor device.""" + +from unittest.mock import MagicMock + +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from tests.common import MockConfigEntry + +POWER_ID = "sensor.jvc_projector_power_status" + + +async def test_entity_state( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + mock_device: MagicMock, + mock_integration: MockConfigEntry, +) -> None: + """Tests entity state is registered.""" + state = hass.states.get(POWER_ID) + assert state + assert entity_registry.async_get(state.entity_id) + + assert state.state == "standby" diff --git a/tests/components/keenetic_ndms2/__init__.py b/tests/components/keenetic_ndms2/__init__.py index f6bc72cb1e4..8ca91d00386 100644 --- a/tests/components/keenetic_ndms2/__init__.py +++ b/tests/components/keenetic_ndms2/__init__.py @@ -1,4 +1,5 @@ """Tests for the Keenetic NDMS2 component.""" + from homeassistant.components import ssdp from homeassistant.components.keenetic_ndms2 import const from homeassistant.const import ( diff --git a/tests/components/kegtron/test_config_flow.py b/tests/components/kegtron/test_config_flow.py index ccc4774e3df..4e21dc238bc 100644 --- a/tests/components/kegtron/test_config_flow.py +++ b/tests/components/kegtron/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Kegtron config flow.""" + from unittest.mock import patch from homeassistant import config_entries diff --git a/tests/components/kegtron/test_sensor.py b/tests/components/kegtron/test_sensor.py index 9825df00cc3..bbae920afff 100644 --- a/tests/components/kegtron/test_sensor.py +++ b/tests/components/kegtron/test_sensor.py @@ -1,4 +1,5 @@ """Test the Kegtron sensors.""" + from homeassistant.components.kegtron.const import DOMAIN from homeassistant.components.sensor import ATTR_STATE_CLASS from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT diff --git a/tests/components/keymitt_ble/__init__.py b/tests/components/keymitt_ble/__init__.py index c6e56739d76..242c1ebe7d6 100644 --- a/tests/components/keymitt_ble/__init__.py +++ b/tests/components/keymitt_ble/__init__.py @@ -1,4 +1,5 @@ """Tests for the MicroBot integration.""" + from unittest.mock import patch from homeassistant.components.bluetooth import BluetoothServiceInfoBleak diff --git a/tests/components/keymitt_ble/test_config_flow.py b/tests/components/keymitt_ble/test_config_flow.py index 3bf2dcc954c..7e60bdfca53 100644 --- a/tests/components/keymitt_ble/test_config_flow.py +++ b/tests/components/keymitt_ble/test_config_flow.py @@ -1,4 +1,5 @@ """Test the MicroBot config flow.""" + from unittest.mock import ANY, AsyncMock, patch from homeassistant.config_entries import SOURCE_BLUETOOTH, SOURCE_USER @@ -130,9 +131,12 @@ async def test_user_setup_already_configured(hass: HomeAssistant) -> None: async def test_user_no_devices(hass: HomeAssistant) -> None: """Test the user initiated form with valid mac.""" - with patch_microbot_api(), patch( - "homeassistant.components.keymitt_ble.config_flow.async_discovered_service_info", - return_value=[], + with ( + patch_microbot_api(), + patch( + "homeassistant.components.keymitt_ble.config_flow.async_discovered_service_info", + return_value=[], + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} @@ -144,9 +148,12 @@ async def test_user_no_devices(hass: HomeAssistant) -> None: async def test_no_link(hass: HomeAssistant) -> None: """Test the user initiated form with invalid response.""" - with patch_microbot_api(), patch( - "homeassistant.components.keymitt_ble.config_flow.async_discovered_service_info", - return_value=[SERVICE_INFO], + with ( + patch_microbot_api(), + patch( + "homeassistant.components.keymitt_ble.config_flow.async_discovered_service_info", + return_value=[SERVICE_INFO], + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} @@ -163,10 +170,13 @@ async def test_no_link(hass: HomeAssistant) -> None: assert result2["type"] == FlowResultType.FORM assert result2["step_id"] == "link" - with patch( - "homeassistant.components.keymitt_ble.config_flow.MicroBotApiClient", - MockMicroBotApiClientFail, - ), patch_async_setup_entry() as mock_setup_entry: + with ( + patch( + "homeassistant.components.keymitt_ble.config_flow.MicroBotApiClient", + MockMicroBotApiClientFail, + ), + patch_async_setup_entry() as mock_setup_entry, + ): result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], USER_INPUT, diff --git a/tests/components/kira/test_init.py b/tests/components/kira/test_init.py index 50c2f4f4651..6b6ad4c1fcf 100644 --- a/tests/components/kira/test_init.py +++ b/tests/components/kira/test_init.py @@ -1,4 +1,5 @@ """The tests for Kira.""" + import os import shutil import tempfile diff --git a/tests/components/kira/test_remote.py b/tests/components/kira/test_remote.py index 105d457bf89..ff3b28617d3 100644 --- a/tests/components/kira/test_remote.py +++ b/tests/components/kira/test_remote.py @@ -1,4 +1,5 @@ """The tests for Kira sensor platform.""" + from unittest.mock import MagicMock from homeassistant.components.kira import remote as kira @@ -15,8 +16,7 @@ DEVICES = [] def add_entities(devices): """Mock add devices.""" - for device in devices: - DEVICES.append(device) + DEVICES.extend(devices) def test_service_call(hass: HomeAssistant) -> None: diff --git a/tests/components/kira/test_sensor.py b/tests/components/kira/test_sensor.py index fec5f982f61..fe0fc95a918 100644 --- a/tests/components/kira/test_sensor.py +++ b/tests/components/kira/test_sensor.py @@ -1,4 +1,5 @@ """The tests for Kira sensor platform.""" + from unittest.mock import MagicMock, patch from homeassistant.components.kira import sensor as kira @@ -13,8 +14,7 @@ DEVICES = [] def add_entities(devices): """Mock add devices.""" - for device in devices: - DEVICES.append(device) + DEVICES.extend(devices) @patch("homeassistant.components.kira.sensor.KiraReceiver.schedule_update_ha_state") @@ -39,7 +39,7 @@ def test_kira_sensor_callback( codeTuple = (codeName, deviceName) sensor._update_callback(codeTuple) - mock_schedule_update_ha_state.assert_called + mock_schedule_update_ha_state.assert_called() assert sensor.state == codeName assert sensor.extra_state_attributes == {kira.CONF_DEVICE: deviceName} diff --git a/tests/components/kitchen_sink/snapshots/test_lawn_mower.ambr b/tests/components/kitchen_sink/snapshots/test_lawn_mower.ambr index 879e78d5534..4189de18ce4 100644 --- a/tests/components/kitchen_sink/snapshots/test_lawn_mower.ambr +++ b/tests/components/kitchen_sink/snapshots/test_lawn_mower.ambr @@ -9,6 +9,7 @@ 'context': , 'entity_id': 'lawn_mower.mower_can_do_all', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'docked', }), @@ -20,6 +21,7 @@ 'context': , 'entity_id': 'lawn_mower.mower_can_dock', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'mowing', }), @@ -31,6 +33,7 @@ 'context': , 'entity_id': 'lawn_mower.mower_can_mow', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'docked', }), @@ -42,6 +45,7 @@ 'context': , 'entity_id': 'lawn_mower.mower_can_pause', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'docked', }), @@ -53,6 +57,7 @@ 'context': , 'entity_id': 'lawn_mower.mower_is_paused', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'paused', }), diff --git a/tests/components/kitchen_sink/snapshots/test_lock.ambr b/tests/components/kitchen_sink/snapshots/test_lock.ambr index 9303401bdd5..616d778e6fd 100644 --- a/tests/components/kitchen_sink/snapshots/test_lock.ambr +++ b/tests/components/kitchen_sink/snapshots/test_lock.ambr @@ -9,6 +9,7 @@ 'context': , 'entity_id': 'lock.another_basic_lock', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unlocked', }), @@ -20,6 +21,7 @@ 'context': , 'entity_id': 'lock.another_openable_lock', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unlocked', }), @@ -31,6 +33,7 @@ 'context': , 'entity_id': 'lock.basic_lock', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'locked', }), @@ -42,6 +45,7 @@ 'context': , 'entity_id': 'lock.openable_lock', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'locked', }), diff --git a/tests/components/kitchen_sink/snapshots/test_sensor.ambr b/tests/components/kitchen_sink/snapshots/test_sensor.ambr index 776a0c03369..bbf88c84eca 100644 --- a/tests/components/kitchen_sink/snapshots/test_sensor.ambr +++ b/tests/components/kitchen_sink/snapshots/test_sensor.ambr @@ -11,6 +11,7 @@ 'context': , 'entity_id': 'sensor.outlet_1_power', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '50', }), @@ -24,6 +25,7 @@ 'context': , 'entity_id': 'sensor.outlet_2_power', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '1500', }), @@ -36,6 +38,7 @@ 'context': , 'entity_id': 'sensor.statistics_issues_issue_1', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '100', }), @@ -48,6 +51,7 @@ 'context': , 'entity_id': 'sensor.statistics_issues_issue_2', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '100', }), @@ -59,6 +63,7 @@ 'context': , 'entity_id': 'sensor.statistics_issues_issue_3', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '100', }), diff --git a/tests/components/kitchen_sink/snapshots/test_switch.ambr b/tests/components/kitchen_sink/snapshots/test_switch.ambr index 89688e9e54e..1cd903a59d6 100644 --- a/tests/components/kitchen_sink/snapshots/test_switch.ambr +++ b/tests/components/kitchen_sink/snapshots/test_switch.ambr @@ -7,6 +7,7 @@ 'context': , 'entity_id': 'switch.outlet_1', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }) @@ -112,6 +113,7 @@ 'context': , 'entity_id': 'switch.outlet_2', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }) diff --git a/tests/components/kitchen_sink/test_config_flow.py b/tests/components/kitchen_sink/test_config_flow.py index e157c3e5d0a..86c1698669e 100644 --- a/tests/components/kitchen_sink/test_config_flow.py +++ b/tests/components/kitchen_sink/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Everything but the Kitchen Sink config flow.""" + from unittest.mock import patch from homeassistant import config_entries, data_entry_flow, setup diff --git a/tests/components/kitchen_sink/test_image.py b/tests/components/kitchen_sink/test_image.py index 4c64bd77eb2..d85530ecad6 100644 --- a/tests/components/kitchen_sink/test_image.py +++ b/tests/components/kitchen_sink/test_image.py @@ -1,4 +1,5 @@ """The tests for the kitchen_sink image platform.""" + from http import HTTPStatus from pathlib import Path from unittest.mock import patch diff --git a/tests/components/kitchen_sink/test_init.py b/tests/components/kitchen_sink/test_init.py index b3f303fcfe1..1547a10bd2b 100644 --- a/tests/components/kitchen_sink/test_init.py +++ b/tests/components/kitchen_sink/test_init.py @@ -1,4 +1,5 @@ """The tests for the Everything but the Kitchen Sink integration.""" + import datetime from http import HTTPStatus from unittest.mock import ANY @@ -244,9 +245,7 @@ async def test_issues_created( "description_placeholders": None, "flow_id": flow_id, "handler": DOMAIN, - "minor_version": 1, "type": "create_entry", - "version": 1, } await ws_client.send_json({"id": 4, "type": "repairs/list_issues"}) diff --git a/tests/components/kitchen_sink/test_lawn_mower.py b/tests/components/kitchen_sink/test_lawn_mower.py index efd1b7485ab..48914ab5a46 100644 --- a/tests/components/kitchen_sink/test_lawn_mower.py +++ b/tests/components/kitchen_sink/test_lawn_mower.py @@ -1,4 +1,5 @@ """The tests for the kitchen_sink lawn mower platform.""" + from unittest.mock import patch import pytest diff --git a/tests/components/kitchen_sink/test_lock.py b/tests/components/kitchen_sink/test_lock.py index a74c9a19a23..ad5e9b7515d 100644 --- a/tests/components/kitchen_sink/test_lock.py +++ b/tests/components/kitchen_sink/test_lock.py @@ -1,4 +1,5 @@ """The tests for the kitchen_sink lock platform.""" + from unittest.mock import patch import pytest diff --git a/tests/components/kitchen_sink/test_sensor.py b/tests/components/kitchen_sink/test_sensor.py index 8d3f611f15d..c4b5f03499e 100644 --- a/tests/components/kitchen_sink/test_sensor.py +++ b/tests/components/kitchen_sink/test_sensor.py @@ -1,4 +1,5 @@ """The tests for the kitchen_sink sensor platform.""" + from unittest.mock import patch import pytest diff --git a/tests/components/kmtronic/conftest.py b/tests/components/kmtronic/conftest.py index 4310f99242e..98205288aa3 100644 --- a/tests/components/kmtronic/conftest.py +++ b/tests/components/kmtronic/conftest.py @@ -1,4 +1,5 @@ """Define fixtures for kmtronic tests.""" + from collections.abc import Generator from unittest.mock import AsyncMock, patch diff --git a/tests/components/kmtronic/test_config_flow.py b/tests/components/kmtronic/test_config_flow.py index ba8f2f5b87e..222bb8bead2 100644 --- a/tests/components/kmtronic/test_config_flow.py +++ b/tests/components/kmtronic/test_config_flow.py @@ -1,4 +1,5 @@ """Test the kmtronic config flow.""" + from http import HTTPStatus from unittest.mock import AsyncMock, Mock, patch diff --git a/tests/components/kmtronic/test_switch.py b/tests/components/kmtronic/test_switch.py index fa60dddf1cd..a5b0e7a4cba 100644 --- a/tests/components/kmtronic/test_switch.py +++ b/tests/components/kmtronic/test_switch.py @@ -1,4 +1,5 @@ """The tests for the KMtronic switch platform.""" + from datetime import timedelta from http import HTTPStatus diff --git a/tests/components/knx/conftest.py b/tests/components/knx/conftest.py index f2feaac2f08..bd724029516 100644 --- a/tests/components/knx/conftest.py +++ b/tests/components/knx/conftest.py @@ -1,4 +1,5 @@ """Conftest for the KNX integration.""" + from __future__ import annotations import asyncio diff --git a/tests/components/knx/test_binary_sensor.py b/tests/components/knx/test_binary_sensor.py index aace7a0224c..b9216aa149a 100644 --- a/tests/components/knx/test_binary_sensor.py +++ b/tests/components/knx/test_binary_sensor.py @@ -1,4 +1,5 @@ """Test KNX binary sensor.""" + from datetime import timedelta from homeassistant.components.knx.const import CONF_STATE_ADDRESS, CONF_SYNC_STATE diff --git a/tests/components/knx/test_button.py b/tests/components/knx/test_button.py index 3dedea7d8d4..613208d5595 100644 --- a/tests/components/knx/test_button.py +++ b/tests/components/knx/test_button.py @@ -1,4 +1,5 @@ """Test KNX button.""" + from datetime import timedelta import logging diff --git a/tests/components/knx/test_config_flow.py b/tests/components/knx/test_config_flow.py index 0f2d8e56050..f5b3d9595d0 100644 --- a/tests/components/knx/test_config_flow.py +++ b/tests/components/knx/test_config_flow.py @@ -1,4 +1,5 @@ """Test the KNX config flow.""" + from contextlib import contextmanager from unittest.mock import Mock, patch @@ -61,26 +62,34 @@ GATEWAY_INDIVIDUAL_ADDRESS = IndividualAddress("1.0.0") @pytest.fixture(name="knx_setup") def fixture_knx_setup(): """Mock KNX entry setup.""" - with patch("homeassistant.components.knx.async_setup", return_value=True), patch( - "homeassistant.components.knx.async_setup_entry", return_value=True - ) as mock_async_setup_entry: + with ( + patch("homeassistant.components.knx.async_setup", return_value=True), + patch( + "homeassistant.components.knx.async_setup_entry", return_value=True + ) as mock_async_setup_entry, + ): yield mock_async_setup_entry @contextmanager def patch_file_upload(return_value=FIXTURE_KEYRING, side_effect=None): """Patch file upload. Yields the Keyring instance (return_value).""" - with patch( - "homeassistant.components.knx.helpers.keyring.process_uploaded_file" - ) as file_upload_mock, patch( - "homeassistant.components.knx.helpers.keyring.sync_load_keyring", - return_value=return_value, - side_effect=side_effect, - ), patch( - "pathlib.Path.mkdir", - ) as mkdir_mock, patch( - "shutil.move", - ) as shutil_move_mock: + with ( + patch( + "homeassistant.components.knx.helpers.keyring.process_uploaded_file" + ) as file_upload_mock, + patch( + "homeassistant.components.knx.helpers.keyring.sync_load_keyring", + return_value=return_value, + side_effect=side_effect, + ), + patch( + "pathlib.Path.mkdir", + ) as mkdir_mock, + patch( + "shutil.move", + ) as shutil_move_mock, + ): file_upload_mock.return_value.__enter__.return_value = Mock() yield return_value if side_effect: diff --git a/tests/components/knx/test_date.py b/tests/components/knx/test_date.py index bfde519f3c0..d3b1ff2058e 100644 --- a/tests/components/knx/test_date.py +++ b/tests/components/knx/test_date.py @@ -1,4 +1,5 @@ """Test KNX date.""" + from homeassistant.components.date import ATTR_DATE, DOMAIN, SERVICE_SET_VALUE from homeassistant.components.knx.const import CONF_RESPOND_TO_READ, KNX_ADDRESS from homeassistant.components.knx.schema import DateSchema diff --git a/tests/components/knx/test_datetime.py b/tests/components/knx/test_datetime.py index f9d9f039367..e2dcfc8d112 100644 --- a/tests/components/knx/test_datetime.py +++ b/tests/components/knx/test_datetime.py @@ -1,4 +1,5 @@ """Test KNX date.""" + from homeassistant.components.datetime import ATTR_DATETIME, DOMAIN, SERVICE_SET_VALUE from homeassistant.components.knx.const import CONF_RESPOND_TO_READ, KNX_ADDRESS from homeassistant.components.knx.schema import DateTimeSchema diff --git a/tests/components/knx/test_device_trigger.py b/tests/components/knx/test_device_trigger.py index f3448947cf8..3c8bf58169b 100644 --- a/tests/components/knx/test_device_trigger.py +++ b/tests/components/knx/test_device_trigger.py @@ -1,4 +1,5 @@ """Tests for KNX device triggers.""" + import pytest import voluptuous_serialize diff --git a/tests/components/knx/test_events.py b/tests/components/knx/test_events.py index f5c1aed7fde..ddb9d50240c 100644 --- a/tests/components/knx/test_events.py +++ b/tests/components/knx/test_events.py @@ -1,4 +1,5 @@ """Test KNX events.""" + import logging import pytest diff --git a/tests/components/knx/test_expose.py b/tests/components/knx/test_expose.py index 4359c54164a..d2b7653cfe8 100644 --- a/tests/components/knx/test_expose.py +++ b/tests/components/knx/test_expose.py @@ -1,4 +1,5 @@ """Test KNX expose.""" + from datetime import timedelta import time from unittest.mock import patch @@ -31,14 +32,17 @@ async def test_binary_expose(hass: HomeAssistant, knx: KNXTestKit) -> None: # Change state to on hass.states.async_set(entity_id, "on", {}) + await hass.async_block_till_done() await knx.assert_write("1/1/8", True) # Change attribute; keep state hass.states.async_set(entity_id, "on", {"brightness": 180}) + await hass.async_block_till_done() await knx.assert_no_telegram() # Change attribute and state hass.states.async_set(entity_id, "off", {"brightness": 0}) + await hass.async_block_till_done() await knx.assert_write("1/1/8", False) @@ -63,10 +67,12 @@ async def test_expose_attribute(hass: HomeAssistant, knx: KNXTestKit) -> None: # Change state to "on"; no attribute hass.states.async_set(entity_id, "on", {}) + await hass.async_block_till_done() await knx.assert_telegram_count(0) # Change attribute; keep state hass.states.async_set(entity_id, "on", {attribute: 1}) + await hass.async_block_till_done() await knx.assert_write("1/1/8", (1,)) # Read in between @@ -75,22 +81,27 @@ async def test_expose_attribute(hass: HomeAssistant, knx: KNXTestKit) -> None: # Change state keep attribute hass.states.async_set(entity_id, "off", {attribute: 1}) + await hass.async_block_till_done() await knx.assert_telegram_count(0) # Change state and attribute hass.states.async_set(entity_id, "on", {attribute: 0}) + await hass.async_block_till_done() await knx.assert_write("1/1/8", (0,)) # Change state to "off"; no attribute hass.states.async_set(entity_id, "off", {}) + await hass.async_block_till_done() await knx.assert_telegram_count(0) # Change attribute; keep state hass.states.async_set(entity_id, "on", {attribute: 1}) + await hass.async_block_till_done() await knx.assert_write("1/1/8", (1,)) # Change state to "off"; null attribute hass.states.async_set(entity_id, "off", {attribute: None}) + await hass.async_block_till_done() await knx.assert_telegram_count(0) @@ -118,18 +129,22 @@ async def test_expose_attribute_with_default( # Change state to "on"; no attribute hass.states.async_set(entity_id, "on", {}) + await hass.async_block_till_done() await knx.assert_write("1/1/8", (0,)) # Change attribute; keep state hass.states.async_set(entity_id, "on", {attribute: 1}) + await hass.async_block_till_done() await knx.assert_write("1/1/8", (1,)) # Change state keep attribute hass.states.async_set(entity_id, "off", {attribute: 1}) + await hass.async_block_till_done() await knx.assert_no_telegram() # Change state and attribute hass.states.async_set(entity_id, "on", {attribute: 3}) + await hass.async_block_till_done() await knx.assert_write("1/1/8", (3,)) # Read in between @@ -138,14 +153,17 @@ async def test_expose_attribute_with_default( # Change state to "off"; no attribute hass.states.async_set(entity_id, "off", {}) + await hass.async_block_till_done() await knx.assert_write("1/1/8", (0,)) # Change state and attribute hass.states.async_set(entity_id, "on", {attribute: 1}) + await hass.async_block_till_done() await knx.assert_write("1/1/8", (1,)) # Change state to "off"; null attribute hass.states.async_set(entity_id, "off", {attribute: None}) + await hass.async_block_till_done() await knx.assert_write("1/1/8", (0,)) @@ -178,6 +196,7 @@ async def test_expose_string(hass: HomeAssistant, knx: KNXTestKit) -> None: "on", {attribute: "This is a very long string that is larger than 14 bytes"}, ) + await hass.async_block_till_done() await knx.assert_write( "1/1/8", (84, 104, 105, 115, 32, 105, 115, 32, 97, 32, 118, 101, 114, 121) ) @@ -199,13 +218,16 @@ async def test_expose_cooldown(hass: HomeAssistant, knx: KNXTestKit) -> None: ) # Change state to 1 hass.states.async_set(entity_id, "1", {}) + await hass.async_block_till_done() await knx.assert_write("1/1/8", (1,)) # Change state to 2 - skip because of cooldown hass.states.async_set(entity_id, "2", {}) + await hass.async_block_till_done() await knx.assert_no_telegram() # Change state to 3 hass.states.async_set(entity_id, "3", {}) + await hass.async_block_till_done() await knx.assert_no_telegram() # Wait for cooldown to pass async_fire_time_changed_exact( @@ -244,6 +266,7 @@ async def test_expose_conversion_exception( "on", {attribute: 101}, ) + await hass.async_block_till_done() await knx.assert_no_telegram() assert ( 'Could not expose fake.entity fake_attribute value "101.0" to KNX:' diff --git a/tests/components/knx/test_fan.py b/tests/components/knx/test_fan.py index 3e89aea7201..39cb851af51 100644 --- a/tests/components/knx/test_fan.py +++ b/tests/components/knx/test_fan.py @@ -1,4 +1,5 @@ """Test KNX fan.""" + from homeassistant.components.knx.const import KNX_ADDRESS from homeassistant.components.knx.schema import FanSchema from homeassistant.const import CONF_NAME, STATE_OFF, STATE_ON diff --git a/tests/components/knx/test_init.py b/tests/components/knx/test_init.py index a5d3d0f3263..2d2889e7718 100644 --- a/tests/components/knx/test_init.py +++ b/tests/components/knx/test_init.py @@ -1,4 +1,5 @@ """Test KNX init.""" + from unittest.mock import patch import pytest @@ -276,9 +277,10 @@ async def test_async_remove_entry( knx.mock_config_entry = config_entry await knx.setup_integration({}) - with patch("pathlib.Path.unlink") as unlink_mock, patch( - "pathlib.Path.rmdir" - ) as rmdir_mock: + with ( + patch("pathlib.Path.unlink") as unlink_mock, + patch("pathlib.Path.rmdir") as rmdir_mock, + ): assert await hass.config_entries.async_remove(config_entry.entry_id) assert unlink_mock.call_count == 3 rmdir_mock.assert_called_once() diff --git a/tests/components/knx/test_interface_device.py b/tests/components/knx/test_interface_device.py index 12ae0ac7d0e..c857022750c 100644 --- a/tests/components/knx/test_interface_device.py +++ b/tests/components/knx/test_interface_device.py @@ -1,4 +1,5 @@ """Test KNX scene.""" + from unittest.mock import patch from xknx.core import XknxConnectionState, XknxConnectionType diff --git a/tests/components/knx/test_light.py b/tests/components/knx/test_light.py index 1f2f23e9cca..a14d1bb32ae 100644 --- a/tests/components/knx/test_light.py +++ b/tests/components/knx/test_light.py @@ -1,4 +1,5 @@ """Test KNX light.""" + from __future__ import annotations from datetime import timedelta diff --git a/tests/components/knx/test_number.py b/tests/components/knx/test_number.py index a0ef514f5f7..be3fe070c10 100644 --- a/tests/components/knx/test_number.py +++ b/tests/components/knx/test_number.py @@ -1,4 +1,5 @@ """Test KNX number.""" + import pytest from homeassistant.components.knx.const import CONF_RESPOND_TO_READ, KNX_ADDRESS diff --git a/tests/components/knx/test_select.py b/tests/components/knx/test_select.py index 1b408a298a2..b53dfae2658 100644 --- a/tests/components/knx/test_select.py +++ b/tests/components/knx/test_select.py @@ -1,4 +1,5 @@ """Test KNX select.""" + import pytest from homeassistant.components.knx.const import ( diff --git a/tests/components/knx/test_sensor.py b/tests/components/knx/test_sensor.py index 10178324c93..22d9993b58f 100644 --- a/tests/components/knx/test_sensor.py +++ b/tests/components/knx/test_sensor.py @@ -1,4 +1,5 @@ """Test KNX sensor.""" + from homeassistant.components.knx.const import CONF_STATE_ADDRESS, CONF_SYNC_STATE from homeassistant.components.knx.schema import SensorSchema from homeassistant.const import CONF_NAME, CONF_TYPE, STATE_UNKNOWN diff --git a/tests/components/knx/test_services.py b/tests/components/knx/test_services.py index 30b297218cc..e93f59ba574 100644 --- a/tests/components/knx/test_services.py +++ b/tests/components/knx/test_services.py @@ -1,4 +1,5 @@ """Test KNX services.""" + from unittest.mock import patch import pytest @@ -207,6 +208,7 @@ async def test_exposure_register(hass: HomeAssistant, knx: KNXTestKit) -> None: # no exposure registered hass.states.async_set(test_entity, STATE_ON, {}) + await hass.async_block_till_done() await knx.assert_no_telegram() # register exposure @@ -217,6 +219,7 @@ async def test_exposure_register(hass: HomeAssistant, knx: KNXTestKit) -> None: blocking=True, ) hass.states.async_set(test_entity, STATE_OFF, {}) + await hass.async_block_till_done() await knx.assert_write(test_address, False) # register exposure @@ -227,6 +230,7 @@ async def test_exposure_register(hass: HomeAssistant, knx: KNXTestKit) -> None: blocking=True, ) hass.states.async_set(test_entity, STATE_ON, {}) + await hass.async_block_till_done() await knx.assert_no_telegram() # register exposure for attribute with default @@ -244,14 +248,18 @@ async def test_exposure_register(hass: HomeAssistant, knx: KNXTestKit) -> None: ) # no attribute on first change wouldn't work because no attribute change since last test hass.states.async_set(test_entity, STATE_ON, {test_attribute: 30}) + await hass.async_block_till_done() await knx.assert_write(test_address, (30,)) hass.states.async_set(test_entity, STATE_OFF, {}) + await hass.async_block_till_done() await knx.assert_write(test_address, (0,)) # don't send same value sequentially hass.states.async_set(test_entity, STATE_ON, {test_attribute: 25}) hass.states.async_set(test_entity, STATE_ON, {test_attribute: 25}) hass.states.async_set(test_entity, STATE_ON, {test_attribute: 25, "unrelated": 2}) hass.states.async_set(test_entity, STATE_OFF, {test_attribute: 25}) + await hass.async_block_till_done() + await hass.async_block_till_done() await knx.assert_telegram_count(1) await knx.assert_write(test_address, (25,)) @@ -263,11 +271,13 @@ async def test_reload_service( """Test reload service.""" await knx.setup_integration({}) - with patch( - "homeassistant.components.knx.async_unload_entry", wraps=knx_async_unload_entry - ) as mock_unload_entry, patch( - "homeassistant.components.knx.async_setup_entry" - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.knx.async_unload_entry", + wraps=knx_async_unload_entry, + ) as mock_unload_entry, + patch("homeassistant.components.knx.async_setup_entry") as mock_setup_entry, + ): await hass.services.async_call( "knx", "reload", diff --git a/tests/components/knx/test_switch.py b/tests/components/knx/test_switch.py index d68970537ab..8dce4cf9c27 100644 --- a/tests/components/knx/test_switch.py +++ b/tests/components/knx/test_switch.py @@ -1,4 +1,5 @@ """Test KNX switch.""" + from homeassistant.components.knx.const import ( CONF_RESPOND_TO_READ, CONF_STATE_ADDRESS, diff --git a/tests/components/knx/test_telegrams.py b/tests/components/knx/test_telegrams.py index be24b3cd3ec..844fc073d61 100644 --- a/tests/components/knx/test_telegrams.py +++ b/tests/components/knx/test_telegrams.py @@ -1,4 +1,5 @@ """KNX Telegrams Tests.""" + from copy import copy from datetime import datetime from typing import Any diff --git a/tests/components/knx/test_text.py b/tests/components/knx/test_text.py index 77f96100b89..e50f3056979 100644 --- a/tests/components/knx/test_text.py +++ b/tests/components/knx/test_text.py @@ -1,4 +1,5 @@ """Test KNX number.""" + from homeassistant.components.knx.const import CONF_RESPOND_TO_READ, KNX_ADDRESS from homeassistant.components.knx.schema import TextSchema from homeassistant.const import CONF_NAME diff --git a/tests/components/knx/test_time.py b/tests/components/knx/test_time.py index 25a22fe8146..9dc4c401ed8 100644 --- a/tests/components/knx/test_time.py +++ b/tests/components/knx/test_time.py @@ -1,4 +1,5 @@ """Test KNX time.""" + from homeassistant.components.knx.const import CONF_RESPOND_TO_READ, KNX_ADDRESS from homeassistant.components.knx.schema import TimeSchema from homeassistant.components.time import ATTR_TIME, DOMAIN, SERVICE_SET_VALUE diff --git a/tests/components/knx/test_weather.py b/tests/components/knx/test_weather.py index 8aaf4fa4338..0adcc309252 100644 --- a/tests/components/knx/test_weather.py +++ b/tests/components/knx/test_weather.py @@ -1,4 +1,5 @@ """Test KNX weather.""" + from homeassistant.components.knx.schema import WeatherSchema from homeassistant.components.weather import ( ATTR_CONDITION_EXCEPTIONAL, diff --git a/tests/components/knx/test_websocket.py b/tests/components/knx/test_websocket.py index 5e5d46af4a6..78cbb98a7a0 100644 --- a/tests/components/knx/test_websocket.py +++ b/tests/components/knx/test_websocket.py @@ -1,4 +1,5 @@ """KNX Websocket Tests.""" + from typing import Any from unittest.mock import patch @@ -72,11 +73,12 @@ async def test_knx_project_file_process( "password": _password, } ) - with patch( - "homeassistant.components.knx.project.process_uploaded_file", - ) as file_upload_mock, patch( - "xknxproject.XKNXProj.parse", return_value=_parse_result - ) as parse_mock: + with ( + patch( + "homeassistant.components.knx.project.process_uploaded_file", + ) as file_upload_mock, + patch("xknxproject.XKNXProj.parse", return_value=_parse_result) as parse_mock, + ): file_upload_mock.return_value.__enter__.return_value = "" res = await client.receive_json() @@ -105,11 +107,12 @@ async def test_knx_project_file_process_error( "password": "", } ) - with patch( - "homeassistant.components.knx.project.process_uploaded_file", - ) as file_upload_mock, patch( - "xknxproject.XKNXProj.parse", side_effect=ValueError - ) as parse_mock: + with ( + patch( + "homeassistant.components.knx.project.process_uploaded_file", + ) as file_upload_mock, + patch("xknxproject.XKNXProj.parse", side_effect=ValueError) as parse_mock, + ): file_upload_mock.return_value.__enter__.return_value = "" res = await client.receive_json() parse_mock.assert_called_once_with() diff --git a/tests/components/kodi/__init__.py b/tests/components/kodi/__init__.py index bef576dd6bf..d55a67ba235 100644 --- a/tests/components/kodi/__init__.py +++ b/tests/components/kodi/__init__.py @@ -1,4 +1,5 @@ """Tests for the Kodi integration.""" + from unittest.mock import patch from homeassistant.components.kodi.const import CONF_WS_PORT, DOMAIN @@ -30,12 +31,16 @@ async def init_integration(hass) -> MockConfigEntry: entry = MockConfigEntry(domain=DOMAIN, data=entry_data, title="name") entry.add_to_hass(hass) - with patch("homeassistant.components.kodi.Kodi.ping", return_value=True), patch( - "homeassistant.components.kodi.Kodi.get_application_properties", - return_value={"version": {"major": 1, "minor": 1}}, - ), patch( - "homeassistant.components.kodi.get_kodi_connection", - return_value=MockConnection(), + with ( + patch("homeassistant.components.kodi.Kodi.ping", return_value=True), + patch( + "homeassistant.components.kodi.Kodi.get_application_properties", + return_value={"version": {"major": 1, "minor": 1}}, + ), + patch( + "homeassistant.components.kodi.get_kodi_connection", + return_value=MockConnection(), + ), ): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/kodi/test_config_flow.py b/tests/components/kodi/test_config_flow.py index 419254bd738..ecc3bc1f672 100644 --- a/tests/components/kodi/test_config_flow.py +++ b/tests/components/kodi/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Kodi config flow.""" + from unittest.mock import AsyncMock, PropertyMock, patch import pytest @@ -41,16 +42,20 @@ async def user_flow(hass): async def test_user_flow(hass: HomeAssistant, user_flow) -> None: """Test a successful user initiated flow.""" - with patch( - "homeassistant.components.kodi.config_flow.Kodi.ping", - return_value=True, - ), patch( - "homeassistant.components.kodi.config_flow.get_kodi_connection", - return_value=MockConnection(), - ), patch( - "homeassistant.components.kodi.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.kodi.config_flow.Kodi.ping", + return_value=True, + ), + patch( + "homeassistant.components.kodi.config_flow.get_kodi_connection", + return_value=MockConnection(), + ), + patch( + "homeassistant.components.kodi.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result = await hass.config_entries.flow.async_configure(user_flow, TEST_HOST) await hass.async_block_till_done() @@ -70,12 +75,15 @@ async def test_user_flow(hass: HomeAssistant, user_flow) -> None: async def test_form_valid_auth(hass: HomeAssistant, user_flow) -> None: """Test we handle valid auth.""" - with patch( - "homeassistant.components.kodi.config_flow.Kodi.ping", - side_effect=InvalidAuthError, - ), patch( - "homeassistant.components.kodi.config_flow.get_kodi_connection", - return_value=MockConnection(), + with ( + patch( + "homeassistant.components.kodi.config_flow.Kodi.ping", + side_effect=InvalidAuthError, + ), + patch( + "homeassistant.components.kodi.config_flow.get_kodi_connection", + return_value=MockConnection(), + ), ): result = await hass.config_entries.flow.async_configure(user_flow, TEST_HOST) @@ -83,16 +91,20 @@ async def test_form_valid_auth(hass: HomeAssistant, user_flow) -> None: assert result["step_id"] == "credentials" assert result["errors"] == {} - with patch( - "homeassistant.components.kodi.config_flow.Kodi.ping", - return_value=True, - ), patch( - "homeassistant.components.kodi.config_flow.get_kodi_connection", - return_value=MockConnection(), - ), patch( - "homeassistant.components.kodi.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.kodi.config_flow.Kodi.ping", + return_value=True, + ), + patch( + "homeassistant.components.kodi.config_flow.get_kodi_connection", + return_value=MockConnection(), + ), + patch( + "homeassistant.components.kodi.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result = await hass.config_entries.flow.async_configure( result["flow_id"], TEST_CREDENTIALS ) @@ -113,16 +125,20 @@ async def test_form_valid_auth(hass: HomeAssistant, user_flow) -> None: async def test_form_valid_ws_port(hass: HomeAssistant, user_flow) -> None: """Test we handle valid websocket port.""" - with patch( - "homeassistant.components.kodi.config_flow.Kodi.ping", - return_value=True, - ), patch.object( - MockWSConnection, - "connect", - AsyncMock(side_effect=CannotConnectError), - ), patch( - "homeassistant.components.kodi.config_flow.get_kodi_connection", - new=get_kodi_connection, + with ( + patch( + "homeassistant.components.kodi.config_flow.Kodi.ping", + return_value=True, + ), + patch.object( + MockWSConnection, + "connect", + AsyncMock(side_effect=CannotConnectError), + ), + patch( + "homeassistant.components.kodi.config_flow.get_kodi_connection", + new=get_kodi_connection, + ), ): result = await hass.config_entries.flow.async_configure(user_flow, TEST_HOST) @@ -130,16 +146,20 @@ async def test_form_valid_ws_port(hass: HomeAssistant, user_flow) -> None: assert result["step_id"] == "ws_port" assert result["errors"] == {} - with patch( - "homeassistant.components.kodi.config_flow.Kodi.ping", - return_value=True, - ), patch( - "homeassistant.components.kodi.config_flow.get_kodi_connection", - return_value=MockConnection(), - ), patch( - "homeassistant.components.kodi.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.kodi.config_flow.Kodi.ping", + return_value=True, + ), + patch( + "homeassistant.components.kodi.config_flow.get_kodi_connection", + return_value=MockConnection(), + ), + patch( + "homeassistant.components.kodi.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result = await hass.config_entries.flow.async_configure( result["flow_id"], TEST_WS_PORT ) @@ -161,16 +181,20 @@ async def test_form_valid_ws_port(hass: HomeAssistant, user_flow) -> None: async def test_form_empty_ws_port(hass: HomeAssistant, user_flow) -> None: """Test we handle an empty websocket port input.""" - with patch( - "homeassistant.components.kodi.config_flow.Kodi.ping", - return_value=True, - ), patch.object( - MockWSConnection, - "connect", - AsyncMock(side_effect=CannotConnectError), - ), patch( - "homeassistant.components.kodi.config_flow.get_kodi_connection", - new=get_kodi_connection, + with ( + patch( + "homeassistant.components.kodi.config_flow.Kodi.ping", + return_value=True, + ), + patch.object( + MockWSConnection, + "connect", + AsyncMock(side_effect=CannotConnectError), + ), + patch( + "homeassistant.components.kodi.config_flow.get_kodi_connection", + new=get_kodi_connection, + ), ): result = await hass.config_entries.flow.async_configure(user_flow, TEST_HOST) @@ -203,12 +227,15 @@ async def test_form_empty_ws_port(hass: HomeAssistant, user_flow) -> None: async def test_form_invalid_auth(hass: HomeAssistant, user_flow) -> None: """Test we handle invalid auth.""" - with patch( - "homeassistant.components.kodi.config_flow.Kodi.ping", - side_effect=InvalidAuthError, - ), patch( - "homeassistant.components.kodi.config_flow.get_kodi_connection", - return_value=MockConnection(), + with ( + patch( + "homeassistant.components.kodi.config_flow.Kodi.ping", + side_effect=InvalidAuthError, + ), + patch( + "homeassistant.components.kodi.config_flow.get_kodi_connection", + return_value=MockConnection(), + ), ): result = await hass.config_entries.flow.async_configure(user_flow, TEST_HOST) @@ -216,12 +243,15 @@ async def test_form_invalid_auth(hass: HomeAssistant, user_flow) -> None: assert result["step_id"] == "credentials" assert result["errors"] == {} - with patch( - "homeassistant.components.kodi.config_flow.Kodi.ping", - side_effect=InvalidAuthError, - ), patch( - "homeassistant.components.kodi.config_flow.get_kodi_connection", - return_value=MockConnection(), + with ( + patch( + "homeassistant.components.kodi.config_flow.Kodi.ping", + side_effect=InvalidAuthError, + ), + patch( + "homeassistant.components.kodi.config_flow.get_kodi_connection", + return_value=MockConnection(), + ), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], TEST_CREDENTIALS @@ -231,12 +261,15 @@ async def test_form_invalid_auth(hass: HomeAssistant, user_flow) -> None: assert result["step_id"] == "credentials" assert result["errors"] == {"base": "invalid_auth"} - with patch( - "homeassistant.components.kodi.config_flow.Kodi.ping", - side_effect=CannotConnectError, - ), patch( - "homeassistant.components.kodi.config_flow.get_kodi_connection", - return_value=MockConnection(), + with ( + patch( + "homeassistant.components.kodi.config_flow.Kodi.ping", + side_effect=CannotConnectError, + ), + patch( + "homeassistant.components.kodi.config_flow.get_kodi_connection", + return_value=MockConnection(), + ), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], TEST_CREDENTIALS @@ -246,12 +279,15 @@ async def test_form_invalid_auth(hass: HomeAssistant, user_flow) -> None: assert result["step_id"] == "credentials" assert result["errors"] == {"base": "cannot_connect"} - with patch( - "homeassistant.components.kodi.config_flow.Kodi.ping", - side_effect=Exception, - ), patch( - "homeassistant.components.kodi.config_flow.get_kodi_connection", - return_value=MockConnection(), + with ( + patch( + "homeassistant.components.kodi.config_flow.Kodi.ping", + side_effect=Exception, + ), + patch( + "homeassistant.components.kodi.config_flow.get_kodi_connection", + return_value=MockConnection(), + ), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], TEST_CREDENTIALS @@ -261,16 +297,20 @@ async def test_form_invalid_auth(hass: HomeAssistant, user_flow) -> None: assert result["step_id"] == "credentials" assert result["errors"] == {"base": "unknown"} - with patch( - "homeassistant.components.kodi.config_flow.Kodi.ping", - return_value=True, - ), patch.object( - MockWSConnection, - "connect", - AsyncMock(side_effect=CannotConnectError), - ), patch( - "homeassistant.components.kodi.config_flow.get_kodi_connection", - new=get_kodi_connection, + with ( + patch( + "homeassistant.components.kodi.config_flow.Kodi.ping", + return_value=True, + ), + patch.object( + MockWSConnection, + "connect", + AsyncMock(side_effect=CannotConnectError), + ), + patch( + "homeassistant.components.kodi.config_flow.get_kodi_connection", + new=get_kodi_connection, + ), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], TEST_CREDENTIALS @@ -283,12 +323,15 @@ async def test_form_invalid_auth(hass: HomeAssistant, user_flow) -> None: async def test_form_cannot_connect_http(hass: HomeAssistant, user_flow) -> None: """Test we handle cannot connect over HTTP error.""" - with patch( - "homeassistant.components.kodi.config_flow.Kodi.ping", - side_effect=CannotConnectError, - ), patch( - "homeassistant.components.kodi.config_flow.get_kodi_connection", - return_value=MockConnection(), + with ( + patch( + "homeassistant.components.kodi.config_flow.Kodi.ping", + side_effect=CannotConnectError, + ), + patch( + "homeassistant.components.kodi.config_flow.get_kodi_connection", + return_value=MockConnection(), + ), ): result = await hass.config_entries.flow.async_configure(user_flow, TEST_HOST) @@ -299,12 +342,15 @@ async def test_form_cannot_connect_http(hass: HomeAssistant, user_flow) -> None: async def test_form_exception_http(hass: HomeAssistant, user_flow) -> None: """Test we handle generic exception over HTTP.""" - with patch( - "homeassistant.components.kodi.config_flow.Kodi.ping", - side_effect=Exception, - ), patch( - "homeassistant.components.kodi.config_flow.get_kodi_connection", - return_value=MockConnection(), + with ( + patch( + "homeassistant.components.kodi.config_flow.Kodi.ping", + side_effect=Exception, + ), + patch( + "homeassistant.components.kodi.config_flow.get_kodi_connection", + return_value=MockConnection(), + ), ): result = await hass.config_entries.flow.async_configure(user_flow, TEST_HOST) @@ -315,16 +361,20 @@ async def test_form_exception_http(hass: HomeAssistant, user_flow) -> None: async def test_form_cannot_connect_ws(hass: HomeAssistant, user_flow) -> None: """Test we handle cannot connect over WebSocket error.""" - with patch( - "homeassistant.components.kodi.config_flow.Kodi.ping", - return_value=True, - ), patch.object( - MockWSConnection, - "connect", - AsyncMock(side_effect=CannotConnectError), - ), patch( - "homeassistant.components.kodi.config_flow.get_kodi_connection", - new=get_kodi_connection, + with ( + patch( + "homeassistant.components.kodi.config_flow.Kodi.ping", + return_value=True, + ), + patch.object( + MockWSConnection, + "connect", + AsyncMock(side_effect=CannotConnectError), + ), + patch( + "homeassistant.components.kodi.config_flow.get_kodi_connection", + new=get_kodi_connection, + ), ): result = await hass.config_entries.flow.async_configure(user_flow, TEST_HOST) @@ -332,14 +382,18 @@ async def test_form_cannot_connect_ws(hass: HomeAssistant, user_flow) -> None: assert result["step_id"] == "ws_port" assert result["errors"] == {} - with patch( - "homeassistant.components.kodi.config_flow.Kodi.ping", - return_value=True, - ), patch.object( - MockWSConnection, "connected", new_callable=PropertyMock(return_value=False) - ), patch( - "homeassistant.components.kodi.config_flow.get_kodi_connection", - new=get_kodi_connection, + with ( + patch( + "homeassistant.components.kodi.config_flow.Kodi.ping", + return_value=True, + ), + patch.object( + MockWSConnection, "connected", new_callable=PropertyMock(return_value=False) + ), + patch( + "homeassistant.components.kodi.config_flow.get_kodi_connection", + new=get_kodi_connection, + ), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], TEST_WS_PORT @@ -349,12 +403,15 @@ async def test_form_cannot_connect_ws(hass: HomeAssistant, user_flow) -> None: assert result["step_id"] == "ws_port" assert result["errors"] == {"base": "cannot_connect"} - with patch( - "homeassistant.components.kodi.config_flow.Kodi.ping", - side_effect=CannotConnectError, - ), patch( - "homeassistant.components.kodi.config_flow.get_kodi_connection", - new=get_kodi_connection, + with ( + patch( + "homeassistant.components.kodi.config_flow.Kodi.ping", + side_effect=CannotConnectError, + ), + patch( + "homeassistant.components.kodi.config_flow.get_kodi_connection", + new=get_kodi_connection, + ), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], TEST_WS_PORT @@ -367,16 +424,20 @@ async def test_form_cannot_connect_ws(hass: HomeAssistant, user_flow) -> None: async def test_form_exception_ws(hass: HomeAssistant, user_flow) -> None: """Test we handle generic exception over WebSocket.""" - with patch( - "homeassistant.components.kodi.config_flow.Kodi.ping", - return_value=True, - ), patch.object( - MockWSConnection, - "connect", - AsyncMock(side_effect=CannotConnectError), - ), patch( - "homeassistant.components.kodi.config_flow.get_kodi_connection", - new=get_kodi_connection, + with ( + patch( + "homeassistant.components.kodi.config_flow.Kodi.ping", + return_value=True, + ), + patch.object( + MockWSConnection, + "connect", + AsyncMock(side_effect=CannotConnectError), + ), + patch( + "homeassistant.components.kodi.config_flow.get_kodi_connection", + new=get_kodi_connection, + ), ): result = await hass.config_entries.flow.async_configure(user_flow, TEST_HOST) @@ -384,14 +445,16 @@ async def test_form_exception_ws(hass: HomeAssistant, user_flow) -> None: assert result["step_id"] == "ws_port" assert result["errors"] == {} - with patch( - "homeassistant.components.kodi.config_flow.Kodi.ping", - return_value=True, - ), patch.object( - MockWSConnection, "connect", AsyncMock(side_effect=Exception) - ), patch( - "homeassistant.components.kodi.config_flow.get_kodi_connection", - new=get_kodi_connection, + with ( + patch( + "homeassistant.components.kodi.config_flow.Kodi.ping", + return_value=True, + ), + patch.object(MockWSConnection, "connect", AsyncMock(side_effect=Exception)), + patch( + "homeassistant.components.kodi.config_flow.get_kodi_connection", + new=get_kodi_connection, + ), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], TEST_WS_PORT @@ -404,12 +467,15 @@ async def test_form_exception_ws(hass: HomeAssistant, user_flow) -> None: async def test_discovery(hass: HomeAssistant) -> None: """Test discovery flow works.""" - with patch( - "homeassistant.components.kodi.config_flow.Kodi.ping", - return_value=True, - ), patch( - "homeassistant.components.kodi.config_flow.get_kodi_connection", - return_value=MockConnection(), + with ( + patch( + "homeassistant.components.kodi.config_flow.Kodi.ping", + return_value=True, + ), + patch( + "homeassistant.components.kodi.config_flow.get_kodi_connection", + return_value=MockConnection(), + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -445,12 +511,15 @@ async def test_discovery(hass: HomeAssistant) -> None: async def test_discovery_cannot_connect_http(hass: HomeAssistant) -> None: """Test discovery aborts if cannot connect.""" - with patch( - "homeassistant.components.kodi.config_flow.Kodi.ping", - side_effect=CannotConnectError, - ), patch( - "homeassistant.components.kodi.config_flow.get_kodi_connection", - return_value=MockConnection(), + with ( + patch( + "homeassistant.components.kodi.config_flow.Kodi.ping", + side_effect=CannotConnectError, + ), + patch( + "homeassistant.components.kodi.config_flow.get_kodi_connection", + return_value=MockConnection(), + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -464,16 +533,20 @@ async def test_discovery_cannot_connect_http(hass: HomeAssistant) -> None: async def test_discovery_cannot_connect_ws(hass: HomeAssistant) -> None: """Test discovery aborts if cannot connect to websocket.""" - with patch( - "homeassistant.components.kodi.config_flow.Kodi.ping", - return_value=True, - ), patch.object( - MockWSConnection, - "connect", - AsyncMock(side_effect=CannotConnectError), - ), patch( - "homeassistant.components.kodi.config_flow.get_kodi_connection", - new=get_kodi_connection, + with ( + patch( + "homeassistant.components.kodi.config_flow.Kodi.ping", + return_value=True, + ), + patch.object( + MockWSConnection, + "connect", + AsyncMock(side_effect=CannotConnectError), + ), + patch( + "homeassistant.components.kodi.config_flow.get_kodi_connection", + new=get_kodi_connection, + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -488,12 +561,15 @@ async def test_discovery_cannot_connect_ws(hass: HomeAssistant) -> None: async def test_discovery_exception_http(hass: HomeAssistant, user_flow) -> None: """Test we handle generic exception during discovery validation.""" - with patch( - "homeassistant.components.kodi.config_flow.Kodi.ping", - side_effect=Exception, - ), patch( - "homeassistant.components.kodi.config_flow.get_kodi_connection", - return_value=MockConnection(), + with ( + patch( + "homeassistant.components.kodi.config_flow.Kodi.ping", + side_effect=Exception, + ), + patch( + "homeassistant.components.kodi.config_flow.get_kodi_connection", + return_value=MockConnection(), + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -507,12 +583,15 @@ async def test_discovery_exception_http(hass: HomeAssistant, user_flow) -> None: async def test_discovery_invalid_auth(hass: HomeAssistant) -> None: """Test we handle invalid auth during discovery.""" - with patch( - "homeassistant.components.kodi.config_flow.Kodi.ping", - side_effect=InvalidAuthError, - ), patch( - "homeassistant.components.kodi.config_flow.get_kodi_connection", - return_value=MockConnection(), + with ( + patch( + "homeassistant.components.kodi.config_flow.Kodi.ping", + side_effect=InvalidAuthError, + ), + patch( + "homeassistant.components.kodi.config_flow.get_kodi_connection", + return_value=MockConnection(), + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -527,12 +606,15 @@ async def test_discovery_invalid_auth(hass: HomeAssistant) -> None: async def test_discovery_duplicate_data(hass: HomeAssistant) -> None: """Test discovery aborts if same mDNS packet arrives.""" - with patch( - "homeassistant.components.kodi.config_flow.Kodi.ping", - return_value=True, - ), patch( - "homeassistant.components.kodi.config_flow.get_kodi_connection", - return_value=MockConnection(), + with ( + patch( + "homeassistant.components.kodi.config_flow.Kodi.ping", + return_value=True, + ), + patch( + "homeassistant.components.kodi.config_flow.get_kodi_connection", + return_value=MockConnection(), + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -587,16 +669,20 @@ async def test_discovery_without_unique_id(hass: HomeAssistant) -> None: async def test_form_import(hass: HomeAssistant) -> None: """Test we get the form with import source.""" - with patch( - "homeassistant.components.kodi.config_flow.Kodi.ping", - return_value=True, - ), patch( - "homeassistant.components.kodi.config_flow.get_kodi_connection", - return_value=MockConnection(), - ), patch( - "homeassistant.components.kodi.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.kodi.config_flow.Kodi.ping", + return_value=True, + ), + patch( + "homeassistant.components.kodi.config_flow.get_kodi_connection", + return_value=MockConnection(), + ), + patch( + "homeassistant.components.kodi.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}, @@ -613,12 +699,15 @@ async def test_form_import(hass: HomeAssistant) -> None: async def test_form_import_invalid_auth(hass: HomeAssistant) -> None: """Test we handle invalid auth on import.""" - with patch( - "homeassistant.components.kodi.config_flow.Kodi.ping", - side_effect=InvalidAuthError, - ), patch( - "homeassistant.components.kodi.config_flow.get_kodi_connection", - return_value=MockConnection(), + with ( + patch( + "homeassistant.components.kodi.config_flow.Kodi.ping", + side_effect=InvalidAuthError, + ), + patch( + "homeassistant.components.kodi.config_flow.get_kodi_connection", + return_value=MockConnection(), + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -632,12 +721,15 @@ async def test_form_import_invalid_auth(hass: HomeAssistant) -> None: async def test_form_import_cannot_connect(hass: HomeAssistant) -> None: """Test we handle cannot connect on import.""" - with patch( - "homeassistant.components.kodi.config_flow.Kodi.ping", - side_effect=CannotConnectError, - ), patch( - "homeassistant.components.kodi.config_flow.get_kodi_connection", - return_value=MockConnection(), + with ( + patch( + "homeassistant.components.kodi.config_flow.Kodi.ping", + side_effect=CannotConnectError, + ), + patch( + "homeassistant.components.kodi.config_flow.get_kodi_connection", + return_value=MockConnection(), + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -651,12 +743,15 @@ async def test_form_import_cannot_connect(hass: HomeAssistant) -> None: async def test_form_import_exception(hass: HomeAssistant) -> None: """Test we handle unknown exception on import.""" - with patch( - "homeassistant.components.kodi.config_flow.Kodi.ping", - side_effect=Exception, - ), patch( - "homeassistant.components.kodi.config_flow.get_kodi_connection", - return_value=MockConnection(), + with ( + patch( + "homeassistant.components.kodi.config_flow.Kodi.ping", + side_effect=Exception, + ), + patch( + "homeassistant.components.kodi.config_flow.get_kodi_connection", + return_value=MockConnection(), + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, diff --git a/tests/components/kodi/test_device_trigger.py b/tests/components/kodi/test_device_trigger.py index 4dbfe3abb57..1737fe5d7c9 100644 --- a/tests/components/kodi/test_device_trigger.py +++ b/tests/components/kodi/test_device_trigger.py @@ -1,4 +1,5 @@ """The tests for Kodi device triggers.""" + import pytest import homeassistant.components.automation as automation diff --git a/tests/components/kodi/test_init.py b/tests/components/kodi/test_init.py index 9c6d67ff120..8b9c5efbaf9 100644 --- a/tests/components/kodi/test_init.py +++ b/tests/components/kodi/test_init.py @@ -1,4 +1,5 @@ """Test the Kodi integration init.""" + from unittest.mock import patch from homeassistant.components.kodi.const import DOMAIN diff --git a/tests/components/kodi/util.py b/tests/components/kodi/util.py index 2b9d819c244..dba0822b1d8 100644 --- a/tests/components/kodi/util.py +++ b/tests/components/kodi/util.py @@ -1,4 +1,5 @@ """Test the Kodi config flow.""" + from ipaddress import ip_address from homeassistant.components import zeroconf diff --git a/tests/components/konnected/test_config_flow.py b/tests/components/konnected/test_config_flow.py index a6dcce08889..c46e115d159 100644 --- a/tests/components/konnected/test_config_flow.py +++ b/tests/components/konnected/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for Konnected Alarm Panel config flow.""" + from unittest.mock import patch import pytest diff --git a/tests/components/konnected/test_init.py b/tests/components/konnected/test_init.py index 658f1053f93..1a2da88624d 100644 --- a/tests/components/konnected/test_init.py +++ b/tests/components/konnected/test_init.py @@ -1,4 +1,5 @@ """Test Konnected setup process.""" + from http import HTTPStatus from unittest.mock import patch diff --git a/tests/components/konnected/test_panel.py b/tests/components/konnected/test_panel.py index 00b4617c062..64cc414cdd3 100644 --- a/tests/components/konnected/test_panel.py +++ b/tests/components/konnected/test_panel.py @@ -1,4 +1,5 @@ """Test Konnected setup process.""" + from datetime import timedelta from unittest.mock import patch diff --git a/tests/components/kostal_plenticore/conftest.py b/tests/components/kostal_plenticore/conftest.py index a83d9fd5e17..6c97b65554d 100644 --- a/tests/components/kostal_plenticore/conftest.py +++ b/tests/components/kostal_plenticore/conftest.py @@ -1,4 +1,5 @@ """Fixtures for Kostal Plenticore tests.""" + from __future__ import annotations from collections.abc import Generator diff --git a/tests/components/kostal_plenticore/test_config_flow.py b/tests/components/kostal_plenticore/test_config_flow.py index d832dbcad47..41acfb1d136 100644 --- a/tests/components/kostal_plenticore/test_config_flow.py +++ b/tests/components/kostal_plenticore/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Kostal Plenticore Solar Inverter config flow.""" + from collections.abc import Generator from unittest.mock import ANY, AsyncMock, MagicMock, patch diff --git a/tests/components/kostal_plenticore/test_diagnostics.py b/tests/components/kostal_plenticore/test_diagnostics.py index d509a323e6a..57d1bb50bba 100644 --- a/tests/components/kostal_plenticore/test_diagnostics.py +++ b/tests/components/kostal_plenticore/test_diagnostics.py @@ -1,4 +1,5 @@ """Test Kostal Plenticore diagnostics.""" + from pykoplenti import SettingsData from homeassistant.components.diagnostics import REDACTED diff --git a/tests/components/kostal_plenticore/test_number.py b/tests/components/kostal_plenticore/test_number.py index fc7d9f213fe..41e3a6c0b6c 100644 --- a/tests/components/kostal_plenticore/test_number.py +++ b/tests/components/kostal_plenticore/test_number.py @@ -1,4 +1,5 @@ """Test Kostal Plenticore number.""" + from collections.abc import Generator from datetime import timedelta from unittest.mock import patch diff --git a/tests/components/kostal_plenticore/test_select.py b/tests/components/kostal_plenticore/test_select.py index 9af2589af9b..121300457fe 100644 --- a/tests/components/kostal_plenticore/test_select.py +++ b/tests/components/kostal_plenticore/test_select.py @@ -1,4 +1,5 @@ """Test the Kostal Plenticore Solar Inverter select platform.""" + from pykoplenti import SettingsData from homeassistant.components.kostal_plenticore.helper import Plenticore diff --git a/tests/components/kraken/conftest.py b/tests/components/kraken/conftest.py index f34dedc4df9..e75122e7f0e 100644 --- a/tests/components/kraken/conftest.py +++ b/tests/components/kraken/conftest.py @@ -1,4 +1,5 @@ """Provide common pytest fixtures for kraken tests.""" + from unittest.mock import patch import pytest diff --git a/tests/components/kraken/const.py b/tests/components/kraken/const.py index 263618dfd2d..9afd2d84007 100644 --- a/tests/components/kraken/const.py +++ b/tests/components/kraken/const.py @@ -1,4 +1,5 @@ """Constants for kraken tests.""" + import pandas as pd TRADEABLE_ASSET_PAIR_RESPONSE = pd.DataFrame( diff --git a/tests/components/kraken/test_config_flow.py b/tests/components/kraken/test_config_flow.py index 74767a9496a..e6639264291 100644 --- a/tests/components/kraken/test_config_flow.py +++ b/tests/components/kraken/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for the kraken config_flow.""" + from unittest.mock import patch from homeassistant.components.kraken.const import CONF_TRACKED_ASSET_PAIRS, DOMAIN @@ -58,15 +59,19 @@ async def test_options(hass: HomeAssistant) -> None: ) entry.add_to_hass(hass) - with patch( - "homeassistant.components.kraken.config_flow.KrakenAPI.get_tradable_asset_pairs", - return_value=TRADEABLE_ASSET_PAIR_RESPONSE, - ), patch( - "pykrakenapi.KrakenAPI.get_tradable_asset_pairs", - return_value=TRADEABLE_ASSET_PAIR_RESPONSE, - ), patch( - "pykrakenapi.KrakenAPI.get_ticker_information", - return_value=TICKER_INFORMATION_RESPONSE, + with ( + patch( + "homeassistant.components.kraken.config_flow.KrakenAPI.get_tradable_asset_pairs", + return_value=TRADEABLE_ASSET_PAIR_RESPONSE, + ), + patch( + "pykrakenapi.KrakenAPI.get_tradable_asset_pairs", + return_value=TRADEABLE_ASSET_PAIR_RESPONSE, + ), + patch( + "pykrakenapi.KrakenAPI.get_ticker_information", + return_value=TICKER_INFORMATION_RESPONSE, + ), ): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/kraken/test_init.py b/tests/components/kraken/test_init.py index 44e809eb815..8aeb7f18121 100644 --- a/tests/components/kraken/test_init.py +++ b/tests/components/kraken/test_init.py @@ -1,4 +1,5 @@ """Tests for the kraken integration.""" + from unittest.mock import patch from pykrakenapi.pykrakenapi import CallRateLimitError, KrakenAPIError @@ -14,12 +15,15 @@ from tests.common import MockConfigEntry async def test_unload_entry(hass: HomeAssistant) -> None: """Test unload for Kraken.""" - with patch( - "pykrakenapi.KrakenAPI.get_tradable_asset_pairs", - return_value=TRADEABLE_ASSET_PAIR_RESPONSE, - ), patch( - "pykrakenapi.KrakenAPI.get_ticker_information", - return_value=TICKER_INFORMATION_RESPONSE, + with ( + patch( + "pykrakenapi.KrakenAPI.get_tradable_asset_pairs", + return_value=TRADEABLE_ASSET_PAIR_RESPONSE, + ), + patch( + "pykrakenapi.KrakenAPI.get_ticker_information", + return_value=TICKER_INFORMATION_RESPONSE, + ), ): entry = MockConfigEntry(domain=DOMAIN) entry.add_to_hass(hass) @@ -34,12 +38,15 @@ async def test_unknown_error( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test unload for Kraken.""" - with patch( - "pykrakenapi.KrakenAPI.get_tradable_asset_pairs", - return_value=TRADEABLE_ASSET_PAIR_RESPONSE, - ), patch( - "pykrakenapi.KrakenAPI.get_ticker_information", - side_effect=KrakenAPIError("EQuery: Error"), + with ( + patch( + "pykrakenapi.KrakenAPI.get_tradable_asset_pairs", + return_value=TRADEABLE_ASSET_PAIR_RESPONSE, + ), + patch( + "pykrakenapi.KrakenAPI.get_ticker_information", + side_effect=KrakenAPIError("EQuery: Error"), + ), ): entry = MockConfigEntry(domain=DOMAIN) entry.add_to_hass(hass) @@ -53,12 +60,15 @@ async def test_callrate_limit( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test unload for Kraken.""" - with patch( - "pykrakenapi.KrakenAPI.get_tradable_asset_pairs", - return_value=TRADEABLE_ASSET_PAIR_RESPONSE, - ), patch( - "pykrakenapi.KrakenAPI.get_ticker_information", - side_effect=CallRateLimitError(), + with ( + patch( + "pykrakenapi.KrakenAPI.get_tradable_asset_pairs", + return_value=TRADEABLE_ASSET_PAIR_RESPONSE, + ), + patch( + "pykrakenapi.KrakenAPI.get_ticker_information", + side_effect=CallRateLimitError(), + ), ): entry = MockConfigEntry(domain=DOMAIN) entry.add_to_hass(hass) diff --git a/tests/components/kraken/test_sensor.py b/tests/components/kraken/test_sensor.py index 791b70c1283..fd0a1dc72d1 100644 --- a/tests/components/kraken/test_sensor.py +++ b/tests/components/kraken/test_sensor.py @@ -1,4 +1,5 @@ """Tests for the kraken sensor platform.""" + from datetime import timedelta from unittest.mock import patch @@ -31,12 +32,15 @@ async def test_sensor( entity_registry_enabled_by_default: None, ) -> None: """Test that sensor has a value.""" - with patch( - "pykrakenapi.KrakenAPI.get_tradable_asset_pairs", - return_value=TRADEABLE_ASSET_PAIR_RESPONSE, - ), patch( - "pykrakenapi.KrakenAPI.get_ticker_information", - return_value=TICKER_INFORMATION_RESPONSE, + with ( + patch( + "pykrakenapi.KrakenAPI.get_tradable_asset_pairs", + return_value=TRADEABLE_ASSET_PAIR_RESPONSE, + ), + patch( + "pykrakenapi.KrakenAPI.get_ticker_information", + return_value=TICKER_INFORMATION_RESPONSE, + ), ): entry = MockConfigEntry( domain=DOMAIN, @@ -139,12 +143,15 @@ async def test_sensors_available_after_restart( freezer: FrozenDateTimeFactory, ) -> None: """Test that all sensors are added again after a restart.""" - with patch( - "pykrakenapi.KrakenAPI.get_tradable_asset_pairs", - return_value=TRADEABLE_ASSET_PAIR_RESPONSE, - ), patch( - "pykrakenapi.KrakenAPI.get_ticker_information", - return_value=TICKER_INFORMATION_RESPONSE, + with ( + patch( + "pykrakenapi.KrakenAPI.get_tradable_asset_pairs", + return_value=TRADEABLE_ASSET_PAIR_RESPONSE, + ), + patch( + "pykrakenapi.KrakenAPI.get_ticker_information", + return_value=TICKER_INFORMATION_RESPONSE, + ), ): entry = MockConfigEntry( domain=DOMAIN, @@ -178,12 +185,15 @@ async def test_sensors_added_after_config_update( hass: HomeAssistant, freezer: FrozenDateTimeFactory ) -> None: """Test that sensors are added when another tracked asset pair is added.""" - with patch( - "pykrakenapi.KrakenAPI.get_tradable_asset_pairs", - return_value=TRADEABLE_ASSET_PAIR_RESPONSE, - ), patch( - "pykrakenapi.KrakenAPI.get_ticker_information", - return_value=TICKER_INFORMATION_RESPONSE, + with ( + patch( + "pykrakenapi.KrakenAPI.get_tradable_asset_pairs", + return_value=TRADEABLE_ASSET_PAIR_RESPONSE, + ), + patch( + "pykrakenapi.KrakenAPI.get_ticker_information", + return_value=TICKER_INFORMATION_RESPONSE, + ), ): entry = MockConfigEntry( domain=DOMAIN, @@ -223,13 +233,16 @@ async def test_missing_pair_marks_sensor_unavailable( hass: HomeAssistant, freezer: FrozenDateTimeFactory ) -> None: """Test that a missing tradable asset pair marks the sensor unavailable.""" - with patch( - "pykrakenapi.KrakenAPI.get_tradable_asset_pairs", - return_value=TRADEABLE_ASSET_PAIR_RESPONSE, - ) as tradeable_asset_pairs_mock, patch( - "pykrakenapi.KrakenAPI.get_ticker_information", - return_value=TICKER_INFORMATION_RESPONSE, - ) as ticket_information_mock: + with ( + patch( + "pykrakenapi.KrakenAPI.get_tradable_asset_pairs", + return_value=TRADEABLE_ASSET_PAIR_RESPONSE, + ) as tradeable_asset_pairs_mock, + patch( + "pykrakenapi.KrakenAPI.get_ticker_information", + return_value=TICKER_INFORMATION_RESPONSE, + ) as ticket_information_mock, + ): entry = MockConfigEntry( domain=DOMAIN, options={ diff --git a/tests/components/kulersky/test_config_flow.py b/tests/components/kulersky/test_config_flow.py index a09fc78797b..9638df360c8 100644 --- a/tests/components/kulersky/test_config_flow.py +++ b/tests/components/kulersky/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Kuler Sky config flow.""" + from unittest.mock import MagicMock, patch import pykulersky @@ -20,13 +21,16 @@ async def test_flow_success(hass: HomeAssistant) -> None: light = MagicMock(spec=pykulersky.Light) light.address = "AA:BB:CC:11:22:33" light.name = "Bedroom" - with patch( - "homeassistant.components.kulersky.config_flow.pykulersky.discover", - return_value=[light], - ), patch( - "homeassistant.components.kulersky.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.kulersky.config_flow.pykulersky.discover", + return_value=[light], + ), + patch( + "homeassistant.components.kulersky.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {}, @@ -49,13 +53,16 @@ async def test_flow_no_devices_found(hass: HomeAssistant) -> None: assert result["type"] == "form" assert result["errors"] is None - with patch( - "homeassistant.components.kulersky.config_flow.pykulersky.discover", - return_value=[], - ), patch( - "homeassistant.components.kulersky.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.kulersky.config_flow.pykulersky.discover", + return_value=[], + ), + patch( + "homeassistant.components.kulersky.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {}, @@ -76,13 +83,16 @@ async def test_flow_exceptions_caught(hass: HomeAssistant) -> None: assert result["type"] == "form" assert result["errors"] is None - with patch( - "homeassistant.components.kulersky.config_flow.pykulersky.discover", - side_effect=pykulersky.PykulerskyException("TEST"), - ), patch( - "homeassistant.components.kulersky.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.kulersky.config_flow.pykulersky.discover", + side_effect=pykulersky.PykulerskyException("TEST"), + ), + patch( + "homeassistant.components.kulersky.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {}, diff --git a/tests/components/kulersky/test_light.py b/tests/components/kulersky/test_light.py index b9cad7c5f9c..90f40d327e4 100644 --- a/tests/components/kulersky/test_light.py +++ b/tests/components/kulersky/test_light.py @@ -1,4 +1,5 @@ """Test the Kuler Sky lights.""" + from unittest.mock import MagicMock, patch import pykulersky diff --git a/tests/components/lacrosse_view/conftest.py b/tests/components/lacrosse_view/conftest.py index 1ea3144e4c2..8edee952bf0 100644 --- a/tests/components/lacrosse_view/conftest.py +++ b/tests/components/lacrosse_view/conftest.py @@ -1,4 +1,5 @@ """Define fixtures for LaCrosse View tests.""" + from collections.abc import Generator from unittest.mock import AsyncMock, patch diff --git a/tests/components/lacrosse_view/test_config_flow.py b/tests/components/lacrosse_view/test_config_flow.py index 075aa7a3767..195c004179b 100644 --- a/tests/components/lacrosse_view/test_config_flow.py +++ b/tests/components/lacrosse_view/test_config_flow.py @@ -1,4 +1,5 @@ """Test the LaCrosse View config flow.""" + from unittest.mock import AsyncMock, patch from lacrosse_view import Location, LoginError @@ -22,12 +23,15 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: assert result["type"] == FlowResultType.FORM assert result["errors"] is None - with patch( - "lacrosse_view.LaCrosse.login", - return_value=True, - ), patch( - "lacrosse_view.LaCrosse.get_locations", - return_value=[Location(id=1, name="Test")], + with ( + patch( + "lacrosse_view.LaCrosse.login", + return_value=True, + ), + patch( + "lacrosse_view.LaCrosse.get_locations", + return_value=[Location(id=1, name="Test")], + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -108,8 +112,9 @@ async def test_form_login_first(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch("lacrosse_view.LaCrosse.login", return_value=True), patch( - "lacrosse_view.LaCrosse.get_locations", side_effect=LoginError + with ( + patch("lacrosse_view.LaCrosse.login", return_value=True), + patch("lacrosse_view.LaCrosse.get_locations", side_effect=LoginError), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -129,9 +134,12 @@ async def test_form_no_locations(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch("lacrosse_view.LaCrosse.login", return_value=True), patch( - "lacrosse_view.LaCrosse.get_locations", - return_value=None, + with ( + patch("lacrosse_view.LaCrosse.login", return_value=True), + patch( + "lacrosse_view.LaCrosse.get_locations", + return_value=None, + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -191,12 +199,15 @@ async def test_already_configured_device( assert result["type"] == FlowResultType.FORM assert result["errors"] is None - with patch( - "lacrosse_view.LaCrosse.login", - return_value=True, - ), patch( - "lacrosse_view.LaCrosse.get_locations", - return_value=[Location(id=1, name="Test")], + with ( + patch( + "lacrosse_view.LaCrosse.login", + return_value=True, + ), + patch( + "lacrosse_view.LaCrosse.get_locations", + return_value=[Location(id=1, name="Test")], + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -256,9 +267,12 @@ async def test_reauth(hass: HomeAssistant) -> None: new_username = "new-username" new_password = "new-password" - with patch("lacrosse_view.LaCrosse.login", return_value=True), patch( - "lacrosse_view.LaCrosse.get_locations", - return_value=[Location(id=1, name="Test")], + with ( + patch("lacrosse_view.LaCrosse.login", return_value=True), + patch( + "lacrosse_view.LaCrosse.get_locations", + return_value=[Location(id=1, name="Test")], + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], diff --git a/tests/components/lacrosse_view/test_diagnostics.py b/tests/components/lacrosse_view/test_diagnostics.py index 29d6f7cacbe..08cef64a935 100644 --- a/tests/components/lacrosse_view/test_diagnostics.py +++ b/tests/components/lacrosse_view/test_diagnostics.py @@ -1,4 +1,5 @@ """Test diagnostics of LaCrosse View.""" + from unittest.mock import patch from syrupy.assertion import SnapshotAssertion @@ -24,8 +25,9 @@ async def test_entry_diagnostics( ) config_entry.add_to_hass(hass) - with patch("lacrosse_view.LaCrosse.login", return_value=True), patch( - "lacrosse_view.LaCrosse.get_sensors", return_value=[TEST_SENSOR] + with ( + patch("lacrosse_view.LaCrosse.login", return_value=True), + patch("lacrosse_view.LaCrosse.get_sensors", return_value=[TEST_SENSOR]), ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/lacrosse_view/test_init.py b/tests/components/lacrosse_view/test_init.py index 2b3f5927bd2..cf11e787ad8 100644 --- a/tests/components/lacrosse_view/test_init.py +++ b/tests/components/lacrosse_view/test_init.py @@ -1,4 +1,5 @@ """Test the LaCrosse View initialization.""" + from datetime import timedelta from unittest.mock import patch @@ -19,9 +20,12 @@ async def test_unload_entry(hass: HomeAssistant) -> None: config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_ENTRY_DATA) config_entry.add_to_hass(hass) - with patch("lacrosse_view.LaCrosse.login", return_value=True), patch( - "lacrosse_view.LaCrosse.get_sensors", - return_value=[TEST_SENSOR], + with ( + patch("lacrosse_view.LaCrosse.login", return_value=True), + patch( + "lacrosse_view.LaCrosse.get_sensors", + return_value=[TEST_SENSOR], + ), ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -62,8 +66,9 @@ async def test_http_error(hass: HomeAssistant) -> None: config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_ENTRY_DATA) config_entry.add_to_hass(hass) - with patch("lacrosse_view.LaCrosse.login", return_value=True), patch( - "lacrosse_view.LaCrosse.get_sensors", side_effect=HTTPError + with ( + patch("lacrosse_view.LaCrosse.login", return_value=True), + patch("lacrosse_view.LaCrosse.get_sensors", side_effect=HTTPError), ): assert not await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -79,9 +84,12 @@ async def test_new_token(hass: HomeAssistant, freezer: FrozenDateTimeFactory) -> config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_ENTRY_DATA) config_entry.add_to_hass(hass) - with patch("lacrosse_view.LaCrosse.login", return_value=True) as login, patch( - "lacrosse_view.LaCrosse.get_sensors", - return_value=[TEST_SENSOR], + with ( + patch("lacrosse_view.LaCrosse.login", return_value=True) as login, + patch( + "lacrosse_view.LaCrosse.get_sensors", + return_value=[TEST_SENSOR], + ), ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -92,9 +100,12 @@ async def test_new_token(hass: HomeAssistant, freezer: FrozenDateTimeFactory) -> assert len(entries) == 1 assert entries[0].state == ConfigEntryState.LOADED - with patch("lacrosse_view.LaCrosse.login", return_value=True) as login, patch( - "lacrosse_view.LaCrosse.get_sensors", - return_value=[TEST_SENSOR], + with ( + patch("lacrosse_view.LaCrosse.login", return_value=True) as login, + patch( + "lacrosse_view.LaCrosse.get_sensors", + return_value=[TEST_SENSOR], + ), ): freezer.tick(timedelta(hours=1)) async_fire_time_changed(hass) @@ -110,9 +121,12 @@ async def test_failed_token( config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_ENTRY_DATA) config_entry.add_to_hass(hass) - with patch("lacrosse_view.LaCrosse.login", return_value=True) as login, patch( - "lacrosse_view.LaCrosse.get_sensors", - return_value=[TEST_SENSOR], + with ( + patch("lacrosse_view.LaCrosse.login", return_value=True) as login, + patch( + "lacrosse_view.LaCrosse.get_sensors", + return_value=[TEST_SENSOR], + ), ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/lacrosse_view/test_sensor.py b/tests/components/lacrosse_view/test_sensor.py index 8fc028e2da1..b9140e6173f 100644 --- a/tests/components/lacrosse_view/test_sensor.py +++ b/tests/components/lacrosse_view/test_sensor.py @@ -1,4 +1,5 @@ """Test the LaCrosse View sensors.""" + from typing import Any from unittest.mock import patch @@ -31,8 +32,9 @@ async def test_entities_added(hass: HomeAssistant) -> None: config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_ENTRY_DATA) config_entry.add_to_hass(hass) - with patch("lacrosse_view.LaCrosse.login", return_value=True), patch( - "lacrosse_view.LaCrosse.get_sensors", return_value=[TEST_SENSOR] + with ( + patch("lacrosse_view.LaCrosse.login", return_value=True), + patch("lacrosse_view.LaCrosse.get_sensors", return_value=[TEST_SENSOR]), ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -52,8 +54,12 @@ async def test_sensor_permission( config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_ENTRY_DATA) config_entry.add_to_hass(hass) - with patch("lacrosse_view.LaCrosse.login", return_value=True), patch( - "lacrosse_view.LaCrosse.get_sensors", return_value=[TEST_NO_PERMISSION_SENSOR] + with ( + patch("lacrosse_view.LaCrosse.login", return_value=True), + patch( + "lacrosse_view.LaCrosse.get_sensors", + return_value=[TEST_NO_PERMISSION_SENSOR], + ), ): assert not await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -73,8 +79,11 @@ async def test_field_not_supported( config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_ENTRY_DATA) config_entry.add_to_hass(hass) - with patch("lacrosse_view.LaCrosse.login", return_value=True), patch( - "lacrosse_view.LaCrosse.get_sensors", return_value=[TEST_UNSUPPORTED_SENSOR] + with ( + patch("lacrosse_view.LaCrosse.login", return_value=True), + patch( + "lacrosse_view.LaCrosse.get_sensors", return_value=[TEST_UNSUPPORTED_SENSOR] + ), ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -105,9 +114,12 @@ async def test_field_types( config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_ENTRY_DATA) config_entry.add_to_hass(hass) - with patch("lacrosse_view.LaCrosse.login", return_value=True), patch( - "lacrosse_view.LaCrosse.get_sensors", - return_value=[test_input], + with ( + patch("lacrosse_view.LaCrosse.login", return_value=True), + patch( + "lacrosse_view.LaCrosse.get_sensors", + return_value=[test_input], + ), ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -125,9 +137,12 @@ async def test_no_field(hass: HomeAssistant, caplog: pytest.LogCaptureFixture) - config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_ENTRY_DATA) config_entry.add_to_hass(hass) - with patch("lacrosse_view.LaCrosse.login", return_value=True), patch( - "lacrosse_view.LaCrosse.get_sensors", - return_value=[TEST_NO_FIELD_SENSOR], + with ( + patch("lacrosse_view.LaCrosse.login", return_value=True), + patch( + "lacrosse_view.LaCrosse.get_sensors", + return_value=[TEST_NO_FIELD_SENSOR], + ), ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -145,9 +160,12 @@ async def test_field_data_missing(hass: HomeAssistant) -> None: config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_ENTRY_DATA) config_entry.add_to_hass(hass) - with patch("lacrosse_view.LaCrosse.login", return_value=True), patch( - "lacrosse_view.LaCrosse.get_sensors", - return_value=[TEST_MISSING_FIELD_DATA_SENSOR], + with ( + patch("lacrosse_view.LaCrosse.login", return_value=True), + patch( + "lacrosse_view.LaCrosse.get_sensors", + return_value=[TEST_MISSING_FIELD_DATA_SENSOR], + ), ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/lamarzocco/__init__.py b/tests/components/lamarzocco/__init__.py index 1e7d5ed0148..ed4d2e0990e 100644 --- a/tests/components/lamarzocco/__init__.py +++ b/tests/components/lamarzocco/__init__.py @@ -1,7 +1,10 @@ """Mock inputs for tests.""" +from lmcloud.const import LaMarzoccoModel + from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo from tests.common import MockConfigEntry @@ -15,6 +18,13 @@ PASSWORD_SELECTION = { USER_INPUT = PASSWORD_SELECTION | {CONF_USERNAME: "username"} +MODEL_DICT = { + LaMarzoccoModel.GS3_AV: ("GS01234", "GS3 AV"), + LaMarzoccoModel.GS3_MP: ("GS01234", "GS3 MP"), + LaMarzoccoModel.LINEA_MICRA: ("MR01234", "Linea Micra"), + LaMarzoccoModel.LINEA_MINI: ("LM01234", "Linea Mini"), +} + async def async_init_integration( hass: HomeAssistant, mock_config_entry: MockConfigEntry @@ -22,3 +32,24 @@ async def async_init_integration( """Set up the La Marzocco integration for testing.""" await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() + + +def get_bluetooth_service_info( + model: LaMarzoccoModel, serial: str +) -> BluetoothServiceInfo: + """Return a mocked BluetoothServiceInfo.""" + if model in (LaMarzoccoModel.GS3_AV, LaMarzoccoModel.GS3_MP): + name = f"GS3_{serial}" + elif model == LaMarzoccoModel.LINEA_MINI: + name = f"MINI_{serial}" + elif model == LaMarzoccoModel.LINEA_MICRA: + name = f"MICRA_{serial}" + return BluetoothServiceInfo( + name=name, + address="aa:bb:cc:dd:ee:ff", + rssi=-63, + manufacturer_data={}, + service_data={}, + service_uuids=[], + source="local", + ) diff --git a/tests/components/lamarzocco/conftest.py b/tests/components/lamarzocco/conftest.py index 17d605a0dde..d76e44d60af 100644 --- a/tests/components/lamarzocco/conftest.py +++ b/tests/components/lamarzocco/conftest.py @@ -7,10 +7,10 @@ from lmcloud.const import LaMarzoccoModel import pytest from homeassistant.components.lamarzocco.const import CONF_MACHINE, DOMAIN -from homeassistant.const import CONF_HOST +from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME from homeassistant.core import HomeAssistant -from . import USER_INPUT, async_init_integration +from . import MODEL_DICT, USER_INPUT, async_init_integration from tests.common import ( MockConfigEntry, @@ -28,7 +28,12 @@ def mock_config_entry( title="My LaMarzocco", domain=DOMAIN, data=USER_INPUT - | {CONF_MACHINE: mock_lamarzocco.serial_number, CONF_HOST: "host"}, + | { + CONF_MACHINE: mock_lamarzocco.serial_number, + CONF_HOST: "host", + CONF_NAME: "name", + CONF_MAC: "mac", + }, unique_id=mock_lamarzocco.serial_number, ) entry.add_to_hass(hass) @@ -58,25 +63,17 @@ def mock_lamarzocco( """Return a mocked LM client.""" model_name = device_fixture - if model_name == LaMarzoccoModel.GS3_AV: - serial_number = "GS01234" - true_model_name = "GS3 AV" - elif model_name == LaMarzoccoModel.GS3_MP: - serial_number = "GS01234" - true_model_name = "GS3 MP" - elif model_name == LaMarzoccoModel.LINEA_MICRA: - serial_number = "MR01234" - true_model_name = "Linea Micra" - elif model_name == LaMarzoccoModel.LINEA_MINI: - serial_number = "LM01234" - true_model_name = "Linea Mini" + (serial_number, true_model_name) = MODEL_DICT[model_name] - with patch( - "homeassistant.components.lamarzocco.coordinator.LaMarzoccoClient", - autospec=True, - ) as lamarzocco_mock, patch( - "homeassistant.components.lamarzocco.config_flow.LaMarzoccoClient", - new=lamarzocco_mock, + with ( + patch( + "homeassistant.components.lamarzocco.coordinator.LaMarzoccoClient", + autospec=True, + ) as lamarzocco_mock, + patch( + "homeassistant.components.lamarzocco.config_flow.LaMarzoccoClient", + new=lamarzocco_mock, + ), ): lamarzocco = lamarzocco_mock.return_value @@ -118,6 +115,9 @@ def mock_lamarzocco( lamarzocco.lm_local_api.websocket_connect = websocket_connect_mock + lamarzocco.lm_bluetooth = MagicMock() + lamarzocco.lm_bluetooth.address = "AA:BB:CC:DD:EE:FF" + yield lamarzocco @@ -130,3 +130,8 @@ def remove_local_connection( del data[CONF_HOST] hass.config_entries.async_update_entry(mock_config_entry, data=data) return mock_config_entry + + +@pytest.fixture(autouse=True) +def mock_bluetooth(enable_bluetooth): + """Auto mock bluetooth.""" diff --git a/tests/components/lamarzocco/snapshots/test_binary_sensor.ambr b/tests/components/lamarzocco/snapshots/test_binary_sensor.ambr index c05610a47a2..f08c2c28851 100644 --- a/tests/components/lamarzocco/snapshots/test_binary_sensor.ambr +++ b/tests/components/lamarzocco/snapshots/test_binary_sensor.ambr @@ -8,6 +8,7 @@ 'context': , 'entity_id': 'binary_sensor.gs01234_brewing_active', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }) @@ -54,6 +55,7 @@ 'context': , 'entity_id': 'binary_sensor.gs01234_water_tank_empty', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }) diff --git a/tests/components/lamarzocco/snapshots/test_button.ambr b/tests/components/lamarzocco/snapshots/test_button.ambr index 5cd38d914b7..023039cc6f7 100644 --- a/tests/components/lamarzocco/snapshots/test_button.ambr +++ b/tests/components/lamarzocco/snapshots/test_button.ambr @@ -7,6 +7,7 @@ 'context': , 'entity_id': 'button.gs01234_start_backflush', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }) diff --git a/tests/components/lamarzocco/snapshots/test_calendar.ambr b/tests/components/lamarzocco/snapshots/test_calendar.ambr index ee318a7fc67..676c0f1b2ad 100644 --- a/tests/components/lamarzocco/snapshots/test_calendar.ambr +++ b/tests/components/lamarzocco/snapshots/test_calendar.ambr @@ -97,6 +97,7 @@ 'context': , 'entity_id': 'calendar.gs01234_auto_on_off_schedule', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }) diff --git a/tests/components/lamarzocco/snapshots/test_number.ambr b/tests/components/lamarzocco/snapshots/test_number.ambr index 2ff24c4d5bf..da35bf718f6 100644 --- a/tests/components/lamarzocco/snapshots/test_number.ambr +++ b/tests/components/lamarzocco/snapshots/test_number.ambr @@ -13,6 +13,7 @@ 'context': , 'entity_id': 'number.gs01234_coffee_target_temperature', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '95', }) @@ -69,6 +70,7 @@ 'context': , 'entity_id': 'number.gs01234_steam_target_temperature', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '128', }) @@ -125,6 +127,7 @@ 'context': , 'entity_id': 'number.gs01234_steam_target_temperature', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '128', }) @@ -181,6 +184,7 @@ 'context': , 'entity_id': 'number.gs01234_tea_water_duration', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '1023', }) @@ -237,6 +241,7 @@ 'context': , 'entity_id': 'number.gs01234_tea_water_duration', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '1023', }) @@ -292,6 +297,7 @@ 'context': , 'entity_id': 'number.gs01234_dose_key_1', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '1023', }) @@ -309,6 +315,7 @@ 'context': , 'entity_id': 'number.gs01234_dose_key_2', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '1023', }) @@ -326,6 +333,7 @@ 'context': , 'entity_id': 'number.gs01234_dose_key_3', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '1023', }) @@ -343,6 +351,7 @@ 'context': , 'entity_id': 'number.gs01234_dose_key_4', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '1023', }) @@ -361,6 +370,7 @@ 'context': , 'entity_id': 'number.gs01234_prebrew_off_time_key_1', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '3', }) @@ -379,6 +389,7 @@ 'context': , 'entity_id': 'number.gs01234_prebrew_off_time_key_2', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '3', }) @@ -397,6 +408,7 @@ 'context': , 'entity_id': 'number.gs01234_prebrew_off_time_key_3', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '3', }) @@ -415,6 +427,7 @@ 'context': , 'entity_id': 'number.gs01234_prebrew_off_time_key_4', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '3', }) @@ -433,6 +446,7 @@ 'context': , 'entity_id': 'number.gs01234_prebrew_on_time_key_1', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '5', }) @@ -451,6 +465,7 @@ 'context': , 'entity_id': 'number.gs01234_prebrew_on_time_key_2', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '5', }) @@ -469,6 +484,7 @@ 'context': , 'entity_id': 'number.gs01234_prebrew_on_time_key_3', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '5', }) @@ -487,6 +503,7 @@ 'context': , 'entity_id': 'number.gs01234_prebrew_on_time_key_4', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '5', }) @@ -505,6 +522,7 @@ 'context': , 'entity_id': 'number.gs01234_preinfusion_time_key_1', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unavailable', }) @@ -523,6 +541,7 @@ 'context': , 'entity_id': 'number.gs01234_preinfusion_time_key_2', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unavailable', }) @@ -541,6 +560,7 @@ 'context': , 'entity_id': 'number.gs01234_preinfusion_time_key_3', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unavailable', }) @@ -559,6 +579,7 @@ 'context': , 'entity_id': 'number.gs01234_preinfusion_time_key_4', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unavailable', }) @@ -577,6 +598,7 @@ 'context': , 'entity_id': 'number.lm01234_prebrew_off_time', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '3', }) @@ -633,6 +655,7 @@ 'context': , 'entity_id': 'number.mr01234_prebrew_off_time', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '3', }) @@ -689,6 +712,7 @@ 'context': , 'entity_id': 'number.lm01234_prebrew_on_time', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '5', }) @@ -745,6 +769,7 @@ 'context': , 'entity_id': 'number.mr01234_prebrew_on_time', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '5', }) @@ -801,6 +826,7 @@ 'context': , 'entity_id': 'number.lm01234_preinfusion_time', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unavailable', }) @@ -857,6 +883,7 @@ 'context': , 'entity_id': 'number.mr01234_preinfusion_time', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unavailable', }) diff --git a/tests/components/lamarzocco/snapshots/test_select.ambr b/tests/components/lamarzocco/snapshots/test_select.ambr index e592d25c85c..1ee5ae7115f 100644 --- a/tests/components/lamarzocco/snapshots/test_select.ambr +++ b/tests/components/lamarzocco/snapshots/test_select.ambr @@ -12,6 +12,7 @@ 'context': , 'entity_id': 'select.gs01234_prebrew_infusion_mode', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }) @@ -68,6 +69,7 @@ 'context': , 'entity_id': 'select.lm01234_prebrew_infusion_mode', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }) @@ -124,6 +126,7 @@ 'context': , 'entity_id': 'select.mr01234_prebrew_infusion_mode', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }) @@ -180,6 +183,7 @@ 'context': , 'entity_id': 'select.mr01234_steam_level', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '3', }) diff --git a/tests/components/lamarzocco/snapshots/test_sensor.ambr b/tests/components/lamarzocco/snapshots/test_sensor.ambr index a97244f5472..71422b8b850 100644 --- a/tests/components/lamarzocco/snapshots/test_sensor.ambr +++ b/tests/components/lamarzocco/snapshots/test_sensor.ambr @@ -48,6 +48,7 @@ 'context': , 'entity_id': 'sensor.gs01234_current_coffee_temperature', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '93', }) @@ -101,6 +102,7 @@ 'context': , 'entity_id': 'sensor.gs01234_current_steam_temperature', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '113', }) @@ -151,6 +153,7 @@ 'context': , 'entity_id': 'sensor.gs01234_shot_timer', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0', }) @@ -200,6 +203,7 @@ 'context': , 'entity_id': 'sensor.gs01234_total_coffees_made', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '13', }) @@ -249,6 +253,7 @@ 'context': , 'entity_id': 'sensor.gs01234_total_flushes_made', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '69', }) diff --git a/tests/components/lamarzocco/snapshots/test_switch.ambr b/tests/components/lamarzocco/snapshots/test_switch.ambr index 7e6edb148b2..59053c5c478 100644 --- a/tests/components/lamarzocco/snapshots/test_switch.ambr +++ b/tests/components/lamarzocco/snapshots/test_switch.ambr @@ -29,7 +29,7 @@ 'via_device_id': None, }) # --- -# name: test_switches[-set_power] +# name: test_switches[-set_power-args_on0-args_off0] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'GS01234', @@ -37,11 +37,12 @@ 'context': , 'entity_id': 'switch.gs01234', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }) # --- -# name: test_switches[-set_power].1 +# name: test_switches[-set_power-args_on0-args_off0].1 EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -74,7 +75,51 @@ 'unit_of_measurement': None, }) # --- -# name: test_switches[_auto_on_off-set_auto_on_off_global] +# name: test_switches[-set_power-kwargs_on0-kwargs_off0] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'GS01234', + 'icon': 'mdi:power', + }), + 'context': , + 'entity_id': 'switch.gs01234', + 'last_changed': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switches[-set_power-kwargs_on0-kwargs_off0].1 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.gs01234', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:power', + 'original_name': None, + 'platform': 'lamarzocco', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'GS01234_main', + 'unit_of_measurement': None, + }) +# --- +# name: test_switches[_auto_on_off-set_auto_on_off_global-args_on1-args_off1] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'GS01234 Auto on/off', @@ -82,11 +127,12 @@ 'context': , 'entity_id': 'switch.gs01234_auto_on_off', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }) # --- -# name: test_switches[_auto_on_off-set_auto_on_off_global].1 +# name: test_switches[_auto_on_off-set_auto_on_off_global-args_on1-args_off1].1 EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -119,7 +165,51 @@ 'unit_of_measurement': None, }) # --- -# name: test_switches[_steam_boiler-set_steam] +# name: test_switches[_auto_on_off-set_auto_on_off_global-kwargs_on1-kwargs_off1] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'GS01234 Auto on/off', + 'icon': 'mdi:alarm', + }), + 'context': , + 'entity_id': 'switch.gs01234_auto_on_off', + 'last_changed': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switches[_auto_on_off-set_auto_on_off_global-kwargs_on1-kwargs_off1].1 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.gs01234_auto_on_off', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': 'mdi:alarm', + 'original_name': 'Auto on/off', + 'platform': 'lamarzocco', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'auto_on_off', + 'unique_id': 'GS01234_auto_on_off', + 'unit_of_measurement': None, + }) +# --- +# name: test_switches[_steam_boiler-set_steam-args_on2-args_off2] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'GS01234 Steam boiler', @@ -127,11 +217,58 @@ 'context': , 'entity_id': 'switch.gs01234_steam_boiler', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }) # --- -# name: test_switches[_steam_boiler-set_steam].1 +# name: test_switches[_steam_boiler-set_steam-args_on2-args_off2].1 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.gs01234_steam_boiler', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Steam boiler', + 'platform': 'lamarzocco', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'steam_boiler', + 'unique_id': 'GS01234_steam_boiler_enable', + 'unit_of_measurement': None, + }) +# --- +# name: test_switches[_steam_boiler-set_steam-kwargs_on2-kwargs_off2] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'GS01234 Steam boiler', + 'icon': 'mdi:water-boiler', + }), + 'context': , + 'entity_id': 'switch.gs01234_steam_boiler', + 'last_changed': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switches[_steam_boiler-set_steam-kwargs_on2-kwargs_off2].1 EntityRegistryEntrySnapshot({ 'aliases': set({ }), diff --git a/tests/components/lamarzocco/snapshots/test_update.ambr b/tests/components/lamarzocco/snapshots/test_update.ambr index bd7ccc2cf59..811b1a6f598 100644 --- a/tests/components/lamarzocco/snapshots/test_update.ambr +++ b/tests/components/lamarzocco/snapshots/test_update.ambr @@ -18,6 +18,7 @@ 'context': , 'entity_id': 'update.gs01234_gateway_firmware', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }) @@ -74,6 +75,7 @@ 'context': , 'entity_id': 'update.gs01234_machine_firmware', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }) diff --git a/tests/components/lamarzocco/test_binary_sensor.py b/tests/components/lamarzocco/test_binary_sensor.py index e475e663768..bb1e16f09a5 100644 --- a/tests/components/lamarzocco/test_binary_sensor.py +++ b/tests/components/lamarzocco/test_binary_sensor.py @@ -1,4 +1,5 @@ """Tests for La Marzocco binary sensors.""" + from unittest.mock import MagicMock import pytest diff --git a/tests/components/lamarzocco/test_button.py b/tests/components/lamarzocco/test_button.py index 7d910a57561..e1a036df17a 100644 --- a/tests/components/lamarzocco/test_button.py +++ b/tests/components/lamarzocco/test_button.py @@ -1,6 +1,5 @@ """Tests for the La Marzocco Buttons.""" - from unittest.mock import MagicMock import pytest diff --git a/tests/components/lamarzocco/test_config_flow.py b/tests/components/lamarzocco/test_config_flow.py index 803055ef8ab..37d9c9a3e95 100644 --- a/tests/components/lamarzocco/test_config_flow.py +++ b/tests/components/lamarzocco/test_config_flow.py @@ -1,16 +1,21 @@ """Test the La Marzocco config flow.""" + from unittest.mock import MagicMock from lmcloud.exceptions import AuthFail, RequestNotSuccessful from homeassistant import config_entries -from homeassistant.components.lamarzocco.const import CONF_MACHINE, DOMAIN +from homeassistant.components.lamarzocco.const import ( + CONF_MACHINE, + CONF_USE_BLUETOOTH, + DOMAIN, +) from homeassistant.config_entries import SOURCE_REAUTH -from homeassistant.const import CONF_HOST, CONF_PASSWORD +from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONF_PASSWORD from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult, FlowResultType -from . import USER_INPUT +from . import USER_INPUT, async_init_integration, get_bluetooth_service_info from tests.common import MockConfigEntry @@ -232,3 +237,131 @@ async def test_reauth_flow( assert result2["reason"] == "reauth_successful" assert len(mock_lamarzocco.get_all_machines.mock_calls) == 1 assert mock_config_entry.data[CONF_PASSWORD] == "new_password" + + +async def test_bluetooth_discovery( + hass: HomeAssistant, mock_lamarzocco: MagicMock +) -> None: + """Test bluetooth discovery.""" + service_info = get_bluetooth_service_info( + mock_lamarzocco.model_name, mock_lamarzocco.serial_number + ) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_BLUETOOTH}, data=service_info + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + USER_INPUT, + ) + assert result2["type"] == FlowResultType.FORM + assert result2["step_id"] == "machine_selection" + + assert len(mock_lamarzocco.get_all_machines.mock_calls) == 1 + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + CONF_HOST: "192.168.1.1", + }, + ) + await hass.async_block_till_done() + + assert result3["type"] == FlowResultType.CREATE_ENTRY + + assert result3["title"] == mock_lamarzocco.serial_number + assert result3["data"] == { + **USER_INPUT, + CONF_HOST: "192.168.1.1", + CONF_MACHINE: mock_lamarzocco.serial_number, + CONF_NAME: service_info.name, + CONF_MAC: "aa:bb:cc:dd:ee:ff", + } + + assert len(mock_lamarzocco.check_local_connection.mock_calls) == 1 + + +async def test_bluetooth_discovery_errors( + hass: HomeAssistant, mock_lamarzocco: MagicMock +) -> None: + """Test bluetooth discovery errors.""" + service_info = get_bluetooth_service_info( + mock_lamarzocco.model_name, mock_lamarzocco.serial_number + ) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=service_info, + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + + mock_lamarzocco.get_all_machines.return_value = [("GS98765", "GS3 MP")] + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + USER_INPUT, + ) + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {"base": "machine_not_found"} + assert len(mock_lamarzocco.get_all_machines.mock_calls) == 1 + + mock_lamarzocco.get_all_machines.return_value = [ + (mock_lamarzocco.serial_number, mock_lamarzocco.model_name) + ] + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + USER_INPUT, + ) + assert result2["type"] == FlowResultType.FORM + assert result2["step_id"] == "machine_selection" + assert len(mock_lamarzocco.get_all_machines.mock_calls) == 2 + + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + CONF_HOST: "192.168.1.1", + }, + ) + await hass.async_block_till_done() + + assert result3["type"] == FlowResultType.CREATE_ENTRY + + assert result3["title"] == mock_lamarzocco.serial_number + assert result3["data"] == { + **USER_INPUT, + CONF_HOST: "192.168.1.1", + CONF_MACHINE: mock_lamarzocco.serial_number, + CONF_NAME: service_info.name, + CONF_MAC: "aa:bb:cc:dd:ee:ff", + } + + +async def test_options_flow( + hass: HomeAssistant, + mock_lamarzocco: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test options flow.""" + await async_init_integration(hass, mock_config_entry) + assert mock_config_entry.state is config_entries.ConfigEntryState.LOADED + + result = await hass.config_entries.options.async_init(mock_config_entry.entry_id) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "init" + + result2 = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_USE_BLUETOOTH: False, + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["data"] == { + CONF_USE_BLUETOOTH: False, + } diff --git a/tests/components/lamarzocco/test_diagnostics.py b/tests/components/lamarzocco/test_diagnostics.py index a42b15dec3c..762b33cc696 100644 --- a/tests/components/lamarzocco/test_diagnostics.py +++ b/tests/components/lamarzocco/test_diagnostics.py @@ -1,4 +1,5 @@ """Tests for the diagnostics data provided by the La Marzocco integration.""" + from syrupy import SnapshotAssertion from homeassistant.core import HomeAssistant diff --git a/tests/components/lamarzocco/test_init.py b/tests/components/lamarzocco/test_init.py index b89ff23b771..a4bc25f64af 100644 --- a/tests/components/lamarzocco/test_init.py +++ b/tests/components/lamarzocco/test_init.py @@ -1,12 +1,16 @@ """Test initialization of lamarzocco.""" -from unittest.mock import MagicMock + +from unittest.mock import MagicMock, patch from lmcloud.exceptions import AuthFail, RequestNotSuccessful from homeassistant.components.lamarzocco.const import DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState +from homeassistant.const import CONF_MAC, CONF_NAME from homeassistant.core import HomeAssistant +from . import async_init_integration, get_bluetooth_service_info + from tests.common import MockConfigEntry @@ -16,8 +20,7 @@ async def test_load_unload_config_entry( mock_lamarzocco: MagicMock, ) -> None: """Test loading and unloading the integration.""" - await hass.config_entries.async_setup(mock_config_entry.entry_id) - await hass.async_block_till_done() + await async_init_integration(hass, mock_config_entry) assert mock_config_entry.state is ConfigEntryState.LOADED @@ -35,8 +38,7 @@ async def test_config_entry_not_ready( """Test the La Marzocco configuration entry not ready.""" mock_lamarzocco.update_local_machine_status.side_effect = RequestNotSuccessful("") - await hass.config_entries.async_setup(mock_config_entry.entry_id) - await hass.async_block_till_done() + await async_init_integration(hass, mock_config_entry) assert len(mock_lamarzocco.update_local_machine_status.mock_calls) == 1 assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY @@ -49,8 +51,7 @@ async def test_invalid_auth( ) -> None: """Test auth error during setup.""" mock_lamarzocco.update_local_machine_status.side_effect = AuthFail("") - await hass.config_entries.async_setup(mock_config_entry.entry_id) - await hass.async_block_till_done() + await async_init_integration(hass, mock_config_entry) assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR assert len(mock_lamarzocco.update_local_machine_status.mock_calls) == 1 @@ -65,3 +66,29 @@ async def test_invalid_auth( assert "context" in flow assert flow["context"].get("source") == SOURCE_REAUTH assert flow["context"].get("entry_id") == mock_config_entry.entry_id + + +async def test_bluetooth_is_set_from_discovery( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_lamarzocco: MagicMock, +) -> None: + """Assert we're not searching for a new BT device when we already found one previously.""" + + # remove the bluetooth configuration from entry + data = mock_config_entry.data.copy() + del data[CONF_NAME] + del data[CONF_MAC] + hass.config_entries.async_update_entry(mock_config_entry, data=data) + + service_info = get_bluetooth_service_info( + mock_lamarzocco.model_name, mock_lamarzocco.serial_number + ) + with patch( + "homeassistant.components.lamarzocco.coordinator.async_discovered_service_info", + return_value=[service_info], + ): + await async_init_integration(hass, mock_config_entry) + mock_lamarzocco.init_bluetooth_with_known_device.assert_called_once() + assert mock_config_entry.data[CONF_NAME] == service_info.name + assert mock_config_entry.data[CONF_MAC] == service_info.address diff --git a/tests/components/lamarzocco/test_number.py b/tests/components/lamarzocco/test_number.py index 86ae1b90126..8cba3d2387d 100644 --- a/tests/components/lamarzocco/test_number.py +++ b/tests/components/lamarzocco/test_number.py @@ -53,7 +53,9 @@ async def test_coffee_boiler( ) assert len(mock_lamarzocco.set_coffee_temp.mock_calls) == 1 - mock_lamarzocco.set_coffee_temp.assert_called_once_with(temperature=95) + mock_lamarzocco.set_coffee_temp.assert_called_once_with( + temperature=95, ble_device=None + ) @pytest.mark.parametrize( @@ -62,7 +64,12 @@ async def test_coffee_boiler( @pytest.mark.parametrize( ("entity_name", "value", "func_name", "kwargs"), [ - ("steam_target_temperature", 131, "set_steam_temp", {"temperature": 131}), + ( + "steam_target_temperature", + 131, + "set_steam_temp", + {"temperature": 131, "ble_device": None}, + ), ("tea_water_duration", 15, "set_dose_hot_water", {"value": 15}), ], ) diff --git a/tests/components/lamarzocco/test_select.py b/tests/components/lamarzocco/test_select.py index a2e4248f0af..497a95f6d0d 100644 --- a/tests/components/lamarzocco/test_select.py +++ b/tests/components/lamarzocco/test_select.py @@ -1,6 +1,5 @@ """Tests for the La Marzocco select entities.""" - from unittest.mock import MagicMock from lmcloud.const import LaMarzoccoModel @@ -51,7 +50,7 @@ async def test_steam_boiler_level( ) assert len(mock_lamarzocco.set_steam_level.mock_calls) == 1 - mock_lamarzocco.set_steam_level.assert_called_once_with(level=1) + mock_lamarzocco.set_steam_level.assert_called_once_with(1, None) @pytest.mark.parametrize( diff --git a/tests/components/lamarzocco/test_sensor.py b/tests/components/lamarzocco/test_sensor.py index 3333fed1464..b5f551309b6 100644 --- a/tests/components/lamarzocco/test_sensor.py +++ b/tests/components/lamarzocco/test_sensor.py @@ -1,4 +1,5 @@ """Tests for La Marzocco sensors.""" + from unittest.mock import MagicMock import pytest diff --git a/tests/components/lamarzocco/test_switch.py b/tests/components/lamarzocco/test_switch.py index 70024e3e340..e1924f0a8ca 100644 --- a/tests/components/lamarzocco/test_switch.py +++ b/tests/components/lamarzocco/test_switch.py @@ -1,9 +1,11 @@ """Tests for La Marzocco switches.""" + from unittest.mock import MagicMock import pytest from syrupy import SnapshotAssertion +from homeassistant.components.lamarzocco.const import DOMAIN from homeassistant.components.switch import ( DOMAIN as SWITCH_DOMAIN, SERVICE_TURN_OFF, @@ -13,15 +15,22 @@ from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er +from tests.common import MockConfigEntry + pytestmark = pytest.mark.usefixtures("init_integration") @pytest.mark.parametrize( - ("entity_name", "method_name"), + ("entity_name", "method_name", "args_on", "args_off"), [ - ("", "set_power"), - ("_auto_on_off", "set_auto_on_off_global"), - ("_steam_boiler", "set_steam"), + ("", "set_power", (True, None), (False, None)), + ( + "_auto_on_off", + "set_auto_on_off_global", + (True,), + (False,), + ), + ("_steam_boiler", "set_steam", (True, None), (False, None)), ], ) async def test_switches( @@ -31,6 +40,8 @@ async def test_switches( snapshot: SnapshotAssertion, entity_name: str, method_name: str, + args_on: tuple, + args_off: tuple, ) -> None: """Test the La Marzocco switches.""" serial_number = mock_lamarzocco.serial_number @@ -55,7 +66,7 @@ async def test_switches( ) assert len(control_fn.mock_calls) == 1 - control_fn.assert_called_once_with(False) + control_fn.assert_called_once_with(*args_off) await hass.services.async_call( SWITCH_DOMAIN, @@ -67,7 +78,7 @@ async def test_switches( ) assert len(control_fn.mock_calls) == 2 - control_fn.assert_called_with(True) + control_fn.assert_called_with(*args_on) async def test_device( @@ -89,3 +100,26 @@ async def test_device( device = device_registry.async_get(entry.device_id) assert device assert device == snapshot + + +async def test_call_without_bluetooth_works( + hass: HomeAssistant, + mock_lamarzocco: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test that if not using bluetooth, the switch still works.""" + serial_number = mock_lamarzocco.serial_number + coordinator = hass.data[DOMAIN][mock_config_entry.entry_id] + coordinator._use_bluetooth = False + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + { + ATTR_ENTITY_ID: f"switch.{serial_number}_steam_boiler", + }, + blocking=True, + ) + + assert len(mock_lamarzocco.set_steam.mock_calls) == 1 + mock_lamarzocco.set_steam.assert_called_once_with(False, None) diff --git a/tests/components/lamarzocco/test_update.py b/tests/components/lamarzocco/test_update.py index 55c5bb0da3d..3b1323d1c73 100644 --- a/tests/components/lamarzocco/test_update.py +++ b/tests/components/lamarzocco/test_update.py @@ -1,6 +1,5 @@ """Tests for the La Marzocco Update Entities.""" - from unittest.mock import MagicMock from lmcloud.const import LaMarzoccoUpdateableComponent diff --git a/tests/components/lametric/conftest.py b/tests/components/lametric/conftest.py index b3a9f2d8665..bd2ae275970 100644 --- a/tests/components/lametric/conftest.py +++ b/tests/components/lametric/conftest.py @@ -1,4 +1,5 @@ """Fixtures for LaMetric integration tests.""" + from __future__ import annotations from collections.abc import Generator @@ -75,11 +76,15 @@ def device_fixture() -> str: @pytest.fixture def mock_lametric(request, device_fixture: str) -> Generator[MagicMock, None, None]: """Return a mocked LaMetric TIME client.""" - with patch( - "homeassistant.components.lametric.coordinator.LaMetricDevice", autospec=True - ) as lametric_mock, patch( - "homeassistant.components.lametric.config_flow.LaMetricDevice", - new=lametric_mock, + with ( + patch( + "homeassistant.components.lametric.coordinator.LaMetricDevice", + autospec=True, + ) as lametric_mock, + patch( + "homeassistant.components.lametric.config_flow.LaMetricDevice", + new=lametric_mock, + ), ): lametric = lametric_mock.return_value lametric.api_key = "mock-api-key" diff --git a/tests/components/lametric/test_button.py b/tests/components/lametric/test_button.py index ce8639db717..e755329b93d 100644 --- a/tests/components/lametric/test_button.py +++ b/tests/components/lametric/test_button.py @@ -1,4 +1,5 @@ """Tests for the LaMetric button platform.""" + from unittest.mock import MagicMock from demetriek import LaMetricConnectionError, LaMetricError @@ -8,7 +9,6 @@ from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRE from homeassistant.components.lametric.const import DOMAIN from homeassistant.const import ( ATTR_ENTITY_ID, - ATTR_ICON, STATE_UNAVAILABLE, STATE_UNKNOWN, EntityCategory, @@ -32,7 +32,6 @@ async def test_button_app_next( """Test the LaMetric next app button.""" state = hass.states.get("button.frenck_s_lametric_next_app") assert state - assert state.attributes.get(ATTR_ICON) == "mdi:arrow-right-bold" assert state.state == STATE_UNKNOWN entry = entity_registry.async_get("button.frenck_s_lametric_next_app") @@ -79,7 +78,6 @@ async def test_button_app_previous( """Test the LaMetric previous app button.""" state = hass.states.get("button.frenck_s_lametric_previous_app") assert state - assert state.attributes.get(ATTR_ICON) == "mdi:arrow-left-bold" assert state.state == STATE_UNKNOWN entry = entity_registry.async_get("button.frenck_s_lametric_previous_app") @@ -126,7 +124,6 @@ async def test_button_dismiss_current_notification( """Test the LaMetric dismiss current notification button.""" state = hass.states.get("button.frenck_s_lametric_dismiss_current_notification") assert state - assert state.attributes.get(ATTR_ICON) == "mdi:bell-cancel" assert state.state == STATE_UNKNOWN entry = entity_registry.async_get( @@ -175,7 +172,6 @@ async def test_button_dismiss_all_notifications( """Test the LaMetric dismiss all notifications button.""" state = hass.states.get("button.frenck_s_lametric_dismiss_all_notifications") assert state - assert state.attributes.get(ATTR_ICON) == "mdi:bell-cancel" assert state.state == STATE_UNKNOWN entry = entity_registry.async_get( diff --git a/tests/components/lametric/test_config_flow.py b/tests/components/lametric/test_config_flow.py index 9b00c1e89aa..e5fa1229e07 100644 --- a/tests/components/lametric/test_config_flow.py +++ b/tests/components/lametric/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for the LaMetric config flow.""" + from http import HTTPStatus from unittest.mock import MagicMock diff --git a/tests/components/lametric/test_diagnostics.py b/tests/components/lametric/test_diagnostics.py index 333985f71a0..e1fcbafcb73 100644 --- a/tests/components/lametric/test_diagnostics.py +++ b/tests/components/lametric/test_diagnostics.py @@ -1,4 +1,5 @@ """Tests for the diagnostics data provided by the LaMetric integration.""" + from syrupy import SnapshotAssertion from homeassistant.core import HomeAssistant diff --git a/tests/components/lametric/test_helpers.py b/tests/components/lametric/test_helpers.py index a1b824086d2..f4341289219 100644 --- a/tests/components/lametric/test_helpers.py +++ b/tests/components/lametric/test_helpers.py @@ -1,4 +1,5 @@ """Tests for the LaMetric helpers.""" + from unittest.mock import MagicMock import pytest diff --git a/tests/components/lametric/test_init.py b/tests/components/lametric/test_init.py index eee09b3acce..7352721e992 100644 --- a/tests/components/lametric/test_init.py +++ b/tests/components/lametric/test_init.py @@ -1,4 +1,5 @@ """Tests for the LaMetric integration.""" + from unittest.mock import MagicMock from demetriek import ( diff --git a/tests/components/lametric/test_notify.py b/tests/components/lametric/test_notify.py index a94b8f2ce53..a46d97f8f81 100644 --- a/tests/components/lametric/test_notify.py +++ b/tests/components/lametric/test_notify.py @@ -1,4 +1,5 @@ """Tests for the LaMetric notify platform.""" + from unittest.mock import MagicMock from demetriek import ( diff --git a/tests/components/lametric/test_number.py b/tests/components/lametric/test_number.py index 7e033722c3d..d5466abbd41 100644 --- a/tests/components/lametric/test_number.py +++ b/tests/components/lametric/test_number.py @@ -1,4 +1,5 @@ """Tests for the LaMetric number platform.""" + from unittest.mock import MagicMock from demetriek import LaMetricConnectionError, LaMetricError @@ -17,7 +18,6 @@ from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, - ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, PERCENTAGE, STATE_UNAVAILABLE, @@ -41,7 +41,6 @@ async def test_brightness( assert state assert state.attributes.get(ATTR_DEVICE_CLASS) is None assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Frenck's LaMetric Brightness" - assert state.attributes.get(ATTR_ICON) == "mdi:brightness-6" assert state.attributes.get(ATTR_MAX) == 100 assert state.attributes.get(ATTR_MIN) == 0 assert state.attributes.get(ATTR_STEP) == 1 @@ -91,7 +90,6 @@ async def test_volume( assert state assert state.attributes.get(ATTR_DEVICE_CLASS) is None assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Frenck's LaMetric Volume" - assert state.attributes.get(ATTR_ICON) == "mdi:volume-high" assert state.attributes.get(ATTR_MAX) == 100 assert state.attributes.get(ATTR_MIN) == 0 assert state.attributes.get(ATTR_STEP) == 1 diff --git a/tests/components/lametric/test_select.py b/tests/components/lametric/test_select.py index 4215ffe2dea..bd7bc775714 100644 --- a/tests/components/lametric/test_select.py +++ b/tests/components/lametric/test_select.py @@ -1,4 +1,5 @@ """Tests for the LaMetric select platform.""" + from unittest.mock import MagicMock from demetriek import BrightnessMode, LaMetricConnectionError, LaMetricError @@ -13,7 +14,6 @@ from homeassistant.components.select import ( from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, - ATTR_ICON, ATTR_OPTION, STATE_UNAVAILABLE, EntityCategory, @@ -37,7 +37,6 @@ async def test_brightness_mode( assert ( state.attributes.get(ATTR_FRIENDLY_NAME) == "Frenck's LaMetric Brightness mode" ) - assert state.attributes.get(ATTR_ICON) == "mdi:brightness-auto" assert state.attributes.get(ATTR_OPTIONS) == ["auto", "manual"] assert state.state == BrightnessMode.AUTO diff --git a/tests/components/lametric/test_sensor.py b/tests/components/lametric/test_sensor.py index 743ed0e8bde..8dff11fb450 100644 --- a/tests/components/lametric/test_sensor.py +++ b/tests/components/lametric/test_sensor.py @@ -1,4 +1,5 @@ """Tests for the LaMetric sensor platform.""" + import pytest from homeassistant.components.lametric.const import DOMAIN @@ -6,7 +7,6 @@ from homeassistant.components.sensor import ATTR_STATE_CLASS, SensorStateClass from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_FRIENDLY_NAME, - ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, PERCENTAGE, EntityCategory, @@ -29,7 +29,6 @@ async def test_wifi_signal( assert state assert state.attributes.get(ATTR_DEVICE_CLASS) is None assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Frenck's LaMetric Wi-Fi signal" - assert state.attributes.get(ATTR_ICON) == "mdi:wifi" assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE assert state.state == "21" diff --git a/tests/components/lametric/test_services.py b/tests/components/lametric/test_services.py index 9a1258a82bb..d3fbd0a18e0 100644 --- a/tests/components/lametric/test_services.py +++ b/tests/components/lametric/test_services.py @@ -1,4 +1,5 @@ """Tests for the LaMetric services.""" + from unittest.mock import MagicMock from demetriek import ( diff --git a/tests/components/lametric/test_switch.py b/tests/components/lametric/test_switch.py index bd4b7856a22..b81428bb402 100644 --- a/tests/components/lametric/test_switch.py +++ b/tests/components/lametric/test_switch.py @@ -1,4 +1,5 @@ """Tests for the LaMetric switch platform.""" + from unittest.mock import MagicMock from demetriek import LaMetricConnectionError, LaMetricError @@ -14,7 +15,6 @@ from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, - ATTR_ICON, STATE_OFF, STATE_UNAVAILABLE, EntityCategory, @@ -40,7 +40,6 @@ async def test_bluetooth( assert state assert state.attributes.get(ATTR_DEVICE_CLASS) is None assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Frenck's LaMetric Bluetooth" - assert state.attributes.get(ATTR_ICON) == "mdi:bluetooth" assert state.state == STATE_OFF entry = entity_registry.async_get(state.entity_id) diff --git a/tests/components/landisgyr_heat_meter/conftest.py b/tests/components/landisgyr_heat_meter/conftest.py index 711fa2110f4..df7e4a44ce9 100644 --- a/tests/components/landisgyr_heat_meter/conftest.py +++ b/tests/components/landisgyr_heat_meter/conftest.py @@ -1,4 +1,5 @@ """Define fixtures for Landis + Gyr Heat Meter tests.""" + from collections.abc import Generator from unittest.mock import AsyncMock, patch diff --git a/tests/components/landisgyr_heat_meter/snapshots/test_sensor.ambr b/tests/components/landisgyr_heat_meter/snapshots/test_sensor.ambr index 5d8b703cdcd..8def1c83a85 100644 --- a/tests/components/landisgyr_heat_meter/snapshots/test_sensor.ambr +++ b/tests/components/landisgyr_heat_meter/snapshots/test_sensor.ambr @@ -12,6 +12,7 @@ 'context': , 'entity_id': 'sensor.heat_meter_volume_usage', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '456.0', }), @@ -26,6 +27,7 @@ 'context': , 'entity_id': 'sensor.heat_meter_heat_usage_gj', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '123.0', }), @@ -39,6 +41,7 @@ 'context': , 'entity_id': 'sensor.heat_meter_heat_previous_year_gj', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '111.0', }), @@ -52,6 +55,7 @@ 'context': , 'entity_id': 'sensor.heat_meter_volume_usage_previous_year', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '450.0', }), @@ -63,6 +67,7 @@ 'context': , 'entity_id': 'sensor.heat_meter_ownership_number', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '123a', }), @@ -74,6 +79,7 @@ 'context': , 'entity_id': 'sensor.heat_meter_error_number', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0', }), @@ -85,6 +91,7 @@ 'context': , 'entity_id': 'sensor.heat_meter_device_number', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'abc1', }), @@ -97,6 +104,7 @@ 'context': , 'entity_id': 'sensor.heat_meter_measurement_period_minutes', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '60', }), @@ -109,6 +117,7 @@ 'context': , 'entity_id': 'sensor.heat_meter_power_max', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '22.1', }), @@ -121,6 +130,7 @@ 'context': , 'entity_id': 'sensor.heat_meter_power_max_previous_year', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '22.4', }), @@ -133,6 +143,7 @@ 'context': , 'entity_id': 'sensor.heat_meter_flowrate_max', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0.744', }), @@ -145,6 +156,7 @@ 'context': , 'entity_id': 'sensor.heat_meter_flowrate_max_previous_year', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0.743', }), @@ -157,6 +169,7 @@ 'context': , 'entity_id': 'sensor.heat_meter_return_temperature_max', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '96.1', }), @@ -169,6 +182,7 @@ 'context': , 'entity_id': 'sensor.heat_meter_return_temperature_max_previous_year', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '96.2', }), @@ -181,6 +195,7 @@ 'context': , 'entity_id': 'sensor.heat_meter_flow_temperature_max', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '98.5', }), @@ -193,6 +208,7 @@ 'context': , 'entity_id': 'sensor.heat_meter_flow_temperature_max_previous_year', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '98.4', }), @@ -205,6 +221,7 @@ 'context': , 'entity_id': 'sensor.heat_meter_operating_hours', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '115575', }), @@ -217,6 +234,7 @@ 'context': , 'entity_id': 'sensor.heat_meter_flow_hours', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '30242', }), @@ -229,6 +247,7 @@ 'context': , 'entity_id': 'sensor.heat_meter_fault_hours', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '5', }), @@ -241,6 +260,7 @@ 'context': , 'entity_id': 'sensor.heat_meter_fault_hours_previous_year', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '5', }), @@ -252,6 +272,7 @@ 'context': , 'entity_id': 'sensor.heat_meter_yearly_set_day', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '01-01', }), @@ -263,6 +284,7 @@ 'context': , 'entity_id': 'sensor.heat_meter_monthly_set_day', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '01', }), @@ -275,6 +297,7 @@ 'context': , 'entity_id': 'sensor.heat_meter_meter_date_time', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '2022-05-19T19:41:17+00:00', }), @@ -287,6 +310,7 @@ 'context': , 'entity_id': 'sensor.heat_meter_measuring_range', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '1.5', }), @@ -297,6 +321,7 @@ 'context': , 'entity_id': 'sensor.heat_meter_settings_and_firmware', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0 1 0 0000 CECV CECV 1 5.16 5.16 F 101008 040404 08 0', }), @@ -315,6 +340,7 @@ 'context': , 'entity_id': 'sensor.heat_meter_heat_usage_mwh', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '123.0', }), @@ -329,6 +355,7 @@ 'context': , 'entity_id': 'sensor.heat_meter_volume_usage', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '456.0', }), @@ -342,6 +369,7 @@ 'context': , 'entity_id': 'sensor.heat_meter_heat_previous_year_mwh', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '111.0', }), @@ -355,6 +383,7 @@ 'context': , 'entity_id': 'sensor.heat_meter_volume_usage_previous_year', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '450.0', }), @@ -366,6 +395,7 @@ 'context': , 'entity_id': 'sensor.heat_meter_ownership_number', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '123a', }), @@ -377,6 +407,7 @@ 'context': , 'entity_id': 'sensor.heat_meter_error_number', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0', }), @@ -388,6 +419,7 @@ 'context': , 'entity_id': 'sensor.heat_meter_device_number', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'abc1', }), @@ -400,6 +432,7 @@ 'context': , 'entity_id': 'sensor.heat_meter_measurement_period_minutes', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '60', }), @@ -412,6 +445,7 @@ 'context': , 'entity_id': 'sensor.heat_meter_power_max', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '22.1', }), @@ -424,6 +458,7 @@ 'context': , 'entity_id': 'sensor.heat_meter_power_max_previous_year', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '22.4', }), @@ -436,6 +471,7 @@ 'context': , 'entity_id': 'sensor.heat_meter_flowrate_max', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0.744', }), @@ -448,6 +484,7 @@ 'context': , 'entity_id': 'sensor.heat_meter_flowrate_max_previous_year', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0.743', }), @@ -460,6 +497,7 @@ 'context': , 'entity_id': 'sensor.heat_meter_return_temperature_max', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '96.1', }), @@ -472,6 +510,7 @@ 'context': , 'entity_id': 'sensor.heat_meter_return_temperature_max_previous_year', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '96.2', }), @@ -484,6 +523,7 @@ 'context': , 'entity_id': 'sensor.heat_meter_flow_temperature_max', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '98.5', }), @@ -496,6 +536,7 @@ 'context': , 'entity_id': 'sensor.heat_meter_flow_temperature_max_previous_year', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '98.4', }), @@ -508,6 +549,7 @@ 'context': , 'entity_id': 'sensor.heat_meter_operating_hours', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '115575', }), @@ -520,6 +562,7 @@ 'context': , 'entity_id': 'sensor.heat_meter_flow_hours', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '30242', }), @@ -532,6 +575,7 @@ 'context': , 'entity_id': 'sensor.heat_meter_fault_hours', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '5', }), @@ -544,6 +588,7 @@ 'context': , 'entity_id': 'sensor.heat_meter_fault_hours_previous_year', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '5', }), @@ -555,6 +600,7 @@ 'context': , 'entity_id': 'sensor.heat_meter_yearly_set_day', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '01-01', }), @@ -566,6 +612,7 @@ 'context': , 'entity_id': 'sensor.heat_meter_monthly_set_day', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '01', }), @@ -578,6 +625,7 @@ 'context': , 'entity_id': 'sensor.heat_meter_meter_date_time', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '2022-05-19T19:41:17+00:00', }), @@ -590,6 +638,7 @@ 'context': , 'entity_id': 'sensor.heat_meter_measuring_range', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '1.5', }), @@ -600,6 +649,7 @@ 'context': , 'entity_id': 'sensor.heat_meter_settings_and_firmware', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0 1 0 0000 CECV CECV 1 5.16 5.16 F 101008 040404 08 0', }), diff --git a/tests/components/landisgyr_heat_meter/test_config_flow.py b/tests/components/landisgyr_heat_meter/test_config_flow.py index 19338d8d576..d53a81a7edf 100644 --- a/tests/components/landisgyr_heat_meter/test_config_flow.py +++ b/tests/components/landisgyr_heat_meter/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Landis + Gyr Heat Meter config flow.""" + from dataclasses import dataclass from unittest.mock import patch diff --git a/tests/components/landisgyr_heat_meter/test_init.py b/tests/components/landisgyr_heat_meter/test_init.py index f8615aa77af..c9768ec681f 100644 --- a/tests/components/landisgyr_heat_meter/test_init.py +++ b/tests/components/landisgyr_heat_meter/test_init.py @@ -1,4 +1,5 @@ """Test the Landis + Gyr Heat Meter init.""" + from unittest.mock import patch from homeassistant.components.landisgyr_heat_meter.const import ( diff --git a/tests/components/landisgyr_heat_meter/test_sensor.py b/tests/components/landisgyr_heat_meter/test_sensor.py index f05d12e49a2..1578c67432d 100644 --- a/tests/components/landisgyr_heat_meter/test_sensor.py +++ b/tests/components/landisgyr_heat_meter/test_sensor.py @@ -1,4 +1,5 @@ """The tests for the Landis+Gyr Heat Meter sensor platform.""" + import datetime from unittest.mock import patch diff --git a/tests/components/lastfm/__init__.py b/tests/components/lastfm/__init__.py index 7e6bb6500b2..8f133607c8d 100644 --- a/tests/components/lastfm/__init__.py +++ b/tests/components/lastfm/__init__.py @@ -1,4 +1,5 @@ """The tests for lastfm.""" + from unittest.mock import patch from pylast import PyLastError, Track diff --git a/tests/components/lastfm/conftest.py b/tests/components/lastfm/conftest.py index c7cada9ba0a..0575df2bbca 100644 --- a/tests/components/lastfm/conftest.py +++ b/tests/components/lastfm/conftest.py @@ -1,4 +1,5 @@ """Configure tests for the LastFM integration.""" + from collections.abc import Awaitable, Callable from unittest.mock import patch diff --git a/tests/components/lastfm/snapshots/test_sensor.ambr b/tests/components/lastfm/snapshots/test_sensor.ambr index 30ad40df428..c05c34b24ee 100644 --- a/tests/components/lastfm/snapshots/test_sensor.ambr +++ b/tests/components/lastfm/snapshots/test_sensor.ambr @@ -5,7 +5,6 @@ 'attribution': 'Data provided by Last.fm', 'entity_picture': 'image', 'friendly_name': 'LastFM testaccount1', - 'icon': 'mdi:radio-fm', 'last_played': 'artist - title', 'play_count': 1, 'top_played': 'artist - title', @@ -13,6 +12,7 @@ 'context': , 'entity_id': 'sensor.lastfm_testaccount1', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'artist - title', }) @@ -23,7 +23,6 @@ 'attribution': 'Data provided by Last.fm', 'entity_picture': 'image', 'friendly_name': 'LastFM testaccount1', - 'icon': 'mdi:radio-fm', 'last_played': None, 'play_count': 0, 'top_played': None, @@ -31,6 +30,7 @@ 'context': , 'entity_id': 'sensor.lastfm_testaccount1', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'Not Scrobbling', }) diff --git a/tests/components/lastfm/test_config_flow.py b/tests/components/lastfm/test_config_flow.py index 8a2c556a8d0..93fc9e5a206 100644 --- a/tests/components/lastfm/test_config_flow.py +++ b/tests/components/lastfm/test_config_flow.py @@ -1,4 +1,5 @@ """Test Lastfm config flow.""" + from unittest.mock import patch from pylast import WSError @@ -140,9 +141,10 @@ async def test_flow_friends_no_friends( hass: HomeAssistant, default_user_no_friends: MockUser ) -> None: """Test options is empty when user has no friends.""" - with patch( - "pylast.User", return_value=default_user_no_friends - ), patch_setup_entry(): + with ( + patch("pylast.User", return_value=default_user_no_friends), + patch_setup_entry(), + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, diff --git a/tests/components/lastfm/test_init.py b/tests/components/lastfm/test_init.py index 2f126af11a3..e7b367fd57f 100644 --- a/tests/components/lastfm/test_init.py +++ b/tests/components/lastfm/test_init.py @@ -1,4 +1,5 @@ """Test LastFM component setup process.""" + from __future__ import annotations from homeassistant.components.lastfm.const import DOMAIN diff --git a/tests/components/launch_library/test_config_flow.py b/tests/components/launch_library/test_config_flow.py index 8a8a2a94937..80a634318b9 100644 --- a/tests/components/launch_library/test_config_flow.py +++ b/tests/components/launch_library/test_config_flow.py @@ -1,4 +1,5 @@ """Test launch_library config flow.""" + from unittest.mock import patch from homeassistant import data_entry_flow diff --git a/tests/components/laundrify/conftest.py b/tests/components/laundrify/conftest.py index 13e408b61a9..91aeebf81ee 100644 --- a/tests/components/laundrify/conftest.py +++ b/tests/components/laundrify/conftest.py @@ -1,4 +1,5 @@ """Configure py.test.""" + import json from unittest.mock import patch @@ -41,11 +42,14 @@ def laundrify_validate_token_fixture(): @pytest.fixture(name="laundrify_api_mock", autouse=True) def laundrify_api_fixture(laundrify_exchange_code, laundrify_validate_token): """Mock valid laundrify API responses.""" - with patch( - "laundrify_aio.LaundrifyAPI.get_account_id", - return_value=VALID_ACCOUNT_ID, - ), patch( - "laundrify_aio.LaundrifyAPI.get_machines", - return_value=json.loads(load_fixture("laundrify/machines.json")), - ) as get_machines_mock: + with ( + patch( + "laundrify_aio.LaundrifyAPI.get_account_id", + return_value=VALID_ACCOUNT_ID, + ), + patch( + "laundrify_aio.LaundrifyAPI.get_machines", + return_value=json.loads(load_fixture("laundrify/machines.json")), + ) as get_machines_mock, + ): yield get_machines_mock diff --git a/tests/components/lawn_mower/test_init.py b/tests/components/lawn_mower/test_init.py index 39d594e1e17..1cf6c7f4b24 100644 --- a/tests/components/lawn_mower/test_init.py +++ b/tests/components/lawn_mower/test_init.py @@ -1,4 +1,5 @@ """The tests for the lawn mower integration.""" + from collections.abc import Generator from unittest.mock import MagicMock diff --git a/tests/components/lcn/conftest.py b/tests/components/lcn/conftest.py index a6bcdf63950..2e50f20561e 100644 --- a/tests/components/lcn/conftest.py +++ b/tests/components/lcn/conftest.py @@ -1,4 +1,5 @@ """Test configuration and mocks for LCN component.""" + import json from unittest.mock import AsyncMock, patch diff --git a/tests/components/lcn/test_binary_sensor.py b/tests/components/lcn/test_binary_sensor.py index c92a45d7cc9..9ba04ac94c7 100644 --- a/tests/components/lcn/test_binary_sensor.py +++ b/tests/components/lcn/test_binary_sensor.py @@ -1,4 +1,5 @@ """Test for the LCN binary sensor platform.""" + from pypck.inputs import ModStatusBinSensors, ModStatusKeyLocks, ModStatusVar from pypck.lcn_addr import LcnAddr from pypck.lcn_defs import Var, VarValue diff --git a/tests/components/lcn/test_config_flow.py b/tests/components/lcn/test_config_flow.py index 5ceb82ed4a1..aa1b5086e65 100644 --- a/tests/components/lcn/test_config_flow.py +++ b/tests/components/lcn/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for the LCN config flow.""" + from unittest.mock import patch from pypck.connection import PchkAuthenticationError, PchkLicenseError @@ -35,9 +36,11 @@ IMPORT_DATA = { async def test_step_import(hass: HomeAssistant) -> None: """Test for import step.""" - with patch("pypck.connection.PchkConnectionManager.async_connect"), patch( - "homeassistant.components.lcn.async_setup", return_value=True - ), patch("homeassistant.components.lcn.async_setup_entry", return_value=True): + with ( + patch("pypck.connection.PchkConnectionManager.async_connect"), + patch("homeassistant.components.lcn.async_setup", return_value=True), + patch("homeassistant.components.lcn.async_setup_entry", return_value=True), + ): data = IMPORT_DATA.copy() result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=data diff --git a/tests/components/lcn/test_cover.py b/tests/components/lcn/test_cover.py index 4705591e1d3..f50921c08a1 100644 --- a/tests/components/lcn/test_cover.py +++ b/tests/components/lcn/test_cover.py @@ -1,4 +1,5 @@ """Test for the LCN cover platform.""" + from unittest.mock import patch from pypck.inputs import ModStatusOutput, ModStatusRelays diff --git a/tests/components/lcn/test_device_trigger.py b/tests/components/lcn/test_device_trigger.py index 59cabb309b0..4ef43e826f3 100644 --- a/tests/components/lcn/test_device_trigger.py +++ b/tests/components/lcn/test_device_trigger.py @@ -1,4 +1,5 @@ """Tests for LCN device triggers.""" + from pypck.inputs import ModSendKeysHost, ModStatusAccessControl from pypck.lcn_addr import LcnAddr from pypck.lcn_defs import AccessControlPeriphery, KeyAction, SendKeyCommand diff --git a/tests/components/lcn/test_events.py b/tests/components/lcn/test_events.py index 4e20e202ffc..eb62f820103 100644 --- a/tests/components/lcn/test_events.py +++ b/tests/components/lcn/test_events.py @@ -1,4 +1,5 @@ """Tests for LCN events.""" + from pypck.inputs import Input, ModSendKeysHost, ModStatusAccessControl from pypck.lcn_addr import LcnAddr from pypck.lcn_defs import AccessControlPeriphery, KeyAction, SendKeyCommand diff --git a/tests/components/lcn/test_init.py b/tests/components/lcn/test_init.py index fb1d09d91d6..292ebc045b2 100644 --- a/tests/components/lcn/test_init.py +++ b/tests/components/lcn/test_init.py @@ -1,4 +1,5 @@ """Test init of LCN integration.""" + from unittest.mock import patch from pypck.connection import ( @@ -126,9 +127,10 @@ async def test_async_setup_entry_raises_timeout_error( async def test_async_setup_from_configuration_yaml(hass: HomeAssistant) -> None: """Test a successful setup using data from configuration.yaml.""" - with patch( - "pypck.connection.PchkConnectionManager", MockPchkConnectionManager - ), patch("homeassistant.components.lcn.async_setup_entry") as async_setup_entry: + with ( + patch("pypck.connection.PchkConnectionManager", MockPchkConnectionManager), + patch("homeassistant.components.lcn.async_setup_entry") as async_setup_entry, + ): await setup_component(hass) assert async_setup_entry.await_count == 2 diff --git a/tests/components/lcn/test_light.py b/tests/components/lcn/test_light.py index 7f23c1e6214..b91f3d5b17c 100644 --- a/tests/components/lcn/test_light.py +++ b/tests/components/lcn/test_light.py @@ -1,4 +1,5 @@ """Test for the LCN light platform.""" + from unittest.mock import patch from pypck.inputs import ModStatusOutput, ModStatusRelays diff --git a/tests/components/lcn/test_sensor.py b/tests/components/lcn/test_sensor.py index b46de397255..cdcd5a195a3 100644 --- a/tests/components/lcn/test_sensor.py +++ b/tests/components/lcn/test_sensor.py @@ -1,4 +1,5 @@ """Test for the LCN sensor platform.""" + from pypck.inputs import ModStatusLedsAndLogicOps, ModStatusVar from pypck.lcn_addr import LcnAddr from pypck.lcn_defs import LedStatus, LogicOpStatus, Var, VarValue diff --git a/tests/components/lcn/test_switch.py b/tests/components/lcn/test_switch.py index a83d45c0889..f24828c5fcb 100644 --- a/tests/components/lcn/test_switch.py +++ b/tests/components/lcn/test_switch.py @@ -1,4 +1,5 @@ """Test for the LCN switch platform.""" + from unittest.mock import patch from pypck.inputs import ModStatusOutput, ModStatusRelays diff --git a/tests/components/ld2410_ble/test_config_flow.py b/tests/components/ld2410_ble/test_config_flow.py index f34ddbfb841..74e7e8a2c8e 100644 --- a/tests/components/ld2410_ble/test_config_flow.py +++ b/tests/components/ld2410_ble/test_config_flow.py @@ -1,4 +1,5 @@ """Test the LD2410 BLE Bluetooth config flow.""" + from unittest.mock import patch from bleak import BleakError @@ -27,12 +28,15 @@ async def test_user_step_success(hass: HomeAssistant) -> None: assert result["step_id"] == "user" assert result["errors"] == {} - with patch( - "homeassistant.components.ld2410_ble.config_flow.LD2410BLE.initialise", - ), patch( - "homeassistant.components.ld2410_ble.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.ld2410_ble.config_flow.LD2410BLE.initialise", + ), + patch( + "homeassistant.components.ld2410_ble.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -113,12 +117,15 @@ async def test_user_step_cannot_connect(hass: HomeAssistant) -> None: assert result2["step_id"] == "user" assert result2["errors"] == {"base": "cannot_connect"} - with patch( - "homeassistant.components.ld2410_ble.config_flow.LD2410BLE.initialise", - ), patch( - "homeassistant.components.ld2410_ble.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.ld2410_ble.config_flow.LD2410BLE.initialise", + ), + patch( + "homeassistant.components.ld2410_ble.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], { @@ -165,12 +172,15 @@ async def test_user_step_unknown_exception(hass: HomeAssistant) -> None: assert result2["step_id"] == "user" assert result2["errors"] == {"base": "unknown"} - with patch( - "homeassistant.components.ld2410_ble.config_flow.LD2410BLE.initialise", - ), patch( - "homeassistant.components.ld2410_ble.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.ld2410_ble.config_flow.LD2410BLE.initialise", + ), + patch( + "homeassistant.components.ld2410_ble.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], { @@ -199,12 +209,15 @@ async def test_bluetooth_step_success(hass: HomeAssistant) -> None: assert result["step_id"] == "user" assert result["errors"] == {} - with patch( - "homeassistant.components.ld2410_ble.config_flow.LD2410BLE.initialise", - ), patch( - "homeassistant.components.ld2410_ble.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.ld2410_ble.config_flow.LD2410BLE.initialise", + ), + patch( + "homeassistant.components.ld2410_ble.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { diff --git a/tests/components/leaone/__init__.py b/tests/components/leaone/__init__.py index c54e07ccd87..3d62314fd9a 100644 --- a/tests/components/leaone/__init__.py +++ b/tests/components/leaone/__init__.py @@ -1,6 +1,5 @@ """Tests for the Leaone integration.""" - from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo SCALE_SERVICE_INFO = BluetoothServiceInfo( diff --git a/tests/components/leaone/test_config_flow.py b/tests/components/leaone/test_config_flow.py index b7e4abdcf6b..edacc3975c4 100644 --- a/tests/components/leaone/test_config_flow.py +++ b/tests/components/leaone/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Leaone config flow.""" + from unittest.mock import patch from homeassistant import config_entries diff --git a/tests/components/led_ble/test_config_flow.py b/tests/components/led_ble/test_config_flow.py index 947dc304bab..5ceda954ba8 100644 --- a/tests/components/led_ble/test_config_flow.py +++ b/tests/components/led_ble/test_config_flow.py @@ -1,4 +1,5 @@ """Test the LED BLE Bluetooth config flow.""" + from unittest.mock import patch from bleak import BleakError @@ -31,12 +32,15 @@ async def test_user_step_success(hass: HomeAssistant) -> None: assert result["step_id"] == "user" assert result["errors"] == {} - with patch( - "homeassistant.components.led_ble.config_flow.LEDBLE.update", - ), patch( - "homeassistant.components.led_ble.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.led_ble.config_flow.LEDBLE.update", + ), + patch( + "homeassistant.components.led_ble.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -117,12 +121,15 @@ async def test_user_step_cannot_connect(hass: HomeAssistant) -> None: assert result2["step_id"] == "user" assert result2["errors"] == {"base": "cannot_connect"} - with patch( - "homeassistant.components.led_ble.config_flow.LEDBLE.update", - ), patch( - "homeassistant.components.led_ble.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.led_ble.config_flow.LEDBLE.update", + ), + patch( + "homeassistant.components.led_ble.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], { @@ -169,12 +176,15 @@ async def test_user_step_unknown_exception(hass: HomeAssistant) -> None: assert result2["step_id"] == "user" assert result2["errors"] == {"base": "unknown"} - with patch( - "homeassistant.components.led_ble.config_flow.LEDBLE.update", - ), patch( - "homeassistant.components.led_ble.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.led_ble.config_flow.LEDBLE.update", + ), + patch( + "homeassistant.components.led_ble.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], { @@ -203,12 +213,15 @@ async def test_bluetooth_step_success(hass: HomeAssistant) -> None: assert result["step_id"] == "user" assert result["errors"] == {} - with patch( - "homeassistant.components.led_ble.config_flow.LEDBLE.update", - ), patch( - "homeassistant.components.led_ble.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.led_ble.config_flow.LEDBLE.update", + ), + patch( + "homeassistant.components.led_ble.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { diff --git a/tests/components/lg_soundbar/test_config_flow.py b/tests/components/lg_soundbar/test_config_flow.py index e4a56fd98ff..4fab919c555 100644 --- a/tests/components/lg_soundbar/test_config_flow.py +++ b/tests/components/lg_soundbar/test_config_flow.py @@ -1,4 +1,5 @@ """Test the lg_soundbar config flow.""" + from __future__ import annotations from collections.abc import Callable @@ -60,11 +61,14 @@ async def test_form(hass: HomeAssistant) -> None: assert result["type"] == "form" assert result["errors"] == {} - with patch( - "homeassistant.components.lg_soundbar.config_flow.temescal" - ) as mock_temescal, patch( - "homeassistant.components.lg_soundbar.async_setup_entry", return_value=True - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.lg_soundbar.config_flow.temescal" + ) as mock_temescal, + patch( + "homeassistant.components.lg_soundbar.async_setup_entry", return_value=True + ) as mock_setup_entry, + ): setup_mock_temescal( hass=hass, mock_temescal=mock_temescal, @@ -98,11 +102,14 @@ async def test_form_mac_info_response_empty(hass: HomeAssistant) -> None: assert result["type"] == "form" assert result["errors"] == {} - with patch( - "homeassistant.components.lg_soundbar.config_flow.temescal" - ) as mock_temescal, patch( - "homeassistant.components.lg_soundbar.async_setup_entry", return_value=True - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.lg_soundbar.config_flow.temescal" + ) as mock_temescal, + patch( + "homeassistant.components.lg_soundbar.async_setup_entry", return_value=True + ) as mock_setup_entry, + ): setup_mock_temescal( hass=hass, mock_temescal=mock_temescal, @@ -141,11 +148,14 @@ async def test_form_uuid_present_in_both_functions_uuid_q_empty( assert result["type"] == "form" assert result["errors"] == {} - with patch( - "homeassistant.components.lg_soundbar.config_flow.temescal" - ) as mock_temescal, patch( - "homeassistant.components.lg_soundbar.async_setup_entry", return_value=True - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.lg_soundbar.config_flow.temescal" + ) as mock_temescal, + patch( + "homeassistant.components.lg_soundbar.async_setup_entry", return_value=True + ) as mock_setup_entry, + ): setup_mock_temescal( hass=hass, mock_temescal=mock_temescal, @@ -186,14 +196,18 @@ async def test_form_uuid_present_in_both_functions_uuid_q_not_empty( assert result["type"] == "form" assert result["errors"] == {} - with patch( - "homeassistant.components.lg_soundbar.config_flow.QUEUE_TIMEOUT", - new=0.1, - ), patch( - "homeassistant.components.lg_soundbar.config_flow.temescal" - ) as mock_temescal, patch( - "homeassistant.components.lg_soundbar.async_setup_entry", return_value=True - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.lg_soundbar.config_flow.QUEUE_TIMEOUT", + new=0.1, + ), + patch( + "homeassistant.components.lg_soundbar.config_flow.temescal" + ) as mock_temescal, + patch( + "homeassistant.components.lg_soundbar.async_setup_entry", return_value=True + ) as mock_setup_entry, + ): setup_mock_temescal( hass=hass, mock_temescal=mock_temescal, @@ -229,11 +243,14 @@ async def test_form_uuid_missing_from_mac_info(hass: HomeAssistant) -> None: assert result["type"] == "form" assert result["errors"] == {} - with patch( - "homeassistant.components.lg_soundbar.config_flow.temescal" - ) as mock_temescal, patch( - "homeassistant.components.lg_soundbar.async_setup_entry", return_value=True - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.lg_soundbar.config_flow.temescal" + ) as mock_temescal, + patch( + "homeassistant.components.lg_soundbar.async_setup_entry", return_value=True + ) as mock_setup_entry, + ): setup_mock_temescal( hass=hass, mock_temescal=mock_temescal, @@ -268,14 +285,18 @@ async def test_form_uuid_not_provided_by_api(hass: HomeAssistant) -> None: assert result["type"] == "form" assert result["errors"] == {} - with patch( - "homeassistant.components.lg_soundbar.config_flow.QUEUE_TIMEOUT", - new=0.1, - ), patch( - "homeassistant.components.lg_soundbar.config_flow.temescal" - ) as mock_temescal, patch( - "homeassistant.components.lg_soundbar.async_setup_entry", return_value=True - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.lg_soundbar.config_flow.QUEUE_TIMEOUT", + new=0.1, + ), + patch( + "homeassistant.components.lg_soundbar.config_flow.temescal" + ) as mock_temescal, + patch( + "homeassistant.components.lg_soundbar.async_setup_entry", return_value=True + ) as mock_setup_entry, + ): setup_mock_temescal( hass=hass, mock_temescal=mock_temescal, @@ -309,14 +330,18 @@ async def test_form_both_queues_empty(hass: HomeAssistant) -> None: assert result["type"] == "form" assert result["errors"] == {} - with patch( - "homeassistant.components.lg_soundbar.config_flow.QUEUE_TIMEOUT", - new=0.1, - ), patch( - "homeassistant.components.lg_soundbar.config_flow.temescal" - ) as mock_temescal, patch( - "homeassistant.components.lg_soundbar.async_setup_entry", return_value=True - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.lg_soundbar.config_flow.QUEUE_TIMEOUT", + new=0.1, + ), + patch( + "homeassistant.components.lg_soundbar.config_flow.temescal" + ) as mock_temescal, + patch( + "homeassistant.components.lg_soundbar.async_setup_entry", return_value=True + ) as mock_setup_entry, + ): setup_mock_temescal(hass=hass, mock_temescal=mock_temescal) result2 = await hass.config_entries.flow.async_configure( @@ -351,12 +376,15 @@ async def test_no_uuid_host_already_configured(hass: HomeAssistant) -> None: assert result["type"] == "form" assert result["errors"] == {} - with patch( - "homeassistant.components.lg_soundbar.config_flow.QUEUE_TIMEOUT", - new=0.1, - ), patch( - "homeassistant.components.lg_soundbar.config_flow.temescal" - ) as mock_temescal: + with ( + patch( + "homeassistant.components.lg_soundbar.config_flow.QUEUE_TIMEOUT", + new=0.1, + ), + patch( + "homeassistant.components.lg_soundbar.config_flow.temescal" + ) as mock_temescal, + ): setup_mock_temescal( hass=hass, mock_temescal=mock_temescal, info={"s_user_name": "name"} ) diff --git a/tests/components/lidarr/conftest.py b/tests/components/lidarr/conftest.py index 308de36954e..5aabc0a822b 100644 --- a/tests/components/lidarr/conftest.py +++ b/tests/components/lidarr/conftest.py @@ -1,4 +1,5 @@ """Configure pytest for Lidarr tests.""" + from __future__ import annotations from collections.abc import Awaitable, Callable, Generator diff --git a/tests/components/lidarr/test_config_flow.py b/tests/components/lidarr/test_config_flow.py index 89bb6614739..01fa05ebb18 100644 --- a/tests/components/lidarr/test_config_flow.py +++ b/tests/components/lidarr/test_config_flow.py @@ -1,4 +1,5 @@ """Test Lidarr config flow.""" + from homeassistant.components.lidarr.const import DEFAULT_NAME, DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.const import CONF_API_KEY, CONF_SOURCE diff --git a/tests/components/lidarr/test_init.py b/tests/components/lidarr/test_init.py index ce3a8536b2f..48c5e3ff9a6 100644 --- a/tests/components/lidarr/test_init.py +++ b/tests/components/lidarr/test_init.py @@ -1,4 +1,5 @@ """Test Lidarr integration.""" + from homeassistant.components.lidarr.const import DEFAULT_NAME, DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant diff --git a/tests/components/lidarr/test_sensor.py b/tests/components/lidarr/test_sensor.py index 7dec62f1c47..3b3f661ce23 100644 --- a/tests/components/lidarr/test_sensor.py +++ b/tests/components/lidarr/test_sensor.py @@ -1,4 +1,5 @@ """The tests for Lidarr sensor platform.""" + from homeassistant.components.sensor import CONF_STATE_CLASS, SensorStateClass from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT from homeassistant.core import HomeAssistant diff --git a/tests/components/lifx/__init__.py b/tests/components/lifx/__init__.py index bb798f6f8a0..505d212a352 100644 --- a/tests/components/lifx/__init__.py +++ b/tests/components/lifx/__init__.py @@ -1,4 +1,5 @@ """Tests for the lifx integration.""" + from __future__ import annotations import asyncio @@ -242,8 +243,12 @@ def _patch_discovery(device: Light | None = None, no_device: bool = False): @contextmanager def _patcher(): - with patch.object(discovery, "DEFAULT_TIMEOUT", 0), patch( - "homeassistant.components.lifx.discovery.LifxDiscovery", MockLifxDiscovery + with ( + patch.object(discovery, "DEFAULT_TIMEOUT", 0), + patch( + "homeassistant.components.lifx.discovery.LifxDiscovery", + MockLifxDiscovery, + ), ): yield diff --git a/tests/components/lifx/conftest.py b/tests/components/lifx/conftest.py index 592dd07080f..c126ca20ecd 100644 --- a/tests/components/lifx/conftest.py +++ b/tests/components/lifx/conftest.py @@ -1,13 +1,23 @@ """Tests for the lifx integration.""" + from unittest.mock import AsyncMock, MagicMock, patch import pytest from homeassistant.components.lifx import config_flow, coordinator, util +from . import _patch_discovery + from tests.common import mock_device_registry, mock_registry +@pytest.fixture +def mock_discovery(): + """Mock discovery.""" + with _patch_discovery(): + yield + + @pytest.fixture def mock_effect_conductor(): """Mock the effect conductor.""" @@ -39,10 +49,11 @@ def lifx_mock_get_source_ip(mock_get_source_ip): @pytest.fixture(autouse=True) def lifx_no_wait_for_timeouts(): """Avoid waiting for timeouts in tests.""" - with patch.object(util, "OVERALL_TIMEOUT", 0), patch.object( - config_flow, "OVERALL_TIMEOUT", 0 - ), patch.object(coordinator, "OVERALL_TIMEOUT", 0), patch.object( - coordinator, "MAX_UPDATE_TIME", 0 + with ( + patch.object(util, "OVERALL_TIMEOUT", 0), + patch.object(config_flow, "OVERALL_TIMEOUT", 0), + patch.object(coordinator, "OVERALL_TIMEOUT", 0), + patch.object(coordinator, "MAX_UPDATE_TIME", 0), ): yield diff --git a/tests/components/lifx/test_binary_sensor.py b/tests/components/lifx/test_binary_sensor.py index 9fa065f3632..9221ac79149 100644 --- a/tests/components/lifx/test_binary_sensor.py +++ b/tests/components/lifx/test_binary_sensor.py @@ -1,8 +1,11 @@ """Test the lifx binary sensor platform.""" + from __future__ import annotations from datetime import timedelta +import pytest + from homeassistant.components import lifx from homeassistant.components.binary_sensor import BinarySensorDeviceClass from homeassistant.const import ( @@ -31,6 +34,7 @@ from . import ( from tests.common import MockConfigEntry, async_fire_time_changed +@pytest.mark.usefixtures("mock_discovery") async def test_hev_cycle_state( hass: HomeAssistant, entity_registry: er.EntityRegistry ) -> None: @@ -43,9 +47,11 @@ async def test_hev_cycle_state( ) config_entry.add_to_hass(hass) bulb = _mocked_clean_bulb() - with _patch_discovery(device=bulb), _patch_config_flow_try_connect( - device=bulb - ), _patch_device(device=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() @@ -64,11 +70,11 @@ async def test_hev_cycle_state( bulb.hev_cycle = {"duration": 7200, "remaining": 0, "last_power": False} async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=30)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get(entity_id).state == STATE_OFF bulb.hev_cycle = None async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=30)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get(entity_id).state == STATE_UNKNOWN diff --git a/tests/components/lifx/test_button.py b/tests/components/lifx/test_button.py index 1fd4da4531e..b2adf4f4c15 100644 --- a/tests/components/lifx/test_button.py +++ b/tests/components/lifx/test_button.py @@ -1,4 +1,5 @@ """Tests for button platform.""" + from unittest.mock import patch import pytest @@ -43,9 +44,11 @@ async def test_button_restart( ) 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): + 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() @@ -76,9 +79,11 @@ async def test_button_identify( ) 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): + 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() diff --git a/tests/components/lifx/test_config_flow.py b/tests/components/lifx/test_config_flow.py index a934c0ce831..0a0c26da424 100644 --- a/tests/components/lifx/test_config_flow.py +++ b/tests/components/lifx/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for the lifx integration config flow.""" + from ipaddress import ip_address import socket from unittest.mock import patch @@ -64,11 +65,12 @@ async def test_discovery(hass: HomeAssistant) -> None: 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: + 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}, @@ -167,9 +169,11 @@ async def test_discovery_with_existing_device_present(hass: HomeAssistant) -> No 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: + 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} ) @@ -204,8 +208,9 @@ async def test_discovery_no_device(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with _patch_discovery(no_device=True), _patch_config_flow_try_connect( - no_device=True + 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() @@ -224,8 +229,9 @@ async def test_manual(hass: HomeAssistant) -> None: assert not result["errors"] # Cannot connect (timeout) - with _patch_discovery(no_device=True), _patch_config_flow_try_connect( - no_device=True + 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} @@ -237,9 +243,12 @@ async def test_manual(hass: HomeAssistant) -> None: 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): + 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} ) @@ -254,8 +263,9 @@ async def test_manual(hass: HomeAssistant) -> None: 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 + 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} @@ -284,15 +294,18 @@ async def test_manual_dns_error(hass: HomeAssistant) -> None: async def async_setup(self): """Mock setup.""" - raise socket.gaierror() + 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, + 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"} @@ -313,9 +326,12 @@ async def test_manual_no_capabilities(hass: HomeAssistant) -> None: 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): + 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} ) @@ -364,8 +380,9 @@ async def test_discovered_by_discovery_and_dhcp(hass: HomeAssistant) -> None: assert result3["type"] == FlowResultType.ABORT assert result3["reason"] == "already_in_progress" - with _patch_discovery(no_device=True), _patch_config_flow_try_connect( - no_device=True + with ( + _patch_discovery(no_device=True), + _patch_config_flow_try_connect(no_device=True), ): result3 = await hass.config_entries.flow.async_init( DOMAIN, @@ -420,11 +437,14 @@ async def test_discovered_by_dhcp_or_discovery( assert result["type"] == FlowResultType.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: + 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() @@ -468,8 +488,9 @@ async def test_discovered_by_dhcp_or_discovery_failed_to_get_device( ) -> None: """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 + 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 @@ -531,8 +552,9 @@ async def test_refuse_relays(hass: HomeAssistant) -> None: assert result["step_id"] == "user" assert not result["errors"] - with _patch_discovery(device=_mocked_relay()), _patch_config_flow_try_connect( - device=_mocked_relay() + 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} @@ -571,9 +593,11 @@ async def test_suggested_area( bulb.group = None bulb.get_group = MockLifxCommandGetGroup(bulb, lifx_group="My LIFX Group") - with _patch_discovery(device=bulb), _patch_config_flow_try_connect( - device=bulb - ), _patch_device(device=bulb): + with ( + _patch_discovery(device=bulb), + _patch_config_flow_try_connect(device=bulb), + _patch_device(device=bulb), + ): await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) await hass.async_block_till_done() diff --git a/tests/components/lifx/test_diagnostics.py b/tests/components/lifx/test_diagnostics.py index a72695502a4..e3588dd3ed1 100644 --- a/tests/components/lifx/test_diagnostics.py +++ b/tests/components/lifx/test_diagnostics.py @@ -1,4 +1,5 @@ """Test LIFX diagnostics.""" + from homeassistant.components import lifx from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant @@ -34,9 +35,11 @@ async def test_bulb_diagnostics( ) 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): + 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() @@ -81,9 +84,11 @@ async def test_clean_bulb_diagnostics( ) config_entry.add_to_hass(hass) bulb = _mocked_clean_bulb() - with _patch_discovery(device=bulb), _patch_config_flow_try_connect( - device=bulb - ), _patch_device(device=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() @@ -133,9 +138,11 @@ async def test_infrared_bulb_diagnostics( ) config_entry.add_to_hass(hass) bulb = _mocked_infrared_bulb() - with _patch_discovery(device=bulb), _patch_config_flow_try_connect( - device=bulb - ), _patch_device(device=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() @@ -192,9 +199,11 @@ async def test_legacy_multizone_bulb_diagnostics( (46420, 65535, 65535, 3500), (46420, 65535, 65535, 3500), ] - with _patch_discovery(device=bulb), _patch_config_flow_try_connect( - device=bulb - ), _patch_device(device=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() @@ -304,9 +313,11 @@ async def test_multizone_bulb_diagnostics( (46420, 65535, 65535, 3500), (46420, 65535, 65535, 3500), ] - with _patch_discovery(device=bulb), _patch_config_flow_try_connect( - device=bulb - ), _patch_device(device=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() diff --git a/tests/components/lifx/test_init.py b/tests/components/lifx/test_init.py index 3f16cc44f41..3d0d127bf5c 100644 --- a/tests/components/lifx/test_init.py +++ b/tests/components/lifx/test_init.py @@ -1,4 +1,5 @@ """Tests for the lifx component.""" + from __future__ import annotations from datetime import timedelta @@ -49,10 +50,12 @@ async def test_configuring_lifx_causes_discovery(hass: HomeAssistant) -> None: def cleanup(self): """Mock cleanup.""" - with _patch_config_flow_try_connect(), patch.object( - discovery, "DEFAULT_TIMEOUT", 0 - ), patch( - "homeassistant.components.lifx.discovery.LifxDiscovery", MockLifxDiscovery + 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() @@ -63,15 +66,15 @@ async def test_configuring_lifx_causes_discovery(hass: HomeAssistant) -> None: assert start_calls == 1 async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert start_calls == 2 async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=15)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert start_calls == 3 async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=30)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert start_calls == 4 @@ -96,9 +99,11 @@ async def test_config_entry_retry(hass: HomeAssistant) -> None: 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): + 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 @@ -138,15 +143,18 @@ async def test_dns_error_at_startup(hass: HomeAssistant) -> None: async def async_setup(self): """Mock setup.""" - raise socket.gaierror() + 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, + 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() diff --git a/tests/components/lifx/test_light.py b/tests/components/lifx/test_light.py index 887e622b5cc..56630053cc0 100644 --- a/tests/components/lifx/test_light.py +++ b/tests/components/lifx/test_light.py @@ -92,9 +92,11 @@ async def test_light_unique_id( ) 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): + 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() @@ -118,9 +120,11 @@ async def test_light_unique_id_new_firmware( ) 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): + 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() @@ -141,9 +145,11 @@ async def test_light_strip(hass: HomeAssistant) -> None: 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): + 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() @@ -438,9 +444,11 @@ async def test_extended_multizone_messages(hass: HomeAssistant) -> None: bulb.color = [65535, 65535, 65535, 3500] bulb.color_zones = [(65535, 65535, 65535, 3500)] * 8 bulb.zones_count = 8 - with _patch_discovery(device=bulb), _patch_config_flow_try_connect( - device=bulb - ), _patch_device(device=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() @@ -663,6 +671,7 @@ async def test_extended_multizone_messages(hass: HomeAssistant) -> None: ) +@pytest.mark.usefixtures("mock_discovery") async def test_matrix_flame_morph_effects(hass: HomeAssistant) -> None: """Test the firmware flame and morph effects on a matrix device.""" config_entry = MockConfigEntry( @@ -672,9 +681,11 @@ async def test_matrix_flame_morph_effects(hass: HomeAssistant) -> None: bulb = _mocked_tile() bulb.power_level = 0 bulb.color = [65535, 65535, 65535, 65535] - with _patch_discovery(device=bulb), _patch_config_flow_try_connect( - device=bulb - ), _patch_device(device=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() @@ -721,7 +732,7 @@ async def test_matrix_flame_morph_effects(hass: HomeAssistant) -> None: ], } async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=30)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(entity_id) assert state.state == STATE_ON @@ -777,7 +788,7 @@ async def test_matrix_flame_morph_effects(hass: HomeAssistant) -> None: ], } async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=30)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(entity_id) assert state.state == STATE_ON @@ -803,6 +814,7 @@ async def test_matrix_flame_morph_effects(hass: HomeAssistant) -> None: bulb.set_power.reset_mock() +@pytest.mark.usefixtures("mock_discovery") async def test_lightstrip_move_effect(hass: HomeAssistant) -> None: """Test the firmware move effect on a light strip.""" config_entry = MockConfigEntry( @@ -813,9 +825,11 @@ async def test_lightstrip_move_effect(hass: HomeAssistant) -> None: bulb.product = 38 bulb.power_level = 0 bulb.color = [65535, 65535, 65535, 65535] - with _patch_discovery(device=bulb), _patch_config_flow_try_connect( - device=bulb - ), _patch_device(device=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() @@ -859,7 +873,7 @@ async def test_lightstrip_move_effect(hass: HomeAssistant) -> None: bulb.power_level = 65535 bulb.effect = {"name": "MOVE", "speed": 4.5, "direction": "Left"} async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=30)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(entity_id) assert state.state == STATE_ON @@ -910,9 +924,11 @@ async def test_color_light_with_temp( 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): + 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() @@ -1071,9 +1087,11 @@ async def test_white_bulb(hass: HomeAssistant) -> None: 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): + 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() @@ -1119,6 +1137,7 @@ async def test_white_bulb(hass: HomeAssistant) -> None: bulb.set_color.reset_mock() +@pytest.mark.usefixtures("mock_discovery") async def test_config_zoned_light_strip_fails( hass: HomeAssistant, entity_registry: er.EntityRegistry ) -> None: @@ -1154,7 +1173,7 @@ async def test_config_zoned_light_strip_fails( 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() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get(entity_id).state == STATE_UNAVAILABLE @@ -1251,9 +1270,11 @@ async def test_brightness_bulb(hass: HomeAssistant) -> None: 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): + 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() @@ -1298,9 +1319,11 @@ async def test_transitions_brightness_only(hass: HomeAssistant) -> None: 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): + 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() @@ -1365,9 +1388,11 @@ async def test_transitions_color_bulb(hass: HomeAssistant) -> None: 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): + 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() @@ -1476,9 +1501,11 @@ async def test_lifx_set_state_color(hass: HomeAssistant) -> None: bulb = _mocked_bulb_new_firmware() bulb.power_level = 65535 bulb.color = [32000, None, 32000, 2700] - with _patch_discovery(device=bulb), _patch_config_flow_try_connect( - device=bulb - ), _patch_device(device=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() @@ -1554,9 +1581,11 @@ async def test_lifx_set_state_kelvin(hass: HomeAssistant) -> None: 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): + 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() @@ -1610,9 +1639,11 @@ async def test_infrared_color_bulb(hass: HomeAssistant) -> None: 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): + 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() @@ -1651,9 +1682,11 @@ async def test_color_bulb_is_actually_off(hass: HomeAssistant) -> None: 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): + 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() @@ -1702,9 +1735,11 @@ async def test_clean_bulb(hass: HomeAssistant) -> None: bulb = _mocked_clean_bulb() bulb.power_level = 0 bulb.hev_cycle = {"duration": 7200, "remaining": 0, "last_power": False} - with _patch_discovery(device=bulb), _patch_config_flow_try_connect( - device=bulb - ), _patch_device(device=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() @@ -1732,9 +1767,11 @@ async def test_set_hev_cycle_state_fails_for_color_bulb(hass: HomeAssistant) -> config_entry.add_to_hass(hass) bulb = _mocked_bulb() bulb.power_level = 0 - with _patch_discovery(device=bulb), _patch_config_flow_try_connect( - device=bulb - ), _patch_device(device=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() @@ -1763,9 +1800,11 @@ async def test_light_strip_zones_not_populated_yet(hass: HomeAssistant) -> None: bulb.color = [65535, 65535, 65535, 65535] assert bulb.get_color_zones.calls == [] - with _patch_discovery(device=bulb), _patch_config_flow_try_connect( - device=bulb - ), _patch_device(device=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() diff --git a/tests/components/lifx/test_migration.py b/tests/components/lifx/test_migration.py index 8aafcce670b..0604ee1c8a7 100644 --- a/tests/components/lifx/test_migration.py +++ b/tests/components/lifx/test_migration.py @@ -1,4 +1,5 @@ """Tests the lifx migration.""" + from __future__ import annotations from datetime import timedelta @@ -129,10 +130,13 @@ async def test_discovery_is_more_frequent_during_migration( 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 + 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() @@ -143,15 +147,15 @@ async def test_discovery_is_more_frequent_during_migration( assert start_calls == 1 async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert start_calls == 3 async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=10)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert start_calls == 4 async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=15)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert start_calls == 5 diff --git a/tests/components/lifx/test_select.py b/tests/components/lifx/test_select.py index 529925be726..a6338566ed4 100644 --- a/tests/components/lifx/test_select.py +++ b/tests/components/lifx/test_select.py @@ -1,6 +1,9 @@ """Tests for the lifx integration select entity.""" + from datetime import timedelta +import pytest + from homeassistant.components import lifx from homeassistant.components.lifx.const import DOMAIN from homeassistant.components.select import DOMAIN as SELECT_DOMAIN @@ -40,9 +43,11 @@ async def test_theme_select( bulb.product = 38 bulb.power_level = 0 bulb.color = [0, 0, 65535, 3500] - with _patch_discovery(device=bulb), _patch_config_flow_try_connect( - device=bulb - ), _patch_device(device=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() @@ -76,9 +81,11 @@ async def test_infrared_brightness( ) config_entry.add_to_hass(hass) bulb = _mocked_infrared_bulb() - with _patch_discovery(device=bulb), _patch_config_flow_try_connect( - device=bulb - ), _patch_device(device=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() @@ -94,6 +101,7 @@ async def test_infrared_brightness( assert state.state == "100%" +@pytest.mark.usefixtures("mock_discovery") async def test_set_infrared_brightness_25_percent(hass: HomeAssistant) -> None: """Test getting and setting infrared brightness.""" @@ -105,9 +113,11 @@ async def test_set_infrared_brightness_25_percent(hass: HomeAssistant) -> None: ) config_entry.add_to_hass(hass) bulb = _mocked_infrared_bulb() - with _patch_discovery(device=bulb), _patch_config_flow_try_connect( - device=bulb - ), _patch_device(device=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() @@ -123,7 +133,7 @@ async def test_set_infrared_brightness_25_percent(hass: HomeAssistant) -> None: bulb.get_infrared = MockLifxCommand(bulb, infrared_brightness=16383) async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=30)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert bulb.set_infrared.calls[0][0][0] == 16383 @@ -133,6 +143,7 @@ async def test_set_infrared_brightness_25_percent(hass: HomeAssistant) -> None: bulb.set_infrared.reset_mock() +@pytest.mark.usefixtures("mock_discovery") async def test_set_infrared_brightness_50_percent(hass: HomeAssistant) -> None: """Test getting and setting infrared brightness.""" @@ -144,9 +155,11 @@ async def test_set_infrared_brightness_50_percent(hass: HomeAssistant) -> None: ) config_entry.add_to_hass(hass) bulb = _mocked_infrared_bulb() - with _patch_discovery(device=bulb), _patch_config_flow_try_connect( - device=bulb - ), _patch_device(device=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() @@ -162,7 +175,7 @@ async def test_set_infrared_brightness_50_percent(hass: HomeAssistant) -> None: bulb.get_infrared = MockLifxCommand(bulb, infrared_brightness=32767) async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=30)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert bulb.set_infrared.calls[0][0][0] == 32767 @@ -172,6 +185,7 @@ async def test_set_infrared_brightness_50_percent(hass: HomeAssistant) -> None: bulb.set_infrared.reset_mock() +@pytest.mark.usefixtures("mock_discovery") async def test_set_infrared_brightness_100_percent(hass: HomeAssistant) -> None: """Test getting and setting infrared brightness.""" @@ -183,9 +197,11 @@ async def test_set_infrared_brightness_100_percent(hass: HomeAssistant) -> None: ) config_entry.add_to_hass(hass) bulb = _mocked_infrared_bulb() - with _patch_discovery(device=bulb), _patch_config_flow_try_connect( - device=bulb - ), _patch_device(device=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() @@ -201,7 +217,7 @@ async def test_set_infrared_brightness_100_percent(hass: HomeAssistant) -> None: bulb.get_infrared = MockLifxCommand(bulb, infrared_brightness=65535) async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=30)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert bulb.set_infrared.calls[0][0][0] == 65535 @@ -211,6 +227,7 @@ async def test_set_infrared_brightness_100_percent(hass: HomeAssistant) -> None: bulb.set_infrared.reset_mock() +@pytest.mark.usefixtures("mock_discovery") async def test_disable_infrared(hass: HomeAssistant) -> None: """Test getting and setting infrared brightness.""" @@ -222,9 +239,11 @@ async def test_disable_infrared(hass: HomeAssistant) -> None: ) config_entry.add_to_hass(hass) bulb = _mocked_infrared_bulb() - with _patch_discovery(device=bulb), _patch_config_flow_try_connect( - device=bulb - ), _patch_device(device=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() @@ -240,7 +259,7 @@ async def test_disable_infrared(hass: HomeAssistant) -> None: bulb.get_infrared = MockLifxCommand(bulb, infrared_brightness=0) async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=30)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert bulb.set_infrared.calls[0][0][0] == 0 @@ -250,6 +269,7 @@ async def test_disable_infrared(hass: HomeAssistant) -> None: bulb.set_infrared.reset_mock() +@pytest.mark.usefixtures("mock_discovery") async def test_invalid_infrared_brightness(hass: HomeAssistant) -> None: """Test getting and setting infrared brightness.""" @@ -261,9 +281,11 @@ async def test_invalid_infrared_brightness(hass: HomeAssistant) -> None: ) config_entry.add_to_hass(hass) bulb = _mocked_infrared_bulb() - with _patch_discovery(device=bulb), _patch_config_flow_try_connect( - device=bulb - ), _patch_device(device=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() @@ -272,7 +294,7 @@ async def test_invalid_infrared_brightness(hass: HomeAssistant) -> None: bulb.get_infrared = MockLifxCommand(bulb, infrared_brightness=12345) async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=30)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(entity_id) assert state.state == STATE_UNKNOWN diff --git a/tests/components/lifx/test_sensor.py b/tests/components/lifx/test_sensor.py index e27bc0de3a8..b7ff563bdbc 100644 --- a/tests/components/lifx/test_sensor.py +++ b/tests/components/lifx/test_sensor.py @@ -1,4 +1,5 @@ """Test the LIFX sensor platform.""" + from __future__ import annotations from datetime import timedelta @@ -44,9 +45,11 @@ async def test_rssi_sensor( ) 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): + 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() @@ -59,12 +62,14 @@ async def test_rssi_sensor( # Test enabling entity updated_entry = entity_registry.async_update_entity( - entry.entity_id, **{"disabled_by": None} + entry.entity_id, disabled_by=None ) - with _patch_discovery(device=bulb), _patch_config_flow_try_connect( - device=bulb - ), _patch_device(device=bulb): + with ( + _patch_discovery(device=bulb), + _patch_config_flow_try_connect(device=bulb), + _patch_device(device=bulb), + ): await hass.config_entries.async_reload(config_entry.entry_id) await hass.async_block_till_done() @@ -96,9 +101,11 @@ async def test_rssi_sensor_old_firmware( ) config_entry.add_to_hass(hass) bulb = _mocked_bulb_old_firmware() - with _patch_discovery(device=bulb), _patch_config_flow_try_connect( - device=bulb - ), _patch_device(device=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() @@ -111,12 +118,14 @@ async def test_rssi_sensor_old_firmware( # Test enabling entity updated_entry = entity_registry.async_update_entity( - entry.entity_id, **{"disabled_by": None} + entry.entity_id, disabled_by=None ) - with _patch_discovery(device=bulb), _patch_config_flow_try_connect( - device=bulb - ), _patch_device(device=bulb): + with ( + _patch_discovery(device=bulb), + _patch_config_flow_try_connect(device=bulb), + _patch_device(device=bulb), + ): await hass.config_entries.async_reload(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/light/common.py b/tests/components/light/common.py index 4f83ffacbdc..26c4d18706d 100644 --- a/tests/components/light/common.py +++ b/tests/components/light/common.py @@ -3,6 +3,7 @@ All containing methods are legacy helpers that should not be used by new components. Instead call the service directly. """ + from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, @@ -20,6 +21,8 @@ from homeassistant.components.light import ( ATTR_WHITE, ATTR_XY_COLOR, DOMAIN, + ColorMode, + LightEntity, ) from homeassistant.const import ( ATTR_ENTITY_ID, @@ -30,6 +33,8 @@ from homeassistant.const import ( ) from homeassistant.loader import bind_hass +from tests.common import MockToggleEntity + @bind_hass def turn_on( @@ -216,3 +221,65 @@ async def async_toggle( } await hass.services.async_call(DOMAIN, SERVICE_TOGGLE, data, blocking=True) + + +TURN_ON_ARG_TO_COLOR_MODE = { + "hs_color": ColorMode.HS, + "xy_color": ColorMode.XY, + "rgb_color": ColorMode.RGB, + "rgbw_color": ColorMode.RGBW, + "rgbww_color": ColorMode.RGBWW, + "color_temp_kelvin": ColorMode.COLOR_TEMP, +} + + +class MockLight(MockToggleEntity, LightEntity): + """Mock light class.""" + + _attr_max_color_temp_kelvin = 6500 + _attr_min_color_temp_kelvin = 2000 + supported_features = 0 + + brightness = None + color_temp_kelvin = None + hs_color = None + rgb_color = None + rgbw_color = None + rgbww_color = None + xy_color = None + + def __init__( + self, + name, + state, + unique_id=None, + supported_color_modes: set[ColorMode] | None = None, + ): + """Initialize the mock light.""" + super().__init__(name, state, unique_id) + if supported_color_modes is None: + supported_color_modes = {ColorMode.ONOFF} + self._attr_supported_color_modes = supported_color_modes + color_mode = ColorMode.UNKNOWN + if len(supported_color_modes) == 1: + color_mode = next(iter(supported_color_modes)) + self._attr_color_mode = color_mode + + def turn_on(self, **kwargs): + """Turn the entity on.""" + super().turn_on(**kwargs) + for key, value in kwargs.items(): + if key in [ + "brightness", + "hs_color", + "xy_color", + "rgb_color", + "rgbw_color", + "rgbww_color", + "color_temp_kelvin", + ]: + setattr(self, key, value) + if key == "white": + setattr(self, "brightness", value) + if key in TURN_ON_ARG_TO_COLOR_MODE: + self._attr_color_mode = TURN_ON_ARG_TO_COLOR_MODE[key] diff --git a/tests/components/light/test_device_action.py b/tests/components/light/test_device_action.py index 3b60b886b02..3f8ed8adbb6 100644 --- a/tests/components/light/test_device_action.py +++ b/tests/components/light/test_device_action.py @@ -1,4 +1,5 @@ """The test for light device automation.""" + import pytest from pytest_unordered import unordered @@ -82,12 +83,12 @@ async def test_get_actions( @pytest.mark.parametrize( ("hidden_by", "entity_category"), - ( + [ (RegistryEntryHider.INTEGRATION, None), (RegistryEntryHider.USER, None), (None, EntityCategory.CONFIG), (None, EntityCategory.DIAGNOSTIC), - ), + ], ) async def test_get_actions_hidden_auxiliary( hass: HomeAssistant, diff --git a/tests/components/light/test_device_condition.py b/tests/components/light/test_device_condition.py index 000784ce63c..16237547bc9 100644 --- a/tests/components/light/test_device_condition.py +++ b/tests/components/light/test_device_condition.py @@ -1,4 +1,5 @@ """The test for light device automation.""" + from datetime import timedelta from freezegun import freeze_time @@ -20,7 +21,9 @@ from tests.common import ( async_get_device_automation_capabilities, async_get_device_automations, async_mock_service, + setup_test_component_platform, ) +from tests.components.light.common import MockLight @pytest.fixture(autouse=True, name="stub_blueprint_populate") @@ -68,12 +71,12 @@ async def test_get_conditions( @pytest.mark.parametrize( ("hidden_by", "entity_category"), - ( + [ (RegistryEntryHider.INTEGRATION, None), (RegistryEntryHider.USER, None), (None, EntityCategory.CONFIG), (None, EntityCategory.DIAGNOSTIC), - ), + ], ) async def test_get_conditions_hidden_auxiliary( hass: HomeAssistant, @@ -322,7 +325,7 @@ async def test_if_fires_on_for_condition( device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, calls, - enable_custom_integrations: None, + mock_light_entities: list[MockLight], ) -> None: """Test for firing if condition is on with delay.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -341,14 +344,10 @@ async def test_if_fires_on_for_condition( point2 = point1 + timedelta(seconds=10) point3 = point2 + timedelta(seconds=10) - platform = getattr(hass.components, f"test.{DOMAIN}") - - platform.init() + setup_test_component_platform(hass, DOMAIN, mock_light_entities) assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) await hass.async_block_till_done() - ent1, ent2, ent3 = platform.ENTITIES - with freeze_time(point1) as freezer: assert await async_setup_component( hass, diff --git a/tests/components/light/test_device_trigger.py b/tests/components/light/test_device_trigger.py index 5ee6752640e..ff692432d31 100644 --- a/tests/components/light/test_device_trigger.py +++ b/tests/components/light/test_device_trigger.py @@ -1,4 +1,5 @@ """The test for light device automation.""" + from datetime import timedelta import pytest @@ -68,12 +69,12 @@ async def test_get_triggers( @pytest.mark.parametrize( ("hidden_by", "entity_category"), - ( + [ (RegistryEntryHider.INTEGRATION, None), (RegistryEntryHider.USER, None), (None, EntityCategory.CONFIG), (None, EntityCategory.DIAGNOSTIC), - ), + ], ) async def test_get_triggers_hidden_auxiliary( hass: HomeAssistant, diff --git a/tests/components/light/test_init.py b/tests/components/light/test_init.py index ca25611f890..6a04d5e33cc 100644 --- a/tests/components/light/test_init.py +++ b/tests/components/light/test_init.py @@ -1,4 +1,5 @@ """The tests for the Light component.""" + from unittest.mock import MagicMock, mock_open, patch import pytest @@ -21,7 +22,13 @@ from homeassistant.exceptions import HomeAssistantError, Unauthorized from homeassistant.setup import async_setup_component import homeassistant.util.color as color_util -from tests.common import MockEntityPlatform, MockUser, async_mock_service +from tests.common import ( + MockEntityPlatform, + MockUser, + async_mock_service, + setup_test_component_platform, +) +from tests.components.light.common import MockLight orig_Profiles = light.Profiles @@ -107,18 +114,19 @@ async def test_methods(hass: HomeAssistant) -> None: async def test_services( - hass: HomeAssistant, mock_light_profiles, enable_custom_integrations: None + hass: HomeAssistant, + mock_light_profiles, + mock_light_entities: list[MockLight], ) -> None: """Test the provided services.""" - platform = getattr(hass.components, "test.light") + setup_test_component_platform(hass, light.DOMAIN, mock_light_entities) - platform.init() assert await async_setup_component( hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) await hass.async_block_till_done() - ent1, ent2, ent3 = platform.ENTITIES + ent1, ent2, ent3 = mock_light_entities ent1.supported_color_modes = [light.ColorMode.HS] ent3.supported_color_modes = [light.ColorMode.HS] ent1.supported_features = light.LightEntityFeature.TRANSITION @@ -437,7 +445,7 @@ async def test_services( @pytest.mark.parametrize( ("profile_name", "last_call", "expected_data"), - ( + [ ( "test", "turn_on", @@ -500,7 +508,7 @@ async def test_services( light.ATTR_TRANSITION: 5.3, }, ), - ), + ], ) async def test_light_profiles( hass: HomeAssistant, @@ -508,11 +516,10 @@ async def test_light_profiles( profile_name, expected_data, last_call, - enable_custom_integrations: None, + mock_light_entities: list[MockLight], ) -> None: """Test light profiles.""" - platform = getattr(hass.components, "test.light") - platform.init() + setup_test_component_platform(hass, light.DOMAIN, mock_light_entities) profile_mock_data = { "test": (0.4, 0.6, 100, 0), @@ -532,7 +539,7 @@ async def test_light_profiles( ) await hass.async_block_till_done() - ent1, _, _ = platform.ENTITIES + ent1, _, _ = mock_light_entities ent1.supported_color_modes = [light.ColorMode.HS] ent1.supported_features = light.LightEntityFeature.TRANSITION @@ -555,11 +562,12 @@ async def test_light_profiles( async def test_default_profiles_group( - hass: HomeAssistant, mock_light_profiles, enable_custom_integrations: None + hass: HomeAssistant, + mock_light_profiles, + mock_light_entities: list[MockLight], ) -> None: """Test default turn-on light profile for all lights.""" - platform = getattr(hass.components, "test.light") - platform.init() + setup_test_component_platform(hass, light.DOMAIN, mock_light_entities) assert await async_setup_component( hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} @@ -569,7 +577,7 @@ async def test_default_profiles_group( profile = light.Profile("group.all_lights.default", 0.4, 0.6, 99, 2) mock_light_profiles[profile.name] = profile - ent, _, _ = platform.ENTITIES + ent, _, _ = mock_light_entities ent.supported_color_modes = [light.ColorMode.HS] ent.supported_features = light.LightEntityFeature.TRANSITION await hass.services.async_call( @@ -590,7 +598,7 @@ async def test_default_profiles_group( "expected_params_state_was_off", "expected_params_state_was_on", ), - ( + [ ( # No turn on params, should apply profile {}, @@ -773,19 +781,18 @@ async def test_default_profiles_group( light.ATTR_TRANSITION: 1, }, ), - ), + ], ) async def test_default_profiles_light( hass: HomeAssistant, mock_light_profiles, extra_call_params, - enable_custom_integrations: None, expected_params_state_was_off, expected_params_state_was_on, + mock_light_entities: list[MockLight], ) -> None: """Test default turn-on light profile for a specific light.""" - platform = getattr(hass.components, "test.light") - platform.init() + setup_test_component_platform(hass, light.DOMAIN, mock_light_entities) assert await async_setup_component( hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} @@ -797,7 +804,7 @@ async def test_default_profiles_light( profile = light.Profile("light.ceiling_2.default", 0.6, 0.6, 100, 3) mock_light_profiles[profile.name] = profile - dev = next(filter(lambda x: x.entity_id == "light.ceiling_2", platform.ENTITIES)) + dev = next(filter(lambda x: x.entity_id == "light.ceiling_2", mock_light_entities)) dev.supported_color_modes = { light.ColorMode.COLOR_TEMP, light.ColorMode.HS, @@ -849,11 +856,13 @@ async def test_default_profiles_light( async def test_light_context( - hass: HomeAssistant, hass_admin_user: MockUser, enable_custom_integrations: None + hass: HomeAssistant, + hass_admin_user: MockUser, + mock_light_entities: list[MockLight], ) -> None: """Test that light context works.""" - platform = getattr(hass.components, "test.light") - platform.init() + setup_test_component_platform(hass, light.DOMAIN, mock_light_entities) + assert await async_setup_component(hass, "light", {"light": {"platform": "test"}}) await hass.async_block_till_done() @@ -875,11 +884,13 @@ async def test_light_context( async def test_light_turn_on_auth( - hass: HomeAssistant, hass_read_only_user: MockUser, enable_custom_integrations: None + hass: HomeAssistant, + hass_read_only_user: MockUser, + mock_light_entities: list[MockLight], ) -> None: """Test that light context works.""" - platform = getattr(hass.components, "test.light") - platform.init() + setup_test_component_platform(hass, light.DOMAIN, mock_light_entities) + assert await async_setup_component(hass, "light", {"light": {"platform": "test"}}) await hass.async_block_till_done() @@ -898,21 +909,22 @@ async def test_light_turn_on_auth( ) -async def test_light_brightness_step( - hass: HomeAssistant, enable_custom_integrations: None -) -> None: +async def test_light_brightness_step(hass: HomeAssistant) -> None: """Test that light context works.""" - platform = getattr(hass.components, "test.light") - platform.init(empty=True) - platform.ENTITIES.append(platform.MockLight("Test_0", STATE_ON)) - platform.ENTITIES.append(platform.MockLight("Test_1", STATE_ON)) - entity0 = platform.ENTITIES[0] + entities = [ + MockLight("Test_0", STATE_ON), + MockLight("Test_1", STATE_ON), + ] + + setup_test_component_platform(hass, light.DOMAIN, entities) + + entity0 = entities[0] entity0.supported_features = light.SUPPORT_BRIGHTNESS # Set color modes to none to trigger backwards compatibility in LightEntity entity0.supported_color_modes = None entity0.color_mode = None entity0.brightness = 100 - entity1 = platform.ENTITIES[1] + entity1 = entities[1] entity1.supported_features = light.SUPPORT_BRIGHTNESS # Set color modes to none to trigger backwards compatibility in LightEntity entity1.supported_color_modes = None @@ -969,12 +981,14 @@ async def test_light_brightness_step( async def test_light_brightness_pct_conversion( - hass: HomeAssistant, enable_custom_integrations: None + hass: HomeAssistant, + enable_custom_integrations: None, + mock_light_entities: list[MockLight], ) -> None: """Test that light brightness percent conversion.""" - platform = getattr(hass.components, "test.light") - platform.init() - entity = platform.ENTITIES[0] + setup_test_component_platform(hass, light.DOMAIN, mock_light_entities) + + entity = mock_light_entities[0] entity.supported_features = light.SUPPORT_BRIGHTNESS # Set color modes to none to trigger backwards compatibility in LightEntity entity.supported_color_modes = None @@ -1127,41 +1141,40 @@ invalid_no_brightness_no_color_no_transition,,, assert invalid_profile_name not in profiles.data -@pytest.mark.parametrize("light_state", (STATE_ON, STATE_OFF)) +@pytest.mark.parametrize("light_state", [STATE_ON, STATE_OFF]) async def test_light_backwards_compatibility_supported_color_modes( - hass: HomeAssistant, light_state, enable_custom_integrations: None + hass: HomeAssistant, light_state ) -> None: """Test supported_color_modes if not implemented by the entity.""" - platform = getattr(hass.components, "test.light") - platform.init(empty=True) + entities = [ + MockLight("Test_0", light_state), + MockLight("Test_1", light_state), + MockLight("Test_2", light_state), + MockLight("Test_3", light_state), + MockLight("Test_4", light_state), + ] - platform.ENTITIES.append(platform.MockLight("Test_0", light_state)) - platform.ENTITIES.append(platform.MockLight("Test_1", light_state)) - platform.ENTITIES.append(platform.MockLight("Test_2", light_state)) - platform.ENTITIES.append(platform.MockLight("Test_3", light_state)) - platform.ENTITIES.append(platform.MockLight("Test_4", light_state)) + entity0 = entities[0] - entity0 = platform.ENTITIES[0] - - entity1 = platform.ENTITIES[1] + entity1 = entities[1] entity1.supported_features = light.SUPPORT_BRIGHTNESS # Set color modes to none to trigger backwards compatibility in LightEntity entity1.supported_color_modes = None entity1.color_mode = None - entity2 = platform.ENTITIES[2] + entity2 = entities[2] entity2.supported_features = light.SUPPORT_BRIGHTNESS | light.SUPPORT_COLOR_TEMP # Set color modes to none to trigger backwards compatibility in LightEntity entity2.supported_color_modes = None entity2.color_mode = None - entity3 = platform.ENTITIES[3] + entity3 = entities[3] entity3.supported_features = light.SUPPORT_BRIGHTNESS | light.SUPPORT_COLOR # Set color modes to none to trigger backwards compatibility in LightEntity entity3.supported_color_modes = None entity3.color_mode = None - entity4 = platform.ENTITIES[4] + entity4 = entities[4] entity4.supported_features = ( light.SUPPORT_BRIGHTNESS | light.SUPPORT_COLOR | light.SUPPORT_COLOR_TEMP ) @@ -1169,6 +1182,8 @@ async def test_light_backwards_compatibility_supported_color_modes( entity4.supported_color_modes = None entity4.color_mode = None + setup_test_component_platform(hass, light.DOMAIN, entities) + assert await async_setup_component(hass, "light", {"light": {"platform": "test"}}) await hass.async_block_till_done() @@ -1211,43 +1226,40 @@ async def test_light_backwards_compatibility_supported_color_modes( assert state.attributes["color_mode"] == light.ColorMode.UNKNOWN -async def test_light_backwards_compatibility_color_mode( - hass: HomeAssistant, enable_custom_integrations: None -) -> None: +async def test_light_backwards_compatibility_color_mode(hass: HomeAssistant) -> None: """Test color_mode if not implemented by the entity.""" - platform = getattr(hass.components, "test.light") - platform.init(empty=True) + entities = [ + MockLight("Test_0", STATE_ON), + MockLight("Test_1", STATE_ON), + MockLight("Test_2", STATE_ON), + MockLight("Test_3", STATE_ON), + MockLight("Test_4", STATE_ON), + ] - platform.ENTITIES.append(platform.MockLight("Test_0", STATE_ON)) - platform.ENTITIES.append(platform.MockLight("Test_1", STATE_ON)) - platform.ENTITIES.append(platform.MockLight("Test_2", STATE_ON)) - platform.ENTITIES.append(platform.MockLight("Test_3", STATE_ON)) - platform.ENTITIES.append(platform.MockLight("Test_4", STATE_ON)) + entity0 = entities[0] - entity0 = platform.ENTITIES[0] - - entity1 = platform.ENTITIES[1] + entity1 = entities[1] entity1.supported_features = light.SUPPORT_BRIGHTNESS # Set color modes to none to trigger backwards compatibility in LightEntity entity1.supported_color_modes = None entity1.color_mode = None entity1.brightness = 100 - entity2 = platform.ENTITIES[2] + entity2 = entities[2] entity2.supported_features = light.SUPPORT_BRIGHTNESS | light.SUPPORT_COLOR_TEMP # Set color modes to none to trigger backwards compatibility in LightEntity entity2.supported_color_modes = None entity2.color_mode = None entity2.color_temp_kelvin = 10000 - entity3 = platform.ENTITIES[3] + entity3 = entities[3] entity3.supported_features = light.SUPPORT_BRIGHTNESS | light.SUPPORT_COLOR # Set color modes to none to trigger backwards compatibility in LightEntity entity3.supported_color_modes = None entity3.color_mode = None entity3.hs_color = (240, 100) - entity4 = platform.ENTITIES[4] + entity4 = entities[4] entity4.supported_features = ( light.SUPPORT_BRIGHTNESS | light.SUPPORT_COLOR | light.SUPPORT_COLOR_TEMP ) @@ -1257,6 +1269,8 @@ async def test_light_backwards_compatibility_color_mode( entity4.hs_color = (240, 100) entity4.color_temp_kelvin = 10000 + setup_test_component_platform(hass, light.DOMAIN, entities) + assert await async_setup_component(hass, "light", {"light": {"platform": "test"}}) await hass.async_block_till_done() @@ -1288,18 +1302,13 @@ async def test_light_backwards_compatibility_color_mode( assert state.attributes["color_mode"] == light.ColorMode.HS -async def test_light_service_call_rgbw( - hass: HomeAssistant, enable_custom_integrations: None -) -> None: +async def test_light_service_call_rgbw(hass: HomeAssistant) -> None: """Test rgbw functionality in service calls.""" - platform = getattr(hass.components, "test.light") - platform.init(empty=True) - - platform.ENTITIES.append(platform.MockLight("Test_rgbw", STATE_ON)) - - entity0 = platform.ENTITIES[0] + entity0 = MockLight("Test_rgbw", STATE_ON) entity0.supported_color_modes = {light.ColorMode.RGBW} + setup_test_component_platform(hass, light.DOMAIN, [entity0]) + assert await async_setup_component(hass, "light", {"light": {"platform": "test"}}) await hass.async_block_till_done() @@ -1321,25 +1330,23 @@ async def test_light_service_call_rgbw( assert data == {"brightness": 255, "rgbw_color": (10, 20, 30, 40)} -async def test_light_state_off( - hass: HomeAssistant, enable_custom_integrations: None -) -> None: +async def test_light_state_off(hass: HomeAssistant) -> None: """Test rgbw color conversion in state updates.""" - platform = getattr(hass.components, "test.light") - platform.init(empty=True) + entities = [ + MockLight("Test_onoff", STATE_OFF), + MockLight("Test_brightness", STATE_OFF), + MockLight("Test_ct", STATE_OFF), + MockLight("Test_rgbw", STATE_OFF), + ] + setup_test_component_platform(hass, light.DOMAIN, entities) - platform.ENTITIES.append(platform.MockLight("Test_onoff", STATE_OFF)) - platform.ENTITIES.append(platform.MockLight("Test_brightness", STATE_OFF)) - platform.ENTITIES.append(platform.MockLight("Test_ct", STATE_OFF)) - platform.ENTITIES.append(platform.MockLight("Test_rgbw", STATE_OFF)) - - entity0 = platform.ENTITIES[0] + entity0 = entities[0] entity0.supported_color_modes = {light.ColorMode.ONOFF} - entity1 = platform.ENTITIES[1] + entity1 = entities[1] entity1.supported_color_modes = {light.ColorMode.BRIGHTNESS} - entity2 = platform.ENTITIES[2] + entity2 = entities[2] entity2.supported_color_modes = {light.ColorMode.COLOR_TEMP} - entity3 = platform.ENTITIES[3] + entity3 = entities[3] entity3.supported_color_modes = {light.ColorMode.RGBW} assert await async_setup_component(hass, "light", {"light": {"platform": "test"}}) @@ -1394,16 +1401,11 @@ async def test_light_state_off( } -async def test_light_state_rgbw( - hass: HomeAssistant, enable_custom_integrations: None -) -> None: +async def test_light_state_rgbw(hass: HomeAssistant) -> None: """Test rgbw color conversion in state updates.""" - platform = getattr(hass.components, "test.light") - platform.init(empty=True) + entity0 = MockLight("Test_rgbw", STATE_ON) + setup_test_component_platform(hass, light.DOMAIN, [entity0]) - platform.ENTITIES.append(platform.MockLight("Test_rgbw", STATE_ON)) - - entity0 = platform.ENTITIES[0] entity0.brightness = 255 entity0.supported_color_modes = {light.ColorMode.RGBW} entity0.color_mode = light.ColorMode.RGBW @@ -1430,16 +1432,11 @@ async def test_light_state_rgbw( } -async def test_light_state_rgbww( - hass: HomeAssistant, enable_custom_integrations: None -) -> None: +async def test_light_state_rgbww(hass: HomeAssistant) -> None: """Test rgbww color conversion in state updates.""" - platform = getattr(hass.components, "test.light") - platform.init(empty=True) + entity0 = MockLight("Test_rgbww", STATE_ON) + setup_test_component_platform(hass, light.DOMAIN, [entity0]) - platform.ENTITIES.append(platform.MockLight("Test_rgbww", STATE_ON)) - - entity0 = platform.ENTITIES[0] entity0.supported_color_modes = {light.ColorMode.RGBWW} entity0.color_mode = light.ColorMode.RGBWW entity0.hs_color = "Invalid" # Should be ignored @@ -1466,51 +1463,49 @@ async def test_light_state_rgbww( } -async def test_light_service_call_color_conversion( - hass: HomeAssistant, enable_custom_integrations: None -) -> None: +async def test_light_service_call_color_conversion(hass: HomeAssistant) -> None: """Test color conversion in service calls.""" - platform = getattr(hass.components, "test.light") - platform.init(empty=True) + entities = [ + MockLight("Test_hs", STATE_ON), + MockLight("Test_rgb", STATE_ON), + MockLight("Test_xy", STATE_ON), + MockLight("Test_all", STATE_ON), + MockLight("Test_legacy", STATE_ON), + MockLight("Test_rgbw", STATE_ON), + MockLight("Test_rgbww", STATE_ON), + MockLight("Test_temperature", STATE_ON), + ] + setup_test_component_platform(hass, light.DOMAIN, entities) - platform.ENTITIES.append(platform.MockLight("Test_hs", STATE_ON)) - platform.ENTITIES.append(platform.MockLight("Test_rgb", STATE_ON)) - platform.ENTITIES.append(platform.MockLight("Test_xy", STATE_ON)) - platform.ENTITIES.append(platform.MockLight("Test_all", STATE_ON)) - platform.ENTITIES.append(platform.MockLight("Test_legacy", STATE_ON)) - platform.ENTITIES.append(platform.MockLight("Test_rgbw", STATE_ON)) - platform.ENTITIES.append(platform.MockLight("Test_rgbww", STATE_ON)) - platform.ENTITIES.append(platform.MockLight("Test_temperature", STATE_ON)) - - entity0 = platform.ENTITIES[0] + entity0 = entities[0] entity0.supported_color_modes = {light.ColorMode.HS} - entity1 = platform.ENTITIES[1] + entity1 = entities[1] entity1.supported_color_modes = {light.ColorMode.RGB} - entity2 = platform.ENTITIES[2] + entity2 = entities[2] entity2.supported_color_modes = {light.ColorMode.XY} - entity3 = platform.ENTITIES[3] + entity3 = entities[3] entity3.supported_color_modes = { light.ColorMode.HS, light.ColorMode.RGB, light.ColorMode.XY, } - entity4 = platform.ENTITIES[4] + entity4 = entities[4] entity4.supported_features = light.SUPPORT_COLOR # Set color modes to none to trigger backwards compatibility in LightEntity entity4.supported_color_modes = None entity4.color_mode = None - entity5 = platform.ENTITIES[5] + entity5 = entities[5] entity5.supported_color_modes = {light.ColorMode.RGBW} - entity6 = platform.ENTITIES[6] + entity6 = entities[6] entity6.supported_color_modes = {light.ColorMode.RGBWW} - entity7 = platform.ENTITIES[7] + entity7 = entities[7] entity7.supported_color_modes = {light.ColorMode.COLOR_TEMP} assert await async_setup_component(hass, "light", {"light": {"platform": "test"}}) @@ -1612,7 +1607,7 @@ async def test_light_service_call_color_conversion( _, data = entity5.last_call("turn_on") assert data == {"brightness": 255, "rgbw_color": (0, 0, 0, 255)} _, data = entity6.last_call("turn_on") - # The midpoint of the the white channels is warm, compensated by adding green + blue + # The midpoint of the white channels is warm, compensated by adding green + blue assert data == {"brightness": 255, "rgbww_color": (0, 76, 141, 255, 255)} _, data = entity7.last_call("turn_on") assert data == {"brightness": 255, "color_temp_kelvin": 5962, "color_temp": 167} @@ -1685,7 +1680,7 @@ async def test_light_service_call_color_conversion( _, data = entity5.last_call("turn_on") assert data == {"brightness": 128, "rgbw_color": (0, 0, 0, 255)} _, data = entity6.last_call("turn_on") - # The midpoint the the white channels is warm, compensated by adding green + blue + # The midpoint the white channels is warm, compensated by adding green + blue assert data == {"brightness": 128, "rgbww_color": (0, 76, 141, 255, 255)} _, data = entity7.last_call("turn_on") assert data == {"brightness": 128, "color_temp_kelvin": 5962, "color_temp": 167} @@ -1758,7 +1753,7 @@ async def test_light_service_call_color_conversion( _, data = entity5.last_call("turn_on") assert data == {"brightness": 128, "rgbw_color": (1, 0, 0, 255)} _, data = entity6.last_call("turn_on") - # The midpoint the the white channels is warm, compensated by adding green + blue + # The midpoint the white channels is warm, compensated by adding green + blue assert data == {"brightness": 128, "rgbww_color": (0, 75, 140, 255, 255)} _, data = entity7.last_call("turn_on") assert data == {"brightness": 128, "color_temp_kelvin": 5962, "color_temp": 167} @@ -1795,7 +1790,7 @@ async def test_light_service_call_color_conversion( _, data = entity5.last_call("turn_on") assert data == {"brightness": 128, "rgbw_color": (128, 0, 0, 64)} _, data = entity6.last_call("turn_on") - # The midpoint the the white channels is warm, compensated by adding green + blue + # The midpoint the white channels is warm, compensated by adding green + blue assert data == {"brightness": 128, "rgbww_color": (128, 0, 30, 117, 117)} _, data = entity7.last_call("turn_on") assert data == {"brightness": 128, "color_temp_kelvin": 3011, "color_temp": 332} @@ -1832,7 +1827,7 @@ async def test_light_service_call_color_conversion( _, data = entity5.last_call("turn_on") assert data == {"brightness": 128, "rgbw_color": (255, 255, 255, 255)} _, data = entity6.last_call("turn_on") - # The midpoint the the white channels is warm, compensated by adding green + blue + # The midpoint the white channels is warm, compensated by adding green + blue assert data == {"brightness": 128, "rgbww_color": (0, 76, 141, 255, 255)} _, data = entity7.last_call("turn_on") assert data == {"brightness": 128, "color_temp_kelvin": 5962, "color_temp": 167} @@ -1903,7 +1898,7 @@ async def test_light_service_call_color_conversion( _, data = entity4.last_call("turn_on") assert data == {"brightness": 128, "hs_color": (27.429, 27.451)} _, data = entity5.last_call("turn_on") - # The midpoint the the white channels is warm, compensated by decreasing green + blue + # The midpoint the white channels is warm, compensated by decreasing green + blue assert data == {"brightness": 128, "rgbw_color": (96, 44, 0, 255)} _, data = entity6.last_call("turn_on") assert data == {"brightness": 128, "rgbww_color": (255, 255, 255, 255, 255)} @@ -1912,46 +1907,46 @@ async def test_light_service_call_color_conversion( async def test_light_service_call_color_conversion_named_tuple( - hass: HomeAssistant, enable_custom_integrations: None + hass: HomeAssistant, ) -> None: """Test a named tuple (RGBColor) is handled correctly.""" - platform = getattr(hass.components, "test.light") - platform.init(empty=True) + entities = [ + MockLight("Test_hs", STATE_ON), + MockLight("Test_rgb", STATE_ON), + MockLight("Test_xy", STATE_ON), + MockLight("Test_all", STATE_ON), + MockLight("Test_legacy", STATE_ON), + MockLight("Test_rgbw", STATE_ON), + MockLight("Test_rgbww", STATE_ON), + ] + setup_test_component_platform(hass, light.DOMAIN, entities) - platform.ENTITIES.append(platform.MockLight("Test_hs", STATE_ON)) - platform.ENTITIES.append(platform.MockLight("Test_rgb", STATE_ON)) - platform.ENTITIES.append(platform.MockLight("Test_xy", STATE_ON)) - platform.ENTITIES.append(platform.MockLight("Test_all", STATE_ON)) - platform.ENTITIES.append(platform.MockLight("Test_legacy", STATE_ON)) - platform.ENTITIES.append(platform.MockLight("Test_rgbw", STATE_ON)) - platform.ENTITIES.append(platform.MockLight("Test_rgbww", STATE_ON)) - - entity0 = platform.ENTITIES[0] + entity0 = entities[0] entity0.supported_color_modes = {light.ColorMode.HS} - entity1 = platform.ENTITIES[1] + entity1 = entities[1] entity1.supported_color_modes = {light.ColorMode.RGB} - entity2 = platform.ENTITIES[2] + entity2 = entities[2] entity2.supported_color_modes = {light.ColorMode.XY} - entity3 = platform.ENTITIES[3] + entity3 = entities[3] entity3.supported_color_modes = { light.ColorMode.HS, light.ColorMode.RGB, light.ColorMode.XY, } - entity4 = platform.ENTITIES[4] + entity4 = entities[4] entity4.supported_features = light.SUPPORT_COLOR # Set color modes to none to trigger backwards compatibility in LightEntity entity4.supported_color_modes = None entity4.color_mode = None - entity5 = platform.ENTITIES[5] + entity5 = entities[5] entity5.supported_color_modes = {light.ColorMode.RGBW} - entity6 = platform.ENTITIES[6] + entity6 = entities[6] entity6.supported_color_modes = {light.ColorMode.RGBWW} assert await async_setup_component(hass, "light", {"light": {"platform": "test"}}) @@ -1991,24 +1986,22 @@ async def test_light_service_call_color_conversion_named_tuple( assert data == {"brightness": 64, "rgbww_color": (128, 0, 0, 0, 0)} -async def test_light_service_call_color_temp_emulation( - hass: HomeAssistant, enable_custom_integrations: None -) -> None: +async def test_light_service_call_color_temp_emulation(hass: HomeAssistant) -> None: """Test color conversion in service calls.""" - platform = getattr(hass.components, "test.light") - platform.init(empty=True) + entities = [ + MockLight("Test_hs_ct", STATE_ON), + MockLight("Test_hs", STATE_ON), + MockLight("Test_hs_white", STATE_ON), + ] + setup_test_component_platform(hass, light.DOMAIN, entities) - platform.ENTITIES.append(platform.MockLight("Test_hs_ct", STATE_ON)) - platform.ENTITIES.append(platform.MockLight("Test_hs", STATE_ON)) - platform.ENTITIES.append(platform.MockLight("Test_hs_white", STATE_ON)) - - entity0 = platform.ENTITIES[0] + entity0 = entities[0] entity0.supported_color_modes = {light.ColorMode.COLOR_TEMP, light.ColorMode.HS} - entity1 = platform.ENTITIES[1] + entity1 = entities[1] entity1.supported_color_modes = {light.ColorMode.HS} - entity2 = platform.ENTITIES[2] + entity2 = entities[2] entity2.supported_color_modes = {light.ColorMode.HS, light.ColorMode.WHITE} assert await async_setup_component(hass, "light", {"light": {"platform": "test"}}) @@ -2051,23 +2044,21 @@ async def test_light_service_call_color_temp_emulation( assert data == {"brightness": 255, "hs_color": (27.001, 19.243)} -async def test_light_service_call_color_temp_conversion( - hass: HomeAssistant, enable_custom_integrations: None -) -> None: +async def test_light_service_call_color_temp_conversion(hass: HomeAssistant) -> None: """Test color temp conversion in service calls.""" - platform = getattr(hass.components, "test.light") - platform.init(empty=True) + entities = [ + MockLight("Test_rgbww_ct", STATE_ON), + MockLight("Test_rgbww", STATE_ON), + ] + setup_test_component_platform(hass, light.DOMAIN, entities) - platform.ENTITIES.append(platform.MockLight("Test_rgbww_ct", STATE_ON)) - platform.ENTITIES.append(platform.MockLight("Test_rgbww", STATE_ON)) - - entity0 = platform.ENTITIES[0] + entity0 = entities[0] entity0.supported_color_modes = { light.ColorMode.COLOR_TEMP, light.ColorMode.RGBWW, } - entity1 = platform.ENTITIES[1] + entity1 = entities[1] entity1.supported_color_modes = {light.ColorMode.RGBWW} assert entity1.min_mireds == 153 assert entity1.max_mireds == 500 @@ -2184,17 +2175,15 @@ async def test_light_service_call_color_temp_conversion( assert data == {"brightness": 255, "rgbww_color": (0, 0, 0, 66, 189)} -async def test_light_mired_color_temp_conversion( - hass: HomeAssistant, enable_custom_integrations: None -) -> None: +async def test_light_mired_color_temp_conversion(hass: HomeAssistant) -> None: """Test color temp conversion from K to legacy mired.""" - platform = getattr(hass.components, "test.light") - platform.init(empty=True) + entities = [ + MockLight("Test_rgbww_ct", STATE_ON), + MockLight("Test_rgbww", STATE_ON), + ] + setup_test_component_platform(hass, light.DOMAIN, entities) - platform.ENTITIES.append(platform.MockLight("Test_rgbww_ct", STATE_ON)) - platform.ENTITIES.append(platform.MockLight("Test_rgbww", STATE_ON)) - - entity0 = platform.ENTITIES[0] + entity0 = entities[0] entity0.supported_color_modes = { light.ColorMode.COLOR_TEMP, } @@ -2232,16 +2221,11 @@ async def test_light_mired_color_temp_conversion( assert state.attributes["color_temp_kelvin"] == 3500 -async def test_light_service_call_white_mode( - hass: HomeAssistant, enable_custom_integrations: None -) -> None: +async def test_light_service_call_white_mode(hass: HomeAssistant) -> None: """Test color_mode white in service calls.""" - platform = getattr(hass.components, "test.light") - platform.init(empty=True) - - platform.ENTITIES.append(platform.MockLight("Test_white", STATE_ON)) - entity0 = platform.ENTITIES[0] + entity0 = MockLight("Test_white", STATE_ON) entity0.supported_color_modes = {light.ColorMode.HS, light.ColorMode.WHITE} + setup_test_component_platform(hass, light.DOMAIN, [entity0]) assert await async_setup_component(hass, "light", {"light": {"platform": "test"}}) await hass.async_block_till_done() @@ -2336,40 +2320,38 @@ async def test_light_service_call_white_mode( assert data == {"white": 128} -async def test_light_state_color_conversion( - hass: HomeAssistant, enable_custom_integrations: None -) -> None: +async def test_light_state_color_conversion(hass: HomeAssistant) -> None: """Test color conversion in state updates.""" - platform = getattr(hass.components, "test.light") - platform.init(empty=True) + entities = [ + MockLight("Test_hs", STATE_ON), + MockLight("Test_rgb", STATE_ON), + MockLight("Test_xy", STATE_ON), + MockLight("Test_legacy", STATE_ON), + ] + setup_test_component_platform(hass, light.DOMAIN, entities) - platform.ENTITIES.append(platform.MockLight("Test_hs", STATE_ON)) - platform.ENTITIES.append(platform.MockLight("Test_rgb", STATE_ON)) - platform.ENTITIES.append(platform.MockLight("Test_xy", STATE_ON)) - platform.ENTITIES.append(platform.MockLight("Test_legacy", STATE_ON)) - - entity0 = platform.ENTITIES[0] + entity0 = entities[0] entity0.supported_color_modes = {light.ColorMode.HS} entity0.color_mode = light.ColorMode.HS entity0.hs_color = (240, 100) entity0.rgb_color = "Invalid" # Should be ignored entity0.xy_color = "Invalid" # Should be ignored - entity1 = platform.ENTITIES[1] + entity1 = entities[1] entity1.supported_color_modes = {light.ColorMode.RGB} entity1.color_mode = light.ColorMode.RGB entity1.hs_color = "Invalid" # Should be ignored entity1.rgb_color = (128, 0, 0) entity1.xy_color = "Invalid" # Should be ignored - entity2 = platform.ENTITIES[2] + entity2 = entities[2] entity2.supported_color_modes = {light.ColorMode.XY} entity2.color_mode = light.ColorMode.XY entity2.hs_color = "Invalid" # Should be ignored entity2.rgb_color = "Invalid" # Should be ignored entity2.xy_color = (0.1, 0.8) - entity3 = platform.ENTITIES[3] + entity3 = entities[3] entity3.hs_color = (240, 100) entity3.supported_features = light.SUPPORT_COLOR # Set color modes to none to trigger backwards compatibility in LightEntity @@ -2405,18 +2387,19 @@ async def test_light_state_color_conversion( async def test_services_filter_parameters( - hass: HomeAssistant, mock_light_profiles, enable_custom_integrations: None + hass: HomeAssistant, + mock_light_profiles, + mock_light_entities: list[MockLight], ) -> None: """Test turn_on and turn_off filters unsupported parameters.""" - platform = getattr(hass.components, "test.light") + setup_test_component_platform(hass, light.DOMAIN, mock_light_entities) - platform.init() assert await async_setup_component( hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) await hass.async_block_till_done() - ent1, _, _ = platform.ENTITIES + ent1, _, _ = mock_light_entities # turn off the light by setting brightness to 0, this should work even if the light # doesn't support brightness @@ -2791,7 +2774,7 @@ def test_report_invalid_color_mode( ( light.ColorMode.ONOFF, {light.ColorMode.ONOFF, light.ColorMode.BRIGHTNESS}, - "tuya", # We don't log issues for tuya + "philips_js", # We don't log issues for philips_js False, ), ], diff --git a/tests/components/light/test_intent.py b/tests/components/light/test_intent.py index 4fccc298192..b21b9367bba 100644 --- a/tests/components/light/test_intent.py +++ b/tests/components/light/test_intent.py @@ -1,4 +1,5 @@ """Tests for the light intents.""" + from homeassistant.components import light from homeassistant.components.light import ATTR_SUPPORTED_COLOR_MODES, ColorMode, intent from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_ON diff --git a/tests/components/light/test_recorder.py b/tests/components/light/test_recorder.py index 1376ee53649..49c9a567856 100644 --- a/tests/components/light/test_recorder.py +++ b/tests/components/light/test_recorder.py @@ -1,4 +1,5 @@ """The tests for light recorder.""" + from __future__ import annotations from datetime import timedelta diff --git a/tests/components/light/test_reproduce_state.py b/tests/components/light/test_reproduce_state.py index 65b83aa0269..aa698129915 100644 --- a/tests/components/light/test_reproduce_state.py +++ b/tests/components/light/test_reproduce_state.py @@ -1,4 +1,5 @@ """Test reproduce state for Light.""" + import pytest from homeassistant.components import light @@ -126,7 +127,7 @@ async def test_reproducing_states( @pytest.mark.parametrize( "color_mode", - ( + [ light.ColorMode.COLOR_TEMP, light.ColorMode.BRIGHTNESS, light.ColorMode.HS, @@ -137,7 +138,7 @@ async def test_reproducing_states( light.ColorMode.UNKNOWN, light.ColorMode.WHITE, light.ColorMode.XY, - ), + ], ) async def test_filter_color_modes( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, color_mode @@ -194,7 +195,7 @@ async def test_filter_color_modes( @pytest.mark.parametrize( "saved_state", - ( + [ NONE_BRIGHTNESS, NONE_EFFECT, NONE_COLOR_TEMP, @@ -203,7 +204,7 @@ async def test_filter_color_modes( NONE_RGBW_COLOR, NONE_RGBWW_COLOR, NONE_XY_COLOR, - ), + ], ) async def test_filter_none(hass: HomeAssistant, saved_state) -> None: """Test filtering of parameters which are None.""" diff --git a/tests/components/light/test_significant_change.py b/tests/components/light/test_significant_change.py index 6bececc0244..87a60b58325 100644 --- a/tests/components/light/test_significant_change.py +++ b/tests/components/light/test_significant_change.py @@ -1,4 +1,5 @@ """Test the Light significant change platform.""" + from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, diff --git a/tests/components/linear_garage_door/test_config_flow.py b/tests/components/linear_garage_door/test_config_flow.py index 5d1ed36ecb7..1851b61fc15 100644 --- a/tests/components/linear_garage_door/test_config_flow.py +++ b/tests/components/linear_garage_door/test_config_flow.py @@ -20,18 +20,23 @@ async def test_form(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.FORM assert result["errors"] is None - with patch( - "homeassistant.components.linear_garage_door.config_flow.Linear.login", - return_value=True, - ), patch( - "homeassistant.components.linear_garage_door.config_flow.Linear.get_sites", - return_value=[{"id": "test-site-id", "name": "test-site-name"}], - ), patch( - "homeassistant.components.linear_garage_door.config_flow.Linear.close", - return_value=None, - ), patch( - "uuid.uuid4", - return_value="test-uuid", + with ( + patch( + "homeassistant.components.linear_garage_door.config_flow.Linear.login", + return_value=True, + ), + patch( + "homeassistant.components.linear_garage_door.config_flow.Linear.get_sites", + return_value=[{"id": "test-site-id", "name": "test-site-name"}], + ), + patch( + "homeassistant.components.linear_garage_door.config_flow.Linear.close", + return_value=None, + ), + patch( + "uuid.uuid4", + return_value="test-uuid", + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -84,18 +89,23 @@ async def test_reauth(hass: HomeAssistant) -> None: assert result1["type"] == FlowResultType.FORM assert result1["step_id"] == "user" - with patch( - "homeassistant.components.linear_garage_door.config_flow.Linear.login", - return_value=True, - ), patch( - "homeassistant.components.linear_garage_door.config_flow.Linear.get_sites", - return_value=[{"id": "test-site-id", "name": "test-site-name"}], - ), patch( - "homeassistant.components.linear_garage_door.config_flow.Linear.close", - return_value=None, - ), patch( - "uuid.uuid4", - return_value="test-uuid", + with ( + patch( + "homeassistant.components.linear_garage_door.config_flow.Linear.login", + return_value=True, + ), + patch( + "homeassistant.components.linear_garage_door.config_flow.Linear.get_sites", + return_value=[{"id": "test-site-id", "name": "test-site-name"}], + ), + patch( + "homeassistant.components.linear_garage_door.config_flow.Linear.close", + return_value=None, + ), + patch( + "uuid.uuid4", + return_value="test-uuid", + ), ): result2 = await hass.config_entries.flow.async_configure( result1["flow_id"], @@ -125,12 +135,15 @@ async def test_form_invalid_login(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch( - "homeassistant.components.linear_garage_door.config_flow.Linear.login", - side_effect=InvalidLoginError, - ), patch( - "homeassistant.components.linear_garage_door.config_flow.Linear.close", - return_value=None, + with ( + patch( + "homeassistant.components.linear_garage_door.config_flow.Linear.login", + side_effect=InvalidLoginError, + ), + patch( + "homeassistant.components.linear_garage_door.config_flow.Linear.close", + return_value=None, + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], diff --git a/tests/components/linear_garage_door/test_cover.py b/tests/components/linear_garage_door/test_cover.py index 428411d39e0..e692d1867dc 100644 --- a/tests/components/linear_garage_door/test_cover.py +++ b/tests/components/linear_garage_door/test_cover.py @@ -56,15 +56,19 @@ async def test_open_cover(hass: HomeAssistant) -> None: assert operate_device.call_count == 0 - with patch( - "homeassistant.components.linear_garage_door.cover.Linear.login", - return_value=True, - ), patch( - "homeassistant.components.linear_garage_door.cover.Linear.operate_device", - return_value=None, - ) as operate_device, patch( - "homeassistant.components.linear_garage_door.cover.Linear.close", - return_value=True, + with ( + patch( + "homeassistant.components.linear_garage_door.cover.Linear.login", + return_value=True, + ), + patch( + "homeassistant.components.linear_garage_door.cover.Linear.operate_device", + return_value=None, + ) as operate_device, + patch( + "homeassistant.components.linear_garage_door.cover.Linear.close", + return_value=True, + ), ): await hass.services.async_call( COVER_DOMAIN, @@ -74,38 +78,51 @@ async def test_open_cover(hass: HomeAssistant) -> None: ) assert operate_device.call_count == 1 - with patch( - "homeassistant.components.linear_garage_door.cover.Linear.login", - return_value=True, - ), patch( - "homeassistant.components.linear_garage_door.cover.Linear.get_devices", - return_value=[ - {"id": "test1", "name": "Test Garage 1", "subdevices": ["GDO", "Light"]}, - {"id": "test2", "name": "Test Garage 2", "subdevices": ["GDO", "Light"]}, - ], - ), patch( - "homeassistant.components.linear_garage_door.cover.Linear.get_device_state", - side_effect=lambda id: { - "test1": { - "GDO": {"Open_B": "true", "Open_P": "100"}, - "Light": {"On_B": "true", "On_P": "100"}, - }, - "test2": { - "GDO": {"Open_B": "false", "Opening_P": "0"}, - "Light": {"On_B": "false", "On_P": "0"}, - }, - "test3": { - "GDO": {"Open_B": "false", "Opening_P": "0"}, - "Light": {"On_B": "false", "On_P": "0"}, - }, - "test4": { - "GDO": {"Open_B": "true", "Opening_P": "100"}, - "Light": {"On_B": "true", "On_P": "100"}, - }, - }[id], - ), patch( - "homeassistant.components.linear_garage_door.cover.Linear.close", - return_value=True, + with ( + patch( + "homeassistant.components.linear_garage_door.cover.Linear.login", + return_value=True, + ), + patch( + "homeassistant.components.linear_garage_door.cover.Linear.get_devices", + return_value=[ + { + "id": "test1", + "name": "Test Garage 1", + "subdevices": ["GDO", "Light"], + }, + { + "id": "test2", + "name": "Test Garage 2", + "subdevices": ["GDO", "Light"], + }, + ], + ), + patch( + "homeassistant.components.linear_garage_door.cover.Linear.get_device_state", + side_effect=lambda id: { + "test1": { + "GDO": {"Open_B": "true", "Open_P": "100"}, + "Light": {"On_B": "true", "On_P": "100"}, + }, + "test2": { + "GDO": {"Open_B": "false", "Opening_P": "0"}, + "Light": {"On_B": "false", "On_P": "0"}, + }, + "test3": { + "GDO": {"Open_B": "false", "Opening_P": "0"}, + "Light": {"On_B": "false", "On_P": "0"}, + }, + "test4": { + "GDO": {"Open_B": "true", "Opening_P": "100"}, + "Light": {"On_B": "true", "On_P": "100"}, + }, + }[id], + ), + patch( + "homeassistant.components.linear_garage_door.cover.Linear.close", + return_value=True, + ), ): async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=60)) await hass.async_block_till_done() @@ -130,15 +147,19 @@ async def test_close_cover(hass: HomeAssistant) -> None: assert operate_device.call_count == 0 - with patch( - "homeassistant.components.linear_garage_door.cover.Linear.login", - return_value=True, - ), patch( - "homeassistant.components.linear_garage_door.cover.Linear.operate_device", - return_value=None, - ) as operate_device, patch( - "homeassistant.components.linear_garage_door.cover.Linear.close", - return_value=True, + with ( + patch( + "homeassistant.components.linear_garage_door.cover.Linear.login", + return_value=True, + ), + patch( + "homeassistant.components.linear_garage_door.cover.Linear.operate_device", + return_value=None, + ) as operate_device, + patch( + "homeassistant.components.linear_garage_door.cover.Linear.close", + return_value=True, + ), ): await hass.services.async_call( COVER_DOMAIN, @@ -148,38 +169,51 @@ async def test_close_cover(hass: HomeAssistant) -> None: ) assert operate_device.call_count == 1 - with patch( - "homeassistant.components.linear_garage_door.cover.Linear.login", - return_value=True, - ), patch( - "homeassistant.components.linear_garage_door.cover.Linear.get_devices", - return_value=[ - {"id": "test1", "name": "Test Garage 1", "subdevices": ["GDO", "Light"]}, - {"id": "test2", "name": "Test Garage 2", "subdevices": ["GDO", "Light"]}, - ], - ), patch( - "homeassistant.components.linear_garage_door.cover.Linear.get_device_state", - side_effect=lambda id: { - "test1": { - "GDO": {"Open_B": "true", "Opening_P": "100"}, - "Light": {"On_B": "true", "On_P": "100"}, - }, - "test2": { - "GDO": {"Open_B": "false", "Open_P": "0"}, - "Light": {"On_B": "false", "On_P": "0"}, - }, - "test3": { - "GDO": {"Open_B": "false", "Opening_P": "0"}, - "Light": {"On_B": "false", "On_P": "0"}, - }, - "test4": { - "GDO": {"Open_B": "true", "Opening_P": "100"}, - "Light": {"On_B": "true", "On_P": "100"}, - }, - }[id], - ), patch( - "homeassistant.components.linear_garage_door.cover.Linear.close", - return_value=True, + with ( + patch( + "homeassistant.components.linear_garage_door.cover.Linear.login", + return_value=True, + ), + patch( + "homeassistant.components.linear_garage_door.cover.Linear.get_devices", + return_value=[ + { + "id": "test1", + "name": "Test Garage 1", + "subdevices": ["GDO", "Light"], + }, + { + "id": "test2", + "name": "Test Garage 2", + "subdevices": ["GDO", "Light"], + }, + ], + ), + patch( + "homeassistant.components.linear_garage_door.cover.Linear.get_device_state", + side_effect=lambda id: { + "test1": { + "GDO": {"Open_B": "true", "Opening_P": "100"}, + "Light": {"On_B": "true", "On_P": "100"}, + }, + "test2": { + "GDO": {"Open_B": "false", "Open_P": "0"}, + "Light": {"On_B": "false", "On_P": "0"}, + }, + "test3": { + "GDO": {"Open_B": "false", "Opening_P": "0"}, + "Light": {"On_B": "false", "On_P": "0"}, + }, + "test4": { + "GDO": {"Open_B": "true", "Opening_P": "100"}, + "Light": {"On_B": "true", "On_P": "100"}, + }, + }[id], + ), + patch( + "homeassistant.components.linear_garage_door.cover.Linear.close", + return_value=True, + ), ): async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=60)) await hass.async_block_till_done() diff --git a/tests/components/linear_garage_door/test_init.py b/tests/components/linear_garage_door/test_init.py index e8d76770050..32ebda7e125 100644 --- a/tests/components/linear_garage_door/test_init.py +++ b/tests/components/linear_garage_door/test_init.py @@ -22,23 +22,28 @@ async def test_unload_entry(hass: HomeAssistant) -> None: ) config_entry.add_to_hass(hass) - with patch( - "homeassistant.components.linear_garage_door.coordinator.Linear.login", - return_value=True, - ), patch( - "homeassistant.components.linear_garage_door.coordinator.Linear.get_devices", - return_value=[ - {"id": "test", "name": "Test Garage", "subdevices": ["GDO", "Light"]} - ], - ), patch( - "homeassistant.components.linear_garage_door.coordinator.Linear.get_device_state", - return_value={ - "GDO": {"Open_B": "true", "Open_P": "100"}, - "Light": {"On_B": "true", "On_P": "10"}, - }, - ), patch( - "homeassistant.components.linear_garage_door.coordinator.Linear.close", - return_value=True, + with ( + patch( + "homeassistant.components.linear_garage_door.coordinator.Linear.login", + return_value=True, + ), + patch( + "homeassistant.components.linear_garage_door.coordinator.Linear.get_devices", + return_value=[ + {"id": "test", "name": "Test Garage", "subdevices": ["GDO", "Light"]} + ], + ), + patch( + "homeassistant.components.linear_garage_door.coordinator.Linear.get_device_state", + return_value={ + "GDO": {"Open_B": "true", "Open_P": "100"}, + "Light": {"On_B": "true", "On_P": "10"}, + }, + ), + patch( + "homeassistant.components.linear_garage_door.coordinator.Linear.close", + return_value=True, + ), ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/linear_garage_door/util.py b/tests/components/linear_garage_door/util.py index d8348b9bb64..1a849ae2348 100644 --- a/tests/components/linear_garage_door/util.py +++ b/tests/components/linear_garage_door/util.py @@ -21,40 +21,61 @@ async def async_init_integration(hass: HomeAssistant) -> MockConfigEntry: ) config_entry.add_to_hass(hass) - with patch( - "homeassistant.components.linear_garage_door.coordinator.Linear.login", - return_value=True, - ), patch( - "homeassistant.components.linear_garage_door.coordinator.Linear.get_devices", - return_value=[ - {"id": "test1", "name": "Test Garage 1", "subdevices": ["GDO", "Light"]}, - {"id": "test2", "name": "Test Garage 2", "subdevices": ["GDO", "Light"]}, - {"id": "test3", "name": "Test Garage 3", "subdevices": ["GDO", "Light"]}, - {"id": "test4", "name": "Test Garage 4", "subdevices": ["GDO", "Light"]}, - ], - ), patch( - "homeassistant.components.linear_garage_door.coordinator.Linear.get_device_state", - side_effect=lambda id: { - "test1": { - "GDO": {"Open_B": "true", "Open_P": "100"}, - "Light": {"On_B": "true", "On_P": "100"}, - }, - "test2": { - "GDO": {"Open_B": "false", "Open_P": "0"}, - "Light": {"On_B": "false", "On_P": "0"}, - }, - "test3": { - "GDO": {"Open_B": "false", "Opening_P": "0"}, - "Light": {"On_B": "false", "On_P": "0"}, - }, - "test4": { - "GDO": {"Open_B": "true", "Opening_P": "100"}, - "Light": {"On_B": "true", "On_P": "100"}, - }, - }[id], - ), patch( - "homeassistant.components.linear_garage_door.coordinator.Linear.close", - return_value=True, + with ( + patch( + "homeassistant.components.linear_garage_door.coordinator.Linear.login", + return_value=True, + ), + patch( + "homeassistant.components.linear_garage_door.coordinator.Linear.get_devices", + return_value=[ + { + "id": "test1", + "name": "Test Garage 1", + "subdevices": ["GDO", "Light"], + }, + { + "id": "test2", + "name": "Test Garage 2", + "subdevices": ["GDO", "Light"], + }, + { + "id": "test3", + "name": "Test Garage 3", + "subdevices": ["GDO", "Light"], + }, + { + "id": "test4", + "name": "Test Garage 4", + "subdevices": ["GDO", "Light"], + }, + ], + ), + patch( + "homeassistant.components.linear_garage_door.coordinator.Linear.get_device_state", + side_effect=lambda id: { + "test1": { + "GDO": {"Open_B": "true", "Open_P": "100"}, + "Light": {"On_B": "true", "On_P": "100"}, + }, + "test2": { + "GDO": {"Open_B": "false", "Open_P": "0"}, + "Light": {"On_B": "false", "On_P": "0"}, + }, + "test3": { + "GDO": {"Open_B": "false", "Opening_P": "0"}, + "Light": {"On_B": "false", "On_P": "0"}, + }, + "test4": { + "GDO": {"Open_B": "true", "Opening_P": "100"}, + "Light": {"On_B": "true", "On_P": "100"}, + }, + }[id], + ), + patch( + "homeassistant.components.linear_garage_door.coordinator.Linear.close", + return_value=True, + ), ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/litejet/__init__.py b/tests/components/litejet/__init__.py index 1ba39e46bf3..3116d9e810d 100644 --- a/tests/components/litejet/__init__.py +++ b/tests/components/litejet/__init__.py @@ -1,4 +1,5 @@ """Tests for the litejet component.""" + from homeassistant.components import scene, switch from homeassistant.components.litejet import DOMAIN from homeassistant.const import CONF_PORT diff --git a/tests/components/litejet/conftest.py b/tests/components/litejet/conftest.py index 2c631265c30..41517acf1e9 100644 --- a/tests/components/litejet/conftest.py +++ b/tests/components/litejet/conftest.py @@ -1,4 +1,5 @@ """Fixtures for LiteJet testing.""" + from datetime import timedelta from unittest.mock import AsyncMock, Mock, patch diff --git a/tests/components/litejet/test_config_flow.py b/tests/components/litejet/test_config_flow.py index b490643f622..b92aa59c9ce 100644 --- a/tests/components/litejet/test_config_flow.py +++ b/tests/components/litejet/test_config_flow.py @@ -1,4 +1,5 @@ """The tests for the litejet component.""" + from unittest.mock import patch from serial import SerialException diff --git a/tests/components/litejet/test_diagnostics.py b/tests/components/litejet/test_diagnostics.py index a2c8bc72476..b20d19e8f16 100644 --- a/tests/components/litejet/test_diagnostics.py +++ b/tests/components/litejet/test_diagnostics.py @@ -1,4 +1,5 @@ """The tests for the litejet component.""" + from homeassistant.core import HomeAssistant from . import async_init_integration diff --git a/tests/components/litejet/test_init.py b/tests/components/litejet/test_init.py index c6f0d5c5b02..d1be342d771 100644 --- a/tests/components/litejet/test_init.py +++ b/tests/components/litejet/test_init.py @@ -1,4 +1,5 @@ """The tests for the litejet component.""" + from homeassistant.components import litejet from homeassistant.components.litejet.const import DOMAIN from homeassistant.core import HomeAssistant diff --git a/tests/components/litejet/test_light.py b/tests/components/litejet/test_light.py index 32f121a88a3..bde75bd1b32 100644 --- a/tests/components/litejet/test_light.py +++ b/tests/components/litejet/test_light.py @@ -1,4 +1,5 @@ """The tests for the litejet component.""" + from homeassistant.components import light from homeassistant.components.light import ATTR_BRIGHTNESS, ATTR_TRANSITION from homeassistant.components.litejet.const import CONF_DEFAULT_TRANSITION diff --git a/tests/components/litejet/test_scene.py b/tests/components/litejet/test_scene.py index 76c1556f66d..16475e2fe31 100644 --- a/tests/components/litejet/test_scene.py +++ b/tests/components/litejet/test_scene.py @@ -1,4 +1,5 @@ """The tests for the litejet component.""" + from homeassistant.components import scene from homeassistant.const import ( ATTR_ENTITY_ID, diff --git a/tests/components/litejet/test_switch.py b/tests/components/litejet/test_switch.py index 472ccc9491a..59b2ecf66e6 100644 --- a/tests/components/litejet/test_switch.py +++ b/tests/components/litejet/test_switch.py @@ -1,4 +1,5 @@ """The tests for the litejet component.""" + from homeassistant.components import switch from homeassistant.const import ( ATTR_ENTITY_ID, diff --git a/tests/components/litejet/test_trigger.py b/tests/components/litejet/test_trigger.py index e3d7caad65e..b9379efdad4 100644 --- a/tests/components/litejet/test_trigger.py +++ b/tests/components/litejet/test_trigger.py @@ -1,4 +1,5 @@ """The tests for the litejet component.""" + from datetime import timedelta import logging from unittest import mock diff --git a/tests/components/litterrobot/common.py b/tests/components/litterrobot/common.py index 5bf6fb7cce6..fe6202edc47 100644 --- a/tests/components/litterrobot/common.py +++ b/tests/components/litterrobot/common.py @@ -1,4 +1,5 @@ """Common utils for Litter-Robot tests.""" + from homeassistant.components.litterrobot import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME diff --git a/tests/components/litterrobot/conftest.py b/tests/components/litterrobot/conftest.py index ce80471797d..181e4fc1a90 100644 --- a/tests/components/litterrobot/conftest.py +++ b/tests/components/litterrobot/conftest.py @@ -1,4 +1,5 @@ """Configure pytest for Litter-Robot tests.""" + from __future__ import annotations from typing import Any @@ -123,11 +124,15 @@ async def setup_integration( ) entry.add_to_hass(hass) - with patch( - "homeassistant.components.litterrobot.hub.Account", return_value=mock_account - ), patch( - "homeassistant.components.litterrobot.PLATFORMS_BY_TYPE", - {Robot: (platform_domain,)} if platform_domain else {}, + with ( + patch( + "homeassistant.components.litterrobot.hub.Account", + return_value=mock_account, + ), + patch( + "homeassistant.components.litterrobot.PLATFORMS_BY_TYPE", + {Robot: (platform_domain,)} if platform_domain else {}, + ), ): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/litterrobot/test_binary_sensor.py b/tests/components/litterrobot/test_binary_sensor.py index c6cfbff907e..c72f747db88 100644 --- a/tests/components/litterrobot/test_binary_sensor.py +++ b/tests/components/litterrobot/test_binary_sensor.py @@ -1,4 +1,5 @@ """Test the Litter-Robot binary sensor entity.""" + from unittest.mock import MagicMock import pytest diff --git a/tests/components/litterrobot/test_button.py b/tests/components/litterrobot/test_button.py index 9a4145dd224..e9ef65c01a4 100644 --- a/tests/components/litterrobot/test_button.py +++ b/tests/components/litterrobot/test_button.py @@ -1,10 +1,11 @@ """Test the Litter-Robot button entity.""" + from unittest.mock import MagicMock from freezegun import freeze_time from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS -from homeassistant.const import ATTR_ENTITY_ID, ATTR_ICON, STATE_UNKNOWN, EntityCategory +from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN, EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -21,7 +22,6 @@ async def test_button( state = hass.states.get(BUTTON_ENTITY) assert state - assert state.attributes.get(ATTR_ICON) == "mdi:delete-variant" assert state.state == STATE_UNKNOWN entry = entity_registry.async_get(BUTTON_ENTITY) diff --git a/tests/components/litterrobot/test_config_flow.py b/tests/components/litterrobot/test_config_flow.py index fcfb373a7b0..d516a3f14a2 100644 --- a/tests/components/litterrobot/test_config_flow.py +++ b/tests/components/litterrobot/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Litter-Robot config flow.""" + from unittest.mock import patch from pylitterbot import Account @@ -24,13 +25,16 @@ async def test_form(hass: HomeAssistant, mock_account) -> None: assert result["type"] == "form" assert result["errors"] == {} - with patch( - "homeassistant.components.litterrobot.config_flow.Account.connect", - return_value=mock_account, - ), patch( - "homeassistant.components.litterrobot.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.litterrobot.config_flow.Account.connect", + return_value=mock_account, + ), + patch( + "homeassistant.components.litterrobot.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], CONFIG[DOMAIN] ) @@ -133,13 +137,16 @@ async def test_step_reauth(hass: HomeAssistant, mock_account: Account) -> None: assert result["type"] == FlowResultType.FORM assert result["step_id"] == "reauth_confirm" - with patch( - "homeassistant.components.litterrobot.config_flow.Account.connect", - return_value=mock_account, - ), patch( - "homeassistant.components.litterrobot.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.litterrobot.config_flow.Account.connect", + return_value=mock_account, + ), + patch( + "homeassistant.components.litterrobot.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_PASSWORD: CONFIG[litterrobot.DOMAIN][CONF_PASSWORD]}, @@ -182,13 +189,16 @@ async def test_step_reauth_failed(hass: HomeAssistant, mock_account: Account) -> assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "invalid_auth"} - with patch( - "homeassistant.components.litterrobot.config_flow.Account.connect", - return_value=mock_account, - ), patch( - "homeassistant.components.litterrobot.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.litterrobot.config_flow.Account.connect", + return_value=mock_account, + ), + patch( + "homeassistant.components.litterrobot.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_PASSWORD: CONFIG[litterrobot.DOMAIN][CONF_PASSWORD]}, diff --git a/tests/components/litterrobot/test_init.py b/tests/components/litterrobot/test_init.py index 25c47ee4945..60f359f08f0 100644 --- a/tests/components/litterrobot/test_init.py +++ b/tests/components/litterrobot/test_init.py @@ -1,4 +1,5 @@ """Test Litter-Robot setup process.""" + from unittest.mock import MagicMock, patch from pylitterbot.exceptions import LitterRobotException, LitterRobotLoginException @@ -46,10 +47,10 @@ async def test_unload_entry(hass: HomeAssistant, mock_account: MagicMock) -> Non @pytest.mark.parametrize( ("side_effect", "expected_state"), - ( + [ (LitterRobotLoginException, ConfigEntryState.SETUP_ERROR), (LitterRobotException, ConfigEntryState.SETUP_RETRY), - ), + ], ) async def test_entry_not_setup( hass: HomeAssistant, diff --git a/tests/components/litterrobot/test_select.py b/tests/components/litterrobot/test_select.py index b35fdf5c917..48ec1bb06a5 100644 --- a/tests/components/litterrobot/test_select.py +++ b/tests/components/litterrobot/test_select.py @@ -1,4 +1,5 @@ """Test the Litter-Robot select entity.""" + from unittest.mock import AsyncMock, MagicMock from pylitterbot import LitterRobot3, LitterRobot4 @@ -36,9 +37,7 @@ async def test_wait_time_select( data = {ATTR_ENTITY_ID: SELECT_ENTITY_ID} - count = 0 - for wait_time in LitterRobot3.VALID_WAIT_TIMES: - count += 1 + for count, wait_time in enumerate(LitterRobot3.VALID_WAIT_TIMES): data[ATTR_OPTION] = wait_time await hass.services.async_call( @@ -48,7 +47,7 @@ async def test_wait_time_select( blocking=True, ) - assert mock_account.robots[0].set_wait_time.call_count == count + assert mock_account.robots[0].set_wait_time.call_count == count + 1 async def test_invalid_wait_time_select(hass: HomeAssistant, mock_account) -> None: @@ -90,9 +89,8 @@ async def test_panel_brightness_select( robot: LitterRobot4 = mock_account_with_litterrobot_4.robots[0] robot.set_panel_brightness = AsyncMock(return_value=True) - count = 0 - for option in select.attributes[ATTR_OPTIONS]: - count += 1 + + for count, option in enumerate(select.attributes[ATTR_OPTIONS]): data[ATTR_OPTION] = option await hass.services.async_call( @@ -102,4 +100,4 @@ async def test_panel_brightness_select( blocking=True, ) - assert robot.set_panel_brightness.call_count == count + assert robot.set_panel_brightness.call_count == count + 1 diff --git a/tests/components/litterrobot/test_sensor.py b/tests/components/litterrobot/test_sensor.py index adb44d59bff..9002894d0ab 100644 --- a/tests/components/litterrobot/test_sensor.py +++ b/tests/components/litterrobot/test_sensor.py @@ -1,4 +1,5 @@ """Test the Litter-Robot sensor entity.""" + from unittest.mock import MagicMock import pytest diff --git a/tests/components/litterrobot/test_switch.py b/tests/components/litterrobot/test_switch.py index eee06101cf3..d81c02bee49 100644 --- a/tests/components/litterrobot/test_switch.py +++ b/tests/components/litterrobot/test_switch.py @@ -1,4 +1,5 @@ """Test the Litter-Robot switch entity.""" + from unittest.mock import MagicMock from pylitterbot import Robot @@ -57,13 +58,11 @@ async def test_on_off_commands( data = {ATTR_ENTITY_ID: entity_id} - count = 0 services = ((SERVICE_TURN_ON, STATE_ON, "1"), (SERVICE_TURN_OFF, STATE_OFF, "0")) - for service, new_state, new_value in services: - count += 1 + for count, (service, new_state, new_value) in enumerate(services): await hass.services.async_call(PLATFORM_DOMAIN, service, data, blocking=True) robot._update_data({updated_field: new_value}, partial=True) - assert getattr(robot, robot_command).call_count == count + assert getattr(robot, robot_command).call_count == count + 1 assert (state := hass.states.get(entity_id)) assert state.state == new_state diff --git a/tests/components/litterrobot/test_time.py b/tests/components/litterrobot/test_time.py index 53f254008e7..f77263d9493 100644 --- a/tests/components/litterrobot/test_time.py +++ b/tests/components/litterrobot/test_time.py @@ -1,4 +1,5 @@ """Test the Litter-Robot time entity.""" + from __future__ import annotations from datetime import datetime, time diff --git a/tests/components/litterrobot/test_update.py b/tests/components/litterrobot/test_update.py index 259b8ad09fe..b1b092e1f02 100644 --- a/tests/components/litterrobot/test_update.py +++ b/tests/components/litterrobot/test_update.py @@ -1,4 +1,5 @@ """Test the Litter-Robot update entity.""" + from unittest.mock import AsyncMock, MagicMock from pylitterbot import LitterRobot4 diff --git a/tests/components/litterrobot/test_vacuum.py b/tests/components/litterrobot/test_vacuum.py index c2df2bc5095..9013d6e83eb 100644 --- a/tests/components/litterrobot/test_vacuum.py +++ b/tests/components/litterrobot/test_vacuum.py @@ -1,4 +1,5 @@ """Test the Litter-Robot vacuum entity.""" + from __future__ import annotations from typing import Any diff --git a/tests/components/livisi/__init__.py b/tests/components/livisi/__init__.py index 48a7e21ad8d..c8700aeebc6 100644 --- a/tests/components/livisi/__init__.py +++ b/tests/components/livisi/__init__.py @@ -1,4 +1,5 @@ """Tests for the LIVISI Smart Home integration.""" + from unittest.mock import patch from homeassistant.const import CONF_HOST, CONF_PASSWORD diff --git a/tests/components/livisi/test_config_flow.py b/tests/components/livisi/test_config_flow.py index 8f13502bbc3..7f4f8568030 100644 --- a/tests/components/livisi/test_config_flow.py +++ b/tests/components/livisi/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Livisi Home Assistant config flow.""" + from unittest.mock import patch from aiolivisi import errors as livisi_errors diff --git a/tests/components/local_calendar/test_config_flow.py b/tests/components/local_calendar/test_config_flow.py index 6cebd42cf30..89ea9d21ff5 100644 --- a/tests/components/local_calendar/test_config_flow.py +++ b/tests/components/local_calendar/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Local Calendar config flow.""" + from unittest.mock import patch from homeassistant import config_entries diff --git a/tests/components/local_calendar/test_diagnostics.py b/tests/components/local_calendar/test_diagnostics.py index 9a1da25d770..721eed19736 100644 --- a/tests/components/local_calendar/test_diagnostics.py +++ b/tests/components/local_calendar/test_diagnostics.py @@ -1,4 +1,5 @@ """Tests for diagnostics platform of local calendar.""" + from aiohttp.test_utils import TestClient from freezegun import freeze_time import pytest diff --git a/tests/components/local_file/test_camera.py b/tests/components/local_file/test_camera.py index d47c5fa69fa..4455d47469c 100644 --- a/tests/components/local_file/test_camera.py +++ b/tests/components/local_file/test_camera.py @@ -1,4 +1,5 @@ """The tests for local file camera component.""" + from http import HTTPStatus from unittest import mock @@ -15,11 +16,13 @@ async def test_loading_file( hass: HomeAssistant, hass_client: ClientSessionGenerator ) -> None: """Test that it loads image from disk.""" - with mock.patch("os.path.isfile", mock.Mock(return_value=True)), mock.patch( - "os.access", mock.Mock(return_value=True) - ), mock.patch( - "homeassistant.components.local_file.camera.mimetypes.guess_type", - mock.Mock(return_value=(None, None)), + with ( + mock.patch("os.path.isfile", mock.Mock(return_value=True)), + mock.patch("os.access", mock.Mock(return_value=True)), + mock.patch( + "homeassistant.components.local_file.camera.mimetypes.guess_type", + mock.Mock(return_value=(None, None)), + ), ): await async_setup_component( hass, @@ -51,8 +54,9 @@ async def test_file_not_readable( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test a warning is shown setup when file is not readable.""" - with mock.patch("os.path.isfile", mock.Mock(return_value=True)), mock.patch( - "os.access", mock.Mock(return_value=False) + with ( + mock.patch("os.path.isfile", mock.Mock(return_value=True)), + mock.patch("os.access", mock.Mock(return_value=False)), ): await async_setup_component( hass, @@ -141,11 +145,13 @@ async def test_camera_content_type( async def test_update_file_path(hass: HomeAssistant) -> None: """Test update_file_path service.""" # Setup platform - with mock.patch("os.path.isfile", mock.Mock(return_value=True)), mock.patch( - "os.access", mock.Mock(return_value=True) - ), mock.patch( - "homeassistant.components.local_file.camera.mimetypes.guess_type", - mock.Mock(return_value=(None, None)), + with ( + mock.patch("os.path.isfile", mock.Mock(return_value=True)), + mock.patch("os.access", mock.Mock(return_value=True)), + mock.patch( + "homeassistant.components.local_file.camera.mimetypes.guess_type", + mock.Mock(return_value=(None, None)), + ), ): camera_1 = {"platform": "local_file", "file_path": "mock/path.jpg"} camera_2 = { diff --git a/tests/components/local_ip/test_config_flow.py b/tests/components/local_ip/test_config_flow.py index 4de150eaf7a..82fcbf6d6e6 100644 --- a/tests/components/local_ip/test_config_flow.py +++ b/tests/components/local_ip/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for the local_ip config_flow.""" + from __future__ import annotations from homeassistant import data_entry_flow diff --git a/tests/components/local_ip/test_init.py b/tests/components/local_ip/test_init.py index 5c9e9b4f551..54126b21243 100644 --- a/tests/components/local_ip/test_init.py +++ b/tests/components/local_ip/test_init.py @@ -1,4 +1,5 @@ """Tests for the local_ip component.""" + from __future__ import annotations from homeassistant import config_entries diff --git a/tests/components/local_todo/conftest.py b/tests/components/local_todo/conftest.py index 5afa005dd64..ca0ef4d3965 100644 --- a/tests/components/local_todo/conftest.py +++ b/tests/components/local_todo/conftest.py @@ -1,4 +1,5 @@ """Common fixtures for the local_todo tests.""" + from collections.abc import Generator from pathlib import Path from typing import Any diff --git a/tests/components/local_todo/test_config_flow.py b/tests/components/local_todo/test_config_flow.py index 6677a39e54a..381c97be167 100644 --- a/tests/components/local_todo/test_config_flow.py +++ b/tests/components/local_todo/test_config_flow.py @@ -1,4 +1,5 @@ """Test the local_todo config flow.""" + from unittest.mock import AsyncMock import pytest diff --git a/tests/components/local_todo/test_todo.py b/tests/components/local_todo/test_todo.py index 760b0260dbb..3074cdcf88f 100644 --- a/tests/components/local_todo/test_todo.py +++ b/tests/components/local_todo/test_todo.py @@ -185,7 +185,7 @@ async def test_bulk_remove( ws_get_items: Callable[[], Awaitable[dict[str, str]]], ) -> None: """Test removing multiple todo items.""" - for i in range(0, 5): + for i in range(5): await hass.services.async_call( TODO_DOMAIN, "add_item", diff --git a/tests/components/locative/test_init.py b/tests/components/locative/test_init.py index 7a1e071958d..938892ad411 100644 --- a/tests/components/locative/test_init.py +++ b/tests/components/locative/test_init.py @@ -1,4 +1,5 @@ """The tests the for Locative device tracker platform.""" + from http import HTTPStatus from unittest.mock import patch diff --git a/tests/components/lock/test_device_action.py b/tests/components/lock/test_device_action.py index 1e451920baf..3396324284b 100644 --- a/tests/components/lock/test_device_action.py +++ b/tests/components/lock/test_device_action.py @@ -1,4 +1,5 @@ """The tests for Lock device actions.""" + import pytest from pytest_unordered import unordered @@ -89,12 +90,12 @@ async def test_get_actions( @pytest.mark.parametrize( ("hidden_by", "entity_category"), - ( + [ (RegistryEntryHider.INTEGRATION, None), (RegistryEntryHider.USER, None), (None, EntityCategory.CONFIG), (None, EntityCategory.DIAGNOSTIC), - ), + ], ) async def test_get_actions_hidden_auxiliary( hass: HomeAssistant, diff --git a/tests/components/lock/test_device_condition.py b/tests/components/lock/test_device_condition.py index 59dcbcb4629..71e1b6ac48e 100644 --- a/tests/components/lock/test_device_condition.py +++ b/tests/components/lock/test_device_condition.py @@ -1,4 +1,5 @@ """The tests for Lock device conditions.""" + import pytest from pytest_unordered import unordered @@ -76,12 +77,12 @@ async def test_get_conditions( @pytest.mark.parametrize( ("hidden_by", "entity_category"), - ( + [ (RegistryEntryHider.INTEGRATION, None), (RegistryEntryHider.USER, None), (None, EntityCategory.CONFIG), (None, EntityCategory.DIAGNOSTIC), - ), + ], ) async def test_get_conditions_hidden_auxiliary( hass: HomeAssistant, diff --git a/tests/components/lock/test_device_trigger.py b/tests/components/lock/test_device_trigger.py index 9c1594760c9..a45fd7527b5 100644 --- a/tests/components/lock/test_device_trigger.py +++ b/tests/components/lock/test_device_trigger.py @@ -1,4 +1,5 @@ """The tests for Lock device triggers.""" + from datetime import timedelta import pytest @@ -75,12 +76,12 @@ async def test_get_triggers( @pytest.mark.parametrize( ("hidden_by", "entity_category"), - ( + [ (RegistryEntryHider.INTEGRATION, None), (RegistryEntryHider.USER, None), (None, EntityCategory.CONFIG), (None, EntityCategory.DIAGNOSTIC), - ), + ], ) async def test_get_triggers_hidden_auxiliary( hass: HomeAssistant, diff --git a/tests/components/lock/test_init.py b/tests/components/lock/test_init.py index 7ebb5bf3027..e98a7bd9eda 100644 --- a/tests/components/lock/test_init.py +++ b/tests/components/lock/test_init.py @@ -1,4 +1,5 @@ """The tests for the lock component.""" + from __future__ import annotations import re diff --git a/tests/components/lock/test_reproduce_state.py b/tests/components/lock/test_reproduce_state.py index 4f3c94f0b90..4fa06d9320b 100644 --- a/tests/components/lock/test_reproduce_state.py +++ b/tests/components/lock/test_reproduce_state.py @@ -1,4 +1,5 @@ """Test reproduce state for Lock.""" + import pytest from homeassistant.core import HomeAssistant, State diff --git a/tests/components/lock/test_significant_change.py b/tests/components/lock/test_significant_change.py index e7ee3fa07c9..6931af97dd9 100644 --- a/tests/components/lock/test_significant_change.py +++ b/tests/components/lock/test_significant_change.py @@ -1,4 +1,5 @@ """Test the Lock significant change platform.""" + from homeassistant.components.lock.significant_change import ( async_check_significant_change, ) diff --git a/tests/components/logbook/common.py b/tests/components/logbook/common.py index 824bbbde21d..67b83a19768 100644 --- a/tests/components/logbook/common.py +++ b/tests/components/logbook/common.py @@ -1,4 +1,5 @@ """Tests for the logbook component.""" + from __future__ import annotations import json diff --git a/tests/components/logbook/test_init.py b/tests/components/logbook/test_init.py index 803aaba346a..d752b896401 100644 --- a/tests/components/logbook/test_init.py +++ b/tests/components/logbook/test_init.py @@ -1,4 +1,5 @@ """The tests for the logbook component.""" + import asyncio import collections from collections.abc import Callable @@ -293,14 +294,25 @@ def create_state_changed_event( state, attributes=None, last_changed=None, + last_reported=None, last_updated=None, ): """Create state changed event.""" old_state = ha.State( - entity_id, "old", attributes, last_changed, last_updated + entity_id, + "old", + attributes, + last_changed=last_changed, + last_reported=last_reported, + last_updated=last_updated, ).as_dict() new_state = ha.State( - entity_id, state, attributes, last_changed, last_updated + entity_id, + state, + attributes, + last_changed=last_changed, + last_reported=last_reported, + last_updated=last_updated, ).as_dict() return create_state_changed_event_from_old_new( diff --git a/tests/components/logbook/test_models.py b/tests/components/logbook/test_models.py index dcafd7e4765..459fd0e06c9 100644 --- a/tests/components/logbook/test_models.py +++ b/tests/components/logbook/test_models.py @@ -1,4 +1,5 @@ """The tests for the logbook component models.""" + from unittest.mock import Mock from homeassistant.components.logbook.models import LazyEventPartialState diff --git a/tests/components/logbook/test_websocket_api.py b/tests/components/logbook/test_websocket_api.py index 7100f914ff4..1be0e5bd9af 100644 --- a/tests/components/logbook/test_websocket_api.py +++ b/tests/components/logbook/test_websocket_api.py @@ -1,4 +1,5 @@ """The tests for the logbook component.""" + import asyncio from collections.abc import Callable from datetime import timedelta @@ -2284,7 +2285,7 @@ async def test_live_stream_with_one_second_commit_interval( hass.bus.async_fire("mock_event", {"device_id": device.id, "message": "7"}) - while len(recieved_rows) != 7: + while len(recieved_rows) < 7: msg = await asyncio.wait_for(websocket_client.receive_json(), 2.5) assert msg["id"] == 7 assert msg["type"] == "event" @@ -2399,8 +2400,9 @@ async def test_stream_consumer_stop_processing( after_ws_created_listeners = hass.bus.async_listeners() - with patch.object(websocket_api, "MAX_PENDING_LOGBOOK_EVENTS", 5), patch.object( - websocket_api, "_async_events_consumer" + with ( + patch.object(websocket_api, "MAX_PENDING_LOGBOOK_EVENTS", 5), + patch.object(websocket_api, "_async_events_consumer"), ): await websocket_client.send_json( { diff --git a/tests/components/logentries/test_init.py b/tests/components/logentries/test_init.py index 98b171c813f..40e73a86c05 100644 --- a/tests/components/logentries/test_init.py +++ b/tests/components/logentries/test_init.py @@ -1,4 +1,5 @@ """The tests for the Logentries component.""" + from unittest.mock import ANY, call, patch import pytest diff --git a/tests/components/logger/conftest.py b/tests/components/logger/conftest.py index 00d27753a61..76fbde68e78 100644 --- a/tests/components/logger/conftest.py +++ b/tests/components/logger/conftest.py @@ -1,4 +1,5 @@ """Test fixtures for the Logger component.""" + import logging import pytest diff --git a/tests/components/logger/test_init.py b/tests/components/logger/test_init.py index 7b47ee60790..3e30ea0ead0 100644 --- a/tests/components/logger/test_init.py +++ b/tests/components/logger/test_init.py @@ -1,4 +1,5 @@ """The tests for the Logger component.""" + from collections import defaultdict import logging from typing import Any diff --git a/tests/components/logger/test_websocket_api.py b/tests/components/logger/test_websocket_api.py index 10c1ceb2f20..c2fcc7f208e 100644 --- a/tests/components/logger/test_websocket_api.py +++ b/tests/components/logger/test_websocket_api.py @@ -1,4 +1,5 @@ """Tests for Logger Websocket API commands.""" + import logging from unittest.mock import patch @@ -101,12 +102,15 @@ async def test_custom_integration_log_level( }, ) - with patch( - "homeassistant.components.logger.helpers.async_get_integration", - return_value=integration, - ), patch( - "homeassistant.components.logger.websocket_api.async_get_integration", - return_value=integration, + with ( + patch( + "homeassistant.components.logger.helpers.async_get_integration", + return_value=integration, + ), + patch( + "homeassistant.components.logger.websocket_api.async_get_integration", + return_value=integration, + ), ): await websocket_client.send_json( { diff --git a/tests/components/logi_circle/test_config_flow.py b/tests/components/logi_circle/test_config_flow.py index 830760040e0..f0de828c186 100644 --- a/tests/components/logi_circle/test_config_flow.py +++ b/tests/components/logi_circle/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for Logi Circle config flow.""" + import asyncio from http import HTTPStatus from unittest.mock import AsyncMock, Mock, patch @@ -6,6 +7,7 @@ from unittest.mock import AsyncMock, Mock, patch import pytest from homeassistant import config_entries, data_entry_flow +from homeassistant.components.http import KEY_HASS from homeassistant.components.logi_circle import config_flow from homeassistant.components.logi_circle.config_flow import ( DOMAIN, @@ -23,7 +25,7 @@ class MockRequest: def __init__(self, hass, query): """Init request object.""" - self.app = {"hass": hass} + self.app = {KEY_HASS: hass} self.query = query diff --git a/tests/fixtures/london_air.json b/tests/components/london_air/fixtures/london_air.json similarity index 100% rename from tests/fixtures/london_air.json rename to tests/components/london_air/fixtures/london_air.json diff --git a/tests/components/london_air/test_sensor.py b/tests/components/london_air/test_sensor.py index 71ac81de1a8..d87d9257704 100644 --- a/tests/components/london_air/test_sensor.py +++ b/tests/components/london_air/test_sensor.py @@ -1,4 +1,5 @@ """The tests for the london_air platform.""" + from http import HTTPStatus import requests_mock @@ -17,7 +18,9 @@ async def test_valid_state( ) -> None: """Test for operational london_air sensor with proper attributes.""" requests_mock.get( - URL, text=load_fixture("london_air.json"), status_code=HTTPStatus.OK + URL, + text=load_fixture("london_air.json", "london_air"), + status_code=HTTPStatus.OK, ) assert await async_setup_component(hass, "sensor", VALID_CONFIG) await hass.async_block_till_done() diff --git a/tests/components/london_underground/test_sensor.py b/tests/components/london_underground/test_sensor.py index 4dda341279d..98f1cc0e09b 100644 --- a/tests/components/london_underground/test_sensor.py +++ b/tests/components/london_underground/test_sensor.py @@ -1,4 +1,5 @@ """The tests for the london_underground platform.""" + from london_tube_status import API_URL from homeassistant.components.london_underground.const import CONF_LINE diff --git a/tests/components/lookin/__init__.py b/tests/components/lookin/__init__.py index bfbb5f66887..e19bdc9fd73 100644 --- a/tests/components/lookin/__init__.py +++ b/tests/components/lookin/__init__.py @@ -1,4 +1,5 @@ """Tests for the lookin integration.""" + from __future__ import annotations from ipaddress import ip_address diff --git a/tests/components/lookin/test_config_flow.py b/tests/components/lookin/test_config_flow.py index 873e21a5cac..18cbe33db3a 100644 --- a/tests/components/lookin/test_config_flow.py +++ b/tests/components/lookin/test_config_flow.py @@ -1,4 +1,5 @@ """Define tests for the lookin config flow.""" + from __future__ import annotations import dataclasses @@ -34,9 +35,10 @@ async def test_manual_setup(hass: HomeAssistant) -> None: assert result["step_id"] == "user" assert not result["errors"] - with _patch_get_info(), patch( - f"{MODULE}.async_setup_entry", return_value=True - ) as mock_setup_entry: + with ( + _patch_get_info(), + patch(f"{MODULE}.async_setup_entry", return_value=True) as mock_setup_entry, + ): result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_HOST: IP_ADDRESS} ) @@ -123,9 +125,12 @@ async def test_discovered_zeroconf(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.FORM assert result["errors"] is None - with _patch_get_info(), patch( - f"{MODULE}.async_setup_entry", return_value=True - ) as mock_async_setup_entry: + with ( + _patch_get_info(), + 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() @@ -138,9 +143,12 @@ async def test_discovered_zeroconf(hass: HomeAssistant) -> None: zc_data_new_ip = dataclasses.replace(ZEROCONF_DATA) zc_data_new_ip.ip_address = ip_address("127.0.0.2") - with _patch_get_info(), patch( - f"{MODULE}.async_setup_entry", return_value=True - ) as mock_async_setup_entry: + with ( + _patch_get_info(), + patch( + f"{MODULE}.async_setup_entry", return_value=True + ) as mock_async_setup_entry, + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, diff --git a/tests/components/loqed/conftest.py b/tests/components/loqed/conftest.py index 616c0cb0552..b4265873457 100644 --- a/tests/components/loqed/conftest.py +++ b/tests/components/loqed/conftest.py @@ -88,8 +88,11 @@ async def integration_fixture( lock_status = json.loads(load_fixture("loqed/status_ok.json")) - with patch("loqedAPI.loqed.LoqedAPI.async_get_lock", return_value=lock), patch( - "loqedAPI.loqed.LoqedAPI.async_get_lock_details", return_value=lock_status + with ( + patch("loqedAPI.loqed.LoqedAPI.async_get_lock", return_value=lock), + patch( + "loqedAPI.loqed.LoqedAPI.async_get_lock_details", return_value=lock_status + ), ): await async_setup_component(hass, DOMAIN, config) await hass.async_block_till_done() diff --git a/tests/components/loqed/test_config_flow.py b/tests/components/loqed/test_config_flow.py index 617b6818a64..ed18f0ae40e 100644 --- a/tests/components/loqed/test_config_flow.py +++ b/tests/components/loqed/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Loqed config flow.""" + from ipaddress import ip_address import json from unittest.mock import Mock, patch @@ -48,17 +49,23 @@ async def test_create_entry_zeroconf(hass: HomeAssistant) -> None: webhook_id = "Webhook_ID" all_locks_response = json.loads(load_fixture("loqed/get_all_locks.json")) - with patch( - "loqedAPI.cloud_loqed.LoqedCloudAPI.async_get_locks", - return_value=all_locks_response, - ), patch( - "loqedAPI.loqed.LoqedAPI.async_get_lock", - return_value=mock_lock, - ), patch( - "homeassistant.components.loqed.async_setup_entry", - return_value=True, - ), patch( - "homeassistant.components.webhook.async_generate_id", return_value=webhook_id + with ( + patch( + "loqedAPI.cloud_loqed.LoqedCloudAPI.async_get_locks", + return_value=all_locks_response, + ), + patch( + "loqedAPI.loqed.LoqedAPI.async_get_lock", + return_value=mock_lock, + ), + patch( + "homeassistant.components.loqed.async_setup_entry", + return_value=True, + ), + patch( + "homeassistant.components.webhook.async_generate_id", + return_value=webhook_id, + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -103,19 +110,26 @@ async def test_create_entry_user( all_locks_response = json.loads(load_fixture("loqed/get_all_locks.json")) found_lock = all_locks_response["data"][0] - with patch( - "loqedAPI.cloud_loqed.LoqedCloudAPI.async_get_locks", - return_value=all_locks_response, - ), patch( - "loqedAPI.loqed.LoqedAPI.async_get_lock", - return_value=mock_lock, - ), patch( - "homeassistant.components.loqed.async_setup_entry", - return_value=True, - ), patch( - "homeassistant.components.webhook.async_generate_id", return_value=webhook_id - ), patch( - "loqedAPI.loqed.LoqedAPI.async_get_lock_details", return_value=lock_result + with ( + patch( + "loqedAPI.cloud_loqed.LoqedCloudAPI.async_get_locks", + return_value=all_locks_response, + ), + patch( + "loqedAPI.loqed.LoqedAPI.async_get_lock", + return_value=mock_lock, + ), + patch( + "homeassistant.components.loqed.async_setup_entry", + return_value=True, + ), + patch( + "homeassistant.components.webhook.async_generate_id", + return_value=webhook_id, + ), + patch( + "loqedAPI.loqed.LoqedAPI.async_get_lock_details", return_value=lock_result + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -207,10 +221,15 @@ async def test_cannot_connect_when_lock_not_reachable( all_locks_response = json.loads(load_fixture("loqed/get_all_locks.json")) - with patch( - "loqedAPI.cloud_loqed.LoqedCloudAPI.async_get_locks", - return_value=all_locks_response, - ), patch("loqedAPI.loqed.LoqedAPI.async_get_lock", side_effect=aiohttp.ClientError): + with ( + patch( + "loqedAPI.cloud_loqed.LoqedCloudAPI.async_get_locks", + return_value=all_locks_response, + ), + patch( + "loqedAPI.loqed.LoqedAPI.async_get_lock", side_effect=aiohttp.ClientError + ), + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_API_TOKEN: "eyadiuyfasiuasf", CONF_NAME: "MyLock"}, diff --git a/tests/components/loqed/test_init.py b/tests/components/loqed/test_init.py index 47f53a1ad20..3d52feead79 100644 --- a/tests/components/loqed/test_init.py +++ b/tests/components/loqed/test_init.py @@ -50,8 +50,11 @@ async def test_setup_webhook_in_bridge( webhooks_fixture = json.loads(load_fixture("loqed/get_all_webhooks.json")) lock.getWebhooks = AsyncMock(side_effect=[[], webhooks_fixture]) - with patch("loqedAPI.loqed.LoqedAPI.async_get_lock", return_value=lock), patch( - "loqedAPI.loqed.LoqedAPI.async_get_lock_details", return_value=lock_status + with ( + patch("loqedAPI.loqed.LoqedAPI.async_get_lock", return_value=lock), + patch( + "loqedAPI.loqed.LoqedAPI.async_get_lock_details", return_value=lock_status + ), ): await async_setup_component(hass, DOMAIN, config) await hass.async_block_till_done() @@ -86,13 +89,19 @@ async def test_setup_cloudhook_in_bridge( webhooks_fixture = json.loads(load_fixture("loqed/get_all_webhooks.json")) lock.getWebhooks = AsyncMock(side_effect=[[], webhooks_fixture]) - with patch("loqedAPI.loqed.LoqedAPI.async_get_lock", return_value=lock), patch( - "loqedAPI.loqed.LoqedAPI.async_get_lock_details", return_value=lock_status - ), patch( - "homeassistant.components.cloud.async_active_subscription", return_value=True - ), patch( - "homeassistant.components.cloud.async_create_cloudhook", - return_value=webhooks_fixture[0]["url"], + with ( + patch("loqedAPI.loqed.LoqedAPI.async_get_lock", return_value=lock), + patch( + "loqedAPI.loqed.LoqedAPI.async_get_lock_details", return_value=lock_status + ), + patch( + "homeassistant.components.cloud.async_active_subscription", + return_value=True, + ), + patch( + "homeassistant.components.cloud.async_create_cloudhook", + return_value=webhooks_fixture[0]["url"], + ), ): await async_setup_component(hass, DOMAIN, config) await hass.async_block_till_done() @@ -113,13 +122,19 @@ async def test_setup_cloudhook_from_entry_in_bridge( lock.getWebhooks = AsyncMock(side_effect=[[], webhooks_fixture]) - with patch("loqedAPI.loqed.LoqedAPI.async_get_lock", return_value=lock), patch( - "loqedAPI.loqed.LoqedAPI.async_get_lock_details", return_value=lock_status - ), patch( - "homeassistant.components.cloud.async_active_subscription", return_value=True - ), patch( - "homeassistant.components.cloud.async_create_cloudhook", - return_value=webhooks_fixture[0]["url"], + with ( + patch("loqedAPI.loqed.LoqedAPI.async_get_lock", return_value=lock), + patch( + "loqedAPI.loqed.LoqedAPI.async_get_lock_details", return_value=lock_status + ), + patch( + "homeassistant.components.cloud.async_active_subscription", + return_value=True, + ), + patch( + "homeassistant.components.cloud.async_create_cloudhook", + return_value=webhooks_fixture[0]["url"], + ), ): await async_setup_component(hass, DOMAIN, config) await hass.async_block_till_done() diff --git a/tests/components/loqed/test_lock.py b/tests/components/loqed/test_lock.py index 59e70212f92..5fd00b66c43 100644 --- a/tests/components/loqed/test_lock.py +++ b/tests/components/loqed/test_lock.py @@ -1,4 +1,5 @@ """Tests the lock platform of the Loqed integration.""" + from loqedAPI import loqed from homeassistant.components.loqed import LoqedDataCoordinator diff --git a/tests/components/lovelace/test_cast.py b/tests/components/lovelace/test_cast.py index 4181d73c4d3..f0a193ec705 100644 --- a/tests/components/lovelace/test_cast.py +++ b/tests/components/lovelace/test_cast.py @@ -1,6 +1,8 @@ """Test the Lovelace Cast platform.""" + +from collections.abc import Generator from time import time -from unittest.mock import patch +from unittest.mock import MagicMock, patch import pytest @@ -14,6 +16,19 @@ from homeassistant.setup import async_setup_component from tests.common import async_mock_service +@pytest.fixture(autouse=True) +def mock_onboarding_done() -> Generator[MagicMock, None, None]: + """Mock that Home Assistant is currently onboarding. + + Enabled to prevent creating default dashboards during test execution. + """ + with patch( + "homeassistant.components.onboarding.async_is_onboarded", + return_value=True, + ) as mock_onboarding: + yield mock_onboarding + + @pytest.fixture async def mock_https_url(hass): """Mock valid URL.""" @@ -43,20 +58,23 @@ async def mock_yaml_dashboard(hass): }, ) - with patch( - "homeassistant.components.lovelace.dashboard.load_yaml_dict", - return_value={ - "title": "YAML Title", - "views": [ - { - "title": "Hello", - }, - {"path": "second-view"}, - ], - }, - ), patch( - "homeassistant.components.lovelace.dashboard.os.path.getmtime", - return_value=time() + 10, + with ( + patch( + "homeassistant.components.lovelace.dashboard.load_yaml_dict", + return_value={ + "title": "YAML Title", + "views": [ + { + "title": "Hello", + }, + {"path": "second-view"}, + ], + }, + ), + patch( + "homeassistant.components.lovelace.dashboard.os.path.getmtime", + return_value=time() + 10, + ), ): yield diff --git a/tests/components/lovelace/test_dashboard.py b/tests/components/lovelace/test_dashboard.py index a772b37f047..47c4981ba2a 100644 --- a/tests/components/lovelace/test_dashboard.py +++ b/tests/components/lovelace/test_dashboard.py @@ -1,6 +1,8 @@ """Test the Lovelace initialization.""" + +from collections.abc import Generator from typing import Any -from unittest.mock import patch +from unittest.mock import MagicMock, patch import pytest @@ -13,6 +15,19 @@ from tests.common import assert_setup_component, async_capture_events from tests.typing import WebSocketGenerator +@pytest.fixture(autouse=True) +def mock_onboarding_done() -> Generator[MagicMock, None, None]: + """Mock that Home Assistant is currently onboarding. + + Enabled to prevent creating default dashboards during test execution. + """ + with patch( + "homeassistant.components.onboarding.async_is_onboarded", + return_value=True, + ) as mock_onboarding: + yield mock_onboarding + + async def test_lovelace_from_storage( hass: HomeAssistant, hass_ws_client, hass_storage: dict[str, Any] ) -> None: @@ -166,7 +181,7 @@ async def test_lovelace_from_yaml( assert len(events) == 1 -@pytest.mark.parametrize("url_path", ("test-panel", "test-panel-no-sidebar")) +@pytest.mark.parametrize("url_path", ["test-panel", "test-panel-no-sidebar"]) async def test_dashboard_from_yaml( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, url_path ) -> None: @@ -276,7 +291,7 @@ async def test_dashboard_from_yaml( async def test_wrong_key_dashboard_from_yaml(hass: HomeAssistant) -> None: """Test we don't load lovelace dashboard without hyphen config from yaml.""" - with assert_setup_component(0): + with assert_setup_component(0, "lovelace"): assert not await async_setup_component( hass, "lovelace", @@ -440,61 +455,6 @@ async def test_storage_dashboards( assert dashboard.CONFIG_STORAGE_KEY.format(dashboard_id) not in hass_storage -async def test_storage_dashboard_migrate( - hass: HomeAssistant, hass_ws_client, hass_storage: dict[str, Any] -) -> None: - """Test changing url path from storage config.""" - hass_storage[dashboard.DASHBOARDS_STORAGE_KEY] = { - "key": "lovelace_dashboards", - "version": 1, - "data": { - "items": [ - { - "icon": "mdi:tools", - "id": "tools", - "mode": "storage", - "require_admin": True, - "show_in_sidebar": True, - "title": "Tools", - "url_path": "tools", - }, - { - "icon": "mdi:tools", - "id": "tools2", - "mode": "storage", - "require_admin": True, - "show_in_sidebar": True, - "title": "Tools", - "url_path": "dashboard-tools", - }, - ] - }, - } - - assert await async_setup_component(hass, "lovelace", {}) - - client = await hass_ws_client(hass) - - # Fetch data - await client.send_json({"id": 5, "type": "lovelace/dashboards/list"}) - response = await client.receive_json() - assert response["success"] - without_hyphen, with_hyphen = response["result"] - - assert without_hyphen["icon"] == "mdi:tools" - assert without_hyphen["id"] == "tools" - assert without_hyphen["mode"] == "storage" - assert without_hyphen["require_admin"] - assert without_hyphen["show_in_sidebar"] - assert without_hyphen["title"] == "Tools" - assert without_hyphen["url_path"] == "lovelace-tools" - - assert ( - with_hyphen - == hass_storage[dashboard.DASHBOARDS_STORAGE_KEY]["data"]["items"][1] - ) - - async def test_websocket_list_dashboards( hass: HomeAssistant, hass_ws_client: WebSocketGenerator ) -> None: diff --git a/tests/components/lovelace/test_init.py b/tests/components/lovelace/test_init.py new file mode 100644 index 00000000000..a88745e4500 --- /dev/null +++ b/tests/components/lovelace/test_init.py @@ -0,0 +1,98 @@ +"""Test the Lovelace initialization.""" + +from collections.abc import Generator +from typing import Any +from unittest.mock import MagicMock, patch + +import pytest + +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.typing import WebSocketGenerator + + +@pytest.fixture +def mock_onboarding_not_done() -> Generator[MagicMock, None, None]: + """Mock that Home Assistant is currently onboarding.""" + with patch( + "homeassistant.components.onboarding.async_is_onboarded", + return_value=False, + ) as mock_onboarding: + yield mock_onboarding + + +@pytest.fixture +def mock_onboarding_done() -> Generator[MagicMock, None, None]: + """Mock that Home Assistant is currently onboarding.""" + with patch( + "homeassistant.components.onboarding.async_is_onboarded", + return_value=True, + ) as mock_onboarding: + yield mock_onboarding + + +@pytest.fixture +def mock_add_onboarding_listener() -> Generator[MagicMock, None, None]: + """Mock that Home Assistant is currently onboarding.""" + with patch( + "homeassistant.components.onboarding.async_add_listener", + ) as mock_add_onboarding_listener: + yield mock_add_onboarding_listener + + +async def test_create_dashboards_when_onboarded( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + hass_storage: dict[str, Any], + mock_onboarding_done, +) -> None: + """Test we don't create dashboards when onboarded.""" + client = await hass_ws_client(hass) + + assert await async_setup_component(hass, "lovelace", {}) + + # List dashboards + await client.send_json_auto_id({"type": "lovelace/dashboards/list"}) + response = await client.receive_json() + assert response["success"] + assert response["result"] == [] + + +async def test_create_dashboards_when_not_onboarded( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + hass_storage: dict[str, Any], + mock_add_onboarding_listener, + mock_onboarding_not_done, +) -> None: + """Test we automatically create dashboards when not onboarded.""" + client = await hass_ws_client(hass) + + assert await async_setup_component(hass, "lovelace", {}) + + # Call onboarding listener + mock_add_onboarding_listener.mock_calls[0][1][1]() + await hass.async_block_till_done() + + # List dashboards + await client.send_json_auto_id({"type": "lovelace/dashboards/list"}) + response = await client.receive_json() + assert response["success"] + assert response["result"] == [ + { + "icon": "mdi:map", + "id": "map", + "mode": "storage", + "require_admin": False, + "show_in_sidebar": True, + "title": "Map", + "url_path": "map", + } + ] + + # List map dashboard config + await client.send_json_auto_id({"type": "lovelace/config", "url_path": "map"}) + response = await client.receive_json() + assert response["success"] + assert response["result"] == {"strategy": {"type": "map"}} diff --git a/tests/components/lovelace/test_resources.py b/tests/components/lovelace/test_resources.py index 4a280eccfda..7591960b589 100644 --- a/tests/components/lovelace/test_resources.py +++ b/tests/components/lovelace/test_resources.py @@ -1,4 +1,5 @@ """Test Lovelace resources.""" + import copy from typing import Any from unittest.mock import patch diff --git a/tests/components/lovelace/test_system_health.py b/tests/components/lovelace/test_system_health.py index 72e7adb3a13..9bd8543004c 100644 --- a/tests/components/lovelace/test_system_health.py +++ b/tests/components/lovelace/test_system_health.py @@ -1,6 +1,10 @@ """Tests for Lovelace system health.""" + +from collections.abc import Generator from typing import Any -from unittest.mock import patch +from unittest.mock import MagicMock, patch + +import pytest from homeassistant.components.lovelace import dashboard from homeassistant.core import HomeAssistant @@ -9,6 +13,19 @@ from homeassistant.setup import async_setup_component from tests.common import get_system_health_info +@pytest.fixture(autouse=True) +def mock_onboarding_done() -> Generator[MagicMock, None, None]: + """Mock that Home Assistant is currently onboarding. + + Enabled to prevent creating default dashboards during test execution. + """ + with patch( + "homeassistant.components.onboarding.async_is_onboarded", + return_value=True, + ) as mock_onboarding: + yield mock_onboarding + + async def test_system_health_info_autogen(hass: HomeAssistant) -> None: """Test system health info endpoint.""" assert await async_setup_component(hass, "lovelace", {}) diff --git a/tests/components/luftdaten/conftest.py b/tests/components/luftdaten/conftest.py index 08cbe7a2c3c..e083e8c97c7 100644 --- a/tests/components/luftdaten/conftest.py +++ b/tests/components/luftdaten/conftest.py @@ -1,4 +1,5 @@ """Fixtures for Luftdaten tests.""" + from __future__ import annotations from collections.abc import Generator @@ -36,10 +37,14 @@ def mock_setup_entry() -> Generator[None, None, None]: @pytest.fixture def mock_luftdaten() -> Generator[None, MagicMock, None]: """Return a mocked Luftdaten client.""" - with patch( - "homeassistant.components.luftdaten.Luftdaten", autospec=True - ) as luftdaten_mock, patch( - "homeassistant.components.luftdaten.config_flow.Luftdaten", new=luftdaten_mock + with ( + patch( + "homeassistant.components.luftdaten.Luftdaten", autospec=True + ) as luftdaten_mock, + patch( + "homeassistant.components.luftdaten.config_flow.Luftdaten", + new=luftdaten_mock, + ), ): luftdaten = luftdaten_mock.return_value luftdaten.validate_sensor.return_value = True diff --git a/tests/components/luftdaten/test_config_flow.py b/tests/components/luftdaten/test_config_flow.py index a0b741f7d2a..7469fe6e486 100644 --- a/tests/components/luftdaten/test_config_flow.py +++ b/tests/components/luftdaten/test_config_flow.py @@ -1,4 +1,5 @@ """Define tests for the Luftdaten config flow.""" + from unittest.mock import MagicMock from luftdaten.exceptions import LuftdatenConnectionError diff --git a/tests/components/luftdaten/test_init.py b/tests/components/luftdaten/test_init.py index 40e40bcd137..dda7c147672 100644 --- a/tests/components/luftdaten/test_init.py +++ b/tests/components/luftdaten/test_init.py @@ -1,4 +1,5 @@ """Tests for the Luftdaten integration.""" + from unittest.mock import AsyncMock, MagicMock, patch from luftdaten.exceptions import LuftdatenError diff --git a/tests/components/luftdaten/test_sensor.py b/tests/components/luftdaten/test_sensor.py index 7a2cac1721b..f2cf12b3fda 100644 --- a/tests/components/luftdaten/test_sensor.py +++ b/tests/components/luftdaten/test_sensor.py @@ -1,4 +1,5 @@ """Tests for the sensors provided by the Luftdaten integration.""" + from homeassistant.components.luftdaten.const import DOMAIN from homeassistant.components.sensor import ( ATTR_STATE_CLASS, diff --git a/tests/components/lupusec/test_config_flow.py b/tests/components/lupusec/test_config_flow.py index 6b07952ff54..2ca313139dc 100644 --- a/tests/components/lupusec/test_config_flow.py +++ b/tests/components/lupusec/test_config_flow.py @@ -1,4 +1,4 @@ -""""Unit tests for the Lupusec config flow.""" +"""Unit tests for the Lupusec config flow.""" from json import JSONDecodeError from unittest.mock import patch @@ -48,12 +48,15 @@ async def test_form_valid_input(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.FORM assert result["errors"] == {} - with patch( - "homeassistant.components.lupusec.async_setup_entry", - return_value=True, - ) as mock_setup_entry, patch( - "homeassistant.components.lupusec.config_flow.lupupy.Lupusec", - ) as mock_initialize_lupusec: + with ( + patch( + "homeassistant.components.lupusec.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + patch( + "homeassistant.components.lupusec.config_flow.lupupy.Lupusec", + ) as mock_initialize_lupusec, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], MOCK_DATA_STEP, @@ -101,12 +104,15 @@ async def test_flow_user_init_data_error_and_recover( assert len(mock_initialize_lupusec.mock_calls) == 1 # Recover - with patch( - "homeassistant.components.lupusec.async_setup_entry", - return_value=True, - ) as mock_setup_entry, patch( - "homeassistant.components.lupusec.config_flow.lupupy.Lupusec", - ) as mock_initialize_lupusec: + with ( + patch( + "homeassistant.components.lupusec.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + patch( + "homeassistant.components.lupusec.config_flow.lupupy.Lupusec", + ) as mock_initialize_lupusec, + ): result3 = await hass.config_entries.flow.async_configure( result["flow_id"], MOCK_DATA_STEP, @@ -160,12 +166,15 @@ async def test_flow_source_import( hass: HomeAssistant, mock_import_step, mock_title ) -> None: """Test configuration import from YAML.""" - with patch( - "homeassistant.components.lupusec.async_setup_entry", - return_value=True, - ) as mock_setup_entry, patch( - "homeassistant.components.lupusec.config_flow.lupupy.Lupusec", - ) as mock_initialize_lupusec: + with ( + patch( + "homeassistant.components.lupusec.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + patch( + "homeassistant.components.lupusec.config_flow.lupupy.Lupusec", + ) as mock_initialize_lupusec, + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, diff --git a/tests/components/lutron/test_config_flow.py b/tests/components/lutron/test_config_flow.py index b1f4b3365c9..db3faa7f911 100644 --- a/tests/components/lutron/test_config_flow.py +++ b/tests/components/lutron/test_config_flow.py @@ -1,4 +1,5 @@ """Test the lutron config flow.""" + from email.message import Message from unittest.mock import AsyncMock, patch from urllib.error import HTTPError @@ -29,8 +30,9 @@ async def test_full_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> No assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" - with patch("homeassistant.components.lutron.config_flow.Lutron.load_xml_db"), patch( - "homeassistant.components.lutron.config_flow.Lutron.guid", "12345678901" + with ( + patch("homeassistant.components.lutron.config_flow.Lutron.load_xml_db"), + patch("homeassistant.components.lutron.config_flow.Lutron.guid", "12345678901"), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -76,8 +78,9 @@ async def test_flow_failure( assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": text_error} - with patch("homeassistant.components.lutron.config_flow.Lutron.load_xml_db"), patch( - "homeassistant.components.lutron.config_flow.Lutron.guid", "12345678901" + with ( + patch("homeassistant.components.lutron.config_flow.Lutron.load_xml_db"), + patch("homeassistant.components.lutron.config_flow.Lutron.guid", "12345678901"), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -101,8 +104,9 @@ async def test_flow_incorrect_guid( assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" - with patch("homeassistant.components.lutron.config_flow.Lutron.load_xml_db"), patch( - "homeassistant.components.lutron.config_flow.Lutron.guid", "12345" + with ( + patch("homeassistant.components.lutron.config_flow.Lutron.load_xml_db"), + patch("homeassistant.components.lutron.config_flow.Lutron.guid", "12345"), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -112,8 +116,9 @@ async def test_flow_incorrect_guid( assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} - with patch("homeassistant.components.lutron.config_flow.Lutron.load_xml_db"), patch( - "homeassistant.components.lutron.config_flow.Lutron.guid", "12345678901" + with ( + patch("homeassistant.components.lutron.config_flow.Lutron.load_xml_db"), + patch("homeassistant.components.lutron.config_flow.Lutron.guid", "12345678901"), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -148,8 +153,9 @@ async def test_import( mock_setup_entry: AsyncMock, ) -> None: """Test import flow.""" - with patch("homeassistant.components.lutron.config_flow.Lutron.load_xml_db"), patch( - "homeassistant.components.lutron.config_flow.Lutron.guid", "12345678901" + with ( + patch("homeassistant.components.lutron.config_flow.Lutron.load_xml_db"), + patch("homeassistant.components.lutron.config_flow.Lutron.guid", "12345678901"), ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, data=MOCK_DATA_IMPORT @@ -188,8 +194,9 @@ async def test_import_flow_failure( async def test_import_flow_guid_failure(hass: HomeAssistant) -> None: """Test handling errors while importing.""" - with patch("homeassistant.components.lutron.config_flow.Lutron.load_xml_db"), patch( - "homeassistant.components.lutron.config_flow.Lutron.guid", "123" + with ( + patch("homeassistant.components.lutron.config_flow.Lutron.load_xml_db"), + patch("homeassistant.components.lutron.config_flow.Lutron.guid", "123"), ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, data=MOCK_DATA_IMPORT diff --git a/tests/components/lutron_caseta/__init__.py b/tests/components/lutron_caseta/__init__.py index 4e8e2d9a504..cc785f71e19 100644 --- a/tests/components/lutron_caseta/__init__.py +++ b/tests/components/lutron_caseta/__init__.py @@ -1,6 +1,5 @@ """Tests for the Lutron Caseta integration.""" - from unittest.mock import patch from homeassistant.components.lutron_caseta import DOMAIN @@ -284,7 +283,7 @@ class MockBridge: :param domain: one of 'light', 'switch', 'cover', 'fan' or 'sensor' :returns list of zero or more of the devices """ - types = _LEAP_DEVICE_TYPES.get(domain, None) + types = _LEAP_DEVICE_TYPES.get(domain) # return immediately if not a supported domain if types is None: diff --git a/tests/components/lutron_caseta/test_config_flow.py b/tests/components/lutron_caseta/test_config_flow.py index 759b23b8f4f..15a4fca7d33 100644 --- a/tests/components/lutron_caseta/test_config_flow.py +++ b/tests/components/lutron_caseta/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Lutron Caseta config flow.""" + from ipaddress import ip_address from pathlib import Path import ssl @@ -53,15 +54,17 @@ async def test_bridge_import_flow(hass: HomeAssistant) -> None: CONF_CA_CERTS: "", } - with patch( - "homeassistant.components.lutron_caseta.async_setup_entry", - return_value=True, - ) as mock_setup_entry, patch( - "homeassistant.components.lutron_caseta.async_setup", return_value=True - ), patch.object( - Smartbridge, - "create_tls", - ) as create_tls: + with ( + patch( + "homeassistant.components.lutron_caseta.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + patch("homeassistant.components.lutron_caseta.async_setup", return_value=True), + patch.object( + Smartbridge, + "create_tls", + ) as create_tls, + ): create_tls.return_value = MockBridge(can_connect=True) result = await hass.config_entries.flow.async_init( @@ -217,15 +220,19 @@ async def test_form_user(hass: HomeAssistant, tmp_path: Path) -> None: assert result2["type"] == "form" assert result2["step_id"] == "link" - with patch( - "homeassistant.components.lutron_caseta.config_flow.async_pair", - return_value=MOCK_ASYNC_PAIR_SUCCESS, - ), patch( - "homeassistant.components.lutron_caseta.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.lutron_caseta.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.lutron_caseta.config_flow.async_pair", + return_value=MOCK_ASYNC_PAIR_SUCCESS, + ), + patch( + "homeassistant.components.lutron_caseta.async_setup", return_value=True + ) as mock_setup, + patch( + "homeassistant.components.lutron_caseta.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], {}, @@ -267,15 +274,19 @@ async def test_form_user_pairing_fails(hass: HomeAssistant, tmp_path: Path) -> N assert result2["type"] == "form" assert result2["step_id"] == "link" - with patch( - "homeassistant.components.lutron_caseta.config_flow.async_pair", - side_effect=TimeoutError, - ), patch( - "homeassistant.components.lutron_caseta.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.lutron_caseta.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.lutron_caseta.config_flow.async_pair", + side_effect=TimeoutError, + ), + patch( + "homeassistant.components.lutron_caseta.async_setup", return_value=True + ) as mock_setup, + patch( + "homeassistant.components.lutron_caseta.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], {}, @@ -313,15 +324,19 @@ async def test_form_user_reuses_existing_assets_when_pairing_again( assert result2["type"] == "form" assert result2["step_id"] == "link" - with patch( - "homeassistant.components.lutron_caseta.config_flow.async_pair", - return_value=MOCK_ASYNC_PAIR_SUCCESS, - ), patch( - "homeassistant.components.lutron_caseta.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.lutron_caseta.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.lutron_caseta.config_flow.async_pair", + return_value=MOCK_ASYNC_PAIR_SUCCESS, + ), + patch( + "homeassistant.components.lutron_caseta.async_setup", return_value=True + ) as mock_setup, + patch( + "homeassistant.components.lutron_caseta.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], {}, @@ -367,11 +382,12 @@ async def test_form_user_reuses_existing_assets_when_pairing_again( assert result2["type"] == "form" assert result2["step_id"] == "link" - with patch( - "homeassistant.components.lutron_caseta.async_setup", return_value=True - ), patch( - "homeassistant.components.lutron_caseta.async_setup_entry", - return_value=True, + with ( + patch("homeassistant.components.lutron_caseta.async_setup", return_value=True), + patch( + "homeassistant.components.lutron_caseta.async_setup_entry", + return_value=True, + ), ): result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], @@ -472,7 +488,7 @@ async def test_zeroconf_not_lutron_device(hass: HomeAssistant) -> None: @pytest.mark.parametrize( - "source", (config_entries.SOURCE_ZEROCONF, config_entries.SOURCE_HOMEKIT) + "source", [config_entries.SOURCE_ZEROCONF, config_entries.SOURCE_HOMEKIT] ) async def test_zeroconf(hass: HomeAssistant, source, tmp_path: Path) -> None: """Test starting a flow from discovery.""" @@ -498,15 +514,19 @@ async def test_zeroconf(hass: HomeAssistant, source, tmp_path: Path) -> None: assert result["type"] == "form" assert result["step_id"] == "link" - with patch( - "homeassistant.components.lutron_caseta.config_flow.async_pair", - return_value=MOCK_ASYNC_PAIR_SUCCESS, - ), patch( - "homeassistant.components.lutron_caseta.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.lutron_caseta.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.lutron_caseta.config_flow.async_pair", + return_value=MOCK_ASYNC_PAIR_SUCCESS, + ), + patch( + "homeassistant.components.lutron_caseta.async_setup", return_value=True + ) as mock_setup, + patch( + "homeassistant.components.lutron_caseta.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {}, diff --git a/tests/components/lutron_caseta/test_cover.py b/tests/components/lutron_caseta/test_cover.py index 7fe8ed22866..5d45f185aef 100644 --- a/tests/components/lutron_caseta/test_cover.py +++ b/tests/components/lutron_caseta/test_cover.py @@ -1,6 +1,5 @@ """Tests for the Lutron Caseta integration.""" - from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er diff --git a/tests/components/lutron_caseta/test_device_trigger.py b/tests/components/lutron_caseta/test_device_trigger.py index 3fafbb8a57f..0e638065cf7 100644 --- a/tests/components/lutron_caseta/test_device_trigger.py +++ b/tests/components/lutron_caseta/test_device_trigger.py @@ -1,4 +1,5 @@ """The tests for Lutron Caséta device triggers.""" + from unittest.mock import patch import pytest diff --git a/tests/components/lutron_caseta/test_diagnostics.py b/tests/components/lutron_caseta/test_diagnostics.py index 0abf54b9589..5c7d20da208 100644 --- a/tests/components/lutron_caseta/test_diagnostics.py +++ b/tests/components/lutron_caseta/test_diagnostics.py @@ -1,4 +1,5 @@ """Test the Lutron Caseta diagnostics.""" + from unittest.mock import ANY, patch from homeassistant.components.lutron_caseta import DOMAIN diff --git a/tests/components/lutron_caseta/test_fan.py b/tests/components/lutron_caseta/test_fan.py index 0147817514d..07744796679 100644 --- a/tests/components/lutron_caseta/test_fan.py +++ b/tests/components/lutron_caseta/test_fan.py @@ -1,6 +1,5 @@ """Tests for the Lutron Caseta integration.""" - from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er diff --git a/tests/components/lutron_caseta/test_light.py b/tests/components/lutron_caseta/test_light.py index cdba9a956e5..5a1568c3cb8 100644 --- a/tests/components/lutron_caseta/test_light.py +++ b/tests/components/lutron_caseta/test_light.py @@ -1,6 +1,5 @@ """Tests for the Lutron Caseta integration.""" - from homeassistant.const import STATE_ON from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er diff --git a/tests/components/lutron_caseta/test_logbook.py b/tests/components/lutron_caseta/test_logbook.py index c0bac43ba6f..b6e8840c85c 100644 --- a/tests/components/lutron_caseta/test_logbook.py +++ b/tests/components/lutron_caseta/test_logbook.py @@ -1,4 +1,5 @@ """The tests for lutron caseta logbook.""" + from unittest.mock import patch from homeassistant.components.lutron_caseta.const import ( diff --git a/tests/components/lyric/test_config_flow.py b/tests/components/lyric/test_config_flow.py index 5a88728cc75..00d623ea3ce 100644 --- a/tests/components/lyric/test_config_flow.py +++ b/tests/components/lyric/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Honeywell Lyric config flow.""" + from http import HTTPStatus from unittest.mock import patch @@ -83,9 +84,12 @@ async def test_full_flow( }, ) - with patch("homeassistant.components.lyric.api.ConfigEntryLyricClient"), patch( - "homeassistant.components.lyric.async_setup_entry", return_value=True - ) as mock_setup: + with ( + patch("homeassistant.components.lyric.api.ConfigEntryLyricClient"), + patch( + "homeassistant.components.lyric.async_setup_entry", return_value=True + ) as mock_setup, + ): result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["data"]["auth_implementation"] == "cred" @@ -151,9 +155,12 @@ async def test_reauthentication_flow( }, ) - with patch("homeassistant.components.lyric.api.ConfigEntryLyricClient"), patch( - "homeassistant.components.lyric.async_setup_entry", return_value=True - ) as mock_setup: + with ( + patch("homeassistant.components.lyric.api.ConfigEntryLyricClient"), + patch( + "homeassistant.components.lyric.async_setup_entry", return_value=True + ) as mock_setup, + ): result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == data_entry_flow.FlowResultType.ABORT diff --git a/tests/components/mailbox/test_init.py b/tests/components/mailbox/test_init.py index 0e48f3c8ccc..8b83f2b0ec7 100644 --- a/tests/components/mailbox/test_init.py +++ b/tests/components/mailbox/test_init.py @@ -1,4 +1,5 @@ """The tests for the mailbox component.""" + from datetime import datetime from hashlib import sha1 from http import HTTPStatus @@ -48,7 +49,7 @@ class TestMailbox(mailbox.Mailbox): """Initialize Test mailbox.""" super().__init__(hass, name) self._messages: dict[str, dict[str, Any]] = {} - for idx in range(0, 10): + for idx in range(10): msg = _create_message(idx) msgsha = msg["sha"] self._messages[msgsha] = msg diff --git a/tests/components/mailgun/test_init.py b/tests/components/mailgun/test_init.py index 03bbfd80287..a36051bd102 100644 --- a/tests/components/mailgun/test_init.py +++ b/tests/components/mailgun/test_init.py @@ -1,4 +1,5 @@ """Test the init file of Mailgun.""" + import hashlib import hmac diff --git a/tests/components/manual/test_alarm_control_panel.py b/tests/components/manual/test_alarm_control_panel.py index 14e28b0999d..7a264134320 100644 --- a/tests/components/manual/test_alarm_control_panel.py +++ b/tests/components/manual/test_alarm_control_panel.py @@ -1,4 +1,5 @@ """The tests for the manual Alarm Control Panel component.""" + from datetime import timedelta from unittest.mock import MagicMock, patch diff --git a/tests/components/manual_mqtt/test_alarm_control_panel.py b/tests/components/manual_mqtt/test_alarm_control_panel.py index 0df1114bf30..5c2704db937 100644 --- a/tests/components/manual_mqtt/test_alarm_control_panel.py +++ b/tests/components/manual_mqtt/test_alarm_control_panel.py @@ -1,4 +1,5 @@ """The tests for the manual_mqtt Alarm Control Panel component.""" + from datetime import timedelta from unittest.mock import patch diff --git a/tests/components/map/__init__.py b/tests/components/map/__init__.py new file mode 100644 index 00000000000..142afc0d5c9 --- /dev/null +++ b/tests/components/map/__init__.py @@ -0,0 +1 @@ +"""Tests for Map.""" diff --git a/tests/components/map/test_init.py b/tests/components/map/test_init.py new file mode 100644 index 00000000000..6d79afefab3 --- /dev/null +++ b/tests/components/map/test_init.py @@ -0,0 +1,116 @@ +"""Test the Map initialization.""" + +from collections.abc import Generator +from typing import Any +from unittest.mock import MagicMock, patch + +import pytest + +from homeassistant.components.map import DOMAIN +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant +from homeassistant.helpers import issue_registry as ir +from homeassistant.setup import async_setup_component + +from tests.common import MockModule, mock_integration + + +@pytest.fixture +def mock_onboarding_not_done() -> Generator[MagicMock, None, None]: + """Mock that Home Assistant is currently onboarding.""" + with patch( + "homeassistant.components.onboarding.async_is_onboarded", + return_value=False, + ) as mock_onboarding: + yield mock_onboarding + + +@pytest.fixture +def mock_onboarding_done() -> Generator[MagicMock, None, None]: + """Mock that Home Assistant is currently onboarding.""" + with patch( + "homeassistant.components.onboarding.async_is_onboarded", + return_value=True, + ) as mock_onboarding: + yield mock_onboarding + + +@pytest.fixture +def mock_create_map_dashboard() -> Generator[MagicMock, None, None]: + """Mock the create map dashboard function.""" + with patch( + "homeassistant.components.map._create_map_dashboard", + ) as mock_create_map_dashboard: + yield mock_create_map_dashboard + + +async def test_create_dashboards_when_onboarded( + hass: HomeAssistant, + hass_storage: dict[str, Any], + mock_onboarding_done, + mock_create_map_dashboard, +) -> None: + """Test we create map dashboard when onboarded.""" + # Mock the lovelace integration to prevent it from creating a map dashboard + mock_integration(hass, MockModule("lovelace")) + + assert await async_setup_component(hass, DOMAIN, {}) + + mock_create_map_dashboard.assert_called_once() + assert hass_storage[DOMAIN]["data"] == {"migrated": True} + + +async def test_create_dashboards_once_when_onboarded( + hass: HomeAssistant, + hass_storage: dict[str, Any], + mock_onboarding_done, + mock_create_map_dashboard, +) -> None: + """Test we create map dashboard once when onboarded.""" + hass_storage[DOMAIN] = { + "version": 1, + "minor_version": 1, + "key": "map", + "data": {"migrated": True}, + } + + # Mock the lovelace integration to prevent it from creating a map dashboard + mock_integration(hass, MockModule("lovelace")) + + assert await async_setup_component(hass, DOMAIN, {}) + + mock_create_map_dashboard.assert_not_called() + assert hass_storage[DOMAIN]["data"] == {"migrated": True} + + +async def test_create_dashboards_when_not_onboarded( + hass: HomeAssistant, + hass_storage: dict[str, Any], + mock_onboarding_not_done, + mock_create_map_dashboard, +) -> None: + """Test we do not create map dashboard when not onboarded.""" + # Mock the lovelace integration to prevent it from creating a map dashboard + mock_integration(hass, MockModule("lovelace")) + + assert await async_setup_component(hass, DOMAIN, {}) + + mock_create_map_dashboard.assert_not_called() + assert hass_storage[DOMAIN]["data"] == {"migrated": True} + + +async def test_create_issue_when_not_manually_configured(hass: HomeAssistant) -> None: + """Test creating issue registry issues.""" + assert await async_setup_component(hass, DOMAIN, {}) + + issue_registry = ir.async_get(hass) + assert not issue_registry.async_get_issue( + HOMEASSISTANT_DOMAIN, "deprecated_yaml_map" + ) + + +async def test_create_issue_when_manually_configured(hass: HomeAssistant) -> None: + """Test creating issue registry issues.""" + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + + issue_registry = ir.async_get(hass) + assert issue_registry.async_get_issue(HOMEASSISTANT_DOMAIN, "deprecated_yaml_map") diff --git a/tests/components/marytts/test_tts.py b/tests/components/marytts/test_tts.py index 474d2f19faf..953c66f58d1 100644 --- a/tests/components/marytts/test_tts.py +++ b/tests/components/marytts/test_tts.py @@ -1,4 +1,5 @@ """The tests for the MaryTTS speech platform.""" + from http import HTTPStatus import io from unittest.mock import patch diff --git a/tests/components/matrix/conftest.py b/tests/components/matrix/conftest.py index 3e7d4833d6f..b3fefe3ac67 100644 --- a/tests/components/matrix/conftest.py +++ b/tests/components/matrix/conftest.py @@ -1,4 +1,5 @@ """Define fixtures available for all tests.""" + from __future__ import annotations import re diff --git a/tests/components/matrix/test_commands.py b/tests/components/matrix/test_commands.py index cbf85ccc597..f71ec22e794 100644 --- a/tests/components/matrix/test_commands.py +++ b/tests/components/matrix/test_commands.py @@ -1,4 +1,5 @@ """Test MatrixBot's ability to parse and respond to commands in matrix rooms.""" + from functools import partial from itertools import chain from typing import Any diff --git a/tests/components/matter/common.py b/tests/components/matter/common.py index d5093367db5..7878ac564fd 100644 --- a/tests/components/matter/common.py +++ b/tests/components/matter/common.py @@ -1,4 +1,5 @@ """Provide common test tools.""" + from __future__ import annotations from functools import cache diff --git a/tests/components/matter/conftest.py b/tests/components/matter/conftest.py index 03443e4c4b9..a04bf68d28a 100644 --- a/tests/components/matter/conftest.py +++ b/tests/components/matter/conftest.py @@ -1,4 +1,5 @@ """Provide common fixtures.""" + from __future__ import annotations import asyncio diff --git a/tests/components/matter/fixtures/nodes/room-airconditioner.json b/tests/components/matter/fixtures/nodes/room-airconditioner.json new file mode 100644 index 00000000000..11c29b0d8f4 --- /dev/null +++ b/tests/components/matter/fixtures/nodes/room-airconditioner.json @@ -0,0 +1,256 @@ +{ + "node_id": 36, + "date_commissioned": "2024-03-27T17:31:23.745932", + "last_interview": "2024-03-27T17:31:23.745939", + "interview_version": 6, + "available": true, + "is_bridge": false, + "attributes": { + "0/29/0": [ + { + "0": 22, + "1": 1 + } + ], + "0/29/1": [29, 31, 40, 48, 49, 51, 60, 62, 63], + "0/29/2": [], + "0/29/3": [1, 2], + "0/29/65532": 0, + "0/29/65533": 2, + "0/29/65528": [], + "0/29/65529": [], + "0/29/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533], + "0/31/0": [ + { + "254": 5 + }, + { + "1": 5, + "2": 2, + "3": [112233], + "4": null, + "254": 6 + } + ], + "0/31/1": [], + "0/31/2": 4, + "0/31/3": 3, + "0/31/4": 4, + "0/31/65532": 0, + "0/31/65533": 1, + "0/31/65528": [], + "0/31/65529": [], + "0/31/65531": [0, 1, 2, 3, 4, 65528, 65529, 65531, 65532, 65533], + "0/40/0": 17, + "0/40/1": "TEST_VENDOR", + "0/40/2": 65521, + "0/40/3": "Room AirConditioner", + "0/40/4": 32774, + "0/40/5": "", + "0/40/6": "**REDACTED**", + "0/40/7": 0, + "0/40/8": "TEST_VERSION", + "0/40/9": 1, + "0/40/10": "1.0", + "0/40/11": "20200101", + "0/40/12": "", + "0/40/13": "", + "0/40/14": "", + "0/40/15": "TEST_SN", + "0/40/16": false, + "0/40/18": "E47F334E22A56610", + "0/40/19": { + "0": 3, + "1": 3 + }, + "0/40/65532": 0, + "0/40/65533": 1, + "0/40/65528": [], + "0/40/65529": [], + "0/40/65531": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 19, 65528, + 65529, 65531, 65532, 65533 + ], + "0/48/0": 0, + "0/48/1": { + "0": 60, + "1": 900 + }, + "0/48/2": 0, + "0/48/3": 0, + "0/48/4": true, + "0/48/65532": 0, + "0/48/65533": 1, + "0/48/65528": [1, 3, 5], + "0/48/65529": [0, 2, 4], + "0/48/65531": [0, 1, 2, 3, 4, 65528, 65529, 65531, 65532, 65533], + "0/49/0": 0, + "0/49/1": null, + "0/49/2": 0, + "0/49/3": 0, + "0/49/4": false, + "0/49/5": 0, + "0/49/6": "", + "0/49/7": 0, + "0/49/65532": 2, + "0/49/65533": 1, + "0/49/65528": [1, 5, 7], + "0/49/65529": [0, 2, 3, 4, 6, 8], + "0/49/65531": [0, 1, 2, 3, 4, 5, 6, 7, 65528, 65529, 65531, 65532, 65533], + "0/51/0": [], + "0/51/1": 0, + "0/51/8": false, + "0/51/65532": 0, + "0/51/65533": 1, + "0/51/65528": [], + "0/51/65529": [], + "0/51/65531": [0, 1, 8, 65528, 65529, 65531, 65532, 65533], + "0/60/0": 0, + "0/60/1": null, + "0/60/2": null, + "0/60/65532": 0, + "0/60/65533": 1, + "0/60/65528": [], + "0/60/65529": [0, 1, 2], + "0/60/65531": [0, 1, 2, 65528, 65529, 65531, 65532, 65533], + "0/62/0": [ + { + "254": 5 + }, + { + "1": "FTABAQEkAgE3AyQTAhgmBIAigScmBYAlTTo3BiQVAiQRJBgkBwEkCAEwCUEE7pKHHHlljFuw2MAQJFOAzVR5tPPIXOjxHrLr7el8KqThQ6CuCFwdmNztUaIQgBcPZm6QRoEn6OGoFoAG8vB0KTcKNQEoARgkAgE2AwQCBAEYMAQUEvPPXEC80Bhik9ZDF3HK0Jo0RG0wBRQ2kjqIaJL5W4CHyhTHPUFcjBrNmxgwC0BJN+cSZw9fkFlIZGzsfS4WYFxzouEZ6LXLjqJXqwhi6uoQqoEhHPITp6sQ8u1ZF7OuQ35q0tZBwt84ZvAo+i59GA==", + "2": "FTABAQEkAgE3AyQUARgmBIAigScmBYAlTTo3BiQTAhgkBwEkCAEwCUEEB0u1A8srBwhdMy9S5+W8C38qv6l9JxhOaVO1E8f3FHDpv6eTSEDWXvUKEOxZcce5cGUF/9tdW2z5M+pwjt2B9jcKNQEpARgkAmAwBBQ2kjqIaJL5W4CHyhTHPUFcjBrNmzAFFJOvH2V2J30vUkl3ZbhqhwBP2wVXGDALQJHZ9heIDcBg2DGc2b18rirq/5aZ2rsyP9BAE1zeTqSYj/pqKyeMS+hCx69jOqh/eAeDpeAzvL7JmKVLB0JLV1sY", + "254": 6 + } + ], + "0/62/1": [ + { + "1": "BER19ZLOakFRLvKKC9VsWzN+xv5V5yHHBFdX7ip/cNhnzVfnaNLLHKGU/DtcNZtU/YH+8kUcWKYvknk1TCcrG4k=", + "2": 24582, + "3": 9865010379846957597, + "4": 3118002441518404838, + "5": "", + "254": 5 + }, + { + "1": "BJUrvCXfXiwdfapIXt1qCtJoem+s2gZJ2KBDQZcPVP1cAYECu6Fjjz2MhMy6OW8ASGmWuke+YavIzIZWYEd6BJU=", + "2": 4939, + "3": 2, + "4": 36, + "5": "", + "254": 6 + } + ], + "0/62/2": 5, + "0/62/3": 2, + "0/62/4": [ + "FTABAQEkAgE3AycU3rGzlMtTrxYYJgQAus0sJgUAwGVSNwYnFN6xs5TLU68WGCQHASQIATAJQQREdfWSzmpBUS7yigvVbFszfsb+VechxwRXV+4qf3DYZ81X52jSyxyhlPw7XDWbVP2B/vJFHFimL5J5NUwnKxuJNwo1ASkBGCQCYDAEFMurIH6818tAIcTnwEZO5c+1WAH8MAUUy6sgfrzXy0AhxOfARk7lz7VYAfwYMAtAM2db17wMsM+JMtR4c2Iaz8nHLI4mVbsPGILOBujrzguB2C7p8Q9x8Cw0NgJP7hDV52F9j7IfHjO37aXZA4LqqBg=", + "FTABAQEkAgE3AyQUARgmBIAigScmBYAlTTo3BiQUARgkBwEkCAEwCUEElSu8Jd9eLB19qkhe3WoK0mh6b6zaBknYoENBlw9U/VwBgQK7oWOPPYyEzLo5bwBIaZa6R75hq8jMhlZgR3oElTcKNQEpARgkAmAwBBSTrx9ldid9L1JJd2W4aocAT9sFVzAFFJOvH2V2J30vUkl3ZbhqhwBP2wVXGDALQPMYkhQcsrqT5v1vgN1LXJr9skDJ6nnuG0QWfs8SVODLGjU73iO1aQVq+Ir5et9RTD/4VrfnI63DW9RA0N+qgCkY" + ], + "0/62/5": 6, + "0/62/65532": 0, + "0/62/65533": 1, + "0/62/65528": [1, 3, 5, 8], + "0/62/65529": [0, 2, 4, 6, 7, 9, 10, 11], + "0/62/65531": [0, 1, 2, 3, 4, 5, 65528, 65529, 65531, 65532, 65533], + "0/63/0": [], + "0/63/1": [], + "0/63/2": 4, + "0/63/3": 3, + "0/63/65532": 0, + "0/63/65533": 2, + "0/63/65528": [2, 5], + "0/63/65529": [0, 1, 3, 4], + "0/63/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533], + "1/3/0": 0, + "1/3/1": 0, + "1/3/65532": 0, + "1/3/65533": 4, + "1/3/65528": [], + "1/3/65529": [0, 64], + "1/3/65531": [0, 1, 65528, 65529, 65531, 65532, 65533], + "1/6/0": false, + "1/6/65532": 0, + "1/6/65533": 5, + "1/6/65528": [], + "1/6/65529": [0, 1, 2], + "1/6/65531": [0, 65528, 65529, 65531, 65532, 65533], + "1/29/0": [ + { + "0": 114, + "1": 1 + } + ], + "1/29/1": [3, 6, 29, 513, 514], + "1/29/2": [], + "1/29/3": [2], + "1/29/65532": 0, + "1/29/65533": 2, + "1/29/65528": [], + "1/29/65529": [], + "1/29/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533], + "1/513/0": 2000, + "1/513/3": 1600, + "1/513/4": 3200, + "1/513/5": 1600, + "1/513/6": 3200, + "1/513/17": 2600, + "1/513/18": 2000, + "1/513/25": 0, + "1/513/27": 4, + "1/513/28": 1, + "1/513/65532": 35, + "1/513/65533": 6, + "1/513/65528": [], + "1/513/65529": [0], + "1/513/65531": [ + 0, 3, 4, 5, 6, 17, 18, 25, 27, 28, 65528, 65529, 65531, 65532, 65533 + ], + "1/514/0": 0, + "1/514/1": 2, + "1/514/2": 0, + "1/514/3": 0, + "1/514/4": 3, + "1/514/5": 0, + "1/514/6": 0, + "1/514/9": 1, + "1/514/10": 0, + "1/514/65532": 11, + "1/514/65533": 4, + "1/514/65528": [], + "1/514/65529": [], + "1/514/65531": [ + 0, 1, 2, 3, 4, 5, 6, 9, 10, 65528, 65529, 65531, 65532, 65533 + ], + "2/3/0": 0, + "2/3/1": 0, + "2/3/65532": 0, + "2/3/65533": 4, + "2/3/65528": [], + "2/3/65529": [0, 64], + "2/3/65531": [0, 1, 65528, 65529, 65531, 65532, 65533], + "2/29/0": [ + { + "0": 770, + "1": 1 + } + ], + "2/29/1": [3, 29, 1026], + "2/29/2": [], + "2/29/3": [], + "2/29/65532": 0, + "2/29/65533": 2, + "2/29/65528": [], + "2/29/65529": [], + "2/29/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533], + "2/1026/0": 0, + "2/1026/1": -500, + "2/1026/2": 6000, + "2/1026/65532": 0, + "2/1026/65533": 1, + "2/1026/65528": [], + "2/1026/65529": [], + "2/1026/65531": [0, 1, 2, 65528, 65529, 65531, 65532, 65533] + }, + "attribute_subscriptions": [] +} diff --git a/tests/components/matter/test_adapter.py b/tests/components/matter/test_adapter.py index 0cc3e360ab6..5f6c48dfcc6 100644 --- a/tests/components/matter/test_adapter.py +++ b/tests/components/matter/test_adapter.py @@ -1,4 +1,5 @@ """Test the adapter.""" + from __future__ import annotations from unittest.mock import MagicMock @@ -55,6 +56,7 @@ async def test_device_registry_single_node_device( assert entry.model == "Mock Light" assert entry.hw_version == "v1.0" assert entry.sw_version == "v1.0" + assert entry.serial_number == "12345678" # This tests needs to be adjusted to remove lingering tasks @@ -83,6 +85,7 @@ async def test_device_registry_single_node_device_alt( # test serial id NOT present as additional identifier assert (DOMAIN, "serial_TEST_SN") not in entry.identifiers + assert entry.serial_number is None @pytest.mark.skip("Waiting for a new test fixture") diff --git a/tests/components/matter/test_api.py b/tests/components/matter/test_api.py index 8e463800f98..b47c014f6b2 100644 --- a/tests/components/matter/test_api.py +++ b/tests/components/matter/test_api.py @@ -1,4 +1,5 @@ """Test the api module.""" + from unittest.mock import AsyncMock, MagicMock, call from matter_server.client.models.node import ( diff --git a/tests/components/matter/test_binary_sensor.py b/tests/components/matter/test_binary_sensor.py index e231012f90d..97a22d6dc98 100644 --- a/tests/components/matter/test_binary_sensor.py +++ b/tests/components/matter/test_binary_sensor.py @@ -1,4 +1,5 @@ """Test Matter binary sensors.""" + from collections.abc import Generator from unittest.mock import MagicMock, patch diff --git a/tests/components/matter/test_climate.py b/tests/components/matter/test_climate.py index 81d210ed579..de4626ef3d1 100644 --- a/tests/components/matter/test_climate.py +++ b/tests/components/matter/test_climate.py @@ -1,4 +1,5 @@ """Test Matter locks.""" + from unittest.mock import MagicMock, call from chip.clusters import Objects as clusters @@ -24,6 +25,16 @@ async def thermostat_fixture( return await setup_integration_with_node_fixture(hass, "thermostat", matter_client) +@pytest.fixture(name="room_airconditioner") +async def room_airconditioner( + hass: HomeAssistant, matter_client: MagicMock +) -> MatterNode: + """Fixture for a room air conditioner node.""" + return await setup_integration_with_node_fixture( + hass, "room-airconditioner", matter_client + ) + + # This tests needs to be adjusted to remove lingering tasks @pytest.mark.parametrize("expected_lingering_tasks", [True]) async def test_thermostat( @@ -386,3 +397,18 @@ async def test_thermostat( clusters.Thermostat.Enums.SetpointAdjustMode.kCool, -40 ), ) + + +# This tests needs to be adjusted to remove lingering tasks +@pytest.mark.parametrize("expected_lingering_tasks", [True]) +async def test_room_airconditioner( + hass: HomeAssistant, + matter_client: MagicMock, + room_airconditioner: MatterNode, +) -> None: + """Test if a climate entity is created for a Room Airconditioner device.""" + state = hass.states.get("climate.room_airconditioner") + assert state + assert state.attributes["current_temperature"] == 20 + assert state.attributes["min_temp"] == 16 + assert state.attributes["max_temp"] == 32 diff --git a/tests/components/matter/test_config_flow.py b/tests/components/matter/test_config_flow.py index eddf6506bfd..e690844c228 100644 --- a/tests/components/matter/test_config_flow.py +++ b/tests/components/matter/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Matter config flow.""" + from __future__ import annotations from collections.abc import Generator diff --git a/tests/components/matter/test_cover.py b/tests/components/matter/test_cover.py index d409983307f..ff6e933a1ab 100644 --- a/tests/components/matter/test_cover.py +++ b/tests/components/matter/test_cover.py @@ -1,4 +1,5 @@ """Test Matter covers.""" + from math import floor from unittest.mock import MagicMock, call diff --git a/tests/components/matter/test_diagnostics.py b/tests/components/matter/test_diagnostics.py index c14eb93f24c..6863619e145 100644 --- a/tests/components/matter/test_diagnostics.py +++ b/tests/components/matter/test_diagnostics.py @@ -1,4 +1,5 @@ """Test the Matter diagnostics platform.""" + from __future__ import annotations import json diff --git a/tests/components/matter/test_door_lock.py b/tests/components/matter/test_door_lock.py index 51d48cddba7..a44b5929f65 100644 --- a/tests/components/matter/test_door_lock.py +++ b/tests/components/matter/test_door_lock.py @@ -1,4 +1,5 @@ """Test Matter locks.""" + from unittest.mock import MagicMock, call from chip.clusters import Objects as clusters diff --git a/tests/components/matter/test_event.py b/tests/components/matter/test_event.py index 0aa9385a74c..2bdcfb6adb7 100644 --- a/tests/components/matter/test_event.py +++ b/tests/components/matter/test_event.py @@ -1,4 +1,5 @@ """Test Matter Event entities.""" + from unittest.mock import MagicMock from matter_server.client.models.node import MatterNode diff --git a/tests/components/matter/test_helpers.py b/tests/components/matter/test_helpers.py index 61988a37122..a4b5e165a93 100644 --- a/tests/components/matter/test_helpers.py +++ b/tests/components/matter/test_helpers.py @@ -1,4 +1,5 @@ """Test the Matter helpers.""" + from __future__ import annotations from unittest.mock import MagicMock diff --git a/tests/components/matter/test_init.py b/tests/components/matter/test_init.py index 2286249bd5d..327e73dd4de 100644 --- a/tests/components/matter/test_init.py +++ b/tests/components/matter/test_init.py @@ -1,4 +1,5 @@ """Test the Matter integration init.""" + from __future__ import annotations import asyncio diff --git a/tests/components/matter/test_light.py b/tests/components/matter/test_light.py index 0376a902f32..9c3c2610d92 100644 --- a/tests/components/matter/test_light.py +++ b/tests/components/matter/test_light.py @@ -1,4 +1,5 @@ """Test Matter lights.""" + from unittest.mock import MagicMock, call from chip.clusters import Objects as clusters diff --git a/tests/components/matter/test_sensor.py b/tests/components/matter/test_sensor.py index 579dd7d94c5..c8af0647d31 100644 --- a/tests/components/matter/test_sensor.py +++ b/tests/components/matter/test_sensor.py @@ -1,4 +1,5 @@ """Test Matter sensors.""" + from datetime import UTC, datetime, timedelta from unittest.mock import MagicMock, patch diff --git a/tests/components/matter/test_switch.py b/tests/components/matter/test_switch.py index ac03d731ee1..5fc23fa7b34 100644 --- a/tests/components/matter/test_switch.py +++ b/tests/components/matter/test_switch.py @@ -1,4 +1,5 @@ """Test Matter switches.""" + from unittest.mock import MagicMock, call from chip.clusters import Objects as clusters diff --git a/tests/components/maxcube/conftest.py b/tests/components/maxcube/conftest.py index f0dd12eb6c6..82a852a5201 100644 --- a/tests/components/maxcube/conftest.py +++ b/tests/components/maxcube/conftest.py @@ -1,4 +1,5 @@ """Tests for EQ3 Max! component.""" + from unittest.mock import create_autospec, patch from maxcube.device import MAX_DEVICE_MODE_AUTOMATIC, MAX_DEVICE_MODE_MANUAL diff --git a/tests/components/maxcube/test_maxcube_binary_sensor.py b/tests/components/maxcube/test_maxcube_binary_sensor.py index 0c73c548211..32ec4e92ee1 100644 --- a/tests/components/maxcube/test_maxcube_binary_sensor.py +++ b/tests/components/maxcube/test_maxcube_binary_sensor.py @@ -1,4 +1,5 @@ """Test EQ3 Max! Window Shutters.""" + from datetime import timedelta from maxcube.cube import MaxCube @@ -42,7 +43,7 @@ async def test_window_shuttler( windowshutter.is_open = False async_fire_time_changed(hass, utcnow() + timedelta(minutes=5)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(ENTITY_ID) assert state.state == STATE_OFF @@ -67,12 +68,12 @@ async def test_window_shuttler_battery( windowshutter.battery = 1 # maxcube-api MAX_DEVICE_BATTERY_LOW async_fire_time_changed(hass, utcnow() + timedelta(minutes=5)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(BATTERY_ENTITY_ID) assert state.state == STATE_ON # on means low windowshutter.battery = 0 # maxcube-api MAX_DEVICE_BATTERY_OK async_fire_time_changed(hass, utcnow() + timedelta(minutes=5)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(BATTERY_ENTITY_ID) assert state.state == STATE_OFF # off means normal diff --git a/tests/components/maxcube/test_maxcube_climate.py b/tests/components/maxcube/test_maxcube_climate.py index 76ab214f3ac..e1e7dc57c47 100644 --- a/tests/components/maxcube/test_maxcube_climate.py +++ b/tests/components/maxcube/test_maxcube_climate.py @@ -1,4 +1,5 @@ """Test EQ3 Max! Thermostats.""" + from datetime import timedelta from maxcube.cube import MaxCube @@ -139,7 +140,7 @@ async def test_thermostat_set_hvac_mode_off( thermostat.valve_position = 0 async_fire_time_changed(hass, utcnow() + timedelta(minutes=5)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(ENTITY_ID) assert state.state == HVACMode.OFF @@ -167,8 +168,8 @@ async def test_thermostat_set_hvac_mode_heat( thermostat.mode = MAX_DEVICE_MODE_MANUAL async_fire_time_changed(hass, utcnow() + timedelta(minutes=5)) - await hass.async_block_till_done() - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(ENTITY_ID) assert state.state == HVACMode.HEAT @@ -203,7 +204,7 @@ async def test_thermostat_set_temperature( thermostat.valve_position = 0 async_fire_time_changed(hass, utcnow() + timedelta(minutes=5)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(ENTITY_ID) assert state.state == HVACMode.AUTO @@ -247,7 +248,7 @@ async def test_thermostat_set_preset_on( thermostat.target_temperature = ON_TEMPERATURE async_fire_time_changed(hass, utcnow() + timedelta(minutes=5)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(ENTITY_ID) assert state.state == HVACMode.HEAT @@ -272,7 +273,7 @@ async def test_thermostat_set_preset_comfort( thermostat.target_temperature = thermostat.comfort_temperature async_fire_time_changed(hass, utcnow() + timedelta(minutes=5)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(ENTITY_ID) assert state.state == HVACMode.HEAT @@ -297,7 +298,7 @@ async def test_thermostat_set_preset_eco( thermostat.target_temperature = thermostat.eco_temperature async_fire_time_changed(hass, utcnow() + timedelta(minutes=5)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(ENTITY_ID) assert state.state == HVACMode.HEAT @@ -322,7 +323,7 @@ async def test_thermostat_set_preset_away( thermostat.target_temperature = thermostat.eco_temperature async_fire_time_changed(hass, utcnow() + timedelta(minutes=5)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(ENTITY_ID) assert state.state == HVACMode.HEAT @@ -347,7 +348,7 @@ async def test_thermostat_set_preset_boost( thermostat.target_temperature = thermostat.eco_temperature async_fire_time_changed(hass, utcnow() + timedelta(minutes=5)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(ENTITY_ID) assert state.state == HVACMode.AUTO @@ -400,7 +401,7 @@ async def test_wallthermostat_set_hvac_mode_heat( wallthermostat.target_temperature = MIN_TEMPERATURE async_fire_time_changed(hass, utcnow() + timedelta(minutes=5)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(WALL_ENTITY_ID) assert state.state == HVACMode.HEAT @@ -424,7 +425,7 @@ async def test_wallthermostat_set_hvac_mode_auto( wallthermostat.target_temperature = 23.0 async_fire_time_changed(hass, utcnow() + timedelta(minutes=5)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(WALL_ENTITY_ID) assert state.state == HVACMode.AUTO diff --git a/tests/components/meater/test_config_flow.py b/tests/components/meater/test_config_flow.py index 66f304c2d6c..6d294259c01 100644 --- a/tests/components/meater/test_config_flow.py +++ b/tests/components/meater/test_config_flow.py @@ -1,4 +1,5 @@ """Define tests for the Meater config flow.""" + from unittest.mock import AsyncMock, patch from meater import AuthenticationError, ServiceUnavailableError diff --git a/tests/components/medcom_ble/__init__.py b/tests/components/medcom_ble/__init__.py index e38b8ce8f01..aa367b93a14 100644 --- a/tests/components/medcom_ble/__init__.py +++ b/tests/components/medcom_ble/__init__.py @@ -1,4 +1,5 @@ """Tests for the Medcom Inspector BLE integration.""" + from __future__ import annotations from unittest.mock import patch diff --git a/tests/components/medcom_ble/test_config_flow.py b/tests/components/medcom_ble/test_config_flow.py index 620b6811757..ca75dfd80ab 100644 --- a/tests/components/medcom_ble/test_config_flow.py +++ b/tests/components/medcom_ble/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Medcom Inspector BLE config flow.""" + from unittest.mock import patch from bleak import BleakError @@ -34,14 +35,17 @@ async def test_bluetooth_discovery(hass: HomeAssistant) -> None: assert result["step_id"] == "bluetooth_confirm" assert result["description_placeholders"] == {"name": "InspectorBLE-D9A0"} - with patch_async_ble_device_from_address(MEDCOM_SERVICE_INFO), patch_medcom_ble( - MedcomBleDevice( - manufacturer="International Medcom", - model="Inspector BLE", - model_raw="Inspector-BLE", - name="Inspector BLE", - identifier="a0d95a570b00", - ) + with ( + patch_async_ble_device_from_address(MEDCOM_SERVICE_INFO), + patch_medcom_ble( + MedcomBleDevice( + manufacturer="International Medcom", + model="Inspector BLE", + model_raw="Inspector-BLE", + name="Inspector BLE", + identifier="a0d95a570b00", + ) + ), ): with patch_async_setup_entry(): result = await hass.config_entries.flow.async_configure( @@ -88,17 +92,21 @@ async def test_user_setup(hass: HomeAssistant) -> None: "a0:d9:5a:57:0b:00": "InspectorBLE-D9A0" } - with patch_async_ble_device_from_address(MEDCOM_SERVICE_INFO), patch_medcom_ble( - MedcomBleDevice( - manufacturer="International Medcom", - model="Inspector BLE", - model_raw="Inspector-BLE", - name="Inspector BLE", - identifier="a0d95a570b00", - ) - ), patch( - "homeassistant.components.medcom_ble.async_setup_entry", - return_value=True, + with ( + patch_async_ble_device_from_address(MEDCOM_SERVICE_INFO), + patch_medcom_ble( + MedcomBleDevice( + manufacturer="International Medcom", + model="Inspector BLE", + model_raw="Inspector-BLE", + name="Inspector BLE", + identifier="a0d95a570b00", + ) + ), + patch( + "homeassistant.components.medcom_ble.async_setup_entry", + return_value=True, + ), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_ADDRESS: "a0:d9:5a:57:0b:00"} @@ -177,8 +185,9 @@ async def test_user_setup_unknown_error(hass: HomeAssistant) -> None: assert result["errors"] is None assert result["data_schema"] is not None - with patch_async_ble_device_from_address(MEDCOM_SERVICE_INFO), patch_medcom_ble( - None, Exception() + with ( + patch_async_ble_device_from_address(MEDCOM_SERVICE_INFO), + patch_medcom_ble(None, Exception()), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_ADDRESS: "a0:d9:5a:57:0b:00"} @@ -207,8 +216,9 @@ async def test_user_setup_unable_to_connect(hass: HomeAssistant) -> None: "a0:d9:5a:57:0b:00": "InspectorBLE-D9A0" } - with patch_async_ble_device_from_address(MEDCOM_SERVICE_INFO), patch_medcom_ble( - side_effect=BleakError("An error") + with ( + patch_async_ble_device_from_address(MEDCOM_SERVICE_INFO), + patch_medcom_ble(side_effect=BleakError("An error")), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_ADDRESS: "a0:d9:5a:57:0b:00"} diff --git a/tests/components/media_extractor/__init__.py b/tests/components/media_extractor/__init__.py index d6faa60d3b4..7aac726501b 100644 --- a/tests/components/media_extractor/__init__.py +++ b/tests/components/media_extractor/__init__.py @@ -1,4 +1,5 @@ """The tests for Media Extractor integration.""" + from typing import Any from tests.common import load_json_object_fixture diff --git a/tests/components/media_extractor/conftest.py b/tests/components/media_extractor/conftest.py index 8c8a6d6fb8d..4b7411340ae 100644 --- a/tests/components/media_extractor/conftest.py +++ b/tests/components/media_extractor/conftest.py @@ -1,4 +1,5 @@ """The tests for Media Extractor integration.""" + from typing import Any from unittest.mock import patch diff --git a/tests/components/media_extractor/test_init.py b/tests/components/media_extractor/test_init.py index d32ad90d87c..e47f0ae1470 100644 --- a/tests/components/media_extractor/test_init.py +++ b/tests/components/media_extractor/test_init.py @@ -1,4 +1,5 @@ """The tests for Media Extractor integration.""" + import os import os.path from typing import Any @@ -189,12 +190,17 @@ async def test_query_error( ) -> None: """Test handling error with query.""" - with patch( - "homeassistant.components.media_extractor.YoutubeDL.extract_info", - return_value=load_json_object_fixture("media_extractor/youtube_1_info.json"), - ), patch( - "homeassistant.components.media_extractor.YoutubeDL.process_ie_result", - side_effect=DownloadError("Message"), + with ( + patch( + "homeassistant.components.media_extractor.YoutubeDL.extract_info", + return_value=load_json_object_fixture( + "media_extractor/youtube_1_info.json" + ), + ), + patch( + "homeassistant.components.media_extractor.YoutubeDL.process_ie_result", + side_effect=DownloadError("Message"), + ), ): await async_setup_component(hass, DOMAIN, empty_media_extractor_config) await hass.async_block_till_done() @@ -232,14 +238,13 @@ async def test_cookiefile_detection( if not os.path.exists(cookies_dir): os.makedirs(cookies_dir) - f = open(cookies_file, "w+", encoding="utf-8") - f.write( - """# Netscape HTTP Cookie File + with open(cookies_file, "w+", encoding="utf-8") as f: + f.write( + """# Netscape HTTP Cookie File - .youtube.com TRUE / TRUE 1701708706 GPS 1 - """ - ) - f.close() + .youtube.com TRUE / TRUE 1701708706 GPS 1 + """ + ) await hass.services.async_call( DOMAIN, diff --git a/tests/components/media_player/common.py b/tests/components/media_player/common.py index db2ef76b210..77076d903a6 100644 --- a/tests/components/media_player/common.py +++ b/tests/components/media_player/common.py @@ -3,6 +3,7 @@ All containing methods are legacy helpers that should not be used by new components. Instead call the service directly. """ + from homeassistant.components.media_player import ( ATTR_INPUT_SOURCE, ATTR_MEDIA_CONTENT_ID, diff --git a/tests/components/media_player/test_async_helpers.py b/tests/components/media_player/test_async_helpers.py index f3b70187f33..e3d89a9ca2e 100644 --- a/tests/components/media_player/test_async_helpers.py +++ b/tests/components/media_player/test_async_helpers.py @@ -1,4 +1,5 @@ """The tests for the Async Media player helper functions.""" + import pytest import homeassistant.components.media_player as mp diff --git a/tests/components/media_player/test_browse_media.py b/tests/components/media_player/test_browse_media.py index c7ce52eb12a..2b7e40923bf 100644 --- a/tests/components/media_player/test_browse_media.py +++ b/tests/components/media_player/test_browse_media.py @@ -1,4 +1,5 @@ """Test media browser helpers for media player.""" + from unittest.mock import Mock, patch import pytest @@ -56,9 +57,12 @@ async def test_process_play_media_url(hass: HomeAssistant, mock_sign_path) -> No async_process_play_media_url(hass, "http://192.168.123.123:8123/path") == "http://192.168.123.123:8123/path?authSig=bla" ) - with pytest.raises(HomeAssistantError), patch( - "homeassistant.components.media_player.browse_media.get_url", - side_effect=NoURLAvailableError, + with ( + pytest.raises(HomeAssistantError), + patch( + "homeassistant.components.media_player.browse_media.get_url", + side_effect=NoURLAvailableError, + ), ): async_process_play_media_url(hass, "/path") @@ -87,7 +91,7 @@ async def test_process_play_media_url(hass: HomeAssistant, mock_sign_path) -> No ) # Not changing a URL which is not absolute and does not start with / - async_process_play_media_url(hass, "hello") == "hello" + assert async_process_play_media_url(hass, "hello") == "hello" async def test_process_play_media_url_for_addon( diff --git a/tests/components/media_player/test_device_condition.py b/tests/components/media_player/test_device_condition.py index ea1f65eab95..3d020b01c3d 100644 --- a/tests/components/media_player/test_device_condition.py +++ b/tests/components/media_player/test_device_condition.py @@ -1,4 +1,5 @@ """The tests for Media player device conditions.""" + import pytest from pytest_unordered import unordered @@ -78,12 +79,12 @@ async def test_get_conditions( @pytest.mark.parametrize( ("hidden_by", "entity_category"), - ( + [ (RegistryEntryHider.INTEGRATION, None), (RegistryEntryHider.USER, None), (None, EntityCategory.CONFIG), (None, EntityCategory.DIAGNOSTIC), - ), + ], ) async def test_get_conditions_hidden_auxiliary( hass: HomeAssistant, diff --git a/tests/components/media_player/test_device_trigger.py b/tests/components/media_player/test_device_trigger.py index afc46c87cff..3f347918f3d 100644 --- a/tests/components/media_player/test_device_trigger.py +++ b/tests/components/media_player/test_device_trigger.py @@ -1,4 +1,5 @@ """The tests for Media player device triggers.""" + from datetime import timedelta import pytest @@ -86,12 +87,12 @@ async def test_get_triggers( @pytest.mark.parametrize( ("hidden_by", "entity_category"), - ( + [ (RegistryEntryHider.INTEGRATION, None), (RegistryEntryHider.USER, None), (None, EntityCategory.CONFIG), (None, EntityCategory.DIAGNOSTIC), - ), + ], ) async def test_get_triggers_hidden_auxiliary( hass: HomeAssistant, diff --git a/tests/components/media_player/test_init.py b/tests/components/media_player/test_init.py index d44ff28c772..436a9e3d05f 100644 --- a/tests/components/media_player/test_init.py +++ b/tests/components/media_player/test_init.py @@ -1,4 +1,5 @@ """Test the base functions of the media player.""" + from http import HTTPStatus from unittest.mock import patch @@ -239,14 +240,14 @@ async def test_group_members_available_when_off(hass: HomeAssistant) -> None: @pytest.mark.parametrize( ("input", "expected"), - ( + [ (True, MediaPlayerEnqueue.ADD), (False, MediaPlayerEnqueue.PLAY), ("play", MediaPlayerEnqueue.PLAY), ("next", MediaPlayerEnqueue.NEXT), ("add", MediaPlayerEnqueue.ADD), ("replace", MediaPlayerEnqueue.REPLACE), - ), + ], ) async def test_enqueue_rewrite(hass: HomeAssistant, input, expected) -> None: """Test that group_members are still available when media_player is off.""" diff --git a/tests/components/media_player/test_recorder.py b/tests/components/media_player/test_recorder.py index a04c7e85119..075e1d37f1a 100644 --- a/tests/components/media_player/test_recorder.py +++ b/tests/components/media_player/test_recorder.py @@ -1,4 +1,5 @@ """The tests for media_player recorder.""" + from __future__ import annotations from datetime import timedelta diff --git a/tests/components/media_player/test_reproduce_state.py b/tests/components/media_player/test_reproduce_state.py index 0ff930dbdbd..0759296ce35 100644 --- a/tests/components/media_player/test_reproduce_state.py +++ b/tests/components/media_player/test_reproduce_state.py @@ -1,4 +1,5 @@ """The tests for reproduction of state.""" + import pytest from homeassistant.components.media_player import ( diff --git a/tests/components/media_player/test_significant_change.py b/tests/components/media_player/test_significant_change.py index 233f133c342..e0635391042 100644 --- a/tests/components/media_player/test_significant_change.py +++ b/tests/components/media_player/test_significant_change.py @@ -1,4 +1,5 @@ """Test the Media Player significant change platform.""" + import pytest from homeassistant.components.media_player import ( diff --git a/tests/components/media_source/test_init.py b/tests/components/media_source/test_init.py index eecfe6cde6e..c37e418020b 100644 --- a/tests/components/media_source/test_init.py +++ b/tests/components/media_source/test_init.py @@ -1,4 +1,5 @@ """Test Media Source initialization.""" + from unittest.mock import Mock, patch import pytest diff --git a/tests/components/media_source/test_local_source.py b/tests/components/media_source/test_local_source.py index bc637caab80..9902aa689ae 100644 --- a/tests/components/media_source/test_local_source.py +++ b/tests/components/media_source/test_local_source.py @@ -1,4 +1,5 @@ """Test Local Media Source.""" + from collections.abc import AsyncGenerator from http import HTTPStatus import io diff --git a/tests/components/media_source/test_models.py b/tests/components/media_source/test_models.py index 35127e88798..12685e28d69 100644 --- a/tests/components/media_source/test_models.py +++ b/tests/components/media_source/test_models.py @@ -1,4 +1,5 @@ """Test Media Source model methods.""" + from homeassistant.components.media_player import MediaClass, MediaType from homeassistant.components.media_source import const, models diff --git a/tests/components/melcloud/test_atw_zone_sensor.py b/tests/components/melcloud/test_atw_zone_sensor.py index 7defe3277cb..5ffb6dd7ff5 100644 --- a/tests/components/melcloud/test_atw_zone_sensor.py +++ b/tests/components/melcloud/test_atw_zone_sensor.py @@ -1,4 +1,5 @@ """Test the MELCloud ATW zone sensor.""" + from unittest.mock import patch import pytest diff --git a/tests/components/melcloud/test_config_flow.py b/tests/components/melcloud/test_config_flow.py index 5ca44d4fe46..0a21a8747e3 100644 --- a/tests/components/melcloud/test_config_flow.py +++ b/tests/components/melcloud/test_config_flow.py @@ -1,4 +1,5 @@ """Test the MELCloud config flow.""" + from http import HTTPStatus from unittest.mock import patch diff --git a/tests/components/melissa/__init__.py b/tests/components/melissa/__init__.py index c4caf0fe671..870727eea8f 100644 --- a/tests/components/melissa/__init__.py +++ b/tests/components/melissa/__init__.py @@ -1 +1,12 @@ """Tests for the melissa component.""" + +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +VALID_CONFIG = {"melissa": {"username": "********", "password": "********"}} + + +async def setup_integration(hass: HomeAssistant) -> None: + """Set up the melissa integration in Home Assistant.""" + assert await async_setup_component(hass, "melissa", VALID_CONFIG) + await hass.async_block_till_done() diff --git a/tests/components/melissa/conftest.py b/tests/components/melissa/conftest.py new file mode 100644 index 00000000000..6a6781263b5 --- /dev/null +++ b/tests/components/melissa/conftest.py @@ -0,0 +1,47 @@ +"""Melissa conftest.""" + +from unittest.mock import AsyncMock, patch + +import pytest + +from tests.common import load_json_object_fixture + + +@pytest.fixture +async def mock_melissa(): + """Mock the Melissa API.""" + with patch( + "homeassistant.components.melissa.AsyncMelissa", autospec=True + ) as mock_client: + mock_client.return_value.async_connect = AsyncMock() + mock_client.return_value.async_fetch_devices.return_value = ( + load_json_object_fixture("fetch_devices.json", "melissa") + ) + mock_client.return_value.async_status.return_value = load_json_object_fixture( + "status.json", "melissa" + ) + mock_client.return_value.async_cur_settings.return_value = ( + load_json_object_fixture("cur_settings.json", "melissa") + ) + + mock_client.return_value.STATE_OFF = 0 + mock_client.return_value.STATE_ON = 1 + mock_client.return_value.STATE_IDLE = 2 + + mock_client.return_value.MODE_AUTO = 0 + mock_client.return_value.MODE_FAN = 1 + mock_client.return_value.MODE_HEAT = 2 + mock_client.return_value.MODE_COOL = 3 + mock_client.return_value.MODE_DRY = 4 + + mock_client.return_value.FAN_AUTO = 0 + mock_client.return_value.FAN_LOW = 1 + mock_client.return_value.FAN_MEDIUM = 2 + mock_client.return_value.FAN_HIGH = 3 + + mock_client.return_value.STATE = "state" + mock_client.return_value.MODE = "mode" + mock_client.return_value.FAN = "fan" + mock_client.return_value.TEMP = "temp" + mock_client.return_value.HUMIDITY = "humidity" + yield mock_client diff --git a/tests/components/melissa/snapshots/test_climate.ambr b/tests/components/melissa/snapshots/test_climate.ambr new file mode 100644 index 00000000000..5d0752ccffe --- /dev/null +++ b/tests/components/melissa/snapshots/test_climate.ambr @@ -0,0 +1,35 @@ +# serializer version: 1 +# name: test_setup_platform + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_humidity': 18.7, + 'current_temperature': 27.4, + 'fan_mode': 'low', + 'fan_modes': list([ + 'auto', + 'high', + 'medium', + 'low', + ]), + 'friendly_name': 'Melissa 12345678', + 'hvac_modes': list([ + , + , + , + , + , + ]), + 'max_temp': 30, + 'min_temp': 16, + 'supported_features': , + 'target_temp_step': 1, + 'temperature': 16, + }), + 'context': , + 'entity_id': 'climate.melissa_12345678', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'heat', + }) +# --- diff --git a/tests/components/melissa/test_climate.py b/tests/components/melissa/test_climate.py index dc2ca4391f1..ff59f925961 100644 --- a/tests/components/melissa/test_climate.py +++ b/tests/components/melissa/test_climate.py @@ -1,367 +1,46 @@ """Test for Melissa climate component.""" -import json -from unittest.mock import AsyncMock, Mock, patch + +from unittest.mock import AsyncMock + +from syrupy import SnapshotAssertion from homeassistant.components.climate import ( - FAN_HIGH, - FAN_LOW, - FAN_MEDIUM, - ClimateEntityFeature, - HVACMode, + DOMAIN as CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, ) -from homeassistant.components.melissa import DATA_MELISSA, climate as melissa -from homeassistant.components.melissa.climate import MelissaClimate -from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature +from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE from homeassistant.core import HomeAssistant +import homeassistant.helpers.entity_registry as er -from tests.common import load_fixture - -_SERIAL = "12345678" +from tests.components.melissa import setup_integration -def melissa_mock(): - """Use this to mock the melissa api.""" - api = Mock() - api.async_fetch_devices = AsyncMock( - return_value=json.loads(load_fixture("fetch_devices.json", "melissa")) - ) - api.async_status = AsyncMock( - return_value=json.loads(load_fixture("status.json", "melissa")) - ) - api.async_cur_settings = AsyncMock( - return_value=json.loads(load_fixture("cur_settings.json", "melissa")) - ) - - api.async_send = AsyncMock(return_value=True) - - api.STATE_OFF = 0 - api.STATE_ON = 1 - api.STATE_IDLE = 2 - - api.MODE_AUTO = 0 - api.MODE_FAN = 1 - api.MODE_HEAT = 2 - api.MODE_COOL = 3 - api.MODE_DRY = 4 - - api.FAN_AUTO = 0 - api.FAN_LOW = 1 - api.FAN_MEDIUM = 2 - api.FAN_HIGH = 3 - - api.STATE = "state" - api.MODE = "mode" - api.FAN = "fan" - api.TEMP = "temp" - return api - - -async def test_setup_platform(hass: HomeAssistant) -> None: +async def test_setup_platform( + hass: HomeAssistant, mock_melissa, snapshot: SnapshotAssertion +) -> None: """Test setup_platform.""" - with patch( - "homeassistant.components.melissa.climate.MelissaClimate" - ) as mocked_thermostat: - api = melissa_mock() - device = (await api.async_fetch_devices())[_SERIAL] - thermostat = mocked_thermostat(api, device["serial_number"], device) - thermostats = [thermostat] + await setup_integration(hass) - hass.data[DATA_MELISSA] = api - - config = {} - add_entities = Mock() - discovery_info = {} - - await melissa.async_setup_platform(hass, config, add_entities, discovery_info) - add_entities.assert_called_once_with(thermostats) + assert hass.states.get("climate.melissa_12345678") == snapshot -async def test_get_name(hass: HomeAssistant) -> None: - """Test name property.""" - with patch("homeassistant.components.melissa"): - api = melissa_mock() - device = (await api.async_fetch_devices())[_SERIAL] - thermostat = MelissaClimate(api, _SERIAL, device) - assert thermostat.name == "Melissa 12345678" +async def test_actions( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, + mock_melissa: AsyncMock, +) -> None: + """Test that the switch can be turned on and off.""" + await setup_integration(hass) + entity_id = "climate.melissa_12345678" -async def test_current_fan_mode(hass: HomeAssistant) -> None: - """Test current_fan_mode property.""" - with patch("homeassistant.components.melissa"): - api = melissa_mock() - device = (await api.async_fetch_devices())[_SERIAL] - thermostat = MelissaClimate(api, _SERIAL, device) - await thermostat.async_update() - assert thermostat.fan_mode == FAN_LOW + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: entity_id, ATTR_TEMPERATURE: 25}, + blocking=True, + ) + await hass.async_block_till_done() - thermostat._cur_settings = None - assert thermostat.fan_mode is None - - -async def test_current_temperature(hass: HomeAssistant) -> None: - """Test current temperature.""" - with patch("homeassistant.components.melissa"): - api = melissa_mock() - device = (await api.async_fetch_devices())[_SERIAL] - thermostat = MelissaClimate(api, _SERIAL, device) - assert thermostat.current_temperature == 27.4 - - -async def test_current_temperature_no_data(hass: HomeAssistant) -> None: - """Test current temperature without data.""" - with patch("homeassistant.components.melissa"): - api = melissa_mock() - device = (await api.async_fetch_devices())[_SERIAL] - thermostat = MelissaClimate(api, _SERIAL, device) - thermostat._data = None - assert thermostat.current_temperature is None - - -async def test_target_temperature_step(hass: HomeAssistant) -> None: - """Test current target_temperature_step.""" - with patch("homeassistant.components.melissa"): - api = melissa_mock() - device = (await api.async_fetch_devices())[_SERIAL] - thermostat = MelissaClimate(api, _SERIAL, device) - assert thermostat.target_temperature_step == 1 - - -async def test_current_operation(hass: HomeAssistant) -> None: - """Test current operation.""" - with patch("homeassistant.components.melissa"): - api = melissa_mock() - device = (await api.async_fetch_devices())[_SERIAL] - thermostat = MelissaClimate(api, _SERIAL, device) - await thermostat.async_update() - assert thermostat.state == HVACMode.HEAT - - thermostat._cur_settings = None - assert thermostat.hvac_action is None - - -async def test_operation_list(hass: HomeAssistant) -> None: - """Test the operation list.""" - with patch("homeassistant.components.melissa"): - api = melissa_mock() - device = (await api.async_fetch_devices())[_SERIAL] - thermostat = MelissaClimate(api, _SERIAL, device) - assert [ - HVACMode.HEAT, - HVACMode.COOL, - HVACMode.DRY, - HVACMode.FAN_ONLY, - HVACMode.OFF, - ] == thermostat.hvac_modes - - -async def test_fan_modes(hass: HomeAssistant) -> None: - """Test the fan list.""" - with patch("homeassistant.components.melissa"): - api = melissa_mock() - device = (await api.async_fetch_devices())[_SERIAL] - thermostat = MelissaClimate(api, _SERIAL, device) - assert ["auto", FAN_HIGH, FAN_MEDIUM, FAN_LOW] == thermostat.fan_modes - - -async def test_target_temperature(hass: HomeAssistant) -> None: - """Test target temperature.""" - with patch("homeassistant.components.melissa"): - api = melissa_mock() - device = (await api.async_fetch_devices())[_SERIAL] - thermostat = MelissaClimate(api, _SERIAL, device) - await thermostat.async_update() - assert thermostat.target_temperature == 16 - - thermostat._cur_settings = None - assert thermostat.target_temperature is None - - -async def test_state(hass: HomeAssistant) -> None: - """Test state.""" - with patch("homeassistant.components.melissa"): - api = melissa_mock() - device = (await api.async_fetch_devices())[_SERIAL] - thermostat = MelissaClimate(api, _SERIAL, device) - await thermostat.async_update() - assert thermostat.state == HVACMode.HEAT - - thermostat._cur_settings = None - assert thermostat.state is None - - -async def test_temperature_unit(hass: HomeAssistant) -> None: - """Test temperature unit.""" - with patch("homeassistant.components.melissa"): - api = melissa_mock() - device = (await api.async_fetch_devices())[_SERIAL] - thermostat = MelissaClimate(api, _SERIAL, device) - assert thermostat.temperature_unit == UnitOfTemperature.CELSIUS - - -async def test_min_temp(hass: HomeAssistant) -> None: - """Test min temp.""" - with patch("homeassistant.components.melissa"): - api = melissa_mock() - device = (await api.async_fetch_devices())[_SERIAL] - thermostat = MelissaClimate(api, _SERIAL, device) - assert thermostat.min_temp == 16 - - -async def test_max_temp(hass: HomeAssistant) -> None: - """Test max temp.""" - with patch("homeassistant.components.melissa"): - api = melissa_mock() - device = (await api.async_fetch_devices())[_SERIAL] - thermostat = MelissaClimate(api, _SERIAL, device) - assert thermostat.max_temp == 30 - - -async def test_supported_features(hass: HomeAssistant) -> None: - """Test supported_features property.""" - with patch("homeassistant.components.melissa"): - api = melissa_mock() - device = (await api.async_fetch_devices())[_SERIAL] - thermostat = MelissaClimate(api, _SERIAL, device) - features = ( - ClimateEntityFeature.TARGET_TEMPERATURE - | ClimateEntityFeature.FAN_MODE - | ClimateEntityFeature.TURN_OFF - | ClimateEntityFeature.TURN_ON - ) - assert thermostat.supported_features == features - - -async def test_set_temperature(hass: HomeAssistant) -> None: - """Test set_temperature.""" - with patch("homeassistant.components.melissa"): - api = melissa_mock() - device = (await api.async_fetch_devices())[_SERIAL] - thermostat = MelissaClimate(api, _SERIAL, device) - await thermostat.async_update() - await thermostat.async_set_temperature(**{ATTR_TEMPERATURE: 25}) - assert thermostat.target_temperature == 25 - - -async def test_fan_mode(hass: HomeAssistant) -> None: - """Test set_fan_mode.""" - with patch("homeassistant.components.melissa"): - api = melissa_mock() - device = (await api.async_fetch_devices())[_SERIAL] - thermostat = MelissaClimate(api, _SERIAL, device) - await thermostat.async_update() - await hass.async_block_till_done() - await thermostat.async_set_fan_mode(FAN_HIGH) - await hass.async_block_till_done() - assert thermostat.fan_mode == FAN_HIGH - - -async def test_set_operation_mode(hass: HomeAssistant) -> None: - """Test set_operation_mode.""" - with patch("homeassistant.components.melissa"): - api = melissa_mock() - device = (await api.async_fetch_devices())[_SERIAL] - thermostat = MelissaClimate(api, _SERIAL, device) - await thermostat.async_update() - await hass.async_block_till_done() - await thermostat.async_set_hvac_mode(HVACMode.COOL) - await hass.async_block_till_done() - assert thermostat.hvac_mode == HVACMode.COOL - - -async def test_send(hass: HomeAssistant) -> None: - """Test send.""" - with patch("homeassistant.components.melissa"): - api = melissa_mock() - device = (await api.async_fetch_devices())[_SERIAL] - thermostat = MelissaClimate(api, _SERIAL, device) - await thermostat.async_update() - await hass.async_block_till_done() - await thermostat.async_send({"fan": api.FAN_MEDIUM}) - await hass.async_block_till_done() - assert thermostat.fan_mode == FAN_MEDIUM - api.async_send.return_value = AsyncMock(return_value=False) - thermostat._cur_settings = None - await thermostat.async_send({"fan": api.FAN_LOW}) - await hass.async_block_till_done() - assert thermostat.fan_mode != FAN_LOW - assert thermostat._cur_settings is None - - -async def test_update(hass: HomeAssistant) -> None: - """Test update.""" - with patch( - "homeassistant.components.melissa.climate._LOGGER.warning" - ) as mocked_warning, patch("homeassistant.components.melissa"): - api = melissa_mock() - device = (await api.async_fetch_devices())[_SERIAL] - thermostat = MelissaClimate(api, _SERIAL, device) - await thermostat.async_update() - assert thermostat.fan_mode == FAN_LOW - assert thermostat.state == HVACMode.HEAT - api.async_status = AsyncMock(side_effect=KeyError("boom")) - await thermostat.async_update() - mocked_warning.assert_called_once_with( - "Unable to update entity %s", thermostat.entity_id - ) - - -async def test_melissa_op_to_hass(hass: HomeAssistant) -> None: - """Test for translate melissa operations to hass.""" - with patch("homeassistant.components.melissa"): - api = melissa_mock() - device = (await api.async_fetch_devices())[_SERIAL] - thermostat = MelissaClimate(api, _SERIAL, device) - assert thermostat.melissa_op_to_hass(1) == HVACMode.FAN_ONLY - assert thermostat.melissa_op_to_hass(2) == HVACMode.HEAT - assert thermostat.melissa_op_to_hass(3) == HVACMode.COOL - assert thermostat.melissa_op_to_hass(4) == HVACMode.DRY - assert thermostat.melissa_op_to_hass(5) is None - - -async def test_melissa_fan_to_hass(hass: HomeAssistant) -> None: - """Test for translate melissa fan state to hass.""" - with patch("homeassistant.components.melissa"): - api = melissa_mock() - device = (await api.async_fetch_devices())[_SERIAL] - thermostat = MelissaClimate(api, _SERIAL, device) - assert thermostat.melissa_fan_to_hass(0) == "auto" - assert thermostat.melissa_fan_to_hass(1) == FAN_LOW - assert thermostat.melissa_fan_to_hass(2) == FAN_MEDIUM - assert thermostat.melissa_fan_to_hass(3) == FAN_HIGH - assert thermostat.melissa_fan_to_hass(4) is None - - -async def test_hass_mode_to_melissa(hass: HomeAssistant) -> None: - """Test for hass operations to melssa.""" - with patch( - "homeassistant.components.melissa.climate._LOGGER.warning" - ) as mocked_warning, patch("homeassistant.components.melissa"): - api = melissa_mock() - device = (await api.async_fetch_devices())[_SERIAL] - thermostat = MelissaClimate(api, _SERIAL, device) - assert thermostat.hass_mode_to_melissa(HVACMode.FAN_ONLY) == 1 - assert thermostat.hass_mode_to_melissa(HVACMode.HEAT) == 2 - assert thermostat.hass_mode_to_melissa(HVACMode.COOL) == 3 - assert thermostat.hass_mode_to_melissa(HVACMode.DRY) == 4 - thermostat.hass_mode_to_melissa("test") - mocked_warning.assert_called_once_with( - "Melissa have no setting for %s mode", "test" - ) - - -async def test_hass_fan_to_melissa(hass: HomeAssistant) -> None: - """Test for translate melissa states to hass.""" - with patch( - "homeassistant.components.melissa.climate._LOGGER.warning" - ) as mocked_warning, patch("homeassistant.components.melissa"): - api = melissa_mock() - device = (await api.async_fetch_devices())[_SERIAL] - thermostat = MelissaClimate(api, _SERIAL, device) - assert thermostat.hass_fan_to_melissa("auto") == 0 - assert thermostat.hass_fan_to_melissa(FAN_LOW) == 1 - assert thermostat.hass_fan_to_melissa(FAN_MEDIUM) == 2 - assert thermostat.hass_fan_to_melissa(FAN_HIGH) == 3 - thermostat.hass_fan_to_melissa("test") - mocked_warning.assert_called_once_with( - "Melissa have no setting for %s fan mode", "test" - ) + assert len(mock_melissa.return_value.async_send.mock_calls) == 2 diff --git a/tests/components/melissa/test_init.py b/tests/components/melissa/test_init.py index 96f3a08bb11..d809f42e409 100644 --- a/tests/components/melissa/test_init.py +++ b/tests/components/melissa/test_init.py @@ -1,24 +1,12 @@ """The test for the Melissa Climate component.""" -from unittest.mock import AsyncMock, patch -from homeassistant.components import melissa from homeassistant.core import HomeAssistant -VALID_CONFIG = {"melissa": {"username": "********", "password": "********"}} +from tests.components.melissa import setup_integration -async def test_setup(hass: HomeAssistant) -> None: +async def test_setup(hass: HomeAssistant, mock_melissa) -> None: """Test setting up the Melissa component.""" - with patch("melissa.AsyncMelissa") as mocked_melissa, patch.object( - melissa, "async_load_platform" - ): - mocked_melissa.return_value.async_connect = AsyncMock() - await melissa.async_setup(hass, VALID_CONFIG) + await setup_integration(hass) - mocked_melissa.assert_called_with(username="********", password="********") - - assert melissa.DATA_MELISSA in hass.data - assert isinstance( - hass.data[melissa.DATA_MELISSA], - type(mocked_melissa.return_value), - ) + mock_melissa.assert_called_with(username="********", password="********") diff --git a/tests/components/melnor/conftest.py b/tests/components/melnor/conftest.py index 3e87a4e646f..361102f22e6 100644 --- a/tests/components/melnor/conftest.py +++ b/tests/components/melnor/conftest.py @@ -1,4 +1,5 @@ """Tests for the melnor integration.""" + from __future__ import annotations from collections.abc import Generator diff --git a/tests/components/melnor/test_config_flow.py b/tests/components/melnor/test_config_flow.py index bb0a017611f..ae4e7b84288 100644 --- a/tests/components/melnor/test_config_flow.py +++ b/tests/components/melnor/test_config_flow.py @@ -1,4 +1,5 @@ """Test the melnor config flow.""" + from unittest.mock import AsyncMock import pytest diff --git a/tests/components/melnor/test_number.py b/tests/components/melnor/test_number.py index a8d358c2ac2..f50d0d79edb 100644 --- a/tests/components/melnor/test_number.py +++ b/tests/components/melnor/test_number.py @@ -1,4 +1,5 @@ """Test the Melnor sensors.""" + from __future__ import annotations from homeassistant.core import HomeAssistant @@ -16,7 +17,11 @@ async def test_manual_watering_minutes(hass: HomeAssistant) -> None: entry = mock_config_entry(hass) - with patch_async_ble_device_from_address(), patch_melnor_device() as device_patch, patch_async_register_callback(): + with ( + patch_async_ble_device_from_address(), + patch_melnor_device() as device_patch, + patch_async_register_callback(), + ): device = device_patch.return_value assert await hass.config_entries.async_setup(entry.entry_id) @@ -29,7 +34,6 @@ async def test_manual_watering_minutes(hass: HomeAssistant) -> None: assert number.attributes["max"] == 360 assert number.attributes["min"] == 1 assert number.attributes["step"] == 1.0 - assert number.attributes["icon"] == "mdi:timer-cog-outline" assert device.zone1.manual_watering_minutes == 0 @@ -52,7 +56,11 @@ async def test_frequency_interval_hours(hass: HomeAssistant) -> None: entry = mock_config_entry(hass) - with patch_async_ble_device_from_address(), patch_melnor_device() as device_patch, patch_async_register_callback(): + with ( + patch_async_ble_device_from_address(), + patch_melnor_device() as device_patch, + patch_async_register_callback(), + ): device = device_patch.return_value assert await hass.config_entries.async_setup(entry.entry_id) @@ -65,7 +73,6 @@ async def test_frequency_interval_hours(hass: HomeAssistant) -> None: assert number.attributes["max"] == 168 assert number.attributes["min"] == 1 assert number.attributes["step"] == 1.0 - assert number.attributes["icon"] == "mdi:calendar-refresh-outline" assert device.zone1.frequency.interval_hours == 0 @@ -88,7 +95,11 @@ async def test_frequency_duration_minutes(hass: HomeAssistant) -> None: entry = mock_config_entry(hass) - with patch_async_ble_device_from_address(), patch_melnor_device() as device_patch, patch_async_register_callback(): + with ( + patch_async_ble_device_from_address(), + patch_melnor_device() as device_patch, + patch_async_register_callback(), + ): device = device_patch.return_value assert await hass.config_entries.async_setup(entry.entry_id) @@ -101,7 +112,6 @@ async def test_frequency_duration_minutes(hass: HomeAssistant) -> None: assert number.attributes["max"] == 360 assert number.attributes["min"] == 1 assert number.attributes["step"] == 1.0 - assert number.attributes["icon"] == "mdi:timer-outline" assert device.zone1.frequency.duration_minutes == 0 diff --git a/tests/components/melnor/test_sensor.py b/tests/components/melnor/test_sensor.py index 291115fae9d..d04494d44ad 100644 --- a/tests/components/melnor/test_sensor.py +++ b/tests/components/melnor/test_sensor.py @@ -1,4 +1,5 @@ """Test the Melnor sensors.""" + from __future__ import annotations from freezegun import freeze_time @@ -25,7 +26,11 @@ async def test_battery_sensor(hass: HomeAssistant) -> None: entry = mock_config_entry(hass) - with patch_async_ble_device_from_address(), patch_melnor_device(), patch_async_register_callback(): + with ( + patch_async_ble_device_from_address(), + patch_melnor_device(), + patch_async_register_callback(), + ): assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -52,9 +57,12 @@ async def test_minutes_remaining_sensor(hass: HomeAssistant) -> None: device.zone1._end_time = (end_time).timestamp() - with freeze_time(now), patch_async_ble_device_from_address(), patch_melnor_device( - device - ), patch_async_register_callback(): + with ( + freeze_time(now), + patch_async_ble_device_from_address(), + patch_melnor_device(device), + patch_async_register_callback(), + ): assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -91,9 +99,12 @@ async def test_schedule_next_cycle_sensor(hass: HomeAssistant) -> None: # we control this mock device.zone1.frequency._next_run_time = next_cycle - with freeze_time(now), patch_async_ble_device_from_address(), patch_melnor_device( - device - ), patch_async_register_callback(): + with ( + freeze_time(now), + patch_async_ble_device_from_address(), + patch_melnor_device(device), + patch_async_register_callback(), + ): assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -126,9 +137,11 @@ async def test_rssi_sensor( device = mock_melnor_device() - with patch_async_ble_device_from_address(), patch_melnor_device( - device - ), patch_async_register_callback(): + with ( + patch_async_ble_device_from_address(), + patch_melnor_device(device), + patch_async_register_callback(), + ): assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/melnor/test_switch.py b/tests/components/melnor/test_switch.py index fdd5e8ad33e..0da0195fe75 100644 --- a/tests/components/melnor/test_switch.py +++ b/tests/components/melnor/test_switch.py @@ -1,4 +1,5 @@ """Test the Melnor sensors.""" + from __future__ import annotations from homeassistant.components.switch import SwitchDeviceClass @@ -18,7 +19,11 @@ async def test_manual_watering_switch_metadata(hass: HomeAssistant) -> None: entry = mock_config_entry(hass) - with patch_async_ble_device_from_address(), patch_melnor_device(), patch_async_register_callback(): + with ( + patch_async_ble_device_from_address(), + patch_melnor_device(), + patch_async_register_callback(), + ): assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -26,7 +31,6 @@ async def test_manual_watering_switch_metadata(hass: HomeAssistant) -> None: assert switch is not None assert switch.attributes["device_class"] == SwitchDeviceClass.SWITCH - assert switch.attributes["icon"] == "mdi:sprinkler" async def test_manual_watering_switch_on_off(hass: HomeAssistant) -> None: @@ -34,7 +38,11 @@ async def test_manual_watering_switch_on_off(hass: HomeAssistant) -> None: entry = mock_config_entry(hass) - with patch_async_ble_device_from_address(), patch_melnor_device() as device_patch, patch_async_register_callback(): + with ( + patch_async_ble_device_from_address(), + patch_melnor_device() as device_patch, + patch_async_register_callback(), + ): device = device_patch.return_value assert await hass.config_entries.async_setup(entry.entry_id) @@ -77,7 +85,11 @@ async def test_schedule_enabled_switch_on_off(hass: HomeAssistant) -> None: entry = mock_config_entry(hass) - with patch_async_ble_device_from_address(), patch_melnor_device() as device_patch, patch_async_register_callback(): + with ( + patch_async_ble_device_from_address(), + patch_melnor_device() as device_patch, + patch_async_register_callback(), + ): device = device_patch.return_value assert await hass.config_entries.async_setup(entry.entry_id) diff --git a/tests/components/melnor/test_time.py b/tests/components/melnor/test_time.py index 682f518d40b..1d12c3b47f8 100644 --- a/tests/components/melnor/test_time.py +++ b/tests/components/melnor/test_time.py @@ -1,4 +1,5 @@ """Test the Melnor time platform.""" + from __future__ import annotations from datetime import time @@ -23,7 +24,11 @@ async def test_schedule_start_time(hass: HomeAssistant) -> None: entry = mock_config_entry(hass) - with patch_async_ble_device_from_address(), patch_melnor_device() as device_patch, patch_async_register_callback(): + with ( + patch_async_ble_device_from_address(), + patch_melnor_device() as device_patch, + patch_async_register_callback(), + ): device = device_patch.return_value assert await hass.config_entries.async_setup(entry.entry_id) diff --git a/tests/components/meraki/test_device_tracker.py b/tests/components/meraki/test_device_tracker.py index 69d1bc1b3a8..d0cd2cf8c5a 100644 --- a/tests/components/meraki/test_device_tracker.py +++ b/tests/components/meraki/test_device_tracker.py @@ -1,4 +1,5 @@ """The tests the for Meraki device tracker.""" + from http import HTTPStatus import json @@ -20,8 +21,9 @@ from homeassistant.setup import async_setup_component def meraki_client(event_loop, hass, hass_client): """Meraki mock client.""" loop = event_loop - assert loop.run_until_complete( - async_setup_component( + + async def setup_and_wait(): + result = await async_setup_component( hass, device_tracker.DOMAIN, { @@ -32,8 +34,10 @@ def meraki_client(event_loop, hass, hass_client): } }, ) - ) + await hass.async_block_till_done() + return result + assert loop.run_until_complete(setup_and_wait()) return loop.run_until_complete(hass_client()) diff --git a/tests/components/met/__init__.py b/tests/components/met/__init__.py index 2ef0f7e12f0..8ea5ce605f0 100644 --- a/tests/components/met/__init__.py +++ b/tests/components/met/__init__.py @@ -1,4 +1,5 @@ """Tests for Met.no.""" + from unittest.mock import patch from homeassistant.components.met.const import CONF_TRACK_HOME, DOMAIN diff --git a/tests/components/met/conftest.py b/tests/components/met/conftest.py index a007620988f..699c1c81795 100644 --- a/tests/components/met/conftest.py +++ b/tests/components/met/conftest.py @@ -1,4 +1,5 @@ """Fixtures for Met weather testing.""" + from unittest.mock import AsyncMock, patch import pytest diff --git a/tests/components/met/test_config_flow.py b/tests/components/met/test_config_flow.py index 24ce8660346..8d0b1620022 100644 --- a/tests/components/met/test_config_flow.py +++ b/tests/components/met/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for Met.no config flow.""" + from unittest.mock import ANY, patch import pytest diff --git a/tests/components/met/test_init.py b/tests/components/met/test_init.py index 0e4e46b09da..b329e2ff01c 100644 --- a/tests/components/met/test_init.py +++ b/tests/components/met/test_init.py @@ -1,4 +1,5 @@ """Test the Met integration init.""" + import pytest from homeassistant.components.met.const import ( diff --git a/tests/components/met/test_weather.py b/tests/components/met/test_weather.py index d1714e7b69e..95547ead14d 100644 --- a/tests/components/met/test_weather.py +++ b/tests/components/met/test_weather.py @@ -1,4 +1,5 @@ """Test Met weather entity.""" + from homeassistant import config_entries from homeassistant.components.met import DOMAIN from homeassistant.components.weather import DOMAIN as WEATHER_DOMAIN diff --git a/tests/components/met_eireann/__init__.py b/tests/components/met_eireann/__init__.py index 3dfadc06f6b..86c3090b0ca 100644 --- a/tests/components/met_eireann/__init__.py +++ b/tests/components/met_eireann/__init__.py @@ -1,4 +1,5 @@ """Tests for Met Éireann.""" + from unittest.mock import patch from homeassistant.components.met_eireann.const import DOMAIN diff --git a/tests/components/met_eireann/conftest.py b/tests/components/met_eireann/conftest.py index e73d1e41cca..d1fd5bd7c89 100644 --- a/tests/components/met_eireann/conftest.py +++ b/tests/components/met_eireann/conftest.py @@ -1,4 +1,5 @@ """Fixtures for Met Éireann weather testing.""" + from unittest.mock import AsyncMock, patch import pytest diff --git a/tests/components/met_eireann/test_config_flow.py b/tests/components/met_eireann/test_config_flow.py index bad0e8b3e05..8c5e7f43ced 100644 --- a/tests/components/met_eireann/test_config_flow.py +++ b/tests/components/met_eireann/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for Met Éireann config flow.""" + from unittest.mock import patch import pytest diff --git a/tests/components/met_eireann/test_init.py b/tests/components/met_eireann/test_init.py index dd34ac25cfb..14457ae2f7d 100644 --- a/tests/components/met_eireann/test_init.py +++ b/tests/components/met_eireann/test_init.py @@ -1,4 +1,5 @@ """Test the Met Éireann integration init.""" + from homeassistant.components.met_eireann.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant diff --git a/tests/components/met_eireann/test_weather.py b/tests/components/met_eireann/test_weather.py index e5c2c66b626..a660c18f7b3 100644 --- a/tests/components/met_eireann/test_weather.py +++ b/tests/components/met_eireann/test_weather.py @@ -1,4 +1,5 @@ """Test Met Éireann weather entity.""" + import datetime from freezegun.api import FrozenDateTimeFactory @@ -44,22 +45,6 @@ async def test_new_config_entry( assert len(er.async_entries_for_config_entry(entity_registry, entry.entry_id)) == 1 -async def test_legacy_config_entry( - hass: HomeAssistant, entity_registry: er.EntityRegistry, mock_weather -) -> None: - """Test the expected entities are created.""" - entity_registry.async_get_or_create( - WEATHER_DOMAIN, - DOMAIN, - "10-20-hourly", - ) - await setup_config_entry(hass) - assert len(hass.states.async_entity_ids("weather")) == 2 - - entry = hass.config_entries.async_entries()[0] - assert len(er.async_entries_for_config_entry(entity_registry, entry.entry_id)) == 2 - - async def test_weather(hass: HomeAssistant, mock_weather) -> None: """Test weather entity.""" await setup_config_entry(hass) diff --git a/tests/components/meteo_france/conftest.py b/tests/components/meteo_france/conftest.py index 0b62a03a53c..123fc00e42a 100644 --- a/tests/components/meteo_france/conftest.py +++ b/tests/components/meteo_france/conftest.py @@ -1,4 +1,5 @@ """Meteo-France generic test utils.""" + from unittest.mock import patch import pytest diff --git a/tests/components/meteo_france/test_config_flow.py b/tests/components/meteo_france/test_config_flow.py index 0405f8efa18..4b9e26f883b 100644 --- a/tests/components/meteo_france/test_config_flow.py +++ b/tests/components/meteo_france/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for the Meteo-France config flow.""" + from unittest.mock import patch from meteofrance_api.model import Place diff --git a/tests/components/meteoclimatic/conftest.py b/tests/components/meteoclimatic/conftest.py index 964f67d6473..a481b811a77 100644 --- a/tests/components/meteoclimatic/conftest.py +++ b/tests/components/meteoclimatic/conftest.py @@ -1,4 +1,5 @@ """Meteoclimatic generic test utils.""" + from unittest.mock import patch import pytest diff --git a/tests/components/meteoclimatic/test_config_flow.py b/tests/components/meteoclimatic/test_config_flow.py index 46527d675f0..42fb5e78d4a 100644 --- a/tests/components/meteoclimatic/test_config_flow.py +++ b/tests/components/meteoclimatic/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for the Meteoclimatic config flow.""" + from unittest.mock import patch from meteoclimatic.exceptions import MeteoclimaticError, StationNotFound diff --git a/tests/components/metoffice/conftest.py b/tests/components/metoffice/conftest.py index b1d1c9f508e..83c7e7853f7 100644 --- a/tests/components/metoffice/conftest.py +++ b/tests/components/metoffice/conftest.py @@ -1,4 +1,5 @@ """Fixtures for Met Office weather integration tests.""" + from unittest.mock import patch from datapoint.exceptions import APIException diff --git a/tests/fixtures/metoffice.json b/tests/components/metoffice/fixtures/metoffice.json similarity index 100% rename from tests/fixtures/metoffice.json rename to tests/components/metoffice/fixtures/metoffice.json diff --git a/tests/components/metoffice/snapshots/test_weather.ambr b/tests/components/metoffice/snapshots/test_weather.ambr index 108a9330403..a6991a8631b 100644 --- a/tests/components/metoffice/snapshots/test_weather.ambr +++ b/tests/components/metoffice/snapshots/test_weather.ambr @@ -1,1316 +1,4 @@ # serializer version: 1 -# name: test_forecast_service - dict({ - 'forecast': list([ - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-26T12:00:00+00:00', - 'precipitation_probability': 9, - 'temperature': 13.0, - 'wind_bearing': 'WNW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-27T12:00:00+00:00', - 'precipitation_probability': 14, - 'temperature': 11.0, - 'wind_bearing': 'NW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-28T12:00:00+00:00', - 'precipitation_probability': 10, - 'temperature': 12.0, - 'wind_bearing': 'ENE', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'rainy', - 'datetime': '2020-04-29T12:00:00+00:00', - 'precipitation_probability': 59, - 'temperature': 13.0, - 'wind_bearing': 'SE', - 'wind_speed': 20.92, - }), - ]), - }) -# --- -# name: test_forecast_service.1 - dict({ - 'forecast': list([ - dict({ - 'condition': 'sunny', - 'datetime': '2020-04-25T15:00:00+00:00', - 'precipitation_probability': 1, - 'temperature': 19.0, - 'wind_bearing': 'S', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-25T18:00:00+00:00', - 'precipitation_probability': 2, - 'temperature': 17.0, - 'wind_bearing': 'WNW', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-25T21:00:00+00:00', - 'precipitation_probability': 1, - 'temperature': 14.0, - 'wind_bearing': 'NW', - 'wind_speed': 3.22, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-26T00:00:00+00:00', - 'precipitation_probability': 1, - 'temperature': 13.0, - 'wind_bearing': 'WSW', - 'wind_speed': 3.22, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-26T03:00:00+00:00', - 'precipitation_probability': 2, - 'temperature': 12.0, - 'wind_bearing': 'WNW', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-26T06:00:00+00:00', - 'precipitation_probability': 5, - 'temperature': 11.0, - 'wind_bearing': 'NW', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-26T09:00:00+00:00', - 'precipitation_probability': 5, - 'temperature': 12.0, - 'wind_bearing': 'WNW', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-26T12:00:00+00:00', - 'precipitation_probability': 6, - 'temperature': 12.0, - 'wind_bearing': 'WNW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-26T15:00:00+00:00', - 'precipitation_probability': 5, - 'temperature': 12.0, - 'wind_bearing': 'NW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-26T18:00:00+00:00', - 'precipitation_probability': 9, - 'temperature': 11.0, - 'wind_bearing': 'NW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-26T21:00:00+00:00', - 'precipitation_probability': 9, - 'temperature': 10.0, - 'wind_bearing': 'NW', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-27T00:00:00+00:00', - 'precipitation_probability': 11, - 'temperature': 9.0, - 'wind_bearing': 'WNW', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-27T03:00:00+00:00', - 'precipitation_probability': 12, - 'temperature': 8.0, - 'wind_bearing': 'WNW', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-27T06:00:00+00:00', - 'precipitation_probability': 14, - 'temperature': 8.0, - 'wind_bearing': 'NW', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-27T09:00:00+00:00', - 'precipitation_probability': 6, - 'temperature': 9.0, - 'wind_bearing': 'NW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-27T12:00:00+00:00', - 'precipitation_probability': 4, - 'temperature': 10.0, - 'wind_bearing': 'NW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'sunny', - 'datetime': '2020-04-27T15:00:00+00:00', - 'precipitation_probability': 1, - 'temperature': 10.0, - 'wind_bearing': 'NW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'sunny', - 'datetime': '2020-04-27T18:00:00+00:00', - 'precipitation_probability': 1, - 'temperature': 10.0, - 'wind_bearing': 'NW', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'clear-night', - 'datetime': '2020-04-27T21:00:00+00:00', - 'precipitation_probability': 1, - 'temperature': 9.0, - 'wind_bearing': 'NW', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'clear-night', - 'datetime': '2020-04-28T00:00:00+00:00', - 'precipitation_probability': 2, - 'temperature': 8.0, - 'wind_bearing': 'NNW', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'clear-night', - 'datetime': '2020-04-28T03:00:00+00:00', - 'precipitation_probability': 3, - 'temperature': 7.0, - 'wind_bearing': 'W', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'sunny', - 'datetime': '2020-04-28T06:00:00+00:00', - 'precipitation_probability': 5, - 'temperature': 6.0, - 'wind_bearing': 'S', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-28T09:00:00+00:00', - 'precipitation_probability': 6, - 'temperature': 9.0, - 'wind_bearing': 'ENE', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-28T12:00:00+00:00', - 'precipitation_probability': 10, - 'temperature': 11.0, - 'wind_bearing': 'ENE', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-28T15:00:00+00:00', - 'precipitation_probability': 10, - 'temperature': 12.0, - 'wind_bearing': 'N', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-28T18:00:00+00:00', - 'precipitation_probability': 10, - 'temperature': 11.0, - 'wind_bearing': 'N', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-28T21:00:00+00:00', - 'precipitation_probability': 9, - 'temperature': 10.0, - 'wind_bearing': 'NNE', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-29T00:00:00+00:00', - 'precipitation_probability': 6, - 'temperature': 9.0, - 'wind_bearing': 'E', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-29T03:00:00+00:00', - 'precipitation_probability': 3, - 'temperature': 8.0, - 'wind_bearing': 'SSE', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-29T06:00:00+00:00', - 'precipitation_probability': 9, - 'temperature': 8.0, - 'wind_bearing': 'SE', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-29T09:00:00+00:00', - 'precipitation_probability': 12, - 'temperature': 10.0, - 'wind_bearing': 'SE', - 'wind_speed': 17.7, - }), - dict({ - 'condition': 'rainy', - 'datetime': '2020-04-29T12:00:00+00:00', - 'precipitation_probability': 47, - 'temperature': 12.0, - 'wind_bearing': 'SE', - 'wind_speed': 20.92, - }), - dict({ - 'condition': 'pouring', - 'datetime': '2020-04-29T15:00:00+00:00', - 'precipitation_probability': 59, - 'temperature': 13.0, - 'wind_bearing': 'SSE', - 'wind_speed': 20.92, - }), - dict({ - 'condition': 'rainy', - 'datetime': '2020-04-29T18:00:00+00:00', - 'precipitation_probability': 39, - 'temperature': 12.0, - 'wind_bearing': 'SSE', - 'wind_speed': 17.7, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-29T21:00:00+00:00', - 'precipitation_probability': 19, - 'temperature': 11.0, - 'wind_bearing': 'SSE', - 'wind_speed': 20.92, - }), - ]), - }) -# --- -# name: test_forecast_service.2 - dict({ - 'forecast': list([ - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-26T12:00:00+00:00', - 'precipitation_probability': 9, - 'temperature': 13.0, - 'wind_bearing': 'WNW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-27T12:00:00+00:00', - 'precipitation_probability': 14, - 'temperature': 11.0, - 'wind_bearing': 'NW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-28T12:00:00+00:00', - 'precipitation_probability': 10, - 'temperature': 12.0, - 'wind_bearing': 'ENE', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'rainy', - 'datetime': '2020-04-29T12:00:00+00:00', - 'precipitation_probability': 59, - 'temperature': 13.0, - 'wind_bearing': 'SE', - 'wind_speed': 20.92, - }), - ]), - }) -# --- -# name: test_forecast_service.3 - dict({ - 'forecast': list([ - dict({ - 'condition': 'sunny', - 'datetime': '2020-04-25T15:00:00+00:00', - 'precipitation_probability': 1, - 'temperature': 19.0, - 'wind_bearing': 'S', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-25T18:00:00+00:00', - 'precipitation_probability': 2, - 'temperature': 17.0, - 'wind_bearing': 'WNW', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-25T21:00:00+00:00', - 'precipitation_probability': 1, - 'temperature': 14.0, - 'wind_bearing': 'NW', - 'wind_speed': 3.22, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-26T00:00:00+00:00', - 'precipitation_probability': 1, - 'temperature': 13.0, - 'wind_bearing': 'WSW', - 'wind_speed': 3.22, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-26T03:00:00+00:00', - 'precipitation_probability': 2, - 'temperature': 12.0, - 'wind_bearing': 'WNW', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-26T06:00:00+00:00', - 'precipitation_probability': 5, - 'temperature': 11.0, - 'wind_bearing': 'NW', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-26T09:00:00+00:00', - 'precipitation_probability': 5, - 'temperature': 12.0, - 'wind_bearing': 'WNW', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-26T12:00:00+00:00', - 'precipitation_probability': 6, - 'temperature': 12.0, - 'wind_bearing': 'WNW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-26T15:00:00+00:00', - 'precipitation_probability': 5, - 'temperature': 12.0, - 'wind_bearing': 'NW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-26T18:00:00+00:00', - 'precipitation_probability': 9, - 'temperature': 11.0, - 'wind_bearing': 'NW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-26T21:00:00+00:00', - 'precipitation_probability': 9, - 'temperature': 10.0, - 'wind_bearing': 'NW', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-27T00:00:00+00:00', - 'precipitation_probability': 11, - 'temperature': 9.0, - 'wind_bearing': 'WNW', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-27T03:00:00+00:00', - 'precipitation_probability': 12, - 'temperature': 8.0, - 'wind_bearing': 'WNW', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-27T06:00:00+00:00', - 'precipitation_probability': 14, - 'temperature': 8.0, - 'wind_bearing': 'NW', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-27T09:00:00+00:00', - 'precipitation_probability': 6, - 'temperature': 9.0, - 'wind_bearing': 'NW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-27T12:00:00+00:00', - 'precipitation_probability': 4, - 'temperature': 10.0, - 'wind_bearing': 'NW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'sunny', - 'datetime': '2020-04-27T15:00:00+00:00', - 'precipitation_probability': 1, - 'temperature': 10.0, - 'wind_bearing': 'NW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'sunny', - 'datetime': '2020-04-27T18:00:00+00:00', - 'precipitation_probability': 1, - 'temperature': 10.0, - 'wind_bearing': 'NW', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'clear-night', - 'datetime': '2020-04-27T21:00:00+00:00', - 'precipitation_probability': 1, - 'temperature': 9.0, - 'wind_bearing': 'NW', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'clear-night', - 'datetime': '2020-04-28T00:00:00+00:00', - 'precipitation_probability': 2, - 'temperature': 8.0, - 'wind_bearing': 'NNW', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'clear-night', - 'datetime': '2020-04-28T03:00:00+00:00', - 'precipitation_probability': 3, - 'temperature': 7.0, - 'wind_bearing': 'W', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'sunny', - 'datetime': '2020-04-28T06:00:00+00:00', - 'precipitation_probability': 5, - 'temperature': 6.0, - 'wind_bearing': 'S', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-28T09:00:00+00:00', - 'precipitation_probability': 6, - 'temperature': 9.0, - 'wind_bearing': 'ENE', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-28T12:00:00+00:00', - 'precipitation_probability': 10, - 'temperature': 11.0, - 'wind_bearing': 'ENE', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-28T15:00:00+00:00', - 'precipitation_probability': 10, - 'temperature': 12.0, - 'wind_bearing': 'N', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-28T18:00:00+00:00', - 'precipitation_probability': 10, - 'temperature': 11.0, - 'wind_bearing': 'N', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-28T21:00:00+00:00', - 'precipitation_probability': 9, - 'temperature': 10.0, - 'wind_bearing': 'NNE', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-29T00:00:00+00:00', - 'precipitation_probability': 6, - 'temperature': 9.0, - 'wind_bearing': 'E', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-29T03:00:00+00:00', - 'precipitation_probability': 3, - 'temperature': 8.0, - 'wind_bearing': 'SSE', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-29T06:00:00+00:00', - 'precipitation_probability': 9, - 'temperature': 8.0, - 'wind_bearing': 'SE', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-29T09:00:00+00:00', - 'precipitation_probability': 12, - 'temperature': 10.0, - 'wind_bearing': 'SE', - 'wind_speed': 17.7, - }), - dict({ - 'condition': 'rainy', - 'datetime': '2020-04-29T12:00:00+00:00', - 'precipitation_probability': 47, - 'temperature': 12.0, - 'wind_bearing': 'SE', - 'wind_speed': 20.92, - }), - dict({ - 'condition': 'pouring', - 'datetime': '2020-04-29T15:00:00+00:00', - 'precipitation_probability': 59, - 'temperature': 13.0, - 'wind_bearing': 'SSE', - 'wind_speed': 20.92, - }), - dict({ - 'condition': 'rainy', - 'datetime': '2020-04-29T18:00:00+00:00', - 'precipitation_probability': 39, - 'temperature': 12.0, - 'wind_bearing': 'SSE', - 'wind_speed': 17.7, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-29T21:00:00+00:00', - 'precipitation_probability': 19, - 'temperature': 11.0, - 'wind_bearing': 'SSE', - 'wind_speed': 20.92, - }), - ]), - }) -# --- -# name: test_forecast_service[forecast] - dict({ - 'weather.met_office_wavertree_daily': dict({ - 'forecast': list([ - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-26T12:00:00+00:00', - 'precipitation_probability': 9, - 'temperature': 13.0, - 'wind_bearing': 'WNW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-27T12:00:00+00:00', - 'precipitation_probability': 14, - 'temperature': 11.0, - 'wind_bearing': 'NW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-28T12:00:00+00:00', - 'precipitation_probability': 10, - 'temperature': 12.0, - 'wind_bearing': 'ENE', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'rainy', - 'datetime': '2020-04-29T12:00:00+00:00', - 'precipitation_probability': 59, - 'temperature': 13.0, - 'wind_bearing': 'SE', - 'wind_speed': 20.92, - }), - ]), - }), - }) -# --- -# name: test_forecast_service[forecast].1 - dict({ - 'weather.met_office_wavertree_daily': dict({ - 'forecast': list([ - dict({ - 'condition': 'sunny', - 'datetime': '2020-04-25T15:00:00+00:00', - 'precipitation_probability': 1, - 'temperature': 19.0, - 'wind_bearing': 'S', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-25T18:00:00+00:00', - 'precipitation_probability': 2, - 'temperature': 17.0, - 'wind_bearing': 'WNW', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-25T21:00:00+00:00', - 'precipitation_probability': 1, - 'temperature': 14.0, - 'wind_bearing': 'NW', - 'wind_speed': 3.22, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-26T00:00:00+00:00', - 'precipitation_probability': 1, - 'temperature': 13.0, - 'wind_bearing': 'WSW', - 'wind_speed': 3.22, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-26T03:00:00+00:00', - 'precipitation_probability': 2, - 'temperature': 12.0, - 'wind_bearing': 'WNW', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-26T06:00:00+00:00', - 'precipitation_probability': 5, - 'temperature': 11.0, - 'wind_bearing': 'NW', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-26T09:00:00+00:00', - 'precipitation_probability': 5, - 'temperature': 12.0, - 'wind_bearing': 'WNW', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-26T12:00:00+00:00', - 'precipitation_probability': 6, - 'temperature': 12.0, - 'wind_bearing': 'WNW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-26T15:00:00+00:00', - 'precipitation_probability': 5, - 'temperature': 12.0, - 'wind_bearing': 'NW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-26T18:00:00+00:00', - 'precipitation_probability': 9, - 'temperature': 11.0, - 'wind_bearing': 'NW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-26T21:00:00+00:00', - 'precipitation_probability': 9, - 'temperature': 10.0, - 'wind_bearing': 'NW', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-27T00:00:00+00:00', - 'precipitation_probability': 11, - 'temperature': 9.0, - 'wind_bearing': 'WNW', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-27T03:00:00+00:00', - 'precipitation_probability': 12, - 'temperature': 8.0, - 'wind_bearing': 'WNW', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-27T06:00:00+00:00', - 'precipitation_probability': 14, - 'temperature': 8.0, - 'wind_bearing': 'NW', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-27T09:00:00+00:00', - 'precipitation_probability': 6, - 'temperature': 9.0, - 'wind_bearing': 'NW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-27T12:00:00+00:00', - 'precipitation_probability': 4, - 'temperature': 10.0, - 'wind_bearing': 'NW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'sunny', - 'datetime': '2020-04-27T15:00:00+00:00', - 'precipitation_probability': 1, - 'temperature': 10.0, - 'wind_bearing': 'NW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'sunny', - 'datetime': '2020-04-27T18:00:00+00:00', - 'precipitation_probability': 1, - 'temperature': 10.0, - 'wind_bearing': 'NW', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'clear-night', - 'datetime': '2020-04-27T21:00:00+00:00', - 'precipitation_probability': 1, - 'temperature': 9.0, - 'wind_bearing': 'NW', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'clear-night', - 'datetime': '2020-04-28T00:00:00+00:00', - 'precipitation_probability': 2, - 'temperature': 8.0, - 'wind_bearing': 'NNW', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'clear-night', - 'datetime': '2020-04-28T03:00:00+00:00', - 'precipitation_probability': 3, - 'temperature': 7.0, - 'wind_bearing': 'W', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'sunny', - 'datetime': '2020-04-28T06:00:00+00:00', - 'precipitation_probability': 5, - 'temperature': 6.0, - 'wind_bearing': 'S', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-28T09:00:00+00:00', - 'precipitation_probability': 6, - 'temperature': 9.0, - 'wind_bearing': 'ENE', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-28T12:00:00+00:00', - 'precipitation_probability': 10, - 'temperature': 11.0, - 'wind_bearing': 'ENE', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-28T15:00:00+00:00', - 'precipitation_probability': 10, - 'temperature': 12.0, - 'wind_bearing': 'N', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-28T18:00:00+00:00', - 'precipitation_probability': 10, - 'temperature': 11.0, - 'wind_bearing': 'N', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-28T21:00:00+00:00', - 'precipitation_probability': 9, - 'temperature': 10.0, - 'wind_bearing': 'NNE', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-29T00:00:00+00:00', - 'precipitation_probability': 6, - 'temperature': 9.0, - 'wind_bearing': 'E', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-29T03:00:00+00:00', - 'precipitation_probability': 3, - 'temperature': 8.0, - 'wind_bearing': 'SSE', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-29T06:00:00+00:00', - 'precipitation_probability': 9, - 'temperature': 8.0, - 'wind_bearing': 'SE', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-29T09:00:00+00:00', - 'precipitation_probability': 12, - 'temperature': 10.0, - 'wind_bearing': 'SE', - 'wind_speed': 17.7, - }), - dict({ - 'condition': 'rainy', - 'datetime': '2020-04-29T12:00:00+00:00', - 'precipitation_probability': 47, - 'temperature': 12.0, - 'wind_bearing': 'SE', - 'wind_speed': 20.92, - }), - dict({ - 'condition': 'pouring', - 'datetime': '2020-04-29T15:00:00+00:00', - 'precipitation_probability': 59, - 'temperature': 13.0, - 'wind_bearing': 'SSE', - 'wind_speed': 20.92, - }), - dict({ - 'condition': 'rainy', - 'datetime': '2020-04-29T18:00:00+00:00', - 'precipitation_probability': 39, - 'temperature': 12.0, - 'wind_bearing': 'SSE', - 'wind_speed': 17.7, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-29T21:00:00+00:00', - 'precipitation_probability': 19, - 'temperature': 11.0, - 'wind_bearing': 'SSE', - 'wind_speed': 20.92, - }), - ]), - }), - }) -# --- -# name: test_forecast_service[forecast].2 - dict({ - 'weather.met_office_wavertree_daily': dict({ - 'forecast': list([ - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-26T12:00:00+00:00', - 'precipitation_probability': 9, - 'temperature': 13.0, - 'wind_bearing': 'WNW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-27T12:00:00+00:00', - 'precipitation_probability': 14, - 'temperature': 11.0, - 'wind_bearing': 'NW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-28T12:00:00+00:00', - 'precipitation_probability': 10, - 'temperature': 12.0, - 'wind_bearing': 'ENE', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'rainy', - 'datetime': '2020-04-29T12:00:00+00:00', - 'precipitation_probability': 59, - 'temperature': 13.0, - 'wind_bearing': 'SE', - 'wind_speed': 20.92, - }), - ]), - }), - }) -# --- -# name: test_forecast_service[forecast].3 - dict({ - 'weather.met_office_wavertree_daily': dict({ - 'forecast': list([ - dict({ - 'condition': 'sunny', - 'datetime': '2020-04-25T15:00:00+00:00', - 'precipitation_probability': 1, - 'temperature': 19.0, - 'wind_bearing': 'S', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-25T18:00:00+00:00', - 'precipitation_probability': 2, - 'temperature': 17.0, - 'wind_bearing': 'WNW', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-25T21:00:00+00:00', - 'precipitation_probability': 1, - 'temperature': 14.0, - 'wind_bearing': 'NW', - 'wind_speed': 3.22, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-26T00:00:00+00:00', - 'precipitation_probability': 1, - 'temperature': 13.0, - 'wind_bearing': 'WSW', - 'wind_speed': 3.22, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-26T03:00:00+00:00', - 'precipitation_probability': 2, - 'temperature': 12.0, - 'wind_bearing': 'WNW', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-26T06:00:00+00:00', - 'precipitation_probability': 5, - 'temperature': 11.0, - 'wind_bearing': 'NW', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-26T09:00:00+00:00', - 'precipitation_probability': 5, - 'temperature': 12.0, - 'wind_bearing': 'WNW', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-26T12:00:00+00:00', - 'precipitation_probability': 6, - 'temperature': 12.0, - 'wind_bearing': 'WNW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-26T15:00:00+00:00', - 'precipitation_probability': 5, - 'temperature': 12.0, - 'wind_bearing': 'NW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-26T18:00:00+00:00', - 'precipitation_probability': 9, - 'temperature': 11.0, - 'wind_bearing': 'NW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-26T21:00:00+00:00', - 'precipitation_probability': 9, - 'temperature': 10.0, - 'wind_bearing': 'NW', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-27T00:00:00+00:00', - 'precipitation_probability': 11, - 'temperature': 9.0, - 'wind_bearing': 'WNW', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-27T03:00:00+00:00', - 'precipitation_probability': 12, - 'temperature': 8.0, - 'wind_bearing': 'WNW', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-27T06:00:00+00:00', - 'precipitation_probability': 14, - 'temperature': 8.0, - 'wind_bearing': 'NW', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-27T09:00:00+00:00', - 'precipitation_probability': 6, - 'temperature': 9.0, - 'wind_bearing': 'NW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-27T12:00:00+00:00', - 'precipitation_probability': 4, - 'temperature': 10.0, - 'wind_bearing': 'NW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'sunny', - 'datetime': '2020-04-27T15:00:00+00:00', - 'precipitation_probability': 1, - 'temperature': 10.0, - 'wind_bearing': 'NW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'sunny', - 'datetime': '2020-04-27T18:00:00+00:00', - 'precipitation_probability': 1, - 'temperature': 10.0, - 'wind_bearing': 'NW', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'clear-night', - 'datetime': '2020-04-27T21:00:00+00:00', - 'precipitation_probability': 1, - 'temperature': 9.0, - 'wind_bearing': 'NW', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'clear-night', - 'datetime': '2020-04-28T00:00:00+00:00', - 'precipitation_probability': 2, - 'temperature': 8.0, - 'wind_bearing': 'NNW', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'clear-night', - 'datetime': '2020-04-28T03:00:00+00:00', - 'precipitation_probability': 3, - 'temperature': 7.0, - 'wind_bearing': 'W', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'sunny', - 'datetime': '2020-04-28T06:00:00+00:00', - 'precipitation_probability': 5, - 'temperature': 6.0, - 'wind_bearing': 'S', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-28T09:00:00+00:00', - 'precipitation_probability': 6, - 'temperature': 9.0, - 'wind_bearing': 'ENE', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-28T12:00:00+00:00', - 'precipitation_probability': 10, - 'temperature': 11.0, - 'wind_bearing': 'ENE', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-28T15:00:00+00:00', - 'precipitation_probability': 10, - 'temperature': 12.0, - 'wind_bearing': 'N', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-28T18:00:00+00:00', - 'precipitation_probability': 10, - 'temperature': 11.0, - 'wind_bearing': 'N', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-28T21:00:00+00:00', - 'precipitation_probability': 9, - 'temperature': 10.0, - 'wind_bearing': 'NNE', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-29T00:00:00+00:00', - 'precipitation_probability': 6, - 'temperature': 9.0, - 'wind_bearing': 'E', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-29T03:00:00+00:00', - 'precipitation_probability': 3, - 'temperature': 8.0, - 'wind_bearing': 'SSE', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-29T06:00:00+00:00', - 'precipitation_probability': 9, - 'temperature': 8.0, - 'wind_bearing': 'SE', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-29T09:00:00+00:00', - 'precipitation_probability': 12, - 'temperature': 10.0, - 'wind_bearing': 'SE', - 'wind_speed': 17.7, - }), - dict({ - 'condition': 'rainy', - 'datetime': '2020-04-29T12:00:00+00:00', - 'precipitation_probability': 47, - 'temperature': 12.0, - 'wind_bearing': 'SE', - 'wind_speed': 20.92, - }), - dict({ - 'condition': 'pouring', - 'datetime': '2020-04-29T15:00:00+00:00', - 'precipitation_probability': 59, - 'temperature': 13.0, - 'wind_bearing': 'SSE', - 'wind_speed': 20.92, - }), - dict({ - 'condition': 'rainy', - 'datetime': '2020-04-29T18:00:00+00:00', - 'precipitation_probability': 39, - 'temperature': 12.0, - 'wind_bearing': 'SSE', - 'wind_speed': 17.7, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-29T21:00:00+00:00', - 'precipitation_probability': 19, - 'temperature': 11.0, - 'wind_bearing': 'SSE', - 'wind_speed': 20.92, - }), - ]), - }), - }) -# --- -# name: test_forecast_service[forecast].4 - dict({ - 'weather.met_office_wavertree_daily': dict({ - 'forecast': list([ - ]), - }), - }) -# --- # name: test_forecast_service[get_forecast] dict({ 'forecast': list([ @@ -2629,7 +1317,7 @@ }), }) # --- -# name: test_forecast_subscription[weather.met_office_wavertree_3_hourly] +# name: test_forecast_subscription[daily] list([ dict({ 'condition': 'cloudy', @@ -2665,7 +1353,7 @@ }), ]) # --- -# name: test_forecast_subscription[weather.met_office_wavertree_3_hourly].1 +# name: test_forecast_subscription[daily].1 list([ dict({ 'condition': 'cloudy', @@ -2701,7 +1389,7 @@ }), ]) # --- -# name: test_forecast_subscription[weather.met_office_wavertree_3_hourly].2 +# name: test_forecast_subscription[hourly] list([ dict({ 'condition': 'sunny', @@ -2985,647 +1673,7 @@ }), ]) # --- -# name: test_forecast_subscription[weather.met_office_wavertree_3_hourly].3 - list([ - dict({ - 'condition': 'sunny', - 'datetime': '2020-04-25T15:00:00+00:00', - 'precipitation_probability': 1, - 'temperature': 19.0, - 'wind_bearing': 'S', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-25T18:00:00+00:00', - 'precipitation_probability': 2, - 'temperature': 17.0, - 'wind_bearing': 'WNW', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-25T21:00:00+00:00', - 'precipitation_probability': 1, - 'temperature': 14.0, - 'wind_bearing': 'NW', - 'wind_speed': 3.22, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-26T00:00:00+00:00', - 'precipitation_probability': 1, - 'temperature': 13.0, - 'wind_bearing': 'WSW', - 'wind_speed': 3.22, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-26T03:00:00+00:00', - 'precipitation_probability': 2, - 'temperature': 12.0, - 'wind_bearing': 'WNW', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-26T06:00:00+00:00', - 'precipitation_probability': 5, - 'temperature': 11.0, - 'wind_bearing': 'NW', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-26T09:00:00+00:00', - 'precipitation_probability': 5, - 'temperature': 12.0, - 'wind_bearing': 'WNW', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-26T12:00:00+00:00', - 'precipitation_probability': 6, - 'temperature': 12.0, - 'wind_bearing': 'WNW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-26T15:00:00+00:00', - 'precipitation_probability': 5, - 'temperature': 12.0, - 'wind_bearing': 'NW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-26T18:00:00+00:00', - 'precipitation_probability': 9, - 'temperature': 11.0, - 'wind_bearing': 'NW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-26T21:00:00+00:00', - 'precipitation_probability': 9, - 'temperature': 10.0, - 'wind_bearing': 'NW', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-27T00:00:00+00:00', - 'precipitation_probability': 11, - 'temperature': 9.0, - 'wind_bearing': 'WNW', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-27T03:00:00+00:00', - 'precipitation_probability': 12, - 'temperature': 8.0, - 'wind_bearing': 'WNW', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-27T06:00:00+00:00', - 'precipitation_probability': 14, - 'temperature': 8.0, - 'wind_bearing': 'NW', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-27T09:00:00+00:00', - 'precipitation_probability': 6, - 'temperature': 9.0, - 'wind_bearing': 'NW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-27T12:00:00+00:00', - 'precipitation_probability': 4, - 'temperature': 10.0, - 'wind_bearing': 'NW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'sunny', - 'datetime': '2020-04-27T15:00:00+00:00', - 'precipitation_probability': 1, - 'temperature': 10.0, - 'wind_bearing': 'NW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'sunny', - 'datetime': '2020-04-27T18:00:00+00:00', - 'precipitation_probability': 1, - 'temperature': 10.0, - 'wind_bearing': 'NW', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'clear-night', - 'datetime': '2020-04-27T21:00:00+00:00', - 'precipitation_probability': 1, - 'temperature': 9.0, - 'wind_bearing': 'NW', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'clear-night', - 'datetime': '2020-04-28T00:00:00+00:00', - 'precipitation_probability': 2, - 'temperature': 8.0, - 'wind_bearing': 'NNW', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'clear-night', - 'datetime': '2020-04-28T03:00:00+00:00', - 'precipitation_probability': 3, - 'temperature': 7.0, - 'wind_bearing': 'W', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'sunny', - 'datetime': '2020-04-28T06:00:00+00:00', - 'precipitation_probability': 5, - 'temperature': 6.0, - 'wind_bearing': 'S', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-28T09:00:00+00:00', - 'precipitation_probability': 6, - 'temperature': 9.0, - 'wind_bearing': 'ENE', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-28T12:00:00+00:00', - 'precipitation_probability': 10, - 'temperature': 11.0, - 'wind_bearing': 'ENE', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-28T15:00:00+00:00', - 'precipitation_probability': 10, - 'temperature': 12.0, - 'wind_bearing': 'N', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-28T18:00:00+00:00', - 'precipitation_probability': 10, - 'temperature': 11.0, - 'wind_bearing': 'N', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-28T21:00:00+00:00', - 'precipitation_probability': 9, - 'temperature': 10.0, - 'wind_bearing': 'NNE', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-29T00:00:00+00:00', - 'precipitation_probability': 6, - 'temperature': 9.0, - 'wind_bearing': 'E', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-29T03:00:00+00:00', - 'precipitation_probability': 3, - 'temperature': 8.0, - 'wind_bearing': 'SSE', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-29T06:00:00+00:00', - 'precipitation_probability': 9, - 'temperature': 8.0, - 'wind_bearing': 'SE', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-29T09:00:00+00:00', - 'precipitation_probability': 12, - 'temperature': 10.0, - 'wind_bearing': 'SE', - 'wind_speed': 17.7, - }), - dict({ - 'condition': 'rainy', - 'datetime': '2020-04-29T12:00:00+00:00', - 'precipitation_probability': 47, - 'temperature': 12.0, - 'wind_bearing': 'SE', - 'wind_speed': 20.92, - }), - dict({ - 'condition': 'pouring', - 'datetime': '2020-04-29T15:00:00+00:00', - 'precipitation_probability': 59, - 'temperature': 13.0, - 'wind_bearing': 'SSE', - 'wind_speed': 20.92, - }), - dict({ - 'condition': 'rainy', - 'datetime': '2020-04-29T18:00:00+00:00', - 'precipitation_probability': 39, - 'temperature': 12.0, - 'wind_bearing': 'SSE', - 'wind_speed': 17.7, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-29T21:00:00+00:00', - 'precipitation_probability': 19, - 'temperature': 11.0, - 'wind_bearing': 'SSE', - 'wind_speed': 20.92, - }), - ]) -# --- -# name: test_forecast_subscription[weather.met_office_wavertree_daily] - list([ - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-26T12:00:00+00:00', - 'precipitation_probability': 9, - 'temperature': 13.0, - 'wind_bearing': 'WNW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-27T12:00:00+00:00', - 'precipitation_probability': 14, - 'temperature': 11.0, - 'wind_bearing': 'NW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-28T12:00:00+00:00', - 'precipitation_probability': 10, - 'temperature': 12.0, - 'wind_bearing': 'ENE', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'rainy', - 'datetime': '2020-04-29T12:00:00+00:00', - 'precipitation_probability': 59, - 'temperature': 13.0, - 'wind_bearing': 'SE', - 'wind_speed': 20.92, - }), - ]) -# --- -# name: test_forecast_subscription[weather.met_office_wavertree_daily].1 - list([ - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-26T12:00:00+00:00', - 'precipitation_probability': 9, - 'temperature': 13.0, - 'wind_bearing': 'WNW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-27T12:00:00+00:00', - 'precipitation_probability': 14, - 'temperature': 11.0, - 'wind_bearing': 'NW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-28T12:00:00+00:00', - 'precipitation_probability': 10, - 'temperature': 12.0, - 'wind_bearing': 'ENE', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'rainy', - 'datetime': '2020-04-29T12:00:00+00:00', - 'precipitation_probability': 59, - 'temperature': 13.0, - 'wind_bearing': 'SE', - 'wind_speed': 20.92, - }), - ]) -# --- -# name: test_forecast_subscription[weather.met_office_wavertree_daily].2 - list([ - dict({ - 'condition': 'sunny', - 'datetime': '2020-04-25T15:00:00+00:00', - 'precipitation_probability': 1, - 'temperature': 19.0, - 'wind_bearing': 'S', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-25T18:00:00+00:00', - 'precipitation_probability': 2, - 'temperature': 17.0, - 'wind_bearing': 'WNW', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-25T21:00:00+00:00', - 'precipitation_probability': 1, - 'temperature': 14.0, - 'wind_bearing': 'NW', - 'wind_speed': 3.22, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-26T00:00:00+00:00', - 'precipitation_probability': 1, - 'temperature': 13.0, - 'wind_bearing': 'WSW', - 'wind_speed': 3.22, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-26T03:00:00+00:00', - 'precipitation_probability': 2, - 'temperature': 12.0, - 'wind_bearing': 'WNW', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-26T06:00:00+00:00', - 'precipitation_probability': 5, - 'temperature': 11.0, - 'wind_bearing': 'NW', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-26T09:00:00+00:00', - 'precipitation_probability': 5, - 'temperature': 12.0, - 'wind_bearing': 'WNW', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-26T12:00:00+00:00', - 'precipitation_probability': 6, - 'temperature': 12.0, - 'wind_bearing': 'WNW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-26T15:00:00+00:00', - 'precipitation_probability': 5, - 'temperature': 12.0, - 'wind_bearing': 'NW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-26T18:00:00+00:00', - 'precipitation_probability': 9, - 'temperature': 11.0, - 'wind_bearing': 'NW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-26T21:00:00+00:00', - 'precipitation_probability': 9, - 'temperature': 10.0, - 'wind_bearing': 'NW', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-27T00:00:00+00:00', - 'precipitation_probability': 11, - 'temperature': 9.0, - 'wind_bearing': 'WNW', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-27T03:00:00+00:00', - 'precipitation_probability': 12, - 'temperature': 8.0, - 'wind_bearing': 'WNW', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-27T06:00:00+00:00', - 'precipitation_probability': 14, - 'temperature': 8.0, - 'wind_bearing': 'NW', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-27T09:00:00+00:00', - 'precipitation_probability': 6, - 'temperature': 9.0, - 'wind_bearing': 'NW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-27T12:00:00+00:00', - 'precipitation_probability': 4, - 'temperature': 10.0, - 'wind_bearing': 'NW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'sunny', - 'datetime': '2020-04-27T15:00:00+00:00', - 'precipitation_probability': 1, - 'temperature': 10.0, - 'wind_bearing': 'NW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'sunny', - 'datetime': '2020-04-27T18:00:00+00:00', - 'precipitation_probability': 1, - 'temperature': 10.0, - 'wind_bearing': 'NW', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'clear-night', - 'datetime': '2020-04-27T21:00:00+00:00', - 'precipitation_probability': 1, - 'temperature': 9.0, - 'wind_bearing': 'NW', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'clear-night', - 'datetime': '2020-04-28T00:00:00+00:00', - 'precipitation_probability': 2, - 'temperature': 8.0, - 'wind_bearing': 'NNW', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'clear-night', - 'datetime': '2020-04-28T03:00:00+00:00', - 'precipitation_probability': 3, - 'temperature': 7.0, - 'wind_bearing': 'W', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'sunny', - 'datetime': '2020-04-28T06:00:00+00:00', - 'precipitation_probability': 5, - 'temperature': 6.0, - 'wind_bearing': 'S', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-28T09:00:00+00:00', - 'precipitation_probability': 6, - 'temperature': 9.0, - 'wind_bearing': 'ENE', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-28T12:00:00+00:00', - 'precipitation_probability': 10, - 'temperature': 11.0, - 'wind_bearing': 'ENE', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-28T15:00:00+00:00', - 'precipitation_probability': 10, - 'temperature': 12.0, - 'wind_bearing': 'N', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-28T18:00:00+00:00', - 'precipitation_probability': 10, - 'temperature': 11.0, - 'wind_bearing': 'N', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-28T21:00:00+00:00', - 'precipitation_probability': 9, - 'temperature': 10.0, - 'wind_bearing': 'NNE', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-29T00:00:00+00:00', - 'precipitation_probability': 6, - 'temperature': 9.0, - 'wind_bearing': 'E', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-29T03:00:00+00:00', - 'precipitation_probability': 3, - 'temperature': 8.0, - 'wind_bearing': 'SSE', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-29T06:00:00+00:00', - 'precipitation_probability': 9, - 'temperature': 8.0, - 'wind_bearing': 'SE', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-29T09:00:00+00:00', - 'precipitation_probability': 12, - 'temperature': 10.0, - 'wind_bearing': 'SE', - 'wind_speed': 17.7, - }), - dict({ - 'condition': 'rainy', - 'datetime': '2020-04-29T12:00:00+00:00', - 'precipitation_probability': 47, - 'temperature': 12.0, - 'wind_bearing': 'SE', - 'wind_speed': 20.92, - }), - dict({ - 'condition': 'pouring', - 'datetime': '2020-04-29T15:00:00+00:00', - 'precipitation_probability': 59, - 'temperature': 13.0, - 'wind_bearing': 'SSE', - 'wind_speed': 20.92, - }), - dict({ - 'condition': 'rainy', - 'datetime': '2020-04-29T18:00:00+00:00', - 'precipitation_probability': 39, - 'temperature': 12.0, - 'wind_bearing': 'SSE', - 'wind_speed': 17.7, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-29T21:00:00+00:00', - 'precipitation_probability': 19, - 'temperature': 11.0, - 'wind_bearing': 'SSE', - 'wind_speed': 20.92, - }), - ]) -# --- -# name: test_forecast_subscription[weather.met_office_wavertree_daily].3 +# name: test_forecast_subscription[hourly].1 list([ dict({ 'condition': 'sunny', diff --git a/tests/components/metoffice/test_config_flow.py b/tests/components/metoffice/test_config_flow.py index 020f5ff154b..e4424b0b394 100644 --- a/tests/components/metoffice/test_config_flow.py +++ b/tests/components/metoffice/test_config_flow.py @@ -1,4 +1,5 @@ """Test the National Weather Service (NWS) config flow.""" + import json from unittest.mock import patch @@ -25,7 +26,7 @@ async def test_form(hass: HomeAssistant, requests_mock: requests_mock.Mocker) -> hass.config.longitude = TEST_LONGITUDE_WAVERTREE # all metoffice test data encapsulated in here - mock_json = json.loads(load_fixture("metoffice.json")) + mock_json = json.loads(load_fixture("metoffice.json", "metoffice")) all_sites = json.dumps(mock_json["all_sites"]) requests_mock.get("/public/data/val/wxfcs/all/json/sitelist/", text=all_sites) @@ -63,7 +64,7 @@ async def test_form_already_configured( hass.config.longitude = TEST_LONGITUDE_WAVERTREE # all metoffice test data encapsulated in here - mock_json = json.loads(load_fixture("metoffice.json")) + mock_json = json.loads(load_fixture("metoffice.json", "metoffice")) all_sites = json.dumps(mock_json["all_sites"]) diff --git a/tests/components/metoffice/test_init.py b/tests/components/metoffice/test_init.py index 10ed0a83f0c..159587ca7c1 100644 --- a/tests/components/metoffice/test_init.py +++ b/tests/components/metoffice/test_init.py @@ -1,4 +1,5 @@ """Tests for metoffice init.""" + from __future__ import annotations import datetime diff --git a/tests/components/metoffice/test_sensor.py b/tests/components/metoffice/test_sensor.py index 6e40dd66efe..6bddd1d2596 100644 --- a/tests/components/metoffice/test_sensor.py +++ b/tests/components/metoffice/test_sensor.py @@ -1,4 +1,5 @@ """The tests for the Met Office sensor component.""" + import datetime import json @@ -30,7 +31,7 @@ async def test_one_sensor_site_running( ) -> None: """Test the Met Office sensor platform.""" # all metoffice test data encapsulated in here - mock_json = json.loads(load_fixture("metoffice.json")) + mock_json = json.loads(load_fixture("metoffice.json", "metoffice")) all_sites = json.dumps(mock_json["all_sites"]) wavertree_hourly = json.dumps(mock_json["wavertree_hourly"]) wavertree_daily = json.dumps(mock_json["wavertree_daily"]) @@ -79,7 +80,7 @@ async def test_two_sensor_sites_running( """Test we handle two sets of sensors running for two different sites.""" # all metoffice test data encapsulated in here - mock_json = json.loads(load_fixture("metoffice.json")) + mock_json = json.loads(load_fixture("metoffice.json", "metoffice")) all_sites = json.dumps(mock_json["all_sites"]) wavertree_hourly = json.dumps(mock_json["wavertree_hourly"]) wavertree_daily = json.dumps(mock_json["wavertree_daily"]) diff --git a/tests/components/metoffice/test_weather.py b/tests/components/metoffice/test_weather.py index 19c27873d5e..64a85897738 100644 --- a/tests/components/metoffice/test_weather.py +++ b/tests/components/metoffice/test_weather.py @@ -1,4 +1,5 @@ """The tests for the Met Office sensor component.""" + import datetime from datetime import timedelta import json @@ -47,7 +48,7 @@ def no_sensor(): async def wavertree_data(requests_mock: requests_mock.Mocker) -> dict[str, _Matcher]: """Mock data for the Wavertree location.""" # all metoffice test data encapsulated in here - mock_json = json.loads(load_fixture("metoffice.json")) + mock_json = json.loads(load_fixture("metoffice.json", "metoffice")) all_sites = json.dumps(mock_json["all_sites"]) wavertree_hourly = json.dumps(mock_json["wavertree_hourly"]) wavertree_daily = json.dumps(mock_json["wavertree_daily"]) @@ -108,14 +109,6 @@ async def test_site_cannot_update( ) -> None: """Test we handle cannot connect error.""" - # Pre-create the hourly entity - entity_registry.async_get_or_create( - WEATHER_DOMAIN, - DOMAIN, - "53.38374_-2.90929", - suggested_object_id="met_office_wavertree_3_hourly", - ) - entry = MockConfigEntry( domain=DOMAIN, data=METOFFICE_CONFIG_WAVERTREE, @@ -124,9 +117,6 @@ async def test_site_cannot_update( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - weather = hass.states.get("weather.met_office_wavertree_3_hourly") - assert weather - weather = hass.states.get("weather.met_office_wavertree_daily") assert weather @@ -135,10 +125,7 @@ async def test_site_cannot_update( future_time = utcnow() + timedelta(minutes=20) async_fire_time_changed(hass, future_time) - await hass.async_block_till_done() - - weather = hass.states.get("weather.met_office_wavertree_3_hourly") - assert weather.state == STATE_UNAVAILABLE + await hass.async_block_till_done(wait_background_tasks=True) weather = hass.states.get("weather.met_office_wavertree_daily") assert weather.state == STATE_UNAVAILABLE @@ -153,14 +140,6 @@ async def test_one_weather_site_running( ) -> None: """Test the Met Office weather platform.""" - # Pre-create the hourly entity - entity_registry.async_get_or_create( - WEATHER_DOMAIN, - DOMAIN, - "53.38374_-2.90929", - suggested_object_id="met_office_wavertree_3_hourly", - ) - entry = MockConfigEntry( domain=DOMAIN, data=METOFFICE_CONFIG_WAVERTREE, @@ -174,30 +153,6 @@ async def test_one_weather_site_running( device_wavertree = dev_reg.async_get_device(identifiers=DEVICE_KEY_WAVERTREE) assert device_wavertree.name == "Met Office Wavertree" - # Wavertree 3-hourly weather platform expected results - weather = hass.states.get("weather.met_office_wavertree_3_hourly") - assert weather - - assert weather.state == "sunny" - assert weather.attributes.get("temperature") == 17 - assert weather.attributes.get("wind_speed") == 14.48 - assert weather.attributes.get("wind_speed_unit") == "km/h" - assert weather.attributes.get("wind_bearing") == "SSE" - assert weather.attributes.get("humidity") == 50 - - # Forecasts added - just pick out 1 entry to check - assert len(weather.attributes.get("forecast")) == 35 - - assert ( - weather.attributes.get("forecast")[26]["datetime"] - == "2020-04-28T21:00:00+00:00" - ) - assert weather.attributes.get("forecast")[26]["condition"] == "cloudy" - assert weather.attributes.get("forecast")[26]["precipitation_probability"] == 9 - assert weather.attributes.get("forecast")[26]["temperature"] == 10 - assert weather.attributes.get("forecast")[26]["wind_speed"] == 6.44 - assert weather.attributes.get("forecast")[26]["wind_bearing"] == "NNE" - # Wavertree daily weather platform expected results weather = hass.states.get("weather.met_office_wavertree_daily") assert weather @@ -208,19 +163,6 @@ async def test_one_weather_site_running( assert weather.attributes.get("wind_bearing") == "SSE" assert weather.attributes.get("humidity") == 50 - # Also has Forecasts added - again, just pick out 1 entry to check - # ensures that daily filters out multiple results per day - assert len(weather.attributes.get("forecast")) == 4 - - assert ( - weather.attributes.get("forecast")[3]["datetime"] == "2020-04-29T12:00:00+00:00" - ) - assert weather.attributes.get("forecast")[3]["condition"] == "rainy" - assert weather.attributes.get("forecast")[3]["precipitation_probability"] == 59 - assert weather.attributes.get("forecast")[3]["temperature"] == 13 - assert weather.attributes.get("forecast")[3]["wind_speed"] == 20.92 - assert weather.attributes.get("forecast")[3]["wind_bearing"] == "SE" - @pytest.mark.freeze_time(datetime.datetime(2020, 4, 25, 12, tzinfo=datetime.UTC)) async def test_two_weather_sites_running( @@ -231,22 +173,8 @@ async def test_two_weather_sites_running( ) -> None: """Test we handle two different weather sites both running.""" - # Pre-create the hourly entities - entity_registry.async_get_or_create( - WEATHER_DOMAIN, - DOMAIN, - "53.38374_-2.90929", - suggested_object_id="met_office_wavertree_3_hourly", - ) - entity_registry.async_get_or_create( - WEATHER_DOMAIN, - DOMAIN, - "52.75556_0.44231", - suggested_object_id="met_office_king_s_lynn_3_hourly", - ) - # all metoffice test data encapsulated in here - mock_json = json.loads(load_fixture("metoffice.json")) + mock_json = json.loads(load_fixture("metoffice.json", "metoffice")) kingslynn_hourly = json.dumps(mock_json["kingslynn_hourly"]) kingslynn_daily = json.dumps(mock_json["kingslynn_daily"]) @@ -278,30 +206,6 @@ async def test_two_weather_sites_running( device_wavertree = dev_reg.async_get_device(identifiers=DEVICE_KEY_WAVERTREE) assert device_wavertree.name == "Met Office Wavertree" - # Wavertree 3-hourly weather platform expected results - weather = hass.states.get("weather.met_office_wavertree_3_hourly") - assert weather - - assert weather.state == "sunny" - assert weather.attributes.get("temperature") == 17 - assert weather.attributes.get("wind_speed") == 14.48 - assert weather.attributes.get("wind_speed_unit") == "km/h" - assert weather.attributes.get("wind_bearing") == "SSE" - assert weather.attributes.get("humidity") == 50 - - # Forecasts added - just pick out 1 entry to check - assert len(weather.attributes.get("forecast")) == 35 - - assert ( - weather.attributes.get("forecast")[18]["datetime"] - == "2020-04-27T21:00:00+00:00" - ) - assert weather.attributes.get("forecast")[18]["condition"] == "clear-night" - assert weather.attributes.get("forecast")[18]["precipitation_probability"] == 1 - assert weather.attributes.get("forecast")[18]["temperature"] == 9 - assert weather.attributes.get("forecast")[18]["wind_speed"] == 6.44 - assert weather.attributes.get("forecast")[18]["wind_bearing"] == "NW" - # Wavertree daily weather platform expected results weather = hass.states.get("weather.met_office_wavertree_daily") assert weather @@ -313,43 +217,6 @@ async def test_two_weather_sites_running( assert weather.attributes.get("wind_bearing") == "SSE" assert weather.attributes.get("humidity") == 50 - # Also has Forecasts added - again, just pick out 1 entry to check - # ensures that daily filters out multiple results per day - assert len(weather.attributes.get("forecast")) == 4 - - assert ( - weather.attributes.get("forecast")[3]["datetime"] == "2020-04-29T12:00:00+00:00" - ) - assert weather.attributes.get("forecast")[3]["condition"] == "rainy" - assert weather.attributes.get("forecast")[3]["precipitation_probability"] == 59 - assert weather.attributes.get("forecast")[3]["temperature"] == 13 - assert weather.attributes.get("forecast")[3]["wind_speed"] == 20.92 - assert weather.attributes.get("forecast")[3]["wind_bearing"] == "SE" - - # King's Lynn 3-hourly weather platform expected results - weather = hass.states.get("weather.met_office_king_s_lynn_3_hourly") - assert weather - - assert weather.state == "sunny" - assert weather.attributes.get("temperature") == 14 - assert weather.attributes.get("wind_speed") == 3.22 - assert weather.attributes.get("wind_speed_unit") == "km/h" - assert weather.attributes.get("wind_bearing") == "E" - assert weather.attributes.get("humidity") == 60 - - # Also has Forecast added - just pick out 1 entry to check - assert len(weather.attributes.get("forecast")) == 35 - - assert ( - weather.attributes.get("forecast")[18]["datetime"] - == "2020-04-27T21:00:00+00:00" - ) - assert weather.attributes.get("forecast")[18]["condition"] == "cloudy" - assert weather.attributes.get("forecast")[18]["precipitation_probability"] == 9 - assert weather.attributes.get("forecast")[18]["temperature"] == 10 - assert weather.attributes.get("forecast")[18]["wind_speed"] == 11.27 - assert weather.attributes.get("forecast")[18]["wind_bearing"] == "SE" - # King's Lynn daily weather platform expected results weather = hass.states.get("weather.met_office_king_s_lynn_daily") assert weather @@ -361,19 +228,6 @@ async def test_two_weather_sites_running( assert weather.attributes.get("wind_bearing") == "ESE" assert weather.attributes.get("humidity") == 75 - # All should have Forecast added - again, just picking out 1 entry to check - # ensures daily filters out multiple results per day - assert len(weather.attributes.get("forecast")) == 4 - - assert ( - weather.attributes.get("forecast")[2]["datetime"] == "2020-04-28T12:00:00+00:00" - ) - assert weather.attributes.get("forecast")[2]["condition"] == "cloudy" - assert weather.attributes.get("forecast")[2]["precipitation_probability"] == 14 - assert weather.attributes.get("forecast")[2]["temperature"] == 11 - assert weather.attributes.get("forecast")[2]["wind_speed"] == 11.27 - assert weather.attributes.get("forecast")[2]["wind_bearing"] == "ESE" - @pytest.mark.freeze_time(datetime.datetime(2020, 4, 25, 12, tzinfo=datetime.UTC)) async def test_new_config_entry( @@ -394,33 +248,6 @@ async def test_new_config_entry( assert len(er.async_entries_for_config_entry(entity_registry, entry.entry_id)) == 1 -@pytest.mark.freeze_time(datetime.datetime(2020, 4, 25, 12, tzinfo=datetime.UTC)) -async def test_legacy_config_entry( - hass: HomeAssistant, entity_registry: er.EntityRegistry, no_sensor, wavertree_data -) -> None: - """Test the expected entities are created.""" - # Pre-create the hourly entity - entity_registry.async_get_or_create( - WEATHER_DOMAIN, - DOMAIN, - "53.38374_-2.90929", - suggested_object_id="met_office_wavertree_3_hourly", - ) - - entry = MockConfigEntry( - domain=DOMAIN, - data=METOFFICE_CONFIG_WAVERTREE, - ) - entry.add_to_hass(hass) - - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - assert len(hass.states.async_entity_ids("weather")) == 2 - entry = hass.config_entries.async_entries()[0] - assert len(er.async_entries_for_config_entry(entity_registry, entry.entry_id)) == 2 - - @pytest.mark.freeze_time(datetime.datetime(2020, 4, 25, 12, tzinfo=datetime.UTC)) @pytest.mark.parametrize( ("service"), @@ -470,7 +297,7 @@ async def test_forecast_service( # Trigger data refetch freezer.tick(DEFAULT_SCAN_INTERVAL + timedelta(seconds=1)) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert wavertree_data["wavertree_daily_mock"].call_count == 2 assert wavertree_data["wavertree_hourly_mock"].call_count == 1 @@ -497,7 +324,7 @@ async def test_forecast_service( freezer.tick(DEFAULT_SCAN_INTERVAL + timedelta(seconds=1)) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) response = await hass.services.async_call( WEATHER_DOMAIN, @@ -512,27 +339,11 @@ async def test_forecast_service( assert response == snapshot -@pytest.mark.parametrize( - "entity_id", - [ - "weather.met_office_wavertree_3_hourly", - "weather.met_office_wavertree_daily", - ], -) @pytest.mark.freeze_time(datetime.datetime(2020, 4, 25, 12, tzinfo=datetime.UTC)) -async def test_forecast_subscription( - hass: HomeAssistant, - hass_ws_client: WebSocketGenerator, - entity_registry: er.EntityRegistry, - freezer: FrozenDateTimeFactory, - snapshot: SnapshotAssertion, - no_sensor, - wavertree_data: dict[str, _Matcher], - entity_id: str, +async def test_legacy_config_entry_is_removed( + hass: HomeAssistant, entity_registry: er.EntityRegistry, no_sensor, wavertree_data ) -> None: - """Test multiple forecast.""" - client = await hass_ws_client(hass) - + """Test the expected entities are created.""" # Pre-create the hourly entity entity_registry.async_get_or_create( WEATHER_DOMAIN, @@ -550,47 +361,75 @@ async def test_forecast_subscription( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - for forecast_type in ("daily", "hourly"): - await client.send_json_auto_id( - { - "type": "weather/subscribe_forecast", - "forecast_type": forecast_type, - "entity_id": entity_id, - } - ) - msg = await client.receive_json() - assert msg["success"] - assert msg["result"] is None - subscription_id = msg["id"] + assert len(hass.states.async_entity_ids("weather")) == 1 + entry = hass.config_entries.async_entries()[0] + assert len(er.async_entries_for_config_entry(entity_registry, entry.entry_id)) == 1 - msg = await client.receive_json() - assert msg["id"] == subscription_id - assert msg["type"] == "event" - forecast1 = msg["event"]["forecast"] - assert forecast1 != [] - assert forecast1 == snapshot +@pytest.mark.freeze_time(datetime.datetime(2020, 4, 25, 12, tzinfo=datetime.UTC)) +@pytest.mark.parametrize("forecast_type", ["daily", "hourly"]) +async def test_forecast_subscription( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + entity_registry: er.EntityRegistry, + freezer: FrozenDateTimeFactory, + snapshot: SnapshotAssertion, + no_sensor, + wavertree_data: dict[str, _Matcher], + forecast_type: str, +) -> None: + """Test multiple forecast.""" + client = await hass_ws_client(hass) - freezer.tick(DEFAULT_SCAN_INTERVAL + timedelta(seconds=1)) - async_fire_time_changed(hass) - await hass.async_block_till_done() - msg = await client.receive_json() + entry = MockConfigEntry( + domain=DOMAIN, + data=METOFFICE_CONFIG_WAVERTREE, + ) + entry.add_to_hass(hass) - assert msg["id"] == subscription_id - assert msg["type"] == "event" - forecast2 = msg["event"]["forecast"] + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() - assert forecast2 != [] - assert forecast2 == snapshot + await client.send_json_auto_id( + { + "type": "weather/subscribe_forecast", + "forecast_type": forecast_type, + "entity_id": "weather.met_office_wavertree_daily", + } + ) + msg = await client.receive_json() + assert msg["success"] + assert msg["result"] is None + subscription_id = msg["id"] - await client.send_json_auto_id( - { - "type": "unsubscribe_events", - "subscription": subscription_id, - } - ) - freezer.tick(timedelta(seconds=1)) - async_fire_time_changed(hass) - await hass.async_block_till_done() - msg = await client.receive_json() - assert msg["success"] + msg = await client.receive_json() + assert msg["id"] == subscription_id + assert msg["type"] == "event" + forecast1 = msg["event"]["forecast"] + + assert forecast1 != [] + assert forecast1 == snapshot + + freezer.tick(DEFAULT_SCAN_INTERVAL + timedelta(seconds=1)) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + msg = await client.receive_json() + + assert msg["id"] == subscription_id + assert msg["type"] == "event" + forecast2 = msg["event"]["forecast"] + + assert forecast2 != [] + assert forecast2 == snapshot + + await client.send_json_auto_id( + { + "type": "unsubscribe_events", + "subscription": subscription_id, + } + ) + freezer.tick(timedelta(seconds=1)) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + msg = await client.receive_json() + assert msg["success"] diff --git a/tests/components/mfi/test_sensor.py b/tests/components/mfi/test_sensor.py index 842812f68f5..24f6a52fa5c 100644 --- a/tests/components/mfi/test_sensor.py +++ b/tests/components/mfi/test_sensor.py @@ -1,4 +1,5 @@ """The tests for the mFi sensor platform.""" + from copy import deepcopy import unittest.mock as mock @@ -92,11 +93,12 @@ async def test_setup_with_tls_disabled(hass: HomeAssistant) -> None: async def test_setup_adds_proper_devices(hass: HomeAssistant) -> None: """Test if setup adds devices.""" - with mock.patch( - "homeassistant.components.mfi.sensor.MFiClient" - ) as mock_client, mock.patch( - "homeassistant.components.mfi.sensor.MfiSensor", side_effect=mfi.MfiSensor - ) as mock_sensor: + with ( + mock.patch("homeassistant.components.mfi.sensor.MFiClient") as mock_client, + mock.patch( + "homeassistant.components.mfi.sensor.MfiSensor", side_effect=mfi.MfiSensor + ) as mock_sensor, + ): ports = { i: mock.MagicMock(model=model, label=f"Port {i}", value=0) for i, model in enumerate(mfi.SENSOR_MODELS) diff --git a/tests/components/mfi/test_switch.py b/tests/components/mfi/test_switch.py index 00daa4c353d..6c69787beef 100644 --- a/tests/components/mfi/test_switch.py +++ b/tests/components/mfi/test_switch.py @@ -1,4 +1,5 @@ """The tests for the mFi switch platform.""" + import unittest.mock as mock import pytest @@ -26,11 +27,12 @@ GOOD_CONFIG = { async def test_setup_adds_proper_devices(hass: HomeAssistant) -> None: """Test if setup adds devices.""" - with mock.patch( - "homeassistant.components.mfi.switch.MFiClient" - ) as mock_client, mock.patch( - "homeassistant.components.mfi.switch.MfiSwitch", side_effect=mfi.MfiSwitch - ) as mock_switch: + with ( + mock.patch("homeassistant.components.mfi.switch.MFiClient") as mock_client, + mock.patch( + "homeassistant.components.mfi.switch.MfiSwitch", side_effect=mfi.MfiSwitch + ) as mock_switch, + ): ports = { i: mock.MagicMock( model=model, label=f"Port {i}", output=False, data={}, ident=f"abcd-{i}" diff --git a/tests/components/microbees/__init__.py b/tests/components/microbees/__init__.py index a33e1c59f5e..59250efa5ee 100644 --- a/tests/components/microbees/__init__.py +++ b/tests/components/microbees/__init__.py @@ -1,4 +1,5 @@ """Tests for the MicroBees component.""" + from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry diff --git a/tests/components/microbees/conftest.py b/tests/components/microbees/conftest.py index a27d1e01194..60df0377e4d 100644 --- a/tests/components/microbees/conftest.py +++ b/tests/components/microbees/conftest.py @@ -1,4 +1,5 @@ """Conftest for microBees tests.""" + import time from unittest.mock import AsyncMock, patch @@ -83,11 +84,14 @@ def mock_microbees(): mock.getBees.return_value = devices mock.getMyProfile.return_value = profile - with patch( - "homeassistant.components.microbees.config_flow.MicroBees", - return_value=mock, - ) as mock, patch( - "homeassistant.components.microbees.MicroBees", - return_value=mock, + with ( + patch( + "homeassistant.components.microbees.config_flow.MicroBees", + return_value=mock, + ) as mock, + patch( + "homeassistant.components.microbees.MicroBees", + return_value=mock, + ), ): yield mock diff --git a/tests/components/microbees/test_config_flow.py b/tests/components/microbees/test_config_flow.py index 62ecbbeb9be..5e57c723f5b 100644 --- a/tests/components/microbees/test_config_flow.py +++ b/tests/components/microbees/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for config flow.""" + from unittest.mock import AsyncMock, patch from microBeesPy import MicroBeesException diff --git a/tests/components/microsoft/test_tts.py b/tests/components/microsoft/test_tts.py index bc6a3ac7dd7..c395dc82419 100644 --- a/tests/components/microsoft/test_tts.py +++ b/tests/components/microsoft/test_tts.py @@ -1,4 +1,5 @@ """Tests for Microsoft text-to-speech.""" + from http import HTTPStatus from unittest.mock import patch @@ -60,6 +61,7 @@ async def test_service_say( await async_setup_component( hass, tts.DOMAIN, {tts.DOMAIN: {"platform": "microsoft", "api_key": ""}} ) + await hass.async_block_till_done() await hass.services.async_call( tts.DOMAIN, @@ -109,6 +111,7 @@ async def test_service_say_en_gb_config( } }, ) + await hass.async_block_till_done() await hass.services.async_call( tts.DOMAIN, @@ -150,6 +153,7 @@ async def test_service_say_en_gb_service( tts.DOMAIN, {tts.DOMAIN: {"platform": "microsoft", "api_key": ""}}, ) + await hass.async_block_till_done() await hass.services.async_call( tts.DOMAIN, @@ -200,6 +204,7 @@ async def test_service_say_fa_ir_config( } }, ) + await hass.async_block_till_done() await hass.services.async_call( tts.DOMAIN, @@ -245,6 +250,7 @@ async def test_service_say_fa_ir_service( } await async_setup_component(hass, tts.DOMAIN, config) + await hass.async_block_till_done() await hass.services.async_call( tts.DOMAIN, @@ -302,6 +308,7 @@ async def test_invalid_language(hass: HomeAssistant, mock_tts, calls) -> None: tts.DOMAIN, {tts.DOMAIN: {"platform": "microsoft", "api_key": "", "language": "en"}}, ) + await hass.async_block_till_done() with pytest.raises(ServiceNotFound): await hass.services.async_call( @@ -326,6 +333,7 @@ async def test_service_say_error( await async_setup_component( hass, tts.DOMAIN, {tts.DOMAIN: {"platform": "microsoft", "api_key": ""}} ) + await hass.async_block_till_done() await hass.services.async_call( tts.DOMAIN, diff --git a/tests/fixtures/microsoft_face_create_person.json b/tests/components/microsoft_face/fixtures/create_person.json similarity index 100% rename from tests/fixtures/microsoft_face_create_person.json rename to tests/components/microsoft_face/fixtures/create_person.json diff --git a/tests/fixtures/microsoft_face_persongroups.json b/tests/components/microsoft_face/fixtures/persongroups.json similarity index 100% rename from tests/fixtures/microsoft_face_persongroups.json rename to tests/components/microsoft_face/fixtures/persongroups.json diff --git a/tests/fixtures/microsoft_face_persons.json b/tests/components/microsoft_face/fixtures/persons.json similarity index 100% rename from tests/fixtures/microsoft_face_persons.json rename to tests/components/microsoft_face/fixtures/persons.json diff --git a/tests/components/microsoft_face/test_init.py b/tests/components/microsoft_face/test_init.py index affdbb4e932..63014a095c0 100644 --- a/tests/components/microsoft_face/test_init.py +++ b/tests/components/microsoft_face/test_init.py @@ -1,4 +1,5 @@ """The tests for the microsoft face platform.""" + from unittest.mock import patch import pytest @@ -133,15 +134,15 @@ async def test_setup_component_test_entities( """Set up component.""" aioclient_mock.get( ENDPOINT_URL.format("persongroups"), - text=load_fixture("microsoft_face_persongroups.json"), + text=load_fixture("persongroups.json", "microsoft_face"), ) aioclient_mock.get( ENDPOINT_URL.format("persongroups/test_group1/persons"), - text=load_fixture("microsoft_face_persons.json"), + text=load_fixture("persons.json", "microsoft_face"), ) aioclient_mock.get( ENDPOINT_URL.format("persongroups/test_group2/persons"), - text=load_fixture("microsoft_face_persons.json"), + text=load_fixture("persons.json", "microsoft_face"), ) with assert_setup_component(3, mf.DOMAIN): @@ -201,15 +202,15 @@ async def test_service_person( """Set up component, test person services.""" aioclient_mock.get( ENDPOINT_URL.format("persongroups"), - text=load_fixture("microsoft_face_persongroups.json"), + text=load_fixture("persongroups.json", "microsoft_face"), ) aioclient_mock.get( ENDPOINT_URL.format("persongroups/test_group1/persons"), - text=load_fixture("microsoft_face_persons.json"), + text=load_fixture("persons.json", "microsoft_face"), ) aioclient_mock.get( ENDPOINT_URL.format("persongroups/test_group2/persons"), - text=load_fixture("microsoft_face_persons.json"), + text=load_fixture("persons.json", "microsoft_face"), ) with assert_setup_component(3, mf.DOMAIN): @@ -219,7 +220,7 @@ async def test_service_person( aioclient_mock.post( ENDPOINT_URL.format("persongroups/test_group1/persons"), - text=load_fixture("microsoft_face_create_person.json"), + text=load_fixture("create_person.json", "microsoft_face"), ) aioclient_mock.delete( ENDPOINT_URL.format( @@ -273,15 +274,15 @@ async def test_service_face( """Set up component, test person face services.""" aioclient_mock.get( ENDPOINT_URL.format("persongroups"), - text=load_fixture("microsoft_face_persongroups.json"), + text=load_fixture("persongroups.json", "microsoft_face"), ) aioclient_mock.get( ENDPOINT_URL.format("persongroups/test_group1/persons"), - text=load_fixture("microsoft_face_persons.json"), + text=load_fixture("persons.json", "microsoft_face"), ) aioclient_mock.get( ENDPOINT_URL.format("persongroups/test_group2/persons"), - text=load_fixture("microsoft_face_persons.json"), + text=load_fixture("persons.json", "microsoft_face"), ) CONFIG["camera"] = {"platform": "demo"} diff --git a/tests/fixtures/microsoft_face_detect.json b/tests/components/microsoft_face_detect/fixtures/detect.json similarity index 100% rename from tests/fixtures/microsoft_face_detect.json rename to tests/components/microsoft_face_detect/fixtures/detect.json diff --git a/tests/components/microsoft_face_detect/fixtures/persongroups.json b/tests/components/microsoft_face_detect/fixtures/persongroups.json new file mode 100644 index 00000000000..5bbb8ceea29 --- /dev/null +++ b/tests/components/microsoft_face_detect/fixtures/persongroups.json @@ -0,0 +1,12 @@ +[ + { + "personGroupId": "test_group1", + "name": "test group1", + "userData": "test" + }, + { + "personGroupId": "test_group2", + "name": "test group2", + "userData": "test" + } +] diff --git a/tests/components/microsoft_face_detect/fixtures/persons.json b/tests/components/microsoft_face_detect/fixtures/persons.json new file mode 100644 index 00000000000..e604bcc2672 --- /dev/null +++ b/tests/components/microsoft_face_detect/fixtures/persons.json @@ -0,0 +1,21 @@ +[ + { + "personId": "25985303-c537-4467-b41d-bdb45cd95ca1", + "name": "Ryan", + "userData": "User-provided data attached to the person", + "persistedFaceIds": [ + "015839fb-fbd9-4f79-ace9-7675fc2f1dd9", + "fce92aed-d578-4d2e-8114-068f8af4492e", + "b64d5e15-8257-4af2-b20a-5a750f8940e7" + ] + }, + { + "personId": "2ae4935b-9659-44c3-977f-61fac20d0538", + "name": "David", + "userData": "User-provided data attached to the person", + "persistedFaceIds": [ + "30ea1073-cc9e-4652-b1e3-d08fb7b95315", + "fbd2a038-dbff-452c-8e79-2ee81b1aa84e" + ] + } +] diff --git a/tests/components/microsoft_face_detect/test_image_processing.py b/tests/components/microsoft_face_detect/test_image_processing.py index 349440124ff..0c0bcb59c0b 100644 --- a/tests/components/microsoft_face_detect/test_image_processing.py +++ b/tests/components/microsoft_face_detect/test_image_processing.py @@ -1,4 +1,5 @@ """The tests for the microsoft face detect platform.""" + from unittest.mock import PropertyMock, patch import pytest @@ -96,15 +97,15 @@ async def test_ms_detect_process_image( """Set up and scan a picture and test plates from event.""" aioclient_mock.get( ENDPOINT_URL.format("persongroups"), - text=load_fixture("microsoft_face_persongroups.json"), + text=load_fixture("persongroups.json", "microsoft_face_detect"), ) aioclient_mock.get( ENDPOINT_URL.format("persongroups/test_group1/persons"), - text=load_fixture("microsoft_face_persons.json"), + text=load_fixture("persons.json", "microsoft_face_detect"), ) aioclient_mock.get( ENDPOINT_URL.format("persongroups/test_group2/persons"), - text=load_fixture("microsoft_face_persons.json"), + text=load_fixture("persons.json", "microsoft_face_detect"), ) await async_setup_component(hass, ip.DOMAIN, CONFIG) @@ -126,7 +127,7 @@ async def test_ms_detect_process_image( aioclient_mock.post( ENDPOINT_URL.format("detect"), - text=load_fixture("microsoft_face_detect.json"), + text=load_fixture("detect.json", "microsoft_face_detect"), params={"returnFaceAttributes": "age,gender"}, ) diff --git a/tests/components/microsoft_face_identify/fixtures/detect.json b/tests/components/microsoft_face_identify/fixtures/detect.json new file mode 100644 index 00000000000..c43f4ec494e --- /dev/null +++ b/tests/components/microsoft_face_identify/fixtures/detect.json @@ -0,0 +1,27 @@ +[ + { + "faceId": "c5c24a82-6845-4031-9d5d-978df9175426", + "faceRectangle": { + "width": 78, + "height": 78, + "left": 394, + "top": 54 + }, + "faceAttributes": { + "age": 71.0, + "gender": "male", + "smile": 0.88, + "facialHair": { + "mustache": 0.8, + "beard": 0.1, + "sideburns": 0.02 + }, + "glasses": "sunglasses", + "headPose": { + "roll": 2.1, + "yaw": 3, + "pitch": 0 + } + } + } +] diff --git a/tests/fixtures/microsoft_face_identify.json b/tests/components/microsoft_face_identify/fixtures/identify.json similarity index 100% rename from tests/fixtures/microsoft_face_identify.json rename to tests/components/microsoft_face_identify/fixtures/identify.json diff --git a/tests/components/microsoft_face_identify/fixtures/persongroups.json b/tests/components/microsoft_face_identify/fixtures/persongroups.json new file mode 100644 index 00000000000..5bbb8ceea29 --- /dev/null +++ b/tests/components/microsoft_face_identify/fixtures/persongroups.json @@ -0,0 +1,12 @@ +[ + { + "personGroupId": "test_group1", + "name": "test group1", + "userData": "test" + }, + { + "personGroupId": "test_group2", + "name": "test group2", + "userData": "test" + } +] diff --git a/tests/components/microsoft_face_identify/fixtures/persons.json b/tests/components/microsoft_face_identify/fixtures/persons.json new file mode 100644 index 00000000000..e604bcc2672 --- /dev/null +++ b/tests/components/microsoft_face_identify/fixtures/persons.json @@ -0,0 +1,21 @@ +[ + { + "personId": "25985303-c537-4467-b41d-bdb45cd95ca1", + "name": "Ryan", + "userData": "User-provided data attached to the person", + "persistedFaceIds": [ + "015839fb-fbd9-4f79-ace9-7675fc2f1dd9", + "fce92aed-d578-4d2e-8114-068f8af4492e", + "b64d5e15-8257-4af2-b20a-5a750f8940e7" + ] + }, + { + "personId": "2ae4935b-9659-44c3-977f-61fac20d0538", + "name": "David", + "userData": "User-provided data attached to the person", + "persistedFaceIds": [ + "30ea1073-cc9e-4652-b1e3-d08fb7b95315", + "fbd2a038-dbff-452c-8e79-2ee81b1aa84e" + ] + } +] diff --git a/tests/components/microsoft_face_identify/test_image_processing.py b/tests/components/microsoft_face_identify/test_image_processing.py index 6581aea835f..6258448dd05 100644 --- a/tests/components/microsoft_face_identify/test_image_processing.py +++ b/tests/components/microsoft_face_identify/test_image_processing.py @@ -1,4 +1,5 @@ """The tests for the microsoft face identify platform.""" + from unittest.mock import PropertyMock, patch import pytest @@ -98,15 +99,15 @@ async def test_ms_identify_process_image( """Set up and scan a picture and test plates from event.""" aioclient_mock.get( ENDPOINT_URL.format("persongroups"), - text=load_fixture("microsoft_face_persongroups.json"), + text=load_fixture("persongroups.json", "microsoft_face_identify"), ) aioclient_mock.get( ENDPOINT_URL.format("persongroups/test_group1/persons"), - text=load_fixture("microsoft_face_persons.json"), + text=load_fixture("persons.json", "microsoft_face_identify"), ) aioclient_mock.get( ENDPOINT_URL.format("persongroups/test_group2/persons"), - text=load_fixture("microsoft_face_persons.json"), + text=load_fixture("persons.json", "microsoft_face_identify"), ) await async_setup_component(hass, ip.DOMAIN, CONFIG) @@ -128,11 +129,11 @@ async def test_ms_identify_process_image( aioclient_mock.post( ENDPOINT_URL.format("detect"), - text=load_fixture("microsoft_face_detect.json"), + text=load_fixture("detect.json", "microsoft_face_identify"), ) aioclient_mock.post( ENDPOINT_URL.format("identify"), - text=load_fixture("microsoft_face_identify.json"), + text=load_fixture("identify.json", "microsoft_face_identify"), ) common.async_scan(hass, entity_id="image_processing.test_local") diff --git a/tests/components/mikrotik/__init__.py b/tests/components/mikrotik/__init__.py index 8e3d5eda19d..ad8521c7787 100644 --- a/tests/components/mikrotik/__init__.py +++ b/tests/components/mikrotik/__init__.py @@ -1,4 +1,5 @@ """Tests for the Mikrotik component.""" + from __future__ import annotations from typing import Any @@ -207,8 +208,9 @@ async def setup_mikrotik_entry(hass: HomeAssistant, **kwargs: Any) -> None: ) config_entry.add_to_hass(hass) - with patch("librouteros.connect"), patch.object( - mikrotik.hub.MikrotikData, "command", new=mock_command + 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 ea73e581710..f48446e3e14 100644 --- a/tests/components/mikrotik/test_config_flow.py +++ b/tests/components/mikrotik/test_config_flow.py @@ -1,4 +1,5 @@ """Test Mikrotik setup process.""" + from unittest.mock import patch import librouteros diff --git a/tests/components/mikrotik/test_device_tracker.py b/tests/components/mikrotik/test_device_tracker.py index bf1dc3abedf..89dc37fd781 100644 --- a/tests/components/mikrotik/test_device_tracker.py +++ b/tests/components/mikrotik/test_device_tracker.py @@ -1,4 +1,5 @@ """The tests for the Mikrotik device tracker platform.""" + from __future__ import annotations from datetime import timedelta @@ -87,7 +88,7 @@ async def test_device_trackers( WIRELESS_DATA.append(DEVICE_2_WIRELESS) async_fire_time_changed(hass, utcnow() + timedelta(seconds=10)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) device_2 = hass.states.get("device_tracker.device_2") assert device_2 @@ -100,7 +101,7 @@ async def test_device_trackers( del WIRELESS_DATA[1] # device 2 is removed from wireless list with freeze_time(utcnow() + timedelta(minutes=4)): async_fire_time_changed(hass, utcnow() + timedelta(minutes=4)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) device_2 = hass.states.get("device_tracker.device_2") assert device_2 @@ -109,7 +110,7 @@ async def test_device_trackers( # test state changes to away if last_seen past consider_home_interval with freeze_time(utcnow() + timedelta(minutes=6)): async_fire_time_changed(hass, utcnow() + timedelta(minutes=6)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) device_2 = hass.states.get("device_tracker.device_2") assert device_2 @@ -265,7 +266,7 @@ async def test_update_failed(hass: HomeAssistant, mock_device_registry_devices) mikrotik.hub.MikrotikData, "command", side_effect=mikrotik.errors.CannotConnect ): async_fire_time_changed(hass, utcnow() + timedelta(seconds=10)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) device_1 = hass.states.get("device_tracker.device_1") assert device_1 diff --git a/tests/components/mikrotik/test_init.py b/tests/components/mikrotik/test_init.py index dc0f0505a4d..96ec0f5771e 100644 --- a/tests/components/mikrotik/test_init.py +++ b/tests/components/mikrotik/test_init.py @@ -1,4 +1,5 @@ """Test Mikrotik setup process.""" + from unittest.mock import MagicMock, patch from librouteros.exceptions import ConnectionClosed, LibRouterosError @@ -17,9 +18,10 @@ from tests.common import MockConfigEntry @pytest.fixture(autouse=True) def mock_api(): """Mock api.""" - with patch("librouteros.create_transport"), patch( - "librouteros.Api.readResponse" - ) as mock_api: + with ( + patch("librouteros.create_transport"), + patch("librouteros.Api.readResponse") as mock_api, + ): yield mock_api diff --git a/tests/components/mill/test_config_flow.py b/tests/components/mill/test_config_flow.py index 59128cc1749..d7740502412 100644 --- a/tests/components/mill/test_config_flow.py +++ b/tests/components/mill/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for Mill config flow.""" + from unittest.mock import patch from homeassistant import config_entries diff --git a/tests/components/mill/test_init.py b/tests/components/mill/test_init.py index 15175dedada..bf0026b8c6c 100644 --- a/tests/components/mill/test_init.py +++ b/tests/components/mill/test_init.py @@ -1,4 +1,6 @@ """Tests for Mill init.""" + +import asyncio from unittest.mock import patch from homeassistant.components import mill @@ -20,9 +22,10 @@ async def test_setup_with_cloud_config(hass: HomeAssistant) -> None: }, ) entry.add_to_hass(hass) - with patch( - "mill.Mill.fetch_heater_and_sensor_data", return_value={} - ) as mock_fetch, patch("mill.Mill.connect", return_value=True) as mock_connect: + with ( + patch("mill.Mill.fetch_heater_and_sensor_data", return_value={}) as mock_fetch, + patch("mill.Mill.connect", return_value=True) as mock_connect, + ): assert await async_setup_component(hass, "mill", {}) assert len(mock_fetch.mock_calls) == 1 assert len(mock_connect.mock_calls) == 1 @@ -44,6 +47,22 @@ async def test_setup_with_cloud_config_fails(hass: HomeAssistant) -> None: assert entry.state is ConfigEntryState.SETUP_RETRY +async def test_setup_with_cloud_config_times_out(hass: HomeAssistant) -> None: + """Test setup of cloud config will retry if timed out.""" + entry = MockConfigEntry( + domain=mill.DOMAIN, + data={ + mill.CONF_USERNAME: "user", + mill.CONF_PASSWORD: "pswd", + mill.CONNECTION_TYPE: mill.CLOUD, + }, + ) + entry.add_to_hass(hass) + with patch("mill.Mill.connect", side_effect=asyncio.TimeoutError): + await hass.config_entries.async_setup(entry.entry_id) + assert entry.state is ConfigEntryState.SETUP_RETRY + + async def test_setup_with_old_cloud_config(hass: HomeAssistant) -> None: """Test setup of old cloud config.""" entry = MockConfigEntry( @@ -54,9 +73,10 @@ async def test_setup_with_old_cloud_config(hass: HomeAssistant) -> None: }, ) entry.add_to_hass(hass) - with patch("mill.Mill.fetch_heater_and_sensor_data", return_value={}), patch( - "mill.Mill.connect", return_value=True - ) as mock_connect: + with ( + patch("mill.Mill.fetch_heater_and_sensor_data", return_value={}), + patch("mill.Mill.connect", return_value=True) as mock_connect, + ): assert await async_setup_component(hass, "mill", {}) assert len(mock_connect.mock_calls) == 1 @@ -72,24 +92,27 @@ async def test_setup_with_local_config(hass: HomeAssistant) -> None: }, ) entry.add_to_hass(hass) - with patch( - "mill_local.Mill.fetch_heater_and_sensor_data", - return_value={ - "ambient_temperature": 20, - "set_temperature": 22, - "current_power": 0, - "control_signal": 0, - "raw_ambient_temperature": 19, - }, - ) as mock_fetch, patch( - "mill_local.Mill.connect", - return_value={ - "name": "panel heater gen. 3", - "version": "0x210927", - "operation_key": "", - "status": "ok", - }, - ) as mock_connect: + with ( + patch( + "mill_local.Mill.fetch_heater_and_sensor_data", + return_value={ + "ambient_temperature": 20, + "set_temperature": 22, + "current_power": 0, + "control_signal": 0, + "raw_ambient_temperature": 19, + }, + ) as mock_fetch, + patch( + "mill_local.Mill.connect", + return_value={ + "name": "panel heater gen. 3", + "version": "0x210927", + "operation_key": "", + "status": "ok", + }, + ) as mock_connect, + ): assert await async_setup_component(hass, "mill", {}) assert len(mock_fetch.mock_calls) == 1 @@ -108,15 +131,17 @@ async def test_unload_entry(hass: HomeAssistant) -> None: ) entry.add_to_hass(hass) - with patch.object( - hass.config_entries, - "async_forward_entry_unload", - return_value=True, - ) as unload_entry, patch( - "mill.Mill.fetch_heater_and_sensor_data", return_value={} - ), patch( - "mill.Mill.connect", - return_value=True, + with ( + patch.object( + hass.config_entries, + "async_forward_entry_unload", + return_value=True, + ) as unload_entry, + patch("mill.Mill.fetch_heater_and_sensor_data", return_value={}), + patch( + "mill.Mill.connect", + return_value=True, + ), ): assert await async_setup_component(hass, "mill", {}) diff --git a/tests/components/min_max/test_config_flow.py b/tests/components/min_max/test_config_flow.py index f180f07b657..367c4cd717d 100644 --- a/tests/components/min_max/test_config_flow.py +++ b/tests/components/min_max/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Min/Max config flow.""" + from unittest.mock import patch import pytest @@ -11,7 +12,7 @@ from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry -@pytest.mark.parametrize("platform", ("sensor",)) +@pytest.mark.parametrize("platform", ["sensor"]) async def test_config_flow(hass: HomeAssistant, platform: str) -> None: """Test the config flow.""" input_sensors = ["sensor.input_one", "sensor.input_two"] @@ -65,7 +66,7 @@ def get_suggested(schema, key): raise Exception -@pytest.mark.parametrize("platform", ("sensor",)) +@pytest.mark.parametrize("platform", ["sensor"]) async def test_options(hass: HomeAssistant, platform: str) -> None: """Test reconfiguring.""" hass.states.async_set("sensor.input_one", "10") diff --git a/tests/components/min_max/test_init.py b/tests/components/min_max/test_init.py index cd07f7060f6..0e180c3135c 100644 --- a/tests/components/min_max/test_init.py +++ b/tests/components/min_max/test_init.py @@ -1,4 +1,5 @@ """Test the Min/Max integration.""" + import pytest from homeassistant.components.min_max.const import DOMAIN @@ -8,7 +9,7 @@ from homeassistant.helpers import entity_registry as er from tests.common import MockConfigEntry -@pytest.mark.parametrize("platform", ("sensor",)) +@pytest.mark.parametrize("platform", ["sensor"]) async def test_setup_and_remove_config_entry( hass: HomeAssistant, entity_registry: er.EntityRegistry, diff --git a/tests/components/min_max/test_sensor.py b/tests/components/min_max/test_sensor.py index acd42f9355e..4d86ee72cc6 100644 --- a/tests/components/min_max/test_sensor.py +++ b/tests/components/min_max/test_sensor.py @@ -1,4 +1,5 @@ """The test for the min/max sensor platform.""" + import statistics from unittest.mock import patch diff --git a/tests/components/minecraft_server/conftest.py b/tests/components/minecraft_server/conftest.py index b118b15d08a..ef8a9d960f6 100644 --- a/tests/components/minecraft_server/conftest.py +++ b/tests/components/minecraft_server/conftest.py @@ -1,4 +1,5 @@ """Fixtures for Minecraft Server integration tests.""" + import pytest from homeassistant.components.minecraft_server.api import MinecraftServerType diff --git a/tests/components/minecraft_server/const.py b/tests/components/minecraft_server/const.py index 92d6c647d8f..6914d36ba5b 100644 --- a/tests/components/minecraft_server/const.py +++ b/tests/components/minecraft_server/const.py @@ -1,4 +1,5 @@ """Constants for Minecraft Server integration tests.""" + from mcstatus.motd import Motd from mcstatus.status_response import ( BedrockStatusPlayers, diff --git a/tests/components/minecraft_server/snapshots/test_binary_sensor.ambr b/tests/components/minecraft_server/snapshots/test_binary_sensor.ambr index 2a62fea7f35..2e4bf49089c 100644 --- a/tests/components/minecraft_server/snapshots/test_binary_sensor.ambr +++ b/tests/components/minecraft_server/snapshots/test_binary_sensor.ambr @@ -4,11 +4,11 @@ 'attributes': ReadOnlyDict({ 'device_class': 'connectivity', 'friendly_name': 'Minecraft Server Status', - 'icon': 'mdi:lan', }), 'context': , 'entity_id': 'binary_sensor.minecraft_server_status', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }) @@ -18,11 +18,11 @@ 'attributes': ReadOnlyDict({ 'device_class': 'connectivity', 'friendly_name': 'Minecraft Server Status', - 'icon': 'mdi:lan', }), 'context': , 'entity_id': 'binary_sensor.minecraft_server_status', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }) @@ -32,11 +32,11 @@ 'attributes': ReadOnlyDict({ 'device_class': 'connectivity', 'friendly_name': 'Minecraft Server Status', - 'icon': 'mdi:lan', }), 'context': , 'entity_id': 'binary_sensor.minecraft_server_status', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }) @@ -46,12 +46,12 @@ 'attributes': ReadOnlyDict({ 'device_class': 'connectivity', 'friendly_name': 'Minecraft Server Status', - 'icon': 'mdi:lan', }), 'context': , 'entity_id': 'binary_sensor.minecraft_server_status', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }) -# --- \ No newline at end of file +# --- diff --git a/tests/components/minecraft_server/snapshots/test_sensor.ambr b/tests/components/minecraft_server/snapshots/test_sensor.ambr index b0f77f27b80..47d638adf79 100644 --- a/tests/components/minecraft_server/snapshots/test_sensor.ambr +++ b/tests/components/minecraft_server/snapshots/test_sensor.ambr @@ -3,12 +3,12 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Minecraft Server Latency', - 'icon': 'mdi:signal', 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.minecraft_server_latency', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '5', }) @@ -17,12 +17,12 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Minecraft Server Players online', - 'icon': 'mdi:account-multiple', 'unit_of_measurement': 'players', }), 'context': , 'entity_id': 'sensor.minecraft_server_players_online', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '3', }) @@ -31,12 +31,12 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Minecraft Server Players max', - 'icon': 'mdi:account-multiple', 'unit_of_measurement': 'players', }), 'context': , 'entity_id': 'sensor.minecraft_server_players_max', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '10', }) @@ -45,11 +45,11 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Minecraft Server World message', - 'icon': 'mdi:minecraft', }), 'context': , 'entity_id': 'sensor.minecraft_server_world_message', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'Dummy MOTD', }) @@ -58,11 +58,11 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Minecraft Server Version', - 'icon': 'mdi:numeric', }), 'context': , 'entity_id': 'sensor.minecraft_server_version', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'Dummy Version', }) @@ -71,11 +71,11 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Minecraft Server Protocol version', - 'icon': 'mdi:numeric', }), 'context': , 'entity_id': 'sensor.minecraft_server_protocol_version', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '123', }) @@ -84,11 +84,11 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Minecraft Server Map name', - 'icon': 'mdi:map', }), 'context': , 'entity_id': 'sensor.minecraft_server_map_name', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'Dummy Map Name', }) @@ -97,11 +97,11 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Minecraft Server Game mode', - 'icon': 'mdi:cog', }), 'context': , 'entity_id': 'sensor.minecraft_server_game_mode', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'Dummy Game Mode', }) @@ -110,11 +110,11 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Minecraft Server Edition', - 'icon': 'mdi:minecraft', }), 'context': , 'entity_id': 'sensor.minecraft_server_edition', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'MCPE', }) @@ -123,12 +123,12 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Minecraft Server Latency', - 'icon': 'mdi:signal', 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.minecraft_server_latency', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '5', }) @@ -137,7 +137,6 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Minecraft Server Players online', - 'icon': 'mdi:account-multiple', 'players_list': list([ 'Player 1', 'Player 2', @@ -148,6 +147,7 @@ 'context': , 'entity_id': 'sensor.minecraft_server_players_online', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '3', }) @@ -156,12 +156,12 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Minecraft Server Players max', - 'icon': 'mdi:account-multiple', 'unit_of_measurement': 'players', }), 'context': , 'entity_id': 'sensor.minecraft_server_players_max', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '10', }) @@ -170,11 +170,11 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Minecraft Server World message', - 'icon': 'mdi:minecraft', }), 'context': , 'entity_id': 'sensor.minecraft_server_world_message', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'Dummy MOTD', }) @@ -183,11 +183,11 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Minecraft Server Version', - 'icon': 'mdi:numeric', }), 'context': , 'entity_id': 'sensor.minecraft_server_version', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'Dummy Version', }) @@ -196,11 +196,11 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Minecraft Server Protocol version', - 'icon': 'mdi:numeric', }), 'context': , 'entity_id': 'sensor.minecraft_server_protocol_version', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '123', }) @@ -209,12 +209,12 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Minecraft Server Latency', - 'icon': 'mdi:signal', 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.minecraft_server_latency', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '5', }) @@ -223,12 +223,12 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Minecraft Server Players online', - 'icon': 'mdi:account-multiple', 'unit_of_measurement': 'players', }), 'context': , 'entity_id': 'sensor.minecraft_server_players_online', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '3', }) @@ -237,12 +237,12 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Minecraft Server Players max', - 'icon': 'mdi:account-multiple', 'unit_of_measurement': 'players', }), 'context': , 'entity_id': 'sensor.minecraft_server_players_max', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '10', }) @@ -251,11 +251,11 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Minecraft Server World message', - 'icon': 'mdi:minecraft', }), 'context': , 'entity_id': 'sensor.minecraft_server_world_message', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'Dummy MOTD', }) @@ -264,11 +264,11 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Minecraft Server Version', - 'icon': 'mdi:numeric', }), 'context': , 'entity_id': 'sensor.minecraft_server_version', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'Dummy Version', }) @@ -277,11 +277,11 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Minecraft Server Protocol version', - 'icon': 'mdi:numeric', }), 'context': , 'entity_id': 'sensor.minecraft_server_protocol_version', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '123', }) @@ -290,11 +290,11 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Minecraft Server Map name', - 'icon': 'mdi:map', }), 'context': , 'entity_id': 'sensor.minecraft_server_map_name', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'Dummy Map Name', }) @@ -303,11 +303,11 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Minecraft Server Game mode', - 'icon': 'mdi:cog', }), 'context': , 'entity_id': 'sensor.minecraft_server_game_mode', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'Dummy Game Mode', }) @@ -316,11 +316,11 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Minecraft Server Edition', - 'icon': 'mdi:minecraft', }), 'context': , 'entity_id': 'sensor.minecraft_server_edition', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'MCPE', }) @@ -329,12 +329,12 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Minecraft Server Latency', - 'icon': 'mdi:signal', 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.minecraft_server_latency', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '5', }) @@ -343,7 +343,6 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Minecraft Server Players online', - 'icon': 'mdi:account-multiple', 'players_list': list([ 'Player 1', 'Player 2', @@ -354,6 +353,7 @@ 'context': , 'entity_id': 'sensor.minecraft_server_players_online', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '3', }) @@ -362,12 +362,12 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Minecraft Server Players max', - 'icon': 'mdi:account-multiple', 'unit_of_measurement': 'players', }), 'context': , 'entity_id': 'sensor.minecraft_server_players_max', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '10', }) @@ -376,11 +376,11 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Minecraft Server World message', - 'icon': 'mdi:minecraft', }), 'context': , 'entity_id': 'sensor.minecraft_server_world_message', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'Dummy MOTD', }) @@ -389,11 +389,11 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Minecraft Server Version', - 'icon': 'mdi:numeric', }), 'context': , 'entity_id': 'sensor.minecraft_server_version', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'Dummy Version', }) @@ -402,11 +402,11 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Minecraft Server Protocol version', - 'icon': 'mdi:numeric', }), 'context': , 'entity_id': 'sensor.minecraft_server_protocol_version', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '123', }) diff --git a/tests/components/minecraft_server/test_binary_sensor.py b/tests/components/minecraft_server/test_binary_sensor.py index 4db564bc143..6321c91d74a 100644 --- a/tests/components/minecraft_server/test_binary_sensor.py +++ b/tests/components/minecraft_server/test_binary_sensor.py @@ -1,4 +1,5 @@ """Tests for Minecraft Server binary sensor.""" + from datetime import timedelta from unittest.mock import patch @@ -51,12 +52,15 @@ async def test_binary_sensor( mock_config_entry = request.getfixturevalue(mock_config_entry) mock_config_entry.add_to_hass(hass) - with patch( - f"homeassistant.components.minecraft_server.api.{server.__name__}.{lookup_function_name}", - return_value=server(host=TEST_HOST, port=TEST_PORT), - ), patch( - f"homeassistant.components.minecraft_server.api.{server.__name__}.async_status", - return_value=status_response, + with ( + patch( + f"homeassistant.components.minecraft_server.api.{server.__name__}.{lookup_function_name}", + return_value=server(host=TEST_HOST, port=TEST_PORT), + ), + patch( + f"homeassistant.components.minecraft_server.api.{server.__name__}.async_status", + return_value=status_response, + ), ): assert await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() @@ -94,12 +98,15 @@ async def test_binary_sensor_update( mock_config_entry = request.getfixturevalue(mock_config_entry) mock_config_entry.add_to_hass(hass) - with patch( - f"homeassistant.components.minecraft_server.api.{server.__name__}.{lookup_function_name}", - return_value=server(host=TEST_HOST, port=TEST_PORT), - ), patch( - f"homeassistant.components.minecraft_server.api.{server.__name__}.async_status", - return_value=status_response, + with ( + patch( + f"homeassistant.components.minecraft_server.api.{server.__name__}.{lookup_function_name}", + return_value=server(host=TEST_HOST, port=TEST_PORT), + ), + patch( + f"homeassistant.components.minecraft_server.api.{server.__name__}.async_status", + return_value=status_response, + ), ): assert await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() @@ -139,12 +146,15 @@ async def test_binary_sensor_update_failure( mock_config_entry = request.getfixturevalue(mock_config_entry) mock_config_entry.add_to_hass(hass) - with patch( - f"homeassistant.components.minecraft_server.api.{server.__name__}.{lookup_function_name}", - return_value=server(host=TEST_HOST, port=TEST_PORT), - ), patch( - f"homeassistant.components.minecraft_server.api.{server.__name__}.async_status", - return_value=status_response, + with ( + patch( + f"homeassistant.components.minecraft_server.api.{server.__name__}.{lookup_function_name}", + return_value=server(host=TEST_HOST, port=TEST_PORT), + ), + patch( + f"homeassistant.components.minecraft_server.api.{server.__name__}.async_status", + return_value=status_response, + ), ): assert await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/minecraft_server/test_config_flow.py b/tests/components/minecraft_server/test_config_flow.py index 2a0208f2251..188b68ce5af 100644 --- a/tests/components/minecraft_server/test_config_flow.py +++ b/tests/components/minecraft_server/test_config_flow.py @@ -37,12 +37,15 @@ async def test_show_config_form(hass: HomeAssistant) -> None: async def test_address_validation_failure(hass: HomeAssistant) -> None: """Test error in case of a failed connection.""" - with patch( - "homeassistant.components.minecraft_server.api.BedrockServer.lookup", - side_effect=ValueError, - ), patch( - "homeassistant.components.minecraft_server.api.JavaServer.async_lookup", - side_effect=ValueError, + with ( + patch( + "homeassistant.components.minecraft_server.api.BedrockServer.lookup", + side_effect=ValueError, + ), + patch( + "homeassistant.components.minecraft_server.api.JavaServer.async_lookup", + side_effect=ValueError, + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT @@ -54,15 +57,19 @@ async def test_address_validation_failure(hass: HomeAssistant) -> None: async def test_java_connection_failure(hass: HomeAssistant) -> None: """Test error in case of a failed connection to a Java Edition server.""" - with patch( - "homeassistant.components.minecraft_server.api.BedrockServer.lookup", - side_effect=ValueError, - ), patch( - "homeassistant.components.minecraft_server.api.JavaServer.async_lookup", - return_value=JavaServer(host=TEST_HOST, port=TEST_PORT), - ), patch( - "homeassistant.components.minecraft_server.api.JavaServer.async_status", - side_effect=OSError, + with ( + patch( + "homeassistant.components.minecraft_server.api.BedrockServer.lookup", + side_effect=ValueError, + ), + patch( + "homeassistant.components.minecraft_server.api.JavaServer.async_lookup", + return_value=JavaServer(host=TEST_HOST, port=TEST_PORT), + ), + patch( + "homeassistant.components.minecraft_server.api.JavaServer.async_status", + side_effect=OSError, + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT @@ -74,12 +81,15 @@ async def test_java_connection_failure(hass: HomeAssistant) -> None: async def test_bedrock_connection_failure(hass: HomeAssistant) -> None: """Test error in case of a failed connection to a Bedrock Edition server.""" - with patch( - "homeassistant.components.minecraft_server.api.BedrockServer.lookup", - return_value=BedrockServer(host=TEST_HOST, port=TEST_PORT), - ), patch( - "homeassistant.components.minecraft_server.api.BedrockServer.async_status", - side_effect=OSError, + with ( + patch( + "homeassistant.components.minecraft_server.api.BedrockServer.lookup", + return_value=BedrockServer(host=TEST_HOST, port=TEST_PORT), + ), + patch( + "homeassistant.components.minecraft_server.api.BedrockServer.async_status", + side_effect=OSError, + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT @@ -91,15 +101,19 @@ async def test_bedrock_connection_failure(hass: HomeAssistant) -> None: async def test_java_connection(hass: HomeAssistant) -> None: """Test config entry in case of a successful connection to a Java Edition server.""" - with patch( - "homeassistant.components.minecraft_server.api.BedrockServer.lookup", - side_effect=ValueError, - ), patch( - "homeassistant.components.minecraft_server.api.JavaServer.async_lookup", - return_value=JavaServer(host=TEST_HOST, port=TEST_PORT), - ), patch( - "homeassistant.components.minecraft_server.api.JavaServer.async_status", - return_value=TEST_JAVA_STATUS_RESPONSE, + with ( + patch( + "homeassistant.components.minecraft_server.api.BedrockServer.lookup", + side_effect=ValueError, + ), + patch( + "homeassistant.components.minecraft_server.api.JavaServer.async_lookup", + return_value=JavaServer(host=TEST_HOST, port=TEST_PORT), + ), + patch( + "homeassistant.components.minecraft_server.api.JavaServer.async_status", + return_value=TEST_JAVA_STATUS_RESPONSE, + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT @@ -114,12 +128,15 @@ async def test_java_connection(hass: HomeAssistant) -> None: async def test_bedrock_connection(hass: HomeAssistant) -> None: """Test config entry in case of a successful connection to a Bedrock Edition server.""" - with patch( - "homeassistant.components.minecraft_server.api.BedrockServer.lookup", - return_value=BedrockServer(host=TEST_HOST, port=TEST_PORT), - ), patch( - "homeassistant.components.minecraft_server.api.BedrockServer.async_status", - return_value=TEST_BEDROCK_STATUS_RESPONSE, + with ( + patch( + "homeassistant.components.minecraft_server.api.BedrockServer.lookup", + return_value=BedrockServer(host=TEST_HOST, port=TEST_PORT), + ), + patch( + "homeassistant.components.minecraft_server.api.BedrockServer.async_status", + return_value=TEST_BEDROCK_STATUS_RESPONSE, + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT @@ -134,12 +151,15 @@ async def test_bedrock_connection(hass: HomeAssistant) -> None: async def test_recovery(hass: HomeAssistant) -> None: """Test config flow recovery (successful connection after a failed connection).""" - with patch( - "homeassistant.components.minecraft_server.api.BedrockServer.lookup", - side_effect=ValueError, - ), patch( - "homeassistant.components.minecraft_server.api.JavaServer.async_lookup", - side_effect=ValueError, + with ( + patch( + "homeassistant.components.minecraft_server.api.BedrockServer.lookup", + side_effect=ValueError, + ), + patch( + "homeassistant.components.minecraft_server.api.JavaServer.async_lookup", + side_effect=ValueError, + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT @@ -147,12 +167,15 @@ async def test_recovery(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} - with patch( - "homeassistant.components.minecraft_server.api.BedrockServer.lookup", - return_value=BedrockServer(host=TEST_HOST, port=TEST_PORT), - ), patch( - "homeassistant.components.minecraft_server.api.BedrockServer.async_status", - return_value=TEST_BEDROCK_STATUS_RESPONSE, + with ( + patch( + "homeassistant.components.minecraft_server.api.BedrockServer.lookup", + return_value=BedrockServer(host=TEST_HOST, port=TEST_PORT), + ), + patch( + "homeassistant.components.minecraft_server.api.BedrockServer.async_status", + return_value=TEST_BEDROCK_STATUS_RESPONSE, + ), ): result2 = await hass.config_entries.flow.async_configure( flow_id=result["flow_id"], user_input=USER_INPUT diff --git a/tests/components/minecraft_server/test_diagnostics.py b/tests/components/minecraft_server/test_diagnostics.py index 80b5c91c1fb..e72d0c5f8db 100644 --- a/tests/components/minecraft_server/test_diagnostics.py +++ b/tests/components/minecraft_server/test_diagnostics.py @@ -1,4 +1,5 @@ """Tests for Minecraft Server diagnostics.""" + from unittest.mock import patch from mcstatus import BedrockServer, JavaServer @@ -48,12 +49,15 @@ async def test_config_entry_diagnostics( lookup_function_name = "lookup" # Setup mock entry. - with patch( - f"mcstatus.server.{server.__name__}.{lookup_function_name}", - return_value=server(host=TEST_HOST, port=TEST_PORT), - ), patch( - f"mcstatus.server.{server.__name__}.async_status", - return_value=status_response, + with ( + patch( + f"mcstatus.server.{server.__name__}.{lookup_function_name}", + return_value=server(host=TEST_HOST, port=TEST_PORT), + ), + patch( + f"mcstatus.server.{server.__name__}.async_status", + return_value=status_response, + ), ): assert await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/minecraft_server/test_init.py b/tests/components/minecraft_server/test_init.py index 3d554bf1a55..9c02fb56d91 100644 --- a/tests/components/minecraft_server/test_init.py +++ b/tests/components/minecraft_server/test_init.py @@ -1,4 +1,5 @@ """Tests for the Minecraft Server integration.""" + from unittest.mock import patch from mcstatus import JavaServer @@ -121,12 +122,15 @@ async def test_setup_and_unload_entry( """Test successful entry setup and unload.""" java_mock_config_entry.add_to_hass(hass) - with patch( - "homeassistant.components.minecraft_server.api.JavaServer.async_lookup", - return_value=JavaServer(host=TEST_HOST, port=TEST_PORT), - ), patch( - "homeassistant.components.minecraft_server.api.JavaServer.async_status", - return_value=TEST_JAVA_STATUS_RESPONSE, + with ( + patch( + "homeassistant.components.minecraft_server.api.JavaServer.async_lookup", + return_value=JavaServer(host=TEST_HOST, port=TEST_PORT), + ), + patch( + "homeassistant.components.minecraft_server.api.JavaServer.async_status", + return_value=TEST_JAVA_STATUS_RESPONSE, + ), ): assert await hass.config_entries.async_setup(java_mock_config_entry.entry_id) await hass.async_block_till_done() @@ -180,12 +184,15 @@ async def test_setup_entry_not_ready( """Test entry setup not ready.""" java_mock_config_entry.add_to_hass(hass) - with patch( - "homeassistant.components.minecraft_server.api.JavaServer.async_lookup", - return_value=JavaServer(host=TEST_HOST, port=TEST_PORT), - ), patch( - "homeassistant.components.minecraft_server.api.JavaServer.async_status", - return_value=OSError, + with ( + patch( + "homeassistant.components.minecraft_server.api.JavaServer.async_lookup", + return_value=JavaServer(host=TEST_HOST, port=TEST_PORT), + ), + patch( + "homeassistant.components.minecraft_server.api.JavaServer.async_status", + return_value=OSError, + ), ): assert not await hass.config_entries.async_setup( java_mock_config_entry.entry_id @@ -213,16 +220,19 @@ async def test_entry_migration( ) # Trigger migration. - with patch( - "homeassistant.components.minecraft_server.api.JavaServer.async_lookup", - side_effect=[ - ValueError, # async_migrate_entry - JavaServer(host=TEST_HOST, port=TEST_PORT), # async_migrate_entry - JavaServer(host=TEST_HOST, port=TEST_PORT), # async_setup_entry - ], - ), patch( - "homeassistant.components.minecraft_server.api.JavaServer.async_status", - return_value=TEST_JAVA_STATUS_RESPONSE, + with ( + patch( + "homeassistant.components.minecraft_server.api.JavaServer.async_lookup", + side_effect=[ + ValueError, # async_migrate_entry + JavaServer(host=TEST_HOST, port=TEST_PORT), # async_migrate_entry + JavaServer(host=TEST_HOST, port=TEST_PORT), # async_setup_entry + ], + ), + patch( + "homeassistant.components.minecraft_server.api.JavaServer.async_status", + return_value=TEST_JAVA_STATUS_RESPONSE, + ), ): assert await hass.config_entries.async_setup(v1_mock_config_entry.entry_id) await hass.async_block_till_done() @@ -275,12 +285,15 @@ async def test_entry_migration_host_only( ) # Trigger migration. - with patch( - "homeassistant.components.minecraft_server.api.JavaServer.async_lookup", - return_value=JavaServer(host=TEST_HOST, port=TEST_PORT), - ), patch( - "homeassistant.components.minecraft_server.api.JavaServer.async_status", - return_value=TEST_JAVA_STATUS_RESPONSE, + with ( + patch( + "homeassistant.components.minecraft_server.api.JavaServer.async_lookup", + return_value=JavaServer(host=TEST_HOST, port=TEST_PORT), + ), + patch( + "homeassistant.components.minecraft_server.api.JavaServer.async_status", + return_value=TEST_JAVA_STATUS_RESPONSE, + ), ): assert await hass.config_entries.async_setup(v1_mock_config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/minecraft_server/test_sensor.py b/tests/components/minecraft_server/test_sensor.py index 7d599669d71..ff62f8ddf36 100644 --- a/tests/components/minecraft_server/test_sensor.py +++ b/tests/components/minecraft_server/test_sensor.py @@ -1,4 +1,5 @@ """Tests for Minecraft Server sensors.""" + from datetime import timedelta from unittest.mock import patch @@ -93,12 +94,15 @@ async def test_sensor( mock_config_entry = request.getfixturevalue(mock_config_entry) mock_config_entry.add_to_hass(hass) - with patch( - f"homeassistant.components.minecraft_server.api.{server.__name__}.{lookup_function_name}", - return_value=server(host=TEST_HOST, port=TEST_PORT), - ), patch( - f"homeassistant.components.minecraft_server.api.{server.__name__}.async_status", - return_value=status_response, + with ( + patch( + f"homeassistant.components.minecraft_server.api.{server.__name__}.{lookup_function_name}", + return_value=server(host=TEST_HOST, port=TEST_PORT), + ), + patch( + f"homeassistant.components.minecraft_server.api.{server.__name__}.async_status", + return_value=status_response, + ), ): assert await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() @@ -144,12 +148,15 @@ async def test_sensor_disabled_by_default( mock_config_entry = request.getfixturevalue(mock_config_entry) mock_config_entry.add_to_hass(hass) - with patch( - f"homeassistant.components.minecraft_server.api.{server.__name__}.{lookup_function_name}", - return_value=server(host=TEST_HOST, port=TEST_PORT), - ), patch( - f"homeassistant.components.minecraft_server.api.{server.__name__}.async_status", - return_value=status_response, + with ( + patch( + f"homeassistant.components.minecraft_server.api.{server.__name__}.{lookup_function_name}", + return_value=server(host=TEST_HOST, port=TEST_PORT), + ), + patch( + f"homeassistant.components.minecraft_server.api.{server.__name__}.async_status", + return_value=status_response, + ), ): assert await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() @@ -198,12 +205,15 @@ async def test_sensor_update( mock_config_entry = request.getfixturevalue(mock_config_entry) mock_config_entry.add_to_hass(hass) - with patch( - f"homeassistant.components.minecraft_server.api.{server.__name__}.{lookup_function_name}", - return_value=server(host=TEST_HOST, port=TEST_PORT), - ), patch( - f"homeassistant.components.minecraft_server.api.{server.__name__}.async_status", - return_value=status_response, + with ( + patch( + f"homeassistant.components.minecraft_server.api.{server.__name__}.{lookup_function_name}", + return_value=server(host=TEST_HOST, port=TEST_PORT), + ), + patch( + f"homeassistant.components.minecraft_server.api.{server.__name__}.async_status", + return_value=status_response, + ), ): assert await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() @@ -254,12 +264,15 @@ async def test_sensor_update_failure( mock_config_entry = request.getfixturevalue(mock_config_entry) mock_config_entry.add_to_hass(hass) - with patch( - f"homeassistant.components.minecraft_server.api.{server.__name__}.{lookup_function_name}", - return_value=server(host=TEST_HOST, port=TEST_PORT), - ), patch( - f"homeassistant.components.minecraft_server.api.{server.__name__}.async_status", - return_value=status_response, + with ( + patch( + f"homeassistant.components.minecraft_server.api.{server.__name__}.{lookup_function_name}", + return_value=server(host=TEST_HOST, port=TEST_PORT), + ), + patch( + f"homeassistant.components.minecraft_server.api.{server.__name__}.async_status", + return_value=status_response, + ), ): assert await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/minio/common.py b/tests/components/minio/common.py index 4719fc79e49..107afe7fe8d 100644 --- a/tests/components/minio/common.py +++ b/tests/components/minio/common.py @@ -1,4 +1,5 @@ """Minio Test event.""" + TEST_EVENT = { "Records": [ { diff --git a/tests/components/minio/test_minio.py b/tests/components/minio/test_minio.py index 7455ede2994..74db0a2fcf9 100644 --- a/tests/components/minio/test_minio.py +++ b/tests/components/minio/test_minio.py @@ -1,4 +1,5 @@ """Tests for Minio Hass related code.""" + import asyncio import json from unittest.mock import MagicMock, call, patch @@ -75,8 +76,6 @@ async def test_minio_services( await hass.async_start() await hass.async_block_till_done() - assert "Setup of domain minio took" in caplog.text - # Call services await hass.services.async_call( DOMAIN, @@ -140,8 +139,6 @@ async def test_minio_listen( await hass.async_start() await hass.async_block_till_done() - assert "Setup of domain minio took" in caplog.text - while not events: await asyncio.sleep(0) diff --git a/tests/components/mjpeg/conftest.py b/tests/components/mjpeg/conftest.py index 0bd85920a8e..e10c267d718 100644 --- a/tests/components/mjpeg/conftest.py +++ b/tests/components/mjpeg/conftest.py @@ -1,4 +1,5 @@ """Fixtures for MJPEG IP Camera integration tests.""" + from __future__ import annotations from collections.abc import Generator diff --git a/tests/components/mjpeg/test_init.py b/tests/components/mjpeg/test_init.py index e96749a98dd..ca3ef03f5c8 100644 --- a/tests/components/mjpeg/test_init.py +++ b/tests/components/mjpeg/test_init.py @@ -1,4 +1,5 @@ """Tests for the MJPEG IP Camera integration.""" + from unittest.mock import AsyncMock, MagicMock from homeassistant.components.mjpeg.const import DOMAIN diff --git a/tests/components/moat/__init__.py b/tests/components/moat/__init__.py index e0af0229cba..09c4c39d1fa 100644 --- a/tests/components/moat/__init__.py +++ b/tests/components/moat/__init__.py @@ -1,6 +1,5 @@ """Tests for the Moat BLE integration.""" - from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo NOT_MOAT_SERVICE_INFO = BluetoothServiceInfo( diff --git a/tests/components/moat/test_config_flow.py b/tests/components/moat/test_config_flow.py index e40cd911dba..ab0825c884e 100644 --- a/tests/components/moat/test_config_flow.py +++ b/tests/components/moat/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Moat config flow.""" + from unittest.mock import patch from homeassistant import config_entries diff --git a/tests/components/moat/test_sensor.py b/tests/components/moat/test_sensor.py index 680f23853e8..6ec090bcac3 100644 --- a/tests/components/moat/test_sensor.py +++ b/tests/components/moat/test_sensor.py @@ -1,4 +1,5 @@ """Test the Moat sensors.""" + 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 diff --git a/tests/components/mobile_app/conftest.py b/tests/components/mobile_app/conftest.py index f69912f176c..aa53c4c6136 100644 --- a/tests/components/mobile_app/conftest.py +++ b/tests/components/mobile_app/conftest.py @@ -1,4 +1,5 @@ """Tests for mobile_app component.""" + from http import HTTPStatus import pytest diff --git a/tests/components/mobile_app/const.py b/tests/components/mobile_app/const.py index 9316af4a6a2..da80eb07513 100644 --- a/tests/components/mobile_app/const.py +++ b/tests/components/mobile_app/const.py @@ -1,4 +1,5 @@ """Constants for mobile_app tests.""" + CALL_SERVICE = { "type": "call_service", "data": {"domain": "test", "service": "mobile_app", "service_data": {"foo": "bar"}}, diff --git a/tests/components/mobile_app/test_binary_sensor.py b/tests/components/mobile_app/test_binary_sensor.py index fe3510865fc..acebd8796b7 100644 --- a/tests/components/mobile_app/test_binary_sensor.py +++ b/tests/components/mobile_app/test_binary_sensor.py @@ -1,4 +1,5 @@ """Entity tests for mobile_app.""" + from http import HTTPStatus import pytest diff --git a/tests/components/mobile_app/test_device_action.py b/tests/components/mobile_app/test_device_action.py index fd064ab653b..7be9c8d304b 100644 --- a/tests/components/mobile_app/test_device_action.py +++ b/tests/components/mobile_app/test_device_action.py @@ -1,4 +1,5 @@ """The tests for Mobile App device actions.""" + from homeassistant.components import automation, device_automation from homeassistant.components.mobile_app import DATA_DEVICES, DOMAIN, util from homeassistant.core import HomeAssistant diff --git a/tests/components/mobile_app/test_device_tracker.py b/tests/components/mobile_app/test_device_tracker.py index 5e775fdf265..21d4d80c791 100644 --- a/tests/components/mobile_app/test_device_tracker.py +++ b/tests/components/mobile_app/test_device_tracker.py @@ -1,4 +1,5 @@ """Test mobile app device tracker.""" + from http import HTTPStatus from homeassistant.core import HomeAssistant diff --git a/tests/components/mobile_app/test_http_api.py b/tests/components/mobile_app/test_http_api.py index 28a8a26657a..d080b7a5106 100644 --- a/tests/components/mobile_app/test_http_api.py +++ b/tests/components/mobile_app/test_http_api.py @@ -1,4 +1,5 @@ """Tests for the mobile_app HTTP API.""" + from binascii import unhexlify from http import HTTPStatus import json diff --git a/tests/components/mobile_app/test_init.py b/tests/components/mobile_app/test_init.py index 5d25e9568cf..15380a0d8d7 100644 --- a/tests/components/mobile_app/test_init.py +++ b/tests/components/mobile_app/test_init.py @@ -1,4 +1,5 @@ """Tests for the mobile app integration.""" + from collections.abc import Awaitable, Callable from typing import Any from unittest.mock import Mock, patch @@ -83,14 +84,17 @@ async def _test_create_cloud_hook( ) config_entry.add_to_hass(hass) - with patch( - "homeassistant.components.cloud.async_active_subscription", - return_value=async_active_subscription_return_value, - ), patch( - "homeassistant.components.cloud.async_is_connected", return_value=True - ), patch( - "homeassistant.components.cloud.async_get_or_create_cloudhook", autospec=True - ) as mock_async_get_or_create_cloudhook: + with ( + patch( + "homeassistant.components.cloud.async_active_subscription", + return_value=async_active_subscription_return_value, + ), + patch("homeassistant.components.cloud.async_is_connected", return_value=True), + patch( + "homeassistant.components.cloud.async_get_or_create_cloudhook", + autospec=True, + ) as mock_async_get_or_create_cloudhook, + ): cloud_hook = "https://hook-url" mock_async_get_or_create_cloudhook.return_value = cloud_hook @@ -119,7 +123,7 @@ async def test_create_cloud_hook_on_setup( await _test_create_cloud_hook(hass, hass_admin_user, {}, True, additional_steps) -@pytest.mark.parametrize("exception", (CloudNotAvailable, ValueError)) +@pytest.mark.parametrize("exception", [CloudNotAvailable, ValueError]) async def test_remove_cloudhook( hass: HomeAssistant, hass_admin_user: MockUser, diff --git a/tests/components/mobile_app/test_logbook.py b/tests/components/mobile_app/test_logbook.py index 6c9d77088a9..8d9d0c068f2 100644 --- a/tests/components/mobile_app/test_logbook.py +++ b/tests/components/mobile_app/test_logbook.py @@ -1,4 +1,5 @@ """The tests for mobile_app logbook.""" + from homeassistant.components.mobile_app.logbook import ( DOMAIN, IOS_EVENT_ZONE_ENTERED, diff --git a/tests/components/mobile_app/test_notify.py b/tests/components/mobile_app/test_notify.py index 23e2530c70a..dacaba32e16 100644 --- a/tests/components/mobile_app/test_notify.py +++ b/tests/components/mobile_app/test_notify.py @@ -1,4 +1,5 @@ """Notify platform tests for mobile_app.""" + from datetime import datetime, timedelta from unittest.mock import patch diff --git a/tests/components/mobile_app/test_sensor.py b/tests/components/mobile_app/test_sensor.py index c1414533fd7..a7fb0ffc183 100644 --- a/tests/components/mobile_app/test_sensor.py +++ b/tests/components/mobile_app/test_sensor.py @@ -1,4 +1,5 @@ """Entity tests for mobile_app.""" + from http import HTTPStatus from unittest.mock import patch @@ -18,10 +19,10 @@ from homeassistant.util.unit_system import METRIC_SYSTEM, US_CUSTOMARY_SYSTEM @pytest.mark.parametrize( ("unit_system", "state_unit", "state1", "state2"), - ( + [ (METRIC_SYSTEM, UnitOfTemperature.CELSIUS, "100", "123"), (US_CUSTOMARY_SYSTEM, UnitOfTemperature.FAHRENHEIT, "212", "253"), - ), + ], ) async def test_sensor( hass: HomeAssistant, @@ -127,7 +128,7 @@ async def test_sensor( @pytest.mark.parametrize( ("unique_id", "unit_system", "state_unit", "state1", "state2"), - ( + [ ("battery_temperature", METRIC_SYSTEM, UnitOfTemperature.CELSIUS, "100", "123"), ( "battery_temperature", @@ -144,7 +145,7 @@ async def test_sensor( "212", "123", ), - ), + ], ) async def test_sensor_migration( hass: HomeAssistant, diff --git a/tests/components/mobile_app/test_webhook.py b/tests/components/mobile_app/test_webhook.py index f7581f03241..4a5f472221f 100644 --- a/tests/components/mobile_app/test_webhook.py +++ b/tests/components/mobile_app/test_webhook.py @@ -1,4 +1,5 @@ """Webhook tests for mobile_app.""" + from binascii import unhexlify from http import HTTPStatus from unittest.mock import patch @@ -347,13 +348,13 @@ async def test_webhook_returns_error_incorrect_json( @pytest.mark.parametrize( ("msg", "generate_response"), - ( + [ (RENDER_TEMPLATE, lambda hass: {"one": "Hello world"}), ( {"type": "get_zones", "data": {}}, lambda hass: [hass.states.get("zone.home").as_dict()], ), - ), + ], ) async def test_webhook_handle_decryption( hass: HomeAssistant, webhook_client, create_registrations, msg, generate_response diff --git a/tests/components/mochad/conftest.py b/tests/components/mochad/conftest.py index bd543eae943..2500070b2f1 100644 --- a/tests/components/mochad/conftest.py +++ b/tests/components/mochad/conftest.py @@ -1,2 +1,3 @@ """mochad conftest.""" + from tests.components.light.conftest import mock_light_profiles # noqa: F401 diff --git a/tests/components/mochad/test_light.py b/tests/components/mochad/test_light.py index 8bd4e1e2a59..b04f9a13933 100644 --- a/tests/components/mochad/test_light.py +++ b/tests/components/mochad/test_light.py @@ -1,4 +1,5 @@ """The tests for the mochad light platform.""" + import unittest.mock as mock import pytest diff --git a/tests/components/mochad/test_switch.py b/tests/components/mochad/test_switch.py index 9780ac3a481..96c3ba60b65 100644 --- a/tests/components/mochad/test_switch.py +++ b/tests/components/mochad/test_switch.py @@ -1,4 +1,5 @@ """The tests for the mochad switch platform.""" + import unittest.mock as mock import pytest @@ -12,8 +13,9 @@ from homeassistant.setup import async_setup_component @pytest.fixture(autouse=True) def pymochad_mock(): """Mock pymochad.""" - with mock.patch("homeassistant.components.mochad.switch.device"), mock.patch( - "homeassistant.components.mochad.switch.MochadException" + with ( + mock.patch("homeassistant.components.mochad.switch.device"), + mock.patch("homeassistant.components.mochad.switch.MochadException"), ): yield diff --git a/tests/components/modbus/conftest.py b/tests/components/modbus/conftest.py index d7e4556f746..f6eff0fd64b 100644 --- a/tests/components/modbus/conftest.py +++ b/tests/components/modbus/conftest.py @@ -1,4 +1,5 @@ """The tests for the Modbus sensor component.""" + import copy from dataclasses import dataclass from datetime import timedelta @@ -10,7 +11,14 @@ from pymodbus.exceptions import ModbusException import pytest from homeassistant.components.modbus.const import MODBUS_DOMAIN as DOMAIN, TCP -from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, CONF_TYPE +from homeassistant.const import ( + CONF_ADDRESS, + CONF_HOST, + CONF_NAME, + CONF_PORT, + CONF_SENSORS, + CONF_TYPE, +) from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -42,19 +50,24 @@ class ReadResult: @pytest.fixture(name="mock_pymodbus") def mock_pymodbus_fixture(): """Mock pymodbus.""" - mock_pb = mock.MagicMock() - with mock.patch( - "homeassistant.components.modbus.modbus.ModbusTcpClient", - return_value=mock_pb, - autospec=True, - ), mock.patch( - "homeassistant.components.modbus.modbus.ModbusSerialClient", - return_value=mock_pb, - autospec=True, - ), mock.patch( - "homeassistant.components.modbus.modbus.ModbusUdpClient", - return_value=mock_pb, - autospec=True, + mock_pb = mock.AsyncMock() + mock_pb.close = mock.MagicMock() + with ( + mock.patch( + "homeassistant.components.modbus.modbus.AsyncModbusTcpClient", + return_value=mock_pb, + autospec=True, + ), + mock.patch( + "homeassistant.components.modbus.modbus.AsyncModbusSerialClient", + return_value=mock_pb, + autospec=True, + ), + mock.patch( + "homeassistant.components.modbus.modbus.AsyncModbusUdpClient", + return_value=mock_pb, + autospec=True, + ), ): yield mock_pb @@ -100,13 +113,20 @@ async def mock_modbus_fixture( CONF_HOST: TEST_MODBUS_HOST, CONF_PORT: TEST_PORT_TCP, CONF_NAME: TEST_MODBUS_NAME, + CONF_SENSORS: [ + { + CONF_NAME: "dummy", + CONF_ADDRESS: 9999, + } + ], **conf, } ] } - mock_pb = mock.MagicMock() + mock_pb = mock.AsyncMock() + mock_pb.close = mock.MagicMock() with mock.patch( - "homeassistant.components.modbus.modbus.ModbusTcpClient", + "homeassistant.components.modbus.modbus.AsyncModbusTcpClient", return_value=mock_pb, autospec=True, ): diff --git a/tests/components/modbus/test_binary_sensor.py b/tests/components/modbus/test_binary_sensor.py index e47a6165b30..567618de3c6 100644 --- a/tests/components/modbus/test_binary_sensor.py +++ b/tests/components/modbus/test_binary_sensor.py @@ -1,4 +1,5 @@ """Thetests for the Modbus sensor component.""" + import pytest from homeassistant.components.binary_sensor import DOMAIN as SENSOR_DOMAIN diff --git a/tests/components/modbus/test_climate.py b/tests/components/modbus/test_climate.py index 47d468ee1d8..3752358c071 100644 --- a/tests/components/modbus/test_climate.py +++ b/tests/components/modbus/test_climate.py @@ -1,4 +1,5 @@ """The tests for the Modbus climate component.""" + import pytest from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN diff --git a/tests/components/modbus/test_cover.py b/tests/components/modbus/test_cover.py index 39897822bc8..fa9e617d96d 100644 --- a/tests/components/modbus/test_cover.py +++ b/tests/components/modbus/test_cover.py @@ -1,4 +1,5 @@ """The tests for the Modbus cover component.""" + from pymodbus.exceptions import ModbusException import pytest @@ -269,7 +270,7 @@ async def test_service_cover_move(hass: HomeAssistant, mock_modbus, mock_ha) -> ) assert hass.states.get(ENTITY_ID).state == STATE_CLOSED - mock_modbus.reset() + await mock_modbus.reset() mock_modbus.read_holding_registers.side_effect = ModbusException("fail write_") await hass.services.async_call( "cover", "close_cover", {"entity_id": ENTITY_ID}, blocking=True diff --git a/tests/components/modbus/test_fan.py b/tests/components/modbus/test_fan.py index 0922329d4b7..9719de3601b 100644 --- a/tests/components/modbus/test_fan.py +++ b/tests/components/modbus/test_fan.py @@ -1,4 +1,5 @@ """The tests for the Modbus fan component.""" + from pymodbus.exceptions import ModbusException import pytest diff --git a/tests/components/modbus/test_init.py b/tests/components/modbus/test_init.py index bd590a9e15c..0ca4703aa5f 100644 --- a/tests/components/modbus/test_init.py +++ b/tests/components/modbus/test_init.py @@ -12,6 +12,7 @@ This file is responsible for testing: It uses binary_sensors/sensors to do black box testing of the read calls. """ + from datetime import timedelta import logging from unittest import mock @@ -41,7 +42,6 @@ from homeassistant.components.modbus.const import ( CONF_BAUDRATE, CONF_BYTESIZE, CONF_CLIMATES, - CONF_CLOSE_COMM_ON_ERROR, CONF_DATA_TYPE, CONF_DEVICE_ADDRESS, CONF_FAN_MODE_HIGH, @@ -60,7 +60,6 @@ from homeassistant.components.modbus.const import ( CONF_MSG_WAIT, CONF_PARITY, CONF_RETRIES, - CONF_RETRY_ON_EMPTY, CONF_SLAVE_COUNT, CONF_STOPBITS, CONF_SWAP, @@ -330,11 +329,6 @@ async def test_ok_struct_validator(do_config) -> None: CONF_VIRTUAL_COUNT: 2, CONF_DATA_TYPE: DataType.INT32, }, - { - CONF_NAME: TEST_ENTITY_NAME, - CONF_DATA_TYPE: DataType.INT16, - CONF_SWAP: CONF_SWAP_WORD, - }, { CONF_NAME: TEST_ENTITY_NAME, CONF_DATA_TYPE: DataType.INT16, @@ -361,6 +355,12 @@ async def test_exception_struct_validator(do_config) -> None: CONF_HOST: TEST_MODBUS_HOST, CONF_PORT: TEST_PORT_TCP, CONF_TIMEOUT: 3, + CONF_SENSORS: [ + { + CONF_NAME: "dummy", + CONF_ADDRESS: 9999, + } + ], }, { CONF_NAME: TEST_MODBUS_NAME, @@ -368,6 +368,12 @@ async def test_exception_struct_validator(do_config) -> None: CONF_HOST: TEST_MODBUS_HOST + " 2", CONF_PORT: TEST_PORT_TCP, CONF_TIMEOUT: 3, + CONF_SENSORS: [ + { + CONF_NAME: "dummy", + CONF_ADDRESS: 9999, + } + ], }, { CONF_NAME: TEST_MODBUS_NAME + "2", @@ -375,6 +381,12 @@ async def test_exception_struct_validator(do_config) -> None: CONF_HOST: TEST_MODBUS_HOST, CONF_PORT: TEST_PORT_TCP, CONF_TIMEOUT: 3, + CONF_SENSORS: [ + { + CONF_NAME: "dummy", + CONF_ADDRESS: 9999, + } + ], }, ], [ @@ -383,6 +395,12 @@ async def test_exception_struct_validator(do_config) -> None: CONF_HOST: TEST_MODBUS_HOST, CONF_PORT: TEST_PORT_TCP, CONF_TIMEOUT: 3, + CONF_SENSORS: [ + { + CONF_NAME: "dummy", + CONF_ADDRESS: 9999, + } + ], }, { CONF_NAME: TEST_MODBUS_NAME + " 2", @@ -390,13 +408,19 @@ async def test_exception_struct_validator(do_config) -> None: CONF_HOST: TEST_MODBUS_HOST, CONF_PORT: TEST_PORT_TCP, CONF_TIMEOUT: 3, + CONF_SENSORS: [ + { + CONF_NAME: "dummy", + CONF_ADDRESS: 9999, + } + ], }, ], ], ) -async def test_check_config(do_config) -> None: +async def test_check_config(hass: HomeAssistant, do_config) -> None: """Test duplicate modbus validator.""" - check_config(do_config) + check_config(hass, do_config) assert len(do_config) == 1 @@ -447,9 +471,9 @@ async def test_check_config(do_config) -> None: ], ], ) -async def test_check_config_sensor(do_config) -> None: +async def test_check_config_sensor(hass: HomeAssistant, do_config) -> None: """Test duplicate entity validator.""" - check_config(do_config) + check_config(hass, do_config) assert len(do_config[0][CONF_SENSORS]) == 1 @@ -662,9 +686,9 @@ async def test_check_config_sensor(do_config) -> None: ], ], ) -async def test_check_config_climate(do_config) -> None: +async def test_check_config_climate(hass: HomeAssistant, do_config) -> None: """Test duplicate entity validator.""" - check_config(do_config) + check_config(hass, do_config) assert len(do_config[0][CONF_CLIMATES]) == 1 @@ -884,9 +908,9 @@ async def test_duplicate_fan_mode_validator(do_config) -> None: ), ], ) -async def test_duplicate_addresses(do_config, sensor_cnt) -> None: +async def test_duplicate_addresses(hass: HomeAssistant, do_config, sensor_cnt) -> None: """Test duplicate entity validator.""" - check_config(do_config) + check_config(hass, do_config) use_inx = len(do_config) - 1 assert len(do_config[use_inx][CONF_SENSORS]) == sensor_cnt @@ -919,9 +943,9 @@ async def test_duplicate_addresses(do_config, sensor_cnt) -> None: ], ], ) -async def test_no_duplicate_names(do_config) -> None: +async def test_no_duplicate_names(hass: HomeAssistant, do_config) -> None: """Test duplicate entity validator.""" - check_config(do_config) + check_config(hass, do_config) assert len(do_config[0][CONF_SENSORS]) == 1 assert len(do_config[0][CONF_BINARY_SENSORS]) == 1 @@ -933,24 +957,24 @@ async def test_no_duplicate_names(do_config) -> None: CONF_TYPE: TCP, CONF_HOST: TEST_MODBUS_HOST, CONF_PORT: TEST_PORT_TCP, - CONF_CLOSE_COMM_ON_ERROR: True, + CONF_SENSORS: [ + { + CONF_NAME: "dummy", + CONF_ADDRESS: 9999, + } + ], }, { CONF_TYPE: TCP, CONF_HOST: TEST_MODBUS_HOST, CONF_PORT: TEST_PORT_TCP, CONF_RETRIES: 3, - }, - { - CONF_TYPE: TCP, - CONF_HOST: TEST_MODBUS_HOST, - CONF_PORT: TEST_PORT_TCP, - CONF_RETRY_ON_EMPTY: True, - }, - { - CONF_TYPE: TCP, - CONF_HOST: TEST_MODBUS_HOST, - CONF_PORT: TEST_PORT_TCP, + CONF_SENSORS: [ + { + CONF_NAME: "dummy", + CONF_ADDRESS: 9999, + } + ], }, { CONF_TYPE: TCP, @@ -959,11 +983,23 @@ async def test_no_duplicate_names(do_config) -> None: CONF_NAME: TEST_MODBUS_NAME, CONF_TIMEOUT: 30, CONF_DELAY: 10, + CONF_SENSORS: [ + { + CONF_NAME: "dummy", + CONF_ADDRESS: 9999, + } + ], }, { CONF_TYPE: UDP, CONF_HOST: TEST_MODBUS_HOST, CONF_PORT: TEST_PORT_TCP, + CONF_SENSORS: [ + { + CONF_NAME: "dummy", + CONF_ADDRESS: 9999, + } + ], }, { CONF_TYPE: UDP, @@ -972,11 +1008,23 @@ async def test_no_duplicate_names(do_config) -> None: CONF_NAME: TEST_MODBUS_NAME, CONF_TIMEOUT: 30, CONF_DELAY: 10, + CONF_SENSORS: [ + { + CONF_NAME: "dummy", + CONF_ADDRESS: 9999, + } + ], }, { CONF_TYPE: RTUOVERTCP, CONF_HOST: TEST_MODBUS_HOST, CONF_PORT: TEST_PORT_TCP, + CONF_SENSORS: [ + { + CONF_NAME: "dummy", + CONF_ADDRESS: 9999, + } + ], }, { CONF_TYPE: RTUOVERTCP, @@ -985,6 +1033,12 @@ async def test_no_duplicate_names(do_config) -> None: CONF_NAME: TEST_MODBUS_NAME, CONF_TIMEOUT: 30, CONF_DELAY: 10, + CONF_SENSORS: [ + { + CONF_NAME: "dummy", + CONF_ADDRESS: 9999, + } + ], }, { CONF_TYPE: SERIAL, @@ -995,6 +1049,12 @@ async def test_no_duplicate_names(do_config) -> None: CONF_PARITY: "E", CONF_STOPBITS: 1, CONF_MSG_WAIT: 100, + CONF_SENSORS: [ + { + CONF_NAME: "dummy", + CONF_ADDRESS: 9999, + } + ], }, { CONF_TYPE: SERIAL, @@ -1007,12 +1067,24 @@ async def test_no_duplicate_names(do_config) -> None: CONF_NAME: TEST_MODBUS_NAME, CONF_TIMEOUT: 30, CONF_DELAY: 10, + CONF_SENSORS: [ + { + CONF_NAME: "dummy", + CONF_ADDRESS: 9999, + } + ], }, { CONF_TYPE: TCP, CONF_HOST: TEST_MODBUS_HOST, CONF_PORT: TEST_PORT_TCP, CONF_DELAY: 5, + CONF_SENSORS: [ + { + CONF_NAME: "dummy", + CONF_ADDRESS: 9999, + } + ], }, [ { @@ -1020,12 +1092,24 @@ async def test_no_duplicate_names(do_config) -> None: CONF_HOST: TEST_MODBUS_HOST, CONF_PORT: TEST_PORT_TCP, CONF_NAME: TEST_MODBUS_NAME, + CONF_SENSORS: [ + { + CONF_NAME: "dummy", + CONF_ADDRESS: 9999, + } + ], }, { CONF_TYPE: TCP, CONF_HOST: TEST_MODBUS_HOST, CONF_PORT: TEST_PORT_TCP, CONF_NAME: f"{TEST_MODBUS_NAME} 2", + CONF_SENSORS: [ + { + CONF_NAME: "dummy", + CONF_ADDRESS: 9999, + } + ], }, { CONF_TYPE: SERIAL, @@ -1036,6 +1120,12 @@ async def test_no_duplicate_names(do_config) -> None: CONF_PARITY: "E", CONF_STOPBITS: 1, CONF_NAME: f"{TEST_MODBUS_NAME} 3", + CONF_SENSORS: [ + { + CONF_NAME: "dummy", + CONF_ADDRESS: 9999, + } + ], }, ], { @@ -1092,6 +1182,12 @@ SERVICE = "service" CONF_PORT: TEST_PORT_SERIAL, CONF_PARITY: "E", CONF_STOPBITS: 1, + CONF_SENSORS: [ + { + CONF_NAME: "dummy", + CONF_ADDRESS: 9999, + } + ], }, ], ) @@ -1232,24 +1328,24 @@ async def mock_modbus_read_pymodbus_fixture( @pytest.mark.parametrize( ("do_domain", "do_group", "do_type", "do_scan_interval"), [ - [SENSOR_DOMAIN, CONF_SENSORS, CALL_TYPE_REGISTER_HOLDING, 10], - [SENSOR_DOMAIN, CONF_SENSORS, CALL_TYPE_REGISTER_INPUT, 10], - [BINARY_SENSOR_DOMAIN, CONF_BINARY_SENSORS, CALL_TYPE_DISCRETE, 10], - [BINARY_SENSOR_DOMAIN, CONF_BINARY_SENSORS, CALL_TYPE_COIL, 1], + (SENSOR_DOMAIN, CONF_SENSORS, CALL_TYPE_REGISTER_HOLDING, 10), + (SENSOR_DOMAIN, CONF_SENSORS, CALL_TYPE_REGISTER_INPUT, 10), + (BINARY_SENSOR_DOMAIN, CONF_BINARY_SENSORS, CALL_TYPE_DISCRETE, 10), + (BINARY_SENSOR_DOMAIN, CONF_BINARY_SENSORS, CALL_TYPE_COIL, 1), ], ) @pytest.mark.parametrize( ("do_return", "do_exception", "do_expect_state", "do_expect_value"), [ - [ReadResult([1]), None, STATE_ON, "1"], - [IllegalFunctionRequest(0x99), None, STATE_UNAVAILABLE, STATE_UNAVAILABLE], - [ExceptionResponse(0x99), None, STATE_UNAVAILABLE, STATE_UNAVAILABLE], - [ + (ReadResult([1]), None, STATE_ON, "1"), + (IllegalFunctionRequest(0x99), None, STATE_UNAVAILABLE, STATE_UNAVAILABLE), + (ExceptionResponse(0x99), None, STATE_UNAVAILABLE, STATE_UNAVAILABLE), + ( ReadResult([1]), ModbusException("fail read_"), STATE_UNAVAILABLE, STATE_UNAVAILABLE, - ], + ), ], ) async def test_pb_read( @@ -1286,11 +1382,17 @@ async def test_pymodbus_constructor_fail( CONF_TYPE: TCP, CONF_HOST: TEST_MODBUS_HOST, CONF_PORT: TEST_PORT_TCP, + CONF_SENSORS: [ + { + CONF_NAME: "dummy", + CONF_ADDRESS: 9999, + }, + ], } ] } with mock.patch( - "homeassistant.components.modbus.modbus.ModbusTcpClient", autospec=True + "homeassistant.components.modbus.modbus.AsyncModbusTcpClient", autospec=True ) as mock_pb: caplog.set_level(logging.ERROR) mock_pb.side_effect = ModbusException("test no class") @@ -1312,6 +1414,12 @@ async def test_pymodbus_close_fail( CONF_TYPE: TCP, CONF_HOST: TEST_MODBUS_HOST, CONF_PORT: TEST_PORT_TCP, + CONF_SENSORS: [ + { + CONF_NAME: "dummy", + CONF_ADDRESS: 9999, + } + ], } ] } @@ -1334,6 +1442,12 @@ async def test_pymodbus_connect_fail( CONF_TYPE: TCP, CONF_HOST: TEST_MODBUS_HOST, CONF_PORT: TEST_PORT_TCP, + CONF_SENSORS: [ + { + CONF_NAME: "dummy", + CONF_ADDRESS: 9999, + } + ], } ] } @@ -1490,7 +1604,7 @@ async def test_stop_restart( async def test_write_no_client(hass: HomeAssistant, mock_modbus) -> None: """Run test for service stop and write without client.""" - mock_modbus.reset() + await mock_modbus.reset() data = { ATTR_HUB: TEST_MODBUS_NAME, } @@ -1539,9 +1653,10 @@ async def test_integration_reload_failed( caplog.clear() yaml_path = get_fixture_path("configuration.yaml", "modbus") - with mock.patch.object( - hass_config, "YAML_CONFIG_FILE", yaml_path - ), mock.patch.object(mock_modbus, "connect", side_effect=ModbusException("error")): + with ( + mock.patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path), + mock.patch.object(mock_modbus, "connect", side_effect=ModbusException("error")), + ): await hass.services.async_call(DOMAIN, SERVICE_RELOAD, blocking=True) await hass.async_block_till_done() @@ -1564,3 +1679,18 @@ async def test_integration_setup_failed( ) await hass.services.async_call(DOMAIN, SERVICE_RELOAD, blocking=True) await hass.async_block_till_done() + + +async def test_no_entities(hass: HomeAssistant) -> None: + """Run test for failing pymodbus constructor.""" + config = { + DOMAIN: [ + { + CONF_NAME: TEST_MODBUS_NAME, + CONF_TYPE: TCP, + CONF_HOST: TEST_MODBUS_HOST, + CONF_PORT: TEST_PORT_TCP, + } + ] + } + assert await async_setup_component(hass, DOMAIN, config) is False diff --git a/tests/components/modbus/test_light.py b/tests/components/modbus/test_light.py index ecd9abd71b8..e5e1b56d77b 100644 --- a/tests/components/modbus/test_light.py +++ b/tests/components/modbus/test_light.py @@ -1,4 +1,5 @@ """The tests for the Modbus light component.""" + from pymodbus.exceptions import ModbusException import pytest diff --git a/tests/components/modbus/test_sensor.py b/tests/components/modbus/test_sensor.py index aa8b15585dc..524acc0dabb 100644 --- a/tests/components/modbus/test_sensor.py +++ b/tests/components/modbus/test_sensor.py @@ -1,4 +1,5 @@ """The tests for the Modbus sensor component.""" + import struct import pytest @@ -115,15 +116,6 @@ SLAVE_UNIQUE_ID = "ground_floor_sensor" } ] }, - { - CONF_SENSORS: [ - { - CONF_NAME: TEST_ENTITY_NAME, - CONF_ADDRESS: 51, - CONF_DATA_TYPE: DataType.INT16, - } - ] - }, { CONF_SENSORS: [ { @@ -909,7 +901,7 @@ async def test_virtual_sensor( hass: HomeAssistant, entity_registry: er.EntityRegistry, mock_do_cycle, expected ) -> None: """Run test for sensor.""" - for i in range(0, len(expected)): + for i in range(len(expected)): entity_id = f"{SENSOR_DOMAIN}.{TEST_ENTITY_NAME}".replace(" ", "_") unique_id = f"{SLAVE_UNIQUE_ID}" if i: @@ -1079,7 +1071,7 @@ async def test_virtual_swap_sensor( hass: HomeAssistant, mock_do_cycle, expected ) -> None: """Run test for sensor.""" - for i in range(0, len(expected)): + for i in range(len(expected)): entity_id = f"{SENSOR_DOMAIN}.{TEST_ENTITY_NAME}".replace(" ", "_") if i: entity_id = f"{entity_id}_{i}" diff --git a/tests/components/modbus/test_switch.py b/tests/components/modbus/test_switch.py index 28c44440581..4eb0a5b3a18 100644 --- a/tests/components/modbus/test_switch.py +++ b/tests/components/modbus/test_switch.py @@ -1,4 +1,5 @@ """The tests for the Modbus switch component.""" + from datetime import timedelta from unittest import mock diff --git a/tests/components/modem_callerid/test_config_flow.py b/tests/components/modem_callerid/test_config_flow.py index a844f436905..aeb8fb6d966 100644 --- a/tests/components/modem_callerid/test_config_flow.py +++ b/tests/components/modem_callerid/test_config_flow.py @@ -1,4 +1,5 @@ """Test Modem Caller ID config flow.""" + from unittest.mock import MagicMock, patch import phone_modem diff --git a/tests/components/modem_callerid/test_init.py b/tests/components/modem_callerid/test_init.py index 748bcfdd85b..ccf97f60e10 100644 --- a/tests/components/modem_callerid/test_init.py +++ b/tests/components/modem_callerid/test_init.py @@ -1,4 +1,5 @@ """Test Modem Caller ID integration.""" + from unittest.mock import patch from phone_modem import exceptions @@ -20,10 +21,14 @@ async def test_setup_entry(hass: HomeAssistant) -> None: data={CONF_DEVICE: com_port().device}, ) entry.add_to_hass(hass) - with patch("aioserial.AioSerial", autospec=True), patch( - "homeassistant.components.modem_callerid.PhoneModem._get_response", - return_value="OK", - ), patch("phone_modem.PhoneModem._modem_sm"): + with ( + patch("aioserial.AioSerial", autospec=True), + patch( + "homeassistant.components.modem_callerid.PhoneModem._get_response", + return_value="OK", + ), + patch("phone_modem.PhoneModem._modem_sm"), + ): await hass.config_entries.async_setup(entry.entry_id) assert entry.state == ConfigEntryState.LOADED diff --git a/tests/components/modern_forms/test_binary_sensor.py b/tests/components/modern_forms/test_binary_sensor.py index 3ea0fca99d5..a605b86d484 100644 --- a/tests/components/modern_forms/test_binary_sensor.py +++ b/tests/components/modern_forms/test_binary_sensor.py @@ -1,7 +1,7 @@ """Tests for the Modern Forms sensor platform.""" + from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.modern_forms.const import DOMAIN -from homeassistant.const import ATTR_ICON from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -37,11 +37,9 @@ async def test_binary_sensors( # Light timer remaining time state = hass.states.get("binary_sensor.modernformsfan_light_sleep_timer_active") assert state - assert state.attributes.get(ATTR_ICON) == "mdi:av-timer" assert state.state == "off" # Fan timer remaining time state = hass.states.get("binary_sensor.modernformsfan_fan_sleep_timer_active") assert state - assert state.attributes.get(ATTR_ICON) == "mdi:av-timer" assert state.state == "off" diff --git a/tests/components/modern_forms/test_config_flow.py b/tests/components/modern_forms/test_config_flow.py index 49bac6a5bb0..6e1d2452479 100644 --- a/tests/components/modern_forms/test_config_flow.py +++ b/tests/components/modern_forms/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for the Modern Forms config flow.""" + from ipaddress import ip_address from unittest.mock import MagicMock, patch diff --git a/tests/components/modern_forms/test_fan.py b/tests/components/modern_forms/test_fan.py index 9dc5ca9960f..82ab6407c12 100644 --- a/tests/components/modern_forms/test_fan.py +++ b/tests/components/modern_forms/test_fan.py @@ -1,4 +1,5 @@ """Tests for the Modern Forms fan platform.""" + from unittest.mock import patch from aiomodernforms import ModernFormsConnectionError @@ -209,9 +210,12 @@ async def test_fan_connection_error( """Test error handling of the Moder Forms fans.""" await init_integration(hass, aioclient_mock) - with patch("homeassistant.components.modern_forms.ModernFormsDevice.update"), patch( - "homeassistant.components.modern_forms.ModernFormsDevice.fan", - side_effect=ModernFormsConnectionError, + with ( + patch("homeassistant.components.modern_forms.ModernFormsDevice.update"), + patch( + "homeassistant.components.modern_forms.ModernFormsDevice.fan", + side_effect=ModernFormsConnectionError, + ), ): await hass.services.async_call( FAN_DOMAIN, diff --git a/tests/components/modern_forms/test_init.py b/tests/components/modern_forms/test_init.py index 9befb36d00d..4f146dfcea5 100644 --- a/tests/components/modern_forms/test_init.py +++ b/tests/components/modern_forms/test_init.py @@ -1,4 +1,5 @@ """Tests for the Modern Forms integration.""" + from unittest.mock import MagicMock, patch from aiomodernforms import ModernFormsConnectionError diff --git a/tests/components/modern_forms/test_light.py b/tests/components/modern_forms/test_light.py index 080290944b2..3b1cfdd90d2 100644 --- a/tests/components/modern_forms/test_light.py +++ b/tests/components/modern_forms/test_light.py @@ -1,4 +1,5 @@ """Tests for the Modern Forms light platform.""" + from unittest.mock import patch from aiomodernforms import ModernFormsConnectionError @@ -137,9 +138,12 @@ async def test_light_connection_error( """Test error handling of the Moder Forms lights.""" await init_integration(hass, aioclient_mock) - with patch("homeassistant.components.modern_forms.ModernFormsDevice.update"), patch( - "homeassistant.components.modern_forms.ModernFormsDevice.light", - side_effect=ModernFormsConnectionError, + with ( + patch("homeassistant.components.modern_forms.ModernFormsDevice.update"), + patch( + "homeassistant.components.modern_forms.ModernFormsDevice.light", + side_effect=ModernFormsConnectionError, + ), ): await hass.services.async_call( LIGHT_DOMAIN, diff --git a/tests/components/modern_forms/test_sensor.py b/tests/components/modern_forms/test_sensor.py index 279942f39a9..9058808443e 100644 --- a/tests/components/modern_forms/test_sensor.py +++ b/tests/components/modern_forms/test_sensor.py @@ -1,8 +1,9 @@ """Tests for the Modern Forms sensor platform.""" + from datetime import datetime from homeassistant.components.sensor import SensorDeviceClass -from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_ICON +from homeassistant.const import ATTR_DEVICE_CLASS from homeassistant.core import HomeAssistant from . import init_integration, modern_forms_timers_set_mock @@ -21,14 +22,12 @@ async def test_sensors( # Light timer remaining time state = hass.states.get("sensor.modernformsfan_light_sleep_time") assert state - assert state.attributes.get(ATTR_ICON) == "mdi:timer-outline" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TIMESTAMP assert state.state == "unknown" # Fan timer remaining time state = hass.states.get("sensor.modernformsfan_fan_sleep_time") assert state - assert state.attributes.get(ATTR_ICON) == "mdi:timer-outline" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TIMESTAMP assert state.state == "unknown" @@ -44,13 +43,11 @@ async def test_active_sensors( # Light timer remaining time state = hass.states.get("sensor.modernformsfan_light_sleep_time") assert state - assert state.attributes.get(ATTR_ICON) == "mdi:timer-outline" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TIMESTAMP datetime.fromisoformat(state.state) # Fan timer remaining time state = hass.states.get("sensor.modernformsfan_fan_sleep_time") assert state - assert state.attributes.get(ATTR_ICON) == "mdi:timer-outline" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TIMESTAMP datetime.fromisoformat(state.state) diff --git a/tests/components/modern_forms/test_switch.py b/tests/components/modern_forms/test_switch.py index b0ddc31150b..8a2012bbd5f 100644 --- a/tests/components/modern_forms/test_switch.py +++ b/tests/components/modern_forms/test_switch.py @@ -1,4 +1,5 @@ """Tests for the Modern Forms switch platform.""" + from unittest.mock import patch from aiomodernforms import ModernFormsConnectionError @@ -7,7 +8,6 @@ import pytest from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.const import ( ATTR_ENTITY_ID, - ATTR_ICON, SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_OFF, @@ -31,7 +31,6 @@ async def test_switch_state( state = hass.states.get("switch.modernformsfan_away_mode") assert state - assert state.attributes.get(ATTR_ICON) == "mdi:airplane-takeoff" assert state.state == STATE_OFF entry = entity_registry.async_get("switch.modernformsfan_away_mode") @@ -40,7 +39,6 @@ async def test_switch_state( state = hass.states.get("switch.modernformsfan_adaptive_learning") assert state - assert state.attributes.get(ATTR_ICON) == "mdi:school-outline" assert state.state == STATE_OFF entry = entity_registry.async_get("switch.modernformsfan_adaptive_learning") @@ -132,9 +130,12 @@ async def test_switch_connection_error( """Test error handling of the Modern Forms switches.""" await init_integration(hass, aioclient_mock) - with patch("homeassistant.components.modern_forms.ModernFormsDevice.update"), patch( - "homeassistant.components.modern_forms.ModernFormsDevice.away", - side_effect=ModernFormsConnectionError, + with ( + patch("homeassistant.components.modern_forms.ModernFormsDevice.update"), + patch( + "homeassistant.components.modern_forms.ModernFormsDevice.away", + side_effect=ModernFormsConnectionError, + ), ): await hass.services.async_call( SWITCH_DOMAIN, diff --git a/tests/components/moehlenhoff_alpha2/test_config_flow.py b/tests/components/moehlenhoff_alpha2/test_config_flow.py index 7123400365e..fcce2d139d2 100644 --- a/tests/components/moehlenhoff_alpha2/test_config_flow.py +++ b/tests/components/moehlenhoff_alpha2/test_config_flow.py @@ -1,4 +1,5 @@ """Test the moehlenhoff_alpha2 config flow.""" + from unittest.mock import patch from homeassistant import config_entries @@ -31,10 +32,13 @@ async def test_form(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.FORM assert not result["errors"] - with patch("moehlenhoff_alpha2.Alpha2Base.update_data", mock_update_data), patch( - "homeassistant.components.moehlenhoff_alpha2.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch("moehlenhoff_alpha2.Alpha2Base.update_data", mock_update_data), + patch( + "homeassistant.components.moehlenhoff_alpha2.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( flow_id=result["flow_id"], user_input={"host": MOCK_BASE_HOST}, diff --git a/tests/components/mold_indicator/test_sensor.py b/tests/components/mold_indicator/test_sensor.py index bfea5bccde4..0acea3d03e6 100644 --- a/tests/components/mold_indicator/test_sensor.py +++ b/tests/components/mold_indicator/test_sensor.py @@ -1,4 +1,5 @@ """The tests for the MoldIndicator sensor.""" + import pytest from homeassistant.components.mold_indicator.sensor import ( diff --git a/tests/components/monoprice/test_config_flow.py b/tests/components/monoprice/test_config_flow.py index 70087910e55..74c69078b1d 100644 --- a/tests/components/monoprice/test_config_flow.py +++ b/tests/components/monoprice/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Monoprice 6-Zone Amplifier config flow.""" + from unittest.mock import patch from serial import SerialException @@ -33,13 +34,16 @@ async def test_form(hass: HomeAssistant) -> None: assert result["type"] == "form" assert result["errors"] == {} - with patch( - "homeassistant.components.monoprice.config_flow.get_monoprice", - return_value=True, - ), patch( - "homeassistant.components.monoprice.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.monoprice.config_flow.get_monoprice", + return_value=True, + ), + patch( + "homeassistant.components.monoprice.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], CONFIG ) diff --git a/tests/components/monoprice/test_media_player.py b/tests/components/monoprice/test_media_player.py index 36577c259d0..f7d88692cf5 100644 --- a/tests/components/monoprice/test_media_player.py +++ b/tests/components/monoprice/test_media_player.py @@ -1,4 +1,5 @@ """The tests for Monoprice Media player platform.""" + from collections import defaultdict from unittest.mock import patch @@ -182,7 +183,7 @@ async def test_service_calls_with_entity_id(hass: HomeAssistant) -> None: # Restoring other media player to its previous state # The zone should not be restored await _call_monoprice_service(hass, SERVICE_RESTORE, {"entity_id": ZONE_2_ID}) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) # Checking that values were not (!) restored state = hass.states.get(ZONE_1_ID) @@ -192,7 +193,7 @@ async def test_service_calls_with_entity_id(hass: HomeAssistant) -> None: # Restoring media player to its previous state await _call_monoprice_service(hass, SERVICE_RESTORE, {"entity_id": ZONE_1_ID}) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(ZONE_1_ID) @@ -225,7 +226,7 @@ async def test_service_calls_with_all_entities(hass: HomeAssistant) -> None: # Restoring media player to its previous state await _call_monoprice_service(hass, SERVICE_RESTORE, {"entity_id": "all"}) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(ZONE_1_ID) @@ -258,7 +259,7 @@ async def test_service_calls_without_relevant_entities(hass: HomeAssistant) -> N # Restoring media player to its previous state await _call_monoprice_service(hass, SERVICE_RESTORE, {"entity_id": "light.demo"}) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(ZONE_1_ID) @@ -272,7 +273,7 @@ async def test_restore_without_snapshort(hass: HomeAssistant) -> None: with patch.object(MockMonoprice, "restore_zone") as method_call: await _call_monoprice_service(hass, SERVICE_RESTORE, {"entity_id": ZONE_1_ID}) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert not method_call.called @@ -294,7 +295,7 @@ async def test_update(hass: HomeAssistant) -> None: monoprice.set_volume(11, 38) await async_update_entity(hass, ZONE_1_ID) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(ZONE_1_ID) @@ -320,7 +321,7 @@ async def test_failed_update(hass: HomeAssistant) -> None: with patch.object(MockMonoprice, "zone_status", side_effect=SerialException): await async_update_entity(hass, ZONE_1_ID) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(ZONE_1_ID) @@ -346,7 +347,7 @@ async def test_empty_update(hass: HomeAssistant) -> None: with patch.object(MockMonoprice, "zone_status", return_value=None): await async_update_entity(hass, ZONE_1_ID) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(ZONE_1_ID) @@ -417,7 +418,7 @@ async def test_unknown_source(hass: HomeAssistant) -> None: monoprice.set_source(11, 5) await async_update_entity(hass, ZONE_1_ID) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(ZONE_1_ID) diff --git a/tests/components/moon/conftest.py b/tests/components/moon/conftest.py index 5c8157f257d..57e957077ab 100644 --- a/tests/components/moon/conftest.py +++ b/tests/components/moon/conftest.py @@ -1,4 +1,5 @@ """Fixtures for Moon integration tests.""" + from __future__ import annotations from collections.abc import Generator diff --git a/tests/components/moon/test_config_flow.py b/tests/components/moon/test_config_flow.py index cd2ab94fefc..8fbab51f5a2 100644 --- a/tests/components/moon/test_config_flow.py +++ b/tests/components/moon/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for the Moon config flow.""" + from unittest.mock import MagicMock from homeassistant.components.moon.const import DOMAIN diff --git a/tests/components/moon/test_sensor.py b/tests/components/moon/test_sensor.py index 38af8dcb912..8dd50bfa99d 100644 --- a/tests/components/moon/test_sensor.py +++ b/tests/components/moon/test_sensor.py @@ -1,4 +1,5 @@ """The test for the moon sensor platform.""" + from __future__ import annotations from unittest.mock import patch @@ -6,7 +7,6 @@ from unittest.mock import patch import pytest from homeassistant.components.moon.sensor import ( - MOON_ICONS, STATE_FIRST_QUARTER, STATE_FULL_MOON, STATE_LAST_QUARTER, @@ -17,7 +17,7 @@ from homeassistant.components.moon.sensor import ( STATE_WAXING_GIBBOUS, ) from homeassistant.components.sensor import ATTR_OPTIONS, SensorDeviceClass -from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_FRIENDLY_NAME, ATTR_ICON +from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_FRIENDLY_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er @@ -25,16 +25,16 @@ from tests.common import MockConfigEntry @pytest.mark.parametrize( - ("moon_value", "native_value", "icon"), + ("moon_value", "native_value"), [ - (0, STATE_NEW_MOON, MOON_ICONS[STATE_NEW_MOON]), - (5, STATE_WAXING_CRESCENT, MOON_ICONS[STATE_WAXING_CRESCENT]), - (7, STATE_FIRST_QUARTER, MOON_ICONS[STATE_FIRST_QUARTER]), - (12, STATE_WAXING_GIBBOUS, MOON_ICONS[STATE_WAXING_GIBBOUS]), - (14.3, STATE_FULL_MOON, MOON_ICONS[STATE_FULL_MOON]), - (20.1, STATE_WANING_GIBBOUS, MOON_ICONS[STATE_WANING_GIBBOUS]), - (20.8, STATE_LAST_QUARTER, MOON_ICONS[STATE_LAST_QUARTER]), - (23, STATE_WANING_CRESCENT, MOON_ICONS[STATE_WANING_CRESCENT]), + (0, STATE_NEW_MOON), + (5, STATE_WAXING_CRESCENT), + (7, STATE_FIRST_QUARTER), + (12, STATE_WAXING_GIBBOUS), + (14.3, STATE_FULL_MOON), + (20.1, STATE_WANING_GIBBOUS), + (20.8, STATE_LAST_QUARTER), + (23, STATE_WANING_CRESCENT), ], ) async def test_moon_day( @@ -44,7 +44,6 @@ async def test_moon_day( mock_config_entry: MockConfigEntry, moon_value: float, native_value: str, - icon: str, ) -> None: """Test the Moon sensor.""" mock_config_entry.add_to_hass(hass) @@ -58,7 +57,6 @@ async def test_moon_day( 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" assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.ENUM assert state.attributes[ATTR_OPTIONS] == [ diff --git a/tests/components/mopeka/__init__.py b/tests/components/mopeka/__init__.py index 3446b1dc66b..43650349be8 100644 --- a/tests/components/mopeka/__init__.py +++ b/tests/components/mopeka/__init__.py @@ -1,6 +1,5 @@ """Tests for the Mopeka integration.""" - from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo NOT_MOPEKA_SERVICE_INFO = BluetoothServiceInfo( diff --git a/tests/components/mopeka/test_config_flow.py b/tests/components/mopeka/test_config_flow.py index 67fc9fc37ed..8de1fd81add 100644 --- a/tests/components/mopeka/test_config_flow.py +++ b/tests/components/mopeka/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Mopeka config flow.""" + from unittest.mock import patch from homeassistant import config_entries diff --git a/tests/components/mopeka/test_sensor.py b/tests/components/mopeka/test_sensor.py index 626aa44efd4..47e436bad9a 100644 --- a/tests/components/mopeka/test_sensor.py +++ b/tests/components/mopeka/test_sensor.py @@ -1,4 +1,5 @@ """Test the Mopeka sensors.""" + from homeassistant.components.mopeka.const import DOMAIN from homeassistant.components.sensor import ATTR_STATE_CLASS from homeassistant.const import ( diff --git a/tests/components/motion_blinds/test_config_flow.py b/tests/components/motion_blinds/test_config_flow.py index 43051e88562..8d290b0b380 100644 --- a/tests/components/motion_blinds/test_config_flow.py +++ b/tests/components/motion_blinds/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Motionblinds config flow.""" + import socket from unittest.mock import Mock, patch @@ -72,41 +73,55 @@ TEST_INTERFACES = [ @pytest.fixture(name="motion_blinds_connect", autouse=True) def motion_blinds_connect_fixture(mock_get_source_ip): """Mock Motionblinds connection and entry setup.""" - with patch( - "homeassistant.components.motion_blinds.gateway.MotionGateway.GetDeviceList", - return_value=True, - ), patch( - "homeassistant.components.motion_blinds.gateway.MotionGateway.Update", - return_value=True, - ), patch( - "homeassistant.components.motion_blinds.gateway.MotionGateway.Check_gateway_multicast", - return_value=True, - ), patch( - "homeassistant.components.motion_blinds.gateway.MotionGateway.device_list", - TEST_DEVICE_LIST, - ), patch( - "homeassistant.components.motion_blinds.gateway.MotionGateway.mac", - TEST_MAC, - ), patch( - "homeassistant.components.motion_blinds.config_flow.MotionDiscovery.discover", - return_value=TEST_DISCOVERY_1, - ), patch( - "homeassistant.components.motion_blinds.config_flow.MotionGateway.GetDeviceList", - return_value=True, - ), patch( - "homeassistant.components.motion_blinds.config_flow.MotionGateway.available", - True, - ), patch( - "homeassistant.components.motion_blinds.gateway.AsyncMotionMulticast.Start_listen", - return_value=True, - ), patch( - "homeassistant.components.motion_blinds.gateway.AsyncMotionMulticast.Stop_listen", - return_value=True, - ), patch( - "homeassistant.components.motion_blinds.gateway.network.async_get_adapters", - return_value=TEST_INTERFACES, - ), patch( - "homeassistant.components.motion_blinds.async_setup_entry", return_value=True + with ( + patch( + "homeassistant.components.motion_blinds.gateway.MotionGateway.GetDeviceList", + return_value=True, + ), + patch( + "homeassistant.components.motion_blinds.gateway.MotionGateway.Update", + return_value=True, + ), + patch( + "homeassistant.components.motion_blinds.gateway.MotionGateway.Check_gateway_multicast", + return_value=True, + ), + patch( + "homeassistant.components.motion_blinds.gateway.MotionGateway.device_list", + TEST_DEVICE_LIST, + ), + patch( + "homeassistant.components.motion_blinds.gateway.MotionGateway.mac", + TEST_MAC, + ), + patch( + "homeassistant.components.motion_blinds.config_flow.MotionDiscovery.discover", + return_value=TEST_DISCOVERY_1, + ), + patch( + "homeassistant.components.motion_blinds.config_flow.MotionGateway.GetDeviceList", + return_value=True, + ), + patch( + "homeassistant.components.motion_blinds.config_flow.MotionGateway.available", + True, + ), + patch( + "homeassistant.components.motion_blinds.gateway.AsyncMotionMulticast.Start_listen", + return_value=True, + ), + patch( + "homeassistant.components.motion_blinds.gateway.AsyncMotionMulticast.Stop_listen", + return_value=True, + ), + patch( + "homeassistant.components.motion_blinds.gateway.network.async_get_adapters", + return_value=TEST_INTERFACES, + ), + patch( + "homeassistant.components.motion_blinds.async_setup_entry", + return_value=True, + ), ): yield diff --git a/tests/components/motion_blinds/test_gateway.py b/tests/components/motion_blinds/test_gateway.py index 4166f741c4a..3258790b940 100644 --- a/tests/components/motion_blinds/test_gateway.py +++ b/tests/components/motion_blinds/test_gateway.py @@ -1,4 +1,5 @@ """Test the Motionblinds config flow.""" + from unittest.mock import Mock from motionblinds import DEVICE_TYPES_WIFI, BlindType diff --git a/tests/components/motionblinds_ble/__init__.py b/tests/components/motionblinds_ble/__init__.py new file mode 100644 index 00000000000..c2385555dbf --- /dev/null +++ b/tests/components/motionblinds_ble/__init__.py @@ -0,0 +1 @@ +"""Tests for the Motionblinds Bluetooth integration.""" diff --git a/tests/components/motionblinds_ble/conftest.py b/tests/components/motionblinds_ble/conftest.py new file mode 100644 index 00000000000..ae487957302 --- /dev/null +++ b/tests/components/motionblinds_ble/conftest.py @@ -0,0 +1,36 @@ +"""Setup the Motionblinds Bluetooth tests.""" + +from unittest.mock import AsyncMock, Mock, patch + +import pytest + +TEST_MAC = "abcd" +TEST_NAME = f"MOTION_{TEST_MAC.upper()}" +TEST_ADDRESS = "test_adress" + + +@pytest.fixture(name="motionblinds_ble_connect", autouse=True) +def motion_blinds_connect_fixture(enable_bluetooth): + """Mock motion blinds ble connection and entry setup.""" + device = Mock() + device.name = TEST_NAME + device.address = TEST_ADDRESS + + bleak_scanner = AsyncMock() + bleak_scanner.discover.return_value = [device] + + with ( + patch( + "homeassistant.components.motionblinds_ble.config_flow.bluetooth.async_scanner_count", + return_value=1, + ), + patch( + "homeassistant.components.motionblinds_ble.config_flow.bluetooth.async_get_scanner", + return_value=bleak_scanner, + ), + patch( + "homeassistant.components.motionblinds_ble.async_setup_entry", + return_value=True, + ), + ): + yield bleak_scanner, device diff --git a/tests/components/motionblinds_ble/test_config_flow.py b/tests/components/motionblinds_ble/test_config_flow.py new file mode 100644 index 00000000000..f540fdf421c --- /dev/null +++ b/tests/components/motionblinds_ble/test_config_flow.py @@ -0,0 +1,256 @@ +"""Test the Motionblinds Bluetooth config flow.""" + +from unittest.mock import patch + +from motionblindsble.const import MotionBlindType + +from homeassistant import config_entries, data_entry_flow +from homeassistant.components.bluetooth.models import BluetoothServiceInfoBleak +from homeassistant.components.motionblinds_ble import const +from homeassistant.const import CONF_ADDRESS +from homeassistant.core import HomeAssistant + +from .conftest import TEST_ADDRESS, TEST_MAC, TEST_NAME + +from tests.components.bluetooth import generate_advertisement_data, generate_ble_device + +TEST_BLIND_TYPE = MotionBlindType.ROLLER.name.lower() + +BLIND_SERVICE_INFO = BluetoothServiceInfoBleak( + name=TEST_NAME, + address=TEST_ADDRESS, + device=generate_ble_device( + address="cc:cc:cc:cc:cc:cc", + name=TEST_NAME, + ), + rssi=-61, + manufacturer_data={000: b"test"}, + service_data={ + "test": bytearray(b"0000"), + }, + service_uuids=[ + "test", + ], + source="local", + advertisement=generate_advertisement_data( + manufacturer_data={000: b"test"}, + service_uuids=["test"], + ), + connectable=True, + time=0, +) + + +async def test_config_flow_manual_success( + hass: HomeAssistant, motionblinds_ble_connect +) -> None: + """Successful flow manually initialized by the user.""" + result = await hass.config_entries.flow.async_init( + const.DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] is data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {const.CONF_MAC_CODE: TEST_MAC}, + ) + assert result["type"] is data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "confirm" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {const.CONF_BLIND_TYPE: MotionBlindType.ROLLER.name.lower()}, + ) + assert result["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["title"] == f"Motionblind {TEST_MAC.upper()}" + assert result["data"] == { + CONF_ADDRESS: TEST_ADDRESS, + const.CONF_LOCAL_NAME: TEST_NAME, + const.CONF_MAC_CODE: TEST_MAC.upper(), + const.CONF_BLIND_TYPE: TEST_BLIND_TYPE, + } + assert result["options"] == {} + + +async def test_config_flow_manual_error_invalid_mac( + hass: HomeAssistant, motionblinds_ble_connect +) -> None: + """Invalid MAC code error flow manually initialized by the user.""" + + # Initialize + result = await hass.config_entries.flow.async_init( + const.DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] is data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + # Try invalid MAC code + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {const.CONF_MAC_CODE: "AABBCC"}, # A MAC code should be 4 characters + ) + assert result["type"] is data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": const.ERROR_INVALID_MAC_CODE} + + # Recover + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {const.CONF_MAC_CODE: TEST_MAC}, + ) + assert result["type"] is data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "confirm" + + # Finish flow + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {const.CONF_BLIND_TYPE: MotionBlindType.ROLLER.name.lower()}, + ) + assert result["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["title"] == f"Motionblind {TEST_MAC.upper()}" + assert result["data"] == { + CONF_ADDRESS: TEST_ADDRESS, + const.CONF_LOCAL_NAME: TEST_NAME, + const.CONF_MAC_CODE: TEST_MAC.upper(), + const.CONF_BLIND_TYPE: TEST_BLIND_TYPE, + } + assert result["options"] == {} + + +async def test_config_flow_manual_error_no_bluetooth_adapter( + hass: HomeAssistant, motionblinds_ble_connect +) -> None: + """No Bluetooth adapter error flow manually initialized by the user.""" + + # Try step_user with zero Bluetooth adapters + with patch( + "homeassistant.components.motionblinds_ble.config_flow.bluetooth.async_scanner_count", + return_value=0, + ): + result = await hass.config_entries.flow.async_init( + const.DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] is data_entry_flow.FlowResultType.ABORT + assert result["reason"] == const.ERROR_NO_BLUETOOTH_ADAPTER + + # Try discovery with zero Bluetooth adapters + result = await hass.config_entries.flow.async_init( + const.DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] is data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + with patch( + "homeassistant.components.motionblinds_ble.config_flow.bluetooth.async_scanner_count", + return_value=0, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {const.CONF_MAC_CODE: TEST_MAC}, + ) + assert result["type"] is data_entry_flow.FlowResultType.ABORT + assert result["reason"] == const.ERROR_NO_BLUETOOTH_ADAPTER + + +async def test_config_flow_manual_error_could_not_find_motor( + hass: HomeAssistant, motionblinds_ble_connect +) -> None: + """Could not find motor error flow manually initialized by the user.""" + + # Initialize + result = await hass.config_entries.flow.async_init( + const.DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] is data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + # Try with MAC code that cannot be found + motionblinds_ble_connect[1].name = "WRONG_NAME" + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {const.CONF_MAC_CODE: TEST_MAC}, + ) + assert result["type"] is data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": const.ERROR_COULD_NOT_FIND_MOTOR} + + # Recover + motionblinds_ble_connect[1].name = TEST_NAME + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {const.CONF_MAC_CODE: TEST_MAC}, + ) + assert result["type"] is data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "confirm" + + # Finish flow + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {const.CONF_BLIND_TYPE: MotionBlindType.ROLLER.name.lower()}, + ) + assert result["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["title"] == f"Motionblind {TEST_MAC.upper()}" + assert result["data"] == { + CONF_ADDRESS: TEST_ADDRESS, + const.CONF_LOCAL_NAME: TEST_NAME, + const.CONF_MAC_CODE: TEST_MAC.upper(), + const.CONF_BLIND_TYPE: TEST_BLIND_TYPE, + } + assert result["options"] == {} + + +async def test_config_flow_manual_error_no_devices_found( + hass: HomeAssistant, motionblinds_ble_connect +) -> None: + """No devices found error flow manually initialized by the user.""" + + # Initialize + result = await hass.config_entries.flow.async_init( + const.DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] is data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + # Try with zero found bluetooth devices + motionblinds_ble_connect[0].discover.return_value = [] + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {const.CONF_MAC_CODE: TEST_MAC}, + ) + assert result["type"] is data_entry_flow.FlowResultType.ABORT + assert result["reason"] == const.ERROR_NO_DEVICES_FOUND + + +async def test_config_flow_bluetooth_success( + hass: HomeAssistant, motionblinds_ble_connect +) -> None: + """Successful bluetooth discovery flow.""" + result = await hass.config_entries.flow.async_init( + const.DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=BLIND_SERVICE_INFO, + ) + + assert result["type"] is data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "confirm" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {const.CONF_BLIND_TYPE: MotionBlindType.ROLLER.name.lower()}, + ) + + assert result["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["title"] == f"Motionblind {TEST_MAC.upper()}" + assert result["data"] == { + CONF_ADDRESS: TEST_ADDRESS, + const.CONF_LOCAL_NAME: TEST_NAME, + const.CONF_MAC_CODE: TEST_MAC.upper(), + const.CONF_BLIND_TYPE: TEST_BLIND_TYPE, + } + assert result["options"] == {} diff --git a/tests/components/motioneye/__init__.py b/tests/components/motioneye/__init__.py index 7558e6fbcc4..183d1b3e6bf 100644 --- a/tests/components/motioneye/__init__.py +++ b/tests/components/motioneye/__init__.py @@ -1,4 +1,5 @@ """Tests for the motionEye integration.""" + from __future__ import annotations from typing import Any diff --git a/tests/components/motioneye/test_camera.py b/tests/components/motioneye/test_camera.py index 5af8d4139eb..32763fbed3a 100644 --- a/tests/components/motioneye/test_camera.py +++ b/tests/components/motioneye/test_camera.py @@ -1,4 +1,5 @@ """Test the motionEye camera.""" + import copy from typing import Any, cast from unittest.mock import AsyncMock, Mock, call diff --git a/tests/components/motioneye/test_config_flow.py b/tests/components/motioneye/test_config_flow.py index 5cb6244010e..7163f2c8152 100644 --- a/tests/components/motioneye/test_config_flow.py +++ b/tests/components/motioneye/test_config_flow.py @@ -1,4 +1,5 @@ """Test the motionEye config flow.""" + from unittest.mock import AsyncMock, patch from motioneye_client.client import ( @@ -38,13 +39,16 @@ async def test_user_success(hass: HomeAssistant) -> None: mock_client = create_mock_motioneye_client() - with patch( - "homeassistant.components.motioneye.MotionEyeClient", - return_value=mock_client, - ), patch( - "homeassistant.components.motioneye.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.motioneye.MotionEyeClient", + return_value=mock_client, + ), + patch( + "homeassistant.components.motioneye.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -94,13 +98,16 @@ async def test_hassio_success(hass: HomeAssistant) -> None: mock_client = create_mock_motioneye_client() - with patch( - "homeassistant.components.motioneye.MotionEyeClient", - return_value=mock_client, - ), patch( - "homeassistant.components.motioneye.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.motioneye.MotionEyeClient", + return_value=mock_client, + ), + patch( + "homeassistant.components.motioneye.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], { @@ -277,13 +284,16 @@ async def test_reauth(hass: HomeAssistant) -> None: CONF_SURVEILLANCE_PASSWORD: "surveillance-password", } - with patch( - "homeassistant.components.motioneye.MotionEyeClient", - return_value=mock_client, - ), patch( - "homeassistant.components.motioneye.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.motioneye.MotionEyeClient", + return_value=mock_client, + ), + patch( + "homeassistant.components.motioneye.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result = await hass.config_entries.flow.async_configure( result["flow_id"], new_data, @@ -429,13 +439,16 @@ async def test_hassio_clean_up_on_user_flow(hass: HomeAssistant) -> None: mock_client = create_mock_motioneye_client() - with patch( - "homeassistant.components.motioneye.MotionEyeClient", - return_value=mock_client, - ), patch( - "homeassistant.components.motioneye.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.motioneye.MotionEyeClient", + return_value=mock_client, + ), + patch( + "homeassistant.components.motioneye.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], { @@ -461,12 +474,15 @@ async def test_options(hass: HomeAssistant) -> None: config_entry = create_mock_motioneye_config_entry(hass) client = create_mock_motioneye_client() - with patch( - "homeassistant.components.motioneye.MotionEyeClient", - return_value=client, - ), patch( - "homeassistant.components.motioneye.async_setup_entry", - return_value=True, + with ( + patch( + "homeassistant.components.motioneye.MotionEyeClient", + return_value=client, + ), + patch( + "homeassistant.components.motioneye.async_setup_entry", + return_value=True, + ), ): await hass.async_block_till_done() @@ -494,13 +510,16 @@ async def test_advanced_options(hass: HomeAssistant) -> None: config_entry = create_mock_motioneye_config_entry(hass) mock_client = create_mock_motioneye_client() - with patch( - "homeassistant.components.motioneye.MotionEyeClient", - return_value=mock_client, - ) as mock_setup, patch( - "homeassistant.components.motioneye.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.motioneye.MotionEyeClient", + return_value=mock_client, + ) as mock_setup, + patch( + "homeassistant.components.motioneye.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): await hass.async_block_till_done() result = await hass.config_entries.options.async_init( diff --git a/tests/components/motioneye/test_media_source.py b/tests/components/motioneye/test_media_source.py index 6b90870c4da..f895ed7fcb2 100644 --- a/tests/components/motioneye/test_media_source.py +++ b/tests/components/motioneye/test_media_source.py @@ -1,4 +1,5 @@ """Test Local Media Source.""" + import logging from unittest.mock import AsyncMock, Mock, call diff --git a/tests/components/motioneye/test_sensor.py b/tests/components/motioneye/test_sensor.py index 0892c0dead0..02e34c16c30 100644 --- a/tests/components/motioneye/test_sensor.py +++ b/tests/components/motioneye/test_sensor.py @@ -1,4 +1,5 @@ """Tests for the motionEye switch platform.""" + import copy from datetime import timedelta from unittest.mock import AsyncMock, patch diff --git a/tests/components/motioneye/test_switch.py b/tests/components/motioneye/test_switch.py index a6fbcc49052..56401e7a28d 100644 --- a/tests/components/motioneye/test_switch.py +++ b/tests/components/motioneye/test_switch.py @@ -1,4 +1,5 @@ """Tests for the motionEye switch platform.""" + import copy from datetime import timedelta from unittest.mock import AsyncMock, call, patch diff --git a/tests/components/motioneye/test_web_hooks.py b/tests/components/motioneye/test_web_hooks.py index 7c66645bb44..fae7fccbb6d 100644 --- a/tests/components/motioneye/test_web_hooks.py +++ b/tests/components/motioneye/test_web_hooks.py @@ -1,4 +1,5 @@ """Test the motionEye camera web hooks.""" + import copy from http import HTTPStatus from unittest.mock import AsyncMock, Mock, call, patch @@ -221,18 +222,18 @@ async def test_setup_camera_with_correct_webhook( cameras = copy.deepcopy(TEST_CAMERAS) cameras[KEY_CAMERAS][0][KEY_WEB_HOOK_NOTIFICATIONS_ENABLED] = True - cameras[KEY_CAMERAS][0][ - KEY_WEB_HOOK_NOTIFICATIONS_HTTP_METHOD - ] = KEY_HTTP_METHOD_POST_JSON + cameras[KEY_CAMERAS][0][KEY_WEB_HOOK_NOTIFICATIONS_HTTP_METHOD] = ( + KEY_HTTP_METHOD_POST_JSON + ) cameras[KEY_CAMERAS][0][KEY_WEB_HOOK_NOTIFICATIONS_URL] = ( "https://internal.url" + URL_WEBHOOK_PATH.format(webhook_id=config_entry.data[CONF_WEBHOOK_ID]) + f"?{WEB_HOOK_MOTION_DETECTED_QUERY_STRING}&device_id={device.id}" ) cameras[KEY_CAMERAS][0][KEY_WEB_HOOK_STORAGE_ENABLED] = True - cameras[KEY_CAMERAS][0][ - KEY_WEB_HOOK_STORAGE_HTTP_METHOD - ] = KEY_HTTP_METHOD_POST_JSON + cameras[KEY_CAMERAS][0][KEY_WEB_HOOK_STORAGE_HTTP_METHOD] = ( + KEY_HTTP_METHOD_POST_JSON + ) cameras[KEY_CAMERAS][0][KEY_WEB_HOOK_STORAGE_URL] = ( "https://internal.url" + URL_WEBHOOK_PATH.format(webhook_id=config_entry.data[CONF_WEBHOOK_ID]) diff --git a/tests/components/motionmount/conftest.py b/tests/components/motionmount/conftest.py index 8a838dac83c..f0b8e2f7df7 100644 --- a/tests/components/motionmount/conftest.py +++ b/tests/components/motionmount/conftest.py @@ -1,4 +1,5 @@ """Fixtures for Vogel's MotionMount integration tests.""" + from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch diff --git a/tests/components/motionmount/test_config_flow.py b/tests/components/motionmount/test_config_flow.py index aa7ea73b577..f24c4e7a2e4 100644 --- a/tests/components/motionmount/test_config_flow.py +++ b/tests/components/motionmount/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for the Vogel's MotionMount config flow.""" + import dataclasses import socket from unittest.mock import MagicMock, PropertyMock diff --git a/tests/components/mqtt/test_alarm_control_panel.py b/tests/components/mqtt/test_alarm_control_panel.py index 89c5d2ffd91..ff78d96d37e 100644 --- a/tests/components/mqtt/test_alarm_control_panel.py +++ b/tests/components/mqtt/test_alarm_control_panel.py @@ -1,4 +1,5 @@ """The tests the MQTT alarm control panel component.""" + import copy import json from typing import Any @@ -34,7 +35,6 @@ from homeassistant.const import ( STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, STATE_UNKNOWN, - Platform, ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError @@ -134,15 +134,6 @@ DEFAULT_CONFIG_REMOTE_CODE_TEXT = { } -@pytest.fixture(autouse=True) -def alarm_control_panel_platform_only(): - """Only setup the alarm_control_panel platform to speed up tests.""" - with patch( - "homeassistant.components.mqtt.PLATFORMS", [Platform.ALARM_CONTROL_PANEL] - ): - yield - - @pytest.mark.parametrize( ("hass_config", "valid"), [ @@ -1341,9 +1332,13 @@ async def test_reload_after_invalid_config( }, ] } - with patch( - "homeassistant.config.load_yaml_config_file", return_value=invalid_config - ), pytest.raises(HomeAssistantError): + with ( + patch( + "homeassistant.config.load_yaml_config_file", + return_value=invalid_config, + ), + pytest.raises(HomeAssistantError), + ): await hass.services.async_call( "mqtt", SERVICE_RELOAD, diff --git a/tests/components/mqtt/test_binary_sensor.py b/tests/components/mqtt/test_binary_sensor.py index 033026226e2..995aadd7dba 100644 --- a/tests/components/mqtt/test_binary_sensor.py +++ b/tests/components/mqtt/test_binary_sensor.py @@ -1,4 +1,5 @@ """The tests for the MQTT binary sensor platform.""" + import copy from datetime import datetime, timedelta import json @@ -17,7 +18,6 @@ from homeassistant.const import ( STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN, - Platform, ) from homeassistant.core import HomeAssistant, State, callback from homeassistant.helpers.typing import ConfigType @@ -71,13 +71,6 @@ DEFAULT_CONFIG = { } -@pytest.fixture(autouse=True) -def binary_sensor_platform_only(): - """Only setup the binary_sensor platform to speed up tests.""" - with patch("homeassistant.components.mqtt.PLATFORMS", [Platform.BINARY_SENSOR]): - yield - - @pytest.mark.parametrize( "hass_config", [ diff --git a/tests/components/mqtt/test_button.py b/tests/components/mqtt/test_button.py index f2f91c5ca75..3d5d295d4d4 100644 --- a/tests/components/mqtt/test_button.py +++ b/tests/components/mqtt/test_button.py @@ -1,4 +1,5 @@ """The tests for the MQTT button platform.""" + import copy from typing import Any from unittest.mock import patch @@ -6,12 +7,7 @@ from unittest.mock import patch import pytest from homeassistant.components import button, mqtt -from homeassistant.const import ( - ATTR_ENTITY_ID, - ATTR_FRIENDLY_NAME, - STATE_UNKNOWN, - Platform, -) +from homeassistant.const import ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, STATE_UNKNOWN from homeassistant.core import HomeAssistant from .test_common import ( @@ -49,13 +45,6 @@ DEFAULT_CONFIG = { } -@pytest.fixture(autouse=True) -def button_platform_only(): - """Only setup the button platform to speed up tests.""" - with patch("homeassistant.components.mqtt.PLATFORMS", [Platform.BUTTON]): - yield - - @pytest.mark.freeze_time("2021-11-08 13:31:44+00:00") @pytest.mark.parametrize( "hass_config", diff --git a/tests/components/mqtt/test_camera.py b/tests/components/mqtt/test_camera.py index 5552457c213..fb0107d6780 100644 --- a/tests/components/mqtt/test_camera.py +++ b/tests/components/mqtt/test_camera.py @@ -1,4 +1,5 @@ """The tests for mqtt camera component.""" + from base64 import b64encode from http import HTTPStatus import json @@ -8,7 +9,6 @@ import pytest from homeassistant.components import camera, mqtt from homeassistant.components.mqtt.camera import MQTT_CAMERA_ATTRIBUTES_BLOCKED -from homeassistant.const import Platform from homeassistant.core import HomeAssistant from .test_common import ( @@ -48,13 +48,6 @@ from tests.typing import ( DEFAULT_CONFIG = {mqtt.DOMAIN: {camera.DOMAIN: {"name": "test", "topic": "test_topic"}}} -@pytest.fixture(autouse=True) -def camera_platform_only(): - """Only setup the camera platform to speed up tests.""" - with patch("homeassistant.components.mqtt.PLATFORMS", [Platform.CAMERA]): - yield - - @pytest.mark.parametrize( "hass_config", [{mqtt.DOMAIN: {camera.DOMAIN: {"topic": "test/camera", "name": "Test Camera"}}}], diff --git a/tests/components/mqtt/test_climate.py b/tests/components/mqtt/test_climate.py index 8d513b98179..1224fce098d 100644 --- a/tests/components/mqtt/test_climate.py +++ b/tests/components/mqtt/test_climate.py @@ -1,4 +1,5 @@ """The tests for the mqtt climate component.""" + import copy import json from typing import Any @@ -31,7 +32,7 @@ from homeassistant.components.mqtt.climate import ( MQTT_CLIMATE_ATTRIBUTES_BLOCKED, VALUE_TEMPLATE_KEYS, ) -from homeassistant.const import ATTR_TEMPERATURE, Platform, UnitOfTemperature +from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature from homeassistant.core import HomeAssistant from homeassistant.exceptions import ServiceValidationError @@ -98,13 +99,6 @@ DEFAULT_CONFIG = { } -@pytest.fixture(autouse=True) -def climate_platform_only(): - """Only setup the climate platform to speed up tests.""" - with patch("homeassistant.components.mqtt.PLATFORMS", [Platform.CLIMATE]): - yield - - @pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG]) async def test_setup_params( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator diff --git a/tests/components/mqtt/test_common.py b/tests/components/mqtt/test_common.py index 54b0f7f3506..9dc52871529 100644 --- a/tests/components/mqtt/test_common.py +++ b/tests/components/mqtt/test_common.py @@ -1,4 +1,5 @@ """Common test objects.""" + from collections.abc import Iterable from contextlib import suppress import copy @@ -384,9 +385,10 @@ async def help_test_default_availability_list_single( ] config[mqtt.DOMAIN][domain]["availability_topic"] = "availability-topic" - with patch( - "homeassistant.config.load_yaml_config_file", return_value=config - ), suppress(vol.MultipleInvalid): + with ( + patch("homeassistant.config.load_yaml_config_file", return_value=config), + suppress(vol.MultipleInvalid), + ): await mqtt_mock_entry() assert ( @@ -589,9 +591,9 @@ async def help_test_setting_attribute_with_template( # Add JSON attributes settings to config config = copy.deepcopy(config) config[mqtt.DOMAIN][domain]["json_attributes_topic"] = "attr-topic" - config[mqtt.DOMAIN][domain][ - "json_attributes_template" - ] = "{{ value_json['Timer1'] | tojson }}" + config[mqtt.DOMAIN][domain]["json_attributes_template"] = ( + "{{ value_json['Timer1'] | tojson }}" + ) with patch("homeassistant.config.load_yaml_config_file", return_value=config): await mqtt_mock_entry() @@ -1326,7 +1328,7 @@ async def help_test_entity_debug_info_max_messages( start_dt = datetime(2019, 1, 1, 0, 0, 0, tzinfo=dt_util.UTC) with freeze_time(start_dt): - for i in range(0, debug_info.STORED_MESSAGES + 1): + for i in range(debug_info.STORED_MESSAGES + 1): async_fire_mqtt_message(hass, "test-topic", f"{i}") debug_info_data = debug_info.info_for_device(hass, device.id) @@ -1697,9 +1699,9 @@ async def help_test_publishing_with_custom_encoding( if test_data["encoding"] is not None: test_config_setup["encoding"] = test_data["encoding"] if template and test_data["cmd_tpl"]: - test_config_setup[ - template - ] = f"{{{{ (('%.1f'|format({tpl_par}))[0] if is_number({tpl_par}) else {tpl_par}[0]) | ord | pack('b') }}}}" + test_config_setup[template] = ( + f"{{{{ (('%.1f'|format({tpl_par}))[0] if is_number({tpl_par}) else {tpl_par}[0]) | ord | pack('b') }}}}" + ) setup_config.append(test_config_setup) # setup service data @@ -1709,10 +1711,8 @@ async def help_test_publishing_with_custom_encoding( # setup test entities using discovery mqtt_mock = await mqtt_mock_entry() - item: int = 0 - for component_config in setup_config: + for item, component_config in enumerate(setup_config): conf = json.dumps(component_config) - item += 1 async_fire_mqtt_message( hass, f"homeassistant/{domain}/component_{item}/config", conf ) diff --git a/tests/components/mqtt/test_config_flow.py b/tests/components/mqtt/test_config_flow.py index c8ffe7909eb..719117e59a9 100644 --- a/tests/components/mqtt/test_config_flow.py +++ b/tests/components/mqtt/test_config_flow.py @@ -1,4 +1,5 @@ """Test config flow.""" + from collections.abc import Generator, Iterator from contextlib import contextmanager from pathlib import Path @@ -54,13 +55,15 @@ def mock_client_key_check_fail() -> Generator[MagicMock, None, None]: @pytest.fixture def mock_ssl_context() -> Generator[dict[str, MagicMock], None, None]: """Mock the SSL context used to load the cert chain and to load verify locations.""" - with patch( - "homeassistant.components.mqtt.config_flow.SSLContext" - ) as mock_context, patch( - "homeassistant.components.mqtt.config_flow.load_pem_private_key" - ) as mock_key_check, patch( - "homeassistant.components.mqtt.config_flow.load_pem_x509_certificate" - ) as mock_cert_check: + with ( + patch("homeassistant.components.mqtt.config_flow.SSLContext") as mock_context, + patch( + "homeassistant.components.mqtt.config_flow.load_pem_private_key" + ) as mock_key_check, + patch( + "homeassistant.components.mqtt.config_flow.load_pem_x509_certificate" + ) as mock_cert_check, + ): yield { "context": mock_context, "load_pem_x509_certificate": mock_cert_check, @@ -122,8 +125,9 @@ def mock_try_connection_time_out() -> Generator[MagicMock, None, None]: """Mock the try connection method with a time out.""" # Patch prevent waiting 5 sec for a timeout - with patch("paho.mqtt.client.Client") as mock_client, patch( - "homeassistant.components.mqtt.config_flow.MQTT_TIMEOUT", 0 + with ( + patch("paho.mqtt.client.Client") as mock_client, + patch("homeassistant.components.mqtt.config_flow.MQTT_TIMEOUT", 0), ): mock_client().loop_start = lambda *args: 1 yield mock_client() diff --git a/tests/components/mqtt/test_cover.py b/tests/components/mqtt/test_cover.py index 3c3349d7f90..b2b1d1bd9c6 100644 --- a/tests/components/mqtt/test_cover.py +++ b/tests/components/mqtt/test_cover.py @@ -1,4 +1,5 @@ """The tests for the MQTT cover platform.""" + from copy import deepcopy from typing import Any from unittest.mock import patch @@ -42,7 +43,6 @@ from homeassistant.const import ( STATE_OPEN, STATE_OPENING, STATE_UNKNOWN, - Platform, ) from homeassistant.core import HomeAssistant @@ -85,13 +85,6 @@ DEFAULT_CONFIG = { } -@pytest.fixture(autouse=True) -def cover_platform_only(): - """Only setup the cover platform to speed up tests.""" - with patch("homeassistant.components.mqtt.PLATFORMS", [Platform.COVER]): - yield - - @pytest.mark.parametrize( "hass_config", [ diff --git a/tests/components/mqtt/test_device_tracker.py b/tests/components/mqtt/test_device_tracker.py index bbed85c2e75..680c48d13c7 100644 --- a/tests/components/mqtt/test_device_tracker.py +++ b/tests/components/mqtt/test_device_tracker.py @@ -1,13 +1,13 @@ """The tests for the MQTT device_tracker platform.""" + from datetime import UTC, datetime -from unittest.mock import patch from freezegun.api import FrozenDateTimeFactory import pytest from homeassistant.components import device_tracker, mqtt from homeassistant.components.mqtt.const import DOMAIN as MQTT_DOMAIN -from homeassistant.const import STATE_HOME, STATE_NOT_HOME, STATE_UNKNOWN, Platform +from homeassistant.const import STATE_HOME, STATE_NOT_HOME, STATE_UNKNOWN from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.setup import async_setup_component @@ -36,13 +36,6 @@ DEFAULT_CONFIG = { } -@pytest.fixture(autouse=True) -def device_tracker_platform_only(): - """Only setup the device_tracker platform to speed up tests.""" - with patch("homeassistant.components.mqtt.PLATFORMS", [Platform.DEVICE_TRACKER]): - yield - - async def test_discover_device_tracker( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, diff --git a/tests/components/mqtt/test_device_trigger.py b/tests/components/mqtt/test_device_trigger.py index bb8d973b9ed..4f4c9a18bd9 100644 --- a/tests/components/mqtt/test_device_trigger.py +++ b/tests/components/mqtt/test_device_trigger.py @@ -1,6 +1,6 @@ """The tests for MQTT device triggers.""" + import json -from unittest.mock import patch import pytest from pytest_unordered import unordered @@ -8,7 +8,6 @@ from pytest_unordered import unordered import homeassistant.components.automation as automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.mqtt import _LOGGER, DOMAIN, debug_info -from homeassistant.const import Platform from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr @@ -36,16 +35,6 @@ def calls(hass: HomeAssistant) -> list[ServiceCall]: return async_mock_service(hass, "test", "automation") -@pytest.fixture(autouse=True) -def binary_sensor_and_sensor_only(): - """Only setup the binary_sensor and sensor platform to speed up tests.""" - with patch( - "homeassistant.components.mqtt.PLATFORMS", - [Platform.BINARY_SENSOR, Platform.SENSOR], - ): - yield - - async def test_get_triggers( hass: HomeAssistant, device_registry: dr.DeviceRegistry, diff --git a/tests/components/mqtt/test_diagnostics.py b/tests/components/mqtt/test_diagnostics.py index eb923ac2f07..f14c1bd5fc4 100644 --- a/tests/components/mqtt/test_diagnostics.py +++ b/tests/components/mqtt/test_diagnostics.py @@ -1,13 +1,13 @@ """Test MQTT diagnostics.""" + import json -from unittest.mock import ANY, patch +from unittest.mock import ANY import pytest from homeassistant.components import mqtt -from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry as dr +from homeassistant.helpers import device_registry as dr, entity_registry as er from tests.common import async_fire_mqtt_message from tests.components.diagnostics import ( @@ -22,19 +22,10 @@ default_config = { } -@pytest.fixture(autouse=True) -def device_tracker_sensor_only(): - """Only setup the device_tracker and sensor platforms to speed up tests.""" - with patch( - "homeassistant.components.mqtt.PLATFORMS", - [Platform.DEVICE_TRACKER, Platform.SENSOR], - ): - yield - - async def test_entry_diagnostics( hass: HomeAssistant, device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, hass_client: ClientSessionGenerator, mqtt_mock_entry: MqttMockHAClientGenerator, ) -> None: @@ -117,6 +108,7 @@ async def test_entry_diagnostics( "attributes": {"friendly_name": "MQTT Sensor"}, "entity_id": "sensor.none_mqtt_sensor", "last_changed": ANY, + "last_reported": ANY, "last_updated": ANY, "state": "unknown", }, @@ -243,6 +235,7 @@ async def test_redact_diagnostics( }, "entity_id": "device_tracker.mqtt_unique", "last_changed": ANY, + "last_reported": ANY, "last_updated": ANY, "state": "home", }, @@ -270,3 +263,28 @@ async def test_redact_diagnostics( "mqtt_config": expected_config, "mqtt_debug_info": expected_debug_info, } + + # Disable the entity and remove the state + ent_registry = er.async_get(hass) + device_tracker_entry = er.async_entries_for_device(ent_registry, device_entry.id)[0] + ent_registry.async_update_entity( + device_tracker_entry.entity_id, disabled_by=er.RegistryEntryDisabler.USER + ) + hass.states.async_remove(device_tracker_entry.entity_id) + + # Assert disabled entries are filtered + assert await get_diagnostics_for_device( + hass, hass_client, config_entry, device_entry + ) == { + "connected": True, + "device": { + "id": device_entry.id, + "name": None, + "name_by_user": None, + "disabled": False, + "disabled_by": None, + "entities": [], + }, + "mqtt_config": expected_config, + "mqtt_debug_info": {"entities": [], "triggers": []}, + } diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index f86800fd95f..24891895fad 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -1,4 +1,5 @@ """The tests for the MQTT discovery.""" + import asyncio import copy import json @@ -68,7 +69,6 @@ async def test_subscribing_config_topic( assert discovery_topic + "/+/+/+/config" in topics -@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.BINARY_SENSOR]) @pytest.mark.parametrize( ("topic", "log"), [ @@ -102,7 +102,6 @@ async def test_invalid_topic( caplog.clear() -@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.BINARY_SENSOR]) async def test_invalid_json( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, @@ -123,7 +122,9 @@ async def test_invalid_json( assert not mock_dispatcher_send.called -@pytest.mark.parametrize("domain", [*list(mqtt.PLATFORMS), "device_automation", "tag"]) +@pytest.mark.parametrize( + "domain", ["tag", "device_automation", Platform.SENSOR, Platform.LIGHT] +) @pytest.mark.no_fail_on_log_exception async def test_discovery_schema_error( hass: HomeAssistant, @@ -146,7 +147,6 @@ async def test_discovery_schema_error( assert "AttributeError: Attribute abc not found" in caplog.text -@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.ALARM_CONTROL_PANEL]) async def test_invalid_config( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, @@ -190,7 +190,6 @@ async def test_only_valid_components( assert not mock_dispatcher_send.called -@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.BINARY_SENSOR]) async def test_correct_config_discovery( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, @@ -211,7 +210,6 @@ async def test_correct_config_discovery( assert ("binary_sensor", "bla") in hass.data["mqtt"].discovery_already_discovered -@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.BINARY_SENSOR]) async def test_discovery_integration_info( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, @@ -264,7 +262,6 @@ async def test_discovery_integration_info( '{ "name": "Beer", "state_topic": "test-topic", "o": {"sw": "bla2mqtt"} }', ], ) -@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.BINARY_SENSOR]) async def test_discovery_with_invalid_integration_info( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, @@ -288,7 +285,6 @@ async def test_discovery_with_invalid_integration_info( ) -@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.FAN]) async def test_discover_fan( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, @@ -309,7 +305,6 @@ async def test_discover_fan( assert ("fan", "bla") in hass.data["mqtt"].discovery_already_discovered -@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.CLIMATE]) async def test_discover_climate( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, @@ -333,7 +328,6 @@ async def test_discover_climate( assert ("climate", "bla") in hass.data["mqtt"].discovery_already_discovered -@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.ALARM_CONTROL_PANEL]) async def test_discover_alarm_control_panel( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, @@ -524,7 +518,6 @@ async def test_discovery_with_object_id( assert (domain, "object bla") in hass.data["mqtt"].discovery_already_discovered -@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.BINARY_SENSOR]) async def test_discovery_incl_nodeid( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, @@ -547,7 +540,6 @@ async def test_discovery_incl_nodeid( ].discovery_already_discovered -@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.BINARY_SENSOR]) async def test_non_duplicate_discovery( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, @@ -576,7 +568,6 @@ async def test_non_duplicate_discovery( assert "Component has already been discovered: binary_sensor bla" in caplog.text -@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.BINARY_SENSOR]) async def test_removal( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, @@ -598,7 +589,6 @@ async def test_removal( assert state is None -@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.BINARY_SENSOR]) async def test_rediscover( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, @@ -629,7 +619,6 @@ async def test_rediscover( assert state is not None -@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.BINARY_SENSOR]) async def test_rapid_rediscover( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, @@ -682,7 +671,6 @@ async def test_rapid_rediscover( assert events[4].data["old_state"] is None -@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.BINARY_SENSOR]) async def test_rapid_rediscover_unique( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, @@ -745,7 +733,6 @@ async def test_rapid_rediscover_unique( assert events[3].data["old_state"] is None -@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.BINARY_SENSOR]) async def test_rapid_reconfigure( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, @@ -801,7 +788,6 @@ async def test_rapid_reconfigure( assert events[2].data["new_state"].attributes["friendly_name"] == "Wine" -@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.BINARY_SENSOR]) async def test_duplicate_removal( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, @@ -887,7 +873,6 @@ async def test_cleanup_device( ) -@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.SENSOR]) async def test_cleanup_device_mqtt( hass: HomeAssistant, device_registry: dr.DeviceRegistry, @@ -933,7 +918,6 @@ async def test_cleanup_device_mqtt( mqtt_mock.async_publish.assert_not_called() -@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.SENSOR]) async def test_cleanup_device_multiple_config_entries( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, @@ -1125,7 +1109,6 @@ async def test_cleanup_device_multiple_config_entries_mqtt( mqtt_mock.async_publish.assert_not_called() -@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.SWITCH]) async def test_discovery_expansion( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, @@ -1188,7 +1171,6 @@ async def test_discovery_expansion( assert state and state.state == STATE_UNAVAILABLE -@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.SWITCH]) async def test_discovery_expansion_2( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, @@ -1233,7 +1215,6 @@ async def test_discovery_expansion_2( assert state.state == STATE_UNKNOWN -@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.SWITCH]) async def test_discovery_expansion_3( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, @@ -1318,7 +1299,6 @@ async def test_discovery_expansion_without_encoding_and_value_template_1( assert state and state.state == STATE_UNAVAILABLE -@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.SWITCH]) async def test_discovery_expansion_without_encoding_and_value_template_2( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, @@ -1419,20 +1399,17 @@ async def test_missing_discover_abbreviations( continue with open(fil, encoding="utf-8") as file: matches = re.findall(regex, file.read()) - for match in matches: - if ( - match[1] not in ABBREVIATIONS.values() - and match[1] not in DEVICE_ABBREVIATIONS.values() - and match[0] not in ABBREVIATIONS_WHITE_LIST - ): - missing.append( - f"{fil}: no abbreviation for {match[1]} ({match[0]})" - ) + missing.extend( + f"{fil}: no abbreviation for {match[1]} ({match[0]})" + for match in matches + if match[1] not in ABBREVIATIONS.values() + and match[1] not in DEVICE_ABBREVIATIONS.values() + and match[0] not in ABBREVIATIONS_WHITE_LIST + ) assert not missing -@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.SWITCH]) async def test_no_implicit_state_topic_switch( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, @@ -1457,7 +1434,6 @@ async def test_no_implicit_state_topic_switch( assert state and state.state == STATE_UNKNOWN -@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.BINARY_SENSOR]) @pytest.mark.parametrize( "mqtt_config_entry_data", [ @@ -1489,7 +1465,6 @@ async def test_complex_discovery_topic_prefix( ].discovery_already_discovered -@patch("homeassistant.components.mqtt.PLATFORMS", []) @patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) @patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) @patch("homeassistant.components.mqtt.client.UNSUBSCRIBE_COOLDOWN", 0.0) @@ -1531,15 +1506,15 @@ async def test_mqtt_integration_discovery_subscribe_unsubscribe( wait_unsub.set() return (0, 0) - with mock_config_flow("comp", TestFlow), patch.object( - mqtt_client_mock, "unsubscribe", side_effect=_mock_unsubscribe + with ( + mock_config_flow("comp", TestFlow), + patch.object(mqtt_client_mock, "unsubscribe", side_effect=_mock_unsubscribe), ): async_fire_mqtt_message(hass, "comp/discovery/bla/config", "") await wait_unsub.wait() mqtt_client_mock.unsubscribe.assert_called_once_with(["comp/discovery/#"]) -@patch("homeassistant.components.mqtt.PLATFORMS", []) @patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) @patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) @patch("homeassistant.components.mqtt.client.UNSUBSCRIBE_COOLDOWN", 0.0) @@ -1571,6 +1546,7 @@ async def test_mqtt_discovery_unsubscribe_once( async def async_step_mqtt(self, discovery_info: MqttServiceInfo) -> FlowResult: """Test mqtt step.""" + await asyncio.sleep(0.1) return self.async_abort(reason="already_configured") with mock_config_flow("comp", TestFlow): @@ -1582,7 +1558,6 @@ async def test_mqtt_discovery_unsubscribe_once( mqtt_client_mock.unsubscribe.assert_called_once_with(["comp/discovery/#"]) -@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.SENSOR]) async def test_clear_config_topic_disabled_entity( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, @@ -1657,7 +1632,6 @@ async def test_clear_config_topic_disabled_entity( ) -@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.SENSOR]) async def test_clean_up_registry_monitoring( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, @@ -1712,7 +1686,6 @@ async def test_clean_up_registry_monitoring( assert len(hooks) == 0 -@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.SENSOR]) async def test_unique_id_collission_has_priority( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, @@ -1759,7 +1732,6 @@ async def test_unique_id_collission_has_priority( assert entity_registry.async_get("sensor.abc123_sbfspot_12345_2") is None -@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.SENSOR]) async def test_update_with_bad_config_not_breaks_discovery( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator ) -> None: diff --git a/tests/components/mqtt/test_event.py b/tests/components/mqtt/test_event.py index bd00120d098..64a2003606c 100644 --- a/tests/components/mqtt/test_event.py +++ b/tests/components/mqtt/test_event.py @@ -1,4 +1,5 @@ """The tests for the MQTT event platform.""" + import copy import json from unittest.mock import patch @@ -8,7 +9,7 @@ import pytest from homeassistant.components import event, mqtt from homeassistant.components.mqtt.event import MQTT_EVENT_ATTRIBUTES_BLOCKED -from homeassistant.const import STATE_UNKNOWN, Platform +from homeassistant.const import STATE_UNKNOWN from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr @@ -64,13 +65,6 @@ DEFAULT_CONFIG = { } -@pytest.fixture(autouse=True) -def event_platform_only(): - """Only setup the event platform to speed up tests.""" - with patch("homeassistant.components.mqtt.PLATFORMS", [Platform.EVENT]): - yield - - @pytest.mark.freeze_time("2023-08-01 00:00:00+00:00") @pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG]) async def test_setting_event_value_via_mqtt_message( @@ -469,12 +463,12 @@ async def test_discovery_update_event_template( config2["name"] = "Milk" config1["state_topic"] = "event/state1" config2["state_topic"] = "event/state1" - config1[ - "value_template" - ] = '{"event_type": "press", "val": "{{ value_json.val | int }}"}' - config2[ - "value_template" - ] = '{"event_type": "press", "val": "{{ value_json.val | int * 2 }}"}' + config1["value_template"] = ( + '{"event_type": "press", "val": "{{ value_json.val | int }}"}' + ) + config2["value_template"] = ( + '{"event_type": "press", "val": "{{ value_json.val | int * 2 }}"}' + ) async_fire_mqtt_message(hass, "homeassistant/event/bla/config", json.dumps(config1)) await hass.async_block_till_done() diff --git a/tests/components/mqtt/test_fan.py b/tests/components/mqtt/test_fan.py index 8980d6951e5..0dbfa3037b2 100644 --- a/tests/components/mqtt/test_fan.py +++ b/tests/components/mqtt/test_fan.py @@ -1,4 +1,5 @@ """Test MQTT fans.""" + import copy from typing import Any from unittest.mock import patch @@ -32,7 +33,6 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, STATE_UNKNOWN, - Platform, ) from homeassistant.core import HomeAssistant @@ -82,13 +82,6 @@ DEFAULT_CONFIG = { } -@pytest.fixture(autouse=True) -def fan_platform_only(): - """Only setup the fan platform to speed up tests.""" - with patch("homeassistant.components.mqtt.PLATFORMS", [Platform.FAN]): - yield - - @pytest.mark.parametrize("hass_config", [{mqtt.DOMAIN: {fan.DOMAIN: {"name": "test"}}}]) async def test_fail_setup_if_no_command_topic( hass: HomeAssistant, diff --git a/tests/components/mqtt/test_humidifier.py b/tests/components/mqtt/test_humidifier.py index d76fe84d0a9..75baca046bd 100644 --- a/tests/components/mqtt/test_humidifier.py +++ b/tests/components/mqtt/test_humidifier.py @@ -1,4 +1,5 @@ """Test MQTT humidifiers.""" + import copy from typing import Any from unittest.mock import patch @@ -33,7 +34,6 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, STATE_UNKNOWN, - Platform, ) from homeassistant.core import HomeAssistant @@ -83,13 +83,6 @@ DEFAULT_CONFIG = { } -@pytest.fixture(autouse=True) -def humidifer_platform_only(): - """Only setup the humidifer platform to speed up tests.""" - with patch("homeassistant.components.mqtt.PLATFORMS", [Platform.HUMIDIFIER]): - yield - - async def async_turn_on( hass: HomeAssistant, entity_id=ENTITY_MATCH_ALL, diff --git a/tests/components/mqtt/test_image.py b/tests/components/mqtt/test_image.py index 5a87f06653b..79e6cf1d281 100644 --- a/tests/components/mqtt/test_image.py +++ b/tests/components/mqtt/test_image.py @@ -1,4 +1,5 @@ """The tests for mqtt image component.""" + from base64 import b64encode from http import HTTPStatus import json @@ -10,7 +11,7 @@ import pytest import respx from homeassistant.components import image, mqtt -from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN, Platform +from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN from homeassistant.core import HomeAssistant from .test_common import ( @@ -53,13 +54,6 @@ DEFAULT_CONFIG = { } -@pytest.fixture(autouse=True) -def image_platform_only(): - """Only setup the image platform to speed up tests.""" - with patch("homeassistant.components.mqtt.PLATFORMS", [Platform.IMAGE]): - yield - - @pytest.mark.freeze_time("2023-04-01 00:00:00+00:00") @pytest.mark.parametrize( "hass_config", @@ -455,7 +449,7 @@ async def test_image_from_url_fails( state = hass.states.get("image.test") - # The image failed to load, the the last image update is registered + # The image failed to load, the last image update is registered # but _last_image was set to `None` assert state.state == "2023-04-01T00:00:00+00:00" client = await hass_client_no_auth() diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 9fe394bd797..a9f2ba4354b 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -1,6 +1,7 @@ """The tests for the MQTT component.""" + import asyncio -from collections.abc import Generator +from copy import deepcopy from datetime import datetime, timedelta from functools import partial import json @@ -19,8 +20,10 @@ from homeassistant.components.mqtt.mixins import MQTT_ENTITY_DEVICE_INFO_SCHEMA from homeassistant.components.mqtt.models import ( MessageCallbackType, MqttCommandTemplateException, + MqttValueTemplateException, ReceiveMessage, ) +from homeassistant.components.sensor import SensorDeviceClass from homeassistant.config_entries import ConfigEntryDisabler, ConfigEntryState from homeassistant.const import ( ATTR_ASSUMED_STATE, @@ -29,7 +32,6 @@ from homeassistant.const import ( SERVICE_RELOAD, STATE_UNAVAILABLE, STATE_UNKNOWN, - Platform, UnitOfTemperature, ) import homeassistant.core as ha @@ -51,10 +53,9 @@ from tests.common import ( async_fire_mqtt_message, async_fire_time_changed, mock_restore_cache, + setup_test_component_platform, ) -from tests.testing_config.custom_components.test.sensor import ( # type: ignore[attr-defined] - DEVICE_CLASSES, -) +from tests.components.sensor.common import MockSensor from tests.typing import ( MqttMockHAClient, MqttMockHAClientGenerator, @@ -89,16 +90,6 @@ class RecordCallsPartial(partial[Any]): __name__ = "RecordCallPartialTest" -@pytest.fixture(autouse=True) -def sensor_platforms_only() -> Generator[None, None, None]: - """Only setup the sensor platforms to speed up tests.""" - with patch( - "homeassistant.components.mqtt.PLATFORMS", - [Platform.SENSOR, Platform.BINARY_SENSOR], - ): - yield - - @pytest.fixture(autouse=True) def mock_storage(hass_storage: dict[str, Any]) -> None: """Autouse hass_storage for the TestCase tests.""" @@ -165,7 +156,6 @@ async def test_mqtt_disconnects_on_home_assistant_stop( assert mqtt_client_mock.loop_stop.call_count == 1 -@patch("homeassistant.components.mqtt.PLATFORMS", []) async def test_mqtt_await_ack_at_disconnect( hass: HomeAssistant, ) -> None: @@ -309,7 +299,6 @@ async def test_command_template_value(hass: HomeAssistant) -> None: assert cmd_tpl.async_render(None, variables=variables) == "beer" -@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.SELECT]) @pytest.mark.parametrize( "config", [ @@ -433,37 +422,30 @@ async def test_value_template_value(hass: HomeAssistant) -> None: assert template_state_calls.call_count == 1 -async def test_value_template_fails( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture -) -> None: +async def test_value_template_fails(hass: HomeAssistant) -> None: """Test the rendering of MQTT value template fails.""" - - # test rendering a value fails entity = MockEntity(entity_id="sensor.test") entity.hass = hass tpl = template.Template("{{ value_json.some_var * 2 }}") val_tpl = mqtt.MqttValueTemplate(tpl, hass=hass, entity=entity) - with pytest.raises(TypeError) as exc: + with pytest.raises(MqttValueTemplateException) as exc: val_tpl.async_render_with_possible_json_value('{"some_var": null }') - assert str(exc.value) == "unsupported operand type(s) for *: 'NoneType' and 'int'" - assert ( + assert str(exc.value) == ( "TypeError: unsupported operand type(s) for *: 'NoneType' and 'int' " "rendering template for entity 'sensor.test', " - "template: '{{ value_json.some_var * 2 }}'" - ) in caplog.text - caplog.clear() - with pytest.raises(TypeError) as exc: + "template: '{{ value_json.some_var * 2 }}' " + 'and payload: {"some_var": null }' + ) + with pytest.raises(MqttValueTemplateException) as exc: val_tpl.async_render_with_possible_json_value( '{"some_var": null }', default=100 ) - assert str(exc.value) == "unsupported operand type(s) for *: 'NoneType' and 'int'" - assert ( + assert str(exc.value) == ( "TypeError: unsupported operand type(s) for *: 'NoneType' and 'int' " "rendering template for entity 'sensor.test', " "template: '{{ value_json.some_var * 2 }}', default value: 100 and payload: " '{"some_var": null }' - ) in caplog.text - await hass.async_block_till_done() + ) async def test_service_call_without_topic_does_not_publish( @@ -568,8 +550,8 @@ async def test_service_call_with_template_topic_renders_invalid_topic( blocking=True, ) assert str(exc.value) == ( - "Unable to publish: topic template 'test/{{ '+' if True else 'topic' }}/topic' " - "produced an invalid topic 'test/+/topic' after rendering " + "Unable to publish: topic template `test/{{ '+' if True else 'topic' }}/topic` " + "produced an invalid topic `test/+/topic` after rendering " "(Wildcards cannot be used in topic names)" ) assert not mqtt_mock.async_publish.called @@ -753,11 +735,11 @@ def test_validate_topic() -> None: with pytest.raises(vol.Invalid): mqtt.util.valid_topic("\u0001") with pytest.raises(vol.Invalid): - mqtt.util.valid_topic("\u001F") + mqtt.util.valid_topic("\u001f") with pytest.raises(vol.Invalid): - mqtt.util.valid_topic("\u007F") + mqtt.util.valid_topic("\u007f") with pytest.raises(vol.Invalid): - mqtt.util.valid_topic("\u009F") + mqtt.util.valid_topic("\u009f") with pytest.raises(vol.Invalid): mqtt.util.valid_topic("\ufdd0") with pytest.raises(vol.Invalid): @@ -2166,7 +2148,6 @@ async def test_handle_message_callback( } ], ) -@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.LIGHT]) async def test_setup_manual_mqtt_with_platform_key( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, @@ -2181,7 +2162,6 @@ async def test_setup_manual_mqtt_with_platform_key( @pytest.mark.parametrize("hass_config", [{mqtt.DOMAIN: {"light": {"name": "test"}}}]) -@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.LIGHT]) async def test_setup_manual_mqtt_with_invalid_config( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, @@ -2192,7 +2172,6 @@ async def test_setup_manual_mqtt_with_invalid_config( assert "required key not provided" in caplog.text -@patch("homeassistant.components.mqtt.PLATFORMS", []) @pytest.mark.parametrize( ("mqtt_config_entry_data", "protocol"), [ @@ -2233,7 +2212,6 @@ async def test_setup_mqtt_client_protocol( @patch("homeassistant.components.mqtt.client.TIMEOUT_ACK", 0.2) -@patch("homeassistant.components.mqtt.PLATFORMS", []) async def test_handle_mqtt_timeout_on_callback( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: @@ -2304,7 +2282,6 @@ async def test_setup_raises_config_entry_not_ready_if_no_connect_broker( ({"broker": "test-broker", "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: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, @@ -2728,7 +2705,7 @@ async def test_mqtt_ws_subscription( async_fire_mqtt_message(hass, "test-topic", "test1") async_fire_mqtt_message(hass, "test-topic", "test2") - async_fire_mqtt_message(hass, "test-topic", b"\xDE\xAD\xBE\xEF") + async_fire_mqtt_message(hass, "test-topic", b"\xde\xad\xbe\xef") response = await client.receive_json() assert response["event"]["topic"] == "test-topic" @@ -2756,7 +2733,7 @@ async def test_mqtt_ws_subscription( async_fire_mqtt_message(hass, "test-topic", "test1", 2) async_fire_mqtt_message(hass, "test-topic", "test2", 2) - async_fire_mqtt_message(hass, "test-topic", b"\xDE\xAD\xBE\xEF", 2) + async_fire_mqtt_message(hass, "test-topic", b"\xde\xad\xbe\xef", 2) response = await client.receive_json() assert response["event"]["topic"] == "test-topic" @@ -2925,7 +2902,6 @@ async def test_mqtt_ws_get_device_debug_info( assert response["result"] == expected_result -@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.CAMERA]) async def test_mqtt_ws_get_device_debug_info_binary( hass: HomeAssistant, device_registry: dr.DeviceRegistry, @@ -3166,12 +3142,12 @@ async def test_debug_info_non_mqtt( device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, mqtt_mock_entry: MqttMockHAClientGenerator, + mock_sensor_entities: dict[str, MockSensor], ) -> None: """Test we get empty debug_info for a device with non MQTT entities.""" await mqtt_mock_entry() domain = "sensor" - platform = getattr(hass.components, f"test.{domain}") - platform.init() + setup_test_component_platform(hass, domain, mock_sensor_entities) config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) @@ -3179,11 +3155,11 @@ async def test_debug_info_non_mqtt( config_entry_id=config_entry.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) - for device_class in DEVICE_CLASSES: + for device_class in SensorDeviceClass: entity_registry.async_get_or_create( domain, "test", - platform.ENTITIES[device_class].unique_id, + mock_sensor_entities[device_class].unique_id, device_id=device_entry.id, ) @@ -3551,7 +3527,6 @@ async def test_subscribe_connection_status( assert mqtt_connected_calls_async[1] is False -@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.LIGHT]) async def test_unload_config_entry( hass: HomeAssistant, mqtt_mock: MqttMockHAClient, @@ -3568,6 +3543,7 @@ async def test_unload_config_entry( # Publish just before unloading to test await cleanup mqtt_client_mock.reset_mock() mqtt.publish(hass, "just_in_time", "published", qos=0, retain=False) + await hass.async_block_till_done() assert await hass.config_entries.async_unload(mqtt_config_entry.entry_id) new_mqtt_config_entry = mqtt_config_entry @@ -3579,7 +3555,6 @@ async def test_unload_config_entry( assert "No ACK from MQTT server" not in caplog.text -@patch("homeassistant.components.mqtt.PLATFORMS", []) async def test_publish_or_subscribe_without_valid_config_entry( hass: HomeAssistant, record_calls: MessageCallbackType ) -> None: @@ -3592,10 +3567,6 @@ async def test_publish_or_subscribe_without_valid_config_entry( await mqtt.async_subscribe(hass, "some-topic", record_calls, qos=0) -@patch( - "homeassistant.components.mqtt.PLATFORMS", - [Platform.ALARM_CONTROL_PANEL, Platform.LIGHT], -) @pytest.mark.parametrize( "hass_config", [ @@ -3627,14 +3598,20 @@ async def test_disabling_and_enabling_entry( config_alarm_control_panel = '{"name": "test_new", "state_topic": "home/alarm", "command_topic": "home/alarm/set"}' config_light = '{"name": "test_new", "command_topic": "test-topic_new"}' - # Discovery of mqtt tag - async_fire_mqtt_message(hass, "homeassistant/tag/abc/config", config_tag) + with patch( + "homeassistant.components.mqtt.mixins.mqtt_config_entry_enabled", + return_value=False, + ): + # Discovery of mqtt tag + async_fire_mqtt_message(hass, "homeassistant/tag/abc/config", config_tag) - # Late discovery of mqtt entities - async_fire_mqtt_message( - hass, "homeassistant/alarm_control_panel/abc/config", config_alarm_control_panel - ) - async_fire_mqtt_message(hass, "homeassistant/light/abc/config", config_light) + # Late discovery of mqtt entities + async_fire_mqtt_message( + hass, + "homeassistant/alarm_control_panel/abc/config", + config_alarm_control_panel, + ) + async_fire_mqtt_message(hass, "homeassistant/light/abc/config", config_light) # Disable MQTT config entry await hass.config_entries.async_set_disabled_by( @@ -3670,7 +3647,6 @@ async def test_disabling_and_enabling_entry( assert hass.states.get("alarm_control_panel.test") is not None -@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.LIGHT]) @pytest.mark.parametrize( ("hass_config", "unique"), [ @@ -4051,3 +4027,127 @@ async def test_reload_with_empty_config( await hass.async_block_till_done() assert hass.states.get("sensor.test") is None + + +@pytest.mark.parametrize( + "hass_config", + [ + { + "mqtt": [ + { + "sensor": { + "name": "test", + "state_topic": "test-topic", + } + }, + ] + } + ], +) +async def test_reload_with_new_platform_config( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, +) -> None: + """Test reloading yaml with new platform config.""" + await mqtt_mock_entry() + assert hass.states.get("sensor.test") is not None + assert hass.states.get("binary_sensor.test") is None + + new_config = { + "mqtt": [ + { + "sensor": { + "name": "test", + "state_topic": "test-topic1", + }, + "binary_sensor": { + "name": "test", + "state_topic": "test-topic2", + }, + }, + ] + } + + # Reload with an new platform config and assert again + with patch("homeassistant.config.load_yaml_config_file", return_value=new_config): + await hass.services.async_call( + "mqtt", + SERVICE_RELOAD, + {}, + blocking=True, + ) + await hass.async_block_till_done() + + assert hass.states.get("sensor.test") is not None + assert hass.states.get("binary_sensor.test") is not None + + +async def test_multi_platform_discovery( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + mqtt_mock_entry: MqttMockHAClientGenerator, +) -> None: + """Test setting up multiple platforms simultaneous.""" + await mqtt_mock_entry() + entity_configs = { + "alarm_control_panel": { + "name": "test", + "state_topic": "alarm/state", + "command_topic": "alarm/command", + }, + "button": {"name": "test", "command_topic": "test-topic"}, + "camera": {"name": "test", "topic": "test_topic"}, + "cover": {"name": "test", "state_topic": "test-topic"}, + "device_tracker": { + "name": "test", + "state_topic": "test-topic", + }, + "fan": { + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + }, + "sensor": {"name": "test", "state_topic": "test-topic"}, + "switch": {"name": "test", "command_topic": "test-topic"}, + "select": { + "name": "test", + "command_topic": "test-topic", + "options": ["milk", "beer"], + }, + } + non_entity_configs = { + "tag": { + "device": {"identifiers": ["tag_0AFFD2"]}, + "topic": "foobar/tag_scanned", + }, + "device_automation": { + "automation_type": "trigger", + "device": {"identifiers": ["device_automation_0AFFD2"]}, + "payload": "short_press", + "topic": "foobar/triggers/button1", + "type": "button_short_press", + "subtype": "button_1", + }, + } + for platform, config in entity_configs.items(): + for set_number in range(2): + set_config = deepcopy(config) + set_config["name"] = f"test_{set_number}" + topic = f"homeassistant/{platform}/bla_{set_number}/config" + async_fire_mqtt_message(hass, topic, json.dumps(set_config)) + for platform, config in non_entity_configs.items(): + topic = f"homeassistant/{platform}/bla/config" + async_fire_mqtt_message(hass, topic, json.dumps(config)) + await hass.async_block_till_done() + for set_number in range(2): + for platform in entity_configs: + entity_id = f"{platform}.test_{set_number}" + state = hass.states.get(entity_id) + assert state is not None + for platform in non_entity_configs: + assert ( + device_registry.async_get_device( + identifiers={("mqtt", f"{platform}_0AFFD2")} + ) + is not None + ) diff --git a/tests/components/mqtt/test_lawn_mower.py b/tests/components/mqtt/test_lawn_mower.py index d2680824fcd..a258339e9cc 100644 --- a/tests/components/mqtt/test_lawn_mower.py +++ b/tests/components/mqtt/test_lawn_mower.py @@ -1,4 +1,5 @@ """The tests for mqtt lawn_mower component.""" + import copy import json from typing import Any @@ -15,12 +16,7 @@ from homeassistant.components.lawn_mower import ( LawnMowerEntityFeature, ) from homeassistant.components.mqtt.lawn_mower import MQTT_LAWN_MOWER_ATTRIBUTES_BLOCKED -from homeassistant.const import ( - ATTR_ASSUMED_STATE, - ATTR_ENTITY_ID, - STATE_UNKNOWN, - Platform, -) +from homeassistant.const import ATTR_ASSUMED_STATE, ATTR_ENTITY_ID, STATE_UNKNOWN from homeassistant.core import HomeAssistant, State from .test_common import ( @@ -78,13 +74,6 @@ DEFAULT_CONFIG = { } -@pytest.fixture(autouse=True) -def lawn_mower_platform_only(): - """Only setup the lawn_mower platform to speed up tests.""" - with patch("homeassistant.components.mqtt.PLATFORMS", [Platform.LAWN_MOWER]): - yield - - @pytest.mark.parametrize( "hass_config", [ @@ -742,8 +731,8 @@ async def test_mqtt_payload_not_a_valid_activity_warning( await hass.async_block_till_done() assert ( - "Invalid activity for lawn_mower.test_lawn_mower: 'painting' (valid activies: ['error', 'paused', 'mowing', 'docked'])" - in caplog.text + "Invalid activity for lawn_mower.test_lawn_mower: 'painting' " + "(valid activities: ['error', 'paused', 'mowing', 'docked'])" in caplog.text ) diff --git a/tests/components/mqtt/test_legacy_vacuum.py b/tests/components/mqtt/test_legacy_vacuum.py index 3e88d4a4335..e4f5e3cd481 100644 --- a/tests/components/mqtt/test_legacy_vacuum.py +++ b/tests/components/mqtt/test_legacy_vacuum.py @@ -5,12 +5,10 @@ # cleanup is planned with HA Core 2025.2 import json -from unittest.mock import patch import pytest from homeassistant.components import mqtt, vacuum -from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.typing import DiscoveryInfoType @@ -20,13 +18,6 @@ from tests.typing import MqttMockHAClientGenerator DEFAULT_CONFIG = {mqtt.DOMAIN: {vacuum.DOMAIN: {"name": "test"}}} -@pytest.fixture(autouse=True) -def vacuum_platform_only(): - """Only setup the vacuum platform to speed up tests.""" - with patch("homeassistant.components.mqtt.PLATFORMS", [Platform.VACUUM]): - yield - - @pytest.mark.parametrize( ("hass_config", "removed"), [ diff --git a/tests/components/mqtt/test_light.py b/tests/components/mqtt/test_light.py index aac79ae1893..492bc6806da 100644 --- a/tests/components/mqtt/test_light.py +++ b/tests/components/mqtt/test_light.py @@ -168,6 +168,7 @@ mqtt: payload_off: "off" """ + import copy from typing import Any from unittest.mock import call, patch @@ -189,13 +190,7 @@ from homeassistant.components.mqtt.light.schema_basic import ( VALUE_TEMPLATE_KEYS, ) from homeassistant.components.mqtt.models import PublishPayloadType -from homeassistant.const import ( - ATTR_ASSUMED_STATE, - STATE_OFF, - STATE_ON, - STATE_UNKNOWN, - Platform, -) +from homeassistant.const import ATTR_ASSUMED_STATE, STATE_OFF, STATE_ON, STATE_UNKNOWN from homeassistant.core import HomeAssistant, State from .test_common import ( @@ -238,13 +233,6 @@ DEFAULT_CONFIG = { } -@pytest.fixture(autouse=True) -def light_platform_only(): - """Only setup the light platform to speed up tests.""" - with patch("homeassistant.components.mqtt.PLATFORMS", [Platform.LIGHT]): - yield - - @pytest.mark.parametrize( "hass_config", [{mqtt.DOMAIN: {light.DOMAIN: {"name": "test"}}}] ) diff --git a/tests/components/mqtt/test_light_json.py b/tests/components/mqtt/test_light_json.py index d1fa2b72a31..ff1b308ef70 100644 --- a/tests/components/mqtt/test_light_json.py +++ b/tests/components/mqtt/test_light_json.py @@ -78,6 +78,7 @@ light: brightness: true brightness_scale: 99 """ + import copy from typing import Any from unittest.mock import call, patch @@ -95,9 +96,9 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, STATE_UNKNOWN, - Platform, ) from homeassistant.core import HomeAssistant, State +from homeassistant.helpers.json import json_dumps from homeassistant.util.json import JsonValueType, json_loads from .test_common import ( @@ -149,7 +150,6 @@ COLOR_MODES_CONFIG = { mqtt.DOMAIN: { light.DOMAIN: { "brightness": True, - "color_mode": True, "effect": True, "command_topic": "test_light_rgb/set", "name": "test", @@ -169,13 +169,6 @@ COLOR_MODES_CONFIG = { } -@pytest.fixture(autouse=True) -def light_platform_only(): - """Only setup the light platform to speed up tests.""" - with patch("homeassistant.components.mqtt.PLATFORMS", [Platform.LIGHT]): - yield - - class JsonValidator: """Helper to compare JSON.""" @@ -217,7 +210,157 @@ async def test_fail_setup_if_color_mode_deprecated( ) -> None: """Test if setup fails if color mode is combined with deprecated config keys.""" assert await mqtt_mock_entry() - assert "color_mode must not be combined with any of" in caplog.text + assert "supported_color_modes must not be combined with any of" in caplog.text + + +@pytest.mark.parametrize( + ("hass_config", "color_modes"), + [ + ( + help_custom_config(light.DOMAIN, DEFAULT_CONFIG, ({"color_temp": True},)), + ("color_temp",), + ), + (help_custom_config(light.DOMAIN, DEFAULT_CONFIG, ({"hs": True},)), ("hs",)), + (help_custom_config(light.DOMAIN, DEFAULT_CONFIG, ({"rgb": True},)), ("rgb",)), + (help_custom_config(light.DOMAIN, DEFAULT_CONFIG, ({"xy": True},)), ("xy",)), + ( + help_custom_config( + light.DOMAIN, DEFAULT_CONFIG, ({"color_temp": True, "rgb": True},) + ), + ("color_temp, rgb", "rgb, color_temp"), + ), + ], + ids=["color_temp", "hs", "rgb", "xy", "color_temp, rgb"], +) +async def test_warning_if_color_mode_flags_are_used( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, + color_modes: tuple[str,], +) -> None: + """Test warnings deprecated config keys without supported color modes defined.""" + with patch( + "homeassistant.components.mqtt.light.schema_json.async_create_issue" + ) as mock_async_create_issue: + assert await mqtt_mock_entry() + assert any( + ( + f"Deprecated flags [{color_modes_case}] used in MQTT JSON light config " + "for handling color mode, please use `supported_color_modes` instead." + in caplog.text + ) + for color_modes_case in color_modes + ) + mock_async_create_issue.assert_called_once() + + +@pytest.mark.parametrize( + ("config", "color_modes"), + [ + ( + help_custom_config(light.DOMAIN, DEFAULT_CONFIG, ({"color_temp": True},)), + ("color_temp",), + ), + (help_custom_config(light.DOMAIN, DEFAULT_CONFIG, ({"hs": True},)), ("hs",)), + (help_custom_config(light.DOMAIN, DEFAULT_CONFIG, ({"rgb": True},)), ("rgb",)), + (help_custom_config(light.DOMAIN, DEFAULT_CONFIG, ({"xy": True},)), ("xy",)), + ( + help_custom_config( + light.DOMAIN, DEFAULT_CONFIG, ({"color_temp": True, "rgb": True},) + ), + ("color_temp, rgb", "rgb, color_temp"), + ), + ], + ids=["color_temp", "hs", "rgb", "xy", "color_temp, rgb"], +) +async def test_warning_on_discovery_if_color_mode_flags_are_used( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, + config: dict[str, Any], + color_modes: tuple[str,], +) -> None: + """Test warnings deprecated config keys with discovery.""" + with patch( + "homeassistant.components.mqtt.light.schema_json.async_create_issue" + ) as mock_async_create_issue: + assert await mqtt_mock_entry() + + config_payload = json_dumps(config[mqtt.DOMAIN][light.DOMAIN][0]) + async_fire_mqtt_message( + hass, + "homeassistant/light/bla/config", + config_payload, + ) + await hass.async_block_till_done() + assert any( + ( + f"Deprecated flags [{color_modes_case}] used in MQTT JSON light config " + "for handling color mode, please " + "use `supported_color_modes` instead" in caplog.text + ) + for color_modes_case in color_modes + ) + mock_async_create_issue.assert_not_called() + + +@pytest.mark.parametrize( + "hass_config", + [ + help_custom_config( + light.DOMAIN, + DEFAULT_CONFIG, + ({"color_mode": True, "supported_color_modes": ["color_temp"]},), + ), + ], + ids=["color_temp"], +) +async def test_warning_if_color_mode_option_flag_is_used( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test warning deprecated color_mode option flag is used.""" + with patch( + "homeassistant.components.mqtt.light.schema_json.async_create_issue" + ) as mock_async_create_issue: + assert await mqtt_mock_entry() + assert "Deprecated flag `color_mode` used in MQTT JSON light config" in caplog.text + mock_async_create_issue.assert_called_once() + + +@pytest.mark.parametrize( + "config", + [ + help_custom_config( + light.DOMAIN, + DEFAULT_CONFIG, + ({"color_mode": True, "supported_color_modes": ["color_temp"]},), + ), + ], + ids=["color_temp"], +) +async def test_warning_on_discovery_if_color_mode_option_flag_is_used( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, + config: dict[str, Any], +) -> None: + """Test warning deprecated color_mode option flag is used.""" + with patch( + "homeassistant.components.mqtt.light.schema_json.async_create_issue" + ) as mock_async_create_issue: + assert await mqtt_mock_entry() + + config_payload = json_dumps(config[mqtt.DOMAIN][light.DOMAIN][0]) + async_fire_mqtt_message( + hass, + "homeassistant/light/bla/config", + config_payload, + ) + await hass.async_block_till_done() + assert "Deprecated flag `color_mode` used in MQTT JSON light config" in caplog.text + mock_async_create_issue.assert_not_called() @pytest.mark.parametrize( @@ -840,7 +983,7 @@ async def test_controlling_the_state_with_legacy_color_handling( assert state.attributes.get("xy_color") is None assert not state.attributes.get(ATTR_ASSUMED_STATE) - for _ in range(0, 2): + for _ in range(2): # Returned state after the light was turned on # Receiving legacy color mode: rgb. async_fire_mqtt_message( diff --git a/tests/components/mqtt/test_light_template.py b/tests/components/mqtt/test_light_template.py index 69e6b52de17..da6195fa32e 100644 --- a/tests/components/mqtt/test_light_template.py +++ b/tests/components/mqtt/test_light_template.py @@ -24,6 +24,7 @@ If your light doesn't support color temp feature, omit `color_temp_template`. If your light doesn't support RGB feature, omit `(red|green|blue)_template`. """ + import copy from typing import Any from unittest.mock import patch @@ -41,7 +42,6 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, STATE_UNKNOWN, - Platform, ) from homeassistant.core import HomeAssistant, State @@ -93,13 +93,6 @@ DEFAULT_CONFIG = { } -@pytest.fixture(autouse=True) -def light_platform_only(): - """Only setup the light platform to speed up tests.""" - with patch("homeassistant.components.mqtt.PLATFORMS", [Platform.LIGHT]): - yield - - @pytest.mark.parametrize( "hass_config", [ diff --git a/tests/components/mqtt/test_lock.py b/tests/components/mqtt/test_lock.py index 082d328c17f..a52d1ab42f4 100644 --- a/tests/components/mqtt/test_lock.py +++ b/tests/components/mqtt/test_lock.py @@ -1,4 +1,5 @@ """The tests for the MQTT lock platform.""" + from typing import Any from unittest.mock import patch @@ -23,7 +24,6 @@ from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, STATE_UNKNOWN, - Platform, ) from homeassistant.core import HomeAssistant @@ -82,13 +82,6 @@ CONFIG_WITH_STATES = { } -@pytest.fixture(autouse=True) -def lock_platform_only(): - """Only setup the lock platform to speed up tests.""" - with patch("homeassistant.components.mqtt.PLATFORMS", [Platform.LOCK]): - yield - - @pytest.mark.parametrize( ("hass_config", "payload", "lock_state"), [ diff --git a/tests/components/mqtt/test_mixins.py b/tests/components/mqtt/test_mixins.py index 7b4b10ad776..2bcd663c243 100644 --- a/tests/components/mqtt/test_mixins.py +++ b/tests/components/mqtt/test_mixins.py @@ -10,7 +10,6 @@ from homeassistant.const import ( ATTR_FRIENDLY_NAME, EVENT_HOMEASSISTANT_STARTED, EVENT_STATE_CHANGED, - Platform, ) from homeassistant.core import CoreState, HomeAssistant, callback from homeassistant.helpers import device_registry as dr, issue_registry as ir @@ -37,7 +36,6 @@ from tests.typing import MqttMockHAClientGenerator, MqttMockPahoClient } ], ) -@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.SENSOR]) async def test_availability_with_shared_state_topic( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, @@ -296,7 +294,6 @@ async def test_availability_with_shared_state_topic( "entity_name_startswith_device_name2", ], ) -@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.SENSOR]) @patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0) async def test_default_entity_and_device_name( hass: HomeAssistant, @@ -340,7 +337,6 @@ async def test_default_entity_and_device_name( assert len(events) == 0 -@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.BINARY_SENSOR]) async def test_name_attribute_is_set_or_not( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, diff --git a/tests/components/mqtt/test_number.py b/tests/components/mqtt/test_number.py index bf4e51dc519..b0f9e79cb3e 100644 --- a/tests/components/mqtt/test_number.py +++ b/tests/components/mqtt/test_number.py @@ -1,4 +1,5 @@ """The tests for mqtt number component.""" + import json from typing import Any from unittest.mock import patch @@ -25,7 +26,6 @@ from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_MODE, ATTR_UNIT_OF_MEASUREMENT, - Platform, UnitOfTemperature, ) from homeassistant.core import HomeAssistant, State @@ -70,13 +70,6 @@ DEFAULT_CONFIG = { } -@pytest.fixture(autouse=True) -def number_platform_only(): - """Only setup the number platform to speed up tests.""" - with patch("homeassistant.components.mqtt.PLATFORMS", [Platform.NUMBER]): - yield - - @pytest.mark.parametrize( ("hass_config", "device_class", "unit_of_measurement", "values"), [ diff --git a/tests/components/mqtt/test_scene.py b/tests/components/mqtt/test_scene.py index 141bfc526d3..3e9eacd3be2 100644 --- a/tests/components/mqtt/test_scene.py +++ b/tests/components/mqtt/test_scene.py @@ -1,4 +1,5 @@ """The tests for the MQTT scene platform.""" + import copy from typing import Any from unittest.mock import patch @@ -6,7 +7,7 @@ from unittest.mock import patch import pytest from homeassistant.components import mqtt, scene -from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_ON, STATE_UNKNOWN, Platform +from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_ON, STATE_UNKNOWN from homeassistant.core import HomeAssistant, State from .test_common import ( @@ -50,13 +51,6 @@ DEFAULT_CONFIG = { } -@pytest.fixture(autouse=True) -def scene_platform_only(): - """Only setup the scene platform to speed up tests.""" - with patch("homeassistant.components.mqtt.PLATFORMS", [Platform.SCENE]): - yield - - @pytest.mark.parametrize( "hass_config", [ diff --git a/tests/components/mqtt/test_select.py b/tests/components/mqtt/test_select.py index 5a17e7fc999..e5e1352abb7 100644 --- a/tests/components/mqtt/test_select.py +++ b/tests/components/mqtt/test_select.py @@ -1,4 +1,5 @@ """The tests for mqtt select component.""" + from collections.abc import Generator import copy import json @@ -15,12 +16,7 @@ from homeassistant.components.select import ( DOMAIN as SELECT_DOMAIN, SERVICE_SELECT_OPTION, ) -from homeassistant.const import ( - ATTR_ASSUMED_STATE, - ATTR_ENTITY_ID, - STATE_UNKNOWN, - Platform, -) +from homeassistant.const import ATTR_ASSUMED_STATE, ATTR_ENTITY_ID, STATE_UNKNOWN from homeassistant.core import HomeAssistant, State from homeassistant.helpers.typing import ConfigType @@ -70,13 +66,6 @@ DEFAULT_CONFIG = { } -@pytest.fixture(autouse=True) -def select_platform_only(): - """Only setup the select platform to speed up tests.""" - with patch("homeassistant.components.mqtt.PLATFORMS", [Platform.SELECT]): - yield - - def _test_run_select_setup_params( topic: str, ) -> Generator[tuple[ConfigType, str], None]: diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index faa48012514..5ab4b660963 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -1,4 +1,5 @@ """The tests for the MQTT sensor platform.""" + import copy from datetime import datetime, timedelta import json @@ -16,7 +17,6 @@ from homeassistant.const import ( EVENT_STATE_CHANGED, STATE_UNAVAILABLE, STATE_UNKNOWN, - Platform, UnitOfTemperature, ) from homeassistant.core import Event, HomeAssistant, State, callback @@ -80,13 +80,6 @@ DEFAULT_CONFIG = { } -@pytest.fixture(autouse=True) -def sensor_platform_only(): - """Only setup the sensor platform to speed up tests.""" - with patch("homeassistant.components.mqtt.PLATFORMS", [Platform.SENSOR]): - yield - - @pytest.mark.parametrize( "hass_config", [ @@ -1456,6 +1449,7 @@ async def test_entity_name( DEFAULT_CONFIG, ( { + "state_class": "total", "availability_topic": "availability-topic", "json_attributes_topic": "json-attributes-topic", "value_template": "{{ value_json.state }}", @@ -1498,6 +1492,7 @@ async def test_skipped_async_ha_write_state( DEFAULT_CONFIG, ( { + "state_class": "total", "value_template": "{{ value_json.some_var * 1 }}", "last_reset_value_template": "{{ value_json.some_var * 2 }}", }, @@ -1517,3 +1512,62 @@ async def test_value_template_fails( "TypeError: unsupported operand type(s) for *: 'NoneType' and 'int' rendering template" in caplog.text ) + + +@pytest.mark.parametrize( + "hass_config", + [ + help_custom_config( + sensor.DOMAIN, + DEFAULT_CONFIG, + ( + { + "state_class": "total_increasing", + "last_reset_value_template": "{{ value_json.last_reset }}", + }, + ), + ), + help_custom_config( + sensor.DOMAIN, + DEFAULT_CONFIG, + ( + { + "state_class": "measurement", + "last_reset_value_template": "{{ value_json.last_reset }}", + }, + ), + ), + help_custom_config( + sensor.DOMAIN, + DEFAULT_CONFIG, + ( + { + "last_reset_value_template": "{{ value_json.last_reset }}", + }, + ), + ), + ], +) +async def test_value_incorrect_state_class_config( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, + hass_config: ConfigType, +) -> None: + """Test a sensor config with incorrect state_class config fails from yaml or discovery.""" + await mqtt_mock_entry() + assert ( + "The option `last_reset_value_template` cannot be used together with state class" + in caplog.text + ) + caplog.clear() + + config_payload = hass_config[mqtt.DOMAIN][sensor.DOMAIN][0] + async_fire_mqtt_message( + hass, "homeassistant/sensor/bla/config", json.dumps(config_payload) + ) + await hass.async_block_till_done() + assert ( + "The option `last_reset_value_template` cannot be used together with state class" + in caplog.text + ) diff --git a/tests/components/mqtt/test_siren.py b/tests/components/mqtt/test_siren.py index 8d319d3a80b..77bec4accfb 100644 --- a/tests/components/mqtt/test_siren.py +++ b/tests/components/mqtt/test_siren.py @@ -1,4 +1,5 @@ """The tests for the MQTT siren platform.""" + import copy from typing import Any from unittest.mock import patch @@ -16,7 +17,6 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, STATE_UNKNOWN, - Platform, ) from homeassistant.core import HomeAssistant @@ -59,13 +59,6 @@ DEFAULT_CONFIG = { } -@pytest.fixture(autouse=True) -def siren_platform_only(): - """Only setup the siren platform to speed up tests.""" - with patch("homeassistant.components.mqtt.PLATFORMS", [Platform.SIREN]): - yield - - async def async_turn_on( hass: HomeAssistant, entity_id: str = ENTITY_MATCH_ALL, @@ -839,7 +832,7 @@ async def test_command_templates( mqtt_mock.async_publish.assert_any_call( "test-topic", "CMD: ON, DURATION: 22, TONE: ping, VOLUME: 0.88", 0, False ) - mqtt_mock.async_publish.call_count == 1 + assert mqtt_mock.async_publish.call_count == 1 mqtt_mock.reset_mock() await async_turn_off( hass, @@ -848,7 +841,7 @@ async def test_command_templates( mqtt_mock.async_publish.assert_any_call( "test-topic", "CMD: OFF, DURATION: , TONE: , VOLUME:", 0, False ) - mqtt_mock.async_publish.call_count == 1 + assert mqtt_mock.async_publish.call_count == 1 mqtt_mock.reset_mock() await async_turn_on( @@ -869,7 +862,7 @@ async def test_command_templates( entity_id="siren.milk", ) mqtt_mock.async_publish.assert_any_call("test-topic", "CMD_OFF: OFF", 0, False) - mqtt_mock.async_publish.call_count == 1 + assert mqtt_mock.async_publish.call_count == 2 mqtt_mock.reset_mock() diff --git a/tests/components/mqtt/test_subscription.py b/tests/components/mqtt/test_subscription.py index fe8c9fb6101..54acc935f1d 100644 --- a/tests/components/mqtt/test_subscription.py +++ b/tests/components/mqtt/test_subscription.py @@ -1,5 +1,6 @@ """The tests for the MQTT subscription component.""" -from unittest.mock import ANY, patch + +from unittest.mock import ANY import pytest @@ -14,13 +15,6 @@ from tests.common import async_fire_mqtt_message from tests.typing import MqttMockHAClientGenerator -@pytest.fixture(autouse=True) -def no_platforms(): - """Skip platform setup to speed up tests.""" - with patch("homeassistant.components.mqtt.PLATFORMS", []): - yield - - async def test_subscribe_topics( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, diff --git a/tests/components/mqtt/test_switch.py b/tests/components/mqtt/test_switch.py index 46a7ebb2060..b497d4a2f52 100644 --- a/tests/components/mqtt/test_switch.py +++ b/tests/components/mqtt/test_switch.py @@ -1,4 +1,5 @@ """The tests for the MQTT switch platform.""" + import copy from typing import Any from unittest.mock import patch @@ -12,7 +13,6 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, STATE_UNKNOWN, - Platform, ) from homeassistant.core import HomeAssistant, State @@ -56,13 +56,6 @@ DEFAULT_CONFIG = { } -@pytest.fixture(autouse=True) -def switch_platform_only(): - """Only setup the switch platform to speed up tests.""" - with patch("homeassistant.components.mqtt.PLATFORMS", [Platform.SWITCH]): - yield - - @pytest.mark.parametrize( ("hass_config", "device_class"), [ diff --git a/tests/components/mqtt/test_tag.py b/tests/components/mqtt/test_tag.py index 7f5a477e62a..9a0da989216 100644 --- a/tests/components/mqtt/test_tag.py +++ b/tests/components/mqtt/test_tag.py @@ -1,4 +1,5 @@ """The tests for MQTT tag scanner.""" + from collections.abc import Generator import copy import json @@ -8,7 +9,6 @@ import pytest from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.mqtt.const import DOMAIN as MQTT_DOMAIN -from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr from homeassistant.setup import async_setup_component @@ -46,13 +46,6 @@ DEFAULT_TAG_SCAN_JSON = ( ) -@pytest.fixture(autouse=True) -def binary_sensor_only() -> Generator[None, None, None]: - """Only setup the binary_sensor platform to speed up test.""" - with patch("homeassistant.components.mqtt.PLATFORMS", [Platform.BINARY_SENSOR]): - yield - - @pytest.fixture def tag_mock() -> Generator[AsyncMock, None, None]: """Fixture to mock tag.""" diff --git a/tests/components/mqtt/test_text.py b/tests/components/mqtt/test_text.py index 265f4afcb90..63c69d3cfac 100644 --- a/tests/components/mqtt/test_text.py +++ b/tests/components/mqtt/test_text.py @@ -1,4 +1,5 @@ """The tests for the MQTT text platform.""" + from __future__ import annotations from typing import Any @@ -7,12 +8,7 @@ from unittest.mock import patch import pytest from homeassistant.components import mqtt, text -from homeassistant.const import ( - ATTR_ASSUMED_STATE, - ATTR_ENTITY_ID, - STATE_UNKNOWN, - Platform, -) +from homeassistant.const import ATTR_ASSUMED_STATE, ATTR_ENTITY_ID, STATE_UNKNOWN from homeassistant.core import HomeAssistant from .test_common import ( @@ -54,13 +50,6 @@ DEFAULT_CONFIG = { } -@pytest.fixture(autouse=True) -def text_platform_only(): - """Only setup the text platform to speed up tests.""" - with patch("homeassistant.components.mqtt.PLATFORMS", [Platform.TEXT]): - yield - - async def async_set_value( hass: HomeAssistant, entity_id: str, value: str | None ) -> None: diff --git a/tests/components/mqtt/test_trigger.py b/tests/components/mqtt/test_trigger.py index 97ded1f2294..90a39bfd4fb 100644 --- a/tests/components/mqtt/test_trigger.py +++ b/tests/components/mqtt/test_trigger.py @@ -1,5 +1,6 @@ """The tests for the MQTT automation.""" -from unittest.mock import ANY, patch + +from unittest.mock import ANY import pytest @@ -22,13 +23,6 @@ def calls(hass: HomeAssistant): return async_mock_service(hass, "test", "automation") -@pytest.fixture(autouse=True) -def no_platforms(): - """Skip platform setup to speed up tests.""" - with patch("homeassistant.components.mqtt.PLATFORMS", []): - yield - - @pytest.fixture(autouse=True) async def setup_comp(hass: HomeAssistant, mqtt_mock_entry): """Initialize components.""" diff --git a/tests/components/mqtt/test_update.py b/tests/components/mqtt/test_update.py index 6a80e18ff23..bb80a0c274f 100644 --- a/tests/components/mqtt/test_update.py +++ b/tests/components/mqtt/test_update.py @@ -1,4 +1,5 @@ """The tests for mqtt update component.""" + import json from unittest.mock import patch @@ -6,13 +7,7 @@ import pytest from homeassistant.components import mqtt, update from homeassistant.components.update import DOMAIN as UPDATE_DOMAIN, SERVICE_INSTALL -from homeassistant.const import ( - ATTR_ENTITY_ID, - STATE_OFF, - STATE_ON, - STATE_UNKNOWN, - Platform, -) +from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON, STATE_UNKNOWN from homeassistant.core import HomeAssistant from .test_common import ( @@ -57,13 +52,6 @@ DEFAULT_CONFIG = { } -@pytest.fixture(autouse=True) -def update_platform_only(): - """Only setup the update platform to speed up tests.""" - with patch("homeassistant.components.mqtt.PLATFORMS", [Platform.UPDATE]): - yield - - @pytest.mark.parametrize( ("hass_config", "device_class"), [ diff --git a/tests/components/mqtt/test_util.py b/tests/components/mqtt/test_util.py index 14153b44d87..b07dfc1f642 100644 --- a/tests/components/mqtt/test_util.py +++ b/tests/components/mqtt/test_util.py @@ -134,7 +134,6 @@ async def test_return_default_get_file_path( assert await hass.async_add_executor_job(_get_file_path, tempdir) -@patch("homeassistant.components.mqtt.PLATFORMS", []) async def test_waiting_for_client_not_loaded( hass: HomeAssistant, mqtt_client_mock: MqttMockPahoClient, @@ -160,10 +159,8 @@ async def test_waiting_for_client_not_loaded( unsubs.append(await mqtt.async_subscribe(hass, "test_topic", lambda msg: None)) # Simulate some integration waiting for the client to become available - hass.async_add_job(_async_just_in_time_subscribe) - hass.async_add_job(_async_just_in_time_subscribe) - hass.async_add_job(_async_just_in_time_subscribe) - hass.async_add_job(_async_just_in_time_subscribe) + for _ in range(4): + hass.async_create_task(_async_just_in_time_subscribe()) assert entry.state == ConfigEntryState.NOT_LOADED assert await hass.config_entries.async_setup(entry.entry_id) @@ -172,7 +169,6 @@ async def test_waiting_for_client_not_loaded( unsub() -@patch("homeassistant.components.mqtt.PLATFORMS", []) async def test_waiting_for_client_loaded( hass: HomeAssistant, mqtt_mock: MqttMockHAClient, @@ -212,7 +208,7 @@ async def test_waiting_for_client_entry_fails( async def _async_just_in_time_subscribe() -> Callable[[], None]: assert not await mqtt.async_wait_for_mqtt_client(hass) - hass.async_add_job(_async_just_in_time_subscribe) + hass.async_create_task(_async_just_in_time_subscribe()) assert entry.state == ConfigEntryState.NOT_LOADED with patch( "homeassistant.components.mqtt.async_setup_entry", @@ -240,7 +236,7 @@ async def test_waiting_for_client_setup_fails( async def _async_just_in_time_subscribe() -> Callable[[], None]: assert not await mqtt.async_wait_for_mqtt_client(hass) - hass.async_add_job(_async_just_in_time_subscribe) + hass.async_create_task(_async_just_in_time_subscribe()) assert entry.state == ConfigEntryState.NOT_LOADED # Simulate MQTT setup fails before the client would become available diff --git a/tests/components/mqtt/test_vacuum.py b/tests/components/mqtt/test_vacuum.py index f48b0b1b375..7563752b2d7 100644 --- a/tests/components/mqtt/test_vacuum.py +++ b/tests/components/mqtt/test_vacuum.py @@ -1,4 +1,5 @@ """The tests for the State vacuum Mqtt platform.""" + from copy import deepcopy import json from typing import Any @@ -31,7 +32,7 @@ from homeassistant.components.vacuum import ( STATE_CLEANING, STATE_DOCKED, ) -from homeassistant.const import CONF_NAME, ENTITY_MATCH_ALL, STATE_UNKNOWN, Platform +from homeassistant.const import CONF_NAME, ENTITY_MATCH_ALL, STATE_UNKNOWN from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError @@ -102,13 +103,6 @@ CONFIG_ALL_SERVICES = help_custom_config( ) -@pytest.fixture(autouse=True) -def vacuum_platform_only(): - """Only setup the vacuum platform to speed up tests.""" - with patch("homeassistant.components.mqtt.PLATFORMS", [Platform.VACUUM]): - yield - - @pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG]) async def test_default_supported_features( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator diff --git a/tests/components/mqtt/test_valve.py b/tests/components/mqtt/test_valve.py index 6a10a038da7..7fd9b10c005 100644 --- a/tests/components/mqtt/test_valve.py +++ b/tests/components/mqtt/test_valve.py @@ -1,4 +1,5 @@ """The tests for the MQTT valve platform.""" + from typing import Any from unittest.mock import patch @@ -26,7 +27,6 @@ from homeassistant.const import ( STATE_OPEN, STATE_OPENING, STATE_UNKNOWN, - Platform, ) from homeassistant.core import HomeAssistant @@ -86,13 +86,6 @@ DEFAULT_CONFIG_REPORTS_POSITION = { } -@pytest.fixture(autouse=True) -def valve_platform_only(): - """Only setup the valve platform to speed up tests.""" - with patch("homeassistant.components.mqtt.PLATFORMS", [Platform.VALVE]): - yield - - @pytest.mark.parametrize( "hass_config", [ diff --git a/tests/components/mqtt/test_water_heater.py b/tests/components/mqtt/test_water_heater.py index 61f430b34f8..ee0aa1c0949 100644 --- a/tests/components/mqtt/test_water_heater.py +++ b/tests/components/mqtt/test_water_heater.py @@ -1,4 +1,5 @@ """The tests for the mqtt water heater component.""" + import copy import json from typing import Any @@ -24,7 +25,7 @@ from homeassistant.components.water_heater import ( STATE_PERFORMANCE, WaterHeaterEntityFeature, ) -from homeassistant.const import ATTR_TEMPERATURE, STATE_OFF, Platform, UnitOfTemperature +from homeassistant.const import ATTR_TEMPERATURE, STATE_OFF, UnitOfTemperature from homeassistant.core import HomeAssistant from homeassistant.util.unit_conversion import TemperatureConverter @@ -95,13 +96,6 @@ DEFAULT_CONFIG = { } -@pytest.fixture(autouse=True) -def water_heater_platform_only(): - """Only setup the water heater platform to speed up tests.""" - with patch("homeassistant.components.mqtt.PLATFORMS", [Platform.WATER_HEATER]): - yield - - @pytest.mark.parametrize("hass_config", [DEFAULT_CONFIG]) async def test_setup_params( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator diff --git a/tests/components/mqtt_eventstream/test_init.py b/tests/components/mqtt_eventstream/test_init.py index 5eabb2202aa..24b4a83c425 100644 --- a/tests/components/mqtt_eventstream/test_init.py +++ b/tests/components/mqtt_eventstream/test_init.py @@ -1,4 +1,5 @@ """The tests for the MQTT eventstream component.""" + import json from unittest.mock import ANY, patch @@ -103,11 +104,12 @@ async def test_state_changed_event_sends_message( event = {} event["event_type"] = EVENT_STATE_CHANGED new_state = { + "attributes": {}, + "entity_id": e_id, + "last_changed": now.isoformat(), + "last_reported": now.isoformat(), "last_updated": now.isoformat(), "state": "on", - "entity_id": e_id, - "attributes": {}, - "last_changed": now.isoformat(), } event["event_data"] = {"new_state": new_state, "entity_id": e_id} diff --git a/tests/components/mqtt_json/test_device_tracker.py b/tests/components/mqtt_json/test_device_tracker.py index 8423ccd8da2..f150f5c86c9 100644 --- a/tests/components/mqtt_json/test_device_tracker.py +++ b/tests/components/mqtt_json/test_device_tracker.py @@ -1,4 +1,5 @@ """The tests for the JSON MQTT device tracker platform.""" + from collections.abc import Generator import json import logging @@ -60,6 +61,8 @@ async def test_setup_fails_without_mqtt_being_setup( DT_DOMAIN, {DT_DOMAIN: {CONF_PLATFORM: "mqtt_json", "devices": {dev_id: topic}}}, ) + await hass.async_block_till_done() + assert "MQTT integration is not available" in caplog.text @@ -82,6 +85,7 @@ async def test_ensure_device_tracker_platform_validation(hass: HomeAssistant) -> DT_DOMAIN, {DT_DOMAIN: {CONF_PLATFORM: "mqtt_json", "devices": {dev_id: topic}}}, ) + await hass.async_block_till_done() assert mock_sp.call_count == 1 @@ -96,6 +100,7 @@ async def test_json_message(hass: HomeAssistant) -> None: DT_DOMAIN, {DT_DOMAIN: {CONF_PLATFORM: "mqtt_json", "devices": {dev_id: topic}}}, ) + await hass.async_block_till_done() async_fire_mqtt_message(hass, topic, location) await hass.async_block_till_done() state = hass.states.get("device_tracker.zanzito") @@ -116,6 +121,7 @@ async def test_non_json_message( DT_DOMAIN, {DT_DOMAIN: {CONF_PLATFORM: "mqtt_json", "devices": {dev_id: topic}}}, ) + await hass.async_block_till_done() caplog.set_level(logging.ERROR) caplog.clear() @@ -137,6 +143,7 @@ async def test_incomplete_message( DT_DOMAIN, {DT_DOMAIN: {CONF_PLATFORM: "mqtt_json", "devices": {dev_id: topic}}}, ) + await hass.async_block_till_done() caplog.set_level(logging.ERROR) caplog.clear() @@ -160,6 +167,8 @@ async def test_single_level_wildcard_topic(hass: HomeAssistant) -> None: DT_DOMAIN, {DT_DOMAIN: {CONF_PLATFORM: "mqtt_json", "devices": {dev_id: subscription}}}, ) + await hass.async_block_till_done() + async_fire_mqtt_message(hass, topic, location) await hass.async_block_till_done() state = hass.states.get("device_tracker.zanzito") @@ -179,6 +188,8 @@ async def test_multi_level_wildcard_topic(hass: HomeAssistant) -> None: DT_DOMAIN, {DT_DOMAIN: {CONF_PLATFORM: "mqtt_json", "devices": {dev_id: subscription}}}, ) + await hass.async_block_till_done() + async_fire_mqtt_message(hass, topic, location) await hass.async_block_till_done() state = hass.states.get("device_tracker.zanzito") @@ -199,6 +210,8 @@ async def test_single_level_wildcard_topic_not_matching(hass: HomeAssistant) -> DT_DOMAIN, {DT_DOMAIN: {CONF_PLATFORM: "mqtt_json", "devices": {dev_id: subscription}}}, ) + 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) is None @@ -217,6 +230,8 @@ async def test_multi_level_wildcard_topic_not_matching(hass: HomeAssistant) -> N DT_DOMAIN, {DT_DOMAIN: {CONF_PLATFORM: "mqtt_json", "devices": {dev_id: subscription}}}, ) + 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) is None diff --git a/tests/components/mqtt_room/test_sensor.py b/tests/components/mqtt_room/test_sensor.py index 822e028f4f6..bc1890f08fa 100644 --- a/tests/components/mqtt_room/test_sensor.py +++ b/tests/components/mqtt_room/test_sensor.py @@ -1,4 +1,5 @@ """The tests for the MQTT room presence sensor.""" + import datetime import json from unittest.mock import patch diff --git a/tests/components/mqtt_statestream/test_init.py b/tests/components/mqtt_statestream/test_init.py index c9e0334d9d9..9798477945c 100644 --- a/tests/components/mqtt_statestream/test_init.py +++ b/tests/components/mqtt_statestream/test_init.py @@ -1,4 +1,5 @@ """The tests for the MQTT statestream component.""" + from unittest.mock import ANY, call import pytest @@ -99,7 +100,7 @@ async def test_setup_and_stop_waits_for_ha( # We use xfail with this test because there is an unhandled exception # in a background task in this test. # The exception is raised by mqtt.async_publish. -@pytest.mark.xfail() +@pytest.mark.xfail async def test_startup_no_mqtt( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: diff --git a/tests/components/mullvad/test_config_flow.py b/tests/components/mullvad/test_config_flow.py index 46dccafa0a6..e1e6570fa67 100644 --- a/tests/components/mullvad/test_config_flow.py +++ b/tests/components/mullvad/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Mullvad config flow.""" + from unittest.mock import patch from mullvad_api import MullvadAPIError @@ -20,12 +21,15 @@ async def test_form_user(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.FORM assert not result["errors"] - with patch( - "homeassistant.components.mullvad.async_setup_entry", - return_value=True, - ) as mock_setup_entry, patch( - "homeassistant.components.mullvad.config_flow.MullvadAPI" - ) as mock_mullvad_api: + with ( + patch( + "homeassistant.components.mullvad.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + patch( + "homeassistant.components.mullvad.config_flow.MullvadAPI" + ) as mock_mullvad_api, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {}, diff --git a/tests/components/mutesync/test_config_flow.py b/tests/components/mutesync/test_config_flow.py index bc2c739f15a..f667671da74 100644 --- a/tests/components/mutesync/test_config_flow.py +++ b/tests/components/mutesync/test_config_flow.py @@ -1,4 +1,5 @@ """Test the mütesync config flow.""" + from unittest.mock import patch import aiohttp @@ -18,13 +19,16 @@ async def test_form(hass: HomeAssistant) -> None: assert result["type"] == "form" assert result["errors"] is None - with patch( - "mutesync.authenticate", - return_value="bla", - ), patch( - "homeassistant.components.mutesync.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "mutesync.authenticate", + return_value="bla", + ), + patch( + "homeassistant.components.mutesync.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { diff --git a/tests/components/my/test_init.py b/tests/components/my/test_init.py index 7e41ff970e5..0bfb2b5b452 100644 --- a/tests/components/my/test_init.py +++ b/tests/components/my/test_init.py @@ -1,4 +1,5 @@ """Test the my init.""" + from unittest import mock from homeassistant.components.my import URL_PATH diff --git a/tests/components/mysensors/conftest.py b/tests/components/mysensors/conftest.py index 6df50f04ae2..bcf852e1368 100644 --- a/tests/components/mysensors/conftest.py +++ b/tests/components/mysensors/conftest.py @@ -1,4 +1,5 @@ """Provide common mysensors fixtures.""" + from __future__ import annotations from collections.abc import AsyncGenerator, Callable, Generator @@ -54,14 +55,17 @@ async def serial_transport_fixture( is_serial_port: MagicMock, ) -> AsyncGenerator[dict[int, Sensor], None]: """Mock a serial transport.""" - with patch( - "mysensors.gateway_serial.AsyncTransport", autospec=True - ) as transport_class, patch("mysensors.task.OTAFirmware", autospec=True), patch( - "mysensors.task.load_fw", autospec=True - ), patch( - "mysensors.task.Persistence", - autospec=True, - ) as persistence_class: + with ( + patch( + "mysensors.gateway_serial.AsyncTransport", autospec=True + ) as transport_class, + patch("mysensors.task.OTAFirmware", autospec=True), + patch("mysensors.task.load_fw", autospec=True), + patch( + "mysensors.task.Persistence", + autospec=True, + ) as persistence_class, + ): persistence = persistence_class.return_value mock_gateway_features(persistence, transport_class, gateway_nodes) diff --git a/tests/components/mysensors/test_binary_sensor.py b/tests/components/mysensors/test_binary_sensor.py index a6dce9c78b9..cb63a08d8a6 100644 --- a/tests/components/mysensors/test_binary_sensor.py +++ b/tests/components/mysensors/test_binary_sensor.py @@ -1,4 +1,5 @@ """Provide tests for mysensors binary sensor platform.""" + from __future__ import annotations from collections.abc import Callable diff --git a/tests/components/mysensors/test_climate.py b/tests/components/mysensors/test_climate.py index 6c386af6fd6..959f92ff512 100644 --- a/tests/components/mysensors/test_climate.py +++ b/tests/components/mysensors/test_climate.py @@ -1,4 +1,5 @@ """Provide tests for mysensors climate platform.""" + from __future__ import annotations from collections.abc import Callable diff --git a/tests/components/mysensors/test_config_flow.py b/tests/components/mysensors/test_config_flow.py index bff13d1604f..f532d09c6bf 100644 --- a/tests/components/mysensors/test_config_flow.py +++ b/tests/components/mysensors/test_config_flow.py @@ -1,4 +1,5 @@ """Test the MySensors config flow.""" + from __future__ import annotations from typing import Any @@ -112,15 +113,20 @@ async def test_config_serial(hass: HomeAssistant) -> None: step = await get_form(hass, CONF_GATEWAY_TYPE_SERIAL, "gw_serial") flow_id = step["flow_id"] - with patch( # mock is_serial_port because otherwise the test will be platform dependent (/dev/ttyACMx vs COMx) - "homeassistant.components.mysensors.config_flow.is_serial_port", - return_value=True, - ), patch( - "homeassistant.components.mysensors.config_flow.try_connect", return_value=True - ), patch( - "homeassistant.components.mysensors.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( # mock is_serial_port because otherwise the test will be platform dependent (/dev/ttyACMx vs COMx) + "homeassistant.components.mysensors.config_flow.is_serial_port", + return_value=True, + ), + patch( + "homeassistant.components.mysensors.config_flow.try_connect", + return_value=True, + ), + patch( + "homeassistant.components.mysensors.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result = await hass.config_entries.flow.async_configure( flow_id, { @@ -149,12 +155,16 @@ async def test_config_tcp(hass: HomeAssistant) -> None: step = await get_form(hass, CONF_GATEWAY_TYPE_TCP, "gw_tcp") flow_id = step["flow_id"] - with patch( - "homeassistant.components.mysensors.config_flow.try_connect", return_value=True - ), patch( - "homeassistant.components.mysensors.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.mysensors.config_flow.try_connect", + return_value=True, + ), + patch( + "homeassistant.components.mysensors.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result = await hass.config_entries.flow.async_configure( flow_id, { @@ -183,12 +193,16 @@ async def test_fail_to_connect(hass: HomeAssistant) -> None: step = await get_form(hass, CONF_GATEWAY_TYPE_TCP, "gw_tcp") flow_id = step["flow_id"] - with patch( - "homeassistant.components.mysensors.config_flow.try_connect", return_value=False - ), patch( - "homeassistant.components.mysensors.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.mysensors.config_flow.try_connect", + return_value=False, + ), + patch( + "homeassistant.components.mysensors.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result = await hass.config_entries.flow.async_configure( flow_id, { @@ -340,15 +354,20 @@ async def test_config_invalid( step = await get_form(hass, gateway_type, expected_step_id) flow_id = step["flow_id"] - with patch( - "homeassistant.components.mysensors.config_flow.try_connect", return_value=True - ), patch( - "homeassistant.components.mysensors.gateway.socket.getaddrinfo", - side_effect=OSError, - ), patch( - "homeassistant.components.mysensors.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.mysensors.config_flow.try_connect", + return_value=True, + ), + patch( + "homeassistant.components.mysensors.gateway.socket.getaddrinfo", + side_effect=OSError, + ), + patch( + "homeassistant.components.mysensors.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result = await hass.config_entries.flow.async_configure( flow_id, user_input, @@ -652,11 +671,16 @@ async def test_duplicate( ) -> None: """Test duplicate detection.""" - with patch("sys.platform", "win32"), patch( - "homeassistant.components.mysensors.config_flow.try_connect", return_value=True - ), patch( - "homeassistant.components.mysensors.async_setup_entry", - return_value=True, + with ( + patch("sys.platform", "win32"), + patch( + "homeassistant.components.mysensors.config_flow.try_connect", + return_value=True, + ), + patch( + "homeassistant.components.mysensors.async_setup_entry", + return_value=True, + ), ): MockConfigEntry(domain=DOMAIN, data=first_input).add_to_hass(hass) diff --git a/tests/components/mysensors/test_cover.py b/tests/components/mysensors/test_cover.py index 7d0a098fc0a..e056bff80fa 100644 --- a/tests/components/mysensors/test_cover.py +++ b/tests/components/mysensors/test_cover.py @@ -1,4 +1,5 @@ """Provide tests for mysensors cover platform.""" + from __future__ import annotations from collections.abc import Callable diff --git a/tests/components/mysensors/test_device_tracker.py b/tests/components/mysensors/test_device_tracker.py index 4d6e638e665..a0838d980a1 100644 --- a/tests/components/mysensors/test_device_tracker.py +++ b/tests/components/mysensors/test_device_tracker.py @@ -1,4 +1,5 @@ """Provide tests for mysensors device tracker platform.""" + from __future__ import annotations from collections.abc import Callable diff --git a/tests/components/mysensors/test_gateway.py b/tests/components/mysensors/test_gateway.py index 81207307b6c..5a840f76bb2 100644 --- a/tests/components/mysensors/test_gateway.py +++ b/tests/components/mysensors/test_gateway.py @@ -1,4 +1,5 @@ """Test function in gateway.py.""" + from unittest.mock import patch import pytest diff --git a/tests/components/mysensors/test_init.py b/tests/components/mysensors/test_init.py index fd61e27a663..8c1eeb64b70 100644 --- a/tests/components/mysensors/test_init.py +++ b/tests/components/mysensors/test_init.py @@ -1,4 +1,5 @@ """Test function in __init__.py.""" + from __future__ import annotations from mysensors import BaseSyncGateway diff --git a/tests/components/mysensors/test_light.py b/tests/components/mysensors/test_light.py index 9696c6e622a..ccff7238d3f 100644 --- a/tests/components/mysensors/test_light.py +++ b/tests/components/mysensors/test_light.py @@ -1,4 +1,5 @@ """Provide tests for mysensors light platform.""" + from __future__ import annotations from collections.abc import Callable diff --git a/tests/components/mysensors/test_remote.py b/tests/components/mysensors/test_remote.py index 586e2e2d048..a6ff4963e82 100644 --- a/tests/components/mysensors/test_remote.py +++ b/tests/components/mysensors/test_remote.py @@ -1,4 +1,5 @@ """Provide tests for mysensors remote platform.""" + from __future__ import annotations from collections.abc import Callable diff --git a/tests/components/mysensors/test_sensor.py b/tests/components/mysensors/test_sensor.py index d80fddea9e3..9ebf71dd7b3 100644 --- a/tests/components/mysensors/test_sensor.py +++ b/tests/components/mysensors/test_sensor.py @@ -1,4 +1,5 @@ """Provide tests for mysensors sensor platform.""" + from __future__ import annotations from collections.abc import Callable diff --git a/tests/components/mysensors/test_switch.py b/tests/components/mysensors/test_switch.py index 49786768ff7..b67c280a10d 100644 --- a/tests/components/mysensors/test_switch.py +++ b/tests/components/mysensors/test_switch.py @@ -1,4 +1,5 @@ """Provide tests for mysensors switch platform.""" + from __future__ import annotations from collections.abc import Callable diff --git a/tests/components/mysensors/test_text.py b/tests/components/mysensors/test_text.py index 7490cfddfbf..9905286bb8a 100644 --- a/tests/components/mysensors/test_text.py +++ b/tests/components/mysensors/test_text.py @@ -1,4 +1,5 @@ """Provide tests for mysensors text platform.""" + from __future__ import annotations from collections.abc import Callable diff --git a/tests/components/mystrom/__init__.py b/tests/components/mystrom/__init__.py index acd520cebaa..ac6ac1d8c54 100644 --- a/tests/components/mystrom/__init__.py +++ b/tests/components/mystrom/__init__.py @@ -1,4 +1,5 @@ """Tests for the myStrom integration.""" + from typing import Any diff --git a/tests/components/mystrom/test_config_flow.py b/tests/components/mystrom/test_config_flow.py index 9459519de75..b64b4edf547 100644 --- a/tests/components/mystrom/test_config_flow.py +++ b/tests/components/mystrom/test_config_flow.py @@ -1,4 +1,5 @@ """Test the myStrom config flow.""" + from unittest.mock import AsyncMock, patch from pymystrom.exceptions import MyStromConnectionError diff --git a/tests/components/mystrom/test_init.py b/tests/components/mystrom/test_init.py index 4100a270e0a..0304a0eb270 100644 --- a/tests/components/mystrom/test_init.py +++ b/tests/components/mystrom/test_init.py @@ -1,4 +1,5 @@ """Test the myStrom init.""" + from unittest.mock import AsyncMock, PropertyMock, patch from pymystrom.exceptions import MyStromConnectionError @@ -25,16 +26,22 @@ async def init_integration( config_entry: MockConfigEntry, device_type: int, ) -> None: - """Inititialize integration for testing.""" - with patch( - "pymystrom.get_device_info", - side_effect=AsyncMock(return_value=get_default_device_response(device_type)), - ), patch( - "homeassistant.components.mystrom._get_mystrom_bulb", - return_value=MyStromBulbMock("6001940376EB", get_default_bulb_state()), - ), patch( - "homeassistant.components.mystrom._get_mystrom_switch", - return_value=MyStromSwitchMock(get_default_switch_state()), + """Initialize integration for testing.""" + with ( + patch( + "pymystrom.get_device_info", + side_effect=AsyncMock( + return_value=get_default_device_response(device_type) + ), + ), + patch( + "homeassistant.components.mystrom._get_mystrom_bulb", + return_value=MyStromBulbMock("6001940376EB", get_default_bulb_state()), + ), + patch( + "homeassistant.components.mystrom._get_mystrom_switch", + return_value=MyStromSwitchMock(get_default_switch_state()), + ), ): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -90,15 +97,18 @@ async def test_init_of_unknown_bulb( hass: HomeAssistant, config_entry: MockConfigEntry ) -> None: """Test the initialization of a unknown myStrom bulb.""" - with patch( - "pymystrom.get_device_info", - side_effect=AsyncMock(return_value={"type": 102, "mac": DEVICE_MAC}), - ), patch("pymystrom.bulb.MyStromBulb.get_state", return_value={}), patch( - "pymystrom.bulb.MyStromBulb.bulb_type", "new_type" - ), patch( - "pymystrom.bulb.MyStromBulb.mac", - new_callable=PropertyMock, - return_value=DEVICE_MAC, + with ( + patch( + "pymystrom.get_device_info", + side_effect=AsyncMock(return_value={"type": 102, "mac": DEVICE_MAC}), + ), + patch("pymystrom.bulb.MyStromBulb.get_state", return_value={}), + patch("pymystrom.bulb.MyStromBulb.bulb_type", "new_type"), + patch( + "pymystrom.bulb.MyStromBulb.mac", + new_callable=PropertyMock, + return_value=DEVICE_MAC, + ), ): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -124,11 +134,13 @@ async def test_init_cannot_connect_because_of_device_info( hass: HomeAssistant, config_entry: MockConfigEntry ) -> None: """Test error handling for failing get_device_info.""" - with patch( - "pymystrom.get_device_info", - side_effect=MyStromConnectionError(), - ), patch("pymystrom.switch.MyStromSwitch.get_state", return_value={}), patch( - "pymystrom.bulb.MyStromBulb.get_state", return_value={} + with ( + patch( + "pymystrom.get_device_info", + side_effect=MyStromConnectionError(), + ), + patch("pymystrom.switch.MyStromSwitch.get_state", return_value={}), + patch("pymystrom.bulb.MyStromBulb.get_state", return_value={}), ): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -140,13 +152,18 @@ async def test_init_cannot_connect_because_of_get_state( hass: HomeAssistant, config_entry: MockConfigEntry ) -> None: """Test error handling for failing get_state.""" - with patch( - "pymystrom.get_device_info", - side_effect=AsyncMock(return_value=get_default_device_response(101)), - ), patch( - "pymystrom.switch.MyStromSwitch.get_state", side_effect=MyStromConnectionError() - ), patch( - "pymystrom.bulb.MyStromBulb.get_state", side_effect=MyStromConnectionError() + with ( + patch( + "pymystrom.get_device_info", + side_effect=AsyncMock(return_value=get_default_device_response(101)), + ), + patch( + "pymystrom.switch.MyStromSwitch.get_state", + side_effect=MyStromConnectionError(), + ), + patch( + "pymystrom.bulb.MyStromBulb.get_state", side_effect=MyStromConnectionError() + ), ): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/mythicbeastsdns/test_init.py b/tests/components/mythicbeastsdns/test_init.py index 7efcf762df2..81d17d2bf77 100644 --- a/tests/components/mythicbeastsdns/test_init.py +++ b/tests/components/mythicbeastsdns/test_init.py @@ -1,4 +1,5 @@ """Test the Mythic Beasts DNS component.""" + import logging from unittest.mock import patch diff --git a/tests/components/myuplink/conftest.py b/tests/components/myuplink/conftest.py index c1937a8ce3c..e08dc4255be 100644 --- a/tests/components/myuplink/conftest.py +++ b/tests/components/myuplink/conftest.py @@ -1,10 +1,12 @@ """Test helpers for myuplink.""" + from collections.abc import AsyncGenerator, Generator import time from typing import Any from unittest.mock import MagicMock, patch from myuplink import Device, DevicePoint, System +import orjson import pytest from homeassistant.components.application_credentials import ( @@ -93,7 +95,7 @@ def load_systems_jv_file(load_systems_file: str) -> dict[str, Any]: @pytest.fixture(scope="session") def load_systems_file() -> str: """Load fixture file for systems.""" - return load_fixture("systems.json", DOMAIN) + return load_fixture("systems-2dev.json", DOMAIN) @pytest.fixture @@ -106,22 +108,22 @@ def system_fixture(load_systems_file: str) -> list[System]: # Fixture group for device points API endpoint. -@pytest.fixture(scope="session") +@pytest.fixture def load_device_points_file() -> str: """Load fixture file for device-points endpoint.""" - return load_fixture("device_points_nibe_f730.json", DOMAIN) + return "device_points_nibe_f730.json" @pytest.fixture -def load_device_points_jv_file(): +def load_device_points_jv_file(load_device_points_file) -> str: """Load fixture file for device_points.""" - return json_loads(load_device_points_file) + return load_fixture(load_device_points_file, DOMAIN) @pytest.fixture -def device_points_fixture(load_device_points_file: str) -> list[DevicePoint]: - """Fixture for devce_points.""" - data = json_loads(load_device_points_file) +def device_points_fixture(load_device_points_jv_file: str) -> list[DevicePoint]: + """Fixture for device_points.""" + data = orjson.loads(load_device_points_jv_file) return [DevicePoint(point_data) for point_data in data] @@ -129,7 +131,7 @@ def device_points_fixture(load_device_points_file: str) -> list[DevicePoint]: def mock_myuplink_client( load_device_file, device_fixture, - load_device_points_file, + load_device_points_jv_file, device_points_fixture, system_fixture, load_systems_jv_file, @@ -149,7 +151,7 @@ def mock_myuplink_client( client.async_get_device_json.return_value = load_device_file client.async_get_device_points.return_value = device_points_fixture - client.async_get_device_points_json.return_value = load_device_points_file + client.async_get_device_points_json.return_value = load_device_points_jv_file yield client diff --git a/tests/components/myuplink/const.py b/tests/components/myuplink/const.py index 6ba324db12a..6001cb151c0 100644 --- a/tests/components/myuplink/const.py +++ b/tests/components/myuplink/const.py @@ -1,3 +1,4 @@ """Constants for myuplink tests.""" + CLIENT_ID = "12345" CLIENT_SECRET = "67890" diff --git a/tests/components/myuplink/fixtures/device-2dev.json b/tests/components/myuplink/fixtures/device-2dev.json new file mode 100644 index 00000000000..96360f87ce7 --- /dev/null +++ b/tests/components/myuplink/fixtures/device-2dev.json @@ -0,0 +1,40 @@ +{ + "id": "robin-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff", + "connectionState": "Connected", + "firmware": { + "currentFwVersion": "9682R7", + "desiredFwVersion": "9682R7" + }, + "product": { + "serialNumber": "222222", + "name": "F730 CU 3x400V" + }, + "availableFeatures": { + "settings": true, + "reboot": true, + "forcesync": true, + "forceUpdate": false, + "requestUpdate": false, + "resetAlarm": true, + "triggerEvent": true, + "getMenu": false, + "getMenuChain": false, + "getGuideQuestion": false, + "sendHaystack": true, + "setSmartMode": false, + "setAidMode": true, + "getZones": false, + "processIntent": false, + "boostHotWater": true, + "boostVentilation": true, + "getScheduleConfig": false, + "getScheduleModes": false, + "getScheduleWeekly": false, + "getScheduleVacation": false, + "setScheduleModes": false, + "setScheduleWeekly": false, + "setScheduleOverride": false, + "setScheduleVacation": false, + "setVentilationMode": false + } +} diff --git a/tests/components/myuplink/fixtures/device_points_nibe_smo20.json b/tests/components/myuplink/fixtures/device_points_nibe_smo20.json new file mode 100644 index 00000000000..b64869c236c --- /dev/null +++ b/tests/components/myuplink/fixtures/device_points_nibe_smo20.json @@ -0,0 +1,13470 @@ +[ + { + "category": "SMO 20", + "parameterId": "30200", + "parameterName": "heat pump", + "parameterUnit": "(EB101)", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 1, + "strVal": "1(EB101)", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 1, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "40004", + "parameterName": "Current outd temp (BT1)", + "parameterUnit": "°C", + "writable": false, + "timestamp": "2024-03-15T09:35:17+00:00", + "value": 11.7, + "strVal": "11.7°C", + "smartHomeCategories": ["sh-outdoorTemp"], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "40014", + "parameterName": "Hot water char­ging (BT6)", + "parameterUnit": "°C", + "writable": false, + "timestamp": "2024-03-15T09:39:12+00:00", + "value": 55.5, + "strVal": "55.5°C", + "smartHomeCategories": ["sh-hwTemp"], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "40047", + "parameterName": "Supply line (BT61)", + "parameterUnit": "°C", + "writable": false, + "timestamp": "2024-02-14T08:36:01+00:00", + "value": -32768, + "strVal": "-32768°C", + "smartHomeCategories": ["sh-supplyTemp"], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "40048", + "parameterName": "Return line (BT62)", + "parameterUnit": "°C", + "writable": false, + "timestamp": "2024-02-14T08:36:01+00:00", + "value": -32768, + "strVal": "-32768°C", + "smartHomeCategories": ["sh-returnTemp"], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "40067", + "parameterName": "Average outdoor temp (BT1)", + "parameterUnit": "°C", + "writable": false, + "timestamp": "2024-03-15T09:27:45+00:00", + "value": 9.7, + "strVal": "9.7°C", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "40071", + "parameterName": "Exter­nal supply line (BT25)", + "parameterUnit": "°C", + "writable": false, + "timestamp": "2024-03-15T09:39:06+00:00", + "value": 31.3, + "strVal": "31.3°C", + "smartHomeCategories": ["sh-supplyTemp"], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "40145", + "parameterName": "Oil temp­erature (EP15-BT29)", + "parameterUnit": "°C", + "writable": false, + "timestamp": "2024-02-14T08:36:01+00:00", + "value": 0, + "strVal": "0°C", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "40146", + "parameterName": "Oil temp­erature (BT29)", + "parameterUnit": "°C", + "writable": false, + "timestamp": "2024-02-14T08:36:01+00:00", + "value": 0, + "strVal": "0°C", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "40152", + "parameterName": "Exter­nal return line (BT71)", + "parameterUnit": "°C", + "writable": false, + "timestamp": "2024-03-15T09:40:02+00:00", + "value": 30, + "strVal": "30°C", + "smartHomeCategories": ["sh-returnTemp"], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "40755", + "parameterName": "time factor:", + "parameterUnit": "hrs", + "writable": false, + "timestamp": "2024-03-08T14:17:41+00:00", + "value": 6, + "strVal": "6hrs", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "Slave 1 (EB101)", + "parameterId": "40782", + "parameterName": "Re­quested com­pressor freq (EB101)", + "parameterUnit": "Hz", + "writable": false, + "timestamp": "2024-03-15T09:31:33+00:00", + "value": 40, + "strVal": "40Hz", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "40940", + "parameterName": "current value", + "parameterUnit": "DM", + "writable": true, + "timestamp": "2024-03-15T09:30:01+00:00", + "value": -458, + "strVal": "-458DM", + "smartHomeCategories": [], + "minValue": -30000, + "maxValue": 30000, + "stepValue": 100, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "40940", + "parameterName": "current value", + "parameterUnit": "DM", + "writable": true, + "timestamp": "2024-03-15T09:30:01+00:00", + "value": -458, + "strVal": "-458DM", + "smartHomeCategories": [], + "minValue": -30000, + "maxValue": 30000, + "stepValue": 100, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "40940", + "parameterName": "Degree minutes", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-03-15T09:30:01+00:00", + "value": -458, + "strVal": "-458", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "40940", + "parameterName": "Degree minutes", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-03-15T09:30:01+00:00", + "value": -458, + "strVal": "-458", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "40940", + "parameterName": "current value", + "parameterUnit": "DM", + "writable": true, + "timestamp": "2024-03-15T09:30:01+00:00", + "value": -458.5, + "strVal": "-458.5DM", + "smartHomeCategories": [], + "minValue": -30000, + "maxValue": 30000, + "stepValue": 100, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "40940", + "parameterName": "current value", + "parameterUnit": "DM", + "writable": true, + "timestamp": "2024-03-15T09:30:01+00:00", + "value": -458.5, + "strVal": "-458.5DM", + "smartHomeCategories": [], + "minValue": -30000, + "maxValue": 30000, + "stepValue": 100, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "40940", + "parameterName": "Degree minutes", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-03-15T09:30:01+00:00", + "value": -458.5, + "strVal": "-458.5", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "40940", + "parameterName": "Degree minutes", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-03-15T09:30:01+00:00", + "value": -458.5, + "strVal": "-458.5", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "40940", + "parameterName": "current value", + "parameterUnit": "DM", + "writable": true, + "timestamp": "2024-03-15T09:30:01+00:00", + "value": -458, + "strVal": "-458DM", + "smartHomeCategories": [], + "minValue": -30000, + "maxValue": 30000, + "stepValue": 100, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "40940", + "parameterName": "current value", + "parameterUnit": "DM", + "writable": true, + "timestamp": "2024-03-15T09:30:01+00:00", + "value": -458, + "strVal": "-458DM", + "smartHomeCategories": [], + "minValue": -30000, + "maxValue": 30000, + "stepValue": 100, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "40940", + "parameterName": "Degree minutes", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-03-15T09:30:01+00:00", + "value": -458, + "strVal": "-458", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "40940", + "parameterName": "Degree minutes", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-03-15T09:30:01+00:00", + "value": -458, + "strVal": "-458", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "40940", + "parameterName": "current value", + "parameterUnit": "DM", + "writable": true, + "timestamp": "2024-03-15T09:30:01+00:00", + "value": -458.5, + "strVal": "-458.5DM", + "smartHomeCategories": [], + "minValue": -30000, + "maxValue": 30000, + "stepValue": 100, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "40940", + "parameterName": "current value", + "parameterUnit": "DM", + "writable": true, + "timestamp": "2024-03-15T09:30:01+00:00", + "value": -458.5, + "strVal": "-458.5DM", + "smartHomeCategories": [], + "minValue": -30000, + "maxValue": 30000, + "stepValue": 100, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "40940", + "parameterName": "Degree minutes", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-03-15T09:30:01+00:00", + "value": -458.5, + "strVal": "-458.5", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "40940", + "parameterName": "Degree minutes", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-03-15T09:30:01+00:00", + "value": -458.5, + "strVal": "-458.5", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "Slave 1 (EB101)", + "parameterId": "41002", + "parameterName": "Fan speed (EB101)", + "parameterUnit": "rpm", + "writable": false, + "timestamp": "2024-03-15T09:32:17+00:00", + "value": 319, + "strVal": "319rpm", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "Slave 1 (EB101)", + "parameterId": "41011", + "parameterName": "EEV-ssh-act (EB101)", + "parameterUnit": "°C", + "writable": false, + "timestamp": "2024-03-15T09:37:46+00:00", + "value": 10.2, + "strVal": "10.2°C", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "Slave 1 (EB101)", + "parameterId": "41017", + "parameterName": "EEV posi­tion (EB101)", + "parameterUnit": "", + "writable": false, + "timestamp": "2024-03-15T09:34:19+00:00", + "value": 96, + "strVal": "96", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "Slave 1 (EB101)", + "parameterId": "41019", + "parameterName": "EVI-ssh-act (EB101)", + "parameterUnit": "°C", + "writable": false, + "timestamp": "2024-02-14T08:36:01+00:00", + "value": 0, + "strVal": "0°C", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "Slave 1 (EB101)", + "parameterId": "41025", + "parameterName": "EVI posi­tion (EB101)", + "parameterUnit": "", + "writable": false, + "timestamp": "2024-02-14T08:36:01+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "Slave 1 (EB101)", + "parameterId": "41162", + "parameterName": "Low pres­sure (EB101-EP14-BP8)", + "parameterUnit": "°C", + "writable": false, + "timestamp": "2024-03-15T09:35:13+00:00", + "value": 4.2, + "strVal": "4.2°C", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "Slave 1 (EB101)", + "parameterId": "41163", + "parameterName": "High pres­sure (EB101-EP14-BP9)", + "parameterUnit": "°C", + "writable": false, + "timestamp": "2024-03-15T09:38:36+00:00", + "value": 64.2, + "strVal": "64.2°C", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "Slave 1 (EB101)", + "parameterId": "41164", + "parameterName": "Injec­tion (EB101-BT81)", + "parameterUnit": "°C", + "writable": false, + "timestamp": "2024-02-14T08:36:01+00:00", + "value": -32768, + "strVal": "-32768°C", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "Slave 1 (EB101)", + "parameterId": "41167", + "parameterName": "Evap­orator (EB101-BT84)", + "parameterUnit": "°C", + "writable": false, + "timestamp": "2024-03-15T09:38:43+00:00", + "value": 8.5, + "strVal": "8.5°C", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "41929", + "parameterName": "Mode (Smart Price Adaption)", + "parameterUnit": "", + "writable": false, + "timestamp": "2024-03-15T09:00:01+00:00", + "value": 1, + "strVal": "Low", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "0", + "text": "Unknown", + "icon": "" + }, + { + "value": "1", + "text": "Low", + "icon": "" + }, + { + "value": "2", + "text": "Normal", + "icon": "" + }, + { + "value": "3", + "text": "High", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "43009", + "parameterName": "Calcu­lated supply climate system 1", + "parameterUnit": "°C", + "writable": false, + "timestamp": "2024-03-15T09:35:44+00:00", + "value": 42.1, + "strVal": "42.1°C", + "smartHomeCategories": ["sh-supplyTemp"], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "43081", + "parameterName": "total operating time:", + "parameterUnit": "hrs", + "writable": false, + "timestamp": "2024-03-12T02:37:40+00:00", + "value": 36.9, + "strVal": "36.9hrs", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "43081", + "parameterName": "total operating time:", + "parameterUnit": "hrs", + "writable": false, + "timestamp": "2024-03-12T02:37:40+00:00", + "value": 36.9, + "strVal": "36.9hrs", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "43081", + "parameterName": "Time factor add heat", + "parameterUnit": "", + "writable": false, + "timestamp": "2024-03-12T02:37:40+00:00", + "value": 36.9, + "strVal": "36.9", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "43081", + "parameterName": "Time factor add heat", + "parameterUnit": "", + "writable": false, + "timestamp": "2024-03-12T02:37:40+00:00", + "value": 36.9, + "strVal": "36.9", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "43081", + "parameterName": "total operating time:", + "parameterUnit": "hrs", + "writable": false, + "timestamp": "2024-03-12T02:37:40+00:00", + "value": 37, + "strVal": "37hrs", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "43081", + "parameterName": "total operating time:", + "parameterUnit": "hrs", + "writable": false, + "timestamp": "2024-03-12T02:37:40+00:00", + "value": 37, + "strVal": "37hrs", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "43081", + "parameterName": "Time factor add heat", + "parameterUnit": "", + "writable": false, + "timestamp": "2024-03-12T02:37:40+00:00", + "value": 37, + "strVal": "37", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "43081", + "parameterName": "Time factor add heat", + "parameterUnit": "", + "writable": false, + "timestamp": "2024-03-12T02:37:40+00:00", + "value": 37, + "strVal": "37", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "43081", + "parameterName": "total operating time:", + "parameterUnit": "hrs", + "writable": false, + "timestamp": "2024-03-12T02:37:40+00:00", + "value": 36.9, + "strVal": "36.9hrs", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "43081", + "parameterName": "total operating time:", + "parameterUnit": "hrs", + "writable": false, + "timestamp": "2024-03-12T02:37:40+00:00", + "value": 36.9, + "strVal": "36.9hrs", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "43081", + "parameterName": "Time factor add heat", + "parameterUnit": "", + "writable": false, + "timestamp": "2024-03-12T02:37:40+00:00", + "value": 36.9, + "strVal": "36.9", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "43081", + "parameterName": "Time factor add heat", + "parameterUnit": "", + "writable": false, + "timestamp": "2024-03-12T02:37:40+00:00", + "value": 36.9, + "strVal": "36.9", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "43081", + "parameterName": "total operating time:", + "parameterUnit": "hrs", + "writable": false, + "timestamp": "2024-03-12T02:37:40+00:00", + "value": 37, + "strVal": "37hrs", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "43081", + "parameterName": "total operating time:", + "parameterUnit": "hrs", + "writable": false, + "timestamp": "2024-03-12T02:37:40+00:00", + "value": 37, + "strVal": "37hrs", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "43081", + "parameterName": "Time factor add heat", + "parameterUnit": "", + "writable": false, + "timestamp": "2024-03-12T02:37:40+00:00", + "value": 37, + "strVal": "37", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "43081", + "parameterName": "Time factor add heat", + "parameterUnit": "", + "writable": false, + "timestamp": "2024-03-12T02:37:40+00:00", + "value": 37, + "strVal": "37", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "43109", + "parameterName": "Current hot water mode", + "parameterUnit": "", + "writable": false, + "timestamp": "2024-03-15T09:29:25+00:00", + "value": 2, + "strVal": "2", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "43161", + "parameterName": "Extern. adjust­ment climate system 1", + "parameterUnit": "", + "writable": false, + "timestamp": "2024-02-14T08:36:01+00:00", + "value": 0, + "strVal": "Off", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "0", + "text": "Off", + "icon": "" + }, + { + "value": "1", + "text": "On", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "Slave 1 (EB101)", + "parameterId": "44014", + "parameterName": "Version (EB101)", + "parameterUnit": "", + "writable": false, + "timestamp": "2024-02-14T08:36:01+00:00", + "value": 10880, + "strVal": "10880", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "Slave 1 (EB101)", + "parameterId": "44032", + "parameterName": "Slave (EB101)", + "parameterUnit": "", + "writable": false, + "timestamp": "2024-02-14T08:36:01+00:00", + "value": 17, + "strVal": "17", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "Slave 1 (EB101)", + "parameterId": "44055", + "parameterName": "Return line (EB101-BT3)", + "parameterUnit": "°C", + "writable": false, + "timestamp": "2024-03-15T09:39:52+00:00", + "value": 56.7, + "strVal": "56.7°C", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "Slave 1 (EB101)", + "parameterId": "44058", + "parameterName": "Supply line (EB101-BT12)", + "parameterUnit": "°C", + "writable": false, + "timestamp": "2024-03-15T09:40:33+00:00", + "value": 64.8, + "strVal": "64.8°C", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "Slave 1 (EB101)", + "parameterId": "44059", + "parameterName": "Dis­charge (EB101-BT14)", + "parameterUnit": "°C", + "writable": false, + "timestamp": "2024-03-15T09:40:31+00:00", + "value": 81.5, + "strVal": "81.5°C", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "Slave 1 (EB101)", + "parameterId": "44060", + "parameterName": "Liquid line (EB101-BT15)", + "parameterUnit": "°C", + "writable": false, + "timestamp": "2024-03-15T09:39:31+00:00", + "value": 56.2, + "strVal": "56.2°C", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "Slave 1 (EB101)", + "parameterId": "44061", + "parameterName": "Suction gas (EB101-BT17)", + "parameterUnit": "°C", + "writable": false, + "timestamp": "2024-03-15T09:40:48+00:00", + "value": 15.2, + "strVal": "15.2°C", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "Slave 1 (EB101)", + "parameterId": "44064", + "parameterName": "Status com­pressor (EB101)", + "parameterUnit": "", + "writable": false, + "timestamp": "2024-03-15T09:20:02+00:00", + "value": 60, + "strVal": "Operating", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "20", + "text": "Off", + "icon": "" + }, + { + "value": "40", + "text": "Starting", + "icon": "" + }, + { + "value": "60", + "text": "Operating", + "icon": "" + }, + { + "value": "100", + "text": "Stopping", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "Slave 1 (EB101)", + "parameterId": "44069", + "parameterName": "No. of starts (EB101-EP14)", + "parameterUnit": "", + "writable": false, + "timestamp": "2024-03-15T09:20:02+00:00", + "value": 1859, + "strVal": "1859", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "Slave 1 (EB101)", + "parameterId": "44069", + "parameterName": "No. of starts (EB101-EP14)", + "parameterUnit": "", + "writable": false, + "timestamp": "2024-03-15T09:20:02+00:00", + "value": 1859, + "strVal": "1859", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "44069", + "parameterName": "number of starts:", + "parameterUnit": "", + "writable": false, + "timestamp": "2024-03-15T09:20:02+00:00", + "value": 1859, + "strVal": "1859", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "44069", + "parameterName": "number of starts:", + "parameterUnit": "", + "writable": false, + "timestamp": "2024-03-15T09:20:02+00:00", + "value": 1859, + "strVal": "1859", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "Slave 1 (EB101)", + "parameterId": "44069", + "parameterName": "No. of starts (EB101-EP14)", + "parameterUnit": "", + "writable": false, + "timestamp": "2024-03-15T09:20:02+00:00", + "value": 1859, + "strVal": "1859", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "Slave 1 (EB101)", + "parameterId": "44069", + "parameterName": "No. of starts (EB101-EP14)", + "parameterUnit": "", + "writable": false, + "timestamp": "2024-03-15T09:20:02+00:00", + "value": 1859, + "strVal": "1859", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "44069", + "parameterName": "number of starts:", + "parameterUnit": "", + "writable": false, + "timestamp": "2024-03-15T09:20:02+00:00", + "value": 1859, + "strVal": "1859", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "44069", + "parameterName": "number of starts:", + "parameterUnit": "", + "writable": false, + "timestamp": "2024-03-15T09:20:02+00:00", + "value": 1859, + "strVal": "1859", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "Slave 1 (EB101)", + "parameterId": "44069", + "parameterName": "No. of starts (EB101-EP14)", + "parameterUnit": "", + "writable": false, + "timestamp": "2024-03-15T09:20:02+00:00", + "value": 1859, + "strVal": "1859", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "Slave 1 (EB101)", + "parameterId": "44069", + "parameterName": "No. of starts (EB101-EP14)", + "parameterUnit": "", + "writable": false, + "timestamp": "2024-03-15T09:20:02+00:00", + "value": 1859, + "strVal": "1859", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "44069", + "parameterName": "number of starts:", + "parameterUnit": "", + "writable": false, + "timestamp": "2024-03-15T09:20:02+00:00", + "value": 1859, + "strVal": "1859", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "44069", + "parameterName": "number of starts:", + "parameterUnit": "", + "writable": false, + "timestamp": "2024-03-15T09:20:02+00:00", + "value": 1859, + "strVal": "1859", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "Slave 1 (EB101)", + "parameterId": "44069", + "parameterName": "No. of starts (EB101-EP14)", + "parameterUnit": "", + "writable": false, + "timestamp": "2024-03-15T09:20:02+00:00", + "value": 1859, + "strVal": "1859", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "Slave 1 (EB101)", + "parameterId": "44069", + "parameterName": "No. of starts (EB101-EP14)", + "parameterUnit": "", + "writable": false, + "timestamp": "2024-03-15T09:20:02+00:00", + "value": 1859, + "strVal": "1859", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "44069", + "parameterName": "number of starts:", + "parameterUnit": "", + "writable": false, + "timestamp": "2024-03-15T09:20:02+00:00", + "value": 1859, + "strVal": "1859", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "44069", + "parameterName": "number of starts:", + "parameterUnit": "", + "writable": false, + "timestamp": "2024-03-15T09:20:02+00:00", + "value": 1859, + "strVal": "1859", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "Slave 1 (EB101)", + "parameterId": "44071", + "parameterName": "Oper. time (EB101-EP14)", + "parameterUnit": "h", + "writable": false, + "timestamp": "2024-03-15T07:55:32+00:00", + "value": 2694, + "strVal": "2694h", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "Slave 1 (EB101)", + "parameterId": "44071", + "parameterName": "Oper. time (EB101-EP14)", + "parameterUnit": "h", + "writable": false, + "timestamp": "2024-03-15T07:55:32+00:00", + "value": 2694, + "strVal": "2694h", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "44071", + "parameterName": "total operating time:", + "parameterUnit": "hrs", + "writable": false, + "timestamp": "2024-03-15T07:55:32+00:00", + "value": 2694, + "strVal": "2694hrs", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "44071", + "parameterName": "total operating time:", + "parameterUnit": "hrs", + "writable": false, + "timestamp": "2024-03-15T07:55:32+00:00", + "value": 2694, + "strVal": "2694hrs", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "Slave 1 (EB101)", + "parameterId": "44071", + "parameterName": "Oper. time (EB101-EP14)", + "parameterUnit": "h", + "writable": false, + "timestamp": "2024-03-15T07:55:32+00:00", + "value": 2694, + "strVal": "2694h", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "Slave 1 (EB101)", + "parameterId": "44071", + "parameterName": "Oper. time (EB101-EP14)", + "parameterUnit": "h", + "writable": false, + "timestamp": "2024-03-15T07:55:32+00:00", + "value": 2694, + "strVal": "2694h", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "44071", + "parameterName": "total operating time:", + "parameterUnit": "hrs", + "writable": false, + "timestamp": "2024-03-15T07:55:32+00:00", + "value": 2694, + "strVal": "2694hrs", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "44071", + "parameterName": "total operating time:", + "parameterUnit": "hrs", + "writable": false, + "timestamp": "2024-03-15T07:55:32+00:00", + "value": 2694, + "strVal": "2694hrs", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "Slave 1 (EB101)", + "parameterId": "44071", + "parameterName": "Oper. time (EB101-EP14)", + "parameterUnit": "h", + "writable": false, + "timestamp": "2024-03-15T07:55:32+00:00", + "value": 2694, + "strVal": "2694h", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "Slave 1 (EB101)", + "parameterId": "44071", + "parameterName": "Oper. time (EB101-EP14)", + "parameterUnit": "h", + "writable": false, + "timestamp": "2024-03-15T07:55:32+00:00", + "value": 2694, + "strVal": "2694h", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "44071", + "parameterName": "total operating time:", + "parameterUnit": "hrs", + "writable": false, + "timestamp": "2024-03-15T07:55:32+00:00", + "value": 2694, + "strVal": "2694hrs", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "44071", + "parameterName": "total operating time:", + "parameterUnit": "hrs", + "writable": false, + "timestamp": "2024-03-15T07:55:32+00:00", + "value": 2694, + "strVal": "2694hrs", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "Slave 1 (EB101)", + "parameterId": "44071", + "parameterName": "Oper. time (EB101-EP14)", + "parameterUnit": "h", + "writable": false, + "timestamp": "2024-03-15T07:55:32+00:00", + "value": 2694, + "strVal": "2694h", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "Slave 1 (EB101)", + "parameterId": "44071", + "parameterName": "Oper. time (EB101-EP14)", + "parameterUnit": "h", + "writable": false, + "timestamp": "2024-03-15T07:55:32+00:00", + "value": 2694, + "strVal": "2694h", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "44071", + "parameterName": "total operating time:", + "parameterUnit": "hrs", + "writable": false, + "timestamp": "2024-03-15T07:55:32+00:00", + "value": 2694, + "strVal": "2694hrs", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "44071", + "parameterName": "total operating time:", + "parameterUnit": "hrs", + "writable": false, + "timestamp": "2024-03-15T07:55:32+00:00", + "value": 2694, + "strVal": "2694hrs", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "Slave 1 (EB101)", + "parameterId": "44073", + "parameterName": "Oper. time hot water (EB101-EP14)", + "parameterUnit": "h", + "writable": false, + "timestamp": "2024-03-15T03:30:27+00:00", + "value": 114, + "strVal": "114h", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "Slave 1 (EB101)", + "parameterId": "44073", + "parameterName": "Oper. time hot water (EB101-EP14)", + "parameterUnit": "h", + "writable": false, + "timestamp": "2024-03-15T03:30:27+00:00", + "value": 114, + "strVal": "114h", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "44073", + "parameterName": "- of which hot water:", + "parameterUnit": "hrs", + "writable": false, + "timestamp": "2024-03-15T03:30:27+00:00", + "value": 114, + "strVal": "114hrs", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "44073", + "parameterName": "- of which hot water:", + "parameterUnit": "hrs", + "writable": false, + "timestamp": "2024-03-15T03:30:27+00:00", + "value": 114, + "strVal": "114hrs", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "Slave 1 (EB101)", + "parameterId": "44073", + "parameterName": "Oper. time hot water (EB101-EP14)", + "parameterUnit": "h", + "writable": false, + "timestamp": "2024-03-15T03:30:27+00:00", + "value": 114, + "strVal": "114h", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "Slave 1 (EB101)", + "parameterId": "44073", + "parameterName": "Oper. time hot water (EB101-EP14)", + "parameterUnit": "h", + "writable": false, + "timestamp": "2024-03-15T03:30:27+00:00", + "value": 114, + "strVal": "114h", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "44073", + "parameterName": "- of which hot water:", + "parameterUnit": "hrs", + "writable": false, + "timestamp": "2024-03-15T03:30:27+00:00", + "value": 114, + "strVal": "114hrs", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "44073", + "parameterName": "- of which hot water:", + "parameterUnit": "hrs", + "writable": false, + "timestamp": "2024-03-15T03:30:27+00:00", + "value": 114, + "strVal": "114hrs", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "Slave 1 (EB101)", + "parameterId": "44073", + "parameterName": "Oper. time hot water (EB101-EP14)", + "parameterUnit": "h", + "writable": false, + "timestamp": "2024-03-15T03:30:27+00:00", + "value": 114, + "strVal": "114h", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "Slave 1 (EB101)", + "parameterId": "44073", + "parameterName": "Oper. time hot water (EB101-EP14)", + "parameterUnit": "h", + "writable": false, + "timestamp": "2024-03-15T03:30:27+00:00", + "value": 114, + "strVal": "114h", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "44073", + "parameterName": "- of which hot water:", + "parameterUnit": "hrs", + "writable": false, + "timestamp": "2024-03-15T03:30:27+00:00", + "value": 114, + "strVal": "114hrs", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "44073", + "parameterName": "- of which hot water:", + "parameterUnit": "hrs", + "writable": false, + "timestamp": "2024-03-15T03:30:27+00:00", + "value": 114, + "strVal": "114hrs", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "Slave 1 (EB101)", + "parameterId": "44073", + "parameterName": "Oper. time hot water (EB101-EP14)", + "parameterUnit": "h", + "writable": false, + "timestamp": "2024-03-15T03:30:27+00:00", + "value": 114, + "strVal": "114h", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "Slave 1 (EB101)", + "parameterId": "44073", + "parameterName": "Oper. time hot water (EB101-EP14)", + "parameterUnit": "h", + "writable": false, + "timestamp": "2024-03-15T03:30:27+00:00", + "value": 114, + "strVal": "114h", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "44073", + "parameterName": "- of which hot water:", + "parameterUnit": "hrs", + "writable": false, + "timestamp": "2024-03-15T03:30:27+00:00", + "value": 114, + "strVal": "114hrs", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "44073", + "parameterName": "- of which hot water:", + "parameterUnit": "hrs", + "writable": false, + "timestamp": "2024-03-15T03:30:27+00:00", + "value": 114, + "strVal": "114hrs", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "Slave 1 (EB101)", + "parameterId": "44362", + "parameterName": "Outd temp­erature (EB101-BT28)", + "parameterUnit": "°C", + "writable": false, + "timestamp": "2024-03-15T09:19:30+00:00", + "value": 11.6, + "strVal": "11.6°C", + "smartHomeCategories": ["sh-outdoorTemp"], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "Slave 1 (EB101)", + "parameterId": "44363", + "parameterName": "Evap­orator (EB101-BT16)", + "parameterUnit": "°C", + "writable": false, + "timestamp": "2024-03-15T09:39:44+00:00", + "value": 5, + "strVal": "5°C", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "Slave 1 (EB101)", + "parameterId": "44396", + "parameterName": "Heating medium pump speed (GP1)", + "parameterUnit": "%", + "writable": false, + "timestamp": "2024-03-15T09:38:24+00:00", + "value": 11, + "strVal": "11%", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "Slave 1 (EB101)", + "parameterId": "44700", + "parameterName": "Low pres­sure (EB101-BP8)", + "parameterUnit": "bar", + "writable": false, + "timestamp": "2024-03-15T09:23:55+00:00", + "value": 4, + "strVal": "4bar", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "Slave 1 (EB101)", + "parameterId": "44701", + "parameterName": "Current com­pressor fre­quency (EB101)", + "parameterUnit": "Hz", + "writable": false, + "timestamp": "2024-03-15T09:32:20+00:00", + "value": 40, + "strVal": "40Hz", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "Slave 1 (EB101)", + "parameterId": "44702", + "parameterName": "Prot. mode, com­pressor (EB101)", + "parameterUnit": "", + "writable": false, + "timestamp": "2024-03-12T02:32:25+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "Slave 1 (EB101)", + "parameterId": "44703", + "parameterName": "Defrost­ing (EB101-EP14)", + "parameterUnit": "", + "writable": false, + "timestamp": "2024-03-15T06:10:59+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "44896", + "parameterName": "Heating offset (Smart Price Adaption)", + "parameterUnit": "", + "writable": false, + "timestamp": "2024-03-15T09:00:01+00:00", + "value": 0.2, + "strVal": "0.2", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "44898", + "parameterName": "Pool offset (Smart Price Adaption)", + "parameterUnit": "", + "writable": false, + "timestamp": "2024-02-14T08:36:01+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "44899", + "parameterName": "Cooling offset (Smart Price Adaption)", + "parameterUnit": "", + "writable": false, + "timestamp": "2024-02-14T08:36:01+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "44908", + "parameterName": "Status (Smart Price Adaption)", + "parameterUnit": "", + "writable": false, + "timestamp": "2024-02-14T08:39:28+00:00", + "value": 30, + "strVal": "30", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "45862", + "parameterName": "Continuous fan de-icing", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:03+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 1, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47007", + "parameterName": "heating curve", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:03+00:00", + "value": 9, + "strVal": "9", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 15, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47011", + "parameterName": "Heating offset climate system 1", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-03-15T09:29:34+00:00", + "value": 5, + "strVal": "5", + "smartHomeCategories": [], + "minValue": -10, + "maxValue": 10, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47015", + "parameterName": "climate system", + "parameterUnit": "°C", + "writable": true, + "timestamp": "2023-11-22T07:03:44+00:00", + "value": 25, + "strVal": "25°C", + "smartHomeCategories": [], + "minValue": 50, + "maxValue": 700, + "stepValue": 10, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47020", + "parameterName": "flow line temp. at 30 °C", + "parameterUnit": "°C", + "writable": true, + "timestamp": "2023-11-22T07:03:44+00:00", + "value": 15, + "strVal": "15°C", + "smartHomeCategories": [], + "minValue": 5, + "maxValue": 80, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47021", + "parameterName": "flow line temp. at 20 °C", + "parameterUnit": "°C", + "writable": true, + "timestamp": "2023-11-22T07:03:44+00:00", + "value": 15, + "strVal": "15°C", + "smartHomeCategories": [], + "minValue": 5, + "maxValue": 80, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47022", + "parameterName": "flow line temp. at 10 °C", + "parameterUnit": "°C", + "writable": true, + "timestamp": "2023-11-22T07:03:44+00:00", + "value": 26, + "strVal": "26°C", + "smartHomeCategories": [], + "minValue": 5, + "maxValue": 80, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47023", + "parameterName": "flow line temp. at 0 °C", + "parameterUnit": "°C", + "writable": true, + "timestamp": "2023-11-22T07:03:44+00:00", + "value": 32, + "strVal": "32°C", + "smartHomeCategories": [], + "minValue": 5, + "maxValue": 80, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47024", + "parameterName": "flow line temp. at -10 °C", + "parameterUnit": "°C", + "writable": true, + "timestamp": "2023-11-22T07:03:44+00:00", + "value": 35, + "strVal": "35°C", + "smartHomeCategories": [], + "minValue": 5, + "maxValue": 80, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47025", + "parameterName": "flow line temp. at -20 °C", + "parameterUnit": "°C", + "writable": true, + "timestamp": "2023-11-22T07:03:44+00:00", + "value": 40, + "strVal": "40°C", + "smartHomeCategories": [], + "minValue": 5, + "maxValue": 80, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47026", + "parameterName": "flow line temp. at -30 °C", + "parameterUnit": "°C", + "writable": true, + "timestamp": "2023-11-22T07:03:44+00:00", + "value": 45, + "strVal": "45°C", + "smartHomeCategories": [], + "minValue": 5, + "maxValue": 80, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47027", + "parameterName": "outdoor temp. point", + "parameterUnit": "°C", + "writable": true, + "timestamp": "2024-02-14T08:36:03+00:00", + "value": 0, + "strVal": "0°C", + "smartHomeCategories": [], + "minValue": -40, + "maxValue": 30, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47028", + "parameterName": "change in curve", + "parameterUnit": "°C", + "writable": true, + "timestamp": "2024-03-07T09:37:01+00:00", + "value": 0, + "strVal": "0°C", + "smartHomeCategories": [], + "minValue": -10, + "maxValue": 10, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47032", + "parameterName": "climate system", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:44+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": -10, + "maxValue": 10, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47043", + "parameterName": "start temp. lux", + "parameterUnit": "°C", + "writable": true, + "timestamp": "2024-03-09T16:50:12+00:00", + "value": 56, + "strVal": "56°C", + "smartHomeCategories": [], + "minValue": 50, + "maxValue": 700, + "stepValue": 10, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47044", + "parameterName": "start temp. normal", + "parameterUnit": "°C", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 46, + "strVal": "46°C", + "smartHomeCategories": [], + "minValue": 50, + "maxValue": 600, + "stepValue": 10, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47045", + "parameterName": "start temp. economy", + "parameterUnit": "°C", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 43, + "strVal": "43°C", + "smartHomeCategories": [], + "minValue": 50, + "maxValue": 550, + "stepValue": 10, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47046", + "parameterName": "stop temp. per. increase", + "parameterUnit": "°C", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 55, + "strVal": "55°C", + "smartHomeCategories": [], + "minValue": 550, + "maxValue": 700, + "stepValue": 10, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47047", + "parameterName": "stop temp. lux", + "parameterUnit": "°C", + "writable": true, + "timestamp": "2024-03-09T16:50:32+00:00", + "value": 59, + "strVal": "59°C", + "smartHomeCategories": [], + "minValue": 50, + "maxValue": 700, + "stepValue": 10, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47048", + "parameterName": "stop temp. normal", + "parameterUnit": "°C", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 50, + "strVal": "50°C", + "smartHomeCategories": [], + "minValue": 50, + "maxValue": 650, + "stepValue": 10, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47049", + "parameterName": "stop temp. economy", + "parameterUnit": "°C", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 47, + "strVal": "47°C", + "smartHomeCategories": [], + "minValue": 50, + "maxValue": 600, + "stepValue": 10, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47050", + "parameterName": "activated", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:44+00:00", + "value": 1, + "strVal": "1", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 1, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47051", + "parameterName": "period", + "parameterUnit": "days", + "writable": true, + "timestamp": "2023-11-22T07:03:44+00:00", + "value": 14, + "strVal": "14days", + "smartHomeCategories": [], + "minValue": 1, + "maxValue": 90, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47054", + "parameterName": "operating time", + "parameterUnit": "min", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 10, + "strVal": "10min", + "smartHomeCategories": [], + "minValue": 1, + "maxValue": 60, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47055", + "parameterName": "downtime", + "parameterUnit": "min", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 20, + "strVal": "20min", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 60, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47056", + "parameterName": "length of period 3", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47057", + "parameterName": "length of period 2", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47058", + "parameterName": "length of period 1", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 20700, + "strVal": "20700", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47059", + "parameterName": "length of period 3", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47060", + "parameterName": "length of period 2", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47061", + "parameterName": "length of period 1", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47137", + "parameterName": "op. mode", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:06+00:00", + "value": 0, + "strVal": "auto", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "0", + "text": "auto", + "icon": "" + }, + { + "value": "1", + "text": "manual", + "icon": "" + }, + { + "value": "2", + "text": "add. heat only", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47206", + "parameterName": "start compressor", + "parameterUnit": "DM", + "writable": true, + "timestamp": "2024-02-14T08:36:03+00:00", + "value": -60, + "strVal": "-60DM", + "smartHomeCategories": [], + "minValue": -1000, + "maxValue": -30, + "stepValue": 10, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47209", + "parameterName": "diff. between additional steps", + "parameterUnit": "DM", + "writable": true, + "timestamp": "2024-02-14T08:36:03+00:00", + "value": 100, + "strVal": "100DM", + "smartHomeCategories": [], + "minValue": 10, + "maxValue": 1000, + "stepValue": 10, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47214", + "parameterName": "fuse size", + "parameterUnit": "A", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 16, + "strVal": "16A", + "smartHomeCategories": [], + "minValue": 1, + "maxValue": 400, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47266", + "parameterName": "speed 4", + "parameterUnit": "%", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 90, + "strVal": "90%", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 100, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47267", + "parameterName": "speed 3", + "parameterUnit": "%", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 70, + "strVal": "70%", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 100, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47268", + "parameterName": "speed 2", + "parameterUnit": "%", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 25, + "strVal": "25%", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 100, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47269", + "parameterName": "speed 1", + "parameterUnit": "%", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 0, + "strVal": "0%", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 100, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47270", + "parameterName": "normal", + "parameterUnit": "%", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 60, + "strVal": "60%", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 100, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47275", + "parameterName": "months btwn filter alarms", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 3, + "strVal": "3", + "smartHomeCategories": [], + "minValue": 1, + "maxValue": 24, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47276", + "parameterName": "activated", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 1, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47277", + "parameterName": "length of period 7", + "parameterUnit": "days", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 2, + "strVal": "2days", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 30, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47278", + "parameterName": "length of period 6", + "parameterUnit": "days", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 2, + "strVal": "2days", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 30, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47279", + "parameterName": "length of period 5", + "parameterUnit": "days", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 2, + "strVal": "2days", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 30, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47280", + "parameterName": "length of period 4", + "parameterUnit": "days", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 3, + "strVal": "3days", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 30, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47281", + "parameterName": "length of period 3", + "parameterUnit": "days", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 2, + "strVal": "2days", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 30, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47282", + "parameterName": "length of period 2", + "parameterUnit": "days", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 2, + "strVal": "2days", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 30, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47283", + "parameterName": "length of period 1", + "parameterUnit": "days", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 2, + "strVal": "2days", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 30, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47284", + "parameterName": "temp. period 7", + "parameterUnit": "°C", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 20, + "strVal": "20°C", + "smartHomeCategories": [], + "minValue": 15, + "maxValue": 70, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47285", + "parameterName": "temp. period 6", + "parameterUnit": "°C", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 30, + "strVal": "30°C", + "smartHomeCategories": [], + "minValue": 15, + "maxValue": 70, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47286", + "parameterName": "temp. period 5", + "parameterUnit": "°C", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 40, + "strVal": "40°C", + "smartHomeCategories": [], + "minValue": 15, + "maxValue": 70, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47287", + "parameterName": "temp. period 4", + "parameterUnit": "°C", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 45, + "strVal": "45°C", + "smartHomeCategories": [], + "minValue": 15, + "maxValue": 70, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47288", + "parameterName": "temp. period 3", + "parameterUnit": "°C", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 40, + "strVal": "40°C", + "smartHomeCategories": [], + "minValue": 15, + "maxValue": 70, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47289", + "parameterName": "temp. period 2", + "parameterUnit": "°C", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 30, + "strVal": "30°C", + "smartHomeCategories": [], + "minValue": 15, + "maxValue": 70, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47290", + "parameterName": "temp. period 1", + "parameterUnit": "°C", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 20, + "strVal": "20°C", + "smartHomeCategories": [], + "minValue": 15, + "maxValue": 70, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47347", + "parameterName": "max. tank temperature", + "parameterUnit": "°C", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 95, + "strVal": "95°C", + "smartHomeCategories": [], + "minValue": 5, + "maxValue": 110, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47348", + "parameterName": "max. solar collector temp.", + "parameterUnit": "°C", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 125, + "strVal": "125°C", + "smartHomeCategories": [], + "minValue": 80, + "maxValue": 200, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47375", + "parameterName": "stop heating", + "parameterUnit": "°C", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 18, + "strVal": "18°C", + "smartHomeCategories": [], + "minValue": -200, + "maxValue": 400, + "stepValue": 10, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47376", + "parameterName": "stop additional heat", + "parameterUnit": "°C", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 5, + "strVal": "5°C", + "smartHomeCategories": [], + "minValue": -250, + "maxValue": 400, + "stepValue": 10, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47377", + "parameterName": "filtering time", + "parameterUnit": "hrs", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 6, + "strVal": "6hrs", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 48, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47378", + "parameterName": "max diff compressor", + "parameterUnit": "°C", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 6, + "strVal": "6°C", + "smartHomeCategories": [], + "minValue": 10, + "maxValue": 250, + "stepValue": 10, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47379", + "parameterName": "max diff addition", + "parameterUnit": "°C", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 3, + "strVal": "3°C", + "smartHomeCategories": [], + "minValue": 10, + "maxValue": 240, + "stepValue": 10, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47388", + "parameterName": "decrease room temp", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 1, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47389", + "parameterName": "deactivate hot water", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 1, + "strVal": "1", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 1, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47394", + "parameterName": "control room sensor syst", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:03+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 1, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47413", + "parameterName": "hot water", + "parameterUnit": "%", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 70, + "strVal": "70%", + "smartHomeCategories": [], + "minValue": 1, + "maxValue": 100, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47414", + "parameterName": "heating", + "parameterUnit": "%", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 70, + "strVal": "70%", + "smartHomeCategories": [], + "minValue": 1, + "maxValue": 100, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47416", + "parameterName": "economy", + "parameterUnit": "%", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 30, + "strVal": "30%", + "smartHomeCategories": [], + "minValue": 1, + "maxValue": 100, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47544", + "parameterName": "start delta-T", + "parameterUnit": "°C", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 8, + "strVal": "8°C", + "smartHomeCategories": [], + "minValue": 1, + "maxValue": 40, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47545", + "parameterName": "stop delta-T", + "parameterUnit": "°C", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 4, + "strVal": "4°C", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 40, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47550", + "parameterName": "solar panel cooling", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 1, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47613", + "parameterName": "max step", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:03+00:00", + "value": 2, + "strVal": "2", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 7, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47614", + "parameterName": "binary stepping", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 1, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47635", + "parameterName": "activated", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47637", + "parameterName": "Start Sunday", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47638", + "parameterName": "Start Saturday", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47639", + "parameterName": "Start Friday", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47640", + "parameterName": "Start Thursday", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47641", + "parameterName": "Start Wednesday", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47642", + "parameterName": "Start Tuesday", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47643", + "parameterName": "Start Monday", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47644", + "parameterName": "Start all days", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47645", + "parameterName": "Finish Sunday", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47646", + "parameterName": "Finish Saturday", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47647", + "parameterName": "Finish Friday", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47648", + "parameterName": "Finish Thursday", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47649", + "parameterName": "Finish Wednesday", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47650", + "parameterName": "Finish Tuesday", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47651", + "parameterName": "Finish Monday", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47652", + "parameterName": "End all days", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47653", + "parameterName": "sun", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "economy", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "-1", + "text": "off", + "icon": "" + }, + { + "value": "0", + "text": "economy", + "icon": "" + }, + { + "value": "1", + "text": "normal", + "icon": "" + }, + { + "value": "2", + "text": "luxury", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47654", + "parameterName": "sat", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "economy", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "-1", + "text": "off", + "icon": "" + }, + { + "value": "0", + "text": "economy", + "icon": "" + }, + { + "value": "1", + "text": "normal", + "icon": "" + }, + { + "value": "2", + "text": "luxury", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47655", + "parameterName": "fri", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "economy", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "-1", + "text": "off", + "icon": "" + }, + { + "value": "0", + "text": "economy", + "icon": "" + }, + { + "value": "1", + "text": "normal", + "icon": "" + }, + { + "value": "2", + "text": "luxury", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47656", + "parameterName": "thur", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "economy", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "-1", + "text": "off", + "icon": "" + }, + { + "value": "0", + "text": "economy", + "icon": "" + }, + { + "value": "1", + "text": "normal", + "icon": "" + }, + { + "value": "2", + "text": "luxury", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47657", + "parameterName": "wed", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "economy", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "-1", + "text": "off", + "icon": "" + }, + { + "value": "0", + "text": "economy", + "icon": "" + }, + { + "value": "1", + "text": "normal", + "icon": "" + }, + { + "value": "2", + "text": "luxury", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47658", + "parameterName": "tues", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "economy", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "-1", + "text": "off", + "icon": "" + }, + { + "value": "0", + "text": "economy", + "icon": "" + }, + { + "value": "1", + "text": "normal", + "icon": "" + }, + { + "value": "2", + "text": "luxury", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47659", + "parameterName": "mon", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "economy", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "-1", + "text": "off", + "icon": "" + }, + { + "value": "0", + "text": "economy", + "icon": "" + }, + { + "value": "1", + "text": "normal", + "icon": "" + }, + { + "value": "2", + "text": "luxury", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47660", + "parameterName": "all", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "economy", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "-1", + "text": "off", + "icon": "" + }, + { + "value": "0", + "text": "economy", + "icon": "" + }, + { + "value": "1", + "text": "normal", + "icon": "" + }, + { + "value": "2", + "text": "luxury", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47669", + "parameterName": "activated", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47671", + "parameterName": "Start Sunday", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47672", + "parameterName": "Start Saturday", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47673", + "parameterName": "Start Friday", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47674", + "parameterName": "Start Thursday", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47675", + "parameterName": "Start Wednesday", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47676", + "parameterName": "Start Tuesday", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47677", + "parameterName": "Start Monday", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47678", + "parameterName": "Start all days", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47679", + "parameterName": "Finish Sunday", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47680", + "parameterName": "Finish Saturday", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47681", + "parameterName": "Finish Friday", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47682", + "parameterName": "Finish Thursday", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47683", + "parameterName": "Finish Wednesday", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47684", + "parameterName": "Finish Tuesday", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47685", + "parameterName": "Finish Monday", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47686", + "parameterName": "End all days", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47687", + "parameterName": "sun", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "economy", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "-1", + "text": "off", + "icon": "" + }, + { + "value": "0", + "text": "economy", + "icon": "" + }, + { + "value": "1", + "text": "normal", + "icon": "" + }, + { + "value": "2", + "text": "luxury", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47688", + "parameterName": "sat", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "economy", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "-1", + "text": "off", + "icon": "" + }, + { + "value": "0", + "text": "economy", + "icon": "" + }, + { + "value": "1", + "text": "normal", + "icon": "" + }, + { + "value": "2", + "text": "luxury", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47689", + "parameterName": "fri", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "economy", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "-1", + "text": "off", + "icon": "" + }, + { + "value": "0", + "text": "economy", + "icon": "" + }, + { + "value": "1", + "text": "normal", + "icon": "" + }, + { + "value": "2", + "text": "luxury", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47690", + "parameterName": "thur", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "economy", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "-1", + "text": "off", + "icon": "" + }, + { + "value": "0", + "text": "economy", + "icon": "" + }, + { + "value": "1", + "text": "normal", + "icon": "" + }, + { + "value": "2", + "text": "luxury", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47691", + "parameterName": "wed", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "economy", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "-1", + "text": "off", + "icon": "" + }, + { + "value": "0", + "text": "economy", + "icon": "" + }, + { + "value": "1", + "text": "normal", + "icon": "" + }, + { + "value": "2", + "text": "luxury", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47692", + "parameterName": "tues", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "economy", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "-1", + "text": "off", + "icon": "" + }, + { + "value": "0", + "text": "economy", + "icon": "" + }, + { + "value": "1", + "text": "normal", + "icon": "" + }, + { + "value": "2", + "text": "luxury", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47693", + "parameterName": "mon", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "economy", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "-1", + "text": "off", + "icon": "" + }, + { + "value": "0", + "text": "economy", + "icon": "" + }, + { + "value": "1", + "text": "normal", + "icon": "" + }, + { + "value": "2", + "text": "luxury", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47694", + "parameterName": "all", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "economy", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "-1", + "text": "off", + "icon": "" + }, + { + "value": "0", + "text": "economy", + "icon": "" + }, + { + "value": "1", + "text": "normal", + "icon": "" + }, + { + "value": "2", + "text": "luxury", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47703", + "parameterName": "activated", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47705", + "parameterName": "Start Sunday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47706", + "parameterName": "Start Saturday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47707", + "parameterName": "Start Friday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47708", + "parameterName": "Start Thursday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47709", + "parameterName": "Start Wednesday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47710", + "parameterName": "Start Tuesday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47711", + "parameterName": "Start Monday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47712", + "parameterName": "Start all days", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47713", + "parameterName": "Finish Sunday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47714", + "parameterName": "Finish Saturday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47715", + "parameterName": "Finish Friday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47716", + "parameterName": "Finish Thursday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47717", + "parameterName": "Finish Wednesday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47718", + "parameterName": "Finish Tuesday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47719", + "parameterName": "Finish Monday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47720", + "parameterName": "End all days", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47721", + "parameterName": "sun", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 1, + "strVal": "speed 1", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "1", + "text": "speed 1", + "icon": "" + }, + { + "value": "2", + "text": "speed 2", + "icon": "" + }, + { + "value": "3", + "text": "speed 3", + "icon": "" + }, + { + "value": "4", + "text": "speed 4", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47722", + "parameterName": "sat", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 1, + "strVal": "speed 1", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "1", + "text": "speed 1", + "icon": "" + }, + { + "value": "2", + "text": "speed 2", + "icon": "" + }, + { + "value": "3", + "text": "speed 3", + "icon": "" + }, + { + "value": "4", + "text": "speed 4", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47723", + "parameterName": "fri", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 1, + "strVal": "speed 1", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "1", + "text": "speed 1", + "icon": "" + }, + { + "value": "2", + "text": "speed 2", + "icon": "" + }, + { + "value": "3", + "text": "speed 3", + "icon": "" + }, + { + "value": "4", + "text": "speed 4", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47724", + "parameterName": "thur", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 1, + "strVal": "speed 1", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "1", + "text": "speed 1", + "icon": "" + }, + { + "value": "2", + "text": "speed 2", + "icon": "" + }, + { + "value": "3", + "text": "speed 3", + "icon": "" + }, + { + "value": "4", + "text": "speed 4", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47725", + "parameterName": "wed", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 1, + "strVal": "speed 1", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "1", + "text": "speed 1", + "icon": "" + }, + { + "value": "2", + "text": "speed 2", + "icon": "" + }, + { + "value": "3", + "text": "speed 3", + "icon": "" + }, + { + "value": "4", + "text": "speed 4", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47726", + "parameterName": "tues", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 1, + "strVal": "speed 1", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "1", + "text": "speed 1", + "icon": "" + }, + { + "value": "2", + "text": "speed 2", + "icon": "" + }, + { + "value": "3", + "text": "speed 3", + "icon": "" + }, + { + "value": "4", + "text": "speed 4", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47727", + "parameterName": "mon", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 1, + "strVal": "speed 1", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "1", + "text": "speed 1", + "icon": "" + }, + { + "value": "2", + "text": "speed 2", + "icon": "" + }, + { + "value": "3", + "text": "speed 3", + "icon": "" + }, + { + "value": "4", + "text": "speed 4", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47728", + "parameterName": "all", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "1", + "text": "speed 1", + "icon": "" + }, + { + "value": "2", + "text": "speed 2", + "icon": "" + }, + { + "value": "3", + "text": "speed 3", + "icon": "" + }, + { + "value": "4", + "text": "speed 4", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47737", + "parameterName": "activated", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47739", + "parameterName": "Start Sunday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47740", + "parameterName": "Start Saturday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47741", + "parameterName": "Start Friday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:47+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47742", + "parameterName": "Start Thursday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:47+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47743", + "parameterName": "Start Wednesday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:47+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47744", + "parameterName": "Start Tuesday", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47745", + "parameterName": "Start Monday", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47746", + "parameterName": "Start all days", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47747", + "parameterName": "Finish Sunday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47748", + "parameterName": "Finish Saturday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47749", + "parameterName": "Finish Friday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:47+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47750", + "parameterName": "Finish Thursday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:47+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47751", + "parameterName": "Finish Wednesday", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47752", + "parameterName": "Finish Tuesday", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47753", + "parameterName": "Finish Monday", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47754", + "parameterName": "End all days", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47755", + "parameterName": "sun", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 1, + "strVal": "speed 1", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "1", + "text": "speed 1", + "icon": "" + }, + { + "value": "2", + "text": "speed 2", + "icon": "" + }, + { + "value": "3", + "text": "speed 3", + "icon": "" + }, + { + "value": "4", + "text": "speed 4", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47756", + "parameterName": "sat", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:47+00:00", + "value": 1, + "strVal": "speed 1", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "1", + "text": "speed 1", + "icon": "" + }, + { + "value": "2", + "text": "speed 2", + "icon": "" + }, + { + "value": "3", + "text": "speed 3", + "icon": "" + }, + { + "value": "4", + "text": "speed 4", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47757", + "parameterName": "fri", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:47+00:00", + "value": 1, + "strVal": "speed 1", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "1", + "text": "speed 1", + "icon": "" + }, + { + "value": "2", + "text": "speed 2", + "icon": "" + }, + { + "value": "3", + "text": "speed 3", + "icon": "" + }, + { + "value": "4", + "text": "speed 4", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47758", + "parameterName": "thur", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:47+00:00", + "value": 1, + "strVal": "speed 1", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "1", + "text": "speed 1", + "icon": "" + }, + { + "value": "2", + "text": "speed 2", + "icon": "" + }, + { + "value": "3", + "text": "speed 3", + "icon": "" + }, + { + "value": "4", + "text": "speed 4", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47759", + "parameterName": "wed", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 1, + "strVal": "speed 1", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "1", + "text": "speed 1", + "icon": "" + }, + { + "value": "2", + "text": "speed 2", + "icon": "" + }, + { + "value": "3", + "text": "speed 3", + "icon": "" + }, + { + "value": "4", + "text": "speed 4", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47760", + "parameterName": "tues", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 1, + "strVal": "speed 1", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "1", + "text": "speed 1", + "icon": "" + }, + { + "value": "2", + "text": "speed 2", + "icon": "" + }, + { + "value": "3", + "text": "speed 3", + "icon": "" + }, + { + "value": "4", + "text": "speed 4", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47761", + "parameterName": "mon", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 1, + "strVal": "speed 1", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "1", + "text": "speed 1", + "icon": "" + }, + { + "value": "2", + "text": "speed 2", + "icon": "" + }, + { + "value": "3", + "text": "speed 3", + "icon": "" + }, + { + "value": "4", + "text": "speed 4", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47762", + "parameterName": "all", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "1", + "text": "speed 1", + "icon": "" + }, + { + "value": "2", + "text": "speed 2", + "icon": "" + }, + { + "value": "3", + "text": "speed 3", + "icon": "" + }, + { + "value": "4", + "text": "speed 4", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47771", + "parameterName": "activated", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47772", + "parameterName": "system", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 1, + "strVal": "1", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "1", + "text": "1", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47773", + "parameterName": "Start Sunday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47774", + "parameterName": "Start Saturday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47775", + "parameterName": "Start Friday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47776", + "parameterName": "Start Thursday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47777", + "parameterName": "Start Wednesday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47778", + "parameterName": "Start Tuesday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47779", + "parameterName": "Start Monday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47780", + "parameterName": "Start all days", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47781", + "parameterName": "Finish Sunday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47782", + "parameterName": "Finish Saturday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47783", + "parameterName": "Finish Friday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47784", + "parameterName": "Finish Thursday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47785", + "parameterName": "Finish Wednesday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47786", + "parameterName": "Finish Tuesday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47787", + "parameterName": "Finish Monday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47788", + "parameterName": "End all days", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47789", + "parameterName": "sun", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": -10, + "maxValue": 10, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47790", + "parameterName": "sat", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": -10, + "maxValue": 10, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47791", + "parameterName": "fri", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": -10, + "maxValue": 10, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47792", + "parameterName": "thur", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": -10, + "maxValue": 10, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47793", + "parameterName": "wed", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": -10, + "maxValue": 10, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47794", + "parameterName": "tues", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": -10, + "maxValue": 10, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47795", + "parameterName": "mon", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": -10, + "maxValue": 10, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47796", + "parameterName": "all", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": -10, + "maxValue": 10, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47805", + "parameterName": "activated", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47806", + "parameterName": "system", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 1, + "strVal": "1", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "1", + "text": "1", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47807", + "parameterName": "Start Sunday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47808", + "parameterName": "Start Saturday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47809", + "parameterName": "Start Friday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47810", + "parameterName": "Start Thursday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47811", + "parameterName": "Start Wednesday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47812", + "parameterName": "Start Tuesday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47813", + "parameterName": "Start Monday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47814", + "parameterName": "Start all days", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47815", + "parameterName": "Finish Sunday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47816", + "parameterName": "Finish Saturday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47817", + "parameterName": "Finish Friday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47818", + "parameterName": "Finish Thursday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47819", + "parameterName": "Finish Wednesday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47820", + "parameterName": "Finish Tuesday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47821", + "parameterName": "Finish Monday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47822", + "parameterName": "End all days", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47823", + "parameterName": "sun", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": -10, + "maxValue": 10, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47824", + "parameterName": "sat", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": -10, + "maxValue": 10, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47825", + "parameterName": "fri", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": -10, + "maxValue": 10, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47826", + "parameterName": "thur", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": -10, + "maxValue": 10, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47827", + "parameterName": "wed", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": -10, + "maxValue": 10, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47828", + "parameterName": "tues", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": -10, + "maxValue": 10, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47829", + "parameterName": "mon", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": -10, + "maxValue": 10, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47830", + "parameterName": "all", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": -10, + "maxValue": 10, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47839", + "parameterName": "activated", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47840", + "parameterName": "system", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 1, + "strVal": "1", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "1", + "text": "1", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47841", + "parameterName": "Start Sunday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47842", + "parameterName": "Start Saturday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47843", + "parameterName": "Start Friday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47844", + "parameterName": "Start Thursday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47845", + "parameterName": "Start Wednesday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47846", + "parameterName": "Start Tuesday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47847", + "parameterName": "Start Monday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47848", + "parameterName": "Start all days", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47849", + "parameterName": "Finish Sunday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47850", + "parameterName": "Finish Saturday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47851", + "parameterName": "Finish Friday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47852", + "parameterName": "Finish Thursday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47853", + "parameterName": "Finish Wednesday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47854", + "parameterName": "Finish Tuesday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47855", + "parameterName": "Finish Monday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47856", + "parameterName": "End all days", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47857", + "parameterName": "sun", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": -10, + "maxValue": 10, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47858", + "parameterName": "sat", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": -10, + "maxValue": 10, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47859", + "parameterName": "fri", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": -10, + "maxValue": 10, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47860", + "parameterName": "thur", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": -10, + "maxValue": 10, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47861", + "parameterName": "wed", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": -10, + "maxValue": 10, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47862", + "parameterName": "tues", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": -10, + "maxValue": 10, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47863", + "parameterName": "mon", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": -10, + "maxValue": 10, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47864", + "parameterName": "all", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": -10, + "maxValue": 10, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47907", + "parameterName": "activated", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47909", + "parameterName": "Start Sunday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47910", + "parameterName": "Start Saturday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47911", + "parameterName": "Start Friday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47912", + "parameterName": "Start Thursday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47913", + "parameterName": "Start Wednesday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47914", + "parameterName": "Start Tuesday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47915", + "parameterName": "Start Monday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47916", + "parameterName": "Start all days", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47917", + "parameterName": "Finish Sunday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47918", + "parameterName": "Finish Saturday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47919", + "parameterName": "Finish Friday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47920", + "parameterName": "Finish Thursday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47921", + "parameterName": "Finish Wednesday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47922", + "parameterName": "Finish Tuesday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47923", + "parameterName": "Finish Monday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47924", + "parameterName": "End all days", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47925", + "parameterName": "sun", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "off", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "0", + "text": "off", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47926", + "parameterName": "sat", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "off", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "0", + "text": "off", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47927", + "parameterName": "fri", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "off", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "0", + "text": "off", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47928", + "parameterName": "thur", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "off", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "0", + "text": "off", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47929", + "parameterName": "wed", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "off", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "0", + "text": "off", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47930", + "parameterName": "tues", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "off", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "0", + "text": "off", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47931", + "parameterName": "mon", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "off", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "0", + "text": "off", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47932", + "parameterName": "all", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "off", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "0", + "text": "off", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47941", + "parameterName": "activated", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47943", + "parameterName": "Start Sunday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47944", + "parameterName": "Start Saturday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47945", + "parameterName": "Start Friday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47946", + "parameterName": "Start Thursday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47947", + "parameterName": "Start Wednesday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47948", + "parameterName": "Start Tuesday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47949", + "parameterName": "Start Monday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47950", + "parameterName": "Start all days", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47951", + "parameterName": "Finish Sunday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47952", + "parameterName": "Finish Saturday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47953", + "parameterName": "Finish Friday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47954", + "parameterName": "Finish Thursday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47955", + "parameterName": "Finish Wednesday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47956", + "parameterName": "Finish Tuesday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47957", + "parameterName": "Finish Monday", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47958", + "parameterName": "End all days", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47959", + "parameterName": "sun", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "off", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "0", + "text": "off", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47960", + "parameterName": "sat", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "off", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "0", + "text": "off", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47961", + "parameterName": "fri", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "off", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "0", + "text": "off", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47962", + "parameterName": "thur", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "off", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "0", + "text": "off", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47963", + "parameterName": "wed", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "off", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "0", + "text": "off", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47964", + "parameterName": "tues", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "off", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "0", + "text": "off", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47965", + "parameterName": "mon", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "off", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "0", + "text": "off", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47966", + "parameterName": "all", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "off", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "0", + "text": "off", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47975", + "parameterName": "activated", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47977", + "parameterName": "Start Sunday", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47978", + "parameterName": "Start Saturday", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47979", + "parameterName": "Start Friday", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47980", + "parameterName": "Start Thursday", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47981", + "parameterName": "Start Wednesday", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47982", + "parameterName": "Start Tuesday", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47983", + "parameterName": "Start Monday", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47984", + "parameterName": "Start all days", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47985", + "parameterName": "Finish Sunday", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47986", + "parameterName": "Finish Saturday", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47987", + "parameterName": "Finish Friday", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47988", + "parameterName": "Finish Thursday", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47989", + "parameterName": "Finish Wednesday", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47990", + "parameterName": "Finish Tuesday", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47991", + "parameterName": "Finish Monday", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47992", + "parameterName": "End all days", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47993", + "parameterName": "sun", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 2, + "strVal": "Add. heat", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "1", + "text": "Com­pressor", + "icon": "" + }, + { + "value": "2", + "text": "Add. heat", + "icon": "" + }, + { + "value": "3", + "text": "Com­pressor and Addi­tional Heat", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47994", + "parameterName": "sat", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 2, + "strVal": "Add. heat", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "1", + "text": "Com­pressor", + "icon": "" + }, + { + "value": "2", + "text": "Add. heat", + "icon": "" + }, + { + "value": "3", + "text": "Com­pressor and Addi­tional Heat", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47995", + "parameterName": "fri", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 2, + "strVal": "Add. heat", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "1", + "text": "Com­pressor", + "icon": "" + }, + { + "value": "2", + "text": "Add. heat", + "icon": "" + }, + { + "value": "3", + "text": "Com­pressor and Addi­tional Heat", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47996", + "parameterName": "thur", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 2, + "strVal": "Add. heat", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "1", + "text": "Com­pressor", + "icon": "" + }, + { + "value": "2", + "text": "Add. heat", + "icon": "" + }, + { + "value": "3", + "text": "Com­pressor and Addi­tional Heat", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47997", + "parameterName": "wed", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 2, + "strVal": "Add. heat", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "1", + "text": "Com­pressor", + "icon": "" + }, + { + "value": "2", + "text": "Add. heat", + "icon": "" + }, + { + "value": "3", + "text": "Com­pressor and Addi­tional Heat", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47998", + "parameterName": "tues", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 2, + "strVal": "Add. heat", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "1", + "text": "Com­pressor", + "icon": "" + }, + { + "value": "2", + "text": "Add. heat", + "icon": "" + }, + { + "value": "3", + "text": "Com­pressor and Addi­tional Heat", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "47999", + "parameterName": "mon", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 2, + "strVal": "Add. heat", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "1", + "text": "Com­pressor", + "icon": "" + }, + { + "value": "2", + "text": "Add. heat", + "icon": "" + }, + { + "value": "3", + "text": "Com­pressor and Addi­tional Heat", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48000", + "parameterName": "all", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "1", + "text": "Com­pressor", + "icon": "" + }, + { + "value": "2", + "text": "Add. heat", + "icon": "" + }, + { + "value": "3", + "text": "Com­pressor and Addi­tional Heat", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48009", + "parameterName": "activated", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48011", + "parameterName": "Start Sunday", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48012", + "parameterName": "Start Saturday", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48013", + "parameterName": "Start Friday", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48014", + "parameterName": "Start Thursday", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48015", + "parameterName": "Start Wednesday", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48016", + "parameterName": "Start Tuesday", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48017", + "parameterName": "Start Monday", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48018", + "parameterName": "Start all days", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48019", + "parameterName": "Finish Sunday", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48020", + "parameterName": "Finish Saturday", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48021", + "parameterName": "Finish Friday", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48022", + "parameterName": "Finish Thursday", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48023", + "parameterName": "Finish Wednesday", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48024", + "parameterName": "Finish Tuesday", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48025", + "parameterName": "Finish Monday", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48026", + "parameterName": "End all days", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48027", + "parameterName": "sun", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 2, + "strVal": "Add. heat", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "1", + "text": "Com­pressor", + "icon": "" + }, + { + "value": "2", + "text": "Add. heat", + "icon": "" + }, + { + "value": "3", + "text": "Com­pressor and Addi­tional Heat", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48028", + "parameterName": "sat", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 2, + "strVal": "Add. heat", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "1", + "text": "Com­pressor", + "icon": "" + }, + { + "value": "2", + "text": "Add. heat", + "icon": "" + }, + { + "value": "3", + "text": "Com­pressor and Addi­tional Heat", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48029", + "parameterName": "fri", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 2, + "strVal": "Add. heat", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "1", + "text": "Com­pressor", + "icon": "" + }, + { + "value": "2", + "text": "Add. heat", + "icon": "" + }, + { + "value": "3", + "text": "Com­pressor and Addi­tional Heat", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48030", + "parameterName": "thur", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 2, + "strVal": "Add. heat", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "1", + "text": "Com­pressor", + "icon": "" + }, + { + "value": "2", + "text": "Add. heat", + "icon": "" + }, + { + "value": "3", + "text": "Com­pressor and Addi­tional Heat", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48031", + "parameterName": "wed", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 2, + "strVal": "Add. heat", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "1", + "text": "Com­pressor", + "icon": "" + }, + { + "value": "2", + "text": "Add. heat", + "icon": "" + }, + { + "value": "3", + "text": "Com­pressor and Addi­tional Heat", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48032", + "parameterName": "tues", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 2, + "strVal": "Add. heat", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "1", + "text": "Com­pressor", + "icon": "" + }, + { + "value": "2", + "text": "Add. heat", + "icon": "" + }, + { + "value": "3", + "text": "Com­pressor and Addi­tional Heat", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48033", + "parameterName": "mon", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 2, + "strVal": "Add. heat", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "1", + "text": "Com­pressor", + "icon": "" + }, + { + "value": "2", + "text": "Add. heat", + "icon": "" + }, + { + "value": "3", + "text": "Com­pressor and Addi­tional Heat", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48034", + "parameterName": "all", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "1", + "text": "Com­pressor", + "icon": "" + }, + { + "value": "2", + "text": "Add. heat", + "icon": "" + }, + { + "value": "3", + "text": "Com­pressor and Addi­tional Heat", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48072", + "parameterName": "start diff additional heat", + "parameterUnit": "DM", + "writable": true, + "timestamp": "2024-02-14T08:36:03+00:00", + "value": 400, + "strVal": "400DM", + "smartHomeCategories": [], + "minValue": 100, + "maxValue": 2000, + "stepValue": 10, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48132", + "parameterName": "temporary lux", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-03-15T09:29:25+00:00", + "value": 4, + "strVal": "one time increase", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "0", + "text": "off", + "icon": "" + }, + { + "value": "1", + "text": "3 hrs", + "icon": "" + }, + { + "value": "2", + "text": "6 hrs", + "icon": "" + }, + { + "value": "3", + "text": "12 hrs", + "icon": "" + }, + { + "value": "4", + "text": "one time increase", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48200", + "parameterName": "freeze protection", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 1, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48228", + "parameterName": "heating", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:01+00:00", + "value": 0, + "strVal": "intermittent", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "0", + "text": "intermittent", + "icon": "" + }, + { + "value": "1", + "text": "auto", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48229", + "parameterName": "heating", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:01+00:00", + "value": 0, + "strVal": "intermittent", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "0", + "text": "intermittent", + "icon": "" + }, + { + "value": "1", + "text": "auto", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48230", + "parameterName": "heating", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:01+00:00", + "value": 0, + "strVal": "intermittent", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "0", + "text": "intermittent", + "icon": "" + }, + { + "value": "1", + "text": "auto", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48231", + "parameterName": "heating", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:01+00:00", + "value": 0, + "strVal": "intermittent", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "0", + "text": "intermittent", + "icon": "" + }, + { + "value": "1", + "text": "auto", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48232", + "parameterName": "heating", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:01+00:00", + "value": 0, + "strVal": "intermittent", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "0", + "text": "intermittent", + "icon": "" + }, + { + "value": "1", + "text": "auto", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48233", + "parameterName": "heating", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:01+00:00", + "value": 0, + "strVal": "intermittent", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "0", + "text": "intermittent", + "icon": "" + }, + { + "value": "1", + "text": "auto", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48234", + "parameterName": "heating", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:01+00:00", + "value": 0, + "strVal": "intermittent", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "0", + "text": "intermittent", + "icon": "" + }, + { + "value": "1", + "text": "auto", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48235", + "parameterName": "heating", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:01+00:00", + "value": 0, + "strVal": "intermittent", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "0", + "text": "intermittent", + "icon": "" + }, + { + "value": "1", + "text": "auto", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48279", + "parameterName": "positioning", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 1, + "strVal": "after QN10", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "0", + "text": "before QN10", + "icon": "" + }, + { + "value": "1", + "text": "after QN10", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48280", + "parameterName": "add. heat in tank", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:03+00:00", + "value": 1, + "strVal": "1", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 1, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48281", + "parameterName": "charge method", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 1, + "strVal": "delta temp", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "0", + "text": "target temp", + "icon": "" + }, + { + "value": "1", + "text": "delta temp", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48356", + "parameterName": "silent mode permitted", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:02+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48357", + "parameterName": "silent mode permitted", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:02+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48358", + "parameterName": "silent mode permitted", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:02+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48359", + "parameterName": "silent mode permitted", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:02+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48360", + "parameterName": "silent mode permitted", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:02+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48361", + "parameterName": "silent mode permitted", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:02+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48362", + "parameterName": "silent mode permitted", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:02+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48363", + "parameterName": "silent mode permitted", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:02+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48375", + "parameterName": "heating", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:02+00:00", + "value": 1, + "strVal": "1", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48376", + "parameterName": "heating", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:02+00:00", + "value": 1, + "strVal": "1", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48377", + "parameterName": "heating", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:02+00:00", + "value": 1, + "strVal": "1", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48378", + "parameterName": "heating", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:02+00:00", + "value": 1, + "strVal": "1", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48379", + "parameterName": "heating", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:02+00:00", + "value": 1, + "strVal": "1", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48380", + "parameterName": "heating", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:02+00:00", + "value": 1, + "strVal": "1", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48381", + "parameterName": "heating", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:02+00:00", + "value": 1, + "strVal": "1", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48382", + "parameterName": "heating", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:01+00:00", + "value": 1, + "strVal": "1", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48391", + "parameterName": "hot water", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:02+00:00", + "value": 1, + "strVal": "1", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48392", + "parameterName": "hot water", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:02+00:00", + "value": 1, + "strVal": "1", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48393", + "parameterName": "hot water", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:02+00:00", + "value": 1, + "strVal": "1", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48394", + "parameterName": "hot water", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:02+00:00", + "value": 1, + "strVal": "1", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48395", + "parameterName": "hot water", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:02+00:00", + "value": 1, + "strVal": "1", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48396", + "parameterName": "hot water", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:02+00:00", + "value": 1, + "strVal": "1", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48397", + "parameterName": "hot water", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:02+00:00", + "value": 1, + "strVal": "1", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48398", + "parameterName": "hot water", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:02+00:00", + "value": 1, + "strVal": "1", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48442", + "parameterName": "activated", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:38:42+00:00", + "value": 1, + "strVal": "1", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 1, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48443", + "parameterName": "affect room temperature", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:38:42+00:00", + "value": 1, + "strVal": "1", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 1, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48444", + "parameterName": "- degree of effect", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:38:42+00:00", + "value": 3, + "strVal": "3", + "smartHomeCategories": [], + "minValue": 1, + "maxValue": 10, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48445", + "parameterName": "affect hot water", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:38:42+00:00", + "value": 1, + "strVal": "1", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 1, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48460", + "parameterName": "max. allowed speed", + "parameterUnit": "%", + "writable": true, + "timestamp": "2024-02-14T08:36:02+00:00", + "value": 100, + "strVal": "100%", + "smartHomeCategories": [], + "minValue": 80, + "maxValue": 100, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48461", + "parameterName": "max. allowed speed", + "parameterUnit": "%", + "writable": true, + "timestamp": "2024-02-14T08:36:02+00:00", + "value": 100, + "strVal": "100%", + "smartHomeCategories": [], + "minValue": 80, + "maxValue": 100, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48462", + "parameterName": "max. allowed speed", + "parameterUnit": "%", + "writable": true, + "timestamp": "2024-02-14T08:36:02+00:00", + "value": 100, + "strVal": "100%", + "smartHomeCategories": [], + "minValue": 80, + "maxValue": 100, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48463", + "parameterName": "max. allowed speed", + "parameterUnit": "%", + "writable": true, + "timestamp": "2024-02-14T08:36:02+00:00", + "value": 100, + "strVal": "100%", + "smartHomeCategories": [], + "minValue": 80, + "maxValue": 100, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48464", + "parameterName": "max. allowed speed", + "parameterUnit": "%", + "writable": true, + "timestamp": "2024-02-14T08:36:02+00:00", + "value": 100, + "strVal": "100%", + "smartHomeCategories": [], + "minValue": 80, + "maxValue": 100, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48465", + "parameterName": "max. allowed speed", + "parameterUnit": "%", + "writable": true, + "timestamp": "2024-02-14T08:36:02+00:00", + "value": 100, + "strVal": "100%", + "smartHomeCategories": [], + "minValue": 80, + "maxValue": 100, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48466", + "parameterName": "max. allowed speed", + "parameterUnit": "%", + "writable": true, + "timestamp": "2024-02-14T08:36:02+00:00", + "value": 100, + "strVal": "100%", + "smartHomeCategories": [], + "minValue": 80, + "maxValue": 100, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48467", + "parameterName": "max. allowed speed", + "parameterUnit": "%", + "writable": true, + "timestamp": "2024-02-14T08:36:02+00:00", + "value": 100, + "strVal": "100%", + "smartHomeCategories": [], + "minValue": 80, + "maxValue": 100, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48566", + "parameterName": "- degree of effect", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:38:42+00:00", + "value": 2, + "strVal": "2", + "smartHomeCategories": [], + "minValue": 1, + "maxValue": 4, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48642", + "parameterName": "start delta-T", + "parameterUnit": "°C", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 20, + "strVal": "20°C", + "smartHomeCategories": [], + "minValue": 2, + "maxValue": 40, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48643", + "parameterName": "stop delta-T", + "parameterUnit": "°C", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 10, + "strVal": "10°C", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 38, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48644", + "parameterName": "max. tank temperature", + "parameterUnit": "°C", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 85, + "strVal": "85°C", + "smartHomeCategories": [], + "minValue": 50, + "maxValue": 100, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48645", + "parameterName": "tank cooling", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 1, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48659", + "parameterName": "blockFreq", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 1, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48660", + "parameterName": "blockFreq", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 1, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48665", + "parameterName": "blockFreq", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:02+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48711", + "parameterName": "stop temp compressor", + "parameterUnit": "°C", + "writable": true, + "timestamp": "2024-02-14T08:36:02+00:00", + "value": -25, + "strVal": "-25°C", + "smartHomeCategories": [], + "minValue": -25, + "maxValue": -2, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48712", + "parameterName": "stop temp compressor", + "parameterUnit": "°C", + "writable": true, + "timestamp": "2024-02-14T08:36:02+00:00", + "value": -25, + "strVal": "-25°C", + "smartHomeCategories": [], + "minValue": -25, + "maxValue": -2, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48713", + "parameterName": "stop temp compressor", + "parameterUnit": "°C", + "writable": true, + "timestamp": "2024-02-14T08:36:02+00:00", + "value": -25, + "strVal": "-25°C", + "smartHomeCategories": [], + "minValue": -25, + "maxValue": -2, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48714", + "parameterName": "stop temp compressor", + "parameterUnit": "°C", + "writable": true, + "timestamp": "2024-02-14T08:36:02+00:00", + "value": -25, + "strVal": "-25°C", + "smartHomeCategories": [], + "minValue": -25, + "maxValue": -2, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48715", + "parameterName": "stop temp compressor", + "parameterUnit": "°C", + "writable": true, + "timestamp": "2024-02-14T08:36:02+00:00", + "value": -25, + "strVal": "-25°C", + "smartHomeCategories": [], + "minValue": -25, + "maxValue": -2, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48716", + "parameterName": "stop temp compressor", + "parameterUnit": "°C", + "writable": true, + "timestamp": "2024-02-14T08:36:02+00:00", + "value": -25, + "strVal": "-25°C", + "smartHomeCategories": [], + "minValue": -25, + "maxValue": -2, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48717", + "parameterName": "stop temp compressor", + "parameterUnit": "°C", + "writable": true, + "timestamp": "2024-02-14T08:36:02+00:00", + "value": -25, + "strVal": "-25°C", + "smartHomeCategories": [], + "minValue": -25, + "maxValue": -2, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48758", + "parameterName": "Relay ctrl", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 0, + "strVal": "Always on", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "0", + "text": "Always on", + "icon": "" + }, + { + "value": "0", + "text": "Temp", + "icon": "" + }, + { + "value": "0", + "text": "Watch", + "icon": "" + }, + { + "value": "0", + "text": "W+T", + "icon": "" + }, + { + "value": "0", + "text": "Always on", + "icon": "" + }, + { + "value": "0", + "text": "Ext. ctrl", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48759", + "parameterName": "Relay ctrl", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 0, + "strVal": "Always on", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "0", + "text": "Always on", + "icon": "" + }, + { + "value": "0", + "text": "Temp", + "icon": "" + }, + { + "value": "0", + "text": "Watch", + "icon": "" + }, + { + "value": "0", + "text": "W+T", + "icon": "" + }, + { + "value": "0", + "text": "Always on", + "icon": "" + }, + { + "value": "0", + "text": "Ext. ctrl", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48760", + "parameterName": "Relay ctrl", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 0, + "strVal": "Always on", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "0", + "text": "Always on", + "icon": "" + }, + { + "value": "0", + "text": "Temp", + "icon": "" + }, + { + "value": "0", + "text": "Watch", + "icon": "" + }, + { + "value": "0", + "text": "W+T", + "icon": "" + }, + { + "value": "0", + "text": "Always on", + "icon": "" + }, + { + "value": "0", + "text": "Ext. ctrl", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48761", + "parameterName": "Relay ctrl", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 0, + "strVal": "Always on", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "0", + "text": "Always on", + "icon": "" + }, + { + "value": "0", + "text": "Temp", + "icon": "" + }, + { + "value": "0", + "text": "Watch", + "icon": "" + }, + { + "value": "0", + "text": "W+T", + "icon": "" + }, + { + "value": "0", + "text": "Always on", + "icon": "" + }, + { + "value": "0", + "text": "Ext. ctrl", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48762", + "parameterName": "Start time (h)", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 23, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48763", + "parameterName": "Start time (h)", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 23, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48764", + "parameterName": "Start time (h)", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 23, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48765", + "parameterName": "Start time (h)", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 23, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48766", + "parameterName": "Stop time (h)", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 23, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48767", + "parameterName": "Stop time (h)", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 23, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48768", + "parameterName": "Stop time (h)", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 23, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48769", + "parameterName": "Stop time (h)", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 23, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48770", + "parameterName": "Switch temp.", + "parameterUnit": "°C", + "writable": true, + "timestamp": "2023-11-22T07:03:46+00:00", + "value": 0, + "strVal": "0°C", + "smartHomeCategories": [], + "minValue": -40, + "maxValue": 100, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48771", + "parameterName": "Switch temp.", + "parameterUnit": "°C", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 0, + "strVal": "0°C", + "smartHomeCategories": [], + "minValue": -40, + "maxValue": 100, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48772", + "parameterName": "Switch temp.", + "parameterUnit": "°C", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 0, + "strVal": "0°C", + "smartHomeCategories": [], + "minValue": -40, + "maxValue": 100, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48773", + "parameterName": "Switch temp.", + "parameterUnit": "°C", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 0, + "strVal": "0°C", + "smartHomeCategories": [], + "minValue": -40, + "maxValue": 100, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48774", + "parameterName": "Use input", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 1, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48775", + "parameterName": "Use input", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 1, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48776", + "parameterName": "Use input", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 1, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48777", + "parameterName": "Use input", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 1, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48826", + "parameterName": "limit RH in the room, syst.", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 1, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48891", + "parameterName": "adjusting ventilation", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 2147483647, + "maxValue": 0, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48941", + "parameterName": "defrost more often", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:02+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "48943", + "parameterName": "temp. passive / active", + "parameterUnit": "°C", + "writable": true, + "timestamp": "2024-02-29T12:08:21+00:00", + "value": 4, + "strVal": "4°C", + "smartHomeCategories": [], + "minValue": -99, + "maxValue": 99, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49190", + "parameterName": "Alarm at min temp", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 1, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49230", + "parameterName": "activ. imm heat in heat mode", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:03+00:00", + "value": 1, + "strVal": "1", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 1, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49383", + "parameterName": "blockFreq", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:02+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49383", + "parameterName": "blockFreq", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:02+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49383", + "parameterName": "blockFreq", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:02+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49383", + "parameterName": "blockFreq", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:02+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49383", + "parameterName": "blockFreq", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:02+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49383", + "parameterName": "blockFreq", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:02+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49383", + "parameterName": "blockFreq", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:02+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49383", + "parameterName": "blockFreq", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:02+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49383", + "parameterName": "blockFreq", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:02+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49383", + "parameterName": "blockFreq", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:02+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49383", + "parameterName": "blockFreq", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:02+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49383", + "parameterName": "blockFreq", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:02+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49383", + "parameterName": "blockFreq", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:02+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49383", + "parameterName": "blockFreq", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:02+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49383", + "parameterName": "blockFreq", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:02+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49383", + "parameterName": "blockFreq", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:02+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49384", + "parameterName": "blockFreq", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:02+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49385", + "parameterName": "blockFreq", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:02+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49386", + "parameterName": "blockFreq", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:02+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49387", + "parameterName": "blockFreq", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:02+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49388", + "parameterName": "blockFreq", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:02+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49390", + "parameterName": "blockFreq", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:02+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49391", + "parameterName": "blockFreq", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:02+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49392", + "parameterName": "blockFreq", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:02+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49393", + "parameterName": "blockFreq", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:02+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49394", + "parameterName": "blockFreq", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:02+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49395", + "parameterName": "blockFreq", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:02+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49396", + "parameterName": "blockFreq", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:02+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49397", + "parameterName": "blockFreq", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:02+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49563", + "parameterName": "QM41 motion interval", + "parameterUnit": "days", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 14, + "strVal": "14days", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 30, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49564", + "parameterName": "fire alarm temperature BT20", + "parameterUnit": "°C", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 38, + "strVal": "38°C", + "smartHomeCategories": [], + "minValue": 38, + "maxValue": 70, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49566", + "parameterName": "set point value BP12", + "parameterUnit": "Pa", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 150, + "strVal": "150Pa", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 400, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49567", + "parameterName": "min. fan speed GQ2", + "parameterUnit": "%", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 10, + "strVal": "10%", + "smartHomeCategories": [], + "minValue": 1, + "maxValue": 100, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49568", + "parameterName": "max. fan speed GQ2", + "parameterUnit": "%", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 100, + "strVal": "100%", + "smartHomeCategories": [], + "minValue": 1, + "maxValue": 100, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49569", + "parameterName": "filt. alarm press.", + "parameterUnit": "Pa", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 150, + "strVal": "150Pa", + "smartHomeCategories": [], + "minValue": 100, + "maxValue": 300, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49570", + "parameterName": "months btwn filter alarms", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 12, + "strVal": "12", + "smartHomeCategories": [], + "minValue": 1, + "maxValue": 24, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49571", + "parameterName": "runtime damper QM41", + "parameterUnit": "sec", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 150, + "strVal": "150sec", + "smartHomeCategories": [], + "minValue": 1, + "maxValue": 300, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49572", + "parameterName": "outd temp compensation", + "parameterUnit": "Pa", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 0, + "strVal": "0Pa", + "smartHomeCategories": [], + "minValue": -100, + "maxValue": 0, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49573", + "parameterName": "type of fire alarm", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 1, + "strVal": "1", + "smartHomeCategories": [], + "minValue": 1, + "maxValue": 3, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49574", + "parameterName": "fan speed during fire alarm", + "parameterUnit": "%", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 70, + "strVal": "70%", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 100, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49580", + "parameterName": "start manual motion QM41", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 2147483647, + "maxValue": 0, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49584", + "parameterName": "kc parameter", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 10, + "strVal": "10", + "smartHomeCategories": [], + "minValue": 1, + "maxValue": 32000, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49610", + "parameterName": "climate system", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 4, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49728", + "parameterName": "man. sel. power at DOT", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 1, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49740", + "parameterName": "start manual defrosting", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:02+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49741", + "parameterName": "start manual defrosting", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:02+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49742", + "parameterName": "start manual defrosting", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:02+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49743", + "parameterName": "start manual defrosting", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:02+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49744", + "parameterName": "start manual defrosting", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:02+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49745", + "parameterName": "start manual defrosting", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:02+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49746", + "parameterName": "start manual defrosting", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:02+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49747", + "parameterName": "start manual defrosting", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:02+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49756", + "parameterName": "fan de-icing", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:03+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 2147483647, + "maxValue": 0, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49797", + "parameterName": "temp. cond. heater EB14", + "parameterUnit": "°C", + "writable": true, + "timestamp": "2023-11-22T07:03:45+00:00", + "value": 4, + "strVal": "4°C", + "smartHomeCategories": [], + "minValue": -20, + "maxValue": 20, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49866", + "parameterName": "min. allowed speed", + "parameterUnit": "%", + "writable": true, + "timestamp": "2024-02-14T08:36:02+00:00", + "value": 1, + "strVal": "1%", + "smartHomeCategories": [], + "minValue": 1, + "maxValue": 50, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49867", + "parameterName": "min. allowed speed", + "parameterUnit": "%", + "writable": true, + "timestamp": "2024-02-14T08:36:02+00:00", + "value": 1, + "strVal": "1%", + "smartHomeCategories": [], + "minValue": 1, + "maxValue": 50, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49868", + "parameterName": "min. allowed speed", + "parameterUnit": "%", + "writable": true, + "timestamp": "2024-02-14T08:36:02+00:00", + "value": 1, + "strVal": "1%", + "smartHomeCategories": [], + "minValue": 1, + "maxValue": 50, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49869", + "parameterName": "min. allowed speed", + "parameterUnit": "%", + "writable": true, + "timestamp": "2024-02-14T08:36:02+00:00", + "value": 1, + "strVal": "1%", + "smartHomeCategories": [], + "minValue": 1, + "maxValue": 50, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49870", + "parameterName": "min. allowed speed", + "parameterUnit": "%", + "writable": true, + "timestamp": "2024-02-14T08:36:02+00:00", + "value": 1, + "strVal": "1%", + "smartHomeCategories": [], + "minValue": 1, + "maxValue": 50, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49871", + "parameterName": "min. allowed speed", + "parameterUnit": "%", + "writable": true, + "timestamp": "2024-02-14T08:36:02+00:00", + "value": 1, + "strVal": "1%", + "smartHomeCategories": [], + "minValue": 1, + "maxValue": 50, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49872", + "parameterName": "min. allowed speed", + "parameterUnit": "%", + "writable": true, + "timestamp": "2024-02-14T08:36:02+00:00", + "value": 1, + "strVal": "1%", + "smartHomeCategories": [], + "minValue": 1, + "maxValue": 50, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49873", + "parameterName": "min. allowed speed", + "parameterUnit": "%", + "writable": true, + "timestamp": "2024-02-14T08:36:02+00:00", + "value": 15, + "strVal": "15%", + "smartHomeCategories": [], + "minValue": 1, + "maxValue": 50, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49909", + "parameterName": "start time", + "parameterUnit": "", + "writable": true, + "timestamp": "2023-11-22T07:03:44+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 23, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49912", + "parameterName": "Start Sunday", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49913", + "parameterName": "Start Saturday", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49914", + "parameterName": "Start Friday", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49915", + "parameterName": "Start Thursday", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49916", + "parameterName": "Start Wednesday", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49917", + "parameterName": "Start Tuesday", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49918", + "parameterName": "Start Monday", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49919", + "parameterName": "Start all days", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:06+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49920", + "parameterName": "Finish Sunday", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49921", + "parameterName": "Finish Saturday", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49922", + "parameterName": "Finish Friday", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49923", + "parameterName": "Finish Thursday", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49924", + "parameterName": "Finish Wednesday", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49925", + "parameterName": "Finish Tuesday", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49926", + "parameterName": "Finish Monday", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:05+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49927", + "parameterName": "End all days", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:06+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49946", + "parameterName": "Start Sunday", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:06+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49947", + "parameterName": "Start Saturday", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:06+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49948", + "parameterName": "Start Friday", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:06+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49949", + "parameterName": "Start Thursday", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:06+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49950", + "parameterName": "Start Wednesday", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:06+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49951", + "parameterName": "Start Tuesday", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:06+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49952", + "parameterName": "Start Monday", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:06+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49953", + "parameterName": "Start all days", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:06+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49954", + "parameterName": "Finish Sunday", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:06+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49955", + "parameterName": "Finish Saturday", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:06+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49956", + "parameterName": "Finish Friday", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:06+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49957", + "parameterName": "Finish Thursday", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:06+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49958", + "parameterName": "Finish Wednesday", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:06+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49959", + "parameterName": "Finish Tuesday", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:06+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49960", + "parameterName": "Finish Monday", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:06+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49961", + "parameterName": "End all days", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-14T08:36:06+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": 0, + "maxValue": 86400, + "stepValue": 900, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "49994", + "parameterName": "Prior­ity", + "parameterUnit": "", + "writable": false, + "timestamp": "2024-03-15T09:29:26+00:00", + "value": 20, + "strVal": "Hot water", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "10", + "text": "Off", + "icon": "" + }, + { + "value": "20", + "text": "Hot water", + "icon": "" + }, + { + "value": "30", + "text": "Heating", + "icon": "" + }, + { + "value": "40", + "text": "Pool", + "icon": "" + }, + { + "value": "41", + "text": "Pool 2", + "icon": "" + }, + { + "value": "50", + "text": "Trans­fer", + "icon": "" + }, + { + "value": "60", + "text": "Cooling", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "Slave 1 (EB101)", + "parameterId": "49996", + "parameterName": "Charge pump (GP12)", + "parameterUnit": "", + "writable": false, + "timestamp": "2024-03-15T09:19:24+00:00", + "value": 1, + "strVal": "On", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "0", + "text": "Off", + "icon": "" + }, + { + "value": "1", + "text": "On", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "50004", + "parameterName": "Tempo­rary lux", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-03-15T09:29:25+00:00", + "value": 1, + "strVal": "on", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "0", + "text": "off", + "icon": "" + }, + { + "value": "1", + "text": "on", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "50096", + "parameterName": "status:", + "parameterUnit": "min", + "writable": false, + "timestamp": "2024-03-15T09:34:48+00:00", + "value": 15004, + "strVal": "15004min", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "15000", + "text": "starts", + "icon": "" + }, + { + "value": "15001", + "text": "runs", + "icon": "" + }, + { + "value": "15003", + "text": "off", + "icon": "" + }, + { + "value": "15004", + "text": "hot water", + "icon": "" + }, + { + "value": "15005", + "text": "heating", + "icon": "" + }, + { + "value": "15006", + "text": "pool", + "icon": "" + }, + { + "value": "10480", + "text": "incomp. hp", + "icon": "" + }, + { + "value": "10065", + "text": "Comm.fault In", + "icon": "" + }, + { + "value": "10066", + "text": "Communication fault with PCA Input.", + "icon": "" + }, + { + "value": "10067", + "text": "Communication fault with PCA Input.", + "icon": "" + }, + { + "value": "10068", + "text": "Com.flt Base", + "icon": "" + }, + { + "value": "10069", + "text": "Communication fault with PCA Base.", + "icon": "" + }, + { + "value": "10070", + "text": "Communication fault with PCA Base.", + "icon": "" + }, + { + "value": "10162", + "text": "Sens flt:BT18", + "icon": "" + }, + { + "value": "15971", + "text": "incomp. inv.", + "icon": "" + }, + { + "value": "10177", + "text": "Sens flt:BT14", + "icon": "" + }, + { + "value": "10071", + "text": "HP alarm", + "icon": "" + }, + { + "value": "10072", + "text": "High pressure alarm", + "icon": "" + }, + { + "value": "10073", + "text": "High pressure alarm", + "icon": "" + }, + { + "value": "10074", + "text": "LP alarm", + "icon": "" + }, + { + "value": "10075", + "text": "Low pressure alarm", + "icon": "" + }, + { + "value": "10076", + "text": "Low pressure alarm", + "icon": "" + }, + { + "value": "10077", + "text": "Comm.flt MC", + "icon": "" + }, + { + "value": "10078", + "text": "Comm.flt with PCA Motor Controller", + "icon": "" + }, + { + "value": "10079", + "text": "Communication fault with PCA Motor Controller.", + "icon": "" + }, + { + "value": "10080", + "text": "MP alarm", + "icon": "" + }, + { + "value": "10081", + "text": "Motor protection alarm", + "icon": "" + }, + { + "value": "10082", + "text": "Motor protection alarm", + "icon": "" + }, + { + "value": "10083", + "text": "Sensor flt:BT1", + "icon": "" + }, + { + "value": "10084", + "text": "Sensor fault: BT1 outdoor sensor", + "icon": "" + }, + { + "value": "10085", + "text": "Sensor fault: BT1 outdoor sensor", + "icon": "" + }, + { + "value": "10086", + "text": "Sensor flt:BT2", + "icon": "" + }, + { + "value": "10087", + "text": "Sensor fault: BT2 flow line sensor 1", + "icon": "" + }, + { + "value": "10088", + "text": "Sensor fault: BT2 flow line sensor 1", + "icon": "" + }, + { + "value": "10089", + "text": "Sens flt:BT12", + "icon": "" + }, + { + "value": "10090", + "text": "Sensor fault: BT12 condenser out", + "icon": "" + }, + { + "value": "10091", + "text": "Sensor fault: BT12 condenser out", + "icon": "" + }, + { + "value": "10092", + "text": "Sensor flt:BT3", + "icon": "" + }, + { + "value": "10095", + "text": "Sensor flt:BT6", + "icon": "" + }, + { + "value": "10154", + "text": "internal electrical addition", + "icon": "" + }, + { + "value": "10183", + "text": "L exh.temp", + "icon": "" + }, + { + "value": "10203", + "text": "Sensor flt:BT7", + "icon": "" + }, + { + "value": "10187", + "text": "Defrosting", + "icon": "" + }, + { + "value": "15117", + "text": "blocked", + "icon": "" + }, + { + "value": "16528", + "text": "Inverter fault", + "icon": "" + }, + { + "value": "10068", + "text": "Com.flt Base", + "icon": "" + }, + { + "value": "10311", + "text": "Inverter I", + "icon": "" + }, + { + "value": "10314", + "text": "Inverter II", + "icon": "" + }, + { + "value": "10317", + "text": "Inverter III", + "icon": "" + }, + { + "value": "10110", + "text": "Hot gas alarm", + "icon": "" + }, + { + "value": "10116", + "text": "TB alarm", + "icon": "" + }, + { + "value": "10122", + "text": "Hi cond. out", + "icon": "" + }, + { + "value": "10276", + "text": "Err: BT63", + "icon": "" + }, + { + "value": "10293", + "text": "serial no", + "icon": "" + }, + { + "value": "10296", + "text": "software", + "icon": "" + }, + { + "value": "11176", + "text": "comm.err hp", + "icon": "" + }, + { + "value": "15117", + "text": "blocked", + "icon": "" + }, + { + "value": "15629", + "text": "ext control", + "icon": "" + }, + { + "value": "15966", + "text": "Pres.alarm", + "icon": "" + }, + { + "value": "15971", + "text": "incomp. inv.", + "icon": "" + }, + { + "value": "16537", + "text": "Soft-start flt", + "icon": "" + }, + { + "value": "16659", + "text": "Acc. block.", + "icon": "" + }, + { + "value": "15468", + "text": "initiating", + "icon": "" + }, + { + "value": "11165", + "text": "pool 2", + "icon": "" + }, + { + "value": "11174", + "text": "com.err.slave", + "icon": "" + }, + { + "value": "15985", + "text": "Com.flt GP12", + "icon": "" + }, + { + "value": "16541", + "text": "not docked", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "50113", + "parameterName": "status:", + "parameterUnit": "kW", + "writable": false, + "timestamp": "2024-03-12T16:16:30+00:00", + "value": 1512, + "strVal": "1512kW", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "15000", + "text": "starts", + "icon": "" + }, + { + "value": "15001", + "text": "runs", + "icon": "" + }, + { + "value": "15003", + "text": "off", + "icon": "" + }, + { + "value": "15004", + "text": "hot water", + "icon": "" + }, + { + "value": "15005", + "text": "heating", + "icon": "" + }, + { + "value": "15006", + "text": "pool", + "icon": "" + }, + { + "value": "10480", + "text": "incomp. hp", + "icon": "" + }, + { + "value": "10065", + "text": "Comm.fault In", + "icon": "" + }, + { + "value": "10066", + "text": "Communication fault with PCA Input.", + "icon": "" + }, + { + "value": "10067", + "text": "Communication fault with PCA Input.", + "icon": "" + }, + { + "value": "10068", + "text": "Com.flt Base", + "icon": "" + }, + { + "value": "10069", + "text": "Communication fault with PCA Base.", + "icon": "" + }, + { + "value": "10070", + "text": "Communication fault with PCA Base.", + "icon": "" + }, + { + "value": "10162", + "text": "Sens flt:BT18", + "icon": "" + }, + { + "value": "15971", + "text": "incomp. inv.", + "icon": "" + }, + { + "value": "10177", + "text": "Sens flt:BT14", + "icon": "" + }, + { + "value": "10071", + "text": "HP alarm", + "icon": "" + }, + { + "value": "10072", + "text": "High pressure alarm", + "icon": "" + }, + { + "value": "10073", + "text": "High pressure alarm", + "icon": "" + }, + { + "value": "10074", + "text": "LP alarm", + "icon": "" + }, + { + "value": "10075", + "text": "Low pressure alarm", + "icon": "" + }, + { + "value": "10076", + "text": "Low pressure alarm", + "icon": "" + }, + { + "value": "10077", + "text": "Comm.flt MC", + "icon": "" + }, + { + "value": "10078", + "text": "Comm.flt with PCA Motor Controller", + "icon": "" + }, + { + "value": "10079", + "text": "Communication fault with PCA Motor Controller.", + "icon": "" + }, + { + "value": "10080", + "text": "MP alarm", + "icon": "" + }, + { + "value": "10081", + "text": "Motor protection alarm", + "icon": "" + }, + { + "value": "10082", + "text": "Motor protection alarm", + "icon": "" + }, + { + "value": "10083", + "text": "Sensor flt:BT1", + "icon": "" + }, + { + "value": "10084", + "text": "Sensor fault: BT1 outdoor sensor", + "icon": "" + }, + { + "value": "10085", + "text": "Sensor fault: BT1 outdoor sensor", + "icon": "" + }, + { + "value": "10086", + "text": "Sensor flt:BT2", + "icon": "" + }, + { + "value": "10087", + "text": "Sensor fault: BT2 flow line sensor 1", + "icon": "" + }, + { + "value": "10088", + "text": "Sensor fault: BT2 flow line sensor 1", + "icon": "" + }, + { + "value": "10089", + "text": "Sens flt:BT12", + "icon": "" + }, + { + "value": "10090", + "text": "Sensor fault: BT12 condenser out", + "icon": "" + }, + { + "value": "10091", + "text": "Sensor fault: BT12 condenser out", + "icon": "" + }, + { + "value": "10092", + "text": "Sensor flt:BT3", + "icon": "" + }, + { + "value": "10095", + "text": "Sensor flt:BT6", + "icon": "" + }, + { + "value": "10154", + "text": "internal electrical addition", + "icon": "" + }, + { + "value": "10183", + "text": "L exh.temp", + "icon": "" + }, + { + "value": "10203", + "text": "Sensor flt:BT7", + "icon": "" + }, + { + "value": "10187", + "text": "Defrosting", + "icon": "" + }, + { + "value": "15117", + "text": "blocked", + "icon": "" + }, + { + "value": "16528", + "text": "Inverter fault", + "icon": "" + }, + { + "value": "10068", + "text": "Com.flt Base", + "icon": "" + }, + { + "value": "10311", + "text": "Inverter I", + "icon": "" + }, + { + "value": "10314", + "text": "Inverter II", + "icon": "" + }, + { + "value": "10317", + "text": "Inverter III", + "icon": "" + }, + { + "value": "10110", + "text": "Hot gas alarm", + "icon": "" + }, + { + "value": "10116", + "text": "TB alarm", + "icon": "" + }, + { + "value": "10122", + "text": "Hi cond. out", + "icon": "" + }, + { + "value": "10276", + "text": "Err: BT63", + "icon": "" + }, + { + "value": "10293", + "text": "serial no", + "icon": "" + }, + { + "value": "10296", + "text": "software", + "icon": "" + }, + { + "value": "11176", + "text": "comm.err hp", + "icon": "" + }, + { + "value": "15117", + "text": "blocked", + "icon": "" + }, + { + "value": "15629", + "text": "ext control", + "icon": "" + }, + { + "value": "15966", + "text": "Pres.alarm", + "icon": "" + }, + { + "value": "15971", + "text": "incomp. inv.", + "icon": "" + }, + { + "value": "16537", + "text": "Soft-start flt", + "icon": "" + }, + { + "value": "16659", + "text": "Acc. block.", + "icon": "" + }, + { + "value": "15468", + "text": "initiating", + "icon": "" + }, + { + "value": "11165", + "text": "pool 2", + "icon": "" + }, + { + "value": "11174", + "text": "com.err.slave", + "icon": "" + }, + { + "value": "15985", + "text": "Com.flt GP12", + "icon": "" + }, + { + "value": "16541", + "text": "not docked", + "icon": "" + } + ], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "SMO 20", + "parameterId": "50114", + "parameterName": "add. heat in tank", + "parameterUnit": "min", + "writable": false, + "timestamp": "2024-03-08T14:20:01+00:00", + "value": 0, + "strVal": "0min", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "0", + "text": "off", + "icon": "" + }, + { + "value": "1", + "text": "on", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + } +] diff --git a/tests/components/myuplink/fixtures/systems-2dev.json b/tests/components/myuplink/fixtures/systems-2dev.json new file mode 100644 index 00000000000..0718fc2301d --- /dev/null +++ b/tests/components/myuplink/fixtures/systems-2dev.json @@ -0,0 +1,34 @@ +{ + "page": 1, + "itemsPerPage": 10, + "numItems": 1, + "systems": [ + { + "systemId": "123456-7890-1234", + "name": "Gotham City", + "securityLevel": "admin", + "hasAlarm": false, + "country": "Sweden", + "devices": [ + { + "id": "robin-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff", + "connectionState": "Connected", + "currentFwVersion": "9682R7", + "product": { + "serialNumber": "222222", + "name": "F730 CU 3x400V" + } + }, + { + "id": "batman-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff", + "connectionState": "Connected", + "currentFwVersion": "9682R7", + "product": { + "serialNumber": "123456", + "name": "F730 CU 3x400V" + } + } + ] + } + ] +} diff --git a/tests/components/myuplink/fixtures/systems.json b/tests/components/myuplink/fixtures/systems.json index 151e9f55a8d..c009816a88f 100644 --- a/tests/components/myuplink/fixtures/systems.json +++ b/tests/components/myuplink/fixtures/systems.json @@ -5,7 +5,7 @@ "systems": [ { "systemId": "123456-7890-1234", - "name": "Gotham City", + "name": "Batcave", "securityLevel": "admin", "hasAlarm": false, "country": "Sweden", diff --git a/tests/components/myuplink/snapshots/test_diagnostics.ambr b/tests/components/myuplink/snapshots/test_diagnostics.ambr index 49bdfeba93e..53664820364 100644 --- a/tests/components/myuplink/snapshots/test_diagnostics.ambr +++ b/tests/components/myuplink/snapshots/test_diagnostics.ambr @@ -1018,6 +1018,1011 @@ ''', }), }), + dict({ + '123456-7890-1234': dict({ + 'device_data': ''' + { + "id": "batman-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff", + "connectionState": "Connected", + "firmware": { + "currentFwVersion": "9682R7", + "desiredFwVersion": "9682R7" + }, + "product": { + "serialNumber": "123456", + "name": "F730 CU 3x400V" + }, + "availableFeatures": { + "settings": true, + "reboot": true, + "forcesync": true, + "forceUpdate": false, + "requestUpdate": false, + "resetAlarm": true, + "triggerEvent": true, + "getMenu": false, + "getMenuChain": false, + "getGuideQuestion": false, + "sendHaystack": true, + "setSmartMode": false, + "setAidMode": true, + "getZones": false, + "processIntent": false, + "boostHotWater": true, + "boostVentilation": true, + "getScheduleConfig": false, + "getScheduleModes": false, + "getScheduleWeekly": false, + "getScheduleVacation": false, + "setScheduleModes": false, + "setScheduleWeekly": false, + "setScheduleOverride": false, + "setScheduleVacation": false, + "setVentilationMode": false + } + } + + ''', + 'points': ''' + [ + { + "category": "NIBEF F730 CU 3x400V", + "parameterId": "40004", + "parameterName": "Current outd temp (BT1)", + "parameterUnit": "°C", + "writable": false, + "timestamp": "2024-02-09T10:56:12+00:00", + "value": -9.3, + "strVal": "-9.3°C", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "NIBEF F730 CU 3x400V", + "parameterId": "40008", + "parameterName": "Supply line (BT2)", + "parameterUnit": "°C", + "writable": false, + "timestamp": "2024-02-09T10:58:51+00:00", + "value": 39.7, + "strVal": "39.7°C", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "NIBEF F730 CU 3x400V", + "parameterId": "40012", + "parameterName": "Return line (BT3)", + "parameterUnit": "°C", + "writable": false, + "timestamp": "2024-02-09T10:08:40+00:00", + "value": 34.4, + "strVal": "34.4°C", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "NIBEF F730 CU 3x400V", + "parameterId": "40013", + "parameterName": "Hot water top (BT7)", + "parameterUnit": "°C", + "writable": false, + "timestamp": "2024-02-09T08:39:32+00:00", + "value": 46, + "strVal": "46°C", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "NIBEF F730 CU 3x400V", + "parameterId": "40014", + "parameterName": "Hot water char­ging (BT6)", + "parameterUnit": "°C", + "writable": false, + "timestamp": "2024-02-09T10:58:42+00:00", + "value": 44.4, + "strVal": "44.4°C", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "NIBEF F730 CU 3x400V", + "parameterId": "40017", + "parameterName": "Con­denser (BT12)", + "parameterUnit": "°C", + "writable": false, + "timestamp": "2024-02-09T10:41:49+00:00", + "value": 37.7, + "strVal": "37.7°C", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "NIBEF F730 CU 3x400V", + "parameterId": "40018", + "parameterName": "Dis­charge (BT14)", + "parameterUnit": "°C", + "writable": false, + "timestamp": "2024-02-09T10:19:24+00:00", + "value": 89.1, + "strVal": "89.1°C", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "NIBEF F730 CU 3x400V", + "parameterId": "40019", + "parameterName": "Liquid line (BT15)", + "parameterUnit": "°C", + "writable": false, + "timestamp": "2024-02-09T10:42:11+00:00", + "value": 34.4, + "strVal": "34.4°C", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "NIBEF F730 CU 3x400V", + "parameterId": "40020", + "parameterName": "Evap­orator (BT16)", + "parameterUnit": "°C", + "writable": false, + "timestamp": "2024-02-09T11:11:00+00:00", + "value": -14.7, + "strVal": "-14.7°C", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "NIBEF F730 CU 3x400V", + "parameterId": "40022", + "parameterName": "Suction gas (BT17)", + "parameterUnit": "°C", + "writable": false, + "timestamp": "2024-02-09T11:11:28+00:00", + "value": -1.1, + "strVal": "-1.1°C", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "NIBEF F730 CU 3x400V", + "parameterId": "40025", + "parameterName": "Exhaust air (BT20)", + "parameterUnit": "°C", + "writable": false, + "timestamp": "2024-02-09T09:48:50+00:00", + "value": 22.5, + "strVal": "22.5°C", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "NIBEF F730 CU 3x400V", + "parameterId": "40026", + "parameterName": "Extract air (BT21)", + "parameterUnit": "°C", + "writable": false, + "timestamp": "2024-02-09T11:11:15+00:00", + "value": -12.1, + "strVal": "-12.1°C", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "NIBEF F730 CU 3x400V", + "parameterId": "40033", + "parameterName": "Room temp­erature (BT50)", + "parameterUnit": "°C", + "writable": false, + "timestamp": "2024-02-09T00:21:48+00:00", + "value": 21.2, + "strVal": "21.2°C", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "NIBEF F730 CU 3x400V", + "parameterId": "40047", + "parameterName": "Supply line (BT61)", + "parameterUnit": "°C", + "writable": false, + "timestamp": "2024-02-01T14:30:32+00:00", + "value": -32768, + "strVal": "-32768°C", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "NIBEF F730 CU 3x400V", + "parameterId": "40048", + "parameterName": "Return line (BT62)", + "parameterUnit": "°C", + "writable": false, + "timestamp": "2024-02-01T14:30:32+00:00", + "value": -32768, + "strVal": "-32768°C", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "NIBEF F730 CU 3x400V", + "parameterId": "40050", + "parameterName": "Value, air veloc­ity sensor (BS1)", + "parameterUnit": "", + "writable": false, + "timestamp": "2024-02-09T11:10:42+00:00", + "value": 101.5, + "strVal": "101.5", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "NIBEF F730 CU 3x400V", + "parameterId": "40067", + "parameterName": "Average outdoor temp (BT1)", + "parameterUnit": "°C", + "writable": false, + "timestamp": "2024-02-09T10:56:45+00:00", + "value": -12.2, + "strVal": "-12.2°C", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "NIBEF F730 CU 3x400V", + "parameterId": "40079", + "parameterName": "Current (BE1)", + "parameterUnit": "A", + "writable": false, + "timestamp": "2024-02-09T09:05:50+00:00", + "value": 3.1, + "strVal": "3.1A", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "NIBEF F730 CU 3x400V", + "parameterId": "40081", + "parameterName": "Current (BE2)", + "parameterUnit": "A", + "writable": false, + "timestamp": "2024-02-09T11:11:19+00:00", + "value": 0.3, + "strVal": "0.3A", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "NIBEF F730 CU 3x400V", + "parameterId": "40083", + "parameterName": "Current (BE3)", + "parameterUnit": "A", + "writable": false, + "timestamp": "2024-02-09T09:46:11+00:00", + "value": 5.7, + "strVal": "5.7A", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "NIBEF F730 CU 3x400V", + "parameterId": "40145", + "parameterName": "Oil temp­erature (EP15-BT29)", + "parameterUnit": "°C", + "writable": false, + "timestamp": "2024-02-01T14:30:32+00:00", + "value": 0, + "strVal": "0°C", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "NIBEF F730 CU 3x400V", + "parameterId": "40146", + "parameterName": "Oil temp­erature (BT29)", + "parameterUnit": "°C", + "writable": false, + "timestamp": "2024-02-01T14:30:32+00:00", + "value": 0, + "strVal": "0°C", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "NIBEF F730 CU 3x400V", + "parameterId": "40940", + "parameterName": "Degree minutes", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-09T11:09:39+00:00", + "value": -875, + "strVal": "-875", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "NIBEF F730 CU 3x400V", + "parameterId": "40940", + "parameterName": "Degree minutes", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-09T11:09:39+00:00", + "value": -875, + "strVal": "-875", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "NIBEF F730 CU 3x400V", + "parameterId": "41778", + "parameterName": "Current com­pressor fre­quency", + "parameterUnit": "Hz", + "writable": false, + "timestamp": "2024-02-09T10:07:47+00:00", + "value": 57, + "strVal": "57Hz", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "NIBEF F730 CU 3x400V", + "parameterId": "42770", + "parameterName": "Desired humid­ity", + "parameterUnit": "%", + "writable": false, + "timestamp": "2024-02-01T14:30:32+00:00", + "value": 0, + "strVal": "0%", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "NIBEF F730 CU 3x400V", + "parameterId": "43009", + "parameterName": "Calcu­lated supply climate system 1", + "parameterUnit": "°C", + "writable": false, + "timestamp": "2024-02-09T11:04:53+00:00", + "value": 37.9, + "strVal": "37.9°C", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "NIBEF F730 CU 3x400V", + "parameterId": "43066", + "parameterName": "Defrost­ing time", + "parameterUnit": "s", + "writable": false, + "timestamp": "2024-02-09T09:45:41+00:00", + "value": 0, + "strVal": "0s", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "NIBEF F730 CU 3x400V", + "parameterId": "43081", + "parameterName": "Time factor add heat", + "parameterUnit": "", + "writable": false, + "timestamp": "2024-02-09T10:59:11+00:00", + "value": 1686.9, + "strVal": "1686.9", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "NIBEF F730 CU 3x400V", + "parameterId": "43108", + "parameterName": "Current fan mode", + "parameterUnit": "%", + "writable": false, + "timestamp": "2024-02-08T16:27:27+00:00", + "value": 0, + "strVal": "0%", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "NIBEF F730 CU 3x400V", + "parameterId": "43109", + "parameterName": "Current hot water mode", + "parameterUnit": "", + "writable": false, + "timestamp": "2024-02-06T21:14:34+00:00", + "value": 0, + "strVal": "0", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "NIBEF F730 CU 3x400V", + "parameterId": "43115", + "parameterName": "Hot water: charge set point value", + "parameterUnit": "°C", + "writable": false, + "timestamp": "2024-02-01T14:30:32+00:00", + "value": 0, + "strVal": "0°C", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "NIBEF F730 CU 3x400V", + "parameterId": "43116", + "parameterName": "Hot water: charge current value ((BT12 | BT63))", + "parameterUnit": "°C", + "writable": false, + "timestamp": "2024-02-01T14:30:32+00:00", + "value": 0, + "strVal": "0°C", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "NIBEF F730 CU 3x400V", + "parameterId": "43122", + "parameterName": "Min com­pressor fre­quency", + "parameterUnit": "Hz", + "writable": false, + "timestamp": "2024-02-01T14:30:32+00:00", + "value": 20, + "strVal": "20Hz", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "NIBEF F730 CU 3x400V", + "parameterId": "43123", + "parameterName": "Max com­pressor fre­quency", + "parameterUnit": "Hz", + "writable": false, + "timestamp": "2024-02-09T10:07:44+00:00", + "value": 57, + "strVal": "57Hz", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "NIBEF F730 CU 3x400V", + "parameterId": "43124", + "parameterName": "Refer­ence, air speed sensor", + "parameterUnit": "m3/h", + "writable": false, + "timestamp": "2024-02-09T09:51:03+00:00", + "value": 127.6, + "strVal": "127.6m3/h", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "NIBEF F730 CU 3x400V", + "parameterId": "43125", + "parameterName": "De­crease from refer­ence value", + "parameterUnit": "%", + "writable": false, + "timestamp": "2024-02-09T11:08:28+00:00", + "value": -1.1, + "strVal": "-1.1%", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "NIBEF F730 CU 3x400V", + "parameterId": "43140", + "parameterName": "Invert­er temp­erature", + "parameterUnit": "°C", + "writable": false, + "timestamp": "2024-02-09T10:19:11+00:00", + "value": 37.2, + "strVal": "37.2°C", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "NIBEF F730 CU 3x400V", + "parameterId": "43146", + "parameterName": "dT Invert­er - exh air (BT20)", + "parameterUnit": "°C", + "writable": false, + "timestamp": "2024-02-09T10:37:53+00:00", + "value": 14.9, + "strVal": "14.9°C", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "0.1", + "zoneId": null + }, + { + "category": "NIBEF F730 CU 3x400V", + "parameterId": "43161", + "parameterName": "Extern. adjust­ment climate system 1", + "parameterUnit": "", + "writable": false, + "timestamp": "2024-02-01T14:30:32+00:00", + "value": 0, + "strVal": "Off", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "0", + "text": "Off", + "icon": "" + }, + { + "value": "1", + "text": "On", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "NIBEF F730 CU 3x400V", + "parameterId": "43427", + "parameterName": "Status com­pressor", + "parameterUnit": "", + "writable": false, + "timestamp": "2024-02-09T09:46:01+00:00", + "value": 60, + "strVal": "runs", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "20", + "text": "off", + "icon": "" + }, + { + "value": "40", + "text": "starts", + "icon": "" + }, + { + "value": "60", + "text": "runs", + "icon": "" + }, + { + "value": "100", + "text": "stops", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "NIBEF F730 CU 3x400V", + "parameterId": "43437", + "parameterName": "Heating medium pump speed (GP1)", + "parameterUnit": "%", + "writable": false, + "timestamp": "2024-02-09T10:34:44+00:00", + "value": 79, + "strVal": "79%", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "NIBEF F730 CU 3x400V", + "parameterId": "49633", + "parameterName": "Desired humid­ity", + "parameterUnit": "%", + "writable": false, + "timestamp": "2024-02-01T14:30:32+00:00", + "value": 50, + "strVal": "50%", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "NIBEF F730 CU 3x400V", + "parameterId": "49993", + "parameterName": "Int elec add heat", + "parameterUnit": "", + "writable": false, + "timestamp": "2024-02-08T18:57:28+00:00", + "value": 6, + "strVal": "Active", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "0", + "text": "Alarm", + "icon": "" + }, + { + "value": "1", + "text": "Alarm", + "icon": "" + }, + { + "value": "2", + "text": "Active", + "icon": "" + }, + { + "value": "3", + "text": "Off", + "icon": "" + }, + { + "value": "4", + "text": "Blocked", + "icon": "" + }, + { + "value": "5", + "text": "Off", + "icon": "" + }, + { + "value": "6", + "text": "Active", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "NIBEF F730 CU 3x400V", + "parameterId": "49994", + "parameterName": "Prior­ity", + "parameterUnit": "", + "writable": false, + "timestamp": "2024-02-08T19:13:05+00:00", + "value": 30, + "strVal": "Heating", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "10", + "text": "Off", + "icon": "" + }, + { + "value": "20", + "text": "Hot water", + "icon": "" + }, + { + "value": "30", + "text": "Heating", + "icon": "" + }, + { + "value": "40", + "text": "Pool", + "icon": "" + }, + { + "value": "41", + "text": "Pool 2", + "icon": "" + }, + { + "value": "50", + "text": "Trans­fer", + "icon": "" + }, + { + "value": "60", + "text": "Cooling", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "NIBEF F730 CU 3x400V", + "parameterId": "49995", + "parameterName": "Pump: Heating medium (GP1)", + "parameterUnit": "", + "writable": false, + "timestamp": "2024-02-01T14:30:32+00:00", + "value": 1, + "strVal": "On", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "0", + "text": "Off", + "icon": "" + }, + { + "value": "1", + "text": "On", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "NIBEF F730 CU 3x400V", + "parameterId": "50004", + "parameterName": "Tempo­rary lux", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-04T21:06:26+00:00", + "value": 0, + "strVal": "off", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "0", + "text": "off", + "icon": "" + }, + { + "value": "1", + "text": "on", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + }, + { + "category": "NIBEF F730 CU 3x400V", + "parameterId": "50005", + "parameterName": "In­creased venti­lation", + "parameterUnit": "", + "writable": true, + "timestamp": "2024-02-08T16:27:26+00:00", + "value": 0, + "strVal": "off", + "smartHomeCategories": [], + "minValue": null, + "maxValue": null, + "stepValue": 1, + "enumValues": [ + { + "value": "0", + "text": "off", + "icon": "" + }, + { + "value": "1", + "text": "on", + "icon": "" + } + ], + "scaleValue": "1", + "zoneId": null + } + ] + + ''', + }), + }), ]), 'itemsPerPage': 10, 'numItems': 1, @@ -1026,6 +2031,15 @@ dict({ 'country': 'Sweden', 'devices': list([ + dict({ + 'connectionState': 'Connected', + 'currentFwVersion': '9682R7', + 'id': 'robin-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff', + 'product': dict({ + 'name': 'F730 CU 3x400V', + 'serialNumber': '**REDACTED**', + }), + }), dict({ 'connectionState': 'Connected', 'currentFwVersion': '9682R7', diff --git a/tests/components/myuplink/test_binary_sensor.py b/tests/components/myuplink/test_binary_sensor.py index 24bfe49985d..19eb4a4f292 100644 --- a/tests/components/myuplink/test_binary_sensor.py +++ b/tests/components/myuplink/test_binary_sensor.py @@ -17,9 +17,9 @@ async def test_sensor_states( """Test sensor state.""" await setup_integration(hass, mock_config_entry) - state = hass.states.get("binary_sensor.f730_cu_3x400v_pump_heating_medium_gp1") + state = hass.states.get("binary_sensor.gotham_city_pump_heating_medium_gp1") assert state is not None assert state.state == "on" assert state.attributes == { - "friendly_name": "F730 CU 3x400V Pump: Heating medium (GP1)", + "friendly_name": "Gotham City Pump: Heating medium (GP1)", } diff --git a/tests/components/myuplink/test_init.py b/tests/components/myuplink/test_init.py index 728bcbc702f..328dc55d4ad 100644 --- a/tests/components/myuplink/test_init.py +++ b/tests/components/myuplink/test_init.py @@ -1,4 +1,5 @@ """Tests for init module.""" + import http import time from unittest.mock import MagicMock @@ -8,10 +9,11 @@ import pytest from homeassistant.components.myuplink.const import DOMAIN, OAUTH2_TOKEN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr from . import setup_integration -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, load_fixture from tests.test_util.aiohttp import AiohttpClientMocker @@ -66,3 +68,33 @@ async def test_expired_token_refresh_failure( await setup_integration(hass, mock_config_entry) assert mock_config_entry.state is expected_state + + +@pytest.mark.parametrize( + "load_systems_file", + [load_fixture("systems.json", DOMAIN)], +) +async def test_devices_created_count( + hass: HomeAssistant, + mock_myuplink_client: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test that one device is created.""" + await setup_integration(hass, mock_config_entry) + + device_registry = dr.async_get(hass) + + assert len(device_registry.devices) == 1 + + +async def test_devices_multiple_created_count( + hass: HomeAssistant, + mock_myuplink_client: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test that multiple device are created.""" + await setup_integration(hass, mock_config_entry) + + device_registry = dr.async_get(hass) + + assert len(device_registry.devices) == 2 diff --git a/tests/components/myuplink/test_number.py b/tests/components/myuplink/test_number.py index 158ef35dc77..899b2302b3c 100644 --- a/tests/components/myuplink/test_number.py +++ b/tests/components/myuplink/test_number.py @@ -14,9 +14,9 @@ from homeassistant.helpers import entity_registry as er TEST_PLATFORM = Platform.NUMBER pytestmark = pytest.mark.parametrize("platforms", [(TEST_PLATFORM,)]) -ENTITY_ID = "number.f730_cu_3x400v_degree_minutes" -ENTITY_FRIENDLY_NAME = "F730 CU 3x400V Degree minutes" -ENTITY_UID = "batman-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-40940" +ENTITY_ID = "number.gotham_city_degree_minutes" +ENTITY_FRIENDLY_NAME = "Gotham City Degree minutes" +ENTITY_UID = "robin-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-40940" async def test_entity_registry( @@ -42,7 +42,6 @@ async def test_attributes( assert state.state == "-875.0" assert state.attributes == { "friendly_name": ENTITY_FRIENDLY_NAME, - "icon": "mdi:thermometer-lines", "min": -3000, "max": 3000, "mode": "auto", @@ -85,3 +84,19 @@ async def test_api_failure( ) await hass.async_block_till_done() mock_myuplink_client.async_set_device_points.assert_called_once() + + +@pytest.mark.parametrize( + "load_device_points_file", + ["device_points_nibe_smo20.json"], +) +async def test_entity_registry_smo20( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + mock_myuplink_client: MagicMock, + setup_platform: None, +) -> None: + """Test that the entities are registered in the entity registry.""" + + entry = entity_registry.async_get("number.gotham_city_change_in_curve") + assert entry.unique_id == "robin-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-47028" diff --git a/tests/components/myuplink/test_sensor.py b/tests/components/myuplink/test_sensor.py index eb8c0f82bbd..8fecb787122 100644 --- a/tests/components/myuplink/test_sensor.py +++ b/tests/components/myuplink/test_sensor.py @@ -17,11 +17,11 @@ async def test_sensor_states( """Test sensor state.""" await setup_integration(hass, mock_config_entry) - state = hass.states.get("sensor.f730_cu_3x400v_average_outdoor_temp_bt1") + state = hass.states.get("sensor.gotham_city_average_outdoor_temp_bt1") assert state is not None assert state.state == "-12.2" assert state.attributes == { - "friendly_name": "F730 CU 3x400V Average outdoor temp (BT1)", + "friendly_name": "Gotham City Average outdoor temp (BT1)", "device_class": "temperature", "state_class": "measurement", "unit_of_measurement": "°C", diff --git a/tests/components/myuplink/test_switch.py b/tests/components/myuplink/test_switch.py index cbc60cbfc0a..efbc2c88371 100644 --- a/tests/components/myuplink/test_switch.py +++ b/tests/components/myuplink/test_switch.py @@ -19,9 +19,9 @@ from homeassistant.helpers import entity_registry as er TEST_PLATFORM = Platform.SWITCH pytestmark = pytest.mark.parametrize("platforms", [(TEST_PLATFORM,)]) -ENTITY_ID = "switch.f730_cu_3x400v_temporary_lux" -ENTITY_FRIENDLY_NAME = "F730 CU 3x400V Tempo­rary lux" -ENTITY_UID = "batman-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-50004" +ENTITY_ID = "switch.gotham_city_temporary_lux" +ENTITY_FRIENDLY_NAME = "Gotham City Tempo\xadrary lux" +ENTITY_UID = "robin-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-50004" async def test_entity_registry( @@ -47,7 +47,6 @@ async def test_attributes( assert state.state == STATE_OFF assert state.attributes == { "friendly_name": ENTITY_FRIENDLY_NAME, - "icon": "mdi:water-alert-outline", } @@ -95,3 +94,19 @@ async def test_api_failure( ) await hass.async_block_till_done() mock_myuplink_client.async_set_device_points.assert_called_once() + + +@pytest.mark.parametrize( + "load_device_points_file", + ["device_points_nibe_smo20.json"], +) +async def test_entity_registry_smo20( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + mock_myuplink_client: MagicMock, + setup_platform: None, +) -> None: + """Test that the entities are registered in the entity registry.""" + + entry = entity_registry.async_get(ENTITY_ID) + assert entry.unique_id == ENTITY_UID diff --git a/tests/components/myuplink/test_update.py b/tests/components/myuplink/test_update.py index f25993e8ef6..82f6ac17f69 100644 --- a/tests/components/myuplink/test_update.py +++ b/tests/components/myuplink/test_update.py @@ -17,6 +17,6 @@ async def test_update_states( """Test update state.""" await setup_integration(hass, mock_config_entry) - state = hass.states.get("update.f730_cu_3x400v_firmware") + state = hass.states.get("update.gotham_city_firmware") assert state is not None assert state.state == "off" diff --git a/tests/components/nam/__init__.py b/tests/components/nam/__init__.py index 0f5befcac09..0484fc12bd6 100644 --- a/tests/components/nam/__init__.py +++ b/tests/components/nam/__init__.py @@ -1,4 +1,5 @@ """Tests for the Nettigo Air Monitor integration.""" + from unittest.mock import AsyncMock, Mock, patch from homeassistant.components.nam.const import DOMAIN @@ -57,9 +58,12 @@ async def init_integration(hass, co2_sensor=True) -> MockConfigEntry: update_response = Mock(json=AsyncMock(return_value=nam_data)) - with patch("homeassistant.components.nam.NettigoAirMonitor.initialize"), patch( - "homeassistant.components.nam.NettigoAirMonitor._async_http_request", - return_value=update_response, + with ( + patch("homeassistant.components.nam.NettigoAirMonitor.initialize"), + patch( + "homeassistant.components.nam.NettigoAirMonitor._async_http_request", + return_value=update_response, + ), ): entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) diff --git a/tests/components/nam/test_button.py b/tests/components/nam/test_button.py index ab4e46975f9..39c37d57f89 100644 --- a/tests/components/nam/test_button.py +++ b/tests/components/nam/test_button.py @@ -1,4 +1,5 @@ """Test button of Nettigo Air Monitor integration.""" + from unittest.mock import patch from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, ButtonDeviceClass @@ -29,9 +30,12 @@ async def test_button_press(hass: HomeAssistant) -> None: await init_integration(hass) now = dt_util.utcnow() - with patch( - "homeassistant.components.nam.NettigoAirMonitor.async_restart" - ) as mock_restart, patch("homeassistant.core.dt_util.utcnow", return_value=now): + with ( + patch( + "homeassistant.components.nam.NettigoAirMonitor.async_restart" + ) as mock_restart, + patch("homeassistant.core.dt_util.utcnow", return_value=now), + ): await hass.services.async_call( BUTTON_DOMAIN, "press", diff --git a/tests/components/nam/test_config_flow.py b/tests/components/nam/test_config_flow.py index 9319eddba81..71bf3cf1525 100644 --- a/tests/components/nam/test_config_flow.py +++ b/tests/components/nam/test_config_flow.py @@ -1,4 +1,5 @@ """Define tests for the Nettigo Air Monitor config flow.""" + from ipaddress import ip_address from unittest.mock import patch @@ -37,15 +38,19 @@ async def test_form_create_entry_without_auth(hass: HomeAssistant) -> None: assert result["step_id"] == "user" assert result["errors"] == {} - with patch( - "homeassistant.components.nam.NettigoAirMonitor.async_check_credentials", - return_value=DEVICE_CONFIG, - ), patch( - "homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address", - return_value="aa:bb:cc:dd:ee:ff", - ), patch( - "homeassistant.components.nam.async_setup_entry", return_value=True - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.nam.NettigoAirMonitor.async_check_credentials", + return_value=DEVICE_CONFIG, + ), + patch( + "homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address", + return_value="aa:bb:cc:dd:ee:ff", + ), + patch( + "homeassistant.components.nam.async_setup_entry", return_value=True + ) as mock_setup_entry, + ): result = await hass.config_entries.flow.async_configure( result["flow_id"], VALID_CONFIG, @@ -67,15 +72,19 @@ async def test_form_create_entry_with_auth(hass: HomeAssistant) -> None: assert result["step_id"] == "user" assert result["errors"] == {} - with patch( - "homeassistant.components.nam.NettigoAirMonitor.async_check_credentials", - return_value=DEVICE_CONFIG_AUTH, - ), patch( - "homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address", - return_value="aa:bb:cc:dd:ee:ff", - ), patch( - "homeassistant.components.nam.async_setup_entry", return_value=True - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.nam.NettigoAirMonitor.async_check_credentials", + return_value=DEVICE_CONFIG_AUTH, + ), + patch( + "homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address", + return_value="aa:bb:cc:dd:ee:ff", + ), + patch( + "homeassistant.components.nam.async_setup_entry", return_value=True + ) as mock_setup_entry, + ): result = await hass.config_entries.flow.async_configure( result["flow_id"], VALID_CONFIG, @@ -108,12 +117,15 @@ async def test_reauth_successful(hass: HomeAssistant) -> None: ) entry.add_to_hass(hass) - with patch( - "homeassistant.components.nam.NettigoAirMonitor.async_check_credentials", - return_value=DEVICE_CONFIG_AUTH, - ), patch( - "homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address", - return_value="aa:bb:cc:dd:ee:ff", + with ( + patch( + "homeassistant.components.nam.NettigoAirMonitor.async_check_credentials", + return_value=DEVICE_CONFIG_AUTH, + ), + patch( + "homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address", + return_value="aa:bb:cc:dd:ee:ff", + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -177,12 +189,15 @@ async def test_reauth_unsuccessful(hass: HomeAssistant) -> None: async def test_form_with_auth_errors(hass: HomeAssistant, error) -> None: """Test we handle errors when auth is required.""" exc, base_error = error - with patch( - "homeassistant.components.nam.NettigoAirMonitor.async_check_credentials", - side_effect=AuthFailedError("Auth Error"), - ), patch( - "homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address", - return_value="aa:bb:cc:dd:ee:ff", + with ( + patch( + "homeassistant.components.nam.NettigoAirMonitor.async_check_credentials", + side_effect=AuthFailedError("Auth Error"), + ), + patch( + "homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address", + return_value="aa:bb:cc:dd:ee:ff", + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -231,12 +246,15 @@ async def test_form_errors(hass: HomeAssistant, error) -> None: async def test_form_abort(hass: HomeAssistant) -> None: """Test we handle abort after error.""" - with patch( - "homeassistant.components.nam.NettigoAirMonitor.async_check_credentials", - return_value=DEVICE_CONFIG, - ), patch( - "homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address", - side_effect=CannotGetMacError("Cannot get MAC address from device"), + with ( + patch( + "homeassistant.components.nam.NettigoAirMonitor.async_check_credentials", + return_value=DEVICE_CONFIG, + ), + patch( + "homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address", + side_effect=CannotGetMacError("Cannot get MAC address from device"), + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -259,12 +277,15 @@ async def test_form_already_configured(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - with patch( - "homeassistant.components.nam.NettigoAirMonitor.async_check_credentials", - return_value=DEVICE_CONFIG, - ), patch( - "homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address", - return_value="aa:bb:cc:dd:ee:ff", + with ( + patch( + "homeassistant.components.nam.NettigoAirMonitor.async_check_credentials", + return_value=DEVICE_CONFIG, + ), + patch( + "homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address", + return_value="aa:bb:cc:dd:ee:ff", + ), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -280,12 +301,15 @@ async def test_form_already_configured(hass: HomeAssistant) -> None: async def test_zeroconf(hass: HomeAssistant) -> None: """Test we get the form.""" - with patch( - "homeassistant.components.nam.NettigoAirMonitor.async_check_credentials", - return_value=DEVICE_CONFIG, - ), patch( - "homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address", - return_value="aa:bb:cc:dd:ee:ff", + with ( + patch( + "homeassistant.components.nam.NettigoAirMonitor.async_check_credentials", + return_value=DEVICE_CONFIG, + ), + patch( + "homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address", + return_value="aa:bb:cc:dd:ee:ff", + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -321,12 +345,15 @@ async def test_zeroconf(hass: HomeAssistant) -> None: async def test_zeroconf_with_auth(hass: HomeAssistant) -> None: """Test that the zeroconf step with auth works.""" - with patch( - "homeassistant.components.nam.NettigoAirMonitor.async_check_credentials", - side_effect=AuthFailedError("Auth Error"), - ), patch( - "homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address", - return_value="aa:bb:cc:dd:ee:ff", + with ( + patch( + "homeassistant.components.nam.NettigoAirMonitor.async_check_credentials", + side_effect=AuthFailedError("Auth Error"), + ), + patch( + "homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address", + return_value="aa:bb:cc:dd:ee:ff", + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -344,15 +371,19 @@ async def test_zeroconf_with_auth(hass: HomeAssistant) -> None: assert result["errors"] == {} assert context["title_placeholders"]["host"] == "10.10.2.3" - with patch( - "homeassistant.components.nam.NettigoAirMonitor.async_check_credentials", - return_value=DEVICE_CONFIG_AUTH, - ), patch( - "homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address", - return_value="aa:bb:cc:dd:ee:ff", - ), patch( - "homeassistant.components.nam.async_setup_entry", return_value=True - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.nam.NettigoAirMonitor.async_check_credentials", + return_value=DEVICE_CONFIG_AUTH, + ), + patch( + "homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address", + return_value="aa:bb:cc:dd:ee:ff", + ), + patch( + "homeassistant.components.nam.async_setup_entry", return_value=True + ) as mock_setup_entry, + ): result = await hass.config_entries.flow.async_configure( result["flow_id"], VALID_AUTH, diff --git a/tests/components/nam/test_diagnostics.py b/tests/components/nam/test_diagnostics.py index 998819128a9..9d13121392f 100644 --- a/tests/components/nam/test_diagnostics.py +++ b/tests/components/nam/test_diagnostics.py @@ -1,4 +1,5 @@ """Test NAM diagnostics.""" + import json from homeassistant.core import HomeAssistant diff --git a/tests/components/nam/test_init.py b/tests/components/nam/test_init.py index 63034d5b075..8b8c3a4835a 100644 --- a/tests/components/nam/test_init.py +++ b/tests/components/nam/test_init.py @@ -1,4 +1,5 @@ """Test init of Nettigo Air Monitor integration.""" + from unittest.mock import patch from nettigo_air_monitor import ApiError, AuthFailedError @@ -53,9 +54,12 @@ async def test_config_not_ready_while_checking_credentials(hass: HomeAssistant) ) entry.add_to_hass(hass) - with patch("homeassistant.components.nam.NettigoAirMonitor.initialize"), patch( - "homeassistant.components.nam.NettigoAirMonitor.async_check_credentials", - side_effect=ApiError("API Error"), + with ( + patch("homeassistant.components.nam.NettigoAirMonitor.initialize"), + patch( + "homeassistant.components.nam.NettigoAirMonitor.async_check_credentials", + side_effect=ApiError("API Error"), + ), ): await hass.config_entries.async_setup(entry.entry_id) assert entry.state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/nam/test_sensor.py b/tests/components/nam/test_sensor.py index 80eedd5b1a3..c88a34ae497 100644 --- a/tests/components/nam/test_sensor.py +++ b/tests/components/nam/test_sensor.py @@ -1,4 +1,5 @@ """Test sensor of Nettigo Air Monitor integration.""" + from datetime import timedelta from unittest.mock import AsyncMock, Mock, patch @@ -487,7 +488,7 @@ async def test_sensor_disabled( # Test enabling entity updated_entry = entity_registry.async_update_entity( - entry.entity_id, **{"disabled_by": None} + entry.entity_id, disabled_by=None ) assert updated_entry != entry @@ -506,9 +507,12 @@ async def test_incompleta_data_after_device_restart(hass: HomeAssistant) -> None future = utcnow() + timedelta(minutes=6) update_response = Mock(json=AsyncMock(return_value=INCOMPLETE_NAM_DATA)) - with patch("homeassistant.components.nam.NettigoAirMonitor.initialize"), patch( - "homeassistant.components.nam.NettigoAirMonitor._async_http_request", - return_value=update_response, + with ( + patch("homeassistant.components.nam.NettigoAirMonitor.initialize"), + patch( + "homeassistant.components.nam.NettigoAirMonitor._async_http_request", + return_value=update_response, + ), ): async_fire_time_changed(hass, future) await hass.async_block_till_done() @@ -528,9 +532,12 @@ async def test_availability(hass: HomeAssistant) -> None: assert state.state == "7.6" future = utcnow() + timedelta(minutes=6) - with patch("homeassistant.components.nam.NettigoAirMonitor.initialize"), patch( - "homeassistant.components.nam.NettigoAirMonitor._async_http_request", - side_effect=ApiError("API Error"), + with ( + patch("homeassistant.components.nam.NettigoAirMonitor.initialize"), + patch( + "homeassistant.components.nam.NettigoAirMonitor._async_http_request", + side_effect=ApiError("API Error"), + ), ): async_fire_time_changed(hass, future) await hass.async_block_till_done() @@ -541,9 +548,12 @@ async def test_availability(hass: HomeAssistant) -> None: future = utcnow() + timedelta(minutes=12) update_response = Mock(json=AsyncMock(return_value=nam_data)) - with patch("homeassistant.components.nam.NettigoAirMonitor.initialize"), patch( - "homeassistant.components.nam.NettigoAirMonitor._async_http_request", - return_value=update_response, + with ( + patch("homeassistant.components.nam.NettigoAirMonitor.initialize"), + patch( + "homeassistant.components.nam.NettigoAirMonitor._async_http_request", + return_value=update_response, + ), ): async_fire_time_changed(hass, future) await hass.async_block_till_done() @@ -561,10 +571,13 @@ async def test_manual_update_entity(hass: HomeAssistant) -> None: await async_setup_component(hass, "homeassistant", {}) update_response = Mock(json=AsyncMock(return_value=nam_data)) - with patch("homeassistant.components.nam.NettigoAirMonitor.initialize"), patch( - "homeassistant.components.nam.NettigoAirMonitor._async_http_request", - return_value=update_response, - ) as mock_get_data: + with ( + patch("homeassistant.components.nam.NettigoAirMonitor.initialize"), + patch( + "homeassistant.components.nam.NettigoAirMonitor._async_http_request", + return_value=update_response, + ) as mock_get_data, + ): await hass.services.async_call( "homeassistant", "update_entity", diff --git a/tests/components/namecheapdns/test_init.py b/tests/components/namecheapdns/test_init.py index 9776d196a11..fdd9081331f 100644 --- a/tests/components/namecheapdns/test_init.py +++ b/tests/components/namecheapdns/test_init.py @@ -1,4 +1,5 @@ """Test the NamecheapDNS component.""" + from datetime import timedelta import pytest diff --git a/tests/components/nanoleaf/test_config_flow.py b/tests/components/nanoleaf/test_config_flow.py index 2fce4e55bbc..5fe32c81eba 100644 --- a/tests/components/nanoleaf/test_config_flow.py +++ b/tests/components/nanoleaf/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Nanoleaf config flow.""" + from __future__ import annotations from ipaddress import ip_address @@ -108,11 +109,14 @@ async def test_user_error_setup_finish( assert result2["type"] == "form" assert result2["step_id"] == "link" - with patch( - "homeassistant.components.nanoleaf.config_flow.Nanoleaf.authorize", - ), patch( - "homeassistant.components.nanoleaf.config_flow.Nanoleaf.get_info", - side_effect=error, + with ( + patch( + "homeassistant.components.nanoleaf.config_flow.Nanoleaf.authorize", + ), + patch( + "homeassistant.components.nanoleaf.config_flow.Nanoleaf.get_info", + side_effect=error, + ), ): result3 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) assert result3["type"] == "abort" @@ -123,12 +127,15 @@ async def test_user_not_authorizing_new_tokens_user_step_link_step( hass: HomeAssistant, ) -> None: """Test we handle NotAuthorizingNewTokens in user step and link step.""" - with patch( - "homeassistant.components.nanoleaf.config_flow.Nanoleaf", - return_value=_mock_nanoleaf(authorize_error=Unauthorized()), - ) as mock_nanoleaf, patch( - "homeassistant.components.nanoleaf.async_setup_entry", return_value=True - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.nanoleaf.config_flow.Nanoleaf", + return_value=_mock_nanoleaf(authorize_error=Unauthorized()), + ) as mock_nanoleaf, + patch( + "homeassistant.components.nanoleaf.async_setup_entry", return_value=True + ) as mock_setup_entry, + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -228,11 +235,14 @@ async def test_discovery_link_unavailable( hass: HomeAssistant, source: type, type_in_discovery_info: str ) -> None: """Test discovery and abort if device is unavailable.""" - with patch( - "homeassistant.components.nanoleaf.config_flow.Nanoleaf.get_info", - ), patch( - "homeassistant.components.nanoleaf.config_flow.load_json_object", - return_value={}, + with ( + patch( + "homeassistant.components.nanoleaf.config_flow.Nanoleaf.get_info", + ), + patch( + "homeassistant.components.nanoleaf.config_flow.load_json_object", + return_value={}, + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -276,12 +286,15 @@ async def test_reauth(hass: HomeAssistant) -> None: ) entry.add_to_hass(hass) - with patch( - "homeassistant.components.nanoleaf.config_flow.Nanoleaf", - return_value=_mock_nanoleaf(), - ), patch( - "homeassistant.components.nanoleaf.async_setup_entry", - return_value=True, + with ( + patch( + "homeassistant.components.nanoleaf.config_flow.Nanoleaf", + return_value=_mock_nanoleaf(), + ), + patch( + "homeassistant.components.nanoleaf.async_setup_entry", + return_value=True, + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -353,22 +366,28 @@ async def test_import_discovery_integration( Test removing the .nanoleaf_conf file if it was the only device in the file. Test updating the .nanoleaf_conf file if it was not the only device in the file. """ - with patch( - "homeassistant.components.nanoleaf.config_flow.load_json_object", - return_value=dict(nanoleaf_conf_file), - ), patch( - "homeassistant.components.nanoleaf.config_flow.Nanoleaf", - return_value=_mock_nanoleaf(TEST_HOST, TEST_TOKEN), - ), patch( - "homeassistant.components.nanoleaf.config_flow.save_json", - return_value=None, - ) as mock_save_json, patch( - "homeassistant.components.nanoleaf.config_flow.os.remove", - return_value=None, - ) as mock_remove, patch( - "homeassistant.components.nanoleaf.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.nanoleaf.config_flow.load_json_object", + return_value=dict(nanoleaf_conf_file), + ), + patch( + "homeassistant.components.nanoleaf.config_flow.Nanoleaf", + return_value=_mock_nanoleaf(TEST_HOST, TEST_TOKEN), + ), + patch( + "homeassistant.components.nanoleaf.config_flow.save_json", + return_value=None, + ) as mock_save_json, + patch( + "homeassistant.components.nanoleaf.config_flow.os.remove", + return_value=None, + ) as mock_remove, + patch( + "homeassistant.components.nanoleaf.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": source}, @@ -403,16 +422,20 @@ async def test_import_discovery_integration( async def test_ssdp_discovery(hass: HomeAssistant) -> None: """Test SSDP discovery.""" - with patch( - "homeassistant.components.nanoleaf.config_flow.load_json_object", - return_value={}, - ), patch( - "homeassistant.components.nanoleaf.config_flow.Nanoleaf", - return_value=_mock_nanoleaf(TEST_HOST, TEST_TOKEN), - ), patch( - "homeassistant.components.nanoleaf.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.nanoleaf.config_flow.load_json_object", + return_value={}, + ), + patch( + "homeassistant.components.nanoleaf.config_flow.Nanoleaf", + return_value=_mock_nanoleaf(TEST_HOST, TEST_TOKEN), + ), + patch( + "homeassistant.components.nanoleaf.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, diff --git a/tests/components/neato/test_config_flow.py b/tests/components/neato/test_config_flow.py index 191721c2e74..34b1f37f56f 100644 --- a/tests/components/neato/test_config_flow.py +++ b/tests/components/neato/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Neato Botvac config flow.""" + from unittest.mock import patch from pybotvac.neato import Neato diff --git a/tests/components/ness_alarm/test_init.py b/tests/components/ness_alarm/test_init.py index 5bf48e0667e..fb003d253de 100644 --- a/tests/components/ness_alarm/test_init.py +++ b/tests/components/ness_alarm/test_init.py @@ -1,4 +1,5 @@ """Tests for the ness_alarm component.""" + from unittest.mock import MagicMock, patch from nessclient import ArmingMode, ArmingState diff --git a/tests/components/nest/conftest.py b/tests/components/nest/conftest.py index 6057db382b5..68c77cb7635 100644 --- a/tests/components/nest/conftest.py +++ b/tests/components/nest/conftest.py @@ -1,4 +1,5 @@ """Common libraries for test setup.""" + from __future__ import annotations from collections.abc import Generator diff --git a/tests/components/nest/test_api.py b/tests/components/nest/test_api.py index 91975f3559f..3e0932c607d 100644 --- a/tests/components/nest/test_api.py +++ b/tests/components/nest/test_api.py @@ -3,10 +3,11 @@ There are two interesting cases to exercise that have different strategies for token refresh and for testing: - API based requests, tested using aioclient_mock -- Pub/sub subcriber initialization, intercepted with patch() +- Pub/sub subscriber initialization, intercepted with patch() The tests below exercise both cases during integration setup. """ + import time from unittest.mock import patch @@ -74,7 +75,7 @@ async def test_auth( (method, url, data, headers) = calls[1] assert headers == {"Authorization": f"Bearer {FAKE_TOKEN}"} - # Verify the susbcriber was created with the correct credentials + # Verify the subscriber was created with the correct credentials assert len(new_subscriber_mock.mock_calls) == 1 assert captured_creds creds = captured_creds diff --git a/tests/components/nest/test_camera.py b/tests/components/nest/test_camera.py index 647a3419501..b68173be201 100644 --- a/tests/components/nest/test_camera.py +++ b/tests/components/nest/test_camera.py @@ -3,6 +3,7 @@ These tests fake out the subscriber/devicemanager, and are not using a real pubsub subscriber. """ + import datetime from http import HTTPStatus from unittest.mock import AsyncMock, Mock, patch diff --git a/tests/components/nest/test_config_flow.py b/tests/components/nest/test_config_flow.py index 39ef42477fb..8d2a9e96d63 100644 --- a/tests/components/nest/test_config_flow.py +++ b/tests/components/nest/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Google Nest Device Access config flow.""" + from __future__ import annotations from http import HTTPStatus diff --git a/tests/components/nest/test_device_trigger.py b/tests/components/nest/test_device_trigger.py index 381cddb2817..51cf4254614 100644 --- a/tests/components/nest/test_device_trigger.py +++ b/tests/components/nest/test_device_trigger.py @@ -1,4 +1,5 @@ """The tests for Nest device triggers.""" + from google_nest_sdm.event import EventMessage import pytest from pytest_unordered import unordered diff --git a/tests/components/nest/test_diagnostics.py b/tests/components/nest/test_diagnostics.py index c9b5f2f0de1..5fb33ff4a47 100644 --- a/tests/components/nest/test_diagnostics.py +++ b/tests/components/nest/test_diagnostics.py @@ -1,4 +1,5 @@ """Test nest diagnostics.""" + from unittest.mock import patch from google_nest_sdm.exceptions import SubscriberException diff --git a/tests/components/nest/test_events.py b/tests/components/nest/test_events.py index 43ed3d489b5..caa86a3d93b 100644 --- a/tests/components/nest/test_events.py +++ b/tests/components/nest/test_events.py @@ -3,6 +3,7 @@ These tests fake out the subscriber/devicemanager, and are not using a real pubsub subscriber. """ + from __future__ import annotations from collections.abc import Mapping @@ -460,9 +461,12 @@ async def test_structure_update_event( }, auth=None, ) - with patch("homeassistant.components.nest.PLATFORMS", [PLATFORM]), patch( - "homeassistant.components.nest.api.GoogleNestSubscriber", - return_value=subscriber, + with ( + patch("homeassistant.components.nest.PLATFORMS", [PLATFORM]), + patch( + "homeassistant.components.nest.api.GoogleNestSubscriber", + return_value=subscriber, + ), ): await subscriber.async_receive_event(message) await hass.async_block_till_done() diff --git a/tests/components/nest/test_init.py b/tests/components/nest/test_init.py index 1e3eed91f19..3cac8649c9c 100644 --- a/tests/components/nest/test_init.py +++ b/tests/components/nest/test_init.py @@ -7,6 +7,7 @@ By default all tests use test fixtures that run in each possible configuration mode (e.g. yaml, ConfigEntry, etc) however some tests override and just run in relevant modes. """ + import logging from typing import Any from unittest.mock import patch @@ -122,11 +123,12 @@ async def test_setup_device_manager_failure( hass: HomeAssistant, caplog, setup_base_platform ) -> None: """Test device manager api failure.""" - with patch( - "homeassistant.components.nest.api.GoogleNestSubscriber.start_async" - ), patch( - "homeassistant.components.nest.api.GoogleNestSubscriber.async_get_device_manager", - side_effect=ApiException(), + with ( + patch("homeassistant.components.nest.api.GoogleNestSubscriber.start_async"), + patch( + "homeassistant.components.nest.api.GoogleNestSubscriber.async_get_device_manager", + side_effect=ApiException(), + ), ): await setup_base_platform() @@ -160,7 +162,7 @@ async def test_subscriber_auth_failure( async def test_setup_missing_subscriber_id( hass: HomeAssistant, warning_caplog, setup_base_platform ) -> None: - """Test missing susbcriber id from configuration.""" + """Test missing subscriber id from configuration.""" await setup_base_platform() assert "Configuration option" in warning_caplog.text @@ -226,11 +228,12 @@ async def test_remove_entry( assert entry.data.get("subscriber_id") == SUBSCRIBER_ID assert entry.data.get("project_id") == PROJECT_ID - with patch( - "homeassistant.components.nest.api.GoogleNestSubscriber.subscriber_id" - ), patch( - "homeassistant.components.nest.api.GoogleNestSubscriber.delete_subscription", - ) as delete: + with ( + patch("homeassistant.components.nest.api.GoogleNestSubscriber.subscriber_id"), + patch( + "homeassistant.components.nest.api.GoogleNestSubscriber.delete_subscription", + ) as delete, + ): assert await hass.config_entries.async_remove(entry.entry_id) assert delete.called diff --git a/tests/components/nest/test_media_source.py b/tests/components/nest/test_media_source.py index a1c62799585..4810c8e2ff5 100644 --- a/tests/components/nest/test_media_source.py +++ b/tests/components/nest/test_media_source.py @@ -3,6 +3,7 @@ These tests simulate recent camera events received by the subscriber exposed as media in the media source. """ + from collections.abc import Generator import datetime from http import HTTPStatus @@ -1052,7 +1053,7 @@ async def test_multiple_devices( assert len(browse.children) == 0 # Send events for device #1 - for i in range(0, 5): + for i in range(5): auth.responses = [ aiohttp.web.json_response(GENERATE_IMAGE_URL_RESPONSE), aiohttp.web.Response(body=IMAGE_BYTES_FROM_EVENT), @@ -1077,7 +1078,7 @@ async def test_multiple_devices( assert len(browse.children) == 0 # Send events for device #2 - for i in range(0, 3): + for i in range(3): auth.responses = [ aiohttp.web.json_response(GENERATE_IMAGE_URL_RESPONSE), aiohttp.web.Response(body=IMAGE_BYTES_FROM_EVENT), @@ -1339,7 +1340,7 @@ async def test_camera_event_media_eviction( assert len(browse.children) == 0 event_timestamp = dt_util.now() - for i in range(0, 7): + for i in range(7): auth.responses = [aiohttp.web.Response(body=f"image-bytes-{i}".encode())] ts = event_timestamp + datetime.timedelta(seconds=i) await subscriber.async_receive_event( diff --git a/tests/components/netatmo/common.py b/tests/components/netatmo/common.py index 1bb2ab00d32..08c8679acf3 100644 --- a/tests/components/netatmo/common.py +++ b/tests/components/netatmo/common.py @@ -1,4 +1,5 @@ """Common methods used across tests for Netatmo.""" + from contextlib import contextmanager import json from typing import Any @@ -110,11 +111,13 @@ async def simulate_webhook(hass: HomeAssistant, webhook_id: str, response) -> No @contextmanager def selected_platforms(platforms: list[Platform]) -> AsyncMock: """Restrict loaded platforms to list given.""" - with patch( - "homeassistant.components.netatmo.data_handler.PLATFORMS", platforms - ), patch( - "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", - ), patch( - "homeassistant.components.netatmo.webhook_generate_url", + with ( + patch("homeassistant.components.netatmo.data_handler.PLATFORMS", platforms), + patch( + "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", + ), + patch( + "homeassistant.components.netatmo.webhook_generate_url", + ), ): yield diff --git a/tests/components/netatmo/conftest.py b/tests/components/netatmo/conftest.py index a21bb8aebe7..d2e6c1fdc88 100644 --- a/tests/components/netatmo/conftest.py +++ b/tests/components/netatmo/conftest.py @@ -1,4 +1,5 @@ """Provide common Netatmo fixtures.""" + from time import time from unittest.mock import AsyncMock, patch diff --git a/tests/components/netatmo/snapshots/test_camera.ambr b/tests/components/netatmo/snapshots/test_camera.ambr index d989d029aa8..94a5ded5031 100644 --- a/tests/components/netatmo/snapshots/test_camera.ambr +++ b/tests/components/netatmo/snapshots/test_camera.ambr @@ -55,6 +55,7 @@ 'context': , 'entity_id': 'camera.front', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'streaming', }) @@ -115,6 +116,7 @@ 'context': , 'entity_id': 'camera.hall', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'streaming', }) @@ -173,6 +175,7 @@ 'context': , 'entity_id': 'camera.netatmo_doorbell', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'idle', }) diff --git a/tests/components/netatmo/snapshots/test_climate.ambr b/tests/components/netatmo/snapshots/test_climate.ambr index 32f9209c3c2..327595e90a5 100644 --- a/tests/components/netatmo/snapshots/test_climate.ambr +++ b/tests/components/netatmo/snapshots/test_climate.ambr @@ -69,6 +69,7 @@ 'context': , 'entity_id': 'climate.bureau', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unavailable', }) @@ -149,6 +150,7 @@ 'context': , 'entity_id': 'climate.cocina', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'auto', }) @@ -228,6 +230,7 @@ 'context': , 'entity_id': 'climate.corridor', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'auto', }) @@ -308,6 +311,7 @@ 'context': , 'entity_id': 'climate.entrada', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'auto', }) @@ -389,6 +393,7 @@ 'context': , 'entity_id': 'climate.livingroom', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'auto', }) diff --git a/tests/components/netatmo/snapshots/test_cover.ambr b/tests/components/netatmo/snapshots/test_cover.ambr index c05e246c02c..e907985ab39 100644 --- a/tests/components/netatmo/snapshots/test_cover.ambr +++ b/tests/components/netatmo/snapshots/test_cover.ambr @@ -44,6 +44,7 @@ 'context': , 'entity_id': 'cover.bubendorff_blind', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'closed', }) @@ -93,6 +94,7 @@ 'context': , 'entity_id': 'cover.entrance_blinds', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'closed', }) diff --git a/tests/components/netatmo/snapshots/test_fan.ambr b/tests/components/netatmo/snapshots/test_fan.ambr index d750dffb1fe..958a8f79704 100644 --- a/tests/components/netatmo/snapshots/test_fan.ambr +++ b/tests/components/netatmo/snapshots/test_fan.ambr @@ -52,6 +52,7 @@ 'context': , 'entity_id': 'fan.centralized_ventilation_controler', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }) diff --git a/tests/components/netatmo/snapshots/test_light.ambr b/tests/components/netatmo/snapshots/test_light.ambr index ed628e4fd7a..dabc7f8528f 100644 --- a/tests/components/netatmo/snapshots/test_light.ambr +++ b/tests/components/netatmo/snapshots/test_light.ambr @@ -50,6 +50,7 @@ 'context': , 'entity_id': 'light.bathroom_light', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }) @@ -104,6 +105,7 @@ 'context': , 'entity_id': 'light.front', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unavailable', }) @@ -160,6 +162,7 @@ 'context': , 'entity_id': 'light.unknown_00_11_22_33_00_11_45_fe', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }) diff --git a/tests/components/netatmo/snapshots/test_select.ambr b/tests/components/netatmo/snapshots/test_select.ambr index 65c98ec29f5..0a95049957e 100644 --- a/tests/components/netatmo/snapshots/test_select.ambr +++ b/tests/components/netatmo/snapshots/test_select.ambr @@ -50,6 +50,7 @@ 'context': , 'entity_id': 'select.myhome', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'Default', }) diff --git a/tests/components/netatmo/snapshots/test_sensor.ambr b/tests/components/netatmo/snapshots/test_sensor.ambr index 8983a1ecf13..df92c644588 100644 --- a/tests/components/netatmo/snapshots/test_sensor.ambr +++ b/tests/components/netatmo/snapshots/test_sensor.ambr @@ -48,6 +48,7 @@ 'context': , 'entity_id': 'sensor.baby_bedroom_co2', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '1053', }) @@ -97,6 +98,7 @@ 'context': , 'entity_id': 'sensor.baby_bedroom_health', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'Fine', }) @@ -150,6 +152,7 @@ 'context': , 'entity_id': 'sensor.baby_bedroom_humidity', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '66', }) @@ -203,6 +206,7 @@ 'context': , 'entity_id': 'sensor.baby_bedroom_noise', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '45', }) @@ -259,6 +263,7 @@ 'context': , 'entity_id': 'sensor.baby_bedroom_pressure', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '1021.4', }) @@ -384,6 +389,7 @@ 'context': , 'entity_id': 'sensor.baby_bedroom_temperature', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '21.6', }) @@ -507,6 +513,7 @@ 'context': , 'entity_id': 'sensor.bedroom_co2', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unavailable', }) @@ -554,6 +561,7 @@ 'context': , 'entity_id': 'sensor.bedroom_health', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unavailable', }) @@ -605,6 +613,7 @@ 'context': , 'entity_id': 'sensor.bedroom_humidity', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unavailable', }) @@ -656,6 +665,7 @@ 'context': , 'entity_id': 'sensor.bedroom_noise', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unavailable', }) @@ -710,6 +720,7 @@ 'context': , 'entity_id': 'sensor.bedroom_pressure', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unavailable', }) @@ -833,6 +844,7 @@ 'context': , 'entity_id': 'sensor.bedroom_temperature', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unavailable', }) @@ -956,6 +968,7 @@ 'context': , 'entity_id': 'sensor.bureau_modulate_battery_percent', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '90', }) @@ -1007,6 +1020,7 @@ 'context': , 'entity_id': 'sensor.cold_water_power', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unavailable', }) @@ -1094,6 +1108,7 @@ 'context': , 'entity_id': 'sensor.consumption_meter_power', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '476', }) @@ -1181,6 +1196,7 @@ 'context': , 'entity_id': 'sensor.corridor_humidity', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '67', }) @@ -1232,6 +1248,7 @@ 'context': , 'entity_id': 'sensor.ecocompteur_power', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unavailable', }) @@ -1319,6 +1336,7 @@ 'context': , 'entity_id': 'sensor.gas_power', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unavailable', }) @@ -1522,6 +1540,7 @@ 'context': , 'entity_id': 'sensor.home_avg_humidity', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '63.2', }) @@ -1578,6 +1597,7 @@ 'context': , 'entity_id': 'sensor.home_avg_pressure', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '1010.4', }) @@ -1631,6 +1651,7 @@ 'context': , 'entity_id': 'sensor.home_avg_rain', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0.1', }) @@ -1722,6 +1743,7 @@ 'context': , 'entity_id': 'sensor.home_avg_rain_today', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '11.3', }) @@ -1775,6 +1797,7 @@ 'context': , 'entity_id': 'sensor.home_avg_temperature', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '22.7', }) @@ -1828,6 +1851,7 @@ 'context': , 'entity_id': 'sensor.home_avg_wind_strength', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '15.0', }) @@ -1995,6 +2019,7 @@ 'context': , 'entity_id': 'sensor.home_max_humidity', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '76', }) @@ -2051,6 +2076,7 @@ 'context': , 'entity_id': 'sensor.home_max_pressure', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '1014.4', }) @@ -2104,6 +2130,7 @@ 'context': , 'entity_id': 'sensor.home_max_rain', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0.5', }) @@ -2195,6 +2222,7 @@ 'context': , 'entity_id': 'sensor.home_max_rain_today', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '12.322', }) @@ -2248,6 +2276,7 @@ 'context': , 'entity_id': 'sensor.home_max_temperature', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '27.4', }) @@ -2301,6 +2330,7 @@ 'context': , 'entity_id': 'sensor.home_max_wind_strength', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '15', }) @@ -2352,6 +2382,7 @@ 'context': , 'entity_id': 'sensor.hot_water_power', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unavailable', }) @@ -2439,6 +2470,7 @@ 'context': , 'entity_id': 'sensor.kitchen_co2', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unavailable', }) @@ -2486,6 +2518,7 @@ 'context': , 'entity_id': 'sensor.kitchen_health', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unavailable', }) @@ -2537,6 +2570,7 @@ 'context': , 'entity_id': 'sensor.kitchen_humidity', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unavailable', }) @@ -2588,6 +2622,7 @@ 'context': , 'entity_id': 'sensor.kitchen_noise', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unavailable', }) @@ -2642,6 +2677,7 @@ 'context': , 'entity_id': 'sensor.kitchen_pressure', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unavailable', }) @@ -2765,6 +2801,7 @@ 'context': , 'entity_id': 'sensor.kitchen_temperature', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unavailable', }) @@ -2888,6 +2925,7 @@ 'context': , 'entity_id': 'sensor.line_1_power', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unavailable', }) @@ -2975,6 +3013,7 @@ 'context': , 'entity_id': 'sensor.line_2_power', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unavailable', }) @@ -3062,6 +3101,7 @@ 'context': , 'entity_id': 'sensor.line_3_power', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unavailable', }) @@ -3149,6 +3189,7 @@ 'context': , 'entity_id': 'sensor.line_4_power', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unavailable', }) @@ -3236,6 +3277,7 @@ 'context': , 'entity_id': 'sensor.line_5_power', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unavailable', }) @@ -3323,6 +3365,7 @@ 'context': , 'entity_id': 'sensor.livingroom_battery_percent', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '75', }) @@ -3374,6 +3417,7 @@ 'context': , 'entity_id': 'sensor.livingroom_co2', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unavailable', }) @@ -3421,6 +3465,7 @@ 'context': , 'entity_id': 'sensor.livingroom_health', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unavailable', }) @@ -3472,6 +3517,7 @@ 'context': , 'entity_id': 'sensor.livingroom_humidity', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unavailable', }) @@ -3523,6 +3569,7 @@ 'context': , 'entity_id': 'sensor.livingroom_noise', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unavailable', }) @@ -3577,6 +3624,7 @@ 'context': , 'entity_id': 'sensor.livingroom_pressure', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unavailable', }) @@ -3700,6 +3748,7 @@ 'context': , 'entity_id': 'sensor.livingroom_temperature', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unavailable', }) @@ -3825,6 +3874,7 @@ 'context': , 'entity_id': 'sensor.parents_bedroom_co2', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '494', }) @@ -3874,6 +3924,7 @@ 'context': , 'entity_id': 'sensor.parents_bedroom_health', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'Fine', }) @@ -3927,6 +3978,7 @@ 'context': , 'entity_id': 'sensor.parents_bedroom_humidity', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '63', }) @@ -3980,6 +4032,7 @@ 'context': , 'entity_id': 'sensor.parents_bedroom_noise', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '42', }) @@ -4036,6 +4089,7 @@ 'context': , 'entity_id': 'sensor.parents_bedroom_pressure', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '1014.5', }) @@ -4161,6 +4215,7 @@ 'context': , 'entity_id': 'sensor.parents_bedroom_temperature', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '20.3', }) @@ -4284,6 +4339,7 @@ 'context': , 'entity_id': 'sensor.prise_power', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0', }) @@ -4371,6 +4427,7 @@ 'context': , 'entity_id': 'sensor.total_power', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unavailable', }) @@ -4458,6 +4515,7 @@ 'context': , 'entity_id': 'sensor.valve1_battery_percent', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '90', }) @@ -4509,6 +4567,7 @@ 'context': , 'entity_id': 'sensor.valve2_battery_percent', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '90', }) @@ -4560,6 +4619,7 @@ 'context': , 'entity_id': 'sensor.villa_bathroom_battery_percent', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '55', }) @@ -4611,6 +4671,7 @@ 'context': , 'entity_id': 'sensor.villa_bathroom_co2', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '1930', }) @@ -4662,6 +4723,7 @@ 'context': , 'entity_id': 'sensor.villa_bathroom_humidity', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '55', }) @@ -4785,6 +4847,7 @@ 'context': , 'entity_id': 'sensor.villa_bathroom_temperature', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '19.4', }) @@ -4872,6 +4935,7 @@ 'context': , 'entity_id': 'sensor.villa_bedroom_battery_percent', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '28', }) @@ -4923,6 +4987,7 @@ 'context': , 'entity_id': 'sensor.villa_bedroom_co2', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '1076', }) @@ -4974,6 +5039,7 @@ 'context': , 'entity_id': 'sensor.villa_bedroom_humidity', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '53', }) @@ -5097,6 +5163,7 @@ 'context': , 'entity_id': 'sensor.villa_bedroom_temperature', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '19.3', }) @@ -5186,6 +5253,7 @@ 'context': , 'entity_id': 'sensor.villa_co2', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '1339', }) @@ -5275,6 +5343,7 @@ 'context': , 'entity_id': 'sensor.villa_garden_battery_percent', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '85', }) @@ -5322,6 +5391,7 @@ 'context': , 'entity_id': 'sensor.villa_garden_direction', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'SW', }) @@ -5557,6 +5627,7 @@ 'context': , 'entity_id': 'sensor.villa_garden_wind_strength', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '4', }) @@ -5610,6 +5681,7 @@ 'context': , 'entity_id': 'sensor.villa_humidity', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '45', }) @@ -5663,6 +5735,7 @@ 'context': , 'entity_id': 'sensor.villa_noise', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '35', }) @@ -5714,6 +5787,7 @@ 'context': , 'entity_id': 'sensor.villa_outdoor_battery_percent', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unavailable', }) @@ -5765,6 +5839,7 @@ 'context': , 'entity_id': 'sensor.villa_outdoor_humidity', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unavailable', }) @@ -5888,6 +5963,7 @@ 'context': , 'entity_id': 'sensor.villa_outdoor_temperature', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unavailable', }) @@ -5980,6 +6056,7 @@ 'context': , 'entity_id': 'sensor.villa_pressure', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '1026.8', }) @@ -6067,6 +6144,7 @@ 'context': , 'entity_id': 'sensor.villa_rain_battery_percent', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '21', }) @@ -6154,6 +6232,7 @@ 'context': , 'entity_id': 'sensor.villa_rain_rain', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '3.7', }) @@ -6243,6 +6322,7 @@ 'context': , 'entity_id': 'sensor.villa_rain_rain_today', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '6.9', }) @@ -6368,6 +6448,7 @@ 'context': , 'entity_id': 'sensor.villa_temperature', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '21.1', }) diff --git a/tests/components/netatmo/snapshots/test_switch.ambr b/tests/components/netatmo/snapshots/test_switch.ambr index 06f40bcd379..22c41aefd42 100644 --- a/tests/components/netatmo/snapshots/test_switch.ambr +++ b/tests/components/netatmo/snapshots/test_switch.ambr @@ -41,6 +41,7 @@ 'context': , 'entity_id': 'switch.prise', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }) diff --git a/tests/components/netatmo/test_camera.py b/tests/components/netatmo/test_camera.py index e845ca08f06..c7398d64e1d 100644 --- a/tests/components/netatmo/test_camera.py +++ b/tests/components/netatmo/test_camera.py @@ -1,4 +1,5 @@ """The tests for Netatmo camera.""" + from datetime import timedelta from typing import Any from unittest.mock import AsyncMock, patch @@ -381,9 +382,10 @@ async def test_service_set_camera_light_invalid_type( "camera_light_mode": "on", } - with patch("pyatmo.home.Home.async_set_state") as mock_set_state, pytest.raises( - HomeAssistantError - ) as excinfo: + with ( + patch("pyatmo.home.Home.async_set_state") as mock_set_state, + pytest.raises(HomeAssistantError) as excinfo, + ): await hass.services.async_call( "netatmo", SERVICE_SET_CAMERA_LIGHT, @@ -408,15 +410,18 @@ async def test_camera_reconnect_webhook( fake_post_hits += 1 return await fake_post_request(*args, **kwargs) - with patch( - "homeassistant.components.netatmo.api.AsyncConfigEntryNetatmoAuth" - ) as mock_auth, patch( - "homeassistant.components.netatmo.data_handler.PLATFORMS", ["camera"] - ), patch( - "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", - ), patch( - "homeassistant.components.netatmo.webhook_generate_url", - ) as mock_webhook: + with ( + patch( + "homeassistant.components.netatmo.api.AsyncConfigEntryNetatmoAuth" + ) as mock_auth, + patch("homeassistant.components.netatmo.data_handler.PLATFORMS", ["camera"]), + patch( + "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", + ), + patch( + "homeassistant.components.netatmo.webhook_generate_url", + ) as mock_webhook, + ): mock_auth.return_value.async_post_api_request.side_effect = fake_post mock_auth.return_value.async_addwebhook.side_effect = AsyncMock() mock_auth.return_value.async_dropwebhook.side_effect = AsyncMock() @@ -504,14 +509,17 @@ async def test_setup_component_no_devices( fake_post_hits += 1 return await fake_post_request(*args, **kwargs) - with patch( - "homeassistant.components.netatmo.api.AsyncConfigEntryNetatmoAuth" - ) as mock_auth, patch( - "homeassistant.components.netatmo.data_handler.PLATFORMS", ["camera"] - ), patch( - "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", - ), patch( - "homeassistant.components.netatmo.webhook_generate_url", + with ( + patch( + "homeassistant.components.netatmo.api.AsyncConfigEntryNetatmoAuth" + ) as mock_auth, + patch("homeassistant.components.netatmo.data_handler.PLATFORMS", ["camera"]), + patch( + "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", + ), + patch( + "homeassistant.components.netatmo.webhook_generate_url", + ), ): mock_auth.return_value.async_post_api_request.side_effect = fake_post_no_data mock_auth.return_value.async_addwebhook.side_effect = AsyncMock() @@ -540,18 +548,21 @@ async def test_camera_image_raises_exception( endpoint = kwargs["endpoint"].split("/")[-1] if "snapshot_720.jpg" in endpoint: - raise pyatmo.ApiError() + raise pyatmo.ApiError return await fake_post_request(*args, **kwargs) - with patch( - "homeassistant.components.netatmo.api.AsyncConfigEntryNetatmoAuth" - ) as mock_auth, patch( - "homeassistant.components.netatmo.data_handler.PLATFORMS", ["camera"] - ), patch( - "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", - ), patch( - "homeassistant.components.netatmo.webhook_generate_url", + with ( + patch( + "homeassistant.components.netatmo.api.AsyncConfigEntryNetatmoAuth" + ) as mock_auth, + patch("homeassistant.components.netatmo.data_handler.PLATFORMS", ["camera"]), + patch( + "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", + ), + patch( + "homeassistant.components.netatmo.webhook_generate_url", + ), ): mock_auth.return_value.async_post_api_request.side_effect = fake_post mock_auth.return_value.async_get_image.side_effect = fake_post diff --git a/tests/components/netatmo/test_climate.py b/tests/components/netatmo/test_climate.py index e4b8c298c26..b25f78b5e2f 100644 --- a/tests/components/netatmo/test_climate.py +++ b/tests/components/netatmo/test_climate.py @@ -1,4 +1,5 @@ """The tests for the Netatmo climate platform.""" + from datetime import timedelta from unittest.mock import AsyncMock, patch diff --git a/tests/components/netatmo/test_config_flow.py b/tests/components/netatmo/test_config_flow.py index afa9ed02645..7866e448734 100644 --- a/tests/components/netatmo/test_config_flow.py +++ b/tests/components/netatmo/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Netatmo config flow.""" + from ipaddress import ip_address from unittest.mock import patch diff --git a/tests/components/netatmo/test_cover.py b/tests/components/netatmo/test_cover.py index 5a7c33fc6ef..509c1de736e 100644 --- a/tests/components/netatmo/test_cover.py +++ b/tests/components/netatmo/test_cover.py @@ -1,4 +1,5 @@ """The tests for Netatmo cover.""" + from unittest.mock import AsyncMock, patch from syrupy import SnapshotAssertion diff --git a/tests/components/netatmo/test_device_trigger.py b/tests/components/netatmo/test_device_trigger.py index ebafd313ff4..f7c31d7681c 100644 --- a/tests/components/netatmo/test_device_trigger.py +++ b/tests/components/netatmo/test_device_trigger.py @@ -1,4 +1,5 @@ """The tests for Netatmo device triggers.""" + import pytest from pytest_unordered import unordered @@ -62,18 +63,18 @@ async def test_get_triggers( expected_triggers = [] for event_type in event_types: if event_type in SUBTYPES: - for subtype in SUBTYPES[event_type]: - expected_triggers.append( - { - "platform": "device", - "domain": NETATMO_DOMAIN, - "type": event_type, - "subtype": subtype, - "device_id": device_entry.id, - "entity_id": entity_entry.id, - "metadata": {"secondary": False}, - } - ) + expected_triggers.extend( + { + "platform": "device", + "domain": NETATMO_DOMAIN, + "type": event_type, + "subtype": subtype, + "device_id": device_entry.id, + "entity_id": entity_entry.id, + "metadata": {"secondary": False}, + } + for subtype in SUBTYPES[event_type] + ) else: expected_triggers.append( { diff --git a/tests/components/netatmo/test_diagnostics.py b/tests/components/netatmo/test_diagnostics.py index 2d13e36150d..48f021295e1 100644 --- a/tests/components/netatmo/test_diagnostics.py +++ b/tests/components/netatmo/test_diagnostics.py @@ -1,4 +1,5 @@ """Test the Netatmo diagnostics.""" + from unittest.mock import AsyncMock, patch from syrupy import SnapshotAssertion @@ -21,12 +22,16 @@ async def test_entry_diagnostics( config_entry: MockConfigEntry, ) -> None: """Test config entry diagnostics.""" - with patch( - "homeassistant.components.netatmo.api.AsyncConfigEntryNetatmoAuth", - ) as mock_auth, patch( - "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", - ), patch( - "homeassistant.components.netatmo.webhook_generate_url", + with ( + patch( + "homeassistant.components.netatmo.api.AsyncConfigEntryNetatmoAuth", + ) as mock_auth, + patch( + "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", + ), + patch( + "homeassistant.components.netatmo.webhook_generate_url", + ), ): mock_auth.return_value.async_post_api_request.side_effect = fake_post_request mock_auth.return_value.async_addwebhook.side_effect = AsyncMock() diff --git a/tests/components/netatmo/test_fan.py b/tests/components/netatmo/test_fan.py index 72dd579af67..989ea1ac364 100644 --- a/tests/components/netatmo/test_fan.py +++ b/tests/components/netatmo/test_fan.py @@ -1,4 +1,5 @@ """The tests for Netatmo fan.""" + from unittest.mock import AsyncMock, patch from syrupy import SnapshotAssertion diff --git a/tests/components/netatmo/test_init.py b/tests/components/netatmo/test_init.py index 2fd9867262d..e4869b73e2e 100644 --- a/tests/components/netatmo/test_init.py +++ b/tests/components/netatmo/test_init.py @@ -1,4 +1,5 @@ """The tests for Netatmo component.""" + from datetime import timedelta from time import time from unittest.mock import AsyncMock, patch @@ -9,6 +10,7 @@ import pytest from syrupy import SnapshotAssertion from homeassistant import config_entries +from homeassistant.components import cloud from homeassistant.components.netatmo import DOMAIN from homeassistant.const import CONF_WEBHOOK_ID, Platform from homeassistant.core import CoreState, HomeAssistant @@ -60,13 +62,15 @@ async def test_setup_component( hass: HomeAssistant, config_entry: MockConfigEntry ) -> None: """Test setup and teardown of the netatmo component.""" - with patch( - "homeassistant.components.netatmo.api.AsyncConfigEntryNetatmoAuth", - ) as mock_auth, patch( - "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", - ) as mock_impl, patch( - "homeassistant.components.netatmo.webhook_generate_url" - ) as mock_webhook: + with ( + patch( + "homeassistant.components.netatmo.api.AsyncConfigEntryNetatmoAuth", + ) as mock_auth, + patch( + "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", + ) as mock_impl, + patch("homeassistant.components.netatmo.webhook_generate_url") as mock_webhook, + ): mock_auth.return_value.async_post_api_request.side_effect = fake_post_request mock_auth.return_value.async_addwebhook.side_effect = AsyncMock() mock_auth.return_value.async_dropwebhook.side_effect = AsyncMock() @@ -102,14 +106,15 @@ async def test_setup_component_with_config( fake_post_hits += 1 return await fake_post_request(*args, **kwargs) - with patch( - "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", - ) as mock_impl, patch( - "homeassistant.components.netatmo.webhook_generate_url" - ) as mock_webhook, patch( - "homeassistant.components.netatmo.api.AsyncConfigEntryNetatmoAuth", - ) as mock_auth, patch( - "homeassistant.components.netatmo.data_handler.PLATFORMS", ["sensor"] + with ( + patch( + "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", + ) as mock_impl, + patch("homeassistant.components.netatmo.webhook_generate_url") as mock_webhook, + patch( + "homeassistant.components.netatmo.api.AsyncConfigEntryNetatmoAuth", + ) as mock_auth, + patch("homeassistant.components.netatmo.data_handler.PLATFORMS", ["sensor"]), ): mock_auth.return_value.async_post_api_request.side_effect = fake_post mock_auth.return_value.async_addwebhook.side_effect = AsyncMock() @@ -167,16 +172,21 @@ async def test_setup_without_https( ) -> None: """Test if set up with cloud link and without https.""" hass.config.components.add("cloud") - with patch( - "homeassistant.helpers.network.get_url", - return_value="http://example.nabu.casa", - ), patch( - "homeassistant.components.netatmo.api.AsyncConfigEntryNetatmoAuth" - ) as mock_auth, patch( - "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", - ), patch( - "homeassistant.components.netatmo.webhook_generate_url" - ) as mock_async_generate_url: + with ( + patch( + "homeassistant.helpers.network.get_url", + return_value="http://example.nabu.casa", + ), + patch( + "homeassistant.components.netatmo.api.AsyncConfigEntryNetatmoAuth" + ) as mock_auth, + patch( + "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", + ), + patch( + "homeassistant.components.netatmo.webhook_generate_url" + ) as mock_async_generate_url, + ): mock_auth.return_value.async_post_api_request.side_effect = fake_post_request mock_async_generate_url.return_value = "http://example.com" assert await async_setup_component( @@ -197,32 +207,34 @@ async def test_setup_with_cloud( await mock_cloud(hass) await hass.async_block_till_done() - with patch( - "homeassistant.components.cloud.async_is_logged_in", return_value=True - ), patch( - "homeassistant.components.cloud.async_is_connected", return_value=True - ), patch( - "homeassistant.components.cloud.async_active_subscription", return_value=True - ), patch( - "homeassistant.components.cloud.async_create_cloudhook", - return_value="https://hooks.nabu.casa/ABCD", - ) as fake_create_cloudhook, patch( - "homeassistant.components.cloud.async_delete_cloudhook" - ) as fake_delete_cloudhook, patch( - "homeassistant.components.netatmo.api.AsyncConfigEntryNetatmoAuth" - ) as mock_auth, patch( - "homeassistant.components.netatmo.data_handler.PLATFORMS", [] - ), patch( - "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", - ), patch( - "homeassistant.components.netatmo.webhook_generate_url", + with ( + patch("homeassistant.components.cloud.async_is_logged_in", return_value=True), + patch.object(cloud, "async_is_connected", return_value=True), + patch.object(cloud, "async_active_subscription", return_value=True), + patch( + "homeassistant.components.cloud.async_create_cloudhook", + return_value="https://hooks.nabu.casa/ABCD", + ) as fake_create_cloudhook, + patch( + "homeassistant.components.cloud.async_delete_cloudhook" + ) as fake_delete_cloudhook, + patch( + "homeassistant.components.netatmo.api.AsyncConfigEntryNetatmoAuth" + ) as mock_auth, + patch("homeassistant.components.netatmo.data_handler.PLATFORMS", []), + patch( + "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", + ), + patch( + "homeassistant.components.netatmo.webhook_generate_url", + ), ): mock_auth.return_value.async_post_api_request.side_effect = fake_post_request assert await async_setup_component( hass, "netatmo", {"netatmo": {"client_id": "123", "client_secret": "abc"}} ) - assert hass.components.cloud.async_active_subscription() is True - assert hass.components.cloud.async_is_connected() is True + assert cloud.async_active_subscription(hass) is True + assert cloud.async_is_connected(hass) is True fake_create_cloudhook.assert_called_once() assert ( @@ -263,31 +275,33 @@ async def test_setup_with_cloudhook(hass: HomeAssistant) -> None: await mock_cloud(hass) await hass.async_block_till_done() - with patch( - "homeassistant.components.cloud.async_is_logged_in", return_value=True - ), patch( - "homeassistant.components.cloud.async_is_connected", return_value=True - ), patch( - "homeassistant.components.cloud.async_active_subscription", return_value=True - ), patch( - "homeassistant.components.cloud.async_create_cloudhook", - return_value="https://hooks.nabu.casa/ABCD", - ) as fake_create_cloudhook, patch( - "homeassistant.components.cloud.async_delete_cloudhook" - ) as fake_delete_cloudhook, patch( - "homeassistant.components.netatmo.api.AsyncConfigEntryNetatmoAuth" - ) as mock_auth, patch( - "homeassistant.components.netatmo.data_handler.PLATFORMS", [] - ), patch( - "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", - ), patch( - "homeassistant.components.netatmo.webhook_generate_url", + with ( + patch("homeassistant.components.cloud.async_is_logged_in", return_value=True), + patch("homeassistant.components.cloud.async_is_connected", return_value=True), + patch.object(cloud, "async_active_subscription", return_value=True), + patch( + "homeassistant.components.cloud.async_create_cloudhook", + return_value="https://hooks.nabu.casa/ABCD", + ) as fake_create_cloudhook, + patch( + "homeassistant.components.cloud.async_delete_cloudhook" + ) as fake_delete_cloudhook, + patch( + "homeassistant.components.netatmo.api.AsyncConfigEntryNetatmoAuth" + ) as mock_auth, + patch("homeassistant.components.netatmo.data_handler.PLATFORMS", []), + patch( + "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", + ), + patch( + "homeassistant.components.netatmo.webhook_generate_url", + ), ): mock_auth.return_value.async_post_api_request.side_effect = fake_post_request mock_auth.return_value.async_addwebhook.side_effect = AsyncMock() mock_auth.return_value.async_dropwebhook.side_effect = AsyncMock() assert await async_setup_component(hass, "netatmo", {}) - assert hass.components.cloud.async_active_subscription() is True + assert cloud.async_active_subscription(hass) is True assert ( hass.config_entries.async_entries("netatmo")[0].data["cloudhook_url"] @@ -312,18 +326,22 @@ async def test_setup_component_with_delay( """Test setup of the netatmo component with delayed startup.""" hass.set_state(CoreState.not_running) - with patch( - "pyatmo.AbstractAsyncAuth.async_addwebhook", side_effect=AsyncMock() - ) as mock_addwebhook, patch( - "pyatmo.AbstractAsyncAuth.async_dropwebhook", side_effect=AsyncMock() - ) as mock_dropwebhook, patch( - "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", - ) as mock_impl, patch( - "homeassistant.components.netatmo.webhook_generate_url" - ) as mock_webhook, patch( - "pyatmo.AbstractAsyncAuth.async_post_api_request", side_effect=fake_post_request - ) as mock_post_api_request, patch( - "homeassistant.components.netatmo.data_handler.PLATFORMS", ["light"] + with ( + patch( + "pyatmo.AbstractAsyncAuth.async_addwebhook", side_effect=AsyncMock() + ) as mock_addwebhook, + patch( + "pyatmo.AbstractAsyncAuth.async_dropwebhook", side_effect=AsyncMock() + ) as mock_dropwebhook, + patch( + "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", + ) as mock_impl, + patch("homeassistant.components.netatmo.webhook_generate_url") as mock_webhook, + patch( + "pyatmo.AbstractAsyncAuth.async_post_api_request", + side_effect=fake_post_request, + ) as mock_post_api_request, + patch("homeassistant.components.netatmo.data_handler.PLATFORMS", ["light"]), ): assert await async_setup_component( hass, "netatmo", {"netatmo": {"client_id": "123", "client_secret": "abc"}} @@ -387,13 +405,15 @@ async def test_setup_component_invalid_token_scope(hass: HomeAssistant) -> None: ) config_entry.add_to_hass(hass) - with patch( - "homeassistant.components.netatmo.api.AsyncConfigEntryNetatmoAuth", - ) as mock_auth, patch( - "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", - ) as mock_impl, patch( - "homeassistant.components.netatmo.webhook_generate_url" - ) as mock_webhook: + with ( + patch( + "homeassistant.components.netatmo.api.AsyncConfigEntryNetatmoAuth", + ) as mock_auth, + patch( + "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", + ) as mock_impl, + patch("homeassistant.components.netatmo.webhook_generate_url") as mock_webhook, + ): mock_auth.return_value.async_post_api_request.side_effect = fake_post_request mock_auth.return_value.async_addwebhook.side_effect = AsyncMock() mock_auth.return_value.async_dropwebhook.side_effect = AsyncMock() @@ -433,15 +453,18 @@ async def test_setup_component_invalid_token( history=(), ) - with patch( - "homeassistant.components.netatmo.api.AsyncConfigEntryNetatmoAuth", - ) as mock_auth, patch( - "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", - ) as mock_impl, patch( - "homeassistant.components.netatmo.webhook_generate_url" - ) as mock_webhook, patch( - "homeassistant.helpers.config_entry_oauth2_flow.OAuth2Session" - ) as mock_session: + with ( + patch( + "homeassistant.components.netatmo.api.AsyncConfigEntryNetatmoAuth", + ) as mock_auth, + patch( + "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", + ) as mock_impl, + patch("homeassistant.components.netatmo.webhook_generate_url") as mock_webhook, + patch( + "homeassistant.helpers.config_entry_oauth2_flow.OAuth2Session" + ) as mock_session, + ): mock_auth.return_value.async_post_api_request.side_effect = fake_post_request mock_auth.return_value.async_addwebhook.side_effect = AsyncMock() mock_auth.return_value.async_dropwebhook.side_effect = AsyncMock() diff --git a/tests/components/netatmo/test_light.py b/tests/components/netatmo/test_light.py index 1c83f9c6772..c90d67e7630 100644 --- a/tests/components/netatmo/test_light.py +++ b/tests/components/netatmo/test_light.py @@ -1,4 +1,5 @@ """The tests for Netatmo light.""" + from unittest.mock import AsyncMock, patch from syrupy import SnapshotAssertion @@ -122,14 +123,17 @@ async def test_setup_component_no_devices(hass: HomeAssistant, config_entry) -> json={}, ) - with patch( - "homeassistant.components.netatmo.api.AsyncConfigEntryNetatmoAuth" - ) as mock_auth, patch( - "homeassistant.components.netatmo.data_handler.PLATFORMS", ["light"] - ), patch( - "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", - ), patch( - "homeassistant.components.netatmo.webhook_generate_url", + with ( + patch( + "homeassistant.components.netatmo.api.AsyncConfigEntryNetatmoAuth" + ) as mock_auth, + patch("homeassistant.components.netatmo.data_handler.PLATFORMS", ["light"]), + patch( + "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", + ), + patch( + "homeassistant.components.netatmo.webhook_generate_url", + ), ): mock_auth.return_value.async_post_api_request.side_effect = ( fake_post_request_no_data diff --git a/tests/components/netatmo/test_media_source.py b/tests/components/netatmo/test_media_source.py index c22bfd51acb..f9aff2749d2 100644 --- a/tests/components/netatmo/test_media_source.py +++ b/tests/components/netatmo/test_media_source.py @@ -1,4 +1,5 @@ """Test Local Media Source.""" + import ast import pytest diff --git a/tests/components/netatmo/test_select.py b/tests/components/netatmo/test_select.py index 055ea355b48..274113405f6 100644 --- a/tests/components/netatmo/test_select.py +++ b/tests/components/netatmo/test_select.py @@ -1,4 +1,5 @@ """The tests for the Netatmo climate platform.""" + from unittest.mock import AsyncMock, patch import pytest diff --git a/tests/components/netatmo/test_sensor.py b/tests/components/netatmo/test_sensor.py index 8829e374f29..073b9faf485 100644 --- a/tests/components/netatmo/test_sensor.py +++ b/tests/components/netatmo/test_sensor.py @@ -1,4 +1,5 @@ """The tests for the Netatmo sensor platform.""" + from unittest.mock import AsyncMock import pytest diff --git a/tests/components/netatmo/test_switch.py b/tests/components/netatmo/test_switch.py index f5ea08ec1fa..dd82fad3d08 100644 --- a/tests/components/netatmo/test_switch.py +++ b/tests/components/netatmo/test_switch.py @@ -1,4 +1,5 @@ """The tests for Netatmo switch.""" + from unittest.mock import AsyncMock, patch from syrupy import SnapshotAssertion diff --git a/tests/components/netgear/test_config_flow.py b/tests/components/netgear/test_config_flow.py index 37787024fb6..c0649d3646e 100644 --- a/tests/components/netgear/test_config_flow.py +++ b/tests/components/netgear/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for the Netgear config flow.""" + from unittest.mock import Mock, patch from pynetgear import DEFAULT_USER @@ -67,9 +68,10 @@ SSDP_URL_SLL = f"https://{HOST}:{PORT}/rootDesc.xml" @pytest.fixture(name="service") def mock_controller_service(): """Mock a successful service.""" - with patch( - "homeassistant.components.netgear.async_setup_entry", return_value=True - ), patch("homeassistant.components.netgear.router.Netgear") as service_mock: + with ( + patch("homeassistant.components.netgear.async_setup_entry", return_value=True), + patch("homeassistant.components.netgear.router.Netgear") as service_mock, + ): service_mock.return_value.get_info = Mock(return_value=ROUTER_INFOS) service_mock.return_value.port = 80 service_mock.return_value.ssl = False diff --git a/tests/components/netgear_lte/conftest.py b/tests/components/netgear_lte/conftest.py index e32034d660b..ff7f30ced5f 100644 --- a/tests/components/netgear_lte/conftest.py +++ b/tests/components/netgear_lte/conftest.py @@ -1,4 +1,5 @@ """Configure pytest for Netgear LTE tests.""" + from __future__ import annotations from aiohttp.client_exceptions import ClientError diff --git a/tests/components/netgear_lte/snapshots/test_binary_sensor.ambr b/tests/components/netgear_lte/snapshots/test_binary_sensor.ambr index 6f3950aaabe..7ff30fb5cbc 100644 --- a/tests/components/netgear_lte/snapshots/test_binary_sensor.ambr +++ b/tests/components/netgear_lte/snapshots/test_binary_sensor.ambr @@ -8,6 +8,7 @@ 'context': , 'entity_id': 'binary_sensor.netgear_lm1200_mobile_connected', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }) @@ -20,6 +21,7 @@ 'context': , 'entity_id': 'binary_sensor.netgear_lm1200_roaming', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }) @@ -33,6 +35,7 @@ 'context': , 'entity_id': 'binary_sensor.netgear_lm1200_wire_connected', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }) diff --git a/tests/components/netgear_lte/snapshots/test_sensor.ambr b/tests/components/netgear_lte/snapshots/test_sensor.ambr index 8d16ff29dfa..cbeb0bf5b9a 100644 --- a/tests/components/netgear_lte/snapshots/test_sensor.ambr +++ b/tests/components/netgear_lte/snapshots/test_sensor.ambr @@ -3,11 +3,11 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Netgear LM1200 Cell ID', - 'icon': 'mdi:radio-tower', }), 'context': , 'entity_id': 'sensor.netgear_lm1200_cell_id', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '12345678', }) @@ -16,11 +16,11 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Netgear LM1200 Connection text', - 'icon': 'mdi:radio-tower', }), 'context': , 'entity_id': 'sensor.netgear_lm1200_connection_text', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '4G', }) @@ -29,11 +29,11 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Netgear LM1200 Connection type', - 'icon': 'mdi:ip', }), 'context': , 'entity_id': 'sensor.netgear_lm1200_connection_type', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'IPv4AndIPv6', }) @@ -42,11 +42,11 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Netgear LM1200 Current band', - 'icon': 'mdi:radio-tower', }), 'context': , 'entity_id': 'sensor.netgear_lm1200_current_band', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'LTE B4', }) @@ -60,6 +60,7 @@ 'context': , 'entity_id': 'sensor.netgear_lm1200_radio_quality', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '52', }) @@ -68,11 +69,11 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Netgear LM1200 Register network display', - 'icon': 'mdi:web', }), 'context': , 'entity_id': 'sensor.netgear_lm1200_register_network_display', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'T-Mobile', }) @@ -87,6 +88,7 @@ 'context': , 'entity_id': 'sensor.netgear_lm1200_rx_level', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '-113', }) @@ -95,11 +97,11 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Netgear LM1200 Service type', - 'icon': 'mdi:radio-tower', }), 'context': , 'entity_id': 'sensor.netgear_lm1200_service_type', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'LTE', }) @@ -108,12 +110,12 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Netgear LM1200 SMS', - 'icon': 'mdi:message-processing', 'unit_of_measurement': 'unread', }), 'context': , 'entity_id': 'sensor.netgear_lm1200_sms', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '1', }) @@ -122,12 +124,12 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Netgear LM1200 SMS total', - 'icon': 'mdi:message-processing', 'unit_of_measurement': 'messages', }), 'context': , 'entity_id': 'sensor.netgear_lm1200_sms_total', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '1', }) @@ -142,6 +144,7 @@ 'context': , 'entity_id': 'sensor.netgear_lm1200_tx_level', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '4', }) @@ -150,11 +153,11 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Netgear LM1200 Upstream', - 'icon': 'mdi:ip-network', }), 'context': , 'entity_id': 'sensor.netgear_lm1200_upstream', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'LTE', }) @@ -169,6 +172,7 @@ 'context': , 'entity_id': 'sensor.netgear_lm1200_usage', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '40.5162000656128', }) diff --git a/tests/components/netgear_lte/test_binary_sensor.py b/tests/components/netgear_lte/test_binary_sensor.py index 660b7dd4fdf..5fbbcfe06f6 100644 --- a/tests/components/netgear_lte/test_binary_sensor.py +++ b/tests/components/netgear_lte/test_binary_sensor.py @@ -1,4 +1,5 @@ """The tests for Netgear LTE binary sensor platform.""" + from syrupy.assertion import SnapshotAssertion from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN diff --git a/tests/components/netgear_lte/test_config_flow.py b/tests/components/netgear_lte/test_config_flow.py index 97a624a14e7..71fe8ddb774 100644 --- a/tests/components/netgear_lte/test_config_flow.py +++ b/tests/components/netgear_lte/test_config_flow.py @@ -1,4 +1,5 @@ """Test Netgear LTE config flow.""" + from unittest.mock import patch import pytest @@ -39,7 +40,7 @@ async def test_flow_user_form(hass: HomeAssistant, connection: None) -> None: assert result["context"]["unique_id"] == "FFFFFFFFFFFFF" -@pytest.mark.parametrize("source", (SOURCE_USER, SOURCE_IMPORT)) +@pytest.mark.parametrize("source", [SOURCE_USER, SOURCE_IMPORT]) async def test_flow_already_configured( hass: HomeAssistant, setup_integration: None, source: str ) -> None: diff --git a/tests/components/netgear_lte/test_init.py b/tests/components/netgear_lte/test_init.py index 9d9b43f5a16..6a71e6d601c 100644 --- a/tests/components/netgear_lte/test_init.py +++ b/tests/components/netgear_lte/test_init.py @@ -1,4 +1,5 @@ """Test Netgear LTE integration.""" + from syrupy.assertion import SnapshotAssertion from homeassistant.components.netgear_lte.const import DOMAIN diff --git a/tests/components/netgear_lte/test_notify.py b/tests/components/netgear_lte/test_notify.py index 12d906138c3..9a55e7a7ad6 100644 --- a/tests/components/netgear_lte/test_notify.py +++ b/tests/components/netgear_lte/test_notify.py @@ -1,4 +1,5 @@ """The tests for the Netgear LTE notify platform.""" + from unittest.mock import patch from homeassistant.components.notify import ( diff --git a/tests/components/netgear_lte/test_sensor.py b/tests/components/netgear_lte/test_sensor.py index 37f6538fe6a..075c3db3b08 100644 --- a/tests/components/netgear_lte/test_sensor.py +++ b/tests/components/netgear_lte/test_sensor.py @@ -1,4 +1,5 @@ """The tests for Netgear LTE sensor platform.""" + from syrupy.assertion import SnapshotAssertion from homeassistant.components.netgear_lte.const import DOMAIN diff --git a/tests/components/netgear_lte/test_services.py b/tests/components/netgear_lte/test_services.py index 5c5c33be980..58e57cf2039 100644 --- a/tests/components/netgear_lte/test_services.py +++ b/tests/components/netgear_lte/test_services.py @@ -1,4 +1,5 @@ """Services tests for the Netgear LTE integration.""" + from unittest.mock import patch from homeassistant.components.netgear_lte.const import DOMAIN diff --git a/tests/components/network/test_init.py b/tests/components/network/test_init.py index 880caecc138..b02692e5086 100644 --- a/tests/components/network/test_init.py +++ b/tests/components/network/test_init.py @@ -1,4 +1,5 @@ """Test the Network Configuration.""" + from ipaddress import IPv4Address from typing import Any from unittest.mock import MagicMock, Mock, patch @@ -39,7 +40,7 @@ def _mock_cond_socket(sockname): """Return addr if it matches the mock sockname.""" if self._addr == sockname: return [sockname] - raise AttributeError() + raise AttributeError return CondMockSock() @@ -74,12 +75,15 @@ async def test_async_detect_interfaces_setting_non_loopback_route( hass: HomeAssistant, hass_storage: dict[str, Any] ) -> None: """Test without default interface config and the route returns a non-loopback address.""" - with patch( - "homeassistant.components.network.util.socket.socket", - return_value=_mock_socket([_NO_LOOPBACK_IPADDR]), - ), patch( - "homeassistant.components.network.util.ifaddr.get_adapters", - return_value=_generate_mock_adapters(), + with ( + patch( + "homeassistant.components.network.util.socket.socket", + return_value=_mock_socket([_NO_LOOPBACK_IPADDR]), + ), + patch( + "homeassistant.components.network.util.ifaddr.get_adapters", + return_value=_generate_mock_adapters(), + ), ): assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) await hass.async_block_till_done() @@ -138,12 +142,15 @@ async def test_async_detect_interfaces_setting_loopback_route( hass: HomeAssistant, hass_storage: dict[str, Any] ) -> None: """Test without default interface config and the route returns a loopback address.""" - with patch( - "homeassistant.components.network.util.socket.socket", - return_value=_mock_socket([_LOOPBACK_IPADDR]), - ), patch( - "homeassistant.components.network.util.ifaddr.get_adapters", - return_value=_generate_mock_adapters(), + with ( + patch( + "homeassistant.components.network.util.socket.socket", + return_value=_mock_socket([_LOOPBACK_IPADDR]), + ), + patch( + "homeassistant.components.network.util.ifaddr.get_adapters", + return_value=_generate_mock_adapters(), + ), ): assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) await hass.async_block_till_done() @@ -201,12 +208,15 @@ async def test_async_detect_interfaces_setting_empty_route( hass: HomeAssistant, hass_storage: dict[str, Any] ) -> None: """Test without default interface config and the route returns nothing.""" - with patch( - "homeassistant.components.network.util.socket.socket", - return_value=_mock_socket([]), - ), patch( - "homeassistant.components.network.util.ifaddr.get_adapters", - return_value=_generate_mock_adapters(), + with ( + patch( + "homeassistant.components.network.util.socket.socket", + return_value=_mock_socket([]), + ), + patch( + "homeassistant.components.network.util.ifaddr.get_adapters", + return_value=_generate_mock_adapters(), + ), ): assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) await hass.async_block_till_done() @@ -264,12 +274,15 @@ async def test_async_detect_interfaces_setting_exception( hass: HomeAssistant, hass_storage: dict[str, Any] ) -> None: """Test without default interface config and the route throws an exception.""" - with patch( - "homeassistant.components.network.util.socket.socket", - return_value=_mock_socket_exception(AttributeError), - ), patch( - "homeassistant.components.network.util.ifaddr.get_adapters", - return_value=_generate_mock_adapters(), + with ( + patch( + "homeassistant.components.network.util.socket.socket", + return_value=_mock_socket_exception(AttributeError), + ), + patch( + "homeassistant.components.network.util.ifaddr.get_adapters", + return_value=_generate_mock_adapters(), + ), ): assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) await hass.async_block_till_done() @@ -332,12 +345,15 @@ async def test_interfaces_configured_from_storage( "key": STORAGE_KEY, "data": {ATTR_CONFIGURED_ADAPTERS: ["eth0", "eth1", "vtun0"]}, } - with patch( - "homeassistant.components.network.util.socket.socket", - return_value=_mock_socket([_NO_LOOPBACK_IPADDR]), - ), patch( - "homeassistant.components.network.util.ifaddr.get_adapters", - return_value=_generate_mock_adapters(), + with ( + patch( + "homeassistant.components.network.util.socket.socket", + return_value=_mock_socket([_NO_LOOPBACK_IPADDR]), + ), + patch( + "homeassistant.components.network.util.ifaddr.get_adapters", + return_value=_generate_mock_adapters(), + ), ): assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) await hass.async_block_till_done() @@ -401,12 +417,15 @@ async def test_interfaces_configured_from_storage_websocket_update( "key": STORAGE_KEY, "data": {ATTR_CONFIGURED_ADAPTERS: ["eth0", "eth1", "vtun0"]}, } - with patch( - "homeassistant.components.network.util.socket.socket", - return_value=_mock_socket([_NO_LOOPBACK_IPADDR]), - ), patch( - "homeassistant.components.network.util.ifaddr.get_adapters", - return_value=_generate_mock_adapters(), + with ( + patch( + "homeassistant.components.network.util.socket.socket", + return_value=_mock_socket([_NO_LOOPBACK_IPADDR]), + ), + patch( + "homeassistant.components.network.util.ifaddr.get_adapters", + return_value=_generate_mock_adapters(), + ), ): assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) await hass.async_block_till_done() @@ -532,12 +551,15 @@ async def test_async_get_source_ip_matching_interface( "data": {ATTR_CONFIGURED_ADAPTERS: ["eth1"]}, } - with patch( - "homeassistant.components.network.util.ifaddr.get_adapters", - return_value=_generate_mock_adapters(), - ), patch( - "homeassistant.components.network.util.socket.socket", - return_value=_mock_socket(["192.168.1.5"]), + with ( + patch( + "homeassistant.components.network.util.ifaddr.get_adapters", + return_value=_generate_mock_adapters(), + ), + patch( + "homeassistant.components.network.util.socket.socket", + return_value=_mock_socket(["192.168.1.5"]), + ), ): assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) await hass.async_block_till_done() @@ -555,12 +577,15 @@ async def test_async_get_source_ip_interface_not_match( "data": {ATTR_CONFIGURED_ADAPTERS: ["vtun0"]}, } - with patch( - "homeassistant.components.network.util.ifaddr.get_adapters", - return_value=_generate_mock_adapters(), - ), patch( - "homeassistant.components.network.util.socket.socket", - return_value=_mock_socket(["192.168.1.5"]), + with ( + patch( + "homeassistant.components.network.util.ifaddr.get_adapters", + return_value=_generate_mock_adapters(), + ), + patch( + "homeassistant.components.network.util.socket.socket", + return_value=_mock_socket(["192.168.1.5"]), + ), ): assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) await hass.async_block_till_done() @@ -578,12 +603,15 @@ async def test_async_get_source_ip_cannot_determine_target( "data": {ATTR_CONFIGURED_ADAPTERS: ["eth1"]}, } - with patch( - "homeassistant.components.network.util.ifaddr.get_adapters", - return_value=_generate_mock_adapters(), - ), patch( - "homeassistant.components.network.util.socket.socket", - return_value=_mock_socket([None]), + with ( + patch( + "homeassistant.components.network.util.ifaddr.get_adapters", + return_value=_generate_mock_adapters(), + ), + patch( + "homeassistant.components.network.util.socket.socket", + return_value=_mock_socket([None]), + ), ): assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) await hass.async_block_till_done() @@ -601,12 +629,15 @@ async def test_async_get_ipv4_broadcast_addresses_default( "data": {ATTR_CONFIGURED_ADAPTERS: ["eth1"]}, } - with patch( - "homeassistant.components.network.util.socket.socket", - return_value=_mock_socket(["192.168.1.5"]), - ), patch( - "homeassistant.components.network.util.ifaddr.get_adapters", - return_value=_generate_mock_adapters(), + with ( + patch( + "homeassistant.components.network.util.socket.socket", + return_value=_mock_socket(["192.168.1.5"]), + ), + patch( + "homeassistant.components.network.util.ifaddr.get_adapters", + return_value=_generate_mock_adapters(), + ), ): assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) await hass.async_block_till_done() @@ -626,12 +657,15 @@ async def test_async_get_ipv4_broadcast_addresses_multiple( "data": {ATTR_CONFIGURED_ADAPTERS: ["eth1", "vtun0"]}, } - with patch( - "homeassistant.components.network.util.socket.socket", - return_value=_mock_socket([_LOOPBACK_IPADDR]), - ), patch( - "homeassistant.components.network.util.ifaddr.get_adapters", - return_value=_generate_mock_adapters(), + with ( + patch( + "homeassistant.components.network.util.socket.socket", + return_value=_mock_socket([_LOOPBACK_IPADDR]), + ), + patch( + "homeassistant.components.network.util.ifaddr.get_adapters", + return_value=_generate_mock_adapters(), + ), ): assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) await hass.async_block_till_done() @@ -653,12 +687,15 @@ async def test_async_get_source_ip_no_enabled_addresses( "data": {ATTR_CONFIGURED_ADAPTERS: ["eth1"]}, } - with patch( - "homeassistant.components.network.util.ifaddr.get_adapters", - return_value=[], - ), patch( - "homeassistant.components.network.util.socket.socket", - return_value=_mock_socket(["192.168.1.5"]), + with ( + patch( + "homeassistant.components.network.util.ifaddr.get_adapters", + return_value=[], + ), + patch( + "homeassistant.components.network.util.socket.socket", + return_value=_mock_socket(["192.168.1.5"]), + ), ): assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) await hass.async_block_till_done() @@ -678,12 +715,15 @@ async def test_async_get_source_ip_cannot_be_determined_and_no_enabled_addresses "data": {ATTR_CONFIGURED_ADAPTERS: ["eth1"]}, } - with patch( - "homeassistant.components.network.util.ifaddr.get_adapters", - return_value=[], - ), patch( - "homeassistant.components.network.util.socket.socket", - return_value=_mock_socket([None]), + with ( + patch( + "homeassistant.components.network.util.ifaddr.get_adapters", + return_value=[], + ), + patch( + "homeassistant.components.network.util.socket.socket", + return_value=_mock_socket([None]), + ), ): assert not await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) await hass.async_block_till_done() @@ -701,12 +741,15 @@ async def test_async_get_source_ip_no_ip_loopback( "data": {ATTR_CONFIGURED_ADAPTERS: ["eth1"]}, } - with patch( - "homeassistant.components.network.util.ifaddr.get_adapters", - return_value=[], - ), patch( - "homeassistant.components.network.util.socket.socket", - return_value=_mock_cond_socket(_LOOPBACK_IPADDR), + with ( + patch( + "homeassistant.components.network.util.ifaddr.get_adapters", + return_value=[], + ), + patch( + "homeassistant.components.network.util.socket.socket", + return_value=_mock_cond_socket(_LOOPBACK_IPADDR), + ), ): assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) await hass.async_block_till_done() @@ -777,12 +820,15 @@ _ADAPTERS_WITH_MANUAL_CONFIG = [ async def test_async_get_announce_addresses(hass: HomeAssistant) -> None: """Test addresses for mDNS/etc announcement.""" first_ip = "172.16.1.5" - with patch( - "homeassistant.components.network.async_get_source_ip", - return_value=first_ip, - ), patch( - "homeassistant.components.network.async_get_adapters", - return_value=_ADAPTERS_WITH_MANUAL_CONFIG, + with ( + patch( + "homeassistant.components.network.async_get_source_ip", + return_value=first_ip, + ), + patch( + "homeassistant.components.network.async_get_adapters", + return_value=_ADAPTERS_WITH_MANUAL_CONFIG, + ), ): actual = await network.async_get_announce_addresses(hass) assert actual[0] == first_ip and actual == [ @@ -794,12 +840,15 @@ async def test_async_get_announce_addresses(hass: HomeAssistant) -> None: ] first_ip = "192.168.1.5" - with patch( - "homeassistant.components.network.async_get_source_ip", - return_value=first_ip, - ), patch( - "homeassistant.components.network.async_get_adapters", - return_value=_ADAPTERS_WITH_MANUAL_CONFIG, + with ( + patch( + "homeassistant.components.network.async_get_source_ip", + return_value=first_ip, + ), + patch( + "homeassistant.components.network.async_get_adapters", + return_value=_ADAPTERS_WITH_MANUAL_CONFIG, + ), ): actual = await network.async_get_announce_addresses(hass) @@ -814,12 +863,15 @@ async def test_async_get_announce_addresses(hass: HomeAssistant) -> None: async def test_async_get_announce_addresses_no_source_ip(hass: HomeAssistant) -> None: """Test addresses for mDNS/etc announcement without source ip.""" - with patch( - "homeassistant.components.network.async_get_source_ip", - return_value=None, - ), patch( - "homeassistant.components.network.async_get_adapters", - return_value=_ADAPTERS_WITH_MANUAL_CONFIG, + with ( + patch( + "homeassistant.components.network.async_get_source_ip", + return_value=None, + ), + patch( + "homeassistant.components.network.async_get_adapters", + return_value=_ADAPTERS_WITH_MANUAL_CONFIG, + ), ): actual = await network.async_get_announce_addresses(hass) assert actual == [ diff --git a/tests/components/nexia/test_binary_sensor.py b/tests/components/nexia/test_binary_sensor.py index f59e968d634..e175afe6214 100644 --- a/tests/components/nexia/test_binary_sensor.py +++ b/tests/components/nexia/test_binary_sensor.py @@ -1,4 +1,5 @@ """The binary_sensor tests for the nexia platform.""" + from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant diff --git a/tests/components/nexia/test_climate.py b/tests/components/nexia/test_climate.py index 5553965b418..900838547f2 100644 --- a/tests/components/nexia/test_climate.py +++ b/tests/components/nexia/test_climate.py @@ -1,4 +1,5 @@ """The lock tests for the august platform.""" + from homeassistant.components.climate import HVACMode from homeassistant.core import HomeAssistant diff --git a/tests/components/nexia/test_config_flow.py b/tests/components/nexia/test_config_flow.py index c07b5c8540e..02a3cf06728 100644 --- a/tests/components/nexia/test_config_flow.py +++ b/tests/components/nexia/test_config_flow.py @@ -1,4 +1,5 @@ """Test the nexia config flow.""" + from unittest.mock import MagicMock, patch import aiohttp @@ -21,16 +22,20 @@ async def test_form(hass: HomeAssistant, brand) -> None: assert result["type"] == "form" assert result["errors"] == {} - with patch( - "homeassistant.components.nexia.config_flow.NexiaHome.get_name", - return_value="myhouse", - ), patch( - "homeassistant.components.nexia.config_flow.NexiaHome.login", - side_effect=MagicMock(), - ), patch( - "homeassistant.components.nexia.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.nexia.config_flow.NexiaHome.get_name", + return_value="myhouse", + ), + patch( + "homeassistant.components.nexia.config_flow.NexiaHome.login", + side_effect=MagicMock(), + ), + patch( + "homeassistant.components.nexia.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_BRAND: brand, CONF_USERNAME: "username", CONF_PASSWORD: "password"}, @@ -53,11 +58,14 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch( - "homeassistant.components.nexia.config_flow.NexiaHome.login", - ), patch( - "homeassistant.components.nexia.config_flow.NexiaHome.get_name", - return_value=None, + with ( + patch( + "homeassistant.components.nexia.config_flow.NexiaHome.login", + ), + patch( + "homeassistant.components.nexia.config_flow.NexiaHome.get_name", + return_value=None, + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], diff --git a/tests/components/nexia/test_diagnostics.py b/tests/components/nexia/test_diagnostics.py index 9f8f7f05a8d..ff9696d1567 100644 --- a/tests/components/nexia/test_diagnostics.py +++ b/tests/components/nexia/test_diagnostics.py @@ -1,4 +1,5 @@ """Test august diagnostics.""" + from syrupy import SnapshotAssertion from homeassistant.core import HomeAssistant diff --git a/tests/components/nexia/test_init.py b/tests/components/nexia/test_init.py index f920592f8a6..8eeb8a9f729 100644 --- a/tests/components/nexia/test_init.py +++ b/tests/components/nexia/test_init.py @@ -1,4 +1,5 @@ """The init tests for the nexia platform.""" + import aiohttp from homeassistant.components.nexia.const import DOMAIN diff --git a/tests/components/nexia/test_scene.py b/tests/components/nexia/test_scene.py index 0678921f7ea..20f214fff27 100644 --- a/tests/components/nexia/test_scene.py +++ b/tests/components/nexia/test_scene.py @@ -1,4 +1,5 @@ """The scene tests for the nexia platform.""" + from homeassistant.core import HomeAssistant from .util import async_init_integration @@ -30,7 +31,6 @@ async def test_automation_scenes(hass: HomeAssistant) -> None: "change Fan Mode to Auto" ), "friendly_name": "Away Short", - "icon": "mdi:script-text-outline", } # Only test for a subset of attributes in case # HA changes the implementation and a new one appears @@ -51,7 +51,6 @@ async def test_automation_scenes(hass: HomeAssistant) -> None: "Activate the mode named 'Power Outage'" ), "friendly_name": "Power Outage", - "icon": "mdi:script-text-outline", } # Only test for a subset of attributes in case # HA changes the implementation and a new one appears @@ -70,7 +69,6 @@ async def test_automation_scenes(hass: HomeAssistant) -> None: "'Home'" ), "friendly_name": "Power Restored", - "icon": "mdi:script-text-outline", } # Only test for a subset of attributes in case # HA changes the implementation and a new one appears diff --git a/tests/components/nexia/test_sensor.py b/tests/components/nexia/test_sensor.py index 23a92af71c8..1f595da43d1 100644 --- a/tests/components/nexia/test_sensor.py +++ b/tests/components/nexia/test_sensor.py @@ -1,4 +1,5 @@ """The sensor tests for the nexia platform.""" + from homeassistant.const import PERCENTAGE, UnitOfTemperature from homeassistant.core import HomeAssistant diff --git a/tests/components/nexia/test_switch.py b/tests/components/nexia/test_switch.py index 0ddef1c807c..821d939bac5 100644 --- a/tests/components/nexia/test_switch.py +++ b/tests/components/nexia/test_switch.py @@ -1,4 +1,5 @@ """The switch tests for the nexia platform.""" + from homeassistant.const import STATE_ON from homeassistant.core import HomeAssistant diff --git a/tests/components/nexia/util.py b/tests/components/nexia/util.py index d47e3fd3d6a..98d5312f0a1 100644 --- a/tests/components/nexia/util.py +++ b/tests/components/nexia/util.py @@ -1,4 +1,5 @@ """Tests for the nexia integration.""" + from unittest.mock import patch import uuid @@ -23,8 +24,9 @@ async def async_init_integration( session_fixture = "nexia/session_123456.json" sign_in_fixture = "nexia/sign_in.json" set_fan_speed_fixture = "nexia/set_fan_speed_2293892.json" - with mock_aiohttp_client() as mock_session, patch( - "nexia.home.load_or_create_uuid", return_value=uuid.uuid4() + with ( + mock_aiohttp_client() as mock_session, + patch("nexia.home.load_or_create_uuid", return_value=uuid.uuid4()), ): nexia = NexiaHome(mock_session) if exception: diff --git a/tests/components/nextbus/conftest.py b/tests/components/nextbus/conftest.py index 4f6a6f22270..84445905c2e 100644 --- a/tests/components/nextbus/conftest.py +++ b/tests/components/nextbus/conftest.py @@ -1,4 +1,5 @@ """Test helpers for NextBus tests.""" + from typing import Any from unittest.mock import MagicMock diff --git a/tests/components/nextbus/test_config_flow.py b/tests/components/nextbus/test_config_flow.py index 0b67f817eb2..466ecb9df61 100644 --- a/tests/components/nextbus/test_config_flow.py +++ b/tests/components/nextbus/test_config_flow.py @@ -1,4 +1,5 @@ """Test the NextBus config flow.""" + from collections.abc import Generator from unittest.mock import MagicMock, patch @@ -69,11 +70,11 @@ async def test_import_config( @pytest.mark.parametrize( ("override", "expected_reason"), - ( + [ ({CONF_AGENCY: "not muni"}, "invalid_agency"), ({CONF_ROUTE: "not F"}, "invalid_route"), ({CONF_STOP: "not 5650"}, "invalid_stop"), - ), + ], ) async def test_import_config_invalid( hass: HomeAssistant, diff --git a/tests/components/nextbus/test_sensor.py b/tests/components/nextbus/test_sensor.py index 92da27783bc..ece40b36fb1 100644 --- a/tests/components/nextbus/test_sensor.py +++ b/tests/components/nextbus/test_sensor.py @@ -1,4 +1,5 @@ """The tests for the nexbus sensor component.""" + from collections.abc import Generator from copy import deepcopy from unittest.mock import MagicMock, patch @@ -273,10 +274,10 @@ async def test_direction_list( @pytest.mark.parametrize( "client_exception", - ( + [ NextBusHTTPError("failed", HTTPError("url", 500, "error", MagicMock(), None)), NextBusFormatError("failed"), - ), + ], ) async def test_prediction_exceptions( hass: HomeAssistant, @@ -311,10 +312,10 @@ async def test_custom_name( @pytest.mark.parametrize( "prediction_results", - ( + [ {}, {"Error": "Failed"}, - ), + ], ) async def test_no_predictions( hass: HomeAssistant, diff --git a/tests/components/nextbus/test_util.py b/tests/components/nextbus/test_util.py index 798171464e6..dfe54249814 100644 --- a/tests/components/nextbus/test_util.py +++ b/tests/components/nextbus/test_util.py @@ -1,4 +1,5 @@ """Test NextBus util functions.""" + from typing import Any import pytest @@ -8,11 +9,11 @@ from homeassistant.components.nextbus.util import listify, maybe_first @pytest.mark.parametrize( ("input", "expected"), - ( + [ ("foo", ["foo"]), (["foo"], ["foo"]), (None, []), - ), + ], ) def test_listify(input: Any, expected: list[Any]) -> None: """Test input listification.""" @@ -21,13 +22,13 @@ def test_listify(input: Any, expected: list[Any]) -> None: @pytest.mark.parametrize( ("input", "expected"), - ( + [ ([], []), (None, None), ("test", "test"), (["test"], "test"), (["test", "second"], "test"), - ), + ], ) def test_maybe_first(input: list[Any] | None, expected: Any) -> None: """Test maybe getting the first thing from a list.""" diff --git a/tests/components/nextcloud/test_config_flow.py b/tests/components/nextcloud/test_config_flow.py index 94c03758621..32c688fb8c2 100644 --- a/tests/components/nextcloud/test_config_flow.py +++ b/tests/components/nextcloud/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for the Nextcloud config flow.""" + from unittest.mock import Mock, patch from nextcloudmonitor import ( diff --git a/tests/components/nextdns/__init__.py b/tests/components/nextdns/__init__.py index a175bffbb75..e4948a9358f 100644 --- a/tests/components/nextdns/__init__.py +++ b/tests/components/nextdns/__init__.py @@ -1,4 +1,5 @@ """Tests for the NextDNS integration.""" + from unittest.mock import patch from nextdns import ( @@ -122,29 +123,39 @@ async def init_integration(hass: HomeAssistant) -> MockConfigEntry: entry_id="d9aa37407ddac7b964a99e86312288d6", ) - 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, - ), patch( - "homeassistant.components.nextdns.NextDns.connection_status", - return_value=CONNECTION_STATUS, + 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, + ), + patch( + "homeassistant.components.nextdns.NextDns.connection_status", + return_value=CONNECTION_STATUS, + ), ): entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) diff --git a/tests/components/nextdns/test_binary_sensor.py b/tests/components/nextdns/test_binary_sensor.py index 08d1f08f5d1..484b4e99aad 100644 --- a/tests/components/nextdns/test_binary_sensor.py +++ b/tests/components/nextdns/test_binary_sensor.py @@ -1,4 +1,5 @@ """Test binary sensor of NextDNS integration.""" + from datetime import timedelta from unittest.mock import patch diff --git a/tests/components/nextdns/test_button.py b/tests/components/nextdns/test_button.py index fabf87f6462..b5f7b01aee2 100644 --- a/tests/components/nextdns/test_button.py +++ b/tests/components/nextdns/test_button.py @@ -1,4 +1,5 @@ """Test button of NextDNS integration.""" + from unittest.mock import patch from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN @@ -30,9 +31,10 @@ async def test_button_press(hass: HomeAssistant) -> None: 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): + 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", diff --git a/tests/components/nextdns/test_config_flow.py b/tests/components/nextdns/test_config_flow.py index da7fa131543..4d9961474c5 100644 --- a/tests/components/nextdns/test_config_flow.py +++ b/tests/components/nextdns/test_config_flow.py @@ -1,4 +1,5 @@ """Define tests for the NextDNS config flow.""" + from unittest.mock import patch from nextdns import ApiError, InvalidApiKeyError @@ -22,11 +23,15 @@ async def test_form_create_entry(hass: HomeAssistant) -> None: assert result["step_id"] == "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: + 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"}, diff --git a/tests/components/nextdns/test_init.py b/tests/components/nextdns/test_init.py index fb9ea74509e..f7b85bb8a54 100644 --- a/tests/components/nextdns/test_init.py +++ b/tests/components/nextdns/test_init.py @@ -1,4 +1,5 @@ """Test init of NextDNS integration.""" + from unittest.mock import patch from nextdns import ApiError diff --git a/tests/components/nextdns/test_sensor.py b/tests/components/nextdns/test_sensor.py index e500ff3c626..a6d9b4c545f 100644 --- a/tests/components/nextdns/test_sensor.py +++ b/tests/components/nextdns/test_sensor.py @@ -1,4 +1,5 @@ """Test sensor of NextDNS integration.""" + from datetime import timedelta from unittest.mock import patch @@ -308,21 +309,27 @@ async def test_availability( 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"), + 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() @@ -348,21 +355,27 @@ async def test_availability( 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, + 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() diff --git a/tests/components/nextdns/test_switch.py b/tests/components/nextdns/test_switch.py index ef87b51e98e..f51ee32fd10 100644 --- a/tests/components/nextdns/test_switch.py +++ b/tests/components/nextdns/test_switch.py @@ -1,4 +1,5 @@ """Test switch of NextDNS integration.""" + from datetime import timedelta from unittest.mock import Mock, patch @@ -725,9 +726,10 @@ async def test_switch_failure(hass: HomeAssistant, exc: Exception) -> None: """Tests that the turn on/off service throws HomeAssistantError.""" await init_integration(hass) - with patch( - "homeassistant.components.nextdns.NextDns.set_setting", side_effect=exc - ), pytest.raises(HomeAssistantError): + with ( + patch("homeassistant.components.nextdns.NextDns.set_setting", side_effect=exc), + pytest.raises(HomeAssistantError), + ): await hass.services.async_call( SWITCH_DOMAIN, SERVICE_TURN_ON, diff --git a/tests/components/nextdns/test_system_health.py b/tests/components/nextdns/test_system_health.py index 14d447947c1..758437c9acb 100644 --- a/tests/components/nextdns/test_system_health.py +++ b/tests/components/nextdns/test_system_health.py @@ -1,4 +1,5 @@ """Test NextDNS system health.""" + import asyncio from aiohttp import ClientError @@ -19,6 +20,7 @@ async def test_nextdns_system_health( aioclient_mock.get(API_ENDPOINT, text="") hass.config.components.add(DOMAIN) assert await async_setup_component(hass, "system_health", {}) + await hass.async_block_till_done() info = await get_system_health_info(hass, DOMAIN) @@ -36,6 +38,7 @@ async def test_nextdns_system_health_fail( aioclient_mock.get(API_ENDPOINT, exc=ClientError) hass.config.components.add(DOMAIN) assert await async_setup_component(hass, "system_health", {}) + await hass.async_block_till_done() info = await get_system_health_info(hass, DOMAIN) diff --git a/tests/components/nfandroidtv/test_config_flow.py b/tests/components/nfandroidtv/test_config_flow.py index 89ea23e0f0c..04fcf699513 100644 --- a/tests/components/nfandroidtv/test_config_flow.py +++ b/tests/components/nfandroidtv/test_config_flow.py @@ -1,4 +1,5 @@ """Test NFAndroidTV config flow.""" + from unittest.mock import patch from notifications_android_tv.notifications import ConnectError diff --git a/tests/components/nibe_heatpump/__init__.py b/tests/components/nibe_heatpump/__init__.py index 3c3db391ba8..d7c1fa5ebad 100644 --- a/tests/components/nibe_heatpump/__init__.py +++ b/tests/components/nibe_heatpump/__init__.py @@ -41,7 +41,7 @@ class MockConnection(Connection): async def read_coil(self, coil: Coil, timeout: float = 0) -> CoilData: """Read of coils.""" if (data := self.coils.get(coil.address, None)) is None: - raise ReadException() + raise ReadException return CoilData(coil, data) async def write_coil(self, coil_data: CoilData, timeout: float = 10.0) -> None: diff --git a/tests/components/nibe_heatpump/conftest.py b/tests/components/nibe_heatpump/conftest.py index a5eb5fb012d..00d4c92c68b 100644 --- a/tests/components/nibe_heatpump/conftest.py +++ b/tests/components/nibe_heatpump/conftest.py @@ -1,4 +1,5 @@ """Test configuration for Nibe Heat Pump.""" + from collections.abc import Generator from contextlib import ExitStack from unittest.mock import AsyncMock, Mock, patch @@ -67,11 +68,12 @@ async def fixture_coils(mock_connection: MockConnection): def get_coil_by_address(self, address): coils_data = get_coil_by_address_original(self, address) if coils_data.address not in mock_connection.coils: - raise CoilNotFoundException() + raise CoilNotFoundException return coils_data - with patch.object(HeatPump, "get_coils", new=get_coils), patch.object( - HeatPump, "get_coil_by_address", new=get_coil_by_address + with ( + patch.object(HeatPump, "get_coils", new=get_coils), + patch.object(HeatPump, "get_coil_by_address", new=get_coil_by_address), ): yield mock_connection.coils diff --git a/tests/components/nibe_heatpump/snapshots/test_climate.ambr b/tests/components/nibe_heatpump/snapshots/test_climate.ambr index f19fd69c47d..0c5cd46f5db 100644 --- a/tests/components/nibe_heatpump/snapshots/test_climate.ambr +++ b/tests/components/nibe_heatpump/snapshots/test_climate.ambr @@ -21,6 +21,7 @@ 'context': , 'entity_id': 'climate.climate_system_s2', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'heat_cool', }) @@ -42,6 +43,7 @@ 'context': , 'entity_id': 'climate.climate_system_s2', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unavailable', }) @@ -68,6 +70,7 @@ 'context': , 'entity_id': 'climate.climate_system_s3', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'heat_cool', }) @@ -89,6 +92,7 @@ 'context': , 'entity_id': 'climate.climate_system_s3', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unavailable', }) @@ -121,6 +125,7 @@ 'context': , 'entity_id': 'climate.climate_system_s2', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'heat_cool', }) @@ -147,6 +152,7 @@ 'context': , 'entity_id': 'climate.climate_system_s2', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'auto', }) @@ -173,6 +179,7 @@ 'context': , 'entity_id': 'climate.climate_system_s2', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'heat', }) @@ -199,6 +206,7 @@ 'context': , 'entity_id': 'climate.climate_system_s2', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'heat_cool', }) @@ -225,6 +233,7 @@ 'context': , 'entity_id': 'climate.climate_system_s2', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'heat_cool', }) @@ -251,6 +260,7 @@ 'context': , 'entity_id': 'climate.climate_system_s2', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'heat_cool', }) @@ -277,6 +287,7 @@ 'context': , 'entity_id': 'climate.climate_system_s2', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'auto', }) @@ -303,6 +314,7 @@ 'context': , 'entity_id': 'climate.climate_system_s2', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'auto', }) @@ -329,6 +341,7 @@ 'context': , 'entity_id': 'climate.climate_system_s1', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'heat_cool', }) @@ -355,6 +368,7 @@ 'context': , 'entity_id': 'climate.climate_system_s1', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'auto', }) @@ -381,6 +395,7 @@ 'context': , 'entity_id': 'climate.climate_system_s1', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'heat', }) @@ -407,6 +422,7 @@ 'context': , 'entity_id': 'climate.climate_system_s1', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'heat_cool', }) @@ -433,6 +449,7 @@ 'context': , 'entity_id': 'climate.climate_system_s1', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'heat_cool', }) @@ -459,6 +476,7 @@ 'context': , 'entity_id': 'climate.climate_system_s1', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'heat_cool', }) @@ -485,6 +503,7 @@ 'context': , 'entity_id': 'climate.climate_system_s1', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'auto', }) @@ -511,6 +530,7 @@ 'context': , 'entity_id': 'climate.climate_system_s1', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'auto', }) diff --git a/tests/components/nibe_heatpump/snapshots/test_coordinator.ambr b/tests/components/nibe_heatpump/snapshots/test_coordinator.ambr index 98e62a833a8..50755533ee5 100644 --- a/tests/components/nibe_heatpump/snapshots/test_coordinator.ambr +++ b/tests/components/nibe_heatpump/snapshots/test_coordinator.ambr @@ -11,6 +11,7 @@ 'context': , 'entity_id': 'number.heating_offset_climate_system_1_40031', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '10.0', }) @@ -27,6 +28,7 @@ 'context': , 'entity_id': 'number.heating_offset_climate_system_1_40031', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unavailable', }) @@ -43,6 +45,7 @@ 'context': , 'entity_id': 'number.heating_offset_climate_system_1_40031', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '10.0', }) @@ -60,6 +63,7 @@ 'context': , 'entity_id': 'number.min_supply_climate_system_1_40035', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unavailable', }) @@ -79,6 +83,7 @@ 'context': , 'entity_id': 'number.heating_offset_climate_system_1_40031', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '10.0', }) @@ -95,6 +100,7 @@ 'context': , 'entity_id': 'number.heating_offset_climate_system_1_40031', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '20.0', }) @@ -111,6 +117,7 @@ 'context': , 'entity_id': 'number.heating_offset_climate_system_1_40031', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '20.0', }) @@ -127,6 +134,7 @@ 'context': , 'entity_id': 'number.heating_offset_climate_system_1_40031', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '30.0', }) diff --git a/tests/components/nibe_heatpump/snapshots/test_number.ambr b/tests/components/nibe_heatpump/snapshots/test_number.ambr index d174c0cc059..343d5569a2d 100644 --- a/tests/components/nibe_heatpump/snapshots/test_number.ambr +++ b/tests/components/nibe_heatpump/snapshots/test_number.ambr @@ -11,6 +11,7 @@ 'context': , 'entity_id': 'number.heat_offset_s1_47011', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '-10.0', }) @@ -27,6 +28,7 @@ 'context': , 'entity_id': 'number.heat_offset_s1_47011', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '10.0', }) @@ -47,6 +49,7 @@ 'context': , 'entity_id': 'number.hw_charge_offset_47062', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '-10.0', }) @@ -64,6 +67,7 @@ 'context': , 'entity_id': 'number.hw_charge_offset_47062', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '10.0', }) @@ -81,6 +85,7 @@ 'context': , 'entity_id': 'number.hw_charge_offset_47062', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unavailable', }) @@ -97,6 +102,7 @@ 'context': , 'entity_id': 'number.heating_offset_climate_system_1_40031', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '-10.0', }) @@ -113,6 +119,7 @@ 'context': , 'entity_id': 'number.heating_offset_climate_system_1_40031', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '10.0', }) @@ -129,6 +136,7 @@ 'context': , 'entity_id': 'number.heating_offset_climate_system_1_40031', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unavailable', }) diff --git a/tests/components/nibe_heatpump/test_button.py b/tests/components/nibe_heatpump/test_button.py index d150d3f2d38..e660340c549 100644 --- a/tests/components/nibe_heatpump/test_button.py +++ b/tests/components/nibe_heatpump/test_button.py @@ -1,4 +1,5 @@ """Test the Nibe Heat Pump config flow.""" + from typing import Any from unittest.mock import AsyncMock, patch diff --git a/tests/components/nibe_heatpump/test_climate.py b/tests/components/nibe_heatpump/test_climate.py index 2b3fe5d8c0e..3a468e51e83 100644 --- a/tests/components/nibe_heatpump/test_climate.py +++ b/tests/components/nibe_heatpump/test_climate.py @@ -1,4 +1,5 @@ """Test the Nibe Heat Pump config flow.""" + from typing import Any from unittest.mock import call, patch diff --git a/tests/components/nibe_heatpump/test_config_flow.py b/tests/components/nibe_heatpump/test_config_flow.py index 9b03159af2f..b4c0b223998 100644 --- a/tests/components/nibe_heatpump/test_config_flow.py +++ b/tests/components/nibe_heatpump/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Nibe Heat Pump config flow.""" + from typing import Any from unittest.mock import AsyncMock, Mock @@ -145,10 +146,10 @@ async def test_nibegw_address_inuse(hass: HomeAssistant, mock_connection: Mock) @pytest.mark.parametrize( ("connection_type", "data"), - ( + [ ("nibegw", MOCK_FLOW_NIBEGW_USERDATA), ("modbus", MOCK_FLOW_MODBUS_USERDATA), - ), + ], ) async def test_read_timeout( hass: HomeAssistant, mock_connection: Mock, connection_type: str, data: dict @@ -166,10 +167,10 @@ async def test_read_timeout( @pytest.mark.parametrize( ("connection_type", "data"), - ( + [ ("nibegw", MOCK_FLOW_NIBEGW_USERDATA), ("modbus", MOCK_FLOW_MODBUS_USERDATA), - ), + ], ) async def test_write_timeout( hass: HomeAssistant, mock_connection: Mock, connection_type: str, data: dict @@ -187,10 +188,10 @@ async def test_write_timeout( @pytest.mark.parametrize( ("connection_type", "data"), - ( + [ ("nibegw", MOCK_FLOW_NIBEGW_USERDATA), ("modbus", MOCK_FLOW_MODBUS_USERDATA), - ), + ], ) async def test_unexpected_exception( hass: HomeAssistant, mock_connection: Mock, connection_type: str, data: dict @@ -208,10 +209,10 @@ async def test_unexpected_exception( @pytest.mark.parametrize( ("connection_type", "data"), - ( + [ ("nibegw", MOCK_FLOW_NIBEGW_USERDATA), ("modbus", MOCK_FLOW_MODBUS_USERDATA), - ), + ], ) async def test_nibegw_invalid_host( hass: HomeAssistant, mock_connection: Mock, connection_type: str, data: dict @@ -232,10 +233,10 @@ async def test_nibegw_invalid_host( @pytest.mark.parametrize( ("connection_type", "data"), - ( + [ ("nibegw", MOCK_FLOW_NIBEGW_USERDATA), ("modbus", MOCK_FLOW_MODBUS_USERDATA), - ), + ], ) async def test_model_missing_coil( hass: HomeAssistant, mock_connection: Mock, connection_type: str, data: dict diff --git a/tests/components/nibe_heatpump/test_coordinator.py b/tests/components/nibe_heatpump/test_coordinator.py index 474802541f2..ffd5c545645 100644 --- a/tests/components/nibe_heatpump/test_coordinator.py +++ b/tests/components/nibe_heatpump/test_coordinator.py @@ -1,4 +1,5 @@ """Test the Nibe Heat Pump config flow.""" + import asyncio from typing import Any from unittest.mock import patch diff --git a/tests/components/nibe_heatpump/test_number.py b/tests/components/nibe_heatpump/test_number.py index 5c4d7f4341b..99f8ab22b6c 100644 --- a/tests/components/nibe_heatpump/test_number.py +++ b/tests/components/nibe_heatpump/test_number.py @@ -1,4 +1,5 @@ """Test the Nibe Heat Pump config flow.""" + from typing import Any from unittest.mock import AsyncMock, patch diff --git a/tests/components/nightscout/__init__.py b/tests/components/nightscout/__init__.py index 6c1a34ebe41..da421d5bba9 100644 --- a/tests/components/nightscout/__init__.py +++ b/tests/components/nightscout/__init__.py @@ -1,4 +1,5 @@ """Tests for the Nightscout integration.""" + import json from unittest.mock import patch @@ -35,12 +36,15 @@ async def init_integration(hass) -> MockConfigEntry: domain=DOMAIN, data={CONF_URL: "https://some.url:1234"}, ) - with patch( - "homeassistant.components.nightscout.NightscoutAPI.get_sgvs", - return_value=GLUCOSE_READINGS, - ), patch( - "homeassistant.components.nightscout.NightscoutAPI.get_server_status", - return_value=SERVER_STATUS, + with ( + patch( + "homeassistant.components.nightscout.NightscoutAPI.get_sgvs", + return_value=GLUCOSE_READINGS, + ), + patch( + "homeassistant.components.nightscout.NightscoutAPI.get_server_status", + return_value=SERVER_STATUS, + ), ): entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) @@ -55,12 +59,15 @@ async def init_integration_unavailable(hass) -> MockConfigEntry: domain=DOMAIN, data={CONF_URL: "https://some.url:1234"}, ) - with patch( - "homeassistant.components.nightscout.NightscoutAPI.get_sgvs", - side_effect=ClientConnectionError(), - ), patch( - "homeassistant.components.nightscout.NightscoutAPI.get_server_status", - return_value=SERVER_STATUS, + with ( + patch( + "homeassistant.components.nightscout.NightscoutAPI.get_sgvs", + side_effect=ClientConnectionError(), + ), + patch( + "homeassistant.components.nightscout.NightscoutAPI.get_server_status", + return_value=SERVER_STATUS, + ), ): entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) @@ -75,11 +82,15 @@ async def init_integration_empty_response(hass) -> MockConfigEntry: domain=DOMAIN, data={CONF_URL: "https://some.url:1234"}, ) - with patch( - "homeassistant.components.nightscout.NightscoutAPI.get_sgvs", return_value=[] - ), patch( - "homeassistant.components.nightscout.NightscoutAPI.get_server_status", - return_value=SERVER_STATUS, + with ( + patch( + "homeassistant.components.nightscout.NightscoutAPI.get_sgvs", + return_value=[], + ), + patch( + "homeassistant.components.nightscout.NightscoutAPI.get_server_status", + return_value=SERVER_STATUS, + ), ): entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) diff --git a/tests/components/nightscout/test_config_flow.py b/tests/components/nightscout/test_config_flow.py index b8805b76691..c3723596a84 100644 --- a/tests/components/nightscout/test_config_flow.py +++ b/tests/components/nightscout/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Nightscout config flow.""" + from http import HTTPStatus from unittest.mock import patch @@ -26,7 +27,11 @@ async def test_form(hass: HomeAssistant) -> None: 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: + with ( + _patch_glucose_readings(), + _patch_server_status(), + _patch_async_setup_entry() as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], CONFIG, @@ -64,12 +69,15 @@ async def test_user_form_api_key_required(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch( - "homeassistant.components.nightscout.NightscoutAPI.get_server_status", - return_value=SERVER_STATUS_STATUS_ONLY, - ), patch( - "homeassistant.components.nightscout.NightscoutAPI.get_sgvs", - side_effect=ClientResponseError(None, None, status=HTTPStatus.UNAUTHORIZED), + with ( + patch( + "homeassistant.components.nightscout.NightscoutAPI.get_server_status", + return_value=SERVER_STATUS_STATUS_ONLY, + ), + patch( + "homeassistant.components.nightscout.NightscoutAPI.get_sgvs", + side_effect=ClientResponseError(None, None, status=HTTPStatus.UNAUTHORIZED), + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], diff --git a/tests/components/nightscout/test_init.py b/tests/components/nightscout/test_init.py index f3e7f8cbfd0..eb4d5a068c9 100644 --- a/tests/components/nightscout/test_init.py +++ b/tests/components/nightscout/test_init.py @@ -1,4 +1,5 @@ """Test the Nightscout config flow.""" + from unittest.mock import patch from aiohttp import ClientError diff --git a/tests/components/nightscout/test_sensor.py b/tests/components/nightscout/test_sensor.py index f224fec43a3..dbc7e143c1a 100644 --- a/tests/components/nightscout/test_sensor.py +++ b/tests/components/nightscout/test_sensor.py @@ -1,4 +1,5 @@ """The sensor tests for the Nightscout platform.""" + from homeassistant.components.nightscout.const import ( ATTR_DELTA, ATTR_DEVICE, diff --git a/tests/components/nina/__init__.py b/tests/components/nina/__init__.py index d8a70a180dd..923df6b6337 100644 --- a/tests/components/nina/__init__.py +++ b/tests/components/nina/__init__.py @@ -1,4 +1,5 @@ """Tests for the Nina integration.""" + import json from typing import Any diff --git a/tests/components/nina/test_binary_sensor.py b/tests/components/nina/test_binary_sensor.py index 8532415c6b1..380d16f5101 100644 --- a/tests/components/nina/test_binary_sensor.py +++ b/tests/components/nina/test_binary_sensor.py @@ -1,4 +1,5 @@ """Test the Nina binary sensor.""" + from __future__ import annotations from typing import Any diff --git a/tests/components/nina/test_config_flow.py b/tests/components/nina/test_config_flow.py index aad24691f42..d3c44258c23 100644 --- a/tests/components/nina/test_config_flow.py +++ b/tests/components/nina/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Nina config flow.""" + from __future__ import annotations from copy import deepcopy @@ -93,12 +94,15 @@ async def test_step_user_unexpected_exception(hass: HomeAssistant) -> None: async def test_step_user(hass: HomeAssistant) -> None: """Test starting a flow by user with valid values.""" - with patch( - "pynina.baseApi.BaseAPI._makeRequest", - wraps=mocked_request_function, - ), patch( - "homeassistant.components.nina.async_setup_entry", - return_value=True, + with ( + patch( + "pynina.baseApi.BaseAPI._makeRequest", + wraps=mocked_request_function, + ), + patch( + "homeassistant.components.nina.async_setup_entry", + return_value=True, + ), ): result: dict[str, Any] = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=deepcopy(DUMMY_DATA) @@ -156,11 +160,12 @@ async def test_options_flow_init(hass: HomeAssistant) -> None: ) config_entry.add_to_hass(hass) - with patch( - "homeassistant.components.nina.async_setup_entry", return_value=True - ), patch( - "pynina.baseApi.BaseAPI._makeRequest", - wraps=mocked_request_function, + with ( + patch("homeassistant.components.nina.async_setup_entry", return_value=True), + patch( + "pynina.baseApi.BaseAPI._makeRequest", + wraps=mocked_request_function, + ), ): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -210,11 +215,12 @@ async def test_options_flow_with_no_selection(hass: HomeAssistant) -> None: ) config_entry.add_to_hass(hass) - with patch( - "homeassistant.components.nina.async_setup_entry", return_value=True - ), patch( - "pynina.baseApi.BaseAPI._makeRequest", - wraps=mocked_request_function, + with ( + patch("homeassistant.components.nina.async_setup_entry", return_value=True), + patch( + "pynina.baseApi.BaseAPI._makeRequest", + wraps=mocked_request_function, + ), ): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -251,12 +257,15 @@ async def test_options_flow_connection_error(hass: HomeAssistant) -> None: ) config_entry.add_to_hass(hass) - with patch( - "pynina.baseApi.BaseAPI._makeRequest", - side_effect=ApiError("Could not connect to Api"), - ), patch( - "homeassistant.components.nina.async_setup_entry", - return_value=True, + 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() @@ -276,12 +285,15 @@ async def test_options_flow_unexpected_exception(hass: HomeAssistant) -> None: ) config_entry.add_to_hass(hass) - with patch( - "pynina.baseApi.BaseAPI._makeRequest", - side_effect=Exception("DUMMY"), - ), patch( - "homeassistant.components.nina.async_setup_entry", - return_value=True, + 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() @@ -300,12 +312,15 @@ async def test_options_flow_entity_removal(hass: HomeAssistant) -> None: ) config_entry.add_to_hass(hass) - with patch( - "pynina.baseApi.BaseAPI._makeRequest", - wraps=mocked_request_function, - ), patch( - "homeassistant.components.nina._async_update_listener" - ) as mock_update_listener: + with ( + patch( + "pynina.baseApi.BaseAPI._makeRequest", + wraps=mocked_request_function, + ), + patch( + "homeassistant.components.nina._async_update_listener" + ) as mock_update_listener, + ): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/nina/test_init.py b/tests/components/nina/test_init.py index da73c8d8711..d7c312a8514 100644 --- a/tests/components/nina/test_init.py +++ b/tests/components/nina/test_init.py @@ -1,4 +1,5 @@ """Test the Nina init file.""" + from typing import Any from unittest.mock import patch diff --git a/tests/components/nmap_tracker/test_config_flow.py b/tests/components/nmap_tracker/test_config_flow.py index 95c944449de..ccbdc112e46 100644 --- a/tests/components/nmap_tracker/test_config_flow.py +++ b/tests/components/nmap_tracker/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Nmap Tracker config flow.""" + from unittest.mock import patch import pytest diff --git a/tests/components/no_ip/test_init.py b/tests/components/no_ip/test_init.py index 40eb650242f..576a04c28a0 100644 --- a/tests/components/no_ip/test_init.py +++ b/tests/components/no_ip/test_init.py @@ -1,4 +1,5 @@ """Test the NO-IP component.""" + from datetime import timedelta import pytest diff --git a/tests/components/nobo_hub/test_config_flow.py b/tests/components/nobo_hub/test_config_flow.py index 5cfcee9cdbf..1d6feb6e28a 100644 --- a/tests/components/nobo_hub/test_config_flow.py +++ b/tests/components/nobo_hub/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Nobø Ecohub config flow.""" + from unittest.mock import PropertyMock, patch from homeassistant import config_entries @@ -30,17 +31,19 @@ async def test_configure_with_discover(hass: HomeAssistant) -> None: assert result2["errors"] == {} assert result2["step_id"] == "selected" - with patch( - "pynobo.nobo.async_connect_hub", return_value=True - ) as mock_connect, patch( - "pynobo.nobo.hub_info", - new_callable=PropertyMock, - create=True, - return_value={"name": "My Nobø Ecohub"}, - ), patch( - "homeassistant.components.nobo_hub.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch("pynobo.nobo.async_connect_hub", return_value=True) as mock_connect, + patch( + "pynobo.nobo.hub_info", + new_callable=PropertyMock, + create=True, + return_value={"name": "My Nobø Ecohub"}, + ), + patch( + "homeassistant.components.nobo_hub.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], { @@ -73,17 +76,19 @@ async def test_configure_manual(hass: HomeAssistant) -> None: assert result["errors"] == {} assert result["step_id"] == "manual" - with patch( - "pynobo.nobo.async_connect_hub", return_value=True - ) as mock_connect, patch( - "pynobo.nobo.hub_info", - new_callable=PropertyMock, - create=True, - return_value={"name": "My Nobø Ecohub"}, - ), patch( - "homeassistant.components.nobo_hub.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch("pynobo.nobo.async_connect_hub", return_value=True) as mock_connect, + patch( + "pynobo.nobo.hub_info", + new_callable=PropertyMock, + create=True, + return_value={"name": "My Nobø Ecohub"}, + ), + patch( + "homeassistant.components.nobo_hub.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -124,17 +129,19 @@ async def test_configure_user_selected_manual(hass: HomeAssistant) -> None: assert result2["errors"] == {} assert result2["step_id"] == "manual" - with patch( - "pynobo.nobo.async_connect_hub", return_value=True - ) as mock_connect, patch( - "pynobo.nobo.hub_info", - new_callable=PropertyMock, - create=True, - return_value={"name": "My Nobø Ecohub"}, - ), patch( - "homeassistant.components.nobo_hub.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch("pynobo.nobo.async_connect_hub", return_value=True) as mock_connect, + patch( + "pynobo.nobo.hub_info", + new_callable=PropertyMock, + create=True, + return_value={"name": "My Nobø Ecohub"}, + ), + patch( + "homeassistant.components.nobo_hub.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { diff --git a/tests/components/notify/common.py b/tests/components/notify/common.py index 0bfa3ba7a32..418de96d1aa 100644 --- a/tests/components/notify/common.py +++ b/tests/components/notify/common.py @@ -3,6 +3,7 @@ All containing methods are legacy helpers that should not be used by new components. Instead call the service directly. """ + from homeassistant.components.notify import ( ATTR_DATA, ATTR_MESSAGE, diff --git a/tests/components/notify/test_init.py b/tests/components/notify/test_init.py index 9fb5b9e531a..0b75a3c4691 100644 --- a/tests/components/notify/test_init.py +++ b/tests/components/notify/test_init.py @@ -1,4 +1,5 @@ """The tests for notify services that change targets.""" + import asyncio from pathlib import Path from unittest.mock import Mock, patch diff --git a/tests/components/notify/test_persistent_notification.py b/tests/components/notify/test_persistent_notification.py index 580b2bdb614..bbf571b69ae 100644 --- a/tests/components/notify/test_persistent_notification.py +++ b/tests/components/notify/test_persistent_notification.py @@ -1,4 +1,5 @@ """The tests for the notify.persistent_notification service.""" + from homeassistant.components import notify import homeassistant.components.persistent_notification as pn from homeassistant.core import HomeAssistant diff --git a/tests/components/notify_events/test_init.py b/tests/components/notify_events/test_init.py index 85f196689a3..bc935b31772 100644 --- a/tests/components/notify_events/test_init.py +++ b/tests/components/notify_events/test_init.py @@ -1,4 +1,5 @@ """The tests for notify_events.""" + from homeassistant.components.notify_events.const import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component diff --git a/tests/components/notify_events/test_notify.py b/tests/components/notify_events/test_notify.py index 8df26801f92..dbfc354404b 100644 --- a/tests/components/notify_events/test_notify.py +++ b/tests/components/notify_events/test_notify.py @@ -1,4 +1,5 @@ """The tests for notify_events.""" + from homeassistant.components.notify import ATTR_DATA, ATTR_MESSAGE, DOMAIN from homeassistant.components.notify_events.notify import ( ATTR_LEVEL, diff --git a/tests/components/notion/conftest.py b/tests/components/notion/conftest.py index 366f78b8c6c..e69905ed72c 100644 --- a/tests/components/notion/conftest.py +++ b/tests/components/notion/conftest.py @@ -1,4 +1,5 @@ """Define fixtures for Notion tests.""" + from collections.abc import Generator import json from unittest.mock import AsyncMock, Mock, patch @@ -119,15 +120,19 @@ def get_client_fixture(client): @pytest.fixture(name="mock_aionotion") async def mock_aionotion_fixture(client): """Define a fixture to patch aionotion.""" - with patch( - "homeassistant.components.notion.async_get_client_with_credentials", - AsyncMock(return_value=client), - ), patch( - "homeassistant.components.notion.async_get_client_with_refresh_token", - AsyncMock(return_value=client), - ), patch( - "homeassistant.components.notion.config_flow.async_get_client_with_credentials", - AsyncMock(return_value=client), + with ( + patch( + "homeassistant.components.notion.async_get_client_with_credentials", + AsyncMock(return_value=client), + ), + patch( + "homeassistant.components.notion.async_get_client_with_refresh_token", + AsyncMock(return_value=client), + ), + patch( + "homeassistant.components.notion.config_flow.async_get_client_with_credentials", + AsyncMock(return_value=client), + ), ): yield diff --git a/tests/components/notion/test_config_flow.py b/tests/components/notion/test_config_flow.py index 72bb3dfee0b..827565db339 100644 --- a/tests/components/notion/test_config_flow.py +++ b/tests/components/notion/test_config_flow.py @@ -1,4 +1,5 @@ """Define tests for the Notion config flow.""" + from unittest.mock import AsyncMock, patch from aionotion.errors import InvalidCredentialsError, NotionError diff --git a/tests/components/notion/test_diagnostics.py b/tests/components/notion/test_diagnostics.py index f0ca64807e1..023b9369f03 100644 --- a/tests/components/notion/test_diagnostics.py +++ b/tests/components/notion/test_diagnostics.py @@ -1,4 +1,5 @@ """Test Notion diagnostics.""" + from homeassistant.components.diagnostics import REDACTED from homeassistant.components.notion import DOMAIN from homeassistant.core import HomeAssistant diff --git a/tests/components/nsw_fuel_station/test_sensor.py b/tests/components/nsw_fuel_station/test_sensor.py index 3aa19cdafc2..898d5757870 100644 --- a/tests/components/nsw_fuel_station/test_sensor.py +++ b/tests/components/nsw_fuel_station/test_sensor.py @@ -1,4 +1,5 @@ """The tests for the NSW Fuel Station sensor platform.""" + from unittest.mock import patch from nsw_fuel import FuelCheckError @@ -89,7 +90,7 @@ async def test_setup(get_fuel_prices, hass: HomeAssistant) -> None: def raise_fuel_check_error(): """Raise fuel check error for testing error cases.""" - raise FuelCheckError() + raise FuelCheckError @patch( diff --git a/tests/components/nsw_rural_fire_service_feed/test_geo_location.py b/tests/components/nsw_rural_fire_service_feed/test_geo_location.py index bf56cb8a985..ad987325b97 100644 --- a/tests/components/nsw_rural_fire_service_feed/test_geo_location.py +++ b/tests/components/nsw_rural_fire_service_feed/test_geo_location.py @@ -1,4 +1,5 @@ """The tests for the NSW Rural Fire Service Feeds platform.""" + import datetime from unittest.mock import ANY, MagicMock, call, patch @@ -230,12 +231,13 @@ async def test_setup_with_custom_location(hass: HomeAssistant) -> None: # Set up some mock feed entries for this test. mock_entry_1 = _generate_mock_feed_entry("1234", "Title 1", 20.5, (-31.1, 150.1)) - with patch( - "aio_geojson_nsw_rfs_incidents.feed_manager.NswRuralFireServiceIncidentsFeed", - wraps=NswRuralFireServiceIncidentsFeed, - ) as mock_feed_manager, patch( - "aio_geojson_client.feed.GeoJsonFeed.update" - ) as mock_feed_update: + with ( + patch( + "aio_geojson_nsw_rfs_incidents.feed_manager.NswRuralFireServiceIncidentsFeed", + wraps=NswRuralFireServiceIncidentsFeed, + ) as mock_feed_manager, + patch("aio_geojson_client.feed.GeoJsonFeed.update") as mock_feed_update, + ): mock_feed_update.return_value = "OK", [mock_entry_1] with assert_setup_component(1, geo_location.DOMAIN): diff --git a/tests/components/nuheat/mocks.py b/tests/components/nuheat/mocks.py index a5c2b403948..091734b8075 100644 --- a/tests/components/nuheat/mocks.py +++ b/tests/components/nuheat/mocks.py @@ -1,4 +1,5 @@ """The test for the NuHeat thermostat module.""" + from unittest.mock import MagicMock, Mock from nuheat.config import SCHEDULE_HOLD, SCHEDULE_RUN, SCHEDULE_TEMPORARY_HOLD diff --git a/tests/components/nuheat/test_climate.py b/tests/components/nuheat/test_climate.py index 7a0e21485c8..bc00df126e5 100644 --- a/tests/components/nuheat/test_climate.py +++ b/tests/components/nuheat/test_climate.py @@ -1,4 +1,5 @@ """The test for the NuHeat thermostat module.""" + from datetime import timedelta from unittest.mock import patch diff --git a/tests/components/nuheat/test_config_flow.py b/tests/components/nuheat/test_config_flow.py index 64b7b56348e..1e7a6215143 100644 --- a/tests/components/nuheat/test_config_flow.py +++ b/tests/components/nuheat/test_config_flow.py @@ -1,4 +1,5 @@ """Test the NuHeat config flow.""" + from http import HTTPStatus from unittest.mock import MagicMock, patch @@ -23,15 +24,19 @@ async def test_form_user(hass: HomeAssistant) -> None: mock_thermostat = _get_mock_thermostat_run() - with patch( - "homeassistant.components.nuheat.config_flow.nuheat.NuHeat.authenticate", - return_value=True, - ), patch( - "homeassistant.components.nuheat.config_flow.nuheat.NuHeat.get_thermostat", - return_value=mock_thermostat, - ), patch( - "homeassistant.components.nuheat.async_setup_entry", return_value=True - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.nuheat.config_flow.nuheat.NuHeat.authenticate", + return_value=True, + ), + patch( + "homeassistant.components.nuheat.config_flow.nuheat.NuHeat.get_thermostat", + return_value=mock_thermostat, + ), + patch( + "homeassistant.components.nuheat.async_setup_entry", return_value=True + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -102,12 +107,15 @@ async def test_form_invalid_thermostat(hass: HomeAssistant) -> None: response_mock = MagicMock() type(response_mock).status_code = HTTPStatus.INTERNAL_SERVER_ERROR - with patch( - "homeassistant.components.nuheat.config_flow.nuheat.NuHeat.authenticate", - return_value=True, - ), patch( - "homeassistant.components.nuheat.config_flow.nuheat.NuHeat.get_thermostat", - side_effect=requests.HTTPError(response=response_mock), + with ( + patch( + "homeassistant.components.nuheat.config_flow.nuheat.NuHeat.authenticate", + return_value=True, + ), + patch( + "homeassistant.components.nuheat.config_flow.nuheat.NuHeat.get_thermostat", + side_effect=requests.HTTPError(response=response_mock), + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], diff --git a/tests/components/nuheat/test_init.py b/tests/components/nuheat/test_init.py index 0e1686dbfde..15829935dab 100644 --- a/tests/components/nuheat/test_init.py +++ b/tests/components/nuheat/test_init.py @@ -1,4 +1,5 @@ """NuHeat component tests.""" + from unittest.mock import patch from homeassistant.components.nuheat.const import DOMAIN diff --git a/tests/components/nuki/test_config_flow.py b/tests/components/nuki/test_config_flow.py index b5f1b65af71..c7575f71545 100644 --- a/tests/components/nuki/test_config_flow.py +++ b/tests/components/nuki/test_config_flow.py @@ -1,4 +1,5 @@ """Test the nuki config flow.""" + from unittest.mock import patch from pynuki.bridge import InvalidCredentialsException @@ -22,13 +23,16 @@ async def test_form(hass: HomeAssistant) -> None: assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} - with patch( - "homeassistant.components.nuki.config_flow.NukiBridge.info", - return_value=MOCK_INFO, - ), patch( - "homeassistant.components.nuki.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.nuki.config_flow.NukiBridge.info", + return_value=MOCK_INFO, + ), + patch( + "homeassistant.components.nuki.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -155,13 +159,16 @@ async def test_dhcp_flow(hass: HomeAssistant) -> None: assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == config_entries.SOURCE_USER - with patch( - "homeassistant.components.nuki.config_flow.NukiBridge.info", - return_value=MOCK_INFO, - ), patch( - "homeassistant.components.nuki.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.nuki.config_flow.NukiBridge.info", + return_value=MOCK_INFO, + ), + patch( + "homeassistant.components.nuki.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -208,12 +215,15 @@ async def test_reauth_success(hass: HomeAssistant) -> None: assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" - with patch( - "homeassistant.components.nuki.config_flow.NukiBridge.info", - return_value=MOCK_INFO, - ), patch( - "homeassistant.components.nuki.async_setup_entry", - return_value=True, + with ( + patch( + "homeassistant.components.nuki.config_flow.NukiBridge.info", + return_value=MOCK_INFO, + ), + patch( + "homeassistant.components.nuki.async_setup_entry", + return_value=True, + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], diff --git a/tests/components/numato/numato_mock.py b/tests/components/numato/numato_mock.py index 1f8b24027de..097a785beb1 100644 --- a/tests/components/numato/numato_mock.py +++ b/tests/components/numato/numato_mock.py @@ -1,4 +1,5 @@ """Mockup for the numato component interface.""" + from numato_gpio import NumatoGpioError @@ -61,7 +62,8 @@ class NumatoModuleMock: Ignore the device list argument, mock discovers /dev/ttyACM0. """ - self.devices[0] = NumatoModuleMock.NumatoDeviceMock("/dev/ttyACM0") + if not self.devices: + self.devices[0] = NumatoModuleMock.NumatoDeviceMock("/dev/ttyACM0") def cleanup(self): """Mockup for the numato device cleanup.""" diff --git a/tests/components/numato/test_binary_sensor.py b/tests/components/numato/test_binary_sensor.py index 3aa6f027af2..e353de5e7df 100644 --- a/tests/components/numato/test_binary_sensor.py +++ b/tests/components/numato/test_binary_sensor.py @@ -1,10 +1,17 @@ """Tests for the numato binary_sensor platform.""" + +import logging +from unittest.mock import patch + +import pytest + from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import discovery from homeassistant.setup import async_setup_component from .common import NUMATO_CFG, mockup_raise +from .numato_mock import NumatoGpioError, NumatoModuleMock MOCKUP_ENTITY_IDS = { "binary_sensor.numato_binary_sensor_mock_port2", @@ -24,23 +31,25 @@ async def test_failing_setups_no_entities( assert entity_id not in hass.states.async_entity_ids() -async def test_setup_callbacks( - hass: HomeAssistant, numato_fixture, monkeypatch -) -> None: +async def test_setup_callbacks(hass: HomeAssistant, numato_fixture) -> None: """During setup a callback shall be registered.""" - numato_fixture.discover() + with patch.object( + NumatoModuleMock.NumatoDeviceMock, "add_event_detect" + ) as mock_add_event_detect: + numato_fixture.discover() + assert await async_setup_component(hass, "numato", NUMATO_CFG) + await hass.async_block_till_done() # wait until services are registered - def mock_add_event_detect(self, port, callback, direction): - assert self == numato_fixture.devices[0] - assert port == 1 - assert callback is callable - assert direction == numato_fixture.BOTH - - monkeypatch.setattr( - numato_fixture.devices[0], "add_event_detect", mock_add_event_detect + mock_add_event_detect.assert_called() + assert {call.args[0] for call in mock_add_event_detect.mock_calls} == { + int(port) + for port in NUMATO_CFG["numato"]["devices"][0]["binary_sensors"]["ports"] + } + assert all(callable(call.args[1]) for call in mock_add_event_detect.mock_calls) + assert all( + call.args[2] == numato_fixture.BOTH for call in mock_add_event_detect.mock_calls ) - assert await async_setup_component(hass, "numato", NUMATO_CFG) async def test_hass_binary_sensor_notification( @@ -72,3 +81,32 @@ async def test_binary_sensor_setup_without_discovery_info( await hass.async_block_till_done() # wait for numato platform to be loaded for entity_id in MOCKUP_ENTITY_IDS: assert entity_id in hass.states.async_entity_ids() + + +async def test_binary_sensor_setup_no_notify( + hass: HomeAssistant, + numato_fixture, + caplog: pytest.LogCaptureFixture, +) -> None: + """Setup of a device without notification capability shall print an info message.""" + caplog.set_level(logging.INFO) + + def raise_notification_error(self, port, callback, direction): + raise NumatoGpioError( + f"{repr(self)} Mockup device doesn't support notifications." + ) + + with patch.object( + NumatoModuleMock.NumatoDeviceMock, + "add_event_detect", + raise_notification_error, + ): + numato_fixture.discover() + assert await async_setup_component(hass, "numato", NUMATO_CFG) + await hass.async_block_till_done() # wait until services are registered + + assert all( + f"updates on binary sensor numato_binary_sensor_mock_port{port} only in polling mode" + in caplog.text + for port in NUMATO_CFG["numato"]["devices"][0]["binary_sensors"]["ports"] + ) diff --git a/tests/components/numato/test_init.py b/tests/components/numato/test_init.py index 827d3daa737..1e84813df94 100644 --- a/tests/components/numato/test_init.py +++ b/tests/components/numato/test_init.py @@ -1,4 +1,5 @@ """Tests for the numato integration.""" + from numato_gpio import NumatoGpioError import pytest diff --git a/tests/components/numato/test_sensor.py b/tests/components/numato/test_sensor.py index e5e871ca9b5..30a9f174941 100644 --- a/tests/components/numato/test_sensor.py +++ b/tests/components/numato/test_sensor.py @@ -1,4 +1,5 @@ """Tests for the numato sensor platform.""" + from homeassistant.const import STATE_UNKNOWN, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import discovery diff --git a/tests/components/numato/test_switch.py b/tests/components/numato/test_switch.py index de57aba7bc7..e69b3481b1d 100644 --- a/tests/components/numato/test_switch.py +++ b/tests/components/numato/test_switch.py @@ -1,4 +1,5 @@ """Tests for the numato switch platform.""" + from homeassistant.components import switch from homeassistant.const import ( ATTR_ENTITY_ID, diff --git a/tests/testing_config/custom_components/test/number.py b/tests/components/number/common.py similarity index 53% rename from tests/testing_config/custom_components/test/number.py rename to tests/components/number/common.py index 3d9a7f4a8c7..9843fea7751 100644 --- a/tests/testing_config/custom_components/test/number.py +++ b/tests/components/number/common.py @@ -1,15 +1,9 @@ -"""Provide a mock number platform. +"""Common helper and classes for number entity tests.""" -Call init before using it in your tests to ensure clean test data. -""" from homeassistant.components.number import NumberEntity, RestoreNumber from tests.common import MockEntity -UNIQUE_NUMBER = "unique_number" - -ENTITIES = [] - class MockNumberEntity(MockEntity, NumberEntity): """Mock number class.""" @@ -55,59 +49,7 @@ class MockRestoreNumber(MockNumberEntity, RestoreNumber): self._values["native_max_value"] = last_number_data.native_max_value self._values["native_min_value"] = last_number_data.native_min_value self._values["native_step"] = last_number_data.native_step - self._values[ - "native_unit_of_measurement" - ] = last_number_data.native_unit_of_measurement + self._values["native_unit_of_measurement"] = ( + last_number_data.native_unit_of_measurement + ) self._values["native_value"] = last_number_data.native_value - - -class LegacyMockNumberEntity(MockEntity, NumberEntity): - """Mock Number class using deprecated features.""" - - @property - def max_value(self): - """Return the native max_value.""" - return self._handle("max_value") - - @property - def min_value(self): - """Return the native min_value.""" - return self._handle("min_value") - - @property - def step(self): - """Return the native step.""" - return self._handle("step") - - @property - def value(self): - """Return the current value.""" - return self._handle("value") - - def set_value(self, value: float) -> None: - """Change the selected option.""" - self._values["value"] = value - - -def init(empty=False): - """Initialize the platform with entities.""" - global ENTITIES - - ENTITIES = ( - [] - if empty - else [ - MockNumberEntity( - name="test", - unique_id=UNIQUE_NUMBER, - native_value=50.0, - ), - ] - ) - - -async def async_setup_platform( - hass, config, async_add_entities_callback, discovery_info=None -): - """Return mock entities.""" - async_add_entities_callback(ENTITIES) diff --git a/tests/components/number/conftest.py b/tests/components/number/conftest.py new file mode 100644 index 00000000000..a84ab03611b --- /dev/null +++ b/tests/components/number/conftest.py @@ -0,0 +1,19 @@ +"""Fixtures for the number entity component tests.""" + +import pytest + +from tests.components.number.common import MockNumberEntity + +UNIQUE_NUMBER = "unique_number" + + +@pytest.fixture +def mock_number_entities() -> list[MockNumberEntity]: + """Return a list of mock number entities.""" + return [ + MockNumberEntity( + name="test", + unique_id="unique_number", + native_value=50.0, + ), + ] diff --git a/tests/components/number/test_device_action.py b/tests/components/number/test_device_action.py index 17c63dd3394..0c34f1bf53c 100644 --- a/tests/components/number/test_device_action.py +++ b/tests/components/number/test_device_action.py @@ -1,4 +1,5 @@ """The tests for Number device actions.""" + import pytest from pytest_unordered import unordered import voluptuous_serialize @@ -61,12 +62,12 @@ async def test_get_actions( @pytest.mark.parametrize( ("hidden_by", "entity_category"), - ( + [ (RegistryEntryHider.INTEGRATION, None), (RegistryEntryHider.USER, None), (None, EntityCategory.CONFIG), (None, EntityCategory.DIAGNOSTIC), - ), + ], ) async def test_get_actions_hidden_auxiliary( hass: HomeAssistant, diff --git a/tests/components/number/test_init.py b/tests/components/number/test_init.py index 279ffbfbbaa..07d2baf4926 100644 --- a/tests/components/number/test_init.py +++ b/tests/components/number/test_init.py @@ -1,4 +1,5 @@ """The tests for the Number component.""" + from collections.abc import Generator from typing import Any from unittest.mock import MagicMock @@ -50,7 +51,9 @@ from tests.common import ( mock_integration, mock_platform, mock_restore_cache_with_extra_data, + setup_test_component_platform, ) +from tests.components.number import common TEST_DOMAIN = "test" @@ -331,10 +334,12 @@ async def test_sync_set_value(hass: HomeAssistant) -> None: assert number.set_value.call_args[0][0] == 42 -async def test_set_value(hass: HomeAssistant, enable_custom_integrations: None) -> None: +async def test_set_value( + hass: HomeAssistant, + mock_number_entities: list[MockNumberEntity], +) -> None: """Test we can only set valid values.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() + setup_test_component_platform(hass, DOMAIN, mock_number_entities) assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) await hass.async_block_till_done() @@ -449,7 +454,6 @@ async def test_set_value(hass: HomeAssistant, enable_custom_integrations: None) ) async def test_temperature_conversion( hass: HomeAssistant, - enable_custom_integrations: None, unit_system, native_unit, state_unit, @@ -466,21 +470,17 @@ async def test_temperature_conversion( ) -> None: """Test temperature conversion.""" hass.config.units = unit_system - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init(empty=True) - platform.ENTITIES.append( - platform.MockNumberEntity( - name="Test", - native_max_value=native_max_value, - native_min_value=native_min_value, - native_step=native_step, - native_unit_of_measurement=native_unit, - native_value=initial_native_value, - device_class=NumberDeviceClass.TEMPERATURE, - ) + entity0 = common.MockNumberEntity( + name="Test", + native_max_value=native_max_value, + native_min_value=native_min_value, + native_step=native_step, + native_unit_of_measurement=native_unit, + native_value=initial_native_value, + device_class=NumberDeviceClass.TEMPERATURE, ) + setup_test_component_platform(hass, DOMAIN, [entity0]) - entity0 = platform.ENTITIES[0] assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) await hass.async_block_till_done() @@ -543,24 +543,19 @@ RESTORE_DATA = { async def test_restore_number_save_state( hass: HomeAssistant, hass_storage: dict[str, Any], - enable_custom_integrations: None, ) -> None: """Test RestoreNumber.""" - platform = getattr(hass.components, "test.number") - platform.init(empty=True) - platform.ENTITIES.append( - platform.MockRestoreNumber( - name="Test", - native_max_value=200.0, - native_min_value=-10.0, - native_step=2.0, - native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, - native_value=123.0, - device_class=NumberDeviceClass.TEMPERATURE, - ) + entity0 = common.MockRestoreNumber( + name="Test", + native_max_value=200.0, + native_min_value=-10.0, + native_step=2.0, + native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, + native_value=123.0, + device_class=NumberDeviceClass.TEMPERATURE, ) + setup_test_component_platform(hass, DOMAIN, [entity0]) - entity0 = platform.ENTITIES[0] assert await async_setup_component(hass, "number", {"number": {"platform": "test"}}) await hass.async_block_till_done() @@ -614,7 +609,6 @@ async def test_restore_number_save_state( ) async def test_restore_number_restore_state( hass: HomeAssistant, - enable_custom_integrations: None, hass_storage: dict[str, Any], native_max_value, native_min_value, @@ -628,17 +622,13 @@ async def test_restore_number_restore_state( """Test RestoreNumber.""" mock_restore_cache_with_extra_data(hass, ((State("number.test", ""), extra_data),)) - platform = getattr(hass.components, "test.number") - platform.init(empty=True) - platform.ENTITIES.append( - platform.MockRestoreNumber( - device_class=device_class, - name="Test", - native_value=None, - ) + entity0 = common.MockRestoreNumber( + device_class=device_class, + name="Test", + native_value=None, ) + setup_test_component_platform(hass, DOMAIN, [entity0]) - entity0 = platform.ENTITIES[0] assert await async_setup_component(hass, "number", {"number": {"platform": "test"}}) await hass.async_block_till_done() @@ -707,7 +697,6 @@ async def test_restore_number_restore_state( ) async def test_custom_unit( hass: HomeAssistant, - enable_custom_integrations: None, device_class, native_unit, custom_unit, @@ -724,19 +713,15 @@ async def test_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 = common.MockNumberEntity( + name="Test", + native_value=native_value, + native_unit_of_measurement=native_unit, + device_class=device_class, + unique_id="very_unique", ) + setup_test_component_platform(hass, DOMAIN, [entity0]) - entity0 = platform.ENTITIES[0] assert await async_setup_component(hass, "number", {"number": {"platform": "test"}}) await hass.async_block_till_done() @@ -788,7 +773,6 @@ async def test_custom_unit( ) async def test_custom_unit_change( hass: HomeAssistant, - enable_custom_integrations: None, native_unit, custom_unit, used_custom_unit, @@ -799,19 +783,15 @@ async def test_custom_unit_change( ) -> None: """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 = common.MockNumberEntity( + name="Test", + native_value=native_value, + native_unit_of_measurement=native_unit, + device_class=NumberDeviceClass.TEMPERATURE, + unique_id="very_unique", ) + setup_test_component_platform(hass, DOMAIN, [entity0]) - entity0 = platform.ENTITIES[0] assert await async_setup_component(hass, "number", {"number": {"platform": "test"}}) await hass.async_block_till_done() diff --git a/tests/components/number/test_recorder.py b/tests/components/number/test_recorder.py index 635354b1176..9117124aac3 100644 --- a/tests/components/number/test_recorder.py +++ b/tests/components/number/test_recorder.py @@ -1,4 +1,5 @@ """The tests for number recorder.""" + from __future__ import annotations from datetime import timedelta diff --git a/tests/components/number/test_reproduce_state.py b/tests/components/number/test_reproduce_state.py index 01b0383ecf7..e941291c04e 100644 --- a/tests/components/number/test_reproduce_state.py +++ b/tests/components/number/test_reproduce_state.py @@ -1,4 +1,5 @@ """Test reproduce state for Number entities.""" + import pytest from homeassistant.components.number.const import ( diff --git a/tests/components/number/test_significant_change.py b/tests/components/number/test_significant_change.py index 1a6491f3de9..e0e02fc1d35 100644 --- a/tests/components/number/test_significant_change.py +++ b/tests/components/number/test_significant_change.py @@ -1,4 +1,5 @@ """Test the Number significant change platform.""" + import pytest from homeassistant.components.number import NumberDeviceClass diff --git a/tests/components/number/test_websocket_api.py b/tests/components/number/test_websocket_api.py index 194f24dc9da..a405ef8c2fc 100644 --- a/tests/components/number/test_websocket_api.py +++ b/tests/components/number/test_websocket_api.py @@ -1,4 +1,5 @@ """Test the number websocket API.""" + from homeassistant.components.number.const import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component diff --git a/tests/components/nut/test_config_flow.py b/tests/components/nut/test_config_flow.py index 46bc2bc2a64..56a7d7d9089 100644 --- a/tests/components/nut/test_config_flow.py +++ b/tests/components/nut/test_config_flow.py @@ -1,8 +1,9 @@ """Test the Network UPS Tools (NUT) config flow.""" + from ipaddress import ip_address from unittest.mock import patch -from pynut2.nut2 import PyNUTError +from aionut import NUTError, NUTLoginError from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components import zeroconf @@ -19,7 +20,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant -from .util import _get_mock_pynutclient +from .util import _get_mock_nutclient from tests.common import MockConfigEntry @@ -50,17 +51,20 @@ async def test_form_zeroconf(hass: HomeAssistant) -> None: assert result["step_id"] == "user" assert result["errors"] == {} - mock_pynut = _get_mock_pynutclient( + mock_pynut = _get_mock_nutclient( list_vars={"battery.voltage": "voltage", "ups.status": "OL"}, list_ups=["ups1"] ) - with patch( - "homeassistant.components.nut.PyNUTClient", - return_value=mock_pynut, - ), patch( - "homeassistant.components.nut.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.nut.AIONUTClient", + return_value=mock_pynut, + ), + patch( + "homeassistant.components.nut.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_USERNAME: "test-username", CONF_PASSWORD: "test-password"}, @@ -88,17 +92,20 @@ async def test_form_user_one_ups(hass: HomeAssistant) -> None: assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} - mock_pynut = _get_mock_pynutclient( + mock_pynut = _get_mock_nutclient( list_vars={"battery.voltage": "voltage", "ups.status": "OL"}, list_ups=["ups1"] ) - with patch( - "homeassistant.components.nut.PyNUTClient", - return_value=mock_pynut, - ), patch( - "homeassistant.components.nut.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.nut.AIONUTClient", + return_value=mock_pynut, + ), + patch( + "homeassistant.components.nut.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -137,13 +144,13 @@ async def test_form_user_multiple_ups(hass: HomeAssistant) -> None: assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} - mock_pynut = _get_mock_pynutclient( + mock_pynut = _get_mock_nutclient( list_vars={"battery.voltage": "voltage"}, list_ups={"ups1": "UPS 1", "ups2": "UPS2"}, ) with patch( - "homeassistant.components.nut.PyNUTClient", + "homeassistant.components.nut.AIONUTClient", return_value=mock_pynut, ): result2 = await hass.config_entries.flow.async_configure( @@ -159,13 +166,16 @@ async def test_form_user_multiple_ups(hass: HomeAssistant) -> None: assert result2["step_id"] == "ups" assert result2["type"] == data_entry_flow.FlowResultType.FORM - with patch( - "homeassistant.components.nut.PyNUTClient", - return_value=mock_pynut, - ), patch( - "homeassistant.components.nut.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.nut.AIONUTClient", + return_value=mock_pynut, + ), + patch( + "homeassistant.components.nut.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], {CONF_ALIAS: "ups2"}, @@ -198,17 +208,20 @@ async def test_form_user_one_ups_with_ignored_entry(hass: HomeAssistant) -> None assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} - mock_pynut = _get_mock_pynutclient( + mock_pynut = _get_mock_nutclient( list_vars={"battery.voltage": "voltage", "ups.status": "OL"}, list_ups=["ups1"] ) - with patch( - "homeassistant.components.nut.PyNUTClient", - return_value=mock_pynut, - ), patch( - "homeassistant.components.nut.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.nut.AIONUTClient", + return_value=mock_pynut, + ), + patch( + "homeassistant.components.nut.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -231,16 +244,16 @@ async def test_form_user_one_ups_with_ignored_entry(hass: HomeAssistant) -> None assert len(mock_setup_entry.mock_calls) == 1 -async def test_form_cannot_connect(hass: HomeAssistant) -> None: - """Test we handle cannot connect error.""" +async def test_form_no_upses_found(hass: HomeAssistant) -> None: + """Test we abort when the NUT server has not UPSes.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - mock_pynut = _get_mock_pynutclient() + mock_pynut = _get_mock_nutclient() with patch( - "homeassistant.components.nut.PyNUTClient", + "homeassistant.components.nut.AIONUTClient", return_value=mock_pynut, ): result2 = await hass.config_entries.flow.async_configure( @@ -253,15 +266,25 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == data_entry_flow.FlowResultType.FORM - assert result2["errors"] == {"base": "cannot_connect"} + assert result2["type"] is data_entry_flow.FlowResultType.ABORT + assert result2["reason"] == "no_ups_found" - with patch( - "homeassistant.components.nut.PyNUTClient.list_ups", - side_effect=PyNUTError, - ), patch( - "homeassistant.components.nut.PyNUTClient.list_vars", - side_effect=PyNUTError, + +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( + "homeassistant.components.nut.AIONUTClient.list_ups", + side_effect=NUTError("no route to host"), + ), + patch( + "homeassistant.components.nut.AIONUTClient.list_vars", + side_effect=NUTError("no route to host"), + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -275,13 +298,17 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} + assert result2["description_placeholders"] == {"error": "no route to host"} - with patch( - "homeassistant.components.nut.PyNUTClient.list_ups", - return_value=["ups1"], - ), patch( - "homeassistant.components.nut.PyNUTClient.list_vars", - side_effect=TypeError, + with ( + patch( + "homeassistant.components.nut.AIONUTClient.list_ups", + return_value={"ups1"}, + ), + patch( + "homeassistant.components.nut.AIONUTClient.list_vars", + side_effect=Exception, + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -296,6 +323,169 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} + mock_pynut = _get_mock_nutclient( + list_vars={"battery.voltage": "voltage", "ups.status": "OL"}, list_ups=["ups1"] + ) + with ( + patch( + "homeassistant.components.nut.AIONUTClient", + return_value=mock_pynut, + ), + patch( + "homeassistant.components.nut.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: "1.1.1.1", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + CONF_PORT: 2222, + }, + ) + await hass.async_block_till_done() + + assert result2["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["title"] == "1.1.1.1:2222" + assert result2["data"] == { + CONF_HOST: "1.1.1.1", + CONF_PASSWORD: "test-password", + CONF_PORT: 2222, + CONF_USERNAME: "test-username", + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_auth_failures(hass: HomeAssistant) -> None: + """Test authentication failures.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with ( + patch( + "homeassistant.components.nut.AIONUTClient.list_ups", + side_effect=NUTLoginError, + ), + patch( + "homeassistant.components.nut.AIONUTClient.list_vars", + side_effect=NUTLoginError, + ), + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: "1.1.1.1", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + CONF_PORT: 2222, + }, + ) + + assert result2["type"] is data_entry_flow.FlowResultType.FORM + assert result2["errors"] == {"password": "invalid_auth"} + + mock_pynut = _get_mock_nutclient( + list_vars={"battery.voltage": "voltage", "ups.status": "OL"}, list_ups=["ups1"] + ) + with ( + patch( + "homeassistant.components.nut.AIONUTClient", + return_value=mock_pynut, + ), + patch( + "homeassistant.components.nut.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: "1.1.1.1", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + CONF_PORT: 2222, + }, + ) + await hass.async_block_till_done() + + assert result2["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["title"] == "1.1.1.1:2222" + assert result2["data"] == { + CONF_HOST: "1.1.1.1", + CONF_PASSWORD: "test-password", + CONF_PORT: 2222, + CONF_USERNAME: "test-username", + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_reauth(hass: HomeAssistant) -> None: + """Test reauth flow.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: "1.1.1.1", + CONF_PORT: 123, + CONF_RESOURCES: ["battery.voltage"], + }, + ) + config_entry.add_to_hass(hass) + config_entry.async_start_reauth(hass) + await hass.async_block_till_done() + flows = hass.config_entries.flow.async_progress_by_handler(DOMAIN) + assert len(flows) == 1 + flow = flows[0] + + with ( + patch( + "homeassistant.components.nut.AIONUTClient.list_ups", + side_effect=NUTLoginError, + ), + patch( + "homeassistant.components.nut.AIONUTClient.list_vars", + side_effect=NUTLoginError, + ), + ): + result2 = await hass.config_entries.flow.async_configure( + flow["flow_id"], + { + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + }, + ) + + assert result2["type"] is data_entry_flow.FlowResultType.FORM + assert result2["errors"] == {"password": "invalid_auth"} + + mock_pynut = _get_mock_nutclient( + list_vars={"battery.voltage": "voltage", "ups.status": "OL"}, list_ups=["ups1"] + ) + with ( + patch( + "homeassistant.components.nut.AIONUTClient", + return_value=mock_pynut, + ), + patch( + "homeassistant.components.nut.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): + result2 = await hass.config_entries.flow.async_configure( + flow["flow_id"], + { + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] is data_entry_flow.FlowResultType.ABORT + assert result2["reason"] == "reauth_successful" + assert len(mock_setup_entry.mock_calls) == 1 + async def test_abort_if_already_setup(hass: HomeAssistant) -> None: """Test we abort if component is already setup.""" @@ -313,13 +503,13 @@ async def test_abort_if_already_setup(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - mock_pynut = _get_mock_pynutclient( + mock_pynut = _get_mock_nutclient( list_vars={"battery.voltage": "voltage"}, list_ups={"ups1": "UPS 1"}, ) with patch( - "homeassistant.components.nut.PyNUTClient", + "homeassistant.components.nut.AIONUTClient", return_value=mock_pynut, ): result2 = await hass.config_entries.flow.async_configure( @@ -351,13 +541,13 @@ async def test_abort_if_already_setup_alias(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - mock_pynut = _get_mock_pynutclient( + mock_pynut = _get_mock_nutclient( list_vars={"battery.voltage": "voltage"}, list_ups={"ups1": "UPS 1", "ups2": "UPS 2"}, ) with patch( - "homeassistant.components.nut.PyNUTClient", + "homeassistant.components.nut.AIONUTClient", return_value=mock_pynut, ): result2 = await hass.config_entries.flow.async_configure( @@ -372,7 +562,7 @@ async def test_abort_if_already_setup_alias(hass: HomeAssistant) -> None: assert result2["type"] == data_entry_flow.FlowResultType.FORM with patch( - "homeassistant.components.nut.PyNUTClient", + "homeassistant.components.nut.AIONUTClient", return_value=mock_pynut, ): result3 = await hass.config_entries.flow.async_configure( diff --git a/tests/components/nut/test_device_action.py b/tests/components/nut/test_device_action.py index c15a2157343..8113b19e313 100644 --- a/tests/components/nut/test_device_action.py +++ b/tests/components/nut/test_device_action.py @@ -1,7 +1,8 @@ """The tests for Network UPS Tools (NUT) device actions.""" -from unittest.mock import MagicMock -from pynut2.nut2 import PyNUTError +from unittest.mock import AsyncMock + +from aionut import NUTError import pytest from pytest_unordered import unordered @@ -98,7 +99,7 @@ async def test_list_commands_exception( ) -> None: """Test there are no actions if list_commands raises exception.""" await async_init_integration( - hass, list_vars={"ups.status": "OL"}, list_commands_side_effect=PyNUTError + hass, list_vars={"ups.status": "OL"}, list_commands_side_effect=NUTError ) device_entry = next(device for device in device_registry.devices.values()) @@ -136,7 +137,7 @@ async def test_action(hass: HomeAssistant, device_registry: dr.DeviceRegistry) - "beeper.enable": None, "beeper.disable": None, } - run_command = MagicMock() + run_command = AsyncMock() await async_init_integration( hass, list_ups={"someUps": "Some UPS"}, @@ -195,7 +196,7 @@ async def test_rund_command_exception( list_commands_return_value = {"beeper.enable": None} error_message = "Something wrong happened" - run_command = MagicMock(side_effect=PyNUTError(error_message)) + run_command = AsyncMock(side_effect=NUTError(error_message)) await async_init_integration( hass, list_vars={"ups.status": "OL"}, diff --git a/tests/components/nut/test_init.py b/tests/components/nut/test_init.py index 698e182c4f7..61a5187407b 100644 --- a/tests/components/nut/test_init.py +++ b/tests/components/nut/test_init.py @@ -1,12 +1,15 @@ """Test init of Nut integration.""" + from unittest.mock import patch +from aionut import NUTError, NUTLoginError + from homeassistant.components.nut.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_HOST, CONF_PORT, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant -from .util import _get_mock_pynutclient +from .util import _get_mock_nutclient from tests.common import MockConfigEntry @@ -19,12 +22,12 @@ async def test_async_setup_entry(hass: HomeAssistant) -> None: ) entry.add_to_hass(hass) - mock_pynut = _get_mock_pynutclient( + mock_pynut = _get_mock_nutclient( list_ups={"ups1": "UPS 1"}, list_vars={"ups.status": "OL"} ) with patch( - "homeassistant.components.nut.PyNUTClient", + "homeassistant.components.nut.AIONUTClient", return_value=mock_pynut, ): await hass.config_entries.async_setup(entry.entry_id) @@ -53,13 +56,43 @@ async def test_config_not_ready(hass: HomeAssistant) -> None: ) entry.add_to_hass(hass) - with patch( - "homeassistant.components.nut.PyNUTClient.list_ups", - return_value=["ups1"], - ), patch( - "homeassistant.components.nut.PyNUTClient.list_vars", - side_effect=ConnectionResetError, + with ( + patch( + "homeassistant.components.nut.AIONUTClient.list_ups", + return_value={"ups1"}, + ), + patch( + "homeassistant.components.nut.AIONUTClient.list_vars", + side_effect=NUTError, + ), ): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() assert entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_auth_fails(hass: HomeAssistant) -> None: + """Test for setup failure if auth has changed.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: "mock", CONF_PORT: "mock"}, + ) + entry.add_to_hass(hass) + + with ( + patch( + "homeassistant.components.nut.AIONUTClient.list_ups", + return_value={"ups1"}, + ), + patch( + "homeassistant.components.nut.AIONUTClient.list_vars", + side_effect=NUTLoginError, + ), + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert entry.state is ConfigEntryState.SETUP_ERROR + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + assert flows[0]["context"]["source"] == "reauth" diff --git a/tests/components/nut/test_sensor.py b/tests/components/nut/test_sensor.py index 014e683b30c..c4a8159b8cc 100644 --- a/tests/components/nut/test_sensor.py +++ b/tests/components/nut/test_sensor.py @@ -1,4 +1,5 @@ """The sensor tests for the nut platform.""" + from unittest.mock import patch import pytest @@ -14,7 +15,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from .util import _get_mock_pynutclient, async_init_integration +from .util import _get_mock_nutclient, async_init_integration from tests.common import MockConfigEntry @@ -99,12 +100,12 @@ async def test_state_sensors(hass: HomeAssistant) -> None: ) entry.add_to_hass(hass) - mock_pynut = _get_mock_pynutclient( + mock_pynut = _get_mock_nutclient( list_ups={"ups1": "UPS 1"}, list_vars={"ups.status": "OL"} ) with patch( - "homeassistant.components.nut.PyNUTClient", + "homeassistant.components.nut.AIONUTClient", return_value=mock_pynut, ): await hass.config_entries.async_setup(entry.entry_id) @@ -124,12 +125,12 @@ async def test_unknown_state_sensors(hass: HomeAssistant) -> None: ) entry.add_to_hass(hass) - mock_pynut = _get_mock_pynutclient( + mock_pynut = _get_mock_nutclient( list_ups={"ups1": "UPS 1"}, list_vars={"ups.status": "OQ"} ) with patch( - "homeassistant.components.nut.PyNUTClient", + "homeassistant.components.nut.AIONUTClient", return_value=mock_pynut, ): await hass.config_entries.async_setup(entry.entry_id) @@ -154,12 +155,12 @@ async def test_stale_options(hass: HomeAssistant) -> None: ) config_entry.add_to_hass(hass) - mock_pynut = _get_mock_pynutclient( + mock_pynut = _get_mock_nutclient( list_ups={"ups1": "UPS 1"}, list_vars={"battery.charge": "10"} ) with patch( - "homeassistant.components.nut.PyNUTClient", + "homeassistant.components.nut.AIONUTClient", return_value=mock_pynut, ): await hass.config_entries.async_setup(config_entry.entry_id) diff --git a/tests/components/nut/util.py b/tests/components/nut/util.py index a0fadf47f19..3bc48764816 100644 --- a/tests/components/nut/util.py +++ b/tests/components/nut/util.py @@ -1,7 +1,7 @@ """Tests for the nut integration.""" import json -from unittest.mock import MagicMock, patch +from unittest.mock import AsyncMock, MagicMock, patch from homeassistant.components.nut.const import DOMAIN from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME @@ -10,25 +10,25 @@ from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry, load_fixture -def _get_mock_pynutclient( +def _get_mock_nutclient( list_vars=None, list_ups=None, list_commands_return_value=None, list_commands_side_effect=None, run_command=None, ): - pynutclient = MagicMock() - type(pynutclient).list_ups = MagicMock(return_value=list_ups) - type(pynutclient).list_vars = MagicMock(return_value=list_vars) + nutclient = MagicMock() + type(nutclient).list_ups = AsyncMock(return_value=list_ups) + type(nutclient).list_vars = AsyncMock(return_value=list_vars) if list_commands_return_value is None: list_commands_return_value = {} - type(pynutclient).list_commands = MagicMock( + type(nutclient).list_commands = AsyncMock( return_value=list_commands_return_value, side_effect=list_commands_side_effect ) if run_command is None: - run_command = MagicMock() - type(pynutclient).run_command = run_command - return pynutclient + run_command = AsyncMock() + type(nutclient).run_command = run_command + return nutclient async def async_init_integration( @@ -52,7 +52,7 @@ async def async_init_integration( if list_vars is None: list_vars = json.loads(load_fixture(ups_fixture)) - mock_pynut = _get_mock_pynutclient( + mock_pynut = _get_mock_nutclient( list_ups=list_ups, list_vars=list_vars, list_commands_return_value=list_commands_return_value, @@ -61,7 +61,7 @@ async def async_init_integration( ) with patch( - "homeassistant.components.nut.PyNUTClient", + "homeassistant.components.nut.AIONUTClient", return_value=mock_pynut, ): entry = MockConfigEntry( diff --git a/tests/components/nws/conftest.py b/tests/components/nws/conftest.py index 022211157b6..7ffde0c5731 100644 --- a/tests/components/nws/conftest.py +++ b/tests/components/nws/conftest.py @@ -1,4 +1,5 @@ """Fixtures for National Weather Service tests.""" + from unittest.mock import AsyncMock, patch import pytest diff --git a/tests/components/nws/const.py b/tests/components/nws/const.py index 106b80998ac..e5fc9df909f 100644 --- a/tests/components/nws/const.py +++ b/tests/components/nws/const.py @@ -1,4 +1,5 @@ """Helpers for interacting with pynws.""" + from homeassistant.components.nws.const import CONF_STATION from homeassistant.components.weather import ( ATTR_CONDITION_LIGHTNING_RAINY, diff --git a/tests/components/nws/snapshots/test_weather.ambr b/tests/components/nws/snapshots/test_weather.ambr index 0db2311085c..f4669f47615 100644 --- a/tests/components/nws/snapshots/test_weather.ambr +++ b/tests/components/nws/snapshots/test_weather.ambr @@ -1,213 +1,4 @@ # serializer version: 1 -# name: test_forecast_service - dict({ - 'forecast': list([ - dict({ - 'condition': 'lightning-rainy', - 'datetime': '2019-08-12T20:00:00-04:00', - 'detailed_description': 'A detailed forecast.', - 'dew_point': -15.6, - 'humidity': 75, - 'is_daytime': False, - 'precipitation_probability': 89, - 'temperature': -12.2, - 'wind_bearing': 180, - 'wind_speed': 16.09, - }), - ]), - }) -# --- -# name: test_forecast_service.1 - dict({ - 'forecast': list([ - dict({ - 'condition': 'lightning-rainy', - 'datetime': '2019-08-12T20:00:00-04:00', - 'detailed_description': 'A detailed forecast.', - 'dew_point': -15.6, - 'humidity': 75, - 'precipitation_probability': 89, - 'temperature': -12.2, - 'wind_bearing': 180, - 'wind_speed': 16.09, - }), - ]), - }) -# --- -# name: test_forecast_service.2 - dict({ - 'forecast': list([ - dict({ - 'condition': 'lightning-rainy', - 'datetime': '2019-08-12T20:00:00-04:00', - 'detailed_description': 'A detailed forecast.', - 'dew_point': -15.6, - 'humidity': 75, - 'is_daytime': False, - 'precipitation_probability': 89, - 'temperature': -12.2, - 'wind_bearing': 180, - 'wind_speed': 16.09, - }), - ]), - }) -# --- -# name: test_forecast_service.3 - dict({ - 'forecast': list([ - dict({ - 'condition': 'lightning-rainy', - 'datetime': '2019-08-12T20:00:00-04:00', - 'detailed_description': 'A detailed forecast.', - 'dew_point': -15.6, - 'humidity': 75, - 'precipitation_probability': 89, - 'temperature': -12.2, - 'wind_bearing': 180, - 'wind_speed': 16.09, - }), - ]), - }) -# --- -# name: test_forecast_service.4 - dict({ - 'forecast': list([ - dict({ - 'condition': 'lightning-rainy', - 'datetime': '2019-08-12T20:00:00-04:00', - 'detailed_description': 'A detailed forecast.', - 'dew_point': -15.6, - 'humidity': 75, - 'precipitation_probability': 89, - 'temperature': -12.2, - 'wind_bearing': 180, - 'wind_speed': 16.09, - }), - ]), - }) -# --- -# name: test_forecast_service.5 - dict({ - 'forecast': list([ - dict({ - 'condition': 'lightning-rainy', - 'datetime': '2019-08-12T20:00:00-04:00', - 'detailed_description': 'A detailed forecast.', - 'dew_point': -15.6, - 'humidity': 75, - 'precipitation_probability': 89, - 'temperature': -12.2, - 'wind_bearing': 180, - 'wind_speed': 16.09, - }), - ]), - }) -# --- -# name: test_forecast_service[forecast] - dict({ - 'weather.abc_daynight': dict({ - 'forecast': list([ - dict({ - 'condition': 'lightning-rainy', - 'datetime': '2019-08-12T20:00:00-04:00', - 'detailed_description': 'A detailed forecast.', - 'dew_point': -15.6, - 'humidity': 75, - 'is_daytime': False, - 'precipitation_probability': 89, - 'temperature': -12.2, - 'wind_bearing': 180, - 'wind_speed': 16.09, - }), - ]), - }), - }) -# --- -# name: test_forecast_service[forecast].1 - dict({ - 'weather.abc_daynight': dict({ - 'forecast': list([ - dict({ - 'condition': 'lightning-rainy', - 'datetime': '2019-08-12T20:00:00-04:00', - 'detailed_description': 'A detailed forecast.', - 'dew_point': -15.6, - 'humidity': 75, - 'precipitation_probability': 89, - 'temperature': -12.2, - 'wind_bearing': 180, - 'wind_speed': 16.09, - }), - ]), - }), - }) -# --- -# name: test_forecast_service[forecast].2 - dict({ - 'weather.abc_daynight': dict({ - 'forecast': list([ - dict({ - 'condition': 'lightning-rainy', - 'datetime': '2019-08-12T20:00:00-04:00', - 'detailed_description': 'A detailed forecast.', - 'dew_point': -15.6, - 'humidity': 75, - 'is_daytime': False, - 'precipitation_probability': 89, - 'temperature': -12.2, - 'wind_bearing': 180, - 'wind_speed': 16.09, - }), - ]), - }), - }) -# --- -# name: test_forecast_service[forecast].3 - dict({ - 'weather.abc_daynight': dict({ - 'forecast': list([ - dict({ - 'condition': 'lightning-rainy', - 'datetime': '2019-08-12T20:00:00-04:00', - 'detailed_description': 'A detailed forecast.', - 'dew_point': -15.6, - 'humidity': 75, - 'precipitation_probability': 89, - 'temperature': -12.2, - 'wind_bearing': 180, - 'wind_speed': 16.09, - }), - ]), - }), - }) -# --- -# name: test_forecast_service[forecast].4 - dict({ - 'weather.abc_daynight': dict({ - 'forecast': list([ - dict({ - 'condition': 'lightning-rainy', - 'datetime': '2019-08-12T20:00:00-04:00', - 'detailed_description': 'A detailed forecast.', - 'dew_point': -15.6, - 'humidity': 75, - 'precipitation_probability': 89, - 'temperature': -12.2, - 'wind_bearing': 180, - 'wind_speed': 16.09, - }), - ]), - }), - }) -# --- -# name: test_forecast_service[forecast].5 - dict({ - 'weather.abc_daynight': dict({ - 'forecast': list([ - ]), - }), - }) -# --- # name: test_forecast_service[get_forecast] dict({ 'forecast': list([ @@ -303,7 +94,7 @@ # --- # name: test_forecast_service[get_forecasts] dict({ - 'weather.abc_daynight': dict({ + 'weather.abc': dict({ 'forecast': list([ dict({ 'condition': 'lightning-rainy', @@ -323,7 +114,7 @@ # --- # name: test_forecast_service[get_forecasts].1 dict({ - 'weather.abc_daynight': dict({ + 'weather.abc': dict({ 'forecast': list([ dict({ 'condition': 'lightning-rainy', @@ -342,7 +133,7 @@ # --- # name: test_forecast_service[get_forecasts].2 dict({ - 'weather.abc_daynight': dict({ + 'weather.abc': dict({ 'forecast': list([ dict({ 'condition': 'lightning-rainy', @@ -362,7 +153,7 @@ # --- # name: test_forecast_service[get_forecasts].3 dict({ - 'weather.abc_daynight': dict({ + 'weather.abc': dict({ 'forecast': list([ dict({ 'condition': 'lightning-rainy', @@ -381,7 +172,7 @@ # --- # name: test_forecast_service[get_forecasts].4 dict({ - 'weather.abc_daynight': dict({ + 'weather.abc': dict({ 'forecast': list([ dict({ 'condition': 'lightning-rainy', @@ -400,13 +191,13 @@ # --- # name: test_forecast_service[get_forecasts].5 dict({ - 'weather.abc_daynight': dict({ + 'weather.abc': dict({ 'forecast': list([ ]), }), }) # --- -# name: test_forecast_subscription[hourly-weather.abc_daynight] +# name: test_forecast_subscription[hourly-weather.abc] list([ dict({ 'condition': 'lightning-rainy', @@ -421,7 +212,7 @@ }), ]) # --- -# name: test_forecast_subscription[hourly-weather.abc_daynight].1 +# name: test_forecast_subscription[hourly-weather.abc].1 list([ dict({ 'condition': 'lightning-rainy', @@ -436,97 +227,3 @@ }), ]) # --- -# name: test_forecast_subscription[hourly] - list([ - dict({ - 'condition': 'lightning-rainy', - 'datetime': '2019-08-12T20:00:00-04:00', - 'detailed_description': 'A detailed forecast.', - 'dew_point': -15.6, - 'humidity': 75, - 'precipitation_probability': 89, - 'temperature': -12.2, - 'wind_bearing': 180, - 'wind_speed': 16.09, - }), - ]) -# --- -# name: test_forecast_subscription[hourly].1 - list([ - dict({ - 'condition': 'lightning-rainy', - 'datetime': '2019-08-12T20:00:00-04:00', - 'detailed_description': 'A detailed forecast.', - 'dew_point': -15.6, - 'humidity': 75, - 'precipitation_probability': 89, - 'temperature': -12.2, - 'wind_bearing': 180, - 'wind_speed': 16.09, - }), - ]) -# --- -# name: test_forecast_subscription[twice_daily-weather.abc_hourly] - list([ - dict({ - 'condition': 'lightning-rainy', - 'datetime': '2019-08-12T20:00:00-04:00', - 'detailed_description': 'A detailed forecast.', - 'dew_point': -15.6, - 'humidity': 75, - 'is_daytime': False, - 'precipitation_probability': 89, - 'temperature': -12.2, - 'wind_bearing': 180, - 'wind_speed': 16.09, - }), - ]) -# --- -# name: test_forecast_subscription[twice_daily-weather.abc_hourly].1 - list([ - dict({ - 'condition': 'lightning-rainy', - 'datetime': '2019-08-12T20:00:00-04:00', - 'detailed_description': 'A detailed forecast.', - 'dew_point': -15.6, - 'humidity': 75, - 'is_daytime': False, - 'precipitation_probability': 89, - 'temperature': -12.2, - 'wind_bearing': 180, - 'wind_speed': 16.09, - }), - ]) -# --- -# name: test_forecast_subscription[twice_daily] - list([ - dict({ - 'condition': 'lightning-rainy', - 'datetime': '2019-08-12T20:00:00-04:00', - 'detailed_description': 'A detailed forecast.', - 'dew_point': -15.6, - 'humidity': 75, - 'is_daytime': False, - 'precipitation_probability': 89, - 'temperature': -12.2, - 'wind_bearing': 180, - 'wind_speed': 16.09, - }), - ]) -# --- -# name: test_forecast_subscription[twice_daily].1 - list([ - dict({ - 'condition': 'lightning-rainy', - 'datetime': '2019-08-12T20:00:00-04:00', - 'detailed_description': 'A detailed forecast.', - 'dew_point': -15.6, - 'humidity': 75, - 'is_daytime': False, - 'precipitation_probability': 89, - 'temperature': -12.2, - 'wind_bearing': 180, - 'wind_speed': 16.09, - }), - ]) -# --- diff --git a/tests/components/nws/test_config_flow.py b/tests/components/nws/test_config_flow.py index 9c02139d67c..fe8017c55e1 100644 --- a/tests/components/nws/test_config_flow.py +++ b/tests/components/nws/test_config_flow.py @@ -1,4 +1,5 @@ """Test the National Weather Service (NWS) config flow.""" + from unittest.mock import patch import aiohttp diff --git a/tests/components/nws/test_init.py b/tests/components/nws/test_init.py index cc4ce114cad..121da07a9ce 100644 --- a/tests/components/nws/test_init.py +++ b/tests/components/nws/test_init.py @@ -1,4 +1,5 @@ """Tests for init module.""" + from homeassistant.components.nws.const import DOMAIN from homeassistant.components.weather import DOMAIN as WEATHER_DOMAIN from homeassistant.const import STATE_UNAVAILABLE diff --git a/tests/components/nws/test_sensor.py b/tests/components/nws/test_sensor.py index 5e36c9c0717..4d29e48ae0b 100644 --- a/tests/components/nws/test_sensor.py +++ b/tests/components/nws/test_sensor.py @@ -1,4 +1,5 @@ """Sensors for National Weather Service (NWS).""" + import pytest from homeassistant.components.nws.const import ATTRIBUTION, DOMAIN diff --git a/tests/components/nws/test_weather.py b/tests/components/nws/test_weather.py index c7478be7c07..0fb5654d7ee 100644 --- a/tests/components/nws/test_weather.py +++ b/tests/components/nws/test_weather.py @@ -1,4 +1,5 @@ """Tests for the NWS weather component.""" + from datetime import timedelta from unittest.mock import patch @@ -11,7 +12,6 @@ from homeassistant.components import nws from homeassistant.components.weather import ( ATTR_CONDITION_CLEAR_NIGHT, ATTR_CONDITION_SUNNY, - ATTR_FORECAST, DOMAIN as WEATHER_DOMAIN, LEGACY_SERVICE_GET_FORECAST, SERVICE_GET_FORECASTS, @@ -58,16 +58,6 @@ async def test_imperial_metric( no_sensor, ) -> None: """Test with imperial and metric units.""" - # enable the hourly entity - registry = er.async_get(hass) - registry.async_get_or_create( - WEATHER_DOMAIN, - nws.DOMAIN, - "35_-75_hourly", - suggested_object_id="abc_hourly", - disabled_by=None, - ) - hass.config.units = units entry = MockConfigEntry( domain=nws.DOMAIN, @@ -77,7 +67,7 @@ async def test_imperial_metric( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - state = hass.states.get("weather.abc_hourly") + state = hass.states.get("weather.abc") assert state assert state.state == ATTR_CONDITION_SUNNY @@ -86,23 +76,6 @@ async def test_imperial_metric( for key, value in result_observation.items(): assert data.get(key) == value - forecast = data.get(ATTR_FORECAST) - for key, value in result_forecast.items(): - assert forecast[0].get(key) == value - - state = hass.states.get("weather.abc_daynight") - - assert state - assert state.state == ATTR_CONDITION_SUNNY - - data = state.attributes - for key, value in result_observation.items(): - assert data.get(key) == value - - forecast = data.get(ATTR_FORECAST) - for key, value in result_forecast.items(): - assert forecast[0].get(key) == value - async def test_night_clear(hass: HomeAssistant, mock_simple_nws, no_sensor) -> None: """Test with clear-night in observation.""" @@ -117,7 +90,7 @@ async def test_night_clear(hass: HomeAssistant, mock_simple_nws, no_sensor) -> N await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - state = hass.states.get("weather.abc_daynight") + state = hass.states.get("weather.abc") assert state.state == ATTR_CONDITION_CLEAR_NIGHT @@ -135,16 +108,12 @@ async def test_none_values(hass: HomeAssistant, mock_simple_nws, no_sensor) -> N await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - state = hass.states.get("weather.abc_daynight") + state = hass.states.get("weather.abc") assert state.state == STATE_UNKNOWN data = state.attributes for key in WEATHER_EXPECTED_OBSERVATION_IMPERIAL: assert data.get(key) is None - forecast = data.get(ATTR_FORECAST) - for key in EXPECTED_FORECAST_IMPERIAL: - assert forecast[0].get(key) is None - async def test_none(hass: HomeAssistant, mock_simple_nws, no_sensor) -> None: """Test with None as observation and forecast.""" @@ -160,7 +129,7 @@ async def test_none(hass: HomeAssistant, mock_simple_nws, no_sensor) -> None: await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - state = hass.states.get("weather.abc_daynight") + state = hass.states.get("weather.abc") assert state assert state.state == STATE_UNKNOWN @@ -168,9 +137,6 @@ async def test_none(hass: HomeAssistant, mock_simple_nws, no_sensor) -> None: for key in WEATHER_EXPECTED_OBSERVATION_IMPERIAL: assert data.get(key) is None - forecast = data.get(ATTR_FORECAST) - assert forecast is None - async def test_error_station(hass: HomeAssistant, mock_simple_nws, no_sensor) -> None: """Test error in setting station.""" @@ -186,8 +152,7 @@ async def test_error_station(hass: HomeAssistant, mock_simple_nws, no_sensor) -> await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert hass.states.get("weather.abc_hourly") is None - assert hass.states.get("weather.abc_daynight") is None + assert hass.states.get("weather.abc") is None async def test_entity_refresh(hass: HomeAssistant, mock_simple_nws, no_sensor) -> None: @@ -210,7 +175,7 @@ async def test_entity_refresh(hass: HomeAssistant, mock_simple_nws, no_sensor) - await hass.services.async_call( "homeassistant", "update_entity", - {"entity_id": "weather.abc_daynight"}, + {"entity_id": "weather.abc"}, blocking=True, ) await hass.async_block_till_done() @@ -224,9 +189,10 @@ async def test_error_observation( ) -> None: """Test error during update observation.""" utc_time = dt_util.utcnow() - with patch("homeassistant.components.nws.utcnow") as mock_utc, patch( - "homeassistant.components.nws.weather.utcnow" - ) as mock_utc_weather: + with ( + patch("homeassistant.components.nws.utcnow") as mock_utc, + patch("homeassistant.components.nws.weather.utcnow") as mock_utc_weather, + ): def increment_time(time): mock_utc.return_value += time @@ -249,7 +215,7 @@ async def test_error_observation( instance.update_observation.assert_called_once() - state = hass.states.get("weather.abc_daynight") + state = hass.states.get("weather.abc") assert state assert state.state == STATE_UNAVAILABLE @@ -260,7 +226,7 @@ async def test_error_observation( assert instance.update_observation.call_count == 2 - state = hass.states.get("weather.abc_daynight") + state = hass.states.get("weather.abc") assert state assert state.state == ATTR_CONDITION_SUNNY @@ -272,7 +238,7 @@ async def test_error_observation( assert instance.update_observation.call_count == 3 - state = hass.states.get("weather.abc_daynight") + state = hass.states.get("weather.abc") assert state assert state.state == ATTR_CONDITION_SUNNY @@ -280,7 +246,7 @@ async def test_error_observation( increment_time(timedelta(minutes=10)) await hass.async_block_till_done() - state = hass.states.get("weather.abc_daynight") + state = hass.states.get("weather.abc") assert state assert state.state == STATE_UNAVAILABLE @@ -300,7 +266,7 @@ async def test_error_forecast(hass: HomeAssistant, mock_simple_nws, no_sensor) - instance.update_forecast.assert_called_once() - state = hass.states.get("weather.abc_daynight") + state = hass.states.get("weather.abc") assert state assert state.state == STATE_UNAVAILABLE @@ -311,50 +277,7 @@ async def test_error_forecast(hass: HomeAssistant, mock_simple_nws, no_sensor) - assert instance.update_forecast.call_count == 2 - state = hass.states.get("weather.abc_daynight") - assert state - assert state.state == ATTR_CONDITION_SUNNY - - -async def test_error_forecast_hourly( - hass: HomeAssistant, mock_simple_nws, no_sensor -) -> None: - """Test error during update forecast hourly.""" - instance = mock_simple_nws.return_value - instance.update_forecast_hourly.side_effect = aiohttp.ClientError - - # enable the hourly entity - registry = er.async_get(hass) - registry.async_get_or_create( - WEATHER_DOMAIN, - nws.DOMAIN, - "35_-75_hourly", - suggested_object_id="abc_hourly", - disabled_by=None, - ) - - entry = MockConfigEntry( - domain=nws.DOMAIN, - data=NWS_CONFIG, - ) - entry.add_to_hass(hass) - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - state = hass.states.get("weather.abc_hourly") - assert state - assert state.state == STATE_UNAVAILABLE - - instance.update_forecast_hourly.assert_called_once() - - instance.update_forecast_hourly.side_effect = None - - async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=1)) - await hass.async_block_till_done() - - assert instance.update_forecast_hourly.call_count == 2 - - state = hass.states.get("weather.abc_hourly") + state = hass.states.get("weather.abc") assert state assert state.state == ATTR_CONDITION_SUNNY @@ -377,30 +300,6 @@ async def test_new_config_entry(hass: HomeAssistant, no_sensor) -> None: assert len(er.async_entries_for_config_entry(registry, entry.entry_id)) == 1 -async def test_legacy_config_entry(hass: HomeAssistant, no_sensor) -> None: - """Test the expected entities are created.""" - registry = er.async_get(hass) - # Pre-create the hourly entity - registry.async_get_or_create( - WEATHER_DOMAIN, - nws.DOMAIN, - "35_-75_hourly", - ) - - entry = MockConfigEntry( - domain=nws.DOMAIN, - data=NWS_CONFIG, - ) - entry.add_to_hass(hass) - - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - assert len(hass.states.async_entity_ids("weather")) == 2 - entry = hass.config_entries.async_entries()[0] - assert len(er.async_entries_for_config_entry(registry, entry.entry_id)) == 2 - - @pytest.mark.parametrize( ("service"), [ @@ -436,7 +335,7 @@ async def test_forecast_service( WEATHER_DOMAIN, service, { - "entity_id": "weather.abc_daynight", + "entity_id": "weather.abc", "type": forecast_type, }, blocking=True, @@ -463,7 +362,7 @@ async def test_forecast_service( WEATHER_DOMAIN, service, { - "entity_id": "weather.abc_daynight", + "entity_id": "weather.abc", "type": forecast_type, }, blocking=True, @@ -486,7 +385,7 @@ async def test_forecast_service( WEATHER_DOMAIN, service, { - "entity_id": "weather.abc_daynight", + "entity_id": "weather.abc", "type": "hourly", }, blocking=True, @@ -503,7 +402,7 @@ async def test_forecast_service( WEATHER_DOMAIN, service, { - "entity_id": "weather.abc_daynight", + "entity_id": "weather.abc", "type": "hourly", }, blocking=True, @@ -514,7 +413,7 @@ async def test_forecast_service( @pytest.mark.parametrize( ("forecast_type", "entity_id"), - [("hourly", "weather.abc_daynight"), ("twice_daily", "weather.abc_hourly")], + [("hourly", "weather.abc")], ) async def test_forecast_subscription( hass: HomeAssistant, diff --git a/tests/components/nx584/test_binary_sensor.py b/tests/components/nx584/test_binary_sensor.py index 044fbc6ae9e..5c57feb471b 100644 --- a/tests/components/nx584/test_binary_sensor.py +++ b/tests/components/nx584/test_binary_sensor.py @@ -1,4 +1,5 @@ """The tests for the nx584 sensor platform.""" + from unittest import mock from nx584 import client as nx584_client @@ -248,8 +249,8 @@ def test_nx584_watcher_run_retries_failures(mock_sleep) -> None: """Fake runner.""" if empty_me: empty_me.pop() - raise requests.exceptions.ConnectionError() - raise StopMe() + raise requests.exceptions.ConnectionError + raise StopMe watcher = nx584.NX584Watcher(None, {}) with mock.patch.object(watcher, "_run") as mock_inner: diff --git a/tests/components/nzbget/__init__.py b/tests/components/nzbget/__init__.py index 4446ac0cd55..d3216b62ef3 100644 --- a/tests/components/nzbget/__init__.py +++ b/tests/components/nzbget/__init__.py @@ -1,4 +1,5 @@ """Tests for the NZBGet integration.""" + from unittest.mock import patch from homeassistant.components.nzbget.const import DOMAIN diff --git a/tests/components/nzbget/conftest.py b/tests/components/nzbget/conftest.py index d62e5a2e8b3..8f48a4306c7 100644 --- a/tests/components/nzbget/conftest.py +++ b/tests/components/nzbget/conftest.py @@ -1,4 +1,5 @@ """Define fixtures available for all tests.""" + from unittest.mock import MagicMock, patch import pytest diff --git a/tests/components/nzbget/test_config_flow.py b/tests/components/nzbget/test_config_flow.py index e26be8b9880..c299d1d6dd5 100644 --- a/tests/components/nzbget/test_config_flow.py +++ b/tests/components/nzbget/test_config_flow.py @@ -1,4 +1,5 @@ """Test the NZBGet config flow.""" + from unittest.mock import patch from pynzbgetapi import NZBGetAPIException @@ -30,7 +31,12 @@ async def test_user_form(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.FORM assert result["errors"] == {} - with _patch_version(), _patch_status(), _patch_history(), _patch_async_setup_entry() as mock_setup_entry: + with ( + _patch_version(), + _patch_status(), + _patch_history(), + _patch_async_setup_entry() as mock_setup_entry, + ): result = await hass.config_entries.flow.async_configure( result["flow_id"], USER_INPUT, @@ -58,7 +64,12 @@ async def test_user_form_show_advanced_options(hass: HomeAssistant) -> None: CONF_VERIFY_SSL: True, } - with _patch_version(), _patch_status(), _patch_history(), _patch_async_setup_entry() as mock_setup_entry: + with ( + _patch_version(), + _patch_status(), + _patch_history(), + _patch_async_setup_entry() as mock_setup_entry, + ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input_advanced, diff --git a/tests/components/nzbget/test_init.py b/tests/components/nzbget/test_init.py index 2e08ccb5630..a119bb953ce 100644 --- a/tests/components/nzbget/test_init.py +++ b/tests/components/nzbget/test_init.py @@ -1,4 +1,5 @@ """Test the NZBGet config flow.""" + from unittest.mock import patch from pynzbgetapi import NZBGetAPIException @@ -31,9 +32,12 @@ async def test_async_setup_raises_entry_not_ready(hass: HomeAssistant) -> None: config_entry = MockConfigEntry(domain=DOMAIN, data=ENTRY_CONFIG) config_entry.add_to_hass(hass) - with _patch_version(), patch( - "homeassistant.components.nzbget.coordinator.NZBGetAPI.status", - side_effect=NZBGetAPIException(), + with ( + _patch_version(), + patch( + "homeassistant.components.nzbget.coordinator.NZBGetAPI.status", + side_effect=NZBGetAPIException(), + ), ): await hass.config_entries.async_setup(config_entry.entry_id) diff --git a/tests/components/nzbget/test_sensor.py b/tests/components/nzbget/test_sensor.py index e9365e36b24..350401ed9a2 100644 --- a/tests/components/nzbget/test_sensor.py +++ b/tests/components/nzbget/test_sensor.py @@ -1,4 +1,5 @@ """Test the NZBGet sensors.""" + from datetime import timedelta from unittest.mock import patch diff --git a/tests/components/nzbget/test_switch.py b/tests/components/nzbget/test_switch.py index 059763c2da3..61343710254 100644 --- a/tests/components/nzbget/test_switch.py +++ b/tests/components/nzbget/test_switch.py @@ -1,4 +1,5 @@ """Test the NZBGet switches.""" + from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.const import ( ATTR_ENTITY_ID, diff --git a/tests/components/octoprint/__init__.py b/tests/components/octoprint/__init__.py index 5176d2209b1..4a896736329 100644 --- a/tests/components/octoprint/__init__.py +++ b/tests/components/octoprint/__init__.py @@ -1,4 +1,5 @@ """Tests for the OctoPrint integration.""" + from __future__ import annotations from typing import Any @@ -45,20 +46,25 @@ async def init_integration( printer_info = OctoprintPrinterInfo(printer) if job is None: job = DEFAULT_JOB - with patch("homeassistant.components.octoprint.PLATFORMS", [platform]), patch( - "pyoctoprintapi.OctoprintClient.get_server_info", return_value={} - ), patch( - "pyoctoprintapi.OctoprintClient.get_printer_info", - return_value=printer_info, - ), patch( - "pyoctoprintapi.OctoprintClient.get_job_info", - return_value=OctoprintJobInfo(job), - ), patch( - "pyoctoprintapi.OctoprintClient.get_tracking_info", - return_value=TrackingSetting({"unique_id": "uuid"}), - ), patch( - "pyoctoprintapi.OctoprintClient.get_discovery_info", - return_value=DiscoverySettings({"upnpUuid": "uuid"}), + with ( + patch("homeassistant.components.octoprint.PLATFORMS", [platform]), + patch("pyoctoprintapi.OctoprintClient.get_server_info", return_value={}), + patch( + "pyoctoprintapi.OctoprintClient.get_printer_info", + return_value=printer_info, + ), + patch( + "pyoctoprintapi.OctoprintClient.get_job_info", + return_value=OctoprintJobInfo(job), + ), + patch( + "pyoctoprintapi.OctoprintClient.get_tracking_info", + return_value=TrackingSetting({"unique_id": "uuid"}), + ), + patch( + "pyoctoprintapi.OctoprintClient.get_discovery_info", + return_value=DiscoverySettings({"upnpUuid": "uuid"}), + ), ): config_entry = MockConfigEntry( domain=DOMAIN, diff --git a/tests/components/octoprint/test_binary_sensor.py b/tests/components/octoprint/test_binary_sensor.py index fa67c052dc9..50572682e7d 100644 --- a/tests/components/octoprint/test_binary_sensor.py +++ b/tests/components/octoprint/test_binary_sensor.py @@ -1,4 +1,5 @@ """The tests for Octoptint binary sensor module.""" + from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er diff --git a/tests/components/octoprint/test_button.py b/tests/components/octoprint/test_button.py index a92afdff3f0..7f272f9927e 100644 --- a/tests/components/octoprint/test_button.py +++ b/tests/components/octoprint/test_button.py @@ -1,6 +1,8 @@ """Test the OctoPrint buttons.""" + from unittest.mock import patch +from freezegun import freeze_time from pyoctoprintapi import OctoprintPrinterInfo import pytest @@ -55,9 +57,10 @@ async def test_pause_job(hass: HomeAssistant) -> None: assert len(pause_command.mock_calls) == 0 # Test pausing the printer when it is stopped - with patch( - "pyoctoprintapi.OctoprintClient.pause_job" - ) as pause_command, pytest.raises(InvalidPrinterState): + with ( + patch("pyoctoprintapi.OctoprintClient.pause_job") as pause_command, + pytest.raises(InvalidPrinterState), + ): coordinator.data["printer"] = OctoprintPrinterInfo( { "state": {"flags": {"printing": False, "paused": False}}, @@ -115,9 +118,10 @@ async def test_resume_job(hass: HomeAssistant) -> None: assert len(resume_command.mock_calls) == 0 # Test resuming the printer when it is stopped - with patch( - "pyoctoprintapi.OctoprintClient.resume_job" - ) as resume_command, pytest.raises(InvalidPrinterState): + with ( + patch("pyoctoprintapi.OctoprintClient.resume_job") as resume_command, + pytest.raises(InvalidPrinterState), + ): coordinator.data["printer"] = OctoprintPrinterInfo( { "state": {"flags": {"printing": False, "paused": False}}, @@ -192,3 +196,82 @@ async def test_stop_job(hass: HomeAssistant) -> None: ) assert len(stop_command.mock_calls) == 0 + + +@freeze_time("2023-01-01 00:00") +async def test_shutdown_system(hass: HomeAssistant) -> None: + """Test the shutdown system button.""" + await init_integration(hass, BUTTON_DOMAIN) + + entity_id = "button.octoprint_shutdown_system" + + # Test shutting down the system + with patch( + "homeassistant.components.octoprint.coordinator.OctoprintClient.shutdown" + ) as shutdown_command: + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + + assert len(shutdown_command.mock_calls) == 1 + + state = hass.states.get(entity_id) + assert state + assert state.state == "2023-01-01T00:00:00+00:00" + + +@freeze_time("2023-01-01 00:00") +async def test_reboot_system(hass: HomeAssistant) -> None: + """Test the reboot system button.""" + await init_integration(hass, BUTTON_DOMAIN) + + entity_id = "button.octoprint_reboot_system" + + # Test rebooting the system + with patch( + "homeassistant.components.octoprint.coordinator.OctoprintClient.reboot_system" + ) as reboot_command: + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + { + ATTR_ENTITY_ID: entity_id, + }, + blocking=True, + ) + + assert len(reboot_command.mock_calls) == 1 + + state = hass.states.get(entity_id) + assert state + assert state.state == "2023-01-01T00:00:00+00:00" + + +@freeze_time("2023-01-01 00:00") +async def test_restart_octoprint(hass: HomeAssistant) -> None: + """Test the restart octoprint button.""" + await init_integration(hass, BUTTON_DOMAIN) + + entity_id = "button.octoprint_restart_octoprint" + + # Test restarting octoprint + with patch( + "homeassistant.components.octoprint.coordinator.OctoprintClient.restart" + ) as restart_command: + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + { + ATTR_ENTITY_ID: entity_id, + }, + blocking=True, + ) + + assert len(restart_command.mock_calls) == 1 + + state = hass.states.get(entity_id) + assert state + assert state.state == "2023-01-01T00:00:00+00:00" diff --git a/tests/components/octoprint/test_camera.py b/tests/components/octoprint/test_camera.py index 49daf9a8227..b1d843f7d39 100644 --- a/tests/components/octoprint/test_camera.py +++ b/tests/components/octoprint/test_camera.py @@ -1,4 +1,5 @@ """The tests for Octoptint camera module.""" + from unittest.mock import patch from pyoctoprintapi import WebcamSettings diff --git a/tests/components/octoprint/test_config_flow.py b/tests/components/octoprint/test_config_flow.py index 8e20983a791..4c8e22e524c 100644 --- a/tests/components/octoprint/test_config_flow.py +++ b/tests/components/octoprint/test_config_flow.py @@ -1,4 +1,5 @@ """Test the OctoPrint config flow.""" + from ipaddress import ip_address from unittest.mock import patch @@ -37,18 +38,23 @@ async def test_form(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert result["type"] == "progress" - with patch( - "pyoctoprintapi.OctoprintClient.get_server_info", - return_value=True, - ), patch( - "pyoctoprintapi.OctoprintClient.get_discovery_info", - return_value=DiscoverySettings({"upnpUuid": "uuid"}), - ), patch( - "homeassistant.components.octoprint.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.octoprint.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "pyoctoprintapi.OctoprintClient.get_server_info", + return_value=True, + ), + patch( + "pyoctoprintapi.OctoprintClient.get_discovery_info", + return_value=DiscoverySettings({"upnpUuid": "uuid"}), + ), + patch( + "homeassistant.components.octoprint.async_setup", return_value=True + ) as mock_setup, + patch( + "homeassistant.components.octoprint.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], ) @@ -207,17 +213,20 @@ async def test_show_zerconf_form(hass: HomeAssistant) -> None: assert result["type"] == "progress" - with patch( - "pyoctoprintapi.OctoprintClient.get_server_info", - return_value=True, - ), patch( - "pyoctoprintapi.OctoprintClient.get_discovery_info", - return_value=DiscoverySettings({"upnpUuid": "uuid"}), - ), patch( - "homeassistant.components.octoprint.async_setup", return_value=True - ), patch( - "homeassistant.components.octoprint.async_setup_entry", - return_value=True, + with ( + patch( + "pyoctoprintapi.OctoprintClient.get_server_info", + return_value=True, + ), + patch( + "pyoctoprintapi.OctoprintClient.get_discovery_info", + return_value=DiscoverySettings({"upnpUuid": "uuid"}), + ), + patch("homeassistant.components.octoprint.async_setup", return_value=True), + patch( + "homeassistant.components.octoprint.async_setup_entry", + return_value=True, + ), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -273,17 +282,20 @@ async def test_show_ssdp_form(hass: HomeAssistant) -> None: assert result["type"] == "progress" - with patch( - "pyoctoprintapi.OctoprintClient.get_server_info", - return_value=True, - ), patch( - "pyoctoprintapi.OctoprintClient.get_discovery_info", - return_value=DiscoverySettings({"upnpUuid": "uuid"}), - ), patch( - "homeassistant.components.octoprint.async_setup", return_value=True - ), patch( - "homeassistant.components.octoprint.async_setup_entry", - return_value=True, + with ( + patch( + "pyoctoprintapi.OctoprintClient.get_server_info", + return_value=True, + ), + patch( + "pyoctoprintapi.OctoprintClient.get_discovery_info", + return_value=DiscoverySettings({"upnpUuid": "uuid"}), + ), + patch("homeassistant.components.octoprint.async_setup", return_value=True), + patch( + "homeassistant.components.octoprint.async_setup_entry", + return_value=True, + ), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -304,19 +316,23 @@ async def test_show_ssdp_form(hass: HomeAssistant) -> None: async def test_import_yaml(hass: HomeAssistant) -> None: """Test that the yaml import works.""" - with patch( - "pyoctoprintapi.OctoprintClient.get_server_info", - return_value=True, - ), patch( - "pyoctoprintapi.OctoprintClient.get_discovery_info", - return_value=DiscoverySettings({"upnpUuid": "uuid"}), - ), patch( - "pyoctoprintapi.OctoprintClient.request_app_key", return_value="test-key" - ), patch( - "homeassistant.components.octoprint.async_setup", return_value=True - ), patch( - "homeassistant.components.octoprint.async_setup_entry", - return_value=True, + with ( + patch( + "pyoctoprintapi.OctoprintClient.get_server_info", + return_value=True, + ), + patch( + "pyoctoprintapi.OctoprintClient.get_discovery_info", + return_value=DiscoverySettings({"upnpUuid": "uuid"}), + ), + patch( + "pyoctoprintapi.OctoprintClient.request_app_key", return_value="test-key" + ), + patch("homeassistant.components.octoprint.async_setup", return_value=True), + patch( + "homeassistant.components.octoprint.async_setup_entry", + return_value=True, + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -344,12 +360,15 @@ async def test_import_duplicate_yaml(hass: HomeAssistant) -> None: unique_id="uuid", ).add_to_hass(hass) - with patch( - "pyoctoprintapi.OctoprintClient.get_discovery_info", - return_value=DiscoverySettings({"upnpUuid": "uuid"}), - ), patch( - "pyoctoprintapi.OctoprintClient.request_app_key", return_value="test-key" - ) as request_app_key: + with ( + patch( + "pyoctoprintapi.OctoprintClient.get_discovery_info", + return_value=DiscoverySettings({"upnpUuid": "uuid"}), + ), + patch( + "pyoctoprintapi.OctoprintClient.request_app_key", return_value="test-key" + ) as request_app_key, + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, @@ -465,18 +484,23 @@ async def test_user_duplicate_entry(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert result["type"] == "progress" - with patch( - "pyoctoprintapi.OctoprintClient.get_server_info", - return_value=True, - ), patch( - "pyoctoprintapi.OctoprintClient.get_discovery_info", - return_value=DiscoverySettings({"upnpUuid": "uuid"}), - ), patch( - "homeassistant.components.octoprint.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.octoprint.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "pyoctoprintapi.OctoprintClient.get_server_info", + return_value=True, + ), + patch( + "pyoctoprintapi.OctoprintClient.get_discovery_info", + return_value=DiscoverySettings({"upnpUuid": "uuid"}), + ), + patch( + "homeassistant.components.octoprint.async_setup", return_value=True + ) as mock_setup, + patch( + "homeassistant.components.octoprint.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], ) diff --git a/tests/components/octoprint/test_sensor.py b/tests/components/octoprint/test_sensor.py index 2a0e14e5ce0..8c1c0a7712e 100644 --- a/tests/components/octoprint/test_sensor.py +++ b/tests/components/octoprint/test_sensor.py @@ -1,4 +1,5 @@ """The tests for Octoptint binary sensor module.""" + from datetime import UTC, datetime from freezegun.api import FrozenDateTimeFactory diff --git a/tests/components/octoprint/test_servics.py b/tests/components/octoprint/test_servics.py index 70e983c4bb4..2b5a89970e8 100644 --- a/tests/components/octoprint/test_servics.py +++ b/tests/components/octoprint/test_servics.py @@ -1,4 +1,5 @@ """Test the OctoPrint services.""" + from unittest.mock import patch from homeassistant.components.octoprint.const import ( diff --git a/tests/components/ollama/__init__.py b/tests/components/ollama/__init__.py new file mode 100644 index 00000000000..22a576e94a4 --- /dev/null +++ b/tests/components/ollama/__init__.py @@ -0,0 +1,14 @@ +"""Tests for the Ollama integration.""" + +from homeassistant.components import ollama +from homeassistant.components.ollama.const import DEFAULT_PROMPT + +TEST_USER_DATA = { + ollama.CONF_URL: "http://localhost:11434", + ollama.CONF_MODEL: "test model", +} + +TEST_OPTIONS = { + ollama.CONF_PROMPT: DEFAULT_PROMPT, + ollama.CONF_MAX_HISTORY: 2, +} diff --git a/tests/components/ollama/conftest.py b/tests/components/ollama/conftest.py new file mode 100644 index 00000000000..78ecf0766d7 --- /dev/null +++ b/tests/components/ollama/conftest.py @@ -0,0 +1,37 @@ +"""Tests Ollama integration.""" + +from unittest.mock import patch + +import pytest + +from homeassistant.components import ollama +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from . import TEST_OPTIONS, TEST_USER_DATA + +from tests.common import MockConfigEntry + + +@pytest.fixture +def mock_config_entry(hass: HomeAssistant) -> MockConfigEntry: + """Mock a config entry.""" + entry = MockConfigEntry( + domain=ollama.DOMAIN, + data=TEST_USER_DATA, + options=TEST_OPTIONS, + ) + entry.add_to_hass(hass) + return entry + + +@pytest.fixture +async def mock_init_component(hass: HomeAssistant, mock_config_entry: MockConfigEntry): + """Initialize integration.""" + assert await async_setup_component(hass, "homeassistant", {}) + + with patch( + "ollama.AsyncClient.list", + ): + assert await async_setup_component(hass, ollama.DOMAIN, {}) + await hass.async_block_till_done() diff --git a/tests/components/ollama/test_config_flow.py b/tests/components/ollama/test_config_flow.py new file mode 100644 index 00000000000..825f3eac436 --- /dev/null +++ b/tests/components/ollama/test_config_flow.py @@ -0,0 +1,234 @@ +"""Test the Ollama config flow.""" + +import asyncio +from unittest.mock import patch + +from httpx import ConnectError +import pytest + +from homeassistant import config_entries +from homeassistant.components import ollama +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from tests.common import MockConfigEntry + +TEST_MODEL = "test_model:latest" + + +async def test_form(hass: HomeAssistant) -> None: + """Test flow when the model is already downloaded.""" + # Pretend we already set up a config entry. + hass.config.components.add(ollama.DOMAIN) + MockConfigEntry( + domain=ollama.DOMAIN, + state=config_entries.ConfigEntryState.LOADED, + ).add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + ollama.DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["errors"] is None + + with ( + patch( + "homeassistant.components.ollama.config_flow.ollama.AsyncClient.list", + # test model is already "downloaded" + return_value={"models": [{"model": TEST_MODEL}]}, + ), + patch( + "homeassistant.components.ollama.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): + # Step 1: URL + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {ollama.CONF_URL: "http://localhost:11434"} + ) + await hass.async_block_till_done() + + # Step 2: model + assert result2["type"] == FlowResultType.FORM + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], {ollama.CONF_MODEL: TEST_MODEL} + ) + await hass.async_block_till_done() + + assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["data"] == { + ollama.CONF_URL: "http://localhost:11434", + ollama.CONF_MODEL: TEST_MODEL, + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_need_download(hass: HomeAssistant) -> None: + """Test flow when a model needs to be downloaded.""" + # Pretend we already set up a config entry. + hass.config.components.add(ollama.DOMAIN) + MockConfigEntry( + domain=ollama.DOMAIN, + state=config_entries.ConfigEntryState.LOADED, + ).add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + ollama.DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["errors"] is None + + pull_ready = asyncio.Event() + pull_called = asyncio.Event() + pull_model: str | None = None + + async def pull(self, model: str, *args, **kwargs) -> None: + nonlocal pull_model + + async with asyncio.timeout(1): + await pull_ready.wait() + + pull_model = model + pull_called.set() + + with ( + patch( + "homeassistant.components.ollama.config_flow.ollama.AsyncClient.list", + # No models are downloaded + return_value={}, + ), + patch( + "homeassistant.components.ollama.config_flow.ollama.AsyncClient.pull", + pull, + ), + patch( + "homeassistant.components.ollama.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): + # Step 1: URL + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {ollama.CONF_URL: "http://localhost:11434"} + ) + await hass.async_block_till_done() + + # Step 2: model + assert result2["type"] == FlowResultType.FORM + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], {ollama.CONF_MODEL: TEST_MODEL} + ) + await hass.async_block_till_done() + + # Step 3: download + assert result3["type"] == FlowResultType.SHOW_PROGRESS + result4 = await hass.config_entries.flow.async_configure( + result3["flow_id"], + ) + await hass.async_block_till_done() + + # Run again without the task finishing. + # We should still be downloading. + assert result4["type"] == FlowResultType.SHOW_PROGRESS + result4 = await hass.config_entries.flow.async_configure( + result4["flow_id"], + ) + await hass.async_block_till_done() + assert result4["type"] == FlowResultType.SHOW_PROGRESS + + # Signal fake pull method to complete + pull_ready.set() + async with asyncio.timeout(1): + await pull_called.wait() + + assert pull_model == TEST_MODEL + + # Step 4: finish + result5 = await hass.config_entries.flow.async_configure( + result4["flow_id"], + ) + + assert result5["type"] == FlowResultType.CREATE_ENTRY + assert result5["data"] == { + ollama.CONF_URL: "http://localhost:11434", + ollama.CONF_MODEL: TEST_MODEL, + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_options( + hass: HomeAssistant, mock_config_entry, mock_init_component +) -> None: + """Test the options form.""" + options_flow = await hass.config_entries.options.async_init( + mock_config_entry.entry_id + ) + options = await hass.config_entries.options.async_configure( + options_flow["flow_id"], + {ollama.CONF_PROMPT: "test prompt", ollama.CONF_MAX_HISTORY: 100}, + ) + await hass.async_block_till_done() + assert options["type"] == FlowResultType.CREATE_ENTRY + assert options["data"] == { + ollama.CONF_PROMPT: "test prompt", + ollama.CONF_MAX_HISTORY: 100, + } + + +@pytest.mark.parametrize( + ("side_effect", "error"), + [ + (ConnectError(message=""), "cannot_connect"), + (RuntimeError(), "unknown"), + ], +) +async def test_form_errors(hass: HomeAssistant, side_effect, error) -> None: + """Test we handle errors.""" + result = await hass.config_entries.flow.async_init( + ollama.DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.ollama.config_flow.ollama.AsyncClient.list", + side_effect=side_effect, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {ollama.CONF_URL: "http://localhost:11434"} + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {"base": error} + + +async def test_download_error(hass: HomeAssistant) -> None: + """Test we handle errors while downloading a model.""" + result = await hass.config_entries.flow.async_init( + ollama.DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with ( + patch( + "homeassistant.components.ollama.config_flow.ollama.AsyncClient.list", + return_value={}, + ), + patch( + "homeassistant.components.ollama.config_flow.ollama.AsyncClient.pull", + side_effect=RuntimeError(), + ), + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {ollama.CONF_URL: "http://localhost:11434"} + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.FORM + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], {ollama.CONF_MODEL: TEST_MODEL} + ) + await hass.async_block_till_done() + + assert result3["type"] == FlowResultType.SHOW_PROGRESS + result4 = await hass.config_entries.flow.async_configure(result3["flow_id"]) + await hass.async_block_till_done() + + assert result4["type"] == FlowResultType.ABORT + assert result4["reason"] == "download_failed" diff --git a/tests/components/ollama/test_init.py b/tests/components/ollama/test_init.py new file mode 100644 index 00000000000..ffe69ca4628 --- /dev/null +++ b/tests/components/ollama/test_init.py @@ -0,0 +1,366 @@ +"""Tests for the Ollama integration.""" + +from unittest.mock import AsyncMock, patch + +from httpx import ConnectError +from ollama import Message, ResponseError +import pytest + +from homeassistant.components import conversation, ollama +from homeassistant.components.homeassistant.exposed_entities import async_expose_entity +from homeassistant.const import ATTR_FRIENDLY_NAME, MATCH_ALL +from homeassistant.core import Context, HomeAssistant +from homeassistant.helpers import ( + area_registry as ar, + device_registry as dr, + entity_registry as er, + intent, +) +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry + + +async def test_chat( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_init_component, + area_registry: ar.AreaRegistry, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, +) -> None: + """Test that the chat function is called with the appropriate arguments.""" + + # Create some areas, devices, and entities + area_kitchen = area_registry.async_get_or_create("kitchen_id") + area_kitchen = area_registry.async_update(area_kitchen.id, name="kitchen") + area_bedroom = area_registry.async_get_or_create("bedroom_id") + area_bedroom = area_registry.async_update(area_bedroom.id, name="bedroom") + area_office = area_registry.async_get_or_create("office_id") + area_office = area_registry.async_update(area_office.id, name="office") + + entry = MockConfigEntry() + entry.add_to_hass(hass) + kitchen_device = device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + connections=set(), + identifiers={("demo", "id-1234")}, + ) + device_registry.async_update_device(kitchen_device.id, area_id=area_kitchen.id) + + kitchen_light = entity_registry.async_get_or_create("light", "demo", "1234") + kitchen_light = entity_registry.async_update_entity( + kitchen_light.entity_id, device_id=kitchen_device.id + ) + hass.states.async_set( + kitchen_light.entity_id, "on", attributes={ATTR_FRIENDLY_NAME: "kitchen light"} + ) + + bedroom_light = entity_registry.async_get_or_create("light", "demo", "5678") + bedroom_light = entity_registry.async_update_entity( + bedroom_light.entity_id, area_id=area_bedroom.id + ) + hass.states.async_set( + bedroom_light.entity_id, "on", attributes={ATTR_FRIENDLY_NAME: "bedroom light"} + ) + + # Hide the office light + office_light = entity_registry.async_get_or_create("light", "demo", "ABCD") + office_light = entity_registry.async_update_entity( + office_light.entity_id, area_id=area_office.id + ) + hass.states.async_set( + office_light.entity_id, "on", attributes={ATTR_FRIENDLY_NAME: "office light"} + ) + async_expose_entity(hass, conversation.DOMAIN, office_light.entity_id, False) + + with patch( + "ollama.AsyncClient.chat", + return_value={"message": {"role": "assistant", "content": "test response"}}, + ) as mock_chat: + result = await conversation.async_converse( + hass, + "test message", + None, + Context(), + agent_id=mock_config_entry.entry_id, + ) + + assert mock_chat.call_count == 1 + args = mock_chat.call_args.kwargs + prompt = args["messages"][0]["content"] + + assert args["model"] == "test model" + assert args["messages"] == [ + Message({"role": "system", "content": prompt}), + Message({"role": "user", "content": "test message"}), + ] + + # Verify only exposed devices/areas are in prompt + assert "kitchen light" in prompt + assert "bedroom light" in prompt + assert "office light" not in prompt + assert "office" not in prompt + + assert ( + result.response.response_type == intent.IntentResponseType.ACTION_DONE + ), result + assert result.response.speech["plain"]["speech"] == "test response" + + +async def test_message_history_trimming( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_init_component +) -> None: + """Test that a single message history is trimmed according to the config.""" + response_idx = 0 + + def response(*args, **kwargs) -> dict: + nonlocal response_idx + response_idx += 1 + return {"message": {"role": "assistant", "content": f"response {response_idx}"}} + + with patch( + "ollama.AsyncClient.chat", + side_effect=response, + ) as mock_chat: + # mock_init_component sets "max_history" to 2 + for i in range(5): + result = await conversation.async_converse( + hass, + f"message {i+1}", + conversation_id="1234", + context=Context(), + agent_id=mock_config_entry.entry_id, + ) + assert ( + result.response.response_type == intent.IntentResponseType.ACTION_DONE + ), result + + assert mock_chat.call_count == 5 + args = mock_chat.call_args_list + prompt = args[0].kwargs["messages"][0]["content"] + + # system + user-1 + assert len(args[0].kwargs["messages"]) == 2 + assert args[0].kwargs["messages"][1]["content"] == "message 1" + + # Full history + # system + user-1 + assistant-1 + user-2 + assert len(args[1].kwargs["messages"]) == 4 + assert args[1].kwargs["messages"][0]["role"] == "system" + assert args[1].kwargs["messages"][0]["content"] == prompt + assert args[1].kwargs["messages"][1]["role"] == "user" + assert args[1].kwargs["messages"][1]["content"] == "message 1" + assert args[1].kwargs["messages"][2]["role"] == "assistant" + assert args[1].kwargs["messages"][2]["content"] == "response 1" + assert args[1].kwargs["messages"][3]["role"] == "user" + assert args[1].kwargs["messages"][3]["content"] == "message 2" + + # Full history + # system + user-1 + assistant-1 + user-2 + assistant-2 + user-3 + assert len(args[2].kwargs["messages"]) == 6 + assert args[2].kwargs["messages"][0]["role"] == "system" + assert args[2].kwargs["messages"][0]["content"] == prompt + assert args[2].kwargs["messages"][1]["role"] == "user" + assert args[2].kwargs["messages"][1]["content"] == "message 1" + assert args[2].kwargs["messages"][2]["role"] == "assistant" + assert args[2].kwargs["messages"][2]["content"] == "response 1" + assert args[2].kwargs["messages"][3]["role"] == "user" + assert args[2].kwargs["messages"][3]["content"] == "message 2" + assert args[2].kwargs["messages"][4]["role"] == "assistant" + assert args[2].kwargs["messages"][4]["content"] == "response 2" + assert args[2].kwargs["messages"][5]["role"] == "user" + assert args[2].kwargs["messages"][5]["content"] == "message 3" + + # Trimmed down to two user messages. + # system + user-2 + assistant-2 + user-3 + assistant-3 + user-4 + assert len(args[3].kwargs["messages"]) == 6 + assert args[3].kwargs["messages"][0]["role"] == "system" + assert args[3].kwargs["messages"][0]["content"] == prompt + assert args[3].kwargs["messages"][1]["role"] == "user" + assert args[3].kwargs["messages"][1]["content"] == "message 2" + assert args[3].kwargs["messages"][2]["role"] == "assistant" + assert args[3].kwargs["messages"][2]["content"] == "response 2" + assert args[3].kwargs["messages"][3]["role"] == "user" + assert args[3].kwargs["messages"][3]["content"] == "message 3" + assert args[3].kwargs["messages"][4]["role"] == "assistant" + assert args[3].kwargs["messages"][4]["content"] == "response 3" + assert args[3].kwargs["messages"][5]["role"] == "user" + assert args[3].kwargs["messages"][5]["content"] == "message 4" + + # Trimmed down to two user messages. + # system + user-3 + assistant-3 + user-4 + assistant-4 + user-5 + assert len(args[3].kwargs["messages"]) == 6 + assert args[4].kwargs["messages"][0]["role"] == "system" + assert args[4].kwargs["messages"][0]["content"] == prompt + assert args[4].kwargs["messages"][1]["role"] == "user" + assert args[4].kwargs["messages"][1]["content"] == "message 3" + assert args[4].kwargs["messages"][2]["role"] == "assistant" + assert args[4].kwargs["messages"][2]["content"] == "response 3" + assert args[4].kwargs["messages"][3]["role"] == "user" + assert args[4].kwargs["messages"][3]["content"] == "message 4" + assert args[4].kwargs["messages"][4]["role"] == "assistant" + assert args[4].kwargs["messages"][4]["content"] == "response 4" + assert args[4].kwargs["messages"][5]["role"] == "user" + assert args[4].kwargs["messages"][5]["content"] == "message 5" + + +async def test_message_history_pruning( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_init_component +) -> None: + """Test that old message histories are pruned.""" + with patch( + "ollama.AsyncClient.chat", + return_value={"message": {"role": "assistant", "content": "test response"}}, + ): + # Create 3 different message histories + conversation_ids: list[str] = [] + for i in range(3): + result = await conversation.async_converse( + hass, + f"message {i+1}", + conversation_id=None, + context=Context(), + agent_id=mock_config_entry.entry_id, + ) + assert ( + result.response.response_type == intent.IntentResponseType.ACTION_DONE + ), result + assert isinstance(result.conversation_id, str) + conversation_ids.append(result.conversation_id) + + agent = await conversation._get_agent_manager(hass).async_get_agent( + mock_config_entry.entry_id + ) + assert isinstance(agent, ollama.OllamaAgent) + assert len(agent._history) == 3 + assert agent._history.keys() == set(conversation_ids) + + # Modify the timestamps of the first 2 histories so they will be pruned + # on the next cycle. + for conversation_id in conversation_ids[:2]: + # Move back 2 hours + agent._history[conversation_id].timestamp -= 2 * 60 * 60 + + # Next cycle + result = await conversation.async_converse( + hass, + "test message", + conversation_id=None, + context=Context(), + agent_id=mock_config_entry.entry_id, + ) + assert ( + result.response.response_type == intent.IntentResponseType.ACTION_DONE + ), result + + # Only the most recent histories should remain + assert len(agent._history) == 2 + assert conversation_ids[-1] in agent._history + assert result.conversation_id in agent._history + + +async def test_message_history_unlimited( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_init_component +) -> None: + """Test that message history is not trimmed when max_history = 0.""" + conversation_id = "1234" + with ( + patch( + "ollama.AsyncClient.chat", + return_value={"message": {"role": "assistant", "content": "test response"}}, + ), + patch.object(mock_config_entry, "options", {ollama.CONF_MAX_HISTORY: 0}), + ): + for i in range(100): + result = await conversation.async_converse( + hass, + f"message {i+1}", + conversation_id=conversation_id, + context=Context(), + agent_id=mock_config_entry.entry_id, + ) + assert ( + result.response.response_type == intent.IntentResponseType.ACTION_DONE + ), result + + agent = await conversation._get_agent_manager(hass).async_get_agent( + mock_config_entry.entry_id + ) + assert isinstance(agent, ollama.OllamaAgent) + + assert len(agent._history) == 1 + assert conversation_id in agent._history + assert agent._history[conversation_id].num_user_messages == 100 + + +async def test_error_handling( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_init_component +) -> None: + """Test error handling during converse.""" + with patch( + "ollama.AsyncClient.chat", + new_callable=AsyncMock, + side_effect=ResponseError("test error"), + ): + result = await conversation.async_converse( + hass, "hello", None, Context(), agent_id=mock_config_entry.entry_id + ) + + assert result.response.response_type == intent.IntentResponseType.ERROR, result + assert result.response.error_code == "unknown", result + + +async def test_template_error( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: + """Test that template error handling works.""" + hass.config_entries.async_update_entry( + mock_config_entry, + options={ + "prompt": "talk like a {% if True %}smarthome{% else %}pirate please.", + }, + ) + with patch( + "ollama.AsyncClient.list", + ): + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + result = await conversation.async_converse( + hass, "hello", None, Context(), agent_id=mock_config_entry.entry_id + ) + + assert result.response.response_type == intent.IntentResponseType.ERROR, result + assert result.response.error_code == "unknown", result + + +async def test_conversation_agent( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_init_component, +) -> None: + """Test OllamaAgent.""" + agent = await conversation._get_agent_manager(hass).async_get_agent( + mock_config_entry.entry_id + ) + assert agent.supported_languages == MATCH_ALL + + +@pytest.mark.parametrize( + ("side_effect", "error"), + [ + (ConnectError(message="Connect error"), "Connect error"), + (RuntimeError("Runtime error"), "Runtime error"), + ], +) +async def test_init_error( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, caplog, side_effect, error +) -> None: + """Test initialization errors.""" + with patch( + "ollama.AsyncClient.list", + side_effect=side_effect, + ): + assert await async_setup_component(hass, ollama.DOMAIN, {}) + await hass.async_block_till_done() + assert error in caplog.text diff --git a/tests/components/omnilogic/test_config_flow.py b/tests/components/omnilogic/test_config_flow.py index 17f4f782744..1c0d6aefcf8 100644 --- a/tests/components/omnilogic/test_config_flow.py +++ b/tests/components/omnilogic/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Omnilogic config flow.""" + from unittest.mock import patch from omnilogic import LoginException, OmniLogicException @@ -21,13 +22,16 @@ async def test_form(hass: HomeAssistant) -> None: assert result["type"] == "form" assert result["errors"] == {} - with patch( - "homeassistant.components.omnilogic.config_flow.OmniLogic.connect", - return_value=True, - ), patch( - "homeassistant.components.omnilogic.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.omnilogic.config_flow.OmniLogic.connect", + return_value=True, + ), + patch( + "homeassistant.components.omnilogic.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], DATA, diff --git a/tests/components/onboarding/test_init.py b/tests/components/onboarding/test_init.py index bcaa9ad611f..6688ecccb5d 100644 --- a/tests/components/onboarding/test_init.py +++ b/tests/components/onboarding/test_init.py @@ -1,4 +1,5 @@ """Tests for the init.""" + from typing import Any from unittest.mock import Mock, patch @@ -47,10 +48,10 @@ async def test_is_onboarded() -> None: assert onboarding.async_is_onboarded(hass) - hass.data[onboarding.DOMAIN] = True + hass.data[onboarding.DOMAIN] = onboarding.OnboardingData([], True, {"done": []}) assert onboarding.async_is_onboarded(hass) - hass.data[onboarding.DOMAIN] = {"done": []} + hass.data[onboarding.DOMAIN] = onboarding.OnboardingData([], False, {"done": []}) assert not onboarding.async_is_onboarded(hass) @@ -61,10 +62,15 @@ async def test_is_user_onboarded() -> None: assert onboarding.async_is_user_onboarded(hass) - hass.data[onboarding.DOMAIN] = True + hass.data[onboarding.DOMAIN] = onboarding.OnboardingData([], True, {"done": []}) assert onboarding.async_is_user_onboarded(hass) - hass.data[onboarding.DOMAIN] = {"done": []} + hass.data[onboarding.DOMAIN] = onboarding.OnboardingData( + [], False, {"done": ["user"]} + ) + assert onboarding.async_is_user_onboarded(hass) + + hass.data[onboarding.DOMAIN] = onboarding.OnboardingData([], False, {"done": []}) assert not onboarding.async_is_user_onboarded(hass) @@ -74,9 +80,10 @@ async def test_having_owner_finishes_user_step( """If owner user already exists, mark user step as complete.""" MockUser(is_owner=True).add_to_hass(hass) - with patch( - "homeassistant.components.onboarding.views.async_setup" - ) as mock_setup, patch.object(onboarding, "STEPS", [onboarding.STEP_USER]): + with ( + patch("homeassistant.components.onboarding.views.async_setup") as mock_setup, + patch.object(onboarding, "STEPS", [onboarding.STEP_USER]), + ): assert await async_setup_component(hass, "onboarding", {}) assert len(mock_setup.mock_calls) == 0 diff --git a/tests/components/onboarding/test_views.py b/tests/components/onboarding/test_views.py index b23f693b230..556b590e746 100644 --- a/tests/components/onboarding/test_views.py +++ b/tests/components/onboarding/test_views.py @@ -1,9 +1,10 @@ """Test the onboarding views.""" + import asyncio from http import HTTPStatus import os from typing import Any -from unittest.mock import patch +from unittest.mock import Mock, patch import pytest @@ -79,30 +80,40 @@ async def mock_supervisor_fixture(hass, aioclient_mock): }, }, ) - with patch.dict(os.environ, {"SUPERVISOR": "127.0.0.1"}), patch( - "homeassistant.components.hassio.HassIO.is_connected", - return_value=True, - ), patch( - "homeassistant.components.hassio.HassIO.get_info", - return_value={}, - ), patch( - "homeassistant.components.hassio.HassIO.get_host_info", - return_value={}, - ), patch( - "homeassistant.components.hassio.HassIO.get_store", - return_value={}, - ), patch( - "homeassistant.components.hassio.HassIO.get_supervisor_info", - return_value={"diagnostics": True}, - ), patch( - "homeassistant.components.hassio.HassIO.get_os_info", - return_value={}, - ), patch( - "homeassistant.components.hassio.HassIO.get_ingress_panels", - return_value={"panels": {}}, - ), patch.dict( - os.environ, - {"SUPERVISOR_TOKEN": "123456"}, + with ( + patch.dict(os.environ, {"SUPERVISOR": "127.0.0.1"}), + patch( + "homeassistant.components.hassio.HassIO.is_connected", + return_value=True, + ), + patch( + "homeassistant.components.hassio.HassIO.get_info", + return_value={}, + ), + patch( + "homeassistant.components.hassio.HassIO.get_host_info", + return_value={}, + ), + patch( + "homeassistant.components.hassio.HassIO.get_store", + return_value={}, + ), + patch( + "homeassistant.components.hassio.HassIO.get_supervisor_info", + return_value={"diagnostics": True}, + ), + patch( + "homeassistant.components.hassio.HassIO.get_os_info", + return_value={}, + ), + patch( + "homeassistant.components.hassio.HassIO.get_ingress_panels", + return_value={"panels": {}}, + ), + patch.dict( + os.environ, + {"SUPERVISOR_TOKEN": "123456"}, + ), ): yield @@ -110,16 +121,18 @@ async def mock_supervisor_fixture(hass, aioclient_mock): @pytest.fixture def mock_default_integrations(): """Mock the default integrations set up during onboarding.""" - with patch( - "homeassistant.components.rpi_power.config_flow.new_under_voltage" - ), patch( - "homeassistant.components.rpi_power.binary_sensor.new_under_voltage" - ), patch( - "homeassistant.components.met.async_setup_entry", return_value=True - ), patch( - "homeassistant.components.radio_browser.async_setup_entry", return_value=True - ), patch( - "homeassistant.components.shopping_list.async_setup_entry", return_value=True + with ( + patch("homeassistant.components.rpi_power.config_flow.new_under_voltage"), + patch("homeassistant.components.rpi_power.binary_sensor.new_under_voltage"), + patch("homeassistant.components.met.async_setup_entry", return_value=True), + patch( + "homeassistant.components.radio_browser.async_setup_entry", + return_value=True, + ), + patch( + "homeassistant.components.shopping_list.async_setup_entry", + return_value=True, + ), ): yield @@ -567,6 +580,28 @@ async def test_onboarding_core_no_rpi_power( assert not rpi_power_state +async def test_onboarding_core_ensures_analytics_loaded( + hass: HomeAssistant, + hass_storage: dict[str, Any], + hass_client: ClientSessionGenerator, + mock_default_integrations, +) -> None: + """Test finishing the core step ensures analytics is ready.""" + mock_storage(hass_storage, {"done": [const.STEP_USER]}) + assert "analytics" not in hass.config.components + + assert await async_setup_component(hass, "onboarding", {}) + await hass.async_block_till_done() + + client = await hass_client() + resp = await client.post("/api/onboarding/core_config") + + assert resp.status == 200 + + await hass.async_block_till_done() + assert "analytics" in hass.config.components + + async def test_onboarding_analytics( hass: HomeAssistant, hass_storage: dict[str, Any], @@ -632,3 +667,64 @@ async def test_onboarding_installation_type_after_done( resp = await client.get("/api/onboarding/installation_type") assert resp.status == 401 + + +async def test_complete_onboarding( + hass: HomeAssistant, hass_client: ClientSessionGenerator +) -> None: + """Test completing onboarding calls listeners.""" + listener_1 = Mock() + onboarding.async_add_listener(hass, listener_1) + listener_1.assert_not_called() + + assert await async_setup_component(hass, "onboarding", {}) + await hass.async_block_till_done() + + listener_2 = Mock() + onboarding.async_add_listener(hass, listener_2) + listener_2.assert_not_called() + + client = await hass_client() + + assert not onboarding.async_is_onboarded(hass) + + # Complete the user step + resp = await client.post( + "/api/onboarding/users", + json={ + "client_id": CLIENT_ID, + "name": "Test Name", + "username": "test-user", + "password": "test-pass", + "language": "en", + }, + ) + assert resp.status == 200 + assert not onboarding.async_is_onboarded(hass) + listener_2.assert_not_called() + + # Complete the core config step + resp = await client.post("/api/onboarding/core_config") + assert resp.status == 200 + assert not onboarding.async_is_onboarded(hass) + listener_2.assert_not_called() + + # Complete the integration step + resp = await client.post( + "/api/onboarding/integration", + json={"client_id": CLIENT_ID, "redirect_uri": CLIENT_REDIRECT_URI}, + ) + assert resp.status == 200 + assert not onboarding.async_is_onboarded(hass) + listener_2.assert_not_called() + + # Complete the analytics step + resp = await client.post("/api/onboarding/analytics") + assert resp.status == 200 + assert onboarding.async_is_onboarded(hass) + listener_1.assert_not_called() # Registered before the integration was setup + listener_2.assert_called_once_with() + + listener_3 = Mock() + onboarding.async_add_listener(hass, listener_3) + listener_3.assert_called_once_with() diff --git a/tests/components/oncue/__init__.py b/tests/components/oncue/__init__.py index 26b08460a93..df1452b176e 100644 --- a/tests/components/oncue/__init__.py +++ b/tests/components/oncue/__init__.py @@ -1,4 +1,5 @@ """Tests for the Oncue integration.""" + from contextlib import contextmanager from unittest.mock import patch @@ -801,11 +802,14 @@ MOCK_ASYNC_FETCH_ALL_UNAVAILABLE_DEVICE = { def _patch_login_and_data(): @contextmanager def _patcher(): - with patch( - "homeassistant.components.oncue.Oncue.async_login", - ), patch( - "homeassistant.components.oncue.Oncue.async_fetch_all", - return_value=MOCK_ASYNC_FETCH_ALL, + with ( + patch( + "homeassistant.components.oncue.Oncue.async_login", + ), + patch( + "homeassistant.components.oncue.Oncue.async_fetch_all", + return_value=MOCK_ASYNC_FETCH_ALL, + ), ): yield @@ -815,11 +819,14 @@ def _patch_login_and_data(): def _patch_login_and_data_offline_device(): @contextmanager def _patcher(): - with patch( - "homeassistant.components.oncue.Oncue.async_login", - ), patch( - "homeassistant.components.oncue.Oncue.async_fetch_all", - return_value=MOCK_ASYNC_FETCH_ALL_OFFLINE_DEVICE, + with ( + patch( + "homeassistant.components.oncue.Oncue.async_login", + ), + patch( + "homeassistant.components.oncue.Oncue.async_fetch_all", + return_value=MOCK_ASYNC_FETCH_ALL_OFFLINE_DEVICE, + ), ): yield @@ -829,9 +836,12 @@ def _patch_login_and_data_offline_device(): def _patch_login_and_data_unavailable(): @contextmanager def _patcher(): - with patch("homeassistant.components.oncue.Oncue.async_login"), patch( - "homeassistant.components.oncue.Oncue.async_fetch_all", - return_value=MOCK_ASYNC_FETCH_ALL_UNAVAILABLE_DEVICE, + with ( + patch("homeassistant.components.oncue.Oncue.async_login"), + patch( + "homeassistant.components.oncue.Oncue.async_fetch_all", + return_value=MOCK_ASYNC_FETCH_ALL_UNAVAILABLE_DEVICE, + ), ): yield @@ -841,9 +851,12 @@ def _patch_login_and_data_unavailable(): def _patch_login_and_data_unavailable_device(): @contextmanager def _patcher(): - with patch("homeassistant.components.oncue.Oncue.async_login"), patch( - "homeassistant.components.oncue.Oncue.async_fetch_all", - return_value=MOCK_ASYNC_FETCH_ALL_UNAVAILABLE_DEVICE, + with ( + patch("homeassistant.components.oncue.Oncue.async_login"), + patch( + "homeassistant.components.oncue.Oncue.async_fetch_all", + return_value=MOCK_ASYNC_FETCH_ALL_UNAVAILABLE_DEVICE, + ), ): yield diff --git a/tests/components/oncue/test_binary_sensor.py b/tests/components/oncue/test_binary_sensor.py index f2e7657089f..12ecb19ebc4 100644 --- a/tests/components/oncue/test_binary_sensor.py +++ b/tests/components/oncue/test_binary_sensor.py @@ -1,4 +1,5 @@ """Tests for the oncue binary_sensor.""" + from __future__ import annotations from homeassistant.components import oncue diff --git a/tests/components/oncue/test_config_flow.py b/tests/components/oncue/test_config_flow.py index 979cf3b2677..d757adec771 100644 --- a/tests/components/oncue/test_config_flow.py +++ b/tests/components/oncue/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Oncue config flow.""" + from unittest.mock import patch from aiooncue import LoginFailedException @@ -19,10 +20,13 @@ async def test_form(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.FORM assert result["errors"] == {} - with patch("homeassistant.components.oncue.config_flow.Oncue.async_login"), patch( - "homeassistant.components.oncue.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch("homeassistant.components.oncue.config_flow.Oncue.async_login"), + patch( + "homeassistant.components.oncue.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { diff --git a/tests/components/oncue/test_init.py b/tests/components/oncue/test_init.py index dd3cb20f373..f10d94d719b 100644 --- a/tests/components/oncue/test_init.py +++ b/tests/components/oncue/test_init.py @@ -1,4 +1,5 @@ """Tests for the oncue component.""" + from __future__ import annotations from unittest.mock import patch diff --git a/tests/components/oncue/test_sensor.py b/tests/components/oncue/test_sensor.py index 65a29e34e4c..13f5a8b944d 100644 --- a/tests/components/oncue/test_sensor.py +++ b/tests/components/oncue/test_sensor.py @@ -1,4 +1,5 @@ """Tests for the oncue sensor.""" + from __future__ import annotations import pytest @@ -24,8 +25,8 @@ from tests.common import MockConfigEntry @pytest.mark.parametrize( ("patcher", "connections"), [ - [_patch_login_and_data, {("mac", "c9:24:22:6f:14:00")}], - [_patch_login_and_data_offline_device, set()], + (_patch_login_and_data, {("mac", "c9:24:22:6f:14:00")}), + (_patch_login_and_data_offline_device, set()), ], ) async def test_sensors(hass: HomeAssistant, patcher, connections) -> None: @@ -151,8 +152,8 @@ async def test_sensors(hass: HomeAssistant, patcher, connections) -> None: @pytest.mark.parametrize( ("patcher", "connections"), [ - [_patch_login_and_data_unavailable_device, set()], - [_patch_login_and_data_unavailable, {("mac", "c9:24:22:6f:14:00")}], + (_patch_login_and_data_unavailable_device, set()), + (_patch_login_and_data_unavailable, {("mac", "c9:24:22:6f:14:00")}), ], ) async def test_sensors_unavailable(hass: HomeAssistant, patcher, connections) -> None: diff --git a/tests/components/ondilo_ico/test_config_flow.py b/tests/components/ondilo_ico/test_config_flow.py index 002b6d43feb..53483241c0b 100644 --- a/tests/components/ondilo_ico/test_config_flow.py +++ b/tests/components/ondilo_ico/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Ondilo ICO config flow.""" + from unittest.mock import patch from homeassistant import config_entries, data_entry_flow, setup diff --git a/tests/components/onewire/__init__.py b/tests/components/onewire/__init__.py index 4185337dce2..ed15cac94be 100644 --- a/tests/components/onewire/__init__.py +++ b/tests/components/onewire/__init__.py @@ -1,4 +1,5 @@ """Tests for 1-Wire integration.""" + from __future__ import annotations from typing import Any @@ -31,7 +32,7 @@ def setup_owproxy_mock_devices( ) # Ensure enough read side effect - dir_side_effect = [main_dir_return_value] + sub_dir_side_effect + dir_side_effect = [main_dir_return_value, *sub_dir_side_effect] read_side_effect = ( main_read_side_effect + sub_read_side_effect @@ -101,7 +102,9 @@ def _setup_owproxy_mock_device_reads( device_sensors = mock_device.get(platform, []) if platform is Platform.SENSOR and device_id.startswith("12"): # We need to check if there is TAI8570 plugged in - for expected_sensor in device_sensors: - sub_read_side_effect.append(expected_sensor[ATTR_INJECT_READS]) - for expected_sensor in device_sensors: - sub_read_side_effect.append(expected_sensor[ATTR_INJECT_READS]) + sub_read_side_effect.extend( + expected_sensor[ATTR_INJECT_READS] for expected_sensor in device_sensors + ) + sub_read_side_effect.extend( + expected_sensor[ATTR_INJECT_READS] for expected_sensor in device_sensors + ) diff --git a/tests/components/onewire/conftest.py b/tests/components/onewire/conftest.py index 031b29d47a7..03a8443049e 100644 --- a/tests/components/onewire/conftest.py +++ b/tests/components/onewire/conftest.py @@ -1,4 +1,5 @@ """Provide common 1-Wire fixtures.""" + from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch diff --git a/tests/components/onewire/const.py b/tests/components/onewire/const.py index cb9fca54f3d..e5f8ac575e9 100644 --- a/tests/components/onewire/const.py +++ b/tests/components/onewire/const.py @@ -1,4 +1,5 @@ """Constants for 1-Wire integration.""" + from pyownet.protocol import Error as ProtocolError from homeassistant.components.onewire.const import Platform @@ -227,6 +228,29 @@ MOCK_OWPROXY_DEVICES = { {ATTR_INJECT_READS: b" 29.123"}, ], }, + "A6.111111111111": { + ATTR_INJECT_READS: [ + b"DS2438", # read device type + ], + Platform.SENSOR: [ + {ATTR_INJECT_READS: b" 25.123"}, + {ATTR_INJECT_READS: b" 72.7563"}, + {ATTR_INJECT_READS: b" 73.7563"}, + {ATTR_INJECT_READS: b" 74.7563"}, + {ATTR_INJECT_READS: b" 75.7563"}, + { + ATTR_INJECT_READS: ProtocolError, + }, + {ATTR_INJECT_READS: b" 969.265"}, + {ATTR_INJECT_READS: b" 65.8839"}, + {ATTR_INJECT_READS: b" 2.97"}, + {ATTR_INJECT_READS: b" 4.74"}, + {ATTR_INJECT_READS: b" 0.12"}, + ], + Platform.SWITCH: [ + {ATTR_INJECT_READS: b" 1"}, + ], + }, "EF.111111111111": { ATTR_INJECT_READS: [ b"HobbyBoards_EF", # read type diff --git a/tests/components/onewire/snapshots/test_binary_sensor.ambr b/tests/components/onewire/snapshots/test_binary_sensor.ambr index 0523c969ade..3123dfb6a5e 100644 --- a/tests/components/onewire/snapshots/test_binary_sensor.ambr +++ b/tests/components/onewire/snapshots/test_binary_sensor.ambr @@ -200,6 +200,7 @@ 'context': , 'entity_id': 'binary_sensor.12_111111111111_sensed_a', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }), @@ -212,6 +213,7 @@ 'context': , 'entity_id': 'binary_sensor.12_111111111111_sensed_b', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }), @@ -820,6 +822,7 @@ 'context': , 'entity_id': 'binary_sensor.29_111111111111_sensed_0', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }), @@ -832,6 +835,7 @@ 'context': , 'entity_id': 'binary_sensor.29_111111111111_sensed_1', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }), @@ -844,6 +848,7 @@ 'context': , 'entity_id': 'binary_sensor.29_111111111111_sensed_2', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }), @@ -856,6 +861,7 @@ 'context': , 'entity_id': 'binary_sensor.29_111111111111_sensed_3', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -868,6 +874,7 @@ 'context': , 'entity_id': 'binary_sensor.29_111111111111_sensed_4', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }), @@ -880,6 +887,7 @@ 'context': , 'entity_id': 'binary_sensor.29_111111111111_sensed_5', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }), @@ -892,6 +900,7 @@ 'context': , 'entity_id': 'binary_sensor.29_111111111111_sensed_6', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }), @@ -904,6 +913,7 @@ 'context': , 'entity_id': 'binary_sensor.29_111111111111_sensed_7', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }), @@ -1058,6 +1068,7 @@ 'context': , 'entity_id': 'binary_sensor.3a_111111111111_sensed_a', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }), @@ -1070,6 +1081,7 @@ 'context': , 'entity_id': 'binary_sensor.3a_111111111111_sensed_b', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }), @@ -1235,6 +1247,46 @@ list([ ]) # --- +# name: test_binary_sensors[A6.111111111111] + list([ + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'onewire', + 'A6.111111111111', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'Maxim Integrated', + 'model': 'DS2438', + 'name': 'A6.111111111111', + 'name_by_user': None, + 'serial_number': None, + 'suggested_area': None, + 'sw_version': None, + 'via_device_id': None, + }), + ]) +# --- +# name: test_binary_sensors[A6.111111111111].1 + list([ + ]) +# --- +# name: test_binary_sensors[A6.111111111111].2 + list([ + ]) +# --- # name: test_binary_sensors[EF.111111111111] list([ DeviceRegistryEntrySnapshot({ @@ -1487,6 +1539,7 @@ 'context': , 'entity_id': 'binary_sensor.ef_111111111113_hub_short_on_branch_0', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }), @@ -1500,6 +1553,7 @@ 'context': , 'entity_id': 'binary_sensor.ef_111111111113_hub_short_on_branch_1', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }), @@ -1513,6 +1567,7 @@ 'context': , 'entity_id': 'binary_sensor.ef_111111111113_hub_short_on_branch_2', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }), @@ -1526,6 +1581,7 @@ 'context': , 'entity_id': 'binary_sensor.ef_111111111113_hub_short_on_branch_3', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }), diff --git a/tests/components/onewire/snapshots/test_sensor.ambr b/tests/components/onewire/snapshots/test_sensor.ambr index 970d63f4dac..aa8c914ece5 100644 --- a/tests/components/onewire/snapshots/test_sensor.ambr +++ b/tests/components/onewire/snapshots/test_sensor.ambr @@ -134,6 +134,7 @@ 'context': , 'entity_id': 'sensor.10_111111111111_temperature', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '25.1', }), @@ -255,6 +256,7 @@ 'context': , 'entity_id': 'sensor.12_111111111111_temperature', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '25.1', }), @@ -270,6 +272,7 @@ 'context': , 'entity_id': 'sensor.12_111111111111_pressure', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '1025.1', }), @@ -390,6 +393,7 @@ 'context': , 'entity_id': 'sensor.1d_111111111111_counter_a', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '251123', }), @@ -404,6 +408,7 @@ 'context': , 'entity_id': 'sensor.1d_111111111111_counter_b', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '248125', }), @@ -552,6 +557,7 @@ 'context': , 'entity_id': 'sensor.1d_111111111111_counter_a', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '251123', }), @@ -566,6 +572,7 @@ 'context': , 'entity_id': 'sensor.1d_111111111111_counter_b', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '248125', }), @@ -654,6 +661,7 @@ 'context': , 'entity_id': 'sensor.22_111111111111_temperature', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -1072,6 +1080,7 @@ 'context': , 'entity_id': 'sensor.26_111111111111_temperature', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '25.1', }), @@ -1087,6 +1096,7 @@ 'context': , 'entity_id': 'sensor.26_111111111111_humidity', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '72.8', }), @@ -1102,6 +1112,7 @@ 'context': , 'entity_id': 'sensor.26_111111111111_hih3600_humidity', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '73.8', }), @@ -1117,6 +1128,7 @@ 'context': , 'entity_id': 'sensor.26_111111111111_hih4000_humidity', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '74.8', }), @@ -1132,6 +1144,7 @@ 'context': , 'entity_id': 'sensor.26_111111111111_hih5030_humidity', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '75.8', }), @@ -1147,6 +1160,7 @@ 'context': , 'entity_id': 'sensor.26_111111111111_htm1735_humidity', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -1162,6 +1176,7 @@ 'context': , 'entity_id': 'sensor.26_111111111111_pressure', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '969.3', }), @@ -1177,6 +1192,7 @@ 'context': , 'entity_id': 'sensor.26_111111111111_illuminance', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '65.9', }), @@ -1192,6 +1208,7 @@ 'context': , 'entity_id': 'sensor.26_111111111111_vad_voltage', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '3.0', }), @@ -1207,6 +1224,7 @@ 'context': , 'entity_id': 'sensor.26_111111111111_vdd_voltage', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '4.7', }), @@ -1222,6 +1240,7 @@ 'context': , 'entity_id': 'sensor.26_111111111111_vis_voltage_difference', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0.1', }), @@ -1310,6 +1329,7 @@ 'context': , 'entity_id': 'sensor.28_111111111111_temperature', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '27.0', }), @@ -1398,6 +1418,7 @@ 'context': , 'entity_id': 'sensor.28_222222222222_temperature', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '27.0', }), @@ -1486,6 +1507,7 @@ 'context': , 'entity_id': 'sensor.28_222222222223_temperature', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '27.0', }), @@ -1713,6 +1735,7 @@ 'context': , 'entity_id': 'sensor.30_111111111111_temperature', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '27.0', }), @@ -1728,6 +1751,7 @@ 'context': , 'entity_id': 'sensor.30_111111111111_thermocouple_k_temperature', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '173.8', }), @@ -1743,6 +1767,7 @@ 'context': , 'entity_id': 'sensor.30_111111111111_voltage', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '3.0', }), @@ -1758,6 +1783,7 @@ 'context': , 'entity_id': 'sensor.30_111111111111_vis_voltage_gradient', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0.1', }), @@ -1886,6 +1912,7 @@ 'context': , 'entity_id': 'sensor.3b_111111111111_temperature', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '28.2', }), @@ -1974,6 +2001,7 @@ 'context': , 'entity_id': 'sensor.42_111111111111_temperature', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '29.1', }), @@ -2161,6 +2189,7 @@ 'context': , 'entity_id': 'sensor.7e_111111111111_temperature', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '13.9', }), @@ -2176,6 +2205,7 @@ 'context': , 'entity_id': 'sensor.7e_111111111111_pressure', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '1012.2', }), @@ -2191,6 +2221,7 @@ 'context': , 'entity_id': 'sensor.7e_111111111111_illuminance', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '65.9', }), @@ -2206,6 +2237,7 @@ 'context': , 'entity_id': 'sensor.7e_111111111111_humidity', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '41.4', }), @@ -2327,6 +2359,7 @@ 'context': , 'entity_id': 'sensor.7e_222222222222_temperature', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '13.9', }), @@ -2342,11 +2375,591 @@ 'context': , 'entity_id': 'sensor.7e_222222222222_pressure', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '1012.2', }), ]) # --- +# name: test_sensors[A6.111111111111] + list([ + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'onewire', + 'A6.111111111111', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'Maxim Integrated', + 'model': 'DS2438', + 'name': 'A6.111111111111', + 'name_by_user': None, + 'serial_number': None, + 'suggested_area': None, + 'sw_version': None, + 'via_device_id': None, + }), + ]) +# --- +# name: test_sensors[A6.111111111111].1 + list([ + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.a6_111111111111_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Temperature', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '/A6.111111111111/temperature', + 'unit_of_measurement': , + }), + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': , + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.a6_111111111111_humidity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Humidity', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '/A6.111111111111/humidity', + 'unit_of_measurement': '%', + }), + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': , + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.a6_111111111111_hih3600_humidity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'HIH3600 humidity', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'humidity_hih3600', + 'unique_id': '/A6.111111111111/HIH3600/humidity', + 'unit_of_measurement': '%', + }), + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': , + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.a6_111111111111_hih4000_humidity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'HIH4000 humidity', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'humidity_hih4000', + 'unique_id': '/A6.111111111111/HIH4000/humidity', + 'unit_of_measurement': '%', + }), + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': , + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.a6_111111111111_hih5030_humidity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'HIH5030 humidity', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'humidity_hih5030', + 'unique_id': '/A6.111111111111/HIH5030/humidity', + 'unit_of_measurement': '%', + }), + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': , + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.a6_111111111111_htm1735_humidity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'HTM1735 humidity', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'humidity_htm1735', + 'unique_id': '/A6.111111111111/HTM1735/humidity', + 'unit_of_measurement': '%', + }), + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': , + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.a6_111111111111_pressure', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Pressure', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '/A6.111111111111/B1-R1-A/pressure', + 'unit_of_measurement': , + }), + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': , + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.a6_111111111111_illuminance', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Illuminance', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '/A6.111111111111/S3-R1-A/illuminance', + 'unit_of_measurement': 'lx', + }), + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': , + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.a6_111111111111_vad_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'VAD voltage', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'voltage_vad', + 'unique_id': '/A6.111111111111/VAD', + 'unit_of_measurement': , + }), + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': , + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.a6_111111111111_vdd_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'VDD voltage', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'voltage_vdd', + 'unique_id': '/A6.111111111111/VDD', + 'unit_of_measurement': , + }), + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': , + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.a6_111111111111_vis_voltage_difference', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'VIS voltage difference', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'voltage_vis', + 'unique_id': '/A6.111111111111/vis', + 'unit_of_measurement': , + }), + ]) +# --- +# name: test_sensors[A6.111111111111].2 + list([ + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'device_file': '/A6.111111111111/temperature', + 'friendly_name': 'A6.111111111111 Temperature', + 'raw_value': 25.123, + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.a6_111111111111_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '25.1', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'humidity', + 'device_file': '/A6.111111111111/humidity', + 'friendly_name': 'A6.111111111111 Humidity', + 'raw_value': 72.7563, + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.a6_111111111111_humidity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '72.8', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'humidity', + 'device_file': '/A6.111111111111/HIH3600/humidity', + 'friendly_name': 'A6.111111111111 HIH3600 humidity', + 'raw_value': 73.7563, + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.a6_111111111111_hih3600_humidity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '73.8', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'humidity', + 'device_file': '/A6.111111111111/HIH4000/humidity', + 'friendly_name': 'A6.111111111111 HIH4000 humidity', + 'raw_value': 74.7563, + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.a6_111111111111_hih4000_humidity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '74.8', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'humidity', + 'device_file': '/A6.111111111111/HIH5030/humidity', + 'friendly_name': 'A6.111111111111 HIH5030 humidity', + 'raw_value': 75.7563, + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.a6_111111111111_hih5030_humidity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '75.8', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'humidity', + 'device_file': '/A6.111111111111/HTM1735/humidity', + 'friendly_name': 'A6.111111111111 HTM1735 humidity', + 'raw_value': None, + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.a6_111111111111_htm1735_humidity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'pressure', + 'device_file': '/A6.111111111111/B1-R1-A/pressure', + 'friendly_name': 'A6.111111111111 Pressure', + 'raw_value': 969.265, + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.a6_111111111111_pressure', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '969.3', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'illuminance', + 'device_file': '/A6.111111111111/S3-R1-A/illuminance', + 'friendly_name': 'A6.111111111111 Illuminance', + 'raw_value': 65.8839, + 'state_class': , + 'unit_of_measurement': 'lx', + }), + 'context': , + 'entity_id': 'sensor.a6_111111111111_illuminance', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '65.9', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'device_file': '/A6.111111111111/VAD', + 'friendly_name': 'A6.111111111111 VAD voltage', + 'raw_value': 2.97, + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.a6_111111111111_vad_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '3.0', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'device_file': '/A6.111111111111/VDD', + 'friendly_name': 'A6.111111111111 VDD voltage', + 'raw_value': 4.74, + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.a6_111111111111_vdd_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '4.7', + }), + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'device_file': '/A6.111111111111/vis', + 'friendly_name': 'A6.111111111111 VIS voltage difference', + 'raw_value': 0.12, + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.a6_111111111111_vis_voltage_difference', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.1', + }), + ]) +# --- # name: test_sensors[EF.111111111111] list([ DeviceRegistryEntrySnapshot({ @@ -2496,6 +3109,7 @@ 'context': , 'entity_id': 'sensor.ef_111111111111_humidity', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '67.7', }), @@ -2511,6 +3125,7 @@ 'context': , 'entity_id': 'sensor.ef_111111111111_raw_humidity', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '65.5', }), @@ -2526,6 +3141,7 @@ 'context': , 'entity_id': 'sensor.ef_111111111111_temperature', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '25.1', }), @@ -2713,6 +3329,7 @@ 'context': , 'entity_id': 'sensor.ef_111111111112_wetness_0', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '41.7', }), @@ -2728,6 +3345,7 @@ 'context': , 'entity_id': 'sensor.ef_111111111112_wetness_1', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '42.5', }), @@ -2743,6 +3361,7 @@ 'context': , 'entity_id': 'sensor.ef_111111111112_moisture_2', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '43.1', }), @@ -2758,6 +3377,7 @@ 'context': , 'entity_id': 'sensor.ef_111111111112_moisture_3', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '44.1', }), diff --git a/tests/components/onewire/snapshots/test_switch.ambr b/tests/components/onewire/snapshots/test_switch.ambr index 4f6498419a9..2ac542d203c 100644 --- a/tests/components/onewire/snapshots/test_switch.ambr +++ b/tests/components/onewire/snapshots/test_switch.ambr @@ -89,6 +89,7 @@ 'context': , 'entity_id': 'switch.05_111111111111_programmed_input_output', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }), @@ -305,6 +306,7 @@ 'context': , 'entity_id': 'switch.12_111111111111_programmed_input_output_a', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }), @@ -317,6 +319,7 @@ 'context': , 'entity_id': 'switch.12_111111111111_programmed_input_output_b', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }), @@ -329,6 +332,7 @@ 'context': , 'entity_id': 'switch.12_111111111111_latch_a', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }), @@ -341,6 +345,7 @@ 'context': , 'entity_id': 'switch.12_111111111111_latch_b', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }), @@ -572,6 +577,7 @@ 'context': , 'entity_id': 'switch.26_111111111111_current_a_d_control', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }), @@ -1240,6 +1246,7 @@ 'context': , 'entity_id': 'switch.29_111111111111_programmed_input_output_0', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }), @@ -1252,6 +1259,7 @@ 'context': , 'entity_id': 'switch.29_111111111111_programmed_input_output_1', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }), @@ -1264,6 +1272,7 @@ 'context': , 'entity_id': 'switch.29_111111111111_programmed_input_output_2', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }), @@ -1276,6 +1285,7 @@ 'context': , 'entity_id': 'switch.29_111111111111_programmed_input_output_3', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -1288,6 +1298,7 @@ 'context': , 'entity_id': 'switch.29_111111111111_programmed_input_output_4', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }), @@ -1300,6 +1311,7 @@ 'context': , 'entity_id': 'switch.29_111111111111_programmed_input_output_5', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }), @@ -1312,6 +1324,7 @@ 'context': , 'entity_id': 'switch.29_111111111111_programmed_input_output_6', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }), @@ -1324,6 +1337,7 @@ 'context': , 'entity_id': 'switch.29_111111111111_programmed_input_output_7', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }), @@ -1336,6 +1350,7 @@ 'context': , 'entity_id': 'switch.29_111111111111_latch_0', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }), @@ -1348,6 +1363,7 @@ 'context': , 'entity_id': 'switch.29_111111111111_latch_1', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }), @@ -1360,6 +1376,7 @@ 'context': , 'entity_id': 'switch.29_111111111111_latch_2', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }), @@ -1372,6 +1389,7 @@ 'context': , 'entity_id': 'switch.29_111111111111_latch_3', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }), @@ -1384,6 +1402,7 @@ 'context': , 'entity_id': 'switch.29_111111111111_latch_4', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }), @@ -1396,6 +1415,7 @@ 'context': , 'entity_id': 'switch.29_111111111111_latch_5', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }), @@ -1408,6 +1428,7 @@ 'context': , 'entity_id': 'switch.29_111111111111_latch_6', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }), @@ -1420,6 +1441,7 @@ 'context': , 'entity_id': 'switch.29_111111111111_latch_7', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }), @@ -1574,6 +1596,7 @@ 'context': , 'entity_id': 'switch.3a_111111111111_programmed_input_output_a', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }), @@ -1586,6 +1609,7 @@ 'context': , 'entity_id': 'switch.3a_111111111111_programmed_input_output_b', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }), @@ -1751,6 +1775,90 @@ list([ ]) # --- +# name: test_switches[A6.111111111111] + list([ + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'onewire', + 'A6.111111111111', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'Maxim Integrated', + 'model': 'DS2438', + 'name': 'A6.111111111111', + 'name_by_user': None, + 'serial_number': None, + 'suggested_area': None, + 'sw_version': None, + 'via_device_id': None, + }), + ]) +# --- +# name: test_switches[A6.111111111111].1 + list([ + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': , + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.a6_111111111111_current_a_d_control', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Current A/D control', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'iad', + 'unique_id': '/A6.111111111111/IAD', + 'unit_of_measurement': None, + }), + ]) +# --- +# name: test_switches[A6.111111111111].2 + list([ + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_file': '/A6.111111111111/IAD', + 'friendly_name': 'A6.111111111111 Current A/D control', + 'raw_value': 1.0, + }), + 'context': , + 'entity_id': 'switch.a6_111111111111_current_a_d_control', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }), + ]) +# --- # name: test_switches[EF.111111111111] list([ DeviceRegistryEntrySnapshot({ @@ -2086,6 +2194,7 @@ 'context': , 'entity_id': 'switch.ef_111111111112_leaf_sensor_0', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }), @@ -2098,6 +2207,7 @@ 'context': , 'entity_id': 'switch.ef_111111111112_leaf_sensor_1', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }), @@ -2110,6 +2220,7 @@ 'context': , 'entity_id': 'switch.ef_111111111112_leaf_sensor_2', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }), @@ -2122,6 +2233,7 @@ 'context': , 'entity_id': 'switch.ef_111111111112_leaf_sensor_3', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }), @@ -2134,6 +2246,7 @@ 'context': , 'entity_id': 'switch.ef_111111111112_moisture_sensor_0', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }), @@ -2146,6 +2259,7 @@ 'context': , 'entity_id': 'switch.ef_111111111112_moisture_sensor_1', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }), @@ -2158,6 +2272,7 @@ 'context': , 'entity_id': 'switch.ef_111111111112_moisture_sensor_2', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }), @@ -2170,6 +2285,7 @@ 'context': , 'entity_id': 'switch.ef_111111111112_moisture_sensor_3', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }), @@ -2346,6 +2462,7 @@ 'context': , 'entity_id': 'switch.ef_111111111113_hub_branch_0', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }), @@ -2358,6 +2475,7 @@ 'context': , 'entity_id': 'switch.ef_111111111113_hub_branch_1', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }), @@ -2370,6 +2488,7 @@ 'context': , 'entity_id': 'switch.ef_111111111113_hub_branch_2', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }), @@ -2382,6 +2501,7 @@ 'context': , 'entity_id': 'switch.ef_111111111113_hub_branch_3', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }), diff --git a/tests/components/onewire/test_binary_sensor.py b/tests/components/onewire/test_binary_sensor.py index 1e13da95651..26b1ed5aed7 100644 --- a/tests/components/onewire/test_binary_sensor.py +++ b/tests/components/onewire/test_binary_sensor.py @@ -1,4 +1,5 @@ """Tests for 1-Wire binary sensors.""" + from collections.abc import Generator from unittest.mock import MagicMock, patch @@ -49,7 +50,7 @@ async def test_binary_sensors( setup_owproxy_mock_devices(owproxy, Platform.BINARY_SENSOR, [device_id]) # Some entities are disabled, enable them and reload before checking states for ent in entity_entries: - entity_registry.async_update_entity(ent.entity_id, **{"disabled_by": None}) + entity_registry.async_update_entity(ent.entity_id, disabled_by=None) await hass.config_entries.async_reload(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/onewire/test_config_flow.py b/tests/components/onewire/test_config_flow.py index 27d8436f8c4..980ecb22d32 100644 --- a/tests/components/onewire/test_config_flow.py +++ b/tests/components/onewire/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for 1-Wire config flow.""" + from unittest.mock import AsyncMock, patch from pyownet import protocol diff --git a/tests/components/onewire/test_diagnostics.py b/tests/components/onewire/test_diagnostics.py index 4e108e41959..dd08e825221 100644 --- a/tests/components/onewire/test_diagnostics.py +++ b/tests/components/onewire/test_diagnostics.py @@ -1,4 +1,5 @@ """Test 1-Wire diagnostics.""" + from collections.abc import Generator from unittest.mock import MagicMock, patch diff --git a/tests/components/onewire/test_init.py b/tests/components/onewire/test_init.py index 01c1841d178..991277d8329 100644 --- a/tests/components/onewire/test_init.py +++ b/tests/components/onewire/test_init.py @@ -1,4 +1,5 @@ """Tests for 1-Wire config flow.""" + from copy import deepcopy from unittest.mock import MagicMock, patch diff --git a/tests/components/onewire/test_sensor.py b/tests/components/onewire/test_sensor.py index f4a993d6d96..848489c837f 100644 --- a/tests/components/onewire/test_sensor.py +++ b/tests/components/onewire/test_sensor.py @@ -1,4 +1,5 @@ """Tests for 1-Wire sensors.""" + from collections.abc import Generator from copy import deepcopy import logging @@ -53,7 +54,7 @@ async def test_sensors( setup_owproxy_mock_devices(owproxy, Platform.SENSOR, [device_id]) # Some entities are disabled, enable them and reload before checking states for ent in entity_entries: - entity_registry.async_update_entity(ent.entity_id, **{"disabled_by": None}) + entity_registry.async_update_entity(ent.entity_id, disabled_by=None) await hass.config_entries.async_reload(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/onewire/test_switch.py b/tests/components/onewire/test_switch.py index 5440cbee598..c6d84d38848 100644 --- a/tests/components/onewire/test_switch.py +++ b/tests/components/onewire/test_switch.py @@ -1,4 +1,5 @@ """Tests for 1-Wire switches.""" + from collections.abc import Generator from unittest.mock import MagicMock, patch @@ -56,7 +57,7 @@ async def test_switches( setup_owproxy_mock_devices(owproxy, Platform.SWITCH, [device_id]) # Some entities are disabled, enable them and reload before checking states for ent in entity_entries: - entity_registry.async_update_entity(ent.entity_id, **{"disabled_by": None}) + entity_registry.async_update_entity(ent.entity_id, disabled_by=None) await hass.config_entries.async_reload(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/onvif/__init__.py b/tests/components/onvif/__init__.py index ae48dd26220..1e7c3273ced 100644 --- a/tests/components/onvif/__init__.py +++ b/tests/components/onvif/__init__.py @@ -1,4 +1,5 @@ """Tests for the ONVIF integration.""" + from unittest.mock import AsyncMock, MagicMock, patch from onvif.exceptions import ONVIFError @@ -124,6 +125,7 @@ def setup_mock_onvif_camera( def setup_mock_device(mock_device, capabilities=None): """Prepare mock ONVIFDevice.""" mock_device.async_setup = AsyncMock(return_value=True) + mock_device.port = 80 mock_device.available = True mock_device.name = NAME mock_device.info = DeviceInfo( @@ -185,13 +187,15 @@ async def setup_onvif_integration( ) config_entry.add_to_hass(hass) - with patch( - "homeassistant.components.onvif.config_flow.get_device" - ) as mock_onvif_camera, patch( - "homeassistant.components.onvif.config_flow.wsdiscovery" - ) as mock_discovery, patch( - "homeassistant.components.onvif.ONVIFDevice" - ) as mock_device: + with ( + patch( + "homeassistant.components.onvif.config_flow.get_device" + ) as mock_onvif_camera, + patch( + "homeassistant.components.onvif.config_flow.wsdiscovery" + ) as mock_discovery, + patch("homeassistant.components.onvif.ONVIFDevice") as mock_device, + ): setup_mock_onvif_camera(mock_onvif_camera, two_profiles=True) # no discovery mock_discovery.return_value = [] diff --git a/tests/components/onvif/test_button.py b/tests/components/onvif/test_button.py index 4b30bc7bdd1..f8d51ae31a0 100644 --- a/tests/components/onvif/test_button.py +++ b/tests/components/onvif/test_button.py @@ -1,4 +1,5 @@ """Test button of ONVIF integration.""" + from unittest.mock import AsyncMock from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, ButtonDeviceClass diff --git a/tests/components/onvif/test_config_flow.py b/tests/components/onvif/test_config_flow.py index fcf0ea5c9dc..e59db13d3bb 100644 --- a/tests/components/onvif/test_config_flow.py +++ b/tests/components/onvif/test_config_flow.py @@ -1,4 +1,5 @@ """Test ONVIF config flow.""" + import logging from unittest.mock import MagicMock, patch @@ -109,13 +110,15 @@ async def test_flow_discovered_devices(hass: HomeAssistant) -> None: assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" - with patch( - "homeassistant.components.onvif.config_flow.get_device" - ) as mock_onvif_camera, patch( - "homeassistant.components.onvif.config_flow.WSDiscovery" - ) as mock_discovery, patch( - "homeassistant.components.onvif.ONVIFDevice" - ) as mock_device: + with ( + patch( + "homeassistant.components.onvif.config_flow.get_device" + ) as mock_onvif_camera, + patch( + "homeassistant.components.onvif.config_flow.WSDiscovery" + ) as mock_discovery, + patch("homeassistant.components.onvif.ONVIFDevice") as mock_device, + ): setup_mock_onvif_camera(mock_onvif_camera) setup_mock_discovery(mock_discovery) setup_mock_device(mock_device) @@ -180,13 +183,15 @@ async def test_flow_discovered_devices_ignore_configured_manual_input( assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" - with patch( - "homeassistant.components.onvif.config_flow.get_device" - ) as mock_onvif_camera, patch( - "homeassistant.components.onvif.config_flow.WSDiscovery" - ) as mock_discovery, patch( - "homeassistant.components.onvif.ONVIFDevice" - ) as mock_device: + with ( + patch( + "homeassistant.components.onvif.config_flow.get_device" + ) as mock_onvif_camera, + patch( + "homeassistant.components.onvif.config_flow.WSDiscovery" + ) as mock_discovery, + patch("homeassistant.components.onvif.ONVIFDevice") as mock_device, + ): setup_mock_onvif_camera(mock_onvif_camera) setup_mock_discovery(mock_discovery, with_mac=True) setup_mock_device(mock_device) @@ -219,13 +224,15 @@ async def test_flow_discovered_no_device(hass: HomeAssistant) -> None: assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" - with patch( - "homeassistant.components.onvif.config_flow.get_device" - ) as mock_onvif_camera, patch( - "homeassistant.components.onvif.config_flow.WSDiscovery" - ) as mock_discovery, patch( - "homeassistant.components.onvif.ONVIFDevice" - ) as mock_device: + with ( + patch( + "homeassistant.components.onvif.config_flow.get_device" + ) as mock_onvif_camera, + patch( + "homeassistant.components.onvif.config_flow.WSDiscovery" + ) as mock_discovery, + patch("homeassistant.components.onvif.ONVIFDevice") as mock_device, + ): setup_mock_onvif_camera(mock_onvif_camera) setup_mock_discovery(mock_discovery, no_devices=True) setup_mock_device(mock_device) @@ -262,13 +269,15 @@ async def test_flow_discovery_ignore_existing_and_abort(hass: HomeAssistant) -> assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" - with patch( - "homeassistant.components.onvif.config_flow.get_device" - ) as mock_onvif_camera, patch( - "homeassistant.components.onvif.config_flow.WSDiscovery" - ) as mock_discovery, patch( - "homeassistant.components.onvif.ONVIFDevice" - ) as mock_device: + with ( + patch( + "homeassistant.components.onvif.config_flow.get_device" + ) as mock_onvif_camera, + patch( + "homeassistant.components.onvif.config_flow.WSDiscovery" + ) as mock_discovery, + patch("homeassistant.components.onvif.ONVIFDevice") as mock_device, + ): setup_mock_onvif_camera(mock_onvif_camera) setup_mock_discovery(mock_discovery, with_name=True, with_mac=True) setup_mock_device(mock_device) @@ -306,13 +315,15 @@ async def test_flow_manual_entry(hass: HomeAssistant) -> None: assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" - with patch( - "homeassistant.components.onvif.config_flow.get_device" - ) as mock_onvif_camera, patch( - "homeassistant.components.onvif.config_flow.WSDiscovery" - ) as mock_discovery, patch( - "homeassistant.components.onvif.ONVIFDevice" - ) as mock_device: + with ( + patch( + "homeassistant.components.onvif.config_flow.get_device" + ) as mock_onvif_camera, + patch( + "homeassistant.components.onvif.config_flow.WSDiscovery" + ) as mock_discovery, + patch("homeassistant.components.onvif.ONVIFDevice") as mock_device, + ): setup_mock_onvif_camera(mock_onvif_camera, two_profiles=True) # no discovery mock_discovery.return_value = [] @@ -363,13 +374,15 @@ async def test_flow_manual_entry_no_profiles(hass: HomeAssistant) -> None: assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" - with patch( - "homeassistant.components.onvif.config_flow.get_device" - ) as mock_onvif_camera, patch( - "homeassistant.components.onvif.config_flow.WSDiscovery" - ) as mock_discovery, patch( - "homeassistant.components.onvif.ONVIFDevice" - ) as mock_device: + with ( + patch( + "homeassistant.components.onvif.config_flow.get_device" + ) as mock_onvif_camera, + patch( + "homeassistant.components.onvif.config_flow.WSDiscovery" + ) as mock_discovery, + patch("homeassistant.components.onvif.ONVIFDevice") as mock_device, + ): setup_mock_onvif_camera(mock_onvif_camera, no_profiles=True) # no discovery mock_discovery.return_value = [] @@ -403,13 +416,15 @@ async def test_flow_manual_entry_no_mac(hass: HomeAssistant) -> None: assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" - with patch( - "homeassistant.components.onvif.config_flow.get_device" - ) as mock_onvif_camera, patch( - "homeassistant.components.onvif.config_flow.WSDiscovery" - ) as mock_discovery, patch( - "homeassistant.components.onvif.ONVIFDevice" - ) as mock_device: + with ( + patch( + "homeassistant.components.onvif.config_flow.get_device" + ) as mock_onvif_camera, + patch( + "homeassistant.components.onvif.config_flow.WSDiscovery" + ) as mock_discovery, + patch("homeassistant.components.onvif.ONVIFDevice") as mock_device, + ): setup_mock_onvif_camera( mock_onvif_camera, with_serial=False, with_interfaces=False ) @@ -445,13 +460,15 @@ async def test_flow_manual_entry_fails(hass: HomeAssistant) -> None: assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" - with patch( - "homeassistant.components.onvif.config_flow.get_device" - ) as mock_onvif_camera, patch( - "homeassistant.components.onvif.config_flow.WSDiscovery" - ) as mock_discovery, patch( - "homeassistant.components.onvif.ONVIFDevice" - ) as mock_device: + with ( + patch( + "homeassistant.components.onvif.config_flow.get_device" + ) as mock_onvif_camera, + patch( + "homeassistant.components.onvif.config_flow.WSDiscovery" + ) as mock_discovery, + patch("homeassistant.components.onvif.ONVIFDevice") as mock_device, + ): setup_mock_onvif_camera( mock_onvif_camera, two_profiles=True, profiles_transient_failure=True ) @@ -553,13 +570,15 @@ async def test_flow_manual_entry_wrong_password(hass: HomeAssistant) -> None: assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" - with patch( - "homeassistant.components.onvif.config_flow.get_device" - ) as mock_onvif_camera, patch( - "homeassistant.components.onvif.config_flow.WSDiscovery" - ) as mock_discovery, patch( - "homeassistant.components.onvif.ONVIFDevice" - ) as mock_device: + with ( + patch( + "homeassistant.components.onvif.config_flow.get_device" + ) as mock_onvif_camera, + patch( + "homeassistant.components.onvif.config_flow.WSDiscovery" + ) as mock_discovery, + patch("homeassistant.components.onvif.ONVIFDevice") as mock_device, + ): setup_mock_onvif_camera(mock_onvif_camera, two_profiles=True, auth_fail=True) # no discovery mock_discovery.return_value = [] @@ -763,14 +782,16 @@ async def test_form_reauth(hass: HomeAssistant) -> None: == entry.data[CONF_USERNAME] ) - with patch( - "homeassistant.components.onvif.config_flow.get_device" - ) as mock_onvif_camera, patch( - "homeassistant.components.onvif.ONVIFDevice" - ) as mock_device, patch( - "homeassistant.components.onvif.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.onvif.config_flow.get_device" + ) as mock_onvif_camera, + patch("homeassistant.components.onvif.ONVIFDevice") as mock_device, + patch( + "homeassistant.components.onvif.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): setup_mock_onvif_camera(mock_onvif_camera, auth_failure=True) setup_mock_device(mock_device) @@ -790,14 +811,16 @@ async def test_form_reauth(hass: HomeAssistant) -> None: "error": "not authorized (subcodes:NotAuthorized)" } - with patch( - "homeassistant.components.onvif.config_flow.get_device" - ) as mock_onvif_camera, patch( - "homeassistant.components.onvif.ONVIFDevice" - ) as mock_device, patch( - "homeassistant.components.onvif.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.onvif.config_flow.get_device" + ) as mock_onvif_camera, + patch("homeassistant.components.onvif.ONVIFDevice") as mock_device, + patch( + "homeassistant.components.onvif.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): setup_mock_onvif_camera(mock_onvif_camera) setup_mock_device(mock_device) @@ -830,13 +853,15 @@ async def test_flow_manual_entry_updates_existing_user_password( assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" - with patch( - "homeassistant.components.onvif.config_flow.get_device" - ) as mock_onvif_camera, patch( - "homeassistant.components.onvif.config_flow.WSDiscovery" - ) as mock_discovery, patch( - "homeassistant.components.onvif.ONVIFDevice" - ) as mock_device: + with ( + patch( + "homeassistant.components.onvif.config_flow.get_device" + ) as mock_onvif_camera, + patch( + "homeassistant.components.onvif.config_flow.WSDiscovery" + ) as mock_discovery, + patch("homeassistant.components.onvif.ONVIFDevice") as mock_device, + ): setup_mock_onvif_camera(mock_onvif_camera, two_profiles=True) # no discovery mock_discovery.return_value = [] @@ -881,13 +906,15 @@ async def test_flow_manual_entry_wrong_port(hass: HomeAssistant) -> None: assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" - with patch( - "homeassistant.components.onvif.config_flow.get_device" - ) as mock_onvif_camera, patch( - "homeassistant.components.onvif.config_flow.WSDiscovery" - ) as mock_discovery, patch( - "homeassistant.components.onvif.ONVIFDevice" - ) as mock_device: + with ( + patch( + "homeassistant.components.onvif.config_flow.get_device" + ) as mock_onvif_camera, + patch( + "homeassistant.components.onvif.config_flow.WSDiscovery" + ) as mock_discovery, + patch("homeassistant.components.onvif.ONVIFDevice") as mock_device, + ): setup_mock_onvif_camera(mock_onvif_camera, wrong_port=True) # no discovery mock_discovery.return_value = [] diff --git a/tests/components/onvif/test_diagnostics.py b/tests/components/onvif/test_diagnostics.py index af7a68a6e0d..d58c8008ea6 100644 --- a/tests/components/onvif/test_diagnostics.py +++ b/tests/components/onvif/test_diagnostics.py @@ -1,4 +1,5 @@ """Test ONVIF diagnostics.""" + from syrupy import SnapshotAssertion from homeassistant.core import HomeAssistant diff --git a/tests/components/onvif/test_switch.py b/tests/components/onvif/test_switch.py index 1228f72ba22..0afa4ff4042 100644 --- a/tests/components/onvif/test_switch.py +++ b/tests/components/onvif/test_switch.py @@ -1,4 +1,5 @@ """Test switch platform of ONVIF integration.""" + from unittest.mock import AsyncMock from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN diff --git a/tests/components/open_meteo/conftest.py b/tests/components/open_meteo/conftest.py index 76bb3039a2f..466d593cd73 100644 --- a/tests/components/open_meteo/conftest.py +++ b/tests/components/open_meteo/conftest.py @@ -1,4 +1,5 @@ """Fixtures for the Open-Meteo integration tests.""" + from __future__ import annotations from collections.abc import Generator diff --git a/tests/components/open_meteo/test_init.py b/tests/components/open_meteo/test_init.py index c149dc2f6a6..19efcbec35c 100644 --- a/tests/components/open_meteo/test_init.py +++ b/tests/components/open_meteo/test_init.py @@ -1,4 +1,5 @@ """Tests for the Open-Meteo integration.""" + from unittest.mock import AsyncMock, MagicMock, patch from open_meteo import OpenMeteoConnectionError diff --git a/tests/components/openai_conversation/conftest.py b/tests/components/openai_conversation/conftest.py index a83c660e509..a8081c01c32 100644 --- a/tests/components/openai_conversation/conftest.py +++ b/tests/components/openai_conversation/conftest.py @@ -1,4 +1,5 @@ """Tests helpers.""" + from unittest.mock import patch import pytest diff --git a/tests/components/openai_conversation/test_config_flow.py b/tests/components/openai_conversation/test_config_flow.py index dd218e88c12..659b3825472 100644 --- a/tests/components/openai_conversation/test_config_flow.py +++ b/tests/components/openai_conversation/test_config_flow.py @@ -1,4 +1,5 @@ """Test the OpenAI Conversation config flow.""" + from unittest.mock import patch from httpx import Response @@ -32,12 +33,15 @@ async def test_form(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.FORM assert result["errors"] is None - with patch( - "homeassistant.components.openai_conversation.config_flow.openai.resources.models.AsyncModels.list", - ), patch( - "homeassistant.components.openai_conversation.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.openai_conversation.config_flow.openai.resources.models.AsyncModels.list", + ), + patch( + "homeassistant.components.openai_conversation.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { diff --git a/tests/components/openai_conversation/test_init.py b/tests/components/openai_conversation/test_init.py index d3a06cabeb3..3a8db2a71c0 100644 --- a/tests/components/openai_conversation/test_init.py +++ b/tests/components/openai_conversation/test_init.py @@ -1,4 +1,5 @@ """Tests for the OpenAI integration.""" + from unittest.mock import AsyncMock, patch from httpx import Response @@ -168,11 +169,14 @@ async def test_template_error( "prompt": "talk like a {% if True %}smarthome{% else %}pirate please.", }, ) - with patch( - "openai.resources.models.AsyncModels.list", - ), patch( - "openai.resources.chat.completions.AsyncCompletions.create", - new_callable=AsyncMock, + with ( + patch( + "openai.resources.models.AsyncModels.list", + ), + patch( + "openai.resources.chat.completions.AsyncCompletions.create", + new_callable=AsyncMock, + ), ): await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() @@ -313,12 +317,17 @@ async def test_generate_image_service_error( mock_config_entry: MockConfigEntry, ) -> None: """Test generate image service handles errors.""" - with patch( - "openai.resources.images.AsyncImages.generate", - side_effect=RateLimitError( - response=Response(status_code=None, request=""), body=None, message="Reason" + with ( + patch( + "openai.resources.images.AsyncImages.generate", + side_effect=RateLimitError( + response=Response(status_code=None, request=""), + body=None, + message="Reason", + ), ), - ), pytest.raises(HomeAssistantError, match="Error generating image: Reason"): + pytest.raises(HomeAssistantError, match="Error generating image: Reason"), + ): await hass.services.async_call( "openai_conversation", "generate_image", diff --git a/tests/fixtures/alpr_cloud.json b/tests/components/openalpr_cloud/fixtures/alpr_cloud.json similarity index 100% rename from tests/fixtures/alpr_cloud.json rename to tests/components/openalpr_cloud/fixtures/alpr_cloud.json diff --git a/tests/components/openalpr_cloud/test_image_processing.py b/tests/components/openalpr_cloud/test_image_processing.py index 700152f80aa..7115c3e7bf0 100644 --- a/tests/components/openalpr_cloud/test_image_processing.py +++ b/tests/components/openalpr_cloud/test_image_processing.py @@ -1,4 +1,5 @@ """The tests for the openalpr cloud platform.""" + from unittest.mock import PropertyMock, patch import pytest @@ -135,7 +136,7 @@ async def test_openalpr_process_image( aioclient_mock.post( OPENALPR_API_URL, params=PARAMS, - text=load_fixture("alpr_cloud.json"), + text=load_fixture("alpr_cloud.json", "openalpr_cloud"), status=200, ) diff --git a/tests/components/openerz/test_sensor.py b/tests/components/openerz/test_sensor.py index 06fa39363ce..6e65774bda4 100644 --- a/tests/components/openerz/test_sensor.py +++ b/tests/components/openerz/test_sensor.py @@ -1,4 +1,5 @@ """Tests for OpenERZ component.""" + from unittest.mock import MagicMock, patch from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN diff --git a/tests/components/openexchangerates/conftest.py b/tests/components/openexchangerates/conftest.py index a1512442fd1..5cb97e0cc53 100644 --- a/tests/components/openexchangerates/conftest.py +++ b/tests/components/openexchangerates/conftest.py @@ -1,4 +1,5 @@ """Provide common fixtures for tests.""" + from collections.abc import Generator from unittest.mock import AsyncMock, patch diff --git a/tests/components/openexchangerates/test_config_flow.py b/tests/components/openexchangerates/test_config_flow.py index 213badcab08..e0896f64340 100644 --- a/tests/components/openexchangerates/test_config_flow.py +++ b/tests/components/openexchangerates/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Open Exchange Rates config flow.""" + import asyncio from collections.abc import Generator from typing import Any diff --git a/tests/components/opengarage/conftest.py b/tests/components/opengarage/conftest.py index 189c3a877ff..24dc8134e4b 100644 --- a/tests/components/opengarage/conftest.py +++ b/tests/components/opengarage/conftest.py @@ -1,4 +1,5 @@ """Fixtures for the OpenGarage integration tests.""" + from __future__ import annotations from collections.abc import Generator diff --git a/tests/components/opengarage/test_button.py b/tests/components/opengarage/test_button.py index b4557a116e8..3742b7c8aec 100644 --- a/tests/components/opengarage/test_button.py +++ b/tests/components/opengarage/test_button.py @@ -1,4 +1,5 @@ """Test the OpenGarage Browser buttons.""" + from unittest.mock import MagicMock import homeassistant.components.button as button diff --git a/tests/components/opengarage/test_config_flow.py b/tests/components/opengarage/test_config_flow.py index 39cbb9c4b6b..7d3e44017b0 100644 --- a/tests/components/opengarage/test_config_flow.py +++ b/tests/components/opengarage/test_config_flow.py @@ -1,4 +1,5 @@ """Test the OpenGarage config flow.""" + from unittest.mock import patch import aiohttp @@ -20,13 +21,16 @@ async def test_form(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.FORM assert result["errors"] is None - with patch( - "opengarage.OpenGarage.update_state", - return_value={"name": "Name of the device", "mac": "unique"}, - ), patch( - "homeassistant.components.opengarage.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "opengarage.OpenGarage.update_state", + return_value={"name": "Name of the device", "mac": "unique"}, + ), + patch( + "homeassistant.components.opengarage.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "http://1.1.1.1", "device_key": "AfsasdnfkjDD"}, diff --git a/tests/fixtures/openhardwaremonitor.json b/tests/components/openhardwaremonitor/fixtures/openhardwaremonitor.json similarity index 100% rename from tests/fixtures/openhardwaremonitor.json rename to tests/components/openhardwaremonitor/fixtures/openhardwaremonitor.json diff --git a/tests/components/openhardwaremonitor/test_sensor.py b/tests/components/openhardwaremonitor/test_sensor.py index 5598abfabc3..944b5487a96 100644 --- a/tests/components/openhardwaremonitor/test_sensor.py +++ b/tests/components/openhardwaremonitor/test_sensor.py @@ -1,4 +1,5 @@ """The tests for the Open Hardware Monitor platform.""" + import requests_mock from homeassistant.core import HomeAssistant @@ -19,7 +20,7 @@ async def test_setup(hass: HomeAssistant, requests_mock: requests_mock.Mocker) - requests_mock.get( "http://localhost:8085/data.json", - text=load_fixture("openhardwaremonitor.json"), + text=load_fixture("openhardwaremonitor.json", "openhardwaremonitor"), ) await async_setup_component(hass, "sensor", config) diff --git a/tests/components/openhome/test_update.py b/tests/components/openhome/test_update.py index d975cc29af4..d3a328b9f9e 100644 --- a/tests/components/openhome/test_update.py +++ b/tests/components/openhome/test_update.py @@ -1,4 +1,5 @@ """Tests for the Openhome update platform.""" + from unittest.mock import AsyncMock, MagicMock, patch import pytest @@ -71,9 +72,10 @@ async def setup_integration( ) entry.add_to_hass(hass) - with patch("homeassistant.components.openhome.PLATFORMS", [Platform.UPDATE]), patch( - "homeassistant.components.openhome.Device", MagicMock() - ) as mock_device: + with ( + patch("homeassistant.components.openhome.PLATFORMS", [Platform.UPDATE]), + patch("homeassistant.components.openhome.Device", MagicMock()) as mock_device, + ): mock_device.return_value.init = AsyncMock() mock_device.return_value.uuid = MagicMock(return_value="uuid") mock_device.return_value.manufacturer = MagicMock(return_value="manufacturer") diff --git a/tests/components/opensky/__init__.py b/tests/components/opensky/__init__.py index 0f24f8931af..668416c6dcb 100644 --- a/tests/components/opensky/__init__.py +++ b/tests/components/opensky/__init__.py @@ -1,4 +1,5 @@ """Opensky tests.""" + from unittest.mock import patch from python_opensky import StatesResponse diff --git a/tests/components/opensky/conftest.py b/tests/components/opensky/conftest.py index 90e0b7251bf..835543b632f 100644 --- a/tests/components/opensky/conftest.py +++ b/tests/components/opensky/conftest.py @@ -1,4 +1,5 @@ """Configure tests for the OpenSky integration.""" + from collections.abc import Awaitable, Callable from unittest.mock import patch diff --git a/tests/components/opensky/snapshots/test_sensor.ambr b/tests/components/opensky/snapshots/test_sensor.ambr index a57b438df67..717927fb461 100644 --- a/tests/components/opensky/snapshots/test_sensor.ambr +++ b/tests/components/opensky/snapshots/test_sensor.ambr @@ -4,13 +4,13 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Information provided by the OpenSky Network (https://opensky-network.org)', 'friendly_name': 'OpenSky', - 'icon': 'mdi:airplane', 'state_class': , 'unit_of_measurement': 'flights', }), 'context': , 'entity_id': 'sensor.opensky', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '2', }) @@ -20,13 +20,13 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Information provided by the OpenSky Network (https://opensky-network.org)', 'friendly_name': 'OpenSky', - 'icon': 'mdi:airplane', 'state_class': , 'unit_of_measurement': 'flights', }), 'context': , 'entity_id': 'sensor.opensky', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '1', }) diff --git a/tests/components/opensky/test_config_flow.py b/tests/components/opensky/test_config_flow.py index 5207ac52f0c..c3ae876d36e 100644 --- a/tests/components/opensky/test_config_flow.py +++ b/tests/components/opensky/test_config_flow.py @@ -1,4 +1,5 @@ """Test OpenSky config flow.""" + from typing import Any from unittest.mock import patch @@ -105,9 +106,12 @@ async def test_options_flow_failures( assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" assert result["errors"]["base"] == error - with patch("python_opensky.OpenSky.authenticate"), patch( - "python_opensky.OpenSky.get_states", - return_value=get_states_response_fixture("opensky/states_1.json"), + with ( + patch("python_opensky.OpenSky.authenticate"), + patch( + "python_opensky.OpenSky.get_states", + return_value=get_states_response_fixture("opensky/states_1.json"), + ), ): result = await hass.config_entries.options.async_configure( result["flow_id"], @@ -139,9 +143,12 @@ async def test_options_flow( entry = hass.config_entries.async_entries(DOMAIN)[0] result = await hass.config_entries.options.async_init(entry.entry_id) await hass.async_block_till_done() - with patch("python_opensky.OpenSky.authenticate"), patch( - "python_opensky.OpenSky.get_states", - return_value=get_states_response_fixture("opensky/states_1.json"), + with ( + patch("python_opensky.OpenSky.authenticate"), + patch( + "python_opensky.OpenSky.get_states", + return_value=get_states_response_fixture("opensky/states_1.json"), + ), ): result = await hass.config_entries.options.async_configure( result["flow_id"], diff --git a/tests/components/opensky/test_init.py b/tests/components/opensky/test_init.py index 4c6cb9c3a33..a9e1668d026 100644 --- a/tests/components/opensky/test_init.py +++ b/tests/components/opensky/test_init.py @@ -1,4 +1,5 @@ """Test OpenSky component setup process.""" + from __future__ import annotations from unittest.mock import patch diff --git a/tests/components/opensky/test_sensor.py b/tests/components/opensky/test_sensor.py index 27c45d1b8ca..df4faaa3e4a 100644 --- a/tests/components/opensky/test_sensor.py +++ b/tests/components/opensky/test_sensor.py @@ -1,4 +1,5 @@ """OpenSky sensor tests.""" + from datetime import timedelta from unittest.mock import patch @@ -72,7 +73,7 @@ async def test_sensor_updating( async def skip_time_and_check_events() -> None: freezer.tick(timedelta(minutes=15)) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert events == snapshot diff --git a/tests/components/opentherm_gw/test_config_flow.py b/tests/components/opentherm_gw/test_config_flow.py index e071785006a..c92f23f46b4 100644 --- a/tests/components/opentherm_gw/test_config_flow.py +++ b/tests/components/opentherm_gw/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Opentherm Gateway config flow.""" + from unittest.mock import patch from pyotgw.vars import OTGW, OTGW_ABOUT @@ -36,18 +37,22 @@ async def test_form_user(hass: HomeAssistant) -> None: assert result["type"] == "form" assert result["errors"] == {} - with patch( - "homeassistant.components.opentherm_gw.async_setup", - return_value=True, - ) as mock_setup, patch( - "homeassistant.components.opentherm_gw.async_setup_entry", - return_value=True, - ) as mock_setup_entry, patch( - "pyotgw.OpenThermGateway.connect", return_value=MINIMAL_STATUS - ) as mock_pyotgw_connect, patch( - "pyotgw.OpenThermGateway.disconnect", return_value=None - ) as mock_pyotgw_disconnect, patch( - "pyotgw.status.StatusManager._process_updates", return_value=None + with ( + patch( + "homeassistant.components.opentherm_gw.async_setup", + return_value=True, + ) as mock_setup, + patch( + "homeassistant.components.opentherm_gw.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + patch( + "pyotgw.OpenThermGateway.connect", return_value=MINIMAL_STATUS + ) as mock_pyotgw_connect, + patch( + "pyotgw.OpenThermGateway.disconnect", return_value=None + ) as mock_pyotgw_disconnect, + patch("pyotgw.status.StatusManager._process_updates", return_value=None), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_NAME: "Test Entry 1", CONF_DEVICE: "/dev/ttyUSB0"} @@ -70,18 +75,22 @@ async def test_form_user(hass: HomeAssistant) -> None: async def test_form_import(hass: HomeAssistant) -> None: """Test import from existing config.""" - with patch( - "homeassistant.components.opentherm_gw.async_setup", - return_value=True, - ) as mock_setup, patch( - "homeassistant.components.opentherm_gw.async_setup_entry", - return_value=True, - ) as mock_setup_entry, patch( - "pyotgw.OpenThermGateway.connect", return_value=MINIMAL_STATUS - ) as mock_pyotgw_connect, patch( - "pyotgw.OpenThermGateway.disconnect", return_value=None - ) as mock_pyotgw_disconnect, patch( - "pyotgw.status.StatusManager._process_updates", return_value=None + with ( + patch( + "homeassistant.components.opentherm_gw.async_setup", + return_value=True, + ) as mock_setup, + patch( + "homeassistant.components.opentherm_gw.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + patch( + "pyotgw.OpenThermGateway.connect", return_value=MINIMAL_STATUS + ) as mock_pyotgw_connect, + patch( + "pyotgw.OpenThermGateway.disconnect", return_value=None + ) as mock_pyotgw_disconnect, + patch("pyotgw.status.StatusManager._process_updates", return_value=None), ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -114,18 +123,22 @@ async def test_form_duplicate_entries(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch( - "homeassistant.components.opentherm_gw.async_setup", - return_value=True, - ) as mock_setup, patch( - "homeassistant.components.opentherm_gw.async_setup_entry", - return_value=True, - ) as mock_setup_entry, patch( - "pyotgw.OpenThermGateway.connect", return_value=MINIMAL_STATUS - ) as mock_pyotgw_connect, patch( - "pyotgw.OpenThermGateway.disconnect", return_value=None - ) as mock_pyotgw_disconnect, patch( - "pyotgw.status.StatusManager._process_updates", return_value=None + with ( + patch( + "homeassistant.components.opentherm_gw.async_setup", + return_value=True, + ) as mock_setup, + patch( + "homeassistant.components.opentherm_gw.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + patch( + "pyotgw.OpenThermGateway.connect", return_value=MINIMAL_STATUS + ) as mock_pyotgw_connect, + patch( + "pyotgw.OpenThermGateway.disconnect", return_value=None + ) as mock_pyotgw_disconnect, + patch("pyotgw.status.StatusManager._process_updates", return_value=None), ): result1 = await hass.config_entries.flow.async_configure( flow1["flow_id"], {CONF_NAME: "Test Entry 1", CONF_DEVICE: "/dev/ttyUSB0"} @@ -153,10 +166,11 @@ async def test_form_connection_timeout(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch( - "pyotgw.OpenThermGateway.connect", side_effect=(TimeoutError) - ) as mock_connect, patch( - "pyotgw.status.StatusManager._process_updates", return_value=None + with ( + patch( + "pyotgw.OpenThermGateway.connect", side_effect=(TimeoutError) + ) as mock_connect, + patch("pyotgw.status.StatusManager._process_updates", return_value=None), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -174,10 +188,11 @@ async def test_form_connection_error(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch( - "pyotgw.OpenThermGateway.connect", side_effect=(SerialException) - ) as mock_connect, patch( - "pyotgw.status.StatusManager._process_updates", return_value=None + with ( + patch( + "pyotgw.OpenThermGateway.connect", side_effect=(SerialException) + ) as mock_connect, + patch("pyotgw.status.StatusManager._process_updates", return_value=None), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_NAME: "Test Entry 1", CONF_DEVICE: "/dev/ttyUSB0"} @@ -205,15 +220,19 @@ async def test_options_migration(hass: HomeAssistant) -> None: ) entry.add_to_hass(hass) - with patch( - "homeassistant.components.opentherm_gw.OpenThermGatewayDevice.connect_and_subscribe", - return_value=True, - ), patch( - "homeassistant.components.opentherm_gw.async_setup", - return_value=True, - ), patch( - "pyotgw.status.StatusManager._process_updates", - return_value=None, + with ( + patch( + "homeassistant.components.opentherm_gw.OpenThermGatewayDevice.connect_and_subscribe", + return_value=True, + ), + patch( + "homeassistant.components.opentherm_gw.async_setup", + return_value=True, + ), + patch( + "pyotgw.status.StatusManager._process_updates", + return_value=None, + ), ): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -250,10 +269,11 @@ async def test_options_form(hass: HomeAssistant) -> None: ) entry.add_to_hass(hass) - with patch( - "homeassistant.components.opentherm_gw.async_setup", return_value=True - ), patch( - "homeassistant.components.opentherm_gw.async_setup_entry", return_value=True + with ( + patch("homeassistant.components.opentherm_gw.async_setup", return_value=True), + patch( + "homeassistant.components.opentherm_gw.async_setup_entry", return_value=True + ), ): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/opentherm_gw/test_init.py b/tests/components/opentherm_gw/test_init.py index bd2c772bacb..77d43039c2b 100644 --- a/tests/components/opentherm_gw/test_init.py +++ b/tests/components/opentherm_gw/test_init.py @@ -1,4 +1,5 @@ """Test Opentherm Gateway init.""" + from unittest.mock import patch from pyotgw.vars import OTGW, OTGW_ABOUT @@ -35,10 +36,13 @@ async def test_device_registry_insert(hass: HomeAssistant) -> None: """Test that the device registry is initialized correctly.""" MOCK_CONFIG_ENTRY.add_to_hass(hass) - with patch( - "homeassistant.components.opentherm_gw.OpenThermGatewayDevice.cleanup", - return_value=None, - ), patch("pyotgw.OpenThermGateway.connect", return_value=MINIMAL_STATUS): + with ( + patch( + "homeassistant.components.opentherm_gw.OpenThermGatewayDevice.cleanup", + return_value=None, + ), + patch("pyotgw.OpenThermGateway.connect", return_value=MINIMAL_STATUS), + ): await setup.async_setup_component(hass, DOMAIN, {}) await hass.async_block_till_done() @@ -66,10 +70,13 @@ async def test_device_registry_update( sw_version=VERSION_OLD, ) - with patch( - "homeassistant.components.opentherm_gw.OpenThermGatewayDevice.cleanup", - return_value=None, - ), patch("pyotgw.OpenThermGateway.connect", return_value=MINIMAL_STATUS_UPD): + with ( + patch( + "homeassistant.components.opentherm_gw.OpenThermGatewayDevice.cleanup", + return_value=None, + ), + patch("pyotgw.OpenThermGateway.connect", return_value=MINIMAL_STATUS_UPD), + ): await setup.async_setup_component(hass, DOMAIN, {}) await hass.async_block_till_done() diff --git a/tests/components/openuv/conftest.py b/tests/components/openuv/conftest.py index 0f59c6279fb..5aad7d5b1a6 100644 --- a/tests/components/openuv/conftest.py +++ b/tests/components/openuv/conftest.py @@ -1,4 +1,5 @@ """Define test fixtures for OpenUV.""" + from collections.abc import Generator import json from unittest.mock import AsyncMock, Mock, patch @@ -78,9 +79,12 @@ def data_uv_index_fixture(): @pytest.fixture(name="mock_pyopenuv") async def mock_pyopenuv_fixture(client): """Define a fixture to patch pyopenuv.""" - with patch( - "homeassistant.components.openuv.config_flow.Client", return_value=client - ), patch("homeassistant.components.openuv.Client", return_value=client): + with ( + patch( + "homeassistant.components.openuv.config_flow.Client", return_value=client + ), + patch("homeassistant.components.openuv.Client", return_value=client), + ): yield diff --git a/tests/components/openuv/test_config_flow.py b/tests/components/openuv/test_config_flow.py index ddc7d3ce85d..db71b712fd9 100644 --- a/tests/components/openuv/test_config_flow.py +++ b/tests/components/openuv/test_config_flow.py @@ -1,4 +1,5 @@ """Define tests for the OpenUV config flow.""" + from unittest.mock import AsyncMock, patch from pyopenuv.errors import InvalidApiKeyError diff --git a/tests/components/openuv/test_diagnostics.py b/tests/components/openuv/test_diagnostics.py index e7efc459630..4b5114bccd1 100644 --- a/tests/components/openuv/test_diagnostics.py +++ b/tests/components/openuv/test_diagnostics.py @@ -1,4 +1,5 @@ """Test OpenUV diagnostics.""" + from homeassistant.components.diagnostics import REDACTED from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component diff --git a/tests/components/openweathermap/test_config_flow.py b/tests/components/openweathermap/test_config_flow.py index 87f76817044..77a10c5b26f 100644 --- a/tests/components/openweathermap/test_config_flow.py +++ b/tests/components/openweathermap/test_config_flow.py @@ -1,4 +1,5 @@ """Define tests for the OpenWeatherMap config flow.""" + from unittest.mock import MagicMock, patch from pyowm.commons.exceptions import APIRequestError, UnauthorizedError diff --git a/tests/components/opnsense/test_device_tracker.py b/tests/components/opnsense/test_device_tracker.py index 67301cfbcd6..6da01d8f44d 100644 --- a/tests/components/opnsense/test_device_tracker.py +++ b/tests/components/opnsense/test_device_tracker.py @@ -1,4 +1,5 @@ """The tests for the opnsense device tracker platform.""" + from unittest import mock import pytest diff --git a/tests/components/opower/conftest.py b/tests/components/opower/conftest.py index 0ee910f84f4..12d1a0dcdce 100644 --- a/tests/components/opower/conftest.py +++ b/tests/components/opower/conftest.py @@ -1,4 +1,5 @@ """Fixtures for the Opower integration tests.""" + import pytest from homeassistant.components.opower.const import DOMAIN diff --git a/tests/components/opower/test_config_flow.py b/tests/components/opower/test_config_flow.py index 0e96af200df..cb796d8e255 100644 --- a/tests/components/opower/test_config_flow.py +++ b/tests/components/opower/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Opower config flow.""" + from collections.abc import Generator from unittest.mock import AsyncMock, patch diff --git a/tests/components/oralb/test_config_flow.py b/tests/components/oralb/test_config_flow.py index 10f529fff70..197da4264d1 100644 --- a/tests/components/oralb/test_config_flow.py +++ b/tests/components/oralb/test_config_flow.py @@ -1,4 +1,5 @@ """Test the OralB config flow.""" + from unittest.mock import patch from homeassistant import config_entries diff --git a/tests/components/oralb/test_sensor.py b/tests/components/oralb/test_sensor.py index b1210712e7a..82f9b86b352 100644 --- a/tests/components/oralb/test_sensor.py +++ b/tests/components/oralb/test_sensor.py @@ -58,9 +58,12 @@ async def test_sensors( # Fastforward time without BLE advertisements monotonic_now = start_monotonic + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1 - with patch_bluetooth_time( - monotonic_now, - ), patch_all_discovered_devices([]): + with ( + patch_bluetooth_time( + monotonic_now, + ), + patch_all_discovered_devices([]), + ): async_fire_time_changed( hass, dt_util.utcnow() @@ -107,9 +110,12 @@ async def test_sensors_io_series_4( # Fast-forward time without BLE advertisements monotonic_now = start_monotonic + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1 - with patch_bluetooth_time( - monotonic_now, - ), patch_all_discovered_devices([]): + with ( + patch_bluetooth_time( + monotonic_now, + ), + patch_all_discovered_devices([]), + ): async_fire_time_changed( hass, dt_util.utcnow() diff --git a/tests/components/osoenergy/test_config_flow.py b/tests/components/osoenergy/test_config_flow.py index 5c7e0b3442c..c88035eef28 100644 --- a/tests/components/osoenergy/test_config_flow.py +++ b/tests/components/osoenergy/test_config_flow.py @@ -1,4 +1,5 @@ """Test the OSO Energy config flow.""" + from unittest.mock import patch from apyosoenergyapi.helper import osoenergy_exceptions @@ -25,12 +26,15 @@ async def test_user_flow(hass: HomeAssistant) -> None: assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} - with patch( - "homeassistant.components.osoenergy.config_flow.OSOEnergy.get_user_email", - return_value=TEST_USER_EMAIL, - ), patch( - "homeassistant.components.osoenergy.async_setup_entry", return_value=True - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.osoenergy.config_flow.OSOEnergy.get_user_email", + return_value=TEST_USER_EMAIL, + ), + patch( + "homeassistant.components.osoenergy.async_setup_entry", return_value=True + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_API_KEY: SUBSCRIPTION_KEY}, diff --git a/tests/components/otbr/__init__.py b/tests/components/otbr/__init__.py index c839cb0d06e..2c9daa127c2 100644 --- a/tests/components/otbr/__init__.py +++ b/tests/components/otbr/__init__.py @@ -1,4 +1,5 @@ """Tests for the Open Thread Border Router integration.""" + BASE_URL = "http://core-silabs-multiprotocol:8081" CONFIG_ENTRY_DATA_MULTIPAN = {"url": "http://core-silabs-multiprotocol:8081"} CONFIG_ENTRY_DATA_THREAD = {"url": "/dev/ttyAMA1"} diff --git a/tests/components/otbr/conftest.py b/tests/components/otbr/conftest.py index c03eef8dcb7..82f167cdd23 100644 --- a/tests/components/otbr/conftest.py +++ b/tests/components/otbr/conftest.py @@ -1,4 +1,5 @@ """Test fixtures for the Open Thread Border Router integration.""" + from unittest.mock import Mock, patch import pytest @@ -27,15 +28,19 @@ async def otbr_config_entry_multipan_fixture(hass): title="Open Thread Border Router", ) config_entry.add_to_hass(hass) - with patch( - "python_otbr_api.OTBR.get_active_dataset_tlvs", return_value=DATASET_CH16 - ), patch( - "python_otbr_api.OTBR.get_border_agent_id", return_value=TEST_BORDER_AGENT_ID - ), patch( - "python_otbr_api.OTBR.get_extended_address", - return_value=TEST_BORDER_AGENT_EXTENDED_ADDRESS, - ), patch( - "homeassistant.components.otbr.util.compute_pskc" + with ( + patch( + "python_otbr_api.OTBR.get_active_dataset_tlvs", return_value=DATASET_CH16 + ), + patch( + "python_otbr_api.OTBR.get_border_agent_id", + return_value=TEST_BORDER_AGENT_ID, + ), + patch( + "python_otbr_api.OTBR.get_extended_address", + return_value=TEST_BORDER_AGENT_EXTENDED_ADDRESS, + ), + patch("homeassistant.components.otbr.util.compute_pskc"), ): # Patch to speed up tests assert await hass.config_entries.async_setup(config_entry.entry_id) @@ -50,15 +55,19 @@ async def otbr_config_entry_thread_fixture(hass): title="Open Thread Border Router", ) config_entry.add_to_hass(hass) - with patch( - "python_otbr_api.OTBR.get_active_dataset_tlvs", return_value=DATASET_CH16 - ), patch( - "python_otbr_api.OTBR.get_border_agent_id", return_value=TEST_BORDER_AGENT_ID - ), patch( - "python_otbr_api.OTBR.get_extended_address", - return_value=TEST_BORDER_AGENT_EXTENDED_ADDRESS, - ), patch( - "homeassistant.components.otbr.util.compute_pskc" + with ( + patch( + "python_otbr_api.OTBR.get_active_dataset_tlvs", return_value=DATASET_CH16 + ), + patch( + "python_otbr_api.OTBR.get_border_agent_id", + return_value=TEST_BORDER_AGENT_ID, + ), + patch( + "python_otbr_api.OTBR.get_extended_address", + return_value=TEST_BORDER_AGENT_EXTENDED_ADDRESS, + ), + patch("homeassistant.components.otbr.util.compute_pskc"), ): # Patch to speed up tests assert await hass.config_entries.async_setup(config_entry.entry_id) diff --git a/tests/components/otbr/test_config_flow.py b/tests/components/otbr/test_config_flow.py index 03d54981b1a..81dcb894be6 100644 --- a/tests/components/otbr/test_config_flow.py +++ b/tests/components/otbr/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Open Thread Border Router config flow.""" + import asyncio from http import HTTPStatus from typing import Any @@ -104,13 +105,16 @@ async def test_user_flow_router_not_setup( assert result["type"] == FlowResultType.FORM assert result["errors"] == {} - with patch( - "homeassistant.components.otbr.config_flow.async_get_preferred_dataset", - return_value=None, - ), patch( - "homeassistant.components.otbr.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.otbr.config_flow.async_get_preferred_dataset", + return_value=None, + ), + patch( + "homeassistant.components.otbr.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -248,11 +252,12 @@ async def test_hassio_discovery_flow_yellow( "version": None, } - with patch( - "homeassistant.components.otbr.async_setup_entry", - return_value=True, - ) as mock_setup_entry, patch( - "homeassistant.components.otbr.config_flow.yellow_hardware.async_info" + with ( + patch( + "homeassistant.components.otbr.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + patch("homeassistant.components.otbr.config_flow.yellow_hardware.async_info"), ): result = await hass.config_entries.flow.async_init( otbr.DOMAIN, context={"source": "hassio"}, data=HASSIO_DATA @@ -275,8 +280,25 @@ async def test_hassio_discovery_flow_yellow( assert config_entry.unique_id == HASSIO_DATA.uuid +@pytest.mark.parametrize( + ("device", "title"), + [ + ( + "/dev/serial/by-id/usb-Nabu_Casa_SkyConnect_v1.0_9e2adbd75b8beb119fe564a0f320645d-if00-port0", + "Home Assistant SkyConnect (Silicon Labs Multiprotocol)", + ), + ( + "/dev/serial/by-id/usb-Nabu_Casa_Home_Assistant_Connect_ZBT-1_9e2adbd75b8beb119fe564a0f320645d-if00-port0", + "Home Assistant Connect ZBT-1 (Silicon Labs Multiprotocol)", + ), + ], +) async def test_hassio_discovery_flow_sky_connect( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, addon_info + device: str, + title: str, + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + addon_info, ) -> None: """Test the hassio discovery flow.""" url = "http://core-silabs-multiprotocol:8081" @@ -285,12 +307,7 @@ async def test_hassio_discovery_flow_sky_connect( addon_info.return_value = { "available": True, "hostname": None, - "options": { - "device": ( - "/dev/serial/by-id/usb-Nabu_Casa_SkyConnect_v1.0_" - "9e2adbd75b8beb119fe564a0f320645d-if00-port0" - ) - }, + "options": {"device": device}, "state": None, "update_available": False, "version": None, @@ -309,7 +326,7 @@ async def test_hassio_discovery_flow_sky_connect( } assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["title"] == "Home Assistant SkyConnect (Silicon Labs Multiprotocol)" + assert result["title"] == title assert result["data"] == expected_data assert result["options"] == {} assert len(mock_setup_entry.mock_calls) == 1 @@ -317,9 +334,7 @@ async def test_hassio_discovery_flow_sky_connect( config_entry = hass.config_entries.async_entries(otbr.DOMAIN)[0] assert config_entry.data == expected_data assert config_entry.options == {} - assert ( - config_entry.title == "Home Assistant SkyConnect (Silicon Labs Multiprotocol)" - ) + assert config_entry.title == title assert config_entry.unique_id == HASSIO_DATA.uuid @@ -413,13 +428,16 @@ async def test_hassio_discovery_flow_router_not_setup( aioclient_mock.put(f"{url}/node/dataset/active", status=HTTPStatus.CREATED) aioclient_mock.put(f"{url}/node/state", status=HTTPStatus.OK) - with patch( - "homeassistant.components.otbr.config_flow.async_get_preferred_dataset", - return_value=None, - ), patch( - "homeassistant.components.otbr.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.otbr.config_flow.async_get_preferred_dataset", + return_value=None, + ), + patch( + "homeassistant.components.otbr.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result = await hass.config_entries.flow.async_init( otbr.DOMAIN, context={"source": "hassio"}, data=HASSIO_DATA ) @@ -467,13 +485,16 @@ async def test_hassio_discovery_flow_router_not_setup_has_preferred( aioclient_mock.put(f"{url}/node/dataset/active", status=HTTPStatus.CREATED) aioclient_mock.put(f"{url}/node/state", status=HTTPStatus.OK) - with patch( - "homeassistant.components.otbr.config_flow.async_get_preferred_dataset", - return_value=DATASET_CH15.hex(), - ), patch( - "homeassistant.components.otbr.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.otbr.config_flow.async_get_preferred_dataset", + return_value=DATASET_CH15.hex(), + ), + patch( + "homeassistant.components.otbr.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result = await hass.config_entries.flow.async_init( otbr.DOMAIN, context={"source": "hassio"}, data=HASSIO_DATA ) @@ -522,13 +543,16 @@ async def test_hassio_discovery_flow_router_not_setup_has_preferred_2( multiprotocol_addon_manager_mock.async_get_channel.return_value = 15 - with patch( - "homeassistant.components.otbr.config_flow.async_get_preferred_dataset", - return_value=DATASET_CH16.hex(), - ), patch( - "homeassistant.components.otbr.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.otbr.config_flow.async_get_preferred_dataset", + return_value=DATASET_CH16.hex(), + ), + patch( + "homeassistant.components.otbr.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result = await hass.config_entries.flow.async_init( otbr.DOMAIN, context={"source": "hassio"}, data=HASSIO_DATA ) diff --git a/tests/components/otbr/test_init.py b/tests/components/otbr/test_init.py index 89a8f433016..7fd4ef6b016 100644 --- a/tests/components/otbr/test_init.py +++ b/tests/components/otbr/test_init.py @@ -1,4 +1,5 @@ """Test the Open Thread Border Router integration.""" + import asyncio from http import HTTPStatus from typing import Any @@ -63,16 +64,22 @@ async def test_import_dataset(hass: HomeAssistant, mock_async_zeroconf: None) -> ) config_entry.add_to_hass(hass) - with patch( - "python_otbr_api.OTBR.get_active_dataset_tlvs", return_value=DATASET_CH16 - ), patch( - "python_otbr_api.OTBR.get_border_agent_id", return_value=TEST_BORDER_AGENT_ID - ), patch( - "python_otbr_api.OTBR.get_extended_address", - return_value=TEST_BORDER_AGENT_EXTENDED_ADDRESS, - ), patch( - "homeassistant.components.thread.dataset_store.BORDER_AGENT_DISCOVERY_TIMEOUT", - 0.1, + with ( + patch( + "python_otbr_api.OTBR.get_active_dataset_tlvs", return_value=DATASET_CH16 + ), + patch( + "python_otbr_api.OTBR.get_border_agent_id", + return_value=TEST_BORDER_AGENT_ID, + ), + patch( + "python_otbr_api.OTBR.get_extended_address", + return_value=TEST_BORDER_AGENT_EXTENDED_ADDRESS, + ), + patch( + "homeassistant.components.thread.dataset_store.BORDER_AGENT_DISCOVERY_TIMEOUT", + 0.1, + ), ): assert await hass.config_entries.async_setup(config_entry.entry_id) @@ -134,16 +141,22 @@ async def test_import_share_radio_channel_collision( title="My OTBR", ) config_entry.add_to_hass(hass) - with patch( - "python_otbr_api.OTBR.get_active_dataset_tlvs", return_value=DATASET_CH16 - ), patch( - "python_otbr_api.OTBR.get_border_agent_id", return_value=TEST_BORDER_AGENT_ID - ), patch( - "python_otbr_api.OTBR.get_extended_address", - return_value=TEST_BORDER_AGENT_EXTENDED_ADDRESS, - ), patch( - "homeassistant.components.thread.dataset_store.DatasetStore.async_add" - ) as mock_add: + with ( + patch( + "python_otbr_api.OTBR.get_active_dataset_tlvs", return_value=DATASET_CH16 + ), + patch( + "python_otbr_api.OTBR.get_border_agent_id", + return_value=TEST_BORDER_AGENT_ID, + ), + patch( + "python_otbr_api.OTBR.get_extended_address", + return_value=TEST_BORDER_AGENT_EXTENDED_ADDRESS, + ), + patch( + "homeassistant.components.thread.dataset_store.DatasetStore.async_add" + ) as mock_add, + ): assert await hass.config_entries.async_setup(config_entry.entry_id) mock_add.assert_called_once_with( @@ -177,16 +190,20 @@ async def test_import_share_radio_no_channel_collision( title="My OTBR", ) config_entry.add_to_hass(hass) - with patch( - "python_otbr_api.OTBR.get_active_dataset_tlvs", return_value=dataset - ), patch( - "python_otbr_api.OTBR.get_border_agent_id", return_value=TEST_BORDER_AGENT_ID - ), patch( - "python_otbr_api.OTBR.get_extended_address", - return_value=TEST_BORDER_AGENT_EXTENDED_ADDRESS, - ), patch( - "homeassistant.components.thread.dataset_store.DatasetStore.async_add" - ) as mock_add: + with ( + patch("python_otbr_api.OTBR.get_active_dataset_tlvs", return_value=dataset), + patch( + "python_otbr_api.OTBR.get_border_agent_id", + return_value=TEST_BORDER_AGENT_ID, + ), + patch( + "python_otbr_api.OTBR.get_extended_address", + return_value=TEST_BORDER_AGENT_EXTENDED_ADDRESS, + ), + patch( + "homeassistant.components.thread.dataset_store.DatasetStore.async_add" + ) as mock_add, + ): assert await hass.config_entries.async_setup(config_entry.entry_id) mock_add.assert_called_once_with( @@ -218,16 +235,20 @@ async def test_import_insecure_dataset(hass: HomeAssistant, dataset: bytes) -> N title="My OTBR", ) config_entry.add_to_hass(hass) - with patch( - "python_otbr_api.OTBR.get_active_dataset_tlvs", return_value=dataset - ), patch( - "python_otbr_api.OTBR.get_border_agent_id", return_value=TEST_BORDER_AGENT_ID - ), patch( - "python_otbr_api.OTBR.get_extended_address", - return_value=TEST_BORDER_AGENT_EXTENDED_ADDRESS, - ), patch( - "homeassistant.components.thread.dataset_store.DatasetStore.async_add" - ) as mock_add: + with ( + patch("python_otbr_api.OTBR.get_active_dataset_tlvs", return_value=dataset), + patch( + "python_otbr_api.OTBR.get_border_agent_id", + return_value=TEST_BORDER_AGENT_ID, + ), + patch( + "python_otbr_api.OTBR.get_extended_address", + return_value=TEST_BORDER_AGENT_EXTENDED_ADDRESS, + ), + patch( + "homeassistant.components.thread.dataset_store.DatasetStore.async_add" + ) as mock_add, + ): assert await hass.config_entries.async_setup(config_entry.entry_id) mock_add.assert_called_once_with( @@ -273,11 +294,14 @@ async def test_border_agent_id_not_supported(hass: HomeAssistant) -> None: title="My OTBR", ) config_entry.add_to_hass(hass) - with patch( - "python_otbr_api.OTBR.get_active_dataset_tlvs", return_value=DATASET_CH16 - ), patch( - "python_otbr_api.OTBR.get_border_agent_id", - side_effect=python_otbr_api.GetBorderAgentIdNotSupportedError, + with ( + patch( + "python_otbr_api.OTBR.get_active_dataset_tlvs", return_value=DATASET_CH16 + ), + patch( + "python_otbr_api.OTBR.get_border_agent_id", + side_effect=python_otbr_api.GetBorderAgentIdNotSupportedError, + ), ): assert not await hass.config_entries.async_setup(config_entry.entry_id) @@ -411,10 +435,11 @@ async def test_remove_extra_entries( config_entry1.add_to_hass(hass) config_entry2.add_to_hass(hass) assert len(hass.config_entries.async_entries(otbr.DOMAIN)) == 2 - with patch( - "python_otbr_api.OTBR.get_active_dataset_tlvs", return_value=DATASET_CH16 - ), patch( - "homeassistant.components.otbr.util.compute_pskc" + with ( + patch( + "python_otbr_api.OTBR.get_active_dataset_tlvs", return_value=DATASET_CH16 + ), + patch("homeassistant.components.otbr.util.compute_pskc"), ): # Patch to speed up tests assert await async_setup_component(hass, otbr.DOMAIN, {}) assert len(hass.config_entries.async_entries(otbr.DOMAIN)) == 1 diff --git a/tests/components/otbr/test_silabs_multiprotocol.py b/tests/components/otbr/test_silabs_multiprotocol.py index 83416ae297d..8d7bed13df6 100644 --- a/tests/components/otbr/test_silabs_multiprotocol.py +++ b/tests/components/otbr/test_silabs_multiprotocol.py @@ -1,4 +1,5 @@ """Test OTBR Silicon Labs Multiprotocol support.""" + from unittest.mock import patch import pytest @@ -40,9 +41,12 @@ async def test_async_change_channel( assert len(store.datasets) == 1 assert list(store.datasets.values())[0].tlv == DATASET_CH16.hex() - with patch("python_otbr_api.OTBR.set_channel") as mock_set_channel, patch( - "python_otbr_api.OTBR.get_pending_dataset_tlvs", - return_value=bytes.fromhex(DATASET_CH16_PENDING), + with ( + patch("python_otbr_api.OTBR.set_channel") as mock_set_channel, + patch( + "python_otbr_api.OTBR.get_pending_dataset_tlvs", + return_value=bytes.fromhex(DATASET_CH16_PENDING), + ), ): await otbr_silabs_multiprotocol.async_change_channel(hass, 15, delay=5 * 300) mock_set_channel.assert_awaited_once_with(15, delay=5 * 300 * 1000) @@ -65,12 +69,16 @@ async def test_async_change_channel_no_pending( assert len(store.datasets) == 1 assert list(store.datasets.values())[0].tlv == DATASET_CH16.hex() - with patch("python_otbr_api.OTBR.set_channel") as mock_set_channel, patch( - "python_otbr_api.OTBR.get_active_dataset_tlvs", - return_value=bytes.fromhex(DATASET_CH16_PENDING), - ), patch( - "python_otbr_api.OTBR.get_pending_dataset_tlvs", - return_value=None, + with ( + patch("python_otbr_api.OTBR.set_channel") as mock_set_channel, + patch( + "python_otbr_api.OTBR.get_active_dataset_tlvs", + return_value=bytes.fromhex(DATASET_CH16_PENDING), + ), + patch( + "python_otbr_api.OTBR.get_pending_dataset_tlvs", + return_value=None, + ), ): await otbr_silabs_multiprotocol.async_change_channel(hass, 15, delay=5 * 300) mock_set_channel.assert_awaited_once_with(15, delay=5 * 300 * 1000) @@ -93,12 +101,16 @@ async def test_async_change_channel_no_update( assert len(store.datasets) == 1 assert list(store.datasets.values())[0].tlv == DATASET_CH16.hex() - with patch("python_otbr_api.OTBR.set_channel") as mock_set_channel, patch( - "python_otbr_api.OTBR.get_active_dataset_tlvs", - return_value=None, - ), patch( - "python_otbr_api.OTBR.get_pending_dataset_tlvs", - return_value=None, + with ( + patch("python_otbr_api.OTBR.set_channel") as mock_set_channel, + patch( + "python_otbr_api.OTBR.get_active_dataset_tlvs", + return_value=None, + ), + patch( + "python_otbr_api.OTBR.get_pending_dataset_tlvs", + return_value=None, + ), ): await otbr_silabs_multiprotocol.async_change_channel(hass, 15, delay=5 * 300) mock_set_channel.assert_awaited_once_with(15, delay=5 * 300 * 1000) diff --git a/tests/components/otbr/test_util.py b/tests/components/otbr/test_util.py index 941c80a52da..3b1edcfeb5b 100644 --- a/tests/components/otbr/test_util.py +++ b/tests/components/otbr/test_util.py @@ -1,4 +1,5 @@ """Test OTBR Utility functions.""" + from unittest.mock import patch import pytest @@ -34,9 +35,12 @@ async def test_factory_reset(hass: HomeAssistant, otbr_config_entry_multipan) -> """Test factory_reset.""" data: otbr.OTBRData = hass.data[otbr.DOMAIN] - with patch("python_otbr_api.OTBR.factory_reset") as factory_reset_mock, patch( - "python_otbr_api.OTBR.delete_active_dataset" - ) as delete_active_dataset_mock: + with ( + patch("python_otbr_api.OTBR.factory_reset") as factory_reset_mock, + patch( + "python_otbr_api.OTBR.delete_active_dataset" + ) as delete_active_dataset_mock, + ): await data.factory_reset() delete_active_dataset_mock.assert_not_called() @@ -49,12 +53,15 @@ async def test_factory_reset_not_supported( """Test factory_reset.""" data: otbr.OTBRData = hass.data[otbr.DOMAIN] - with patch( - "python_otbr_api.OTBR.factory_reset", - side_effect=python_otbr_api.FactoryResetNotSupportedError, - ) as factory_reset_mock, patch( - "python_otbr_api.OTBR.delete_active_dataset" - ) as delete_active_dataset_mock: + with ( + patch( + "python_otbr_api.OTBR.factory_reset", + side_effect=python_otbr_api.FactoryResetNotSupportedError, + ) as factory_reset_mock, + patch( + "python_otbr_api.OTBR.delete_active_dataset" + ) as delete_active_dataset_mock, + ): await data.factory_reset() delete_active_dataset_mock.assert_called_once_with() @@ -67,13 +74,17 @@ async def test_factory_reset_error_1( """Test factory_reset.""" data: otbr.OTBRData = hass.data[otbr.DOMAIN] - with patch( - "python_otbr_api.OTBR.factory_reset", - side_effect=python_otbr_api.OTBRError, - ) as factory_reset_mock, patch( - "python_otbr_api.OTBR.delete_active_dataset" - ) as delete_active_dataset_mock, pytest.raises( - HomeAssistantError, + with ( + patch( + "python_otbr_api.OTBR.factory_reset", + side_effect=python_otbr_api.OTBRError, + ) as factory_reset_mock, + patch( + "python_otbr_api.OTBR.delete_active_dataset" + ) as delete_active_dataset_mock, + pytest.raises( + HomeAssistantError, + ), ): await data.factory_reset() @@ -87,14 +98,18 @@ async def test_factory_reset_error_2( """Test factory_reset.""" data: otbr.OTBRData = hass.data[otbr.DOMAIN] - with patch( - "python_otbr_api.OTBR.factory_reset", - side_effect=python_otbr_api.FactoryResetNotSupportedError, - ) as factory_reset_mock, patch( - "python_otbr_api.OTBR.delete_active_dataset", - side_effect=python_otbr_api.OTBRError, - ) as delete_active_dataset_mock, pytest.raises( - HomeAssistantError, + with ( + patch( + "python_otbr_api.OTBR.factory_reset", + side_effect=python_otbr_api.FactoryResetNotSupportedError, + ) as factory_reset_mock, + patch( + "python_otbr_api.OTBR.delete_active_dataset", + side_effect=python_otbr_api.OTBRError, + ) as delete_active_dataset_mock, + pytest.raises( + HomeAssistantError, + ), ): await data.factory_reset() diff --git a/tests/components/otbr/test_websocket_api.py b/tests/components/otbr/test_websocket_api.py index 52aa792b814..c8ac839f629 100644 --- a/tests/components/otbr/test_websocket_api.py +++ b/tests/components/otbr/test_websocket_api.py @@ -1,4 +1,5 @@ """Test OTBR Websocket API.""" + from unittest.mock import patch import pytest @@ -34,16 +35,22 @@ async def test_get_info( ) -> None: """Test async_get_info.""" - with patch( - "python_otbr_api.OTBR.get_active_dataset", - return_value=python_otbr_api.ActiveDataSet(channel=16), - ), patch( - "python_otbr_api.OTBR.get_active_dataset_tlvs", return_value=DATASET_CH16 - ), patch( - "python_otbr_api.OTBR.get_border_agent_id", return_value=TEST_BORDER_AGENT_ID - ), patch( - "python_otbr_api.OTBR.get_extended_address", - return_value=TEST_BORDER_AGENT_EXTENDED_ADDRESS, + with ( + patch( + "python_otbr_api.OTBR.get_active_dataset", + return_value=python_otbr_api.ActiveDataSet(channel=16), + ), + patch( + "python_otbr_api.OTBR.get_active_dataset_tlvs", return_value=DATASET_CH16 + ), + patch( + "python_otbr_api.OTBR.get_border_agent_id", + return_value=TEST_BORDER_AGENT_ID, + ), + patch( + "python_otbr_api.OTBR.get_extended_address", + return_value=TEST_BORDER_AGENT_EXTENDED_ADDRESS, + ), ): await websocket_client.send_json_auto_id({"type": "otbr/info"}) msg = await websocket_client.receive_json() @@ -80,11 +87,15 @@ async def test_get_info_fetch_fails( websocket_client, ) -> None: """Test async_get_info.""" - with patch( - "python_otbr_api.OTBR.get_active_dataset", - side_effect=python_otbr_api.OTBRError, - ), patch( - "python_otbr_api.OTBR.get_border_agent_id", return_value=TEST_BORDER_AGENT_ID + with ( + patch( + "python_otbr_api.OTBR.get_active_dataset", + side_effect=python_otbr_api.OTBRError, + ), + patch( + "python_otbr_api.OTBR.get_border_agent_id", + return_value=TEST_BORDER_AGENT_ID, + ), ): await websocket_client.send_json_auto_id({"type": "otbr/info"}) msg = await websocket_client.receive_json() @@ -101,19 +112,20 @@ async def test_create_network( ) -> None: """Test create network.""" - with patch( - "python_otbr_api.OTBR.create_active_dataset" - ) as create_dataset_mock, patch( - "python_otbr_api.OTBR.factory_reset" - ) as factory_reset_mock, patch( - "python_otbr_api.OTBR.set_enabled" - ) as set_enabled_mock, patch( - "python_otbr_api.OTBR.get_active_dataset_tlvs", return_value=DATASET_CH16 - ) as get_active_dataset_tlvs_mock, patch( - "homeassistant.components.thread.dataset_store.DatasetStore.async_add" - ) as mock_add, patch( - "homeassistant.components.otbr.util.random.randint", - return_value=0x1234, + with ( + patch("python_otbr_api.OTBR.create_active_dataset") as create_dataset_mock, + patch("python_otbr_api.OTBR.factory_reset") as factory_reset_mock, + patch("python_otbr_api.OTBR.set_enabled") as set_enabled_mock, + patch( + "python_otbr_api.OTBR.get_active_dataset_tlvs", return_value=DATASET_CH16 + ) as get_active_dataset_tlvs_mock, + patch( + "homeassistant.components.thread.dataset_store.DatasetStore.async_add" + ) as mock_add, + patch( + "homeassistant.components.otbr.util.random.randint", + return_value=0x1234, + ), ): await websocket_client.send_json_auto_id({"type": "otbr/create_network"}) @@ -174,12 +186,16 @@ async def test_create_network_fails_2( websocket_client, ) -> None: """Test create network.""" - with patch( - "python_otbr_api.OTBR.set_enabled", - ), patch( - "python_otbr_api.OTBR.create_active_dataset", - side_effect=python_otbr_api.OTBRError, - ), patch("python_otbr_api.OTBR.factory_reset"): + with ( + patch( + "python_otbr_api.OTBR.set_enabled", + ), + patch( + "python_otbr_api.OTBR.create_active_dataset", + side_effect=python_otbr_api.OTBRError, + ), + patch("python_otbr_api.OTBR.factory_reset"), + ): await websocket_client.send_json_auto_id({"type": "otbr/create_network"}) msg = await websocket_client.receive_json() @@ -194,13 +210,17 @@ async def test_create_network_fails_3( websocket_client, ) -> None: """Test create network.""" - with patch( - "python_otbr_api.OTBR.set_enabled", - side_effect=[None, python_otbr_api.OTBRError], - ), patch( - "python_otbr_api.OTBR.create_active_dataset", - ), patch( - "python_otbr_api.OTBR.factory_reset", + with ( + patch( + "python_otbr_api.OTBR.set_enabled", + side_effect=[None, python_otbr_api.OTBRError], + ), + patch( + "python_otbr_api.OTBR.create_active_dataset", + ), + patch( + "python_otbr_api.OTBR.factory_reset", + ), ): await websocket_client.send_json_auto_id({"type": "otbr/create_network"}) msg = await websocket_client.receive_json() @@ -216,13 +236,16 @@ async def test_create_network_fails_4( websocket_client, ) -> None: """Test create network.""" - with patch("python_otbr_api.OTBR.set_enabled"), patch( - "python_otbr_api.OTBR.create_active_dataset" - ), patch( - "python_otbr_api.OTBR.get_active_dataset_tlvs", - side_effect=python_otbr_api.OTBRError, - ), patch( - "python_otbr_api.OTBR.factory_reset", + with ( + patch("python_otbr_api.OTBR.set_enabled"), + patch("python_otbr_api.OTBR.create_active_dataset"), + patch( + "python_otbr_api.OTBR.get_active_dataset_tlvs", + side_effect=python_otbr_api.OTBRError, + ), + patch( + "python_otbr_api.OTBR.factory_reset", + ), ): await websocket_client.send_json_auto_id({"type": "otbr/create_network"}) msg = await websocket_client.receive_json() @@ -238,10 +261,11 @@ async def test_create_network_fails_5( websocket_client, ) -> None: """Test create network.""" - with patch("python_otbr_api.OTBR.set_enabled"), patch( - "python_otbr_api.OTBR.create_active_dataset" - ), patch("python_otbr_api.OTBR.get_active_dataset_tlvs", return_value=None), patch( - "python_otbr_api.OTBR.factory_reset" + with ( + patch("python_otbr_api.OTBR.set_enabled"), + patch("python_otbr_api.OTBR.create_active_dataset"), + patch("python_otbr_api.OTBR.get_active_dataset_tlvs", return_value=None), + patch("python_otbr_api.OTBR.factory_reset"), ): await websocket_client.send_json_auto_id({"type": "otbr/create_network"}) msg = await websocket_client.receive_json() @@ -257,11 +281,14 @@ async def test_create_network_fails_6( websocket_client, ) -> None: """Test create network.""" - with patch("python_otbr_api.OTBR.set_enabled"), patch( - "python_otbr_api.OTBR.create_active_dataset" - ), patch("python_otbr_api.OTBR.get_active_dataset_tlvs", return_value=None), patch( - "python_otbr_api.OTBR.factory_reset", - side_effect=python_otbr_api.OTBRError, + with ( + patch("python_otbr_api.OTBR.set_enabled"), + patch("python_otbr_api.OTBR.create_active_dataset"), + patch("python_otbr_api.OTBR.get_active_dataset_tlvs", return_value=None), + patch( + "python_otbr_api.OTBR.factory_reset", + side_effect=python_otbr_api.OTBRError, + ), ): await websocket_client.send_json_auto_id({"type": "otbr/create_network"}) msg = await websocket_client.receive_json() @@ -282,11 +309,12 @@ async def test_set_network( dataset_store = await thread.dataset_store.async_get_store(hass) dataset_id = list(dataset_store.datasets)[1] - with patch( - "python_otbr_api.OTBR.set_active_dataset_tlvs" - ) as set_active_dataset_tlvs_mock, patch( - "python_otbr_api.OTBR.set_enabled" - ) as set_enabled_mock: + with ( + patch( + "python_otbr_api.OTBR.set_active_dataset_tlvs" + ) as set_active_dataset_tlvs_mock, + patch("python_otbr_api.OTBR.set_enabled") as set_enabled_mock, + ): await websocket_client.send_json_auto_id( { "type": "otbr/set_network", @@ -410,11 +438,14 @@ async def test_set_network_fails_2( dataset_store = await thread.dataset_store.async_get_store(hass) dataset_id = list(dataset_store.datasets)[1] - with patch( - "python_otbr_api.OTBR.set_enabled", - ), patch( - "python_otbr_api.OTBR.set_active_dataset_tlvs", - side_effect=python_otbr_api.OTBRError, + with ( + patch( + "python_otbr_api.OTBR.set_enabled", + ), + patch( + "python_otbr_api.OTBR.set_active_dataset_tlvs", + side_effect=python_otbr_api.OTBRError, + ), ): await websocket_client.send_json_auto_id( { @@ -439,11 +470,14 @@ async def test_set_network_fails_3( dataset_store = await thread.dataset_store.async_get_store(hass) dataset_id = list(dataset_store.datasets)[1] - with patch( - "python_otbr_api.OTBR.set_enabled", - side_effect=[None, python_otbr_api.OTBRError], - ), patch( - "python_otbr_api.OTBR.set_active_dataset_tlvs", + with ( + patch( + "python_otbr_api.OTBR.set_enabled", + side_effect=[None, python_otbr_api.OTBRError], + ), + patch( + "python_otbr_api.OTBR.set_active_dataset_tlvs", + ), ): await websocket_client.send_json_auto_id( { diff --git a/tests/components/ourgroceries/conftest.py b/tests/components/ourgroceries/conftest.py index c5fdec3ecb7..00aab0df834 100644 --- a/tests/components/ourgroceries/conftest.py +++ b/tests/components/ourgroceries/conftest.py @@ -1,4 +1,5 @@ """Common fixtures for the OurGroceries tests.""" + from collections.abc import Generator from unittest.mock import AsyncMock, patch diff --git a/tests/components/ourgroceries/test_config_flow.py b/tests/components/ourgroceries/test_config_flow.py index 78504e1fb7a..0eb17cd4ff6 100644 --- a/tests/components/ourgroceries/test_config_flow.py +++ b/tests/components/ourgroceries/test_config_flow.py @@ -1,4 +1,5 @@ """Test the OurGroceries config flow.""" + from unittest.mock import AsyncMock, patch import pytest diff --git a/tests/components/ourgroceries/test_init.py b/tests/components/ourgroceries/test_init.py index 43905c4fcf9..ae8452652ae 100644 --- a/tests/components/ourgroceries/test_init.py +++ b/tests/components/ourgroceries/test_init.py @@ -1,4 +1,5 @@ """Unit tests for the OurGroceries integration.""" + from unittest.mock import AsyncMock import pytest diff --git a/tests/components/ourgroceries/test_todo.py b/tests/components/ourgroceries/test_todo.py index 8ede2a40cc8..672e2e14447 100644 --- a/tests/components/ourgroceries/test_todo.py +++ b/tests/components/ourgroceries/test_todo.py @@ -1,4 +1,5 @@ """Unit tests for the OurGroceries todo platform.""" + from unittest.mock import AsyncMock from aiohttp import ClientError @@ -274,7 +275,7 @@ async def test_coordinator_error( ourgroceries.get_list_items.side_effect = exception freezer.tick(SCAN_INTERVAL) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get("todo.test_list") assert state.state == STATE_UNAVAILABLE diff --git a/tests/components/overkiz/__init__.py b/tests/components/overkiz/__init__.py index 407527b619e..e7d729ea41c 100644 --- a/tests/components/overkiz/__init__.py +++ b/tests/components/overkiz/__init__.py @@ -1,4 +1,5 @@ """Tests for the overkiz component.""" + import humps from pyoverkiz.models import Setup diff --git a/tests/components/overkiz/conftest.py b/tests/components/overkiz/conftest.py index da6d3a60839..d1da5d89134 100644 --- a/tests/components/overkiz/conftest.py +++ b/tests/components/overkiz/conftest.py @@ -1,4 +1,5 @@ """Configuration for overkiz tests.""" + from collections.abc import Generator from unittest.mock import AsyncMock, Mock, patch diff --git a/tests/components/overkiz/test_config_flow.py b/tests/components/overkiz/test_config_flow.py index 9b7b9f38287..dbe8c690bc4 100644 --- a/tests/components/overkiz/test_config_flow.py +++ b/tests/components/overkiz/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for Overkiz config flow.""" + from __future__ import annotations from ipaddress import ip_address @@ -95,9 +96,12 @@ async def test_form_cloud(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> N assert result3["type"] == "form" assert result3["step_id"] == "cloud" - with patch("pyoverkiz.client.OverkizClient.login", return_value=True), patch( - "pyoverkiz.client.OverkizClient.get_gateways", - return_value=MOCK_GATEWAY_RESPONSE, + with ( + patch("pyoverkiz.client.OverkizClient.login", return_value=True), + patch( + "pyoverkiz.client.OverkizClient.get_gateways", + return_value=MOCK_GATEWAY_RESPONSE, + ), ): await hass.config_entries.flow.async_configure( result["flow_id"], @@ -127,9 +131,12 @@ async def test_form_only_cloud_supported( assert result2["type"] == "form" assert result2["step_id"] == "cloud" - with patch("pyoverkiz.client.OverkizClient.login", return_value=True), patch( - "pyoverkiz.client.OverkizClient.get_gateways", - return_value=MOCK_GATEWAY_RESPONSE, + with ( + patch("pyoverkiz.client.OverkizClient.login", return_value=True), + patch( + "pyoverkiz.client.OverkizClient.get_gateways", + return_value=MOCK_GATEWAY_RESPONSE, + ), ): await hass.config_entries.flow.async_configure( result["flow_id"], @@ -417,9 +424,12 @@ async def test_cloud_abort_on_duplicate_entry( assert result3["type"] == "form" assert result3["step_id"] == "cloud" - with patch("pyoverkiz.client.OverkizClient.login", return_value=True), patch( - "pyoverkiz.client.OverkizClient.get_gateways", - return_value=MOCK_GATEWAY_RESPONSE, + with ( + patch("pyoverkiz.client.OverkizClient.login", return_value=True), + patch( + "pyoverkiz.client.OverkizClient.get_gateways", + return_value=MOCK_GATEWAY_RESPONSE, + ), ): result4 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -524,9 +534,12 @@ async def test_cloud_allow_multiple_unique_entries( assert result3["type"] == "form" assert result3["step_id"] == "cloud" - with patch("pyoverkiz.client.OverkizClient.login", return_value=True), patch( - "pyoverkiz.client.OverkizClient.get_gateways", - return_value=MOCK_GATEWAY_RESPONSE, + with ( + patch("pyoverkiz.client.OverkizClient.login", return_value=True), + patch( + "pyoverkiz.client.OverkizClient.get_gateways", + return_value=MOCK_GATEWAY_RESPONSE, + ), ): result4 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -572,9 +585,12 @@ async def test_cloud_reauth_success(hass: HomeAssistant) -> None: assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "cloud" - with patch("pyoverkiz.client.OverkizClient.login", return_value=True), patch( - "pyoverkiz.client.OverkizClient.get_gateways", - return_value=MOCK_GATEWAY_RESPONSE, + with ( + patch("pyoverkiz.client.OverkizClient.login", return_value=True), + patch( + "pyoverkiz.client.OverkizClient.get_gateways", + return_value=MOCK_GATEWAY_RESPONSE, + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -619,9 +635,12 @@ async def test_cloud_reauth_wrong_account(hass: HomeAssistant) -> None: assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "cloud" - with patch("pyoverkiz.client.OverkizClient.login", return_value=True), patch( - "pyoverkiz.client.OverkizClient.get_gateways", - return_value=MOCK_GATEWAY2_RESPONSE, + with ( + patch("pyoverkiz.client.OverkizClient.login", return_value=True), + patch( + "pyoverkiz.client.OverkizClient.get_gateways", + return_value=MOCK_GATEWAY2_RESPONSE, + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -778,8 +797,9 @@ async def test_dhcp_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> No {"api_type": "cloud"}, ) - with patch("pyoverkiz.client.OverkizClient.login", return_value=True), patch( - "pyoverkiz.client.OverkizClient.get_gateways", return_value=None + with ( + patch("pyoverkiz.client.OverkizClient.login", return_value=True), + patch("pyoverkiz.client.OverkizClient.get_gateways", return_value=None), ): result4 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -851,9 +871,12 @@ async def test_zeroconf_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock) - assert result3["type"] == "form" assert result3["step_id"] == "cloud" - with patch("pyoverkiz.client.OverkizClient.login", return_value=True), patch( - "pyoverkiz.client.OverkizClient.get_gateways", - return_value=MOCK_GATEWAY_RESPONSE, + with ( + patch("pyoverkiz.client.OverkizClient.login", return_value=True), + patch( + "pyoverkiz.client.OverkizClient.get_gateways", + return_value=MOCK_GATEWAY_RESPONSE, + ), ): result4 = await hass.config_entries.flow.async_configure( result["flow_id"], diff --git a/tests/components/overkiz/test_diagnostics.py b/tests/components/overkiz/test_diagnostics.py index 6d0498c237b..672370c2667 100644 --- a/tests/components/overkiz/test_diagnostics.py +++ b/tests/components/overkiz/test_diagnostics.py @@ -1,4 +1,5 @@ """Tests for the diagnostics data provided by the Overkiz integration.""" + from unittest.mock import AsyncMock, patch from syrupy import SnapshotAssertion diff --git a/tests/components/overkiz/test_init.py b/tests/components/overkiz/test_init.py index ddecee7c167..ba4de56ad86 100644 --- a/tests/components/overkiz/test_init.py +++ b/tests/components/overkiz/test_init.py @@ -1,4 +1,5 @@ """Tests for Overkiz integration init.""" + from homeassistant.components.overkiz.const import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er diff --git a/tests/components/ovo_energy/test_config_flow.py b/tests/components/ovo_energy/test_config_flow.py index 1e9a0672473..3975be7cf80 100644 --- a/tests/components/ovo_energy/test_config_flow.py +++ b/tests/components/ovo_energy/test_config_flow.py @@ -1,4 +1,5 @@ """Test the OVO Energy config flow.""" + from unittest.mock import patch import aiohttp @@ -81,15 +82,19 @@ async def test_full_flow_implementation(hass: HomeAssistant) -> None: assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" - with patch( - "homeassistant.components.ovo_energy.config_flow.OVOEnergy.authenticate", - return_value=True, - ), patch( - "homeassistant.components.ovo_energy.config_flow.OVOEnergy.username", - "some_name", - ), patch( - "homeassistant.components.ovo_energy.async_setup_entry", - return_value=True, + with ( + patch( + "homeassistant.components.ovo_energy.config_flow.OVOEnergy.authenticate", + return_value=True, + ), + patch( + "homeassistant.components.ovo_energy.config_flow.OVOEnergy.username", + "some_name", + ), + patch( + "homeassistant.components.ovo_energy.async_setup_entry", + return_value=True, + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -174,12 +179,15 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: assert result["step_id"] == "reauth" assert result["errors"] == {"base": "authorization_error"} - with patch( - "homeassistant.components.ovo_energy.config_flow.OVOEnergy.authenticate", - return_value=True, - ), patch( - "homeassistant.components.ovo_energy.config_flow.OVOEnergy.username", - return_value=FIXTURE_USER_INPUT[CONF_USERNAME], + with ( + patch( + "homeassistant.components.ovo_energy.config_flow.OVOEnergy.authenticate", + return_value=True, + ), + patch( + "homeassistant.components.ovo_energy.config_flow.OVOEnergy.username", + return_value=FIXTURE_USER_INPUT[CONF_USERNAME], + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], diff --git a/tests/components/owntracks/test_config_flow.py b/tests/components/owntracks/test_config_flow.py index 498e930f1e4..8b353789c83 100644 --- a/tests/components/owntracks/test_config_flow.py +++ b/tests/components/owntracks/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for OwnTracks config flow.""" + from unittest.mock import patch import pytest @@ -125,7 +126,7 @@ async def test_unload(hass: HomeAssistant) -> None: ) with patch( - "homeassistant.config_entries.ConfigEntries.async_forward_entry_setup" + "homeassistant.config_entries.ConfigEntries.async_forward_entry_setups" ) as mock_forward: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data={} @@ -134,8 +135,7 @@ async def test_unload(hass: HomeAssistant) -> None: assert len(mock_forward.mock_calls) == 1 entry = result["result"] - assert mock_forward.mock_calls[0][1][0] is entry - assert mock_forward.mock_calls[0][1][1] == "device_tracker" + mock_forward.assert_called_once_with(entry, ["device_tracker"]) assert entry.data["webhook_id"] in hass.data["webhook"] with patch( @@ -145,8 +145,7 @@ async def test_unload(hass: HomeAssistant) -> None: assert await hass.config_entries.async_unload(entry.entry_id) assert len(mock_unload.mock_calls) == 1 - assert mock_forward.mock_calls[0][1][0] is entry - assert mock_forward.mock_calls[0][1][1] == "device_tracker" + mock_forward.assert_called_once_with(entry, ["device_tracker"]) assert entry.data["webhook_id"] not in hass.data["webhook"] @@ -154,15 +153,17 @@ async def test_with_cloud_sub(hass: HomeAssistant) -> None: """Test creating a config flow while subscribed.""" assert await async_setup_component(hass, "cloud", {}) - with patch( - "homeassistant.components.cloud.async_active_subscription", return_value=True - ), patch( - "homeassistant.components.cloud.async_is_logged_in", return_value=True - ), patch( - "homeassistant.components.cloud.async_is_connected", return_value=True - ), patch( - "hass_nabucasa.cloudhooks.Cloudhooks.async_create", - return_value={"cloudhook_url": "https://hooks.nabu.casa/ABCD"}, + with ( + patch( + "homeassistant.components.cloud.async_active_subscription", + return_value=True, + ), + patch("homeassistant.components.cloud.async_is_logged_in", return_value=True), + patch("homeassistant.components.cloud.async_is_connected", return_value=True), + patch( + "hass_nabucasa.cloudhooks.Cloudhooks.async_create", + return_value={"cloudhook_url": "https://hooks.nabu.casa/ABCD"}, + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data={} @@ -181,15 +182,17 @@ async def test_with_cloud_sub_not_connected(hass: HomeAssistant) -> None: """Test creating a config flow while subscribed.""" assert await async_setup_component(hass, "cloud", {}) - with patch( - "homeassistant.components.cloud.async_active_subscription", return_value=True - ), patch( - "homeassistant.components.cloud.async_is_logged_in", return_value=True - ), patch( - "homeassistant.components.cloud.async_is_connected", return_value=False - ), patch( - "hass_nabucasa.cloudhooks.Cloudhooks.async_create", - return_value={"cloudhook_url": "https://hooks.nabu.casa/ABCD"}, + with ( + patch( + "homeassistant.components.cloud.async_active_subscription", + return_value=True, + ), + patch("homeassistant.components.cloud.async_is_logged_in", return_value=True), + patch("homeassistant.components.cloud.async_is_connected", return_value=False), + patch( + "hass_nabucasa.cloudhooks.Cloudhooks.async_create", + return_value={"cloudhook_url": "https://hooks.nabu.casa/ABCD"}, + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data={} diff --git a/tests/components/owntracks/test_device_tracker.py b/tests/components/owntracks/test_device_tracker.py index 1be21e8b1b2..a36d03e973c 100644 --- a/tests/components/owntracks/test_device_tracker.py +++ b/tests/components/owntracks/test_device_tracker.py @@ -1,4 +1,5 @@ """The tests for the Owntracks device tracker.""" + import json from unittest.mock import patch @@ -963,7 +964,7 @@ async def test_mobile_exit_move_beacon(hass: HomeAssistant, context) -> None: async def test_mobile_multiple_async_enter_exit(hass: HomeAssistant, context) -> None: """Test the multiple entering.""" # Test race condition - for _ in range(0, 20): + for _ in range(20): async_fire_mqtt_message( hass, EVENT_TOPIC, json.dumps(MOBILE_BEACON_ENTER_EVENT_MESSAGE) ) @@ -1382,7 +1383,7 @@ def mock_cipher(): (mkey, plaintext) = pickle.loads(base64.b64decode(ciphertext)) if key != mkey: - raise ValueError() + raise ValueError return plaintext return len(TEST_SECRET_KEY), mock_decrypt diff --git a/tests/components/owntracks/test_helper.py b/tests/components/owntracks/test_helper.py index 8e1eee34113..6e6bdccceca 100644 --- a/tests/components/owntracks/test_helper.py +++ b/tests/components/owntracks/test_helper.py @@ -1,4 +1,5 @@ """Test the owntracks_http platform.""" + from unittest.mock import patch import pytest diff --git a/tests/components/owntracks/test_init.py b/tests/components/owntracks/test_init.py index a54a841030b..7e85b67f9de 100644 --- a/tests/components/owntracks/test_init.py +++ b/tests/components/owntracks/test_init.py @@ -1,4 +1,5 @@ """Test the owntracks_http platform.""" + import pytest from homeassistant.components import owntracks diff --git a/tests/components/p1_monitor/conftest.py b/tests/components/p1_monitor/conftest.py index 5f6713f5228..e95cb245f5e 100644 --- a/tests/components/p1_monitor/conftest.py +++ b/tests/components/p1_monitor/conftest.py @@ -1,4 +1,5 @@ """Fixtures for P1 Monitor integration tests.""" + import json from unittest.mock import AsyncMock, MagicMock, patch diff --git a/tests/components/p1_monitor/test_config_flow.py b/tests/components/p1_monitor/test_config_flow.py index 419f24871ef..ec1af77a646 100644 --- a/tests/components/p1_monitor/test_config_flow.py +++ b/tests/components/p1_monitor/test_config_flow.py @@ -1,4 +1,5 @@ """Test the P1 Monitor config flow.""" + from unittest.mock import patch from p1monitor import P1MonitorError @@ -19,11 +20,14 @@ async def test_full_user_flow(hass: HomeAssistant) -> None: assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == "user" - with patch( - "homeassistant.components.p1_monitor.config_flow.P1Monitor.smartmeter" - ) as mock_p1monitor, patch( - "homeassistant.components.p1_monitor.async_setup_entry", return_value=True - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.p1_monitor.config_flow.P1Monitor.smartmeter" + ) as mock_p1monitor, + patch( + "homeassistant.components.p1_monitor.async_setup_entry", return_value=True + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_HOST: "example.com"}, diff --git a/tests/components/p1_monitor/test_init.py b/tests/components/p1_monitor/test_init.py index d7817faecdf..f8de8767a09 100644 --- a/tests/components/p1_monitor/test_init.py +++ b/tests/components/p1_monitor/test_init.py @@ -1,4 +1,5 @@ """Tests for the P1 Monitor integration.""" + from unittest.mock import AsyncMock, MagicMock, patch from p1monitor import P1MonitorConnectionError diff --git a/tests/components/p1_monitor/test_sensor.py b/tests/components/p1_monitor/test_sensor.py index f84df458d4b..e1ea53ba6cc 100644 --- a/tests/components/p1_monitor/test_sensor.py +++ b/tests/components/p1_monitor/test_sensor.py @@ -1,4 +1,5 @@ """Tests for the sensors provided by the P1 Monitor integration.""" + from unittest.mock import MagicMock from p1monitor import P1MonitorNoDataError @@ -13,7 +14,6 @@ from homeassistant.components.sensor import ( from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_FRIENDLY_NAME, - ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, CURRENCY_EURO, UnitOfElectricCurrent, @@ -47,7 +47,6 @@ async def test_smartmeter( assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPower.WATT assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER - assert ATTR_ICON not in state.attributes state = hass.states.get("sensor.smartmeter_energy_consumption_high_tariff") entry = entity_registry.async_get( @@ -64,7 +63,6 @@ async def test_smartmeter( assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL_INCREASING assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.KILO_WATT_HOUR assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY - assert ATTR_ICON not in state.attributes state = hass.states.get("sensor.smartmeter_energy_tariff_period") entry = entity_registry.async_get("sensor.smartmeter_energy_tariff_period") @@ -73,7 +71,6 @@ async def test_smartmeter( assert entry.unique_id == f"{entry_id}_smartmeter_energy_tariff_period" assert state.state == "high" assert state.attributes.get(ATTR_FRIENDLY_NAME) == "SmartMeter Energy tariff period" - assert state.attributes.get(ATTR_ICON) == "mdi:calendar-clock" assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes assert ATTR_DEVICE_CLASS not in state.attributes @@ -109,7 +106,6 @@ async def test_phases( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfElectricPotential.VOLT ) assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.VOLTAGE - assert ATTR_ICON not in state.attributes state = hass.states.get("sensor.phases_current_phase_l1") entry = entity_registry.async_get("sensor.phases_current_phase_l1") @@ -123,7 +119,6 @@ async def test_phases( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfElectricCurrent.AMPERE ) assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.CURRENT - assert ATTR_ICON not in state.attributes state = hass.states.get("sensor.phases_power_consumed_phase_l1") entry = entity_registry.async_get("sensor.phases_power_consumed_phase_l1") @@ -135,7 +130,6 @@ async def test_phases( assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPower.WATT assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER - assert ATTR_ICON not in state.attributes assert entry.device_id device_entry = device_registry.async_get(entry.device_id) @@ -245,7 +239,7 @@ async def test_no_watermeter( @pytest.mark.parametrize( "entity_id", - ("sensor.smartmeter_gas_consumption",), + ["sensor.smartmeter_gas_consumption"], ) async def test_smartmeter_disabled_by_default( hass: HomeAssistant, init_integration: MockConfigEntry, entity_id: str diff --git a/tests/components/panasonic_viera/test_config_flow.py b/tests/components/panasonic_viera/test_config_flow.py index 34ac218a839..f9664f5d657 100644 --- a/tests/components/panasonic_viera/test_config_flow.py +++ b/tests/components/panasonic_viera/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Panasonic Viera config flow.""" + from unittest.mock import patch from panasonic_viera import SOAPError diff --git a/tests/components/panasonic_viera/test_init.py b/tests/components/panasonic_viera/test_init.py index 03d23316bcc..7165b90d93c 100644 --- a/tests/components/panasonic_viera/test_init.py +++ b/tests/components/panasonic_viera/test_init.py @@ -1,4 +1,5 @@ """Test the Panasonic Viera setup process.""" + from unittest.mock import Mock, patch from homeassistant.components.panasonic_viera.const import ( diff --git a/tests/components/panasonic_viera/test_media_player.py b/tests/components/panasonic_viera/test_media_player.py index 1203bf1ed51..dab56542e6a 100644 --- a/tests/components/panasonic_viera/test_media_player.py +++ b/tests/components/panasonic_viera/test_media_player.py @@ -23,7 +23,7 @@ async def test_media_player_handle_URLerror( mock_remote.get_mute = Mock(side_effect=URLError(None, None)) async_fire_time_changed(hass, utcnow() + timedelta(minutes=2)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state_tv = hass.states.get("media_player.panasonic_viera_tv") assert state_tv.state == STATE_UNAVAILABLE @@ -41,7 +41,7 @@ async def test_media_player_handle_HTTPError( mock_remote.get_mute = Mock(side_effect=HTTPError(None, 400, None, None, None)) async_fire_time_changed(hass, utcnow() + timedelta(minutes=2)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state_tv = hass.states.get("media_player.panasonic_viera_tv") assert state_tv.state == STATE_OFF diff --git a/tests/components/panasonic_viera/test_remote.py b/tests/components/panasonic_viera/test_remote.py index cb9777c1177..05254753d3f 100644 --- a/tests/components/panasonic_viera/test_remote.py +++ b/tests/components/panasonic_viera/test_remote.py @@ -1,4 +1,5 @@ """Test the Panasonic Viera remote entity.""" + from unittest.mock import Mock, call from panasonic_viera import Keys, SOAPError diff --git a/tests/components/panel_custom/test_init.py b/tests/components/panel_custom/test_init.py index d84b4c812c7..dc0f06d2a56 100644 --- a/tests/components/panel_custom/test_init.py +++ b/tests/components/panel_custom/test_init.py @@ -1,4 +1,5 @@ """The tests for the panel_custom component.""" + from unittest.mock import Mock, patch from homeassistant import setup diff --git a/tests/components/panel_iframe/test_init.py b/tests/components/panel_iframe/test_init.py index bd8950163a9..0e898fd6266 100644 --- a/tests/components/panel_iframe/test_init.py +++ b/tests/components/panel_iframe/test_init.py @@ -1,17 +1,44 @@ """The tests for the panel_iframe component.""" + +from typing import Any + import pytest -from homeassistant.components import frontend +from homeassistant.components.panel_iframe import DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.helpers import issue_registry as ir from homeassistant.setup import async_setup_component +from tests.typing import WebSocketGenerator + +TEST_CONFIG = { + "router": { + "icon": "mdi:network-wireless", + "title": "Router", + "url": "http://192.168.1.1", + "require_admin": True, + }, + "weather": { + "icon": "mdi:weather", + "title": "Weather", + "url": "https://www.wunderground.com/us/ca/san-diego", + "require_admin": True, + }, + "api": {"icon": "mdi:weather", "title": "Api", "url": "/api"}, + "ftp": { + "icon": "mdi:weather", + "title": "FTP", + "url": "ftp://some/ftp", + }, +} + @pytest.mark.parametrize( "config_to_try", - ( + [ {"invalid space": {"url": "https://home-assistant.io"}}, {"router": {"url": "not-a-url"}}, - ), + ], ) async def test_wrong_config(hass: HomeAssistant, config_to_try) -> None: """Test setup with wrong configuration.""" @@ -20,73 +47,107 @@ async def test_wrong_config(hass: HomeAssistant, config_to_try) -> None: ) -async def test_correct_config(hass: HomeAssistant) -> None: - """Test correct config.""" +async def test_import_config( + hass: HomeAssistant, + hass_storage: dict[str, Any], + hass_ws_client: WebSocketGenerator, +) -> None: + """Test import config.""" + client = await hass_ws_client(hass) + assert await async_setup_component( hass, "panel_iframe", - { - "panel_iframe": { - "router": { - "icon": "mdi:network-wireless", - "title": "Router", - "url": "http://192.168.1.1", - "require_admin": True, - }, - "weather": { - "icon": "mdi:weather", - "title": "Weather", - "url": "https://www.wunderground.com/us/ca/san-diego", - "require_admin": True, - }, - "api": {"icon": "mdi:weather", "title": "Api", "url": "/api"}, - "ftp": { - "icon": "mdi:weather", - "title": "FTP", - "url": "ftp://some/ftp", - }, - } - }, + {"panel_iframe": TEST_CONFIG}, ) - panels = hass.data[frontend.DATA_PANELS] + # List dashboards + await client.send_json_auto_id({"type": "lovelace/dashboards/list"}) + response = await client.receive_json() + assert response["success"] + assert response["result"] == [ + { + "icon": "mdi:network-wireless", + "id": "router", + "mode": "storage", + "require_admin": True, + "show_in_sidebar": True, + "title": "Router", + "url_path": "router", + }, + { + "icon": "mdi:weather", + "id": "weather", + "mode": "storage", + "require_admin": True, + "show_in_sidebar": True, + "title": "Weather", + "url_path": "weather", + }, + { + "icon": "mdi:weather", + "id": "api", + "mode": "storage", + "require_admin": False, + "show_in_sidebar": True, + "title": "Api", + "url_path": "api", + }, + { + "icon": "mdi:weather", + "id": "ftp", + "mode": "storage", + "require_admin": False, + "show_in_sidebar": True, + "title": "FTP", + "url_path": "ftp", + }, + ] - assert panels.get("router").to_response() == { - "component_name": "iframe", - "config": {"url": "http://192.168.1.1"}, - "config_panel_domain": None, - "icon": "mdi:network-wireless", - "title": "Router", - "url_path": "router", - "require_admin": True, + for url_path in ["api", "ftp", "router", "weather"]: + await client.send_json_auto_id( + {"type": "lovelace/config", "url_path": url_path} + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == { + "strategy": {"type": "iframe", "url": TEST_CONFIG[url_path]["url"]} + } + + assert hass_storage[DOMAIN]["data"] == {"migrated": True} + + +async def test_import_config_once( + hass: HomeAssistant, + hass_storage: dict[str, Any], + hass_ws_client: WebSocketGenerator, +) -> None: + """Test import config only happens once.""" + client = await hass_ws_client(hass) + + hass_storage[DOMAIN] = { + "version": 1, + "minor_version": 1, + "key": "map", + "data": {"migrated": True}, } - assert panels.get("weather").to_response() == { - "component_name": "iframe", - "config": {"url": "https://www.wunderground.com/us/ca/san-diego"}, - "config_panel_domain": None, - "icon": "mdi:weather", - "title": "Weather", - "url_path": "weather", - "require_admin": True, - } + assert await async_setup_component( + hass, + "panel_iframe", + {"panel_iframe": TEST_CONFIG}, + ) - assert panels.get("api").to_response() == { - "component_name": "iframe", - "config": {"url": "/api"}, - "config_panel_domain": None, - "icon": "mdi:weather", - "title": "Api", - "url_path": "api", - "require_admin": False, - } + # List dashboards + await client.send_json_auto_id({"type": "lovelace/dashboards/list"}) + response = await client.receive_json() + assert response["success"] + assert response["result"] == [] - assert panels.get("ftp").to_response() == { - "component_name": "iframe", - "config": {"url": "ftp://some/ftp"}, - "config_panel_domain": None, - "icon": "mdi:weather", - "title": "FTP", - "url_path": "ftp", - "require_admin": False, - } + +async def test_create_issue_when_manually_configured(hass: HomeAssistant) -> None: + """Test creating issue registry issues.""" + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + + issue_registry = ir.async_get(hass) + assert issue_registry.async_get_issue(DOMAIN, "deprecated_yaml") diff --git a/tests/components/peco/test_config_flow.py b/tests/components/peco/test_config_flow.py index 833c66ab37a..df178e125e1 100644 --- a/tests/components/peco/test_config_flow.py +++ b/tests/components/peco/test_config_flow.py @@ -1,4 +1,5 @@ """Test the PECO Outage Counter config flow.""" + from unittest.mock import patch from peco import HttpError, IncompatibleMeterError, UnresponsiveMeterError @@ -48,10 +49,13 @@ async def test_invalid_county(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" - with patch( - "homeassistant.components.peco.async_setup_entry", - return_value=True, - ), pytest.raises(Invalid): + with ( + patch( + "homeassistant.components.peco.async_setup_entry", + return_value=True, + ), + pytest.raises(Invalid), + ): await hass.config_entries.flow.async_configure( result["flow_id"], { diff --git a/tests/components/peco/test_init.py b/tests/components/peco/test_init.py index c8a7c5ccbd5..55dc0a15a4b 100644 --- a/tests/components/peco/test_init.py +++ b/tests/components/peco/test_init.py @@ -1,4 +1,5 @@ """Test the PECO Outage Counter init file.""" + from unittest.mock import patch from peco import ( @@ -27,18 +28,21 @@ async def test_unload_entry(hass: HomeAssistant) -> None: config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_ENTRY_DATA) config_entry.add_to_hass(hass) - with patch( - "peco.PecoOutageApi.get_outage_totals", - return_value=OutageResults( - customers_out=0, - percent_customers_out=0, - outage_count=0, - customers_served=350394, + with ( + patch( + "peco.PecoOutageApi.get_outage_totals", + return_value=OutageResults( + customers_out=0, + percent_customers_out=0, + outage_count=0, + customers_served=350394, + ), ), - ), patch( - "peco.PecoOutageApi.get_map_alerts", - return_value=AlertResults( - alert_content="Testing 1234", alert_title="Testing 4321" + patch( + "peco.PecoOutageApi.get_map_alerts", + return_value=AlertResults( + alert_content="Testing 1234", alert_title="Testing 4321" + ), ), ): assert await hass.config_entries.async_setup(config_entry.entry_id) @@ -163,21 +167,25 @@ async def test_unresponsive_meter_error(hass: HomeAssistant) -> None: config_entry = MockConfigEntry(domain=DOMAIN, data=METER_DATA) config_entry.add_to_hass(hass) - with patch( - "peco.PecoOutageApi.meter_check", - side_effect=UnresponsiveMeterError(), - ), patch( - "peco.PecoOutageApi.get_outage_count", - return_value=OutageResults( - customers_out=0, - percent_customers_out=0, - outage_count=0, - customers_served=350394, + with ( + patch( + "peco.PecoOutageApi.meter_check", + side_effect=UnresponsiveMeterError(), ), - ), patch( - "peco.PecoOutageApi.get_map_alerts", - return_value=AlertResults( - alert_content="Testing 1234", alert_title="Testing 4321" + patch( + "peco.PecoOutageApi.get_outage_count", + return_value=OutageResults( + customers_out=0, + percent_customers_out=0, + outage_count=0, + customers_served=350394, + ), + ), + patch( + "peco.PecoOutageApi.get_map_alerts", + return_value=AlertResults( + alert_content="Testing 1234", alert_title="Testing 4321" + ), ), ): assert not await hass.config_entries.async_setup(config_entry.entry_id) @@ -193,21 +201,25 @@ async def test_meter_http_error(hass: HomeAssistant) -> None: config_entry = MockConfigEntry(domain=DOMAIN, data=METER_DATA) config_entry.add_to_hass(hass) - with patch( - "peco.PecoOutageApi.meter_check", - side_effect=HttpError(), - ), patch( - "peco.PecoOutageApi.get_outage_count", - return_value=OutageResults( - customers_out=0, - percent_customers_out=0, - outage_count=0, - customers_served=350394, + with ( + patch( + "peco.PecoOutageApi.meter_check", + side_effect=HttpError(), ), - ), patch( - "peco.PecoOutageApi.get_map_alerts", - return_value=AlertResults( - alert_content="Testing 1234", alert_title="Testing 4321" + patch( + "peco.PecoOutageApi.get_outage_count", + return_value=OutageResults( + customers_out=0, + percent_customers_out=0, + outage_count=0, + customers_served=350394, + ), + ), + patch( + "peco.PecoOutageApi.get_map_alerts", + return_value=AlertResults( + alert_content="Testing 1234", alert_title="Testing 4321" + ), ), ): assert not await hass.config_entries.async_setup(config_entry.entry_id) @@ -223,21 +235,25 @@ async def test_meter_bad_json(hass: HomeAssistant) -> None: config_entry = MockConfigEntry(domain=DOMAIN, data=METER_DATA) config_entry.add_to_hass(hass) - with patch( - "peco.PecoOutageApi.meter_check", - side_effect=BadJSONError(), - ), patch( - "peco.PecoOutageApi.get_outage_count", - return_value=OutageResults( - customers_out=0, - percent_customers_out=0, - outage_count=0, - customers_served=350394, + with ( + patch( + "peco.PecoOutageApi.meter_check", + side_effect=BadJSONError(), ), - ), patch( - "peco.PecoOutageApi.get_map_alerts", - return_value=AlertResults( - alert_content="Testing 1234", alert_title="Testing 4321" + patch( + "peco.PecoOutageApi.get_outage_count", + return_value=OutageResults( + customers_out=0, + percent_customers_out=0, + outage_count=0, + customers_served=350394, + ), + ), + patch( + "peco.PecoOutageApi.get_map_alerts", + return_value=AlertResults( + alert_content="Testing 1234", alert_title="Testing 4321" + ), ), ): assert not await hass.config_entries.async_setup(config_entry.entry_id) @@ -253,21 +269,25 @@ async def test_meter_timeout(hass: HomeAssistant) -> None: config_entry = MockConfigEntry(domain=DOMAIN, data=METER_DATA) config_entry.add_to_hass(hass) - with patch( - "peco.PecoOutageApi.meter_check", - side_effect=TimeoutError(), - ), patch( - "peco.PecoOutageApi.get_outage_count", - return_value=OutageResults( - customers_out=0, - percent_customers_out=0, - outage_count=0, - customers_served=350394, + with ( + patch( + "peco.PecoOutageApi.meter_check", + side_effect=TimeoutError(), ), - ), patch( - "peco.PecoOutageApi.get_map_alerts", - return_value=AlertResults( - alert_content="Testing 1234", alert_title="Testing 4321" + patch( + "peco.PecoOutageApi.get_outage_count", + return_value=OutageResults( + customers_out=0, + percent_customers_out=0, + outage_count=0, + customers_served=350394, + ), + ), + patch( + "peco.PecoOutageApi.get_map_alerts", + return_value=AlertResults( + alert_content="Testing 1234", alert_title="Testing 4321" + ), ), ): assert not await hass.config_entries.async_setup(config_entry.entry_id) @@ -283,21 +303,25 @@ async def test_meter_data(hass: HomeAssistant) -> None: config_entry = MockConfigEntry(domain=DOMAIN, data=METER_DATA) config_entry.add_to_hass(hass) - with patch( - "peco.PecoOutageApi.meter_check", - return_value=True, - ), patch( - "peco.PecoOutageApi.get_outage_count", - return_value=OutageResults( - customers_out=0, - percent_customers_out=0, - outage_count=0, - customers_served=350394, + with ( + patch( + "peco.PecoOutageApi.meter_check", + return_value=True, ), - ), patch( - "peco.PecoOutageApi.get_map_alerts", - return_value=AlertResults( - alert_content="Testing 1234", alert_title="Testing 4321" + patch( + "peco.PecoOutageApi.get_outage_count", + return_value=OutageResults( + customers_out=0, + percent_customers_out=0, + outage_count=0, + customers_served=350394, + ), + ), + patch( + "peco.PecoOutageApi.get_map_alerts", + return_value=AlertResults( + alert_content="Testing 1234", alert_title="Testing 4321" + ), ), ): assert await hass.config_entries.async_setup(config_entry.entry_id) diff --git a/tests/components/peco/test_sensor.py b/tests/components/peco/test_sensor.py index c5dcd843bd4..2546b7c8996 100644 --- a/tests/components/peco/test_sensor.py +++ b/tests/components/peco/test_sensor.py @@ -1,4 +1,5 @@ """Test the PECO Outage Counter sensors.""" + from unittest.mock import patch from peco import AlertResults, OutageResults @@ -33,18 +34,21 @@ async def test_sensor_available( config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_ENTRY_DATA) config_entry.add_to_hass(hass) - with patch( - "peco.PecoOutageApi.get_outage_totals", - return_value=OutageResults( - customers_out=123, - percent_customers_out=15.589, - outage_count=456, - customers_served=789, + with ( + patch( + "peco.PecoOutageApi.get_outage_totals", + return_value=OutageResults( + customers_out=123, + percent_customers_out=15.589, + outage_count=456, + customers_served=789, + ), ), - ), patch( - "peco.PecoOutageApi.get_map_alerts", - return_value=AlertResults( - alert_content="Testing 1234", alert_title="Testing 4321" + patch( + "peco.PecoOutageApi.get_map_alerts", + return_value=AlertResults( + alert_content="Testing 1234", alert_title="Testing 4321" + ), ), ): assert await hass.config_entries.async_setup(config_entry.entry_id) @@ -65,18 +69,21 @@ async def test_sensor_available( config_entry = MockConfigEntry(domain=DOMAIN, data=COUNTY_ENTRY_DATA) config_entry.add_to_hass(hass) - with patch( - "peco.PecoOutageApi.get_outage_count", - return_value=OutageResults( - customers_out=123, - percent_customers_out=15.589, - outage_count=456, - customers_served=789, + with ( + patch( + "peco.PecoOutageApi.get_outage_count", + return_value=OutageResults( + customers_out=123, + percent_customers_out=15.589, + outage_count=456, + customers_served=789, + ), ), - ), patch( - "peco.PecoOutageApi.get_map_alerts", - return_value=AlertResults( - alert_content="Testing 1234", alert_title="Testing 4321" + patch( + "peco.PecoOutageApi.get_map_alerts", + return_value=AlertResults( + alert_content="Testing 1234", alert_title="Testing 4321" + ), ), ): assert await hass.config_entries.async_setup(config_entry.entry_id) diff --git a/tests/components/pegel_online/test_config_flow.py b/tests/components/pegel_online/test_config_flow.py index 61f7dc75255..fedcba94616 100644 --- a/tests/components/pegel_online/test_config_flow.py +++ b/tests/components/pegel_online/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for Pegel Online config flow.""" + from unittest.mock import patch from aiohttp.client_exceptions import ClientError @@ -35,11 +36,14 @@ async def test_user(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" - with patch( - "homeassistant.components.pegel_online.async_setup_entry", return_value=True - ) as mock_setup_entry, patch( - "homeassistant.components.pegel_online.config_flow.PegelOnline", - ) as pegelonline: + with ( + patch( + "homeassistant.components.pegel_online.async_setup_entry", return_value=True + ) as mock_setup_entry, + patch( + "homeassistant.components.pegel_online.config_flow.PegelOnline", + ) as pegelonline, + ): pegelonline.return_value = PegelOnlineMock(nearby_stations=MOCK_NEARBY_STATIONS) result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_DATA_STEP1 @@ -99,11 +103,14 @@ async def test_connection_error(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" - with patch( - "homeassistant.components.pegel_online.async_setup_entry", return_value=True - ) as mock_setup_entry, patch( - "homeassistant.components.pegel_online.config_flow.PegelOnline", - ) as pegelonline: + with ( + patch( + "homeassistant.components.pegel_online.async_setup_entry", return_value=True + ) as mock_setup_entry, + patch( + "homeassistant.components.pegel_online.config_flow.PegelOnline", + ) as pegelonline, + ): # connection issue during setup pegelonline.return_value = PegelOnlineMock(side_effect=ClientError) result = await hass.config_entries.flow.async_configure( @@ -141,11 +148,14 @@ async def test_user_no_stations(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" - with patch( - "homeassistant.components.pegel_online.async_setup_entry", return_value=True - ) as mock_setup_entry, patch( - "homeassistant.components.pegel_online.config_flow.PegelOnline", - ) as pegelonline: + with ( + patch( + "homeassistant.components.pegel_online.async_setup_entry", return_value=True + ) as mock_setup_entry, + patch( + "homeassistant.components.pegel_online.config_flow.PegelOnline", + ) as pegelonline, + ): # no stations found pegelonline.return_value = PegelOnlineMock(nearby_stations={}) result = await hass.config_entries.flow.async_configure( diff --git a/tests/components/pegel_online/test_init.py b/tests/components/pegel_online/test_init.py index 2b5ba3642ec..ee2e78af7cf 100644 --- a/tests/components/pegel_online/test_init.py +++ b/tests/components/pegel_online/test_init.py @@ -1,4 +1,5 @@ """Test pegel_online component.""" + from unittest.mock import patch from aiohttp.client_exceptions import ClientError diff --git a/tests/components/pegel_online/test_sensor.py b/tests/components/pegel_online/test_sensor.py index a02c7538280..e911ec571cd 100644 --- a/tests/components/pegel_online/test_sensor.py +++ b/tests/components/pegel_online/test_sensor.py @@ -1,4 +1,5 @@ """Test pegel_online component.""" + from unittest.mock import patch from aiopegelonline.models import Station, StationMeasurements diff --git a/tests/components/permobil/conftest.py b/tests/components/permobil/conftest.py index 2dcf9bd5ad2..74d17616af7 100644 --- a/tests/components/permobil/conftest.py +++ b/tests/components/permobil/conftest.py @@ -1,4 +1,5 @@ """Common fixtures for the MyPermobil tests.""" + from collections.abc import Generator from unittest.mock import AsyncMock, Mock, patch diff --git a/tests/components/permobil/test_config_flow.py b/tests/components/permobil/test_config_flow.py index 0f303cc0482..5968e247a95 100644 --- a/tests/components/permobil/test_config_flow.py +++ b/tests/components/permobil/test_config_flow.py @@ -1,4 +1,5 @@ """Test the MyPermobil config flow.""" + from unittest.mock import Mock, patch from mypermobil import ( diff --git a/tests/components/persistent_notification/test_init.py b/tests/components/persistent_notification/test_init.py index 921f4b12045..3e99e268231 100644 --- a/tests/components/persistent_notification/test_init.py +++ b/tests/components/persistent_notification/test_init.py @@ -1,6 +1,5 @@ """The tests for the persistent notification component.""" - import homeassistant.components.persistent_notification as pn from homeassistant.components.websocket_api.const import TYPE_RESULT from homeassistant.core import HomeAssistant diff --git a/tests/components/persistent_notification/test_trigger.py b/tests/components/persistent_notification/test_trigger.py index 3cf3655a3b6..16208143447 100644 --- a/tests/components/persistent_notification/test_trigger.py +++ b/tests/components/persistent_notification/test_trigger.py @@ -1,4 +1,5 @@ """The tests for the persistent notification component triggers.""" + from typing import Any import homeassistant.components.persistent_notification as pn diff --git a/tests/components/person/conftest.py b/tests/components/person/conftest.py index 4079ff24267..7f06b854c5c 100644 --- a/tests/components/person/conftest.py +++ b/tests/components/person/conftest.py @@ -1,4 +1,5 @@ """The tests for the person component.""" + import logging import pytest diff --git a/tests/components/person/test_init.py b/tests/components/person/test_init.py index a9f91801883..b00a0ff1a6b 100644 --- a/tests/components/person/test_init.py +++ b/tests/components/person/test_init.py @@ -1,4 +1,5 @@ """The tests for the person component.""" + from typing import Any from unittest.mock import patch @@ -348,8 +349,8 @@ async def test_create_person_during_run(hass: HomeAssistant) -> None: hass.states.async_set(DEVICE_TRACKER, "home") await hass.async_block_till_done() - await hass.components.person.async_create_person( - "tracked person", device_trackers=[DEVICE_TRACKER] + await person.async_create_person( + hass, "tracked person", device_trackers=[DEVICE_TRACKER] ) await hass.async_block_till_done() diff --git a/tests/components/person/test_recorder.py b/tests/components/person/test_recorder.py index 51b5691bd8e..4d25ce7add4 100644 --- a/tests/components/person/test_recorder.py +++ b/tests/components/person/test_recorder.py @@ -1,4 +1,5 @@ """The tests for update recorder.""" + from __future__ import annotations from datetime import timedelta diff --git a/tests/components/person/test_significant_change.py b/tests/components/person/test_significant_change.py index 36de0d32b59..e642d3b7270 100644 --- a/tests/components/person/test_significant_change.py +++ b/tests/components/person/test_significant_change.py @@ -1,4 +1,5 @@ """Test the Person significant change platform.""" + from homeassistant.components.person.significant_change import ( async_check_significant_change, ) diff --git a/tests/components/philips_js/conftest.py b/tests/components/philips_js/conftest.py index bc94d721cc9..3591546dfe9 100644 --- a/tests/components/philips_js/conftest.py +++ b/tests/components/philips_js/conftest.py @@ -1,4 +1,5 @@ """Standard setup for tests.""" + from collections.abc import Generator from unittest.mock import AsyncMock, create_autospec, patch @@ -15,10 +16,13 @@ from tests.common import MockConfigEntry, mock_device_registry @pytest.fixture def mock_setup_entry() -> Generator[AsyncMock, None, None]: """Disable component setup.""" - with patch( - "homeassistant.components.philips_js.async_setup_entry", return_value=True - ) as mock_setup_entry, patch( - "homeassistant.components.philips_js.async_unload_entry", return_value=True + with ( + patch( + "homeassistant.components.philips_js.async_setup_entry", return_value=True + ) as mock_setup_entry, + patch( + "homeassistant.components.philips_js.async_unload_entry", return_value=True + ), ): yield mock_setup_entry @@ -48,9 +52,12 @@ def mock_tv(): tv.ambilight_styles = {} tv.ambilight_cached = {} - with patch( - "homeassistant.components.philips_js.config_flow.PhilipsTV", return_value=tv - ), patch("homeassistant.components.philips_js.PhilipsTV", return_value=tv): + with ( + patch( + "homeassistant.components.philips_js.config_flow.PhilipsTV", return_value=tv + ), + patch("homeassistant.components.philips_js.PhilipsTV", return_value=tv), + ): yield tv diff --git a/tests/components/philips_js/test_binary_sensor.py b/tests/components/philips_js/test_binary_sensor.py index 01233706d07..c50c776d416 100644 --- a/tests/components/philips_js/test_binary_sensor.py +++ b/tests/components/philips_js/test_binary_sensor.py @@ -1,4 +1,5 @@ """The tests for philips_js binary_sensor.""" + import pytest from homeassistant.const import STATE_OFF, STATE_ON diff --git a/tests/components/philips_js/test_config_flow.py b/tests/components/philips_js/test_config_flow.py index 8229f4e8fa9..07f1f2be933 100644 --- a/tests/components/philips_js/test_config_flow.py +++ b/tests/components/philips_js/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Philips TV config flow.""" + from unittest.mock import ANY from haphilipsjs import PairingFailure diff --git a/tests/components/philips_js/test_device_trigger.py b/tests/components/philips_js/test_device_trigger.py index 0e8427b29e5..3a59e3c6662 100644 --- a/tests/components/philips_js/test_device_trigger.py +++ b/tests/components/philips_js/test_device_trigger.py @@ -1,4 +1,5 @@ """The tests for Philips TV device triggers.""" + import pytest from pytest_unordered import unordered diff --git a/tests/components/pi_hole/__init__.py b/tests/components/pi_hole/__init__.py index 8295f933d46..38231778624 100644 --- a/tests/components/pi_hole/__init__.py +++ b/tests/components/pi_hole/__init__.py @@ -1,4 +1,5 @@ """Tests for the pi_hole component.""" + from unittest.mock import AsyncMock, MagicMock, patch from hole.exceptions import HoleError diff --git a/tests/components/pi_hole/test_config_flow.py b/tests/components/pi_hole/test_config_flow.py index 1c8abdcfedb..350b8b899d8 100644 --- a/tests/components/pi_hole/test_config_flow.py +++ b/tests/components/pi_hole/test_config_flow.py @@ -1,4 +1,5 @@ """Test pi_hole config flow.""" + from homeassistant.components import pi_hole from homeassistant.components.pi_hole.const import DOMAIN from homeassistant.config_entries import SOURCE_USER @@ -112,6 +113,7 @@ async def test_flow_reauth(hass: HomeAssistant) -> None: entry.add_to_hass(hass) with _patch_init_hole(mocked_hole), _patch_config_flow_hole(mocked_hole): assert not await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() flows = hass.config_entries.flow.async_progress() diff --git a/tests/components/pi_hole/test_init.py b/tests/components/pi_hole/test_init.py index b0b71fe13ed..a58a46680bb 100644 --- a/tests/components/pi_hole/test_init.py +++ b/tests/components/pi_hole/test_init.py @@ -1,4 +1,5 @@ """Test pi_hole component.""" + import logging from unittest.mock import AsyncMock diff --git a/tests/components/picnic/conftest.py b/tests/components/picnic/conftest.py index 5bb84c7a1c1..569d65df387 100644 --- a/tests/components/picnic/conftest.py +++ b/tests/components/picnic/conftest.py @@ -1,4 +1,5 @@ """Conftest for Picnic tests.""" + from collections.abc import Awaitable, Callable import json from unittest.mock import MagicMock, patch diff --git a/tests/components/picnic/test_config_flow.py b/tests/components/picnic/test_config_flow.py index d90551b01df..ec103e9f3a3 100644 --- a/tests/components/picnic/test_config_flow.py +++ b/tests/components/picnic/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Picnic config flow.""" + from unittest.mock import patch import pytest diff --git a/tests/components/picnic/test_sensor.py b/tests/components/picnic/test_sensor.py index 1535ac1d9ad..37191642c07 100644 --- a/tests/components/picnic/test_sensor.py +++ b/tests/components/picnic/test_sensor.py @@ -1,4 +1,5 @@ """The tests for the Picnic sensor platform.""" + import copy from datetime import timedelta import unittest diff --git a/tests/components/picnic/test_services.py b/tests/components/picnic/test_services.py index bc80ff73a11..b7d5cb0b011 100644 --- a/tests/components/picnic/test_services.py +++ b/tests/components/picnic/test_services.py @@ -1,4 +1,5 @@ """Tests for the Picnic services.""" + from unittest.mock import MagicMock, patch import pytest diff --git a/tests/components/pilight/test_init.py b/tests/components/pilight/test_init.py index 96f384f98b9..621d002bb62 100644 --- a/tests/components/pilight/test_init.py +++ b/tests/components/pilight/test_init.py @@ -1,9 +1,11 @@ """The tests for the pilight component.""" + from datetime import timedelta import logging import socket from unittest.mock import patch +import pytest from voluptuous import MultipleInvalid from homeassistant.components import pilight @@ -69,9 +71,10 @@ class PilightDaemonSim: @patch("homeassistant.components.pilight._LOGGER.error") async def test_connection_failed_error(mock_error, hass: HomeAssistant) -> None: """Try to connect at 127.0.0.1:5001 with socket error.""" - with assert_setup_component(4), patch( - "pilight.pilight.Client", side_effect=socket.error - ) as mock_client: + with ( + assert_setup_component(4), + patch("pilight.pilight.Client", side_effect=socket.error) as mock_client, + ): assert not await async_setup_component( hass, pilight.DOMAIN, {pilight.DOMAIN: {}} ) @@ -84,9 +87,10 @@ async def test_connection_failed_error(mock_error, hass: HomeAssistant) -> None: @patch("homeassistant.components.pilight._LOGGER.error") async def test_connection_timeout_error(mock_error, hass: HomeAssistant) -> None: """Try to connect at 127.0.0.1:5001 with socket timeout.""" - with assert_setup_component(4), patch( - "pilight.pilight.Client", side_effect=socket.timeout - ) as mock_client: + with ( + assert_setup_component(4), + patch("pilight.pilight.Client", side_effect=socket.timeout) as mock_client, + ): assert not await async_setup_component( hass, pilight.DOMAIN, {pilight.DOMAIN: {}} ) @@ -103,7 +107,7 @@ async def test_send_code_no_protocol(hass: HomeAssistant) -> None: assert await async_setup_component(hass, pilight.DOMAIN, {pilight.DOMAIN: {}}) # Call without protocol info, should raise an error - try: + with pytest.raises(MultipleInvalid) as excinfo: await hass.services.async_call( pilight.DOMAIN, pilight.SERVICE_NAME, @@ -111,8 +115,7 @@ async def test_send_code_no_protocol(hass: HomeAssistant) -> None: blocking=True, ) await hass.async_block_till_done() - except MultipleInvalid as error: - assert "required key not provided @ data['protocol']" in str(error) + assert "required key not provided @ data['protocol']" in str(excinfo.value) @patch("homeassistant.components.pilight._LOGGER.error") @@ -141,8 +144,9 @@ async def test_send_code(mock_pilight_error, hass: HomeAssistant) -> None: @patch("homeassistant.components.pilight._LOGGER.error") async def test_send_code_fail(mock_pilight_error, hass: HomeAssistant) -> None: """Check IOError exception error message.""" - with assert_setup_component(4), patch( - "pilight.pilight.Client.send_code", side_effect=IOError + with ( + assert_setup_component(4), + patch("pilight.pilight.Client.send_code", side_effect=IOError), ): assert await async_setup_component(hass, pilight.DOMAIN, {pilight.DOMAIN: {}}) diff --git a/tests/components/pilight/test_sensor.py b/tests/components/pilight/test_sensor.py index 54a26f690d5..629f0f13de4 100644 --- a/tests/components/pilight/test_sensor.py +++ b/tests/components/pilight/test_sensor.py @@ -1,4 +1,5 @@ """The tests for the Pilight sensor platform.""" + import logging import pytest diff --git a/tests/components/ping/conftest.py b/tests/components/ping/conftest.py index 24dd3314e3c..9bbbc9e6e32 100644 --- a/tests/components/ping/conftest.py +++ b/tests/components/ping/conftest.py @@ -1,4 +1,5 @@ """Test configuration for ping.""" + from unittest.mock import patch from icmplib import Host @@ -16,21 +17,25 @@ from tests.common import MockConfigEntry @pytest.fixture def patch_setup(*args, **kwargs): """Patch setup methods.""" - with patch( - "homeassistant.components.ping.async_setup_entry", - return_value=True, - ), patch("homeassistant.components.ping.async_setup", return_value=True): + with ( + patch( + "homeassistant.components.ping.async_setup_entry", + return_value=True, + ), + patch("homeassistant.components.ping.async_setup", return_value=True), + ): yield @pytest.fixture(autouse=True) async def patch_ping(): """Patch icmplib async_ping.""" - mock = Host("10.10.10.10", 5, [10, 1, 2]) + mock = Host("10.10.10.10", 5, [10, 1, 2, 5, 6]) - with patch( - "homeassistant.components.ping.helpers.async_ping", return_value=mock - ), patch("homeassistant.components.ping.async_ping", return_value=mock): + with ( + patch("homeassistant.components.ping.helpers.async_ping", return_value=mock), + patch("homeassistant.components.ping.async_ping", return_value=mock), + ): yield mock diff --git a/tests/components/ping/const.py b/tests/components/ping/const.py index 048924292c7..f680f20d270 100644 --- a/tests/components/ping/const.py +++ b/tests/components/ping/const.py @@ -1,4 +1,5 @@ """Constants for tests.""" + from datetime import timedelta from icmplib import Host diff --git a/tests/components/ping/snapshots/test_binary_sensor.ambr b/tests/components/ping/snapshots/test_binary_sensor.ambr index 0924c383fc2..98ea9a8a847 100644 --- a/tests/components/ping/snapshots/test_binary_sensor.ambr +++ b/tests/components/ping/snapshots/test_binary_sensor.ambr @@ -72,7 +72,7 @@ 'domain': 'binary_sensor', 'entity_category': None, 'entity_id': 'binary_sensor.10_10_10_10', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -83,7 +83,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': '10.10.10.10', + 'original_name': None, 'platform': 'ping', 'previous_unique_id': None, 'supported_features': 0, @@ -96,14 +96,15 @@ 'attributes': ReadOnlyDict({ 'device_class': 'connectivity', 'friendly_name': '10.10.10.10', - 'round_trip_time_avg': 4.333, + 'round_trip_time_avg': 4.8, 'round_trip_time_max': 10, - 'round_trip_time_mdev': '', + 'round_trip_time_mdev': None, 'round_trip_time_min': 1, }), 'context': , 'entity_id': 'binary_sensor.10_10_10_10', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }) @@ -113,10 +114,15 @@ 'attributes': ReadOnlyDict({ 'device_class': 'connectivity', 'friendly_name': '10.10.10.10', + 'round_trip_time_avg': None, + 'round_trip_time_max': None, + 'round_trip_time_mdev': None, + 'round_trip_time_min': None, }), 'context': , 'entity_id': 'binary_sensor.10_10_10_10', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }) diff --git a/tests/components/ping/snapshots/test_sensor.ambr b/tests/components/ping/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..d1548f7559c --- /dev/null +++ b/tests/components/ping/snapshots/test_sensor.ambr @@ -0,0 +1,157 @@ +# serializer version: 1 +# name: test_setup_and_update[round_trip_time_average] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.10_10_10_10_round_trip_time_average', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Round Trip Time Average', + 'platform': 'ping', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'round_trip_time_avg', + 'unit_of_measurement': , + }) +# --- +# name: test_setup_and_update[round_trip_time_average].1 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': '10.10.10.10 Round Trip Time Average', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.10_10_10_10_round_trip_time_average', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '4.8', + }) +# --- +# name: test_setup_and_update[round_trip_time_maximum] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.10_10_10_10_round_trip_time_maximum', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Round Trip Time Maximum', + 'platform': 'ping', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'round_trip_time_max', + 'unit_of_measurement': , + }) +# --- +# name: test_setup_and_update[round_trip_time_maximum].1 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': '10.10.10.10 Round Trip Time Maximum', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.10_10_10_10_round_trip_time_maximum', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '10', + }) +# --- +# name: test_setup_and_update[round_trip_time_mean_deviation] + None +# --- +# name: test_setup_and_update[round_trip_time_mean_deviation].1 + None +# --- +# name: test_setup_and_update[round_trip_time_minimum] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.10_10_10_10_round_trip_time_minimum', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Round Trip Time Minimum', + 'platform': 'ping', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'round_trip_time_min', + 'unit_of_measurement': , + }) +# --- +# name: test_setup_and_update[round_trip_time_minimum].1 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': '10.10.10.10 Round Trip Time Minimum', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.10_10_10_10_round_trip_time_minimum', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1', + }) +# --- diff --git a/tests/components/ping/test_binary_sensor.py b/tests/components/ping/test_binary_sensor.py index 68f647008e3..a8346b9a634 100644 --- a/tests/components/ping/test_binary_sensor.py +++ b/tests/components/ping/test_binary_sensor.py @@ -1,4 +1,5 @@ """Test the binary sensor platform of ping.""" + from datetime import timedelta from unittest.mock import patch diff --git a/tests/components/ping/test_config_flow.py b/tests/components/ping/test_config_flow.py index 8757a5b5e0d..541bdca8b1e 100644 --- a/tests/components/ping/test_config_flow.py +++ b/tests/components/ping/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Ping (ICMP) config flow.""" + from __future__ import annotations import pytest @@ -16,7 +17,7 @@ from tests.common import MockConfigEntry @pytest.mark.parametrize( ("host", "expected_title"), - (("192.618.178.1", "192.618.178.1"),), + [("192.618.178.1", "192.618.178.1")], ) @pytest.mark.usefixtures("patch_setup") async def test_form(hass: HomeAssistant, host, expected_title) -> None: @@ -48,7 +49,7 @@ async def test_form(hass: HomeAssistant, host, expected_title) -> None: @pytest.mark.parametrize( ("host", "count", "expected_title"), - (("192.618.178.1", 10, "192.618.178.1"),), + [("192.618.178.1", 10, "192.618.178.1")], ) @pytest.mark.usefixtures("patch_setup") async def test_options(hass: HomeAssistant, host, count, expected_title) -> None: diff --git a/tests/components/ping/test_device_tracker.py b/tests/components/ping/test_device_tracker.py index de6b4918262..a01bd0fa1bf 100644 --- a/tests/components/ping/test_device_tracker.py +++ b/tests/components/ping/test_device_tracker.py @@ -1,4 +1,5 @@ """Test the binary sensor platform of ping.""" + from collections.abc import Generator from datetime import timedelta from unittest.mock import patch @@ -122,9 +123,12 @@ async def test_import_delete_known_devices( } files = {legacy.YAML_DEVICES: dump(yaml_devices)} - with patch_yaml_files(files, True), patch( - "homeassistant.components.ping.device_tracker.remove_device_from_config" - ) as remove_device_from_config: + with ( + patch_yaml_files(files, True), + patch( + "homeassistant.components.ping.device_tracker.remove_device_from_config" + ) as remove_device_from_config, + ): await async_setup_component( hass, "device_tracker", diff --git a/tests/components/ping/test_sensor.py b/tests/components/ping/test_sensor.py new file mode 100644 index 00000000000..5c4833aaf06 --- /dev/null +++ b/tests/components/ping/test_sensor.py @@ -0,0 +1,33 @@ +"""Test sensor platform of Ping.""" + +import pytest +from syrupy import SnapshotAssertion +from syrupy.filters import props + +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default", "setup_integration") +@pytest.mark.parametrize( + "sensor_name", + [ + "round_trip_time_average", + "round_trip_time_maximum", + "round_trip_time_mean_deviation", # should be None in the snapshot + "round_trip_time_minimum", + ], +) +async def test_setup_and_update( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, + sensor_name: str, +) -> None: + """Test sensor setup and update.""" + + entry = entity_registry.async_get(f"sensor.10_10_10_10_{sensor_name}") + assert entry == snapshot(exclude=props("unique_id")) + + state = hass.states.get(f"sensor.10_10_10_10_{sensor_name}") + assert state == snapshot diff --git a/tests/components/pjlink/test_media_player.py b/tests/components/pjlink/test_media_player.py index 941a3cefe3a..d44bc942290 100644 --- a/tests/components/pjlink/test_media_player.py +++ b/tests/components/pjlink/test_media_player.py @@ -1,4 +1,5 @@ """Test the pjlink media player platform.""" + from datetime import timedelta import socket from unittest.mock import create_autospec, patch @@ -207,7 +208,7 @@ async def test_update_unavailable(projector_from_address, hass: HomeAssistant) - projector_from_address.side_effect = socket.timeout async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get("media_player.test") assert state.state == "unavailable" @@ -236,7 +237,7 @@ async def test_unavailable_time(mocked_projector, hass: HomeAssistant) -> None: mocked_projector.get_power.side_effect = ProjectorError("unavailable time") async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get("media_player.test") assert state.state == "off" diff --git a/tests/components/plaato/test_config_flow.py b/tests/components/plaato/test_config_flow.py index 8e319e0e257..7088b672f69 100644 --- a/tests/components/plaato/test_config_flow.py +++ b/tests/components/plaato/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Plaato config flow.""" + from unittest.mock import patch from pyplaato.models.device import PlaatoDeviceType @@ -26,10 +27,15 @@ UNIQUE_ID = "plaato_unique_id" @pytest.fixture(name="webhook_id") def mock_webhook_id(): """Mock webhook_id.""" - with patch( - "homeassistant.components.webhook.async_generate_id", return_value=WEBHOOK_ID - ), patch( - "homeassistant.components.webhook.async_generate_url", return_value="hook_id" + with ( + patch( + "homeassistant.components.webhook.async_generate_id", + return_value=WEBHOOK_ID, + ), + patch( + "homeassistant.components.webhook.async_generate_url", + return_value="hook_id", + ), ): yield @@ -100,15 +106,17 @@ async def test_show_config_form_validate_webhook( assert result["step_id"] == "api_method" assert await async_setup_component(hass, "cloud", {}) - with patch( - "homeassistant.components.cloud.async_active_subscription", return_value=True - ), patch( - "homeassistant.components.cloud.async_is_logged_in", return_value=True - ), patch( - "homeassistant.components.cloud.async_is_connected", return_value=True - ), patch( - "hass_nabucasa.cloudhooks.Cloudhooks.async_create", - return_value={"cloudhook_url": "https://hooks.nabu.casa/ABCD"}, + with ( + patch( + "homeassistant.components.cloud.async_active_subscription", + return_value=True, + ), + patch("homeassistant.components.cloud.async_is_logged_in", return_value=True), + patch("homeassistant.components.cloud.async_is_connected", return_value=True), + patch( + "hass_nabucasa.cloudhooks.Cloudhooks.async_create", + return_value={"cloudhook_url": "https://hooks.nabu.casa/ABCD"}, + ), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -146,15 +154,17 @@ async def test_show_config_form_validate_webhook_not_connected( assert result["step_id"] == "api_method" assert await async_setup_component(hass, "cloud", {}) - with patch( - "homeassistant.components.cloud.async_active_subscription", return_value=True - ), patch( - "homeassistant.components.cloud.async_is_logged_in", return_value=True - ), patch( - "homeassistant.components.cloud.async_is_connected", return_value=False - ), patch( - "hass_nabucasa.cloudhooks.Cloudhooks.async_create", - return_value={"cloudhook_url": "https://hooks.nabu.casa/ABCD"}, + with ( + patch( + "homeassistant.components.cloud.async_active_subscription", + return_value=True, + ), + patch("homeassistant.components.cloud.async_is_logged_in", return_value=True), + patch("homeassistant.components.cloud.async_is_connected", return_value=False), + patch( + "hass_nabucasa.cloudhooks.Cloudhooks.async_create", + return_value={"cloudhook_url": "https://hooks.nabu.casa/ABCD"}, + ), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], diff --git a/tests/components/plant/test_init.py b/tests/components/plant/test_init.py index f1ab4e5963f..d173544284d 100644 --- a/tests/components/plant/test_init.py +++ b/tests/components/plant/test_init.py @@ -1,4 +1,5 @@ """Unit tests for platform/plant.py.""" + from datetime import datetime, timedelta import homeassistant.components.plant as plant diff --git a/tests/components/plex/conftest.py b/tests/components/plex/conftest.py index 92818633df4..d6c91a9d9a8 100644 --- a/tests/components/plex/conftest.py +++ b/tests/components/plex/conftest.py @@ -1,4 +1,5 @@ """Fixtures for Plex tests.""" + from collections.abc import Generator from unittest.mock import AsyncMock, patch diff --git a/tests/components/plex/const.py b/tests/components/plex/const.py index ff9d3c0e9b3..f1399eb9d7d 100644 --- a/tests/components/plex/const.py +++ b/tests/components/plex/const.py @@ -1,4 +1,5 @@ """Constants used by Plex tests.""" + from homeassistant.components.plex import const from homeassistant.const import ( CONF_CLIENT_ID, diff --git a/tests/components/plex/helpers.py b/tests/components/plex/helpers.py index 246922bccae..00d0a4539c1 100644 --- a/tests/components/plex/helpers.py +++ b/tests/components/plex/helpers.py @@ -1,4 +1,5 @@ """Helper methods for Plex tests.""" + from datetime import timedelta from plexwebsocket import SIGNAL_CONNECTION_STATE, STATE_CONNECTED diff --git a/tests/components/plex/test_browse_media.py b/tests/components/plex/test_browse_media.py index af80b37a4af..11eb73ad608 100644 --- a/tests/components/plex/test_browse_media.py +++ b/tests/components/plex/test_browse_media.py @@ -1,4 +1,5 @@ """Tests for Plex media browser.""" + from http import HTTPStatus from unittest.mock import Mock, patch diff --git a/tests/components/plex/test_button.py b/tests/components/plex/test_button.py index a37a3ea2df2..8033cacb0df 100644 --- a/tests/components/plex/test_button.py +++ b/tests/components/plex/test_button.py @@ -1,4 +1,5 @@ """Tests for Plex buttons.""" + from datetime import timedelta from unittest.mock import patch diff --git a/tests/components/plex/test_config_flow.py b/tests/components/plex/test_config_flow.py index b2a4f3c23a5..9cfcda1b29d 100644 --- a/tests/components/plex/test_config_flow.py +++ b/tests/components/plex/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for Plex config flow.""" + import copy from http import HTTPStatus import ssl @@ -58,10 +59,12 @@ async def test_bad_credentials( assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" - with patch( - "plexapi.myplex.MyPlexAccount", side_effect=plexapi.exceptions.Unauthorized - ), patch("plexauth.PlexAuth.initiate_auth"), patch( - "plexauth.PlexAuth.token", return_value="BAD TOKEN" + with ( + patch( + "plexapi.myplex.MyPlexAccount", side_effect=plexapi.exceptions.Unauthorized + ), + patch("plexauth.PlexAuth.initiate_auth"), + patch("plexauth.PlexAuth.token", return_value="BAD TOKEN"), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} @@ -88,11 +91,13 @@ async def test_bad_hostname( assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" - with patch( - "plexapi.myplex.MyPlexResource.connect", - side_effect=requests.exceptions.ConnectionError, - ), patch("plexauth.PlexAuth.initiate_auth"), patch( - "plexauth.PlexAuth.token", return_value=MOCK_TOKEN + with ( + patch( + "plexapi.myplex.MyPlexResource.connect", + side_effect=requests.exceptions.ConnectionError, + ), + patch("plexauth.PlexAuth.initiate_auth"), + patch("plexauth.PlexAuth.token", return_value=MOCK_TOKEN), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} @@ -119,9 +124,11 @@ async def test_unknown_exception( assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" - with patch("plexapi.myplex.MyPlexAccount", side_effect=Exception), patch( - "plexauth.PlexAuth.initiate_auth" - ), patch("plexauth.PlexAuth.token", return_value="MOCK_TOKEN"): + with ( + patch("plexapi.myplex.MyPlexAccount", side_effect=Exception), + patch("plexauth.PlexAuth.initiate_auth"), + patch("plexauth.PlexAuth.token", return_value="MOCK_TOKEN"), + ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) @@ -151,8 +158,9 @@ async def test_no_servers_found( assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" - with patch("plexauth.PlexAuth.initiate_auth"), patch( - "plexauth.PlexAuth.token", return_value=MOCK_TOKEN + with ( + patch("plexauth.PlexAuth.initiate_auth"), + patch("plexauth.PlexAuth.token", return_value=MOCK_TOKEN), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} @@ -181,8 +189,9 @@ async def test_single_available_server( assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" - with patch("plexauth.PlexAuth.initiate_auth"), patch( - "plexauth.PlexAuth.token", return_value=MOCK_TOKEN + with ( + patch("plexauth.PlexAuth.initiate_auth"), + patch("plexauth.PlexAuth.token", return_value=MOCK_TOKEN), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} @@ -228,8 +237,9 @@ async def test_multiple_servers_with_selection( "https://plex.tv/api/v2/resources", text=plextv_resources_two_servers, ) - with patch("plexauth.PlexAuth.initiate_auth"), patch( - "plexauth.PlexAuth.token", return_value=MOCK_TOKEN + with ( + patch("plexauth.PlexAuth.initiate_auth"), + patch("plexauth.PlexAuth.token", return_value=MOCK_TOKEN), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} @@ -293,8 +303,9 @@ async def test_adding_last_unconfigured_server( text=plextv_resources_two_servers, ) - with patch("plexauth.PlexAuth.initiate_auth"), patch( - "plexauth.PlexAuth.token", return_value=MOCK_TOKEN + with ( + patch("plexauth.PlexAuth.initiate_auth"), + patch("plexauth.PlexAuth.token", return_value=MOCK_TOKEN), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} @@ -352,8 +363,9 @@ async def test_all_available_servers_configured( text=plextv_resources_two_servers, ) - with patch("plexauth.PlexAuth.initiate_auth"), patch( - "plexauth.PlexAuth.token", return_value=MOCK_TOKEN + with ( + patch("plexauth.PlexAuth.initiate_auth"), + patch("plexauth.PlexAuth.token", return_value=MOCK_TOKEN), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} @@ -477,8 +489,9 @@ async def test_external_timed_out( assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" - with patch("plexauth.PlexAuth.initiate_auth"), patch( - "plexauth.PlexAuth.token", return_value=None + with ( + patch("plexauth.PlexAuth.initiate_auth"), + patch("plexauth.PlexAuth.token", return_value=None), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} @@ -505,8 +518,9 @@ async def test_callback_view( assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" - with patch("plexauth.PlexAuth.initiate_auth"), patch( - "plexauth.PlexAuth.token", return_value=MOCK_TOKEN + with ( + patch("plexauth.PlexAuth.initiate_auth"), + patch("plexauth.PlexAuth.token", return_value=MOCK_TOKEN), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} @@ -636,8 +650,9 @@ async def test_manual_config( assert result["step_id"] == "manual_setup" assert result["errors"]["base"] == "ssl_error" - with patch("homeassistant.components.plex.PlexWebsocket", autospec=True), patch( - "homeassistant.components.plex.GDM", return_value=MockGDM(disabled=True) + with ( + patch("homeassistant.components.plex.PlexWebsocket", autospec=True), + patch("homeassistant.components.plex.GDM", return_value=MockGDM(disabled=True)), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MANUAL_SERVER @@ -677,9 +692,10 @@ async def test_manual_config_with_token( assert result["type"] == FlowResultType.FORM assert result["step_id"] == "manual_setup" - with patch( - "homeassistant.components.plex.GDM", return_value=MockGDM(disabled=True) - ), patch("homeassistant.components.plex.PlexWebsocket", autospec=True): + with ( + patch("homeassistant.components.plex.GDM", return_value=MockGDM(disabled=True)), + patch("homeassistant.components.plex.PlexWebsocket", autospec=True), + ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_TOKEN: MOCK_TOKEN} ) @@ -740,8 +756,9 @@ async def test_reauth( ) flow_id = result["flow_id"] - with patch("plexauth.PlexAuth.initiate_auth"), patch( - "plexauth.PlexAuth.token", return_value="BRAND_NEW_TOKEN" + with ( + patch("plexauth.PlexAuth.initiate_auth"), + patch("plexauth.PlexAuth.token", return_value="BRAND_NEW_TOKEN"), ): result = await hass.config_entries.flow.async_configure(flow_id, user_input={}) assert result["type"] == FlowResultType.EXTERNAL_STEP @@ -791,8 +808,9 @@ async def test_reauth_multiple_servers_available( flow_id = result["flow_id"] - with patch("plexauth.PlexAuth.initiate_auth"), patch( - "plexauth.PlexAuth.token", return_value="BRAND_NEW_TOKEN" + with ( + patch("plexauth.PlexAuth.initiate_auth"), + patch("plexauth.PlexAuth.token", return_value="BRAND_NEW_TOKEN"), ): result = await hass.config_entries.flow.async_configure(flow_id, user_input={}) assert result["type"] == FlowResultType.EXTERNAL_STEP @@ -825,9 +843,11 @@ async def test_client_request_missing(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" - with patch("plexauth.PlexAuth.initiate_auth"), patch( - "plexauth.PlexAuth.token", return_value=None - ), pytest.raises(RuntimeError): + with ( + patch("plexauth.PlexAuth.initiate_auth"), + patch("plexauth.PlexAuth.token", return_value=None), + pytest.raises(RuntimeError), + ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) @@ -847,12 +867,16 @@ async def test_client_header_issues( assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" - with patch("plexauth.PlexAuth.initiate_auth"), patch( - "plexauth.PlexAuth.token", return_value=None - ), patch( - "homeassistant.components.http.current_request.get", return_value=MockRequest() - ), pytest.raises( - RuntimeError, + with ( + patch("plexauth.PlexAuth.initiate_auth"), + patch("plexauth.PlexAuth.token", return_value=None), + patch( + "homeassistant.components.http.current_request.get", + return_value=MockRequest(), + ), + pytest.raises( + RuntimeError, + ), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} diff --git a/tests/components/plex/test_device_handling.py b/tests/components/plex/test_device_handling.py index 5887079ce21..c3c26ec0bdd 100644 --- a/tests/components/plex/test_device_handling.py +++ b/tests/components/plex/test_device_handling.py @@ -1,4 +1,5 @@ """Tests for handling the device registry.""" + import requests_mock from homeassistant.components.plex.const import DOMAIN diff --git a/tests/components/plex/test_init.py b/tests/components/plex/test_init.py index 47bdc472b2b..a1a05db9d9a 100644 --- a/tests/components/plex/test_init.py +++ b/tests/components/plex/test_init.py @@ -1,4 +1,5 @@ """Tests for Plex setup.""" + import copy from datetime import timedelta from http import HTTPStatus @@ -350,9 +351,13 @@ async def test_trigger_reauth( assert entry.state is ConfigEntryState.LOADED - with patch( - "plexapi.server.PlexServer.clients", side_effect=plexapi.exceptions.Unauthorized - ), patch("plexapi.server.PlexServer", side_effect=plexapi.exceptions.Unauthorized): + with ( + patch( + "plexapi.server.PlexServer.clients", + side_effect=plexapi.exceptions.Unauthorized, + ), + patch("plexapi.server.PlexServer", side_effect=plexapi.exceptions.Unauthorized), + ): trigger_plex_update(mock_websocket) await wait_for_debouncer(hass) diff --git a/tests/components/plex/test_media_players.py b/tests/components/plex/test_media_players.py index e9efc945f71..6e6fe29e4d4 100644 --- a/tests/components/plex/test_media_players.py +++ b/tests/components/plex/test_media_players.py @@ -1,4 +1,5 @@ """Tests for Plex media_players.""" + from unittest.mock import patch from plexapi.exceptions import NotFound diff --git a/tests/components/plex/test_media_search.py b/tests/components/plex/test_media_search.py index 21b50724786..5578ecd2550 100644 --- a/tests/components/plex/test_media_search.py +++ b/tests/components/plex/test_media_search.py @@ -1,4 +1,5 @@ """Tests for Plex server.""" + from unittest.mock import patch from plexapi.exceptions import BadRequest, NotFound @@ -40,8 +41,9 @@ async def test_media_lookups( }, True, ) - with pytest.raises(MediaNotFound) as excinfo, patch( - "plexapi.server.PlexServer.fetchItem", side_effect=NotFound + with ( + pytest.raises(MediaNotFound) as excinfo, + patch("plexapi.server.PlexServer.fetchItem", side_effect=NotFound), ): await hass.services.async_call( MEDIA_PLAYER_DOMAIN, @@ -246,7 +248,7 @@ async def test_media_lookups( }, True, ) - search.assert_called_with(**{"title": "Movie 1", "libtype": None}) + search.assert_called_with(title="Movie 1", libtype=None) with pytest.raises(MediaNotFound) as excinfo: payload = '{"title": "Movie 1"}' diff --git a/tests/components/plex/test_playback.py b/tests/components/plex/test_playback.py index 9ea684256c4..33c8b130749 100644 --- a/tests/components/plex/test_playback.py +++ b/tests/components/plex/test_playback.py @@ -1,4 +1,5 @@ """Tests for Plex player playback methods/services.""" + from http import HTTPStatus from unittest.mock import Mock, patch @@ -64,11 +65,14 @@ async def test_media_player_playback( # Test media lookup failure payload = '{"library_name": "Movies", "title": "Movie 1" }' - with patch( - "plexapi.library.LibrarySection.search", - return_value=None, - __qualname__="search", - ), pytest.raises(HomeAssistantError) as excinfo: + with ( + patch( + "plexapi.library.LibrarySection.search", + return_value=None, + __qualname__="search", + ), + pytest.raises(HomeAssistantError) as excinfo, + ): await hass.services.async_call( MP_DOMAIN, SERVICE_PLAY_MEDIA, @@ -215,13 +219,16 @@ async def test_media_player_playback( # Test multiple choices with allow_multiple movies = [movie1, movie2, movie3] - with patch( - "plexapi.library.LibrarySection.search", - return_value=movies, - __qualname__="search", - ), patch( - "homeassistant.components.plex.server.PlexServer.create_playqueue" - ) as mock_create_playqueue: + with ( + patch( + "plexapi.library.LibrarySection.search", + return_value=movies, + __qualname__="search", + ), + patch( + "homeassistant.components.plex.server.PlexServer.create_playqueue" + ) as mock_create_playqueue, + ): await hass.services.async_call( MP_DOMAIN, SERVICE_PLAY_MEDIA, diff --git a/tests/components/plex/test_sensor.py b/tests/components/plex/test_sensor.py index 93014dfedd1..6002429e84d 100644 --- a/tests/components/plex/test_sensor.py +++ b/tests/components/plex/test_sensor.py @@ -1,4 +1,5 @@ """Tests for Plex sensors.""" + from datetime import datetime, timedelta from http import HTTPStatus from unittest.mock import patch diff --git a/tests/components/plex/test_server.py b/tests/components/plex/test_server.py index 64abe27b16b..ec798af6d03 100644 --- a/tests/components/plex/test_server.py +++ b/tests/components/plex/test_server.py @@ -1,4 +1,5 @@ """Tests for Plex server.""" + import copy from unittest.mock import patch diff --git a/tests/components/plex/test_services.py b/tests/components/plex/test_services.py index dfd02bb1d3f..c84322e1c14 100644 --- a/tests/components/plex/test_services.py +++ b/tests/components/plex/test_services.py @@ -1,4 +1,5 @@ """Tests for various Plex services.""" + from http import HTTPStatus from unittest.mock import patch diff --git a/tests/components/plex/test_update.py b/tests/components/plex/test_update.py index ce50f67a0d9..a96e0409bbb 100644 --- a/tests/components/plex/test_update.py +++ b/tests/components/plex/test_update.py @@ -1,4 +1,5 @@ """Tests for update entities.""" + import pytest import requests_mock diff --git a/tests/components/plugwise/conftest.py b/tests/components/plugwise/conftest.py index 4d81956eacb..c211cd0a741 100644 --- a/tests/components/plugwise/conftest.py +++ b/tests/components/plugwise/conftest.py @@ -1,4 +1,5 @@ """Setup mocks for the Plugwise integration tests.""" + from __future__ import annotations from collections.abc import Generator @@ -86,10 +87,7 @@ def mock_smile_adam() -> Generator[None, MagicMock, None]: smile.smile_hostname = "smile98765" smile.smile_model = "Gateway" smile.smile_name = "Adam" - smile.connect.return_value = True - - smile.notifications = _read_json(chosen_env, "notifications") all_data = _read_json(chosen_env, "all_data") smile.async_update.return_value = PlugwiseData( all_data["gateway"], all_data["devices"] @@ -115,10 +113,7 @@ def mock_smile_adam_2() -> Generator[None, MagicMock, None]: smile.smile_hostname = "smile98765" smile.smile_model = "Gateway" smile.smile_name = "Adam" - smile.connect.return_value = True - - smile.notifications = _read_json(chosen_env, "notifications") all_data = _read_json(chosen_env, "all_data") smile.async_update.return_value = PlugwiseData( all_data["gateway"], all_data["devices"] @@ -144,10 +139,7 @@ def mock_smile_adam_3() -> Generator[None, MagicMock, None]: smile.smile_hostname = "smile98765" smile.smile_model = "Gateway" smile.smile_name = "Adam" - smile.connect.return_value = True - - smile.notifications = _read_json(chosen_env, "notifications") all_data = _read_json(chosen_env, "all_data") smile.async_update.return_value = PlugwiseData( all_data["gateway"], all_data["devices"] @@ -173,10 +165,7 @@ def mock_smile_adam_4() -> Generator[None, MagicMock, None]: smile.smile_hostname = "smile98765" smile.smile_model = "Gateway" smile.smile_name = "Adam" - smile.connect.return_value = True - - smile.notifications = _read_json(chosen_env, "notifications") all_data = _read_json(chosen_env, "all_data") smile.async_update.return_value = PlugwiseData( all_data["gateway"], all_data["devices"] @@ -201,10 +190,7 @@ def mock_smile_anna() -> Generator[None, MagicMock, None]: smile.smile_hostname = "smile98765" smile.smile_model = "Gateway" smile.smile_name = "Smile Anna" - smile.connect.return_value = True - - smile.notifications = _read_json(chosen_env, "notifications") all_data = _read_json(chosen_env, "all_data") smile.async_update.return_value = PlugwiseData( all_data["gateway"], all_data["devices"] @@ -229,10 +215,7 @@ def mock_smile_anna_2() -> Generator[None, MagicMock, None]: smile.smile_hostname = "smile98765" smile.smile_model = "Gateway" smile.smile_name = "Smile Anna" - smile.connect.return_value = True - - smile.notifications = _read_json(chosen_env, "notifications") all_data = _read_json(chosen_env, "all_data") smile.async_update.return_value = PlugwiseData( all_data["gateway"], all_data["devices"] @@ -257,10 +240,7 @@ def mock_smile_anna_3() -> Generator[None, MagicMock, None]: smile.smile_hostname = "smile98765" smile.smile_model = "Gateway" smile.smile_name = "Smile Anna" - smile.connect.return_value = True - - smile.notifications = _read_json(chosen_env, "notifications") all_data = _read_json(chosen_env, "all_data") smile.async_update.return_value = PlugwiseData( all_data["gateway"], all_data["devices"] @@ -272,23 +252,20 @@ def mock_smile_anna_3() -> Generator[None, MagicMock, None]: @pytest.fixture def mock_smile_p1() -> Generator[None, MagicMock, None]: """Create a Mock P1 DSMR environment for testing exceptions.""" - chosen_env = "p1v3_full_option" + chosen_env = "p1v4_442_single" with patch( "homeassistant.components.plugwise.coordinator.Smile", autospec=True ) as smile_mock: smile = smile_mock.return_value - smile.gateway_id = "e950c7d5e1ee407a858e2a8b5016c8b3" + smile.gateway_id = "a455b61e52394b2db5081ce025a430f3" smile.heater_id = None - smile.smile_version = "3.3.9" + smile.smile_version = "4.4.2" smile.smile_type = "power" smile.smile_hostname = "smile98765" smile.smile_model = "Gateway" smile.smile_name = "Smile P1" - smile.connect.return_value = True - - smile.notifications = _read_json(chosen_env, "notifications") all_data = _read_json(chosen_env, "all_data") smile.async_update.return_value = PlugwiseData( all_data["gateway"], all_data["devices"] @@ -313,10 +290,7 @@ def mock_smile_p1_2() -> Generator[None, MagicMock, None]: smile.smile_hostname = "smile98765" smile.smile_model = "Gateway" smile.smile_name = "Smile P1" - smile.connect.return_value = True - - smile.notifications = _read_json(chosen_env, "notifications") all_data = _read_json(chosen_env, "all_data") smile.async_update.return_value = PlugwiseData( all_data["gateway"], all_data["devices"] @@ -341,7 +315,6 @@ def mock_stretch() -> Generator[None, MagicMock, None]: smile.smile_hostname = "stretch98765" smile.smile_model = "Gateway" smile.smile_name = "Stretch" - smile.connect.return_value = True all_data = _read_json(chosen_env, "all_data") smile.async_update.return_value = PlugwiseData( diff --git a/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/all_data.json b/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/all_data.json index f97182782e6..47c8e4dceb0 100644 --- a/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/all_data.json +++ b/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/all_data.json @@ -28,7 +28,7 @@ "model": "Plug", "name": "Playstation Smart Plug", "sensors": { - "electricity_consumed": 82.6, + "electricity_consumed": 84.1, "electricity_consumed_interval": 8.6, "electricity_produced": 0.0, "electricity_produced_interval": 0.0 diff --git a/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/device_list.json b/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/device_list.json deleted file mode 100644 index 104a723e463..00000000000 --- a/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/device_list.json +++ /dev/null @@ -1,20 +0,0 @@ -[ - "fe799307f1624099878210aa0b9f1475", - "90986d591dcd426cae3ec3e8111ff730", - "df4a4a8169904cdb9c03d61a21f42140", - "b310b72a0e354bfab43089919b9a88bf", - "a2c3583e0a6349358998b760cea82d2a", - "b59bcebaf94b499ea7d46e4a66fb62d8", - "d3da73bde12a47d5a6b8f9dad971f2ec", - "21f2b542c49845e6bb416884c55778d6", - "78d1126fc4c743db81b61c20e88342a7", - "cd0ddb54ef694e11ac18ed1cbce5dbbd", - "4a810418d5394b3f82727340b91ba740", - "02cf28bfec924855854c544690a609ef", - "a28f588dc4a049a483fd03a30361ad3a", - "6a3bf693d05e48e0b460c815a4fdd09d", - "680423ff840043738f42cc7f1ff97a36", - "f1fee6043d3642a9b0a65297455f008e", - "675416a629f343c495449970e2ca37b5", - "e7693eb9582644e5b865dba8d4447cf1" -] diff --git a/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/notifications.json b/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/notifications.json deleted file mode 100644 index 8749be4c345..00000000000 --- a/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/notifications.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "af82e4ccf9c548528166d38e560662a4": { - "warning": "Node Plug (with MAC address 000D6F000D13CB01, in room 'n.a.') has been unreachable since 23:03 2020-01-18. Please check the connection and restart the device." - } -} diff --git a/tests/components/plugwise/fixtures/anna_heatpump_heating/device_list.json b/tests/components/plugwise/fixtures/anna_heatpump_heating/device_list.json deleted file mode 100644 index ffb8cf62575..00000000000 --- a/tests/components/plugwise/fixtures/anna_heatpump_heating/device_list.json +++ /dev/null @@ -1,5 +0,0 @@ -[ - "015ae9ea3f964e668e490fa39da3870b", - "1cbf783bb11e4a7c8a6843dee3a86927", - "3cb70739631c4d17a86b8b12e8a5161b" -] diff --git a/tests/components/plugwise/fixtures/m_adam_cooling/device_list.json b/tests/components/plugwise/fixtures/m_adam_cooling/device_list.json deleted file mode 100644 index 35fe367eb34..00000000000 --- a/tests/components/plugwise/fixtures/m_adam_cooling/device_list.json +++ /dev/null @@ -1,8 +0,0 @@ -[ - "da224107914542988a88561b4452b0f6", - "056ee145a816487eaa69243c3280f8bf", - "e2f4322d57924fa090fbbc48b3a140dc", - "ad4838d7d35c4d6ea796ee12ae5aedf8", - "1772a4ea304041adb83f357b751341ff", - "e8ef2a01ed3b4139a53bf749204fe6b4" -] diff --git a/tests/components/plugwise/fixtures/m_adam_heating/device_list.json b/tests/components/plugwise/fixtures/m_adam_heating/device_list.json deleted file mode 100644 index 35fe367eb34..00000000000 --- a/tests/components/plugwise/fixtures/m_adam_heating/device_list.json +++ /dev/null @@ -1,8 +0,0 @@ -[ - "da224107914542988a88561b4452b0f6", - "056ee145a816487eaa69243c3280f8bf", - "e2f4322d57924fa090fbbc48b3a140dc", - "ad4838d7d35c4d6ea796ee12ae5aedf8", - "1772a4ea304041adb83f357b751341ff", - "e8ef2a01ed3b4139a53bf749204fe6b4" -] diff --git a/tests/components/plugwise/fixtures/m_adam_heating/notifications.json b/tests/components/plugwise/fixtures/m_adam_heating/notifications.json deleted file mode 100644 index 0967ef424bc..00000000000 --- a/tests/components/plugwise/fixtures/m_adam_heating/notifications.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/tests/components/plugwise/fixtures/m_adam_jip/all_data.json b/tests/components/plugwise/fixtures/m_adam_jip/all_data.json index 915f438c105..378a5e0a760 100644 --- a/tests/components/plugwise/fixtures/m_adam_jip/all_data.json +++ b/tests/components/plugwise/fixtures/m_adam_jip/all_data.json @@ -67,7 +67,7 @@ "name": "Tom Slaapkamer", "sensors": { "setpoint": 13.0, - "temperature": 24.3, + "temperature": 24.2, "temperature_difference": 1.7, "valve_position": 0.0 }, diff --git a/tests/components/plugwise/fixtures/m_adam_jip/device_list.json b/tests/components/plugwise/fixtures/m_adam_jip/device_list.json deleted file mode 100644 index 049845bc828..00000000000 --- a/tests/components/plugwise/fixtures/m_adam_jip/device_list.json +++ /dev/null @@ -1,13 +0,0 @@ -[ - "b5c2386c6f6342669e50fe49dd05b188", - "e4684553153b44afbef2200885f379dc", - "a6abc6a129ee499c88a4d420cc413b47", - "1346fbd8498d4dbcab7e18d51b771f3d", - "833de10f269c4deab58fb9df69901b4e", - "6f3e9d7084214c21b9dfa46f6eeb8700", - "f61f1a2535f54f52ad006a3d18e459ca", - "d4496250d0e942cfa7aea3476e9070d5", - "356b65335e274d769c338223e7af9c33", - "1da4d325838e4ad8aac12177214505c9", - "457ce8414de24596a2d5e7dbc9c7682f" -] diff --git a/tests/components/plugwise/fixtures/m_adam_jip/notifications.json b/tests/components/plugwise/fixtures/m_adam_jip/notifications.json deleted file mode 100644 index 0967ef424bc..00000000000 --- a/tests/components/plugwise/fixtures/m_adam_jip/notifications.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/tests/components/plugwise/fixtures/m_anna_heatpump_cooling/device_list.json b/tests/components/plugwise/fixtures/m_anna_heatpump_cooling/device_list.json deleted file mode 100644 index ffb8cf62575..00000000000 --- a/tests/components/plugwise/fixtures/m_anna_heatpump_cooling/device_list.json +++ /dev/null @@ -1,5 +0,0 @@ -[ - "015ae9ea3f964e668e490fa39da3870b", - "1cbf783bb11e4a7c8a6843dee3a86927", - "3cb70739631c4d17a86b8b12e8a5161b" -] diff --git a/tests/components/plugwise/fixtures/m_anna_heatpump_cooling/notifications.json b/tests/components/plugwise/fixtures/m_anna_heatpump_cooling/notifications.json deleted file mode 100644 index 0967ef424bc..00000000000 --- a/tests/components/plugwise/fixtures/m_anna_heatpump_cooling/notifications.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/tests/components/plugwise/fixtures/m_anna_heatpump_idle/device_list.json b/tests/components/plugwise/fixtures/m_anna_heatpump_idle/device_list.json deleted file mode 100644 index ffb8cf62575..00000000000 --- a/tests/components/plugwise/fixtures/m_anna_heatpump_idle/device_list.json +++ /dev/null @@ -1,5 +0,0 @@ -[ - "015ae9ea3f964e668e490fa39da3870b", - "1cbf783bb11e4a7c8a6843dee3a86927", - "3cb70739631c4d17a86b8b12e8a5161b" -] diff --git a/tests/components/plugwise/fixtures/m_anna_heatpump_idle/notifications.json b/tests/components/plugwise/fixtures/m_anna_heatpump_idle/notifications.json deleted file mode 100644 index 0967ef424bc..00000000000 --- a/tests/components/plugwise/fixtures/m_anna_heatpump_idle/notifications.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/tests/components/plugwise/fixtures/p1v3_full_option/all_data.json b/tests/components/plugwise/fixtures/p1v3_full_option/all_data.json deleted file mode 100644 index 0a47893c077..00000000000 --- a/tests/components/plugwise/fixtures/p1v3_full_option/all_data.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "devices": { - "cd3e822288064775a7c4afcdd70bdda2": { - "binary_sensors": { - "plugwise_notification": false - }, - "dev_class": "gateway", - "firmware": "3.3.9", - "hardware": "AME Smile 2.0 board", - "location": "cd3e822288064775a7c4afcdd70bdda2", - "mac_address": "012345670001", - "model": "Gateway", - "name": "Smile P1", - "vendor": "Plugwise" - }, - "e950c7d5e1ee407a858e2a8b5016c8b3": { - "available": true, - "dev_class": "smartmeter", - "location": "cd3e822288064775a7c4afcdd70bdda2", - "model": "2M550E-1012", - "name": "P1", - "sensors": { - "electricity_consumed_off_peak_cumulative": 551.09, - "electricity_consumed_off_peak_interval": 0, - "electricity_consumed_off_peak_point": 0, - "electricity_consumed_peak_cumulative": 442.932, - "electricity_consumed_peak_interval": 0, - "electricity_consumed_peak_point": 0, - "electricity_produced_off_peak_cumulative": 154.491, - "electricity_produced_off_peak_interval": 0, - "electricity_produced_off_peak_point": 0, - "electricity_produced_peak_cumulative": 396.559, - "electricity_produced_peak_interval": 0, - "electricity_produced_peak_point": 2816, - "gas_consumed_cumulative": 584.85, - "gas_consumed_interval": 0.0, - "net_electricity_cumulative": 442.972, - "net_electricity_point": -2816 - }, - "vendor": "ISKRAEMECO" - } - }, - "gateway": { - "gateway_id": "cd3e822288064775a7c4afcdd70bdda2", - "item_count": 31, - "notifications": {}, - "smile_name": "Smile P1" - } -} diff --git a/tests/components/plugwise/fixtures/p1v3_full_option/device_list.json b/tests/components/plugwise/fixtures/p1v3_full_option/device_list.json deleted file mode 100644 index 8af35165c7e..00000000000 --- a/tests/components/plugwise/fixtures/p1v3_full_option/device_list.json +++ /dev/null @@ -1 +0,0 @@ -["cd3e822288064775a7c4afcdd70bdda2", "e950c7d5e1ee407a858e2a8b5016c8b3"] diff --git a/tests/components/plugwise/fixtures/p1v3_full_option/notifications.json b/tests/components/plugwise/fixtures/p1v3_full_option/notifications.json deleted file mode 100644 index 0967ef424bc..00000000000 --- a/tests/components/plugwise/fixtures/p1v3_full_option/notifications.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/tests/components/plugwise/fixtures/p1v4_442_single/all_data.json b/tests/components/plugwise/fixtures/p1v4_442_single/all_data.json new file mode 100644 index 00000000000..318035a5d2c --- /dev/null +++ b/tests/components/plugwise/fixtures/p1v4_442_single/all_data.json @@ -0,0 +1,49 @@ +{ + "devices": { + "a455b61e52394b2db5081ce025a430f3": { + "binary_sensors": { + "plugwise_notification": false + }, + "dev_class": "gateway", + "firmware": "4.4.2", + "hardware": "AME Smile 2.0 board", + "location": "a455b61e52394b2db5081ce025a430f3", + "mac_address": "012345670001", + "model": "Gateway", + "name": "Smile P1", + "vendor": "Plugwise" + }, + "ba4de7613517478da82dd9b6abea36af": { + "available": true, + "dev_class": "smartmeter", + "location": "a455b61e52394b2db5081ce025a430f3", + "model": "KFM5KAIFA-METER", + "name": "P1", + "sensors": { + "electricity_consumed_off_peak_cumulative": 17643.423, + "electricity_consumed_off_peak_interval": 15, + "electricity_consumed_off_peak_point": 486, + "electricity_consumed_peak_cumulative": 13966.608, + "electricity_consumed_peak_interval": 0, + "electricity_consumed_peak_point": 0, + "electricity_phase_one_consumed": 486, + "electricity_phase_one_produced": 0, + "electricity_produced_off_peak_cumulative": 0.0, + "electricity_produced_off_peak_interval": 0, + "electricity_produced_off_peak_point": 0, + "electricity_produced_peak_cumulative": 0.0, + "electricity_produced_peak_interval": 0, + "electricity_produced_peak_point": 0, + "net_electricity_cumulative": 31610.031, + "net_electricity_point": 486 + }, + "vendor": "SHENZHEN KAIFA TECHNOLOGY \uff08CHENGDU\uff09 CO., LTD." + } + }, + "gateway": { + "gateway_id": "a455b61e52394b2db5081ce025a430f3", + "item_count": 31, + "notifications": {}, + "smile_name": "Smile P1" + } +} diff --git a/tests/components/plugwise/fixtures/p1v4_442_triple/device_list.json b/tests/components/plugwise/fixtures/p1v4_442_triple/device_list.json deleted file mode 100644 index 7b301f50924..00000000000 --- a/tests/components/plugwise/fixtures/p1v4_442_triple/device_list.json +++ /dev/null @@ -1 +0,0 @@ -["03e65b16e4b247a29ae0d75a78cb492e", "b82b6b3322484f2ea4e25e0bd5f3d61f"] diff --git a/tests/components/plugwise/fixtures/p1v4_442_triple/notifications.json b/tests/components/plugwise/fixtures/p1v4_442_triple/notifications.json deleted file mode 100644 index 49db062035a..00000000000 --- a/tests/components/plugwise/fixtures/p1v4_442_triple/notifications.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "97a04c0c263049b29350a660b4cdd01e": { - "warning": "The Smile P1 is not connected to a smart meter." - } -} diff --git a/tests/components/plugwise/fixtures/stretch_v31/all_data.json b/tests/components/plugwise/fixtures/stretch_v31/all_data.json index 6b1012b0d87..f42cde65b39 100644 --- a/tests/components/plugwise/fixtures/stretch_v31/all_data.json +++ b/tests/components/plugwise/fixtures/stretch_v31/all_data.json @@ -23,7 +23,7 @@ "electricity_produced": 0.0 }, "switches": { - "lock": false, + "lock": true, "relay": true }, "vendor": "Plugwise", diff --git a/tests/components/plugwise/fixtures/stretch_v31/device_list.json b/tests/components/plugwise/fixtures/stretch_v31/device_list.json deleted file mode 100644 index b2c839ae9d3..00000000000 --- a/tests/components/plugwise/fixtures/stretch_v31/device_list.json +++ /dev/null @@ -1,10 +0,0 @@ -[ - "0000aaaa0000aaaa0000aaaa0000aa00", - "5871317346d045bc9f6b987ef25ee638", - "e1c884e7dede431dadee09506ec4f859", - "aac7b735042c4832ac9ff33aae4f453b", - "cfe95cf3de1948c0b8955125bf754614", - "059e4d03c7a34d278add5c7a4a781d19", - "d950b314e9d8499f968e6db8d82ef78c", - "d03738edfcc947f7b8f4573571d90d2d" -] diff --git a/tests/components/plugwise/fixtures/stretch_v31/notifications.json b/tests/components/plugwise/fixtures/stretch_v31/notifications.json deleted file mode 100644 index 0967ef424bc..00000000000 --- a/tests/components/plugwise/fixtures/stretch_v31/notifications.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/tests/components/plugwise/snapshots/test_diagnostics.ambr b/tests/components/plugwise/snapshots/test_diagnostics.ambr index c2bbea9418a..0fa3df4e660 100644 --- a/tests/components/plugwise/snapshots/test_diagnostics.ambr +++ b/tests/components/plugwise/snapshots/test_diagnostics.ambr @@ -30,7 +30,7 @@ 'model': 'Plug', 'name': 'Playstation Smart Plug', 'sensors': dict({ - 'electricity_consumed': 82.6, + 'electricity_consumed': 84.1, 'electricity_consumed_interval': 8.6, 'electricity_produced': 0.0, 'electricity_produced_interval': 0.0, diff --git a/tests/components/plugwise/test_climate.py b/tests/components/plugwise/test_climate.py index c5ab3a209c2..8041d2778ef 100644 --- a/tests/components/plugwise/test_climate.py +++ b/tests/components/plugwise/test_climate.py @@ -98,9 +98,9 @@ async def test_adam_3_climate_entity_attributes( HVACMode.COOL, ] data = mock_smile_adam_3.async_update.return_value - data.devices["da224107914542988a88561b4452b0f6"][ - "select_regulation_mode" - ] = "heating" + data.devices["da224107914542988a88561b4452b0f6"]["select_regulation_mode"] = ( + "heating" + ) data.devices["ad4838d7d35c4d6ea796ee12ae5aedf8"]["control_state"] = "heating" data.devices["056ee145a816487eaa69243c3280f8bf"]["binary_sensors"][ "cooling_state" @@ -121,9 +121,9 @@ async def test_adam_3_climate_entity_attributes( HVACMode.HEAT, ] data = mock_smile_adam_3.async_update.return_value - data.devices["da224107914542988a88561b4452b0f6"][ - "select_regulation_mode" - ] = "cooling" + data.devices["da224107914542988a88561b4452b0f6"]["select_regulation_mode"] = ( + "cooling" + ) data.devices["ad4838d7d35c4d6ea796ee12ae5aedf8"]["control_state"] = "cooling" data.devices["056ee145a816487eaa69243c3280f8bf"]["binary_sensors"][ "cooling_state" diff --git a/tests/components/plugwise/test_config_flow.py b/tests/components/plugwise/test_config_flow.py index 438ab1b0870..6e2f4e63d85 100644 --- a/tests/components/plugwise/test_config_flow.py +++ b/tests/components/plugwise/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Plugwise config flow.""" + from ipaddress import ip_address from unittest.mock import AsyncMock, MagicMock, patch diff --git a/tests/components/plugwise/test_diagnostics.py b/tests/components/plugwise/test_diagnostics.py index 045b8641f69..a2b0521d6e1 100644 --- a/tests/components/plugwise/test_diagnostics.py +++ b/tests/components/plugwise/test_diagnostics.py @@ -1,4 +1,5 @@ """Tests for the diagnostics data provided by the Plugwise integration.""" + from unittest.mock import MagicMock from syrupy import SnapshotAssertion diff --git a/tests/components/plugwise/test_init.py b/tests/components/plugwise/test_init.py index 1b5297b71d2..4eb0b2cb56a 100644 --- a/tests/components/plugwise/test_init.py +++ b/tests/components/plugwise/test_init.py @@ -1,4 +1,5 @@ """Tests for the Plugwise Climate integration.""" + from unittest.mock import MagicMock from plugwise.exceptions import ( diff --git a/tests/components/plugwise/test_sensor.py b/tests/components/plugwise/test_sensor.py index 46f31e1458f..d1df8454f4e 100644 --- a/tests/components/plugwise/test_sensor.py +++ b/tests/components/plugwise/test_sensor.py @@ -111,38 +111,28 @@ async def test_anna_as_smt_climate_sensor_entities( assert float(state.state) == 86.0 -async def test_anna_climate_sensor_entities( - hass: HomeAssistant, mock_smile_anna: MagicMock, init_integration: MockConfigEntry -) -> None: - """Test creation of climate related sensor entities.""" - state = hass.states.get("sensor.opentherm_outdoor_air_temperature") - assert state - assert float(state.state) == 3.0 - - async def test_p1_dsmr_sensor_entities( hass: HomeAssistant, mock_smile_p1: MagicMock, init_integration: MockConfigEntry ) -> None: """Test creation of power related sensor entities.""" state = hass.states.get("sensor.p1_net_electricity_point") assert state - assert float(state.state) == -2816.0 + assert int(state.state) == 486 state = hass.states.get("sensor.p1_electricity_consumed_off_peak_cumulative") assert state - assert float(state.state) == 551.09 + assert float(state.state) == 17643.423 state = hass.states.get("sensor.p1_electricity_produced_peak_point") assert state - assert float(state.state) == 2816.0 + assert int(state.state) == 0 state = hass.states.get("sensor.p1_electricity_consumed_peak_cumulative") assert state - assert float(state.state) == 442.932 + assert float(state.state) == 13966.608 state = hass.states.get("sensor.p1_gas_consumed_cumulative") - assert state - assert float(state.state) == 584.85 + assert not state async def test_p1_3ph_dsmr_sensor_entities( @@ -151,15 +141,15 @@ async def test_p1_3ph_dsmr_sensor_entities( """Test creation of power related sensor entities.""" state = hass.states.get("sensor.p1_electricity_phase_one_consumed") assert state - assert float(state.state) == 1763.0 + assert int(state.state) == 1763 state = hass.states.get("sensor.p1_electricity_phase_two_consumed") assert state - assert float(state.state) == 1703.0 + assert int(state.state) == 1703 state = hass.states.get("sensor.p1_electricity_phase_three_consumed") assert state - assert float(state.state) == 2080.0 + assert int(state.state) == 2080 entity_id = "sensor.p1_voltage_phase_one" state = hass.states.get(entity_id) diff --git a/tests/components/plugwise/test_switch.py b/tests/components/plugwise/test_switch.py index 2d47a420fe8..fa58bd4c8eb 100644 --- a/tests/components/plugwise/test_switch.py +++ b/tests/components/plugwise/test_switch.py @@ -1,4 +1,5 @@ """Tests for the Plugwise switch integration.""" + from unittest.mock import MagicMock from plugwise.exceptions import PlugwiseException diff --git a/tests/components/plum_lightpad/test_config_flow.py b/tests/components/plum_lightpad/test_config_flow.py index 40852094f5b..37934942734 100644 --- a/tests/components/plum_lightpad/test_config_flow.py +++ b/tests/components/plum_lightpad/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Plum Lightpad config flow.""" + from unittest.mock import patch from requests.exceptions import ConnectTimeout @@ -19,12 +20,13 @@ async def test_form(hass: HomeAssistant) -> None: assert result["type"] == "form" assert result["errors"] == {} - with patch( - "homeassistant.components.plum_lightpad.utils.Plum.loadCloudData" - ), patch( - "homeassistant.components.plum_lightpad.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch("homeassistant.components.plum_lightpad.utils.Plum.loadCloudData"), + patch( + "homeassistant.components.plum_lightpad.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"username": "test-plum-username", "password": "test-plum-password"}, @@ -71,11 +73,12 @@ async def test_form_one_entry_per_email_allowed(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch( - "homeassistant.components.plum_lightpad.utils.Plum.loadCloudData" - ), patch( - "homeassistant.components.plum_lightpad.async_setup_entry" - ) as mock_setup_entry: + with ( + patch("homeassistant.components.plum_lightpad.utils.Plum.loadCloudData"), + patch( + "homeassistant.components.plum_lightpad.async_setup_entry" + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"username": "test-plum-username", "password": "test-plum-password"}, diff --git a/tests/components/plum_lightpad/test_init.py b/tests/components/plum_lightpad/test_init.py index 66402abf13c..c34ecfd8deb 100644 --- a/tests/components/plum_lightpad/test_init.py +++ b/tests/components/plum_lightpad/test_init.py @@ -1,4 +1,5 @@ """Tests for the Plum Lightpad config flow.""" + from unittest.mock import Mock, patch from aiohttp import ContentTypeError @@ -27,11 +28,14 @@ async def test_async_setup_entry_sets_up_light(hass: HomeAssistant) -> None: ) config_entry.add_to_hass(hass) - with patch( - "homeassistant.components.plum_lightpad.utils.Plum.loadCloudData" - ) as mock_loadCloudData, patch( - "homeassistant.components.plum_lightpad.light.async_setup_entry" - ) as mock_light_async_setup_entry: + with ( + patch( + "homeassistant.components.plum_lightpad.utils.Plum.loadCloudData" + ) as mock_loadCloudData, + patch( + "homeassistant.components.plum_lightpad.light.async_setup_entry" + ) as mock_light_async_setup_entry, + ): result = await hass.config_entries.async_setup(config_entry.entry_id) assert result is True @@ -49,12 +53,15 @@ async def test_async_setup_entry_handles_auth_error(hass: HomeAssistant) -> None ) config_entry.add_to_hass(hass) - with patch( - "homeassistant.components.plum_lightpad.utils.Plum.loadCloudData", - side_effect=ContentTypeError(Mock(), None), - ), patch( - "homeassistant.components.plum_lightpad.light.async_setup_entry" - ) as mock_light_async_setup_entry: + with ( + patch( + "homeassistant.components.plum_lightpad.utils.Plum.loadCloudData", + side_effect=ContentTypeError(Mock(), None), + ), + patch( + "homeassistant.components.plum_lightpad.light.async_setup_entry" + ) as mock_light_async_setup_entry, + ): result = await hass.config_entries.async_setup(config_entry.entry_id) assert result is False @@ -69,12 +76,15 @@ async def test_async_setup_entry_handles_http_error(hass: HomeAssistant) -> None ) config_entry.add_to_hass(hass) - with patch( - "homeassistant.components.plum_lightpad.utils.Plum.loadCloudData", - side_effect=HTTPError, - ), patch( - "homeassistant.components.plum_lightpad.light.async_setup_entry" - ) as mock_light_async_setup_entry: + with ( + patch( + "homeassistant.components.plum_lightpad.utils.Plum.loadCloudData", + side_effect=HTTPError, + ), + patch( + "homeassistant.components.plum_lightpad.light.async_setup_entry" + ) as mock_light_async_setup_entry, + ): result = await hass.config_entries.async_setup(config_entry.entry_id) assert result is False diff --git a/tests/components/point/test_config_flow.py b/tests/components/point/test_config_flow.py index d58475f4994..67745251bf9 100644 --- a/tests/components/point/test_config_flow.py +++ b/tests/components/point/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for the Point config flow.""" + from unittest.mock import AsyncMock, patch import pytest diff --git a/tests/components/poolsense/test_config_flow.py b/tests/components/poolsense/test_config_flow.py index 71303e48dbf..7a91e546a59 100644 --- a/tests/components/poolsense/test_config_flow.py +++ b/tests/components/poolsense/test_config_flow.py @@ -1,4 +1,5 @@ """Test the PoolSense config flow.""" + from unittest.mock import patch from homeassistant import data_entry_flow @@ -36,11 +37,12 @@ async def test_invalid_credentials(hass: HomeAssistant) -> None: async def test_valid_credentials(hass: HomeAssistant) -> None: """Test we handle invalid credentials.""" - with patch( - "poolsense.PoolSense.test_poolsense_credentials", return_value=True - ), patch( - "homeassistant.components.poolsense.async_setup_entry", return_value=True - ) as mock_setup_entry: + with ( + patch("poolsense.PoolSense.test_poolsense_credentials", return_value=True), + patch( + "homeassistant.components.poolsense.async_setup_entry", return_value=True + ) as mock_setup_entry, + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, diff --git a/tests/components/powerwall/test_binary_sensor.py b/tests/components/powerwall/test_binary_sensor.py index f24c0e910a2..6b7e73373ee 100644 --- a/tests/components/powerwall/test_binary_sensor.py +++ b/tests/components/powerwall/test_binary_sensor.py @@ -1,4 +1,5 @@ """The binary sensor tests for the powerwall platform.""" + from unittest.mock import patch from homeassistant.components.powerwall.const import DOMAIN @@ -17,11 +18,14 @@ async def test_sensors(hass: HomeAssistant) -> None: config_entry = MockConfigEntry(domain=DOMAIN, data={CONF_IP_ADDRESS: "1.2.3.4"}) config_entry.add_to_hass(hass) - with patch( - "homeassistant.components.powerwall.config_flow.Powerwall", - return_value=mock_powerwall, - ), patch( - "homeassistant.components.powerwall.Powerwall", return_value=mock_powerwall + with ( + patch( + "homeassistant.components.powerwall.config_flow.Powerwall", + return_value=mock_powerwall, + ), + patch( + "homeassistant.components.powerwall.Powerwall", return_value=mock_powerwall + ), ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -84,11 +88,14 @@ async def test_sensors_with_empty_meters(hass: HomeAssistant) -> None: config_entry = MockConfigEntry(domain=DOMAIN, data={CONF_IP_ADDRESS: "1.2.3.4"}) config_entry.add_to_hass(hass) - with patch( - "homeassistant.components.powerwall.config_flow.Powerwall", - return_value=mock_powerwall, - ), patch( - "homeassistant.components.powerwall.Powerwall", return_value=mock_powerwall + with ( + patch( + "homeassistant.components.powerwall.config_flow.Powerwall", + return_value=mock_powerwall, + ), + patch( + "homeassistant.components.powerwall.Powerwall", return_value=mock_powerwall + ), ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/powerwall/test_config_flow.py b/tests/components/powerwall/test_config_flow.py index 5b9e27460fd..83156ffb170 100644 --- a/tests/components/powerwall/test_config_flow.py +++ b/tests/components/powerwall/test_config_flow.py @@ -41,13 +41,16 @@ async def test_form_source_user(hass: HomeAssistant) -> None: mock_powerwall = await _mock_powerwall_site_name(hass, "MySite") - with patch( - "homeassistant.components.powerwall.config_flow.Powerwall", - return_value=mock_powerwall, - ), patch( - "homeassistant.components.powerwall.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.powerwall.config_flow.Powerwall", + return_value=mock_powerwall, + ), + patch( + "homeassistant.components.powerwall.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], VALID_CONFIG, @@ -60,7 +63,7 @@ async def test_form_source_user(hass: HomeAssistant) -> None: assert len(mock_setup_entry.mock_calls) == 1 -@pytest.mark.parametrize("exc", (PowerwallUnreachableError, TimeoutError)) +@pytest.mark.parametrize("exc", [PowerwallUnreachableError, TimeoutError]) async def test_form_cannot_connect(hass: HomeAssistant, exc: Exception) -> None: """Test we handle cannot connect error.""" result = await hass.config_entries.flow.async_init( @@ -193,13 +196,16 @@ async def test_already_configured_with_ignored(hass: HomeAssistant) -> None: assert result["type"] == "form" assert result["errors"] is None - with patch( - "homeassistant.components.powerwall.config_flow.Powerwall", - return_value=mock_powerwall, - ), patch( - "homeassistant.components.powerwall.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.powerwall.config_flow.Powerwall", + return_value=mock_powerwall, + ), + patch( + "homeassistant.components.powerwall.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {}, @@ -232,13 +238,16 @@ async def test_dhcp_discovery_manual_configure(hass: HomeAssistant) -> None: assert result["type"] == "form" assert result["errors"] == {} - with patch( - "homeassistant.components.powerwall.config_flow.Powerwall", - return_value=mock_powerwall, - ), patch( - "homeassistant.components.powerwall.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.powerwall.config_flow.Powerwall", + return_value=mock_powerwall, + ), + patch( + "homeassistant.components.powerwall.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], VALID_CONFIG, @@ -271,13 +280,16 @@ async def test_dhcp_discovery_auto_configure(hass: HomeAssistant) -> None: assert result["type"] == "form" assert result["errors"] is None - with patch( - "homeassistant.components.powerwall.config_flow.Powerwall", - return_value=mock_powerwall, - ), patch( - "homeassistant.components.powerwall.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.powerwall.config_flow.Powerwall", + return_value=mock_powerwall, + ), + patch( + "homeassistant.components.powerwall.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {}, @@ -333,13 +345,16 @@ async def test_form_reauth(hass: HomeAssistant) -> None: mock_powerwall = await _mock_powerwall_site_name(hass, "My site") - with patch( - "homeassistant.components.powerwall.config_flow.Powerwall", - return_value=mock_powerwall, - ), patch( - "homeassistant.components.powerwall.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.powerwall.config_flow.Powerwall", + return_value=mock_powerwall, + ), + patch( + "homeassistant.components.powerwall.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -364,12 +379,15 @@ async def test_dhcp_discovery_update_ip_address(hass: HomeAssistant) -> None: mock_powerwall = MagicMock(login=MagicMock(side_effect=PowerwallUnreachableError)) mock_powerwall.__aenter__.return_value = mock_powerwall - with patch( - "homeassistant.components.powerwall.config_flow.Powerwall", - return_value=mock_powerwall, - ), patch( - "homeassistant.components.powerwall.async_setup_entry", - return_value=True, + with ( + patch( + "homeassistant.components.powerwall.config_flow.Powerwall", + return_value=mock_powerwall, + ), + patch( + "homeassistant.components.powerwall.async_setup_entry", + return_value=True, + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -398,12 +416,15 @@ async def test_dhcp_discovery_does_not_update_ip_when_auth_fails( entry.add_to_hass(hass) mock_powerwall = MagicMock(login=MagicMock(side_effect=AccessDeniedError("any"))) - with patch( - "homeassistant.components.powerwall.config_flow.Powerwall", - return_value=mock_powerwall, - ), patch( - "homeassistant.components.powerwall.async_setup_entry", - return_value=True, + with ( + patch( + "homeassistant.components.powerwall.config_flow.Powerwall", + return_value=mock_powerwall, + ), + patch( + "homeassistant.components.powerwall.async_setup_entry", + return_value=True, + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -432,12 +453,15 @@ async def test_dhcp_discovery_does_not_update_ip_when_auth_successful( entry.add_to_hass(hass) mock_powerwall = MagicMock(login=MagicMock(return_value=True)) - with patch( - "homeassistant.components.powerwall.config_flow.Powerwall", - return_value=mock_powerwall, - ), patch( - "homeassistant.components.powerwall.async_setup_entry", - return_value=True, + with ( + patch( + "homeassistant.components.powerwall.config_flow.Powerwall", + return_value=mock_powerwall, + ), + patch( + "homeassistant.components.powerwall.async_setup_entry", + return_value=True, + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -464,12 +488,15 @@ async def test_dhcp_discovery_updates_unique_id(hass: HomeAssistant) -> None: entry.add_to_hass(hass) mock_powerwall = await _mock_powerwall_site_name(hass, "Some site") - with patch( - "homeassistant.components.powerwall.config_flow.Powerwall", - return_value=mock_powerwall, - ), patch( - "homeassistant.components.powerwall.async_setup_entry", - return_value=True, + with ( + patch( + "homeassistant.components.powerwall.config_flow.Powerwall", + return_value=mock_powerwall, + ), + patch( + "homeassistant.components.powerwall.async_setup_entry", + return_value=True, + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -500,12 +527,15 @@ async def test_dhcp_discovery_updates_unique_id_when_entry_is_failed( entry.mock_state(hass, config_entries.ConfigEntryState.SETUP_ERROR) mock_powerwall = await _mock_powerwall_site_name(hass, "Some site") - with patch( - "homeassistant.components.powerwall.config_flow.Powerwall", - return_value=mock_powerwall, - ), patch( - "homeassistant.components.powerwall.async_setup_entry", - return_value=True, + with ( + patch( + "homeassistant.components.powerwall.config_flow.Powerwall", + return_value=mock_powerwall, + ), + patch( + "homeassistant.components.powerwall.async_setup_entry", + return_value=True, + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -535,11 +565,14 @@ async def test_discovered_wifi_does_not_update_ip_if_is_still_online( entry.add_to_hass(hass) mock_powerwall = await _mock_powerwall_with_fixtures(hass) - with patch( - "homeassistant.components.powerwall.config_flow.Powerwall", - return_value=mock_powerwall, - ), patch( - "homeassistant.components.powerwall.Powerwall", return_value=mock_powerwall + with ( + patch( + "homeassistant.components.powerwall.config_flow.Powerwall", + return_value=mock_powerwall, + ), + patch( + "homeassistant.components.powerwall.Powerwall", return_value=mock_powerwall + ), ): assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -572,11 +605,14 @@ async def test_discovered_wifi_does_not_update_ip_online_but_access_denied( mock_powerwall_no_access = await _mock_powerwall_with_fixtures(hass) mock_powerwall_no_access.login.side_effect = AccessDeniedError("any") - with patch( - "homeassistant.components.powerwall.config_flow.Powerwall", - return_value=mock_powerwall_no_access, - ), patch( - "homeassistant.components.powerwall.Powerwall", return_value=mock_powerwall + with ( + patch( + "homeassistant.components.powerwall.config_flow.Powerwall", + return_value=mock_powerwall_no_access, + ), + patch( + "homeassistant.components.powerwall.Powerwall", return_value=mock_powerwall + ), ): assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/powerwall/test_init.py b/tests/components/powerwall/test_init.py index ed0dc0ebde8..de8da12ccb5 100644 --- a/tests/components/powerwall/test_init.py +++ b/tests/components/powerwall/test_init.py @@ -40,11 +40,14 @@ async def test_update_data_reauthenticate_on_access_denied(hass: HomeAssistant) domain=DOMAIN, data={CONF_IP_ADDRESS: "1.2.3.4", CONF_PASSWORD: "password"} ) config_entry.add_to_hass(hass) - with patch( - "homeassistant.components.powerwall.config_flow.Powerwall", - return_value=mock_powerwall, - ), patch( - "homeassistant.components.powerwall.Powerwall", return_value=mock_powerwall + with ( + patch( + "homeassistant.components.powerwall.config_flow.Powerwall", + return_value=mock_powerwall, + ), + patch( + "homeassistant.components.powerwall.Powerwall", return_value=mock_powerwall + ), ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/powerwall/test_sensor.py b/tests/components/powerwall/test_sensor.py index 2de79a6a6dc..2ec9f44bd0e 100644 --- a/tests/components/powerwall/test_sensor.py +++ b/tests/components/powerwall/test_sensor.py @@ -1,4 +1,5 @@ """The sensor tests for the powerwall platform.""" + from datetime import timedelta from unittest.mock import Mock, patch @@ -33,11 +34,14 @@ async def test_sensors( config_entry = MockConfigEntry(domain=DOMAIN, data={CONF_IP_ADDRESS: "1.2.3.4"}) config_entry.add_to_hass(hass) - with patch( - "homeassistant.components.powerwall.config_flow.Powerwall", - return_value=mock_powerwall, - ), patch( - "homeassistant.components.powerwall.Powerwall", return_value=mock_powerwall + with ( + patch( + "homeassistant.components.powerwall.config_flow.Powerwall", + return_value=mock_powerwall, + ), + patch( + "homeassistant.components.powerwall.Powerwall", return_value=mock_powerwall + ), ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -202,11 +206,14 @@ async def test_sensor_backup_reserve_unavailable(hass: HomeAssistant) -> None: config_entry = MockConfigEntry(domain=DOMAIN, data={CONF_IP_ADDRESS: "1.2.3.4"}) config_entry.add_to_hass(hass) - with patch( - "homeassistant.components.powerwall.config_flow.Powerwall", - return_value=mock_powerwall, - ), patch( - "homeassistant.components.powerwall.Powerwall", return_value=mock_powerwall + with ( + patch( + "homeassistant.components.powerwall.config_flow.Powerwall", + return_value=mock_powerwall, + ), + patch( + "homeassistant.components.powerwall.Powerwall", return_value=mock_powerwall + ), ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -222,11 +229,14 @@ async def test_sensors_with_empty_meters(hass: HomeAssistant) -> None: config_entry = MockConfigEntry(domain=DOMAIN, data={CONF_IP_ADDRESS: "1.2.3.4"}) config_entry.add_to_hass(hass) - with patch( - "homeassistant.components.powerwall.config_flow.Powerwall", - return_value=mock_powerwall, - ), patch( - "homeassistant.components.powerwall.Powerwall", return_value=mock_powerwall + with ( + patch( + "homeassistant.components.powerwall.config_flow.Powerwall", + return_value=mock_powerwall, + ), + patch( + "homeassistant.components.powerwall.Powerwall", return_value=mock_powerwall + ), ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -260,11 +270,14 @@ async def test_unique_id_migrate( ) assert old_mysite_load_power_entity.entity_id == "sensor.mysite_load_power" - with patch( - "homeassistant.components.powerwall.config_flow.Powerwall", - return_value=mock_powerwall, - ), patch( - "homeassistant.components.powerwall.Powerwall", return_value=mock_powerwall + with ( + patch( + "homeassistant.components.powerwall.config_flow.Powerwall", + return_value=mock_powerwall, + ), + patch( + "homeassistant.components.powerwall.Powerwall", return_value=mock_powerwall + ), ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/powerwall/test_switch.py b/tests/components/powerwall/test_switch.py index e63d6031155..fdcdd5150ed 100644 --- a/tests/components/powerwall/test_switch.py +++ b/tests/components/powerwall/test_switch.py @@ -1,4 +1,5 @@ """Test for Powerwall off-grid switch.""" + from unittest.mock import patch import pytest diff --git a/tests/components/private_ble_device/test_config_flow.py b/tests/components/private_ble_device/test_config_flow.py index bb58cfedb29..2acb89240a1 100644 --- a/tests/components/private_ble_device/test_config_flow.py +++ b/tests/components/private_ble_device/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for private bluetooth device config flow.""" + from unittest.mock import patch from homeassistant import config_entries diff --git a/tests/components/private_ble_device/test_device_tracker.py b/tests/components/private_ble_device/test_device_tracker.py index 3834254ac7f..9d784ecdfa7 100644 --- a/tests/components/private_ble_device/test_device_tracker.py +++ b/tests/components/private_ble_device/test_device_tracker.py @@ -1,6 +1,5 @@ """Tests for polling measures.""" - import time from habluetooth.advertisement_tracker import ADVERTISING_TIMES_NEEDED diff --git a/tests/components/private_ble_device/test_sensor.py b/tests/components/private_ble_device/test_sensor.py index 15e205c8c86..43667a0e9d2 100644 --- a/tests/components/private_ble_device/test_sensor.py +++ b/tests/components/private_ble_device/test_sensor.py @@ -1,6 +1,5 @@ """Tests for sensors.""" - from habluetooth.advertisement_tracker import ADVERTISING_TIMES_NEEDED from homeassistant.components.bluetooth import async_set_fallback_availability_interval diff --git a/tests/components/profiler/test_config_flow.py b/tests/components/profiler/test_config_flow.py index 5d62368822a..93542f90520 100644 --- a/tests/components/profiler/test_config_flow.py +++ b/tests/components/profiler/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Profiler config flow.""" + from unittest.mock import patch from homeassistant import config_entries diff --git a/tests/components/profiler/test_init.py b/tests/components/profiler/test_init.py index b8a81a40e37..3cade465347 100644 --- a/tests/components/profiler/test_init.py +++ b/tests/components/profiler/test_init.py @@ -1,22 +1,28 @@ """Test the Profiler config flow.""" + from datetime import timedelta from functools import lru_cache +import logging import os from pathlib import Path from unittest.mock import patch +from freezegun.api import FrozenDateTimeFactory from lru import LRU +import objgraph import pytest from homeassistant.components.profiler import ( _LRU_CACHE_WRAPPER_OBJECT, _SQLALCHEMY_LRU_OBJECT, + CONF_ENABLED, CONF_SECONDS, SERVICE_DUMP_LOG_OBJECTS, SERVICE_LOG_EVENT_LOOP_SCHEDULED, SERVICE_LOG_THREAD_FRAMES, SERVICE_LRU_STATS, SERVICE_MEMORY, + SERVICE_SET_ASYNCIO_DEBUG, SERVICE_START, SERVICE_START_LOG_OBJECT_SOURCES, SERVICE_START_LOG_OBJECTS, @@ -95,7 +101,9 @@ async def test_memory_usage(hass: HomeAssistant, tmp_path: Path) -> None: async def test_object_growth_logging( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + freezer: FrozenDateTimeFactory, ) -> None: """Test we can setup and the service and we can dump objects to the log.""" @@ -108,30 +116,31 @@ async def test_object_growth_logging( assert hass.services.has_service(DOMAIN, SERVICE_START_LOG_OBJECTS) assert hass.services.has_service(DOMAIN, SERVICE_STOP_LOG_OBJECTS) - with patch("objgraph.growth"): + with patch.object(objgraph, "growth"): await hass.services.async_call( - DOMAIN, SERVICE_START_LOG_OBJECTS, {CONF_SCAN_INTERVAL: 10}, blocking=True + DOMAIN, SERVICE_START_LOG_OBJECTS, {CONF_SCAN_INTERVAL: 1}, blocking=True ) with pytest.raises(HomeAssistantError, match="Object logging already started"): await hass.services.async_call( DOMAIN, SERVICE_START_LOG_OBJECTS, - {CONF_SCAN_INTERVAL: 10}, + {CONF_SCAN_INTERVAL: 1}, blocking=True, ) assert "Growth" in caplog.text + await hass.async_block_till_done(wait_background_tasks=True) caplog.clear() - async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=11)) - await hass.async_block_till_done() + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=2)) + await hass.async_block_till_done(wait_background_tasks=True) assert "Growth" in caplog.text await hass.services.async_call(DOMAIN, SERVICE_STOP_LOG_OBJECTS, {}, blocking=True) caplog.clear() async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=21)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert "Growth" not in caplog.text with pytest.raises(HomeAssistantError, match="Object logging not running"): @@ -139,17 +148,18 @@ async def test_object_growth_logging( DOMAIN, SERVICE_STOP_LOG_OBJECTS, {}, blocking=True ) - with patch("objgraph.growth"): + with patch.object(objgraph, "growth"): await hass.services.async_call( DOMAIN, SERVICE_START_LOG_OBJECTS, {CONF_SCAN_INTERVAL: 10}, blocking=True ) + await hass.async_block_till_done(wait_background_tasks=True) caplog.clear() assert await hass.config_entries.async_unload(entry.entry_id) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=31)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert "Growth" not in caplog.text @@ -322,18 +332,19 @@ async def test_log_object_sources( caplog.clear() async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=11)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert "No new object growth found" in caplog.text fake_object2 = FakeObject() - with patch("gc.collect"), patch( - "gc.get_objects", return_value=[fake_object, fake_object2] + with ( + patch("gc.collect"), + patch("gc.get_objects", return_value=[fake_object, fake_object2]), ): caplog.clear() async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=21)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert "New object FakeObject (1/2)" in caplog.text many_objects = [FakeObject() for _ in range(30)] @@ -341,7 +352,7 @@ async def test_log_object_sources( caplog.clear() async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=31)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert "New object FakeObject (2/30)" in caplog.text assert "New objects overflowed by {'FakeObject': 25}" in caplog.text @@ -351,7 +362,7 @@ async def test_log_object_sources( caplog.clear() async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=41)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert "FakeObject" not in caplog.text assert "No new object growth found" not in caplog.text @@ -359,7 +370,7 @@ async def test_log_object_sources( await hass.async_block_till_done() async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=51)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert "FakeObject" not in caplog.text assert "No new object growth found" not in caplog.text @@ -367,3 +378,48 @@ async def test_log_object_sources( await hass.services.async_call( DOMAIN, SERVICE_STOP_LOG_OBJECT_SOURCES, {}, blocking=True ) + + +async def test_set_asyncio_debug( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test setting asyncio debug.""" + + entry = MockConfigEntry(domain=DOMAIN) + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert hass.services.has_service(DOMAIN, SERVICE_SET_ASYNCIO_DEBUG) + + hass.loop.set_debug(False) + original_level = logging.getLogger().getEffectiveLevel() + logging.getLogger().setLevel(logging.WARNING) + + await hass.services.async_call( + DOMAIN, SERVICE_SET_ASYNCIO_DEBUG, {CONF_ENABLED: False}, blocking=True + ) + # Ensure logging level is only increased if we enable + assert logging.getLogger().getEffectiveLevel() == logging.WARNING + + await hass.services.async_call(DOMAIN, SERVICE_SET_ASYNCIO_DEBUG, {}, blocking=True) + assert hass.loop.get_debug() is True + + # Ensure logging is at least at INFO level + assert logging.getLogger().getEffectiveLevel() == logging.INFO + + await hass.services.async_call( + DOMAIN, SERVICE_SET_ASYNCIO_DEBUG, {CONF_ENABLED: False}, blocking=True + ) + assert hass.loop.get_debug() is False + + await hass.services.async_call( + DOMAIN, SERVICE_SET_ASYNCIO_DEBUG, {CONF_ENABLED: True}, blocking=True + ) + assert hass.loop.get_debug() is True + + logging.getLogger().setLevel(original_level) + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/progettihwsw/test_config_flow.py b/tests/components/progettihwsw/test_config_flow.py index 7685d917644..7774adb5208 100644 --- a/tests/components/progettihwsw/test_config_flow.py +++ b/tests/components/progettihwsw/test_config_flow.py @@ -1,4 +1,5 @@ """Test the ProgettiHWSW Automation config flow.""" + from unittest.mock import patch from homeassistant import config_entries diff --git a/tests/components/prometheus/test_init.py b/tests/components/prometheus/test_init.py index 7ee534f91ce..99b73209ad7 100644 --- a/tests/components/prometheus/test_init.py +++ b/tests/components/prometheus/test_init.py @@ -1,4 +1,5 @@ """The tests for the Prometheus exporter.""" + from dataclasses import dataclass import datetime from http import HTTPStatus @@ -65,6 +66,8 @@ from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util +from tests.typing import ClientSessionGenerator + PROMETHEUS_PATH = "homeassistant.components.prometheus" @@ -77,7 +80,11 @@ class FilterTest: @pytest.fixture(name="client") -async def setup_prometheus_client(hass, hass_client, namespace): +async def setup_prometheus_client( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + namespace: str, +): """Initialize an hass_client with Prometheus component.""" # Reset registry prometheus_client.REGISTRY = prometheus_client.CollectorRegistry(auto_describe=True) @@ -110,7 +117,12 @@ async def generate_latest_metrics(client): @pytest.mark.parametrize("namespace", [""]) -async def test_setup_enumeration(hass, hass_client, entity_registry, namespace): +async def test_setup_enumeration( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + entity_registry: er.EntityRegistry, + namespace: str, +) -> None: """Test that setup enumerates existing states/entities.""" # The order of when things are created must be carefully controlled in @@ -138,7 +150,9 @@ async def test_setup_enumeration(hass, hass_client, entity_registry, namespace): @pytest.mark.parametrize("namespace", [""]) -async def test_view_empty_namespace(client, sensor_entities) -> None: +async def test_view_empty_namespace( + client: ClientSessionGenerator, sensor_entities: dict[str, er.RegistryEntry] +) -> None: """Test prometheus metrics view.""" body = await generate_latest_metrics(client) @@ -162,7 +176,9 @@ async def test_view_empty_namespace(client, sensor_entities) -> None: @pytest.mark.parametrize("namespace", [None]) -async def test_view_default_namespace(client, sensor_entities) -> None: +async def test_view_default_namespace( + client: ClientSessionGenerator, sensor_entities: dict[str, er.RegistryEntry] +) -> None: """Test prometheus metrics view.""" body = await generate_latest_metrics(client) @@ -180,7 +196,9 @@ async def test_view_default_namespace(client, sensor_entities) -> None: @pytest.mark.parametrize("namespace", [""]) -async def test_sensor_unit(client, sensor_entities) -> None: +async def test_sensor_unit( + client: ClientSessionGenerator, sensor_entities: dict[str, er.RegistryEntry] +) -> None: """Test prometheus metrics for sensors with a unit.""" body = await generate_latest_metrics(client) @@ -210,7 +228,9 @@ async def test_sensor_unit(client, sensor_entities) -> None: @pytest.mark.parametrize("namespace", [""]) -async def test_sensor_without_unit(client, sensor_entities) -> None: +async def test_sensor_without_unit( + client: ClientSessionGenerator, sensor_entities: dict[str, er.RegistryEntry] +) -> None: """Test prometheus metrics for sensors without a unit.""" body = await generate_latest_metrics(client) @@ -234,7 +254,9 @@ async def test_sensor_without_unit(client, sensor_entities) -> None: @pytest.mark.parametrize("namespace", [""]) -async def test_sensor_device_class(client, sensor_entities) -> None: +async def test_sensor_device_class( + client: ClientSessionGenerator, sensor_entities: dict[str, er.RegistryEntry] +) -> None: """Test prometheus metrics for sensor with a device_class.""" body = await generate_latest_metrics(client) @@ -270,7 +292,9 @@ async def test_sensor_device_class(client, sensor_entities) -> None: @pytest.mark.parametrize("namespace", [""]) -async def test_input_number(client, input_number_entities) -> None: +async def test_input_number( + client: ClientSessionGenerator, input_number_entities: dict[str, er.RegistryEntry] +) -> None: """Test prometheus metrics for input_number.""" body = await generate_latest_metrics(client) @@ -294,7 +318,9 @@ async def test_input_number(client, input_number_entities) -> None: @pytest.mark.parametrize("namespace", [""]) -async def test_number(client, number_entities) -> None: +async def test_number( + client: ClientSessionGenerator, number_entities: dict[str, er.RegistryEntry] +) -> None: """Test prometheus metrics for number.""" body = await generate_latest_metrics(client) @@ -318,7 +344,9 @@ async def test_number(client, number_entities) -> None: @pytest.mark.parametrize("namespace", [""]) -async def test_battery(client, sensor_entities) -> None: +async def test_battery( + client: ClientSessionGenerator, sensor_entities: dict[str, er.RegistryEntry] +) -> None: """Test prometheus metrics for battery.""" body = await generate_latest_metrics(client) @@ -330,7 +358,10 @@ async def test_battery(client, sensor_entities) -> None: @pytest.mark.parametrize("namespace", [""]) -async def test_climate(client, climate_entities) -> None: +async def test_climate( + client: ClientSessionGenerator, + climate_entities: dict[str, er.RegistryEntry | dict[str, Any]], +) -> None: """Test prometheus metrics for climate entities.""" body = await generate_latest_metrics(client) @@ -366,7 +397,10 @@ async def test_climate(client, climate_entities) -> None: @pytest.mark.parametrize("namespace", [""]) -async def test_humidifier(client, humidifier_entities) -> None: +async def test_humidifier( + client: ClientSessionGenerator, + humidifier_entities: dict[str, er.RegistryEntry | dict[str, Any]], +) -> None: """Test prometheus metrics for humidifier entities.""" body = await generate_latest_metrics(client) @@ -397,7 +431,10 @@ async def test_humidifier(client, humidifier_entities) -> None: @pytest.mark.parametrize("namespace", [""]) -async def test_attributes(client, switch_entities) -> None: +async def test_attributes( + client: ClientSessionGenerator, + switch_entities: dict[str, er.RegistryEntry | dict[str, Any]], +) -> None: """Test prometheus metrics for entity attributes.""" body = await generate_latest_metrics(client) @@ -427,7 +464,9 @@ async def test_attributes(client, switch_entities) -> None: @pytest.mark.parametrize("namespace", [""]) -async def test_binary_sensor(client, binary_sensor_entities) -> None: +async def test_binary_sensor( + client: ClientSessionGenerator, binary_sensor_entities: dict[str, er.RegistryEntry] +) -> None: """Test prometheus metrics for binary_sensor.""" body = await generate_latest_metrics(client) @@ -445,7 +484,9 @@ async def test_binary_sensor(client, binary_sensor_entities) -> None: @pytest.mark.parametrize("namespace", [""]) -async def test_input_boolean(client, input_boolean_entities) -> None: +async def test_input_boolean( + client: ClientSessionGenerator, input_boolean_entities: dict[str, er.RegistryEntry] +) -> None: """Test prometheus metrics for input_boolean.""" body = await generate_latest_metrics(client) @@ -463,7 +504,9 @@ async def test_input_boolean(client, input_boolean_entities) -> None: @pytest.mark.parametrize("namespace", [""]) -async def test_light(client, light_entities) -> None: +async def test_light( + client: ClientSessionGenerator, light_entities: dict[str, er.RegistryEntry] +) -> None: """Test prometheus metrics for lights.""" body = await generate_latest_metrics(client) @@ -499,7 +542,9 @@ async def test_light(client, light_entities) -> None: @pytest.mark.parametrize("namespace", [""]) -async def test_lock(client, lock_entities) -> None: +async def test_lock( + client: ClientSessionGenerator, lock_entities: dict[str, er.RegistryEntry] +) -> None: """Test prometheus metrics for lock.""" body = await generate_latest_metrics(client) @@ -517,7 +562,9 @@ async def test_lock(client, lock_entities) -> None: @pytest.mark.parametrize("namespace", [""]) -async def test_cover(client, cover_entities) -> None: +async def test_cover( + client: ClientSessionGenerator, cover_entities: dict[str, er.RegistryEntry] +) -> None: """Test prometheus metrics for cover.""" data = {**cover_entities} body = await generate_latest_metrics(client) @@ -576,7 +623,9 @@ async def test_cover(client, cover_entities) -> None: @pytest.mark.parametrize("namespace", [""]) -async def test_device_tracker(client, device_tracker_entities) -> None: +async def test_device_tracker( + client: ClientSessionGenerator, device_tracker_entities: dict[str, er.RegistryEntry] +) -> None: """Test prometheus metrics for device_tracker.""" body = await generate_latest_metrics(client) @@ -593,7 +642,9 @@ async def test_device_tracker(client, device_tracker_entities) -> None: @pytest.mark.parametrize("namespace", [""]) -async def test_counter(client, counter_entities) -> None: +async def test_counter( + client: ClientSessionGenerator, counter_entities: dict[str, er.RegistryEntry] +) -> None: """Test prometheus metrics for counter.""" body = await generate_latest_metrics(client) @@ -605,7 +656,9 @@ async def test_counter(client, counter_entities) -> None: @pytest.mark.parametrize("namespace", [""]) -async def test_update(client, update_entities) -> None: +async def test_update( + client: ClientSessionGenerator, update_entities: dict[str, er.RegistryEntry] +) -> None: """Test prometheus metrics for update.""" body = await generate_latest_metrics(client) @@ -625,9 +678,9 @@ async def test_update(client, update_entities) -> None: async def test_renaming_entity_name( hass: HomeAssistant, entity_registry: er.EntityRegistry, - client, - sensor_entities, - climate_entities, + client: ClientSessionGenerator, + sensor_entities: dict[str, er.RegistryEntry], + climate_entities: dict[str, er.RegistryEntry | dict[str, Any]], ) -> None: """Test renaming entity name.""" data = {**sensor_entities, **climate_entities} @@ -751,9 +804,9 @@ async def test_renaming_entity_name( async def test_renaming_entity_id( hass: HomeAssistant, entity_registry: er.EntityRegistry, - client, - sensor_entities, - climate_entities, + client: ClientSessionGenerator, + sensor_entities: dict[str, er.RegistryEntry], + climate_entities: dict[str, er.RegistryEntry | dict[str, Any]], ) -> None: """Test renaming entity id.""" data = {**sensor_entities, **climate_entities} @@ -831,9 +884,9 @@ async def test_renaming_entity_id( async def test_deleting_entity( hass: HomeAssistant, entity_registry: er.EntityRegistry, - client, - sensor_entities, - climate_entities, + client: ClientSessionGenerator, + sensor_entities: dict[str, er.RegistryEntry], + climate_entities: dict[str, er.RegistryEntry | dict[str, Any]], ) -> None: """Test deleting a entity.""" data = {**sensor_entities, **climate_entities} @@ -910,9 +963,9 @@ async def test_deleting_entity( async def test_disabling_entity( hass: HomeAssistant, entity_registry: er.EntityRegistry, - client, - sensor_entities, - climate_entities, + client: ClientSessionGenerator, + sensor_entities: dict[str, er.RegistryEntry], + climate_entities: dict[str, er.RegistryEntry | dict[str, Any]], ) -> None: """Test disabling a entity.""" data = {**sensor_entities, **climate_entities} @@ -1760,14 +1813,14 @@ def mock_client_fixture(): yield counter_client -async def test_minimal_config(hass: HomeAssistant, mock_client) -> None: +async def test_minimal_config(hass: HomeAssistant, mock_client: mock.MagicMock) -> None: """Test the minimal config and defaults of component.""" config = {prometheus.DOMAIN: {}} assert await async_setup_component(hass, prometheus.DOMAIN, config) await hass.async_block_till_done() -async def test_full_config(hass: HomeAssistant, mock_client) -> None: +async def test_full_config(hass: HomeAssistant, mock_client: mock.MagicMock) -> None: """Test the full config of component.""" config = { prometheus.DOMAIN: { @@ -1792,14 +1845,14 @@ async def test_full_config(hass: HomeAssistant, mock_client) -> None: await hass.async_block_till_done() -async def _setup(hass, filter_config): +async def _setup(hass: HomeAssistant, filter_config): """Shared set up for filtering tests.""" config = {prometheus.DOMAIN: {"filter": filter_config}} assert await async_setup_component(hass, prometheus.DOMAIN, config) await hass.async_block_till_done() -async def test_allowlist(hass: HomeAssistant, mock_client) -> None: +async def test_allowlist(hass: HomeAssistant, mock_client: mock.MagicMock) -> None: """Test an allowlist only config.""" await _setup( hass, @@ -1828,7 +1881,7 @@ async def test_allowlist(hass: HomeAssistant, mock_client) -> None: mock_client.labels.reset_mock() -async def test_denylist(hass: HomeAssistant, mock_client) -> None: +async def test_denylist(hass: HomeAssistant, mock_client: mock.MagicMock) -> None: """Test a denylist only config.""" await _setup( hass, @@ -1857,7 +1910,9 @@ async def test_denylist(hass: HomeAssistant, mock_client) -> None: mock_client.labels.reset_mock() -async def test_filtered_denylist(hass: HomeAssistant, mock_client) -> None: +async def test_filtered_denylist( + hass: HomeAssistant, mock_client: mock.MagicMock +) -> None: """Test a denylist config with a filtering allowlist.""" await _setup( hass, diff --git a/tests/components/prosegur/conftest.py b/tests/components/prosegur/conftest.py index 91bc7f88405..0b18c2c5e17 100644 --- a/tests/components/prosegur/conftest.py +++ b/tests/components/prosegur/conftest.py @@ -1,4 +1,5 @@ """Define test fixtures for Prosegur.""" + from unittest.mock import AsyncMock, MagicMock, patch from pyprosegur.installation import Camera @@ -61,9 +62,12 @@ async def init_integration( """Set up the Prosegur integration for testing.""" mock_config_entry.add_to_hass(hass) - with patch( - "pyprosegur.installation.Installation.retrieve", return_value=mock_install - ), patch("pyprosegur.auth.Auth.login"): + with ( + patch( + "pyprosegur.installation.Installation.retrieve", return_value=mock_install + ), + patch("pyprosegur.auth.Auth.login"), + ): await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/prosegur/test_alarm_control_panel.py b/tests/components/prosegur/test_alarm_control_panel.py index d5244de1b43..534c852c616 100644 --- a/tests/components/prosegur/test_alarm_control_panel.py +++ b/tests/components/prosegur/test_alarm_control_panel.py @@ -1,4 +1,5 @@ """Tests for the Prosegur alarm control panel device.""" + from unittest.mock import AsyncMock, patch from pyprosegur.installation import Status diff --git a/tests/components/prosegur/test_camera.py b/tests/components/prosegur/test_camera.py index 58017085aed..ed503d676ff 100644 --- a/tests/components/prosegur/test_camera.py +++ b/tests/components/prosegur/test_camera.py @@ -1,4 +1,5 @@ """The camera tests for the prosegur platform.""" + import logging from unittest.mock import AsyncMock @@ -33,9 +34,10 @@ async def test_camera_fail( return_value=b"ABC", side_effect=ProsegurException() ) - with caplog.at_level( - logging.ERROR, logger="homeassistant.components.prosegur" - ), pytest.raises(HomeAssistantError) as exc: + with ( + caplog.at_level(logging.ERROR, logger="homeassistant.components.prosegur"), + pytest.raises(HomeAssistantError) as exc, + ): await camera.async_get_image(hass, "camera.contract_1234abcd_test_cam") assert "Unable to get image" in str(exc.value) diff --git a/tests/components/prosegur/test_config_flow.py b/tests/components/prosegur/test_config_flow.py index 2c08f9de109..3c3c2468696 100644 --- a/tests/components/prosegur/test_config_flow.py +++ b/tests/components/prosegur/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Prosegur Alarm config flow.""" + from unittest.mock import patch import pytest @@ -21,13 +22,16 @@ async def test_form(hass: HomeAssistant, mock_list_contracts) -> None: assert result["type"] == "form" assert result["errors"] == {} - with patch( - "homeassistant.components.prosegur.config_flow.Installation.list", - return_value=mock_list_contracts, - ) as mock_retrieve, patch( - "homeassistant.components.prosegur.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.prosegur.config_flow.Installation.list", + return_value=mock_list_contracts, + ) as mock_retrieve, + patch( + "homeassistant.components.prosegur.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -152,13 +156,16 @@ async def test_reauth_flow(hass: HomeAssistant, mock_list_contracts) -> None: assert result["type"] == FlowResultType.FORM assert result["errors"] == {} - with patch( - "homeassistant.components.prosegur.config_flow.Installation.list", - return_value=mock_list_contracts, - ) as mock_installation, patch( - "homeassistant.components.prosegur.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.prosegur.config_flow.Installation.list", + return_value=mock_list_contracts, + ) as mock_installation, + patch( + "homeassistant.components.prosegur.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { diff --git a/tests/components/prosegur/test_diagnostics.py b/tests/components/prosegur/test_diagnostics.py index daa92de1aa0..c4cd92d48cf 100644 --- a/tests/components/prosegur/test_diagnostics.py +++ b/tests/components/prosegur/test_diagnostics.py @@ -1,4 +1,5 @@ """Test Prosegur diagnostics.""" + from unittest.mock import patch from homeassistant.core import HomeAssistant diff --git a/tests/components/prosegur/test_init.py b/tests/components/prosegur/test_init.py index cdc7135cf1f..c7de490fdaa 100644 --- a/tests/components/prosegur/test_init.py +++ b/tests/components/prosegur/test_init.py @@ -1,4 +1,5 @@ """Tests prosegur setup.""" + from unittest.mock import patch import pytest diff --git a/tests/components/proximity/conftest.py b/tests/components/proximity/conftest.py index 960ab6cf916..ba73d971131 100644 --- a/tests/components/proximity/conftest.py +++ b/tests/components/proximity/conftest.py @@ -1,4 +1,5 @@ """Config test for proximity.""" + import pytest from homeassistant.core import HomeAssistant diff --git a/tests/components/proximity/test_config_flow.py b/tests/components/proximity/test_config_flow.py index 3c94e941227..1841c10873c 100644 --- a/tests/components/proximity/test_config_flow.py +++ b/tests/components/proximity/test_config_flow.py @@ -1,4 +1,5 @@ """Test proximity config flow.""" + from unittest.mock import patch import pytest diff --git a/tests/components/proximity/test_diagnostics.py b/tests/components/proximity/test_diagnostics.py index e23d8180672..26d1d293efd 100644 --- a/tests/components/proximity/test_diagnostics.py +++ b/tests/components/proximity/test_diagnostics.py @@ -1,4 +1,5 @@ """Tests for proximity diagnostics platform.""" + from __future__ import annotations from syrupy.assertion import SnapshotAssertion @@ -70,4 +71,6 @@ async def test_entry_diagnostics( assert await get_diagnostics_for_config_entry( hass, hass_client, mock_entry - ) == snapshot(exclude=props("entry_id", "last_changed", "last_updated")) + ) == snapshot( + exclude=props("entry_id", "last_changed", "last_reported", "last_updated") + ) diff --git a/tests/components/prusalink/conftest.py b/tests/components/prusalink/conftest.py index 1e514342068..104e4d47afa 100644 --- a/tests/components/prusalink/conftest.py +++ b/tests/components/prusalink/conftest.py @@ -1,4 +1,5 @@ """Fixtures for PrusaLink.""" + from unittest.mock import patch import pytest @@ -120,6 +121,32 @@ def mock_job_api_idle(hass): yield resp +@pytest.fixture +def mock_job_api_idle_mk3(hass): + """Mock PrusaLink job API having a job with idle state (MK3).""" + resp = { + "id": 129, + "state": "IDLE", + "progress": 0.0, + "time_remaining": None, + "time_printing": 0, + "file": { + "refs": { + "icon": "/thumb/s/usb/TabletStand3~4.BGC", + "thumbnail": "/thumb/l/usb/TabletStand3~4.BGC", + "download": "/usb/TabletStand3~4.BGC", + }, + "name": "TabletStand3~4.BGC", + "display_name": "TabletStand3.bgcode", + "path": "/usb", + "size": 754535, + "m_timestamp": 1698686881, + }, + } + with patch("pyprusalink.PrusaLink.get_job", return_value=resp): + yield resp + + @pytest.fixture def mock_job_api_printing(hass): """Mock PrusaLink printing.""" diff --git a/tests/components/prusalink/test_button.py b/tests/components/prusalink/test_button.py index 5324e337780..54f3854161c 100644 --- a/tests/components/prusalink/test_button.py +++ b/tests/components/prusalink/test_button.py @@ -1,4 +1,5 @@ """Test Prusalink buttons.""" + from unittest.mock import patch from pyprusalink.types import Conflict @@ -21,10 +22,10 @@ def setup_button_platform_only(): @pytest.mark.parametrize( ("object_id", "method"), - ( + [ ("mock_title_cancel_job", "cancel_job"), ("mock_title_pause_job", "pause_job"), - ), + ], ) async def test_button_pause_cancel( hass: HomeAssistant, @@ -54,8 +55,9 @@ async def test_button_pause_cancel( assert len(mock_meth.mock_calls) == 1 # Verify it calls correct method + does error handling - with pytest.raises(HomeAssistantError), patch( - f"pyprusalink.PrusaLink.{method}", side_effect=Conflict + with ( + pytest.raises(HomeAssistantError), + patch(f"pyprusalink.PrusaLink.{method}", side_effect=Conflict), ): await hass.services.async_call( "button", @@ -67,10 +69,10 @@ async def test_button_pause_cancel( @pytest.mark.parametrize( ("object_id", "method"), - ( + [ ("mock_title_cancel_job", "cancel_job"), ("mock_title_resume_job", "resume_job"), - ), + ], ) async def test_button_resume_cancel( hass: HomeAssistant, @@ -88,8 +90,11 @@ async def test_button_resume_cancel( assert state is not None assert state.state == "unknown" - with patch(f"pyprusalink.PrusaLink.{method}") as mock_meth, patch( - "homeassistant.components.prusalink.PrusaLinkUpdateCoordinator._fetch_data" + with ( + patch(f"pyprusalink.PrusaLink.{method}") as mock_meth, + patch( + "homeassistant.components.prusalink.PrusaLinkUpdateCoordinator._fetch_data" + ), ): await hass.services.async_call( "button", @@ -101,8 +106,9 @@ async def test_button_resume_cancel( assert len(mock_meth.mock_calls) == 1 # Verify it calls correct method + does error handling - with pytest.raises(HomeAssistantError), patch( - f"pyprusalink.PrusaLink.{method}", side_effect=Conflict + with ( + pytest.raises(HomeAssistantError), + patch(f"pyprusalink.PrusaLink.{method}", side_effect=Conflict), ): await hass.services.async_call( "button", diff --git a/tests/components/prusalink/test_camera.py b/tests/components/prusalink/test_camera.py index b84a13a3df8..c770a7f228d 100644 --- a/tests/components/prusalink/test_camera.py +++ b/tests/components/prusalink/test_camera.py @@ -1,4 +1,5 @@ """Test Prusalink camera.""" + from unittest.mock import patch import pytest @@ -34,6 +35,24 @@ async def test_camera_no_job( assert resp.status == 500 +async def test_camera_idle_job_mk3( + hass: HomeAssistant, + mock_config_entry, + mock_api, + mock_job_api_idle_mk3, + hass_client: ClientSessionGenerator, +) -> None: + """Test camera while job state is idle (MK3).""" + assert await async_setup_component(hass, "prusalink", {}) + state = hass.states.get("camera.mock_title_preview") + assert state is not None + assert state.state == "unavailable" + + client = await hass_client() + resp = await client.get("/api/camera_proxy/camera.mock_title_preview") + assert resp.status == 500 + + async def test_camera_active_job( hass: HomeAssistant, mock_config_entry, diff --git a/tests/components/prusalink/test_config_flow.py b/tests/components/prusalink/test_config_flow.py index 3d6f6221a50..e7db5b54dac 100644 --- a/tests/components/prusalink/test_config_flow.py +++ b/tests/components/prusalink/test_config_flow.py @@ -1,4 +1,5 @@ """Test the PrusaLink config flow.""" + from unittest.mock import patch from homeassistant import config_entries @@ -40,6 +41,34 @@ async def test_form(hass: HomeAssistant, mock_version_api) -> None: assert len(mock_setup_entry.mock_calls) == 1 +async def test_form_mk3(hass: HomeAssistant, mock_version_api) -> None: + """Test it works for MK2/MK3.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + mock_version_api["api"] = "0.9.0-legacy" + mock_version_api["server"] = "0.7.2" + mock_version_api["original"] = "PrusaLink I3MK3S" + + with patch( + "homeassistant.components.prusalink.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "http://1.1.1.1/", + "username": "abcdefg", + "password": "abcdefg", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert len(mock_setup_entry.mock_calls) == 1 + + async def test_form_invalid_auth(hass: HomeAssistant) -> None: """Test we handle invalid auth.""" result = await hass.config_entries.flow.async_init( @@ -128,6 +157,31 @@ async def test_form_invalid_version_2(hass: HomeAssistant, mock_version_api) -> assert result2["errors"] == {"base": "not_supported"} +async def test_form_invalid_mk3_server_version( + hass: HomeAssistant, mock_version_api +) -> None: + """Test we handle invalid version for MK2/MK3.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + mock_version_api["api"] = "0.7.2" + mock_version_api["server"] = "i am not a version" + mock_version_api["original"] = "PrusaLink I3MK3S" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "username": "abcdefg", + "password": "abcdefg", + }, + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {"base": "not_supported"} + + async def test_form_cannot_connect(hass: HomeAssistant) -> None: """Test we handle cannot connect error.""" result = await hass.config_entries.flow.async_init( diff --git a/tests/components/prusalink/test_init.py b/tests/components/prusalink/test_init.py index 5b261207e93..1160143ea11 100644 --- a/tests/components/prusalink/test_init.py +++ b/tests/components/prusalink/test_init.py @@ -1,4 +1,5 @@ """Test setting up and unloading PrusaLink.""" + from datetime import timedelta from unittest.mock import patch @@ -43,18 +44,23 @@ async def test_failed_update( assert await hass.config_entries.async_setup(mock_config_entry.entry_id) assert mock_config_entry.state == ConfigEntryState.LOADED - with patch( - "homeassistant.components.prusalink.PrusaLink.get_version", - side_effect=exception, - ), patch( - "homeassistant.components.prusalink.PrusaLink.get_status", - side_effect=exception, - ), patch( - "homeassistant.components.prusalink.PrusaLink.get_legacy_printer", - side_effect=exception, - ), patch( - "homeassistant.components.prusalink.PrusaLink.get_job", - side_effect=exception, + with ( + patch( + "homeassistant.components.prusalink.PrusaLink.get_version", + side_effect=exception, + ), + patch( + "homeassistant.components.prusalink.PrusaLink.get_status", + side_effect=exception, + ), + patch( + "homeassistant.components.prusalink.PrusaLink.get_legacy_printer", + side_effect=exception, + ), + patch( + "homeassistant.components.prusalink.PrusaLink.get_job", + side_effect=exception, + ), ): async_fire_time_changed(hass, utcnow() + timedelta(seconds=30), fire_all=True) await hass.async_block_till_done() diff --git a/tests/components/prusalink/test_sensor.py b/tests/components/prusalink/test_sensor.py index 366f2d3abc8..b15e9198da6 100644 --- a/tests/components/prusalink/test_sensor.py +++ b/tests/components/prusalink/test_sensor.py @@ -27,11 +27,12 @@ from homeassistant.setup import async_setup_component @pytest.fixture(autouse=True) def setup_sensor_platform_only(): """Only setup sensor platform.""" - with patch( - "homeassistant.components.prusalink.PLATFORMS", [Platform.SENSOR] - ), patch( - "homeassistant.helpers.entity.Entity.entity_registry_enabled_default", - PropertyMock(return_value=True), + with ( + patch("homeassistant.components.prusalink.PLATFORMS", [Platform.SENSOR]), + patch( + "homeassistant.helpers.entity.Entity.entity_registry_enabled_default", + PropertyMock(return_value=True), + ), ): yield @@ -135,6 +136,110 @@ async def test_sensors_no_job(hass: HomeAssistant, mock_config_entry, mock_api) assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == REVOLUTIONS_PER_MINUTE +async def test_sensors_idle_job_mk3( + hass: HomeAssistant, + mock_config_entry, + mock_api, + mock_job_api_idle_mk3, +) -> None: + """Test sensors while job state is idle (MK3).""" + assert await async_setup_component(hass, "prusalink", {}) + + state = hass.states.get("sensor.mock_title") + assert state is not None + assert state.state == "idle" + assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.ENUM + assert state.attributes[ATTR_OPTIONS] == [ + "idle", + "busy", + "printing", + "paused", + "finished", + "stopped", + "error", + "attention", + "ready", + ] + + state = hass.states.get("sensor.mock_title_heatbed_temperature") + assert state is not None + assert state.state == "41.9" + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfTemperature.CELSIUS + assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.TEMPERATURE + assert state.attributes[ATTR_STATE_CLASS] == SensorStateClass.MEASUREMENT + + state = hass.states.get("sensor.mock_title_nozzle_temperature") + assert state is not None + assert state.state == "47.8" + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfTemperature.CELSIUS + assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.TEMPERATURE + assert state.attributes[ATTR_STATE_CLASS] == SensorStateClass.MEASUREMENT + + state = hass.states.get("sensor.mock_title_heatbed_target_temperature") + assert state is not None + assert state.state == "60.5" + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfTemperature.CELSIUS + assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.TEMPERATURE + assert state.attributes[ATTR_STATE_CLASS] == SensorStateClass.MEASUREMENT + + state = hass.states.get("sensor.mock_title_nozzle_target_temperature") + assert state is not None + assert state.state == "210.1" + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfTemperature.CELSIUS + assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.TEMPERATURE + assert state.attributes[ATTR_STATE_CLASS] == SensorStateClass.MEASUREMENT + + state = hass.states.get("sensor.mock_title_z_height") + assert state is not None + assert state.state == "1.8" + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfLength.MILLIMETERS + assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.DISTANCE + assert state.attributes[ATTR_STATE_CLASS] == SensorStateClass.MEASUREMENT + + state = hass.states.get("sensor.mock_title_print_speed") + assert state is not None + assert state.state == "100" + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == PERCENTAGE + + state = hass.states.get("sensor.mock_title_material") + assert state is not None + assert state.state == "PLA" + + state = hass.states.get("sensor.mock_title_print_flow") + assert state is not None + assert state.state == "100" + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == PERCENTAGE + + state = hass.states.get("sensor.mock_title_progress") + assert state is not None + assert state.state == "unavailable" + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "%" + + state = hass.states.get("sensor.mock_title_filename") + assert state is not None + assert state.state == "unavailable" + + state = hass.states.get("sensor.mock_title_print_start") + assert state is not None + assert state.state == "unavailable" + assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.TIMESTAMP + + state = hass.states.get("sensor.mock_title_print_finish") + assert state is not None + assert state.state == "unavailable" + assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.TIMESTAMP + + state = hass.states.get("sensor.mock_title_hotend_fan") + assert state is not None + assert state.state == "100" + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == REVOLUTIONS_PER_MINUTE + + state = hass.states.get("sensor.mock_title_print_fan") + assert state is not None + assert state.state == "75" + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == REVOLUTIONS_PER_MINUTE + + async def test_sensors_active_job( hass: HomeAssistant, mock_config_entry, diff --git a/tests/components/ps4/conftest.py b/tests/components/ps4/conftest.py index 13b6a6e997c..d95acc7e92f 100644 --- a/tests/components/ps4/conftest.py +++ b/tests/components/ps4/conftest.py @@ -1,4 +1,5 @@ """Test configuration for PS4.""" + from collections.abc import Generator from unittest.mock import MagicMock, patch diff --git a/tests/components/ps4/test_config_flow.py b/tests/components/ps4/test_config_flow.py index 242470fa8e7..db478903d1e 100644 --- a/tests/components/ps4/test_config_flow.py +++ b/tests/components/ps4/test_config_flow.py @@ -1,4 +1,5 @@ """Define tests for the PlayStation 4 config flow.""" + from unittest.mock import patch from pyps4_2ndscreen.errors import CredentialTimeout @@ -126,8 +127,11 @@ async def test_full_flow_implementation(hass: HomeAssistant) -> None: assert result["step_id"] == "link" # User Input results in created entry. - with patch("pyps4_2ndscreen.Helper.link", return_value=(True, True)), patch( - "pyps4_2ndscreen.Helper.has_devices", return_value=[{"host-ip": MOCK_HOST}] + with ( + patch("pyps4_2ndscreen.Helper.link", return_value=(True, True)), + patch( + "pyps4_2ndscreen.Helper.has_devices", return_value=[{"host-ip": MOCK_HOST}] + ), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_CONFIG @@ -168,9 +172,12 @@ async def test_multiple_flow_implementation(hass: HomeAssistant) -> None: assert result["step_id"] == "link" # User Input results in created entry. - with patch("pyps4_2ndscreen.Helper.link", return_value=(True, True)), patch( - "pyps4_2ndscreen.Helper.has_devices", - return_value=[{"host-ip": MOCK_HOST}, {"host-ip": MOCK_HOST_ADDITIONAL}], + with ( + patch("pyps4_2ndscreen.Helper.link", return_value=(True, True)), + patch( + "pyps4_2ndscreen.Helper.has_devices", + return_value=[{"host-ip": MOCK_HOST}, {"host-ip": MOCK_HOST_ADDITIONAL}], + ), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_CONFIG @@ -190,9 +197,12 @@ async def test_multiple_flow_implementation(hass: HomeAssistant) -> None: # Test additional flow. # User Step Started, results in Step Mode: - with patch("pyps4_2ndscreen.Helper.port_bind", return_value=None), patch( - "pyps4_2ndscreen.Helper.has_devices", - return_value=[{"host-ip": MOCK_HOST}, {"host-ip": MOCK_HOST_ADDITIONAL}], + with ( + patch("pyps4_2ndscreen.Helper.port_bind", return_value=None), + patch( + "pyps4_2ndscreen.Helper.has_devices", + return_value=[{"host-ip": MOCK_HOST}, {"host-ip": MOCK_HOST_ADDITIONAL}], + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -220,10 +230,13 @@ async def test_multiple_flow_implementation(hass: HomeAssistant) -> None: assert result["step_id"] == "link" # Step Link - with patch( - "pyps4_2ndscreen.Helper.has_devices", - return_value=[{"host-ip": MOCK_HOST}, {"host-ip": MOCK_HOST_ADDITIONAL}], - ), patch("pyps4_2ndscreen.Helper.link", return_value=(True, True)): + with ( + patch( + "pyps4_2ndscreen.Helper.has_devices", + return_value=[{"host-ip": MOCK_HOST}, {"host-ip": MOCK_HOST_ADDITIONAL}], + ), + patch("pyps4_2ndscreen.Helper.link", return_value=(True, True)), + ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_CONFIG_ADDITIONAL ) @@ -340,11 +353,14 @@ async def test_0_pin(hass: HomeAssistant) -> None: assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "mode" - with patch( - "pyps4_2ndscreen.Helper.has_devices", return_value=[{"host-ip": MOCK_HOST}] - ), patch( - "homeassistant.components.ps4.config_flow.location.async_detect_location_info", - return_value=MOCK_LOCATION, + with ( + patch( + "pyps4_2ndscreen.Helper.has_devices", return_value=[{"host-ip": MOCK_HOST}] + ), + patch( + "homeassistant.components.ps4.config_flow.location.async_detect_location_info", + return_value=MOCK_LOCATION, + ), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], MOCK_AUTO @@ -354,10 +370,11 @@ async def test_0_pin(hass: HomeAssistant) -> None: mock_config = MOCK_CONFIG mock_config[CONF_CODE] = MOCK_CODE_LEAD_0 - with patch( - "pyps4_2ndscreen.Helper.link", return_value=(True, True) - ) as mock_call, patch( - "pyps4_2ndscreen.Helper.has_devices", return_value=[{"host-ip": MOCK_HOST}] + with ( + patch("pyps4_2ndscreen.Helper.link", return_value=(True, True)) as mock_call, + patch( + "pyps4_2ndscreen.Helper.has_devices", return_value=[{"host-ip": MOCK_HOST}] + ), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], mock_config diff --git a/tests/components/ps4/test_init.py b/tests/components/ps4/test_init.py index 1252348b3e0..238c3c15112 100644 --- a/tests/components/ps4/test_init.py +++ b/tests/components/ps4/test_init.py @@ -1,4 +1,5 @@ """Tests for the PS4 Integration.""" + from unittest.mock import MagicMock, patch from homeassistant import config_entries, data_entry_flow @@ -121,10 +122,13 @@ async def test_ps4_integration_setup(hass: HomeAssistant) -> None: async def test_creating_entry_sets_up_media_player(hass: HomeAssistant) -> None: """Test setting up PS4 loads the media player.""" mock_flow = "homeassistant.components.ps4.PlayStation4FlowHandler.async_step_user" - with patch( - "homeassistant.components.ps4.media_player.async_setup_entry", - return_value=True, - ) as mock_setup, patch(mock_flow, return_value=MOCK_FLOW_RESULT): + with ( + patch( + "homeassistant.components.ps4.media_player.async_setup_entry", + return_value=True, + ) as mock_setup, + patch(mock_flow, return_value=MOCK_FLOW_RESULT), + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -155,12 +159,15 @@ async def test_config_flow_entry_migrate( assert mock_e_entry.entity_id == mock_entity_id assert mock_e_entry.unique_id == MOCK_UNIQUE_ID - with patch( - "homeassistant.util.location.async_detect_location_info", - return_value=MOCK_LOCATION, - ), patch( - "homeassistant.helpers.entity_registry.async_get", - return_value=entity_registry, + with ( + patch( + "homeassistant.util.location.async_detect_location_info", + return_value=MOCK_LOCATION, + ), + patch( + "homeassistant.helpers.entity_registry.async_get", + return_value=entity_registry, + ), ): await ps4.async_migrate_entry(hass, mock_entry) @@ -204,9 +211,10 @@ def test_games_reformat_to_dict( ) -> None: """Test old data format is converted to new format.""" patch_load_json_object.return_value = MOCK_GAMES_DATA_OLD_STR_FORMAT - with patch( - "homeassistant.components.ps4.save_json", side_effect=MagicMock() - ), patch("os.path.isfile", return_value=True): + with ( + patch("homeassistant.components.ps4.save_json", side_effect=MagicMock()), + patch("os.path.isfile", return_value=True), + ): mock_games = ps4.load_games(hass, MOCK_ENTRY_ID) # New format is a nested dict. @@ -225,9 +233,10 @@ def test_games_reformat_to_dict( def test_load_games(hass: HomeAssistant, patch_load_json_object: MagicMock) -> None: """Test that games are loaded correctly.""" patch_load_json_object.return_value = MOCK_GAMES - with patch( - "homeassistant.components.ps4.save_json", side_effect=MagicMock() - ), patch("os.path.isfile", return_value=True): + with ( + patch("homeassistant.components.ps4.save_json", side_effect=MagicMock()), + patch("os.path.isfile", return_value=True), + ): mock_games = ps4.load_games(hass, MOCK_ENTRY_ID) assert isinstance(mock_games, dict) @@ -245,9 +254,10 @@ def test_loading_games_returns_dict( ) -> None: """Test that loading games always returns a dict.""" patch_load_json_object.side_effect = HomeAssistantError - with patch( - "homeassistant.components.ps4.save_json", side_effect=MagicMock() - ), patch("os.path.isfile", return_value=True): + with ( + patch("homeassistant.components.ps4.save_json", side_effect=MagicMock()), + patch("os.path.isfile", return_value=True), + ): mock_games = ps4.load_games(hass, MOCK_ENTRY_ID) assert isinstance(mock_games, dict) diff --git a/tests/components/ps4/test_media_player.py b/tests/components/ps4/test_media_player.py index 74b13d2f909..6adcad03016 100644 --- a/tests/components/ps4/test_media_player.py +++ b/tests/components/ps4/test_media_player.py @@ -1,4 +1,5 @@ """Tests for the PS4 media player platform.""" + from unittest.mock import MagicMock, patch from pyps4_2ndscreen.credential import get_ddp_message @@ -233,6 +234,7 @@ async def test_media_attributes_are_fetched(hass: HomeAssistant) -> None: with patch(mock_func, return_value=mock_result) as mock_fetch: await mock_ddp_response(hass, MOCK_STATUS_PLAYING) + await hass.async_block_till_done(wait_background_tasks=True) mock_state = hass.states.get(mock_entity_id) mock_attrs = dict(mock_state.attributes) @@ -254,6 +256,7 @@ async def test_media_attributes_are_fetched(hass: HomeAssistant) -> None: with patch(mock_func, return_value=mock_result) as mock_fetch_app: await mock_ddp_response(hass, MOCK_STATUS_PLAYING) + await hass.async_block_till_done(wait_background_tasks=True) mock_state = hass.states.get(mock_entity_id) mock_attrs = dict(mock_state.attributes) @@ -460,8 +463,9 @@ async def test_select_source( with patch("pyps4_2ndscreen.ps4.get_status", return_value=MOCK_STATUS_IDLE): mock_entity_id = await setup_mock_component(hass) - with patch("pyps4_2ndscreen.ps4.Ps4Async.start_title") as mock_call, patch( - "homeassistant.components.ps4.media_player.PS4Device.async_update" + with ( + patch("pyps4_2ndscreen.ps4.Ps4Async.start_title") as mock_call, + patch("homeassistant.components.ps4.media_player.PS4Device.async_update"), ): # Test with title name. await hass.services.async_call( @@ -482,8 +486,9 @@ async def test_select_source_caps( with patch("pyps4_2ndscreen.ps4.get_status", return_value=MOCK_STATUS_IDLE): mock_entity_id = await setup_mock_component(hass) - with patch("pyps4_2ndscreen.ps4.Ps4Async.start_title") as mock_call, patch( - "homeassistant.components.ps4.media_player.PS4Device.async_update" + with ( + patch("pyps4_2ndscreen.ps4.Ps4Async.start_title") as mock_call, + patch("homeassistant.components.ps4.media_player.PS4Device.async_update"), ): # Test with title name in caps. await hass.services.async_call( @@ -507,8 +512,9 @@ async def test_select_source_id( with patch("pyps4_2ndscreen.ps4.get_status", return_value=MOCK_STATUS_IDLE): mock_entity_id = await setup_mock_component(hass) - with patch("pyps4_2ndscreen.ps4.Ps4Async.start_title") as mock_call, patch( - "homeassistant.components.ps4.media_player.PS4Device.async_update" + with ( + patch("pyps4_2ndscreen.ps4.Ps4Async.start_title") as mock_call, + patch("homeassistant.components.ps4.media_player.PS4Device.async_update"), ): # Test with title ID. await hass.services.async_call( diff --git a/tests/components/pure_energie/conftest.py b/tests/components/pure_energie/conftest.py index 4bb89860ce3..40e6f803e83 100644 --- a/tests/components/pure_energie/conftest.py +++ b/tests/components/pure_energie/conftest.py @@ -1,4 +1,5 @@ """Fixtures for Pure Energie integration tests.""" + from collections.abc import Generator import json from unittest.mock import AsyncMock, MagicMock, patch diff --git a/tests/components/pure_energie/test_config_flow.py b/tests/components/pure_energie/test_config_flow.py index 992ce8bbb2c..596853800aa 100644 --- a/tests/components/pure_energie/test_config_flow.py +++ b/tests/components/pure_energie/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Pure Energie config flow.""" + from ipaddress import ip_address from unittest.mock import MagicMock diff --git a/tests/components/pure_energie/test_init.py b/tests/components/pure_energie/test_init.py index 80dce11e603..0a56240aaad 100644 --- a/tests/components/pure_energie/test_init.py +++ b/tests/components/pure_energie/test_init.py @@ -1,4 +1,5 @@ """Tests for the Pure Energie integration.""" + from unittest.mock import AsyncMock, MagicMock, patch from gridnet import GridNetConnectionError diff --git a/tests/components/purpleair/conftest.py b/tests/components/purpleair/conftest.py index 4883f79b349..1305c98308d 100644 --- a/tests/components/purpleair/conftest.py +++ b/tests/components/purpleair/conftest.py @@ -1,4 +1,5 @@ """Define fixtures for PurpleAir tests.""" + from unittest.mock import AsyncMock, Mock, patch from aiopurpleair.endpoints.sensors import NearbySensorResult @@ -73,9 +74,10 @@ def get_sensors_response_fixture(): @pytest.fixture(name="mock_aiopurpleair") async def mock_aiopurpleair_fixture(api): """Define a fixture to patch aiopurpleair.""" - with patch( - "homeassistant.components.purpleair.config_flow.API", return_value=api - ), patch("homeassistant.components.purpleair.coordinator.API", return_value=api): + with ( + patch("homeassistant.components.purpleair.config_flow.API", return_value=api), + patch("homeassistant.components.purpleair.coordinator.API", return_value=api), + ): yield api diff --git a/tests/components/purpleair/test_config_flow.py b/tests/components/purpleair/test_config_flow.py index b72ac7e3a79..efd0db6fd37 100644 --- a/tests/components/purpleair/test_config_flow.py +++ b/tests/components/purpleair/test_config_flow.py @@ -1,4 +1,5 @@ """Define tests for the PurpleAir config flow.""" + from unittest.mock import AsyncMock, patch from aiopurpleair.errors import InvalidApiKeyError, PurpleAirError diff --git a/tests/components/purpleair/test_diagnostics.py b/tests/components/purpleair/test_diagnostics.py index 85b078d0765..13dcd1338e0 100644 --- a/tests/components/purpleair/test_diagnostics.py +++ b/tests/components/purpleair/test_diagnostics.py @@ -1,4 +1,5 @@ """Test PurpleAir diagnostics.""" + from homeassistant.components.diagnostics import REDACTED from homeassistant.core import HomeAssistant diff --git a/tests/components/push/test_camera.py b/tests/components/push/test_camera.py index 55db1a94b2b..df296e7cb57 100644 --- a/tests/components/push/test_camera.py +++ b/tests/components/push/test_camera.py @@ -1,4 +1,5 @@ """The tests for generic camera component.""" + from datetime import timedelta from http import HTTPStatus import io diff --git a/tests/components/pushbullet/test_config_flow.py b/tests/components/pushbullet/test_config_flow.py index d7baef682b8..01c946286b4 100644 --- a/tests/components/pushbullet/test_config_flow.py +++ b/tests/components/pushbullet/test_config_flow.py @@ -1,4 +1,5 @@ """Test pushbullet config flow.""" + from unittest.mock import patch from pushbullet import InvalidKeyError, PushbulletError diff --git a/tests/components/pushbullet/test_init.py b/tests/components/pushbullet/test_init.py index 6f8e3776b35..72672f36176 100644 --- a/tests/components/pushbullet/test_init.py +++ b/tests/components/pushbullet/test_init.py @@ -1,4 +1,5 @@ """Test pushbullet integration.""" + from unittest.mock import patch from pushbullet import InvalidKeyError, PushbulletError diff --git a/tests/components/pushbullet/test_notify.py b/tests/components/pushbullet/test_notify.py index 53661f01229..bec098e3310 100644 --- a/tests/components/pushbullet/test_notify.py +++ b/tests/components/pushbullet/test_notify.py @@ -1,4 +1,5 @@ """Test pushbullet notification platform.""" + from http import HTTPStatus import requests_mock diff --git a/tests/components/pushover/test_config_flow.py b/tests/components/pushover/test_config_flow.py index 642f1b1b1bb..fcaedf2b5a6 100644 --- a/tests/components/pushover/test_config_flow.py +++ b/tests/components/pushover/test_config_flow.py @@ -1,4 +1,5 @@ """Test pushbullet config flow.""" + from unittest.mock import MagicMock, patch from pushover_complete import BadAPIRequestError diff --git a/tests/components/pushover/test_init.py b/tests/components/pushover/test_init.py index 261426345d1..15e537fd41f 100644 --- a/tests/components/pushover/test_init.py +++ b/tests/components/pushover/test_init.py @@ -1,4 +1,5 @@ """Test pushbullet integration.""" + from unittest.mock import MagicMock, patch from pushover_complete import BadAPIRequestError diff --git a/tests/components/pvoutput/conftest.py b/tests/components/pvoutput/conftest.py index 2bf85e5070e..122b55ca4c2 100644 --- a/tests/components/pvoutput/conftest.py +++ b/tests/components/pvoutput/conftest.py @@ -1,4 +1,5 @@ """Fixtures for PVOutput integration tests.""" + from __future__ import annotations from collections.abc import Generator @@ -37,10 +38,13 @@ def mock_setup_entry() -> Generator[AsyncMock, None, None]: @pytest.fixture def mock_pvoutput() -> Generator[None, MagicMock, None]: """Return a mocked PVOutput client.""" - with patch( - "homeassistant.components.pvoutput.coordinator.PVOutput", autospec=True - ) as pvoutput_mock, patch( - "homeassistant.components.pvoutput.config_flow.PVOutput", new=pvoutput_mock + with ( + patch( + "homeassistant.components.pvoutput.coordinator.PVOutput", autospec=True + ) as pvoutput_mock, + patch( + "homeassistant.components.pvoutput.config_flow.PVOutput", new=pvoutput_mock + ), ): pvoutput = pvoutput_mock.return_value pvoutput.status.return_value = Status.from_dict( diff --git a/tests/components/pvoutput/test_init.py b/tests/components/pvoutput/test_init.py index 87c432cebb2..c351c2a4296 100644 --- a/tests/components/pvoutput/test_init.py +++ b/tests/components/pvoutput/test_init.py @@ -1,4 +1,5 @@ """Tests for the PVOutput integration.""" + from unittest.mock import MagicMock from pvo import ( diff --git a/tests/components/pvoutput/test_sensor.py b/tests/components/pvoutput/test_sensor.py index 61f55e1f552..6d1e239f0f3 100644 --- a/tests/components/pvoutput/test_sensor.py +++ b/tests/components/pvoutput/test_sensor.py @@ -1,4 +1,5 @@ """Tests for the sensors provided by the PVOutput integration.""" + from homeassistant.components.pvoutput.const import DOMAIN from homeassistant.components.sensor import ( ATTR_STATE_CLASS, diff --git a/tests/components/pvpc_hourly_pricing/conftest.py b/tests/components/pvpc_hourly_pricing/conftest.py index 3bf1b08a51d..5a09d1f3487 100644 --- a/tests/components/pvpc_hourly_pricing/conftest.py +++ b/tests/components/pvpc_hourly_pricing/conftest.py @@ -1,4 +1,5 @@ """Tests for the pvpc_hourly_pricing integration.""" + from http import HTTPStatus import pytest diff --git a/tests/components/pvpc_hourly_pricing/test_config_flow.py b/tests/components/pvpc_hourly_pricing/test_config_flow.py index 087edcc1557..2a4c5688b5f 100644 --- a/tests/components/pvpc_hourly_pricing/test_config_flow.py +++ b/tests/components/pvpc_hourly_pricing/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for the pvpc_hourly_pricing config_flow.""" + from datetime import datetime, timedelta from freezegun.api import FrozenDateTimeFactory @@ -222,7 +223,7 @@ async def test_reauth( # check reauth trigger with bad-auth responses freezer.move_to(_MOCK_TIME_BAD_AUTH_RESPONSES) async_fire_time_changed(hass, _MOCK_TIME_BAD_AUTH_RESPONSES) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert pvpc_aioclient_mock.call_count == 6 result = hass.config_entries.flow.async_progress_by_handler(DOMAIN)[0] @@ -251,5 +252,5 @@ async def test_reauth( assert result["reason"] == "reauth_successful" assert pvpc_aioclient_mock.call_count == 8 - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert pvpc_aioclient_mock.call_count == 10 diff --git a/tests/components/python_script/test_init.py b/tests/components/python_script/test_init.py index 0692bdfd816..1c6fead6c4a 100644 --- a/tests/components/python_script/test_init.py +++ b/tests/components/python_script/test_init.py @@ -1,4 +1,5 @@ """Test the python_script component.""" + import logging from unittest.mock import mock_open, patch @@ -19,20 +20,28 @@ async def test_setup(hass: HomeAssistant) -> None: "/some/config/dir/python_scripts/hello.py", "/some/config/dir/python_scripts/world_beer.py", ] - with patch( - "homeassistant.components.python_script.os.path.isdir", return_value=True - ), patch("homeassistant.components.python_script.glob.iglob", return_value=scripts): + with ( + patch( + "homeassistant.components.python_script.os.path.isdir", return_value=True + ), + patch( + "homeassistant.components.python_script.glob.iglob", return_value=scripts + ), + ): res = await async_setup_component(hass, "python_script", {}) assert res assert hass.services.has_service("python_script", "hello") assert hass.services.has_service("python_script", "world_beer") - with patch( - "homeassistant.components.python_script.open", - mock_open(read_data="fake source"), - create=True, - ), patch("homeassistant.components.python_script.execute") as mock_ex: + with ( + patch( + "homeassistant.components.python_script.open", + mock_open(read_data="fake source"), + create=True, + ), + patch("homeassistant.components.python_script.execute") as mock_ex, + ): await hass.services.async_call( "python_script", "hello", {"some": "data"}, blocking=True ) @@ -69,7 +78,7 @@ hass.states.set('test.entity', data.get('name', 'not set')) """ hass.async_add_executor_job(execute, hass, "test.py", source, {"name": "paulus"}) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.is_state("test.entity", "paulus") @@ -87,7 +96,7 @@ print("This triggers warning.") """ hass.async_add_executor_job(execute, hass, "test.py", source, {}) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert "Don't use print() inside scripts." in caplog.text @@ -102,7 +111,7 @@ logger.info('Logging from inside script') """ hass.async_add_executor_job(execute, hass, "test.py", source, {}) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert "Logging from inside script" in caplog.text @@ -117,7 +126,7 @@ this is not valid Python """ hass.async_add_executor_job(execute, hass, "test.py", source, {}) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert "Error loading script test.py" in caplog.text @@ -131,8 +140,8 @@ async def test_execute_runtime_error( raise Exception('boom') """ - hass.async_add_executor_job(execute, hass, "test.py", source, {}) - await hass.async_block_till_done() + await hass.async_add_executor_job(execute, hass, "test.py", source, {}) + await hass.async_block_till_done(wait_background_tasks=True) assert "Error executing script: boom" in caplog.text @@ -144,7 +153,7 @@ raise Exception('boom') """ task = hass.async_add_executor_job(execute, hass, "test.py", source, {}, True) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert type(task.exception()) == HomeAssistantError assert "Error executing script (Exception): boom" in str(task.exception()) @@ -159,7 +168,7 @@ async def test_accessing_async_methods( hass.async_stop() """ - hass.async_add_executor_job(execute, hass, "test.py", source, {}) + await hass.async_add_executor_job(execute, hass, "test.py", source, {}) await hass.async_block_till_done() assert "Not allowed to access async methods" in caplog.text @@ -172,7 +181,7 @@ hass.async_stop() """ task = hass.async_add_executor_job(execute, hass, "test.py", source, {}, True) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert type(task.exception()) == ServiceValidationError assert "Not allowed to access async methods" in str(task.exception()) @@ -189,7 +198,7 @@ mylist = [1, 2, 3, 4] logger.info('Logging from inside script: %s %s' % (mydict["a"], mylist[2])) """ - hass.async_add_executor_job(execute, hass, "test.py", source, {}) + await hass.async_add_executor_job(execute, hass, "test.py", source, {}) await hass.async_block_till_done() assert "Logging from inside script: 1 3" in caplog.text @@ -208,7 +217,7 @@ async def test_accessing_forbidden_methods( "time.tzset()": "TimeWrapper.tzset", }.items(): caplog.records.clear() - hass.async_add_executor_job(execute, hass, "test.py", source, {}) + await hass.async_add_executor_job(execute, hass, "test.py", source, {}) await hass.async_block_till_done() assert f"Not allowed to access {name}" in caplog.text @@ -222,7 +231,7 @@ async def test_accessing_forbidden_methods_with_response(hass: HomeAssistant) -> "time.tzset()": "TimeWrapper.tzset", }.items(): task = hass.async_add_executor_job(execute, hass, "test.py", source, {}, True) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert type(task.exception()) == ServiceValidationError assert f"Not allowed to access {name}" in str(task.exception()) @@ -235,7 +244,7 @@ for i in [1, 2]: hass.states.set('hello.{}'.format(i), 'world') """ - hass.async_add_executor_job(execute, hass, "test.py", source, {}) + await hass.async_add_executor_job(execute, hass, "test.py", source, {}) await hass.async_block_till_done() assert hass.states.is_state("hello.1", "world") @@ -249,7 +258,7 @@ for index, value in enumerate(["earth", "mars"]): hass.states.set('hello.{}'.format(index), value) """ - hass.async_add_job(execute, hass, "test.py", source, {}) + await hass.async_add_executor_job(execute, hass, "test.py", source, {}) await hass.async_block_till_done() assert hass.states.is_state("hello.0", "earth") @@ -270,7 +279,7 @@ hass.states.set('hello.ab_list', '{}'.format(ab_list)) """ hass.async_add_executor_job(execute, hass, "test.py", source, {}) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.is_state("hello.a", "1") assert hass.states.is_state("hello.b", "2") @@ -293,7 +302,7 @@ hass.states.set('hello.b', a[1]) hass.states.set('hello.c', a[2]) """ hass.async_add_executor_job(execute, hass, "test.py", source, {}) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.is_state("hello.a", "1") assert hass.states.is_state("hello.b", "2") @@ -316,7 +325,7 @@ hass.states.set('module.datetime', """ hass.async_add_executor_job(execute, hass, "test.py", source, {}) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.is_state("module.time", "1986") assert hass.states.is_state("module.time_strptime", "12:34") @@ -342,7 +351,7 @@ def b(): b() """ hass.async_add_executor_job(execute, hass, "test.py", source, {}) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.is_state("hello.a", "one") assert hass.states.is_state("hello.b", "two") @@ -356,9 +365,14 @@ async def test_reload(hass: HomeAssistant) -> None: "/some/config/dir/python_scripts/hello.py", "/some/config/dir/python_scripts/world_beer.py", ] - with patch( - "homeassistant.components.python_script.os.path.isdir", return_value=True - ), patch("homeassistant.components.python_script.glob.iglob", return_value=scripts): + with ( + patch( + "homeassistant.components.python_script.os.path.isdir", return_value=True + ), + patch( + "homeassistant.components.python_script.glob.iglob", return_value=scripts + ), + ): res = await async_setup_component(hass, "python_script", {}) assert res @@ -370,9 +384,14 @@ async def test_reload(hass: HomeAssistant) -> None: "/some/config/dir/python_scripts/hello2.py", "/some/config/dir/python_scripts/world_beer.py", ] - with patch( - "homeassistant.components.python_script.os.path.isdir", return_value=True - ), patch("homeassistant.components.python_script.glob.iglob", return_value=scripts): + with ( + patch( + "homeassistant.components.python_script.os.path.isdir", return_value=True + ), + patch( + "homeassistant.components.python_script.glob.iglob", return_value=scripts + ), + ): await hass.services.async_call("python_script", "reload", {}, blocking=True) assert not hass.services.has_service("python_script", "hello") @@ -402,14 +421,19 @@ async def test_service_descriptions(hass: HomeAssistant) -> None: f"{hass.config.config_dir}/{FOLDER}/services.yaml": service_descriptions1 } - with patch( - "homeassistant.components.python_script.os.path.isdir", return_value=True - ), patch( - "homeassistant.components.python_script.glob.iglob", return_value=scripts1 - ), patch( - "homeassistant.components.python_script.os.path.exists", return_value=True - ), patch_yaml_files( - services_yaml1, + with ( + patch( + "homeassistant.components.python_script.os.path.isdir", return_value=True + ), + patch( + "homeassistant.components.python_script.glob.iglob", return_value=scripts1 + ), + patch( + "homeassistant.components.python_script.os.path.exists", return_value=True + ), + patch_yaml_files( + services_yaml1, + ), ): await async_setup_component(hass, DOMAIN, {}) @@ -451,14 +475,19 @@ async def test_service_descriptions(hass: HomeAssistant) -> None: f"{hass.config.config_dir}/{FOLDER}/services.yaml": service_descriptions2 } - with patch( - "homeassistant.components.python_script.os.path.isdir", return_value=True - ), patch( - "homeassistant.components.python_script.glob.iglob", return_value=scripts2 - ), patch( - "homeassistant.components.python_script.os.path.exists", return_value=True - ), patch_yaml_files( - services_yaml2, + with ( + patch( + "homeassistant.components.python_script.os.path.isdir", return_value=True + ), + patch( + "homeassistant.components.python_script.glob.iglob", return_value=scripts2 + ), + patch( + "homeassistant.components.python_script.os.path.exists", return_value=True + ), + patch_yaml_files( + services_yaml2, + ), ): await hass.services.async_call(DOMAIN, "reload", {}, blocking=True) descriptions = await async_get_all_descriptions(hass) @@ -488,7 +517,7 @@ time.sleep(5) with patch("homeassistant.components.python_script.time.sleep"): hass.async_add_executor_job(execute, hass, "test.py", source, {}) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert caplog.text.count("time.sleep") == 1 @@ -502,9 +531,14 @@ async def test_execute_with_output( scripts = [ "/some/config/dir/python_scripts/hello.py", ] - with patch( - "homeassistant.components.python_script.os.path.isdir", return_value=True - ), patch("homeassistant.components.python_script.glob.iglob", return_value=scripts): + with ( + patch( + "homeassistant.components.python_script.os.path.isdir", return_value=True + ), + patch( + "homeassistant.components.python_script.glob.iglob", return_value=scripts + ), + ): await async_setup_component(hass, "python_script", {}) source = """ @@ -541,9 +575,14 @@ async def test_execute_no_output( scripts = [ "/some/config/dir/python_scripts/hello.py", ] - with patch( - "homeassistant.components.python_script.os.path.isdir", return_value=True - ), patch("homeassistant.components.python_script.glob.iglob", return_value=scripts): + with ( + patch( + "homeassistant.components.python_script.os.path.isdir", return_value=True + ), + patch( + "homeassistant.components.python_script.glob.iglob", return_value=scripts + ), + ): await async_setup_component(hass, "python_script", {}) source = """ @@ -575,20 +614,28 @@ async def test_execute_wrong_output_type(hass: HomeAssistant) -> None: scripts = [ "/some/config/dir/python_scripts/hello.py", ] - with patch( - "homeassistant.components.python_script.os.path.isdir", return_value=True - ), patch("homeassistant.components.python_script.glob.iglob", return_value=scripts): + with ( + patch( + "homeassistant.components.python_script.os.path.isdir", return_value=True + ), + patch( + "homeassistant.components.python_script.glob.iglob", return_value=scripts + ), + ): await async_setup_component(hass, "python_script", {}) source = """ output = f"hello {data.get('name', 'World')}" """ - with patch( - "homeassistant.components.python_script.open", - mock_open(read_data=source), - create=True, - ), pytest.raises(ServiceValidationError): + with ( + patch( + "homeassistant.components.python_script.open", + mock_open(read_data=source), + create=True, + ), + pytest.raises(ServiceValidationError), + ): await hass.services.async_call( "python_script", "hello", @@ -617,7 +664,7 @@ hass.states.set('hello.c', c) """ hass.async_add_executor_job(execute, hass, "aug_assign.py", source, {}) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get("hello.a").state == str(((10 + 20) * 5) - 8) assert hass.states.get("hello.b").state == ("foo" + "bar") * 2 @@ -639,5 +686,5 @@ async def test_prohibited_augmented_assignment_operations( ) -> None: """Test that prohibited augmented assignment operations raise an error.""" hass.async_add_executor_job(execute, hass, "aug_assign_prohibited.py", case, {}) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert error in caplog.text diff --git a/tests/components/qbittorrent/conftest.py b/tests/components/qbittorrent/conftest.py index 448f68db81e..9a5ead35a05 100644 --- a/tests/components/qbittorrent/conftest.py +++ b/tests/components/qbittorrent/conftest.py @@ -1,4 +1,5 @@ """Fixtures for testing qBittorrent component.""" + from collections.abc import Generator from unittest.mock import AsyncMock, patch diff --git a/tests/components/qbittorrent/test_config_flow.py b/tests/components/qbittorrent/test_config_flow.py index 4131b9142e2..8a424f5c87b 100644 --- a/tests/components/qbittorrent/test_config_flow.py +++ b/tests/components/qbittorrent/test_config_flow.py @@ -1,4 +1,5 @@ """Test the qBittorrent config flow.""" + import pytest from requests.exceptions import RequestException import requests_mock diff --git a/tests/components/qingping/__init__.py b/tests/components/qingping/__init__.py index e06d80e66c5..8b61166f61c 100644 --- a/tests/components/qingping/__init__.py +++ b/tests/components/qingping/__init__.py @@ -1,6 +1,5 @@ """Tests for the Qingping integration.""" - from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo NOT_QINGPING_SERVICE_INFO = BluetoothServiceInfo( diff --git a/tests/components/qingping/test_binary_sensor.py b/tests/components/qingping/test_binary_sensor.py index f201b3b55ff..adf7686b20e 100644 --- a/tests/components/qingping/test_binary_sensor.py +++ b/tests/components/qingping/test_binary_sensor.py @@ -1,4 +1,5 @@ """Test the Qingping binary sensors.""" + from datetime import timedelta import time @@ -72,9 +73,12 @@ async def test_binary_sensor_restore_state(hass: HomeAssistant) -> None: # Fastforward time without BLE advertisements monotonic_now = start_monotonic + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1 - with patch_bluetooth_time( - monotonic_now, - ), patch_all_discovered_devices([]): + with ( + patch_bluetooth_time( + monotonic_now, + ), + patch_all_discovered_devices([]), + ): async_fire_time_changed( hass, dt_util.utcnow() diff --git a/tests/components/qingping/test_config_flow.py b/tests/components/qingping/test_config_flow.py index aed1b45286a..c5b5dd94cc2 100644 --- a/tests/components/qingping/test_config_flow.py +++ b/tests/components/qingping/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Qingping config flow.""" + from unittest.mock import patch from homeassistant import config_entries diff --git a/tests/components/qingping/test_sensor.py b/tests/components/qingping/test_sensor.py index 12e3ec85c52..160841d9ff5 100644 --- a/tests/components/qingping/test_sensor.py +++ b/tests/components/qingping/test_sensor.py @@ -1,4 +1,5 @@ """Test the Qingping sensors.""" + from datetime import timedelta import time @@ -82,9 +83,12 @@ async def test_binary_sensor_restore_state(hass: HomeAssistant) -> None: # Fastforward time without BLE advertisements monotonic_now = start_monotonic + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1 - with patch_bluetooth_time( - monotonic_now, - ), patch_all_discovered_devices([]): + with ( + patch_bluetooth_time( + monotonic_now, + ), + patch_all_discovered_devices([]), + ): async_fire_time_changed( hass, dt_util.utcnow() diff --git a/tests/components/qld_bushfire/test_geo_location.py b/tests/components/qld_bushfire/test_geo_location.py index 9cd12ea0447..522c5fabe90 100644 --- a/tests/components/qld_bushfire/test_geo_location.py +++ b/tests/components/qld_bushfire/test_geo_location.py @@ -1,4 +1,5 @@ """The tests for the Queensland Bushfire Alert Feed platform.""" + import datetime from unittest.mock import MagicMock, call, patch @@ -177,7 +178,7 @@ async def test_setup(hass: HomeAssistant, freezer: FrozenDateTimeFactory) -> Non # so no changes to entities. mock_feed_update.return_value = "OK_NO_DATA", None async_fire_time_changed(hass, utcnow + 2 * SCAN_INTERVAL) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) all_states = hass.states.async_all() assert len(all_states) == 3 @@ -185,7 +186,7 @@ async def test_setup(hass: HomeAssistant, freezer: FrozenDateTimeFactory) -> Non # Simulate an update - empty data, removes all entities mock_feed_update.return_value = "ERROR", None async_fire_time_changed(hass, utcnow + 3 * SCAN_INTERVAL) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) all_states = hass.states.async_all() assert len(all_states) == 0 @@ -198,10 +199,13 @@ async def test_setup_with_custom_location(hass: HomeAssistant) -> None: "1234", "Title 1", 20.5, (38.1, -3.1), category="Category 1" ) - with patch( - "georss_qld_bushfire_alert_client.feed_manager.QldBushfireAlertFeed", - wraps=QldBushfireAlertFeed, - ) as mock_feed, patch("georss_client.feed.GeoRssFeed.update") as mock_feed_update: + with ( + patch( + "georss_qld_bushfire_alert_client.feed_manager.QldBushfireAlertFeed", + wraps=QldBushfireAlertFeed, + ) as mock_feed, + patch("georss_client.feed.GeoRssFeed.update") as mock_feed_update, + ): mock_feed_update.return_value = "OK", [mock_entry_1] with assert_setup_component(1, geo_location.DOMAIN): diff --git a/tests/components/qnap/conftest.py b/tests/components/qnap/conftest.py index 7f26763d4d7..5c6d5eb65fc 100644 --- a/tests/components/qnap/conftest.py +++ b/tests/components/qnap/conftest.py @@ -1,4 +1,5 @@ """Setup the QNAP tests.""" + from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch diff --git a/tests/components/qnap/test_config_flow.py b/tests/components/qnap/test_config_flow.py index 75af07cbf8b..881086b9e10 100644 --- a/tests/components/qnap/test_config_flow.py +++ b/tests/components/qnap/test_config_flow.py @@ -1,4 +1,5 @@ """Test the QNAP config flow.""" + from unittest.mock import MagicMock import pytest diff --git a/tests/components/qnap_qsw/test_button.py b/tests/components/qnap_qsw/test_button.py index 27b5fcb075d..01a1951791c 100644 --- a/tests/components/qnap_qsw/test_button.py +++ b/tests/components/qnap_qsw/test_button.py @@ -18,13 +18,16 @@ async def test_qnap_buttons(hass: HomeAssistant) -> None: assert state assert state.state == STATE_UNKNOWN - with patch( - "homeassistant.components.qnap_qsw.QnapQswApi.get_users_verification", - return_value=USERS_VERIFICATION_MOCK, - ) as mock_users_verification, patch( - "homeassistant.components.qnap_qsw.QnapQswApi.post_system_command", - return_value=SYSTEM_COMMAND_MOCK, - ) as mock_post_system_command: + with ( + patch( + "homeassistant.components.qnap_qsw.QnapQswApi.get_users_verification", + return_value=USERS_VERIFICATION_MOCK, + ) as mock_users_verification, + patch( + "homeassistant.components.qnap_qsw.QnapQswApi.post_system_command", + return_value=SYSTEM_COMMAND_MOCK, + ) as mock_post_system_command, + ): await hass.services.async_call( BUTTON_DOMAIN, SERVICE_PRESS, diff --git a/tests/components/qnap_qsw/test_config_flow.py b/tests/components/qnap_qsw/test_config_flow.py index 57f6094c850..26a6581b207 100644 --- a/tests/components/qnap_qsw/test_config_flow.py +++ b/tests/components/qnap_qsw/test_config_flow.py @@ -31,18 +31,23 @@ TEST_USERNAME = "test-username" async def test_form(hass: HomeAssistant) -> None: """Test that the form is served with valid input.""" - with patch( - "homeassistant.components.qnap_qsw.async_setup_entry", - return_value=True, - ) as mock_setup_entry, patch( - "homeassistant.components.qnap_qsw.QnapQswApi.get_live", - return_value=LIVE_MOCK, - ), patch( - "homeassistant.components.qnap_qsw.QnapQswApi.get_system_board", - return_value=SYSTEM_BOARD_MOCK, - ), patch( - "homeassistant.components.qnap_qsw.QnapQswApi.post_users_login", - return_value=USERS_LOGIN_MOCK, + with ( + patch( + "homeassistant.components.qnap_qsw.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + patch( + "homeassistant.components.qnap_qsw.QnapQswApi.get_live", + return_value=LIVE_MOCK, + ), + patch( + "homeassistant.components.qnap_qsw.QnapQswApi.get_system_board", + return_value=SYSTEM_BOARD_MOCK, + ), + patch( + "homeassistant.components.qnap_qsw.QnapQswApi.post_users_login", + return_value=USERS_LOGIN_MOCK, + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} @@ -162,18 +167,23 @@ async def test_dhcp_flow(hass: HomeAssistant) -> None: assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "discovered_connection" - with patch( - "homeassistant.components.qnap_qsw.async_setup_entry", - return_value=True, - ) as mock_setup_entry, patch( - "homeassistant.components.qnap_qsw.QnapQswApi.get_live", - return_value=LIVE_MOCK, - ), patch( - "homeassistant.components.qnap_qsw.QnapQswApi.get_system_board", - return_value=SYSTEM_BOARD_MOCK, - ), patch( - "homeassistant.components.qnap_qsw.QnapQswApi.post_users_login", - return_value=USERS_LOGIN_MOCK, + with ( + patch( + "homeassistant.components.qnap_qsw.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + patch( + "homeassistant.components.qnap_qsw.QnapQswApi.get_live", + return_value=LIVE_MOCK, + ), + patch( + "homeassistant.components.qnap_qsw.QnapQswApi.get_system_board", + return_value=SYSTEM_BOARD_MOCK, + ), + patch( + "homeassistant.components.qnap_qsw.QnapQswApi.post_users_login", + return_value=USERS_LOGIN_MOCK, + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], diff --git a/tests/components/qnap_qsw/test_coordinator.py b/tests/components/qnap_qsw/test_coordinator.py index 8a5f07e8173..388bab635c8 100644 --- a/tests/components/qnap_qsw/test_coordinator.py +++ b/tests/components/qnap_qsw/test_coordinator.py @@ -39,40 +39,52 @@ async def test_coordinator_client_connector_error( entry = MockConfigEntry(domain=DOMAIN, data=CONFIG) entry.add_to_hass(hass) - with patch( - "homeassistant.components.qnap_qsw.QnapQswApi.get_firmware_condition", - return_value=FIRMWARE_CONDITION_MOCK, - ) as mock_firmware_condition, patch( - "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_lacp_info", - return_value=LACP_INFO_MOCK, - ) as mock_lacp_info, patch( - "homeassistant.components.qnap_qsw.QnapQswApi.get_ports_statistics", - return_value=PORTS_STATISTICS_MOCK, - ) as mock_ports_statistics, patch( - "homeassistant.components.qnap_qsw.QnapQswApi.get_ports_status", - return_value=PORTS_STATUS_MOCK, - ) as mock_ports_status, patch( - "homeassistant.components.qnap_qsw.QnapQswApi.get_system_board", - return_value=SYSTEM_BOARD_MOCK, - ) as mock_system_board, patch( - "homeassistant.components.qnap_qsw.QnapQswApi.get_system_sensor", - return_value=SYSTEM_SENSOR_MOCK, - ) as mock_system_sensor, patch( - "homeassistant.components.qnap_qsw.QnapQswApi.get_system_time", - return_value=SYSTEM_TIME_MOCK, - ) as mock_system_time, patch( - "homeassistant.components.qnap_qsw.QnapQswApi.get_users_verification", - return_value=USERS_VERIFICATION_MOCK, - ) as mock_users_verification, patch( - "homeassistant.components.qnap_qsw.QnapQswApi.post_users_login", - return_value=USERS_LOGIN_MOCK, - ) as mock_users_login: + with ( + patch( + "homeassistant.components.qnap_qsw.QnapQswApi.get_firmware_condition", + return_value=FIRMWARE_CONDITION_MOCK, + ) as mock_firmware_condition, + patch( + "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_lacp_info", + return_value=LACP_INFO_MOCK, + ) as mock_lacp_info, + patch( + "homeassistant.components.qnap_qsw.QnapQswApi.get_ports_statistics", + return_value=PORTS_STATISTICS_MOCK, + ) as mock_ports_statistics, + patch( + "homeassistant.components.qnap_qsw.QnapQswApi.get_ports_status", + return_value=PORTS_STATUS_MOCK, + ) as mock_ports_status, + patch( + "homeassistant.components.qnap_qsw.QnapQswApi.get_system_board", + return_value=SYSTEM_BOARD_MOCK, + ) as mock_system_board, + patch( + "homeassistant.components.qnap_qsw.QnapQswApi.get_system_sensor", + return_value=SYSTEM_SENSOR_MOCK, + ) as mock_system_sensor, + patch( + "homeassistant.components.qnap_qsw.QnapQswApi.get_system_time", + return_value=SYSTEM_TIME_MOCK, + ) as mock_system_time, + patch( + "homeassistant.components.qnap_qsw.QnapQswApi.get_users_verification", + return_value=USERS_VERIFICATION_MOCK, + ) as mock_users_verification, + patch( + "homeassistant.components.qnap_qsw.QnapQswApi.post_users_login", + return_value=USERS_LOGIN_MOCK, + ) as mock_users_login, + ): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -103,7 +115,7 @@ async def test_coordinator_client_connector_error( mock_system_sensor.side_effect = QswError freezer.tick(DATA_SCAN_INTERVAL) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) mock_system_sensor.assert_called_once() mock_users_verification.assert_called() @@ -115,7 +127,7 @@ async def test_coordinator_client_connector_error( mock_firmware_update_check.side_effect = APIError freezer.tick(FW_SCAN_INTERVAL) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) mock_firmware_update_check.assert_called_once() mock_firmware_update_check.reset_mock() @@ -123,7 +135,7 @@ async def test_coordinator_client_connector_error( mock_firmware_update_check.side_effect = QswError freezer.tick(FW_SCAN_INTERVAL) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) mock_firmware_update_check.assert_called_once() diff --git a/tests/components/qnap_qsw/test_init.py b/tests/components/qnap_qsw/test_init.py index fedfdd26543..9b2d10f1ed0 100644 --- a/tests/components/qnap_qsw/test_init.py +++ b/tests/components/qnap_qsw/test_init.py @@ -21,15 +21,19 @@ async def test_firmware_check_error(hass: HomeAssistant) -> None: ) 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, + 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() @@ -44,15 +48,19 @@ 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( - "homeassistant.components.qnap_qsw.QnapQswApi.update", - return_value=None, + with ( + patch( + "homeassistant.components.qnap_qsw.QnapQswApi.check_firmware", + return_value=None, + ), + 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() diff --git a/tests/components/qnap_qsw/test_update.py b/tests/components/qnap_qsw/test_update.py index f6eb9705912..df7b4b9af6b 100644 --- a/tests/components/qnap_qsw/test_update.py +++ b/tests/components/qnap_qsw/test_update.py @@ -47,16 +47,20 @@ async def test_qnap_qsw_update(hass: HomeAssistant) -> None: ) assert update.attributes[ATTR_IN_PROGRESS] is False - with 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_users_verification", - return_value=USERS_VERIFICATION_MOCK, - ) as mock_users_verification, patch( - "homeassistant.components.qnap_qsw.QnapQswApi.post_firmware_update_live", - return_value=FIRMWARE_UPDATE_LIVE_MOCK, - ) as mock_firmware_update_live: + with ( + 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_users_verification", + return_value=USERS_VERIFICATION_MOCK, + ) as mock_users_verification, + patch( + "homeassistant.components.qnap_qsw.QnapQswApi.post_firmware_update_live", + return_value=FIRMWARE_UPDATE_LIVE_MOCK, + ) as mock_firmware_update_live, + ): await hass.services.async_call( UPDATE_DOMAIN, SERVICE_INSTALL, diff --git a/tests/components/qnap_qsw/util.py b/tests/components/qnap_qsw/util.py index b0dd5d5bf60..63238bb30a1 100644 --- a/tests/components/qnap_qsw/util.py +++ b/tests/components/qnap_qsw/util.py @@ -503,39 +503,51 @@ async def async_init_integration( ) config_entry.add_to_hass(hass) - with patch( - "homeassistant.components.qnap_qsw.QnapQswApi.get_firmware_condition", - return_value=FIRMWARE_CONDITION_MOCK, - ), 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_lacp_info", - return_value=LACP_INFO_MOCK, - ), patch( - "homeassistant.components.qnap_qsw.QnapQswApi.get_ports_statistics", - return_value=PORTS_STATISTICS_MOCK, - ), patch( - "homeassistant.components.qnap_qsw.QnapQswApi.get_ports_status", - return_value=PORTS_STATUS_MOCK, - ), patch( - "homeassistant.components.qnap_qsw.QnapQswApi.get_system_board", - return_value=SYSTEM_BOARD_MOCK, - ), patch( - "homeassistant.components.qnap_qsw.QnapQswApi.get_system_sensor", - return_value=SYSTEM_SENSOR_MOCK, - ), 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, + with ( + patch( + "homeassistant.components.qnap_qsw.QnapQswApi.get_firmware_condition", + return_value=FIRMWARE_CONDITION_MOCK, + ), + 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_lacp_info", + return_value=LACP_INFO_MOCK, + ), + patch( + "homeassistant.components.qnap_qsw.QnapQswApi.get_ports_statistics", + return_value=PORTS_STATISTICS_MOCK, + ), + patch( + "homeassistant.components.qnap_qsw.QnapQswApi.get_ports_status", + return_value=PORTS_STATUS_MOCK, + ), + patch( + "homeassistant.components.qnap_qsw.QnapQswApi.get_system_board", + return_value=SYSTEM_BOARD_MOCK, + ), + patch( + "homeassistant.components.qnap_qsw.QnapQswApi.get_system_sensor", + return_value=SYSTEM_SENSOR_MOCK, + ), + 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, + ), ): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/qwikswitch/test_init.py b/tests/components/qwikswitch/test_init.py index 6d440691780..32a0d0d20db 100644 --- a/tests/components/qwikswitch/test_init.py +++ b/tests/components/qwikswitch/test_init.py @@ -1,4 +1,5 @@ """Test qwikswitch sensors.""" + import asyncio from typing import Any from unittest.mock import Mock @@ -173,6 +174,7 @@ async def test_switch_device( await hass.services.async_call( "switch", "turn_off", {"entity_id": "switch.switch_1"}, blocking=True ) + await hass.async_block_till_done() assert ( "GET", URL("http://127.0.0.1:2020/@a00001=0"), @@ -253,6 +255,7 @@ async def test_light_device( await hass.services.async_call( "light", "turn_on", {"entity_id": "light.dim_3"}, blocking=True ) + await hass.async_block_till_done() assert ( "GET", URL("http://127.0.0.1:2020/@a00003=100"), diff --git a/tests/components/rabbitair/test_config_flow.py b/tests/components/rabbitair/test_config_flow.py index b0c85fbf402..4b5867b441b 100644 --- a/tests/components/rabbitair/test_config_flow.py +++ b/tests/components/rabbitair/test_config_flow.py @@ -1,4 +1,5 @@ """Test the RabbitAir config flow.""" + from __future__ import annotations from collections.abc import Generator @@ -44,8 +45,9 @@ def use_mocked_zeroconf(mock_async_zeroconf): @pytest.fixture def rabbitair_connect() -> Generator[None, None, None]: """Mock connection.""" - with patch("rabbitair.UdpClient.get_info", return_value=get_mock_info()), patch( - "rabbitair.UdpClient.get_state", return_value=get_mock_state() + with ( + patch("rabbitair.UdpClient.get_info", return_value=get_mock_info()), + patch("rabbitair.UdpClient.get_state", return_value=get_mock_state()), ): yield diff --git a/tests/components/rachio/test_config_flow.py b/tests/components/rachio/test_config_flow.py index 26083f51e63..b7325349746 100644 --- a/tests/components/rachio/test_config_flow.py +++ b/tests/components/rachio/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Rachio config flow.""" + from ipaddress import ip_address from unittest.mock import MagicMock, patch @@ -38,12 +39,16 @@ async def test_form(hass: HomeAssistant) -> None: info=({"status": 200}, {"id": "myid"}), ) - with patch( - "homeassistant.components.rachio.config_flow.Rachio", return_value=rachio_mock - ), patch( - "homeassistant.components.rachio.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.rachio.config_flow.Rachio", + return_value=rachio_mock, + ), + patch( + "homeassistant.components.rachio.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { diff --git a/tests/components/radarr/__init__.py b/tests/components/radarr/__init__.py index 47204ebf537..0e6f708d329 100644 --- a/tests/components/radarr/__init__.py +++ b/tests/components/radarr/__init__.py @@ -1,4 +1,5 @@ """Tests for the Radarr component.""" + from http import HTTPStatus from unittest.mock import patch diff --git a/tests/components/radarr/test_binary_sensor.py b/tests/components/radarr/test_binary_sensor.py index cd1df721d5f..bd622ded78d 100644 --- a/tests/components/radarr/test_binary_sensor.py +++ b/tests/components/radarr/test_binary_sensor.py @@ -1,4 +1,5 @@ """The tests for Radarr binary sensor platform.""" + import pytest from homeassistant.components.binary_sensor import BinarySensorDeviceClass diff --git a/tests/components/radarr/test_calendar.py b/tests/components/radarr/test_calendar.py index 61e9bc27c9b..e82760cadba 100644 --- a/tests/components/radarr/test_calendar.py +++ b/tests/components/radarr/test_calendar.py @@ -1,4 +1,5 @@ """The tests for Radarr calendar platform.""" + from datetime import timedelta from freezegun.api import FrozenDateTimeFactory diff --git a/tests/components/radarr/test_config_flow.py b/tests/components/radarr/test_config_flow.py index 5eab7c02bb9..9733393836a 100644 --- a/tests/components/radarr/test_config_flow.py +++ b/tests/components/radarr/test_config_flow.py @@ -1,4 +1,5 @@ """Test Radarr config flow.""" + from unittest.mock import patch from aiopyarr import exceptions diff --git a/tests/components/radarr/test_init.py b/tests/components/radarr/test_init.py index 62660c12874..c4226d3f3fb 100644 --- a/tests/components/radarr/test_init.py +++ b/tests/components/radarr/test_init.py @@ -1,4 +1,5 @@ """Test Radarr integration.""" + import pytest from homeassistant.components.radarr.const import DEFAULT_NAME, DOMAIN diff --git a/tests/components/radarr/test_sensor.py b/tests/components/radarr/test_sensor.py index 11f55b712cd..b75034acc8f 100644 --- a/tests/components/radarr/test_sensor.py +++ b/tests/components/radarr/test_sensor.py @@ -1,4 +1,5 @@ """The tests for Radarr sensor platform.""" + import pytest from homeassistant.components.sensor import ( diff --git a/tests/components/radio_browser/conftest.py b/tests/components/radio_browser/conftest.py index 5a5b888d944..fa732912dc0 100644 --- a/tests/components/radio_browser/conftest.py +++ b/tests/components/radio_browser/conftest.py @@ -1,4 +1,5 @@ """Fixtures for the Radio Browser integration tests.""" + from __future__ import annotations from collections.abc import Generator diff --git a/tests/components/radio_browser/test_config_flow.py b/tests/components/radio_browser/test_config_flow.py index d958efc4e50..0c0f2f479a8 100644 --- a/tests/components/radio_browser/test_config_flow.py +++ b/tests/components/radio_browser/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Radio Browser config flow.""" + from unittest.mock import AsyncMock from homeassistant.components.radio_browser.const import DOMAIN diff --git a/tests/components/radiotherm/test_config_flow.py b/tests/components/radiotherm/test_config_flow.py index 8da7c850aa5..7729dfb86b7 100644 --- a/tests/components/radiotherm/test_config_flow.py +++ b/tests/components/radiotherm/test_config_flow.py @@ -33,13 +33,16 @@ async def test_form(hass: HomeAssistant) -> None: assert result["type"] == "form" assert result["errors"] == {} - with patch( - "homeassistant.components.radiotherm.data.radiotherm.get_thermostat", - return_value=_mock_radiotherm(), - ), patch( - "homeassistant.components.radiotherm.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.radiotherm.data.radiotherm.get_thermostat", + return_value=_mock_radiotherm(), + ), + patch( + "homeassistant.components.radiotherm.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -209,12 +212,15 @@ async def test_user_unique_id_already_exists(hass: HomeAssistant) -> None: assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} - with patch( - "homeassistant.components.radiotherm.data.radiotherm.get_thermostat", - return_value=_mock_radiotherm(), - ), patch( - "homeassistant.components.radiotherm.async_setup_entry", - return_value=True, + with ( + patch( + "homeassistant.components.radiotherm.data.radiotherm.get_thermostat", + return_value=_mock_radiotherm(), + ), + patch( + "homeassistant.components.radiotherm.async_setup_entry", + return_value=True, + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], diff --git a/tests/components/rainbird/conftest.py b/tests/components/rainbird/conftest.py index 53df24264df..10101986007 100644 --- a/tests/components/rainbird/conftest.py +++ b/tests/components/rainbird/conftest.py @@ -171,12 +171,15 @@ def aioclient_mock(hass: HomeAssistant) -> Generator[AiohttpClientMocker, None, hass.bus.async_listen_once(EVENT_HOMEASSISTANT_CLOSE, close_session) return session - with patch( - "homeassistant.components.rainbird.async_create_clientsession", - side_effect=create_session, - ), patch( - "homeassistant.components.rainbird.config_flow.async_create_clientsession", - side_effect=create_session, + with ( + patch( + "homeassistant.components.rainbird.async_create_clientsession", + side_effect=create_session, + ), + patch( + "homeassistant.components.rainbird.config_flow.async_create_clientsession", + side_effect=create_session, + ), ): yield mocker diff --git a/tests/components/rainbird/test_binary_sensor.py b/tests/components/rainbird/test_binary_sensor.py index 826a7635c53..83a45de93ff 100644 --- a/tests/components/rainbird/test_binary_sensor.py +++ b/tests/components/rainbird/test_binary_sensor.py @@ -1,6 +1,5 @@ """Tests for rainbird sensor platform.""" - from http import HTTPStatus import pytest @@ -53,7 +52,6 @@ async def test_rainsensor( assert rainsensor.state == expected_state assert rainsensor.attributes == { "friendly_name": "Rain Bird Controller Rainsensor", - "icon": "mdi:water", } diff --git a/tests/components/rainbird/test_calendar.py b/tests/components/rainbird/test_calendar.py index 673d32998d5..6258ac56249 100644 --- a/tests/components/rainbird/test_calendar.py +++ b/tests/components/rainbird/test_calendar.py @@ -1,6 +1,5 @@ """Tests for rainbird calendar platform.""" - from collections.abc import Awaitable, Callable import datetime from http import HTTPStatus @@ -204,7 +203,6 @@ async def test_event_state( "description": "", "location": "", "friendly_name": "Rain Bird Controller", - "icon": "mdi:sprinkler", } assert state.state == expected_state @@ -248,7 +246,6 @@ async def test_no_schedule( assert state.state == "unavailable" assert state.attributes == { "friendly_name": "Rain Bird Controller", - "icon": "mdi:sprinkler", } client = await hass_client() @@ -276,7 +273,6 @@ async def test_program_schedule_disabled( assert state.state == "off" assert state.attributes == { "friendly_name": "Rain Bird Controller", - "icon": "mdi:sprinkler", } diff --git a/tests/components/rainbird/test_number.py b/tests/components/rainbird/test_number.py index b0c1856819e..0830a238fd7 100644 --- a/tests/components/rainbird/test_number.py +++ b/tests/components/rainbird/test_number.py @@ -57,7 +57,6 @@ async def test_number_values( assert raindelay.state == expected_state assert raindelay.attributes == { "friendly_name": "Rain Bird Controller Rain delay", - "icon": "mdi:water-off", "min": 0, "max": 14, "mode": "auto", diff --git a/tests/components/rainbird/test_sensor.py b/tests/components/rainbird/test_sensor.py index ebe852ccf46..730e1d50809 100644 --- a/tests/components/rainbird/test_sensor.py +++ b/tests/components/rainbird/test_sensor.py @@ -51,7 +51,6 @@ async def test_sensors( assert raindelay.state == expected_state assert raindelay.attributes == { "friendly_name": "Rain Bird Controller Raindelay", - "icon": "mdi:water-off", } entity_entry = entity_registry.async_get("sensor.rain_bird_controller_raindelay") diff --git a/tests/components/rainforest_eagle/__init__.py b/tests/components/rainforest_eagle/__init__.py index 3d0a51e8452..67d0a3180d4 100644 --- a/tests/components/rainforest_eagle/__init__.py +++ b/tests/components/rainforest_eagle/__init__.py @@ -1,6 +1,5 @@ """Tests for the Rainforest Eagle integration.""" - MOCK_CLOUD_ID = "12345" MOCK_200_RESPONSE_WITH_PRICE = { "zigbee:InstantaneousDemand": { diff --git a/tests/components/rainforest_eagle/conftest.py b/tests/components/rainforest_eagle/conftest.py index d3a8687f724..9ea607b1db4 100644 --- a/tests/components/rainforest_eagle/conftest.py +++ b/tests/components/rainforest_eagle/conftest.py @@ -1,4 +1,5 @@ """Conftest for rainforest_eagle.""" + from unittest.mock import AsyncMock, Mock, patch import pytest diff --git a/tests/components/rainforest_eagle/test_config_flow.py b/tests/components/rainforest_eagle/test_config_flow.py index d9b66b0feec..191a7a4793e 100644 --- a/tests/components/rainforest_eagle/test_config_flow.py +++ b/tests/components/rainforest_eagle/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Rainforest Eagle config flow.""" + from unittest.mock import patch from homeassistant import config_entries @@ -24,13 +25,16 @@ async def test_form(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.FORM assert result["errors"] is None - with patch( - "homeassistant.components.rainforest_eagle.data.async_get_type", - return_value=(TYPE_EAGLE_200, "mock-hw"), - ), patch( - "homeassistant.components.rainforest_eagle.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.rainforest_eagle.data.async_get_type", + return_value=(TYPE_EAGLE_200, "mock-hw"), + ), + patch( + "homeassistant.components.rainforest_eagle.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { diff --git a/tests/components/rainforest_eagle/test_diagnostics.py b/tests/components/rainforest_eagle/test_diagnostics.py index a3331244209..ed13c33f7b8 100644 --- a/tests/components/rainforest_eagle/test_diagnostics.py +++ b/tests/components/rainforest_eagle/test_diagnostics.py @@ -1,4 +1,5 @@ """Test the Rainforest Eagle diagnostics.""" + from homeassistant.components.diagnostics import REDACTED from homeassistant.components.rainforest_eagle.const import ( CONF_CLOUD_ID, diff --git a/tests/components/rainforest_eagle/test_sensor.py b/tests/components/rainforest_eagle/test_sensor.py index 5e76a81932a..31630913a70 100644 --- a/tests/components/rainforest_eagle/test_sensor.py +++ b/tests/components/rainforest_eagle/test_sensor.py @@ -1,4 +1,5 @@ """Tests for rainforest eagle sensors.""" + from homeassistant.components.rainforest_eagle.const import DOMAIN from homeassistant.core import HomeAssistant diff --git a/tests/components/rainforest_raven/test_config_flow.py b/tests/components/rainforest_raven/test_config_flow.py index c7364c9435e..d7b188d6b14 100644 --- a/tests/components/rainforest_raven/test_config_flow.py +++ b/tests/components/rainforest_raven/test_config_flow.py @@ -1,4 +1,5 @@ """Test Rainforest RAVEn config flow.""" + from unittest.mock import patch from aioraven.device import RAVEnConnectionError diff --git a/tests/components/rainforest_raven/test_diagnostics.py b/tests/components/rainforest_raven/test_diagnostics.py index 639eacadc76..fe01dc1d0f9 100644 --- a/tests/components/rainforest_raven/test_diagnostics.py +++ b/tests/components/rainforest_raven/test_diagnostics.py @@ -1,4 +1,5 @@ """Test the Rainforest Eagle diagnostics.""" + from dataclasses import asdict import pytest diff --git a/tests/components/rainforest_raven/test_init.py b/tests/components/rainforest_raven/test_init.py index b99d94f4b43..1cc50971e09 100644 --- a/tests/components/rainforest_raven/test_init.py +++ b/tests/components/rainforest_raven/test_init.py @@ -1,4 +1,5 @@ """Tests for the Rainforest RAVEn component initialisation.""" + import pytest from homeassistant.components.rainforest_raven.const import DOMAIN diff --git a/tests/components/rainforest_raven/test_sensor.py b/tests/components/rainforest_raven/test_sensor.py index e637e22ecf9..36e6572149f 100644 --- a/tests/components/rainforest_raven/test_sensor.py +++ b/tests/components/rainforest_raven/test_sensor.py @@ -1,4 +1,5 @@ """Tests for the Rainforest RAVEn sensors.""" + import pytest from homeassistant.core import HomeAssistant diff --git a/tests/components/rainmachine/conftest.py b/tests/components/rainmachine/conftest.py index 2697e908c94..9b0f8f0442a 100644 --- a/tests/components/rainmachine/conftest.py +++ b/tests/components/rainmachine/conftest.py @@ -1,4 +1,5 @@ """Define test fixtures for RainMachine.""" + import json from unittest.mock import AsyncMock, patch @@ -129,13 +130,16 @@ def data_zones_fixture(): @pytest.fixture(name="setup_rainmachine") async def setup_rainmachine_fixture(hass, client, config): """Define a fixture to set up RainMachine.""" - with patch( - "homeassistant.components.rainmachine.Client", return_value=client - ), patch( - "homeassistant.components.rainmachine.config_flow.Client", return_value=client - ), patch( - "homeassistant.components.rainmachine.PLATFORMS", - [], + with ( + patch("homeassistant.components.rainmachine.Client", return_value=client), + patch( + "homeassistant.components.rainmachine.config_flow.Client", + return_value=client, + ), + patch( + "homeassistant.components.rainmachine.PLATFORMS", + [], + ), ): assert await async_setup_component(hass, DOMAIN, config) await hass.async_block_till_done() diff --git a/tests/components/rainmachine/test_config_flow.py b/tests/components/rainmachine/test_config_flow.py index 631f1d5a3f8..1c065a8f7ce 100644 --- a/tests/components/rainmachine/test_config_flow.py +++ b/tests/components/rainmachine/test_config_flow.py @@ -1,4 +1,5 @@ """Define tests for the OpenUV config flow.""" + from ipaddress import ip_address from unittest.mock import patch @@ -81,10 +82,14 @@ async def test_migrate_1_2( assert entity_entry.entity_id == entity_id assert entity_entry.unique_id == old_unique_id - with patch( - "homeassistant.components.rainmachine.async_setup_entry", return_value=True - ), patch( - "homeassistant.components.rainmachine.config_flow.Client", return_value=client + with ( + patch( + "homeassistant.components.rainmachine.async_setup_entry", return_value=True + ), + patch( + "homeassistant.components.rainmachine.config_flow.Client", + return_value=client, + ), ): await setup.async_setup_component(hass, DOMAIN, {}) await hass.async_block_till_done() @@ -234,10 +239,14 @@ async def test_step_homekit_zeroconf_new_controller_when_some_exist( assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" - with patch( - "homeassistant.components.rainmachine.async_setup_entry", return_value=True - ), patch( - "homeassistant.components.rainmachine.config_flow.Client", return_value=client + with ( + patch( + "homeassistant.components.rainmachine.async_setup_entry", return_value=True + ), + patch( + "homeassistant.components.rainmachine.config_flow.Client", + return_value=client, + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], diff --git a/tests/components/rainmachine/test_diagnostics.py b/tests/components/rainmachine/test_diagnostics.py index 2180bf2a20e..6ea50e5b102 100644 --- a/tests/components/rainmachine/test_diagnostics.py +++ b/tests/components/rainmachine/test_diagnostics.py @@ -1,4 +1,5 @@ """Test RainMachine diagnostics.""" + from regenmaschine.errors import RainMachineError from homeassistant.components.diagnostics import REDACTED diff --git a/tests/components/random/test_binary_sensor.py b/tests/components/random/test_binary_sensor.py index 3bcf43ae22e..8884b48a1c1 100644 --- a/tests/components/random/test_binary_sensor.py +++ b/tests/components/random/test_binary_sensor.py @@ -1,4 +1,5 @@ """The test for the Random binary sensor platform.""" + from unittest.mock import patch from homeassistant.core import HomeAssistant diff --git a/tests/components/random/test_config_flow.py b/tests/components/random/test_config_flow.py index 909e866ea92..4ffa59da4e4 100644 --- a/tests/components/random/test_config_flow.py +++ b/tests/components/random/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Random config flow.""" + from typing import Any from unittest.mock import patch @@ -22,7 +23,7 @@ from tests.common import MockConfigEntry "extra_input", "extra_options", ), - ( + [ ( "binary_sensor", {}, @@ -46,7 +47,7 @@ from tests.common import MockConfigEntry {}, {"minimum": 0, "maximum": 20}, ), - ), + ], ) async def test_config_flow( hass: HomeAssistant, @@ -134,7 +135,7 @@ async def test_wrong_uom( "extra_options", "options_options", ), - ( + [ ( "sensor", { @@ -150,7 +151,7 @@ async def test_wrong_uom( "unit_of_measurement": UnitOfPower.WATT, }, ), - ), + ], ) async def test_options( hass: HomeAssistant, diff --git a/tests/components/random/test_sensor.py b/tests/components/random/test_sensor.py index 4682ae9078e..d2b953aea75 100644 --- a/tests/components/random/test_sensor.py +++ b/tests/components/random/test_sensor.py @@ -1,4 +1,5 @@ """The test for the random number sensor platform.""" + from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component diff --git a/tests/components/rapt_ble/test_config_flow.py b/tests/components/rapt_ble/test_config_flow.py index 46b7265b238..b71843bd44f 100644 --- a/tests/components/rapt_ble/test_config_flow.py +++ b/tests/components/rapt_ble/test_config_flow.py @@ -1,4 +1,5 @@ """Test the RAPT config flow.""" + from unittest.mock import patch from homeassistant import config_entries diff --git a/tests/components/raspberry_pi/test_config_flow.py b/tests/components/raspberry_pi/test_config_flow.py index 68306b3ea9a..05fea6ed3d3 100644 --- a/tests/components/raspberry_pi/test_config_flow.py +++ b/tests/components/raspberry_pi/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Raspberry Pi config flow.""" + from unittest.mock import patch from homeassistant.components.raspberry_pi.const import DOMAIN diff --git a/tests/components/raspberry_pi/test_hardware.py b/tests/components/raspberry_pi/test_hardware.py index d41fbf2f5e1..19d9a65fadd 100644 --- a/tests/components/raspberry_pi/test_hardware.py +++ b/tests/components/raspberry_pi/test_hardware.py @@ -1,4 +1,5 @@ """Test the Raspberry Pi hardware platform.""" + from unittest.mock import patch import pytest @@ -18,6 +19,7 @@ async def test_hardware_info( """Test we can get the board info.""" mock_integration(hass, MockModule("hassio")) await async_setup_component(hass, HASSIO_DOMAIN, {}) + await hass.async_block_till_done() # Setup the config entry config_entry = MockConfigEntry( @@ -70,6 +72,7 @@ async def test_hardware_info_fail( """Test async_info raises if os_info is not as expected.""" mock_integration(hass, MockModule("hassio")) await async_setup_component(hass, HASSIO_DOMAIN, {}) + await hass.async_block_till_done() # Setup the config entry config_entry = MockConfigEntry( diff --git a/tests/components/raspberry_pi/test_init.py b/tests/components/raspberry_pi/test_init.py index 88518cc00b0..80b5eedf2af 100644 --- a/tests/components/raspberry_pi/test_init.py +++ b/tests/components/raspberry_pi/test_init.py @@ -1,4 +1,5 @@ """Test the Raspberry Pi integration.""" + from unittest.mock import patch import pytest @@ -36,11 +37,12 @@ async def test_setup_entry(hass: HomeAssistant) -> None: ) config_entry.add_to_hass(hass) assert not hass.config_entries.async_entries("rpi_power") - with patch( - "homeassistant.components.raspberry_pi.get_os_info", - return_value={"board": "rpi"}, - ) as mock_get_os_info, patch( - "homeassistant.components.rpi_power.config_flow.new_under_voltage" + with ( + patch( + "homeassistant.components.raspberry_pi.get_os_info", + return_value={"board": "rpi"}, + ) as mock_get_os_info, + patch("homeassistant.components.rpi_power.config_flow.new_under_voltage"), ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/rdw/conftest.py b/tests/components/rdw/conftest.py index 5fe40b0b497..7e9f485eaef 100644 --- a/tests/components/rdw/conftest.py +++ b/tests/components/rdw/conftest.py @@ -1,4 +1,5 @@ """Fixtures for RDW integration tests.""" + from __future__ import annotations from collections.abc import Generator diff --git a/tests/components/rdw/test_binary_sensor.py b/tests/components/rdw/test_binary_sensor.py index aea188db773..4c21f5f881f 100644 --- a/tests/components/rdw/test_binary_sensor.py +++ b/tests/components/rdw/test_binary_sensor.py @@ -1,4 +1,5 @@ """Tests for the sensors provided by the RDW integration.""" + from homeassistant.components.binary_sensor import BinarySensorDeviceClass from homeassistant.components.rdw.const import DOMAIN from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_FRIENDLY_NAME, ATTR_ICON @@ -23,7 +24,6 @@ async def test_vehicle_binary_sensors( assert entry.unique_id == "11ZKZ3_liability_insured" assert state.state == "off" 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.skoda_11zkz3_pending_recall") diff --git a/tests/components/rdw/test_diagnostics.py b/tests/components/rdw/test_diagnostics.py index 28b7714fcce..a5e8c72dba1 100644 --- a/tests/components/rdw/test_diagnostics.py +++ b/tests/components/rdw/test_diagnostics.py @@ -1,4 +1,5 @@ """Tests for the diagnostics data provided by the RDW integration.""" + from syrupy import SnapshotAssertion from homeassistant.core import HomeAssistant diff --git a/tests/components/rdw/test_init.py b/tests/components/rdw/test_init.py index b31b0aa8d81..6f4454325d5 100644 --- a/tests/components/rdw/test_init.py +++ b/tests/components/rdw/test_init.py @@ -1,4 +1,5 @@ """Tests for the RDW integration.""" + from unittest.mock import AsyncMock, MagicMock, patch from homeassistant.components.rdw.const import DOMAIN diff --git a/tests/components/rdw/test_sensor.py b/tests/components/rdw/test_sensor.py index 3e7ad7ab89e..ef8ce48e7ce 100644 --- a/tests/components/rdw/test_sensor.py +++ b/tests/components/rdw/test_sensor.py @@ -1,4 +1,5 @@ """Tests for the sensors provided by the RDW integration.""" + from homeassistant.components.rdw.const import DOMAIN from homeassistant.components.sensor import ATTR_STATE_CLASS, SensorDeviceClass from homeassistant.const import ( diff --git a/tests/components/recollect_waste/conftest.py b/tests/components/recollect_waste/conftest.py index 861d4804f85..360dd8aac98 100644 --- a/tests/components/recollect_waste/conftest.py +++ b/tests/components/recollect_waste/conftest.py @@ -1,4 +1,5 @@ """Define test fixtures for ReCollect Waste.""" + from datetime import date from unittest.mock import AsyncMock, Mock, patch @@ -55,12 +56,15 @@ def pickup_events_fixture(): @pytest.fixture(name="mock_aiorecollect") async def mock_aiorecollect_fixture(client): """Define a fixture to patch aiorecollect.""" - with patch( - "homeassistant.components.recollect_waste.Client", - return_value=client, - ), patch( - "homeassistant.components.recollect_waste.config_flow.Client", - return_value=client, + with ( + patch( + "homeassistant.components.recollect_waste.Client", + return_value=client, + ), + patch( + "homeassistant.components.recollect_waste.config_flow.Client", + return_value=client, + ), ): yield diff --git a/tests/components/recollect_waste/test_config_flow.py b/tests/components/recollect_waste/test_config_flow.py index 9688400a8ea..a65b0d27a74 100644 --- a/tests/components/recollect_waste/test_config_flow.py +++ b/tests/components/recollect_waste/test_config_flow.py @@ -1,4 +1,5 @@ """Define tests for the ReCollect Waste config flow.""" + from unittest.mock import AsyncMock, patch from aiorecollect.errors import RecollectError diff --git a/tests/components/recollect_waste/test_diagnostics.py b/tests/components/recollect_waste/test_diagnostics.py index 69ff1596d7c..6c8549786e8 100644 --- a/tests/components/recollect_waste/test_diagnostics.py +++ b/tests/components/recollect_waste/test_diagnostics.py @@ -1,4 +1,5 @@ """Test ReCollect Waste diagnostics.""" + from homeassistant.components.diagnostics import REDACTED from homeassistant.core import HomeAssistant diff --git a/tests/components/recorder/auto_repairs/events/test_schema.py b/tests/components/recorder/auto_repairs/events/test_schema.py index 1dc9fb1f560..5713e287222 100644 --- a/tests/components/recorder/auto_repairs/events/test_schema.py +++ b/tests/components/recorder/auto_repairs/events/test_schema.py @@ -12,7 +12,7 @@ from tests.typing import RecorderInstanceGenerator @pytest.mark.parametrize("enable_schema_validation", [True]) -@pytest.mark.parametrize("db_engine", ("mysql", "postgresql")) +@pytest.mark.parametrize("db_engine", ["mysql", "postgresql"]) async def test_validate_db_schema_fix_float_issue( async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant, @@ -23,14 +23,18 @@ async def test_validate_db_schema_fix_float_issue( Note: The test uses SQLite, the purpose is only to exercise the code. """ - with patch( - "homeassistant.components.recorder.core.Recorder.dialect_name", db_engine - ), patch( - "homeassistant.components.recorder.auto_repairs.schema._validate_db_schema_precision", - return_value={"events.double precision"}, - ), patch( - "homeassistant.components.recorder.migration._modify_columns" - ) as modify_columns_mock: + with ( + patch( + "homeassistant.components.recorder.core.Recorder.dialect_name", db_engine + ), + patch( + "homeassistant.components.recorder.auto_repairs.schema._validate_db_schema_precision", + return_value={"events.double precision"}, + ), + patch( + "homeassistant.components.recorder.migration._modify_columns" + ) as modify_columns_mock, + ): await async_setup_recorder_instance(hass) await async_wait_recording_done(hass) @@ -55,11 +59,12 @@ async def test_validate_db_schema_fix_utf8_issue_event_data( Note: The test uses SQLite, the purpose is only to exercise the code. """ - with patch( - "homeassistant.components.recorder.core.Recorder.dialect_name", "mysql" - ), patch( - "homeassistant.components.recorder.auto_repairs.schema._validate_table_schema_supports_utf8", - return_value={"event_data.4-byte UTF-8"}, + with ( + patch("homeassistant.components.recorder.core.Recorder.dialect_name", "mysql"), + patch( + "homeassistant.components.recorder.auto_repairs.schema._validate_table_schema_supports_utf8", + return_value={"event_data.4-byte UTF-8"}, + ), ): await async_setup_recorder_instance(hass) await async_wait_recording_done(hass) @@ -85,11 +90,12 @@ async def test_validate_db_schema_fix_collation_issue( Note: The test uses SQLite, the purpose is only to exercise the code. """ - with patch( - "homeassistant.components.recorder.core.Recorder.dialect_name", "mysql" - ), patch( - "homeassistant.components.recorder.auto_repairs.schema._validate_table_schema_has_correct_collation", - return_value={"events.utf8mb4_unicode_ci"}, + with ( + patch("homeassistant.components.recorder.core.Recorder.dialect_name", "mysql"), + patch( + "homeassistant.components.recorder.auto_repairs.schema._validate_table_schema_has_correct_collation", + return_value={"events.utf8mb4_unicode_ci"}, + ), ): await async_setup_recorder_instance(hass) await async_wait_recording_done(hass) diff --git a/tests/components/recorder/auto_repairs/states/test_schema.py b/tests/components/recorder/auto_repairs/states/test_schema.py index f3d733c7c45..7d14a873bfe 100644 --- a/tests/components/recorder/auto_repairs/states/test_schema.py +++ b/tests/components/recorder/auto_repairs/states/test_schema.py @@ -12,7 +12,7 @@ from tests.typing import RecorderInstanceGenerator @pytest.mark.parametrize("enable_schema_validation", [True]) -@pytest.mark.parametrize("db_engine", ("mysql", "postgresql")) +@pytest.mark.parametrize("db_engine", ["mysql", "postgresql"]) async def test_validate_db_schema_fix_float_issue( async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant, @@ -23,14 +23,18 @@ async def test_validate_db_schema_fix_float_issue( Note: The test uses SQLite, the purpose is only to exercise the code. """ - with patch( - "homeassistant.components.recorder.core.Recorder.dialect_name", db_engine - ), patch( - "homeassistant.components.recorder.auto_repairs.schema._validate_db_schema_precision", - return_value={"states.double precision"}, - ), patch( - "homeassistant.components.recorder.migration._modify_columns" - ) as modify_columns_mock: + with ( + patch( + "homeassistant.components.recorder.core.Recorder.dialect_name", db_engine + ), + patch( + "homeassistant.components.recorder.auto_repairs.schema._validate_db_schema_precision", + return_value={"states.double precision"}, + ), + patch( + "homeassistant.components.recorder.migration._modify_columns" + ) as modify_columns_mock, + ): await async_setup_recorder_instance(hass) await async_wait_recording_done(hass) @@ -41,6 +45,7 @@ async def test_validate_db_schema_fix_float_issue( ) modification = [ "last_changed_ts DOUBLE PRECISION", + "last_reported_ts DOUBLE PRECISION", "last_updated_ts DOUBLE PRECISION", ] modify_columns_mock.assert_called_once_with(ANY, ANY, "states", modification) @@ -56,11 +61,12 @@ async def test_validate_db_schema_fix_utf8_issue_states( Note: The test uses SQLite, the purpose is only to exercise the code. """ - with patch( - "homeassistant.components.recorder.core.Recorder.dialect_name", "mysql" - ), patch( - "homeassistant.components.recorder.auto_repairs.schema._validate_table_schema_supports_utf8", - return_value={"states.4-byte UTF-8"}, + with ( + patch("homeassistant.components.recorder.core.Recorder.dialect_name", "mysql"), + patch( + "homeassistant.components.recorder.auto_repairs.schema._validate_table_schema_supports_utf8", + return_value={"states.4-byte UTF-8"}, + ), ): await async_setup_recorder_instance(hass) await async_wait_recording_done(hass) @@ -85,11 +91,12 @@ async def test_validate_db_schema_fix_utf8_issue_state_attributes( Note: The test uses SQLite, the purpose is only to exercise the code. """ - with patch( - "homeassistant.components.recorder.core.Recorder.dialect_name", "mysql" - ), patch( - "homeassistant.components.recorder.auto_repairs.schema._validate_table_schema_supports_utf8", - return_value={"state_attributes.4-byte UTF-8"}, + with ( + patch("homeassistant.components.recorder.core.Recorder.dialect_name", "mysql"), + patch( + "homeassistant.components.recorder.auto_repairs.schema._validate_table_schema_supports_utf8", + return_value={"state_attributes.4-byte UTF-8"}, + ), ): await async_setup_recorder_instance(hass) await async_wait_recording_done(hass) @@ -115,11 +122,12 @@ async def test_validate_db_schema_fix_collation_issue( Note: The test uses SQLite, the purpose is only to exercise the code. """ - with patch( - "homeassistant.components.recorder.core.Recorder.dialect_name", "mysql" - ), patch( - "homeassistant.components.recorder.auto_repairs.schema._validate_table_schema_has_correct_collation", - return_value={"states.utf8mb4_unicode_ci"}, + with ( + patch("homeassistant.components.recorder.core.Recorder.dialect_name", "mysql"), + patch( + "homeassistant.components.recorder.auto_repairs.schema._validate_table_schema_has_correct_collation", + return_value={"states.utf8mb4_unicode_ci"}, + ), ): await async_setup_recorder_instance(hass) await async_wait_recording_done(hass) diff --git a/tests/components/recorder/auto_repairs/statistics/test_duplicates.py b/tests/components/recorder/auto_repairs/statistics/test_duplicates.py index c2aee941efc..2a1c3c5d209 100644 --- a/tests/components/recorder/auto_repairs/statistics/test_duplicates.py +++ b/tests/components/recorder/auto_repairs/statistics/test_duplicates.py @@ -1,4 +1,5 @@ """Test removing statistics duplicates.""" + from collections.abc import Callable import importlib from pathlib import Path @@ -77,11 +78,12 @@ def test_duplicate_statistics_handle_integrity_error( } ] - with patch.object( - statistics, "_statistics_exists", return_value=False - ), patch.object( - statistics, "_insert_statistics", wraps=statistics._insert_statistics - ) as insert_statistics_mock: + with ( + patch.object(statistics, "_statistics_exists", return_value=False), + patch.object( + statistics, "_insert_statistics", wraps=statistics._insert_statistics + ) as insert_statistics_mock, + ): async_add_external_statistics( hass, external_energy_metadata_1, external_energy_statistics_1 ) @@ -163,11 +165,17 @@ def test_delete_metadata_duplicates( } # Create some duplicated statistics_meta with schema version 28 - with patch.object(recorder, "db_schema", old_db_schema), patch.object( - recorder.migration, "SCHEMA_VERSION", old_db_schema.SCHEMA_VERSION - ), patch( - "homeassistant.components.recorder.core.create_engine", new=_create_engine_28 - ), get_test_home_assistant() as hass: + with ( + patch.object(recorder, "db_schema", old_db_schema), + patch.object( + recorder.migration, "SCHEMA_VERSION", old_db_schema.SCHEMA_VERSION + ), + patch( + "homeassistant.components.recorder.core.create_engine", + new=_create_engine_28, + ), + get_test_home_assistant() as hass, + ): recorder_helper.async_initialize_recorder(hass) setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) wait_recording_done(hass) @@ -255,11 +263,17 @@ def test_delete_metadata_duplicates_many( } # Create some duplicated statistics with schema version 28 - with patch.object(recorder, "db_schema", old_db_schema), patch.object( - recorder.migration, "SCHEMA_VERSION", old_db_schema.SCHEMA_VERSION - ), patch( - "homeassistant.components.recorder.core.create_engine", new=_create_engine_28 - ), get_test_home_assistant() as hass: + with ( + patch.object(recorder, "db_schema", old_db_schema), + patch.object( + recorder.migration, "SCHEMA_VERSION", old_db_schema.SCHEMA_VERSION + ), + patch( + "homeassistant.components.recorder.core.create_engine", + new=_create_engine_28, + ), + get_test_home_assistant() as hass, + ): recorder_helper.async_initialize_recorder(hass) setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) wait_recording_done(hass) diff --git a/tests/components/recorder/auto_repairs/statistics/test_schema.py b/tests/components/recorder/auto_repairs/statistics/test_schema.py index 032cd57ce49..0badceee0d2 100644 --- a/tests/components/recorder/auto_repairs/statistics/test_schema.py +++ b/tests/components/recorder/auto_repairs/statistics/test_schema.py @@ -21,11 +21,12 @@ async def test_validate_db_schema_fix_utf8_issue( Note: The test uses SQLite, the purpose is only to exercise the code. """ - with patch( - "homeassistant.components.recorder.core.Recorder.dialect_name", "mysql" - ), patch( - "homeassistant.components.recorder.auto_repairs.schema._validate_table_schema_supports_utf8", - return_value={"statistics_meta.4-byte UTF-8"}, + with ( + patch("homeassistant.components.recorder.core.Recorder.dialect_name", "mysql"), + patch( + "homeassistant.components.recorder.auto_repairs.schema._validate_table_schema_supports_utf8", + return_value={"statistics_meta.4-byte UTF-8"}, + ), ): await async_setup_recorder_instance(hass) await async_wait_recording_done(hass) @@ -42,8 +43,8 @@ async def test_validate_db_schema_fix_utf8_issue( @pytest.mark.parametrize("enable_schema_validation", [True]) -@pytest.mark.parametrize("table", ("statistics_short_term", "statistics")) -@pytest.mark.parametrize("db_engine", ("mysql", "postgresql")) +@pytest.mark.parametrize("table", ["statistics_short_term", "statistics"]) +@pytest.mark.parametrize("db_engine", ["mysql", "postgresql"]) async def test_validate_db_schema_fix_float_issue( async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant, @@ -55,14 +56,18 @@ async def test_validate_db_schema_fix_float_issue( Note: The test uses SQLite, the purpose is only to exercise the code. """ - with patch( - "homeassistant.components.recorder.core.Recorder.dialect_name", db_engine - ), patch( - "homeassistant.components.recorder.auto_repairs.schema._validate_db_schema_precision", - return_value={f"{table}.double precision"}, - ), patch( - "homeassistant.components.recorder.migration._modify_columns" - ) as modify_columns_mock: + with ( + patch( + "homeassistant.components.recorder.core.Recorder.dialect_name", db_engine + ), + patch( + "homeassistant.components.recorder.auto_repairs.schema._validate_db_schema_precision", + return_value={f"{table}.double precision"}, + ), + patch( + "homeassistant.components.recorder.migration._modify_columns" + ) as modify_columns_mock, + ): await async_setup_recorder_instance(hass) await async_wait_recording_done(hass) @@ -94,11 +99,12 @@ async def test_validate_db_schema_fix_collation_issue( Note: The test uses SQLite, the purpose is only to exercise the code. """ - with patch( - "homeassistant.components.recorder.core.Recorder.dialect_name", "mysql" - ), patch( - "homeassistant.components.recorder.auto_repairs.schema._validate_table_schema_has_correct_collation", - return_value={"statistics.utf8mb4_unicode_ci"}, + with ( + patch("homeassistant.components.recorder.core.Recorder.dialect_name", "mysql"), + patch( + "homeassistant.components.recorder.auto_repairs.schema._validate_table_schema_has_correct_collation", + return_value={"statistics.utf8mb4_unicode_ci"}, + ), ): await async_setup_recorder_instance(hass) await async_wait_recording_done(hass) diff --git a/tests/components/recorder/auto_repairs/test_schema.py b/tests/components/recorder/auto_repairs/test_schema.py index 83fb64dca6c..14c74e2614e 100644 --- a/tests/components/recorder/auto_repairs/test_schema.py +++ b/tests/components/recorder/auto_repairs/test_schema.py @@ -23,7 +23,7 @@ from tests.typing import RecorderInstanceGenerator @pytest.mark.parametrize("enable_schema_validation", [True]) -@pytest.mark.parametrize("db_engine", ("mysql", "postgresql")) +@pytest.mark.parametrize("db_engine", ["mysql", "postgresql"]) async def test_validate_db_schema( async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant, diff --git a/tests/components/recorder/common.py b/tests/components/recorder/common.py index d0ed6f15d43..e8fd6dbcf53 100644 --- a/tests/components/recorder/common.py +++ b/tests/components/recorder/common.py @@ -1,4 +1,5 @@ """Common test utils for working with recorder.""" + from __future__ import annotations import asyncio @@ -19,7 +20,13 @@ from sqlalchemy.orm.session import Session from homeassistant import core as ha from homeassistant.components import recorder -from homeassistant.components.recorder import Recorder, core, get_instance, statistics +from homeassistant.components.recorder import ( + Recorder, + core, + get_instance, + migration, + statistics, +) from homeassistant.components.recorder.db_schema import ( Events, EventTypes, @@ -186,6 +193,7 @@ def assert_states_equal_without_context(state: State, other: State) -> None: """Assert that two states are equal, ignoring context.""" assert_states_equal_without_context_and_last_changed(state, other) assert state.last_changed == other.last_changed + assert state.last_reported == other.last_reported def assert_states_equal_without_context_and_last_changed( @@ -408,19 +416,24 @@ def old_db_schema(schema_version_postfix: str) -> Iterator[None]: importlib.import_module(schema_module) old_db_schema = sys.modules[schema_module] - with patch.object(recorder, "db_schema", old_db_schema), patch.object( - recorder.migration, "SCHEMA_VERSION", old_db_schema.SCHEMA_VERSION - ), patch.object(core, "StatesMeta", old_db_schema.StatesMeta), patch.object( - core, "EventTypes", old_db_schema.EventTypes - ), patch.object(core, "EventData", old_db_schema.EventData), patch.object( - core, "States", old_db_schema.States - ), patch.object(core, "Events", old_db_schema.Events), patch.object( - core, "StateAttributes", old_db_schema.StateAttributes - ), patch.object(core, "EntityIDMigrationTask", core.RecorderTask), patch( - CREATE_ENGINE_TARGET, - new=partial( - create_engine_test_for_schema_version_postfix, - schema_version_postfix=schema_version_postfix, + with ( + patch.object(recorder, "db_schema", old_db_schema), + patch.object( + recorder.migration, "SCHEMA_VERSION", old_db_schema.SCHEMA_VERSION + ), + patch.object(core, "StatesMeta", old_db_schema.StatesMeta), + patch.object(core, "EventTypes", old_db_schema.EventTypes), + patch.object(core, "EventData", old_db_schema.EventData), + patch.object(core, "States", old_db_schema.States), + patch.object(core, "Events", old_db_schema.Events), + patch.object(core, "StateAttributes", old_db_schema.StateAttributes), + patch.object(migration.EntityIDMigration, "task", core.RecorderTask), + patch( + CREATE_ENGINE_TARGET, + new=partial( + create_engine_test_for_schema_version_postfix, + schema_version_postfix=schema_version_postfix, + ), ), ): yield diff --git a/tests/components/recorder/db_schema_16.py b/tests/components/recorder/db_schema_16.py index 8c491b82c39..4d48400e370 100644 --- a/tests/components/recorder/db_schema_16.py +++ b/tests/components/recorder/db_schema_16.py @@ -84,7 +84,7 @@ class Events(Base): # type: ignore context_user_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID), index=True) context_parent_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID), index=True) - __table_args__ = ( + __table_args__ = ( # noqa: PIE794 # Used for fetching events at a specific time # see logbook Index("ix_events_event_type_time_fired", "event_type", "time_fired"), @@ -156,7 +156,7 @@ class States(Base): # type: ignore event = relationship("Events", uselist=False) old_state = relationship("States", remote_side=[state_id]) - __table_args__ = ( + __table_args__ = ( # noqa: PIE794 # 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"), @@ -237,7 +237,7 @@ class Statistics(Base): # type: ignore state = Column(Float()) sum = Column(Float()) - __table_args__ = ( + __table_args__ = ( # noqa: PIE794 # Used for fetching statistics for a certain entity at a specific time Index("ix_statistics_statistic_id_start", "statistic_id", "start"), ) diff --git a/tests/components/recorder/db_schema_22.py b/tests/components/recorder/db_schema_22.py index 329e5d262bc..0d336c96403 100644 --- a/tests/components/recorder/db_schema_22.py +++ b/tests/components/recorder/db_schema_22.py @@ -439,13 +439,11 @@ class StatisticsRuns(Base): # type: ignore @overload -def process_timestamp(ts: None) -> None: - ... +def process_timestamp(ts: None) -> None: ... @overload -def process_timestamp(ts: datetime) -> datetime: - ... +def process_timestamp(ts: datetime) -> datetime: ... def process_timestamp(ts: datetime | None) -> datetime | None: @@ -459,13 +457,11 @@ def process_timestamp(ts: datetime | None) -> datetime | None: @overload -def process_timestamp_to_utc_isoformat(ts: None) -> None: - ... +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) -> str: ... def process_timestamp_to_utc_isoformat(ts: datetime | None) -> str | None: diff --git a/tests/components/recorder/db_schema_23.py b/tests/components/recorder/db_schema_23.py index a89599520c0..d4b6e8b0a73 100644 --- a/tests/components/recorder/db_schema_23.py +++ b/tests/components/recorder/db_schema_23.py @@ -5,6 +5,7 @@ used by Home Assistant Core 2021.11.0, which adds the name column to statistics_meta. It is used to test the schema migration logic. """ + from __future__ import annotations from datetime import datetime, timedelta @@ -428,13 +429,11 @@ class StatisticsRuns(Base): # type: ignore @overload -def process_timestamp(ts: None) -> None: - ... +def process_timestamp(ts: None) -> None: ... @overload -def process_timestamp(ts: datetime) -> datetime: - ... +def process_timestamp(ts: datetime) -> datetime: ... def process_timestamp(ts: datetime | None) -> datetime | None: @@ -448,13 +447,11 @@ def process_timestamp(ts: datetime | None) -> datetime | None: @overload -def process_timestamp_to_utc_isoformat(ts: None) -> None: - ... +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) -> str: ... def process_timestamp_to_utc_isoformat(ts: datetime | None) -> str | None: diff --git a/tests/components/recorder/db_schema_23_with_newer_columns.py b/tests/components/recorder/db_schema_23_with_newer_columns.py index 160ddc5761c..6893a7257f4 100644 --- a/tests/components/recorder/db_schema_23_with_newer_columns.py +++ b/tests/components/recorder/db_schema_23_with_newer_columns.py @@ -9,6 +9,7 @@ allow the recorder to startup successfully. It is used to test the schema migration logic. """ + from __future__ import annotations from datetime import datetime, timedelta @@ -253,7 +254,7 @@ class States(Base): # type: ignore TIMESTAMP_TYPE, default=time.time ) # *** Not originally in v23, only added for recorder to startup ok last_updated = Column(DATETIME_TYPE, default=dt_util.utcnow, index=True) - last_updated_ts = Column( + last_updated_ts = Column( # noqa: PIE794 TIMESTAMP_TYPE, default=time.time, index=True ) # *** Not originally in v23, only added for recorder to startup ok created = Column(DATETIME_TYPE, default=dt_util.utcnow) @@ -552,13 +553,11 @@ class StatisticsRuns(Base): # type: ignore @overload -def process_timestamp(ts: None) -> None: - ... +def process_timestamp(ts: None) -> None: ... @overload -def process_timestamp(ts: datetime) -> datetime: - ... +def process_timestamp(ts: datetime) -> datetime: ... def process_timestamp(ts: datetime | None) -> datetime | None: @@ -572,13 +571,11 @@ def process_timestamp(ts: datetime | None) -> datetime | None: @overload -def process_timestamp_to_utc_isoformat(ts: None) -> None: - ... +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) -> str: ... def process_timestamp_to_utc_isoformat(ts: datetime | None) -> str | None: diff --git a/tests/components/recorder/db_schema_25.py b/tests/components/recorder/db_schema_25.py index 24b5b764c65..0d763f91b67 100644 --- a/tests/components/recorder/db_schema_25.py +++ b/tests/components/recorder/db_schema_25.py @@ -1,4 +1,5 @@ """Models for SQLAlchemy.""" + from __future__ import annotations from datetime import datetime, timedelta @@ -488,13 +489,11 @@ class StatisticsRuns(Base): # type: ignore[misc,valid-type] @overload -def process_timestamp(ts: None) -> None: - ... +def process_timestamp(ts: None) -> None: ... @overload -def process_timestamp(ts: datetime) -> datetime: - ... +def process_timestamp(ts: datetime) -> datetime: ... def process_timestamp(ts: datetime | None) -> datetime | None: @@ -508,13 +507,11 @@ def process_timestamp(ts: datetime | None) -> datetime | None: @overload -def process_timestamp_to_utc_isoformat(ts: None) -> None: - ... +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) -> str: ... def process_timestamp_to_utc_isoformat(ts: datetime | None) -> str | None: diff --git a/tests/components/recorder/db_schema_28.py b/tests/components/recorder/db_schema_28.py index 9df32f1b6c1..feaf877b36f 100644 --- a/tests/components/recorder/db_schema_28.py +++ b/tests/components/recorder/db_schema_28.py @@ -3,6 +3,7 @@ This file contains the model definitions for schema version 28. It is used to test the schema migration logic. """ + from __future__ import annotations from datetime import datetime, timedelta @@ -653,13 +654,11 @@ class StatisticsRuns(Base): # type: ignore[misc,valid-type] @overload -def process_timestamp(ts: None) -> None: - ... +def process_timestamp(ts: None) -> None: ... @overload -def process_timestamp(ts: datetime) -> datetime: - ... +def process_timestamp(ts: datetime) -> datetime: ... def process_timestamp(ts: datetime | None) -> datetime | None: @@ -673,13 +672,11 @@ def process_timestamp(ts: datetime | None) -> datetime | None: @overload -def process_timestamp_to_utc_isoformat(ts: None) -> None: - ... +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) -> str: ... def process_timestamp_to_utc_isoformat(ts: datetime | None) -> str | None: diff --git a/tests/components/recorder/db_schema_30.py b/tests/components/recorder/db_schema_30.py index c1a61159c98..b82213cbc89 100644 --- a/tests/components/recorder/db_schema_30.py +++ b/tests/components/recorder/db_schema_30.py @@ -3,6 +3,7 @@ This file contains the model definitions for schema version 30. It is used to test the schema migration logic. """ + from __future__ import annotations from collections.abc import Callable @@ -374,6 +375,9 @@ class States(Base): # type: ignore[misc,valid-type] last_changed_ts = Column( TIMESTAMP_TYPE ) # *** Not originally in v30, only added for recorder to startup ok + last_reported_ts = Column( + TIMESTAMP_TYPE + ) # *** Not originally in v30, only added for recorder to startup ok last_updated = Column(DATETIME_TYPE, default=dt_util.utcnow, index=True) last_updated_ts = Column( TIMESTAMP_TYPE, default=time.time, index=True @@ -737,13 +741,11 @@ OLD_STATE = aliased(States, name="old_state") @overload -def process_timestamp(ts: None) -> None: - ... +def process_timestamp(ts: None) -> None: ... @overload -def process_timestamp(ts: datetime) -> datetime: - ... +def process_timestamp(ts: datetime) -> datetime: ... def process_timestamp(ts: datetime | None) -> datetime | None: diff --git a/tests/components/recorder/db_schema_32.py b/tests/components/recorder/db_schema_32.py index e092de28eca..15b56e2fc86 100644 --- a/tests/components/recorder/db_schema_32.py +++ b/tests/components/recorder/db_schema_32.py @@ -3,6 +3,7 @@ This file contains the model definitions for schema version 30. It is used to test the schema migration logic. """ + from __future__ import annotations from collections.abc import Callable @@ -371,6 +372,9 @@ class States(Base): # type: ignore[misc,valid-type] ) last_changed = Column(DATETIME_TYPE) last_changed_ts = Column(TIMESTAMP_TYPE) + last_reported_ts = Column( + TIMESTAMP_TYPE + ) # *** Not originally in v32, only added for recorder to startup ok last_updated = Column(DATETIME_TYPE, default=dt_util.utcnow, index=True) last_updated_ts = Column(TIMESTAMP_TYPE, default=time.time, index=True) old_state_id = Column(Integer, ForeignKey("states.state_id"), index=True) @@ -738,13 +742,11 @@ OLD_STATE = aliased(States, name="old_state") @overload -def process_timestamp(ts: None) -> None: - ... +def process_timestamp(ts: None) -> None: ... @overload -def process_timestamp(ts: datetime) -> datetime: - ... +def process_timestamp(ts: datetime) -> datetime: ... def process_timestamp(ts: datetime | None) -> datetime | None: diff --git a/tests/components/recorder/db_schema_42.py b/tests/components/recorder/db_schema_42.py new file mode 100644 index 00000000000..b8e49aef592 --- /dev/null +++ b/tests/components/recorder/db_schema_42.py @@ -0,0 +1,838 @@ +"""Models for SQLAlchemy. + +This file contains the model definitions for schema version 42. +It is used to test the schema migration logic. +""" + +from __future__ import annotations + +from collections.abc import Callable +from datetime import datetime, timedelta +import logging +import time +from typing import Any, Self, cast + +import ciso8601 +from fnv_hash_fast import fnv1a_32 +from sqlalchemy import ( + CHAR, + JSON, + BigInteger, + Boolean, + ColumnElement, + DateTime, + Float, + ForeignKey, + Identity, + Index, + Integer, + LargeBinary, + SmallInteger, + String, + Text, + case, + type_coerce, +) +from sqlalchemy.dialects import mysql, oracle, postgresql, sqlite +from sqlalchemy.engine.interfaces import Dialect +from sqlalchemy.ext.compiler import compiles +from sqlalchemy.orm import DeclarativeBase, Mapped, aliased, mapped_column, relationship +from sqlalchemy.types import TypeDecorator + +from homeassistant.components.recorder.const import ( + ALL_DOMAIN_EXCLUDE_ATTRS, + SupportedDialect, +) +from homeassistant.components.recorder.models import ( + StatisticData, + StatisticDataTimestamp, + StatisticMetaData, + bytes_to_ulid_or_none, + bytes_to_uuid_hex_or_none, + datetime_to_timestamp_or_none, + process_timestamp, + ulid_to_bytes_or_none, + uuid_hex_to_bytes_or_none, +) +from homeassistant.const import ( + MAX_LENGTH_EVENT_EVENT_TYPE, + MAX_LENGTH_STATE_ENTITY_ID, + MAX_LENGTH_STATE_STATE, +) +from homeassistant.core import Context, Event, EventOrigin, State +from homeassistant.helpers.json import JSON_DUMP, json_bytes, json_bytes_strip_null +import homeassistant.util.dt as dt_util +from homeassistant.util.json import ( + JSON_DECODE_EXCEPTIONS, + json_loads, + json_loads_object, +) + + +# SQLAlchemy Schema +class Base(DeclarativeBase): + """Base class for tables.""" + + +SCHEMA_VERSION = 42 + +_LOGGER = logging.getLogger(__name__) + +TABLE_EVENTS = "events" +TABLE_EVENT_DATA = "event_data" +TABLE_EVENT_TYPES = "event_types" +TABLE_STATES = "states" +TABLE_STATE_ATTRIBUTES = "state_attributes" +TABLE_STATES_META = "states_meta" +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" + +STATISTICS_TABLES = ("statistics", "statistics_short_term") + +MAX_STATE_ATTRS_BYTES = 16384 +MAX_EVENT_DATA_BYTES = 32768 + +PSQL_DIALECT = SupportedDialect.POSTGRESQL + +ALL_TABLES = [ + TABLE_STATES, + TABLE_STATE_ATTRIBUTES, + TABLE_EVENTS, + TABLE_EVENT_DATA, + TABLE_EVENT_TYPES, + TABLE_RECORDER_RUNS, + TABLE_SCHEMA_CHANGES, + TABLE_STATES_META, + TABLE_STATISTICS, + TABLE_STATISTICS_META, + TABLE_STATISTICS_RUNS, + TABLE_STATISTICS_SHORT_TERM, +] + +TABLES_TO_CHECK = [ + TABLE_STATES, + TABLE_EVENTS, + TABLE_RECORDER_RUNS, + TABLE_SCHEMA_CHANGES, +] + +LAST_UPDATED_INDEX_TS = "ix_states_last_updated_ts" +METADATA_ID_LAST_UPDATED_INDEX_TS = "ix_states_metadata_id_last_updated_ts" +EVENTS_CONTEXT_ID_BIN_INDEX = "ix_events_context_id_bin" +STATES_CONTEXT_ID_BIN_INDEX = "ix_states_context_id_bin" +LEGACY_STATES_EVENT_ID_INDEX = "ix_states_event_id" +LEGACY_STATES_ENTITY_ID_LAST_UPDATED_INDEX = "ix_states_entity_id_last_updated_ts" +CONTEXT_ID_BIN_MAX_LENGTH = 16 + +MYSQL_COLLATE = "utf8mb4_unicode_ci" +MYSQL_DEFAULT_CHARSET = "utf8mb4" +MYSQL_ENGINE = "InnoDB" + +_DEFAULT_TABLE_ARGS = { + "mysql_default_charset": MYSQL_DEFAULT_CHARSET, + "mysql_collate": MYSQL_COLLATE, + "mysql_engine": MYSQL_ENGINE, + "mariadb_default_charset": MYSQL_DEFAULT_CHARSET, + "mariadb_collate": MYSQL_COLLATE, + "mariadb_engine": MYSQL_ENGINE, +} + + +class UnusedDateTime(DateTime): + """An unused column type that behaves like a datetime.""" + + +class Unused(CHAR): + """An unused column type that behaves like a string.""" + + +@compiles(UnusedDateTime, "mysql", "mariadb", "sqlite") # type: ignore[misc,no-untyped-call] +@compiles(Unused, "mysql", "mariadb", "sqlite") # type: ignore[misc,no-untyped-call] +def compile_char_zero(type_: TypeDecorator, compiler: Any, **kw: Any) -> str: + """Compile UnusedDateTime and Unused as CHAR(0) on mysql, mariadb, and sqlite.""" + return "CHAR(0)" # Uses 1 byte on MySQL (no change on sqlite) + + +@compiles(Unused, "postgresql") # type: ignore[misc,no-untyped-call] +def compile_char_one(type_: TypeDecorator, compiler: Any, **kw: Any) -> str: + """Compile Unused as CHAR(1) on postgresql.""" + return "CHAR(1)" # Uses 1 byte + + +class FAST_PYSQLITE_DATETIME(sqlite.DATETIME): + """Use ciso8601 to parse datetimes instead of sqlalchemy built-in regex.""" + + def result_processor(self, dialect, coltype): # type: ignore[no-untyped-def] + """Offload the datetime parsing to ciso8601.""" + return lambda value: None if value is None else ciso8601.parse_datetime(value) + + +class NativeLargeBinary(LargeBinary): + """A faster version of LargeBinary for engines that support python bytes natively.""" + + def result_processor(self, dialect, coltype): # type: ignore[no-untyped-def] + """No conversion needed for engines that support native bytes.""" + return None + + +# For MariaDB and MySQL we can use an unsigned integer type since it will fit 2**32 +# for sqlite and postgresql we use a bigint +UINT_32_TYPE = BigInteger().with_variant( + mysql.INTEGER(unsigned=True), # type: ignore[no-untyped-call] + "mysql", + "mariadb", +) +JSON_VARIANT_CAST = Text().with_variant( + postgresql.JSON(none_as_null=True), # type: ignore[no-untyped-call] + "postgresql", +) +JSONB_VARIANT_CAST = Text().with_variant( + postgresql.JSONB(none_as_null=True), # type: ignore[no-untyped-call] + "postgresql", +) +DATETIME_TYPE = ( + DateTime(timezone=True) + .with_variant(mysql.DATETIME(timezone=True, fsp=6), "mysql", "mariadb") # type: ignore[no-untyped-call] + .with_variant(FAST_PYSQLITE_DATETIME(), "sqlite") # type: ignore[no-untyped-call] +) +DOUBLE_TYPE = ( + Float() + .with_variant(mysql.DOUBLE(asdecimal=False), "mysql", "mariadb") # type: ignore[no-untyped-call] + .with_variant(oracle.DOUBLE_PRECISION(), "oracle") + .with_variant(postgresql.DOUBLE_PRECISION(), "postgresql") +) +UNUSED_LEGACY_COLUMN = Unused(0) +UNUSED_LEGACY_DATETIME_COLUMN = UnusedDateTime(timezone=True) +UNUSED_LEGACY_INTEGER_COLUMN = SmallInteger() +DOUBLE_PRECISION_TYPE_SQL = "DOUBLE PRECISION" +CONTEXT_BINARY_TYPE = LargeBinary(CONTEXT_ID_BIN_MAX_LENGTH).with_variant( + NativeLargeBinary(CONTEXT_ID_BIN_MAX_LENGTH), "mysql", "mariadb", "sqlite" +) + +TIMESTAMP_TYPE = DOUBLE_TYPE + + +class JSONLiteral(JSON): + """Teach SA how to literalize json.""" + + def literal_processor(self, dialect: Dialect) -> Callable[[Any], str]: + """Processor to convert a value to JSON.""" + + def process(value: Any) -> str: + """Dump json.""" + return JSON_DUMP(value) + + return process + + +EVENT_ORIGIN_ORDER = [EventOrigin.local, EventOrigin.remote] +EVENT_ORIGIN_TO_IDX = {origin: idx for idx, origin in enumerate(EVENT_ORIGIN_ORDER)} + + +class Events(Base): + """Event history data.""" + + __table_args__ = ( + # Used for fetching events at a specific time + # see logbook + Index( + "ix_events_event_type_id_time_fired_ts", "event_type_id", "time_fired_ts" + ), + Index( + EVENTS_CONTEXT_ID_BIN_INDEX, + "context_id_bin", + mysql_length=CONTEXT_ID_BIN_MAX_LENGTH, + mariadb_length=CONTEXT_ID_BIN_MAX_LENGTH, + ), + _DEFAULT_TABLE_ARGS, + ) + __tablename__ = TABLE_EVENTS + event_id: Mapped[int] = mapped_column(Integer, Identity(), primary_key=True) + event_type: Mapped[str | None] = mapped_column(UNUSED_LEGACY_COLUMN) + event_data: Mapped[str | None] = mapped_column(UNUSED_LEGACY_COLUMN) + origin: Mapped[str | None] = mapped_column(UNUSED_LEGACY_COLUMN) + origin_idx: Mapped[int | None] = mapped_column(SmallInteger) + time_fired: Mapped[datetime | None] = mapped_column(UNUSED_LEGACY_DATETIME_COLUMN) + time_fired_ts: Mapped[float | None] = mapped_column(TIMESTAMP_TYPE, index=True) + context_id: Mapped[str | None] = mapped_column(UNUSED_LEGACY_COLUMN) + context_user_id: Mapped[str | None] = mapped_column(UNUSED_LEGACY_COLUMN) + context_parent_id: Mapped[str | None] = mapped_column(UNUSED_LEGACY_COLUMN) + data_id: Mapped[int | None] = mapped_column( + Integer, ForeignKey("event_data.data_id"), index=True + ) + context_id_bin: Mapped[bytes | None] = mapped_column(CONTEXT_BINARY_TYPE) + context_user_id_bin: Mapped[bytes | None] = mapped_column(CONTEXT_BINARY_TYPE) + context_parent_id_bin: Mapped[bytes | None] = mapped_column(CONTEXT_BINARY_TYPE) + event_type_id: Mapped[int | None] = mapped_column( + Integer, ForeignKey("event_types.event_type_id") + ) + event_data_rel: Mapped[EventData | None] = relationship("EventData") + event_type_rel: Mapped[EventTypes | None] = relationship("EventTypes") + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + "" + ) + + @property + def _time_fired_isotime(self) -> str | None: + """Return time_fired as an isotime string.""" + date_time: datetime | None + if self.time_fired_ts is not None: + date_time = dt_util.utc_from_timestamp(self.time_fired_ts) + else: + date_time = process_timestamp(self.time_fired) + if date_time is None: + return None + return date_time.isoformat(sep=" ", timespec="seconds") + + @staticmethod + def from_event(event: Event) -> Events: + """Create an event database object from a native event.""" + return Events( + event_type=None, + event_data=None, + origin_idx=EVENT_ORIGIN_TO_IDX.get(event.origin), + time_fired=None, + time_fired_ts=event.time_fired_timestamp, + context_id=None, + context_id_bin=ulid_to_bytes_or_none(event.context.id), + context_user_id=None, + context_user_id_bin=uuid_hex_to_bytes_or_none(event.context.user_id), + context_parent_id=None, + context_parent_id_bin=ulid_to_bytes_or_none(event.context.parent_id), + ) + + def to_native(self, validate_entity_id: bool = True) -> Event | None: + """Convert to a native HA Event.""" + context = Context( + id=bytes_to_ulid_or_none(self.context_id_bin), + user_id=bytes_to_uuid_hex_or_none(self.context_user_id_bin), + parent_id=bytes_to_ulid_or_none(self.context_parent_id_bin), + ) + try: + return Event( + self.event_type or "", + json_loads_object(self.event_data) if self.event_data else {}, + EventOrigin(self.origin) + if self.origin + else EVENT_ORIGIN_ORDER[self.origin_idx or 0], + dt_util.utc_from_timestamp(self.time_fired_ts or 0), + context=context, + ) + except JSON_DECODE_EXCEPTIONS: + # When json_loads fails + _LOGGER.exception("Error converting to event: %s", self) + return None + + +class EventData(Base): + """Event data history.""" + + __table_args__ = (_DEFAULT_TABLE_ARGS,) + __tablename__ = TABLE_EVENT_DATA + data_id: Mapped[int] = mapped_column(Integer, Identity(), primary_key=True) + hash: Mapped[int | None] = mapped_column(UINT_32_TYPE, index=True) + # Note that this is not named attributes to avoid confusion with the states table + shared_data: Mapped[str | None] = mapped_column( + Text().with_variant(mysql.LONGTEXT, "mysql", "mariadb") + ) + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + "" + ) + + @staticmethod + def shared_data_bytes_from_event( + event: Event, dialect: SupportedDialect | None + ) -> bytes: + """Create shared_data from an event.""" + if dialect == SupportedDialect.POSTGRESQL: + bytes_result = json_bytes_strip_null(event.data) + bytes_result = json_bytes(event.data) + if len(bytes_result) > MAX_EVENT_DATA_BYTES: + _LOGGER.warning( + "Event data for %s exceed maximum size of %s bytes. " + "This can cause database performance issues; Event data " + "will not be stored", + event.event_type, + MAX_EVENT_DATA_BYTES, + ) + return b"{}" + return bytes_result + + @staticmethod + def hash_shared_data_bytes(shared_data_bytes: bytes) -> int: + """Return the hash of json encoded shared data.""" + return fnv1a_32(shared_data_bytes) + + def to_native(self) -> dict[str, Any]: + """Convert to an event data dictionary.""" + shared_data = self.shared_data + if shared_data is None: + return {} + try: + return cast(dict[str, Any], json_loads(shared_data)) + except JSON_DECODE_EXCEPTIONS: + _LOGGER.exception("Error converting row to event data: %s", self) + return {} + + +class EventTypes(Base): + """Event type history.""" + + __table_args__ = (_DEFAULT_TABLE_ARGS,) + __tablename__ = TABLE_EVENT_TYPES + event_type_id: Mapped[int] = mapped_column(Integer, Identity(), primary_key=True) + event_type: Mapped[str | None] = mapped_column( + String(MAX_LENGTH_EVENT_EVENT_TYPE), index=True, unique=True + ) + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + "" + ) + + +class States(Base): + """State change history.""" + + __table_args__ = ( + # Used for fetching the state of entities at a specific time + # (get_states in history.py) + Index(METADATA_ID_LAST_UPDATED_INDEX_TS, "metadata_id", "last_updated_ts"), + Index( + STATES_CONTEXT_ID_BIN_INDEX, + "context_id_bin", + mysql_length=CONTEXT_ID_BIN_MAX_LENGTH, + mariadb_length=CONTEXT_ID_BIN_MAX_LENGTH, + ), + _DEFAULT_TABLE_ARGS, + ) + __tablename__ = TABLE_STATES + state_id: Mapped[int] = mapped_column(Integer, Identity(), primary_key=True) + entity_id: Mapped[str | None] = mapped_column(UNUSED_LEGACY_COLUMN) + state: Mapped[str | None] = mapped_column(String(MAX_LENGTH_STATE_STATE)) + attributes: Mapped[str | None] = mapped_column(UNUSED_LEGACY_COLUMN) + event_id: Mapped[int | None] = mapped_column(UNUSED_LEGACY_INTEGER_COLUMN) + last_changed: Mapped[datetime | None] = mapped_column(UNUSED_LEGACY_DATETIME_COLUMN) + last_changed_ts: Mapped[float | None] = mapped_column(TIMESTAMP_TYPE) + last_updated: Mapped[datetime | None] = mapped_column(UNUSED_LEGACY_DATETIME_COLUMN) + last_updated_ts: Mapped[float | None] = mapped_column( + TIMESTAMP_TYPE, default=time.time, index=True + ) + old_state_id: Mapped[int | None] = mapped_column( + Integer, ForeignKey("states.state_id"), index=True + ) + attributes_id: Mapped[int | None] = mapped_column( + Integer, ForeignKey("state_attributes.attributes_id"), index=True + ) + context_id: Mapped[str | None] = mapped_column(UNUSED_LEGACY_COLUMN) + context_user_id: Mapped[str | None] = mapped_column(UNUSED_LEGACY_COLUMN) + context_parent_id: Mapped[str | None] = mapped_column(UNUSED_LEGACY_COLUMN) + origin_idx: Mapped[int | None] = mapped_column( + SmallInteger + ) # 0 is local, 1 is remote + old_state: Mapped[States | None] = relationship("States", remote_side=[state_id]) + state_attributes: Mapped[StateAttributes | None] = relationship("StateAttributes") + context_id_bin: Mapped[bytes | None] = mapped_column(CONTEXT_BINARY_TYPE) + context_user_id_bin: Mapped[bytes | None] = mapped_column(CONTEXT_BINARY_TYPE) + context_parent_id_bin: Mapped[bytes | None] = mapped_column(CONTEXT_BINARY_TYPE) + metadata_id: Mapped[int | None] = mapped_column( + Integer, ForeignKey("states_meta.metadata_id") + ) + states_meta_rel: Mapped[StatesMeta | None] = relationship("StatesMeta") + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + f"" + ) + + @property + def _last_updated_isotime(self) -> str | None: + """Return last_updated as an isotime string.""" + date_time: datetime | None + if self.last_updated_ts is not None: + date_time = dt_util.utc_from_timestamp(self.last_updated_ts) + else: + date_time = process_timestamp(self.last_updated) + if date_time is None: + return None + return date_time.isoformat(sep=" ", timespec="seconds") + + @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, + context_id=None, + context_id_bin=ulid_to_bytes_or_none(event.context.id), + context_user_id=None, + context_user_id_bin=uuid_hex_to_bytes_or_none(event.context.user_id), + context_parent_id=None, + context_parent_id_bin=ulid_to_bytes_or_none(event.context.parent_id), + origin_idx=EVENT_ORIGIN_TO_IDX.get(event.origin), + last_updated=None, + last_changed=None, + ) + # None state means the state was removed from the state machine + if state is None: + dbstate.state = "" + dbstate.last_updated_ts = event.time_fired_timestamp + dbstate.last_changed_ts = None + return dbstate + + dbstate.state = state.state + dbstate.last_updated_ts = state.last_updated_timestamp + if state.last_updated == state.last_changed: + dbstate.last_changed_ts = None + else: + dbstate.last_changed_ts = state.last_changed_timestamp + + return dbstate + + def to_native(self, validate_entity_id: bool = True) -> State | None: + """Convert to an HA state object.""" + context = Context( + id=bytes_to_ulid_or_none(self.context_id_bin), + user_id=bytes_to_uuid_hex_or_none(self.context_user_id_bin), + parent_id=bytes_to_ulid_or_none(self.context_parent_id_bin), + ) + try: + attrs = json_loads_object(self.attributes) if self.attributes else {} + except JSON_DECODE_EXCEPTIONS: + # When json_loads fails + _LOGGER.exception("Error converting row to state: %s", self) + return None + if self.last_changed_ts is None or self.last_changed_ts == self.last_updated_ts: + last_changed = last_updated = dt_util.utc_from_timestamp( + self.last_updated_ts or 0 + ) + else: + last_updated = dt_util.utc_from_timestamp(self.last_updated_ts or 0) + last_changed = dt_util.utc_from_timestamp(self.last_changed_ts or 0) + return State( + self.entity_id or "", + self.state, # type: ignore[arg-type] + # Join the state_attributes table on attributes_id to get the attributes + # for newer states + attrs, + last_changed, + last_updated, + context=context, + validate_entity_id=validate_entity_id, + ) + + +class StateAttributes(Base): + """State attribute change history.""" + + __table_args__ = (_DEFAULT_TABLE_ARGS,) + __tablename__ = TABLE_STATE_ATTRIBUTES + attributes_id: Mapped[int] = mapped_column(Integer, Identity(), primary_key=True) + hash: Mapped[int | None] = mapped_column(UINT_32_TYPE, index=True) + # Note that this is not named attributes to avoid confusion with the states table + shared_attrs: Mapped[str | None] = mapped_column( + Text().with_variant(mysql.LONGTEXT, "mysql", "mariadb") + ) + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + f"" + ) + + @staticmethod + def shared_attrs_bytes_from_event( + event: Event, + dialect: SupportedDialect | None, + ) -> bytes: + """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 b"{}" + if state_info := state.state_info: + exclude_attrs = { + *ALL_DOMAIN_EXCLUDE_ATTRS, + *state_info["unrecorded_attributes"], + } + else: + exclude_attrs = ALL_DOMAIN_EXCLUDE_ATTRS + encoder = json_bytes_strip_null if dialect == PSQL_DIALECT else json_bytes + bytes_result = encoder( + {k: v for k, v in state.attributes.items() if k not in exclude_attrs} + ) + if len(bytes_result) > MAX_STATE_ATTRS_BYTES: + _LOGGER.warning( + "State attributes for %s exceed maximum size of %s bytes. " + "This can cause database performance issues; Attributes " + "will not be stored", + state.entity_id, + MAX_STATE_ATTRS_BYTES, + ) + return b"{}" + return bytes_result + + @staticmethod + def hash_shared_attrs_bytes(shared_attrs_bytes: bytes) -> int: + """Return the hash of json encoded shared attributes.""" + return fnv1a_32(shared_attrs_bytes) + + def to_native(self) -> dict[str, Any]: + """Convert to a state attributes dictionary.""" + shared_attrs = self.shared_attrs + if shared_attrs is None: + return {} + try: + return cast(dict[str, Any], json_loads(shared_attrs)) + except JSON_DECODE_EXCEPTIONS: + # When json_loads fails + _LOGGER.exception("Error converting row to state attributes: %s", self) + return {} + + +class StatesMeta(Base): + """Metadata for states.""" + + __table_args__ = (_DEFAULT_TABLE_ARGS,) + __tablename__ = TABLE_STATES_META + metadata_id: Mapped[int] = mapped_column(Integer, Identity(), primary_key=True) + entity_id: Mapped[str | None] = mapped_column( + String(MAX_LENGTH_STATE_ENTITY_ID), index=True, unique=True + ) + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + "" + ) + + +class StatisticsBase: + """Statistics base class.""" + + id: Mapped[int] = mapped_column(Integer, Identity(), primary_key=True) + created: Mapped[datetime | None] = mapped_column(UNUSED_LEGACY_DATETIME_COLUMN) + created_ts: Mapped[float | None] = mapped_column(TIMESTAMP_TYPE, default=time.time) + metadata_id: Mapped[int | None] = mapped_column( + Integer, + ForeignKey(f"{TABLE_STATISTICS_META}.id", ondelete="CASCADE"), + ) + start: Mapped[datetime | None] = mapped_column(UNUSED_LEGACY_DATETIME_COLUMN) + start_ts: Mapped[float | None] = mapped_column(TIMESTAMP_TYPE, index=True) + mean: Mapped[float | None] = mapped_column(DOUBLE_TYPE) + min: Mapped[float | None] = mapped_column(DOUBLE_TYPE) + max: Mapped[float | None] = mapped_column(DOUBLE_TYPE) + last_reset: Mapped[datetime | None] = mapped_column(UNUSED_LEGACY_DATETIME_COLUMN) + last_reset_ts: Mapped[float | None] = mapped_column(TIMESTAMP_TYPE) + state: Mapped[float | None] = mapped_column(DOUBLE_TYPE) + sum: Mapped[float | None] = mapped_column(DOUBLE_TYPE) + + duration: timedelta + + @classmethod + def from_stats(cls, metadata_id: int, stats: StatisticData) -> Self: + """Create object from a statistics with datatime objects.""" + return cls( # type: ignore[call-arg] + metadata_id=metadata_id, + created=None, + created_ts=time.time(), + start=None, + start_ts=dt_util.utc_to_timestamp(stats["start"]), + mean=stats.get("mean"), + min=stats.get("min"), + max=stats.get("max"), + last_reset=None, + last_reset_ts=datetime_to_timestamp_or_none(stats.get("last_reset")), + state=stats.get("state"), + sum=stats.get("sum"), + ) + + @classmethod + def from_stats_ts(cls, metadata_id: int, stats: StatisticDataTimestamp) -> Self: + """Create object from a statistics with timestamps.""" + return cls( # type: ignore[call-arg] + metadata_id=metadata_id, + created=None, + created_ts=time.time(), + start=None, + start_ts=stats["start_ts"], + mean=stats.get("mean"), + min=stats.get("min"), + max=stats.get("max"), + last_reset=None, + last_reset_ts=stats.get("last_reset_ts"), + state=stats.get("state"), + sum=stats.get("sum"), + ) + + +class Statistics(Base, StatisticsBase): + """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_ts", + "metadata_id", + "start_ts", + unique=True, + ), + ) + __tablename__ = TABLE_STATISTICS + + +class StatisticsShortTerm(Base, StatisticsBase): + """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_ts", + "metadata_id", + "start_ts", + unique=True, + ), + ) + __tablename__ = TABLE_STATISTICS_SHORT_TERM + + +class StatisticsMeta(Base): + """Statistics meta data.""" + + __table_args__ = (_DEFAULT_TABLE_ARGS,) + __tablename__ = TABLE_STATISTICS_META + id: Mapped[int] = mapped_column(Integer, Identity(), primary_key=True) + statistic_id: Mapped[str | None] = mapped_column( + String(255), index=True, unique=True + ) + source: Mapped[str | None] = mapped_column(String(32)) + unit_of_measurement: Mapped[str | None] = mapped_column(String(255)) + has_mean: Mapped[bool | None] = mapped_column(Boolean) + has_sum: Mapped[bool | None] = mapped_column(Boolean) + name: Mapped[str | None] = mapped_column(String(255)) + + @staticmethod + def from_meta(meta: StatisticMetaData) -> StatisticsMeta: + """Create object from meta data.""" + return StatisticsMeta(**meta) + + +class RecorderRuns(Base): + """Representation of recorder run.""" + + __table_args__ = (Index("ix_recorder_runs_start_end", "start", "end"),) + __tablename__ = TABLE_RECORDER_RUNS + run_id: Mapped[int] = mapped_column(Integer, Identity(), primary_key=True) + start: Mapped[datetime] = mapped_column(DATETIME_TYPE, default=dt_util.utcnow) + end: Mapped[datetime | None] = mapped_column(DATETIME_TYPE) + closed_incorrect: Mapped[bool] = mapped_column(Boolean, default=False) + created: Mapped[datetime] = mapped_column(DATETIME_TYPE, 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 to_native(self, validate_entity_id: bool = True) -> Self: + """Return self, native format is this model.""" + return self + + +class SchemaChanges(Base): + """Representation of schema version changes.""" + + __tablename__ = TABLE_SCHEMA_CHANGES + change_id: Mapped[int] = mapped_column(Integer, Identity(), primary_key=True) + schema_version: Mapped[int | None] = mapped_column(Integer) + changed: Mapped[datetime] = mapped_column(DATETIME_TYPE, default=dt_util.utcnow) + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + "" + ) + + +class StatisticsRuns(Base): + """Representation of statistics run.""" + + __tablename__ = TABLE_STATISTICS_RUNS + run_id: Mapped[int] = mapped_column(Integer, Identity(), primary_key=True) + start: Mapped[datetime] = mapped_column(DATETIME_TYPE, index=True) + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + f"" + ) + + +EVENT_DATA_JSON = type_coerce( + EventData.shared_data.cast(JSONB_VARIANT_CAST), JSONLiteral(none_as_null=True) +) +OLD_FORMAT_EVENT_DATA_JSON = type_coerce( + Events.event_data.cast(JSONB_VARIANT_CAST), JSONLiteral(none_as_null=True) +) + +SHARED_ATTRS_JSON = type_coerce( + StateAttributes.shared_attrs.cast(JSON_VARIANT_CAST), JSON(none_as_null=True) +) +OLD_FORMAT_ATTRS_JSON = type_coerce( + States.attributes.cast(JSON_VARIANT_CAST), JSON(none_as_null=True) +) + +ENTITY_ID_IN_EVENT: ColumnElement = EVENT_DATA_JSON["entity_id"] +OLD_ENTITY_ID_IN_EVENT: ColumnElement = OLD_FORMAT_EVENT_DATA_JSON["entity_id"] +DEVICE_ID_IN_EVENT: ColumnElement = EVENT_DATA_JSON["device_id"] +OLD_STATE = aliased(States, name="old_state") + +SHARED_ATTR_OR_LEGACY_ATTRIBUTES = case( + (StateAttributes.shared_attrs.is_(None), States.attributes), + else_=StateAttributes.shared_attrs, +).label("attributes") +SHARED_DATA_OR_LEGACY_EVENT_DATA = case( + (EventData.shared_data.is_(None), Events.event_data), else_=EventData.shared_data +).label("event_data") diff --git a/tests/components/recorder/table_managers/test_recorder_runs.py b/tests/components/recorder/table_managers/test_recorder_runs.py index 2946850ec11..41f3a8fef4d 100644 --- a/tests/components/recorder/table_managers/test_recorder_runs.py +++ b/tests/components/recorder/table_managers/test_recorder_runs.py @@ -1,4 +1,5 @@ """Test recorder runs table manager.""" + from datetime import timedelta from unittest.mock import patch diff --git a/tests/components/recorder/table_managers/test_statistics_meta.py b/tests/components/recorder/table_managers/test_statistics_meta.py index ab6615c6dd0..66edb84c3ef 100644 --- a/tests/components/recorder/table_managers/test_statistics_meta.py +++ b/tests/components/recorder/table_managers/test_statistics_meta.py @@ -1,4 +1,5 @@ """The tests for the Recorder component.""" + from __future__ import annotations import pytest @@ -44,8 +45,11 @@ async def test_unsafe_calls_to_statistics_meta_manager( instance = await async_setup_recorder_instance( hass, {recorder.CONF_COMMIT_INTERVAL: 0} ) - with session_scope(session=instance.get_session()) as session, pytest.raises( - RuntimeError, match="Detected unsafe call not in recorder thread" + with ( + session_scope(session=instance.get_session()) as session, + pytest.raises( + RuntimeError, match="Detected unsafe call not in recorder thread" + ), ): instance.statistics_meta_manager.delete( session, diff --git a/tests/components/recorder/test_backup.py b/tests/components/recorder/test_backup.py index de2bc52d369..d181c449bbf 100644 --- a/tests/components/recorder/test_backup.py +++ b/tests/components/recorder/test_backup.py @@ -1,4 +1,5 @@ """Test backup platform for the Recorder integration.""" + from unittest.mock import patch import pytest @@ -22,10 +23,13 @@ async def test_async_pre_backup_with_timeout( recorder_mock: Recorder, hass: HomeAssistant ) -> None: """Test pre backup with timeout.""" - with patch( - "homeassistant.components.recorder.core.Recorder.lock_database", - side_effect=TimeoutError(), - ) as lock_mock, pytest.raises(TimeoutError): + with ( + patch( + "homeassistant.components.recorder.core.Recorder.lock_database", + side_effect=TimeoutError(), + ) as lock_mock, + pytest.raises(TimeoutError), + ): await async_pre_backup(hass) assert lock_mock.called @@ -34,10 +38,13 @@ async def test_async_pre_backup_with_migration( recorder_mock: Recorder, hass: HomeAssistant ) -> None: """Test pre backup with migration.""" - with patch( - "homeassistant.components.recorder.backup.async_migration_in_progress", - return_value=True, - ), pytest.raises(HomeAssistantError): + with ( + patch( + "homeassistant.components.recorder.backup.async_migration_in_progress", + return_value=True, + ), + pytest.raises(HomeAssistantError), + ): await async_pre_backup(hass) @@ -54,9 +61,12 @@ async def test_async_post_backup_failure( recorder_mock: Recorder, hass: HomeAssistant ) -> None: """Test post backup failure.""" - with patch( - "homeassistant.components.recorder.core.Recorder.unlock_database", - return_value=False, - ) as unlock_mock, pytest.raises(HomeAssistantError): + with ( + patch( + "homeassistant.components.recorder.core.Recorder.unlock_database", + return_value=False, + ) as unlock_mock, + pytest.raises(HomeAssistantError), + ): await async_post_backup(hass) assert unlock_mock.called diff --git a/tests/components/recorder/test_entity_registry.py b/tests/components/recorder/test_entity_registry.py index 9ec3087d477..37223f206a1 100644 --- a/tests/components/recorder/test_entity_registry.py +++ b/tests/components/recorder/test_entity_registry.py @@ -1,4 +1,5 @@ """The tests for sensor recorder platform.""" + from collections.abc import Callable from unittest.mock import patch diff --git a/tests/components/recorder/test_filters_with_entityfilter.py b/tests/components/recorder/test_filters_with_entityfilter.py index 896987ee58d..1ee127a9989 100644 --- a/tests/components/recorder/test_filters_with_entityfilter.py +++ b/tests/components/recorder/test_filters_with_entityfilter.py @@ -1,4 +1,5 @@ """The tests for the recorder filter matching the EntityFilter component.""" + import json from sqlalchemy import select @@ -535,7 +536,7 @@ async def test_same_entity_included_excluded_include_domain_wins( async def test_specificly_included_entity_always_wins( recorder_mock: Recorder, hass: HomeAssistant ) -> None: - """Test specificlly included entity always wins.""" + """Test specifically included entity always wins.""" filter_accept = { "media_player.test2", "media_player.test3", @@ -585,7 +586,7 @@ async def test_specificly_included_entity_always_wins( async def test_specificly_included_entity_always_wins_over_glob( recorder_mock: Recorder, hass: HomeAssistant ) -> None: - """Test specificlly included entity always wins over a glob.""" + """Test specifically included entity always wins over a glob.""" filter_accept = { "sensor.apc900va_status", "sensor.apc900va_battery_charge", diff --git a/tests/components/recorder/test_filters_with_entityfilter_schema_37.py b/tests/components/recorder/test_filters_with_entityfilter_schema_37.py index 9a03c024a83..f5eec10f805 100644 --- a/tests/components/recorder/test_filters_with_entityfilter_schema_37.py +++ b/tests/components/recorder/test_filters_with_entityfilter_schema_37.py @@ -1,4 +1,5 @@ """The tests for the recorder filter matching the EntityFilter component.""" + import json from unittest.mock import patch @@ -552,7 +553,7 @@ async def test_same_entity_included_excluded_include_domain_wins( async def test_specificly_included_entity_always_wins( legacy_recorder_mock: Recorder, hass: HomeAssistant ) -> None: - """Test specificlly included entity always wins.""" + """Test specifically included entity always wins.""" filter_accept = { "media_player.test2", "media_player.test3", @@ -602,7 +603,7 @@ async def test_specificly_included_entity_always_wins( async def test_specificly_included_entity_always_wins_over_glob( legacy_recorder_mock: Recorder, hass: HomeAssistant ) -> None: - """Test specificlly included entity always wins over a glob.""" + """Test specifically included entity always wins over a glob.""" filter_accept = { "sensor.apc900va_status", "sensor.apc900va_battery_charge", diff --git a/tests/components/recorder/test_history.py b/tests/components/recorder/test_history.py index 21af6b01182..d16a6856399 100644 --- a/tests/components/recorder/test_history.py +++ b/tests/components/recorder/test_history.py @@ -1,4 +1,5 @@ """The tests the History component.""" + from __future__ import annotations from collections.abc import Callable @@ -245,6 +246,41 @@ def test_state_changes_during_period( assert_multiple_states_equal_without_context(states[:limit], hist[entity_id]) +def test_state_changes_during_period_last_reported( + hass_recorder: Callable[..., HomeAssistant], +) -> None: + """Test state change during period.""" + hass = hass_recorder() + entity_id = "media_player.test" + + def set_state(state): + """Set the state.""" + hass.states.set(entity_id, state) + wait_recording_done(hass) + return hass.states.get(entity_id) + + start = dt_util.utcnow() + point1 = start + timedelta(seconds=1) + point2 = point1 + timedelta(seconds=1) + end = point2 + timedelta(seconds=1) + + with freeze_time(start) as freezer: + set_state("idle") + + freezer.move_to(point1) + set_state("YouTube") + + freezer.move_to(point2) + states = [set_state("YouTube")] + + freezer.move_to(end) + set_state("Netflix") + + hist = history.state_changes_during_period(hass, start, end, entity_id) + + assert_multiple_states_equal_without_context(states, hist[entity_id]) + + def test_state_changes_during_period_descending( hass_recorder: Callable[..., HomeAssistant], ) -> None: @@ -379,6 +415,38 @@ def test_get_last_state_changes(hass_recorder: Callable[..., HomeAssistant]) -> assert_multiple_states_equal_without_context(states, hist[entity_id]) +def test_get_last_state_changes_last_reported( + hass_recorder: Callable[..., HomeAssistant], +) -> None: + """Test number of state changes.""" + hass = hass_recorder() + entity_id = "sensor.test" + + def set_state(state): + """Set the state.""" + hass.states.set(entity_id, state) + wait_recording_done(hass) + return hass.states.get(entity_id) + + start = dt_util.utcnow() - timedelta(minutes=2) + point = start + timedelta(minutes=1) + point2 = point + timedelta(minutes=1, seconds=1) + states = [] + + with freeze_time(start) as freezer: + set_state("1") + + freezer.move_to(point) + states.append(set_state("1")) + + freezer.move_to(point2) + states.append(set_state("2")) + + hist = history.get_last_state_changes(hass, 2, entity_id) + + assert_multiple_states_equal_without_context(states, hist[entity_id]) + + def test_get_last_state_change(hass_recorder: Callable[..., HomeAssistant]) -> None: """Test getting the last state change for an entity.""" hass = hass_recorder() @@ -576,6 +644,7 @@ def test_get_significant_states_without_initial( ) ) del states["media_player.test2"] + del states["thermostat.test3"] hist = history.get_significant_states( hass, @@ -597,6 +666,7 @@ def test_get_significant_states_entity_id( del states["media_player.test3"] del states["thermostat.test"] del states["thermostat.test2"] + del states["thermostat.test3"] del states["script.can_cancel_this_one"] hist = history.get_significant_states(hass, zero, four, ["media_player.test"]) @@ -657,9 +727,7 @@ def test_get_significant_states_only( return hass.states.get(entity_id) start = dt_util.utcnow() - timedelta(minutes=4) - points = [] - for i in range(1, 4): - points.append(start + timedelta(minutes=i)) + points = [start + timedelta(minutes=i) for i in range(1, 4)] states = [] with freeze_time(start) as freezer: @@ -746,6 +814,7 @@ def record_states(hass) -> tuple[datetime, datetime, dict[str, list[State]]]: mp3 = "media_player.test3" therm = "thermostat.test" therm2 = "thermostat.test2" + therm3 = "thermostat.test3" zone = "zone.home" script_c = "script.can_cancel_this_one" @@ -761,7 +830,7 @@ def record_states(hass) -> tuple[datetime, datetime, dict[str, list[State]]]: three = two + timedelta(seconds=1) four = three + timedelta(seconds=1) - states = {therm: [], therm2: [], mp: [], mp2: [], mp3: [], script_c: []} + states = {therm: [], therm2: [], therm3: [], mp: [], mp2: [], mp3: [], script_c: []} with freeze_time(one) as freezer: states[mp].append( set_state(mp, "idle", attributes={"media_title": str(sentinel.mt1)}) @@ -775,6 +844,8 @@ def record_states(hass) -> tuple[datetime, datetime, dict[str, list[State]]]: states[therm].append( set_state(therm, 20, attributes={"current_temperature": 19.5}) ) + # This state will be updated + set_state(therm3, 20, attributes={"current_temperature": 19.5}) freezer.move_to(one + timedelta(microseconds=1)) states[mp].append( @@ -795,6 +866,8 @@ def record_states(hass) -> tuple[datetime, datetime, dict[str, list[State]]]: states[therm2].append( set_state(therm2, 20, attributes={"current_temperature": 19}) ) + # This state will be updated + set_state(therm3, 20, attributes={"current_temperature": 19.5}) freezer.move_to(three) states[mp].append( @@ -807,6 +880,9 @@ def record_states(hass) -> tuple[datetime, datetime, dict[str, list[State]]]: states[therm].append( set_state(therm, 21, attributes={"current_temperature": 20}) ) + states[therm3].append( + set_state(therm3, 20, attributes={"current_temperature": 19.5}) + ) return zero, four, states diff --git a/tests/components/recorder/test_history_db_schema_30.py b/tests/components/recorder/test_history_db_schema_30.py index 4f75dc15b15..cbe4c3ac5c8 100644 --- a/tests/components/recorder/test_history_db_schema_30.py +++ b/tests/components/recorder/test_history_db_schema_30.py @@ -1,4 +1,5 @@ """The tests the History component.""" + from __future__ import annotations from collections.abc import Callable @@ -44,8 +45,9 @@ def test_get_full_significant_states_with_session_entity_no_matches( now = dt_util.utcnow() time_before_recorder_ran = now - timedelta(days=1000) instance = recorder.get_instance(hass) - with session_scope(hass=hass) as session, patch.object( - instance.states_meta_manager, "active", False + with ( + session_scope(hass=hass) as session, + patch.object(instance.states_meta_manager, "active", False), ): assert ( history.get_full_significant_states_with_session( @@ -73,8 +75,9 @@ def test_significant_states_with_session_entity_minimal_response_no_matches( now = dt_util.utcnow() time_before_recorder_ran = now - timedelta(days=1000) instance = recorder.get_instance(hass) - with session_scope(hass=hass) as session, patch.object( - instance.states_meta_manager, "active", False + with ( + session_scope(hass=hass) as session, + patch.object(instance.states_meta_manager, "active", False), ): assert ( history.get_significant_states_with_session( @@ -516,9 +519,7 @@ def test_get_significant_states_only( return hass.states.get(entity_id) start = dt_util.utcnow() - timedelta(minutes=4) - points = [] - for i in range(1, 4): - points.append(start + timedelta(minutes=i)) + points = [start + timedelta(minutes=i) for i in range(1, 4)] states = [] with freeze_time(start) as freezer: diff --git a/tests/components/recorder/test_history_db_schema_32.py b/tests/components/recorder/test_history_db_schema_32.py index 477c13d6166..b926aa1903b 100644 --- a/tests/components/recorder/test_history_db_schema_32.py +++ b/tests/components/recorder/test_history_db_schema_32.py @@ -1,4 +1,5 @@ """The tests the History component.""" + from __future__ import annotations from collections.abc import Callable @@ -44,8 +45,9 @@ def test_get_full_significant_states_with_session_entity_no_matches( now = dt_util.utcnow() time_before_recorder_ran = now - timedelta(days=1000) instance = recorder.get_instance(hass) - with session_scope(hass=hass) as session, patch.object( - instance.states_meta_manager, "active", False + with ( + session_scope(hass=hass) as session, + patch.object(instance.states_meta_manager, "active", False), ): assert ( history.get_full_significant_states_with_session( @@ -73,8 +75,9 @@ def test_significant_states_with_session_entity_minimal_response_no_matches( now = dt_util.utcnow() time_before_recorder_ran = now - timedelta(days=1000) instance = recorder.get_instance(hass) - with session_scope(hass=hass) as session, patch.object( - instance.states_meta_manager, "active", False + with ( + session_scope(hass=hass) as session, + patch.object(instance.states_meta_manager, "active", False), ): assert ( history.get_significant_states_with_session( @@ -506,9 +509,7 @@ def test_get_significant_states_only( return hass.states.get(entity_id) start = dt_util.utcnow() - timedelta(minutes=4) - points = [] - for i in range(1, 4): - points.append(start + timedelta(minutes=i)) + points = [start + timedelta(minutes=i) for i in range(1, 4)] states = [] with freeze_time(start) as freezer: diff --git a/tests/components/recorder/test_history_db_schema_42.py b/tests/components/recorder/test_history_db_schema_42.py new file mode 100644 index 00000000000..98ed6089de6 --- /dev/null +++ b/tests/components/recorder/test_history_db_schema_42.py @@ -0,0 +1,1278 @@ +"""The tests the History component.""" + +from __future__ import annotations + +from collections.abc import Callable +from copy import copy +from datetime import datetime, timedelta +import json +from unittest.mock import patch, sentinel + +from freezegun import freeze_time +import pytest +from sqlalchemy import text + +from homeassistant.components import recorder +from homeassistant.components.recorder import Recorder, get_instance, history +from homeassistant.components.recorder.filters import Filters +from homeassistant.components.recorder.history import legacy +from homeassistant.components.recorder.models import process_timestamp +from homeassistant.components.recorder.models.legacy import ( + LegacyLazyState, + LegacyLazyStatePreSchema31, +) +from homeassistant.components.recorder.util import session_scope +import homeassistant.core as ha +from homeassistant.core import HomeAssistant, State +from homeassistant.helpers.json import JSONEncoder +import homeassistant.util.dt as dt_util + +from .common import ( + assert_dict_of_states_equal_without_context_and_last_changed, + assert_multiple_states_equal_without_context, + assert_multiple_states_equal_without_context_and_last_changed, + assert_states_equal_without_context, + async_recorder_block_till_done, + async_wait_recording_done, + old_db_schema, + wait_recording_done, +) +from .db_schema_42 import Events, RecorderRuns, StateAttributes, States, StatesMeta + +from tests.typing import RecorderInstanceGenerator + + +@pytest.fixture(autouse=True) +def db_schema_42(): + """Fixture to initialize the db with the old schema 42.""" + with old_db_schema("42"): + yield + + +async def _async_get_states( + hass: HomeAssistant, + utc_point_in_time: datetime, + entity_ids: list[str] | None = None, + run: RecorderRuns | None = None, + no_attributes: bool = False, +): + """Get states from the database.""" + + def _get_states_with_session(): + with session_scope(hass=hass, read_only=True) as session: + attr_cache = {} + pre_31_schema = get_instance(hass).schema_version < 31 + return [ + LegacyLazyStatePreSchema31(row, attr_cache, None) + if pre_31_schema + else LegacyLazyState( + row, + attr_cache, + None, + row.entity_id, + ) + for row in legacy._get_rows_with_session( + hass, + session, + utc_point_in_time, + entity_ids, + run, + no_attributes, + ) + ] + + return await recorder.get_instance(hass).async_add_executor_job( + _get_states_with_session + ) + + +def _add_db_entries( + hass: ha.HomeAssistant, point: datetime, entity_ids: list[str] +) -> None: + with session_scope(hass=hass) as session: + for idx, entity_id in enumerate(entity_ids): + session.add( + Events( + event_id=1001 + idx, + event_type="state_changed", + event_data="{}", + origin="LOCAL", + time_fired=point, + ) + ) + session.add( + States( + entity_id=entity_id, + state="on", + attributes='{"name":"the light"}', + last_changed=None, + last_updated=point, + event_id=1001 + idx, + attributes_id=1002 + idx, + ) + ) + session.add( + StateAttributes( + shared_attrs='{"name":"the shared light"}', + hash=1234 + idx, + attributes_id=1002 + idx, + ) + ) + + +def test_get_full_significant_states_with_session_entity_no_matches( + hass_recorder: Callable[..., HomeAssistant], +) -> None: + """Test getting states at a specific point in time for entities that never have been recorded.""" + hass = hass_recorder() + now = dt_util.utcnow() + time_before_recorder_ran = now - timedelta(days=1000) + with session_scope(hass=hass, read_only=True) as session: + assert ( + history.get_full_significant_states_with_session( + hass, session, time_before_recorder_ran, now, entity_ids=["demo.id"] + ) + == {} + ) + assert ( + history.get_full_significant_states_with_session( + hass, + session, + time_before_recorder_ran, + now, + entity_ids=["demo.id", "demo.id2"], + ) + == {} + ) + + +def test_significant_states_with_session_entity_minimal_response_no_matches( + hass_recorder: Callable[..., HomeAssistant], +) -> None: + """Test getting states at a specific point in time for entities that never have been recorded.""" + hass = hass_recorder() + now = dt_util.utcnow() + time_before_recorder_ran = now - timedelta(days=1000) + with session_scope(hass=hass, read_only=True) as session: + assert ( + history.get_significant_states_with_session( + hass, + session, + time_before_recorder_ran, + now, + entity_ids=["demo.id"], + minimal_response=True, + ) + == {} + ) + assert ( + history.get_significant_states_with_session( + hass, + session, + time_before_recorder_ran, + now, + entity_ids=["demo.id", "demo.id2"], + minimal_response=True, + ) + == {} + ) + + +def test_significant_states_with_session_single_entity( + hass_recorder: Callable[..., HomeAssistant], +) -> None: + """Test get_significant_states_with_session with a single entity.""" + hass = hass_recorder() + hass.states.set("demo.id", "any", {"attr": True}) + hass.states.set("demo.id", "any2", {"attr": True}) + wait_recording_done(hass) + now = dt_util.utcnow() + with session_scope(hass=hass, read_only=True) as session: + states = history.get_significant_states_with_session( + hass, + session, + now - timedelta(days=1), + now, + entity_ids=["demo.id"], + minimal_response=False, + ) + assert len(states["demo.id"]) == 2 + + +@pytest.mark.parametrize( + ("attributes", "no_attributes", "limit"), + [ + ({"attr": True}, False, 5000), + ({}, True, 5000), + ({"attr": True}, False, 3), + ({}, True, 3), + ], +) +def test_state_changes_during_period( + hass_recorder: Callable[..., HomeAssistant], attributes, no_attributes, limit +) -> None: + """Test state change during period.""" + hass = hass_recorder() + entity_id = "media_player.test" + + def set_state(state): + """Set the state.""" + hass.states.set(entity_id, state, attributes) + wait_recording_done(hass) + return hass.states.get(entity_id) + + start = dt_util.utcnow() + point = start + timedelta(seconds=1) + end = point + timedelta(seconds=1) + + with freeze_time(start) as freezer: + set_state("idle") + set_state("YouTube") + + freezer.move_to(point) + states = [ + set_state("idle"), + set_state("Netflix"), + set_state("Plex"), + set_state("YouTube"), + ] + + freezer.move_to(end) + set_state("Netflix") + set_state("Plex") + + hist = history.state_changes_during_period( + hass, start, end, entity_id, no_attributes, limit=limit + ) + + assert_multiple_states_equal_without_context(states[:limit], hist[entity_id]) + + +def test_state_changes_during_period_last_reported( + hass_recorder: Callable[..., HomeAssistant], +) -> None: + """Test state change during period.""" + hass = hass_recorder() + entity_id = "media_player.test" + + def set_state(state): + """Set the state.""" + hass.states.set(entity_id, state) + wait_recording_done(hass) + return ha.State.from_dict(hass.states.get(entity_id).as_dict()) + + start = dt_util.utcnow() + point1 = start + timedelta(seconds=1) + point2 = point1 + timedelta(seconds=1) + end = point2 + timedelta(seconds=1) + + with freeze_time(start) as freezer: + set_state("idle") + + freezer.move_to(point1) + states = [set_state("YouTube")] + + freezer.move_to(point2) + set_state("YouTube") + + freezer.move_to(end) + set_state("Netflix") + + hist = history.state_changes_during_period(hass, start, end, entity_id) + + assert_multiple_states_equal_without_context(states, hist[entity_id]) + + +def test_state_changes_during_period_descending( + hass_recorder: Callable[..., HomeAssistant], +) -> None: + """Test state change during period descending.""" + hass = hass_recorder() + entity_id = "media_player.test" + + def set_state(state): + """Set the state.""" + hass.states.set(entity_id, state, {"any": 1}) + wait_recording_done(hass) + return hass.states.get(entity_id) + + start = dt_util.utcnow().replace(microsecond=0) + point = start + timedelta(seconds=1) + point2 = start + timedelta(seconds=1, microseconds=100) + point3 = start + timedelta(seconds=1, microseconds=200) + point4 = start + timedelta(seconds=1, microseconds=300) + end = point + timedelta(seconds=1, microseconds=400) + + with freeze_time(start) as freezer: + set_state("idle") + set_state("YouTube") + + freezer.move_to(point) + states = [set_state("idle")] + + freezer.move_to(point2) + states.append(set_state("Netflix")) + + freezer.move_to(point3) + states.append(set_state("Plex")) + + freezer.move_to(point4) + states.append(set_state("YouTube")) + + freezer.move_to(end) + set_state("Netflix") + set_state("Plex") + + hist = history.state_changes_during_period( + hass, start, end, entity_id, no_attributes=False, descending=False + ) + + assert_multiple_states_equal_without_context(states, hist[entity_id]) + + hist = history.state_changes_during_period( + hass, start, end, entity_id, no_attributes=False, descending=True + ) + assert_multiple_states_equal_without_context( + states, list(reversed(list(hist[entity_id]))) + ) + + start_time = point2 + timedelta(microseconds=10) + hist = history.state_changes_during_period( + hass, + start_time, # Pick a point where we will generate a start time state + end, + entity_id, + no_attributes=False, + descending=True, + include_start_time_state=True, + ) + hist_states = list(hist[entity_id]) + assert hist_states[-1].last_updated == start_time + assert hist_states[-1].last_changed == start_time + assert len(hist_states) == 3 + # Make sure they are in descending order + assert ( + hist_states[0].last_updated + > hist_states[1].last_updated + > hist_states[2].last_updated + ) + assert ( + hist_states[0].last_changed + > hist_states[1].last_changed + > hist_states[2].last_changed + ) + hist = history.state_changes_during_period( + hass, + start_time, # Pick a point where we will generate a start time state + end, + entity_id, + no_attributes=False, + descending=False, + include_start_time_state=True, + ) + hist_states = list(hist[entity_id]) + assert hist_states[0].last_updated == start_time + assert hist_states[0].last_changed == start_time + assert len(hist_states) == 3 + # Make sure they are in ascending order + assert ( + hist_states[0].last_updated + < hist_states[1].last_updated + < hist_states[2].last_updated + ) + assert ( + hist_states[0].last_changed + < hist_states[1].last_changed + < hist_states[2].last_changed + ) + + +def test_get_last_state_changes(hass_recorder: Callable[..., HomeAssistant]) -> None: + """Test number of state changes.""" + hass = hass_recorder() + entity_id = "sensor.test" + + def set_state(state): + """Set the state.""" + hass.states.set(entity_id, state) + wait_recording_done(hass) + return hass.states.get(entity_id) + + start = dt_util.utcnow() - timedelta(minutes=2) + point = start + timedelta(minutes=1) + point2 = point + timedelta(minutes=1, seconds=1) + states = [] + + with freeze_time(start) as freezer: + set_state("1") + + freezer.move_to(point) + states.append(set_state("2")) + + freezer.move_to(point2) + states.append(set_state("3")) + + hist = history.get_last_state_changes(hass, 2, entity_id) + + assert_multiple_states_equal_without_context(states, hist[entity_id]) + + +def test_get_last_state_changes_last_reported( + hass_recorder: Callable[..., HomeAssistant], +) -> None: + """Test number of state changes.""" + hass = hass_recorder() + entity_id = "sensor.test" + + def set_state(state): + """Set the state.""" + hass.states.set(entity_id, state) + wait_recording_done(hass) + return ha.State.from_dict(hass.states.get(entity_id).as_dict()) + + start = dt_util.utcnow() - timedelta(minutes=2) + point = start + timedelta(minutes=1) + point2 = point + timedelta(minutes=1, seconds=1) + states = [] + + with freeze_time(start) as freezer: + states.append(set_state("1")) + + freezer.move_to(point) + set_state("1") + + freezer.move_to(point2) + states.append(set_state("2")) + + hist = history.get_last_state_changes(hass, 2, entity_id) + + assert_multiple_states_equal_without_context(states, hist[entity_id]) + + +def test_get_last_state_change(hass_recorder: Callable[..., HomeAssistant]) -> None: + """Test getting the last state change for an entity.""" + hass = hass_recorder() + entity_id = "sensor.test" + + def set_state(state): + """Set the state.""" + hass.states.set(entity_id, state) + wait_recording_done(hass) + return hass.states.get(entity_id) + + start = dt_util.utcnow() - timedelta(minutes=2) + point = start + timedelta(minutes=1) + point2 = point + timedelta(minutes=1, seconds=1) + states = [] + + with freeze_time(start) as freezer: + set_state("1") + + freezer.move_to(point) + set_state("2") + + freezer.move_to(point2) + states.append(set_state("3")) + + hist = history.get_last_state_changes(hass, 1, entity_id) + + assert_multiple_states_equal_without_context(states, hist[entity_id]) + + +def test_ensure_state_can_be_copied( + hass_recorder: Callable[..., HomeAssistant], +) -> None: + """Ensure a state can pass though copy(). + + The filter integration uses copy() on states + from history. + """ + hass = hass_recorder() + entity_id = "sensor.test" + + def set_state(state): + """Set the state.""" + hass.states.set(entity_id, state) + wait_recording_done(hass) + return hass.states.get(entity_id) + + start = dt_util.utcnow() - timedelta(minutes=2) + point = start + timedelta(minutes=1) + + with freeze_time(start) as freezer: + set_state("1") + + freezer.move_to(point) + set_state("2") + + hist = history.get_last_state_changes(hass, 2, entity_id) + + assert_states_equal_without_context(copy(hist[entity_id][0]), hist[entity_id][0]) + assert_states_equal_without_context(copy(hist[entity_id][1]), hist[entity_id][1]) + + +def test_get_significant_states(hass_recorder: Callable[..., HomeAssistant]) -> None: + """Test that only significant states are returned. + + We should get back every thermostat change that + includes an attribute change, but only the state updates for + media player (attribute changes are not significant and not returned). + """ + hass = hass_recorder() + zero, four, states = record_states(hass) + hist = history.get_significant_states(hass, zero, four, entity_ids=list(states)) + assert_dict_of_states_equal_without_context_and_last_changed(states, hist) + + +def test_get_significant_states_minimal_response( + hass_recorder: Callable[..., HomeAssistant], +) -> None: + """Test that only significant states are returned. + + When minimal responses is set only the first and + last states return a complete state. + + We should get back every thermostat change that + includes an attribute change, but only the state updates for + media player (attribute changes are not significant and not returned). + """ + hass = hass_recorder() + zero, four, states = record_states(hass) + hist = history.get_significant_states( + hass, zero, four, minimal_response=True, entity_ids=list(states) + ) + entites_with_reducable_states = [ + "media_player.test", + "media_player.test3", + ] + + # All states for media_player.test state are reduced + # down to last_changed and state when minimal_response + # is set except for the first state. + # is set. We use JSONEncoder to make sure that are + # pre-encoded last_changed is always the same as what + # will happen with encoding a native state + for entity_id in entites_with_reducable_states: + entity_states = states[entity_id] + for state_idx in range(1, len(entity_states)): + input_state = entity_states[state_idx] + orig_last_changed = orig_last_changed = json.dumps( + process_timestamp(input_state.last_changed), + cls=JSONEncoder, + ).replace('"', "") + orig_state = input_state.state + entity_states[state_idx] = { + "last_changed": orig_last_changed, + "state": orig_state, + } + + assert len(hist) == len(states) + assert_states_equal_without_context( + states["media_player.test"][0], hist["media_player.test"][0] + ) + assert states["media_player.test"][1] == hist["media_player.test"][1] + assert states["media_player.test"][2] == hist["media_player.test"][2] + + assert_multiple_states_equal_without_context( + states["media_player.test2"], hist["media_player.test2"] + ) + assert_states_equal_without_context( + states["media_player.test3"][0], hist["media_player.test3"][0] + ) + assert states["media_player.test3"][1] == hist["media_player.test3"][1] + + assert_multiple_states_equal_without_context( + states["script.can_cancel_this_one"], hist["script.can_cancel_this_one"] + ) + assert_multiple_states_equal_without_context_and_last_changed( + states["thermostat.test"], hist["thermostat.test"] + ) + assert_multiple_states_equal_without_context_and_last_changed( + states["thermostat.test2"], hist["thermostat.test2"] + ) + + +@pytest.mark.parametrize("time_zone", ["Europe/Berlin", "US/Hawaii", "UTC"]) +def test_get_significant_states_with_initial( + time_zone, hass_recorder: Callable[..., HomeAssistant] +) -> None: + """Test that only significant states are returned. + + We should get back every thermostat change that + includes an attribute change, but only the state updates for + media player (attribute changes are not significant and not returned). + """ + hass = hass_recorder() + hass.config.set_time_zone(time_zone) + zero, four, states = record_states(hass) + one_and_half = zero + timedelta(seconds=1.5) + for entity_id in states: + if entity_id == "media_player.test": + states[entity_id] = states[entity_id][1:] + for state in states[entity_id]: + # If the state is recorded before the start time + # start it will have its last_updated and last_changed + # set to the start time. + if state.last_updated < one_and_half: + state.last_updated = one_and_half + state.last_changed = one_and_half + + hist = history.get_significant_states( + hass, one_and_half, four, include_start_time_state=True, entity_ids=list(states) + ) + assert_dict_of_states_equal_without_context_and_last_changed(states, hist) + + +def test_get_significant_states_without_initial( + hass_recorder: Callable[..., HomeAssistant], +) -> None: + """Test that only significant states are returned. + + We should get back every thermostat change that + includes an attribute change, but only the state updates for + media player (attribute changes are not significant and not returned). + """ + hass = hass_recorder() + zero, four, states = record_states(hass) + one = zero + timedelta(seconds=1) + one_with_microsecond = zero + timedelta(seconds=1, microseconds=1) + one_and_half = zero + timedelta(seconds=1.5) + for entity_id in states: + states[entity_id] = list( + filter( + lambda s: s.last_changed != one + and s.last_changed != one_with_microsecond, + states[entity_id], + ) + ) + del states["media_player.test2"] + del states["thermostat.test3"] + + hist = history.get_significant_states( + hass, + one_and_half, + four, + include_start_time_state=False, + entity_ids=list(states), + ) + assert_dict_of_states_equal_without_context_and_last_changed(states, hist) + + +def test_get_significant_states_entity_id( + hass_recorder: Callable[..., HomeAssistant], +) -> None: + """Test that only significant states are returned for one entity.""" + hass = hass_recorder() + zero, four, states = record_states(hass) + del states["media_player.test2"] + del states["media_player.test3"] + del states["thermostat.test"] + del states["thermostat.test2"] + del states["thermostat.test3"] + del states["script.can_cancel_this_one"] + + hist = history.get_significant_states(hass, zero, four, ["media_player.test"]) + assert_dict_of_states_equal_without_context_and_last_changed(states, hist) + + +def test_get_significant_states_multiple_entity_ids( + hass_recorder: Callable[..., HomeAssistant], +) -> None: + """Test that only significant states are returned for one entity.""" + hass = hass_recorder() + zero, four, states = record_states(hass) + + hist = history.get_significant_states( + hass, + zero, + four, + ["media_player.test", "thermostat.test"], + ) + + assert_multiple_states_equal_without_context_and_last_changed( + states["media_player.test"], hist["media_player.test"] + ) + assert_multiple_states_equal_without_context_and_last_changed( + states["thermostat.test"], hist["thermostat.test"] + ) + + +def test_get_significant_states_are_ordered( + hass_recorder: Callable[..., HomeAssistant], +) -> None: + """Test order of results from get_significant_states. + + When entity ids are given, the results should be returned with the data + in the same order. + """ + hass = hass_recorder() + zero, four, _states = record_states(hass) + entity_ids = ["media_player.test", "media_player.test2"] + hist = history.get_significant_states(hass, zero, four, entity_ids) + assert list(hist.keys()) == entity_ids + entity_ids = ["media_player.test2", "media_player.test"] + hist = history.get_significant_states(hass, zero, four, entity_ids) + assert list(hist.keys()) == entity_ids + + +def test_get_significant_states_only( + hass_recorder: Callable[..., HomeAssistant], +) -> None: + """Test significant states when significant_states_only is set.""" + hass = hass_recorder() + entity_id = "sensor.test" + + def set_state(state, **kwargs): + """Set the state.""" + hass.states.set(entity_id, state, **kwargs) + wait_recording_done(hass) + return hass.states.get(entity_id) + + start = dt_util.utcnow() - timedelta(minutes=4) + points = [start + timedelta(minutes=i) for i in range(1, 4)] + + states = [] + with freeze_time(start) as freezer: + set_state("123", attributes={"attribute": 10.64}) + + freezer.move_to(points[0]) + # Attributes are different, state not + states.append(set_state("123", attributes={"attribute": 21.42})) + + freezer.move_to(points[1]) + # state is different, attributes not + states.append(set_state("32", attributes={"attribute": 21.42})) + + freezer.move_to(points[2]) + # everything is different + states.append(set_state("412", attributes={"attribute": 54.23})) + + hist = history.get_significant_states( + hass, + start, + significant_changes_only=True, + entity_ids=list({state.entity_id for state in states}), + ) + + assert len(hist[entity_id]) == 2 + assert not any( + state.last_updated == states[0].last_updated for state in hist[entity_id] + ) + assert any( + state.last_updated == states[1].last_updated for state in hist[entity_id] + ) + assert any( + state.last_updated == states[2].last_updated for state in hist[entity_id] + ) + + hist = history.get_significant_states( + hass, + start, + significant_changes_only=False, + entity_ids=list({state.entity_id for state in states}), + ) + + assert len(hist[entity_id]) == 3 + assert_multiple_states_equal_without_context_and_last_changed( + states, hist[entity_id] + ) + + +async def test_get_significant_states_only_minimal_response( + recorder_mock: Recorder, hass: HomeAssistant +) -> None: + """Test significant states when significant_states_only is True.""" + now = dt_util.utcnow() + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.test", "on", attributes={"any": "attr"}) + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.test", "off", attributes={"any": "attr"}) + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.test", "off", attributes={"any": "changed"}) + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.test", "off", attributes={"any": "again"}) + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.test", "on", attributes={"any": "attr"}) + await async_wait_recording_done(hass) + + hist = history.get_significant_states( + hass, + now, + minimal_response=True, + significant_changes_only=False, + entity_ids=["sensor.test"], + ) + assert len(hist["sensor.test"]) == 3 + + +def record_states(hass) -> tuple[datetime, datetime, dict[str, list[State]]]: + """Record some test states. + + We inject a bunch of state updates from media player, zone and + thermostat. + """ + mp = "media_player.test" + mp2 = "media_player.test2" + mp3 = "media_player.test3" + therm = "thermostat.test" + therm2 = "thermostat.test2" + therm3 = "thermostat.test3" + zone = "zone.home" + script_c = "script.can_cancel_this_one" + + def set_state(entity_id, state, **kwargs): + """Set the state.""" + hass.states.set(entity_id, state, **kwargs) + wait_recording_done(hass) + return hass.states.get(entity_id) + + zero = dt_util.utcnow() + one = zero + timedelta(seconds=1) + two = one + timedelta(seconds=1) + three = two + timedelta(seconds=1) + four = three + timedelta(seconds=1) + + states = {therm: [], therm2: [], therm3: [], mp: [], mp2: [], mp3: [], script_c: []} + with freeze_time(one) as freezer: + states[mp].append( + set_state(mp, "idle", attributes={"media_title": str(sentinel.mt1)}) + ) + states[mp2].append( + set_state(mp2, "YouTube", attributes={"media_title": str(sentinel.mt2)}) + ) + states[mp3].append( + set_state(mp3, "idle", attributes={"media_title": str(sentinel.mt1)}) + ) + states[therm].append( + set_state(therm, 20, attributes={"current_temperature": 19.5}) + ) + # This state will be updated + set_state(therm3, 20, attributes={"current_temperature": 19.5}) + + freezer.move_to(one + timedelta(microseconds=1)) + states[mp].append( + set_state(mp, "YouTube", attributes={"media_title": str(sentinel.mt2)}) + ) + + freezer.move_to(two) + # This state will be skipped only different in time + set_state(mp, "YouTube", attributes={"media_title": str(sentinel.mt3)}) + # This state will be skipped because domain is excluded + set_state(zone, "zoning") + states[script_c].append( + set_state(script_c, "off", attributes={"can_cancel": True}) + ) + states[therm].append( + set_state(therm, 21, attributes={"current_temperature": 19.8}) + ) + states[therm2].append( + set_state(therm2, 20, attributes={"current_temperature": 19}) + ) + # This state will be updated + set_state(therm3, 20, attributes={"current_temperature": 19.5}) + + freezer.move_to(three) + states[mp].append( + set_state(mp, "Netflix", attributes={"media_title": str(sentinel.mt4)}) + ) + states[mp3].append( + set_state(mp3, "Netflix", attributes={"media_title": str(sentinel.mt3)}) + ) + # Attributes changed even though state is the same + states[therm].append( + set_state(therm, 21, attributes={"current_temperature": 20}) + ) + states[therm3].append( + set_state(therm3, 20, attributes={"current_temperature": 19.5}) + ) + + return zero, four, states + + +async def test_state_changes_during_period_query_during_migration_to_schema_25( + async_setup_recorder_instance: RecorderInstanceGenerator, + hass: HomeAssistant, + recorder_db_url: str, +) -> None: + """Test we can query data prior to schema 25 and during migration to schema 25.""" + if recorder_db_url.startswith(("mysql://", "postgresql://")): + # This test doesn't run on MySQL / MariaDB / Postgresql; we can't drop table state_attributes + return + + instance = await async_setup_recorder_instance(hass, {}) + + with patch.object(instance.states_meta_manager, "active", False): + start = dt_util.utcnow() + point = start + timedelta(seconds=1) + end = point + timedelta(seconds=1) + entity_id = "light.test" + await recorder.get_instance(hass).async_add_executor_job( + _add_db_entries, hass, point, [entity_id] + ) + + no_attributes = True + hist = history.state_changes_during_period( + hass, start, end, entity_id, no_attributes, include_start_time_state=False + ) + state = hist[entity_id][0] + assert state.attributes == {} + + no_attributes = False + hist = history.state_changes_during_period( + hass, start, end, entity_id, no_attributes, include_start_time_state=False + ) + state = hist[entity_id][0] + assert state.attributes == {"name": "the shared light"} + + with instance.engine.connect() as conn: + conn.execute(text("update states set attributes_id=NULL;")) + conn.execute(text("drop table state_attributes;")) + conn.commit() + + with patch.object(instance, "schema_version", 24): + instance.states_meta_manager.active = False + no_attributes = True + hist = history.state_changes_during_period( + hass, + start, + end, + entity_id, + no_attributes, + include_start_time_state=False, + ) + state = hist[entity_id][0] + assert state.attributes == {} + + no_attributes = False + hist = history.state_changes_during_period( + hass, + start, + end, + entity_id, + no_attributes, + include_start_time_state=False, + ) + state = hist[entity_id][0] + assert state.attributes == {"name": "the light"} + + +async def test_get_states_query_during_migration_to_schema_25( + async_setup_recorder_instance: RecorderInstanceGenerator, + hass: HomeAssistant, + recorder_db_url: str, +) -> None: + """Test we can query data prior to schema 25 and during migration to schema 25.""" + if recorder_db_url.startswith(("mysql://", "postgresql://")): + # This test doesn't run on MySQL / MariaDB / Postgresql; we can't drop table state_attributes + return + + instance = await async_setup_recorder_instance(hass, {}) + + start = dt_util.utcnow() + point = start + timedelta(seconds=1) + end = point + timedelta(seconds=1) + entity_id = "light.test" + await instance.async_add_executor_job(_add_db_entries, hass, point, [entity_id]) + assert instance.states_meta_manager.active + + no_attributes = True + hist = await _async_get_states(hass, end, [entity_id], no_attributes=no_attributes) + state = hist[0] + assert state.attributes == {} + + no_attributes = False + hist = await _async_get_states(hass, end, [entity_id], no_attributes=no_attributes) + state = hist[0] + assert state.attributes == {"name": "the shared light"} + + with instance.engine.connect() as conn: + conn.execute(text("update states set attributes_id=NULL;")) + conn.execute(text("drop table state_attributes;")) + conn.commit() + + with patch.object(instance, "schema_version", 24): + instance.states_meta_manager.active = False + no_attributes = True + hist = await _async_get_states( + hass, end, [entity_id], no_attributes=no_attributes + ) + state = hist[0] + assert state.attributes == {} + + no_attributes = False + hist = await _async_get_states( + hass, end, [entity_id], no_attributes=no_attributes + ) + state = hist[0] + assert state.attributes == {"name": "the light"} + + +async def test_get_states_query_during_migration_to_schema_25_multiple_entities( + async_setup_recorder_instance: RecorderInstanceGenerator, + hass: HomeAssistant, + recorder_db_url: str, +) -> None: + """Test we can query data prior to schema 25 and during migration to schema 25.""" + if recorder_db_url.startswith(("mysql://", "postgresql://")): + # This test doesn't run on MySQL / MariaDB / Postgresql; we can't drop table state_attributes + return + + instance = await async_setup_recorder_instance(hass, {}) + + start = dt_util.utcnow() + point = start + timedelta(seconds=1) + end = point + timedelta(seconds=1) + entity_id_1 = "light.test" + entity_id_2 = "switch.test" + entity_ids = [entity_id_1, entity_id_2] + + await instance.async_add_executor_job(_add_db_entries, hass, point, entity_ids) + assert instance.states_meta_manager.active + + no_attributes = True + hist = await _async_get_states(hass, end, entity_ids, no_attributes=no_attributes) + assert hist[0].attributes == {} + assert hist[1].attributes == {} + + no_attributes = False + hist = await _async_get_states(hass, end, entity_ids, no_attributes=no_attributes) + assert hist[0].attributes == {"name": "the shared light"} + assert hist[1].attributes == {"name": "the shared light"} + + with instance.engine.connect() as conn: + conn.execute(text("update states set attributes_id=NULL;")) + conn.execute(text("drop table state_attributes;")) + conn.commit() + + with patch.object(instance, "schema_version", 24): + instance.states_meta_manager.active = False + no_attributes = True + hist = await _async_get_states( + hass, end, entity_ids, no_attributes=no_attributes + ) + assert hist[0].attributes == {} + assert hist[1].attributes == {} + + no_attributes = False + hist = await _async_get_states( + hass, end, entity_ids, no_attributes=no_attributes + ) + assert hist[0].attributes == {"name": "the light"} + assert hist[1].attributes == {"name": "the light"} + + +async def test_get_full_significant_states_handles_empty_last_changed( + async_setup_recorder_instance: RecorderInstanceGenerator, + hass: HomeAssistant, +) -> None: + """Test getting states when last_changed is null.""" + await async_setup_recorder_instance(hass, {}) + + now = dt_util.utcnow() + hass.states.async_set("sensor.one", "on", {"attr": "original"}) + state0 = hass.states.get("sensor.one") + await hass.async_block_till_done() + hass.states.async_set("sensor.one", "on", {"attr": "new"}) + state1 = hass.states.get("sensor.one") + + assert state0.last_changed == state1.last_changed + assert state0.last_updated != state1.last_updated + await async_wait_recording_done(hass) + + def _get_entries(): + with session_scope(hass=hass, read_only=True) as session: + return history.get_full_significant_states_with_session( + hass, + session, + now, + dt_util.utcnow(), + entity_ids=["sensor.one"], + significant_changes_only=False, + ) + + states = await recorder.get_instance(hass).async_add_executor_job(_get_entries) + sensor_one_states: list[State] = states["sensor.one"] + assert_states_equal_without_context(sensor_one_states[0], state0) + assert_states_equal_without_context(sensor_one_states[1], state1) + assert sensor_one_states[0].last_changed == sensor_one_states[1].last_changed + assert sensor_one_states[0].last_updated != sensor_one_states[1].last_updated + + def _fetch_native_states() -> list[State]: + with session_scope(hass=hass, read_only=True) as session: + native_states = [] + db_state_attributes = { + state_attributes.attributes_id: state_attributes + for state_attributes in session.query(StateAttributes) + } + metadata_id_to_entity_id = { + states_meta.metadata_id: states_meta + for states_meta in session.query(StatesMeta) + } + for db_state in session.query(States): + db_state.entity_id = metadata_id_to_entity_id[ + db_state.metadata_id + ].entity_id + state = db_state.to_native() + state.attributes = db_state_attributes[ + db_state.attributes_id + ].to_native() + native_states.append(state) + return native_states + + native_sensor_one_states = await recorder.get_instance(hass).async_add_executor_job( + _fetch_native_states + ) + assert_states_equal_without_context(native_sensor_one_states[0], state0) + assert_states_equal_without_context(native_sensor_one_states[1], state1) + assert ( + native_sensor_one_states[0].last_changed + == native_sensor_one_states[1].last_changed + ) + assert ( + native_sensor_one_states[0].last_updated + != native_sensor_one_states[1].last_updated + ) + + def _fetch_db_states() -> list[States]: + with session_scope(hass=hass, read_only=True) as session: + states = list(session.query(States)) + session.expunge_all() + return states + + db_sensor_one_states = await recorder.get_instance(hass).async_add_executor_job( + _fetch_db_states + ) + assert db_sensor_one_states[0].last_changed is None + assert db_sensor_one_states[0].last_changed_ts is None + + assert ( + process_timestamp( + dt_util.utc_from_timestamp(db_sensor_one_states[1].last_changed_ts) + ) + == state0.last_changed + ) + assert db_sensor_one_states[0].last_updated_ts is not None + assert db_sensor_one_states[1].last_updated_ts is not None + assert ( + db_sensor_one_states[0].last_updated_ts + != db_sensor_one_states[1].last_updated_ts + ) + + +def test_state_changes_during_period_multiple_entities_single_test( + hass_recorder: Callable[..., HomeAssistant], +) -> None: + """Test state change during period with multiple entities in the same test. + + This test ensures the sqlalchemy query cache does not + generate incorrect results. + """ + hass = hass_recorder() + start = dt_util.utcnow() + test_entites = {f"sensor.{i}": str(i) for i in range(30)} + for entity_id, value in test_entites.items(): + hass.states.set(entity_id, value) + + wait_recording_done(hass) + end = dt_util.utcnow() + + for entity_id, value in test_entites.items(): + hist = history.state_changes_during_period(hass, start, end, entity_id) + assert len(hist) == 1 + assert hist[entity_id][0].state == value + + +@pytest.mark.freeze_time("2039-01-19 03:14:07.555555-00:00") +async def test_get_full_significant_states_past_year_2038( + async_setup_recorder_instance: RecorderInstanceGenerator, + hass: HomeAssistant, +) -> None: + """Test we can store times past year 2038.""" + await async_setup_recorder_instance(hass, {}) + past_2038_time = dt_util.parse_datetime("2039-01-19 03:14:07.555555-00:00") + hass.states.async_set("sensor.one", "on", {"attr": "original"}) + state0 = hass.states.get("sensor.one") + await hass.async_block_till_done() + + hass.states.async_set("sensor.one", "on", {"attr": "new"}) + state1 = hass.states.get("sensor.one") + + await async_wait_recording_done(hass) + + def _get_entries(): + with session_scope(hass=hass, read_only=True) as session: + return history.get_full_significant_states_with_session( + hass, + session, + past_2038_time - timedelta(days=365), + past_2038_time + timedelta(days=365), + entity_ids=["sensor.one"], + significant_changes_only=False, + ) + + states = await recorder.get_instance(hass).async_add_executor_job(_get_entries) + sensor_one_states: list[State] = states["sensor.one"] + assert_states_equal_without_context(sensor_one_states[0], state0) + assert_states_equal_without_context(sensor_one_states[1], state1) + assert sensor_one_states[0].last_changed == past_2038_time + assert sensor_one_states[0].last_updated == past_2038_time + + +def test_get_significant_states_without_entity_ids_raises( + hass_recorder: Callable[..., HomeAssistant], +) -> None: + """Test at least one entity id is required for get_significant_states.""" + hass = hass_recorder() + now = dt_util.utcnow() + with pytest.raises(ValueError, match="entity_ids must be provided"): + history.get_significant_states(hass, now, None) + + +def test_state_changes_during_period_without_entity_ids_raises( + hass_recorder: Callable[..., HomeAssistant], +) -> None: + """Test at least one entity id is required for state_changes_during_period.""" + hass = hass_recorder() + now = dt_util.utcnow() + with pytest.raises(ValueError, match="entity_id must be provided"): + history.state_changes_during_period(hass, now, None) + + +def test_get_significant_states_with_filters_raises( + hass_recorder: Callable[..., HomeAssistant], +) -> None: + """Test passing filters is no longer supported.""" + hass = hass_recorder() + now = dt_util.utcnow() + with pytest.raises(NotImplementedError, match="Filters are no longer supported"): + history.get_significant_states( + hass, now, None, ["media_player.test"], Filters() + ) + + +def test_get_significant_states_with_non_existent_entity_ids_returns_empty( + hass_recorder: Callable[..., HomeAssistant], +) -> None: + """Test get_significant_states returns an empty dict when entities not in the db.""" + hass = hass_recorder() + now = dt_util.utcnow() + assert history.get_significant_states(hass, now, None, ["nonexistent.entity"]) == {} + + +def test_state_changes_during_period_with_non_existent_entity_ids_returns_empty( + hass_recorder: Callable[..., HomeAssistant], +) -> None: + """Test state_changes_during_period returns an empty dict when entities not in the db.""" + hass = hass_recorder() + now = dt_util.utcnow() + assert ( + history.state_changes_during_period(hass, now, None, "nonexistent.entity") == {} + ) + + +def test_get_last_state_changes_with_non_existent_entity_ids_returns_empty( + hass_recorder: Callable[..., HomeAssistant], +) -> None: + """Test get_last_state_changes returns an empty dict when entities not in the db.""" + hass = hass_recorder() + assert history.get_last_state_changes(hass, 1, "nonexistent.entity") == {} diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index b1380cb300c..cde2da3cc83 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -1,4 +1,5 @@ """The tests for the Recorder component.""" + from __future__ import annotations import asyncio @@ -101,8 +102,9 @@ from tests.typing import RecorderInstanceGenerator @pytest.fixture def small_cache_size() -> None: """Patch the default cache size to 8.""" - with patch.object(state_attributes_table_manager, "CACHE_SIZE", 8), patch.object( - states_meta_table_manager, "CACHE_SIZE", 8 + with ( + patch.object(state_attributes_table_manager, "CACHE_SIZE", 8), + patch.object(states_meta_table_manager, "CACHE_SIZE", 8), ): yield @@ -141,7 +143,7 @@ async def test_shutdown_before_startup_finishes( hass.set_state(CoreState.not_running) recorder_helper.async_initialize_recorder(hass) - hass.create_task(async_setup_recorder_instance(hass, config)) + hass.async_create_task(async_setup_recorder_instance(hass, config)) await recorder_helper.async_wait_recorder(hass) instance = get_instance(hass) @@ -171,7 +173,7 @@ async def test_canceled_before_startup_finishes( """Test recorder shuts down when its startup future is canceled out from under it.""" hass.set_state(CoreState.not_running) recorder_helper.async_initialize_recorder(hass) - hass.create_task(async_setup_recorder_instance(hass)) + hass.async_create_task(async_setup_recorder_instance(hass)) await recorder_helper.async_wait_recorder(hass) instance = get_instance(hass) @@ -223,7 +225,7 @@ async def test_state_gets_saved_when_set_before_start_event( hass.set_state(CoreState.not_running) recorder_helper.async_initialize_recorder(hass) - hass.create_task(async_setup_recorder_instance(hass)) + hass.async_create_task(async_setup_recorder_instance(hass)) await recorder_helper.async_wait_recorder(hass) entity_id = "test.recorder" @@ -273,11 +275,11 @@ async def test_saving_state(recorder_mock: Recorder, hass: HomeAssistant) -> Non @pytest.mark.parametrize( ("dialect_name", "expected_attributes"), - ( + [ (SupportedDialect.MYSQL, {"test_attr": 5, "test_attr_10": "silly\0stuff"}), (SupportedDialect.POSTGRESQL, {"test_attr": 5, "test_attr_10": "silly"}), (SupportedDialect.SQLITE, {"test_attr": 5, "test_attr_10": "silly\0stuff"}), - ), + ], ) async def test_saving_state_with_nul( recorder_mock: Recorder, hass: HomeAssistant, dialect_name, expected_attributes @@ -325,8 +327,9 @@ async def test_saving_many_states( entity_id = "test.recorder" attributes = {"test_attr": 5, "test_attr_10": "nice"} - with patch.object(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) @@ -385,10 +388,13 @@ def test_saving_state_with_exception( "insert the state", "fake params", "forced to fail" ) - with patch("time.sleep"), patch.object( - get_instance(hass).event_session, - "flush", - side_effect=_throw_if_state_in_session, + with ( + patch("time.sleep"), + patch.object( + get_instance(hass).event_session, + "flush", + side_effect=_throw_if_state_in_session, + ), ): hass.states.set(entity_id, "fail", attributes) wait_recording_done(hass) @@ -427,10 +433,13 @@ def test_saving_state_with_sqlalchemy_exception( "insert the state", "fake params", "forced to fail" ) - with patch("time.sleep"), patch.object( - get_instance(hass).event_session, - "flush", - side_effect=_throw_if_state_in_session, + with ( + patch("time.sleep"), + patch.object( + get_instance(hass).event_session, + "flush", + side_effect=_throw_if_state_in_session, + ), ): hass.states.set(entity_id, "fail", attributes) wait_recording_done(hass) @@ -463,11 +472,14 @@ async def test_force_shutdown_with_queue_of_writes_that_generate_exceptions( await async_wait_recording_done(hass) - with patch.object(instance, "db_retry_wait", 0.01), patch.object( - instance.event_session, - "flush", - side_effect=OperationalError( - "insert the state", "fake params", "forced to fail" + with ( + patch.object(instance, "db_retry_wait", 0.01), + patch.object( + instance.event_session, + "flush", + side_effect=OperationalError( + "insert the state", "fake params", "forced to fail" + ), ), ): for _ in range(100): @@ -542,7 +554,7 @@ def test_saving_state_with_commit_interval_zero( ) -> None: """Test saving a state with a commit interval of zero.""" hass = hass_recorder({"commit_interval": 0}) - get_instance(hass).commit_interval == 0 + assert get_instance(hass).commit_interval == 0 entity_id = "test.recorder" state = "restoring_from_db" @@ -897,8 +909,9 @@ def test_saving_event_invalid_context_ulid( def test_recorder_setup_failure(hass: HomeAssistant) -> None: """Test some exceptions.""" recorder_helper.async_initialize_recorder(hass) - with patch.object(Recorder, "_setup_connection") as setup, patch( - "homeassistant.components.recorder.core.time.sleep" + with ( + patch.object(Recorder, "_setup_connection") as setup, + patch("homeassistant.components.recorder.core.time.sleep"), ): setup.side_effect = ImportError("driver not found") rec = _default_recorder(hass) @@ -912,10 +925,11 @@ def test_recorder_setup_failure(hass: HomeAssistant) -> None: def test_recorder_validate_schema_failure(hass: HomeAssistant) -> None: """Test some exceptions.""" recorder_helper.async_initialize_recorder(hass) - with patch( - "homeassistant.components.recorder.migration._get_schema_version" - ) as inspect_schema_version, patch( - "homeassistant.components.recorder.core.time.sleep" + with ( + patch( + "homeassistant.components.recorder.migration._get_schema_version" + ) as inspect_schema_version, + patch("homeassistant.components.recorder.core.time.sleep"), ): inspect_schema_version.side_effect = ImportError("driver not found") rec = _default_recorder(hass) @@ -929,8 +943,9 @@ def test_recorder_validate_schema_failure(hass: HomeAssistant) -> None: def test_recorder_setup_failure_without_event_listener(hass: HomeAssistant) -> None: """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" + with ( + patch.object(Recorder, "_setup_connection") as setup, + patch("homeassistant.components.recorder.core.time.sleep"), ): setup.side_effect = ImportError("driver not found") rec = _default_recorder(hass) @@ -986,11 +1001,14 @@ def test_auto_purge(hass_recorder: Callable[..., HomeAssistant]) -> None: test_time = datetime(now.year + 2, 1, 1, 4, 15, 0, tzinfo=tz) run_tasks_at_time(hass, test_time) - with patch( - "homeassistant.components.recorder.purge.purge_old_data", return_value=True - ) as purge_old_data, patch( - "homeassistant.components.recorder.tasks.periodic_db_cleanups" - ) as periodic_db_cleanups: + with ( + patch( + "homeassistant.components.recorder.purge.purge_old_data", return_value=True + ) as purge_old_data, + patch( + "homeassistant.components.recorder.tasks.periodic_db_cleanups" + ) as periodic_db_cleanups, + ): # Advance one day, and the purge task should run test_time = test_time + timedelta(days=1) run_tasks_at_time(hass, test_time) @@ -1046,13 +1064,17 @@ def test_auto_purge_auto_repack_on_second_sunday( test_time = datetime(now.year + 2, 1, 1, 4, 15, 0, tzinfo=tz) run_tasks_at_time(hass, test_time) - with patch( - "homeassistant.components.recorder.core.is_second_sunday", return_value=True - ), patch( - "homeassistant.components.recorder.purge.purge_old_data", return_value=True - ) as purge_old_data, patch( - "homeassistant.components.recorder.tasks.periodic_db_cleanups" - ) as periodic_db_cleanups: + with ( + patch( + "homeassistant.components.recorder.core.is_second_sunday", return_value=True + ), + patch( + "homeassistant.components.recorder.purge.purge_old_data", return_value=True + ) as purge_old_data, + patch( + "homeassistant.components.recorder.tasks.periodic_db_cleanups" + ) as periodic_db_cleanups, + ): # Advance one day, and the purge task should run test_time = test_time + timedelta(days=1) run_tasks_at_time(hass, test_time) @@ -1086,13 +1108,17 @@ def test_auto_purge_auto_repack_disabled_on_second_sunday( test_time = datetime(now.year + 2, 1, 1, 4, 15, 0, tzinfo=tz) run_tasks_at_time(hass, test_time) - with patch( - "homeassistant.components.recorder.core.is_second_sunday", return_value=True - ), patch( - "homeassistant.components.recorder.purge.purge_old_data", return_value=True - ) as purge_old_data, patch( - "homeassistant.components.recorder.tasks.periodic_db_cleanups" - ) as periodic_db_cleanups: + with ( + patch( + "homeassistant.components.recorder.core.is_second_sunday", return_value=True + ), + patch( + "homeassistant.components.recorder.purge.purge_old_data", return_value=True + ) as purge_old_data, + patch( + "homeassistant.components.recorder.tasks.periodic_db_cleanups" + ) as periodic_db_cleanups, + ): # Advance one day, and the purge task should run test_time = test_time + timedelta(days=1) run_tasks_at_time(hass, test_time) @@ -1126,14 +1152,18 @@ def test_auto_purge_no_auto_repack_on_not_second_sunday( test_time = datetime(now.year + 2, 1, 1, 4, 15, 0, tzinfo=tz) run_tasks_at_time(hass, test_time) - with patch( - "homeassistant.components.recorder.core.is_second_sunday", - return_value=False, - ), patch( - "homeassistant.components.recorder.purge.purge_old_data", return_value=True - ) as purge_old_data, patch( - "homeassistant.components.recorder.tasks.periodic_db_cleanups" - ) as periodic_db_cleanups: + with ( + patch( + "homeassistant.components.recorder.core.is_second_sunday", + return_value=False, + ), + patch( + "homeassistant.components.recorder.purge.purge_old_data", return_value=True + ) as purge_old_data, + patch( + "homeassistant.components.recorder.tasks.periodic_db_cleanups" + ) as periodic_db_cleanups, + ): # Advance one day, and the purge task should run test_time = test_time + timedelta(days=1) run_tasks_at_time(hass, test_time) @@ -1164,11 +1194,14 @@ def test_auto_purge_disabled(hass_recorder: Callable[..., HomeAssistant]) -> Non test_time = datetime(now.year + 2, 1, 1, 4, 15, 0, tzinfo=tz) run_tasks_at_time(hass, test_time) - with patch( - "homeassistant.components.recorder.purge.purge_old_data", return_value=True - ) as purge_old_data, patch( - "homeassistant.components.recorder.tasks.periodic_db_cleanups" - ) as periodic_db_cleanups: + with ( + patch( + "homeassistant.components.recorder.purge.purge_old_data", return_value=True + ) as purge_old_data, + patch( + "homeassistant.components.recorder.tasks.periodic_db_cleanups" + ) as periodic_db_cleanups, + ): # Advance one day, and the purge task should run test_time = test_time + timedelta(days=1) run_tasks_at_time(hass, test_time) @@ -1809,9 +1842,11 @@ async def test_database_lock_and_overflow( ) ) - with patch.object(recorder.core, "MAX_QUEUE_BACKLOG_MIN_VALUE", 1), patch.object( - recorder.core, "DB_LOCK_QUEUE_CHECK_TIMEOUT", 0.01 - ), patch.object(recorder.core, "QUEUE_PERCENTAGE_ALLOWED_AVAILABLE_MEMORY", 0): + with ( + patch.object(recorder.core, "MAX_QUEUE_BACKLOG_MIN_VALUE", 1), + patch.object(recorder.core, "DB_LOCK_QUEUE_CHECK_TIMEOUT", 0.01), + patch.object(recorder.core, "QUEUE_PERCENTAGE_ALLOWED_AVAILABLE_MEMORY", 0), + ): await async_setup_recorder_instance(hass, config) await hass.async_block_till_done() event_type = "EVENT_TEST" @@ -1878,12 +1913,15 @@ async def test_database_lock_and_overflow_checks_available_memory( event_types = (event_type,) await async_wait_recording_done(hass) - with patch.object(recorder.core, "MAX_QUEUE_BACKLOG_MIN_VALUE", 1), patch.object( - recorder.core, "QUEUE_PERCENTAGE_ALLOWED_AVAILABLE_MEMORY", 1 - ), patch.object(recorder.core, "DB_LOCK_QUEUE_CHECK_TIMEOUT", 0.01), patch.object( - recorder.core.Recorder, - "_available_memory", - return_value=recorder.core.ESTIMATED_QUEUE_ITEM_SIZE * 4, + with ( + patch.object(recorder.core, "MAX_QUEUE_BACKLOG_MIN_VALUE", 1), + patch.object(recorder.core, "QUEUE_PERCENTAGE_ALLOWED_AVAILABLE_MEMORY", 1), + patch.object(recorder.core, "DB_LOCK_QUEUE_CHECK_TIMEOUT", 0.01), + patch.object( + recorder.core.Recorder, + "_available_memory", + return_value=recorder.core.ESTIMATED_QUEUE_ITEM_SIZE * 4, + ), ): instance = get_instance(hass) @@ -2066,7 +2104,7 @@ def test_deduplication_state_attributes_inside_commit_interval( hass.states.set(entity_id, "on", attributes) hass.states.set(entity_id, "off", attributes) - # Now exaust the cache to ensure we go back to the db + # Now exhaust the cache to ensure we go back to the db for attr_id in range(5): hass.states.set(entity_id, "on", {"test_attr": attr_id}) hass.states.set(entity_id, "off", {"test_attr": attr_id}) @@ -2115,14 +2153,14 @@ async def test_async_block_till_done( @pytest.mark.parametrize( ("db_url", "echo"), - ( + [ ("sqlite://blabla", None), ("mariadb://blabla", False), ("mysql://blabla", False), ("mariadb+pymysql://blabla", False), ("mysql+pymysql://blabla", False), ("postgresql://blabla", False), - ), + ], ) async def test_disable_echo( hass: HomeAssistant, db_url, echo, caplog: pytest.LogCaptureFixture @@ -2135,10 +2173,11 @@ async def test_disable_echo( callback(None, None) mock_event = MockEvent() - with patch( - "homeassistant.components.recorder.core.create_engine" - ) as create_engine_mock, patch( - "homeassistant.components.recorder.core.sqlalchemy_event", mock_event + with ( + patch( + "homeassistant.components.recorder.core.create_engine" + ) as create_engine_mock, + patch("homeassistant.components.recorder.core.sqlalchemy_event", mock_event), ): await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_DB_URL: db_url}}) create_engine_mock.assert_called_once() @@ -2147,7 +2186,7 @@ async def test_disable_echo( @pytest.mark.parametrize( ("config_url", "expected_connect_args"), - ( + [ ( "mariadb://user:password@SERVER_IP/DB_NAME", {"charset": "utf8mb4"}, @@ -2180,7 +2219,7 @@ async def test_disable_echo( "sqlite://blabla", {}, ), - ), + ], ) async def test_mysql_missing_utf8mb4( hass: HomeAssistant, config_url, expected_connect_args @@ -2193,10 +2232,11 @@ async def test_mysql_missing_utf8mb4( callback(None, None) mock_event = MockEvent() - with patch( - "homeassistant.components.recorder.core.create_engine" - ) as create_engine_mock, patch( - "homeassistant.components.recorder.core.sqlalchemy_event", mock_event + with ( + patch( + "homeassistant.components.recorder.core.create_engine" + ) as create_engine_mock, + patch("homeassistant.components.recorder.core.sqlalchemy_event", mock_event), ): await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_DB_URL: config_url}}) create_engine_mock.assert_called_once() @@ -2208,11 +2248,11 @@ async def test_mysql_missing_utf8mb4( @pytest.mark.parametrize( "config_url", - ( + [ "mysql://user:password@SERVER_IP/DB_NAME", "mysql://user:password@SERVER_IP/DB_NAME?charset=utf8mb4", "mysql://user:password@SERVER_IP/DB_NAME?blah=bleh&charset=other", - ), + ], ) async def test_connect_args_priority(hass: HomeAssistant, config_url) -> None: """Test connect_args has priority over URL query.""" @@ -2225,8 +2265,11 @@ async def test_connect_args_priority(hass: HomeAssistant, config_url) -> None: __bases__ = [] _has_events = False - def __init__(*args, **kwargs): - ... + def __init__(*args, **kwargs): ... + + @property + def is_async(self): + return False def connect(self, *args, **params): nonlocal connect_params @@ -2241,33 +2284,29 @@ async def test_connect_args_priority(hass: HomeAssistant, config_url) -> None: return "mysql" @classmethod - def import_dbapi(cls): - ... + def import_dbapi(cls): ... - def engine_created(*args): - ... + def engine_created(*args): ... def get_dialect_pool_class(self, *args): return pool.RecorderPool - def initialize(*args): - ... + def initialize(*args): ... def on_connect_url(self, url): return False - def _builtin_onconnect(self): - ... + def _builtin_onconnect(self): ... class MockEntrypoint: - def engine_created(*_): - ... + def engine_created(*_): ... def get_dialect_cls(*_): return MockDialect - with patch("sqlalchemy.engine.url.URL._get_entrypoint", MockEntrypoint), patch( - "sqlalchemy.engine.create.util.get_cls_kwargs", return_value=["echo"] + with ( + patch("sqlalchemy.engine.url.URL._get_entrypoint", MockEntrypoint), + patch("sqlalchemy.engine.create.util.get_cls_kwargs", return_value=["echo"]), ): await async_setup_component( hass, @@ -2358,8 +2397,9 @@ async def test_clean_shutdown_when_recorder_thread_raises_during_initialize_data hass: HomeAssistant, ) -> None: """Test we still shutdown cleanly when the recorder thread raises during initialize_database.""" - with patch.object(migration, "initialize_database", side_effect=Exception), patch( - "homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", True + with ( + patch.object(migration, "initialize_database", side_effect=Exception), + patch("homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", True), ): if recorder.DOMAIN not in hass.data: recorder_helper.async_initialize_recorder(hass) @@ -2385,8 +2425,9 @@ async def test_clean_shutdown_when_recorder_thread_raises_during_validate_db_sch hass: HomeAssistant, ) -> None: """Test we still shutdown cleanly when the recorder thread raises during validate_db_schema.""" - with patch.object(migration, "validate_db_schema", side_effect=Exception), patch( - "homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", True + with ( + patch.object(migration, "validate_db_schema", side_effect=Exception), + patch("homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", True), ): if recorder.DOMAIN not in hass.data: recorder_helper.async_initialize_recorder(hass) @@ -2410,16 +2451,18 @@ async def test_clean_shutdown_when_recorder_thread_raises_during_validate_db_sch async def test_clean_shutdown_when_schema_migration_fails(hass: HomeAssistant) -> None: """Test we still shutdown cleanly when schema migration fails.""" - with patch.object( - migration, - "validate_db_schema", - return_value=MagicMock(valid=False, current_version=1), - ), patch( - "homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", True - ), patch.object( - migration, - "migrate_schema", - side_effect=Exception, + with ( + patch.object( + migration, + "validate_db_schema", + return_value=MagicMock(valid=False, current_version=1), + ), + patch("homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", True), + patch.object( + migration, + "migrate_schema", + side_effect=Exception, + ), ): if recorder.DOMAIN not in hass.data: recorder_helper.async_initialize_recorder(hass) @@ -2504,7 +2547,7 @@ async def test_commit_before_commits_pending_writes( } recorder_helper.async_initialize_recorder(hass) - hass.create_task(async_setup_recorder_instance(hass, config)) + hass.async_create_task(async_setup_recorder_instance(hass, config)) await recorder_helper.async_wait_recorder(hass) instance = get_instance(hass) assert instance.commit_interval == 60 diff --git a/tests/components/recorder/test_migrate.py b/tests/components/recorder/test_migrate.py index 600570acf66..01d5912a683 100644 --- a/tests/components/recorder/test_migrate.py +++ b/tests/components/recorder/test_migrate.py @@ -52,13 +52,17 @@ async def test_schema_update_calls(recorder_db_url: str, hass: HomeAssistant) -> """Test that schema migrations occur in correct order.""" assert recorder.util.async_migration_in_progress(hass) is False - with patch("homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", True), patch( - "homeassistant.components.recorder.core.create_engine", - new=create_engine_test, - ), patch( - "homeassistant.components.recorder.migration._apply_update", - wraps=migration._apply_update, - ) as update: + with ( + patch("homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", True), + patch( + "homeassistant.components.recorder.core.create_engine", + new=create_engine_test, + ), + patch( + "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": recorder_db_url}} @@ -72,7 +76,7 @@ async def test_schema_update_calls(recorder_db_url: str, hass: HomeAssistant) -> update.assert_has_calls( [ call(instance, hass, engine, session_maker, version + 1, 0) - for version in range(0, db_schema.SCHEMA_VERSION) + for version in range(db_schema.SCHEMA_VERSION) ] ) @@ -89,9 +93,12 @@ async def test_migration_in_progress(recorder_db_url: str, hass: HomeAssistant) assert recorder.util.async_migration_in_progress(hass) is False - with patch("homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", True), patch( - "homeassistant.components.recorder.core.create_engine", - new=create_engine_test, + 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( @@ -111,18 +118,25 @@ async def test_database_migration_failed( """Test we notify if the migration fails.""" assert recorder.util.async_migration_in_progress(hass) is False - with patch("homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", True), patch( - "homeassistant.components.recorder.core.create_engine", - new=create_engine_test, - ), patch( - "homeassistant.components.recorder.migration._apply_update", - side_effect=ValueError, - ), patch( - "homeassistant.components.persistent_notification.create", side_effect=pn.create - ) as mock_create, patch( - "homeassistant.components.persistent_notification.dismiss", - side_effect=pn.dismiss, - ) as mock_dismiss: + with ( + patch("homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", True), + patch( + "homeassistant.components.recorder.core.create_engine", + new=create_engine_test, + ), + patch( + "homeassistant.components.recorder.migration._apply_update", + side_effect=ValueError, + ), + patch( + "homeassistant.components.persistent_notification.create", + side_effect=pn.create, + ) as mock_create, + patch( + "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": recorder_db_url}} @@ -152,16 +166,22 @@ async def test_database_migration_encounters_corruption( sqlite3_exception = DatabaseError("statement", {}, []) sqlite3_exception.__cause__ = sqlite3.DatabaseError() - with patch("homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", True), patch( - "homeassistant.components.recorder.migration._schema_is_current", - side_effect=[False], - ), patch( - "homeassistant.components.recorder.migration.migrate_schema", - side_effect=sqlite3_exception, - ), patch( - "homeassistant.components.recorder.core.move_away_broken_database" - ) as move_away, patch( - "homeassistant.components.recorder.Recorder._schedule_compile_missing_statistics", + with ( + patch("homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", True), + patch( + "homeassistant.components.recorder.migration._schema_is_current", + side_effect=[False], + ), + patch( + "homeassistant.components.recorder.migration.migrate_schema", + side_effect=sqlite3_exception, + ), + patch( + "homeassistant.components.recorder.core.move_away_broken_database" + ) as move_away, + patch( + "homeassistant.components.recorder.Recorder._schedule_compile_missing_statistics", + ), ): recorder_helper.async_initialize_recorder(hass) await async_setup_component( @@ -181,20 +201,28 @@ async def test_database_migration_encounters_corruption_not_sqlite( """Test we fail on database error when we cannot recover.""" assert recorder.util.async_migration_in_progress(hass) is False - with patch("homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", True), patch( - "homeassistant.components.recorder.migration._schema_is_current", - side_effect=[False], - ), patch( - "homeassistant.components.recorder.migration.migrate_schema", - side_effect=DatabaseError("statement", {}, []), - ), patch( - "homeassistant.components.recorder.core.move_away_broken_database" - ) as move_away, patch( - "homeassistant.components.persistent_notification.create", side_effect=pn.create - ) as mock_create, patch( - "homeassistant.components.persistent_notification.dismiss", - side_effect=pn.dismiss, - ) as mock_dismiss: + with ( + patch("homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", True), + patch( + "homeassistant.components.recorder.migration._schema_is_current", + side_effect=[False], + ), + patch( + "homeassistant.components.recorder.migration.migrate_schema", + side_effect=DatabaseError("statement", {}, []), + ), + patch( + "homeassistant.components.recorder.core.move_away_broken_database" + ) as move_away, + patch( + "homeassistant.components.persistent_notification.create", + side_effect=pn.create, + ) as mock_create, + patch( + "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": recorder_db_url}} @@ -218,12 +246,15 @@ async def test_events_during_migration_are_queued( assert recorder.util.async_migration_in_progress(hass) is False - with patch( - "homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", - True, - ), patch( - "homeassistant.components.recorder.core.create_engine", - new=create_engine_test, + 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( @@ -254,11 +285,14 @@ async def test_events_during_migration_queue_exhausted( assert recorder.util.async_migration_in_progress(hass) is False - with patch("homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", True), patch( - "homeassistant.components.recorder.core.create_engine", - new=create_engine_test, - ), patch.object(recorder.core, "MAX_QUEUE_BACKLOG_MIN_VALUE", 1), patch.object( - recorder.core, "QUEUE_PERCENTAGE_ALLOWED_AVAILABLE_MEMORY", 0 + with ( + patch("homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", True), + patch( + "homeassistant.components.recorder.core.create_engine", + new=create_engine_test, + ), + patch.object(recorder.core, "MAX_QUEUE_BACKLOG_MIN_VALUE", 1), + patch.object(recorder.core, "QUEUE_PERCENTAGE_ALLOWED_AVAILABLE_MEMORY", 0), ): recorder_helper.async_initialize_recorder(hass) await async_setup_component( @@ -372,32 +406,42 @@ async def test_schema_migrate( raise mysql_exception real_create_index(*args) - with patch("homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", True), patch( - "homeassistant.components.recorder.core.create_engine", - new=_create_engine_test, - ), patch( - "homeassistant.components.recorder.Recorder._setup_run", - side_effect=_mock_setup_run, - autospec=True, - ) as setup_run, patch( - "homeassistant.components.recorder.migration.migrate_schema", - wraps=_instrument_migrate_schema, - ), patch( - "homeassistant.components.recorder.migration._apply_update", - wraps=_instrument_apply_update, - ) as apply_update_mock, patch( - "homeassistant.components.recorder.util.time.sleep" - ), patch( - "homeassistant.components.recorder.migration._create_index", - wraps=_sometimes_failing_create_index, - ), patch( - "homeassistant.components.recorder.Recorder._schedule_compile_missing_statistics", - ), patch( - "homeassistant.components.recorder.Recorder._process_state_changed_event_into_session", - ), patch( - "homeassistant.components.recorder.Recorder._process_non_state_changed_event_into_session", - ), patch( - "homeassistant.components.recorder.Recorder._pre_process_startup_events", + with ( + patch("homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", True), + patch( + "homeassistant.components.recorder.core.create_engine", + new=_create_engine_test, + ), + patch( + "homeassistant.components.recorder.Recorder._setup_run", + side_effect=_mock_setup_run, + autospec=True, + ) as setup_run, + patch( + "homeassistant.components.recorder.migration.migrate_schema", + wraps=_instrument_migrate_schema, + ), + patch( + "homeassistant.components.recorder.migration._apply_update", + wraps=_instrument_apply_update, + ) as apply_update_mock, + patch("homeassistant.components.recorder.util.time.sleep"), + patch( + "homeassistant.components.recorder.migration._create_index", + wraps=_sometimes_failing_create_index, + ), + patch( + "homeassistant.components.recorder.Recorder._schedule_compile_missing_statistics", + ), + patch( + "homeassistant.components.recorder.Recorder._process_state_changed_event_into_session", + ), + patch( + "homeassistant.components.recorder.Recorder._process_non_state_changed_event_into_session", + ), + patch( + "homeassistant.components.recorder.Recorder._pre_process_startup_events", + ), ): recorder_helper.async_initialize_recorder(hass) hass.async_create_task( @@ -497,11 +541,14 @@ def test_forgiving_drop_index( instance.get_session, "states", "ix_states_context_id_bin" ) - with patch( - "homeassistant.components.recorder.migration.get_index_by_name", - return_value="ix_states_context_id_bin", - ), patch.object( - session, "connection", side_effect=SQLAlchemyError("connection failure") + with ( + patch( + "homeassistant.components.recorder.migration.get_index_by_name", + return_value="ix_states_context_id_bin", + ), + patch.object( + session, "connection", side_effect=SQLAlchemyError("connection failure") + ), ): migration._drop_index( instance.get_session, "states", "ix_states_context_id_bin" @@ -509,11 +556,14 @@ def test_forgiving_drop_index( assert "Failed to drop index" in caplog.text assert "connection failure" in caplog.text caplog.clear() - with patch( - "homeassistant.components.recorder.migration.get_index_by_name", - return_value="ix_states_context_id_bin", - ), patch.object( - session, "connection", side_effect=SQLAlchemyError("connection failure") + with ( + patch( + "homeassistant.components.recorder.migration.get_index_by_name", + return_value="ix_states_context_id_bin", + ), + patch.object( + session, "connection", side_effect=SQLAlchemyError("connection failure") + ), ): migration._drop_index( instance.get_session, "states", "ix_states_context_id_bin", quiet=True diff --git a/tests/components/recorder/test_migration_from_schema_32.py b/tests/components/recorder/test_migration_from_schema_32.py index 2849c0703da..646cd338949 100644 --- a/tests/components/recorder/test_migration_from_schema_32.py +++ b/tests/components/recorder/test_migration_from_schema_32.py @@ -1,4 +1,5 @@ """The tests for the recorder filter matching the EntityFilter component.""" + import datetime import importlib import sys @@ -21,7 +22,10 @@ from homeassistant.components.recorder.db_schema import ( StatesMeta, ) from homeassistant.components.recorder.models import process_timestamp -from homeassistant.components.recorder.queries import select_event_type_ids +from homeassistant.components.recorder.queries import ( + get_migration_changes, + select_event_type_ids, +) from homeassistant.components.recorder.tasks import ( EntityIDMigrationTask, EntityIDPostMigrationTask, @@ -29,7 +33,10 @@ from homeassistant.components.recorder.tasks import ( EventTypeIDMigrationTask, StatesContextIDMigrationTask, ) -from homeassistant.components.recorder.util import session_scope +from homeassistant.components.recorder.util import ( + execute_stmt_lambda_element, + session_scope, +) from homeassistant.core import HomeAssistant import homeassistant.util.dt as dt_util from homeassistant.util.ulid import bytes_to_ulid, ulid_at_time, ulid_to_bytes @@ -52,6 +59,11 @@ async def _async_wait_migration_done(hass: HomeAssistant) -> None: await async_recorder_block_till_done(hass) +def _get_migration_id(hass: HomeAssistant) -> dict[str, int]: + with session_scope(hass=hass, read_only=True) as session: + return dict(execute_stmt_lambda_element(session, get_migration_changes())) + + def _create_engine_test(*args, **kwargs): """Test version of create_engine that initializes with old schema. @@ -80,16 +92,19 @@ def db_schema_32(): importlib.import_module(SCHEMA_MODULE) old_db_schema = sys.modules[SCHEMA_MODULE] - with patch.object(recorder, "db_schema", old_db_schema), patch.object( - recorder.migration, "SCHEMA_VERSION", old_db_schema.SCHEMA_VERSION - ), patch.object(core, "StatesMeta", old_db_schema.StatesMeta), patch.object( - core, "EventTypes", old_db_schema.EventTypes - ), patch.object(core, "EventData", old_db_schema.EventData), patch.object( - core, "States", old_db_schema.States - ), patch.object(core, "Events", old_db_schema.Events), patch.object( - core, "StateAttributes", old_db_schema.StateAttributes - ), patch.object(core, "EntityIDMigrationTask", core.RecorderTask), patch( - CREATE_ENGINE_TARGET, new=_create_engine_test + with ( + patch.object(recorder, "db_schema", old_db_schema), + patch.object( + recorder.migration, "SCHEMA_VERSION", old_db_schema.SCHEMA_VERSION + ), + patch.object(core, "StatesMeta", old_db_schema.StatesMeta), + patch.object(core, "EventTypes", old_db_schema.EventTypes), + patch.object(core, "EventData", old_db_schema.EventData), + patch.object(core, "States", old_db_schema.States), + patch.object(core, "Events", old_db_schema.Events), + patch.object(core, "StateAttributes", old_db_schema.StateAttributes), + patch.object(migration.EntityIDMigration, "task", core.RecorderTask), + patch(CREATE_ENGINE_TARGET, new=_create_engine_test), ): yield @@ -307,6 +322,12 @@ async def test_migrate_events_context_ids( event_with_garbage_context_id_no_time_fired_ts["context_parent_id_bin"] is None ) + migration_changes = await instance.async_add_executor_job(_get_migration_id, hass) + assert ( + migration_changes[migration.EventsContextIDMigration.migration_id] + == migration.EventsContextIDMigration.migration_version + ) + @pytest.mark.parametrize("enable_migrate_context_ids", [True]) async def test_migrate_states_context_ids( @@ -494,6 +515,12 @@ async def test_migrate_states_context_ids( == b"\n\xe2\x97\x99\xeeNOE\x81\x16\xf5\x82\xd7\xd3\xeee" ) + migration_changes = await instance.async_add_executor_job(_get_migration_id, hass) + assert ( + migration_changes[migration.StatesContextIDMigration.migration_id] + == migration.StatesContextIDMigration.migration_version + ) + @pytest.mark.parametrize("enable_migrate_event_type_ids", [True]) async def test_migrate_event_type_ids( @@ -577,6 +604,12 @@ async def test_migrate_event_type_ids( assert mapped["event_type_one"] is not None assert mapped["event_type_two"] is not None + migration_changes = await instance.async_add_executor_job(_get_migration_id, hass) + assert ( + migration_changes[migration.EventTypeIDMigration.migration_id] + == migration.EventTypeIDMigration.migration_version + ) + @pytest.mark.parametrize("enable_migrate_entity_ids", [True]) async def test_migrate_entity_ids( @@ -645,6 +678,12 @@ async def test_migrate_entity_ids( assert len(states_by_entity_id["sensor.two"]) == 2 assert len(states_by_entity_id["sensor.one"]) == 1 + migration_changes = await instance.async_add_executor_job(_get_migration_id, hass) + assert ( + migration_changes[migration.EntityIDMigration.migration_id] + == migration.EntityIDMigration.migration_version + ) + @pytest.mark.parametrize("enable_migrate_entity_ids", [True]) async def test_post_migrate_entity_ids( @@ -770,6 +809,16 @@ async def test_migrate_null_entity_ids( assert len(states_by_entity_id[migration._EMPTY_ENTITY_ID]) == 1000 assert len(states_by_entity_id["sensor.one"]) == 2 + def _get_migration_id(): + with session_scope(hass=hass, read_only=True) as session: + return dict(execute_stmt_lambda_element(session, get_migration_changes())) + + migration_changes = await instance.async_add_executor_job(_get_migration_id) + assert ( + migration_changes[migration.EntityIDMigration.migration_id] + == migration.EntityIDMigration.migration_version + ) + @pytest.mark.parametrize("enable_migrate_event_type_ids", [True]) async def test_migrate_null_event_type_ids( @@ -846,6 +895,16 @@ async def test_migrate_null_event_type_ids( assert len(events_by_type["event_type_one"]) == 2 assert len(events_by_type[migration._EMPTY_EVENT_TYPE]) == 1000 + def _get_migration_id(): + with session_scope(hass=hass, read_only=True) as session: + return dict(execute_stmt_lambda_element(session, get_migration_changes())) + + migration_changes = await instance.async_add_executor_job(_get_migration_id) + assert ( + migration_changes[migration.EventTypeIDMigration.migration_id] + == migration.EventTypeIDMigration.migration_version + ) + async def test_stats_timestamp_conversion_is_reentrant( async_setup_recorder_instance: RecorderInstanceGenerator, @@ -913,18 +972,17 @@ async def test_stats_timestamp_conversion_is_reentrant( def _get_all_short_term_stats() -> list[dict[str, Any]]: with session_scope(hass=hass) as session: - results = [] - for result in ( - session.query(old_db_schema.StatisticsShortTerm) - .where(old_db_schema.StatisticsShortTerm.metadata_id == 1000) - .all() - ): - results.append( - { - field.name: getattr(result, field.name) - for field in old_db_schema.StatisticsShortTerm.__table__.c - } + results = [ + { + field.name: getattr(result, field.name) + for field in old_db_schema.StatisticsShortTerm.__table__.c + } + for result in ( + session.query(old_db_schema.StatisticsShortTerm) + .where(old_db_schema.StatisticsShortTerm.metadata_id == 1000) + .all() ) + ] return sorted(results, key=lambda row: row["start_ts"]) # Do not optimize this block, its intentionally written to interleave @@ -1098,14 +1156,12 @@ async def test_stats_timestamp_with_one_by_one( def _get_all_stats(table: old_db_schema.StatisticsBase) -> list[dict[str, Any]]: """Get all stats from a table.""" with session_scope(hass=hass) as session: - results = [] - for result in session.query(table).where(table.metadata_id == 1000).all(): - results.append( - { - field.name: getattr(result, field.name) - for field in table.__table__.c - } - ) + results = [ + {field.name: getattr(result, field.name) for field in table.__table__.c} + for result in session.query(table) + .where(table.metadata_id == 1000) + .all() + ] return sorted(results, key=lambda row: row["start_ts"]) def _insert_and_do_migration(): @@ -1241,14 +1297,17 @@ async def test_stats_timestamp_with_one_by_one_removes_duplicates( one_month_ago = now - datetime.timedelta(days=30) def _do_migration(): - with patch.object( - migration, - "_migrate_statistics_columns_to_timestamp", - side_effect=IntegrityError("test", "test", "test"), - ), patch.object( - migration, - "migrate_single_statistics_row_to_timestamp", - side_effect=IntegrityError("test", "test", "test"), + with ( + patch.object( + migration, + "_migrate_statistics_columns_to_timestamp", + side_effect=IntegrityError("test", "test", "test"), + ), + patch.object( + migration, + "migrate_single_statistics_row_to_timestamp", + side_effect=IntegrityError("test", "test", "test"), + ), ): migration._migrate_statistics_columns_to_timestamp_removing_duplicates( hass, instance, instance.get_session, instance.engine @@ -1325,14 +1384,12 @@ async def test_stats_timestamp_with_one_by_one_removes_duplicates( def _get_all_stats(table: old_db_schema.StatisticsBase) -> list[dict[str, Any]]: """Get all stats from a table.""" with session_scope(hass=hass) as session: - results = [] - for result in session.query(table).where(table.metadata_id == 1000).all(): - results.append( - { - field.name: getattr(result, field.name) - for field in table.__table__.c - } - ) + results = [ + {field.name: getattr(result, field.name) for field in table.__table__.c} + for result in session.query(table) + .where(table.metadata_id == 1000) + .all() + ] return sorted(results, key=lambda row: row["start_ts"]) def _insert_and_do_migration(): diff --git a/tests/components/recorder/test_migration_run_time_migrations_remember.py b/tests/components/recorder/test_migration_run_time_migrations_remember.py new file mode 100644 index 00000000000..4f59edb097f --- /dev/null +++ b/tests/components/recorder/test_migration_run_time_migrations_remember.py @@ -0,0 +1,170 @@ +"""Test run time migrations are remembered in the migration_changes table.""" + +import importlib +from pathlib import Path +import sys +from unittest.mock import patch + +import pytest +from sqlalchemy import create_engine +from sqlalchemy.orm import Session + +from homeassistant.components import recorder +from homeassistant.components.recorder import core, migration, statistics +from homeassistant.components.recorder.queries import get_migration_changes +from homeassistant.components.recorder.tasks import StatesContextIDMigrationTask +from homeassistant.components.recorder.util import ( + execute_stmt_lambda_element, + session_scope, +) +from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.core import HomeAssistant + +from .common import async_recorder_block_till_done, async_wait_recording_done + +from tests.common import async_test_home_assistant +from tests.typing import RecorderInstanceGenerator + +CREATE_ENGINE_TARGET = "homeassistant.components.recorder.core.create_engine" +SCHEMA_MODULE = "tests.components.recorder.db_schema_32" + + +async def _async_wait_migration_done(hass: HomeAssistant) -> None: + """Wait for the migration to be done.""" + await recorder.get_instance(hass).async_block_till_done() + await async_recorder_block_till_done(hass) + + +def _get_migration_id(hass: HomeAssistant) -> dict[str, int]: + with session_scope(hass=hass, read_only=True) as session: + return dict(execute_stmt_lambda_element(session, get_migration_changes())) + + +def _create_engine_test(*args, **kwargs): + """Test version of create_engine that initializes with old schema. + + This simulates an existing db with the old schema. + """ + importlib.import_module(SCHEMA_MODULE) + old_db_schema = sys.modules[SCHEMA_MODULE] + engine = create_engine(*args, **kwargs) + old_db_schema.Base.metadata.create_all(engine) + with Session(engine) as session: + session.add( + recorder.db_schema.StatisticsRuns(start=statistics.get_start_time()) + ) + session.add( + recorder.db_schema.SchemaChanges( + schema_version=old_db_schema.SCHEMA_VERSION + ) + ) + session.commit() + return engine + + +@pytest.mark.parametrize("enable_migrate_context_ids", [True]) +async def test_migration_changes_prevent_trying_to_migrate_again( + async_setup_recorder_instance: RecorderInstanceGenerator, + tmp_path: Path, + recorder_db_url: str, +) -> None: + """Test that we do not try to migrate when migration_changes indicate its already migrated. + + This test will start Home Assistant 3 times: + + 1. With schema 32 to populate the data + 2. With current schema so the migration happens + 3. With current schema to verify we do not have to query to see if the migration is done + """ + if recorder_db_url.startswith(("mysql://", "postgresql://")): + # This test uses a test database between runs so its + # SQLite specific + return + + config = { + recorder.CONF_DB_URL: "sqlite:///" + str(tmp_path / "pytest.db"), + recorder.CONF_COMMIT_INTERVAL: 1, + } + importlib.import_module(SCHEMA_MODULE) + old_db_schema = sys.modules[SCHEMA_MODULE] + + # Start with db schema that needs migration (version 32) + with ( + patch.object(recorder, "db_schema", old_db_schema), + patch.object( + recorder.migration, "SCHEMA_VERSION", old_db_schema.SCHEMA_VERSION + ), + patch.object(core, "StatesMeta", old_db_schema.StatesMeta), + patch.object(core, "EventTypes", old_db_schema.EventTypes), + patch.object(core, "EventData", old_db_schema.EventData), + patch.object(core, "States", old_db_schema.States), + patch.object(core, "Events", old_db_schema.Events), + patch.object(core, "StateAttributes", old_db_schema.StateAttributes), + patch.object(migration.EntityIDMigration, "task", core.RecorderTask), + patch(CREATE_ENGINE_TARGET, new=_create_engine_test), + ): + async with async_test_home_assistant() as hass: + await async_setup_recorder_instance(hass, config) + await hass.async_block_till_done() + await async_wait_recording_done(hass) + await _async_wait_migration_done(hass) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + await hass.async_stop() + + # Now start again with current db schema + async with async_test_home_assistant() as hass: + await async_setup_recorder_instance(hass, config) + await hass.async_block_till_done() + await async_wait_recording_done(hass) + await _async_wait_migration_done(hass) + instance = recorder.get_instance(hass) + migration_changes = await instance.async_add_executor_job( + _get_migration_id, hass + ) + assert ( + migration_changes[migration.StatesContextIDMigration.migration_id] + == migration.StatesContextIDMigration.migration_version + ) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + await hass.async_stop() + + original_queue_task = core.Recorder.queue_task + tasks = [] + + def _queue_task(self, task): + tasks.append(task) + original_queue_task(self, task) + + # Finally verify we did not call needs_migrate_query on StatesContextIDMigration + async with async_test_home_assistant() as hass: + with ( + patch( + "homeassistant.components.recorder.core.Recorder.queue_task", + _queue_task, + ), + patch.object( + migration.StatesContextIDMigration, + "needs_migrate_query", + side_effect=RuntimeError("Should not be called"), + ), + ): + await async_setup_recorder_instance(hass, config) + await hass.async_block_till_done() + await async_wait_recording_done(hass) + await _async_wait_migration_done(hass) + instance = recorder.get_instance(hass) + migration_changes = await instance.async_add_executor_job( + _get_migration_id, hass + ) + assert ( + migration_changes[migration.StatesContextIDMigration.migration_id] + == migration.StatesContextIDMigration.migration_version + ) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + await hass.async_stop() + + for task in tasks: + assert not isinstance(task, StatesContextIDMigrationTask) diff --git a/tests/components/recorder/test_models.py b/tests/components/recorder/test_models.py index 639efd0678d..262fb48af4d 100644 --- a/tests/components/recorder/test_models.py +++ b/tests/components/recorder/test_models.py @@ -1,4 +1,5 @@ """The tests for the Recorder component.""" + from datetime import datetime, timedelta from unittest.mock import PropertyMock @@ -97,7 +98,7 @@ def test_repr() -> None: EVENT_STATE_CHANGED, {"entity_id": "sensor.temperature", "old_state": None, "new_state": state}, context=state.context, - time_fired=fixed_time, + time_fired_timestamp=fixed_time.timestamp(), ) assert "2016-07-09 11:00:00+00:00" in repr(States.from_event(event)) assert "2016-07-09 11:00:00+00:00" in repr(Events.from_event(event)) @@ -163,7 +164,7 @@ def test_from_event_to_delete_state() -> None: assert db_state.entity_id == "sensor.temperature" assert db_state.state == "" assert db_state.last_changed_ts is None - assert db_state.last_updated_ts == event.time_fired.timestamp() + assert db_state.last_updated_ts == pytest.approx(event.time_fired.timestamp()) def test_states_from_native_invalid_entity_id() -> None: @@ -246,7 +247,10 @@ async def test_process_timestamp_to_utc_isoformat() -> None: async def test_event_to_db_model() -> None: """Test we can round trip Event conversion.""" event = ha.Event( - "state_changed", {"some": "attr"}, ha.EventOrigin.local, dt_util.utcnow() + "state_changed", + {"some": "attr"}, + ha.EventOrigin.local, + dt_util.utcnow().timestamp(), ) db_event = Events.from_event(event) dialect = SupportedDialect.MYSQL diff --git a/tests/components/recorder/test_models_legacy.py b/tests/components/recorder/test_models_legacy.py index f830ac53544..f4cdcd7268b 100644 --- a/tests/components/recorder/test_models_legacy.py +++ b/tests/components/recorder/test_models_legacy.py @@ -1,4 +1,5 @@ """The tests for the Recorder component legacy models.""" + from datetime import datetime, timedelta from unittest.mock import PropertyMock diff --git a/tests/components/recorder/test_pool.py b/tests/components/recorder/test_pool.py index 3a6ff50af2b..541fc8d714b 100644 --- a/tests/components/recorder/test_pool.py +++ b/tests/components/recorder/test_pool.py @@ -1,4 +1,5 @@ """Test pool.""" + import threading import pytest diff --git a/tests/components/recorder/test_purge.py b/tests/components/recorder/test_purge.py index 2a9260a28a4..b2da3f1d62f 100644 --- a/tests/components/recorder/test_purge.py +++ b/tests/components/recorder/test_purge.py @@ -1,4 +1,5 @@ """Test data purging.""" + from datetime import datetime, timedelta import json import sqlite3 @@ -78,9 +79,11 @@ async def test_purge_big_database( await _add_test_states(hass, wait_recording_done=False) await async_wait_recording_done(hass) - with patch.object(instance, "max_bind_vars", 72), patch.object( - instance.database_engine, "max_bind_vars", 72 - ), session_scope(hass=hass) as session: + with ( + patch.object(instance, "max_bind_vars", 72), + patch.object(instance.database_engine, "max_bind_vars", 72), + session_scope(hass=hass) as session, + ): states = session.query(States) state_attributes = session.query(StateAttributes) assert states.count() == 72 @@ -207,11 +210,14 @@ async def test_purge_old_states_encouters_database_corruption( sqlite3_exception = DatabaseError("statement", {}, []) sqlite3_exception.__cause__ = sqlite3.DatabaseError() - with patch( - "homeassistant.components.recorder.core.move_away_broken_database" - ) as move_away, patch( - "homeassistant.components.recorder.purge.purge_old_data", - side_effect=sqlite3_exception, + with ( + patch( + "homeassistant.components.recorder.core.move_away_broken_database" + ) as move_away, + patch( + "homeassistant.components.recorder.purge.purge_old_data", + side_effect=sqlite3_exception, + ), ): await hass.services.async_call(recorder.DOMAIN, SERVICE_PURGE, {"keep_days": 0}) await hass.async_block_till_done() @@ -239,12 +245,14 @@ async def test_purge_old_states_encounters_temporary_mysql_error( mysql_exception = OperationalError("statement", {}, []) mysql_exception.orig = Exception(1205, "retryable") - with patch( - "homeassistant.components.recorder.util.time.sleep" - ) as sleep_mock, patch( - "homeassistant.components.recorder.purge._purge_old_recorder_runs", - side_effect=[mysql_exception, None], - ), patch.object(instance.engine.dialect, "name", "mysql"): + with ( + patch("homeassistant.components.recorder.util.time.sleep") as sleep_mock, + patch( + "homeassistant.components.recorder.purge._purge_old_recorder_runs", + side_effect=[mysql_exception, None], + ), + patch.object(instance.engine.dialect, "name", "mysql"), + ): await hass.services.async_call(recorder.DOMAIN, SERVICE_PURGE, {"keep_days": 0}) await hass.async_block_till_done() await async_wait_recording_done(hass) @@ -380,7 +388,7 @@ async def test_purge_old_statistics_runs( assert statistics_runs.count() == 1 -@pytest.mark.parametrize("use_sqlite", (True, False), indirect=True) +@pytest.mark.parametrize("use_sqlite", [True, False], indirect=True) async def test_purge_method( async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant, @@ -505,7 +513,7 @@ async def test_purge_method( ) -@pytest.mark.parametrize("use_sqlite", (True, False), indirect=True) +@pytest.mark.parametrize("use_sqlite", [True, False], indirect=True) async def test_purge_edge_case( async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant, @@ -748,7 +756,7 @@ async def test_purge_cutoff_date( assert state_attributes.count() == 0 -@pytest.mark.parametrize("use_sqlite", (True, False), indirect=True) +@pytest.mark.parametrize("use_sqlite", [True, False], indirect=True) async def test_purge_filtered_states( async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant, @@ -943,7 +951,7 @@ async def test_purge_filtered_states( assert session.query(StateAttributes).count() == 0 -@pytest.mark.parametrize("use_sqlite", (True, False), indirect=True) +@pytest.mark.parametrize("use_sqlite", [True, False], indirect=True) async def test_purge_filtered_states_to_empty( async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant, @@ -997,7 +1005,7 @@ async def test_purge_filtered_states_to_empty( await async_wait_purge_done(hass) -@pytest.mark.parametrize("use_sqlite", (True, False), indirect=True) +@pytest.mark.parametrize("use_sqlite", [True, False], indirect=True) async def test_purge_without_state_attributes_filtered_states_to_empty( async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant, @@ -1653,8 +1661,9 @@ async def test_purge_many_old_events( old_events_count = 5 instance = await async_setup_recorder_instance(hass) - with patch.object(instance, "max_bind_vars", old_events_count), patch.object( - instance.database_engine, "max_bind_vars", old_events_count + with ( + patch.object(instance, "max_bind_vars", old_events_count), + patch.object(instance.database_engine, "max_bind_vars", old_events_count), ): await _add_test_events(hass, old_events_count) diff --git a/tests/components/recorder/test_purge_v32_schema.py b/tests/components/recorder/test_purge_v32_schema.py index e8f9130165f..3946d8896f7 100644 --- a/tests/components/recorder/test_purge_v32_schema.py +++ b/tests/components/recorder/test_purge_v32_schema.py @@ -174,11 +174,14 @@ async def test_purge_old_states_encouters_database_corruption( sqlite3_exception = DatabaseError("statement", {}, []) sqlite3_exception.__cause__ = sqlite3.DatabaseError() - with patch( - "homeassistant.components.recorder.core.move_away_broken_database" - ) as move_away, patch( - "homeassistant.components.recorder.purge.purge_old_data", - side_effect=sqlite3_exception, + with ( + patch( + "homeassistant.components.recorder.core.move_away_broken_database" + ) as move_away, + patch( + "homeassistant.components.recorder.purge.purge_old_data", + side_effect=sqlite3_exception, + ), ): await hass.services.async_call(recorder.DOMAIN, SERVICE_PURGE, {"keep_days": 0}) await hass.async_block_till_done() @@ -207,12 +210,14 @@ async def test_purge_old_states_encounters_temporary_mysql_error( mysql_exception = OperationalError("statement", {}, []) mysql_exception.orig = Exception(1205, "retryable") - with patch( - "homeassistant.components.recorder.util.time.sleep" - ) as sleep_mock, patch( - "homeassistant.components.recorder.purge._purge_old_recorder_runs", - side_effect=[mysql_exception, None], - ), patch.object(instance.engine.dialect, "name", "mysql"): + with ( + patch("homeassistant.components.recorder.util.time.sleep") as sleep_mock, + patch( + "homeassistant.components.recorder.purge._purge_old_recorder_runs", + side_effect=[mysql_exception, None], + ), + patch.object(instance.engine.dialect, "name", "mysql"), + ): await hass.services.async_call(recorder.DOMAIN, SERVICE_PURGE, {"keep_days": 0}) await hass.async_block_till_done() await async_wait_recording_done(hass) @@ -349,7 +354,7 @@ async def test_purge_old_statistics_runs( assert statistics_runs.count() == 1 -@pytest.mark.parametrize("use_sqlite", (True, False), indirect=True) +@pytest.mark.parametrize("use_sqlite", [True, False], indirect=True) async def test_purge_method( async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant, @@ -469,7 +474,7 @@ async def test_purge_method( ) -@pytest.mark.parametrize("use_sqlite", (True, False), indirect=True) +@pytest.mark.parametrize("use_sqlite", [True, False], indirect=True) async def test_purge_edge_case( async_setup_recorder_instance: RecorderInstanceGenerator, hass: HomeAssistant, @@ -937,8 +942,9 @@ async def test_purge_many_old_events( await async_attach_db_engine(hass) old_events_count = 5 - with patch.object(instance, "max_bind_vars", old_events_count), patch.object( - instance.database_engine, "max_bind_vars", old_events_count + with ( + patch.object(instance, "max_bind_vars", old_events_count), + patch.object(instance.database_engine, "max_bind_vars", old_events_count), ): await _add_test_events(hass, old_events_count) diff --git a/tests/components/recorder/test_statistics.py b/tests/components/recorder/test_statistics.py index 69d42a6e7bd..d469db8831e 100644 --- a/tests/components/recorder/test_statistics.py +++ b/tests/components/recorder/test_statistics.py @@ -1,4 +1,5 @@ """The tests for sensor recorder platform.""" + from collections.abc import Callable from datetime import timedelta from unittest.mock import patch @@ -455,7 +456,7 @@ def test_rename_entity_collision( ) -> None: """Test statistics is migrated when entity_id is changed. - This test relies on the the safeguard in the statistics_meta_manager + This test relies on the safeguard in the statistics_meta_manager and should not hit the filter_unique_constraint_integrity_error safeguard. """ hass = hass_recorder() @@ -681,13 +682,13 @@ def test_statistics_duplicated( caplog.clear() -@pytest.mark.parametrize("last_reset_str", ("2022-01-01T00:00:00+02:00", None)) +@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( recorder_mock: Recorder, diff --git a/tests/components/recorder/test_statistics_v23_migration.py b/tests/components/recorder/test_statistics_v23_migration.py index 3aa96e18503..28c7613e761 100644 --- a/tests/components/recorder/test_statistics_v23_migration.py +++ b/tests/components/recorder/test_statistics_v23_migration.py @@ -3,6 +3,7 @@ The v23 schema used for these tests has been slightly modified to add the EventData table to allow the recorder to startup successfully. """ + from functools import partial import importlib import json @@ -159,15 +160,20 @@ def test_delete_duplicates(caplog: pytest.LogCaptureFixture, tmp_path: Path) -> } # Create some duplicated statistics with schema version 23 - with patch.object(recorder, "db_schema", old_db_schema), patch.object( - recorder.migration, "SCHEMA_VERSION", old_db_schema.SCHEMA_VERSION - ), patch( - CREATE_ENGINE_TARGET, - new=partial( - create_engine_test_for_schema_version_postfix, - schema_version_postfix=SCHEMA_VERSION_POSTFIX, + with ( + patch.object(recorder, "db_schema", old_db_schema), + patch.object( + recorder.migration, "SCHEMA_VERSION", old_db_schema.SCHEMA_VERSION ), - ), get_test_home_assistant() as hass: + patch( + CREATE_ENGINE_TARGET, + new=partial( + create_engine_test_for_schema_version_postfix, + schema_version_postfix=SCHEMA_VERSION_POSTFIX, + ), + ), + get_test_home_assistant() as hass, + ): recorder_helper.async_initialize_recorder(hass) setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) wait_recording_done(hass) @@ -336,15 +342,20 @@ def test_delete_duplicates_many( } # Create some duplicated statistics with schema version 23 - with patch.object(recorder, "db_schema", old_db_schema), patch.object( - recorder.migration, "SCHEMA_VERSION", old_db_schema.SCHEMA_VERSION - ), patch( - CREATE_ENGINE_TARGET, - new=partial( - create_engine_test_for_schema_version_postfix, - schema_version_postfix=SCHEMA_VERSION_POSTFIX, + with ( + patch.object(recorder, "db_schema", old_db_schema), + patch.object( + recorder.migration, "SCHEMA_VERSION", old_db_schema.SCHEMA_VERSION ), - ), get_test_home_assistant() as hass: + patch( + CREATE_ENGINE_TARGET, + new=partial( + create_engine_test_for_schema_version_postfix, + schema_version_postfix=SCHEMA_VERSION_POSTFIX, + ), + ), + get_test_home_assistant() as hass, + ): recorder_helper.async_initialize_recorder(hass) setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) wait_recording_done(hass) @@ -490,15 +501,20 @@ def test_delete_duplicates_non_identical( } # Create some duplicated statistics with schema version 23 - with patch.object(recorder, "db_schema", old_db_schema), patch.object( - recorder.migration, "SCHEMA_VERSION", old_db_schema.SCHEMA_VERSION - ), patch( - CREATE_ENGINE_TARGET, - new=partial( - create_engine_test_for_schema_version_postfix, - schema_version_postfix=SCHEMA_VERSION_POSTFIX, + with ( + patch.object(recorder, "db_schema", old_db_schema), + patch.object( + recorder.migration, "SCHEMA_VERSION", old_db_schema.SCHEMA_VERSION ), - ), get_test_home_assistant() as hass: + patch( + CREATE_ENGINE_TARGET, + new=partial( + create_engine_test_for_schema_version_postfix, + schema_version_postfix=SCHEMA_VERSION_POSTFIX, + ), + ), + get_test_home_assistant() as hass, + ): recorder_helper.async_initialize_recorder(hass) setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) wait_recording_done(hass) @@ -599,15 +615,20 @@ def test_delete_duplicates_short_term( } # Create some duplicated statistics with schema version 23 - with patch.object(recorder, "db_schema", old_db_schema), patch.object( - recorder.migration, "SCHEMA_VERSION", old_db_schema.SCHEMA_VERSION - ), patch( - CREATE_ENGINE_TARGET, - new=partial( - create_engine_test_for_schema_version_postfix, - schema_version_postfix=SCHEMA_VERSION_POSTFIX, + with ( + patch.object(recorder, "db_schema", old_db_schema), + patch.object( + recorder.migration, "SCHEMA_VERSION", old_db_schema.SCHEMA_VERSION ), - ), get_test_home_assistant() as hass: + patch( + CREATE_ENGINE_TARGET, + new=partial( + create_engine_test_for_schema_version_postfix, + schema_version_postfix=SCHEMA_VERSION_POSTFIX, + ), + ), + get_test_home_assistant() as hass, + ): recorder_helper.async_initialize_recorder(hass) setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) wait_recording_done(hass) diff --git a/tests/components/recorder/test_system_health.py b/tests/components/recorder/test_system_health.py index 5adacaf0ab6..ee4217dab69 100644 --- a/tests/components/recorder/test_system_health.py +++ b/tests/components/recorder/test_system_health.py @@ -1,4 +1,5 @@ """Test recorder system health.""" + from unittest.mock import ANY, Mock, patch import pytest @@ -44,11 +45,14 @@ async def test_recorder_system_health_alternate_dbms( """Test recorder system health.""" assert await async_setup_component(hass, "system_health", {}) await async_wait_recording_done(hass) - with patch( - "homeassistant.components.recorder.core.Recorder.dialect_name", dialect_name - ), patch( - "sqlalchemy.orm.session.Session.execute", - return_value=Mock(scalar=Mock(return_value=("1048576"))), + with ( + patch( + "homeassistant.components.recorder.core.Recorder.dialect_name", dialect_name + ), + patch( + "sqlalchemy.orm.session.Session.execute", + return_value=Mock(scalar=Mock(return_value=("1048576"))), + ), ): info = await get_system_health_info(hass, "recorder") instance = get_instance(hass) @@ -72,15 +76,19 @@ async def test_recorder_system_health_db_url_missing_host( await async_wait_recording_done(hass) instance = get_instance(hass) - with patch( - "homeassistant.components.recorder.core.Recorder.dialect_name", dialect_name - ), patch.object( - instance, - "db_url", - "postgresql://homeassistant:blabla@/home_assistant?host=/config/socket", - ), patch( - "sqlalchemy.orm.session.Session.execute", - return_value=Mock(scalar=Mock(return_value=("1048576"))), + with ( + patch( + "homeassistant.components.recorder.core.Recorder.dialect_name", dialect_name + ), + patch.object( + instance, + "db_url", + "postgresql://homeassistant:blabla@/home_assistant?host=/config/socket", + ), + patch( + "sqlalchemy.orm.session.Session.execute", + return_value=Mock(scalar=Mock(return_value=("1048576"))), + ), ): info = await get_system_health_info(hass, "recorder") assert info == { diff --git a/tests/components/recorder/test_util.py b/tests/components/recorder/test_util.py index 0d3ec463825..549280efba2 100644 --- a/tests/components/recorder/test_util.py +++ b/tests/components/recorder/test_util.py @@ -1,4 +1,5 @@ """Test util methods.""" + from collections.abc import Callable from datetime import UTC, datetime, timedelta import os @@ -45,9 +46,11 @@ from tests.typing import RecorderInstanceGenerator def test_session_scope_not_setup(hass_recorder: Callable[..., HomeAssistant]) -> None: """Try to create a session scope when not setup.""" hass = hass_recorder() - with patch.object( - util.get_instance(hass), "get_session", return_value=None - ), pytest.raises(RuntimeError), util.session_scope(hass=hass): + with ( + patch.object(util.get_instance(hass), "get_session", return_value=None), + pytest.raises(RuntimeError), + util.session_scope(hass=hass), + ): pass @@ -59,14 +62,15 @@ def test_recorder_bad_execute(hass_recorder: Callable[..., HomeAssistant]) -> No def to_native(validate_entity_id=True): """Raise exception.""" - raise SQLAlchemyError() + raise SQLAlchemyError mck1 = MagicMock() mck1.to_native = to_native - with pytest.raises(SQLAlchemyError), patch( - "homeassistant.components.recorder.core.time.sleep" - ) as e_mock: + with ( + pytest.raises(SQLAlchemyError), + patch("homeassistant.components.recorder.core.time.sleep") as e_mock, + ): util.execute((mck1,), to_native=True) assert e_mock.call_count == 2 @@ -145,12 +149,15 @@ async def test_last_run_was_recently_clean( thirty_min_future_time = dt_util.utcnow() + timedelta(minutes=30) async with async_test_home_assistant() as hass: - with patch( - "homeassistant.components.recorder.util.last_run_was_recently_clean", - wraps=_last_run_was_recently_clean, - ) as last_run_was_recently_clean_mock, patch( - "homeassistant.components.recorder.core.dt_util.utcnow", - return_value=thirty_min_future_time, + with ( + patch( + "homeassistant.components.recorder.util.last_run_was_recently_clean", + wraps=_last_run_was_recently_clean, + ) as last_run_was_recently_clean_mock, + patch( + "homeassistant.components.recorder.core.dt_util.utcnow", + return_value=thirty_min_future_time, + ), ): await async_setup_recorder_instance(hass, config) last_run_was_recently_clean_mock.assert_called_once() @@ -751,17 +758,23 @@ def test_combined_checks( assert "restarted cleanly and passed the basic sanity check" in caplog.text caplog.clear() - with patch( - "homeassistant.components.recorder.util.last_run_was_recently_clean", - side_effect=sqlite3.DatabaseError, - ), pytest.raises(sqlite3.DatabaseError): + with ( + patch( + "homeassistant.components.recorder.util.last_run_was_recently_clean", + side_effect=sqlite3.DatabaseError, + ), + pytest.raises(sqlite3.DatabaseError), + ): util.run_checks_on_open_db("fake_db_path", cursor) caplog.clear() - with patch( - "homeassistant.components.recorder.util.last_run_was_recently_clean", - side_effect=sqlite3.DatabaseError, - ), pytest.raises(sqlite3.DatabaseError): + with ( + patch( + "homeassistant.components.recorder.util.last_run_was_recently_clean", + side_effect=sqlite3.DatabaseError, + ), + pytest.raises(sqlite3.DatabaseError), + ): util.run_checks_on_open_db("fake_db_path", cursor) cursor.execute("DROP TABLE events;") @@ -912,7 +925,7 @@ def test_execute_stmt_lambda_element( start_time_ts = dt_util.utcnow().timestamp() stmt = lambda_stmt( lambda: _get_single_entity_start_time_stmt( - start_time_ts, metadata_id, False, False + start_time_ts, metadata_id, False, False, False ) ) rows = util.execute_stmt_lambda_element(session, stmt) diff --git a/tests/components/recorder/test_v32_migration.py b/tests/components/recorder/test_v32_migration.py index e423c479df4..a07c63b3376 100644 --- a/tests/components/recorder/test_v32_migration.py +++ b/tests/components/recorder/test_v32_migration.py @@ -1,4 +1,5 @@ """The tests for recorder platform migrating data from v30.""" + from datetime import timedelta import importlib from pathlib import Path @@ -77,13 +78,13 @@ async def test_migrate_times(caplog: pytest.LogCaptureFixture, tmp_path: Path) - "new_state": mock_state, }, EventOrigin.local, - time_fired=now, + time_fired_timestamp=now.timestamp(), ) custom_event = Event( "custom_event", {"entity_id": "sensor.custom"}, EventOrigin.local, - time_fired=now, + time_fired_timestamp=now.timestamp(), ) number_of_migrations = 5 @@ -91,26 +92,33 @@ async def test_migrate_times(caplog: pytest.LogCaptureFixture, tmp_path: Path) - with session_scope(hass=hass) as session: return inspect(session.connection()).get_indexes("states") - with patch.object(recorder, "db_schema", old_db_schema), patch.object( - recorder.migration, "SCHEMA_VERSION", old_db_schema.SCHEMA_VERSION - ), patch.object(core, "StatesMeta", old_db_schema.StatesMeta), patch.object( - core, "EventTypes", old_db_schema.EventTypes - ), patch.object(core, "EventData", old_db_schema.EventData), patch.object( - core, "States", old_db_schema.States - ), patch.object(core, "Events", old_db_schema.Events), patch( - CREATE_ENGINE_TARGET, new=_create_engine_test - ), patch( - "homeassistant.components.recorder.Recorder._migrate_events_context_ids", - ), patch( - "homeassistant.components.recorder.Recorder._migrate_states_context_ids", - ), patch( - "homeassistant.components.recorder.Recorder._migrate_event_type_ids", - ), patch( - "homeassistant.components.recorder.Recorder._migrate_entity_ids", - ), patch( - "homeassistant.components.recorder.Recorder._post_migrate_entity_ids" - ), patch( - "homeassistant.components.recorder.Recorder._cleanup_legacy_states_event_ids" + with ( + patch.object(recorder, "db_schema", old_db_schema), + patch.object( + recorder.migration, "SCHEMA_VERSION", old_db_schema.SCHEMA_VERSION + ), + patch.object(core, "StatesMeta", old_db_schema.StatesMeta), + patch.object(core, "EventTypes", old_db_schema.EventTypes), + patch.object(core, "EventData", old_db_schema.EventData), + patch.object(core, "States", old_db_schema.States), + patch.object(core, "Events", old_db_schema.Events), + patch(CREATE_ENGINE_TARGET, new=_create_engine_test), + patch( + "homeassistant.components.recorder.Recorder._migrate_events_context_ids", + ), + patch( + "homeassistant.components.recorder.Recorder._migrate_states_context_ids", + ), + patch( + "homeassistant.components.recorder.Recorder._migrate_event_type_ids", + ), + patch( + "homeassistant.components.recorder.Recorder._migrate_entity_ids", + ), + patch("homeassistant.components.recorder.Recorder._post_migrate_entity_ids"), + patch( + "homeassistant.components.recorder.Recorder._cleanup_legacy_states_event_ids" + ), ): async with async_test_home_assistant() as hass: recorder_helper.async_initialize_recorder(hass) @@ -241,13 +249,13 @@ async def test_migrate_can_resume_entity_id_post_migration( "new_state": mock_state, }, EventOrigin.local, - time_fired=now, + time_fired_timestamp=now.timestamp(), ) custom_event = Event( "custom_event", {"entity_id": "sensor.custom"}, EventOrigin.local, - time_fired=now, + time_fired_timestamp=now.timestamp(), ) number_of_migrations = 5 @@ -255,26 +263,33 @@ async def test_migrate_can_resume_entity_id_post_migration( with session_scope(hass=hass) as session: return inspect(session.connection()).get_indexes("states") - with patch.object(recorder, "db_schema", old_db_schema), patch.object( - recorder.migration, "SCHEMA_VERSION", old_db_schema.SCHEMA_VERSION - ), patch.object(core, "StatesMeta", old_db_schema.StatesMeta), patch.object( - core, "EventTypes", old_db_schema.EventTypes - ), patch.object(core, "EventData", old_db_schema.EventData), patch.object( - core, "States", old_db_schema.States - ), patch.object(core, "Events", old_db_schema.Events), patch( - CREATE_ENGINE_TARGET, new=_create_engine_test - ), patch( - "homeassistant.components.recorder.Recorder._migrate_events_context_ids", - ), patch( - "homeassistant.components.recorder.Recorder._migrate_states_context_ids", - ), patch( - "homeassistant.components.recorder.Recorder._migrate_event_type_ids", - ), patch( - "homeassistant.components.recorder.Recorder._migrate_entity_ids", - ), patch( - "homeassistant.components.recorder.Recorder._post_migrate_entity_ids" - ), patch( - "homeassistant.components.recorder.Recorder._cleanup_legacy_states_event_ids" + with ( + patch.object(recorder, "db_schema", old_db_schema), + patch.object( + recorder.migration, "SCHEMA_VERSION", old_db_schema.SCHEMA_VERSION + ), + patch.object(core, "StatesMeta", old_db_schema.StatesMeta), + patch.object(core, "EventTypes", old_db_schema.EventTypes), + patch.object(core, "EventData", old_db_schema.EventData), + patch.object(core, "States", old_db_schema.States), + patch.object(core, "Events", old_db_schema.Events), + patch(CREATE_ENGINE_TARGET, new=_create_engine_test), + patch( + "homeassistant.components.recorder.Recorder._migrate_events_context_ids", + ), + patch( + "homeassistant.components.recorder.Recorder._migrate_states_context_ids", + ), + patch( + "homeassistant.components.recorder.Recorder._migrate_event_type_ids", + ), + patch( + "homeassistant.components.recorder.Recorder._migrate_entity_ids", + ), + patch("homeassistant.components.recorder.Recorder._post_migrate_entity_ids"), + patch( + "homeassistant.components.recorder.Recorder._cleanup_legacy_states_event_ids" + ), ): async with async_test_home_assistant() as hass: recorder_helper.async_initialize_recorder(hass) diff --git a/tests/components/recorder/test_websocket_api.py b/tests/components/recorder/test_websocket_api.py index d35be0abc9b..d594218e9d4 100644 --- a/tests/components/recorder/test_websocket_api.py +++ b/tests/components/recorder/test_websocket_api.py @@ -1,4 +1,5 @@ """The tests for sensor recorder platform.""" + import datetime from datetime import timedelta from statistics import fmean @@ -215,7 +216,7 @@ async def test_statistics_during_period( @pytest.mark.freeze_time(datetime.datetime(2022, 10, 21, 7, 25, tzinfo=datetime.UTC)) -@pytest.mark.parametrize("offset", (0, 1, 2)) +@pytest.mark.parametrize("offset", [0, 1, 2]) async def test_statistic_during_period( recorder_mock: Recorder, hass: HomeAssistant, @@ -241,7 +242,7 @@ async def test_statistic_during_period( "min": -76 + i * 2, "sum": i, } - for i in range(0, 39) + for i in range(39) ] imported_stats = [] slice_end = 12 - offset @@ -254,7 +255,7 @@ async def test_statistic_during_period( "sum": imported_stats_5min[slice_end - 1]["sum"], } ) - for i in range(0, 2): + for i in range(2): slice_start = i * 12 + (12 - offset) slice_end = (i + 1) * 12 + (12 - offset) assert imported_stats_5min[slice_start]["start"].minute == 0 @@ -663,7 +664,7 @@ async def test_statistic_during_period_hole( "min": -76 + i * 2, "sum": i, } - for i in range(0, 6) + for i in range(6) ] imported_metadata = { @@ -796,7 +797,7 @@ async def test_statistic_during_period_hole( @pytest.mark.freeze_time(datetime.datetime(2022, 10, 21, 7, 25, tzinfo=datetime.UTC)) @pytest.mark.parametrize( ("calendar_period", "start_time", "end_time"), - ( + [ ( {"period": "hour"}, "2022-10-21T07:00:00+00:00", @@ -847,7 +848,7 @@ async def test_statistic_during_period_hole( "2021-01-01T08:00:00+00:00", "2022-01-01T08:00:00+00:00", ), - ), + ], ) async def test_statistic_during_period_calendar( recorder_mock: Recorder, @@ -2165,16 +2166,19 @@ async def test_recorder_info_migration_queue_exhausted( migration_done.wait() return real_migration(*args) - with patch("homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", True), patch( - "homeassistant.components.recorder.Recorder.async_periodic_statistics" - ), patch( - "homeassistant.components.recorder.core.create_engine", - new=create_engine_test, - ), patch.object(recorder.core, "MAX_QUEUE_BACKLOG_MIN_VALUE", 1), patch.object( - recorder.core, "QUEUE_PERCENTAGE_ALLOWED_AVAILABLE_MEMORY", 0 - ), patch( - "homeassistant.components.recorder.migration._apply_update", - wraps=stalled_migration, + with ( + patch("homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", True), + patch("homeassistant.components.recorder.Recorder.async_periodic_statistics"), + patch( + "homeassistant.components.recorder.core.create_engine", + new=create_engine_test, + ), + patch.object(recorder.core, "MAX_QUEUE_BACKLOG_MIN_VALUE", 1), + patch.object(recorder.core, "QUEUE_PERCENTAGE_ALLOWED_AVAILABLE_MEMORY", 0), + patch( + "homeassistant.components.recorder.migration._apply_update", + wraps=stalled_migration, + ), ): recorder_helper.async_initialize_recorder(hass) hass.create_task( @@ -2392,10 +2396,10 @@ async def test_get_statistics_metadata( @pytest.mark.parametrize( ("source", "statistic_id"), - ( + [ ("test", "test:total_energy_import"), ("recorder", "sensor.total_energy_import"), - ), + ], ) async def test_import_statistics( recorder_mock: Recorder, @@ -2606,10 +2610,10 @@ async def test_import_statistics( @pytest.mark.parametrize( ("source", "statistic_id"), - ( + [ ("test", "test:total_energy_import"), ("recorder", "sensor.total_energy_import"), - ), + ], ) async def test_adjust_sum_statistics_energy( recorder_mock: Recorder, @@ -2799,10 +2803,10 @@ async def test_adjust_sum_statistics_energy( @pytest.mark.parametrize( ("source", "statistic_id"), - ( + [ ("test", "test:total_gas"), ("recorder", "sensor.total_gas"), - ), + ], ) async def test_adjust_sum_statistics_gas( recorder_mock: Recorder, @@ -2999,14 +3003,14 @@ async def test_adjust_sum_statistics_gas( "valid_units", "invalid_units", ), - ( + [ ("kWh", "kWh", "energy", 1, ("Wh", "kWh", "MWh"), ("ft³", "m³", "cats", None)), ("MWh", "MWh", "energy", 1, ("Wh", "kWh", "MWh"), ("ft³", "m³", "cats", None)), ("m³", "m³", "volume", 1, ("ft³", "m³"), ("Wh", "kWh", "MWh", "cats", None)), ("ft³", "ft³", "volume", 1, ("ft³", "m³"), ("Wh", "kWh", "MWh", "cats", None)), ("dogs", "dogs", None, 1, ("dogs",), ("cats", None)), (None, None, "unitless", 1, (None,), ("cats",)), - ), + ], ) async def test_adjust_sum_statistics_errors( recorder_mock: Recorder, diff --git a/tests/components/recovery_mode/test_init.py b/tests/components/recovery_mode/test_init.py index ec8db443ef1..506cd010725 100644 --- a/tests/components/recovery_mode/test_init.py +++ b/tests/components/recovery_mode/test_init.py @@ -1,4 +1,5 @@ """Tests for the Recovery Mode integration.""" + from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component diff --git a/tests/components/reddit/__init__.py b/tests/components/reddit/__init__.py index 67e0db82f42..d45d10130bd 100644 --- a/tests/components/reddit/__init__.py +++ b/tests/components/reddit/__init__.py @@ -1 +1 @@ -"""Tests for the the Reddit component.""" +"""Tests for the Reddit component.""" diff --git a/tests/components/reddit/test_sensor.py b/tests/components/reddit/test_sensor.py index e3d31d29ff8..92ee282e9c8 100644 --- a/tests/components/reddit/test_sensor.py +++ b/tests/components/reddit/test_sensor.py @@ -1,4 +1,5 @@ """The tests for the Reddit platform.""" + import copy from unittest.mock import patch diff --git a/tests/components/refoss/__init__.py b/tests/components/refoss/__init__.py index 34df1b41714..e7c160ef0af 100644 --- a/tests/components/refoss/__init__.py +++ b/tests/components/refoss/__init__.py @@ -1,4 +1,5 @@ """Common helpers for refoss test cases.""" + import asyncio import logging from unittest.mock import AsyncMock, Mock diff --git a/tests/components/refoss/conftest.py b/tests/components/refoss/conftest.py index 2fc695bbb2e..d627af5b5ab 100644 --- a/tests/components/refoss/conftest.py +++ b/tests/components/refoss/conftest.py @@ -1,4 +1,5 @@ """Pytest module configuration.""" + from collections.abc import Generator from unittest.mock import AsyncMock, patch diff --git a/tests/components/refoss/test_config_flow.py b/tests/components/refoss/test_config_flow.py index 2a5842ffe46..f022c950635 100644 --- a/tests/components/refoss/test_config_flow.py +++ b/tests/components/refoss/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for the refoss Integration.""" + from unittest.mock import AsyncMock, patch from homeassistant import config_entries, data_entry_flow @@ -13,15 +14,19 @@ async def test_creating_entry_sets_up( hass: HomeAssistant, mock_setup_entry: AsyncMock ) -> None: """Test setting up refoss.""" - with patch( - "homeassistant.components.refoss.util.Discovery", - return_value=FakeDiscovery(), - ), patch( - "homeassistant.components.refoss.bridge.async_build_base_device", - return_value=build_base_device_mock(), - ), patch( - "homeassistant.components.refoss.switch.isinstance", - return_value=True, + with ( + patch( + "homeassistant.components.refoss.util.Discovery", + return_value=FakeDiscovery(), + ), + patch( + "homeassistant.components.refoss.bridge.async_build_base_device", + return_value=build_base_device_mock(), + ), + patch( + "homeassistant.components.refoss.switch.isinstance", + return_value=True, + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} diff --git a/tests/components/remember_the_milk/test_init.py b/tests/components/remember_the_milk/test_init.py index 703e1de7b93..c68fe14430a 100644 --- a/tests/components/remember_the_milk/test_init.py +++ b/tests/components/remember_the_milk/test_init.py @@ -1,4 +1,5 @@ """Tests for the Remember The Milk component.""" + from unittest.mock import Mock, mock_open, patch import homeassistant.components.remember_the_milk as rtm @@ -9,9 +10,11 @@ from .const import JSON_STRING, PROFILE, TOKEN def test_create_new(hass: HomeAssistant) -> None: """Test creating a new config file.""" - with patch("builtins.open", mock_open()), patch( - "os.path.isfile", Mock(return_value=False) - ), patch.object(rtm.RememberTheMilkConfiguration, "save_config"): + with ( + patch("builtins.open", mock_open()), + patch("os.path.isfile", Mock(return_value=False)), + patch.object(rtm.RememberTheMilkConfiguration, "save_config"), + ): config = rtm.RememberTheMilkConfiguration(hass) config.set_token(PROFILE, TOKEN) assert config.get_token(PROFILE) == TOKEN @@ -19,8 +22,9 @@ def test_create_new(hass: HomeAssistant) -> None: def test_load_config(hass: HomeAssistant) -> None: """Test loading an existing token from the file.""" - with patch("builtins.open", mock_open(read_data=JSON_STRING)), patch( - "os.path.isfile", Mock(return_value=True) + with ( + patch("builtins.open", mock_open(read_data=JSON_STRING)), + patch("os.path.isfile", Mock(return_value=True)), ): config = rtm.RememberTheMilkConfiguration(hass) assert config.get_token(PROFILE) == TOKEN @@ -28,8 +32,9 @@ def test_load_config(hass: HomeAssistant) -> None: def test_invalid_data(hass: HomeAssistant) -> None: """Test starts with invalid data and should not raise an exception.""" - with patch("builtins.open", mock_open(read_data="random characters")), patch( - "os.path.isfile", Mock(return_value=True) + with ( + patch("builtins.open", mock_open(read_data="random characters")), + patch("os.path.isfile", Mock(return_value=True)), ): config = rtm.RememberTheMilkConfiguration(hass) assert config is not None @@ -41,9 +46,11 @@ def test_id_map(hass: HomeAssistant) -> None: list_id = "mylist" timeseries_id = "my_timeseries" rtm_id = "rtm-id-4567" - with patch("builtins.open", mock_open()), patch( - "os.path.isfile", Mock(return_value=False) - ), patch.object(rtm.RememberTheMilkConfiguration, "save_config"): + with ( + patch("builtins.open", mock_open()), + patch("os.path.isfile", Mock(return_value=False)), + patch.object(rtm.RememberTheMilkConfiguration, "save_config"), + ): config = rtm.RememberTheMilkConfiguration(hass) assert config.get_rtm_id(PROFILE, hass_id) is None @@ -55,8 +62,9 @@ def test_id_map(hass: HomeAssistant) -> None: def test_load_key_map(hass: HomeAssistant) -> None: """Test loading an existing key map from the file.""" - with patch("builtins.open", mock_open(read_data=JSON_STRING)), patch( - "os.path.isfile", Mock(return_value=True) + with ( + patch("builtins.open", mock_open(read_data=JSON_STRING)), + patch("os.path.isfile", Mock(return_value=True)), ): config = rtm.RememberTheMilkConfiguration(hass) assert config.get_rtm_id(PROFILE, "1234") == ("0", "1", "2") diff --git a/tests/components/remote/test_device_action.py b/tests/components/remote/test_device_action.py index 76e6075a18f..09c68843872 100644 --- a/tests/components/remote/test_device_action.py +++ b/tests/components/remote/test_device_action.py @@ -1,4 +1,5 @@ """The test for remote device automation.""" + import pytest from pytest_unordered import unordered @@ -62,12 +63,12 @@ async def test_get_actions( @pytest.mark.parametrize( ("hidden_by", "entity_category"), - ( + [ (RegistryEntryHider.INTEGRATION, None), (RegistryEntryHider.USER, None), (None, EntityCategory.CONFIG), (None, EntityCategory.DIAGNOSTIC), - ), + ], ) async def test_get_actions_hidden_auxiliary( hass: HomeAssistant, diff --git a/tests/components/remote/test_device_condition.py b/tests/components/remote/test_device_condition.py index 1048aa1b081..e7826f4952c 100644 --- a/tests/components/remote/test_device_condition.py +++ b/tests/components/remote/test_device_condition.py @@ -1,4 +1,5 @@ """The test for remote device automation.""" + from datetime import timedelta from freezegun import freeze_time @@ -68,12 +69,12 @@ async def test_get_conditions( @pytest.mark.parametrize( ("hidden_by", "entity_category"), - ( + [ (RegistryEntryHider.INTEGRATION, None), (RegistryEntryHider.USER, None), (None, EntityCategory.CONFIG), (None, EntityCategory.DIAGNOSTIC), - ), + ], ) async def test_get_conditions_hidden_auxiliary( hass: HomeAssistant, diff --git a/tests/components/remote/test_device_trigger.py b/tests/components/remote/test_device_trigger.py index 711b9672aa0..b77d971e9a6 100644 --- a/tests/components/remote/test_device_trigger.py +++ b/tests/components/remote/test_device_trigger.py @@ -1,4 +1,5 @@ """The test for remote device automation.""" + from datetime import timedelta import pytest @@ -68,12 +69,12 @@ async def test_get_triggers( @pytest.mark.parametrize( ("hidden_by", "entity_category"), - ( + [ (RegistryEntryHider.INTEGRATION, None), (RegistryEntryHider.USER, None), (None, EntityCategory.CONFIG), (None, EntityCategory.DIAGNOSTIC), - ), + ], ) async def test_get_triggers_hidden_auxiliary( hass: HomeAssistant, diff --git a/tests/components/remote/test_init.py b/tests/components/remote/test_init.py index be4a4843097..15fbb1174c6 100644 --- a/tests/components/remote/test_init.py +++ b/tests/components/remote/test_init.py @@ -1,4 +1,5 @@ """The tests for the Remote component, adapted from Light Test.""" + import pytest import homeassistant.components.remote as remote diff --git a/tests/components/remote/test_reproduce_state.py b/tests/components/remote/test_reproduce_state.py index 910e3e867c6..7d1954e3ce1 100644 --- a/tests/components/remote/test_reproduce_state.py +++ b/tests/components/remote/test_reproduce_state.py @@ -1,4 +1,5 @@ """Test reproduce state for Remote.""" + import pytest from homeassistant.core import HomeAssistant, State diff --git a/tests/components/remote/test_significant_change.py b/tests/components/remote/test_significant_change.py index dcbfce213d6..050d5a2ffc7 100644 --- a/tests/components/remote/test_significant_change.py +++ b/tests/components/remote/test_significant_change.py @@ -1,4 +1,5 @@ """Test the Remote significant change platform.""" + from homeassistant.components.remote import ATTR_ACTIVITY_LIST, ATTR_CURRENT_ACTIVITY from homeassistant.components.remote.significant_change import ( async_check_significant_change, diff --git a/tests/components/renault/__init__.py b/tests/components/renault/__init__.py index 8c47410ce40..86fddfd5bac 100644 --- a/tests/components/renault/__init__.py +++ b/tests/components/renault/__init__.py @@ -1,4 +1,5 @@ """Tests for the Renault integration.""" + from __future__ import annotations from types import MappingProxyType @@ -81,8 +82,6 @@ def check_entities_no_data( assert state.state == expected_state for attr in FIXED_ATTRIBUTES: assert state.attributes.get(attr) == expected_entity.get(attr) - # Check dynamic attributes: - assert state.attributes.get(ATTR_ICON) == get_no_data_icon(expected_entity) def check_entities_unavailable( @@ -100,5 +99,3 @@ def check_entities_unavailable( assert state.state == STATE_UNAVAILABLE for attr in FIXED_ATTRIBUTES: assert state.attributes.get(attr) == expected_entity.get(attr) - # Check dynamic attributes: - assert state.attributes.get(ATTR_ICON) == get_no_data_icon(expected_entity) diff --git a/tests/components/renault/conftest.py b/tests/components/renault/conftest.py index 312ddbf6092..c06abc8efd0 100644 --- a/tests/components/renault/conftest.py +++ b/tests/components/renault/conftest.py @@ -1,4 +1,5 @@ """Provide common Renault fixtures.""" + from collections.abc import Generator import contextlib from types import MappingProxyType @@ -56,9 +57,12 @@ async def patch_renault_account(hass: HomeAssistant) -> RenaultAccount: MOCK_ACCOUNT_ID, websession=aiohttp_client.async_get_clientsession(hass), ) - with patch("renault_api.renault_session.RenaultSession.login"), patch( - "renault_api.renault_client.RenaultClient.get_api_account", - return_value=renault_account, + with ( + patch("renault_api.renault_session.RenaultSession.login"), + patch( + "renault_api.renault_client.RenaultClient.get_api_account", + return_value=renault_account, + ), ): yield renault_account @@ -124,27 +128,35 @@ def patch_fixtures_with_data(vehicle_type: str): """Mock fixtures.""" mock_fixtures = _get_fixtures(vehicle_type) - with patch( - "renault_api.renault_vehicle.RenaultVehicle.get_battery_status", - return_value=mock_fixtures["battery_status"], - ), patch( - "renault_api.renault_vehicle.RenaultVehicle.get_charge_mode", - return_value=mock_fixtures["charge_mode"], - ), patch( - "renault_api.renault_vehicle.RenaultVehicle.get_cockpit", - return_value=mock_fixtures["cockpit"], - ), patch( - "renault_api.renault_vehicle.RenaultVehicle.get_hvac_status", - return_value=mock_fixtures["hvac_status"], - ), patch( - "renault_api.renault_vehicle.RenaultVehicle.get_location", - return_value=mock_fixtures["location"], - ), patch( - "renault_api.renault_vehicle.RenaultVehicle.get_lock_status", - return_value=mock_fixtures["lock_status"], - ), patch( - "renault_api.renault_vehicle.RenaultVehicle.get_res_state", - return_value=mock_fixtures["res_state"], + with ( + patch( + "renault_api.renault_vehicle.RenaultVehicle.get_battery_status", + return_value=mock_fixtures["battery_status"], + ), + patch( + "renault_api.renault_vehicle.RenaultVehicle.get_charge_mode", + return_value=mock_fixtures["charge_mode"], + ), + patch( + "renault_api.renault_vehicle.RenaultVehicle.get_cockpit", + return_value=mock_fixtures["cockpit"], + ), + patch( + "renault_api.renault_vehicle.RenaultVehicle.get_hvac_status", + return_value=mock_fixtures["hvac_status"], + ), + patch( + "renault_api.renault_vehicle.RenaultVehicle.get_location", + return_value=mock_fixtures["location"], + ), + patch( + "renault_api.renault_vehicle.RenaultVehicle.get_lock_status", + return_value=mock_fixtures["lock_status"], + ), + patch( + "renault_api.renault_vehicle.RenaultVehicle.get_res_state", + return_value=mock_fixtures["res_state"], + ), ): yield @@ -154,27 +166,35 @@ def patch_fixtures_with_no_data(): """Mock fixtures.""" mock_fixtures = _get_fixtures("") - with patch( - "renault_api.renault_vehicle.RenaultVehicle.get_battery_status", - return_value=mock_fixtures["battery_status"], - ), patch( - "renault_api.renault_vehicle.RenaultVehicle.get_charge_mode", - return_value=mock_fixtures["charge_mode"], - ), patch( - "renault_api.renault_vehicle.RenaultVehicle.get_cockpit", - return_value=mock_fixtures["cockpit"], - ), patch( - "renault_api.renault_vehicle.RenaultVehicle.get_hvac_status", - return_value=mock_fixtures["hvac_status"], - ), patch( - "renault_api.renault_vehicle.RenaultVehicle.get_location", - return_value=mock_fixtures["location"], - ), patch( - "renault_api.renault_vehicle.RenaultVehicle.get_lock_status", - return_value=mock_fixtures["lock_status"], - ), patch( - "renault_api.renault_vehicle.RenaultVehicle.get_res_state", - return_value=mock_fixtures["res_state"], + with ( + patch( + "renault_api.renault_vehicle.RenaultVehicle.get_battery_status", + return_value=mock_fixtures["battery_status"], + ), + patch( + "renault_api.renault_vehicle.RenaultVehicle.get_charge_mode", + return_value=mock_fixtures["charge_mode"], + ), + patch( + "renault_api.renault_vehicle.RenaultVehicle.get_cockpit", + return_value=mock_fixtures["cockpit"], + ), + patch( + "renault_api.renault_vehicle.RenaultVehicle.get_hvac_status", + return_value=mock_fixtures["hvac_status"], + ), + patch( + "renault_api.renault_vehicle.RenaultVehicle.get_location", + return_value=mock_fixtures["location"], + ), + patch( + "renault_api.renault_vehicle.RenaultVehicle.get_lock_status", + return_value=mock_fixtures["lock_status"], + ), + patch( + "renault_api.renault_vehicle.RenaultVehicle.get_res_state", + return_value=mock_fixtures["res_state"], + ), ): yield @@ -182,27 +202,35 @@ def patch_fixtures_with_no_data(): @contextlib.contextmanager def _patch_fixtures_with_side_effect(side_effect: Any): """Mock fixtures.""" - with patch( - "renault_api.renault_vehicle.RenaultVehicle.get_battery_status", - side_effect=side_effect, - ), patch( - "renault_api.renault_vehicle.RenaultVehicle.get_charge_mode", - side_effect=side_effect, - ), patch( - "renault_api.renault_vehicle.RenaultVehicle.get_cockpit", - side_effect=side_effect, - ), patch( - "renault_api.renault_vehicle.RenaultVehicle.get_hvac_status", - side_effect=side_effect, - ), patch( - "renault_api.renault_vehicle.RenaultVehicle.get_location", - side_effect=side_effect, - ), patch( - "renault_api.renault_vehicle.RenaultVehicle.get_lock_status", - side_effect=side_effect, - ), patch( - "renault_api.renault_vehicle.RenaultVehicle.get_res_state", - side_effect=side_effect, + with ( + patch( + "renault_api.renault_vehicle.RenaultVehicle.get_battery_status", + side_effect=side_effect, + ), + patch( + "renault_api.renault_vehicle.RenaultVehicle.get_charge_mode", + side_effect=side_effect, + ), + patch( + "renault_api.renault_vehicle.RenaultVehicle.get_cockpit", + side_effect=side_effect, + ), + patch( + "renault_api.renault_vehicle.RenaultVehicle.get_hvac_status", + side_effect=side_effect, + ), + patch( + "renault_api.renault_vehicle.RenaultVehicle.get_location", + side_effect=side_effect, + ), + patch( + "renault_api.renault_vehicle.RenaultVehicle.get_lock_status", + side_effect=side_effect, + ), + patch( + "renault_api.renault_vehicle.RenaultVehicle.get_res_state", + side_effect=side_effect, + ), ): yield diff --git a/tests/components/renault/const.py b/tests/components/renault/const.py index 342ab803f33..d849c658149 100644 --- a/tests/components/renault/const.py +++ b/tests/components/renault/const.py @@ -1,4 +1,5 @@ """Constants for the Renault integration tests.""" + from homeassistant.components.binary_sensor import BinarySensorDeviceClass from homeassistant.components.renault.const import ( CONF_KAMEREON_ACCOUNT_ID, diff --git a/tests/components/renault/snapshots/test_binary_sensor.ambr b/tests/components/renault/snapshots/test_binary_sensor.ambr index 8adbf1e9d02..7f30faac38e 100644 --- a/tests/components/renault/snapshots/test_binary_sensor.ambr +++ b/tests/components/renault/snapshots/test_binary_sensor.ambr @@ -231,6 +231,7 @@ 'context': , 'entity_id': 'binary_sensor.reg_number_lock', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -242,6 +243,7 @@ 'context': , 'entity_id': 'binary_sensor.reg_number_hatch', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -253,6 +255,7 @@ 'context': , 'entity_id': 'binary_sensor.reg_number_rear_left_door', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -264,6 +267,7 @@ 'context': , 'entity_id': 'binary_sensor.reg_number_rear_right_door', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -275,6 +279,7 @@ 'context': , 'entity_id': 'binary_sensor.reg_number_driver_door', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -286,6 +291,7 @@ 'context': , 'entity_id': 'binary_sensor.reg_number_passenger_door', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -585,6 +591,7 @@ 'context': , 'entity_id': 'binary_sensor.reg_number_plug', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -596,6 +603,7 @@ 'context': , 'entity_id': 'binary_sensor.reg_number_charging', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -607,6 +615,7 @@ 'context': , 'entity_id': 'binary_sensor.reg_number_lock', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -618,6 +627,7 @@ 'context': , 'entity_id': 'binary_sensor.reg_number_hatch', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -629,6 +639,7 @@ 'context': , 'entity_id': 'binary_sensor.reg_number_rear_left_door', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -640,6 +651,7 @@ 'context': , 'entity_id': 'binary_sensor.reg_number_rear_right_door', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -651,6 +663,7 @@ 'context': , 'entity_id': 'binary_sensor.reg_number_driver_door', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -662,6 +675,7 @@ 'context': , 'entity_id': 'binary_sensor.reg_number_passenger_door', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -785,7 +799,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:fan-off', + 'original_icon': None, 'original_name': 'HVAC', 'platform': 'renault', 'previous_unique_id': None, @@ -806,6 +820,7 @@ 'context': , 'entity_id': 'binary_sensor.reg_number_plug', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -817,17 +832,18 @@ 'context': , 'entity_id': 'binary_sensor.reg_number_charging', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'REG-NUMBER HVAC', - 'icon': 'mdi:fan-off', }), 'context': , 'entity_id': 'binary_sensor.reg_number_hvac', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -951,7 +967,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:fan-off', + 'original_icon': None, 'original_name': 'HVAC', 'platform': 'renault', 'previous_unique_id': None, @@ -1158,6 +1174,7 @@ 'context': , 'entity_id': 'binary_sensor.reg_number_plug', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -1169,17 +1186,18 @@ 'context': , 'entity_id': 'binary_sensor.reg_number_charging', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'REG-NUMBER HVAC', - 'icon': 'mdi:fan-off', }), 'context': , 'entity_id': 'binary_sensor.reg_number_hvac', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -1191,6 +1209,7 @@ 'context': , 'entity_id': 'binary_sensor.reg_number_lock', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -1202,6 +1221,7 @@ 'context': , 'entity_id': 'binary_sensor.reg_number_hatch', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -1213,6 +1233,7 @@ 'context': , 'entity_id': 'binary_sensor.reg_number_rear_left_door', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -1224,6 +1245,7 @@ 'context': , 'entity_id': 'binary_sensor.reg_number_rear_right_door', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -1235,6 +1257,7 @@ 'context': , 'entity_id': 'binary_sensor.reg_number_driver_door', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -1246,6 +1269,7 @@ 'context': , 'entity_id': 'binary_sensor.reg_number_passenger_door', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -1483,6 +1507,7 @@ 'context': , 'entity_id': 'binary_sensor.reg_number_lock', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }), @@ -1494,6 +1519,7 @@ 'context': , 'entity_id': 'binary_sensor.reg_number_hatch', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }), @@ -1505,6 +1531,7 @@ 'context': , 'entity_id': 'binary_sensor.reg_number_rear_left_door', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }), @@ -1516,6 +1543,7 @@ 'context': , 'entity_id': 'binary_sensor.reg_number_rear_right_door', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }), @@ -1527,6 +1555,7 @@ 'context': , 'entity_id': 'binary_sensor.reg_number_driver_door', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }), @@ -1538,6 +1567,7 @@ 'context': , 'entity_id': 'binary_sensor.reg_number_passenger_door', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }), @@ -1837,6 +1867,7 @@ 'context': , 'entity_id': 'binary_sensor.reg_number_plug', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }), @@ -1848,6 +1879,7 @@ 'context': , 'entity_id': 'binary_sensor.reg_number_charging', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }), @@ -1859,6 +1891,7 @@ 'context': , 'entity_id': 'binary_sensor.reg_number_lock', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }), @@ -1870,6 +1903,7 @@ 'context': , 'entity_id': 'binary_sensor.reg_number_hatch', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }), @@ -1881,6 +1915,7 @@ 'context': , 'entity_id': 'binary_sensor.reg_number_rear_left_door', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }), @@ -1892,6 +1927,7 @@ 'context': , 'entity_id': 'binary_sensor.reg_number_rear_right_door', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }), @@ -1903,6 +1939,7 @@ 'context': , 'entity_id': 'binary_sensor.reg_number_driver_door', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }), @@ -1914,6 +1951,7 @@ 'context': , 'entity_id': 'binary_sensor.reg_number_passenger_door', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }), @@ -2037,7 +2075,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:fan-off', + 'original_icon': None, 'original_name': 'HVAC', 'platform': 'renault', 'previous_unique_id': None, @@ -2058,6 +2096,7 @@ 'context': , 'entity_id': 'binary_sensor.reg_number_plug', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }), @@ -2069,17 +2108,18 @@ 'context': , 'entity_id': 'binary_sensor.reg_number_charging', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }), StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'REG-NUMBER HVAC', - 'icon': 'mdi:fan-off', }), 'context': , 'entity_id': 'binary_sensor.reg_number_hvac', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }), @@ -2203,7 +2243,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:fan-off', + 'original_icon': None, 'original_name': 'HVAC', 'platform': 'renault', 'previous_unique_id': None, @@ -2410,6 +2450,7 @@ 'context': , 'entity_id': 'binary_sensor.reg_number_plug', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }), @@ -2421,17 +2462,18 @@ 'context': , 'entity_id': 'binary_sensor.reg_number_charging', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }), StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'REG-NUMBER HVAC', - 'icon': 'mdi:fan-off', }), 'context': , 'entity_id': 'binary_sensor.reg_number_hvac', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }), @@ -2443,6 +2485,7 @@ 'context': , 'entity_id': 'binary_sensor.reg_number_lock', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }), @@ -2454,6 +2497,7 @@ 'context': , 'entity_id': 'binary_sensor.reg_number_hatch', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }), @@ -2465,6 +2509,7 @@ 'context': , 'entity_id': 'binary_sensor.reg_number_rear_left_door', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }), @@ -2476,6 +2521,7 @@ 'context': , 'entity_id': 'binary_sensor.reg_number_rear_right_door', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }), @@ -2487,6 +2533,7 @@ 'context': , 'entity_id': 'binary_sensor.reg_number_driver_door', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }), @@ -2498,6 +2545,7 @@ 'context': , 'entity_id': 'binary_sensor.reg_number_passenger_door', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }), diff --git a/tests/components/renault/snapshots/test_button.ambr b/tests/components/renault/snapshots/test_button.ambr index 58903962a2e..daef84b5c0a 100644 --- a/tests/components/renault/snapshots/test_button.ambr +++ b/tests/components/renault/snapshots/test_button.ambr @@ -55,7 +55,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:air-conditioner', + 'original_icon': None, 'original_name': 'Start air conditioner', 'platform': 'renault', 'previous_unique_id': None, @@ -71,11 +71,11 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'REG-NUMBER Start air conditioner', - 'icon': 'mdi:air-conditioner', }), 'context': , 'entity_id': 'button.reg_number_start_air_conditioner', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -137,7 +137,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:air-conditioner', + 'original_icon': None, 'original_name': 'Start air conditioner', 'platform': 'renault', 'previous_unique_id': None, @@ -168,7 +168,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:ev-station', + 'original_icon': None, 'original_name': 'Start charge', 'platform': 'renault', 'previous_unique_id': None, @@ -199,7 +199,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:ev-station', + 'original_icon': None, 'original_name': 'Stop charge', 'platform': 'renault', 'previous_unique_id': None, @@ -215,33 +215,33 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'REG-NUMBER Start air conditioner', - 'icon': 'mdi:air-conditioner', }), 'context': , 'entity_id': 'button.reg_number_start_air_conditioner', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'REG-NUMBER Start charge', - 'icon': 'mdi:ev-station', }), 'context': , 'entity_id': 'button.reg_number_start_charge', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'REG-NUMBER Stop charge', - 'icon': 'mdi:ev-station', }), 'context': , 'entity_id': 'button.reg_number_stop_charge', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -303,7 +303,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:air-conditioner', + 'original_icon': None, 'original_name': 'Start air conditioner', 'platform': 'renault', 'previous_unique_id': None, @@ -334,7 +334,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:ev-station', + 'original_icon': None, 'original_name': 'Start charge', 'platform': 'renault', 'previous_unique_id': None, @@ -365,7 +365,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:ev-station', + 'original_icon': None, 'original_name': 'Stop charge', 'platform': 'renault', 'previous_unique_id': None, @@ -381,33 +381,33 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'REG-NUMBER Start air conditioner', - 'icon': 'mdi:air-conditioner', }), 'context': , 'entity_id': 'button.reg_number_start_air_conditioner', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'REG-NUMBER Start charge', - 'icon': 'mdi:ev-station', }), 'context': , 'entity_id': 'button.reg_number_start_charge', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'REG-NUMBER Stop charge', - 'icon': 'mdi:ev-station', }), 'context': , 'entity_id': 'button.reg_number_stop_charge', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -469,7 +469,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:air-conditioner', + 'original_icon': None, 'original_name': 'Start air conditioner', 'platform': 'renault', 'previous_unique_id': None, @@ -500,7 +500,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:ev-station', + 'original_icon': None, 'original_name': 'Start charge', 'platform': 'renault', 'previous_unique_id': None, @@ -531,7 +531,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:ev-station', + 'original_icon': None, 'original_name': 'Stop charge', 'platform': 'renault', 'previous_unique_id': None, @@ -547,33 +547,33 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'REG-NUMBER Start air conditioner', - 'icon': 'mdi:air-conditioner', }), 'context': , 'entity_id': 'button.reg_number_start_air_conditioner', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'REG-NUMBER Start charge', - 'icon': 'mdi:ev-station', }), 'context': , 'entity_id': 'button.reg_number_start_charge', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'REG-NUMBER Stop charge', - 'icon': 'mdi:ev-station', }), 'context': , 'entity_id': 'button.reg_number_stop_charge', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -635,7 +635,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:air-conditioner', + 'original_icon': None, 'original_name': 'Start air conditioner', 'platform': 'renault', 'previous_unique_id': None, @@ -651,11 +651,11 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'REG-NUMBER Start air conditioner', - 'icon': 'mdi:air-conditioner', }), 'context': , 'entity_id': 'button.reg_number_start_air_conditioner', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -717,7 +717,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:air-conditioner', + 'original_icon': None, 'original_name': 'Start air conditioner', 'platform': 'renault', 'previous_unique_id': None, @@ -748,7 +748,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:ev-station', + 'original_icon': None, 'original_name': 'Start charge', 'platform': 'renault', 'previous_unique_id': None, @@ -779,7 +779,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:ev-station', + 'original_icon': None, 'original_name': 'Stop charge', 'platform': 'renault', 'previous_unique_id': None, @@ -795,33 +795,33 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'REG-NUMBER Start air conditioner', - 'icon': 'mdi:air-conditioner', }), 'context': , 'entity_id': 'button.reg_number_start_air_conditioner', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'REG-NUMBER Start charge', - 'icon': 'mdi:ev-station', }), 'context': , 'entity_id': 'button.reg_number_start_charge', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'REG-NUMBER Stop charge', - 'icon': 'mdi:ev-station', }), 'context': , 'entity_id': 'button.reg_number_stop_charge', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -883,7 +883,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:air-conditioner', + 'original_icon': None, 'original_name': 'Start air conditioner', 'platform': 'renault', 'previous_unique_id': None, @@ -914,7 +914,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:ev-station', + 'original_icon': None, 'original_name': 'Start charge', 'platform': 'renault', 'previous_unique_id': None, @@ -945,7 +945,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:ev-station', + 'original_icon': None, 'original_name': 'Stop charge', 'platform': 'renault', 'previous_unique_id': None, @@ -961,33 +961,33 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'REG-NUMBER Start air conditioner', - 'icon': 'mdi:air-conditioner', }), 'context': , 'entity_id': 'button.reg_number_start_air_conditioner', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'REG-NUMBER Start charge', - 'icon': 'mdi:ev-station', }), 'context': , 'entity_id': 'button.reg_number_start_charge', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'REG-NUMBER Stop charge', - 'icon': 'mdi:ev-station', }), 'context': , 'entity_id': 'button.reg_number_stop_charge', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -1049,7 +1049,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:air-conditioner', + 'original_icon': None, 'original_name': 'Start air conditioner', 'platform': 'renault', 'previous_unique_id': None, @@ -1080,7 +1080,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:ev-station', + 'original_icon': None, 'original_name': 'Start charge', 'platform': 'renault', 'previous_unique_id': None, @@ -1111,7 +1111,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:ev-station', + 'original_icon': None, 'original_name': 'Stop charge', 'platform': 'renault', 'previous_unique_id': None, @@ -1127,33 +1127,33 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'REG-NUMBER Start air conditioner', - 'icon': 'mdi:air-conditioner', }), 'context': , 'entity_id': 'button.reg_number_start_air_conditioner', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'REG-NUMBER Start charge', - 'icon': 'mdi:ev-station', }), 'context': , 'entity_id': 'button.reg_number_start_charge', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'REG-NUMBER Stop charge', - 'icon': 'mdi:ev-station', }), 'context': , 'entity_id': 'button.reg_number_stop_charge', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), diff --git a/tests/components/renault/snapshots/test_device_tracker.ambr b/tests/components/renault/snapshots/test_device_tracker.ambr index 2dd61ce6ace..8fe1713dc0b 100644 --- a/tests/components/renault/snapshots/test_device_tracker.ambr +++ b/tests/components/renault/snapshots/test_device_tracker.ambr @@ -55,7 +55,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:car', + 'original_icon': None, 'original_name': 'Location', 'platform': 'renault', 'previous_unique_id': None, @@ -71,12 +71,12 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'REG-NUMBER Location', - 'icon': 'mdi:car', 'source_type': , }), 'context': , 'entity_id': 'device_tracker.reg_number_location', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -138,7 +138,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:car', + 'original_icon': None, 'original_name': 'Location', 'platform': 'renault', 'previous_unique_id': None, @@ -154,12 +154,12 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'REG-NUMBER Location', - 'icon': 'mdi:car', 'source_type': , }), 'context': , 'entity_id': 'device_tracker.reg_number_location', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -261,7 +261,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:car', + 'original_icon': None, 'original_name': 'Location', 'platform': 'renault', 'previous_unique_id': None, @@ -277,12 +277,12 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'REG-NUMBER Location', - 'icon': 'mdi:car', 'source_type': , }), 'context': , 'entity_id': 'device_tracker.reg_number_location', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -344,7 +344,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:car', + 'original_icon': None, 'original_name': 'Location', 'platform': 'renault', 'previous_unique_id': None, @@ -361,7 +361,6 @@ 'attributes': ReadOnlyDict({ 'friendly_name': 'REG-NUMBER Location', 'gps_accuracy': 0, - 'icon': 'mdi:car', 'latitude': 48.1234567, 'longitude': 11.1234567, 'source_type': , @@ -369,6 +368,7 @@ 'context': , 'entity_id': 'device_tracker.reg_number_location', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'not_home', }), @@ -430,7 +430,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:car', + 'original_icon': None, 'original_name': 'Location', 'platform': 'renault', 'previous_unique_id': None, @@ -447,7 +447,6 @@ 'attributes': ReadOnlyDict({ 'friendly_name': 'REG-NUMBER Location', 'gps_accuracy': 0, - 'icon': 'mdi:car', 'latitude': 48.1234567, 'longitude': 11.1234567, 'source_type': , @@ -455,6 +454,7 @@ 'context': , 'entity_id': 'device_tracker.reg_number_location', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'not_home', }), @@ -556,7 +556,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:car', + 'original_icon': None, 'original_name': 'Location', 'platform': 'renault', 'previous_unique_id': None, @@ -573,7 +573,6 @@ 'attributes': ReadOnlyDict({ 'friendly_name': 'REG-NUMBER Location', 'gps_accuracy': 0, - 'icon': 'mdi:car', 'latitude': 48.1234567, 'longitude': 11.1234567, 'source_type': , @@ -581,6 +580,7 @@ 'context': , 'entity_id': 'device_tracker.reg_number_location', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'not_home', }), diff --git a/tests/components/renault/snapshots/test_select.ambr b/tests/components/renault/snapshots/test_select.ambr index 173afa6bdb9..7e8356ee070 100644 --- a/tests/components/renault/snapshots/test_select.ambr +++ b/tests/components/renault/snapshots/test_select.ambr @@ -101,7 +101,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:calendar-remove', + 'original_icon': None, 'original_name': 'Charge mode', 'platform': 'renault', 'previous_unique_id': None, @@ -117,7 +117,6 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'REG-NUMBER Charge mode', - 'icon': 'mdi:calendar-remove', 'options': list([ 'always', 'always_charging', @@ -127,6 +126,7 @@ 'context': , 'entity_id': 'select.reg_number_charge_mode', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -194,7 +194,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:calendar-remove', + 'original_icon': None, 'original_name': 'Charge mode', 'platform': 'renault', 'previous_unique_id': None, @@ -210,7 +210,6 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'REG-NUMBER Charge mode', - 'icon': 'mdi:calendar-remove', 'options': list([ 'always', 'always_charging', @@ -220,6 +219,7 @@ 'context': , 'entity_id': 'select.reg_number_charge_mode', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -287,7 +287,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:calendar-remove', + 'original_icon': None, 'original_name': 'Charge mode', 'platform': 'renault', 'previous_unique_id': None, @@ -303,7 +303,6 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'REG-NUMBER Charge mode', - 'icon': 'mdi:calendar-remove', 'options': list([ 'always', 'always_charging', @@ -313,6 +312,7 @@ 'context': , 'entity_id': 'select.reg_number_charge_mode', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -420,7 +420,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:calendar-remove', + 'original_icon': None, 'original_name': 'Charge mode', 'platform': 'renault', 'previous_unique_id': None, @@ -436,7 +436,6 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'REG-NUMBER Charge mode', - 'icon': 'mdi:calendar-remove', 'options': list([ 'always', 'always_charging', @@ -446,6 +445,7 @@ 'context': , 'entity_id': 'select.reg_number_charge_mode', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'always', }), @@ -513,7 +513,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:calendar-remove', + 'original_icon': None, 'original_name': 'Charge mode', 'platform': 'renault', 'previous_unique_id': None, @@ -529,7 +529,6 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'REG-NUMBER Charge mode', - 'icon': 'mdi:calendar-remove', 'options': list([ 'always', 'always_charging', @@ -539,6 +538,7 @@ 'context': , 'entity_id': 'select.reg_number_charge_mode', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'always', }), @@ -606,7 +606,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:calendar-clock', + 'original_icon': None, 'original_name': 'Charge mode', 'platform': 'renault', 'previous_unique_id': None, @@ -622,7 +622,6 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'REG-NUMBER Charge mode', - 'icon': 'mdi:calendar-clock', 'options': list([ 'always', 'always_charging', @@ -632,6 +631,7 @@ 'context': , 'entity_id': 'select.reg_number_charge_mode', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'schedule_mode', }), diff --git a/tests/components/renault/snapshots/test_sensor.ambr b/tests/components/renault/snapshots/test_sensor.ambr index 866728eb09b..5909c66bc5c 100644 --- a/tests/components/renault/snapshots/test_sensor.ambr +++ b/tests/components/renault/snapshots/test_sensor.ambr @@ -57,7 +57,7 @@ 'options': dict({ }), 'original_device_class': , - 'original_icon': 'mdi:sign-direction', + 'original_icon': None, 'original_name': 'Mileage', 'platform': 'renault', 'previous_unique_id': None, @@ -90,7 +90,7 @@ 'options': dict({ }), 'original_device_class': , - 'original_icon': 'mdi:gas-station', + 'original_icon': None, 'original_name': 'Fuel autonomy', 'platform': 'renault', 'previous_unique_id': None, @@ -123,7 +123,7 @@ 'options': dict({ }), 'original_device_class': , - 'original_icon': 'mdi:fuel', + 'original_icon': None, 'original_name': 'Fuel quantity', 'platform': 'renault', 'previous_unique_id': None, @@ -233,13 +233,13 @@ 'attributes': ReadOnlyDict({ 'device_class': 'distance', 'friendly_name': 'REG-NUMBER Mileage', - 'icon': 'mdi:sign-direction', 'state_class': , 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.reg_number_mileage', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -247,13 +247,13 @@ 'attributes': ReadOnlyDict({ 'device_class': 'distance', 'friendly_name': 'REG-NUMBER Fuel autonomy', - 'icon': 'mdi:gas-station', 'state_class': , 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.reg_number_fuel_autonomy', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -261,13 +261,13 @@ 'attributes': ReadOnlyDict({ 'device_class': 'volume', 'friendly_name': 'REG-NUMBER Fuel quantity', - 'icon': 'mdi:fuel', 'state_class': , 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.reg_number_fuel_quantity', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -279,6 +279,7 @@ 'context': , 'entity_id': 'sensor.reg_number_last_location_activity', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -289,6 +290,7 @@ 'context': , 'entity_id': 'sensor.reg_number_remote_engine_start', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -299,6 +301,7 @@ 'context': , 'entity_id': 'sensor.reg_number_remote_engine_start_code', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -404,7 +407,7 @@ 'options': dict({ }), 'original_device_class': , - 'original_icon': 'mdi:flash-off', + 'original_icon': None, 'original_name': 'Charge state', 'platform': 'renault', 'previous_unique_id': None, @@ -437,7 +440,7 @@ 'options': dict({ }), 'original_device_class': , - 'original_icon': 'mdi:timer', + 'original_icon': None, 'original_name': 'Charging remaining time', 'platform': 'renault', 'previous_unique_id': None, @@ -508,7 +511,7 @@ 'options': dict({ }), 'original_device_class': , - 'original_icon': 'mdi:power-plug-off', + 'original_icon': None, 'original_name': 'Plug state', 'platform': 'renault', 'previous_unique_id': None, @@ -541,7 +544,7 @@ 'options': dict({ }), 'original_device_class': , - 'original_icon': 'mdi:ev-station', + 'original_icon': None, 'original_name': 'Battery autonomy', 'platform': 'renault', 'previous_unique_id': None, @@ -671,7 +674,7 @@ 'options': dict({ }), 'original_device_class': , - 'original_icon': 'mdi:sign-direction', + 'original_icon': None, 'original_name': 'Mileage', 'platform': 'renault', 'previous_unique_id': None, @@ -704,7 +707,7 @@ 'options': dict({ }), 'original_device_class': , - 'original_icon': 'mdi:gas-station', + 'original_icon': None, 'original_name': 'Fuel autonomy', 'platform': 'renault', 'previous_unique_id': None, @@ -737,7 +740,7 @@ 'options': dict({ }), 'original_device_class': , - 'original_icon': 'mdi:fuel', + 'original_icon': None, 'original_name': 'Fuel quantity', 'platform': 'renault', 'previous_unique_id': None, @@ -853,6 +856,7 @@ 'context': , 'entity_id': 'sensor.reg_number_battery', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -860,7 +864,6 @@ 'attributes': ReadOnlyDict({ 'device_class': 'enum', 'friendly_name': 'REG-NUMBER Charge state', - 'icon': 'mdi:flash-off', 'options': list([ 'not_in_charge', 'waiting_for_a_planned_charge', @@ -875,6 +878,7 @@ 'context': , 'entity_id': 'sensor.reg_number_charge_state', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -882,13 +886,13 @@ 'attributes': ReadOnlyDict({ 'device_class': 'duration', 'friendly_name': 'REG-NUMBER Charging remaining time', - 'icon': 'mdi:timer', 'state_class': , 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.reg_number_charging_remaining_time', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -902,6 +906,7 @@ 'context': , 'entity_id': 'sensor.reg_number_admissible_charging_power', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -909,7 +914,6 @@ 'attributes': ReadOnlyDict({ 'device_class': 'enum', 'friendly_name': 'REG-NUMBER Plug state', - 'icon': 'mdi:power-plug-off', 'options': list([ 'unplugged', 'plugged', @@ -920,6 +924,7 @@ 'context': , 'entity_id': 'sensor.reg_number_plug_state', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -927,13 +932,13 @@ 'attributes': ReadOnlyDict({ 'device_class': 'distance', 'friendly_name': 'REG-NUMBER Battery autonomy', - 'icon': 'mdi:ev-station', 'state_class': , 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.reg_number_battery_autonomy', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -947,6 +952,7 @@ 'context': , 'entity_id': 'sensor.reg_number_battery_available_energy', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -960,6 +966,7 @@ 'context': , 'entity_id': 'sensor.reg_number_battery_temperature', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -971,6 +978,7 @@ 'context': , 'entity_id': 'sensor.reg_number_last_battery_activity', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -978,13 +986,13 @@ 'attributes': ReadOnlyDict({ 'device_class': 'distance', 'friendly_name': 'REG-NUMBER Mileage', - 'icon': 'mdi:sign-direction', 'state_class': , 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.reg_number_mileage', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -992,13 +1000,13 @@ 'attributes': ReadOnlyDict({ 'device_class': 'distance', 'friendly_name': 'REG-NUMBER Fuel autonomy', - 'icon': 'mdi:gas-station', 'state_class': , 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.reg_number_fuel_autonomy', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -1006,13 +1014,13 @@ 'attributes': ReadOnlyDict({ 'device_class': 'volume', 'friendly_name': 'REG-NUMBER Fuel quantity', - 'icon': 'mdi:fuel', 'state_class': , 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.reg_number_fuel_quantity', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -1024,6 +1032,7 @@ 'context': , 'entity_id': 'sensor.reg_number_last_location_activity', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -1034,6 +1043,7 @@ 'context': , 'entity_id': 'sensor.reg_number_remote_engine_start', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -1044,6 +1054,7 @@ 'context': , 'entity_id': 'sensor.reg_number_remote_engine_start_code', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -1149,7 +1160,7 @@ 'options': dict({ }), 'original_device_class': , - 'original_icon': 'mdi:flash-off', + 'original_icon': None, 'original_name': 'Charge state', 'platform': 'renault', 'previous_unique_id': None, @@ -1182,7 +1193,7 @@ 'options': dict({ }), 'original_device_class': , - 'original_icon': 'mdi:timer', + 'original_icon': None, 'original_name': 'Charging remaining time', 'platform': 'renault', 'previous_unique_id': None, @@ -1253,7 +1264,7 @@ 'options': dict({ }), 'original_device_class': , - 'original_icon': 'mdi:power-plug-off', + 'original_icon': None, 'original_name': 'Plug state', 'platform': 'renault', 'previous_unique_id': None, @@ -1286,7 +1297,7 @@ 'options': dict({ }), 'original_device_class': , - 'original_icon': 'mdi:ev-station', + 'original_icon': None, 'original_name': 'Battery autonomy', 'platform': 'renault', 'previous_unique_id': None, @@ -1416,7 +1427,7 @@ 'options': dict({ }), 'original_device_class': , - 'original_icon': 'mdi:sign-direction', + 'original_icon': None, 'original_name': 'Mileage', 'platform': 'renault', 'previous_unique_id': None, @@ -1596,6 +1607,7 @@ 'context': , 'entity_id': 'sensor.reg_number_battery', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -1603,7 +1615,6 @@ 'attributes': ReadOnlyDict({ 'device_class': 'enum', 'friendly_name': 'REG-NUMBER Charge state', - 'icon': 'mdi:flash-off', 'options': list([ 'not_in_charge', 'waiting_for_a_planned_charge', @@ -1618,6 +1629,7 @@ 'context': , 'entity_id': 'sensor.reg_number_charge_state', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -1625,13 +1637,13 @@ 'attributes': ReadOnlyDict({ 'device_class': 'duration', 'friendly_name': 'REG-NUMBER Charging remaining time', - 'icon': 'mdi:timer', 'state_class': , 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.reg_number_charging_remaining_time', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -1645,6 +1657,7 @@ 'context': , 'entity_id': 'sensor.reg_number_charging_power', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -1652,7 +1665,6 @@ 'attributes': ReadOnlyDict({ 'device_class': 'enum', 'friendly_name': 'REG-NUMBER Plug state', - 'icon': 'mdi:power-plug-off', 'options': list([ 'unplugged', 'plugged', @@ -1663,6 +1675,7 @@ 'context': , 'entity_id': 'sensor.reg_number_plug_state', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -1670,13 +1683,13 @@ 'attributes': ReadOnlyDict({ 'device_class': 'distance', 'friendly_name': 'REG-NUMBER Battery autonomy', - 'icon': 'mdi:ev-station', 'state_class': , 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.reg_number_battery_autonomy', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -1690,6 +1703,7 @@ 'context': , 'entity_id': 'sensor.reg_number_battery_available_energy', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -1703,6 +1717,7 @@ 'context': , 'entity_id': 'sensor.reg_number_battery_temperature', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -1714,6 +1729,7 @@ 'context': , 'entity_id': 'sensor.reg_number_last_battery_activity', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -1721,13 +1737,13 @@ 'attributes': ReadOnlyDict({ 'device_class': 'distance', 'friendly_name': 'REG-NUMBER Mileage', - 'icon': 'mdi:sign-direction', 'state_class': , 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.reg_number_mileage', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -1741,6 +1757,7 @@ 'context': , 'entity_id': 'sensor.reg_number_outside_temperature', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -1752,6 +1769,7 @@ 'context': , 'entity_id': 'sensor.reg_number_hvac_soc_threshold', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -1763,6 +1781,7 @@ 'context': , 'entity_id': 'sensor.reg_number_last_hvac_activity', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -1773,6 +1792,7 @@ 'context': , 'entity_id': 'sensor.reg_number_remote_engine_start', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -1783,6 +1803,7 @@ 'context': , 'entity_id': 'sensor.reg_number_remote_engine_start_code', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -1888,7 +1909,7 @@ 'options': dict({ }), 'original_device_class': , - 'original_icon': 'mdi:flash-off', + 'original_icon': None, 'original_name': 'Charge state', 'platform': 'renault', 'previous_unique_id': None, @@ -1921,7 +1942,7 @@ 'options': dict({ }), 'original_device_class': , - 'original_icon': 'mdi:timer', + 'original_icon': None, 'original_name': 'Charging remaining time', 'platform': 'renault', 'previous_unique_id': None, @@ -1992,7 +2013,7 @@ 'options': dict({ }), 'original_device_class': , - 'original_icon': 'mdi:power-plug-off', + 'original_icon': None, 'original_name': 'Plug state', 'platform': 'renault', 'previous_unique_id': None, @@ -2025,7 +2046,7 @@ 'options': dict({ }), 'original_device_class': , - 'original_icon': 'mdi:ev-station', + 'original_icon': None, 'original_name': 'Battery autonomy', 'platform': 'renault', 'previous_unique_id': None, @@ -2155,7 +2176,7 @@ 'options': dict({ }), 'original_device_class': , - 'original_icon': 'mdi:sign-direction', + 'original_icon': None, 'original_name': 'Mileage', 'platform': 'renault', 'previous_unique_id': None, @@ -2366,6 +2387,7 @@ 'context': , 'entity_id': 'sensor.reg_number_battery', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -2373,7 +2395,6 @@ 'attributes': ReadOnlyDict({ 'device_class': 'enum', 'friendly_name': 'REG-NUMBER Charge state', - 'icon': 'mdi:flash-off', 'options': list([ 'not_in_charge', 'waiting_for_a_planned_charge', @@ -2388,6 +2409,7 @@ 'context': , 'entity_id': 'sensor.reg_number_charge_state', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -2395,13 +2417,13 @@ 'attributes': ReadOnlyDict({ 'device_class': 'duration', 'friendly_name': 'REG-NUMBER Charging remaining time', - 'icon': 'mdi:timer', 'state_class': , 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.reg_number_charging_remaining_time', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -2415,6 +2437,7 @@ 'context': , 'entity_id': 'sensor.reg_number_admissible_charging_power', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -2422,7 +2445,6 @@ 'attributes': ReadOnlyDict({ 'device_class': 'enum', 'friendly_name': 'REG-NUMBER Plug state', - 'icon': 'mdi:power-plug-off', 'options': list([ 'unplugged', 'plugged', @@ -2433,6 +2455,7 @@ 'context': , 'entity_id': 'sensor.reg_number_plug_state', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -2440,13 +2463,13 @@ 'attributes': ReadOnlyDict({ 'device_class': 'distance', 'friendly_name': 'REG-NUMBER Battery autonomy', - 'icon': 'mdi:ev-station', 'state_class': , 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.reg_number_battery_autonomy', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -2460,6 +2483,7 @@ 'context': , 'entity_id': 'sensor.reg_number_battery_available_energy', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -2473,6 +2497,7 @@ 'context': , 'entity_id': 'sensor.reg_number_battery_temperature', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -2484,6 +2509,7 @@ 'context': , 'entity_id': 'sensor.reg_number_last_battery_activity', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -2491,13 +2517,13 @@ 'attributes': ReadOnlyDict({ 'device_class': 'distance', 'friendly_name': 'REG-NUMBER Mileage', - 'icon': 'mdi:sign-direction', 'state_class': , 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.reg_number_mileage', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -2511,6 +2537,7 @@ 'context': , 'entity_id': 'sensor.reg_number_outside_temperature', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -2522,6 +2549,7 @@ 'context': , 'entity_id': 'sensor.reg_number_hvac_soc_threshold', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -2533,6 +2561,7 @@ 'context': , 'entity_id': 'sensor.reg_number_last_hvac_activity', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -2544,6 +2573,7 @@ 'context': , 'entity_id': 'sensor.reg_number_last_location_activity', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -2554,6 +2584,7 @@ 'context': , 'entity_id': 'sensor.reg_number_remote_engine_start', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -2564,6 +2595,7 @@ 'context': , 'entity_id': 'sensor.reg_number_remote_engine_start_code', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -2627,7 +2659,7 @@ 'options': dict({ }), 'original_device_class': , - 'original_icon': 'mdi:sign-direction', + 'original_icon': None, 'original_name': 'Mileage', 'platform': 'renault', 'previous_unique_id': None, @@ -2660,7 +2692,7 @@ 'options': dict({ }), 'original_device_class': , - 'original_icon': 'mdi:gas-station', + 'original_icon': None, 'original_name': 'Fuel autonomy', 'platform': 'renault', 'previous_unique_id': None, @@ -2693,7 +2725,7 @@ 'options': dict({ }), 'original_device_class': , - 'original_icon': 'mdi:fuel', + 'original_icon': None, 'original_name': 'Fuel quantity', 'platform': 'renault', 'previous_unique_id': None, @@ -2803,13 +2835,13 @@ 'attributes': ReadOnlyDict({ 'device_class': 'distance', 'friendly_name': 'REG-NUMBER Mileage', - 'icon': 'mdi:sign-direction', 'state_class': , 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.reg_number_mileage', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '5567', }), @@ -2817,13 +2849,13 @@ 'attributes': ReadOnlyDict({ 'device_class': 'distance', 'friendly_name': 'REG-NUMBER Fuel autonomy', - 'icon': 'mdi:gas-station', 'state_class': , 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.reg_number_fuel_autonomy', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '35', }), @@ -2831,13 +2863,13 @@ 'attributes': ReadOnlyDict({ 'device_class': 'volume', 'friendly_name': 'REG-NUMBER Fuel quantity', - 'icon': 'mdi:fuel', 'state_class': , 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.reg_number_fuel_quantity', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '3', }), @@ -2849,6 +2881,7 @@ 'context': , 'entity_id': 'sensor.reg_number_last_location_activity', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '2020-02-18T16:58:38+00:00', }), @@ -2859,6 +2892,7 @@ 'context': , 'entity_id': 'sensor.reg_number_remote_engine_start', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'Stopped, ready for RES', }), @@ -2869,6 +2903,7 @@ 'context': , 'entity_id': 'sensor.reg_number_remote_engine_start_code', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '10', }), @@ -2974,7 +3009,7 @@ 'options': dict({ }), 'original_device_class': , - 'original_icon': 'mdi:flash', + 'original_icon': None, 'original_name': 'Charge state', 'platform': 'renault', 'previous_unique_id': None, @@ -3007,7 +3042,7 @@ 'options': dict({ }), 'original_device_class': , - 'original_icon': 'mdi:timer', + 'original_icon': None, 'original_name': 'Charging remaining time', 'platform': 'renault', 'previous_unique_id': None, @@ -3078,7 +3113,7 @@ 'options': dict({ }), 'original_device_class': , - 'original_icon': 'mdi:power-plug', + 'original_icon': None, 'original_name': 'Plug state', 'platform': 'renault', 'previous_unique_id': None, @@ -3111,7 +3146,7 @@ 'options': dict({ }), 'original_device_class': , - 'original_icon': 'mdi:ev-station', + 'original_icon': None, 'original_name': 'Battery autonomy', 'platform': 'renault', 'previous_unique_id': None, @@ -3241,7 +3276,7 @@ 'options': dict({ }), 'original_device_class': , - 'original_icon': 'mdi:sign-direction', + 'original_icon': None, 'original_name': 'Mileage', 'platform': 'renault', 'previous_unique_id': None, @@ -3274,7 +3309,7 @@ 'options': dict({ }), 'original_device_class': , - 'original_icon': 'mdi:gas-station', + 'original_icon': None, 'original_name': 'Fuel autonomy', 'platform': 'renault', 'previous_unique_id': None, @@ -3307,7 +3342,7 @@ 'options': dict({ }), 'original_device_class': , - 'original_icon': 'mdi:fuel', + 'original_icon': None, 'original_name': 'Fuel quantity', 'platform': 'renault', 'previous_unique_id': None, @@ -3423,6 +3458,7 @@ 'context': , 'entity_id': 'sensor.reg_number_battery', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '60', }), @@ -3430,7 +3466,6 @@ 'attributes': ReadOnlyDict({ 'device_class': 'enum', 'friendly_name': 'REG-NUMBER Charge state', - 'icon': 'mdi:flash', 'options': list([ 'not_in_charge', 'waiting_for_a_planned_charge', @@ -3445,6 +3480,7 @@ 'context': , 'entity_id': 'sensor.reg_number_charge_state', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'charge_in_progress', }), @@ -3452,13 +3488,13 @@ 'attributes': ReadOnlyDict({ 'device_class': 'duration', 'friendly_name': 'REG-NUMBER Charging remaining time', - 'icon': 'mdi:timer', 'state_class': , 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.reg_number_charging_remaining_time', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '145', }), @@ -3472,6 +3508,7 @@ 'context': , 'entity_id': 'sensor.reg_number_admissible_charging_power', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '27.0', }), @@ -3479,7 +3516,6 @@ 'attributes': ReadOnlyDict({ 'device_class': 'enum', 'friendly_name': 'REG-NUMBER Plug state', - 'icon': 'mdi:power-plug', 'options': list([ 'unplugged', 'plugged', @@ -3490,6 +3526,7 @@ 'context': , 'entity_id': 'sensor.reg_number_plug_state', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'plugged', }), @@ -3497,13 +3534,13 @@ 'attributes': ReadOnlyDict({ 'device_class': 'distance', 'friendly_name': 'REG-NUMBER Battery autonomy', - 'icon': 'mdi:ev-station', 'state_class': , 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.reg_number_battery_autonomy', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '141', }), @@ -3517,6 +3554,7 @@ 'context': , 'entity_id': 'sensor.reg_number_battery_available_energy', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '31', }), @@ -3530,6 +3568,7 @@ 'context': , 'entity_id': 'sensor.reg_number_battery_temperature', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '20', }), @@ -3541,6 +3580,7 @@ 'context': , 'entity_id': 'sensor.reg_number_last_battery_activity', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '2020-01-12T21:40:16+00:00', }), @@ -3548,13 +3588,13 @@ 'attributes': ReadOnlyDict({ 'device_class': 'distance', 'friendly_name': 'REG-NUMBER Mileage', - 'icon': 'mdi:sign-direction', 'state_class': , 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.reg_number_mileage', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '5567', }), @@ -3562,13 +3602,13 @@ 'attributes': ReadOnlyDict({ 'device_class': 'distance', 'friendly_name': 'REG-NUMBER Fuel autonomy', - 'icon': 'mdi:gas-station', 'state_class': , 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.reg_number_fuel_autonomy', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '35', }), @@ -3576,13 +3616,13 @@ 'attributes': ReadOnlyDict({ 'device_class': 'volume', 'friendly_name': 'REG-NUMBER Fuel quantity', - 'icon': 'mdi:fuel', 'state_class': , 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.reg_number_fuel_quantity', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '3', }), @@ -3594,6 +3634,7 @@ 'context': , 'entity_id': 'sensor.reg_number_last_location_activity', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '2020-02-18T16:58:38+00:00', }), @@ -3604,6 +3645,7 @@ 'context': , 'entity_id': 'sensor.reg_number_remote_engine_start', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'Stopped, ready for RES', }), @@ -3614,6 +3656,7 @@ 'context': , 'entity_id': 'sensor.reg_number_remote_engine_start_code', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '10', }), @@ -3719,7 +3762,7 @@ 'options': dict({ }), 'original_device_class': , - 'original_icon': 'mdi:flash', + 'original_icon': None, 'original_name': 'Charge state', 'platform': 'renault', 'previous_unique_id': None, @@ -3752,7 +3795,7 @@ 'options': dict({ }), 'original_device_class': , - 'original_icon': 'mdi:timer', + 'original_icon': None, 'original_name': 'Charging remaining time', 'platform': 'renault', 'previous_unique_id': None, @@ -3823,7 +3866,7 @@ 'options': dict({ }), 'original_device_class': , - 'original_icon': 'mdi:power-plug', + 'original_icon': None, 'original_name': 'Plug state', 'platform': 'renault', 'previous_unique_id': None, @@ -3856,7 +3899,7 @@ 'options': dict({ }), 'original_device_class': , - 'original_icon': 'mdi:ev-station', + 'original_icon': None, 'original_name': 'Battery autonomy', 'platform': 'renault', 'previous_unique_id': None, @@ -3986,7 +4029,7 @@ 'options': dict({ }), 'original_device_class': , - 'original_icon': 'mdi:sign-direction', + 'original_icon': None, 'original_name': 'Mileage', 'platform': 'renault', 'previous_unique_id': None, @@ -4166,6 +4209,7 @@ 'context': , 'entity_id': 'sensor.reg_number_battery', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '60', }), @@ -4173,7 +4217,6 @@ 'attributes': ReadOnlyDict({ 'device_class': 'enum', 'friendly_name': 'REG-NUMBER Charge state', - 'icon': 'mdi:flash', 'options': list([ 'not_in_charge', 'waiting_for_a_planned_charge', @@ -4188,6 +4231,7 @@ 'context': , 'entity_id': 'sensor.reg_number_charge_state', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'charge_in_progress', }), @@ -4195,13 +4239,13 @@ 'attributes': ReadOnlyDict({ 'device_class': 'duration', 'friendly_name': 'REG-NUMBER Charging remaining time', - 'icon': 'mdi:timer', 'state_class': , 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.reg_number_charging_remaining_time', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '145', }), @@ -4215,6 +4259,7 @@ 'context': , 'entity_id': 'sensor.reg_number_charging_power', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0.027', }), @@ -4222,7 +4267,6 @@ 'attributes': ReadOnlyDict({ 'device_class': 'enum', 'friendly_name': 'REG-NUMBER Plug state', - 'icon': 'mdi:power-plug', 'options': list([ 'unplugged', 'plugged', @@ -4233,6 +4277,7 @@ 'context': , 'entity_id': 'sensor.reg_number_plug_state', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'plugged', }), @@ -4240,13 +4285,13 @@ 'attributes': ReadOnlyDict({ 'device_class': 'distance', 'friendly_name': 'REG-NUMBER Battery autonomy', - 'icon': 'mdi:ev-station', 'state_class': , 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.reg_number_battery_autonomy', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '141', }), @@ -4260,6 +4305,7 @@ 'context': , 'entity_id': 'sensor.reg_number_battery_available_energy', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '31', }), @@ -4273,6 +4319,7 @@ 'context': , 'entity_id': 'sensor.reg_number_battery_temperature', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '20', }), @@ -4284,6 +4331,7 @@ 'context': , 'entity_id': 'sensor.reg_number_last_battery_activity', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '2020-01-12T21:40:16+00:00', }), @@ -4291,13 +4339,13 @@ 'attributes': ReadOnlyDict({ 'device_class': 'distance', 'friendly_name': 'REG-NUMBER Mileage', - 'icon': 'mdi:sign-direction', 'state_class': , 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.reg_number_mileage', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '49114', }), @@ -4311,6 +4359,7 @@ 'context': , 'entity_id': 'sensor.reg_number_outside_temperature', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '8.0', }), @@ -4322,6 +4371,7 @@ 'context': , 'entity_id': 'sensor.reg_number_hvac_soc_threshold', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -4333,6 +4383,7 @@ 'context': , 'entity_id': 'sensor.reg_number_last_hvac_activity', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -4343,6 +4394,7 @@ 'context': , 'entity_id': 'sensor.reg_number_remote_engine_start', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -4353,6 +4405,7 @@ 'context': , 'entity_id': 'sensor.reg_number_remote_engine_start_code', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -4458,7 +4511,7 @@ 'options': dict({ }), 'original_device_class': , - 'original_icon': 'mdi:flash-off', + 'original_icon': None, 'original_name': 'Charge state', 'platform': 'renault', 'previous_unique_id': None, @@ -4491,7 +4544,7 @@ 'options': dict({ }), 'original_device_class': , - 'original_icon': 'mdi:timer', + 'original_icon': None, 'original_name': 'Charging remaining time', 'platform': 'renault', 'previous_unique_id': None, @@ -4562,7 +4615,7 @@ 'options': dict({ }), 'original_device_class': , - 'original_icon': 'mdi:power-plug-off', + 'original_icon': None, 'original_name': 'Plug state', 'platform': 'renault', 'previous_unique_id': None, @@ -4595,7 +4648,7 @@ 'options': dict({ }), 'original_device_class': , - 'original_icon': 'mdi:ev-station', + 'original_icon': None, 'original_name': 'Battery autonomy', 'platform': 'renault', 'previous_unique_id': None, @@ -4725,7 +4778,7 @@ 'options': dict({ }), 'original_device_class': , - 'original_icon': 'mdi:sign-direction', + 'original_icon': None, 'original_name': 'Mileage', 'platform': 'renault', 'previous_unique_id': None, @@ -4936,6 +4989,7 @@ 'context': , 'entity_id': 'sensor.reg_number_battery', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '50', }), @@ -4943,7 +4997,6 @@ 'attributes': ReadOnlyDict({ 'device_class': 'enum', 'friendly_name': 'REG-NUMBER Charge state', - 'icon': 'mdi:flash-off', 'options': list([ 'not_in_charge', 'waiting_for_a_planned_charge', @@ -4958,6 +5011,7 @@ 'context': , 'entity_id': 'sensor.reg_number_charge_state', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'charge_error', }), @@ -4965,13 +5019,13 @@ 'attributes': ReadOnlyDict({ 'device_class': 'duration', 'friendly_name': 'REG-NUMBER Charging remaining time', - 'icon': 'mdi:timer', 'state_class': , 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.reg_number_charging_remaining_time', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -4985,6 +5039,7 @@ 'context': , 'entity_id': 'sensor.reg_number_admissible_charging_power', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -4992,7 +5047,6 @@ 'attributes': ReadOnlyDict({ 'device_class': 'enum', 'friendly_name': 'REG-NUMBER Plug state', - 'icon': 'mdi:power-plug-off', 'options': list([ 'unplugged', 'plugged', @@ -5003,6 +5057,7 @@ 'context': , 'entity_id': 'sensor.reg_number_plug_state', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unplugged', }), @@ -5010,13 +5065,13 @@ 'attributes': ReadOnlyDict({ 'device_class': 'distance', 'friendly_name': 'REG-NUMBER Battery autonomy', - 'icon': 'mdi:ev-station', 'state_class': , 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.reg_number_battery_autonomy', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '128', }), @@ -5030,6 +5085,7 @@ 'context': , 'entity_id': 'sensor.reg_number_battery_available_energy', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0', }), @@ -5043,6 +5099,7 @@ 'context': , 'entity_id': 'sensor.reg_number_battery_temperature', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -5054,6 +5111,7 @@ 'context': , 'entity_id': 'sensor.reg_number_last_battery_activity', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '2020-11-17T08:06:48+00:00', }), @@ -5061,13 +5119,13 @@ 'attributes': ReadOnlyDict({ 'device_class': 'distance', 'friendly_name': 'REG-NUMBER Mileage', - 'icon': 'mdi:sign-direction', 'state_class': , 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.reg_number_mileage', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '49114', }), @@ -5081,6 +5139,7 @@ 'context': , 'entity_id': 'sensor.reg_number_outside_temperature', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), @@ -5092,6 +5151,7 @@ 'context': , 'entity_id': 'sensor.reg_number_hvac_soc_threshold', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '30.0', }), @@ -5103,6 +5163,7 @@ 'context': , 'entity_id': 'sensor.reg_number_last_hvac_activity', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '2020-12-03T00:00:00+00:00', }), @@ -5114,6 +5175,7 @@ 'context': , 'entity_id': 'sensor.reg_number_last_location_activity', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '2020-02-18T16:58:38+00:00', }), @@ -5124,6 +5186,7 @@ 'context': , 'entity_id': 'sensor.reg_number_remote_engine_start', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'Stopped, ready for RES', }), @@ -5134,6 +5197,7 @@ 'context': , 'entity_id': 'sensor.reg_number_remote_engine_start_code', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '10', }), diff --git a/tests/components/renault/test_binary_sensor.py b/tests/components/renault/test_binary_sensor.py index f1e3511dc2c..7a0d593a4c4 100644 --- a/tests/components/renault/test_binary_sensor.py +++ b/tests/components/renault/test_binary_sensor.py @@ -1,4 +1,5 @@ """Tests for Renault binary sensors.""" + from collections.abc import Generator from unittest.mock import patch diff --git a/tests/components/renault/test_button.py b/tests/components/renault/test_button.py index 47a411ce791..d592f040c97 100644 --- a/tests/components/renault/test_button.py +++ b/tests/components/renault/test_button.py @@ -1,4 +1,5 @@ """Tests for Renault sensors.""" + from collections.abc import Generator from unittest.mock import patch diff --git a/tests/components/renault/test_config_flow.py b/tests/components/renault/test_config_flow.py index 5d933c03c65..eca7991a27c 100644 --- a/tests/components/renault/test_config_flow.py +++ b/tests/components/renault/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Renault config flow.""" + from unittest.mock import AsyncMock, PropertyMock, patch import pytest @@ -60,11 +61,15 @@ async def test_config_flow_single_account( ) # Account list single - with patch("renault_api.renault_session.RenaultSession.login"), patch( - "renault_api.renault_account.RenaultAccount.account_id", return_value="123" - ), patch( - "renault_api.renault_client.RenaultClient.get_api_accounts", - return_value=[renault_account], + with ( + patch("renault_api.renault_session.RenaultSession.login"), + patch( + "renault_api.renault_account.RenaultAccount.account_id", return_value="123" + ), + patch( + "renault_api.renault_client.RenaultClient.get_api_accounts", + return_value=[renault_account], + ), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -96,9 +101,12 @@ async def test_config_flow_no_account( assert result["errors"] == {} # Account list empty - with patch("renault_api.renault_session.RenaultSession.login"), patch( - "renault_api.renault_client.RenaultClient.get_api_accounts", - return_value=[], + with ( + patch("renault_api.renault_session.RenaultSession.login"), + patch( + "renault_api.renault_client.RenaultClient.get_api_accounts", + return_value=[], + ), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -135,10 +143,14 @@ async def test_config_flow_multiple_accounts( ) # Multiple accounts - with patch("renault_api.renault_session.RenaultSession.login"), patch( - "renault_api.renault_client.RenaultClient.get_api_accounts", - return_value=[renault_account_1, renault_account_2], - ), patch("renault_api.renault_account.RenaultAccount.get_vehicles"): + with ( + patch("renault_api.renault_session.RenaultSession.login"), + patch( + "renault_api.renault_client.RenaultClient.get_api_accounts", + return_value=[renault_account_1, renault_account_2], + ), + patch("renault_api.renault_account.RenaultAccount.get_vehicles"), + ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={ @@ -183,10 +195,14 @@ async def test_config_flow_duplicate( "account_id_1", websession=aiohttp_client.async_get_clientsession(hass), ) - with patch("renault_api.renault_session.RenaultSession.login"), patch( - "renault_api.renault_client.RenaultClient.get_api_accounts", - return_value=[renault_account], - ), patch("renault_api.renault_account.RenaultAccount.get_vehicles"): + with ( + patch("renault_api.renault_session.RenaultSession.login"), + patch( + "renault_api.renault_client.RenaultClient.get_api_accounts", + return_value=[renault_account], + ), + patch("renault_api.renault_account.RenaultAccount.get_vehicles"), + ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={ diff --git a/tests/components/renault/test_device_tracker.py b/tests/components/renault/test_device_tracker.py index a551d2df986..a809ce82e6e 100644 --- a/tests/components/renault/test_device_tracker.py +++ b/tests/components/renault/test_device_tracker.py @@ -1,4 +1,5 @@ """Tests for Renault sensors.""" + from collections.abc import Generator from unittest.mock import patch diff --git a/tests/components/renault/test_diagnostics.py b/tests/components/renault/test_diagnostics.py index 76ea88b4b45..3c8c1c7449e 100644 --- a/tests/components/renault/test_diagnostics.py +++ b/tests/components/renault/test_diagnostics.py @@ -1,4 +1,5 @@ """Test Renault diagnostics.""" + import pytest from homeassistant.components.diagnostics import REDACTED diff --git a/tests/components/renault/test_init.py b/tests/components/renault/test_init.py index 0f26bf6fbdb..6f222c760a7 100644 --- a/tests/components/renault/test_init.py +++ b/tests/components/renault/test_init.py @@ -1,4 +1,5 @@ """Tests for Renault setup process.""" + from collections.abc import Generator from typing import Any from unittest.mock import Mock, patch diff --git a/tests/components/renault/test_select.py b/tests/components/renault/test_select.py index f170dec6c4a..5dcd798def2 100644 --- a/tests/components/renault/test_select.py +++ b/tests/components/renault/test_select.py @@ -1,4 +1,5 @@ """Tests for Renault selects.""" + from collections.abc import Generator from unittest.mock import patch diff --git a/tests/components/renault/test_sensor.py b/tests/components/renault/test_sensor.py index fb61f31ec44..bd94aa8d8e1 100644 --- a/tests/components/renault/test_sensor.py +++ b/tests/components/renault/test_sensor.py @@ -1,4 +1,5 @@ """Tests for Renault sensors.""" + from collections.abc import Generator from unittest.mock import patch @@ -49,7 +50,7 @@ async def test_sensors( # Some entities are disabled, enable them and reload before checking states for ent in entity_entries: - entity_registry.async_update_entity(ent.entity_id, **{"disabled_by": None}) + entity_registry.async_update_entity(ent.entity_id, disabled_by=None) await hass.config_entries.async_reload(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/renault/test_services.py b/tests/components/renault/test_services.py index 7f5cb9a8184..e97988a09f7 100644 --- a/tests/components/renault/test_services.py +++ b/tests/components/renault/test_services.py @@ -1,4 +1,5 @@ """Tests for Renault sensors.""" + from collections.abc import Generator from datetime import datetime from unittest.mock import patch @@ -89,10 +90,13 @@ async def test_service_set_ac_cancel( ATTR_VEHICLE: get_device_id(hass), } - with patch( - "renault_api.renault_vehicle.RenaultVehicle.set_ac_stop", - side_effect=RenaultException("Didn't work"), - ) as mock_action, pytest.raises(HomeAssistantError, match="Didn't work"): + with ( + patch( + "renault_api.renault_vehicle.RenaultVehicle.set_ac_stop", + side_effect=RenaultException("Didn't work"), + ) as mock_action, + pytest.raises(HomeAssistantError, match="Didn't work"), + ): await hass.services.async_call( DOMAIN, SERVICE_AC_CANCEL, service_data=data, blocking=True ) @@ -171,19 +175,22 @@ async def test_service_set_charge_schedule( ATTR_SCHEDULES: schedules, } - with patch( - "renault_api.renault_vehicle.RenaultVehicle.get_charging_settings", - return_value=schemas.KamereonVehicleDataResponseSchema.loads( - load_fixture("renault/charging_settings.json") - ).get_attributes(schemas.KamereonVehicleChargingSettingsDataSchema), - ), patch( - "renault_api.renault_vehicle.RenaultVehicle.set_charge_schedules", - return_value=( - schemas.KamereonVehicleHvacStartActionDataSchema.loads( - load_fixture("renault/action.set_charge_schedules.json") - ) + with ( + patch( + "renault_api.renault_vehicle.RenaultVehicle.get_charging_settings", + return_value=schemas.KamereonVehicleDataResponseSchema.loads( + load_fixture("renault/charging_settings.json") + ).get_attributes(schemas.KamereonVehicleChargingSettingsDataSchema), ), - ) as mock_action: + patch( + "renault_api.renault_vehicle.RenaultVehicle.set_charge_schedules", + return_value=( + schemas.KamereonVehicleHvacStartActionDataSchema.loads( + load_fixture("renault/action.set_charge_schedules.json") + ) + ), + ) as mock_action, + ): await hass.services.async_call( DOMAIN, SERVICE_CHARGE_SET_SCHEDULES, service_data=data, blocking=True ) @@ -217,19 +224,22 @@ async def test_service_set_charge_schedule_multi( ATTR_SCHEDULES: schedules, } - with patch( - "renault_api.renault_vehicle.RenaultVehicle.get_charging_settings", - return_value=schemas.KamereonVehicleDataResponseSchema.loads( - load_fixture("renault/charging_settings.json") - ).get_attributes(schemas.KamereonVehicleChargingSettingsDataSchema), - ), patch( - "renault_api.renault_vehicle.RenaultVehicle.set_charge_schedules", - return_value=( - schemas.KamereonVehicleHvacStartActionDataSchema.loads( - load_fixture("renault/action.set_charge_schedules.json") - ) + with ( + patch( + "renault_api.renault_vehicle.RenaultVehicle.get_charging_settings", + return_value=schemas.KamereonVehicleDataResponseSchema.loads( + load_fixture("renault/charging_settings.json") + ).get_attributes(schemas.KamereonVehicleChargingSettingsDataSchema), ), - ) as mock_action: + patch( + "renault_api.renault_vehicle.RenaultVehicle.set_charge_schedules", + return_value=( + schemas.KamereonVehicleHvacStartActionDataSchema.loads( + load_fixture("renault/action.set_charge_schedules.json") + ) + ), + ) as mock_action, + ): await hass.services.async_call( DOMAIN, SERVICE_CHARGE_SET_SCHEDULES, service_data=data, blocking=True ) diff --git a/tests/components/renson/test_config_flow.py b/tests/components/renson/test_config_flow.py index 578c6125427..6d51605824e 100644 --- a/tests/components/renson/test_config_flow.py +++ b/tests/components/renson/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Renson config flow.""" + from unittest.mock import patch from homeassistant import config_entries @@ -15,13 +16,16 @@ async def test_form(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.FORM assert result["errors"] is None - with patch( - "homeassistant.components.renson.config_flow.renson", - return_value={"title": "Renson"}, - ), patch( - "homeassistant.components.renson.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.renson.config_flow.renson", + return_value={"title": "Renson"}, + ), + patch( + "homeassistant.components.renson.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { diff --git a/tests/components/reolink/conftest.py b/tests/components/reolink/conftest.py index 948ec41f551..5fd52b97b6b 100644 --- a/tests/components/reolink/conftest.py +++ b/tests/components/reolink/conftest.py @@ -1,4 +1,5 @@ """Setup the Reolink tests.""" + from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch @@ -50,12 +51,15 @@ def reolink_connect_class( mock_get_source_ip: None, ) -> Generator[MagicMock, None, None]: """Mock reolink connection and return both the host_mock and host_mock_class.""" - with patch( - "homeassistant.components.reolink.host.webhook.async_register", - return_value=True, - ), patch( - "homeassistant.components.reolink.host.Host", autospec=True - ) as host_mock_class: + with ( + patch( + "homeassistant.components.reolink.host.webhook.async_register", + return_value=True, + ), + patch( + "homeassistant.components.reolink.host.Host", autospec=True + ) as host_mock_class, + ): host_mock = host_mock_class.return_value host_mock.get_host_data.return_value = None host_mock.get_states.return_value = None diff --git a/tests/components/reolink/test_config_flow.py b/tests/components/reolink/test_config_flow.py index 89ab4be9f1e..e8818c9e560 100644 --- a/tests/components/reolink/test_config_flow.py +++ b/tests/components/reolink/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Reolink config flow.""" + from datetime import timedelta import json from typing import Any @@ -473,19 +474,18 @@ async def test_dhcp_ip_update( const.DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=dhcp_data ) - expected_calls = [] - for host in host_call_list: - expected_calls.append( - call( - host, - TEST_USERNAME, - TEST_PASSWORD, - port=TEST_PORT, - use_https=TEST_USE_HTTPS, - protocol=DEFAULT_PROTOCOL, - timeout=DEFAULT_TIMEOUT, - ) + expected_calls = [ + call( + host, + TEST_USERNAME, + TEST_PASSWORD, + port=TEST_PORT, + use_https=TEST_USE_HTTPS, + protocol=DEFAULT_PROTOCOL, + timeout=DEFAULT_TIMEOUT, ) + for host in host_call_list + ] assert reolink_connect_class.call_args_list == expected_calls diff --git a/tests/components/reolink/test_init.py b/tests/components/reolink/test_init.py index 65490129486..8ebce5d350e 100644 --- a/tests/components/reolink/test_init.py +++ b/tests/components/reolink/test_init.py @@ -1,4 +1,5 @@ """Test the Reolink init.""" + from datetime import timedelta from typing import Any from unittest.mock import AsyncMock, MagicMock, Mock, patch @@ -235,12 +236,14 @@ async def test_https_repair_issue( hass, {"country": "GB", "internal_url": "https://test_homeassistant_address"} ) - with patch( - "homeassistant.components.reolink.host.FIRST_ONVIF_TIMEOUT", new=0 - ), patch( - "homeassistant.components.reolink.host.FIRST_ONVIF_LONG_POLL_TIMEOUT", new=0 - ), patch( - "homeassistant.components.reolink.host.ReolinkHost._async_long_polling", + with ( + patch("homeassistant.components.reolink.host.FIRST_ONVIF_TIMEOUT", new=0), + patch( + "homeassistant.components.reolink.host.FIRST_ONVIF_LONG_POLL_TIMEOUT", new=0 + ), + patch( + "homeassistant.components.reolink.host.ReolinkHost._async_long_polling", + ), ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -260,12 +263,14 @@ async def test_ssl_repair_issue( hass, {"country": "GB", "internal_url": "http://test_homeassistant_address"} ) - with patch( - "homeassistant.components.reolink.host.FIRST_ONVIF_TIMEOUT", new=0 - ), patch( - "homeassistant.components.reolink.host.FIRST_ONVIF_LONG_POLL_TIMEOUT", new=0 - ), patch( - "homeassistant.components.reolink.host.ReolinkHost._async_long_polling", + with ( + patch("homeassistant.components.reolink.host.FIRST_ONVIF_TIMEOUT", new=0), + patch( + "homeassistant.components.reolink.host.FIRST_ONVIF_LONG_POLL_TIMEOUT", new=0 + ), + patch( + "homeassistant.components.reolink.host.ReolinkHost._async_long_polling", + ), ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -298,12 +303,14 @@ async def test_webhook_repair_issue( hass: HomeAssistant, config_entry: MockConfigEntry ) -> None: """Test repairs issue is raised when the webhook url is unreachable.""" - with patch( - "homeassistant.components.reolink.host.FIRST_ONVIF_TIMEOUT", new=0 - ), patch( - "homeassistant.components.reolink.host.FIRST_ONVIF_LONG_POLL_TIMEOUT", new=0 - ), patch( - "homeassistant.components.reolink.host.ReolinkHost._async_long_polling", + with ( + patch("homeassistant.components.reolink.host.FIRST_ONVIF_TIMEOUT", new=0), + patch( + "homeassistant.components.reolink.host.FIRST_ONVIF_LONG_POLL_TIMEOUT", new=0 + ), + patch( + "homeassistant.components.reolink.host.ReolinkHost._async_long_polling", + ), ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/reolink/test_media_source.py b/tests/components/reolink/test_media_source.py index c7abc5b8e0e..1eb45945eee 100644 --- a/tests/components/reolink/test_media_source.py +++ b/tests/components/reolink/test_media_source.py @@ -1,4 +1,5 @@ """Tests for the Reolink media_source platform.""" + from datetime import datetime, timedelta import logging from unittest.mock import AsyncMock, MagicMock, patch @@ -64,6 +65,17 @@ async def setup_component(hass: HomeAssistant) -> None: assert await async_setup_component(hass, MEDIA_STREAM_DOMAIN, {}) +async def test_platform_loads_before_config_entry( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, +) -> None: + """Test that the platform can be loaded before the config entry.""" + # Fake that the config entry is not loaded before the media_source platform + assert await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + assert mock_setup_entry.call_count == 0 + + async def test_resolve( hass: HomeAssistant, reolink_connect: MagicMock, diff --git a/tests/components/repairs/__init__.py b/tests/components/repairs/__init__.py index 4d584da1706..a6786db9685 100644 --- a/tests/components/repairs/__init__.py +++ b/tests/components/repairs/__init__.py @@ -1,6 +1,5 @@ """Tests for the repairs integration.""" - from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component diff --git a/tests/components/repairs/test_init.py b/tests/components/repairs/test_init.py index 977bd9b5e55..ec34409eb74 100644 --- a/tests/components/repairs/test_init.py +++ b/tests/components/repairs/test_init.py @@ -1,4 +1,5 @@ """Test the repairs websocket API.""" + from unittest.mock import AsyncMock, Mock from freezegun.api import FrozenDateTimeFactory @@ -124,7 +125,7 @@ async def test_create_update_issue( ) -@pytest.mark.parametrize("ha_version", ("2022.9.cat", "In the future: 2023.1.1")) +@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: WebSocketGenerator, ha_version ) -> None: diff --git a/tests/components/repairs/test_websocket_api.py b/tests/components/repairs/test_websocket_api.py index 0cf6b22dc0c..846b25ae8c2 100644 --- a/tests/components/repairs/test_websocket_api.py +++ b/tests/components/repairs/test_websocket_api.py @@ -1,4 +1,5 @@ """Test the repairs websocket API.""" + from __future__ import annotations from http import HTTPStatus @@ -269,10 +270,10 @@ async def test_fix_non_existing_issue( @pytest.mark.parametrize( ("domain", "step", "description_placeholders"), - ( + [ ("fake_integration", "custom_step", None), ("fake_integration_default_handler", "confirm", {"abc": "123"}), - ), + ], ) async def test_fix_issue( hass: HomeAssistant, @@ -338,9 +339,7 @@ async def test_fix_issue( "description_placeholders": None, "flow_id": flow_id, "handler": domain, - "minor_version": 1, "type": "create_entry", - "version": 1, } await ws_client.send_json({"id": 4, "type": "repairs/list_issues"}) diff --git a/tests/components/rest/test_init.py b/tests/components/rest/test_init.py index 2c6c32783f1..38a1661a831 100644 --- a/tests/components/rest/test_init.py +++ b/tests/components/rest/test_init.py @@ -468,7 +468,7 @@ async def test_config_schema_via_packages(hass: HomeAssistant) -> None: "pack_11": {"rest": {"resource": "http://url1"}}, "pack_list": {"rest": [{"resource": "http://url2"}]}, } - config = {hass_config.CONF_CORE: {hass_config.CONF_PACKAGES: packages}} + config = {hass_config.HA_DOMAIN: {hass_config.CONF_PACKAGES: packages}} await hass_config.merge_packages_config(hass, config, packages) assert len(config) == 2 diff --git a/tests/components/rest/test_notify.py b/tests/components/rest/test_notify.py index f9a2e88c732..9f47e74c535 100644 --- a/tests/components/rest/test_notify.py +++ b/tests/components/rest/test_notify.py @@ -1,4 +1,5 @@ """The tests for the rest.notify platform.""" + from unittest.mock import patch import respx diff --git a/tests/components/rest/test_sensor.py b/tests/components/rest/test_sensor.py index 2e4b06ac2d2..3de386be214 100644 --- a/tests/components/rest/test_sensor.py +++ b/tests/components/rest/test_sensor.py @@ -1,4 +1,5 @@ """The tests for the REST sensor platform.""" + from http import HTTPStatus import ssl from unittest.mock import AsyncMock, MagicMock, patch @@ -161,11 +162,11 @@ async def test_setup_encoding(hass: HomeAssistant) -> None: @respx.mock @pytest.mark.parametrize( ("ssl_cipher_list", "ssl_cipher_list_expected"), - ( + [ ("python_default", SSLCipherList.PYTHON_DEFAULT), ("intermediate", SSLCipherList.INTERMEDIATE), ("modern", SSLCipherList.MODERN), - ), + ], ) async def test_setup_ssl_ciphers( hass: HomeAssistant, ssl_cipher_list: str, ssl_cipher_list_expected: SSLCipherList diff --git a/tests/components/rest/test_switch.py b/tests/components/rest/test_switch.py index 7be2ce4c63e..551994312d4 100644 --- a/tests/components/rest/test_switch.py +++ b/tests/components/rest/test_switch.py @@ -1,4 +1,5 @@ """The tests for the REST switch platform.""" + from http import HTTPStatus import httpx diff --git a/tests/components/rest_command/conftest.py b/tests/components/rest_command/conftest.py index 1a624b7534f..ec1cfb16ee6 100644 --- a/tests/components/rest_command/conftest.py +++ b/tests/components/rest_command/conftest.py @@ -1,4 +1,5 @@ """Fixtures for the trend component tests.""" + from collections.abc import Awaitable, Callable from typing import Any diff --git a/tests/components/rest_command/test_init.py b/tests/components/rest_command/test_init.py index ef7707cad76..567391a4b32 100644 --- a/tests/components/rest_command/test_init.py +++ b/tests/components/rest_command/test_init.py @@ -1,4 +1,5 @@ """The tests for the rest command platform.""" + import base64 from http import HTTPStatus from unittest.mock import patch @@ -65,11 +66,9 @@ async def test_rest_command_timeout( aioclient_mock.get(TEST_URL, exc=TimeoutError()) - with pytest.raises( - HomeAssistantError, - match=r"^Timeout when calling resource 'https://example.com/'$", - ): + with pytest.raises(HomeAssistantError) as exc: await hass.services.async_call(DOMAIN, "get_test", {}, blocking=True) + assert str(exc.value) == "Timeout when calling resource 'https://example.com/'" assert len(aioclient_mock.mock_calls) == 1 @@ -84,12 +83,13 @@ async def test_rest_command_aiohttp_error( aioclient_mock.get(TEST_URL, exc=aiohttp.ClientError()) - with pytest.raises( - HomeAssistantError, - match=r"^Client error occurred when calling resource 'https://example.com/'$", - ): + with pytest.raises(HomeAssistantError) as exc: await hass.services.async_call(DOMAIN, "get_test", {}, blocking=True) + assert ( + str(exc.value) + == "Client error occurred when calling resource 'https://example.com/'" + ) assert len(aioclient_mock.mock_calls) == 1 @@ -335,13 +335,14 @@ async def test_rest_command_get_response_malformed_json( assert not response # Throws error when requesting response - with pytest.raises( - HomeAssistantError, - match=r"^Response of 'https://example.com/' could not be decoded as JSON$", - ): + with pytest.raises(HomeAssistantError) as exc: await hass.services.async_call( DOMAIN, "get_test", {}, blocking=True, return_response=True ) + assert ( + str(exc.value) + == "The response of 'https://example.com/' could not be decoded as JSON" + ) async def test_rest_command_get_response_none( @@ -368,12 +369,13 @@ async def test_rest_command_get_response_none( assert not response # Throws Decode error when requesting response - with pytest.raises( - HomeAssistantError, - match=r"^Response of 'https://example.com/' could not be decoded as text$", - ): + with pytest.raises(HomeAssistantError) as exc: response = await hass.services.async_call( DOMAIN, "get_test", {}, blocking=True, return_response=True ) + assert ( + str(exc.value) + == "The response of 'https://example.com/' could not be decoded as text" + ) assert not response diff --git a/tests/components/rflink/conftest.py b/tests/components/rflink/conftest.py index dcaeb0a5e01..b7c32bf0f13 100644 --- a/tests/components/rflink/conftest.py +++ b/tests/components/rflink/conftest.py @@ -1,2 +1,3 @@ """rflink conftest.""" + from tests.components.light.conftest import mock_light_profiles # noqa: F401 diff --git a/tests/components/rflink/test_binary_sensor.py b/tests/components/rflink/test_binary_sensor.py index 416bd4f71b4..c92eaa30fe8 100644 --- a/tests/components/rflink/test_binary_sensor.py +++ b/tests/components/rflink/test_binary_sensor.py @@ -3,6 +3,7 @@ Test setup of rflink sensor component/platform. Verify manual and automatic sensor creation. """ + from datetime import timedelta from freezegun import freeze_time diff --git a/tests/components/rflink/test_cover.py b/tests/components/rflink/test_cover.py index 71b3d2067d0..0829fddef51 100644 --- a/tests/components/rflink/test_cover.py +++ b/tests/components/rflink/test_cover.py @@ -4,6 +4,7 @@ Test setup of RFLink covers component/platform. State tracking and control of RFLink cover devices. """ + from homeassistant.components.rflink import EVENT_BUTTON_PRESSED from homeassistant.const import ( ATTR_ENTITY_ID, diff --git a/tests/components/rflink/test_sensor.py b/tests/components/rflink/test_sensor.py index a4330a86b4f..e375f3ae863 100644 --- a/tests/components/rflink/test_sensor.py +++ b/tests/components/rflink/test_sensor.py @@ -4,6 +4,7 @@ Test setup of rflink sensor component/platform. Verify manual and automatic sensor creation. """ + from homeassistant.components.rflink import ( CONF_RECONNECT_INTERVAL, DATA_ENTITY_LOOKUP, diff --git a/tests/components/rflink/test_switch.py b/tests/components/rflink/test_switch.py index 35646d0fd22..705856565ae 100644 --- a/tests/components/rflink/test_switch.py +++ b/tests/components/rflink/test_switch.py @@ -4,6 +4,7 @@ Test setup of rflink switch component/platform. State tracking and control of Rflink switch devices. """ + from homeassistant.components.rflink import EVENT_BUTTON_PRESSED from homeassistant.const import ( ATTR_ENTITY_ID, diff --git a/tests/components/rflink/test_utils.py b/tests/components/rflink/test_utils.py index 9a9caebab17..170a05f8623 100644 --- a/tests/components/rflink/test_utils.py +++ b/tests/components/rflink/test_utils.py @@ -1,4 +1,5 @@ """Test for RFLink utils methods.""" + from homeassistant.components.rflink.utils import ( brightness_to_rflink, rflink_to_brightness, diff --git a/tests/components/rfxtrx/conftest.py b/tests/components/rfxtrx/conftest.py index 8208c1138c0..5e0223173f9 100644 --- a/tests/components/rfxtrx/conftest.py +++ b/tests/components/rfxtrx/conftest.py @@ -1,4 +1,5 @@ """Common test tools.""" + from __future__ import annotations from unittest.mock import Mock, patch @@ -68,8 +69,9 @@ async def setup_rfx_test_cfg( async def transport_mock(hass): """Fixture that make sure all transports are fake.""" transport = Mock(spec=RFXtrxTransport) - with patch("RFXtrx.PySerialTransport", new=transport), patch( - "RFXtrx.PyNetworkTransport", transport + with ( + patch("RFXtrx.PySerialTransport", new=transport), + patch("RFXtrx.PyNetworkTransport", transport), ): yield transport diff --git a/tests/components/rfxtrx/snapshots/test_event.ambr b/tests/components/rfxtrx/snapshots/test_event.ambr new file mode 100644 index 00000000000..9fb99bacef3 --- /dev/null +++ b/tests/components/rfxtrx/snapshots/test_event.ambr @@ -0,0 +1,124 @@ +# serializer version: 1 +# name: test_control_event.2 + ... +# --- +# name: test_control_event.3 + ... + + 'Command': 'On', + + 'Rssi numeric': 5, + ... + - 'event_type': None, + + 'event_type': 'on', + ... + - 'state': 'unknown', + + 'state': '2021-01-09T12:00:00.000+00:00', + ... +# --- +# name: test_control_event[1] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'assumed_state': True, + 'event_type': None, + 'event_types': list([ + 'off', + 'on', + 'dim', + 'bright', + 'all_group_off', + 'all_group_on', + 'chime', + 'illegal_command', + ]), + 'friendly_name': 'ARC C1', + }), + 'context': , + 'entity_id': 'event.arc_c1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_control_event[2] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'assumed_state': True, + 'event_type': None, + 'event_types': list([ + 'off', + 'on', + 'dim', + 'bright', + 'all_group_off', + 'all_group_on', + 'chime', + 'illegal_command', + ]), + 'friendly_name': 'ARC D1', + }), + 'context': , + 'entity_id': 'event.arc_d1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_status_event.1 + ... + + 'Battery numeric': 9, + + 'Rssi numeric': 8, + + 'Sensor Status': 'Normal', + ... + - 'event_type': None, + + 'event_type': 'normal', + ... + - 'state': 'unknown', + + 'state': '2021-01-09T12:00:00.000+00:00', + ... +# --- +# name: test_status_event[1] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'assumed_state': True, + 'event_type': None, + 'event_types': list([ + 'normal', + 'normal_delayed', + 'alarm', + 'alarm_delayed', + 'motion', + 'no_motion', + 'panic', + 'end_panic', + 'ir', + 'arm_away', + 'arm_away_delayed', + 'arm_home', + 'arm_home_delayed', + 'disarm', + 'light_1_off', + 'light_1_on', + 'light_2_off', + 'light_2_on', + 'dark_detected', + 'light_detected', + 'battery_low', + 'pairing_kd101', + 'normal_tamper', + 'normal_delayed_tamper', + 'alarm_tamper', + 'alarm_delayed_tamper', + 'motion_tamper', + 'no_motion_tamper', + ]), + 'friendly_name': 'X10 Security d3dc54:32', + }), + 'context': , + 'entity_id': 'event.x10_security_d3dc54_32', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- diff --git a/tests/components/rfxtrx/test_binary_sensor.py b/tests/components/rfxtrx/test_binary_sensor.py index 2c1f763ea66..8f212b6e976 100644 --- a/tests/components/rfxtrx/test_binary_sensor.py +++ b/tests/components/rfxtrx/test_binary_sensor.py @@ -1,4 +1,5 @@ """The tests for the Rfxtrx sensor platform.""" + import pytest from homeassistant.components.rfxtrx import DOMAIN @@ -97,7 +98,7 @@ async def test_pt2262_unconfigured(hass: HomeAssistant, rfxtrx) -> None: @pytest.mark.parametrize( ("state", "event"), - [["on", "0b1100cd0213c7f230010f71"], ["off", "0b1100cd0213c7f230000f71"]], + [("on", "0b1100cd0213c7f230010f71"), ("off", "0b1100cd0213c7f230000f71")], ) async def test_state_restore(hass: HomeAssistant, rfxtrx, state, event) -> None: """State restoration.""" diff --git a/tests/components/rfxtrx/test_config_flow.py b/tests/components/rfxtrx/test_config_flow.py index e527b5dfb7b..d3c87885782 100644 --- a/tests/components/rfxtrx/test_config_flow.py +++ b/tests/components/rfxtrx/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Rfxtrx config flow.""" + import os from unittest.mock import MagicMock, patch, sentinel diff --git a/tests/components/rfxtrx/test_cover.py b/tests/components/rfxtrx/test_cover.py index d8eec762ec4..211209f79e5 100644 --- a/tests/components/rfxtrx/test_cover.py +++ b/tests/components/rfxtrx/test_cover.py @@ -1,4 +1,5 @@ """The tests for the Rfxtrx cover platform.""" + from unittest.mock import call import pytest @@ -203,9 +204,9 @@ async def test_rfy_cover(hass: HomeAssistant, rfxtrx) -> None: ) assert rfxtrx.transport.send.mock_calls == [ - call(bytearray(b"\x0C\x1a\x00\x00\x01\x02\x03\x01\x00\x00\x00\x00\x00")), - call(bytearray(b"\x0C\x1a\x00\x01\x01\x02\x03\x01\x01\x00\x00\x00\x00")), - call(bytearray(b"\x0C\x1a\x00\x02\x01\x02\x03\x01\x03\x00\x00\x00\x00")), + call(bytearray(b"\x0c\x1a\x00\x00\x01\x02\x03\x01\x00\x00\x00\x00\x00")), + call(bytearray(b"\x0c\x1a\x00\x01\x01\x02\x03\x01\x01\x00\x00\x00\x00")), + call(bytearray(b"\x0c\x1a\x00\x02\x01\x02\x03\x01\x03\x00\x00\x00\x00")), ] # Test a blind with venetian mode set to US @@ -256,12 +257,12 @@ async def test_rfy_cover(hass: HomeAssistant, rfxtrx) -> None: ) assert rfxtrx.transport.send.mock_calls == [ - call(bytearray(b"\x0C\x1a\x00\x00\x01\x02\x03\x02\x00\x00\x00\x00\x00")), - call(bytearray(b"\x0C\x1a\x00\x01\x01\x02\x03\x02\x0F\x00\x00\x00\x00")), - call(bytearray(b"\x0C\x1a\x00\x02\x01\x02\x03\x02\x10\x00\x00\x00\x00")), - call(bytearray(b"\x0C\x1a\x00\x03\x01\x02\x03\x02\x11\x00\x00\x00\x00")), - call(bytearray(b"\x0C\x1a\x00\x04\x01\x02\x03\x02\x12\x00\x00\x00\x00")), - call(bytearray(b"\x0C\x1a\x00\x00\x01\x02\x03\x02\x00\x00\x00\x00\x00")), + call(bytearray(b"\x0c\x1a\x00\x00\x01\x02\x03\x02\x00\x00\x00\x00\x00")), + call(bytearray(b"\x0c\x1a\x00\x01\x01\x02\x03\x02\x0f\x00\x00\x00\x00")), + call(bytearray(b"\x0c\x1a\x00\x02\x01\x02\x03\x02\x10\x00\x00\x00\x00")), + call(bytearray(b"\x0c\x1a\x00\x03\x01\x02\x03\x02\x11\x00\x00\x00\x00")), + call(bytearray(b"\x0c\x1a\x00\x04\x01\x02\x03\x02\x12\x00\x00\x00\x00")), + call(bytearray(b"\x0c\x1a\x00\x00\x01\x02\x03\x02\x00\x00\x00\x00\x00")), ] # Test a blind with venetian mode set to EU @@ -312,10 +313,10 @@ async def test_rfy_cover(hass: HomeAssistant, rfxtrx) -> None: ) assert rfxtrx.transport.send.mock_calls == [ - call(bytearray(b"\x0C\x1a\x00\x00\x01\x02\x03\x03\x00\x00\x00\x00\x00")), - call(bytearray(b"\x0C\x1a\x00\x01\x01\x02\x03\x03\x11\x00\x00\x00\x00")), - call(bytearray(b"\x0C\x1a\x00\x02\x01\x02\x03\x03\x12\x00\x00\x00\x00")), - call(bytearray(b"\x0C\x1a\x00\x03\x01\x02\x03\x03\x0F\x00\x00\x00\x00")), - call(bytearray(b"\x0C\x1a\x00\x04\x01\x02\x03\x03\x10\x00\x00\x00\x00")), - call(bytearray(b"\x0C\x1a\x00\x00\x01\x02\x03\x03\x00\x00\x00\x00\x00")), + call(bytearray(b"\x0c\x1a\x00\x00\x01\x02\x03\x03\x00\x00\x00\x00\x00")), + call(bytearray(b"\x0c\x1a\x00\x01\x01\x02\x03\x03\x11\x00\x00\x00\x00")), + call(bytearray(b"\x0c\x1a\x00\x02\x01\x02\x03\x03\x12\x00\x00\x00\x00")), + call(bytearray(b"\x0c\x1a\x00\x03\x01\x02\x03\x03\x0f\x00\x00\x00\x00")), + call(bytearray(b"\x0c\x1a\x00\x04\x01\x02\x03\x03\x10\x00\x00\x00\x00")), + call(bytearray(b"\x0c\x1a\x00\x00\x01\x02\x03\x03\x00\x00\x00\x00\x00")), ] diff --git a/tests/components/rfxtrx/test_device_action.py b/tests/components/rfxtrx/test_device_action.py index 6b2431fb763..a717fcf35d6 100644 --- a/tests/components/rfxtrx/test_device_action.py +++ b/tests/components/rfxtrx/test_device_action.py @@ -1,4 +1,5 @@ """The tests for RFXCOM RFXtrx device actions.""" + from __future__ import annotations from typing import Any, NamedTuple @@ -66,15 +67,15 @@ def _get_expected_actions(data): @pytest.mark.parametrize( ("device", "expected"), [ - [ + ( DEVICE_LIGHTING_1, list(_get_expected_actions(RFXtrx.lowlevel.Lighting1.COMMANDS)), - ], - [ + ), + ( DEVICE_BLINDS_1, list(_get_expected_actions(RFXtrx.lowlevel.RollerTrol.COMMANDS)), - ], - [DEVICE_TEMPHUM_1, []], + ), + (DEVICE_TEMPHUM_1, []), ], ) async def test_get_actions( @@ -114,21 +115,21 @@ async def test_get_actions( @pytest.mark.parametrize( ("device", "config", "expected"), [ - [ + ( DEVICE_LIGHTING_1, {"type": "send_command", "subtype": "On"}, "0710000045050100", - ], - [ + ), + ( DEVICE_LIGHTING_1, {"type": "send_command", "subtype": "Off"}, "0710000045050000", - ], - [ + ), + ( DEVICE_BLINDS_1, {"type": "send_command", "subtype": "Stop"}, "09190000009ba8010200", - ], + ), ], ) async def test_action( diff --git a/tests/components/rfxtrx/test_device_trigger.py b/tests/components/rfxtrx/test_device_trigger.py index a253810c4c8..7d24ec3ff6a 100644 --- a/tests/components/rfxtrx/test_device_trigger.py +++ b/tests/components/rfxtrx/test_device_trigger.py @@ -1,4 +1,5 @@ """The tests for RFXCOM RFXtrx device triggers.""" + from __future__ import annotations from typing import Any, NamedTuple @@ -60,7 +61,7 @@ async def setup_entry(hass, devices): @pytest.mark.parametrize( ("event", "expected"), [ - [ + ( EVENT_LIGHTING_1, [ {"type": "command", "subtype": subtype} @@ -75,7 +76,7 @@ async def setup_entry(hass, devices): "Illegal command", ] ], - ] + ) ], ) async def test_get_triggers( diff --git a/tests/components/rfxtrx/test_event.py b/tests/components/rfxtrx/test_event.py new file mode 100644 index 00000000000..1a4305d97f6 --- /dev/null +++ b/tests/components/rfxtrx/test_event.py @@ -0,0 +1,103 @@ +"""The tests for the Rfxtrx sensor platform.""" + +from unittest.mock import patch + +from freezegun.api import FrozenDateTimeFactory +import pytest +from RFXtrx import ControlEvent +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.rfxtrx import get_rfx_object +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant + +from .conftest import setup_rfx_test_cfg + + +@pytest.fixture(autouse=True) +def required_platforms_only(): + """Only set up the required platform and required base platforms to speed up tests.""" + with patch( + "homeassistant.components.rfxtrx.PLATFORMS", + (Platform.EVENT,), + ): + yield + + +async def test_control_event( + hass: HomeAssistant, + rfxtrx, + freezer: FrozenDateTimeFactory, + snapshot: SnapshotAssertion, +) -> None: + """Test event update updates correct event object.""" + hass.config.set_time_zone("UTC") + freezer.move_to("2021-01-09 12:00:00+00:00") + + await setup_rfx_test_cfg( + hass, + devices={ + "0710013d43010150": {}, + "0710013d44010150": {}, + }, + ) + + assert hass.states.get("event.arc_c1") == snapshot(name="1") + assert hass.states.get("event.arc_d1") == snapshot(name="2") + + # only signal one, to make sure we have no overhearing + await rfxtrx.signal("0710013d44010150") + + assert hass.states.get("event.arc_c1") == snapshot(diff="1") + assert hass.states.get("event.arc_d1") == snapshot(diff="2") + + +async def test_status_event( + hass: HomeAssistant, + rfxtrx, + freezer: FrozenDateTimeFactory, + snapshot: SnapshotAssertion, +) -> None: + """Test event update updates correct event object.""" + hass.config.set_time_zone("UTC") + freezer.move_to("2021-01-09 12:00:00+00:00") + + await setup_rfx_test_cfg( + hass, + devices={ + "0820004dd3dc540089": {}, + }, + ) + + assert hass.states.get("event.x10_security_d3dc54_32") == snapshot(name="1") + + await rfxtrx.signal("0820004dd3dc540089") + + assert hass.states.get("event.x10_security_d3dc54_32") == snapshot(diff="1") + + +async def test_invalid_event_type( + hass: HomeAssistant, + rfxtrx, + freezer: FrozenDateTimeFactory, + snapshot: SnapshotAssertion, +) -> None: + """Test with 1 sensor.""" + await setup_rfx_test_cfg( + hass, + devices={ + "0710013d43010150": {}, + }, + ) + + state = hass.states.get("event.arc_c1") + + # Invalid event type should not trigger change + event = get_rfx_object("0710013d43010150") + assert isinstance(event, ControlEvent) + event.values["Command"] = "invalid_command" + + rfxtrx.event_callback(event) + await hass.async_block_till_done() + + assert hass.states.get("event.arc_c1") == state diff --git a/tests/components/rfxtrx/test_init.py b/tests/components/rfxtrx/test_init.py index 88a63e47cf1..b969a63a990 100644 --- a/tests/components/rfxtrx/test_init.py +++ b/tests/components/rfxtrx/test_init.py @@ -1,4 +1,5 @@ """The tests for the Rfxtrx component.""" + from __future__ import annotations from unittest.mock import ANY, call diff --git a/tests/components/rfxtrx/test_light.py b/tests/components/rfxtrx/test_light.py index 30f54352fb9..c95df855da5 100644 --- a/tests/components/rfxtrx/test_light.py +++ b/tests/components/rfxtrx/test_light.py @@ -1,4 +1,5 @@ """The tests for the Rfxtrx light platform.""" + from unittest.mock import call import pytest @@ -90,7 +91,7 @@ async def test_one_light(hass: HomeAssistant, rfxtrx) -> None: @pytest.mark.parametrize( - ("state", "brightness"), [["on", 100], ["on", 50], ["off", None]] + ("state", "brightness"), [("on", 100), ("on", 50), ("off", None)] ) async def test_state_restore(hass: HomeAssistant, rfxtrx, state, brightness) -> None: """State restoration.""" diff --git a/tests/components/rfxtrx/test_sensor.py b/tests/components/rfxtrx/test_sensor.py index e80e612b283..4336798768f 100644 --- a/tests/components/rfxtrx/test_sensor.py +++ b/tests/components/rfxtrx/test_sensor.py @@ -1,4 +1,5 @@ """The tests for the Rfxtrx sensor platform.""" + import pytest from homeassistant.components.rfxtrx import DOMAIN @@ -51,7 +52,7 @@ async def test_one_sensor(hass: HomeAssistant, rfxtrx) -> None: @pytest.mark.parametrize( ("state", "event"), - [["18.4", "0a520801070100b81b0279"], ["17.9", "0a52085e070100b31b0279"]], + [("18.4", "0a520801070100b81b0279"), ("17.9", "0a52085e070100b31b0279")], ) async def test_state_restore(hass: HomeAssistant, rfxtrx, state, event) -> None: """State restoration.""" diff --git a/tests/components/rfxtrx/test_siren.py b/tests/components/rfxtrx/test_siren.py index 6e428f45d92..f8db86cff8d 100644 --- a/tests/components/rfxtrx/test_siren.py +++ b/tests/components/rfxtrx/test_siren.py @@ -1,4 +1,5 @@ """The tests for the Rfxtrx siren platform.""" + from unittest.mock import call from homeassistant.components.rfxtrx import DOMAIN diff --git a/tests/components/rfxtrx/test_switch.py b/tests/components/rfxtrx/test_switch.py index ec835ebda52..63aacdd5eab 100644 --- a/tests/components/rfxtrx/test_switch.py +++ b/tests/components/rfxtrx/test_switch.py @@ -1,4 +1,5 @@ """The tests for the RFXtrx switch platform.""" + from unittest.mock import call import pytest diff --git a/tests/components/rhasspy/test_config_flow.py b/tests/components/rhasspy/test_config_flow.py index 53c82c0cecd..1a53dd32e04 100644 --- a/tests/components/rhasspy/test_config_flow.py +++ b/tests/components/rhasspy/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Rhasspy config flow.""" + from unittest.mock import patch from homeassistant import config_entries diff --git a/tests/components/rhasspy/test_init.py b/tests/components/rhasspy/test_init.py index e4f0b346347..c03083c0b8a 100644 --- a/tests/components/rhasspy/test_init.py +++ b/tests/components/rhasspy/test_init.py @@ -1,4 +1,5 @@ """Tests for the Rhasspy integration.""" + from homeassistant.components.rhasspy.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant diff --git a/tests/components/ridwell/conftest.py b/tests/components/ridwell/conftest.py index 651c2a96388..32907ac8037 100644 --- a/tests/components/ridwell/conftest.py +++ b/tests/components/ridwell/conftest.py @@ -1,4 +1,5 @@ """Define test fixtures for Ridwell.""" + from datetime import date from unittest.mock import AsyncMock, Mock, patch @@ -78,12 +79,15 @@ def config_fixture(hass): @pytest.fixture(name="mock_aioridwell") async def mock_aioridwell_fixture(hass, client, config): """Define a fixture to patch aioridwell.""" - with patch( - "homeassistant.components.ridwell.config_flow.async_get_client", - return_value=client, - ), patch( - "homeassistant.components.ridwell.coordinator.async_get_client", - return_value=client, + with ( + patch( + "homeassistant.components.ridwell.config_flow.async_get_client", + return_value=client, + ), + patch( + "homeassistant.components.ridwell.coordinator.async_get_client", + return_value=client, + ), ): yield diff --git a/tests/components/ridwell/test_config_flow.py b/tests/components/ridwell/test_config_flow.py index 990ac656696..15352929b4c 100644 --- a/tests/components/ridwell/test_config_flow.py +++ b/tests/components/ridwell/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Ridwell config flow.""" + from unittest.mock import AsyncMock, patch from aioridwell.errors import InvalidCredentialsError, RidwellError diff --git a/tests/components/ridwell/test_diagnostics.py b/tests/components/ridwell/test_diagnostics.py index c87004a8e76..adfbb525283 100644 --- a/tests/components/ridwell/test_diagnostics.py +++ b/tests/components/ridwell/test_diagnostics.py @@ -1,4 +1,5 @@ """Test Ridwell diagnostics.""" + from syrupy import SnapshotAssertion from homeassistant.core import HomeAssistant diff --git a/tests/components/ring/common.py b/tests/components/ring/common.py index 93a6e4f91e0..c6852bf87d6 100644 --- a/tests/components/ring/common.py +++ b/tests/components/ring/common.py @@ -1,4 +1,5 @@ """Common methods used across the tests for ring devices.""" + from unittest.mock import patch from homeassistant.components.ring import DOMAIN diff --git a/tests/components/ring/conftest.py b/tests/components/ring/conftest.py index e9800393835..70c067af887 100644 --- a/tests/components/ring/conftest.py +++ b/tests/components/ring/conftest.py @@ -1,4 +1,5 @@ """Configuration for Ring tests.""" + from collections.abc import Generator import re from unittest.mock import AsyncMock, Mock, patch @@ -26,20 +27,15 @@ def mock_setup_entry() -> Generator[AsyncMock, None, None]: @pytest.fixture def mock_ring_auth(): """Mock ring_doorbell.Auth.""" - with patch("ring_doorbell.Auth", autospec=True) as mock_ring_auth: + with patch( + "homeassistant.components.ring.config_flow.Auth", autospec=True + ) as mock_ring_auth: mock_ring_auth.return_value.fetch_token.return_value = { "access_token": "mock-token" } yield mock_ring_auth.return_value -@pytest.fixture -def mock_ring(): - """Mock ring_doorbell.Ring.""" - with patch("ring_doorbell.Ring", autospec=True) as mock_ring: - yield mock_ring.return_value - - @pytest.fixture def mock_config_entry() -> MockConfigEntry: """Mock ConfigEntry.""" @@ -59,7 +55,6 @@ async def mock_added_config_entry( hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_ring_auth: Mock, - mock_ring: Mock, ) -> MockConfigEntry: """Mock ConfigEntry that's been added to HA.""" mock_config_entry.add_to_hass(hass) @@ -101,7 +96,7 @@ def requests_mock_fixture(): re.compile( r"https:\/\/api\.ring\.com\/clients_api\/doorbots\/\d+\/history" ), - text=load_fixture("doorbots.json", "ring"), + text=load_fixture("doorbot_history.json", "ring"), ) # Mocks the response for getting the health of a device mock.get( @@ -120,4 +115,31 @@ def requests_mock_fixture(): status_code=200, json={"url": "http://127.0.0.1/foo"}, ) + mock.get( + "https://api.ring.com/groups/v1/locations/mock-location-id/groups", + text=load_fixture("groups.json", "ring"), + ) + # Mocks the response for getting the history of the intercom + mock.get( + "https://api.ring.com/clients_api/doorbots/185036587/history", + text=load_fixture("intercom_history.json", "ring"), + ) + # Mocks the response for setting properties in settings (i.e. motion_detection) + mock.patch( + re.compile( + r"https:\/\/api\.ring\.com\/devices\/v1\/devices\/\d+\/settings" + ), + text="ok", + ) + # Mocks the open door command for intercom devices + mock.put( + "https://api.ring.com/commands/v1/devices/185036587/device_rpc", + status_code=200, + text="{}", + ) + # Mocks the response for getting the history of the intercom + mock.get( + "https://api.ring.com/clients_api/doorbots/185036587/history", + text=load_fixture("intercom_history.json", "ring"), + ) yield mock diff --git a/tests/components/ring/fixtures/devices.json b/tests/components/ring/fixtures/devices.json index aff234f9726..8deee7ec413 100644 --- a/tests/components/ring/fixtures/devices.json +++ b/tests/components/ring/fixtures/devices.json @@ -69,6 +69,7 @@ "enable_vod": true, "live_view_preset_profile": "highest", "live_view_presets": ["low", "middle", "high", "highest"], + "motion_detection_enabled": true, "motion_announcement": false, "motion_snooze_preset_profile": "low", "motion_snooze_presets": ["null", "low", "medium", "high"] @@ -133,6 +134,7 @@ }, "live_view_preset_profile": "highest", "live_view_presets": ["low", "middle", "high", "highest"], + "motion_detection_enabled": false, "motion_announcement": false, "motion_snooze_preset_profile": "low", "motion_snooze_presets": ["none", "low", "medium", "high"], @@ -281,6 +283,7 @@ }, "live_view_preset_profile": "highest", "live_view_presets": ["low", "middle", "high", "highest"], + "motion_detection_enabled": true, "motion_announcement": false, "motion_snooze_preset_profile": "low", "motion_snooze_presets": ["none", "low", "medium", "high"], @@ -375,5 +378,92 @@ "subscribed_motions": true, "time_zone": "America/New_York" } + ], + "other": [ + { + "id": 185036587, + "kind": "intercom_handset_audio", + "description": "Ingress", + "location_id": "mock-location-id", + "schema_id": null, + "is_sidewalk_gateway": false, + "created_at": "2023-12-01T18:05:25Z", + "deactivated_at": null, + "owner": { + "id": 762490876, + "first_name": "", + "last_name": "", + "email": "" + }, + "device_id": "124ba1b3fe1a", + "time_zone": "Europe/Rome", + "firmware_version": "Up to Date", + "owned": true, + "ring_net_id": null, + "settings": { + "features_confirmed": 5, + "show_recordings": true, + "recording_ttl": 180, + "recording_enabled": false, + "keep_alive": null, + "keep_alive_auto": 45.0, + "doorbell_volume": 8, + "enable_chime": 1, + "theft_alarm_enable": 0, + "use_cached_domain": 1, + "use_server_ip": 0, + "server_domain": "fw.ring.com", + "server_ip": null, + "enable_log": 1, + "forced_keep_alive": null, + "mic_volume": 11, + "chime_settings": { + "enable": true, + "type": 2, + "duration": 10 + }, + "intercom_settings": { + "ring_to_open": false, + "predecessor": "{\"make\":\"Comelit\",\"model\":\"2738W\",\"wires\":2}", + "config": "{\"intercom_type\": 2, \"number_of_wires\": 2, \"autounlock_enabled\": false, \"speaker_gain\": [-49, -35, -25, -21, -16, -9, -6, -3, 0, 3, 6, 9], \"digital\": {\"audio_amp\": 0, \"chg_en\": false, \"fast_chg\": false, \"bypass\": false, \"idle_lvl\": 32, \"ext_audio\": false, \"ext_audio_term\": 0, \"off_hk_tm\": 0, \"unlk_ka\": false, \"unlock\": {\"cap_tm\": 1000, \"rpl_tm\": 1500, \"gain\": 1000, \"cmp_thr\": 4000, \"lvl\": 25000, \"thr\": 30, \"thr2\": 0, \"offln\": false, \"ac\": true, \"bias\": \"h\", \"tx_pin\": \"TXD2\", \"ack\": 0, \"prot\": \"Comelit_SB2\", \"ask\": {\"f\": 25000, \"b\": 333}}, \"ring\": {\"cap_tm\": 40, \"rpl_tm\": 200, \"gain\": 2000, \"cmp_thr\": 4500, \"lvl\": 28000, \"thr\": 30, \"thr2\": 0, \"offln\": false, \"ac\": true, \"bias\": \"m\", \"tx_pin\": \"TXD2\", \"ack\": 0, \"prot\": \"Comelit_SB2\", \"ask\": {\"f\": 25000, \"b\": 333}}, \"hook_off\": {\"cap_tm\": 1000, \"rpl_tm\": 1500, \"gain\": 1000, \"cmp_thr\": 4000, \"lvl\": 25000, \"thr\": 30, \"thr2\": 0, \"offln\": false, \"ac\": true, \"bias\": \"h\", \"tx_pin\": \"TXD2\", \"ack\": 0, \"prot\": \"Comelit_SB2\", \"ask\": {\"f\": 25000, \"b\": 333}}, \"hook_on\": {\"cap_tm\": 1000, \"rpl_tm\": 1500, \"gain\": 1000, \"cmp_thr\": 4000, \"lvl\": 25000, \"thr\": 30, \"thr2\": 0, \"offln\": false, \"ac\": true, \"bias\": \"h\", \"tx_pin\": \"TXD2\", \"ack\": 0, \"prot\": \"Comelit_SB2\", \"ask\": {\"f\": 25000, \"b\": 333}}}}", + "intercom_type": "DF", + "replication": 1, + "unlock_mode": 0 + }, + "voice_volume": 11 + }, + "alerts": { + "connection": "online", + "ota_status": "timeout" + }, + "function": { + "name": null + }, + "subscribed": false, + "battery_life": "52", + "features": { + "cfes_eligible": false, + "motion_zone_recommendation": false, + "motions_enabled": true, + "show_recordings": true, + "show_vod_settings": true, + "rich_notifications_eligible": false, + "show_offline_motion_events": false, + "sheila_camera_eligible": null, + "sheila_camera_processing_eligible": null, + "dynamic_network_switching_eligible": false, + "chime_auto_detect_capable": false, + "missing_key_delivery_address": false, + "show_24x7_lite": false, + "recording_24x7_eligible": null + }, + "metadata": { + "ethernet": false, + "legacy_fw_migrated": true, + "imported_from_amazon": false, + "is_sidewalk_gateway": false, + "key_access_point_associated": true + } + } ] } diff --git a/tests/components/ring/fixtures/devices_updated.json b/tests/components/ring/fixtures/devices_updated.json index 5a4584b72db..01ea2ca25f5 100644 --- a/tests/components/ring/fixtures/devices_updated.json +++ b/tests/components/ring/fixtures/devices_updated.json @@ -69,6 +69,7 @@ "enable_vod": true, "live_view_preset_profile": "highest", "live_view_presets": ["low", "middle", "high", "highest"], + "motion_detection_enabled": true, "motion_announcement": false, "motion_snooze_preset_profile": "low", "motion_snooze_presets": ["null", "low", "medium", "high"] @@ -133,6 +134,7 @@ }, "live_view_preset_profile": "highest", "live_view_presets": ["low", "middle", "high", "highest"], + "motion_detection_enabled": true, "motion_announcement": false, "motion_snooze_preset_profile": "low", "motion_snooze_presets": ["none", "low", "medium", "high"], @@ -281,6 +283,7 @@ }, "live_view_preset_profile": "highest", "live_view_presets": ["low", "middle", "high", "highest"], + "motion_detection_enabled": false, "motion_announcement": false, "motion_snooze_preset_profile": "low", "motion_snooze_presets": ["none", "low", "medium", "high"], diff --git a/tests/components/ring/fixtures/doorbots.json b/tests/components/ring/fixtures/doorbot_history.json similarity index 100% rename from tests/components/ring/fixtures/doorbots.json rename to tests/components/ring/fixtures/doorbot_history.json diff --git a/tests/components/ring/fixtures/groups.json b/tests/components/ring/fixtures/groups.json new file mode 100644 index 00000000000..399aaac1641 --- /dev/null +++ b/tests/components/ring/fixtures/groups.json @@ -0,0 +1,24 @@ +{ + "device_groups": [ + { + "device_group_id": "mock-group-id", + "location_id": "mock-location-id", + "name": "Landscape", + "devices": [ + { + "doorbot_id": 12345678, + "location_id": "mock-location-id", + "type": "beams_ct200_transformer", + "mac_address": null, + "hardware_id": "1234567890", + "name": "Mock Transformer", + "deleted_at": null + } + ], + "created_at": "2020-11-03T22:07:05Z", + "updated_at": "2020-11-19T03:52:59Z", + "deleted_at": null, + "external_id": "12345678-1234-5678-90ab-1234567890ab" + } + ] +} diff --git a/tests/components/ring/fixtures/intercom_history.json b/tests/components/ring/fixtures/intercom_history.json new file mode 100644 index 00000000000..fccd87b9227 --- /dev/null +++ b/tests/components/ring/fixtures/intercom_history.json @@ -0,0 +1,116 @@ +[ + { + "id": 7330963245622279024, + "created_at": "2024-02-02T11:21:24.000Z", + "answered": false, + "events": [], + "kind": "ding", + "favorite": false, + "snapshot_url": "", + "recording": { + "status": "ready" + }, + "duration": 40.0, + "cv_properties": { + "person_detected": null, + "stream_broken": false, + "detection_type": null, + "cv_triggers": null, + "detection_types": null, + "security_alerts": null + }, + "properties": { + "is_alexa": false, + "is_sidewalk": false, + "is_autoreply": false + }, + "doorbot": { + "id": 185036587, + "description": "Ingresso", + "type": "intercom_handset_audio" + }, + "device_placement": null, + "geolocation": null, + "last_location": null, + "siren": null, + "is_e2ee": false, + "had_subscription": false, + "owner_id": "762490876" + }, + { + "id": 7323267080901445808, + "created_at": "2024-01-12T17:36:28.000Z", + "answered": true, + "events": [], + "kind": "on_demand", + "favorite": false, + "snapshot_url": "", + "recording": { + "status": "ready" + }, + "duration": 13.0, + "cv_properties": { + "person_detected": null, + "stream_broken": false, + "detection_type": null, + "cv_triggers": null, + "detection_types": null, + "security_alerts": null + }, + "properties": { + "is_alexa": false, + "is_sidewalk": false, + "is_autoreply": false + }, + "doorbot": { + "id": 185036587, + "description": "Ingresso", + "type": "intercom_handset_audio" + }, + "device_placement": null, + "geolocation": null, + "last_location": null, + "siren": null, + "is_e2ee": false, + "had_subscription": false, + "owner_id": "762490876" + }, + { + "id": 7307399027047288688, + "created_at": "2023-12-01T18:44:28.000Z", + "answered": true, + "events": [], + "kind": "on_demand", + "favorite": false, + "snapshot_url": "", + "recording": { + "status": "ready" + }, + "duration": 43.0, + "cv_properties": { + "person_detected": null, + "stream_broken": false, + "detection_type": null, + "cv_triggers": null, + "detection_types": null, + "security_alerts": null + }, + "properties": { + "is_alexa": false, + "is_sidewalk": false, + "is_autoreply": false + }, + "doorbot": { + "id": 185036587, + "description": "Ingresso", + "type": "intercom_handset_audio" + }, + "device_placement": null, + "geolocation": null, + "last_location": null, + "siren": null, + "is_e2ee": false, + "had_subscription": false, + "owner_id": "762490876" + } +] diff --git a/tests/components/ring/snapshots/test_diagnostics.ambr b/tests/components/ring/snapshots/test_diagnostics.ambr index 64e753ba2b3..1a9d8898af6 100644 --- a/tests/components/ring/snapshots/test_diagnostics.ambr +++ b/tests/components/ring/snapshots/test_diagnostics.ambr @@ -82,6 +82,7 @@ 'highest', ]), 'motion_announcement': False, + 'motion_detection_enabled': True, 'motion_snooze_preset_profile': 'low', 'motion_snooze_presets': list([ 'null', @@ -158,6 +159,7 @@ 'highest', ]), 'motion_announcement': False, + 'motion_detection_enabled': False, 'motion_snooze_preset_profile': 'low', 'motion_snooze_presets': list([ 'none', @@ -398,6 +400,7 @@ 'highest', ]), 'motion_announcement': False, + 'motion_detection_enabled': True, 'motion_snooze_preset_profile': 'low', 'motion_snooze_presets': list([ 'none', @@ -574,6 +577,91 @@ 'subscribed_motions': True, 'time_zone': 'America/New_York', }), + dict({ + 'alerts': dict({ + 'connection': 'online', + 'ota_status': 'timeout', + }), + 'battery_life': '52', + 'created_at': '2023-12-01T18:05:25Z', + 'deactivated_at': None, + 'description': '**REDACTED**', + 'device_id': '**REDACTED**', + 'features': dict({ + 'cfes_eligible': False, + 'chime_auto_detect_capable': False, + 'dynamic_network_switching_eligible': False, + 'missing_key_delivery_address': False, + 'motion_zone_recommendation': False, + 'motions_enabled': True, + 'recording_24x7_eligible': None, + 'rich_notifications_eligible': False, + 'sheila_camera_eligible': None, + 'sheila_camera_processing_eligible': None, + 'show_24x7_lite': False, + 'show_offline_motion_events': False, + 'show_recordings': True, + 'show_vod_settings': True, + }), + 'firmware_version': 'Up to Date', + 'function': dict({ + 'name': None, + }), + 'id': '**REDACTED**', + 'is_sidewalk_gateway': False, + 'kind': 'intercom_handset_audio', + 'location_id': '**REDACTED**', + 'metadata': dict({ + 'ethernet': False, + 'imported_from_amazon': False, + 'is_sidewalk_gateway': False, + 'key_access_point_associated': True, + 'legacy_fw_migrated': True, + }), + 'owned': True, + 'owner': dict({ + 'email': '', + 'first_name': '', + 'id': '**REDACTED**', + 'last_name': '', + }), + 'ring_net_id': None, + 'schema_id': None, + 'settings': dict({ + 'chime_settings': dict({ + 'duration': 10, + 'enable': True, + 'type': 2, + }), + 'doorbell_volume': 8, + 'enable_chime': 1, + 'enable_log': 1, + 'features_confirmed': 5, + 'forced_keep_alive': None, + 'intercom_settings': dict({ + 'config': '{"intercom_type": 2, "number_of_wires": 2, "autounlock_enabled": false, "speaker_gain": [-49, -35, -25, -21, -16, -9, -6, -3, 0, 3, 6, 9], "digital": {"audio_amp": 0, "chg_en": false, "fast_chg": false, "bypass": false, "idle_lvl": 32, "ext_audio": false, "ext_audio_term": 0, "off_hk_tm": 0, "unlk_ka": false, "unlock": {"cap_tm": 1000, "rpl_tm": 1500, "gain": 1000, "cmp_thr": 4000, "lvl": 25000, "thr": 30, "thr2": 0, "offln": false, "ac": true, "bias": "h", "tx_pin": "TXD2", "ack": 0, "prot": "Comelit_SB2", "ask": {"f": 25000, "b": 333}}, "ring": {"cap_tm": 40, "rpl_tm": 200, "gain": 2000, "cmp_thr": 4500, "lvl": 28000, "thr": 30, "thr2": 0, "offln": false, "ac": true, "bias": "m", "tx_pin": "TXD2", "ack": 0, "prot": "Comelit_SB2", "ask": {"f": 25000, "b": 333}}, "hook_off": {"cap_tm": 1000, "rpl_tm": 1500, "gain": 1000, "cmp_thr": 4000, "lvl": 25000, "thr": 30, "thr2": 0, "offln": false, "ac": true, "bias": "h", "tx_pin": "TXD2", "ack": 0, "prot": "Comelit_SB2", "ask": {"f": 25000, "b": 333}}, "hook_on": {"cap_tm": 1000, "rpl_tm": 1500, "gain": 1000, "cmp_thr": 4000, "lvl": 25000, "thr": 30, "thr2": 0, "offln": false, "ac": true, "bias": "h", "tx_pin": "TXD2", "ack": 0, "prot": "Comelit_SB2", "ask": {"f": 25000, "b": 333}}}}', + 'intercom_type': 'DF', + 'predecessor': '{"make":"Comelit","model":"2738W","wires":2}', + 'replication': 1, + 'ring_to_open': False, + 'unlock_mode': 0, + }), + 'keep_alive': None, + 'keep_alive_auto': 45.0, + 'mic_volume': 11, + 'recording_enabled': False, + 'recording_ttl': 180, + 'server_domain': 'fw.ring.com', + 'server_ip': None, + 'show_recordings': True, + 'theft_alarm_enable': 0, + 'use_cached_domain': 1, + 'use_server_ip': 0, + 'voice_volume': 11, + }), + 'subscribed': False, + 'time_zone': 'Europe/Rome', + }), ]), }) # --- diff --git a/tests/components/ring/test_binary_sensor.py b/tests/components/ring/test_binary_sensor.py index fa211b7e881..ba73de05c9b 100644 --- a/tests/components/ring/test_binary_sensor.py +++ b/tests/components/ring/test_binary_sensor.py @@ -1,4 +1,5 @@ """The tests for the Ring binary sensor platform.""" + from time import time from unittest.mock import patch @@ -32,6 +33,10 @@ async def test_binary_sensor( assert motion_state.state == "on" assert motion_state.attributes["device_class"] == "motion" - ding_state = hass.states.get("binary_sensor.front_door_ding") - assert ding_state is not None - assert ding_state.state == "off" + front_ding_state = hass.states.get("binary_sensor.front_door_ding") + assert front_ding_state is not None + assert front_ding_state.state == "off" + + ingress_ding_state = hass.states.get("binary_sensor.ingress_ding") + assert ingress_ding_state is not None + assert ingress_ding_state.state == "off" diff --git a/tests/components/ring/test_button.py b/tests/components/ring/test_button.py new file mode 100644 index 00000000000..6f0c29b1fcc --- /dev/null +++ b/tests/components/ring/test_button.py @@ -0,0 +1,42 @@ +"""The tests for the Ring button platform.""" + +import requests_mock + +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from .common import setup_platform + + +async def test_entity_registry( + hass: HomeAssistant, + requests_mock: requests_mock.Mocker, + entity_registry: er.EntityRegistry, +) -> None: + """Tests that the devices are registered in the entity registry.""" + await setup_platform(hass, Platform.BUTTON) + + entry = entity_registry.async_get("button.ingress_open_door") + assert entry.unique_id == "185036587-open_door" + + +async def test_button_opens_door( + hass: HomeAssistant, requests_mock: requests_mock.Mocker +) -> None: + """Tests the door open button works correctly.""" + await setup_platform(hass, Platform.BUTTON) + + # Mocks the response for opening door + mock = requests_mock.put( + "https://api.ring.com/commands/v1/devices/185036587/device_rpc", + status_code=200, + text="{}", + ) + + await hass.services.async_call( + "button", "press", {"entity_id": "button.ingress_open_door"}, blocking=True + ) + + await hass.async_block_till_done() + assert mock.called_once diff --git a/tests/components/ring/test_camera.py b/tests/components/ring/test_camera.py new file mode 100644 index 00000000000..de61d7a1452 --- /dev/null +++ b/tests/components/ring/test_camera.py @@ -0,0 +1,141 @@ +"""The tests for the Ring switch platform.""" + +from unittest.mock import PropertyMock, patch + +import pytest +import requests_mock +import ring_doorbell + +from homeassistant.config_entries import SOURCE_REAUTH +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import entity_registry as er + +from .common import setup_platform + +from tests.common import load_fixture + + +async def test_entity_registry( + hass: HomeAssistant, requests_mock: requests_mock.Mocker +) -> None: + """Tests that the devices are registered in the entity registry.""" + await setup_platform(hass, Platform.CAMERA) + entity_registry = er.async_get(hass) + + entry = entity_registry.async_get("camera.front") + assert entry.unique_id == 765432 + + entry = entity_registry.async_get("camera.internal") + assert entry.unique_id == 345678 + + +@pytest.mark.parametrize( + ("entity_name", "expected_state", "friendly_name"), + [ + ("camera.internal", True, "Internal"), + ("camera.front", None, "Front"), + ], + ids=["On", "Off"], +) +async def test_camera_motion_detection_state_reports_correctly( + hass: HomeAssistant, + requests_mock: requests_mock.Mocker, + entity_name, + expected_state, + friendly_name, +) -> None: + """Tests that the initial state of a device that should be off is correct.""" + await setup_platform(hass, Platform.CAMERA) + + state = hass.states.get(entity_name) + assert state.attributes.get("motion_detection") is expected_state + assert state.attributes.get("friendly_name") == friendly_name + + +async def test_camera_motion_detection_can_be_turned_on( + hass: HomeAssistant, requests_mock: requests_mock.Mocker +) -> None: + """Tests the siren turns on correctly.""" + await setup_platform(hass, Platform.CAMERA) + + state = hass.states.get("camera.front") + assert state.attributes.get("motion_detection") is not True + + await hass.services.async_call( + "camera", + "enable_motion_detection", + {"entity_id": "camera.front"}, + blocking=True, + ) + + await hass.async_block_till_done() + + state = hass.states.get("camera.front") + assert state.attributes.get("motion_detection") is True + + +async def test_updates_work( + hass: HomeAssistant, requests_mock: requests_mock.Mocker +) -> None: + """Tests the update service works correctly.""" + await setup_platform(hass, Platform.CAMERA) + state = hass.states.get("camera.internal") + assert state.attributes.get("motion_detection") is True + # Changes the return to indicate that the switch is now on. + requests_mock.get( + "https://api.ring.com/clients_api/ring_devices", + text=load_fixture("devices_updated.json", "ring"), + ) + + await hass.services.async_call("ring", "update", {}, blocking=True) + + await hass.async_block_till_done() + + state = hass.states.get("camera.internal") + assert state.attributes.get("motion_detection") is not True + + +@pytest.mark.parametrize( + ("exception_type", "reauth_expected"), + [ + (ring_doorbell.AuthenticationError, True), + (ring_doorbell.RingTimeout, False), + (ring_doorbell.RingError, False), + ], + ids=["Authentication", "Timeout", "Other"], +) +async def test_motion_detection_errors_when_turned_on( + hass: HomeAssistant, + requests_mock: requests_mock.Mocker, + exception_type, + reauth_expected, +) -> None: + """Tests the motion detection errors are handled correctly.""" + await setup_platform(hass, Platform.CAMERA) + config_entry = hass.config_entries.async_entries("ring")[0] + + assert not any(config_entry.async_get_active_flows(hass, {SOURCE_REAUTH})) + + with patch.object( + ring_doorbell.RingDoorBell, "motion_detection", new_callable=PropertyMock + ) as mock_motion_detection: + mock_motion_detection.side_effect = exception_type + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + "camera", + "enable_motion_detection", + {"entity_id": "camera.front"}, + blocking=True, + ) + await hass.async_block_till_done() + assert mock_motion_detection.call_count == 1 + assert ( + any( + flow + for flow in config_entry.async_get_active_flows(hass, {SOURCE_REAUTH}) + if flow["handler"] == "ring" + ) + == reauth_expected + ) diff --git a/tests/components/ring/test_config_flow.py b/tests/components/ring/test_config_flow.py index 53c7e139a51..f9c24ad77c5 100644 --- a/tests/components/ring/test_config_flow.py +++ b/tests/components/ring/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Ring config flow.""" + from unittest.mock import AsyncMock, Mock import pytest diff --git a/tests/components/ring/test_init.py b/tests/components/ring/test_init.py index 8d169002e38..ba5dd03ba9c 100644 --- a/tests/components/ring/test_init.py +++ b/tests/components/ring/test_init.py @@ -11,6 +11,7 @@ import homeassistant.components.ring as ring from homeassistant.components.ring import DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState from homeassistant.core import HomeAssistant +from homeassistant.helpers.issue_registry import IssueRegistry from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util @@ -147,7 +148,7 @@ async def test_auth_failure_on_device_update( side_effect=AuthenticationError, ): async_fire_time_changed(hass, dt_util.now() + timedelta(minutes=20)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert "Authentication failed while fetching devices data: " in [ record.message @@ -191,7 +192,7 @@ async def test_error_on_global_update( side_effect=error_type, ): async_fire_time_changed(hass, dt_util.now() + timedelta(minutes=20)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert log_msg in [ record.message for record in caplog.records if record.levelname == "ERROR" @@ -232,9 +233,36 @@ async def test_error_on_device_update( side_effect=error_type, ): async_fire_time_changed(hass, dt_util.now() + timedelta(minutes=20)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert log_msg in [ record.message for record in caplog.records if record.levelname == "ERROR" ] assert mock_config_entry.entry_id in hass.data[DOMAIN] + + +async def test_issue_deprecated_service_ring_update( + hass: HomeAssistant, + issue_registry: IssueRegistry, + caplog: pytest.LogCaptureFixture, + requests_mock: requests_mock.Mocker, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the issue is raised on deprecated service ring.update.""" + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + _ = await hass.services.async_call(DOMAIN, "update", {}, blocking=True) + + issue = issue_registry.async_get_issue("ring", "deprecated_service_ring_update") + assert issue + assert issue.issue_domain == "ring" + assert issue.issue_id == "deprecated_service_ring_update" + assert issue.translation_key == "deprecated_service_ring_update" + + assert ( + "Detected use of service 'ring.update'. " + "This is deprecated and will stop working in Home Assistant 2024.10. " + "Use 'homeassistant.update_entity' instead which updates all ring entities" + ) in caplog.text diff --git a/tests/components/ring/test_light.py b/tests/components/ring/test_light.py index 7607f9fa5db..621c0b8f1d0 100644 --- a/tests/components/ring/test_light.py +++ b/tests/components/ring/test_light.py @@ -1,8 +1,15 @@ """The tests for the Ring light platform.""" -import requests_mock +from unittest.mock import PropertyMock, patch + +import pytest +import requests_mock +import ring_doorbell + +from homeassistant.config_entries import SOURCE_REAUTH from homeassistant.const import Platform from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er from .common import setup_platform @@ -89,3 +96,44 @@ async def test_updates_work( state = hass.states.get("light.front_light") assert state.state == "on" + + +@pytest.mark.parametrize( + ("exception_type", "reauth_expected"), + [ + (ring_doorbell.AuthenticationError, True), + (ring_doorbell.RingTimeout, False), + (ring_doorbell.RingError, False), + ], + ids=["Authentication", "Timeout", "Other"], +) +async def test_light_errors_when_turned_on( + hass: HomeAssistant, + requests_mock: requests_mock.Mocker, + exception_type, + reauth_expected, +) -> None: + """Tests the light turns on correctly.""" + await setup_platform(hass, Platform.LIGHT) + config_entry = hass.config_entries.async_entries("ring")[0] + + assert not any(config_entry.async_get_active_flows(hass, {SOURCE_REAUTH})) + + with patch.object( + ring_doorbell.RingStickUpCam, "lights", new_callable=PropertyMock + ) as mock_lights: + mock_lights.side_effect = exception_type + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + "light", "turn_on", {"entity_id": "light.front_light"}, blocking=True + ) + await hass.async_block_till_done() + assert mock_lights.call_count == 1 + assert ( + any( + flow + for flow in config_entry.async_get_active_flows(hass, {SOURCE_REAUTH}) + if flow["handler"] == "ring" + ) + == reauth_expected + ) diff --git a/tests/components/ring/test_sensor.py b/tests/components/ring/test_sensor.py index 34b6295b740..2c866586c6c 100644 --- a/tests/components/ring/test_sensor.py +++ b/tests/components/ring/test_sensor.py @@ -1,4 +1,5 @@ """The tests for the Ring sensor platform.""" + import logging from freezegun.api import FrozenDateTimeFactory @@ -39,13 +40,19 @@ async def test_sensor(hass: HomeAssistant, requests_mock: requests_mock.Mocker) assert downstairs_volume_state is not None assert downstairs_volume_state.state == "2" - front_door_last_activity_state = hass.states.get("sensor.front_door_last_activity") - assert front_door_last_activity_state is not None - downstairs_wifi_signal_strength_state = hass.states.get( "sensor.downstairs_wifi_signal_strength" ) + ingress_mic_volume_state = hass.states.get("sensor.ingress_mic_volume") + assert ingress_mic_volume_state.state == "11" + + ingress_doorbell_volume_state = hass.states.get("sensor.ingress_doorbell_volume") + assert ingress_doorbell_volume_state.state == "8" + + ingress_voice_volume_state = hass.states.get("sensor.ingress_voice_volume") + assert ingress_voice_volume_state.state == "11" + if not WIFI_ENABLED: return @@ -65,6 +72,24 @@ async def test_sensor(hass: HomeAssistant, requests_mock: requests_mock.Mocker) assert front_door_wifi_signal_strength_state.state == "-58" +async def test_history( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + requests_mock: requests_mock.Mocker, +) -> None: + """Test history derived sensors.""" + await setup_platform(hass, Platform.SENSOR) + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done(True) + + front_door_last_activity_state = hass.states.get("sensor.front_door_last_activity") + assert front_door_last_activity_state.state == "2017-03-05T15:03:40+00:00" + + ingress_last_activity_state = hass.states.get("sensor.ingress_last_activity") + assert ingress_last_activity_state.state == "2024-02-02T11:21:24+00:00" + + async def test_only_chime_devices( hass: HomeAssistant, requests_mock: requests_mock.Mocker, diff --git a/tests/components/ring/test_siren.py b/tests/components/ring/test_siren.py index 916da5d24fb..b3d46c601de 100644 --- a/tests/components/ring/test_siren.py +++ b/tests/components/ring/test_siren.py @@ -1,8 +1,15 @@ """The tests for the Ring button platform.""" -import requests_mock +from unittest.mock import patch + +import pytest +import requests_mock +import ring_doorbell + +from homeassistant.config_entries import SOURCE_REAUTH from homeassistant.const import Platform from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er from .common import setup_platform @@ -144,3 +151,46 @@ async def test_motion_chime_can_be_played( state = hass.states.get("siren.downstairs_siren") assert state.state == "unknown" + + +@pytest.mark.parametrize( + ("exception_type", "reauth_expected"), + [ + (ring_doorbell.AuthenticationError, True), + (ring_doorbell.RingTimeout, False), + (ring_doorbell.RingError, False), + ], + ids=["Authentication", "Timeout", "Other"], +) +async def test_siren_errors_when_turned_on( + hass: HomeAssistant, + requests_mock: requests_mock.Mocker, + exception_type, + reauth_expected, +) -> None: + """Tests the siren turns on correctly.""" + await setup_platform(hass, Platform.SIREN) + config_entry = hass.config_entries.async_entries("ring")[0] + + assert not any(config_entry.async_get_active_flows(hass, {SOURCE_REAUTH})) + + with patch.object( + ring_doorbell.RingChime, "test_sound", side_effect=exception_type + ) as mock_siren: + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + "siren", + "turn_on", + {"entity_id": "siren.downstairs_siren", "tone": "motion"}, + blocking=True, + ) + await hass.async_block_till_done() + assert mock_siren.call_count == 1 + assert ( + any( + flow + for flow in config_entry.async_get_active_flows(hass, {SOURCE_REAUTH}) + if flow["handler"] == "ring" + ) + == reauth_expected + ) diff --git a/tests/components/ring/test_switch.py b/tests/components/ring/test_switch.py index b856a2f850c..e4ddd7cd855 100644 --- a/tests/components/ring/test_switch.py +++ b/tests/components/ring/test_switch.py @@ -1,8 +1,15 @@ """The tests for the Ring switch platform.""" -import requests_mock +from unittest.mock import PropertyMock, patch + +import pytest +import requests_mock +import ring_doorbell + +from homeassistant.config_entries import SOURCE_REAUTH from homeassistant.const import ATTR_ENTITY_ID, Platform from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component @@ -45,7 +52,6 @@ async def test_siren_on_reports_correctly( state = hass.states.get("switch.internal_siren") assert state.state == "on" assert state.attributes.get("friendly_name") == "Internal Siren" - assert state.attributes.get("icon") == "mdi:alarm-bell" async def test_siren_can_be_turned_on( @@ -97,3 +103,44 @@ async def test_updates_work( state = hass.states.get("switch.front_siren") assert state.state == "on" + + +@pytest.mark.parametrize( + ("exception_type", "reauth_expected"), + [ + (ring_doorbell.AuthenticationError, True), + (ring_doorbell.RingTimeout, False), + (ring_doorbell.RingError, False), + ], + ids=["Authentication", "Timeout", "Other"], +) +async def test_switch_errors_when_turned_on( + hass: HomeAssistant, + requests_mock: requests_mock.Mocker, + exception_type, + reauth_expected, +) -> None: + """Tests the switch turns on correctly.""" + await setup_platform(hass, Platform.SWITCH) + config_entry = hass.config_entries.async_entries("ring")[0] + + assert not any(config_entry.async_get_active_flows(hass, {SOURCE_REAUTH})) + + with patch.object( + ring_doorbell.RingStickUpCam, "siren", new_callable=PropertyMock + ) as mock_switch: + mock_switch.side_effect = exception_type + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + "switch", "turn_on", {"entity_id": "switch.front_siren"}, blocking=True + ) + await hass.async_block_till_done() + assert mock_switch.call_count == 1 + assert ( + any( + flow + for flow in config_entry.async_get_active_flows(hass, {SOURCE_REAUTH}) + if flow["handler"] == "ring" + ) + == reauth_expected + ) diff --git a/tests/components/risco/conftest.py b/tests/components/risco/conftest.py index e08e6b29852..ab3b64b245d 100644 --- a/tests/components/risco/conftest.py +++ b/tests/components/risco/conftest.py @@ -1,4 +1,5 @@ """Fixtures for Risco tests.""" + from unittest.mock import MagicMock, PropertyMock, patch import pytest @@ -13,7 +14,7 @@ from homeassistant.const import ( CONF_USERNAME, ) -from .util import TEST_SITE_NAME, TEST_SITE_UUID, zone_mock +from .util import TEST_SITE_NAME, TEST_SITE_UUID, system_mock, zone_mock from tests.common import MockConfigEntry @@ -35,25 +36,30 @@ def two_zone_cloud(): """Fixture to mock alarm with two zones.""" zone_mocks = {0: zone_mock(), 1: zone_mock()} alarm_mock = MagicMock() - with patch.object( - zone_mocks[0], "id", new_callable=PropertyMock(return_value=0) - ), patch.object( - zone_mocks[0], "name", new_callable=PropertyMock(return_value="Zone 0") - ), patch.object( - zone_mocks[0], "bypassed", new_callable=PropertyMock(return_value=False) - ), patch.object( - zone_mocks[1], "id", new_callable=PropertyMock(return_value=1) - ), patch.object( - zone_mocks[1], "name", new_callable=PropertyMock(return_value="Zone 1") - ), patch.object( - zone_mocks[1], "bypassed", new_callable=PropertyMock(return_value=False) - ), patch.object( - alarm_mock, - "zones", - new_callable=PropertyMock(return_value=zone_mocks), - ), patch( - "homeassistant.components.risco.RiscoCloud.get_state", - return_value=alarm_mock, + with ( + patch.object(zone_mocks[0], "id", new_callable=PropertyMock(return_value=0)), + patch.object( + zone_mocks[0], "name", new_callable=PropertyMock(return_value="Zone 0") + ), + patch.object( + zone_mocks[0], "bypassed", new_callable=PropertyMock(return_value=False) + ), + patch.object(zone_mocks[1], "id", new_callable=PropertyMock(return_value=1)), + patch.object( + zone_mocks[1], "name", new_callable=PropertyMock(return_value="Zone 1") + ), + patch.object( + zone_mocks[1], "bypassed", new_callable=PropertyMock(return_value=False) + ), + patch.object( + alarm_mock, + "zones", + new_callable=PropertyMock(return_value=zone_mocks), + ), + patch( + "homeassistant.components.risco.RiscoCloud.get_state", + return_value=alarm_mock, + ), ): yield zone_mocks @@ -62,32 +68,49 @@ def two_zone_cloud(): def two_zone_local(): """Fixture to mock alarm with two zones.""" zone_mocks = {0: zone_mock(), 1: zone_mock()} - with patch.object( - zone_mocks[0], "id", new_callable=PropertyMock(return_value=0) - ), patch.object( - zone_mocks[0], "name", new_callable=PropertyMock(return_value="Zone 0") - ), patch.object( - zone_mocks[0], "alarmed", new_callable=PropertyMock(return_value=False) - ), patch.object( - zone_mocks[0], "bypassed", new_callable=PropertyMock(return_value=False) - ), patch.object( - zone_mocks[0], "armed", new_callable=PropertyMock(return_value=False) - ), patch.object( - zone_mocks[1], "id", new_callable=PropertyMock(return_value=1) - ), patch.object( - zone_mocks[1], "name", new_callable=PropertyMock(return_value="Zone 1") - ), patch.object( - zone_mocks[1], "alarmed", new_callable=PropertyMock(return_value=False) - ), patch.object( - zone_mocks[1], "bypassed", new_callable=PropertyMock(return_value=False) - ), patch.object( - zone_mocks[1], "armed", new_callable=PropertyMock(return_value=False) - ), patch( - "homeassistant.components.risco.RiscoLocal.partitions", - new_callable=PropertyMock(return_value={}), - ), patch( - "homeassistant.components.risco.RiscoLocal.zones", - new_callable=PropertyMock(return_value=zone_mocks), + system = system_mock() + with ( + patch.object(zone_mocks[0], "id", new_callable=PropertyMock(return_value=0)), + patch.object( + zone_mocks[0], "name", new_callable=PropertyMock(return_value="Zone 0") + ), + patch.object( + zone_mocks[0], "alarmed", new_callable=PropertyMock(return_value=False) + ), + patch.object( + zone_mocks[0], "bypassed", new_callable=PropertyMock(return_value=False) + ), + patch.object( + zone_mocks[0], "armed", new_callable=PropertyMock(return_value=False) + ), + patch.object(zone_mocks[1], "id", new_callable=PropertyMock(return_value=1)), + patch.object( + zone_mocks[1], "name", new_callable=PropertyMock(return_value="Zone 1") + ), + patch.object( + zone_mocks[1], "alarmed", new_callable=PropertyMock(return_value=False) + ), + patch.object( + zone_mocks[1], "bypassed", new_callable=PropertyMock(return_value=False) + ), + patch.object( + zone_mocks[1], "armed", new_callable=PropertyMock(return_value=False) + ), + patch.object( + system, "name", new_callable=PropertyMock(return_value=TEST_SITE_NAME) + ), + patch( + "homeassistant.components.risco.RiscoLocal.partitions", + new_callable=PropertyMock(return_value={}), + ), + patch( + "homeassistant.components.risco.RiscoLocal.zones", + new_callable=PropertyMock(return_value=zone_mocks), + ), + patch( + "homeassistant.components.risco.RiscoLocal.system", + new_callable=PropertyMock(return_value=system), + ), ): yield zone_mocks @@ -130,20 +153,26 @@ def login_with_error(exception): @pytest.fixture async def setup_risco_cloud(hass, cloud_config_entry, events): """Set up a Risco integration for testing.""" - with patch( - "homeassistant.components.risco.RiscoCloud.login", - return_value=True, - ), patch( - "homeassistant.components.risco.RiscoCloud.site_uuid", - new_callable=PropertyMock(return_value=TEST_SITE_UUID), - ), patch( - "homeassistant.components.risco.RiscoCloud.site_name", - new_callable=PropertyMock(return_value=TEST_SITE_NAME), - ), patch( - "homeassistant.components.risco.RiscoCloud.close", - ), patch( - "homeassistant.components.risco.RiscoCloud.get_events", - return_value=events, + with ( + patch( + "homeassistant.components.risco.RiscoCloud.login", + return_value=True, + ), + patch( + "homeassistant.components.risco.RiscoCloud.site_uuid", + new_callable=PropertyMock(return_value=TEST_SITE_UUID), + ), + patch( + "homeassistant.components.risco.RiscoCloud.site_name", + new_callable=PropertyMock(return_value=TEST_SITE_NAME), + ), + patch( + "homeassistant.components.risco.RiscoCloud.close", + ), + patch( + "homeassistant.components.risco.RiscoCloud.get_events", + return_value=events, + ), ): await hass.config_entries.async_setup(cloud_config_entry.entry_id) await hass.async_block_till_done() @@ -174,14 +203,18 @@ def connect_with_error(exception): @pytest.fixture async def setup_risco_local(hass, local_config_entry): """Set up a local Risco integration for testing.""" - with patch( - "homeassistant.components.risco.RiscoLocal.connect", - return_value=True, - ), patch( - "homeassistant.components.risco.RiscoLocal.id", - new_callable=PropertyMock(return_value=TEST_SITE_UUID), - ), patch( - "homeassistant.components.risco.RiscoLocal.disconnect", + with ( + patch( + "homeassistant.components.risco.RiscoLocal.connect", + return_value=True, + ), + patch( + "homeassistant.components.risco.RiscoLocal.id", + new_callable=PropertyMock(return_value=TEST_SITE_UUID), + ), + patch( + "homeassistant.components.risco.RiscoLocal.disconnect", + ), ): await hass.config_entries.async_setup(local_config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/risco/test_alarm_control_panel.py b/tests/components/risco/test_alarm_control_panel.py index e49817469b4..ff831b59062 100644 --- a/tests/components/risco/test_alarm_control_panel.py +++ b/tests/components/risco/test_alarm_control_panel.py @@ -1,4 +1,5 @@ """Tests for the Risco alarm control panel device.""" + from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch import pytest @@ -91,17 +92,22 @@ def two_part_cloud_alarm(): """Fixture to mock alarm with two partitions.""" partition_mocks = {0: _partition_mock(), 1: _partition_mock()} alarm_mock = MagicMock() - with patch.object( - partition_mocks[0], "id", new_callable=PropertyMock(return_value=0) - ), patch.object( - partition_mocks[1], "id", new_callable=PropertyMock(return_value=1) - ), patch.object( - alarm_mock, - "partitions", - new_callable=PropertyMock(return_value=partition_mocks), - ), patch( - "homeassistant.components.risco.RiscoCloud.get_state", - return_value=alarm_mock, + with ( + patch.object( + partition_mocks[0], "id", new_callable=PropertyMock(return_value=0) + ), + patch.object( + partition_mocks[1], "id", new_callable=PropertyMock(return_value=1) + ), + patch.object( + alarm_mock, + "partitions", + new_callable=PropertyMock(return_value=partition_mocks), + ), + patch( + "homeassistant.components.risco.RiscoCloud.get_state", + return_value=alarm_mock, + ), ): yield partition_mocks @@ -110,20 +116,27 @@ def two_part_cloud_alarm(): def two_part_local_alarm(): """Fixture to mock alarm with two partitions.""" partition_mocks = {0: _partition_mock(), 1: _partition_mock()} - with patch.object( - partition_mocks[0], "id", new_callable=PropertyMock(return_value=0) - ), patch.object( - partition_mocks[0], "name", new_callable=PropertyMock(return_value="Name 0") - ), patch.object( - partition_mocks[1], "id", new_callable=PropertyMock(return_value=1) - ), patch.object( - partition_mocks[1], "name", new_callable=PropertyMock(return_value="Name 1") - ), patch( - "homeassistant.components.risco.RiscoLocal.zones", - new_callable=PropertyMock(return_value={}), - ), patch( - "homeassistant.components.risco.RiscoLocal.partitions", - new_callable=PropertyMock(return_value=partition_mocks), + with ( + patch.object( + partition_mocks[0], "id", new_callable=PropertyMock(return_value=0) + ), + patch.object( + partition_mocks[0], "name", new_callable=PropertyMock(return_value="Name 0") + ), + patch.object( + partition_mocks[1], "id", new_callable=PropertyMock(return_value=1) + ), + patch.object( + partition_mocks[1], "name", new_callable=PropertyMock(return_value="Name 1") + ), + patch( + "homeassistant.components.risco.RiscoLocal.zones", + new_callable=PropertyMock(return_value={}), + ), + patch( + "homeassistant.components.risco.RiscoLocal.partitions", + new_callable=PropertyMock(return_value=partition_mocks), + ), ): yield partition_mocks @@ -511,7 +524,8 @@ async def _check_local_state( @pytest.fixture -def _mock_partition_handler(): +def mock_partition_handler(): + """Create a mock for add_partition_handler.""" with patch( "homeassistant.components.risco.RiscoLocal.add_partition_handler" ) as mock: @@ -522,11 +536,11 @@ def _mock_partition_handler(): async def test_local_states( hass: HomeAssistant, two_part_local_alarm, - _mock_partition_handler, + mock_partition_handler, setup_risco_local, ) -> None: """Test the various alarm states.""" - callback = _mock_partition_handler.call_args.args[0] + callback = mock_partition_handler.call_args.args[0] assert callback is not None diff --git a/tests/components/risco/test_binary_sensor.py b/tests/components/risco/test_binary_sensor.py index ee74dbbedc8..ea18c59e236 100644 --- a/tests/components/risco/test_binary_sensor.py +++ b/tests/components/risco/test_binary_sensor.py @@ -1,4 +1,5 @@ """Tests for the Risco binary sensors.""" + from unittest.mock import PropertyMock, patch import pytest @@ -10,7 +11,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.entity_component import async_update_entity -from .util import TEST_SITE_UUID +from .util import TEST_SITE_NAME, TEST_SITE_UUID, system_mock FIRST_ENTITY_ID = "binary_sensor.zone_0" SECOND_ENTITY_ID = "binary_sensor.zone_1" @@ -115,6 +116,10 @@ async def test_local_setup( assert device is not None assert device.manufacturer == "Risco" + device = registry.async_get_device(identifiers={(DOMAIN, TEST_SITE_UUID)}) + assert device is not None + assert device.manufacturer == "Risco" + async def _check_local_state( hass, zones, property, value, entity_id, zone_id, callback @@ -133,16 +138,17 @@ async def _check_local_state( @pytest.fixture -def _mock_zone_handler(): +def mock_zone_handler(): + """Create a mock for add_zone_handler.""" with patch("homeassistant.components.risco.RiscoLocal.add_zone_handler") as mock: yield mock async def test_local_states( - hass: HomeAssistant, two_zone_local, _mock_zone_handler, setup_risco_local + hass: HomeAssistant, two_zone_local, mock_zone_handler, setup_risco_local ) -> None: """Test the various zone states.""" - callback = _mock_zone_handler.call_args.args[0] + callback = mock_zone_handler.call_args.args[0] assert callback is not None @@ -161,10 +167,10 @@ async def test_local_states( async def test_alarmed_local_states( - hass: HomeAssistant, two_zone_local, _mock_zone_handler, setup_risco_local + hass: HomeAssistant, two_zone_local, mock_zone_handler, setup_risco_local ) -> None: """Test the various zone alarmed states.""" - callback = _mock_zone_handler.call_args.args[0] + callback = mock_zone_handler.call_args.args[0] assert callback is not None @@ -183,10 +189,10 @@ async def test_alarmed_local_states( async def test_armed_local_states( - hass: HomeAssistant, two_zone_local, _mock_zone_handler, setup_risco_local + hass: HomeAssistant, two_zone_local, mock_zone_handler, setup_risco_local ) -> None: """Test the various zone armed states.""" - callback = _mock_zone_handler.call_args.args[0] + callback = mock_zone_handler.call_args.args[0] assert callback is not None @@ -202,3 +208,73 @@ async def test_armed_local_states( await _check_local_state( hass, two_zone_local, "armed", False, SECOND_ARMED_ENTITY_ID, 1, callback ) + + +async def _check_system_state(hass, system, property, value, callback): + with patch.object( + system, + property, + new_callable=PropertyMock(return_value=value), + ): + await callback(system) + await hass.async_block_till_done() + + expected_value = STATE_ON if value else STATE_OFF + if property == "ac_trouble": + property = "a_c_trouble" + entity_id = f"binary_sensor.test_site_name_{property}" + assert hass.states.get(entity_id).state == expected_value + + +@pytest.fixture +def mock_system_handler(): + """Create a mock for add_system_handler.""" + with patch("homeassistant.components.risco.RiscoLocal.add_system_handler") as mock: + yield mock + + +@pytest.fixture +def system_only_local(): + """Fixture to mock a system with no zones or partitions.""" + system = system_mock() + with ( + patch.object( + system, "name", new_callable=PropertyMock(return_value=TEST_SITE_NAME) + ), + patch( + "homeassistant.components.risco.RiscoLocal.zones", + new_callable=PropertyMock(return_value={}), + ), + patch( + "homeassistant.components.risco.RiscoLocal.partitions", + new_callable=PropertyMock(return_value={}), + ), + patch( + "homeassistant.components.risco.RiscoLocal.system", + new_callable=PropertyMock(return_value=system), + ), + ): + yield system + + +async def test_system_states( + hass: HomeAssistant, system_only_local, mock_system_handler, setup_risco_local +) -> None: + """Test the various zone states.""" + callback = mock_system_handler.call_args.args[0] + + assert callback is not None + + properties = [ + "low_battery_trouble", + "ac_trouble", + "monitoring_station_1_trouble", + "monitoring_station_2_trouble", + "monitoring_station_3_trouble", + "phone_line_trouble", + "clock_trouble", + "box_tamper", + ] + for property in properties: + await _check_system_state(hass, system_only_local, property, True, callback) + await _check_system_state(hass, system_only_local, property, False, callback) diff --git a/tests/components/risco/test_config_flow.py b/tests/components/risco/test_config_flow.py index cc6cefc1325..d031f4e8542 100644 --- a/tests/components/risco/test_config_flow.py +++ b/tests/components/risco/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Risco config flow.""" + from unittest.mock import PropertyMock, patch import pytest @@ -65,18 +66,23 @@ async def test_cloud_form(hass: HomeAssistant) -> None: assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {} - with patch( - "homeassistant.components.risco.config_flow.RiscoCloud.login", - return_value=True, - ), patch( - "homeassistant.components.risco.config_flow.RiscoCloud.site_name", - new_callable=PropertyMock(return_value=TEST_SITE_NAME), - ), patch( - "homeassistant.components.risco.config_flow.RiscoCloud.close" - ) as mock_close, patch( - "homeassistant.components.risco.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.risco.config_flow.RiscoCloud.login", + return_value=True, + ), + patch( + "homeassistant.components.risco.config_flow.RiscoCloud.site_name", + new_callable=PropertyMock(return_value=TEST_SITE_NAME), + ), + patch( + "homeassistant.components.risco.config_flow.RiscoCloud.close" + ) as mock_close, + patch( + "homeassistant.components.risco.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], TEST_CLOUD_DATA ) @@ -155,18 +161,23 @@ async def test_form_reauth(hass: HomeAssistant, cloud_config_entry) -> None: assert result["type"] == "form" assert result["errors"] == {} - with patch( - "homeassistant.components.risco.config_flow.RiscoCloud.login", - return_value=True, - ), patch( - "homeassistant.components.risco.config_flow.RiscoCloud.site_name", - new_callable=PropertyMock(return_value=TEST_SITE_NAME), - ), patch( - "homeassistant.components.risco.config_flow.RiscoCloud.close", - ), patch( - "homeassistant.components.risco.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.risco.config_flow.RiscoCloud.login", + return_value=True, + ), + patch( + "homeassistant.components.risco.config_flow.RiscoCloud.site_name", + new_callable=PropertyMock(return_value=TEST_SITE_NAME), + ), + patch( + "homeassistant.components.risco.config_flow.RiscoCloud.close", + ), + patch( + "homeassistant.components.risco.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {**TEST_CLOUD_DATA, CONF_PASSWORD: "new_password"} ) @@ -191,18 +202,23 @@ async def test_form_reauth_with_new_username( assert result["type"] == "form" assert result["errors"] == {} - with patch( - "homeassistant.components.risco.config_flow.RiscoCloud.login", - return_value=True, - ), patch( - "homeassistant.components.risco.config_flow.RiscoCloud.site_name", - new_callable=PropertyMock(return_value=TEST_SITE_NAME), - ), patch( - "homeassistant.components.risco.config_flow.RiscoCloud.close", - ), patch( - "homeassistant.components.risco.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.risco.config_flow.RiscoCloud.login", + return_value=True, + ), + patch( + "homeassistant.components.risco.config_flow.RiscoCloud.site_name", + new_callable=PropertyMock(return_value=TEST_SITE_NAME), + ), + patch( + "homeassistant.components.risco.config_flow.RiscoCloud.close", + ), + patch( + "homeassistant.components.risco.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {**TEST_CLOUD_DATA, CONF_USERNAME: "new_user"} ) @@ -229,18 +245,23 @@ async def test_local_form(hass: HomeAssistant) -> None: assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {} - with patch( - "homeassistant.components.risco.config_flow.RiscoLocal.connect", - return_value=True, - ), patch( - "homeassistant.components.risco.config_flow.RiscoLocal.id", - new_callable=PropertyMock(return_value=TEST_SITE_NAME), - ), patch( - "homeassistant.components.risco.config_flow.RiscoLocal.disconnect" - ) as mock_close, patch( - "homeassistant.components.risco.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.risco.config_flow.RiscoLocal.connect", + return_value=True, + ), + patch( + "homeassistant.components.risco.config_flow.RiscoLocal.id", + new_callable=PropertyMock(return_value=TEST_SITE_NAME), + ), + patch( + "homeassistant.components.risco.config_flow.RiscoLocal.disconnect" + ) as mock_close, + patch( + "homeassistant.components.risco.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], TEST_LOCAL_DATA ) @@ -248,7 +269,8 @@ async def test_local_form(hass: HomeAssistant) -> None: expected_data = { **TEST_LOCAL_DATA, - **{"type": "local", CONF_COMMUNICATION_DELAY: 0}, + "type": "local", + CONF_COMMUNICATION_DELAY: 0, } assert result3["type"] == FlowResultType.CREATE_ENTRY assert result3["title"] == TEST_SITE_NAME @@ -300,14 +322,18 @@ async def test_form_local_already_exists(hass: HomeAssistant) -> None: result["flow_id"], {"next_step_id": "local"} ) - with patch( - "homeassistant.components.risco.config_flow.RiscoLocal.connect", - return_value=True, - ), patch( - "homeassistant.components.risco.config_flow.RiscoLocal.id", - new_callable=PropertyMock(return_value=TEST_SITE_NAME), - ), patch( - "homeassistant.components.risco.config_flow.RiscoLocal.disconnect", + with ( + patch( + "homeassistant.components.risco.config_flow.RiscoLocal.connect", + return_value=True, + ), + patch( + "homeassistant.components.risco.config_flow.RiscoLocal.id", + new_callable=PropertyMock(return_value=TEST_SITE_NAME), + ), + patch( + "homeassistant.components.risco.config_flow.RiscoLocal.disconnect", + ), ): result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], TEST_LOCAL_DATA diff --git a/tests/components/risco/test_sensor.py b/tests/components/risco/test_sensor.py index e8bae275cc2..157eb3e62b5 100644 --- a/tests/components/risco/test_sensor.py +++ b/tests/components/risco/test_sensor.py @@ -1,4 +1,5 @@ """Tests for the Risco event sensors.""" + from datetime import timedelta from unittest.mock import MagicMock, PropertyMock, patch @@ -110,12 +111,15 @@ CATEGORIES_TO_EVENTS = { @pytest.fixture def _no_zones_and_partitions(): - with patch( - "homeassistant.components.risco.RiscoLocal.zones", - new_callable=PropertyMock(return_value=[]), - ), patch( - "homeassistant.components.risco.RiscoLocal.partitions", - new_callable=PropertyMock(return_value=[]), + with ( + patch( + "homeassistant.components.risco.RiscoLocal.zones", + new_callable=PropertyMock(return_value=[]), + ), + patch( + "homeassistant.components.risco.RiscoLocal.partitions", + new_callable=PropertyMock(return_value=[]), + ), ): yield @@ -162,7 +166,8 @@ def _set_utc_time_zone(hass): @pytest.fixture -def _save_mock(): +def save_mock(): + """Create a mock for async_save.""" with patch( "homeassistant.components.risco.Store.async_save", ) as save_mock: @@ -174,7 +179,7 @@ async def test_cloud_setup( hass: HomeAssistant, two_zone_cloud, _set_utc_time_zone, - _save_mock, + save_mock, setup_risco_cloud, ) -> None: """Test entity setup.""" @@ -182,15 +187,18 @@ async def test_cloud_setup( for id in ENTITY_IDS.values(): assert registry.async_is_registered(id) - _save_mock.assert_awaited_once_with({LAST_EVENT_TIMESTAMP_KEY: TEST_EVENTS[0].time}) + save_mock.assert_awaited_once_with({LAST_EVENT_TIMESTAMP_KEY: TEST_EVENTS[0].time}) for category, entity_id in ENTITY_IDS.items(): _check_state(hass, category, entity_id) - with patch( - "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}, + with ( + patch( + "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}, + ), ): async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=65)) await hass.async_block_till_done() diff --git a/tests/components/risco/test_switch.py b/tests/components/risco/test_switch.py index 07058119a62..100796b9ea1 100644 --- a/tests/components/risco/test_switch.py +++ b/tests/components/risco/test_switch.py @@ -1,4 +1,5 @@ """Tests for the Risco binary sensors.""" + from unittest.mock import PropertyMock, patch import pytest @@ -123,16 +124,17 @@ async def _check_local_state(hass, zones, bypassed, entity_id, zone_id, callback @pytest.fixture -def _mock_zone_handler(): +def mock_zone_handler(): + """Create a mock for add_zone_handler.""" with patch("homeassistant.components.risco.RiscoLocal.add_zone_handler") as mock: yield mock async def test_local_states( - hass: HomeAssistant, two_zone_local, _mock_zone_handler, setup_risco_local + hass: HomeAssistant, two_zone_local, mock_zone_handler, setup_risco_local ) -> None: """Test the various alarm states.""" - callback = _mock_zone_handler.call_args.args[0] + callback = mock_zone_handler.call_args.args[0] assert callback is not None diff --git a/tests/components/risco/util.py b/tests/components/risco/util.py index b2600383f2a..275e9db012d 100644 --- a/tests/components/risco/util.py +++ b/tests/components/risco/util.py @@ -1,4 +1,5 @@ """Utilities for Risco tests.""" + from unittest.mock import AsyncMock, MagicMock TEST_SITE_UUID = "test-site-uuid" @@ -10,3 +11,18 @@ def zone_mock(): return MagicMock( triggered=False, bypassed=False, bypass=AsyncMock(return_value=True) ) + + +def system_mock(): + """Return a mocked system.""" + return MagicMock( + low_battery_trouble=False, + ac_trouble=False, + monitoring_station_1_trouble=False, + monitoring_station_2_trouble=False, + monitoring_station_3_trouble=False, + phone_line_trouble=False, + clock_trouble=False, + box_tamper=False, + programming_mode=False, + ) diff --git a/tests/components/rituals_perfume_genie/common.py b/tests/components/rituals_perfume_genie/common.py index f8bcc10ca59..f2a54ca5def 100644 --- a/tests/components/rituals_perfume_genie/common.py +++ b/tests/components/rituals_perfume_genie/common.py @@ -1,4 +1,5 @@ """Common methods used across tests for Rituals Perfume Genie.""" + from __future__ import annotations from unittest.mock import AsyncMock, MagicMock, patch diff --git a/tests/components/rituals_perfume_genie/test_binary_sensor.py b/tests/components/rituals_perfume_genie/test_binary_sensor.py index dae654d6e16..8f94dcc215a 100644 --- a/tests/components/rituals_perfume_genie/test_binary_sensor.py +++ b/tests/components/rituals_perfume_genie/test_binary_sensor.py @@ -1,4 +1,5 @@ """Tests for the Rituals Perfume Genie binary sensor platform.""" + from homeassistant.components.binary_sensor import BinarySensorDeviceClass from homeassistant.const import ATTR_DEVICE_CLASS, STATE_ON, EntityCategory from homeassistant.core import HomeAssistant diff --git a/tests/components/rituals_perfume_genie/test_config_flow.py b/tests/components/rituals_perfume_genie/test_config_flow.py index f656e71b579..45f14399f15 100644 --- a/tests/components/rituals_perfume_genie/test_config_flow.py +++ b/tests/components/rituals_perfume_genie/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Rituals Perfume Genie config flow.""" + from http import HTTPStatus from unittest.mock import AsyncMock, MagicMock, patch @@ -31,13 +32,16 @@ async def test_form(hass: HomeAssistant) -> None: assert result["type"] == "form" assert result["errors"] is None - with patch( - "homeassistant.components.rituals_perfume_genie.config_flow.Account", - side_effect=_mock_account, - ), patch( - "homeassistant.components.rituals_perfume_genie.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.rituals_perfume_genie.config_flow.Account", + side_effect=_mock_account, + ), + patch( + "homeassistant.components.rituals_perfume_genie.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { diff --git a/tests/components/rituals_perfume_genie/test_diagnostics.py b/tests/components/rituals_perfume_genie/test_diagnostics.py index a57f14f9afd..744f064a5da 100644 --- a/tests/components/rituals_perfume_genie/test_diagnostics.py +++ b/tests/components/rituals_perfume_genie/test_diagnostics.py @@ -1,4 +1,5 @@ """Tests for the diagnostics data provided by the Rituals Perfume Genie integration.""" + from syrupy.assertion import SnapshotAssertion from homeassistant.core import HomeAssistant diff --git a/tests/components/rituals_perfume_genie/test_init.py b/tests/components/rituals_perfume_genie/test_init.py index 7f2f06b707c..d1001d1ad93 100644 --- a/tests/components/rituals_perfume_genie/test_init.py +++ b/tests/components/rituals_perfume_genie/test_init.py @@ -1,4 +1,5 @@ """Tests for the Rituals Perfume Genie integration.""" + from unittest.mock import patch import aiohttp diff --git a/tests/components/rituals_perfume_genie/test_number.py b/tests/components/rituals_perfume_genie/test_number.py index 87d81aa8ec0..f88bcc6d0cb 100644 --- a/tests/components/rituals_perfume_genie/test_number.py +++ b/tests/components/rituals_perfume_genie/test_number.py @@ -1,4 +1,5 @@ """Tests for the Rituals Perfume Genie number platform.""" + from __future__ import annotations import pytest @@ -11,7 +12,7 @@ from homeassistant.components.number import ( DOMAIN as NUMBER_DOMAIN, SERVICE_SET_VALUE, ) -from homeassistant.const import ATTR_ENTITY_ID, ATTR_ICON +from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component @@ -35,7 +36,6 @@ async def test_number_entity( state = hass.states.get("number.genie_perfume_amount") assert state assert state.state == str(diffuser.perfume_amount) - assert state.attributes[ATTR_ICON] == "mdi:gauge" assert state.attributes[ATTR_MIN] == 1 assert state.attributes[ATTR_MAX] == 3 diff --git a/tests/components/rituals_perfume_genie/test_select.py b/tests/components/rituals_perfume_genie/test_select.py index a055e8fed05..17612edfd97 100644 --- a/tests/components/rituals_perfume_genie/test_select.py +++ b/tests/components/rituals_perfume_genie/test_select.py @@ -1,4 +1,5 @@ """Tests for the Rituals Perfume Genie select platform.""" + import pytest from homeassistant.components.homeassistant import SERVICE_UPDATE_ENTITY @@ -10,7 +11,6 @@ from homeassistant.components.select import ( from homeassistant.const import ( AREA_SQUARE_METERS, ATTR_ENTITY_ID, - ATTR_ICON, SERVICE_SELECT_OPTION, EntityCategory, ) @@ -33,7 +33,6 @@ async def test_select_entity( state = hass.states.get("select.genie_room_size") assert state assert state.state == str(diffuser.room_size_square_meter) - assert state.attributes[ATTR_ICON] == "mdi:ruler-square" assert state.attributes[ATTR_OPTIONS] == ["15", "30", "60", "100"] entry = entity_registry.async_get("select.genie_room_size") diff --git a/tests/components/rituals_perfume_genie/test_sensor.py b/tests/components/rituals_perfume_genie/test_sensor.py index eb4211f1a20..fd273d238fe 100644 --- a/tests/components/rituals_perfume_genie/test_sensor.py +++ b/tests/components/rituals_perfume_genie/test_sensor.py @@ -1,8 +1,8 @@ """Tests for the Rituals Perfume Genie sensor platform.""" + from homeassistant.components.rituals_perfume_genie.sensor import SensorDeviceClass from homeassistant.const import ( ATTR_DEVICE_CLASS, - ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, PERCENTAGE, EntityCategory, @@ -29,7 +29,6 @@ async def test_sensors_diffuser_v1_battery_cartridge( state = hass.states.get("sensor.genie_perfume") assert state assert state.state == diffuser.perfume - assert state.attributes.get(ATTR_ICON) == "mdi:tag" entry = entity_registry.async_get("sensor.genie_perfume") assert entry @@ -38,7 +37,6 @@ async def test_sensors_diffuser_v1_battery_cartridge( state = hass.states.get("sensor.genie_fill") assert state assert state.state == diffuser.fill - assert state.attributes.get(ATTR_ICON) == "mdi:beaker" entry = entity_registry.async_get("sensor.genie_fill") assert entry diff --git a/tests/components/rituals_perfume_genie/test_switch.py b/tests/components/rituals_perfume_genie/test_switch.py index 69c2dc01923..7e6a94906e1 100644 --- a/tests/components/rituals_perfume_genie/test_switch.py +++ b/tests/components/rituals_perfume_genie/test_switch.py @@ -1,4 +1,5 @@ """Tests for the Rituals Perfume Genie switch platform.""" + from __future__ import annotations from homeassistant.components.homeassistant import SERVICE_UPDATE_ENTITY @@ -6,7 +7,6 @@ from homeassistant.components.rituals_perfume_genie.const import DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.const import ( ATTR_ENTITY_ID, - ATTR_ICON, SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_OFF, @@ -34,7 +34,6 @@ async def test_switch_entity( state = hass.states.get("switch.genie") assert state assert state.state == STATE_ON - assert state.attributes.get(ATTR_ICON) == "mdi:fan" entry = entity_registry.async_get("switch.genie") assert entry diff --git a/tests/components/rmvtransport/test_sensor.py b/tests/components/rmvtransport/test_sensor.py index 92cc04ca3d4..c17eaac2105 100644 --- a/tests/components/rmvtransport/test_sensor.py +++ b/tests/components/rmvtransport/test_sensor.py @@ -1,4 +1,5 @@ """The tests for the rmvtransport platform.""" + import datetime from unittest.mock import patch diff --git a/tests/components/roborock/conftest.py b/tests/components/roborock/conftest.py index efbc2ea7f9d..91331a1486a 100644 --- a/tests/components/roborock/conftest.py +++ b/tests/components/roborock/conftest.py @@ -1,4 +1,5 @@ """Global fixtures for Roborock integration.""" + from unittest.mock import patch import pytest @@ -29,41 +30,50 @@ from tests.common import MockConfigEntry @pytest.fixture(name="bypass_api_fixture") def bypass_api_fixture() -> None: """Skip calls to the API.""" - with patch( - "homeassistant.components.roborock.RoborockMqttClient.async_connect" - ), patch( - "homeassistant.components.roborock.RoborockMqttClient._send_command" - ), patch( - "homeassistant.components.roborock.RoborockApiClient.get_home_data", - return_value=HOME_DATA, - ), patch( - "homeassistant.components.roborock.RoborockMqttClient.get_networking", - return_value=NETWORK_INFO, - ), patch( - "homeassistant.components.roborock.coordinator.RoborockLocalClient.get_prop", - return_value=PROP, - ), patch( - "homeassistant.components.roborock.coordinator.RoborockMqttClient.get_multi_maps_list", - return_value=MULTI_MAP_LIST, - ), patch( - "homeassistant.components.roborock.coordinator.RoborockLocalClient.get_multi_maps_list", - return_value=MULTI_MAP_LIST, - ), patch( - "homeassistant.components.roborock.image.RoborockMapDataParser.parse", - return_value=MAP_DATA, - ), patch( - "homeassistant.components.roborock.coordinator.RoborockLocalClient.send_message" - ), patch( - "homeassistant.components.roborock.RoborockMqttClient._wait_response" - ), patch( - "homeassistant.components.roborock.coordinator.RoborockLocalClient._wait_response" - ), patch( - "roborock.api.AttributeCache.async_value", - ), patch( - "roborock.api.AttributeCache.value", - ), patch( - "homeassistant.components.roborock.image.MAP_SLEEP", - 0, + with ( + patch("homeassistant.components.roborock.RoborockMqttClient.async_connect"), + patch("homeassistant.components.roborock.RoborockMqttClient._send_command"), + patch( + "homeassistant.components.roborock.RoborockApiClient.get_home_data", + return_value=HOME_DATA, + ), + patch( + "homeassistant.components.roborock.RoborockMqttClient.get_networking", + return_value=NETWORK_INFO, + ), + patch( + "homeassistant.components.roborock.coordinator.RoborockLocalClient.get_prop", + return_value=PROP, + ), + patch( + "homeassistant.components.roborock.coordinator.RoborockMqttClient.get_multi_maps_list", + return_value=MULTI_MAP_LIST, + ), + patch( + "homeassistant.components.roborock.coordinator.RoborockLocalClient.get_multi_maps_list", + return_value=MULTI_MAP_LIST, + ), + patch( + "homeassistant.components.roborock.image.RoborockMapDataParser.parse", + return_value=MAP_DATA, + ), + patch( + "homeassistant.components.roborock.coordinator.RoborockLocalClient.send_message" + ), + patch("homeassistant.components.roborock.RoborockMqttClient._wait_response"), + patch( + "homeassistant.components.roborock.coordinator.RoborockLocalClient._wait_response" + ), + patch( + "roborock.api.AttributeCache.async_value", + ), + patch( + "roborock.api.AttributeCache.value", + ), + patch( + "homeassistant.components.roborock.image.MAP_SLEEP", + 0, + ), ): yield diff --git a/tests/components/roborock/mock_data.py b/tests/components/roborock/mock_data.py index 8935a77f142..16ebc8806f9 100644 --- a/tests/components/roborock/mock_data.py +++ b/tests/components/roborock/mock_data.py @@ -1,4 +1,5 @@ """Mock data for Roborock tests.""" + from __future__ import annotations from PIL import Image diff --git a/tests/components/roborock/test_button.py b/tests/components/roborock/test_button.py index 3948e0c161a..5654dac9218 100644 --- a/tests/components/roborock/test_button.py +++ b/tests/components/roborock/test_button.py @@ -1,4 +1,5 @@ """Test Roborock Button platform.""" + from unittest.mock import patch import pytest diff --git a/tests/components/roborock/test_config_flow.py b/tests/components/roborock/test_config_flow.py index e2454b3ad57..b5cff60cddb 100644 --- a/tests/components/roborock/test_config_flow.py +++ b/tests/components/roborock/test_config_flow.py @@ -1,4 +1,5 @@ """Test Roborock config flow.""" + from copy import deepcopy from unittest.mock import patch diff --git a/tests/components/roborock/test_image.py b/tests/components/roborock/test_image.py index 19fa5152e2d..77829e5aaa6 100644 --- a/tests/components/roborock/test_image.py +++ b/tests/components/roborock/test_image.py @@ -1,4 +1,5 @@ """Test Roborock Image platform.""" + import copy from datetime import timedelta from http import HTTPStatus @@ -34,11 +35,14 @@ async def test_floorplan_image( # Copy the device prop so we don't override it prop = copy.deepcopy(PROP) prop.status.in_cleaning = 1 - with patch( - "homeassistant.components.roborock.coordinator.RoborockLocalClient.get_prop", - return_value=prop, - ), patch( - "homeassistant.components.roborock.image.dt_util.utcnow", return_value=now + with ( + patch( + "homeassistant.components.roborock.coordinator.RoborockLocalClient.get_prop", + return_value=prop, + ), + patch( + "homeassistant.components.roborock.image.dt_util.utcnow", return_value=now + ), ): async_fire_time_changed(hass, now) await hass.async_block_till_done() @@ -62,14 +66,18 @@ async def test_floorplan_image_failed_parse( prop = copy.deepcopy(PROP) prop.status.in_cleaning = 1 # Update image, but get none for parse image. - with patch( - "homeassistant.components.roborock.image.RoborockMapDataParser.parse", - return_value=map_data, - ), patch( - "homeassistant.components.roborock.coordinator.RoborockLocalClient.get_prop", - return_value=prop, - ), patch( - "homeassistant.components.roborock.image.dt_util.utcnow", return_value=now + with ( + patch( + "homeassistant.components.roborock.image.RoborockMapDataParser.parse", + return_value=map_data, + ), + patch( + "homeassistant.components.roborock.coordinator.RoborockLocalClient.get_prop", + return_value=prop, + ), + patch( + "homeassistant.components.roborock.image.dt_util.utcnow", return_value=now + ), ): async_fire_time_changed(hass, now) resp = await client.get("/api/image_proxy/image.roborock_s7_maxv_upstairs") diff --git a/tests/components/roborock/test_init.py b/tests/components/roborock/test_init.py index dd85861d53c..08a3afe6c5e 100644 --- a/tests/components/roborock/test_init.py +++ b/tests/components/roborock/test_init.py @@ -1,4 +1,5 @@ """Test for Roborock init.""" + from unittest.mock import patch from roborock import RoborockException, RoborockInvalidCredentials @@ -31,11 +32,14 @@ async def test_config_entry_not_ready( hass: HomeAssistant, mock_roborock_entry: MockConfigEntry ) -> None: """Test that when coordinator update fails, entry retries.""" - with patch( - "homeassistant.components.roborock.RoborockApiClient.get_home_data", - ), patch( - "homeassistant.components.roborock.coordinator.RoborockLocalClient.get_prop", - side_effect=RoborockException(), + with ( + patch( + "homeassistant.components.roborock.RoborockApiClient.get_home_data", + ), + patch( + "homeassistant.components.roborock.coordinator.RoborockLocalClient.get_prop", + side_effect=RoborockException(), + ), ): await async_setup_component(hass, DOMAIN, {}) assert mock_roborock_entry.state is ConfigEntryState.SETUP_RETRY @@ -45,12 +49,15 @@ async def test_config_entry_not_ready_home_data( hass: HomeAssistant, mock_roborock_entry: MockConfigEntry ) -> None: """Test that when we fail to get home data, entry retries.""" - with patch( - "homeassistant.components.roborock.RoborockApiClient.get_home_data", - side_effect=RoborockException(), - ), patch( - "homeassistant.components.roborock.coordinator.RoborockLocalClient.get_prop", - side_effect=RoborockException(), + with ( + patch( + "homeassistant.components.roborock.RoborockApiClient.get_home_data", + side_effect=RoborockException(), + ), + patch( + "homeassistant.components.roborock.coordinator.RoborockLocalClient.get_prop", + side_effect=RoborockException(), + ), ): await async_setup_component(hass, DOMAIN, {}) assert mock_roborock_entry.state is ConfigEntryState.SETUP_RETRY @@ -84,12 +91,15 @@ async def test_cloud_client_fails_props( hass: HomeAssistant, mock_roborock_entry: MockConfigEntry, bypass_api_fixture ) -> None: """Test that if networking succeeds, but we can't communicate with the vacuum, we can't get props, fail.""" - with patch( - "homeassistant.components.roborock.coordinator.RoborockLocalClient.ping", - side_effect=RoborockException(), - ), patch( - "homeassistant.components.roborock.coordinator.RoborockMqttClient.get_prop", - side_effect=RoborockException(), + with ( + patch( + "homeassistant.components.roborock.coordinator.RoborockLocalClient.ping", + side_effect=RoborockException(), + ), + patch( + "homeassistant.components.roborock.coordinator.RoborockMqttClient.get_prop", + side_effect=RoborockException(), + ), ): await async_setup_component(hass, DOMAIN, {}) assert mock_roborock_entry.state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/roborock/test_number.py b/tests/components/roborock/test_number.py index b660bfc2969..1c20a93cace 100644 --- a/tests/components/roborock/test_number.py +++ b/tests/components/roborock/test_number.py @@ -1,4 +1,5 @@ """Test Roborock Number platform.""" + from unittest.mock import patch import pytest diff --git a/tests/components/roborock/test_select.py b/tests/components/roborock/test_select.py index bcea4e6246b..9310d4e2e9a 100644 --- a/tests/components/roborock/test_select.py +++ b/tests/components/roborock/test_select.py @@ -1,4 +1,5 @@ """Test Roborock Select platform.""" + from unittest.mock import patch import pytest @@ -47,10 +48,13 @@ async def test_update_failure( setup_entry: MockConfigEntry, ) -> None: """Test that changing a value will raise a homeassistanterror when it fails.""" - with patch( - "homeassistant.components.roborock.coordinator.RoborockLocalClient.send_message", - side_effect=RoborockException(), - ), pytest.raises(HomeAssistantError): + with ( + patch( + "homeassistant.components.roborock.coordinator.RoborockLocalClient.send_message", + side_effect=RoborockException(), + ), + pytest.raises(HomeAssistantError), + ): await hass.services.async_call( "select", SERVICE_SELECT_OPTION, diff --git a/tests/components/roborock/test_sensor.py b/tests/components/roborock/test_sensor.py index 4966c8fa3be..a5f4164eee1 100644 --- a/tests/components/roborock/test_sensor.py +++ b/tests/components/roborock/test_sensor.py @@ -1,4 +1,5 @@ """Test Roborock Sensors.""" + from unittest.mock import patch from roborock import DeviceData, HomeDataDevice diff --git a/tests/components/roborock/test_switch.py b/tests/components/roborock/test_switch.py index fb301390fee..42a5e92f32a 100644 --- a/tests/components/roborock/test_switch.py +++ b/tests/components/roborock/test_switch.py @@ -1,4 +1,5 @@ """Test Roborock Switch platform.""" + from unittest.mock import patch import pytest diff --git a/tests/components/roborock/test_time.py b/tests/components/roborock/test_time.py index 1cf2fe6bed5..378c642b2f4 100644 --- a/tests/components/roborock/test_time.py +++ b/tests/components/roborock/test_time.py @@ -1,4 +1,5 @@ """Test Roborock Time platform.""" + from datetime import time from unittest.mock import patch diff --git a/tests/components/roborock/test_vacuum.py b/tests/components/roborock/test_vacuum.py index ecc501cc542..a3d5854edd1 100644 --- a/tests/components/roborock/test_vacuum.py +++ b/tests/components/roborock/test_vacuum.py @@ -1,6 +1,6 @@ """Tests for Roborock vacuums.""" - +import copy from typing import Any from unittest.mock import patch @@ -8,6 +8,7 @@ import pytest from roborock import RoborockException from roborock.roborock_typing import RoborockCommand +from homeassistant.components.roborock import DOMAIN from homeassistant.components.vacuum import ( SERVICE_CLEAN_SPOT, SERVICE_LOCATE, @@ -22,8 +23,10 @@ from homeassistant.const import ATTR_ENTITY_ID, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er +from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry +from tests.components.roborock.mock_data import PROP ENTITY_ID = "vacuum.roborock_s7_maxv" DEVICE_ID = "abc123" @@ -90,17 +93,61 @@ async def test_commands( assert mock_send_command.call_args[0][1] == called_params +@pytest.mark.parametrize( + ("in_cleaning_int", "expected_command"), + [ + (0, RoborockCommand.APP_START), + (1, RoborockCommand.APP_START), + (2, RoborockCommand.RESUME_ZONED_CLEAN), + (3, RoborockCommand.RESUME_SEGMENT_CLEAN), + ], +) +async def test_resume_cleaning( + hass: HomeAssistant, + bypass_api_fixture, + mock_roborock_entry: MockConfigEntry, + in_cleaning_int: int, + expected_command: RoborockCommand, +) -> None: + """Test resuming clean on start button when a clean is paused.""" + prop = copy.deepcopy(PROP) + prop.status.in_cleaning = in_cleaning_int + with patch( + "homeassistant.components.roborock.coordinator.RoborockLocalClient.get_prop", + return_value=prop, + ): + await async_setup_component(hass, DOMAIN, {}) + vacuum = hass.states.get(ENTITY_ID) + assert vacuum + + data = {ATTR_ENTITY_ID: ENTITY_ID} + with patch( + "homeassistant.components.roborock.coordinator.RoborockLocalClient.send_command" + ) as mock_send_command: + await hass.services.async_call( + Platform.VACUUM, + SERVICE_START, + data, + blocking=True, + ) + assert mock_send_command.call_count == 1 + assert mock_send_command.call_args[0][0] == expected_command + + async def test_failed_user_command( hass: HomeAssistant, bypass_api_fixture, setup_entry: MockConfigEntry, ) -> None: """Test that when a user sends an invalid command, we raise HomeAssistantError.""" - data = {ATTR_ENTITY_ID: ENTITY_ID, **{"command": "fake_command"}} - with patch( - "homeassistant.components.roborock.coordinator.RoborockLocalClient.send_command", - side_effect=RoborockException(), - ), pytest.raises(HomeAssistantError, match="Error while calling fake_command"): + data = {ATTR_ENTITY_ID: ENTITY_ID, "command": "fake_command"} + with ( + patch( + "homeassistant.components.roborock.coordinator.RoborockLocalClient.send_command", + side_effect=RoborockException(), + ), + pytest.raises(HomeAssistantError, match="Error while calling fake_command"), + ): await hass.services.async_call( Platform.VACUUM, SERVICE_SEND_COMMAND, diff --git a/tests/components/roku/__init__.py b/tests/components/roku/__init__.py index fc12bb9731d..fe3ef215524 100644 --- a/tests/components/roku/__init__.py +++ b/tests/components/roku/__init__.py @@ -1,4 +1,5 @@ """Tests for the Roku component.""" + from ipaddress import ip_address from homeassistant.components import ssdp, zeroconf diff --git a/tests/components/roku/conftest.py b/tests/components/roku/conftest.py index 2015d01ea68..4cec3e233e6 100644 --- a/tests/components/roku/conftest.py +++ b/tests/components/roku/conftest.py @@ -1,4 +1,5 @@ """Fixtures for Roku integration tests.""" + from collections.abc import Generator import json from unittest.mock import MagicMock, patch diff --git a/tests/components/roku/test_binary_sensor.py b/tests/components/roku/test_binary_sensor.py index 14d7eb392ad..076e16ebad0 100644 --- a/tests/components/roku/test_binary_sensor.py +++ b/tests/components/roku/test_binary_sensor.py @@ -1,4 +1,5 @@ """Tests for the sensors provided by the Roku integration.""" + from unittest.mock import MagicMock import pytest @@ -6,12 +7,7 @@ from rokuecp import Device as RokuDevice from homeassistant.components.binary_sensor import STATE_OFF, STATE_ON from homeassistant.components.roku.const import DOMAIN -from homeassistant.const import ( - ATTR_DEVICE_CLASS, - ATTR_FRIENDLY_NAME, - ATTR_ICON, - EntityCategory, -) +from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_FRIENDLY_NAME, EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er @@ -35,7 +31,6 @@ async def test_roku_binary_sensors( 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_ICON) == "mdi:headphones" assert ATTR_DEVICE_CLASS not in state.attributes state = hass.states.get("binary_sensor.my_roku_3_supports_airplay") @@ -46,7 +41,6 @@ async def test_roku_binary_sensors( assert entry.entity_category == EntityCategory.DIAGNOSTIC assert state.state == STATE_OFF assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Roku 3 Supports AirPlay" - assert state.attributes.get(ATTR_ICON) == "mdi:cast-variant" assert ATTR_DEVICE_CLASS not in state.attributes state = hass.states.get("binary_sensor.my_roku_3_supports_ethernet") @@ -57,7 +51,6 @@ async def test_roku_binary_sensors( 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_ICON) == "mdi:ethernet" assert ATTR_DEVICE_CLASS not in state.attributes state = hass.states.get("binary_sensor.my_roku_3_supports_find_remote") @@ -68,7 +61,6 @@ async def test_roku_binary_sensors( 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_ICON) == "mdi:remote" assert ATTR_DEVICE_CLASS not in state.attributes assert entry.device_id @@ -112,7 +104,6 @@ async def test_rokutv_binary_sensors( state.attributes.get(ATTR_FRIENDLY_NAME) == '58" Onn Roku TV Headphones connected' ) - assert state.attributes.get(ATTR_ICON) == "mdi:headphones" assert ATTR_DEVICE_CLASS not in state.attributes state = hass.states.get("binary_sensor.58_onn_roku_tv_supports_airplay") @@ -125,7 +116,6 @@ async def test_rokutv_binary_sensors( assert ( state.attributes.get(ATTR_FRIENDLY_NAME) == '58" Onn Roku TV Supports AirPlay' ) - assert state.attributes.get(ATTR_ICON) == "mdi:cast-variant" assert ATTR_DEVICE_CLASS not in state.attributes state = hass.states.get("binary_sensor.58_onn_roku_tv_supports_ethernet") @@ -138,7 +128,6 @@ async def test_rokutv_binary_sensors( assert ( 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 state = hass.states.get("binary_sensor.58_onn_roku_tv_supports_find_remote") @@ -154,7 +143,6 @@ async def test_rokutv_binary_sensors( state.attributes.get(ATTR_FRIENDLY_NAME) == '58" Onn Roku TV Supports find remote' ) - assert state.attributes.get(ATTR_ICON) == "mdi:remote" assert ATTR_DEVICE_CLASS not in state.attributes assert entry.device_id diff --git a/tests/components/roku/test_config_flow.py b/tests/components/roku/test_config_flow.py index e86084e7718..34640474bcd 100644 --- a/tests/components/roku/test_config_flow.py +++ b/tests/components/roku/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Roku config flow.""" + import dataclasses from unittest.mock import MagicMock diff --git a/tests/components/roku/test_diagnostics.py b/tests/components/roku/test_diagnostics.py index 708e6d3f5e3..37e0d43a582 100644 --- a/tests/components/roku/test_diagnostics.py +++ b/tests/components/roku/test_diagnostics.py @@ -1,4 +1,5 @@ """Tests for the diagnostics data provided by the Roku integration.""" + from rokuecp import Device as RokuDevice from syrupy import SnapshotAssertion diff --git a/tests/components/roku/test_init.py b/tests/components/roku/test_init.py index 7f291f020d9..a4fc8477ac3 100644 --- a/tests/components/roku/test_init.py +++ b/tests/components/roku/test_init.py @@ -1,4 +1,5 @@ """Tests for the Roku integration.""" + from unittest.mock import AsyncMock, MagicMock, patch from rokuecp import RokuConnectionError diff --git a/tests/components/roku/test_media_player.py b/tests/components/roku/test_media_player.py index 776962e071c..ec7213d3b3c 100644 --- a/tests/components/roku/test_media_player.py +++ b/tests/components/roku/test_media_player.py @@ -1,4 +1,5 @@ """Tests for the Roku Media Player platform.""" + from datetime import timedelta from unittest.mock import MagicMock, patch diff --git a/tests/components/roku/test_remote.py b/tests/components/roku/test_remote.py index 5a0f00ab3b6..3d40006a259 100644 --- a/tests/components/roku/test_remote.py +++ b/tests/components/roku/test_remote.py @@ -1,4 +1,5 @@ """The tests for the Roku remote platform.""" + from unittest.mock import MagicMock from homeassistant.components.remote import ( diff --git a/tests/components/roku/test_select.py b/tests/components/roku/test_select.py index 8de1ff5a248..fa93dfd4b8d 100644 --- a/tests/components/roku/test_select.py +++ b/tests/components/roku/test_select.py @@ -1,4 +1,5 @@ """Tests for the Roku select platform.""" + from unittest.mock import MagicMock import pytest @@ -17,7 +18,7 @@ from homeassistant.components.select import ( ATTR_OPTIONS, DOMAIN as SELECT_DOMAIN, ) -from homeassistant.const import ATTR_ENTITY_ID, ATTR_ICON, SERVICE_SELECT_OPTION +from homeassistant.const import ATTR_ENTITY_ID, SERVICE_SELECT_OPTION from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er @@ -49,12 +50,11 @@ async def test_application_state( state = hass.states.get("select.my_roku_3_application") assert state - assert state.attributes.get(ATTR_ICON) == "mdi:application" assert state.attributes.get(ATTR_OPTIONS) == [ "Home", "Amazon Video on Demand", "Free FrameChannel Service", - "MLB.TV" + "\u00AE", + "MLB.TV" + "\u00ae", "Mediafly", "Netflix", "Pandora", @@ -174,7 +174,6 @@ async def test_channel_state( state = hass.states.get("select.58_onn_roku_tv_channel") assert state - assert state.attributes.get(ATTR_ICON) == "mdi:television" assert state.attributes.get(ATTR_OPTIONS) == [ "99.1", "QVC (1.3)", diff --git a/tests/components/roku/test_sensor.py b/tests/components/roku/test_sensor.py index ab7b9ac00f5..2d431e7f5dc 100644 --- a/tests/components/roku/test_sensor.py +++ b/tests/components/roku/test_sensor.py @@ -1,4 +1,5 @@ """Tests for the sensors provided by the Roku integration.""" + from unittest.mock import MagicMock import pytest @@ -7,7 +8,6 @@ from homeassistant.components.roku.const import DOMAIN from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_FRIENDLY_NAME, - ATTR_ICON, STATE_UNKNOWN, EntityCategory, ) @@ -35,7 +35,6 @@ async def test_roku_sensors( assert entry.entity_category == EntityCategory.DIAGNOSTIC assert state.state == "Roku" assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Roku 3 Active app" - assert state.attributes.get(ATTR_ICON) == "mdi:application" assert ATTR_DEVICE_CLASS not in state.attributes state = hass.states.get("sensor.my_roku_3_active_app_id") @@ -46,7 +45,6 @@ async def test_roku_sensors( assert entry.entity_category == EntityCategory.DIAGNOSTIC assert state.state == STATE_UNKNOWN assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Roku 3 Active app ID" - assert state.attributes.get(ATTR_ICON) == "mdi:application-cog" assert ATTR_DEVICE_CLASS not in state.attributes assert entry.device_id @@ -84,7 +82,6 @@ async def test_rokutv_sensors( assert entry.entity_category == EntityCategory.DIAGNOSTIC assert state.state == "Antenna TV" assert state.attributes.get(ATTR_FRIENDLY_NAME) == '58" Onn Roku TV Active app' - assert state.attributes.get(ATTR_ICON) == "mdi:application" assert ATTR_DEVICE_CLASS not in state.attributes state = hass.states.get("sensor.58_onn_roku_tv_active_app_id") @@ -95,7 +92,6 @@ async def test_rokutv_sensors( assert entry.entity_category == EntityCategory.DIAGNOSTIC assert state.state == "tvinput.dtv" assert state.attributes.get(ATTR_FRIENDLY_NAME) == '58" Onn Roku TV Active app ID' - assert state.attributes.get(ATTR_ICON) == "mdi:application-cog" assert ATTR_DEVICE_CLASS not in state.attributes assert entry.device_id diff --git a/tests/components/romy/test_config_flow.py b/tests/components/romy/test_config_flow.py index a24a3f46bfa..480a37fa068 100644 --- a/tests/components/romy/test_config_flow.py +++ b/tests/components/romy/test_config_flow.py @@ -1,4 +1,5 @@ """Test the ROMY config flow.""" + from ipaddress import ip_address from unittest.mock import Mock, PropertyMock, patch diff --git a/tests/components/roomba/test_config_flow.py b/tests/components/roomba/test_config_flow.py index 4d44035893d..282884c0be3 100644 --- a/tests/components/roomba/test_config_flow.py +++ b/tests/components/roomba/test_config_flow.py @@ -1,4 +1,5 @@ """Test the iRobot Roomba config flow.""" + from ipaddress import ip_address from unittest.mock import MagicMock, PropertyMock, patch @@ -98,12 +99,12 @@ def _mocked_discovery(*_): roomba = RoombaInfo( hostname="irobot-BLID", - robotname="robot_name", + robot_name="robot_name", ip=MOCK_IP, mac="mac", - sw="firmware", + firmware="firmware", sku="sku", - cap={"cap": 1}, + capabilities={"cap": 1}, ) roomba_discovery.get_all = MagicMock(return_value=[roomba]) @@ -173,16 +174,20 @@ async def test_form_user_discovery_and_password_fetch(hass: HomeAssistant) -> No assert result2["errors"] is None assert result2["step_id"] == "link" - with patch( - "homeassistant.components.roomba.config_flow.RoombaFactory.create_roomba", - return_value=mocked_roomba, - ), patch( - "homeassistant.components.roomba.config_flow.RoombaPassword", - _mocked_getpassword, - ), patch( - "homeassistant.components.roomba.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.roomba.config_flow.RoombaFactory.create_roomba", + return_value=mocked_roomba, + ), + patch( + "homeassistant.components.roomba.config_flow.RoombaPassword", + _mocked_getpassword, + ), + patch( + "homeassistant.components.roomba.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], {}, @@ -294,16 +299,20 @@ async def test_form_user_discovery_manual_and_auto_password_fetch( assert result3["type"] == data_entry_flow.FlowResultType.FORM assert result3["errors"] is None - with patch( - "homeassistant.components.roomba.config_flow.RoombaFactory.create_roomba", - return_value=mocked_roomba, - ), patch( - "homeassistant.components.roomba.config_flow.RoombaPassword", - _mocked_getpassword, - ), patch( - "homeassistant.components.roomba.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.roomba.config_flow.RoombaFactory.create_roomba", + return_value=mocked_roomba, + ), + patch( + "homeassistant.components.roomba.config_flow.RoombaPassword", + _mocked_getpassword, + ), + patch( + "homeassistant.components.roomba.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result4 = await hass.config_entries.flow.async_configure( result3["flow_id"], {}, @@ -427,16 +436,20 @@ async def test_form_user_discovery_no_devices_found_and_auto_password_fetch( assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] is None - with patch( - "homeassistant.components.roomba.config_flow.RoombaFactory.create_roomba", - return_value=mocked_roomba, - ), patch( - "homeassistant.components.roomba.config_flow.RoombaPassword", - _mocked_getpassword, - ), patch( - "homeassistant.components.roomba.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.roomba.config_flow.RoombaFactory.create_roomba", + return_value=mocked_roomba, + ), + patch( + "homeassistant.components.roomba.config_flow.RoombaPassword", + _mocked_getpassword, + ), + patch( + "homeassistant.components.roomba.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], {}, @@ -500,13 +513,16 @@ async def test_form_user_discovery_no_devices_found_and_password_fetch_fails( ) await hass.async_block_till_done() - with patch( - "homeassistant.components.roomba.config_flow.RoombaFactory.create_roomba", - return_value=mocked_roomba, - ), patch( - "homeassistant.components.roomba.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.roomba.config_flow.RoombaFactory.create_roomba", + return_value=mocked_roomba, + ), + patch( + "homeassistant.components.roomba.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result4 = await hass.config_entries.flow.async_configure( result3["flow_id"], {CONF_PASSWORD: "password"}, @@ -571,13 +587,16 @@ async def test_form_user_discovery_not_devices_found_and_password_fetch_fails_an ) await hass.async_block_till_done() - with patch( - "homeassistant.components.roomba.config_flow.RoombaFactory.create_roomba", - return_value=mocked_roomba, - ), patch( - "homeassistant.components.roomba.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.roomba.config_flow.RoombaFactory.create_roomba", + return_value=mocked_roomba, + ), + patch( + "homeassistant.components.roomba.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result4 = await hass.config_entries.flow.async_configure( result3["flow_id"], {CONF_PASSWORD: "password"}, @@ -630,13 +649,16 @@ async def test_form_user_discovery_and_password_fetch_gets_connection_refused( ) await hass.async_block_till_done() - with patch( - "homeassistant.components.roomba.config_flow.RoombaFactory.create_roomba", - return_value=mocked_roomba, - ), patch( - "homeassistant.components.roomba.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.roomba.config_flow.RoombaFactory.create_roomba", + return_value=mocked_roomba, + ), + patch( + "homeassistant.components.roomba.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result4 = await hass.config_entries.flow.async_configure( result3["flow_id"], {CONF_PASSWORD: "password"}, @@ -684,16 +706,20 @@ async def test_dhcp_discovery_and_roomba_discovery_finds( assert result["step_id"] == "link" assert result["description_placeholders"] == {"name": "robot_name"} - with patch( - "homeassistant.components.roomba.config_flow.RoombaFactory.create_roomba", - return_value=mocked_roomba, - ), patch( - "homeassistant.components.roomba.config_flow.RoombaPassword", - _mocked_getpassword, - ), patch( - "homeassistant.components.roomba.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.roomba.config_flow.RoombaFactory.create_roomba", + return_value=mocked_roomba, + ), + patch( + "homeassistant.components.roomba.config_flow.RoombaPassword", + _mocked_getpassword, + ), + patch( + "homeassistant.components.roomba.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {}, @@ -758,16 +784,20 @@ async def test_dhcp_discovery_falls_back_to_manual( assert result3["type"] == data_entry_flow.FlowResultType.FORM assert result3["errors"] is None - with patch( - "homeassistant.components.roomba.config_flow.RoombaFactory.create_roomba", - return_value=mocked_roomba, - ), patch( - "homeassistant.components.roomba.config_flow.RoombaPassword", - _mocked_getpassword, - ), patch( - "homeassistant.components.roomba.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.roomba.config_flow.RoombaFactory.create_roomba", + return_value=mocked_roomba, + ), + patch( + "homeassistant.components.roomba.config_flow.RoombaPassword", + _mocked_getpassword, + ), + patch( + "homeassistant.components.roomba.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result4 = await hass.config_entries.flow.async_configure( result3["flow_id"], {}, @@ -824,16 +854,20 @@ async def test_dhcp_discovery_no_devices_falls_back_to_manual( assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] is None - with patch( - "homeassistant.components.roomba.config_flow.RoombaFactory.create_roomba", - return_value=mocked_roomba, - ), patch( - "homeassistant.components.roomba.config_flow.RoombaPassword", - _mocked_getpassword, - ), patch( - "homeassistant.components.roomba.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.roomba.config_flow.RoombaFactory.create_roomba", + return_value=mocked_roomba, + ), + patch( + "homeassistant.components.roomba.config_flow.RoombaPassword", + _mocked_getpassword, + ), + patch( + "homeassistant.components.roomba.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], {}, diff --git a/tests/components/roon/test_config_flow.py b/tests/components/roon/test_config_flow.py index 1ce8d716824..c31e689e05b 100644 --- a/tests/components/roon/test_config_flow.py +++ b/tests/components/roon/test_config_flow.py @@ -1,4 +1,5 @@ """Test the roon config flow.""" + from unittest.mock import patch from homeassistant import config_entries, data_entry_flow @@ -72,15 +73,19 @@ class RoonDiscoveryFailedMock(RoonDiscoveryMock): async def test_successful_discovery_and_auth(hass: HomeAssistant) -> None: """Test when discovery and auth both work ok.""" - with patch( - "homeassistant.components.roon.config_flow.RoonApi", - return_value=RoonApiMock(), - ), patch( - "homeassistant.components.roon.config_flow.RoonDiscovery", - return_value=RoonDiscoveryMock(), - ), patch( - "homeassistant.components.roon.async_setup_entry", - return_value=True, + with ( + patch( + "homeassistant.components.roon.config_flow.RoonApi", + return_value=RoonApiMock(), + ), + patch( + "homeassistant.components.roon.config_flow.RoonDiscovery", + return_value=RoonDiscoveryMock(), + ), + patch( + "homeassistant.components.roon.async_setup_entry", + return_value=True, + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -110,15 +115,19 @@ async def test_successful_discovery_and_auth(hass: HomeAssistant) -> None: async def test_unsuccessful_discovery_user_form_and_auth(hass: HomeAssistant) -> None: """Test unsuccessful discover, user adding the host via the form and then successful auth.""" - with patch( - "homeassistant.components.roon.config_flow.RoonApi", - return_value=RoonApiMock(), - ), patch( - "homeassistant.components.roon.config_flow.RoonDiscovery", - return_value=RoonDiscoveryFailedMock(), - ), patch( - "homeassistant.components.roon.async_setup_entry", - return_value=True, + with ( + patch( + "homeassistant.components.roon.config_flow.RoonApi", + return_value=RoonApiMock(), + ), + patch( + "homeassistant.components.roon.config_flow.RoonDiscovery", + return_value=RoonDiscoveryFailedMock(), + ), + patch( + "homeassistant.components.roon.async_setup_entry", + return_value=True, + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -157,12 +166,15 @@ async def test_duplicate_config(hass: HomeAssistant) -> None: hass ) - with patch( - "homeassistant.components.roon.config_flow.RoonApi", - return_value=RoonApiMock(), - ), patch( - "homeassistant.components.roon.config_flow.RoonDiscovery", - return_value=RoonDiscoveryFailedMock(), + with ( + patch( + "homeassistant.components.roon.config_flow.RoonApi", + return_value=RoonApiMock(), + ), + patch( + "homeassistant.components.roon.config_flow.RoonDiscovery", + return_value=RoonDiscoveryFailedMock(), + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -189,21 +201,27 @@ async def test_duplicate_config(hass: HomeAssistant) -> None: async def test_successful_discovery_no_auth(hass: HomeAssistant) -> None: """Test successful discover, but failed auth.""" - with patch( - "homeassistant.components.roon.config_flow.RoonApi", - return_value=RoonApiMockNoToken(), - ), patch( - "homeassistant.components.roon.config_flow.RoonDiscovery", - return_value=RoonDiscoveryMock(), - ), patch( - "homeassistant.components.roon.config_flow.TIMEOUT", - 0, - ), patch( - "homeassistant.components.roon.config_flow.AUTHENTICATE_TIMEOUT", - 0.01, - ), patch( - "homeassistant.components.roon.async_setup_entry", - return_value=True, + with ( + patch( + "homeassistant.components.roon.config_flow.RoonApi", + return_value=RoonApiMockNoToken(), + ), + patch( + "homeassistant.components.roon.config_flow.RoonDiscovery", + return_value=RoonDiscoveryMock(), + ), + patch( + "homeassistant.components.roon.config_flow.TIMEOUT", + 0, + ), + patch( + "homeassistant.components.roon.config_flow.AUTHENTICATE_TIMEOUT", + 0.01, + ), + patch( + "homeassistant.components.roon.async_setup_entry", + return_value=True, + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -226,15 +244,19 @@ async def test_successful_discovery_no_auth(hass: HomeAssistant) -> None: async def test_unexpected_exception(hass: HomeAssistant) -> None: """Test successful discover, and unexpected exception during auth.""" - with patch( - "homeassistant.components.roon.config_flow.RoonApi", - return_value=RoonApiMockException(), - ), patch( - "homeassistant.components.roon.config_flow.RoonDiscovery", - return_value=RoonDiscoveryMock(), - ), patch( - "homeassistant.components.roon.async_setup_entry", - return_value=True, + with ( + patch( + "homeassistant.components.roon.config_flow.RoonApi", + return_value=RoonApiMockException(), + ), + patch( + "homeassistant.components.roon.config_flow.RoonDiscovery", + return_value=RoonDiscoveryMock(), + ), + patch( + "homeassistant.components.roon.async_setup_entry", + return_value=True, + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} diff --git a/tests/components/rova/__init__.py b/tests/components/rova/__init__.py new file mode 100644 index 00000000000..b9b0e68ed3c --- /dev/null +++ b/tests/components/rova/__init__.py @@ -0,0 +1,18 @@ +"""Tests for the Rova component.""" + +from unittest.mock import patch + +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def setup_with_selected_platforms( + hass: HomeAssistant, entry: MockConfigEntry, platforms: list[Platform] +) -> None: + """Set up the Rova integration with the selected platforms.""" + entry.add_to_hass(hass) + with patch("homeassistant.components.rova.PLATFORMS", platforms): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/rova/conftest.py b/tests/components/rova/conftest.py new file mode 100644 index 00000000000..ced0baf5662 --- /dev/null +++ b/tests/components/rova/conftest.py @@ -0,0 +1,48 @@ +"""Common fixtures for Rova tests.""" + +from unittest.mock import MagicMock, patch + +import pytest + +from homeassistant.components.rova.const import ( + CONF_HOUSE_NUMBER, + CONF_HOUSE_NUMBER_SUFFIX, + CONF_ZIP_CODE, + DOMAIN, +) + +from tests.common import MockConfigEntry, load_json_array_fixture + + +@pytest.fixture +def mock_rova(): + """Mock a successful Rova API.""" + api = MagicMock() + + with ( + patch( + "homeassistant.components.rova.config_flow.Rova", + return_value=api, + ) as api, + patch("homeassistant.components.rova.Rova", return_value=api), + ): + api.is_rova_area.return_value = True + api.get_calendar_items.return_value = load_json_array_fixture( + "calendar_items.json", DOMAIN + ) + yield api + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Mock config entry.""" + return MockConfigEntry( + domain=DOMAIN, + unique_id="8381BE13", + title="8381BE 13", + data={ + CONF_ZIP_CODE: "8381BE", + CONF_HOUSE_NUMBER: "13", + CONF_HOUSE_NUMBER_SUFFIX: "", + }, + ) diff --git a/tests/components/rova/fixtures/calendar_items.json b/tests/components/rova/fixtures/calendar_items.json new file mode 100644 index 00000000000..168bedb0d50 --- /dev/null +++ b/tests/components/rova/fixtures/calendar_items.json @@ -0,0 +1,18 @@ +[ + { + "GarbageTypeCode": "GFT", + "Date": "2024-02-21T00:00:00" + }, + { + "GarbageTypeCode": "PAPIER", + "Date": "2024-03-06T00:00:00" + }, + { + "GarbageTypeCode": "PMD", + "Date": "2024-03-12T00:00:00" + }, + { + "GarbageTypeCode": "RESTAFVAL", + "Date": "2024-03-12T00:00:00" + } +] diff --git a/tests/components/rova/snapshots/test_init.ambr b/tests/components/rova/snapshots/test_init.ambr new file mode 100644 index 00000000000..340b0e6d472 --- /dev/null +++ b/tests/components/rova/snapshots/test_init.ambr @@ -0,0 +1,31 @@ +# serializer version: 1 +# name: test_service + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': , + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'rova', + '8381BE13', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': None, + 'model': None, + 'name': '8381BE 13', + 'name_by_user': None, + 'serial_number': None, + 'suggested_area': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- diff --git a/tests/components/rova/snapshots/test_sensor.ambr b/tests/components/rova/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..866f1c735c1 --- /dev/null +++ b/tests/components/rova/snapshots/test_sensor.ambr @@ -0,0 +1,189 @@ +# serializer version: 1 +# name: test_all_entities[sensor.8381be_13_bio-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.8381be_13_bio', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Bio', + 'platform': 'rova', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'bio', + 'unique_id': '8381BE13_gft', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.8381be_13_bio-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'timestamp', + 'friendly_name': '8381BE 13 Bio', + }), + 'context': , + 'entity_id': 'sensor.8381be_13_bio', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2024-02-20T23:00:00+00:00', + }) +# --- +# name: test_all_entities[sensor.8381be_13_paper-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.8381be_13_paper', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Paper', + 'platform': 'rova', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'paper', + 'unique_id': '8381BE13_papier', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.8381be_13_paper-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'timestamp', + 'friendly_name': '8381BE 13 Paper', + }), + 'context': , + 'entity_id': 'sensor.8381be_13_paper', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2024-03-05T23:00:00+00:00', + }) +# --- +# name: test_all_entities[sensor.8381be_13_plastic-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.8381be_13_plastic', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Plastic', + 'platform': 'rova', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'plastic', + 'unique_id': '8381BE13_pmd', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.8381be_13_plastic-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'timestamp', + 'friendly_name': '8381BE 13 Plastic', + }), + 'context': , + 'entity_id': 'sensor.8381be_13_plastic', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2024-03-11T23:00:00+00:00', + }) +# --- +# name: test_all_entities[sensor.8381be_13_residual-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.8381be_13_residual', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Residual', + 'platform': 'rova', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'residual', + 'unique_id': '8381BE13_restafval', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.8381be_13_residual-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'timestamp', + 'friendly_name': '8381BE 13 Residual', + }), + 'context': , + 'entity_id': 'sensor.8381be_13_residual', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2024-03-11T23:00:00+00:00', + }) +# --- diff --git a/tests/components/rova/test_config_flow.py b/tests/components/rova/test_config_flow.py new file mode 100644 index 00000000000..357cd9eb344 --- /dev/null +++ b/tests/components/rova/test_config_flow.py @@ -0,0 +1,270 @@ +"""Tests for the Rova config flow.""" + +from unittest.mock import MagicMock + +import pytest +from requests.exceptions import ConnectTimeout, HTTPError + +from homeassistant import data_entry_flow +from homeassistant.components.rova.const import ( + CONF_HOUSE_NUMBER, + CONF_HOUSE_NUMBER_SUFFIX, + CONF_ZIP_CODE, + DOMAIN, +) +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + +ZIP_CODE = "7991AD" +HOUSE_NUMBER = "10" +HOUSE_NUMBER_SUFFIX = "a" + + +async def test_user(hass: HomeAssistant, mock_rova: MagicMock) -> None: + """Test user config.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result.get("type") == data_entry_flow.FlowResultType.FORM + assert result.get("step_id") == "user" + + # test with all information provided + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={ + CONF_ZIP_CODE: ZIP_CODE, + CONF_HOUSE_NUMBER: HOUSE_NUMBER, + CONF_HOUSE_NUMBER_SUFFIX: HOUSE_NUMBER_SUFFIX, + }, + ) + assert result.get("type") == data_entry_flow.FlowResultType.CREATE_ENTRY + + data = result.get("data") + assert data + assert data[CONF_ZIP_CODE] == ZIP_CODE + assert data[CONF_HOUSE_NUMBER] == HOUSE_NUMBER + assert data[CONF_HOUSE_NUMBER_SUFFIX] == HOUSE_NUMBER_SUFFIX + + +async def test_error_if_not_rova_area( + hass: HomeAssistant, mock_rova: MagicMock +) -> None: + """Test we raise errors if rova does not collect at the given address.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + # test with area where rova does not collect + mock_rova.return_value.is_rova_area.return_value = False + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_ZIP_CODE: ZIP_CODE, + CONF_HOUSE_NUMBER: HOUSE_NUMBER, + CONF_HOUSE_NUMBER_SUFFIX: HOUSE_NUMBER_SUFFIX, + }, + ) + + assert result.get("type") == data_entry_flow.FlowResultType.FORM + assert result.get("errors") == {"base": "invalid_rova_area"} + + # now reset the return value and test if we can recover + mock_rova.return_value.is_rova_area.return_value = True + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_ZIP_CODE: ZIP_CODE, + CONF_HOUSE_NUMBER: HOUSE_NUMBER, + CONF_HOUSE_NUMBER_SUFFIX: HOUSE_NUMBER_SUFFIX, + }, + ) + + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["title"] == f"{ZIP_CODE} {HOUSE_NUMBER} {HOUSE_NUMBER_SUFFIX}" + assert result["data"] == { + CONF_ZIP_CODE: ZIP_CODE, + CONF_HOUSE_NUMBER: HOUSE_NUMBER, + CONF_HOUSE_NUMBER_SUFFIX: HOUSE_NUMBER_SUFFIX, + } + + +async def test_abort_if_already_setup(hass: HomeAssistant) -> None: + """Test we abort if rova is already setup.""" + MockConfigEntry( + domain=DOMAIN, + unique_id=f"{ZIP_CODE}{HOUSE_NUMBER}{HOUSE_NUMBER_SUFFIX}", + data={ + CONF_ZIP_CODE: ZIP_CODE, + CONF_HOUSE_NUMBER: HOUSE_NUMBER, + CONF_HOUSE_NUMBER_SUFFIX: HOUSE_NUMBER_SUFFIX, + }, + ).add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={ + CONF_ZIP_CODE: ZIP_CODE, + CONF_HOUSE_NUMBER: HOUSE_NUMBER, + CONF_HOUSE_NUMBER_SUFFIX: HOUSE_NUMBER_SUFFIX, + }, + ) + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +@pytest.mark.parametrize( + ("exception", "error"), + [ + (ConnectTimeout(), "cannot_connect"), + (HTTPError(), "cannot_connect"), + ], +) +async def test_abort_if_api_throws_exception( + hass: HomeAssistant, exception: Exception, error: str, mock_rova: MagicMock +) -> None: + """Test different exceptions for the Rova entity.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) + + # test with exception + mock_rova.return_value.is_rova_area.side_effect = exception + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_ZIP_CODE: ZIP_CODE, + CONF_HOUSE_NUMBER: HOUSE_NUMBER, + CONF_HOUSE_NUMBER_SUFFIX: HOUSE_NUMBER_SUFFIX, + }, + ) + assert result.get("type") == data_entry_flow.FlowResultType.FORM + assert result.get("errors") == {"base": error} + + # now reset the side effect to see if we can recover + mock_rova.return_value.is_rova_area.side_effect = None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_ZIP_CODE: ZIP_CODE, + CONF_HOUSE_NUMBER: HOUSE_NUMBER, + CONF_HOUSE_NUMBER_SUFFIX: HOUSE_NUMBER_SUFFIX, + }, + ) + + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["title"] == f"{ZIP_CODE} {HOUSE_NUMBER} {HOUSE_NUMBER_SUFFIX}" + assert result["data"] == { + CONF_ZIP_CODE: ZIP_CODE, + CONF_HOUSE_NUMBER: HOUSE_NUMBER, + CONF_HOUSE_NUMBER_SUFFIX: HOUSE_NUMBER_SUFFIX, + } + + +async def test_import(hass: HomeAssistant, mock_rova: MagicMock) -> None: + """Test import flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={ + CONF_ZIP_CODE: ZIP_CODE, + CONF_HOUSE_NUMBER: HOUSE_NUMBER, + CONF_HOUSE_NUMBER_SUFFIX: HOUSE_NUMBER_SUFFIX, + }, + ) + + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["title"] == f"{ZIP_CODE} {HOUSE_NUMBER} {HOUSE_NUMBER_SUFFIX}" + assert result["data"] == { + CONF_ZIP_CODE: ZIP_CODE, + CONF_HOUSE_NUMBER: HOUSE_NUMBER, + CONF_HOUSE_NUMBER_SUFFIX: HOUSE_NUMBER_SUFFIX, + } + + +async def test_import_already_configured( + hass: HomeAssistant, mock_rova: MagicMock +) -> None: + """Test we abort import flow when entry is already configured.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id=f"{ZIP_CODE}{HOUSE_NUMBER}{HOUSE_NUMBER_SUFFIX}", + data={ + CONF_ZIP_CODE: ZIP_CODE, + CONF_HOUSE_NUMBER: HOUSE_NUMBER, + CONF_HOUSE_NUMBER_SUFFIX: HOUSE_NUMBER_SUFFIX, + }, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={ + CONF_ZIP_CODE: ZIP_CODE, + CONF_HOUSE_NUMBER: HOUSE_NUMBER, + CONF_HOUSE_NUMBER_SUFFIX: HOUSE_NUMBER_SUFFIX, + }, + ) + + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_import_if_not_rova_area( + hass: HomeAssistant, mock_rova: MagicMock +) -> None: + """Test we abort if rova does not collect at the given address.""" + + # test with area where rova does not collect + mock_rova.return_value.is_rova_area.return_value = False + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={ + CONF_ZIP_CODE: ZIP_CODE, + CONF_HOUSE_NUMBER: HOUSE_NUMBER, + CONF_HOUSE_NUMBER_SUFFIX: HOUSE_NUMBER_SUFFIX, + }, + ) + + assert result.get("type") == data_entry_flow.FlowResultType.ABORT + assert result.get("reason") == "invalid_rova_area" + + +@pytest.mark.parametrize( + ("exception", "error"), + [ + (ConnectTimeout(), "cannot_connect"), + (HTTPError(), "cannot_connect"), + ], +) +async def test_import_connection_errors( + hass: HomeAssistant, exception: Exception, error: str, mock_rova: MagicMock +) -> None: + """Test import connection errors flow.""" + + # test with HTTPError + mock_rova.return_value.is_rova_area.side_effect = exception + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={ + CONF_ZIP_CODE: ZIP_CODE, + CONF_HOUSE_NUMBER: HOUSE_NUMBER, + CONF_HOUSE_NUMBER_SUFFIX: HOUSE_NUMBER_SUFFIX, + }, + ) + + assert result.get("type") == data_entry_flow.FlowResultType.ABORT + assert result.get("reason") == error diff --git a/tests/components/rova/test_init.py b/tests/components/rova/test_init.py new file mode 100644 index 00000000000..3dff0cf4c27 --- /dev/null +++ b/tests/components/rova/test_init.py @@ -0,0 +1,87 @@ +"""Tests for the Rova integration init.""" + +from unittest.mock import MagicMock + +import pytest +from requests import ConnectTimeout +from syrupy import SnapshotAssertion + +from homeassistant.components.rova import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, issue_registry as ir + +from tests.common import MockConfigEntry +from tests.components.rova import setup_with_selected_platforms + + +async def test_reload( + hass: HomeAssistant, + mock_rova: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test reloading the integration.""" + await setup_with_selected_platforms(hass, mock_config_entry, [Platform.SENSOR]) + + assert mock_config_entry.state == ConfigEntryState.LOADED + + assert await hass.config_entries.async_unload(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.state == ConfigEntryState.NOT_LOADED + + +async def test_service( + hass: HomeAssistant, + mock_rova: MagicMock, + mock_config_entry: MockConfigEntry, + device_registry: dr.DeviceRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test the Rova service.""" + await setup_with_selected_platforms(hass, mock_config_entry, [Platform.SENSOR]) + + device_entry = device_registry.async_get_device( + identifiers={(DOMAIN, mock_config_entry.unique_id)} + ) + assert device_entry is not None + assert device_entry == snapshot + + +@pytest.mark.parametrize( + "method", + [ + "is_rova_area", + "get_calendar_items", + ], +) +async def test_retry_after_failure( + hass: HomeAssistant, + mock_rova: MagicMock, + mock_config_entry: MockConfigEntry, + method: str, +) -> None: + """Test we retry after a failure.""" + getattr(mock_rova, method).side_effect = ConnectTimeout + mock_config_entry.add_to_hass(hass) + assert not await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.state == ConfigEntryState.SETUP_RETRY + + +async def test_issue_if_not_rova_area( + hass: HomeAssistant, + mock_rova: MagicMock, + mock_config_entry: MockConfigEntry, + issue_registry: ir.IssueRegistry, +) -> None: + """Test we create an issue if rova does not collect at the given address.""" + mock_rova.is_rova_area.return_value = False + mock_config_entry.add_to_hass(hass) + assert not await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.state == ConfigEntryState.SETUP_ERROR + assert len(issue_registry.issues) == 1 diff --git a/tests/components/rova/test_sensor.py b/tests/components/rova/test_sensor.py new file mode 100644 index 00000000000..ae8b64363da --- /dev/null +++ b/tests/components/rova/test_sensor.py @@ -0,0 +1,34 @@ +"""Tests for the Rova component.""" + +from unittest.mock import MagicMock + +from syrupy import SnapshotAssertion + +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import setup_with_selected_platforms + +from tests.common import MockConfigEntry + + +async def test_all_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_rova: MagicMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test all entities.""" + await setup_with_selected_platforms(hass, mock_config_entry, [Platform.SENSOR]) + entity_entries = er.async_entries_for_config_entry( + entity_registry, mock_config_entry.entry_id + ) + + assert entity_entries + for entity_entry in entity_entries: + assert hass.states.get(entity_entry.entity_id) == snapshot( + name=f"{entity_entry.entity_id}-state" + ) + assert entity_entry == snapshot(name=f"{entity_entry.entity_id}-entry") diff --git a/tests/components/rpi_power/test_binary_sensor.py b/tests/components/rpi_power/test_binary_sensor.py index 9ba81a69c72..78b7b9261b9 100644 --- a/tests/components/rpi_power/test_binary_sensor.py +++ b/tests/components/rpi_power/test_binary_sensor.py @@ -1,4 +1,5 @@ """Tests for rpi_power binary sensor.""" + from datetime import timedelta import logging from unittest.mock import MagicMock diff --git a/tests/components/rpi_power/test_config_flow.py b/tests/components/rpi_power/test_config_flow.py index 5c474fc0821..1cb9f772d70 100644 --- a/tests/components/rpi_power/test_config_flow.py +++ b/tests/components/rpi_power/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for rpi_power config flow.""" + from unittest.mock import MagicMock from homeassistant.components.rpi_power.const import DOMAIN diff --git a/tests/components/rss_feed_template/test_init.py b/tests/components/rss_feed_template/test_init.py index 6dc247ec6b8..351c9e9d1cb 100644 --- a/tests/components/rss_feed_template/test_init.py +++ b/tests/components/rss_feed_template/test_init.py @@ -1,4 +1,5 @@ """The tests for the rss_feed_api component.""" + from http import HTTPStatus from defusedxml import ElementTree diff --git a/tests/components/rtsp_to_webrtc/conftest.py b/tests/components/rtsp_to_webrtc/conftest.py index edb8c7c4aca..e968df9d860 100644 --- a/tests/components/rtsp_to_webrtc/conftest.py +++ b/tests/components/rtsp_to_webrtc/conftest.py @@ -45,12 +45,15 @@ async def mock_camera(hass) -> AsyncGenerator[None, None]: hass, "camera", {camera.DOMAIN: {"platform": "demo"}} ) await hass.async_block_till_done() - with patch( - "homeassistant.components.demo.camera.Path.read_bytes", - return_value=b"Test", - ), patch( - "homeassistant.components.camera.Camera.stream_source", - return_value=STREAM_SOURCE, + with ( + patch( + "homeassistant.components.demo.camera.Path.read_bytes", + return_value=b"Test", + ), + patch( + "homeassistant.components.camera.Camera.stream_source", + return_value=STREAM_SOURCE, + ), ): yield diff --git a/tests/components/rtsp_to_webrtc/test_config_flow.py b/tests/components/rtsp_to_webrtc/test_config_flow.py index 13885f06d3e..16d4779a92c 100644 --- a/tests/components/rtsp_to_webrtc/test_config_flow.py +++ b/tests/components/rtsp_to_webrtc/test_config_flow.py @@ -26,10 +26,13 @@ async def test_web_full_flow(hass: HomeAssistant) -> None: assert result.get("step_id") == "user" assert result.get("data_schema").schema.get("server_url") == str assert not result.get("errors") - with patch("rtsp_to_webrtc.client.Client.heartbeat"), patch( - "homeassistant.components.rtsp_to_webrtc.async_setup_entry", - return_value=True, - ) as mock_setup: + with ( + patch("rtsp_to_webrtc.client.Client.heartbeat"), + patch( + "homeassistant.components.rtsp_to_webrtc.async_setup_entry", + return_value=True, + ) as mock_setup, + ): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"server_url": "https://example.com"} ) @@ -131,10 +134,13 @@ async def test_hassio_discovery(hass: HomeAssistant) -> None: assert result.get("step_id") == "hassio_confirm" assert result.get("description_placeholders") == {"addon": "RTSPtoWebRTC"} - with patch("rtsp_to_webrtc.client.Client.heartbeat"), patch( - "homeassistant.components.rtsp_to_webrtc.async_setup_entry", - return_value=True, - ) as mock_setup: + with ( + patch("rtsp_to_webrtc.client.Client.heartbeat"), + patch( + "homeassistant.components.rtsp_to_webrtc.async_setup_entry", + return_value=True, + ) as mock_setup, + ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) diff --git a/tests/components/rtsp_to_webrtc/test_diagnostics.py b/tests/components/rtsp_to_webrtc/test_diagnostics.py index 270af9267df..e020ebfd5f3 100644 --- a/tests/components/rtsp_to_webrtc/test_diagnostics.py +++ b/tests/components/rtsp_to_webrtc/test_diagnostics.py @@ -1,4 +1,5 @@ """Test nest diagnostics.""" + from typing import Any from homeassistant.core import HomeAssistant diff --git a/tests/components/ruckus_unleashed/__init__.py b/tests/components/ruckus_unleashed/__init__.py index 06ad2352988..97b554b1eb5 100644 --- a/tests/components/ruckus_unleashed/__init__.py +++ b/tests/components/ruckus_unleashed/__init__.py @@ -1,4 +1,5 @@ """Tests for the Ruckus Unleashed integration.""" + from unittest.mock import AsyncMock, patch from aioruckus import AjaxSession, RuckusAjaxApi diff --git a/tests/components/ruckus_unleashed/test_config_flow.py b/tests/components/ruckus_unleashed/test_config_flow.py index cd74395fa66..ae0ccb0a9b1 100644 --- a/tests/components/ruckus_unleashed/test_config_flow.py +++ b/tests/components/ruckus_unleashed/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Ruckus Unleashed config flow.""" + from copy import deepcopy from datetime import timedelta from unittest.mock import AsyncMock, patch @@ -39,10 +40,13 @@ async def test_form(hass: HomeAssistant) -> None: assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} - with RuckusAjaxApiPatchContext(), patch( - "homeassistant.components.ruckus_unleashed.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + RuckusAjaxApiPatchContext(), + patch( + "homeassistant.components.ruckus_unleashed.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], CONFIG, diff --git a/tests/components/ruckus_unleashed/test_device_tracker.py b/tests/components/ruckus_unleashed/test_device_tracker.py index cda3836a0a4..6da0f68b5d8 100644 --- a/tests/components/ruckus_unleashed/test_device_tracker.py +++ b/tests/components/ruckus_unleashed/test_device_tracker.py @@ -1,4 +1,5 @@ """The sensor tests for the Ruckus Unleashed platform.""" + from datetime import timedelta from unittest.mock import AsyncMock diff --git a/tests/components/ruckus_unleashed/test_init.py b/tests/components/ruckus_unleashed/test_init.py index c8246a5ac1e..48c0a5a270e 100644 --- a/tests/components/ruckus_unleashed/test_init.py +++ b/tests/components/ruckus_unleashed/test_init.py @@ -1,4 +1,5 @@ """Test the Ruckus Unleashed config flow.""" + from unittest.mock import AsyncMock from aioruckus.const import ERROR_CONNECT_TIMEOUT, ERROR_LOGIN_INCORRECT diff --git a/tests/components/ruuvi_gateway/consts.py b/tests/components/ruuvi_gateway/consts.py index bd544fb2098..077c5114acf 100644 --- a/tests/components/ruuvi_gateway/consts.py +++ b/tests/components/ruuvi_gateway/consts.py @@ -1,4 +1,5 @@ """Constants for ruuvi_gateway tests.""" + from __future__ import annotations ASYNC_SETUP_ENTRY = "homeassistant.components.ruuvi_gateway.async_setup_entry" diff --git a/tests/components/ruuvi_gateway/test_config_flow.py b/tests/components/ruuvi_gateway/test_config_flow.py index 42dac479955..e9e8446f8ac 100644 --- a/tests/components/ruuvi_gateway/test_config_flow.py +++ b/tests/components/ruuvi_gateway/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Ruuvi Gateway config flow.""" + from unittest.mock import patch from aioruuvigateway.excs import CannotConnect, InvalidAuth diff --git a/tests/components/ruuvi_gateway/utils.py b/tests/components/ruuvi_gateway/utils.py index 76bde687321..0f928036373 100644 --- a/tests/components/ruuvi_gateway/utils.py +++ b/tests/components/ruuvi_gateway/utils.py @@ -1,4 +1,5 @@ """Utilities for ruuvi_gateway tests.""" + from __future__ import annotations import time diff --git a/tests/components/ruuvitag_ble/fixtures.py b/tests/components/ruuvitag_ble/fixtures.py index 26eee1bac5e..5d6ac9ea470 100644 --- a/tests/components/ruuvitag_ble/fixtures.py +++ b/tests/components/ruuvitag_ble/fixtures.py @@ -1,4 +1,5 @@ """Fixtures for testing RuuviTag BLE.""" + from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo NOT_RUUVITAG_SERVICE_INFO = BluetoothServiceInfo( diff --git a/tests/components/ruuvitag_ble/test_config_flow.py b/tests/components/ruuvitag_ble/test_config_flow.py index dcfda4e30e2..6f668b0168b 100644 --- a/tests/components/ruuvitag_ble/test_config_flow.py +++ b/tests/components/ruuvitag_ble/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Ruuvitag config flow.""" + from unittest.mock import patch import pytest diff --git a/tests/components/rympro/test_config_flow.py b/tests/components/rympro/test_config_flow.py index c20c6c5a699..f5591d8e0c7 100644 --- a/tests/components/rympro/test_config_flow.py +++ b/tests/components/rympro/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Read Your Meter Pro config flow.""" + from unittest.mock import patch import pytest @@ -24,7 +25,8 @@ TEST_DATA = { @pytest.fixture -def _config_entry(hass): +def config_entry(hass: HomeAssistant) -> MockConfigEntry: + """Create a mock config entry.""" config_entry = MockConfigEntry( domain=DOMAIN, data=TEST_DATA, @@ -42,16 +44,20 @@ async def test_form(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.FORM assert result["errors"] is None - with patch( - "homeassistant.components.rympro.config_flow.RymPro.login", - return_value="test-token", - ), patch( - "homeassistant.components.rympro.config_flow.RymPro.account_info", - return_value={"accountNumber": TEST_DATA[CONF_UNIQUE_ID]}, - ), patch( - "homeassistant.components.rympro.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.rympro.config_flow.RymPro.login", + return_value="test-token", + ), + patch( + "homeassistant.components.rympro.config_flow.RymPro.account_info", + return_value={"accountNumber": TEST_DATA[CONF_UNIQUE_ID]}, + ), + patch( + "homeassistant.components.rympro.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -96,16 +102,20 @@ async def test_login_error(hass: HomeAssistant, exception, error) -> None: assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": error} - with patch( - "homeassistant.components.rympro.config_flow.RymPro.login", - return_value="test-token", - ), patch( - "homeassistant.components.rympro.config_flow.RymPro.account_info", - return_value={"accountNumber": TEST_DATA[CONF_UNIQUE_ID]}, - ), patch( - "homeassistant.components.rympro.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.rympro.config_flow.RymPro.login", + return_value="test-token", + ), + patch( + "homeassistant.components.rympro.config_flow.RymPro.account_info", + return_value={"accountNumber": TEST_DATA[CONF_UNIQUE_ID]}, + ), + patch( + "homeassistant.components.rympro.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], { @@ -121,18 +131,21 @@ async def test_login_error(hass: HomeAssistant, exception, error) -> None: assert len(mock_setup_entry.mock_calls) == 1 -async def test_form_already_exists(hass: HomeAssistant, _config_entry) -> None: +async def test_form_already_exists(hass: HomeAssistant, config_entry) -> None: """Test that a flow with an existing account aborts.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch( - "homeassistant.components.rympro.config_flow.RymPro.login", - return_value="test-token", - ), patch( - "homeassistant.components.rympro.config_flow.RymPro.account_info", - return_value={"accountNumber": TEST_DATA[CONF_UNIQUE_ID]}, + with ( + patch( + "homeassistant.components.rympro.config_flow.RymPro.login", + return_value="test-token", + ), + patch( + "homeassistant.components.rympro.config_flow.RymPro.account_info", + return_value={"accountNumber": TEST_DATA[CONF_UNIQUE_ID]}, + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -147,30 +160,34 @@ async def test_form_already_exists(hass: HomeAssistant, _config_entry) -> None: assert result2["reason"] == "already_configured" -async def test_form_reauth(hass: HomeAssistant, _config_entry) -> None: +async def test_form_reauth(hass: HomeAssistant, config_entry) -> None: """Test reauthentication.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={ "source": config_entries.SOURCE_REAUTH, - "entry_id": _config_entry.entry_id, + "entry_id": config_entry.entry_id, }, - data=_config_entry.data, + data=config_entry.data, ) assert result["type"] == "form" assert result["errors"] is None - with patch( - "homeassistant.components.rympro.config_flow.RymPro.login", - return_value="test-token", - ), patch( - "homeassistant.components.rympro.config_flow.RymPro.account_info", - return_value={"accountNumber": TEST_DATA[CONF_UNIQUE_ID]}, - ), patch( - "homeassistant.components.rympro.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.rympro.config_flow.RymPro.login", + return_value="test-token", + ), + patch( + "homeassistant.components.rympro.config_flow.RymPro.account_info", + return_value={"accountNumber": TEST_DATA[CONF_UNIQUE_ID]}, + ), + patch( + "homeassistant.components.rympro.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -182,34 +199,38 @@ async def test_form_reauth(hass: HomeAssistant, _config_entry) -> None: assert result2["type"] == "abort" assert result2["reason"] == "reauth_successful" - assert _config_entry.data[CONF_PASSWORD] == "new_password" + assert config_entry.data[CONF_PASSWORD] == "new_password" assert len(mock_setup_entry.mock_calls) == 1 -async def test_form_reauth_with_new_account(hass: HomeAssistant, _config_entry) -> None: +async def test_form_reauth_with_new_account(hass: HomeAssistant, config_entry) -> None: """Test reauthentication with new account.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={ "source": config_entries.SOURCE_REAUTH, - "entry_id": _config_entry.entry_id, + "entry_id": config_entry.entry_id, }, - data=_config_entry.data, + data=config_entry.data, ) assert result["type"] == "form" assert result["errors"] is None - with patch( - "homeassistant.components.rympro.config_flow.RymPro.login", - return_value="test-token", - ), patch( - "homeassistant.components.rympro.config_flow.RymPro.account_info", - return_value={"accountNumber": "new-account-number"}, - ), patch( - "homeassistant.components.rympro.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.rympro.config_flow.RymPro.login", + return_value="test-token", + ), + patch( + "homeassistant.components.rympro.config_flow.RymPro.account_info", + return_value={"accountNumber": "new-account-number"}, + ), + patch( + "homeassistant.components.rympro.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -221,6 +242,6 @@ async def test_form_reauth_with_new_account(hass: HomeAssistant, _config_entry) assert result2["type"] == "abort" assert result2["reason"] == "reauth_successful" - assert _config_entry.data[CONF_UNIQUE_ID] == "new-account-number" - assert _config_entry.unique_id == "new-account-number" + assert config_entry.data[CONF_UNIQUE_ID] == "new-account-number" + assert config_entry.unique_id == "new-account-number" assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/sabnzbd/conftest.py b/tests/components/sabnzbd/conftest.py index 01cea606654..d1854017452 100644 --- a/tests/components/sabnzbd/conftest.py +++ b/tests/components/sabnzbd/conftest.py @@ -1,4 +1,5 @@ """Configuration for Sabnzbd tests.""" + from collections.abc import Generator from unittest.mock import AsyncMock, patch diff --git a/tests/components/sabnzbd/test_config_flow.py b/tests/components/sabnzbd/test_config_flow.py index 05040186bb3..2da1c7c87db 100644 --- a/tests/components/sabnzbd/test_config_flow.py +++ b/tests/components/sabnzbd/test_config_flow.py @@ -1,4 +1,5 @@ """Define tests for the Sabnzbd config flow.""" + from unittest.mock import AsyncMock, patch from pysabnzbd import SabnzbdApiException diff --git a/tests/components/sabnzbd/test_init.py b/tests/components/sabnzbd/test_init.py index e41a23bbfc9..e666f9f1d3e 100644 --- a/tests/components/sabnzbd/test_init.py +++ b/tests/components/sabnzbd/test_init.py @@ -1,4 +1,5 @@ """Tests for the SABnzbd Integration.""" + from unittest.mock import patch from homeassistant.components.sabnzbd import DEFAULT_NAME, DOMAIN, OLD_SENSOR_KEYS diff --git a/tests/components/samsungtv/__init__.py b/tests/components/samsungtv/__init__.py index be28d6132ab..f77cd7a9b3e 100644 --- a/tests/components/samsungtv/__init__.py +++ b/tests/components/samsungtv/__init__.py @@ -1,4 +1,5 @@ """Tests for the samsungtv component.""" + from __future__ import annotations from datetime import timedelta diff --git a/tests/components/samsungtv/conftest.py b/tests/components/samsungtv/conftest.py index 6754faf2da6..8bef7317918 100644 --- a/tests/components/samsungtv/conftest.py +++ b/tests/components/samsungtv/conftest.py @@ -1,4 +1,5 @@ """Fixtures for Samsung TV.""" + from __future__ import annotations from collections.abc import Awaitable, Callable, Generator @@ -40,14 +41,16 @@ def mock_setup_entry() -> Generator[AsyncMock, None, None]: @pytest.fixture(autouse=True) async def silent_ssdp_scanner(hass): """Start SSDP component and get Scanner, prevent actual SSDP traffic.""" - with patch( - "homeassistant.components.ssdp.Scanner._async_start_ssdp_listeners" - ), patch("homeassistant.components.ssdp.Scanner._async_stop_ssdp_listeners"), patch( - "homeassistant.components.ssdp.Scanner.async_scan" - ), patch( - "homeassistant.components.ssdp.Server._async_start_upnp_servers", - ), patch( - "homeassistant.components.ssdp.Server._async_stop_upnp_servers", + with ( + patch("homeassistant.components.ssdp.Scanner._async_start_ssdp_listeners"), + patch("homeassistant.components.ssdp.Scanner._async_stop_ssdp_listeners"), + patch("homeassistant.components.ssdp.Scanner.async_scan"), + patch( + "homeassistant.components.ssdp.Server._async_start_upnp_servers", + ), + patch( + "homeassistant.components.ssdp.Server._async_stop_upnp_servers", + ), ): yield diff --git a/tests/components/samsungtv/const.py b/tests/components/samsungtv/const.py index 347886419f3..43d240ed779 100644 --- a/tests/components/samsungtv/const.py +++ b/tests/components/samsungtv/const.py @@ -1,4 +1,5 @@ """Constants for the samsungtv tests.""" + from samsungtvws.event import ED_INSTALLED_APP_EVENT from homeassistant.components import ssdp diff --git a/tests/components/samsungtv/snapshots/test_init.ambr b/tests/components/samsungtv/snapshots/test_init.ambr index 9087c1d95f9..404b9a6b3af 100644 --- a/tests/components/samsungtv/snapshots/test_init.ambr +++ b/tests/components/samsungtv/snapshots/test_init.ambr @@ -14,6 +14,7 @@ 'context': , 'entity_id': 'media_player.any', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }) diff --git a/tests/components/samsungtv/test_config_flow.py b/tests/components/samsungtv/test_config_flow.py index 60b89766155..a300c28b945 100644 --- a/tests/components/samsungtv/test_config_flow.py +++ b/tests/components/samsungtv/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for Samsung TV config flow.""" + from copy import deepcopy from ipaddress import ip_address from unittest.mock import ANY, AsyncMock, Mock, call, patch @@ -391,12 +392,15 @@ async def test_user_legacy_not_supported(hass: HomeAssistant) -> None: @pytest.mark.usefixtures("rest_api", "remoteencws_failing") async def test_user_websocket_not_supported(hass: HomeAssistant) -> None: """Test starting a flow by user for not supported device.""" - with patch( - "homeassistant.components.samsungtv.bridge.Remote", - side_effect=OSError("Boom"), - ), patch( - "homeassistant.components.samsungtv.bridge.SamsungTVWSAsyncRemote.open", - side_effect=WebSocketProtocolError("Boom"), + with ( + patch( + "homeassistant.components.samsungtv.bridge.Remote", + side_effect=OSError("Boom"), + ), + patch( + "homeassistant.components.samsungtv.bridge.SamsungTVWSAsyncRemote.open", + side_effect=WebSocketProtocolError("Boom"), + ), ): # websocket device not supported result = await hass.config_entries.flow.async_init( @@ -411,12 +415,15 @@ async def test_user_websocket_access_denied( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test starting a flow by user for not supported device.""" - with patch( - "homeassistant.components.samsungtv.bridge.Remote", - side_effect=OSError("Boom"), - ), patch( - "homeassistant.components.samsungtv.bridge.SamsungTVWSAsyncRemote.open", - side_effect=ConnectionClosedError(rcvd=None, sent=frames.Close(1002, "")), + with ( + patch( + "homeassistant.components.samsungtv.bridge.Remote", + side_effect=OSError("Boom"), + ), + patch( + "homeassistant.components.samsungtv.bridge.SamsungTVWSAsyncRemote.open", + side_effect=ConnectionClosedError(rcvd=None, sent=frames.Close(1002, "")), + ), ): # websocket device not supported result = await hass.config_entries.flow.async_init( @@ -430,12 +437,15 @@ async def test_user_websocket_access_denied( @pytest.mark.usefixtures("rest_api", "remoteencws_failing") async def test_user_websocket_auth_retry(hass: HomeAssistant) -> None: """Test starting a flow by user for not supported device.""" - with patch( - "homeassistant.components.samsungtv.bridge.Remote", - side_effect=OSError("Boom"), - ), patch( - "homeassistant.components.samsungtv.bridge.SamsungTVWSAsyncRemote.open", - side_effect=UnauthorizedError, + with ( + patch( + "homeassistant.components.samsungtv.bridge.Remote", + side_effect=OSError("Boom"), + ), + patch( + "homeassistant.components.samsungtv.bridge.SamsungTVWSAsyncRemote.open", + side_effect=UnauthorizedError, + ), ): # websocket device not supported result = await hass.config_entries.flow.async_init( @@ -444,11 +454,14 @@ async def test_user_websocket_auth_retry(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.FORM assert result["step_id"] == "pairing" assert result["errors"] == {"base": "auth_missing"} - with patch( - "homeassistant.components.samsungtv.bridge.Remote", - side_effect=OSError("Boom"), - ), patch( - "homeassistant.components.samsungtv.bridge.SamsungTVWSAsyncRemote.open", + with ( + patch( + "homeassistant.components.samsungtv.bridge.Remote", + side_effect=OSError("Boom"), + ), + patch( + "homeassistant.components.samsungtv.bridge.SamsungTVWSAsyncRemote.open", + ), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} @@ -465,12 +478,15 @@ async def test_user_websocket_auth_retry(hass: HomeAssistant) -> None: @pytest.mark.usefixtures("rest_api_failing") async def test_user_not_successful(hass: HomeAssistant) -> None: """Test starting a flow by user but no connection found.""" - with patch( - "homeassistant.components.samsungtv.bridge.Remote", - side_effect=OSError("Boom"), - ), patch( - "homeassistant.components.samsungtv.bridge.SamsungTVWSAsyncRemote.open", - side_effect=OSError("Boom"), + with ( + patch( + "homeassistant.components.samsungtv.bridge.Remote", + side_effect=OSError("Boom"), + ), + patch( + "homeassistant.components.samsungtv.bridge.SamsungTVWSAsyncRemote.open", + side_effect=OSError("Boom"), + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA @@ -482,12 +498,15 @@ async def test_user_not_successful(hass: HomeAssistant) -> None: @pytest.mark.usefixtures("rest_api_failing") async def test_user_not_successful_2(hass: HomeAssistant) -> None: """Test starting a flow by user but no connection found.""" - with patch( - "homeassistant.components.samsungtv.bridge.Remote", - side_effect=OSError("Boom"), - ), patch( - "homeassistant.components.samsungtv.bridge.SamsungTVWSAsyncRemote.open", - side_effect=ConnectionFailure("Boom"), + with ( + patch( + "homeassistant.components.samsungtv.bridge.Remote", + side_effect=OSError("Boom"), + ), + patch( + "homeassistant.components.samsungtv.bridge.SamsungTVWSAsyncRemote.open", + side_effect=ConnectionFailure("Boom"), + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA @@ -756,16 +775,19 @@ async def test_ssdp_encrypted_websocket_not_supported( @pytest.mark.usefixtures("rest_api_failing") async def test_ssdp_websocket_cannot_connect(hass: HomeAssistant) -> None: """Test starting a flow from discovery and we cannot connect.""" - with patch( - "homeassistant.components.samsungtv.bridge.Remote", - side_effect=OSError("Boom"), - ), patch( - "homeassistant.components.samsungtv.bridge.SamsungTVEncryptedWSAsyncRemote.start_listening", - side_effect=WebSocketProtocolError("Boom"), - ), patch( - "homeassistant.components.samsungtv.bridge.SamsungTVWSAsyncRemote", - ) as remotews, patch.object( - remotews, "open", side_effect=WebSocketProtocolError("Boom") + with ( + patch( + "homeassistant.components.samsungtv.bridge.Remote", + side_effect=OSError("Boom"), + ), + patch( + "homeassistant.components.samsungtv.bridge.SamsungTVEncryptedWSAsyncRemote.start_listening", + side_effect=WebSocketProtocolError("Boom"), + ), + patch( + "homeassistant.components.samsungtv.bridge.SamsungTVWSAsyncRemote", + ) as remotews, + patch.object(remotews, "open", side_effect=WebSocketProtocolError("Boom")), ): # device not supported result = await hass.config_entries.flow.async_init( @@ -792,15 +814,19 @@ async def test_ssdp_model_not_supported(hass: HomeAssistant) -> None: @pytest.mark.usefixtures("remoteencws_failing") async def test_ssdp_not_successful(hass: HomeAssistant) -> None: """Test starting a flow from discovery but no device found.""" - with patch( - "homeassistant.components.samsungtv.bridge.Remote", - side_effect=OSError("Boom"), - ), patch( - "homeassistant.components.samsungtv.bridge.SamsungTVWSAsyncRemote.open", - side_effect=OSError("Boom"), - ), patch( - "homeassistant.components.samsungtv.bridge.SamsungTVWSBridge.async_device_info", - return_value=MOCK_DEVICE_INFO, + with ( + patch( + "homeassistant.components.samsungtv.bridge.Remote", + side_effect=OSError("Boom"), + ), + patch( + "homeassistant.components.samsungtv.bridge.SamsungTVWSAsyncRemote.open", + side_effect=OSError("Boom"), + ), + patch( + "homeassistant.components.samsungtv.bridge.SamsungTVWSBridge.async_device_info", + return_value=MOCK_DEVICE_INFO, + ), ): # confirm to add the entry result = await hass.config_entries.flow.async_init( @@ -820,15 +846,19 @@ async def test_ssdp_not_successful(hass: HomeAssistant) -> None: @pytest.mark.usefixtures("remoteencws_failing") async def test_ssdp_not_successful_2(hass: HomeAssistant) -> None: """Test starting a flow from discovery but no device found.""" - with patch( - "homeassistant.components.samsungtv.bridge.Remote", - side_effect=OSError("Boom"), - ), patch( - "homeassistant.components.samsungtv.bridge.SamsungTVWSAsyncRemote.open", - side_effect=ConnectionFailure("Boom"), - ), patch( - "homeassistant.components.samsungtv.bridge.SamsungTVWSBridge.async_device_info", - return_value=MOCK_DEVICE_INFO, + with ( + patch( + "homeassistant.components.samsungtv.bridge.Remote", + side_effect=OSError("Boom"), + ), + patch( + "homeassistant.components.samsungtv.bridge.SamsungTVWSAsyncRemote.open", + side_effect=ConnectionFailure("Boom"), + ), + patch( + "homeassistant.components.samsungtv.bridge.SamsungTVWSBridge.async_device_info", + return_value=MOCK_DEVICE_INFO, + ), ): # confirm to add the entry result = await hass.config_entries.flow.async_init( @@ -1038,14 +1068,18 @@ async def test_zeroconf_and_dhcp_same_time(hass: HomeAssistant) -> None: @pytest.mark.usefixtures("remoteencws_failing") async def test_autodetect_websocket(hass: HomeAssistant) -> None: """Test for send key with autodetection of protocol.""" - with patch( - "homeassistant.components.samsungtv.bridge.Remote", - side_effect=OSError("Boom"), - ), patch( - "homeassistant.components.samsungtv.bridge.SamsungTVWSAsyncRemote" - ) as remotews, patch( - "homeassistant.components.samsungtv.bridge.SamsungTVAsyncRest", - ) as rest_api_class: + with ( + patch( + "homeassistant.components.samsungtv.bridge.Remote", + side_effect=OSError("Boom"), + ), + patch( + "homeassistant.components.samsungtv.bridge.SamsungTVWSAsyncRemote" + ) as remotews, + patch( + "homeassistant.components.samsungtv.bridge.SamsungTVAsyncRest", + ) as rest_api_class, + ): remote = Mock(SamsungTVWSAsyncRemote) remote.__aenter__ = AsyncMock(return_value=remote) remote.__aexit__ = AsyncMock(return_value=False) @@ -1085,14 +1119,18 @@ async def test_autodetect_websocket(hass: HomeAssistant) -> None: async def test_websocket_no_mac(hass: HomeAssistant, mac_address: Mock) -> None: """Test for send key with autodetection of protocol.""" mac_address.return_value = "gg:ee:tt:mm:aa:cc" - with patch( - "homeassistant.components.samsungtv.bridge.Remote", - side_effect=OSError("Boom"), - ), patch( - "homeassistant.components.samsungtv.bridge.SamsungTVWSAsyncRemote" - ) as remotews, patch( - "homeassistant.components.samsungtv.bridge.SamsungTVAsyncRest", - ) as rest_api_class: + with ( + patch( + "homeassistant.components.samsungtv.bridge.Remote", + side_effect=OSError("Boom"), + ), + patch( + "homeassistant.components.samsungtv.bridge.SamsungTVWSAsyncRemote" + ) as remotews, + patch( + "homeassistant.components.samsungtv.bridge.SamsungTVAsyncRest", + ) as rest_api_class, + ): remote = Mock(SamsungTVWSAsyncRemote) remote.__aenter__ = AsyncMock(return_value=remote) remote.__aexit__ = AsyncMock(return_value=False) @@ -1188,13 +1226,16 @@ async def test_autodetect_legacy(hass: HomeAssistant) -> None: async def test_autodetect_none(hass: HomeAssistant) -> None: """Test for send key with autodetection of protocol.""" - with patch( - "homeassistant.components.samsungtv.bridge.Remote", - side_effect=OSError("Boom"), - ) as remote, patch( - "homeassistant.components.samsungtv.bridge.SamsungTVAsyncRest.rest_device_info", - side_effect=ResponseError, - ) as rest_device_info: + with ( + patch( + "homeassistant.components.samsungtv.bridge.Remote", + side_effect=OSError("Boom"), + ) as remote, + patch( + "homeassistant.components.samsungtv.bridge.SamsungTVAsyncRest.rest_device_info", + side_effect=ResponseError, + ) as rest_device_info, + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA ) @@ -1617,12 +1658,15 @@ async def test_update_legacy_missing_mac_from_dhcp_no_unique_id( data=MOCK_LEGACY_ENTRY, ) entry.add_to_hass(hass) - with patch( - "homeassistant.components.samsungtv.bridge.Remote.__enter__", - return_value=True, - ), patch( - "homeassistant.components.samsungtv.bridge.SamsungTVEncryptedWSAsyncRemote.start_listening", - side_effect=WebSocketProtocolError("Boom"), + with ( + patch( + "homeassistant.components.samsungtv.bridge.Remote.__enter__", + return_value=True, + ), + patch( + "homeassistant.components.samsungtv.bridge.SamsungTVEncryptedWSAsyncRemote.start_listening", + side_effect=WebSocketProtocolError("Boom"), + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, diff --git a/tests/components/samsungtv/test_device_trigger.py b/tests/components/samsungtv/test_device_trigger.py index 92df6356f58..a1fb585bfaa 100644 --- a/tests/components/samsungtv/test_device_trigger.py +++ b/tests/components/samsungtv/test_device_trigger.py @@ -1,4 +1,5 @@ """The tests for Samsung TV device triggers.""" + import pytest from homeassistant.components import automation diff --git a/tests/components/samsungtv/test_diagnostics.py b/tests/components/samsungtv/test_diagnostics.py index 651b6f27a44..2e590518187 100644 --- a/tests/components/samsungtv/test_diagnostics.py +++ b/tests/components/samsungtv/test_diagnostics.py @@ -1,4 +1,5 @@ """Test samsungtv diagnostics.""" + from unittest.mock import Mock import pytest diff --git a/tests/components/samsungtv/test_init.py b/tests/components/samsungtv/test_init.py index cde5cebcefe..5bf8f2cacac 100644 --- a/tests/components/samsungtv/test_init.py +++ b/tests/components/samsungtv/test_init.py @@ -1,4 +1,5 @@ """Tests for the Samsung TV Integration.""" + from unittest.mock import AsyncMock, Mock, patch import pytest @@ -75,17 +76,20 @@ async def test_setup(hass: HomeAssistant) -> None: async def test_setup_without_port_device_offline(hass: HomeAssistant) -> None: """Test import from yaml when the device is offline.""" - with patch( - "homeassistant.components.samsungtv.bridge.Remote", side_effect=OSError - ), patch( - "homeassistant.components.samsungtv.bridge.SamsungTVEncryptedWSAsyncRemote.start_listening", - side_effect=OSError, - ), patch( - "homeassistant.components.samsungtv.bridge.SamsungTVWSAsyncRemote.open", - side_effect=OSError, - ), patch( - "homeassistant.components.samsungtv.bridge.SamsungTVWSBridge.async_device_info", - return_value=None, + with ( + patch("homeassistant.components.samsungtv.bridge.Remote", side_effect=OSError), + patch( + "homeassistant.components.samsungtv.bridge.SamsungTVEncryptedWSAsyncRemote.start_listening", + side_effect=OSError, + ), + patch( + "homeassistant.components.samsungtv.bridge.SamsungTVWSAsyncRemote.open", + side_effect=OSError, + ), + patch( + "homeassistant.components.samsungtv.bridge.SamsungTVWSBridge.async_device_info", + return_value=None, + ), ): await setup_samsungtv_entry(hass, MOCK_CONFIG) diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index c4c6a08b88b..db4f3f0e41f 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -1,4 +1,5 @@ """Tests for samsungtv component.""" + from copy import deepcopy from datetime import datetime, timedelta import logging @@ -199,7 +200,7 @@ async def test_setup_websocket_2( next_update = mock_now + timedelta(minutes=5) freezer.move_to(next_update) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(entity_id) assert state @@ -224,7 +225,7 @@ async def test_setup_encrypted_websocket( next_update = mock_now + timedelta(minutes=5) freezer.move_to(next_update) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(ENTITY_ID) assert state @@ -241,7 +242,7 @@ async def test_update_on( next_update = mock_now + timedelta(minutes=5) freezer.move_to(next_update) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(ENTITY_ID) assert state.state == STATE_ON @@ -261,7 +262,7 @@ async def test_update_off( next_update = mock_now + timedelta(minutes=5) freezer.move_to(next_update) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(ENTITY_ID) assert state.state == STATE_UNAVAILABLE @@ -289,7 +290,7 @@ async def test_update_off_ws_no_power_state( next_update = mock_now + timedelta(minutes=5) freezer.move_to(next_update) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(ENTITY_ID) assert state.state == STATE_OFF @@ -305,11 +306,14 @@ async def test_update_off_ws_with_power_state( mock_now: datetime, ) -> None: """Testing update tv off.""" - with patch.object( - rest_api, "rest_device_info", side_effect=HttpApiError - ) as mock_device_info, patch.object( - remotews, "start_listening", side_effect=WebSocketException("Boom") - ) as mock_start_listening: + with ( + patch.object( + rest_api, "rest_device_info", side_effect=HttpApiError + ) as mock_device_info, + patch.object( + remotews, "start_listening", side_effect=WebSocketException("Boom") + ) as mock_start_listening, + ): await setup_samsungtv_entry(hass, MOCK_CONFIGWS) mock_device_info.assert_called_once() @@ -325,7 +329,7 @@ async def test_update_off_ws_with_power_state( next_update = mock_now + timedelta(minutes=1) freezer.move_to(next_update) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) remotews.start_listening.assert_called_once() rest_api.rest_device_info.assert_called_once() @@ -341,7 +345,7 @@ async def test_update_off_ws_with_power_state( next_update = mock_now + timedelta(minutes=2) freezer.move_to(next_update) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) rest_api.rest_device_info.assert_called_once() @@ -354,7 +358,7 @@ async def test_update_off_ws_with_power_state( next_update = mock_now + timedelta(minutes=3) freezer.move_to(next_update) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) rest_api.rest_device_info.assert_called_once() @@ -385,7 +389,7 @@ async def test_update_off_encryptedws( next_update = mock_now + timedelta(minutes=5) freezer.move_to(next_update) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(ENTITY_ID) assert state.state == STATE_OFF @@ -406,12 +410,12 @@ async def test_update_access_denied( next_update = mock_now + timedelta(minutes=5) freezer.move_to(next_update) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) next_update = mock_now + timedelta(minutes=10) freezer.move_to(next_update) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert [ flow @@ -433,15 +437,18 @@ async def test_update_ws_connection_failure( """Testing update tv connection failure exception.""" await setup_samsungtv_entry(hass, MOCK_CONFIGWS) - with patch.object( - remotews, - "start_listening", - side_effect=ConnectionFailure('{"event": "ms.voiceApp.hide"}'), - ), patch.object(remotews, "is_alive", return_value=False): + with ( + patch.object( + remotews, + "start_listening", + side_effect=ConnectionFailure('{"event": "ms.voiceApp.hide"}'), + ), + patch.object(remotews, "is_alive", return_value=False), + ): next_update = mock_now + timedelta(minutes=5) freezer.move_to(next_update) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert ( "Unexpected ConnectionFailure trying to get remote for fake_host, please " @@ -463,13 +470,16 @@ async def test_update_ws_connection_closed( """Testing update tv connection failure exception.""" await setup_samsungtv_entry(hass, MOCK_CONFIGWS) - with patch.object( - remotews, "start_listening", side_effect=ConnectionClosedError(None, None) - ), patch.object(remotews, "is_alive", return_value=False): + with ( + patch.object( + remotews, "start_listening", side_effect=ConnectionClosedError(None, None) + ), + patch.object(remotews, "is_alive", return_value=False), + ): next_update = mock_now + timedelta(minutes=5) freezer.move_to(next_update) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(ENTITY_ID) assert state.state == STATE_OFF @@ -485,13 +495,14 @@ async def test_update_ws_unauthorized_error( """Testing update tv unauthorized failure exception.""" await setup_samsungtv_entry(hass, MOCK_CONFIGWS) - with patch.object( - remotews, "start_listening", side_effect=UnauthorizedError - ), patch.object(remotews, "is_alive", return_value=False): + with ( + patch.object(remotews, "start_listening", side_effect=UnauthorizedError), + patch.object(remotews, "is_alive", return_value=False), + ): next_update = mock_now + timedelta(minutes=5) freezer.move_to(next_update) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert [ flow @@ -516,7 +527,7 @@ async def test_update_unhandled_response( next_update = mock_now + timedelta(minutes=5) freezer.move_to(next_update) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(ENTITY_ID) assert state.state == STATE_ON @@ -536,7 +547,7 @@ async def test_connection_closed_during_update_can_recover( next_update = mock_now + timedelta(minutes=5) freezer.move_to(next_update) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(ENTITY_ID) assert state.state == STATE_UNAVAILABLE @@ -544,7 +555,7 @@ async def test_connection_closed_during_update_can_recover( next_update = mock_now + timedelta(minutes=10) freezer.move_to(next_update) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(ENTITY_ID) assert state.state == STATE_ON @@ -704,7 +715,7 @@ async def test_state(hass: HomeAssistant, freezer: FrozenDateTimeFactory) -> Non ): freezer.move_to(next_update) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(ENTITY_ID) # Should be STATE_UNAVAILABLE since there is no way to turn it back on @@ -1437,13 +1448,16 @@ async def test_upnp_re_subscribe_events( assert dmr_device.async_subscribe_services.call_count == 1 assert dmr_device.async_unsubscribe_services.call_count == 0 - with patch.object( - remotews, "start_listening", side_effect=WebSocketException("Boom") - ), patch.object(remotews, "is_alive", return_value=False): + with ( + patch.object( + remotews, "start_listening", side_effect=WebSocketException("Boom") + ), + patch.object(remotews, "is_alive", return_value=False), + ): next_update = mock_now + timedelta(minutes=5) freezer.move_to(next_update) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(ENTITY_ID) assert state.state == STATE_OFF @@ -1453,7 +1467,7 @@ async def test_upnp_re_subscribe_events( next_update = mock_now + timedelta(minutes=10) freezer.move_to(next_update) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(ENTITY_ID) assert state.state == STATE_ON @@ -1483,13 +1497,16 @@ async def test_upnp_failed_re_subscribe_events( assert dmr_device.async_subscribe_services.call_count == 1 assert dmr_device.async_unsubscribe_services.call_count == 0 - with patch.object( - remotews, "start_listening", side_effect=WebSocketException("Boom") - ), patch.object(remotews, "is_alive", return_value=False): + with ( + patch.object( + remotews, "start_listening", side_effect=WebSocketException("Boom") + ), + patch.object(remotews, "is_alive", return_value=False), + ): next_update = mock_now + timedelta(minutes=5) freezer.move_to(next_update) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(ENTITY_ID) assert state.state == STATE_OFF @@ -1500,7 +1517,7 @@ async def test_upnp_failed_re_subscribe_events( with patch.object(dmr_device, "async_subscribe_services", side_effect=error): freezer.move_to(next_update) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(ENTITY_ID) assert state.state == STATE_ON diff --git a/tests/components/samsungtv/test_remote.py b/tests/components/samsungtv/test_remote.py index 88cf47bf148..1f9115afca5 100644 --- a/tests/components/samsungtv/test_remote.py +++ b/tests/components/samsungtv/test_remote.py @@ -1,4 +1,5 @@ """The tests for the SamsungTV remote platform.""" + from unittest.mock import Mock import pytest diff --git a/tests/components/samsungtv/test_trigger.py b/tests/components/samsungtv/test_trigger.py index 12af639b251..0bf57a899a9 100644 --- a/tests/components/samsungtv/test_trigger.py +++ b/tests/components/samsungtv/test_trigger.py @@ -1,4 +1,5 @@ """The tests for WebOS TV automation triggers.""" + from unittest.mock import patch import pytest diff --git a/tests/components/scene/common.py b/tests/components/scene/common.py index cdf124add29..e20da63c402 100644 --- a/tests/components/scene/common.py +++ b/tests/components/scene/common.py @@ -3,6 +3,7 @@ All containing methods are legacy helpers that should not be used by new components. Instead call the service directly. """ + from homeassistant.components.scene import DOMAIN from homeassistant.const import ATTR_ENTITY_ID, ENTITY_MATCH_ALL, SERVICE_TURN_ON from homeassistant.loader import bind_hass diff --git a/tests/components/scene/test_init.py b/tests/components/scene/test_init.py index e0b2af87187..a878b27614e 100644 --- a/tests/components/scene/test_init.py +++ b/tests/components/scene/test_init.py @@ -1,4 +1,5 @@ """The tests for the Scene component.""" + import io from unittest.mock import patch @@ -17,15 +18,23 @@ from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util from homeassistant.util.yaml import loader as yaml_loader -from tests.common import async_mock_service, mock_restore_cache +from tests.common import ( + async_mock_service, + mock_restore_cache, + setup_test_component_platform, +) +from tests.components.light.common import MockLight @pytest.fixture(autouse=True) -def entities(hass): +def entities( + hass: HomeAssistant, + mock_light_entities: list[MockLight], +) -> list[MockLight]: """Initialize the test light.""" - platform = getattr(hass.components, "test.light") - platform.init() - return platform.ENTITIES[0:2] + entities = mock_light_entities[0:2] + setup_test_component_platform(hass, light.DOMAIN, entities) + return entities async def test_config_yaml_alias_anchor( @@ -227,6 +236,7 @@ async def activate(hass, entity_id=ENTITY_MATCH_ALL): async def test_services_registered(hass: HomeAssistant) -> None: """Test we register services with empty config.""" assert await async_setup_component(hass, "scene", {}) + await hass.async_block_till_done() assert hass.services.has_service("scene", "reload") assert hass.services.has_service("scene", "turn_on") assert hass.services.has_service("scene", "apply") @@ -261,3 +271,15 @@ async def turn_off_lights(hass, entity_ids): blocking=True, ) await hass.async_block_till_done() + + +async def test_invalid_platform( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test invalid platform.""" + await async_setup_component( + hass, scene.DOMAIN, {scene.DOMAIN: {"platform": "does_not_exist"}} + ) + await hass.async_block_till_done() + assert "Invalid platform specified" in caplog.text + assert "does_not_exist" in caplog.text diff --git a/tests/components/schedule/test_init.py b/tests/components/schedule/test_init.py index 70ba6dfde3c..ddb98cee39d 100644 --- a/tests/components/schedule/test_init.py +++ b/tests/components/schedule/test_init.py @@ -1,4 +1,5 @@ """Test for the Schedule integration.""" + from __future__ import annotations from collections.abc import Callable, Coroutine @@ -117,7 +118,7 @@ async def test_invalid_config(hass: HomeAssistant) -> None: @pytest.mark.parametrize( ("schedule", "error"), - ( + [ ( [ {CONF_FROM: "00:00:00", CONF_TO: "23:59:59"}, @@ -152,7 +153,7 @@ async def test_invalid_config(hass: HomeAssistant) -> None: ], "Invalid time range, from 06:00:00 is after 05:00:00", ), - ), + ], ) async def test_invalid_schedules( hass: HomeAssistant, @@ -418,10 +419,10 @@ async def test_non_adjacent_within_day( @pytest.mark.parametrize( "schedule", - ( + [ {CONF_FROM: "00:00:00", CONF_TO: "24:00"}, {CONF_FROM: "00:00:00", CONF_TO: "24:00:00"}, - ), + ], ) async def test_to_midnight( hass: HomeAssistant, @@ -594,11 +595,11 @@ async def test_ws_delete( @pytest.mark.freeze_time("2022-08-10 20:10:00-07:00") @pytest.mark.parametrize( ("to", "next_event", "saved_to"), - ( + [ ("23:59:59", "2022-08-10T23:59:59-07:00", "23:59:59"), ("24:00", "2022-08-11T00:00:00-07:00", "24:00:00"), ("24:00:00", "2022-08-11T00:00:00-07:00", "24:00:00"), - ), + ], ) async def test_update( hass: HomeAssistant, @@ -664,11 +665,11 @@ async def test_update( @pytest.mark.freeze_time("2022-08-11 8:52:00-07:00") @pytest.mark.parametrize( ("to", "next_event", "saved_to"), - ( + [ ("14:00:00", "2022-08-15T14:00:00-07:00", "14:00:00"), ("24:00", "2022-08-16T00:00:00-07:00", "24:00:00"), ("24:00:00", "2022-08-16T00:00:00-07:00", "24:00:00"), - ), + ], ) async def test_ws_create( hass: HomeAssistant, diff --git a/tests/components/schedule/test_recorder.py b/tests/components/schedule/test_recorder.py index 58a171f9102..df28730ee79 100644 --- a/tests/components/schedule/test_recorder.py +++ b/tests/components/schedule/test_recorder.py @@ -1,4 +1,5 @@ """The tests for recorder platform.""" + from __future__ import annotations from datetime import timedelta diff --git a/tests/components/schlage/conftest.py b/tests/components/schlage/conftest.py index 5f9676b7d09..40d880b73f8 100644 --- a/tests/components/schlage/conftest.py +++ b/tests/components/schlage/conftest.py @@ -1,4 +1,5 @@ """Common fixtures for the Schlage tests.""" + from collections.abc import Generator from unittest.mock import AsyncMock, Mock, create_autospec, patch diff --git a/tests/components/schlage/test_binary_sensor.py b/tests/components/schlage/test_binary_sensor.py index 4673f263c8c..97f11577b86 100644 --- a/tests/components/schlage/test_binary_sensor.py +++ b/tests/components/schlage/test_binary_sensor.py @@ -22,7 +22,7 @@ async def test_keypad_disabled_binary_sensor( # Make the coordinator refresh data. async_fire_time_changed(hass, utcnow() + timedelta(seconds=31)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) keypad = hass.states.get("binary_sensor.vault_door_keypad_disabled") assert keypad is not None @@ -43,7 +43,7 @@ async def test_keypad_disabled_binary_sensor_use_previous_logs_on_failure( # Make the coordinator refresh data. async_fire_time_changed(hass, utcnow() + timedelta(seconds=31)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) keypad = hass.states.get("binary_sensor.vault_door_keypad_disabled") assert keypad is not None diff --git a/tests/components/schlage/test_config_flow.py b/tests/components/schlage/test_config_flow.py index 14121f5d9ca..118ae44d15b 100644 --- a/tests/components/schlage/test_config_flow.py +++ b/tests/components/schlage/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Schlage config flow.""" + from unittest.mock import AsyncMock, Mock from pyschlage.exceptions import Error as PyschlageError, NotAuthorizedError diff --git a/tests/components/schlage/test_lock.py b/tests/components/schlage/test_lock.py index 0972aa97033..5b26da7b27e 100644 --- a/tests/components/schlage/test_lock.py +++ b/tests/components/schlage/test_lock.py @@ -59,7 +59,7 @@ async def test_changed_by( # Make the coordinator refresh data. async_fire_time_changed(hass, utcnow() + timedelta(seconds=31)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) mock_lock.last_changed_by.assert_called_once_with() lock_device = hass.states.get("lock.vault_door") diff --git a/tests/components/schlage/test_switch.py b/tests/components/schlage/test_switch.py index 30e56b0686f..bf74a79b406 100644 --- a/tests/components/schlage/test_switch.py +++ b/tests/components/schlage/test_switch.py @@ -1,4 +1,5 @@ """Test schlage switch.""" + from unittest.mock import Mock from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN diff --git a/tests/components/scrape/__init__.py b/tests/components/scrape/__init__.py index 3d57970a528..de061d051b2 100644 --- a/tests/components/scrape/__init__.py +++ b/tests/components/scrape/__init__.py @@ -1,4 +1,5 @@ """Tests for scrape component.""" + from __future__ import annotations from typing import Any diff --git a/tests/components/scrape/conftest.py b/tests/components/scrape/conftest.py index 026daeea38c..a7181943884 100644 --- a/tests/components/scrape/conftest.py +++ b/tests/components/scrape/conftest.py @@ -1,4 +1,5 @@ """Fixtures for the Scrape integration.""" + from __future__ import annotations from collections.abc import Generator diff --git a/tests/components/scrape/test_config_flow.py b/tests/components/scrape/test_config_flow.py index 7dd2954f8c3..8e281e148fc 100644 --- a/tests/components/scrape/test_config_flow.py +++ b/tests/components/scrape/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Scrape config flow.""" + from __future__ import annotations from unittest.mock import AsyncMock, patch @@ -330,9 +331,12 @@ async def test_options_add_remove_sensor_flow( assert result["step_id"] == "add_sensor" mocker = MockRestData("test_scrape_sensor2") - with patch("homeassistant.components.rest.RestData", return_value=mocker), patch( - "homeassistant.components.scrape.config_flow.uuid.uuid1", - return_value=uuid.UUID("3699ef88-69e6-11ed-a1eb-0242ac120003"), + with ( + patch("homeassistant.components.rest.RestData", return_value=mocker), + patch( + "homeassistant.components.scrape.config_flow.uuid.uuid1", + return_value=uuid.UUID("3699ef88-69e6-11ed-a1eb-0242ac120003"), + ), ): result = await hass.config_entries.options.async_configure( result["flow_id"], diff --git a/tests/components/scrape/test_init.py b/tests/components/scrape/test_init.py index 638e25a6e05..8ad766a80bd 100644 --- a/tests/components/scrape/test_init.py +++ b/tests/components/scrape/test_init.py @@ -1,4 +1,5 @@ """Test Scrape component setup process.""" + from __future__ import annotations from unittest.mock import patch diff --git a/tests/components/scrape/test_sensor.py b/tests/components/scrape/test_sensor.py index 559c94633cd..4d9c2b732dc 100644 --- a/tests/components/scrape/test_sensor.py +++ b/tests/components/scrape/test_sensor.py @@ -1,27 +1,40 @@ """The tests for the Scrape sensor platform.""" + from __future__ import annotations from datetime import timedelta from unittest.mock import patch +from freezegun.api import FrozenDateTimeFactory import pytest +from homeassistant.components.rest.const import DEFAULT_METHOD +from homeassistant.components.rest.data import DEFAULT_TIMEOUT from homeassistant.components.scrape.const import ( + CONF_ENCODING, CONF_INDEX, CONF_SELECT, + DEFAULT_ENCODING, DEFAULT_SCAN_INTERVAL, + DEFAULT_VERIFY_SSL, ) from homeassistant.components.sensor import ( CONF_STATE_CLASS, + DOMAIN as SENSOR_DOMAIN, SensorDeviceClass, SensorStateClass, ) from homeassistant.const import ( CONF_DEVICE_CLASS, CONF_ICON, + CONF_METHOD, CONF_NAME, + CONF_RESOURCE, + CONF_TIMEOUT, CONF_UNIQUE_ID, CONF_UNIT_OF_MEASUREMENT, + CONF_VALUE_TEMPLATE, + CONF_VERIFY_SSL, STATE_UNAVAILABLE, STATE_UNKNOWN, UnitOfTemperature, @@ -248,7 +261,7 @@ async def test_scrape_sensor_no_data_refresh(hass: HomeAssistant) -> None: mocker.payload = "test_scrape_sensor_no_data" async_fire_time_changed(hass, dt_util.utcnow() + DEFAULT_SCAN_INTERVAL) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get("sensor.ha_version") assert state is not None @@ -528,7 +541,7 @@ async def test_templates_with_yaml(hass: HomeAssistant) -> None: hass, dt_util.utcnow() + timedelta(minutes=10), ) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get("sensor.get_values_with_template") assert state.state == "Current Version: 2021.12.10" @@ -542,7 +555,7 @@ async def test_templates_with_yaml(hass: HomeAssistant) -> None: hass, dt_util.utcnow() + timedelta(minutes=20), ) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get("sensor.get_values_with_template") assert state.state == STATE_UNAVAILABLE @@ -555,9 +568,57 @@ async def test_templates_with_yaml(hass: HomeAssistant) -> None: hass, dt_util.utcnow() + timedelta(minutes=30), ) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get("sensor.get_values_with_template") assert state.state == "Current Version: 2021.12.10" assert state.attributes[CONF_ICON] == "mdi:on" assert state.attributes["entity_picture"] == "/local/picture1.jpg" + + +@pytest.mark.parametrize( + "get_config", + [ + { + CONF_RESOURCE: "https://www.home-assistant.io", + CONF_METHOD: DEFAULT_METHOD, + CONF_VERIFY_SSL: DEFAULT_VERIFY_SSL, + CONF_TIMEOUT: DEFAULT_TIMEOUT, + CONF_ENCODING: DEFAULT_ENCODING, + SENSOR_DOMAIN: [ + { + CONF_SELECT: ".current-version h1", + CONF_NAME: "Current version", + CONF_VALUE_TEMPLATE: "{{ value.split(':')[1] }}", + CONF_INDEX: 0, + CONF_UNIQUE_ID: "3699ef88-69e6-11ed-a1eb-0242ac120002", + CONF_AVAILABILITY: '{{ states("sensor.input1")=="on" }}', + } + ], + } + ], +) +async def test_availability( + hass: HomeAssistant, + loaded_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test availability when setup from config entry.""" + + hass.states.async_set("sensor.input1", "on") + freezer.tick(timedelta(minutes=10)) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + + state = hass.states.get("sensor.current_version") + assert state.state == "2021.12.10" + + hass.states.async_set("sensor.input1", "off") + await hass.async_block_till_done() + + freezer.tick(timedelta(minutes=10)) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + + state = hass.states.get("sensor.current_version") + assert state.state == STATE_UNAVAILABLE diff --git a/tests/components/screenlogic/__init__.py b/tests/components/screenlogic/__init__.py index ee747a6ca74..c9889e6b4b8 100644 --- a/tests/components/screenlogic/__init__.py +++ b/tests/components/screenlogic/__init__.py @@ -1,4 +1,5 @@ """Tests for the Screenlogic integration.""" + from collections.abc import Callable import logging diff --git a/tests/components/screenlogic/conftest.py b/tests/components/screenlogic/conftest.py index 3795df3dddc..7c4d6adf16b 100644 --- a/tests/components/screenlogic/conftest.py +++ b/tests/components/screenlogic/conftest.py @@ -1,4 +1,5 @@ """Setup fixtures for ScreenLogic integration tests.""" + import pytest from homeassistant.components.screenlogic import DOMAIN diff --git a/tests/components/screenlogic/test_config_flow.py b/tests/components/screenlogic/test_config_flow.py index 8e40f5f0e5c..be1617e3105 100644 --- a/tests/components/screenlogic/test_config_flow.py +++ b/tests/components/screenlogic/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Pentair ScreenLogic config flow.""" + from unittest.mock import patch from screenlogicpy import ScreenLogicError @@ -99,12 +100,15 @@ async def test_flow_discover_error(hass: HomeAssistant) -> None: assert result["errors"] == {} assert result["step_id"] == "gateway_entry" - with patch( - "homeassistant.components.screenlogic.async_setup_entry", - return_value=True, - ) as mock_setup_entry, patch( - "homeassistant.components.screenlogic.config_flow.login.async_get_mac_address", - return_value="00-C0-33-01-01-01", + with ( + patch( + "homeassistant.components.screenlogic.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + patch( + "homeassistant.components.screenlogic.config_flow.login.async_get_mac_address", + return_value="00-C0-33-01-01-01", + ), ): result3 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -140,12 +144,15 @@ async def test_dhcp(hass: HomeAssistant) -> None: assert result["type"] == "form" assert result["step_id"] == "gateway_entry" - with patch( - "homeassistant.components.screenlogic.async_setup_entry", - return_value=True, - ) as mock_setup_entry, patch( - "homeassistant.components.screenlogic.config_flow.login.async_get_mac_address", - return_value="00-C0-33-01-01-01", + with ( + patch( + "homeassistant.components.screenlogic.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + patch( + "homeassistant.components.screenlogic.config_flow.login.async_get_mac_address", + return_value="00-C0-33-01-01-01", + ), ): result3 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -195,12 +202,15 @@ async def test_form_manual_entry(hass: HomeAssistant) -> None: assert result2["errors"] == {} assert result2["step_id"] == "gateway_entry" - with patch( - "homeassistant.components.screenlogic.async_setup_entry", - return_value=True, - ) as mock_setup_entry, patch( - "homeassistant.components.screenlogic.config_flow.login.async_get_mac_address", - return_value="00-C0-33-01-01-01", + with ( + patch( + "homeassistant.components.screenlogic.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + patch( + "homeassistant.components.screenlogic.config_flow.login.async_get_mac_address", + return_value="00-C0-33-01-01-01", + ), ): result3 = await hass.config_entries.flow.async_configure( result["flow_id"], diff --git a/tests/components/screenlogic/test_data.py b/tests/components/screenlogic/test_data.py index ead064f7d93..d17db6c5b33 100644 --- a/tests/components/screenlogic/test_data.py +++ b/tests/components/screenlogic/test_data.py @@ -1,4 +1,5 @@ """Tests for ScreenLogic integration data processing.""" + from unittest.mock import DEFAULT, patch from screenlogicpy import ScreenLogicGateway @@ -52,16 +53,19 @@ async def test_async_cleanup_entries( assert unused_entity assert unused_entity.unique_id == TEST_UNUSED_ENTRY["unique_id"] - with patch( - GATEWAY_DISCOVERY_IMPORT_PATH, - return_value={}, - ), patch.multiple( - ScreenLogicGateway, - async_connect=lambda *args, **kwargs: stub_async_connect( - DATA_MIN_ENTITY_CLEANUP, *args, **kwargs + with ( + patch( + GATEWAY_DISCOVERY_IMPORT_PATH, + return_value={}, + ), + patch.multiple( + ScreenLogicGateway, + async_connect=lambda *args, **kwargs: stub_async_connect( + DATA_MIN_ENTITY_CLEANUP, *args, **kwargs + ), + is_connected=True, + _async_connected_request=DEFAULT, ), - is_connected=True, - _async_connected_request=DEFAULT, ): assert await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/screenlogic/test_diagnostics.py b/tests/components/screenlogic/test_diagnostics.py index dcbca954730..0b587bcd0e5 100644 --- a/tests/components/screenlogic/test_diagnostics.py +++ b/tests/components/screenlogic/test_diagnostics.py @@ -1,4 +1,5 @@ """Testing for ScreenLogic diagnostics.""" + from unittest.mock import DEFAULT, patch from screenlogicpy import ScreenLogicGateway @@ -34,17 +35,20 @@ async def test_diagnostics( config_entry_id=mock_config_entry.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, MOCK_ADAPTER_MAC)}, ) - with patch( - GATEWAY_DISCOVERY_IMPORT_PATH, - return_value={}, - ), patch.multiple( - ScreenLogicGateway, - async_connect=lambda *args, **kwargs: stub_async_connect( - DATA_FULL_CHEM, *args, **kwargs + with ( + patch( + GATEWAY_DISCOVERY_IMPORT_PATH, + return_value={}, + ), + patch.multiple( + ScreenLogicGateway, + async_connect=lambda *args, **kwargs: stub_async_connect( + DATA_FULL_CHEM, *args, **kwargs + ), + is_connected=True, + _async_connected_request=DEFAULT, + get_debug=lambda self: {}, ), - is_connected=True, - _async_connected_request=DEFAULT, - get_debug=lambda self: {}, ): assert await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/screenlogic/test_init.py b/tests/components/screenlogic/test_init.py index cf0a7ef3f38..9c296fd8afd 100644 --- a/tests/components/screenlogic/test_init.py +++ b/tests/components/screenlogic/test_init.py @@ -1,4 +1,5 @@ """Tests for ScreenLogic integration init.""" + from dataclasses import dataclass from unittest.mock import DEFAULT, patch @@ -156,14 +157,17 @@ async def test_async_migrate_entries( assert entity.unique_id == old_uid assert entity.entity_id == old_eid - with patch( - GATEWAY_DISCOVERY_IMPORT_PATH, - return_value={}, - ), patch.multiple( - ScreenLogicGateway, - async_connect=MIGRATION_CONNECT, - is_connected=True, - _async_connected_request=DEFAULT, + with ( + patch( + GATEWAY_DISCOVERY_IMPORT_PATH, + return_value={}, + ), + patch.multiple( + ScreenLogicGateway, + async_connect=MIGRATION_CONNECT, + is_connected=True, + _async_connected_request=DEFAULT, + ), ): assert await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() @@ -215,23 +219,27 @@ async def test_entity_migration_data( ) # This patch simulates bad data being added to ENTITY_MIGRATIONS - with patch.dict( - "homeassistant.components.screenlogic.data.ENTITY_MIGRATIONS", - { - "missing_device": { - "new_key": "state", - "old_name": "Missing Migration Device", - "new_name": "Bad ENTITY_MIGRATIONS Entry", + with ( + patch.dict( + "homeassistant.components.screenlogic.data.ENTITY_MIGRATIONS", + { + "missing_device": { + "new_key": "state", + "old_name": "Missing Migration Device", + "new_name": "Bad ENTITY_MIGRATIONS Entry", + }, }, - }, - ), patch( - GATEWAY_DISCOVERY_IMPORT_PATH, - return_value={}, - ), patch.multiple( - ScreenLogicGateway, - async_connect=MIGRATION_CONNECT, - is_connected=True, - _async_connected_request=DEFAULT, + ), + patch( + GATEWAY_DISCOVERY_IMPORT_PATH, + return_value={}, + ), + patch.multiple( + ScreenLogicGateway, + async_connect=MIGRATION_CONNECT, + is_connected=True, + _async_connected_request=DEFAULT, + ), ): assert await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() @@ -263,14 +271,17 @@ async def test_platform_setup( mock_config_entry.add_to_hass(hass) - with patch( - GATEWAY_DISCOVERY_IMPORT_PATH, - return_value={}, - ), patch.multiple( - ScreenLogicGateway, - async_connect=stub_connect, - is_connected=True, - _async_connected_request=DEFAULT, + with ( + patch( + GATEWAY_DISCOVERY_IMPORT_PATH, + return_value={}, + ), + patch.multiple( + ScreenLogicGateway, + async_connect=stub_connect, + is_connected=True, + _async_connected_request=DEFAULT, + ), ): assert await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/script/test_blueprint.py b/tests/components/script/test_blueprint.py index 6c9f17f29b7..bccf1d9aa50 100644 --- a/tests/components/script/test_blueprint.py +++ b/tests/components/script/test_blueprint.py @@ -1,4 +1,5 @@ """Test script blueprints.""" + import asyncio from collections.abc import Iterator import contextlib diff --git a/tests/components/script/test_init.py b/tests/components/script/test_init.py index 2d21dc924dd..ba448230c35 100644 --- a/tests/components/script/test_init.py +++ b/tests/components/script/test_init.py @@ -1,4 +1,5 @@ """The tests for the Script component.""" + import asyncio from datetime import timedelta from typing import Any @@ -195,7 +196,7 @@ async def test_setup_with_invalid_configs( @pytest.mark.parametrize( ("object_id", "broken_config", "problem", "details"), - ( + [ ( "Bad Script", {}, @@ -211,7 +212,7 @@ async def test_setup_with_invalid_configs( "reload, toggle, turn_off, turn_on. Got 'turn_on'" ), ), - ), + ], ) async def test_bad_config_validation_critical( hass: HomeAssistant, @@ -251,7 +252,7 @@ async def test_bad_config_validation_critical( @pytest.mark.parametrize( ("object_id", "broken_config", "problem", "details"), - ( + [ ( "bad_script", {}, @@ -271,7 +272,7 @@ async def test_bad_config_validation_critical( "failed to setup actions", "Unknown entity registry entry abcdabcdabcdabcdabcdabcdabcdabcd.", ), - ), + ], ) async def test_bad_config_validation( hass: HomeAssistant, @@ -423,7 +424,7 @@ async def test_reload_unchanged_does_not_stop(hass: HomeAssistant, calls) -> Non @pytest.mark.parametrize( "script_config", - ( + [ { "test": { "sequence": [{"service": "test.script"}], @@ -457,7 +458,7 @@ async def test_reload_unchanged_does_not_stop(hass: HomeAssistant, calls) -> Non } } }, - ), + ], ) async def test_reload_unchanged_script( hass: HomeAssistant, calls, script_config @@ -683,11 +684,17 @@ async def test_extraction_functions_not_setup(hass: HomeAssistant) -> None: assert script.devices_in_script(hass, "script.test") == [] assert script.scripts_with_entity(hass, "light.in_both") == [] assert script.entities_in_script(hass, "script.test") == [] + assert script.scripts_with_floor(hass, "floor-in-both") == [] + assert script.floors_in_script(hass, "script.test") == [] + assert script.scripts_with_label(hass, "label-in-both") == [] + assert script.labels_in_script(hass, "script.test") == [] async def test_extraction_functions_unknown_script(hass: HomeAssistant) -> None: """Test extraction functions for an unknown script.""" assert await async_setup_component(hass, DOMAIN, {}) + assert script.labels_in_script(hass, "script.unknown") == [] + assert script.floors_in_script(hass, "script.unknown") == [] assert script.areas_in_script(hass, "script.unknown") == [] assert script.blueprint_in_script(hass, "script.unknown") is None assert script.devices_in_script(hass, "script.unknown") == [] @@ -711,6 +718,10 @@ async def test_extraction_functions_unavailable_script(hass: HomeAssistant) -> N assert script.devices_in_script(hass, entity_id) == [] assert script.scripts_with_entity(hass, "light.in_both") == [] assert script.entities_in_script(hass, entity_id) == [] + assert script.scripts_with_floor(hass, "floor-in-both") == [] + assert script.floors_in_script(hass, entity_id) == [] + assert script.scripts_with_label(hass, "label-in-both") == [] + assert script.labels_in_script(hass, entity_id) == [] async def test_extraction_functions( @@ -755,6 +766,14 @@ async def test_extraction_functions( "service": "test.test", "target": {"area_id": "area-in-both"}, }, + { + "service": "test.test", + "target": {"floor_id": "floor-in-both"}, + }, + { + "service": "test.test", + "target": {"label_id": "label-in-both"}, + }, ] }, "test2": { @@ -803,6 +822,22 @@ async def test_extraction_functions( "service": "test.test", "target": {"area_id": "area-in-last"}, }, + { + "service": "test.test", + "target": {"floor_id": "floor-in-both"}, + }, + { + "service": "test.test", + "target": {"floor_id": "floor-in-last"}, + }, + { + "service": "test.test", + "target": {"label_id": "label-in-both"}, + }, + { + "service": "test.test", + "target": {"label_id": "label-in-last"}, + }, ], }, } @@ -834,6 +869,22 @@ async def test_extraction_functions( "area-in-both", "area-in-last", } + assert set(script.scripts_with_floor(hass, "floor-in-both")) == { + "script.test1", + "script.test3", + } + assert set(script.floors_in_script(hass, "script.test3")) == { + "floor-in-both", + "floor-in-last", + } + assert set(script.scripts_with_label(hass, "label-in-both")) == { + "script.test1", + "script.test3", + } + assert set(script.labels_in_script(hass, "script.test3")) == { + "label-in-both", + "label-in-last", + } assert script.blueprint_in_script(hass, "script.test3") is None @@ -898,6 +949,7 @@ async def test_logbook_humanify_script_started_event(hass: HomeAssistant) -> Non hass.config.components.add("recorder") await async_setup_component(hass, DOMAIN, {}) await async_setup_component(hass, "logbook", {}) + await hass.async_block_till_done() event1, event2 = mock_humanify( hass, @@ -1112,7 +1164,7 @@ async def test_script_variables( async def test_script_this_var_always( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: - """Test script always has reference to this, even with no variabls are configured.""" + """Test script always has reference to this, even with no variables are configured.""" assert await async_setup_component( hass, @@ -1180,12 +1232,12 @@ async def test_script_restore_last_triggered(hass: HomeAssistant) -> None: @pytest.mark.parametrize( ("script_mode", "warning_msg"), - ( + [ (SCRIPT_MODE_PARALLEL, "Maximum number of runs exceeded"), (SCRIPT_MODE_QUEUED, "Disallowed recursion detected"), (SCRIPT_MODE_RESTART, "Disallowed recursion detected"), (SCRIPT_MODE_SINGLE, "Already running"), - ), + ], ) async def test_recursive_script( hass: HomeAssistant, script_mode, warning_msg, caplog: pytest.LogCaptureFixture @@ -1230,12 +1282,12 @@ async def test_recursive_script( @pytest.mark.parametrize( ("script_mode", "warning_msg"), - ( + [ (SCRIPT_MODE_PARALLEL, "Maximum number of runs exceeded"), (SCRIPT_MODE_QUEUED, "Disallowed recursion detected"), (SCRIPT_MODE_RESTART, "Disallowed recursion detected"), (SCRIPT_MODE_SINGLE, "Already running"), - ), + ], ) async def test_recursive_script_indirect( hass: HomeAssistant, script_mode, warning_msg, caplog: pytest.LogCaptureFixture @@ -1537,7 +1589,7 @@ async def test_blueprint_automation(hass: HomeAssistant, calls) -> None: @pytest.mark.parametrize( ("blueprint_inputs", "problem", "details"), - ( + [ ( # No input {}, @@ -1560,7 +1612,7 @@ async def test_blueprint_automation(hass: HomeAssistant, calls) -> None: "Blueprint 'Call service' generated invalid script", "value should be a string for dictionary value @ data['sequence'][0]['service']", ), - ), + ], ) async def test_blueprint_script_bad_config( hass: HomeAssistant, @@ -1619,7 +1671,7 @@ async def test_blueprint_script_fails_substitution( ) -@pytest.mark.parametrize("response", ({"value": 5}, '{"value": 5}')) +@pytest.mark.parametrize("response", [{"value": 5}, '{"value": 5}']) async def test_responses(hass: HomeAssistant, response: Any) -> None: """Test we can get responses.""" mock_restore_cache(hass, ()) diff --git a/tests/components/script/test_recorder.py b/tests/components/script/test_recorder.py index 4e98ea9e670..465d287318d 100644 --- a/tests/components/script/test_recorder.py +++ b/tests/components/script/test_recorder.py @@ -1,4 +1,5 @@ """The tests for script recorder.""" + from __future__ import annotations import pytest diff --git a/tests/components/search/test_init.py b/tests/components/search/test_init.py index ebf70a6239c..a817fbfc39e 100644 --- a/tests/components/search/test_init.py +++ b/tests/components/search/test_init.py @@ -1,13 +1,18 @@ """Tests for Search integration.""" -import pytest -from homeassistant.components import search +import pytest +from pytest_unordered import unordered + +from homeassistant.components.search import ItemType, Searcher from homeassistant.core import HomeAssistant from homeassistant.helpers import ( area_registry as ar, device_registry as dr, entity_registry as er, + floor_registry as fr, + label_registry as lr, ) +from homeassistant.helpers.entity import EntityInfo from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry @@ -19,38 +24,79 @@ def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None: """Stub copying the blueprints to the config folder.""" -MOCK_ENTITY_SOURCES = { - "light.platform_config_source": { - "domain": "wled", - }, - "light.config_entry_source": { - "config_entry": "config_entry_id", - "domain": "wled", - }, -} - - async def test_search( hass: HomeAssistant, area_registry: ar.AreaRegistry, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, + floor_registry: fr.FloorRegistry, + label_registry: lr.LabelRegistry, + hass_ws_client: WebSocketGenerator, ) -> None: - """Test that search works.""" - living_room_area = area_registry.async_create("Living Room") + """Test search.""" + assert await async_setup_component(hass, "search", {}) - # Light strip with 2 lights. + # Labels + label_energy = label_registry.async_create("Energy") + label_christmas = label_registry.async_create("Christmas") + label_other = label_registry.async_create("Other") + + # Floors + first_floor = floor_registry.async_create("First Floor") + second_floor = floor_registry.async_create("Second Floor") + + # Areas + bedroom_area = area_registry.async_create( + "Bedroom", floor_id=second_floor.floor_id, labels={label_other.label_id} + ) + kitchen_area = area_registry.async_create("Kitchen", floor_id=first_floor.floor_id) + living_room_area = area_registry.async_create( + "Living Room", floor_id=first_floor.floor_id + ) + + # Config entries + hue_config_entry = MockConfigEntry(domain="hue") + hue_config_entry.add_to_hass(hass) wled_config_entry = MockConfigEntry(domain="wled") wled_config_entry.add_to_hass(hass) + # Devices + hue_device = device_registry.async_get_or_create( + config_entry_id=hue_config_entry.entry_id, + name="Light Strip", + identifiers={("hue", "hue-1")}, + ) + device_registry.async_update_device(hue_device.id, area_id=kitchen_area.id) + wled_device = device_registry.async_get_or_create( config_entry_id=wled_config_entry.entry_id, name="Light Strip", identifiers=({"wled", "wled-1"}), ) + device_registry.async_update_device( + wled_device.id, area_id=living_room_area.id, labels={label_christmas.label_id} + ) - device_registry.async_update_device(wled_device.id, area_id=living_room_area.id) - + # Entities + hue_segment_1_entity = entity_registry.async_get_or_create( + "light", + "hue", + "hue-1-seg-1", + suggested_object_id="hue segment 1", + config_entry=hue_config_entry, + device_id=hue_device.id, + ) + entity_registry.async_update_entity( + hue_segment_1_entity.entity_id, labels={label_energy.label_id} + ) + hue_segment_2_entity = entity_registry.async_get_or_create( + "light", + "hue", + "hue-1-seg-2", + suggested_object_id="hue segment 2", + config_entry=hue_config_entry, + device_id=hue_device.id, + ) wled_segment_1_entity = entity_registry.async_get_or_create( "light", "wled", @@ -67,48 +113,59 @@ async def test_search( config_entry=wled_config_entry, device_id=wled_device.id, ) + entity_registry.async_update_entity( + wled_segment_2_entity.entity_id, area_id=bedroom_area.id + ) + scene_wled_hue_entity = entity_registry.async_get_or_create( + "scene", + "homeassistant", + "wled_hue", + suggested_object_id="scene_wled_hue", + ) + entity_registry.async_update_entity( + scene_wled_hue_entity.entity_id, + area_id=bedroom_area.id, + labels={label_other.label_id}, + ) + + # Persons can technically be assigned to areas + person_paulus_entity = entity_registry.async_get_or_create( + "person", + "person", + "abcd", + suggested_object_id="paulus", + ) + entity_registry.async_update_entity( + person_paulus_entity.entity_id, + area_id=bedroom_area.id, + labels={label_other.label_id}, + ) + + script_scene_entity = entity_registry.async_get_or_create( + "script", + "script", + "scene", + suggested_object_id="scene", + ) + entity_registry.async_update_entity( + script_scene_entity.entity_id, + area_id=bedroom_area.id, + labels={label_other.label_id}, + ) + + # Entity sources entity_sources = { - "light.wled_platform_config_source": { - "domain": "wled", - }, - "light.wled_config_entry_source": { - "config_entry": wled_config_entry.entry_id, - "domain": "wled", - }, + "light.wled_platform_config_source": EntityInfo( + domain="wled", + ), + "light.wled_config_entry_source": EntityInfo( + config_entry=wled_config_entry.entry_id, + domain="wled", + ), } - # Non related info. - kitchen_area = area_registry.async_create("Kitchen") - - hue_config_entry = MockConfigEntry(domain="hue") - hue_config_entry.add_to_hass(hass) - - hue_device = device_registry.async_get_or_create( - config_entry_id=hue_config_entry.entry_id, - name="Light Strip", - identifiers=({"hue", "hue-1"}), - ) - - device_registry.async_update_device(hue_device.id, area_id=kitchen_area.id) - - hue_segment_1_entity = entity_registry.async_get_or_create( - "light", - "hue", - "hue-1-seg-1", - suggested_object_id="hue segment 1", - config_entry=hue_config_entry, - device_id=hue_device.id, - ) - hue_segment_2_entity = entity_registry.async_get_or_create( - "light", - "hue", - "hue-1-seg-2", - suggested_object_id="hue segment 2", - config_entry=hue_config_entry, - device_id=hue_device.id, - ) - + # Groups await async_setup_component( hass, "group", @@ -141,6 +198,22 @@ async def test_search( }, ) + # Persons + assert await async_setup_component( + hass, + "person", + { + "person": [ + { + "id": "abcd", + "name": "Paulus", + "device_trackers": ["device_tracker.paulus_iphone"], + } + ] + }, + ) + + # Scenes await async_setup_component( hass, "scene", @@ -155,6 +228,7 @@ async def test_search( "entities": {hue_segment_1_entity.entity_id: "on"}, }, { + "id": "wled_hue", "name": "scene_wled_hue", "entities": { wled_segment_1_entity.entity_id: "on", @@ -167,11 +241,144 @@ async def test_search( }, ) - await async_setup_component( + # Automations + assert await async_setup_component( + hass, + "automation", + { + "automation": [ + { + "id": "unique_id", + "alias": "blueprint_automation_1", + "trigger": {"platform": "template", "value_template": "true"}, + "use_blueprint": { + "path": "test_event_service.yaml", + "input": { + "trigger_event": "blueprint_event_1", + "service_to_call": "test.automation_1", + "a_number": 5, + }, + }, + }, + { + "alias": "blueprint_automation_2", + "trigger": {"platform": "template", "value_template": "true"}, + "use_blueprint": { + "path": "test_event_service.yaml", + "input": { + "trigger_event": "blueprint_event_2", + "service_to_call": "test.automation_2", + "a_number": 5, + }, + }, + }, + { + "alias": "wled_entity", + "trigger": {"platform": "template", "value_template": "true"}, + "action": [ + { + "service": "test.script", + "data": {"entity_id": wled_segment_1_entity.entity_id}, + }, + ], + }, + { + "alias": "wled_device", + "trigger": {"platform": "template", "value_template": "true"}, + "action": [ + { + "domain": "light", + "device_id": wled_device.id, + "entity_id": wled_segment_1_entity.entity_id, + "type": "turn_on", + }, + ], + }, + { + "alias": "floor", + "trigger": {"platform": "template", "value_template": "true"}, + "action": [ + { + "service": "test.script", + "target": {"floor_id": first_floor.floor_id}, + }, + ], + }, + { + "alias": "area", + "trigger": {"platform": "template", "value_template": "true"}, + "action": [ + { + "service": "test.script", + "target": {"area_id": kitchen_area.id}, + }, + ], + }, + { + "alias": "group", + "trigger": {"platform": "template", "value_template": "true"}, + "action": [ + { + "service": "homeassistant.turn_on", + "target": {"entity_id": "group.wled_hue"}, + }, + ], + }, + { + "alias": "scene", + "trigger": {"platform": "template", "value_template": "true"}, + "action": [ + { + "scene": scene_wled_hue_entity.entity_id, + }, + ], + }, + { + "alias": "script", + "trigger": {"platform": "template", "value_template": "true"}, + "action": [ + { + "service": "script.turn_on", + "data": {"entity_id": script_scene_entity.entity_id}, + }, + ], + }, + { + "alias": "label", + "trigger": {"platform": "template", "value_template": "true"}, + "action": [ + { + "service": "script.turn_on", + "target": {"label_id": label_christmas.label_id}, + }, + ], + }, + ] + }, + ) + + # Scripts + assert await async_setup_component( hass, "script", { "script": { + "blueprint_script_1": { + "use_blueprint": { + "path": "test_service.yaml", + "input": { + "service_to_call": "test.automation", + }, + } + }, + "blueprint_script_2": { + "use_blueprint": { + "path": "test_service.yaml", + "input": { + "service_to_call": "test.automation", + }, + } + }, "wled": { "sequence": [ { @@ -204,375 +411,550 @@ async def test_search( }, ] }, + "device": { + "sequence": [ + { + "service": "test.script", + "target": {"device_id": hue_device.id}, + }, + ], + }, + "floor": { + "sequence": [ + { + "service": "test.script", + "target": {"floor_id": first_floor.floor_id}, + }, + ], + }, + "area": { + "sequence": [ + { + "service": "test.script", + "target": {"area_id": kitchen_area.id}, + }, + ], + }, + "group": { + "sequence": [ + { + "service": "test.script", + "target": {"entity_id": "group.wled_hue"}, + }, + ], + }, + "scene": { + "sequence": [ + { + "scene": scene_wled_hue_entity.entity_id, + }, + ], + }, + "label": { + "sequence": [ + { + "service": "test.script", + "target": {"label_id": label_other.label_id}, + }, + ], + }, + "nested": { + "sequence": [ + { + "service": "script.turn_on", + "data": {"entity_id": script_scene_entity.entity_id}, + }, + ], + }, } }, ) - assert await async_setup_component( - hass, - "automation", - { - "automation": [ - { - "alias": "wled_entity", - "trigger": {"platform": "template", "value_template": "true"}, - "action": [ - { - "service": "test.script", - "data": {"entity_id": wled_segment_1_entity.entity_id}, - }, - ], - }, - { - "alias": "wled_device", - "trigger": {"platform": "template", "value_template": "true"}, - "action": [ - { - "domain": "light", - "device_id": wled_device.id, - "entity_id": wled_segment_1_entity.entity_id, - "type": "turn_on", - }, - ], - }, - ] + def search(item_type: ItemType, item_id: str) -> dict[str, set[str]]: + """Search.""" + searcher = Searcher(hass, entity_sources) + return searcher.async_search(item_type, item_id) + + # + # Tests + # + assert not search(ItemType.AREA, "unknown") + assert search(ItemType.AREA, bedroom_area.id) == { + ItemType.AUTOMATION: {"automation.scene", "automation.script"}, + ItemType.CONFIG_ENTRY: {wled_config_entry.entry_id}, + ItemType.ENTITY: { + wled_segment_2_entity.entity_id, + scene_wled_hue_entity.entity_id, + script_scene_entity.entity_id, + person_paulus_entity.entity_id, }, - ) - - # Ensure automations set up correctly. - assert hass.states.get("automation.wled_entity") is not None - assert hass.states.get("automation.wled_device") is not None - - # Explore the graph from every node and make sure we find the same results - expected = { - "config_entry": {wled_config_entry.entry_id}, - "area": {living_room_area.id}, - "device": {wled_device.id}, - "entity": {wled_segment_1_entity.entity_id, wled_segment_2_entity.entity_id}, - "scene": {"scene.scene_wled_seg_1", "scene.scene_wled_hue"}, - "group": {"group.wled", "group.wled_hue"}, - "script": {"script.wled"}, - "automation": {"automation.wled_entity", "automation.wled_device"}, + ItemType.FLOOR: {second_floor.floor_id}, + ItemType.GROUP: {"group.wled", "group.wled_hue"}, + ItemType.LABEL: {label_other.label_id}, + ItemType.PERSON: {person_paulus_entity.entity_id}, + ItemType.SCENE: {scene_wled_hue_entity.entity_id}, + ItemType.SCRIPT: {script_scene_entity.entity_id, "script.nested"}, + } + assert search(ItemType.AREA, living_room_area.id) == { + ItemType.AUTOMATION: {"automation.wled_device", "automation.wled_entity"}, + ItemType.CONFIG_ENTRY: {wled_config_entry.entry_id}, + ItemType.DEVICE: {wled_device.id}, + ItemType.ENTITY: {wled_segment_1_entity.entity_id}, + ItemType.FLOOR: {first_floor.floor_id}, + ItemType.GROUP: {"group.wled", "group.wled_hue"}, + ItemType.SCENE: {"scene.scene_wled_seg_1", scene_wled_hue_entity.entity_id}, + ItemType.SCRIPT: {"script.wled"}, + } + assert search(ItemType.AREA, kitchen_area.id) == { + ItemType.AUTOMATION: {"automation.area"}, + ItemType.CONFIG_ENTRY: {hue_config_entry.entry_id}, + ItemType.DEVICE: {hue_device.id}, + ItemType.ENTITY: { + hue_segment_1_entity.entity_id, + hue_segment_2_entity.entity_id, + }, + ItemType.FLOOR: {first_floor.floor_id}, + ItemType.GROUP: {"group.hue", "group.wled_hue"}, + ItemType.SCENE: {"scene.scene_hue_seg_1", scene_wled_hue_entity.entity_id}, + ItemType.SCRIPT: {"script.area", "script.device", "script.hue"}, } - for search_type, search_id in ( - ("config_entry", wled_config_entry.entry_id), - ("area", living_room_area.id), - ("device", wled_device.id), - ("entity", wled_segment_1_entity.entity_id), - ("entity", wled_segment_2_entity.entity_id), - ("scene", "scene.scene_wled_seg_1"), - ("group", "group.wled"), - ("script", "script.wled"), - ("automation", "automation.wled_entity"), - ("automation", "automation.wled_device"), - ): - searcher = search.Searcher( - hass, device_registry, entity_registry, entity_sources - ) - results = searcher.async_search(search_type, search_id) - # Add the item we searched for, it's omitted from results - results.setdefault(search_type, set()).add(search_id) - - assert ( - results == expected - ), f"Results for {search_type}/{search_id} do not match up" - - # For combined things, needs to return everything. - expected_combined = { - "config_entry": {wled_config_entry.entry_id, hue_config_entry.entry_id}, - "area": {living_room_area.id, kitchen_area.id}, - "device": {wled_device.id, hue_device.id}, - "entity": { + assert not search(ItemType.AUTOMATION, "automation.unknown") + assert search(ItemType.AUTOMATION, "automation.blueprint_automation_1") == { + ItemType.AUTOMATION_BLUEPRINT: {"test_event_service.yaml"}, + ItemType.ENTITY: {"light.kitchen"}, + } + assert search(ItemType.AUTOMATION, "automation.blueprint_automation_2") == { + ItemType.AUTOMATION_BLUEPRINT: {"test_event_service.yaml"}, + ItemType.ENTITY: {"light.kitchen"}, + } + assert search(ItemType.AUTOMATION, "automation.wled_entity") == { + ItemType.AREA: {living_room_area.id}, + ItemType.CONFIG_ENTRY: {wled_config_entry.entry_id}, + ItemType.DEVICE: {wled_device.id}, + ItemType.ENTITY: {wled_segment_1_entity.entity_id}, + ItemType.FLOOR: {first_floor.floor_id}, + } + assert search(ItemType.AUTOMATION, "automation.wled_device") == { + ItemType.AREA: {living_room_area.id}, + ItemType.CONFIG_ENTRY: {wled_config_entry.entry_id}, + ItemType.DEVICE: {wled_device.id}, + ItemType.FLOOR: {first_floor.floor_id}, + } + assert search(ItemType.AUTOMATION, "automation.floor") == { + ItemType.FLOOR: {first_floor.floor_id}, + } + assert search(ItemType.AUTOMATION, "automation.area") == { + ItemType.AREA: {kitchen_area.id}, + ItemType.FLOOR: {first_floor.floor_id}, + } + assert search(ItemType.AUTOMATION, "automation.group") == { + ItemType.AREA: {bedroom_area.id, living_room_area.id, kitchen_area.id}, + ItemType.CONFIG_ENTRY: {wled_config_entry.entry_id, hue_config_entry.entry_id}, + ItemType.DEVICE: {wled_device.id, hue_device.id}, + ItemType.ENTITY: { + "group.wled_hue", wled_segment_1_entity.entity_id, wled_segment_2_entity.entity_id, hue_segment_1_entity.entity_id, hue_segment_2_entity.entity_id, }, - "scene": { - "scene.scene_wled_seg_1", - "scene.scene_hue_seg_1", - "scene.scene_wled_hue", + ItemType.FLOOR: {first_floor.floor_id, second_floor.floor_id}, + ItemType.GROUP: {"group.wled_hue"}, + } + assert search(ItemType.AUTOMATION, "automation.scene") == { + ItemType.AREA: {bedroom_area.id, kitchen_area.id, living_room_area.id}, + ItemType.CONFIG_ENTRY: {wled_config_entry.entry_id, hue_config_entry.entry_id}, + ItemType.DEVICE: {wled_device.id, hue_device.id}, + ItemType.ENTITY: { + wled_segment_1_entity.entity_id, + wled_segment_2_entity.entity_id, + hue_segment_1_entity.entity_id, + hue_segment_2_entity.entity_id, + scene_wled_hue_entity.entity_id, }, - "group": {"group.wled", "group.hue", "group.wled_hue"}, - "script": {"script.wled", "script.hue"}, - "automation": {"automation.wled_entity", "automation.wled_device"}, + ItemType.FLOOR: {first_floor.floor_id, second_floor.floor_id}, + ItemType.SCENE: {scene_wled_hue_entity.entity_id}, } - for search_type, search_id in ( - ("scene", "scene.scene_wled_hue"), - ("group", "group.wled_hue"), - ): - searcher = search.Searcher( - hass, device_registry, entity_registry, entity_sources - ) - results = searcher.async_search(search_type, search_id) - # Add the item we searched for, it's omitted from results - results.setdefault(search_type, set()).add(search_id) - assert ( - results == expected_combined - ), f"Results for {search_type}/{search_id} do not match up" - - for search_type, search_id in ( - ("entity", "automation.non_existing"), - ("entity", "scene.non_existing"), - ("entity", "group.non_existing"), - ("entity", "script.non_existing"), - ("entity", "light.non_existing"), - ("area", "non_existing"), - ("config_entry", "non_existing"), - ("device", "non_existing"), - ("group", "group.non_existing"), - ("scene", "scene.non_existing"), - ("script", "script.non_existing"), - ("automation", "automation.non_existing"), - ): - searcher = search.Searcher( - hass, device_registry, entity_registry, entity_sources - ) - assert searcher.async_search(search_type, search_id) == {} - - # Test search of templated script. We can't find referenced areas, devices or - # entities within templated services, but searching them should not raise or - # otherwise fail. - assert hass.states.get("script.script_with_templated_services") - for search_type, search_id in ( - ("area", "script.script_with_templated_services"), - ("device", "script.script_with_templated_services"), - ("entity", "script.script_with_templated_services"), - ): - searcher = search.Searcher( - hass, device_registry, entity_registry, entity_sources - ) - assert searcher.async_search(search_type, search_id) == {} - - searcher = search.Searcher(hass, device_registry, entity_registry, entity_sources) - assert searcher.async_search("entity", "light.wled_config_entry_source") == { - "config_entry": {wled_config_entry.entry_id}, - } - - -async def test_area_lookup( - hass: HomeAssistant, - area_registry: ar.AreaRegistry, - device_registry: dr.DeviceRegistry, - entity_registry: er.EntityRegistry, -) -> None: - """Test area based lookup.""" - living_room_area = area_registry.async_create("Living Room") - - await async_setup_component( - hass, - "script", - { - "script": { - "wled": { - "sequence": [ - { - "service": "light.turn_on", - "target": {"area_id": living_room_area.id}, - }, - ] - }, - } + assert search(ItemType.AUTOMATION, "automation.script") == { + ItemType.AREA: {bedroom_area.id, kitchen_area.id, living_room_area.id}, + ItemType.CONFIG_ENTRY: {wled_config_entry.entry_id, hue_config_entry.entry_id}, + ItemType.DEVICE: {wled_device.id, hue_device.id}, + ItemType.ENTITY: { + wled_segment_1_entity.entity_id, + wled_segment_2_entity.entity_id, + hue_segment_1_entity.entity_id, + hue_segment_2_entity.entity_id, + scene_wled_hue_entity.entity_id, + script_scene_entity.entity_id, }, - ) - - assert await async_setup_component( - hass, - "automation", - { - "automation": [ - { - "alias": "area_turn_on", - "trigger": {"platform": "template", "value_template": "true"}, - "action": [ - { - "service": "light.turn_on", - "data": { - "area_id": living_room_area.id, - }, - }, - ], - }, - ] - }, - ) - - searcher = search.Searcher( - hass, device_registry, entity_registry, MOCK_ENTITY_SOURCES - ) - assert searcher.async_search("area", living_room_area.id) == { - "script": {"script.wled"}, - "automation": {"automation.area_turn_on"}, + ItemType.FLOOR: {first_floor.floor_id, second_floor.floor_id}, + ItemType.SCENE: {scene_wled_hue_entity.entity_id}, + ItemType.SCRIPT: {script_scene_entity.entity_id}, } - searcher = search.Searcher( - hass, device_registry, entity_registry, MOCK_ENTITY_SOURCES - ) - assert searcher.async_search("automation", "automation.area_turn_on") == { - "area": {living_room_area.id}, - } - - -async def test_person_lookup(hass: HomeAssistant) -> None: - """Test searching persons.""" - assert await async_setup_component( - hass, - "person", - { - "person": [ - { - "id": "abcd", - "name": "Paulus", - "device_trackers": ["device_tracker.paulus_iphone"], - } - ] - }, - ) - - device_reg = dr.async_get(hass) - entity_reg = er.async_get(hass) - - searcher = search.Searcher(hass, device_reg, entity_reg, MOCK_ENTITY_SOURCES) - assert searcher.async_search("entity", "device_tracker.paulus_iphone") == { - "person": {"person.paulus"}, - } - - searcher = search.Searcher(hass, device_reg, entity_reg, MOCK_ENTITY_SOURCES) - assert searcher.async_search("entity", "person.paulus") == { - "entity": {"device_tracker.paulus_iphone"}, - } - - -async def test_automation_blueprint(hass): - """Test searching for automation blueprints.""" - - assert await async_setup_component( - hass, - "automation", - { - "automation": [ - { - "alias": "blueprint_automation_1", - "trigger": {"platform": "template", "value_template": "true"}, - "use_blueprint": { - "path": "test_event_service.yaml", - "input": { - "trigger_event": "blueprint_event_1", - "service_to_call": "test.automation_1", - "a_number": 5, - }, - }, - }, - { - "alias": "blueprint_automation_2", - "trigger": {"platform": "template", "value_template": "true"}, - "use_blueprint": { - "path": "test_event_service.yaml", - "input": { - "trigger_event": "blueprint_event_2", - "service_to_call": "test.automation_2", - "a_number": 5, - }, - }, - }, - ] - }, - ) - - # Ensure automations set up correctly. - assert hass.states.get("automation.blueprint_automation_1") is not None - assert hass.states.get("automation.blueprint_automation_1") is not None - - device_reg = dr.async_get(hass) - entity_reg = er.async_get(hass) - - searcher = search.Searcher(hass, device_reg, entity_reg, MOCK_ENTITY_SOURCES) - assert searcher.async_search("automation", "automation.blueprint_automation_1") == { - "automation": {"automation.blueprint_automation_2"}, - "automation_blueprint": {"test_event_service.yaml"}, - "entity": {"light.kitchen"}, - } - - searcher = search.Searcher(hass, device_reg, entity_reg, MOCK_ENTITY_SOURCES) - assert searcher.async_search("automation_blueprint", "test_event_service.yaml") == { - "automation": { + assert not search(ItemType.AUTOMATION_BLUEPRINT, "unknown.yaml") + assert search(ItemType.AUTOMATION_BLUEPRINT, "test_event_service.yaml") == { + ItemType.AUTOMATION: { "automation.blueprint_automation_1", "automation.blueprint_automation_2", + } + } + + assert not search(ItemType.CONFIG_ENTRY, "unknown") + assert search(ItemType.CONFIG_ENTRY, hue_config_entry.entry_id) == { + ItemType.AREA: {kitchen_area.id}, + ItemType.DEVICE: {hue_device.id}, + ItemType.ENTITY: { + hue_segment_1_entity.entity_id, + hue_segment_2_entity.entity_id, + }, + ItemType.FLOOR: {first_floor.floor_id}, + ItemType.GROUP: {"group.hue", "group.wled_hue"}, + ItemType.SCENE: {"scene.scene_hue_seg_1", scene_wled_hue_entity.entity_id}, + ItemType.SCRIPT: {"script.device", "script.hue"}, + } + assert search(ItemType.CONFIG_ENTRY, wled_config_entry.entry_id) == { + ItemType.AREA: {bedroom_area.id, living_room_area.id}, + ItemType.AUTOMATION: {"automation.wled_entity", "automation.wled_device"}, + ItemType.DEVICE: {wled_device.id}, + ItemType.ENTITY: { + wled_segment_1_entity.entity_id, + wled_segment_2_entity.entity_id, + }, + ItemType.FLOOR: {first_floor.floor_id, second_floor.floor_id}, + ItemType.GROUP: {"group.wled", "group.wled_hue"}, + ItemType.SCENE: {"scene.scene_wled_seg_1", scene_wled_hue_entity.entity_id}, + ItemType.SCRIPT: {"script.wled"}, + } + + assert not search(ItemType.DEVICE, "unknown") + assert search(ItemType.DEVICE, wled_device.id) == { + ItemType.AREA: {bedroom_area.id, living_room_area.id}, + ItemType.AUTOMATION: {"automation.wled_entity", "automation.wled_device"}, + ItemType.CONFIG_ENTRY: {wled_config_entry.entry_id}, + ItemType.ENTITY: { + wled_segment_1_entity.entity_id, + wled_segment_2_entity.entity_id, + }, + ItemType.FLOOR: {first_floor.floor_id, second_floor.floor_id}, + ItemType.GROUP: {"group.wled", "group.wled_hue"}, + ItemType.LABEL: {label_christmas.label_id}, + ItemType.SCENE: {"scene.scene_wled_seg_1", scene_wled_hue_entity.entity_id}, + ItemType.SCRIPT: {"script.wled"}, + } + assert search(ItemType.DEVICE, hue_device.id) == { + ItemType.AREA: {kitchen_area.id}, + ItemType.CONFIG_ENTRY: {hue_config_entry.entry_id}, + ItemType.ENTITY: { + hue_segment_1_entity.entity_id, + hue_segment_2_entity.entity_id, + }, + ItemType.FLOOR: {first_floor.floor_id}, + ItemType.GROUP: {"group.hue", "group.wled_hue"}, + ItemType.SCENE: {"scene.scene_hue_seg_1", scene_wled_hue_entity.entity_id}, + ItemType.SCRIPT: {"script.device", "script.hue"}, + } + + assert not search(ItemType.ENTITY, "sensor.unknown") + assert search(ItemType.ENTITY, wled_segment_1_entity.entity_id) == { + ItemType.AREA: {living_room_area.id}, + ItemType.AUTOMATION: {"automation.wled_entity"}, + ItemType.CONFIG_ENTRY: {wled_config_entry.entry_id}, + ItemType.DEVICE: {wled_device.id}, + ItemType.FLOOR: {first_floor.floor_id}, + ItemType.GROUP: {"group.wled", "group.wled_hue"}, + ItemType.SCENE: {"scene.scene_wled_seg_1", scene_wled_hue_entity.entity_id}, + ItemType.SCRIPT: {"script.wled"}, + } + assert search(ItemType.ENTITY, wled_segment_2_entity.entity_id) == { + ItemType.AREA: {bedroom_area.id}, + ItemType.CONFIG_ENTRY: {wled_config_entry.entry_id}, + ItemType.DEVICE: {wled_device.id}, + ItemType.FLOOR: {second_floor.floor_id}, + ItemType.GROUP: {"group.wled", "group.wled_hue"}, + ItemType.SCENE: {scene_wled_hue_entity.entity_id}, + } + assert search(ItemType.ENTITY, hue_segment_1_entity.entity_id) == { + ItemType.AREA: {kitchen_area.id}, + ItemType.CONFIG_ENTRY: {hue_config_entry.entry_id}, + ItemType.DEVICE: {hue_device.id}, + ItemType.FLOOR: {first_floor.floor_id}, + ItemType.GROUP: {"group.hue", "group.wled_hue"}, + ItemType.LABEL: {label_energy.label_id}, + ItemType.SCENE: {"scene.scene_hue_seg_1", scene_wled_hue_entity.entity_id}, + ItemType.SCRIPT: {"script.hue"}, + } + assert search(ItemType.ENTITY, hue_segment_2_entity.entity_id) == { + ItemType.AREA: {kitchen_area.id}, + ItemType.CONFIG_ENTRY: {hue_config_entry.entry_id}, + ItemType.DEVICE: {hue_device.id}, + ItemType.FLOOR: {first_floor.floor_id}, + ItemType.GROUP: {"group.hue", "group.wled_hue"}, + ItemType.SCENE: {scene_wled_hue_entity.entity_id}, + } + assert not search(ItemType.ENTITY, "automation.wled") + assert search(ItemType.ENTITY, script_scene_entity.entity_id) == { + ItemType.AREA: {bedroom_area.id}, + ItemType.AUTOMATION: {"automation.script"}, + ItemType.FLOOR: {second_floor.floor_id}, + ItemType.LABEL: {label_other.label_id}, + ItemType.SCRIPT: {"script.nested"}, + } + assert search(ItemType.ENTITY, "group.wled_hue") == { + ItemType.AUTOMATION: {"automation.group"}, + ItemType.SCRIPT: {"script.group"}, + } + assert search(ItemType.ENTITY, person_paulus_entity.entity_id) == { + ItemType.AREA: {bedroom_area.id}, + ItemType.FLOOR: {second_floor.floor_id}, + ItemType.LABEL: {label_other.label_id}, + } + assert search(ItemType.ENTITY, scene_wled_hue_entity.entity_id) == { + ItemType.AREA: {bedroom_area.id}, + ItemType.AUTOMATION: {"automation.scene"}, + ItemType.FLOOR: {second_floor.floor_id}, + ItemType.LABEL: {label_other.label_id}, + ItemType.SCRIPT: {script_scene_entity.entity_id}, + } + assert search(ItemType.ENTITY, "device_tracker.paulus_iphone") == { + ItemType.PERSON: {person_paulus_entity.entity_id}, + } + assert search(ItemType.ENTITY, "light.wled_config_entry_source") == { + ItemType.CONFIG_ENTRY: {wled_config_entry.entry_id}, + } + + assert not search(ItemType.FLOOR, "unknown") + assert search(ItemType.FLOOR, first_floor.floor_id) == { + ItemType.AREA: {kitchen_area.id, living_room_area.id}, + ItemType.AUTOMATION: { + "automation.area", + "automation.floor", + "automation.wled_device", + "automation.wled_entity", + }, + ItemType.CONFIG_ENTRY: {hue_config_entry.entry_id, wled_config_entry.entry_id}, + ItemType.DEVICE: {hue_device.id, wled_device.id}, + ItemType.ENTITY: { + wled_segment_1_entity.entity_id, + hue_segment_1_entity.entity_id, + hue_segment_2_entity.entity_id, + }, + ItemType.GROUP: {"group.hue", "group.wled", "group.wled_hue"}, + ItemType.SCENE: { + "scene.scene_hue_seg_1", + "scene.scene_wled_seg_1", + scene_wled_hue_entity.entity_id, + }, + ItemType.SCRIPT: { + "script.device", + "script.area", + "script.floor", + "script.hue", + "script.wled", }, } - - -async def test_script_blueprint(hass): - """Test searching for script blueprints.""" - - assert await async_setup_component( - hass, - "script", - { - "script": { - "blueprint_script_1": { - "use_blueprint": { - "path": "test_service.yaml", - "input": { - "service_to_call": "test.automation", - }, - } - }, - "blueprint_script_2": { - "use_blueprint": { - "path": "test_service.yaml", - "input": { - "service_to_call": "test.automation", - }, - } - }, - } + assert search(ItemType.FLOOR, second_floor.floor_id) == { + ItemType.AREA: {bedroom_area.id}, + ItemType.AUTOMATION: {"automation.scene", "automation.script"}, + ItemType.CONFIG_ENTRY: {wled_config_entry.entry_id}, + ItemType.ENTITY: { + wled_segment_2_entity.entity_id, + person_paulus_entity.entity_id, + scene_wled_hue_entity.entity_id, + script_scene_entity.entity_id, }, - ) - - # Ensure automations set up correctly. - assert hass.states.get("script.blueprint_script_1") is not None - assert hass.states.get("script.blueprint_script_1") is not None - - device_reg = dr.async_get(hass) - entity_reg = er.async_get(hass) - - searcher = search.Searcher(hass, device_reg, entity_reg, MOCK_ENTITY_SOURCES) - assert searcher.async_search("script", "script.blueprint_script_1") == { - "entity": {"light.kitchen"}, - "script": {"script.blueprint_script_2"}, - "script_blueprint": {"test_service.yaml"}, + ItemType.GROUP: {"group.wled", "group.wled_hue"}, + ItemType.PERSON: {person_paulus_entity.entity_id}, + ItemType.SCENE: {scene_wled_hue_entity.entity_id}, + ItemType.SCRIPT: {script_scene_entity.entity_id, "script.nested"}, } - searcher = search.Searcher(hass, device_reg, entity_reg, MOCK_ENTITY_SOURCES) - assert searcher.async_search("script_blueprint", "test_service.yaml") == { - "script": {"script.blueprint_script_1", "script.blueprint_script_2"}, + assert not search(ItemType.GROUP, "group.unknown") + assert search(ItemType.GROUP, "group.wled") == { + ItemType.AREA: {bedroom_area.id, living_room_area.id}, + ItemType.CONFIG_ENTRY: {wled_config_entry.entry_id}, + ItemType.DEVICE: {wled_device.id}, + ItemType.ENTITY: { + wled_segment_1_entity.entity_id, + wled_segment_2_entity.entity_id, + }, + ItemType.FLOOR: {first_floor.floor_id, second_floor.floor_id}, + } + assert search(ItemType.GROUP, "group.hue") == { + ItemType.AREA: {kitchen_area.id}, + ItemType.CONFIG_ENTRY: {hue_config_entry.entry_id}, + ItemType.DEVICE: {hue_device.id}, + ItemType.ENTITY: { + hue_segment_1_entity.entity_id, + hue_segment_2_entity.entity_id, + }, + ItemType.FLOOR: {first_floor.floor_id}, + } + assert search(ItemType.GROUP, "group.wled_hue") == { + ItemType.AREA: {bedroom_area.id, living_room_area.id, kitchen_area.id}, + ItemType.AUTOMATION: {"automation.group"}, + ItemType.CONFIG_ENTRY: {wled_config_entry.entry_id, hue_config_entry.entry_id}, + ItemType.DEVICE: {wled_device.id, hue_device.id}, + ItemType.ENTITY: { + wled_segment_1_entity.entity_id, + wled_segment_2_entity.entity_id, + hue_segment_1_entity.entity_id, + hue_segment_2_entity.entity_id, + }, + ItemType.FLOOR: {first_floor.floor_id, second_floor.floor_id}, + ItemType.SCRIPT: {"script.group"}, } + assert not search(ItemType.LABEL, "unknown") + assert search(ItemType.LABEL, label_christmas.label_id) == { + ItemType.AUTOMATION: {"automation.label"}, + ItemType.DEVICE: {wled_device.id}, + } + assert search(ItemType.LABEL, label_energy.label_id) == { + ItemType.ENTITY: {hue_segment_1_entity.entity_id}, + } + assert search(ItemType.LABEL, label_other.label_id) == { + ItemType.AREA: {bedroom_area.id}, + ItemType.ENTITY: { + scene_wled_hue_entity.entity_id, + person_paulus_entity.entity_id, + script_scene_entity.entity_id, + }, + ItemType.PERSON: {person_paulus_entity.entity_id}, + ItemType.SCENE: {scene_wled_hue_entity.entity_id}, + ItemType.SCRIPT: {"script.label", script_scene_entity.entity_id}, + } -async def test_ws_api(hass: HomeAssistant, hass_ws_client: WebSocketGenerator) -> None: - """Test WS API.""" - assert await async_setup_component(hass, "search", {}) + assert not search(ItemType.PERSON, "person.unknown") + assert search(ItemType.PERSON, person_paulus_entity.entity_id) == { + ItemType.AREA: {bedroom_area.id}, + ItemType.ENTITY: {"device_tracker.paulus_iphone"}, + ItemType.FLOOR: {second_floor.floor_id}, + ItemType.LABEL: {label_other.label_id}, + } - area_reg = ar.async_get(hass) - device_reg = dr.async_get(hass) + assert not search(ItemType.SCENE, "scene.unknown") + assert search(ItemType.SCENE, "scene.scene_wled_seg_1") == { + ItemType.AREA: {living_room_area.id}, + ItemType.CONFIG_ENTRY: {wled_config_entry.entry_id}, + ItemType.DEVICE: {wled_device.id}, + ItemType.ENTITY: {wled_segment_1_entity.entity_id}, + ItemType.FLOOR: {first_floor.floor_id}, + } + assert search(ItemType.SCENE, "scene.scene_hue_seg_1") == { + ItemType.AREA: {kitchen_area.id}, + ItemType.CONFIG_ENTRY: {hue_config_entry.entry_id}, + ItemType.DEVICE: {hue_device.id}, + ItemType.ENTITY: {hue_segment_1_entity.entity_id}, + ItemType.FLOOR: {first_floor.floor_id}, + } + assert search(ItemType.SCENE, scene_wled_hue_entity.entity_id) == { + ItemType.AREA: {bedroom_area.id, living_room_area.id, kitchen_area.id}, + ItemType.AUTOMATION: {"automation.scene"}, + ItemType.CONFIG_ENTRY: {wled_config_entry.entry_id, hue_config_entry.entry_id}, + ItemType.DEVICE: {wled_device.id, hue_device.id}, + ItemType.ENTITY: { + wled_segment_1_entity.entity_id, + wled_segment_2_entity.entity_id, + hue_segment_1_entity.entity_id, + hue_segment_2_entity.entity_id, + }, + ItemType.FLOOR: {first_floor.floor_id, second_floor.floor_id}, + ItemType.LABEL: {label_other.label_id}, + ItemType.SCRIPT: {script_scene_entity.entity_id}, + } - kitchen_area = area_reg.async_create("Kitchen") + assert not search(ItemType.SCRIPT, "script.unknown") + assert search(ItemType.SCRIPT, "script.blueprint_script_1") == { + ItemType.ENTITY: {"light.kitchen"}, + ItemType.SCRIPT_BLUEPRINT: {"test_service.yaml"}, + } + assert search(ItemType.SCRIPT, "script.blueprint_script_2") == { + ItemType.ENTITY: {"light.kitchen"}, + ItemType.SCRIPT_BLUEPRINT: {"test_service.yaml"}, + } + assert search(ItemType.SCRIPT, "script.wled") == { + ItemType.AREA: {living_room_area.id}, + ItemType.CONFIG_ENTRY: {wled_config_entry.entry_id}, + ItemType.DEVICE: {wled_device.id}, + ItemType.ENTITY: {wled_segment_1_entity.entity_id}, + ItemType.FLOOR: {first_floor.floor_id}, + } + assert search(ItemType.SCRIPT, "script.hue") == { + ItemType.AREA: {kitchen_area.id}, + ItemType.CONFIG_ENTRY: {hue_config_entry.entry_id}, + ItemType.DEVICE: {hue_device.id}, + ItemType.ENTITY: {hue_segment_1_entity.entity_id}, + ItemType.FLOOR: {first_floor.floor_id}, + } + assert search(ItemType.SCRIPT, "script.script_with_templated_services") == {} + assert search(ItemType.SCRIPT, "script.device") == { + ItemType.AREA: {kitchen_area.id}, + ItemType.CONFIG_ENTRY: {hue_config_entry.entry_id}, + ItemType.DEVICE: {hue_device.id}, + ItemType.FLOOR: {first_floor.floor_id}, + } + assert search(ItemType.SCRIPT, "script.floor") == { + ItemType.FLOOR: {first_floor.floor_id}, + } + assert search(ItemType.SCRIPT, "script.area") == { + ItemType.AREA: {kitchen_area.id}, + ItemType.FLOOR: {first_floor.floor_id}, + } + assert search(ItemType.SCRIPT, "script.group") == { + ItemType.AREA: {bedroom_area.id, living_room_area.id, kitchen_area.id}, + ItemType.CONFIG_ENTRY: {wled_config_entry.entry_id, hue_config_entry.entry_id}, + ItemType.DEVICE: {wled_device.id, hue_device.id}, + ItemType.ENTITY: { + "group.wled_hue", + wled_segment_1_entity.entity_id, + wled_segment_2_entity.entity_id, + hue_segment_1_entity.entity_id, + hue_segment_2_entity.entity_id, + }, + ItemType.FLOOR: {first_floor.floor_id, second_floor.floor_id}, + ItemType.GROUP: {"group.wled_hue"}, + } + assert search(ItemType.SCRIPT, script_scene_entity.entity_id) == { + ItemType.AREA: {bedroom_area.id, kitchen_area.id, living_room_area.id}, + ItemType.CONFIG_ENTRY: {wled_config_entry.entry_id, hue_config_entry.entry_id}, + ItemType.DEVICE: {wled_device.id, hue_device.id}, + ItemType.ENTITY: { + wled_segment_1_entity.entity_id, + wled_segment_2_entity.entity_id, + hue_segment_1_entity.entity_id, + hue_segment_2_entity.entity_id, + scene_wled_hue_entity.entity_id, + }, + ItemType.FLOOR: {first_floor.floor_id, second_floor.floor_id}, + ItemType.LABEL: {label_other.label_id}, + ItemType.SCENE: {scene_wled_hue_entity.entity_id}, + } + assert search(ItemType.SCRIPT, "script.nested") == { + ItemType.AREA: {bedroom_area.id, kitchen_area.id, living_room_area.id}, + ItemType.CONFIG_ENTRY: {wled_config_entry.entry_id, hue_config_entry.entry_id}, + ItemType.DEVICE: {wled_device.id, hue_device.id}, + ItemType.ENTITY: { + wled_segment_1_entity.entity_id, + wled_segment_2_entity.entity_id, + hue_segment_1_entity.entity_id, + hue_segment_2_entity.entity_id, + scene_wled_hue_entity.entity_id, + script_scene_entity.entity_id, + }, + ItemType.FLOOR: {first_floor.floor_id, second_floor.floor_id}, + ItemType.SCENE: {scene_wled_hue_entity.entity_id}, + ItemType.SCRIPT: {script_scene_entity.entity_id}, + } - hue_config_entry = MockConfigEntry(domain="hue") - hue_config_entry.add_to_hass(hass) - - hue_device = device_reg.async_get_or_create( - config_entry_id=hue_config_entry.entry_id, - name="Light Strip", - identifiers=({"hue", "hue-1"}), - ) - - device_reg.async_update_device(hue_device.id, area_id=kitchen_area.id) + assert not search(ItemType.SCRIPT_BLUEPRINT, "unknown.yaml") + assert search(ItemType.SCRIPT_BLUEPRINT, "test_service.yaml") == { + ItemType.SCRIPT: {"script.blueprint_script_1", "script.blueprint_script_2"}, + } + # WebSocket client = await hass_ws_client(hass) - await client.send_json( { "id": 1, @@ -584,6 +966,23 @@ async def test_ws_api(hass: HomeAssistant, hass_ws_client: WebSocketGenerator) - response = await client.receive_json() assert response["success"] assert response["result"] == { - "config_entry": [hue_config_entry.entry_id], - "area": [kitchen_area.id], + ItemType.AREA: [kitchen_area.id], + ItemType.ENTITY: unordered( + [ + hue_segment_1_entity.entity_id, + hue_segment_2_entity.entity_id, + ] + ), + ItemType.GROUP: unordered( + [ + "group.hue", + "group.wled_hue", + ] + ), + ItemType.CONFIG_ENTRY: [hue_config_entry.entry_id], + ItemType.FLOOR: [first_floor.floor_id], + ItemType.SCENE: unordered( + ["scene.scene_hue_seg_1", scene_wled_hue_entity.entity_id] + ), + ItemType.SCRIPT: unordered(["script.device", "script.hue"]), } diff --git a/tests/components/season/conftest.py b/tests/components/season/conftest.py index 40d95f3331b..b0b4f1058d9 100644 --- a/tests/components/season/conftest.py +++ b/tests/components/season/conftest.py @@ -1,4 +1,5 @@ """Fixtures for Season integration tests.""" + from __future__ import annotations from collections.abc import Generator diff --git a/tests/components/season/test_config_flow.py b/tests/components/season/test_config_flow.py index 884c5a3ddc8..e0a140f7136 100644 --- a/tests/components/season/test_config_flow.py +++ b/tests/components/season/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for the Season config flow.""" + from unittest.mock import MagicMock from homeassistant.components.season.const import DOMAIN, TYPE_ASTRONOMICAL diff --git a/tests/components/season/test_init.py b/tests/components/season/test_init.py index 9d964512160..6c716d5e4a5 100644 --- a/tests/components/season/test_init.py +++ b/tests/components/season/test_init.py @@ -1,4 +1,5 @@ """Tests for the Season integration.""" + from homeassistant.components.season.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant diff --git a/tests/components/season/test_sensor.py b/tests/components/season/test_sensor.py index 413291c4f75..dd42ad6ce1c 100644 --- a/tests/components/season/test_sensor.py +++ b/tests/components/season/test_sensor.py @@ -1,4 +1,5 @@ """The tests for the Season integration.""" + from datetime import datetime from freezegun import freeze_time diff --git a/tests/components/select/test_device_action.py b/tests/components/select/test_device_action.py index 121b41fcb2b..c83e2585d5b 100644 --- a/tests/components/select/test_device_action.py +++ b/tests/components/select/test_device_action.py @@ -1,4 +1,5 @@ """The tests for Select device actions.""" + import pytest from pytest_unordered import unordered import voluptuous_serialize @@ -62,12 +63,12 @@ async def test_get_actions( @pytest.mark.parametrize( ("hidden_by", "entity_category"), - ( + [ (er.RegistryEntryHider.INTEGRATION, None), (er.RegistryEntryHider.USER, None), (None, EntityCategory.CONFIG), (None, EntityCategory.DIAGNOSTIC), - ), + ], ) async def test_get_actions_hidden_auxiliary( hass: HomeAssistant, @@ -114,7 +115,7 @@ async def test_get_actions_hidden_auxiliary( assert actions == unordered(expected_actions) -@pytest.mark.parametrize("action_type", ("select_first", "select_last")) +@pytest.mark.parametrize("action_type", ["select_first", "select_last"]) async def test_action_select_first_last( hass: HomeAssistant, device_registry: dr.DeviceRegistry, @@ -163,7 +164,7 @@ async def test_action_select_first_last( assert select_calls[0].data == {"entity_id": entry.entity_id} -@pytest.mark.parametrize("action_type", ("select_first", "select_last")) +@pytest.mark.parametrize("action_type", ["select_first", "select_last"]) async def test_action_select_first_last_legacy( hass: HomeAssistant, device_registry: dr.DeviceRegistry, diff --git a/tests/components/select/test_device_condition.py b/tests/components/select/test_device_condition.py index 3e0ecd6e547..526ad678c19 100644 --- a/tests/components/select/test_device_condition.py +++ b/tests/components/select/test_device_condition.py @@ -1,4 +1,5 @@ """The tests for Select device conditions.""" + from __future__ import annotations import pytest @@ -66,12 +67,12 @@ async def test_get_conditions( @pytest.mark.parametrize( ("hidden_by", "entity_category"), - ( + [ (er.RegistryEntryHider.INTEGRATION, None), (er.RegistryEntryHider.USER, None), (None, EntityCategory.CONFIG), (None, EntityCategory.DIAGNOSTIC), - ), + ], ) async def test_get_conditions_hidden_auxiliary( hass: HomeAssistant, diff --git a/tests/components/select/test_device_trigger.py b/tests/components/select/test_device_trigger.py index 0be5c605dc1..e587e125e11 100644 --- a/tests/components/select/test_device_trigger.py +++ b/tests/components/select/test_device_trigger.py @@ -1,4 +1,5 @@ """The tests for Select device triggers.""" + from __future__ import annotations import pytest @@ -66,12 +67,12 @@ async def test_get_triggers( @pytest.mark.parametrize( ("hidden_by", "entity_category"), - ( + [ (er.RegistryEntryHider.INTEGRATION, None), (er.RegistryEntryHider.USER, None), (None, EntityCategory.CONFIG), (None, EntityCategory.DIAGNOSTIC), - ), + ], ) async def test_get_triggers_hidden_auxiliary( hass: HomeAssistant, diff --git a/tests/components/select/test_init.py b/tests/components/select/test_init.py index 604bf3f0fb9..b135a6e1ab0 100644 --- a/tests/components/select/test_init.py +++ b/tests/components/select/test_init.py @@ -1,4 +1,5 @@ """The tests for the Select component.""" + from unittest.mock import MagicMock import pytest diff --git a/tests/components/select/test_recorder.py b/tests/components/select/test_recorder.py index 53911578d53..73dea423f6a 100644 --- a/tests/components/select/test_recorder.py +++ b/tests/components/select/test_recorder.py @@ -1,4 +1,5 @@ """The tests for select recorder.""" + from __future__ import annotations from datetime import timedelta diff --git a/tests/components/select/test_reproduce_state.py b/tests/components/select/test_reproduce_state.py index bbd1ae17a7b..579ef3301d9 100644 --- a/tests/components/select/test_reproduce_state.py +++ b/tests/components/select/test_reproduce_state.py @@ -1,4 +1,5 @@ """Test reproduce state for select entities.""" + import pytest from homeassistant.components.select.const import ( diff --git a/tests/components/select/test_significant_change.py b/tests/components/select/test_significant_change.py index 34ae5cad54e..7cba7e4962d 100644 --- a/tests/components/select/test_significant_change.py +++ b/tests/components/select/test_significant_change.py @@ -1,4 +1,5 @@ """Test the select significant change platform.""" + from homeassistant.components.select.significant_change import ( async_check_significant_change, ) diff --git a/tests/components/sense/test_config_flow.py b/tests/components/sense/test_config_flow.py index 40a6189dc74..dc1cee43662 100644 --- a/tests/components/sense/test_config_flow.py +++ b/tests/components/sense/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Sense config flow.""" + from unittest.mock import AsyncMock, patch import pytest diff --git a/tests/components/sensibo/__init__.py b/tests/components/sensibo/__init__.py index da585f8d1e8..09a57640472 100644 --- a/tests/components/sensibo/__init__.py +++ b/tests/components/sensibo/__init__.py @@ -1,4 +1,5 @@ """Tests for the Sensibo integration.""" + from __future__ import annotations from homeassistant.const import CONF_API_KEY diff --git a/tests/components/sensibo/conftest.py b/tests/components/sensibo/conftest.py index 17c295b4c48..d98b19c3833 100644 --- a/tests/components/sensibo/conftest.py +++ b/tests/components/sensibo/conftest.py @@ -1,4 +1,5 @@ """Fixtures for the Sensibo integration.""" + from __future__ import annotations import json @@ -33,15 +34,19 @@ async def load_int(hass: HomeAssistant, get_data: SensiboData) -> MockConfigEntr config_entry.add_to_hass(hass) - with patch( - "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", - return_value=get_data, - ), patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices", - return_value={"result": [{"id": "xyzxyz"}, {"id": "abcabc"}]}, - ), patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_me", - return_value={"result": {"username": "username"}}, + with ( + patch( + "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", + return_value=get_data, + ), + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices", + return_value={"result": [{"id": "xyzxyz"}, {"id": "abcabc"}]}, + ), + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_me", + return_value={"result": {"username": "username"}}, + ), ): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/sensibo/snapshots/test_sensor.ambr b/tests/components/sensibo/snapshots/test_sensor.ambr index 4522071049d..d645bdbd383 100644 --- a/tests/components/sensibo/snapshots/test_sensor.ambr +++ b/tests/components/sensibo/snapshots/test_sensor.ambr @@ -3,7 +3,6 @@ ReadOnlyDict({ 'device_class': 'pm25', 'friendly_name': 'Kitchen PM2.5', - 'icon': 'mdi:air-filter', 'state_class': , 'unit_of_measurement': 'µg/m³', }) diff --git a/tests/components/sensibo/test_binary_sensor.py b/tests/components/sensibo/test_binary_sensor.py index 99bcfac8c9b..24653e6b7c7 100644 --- a/tests/components/sensibo/test_binary_sensor.py +++ b/tests/components/sensibo/test_binary_sensor.py @@ -1,4 +1,5 @@ """The test for the sensibo binary sensor platform.""" + from __future__ import annotations from datetime import timedelta diff --git a/tests/components/sensibo/test_button.py b/tests/components/sensibo/test_button.py index 2277c84d187..6d7ce442562 100644 --- a/tests/components/sensibo/test_button.py +++ b/tests/components/sensibo/test_button.py @@ -1,4 +1,5 @@ """The test for the sensibo button platform.""" + from __future__ import annotations from datetime import datetime, timedelta @@ -41,12 +42,15 @@ async def test_button( today_str = today.isoformat() freezer.move_to(today) - with patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", - return_value=get_data, - ), patch( - "homeassistant.components.sensibo.util.SensiboClient.async_reset_filter", - return_value={"status": "success"}, + with ( + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", + return_value=get_data, + ), + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_reset_filter", + return_value={"status": "success"}, + ), ): await hass.services.async_call( BUTTON_DOMAIN, @@ -93,14 +97,18 @@ async def test_button_failure( state_button = hass.states.get("button.hallway_reset_filter") - with patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", - return_value=get_data, - ), patch( - "homeassistant.components.sensibo.util.SensiboClient.async_reset_filter", - return_value={"status": "failure"}, - ), pytest.raises( - HomeAssistantError, + with ( + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", + return_value=get_data, + ), + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_reset_filter", + return_value={"status": "failure"}, + ), + pytest.raises( + HomeAssistantError, + ), ): await hass.services.async_call( BUTTON_DOMAIN, diff --git a/tests/components/sensibo/test_climate.py b/tests/components/sensibo/test_climate.py index bf0113cb22b..061e31f9771 100644 --- a/tests/components/sensibo/test_climate.py +++ b/tests/components/sensibo/test_climate.py @@ -1,4 +1,5 @@ """The test for the sensibo binary sensor platform.""" + from __future__ import annotations from datetime import datetime, timedelta @@ -120,12 +121,15 @@ async def test_climate_fan( state1 = hass.states.get("climate.hallway") assert state1.attributes["fan_mode"] == "high" - with patch( - "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", - return_value=get_data, - ), patch( - "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", - return_value={"result": {"status": "Success"}}, + with ( + patch( + "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", + return_value=get_data, + ), + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", + return_value={"result": {"status": "Success"}}, + ), ): await hass.services.async_call( CLIMATE_DOMAIN, @@ -161,9 +165,12 @@ async def test_climate_fan( ) await hass.async_block_till_done() - with patch( - "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", - ), pytest.raises(HomeAssistantError): + with ( + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", + ), + pytest.raises(HomeAssistantError), + ): await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_FAN_MODE, @@ -187,12 +194,15 @@ async def test_climate_swing( state1 = hass.states.get("climate.hallway") assert state1.attributes["swing_mode"] == "stopped" - with patch( - "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", - return_value=get_data, - ), patch( - "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", - return_value={"result": {"status": "Success"}}, + with ( + patch( + "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", + return_value=get_data, + ), + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", + return_value={"result": {"status": "Success"}}, + ), ): await hass.services.async_call( CLIMATE_DOMAIN, @@ -227,9 +237,12 @@ async def test_climate_swing( ) await hass.async_block_till_done() - with patch( - "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", - ), pytest.raises(HomeAssistantError): + with ( + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", + ), + pytest.raises(HomeAssistantError), + ): await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_SWING_MODE, @@ -253,12 +266,15 @@ async def test_climate_temperatures( state1 = hass.states.get("climate.hallway") assert state1.attributes["temperature"] == 25 - with patch( - "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", - return_value=get_data, - ), patch( - "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", - return_value={"result": {"status": "Success"}}, + with ( + patch( + "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", + return_value=get_data, + ), + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", + return_value={"result": {"status": "Success"}}, + ), ): await hass.services.async_call( CLIMATE_DOMAIN, @@ -271,12 +287,15 @@ async def test_climate_temperatures( state2 = hass.states.get("climate.hallway") assert state2.attributes["temperature"] == 20 - with patch( - "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", - return_value=get_data, - ), patch( - "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", - return_value={"result": {"status": "Success"}}, + with ( + patch( + "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", + return_value=get_data, + ), + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", + return_value={"result": {"status": "Success"}}, + ), ): await hass.services.async_call( CLIMATE_DOMAIN, @@ -289,12 +308,15 @@ async def test_climate_temperatures( state2 = hass.states.get("climate.hallway") assert state2.attributes["temperature"] == 16 - with patch( - "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", - return_value=get_data, - ), patch( - "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", - return_value={"result": {"status": "Success"}}, + with ( + patch( + "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", + return_value=get_data, + ), + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", + return_value={"result": {"status": "Success"}}, + ), ): await hass.services.async_call( CLIMATE_DOMAIN, @@ -307,12 +329,15 @@ async def test_climate_temperatures( state2 = hass.states.get("climate.hallway") assert state2.attributes["temperature"] == 19 - with patch( - "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", - return_value=get_data, - ), patch( - "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", - return_value={"result": {"status": "Success"}}, + with ( + patch( + "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", + return_value=get_data, + ), + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", + return_value={"result": {"status": "Success"}}, + ), ): await hass.services.async_call( CLIMATE_DOMAIN, @@ -325,12 +350,15 @@ async def test_climate_temperatures( state2 = hass.states.get("climate.hallway") assert state2.attributes["temperature"] == 20 - with patch( - "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", - return_value=get_data, - ), patch( - "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", - return_value={"result": {"status": "Success"}}, + with ( + patch( + "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", + return_value=get_data, + ), + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", + return_value={"result": {"status": "Success"}}, + ), ): await hass.services.async_call( CLIMATE_DOMAIN, @@ -343,10 +371,13 @@ async def test_climate_temperatures( state2 = hass.states.get("climate.hallway") assert state2.attributes["temperature"] == 20 - with patch( - "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", - return_value={"result": {"status": "Success"}}, - ), pytest.raises(MultipleInvalid): + with ( + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", + return_value={"result": {"status": "Success"}}, + ), + pytest.raises(MultipleInvalid), + ): await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, @@ -380,10 +411,13 @@ async def test_climate_temperatures( ) await hass.async_block_till_done() - with patch( - "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", - return_value={"result": {"status": "Success"}}, - ), pytest.raises(HomeAssistantError): + with ( + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", + return_value={"result": {"status": "Success"}}, + ), + pytest.raises(HomeAssistantError), + ): await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, @@ -436,9 +470,12 @@ async def test_climate_temperature_is_none( state1 = hass.states.get("climate.hallway") assert state1.attributes["temperature"] == 25 - with patch( - "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", - ), pytest.raises(ServiceValidationError): + with ( + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", + ), + pytest.raises(ServiceValidationError), + ): await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, @@ -490,12 +527,15 @@ async def test_climate_hvac_mode( state1 = hass.states.get("climate.hallway") assert state1.state == "heat" - with patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", - return_value=get_data, - ), patch( - "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", - return_value={"result": {"status": "Success"}}, + with ( + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", + return_value=get_data, + ), + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", + return_value={"result": {"status": "Success"}}, + ), ): await hass.services.async_call( CLIMATE_DOMAIN, @@ -519,12 +559,15 @@ async def test_climate_hvac_mode( ) await hass.async_block_till_done() - with patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", - return_value=get_data, - ), patch( - "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", - return_value={"result": {"status": "Success"}}, + with ( + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", + return_value=get_data, + ), + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", + return_value={"result": {"status": "Success"}}, + ), ): await hass.services.async_call( CLIMATE_DOMAIN, @@ -561,12 +604,15 @@ async def test_climate_on_off( state1 = hass.states.get("climate.hallway") assert state1.state == "heat" - with patch( - "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", - return_value=get_data, - ), patch( - "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", - return_value={"result": {"status": "Success"}}, + with ( + patch( + "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", + return_value=get_data, + ), + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", + return_value={"result": {"status": "Success"}}, + ), ): await hass.services.async_call( CLIMATE_DOMAIN, @@ -579,12 +625,15 @@ async def test_climate_on_off( state2 = hass.states.get("climate.hallway") assert state2.state == "off" - with patch( - "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", - return_value=get_data, - ), patch( - "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", - return_value={"result": {"status": "Success"}}, + with ( + patch( + "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", + return_value=get_data, + ), + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", + return_value={"result": {"status": "Success"}}, + ), ): await hass.services.async_call( CLIMATE_DOMAIN, @@ -621,10 +670,15 @@ async def test_climate_service_failed( state1 = hass.states.get("climate.hallway") assert state1.state == "heat" - with patch( - "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", - return_value={"result": {"status": "Error", "failureReason": "Did not work"}}, - ), pytest.raises(HomeAssistantError): + with ( + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", + return_value={ + "result": {"status": "Error", "failureReason": "Did not work"} + }, + ), + pytest.raises(HomeAssistantError), + ): await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_TURN_OFF, @@ -660,12 +714,15 @@ async def test_climate_assumed_state( state1 = hass.states.get("climate.hallway") assert state1.state == "heat" - with patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", - return_value=get_data, - ), patch( - "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", - return_value={"result": {"status": "Success"}}, + with ( + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", + return_value=get_data, + ), + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", + return_value={"result": {"status": "Success"}}, + ), ): await hass.services.async_call( DOMAIN, @@ -735,14 +792,18 @@ async def test_climate_set_timer( state_climate = hass.states.get("climate.hallway") assert hass.states.get("sensor.hallway_timer_end_time").state == STATE_UNKNOWN - with patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", - return_value=get_data, - ), patch( - "homeassistant.components.sensibo.util.SensiboClient.async_set_timer", - return_value={"status": "failure"}, - ), pytest.raises( - MultipleInvalid, + with ( + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", + return_value=get_data, + ), + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_timer", + return_value={"status": "failure"}, + ), + pytest.raises( + MultipleInvalid, + ), ): await hass.services.async_call( DOMAIN, @@ -754,14 +815,18 @@ async def test_climate_set_timer( ) await hass.async_block_till_done() - with patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", - return_value=get_data, - ), patch( - "homeassistant.components.sensibo.util.SensiboClient.async_set_timer", - return_value={"status": "failure"}, - ), pytest.raises( - HomeAssistantError, + with ( + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", + return_value=get_data, + ), + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_timer", + return_value={"status": "failure"}, + ), + pytest.raises( + HomeAssistantError, + ), ): await hass.services.async_call( DOMAIN, @@ -774,12 +839,15 @@ async def test_climate_set_timer( ) await hass.async_block_till_done() - with patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", - return_value=get_data, - ), patch( - "homeassistant.components.sensibo.util.SensiboClient.async_set_timer", - return_value={"status": "success", "result": {"id": "SzTGE4oZ4D"}}, + with ( + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", + return_value=get_data, + ), + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_timer", + return_value={"status": "success", "result": {"id": "SzTGE4oZ4D"}}, + ), ): await hass.services.async_call( DOMAIN, @@ -840,12 +908,16 @@ async def test_climate_pure_boost( state2 = hass.states.get("switch.kitchen_pure_boost") assert state2.state == "off" - with patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", - ), patch( - "homeassistant.components.sensibo.util.SensiboClient.async_set_pureboost", - ), pytest.raises( - MultipleInvalid, + with ( + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", + ), + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_pureboost", + ), + pytest.raises( + MultipleInvalid, + ), ): await hass.services.async_call( DOMAIN, @@ -860,22 +932,25 @@ async def test_climate_pure_boost( ) await hass.async_block_till_done() - with patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", - return_value=get_data, - ), patch( - "homeassistant.components.sensibo.util.SensiboClient.async_set_pureboost", - return_value={ - "status": "success", - "result": { - "enabled": True, - "sensitivity": "S", - "measurements_integration": True, - "ac_integration": False, - "geo_integration": False, - "prime_integration": True, + with ( + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", + return_value=get_data, + ), + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_pureboost", + return_value={ + "status": "success", + "result": { + "enabled": True, + "sensitivity": "S", + "measurements_integration": True, + "ac_integration": False, + "geo_integration": False, + "prime_integration": True, + }, }, - }, + ), ): await hass.services.async_call( DOMAIN, @@ -942,12 +1017,16 @@ async def test_climate_climate_react( state_climate = hass.states.get("climate.hallway") - with patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", - ), patch( - "homeassistant.components.sensibo.util.SensiboClient.async_set_climate_react", - ), pytest.raises( - MultipleInvalid, + with ( + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", + ), + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_climate_react", + ), + pytest.raises( + MultipleInvalid, + ), ): await hass.services.async_call( DOMAIN, @@ -962,41 +1041,44 @@ async def test_climate_climate_react( ) await hass.async_block_till_done() - with patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", - return_value=get_data, - ), patch( - "homeassistant.components.sensibo.util.SensiboClient.async_set_climate_react", - return_value={ - "status": "success", - "result": { - "enabled": True, - "deviceUid": "ABC999111", - "highTemperatureState": { - "on": True, - "targetTemperature": 15, - "temperatureUnit": "C", - "mode": "cool", - "fanLevel": "high", - "swing": "stopped", - "horizontalSwing": "stopped", - "light": "on", + with ( + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", + return_value=get_data, + ), + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_climate_react", + return_value={ + "status": "success", + "result": { + "enabled": True, + "deviceUid": "ABC999111", + "highTemperatureState": { + "on": True, + "targetTemperature": 15, + "temperatureUnit": "C", + "mode": "cool", + "fanLevel": "high", + "swing": "stopped", + "horizontalSwing": "stopped", + "light": "on", + }, + "highTemperatureThreshold": 30.5, + "lowTemperatureState": { + "on": True, + "targetTemperature": 25, + "temperatureUnit": "C", + "mode": "heat", + "fanLevel": "low", + "swing": "stopped", + "horizontalSwing": "stopped", + "light": "on", + }, + "lowTemperatureThreshold": 5.5, + "type": "temperature", }, - "highTemperatureThreshold": 30.5, - "lowTemperatureState": { - "on": True, - "targetTemperature": 25, - "temperatureUnit": "C", - "mode": "heat", - "fanLevel": "low", - "swing": "stopped", - "horizontalSwing": "stopped", - "light": "on", - }, - "lowTemperatureThreshold": 5.5, - "type": "temperature", }, - }, + ), ): await hass.services.async_call( DOMAIN, @@ -1105,41 +1187,44 @@ async def test_climate_climate_react_fahrenheit( state_climate = hass.states.get("climate.hallway") - with patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", - return_value=get_data, - ), patch( - "homeassistant.components.sensibo.util.SensiboClient.async_set_climate_react", - return_value={ - "status": "success", - "result": { - "enabled": True, - "deviceUid": "ABC999111", - "highTemperatureState": { - "on": True, - "targetTemperature": 65, - "temperatureUnit": "F", - "mode": "cool", - "fanLevel": "high", - "swing": "stopped", - "horizontalSwing": "stopped", - "light": "on", + with ( + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", + return_value=get_data, + ), + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_climate_react", + return_value={ + "status": "success", + "result": { + "enabled": True, + "deviceUid": "ABC999111", + "highTemperatureState": { + "on": True, + "targetTemperature": 65, + "temperatureUnit": "F", + "mode": "cool", + "fanLevel": "high", + "swing": "stopped", + "horizontalSwing": "stopped", + "light": "on", + }, + "highTemperatureThreshold": 77, + "lowTemperatureState": { + "on": True, + "targetTemperature": 85, + "temperatureUnit": "F", + "mode": "heat", + "fanLevel": "low", + "swing": "stopped", + "horizontalSwing": "stopped", + "light": "on", + }, + "lowTemperatureThreshold": 32, + "type": "temperature", }, - "highTemperatureThreshold": 77, - "lowTemperatureState": { - "on": True, - "targetTemperature": 85, - "temperatureUnit": "F", - "mode": "heat", - "fanLevel": "low", - "swing": "stopped", - "horizontalSwing": "stopped", - "light": "on", - }, - "lowTemperatureThreshold": 32, - "type": "temperature", }, - }, + ), ): await hass.services.async_call( DOMAIN, @@ -1249,12 +1334,16 @@ async def test_climate_full_ac_state( state_climate = hass.states.get("climate.hallway") assert state_climate.state == "heat" - with patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", - ), patch( - "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_states", - ), pytest.raises( - MultipleInvalid, + with ( + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", + ), + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_states", + ), + pytest.raises( + MultipleInvalid, + ), ): await hass.services.async_call( DOMAIN, @@ -1267,12 +1356,15 @@ async def test_climate_full_ac_state( ) await hass.async_block_till_done() - with patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", - return_value=get_data, - ), patch( - "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_states", - return_value={"result": {"status": "Success"}}, + with ( + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", + return_value=get_data, + ), + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_states", + return_value={"result": {"status": "Success"}}, + ), ): await hass.services.async_call( DOMAIN, @@ -1328,9 +1420,12 @@ async def test_climate_fan_mode_and_swing_mode_not_supported( assert state1.attributes["fan_mode"] == "high" assert state1.attributes["swing_mode"] == "stopped" - with patch( - "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", - ), pytest.raises(ServiceValidationError): + with ( + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", + ), + pytest.raises(ServiceValidationError), + ): await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_SWING_MODE, @@ -1338,9 +1433,12 @@ async def test_climate_fan_mode_and_swing_mode_not_supported( blocking=True, ) - with patch( - "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", - ), pytest.raises(ServiceValidationError): + with ( + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", + ), + pytest.raises(ServiceValidationError), + ): await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_FAN_MODE, diff --git a/tests/components/sensibo/test_config_flow.py b/tests/components/sensibo/test_config_flow.py index feba0e2c39b..3b1117f0908 100644 --- a/tests/components/sensibo/test_config_flow.py +++ b/tests/components/sensibo/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Sensibo config flow.""" + from __future__ import annotations from typing import Any @@ -28,16 +29,20 @@ async def test_form(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.FORM assert result["errors"] == {} - with patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices", - return_value={"result": [{"id": "xyzxyz"}, {"id": "abcabc"}]}, - ), patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_me", - return_value={"result": {"username": "username"}}, - ), patch( - "homeassistant.components.sensibo.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices", + return_value={"result": [{"id": "xyzxyz"}, {"id": "abcabc"}]}, + ), + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_me", + return_value={"result": {"username": "username"}}, + ), + patch( + "homeassistant.components.sensibo.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -89,15 +94,19 @@ async def test_flow_fails( assert result2["errors"] == {"base": p_error} - with patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices", - return_value={"result": [{"id": "xyzxyz"}, {"id": "abcabc"}]}, - ), patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_me", - return_value={"result": {"username": "username"}}, - ), patch( - "homeassistant.components.sensibo.async_setup_entry", - return_value=True, + with ( + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices", + return_value={"result": [{"id": "xyzxyz"}, {"id": "abcabc"}]}, + ), + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_me", + return_value={"result": {"username": "username"}}, + ), + patch( + "homeassistant.components.sensibo.async_setup_entry", + return_value=True, + ), ): result3 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -123,12 +132,15 @@ async def test_flow_get_no_devices(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.FORM assert result["step_id"] == config_entries.SOURCE_USER - with patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices", - return_value={"result": []}, - ), patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_me", - return_value={"result": {}}, + with ( + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices", + return_value={"result": []}, + ), + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_me", + return_value={"result": {}}, + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -150,12 +162,15 @@ async def test_flow_get_no_username(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.FORM assert result["step_id"] == config_entries.SOURCE_USER - with patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices", - return_value={"result": [{"id": "xyzxyz"}, {"id": "abcabc"}]}, - ), patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_me", - return_value={"result": {}}, + with ( + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices", + return_value={"result": [{"id": "xyzxyz"}, {"id": "abcabc"}]}, + ), + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_me", + return_value={"result": {}}, + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -190,16 +205,20 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.FORM assert result["errors"] == {} - with patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices", - return_value={"result": [{"id": "xyzxyz"}, {"id": "abcabc"}]}, - ), patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_me", - return_value={"result": {"username": "username"}}, - ) as mock_sensibo, patch( - "homeassistant.components.sensibo.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices", + return_value={"result": [{"id": "xyzxyz"}, {"id": "abcabc"}]}, + ), + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_me", + return_value={"result": {"username": "username"}}, + ) as mock_sensibo, + patch( + "homeassistant.components.sensibo.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_API_KEY: "1234567891"}, @@ -259,15 +278,19 @@ async def test_reauth_flow_error( assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": p_error} - with patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices", - return_value={"result": [{"id": "xyzxyz"}, {"id": "abcabc"}]}, - ), patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_me", - return_value={"result": {"username": "username"}}, - ), patch( - "homeassistant.components.sensibo.async_setup_entry", - return_value=True, + with ( + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices", + return_value={"result": [{"id": "xyzxyz"}, {"id": "abcabc"}]}, + ), + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_me", + return_value={"result": {"username": "username"}}, + ), + patch( + "homeassistant.components.sensibo.async_setup_entry", + return_value=True, + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -328,12 +351,15 @@ async def test_flow_reauth_no_username_or_device( assert result["type"] == FlowResultType.FORM assert result["step_id"] == "reauth_confirm" - with patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices", - return_value=get_devices, - ), patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_me", - return_value=get_me, + with ( + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices", + return_value=get_devices, + ), + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_me", + return_value=get_me, + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], diff --git a/tests/components/sensibo/test_coordinator.py b/tests/components/sensibo/test_coordinator.py index 3c02fb0d3a9..d81b7fd613c 100644 --- a/tests/components/sensibo/test_coordinator.py +++ b/tests/components/sensibo/test_coordinator.py @@ -1,4 +1,5 @@ """The test for the sensibo coordinator.""" + from __future__ import annotations from datetime import timedelta @@ -34,14 +35,18 @@ async def test_coordinator( config_entry.add_to_hass(hass) - with patch( - "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", - ) as mock_data, patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices", - return_value={"result": [{"id": "xyzxyz"}, {"id": "abcabc"}]}, - ), patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_me", - return_value={"result": {"username": "username"}}, + with ( + patch( + "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", + ) as mock_data, + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices", + return_value={"result": [{"id": "xyzxyz"}, {"id": "abcabc"}]}, + ), + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_me", + return_value={"result": {"username": "username"}}, + ), ): monkeypatch.setattr(get_data.parsed["ABC999111"], "hvac_mode", "heat") monkeypatch.setattr(get_data.parsed["ABC999111"], "device_on", True) diff --git a/tests/components/sensibo/test_diagnostics.py b/tests/components/sensibo/test_diagnostics.py index 320125e6403..1fe72cca0f3 100644 --- a/tests/components/sensibo/test_diagnostics.py +++ b/tests/components/sensibo/test_diagnostics.py @@ -1,4 +1,5 @@ """Test Sensibo diagnostics.""" + from __future__ import annotations from syrupy.assertion import SnapshotAssertion diff --git a/tests/components/sensibo/test_entity.py b/tests/components/sensibo/test_entity.py index aff4ba45eaa..071e5473e5c 100644 --- a/tests/components/sensibo/test_entity.py +++ b/tests/components/sensibo/test_entity.py @@ -1,4 +1,5 @@ """The test for the sensibo entity.""" + from __future__ import annotations from unittest.mock import patch @@ -72,10 +73,13 @@ async def test_entity_failed_service_calls( state = hass.states.get("climate.hallway") assert state.attributes["fan_mode"] == "low" - with patch( - "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", - side_effect=p_error, - ), pytest.raises(HomeAssistantError): + with ( + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", + side_effect=p_error, + ), + pytest.raises(HomeAssistantError), + ): await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_FAN_MODE, diff --git a/tests/components/sensibo/test_init.py b/tests/components/sensibo/test_init.py index 90dbcd86a96..9698d5241cc 100644 --- a/tests/components/sensibo/test_init.py +++ b/tests/components/sensibo/test_init.py @@ -1,4 +1,5 @@ """Test for Sensibo component Init.""" + from __future__ import annotations from unittest.mock import patch @@ -31,15 +32,19 @@ async def test_setup_entry(hass: HomeAssistant, get_data: SensiboData) -> None: ) entry.add_to_hass(hass) - with patch( - "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", - return_value=get_data, - ), patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices", - return_value={"result": [{"id": "xyzxyz"}, {"id": "abcabc"}]}, - ), patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_me", - return_value={"result": {"username": "username"}}, + with ( + patch( + "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", + return_value=get_data, + ), + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices", + return_value={"result": [{"id": "xyzxyz"}, {"id": "abcabc"}]}, + ), + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_me", + return_value={"result": {"username": "username"}}, + ), ): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -59,15 +64,19 @@ async def test_migrate_entry(hass: HomeAssistant, get_data: SensiboData) -> None ) entry.add_to_hass(hass) - with patch( - "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", - return_value=get_data, - ), patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices", - return_value={"result": [{"id": "xyzxyz"}, {"id": "abcabc"}]}, - ), patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_me", - return_value={"result": {"username": "username"}}, + with ( + patch( + "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", + return_value=get_data, + ), + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices", + return_value={"result": [{"id": "xyzxyz"}, {"id": "abcabc"}]}, + ), + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_me", + return_value={"result": {"username": "username"}}, + ), ): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -89,13 +98,17 @@ async def test_migrate_entry_fails(hass: HomeAssistant, get_data: SensiboData) - ) entry.add_to_hass(hass) - with patch( - "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", - ), patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices", - ), patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_me", - side_effect=NoUsernameError("No username returned"), + with ( + patch( + "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", + ), + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices", + ), + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_me", + side_effect=NoUsernameError("No username returned"), + ), ): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -117,15 +130,19 @@ async def test_unload_entry(hass: HomeAssistant, get_data: SensiboData) -> None: ) entry.add_to_hass(hass) - with patch( - "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", - return_value=get_data, - ), patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices", - return_value={"result": [{"id": "xyzxyz"}, {"id": "abcabc"}]}, - ), patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_me", - return_value={"result": {"username": "username"}}, + with ( + patch( + "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", + return_value=get_data, + ), + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices", + return_value={"result": [{"id": "xyzxyz"}, {"id": "abcabc"}]}, + ), + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_me", + return_value={"result": {"username": "username"}}, + ), ): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/sensibo/test_number.py b/tests/components/sensibo/test_number.py index bdf3e5721c7..e0a5a6a8bde 100644 --- a/tests/components/sensibo/test_number.py +++ b/tests/components/sensibo/test_number.py @@ -1,4 +1,5 @@ """The test for the sensibo number platform.""" + from __future__ import annotations from datetime import timedelta @@ -62,12 +63,15 @@ async def test_number_set_value( state1 = hass.states.get("number.hallway_temperature_calibration") assert state1.state == "0.1" - with patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", - return_value=get_data, - ), patch( - "homeassistant.components.sensibo.util.SensiboClient.async_set_calibration", - return_value={"status": "failure"}, + with ( + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", + return_value=get_data, + ), + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_calibration", + return_value={"status": "failure"}, + ), ): with pytest.raises(HomeAssistantError): await hass.services.async_call( @@ -81,12 +85,15 @@ async def test_number_set_value( state2 = hass.states.get("number.hallway_temperature_calibration") assert state2.state == "0.1" - with patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", - return_value=get_data, - ), patch( - "homeassistant.components.sensibo.util.SensiboClient.async_set_calibration", - return_value={"status": "success"}, + with ( + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", + return_value=get_data, + ), + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_calibration", + return_value={"status": "success"}, + ), ): await hass.services.async_call( NUMBER_DOMAIN, diff --git a/tests/components/sensibo/test_select.py b/tests/components/sensibo/test_select.py index 41a67dfbe79..7a9c89ef612 100644 --- a/tests/components/sensibo/test_select.py +++ b/tests/components/sensibo/test_select.py @@ -1,4 +1,5 @@ """The test for the sensibo select platform.""" + from __future__ import annotations from datetime import timedelta @@ -83,14 +84,18 @@ async def test_select_set_option( state1 = hass.states.get("select.hallway_horizontal_swing") assert state1.state == "stopped" - with patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", - return_value=get_data, - ), patch( - "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", - return_value={"result": {"status": "failed"}}, - ), pytest.raises( - HomeAssistantError, + with ( + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", + return_value=get_data, + ), + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", + return_value={"result": {"status": "failed"}}, + ), + pytest.raises( + HomeAssistantError, + ), ): await hass.services.async_call( SELECT_DOMAIN, @@ -126,13 +131,19 @@ async def test_select_set_option( ) await hass.async_block_till_done() - with patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", - ), patch( - "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", - return_value={"result": {"status": "Failed", "failureReason": "No connection"}}, - ), pytest.raises( - HomeAssistantError, + with ( + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", + ), + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", + return_value={ + "result": {"status": "Failed", "failureReason": "No connection"} + }, + ), + pytest.raises( + HomeAssistantError, + ), ): await hass.services.async_call( SELECT_DOMAIN, @@ -145,12 +156,15 @@ async def test_select_set_option( state2 = hass.states.get("select.hallway_horizontal_swing") assert state2.state == "stopped" - with patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", - return_value=get_data, - ), patch( - "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", - return_value={"result": {"status": "Success"}}, + with ( + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", + return_value=get_data, + ), + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", + return_value={"result": {"status": "Success"}}, + ), ): await hass.services.async_call( SELECT_DOMAIN, diff --git a/tests/components/sensibo/test_sensor.py b/tests/components/sensibo/test_sensor.py index b3089c37e68..4e254568ac4 100644 --- a/tests/components/sensibo/test_sensor.py +++ b/tests/components/sensibo/test_sensor.py @@ -1,4 +1,5 @@ """The test for the sensibo select platform.""" + from __future__ import annotations from datetime import timedelta diff --git a/tests/components/sensibo/test_switch.py b/tests/components/sensibo/test_switch.py index e319be85c73..cc3c8881bec 100644 --- a/tests/components/sensibo/test_switch.py +++ b/tests/components/sensibo/test_switch.py @@ -1,4 +1,5 @@ """The test for the sensibo switch platform.""" + from __future__ import annotations from datetime import timedelta @@ -36,12 +37,15 @@ async def test_switch_timer( assert state1.attributes["id"] is None assert state1.attributes["turn_on"] is None - with patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", - return_value=get_data, - ), patch( - "homeassistant.components.sensibo.util.SensiboClient.async_set_timer", - return_value={"status": "success", "result": {"id": "SzTGE4oZ4D"}}, + with ( + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", + return_value=get_data, + ), + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_timer", + return_value={"status": "success", "result": {"id": "SzTGE4oZ4D"}}, + ), ): await hass.services.async_call( SWITCH_DOMAIN, @@ -70,12 +74,15 @@ async def test_switch_timer( assert state1.attributes["id"] == "SzTGE4oZ4D" assert state1.attributes["turn_on"] is False - with patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", - return_value=get_data, - ), patch( - "homeassistant.components.sensibo.util.SensiboClient.async_del_timer", - return_value={"status": "success", "result": {"id": "SzTGE4oZ4D"}}, + with ( + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", + return_value=get_data, + ), + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_del_timer", + return_value={"status": "success", "result": {"id": "SzTGE4oZ4D"}}, + ), ): await hass.services.async_call( SWITCH_DOMAIN, @@ -114,12 +121,15 @@ async def test_switch_pure_boost( state1 = hass.states.get("switch.kitchen_pure_boost") assert state1.state == STATE_OFF - with patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", - return_value=get_data, - ), patch( - "homeassistant.components.sensibo.util.SensiboClient.async_set_pureboost", - return_value={"status": "success"}, + with ( + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", + return_value=get_data, + ), + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_pureboost", + return_value={"status": "success"}, + ), ): await hass.services.async_call( SWITCH_DOMAIN, @@ -146,12 +156,15 @@ async def test_switch_pure_boost( state1 = hass.states.get("switch.kitchen_pure_boost") assert state1.state == STATE_ON - with patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", - return_value=get_data, - ), patch( - "homeassistant.components.sensibo.util.SensiboClient.async_set_pureboost", - return_value={"status": "success"}, + with ( + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", + return_value=get_data, + ), + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_pureboost", + return_value={"status": "success"}, + ), ): await hass.services.async_call( SWITCH_DOMAIN, @@ -189,14 +202,18 @@ async def test_switch_command_failure( state1 = hass.states.get("switch.hallway_timer") - with patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", - return_value=get_data, - ), patch( - "homeassistant.components.sensibo.util.SensiboClient.async_set_timer", - return_value={"status": "failure"}, - ), pytest.raises( - HomeAssistantError, + with ( + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", + return_value=get_data, + ), + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_timer", + return_value={"status": "failure"}, + ), + pytest.raises( + HomeAssistantError, + ), ): await hass.services.async_call( SWITCH_DOMAIN, @@ -207,14 +224,18 @@ async def test_switch_command_failure( blocking=True, ) - with patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", - return_value=get_data, - ), patch( - "homeassistant.components.sensibo.util.SensiboClient.async_del_timer", - return_value={"status": "failure"}, - ), pytest.raises( - HomeAssistantError, + with ( + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", + return_value=get_data, + ), + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_del_timer", + return_value={"status": "failure"}, + ), + pytest.raises( + HomeAssistantError, + ), ): await hass.services.async_call( SWITCH_DOMAIN, @@ -237,12 +258,15 @@ async def test_switch_climate_react( state1 = hass.states.get("switch.hallway_climate_react") assert state1.state == STATE_OFF - with patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", - return_value=get_data, - ), patch( - "homeassistant.components.sensibo.util.SensiboClient.async_enable_climate_react", - return_value={"status": "success"}, + with ( + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", + return_value=get_data, + ), + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_enable_climate_react", + return_value={"status": "success"}, + ), ): await hass.services.async_call( SWITCH_DOMAIN, @@ -268,12 +292,15 @@ async def test_switch_climate_react( state1 = hass.states.get("switch.hallway_climate_react") assert state1.state == STATE_ON - with patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", - return_value=get_data, - ), patch( - "homeassistant.components.sensibo.util.SensiboClient.async_enable_climate_react", - return_value={"status": "success"}, + with ( + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", + return_value=get_data, + ), + patch( + "homeassistant.components.sensibo.util.SensiboClient.async_enable_climate_react", + return_value={"status": "success"}, + ), ): await hass.services.async_call( SWITCH_DOMAIN, diff --git a/tests/components/sensibo/test_update.py b/tests/components/sensibo/test_update.py index 72e9ae9f902..23b2719d5b5 100644 --- a/tests/components/sensibo/test_update.py +++ b/tests/components/sensibo/test_update.py @@ -1,4 +1,5 @@ """The test for the sensibo update platform.""" + from __future__ import annotations from datetime import timedelta diff --git a/tests/components/sensirion_ble/fixtures.py b/tests/components/sensirion_ble/fixtures.py index c49ea3c1da2..96101a3e7b6 100644 --- a/tests/components/sensirion_ble/fixtures.py +++ b/tests/components/sensirion_ble/fixtures.py @@ -1,4 +1,5 @@ """Fixtures for testing Sensirion BLE.""" + from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo NOT_SENSIRION_SERVICE_INFO = BluetoothServiceInfo( diff --git a/tests/components/sensirion_ble/test_config_flow.py b/tests/components/sensirion_ble/test_config_flow.py index 542c49e285b..e93d060fd3e 100644 --- a/tests/components/sensirion_ble/test_config_flow.py +++ b/tests/components/sensirion_ble/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Sensirion config flow.""" + from unittest.mock import patch import pytest diff --git a/tests/components/sensor/__init__.py b/tests/components/sensor/__init__.py index 6563041ce99..58e0e8e4c7d 100644 --- a/tests/components/sensor/__init__.py +++ b/tests/components/sensor/__init__.py @@ -1,4 +1,5 @@ """The tests for Sensor platforms.""" + import pytest pytest.register_assert_rewrite("tests.components.recorder.common") diff --git a/tests/testing_config/custom_components/test/sensor.py b/tests/components/sensor/common.py similarity index 81% rename from tests/testing_config/custom_components/test/sensor.py rename to tests/components/sensor/common.py index d436a94e329..53a93b73da3 100644 --- a/tests/testing_config/custom_components/test/sensor.py +++ b/tests/components/sensor/common.py @@ -1,9 +1,6 @@ -"""Provide a mock sensor platform. +"""Common test utilities for sensor entity component tests.""" -Call init before using it in your tests to ensure clean test data. -""" from homeassistant.components.sensor import ( - DEVICE_CLASSES, RestoreSensor, SensorDeviceClass, SensorEntity, @@ -23,8 +20,6 @@ from homeassistant.const import ( from tests.common import MockEntity -DEVICE_CLASSES.append("none") - UNITS_OF_MEASUREMENT = { SensorDeviceClass.APPARENT_POWER: UnitOfApparentPower.VOLT_AMPERE, # apparent power (VA) SensorDeviceClass.BATTERY: PERCENTAGE, # % of battery that is left @@ -55,34 +50,6 @@ UNITS_OF_MEASUREMENT = { SensorDeviceClass.GAS: UnitOfVolume.CUBIC_METERS, # gas (m³) } -ENTITIES = {} - - -def init(empty=False): - """Initialize the platform with entities.""" - global ENTITIES - - ENTITIES = ( - {} - if empty - else { - device_class: MockSensor( - name=f"{device_class} sensor", - unique_id=f"unique_{device_class}", - device_class=device_class, - native_unit_of_measurement=UNITS_OF_MEASUREMENT.get(device_class), - ) - for device_class in DEVICE_CLASSES - } - ) - - -async def async_setup_platform( - hass, config, async_add_entities_callback, discovery_info=None -): - """Return mock entities.""" - async_add_entities_callback(list(ENTITIES.values())) - class MockSensor(MockEntity, SensorEntity): """Mock Sensor class.""" @@ -137,6 +104,19 @@ class MockRestoreSensor(MockSensor, RestoreSensor): if (last_sensor_data := await self.async_get_last_sensor_data()) is None: return self._values["native_value"] = last_sensor_data.native_value - self._values[ - "native_unit_of_measurement" - ] = last_sensor_data.native_unit_of_measurement + self._values["native_unit_of_measurement"] = ( + last_sensor_data.native_unit_of_measurement + ) + + +def get_mock_sensor_entities() -> dict[str, MockSensor]: + """Get mock sensor entities.""" + return { + device_class: MockSensor( + name=f"{device_class} sensor", + unique_id=f"unique_{device_class}", + device_class=device_class, + native_unit_of_measurement=UNITS_OF_MEASUREMENT.get(device_class), + ) + for device_class in SensorDeviceClass + } diff --git a/tests/components/sensor/test_device_condition.py b/tests/components/sensor/test_device_condition.py index e0a8bebf5fc..b633c744205 100644 --- a/tests/components/sensor/test_device_condition.py +++ b/tests/components/sensor/test_device_condition.py @@ -1,4 +1,5 @@ """The test for sensor device automation.""" + import pytest from pytest_unordered import unordered @@ -25,8 +26,9 @@ from tests.common import ( async_get_device_automation_capabilities, async_get_device_automations, async_mock_service, + setup_test_component_platform, ) -from tests.testing_config.custom_components.test.sensor import UNITS_OF_MEASUREMENT +from tests.components.sensor.common import UNITS_OF_MEASUREMENT, MockSensor @pytest.fixture(autouse=True, name="stub_blueprint_populate") @@ -84,11 +86,10 @@ async def test_get_conditions( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - enable_custom_integrations: None, + mock_sensor_entities: dict[str, MockSensor], ) -> None: """Test we get the expected conditions from a sensor.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() + setup_test_component_platform(hass, DOMAIN, mock_sensor_entities.values()) assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) await hass.async_block_till_done() sensor_entries = {} @@ -103,7 +104,7 @@ async def test_get_conditions( sensor_entries[device_class] = entity_registry.async_get_or_create( DOMAIN, "test", - platform.ENTITIES[device_class].unique_id, + mock_sensor_entities[device_class].unique_id, device_id=device_entry.id, ) @@ -130,12 +131,12 @@ async def test_get_conditions( @pytest.mark.parametrize( ("hidden_by", "entity_category"), - ( + [ (RegistryEntryHider.INTEGRATION, None), (RegistryEntryHider.USER, None), (None, EntityCategory.CONFIG), (None, EntityCategory.DIAGNOSTIC), - ), + ], ) async def test_get_conditions_hidden_auxiliary( hass: HomeAssistant, @@ -224,13 +225,13 @@ async def test_get_conditions_no_state( @pytest.mark.parametrize( ("state_class", "unit", "condition_types"), - ( + [ (SensorStateClass.MEASUREMENT, None, ["is_value"]), (SensorStateClass.TOTAL, None, ["is_value"]), (SensorStateClass.TOTAL_INCREASING, None, ["is_value"]), (SensorStateClass.MEASUREMENT, "dogs", ["is_value"]), (None, None, []), - ), + ], ) async def test_get_conditions_no_unit_or_stateclass( hass: HomeAssistant, @@ -283,6 +284,7 @@ async def test_get_condition_capabilities( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, + mock_sensor_entities: dict[str, MockSensor], set_state, device_class_reg, device_class_state, @@ -290,8 +292,7 @@ async def test_get_condition_capabilities( unit_state, ) -> None: """Test we get the expected capabilities from a sensor condition.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() + setup_test_component_platform(hass, DOMAIN, mock_sensor_entities.values()) config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) @@ -302,7 +303,7 @@ async def test_get_condition_capabilities( entity_id = entity_registry.async_get_or_create( DOMAIN, "test", - platform.ENTITIES["battery"].unique_id, + mock_sensor_entities["battery"].unique_id, device_id=device_entry.id, original_device_class=device_class_reg, unit_of_measurement=unit_reg, @@ -352,6 +353,7 @@ async def test_get_condition_capabilities_legacy( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, + mock_sensor_entities: dict[str, MockSensor], set_state, device_class_reg, device_class_state, @@ -359,8 +361,7 @@ async def test_get_condition_capabilities_legacy( unit_state, ) -> None: """Test we get the expected capabilities from a sensor condition.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() + setup_test_component_platform(hass, DOMAIN, mock_sensor_entities.values()) config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) @@ -371,7 +372,7 @@ async def test_get_condition_capabilities_legacy( entity_id = entity_registry.async_get_or_create( DOMAIN, "test", - platform.ENTITIES["battery"].unique_id, + mock_sensor_entities["battery"].unique_id, device_id=device_entry.id, original_device_class=device_class_reg, unit_of_measurement=unit_reg, @@ -416,11 +417,13 @@ async def test_get_condition_capabilities_legacy( async def test_get_condition_capabilities_none( hass: HomeAssistant, entity_registry: er.EntityRegistry, - enable_custom_integrations: None, ) -> None: """Test we get the expected capabilities from a sensor condition.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() + entity = MockSensor( + name="none sensor", + unique_id="unique_none", + ) + setup_test_component_platform(hass, DOMAIN, [entity]) config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) @@ -428,7 +431,7 @@ async def test_get_condition_capabilities_none( entry_none = entity_registry.async_get_or_create( DOMAIN, "test", - platform.ENTITIES["none"].unique_id, + entity.unique_id, ) assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) diff --git a/tests/components/sensor/test_device_trigger.py b/tests/components/sensor/test_device_trigger.py index bbc59cca322..98bea960fcc 100644 --- a/tests/components/sensor/test_device_trigger.py +++ b/tests/components/sensor/test_device_trigger.py @@ -1,4 +1,5 @@ """The test for sensor device automation.""" + from datetime import timedelta import pytest @@ -29,8 +30,9 @@ from tests.common import ( async_get_device_automation_capabilities, async_get_device_automations, async_mock_service, + setup_test_component_platform, ) -from tests.testing_config.custom_components.test.sensor import UNITS_OF_MEASUREMENT +from tests.components.sensor.common import UNITS_OF_MEASUREMENT, MockSensor @pytest.fixture(autouse=True, name="stub_blueprint_populate") @@ -86,11 +88,10 @@ async def test_get_triggers( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - enable_custom_integrations: None, + mock_sensor_entities: dict[str, MockSensor], ) -> None: """Test we get the expected triggers from a sensor.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() + setup_test_component_platform(hass, DOMAIN, mock_sensor_entities.values()) assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) await hass.async_block_till_done() sensor_entries: dict[SensorDeviceClass, er.RegistryEntry] = {} @@ -105,7 +106,7 @@ async def test_get_triggers( sensor_entries[device_class] = entity_registry.async_get_or_create( DOMAIN, "test", - platform.ENTITIES[device_class].unique_id, + mock_sensor_entities[device_class].unique_id, device_id=device_entry.id, ) @@ -132,12 +133,12 @@ async def test_get_triggers( @pytest.mark.parametrize( ("hidden_by", "entity_category"), - ( + [ (RegistryEntryHider.INTEGRATION, None), (RegistryEntryHider.USER, None), (None, EntityCategory.CONFIG), (None, EntityCategory.DIAGNOSTIC), - ), + ], ) async def test_get_triggers_hidden_auxiliary( hass: HomeAssistant, @@ -181,13 +182,13 @@ async def test_get_triggers_hidden_auxiliary( @pytest.mark.parametrize( ("state_class", "unit", "trigger_types"), - ( + [ (SensorStateClass.MEASUREMENT, None, ["value"]), (SensorStateClass.TOTAL, None, ["value"]), (SensorStateClass.TOTAL_INCREASING, None, ["value"]), (SensorStateClass.MEASUREMENT, "dogs", ["value"]), (None, None, []), - ), + ], ) async def test_get_triggers_no_unit_or_stateclass( hass: HomeAssistant, @@ -240,6 +241,7 @@ async def test_get_trigger_capabilities( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, + mock_sensor_entities: dict[str, MockSensor], set_state, device_class_reg, device_class_state, @@ -247,8 +249,7 @@ async def test_get_trigger_capabilities( unit_state, ) -> None: """Test we get the expected capabilities from a sensor trigger.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() + setup_test_component_platform(hass, DOMAIN, mock_sensor_entities) config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) @@ -259,7 +260,7 @@ async def test_get_trigger_capabilities( entity_id = entity_registry.async_get_or_create( DOMAIN, "test", - platform.ENTITIES["battery"].unique_id, + mock_sensor_entities["battery"].unique_id, device_id=device_entry.id, original_device_class=device_class_reg, unit_of_measurement=unit_reg, @@ -310,6 +311,7 @@ async def test_get_trigger_capabilities_legacy( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, + mock_sensor_entities: dict[str, MockSensor], set_state, device_class_reg, device_class_state, @@ -317,8 +319,7 @@ async def test_get_trigger_capabilities_legacy( unit_state, ) -> None: """Test we get the expected capabilities from a sensor trigger.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() + setup_test_component_platform(hass, DOMAIN, mock_sensor_entities) config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) @@ -329,7 +330,7 @@ async def test_get_trigger_capabilities_legacy( entity_id = entity_registry.async_get_or_create( DOMAIN, "test", - platform.ENTITIES["battery"].unique_id, + mock_sensor_entities["battery"].unique_id, device_id=device_entry.id, original_device_class=device_class_reg, unit_of_measurement=unit_reg, @@ -373,11 +374,13 @@ async def test_get_trigger_capabilities_legacy( async def test_get_trigger_capabilities_none( hass: HomeAssistant, entity_registry: er.EntityRegistry, - enable_custom_integrations: None, ) -> None: """Test we get the expected capabilities from a sensor trigger.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() + entity = MockSensor( + name="none sensor", + unique_id="unique_none", + ) + setup_test_component_platform(hass, DOMAIN, [entity]) config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) @@ -385,7 +388,7 @@ async def test_get_trigger_capabilities_none( entry_none = entity_registry.async_get_or_create( DOMAIN, "test", - platform.ENTITIES["none"].unique_id, + entity.unique_id, ) assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) diff --git a/tests/components/sensor/test_helpers.py b/tests/components/sensor/test_helpers.py index f103a762842..e197579fa66 100644 --- a/tests/components/sensor/test_helpers.py +++ b/tests/components/sensor/test_helpers.py @@ -1,4 +1,5 @@ """The test for sensor helpers.""" + import pytest from homeassistant.components.sensor import SensorDeviceClass diff --git a/tests/components/sensor/test_init.py b/tests/components/sensor/test_init.py index 52e1851833e..0ecb4b9c60f 100644 --- a/tests/components/sensor/test_init.py +++ b/tests/components/sensor/test_init.py @@ -1,4 +1,5 @@ """The test for sensor entity.""" + from __future__ import annotations from collections.abc import Generator @@ -62,7 +63,9 @@ from tests.common import ( mock_integration, mock_platform, mock_restore_cache_with_extra_data, + setup_test_component_platform, ) +from tests.components.sensor.common import MockRestoreSensor, MockSensor TEST_DOMAIN = "test" @@ -102,7 +105,6 @@ TEST_DOMAIN = "test" ) async def test_temperature_conversion( hass: HomeAssistant, - enable_custom_integrations: None, unit_system, native_unit, state_unit, @@ -111,16 +113,14 @@ async def test_temperature_conversion( ) -> None: """Test temperature conversion.""" hass.config.units = unit_system - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - platform.ENTITIES["0"] = platform.MockSensor( + entity0 = MockSensor( name="Test", native_value=str(native_value), native_unit_of_measurement=native_unit, device_class=SensorDeviceClass.TEMPERATURE, ) + setup_test_component_platform(hass, sensor.DOMAIN, [entity0]) - entity0 = platform.ENTITIES["0"] assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() @@ -129,21 +129,19 @@ async def test_temperature_conversion( assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == state_unit -@pytest.mark.parametrize("device_class", (None, SensorDeviceClass.PRESSURE)) +@pytest.mark.parametrize("device_class", [None, SensorDeviceClass.PRESSURE]) async def test_temperature_conversion_wrong_device_class( - hass: HomeAssistant, device_class, enable_custom_integrations: None + hass: HomeAssistant, device_class ) -> None: """Test temperatures are not converted if the sensor has wrong device class.""" - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - platform.ENTITIES["0"] = platform.MockSensor( + entity0 = MockSensor( name="Test", native_value="0.0", native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, device_class=device_class, ) + setup_test_component_platform(hass, sensor.DOMAIN, [entity0]) - entity0 = platform.ENTITIES["0"] assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() @@ -153,25 +151,23 @@ async def test_temperature_conversion_wrong_device_class( assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfTemperature.FAHRENHEIT -@pytest.mark.parametrize("state_class", ("measurement", "total_increasing")) +@pytest.mark.parametrize("state_class", ["measurement", "total_increasing"]) async def test_deprecated_last_reset( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, - enable_custom_integrations: None, state_class, ) -> None: """Test warning on deprecated last reset.""" - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - platform.ENTITIES["0"] = platform.MockSensor( + entity0 = MockSensor( name="Test", state_class=state_class, last_reset=dt_util.utc_from_timestamp(0) ) + setup_test_component_platform(hass, sensor.DOMAIN, [entity0]) assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() assert ( - "Entity sensor.test () " + "Entity sensor.test () " 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." @@ -184,7 +180,6 @@ async def test_deprecated_last_reset( async def test_datetime_conversion( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, - enable_custom_integrations: None, ) -> None: """Test conversion of datetime.""" test_timestamp = datetime(2017, 12, 19, 18, 29, 42, tzinfo=UTC) @@ -192,51 +187,49 @@ async def test_datetime_conversion( dt_util.get_time_zone("Europe/Amsterdam") ) test_date = date(2017, 12, 19) - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - platform.ENTITIES["0"] = platform.MockSensor( - name="Test", - native_value=test_timestamp, - device_class=SensorDeviceClass.TIMESTAMP, - ) - platform.ENTITIES["1"] = platform.MockSensor( - name="Test", native_value=test_date, device_class=SensorDeviceClass.DATE - ) - platform.ENTITIES["2"] = platform.MockSensor( - name="Test", native_value=None, device_class=SensorDeviceClass.TIMESTAMP - ) - platform.ENTITIES["3"] = platform.MockSensor( - name="Test", native_value=None, device_class=SensorDeviceClass.DATE - ) - platform.ENTITIES["4"] = platform.MockSensor( - name="Test", - native_value=test_local_timestamp, - device_class=SensorDeviceClass.TIMESTAMP, - ) + entities = [ + MockSensor( + name="Test", + native_value=test_timestamp, + device_class=SensorDeviceClass.TIMESTAMP, + ), + MockSensor( + name="Test", native_value=test_date, device_class=SensorDeviceClass.DATE + ), + MockSensor( + name="Test", native_value=None, device_class=SensorDeviceClass.TIMESTAMP + ), + MockSensor(name="Test", native_value=None, device_class=SensorDeviceClass.DATE), + MockSensor( + name="Test", + native_value=test_local_timestamp, + device_class=SensorDeviceClass.TIMESTAMP, + ), + ] + setup_test_component_platform(hass, sensor.DOMAIN, entities) assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() - state = hass.states.get(platform.ENTITIES["0"].entity_id) + state = hass.states.get(entities[0].entity_id) assert state.state == test_timestamp.isoformat() - state = hass.states.get(platform.ENTITIES["1"].entity_id) + state = hass.states.get(entities[1].entity_id) assert state.state == test_date.isoformat() - state = hass.states.get(platform.ENTITIES["2"].entity_id) + state = hass.states.get(entities[2].entity_id) assert state.state == STATE_UNKNOWN - state = hass.states.get(platform.ENTITIES["3"].entity_id) + state = hass.states.get(entities[3].entity_id) assert state.state == STATE_UNKNOWN - state = hass.states.get(platform.ENTITIES["4"].entity_id) + state = hass.states.get(entities[4].entity_id) assert state.state == test_timestamp.isoformat() async def test_a_sensor_with_a_non_numeric_device_class( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, - enable_custom_integrations: None, ) -> None: """Test that a sensor with a non numeric device class will be non numeric. @@ -248,29 +241,29 @@ async def test_a_sensor_with_a_non_numeric_device_class( dt_util.get_time_zone("Europe/Amsterdam") ) - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - platform.ENTITIES["0"] = platform.MockSensor( - name="Test", - native_value=test_local_timestamp, - native_unit_of_measurement="", - device_class=SensorDeviceClass.TIMESTAMP, - ) - - platform.ENTITIES["1"] = platform.MockSensor( - name="Test", - native_value=test_local_timestamp, - state_class="", - device_class=SensorDeviceClass.TIMESTAMP, - ) + entities = [ + MockSensor( + name="Test", + native_value=test_local_timestamp, + native_unit_of_measurement="", + device_class=SensorDeviceClass.TIMESTAMP, + ), + MockSensor( + name="Test", + native_value=test_local_timestamp, + state_class="", + device_class=SensorDeviceClass.TIMESTAMP, + ), + ] + setup_test_component_platform(hass, sensor.DOMAIN, entities) assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() - state = hass.states.get(platform.ENTITIES["0"].entity_id) + state = hass.states.get(entities[0].entity_id) assert state.state == test_timestamp.isoformat() - state = hass.states.get(platform.ENTITIES["1"].entity_id) + state = hass.states.get(entities[1].entity_id) assert state.state == test_timestamp.isoformat() @@ -284,17 +277,15 @@ async def test_a_sensor_with_a_non_numeric_device_class( async def test_deprecated_datetime_str( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, - enable_custom_integrations: None, device_class, state_value, provides, ) -> None: """Test warning on deprecated str for a date(time) value.""" - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - platform.ENTITIES["0"] = platform.MockSensor( + entity0 = MockSensor( name="Test", native_value=state_value, device_class=device_class ) + setup_test_component_platform(hass, sensor.DOMAIN, [entity0]) assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() @@ -308,17 +299,15 @@ async def test_deprecated_datetime_str( async def test_reject_timezoneless_datetime_str( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, - enable_custom_integrations: None, ) -> None: """Test rejection of timezone-less datetime objects as timestamp.""" test_timestamp = datetime(2017, 12, 19, 18, 29, 42, tzinfo=None) - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - platform.ENTITIES["0"] = platform.MockSensor( + entity0 = MockSensor( name="Test", native_value=test_timestamp, device_class=SensorDeviceClass.TIMESTAMP, ) + setup_test_component_platform(hass, sensor.DOMAIN, [entity0]) assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() @@ -402,7 +391,6 @@ RESTORE_DATA = { ) async def test_restore_sensor_save_state( hass: HomeAssistant, - enable_custom_integrations: None, hass_storage: dict[str, Any], native_value, native_value_type, @@ -411,16 +399,14 @@ async def test_restore_sensor_save_state( uom, ) -> None: """Test RestoreSensor.""" - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - platform.ENTITIES["0"] = platform.MockRestoreSensor( + entity0 = MockRestoreSensor( name="Test", native_value=native_value, native_unit_of_measurement=uom, device_class=device_class, ) + setup_test_component_platform(hass, sensor.DOMAIN, [entity0]) - entity0 = platform.ENTITIES["0"] assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() @@ -471,7 +457,6 @@ async def test_restore_sensor_save_state( ) async def test_restore_sensor_restore_state( hass: HomeAssistant, - enable_custom_integrations: None, hass_storage: dict[str, Any], native_value, native_value_type, @@ -482,14 +467,12 @@ async def test_restore_sensor_restore_state( """Test RestoreSensor.""" mock_restore_cache_with_extra_data(hass, ((State("sensor.test", ""), extra_data),)) - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - platform.ENTITIES["0"] = platform.MockRestoreSensor( + entity0 = MockRestoreSensor( name="Test", device_class=device_class, ) + setup_test_component_platform(hass, sensor.DOMAIN, [entity0]) - entity0 = platform.ENTITIES["0"] assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() @@ -620,7 +603,6 @@ async def test_restore_sensor_restore_state( ) async def test_custom_unit( hass: HomeAssistant, - enable_custom_integrations: None, device_class, native_unit, custom_unit, @@ -637,17 +619,15 @@ async def test_custom_unit( ) await hass.async_block_till_done() - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - platform.ENTITIES["0"] = platform.MockSensor( + entity0 = MockSensor( name="Test", native_value=str(native_value), native_unit_of_measurement=native_unit, device_class=device_class, unique_id="very_unique", ) + setup_test_component_platform(hass, sensor.DOMAIN, [entity0]) - entity0 = platform.ENTITIES["0"] assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() @@ -883,7 +863,6 @@ async def test_custom_unit( ) async def test_custom_unit_change( hass: HomeAssistant, - enable_custom_integrations: None, native_unit, custom_unit, state_unit, @@ -894,17 +873,15 @@ async def test_custom_unit_change( ) -> None: """Test custom unit changes are picked up.""" entity_registry = er.async_get(hass) - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - platform.ENTITIES["0"] = platform.MockSensor( + entity0 = MockSensor( name="Test", native_value=str(native_value), native_unit_of_measurement=native_unit, device_class=device_class, unique_id="very_unique", ) + setup_test_component_platform(hass, sensor.DOMAIN, [entity0]) - entity0 = platform.ENTITIES["0"] assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() @@ -971,7 +948,6 @@ async def test_custom_unit_change( ) async def test_unit_conversion_priority( hass: HomeAssistant, - enable_custom_integrations: None, unit_system, native_unit, automatic_unit, @@ -989,27 +965,21 @@ async def test_unit_conversion_priority( hass.config.units = unit_system entity_registry = er.async_get(hass) - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - platform.ENTITIES["0"] = platform.MockSensor( + entity0 = MockSensor( name="Test", device_class=device_class, native_unit_of_measurement=native_unit, native_value=str(native_value), unique_id="very_unique", ) - entity0 = platform.ENTITIES["0"] - - platform.ENTITIES["1"] = platform.MockSensor( + entity1 = MockSensor( name="Test", device_class=device_class, native_unit_of_measurement=native_unit, native_value=str(native_value), ) - entity1 = platform.ENTITIES["1"] - - platform.ENTITIES["2"] = platform.MockSensor( + entity2 = MockSensor( name="Test", device_class=device_class, native_unit_of_measurement=native_unit, @@ -1017,16 +987,23 @@ async def test_unit_conversion_priority( suggested_unit_of_measurement=suggested_unit, unique_id="very_unique_2", ) - entity2 = platform.ENTITIES["2"] - - platform.ENTITIES["3"] = platform.MockSensor( + entity3 = MockSensor( name="Test", device_class=device_class, native_unit_of_measurement=native_unit, native_value=str(native_value), suggested_unit_of_measurement=suggested_unit, ) - entity3 = platform.ENTITIES["3"] + setup_test_component_platform( + hass, + sensor.DOMAIN, + [ + entity0, + entity1, + entity2, + entity3, + ], + ) assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() @@ -1118,7 +1095,6 @@ async def test_unit_conversion_priority( ) async def test_unit_conversion_priority_precision( hass: HomeAssistant, - enable_custom_integrations: None, unit_system, native_unit, automatic_unit, @@ -1137,10 +1113,8 @@ async def test_unit_conversion_priority_precision( hass.config.units = unit_system entity_registry = er.async_get(hass) - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - platform.ENTITIES["0"] = platform.MockSensor( + entity0 = MockSensor( name="Test", device_class=device_class, native_unit_of_measurement=native_unit, @@ -1148,18 +1122,14 @@ async def test_unit_conversion_priority_precision( suggested_display_precision=suggested_precision, unique_id="very_unique", ) - entity0 = platform.ENTITIES["0"] - - platform.ENTITIES["1"] = platform.MockSensor( + entity1 = MockSensor( name="Test", device_class=device_class, native_unit_of_measurement=native_unit, native_value=str(native_value), suggested_display_precision=suggested_precision, ) - entity1 = platform.ENTITIES["1"] - - platform.ENTITIES["2"] = platform.MockSensor( + entity2 = MockSensor( name="Test", device_class=device_class, native_unit_of_measurement=native_unit, @@ -1168,9 +1138,7 @@ async def test_unit_conversion_priority_precision( suggested_unit_of_measurement=suggested_unit, unique_id="very_unique_2", ) - entity2 = platform.ENTITIES["2"] - - platform.ENTITIES["3"] = platform.MockSensor( + entity3 = MockSensor( name="Test", device_class=device_class, native_unit_of_measurement=native_unit, @@ -1178,7 +1146,16 @@ async def test_unit_conversion_priority_precision( suggested_display_precision=suggested_precision, suggested_unit_of_measurement=suggested_unit, ) - entity3 = platform.ENTITIES["3"] + setup_test_component_platform( + hass, + sensor.DOMAIN, + [ + entity0, + entity1, + entity2, + entity3, + ], + ) assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() @@ -1279,7 +1256,6 @@ async def test_unit_conversion_priority_precision( ) async def test_unit_conversion_priority_suggested_unit_change( hass: HomeAssistant, - enable_custom_integrations: None, unit_system, native_unit, original_unit, @@ -1293,8 +1269,6 @@ async def test_unit_conversion_priority_suggested_unit_change( hass.config.units = unit_system entity_registry = er.async_get(hass) - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) # Pre-register entities entry = entity_registry.async_get_or_create( @@ -1314,16 +1288,14 @@ async def test_unit_conversion_priority_suggested_unit_change( {"suggested_unit_of_measurement": original_unit}, ) - platform.ENTITIES["0"] = platform.MockSensor( + entity0 = MockSensor( name="Test", device_class=device_class, native_unit_of_measurement=native_unit, native_value=str(native_value), unique_id="very_unique", ) - entity0 = platform.ENTITIES["0"] - - platform.ENTITIES["1"] = platform.MockSensor( + entity1 = MockSensor( name="Test", device_class=device_class, native_unit_of_measurement=native_unit, @@ -1331,7 +1303,7 @@ async def test_unit_conversion_priority_suggested_unit_change( suggested_unit_of_measurement=suggested_unit, unique_id="very_unique_2", ) - entity1 = platform.ENTITIES["1"] + setup_test_component_platform(hass, sensor.DOMAIN, [entity0, entity1]) assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() @@ -1391,7 +1363,6 @@ async def test_unit_conversion_priority_suggested_unit_change( ) async def test_unit_conversion_priority_suggested_unit_change_2( hass: HomeAssistant, - enable_custom_integrations: None, native_unit_1, native_unit_2, suggested_unit, @@ -1404,8 +1375,6 @@ async def test_unit_conversion_priority_suggested_unit_change_2( hass.config.units = METRIC_SYSTEM entity_registry = er.async_get(hass) - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) # Pre-register entities entity_registry.async_get_or_create( @@ -1415,16 +1384,14 @@ async def test_unit_conversion_priority_suggested_unit_change_2( "sensor", "test", "very_unique_2", unit_of_measurement=native_unit_1 ) - platform.ENTITIES["0"] = platform.MockSensor( + entity0 = MockSensor( name="Test", device_class=device_class, native_unit_of_measurement=native_unit_2, native_value=str(native_value), unique_id="very_unique", ) - entity0 = platform.ENTITIES["0"] - - platform.ENTITIES["1"] = platform.MockSensor( + entity1 = MockSensor( name="Test", device_class=device_class, native_unit_of_measurement=native_unit_2, @@ -1432,7 +1399,7 @@ async def test_unit_conversion_priority_suggested_unit_change_2( suggested_unit_of_measurement=suggested_unit, unique_id="very_unique_2", ) - entity1 = platform.ENTITIES["1"] + setup_test_component_platform(hass, sensor.DOMAIN, [entity0, entity1]) assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() @@ -1495,7 +1462,6 @@ async def test_unit_conversion_priority_suggested_unit_change_2( ) async def test_suggested_precision_option( hass: HomeAssistant, - enable_custom_integrations: None, unit_system, native_unit, integration_suggested_precision, @@ -1509,10 +1475,7 @@ async def test_suggested_precision_option( hass.config.units = unit_system entity_registry = er.async_get(hass) - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - - platform.ENTITIES["0"] = platform.MockSensor( + entity0 = MockSensor( name="Test", device_class=device_class, native_unit_of_measurement=native_unit, @@ -1520,7 +1483,7 @@ async def test_suggested_precision_option( suggested_display_precision=integration_suggested_precision, unique_id="very_unique", ) - entity0 = platform.ENTITIES["0"] + setup_test_component_platform(hass, sensor.DOMAIN, [entity0]) assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() @@ -1573,7 +1536,6 @@ async def test_suggested_precision_option( ) async def test_suggested_precision_option_update( hass: HomeAssistant, - enable_custom_integrations: None, unit_system, native_unit, suggested_unit, @@ -1589,8 +1551,6 @@ async def test_suggested_precision_option_update( hass.config.units = unit_system entity_registry = er.async_get(hass) - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) # Pre-register entities entry = entity_registry.async_get_or_create("sensor", "test", "very_unique") @@ -1609,7 +1569,7 @@ async def test_suggested_precision_option_update( }, ) - platform.ENTITIES["0"] = platform.MockSensor( + entity0 = MockSensor( name="Test", device_class=device_class, native_unit_of_measurement=native_unit, @@ -1617,7 +1577,7 @@ async def test_suggested_precision_option_update( suggested_display_precision=new_precision, unique_id="very_unique", ) - entity0 = platform.ENTITIES["0"] + setup_test_component_platform(hass, sensor.DOMAIN, [entity0]) assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() @@ -1665,7 +1625,6 @@ async def test_suggested_precision_option_update( ) async def test_unit_conversion_priority_legacy_conversion_removed( hass: HomeAssistant, - enable_custom_integrations: None, unit_system, native_unit, original_unit, @@ -1678,22 +1637,20 @@ async def test_unit_conversion_priority_legacy_conversion_removed( hass.config.units = unit_system entity_registry = er.async_get(hass) - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) # Pre-register entities entity_registry.async_get_or_create( "sensor", "test", "very_unique", unit_of_measurement=original_unit ) - platform.ENTITIES["0"] = platform.MockSensor( + entity0 = MockSensor( name="Test", device_class=device_class, native_unit_of_measurement=native_unit, native_value=str(native_value), unique_id="very_unique", ) - entity0 = platform.ENTITIES["0"] + setup_test_component_platform(hass, sensor.DOMAIN, [entity0]) assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() @@ -1714,17 +1671,15 @@ def test_device_classes_aligned() -> None: async def test_value_unknown_in_enumeration( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, - enable_custom_integrations: None, ) -> None: """Test warning on invalid enum value.""" - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - platform.ENTITIES["0"] = platform.MockSensor( + entity0 = MockSensor( name="Test", native_value="invalid_option", device_class=SensorDeviceClass.ENUM, options=["option1", "option2"], ) + setup_test_component_platform(hass, sensor.DOMAIN, [entity0]) assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() @@ -1738,17 +1693,15 @@ async def test_value_unknown_in_enumeration( async def test_invalid_enumeration_entity_with_device_class( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, - enable_custom_integrations: None, ) -> None: """Test warning on entities that provide an enum with a device class.""" - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - platform.ENTITIES["0"] = platform.MockSensor( + entity0 = MockSensor( name="Test", native_value=21, device_class=SensorDeviceClass.POWER, options=["option1", "option2"], ) + setup_test_component_platform(hass, sensor.DOMAIN, [entity0]) assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() @@ -1762,16 +1715,14 @@ async def test_invalid_enumeration_entity_with_device_class( async def test_invalid_enumeration_entity_without_device_class( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, - enable_custom_integrations: None, ) -> None: """Test warning on entities that provide an enum without a device class.""" - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - platform.ENTITIES["0"] = platform.MockSensor( + entity0 = MockSensor( name="Test", native_value=21, options=["option1", "option2"], ) + setup_test_component_platform(hass, sensor.DOMAIN, [entity0]) assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() @@ -1784,28 +1735,26 @@ async def test_invalid_enumeration_entity_without_device_class( @pytest.mark.parametrize( "device_class", - ( + [ SensorDeviceClass.DATE, SensorDeviceClass.ENUM, SensorDeviceClass.TIMESTAMP, - ), + ], ) async def test_non_numeric_device_class_with_unit_of_measurement( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, - enable_custom_integrations: None, device_class: SensorDeviceClass, ) -> None: """Test error on numeric entities that provide an unit of measurement.""" - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - platform.ENTITIES["0"] = platform.MockSensor( + entity0 = MockSensor( name="Test", native_value=None, device_class=device_class, native_unit_of_measurement=UnitOfTemperature.CELSIUS, options=["option1", "option2"], ) + setup_test_component_platform(hass, sensor.DOMAIN, [entity0]) assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() @@ -1818,7 +1767,7 @@ async def test_non_numeric_device_class_with_unit_of_measurement( @pytest.mark.parametrize( "device_class", - ( + [ SensorDeviceClass.APPARENT_POWER, SensorDeviceClass.AQI, SensorDeviceClass.ATMOSPHERIC_PRESSURE, @@ -1863,23 +1812,21 @@ async def test_non_numeric_device_class_with_unit_of_measurement( SensorDeviceClass.WATER, SensorDeviceClass.WEIGHT, SensorDeviceClass.WIND_SPEED, - ), + ], ) async def test_device_classes_with_invalid_unit_of_measurement( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, - enable_custom_integrations: None, device_class: SensorDeviceClass, ) -> None: """Test error when unit of measurement is not valid for used device class.""" - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - platform.ENTITIES["0"] = platform.MockSensor( + entity0 = MockSensor( name="Test", native_value="1.0", device_class=device_class, native_unit_of_measurement="INVALID!", ) + setup_test_component_platform(hass, sensor.DOMAIN, [entity0]) units = [ str(unit) if unit else "no unit of measurement" for unit in DEVICE_CLASS_UNITS.get(device_class, set()) @@ -1919,7 +1866,6 @@ async def test_device_classes_with_invalid_unit_of_measurement( async def test_non_numeric_validation_error( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, - enable_custom_integrations: None, native_value: Any, problem: str, device_class: SensorDeviceClass | None, @@ -1927,16 +1873,14 @@ async def test_non_numeric_validation_error( unit: str | None, ) -> None: """Test error on expected numeric entities.""" - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - platform.ENTITIES["0"] = platform.MockSensor( + entity0 = MockSensor( name="Test", native_value=native_value, device_class=device_class, native_unit_of_measurement=unit, state_class=state_class, ) - entity0 = platform.ENTITIES["0"] + setup_test_component_platform(hass, sensor.DOMAIN, [entity0]) assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() @@ -1951,7 +1895,7 @@ async def test_non_numeric_validation_error( @pytest.mark.parametrize( - ("device_class", "state_class", "unit", "precision"), ((None, None, None, 1),) + ("device_class", "state_class", "unit", "precision"), [(None, None, None, 1)] ) @pytest.mark.parametrize( ("native_value", "expected"), @@ -1965,7 +1909,6 @@ async def test_non_numeric_validation_error( async def test_non_numeric_validation_raise( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, - enable_custom_integrations: None, native_value: Any, expected: str, device_class: SensorDeviceClass | None, @@ -1974,9 +1917,7 @@ async def test_non_numeric_validation_raise( precision, ) -> None: """Test error on expected numeric entities.""" - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - platform.ENTITIES["0"] = platform.MockSensor( + entity0 = MockSensor( name="Test", device_class=device_class, native_unit_of_measurement=unit, @@ -1984,7 +1925,7 @@ async def test_non_numeric_validation_raise( state_class=state_class, suggested_display_precision=precision, ) - entity0 = platform.ENTITIES["0"] + setup_test_component_platform(hass, sensor.DOMAIN, [entity0]) assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() @@ -2017,7 +1958,6 @@ async def test_non_numeric_validation_raise( async def test_numeric_validation( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, - enable_custom_integrations: None, native_value: Any, expected: str, device_class: SensorDeviceClass | None, @@ -2025,16 +1965,14 @@ async def test_numeric_validation( unit: str | None, ) -> None: """Test does not error on expected numeric entities.""" - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - platform.ENTITIES["0"] = platform.MockSensor( + entity0 = MockSensor( name="Test", native_value=native_value, device_class=device_class, native_unit_of_measurement=unit, state_class=state_class, ) - entity0 = platform.ENTITIES["0"] + setup_test_component_platform(hass, sensor.DOMAIN, [entity0]) assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() @@ -2051,18 +1989,15 @@ async def test_numeric_validation( async def test_numeric_validation_ignores_custom_device_class( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, - enable_custom_integrations: None, ) -> None: """Test does not error on expected numeric entities.""" native_value = "Three elephants" - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - platform.ENTITIES["0"] = platform.MockSensor( + entity0 = MockSensor( name="Test", native_value=native_value, device_class="custom__deviceclass", ) - entity0 = platform.ENTITIES["0"] + setup_test_component_platform(hass, sensor.DOMAIN, [entity0]) assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() @@ -2083,18 +2018,16 @@ async def test_numeric_validation_ignores_custom_device_class( async def test_device_classes_with_invalid_state_class( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, - enable_custom_integrations: None, device_class: SensorDeviceClass, ) -> None: """Test error when unit of measurement is not valid for used device class.""" - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - platform.ENTITIES["0"] = platform.MockSensor( + entity0 = MockSensor( name="Test", native_value=None, state_class="INVALID!", device_class=device_class, ) + setup_test_component_platform(hass, sensor.DOMAIN, [entity0]) assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() @@ -2132,7 +2065,6 @@ async def test_device_classes_with_invalid_state_class( async def test_numeric_state_expected_helper( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, - enable_custom_integrations: None, device_class: SensorDeviceClass | None, state_class: SensorStateClass | None, native_unit_of_measurement: str | None, @@ -2140,9 +2072,7 @@ async def test_numeric_state_expected_helper( is_numeric: bool, ) -> None: """Test numeric_state_expected helper.""" - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - platform.ENTITIES["0"] = platform.MockSensor( + entity0 = MockSensor( name="Test", native_value=None, device_class=device_class, @@ -2150,11 +2080,11 @@ async def test_numeric_state_expected_helper( native_unit_of_measurement=native_unit_of_measurement, suggested_display_precision=suggested_precision, ) + setup_test_component_platform(hass, sensor.DOMAIN, [entity0]) assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() - entity0 = platform.ENTITIES["0"] state = hass.states.get(entity0.entity_id) assert state is not None @@ -2198,7 +2128,6 @@ async def test_numeric_state_expected_helper( ) async def test_unit_conversion_update( hass: HomeAssistant, - enable_custom_integrations: None, unit_system_1, unit_system_2, native_unit, @@ -2218,9 +2147,8 @@ async def test_unit_conversion_update( hass.config.units = unit_system_1 entity_registry = er.async_get(hass) - platform = getattr(hass.components, "test.sensor") - entity0 = platform.MockSensor( + entity0 = MockSensor( name="Test 0", device_class=device_class, native_unit_of_measurement=native_unit, @@ -2228,7 +2156,7 @@ async def test_unit_conversion_update( unique_id="very_unique", ) - entity1 = platform.MockSensor( + entity1 = MockSensor( name="Test 1", device_class=device_class, native_unit_of_measurement=native_unit, @@ -2236,7 +2164,7 @@ async def test_unit_conversion_update( unique_id="very_unique_1", ) - entity2 = platform.MockSensor( + entity2 = MockSensor( name="Test 2", device_class=device_class, native_unit_of_measurement=native_unit, @@ -2245,7 +2173,7 @@ async def test_unit_conversion_update( unique_id="very_unique_2", ) - entity3 = platform.MockSensor( + entity3 = MockSensor( name="Test 3", device_class=device_class, native_unit_of_measurement=native_unit, @@ -2254,7 +2182,7 @@ async def test_unit_conversion_update( unique_id="very_unique_3", ) - entity4 = platform.MockSensor( + entity4 = MockSensor( name="Test 4", device_class=device_class, native_unit_of_measurement=native_unit, @@ -2543,11 +2471,8 @@ async def test_entity_category_config_raises_error( caplog: pytest.LogCaptureFixture, ) -> None: """Test error is raised when entity category is set to config.""" - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - platform.ENTITIES["0"] = platform.MockSensor( - name="Test", entity_category=EntityCategory.CONFIG - ) + entity0 = MockSensor(name="Test", entity_category=EntityCategory.CONFIG) + setup_test_component_platform(hass, sensor.DOMAIN, [entity0]) assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() @@ -2643,13 +2568,11 @@ async def test_suggested_unit_guard_invalid_unit( An invalid suggested unit creates a log entry and the suggested unit will be ignored. """ entity_registry = er.async_get(hass) - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) state_value = 10 invalid_suggested_unit = "invalid_unit" - entity = platform.ENTITIES["0"] = platform.MockSensor( + entity = MockSensor( name="Invalid", device_class=device_class, native_unit_of_measurement=native_unit, @@ -2657,6 +2580,7 @@ async def test_suggested_unit_guard_invalid_unit( native_value=str(state_value), unique_id="invalid", ) + setup_test_component_platform(hass, sensor.DOMAIN, [entity]) assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() @@ -2673,10 +2597,10 @@ async def test_suggested_unit_guard_invalid_unit( "homeassistant.components.sensor", logging.WARNING, ( - " sets an" - " invalid suggested_unit_of_measurement. Please report it to the author" - " of the 'test' custom integration. This warning will become an error in" - " Home Assistant Core 2024.5" + " sets an" + " invalid suggested_unit_of_measurement. Please create a bug report at " + "https://github.com/home-assistant/core/issues?q=is%3Aopen+is%3Aissue+label%3A%22integration%3A+test%22." + " This warning will become an error in Home Assistant Core 2024.5" ), ) in caplog.record_tuples @@ -2714,10 +2638,8 @@ async def test_suggested_unit_guard_valid_unit( in the entity registry. """ entity_registry = er.async_get(hass) - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - entity = platform.ENTITIES["0"] = platform.MockSensor( + entity = MockSensor( name="Valid", device_class=device_class, native_unit_of_measurement=native_unit, @@ -2725,6 +2647,7 @@ async def test_suggested_unit_guard_valid_unit( suggested_unit_of_measurement=suggested_unit, unique_id="valid", ) + setup_test_component_platform(hass, sensor.DOMAIN, [entity]) assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index b4b535473c1..8084fe69e89 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -1,4 +1,5 @@ """The tests for sensor recorder platform.""" + from collections.abc import Callable from datetime import datetime, timedelta import math @@ -32,13 +33,14 @@ from homeassistant.components.recorder.statistics import ( list_statistic_ids, ) from homeassistant.components.recorder.util import get_instance, session_scope -from homeassistant.components.sensor import ATTR_OPTIONS, SensorDeviceClass +from homeassistant.components.sensor import ATTR_OPTIONS, DOMAIN, SensorDeviceClass from homeassistant.const import ATTR_FRIENDLY_NAME, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant, State from homeassistant.setup import async_setup_component, setup_component import homeassistant.util.dt as dt_util from homeassistant.util.unit_system import METRIC_SYSTEM, US_CUSTOMARY_SYSTEM +from tests.common import setup_test_component_platform from tests.components.recorder.common import ( assert_dict_of_states_equal_without_context_and_last_changed, assert_multiple_states_equal_without_context_and_last_changed, @@ -48,6 +50,7 @@ from tests.components.recorder.common import ( statistics_during_period, wait_recording_done, ) +from tests.components.sensor.common import MockSensor from tests.typing import WebSocketGenerator BATTERY_SENSOR_ATTRIBUTES = { @@ -1362,11 +1365,9 @@ def test_compile_hourly_sum_statistics_negative_state( hass = hass_recorder() hass.data.pop(loader.DATA_CUSTOM_COMPONENTS) - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - mocksensor = platform.MockSensor(name="custom_sensor") + mocksensor = MockSensor(name="custom_sensor") mocksensor._attr_should_poll = False - platform.ENTITIES["custom_sensor"] = mocksensor + setup_test_component_platform(hass, DOMAIN, [mocksensor], built_in=False) setup_component(hass, "homeassistant", {}) setup_component( @@ -4139,14 +4140,14 @@ async def test_validate_unit_change_convertible( # No statistics, unit in state matching device class - empty response hass.states.async_set( - "sensor.test", 10, attributes={**attributes, **{"unit_of_measurement": unit}} + "sensor.test", 10, attributes={**attributes, "unit_of_measurement": unit} ) await async_recorder_block_till_done(hass) await assert_validation_result(client, {}) # No statistics, unit in state not matching device class - empty response hass.states.async_set( - "sensor.test", 11, attributes={**attributes, **{"unit_of_measurement": "dogs"}} + "sensor.test", 11, attributes={**attributes, "unit_of_measurement": "dogs"} ) await async_recorder_block_till_done(hass) await assert_validation_result(client, {}) @@ -4155,7 +4156,7 @@ async def test_validate_unit_change_convertible( await async_recorder_block_till_done(hass) do_adhoc_statistics(hass, start=now) hass.states.async_set( - "sensor.test", 12, attributes={**attributes, **{"unit_of_measurement": "dogs"}} + "sensor.test", 12, attributes={**attributes, "unit_of_measurement": "dogs"} ) await async_recorder_block_till_done(hass) expected = { @@ -4175,7 +4176,7 @@ async def test_validate_unit_change_convertible( # Valid state - empty response hass.states.async_set( - "sensor.test", 13, attributes={**attributes, **{"unit_of_measurement": unit}} + "sensor.test", 13, attributes={**attributes, "unit_of_measurement": unit} ) await async_recorder_block_till_done(hass) await assert_validation_result(client, {}) @@ -4187,7 +4188,7 @@ async def test_validate_unit_change_convertible( # Valid state in compatible unit - empty response hass.states.async_set( - "sensor.test", 13, attributes={**attributes, **{"unit_of_measurement": unit2}} + "sensor.test", 13, attributes={**attributes, "unit_of_measurement": unit2} ) await async_recorder_block_till_done(hass) await assert_validation_result(client, {}) @@ -4262,7 +4263,7 @@ async def test_validate_statistics_unit_ignore_device_class( do_adhoc_statistics(hass, start=now) await async_recorder_block_till_done(hass) hass.states.async_set( - "sensor.test", 12, attributes={**attributes, **{"unit_of_measurement": "dogs"}} + "sensor.test", 12, attributes={**attributes, "unit_of_measurement": "dogs"} ) await hass.async_block_till_done() await assert_validation_result(client, {}) @@ -4349,14 +4350,14 @@ async def test_validate_statistics_unit_change_no_device_class( # No statistics, sensor state set - empty response hass.states.async_set( - "sensor.test", 10, attributes={**attributes, **{"unit_of_measurement": unit}} + "sensor.test", 10, attributes={**attributes, "unit_of_measurement": unit} ) await async_recorder_block_till_done(hass) await assert_validation_result(client, {}) # No statistics, sensor state set to an incompatible unit - empty response hass.states.async_set( - "sensor.test", 11, attributes={**attributes, **{"unit_of_measurement": "dogs"}} + "sensor.test", 11, attributes={**attributes, "unit_of_measurement": "dogs"} ) await async_recorder_block_till_done(hass) await assert_validation_result(client, {}) @@ -4365,7 +4366,7 @@ async def test_validate_statistics_unit_change_no_device_class( await async_recorder_block_till_done(hass) do_adhoc_statistics(hass, start=now) hass.states.async_set( - "sensor.test", 12, attributes={**attributes, **{"unit_of_measurement": "dogs"}} + "sensor.test", 12, attributes={**attributes, "unit_of_measurement": "dogs"} ) await async_recorder_block_till_done(hass) expected = { @@ -4385,7 +4386,7 @@ async def test_validate_statistics_unit_change_no_device_class( # Valid state - empty response hass.states.async_set( - "sensor.test", 13, attributes={**attributes, **{"unit_of_measurement": unit}} + "sensor.test", 13, attributes={**attributes, "unit_of_measurement": unit} ) await async_recorder_block_till_done(hass) await assert_validation_result(client, {}) @@ -4397,7 +4398,7 @@ async def test_validate_statistics_unit_change_no_device_class( # Valid state in compatible unit - empty response hass.states.async_set( - "sensor.test", 13, attributes={**attributes, **{"unit_of_measurement": unit2}} + "sensor.test", 13, attributes={**attributes, "unit_of_measurement": unit2} ) await async_recorder_block_till_done(hass) await assert_validation_result(client, {}) @@ -4738,13 +4739,13 @@ async def test_validate_statistics_unit_change_no_conversion( # No statistics, original unit - empty response hass.states.async_set( - "sensor.test", 10, attributes={**attributes, **{"unit_of_measurement": unit1}} + "sensor.test", 10, attributes={**attributes, "unit_of_measurement": unit1} ) await assert_validation_result(client, {}) # No statistics, changed unit - empty response hass.states.async_set( - "sensor.test", 11, attributes={**attributes, **{"unit_of_measurement": unit2}} + "sensor.test", 11, attributes={**attributes, "unit_of_measurement": unit2} ) await assert_validation_result(client, {}) @@ -4756,7 +4757,7 @@ async def test_validate_statistics_unit_change_no_conversion( # No statistics, original unit - empty response hass.states.async_set( - "sensor.test", 12, attributes={**attributes, **{"unit_of_measurement": unit1}} + "sensor.test", 12, attributes={**attributes, "unit_of_measurement": unit1} ) await assert_validation_result(client, {}) @@ -4771,7 +4772,7 @@ async def test_validate_statistics_unit_change_no_conversion( # Change unit - expect error hass.states.async_set( - "sensor.test", 13, attributes={**attributes, **{"unit_of_measurement": unit2}} + "sensor.test", 13, attributes={**attributes, "unit_of_measurement": unit2} ) await async_recorder_block_till_done(hass) expected = { @@ -4791,7 +4792,7 @@ async def test_validate_statistics_unit_change_no_conversion( # Original unit - empty response hass.states.async_set( - "sensor.test", 14, attributes={**attributes, **{"unit_of_measurement": unit1}} + "sensor.test", 14, attributes={**attributes, "unit_of_measurement": unit1} ) await async_recorder_block_till_done(hass) await assert_validation_result(client, {}) @@ -4873,7 +4874,7 @@ async def test_validate_statistics_unit_change_equivalent_units( # No statistics, original unit - empty response hass.states.async_set( - "sensor.test", 10, attributes={**attributes, **{"unit_of_measurement": unit1}} + "sensor.test", 10, attributes={**attributes, "unit_of_measurement": unit1} ) await assert_validation_result(client, {}) @@ -4887,7 +4888,7 @@ async def test_validate_statistics_unit_change_equivalent_units( # Units changed to an equivalent unit - empty response hass.states.async_set( - "sensor.test", 12, attributes={**attributes, **{"unit_of_measurement": unit2}} + "sensor.test", 12, attributes={**attributes, "unit_of_measurement": unit2} ) await assert_validation_result(client, {}) @@ -4959,7 +4960,7 @@ async def test_validate_statistics_unit_change_equivalent_units_2( # No statistics, original unit - empty response hass.states.async_set( - "sensor.test", 10, attributes={**attributes, **{"unit_of_measurement": unit1}} + "sensor.test", 10, attributes={**attributes, "unit_of_measurement": unit1} ) await assert_validation_result(client, {}) @@ -4973,7 +4974,7 @@ async def test_validate_statistics_unit_change_equivalent_units_2( # Units changed to an equivalent unit which is not known by the unit converters hass.states.async_set( - "sensor.test", 12, attributes={**attributes, **{"unit_of_measurement": unit2}} + "sensor.test", 12, attributes={**attributes, "unit_of_measurement": unit2} ) expected = { "sensor.test": [ @@ -5177,9 +5178,7 @@ async def test_exclude_attributes( recorder_mock: Recorder, hass: HomeAssistant, enable_custom_integrations: None ) -> None: """Test sensor attributes to be excluded.""" - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - platform.ENTITIES["0"] = platform.MockSensor( + entity0 = MockSensor( has_entity_name=True, unique_id="test", name="Test", @@ -5187,6 +5186,7 @@ async def test_exclude_attributes( device_class=SensorDeviceClass.ENUM, options=["option1", "option2"], ) + setup_test_component_platform(hass, DOMAIN, [entity0]) assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() await async_wait_recording_done(hass) diff --git a/tests/components/sensor/test_recorder_missing_stats.py b/tests/components/sensor/test_recorder_missing_stats.py index 810eaf6d730..88c98e6589f 100644 --- a/tests/components/sensor/test_recorder_missing_stats.py +++ b/tests/components/sensor/test_recorder_missing_stats.py @@ -1,4 +1,5 @@ """The tests for sensor recorder platform can catch up.""" + from datetime import datetime, timedelta from pathlib import Path from unittest.mock import patch diff --git a/tests/components/sensor/test_significant_change.py b/tests/components/sensor/test_significant_change.py index fa530b66381..e1a2325cd11 100644 --- a/tests/components/sensor/test_significant_change.py +++ b/tests/components/sensor/test_significant_change.py @@ -1,4 +1,5 @@ """Test the sensor significant change platform.""" + import pytest from homeassistant.components.sensor import SensorDeviceClass, significant_change diff --git a/tests/components/sensor/test_websocket_api.py b/tests/components/sensor/test_websocket_api.py index bd0a68598e1..6f4eeb252e2 100644 --- a/tests/components/sensor/test_websocket_api.py +++ b/tests/components/sensor/test_websocket_api.py @@ -1,4 +1,5 @@ """Test the sensor websocket API.""" + from pytest_unordered import unordered from homeassistant.components.sensor.const import ( @@ -30,7 +31,18 @@ async def test_device_class_units( msg = await client.receive_json() assert msg["success"] assert msg["result"] == { - "units": ["ft/s", "in/d", "in/h", "km/h", "kn", "m/s", "mm/d", "mm/h", "mph"] + "units": [ + "Beaufort", + "ft/s", + "in/d", + "in/h", + "km/h", + "kn", + "m/s", + "mm/d", + "mm/h", + "mph", + ] } # Device class with units which include `None` diff --git a/tests/components/sensorpro/__init__.py b/tests/components/sensorpro/__init__.py index f92eb700093..da40ff9a3f7 100644 --- a/tests/components/sensorpro/__init__.py +++ b/tests/components/sensorpro/__init__.py @@ -1,6 +1,5 @@ """Tests for the SensorPro integration.""" - from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo NOT_SENSORPRO_SERVICE_INFO = BluetoothServiceInfo( diff --git a/tests/components/sensorpro/test_config_flow.py b/tests/components/sensorpro/test_config_flow.py index b876fb215c1..1558e774f21 100644 --- a/tests/components/sensorpro/test_config_flow.py +++ b/tests/components/sensorpro/test_config_flow.py @@ -1,4 +1,5 @@ """Test the SensorPro config flow.""" + from unittest.mock import patch from homeassistant import config_entries diff --git a/tests/components/sensorpro/test_sensor.py b/tests/components/sensorpro/test_sensor.py index 5cb4483e92c..b98c629b51a 100644 --- a/tests/components/sensorpro/test_sensor.py +++ b/tests/components/sensorpro/test_sensor.py @@ -1,4 +1,5 @@ """Test the SensorPro sensors.""" + from homeassistant.components.sensor import ATTR_STATE_CLASS from homeassistant.components.sensorpro.const import DOMAIN from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT diff --git a/tests/components/sensorpush/__init__.py b/tests/components/sensorpush/__init__.py index c281d4dc086..aae960970dd 100644 --- a/tests/components/sensorpush/__init__.py +++ b/tests/components/sensorpush/__init__.py @@ -1,6 +1,5 @@ """Tests for the SensorPush integration.""" - from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo NOT_SENSOR_PUSH_SERVICE_INFO = BluetoothServiceInfo( diff --git a/tests/components/sensorpush/test_config_flow.py b/tests/components/sensorpush/test_config_flow.py index cf1cc7ea6fc..abbe04178c2 100644 --- a/tests/components/sensorpush/test_config_flow.py +++ b/tests/components/sensorpush/test_config_flow.py @@ -1,4 +1,5 @@ """Test the SensorPush config flow.""" + from unittest.mock import patch from homeassistant import config_entries diff --git a/tests/components/sensorpush/test_sensor.py b/tests/components/sensorpush/test_sensor.py index 2e7a0867309..cf69fc903f6 100644 --- a/tests/components/sensorpush/test_sensor.py +++ b/tests/components/sensorpush/test_sensor.py @@ -1,4 +1,5 @@ """Test the SensorPush sensors.""" + from datetime import timedelta import time @@ -55,9 +56,12 @@ async def test_sensors(hass: HomeAssistant) -> None: # Fastforward time without BLE advertisements monotonic_now = start_monotonic + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1 - with patch_bluetooth_time( - monotonic_now, - ), patch_all_discovered_devices([]): + with ( + patch_bluetooth_time( + monotonic_now, + ), + patch_all_discovered_devices([]), + ): async_fire_time_changed( hass, dt_util.utcnow() diff --git a/tests/components/sentry/conftest.py b/tests/components/sentry/conftest.py index a7347d44bab..781250b2753 100644 --- a/tests/components/sentry/conftest.py +++ b/tests/components/sentry/conftest.py @@ -1,4 +1,5 @@ """Configuration for Sentry tests.""" + from __future__ import annotations from typing import Any diff --git a/tests/components/sentry/test_config_flow.py b/tests/components/sentry/test_config_flow.py index 34ed1d2c4b5..0c3fc45b68b 100644 --- a/tests/components/sentry/test_config_flow.py +++ b/tests/components/sentry/test_config_flow.py @@ -1,4 +1,5 @@ """Test the sentry config flow.""" + import logging from unittest.mock import patch @@ -31,10 +32,13 @@ async def test_full_user_flow_implementation(hass: HomeAssistant) -> None: assert result.get("type") == FlowResultType.FORM assert result.get("errors") == {} - with patch("homeassistant.components.sentry.config_flow.Dsn"), patch( - "homeassistant.components.sentry.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch("homeassistant.components.sentry.config_flow.Dsn"), + patch( + "homeassistant.components.sentry.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"dsn": "http://public@sentry.local/1"}, diff --git a/tests/components/sentry/test_init.py b/tests/components/sentry/test_init.py index 73f6a7cfd09..3dd1dcd9b46 100644 --- a/tests/components/sentry/test_init.py +++ b/tests/components/sentry/test_init.py @@ -1,4 +1,5 @@ """Tests for Sentry integration.""" + import logging from unittest.mock import Mock, patch @@ -29,15 +30,18 @@ async def test_setup_entry(hass: HomeAssistant) -> None: ) entry.add_to_hass(hass) - with patch( - "homeassistant.components.sentry.AioHttpIntegration" - ) as sentry_aiohttp_mock, patch( - "homeassistant.components.sentry.SqlalchemyIntegration" - ) as sentry_sqlalchemy_mock, patch( - "homeassistant.components.sentry.LoggingIntegration" - ) as sentry_logging_mock, patch( - "homeassistant.components.sentry.sentry_sdk" - ) as sentry_mock: + with ( + patch( + "homeassistant.components.sentry.AioHttpIntegration" + ) as sentry_aiohttp_mock, + patch( + "homeassistant.components.sentry.SqlalchemyIntegration" + ) as sentry_sqlalchemy_mock, + patch( + "homeassistant.components.sentry.LoggingIntegration" + ) as sentry_logging_mock, + patch("homeassistant.components.sentry.sentry_sdk") as sentry_mock, + ): assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -83,11 +87,12 @@ async def test_setup_entry_with_tracing(hass: HomeAssistant) -> None: ) entry.add_to_hass(hass) - with patch("homeassistant.components.sentry.AioHttpIntegration"), patch( - "homeassistant.components.sentry.SqlalchemyIntegration" - ), patch("homeassistant.components.sentry.LoggingIntegration"), patch( - "homeassistant.components.sentry.sentry_sdk" - ) as sentry_mock: + with ( + patch("homeassistant.components.sentry.AioHttpIntegration"), + patch("homeassistant.components.sentry.SqlalchemyIntegration"), + patch("homeassistant.components.sentry.LoggingIntegration"), + patch("homeassistant.components.sentry.sentry_sdk") as sentry_mock, + ): assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/senz/test_config_flow.py b/tests/components/senz/test_config_flow.py index e9179f9ab30..04ef1a6de0c 100644 --- a/tests/components/senz/test_config_flow.py +++ b/tests/components/senz/test_config_flow.py @@ -1,4 +1,5 @@ """Test the SENZ config flow.""" + from unittest.mock import patch from aiosenz import AUTHORIZATION_ENDPOINT, TOKEN_ENDPOINT diff --git a/tests/components/seventeentrack/__init__.py b/tests/components/seventeentrack/__init__.py index 5cc3bf34871..4101f34496e 100644 --- a/tests/components/seventeentrack/__init__.py +++ b/tests/components/seventeentrack/__init__.py @@ -1 +1,28 @@ """Tests for the seventeentrack component.""" + +from datetime import timedelta + +from freezegun.api import FrozenDateTimeFactory + +from homeassistant.components.seventeentrack.sensor import DEFAULT_SCAN_INTERVAL +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry, async_fire_time_changed + + +async def init_integration( + hass: HomeAssistant, + config_entry: MockConfigEntry, +) -> None: + """Set up the 17Track integration in Home Assistant.""" + + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + +async def goto_future(hass: HomeAssistant, freezer: FrozenDateTimeFactory): + """Move to future.""" + freezer.tick(DEFAULT_SCAN_INTERVAL + timedelta(minutes=1)) + async_fire_time_changed(hass) + await hass.async_block_till_done() diff --git a/tests/components/seventeentrack/conftest.py b/tests/components/seventeentrack/conftest.py new file mode 100644 index 00000000000..052d66a4696 --- /dev/null +++ b/tests/components/seventeentrack/conftest.py @@ -0,0 +1,153 @@ +"""Configuration for 17Track tests.""" + +from collections.abc import Generator +from typing import Optional +from unittest.mock import AsyncMock, patch + +from py17track.package import Package +import pytest + +from homeassistant.components.seventeentrack.const import ( + DEFAULT_SHOW_ARCHIVED, + DEFAULT_SHOW_DELIVERED, +) +from homeassistant.components.seventeentrack.sensor import ( + CONF_SHOW_ARCHIVED, + CONF_SHOW_DELIVERED, +) +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME + +from tests.common import MockConfigEntry + +DEFAULT_SUMMARY = { + "Not Found": 0, + "In Transit": 0, + "Expired": 0, + "Ready to be Picked Up": 0, + "Undelivered": 0, + "Delivered": 0, + "Returned": 0, +} + +ACCOUNT_ID = "1234" + +NEW_SUMMARY_DATA = { + "Not Found": 1, + "In Transit": 1, + "Expired": 1, + "Ready to be Picked Up": 1, + "Undelivered": 1, + "Delivered": 1, + "Returned": 1, +} + +VALID_CONFIG = { + CONF_USERNAME: "test", + CONF_PASSWORD: "test", +} + +INVALID_CONFIG = {"notusername": "seventeentrack", "notpassword": "test"} + +VALID_OPTIONS = { + CONF_SHOW_ARCHIVED: True, + CONF_SHOW_DELIVERED: True, +} + +NO_DELIVERED_OPTIONS = { + CONF_SHOW_ARCHIVED: False, + CONF_SHOW_DELIVERED: False, +} + +VALID_PLATFORM_CONFIG_FULL = { + "sensor": { + "platform": "seventeentrack", + CONF_USERNAME: "test", + CONF_PASSWORD: "test", + CONF_SHOW_ARCHIVED: True, + CONF_SHOW_DELIVERED: True, + } +} + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock, None, None]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.seventeentrack.async_setup_entry", return_value=True + ) as mock_setup_entry: + yield mock_setup_entry + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Return the default mocked config entry.""" + return MockConfigEntry( + domain="seventeentrack", + data=VALID_CONFIG, + options=VALID_OPTIONS, + unique_id=ACCOUNT_ID, + ) + + +@pytest.fixture +def mock_config_entry_with_default_options() -> MockConfigEntry: + """Return the default mocked config entry.""" + return MockConfigEntry( + domain="seventeentrack", + data=VALID_CONFIG, + options={ + CONF_SHOW_ARCHIVED: DEFAULT_SHOW_ARCHIVED, + CONF_SHOW_DELIVERED: DEFAULT_SHOW_DELIVERED, + }, + unique_id=ACCOUNT_ID, + ) + + +@pytest.fixture +def mock_seventeentrack(): + """Build a fixture for the 17Track API.""" + mock_seventeentrack_api = AsyncMock() + with ( + patch( + "homeassistant.components.seventeentrack.SeventeenTrackClient", + return_value=mock_seventeentrack_api, + ), + patch( + "homeassistant.components.seventeentrack.config_flow.SeventeenTrackClient", + return_value=mock_seventeentrack_api, + ) as mock_seventeentrack_api, + ): + mock_seventeentrack_api.return_value.profile.account_id = ACCOUNT_ID + mock_seventeentrack_api.return_value.profile.login.return_value = True + mock_seventeentrack_api.return_value.profile.packages.return_value = [] + mock_seventeentrack_api.return_value.profile.summary.return_value = ( + DEFAULT_SUMMARY + ) + yield mock_seventeentrack_api + + +def get_package( + tracking_number: str = "456", + destination_country: int = 206, + friendly_name: Optional[str] = "friendly name 1", + info_text: str = "info text 1", + location: str = "location 1", + timestamp: str = "2020-08-10 10:32", + origin_country: int = 206, + package_type: int = 2, + status: int = 0, + tz: str = "UTC", +): + """Build a Package of the 17Track API.""" + return Package( + tracking_number=tracking_number, + destination_country=destination_country, + friendly_name=friendly_name, + info_text=info_text, + location=location, + timestamp=timestamp, + origin_country=origin_country, + package_type=package_type, + status=status, + tz=tz, + ) diff --git a/tests/components/seventeentrack/test_config_flow.py b/tests/components/seventeentrack/test_config_flow.py new file mode 100644 index 00000000000..ae48fb6c792 --- /dev/null +++ b/tests/components/seventeentrack/test_config_flow.py @@ -0,0 +1,208 @@ +"""Define tests for the 17Track config flow.""" + +from unittest.mock import AsyncMock + +from py17track.errors import SeventeenTrackError +import pytest + +from homeassistant import config_entries, data_entry_flow +from homeassistant.components.seventeentrack import DOMAIN +from homeassistant.components.seventeentrack.const import ( + CONF_SHOW_ARCHIVED, + CONF_SHOW_DELIVERED, +) +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from tests.common import MockConfigEntry + +ACCOUNT_ID = "1234" + +VALID_CONFIG = { + CONF_USERNAME: "someemail@gmail.com", + CONF_PASSWORD: "edc3eee7330e4fdda04489e3fbc283d0", +} + +VALID_CONFIG_OLD = { + CONF_USERNAME: "someemail@gmail.com", + CONF_PASSWORD: "edc3eee7330e4fdda04489e3fbc283d0", +} + + +async def test_create_entry( + hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_seventeentrack: AsyncMock +) -> None: + """Test that the user step works.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {} + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + VALID_CONFIG, + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["title"] == "someemail@gmail.com" + assert result2["data"] == { + CONF_PASSWORD: "edc3eee7330e4fdda04489e3fbc283d0", + CONF_USERNAME: "someemail@gmail.com", + } + + +@pytest.mark.parametrize( + ("return_value", "side_effect", "error"), + [ + ( + False, + None, + "invalid_auth", + ), + ( + True, + SeventeenTrackError(), + "cannot_connect", + ), + ], +) +async def test_flow_fails( + hass: HomeAssistant, + mock_seventeentrack: AsyncMock, + return_value, + side_effect, + error, +) -> None: + """Test that the user step fails.""" + mock_seventeentrack.return_value.profile.login.return_value = return_value + mock_seventeentrack.return_value.profile.login.side_effect = side_effect + failed_result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data=VALID_CONFIG, + ) + + assert failed_result["errors"] == {"base": error} + + mock_seventeentrack.return_value.profile.login.return_value = True + mock_seventeentrack.return_value.profile.login.side_effect = None + + result = await hass.config_entries.flow.async_configure( + failed_result["flow_id"], + VALID_CONFIG, + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["title"] == "someemail@gmail.com" + assert result["data"] == { + CONF_PASSWORD: "edc3eee7330e4fdda04489e3fbc283d0", + CONF_USERNAME: "someemail@gmail.com", + } + + +async def test_import_flow(hass: HomeAssistant, mock_seventeentrack: AsyncMock) -> None: + """Test the import configuration flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=VALID_CONFIG_OLD, + ) + + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["title"] == "someemail@gmail.com" + assert result["data"][CONF_USERNAME] == "someemail@gmail.com" + assert result["data"][CONF_PASSWORD] == "edc3eee7330e4fdda04489e3fbc283d0" + + +@pytest.mark.parametrize( + ("return_value", "side_effect", "error"), + [ + ( + False, + None, + "invalid_auth", + ), + ( + True, + SeventeenTrackError(), + "cannot_connect", + ), + ], +) +async def test_import_flow_cannot_connect_error( + hass: HomeAssistant, + mock_seventeentrack: AsyncMock, + return_value, + side_effect, + error, +) -> None: + """Test the import configuration flow with error.""" + mock_seventeentrack.return_value.profile.login.return_value = return_value + mock_seventeentrack.return_value.profile.login.side_effect = side_effect + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=VALID_CONFIG_OLD, + ) + + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == error + + +async def test_option_flow(hass: HomeAssistant, mock_seventeentrack: AsyncMock) -> None: + """Test option flow.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=VALID_CONFIG, + options={ + CONF_SHOW_ARCHIVED: False, + CONF_SHOW_DELIVERED: False, + }, + ) + 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"] == 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_ARCHIVED: True, CONF_SHOW_DELIVERED: False}, + ) + + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["data"][CONF_SHOW_ARCHIVED] + assert not result["data"][CONF_SHOW_DELIVERED] + + +async def test_import_flow_already_configured( + hass: HomeAssistant, mock_seventeentrack: AsyncMock +) -> None: + """Test the import configuration flow with error.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=VALID_CONFIG, + unique_id=ACCOUNT_ID, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + result_aborted = await hass.config_entries.flow.async_configure( + result["flow_id"], + VALID_CONFIG, + ) + await hass.async_block_till_done() + + assert result_aborted["type"] == data_entry_flow.FlowResultType.ABORT + assert result_aborted["reason"] == "already_configured" diff --git a/tests/components/seventeentrack/test_sensor.py b/tests/components/seventeentrack/test_sensor.py index 653e4b956b6..aa7f61ad318 100644 --- a/tests/components/seventeentrack/test_sensor.py +++ b/tests/components/seventeentrack/test_sensor.py @@ -1,283 +1,192 @@ """Tests for the seventeentrack sensor.""" + from __future__ import annotations -import datetime -from unittest.mock import patch +from unittest.mock import AsyncMock, patch -from py17track.package import Package -import pytest +from freezegun.api import FrozenDateTimeFactory +from py17track.errors import SeventeenTrackError -from homeassistant.components.seventeentrack.sensor import ( - CONF_SHOW_ARCHIVED, - CONF_SHOW_DELIVERED, -) -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.helpers.issue_registry import IssueRegistry from homeassistant.setup import async_setup_component -from homeassistant.util import utcnow -from tests.common import async_fire_time_changed +from . import goto_future, init_integration +from .conftest import ( + DEFAULT_SUMMARY, + NEW_SUMMARY_DATA, + VALID_PLATFORM_CONFIG_FULL, + get_package, +) -VALID_CONFIG_MINIMAL = { - "sensor": { - "platform": "seventeentrack", - CONF_USERNAME: "test", - CONF_PASSWORD: "test", - } -} - -INVALID_CONFIG = {"sensor": {"platform": "seventeentrack", "boom": "test"}} - -VALID_CONFIG_FULL = { - "sensor": { - "platform": "seventeentrack", - CONF_USERNAME: "test", - CONF_PASSWORD: "test", - CONF_SHOW_ARCHIVED: True, - CONF_SHOW_DELIVERED: True, - } -} - -VALID_CONFIG_FULL_NO_DELIVERED = { - "sensor": { - "platform": "seventeentrack", - CONF_USERNAME: "test", - CONF_PASSWORD: "test", - CONF_SHOW_ARCHIVED: False, - CONF_SHOW_DELIVERED: False, - } -} - -DEFAULT_SUMMARY = { - "Not Found": 0, - "In Transit": 0, - "Expired": 0, - "Ready to be Picked Up": 0, - "Undelivered": 0, - "Delivered": 0, - "Returned": 0, -} - -NEW_SUMMARY_DATA = { - "Not Found": 1, - "In Transit": 1, - "Expired": 1, - "Ready to be Picked Up": 1, - "Undelivered": 1, - "Delivered": 1, - "Returned": 1, -} +from tests.common import MockConfigEntry -class ClientMock: - """Mock the py17track client to inject the ProfileMock.""" - - def __init__(self, session) -> None: - """Mock the profile.""" - self.profile = ProfileMock() - - -class ProfileMock: - """ProfileMock will mock data coming from 17track.""" - - package_list = [] - login_result = True - summary_data = DEFAULT_SUMMARY - account_id = "123" - - @classmethod - def reset(cls): - """Reset data to defaults.""" - cls.package_list = [] - cls.login_result = True - cls.summary_data = DEFAULT_SUMMARY - cls.account_id = "123" - - def __init__(self) -> None: - """Override Account id.""" - self.account_id = self.__class__.account_id - - async def login(self, email: str, password: str) -> bool: - """Login mock.""" - return self.__class__.login_result - - async def packages( - self, - package_state: int | str = "", - show_archived: bool = False, - tz: str = "UTC", - ) -> list: - """Packages mock.""" # noqa: D401 - return self.__class__.package_list[:] - - async def summary(self, show_archived: bool = False) -> dict: - """Summary mock.""" - return self.__class__.summary_data - - -@pytest.fixture(autouse=True, name="mock_client") -def fixture_mock_client(): - """Mock py17track client.""" - with patch( - "homeassistant.components.seventeentrack.sensor.SeventeenTrackClient", - new=ClientMock, - ): - yield - ProfileMock.reset() - - -async def _setup_seventeentrack(hass, config=None, summary_data=None): - """Set up component using config.""" - if not config: - config = VALID_CONFIG_MINIMAL - if not summary_data: - summary_data = {} - - ProfileMock.summary_data = summary_data - assert await async_setup_component(hass, "sensor", config) - await hass.async_block_till_done() - - -async def _goto_future(hass, future=None): - """Move to future.""" - if not future: - future = utcnow() + datetime.timedelta(minutes=10) - with patch("homeassistant.util.utcnow", return_value=future): - async_fire_time_changed(hass, future) - await hass.async_block_till_done() - async_fire_time_changed(hass, future) - await hass.async_block_till_done() - - -async def test_full_valid_config(hass: HomeAssistant) -> None: +async def test_full_valid_config( + hass: HomeAssistant, + mock_seventeentrack: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: """Ensure everything starts correctly.""" - assert await async_setup_component(hass, "sensor", VALID_CONFIG_FULL) - await hass.async_block_till_done() - assert len(hass.states.async_entity_ids()) == len(ProfileMock.summary_data.keys()) + await init_integration(hass, mock_config_entry) + assert len(hass.states.async_entity_ids()) == len(DEFAULT_SUMMARY.keys()) -async def test_valid_config(hass: HomeAssistant) -> None: +async def test_valid_config( + hass: HomeAssistant, + mock_seventeentrack: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: """Ensure everything starts correctly.""" - assert await async_setup_component(hass, "sensor", VALID_CONFIG_MINIMAL) - await hass.async_block_till_done() - assert len(hass.states.async_entity_ids()) == len(ProfileMock.summary_data.keys()) + await init_integration(hass, mock_config_entry) + assert len(hass.states.async_entity_ids()) == len(DEFAULT_SUMMARY.keys()) -async def test_invalid_config(hass: HomeAssistant) -> None: +async def test_invalid_config( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: """Ensure nothing is created when config is wrong.""" - assert await async_setup_component(hass, "sensor", INVALID_CONFIG) - + await init_integration(hass, mock_config_entry) assert not hass.states.async_entity_ids("sensor") -async def test_add_package(hass: HomeAssistant) -> None: - """Ensure package is added correctly when user add a new package.""" - package = Package( - tracking_number="456", - destination_country=206, - friendly_name="friendly name 1", - info_text="info text 1", - location="location 1", - timestamp="2020-08-10 10:32", - origin_country=206, - package_type=2, +async def test_login_exception( + hass: HomeAssistant, + mock_seventeentrack: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Ensure everything starts correctly.""" + mock_seventeentrack.return_value.profile.login.side_effect = SeventeenTrackError( + "Error" ) - ProfileMock.package_list = [package] + await init_integration(hass, mock_config_entry) + assert not hass.states.async_entity_ids("sensor") - await _setup_seventeentrack(hass) + +async def test_add_package( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + mock_seventeentrack: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Ensure package is added correctly when user add a new package.""" + package = get_package() + mock_seventeentrack.return_value.profile.packages.return_value = [package] + mock_seventeentrack.return_value.profile.summary.return_value = {} + + await init_integration(hass, mock_config_entry) assert hass.states.get("sensor.seventeentrack_package_456") is not None assert len(hass.states.async_entity_ids()) == 1 - package2 = Package( + package2 = get_package( tracking_number="789", - destination_country=206, friendly_name="friendly name 2", info_text="info text 2", location="location 2", timestamp="2020-08-10 14:25", - origin_country=206, - package_type=2, ) - ProfileMock.package_list = [package, package2] + mock_seventeentrack.return_value.profile.packages.return_value = [package, package2] - await _goto_future(hass) + await goto_future(hass, freezer) assert hass.states.get("sensor.seventeentrack_package_789") is not None assert len(hass.states.async_entity_ids()) == 2 -async def test_remove_package(hass: HomeAssistant) -> None: +async def test_add_package_default_friendly_name( + hass: HomeAssistant, + mock_seventeentrack: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Ensure package is added correctly with default friendly name when user add a new package without his own friendly name.""" + package = get_package(friendly_name=None) + mock_seventeentrack.return_value.profile.packages.return_value = [package] + mock_seventeentrack.return_value.profile.summary.return_value = {} + + await init_integration(hass, mock_config_entry) + state_456 = hass.states.get("sensor.seventeentrack_package_456") + assert state_456 is not None + assert state_456.attributes["friendly_name"] == "Seventeentrack Package: 456" + assert len(hass.states.async_entity_ids()) == 1 + + +async def test_remove_package( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + mock_seventeentrack: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: """Ensure entity is not there anymore if package is not there.""" - package1 = Package( - tracking_number="456", - destination_country=206, - friendly_name="friendly name 1", - info_text="info text 1", - location="location 1", - timestamp="2020-08-10 10:32", - origin_country=206, - package_type=2, - ) - package2 = Package( + package1 = get_package() + package2 = get_package( tracking_number="789", - destination_country=206, friendly_name="friendly name 2", info_text="info text 2", location="location 2", timestamp="2020-08-10 14:25", - origin_country=206, - package_type=2, ) - ProfileMock.package_list = [package1, package2] + mock_seventeentrack.return_value.profile.packages.return_value = [ + package1, + package2, + ] + mock_seventeentrack.return_value.profile.summary.return_value = {} - await _setup_seventeentrack(hass) + await init_integration(hass, mock_config_entry) assert hass.states.get("sensor.seventeentrack_package_456") is not None assert hass.states.get("sensor.seventeentrack_package_789") is not None assert len(hass.states.async_entity_ids()) == 2 - ProfileMock.package_list = [package2] + mock_seventeentrack.return_value.profile.packages.return_value = [package2] - await _goto_future(hass) + await goto_future(hass, freezer) + + assert hass.states.get("sensor.seventeentrack_package_456").state == "unavailable" + assert len(hass.states.async_entity_ids()) == 2 + + await goto_future(hass, freezer) assert hass.states.get("sensor.seventeentrack_package_456") is None assert hass.states.get("sensor.seventeentrack_package_789") is not None assert len(hass.states.async_entity_ids()) == 1 -async def test_friendly_name_changed(hass: HomeAssistant) -> None: - """Test friendly name change.""" - package = Package( - tracking_number="456", - destination_country=206, - friendly_name="friendly name 1", - info_text="info text 1", - location="location 1", - timestamp="2020-08-10 10:32", - origin_country=206, - package_type=2, +async def test_package_error( + hass: HomeAssistant, + mock_seventeentrack: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Ensure package is added correctly when user add a new package.""" + mock_seventeentrack.return_value.profile.packages.side_effect = SeventeenTrackError( + "Error" ) - ProfileMock.package_list = [package] + mock_seventeentrack.return_value.profile.summary.return_value = {} - await _setup_seventeentrack(hass) + await init_integration(hass, mock_config_entry) + assert hass.states.get("sensor.seventeentrack_package_456") is None + + +async def test_friendly_name_changed( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + mock_seventeentrack: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test friendly name change.""" + package = get_package() + mock_seventeentrack.return_value.profile.packages.return_value = [package] + mock_seventeentrack.return_value.profile.summary.return_value = {} + + await init_integration(hass, mock_config_entry) assert hass.states.get("sensor.seventeentrack_package_456") is not None assert len(hass.states.async_entity_ids()) == 1 - package = Package( - tracking_number="456", - destination_country=206, - friendly_name="friendly name 2", - info_text="info text 1", - location="location 1", - timestamp="2020-08-10 10:32", - origin_country=206, - package_type=2, - ) - ProfileMock.package_list = [package] + package = get_package(friendly_name="friendly name 2") + mock_seventeentrack.return_value.profile.packages.return_value = [package] - await _goto_future(hass) + await goto_future(hass, freezer) assert hass.states.get("sensor.seventeentrack_package_456") is not None entity = hass.data["entity_components"]["sensor"].get_entity( @@ -287,169 +196,172 @@ async def test_friendly_name_changed(hass: HomeAssistant) -> None: assert len(hass.states.async_entity_ids()) == 1 -async def test_delivered_not_shown(hass: HomeAssistant) -> None: +async def test_delivered_not_shown( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + mock_seventeentrack: AsyncMock, + mock_config_entry_with_default_options: MockConfigEntry, +) -> None: """Ensure delivered packages are not shown.""" - package = Package( - tracking_number="456", - destination_country=206, - friendly_name="friendly name 1", - info_text="info text 1", - location="location 1", - timestamp="2020-08-10 10:32", - origin_country=206, - package_type=2, - status=40, - ) - ProfileMock.package_list = [package] + package = get_package(status=40) + mock_seventeentrack.return_value.profile.packages.return_value = [package] + mock_seventeentrack.return_value.profile.summary.return_value = {} with patch( "homeassistant.components.seventeentrack.sensor.persistent_notification" ) as persistent_notification_mock: - await _setup_seventeentrack(hass, VALID_CONFIG_FULL_NO_DELIVERED) - await _goto_future(hass) + await init_integration(hass, mock_config_entry_with_default_options) + await goto_future(hass, freezer) assert not hass.states.async_entity_ids() persistent_notification_mock.create.assert_called() -async def test_delivered_shown(hass: HomeAssistant) -> None: +async def test_delivered_shown( + hass: HomeAssistant, + mock_seventeentrack: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: """Ensure delivered packages are show when user choose to show them.""" - package = Package( - tracking_number="456", - destination_country=206, - friendly_name="friendly name 1", - info_text="info text 1", - location="location 1", - timestamp="2020-08-10 10:32", - origin_country=206, - package_type=2, - status=40, - ) - ProfileMock.package_list = [package] + package = get_package(status=40) + mock_seventeentrack.return_value.profile.packages.return_value = [package] + mock_seventeentrack.return_value.profile.summary.return_value = {} with patch( "homeassistant.components.seventeentrack.sensor.persistent_notification" ) as persistent_notification_mock: - await _setup_seventeentrack(hass, VALID_CONFIG_FULL) + await init_integration(hass, mock_config_entry) assert hass.states.get("sensor.seventeentrack_package_456") is not None assert len(hass.states.async_entity_ids()) == 1 persistent_notification_mock.create.assert_not_called() -async def test_becomes_delivered_not_shown_notification(hass: HomeAssistant) -> None: +async def test_becomes_delivered_not_shown_notification( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + mock_seventeentrack: AsyncMock, + mock_config_entry_with_default_options: MockConfigEntry, +) -> None: """Ensure notification is triggered when package becomes delivered.""" - package = Package( - tracking_number="456", - destination_country=206, - friendly_name="friendly name 1", - info_text="info text 1", - location="location 1", - timestamp="2020-08-10 10:32", - origin_country=206, - package_type=2, - ) - ProfileMock.package_list = [package] + package = get_package() + mock_seventeentrack.return_value.profile.packages.return_value = [package] + mock_seventeentrack.return_value.profile.summary.return_value = {} - await _setup_seventeentrack(hass, VALID_CONFIG_FULL_NO_DELIVERED) + await init_integration(hass, mock_config_entry_with_default_options) assert hass.states.get("sensor.seventeentrack_package_456") is not None assert len(hass.states.async_entity_ids()) == 1 - package_delivered = Package( - tracking_number="456", - destination_country=206, - friendly_name="friendly name 1", - info_text="info text 1", - location="location 1", - timestamp="2020-08-10 10:32", - origin_country=206, - package_type=2, - status=40, - ) - ProfileMock.package_list = [package_delivered] + package_delivered = get_package(status=40) + mock_seventeentrack.return_value.profile.packages.return_value = [package_delivered] with patch( "homeassistant.components.seventeentrack.sensor.persistent_notification" ) as persistent_notification_mock: - await _goto_future(hass) + await goto_future(hass, freezer) + await goto_future(hass, freezer) persistent_notification_mock.create.assert_called() assert not hass.states.async_entity_ids() -async def test_summary_correctly_updated(hass: HomeAssistant) -> None: +async def test_summary_correctly_updated( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + mock_seventeentrack: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: """Ensure summary entities are not duplicated.""" - package = Package( - tracking_number="456", - destination_country=206, - friendly_name="friendly name 1", - info_text="info text 1", - location="location 1", - timestamp="2020-08-10 10:32", - origin_country=206, - package_type=2, - status=30, - ) - ProfileMock.package_list = [package] + package = get_package(status=30) + mock_seventeentrack.return_value.profile.packages.return_value = [package] + mock_seventeentrack.return_value.profile.summary.return_value = DEFAULT_SUMMARY - await _setup_seventeentrack(hass, summary_data=DEFAULT_SUMMARY) + await init_integration(hass, mock_config_entry) assert len(hass.states.async_entity_ids()) == 8 - for state in hass.states.async_all(): - if state.entity_id == "sensor.seventeentrack_package_456": - break - assert state.state == "0" - assert ( - len( - hass.states.get( - "sensor.seventeentrack_packages_ready_to_be_picked_up" - ).attributes["packages"] - ) - == 1 + state_ready_picked = hass.states.get( + "sensor.seventeentrack_packages_ready_to_be_picked_up" ) + assert state_ready_picked is not None + assert len(state_ready_picked.attributes["packages"]) == 1 - ProfileMock.package_list = [] - ProfileMock.summary_data = NEW_SUMMARY_DATA + mock_seventeentrack.return_value.profile.packages.return_value = [] + mock_seventeentrack.return_value.profile.summary.return_value = NEW_SUMMARY_DATA - await _goto_future(hass) + await goto_future(hass, freezer) + await goto_future(hass, freezer) assert len(hass.states.async_entity_ids()) == 7 for state in hass.states.async_all(): assert state.state == "1" + state_ready_picked = hass.states.get( + "sensor.seventeentrack_packages_ready_to_be_picked_up" + ) + assert state_ready_picked is not None + assert state_ready_picked.attributes["packages"] is None + + +async def test_summary_error( + hass: HomeAssistant, + mock_seventeentrack: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test summary empty if error.""" + package = get_package(status=30) + mock_seventeentrack.return_value.profile.packages.return_value = [package] + mock_seventeentrack.return_value.profile.summary.side_effect = SeventeenTrackError( + "Error" + ) + + await init_integration(hass, mock_config_entry) + + assert len(hass.states.async_entity_ids()) == 1 + assert ( - hass.states.get( - "sensor.seventeentrack_packages_ready_to_be_picked_up" - ).attributes["packages"] - is None + hass.states.get("sensor.seventeentrack_packages_ready_to_be_picked_up") is None ) -async def test_utc_timestamp(hass: HomeAssistant) -> None: +async def test_utc_timestamp( + hass: HomeAssistant, + mock_seventeentrack: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: """Ensure package timestamp is converted correctly from HA-defined time zone to UTC.""" - package = Package( - tracking_number="456", - destination_country=206, - friendly_name="friendly name 1", - info_text="info text 1", - location="location 1", - timestamp="2020-08-10 10:32", - origin_country=206, - package_type=2, - tz="Asia/Jakarta", - ) - ProfileMock.package_list = [package] - await _setup_seventeentrack(hass) + package = get_package(tz="Asia/Jakarta") + mock_seventeentrack.return_value.profile.packages.return_value = [package] + mock_seventeentrack.return_value.profile.summary.return_value = {} + + await init_integration(hass, mock_config_entry) + assert hass.states.get("sensor.seventeentrack_package_456") is not None assert len(hass.states.async_entity_ids()) == 1 - assert ( - str( - hass.states.get("sensor.seventeentrack_package_456").attributes.get( - "timestamp" - ) - ) - == "2020-08-10 03:32:00+00:00" - ) + state_456 = hass.states.get("sensor.seventeentrack_package_456") + assert state_456 is not None + assert str(state_456.attributes.get("timestamp")) == "2020-08-10 03:32:00+00:00" + + +async def test_non_valid_platform_config( + hass: HomeAssistant, mock_seventeentrack: AsyncMock +) -> None: + """Test if login fails.""" + mock_seventeentrack.return_value.profile.login.return_value = False + assert await async_setup_component(hass, "sensor", VALID_PLATFORM_CONFIG_FULL) + await hass.async_block_till_done() + assert len(hass.states.async_entity_ids()) == 0 + + +async def test_full_valid_platform_config( + hass: HomeAssistant, + mock_seventeentrack: AsyncMock, + issue_registry: IssueRegistry, +) -> None: + """Ensure everything starts correctly.""" + assert await async_setup_component(hass, "sensor", VALID_PLATFORM_CONFIG_FULL) + await hass.async_block_till_done() + assert len(hass.states.async_entity_ids()) == len(DEFAULT_SUMMARY.keys()) + assert len(issue_registry.issues) == 1 diff --git a/tests/components/sfr_box/conftest.py b/tests/components/sfr_box/conftest.py index a8cd6fd8bd4..dec99738a03 100644 --- a/tests/components/sfr_box/conftest.py +++ b/tests/components/sfr_box/conftest.py @@ -1,4 +1,5 @@ """Provide common SFR Box fixtures.""" + from collections.abc import Generator import json from unittest.mock import AsyncMock, patch diff --git a/tests/components/sfr_box/snapshots/test_binary_sensor.ambr b/tests/components/sfr_box/snapshots/test_binary_sensor.ambr index 1333121df87..7422c1395c3 100644 --- a/tests/components/sfr_box/snapshots/test_binary_sensor.ambr +++ b/tests/components/sfr_box/snapshots/test_binary_sensor.ambr @@ -107,6 +107,7 @@ 'context': , 'entity_id': 'binary_sensor.sfr_box_wan_status', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }), @@ -118,6 +119,7 @@ 'context': , 'entity_id': 'binary_sensor.sfr_box_dsl_status', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }), @@ -231,6 +233,7 @@ 'context': , 'entity_id': 'binary_sensor.sfr_box_wan_status', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }), @@ -242,6 +245,7 @@ 'context': , 'entity_id': 'binary_sensor.sfr_box_ftth_status', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }), diff --git a/tests/components/sfr_box/snapshots/test_button.ambr b/tests/components/sfr_box/snapshots/test_button.ambr index 0ca62e8caed..0dfbf187f6d 100644 --- a/tests/components/sfr_box/snapshots/test_button.ambr +++ b/tests/components/sfr_box/snapshots/test_button.ambr @@ -76,6 +76,7 @@ 'context': , 'entity_id': 'button.sfr_box_restart', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }), diff --git a/tests/components/sfr_box/snapshots/test_sensor.ambr b/tests/components/sfr_box/snapshots/test_sensor.ambr index be2ee848029..0f39eed9e60 100644 --- a/tests/components/sfr_box/snapshots/test_sensor.ambr +++ b/tests/components/sfr_box/snapshots/test_sensor.ambr @@ -565,6 +565,7 @@ 'context': , 'entity_id': 'sensor.sfr_box_network_infrastructure', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'adsl', }), @@ -577,6 +578,7 @@ 'context': , 'entity_id': 'sensor.sfr_box_voltage', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '12251', }), @@ -589,6 +591,7 @@ 'context': , 'entity_id': 'sensor.sfr_box_temperature', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '27.56', }), @@ -607,6 +610,7 @@ 'context': , 'entity_id': 'sensor.sfr_box_wan_mode', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'adsl_routed', }), @@ -617,6 +621,7 @@ 'context': , 'entity_id': 'sensor.sfr_box_dsl_line_mode', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'ADSL2+', }), @@ -627,6 +632,7 @@ 'context': , 'entity_id': 'sensor.sfr_box_dsl_counter', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '16', }), @@ -637,6 +643,7 @@ 'context': , 'entity_id': 'sensor.sfr_box_dsl_crc', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0', }), @@ -650,6 +657,7 @@ 'context': , 'entity_id': 'sensor.sfr_box_dsl_noise_down', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '5.8', }), @@ -663,6 +671,7 @@ 'context': , 'entity_id': 'sensor.sfr_box_dsl_noise_up', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '6.0', }), @@ -676,6 +685,7 @@ 'context': , 'entity_id': 'sensor.sfr_box_dsl_attenuation_down', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '28.5', }), @@ -689,6 +699,7 @@ 'context': , 'entity_id': 'sensor.sfr_box_dsl_attenuation_up', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '20.8', }), @@ -702,6 +713,7 @@ 'context': , 'entity_id': 'sensor.sfr_box_dsl_rate_down', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '5549', }), @@ -715,6 +727,7 @@ 'context': , 'entity_id': 'sensor.sfr_box_dsl_rate_up', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '187', }), @@ -734,6 +747,7 @@ 'context': , 'entity_id': 'sensor.sfr_box_dsl_line_status', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'no_defect', }), @@ -757,6 +771,7 @@ 'context': , 'entity_id': 'sensor.sfr_box_dsl_training', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'showtime', }), diff --git a/tests/components/sfr_box/test_binary_sensor.py b/tests/components/sfr_box/test_binary_sensor.py index 65f3c8f8c0e..f3d012712ca 100644 --- a/tests/components/sfr_box/test_binary_sensor.py +++ b/tests/components/sfr_box/test_binary_sensor.py @@ -1,4 +1,5 @@ """Test the SFR Box binary sensors.""" + from collections.abc import Generator from unittest.mock import patch diff --git a/tests/components/sfr_box/test_button.py b/tests/components/sfr_box/test_button.py index 5a833056291..618ad6fc34b 100644 --- a/tests/components/sfr_box/test_button.py +++ b/tests/components/sfr_box/test_button.py @@ -1,4 +1,5 @@ """Test the SFR Box buttons.""" + from collections.abc import Generator from unittest.mock import patch @@ -19,9 +20,12 @@ pytestmark = pytest.mark.usefixtures("system_get_info", "dsl_get_info", "wan_get @pytest.fixture(autouse=True) def override_platforms() -> Generator[None, None, None]: """Override PLATFORMS_WITH_AUTH.""" - with patch( - "homeassistant.components.sfr_box.PLATFORMS_WITH_AUTH", [Platform.BUTTON] - ), patch("homeassistant.components.sfr_box.coordinator.SFRBox.authenticate"): + with ( + patch( + "homeassistant.components.sfr_box.PLATFORMS_WITH_AUTH", [Platform.BUTTON] + ), + patch("homeassistant.components.sfr_box.coordinator.SFRBox.authenticate"), + ): yield @@ -72,10 +76,13 @@ async def test_reboot(hass: HomeAssistant, config_entry_with_auth: ConfigEntry) # Reboot failed service_data = {ATTR_ENTITY_ID: "button.sfr_box_restart"} - with patch( - "homeassistant.components.sfr_box.button.SFRBox.system_reboot", - side_effect=SFRBoxError, - ) as mock_action, pytest.raises(HomeAssistantError): + with ( + patch( + "homeassistant.components.sfr_box.button.SFRBox.system_reboot", + side_effect=SFRBoxError, + ) as mock_action, + pytest.raises(HomeAssistantError), + ): await hass.services.async_call( BUTTON_DOMAIN, SERVICE_PRESS, service_data=service_data, blocking=True ) diff --git a/tests/components/sfr_box/test_config_flow.py b/tests/components/sfr_box/test_config_flow.py index c8130d5d617..282d7dbbb4c 100644 --- a/tests/components/sfr_box/test_config_flow.py +++ b/tests/components/sfr_box/test_config_flow.py @@ -1,4 +1,5 @@ """Test the SFR Box config flow.""" + import json from unittest.mock import AsyncMock, patch diff --git a/tests/components/sfr_box/test_diagnostics.py b/tests/components/sfr_box/test_diagnostics.py index a433236ab7a..512a737d434 100644 --- a/tests/components/sfr_box/test_diagnostics.py +++ b/tests/components/sfr_box/test_diagnostics.py @@ -1,4 +1,5 @@ """Test the SFR Box diagnostics.""" + from collections.abc import Generator from unittest.mock import patch diff --git a/tests/components/sfr_box/test_init.py b/tests/components/sfr_box/test_init.py index df4d1242e02..4bcd4ae9208 100644 --- a/tests/components/sfr_box/test_init.py +++ b/tests/components/sfr_box/test_init.py @@ -1,4 +1,5 @@ """Test the SFR Box setup process.""" + from collections.abc import Generator from unittest.mock import patch diff --git a/tests/components/sfr_box/test_sensor.py b/tests/components/sfr_box/test_sensor.py index c374837c5a7..afdcf87b9db 100644 --- a/tests/components/sfr_box/test_sensor.py +++ b/tests/components/sfr_box/test_sensor.py @@ -1,4 +1,5 @@ """Test the SFR Box sensors.""" + from collections.abc import Generator from unittest.mock import patch @@ -45,7 +46,7 @@ async def test_sensors( # Some entities are disabled, enable them and reload before checking states for ent in entity_entries: - entity_registry.async_update_entity(ent.entity_id, **{"disabled_by": None}) + entity_registry.async_update_entity(ent.entity_id, disabled_by=None) await hass.config_entries.async_reload(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/sharkiq/test_config_flow.py b/tests/components/sharkiq/test_config_flow.py index a98eff6f2bb..a81c185fd71 100644 --- a/tests/components/sharkiq/test_config_flow.py +++ b/tests/components/sharkiq/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Shark IQ config flow.""" + from unittest.mock import patch import aiohttp @@ -43,10 +44,13 @@ async def test_form(hass: HomeAssistant) -> None: assert result["type"] == "form" assert result["errors"] == {} - with patch("sharkiq.AylaApi.async_sign_in", return_value=True), patch( - "homeassistant.components.sharkiq.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch("sharkiq.AylaApi.async_sign_in", return_value=True), + patch( + "homeassistant.components.sharkiq.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], CONFIG, diff --git a/tests/components/sharkiq/test_vacuum.py b/tests/components/sharkiq/test_vacuum.py index 34b49f5d581..4a1671a616f 100644 --- a/tests/components/sharkiq/test_vacuum.py +++ b/tests/components/sharkiq/test_vacuum.py @@ -1,4 +1,5 @@ """Test the Shark IQ vacuum entity.""" + from __future__ import annotations from collections.abc import Iterable diff --git a/tests/components/shell_command/test_init.py b/tests/components/shell_command/test_init.py index b0c0680c905..93b06ddf9d8 100644 --- a/tests/components/shell_command/test_init.py +++ b/tests/components/shell_command/test_init.py @@ -1,4 +1,5 @@ """The tests for the Shell command component.""" + from __future__ import annotations import asyncio @@ -250,9 +251,12 @@ async def test_do_not_run_forever( ) await hass.async_block_till_done() - with patch.object(shell_command, "COMMAND_TIMEOUT", 0.001), patch( - "homeassistant.components.shell_command.asyncio.create_subprocess_shell", - side_effect=mock_create_subprocess_shell, + with ( + patch.object(shell_command, "COMMAND_TIMEOUT", 0.001), + patch( + "homeassistant.components.shell_command.asyncio.create_subprocess_shell", + side_effect=mock_create_subprocess_shell, + ), ): with pytest.raises(TimeoutError): await hass.services.async_call( diff --git a/tests/components/shelly/__init__.py b/tests/components/shelly/__init__.py index 266e2de4920..348b1115a6f 100644 --- a/tests/components/shelly/__init__.py +++ b/tests/components/shelly/__init__.py @@ -1,4 +1,5 @@ """Tests for the Shelly integration.""" + from collections.abc import Mapping from copy import deepcopy from datetime import timedelta @@ -125,6 +126,18 @@ def register_entity( return f"{domain}.{object_id}" +def get_entity( + hass: HomeAssistant, + domain: str, + unique_id: str, +) -> str | None: + """Get Shelly entity.""" + entity_registry = async_get(hass) + return entity_registry.async_get_entity_id( + domain, DOMAIN, f"{MOCK_MAC}-{unique_id}" + ) + + def get_entity_state(hass: HomeAssistant, entity_id: str) -> str: """Return entity state.""" entity = hass.states.get(entity_id) diff --git a/tests/components/shelly/bluetooth/__init__.py b/tests/components/shelly/bluetooth/__init__.py index a4b1f4cdb7e..14588d5762c 100644 --- a/tests/components/shelly/bluetooth/__init__.py +++ b/tests/components/shelly/bluetooth/__init__.py @@ -1,2 +1,3 @@ """Bluetooth tests for Shelly integration.""" + from __future__ import annotations diff --git a/tests/components/shelly/bluetooth/test_scanner.py b/tests/components/shelly/bluetooth/test_scanner.py index d9ec0064606..c7bbb5cb708 100644 --- a/tests/components/shelly/bluetooth/test_scanner.py +++ b/tests/components/shelly/bluetooth/test_scanner.py @@ -1,4 +1,5 @@ """Test the shelly bluetooth scanner.""" + from __future__ import annotations from aioshelly.ble.const import BLE_SCAN_RESULT_EVENT diff --git a/tests/components/shelly/conftest.py b/tests/components/shelly/conftest.py index 1d4a00f34ca..3cd27101f76 100644 --- a/tests/components/shelly/conftest.py +++ b/tests/components/shelly/conftest.py @@ -1,4 +1,5 @@ """Test configuration for Shelly.""" + from unittest.mock import AsyncMock, Mock, PropertyMock, patch from aioshelly.block_device import BlockDevice, BlockUpdateType @@ -168,6 +169,11 @@ MOCK_CONFIG = { "input:1": {"id": 1, "type": "analog", "enable": True}, "input:2": {"id": 2, "name": "Gas", "type": "count", "enable": True}, "light:0": {"name": "test light_0"}, + "light:1": {"name": "test light_1"}, + "light:2": {"name": "test light_2"}, + "light:3": {"name": "test light_3"}, + "rgb:0": {"name": "test rgb_0"}, + "rgbw:0": {"name": "test rgbw_0"}, "switch:0": {"name": "test switch_0"}, "cover:0": {"name": "test cover_0"}, "thermostat:0": { @@ -222,6 +228,11 @@ MOCK_STATUS_RPC = { "input:1": {"id": 1, "percent": 89, "xpercent": 8.9}, "input:2": {"id": 2, "counts": {"total": 56174, "xtotal": 561.74}}, "light:0": {"output": True, "brightness": 53.0}, + "light:1": {"output": True, "brightness": 53.0}, + "light:2": {"output": True, "brightness": 53.0}, + "light:3": {"output": True, "brightness": 53.0}, + "rgb:0": {"output": True, "brightness": 53.0, "rgb": [45, 55, 65]}, + "rgbw:0": {"output": True, "brightness": 53.0, "rgb": [21, 22, 23], "white": 120}, "cloud": {"connected": False}, "cover:0": { "state": "stopped", @@ -350,8 +361,9 @@ def _mock_rpc_device(version: str | None = None): @pytest.fixture async def mock_rpc_device(): """Mock rpc (Gen2, Websocket) device with BLE support.""" - with patch("aioshelly.rpc_device.RpcDevice.create") as rpc_device_mock, patch( - "homeassistant.components.shelly.bluetooth.async_start_scanner" + with ( + patch("aioshelly.rpc_device.RpcDevice.create") as rpc_device_mock, + patch("homeassistant.components.shelly.bluetooth.async_start_scanner"), ): def update(): diff --git a/tests/components/shelly/test_binary_sensor.py b/tests/components/shelly/test_binary_sensor.py index db1f27b0c1a..00a430cd4b1 100644 --- a/tests/components/shelly/test_binary_sensor.py +++ b/tests/components/shelly/test_binary_sensor.py @@ -1,4 +1,5 @@ """Tests for Shelly binary sensor platform.""" + from unittest.mock import Mock from aioshelly.const import MODEL_MOTION diff --git a/tests/components/shelly/test_button.py b/tests/components/shelly/test_button.py index 33b11d0e045..14349411670 100644 --- a/tests/components/shelly/test_button.py +++ b/tests/components/shelly/test_button.py @@ -1,4 +1,5 @@ """Tests for Shelly button platform.""" + from unittest.mock import Mock import pytest diff --git a/tests/components/shelly/test_climate.py b/tests/components/shelly/test_climate.py index f435d337537..7e0e2d1ce46 100644 --- a/tests/components/shelly/test_climate.py +++ b/tests/components/shelly/test_climate.py @@ -1,4 +1,5 @@ """Tests for Shelly climate platform.""" + from copy import deepcopy from unittest.mock import AsyncMock, Mock, PropertyMock diff --git a/tests/components/shelly/test_config_flow.py b/tests/components/shelly/test_config_flow.py index 50e42f71b33..1e7bbc01d6d 100644 --- a/tests/components/shelly/test_config_flow.py +++ b/tests/components/shelly/test_config_flow.py @@ -1,11 +1,14 @@ """Test the Shelly config flow.""" + from dataclasses import replace +from datetime import timedelta from ipaddress import ip_address from typing import Any from unittest.mock import AsyncMock, Mock, patch -from aioshelly.const import MODEL_1, MODEL_PLUS_2PM +from aioshelly.const import DEFAULT_HTTP_PORT, MODEL_1, MODEL_PLUS_2PM from aioshelly.exceptions import ( + CustomPortNotSupported, DeviceConnectionError, FirmwareUnsupported, InvalidAuthError, @@ -20,13 +23,15 @@ from homeassistant.components.shelly.const import ( DOMAIN, BLEScannerMode, ) +from homeassistant.components.shelly.coordinator import ENTRY_RELOAD_COOLDOWN from homeassistant.config_entries import SOURCE_REAUTH from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util from . import init_integration -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_fire_time_changed from tests.typing import WebSocketGenerator DISCOVERY_INFO = zeroconf.ZeroconfServiceInfo( @@ -50,17 +55,18 @@ DISCOVERY_INFO_WITH_MAC = zeroconf.ZeroconfServiceInfo( @pytest.mark.parametrize( - ("gen", "model"), + ("gen", "model", "port"), [ - (1, MODEL_1), - (2, MODEL_PLUS_2PM), - (3, MODEL_PLUS_2PM), + (1, MODEL_1, DEFAULT_HTTP_PORT), + (2, MODEL_PLUS_2PM, DEFAULT_HTTP_PORT), + (3, MODEL_PLUS_2PM, 11200), ], ) async def test_form( hass: HomeAssistant, gen: int, model: str, + port: int, mock_block_device: Mock, mock_rpc_device: Mock, ) -> None: @@ -68,21 +74,31 @@ async def test_form( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} - with patch( - "homeassistant.components.shelly.config_flow.get_info", - return_value={"mac": "test-mac", "type": MODEL_1, "auth": False, "gen": gen}, - ), patch( - "homeassistant.components.shelly.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.shelly.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.shelly.config_flow.get_info", + return_value={ + "mac": "test-mac", + "type": MODEL_1, + "auth": False, + "gen": gen, + "port": port, + }, + ), + patch( + "homeassistant.components.shelly.async_setup", return_value=True + ) as mock_setup, + patch( + "homeassistant.components.shelly.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"}, + {"host": "1.1.1.1", "port": port}, ) await hass.async_block_till_done() @@ -90,6 +106,7 @@ async def test_form( assert result2["title"] == "Test name" assert result2["data"] == { "host": "1.1.1.1", + "port": port, "model": model, "sleep_period": 0, "gen": gen, @@ -98,6 +115,36 @@ async def test_form( assert len(mock_setup_entry.mock_calls) == 1 +async def test_form_gen1_custom_port( + hass: HomeAssistant, + mock_block_device: Mock, +) -> None: + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["errors"] == {} + + with ( + patch( + "homeassistant.components.shelly.config_flow.get_info", + return_value={"mac": "test-mac", "type": MODEL_1, "gen": 1}, + ), + patch( + "aioshelly.block_device.BlockDevice.create", + side_effect=CustomPortNotSupported, + ), + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"host": "1.1.1.1", "port": "1100"}, + ) + + assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["errors"]["base"] == "custom_port_not_supported" + + @pytest.mark.parametrize( ("gen", "model", "user_input", "username"), [ @@ -149,12 +196,15 @@ async def test_form_auth( assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} - with patch( - "homeassistant.components.shelly.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.shelly.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.shelly.async_setup", return_value=True + ) as mock_setup, + patch( + "homeassistant.components.shelly.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], user_input ) @@ -164,6 +214,7 @@ async def test_form_auth( assert result3["title"] == "Test name" assert result3["data"] == { "host": "1.1.1.1", + "port": DEFAULT_HTTP_PORT, "model": model, "sleep_period": 0, "gen": gen, @@ -292,11 +343,14 @@ async def test_form_errors_test_connection( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch( - "homeassistant.components.shelly.config_flow.get_info", - return_value={"mac": "test-mac", "auth": False}, - ), patch( - "aioshelly.block_device.BlockDevice.create", new=AsyncMock(side_effect=exc) + with ( + patch( + "homeassistant.components.shelly.config_flow.get_info", + return_value={"mac": "test-mac", "auth": False}, + ), + patch( + "aioshelly.block_device.BlockDevice.create", new=AsyncMock(side_effect=exc) + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -352,15 +406,19 @@ async def test_user_setup_ignored_device( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch( - "homeassistant.components.shelly.config_flow.get_info", - return_value={"mac": "test-mac", "type": MODEL_1, "auth": False}, - ), patch( - "homeassistant.components.shelly.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.shelly.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.shelly.config_flow.get_info", + return_value={"mac": "test-mac", "type": MODEL_1, "auth": False}, + ), + patch( + "homeassistant.components.shelly.async_setup", return_value=True + ) as mock_setup, + patch( + "homeassistant.components.shelly.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"}, @@ -513,12 +571,15 @@ async def test_zeroconf( ) assert context["title_placeholders"]["name"] == "shelly1pm-12345" assert context["confirm_only"] is True - with patch( - "homeassistant.components.shelly.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.shelly.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.shelly.async_setup", return_value=True + ) as mock_setup, + patch( + "homeassistant.components.shelly.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {}, @@ -568,12 +629,15 @@ async def test_zeroconf_sleeping_device( if flow["flow_id"] == result["flow_id"] ) assert context["title_placeholders"]["name"] == "shelly1pm-12345" - with patch( - "homeassistant.components.shelly.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.shelly.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.shelly.async_setup", return_value=True + ) as mock_setup, + patch( + "homeassistant.components.shelly.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {}, @@ -594,17 +658,20 @@ async def test_zeroconf_sleeping_device( async def test_zeroconf_sleeping_device_error(hass: HomeAssistant) -> None: """Test sleeping device configuration via zeroconf with error.""" - with patch( - "homeassistant.components.shelly.config_flow.get_info", - return_value={ - "mac": "test-mac", - "type": MODEL_1, - "auth": False, - "sleep_mode": True, - }, - ), patch( - "aioshelly.block_device.BlockDevice.create", - new=AsyncMock(side_effect=DeviceConnectionError), + with ( + patch( + "homeassistant.components.shelly.config_flow.get_info", + return_value={ + "mac": "test-mac", + "type": MODEL_1, + "auth": False, + "sleep_mode": True, + }, + ), + patch( + "aioshelly.block_device.BlockDevice.create", + new=AsyncMock(side_effect=DeviceConnectionError), + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -737,12 +804,15 @@ async def test_zeroconf_require_auth( assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} - with patch( - "homeassistant.components.shelly.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.shelly.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.shelly.async_setup", return_value=True + ) as mock_setup, + patch( + "homeassistant.components.shelly.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"username": "test username", "password": "test password"}, @@ -753,6 +823,7 @@ async def test_zeroconf_require_auth( assert result2["title"] == "Test name" assert result2["data"] == { "host": "1.1.1.1", + "port": DEFAULT_HTTP_PORT, "model": MODEL_1, "sleep_period": 0, "gen": 1, @@ -823,15 +894,19 @@ async def test_reauth_unsuccessful( ) entry.add_to_hass(hass) - with patch( - "homeassistant.components.shelly.config_flow.get_info", - return_value={"mac": "test-mac", "type": MODEL_1, "auth": True, "gen": gen}, - ), patch( - "aioshelly.block_device.BlockDevice.create", - new=AsyncMock(side_effect=InvalidAuthError), - ), patch( - "aioshelly.rpc_device.RpcDevice.create", - new=AsyncMock(side_effect=InvalidAuthError), + with ( + patch( + "homeassistant.components.shelly.config_flow.get_info", + return_value={"mac": "test-mac", "type": MODEL_1, "auth": True, "gen": gen}, + ), + patch( + "aioshelly.block_device.BlockDevice.create", + new=AsyncMock(side_effect=InvalidAuthError), + ), + patch( + "aioshelly.rpc_device.RpcDevice.create", + new=AsyncMock(side_effect=InvalidAuthError), + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -1029,6 +1104,9 @@ async def test_zeroconf_already_configured_triggers_refresh_mac_in_name( monkeypatch.setattr(mock_rpc_device, "connected", False) mock_rpc_device.mock_disconnected() + async_fire_time_changed( + hass, dt_util.utcnow() + timedelta(seconds=ENTRY_RELOAD_COOLDOWN) + ) await hass.async_block_till_done() assert len(mock_rpc_device.initialize.mock_calls) == 2 @@ -1061,6 +1139,9 @@ async def test_zeroconf_already_configured_triggers_refresh( monkeypatch.setattr(mock_rpc_device, "connected", False) mock_rpc_device.mock_disconnected() + async_fire_time_changed( + hass, dt_util.utcnow() + timedelta(seconds=ENTRY_RELOAD_COOLDOWN) + ) await hass.async_block_till_done() assert len(mock_rpc_device.initialize.mock_calls) == 2 @@ -1082,6 +1163,7 @@ async def test_zeroconf_sleeping_device_not_triggers_refresh( await hass.async_block_till_done() mock_rpc_device.mock_update() + await hass.async_block_till_done() assert "online, resuming setup" in caplog.text @@ -1099,6 +1181,9 @@ async def test_zeroconf_sleeping_device_not_triggers_refresh( monkeypatch.setattr(mock_rpc_device, "connected", False) mock_rpc_device.mock_disconnected() + async_fire_time_changed( + hass, dt_util.utcnow() + timedelta(seconds=ENTRY_RELOAD_COOLDOWN) + ) await hass.async_block_till_done() assert len(mock_rpc_device.initialize.mock_calls) == 0 assert "device did not update" not in caplog.text @@ -1112,15 +1197,19 @@ async def test_sleeping_device_gen2_with_new_firmware( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} - with patch( - "homeassistant.components.shelly.config_flow.get_info", - return_value={"mac": "test-mac", "gen": 2}, - ), patch("homeassistant.components.shelly.async_setup", return_value=True), patch( - "homeassistant.components.shelly.async_setup_entry", - return_value=True, + with ( + patch( + "homeassistant.components.shelly.config_flow.get_info", + return_value={"mac": "test-mac", "gen": 2}, + ), + patch("homeassistant.components.shelly.async_setup", return_value=True), + patch( + "homeassistant.components.shelly.async_setup_entry", + return_value=True, + ), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -1130,6 +1219,7 @@ async def test_sleeping_device_gen2_with_new_firmware( assert result["data"] == { "host": "1.1.1.1", + "port": DEFAULT_HTTP_PORT, "model": MODEL_PLUS_2PM, "sleep_period": 666, "gen": 2, diff --git a/tests/components/shelly/test_coordinator.py b/tests/components/shelly/test_coordinator.py index 940ab2123f0..c16f78b83ff 100644 --- a/tests/components/shelly/test_coordinator.py +++ b/tests/components/shelly/test_coordinator.py @@ -1,4 +1,5 @@ """Tests for Shelly coordinator.""" + from datetime import timedelta from unittest.mock import AsyncMock, Mock, patch diff --git a/tests/components/shelly/test_cover.py b/tests/components/shelly/test_cover.py index 890bdaf8619..cd5efb76cfe 100644 --- a/tests/components/shelly/test_cover.py +++ b/tests/components/shelly/test_cover.py @@ -1,4 +1,5 @@ """Tests for Shelly cover platform.""" + from unittest.mock import Mock import pytest diff --git a/tests/components/shelly/test_device_trigger.py b/tests/components/shelly/test_device_trigger.py index d6f48a03eab..c4db8acaf6d 100644 --- a/tests/components/shelly/test_device_trigger.py +++ b/tests/components/shelly/test_device_trigger.py @@ -1,4 +1,5 @@ """The tests for Shelly device triggers.""" + from unittest.mock import Mock from aioshelly.const import MODEL_BUTTON1 diff --git a/tests/components/shelly/test_diagnostics.py b/tests/components/shelly/test_diagnostics.py index d65244ee0b7..f7f238f3327 100644 --- a/tests/components/shelly/test_diagnostics.py +++ b/tests/components/shelly/test_diagnostics.py @@ -1,4 +1,5 @@ """Tests for Shelly diagnostics platform.""" + from unittest.mock import ANY, Mock from aioshelly.ble.const import BLE_SCAN_RESULT_EVENT diff --git a/tests/components/shelly/test_event.py b/tests/components/shelly/test_event.py index d7319811748..2465b016808 100644 --- a/tests/components/shelly/test_event.py +++ b/tests/components/shelly/test_event.py @@ -1,4 +1,5 @@ """Tests for Shelly button platform.""" + from unittest.mock import Mock from aioshelly.const import MODEL_I3 diff --git a/tests/components/shelly/test_init.py b/tests/components/shelly/test_init.py index 79115354299..754f1111548 100644 --- a/tests/components/shelly/test_init.py +++ b/tests/components/shelly/test_init.py @@ -1,6 +1,11 @@ """Test cases for the Shelly component.""" -from unittest.mock import AsyncMock, Mock, patch +from ipaddress import IPv4Address +from unittest.mock import AsyncMock, Mock, call, patch + +from aioshelly.block_device import COAP +from aioshelly.common import ConnectionOptions +from aioshelly.const import MODEL_PLUS_2PM from aioshelly.exceptions import ( DeviceConnectionError, FirmwareUnsupported, @@ -13,13 +18,14 @@ from homeassistant.components.shelly.const import ( BLOCK_EXPECTED_SLEEP_PERIOD, BLOCK_WRONG_SLEEP_PERIOD, CONF_BLE_SCANNER_MODE, + CONF_GEN, CONF_SLEEP_PERIOD, DOMAIN, MODELS_WITH_WRONG_SLEEP_PERIOD, BLEScannerMode, ) from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState -from homeassistant.const import STATE_ON, STATE_UNAVAILABLE +from homeassistant.const import CONF_HOST, CONF_PORT, STATE_ON, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import ( CONNECTION_NETWORK_MAC, @@ -48,6 +54,63 @@ async def test_custom_coap_port( assert "Starting CoAP context with UDP port 7632" in caplog.text +async def test_ip_address_with_only_default_interface( + hass: HomeAssistant, mock_block_device: Mock, caplog: pytest.LogCaptureFixture +) -> None: + """Test more local ip addresses with only the default interface..""" + with ( + patch( + "homeassistant.components.network.async_only_default_interface_enabled", + return_value=True, + ), + patch( + "homeassistant.components.network.async_get_enabled_source_ips", + return_value=[IPv4Address("192.168.1.10"), IPv4Address("10.10.10.10")], + ), + patch( + "homeassistant.components.shelly.utils.COAP", + autospec=COAP, + ) as mock_coap_init, + ): + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {"coap_port": 7632}}) + await hass.async_block_till_done() + + await init_integration(hass, 1) + assert "Starting CoAP context with UDP port 7632" in caplog.text + # Make sure COAP.initialize is called with an empty list + # when async_only_default_interface_enabled is True even if + # async_get_enabled_source_ips returns more than one address + assert mock_coap_init.mock_calls[1] == call().initialize(7632, []) + + +async def test_ip_address_without_only_default_interface( + hass: HomeAssistant, mock_block_device: Mock, caplog: pytest.LogCaptureFixture +) -> None: + """Test more local ip addresses without only the default interface..""" + with ( + patch( + "homeassistant.components.network.async_only_default_interface_enabled", + return_value=False, + ), + patch( + "homeassistant.components.network.async_get_enabled_source_ips", + return_value=[IPv4Address("192.168.1.10"), IPv4Address("10.10.10.10")], + ), + patch( + "homeassistant.components.shelly.utils.COAP", + autospec=COAP, + ) as mock_coap_init, + ): + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {"coap_port": 7632}}) + await hass.async_block_till_done() + + await init_integration(hass, 1) + assert "Starting CoAP context with UDP port 7632" in caplog.text + assert mock_coap_init.mock_calls[1] == call().initialize( + 7632, [IPv4Address("192.168.1.10"), IPv4Address("10.10.10.10")] + ) + + @pytest.mark.parametrize("gen", [1, 2, 3]) async def test_shared_device_mac( hass: HomeAssistant, @@ -340,6 +403,49 @@ async def test_entry_missing_gen(hass: HomeAssistant, mock_block_device: Mock) - assert hass.states.get("switch.test_name_channel_1").state is STATE_ON +async def test_entry_missing_port(hass: HomeAssistant) -> None: + """Test successful Gen2 device init when port is missing in entry data.""" + data = { + CONF_HOST: "192.168.1.37", + CONF_SLEEP_PERIOD: 0, + "model": MODEL_PLUS_2PM, + CONF_GEN: 2, + } + entry = MockConfigEntry(domain=DOMAIN, data=data, unique_id=MOCK_MAC) + entry.add_to_hass(hass) + with patch( + "homeassistant.components.shelly.RpcDevice.create", return_value=Mock() + ) as rpc_device_mock: + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert rpc_device_mock.call_args[0][2] == ConnectionOptions( + ip_address="192.168.1.37", device_mac="123456789ABC", port=80 + ) + + +async def test_rpc_entry_custom_port(hass: HomeAssistant) -> None: + """Test successful Gen2 device init using custom port.""" + data = { + CONF_HOST: "192.168.1.37", + CONF_SLEEP_PERIOD: 0, + "model": MODEL_PLUS_2PM, + CONF_GEN: 2, + CONF_PORT: 8001, + } + entry = MockConfigEntry(domain=DOMAIN, data=data, unique_id=MOCK_MAC) + entry.add_to_hass(hass) + with patch( + "homeassistant.components.shelly.RpcDevice.create", return_value=Mock() + ) as rpc_device_mock: + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert rpc_device_mock.call_args[0][2] == ConnectionOptions( + ip_address="192.168.1.37", device_mac="123456789ABC", port=8001 + ) + + @pytest.mark.parametrize(("model"), MODELS_WITH_WRONG_SLEEP_PERIOD) async def test_sleeping_block_device_wrong_sleep_period( hass: HomeAssistant, mock_block_device: Mock, model: str diff --git a/tests/components/shelly/test_light.py b/tests/components/shelly/test_light.py index f484f526e3f..2c464a8c39c 100644 --- a/tests/components/shelly/test_light.py +++ b/tests/components/shelly/test_light.py @@ -1,4 +1,5 @@ """Tests for Shelly light platform.""" + from unittest.mock import AsyncMock, Mock from aioshelly.const import ( @@ -28,6 +29,7 @@ from homeassistant.components.light import ( ColorMode, LightEntityFeature, ) +from homeassistant.components.shelly.const import SHELLY_PLUS_RGBW_CHANNELS from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, @@ -37,7 +39,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_registry import EntityRegistry -from . import init_integration, mutate_rpc_device_status +from . import get_entity, init_integration, mutate_rpc_device_status, register_entity from .conftest import mock_white_light_set_state RELAY_BLOCK_ID = 0 @@ -538,6 +540,218 @@ async def test_rpc_light( assert state.state == STATE_ON assert state.attributes[ATTR_BRIGHTNESS] == 33 + # Turn on, transition = 10.1 + mock_rpc_device.call_rpc.reset_mock() + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id, ATTR_TRANSITION: 10.1}, + blocking=True, + ) + + mock_rpc_device.mock_update() + + mock_rpc_device.call_rpc.assert_called_once_with( + "Light.Set", {"id": 0, "on": True, "transition_duration": 10.1} + ) + state = hass.states.get(entity_id) + assert state.state == STATE_ON + + # Turn off, transition = 0.4, should be limited to 0.5 + mock_rpc_device.call_rpc.reset_mock() + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: entity_id, ATTR_TRANSITION: 0.4}, + blocking=True, + ) + + mutate_rpc_device_status(monkeypatch, mock_rpc_device, "light:0", "output", False) + mock_rpc_device.mock_update() + + mock_rpc_device.call_rpc.assert_called_once_with( + "Light.Set", {"id": 0, "on": False, "transition_duration": 0.5} + ) + + state = hass.states.get(entity_id) + assert state.state == STATE_OFF + entry = entity_registry.async_get(entity_id) assert entry assert entry.unique_id == "123456789ABC-light:0" + + +async def test_rpc_device_rgb_profile( + hass: HomeAssistant, + mock_rpc_device: Mock, + entity_registry: EntityRegistry, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Test RPC device in RGB profile.""" + for i in range(SHELLY_PLUS_RGBW_CHANNELS): + monkeypatch.delitem(mock_rpc_device.status, f"light:{i}") + monkeypatch.delitem(mock_rpc_device.status, "rgbw:0") + entity_id = "light.test_rgb_0" + await init_integration(hass, 2) + + # Test initial + state = hass.states.get(entity_id) + attributes = state.attributes + assert state.state == STATE_ON + assert attributes[ATTR_RGB_COLOR] == (45, 55, 65) + assert attributes[ATTR_SUPPORTED_COLOR_MODES] == [ColorMode.RGB] + assert attributes[ATTR_SUPPORTED_FEATURES] == LightEntityFeature.TRANSITION + + # Turn on, RGB = [70, 80, 90] + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id, ATTR_RGB_COLOR: [70, 80, 90]}, + blocking=True, + ) + + mutate_rpc_device_status(monkeypatch, mock_rpc_device, "rgb:0", "rgb", [70, 80, 90]) + mock_rpc_device.mock_update() + + mock_rpc_device.call_rpc.assert_called_once_with( + "RGB.Set", {"id": 0, "on": True, "rgb": [70, 80, 90]} + ) + + state = hass.states.get(entity_id) + attributes = state.attributes + assert state.state == STATE_ON + assert attributes[ATTR_COLOR_MODE] == ColorMode.RGB + assert attributes[ATTR_RGB_COLOR] == (70, 80, 90) + + entry = entity_registry.async_get(entity_id) + assert entry + assert entry.unique_id == "123456789ABC-rgb:0" + + +async def test_rpc_device_rgbw_profile( + hass: HomeAssistant, + mock_rpc_device: Mock, + entity_registry: EntityRegistry, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Test RPC device in RGBW profile.""" + for i in range(SHELLY_PLUS_RGBW_CHANNELS): + monkeypatch.delitem(mock_rpc_device.status, f"light:{i}") + monkeypatch.delitem(mock_rpc_device.status, "rgb:0") + entity_id = "light.test_rgbw_0" + await init_integration(hass, 2) + + # Test initial + state = hass.states.get(entity_id) + attributes = state.attributes + assert state.state == STATE_ON + assert attributes[ATTR_RGBW_COLOR] == (21, 22, 23, 120) + assert attributes[ATTR_SUPPORTED_COLOR_MODES] == [ColorMode.RGBW] + assert attributes[ATTR_SUPPORTED_FEATURES] == LightEntityFeature.TRANSITION + + # Turn on, RGBW = [72, 82, 92, 128] + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id, ATTR_RGBW_COLOR: [72, 82, 92, 128]}, + blocking=True, + ) + + mutate_rpc_device_status( + monkeypatch, mock_rpc_device, "rgbw:0", "rgb", [72, 82, 92] + ) + mutate_rpc_device_status(monkeypatch, mock_rpc_device, "rgbw:0", "white", 128) + mock_rpc_device.mock_update() + + mock_rpc_device.call_rpc.assert_called_once_with( + "RGBW.Set", {"id": 0, "on": True, "rgb": [72, 82, 92], "white": 128} + ) + + state = hass.states.get(entity_id) + attributes = state.attributes + assert state.state == STATE_ON + assert attributes[ATTR_COLOR_MODE] == ColorMode.RGBW + assert attributes[ATTR_RGBW_COLOR] == (72, 82, 92, 128) + + entry = entity_registry.async_get(entity_id) + assert entry + assert entry.unique_id == "123456789ABC-rgbw:0" + + +async def test_rpc_rgbw_device_light_mode_remove_others( + hass: HomeAssistant, + mock_rpc_device: Mock, + entity_registry: EntityRegistry, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Test Shelly RPC RGBW device in light mode removes RGB/RGBW entities.""" + # register lights + monkeypatch.delitem(mock_rpc_device.status, "rgb:0") + monkeypatch.delitem(mock_rpc_device.status, "rgbw:0") + register_entity(hass, LIGHT_DOMAIN, "test_rgb_0", "rgb:0") + register_entity(hass, LIGHT_DOMAIN, "test_rgbw_0", "rgbw:0") + + # verify RGB & RGBW entities created + assert get_entity(hass, LIGHT_DOMAIN, "rgb:0") is not None + assert get_entity(hass, LIGHT_DOMAIN, "rgbw:0") is not None + + # init to remove RGB & RGBW + await init_integration(hass, 2) + + # verify we have 4 lights + for i in range(SHELLY_PLUS_RGBW_CHANNELS): + entity_id = f"light.test_light_{i}" + assert hass.states.get(entity_id).state == STATE_ON + entry = entity_registry.async_get(entity_id) + assert entry + assert entry.unique_id == f"123456789ABC-light:{i}" + + # verify RGB & RGBW entities removed + assert get_entity(hass, LIGHT_DOMAIN, "rgb:0") is None + assert get_entity(hass, LIGHT_DOMAIN, "rgbw:0") is None + + +@pytest.mark.parametrize( + ("active_mode", "removed_mode"), + [ + ("rgb", "rgbw"), + ("rgbw", "rgb"), + ], +) +async def test_rpc_rgbw_device_rgb_w_modes_remove_others( + hass: HomeAssistant, + mock_rpc_device: Mock, + entity_registry: EntityRegistry, + monkeypatch: pytest.MonkeyPatch, + active_mode: str, + removed_mode: str, +) -> None: + """Test Shelly RPC RGBW device in RGB/W modes other lights.""" + removed_key = f"{removed_mode}:0" + + # register lights + for i in range(SHELLY_PLUS_RGBW_CHANNELS): + monkeypatch.delitem(mock_rpc_device.status, f"light:{i}") + entity_id = f"light.test_light_{i}" + register_entity(hass, LIGHT_DOMAIN, entity_id, f"light:{i}") + monkeypatch.delitem(mock_rpc_device.status, f"{removed_mode}:0") + register_entity(hass, LIGHT_DOMAIN, f"test_{removed_key}", removed_key) + + # verify lights entities created + for i in range(SHELLY_PLUS_RGBW_CHANNELS): + assert get_entity(hass, LIGHT_DOMAIN, f"light:{i}") is not None + assert get_entity(hass, LIGHT_DOMAIN, removed_key) is not None + + await init_integration(hass, 2) + + # verify we have RGB/w light + entity_id = f"light.test_{active_mode}_0" + assert hass.states.get(entity_id).state == STATE_ON + entry = entity_registry.async_get(entity_id) + assert entry + assert entry.unique_id == f"123456789ABC-{active_mode}:0" + + # verify light & RGB/W entities removed + for i in range(SHELLY_PLUS_RGBW_CHANNELS): + assert get_entity(hass, LIGHT_DOMAIN, f"light:{i}") is None + assert get_entity(hass, LIGHT_DOMAIN, removed_key) is None diff --git a/tests/components/shelly/test_logbook.py b/tests/components/shelly/test_logbook.py index 35c0d5a17de..cd1714d6b26 100644 --- a/tests/components/shelly/test_logbook.py +++ b/tests/components/shelly/test_logbook.py @@ -1,4 +1,5 @@ """The tests for Shelly logbook.""" + from unittest.mock import Mock from homeassistant.components.shelly.const import ( @@ -31,6 +32,7 @@ async def test_humanify_shelly_click_event_block_device( hass.config.components.add("recorder") assert await async_setup_component(hass, "logbook", {}) + await hass.async_block_till_done() event1, event2 = mock_humanify( hass, @@ -81,6 +83,7 @@ async def test_humanify_shelly_click_event_rpc_device( hass.config.components.add("recorder") assert await async_setup_component(hass, "logbook", {}) + await hass.async_block_till_done() event1, event2 = mock_humanify( hass, diff --git a/tests/components/shelly/test_number.py b/tests/components/shelly/test_number.py index 855ac263b0b..ecc6d7410bf 100644 --- a/tests/components/shelly/test_number.py +++ b/tests/components/shelly/test_number.py @@ -1,4 +1,5 @@ """Tests for Shelly number platform.""" + from unittest.mock import AsyncMock, Mock from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError diff --git a/tests/components/shelly/test_sensor.py b/tests/components/shelly/test_sensor.py index 03bcc545d15..0a15b78994b 100644 --- a/tests/components/shelly/test_sensor.py +++ b/tests/components/shelly/test_sensor.py @@ -1,4 +1,5 @@ """Tests for Shelly sensor platform.""" + from copy import deepcopy from unittest.mock import Mock diff --git a/tests/components/shelly/test_switch.py b/tests/components/shelly/test_switch.py index e3ba9c9da73..a57a9890921 100644 --- a/tests/components/shelly/test_switch.py +++ b/tests/components/shelly/test_switch.py @@ -1,4 +1,5 @@ """Tests for Shelly switch platform.""" + from copy import deepcopy from unittest.mock import AsyncMock, Mock diff --git a/tests/components/shelly/test_update.py b/tests/components/shelly/test_update.py index b3a4ed5f703..f3960620a21 100644 --- a/tests/components/shelly/test_update.py +++ b/tests/components/shelly/test_update.py @@ -1,4 +1,5 @@ """Tests for Shelly update platform.""" + from unittest.mock import AsyncMock, Mock from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError, RpcCallError @@ -254,6 +255,16 @@ async def test_rpc_update( {ATTR_ENTITY_ID: entity_id}, blocking=True, ) + + assert mock_rpc_device.trigger_ota_update.call_count == 1 + + state = hass.states.get(entity_id) + assert state.state == STATE_ON + assert state.attributes[ATTR_INSTALLED_VERSION] == "1" + assert state.attributes[ATTR_LATEST_VERSION] == "2" + assert state.attributes[ATTR_IN_PROGRESS] is True + assert state.attributes[ATTR_RELEASE_URL] == GEN2_RELEASE_URL + inject_rpc_device_event( monkeypatch, mock_rpc_device, @@ -269,14 +280,7 @@ async def test_rpc_update( }, ) - assert mock_rpc_device.trigger_ota_update.call_count == 1 - - state = hass.states.get(entity_id) - assert state.state == STATE_ON - assert state.attributes[ATTR_INSTALLED_VERSION] == "1" - assert state.attributes[ATTR_LATEST_VERSION] == "2" - assert state.attributes[ATTR_IN_PROGRESS] == 0 - assert state.attributes[ATTR_RELEASE_URL] == GEN2_RELEASE_URL + assert hass.states.get(entity_id).attributes[ATTR_IN_PROGRESS] == 0 inject_rpc_device_event( monkeypatch, diff --git a/tests/components/shelly/test_utils.py b/tests/components/shelly/test_utils.py index 73cb7e83fdd..7c4ea8accae 100644 --- a/tests/components/shelly/test_utils.py +++ b/tests/components/shelly/test_utils.py @@ -1,4 +1,5 @@ """Tests for Shelly utils.""" + from typing import Any from unittest.mock import Mock diff --git a/tests/components/shelly/test_valve.py b/tests/components/shelly/test_valve.py index 1f9d2f76399..b588cd28906 100644 --- a/tests/components/shelly/test_valve.py +++ b/tests/components/shelly/test_valve.py @@ -1,4 +1,5 @@ """Tests for Shelly valve platform.""" + from unittest.mock import Mock from aioshelly.const import MODEL_GAS diff --git a/tests/components/shopping_list/conftest.py b/tests/components/shopping_list/conftest.py index aec55362d0b..dd1b690e1e3 100644 --- a/tests/components/shopping_list/conftest.py +++ b/tests/components/shopping_list/conftest.py @@ -1,4 +1,5 @@ """Shopping list test helpers.""" + from unittest.mock import patch import pytest @@ -12,8 +13,9 @@ from tests.common import MockConfigEntry @pytest.fixture(autouse=True) def mock_shopping_list_io(): """Stub out the persistence.""" - with patch("homeassistant.components.shopping_list.ShoppingData.save"), patch( - "homeassistant.components.shopping_list.ShoppingData.async_load" + with ( + patch("homeassistant.components.shopping_list.ShoppingData.save"), + patch("homeassistant.components.shopping_list.ShoppingData.async_load"), ): yield diff --git a/tests/components/shopping_list/test_config_flow.py b/tests/components/shopping_list/test_config_flow.py index 34d74d18046..1d807e87ca2 100644 --- a/tests/components/shopping_list/test_config_flow.py +++ b/tests/components/shopping_list/test_config_flow.py @@ -1,4 +1,5 @@ """Test config flow.""" + from homeassistant.components.shopping_list.const import DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER from homeassistant.core import HomeAssistant diff --git a/tests/components/shopping_list/test_init.py b/tests/components/shopping_list/test_init.py index a28b1ee0cfb..c28ea66a32b 100644 --- a/tests/components/shopping_list/test_init.py +++ b/tests/components/shopping_list/test_init.py @@ -1,4 +1,5 @@ """Test shopping list component.""" + from http import HTTPStatus import pytest diff --git a/tests/components/shopping_list/test_intent.py b/tests/components/shopping_list/test_intent.py index 50c698def5d..07128835b6a 100644 --- a/tests/components/shopping_list/test_intent.py +++ b/tests/components/shopping_list/test_intent.py @@ -1,4 +1,5 @@ """Test Shopping List intents.""" + from homeassistant.core import HomeAssistant from homeassistant.helpers import intent diff --git a/tests/components/shopping_list/test_todo.py b/tests/components/shopping_list/test_todo.py index 373c449497c..fb6f61d4edf 100644 --- a/tests/components/shopping_list/test_todo.py +++ b/tests/components/shopping_list/test_todo.py @@ -166,7 +166,7 @@ async def test_bulk_remove( ) -> None: """Test removing a todo item.""" - for _i in range(0, 5): + for _i in range(5): await hass.services.async_call( TODO_DOMAIN, "add_item", diff --git a/tests/components/sia/test_config_flow.py b/tests/components/sia/test_config_flow.py index ef252991d7c..542c06da24f 100644 --- a/tests/components/sia/test_config_flow.py +++ b/tests/components/sia/test_config_flow.py @@ -1,4 +1,5 @@ """Test the sia config flow.""" + from unittest.mock import patch import pytest diff --git a/tests/components/sigfox/test_sensor.py b/tests/components/sigfox/test_sensor.py index dba30995bc4..c9876f5f3f9 100644 --- a/tests/components/sigfox/test_sensor.py +++ b/tests/components/sigfox/test_sensor.py @@ -1,4 +1,5 @@ """Tests for the sigfox sensor.""" + from http import HTTPStatus import re diff --git a/tests/components/sighthound/test_image_processing.py b/tests/components/sighthound/test_image_processing.py index 5961b925a2a..09d6c2a1ca8 100644 --- a/tests/components/sighthound/test_image_processing.py +++ b/tests/components/sighthound/test_image_processing.py @@ -1,4 +1,5 @@ """Tests for the Sighthound integration.""" + from copy import deepcopy import datetime import os diff --git a/tests/components/signal_messenger/conftest.py b/tests/components/signal_messenger/conftest.py index 017f598b93c..ecafff1ef4a 100644 --- a/tests/components/signal_messenger/conftest.py +++ b/tests/components/signal_messenger/conftest.py @@ -1,4 +1,5 @@ """Signal notification test helpers.""" + from http import HTTPStatus from pysignalclirestapi import SignalCliRestApi diff --git a/tests/components/signal_messenger/test_notify.py b/tests/components/signal_messenger/test_notify.py index 360a2b24bec..e2f76d54c87 100644 --- a/tests/components/signal_messenger/test_notify.py +++ b/tests/components/signal_messenger/test_notify.py @@ -1,4 +1,5 @@ """The tests for the signal_messenger platform.""" + import base64 import json import logging @@ -70,9 +71,12 @@ def test_send_message_to_api_with_bad_data_throws_error( ) -> None: """Test sending a message with bad data to the API throws an error.""" signal_requests_mock = signal_requests_mock_factory(False) - with caplog.at_level( - logging.DEBUG, logger="homeassistant.components.signal_messenger.notify" - ), pytest.raises(SignalCliRestApiError) as exc: + with ( + caplog.at_level( + logging.DEBUG, logger="homeassistant.components.signal_messenger.notify" + ), + pytest.raises(SignalCliRestApiError) as exc, + ): signal_notification_service.send_message(MESSAGE) assert "Sending signal message" in caplog.text @@ -87,11 +91,14 @@ def test_send_message_with_bad_data_throws_vol_error( caplog: pytest.LogCaptureFixture, ) -> None: """Test sending a message with bad data throws an error.""" - with caplog.at_level( - logging.DEBUG, logger="homeassistant.components.signal_messenger.notify" - ), pytest.raises(vol.Invalid) as exc: + with ( + caplog.at_level( + logging.DEBUG, logger="homeassistant.components.signal_messenger.notify" + ), + pytest.raises(vol.Invalid) as exc, + ): data = {"test": "test"} - signal_notification_service.send_message(MESSAGE, **{"data": data}) + signal_notification_service.send_message(MESSAGE, data=data) assert "Sending signal message" in caplog.text assert "extra keys not allowed" in str(exc.value) @@ -104,14 +111,17 @@ def test_send_message_with_attachment( ) -> None: """Test send message with attachment.""" signal_requests_mock = signal_requests_mock_factory() - with caplog.at_level( - logging.DEBUG, logger="homeassistant.components.signal_messenger.notify" - ), tempfile.NamedTemporaryFile( - mode="w", suffix=".png", prefix=os.path.basename(__file__) - ) as temp_file: + with ( + caplog.at_level( + logging.DEBUG, logger="homeassistant.components.signal_messenger.notify" + ), + tempfile.NamedTemporaryFile( + mode="w", suffix=".png", prefix=os.path.basename(__file__) + ) as temp_file, + ): temp_file.write("attachment_data") data = {"attachments": [temp_file.name]} - signal_notification_service.send_message(MESSAGE, **{"data": data}) + signal_notification_service.send_message(MESSAGE, data=data) assert "Sending signal message" in caplog.text assert signal_requests_mock.called @@ -130,7 +140,7 @@ def test_send_message_with_attachment_as_url( logging.DEBUG, logger="homeassistant.components.signal_messenger.notify" ): data = {"urls": [URL_ATTACHMENT]} - signal_notification_service.send_message(MESSAGE, **{"data": data}) + signal_notification_service.send_message(MESSAGE, data=data) assert "Sending signal message" in caplog.text assert signal_requests_mock.called diff --git a/tests/components/simplepush/test_config_flow.py b/tests/components/simplepush/test_config_flow.py index 02db81ceaa7..3905014747b 100644 --- a/tests/components/simplepush/test_config_flow.py +++ b/tests/components/simplepush/test_config_flow.py @@ -1,4 +1,5 @@ """Test Simplepush config flow.""" + from unittest.mock import patch import pytest diff --git a/tests/components/simplisafe/common.py b/tests/components/simplisafe/common.py index 68d1c4c94b7..27de1224f36 100644 --- a/tests/components/simplisafe/common.py +++ b/tests/components/simplisafe/common.py @@ -1,4 +1,5 @@ """Define common SimpliSafe test constants/etc.""" + REFRESH_TOKEN = "token123" USERNAME = "user@email.com" USER_ID = "12345" diff --git a/tests/components/simplisafe/conftest.py b/tests/components/simplisafe/conftest.py index 1b9f9f02cee..cc387ee765b 100644 --- a/tests/components/simplisafe/conftest.py +++ b/tests/components/simplisafe/conftest.py @@ -1,4 +1,5 @@ """Define test fixtures for SimpliSafe.""" + import json from unittest.mock import AsyncMock, Mock, patch @@ -94,20 +95,26 @@ def reauth_config_fixture(): @pytest.fixture(name="setup_simplisafe") async def setup_simplisafe_fixture(hass, api, config): """Define a fixture to set up SimpliSafe.""" - with patch( - "homeassistant.components.simplisafe.config_flow.API.async_from_auth", - return_value=api, - ), patch( - "homeassistant.components.simplisafe.API.async_from_auth", - return_value=api, - ), patch( - "homeassistant.components.simplisafe.API.async_from_refresh_token", - return_value=api, - ), patch( - "homeassistant.components.simplisafe.SimpliSafe._async_start_websocket_loop" - ), patch( - "homeassistant.components.simplisafe.PLATFORMS", - [], + with ( + patch( + "homeassistant.components.simplisafe.config_flow.API.async_from_auth", + return_value=api, + ), + patch( + "homeassistant.components.simplisafe.API.async_from_auth", + return_value=api, + ), + patch( + "homeassistant.components.simplisafe.API.async_from_refresh_token", + return_value=api, + ), + patch( + "homeassistant.components.simplisafe.SimpliSafe._async_start_websocket_loop" + ), + patch( + "homeassistant.components.simplisafe.PLATFORMS", + [], + ), ): assert await async_setup_component(hass, DOMAIN, config) await hass.async_block_till_done() diff --git a/tests/components/simplisafe/test_config_flow.py b/tests/components/simplisafe/test_config_flow.py index 53d9c86185f..af92833eb5b 100644 --- a/tests/components/simplisafe/test_config_flow.py +++ b/tests/components/simplisafe/test_config_flow.py @@ -1,4 +1,5 @@ """Define tests for the SimpliSafe config flow.""" + import logging from unittest.mock import patch @@ -98,9 +99,12 @@ async def test_step_reauth(config_entry, hass: HomeAssistant, setup_simplisafe) ) assert result["step_id"] == "user" - with patch( - "homeassistant.components.simplisafe.async_setup_entry", return_value=True - ), patch("homeassistant.config_entries.ConfigEntries.async_reload"): + with ( + patch( + "homeassistant.components.simplisafe.async_setup_entry", return_value=True + ), + patch("homeassistant.config_entries.ConfigEntries.async_reload"), + ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_AUTH_CODE: VALID_AUTH_CODE} ) @@ -124,9 +128,12 @@ async def test_step_reauth_wrong_account( ) assert result["step_id"] == "user" - with patch( - "homeassistant.components.simplisafe.async_setup_entry", return_value=True - ), patch("homeassistant.config_entries.ConfigEntries.async_reload"): + with ( + patch( + "homeassistant.components.simplisafe.async_setup_entry", return_value=True + ), + patch("homeassistant.config_entries.ConfigEntries.async_reload"), + ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_AUTH_CODE: VALID_AUTH_CODE} ) @@ -162,9 +169,12 @@ async def test_step_user( ) assert result["step_id"] == "user" - with patch( - "homeassistant.components.simplisafe.async_setup_entry", return_value=True - ), patch("homeassistant.config_entries.ConfigEntries.async_reload"): + with ( + patch( + "homeassistant.components.simplisafe.async_setup_entry", return_value=True + ), + patch("homeassistant.config_entries.ConfigEntries.async_reload"), + ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_AUTH_CODE: auth_code} ) diff --git a/tests/components/simplisafe/test_diagnostics.py b/tests/components/simplisafe/test_diagnostics.py index 538165bd769..e6a9d70b164 100644 --- a/tests/components/simplisafe/test_diagnostics.py +++ b/tests/components/simplisafe/test_diagnostics.py @@ -1,4 +1,5 @@ """Test SimpliSafe diagnostics.""" + from homeassistant.components.diagnostics import REDACTED from homeassistant.core import HomeAssistant diff --git a/tests/components/simplisafe/test_init.py b/tests/components/simplisafe/test_init.py index cc7b2b8d2b6..f626f479a2f 100644 --- a/tests/components/simplisafe/test_init.py +++ b/tests/components/simplisafe/test_init.py @@ -1,4 +1,5 @@ """Define tests for SimpliSafe setup.""" + from unittest.mock import patch from homeassistant.components.simplisafe import DOMAIN @@ -22,20 +23,26 @@ async def test_base_station_migration( name="old", ) - with patch( - "homeassistant.components.simplisafe.config_flow.API.async_from_auth", - return_value=api, - ), patch( - "homeassistant.components.simplisafe.API.async_from_auth", - return_value=api, - ), patch( - "homeassistant.components.simplisafe.API.async_from_refresh_token", - return_value=api, - ), patch( - "homeassistant.components.simplisafe.SimpliSafe._async_start_websocket_loop" - ), patch( - "homeassistant.components.simplisafe.PLATFORMS", - [], + with ( + patch( + "homeassistant.components.simplisafe.config_flow.API.async_from_auth", + return_value=api, + ), + patch( + "homeassistant.components.simplisafe.API.async_from_auth", + return_value=api, + ), + patch( + "homeassistant.components.simplisafe.API.async_from_refresh_token", + return_value=api, + ), + patch( + "homeassistant.components.simplisafe.SimpliSafe._async_start_websocket_loop" + ), + patch( + "homeassistant.components.simplisafe.PLATFORMS", + [], + ), ): assert await async_setup_component(hass, DOMAIN, config) await hass.async_block_till_done() diff --git a/tests/components/simulated/test_sensor.py b/tests/components/simulated/test_sensor.py index 32c04c7a462..d32eca8c66e 100644 --- a/tests/components/simulated/test_sensor.py +++ b/tests/components/simulated/test_sensor.py @@ -1,4 +1,5 @@ """The tests for the simulated sensor.""" + from homeassistant.components.simulated.sensor import ( CONF_AMP, CONF_FWHM, diff --git a/tests/components/siren/test_init.py b/tests/components/siren/test_init.py index 1cf44d16ea0..168300d0abe 100644 --- a/tests/components/siren/test_init.py +++ b/tests/components/siren/test_init.py @@ -1,4 +1,5 @@ """The tests for the siren component.""" + from types import ModuleType from unittest.mock import MagicMock diff --git a/tests/components/siren/test_recorder.py b/tests/components/siren/test_recorder.py index 76b497e024a..5fb16f73175 100644 --- a/tests/components/siren/test_recorder.py +++ b/tests/components/siren/test_recorder.py @@ -1,4 +1,5 @@ """The tests for siren recorder.""" + from __future__ import annotations from datetime import timedelta @@ -8,7 +9,7 @@ from homeassistant.components.recorder import Recorder from homeassistant.components.recorder.history import get_significant_states from homeassistant.components.siren import ATTR_AVAILABLE_TONES from homeassistant.const import ATTR_FRIENDLY_NAME -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, split_entity_id from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util @@ -31,7 +32,11 @@ async def test_exclude_attributes(recorder_mock: Recorder, hass: HomeAssistant) get_significant_states, hass, now, None, hass.states.async_entity_ids() ) assert len(states) >= 1 - for entity_states in states.values(): - for state in entity_states: - assert ATTR_AVAILABLE_TONES not in state.attributes - assert ATTR_FRIENDLY_NAME in state.attributes + for state in ( + state + for entity_states in states.values() + for state in entity_states + if split_entity_id(state.entity_id)[0] == siren.DOMAIN + ): + assert ATTR_AVAILABLE_TONES not in state.attributes + assert ATTR_FRIENDLY_NAME in state.attributes diff --git a/tests/components/skybell/conftest.py b/tests/components/skybell/conftest.py index beb3fec9b98..6120d168572 100644 --- a/tests/components/skybell/conftest.py +++ b/tests/components/skybell/conftest.py @@ -1,4 +1,5 @@ """Configure pytest for Skybell tests.""" + from unittest.mock import AsyncMock, patch from aioskybell import Skybell, SkybellDevice @@ -35,10 +36,13 @@ def skybell_mock(): mocked_skybell.async_send_request.return_value = {"id": USER_ID} mocked_skybell.user_id = USER_ID - with patch( - "homeassistant.components.skybell.config_flow.Skybell", - return_value=mocked_skybell, - ), patch("homeassistant.components.skybell.Skybell", return_value=mocked_skybell): + with ( + patch( + "homeassistant.components.skybell.config_flow.Skybell", + return_value=mocked_skybell, + ), + patch("homeassistant.components.skybell.Skybell", return_value=mocked_skybell), + ): yield mocked_skybell diff --git a/tests/components/skybell/test_binary_sensor.py b/tests/components/skybell/test_binary_sensor.py index 8e0bc884730..004f8160366 100644 --- a/tests/components/skybell/test_binary_sensor.py +++ b/tests/components/skybell/test_binary_sensor.py @@ -1,4 +1,5 @@ """Binary sensor tests for the Skybell integration.""" + from homeassistant.components.binary_sensor import BinarySensorDeviceClass from homeassistant.const import ATTR_DEVICE_CLASS, STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant diff --git a/tests/components/skybell/test_config_flow.py b/tests/components/skybell/test_config_flow.py index d83f4243d7f..535eb91f01b 100644 --- a/tests/components/skybell/test_config_flow.py +++ b/tests/components/skybell/test_config_flow.py @@ -1,4 +1,5 @@ """Test SkyBell config flow.""" + from unittest.mock import patch from aioskybell import exceptions diff --git a/tests/components/slack/__init__.py b/tests/components/slack/__init__.py index 6a258ce9027..acb52a11a6c 100644 --- a/tests/components/slack/__init__.py +++ b/tests/components/slack/__init__.py @@ -1,4 +1,5 @@ """Tests for the Slack integration.""" + from __future__ import annotations import json diff --git a/tests/components/slack/test_config_flow.py b/tests/components/slack/test_config_flow.py index e941e4ba47c..c7b8d927c94 100644 --- a/tests/components/slack/test_config_flow.py +++ b/tests/components/slack/test_config_flow.py @@ -1,4 +1,5 @@ """Test Slack config flow.""" + from unittest.mock import patch from homeassistant import config_entries, data_entry_flow diff --git a/tests/components/slack/test_init.py b/tests/components/slack/test_init.py index 487a65b8ef0..e206e066c67 100644 --- a/tests/components/slack/test_init.py +++ b/tests/components/slack/test_init.py @@ -1,4 +1,5 @@ """Test Slack integration.""" + from homeassistant.components.slack.const import DOMAIN from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.core import HomeAssistant diff --git a/tests/components/slack/test_notify.py b/tests/components/slack/test_notify.py index 6c90ad8cd39..e2c8e907185 100644 --- a/tests/components/slack/test_notify.py +++ b/tests/components/slack/test_notify.py @@ -1,4 +1,5 @@ """Test slack notifications.""" + from __future__ import annotations from unittest.mock import AsyncMock, Mock diff --git a/tests/components/sleepiq/conftest.py b/tests/components/sleepiq/conftest.py index 58718edcafb..3a53e8ce684 100644 --- a/tests/components/sleepiq/conftest.py +++ b/tests/components/sleepiq/conftest.py @@ -1,4 +1,5 @@ """Common methods for SleepIQ.""" + from __future__ import annotations from collections.abc import Generator diff --git a/tests/components/sleepiq/test_binary_sensor.py b/tests/components/sleepiq/test_binary_sensor.py index 25147dd3823..bbb0200dd23 100644 --- a/tests/components/sleepiq/test_binary_sensor.py +++ b/tests/components/sleepiq/test_binary_sensor.py @@ -1,4 +1,5 @@ """The tests for SleepIQ binary sensor platform.""" + from homeassistant.components.binary_sensor import DOMAIN, BinarySensorDeviceClass from homeassistant.const import ( ATTR_DEVICE_CLASS, diff --git a/tests/components/sleepiq/test_button.py b/tests/components/sleepiq/test_button.py index c766db6b286..0979d01ba7b 100644 --- a/tests/components/sleepiq/test_button.py +++ b/tests/components/sleepiq/test_button.py @@ -1,4 +1,5 @@ """The tests for SleepIQ binary sensor platform.""" + from homeassistant.components.button import DOMAIN from homeassistant.const import ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME from homeassistant.core import HomeAssistant diff --git a/tests/components/sleepiq/test_config_flow.py b/tests/components/sleepiq/test_config_flow.py index 0f251675892..b623252cec4 100644 --- a/tests/components/sleepiq/test_config_flow.py +++ b/tests/components/sleepiq/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for the SleepIQ config flow.""" + from unittest.mock import AsyncMock, patch from asyncsleepiq import SleepIQLoginException, SleepIQTimeoutException diff --git a/tests/components/sleepiq/test_init.py b/tests/components/sleepiq/test_init.py index 6a02f795805..216d0e49b08 100644 --- a/tests/components/sleepiq/test_init.py +++ b/tests/components/sleepiq/test_init.py @@ -1,4 +1,5 @@ """Tests for the SleepIQ integration.""" + from asyncsleepiq import ( SleepIQAPIException, SleepIQLoginException, diff --git a/tests/components/sleepiq/test_light.py b/tests/components/sleepiq/test_light.py index 24193308f46..e261115c415 100644 --- a/tests/components/sleepiq/test_light.py +++ b/tests/components/sleepiq/test_light.py @@ -1,4 +1,5 @@ """The tests for SleepIQ light platform.""" + from homeassistant.components.light import DOMAIN from homeassistant.components.sleepiq.coordinator import LONGER_UPDATE_INTERVAL from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON @@ -63,7 +64,7 @@ async def test_switch_get_states(hass: HomeAssistant, mock_asyncsleepiq) -> None mock_asyncsleepiq.beds[BED_ID].foundation.lights[0].is_on = True async_fire_time_changed(hass, utcnow() + LONGER_UPDATE_INTERVAL) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert ( hass.states.get(f"light.sleepnumber_{BED_NAME_LOWER}_light_1").state == STATE_ON diff --git a/tests/components/sleepiq/test_number.py b/tests/components/sleepiq/test_number.py index 4676cf94174..f3a38cc89e5 100644 --- a/tests/components/sleepiq/test_number.py +++ b/tests/components/sleepiq/test_number.py @@ -1,4 +1,5 @@ """The tests for SleepIQ number platform.""" + from homeassistant.components.number import ( ATTR_MAX, ATTR_MIN, diff --git a/tests/components/sleepiq/test_select.py b/tests/components/sleepiq/test_select.py index c4ec3896bd7..cc61494689e 100644 --- a/tests/components/sleepiq/test_select.py +++ b/tests/components/sleepiq/test_select.py @@ -1,4 +1,5 @@ """Tests for the SleepIQ select platform.""" + from unittest.mock import MagicMock from asyncsleepiq import FootWarmingTemps diff --git a/tests/components/sleepiq/test_sensor.py b/tests/components/sleepiq/test_sensor.py index a707a0169af..c027aaee87b 100644 --- a/tests/components/sleepiq/test_sensor.py +++ b/tests/components/sleepiq/test_sensor.py @@ -1,4 +1,5 @@ """The tests for SleepIQ sensor platform.""" + from homeassistant.components.sensor import DOMAIN from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_ICON from homeassistant.core import HomeAssistant diff --git a/tests/components/sleepiq/test_switch.py b/tests/components/sleepiq/test_switch.py index 04d0b6657d7..8ab865663dc 100644 --- a/tests/components/sleepiq/test_switch.py +++ b/tests/components/sleepiq/test_switch.py @@ -1,4 +1,5 @@ """The tests for SleepIQ switch platform.""" + from homeassistant.components.sleepiq.coordinator import LONGER_UPDATE_INTERVAL from homeassistant.components.switch import DOMAIN from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON @@ -58,7 +59,7 @@ async def test_switch_get_states(hass: HomeAssistant, mock_asyncsleepiq) -> None mock_asyncsleepiq.beds[BED_ID].paused = True async_fire_time_changed(hass, utcnow() + LONGER_UPDATE_INTERVAL) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert ( hass.states.get(f"switch.sleepnumber_{BED_NAME_LOWER}_pause_mode").state diff --git a/tests/components/slimproto/conftest.py b/tests/components/slimproto/conftest.py index faf91ea27d1..637f5ec0a99 100644 --- a/tests/components/slimproto/conftest.py +++ b/tests/components/slimproto/conftest.py @@ -1,4 +1,5 @@ """Fixtures for the SlimProto Player integration tests.""" + from __future__ import annotations from collections.abc import Generator diff --git a/tests/components/slimproto/test_config_flow.py b/tests/components/slimproto/test_config_flow.py index 15ea5434fc5..686768c6eb6 100644 --- a/tests/components/slimproto/test_config_flow.py +++ b/tests/components/slimproto/test_config_flow.py @@ -1,4 +1,5 @@ """Test the SlimProto Player config flow.""" + from unittest.mock import AsyncMock from homeassistant.components.slimproto.const import DEFAULT_NAME, DOMAIN diff --git a/tests/components/sma/__init__.py b/tests/components/sma/__init__.py index 0c6c8a7ee67..aefb99cf1b1 100644 --- a/tests/components/sma/__init__.py +++ b/tests/components/sma/__init__.py @@ -1,4 +1,5 @@ """Tests for the sma integration.""" + from unittest.mock import patch MOCK_DEVICE = { diff --git a/tests/components/sma/conftest.py b/tests/components/sma/conftest.py index 2ce5db5e0ca..a98eda673e4 100644 --- a/tests/components/sma/conftest.py +++ b/tests/components/sma/conftest.py @@ -1,4 +1,5 @@ """Fixtures for sma tests.""" + from unittest.mock import patch from pysma.const import GENERIC_SENSORS @@ -31,8 +32,11 @@ async def init_integration(hass, mock_config_entry): """Create a fake SMA Config Entry.""" mock_config_entry.add_to_hass(hass) - with patch("pysma.SMA.read"), patch( - "pysma.SMA.get_sensors", return_value=Sensors(sensor_map[GENERIC_SENSORS]) + with ( + patch("pysma.SMA.read"), + patch( + "pysma.SMA.get_sensors", return_value=Sensors(sensor_map[GENERIC_SENSORS]) + ), ): await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/sma/test_config_flow.py b/tests/components/sma/test_config_flow.py index 541376d1536..d73d8eb9728 100644 --- a/tests/components/sma/test_config_flow.py +++ b/tests/components/sma/test_config_flow.py @@ -1,4 +1,5 @@ """Test the sma config flow.""" + from unittest.mock import patch from pysma.exceptions import ( @@ -24,9 +25,11 @@ async def test_form(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.FORM assert result["errors"] == {} - with patch("pysma.SMA.new_session", return_value=True), patch( - "pysma.SMA.device_info", return_value=MOCK_DEVICE - ), _patch_async_setup_entry() as mock_setup_entry: + with ( + patch("pysma.SMA.new_session", return_value=True), + patch("pysma.SMA.device_info", return_value=MOCK_DEVICE), + _patch_async_setup_entry() as mock_setup_entry, + ): result = await hass.config_entries.flow.async_configure( result["flow_id"], MOCK_USER_INPUT, @@ -46,9 +49,10 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - with patch( - "pysma.SMA.new_session", side_effect=SmaConnectionException - ), _patch_async_setup_entry() as mock_setup_entry: + with ( + patch("pysma.SMA.new_session", side_effect=SmaConnectionException), + _patch_async_setup_entry() as mock_setup_entry, + ): result = await hass.config_entries.flow.async_configure( result["flow_id"], MOCK_USER_INPUT, @@ -65,9 +69,10 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - with patch( - "pysma.SMA.new_session", side_effect=SmaAuthenticationException - ), _patch_async_setup_entry() as mock_setup_entry: + with ( + patch("pysma.SMA.new_session", side_effect=SmaAuthenticationException), + _patch_async_setup_entry() as mock_setup_entry, + ): result = await hass.config_entries.flow.async_configure( result["flow_id"], MOCK_USER_INPUT, @@ -84,9 +89,11 @@ async def test_form_cannot_retrieve_device_info(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - with patch("pysma.SMA.new_session", return_value=True), patch( - "pysma.SMA.read", side_effect=SmaReadException - ), _patch_async_setup_entry() as mock_setup_entry: + with ( + patch("pysma.SMA.new_session", return_value=True), + patch("pysma.SMA.read", side_effect=SmaReadException), + _patch_async_setup_entry() as mock_setup_entry, + ): result = await hass.config_entries.flow.async_configure( result["flow_id"], MOCK_USER_INPUT, @@ -103,9 +110,10 @@ async def test_form_unexpected_exception(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - with patch( - "pysma.SMA.new_session", side_effect=Exception - ), _patch_async_setup_entry() as mock_setup_entry: + with ( + patch("pysma.SMA.new_session", side_effect=Exception), + _patch_async_setup_entry() as mock_setup_entry, + ): result = await hass.config_entries.flow.async_configure( result["flow_id"], MOCK_USER_INPUT, @@ -124,11 +132,12 @@ async def test_form_already_configured(hass: HomeAssistant, mock_config_entry) - DOMAIN, context={"source": SOURCE_USER} ) - with patch("pysma.SMA.new_session", return_value=True), patch( - "pysma.SMA.device_info", return_value=MOCK_DEVICE - ), patch( - "pysma.SMA.close_session", return_value=True - ), _patch_async_setup_entry() as mock_setup_entry: + with ( + patch("pysma.SMA.new_session", return_value=True), + patch("pysma.SMA.device_info", return_value=MOCK_DEVICE), + patch("pysma.SMA.close_session", return_value=True), + _patch_async_setup_entry() as mock_setup_entry, + ): result = await hass.config_entries.flow.async_configure( result["flow_id"], MOCK_USER_INPUT, diff --git a/tests/components/sma/test_sensor.py b/tests/components/sma/test_sensor.py index acc26a8bf90..de7e1167f1f 100644 --- a/tests/components/sma/test_sensor.py +++ b/tests/components/sma/test_sensor.py @@ -1,4 +1,5 @@ """Test the sma sensor platform.""" + from pysma.const import ( ENERGY_METER_VIA_INVERTER, GENERIC_SENSORS, diff --git a/tests/components/smappee/test_config_flow.py b/tests/components/smappee/test_config_flow.py index 8d4d7b8c3b2..b5551c03c77 100644 --- a/tests/components/smappee/test_config_flow.py +++ b/tests/components/smappee/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Smappee component config flow module.""" + from http import HTTPStatus from ipaddress import ip_address from unittest.mock import patch @@ -117,8 +118,9 @@ async def test_show_zeroconf_connection_error_form_next_generation( async def test_connection_error(hass: HomeAssistant) -> None: """Test we show user form on Smappee connection error.""" - with patch("pysmappee.api.SmappeeLocalApi.logon", return_value=None), patch( - "pysmappee.mqtt.SmappeeLocalMqtt.start_attempt", return_value=None + with ( + patch("pysmappee.api.SmappeeLocalApi.logon", return_value=None), + patch("pysmappee.mqtt.SmappeeLocalMqtt.start_attempt", return_value=None), ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -142,11 +144,13 @@ async def test_connection_error(hass: HomeAssistant) -> None: async def test_user_local_connection_error(hass: HomeAssistant) -> None: """Test we show user form on Smappee connection error in local next generation option.""" - with patch("pysmappee.api.SmappeeLocalApi.logon", return_value=None), patch( - "pysmappee.mqtt.SmappeeLocalMqtt.start_attempt", return_value=True - ), patch("pysmappee.mqtt.SmappeeLocalMqtt.start", return_value=True), patch( - "pysmappee.mqtt.SmappeeLocalMqtt.stop", return_value=True - ), patch("pysmappee.mqtt.SmappeeLocalMqtt.is_config_ready", return_value=None): + with ( + patch("pysmappee.api.SmappeeLocalApi.logon", return_value=None), + patch("pysmappee.mqtt.SmappeeLocalMqtt.start_attempt", return_value=True), + patch("pysmappee.mqtt.SmappeeLocalMqtt.start", return_value=True), + patch("pysmappee.mqtt.SmappeeLocalMqtt.stop", return_value=True), + patch("pysmappee.mqtt.SmappeeLocalMqtt.is_config_ready", return_value=None), + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, @@ -189,14 +193,19 @@ async def test_zeroconf_wrong_mdns(hass: HomeAssistant) -> None: async def test_full_user_wrong_mdns(hass: HomeAssistant) -> None: """Test we abort user flow if unsupported mDNS name got resolved.""" - with patch("pysmappee.api.SmappeeLocalApi.logon", return_value={}), patch( - "pysmappee.api.SmappeeLocalApi.load_advanced_config", - return_value=[{"key": "mdnsHostName", "value": "Smappee5100000001"}], - ), patch( - "pysmappee.api.SmappeeLocalApi.load_command_control_config", return_value=[] - ), patch( - "pysmappee.api.SmappeeLocalApi.load_instantaneous", - return_value=[{"key": "phase0ActivePower", "value": 0}], + with ( + patch("pysmappee.api.SmappeeLocalApi.logon", return_value={}), + patch( + "pysmappee.api.SmappeeLocalApi.load_advanced_config", + return_value=[{"key": "mdnsHostName", "value": "Smappee5100000001"}], + ), + patch( + "pysmappee.api.SmappeeLocalApi.load_command_control_config", return_value=[] + ), + patch( + "pysmappee.api.SmappeeLocalApi.load_instantaneous", + return_value=[{"key": "phase0ActivePower", "value": 0}], + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -220,14 +229,19 @@ async def test_full_user_wrong_mdns(hass: HomeAssistant) -> None: async def test_user_device_exists_abort(hass: HomeAssistant) -> None: """Test we abort user flow if Smappee device already configured.""" - with patch("pysmappee.api.SmappeeLocalApi.logon", return_value={}), patch( - "pysmappee.api.SmappeeLocalApi.load_advanced_config", - return_value=[{"key": "mdnsHostName", "value": "Smappee1006000212"}], - ), patch( - "pysmappee.api.SmappeeLocalApi.load_command_control_config", return_value=[] - ), patch( - "pysmappee.api.SmappeeLocalApi.load_instantaneous", - return_value=[{"key": "phase0ActivePower", "value": 0}], + with ( + patch("pysmappee.api.SmappeeLocalApi.logon", return_value={}), + patch( + "pysmappee.api.SmappeeLocalApi.load_advanced_config", + return_value=[{"key": "mdnsHostName", "value": "Smappee1006000212"}], + ), + patch( + "pysmappee.api.SmappeeLocalApi.load_command_control_config", return_value=[] + ), + patch( + "pysmappee.api.SmappeeLocalApi.load_instantaneous", + return_value=[{"key": "phase0ActivePower", "value": 0}], + ), ): config_entry = MockConfigEntry( domain=DOMAIN, @@ -261,14 +275,19 @@ async def test_user_device_exists_abort(hass: HomeAssistant) -> None: async def test_zeroconf_device_exists_abort(hass: HomeAssistant) -> None: """Test we abort zeroconf flow if Smappee device already configured.""" - with patch("pysmappee.api.SmappeeLocalApi.logon", return_value={}), patch( - "pysmappee.api.SmappeeLocalApi.load_advanced_config", - return_value=[{"key": "mdnsHostName", "value": "Smappee1006000212"}], - ), patch( - "pysmappee.api.SmappeeLocalApi.load_command_control_config", return_value=[] - ), patch( - "pysmappee.api.SmappeeLocalApi.load_instantaneous", - return_value=[{"key": "phase0ActivePower", "value": 0}], + with ( + patch("pysmappee.api.SmappeeLocalApi.logon", return_value={}), + patch( + "pysmappee.api.SmappeeLocalApi.load_advanced_config", + return_value=[{"key": "mdnsHostName", "value": "Smappee1006000212"}], + ), + patch( + "pysmappee.api.SmappeeLocalApi.load_command_control_config", return_value=[] + ), + patch( + "pysmappee.api.SmappeeLocalApi.load_instantaneous", + return_value=[{"key": "phase0ActivePower", "value": 0}], + ), ): config_entry = MockConfigEntry( domain=DOMAIN, @@ -464,15 +483,21 @@ async def test_full_user_flow( async def test_full_zeroconf_flow(hass: HomeAssistant) -> None: """Test the full zeroconf flow.""" - with patch("pysmappee.api.SmappeeLocalApi.logon", return_value={}), patch( - "pysmappee.api.SmappeeLocalApi.load_advanced_config", - return_value=[{"key": "mdnsHostName", "value": "Smappee1006000212"}], - ), patch( - "pysmappee.api.SmappeeLocalApi.load_command_control_config", return_value=[] - ), patch( - "pysmappee.api.SmappeeLocalApi.load_instantaneous", - return_value=[{"key": "phase0ActivePower", "value": 0}], - ), patch("homeassistant.components.smappee.async_setup_entry", return_value=True): + with ( + patch("pysmappee.api.SmappeeLocalApi.logon", return_value={}), + patch( + "pysmappee.api.SmappeeLocalApi.load_advanced_config", + return_value=[{"key": "mdnsHostName", "value": "Smappee1006000212"}], + ), + patch( + "pysmappee.api.SmappeeLocalApi.load_command_control_config", return_value=[] + ), + patch( + "pysmappee.api.SmappeeLocalApi.load_instantaneous", + return_value=[{"key": "phase0ActivePower", "value": 0}], + ), + patch("homeassistant.components.smappee.async_setup_entry", return_value=True), + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, @@ -504,15 +529,21 @@ async def test_full_zeroconf_flow(hass: HomeAssistant) -> None: async def test_full_user_local_flow(hass: HomeAssistant) -> None: """Test the full zeroconf flow.""" - with patch("pysmappee.api.SmappeeLocalApi.logon", return_value={}), patch( - "pysmappee.api.SmappeeLocalApi.load_advanced_config", - return_value=[{"key": "mdnsHostName", "value": "Smappee1006000212"}], - ), patch( - "pysmappee.api.SmappeeLocalApi.load_command_control_config", return_value=[] - ), patch( - "pysmappee.api.SmappeeLocalApi.load_instantaneous", - return_value=[{"key": "phase0ActivePower", "value": 0}], - ), patch("homeassistant.components.smappee.async_setup_entry", return_value=True): + with ( + patch("pysmappee.api.SmappeeLocalApi.logon", return_value={}), + patch( + "pysmappee.api.SmappeeLocalApi.load_advanced_config", + return_value=[{"key": "mdnsHostName", "value": "Smappee1006000212"}], + ), + patch( + "pysmappee.api.SmappeeLocalApi.load_command_control_config", return_value=[] + ), + patch( + "pysmappee.api.SmappeeLocalApi.load_instantaneous", + return_value=[{"key": "phase0ActivePower", "value": 0}], + ), + patch("homeassistant.components.smappee.async_setup_entry", return_value=True), + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, @@ -541,14 +572,16 @@ async def test_full_user_local_flow(hass: HomeAssistant) -> None: async def test_full_zeroconf_flow_next_generation(hass: HomeAssistant) -> None: """Test the full zeroconf flow.""" - with patch( - "pysmappee.mqtt.SmappeeLocalMqtt.start_attempt", return_value=True - ), patch( - "pysmappee.mqtt.SmappeeLocalMqtt.start", - return_value=None, - ), patch( - "pysmappee.mqtt.SmappeeLocalMqtt.is_config_ready", - return_value=None, + with ( + patch("pysmappee.mqtt.SmappeeLocalMqtt.start_attempt", return_value=True), + patch( + "pysmappee.mqtt.SmappeeLocalMqtt.start", + return_value=None, + ), + patch( + "pysmappee.mqtt.SmappeeLocalMqtt.is_config_ready", + return_value=None, + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, diff --git a/tests/components/smappee/test_init.py b/tests/components/smappee/test_init.py index 34700c26a5a..cc752964a27 100644 --- a/tests/components/smappee/test_init.py +++ b/tests/components/smappee/test_init.py @@ -1,4 +1,5 @@ """Tests for the Smappee component init module.""" + from unittest.mock import patch from homeassistant.components.smappee.const import DOMAIN @@ -10,14 +11,19 @@ from tests.common import MockConfigEntry async def test_unload_config_entry(hass: HomeAssistant) -> None: """Test unload config entry flow.""" - with patch("pysmappee.api.SmappeeLocalApi.logon", return_value={}), patch( - "pysmappee.api.SmappeeLocalApi.load_advanced_config", - return_value=[{"key": "mdnsHostName", "value": "Smappee1006000212"}], - ), patch( - "pysmappee.api.SmappeeLocalApi.load_command_control_config", return_value=[] - ), patch( - "pysmappee.api.SmappeeLocalApi.load_instantaneous", - return_value=[{"key": "phase0ActivePower", "value": 0}], + with ( + patch("pysmappee.api.SmappeeLocalApi.logon", return_value={}), + patch( + "pysmappee.api.SmappeeLocalApi.load_advanced_config", + return_value=[{"key": "mdnsHostName", "value": "Smappee1006000212"}], + ), + patch( + "pysmappee.api.SmappeeLocalApi.load_command_control_config", return_value=[] + ), + patch( + "pysmappee.api.SmappeeLocalApi.load_instantaneous", + return_value=[{"key": "phase0ActivePower", "value": 0}], + ), ): config_entry = MockConfigEntry( domain=DOMAIN, diff --git a/tests/components/smart_meter_texas/conftest.py b/tests/components/smart_meter_texas/conftest.py index 782deafbcf3..04a3344b5cc 100644 --- a/tests/components/smart_meter_texas/conftest.py +++ b/tests/components/smart_meter_texas/conftest.py @@ -1,4 +1,5 @@ """Test configuration and mocks for Smart Meter Texas.""" + from http import HTTPStatus import json diff --git a/tests/components/smart_meter_texas/test_config_flow.py b/tests/components/smart_meter_texas/test_config_flow.py index 0fb56937f0a..53f7a2eb5fd 100644 --- a/tests/components/smart_meter_texas/test_config_flow.py +++ b/tests/components/smart_meter_texas/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Smart Meter Texas config flow.""" + from unittest.mock import patch from aiohttp import ClientError @@ -27,10 +28,13 @@ async def test_form(hass: HomeAssistant) -> None: assert result["type"] == "form" assert result["errors"] == {} - with patch("smart_meter_texas.Client.authenticate", return_value=True), patch( - "homeassistant.components.smart_meter_texas.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch("smart_meter_texas.Client.authenticate", return_value=True), + patch( + "homeassistant.components.smart_meter_texas.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], TEST_LOGIN ) diff --git a/tests/components/smart_meter_texas/test_init.py b/tests/components/smart_meter_texas/test_init.py index f3e8e73a24d..df0d5385c66 100644 --- a/tests/components/smart_meter_texas/test_init.py +++ b/tests/components/smart_meter_texas/test_init.py @@ -1,4 +1,5 @@ """Test the Smart Meter Texas module.""" + from unittest.mock import patch from homeassistant.components.homeassistant import ( diff --git a/tests/components/smart_meter_texas/test_sensor.py b/tests/components/smart_meter_texas/test_sensor.py index 7960b007d74..36142d5f059 100644 --- a/tests/components/smart_meter_texas/test_sensor.py +++ b/tests/components/smart_meter_texas/test_sensor.py @@ -1,4 +1,5 @@ """Test the Smart Meter Texas sensor entity.""" + from unittest.mock import patch from homeassistant.components.homeassistant import ( diff --git a/tests/components/smartthings/conftest.py b/tests/components/smartthings/conftest.py index a7a819d6ac9..f15ba85c07e 100644 --- a/tests/components/smartthings/conftest.py +++ b/tests/components/smartthings/conftest.py @@ -1,4 +1,5 @@ """Test configuration and mocks for the SmartThings component.""" + import secrets from unittest.mock import Mock, patch from uuid import uuid4 @@ -182,9 +183,11 @@ def smartthings_mock_fixture(locations): smartthings_mock = Mock(SmartThings) smartthings_mock.location.side_effect = _location mock = Mock(return_value=smartthings_mock) - with patch(COMPONENT_PREFIX + "SmartThings", new=mock), patch( - COMPONENT_PREFIX + "config_flow.SmartThings", new=mock - ), patch(COMPONENT_PREFIX + "smartapp.SmartThings", new=mock): + with ( + patch(COMPONENT_PREFIX + "SmartThings", new=mock), + patch(COMPONENT_PREFIX + "config_flow.SmartThings", new=mock), + patch(COMPONENT_PREFIX + "smartapp.SmartThings", new=mock), + ): yield smartthings_mock diff --git a/tests/components/smartthings/test_binary_sensor.py b/tests/components/smartthings/test_binary_sensor.py index 1c222b3ca78..9d704cdf8c9 100644 --- a/tests/components/smartthings/test_binary_sensor.py +++ b/tests/components/smartthings/test_binary_sensor.py @@ -3,6 +3,7 @@ The only mocking required is of the underlying SmartThings API object so real HTTP calls are not initiated during testing. """ + from pysmartthings import ATTRIBUTES, CAPABILITIES, Attribute, Capability from homeassistant.components.binary_sensor import ( diff --git a/tests/components/smartthings/test_climate.py b/tests/components/smartthings/test_climate.py index 475a8f09e03..3fb293e587f 100644 --- a/tests/components/smartthings/test_climate.py +++ b/tests/components/smartthings/test_climate.py @@ -3,6 +3,7 @@ The only mocking required is of the underlying SmartThings API object so real HTTP calls are not initiated during testing. """ + from pysmartthings import Attribute, Capability from pysmartthings.device import Status import pytest diff --git a/tests/components/smartthings/test_config_flow.py b/tests/components/smartthings/test_config_flow.py index 168756b0dfe..e3dcf76bbaf 100644 --- a/tests/components/smartthings/test_config_flow.py +++ b/tests/components/smartthings/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for the SmartThings config flow module.""" + from http import HTTPStatus from unittest.mock import AsyncMock, Mock, patch from uuid import uuid4 @@ -358,15 +359,18 @@ async def test_entry_created_with_cloudhook( request.location_id = location.location_id request.refresh_token = refresh_token - with patch.object( - smartapp.cloud, - "async_active_subscription", - Mock(return_value=True), - ), patch.object( - smartapp.cloud, - "async_create_cloudhook", - AsyncMock(return_value="http://cloud.test"), - ) as mock_create_cloudhook: + with ( + patch.object( + smartapp.cloud, + "async_active_subscription", + Mock(return_value=True), + ), + patch.object( + smartapp.cloud, + "async_create_cloudhook", + AsyncMock(return_value="http://cloud.test"), + ) as mock_create_cloudhook, + ): await smartapp.setup_smartapp_endpoint(hass, True) # Webhook confirmation shown diff --git a/tests/components/smartthings/test_cover.py b/tests/components/smartthings/test_cover.py index 081b40e57a9..c4f6c15a3fe 100644 --- a/tests/components/smartthings/test_cover.py +++ b/tests/components/smartthings/test_cover.py @@ -3,6 +3,7 @@ The only mocking required is of the underlying SmartThings API object so real HTTP calls are not initiated during testing. """ + from pysmartthings import Attribute, Capability from homeassistant.components.cover import ( diff --git a/tests/components/smartthings/test_fan.py b/tests/components/smartthings/test_fan.py index ca2f97b2909..b8928ef5247 100644 --- a/tests/components/smartthings/test_fan.py +++ b/tests/components/smartthings/test_fan.py @@ -3,6 +3,7 @@ The only mocking required is of the underlying SmartThings API object so real HTTP calls are not initiated during testing. """ + from pysmartthings import Attribute, Capability from homeassistant.components.fan import ( diff --git a/tests/components/smartthings/test_init.py b/tests/components/smartthings/test_init.py index ec2ec3abe19..6ff640e012a 100644 --- a/tests/components/smartthings/test_init.py +++ b/tests/components/smartthings/test_init.py @@ -1,4 +1,5 @@ """Tests for the SmartThings component init module.""" + from http import HTTPStatus from unittest.mock import Mock, patch from uuid import uuid4 @@ -178,11 +179,13 @@ async def test_scenes_unauthorized_loads_platforms( ] smartthings_mock.subscriptions.return_value = subscriptions - with patch.object(hass.config_entries, "async_forward_entry_setup") as forward_mock: + with patch.object( + hass.config_entries, "async_forward_entry_setups" + ) as forward_mock: assert await hass.config_entries.async_setup(config_entry.entry_id) # Assert platforms loaded await hass.async_block_till_done() - assert forward_mock.call_count == len(PLATFORMS) + forward_mock.assert_called_once_with(config_entry, PLATFORMS) async def test_config_entry_loads_platforms( @@ -210,11 +213,13 @@ async def test_config_entry_loads_platforms( ] smartthings_mock.subscriptions.return_value = subscriptions - with patch.object(hass.config_entries, "async_forward_entry_setup") as forward_mock: + with patch.object( + hass.config_entries, "async_forward_entry_setups" + ) as forward_mock: assert await hass.config_entries.async_setup(config_entry.entry_id) # Assert platforms loaded await hass.async_block_till_done() - assert forward_mock.call_count == len(PLATFORMS) + forward_mock.assert_called_once_with(config_entry, PLATFORMS) async def test_config_entry_loads_unconnected_cloud( @@ -242,10 +247,12 @@ async def test_config_entry_loads_unconnected_cloud( subscription_factory(capability) for capability in device.capabilities ] smartthings_mock.subscriptions.return_value = subscriptions - with patch.object(hass.config_entries, "async_forward_entry_setup") as forward_mock: + with patch.object( + hass.config_entries, "async_forward_entry_setups" + ) as forward_mock: assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert forward_mock.call_count == len(PLATFORMS) + forward_mock.assert_called_once_with(config_entry, PLATFORMS) async def test_unload_entry(hass: HomeAssistant, config_entry) -> None: @@ -289,11 +296,12 @@ async def test_remove_entry_cloudhook( config_entry.add_to_hass(hass) hass.data[DOMAIN][CONF_CLOUDHOOK_URL] = "https://test.cloud" # Act - with patch.object( - cloud, "async_is_logged_in", return_value=True - ) as mock_async_is_logged_in, patch.object( - cloud, "async_delete_cloudhook" - ) as mock_async_delete_cloudhook: + with ( + patch.object( + cloud, "async_is_logged_in", return_value=True + ) as mock_async_is_logged_in, + patch.object(cloud, "async_delete_cloudhook") as mock_async_delete_cloudhook, + ): await smartthings.async_remove_entry(hass, config_entry) # Assert assert smartthings_mock.delete_installed_app.call_count == 1 diff --git a/tests/components/smartthings/test_light.py b/tests/components/smartthings/test_light.py index b6e4e5a107b..53de2273707 100644 --- a/tests/components/smartthings/test_light.py +++ b/tests/components/smartthings/test_light.py @@ -3,6 +3,7 @@ The only mocking required is of the underlying SmartThings API object so real HTTP calls are not initiated during testing. """ + from pysmartthings import Attribute, Capability import pytest @@ -147,6 +148,8 @@ async def test_turn_off(hass: HomeAssistant, light_devices) -> None: await hass.services.async_call( "light", "turn_off", {"entity_id": "light.color_dimmer_2"}, blocking=True ) + # This test schedules and update right after the call + await hass.async_block_till_done() # Assert state = hass.states.get("light.color_dimmer_2") assert state is not None @@ -164,6 +167,8 @@ async def test_turn_off_with_transition(hass: HomeAssistant, light_devices) -> N {ATTR_ENTITY_ID: "light.color_dimmer_2", ATTR_TRANSITION: 2}, blocking=True, ) + # This test schedules and update right after the call + await hass.async_block_till_done() # Assert state = hass.states.get("light.color_dimmer_2") assert state is not None @@ -178,6 +183,8 @@ async def test_turn_on(hass: HomeAssistant, light_devices) -> None: await hass.services.async_call( "light", "turn_on", {ATTR_ENTITY_ID: "light.color_dimmer_1"}, blocking=True ) + # This test schedules and update right after the call + await hass.async_block_till_done() # Assert state = hass.states.get("light.color_dimmer_1") assert state is not None @@ -199,6 +206,8 @@ async def test_turn_on_with_brightness(hass: HomeAssistant, light_devices) -> No }, blocking=True, ) + # This test schedules and update right after the call + await hass.async_block_till_done() # Assert state = hass.states.get("light.color_dimmer_1") assert state is not None @@ -225,6 +234,8 @@ async def test_turn_on_with_minimal_brightness( {ATTR_ENTITY_ID: "light.color_dimmer_1", ATTR_BRIGHTNESS: 2}, blocking=True, ) + # This test schedules and update right after the call + await hass.async_block_till_done() # Assert state = hass.states.get("light.color_dimmer_1") assert state is not None @@ -244,6 +255,8 @@ async def test_turn_on_with_color(hass: HomeAssistant, light_devices) -> None: {ATTR_ENTITY_ID: "light.color_dimmer_2", ATTR_HS_COLOR: (180, 50)}, blocking=True, ) + # This test schedules and update right after the call + await hass.async_block_till_done() # Assert state = hass.states.get("light.color_dimmer_2") assert state is not None @@ -262,6 +275,8 @@ async def test_turn_on_with_color_temp(hass: HomeAssistant, light_devices) -> No {ATTR_ENTITY_ID: "light.color_dimmer_2", ATTR_COLOR_TEMP: 300}, blocking=True, ) + # This test schedules and update right after the call + await hass.async_block_till_done() # Assert state = hass.states.get("light.color_dimmer_2") assert state is not None diff --git a/tests/components/smartthings/test_lock.py b/tests/components/smartthings/test_lock.py index 10981433c1d..2e149df6213 100644 --- a/tests/components/smartthings/test_lock.py +++ b/tests/components/smartthings/test_lock.py @@ -3,6 +3,7 @@ The only mocking required is of the underlying SmartThings API object so real HTTP calls are not initiated during testing. """ + from pysmartthings import Attribute, Capability from pysmartthings.device import Status diff --git a/tests/components/smartthings/test_scene.py b/tests/components/smartthings/test_scene.py index 489ca87371c..1eaaad55d0f 100644 --- a/tests/components/smartthings/test_scene.py +++ b/tests/components/smartthings/test_scene.py @@ -3,6 +3,7 @@ The only mocking required is of the underlying SmartThings API object so real HTTP calls are not initiated during testing. """ + from homeassistant.components.scene import DOMAIN as SCENE_DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_ON, STATE_UNAVAILABLE diff --git a/tests/components/smartthings/test_sensor.py b/tests/components/smartthings/test_sensor.py index 35d38dd33de..6529a7f25f0 100644 --- a/tests/components/smartthings/test_sensor.py +++ b/tests/components/smartthings/test_sensor.py @@ -3,6 +3,7 @@ The only mocking required is of the underlying SmartThings API object so real HTTP calls are not initiated during testing. """ + from pysmartthings import ATTRIBUTES, CAPABILITIES, Attribute, Capability from homeassistant.components.sensor import ( diff --git a/tests/components/smartthings/test_smartapp.py b/tests/components/smartthings/test_smartapp.py index 155ed8b3ff1..c7861866fad 100644 --- a/tests/components/smartthings/test_smartapp.py +++ b/tests/components/smartthings/test_smartapp.py @@ -1,4 +1,5 @@ """Tests for the smartapp module.""" + from unittest.mock import AsyncMock, Mock, patch from uuid import uuid4 diff --git a/tests/components/smartthings/test_switch.py b/tests/components/smartthings/test_switch.py index e9dd8ad1b68..d858a9eea5a 100644 --- a/tests/components/smartthings/test_switch.py +++ b/tests/components/smartthings/test_switch.py @@ -3,6 +3,7 @@ The only mocking required is of the underlying SmartThings API object so real HTTP calls are not initiated during testing. """ + from pysmartthings import Attribute, Capability from homeassistant.components.smartthings.const import DOMAIN, SIGNAL_SMARTTHINGS_UPDATE diff --git a/tests/components/smarttub/__init__.py b/tests/components/smarttub/__init__.py index f6abd4cb5d7..747a901bbf6 100644 --- a/tests/components/smarttub/__init__.py +++ b/tests/components/smarttub/__init__.py @@ -3,13 +3,14 @@ from datetime import timedelta from homeassistant.components.smarttub.const import SCAN_INTERVAL +from homeassistant.core import HomeAssistant from homeassistant.util import dt as dt_util from tests.common import async_fire_time_changed -async def trigger_update(hass): +async def trigger_update(hass: HomeAssistant) -> None: """Trigger a polling update by moving time forward.""" new_time = dt_util.utcnow() + timedelta(seconds=SCAN_INTERVAL + 1) async_fire_time_changed(hass, new_time) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) diff --git a/tests/components/smarttub/test_binary_sensor.py b/tests/components/smarttub/test_binary_sensor.py index 97ed23d3d0b..3365b03b041 100644 --- a/tests/components/smarttub/test_binary_sensor.py +++ b/tests/components/smarttub/test_binary_sensor.py @@ -1,4 +1,5 @@ """Test the SmartTub binary sensor platform.""" + from datetime import datetime from unittest.mock import create_autospec diff --git a/tests/components/smarttub/test_climate.py b/tests/components/smarttub/test_climate.py index 40e3c05b509..e4e73b8b131 100644 --- a/tests/components/smarttub/test_climate.py +++ b/tests/components/smarttub/test_climate.py @@ -1,4 +1,5 @@ """Test the SmartTub climate platform.""" + import smarttub from homeassistant.components.climate import ( diff --git a/tests/components/smarttub/test_config_flow.py b/tests/components/smarttub/test_config_flow.py index 137b73480ea..df3695f31af 100644 --- a/tests/components/smarttub/test_config_flow.py +++ b/tests/components/smarttub/test_config_flow.py @@ -1,4 +1,5 @@ """Test the smarttub config flow.""" + from unittest.mock import patch from smarttub import LoginFailed diff --git a/tests/components/smarttub/test_init.py b/tests/components/smarttub/test_init.py index 083e0dc8b46..b1eac3fd98b 100644 --- a/tests/components/smarttub/test_init.py +++ b/tests/components/smarttub/test_init.py @@ -1,4 +1,5 @@ """Test smarttub setup process.""" + from unittest.mock import patch from smarttub import LoginFailed diff --git a/tests/components/smarttub/test_light.py b/tests/components/smarttub/test_light.py index f6a3977f373..a3e4e4a5cf7 100644 --- a/tests/components/smarttub/test_light.py +++ b/tests/components/smarttub/test_light.py @@ -1,4 +1,5 @@ """Test the SmartTub light platform.""" + import pytest from smarttub import SpaLight diff --git a/tests/components/smarttub/test_sensor.py b/tests/components/smarttub/test_sensor.py index 5e476dcaaa5..9bdffb62534 100644 --- a/tests/components/smarttub/test_sensor.py +++ b/tests/components/smarttub/test_sensor.py @@ -1,4 +1,5 @@ """Test the SmartTub sensor platform.""" + import pytest import smarttub diff --git a/tests/components/smarttub/test_switch.py b/tests/components/smarttub/test_switch.py index d7e284e9ac2..42fd3d45d88 100644 --- a/tests/components/smarttub/test_switch.py +++ b/tests/components/smarttub/test_switch.py @@ -1,4 +1,5 @@ """Test the SmartTub switch platform.""" + import pytest from homeassistant.const import STATE_OFF, STATE_ON diff --git a/tests/components/smhi/__init__.py b/tests/components/smhi/__init__.py index 377552da4d5..a0bbf854699 100644 --- a/tests/components/smhi/__init__.py +++ b/tests/components/smhi/__init__.py @@ -1,4 +1,5 @@ """Tests for the SMHI component.""" + ENTITY_ID = "weather.smhi_test" TEST_CONFIG = { "name": "test", diff --git a/tests/components/smhi/common.py b/tests/components/smhi/common.py index 8a12cf651b7..7339ba76ac1 100644 --- a/tests/components/smhi/common.py +++ b/tests/components/smhi/common.py @@ -1,4 +1,5 @@ """Common test utilities.""" + from unittest.mock import Mock diff --git a/tests/components/smhi/conftest.py b/tests/components/smhi/conftest.py index c474bc50b51..df6a81a223d 100644 --- a/tests/components/smhi/conftest.py +++ b/tests/components/smhi/conftest.py @@ -1,4 +1,5 @@ """Provide common smhi fixtures.""" + import pytest from homeassistant.components.smhi.const import DOMAIN diff --git a/tests/components/smhi/snapshots/test_weather.ambr b/tests/components/smhi/snapshots/test_weather.ambr index eb7378b5cba..0fef9e19ec3 100644 --- a/tests/components/smhi/snapshots/test_weather.ambr +++ b/tests/components/smhi/snapshots/test_weather.ambr @@ -1,338 +1,4 @@ # serializer version: 1 -# name: test_forecast_daily - dict({ - 'cloud_coverage': 100, - 'condition': 'cloudy', - 'datetime': '2023-08-07T12:00:00', - 'humidity': 96, - 'precipitation': 0.0, - 'pressure': 991.0, - 'temperature': 18.0, - 'templow': 15.0, - 'wind_bearing': 114, - 'wind_gust_speed': 32.76, - 'wind_speed': 10.08, - }) -# --- -# name: test_forecast_daily.1 - dict({ - 'cloud_coverage': 75, - 'condition': 'partlycloudy', - 'datetime': '2023-08-13T12:00:00', - 'humidity': 59, - 'precipitation': 0.0, - 'pressure': 1013.0, - 'temperature': 20.0, - 'templow': 14.0, - 'wind_bearing': 234, - 'wind_gust_speed': 35.64, - 'wind_speed': 14.76, - }) -# --- -# name: test_forecast_daily.2 - dict({ - 'cloud_coverage': 100, - 'condition': 'fog', - 'datetime': '2023-08-07T09:00:00', - 'humidity': 100, - 'precipitation': 0.0, - 'pressure': 992.0, - 'temperature': 18.0, - 'templow': 18.0, - 'wind_bearing': 103, - 'wind_gust_speed': 23.76, - 'wind_speed': 9.72, - }) -# --- -# name: test_forecast_daily.3 - dict({ - 'cloud_coverage': 100, - 'condition': 'cloudy', - 'datetime': '2023-08-07T15:00:00', - 'humidity': 89, - 'precipitation': 0.0, - 'pressure': 991.0, - 'temperature': 16.0, - 'templow': 16.0, - 'wind_bearing': 108, - 'wind_gust_speed': 31.68, - 'wind_speed': 12.24, - }) -# --- -# name: test_forecast_service - dict({ - 'forecast': list([ - dict({ - 'cloud_coverage': 100, - 'condition': 'cloudy', - 'datetime': '2023-08-07T12:00:00', - 'humidity': 96, - 'precipitation': 0.0, - 'pressure': 991.0, - 'temperature': 18.0, - 'templow': 15.0, - 'wind_bearing': 114, - 'wind_gust_speed': 32.76, - 'wind_speed': 10.08, - }), - dict({ - 'cloud_coverage': 100, - 'condition': 'rainy', - 'datetime': '2023-08-08T12:00:00', - 'humidity': 97, - 'precipitation': 10.6, - 'pressure': 984.0, - 'temperature': 15.0, - 'templow': 11.0, - 'wind_bearing': 183, - 'wind_gust_speed': 27.36, - 'wind_speed': 11.16, - }), - dict({ - 'cloud_coverage': 100, - 'condition': 'rainy', - 'datetime': '2023-08-09T12:00:00', - 'humidity': 95, - 'precipitation': 6.3, - 'pressure': 1001.0, - 'temperature': 12.0, - 'templow': 11.0, - 'wind_bearing': 166, - 'wind_gust_speed': 48.24, - 'wind_speed': 18.0, - }), - dict({ - 'cloud_coverage': 100, - 'condition': 'cloudy', - 'datetime': '2023-08-10T12:00:00', - 'humidity': 75, - 'precipitation': 4.8, - 'pressure': 1011.0, - 'temperature': 14.0, - 'templow': 10.0, - 'wind_bearing': 174, - 'wind_gust_speed': 29.16, - 'wind_speed': 11.16, - }), - dict({ - 'cloud_coverage': 100, - 'condition': 'cloudy', - 'datetime': '2023-08-11T12:00:00', - 'humidity': 69, - 'precipitation': 0.6, - 'pressure': 1015.0, - 'temperature': 18.0, - 'templow': 12.0, - 'wind_bearing': 197, - 'wind_gust_speed': 27.36, - 'wind_speed': 10.08, - }), - dict({ - 'cloud_coverage': 100, - 'condition': 'cloudy', - 'datetime': '2023-08-12T12:00:00', - 'humidity': 82, - 'precipitation': 0.0, - 'pressure': 1014.0, - 'temperature': 17.0, - 'templow': 12.0, - 'wind_bearing': 225, - 'wind_gust_speed': 28.08, - 'wind_speed': 8.64, - }), - dict({ - 'cloud_coverage': 75, - 'condition': 'partlycloudy', - 'datetime': '2023-08-13T12:00:00', - 'humidity': 59, - 'precipitation': 0.0, - 'pressure': 1013.0, - 'temperature': 20.0, - 'templow': 14.0, - 'wind_bearing': 234, - 'wind_gust_speed': 35.64, - 'wind_speed': 14.76, - }), - dict({ - 'cloud_coverage': 100, - 'condition': 'partlycloudy', - 'datetime': '2023-08-14T12:00:00', - 'humidity': 56, - 'precipitation': 0.0, - 'pressure': 1015.0, - 'temperature': 21.0, - 'templow': 14.0, - 'wind_bearing': 216, - 'wind_gust_speed': 33.12, - 'wind_speed': 13.68, - }), - dict({ - 'cloud_coverage': 88, - 'condition': 'partlycloudy', - 'datetime': '2023-08-15T12:00:00', - 'humidity': 64, - 'precipitation': 3.6, - 'pressure': 1014.0, - 'temperature': 20.0, - 'templow': 14.0, - 'wind_bearing': 226, - 'wind_gust_speed': 33.12, - 'wind_speed': 13.68, - }), - dict({ - 'cloud_coverage': 75, - 'condition': 'partlycloudy', - 'datetime': '2023-08-16T12:00:00', - 'humidity': 61, - 'precipitation': 2.4, - 'pressure': 1014.0, - 'temperature': 20.0, - 'templow': 14.0, - 'wind_bearing': 233, - 'wind_gust_speed': 33.48, - 'wind_speed': 14.04, - }), - ]), - }) -# --- -# name: test_forecast_service[forecast] - dict({ - 'weather.smhi_test': dict({ - 'forecast': list([ - dict({ - 'cloud_coverage': 100, - 'condition': 'cloudy', - 'datetime': '2023-08-07T12:00:00', - 'humidity': 96, - 'precipitation': 0.0, - 'pressure': 991.0, - 'temperature': 18.0, - 'templow': 15.0, - 'wind_bearing': 114, - 'wind_gust_speed': 32.76, - 'wind_speed': 10.08, - }), - dict({ - 'cloud_coverage': 100, - 'condition': 'rainy', - 'datetime': '2023-08-08T12:00:00', - 'humidity': 97, - 'precipitation': 10.6, - 'pressure': 984.0, - 'temperature': 15.0, - 'templow': 11.0, - 'wind_bearing': 183, - 'wind_gust_speed': 27.36, - 'wind_speed': 11.16, - }), - dict({ - 'cloud_coverage': 100, - 'condition': 'rainy', - 'datetime': '2023-08-09T12:00:00', - 'humidity': 95, - 'precipitation': 6.3, - 'pressure': 1001.0, - 'temperature': 12.0, - 'templow': 11.0, - 'wind_bearing': 166, - 'wind_gust_speed': 48.24, - 'wind_speed': 18.0, - }), - dict({ - 'cloud_coverage': 100, - 'condition': 'cloudy', - 'datetime': '2023-08-10T12:00:00', - 'humidity': 75, - 'precipitation': 4.8, - 'pressure': 1011.0, - 'temperature': 14.0, - 'templow': 10.0, - 'wind_bearing': 174, - 'wind_gust_speed': 29.16, - 'wind_speed': 11.16, - }), - dict({ - 'cloud_coverage': 100, - 'condition': 'cloudy', - 'datetime': '2023-08-11T12:00:00', - 'humidity': 69, - 'precipitation': 0.6, - 'pressure': 1015.0, - 'temperature': 18.0, - 'templow': 12.0, - 'wind_bearing': 197, - 'wind_gust_speed': 27.36, - 'wind_speed': 10.08, - }), - dict({ - 'cloud_coverage': 100, - 'condition': 'cloudy', - 'datetime': '2023-08-12T12:00:00', - 'humidity': 82, - 'precipitation': 0.0, - 'pressure': 1014.0, - 'temperature': 17.0, - 'templow': 12.0, - 'wind_bearing': 225, - 'wind_gust_speed': 28.08, - 'wind_speed': 8.64, - }), - dict({ - 'cloud_coverage': 75, - 'condition': 'partlycloudy', - 'datetime': '2023-08-13T12:00:00', - 'humidity': 59, - 'precipitation': 0.0, - 'pressure': 1013.0, - 'temperature': 20.0, - 'templow': 14.0, - 'wind_bearing': 234, - 'wind_gust_speed': 35.64, - 'wind_speed': 14.76, - }), - dict({ - 'cloud_coverage': 100, - 'condition': 'partlycloudy', - 'datetime': '2023-08-14T12:00:00', - 'humidity': 56, - 'precipitation': 0.0, - 'pressure': 1015.0, - 'temperature': 21.0, - 'templow': 14.0, - 'wind_bearing': 216, - 'wind_gust_speed': 33.12, - 'wind_speed': 13.68, - }), - dict({ - 'cloud_coverage': 88, - 'condition': 'partlycloudy', - 'datetime': '2023-08-15T12:00:00', - 'humidity': 64, - 'precipitation': 3.6, - 'pressure': 1014.0, - 'temperature': 20.0, - 'templow': 14.0, - 'wind_bearing': 226, - 'wind_gust_speed': 33.12, - 'wind_speed': 13.68, - }), - dict({ - 'cloud_coverage': 75, - 'condition': 'partlycloudy', - 'datetime': '2023-08-16T12:00:00', - 'humidity': 61, - 'precipitation': 2.4, - 'pressure': 1014.0, - 'temperature': 20.0, - 'templow': 14.0, - 'wind_bearing': 233, - 'wind_gust_speed': 33.48, - 'wind_speed': 14.04, - }), - ]), - }), - }) -# --- # name: test_forecast_service[get_forecast] dict({ 'forecast': list([ @@ -671,138 +337,6 @@ ReadOnlyDict({ 'attribution': 'Swedish weather institute (SMHI)', 'cloud_coverage': 100, - 'forecast': list([ - dict({ - 'cloud_coverage': 100, - 'condition': 'cloudy', - 'datetime': '2023-08-07T12:00:00', - 'humidity': 96, - 'precipitation': 0.0, - 'pressure': 991.0, - 'temperature': 18.0, - 'templow': 15.0, - 'wind_bearing': 114, - 'wind_gust_speed': 32.76, - 'wind_speed': 10.08, - }), - dict({ - 'cloud_coverage': 100, - 'condition': 'rainy', - 'datetime': '2023-08-08T12:00:00', - 'humidity': 97, - 'precipitation': 10.6, - 'pressure': 984.0, - 'temperature': 15.0, - 'templow': 11.0, - 'wind_bearing': 183, - 'wind_gust_speed': 27.36, - 'wind_speed': 11.16, - }), - dict({ - 'cloud_coverage': 100, - 'condition': 'rainy', - 'datetime': '2023-08-09T12:00:00', - 'humidity': 95, - 'precipitation': 6.3, - 'pressure': 1001.0, - 'temperature': 12.0, - 'templow': 11.0, - 'wind_bearing': 166, - 'wind_gust_speed': 48.24, - 'wind_speed': 18.0, - }), - dict({ - 'cloud_coverage': 100, - 'condition': 'cloudy', - 'datetime': '2023-08-10T12:00:00', - 'humidity': 75, - 'precipitation': 4.8, - 'pressure': 1011.0, - 'temperature': 14.0, - 'templow': 10.0, - 'wind_bearing': 174, - 'wind_gust_speed': 29.16, - 'wind_speed': 11.16, - }), - dict({ - 'cloud_coverage': 100, - 'condition': 'cloudy', - 'datetime': '2023-08-11T12:00:00', - 'humidity': 69, - 'precipitation': 0.6, - 'pressure': 1015.0, - 'temperature': 18.0, - 'templow': 12.0, - 'wind_bearing': 197, - 'wind_gust_speed': 27.36, - 'wind_speed': 10.08, - }), - dict({ - 'cloud_coverage': 100, - 'condition': 'cloudy', - 'datetime': '2023-08-12T12:00:00', - 'humidity': 82, - 'precipitation': 0.0, - 'pressure': 1014.0, - 'temperature': 17.0, - 'templow': 12.0, - 'wind_bearing': 225, - 'wind_gust_speed': 28.08, - 'wind_speed': 8.64, - }), - dict({ - 'cloud_coverage': 75, - 'condition': 'partlycloudy', - 'datetime': '2023-08-13T12:00:00', - 'humidity': 59, - 'precipitation': 0.0, - 'pressure': 1013.0, - 'temperature': 20.0, - 'templow': 14.0, - 'wind_bearing': 234, - 'wind_gust_speed': 35.64, - 'wind_speed': 14.76, - }), - dict({ - 'cloud_coverage': 100, - 'condition': 'partlycloudy', - 'datetime': '2023-08-14T12:00:00', - 'humidity': 56, - 'precipitation': 0.0, - 'pressure': 1015.0, - 'temperature': 21.0, - 'templow': 14.0, - 'wind_bearing': 216, - 'wind_gust_speed': 33.12, - 'wind_speed': 13.68, - }), - dict({ - 'cloud_coverage': 88, - 'condition': 'partlycloudy', - 'datetime': '2023-08-15T12:00:00', - 'humidity': 64, - 'precipitation': 3.6, - 'pressure': 1014.0, - 'temperature': 20.0, - 'templow': 14.0, - 'wind_bearing': 226, - 'wind_gust_speed': 33.12, - 'wind_speed': 13.68, - }), - dict({ - 'cloud_coverage': 75, - 'condition': 'partlycloudy', - 'datetime': '2023-08-16T12:00:00', - 'humidity': 61, - 'precipitation': 2.4, - 'pressure': 1014.0, - 'temperature': 20.0, - 'templow': 14.0, - 'wind_bearing': 233, - 'wind_gust_speed': 33.48, - 'wind_speed': 14.04, - }), - ]), 'friendly_name': 'test', 'humidity': 100, 'precipitation_unit': , @@ -820,18 +354,3 @@ 'wind_speed_unit': , }) # --- -# name: test_setup_hass.1 - dict({ - 'cloud_coverage': 100, - 'condition': 'rainy', - 'datetime': '2023-08-08T12:00:00', - 'humidity': 97, - 'precipitation': 10.6, - 'pressure': 984.0, - 'temperature': 15.0, - 'templow': 11.0, - 'wind_bearing': 183, - 'wind_gust_speed': 27.36, - 'wind_speed': 11.16, - }) -# --- diff --git a/tests/components/smhi/test_config_flow.py b/tests/components/smhi/test_config_flow.py index f33849694c8..7d8701eca45 100644 --- a/tests/components/smhi/test_config_flow.py +++ b/tests/components/smhi/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Smhi config flow.""" + from __future__ import annotations from unittest.mock import patch @@ -7,9 +8,11 @@ from smhi.smhi_lib import SmhiForecastException from homeassistant import config_entries from homeassistant.components.smhi.const import DOMAIN +from homeassistant.components.weather import DOMAIN as WEATHER_DOMAIN from homeassistant.const import CONF_LATITUDE, CONF_LOCATION, CONF_LONGITUDE from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers import device_registry as dr, entity_registry as er from tests.common import MockConfigEntry @@ -26,13 +29,16 @@ async def test_form(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.FORM assert result["errors"] == {} - with patch( - "homeassistant.components.smhi.config_flow.Smhi.async_get_forecast", - return_value={"test": "something", "test2": "something else"}, - ), patch( - "homeassistant.components.smhi.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.smhi.config_flow.Smhi.async_get_forecast", + return_value={"test": "something", "test2": "something else"}, + ), + patch( + "homeassistant.components.smhi.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -59,12 +65,15 @@ async def test_form(hass: HomeAssistant) -> None: result3 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch( - "homeassistant.components.smhi.config_flow.Smhi.async_get_forecast", - return_value={"test": "something", "test2": "something else"}, - ), patch( - "homeassistant.components.smhi.async_setup_entry", - return_value=True, + with ( + patch( + "homeassistant.components.smhi.config_flow.Smhi.async_get_forecast", + return_value={"test": "something", "test2": "something else"}, + ), + patch( + "homeassistant.components.smhi.async_setup_entry", + return_value=True, + ), ): result4 = await hass.config_entries.flow.async_configure( result3["flow_id"], @@ -113,12 +122,15 @@ async def test_form_invalid_coordinates(hass: HomeAssistant) -> None: assert result2["errors"] == {"base": "wrong_location"} # Continue flow with new coordinates - with patch( - "homeassistant.components.smhi.config_flow.Smhi.async_get_forecast", - return_value={"test": "something", "test2": "something else"}, - ), patch( - "homeassistant.components.smhi.async_setup_entry", - return_value=True, + with ( + patch( + "homeassistant.components.smhi.config_flow.Smhi.async_get_forecast", + return_value={"test": "something", "test2": "something else"}, + ), + patch( + "homeassistant.components.smhi.async_setup_entry", + return_value=True, + ), ): result3 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -177,3 +189,98 @@ async def test_form_unique_id_exist(hass: HomeAssistant) -> None: assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "already_configured" + + +async def test_reconfigure_flow( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, +) -> None: + """Test re-configuration flow.""" + entry = MockConfigEntry( + domain=DOMAIN, + title="Home", + unique_id="57.2898-13.6304", + data={"location": {"latitude": 57.2898, "longitude": 13.6304}, "name": "Home"}, + version=2, + ) + entry.add_to_hass(hass) + + entity = entity_registry.async_get_or_create( + WEATHER_DOMAIN, DOMAIN, "57.2898, 13.6304" + ) + device = device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + identifiers={(DOMAIN, "57.2898, 13.6304")}, + manufacturer="SMHI", + model="v2", + name=entry.title, + ) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_RECONFIGURE, + "entry_id": entry.entry_id, + }, + ) + assert result["type"] == FlowResultType.FORM + + with patch( + "homeassistant.components.smhi.config_flow.Smhi.async_get_forecast", + side_effect=SmhiForecastException, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_LOCATION: { + CONF_LATITUDE: 0.0, + CONF_LONGITUDE: 0.0, + } + }, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {"base": "wrong_location"} + + with ( + patch( + "homeassistant.components.smhi.config_flow.Smhi.async_get_forecast", + return_value={"test": "something", "test2": "something else"}, + ), + patch( + "homeassistant.components.smhi.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_LOCATION: { + CONF_LATITUDE: 58.2898, + CONF_LONGITUDE: 14.6304, + } + }, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + entry = hass.config_entries.async_get_entry(entry.entry_id) + assert entry.title == "Home" + assert entry.unique_id == "58.2898-14.6304" + assert entry.data == { + "location": { + "latitude": 58.2898, + "longitude": 14.6304, + }, + "name": "Home", + } + entity = entity_registry.async_get(entity.entity_id) + assert entity + assert entity.unique_id == "58.2898, 14.6304" + device = device_registry.async_get(device.id) + assert device + assert device.identifiers == {(DOMAIN, "58.2898, 14.6304")} + assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/smhi/test_init.py b/tests/components/smhi/test_init.py index ec6e4c417bb..aefbccb64ec 100644 --- a/tests/components/smhi/test_init.py +++ b/tests/components/smhi/test_init.py @@ -1,4 +1,5 @@ """Test SMHI component setup process.""" + from unittest.mock import patch from smhi.smhi_lib import APIURL_TEMPLATE diff --git a/tests/components/smhi/test_weather.py b/tests/components/smhi/test_weather.py index 2ad9153dd41..4d187e7c728 100644 --- a/tests/components/smhi/test_weather.py +++ b/tests/components/smhi/test_weather.py @@ -1,4 +1,5 @@ """Test for the smhi weather entity.""" + from datetime import datetime, timedelta from unittest.mock import patch @@ -9,7 +10,6 @@ from syrupy.assertion import SnapshotAssertion from homeassistant.components.smhi.const import ATTR_SMHI_THUNDER_PROBABILITY from homeassistant.components.smhi.weather import CONDITION_CLASSES, RETRY_TIMEOUT from homeassistant.components.weather import ( - ATTR_FORECAST, ATTR_FORECAST_CONDITION, ATTR_WEATHER_HUMIDITY, ATTR_WEATHER_PRESSURE, @@ -64,10 +64,6 @@ async def test_setup_hass( assert state assert state.state == "fog" assert state.attributes == snapshot - assert len(state.attributes["forecast"]) == 10 - - forecast = state.attributes["forecast"][1] - assert forecast == snapshot async def test_properties_no_data(hass: HomeAssistant) -> None: @@ -94,7 +90,6 @@ async def test_properties_no_data(hass: HomeAssistant) -> None: assert ATTR_WEATHER_VISIBILITY not in state.attributes assert ATTR_WEATHER_WIND_SPEED not in state.attributes assert ATTR_WEATHER_WIND_BEARING not in state.attributes - assert ATTR_FORECAST not in state.attributes assert ATTR_WEATHER_CLOUD_COVERAGE not in state.attributes assert ATTR_SMHI_THUNDER_PROBABILITY not in state.attributes assert ATTR_WEATHER_WIND_GUST_SPEED not in state.attributes @@ -164,12 +159,15 @@ async def test_properties_unknown_symbol(hass: HomeAssistant) -> None: entry = MockConfigEntry(domain="smhi", data=TEST_CONFIG, version=2) entry.add_to_hass(hass) - with patch( - "homeassistant.components.smhi.weather.Smhi.async_get_forecast", - return_value=testdata, - ), patch( - "homeassistant.components.smhi.weather.Smhi.async_get_forecast_hour", - return_value=None, + with ( + patch( + "homeassistant.components.smhi.weather.Smhi.async_get_forecast", + return_value=testdata, + ), + patch( + "homeassistant.components.smhi.weather.Smhi.async_get_forecast_hour", + return_value=None, + ), ): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -179,10 +177,16 @@ async def test_properties_unknown_symbol(hass: HomeAssistant) -> None: assert state assert state.name == "test" assert state.state == STATE_UNKNOWN - assert ATTR_FORECAST in state.attributes + response = await hass.services.async_call( + WEATHER_DOMAIN, + SERVICE_GET_FORECASTS, + {"entity_id": ENTITY_ID, "type": "daily"}, + blocking=True, + return_response=True, + ) assert all( forecast[ATTR_FORECAST_CONDITION] is None - for forecast in state.attributes[ATTR_FORECAST] + for forecast in response[ENTITY_ID]["forecast"] ) diff --git a/tests/components/smtp/test_notify.py b/tests/components/smtp/test_notify.py index 182b45d9c1b..b27a7c2d863 100644 --- a/tests/components/smtp/test_notify.py +++ b/tests/components/smtp/test_notify.py @@ -1,4 +1,5 @@ """The tests for the notify smtp platform.""" + from pathlib import Path import re from unittest.mock import patch @@ -50,8 +51,11 @@ async def test_reload_notify(hass: HomeAssistant) -> None: assert hass.services.has_service(notify.DOMAIN, DOMAIN) yaml_path = get_fixture_path("configuration.yaml", "smtp") - with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path), patch( - "homeassistant.components.smtp.notify.MailNotificationService.connection_is_valid" + with ( + patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path), + patch( + "homeassistant.components.smtp.notify.MailNotificationService.connection_is_valid" + ), ): await hass.services.async_call( DOMAIN, @@ -165,9 +169,10 @@ def test_sending_insecure_files_fails( """Verify if we cannot send messages with insecure attachments.""" sample_email = "" message.hass = hass - with patch("email.utils.make_msgid", return_value=sample_email), pytest.raises( - ServiceValidationError - ) as exc: + with ( + patch("email.utils.make_msgid", return_value=sample_email), + pytest.raises(ServiceValidationError) as exc, + ): result, _ = message.send_message(message_data, data=data) assert content_type in result assert exc.value.translation_key == "remote_path_not_allowed" diff --git a/tests/components/snapcast/conftest.py b/tests/components/snapcast/conftest.py index 00d031192d8..7d29b098482 100644 --- a/tests/components/snapcast/conftest.py +++ b/tests/components/snapcast/conftest.py @@ -1,4 +1,5 @@ """Test the snapcast config flow.""" + from collections.abc import Generator from unittest.mock import AsyncMock, patch diff --git a/tests/components/snips/test_init.py b/tests/components/snips/test_init.py index da2f23bc49c..9590473f218 100644 --- a/tests/components/snips/test_init.py +++ b/tests/components/snips/test_init.py @@ -1,4 +1,5 @@ """Test the Snips component.""" + import json import logging diff --git a/tests/components/snmp/test_float_sensor.py b/tests/components/snmp/test_float_sensor.py new file mode 100644 index 00000000000..0e11ee03968 --- /dev/null +++ b/tests/components/snmp/test_float_sensor.py @@ -0,0 +1,79 @@ +"""SNMP sensor tests.""" + +from unittest.mock import patch + +from pysnmp.proto.rfc1902 import Opaque +import pytest + +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er +from homeassistant.setup import async_setup_component + + +@pytest.fixture(autouse=True) +def hlapi_mock(): + """Mock out 3rd party API.""" + mock_data = Opaque(value=b"\x9fx\x04=\xa4\x00\x00") + with patch( + "homeassistant.components.snmp.sensor.getCmd", + return_value=(None, None, None, [[mock_data]]), + ): + yield + + +async def test_basic_config(hass: HomeAssistant) -> None: + """Test basic entity configuration.""" + + config = { + SENSOR_DOMAIN: { + "platform": "snmp", + "host": "192.168.1.32", + "baseoid": "1.3.6.1.4.1.2021.10.1.3.1", + }, + } + + assert await async_setup_component(hass, SENSOR_DOMAIN, config) + await hass.async_block_till_done() + + state = hass.states.get("sensor.snmp") + assert state.state == "0.080078125" + assert state.attributes == {"friendly_name": "SNMP"} + + +async def test_entity_config(hass: HomeAssistant) -> None: + """Test entity configuration.""" + + config = { + SENSOR_DOMAIN: { + # SNMP configuration + "platform": "snmp", + "host": "192.168.1.32", + "baseoid": "1.3.6.1.4.1.2021.10.1.3.1", + # Entity configuration + "icon": "{{'mdi:one_two_three'}}", + "picture": "{{'blabla.png'}}", + "device_class": "temperature", + "name": "{{'SNMP' + ' ' + 'Sensor'}}", + "state_class": "measurement", + "unique_id": "very_unique", + "unit_of_measurement": "°C", + }, + } + + assert await async_setup_component(hass, SENSOR_DOMAIN, config) + await hass.async_block_till_done() + + entity_registry = er.async_get(hass) + assert entity_registry.async_get("sensor.snmp_sensor").unique_id == "very_unique" + + state = hass.states.get("sensor.snmp_sensor") + assert state.state == "0.080078125" + assert state.attributes == { + "device_class": "temperature", + "entity_picture": "blabla.png", + "friendly_name": "SNMP Sensor", + "icon": "mdi:one_two_three", + "state_class": "measurement", + "unit_of_measurement": "°C", + } diff --git a/tests/components/snmp/test_sensor.py b/tests/components/snmp/test_integer_sensor.py similarity index 91% rename from tests/components/snmp/test_sensor.py rename to tests/components/snmp/test_integer_sensor.py index d6637946da8..0ea9ac4d434 100644 --- a/tests/components/snmp/test_sensor.py +++ b/tests/components/snmp/test_integer_sensor.py @@ -1,7 +1,8 @@ """SNMP sensor tests.""" -from unittest.mock import MagicMock, Mock, patch +from unittest.mock import patch +from pysnmp.hlapi import Integer32 import pytest from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN @@ -13,8 +14,7 @@ from homeassistant.setup import async_setup_component @pytest.fixture(autouse=True) def hlapi_mock(): """Mock out 3rd party API.""" - mock_data = MagicMock() - mock_data.prettyPrint = Mock(return_value="13.5") + mock_data = Integer32(13) with patch( "homeassistant.components.snmp.sensor.getCmd", return_value=(None, None, None, [[mock_data]]), @@ -37,7 +37,7 @@ async def test_basic_config(hass: HomeAssistant) -> None: await hass.async_block_till_done() state = hass.states.get("sensor.snmp") - assert state.state == "13.5" + assert state.state == "13" assert state.attributes == {"friendly_name": "SNMP"} @@ -68,7 +68,7 @@ async def test_entity_config(hass: HomeAssistant) -> None: assert entity_registry.async_get("sensor.snmp_sensor").unique_id == "very_unique" state = hass.states.get("sensor.snmp_sensor") - assert state.state == "13.5" + assert state.state == "13" assert state.attributes == { "device_class": "temperature", "entity_picture": "blabla.png", diff --git a/tests/components/snmp/test_string_sensor.py b/tests/components/snmp/test_string_sensor.py new file mode 100644 index 00000000000..536b819b711 --- /dev/null +++ b/tests/components/snmp/test_string_sensor.py @@ -0,0 +1,73 @@ +"""SNMP sensor tests.""" + +from unittest.mock import patch + +from pysnmp.proto.rfc1902 import OctetString +import pytest + +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er +from homeassistant.setup import async_setup_component + + +@pytest.fixture(autouse=True) +def hlapi_mock(): + """Mock out 3rd party API.""" + mock_data = OctetString("98F") + with patch( + "homeassistant.components.snmp.sensor.getCmd", + return_value=(None, None, None, [[mock_data]]), + ): + yield + + +async def test_basic_config(hass: HomeAssistant) -> None: + """Test basic entity configuration.""" + + config = { + SENSOR_DOMAIN: { + "platform": "snmp", + "host": "192.168.1.32", + "baseoid": "1.3.6.1.4.1.2021.10.1.3.1", + }, + } + + assert await async_setup_component(hass, SENSOR_DOMAIN, config) + await hass.async_block_till_done() + + state = hass.states.get("sensor.snmp") + assert state.state == "98F" + assert state.attributes == {"friendly_name": "SNMP"} + + +async def test_entity_config(hass: HomeAssistant) -> None: + """Test entity configuration.""" + + config = { + SENSOR_DOMAIN: { + # SNMP configuration + "platform": "snmp", + "host": "192.168.1.32", + "baseoid": "1.3.6.1.4.1.2021.10.1.3.1", + # Entity configuration + "icon": "{{'mdi:one_two_three'}}", + "picture": "{{'blabla.png'}}", + "name": "{{'SNMP' + ' ' + 'Sensor'}}", + "unique_id": "very_unique", + }, + } + + assert await async_setup_component(hass, SENSOR_DOMAIN, config) + await hass.async_block_till_done() + + entity_registry = er.async_get(hass) + assert entity_registry.async_get("sensor.snmp_sensor").unique_id == "very_unique" + + state = hass.states.get("sensor.snmp_sensor") + assert state.state == "98F" + assert state.attributes == { + "entity_picture": "blabla.png", + "friendly_name": "SNMP Sensor", + "icon": "mdi:one_two_three", + } diff --git a/tests/components/snooz/__init__.py b/tests/components/snooz/__init__.py index 1e414fb337c..c314fde5c90 100644 --- a/tests/components/snooz/__init__.py +++ b/tests/components/snooz/__init__.py @@ -1,4 +1,5 @@ """Tests for the Snooz component.""" + from __future__ import annotations from dataclasses import dataclass @@ -99,11 +100,12 @@ async def create_mock_snooz_config_entry( ) -> MockConfigEntry: """Create a mock config entry.""" - with patch( - "homeassistant.components.snooz.SnoozDevice", return_value=device - ), patch( - "homeassistant.components.snooz.async_ble_device_from_address", - return_value=generate_ble_device(device.address, device.name), + with ( + patch("homeassistant.components.snooz.SnoozDevice", return_value=device), + patch( + "homeassistant.components.snooz.async_ble_device_from_address", + return_value=generate_ble_device(device.address, device.name), + ), ): entry = MockConfigEntry( domain=DOMAIN, diff --git a/tests/components/snooz/conftest.py b/tests/components/snooz/conftest.py index cb4873e7937..8cdc2ec0982 100644 --- a/tests/components/snooz/conftest.py +++ b/tests/components/snooz/conftest.py @@ -1,4 +1,5 @@ """Snooz test fixtures and configuration.""" + from __future__ import annotations import pytest diff --git a/tests/components/snooz/test_config_flow.py b/tests/components/snooz/test_config_flow.py index 385a47cf578..172ca3cd143 100644 --- a/tests/components/snooz/test_config_flow.py +++ b/tests/components/snooz/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Snooz config flow.""" + from __future__ import annotations from asyncio import Event diff --git a/tests/components/snooz/test_fan.py b/tests/components/snooz/test_fan.py index 795525fdf71..ddc93a4ba1f 100644 --- a/tests/components/snooz/test_fan.py +++ b/tests/components/snooz/test_fan.py @@ -1,4 +1,5 @@ """Test Snooz fan entity.""" + from __future__ import annotations from datetime import timedelta diff --git a/tests/components/snooz/test_init.py b/tests/components/snooz/test_init.py index a7a0566d7c6..b1ab06fcc8e 100644 --- a/tests/components/snooz/test_init.py +++ b/tests/components/snooz/test_init.py @@ -1,4 +1,5 @@ """Test Snooz configuration.""" + from __future__ import annotations import pytest diff --git a/tests/components/solaredge/test_config_flow.py b/tests/components/solaredge/test_config_flow.py index c23d2578d1c..81b97c071fd 100644 --- a/tests/components/solaredge/test_config_flow.py +++ b/tests/components/solaredge/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for the SolarEdge config flow.""" + from unittest.mock import Mock, patch import pytest diff --git a/tests/components/solaredge/test_coordinator.py b/tests/components/solaredge/test_coordinator.py index de9aab016ee..b1496d18d93 100644 --- a/tests/components/solaredge/test_coordinator.py +++ b/tests/components/solaredge/test_coordinator.py @@ -1,4 +1,5 @@ """Tests for the SolarEdge coordinator services.""" + from unittest.mock import patch from freezegun.api import FrozenDateTimeFactory @@ -52,7 +53,7 @@ async def test_solaredgeoverviewdataservice_energy_values_validity( mock_solaredge().get_overview.return_value = mock_overview_data freezer.tick(OVERVIEW_UPDATE_DELAY) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get("sensor.solaredge_lifetime_energy") assert state assert state.state == str(mock_overview_data["overview"]["lifeTimeData"]["energy"]) @@ -62,7 +63,7 @@ async def test_solaredgeoverviewdataservice_energy_values_validity( mock_solaredge().get_overview.return_value = mock_overview_data freezer.tick(OVERVIEW_UPDATE_DELAY) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get("sensor.solaredge_lifetime_energy") assert state @@ -73,7 +74,7 @@ async def test_solaredgeoverviewdataservice_energy_values_validity( mock_solaredge().get_overview.return_value = mock_overview_data freezer.tick(OVERVIEW_UPDATE_DELAY) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get("sensor.solaredge_lifetime_energy") assert state @@ -84,7 +85,7 @@ async def test_solaredgeoverviewdataservice_energy_values_validity( mock_solaredge().get_overview.return_value = mock_overview_data freezer.tick(OVERVIEW_UPDATE_DELAY) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get("sensor.solaredge_energy_this_year") assert state @@ -102,7 +103,7 @@ async def test_solaredgeoverviewdataservice_energy_values_validity( mock_solaredge().get_overview.return_value = mock_overview_data freezer.tick(OVERVIEW_UPDATE_DELAY) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get("sensor.solaredge_lifetime_energy") assert state diff --git a/tests/components/solarlog/test_config_flow.py b/tests/components/solarlog/test_config_flow.py index 9383f517104..16f25264b9d 100644 --- a/tests/components/solarlog/test_config_flow.py +++ b/tests/components/solarlog/test_config_flow.py @@ -1,4 +1,5 @@ """Test the solarlog config flow.""" + from unittest.mock import patch import pytest @@ -24,13 +25,16 @@ async def test_form(hass: HomeAssistant) -> None: assert result["type"] == "form" assert result["errors"] == {} - with patch( - "homeassistant.components.solarlog.config_flow.SolarLogConfigFlow._test_connection", - return_value={"title": "solarlog test 1 2 3"}, - ), patch( - "homeassistant.components.solarlog.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.solarlog.config_flow.SolarLogConfigFlow._test_connection", + return_value={"title": "solarlog test 1 2 3"}, + ), + patch( + "homeassistant.components.solarlog.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": HOST, "name": NAME} ) diff --git a/tests/components/solax/test_config_flow.py b/tests/components/solax/test_config_flow.py index 23be906137b..c671fe39cec 100644 --- a/tests/components/solax/test_config_flow.py +++ b/tests/components/solax/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for the solax config flow.""" + from unittest.mock import patch from solax import RealTimeAPI @@ -29,13 +30,17 @@ async def test_form_success(hass: HomeAssistant) -> None: assert flow["type"] == "form" assert flow["errors"] == {} - with patch( - "homeassistant.components.solax.config_flow.real_time_api", - return_value=__mock_real_time_api_success(), - ), patch("solax.RealTimeAPI.get_data", return_value=__mock_get_data()), patch( - "homeassistant.components.solax.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.solax.config_flow.real_time_api", + return_value=__mock_real_time_api_success(), + ), + patch("solax.RealTimeAPI.get_data", return_value=__mock_get_data()), + patch( + "homeassistant.components.solax.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): entry_result = await hass.config_entries.flow.async_configure( flow["flow_id"], {CONF_IP_ADDRESS: "192.168.1.87", CONF_PORT: 80, CONF_PASSWORD: "password"}, diff --git a/tests/components/soma/test_config_flow.py b/tests/components/soma/test_config_flow.py index 91a0d27b428..04a93bb5a58 100644 --- a/tests/components/soma/test_config_flow.py +++ b/tests/components/soma/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for the Soma config flow.""" + from unittest.mock import patch from api.soma_api import SomaApi diff --git a/tests/components/somfy_mylink/test_config_flow.py b/tests/components/somfy_mylink/test_config_flow.py index 68f0bc0a04c..a01f4d640a1 100644 --- a/tests/components/somfy_mylink/test_config_flow.py +++ b/tests/components/somfy_mylink/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Somfy MyLink config flow.""" + from unittest.mock import patch import pytest @@ -25,13 +26,16 @@ async def test_form_user(hass: HomeAssistant) -> None: assert result["type"] == "form" assert result["errors"] == {} - with patch( - "homeassistant.components.somfy_mylink.config_flow.SomfyMyLinkSynergy.status_info", - return_value={"any": "data"}, - ), patch( - "homeassistant.components.somfy_mylink.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.somfy_mylink.config_flow.SomfyMyLinkSynergy.status_info", + return_value={"any": "data"}, + ), + patch( + "homeassistant.components.somfy_mylink.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -66,13 +70,16 @@ async def test_form_user_already_configured(hass: HomeAssistant) -> None: assert result["type"] == "form" assert result["errors"] == {} - with patch( - "homeassistant.components.somfy_mylink.config_flow.SomfyMyLinkSynergy.status_info", - return_value={"any": "data"}, - ), patch( - "homeassistant.components.somfy_mylink.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.somfy_mylink.config_flow.SomfyMyLinkSynergy.status_info", + return_value={"any": "data"}, + ), + patch( + "homeassistant.components.somfy_mylink.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -242,13 +249,16 @@ async def test_form_user_already_configured_from_dhcp(hass: HomeAssistant) -> No ) config_entry.add_to_hass(hass) - with patch( - "homeassistant.components.somfy_mylink.config_flow.SomfyMyLinkSynergy.status_info", - return_value={"any": "data"}, - ), patch( - "homeassistant.components.somfy_mylink.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.somfy_mylink.config_flow.SomfyMyLinkSynergy.status_info", + return_value={"any": "data"}, + ), + patch( + "homeassistant.components.somfy_mylink.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, @@ -300,13 +310,16 @@ async def test_dhcp_discovery(hass: HomeAssistant) -> None: assert result["type"] == "form" assert result["errors"] == {} - with patch( - "homeassistant.components.somfy_mylink.config_flow.SomfyMyLinkSynergy.status_info", - return_value={"any": "data"}, - ), patch( - "homeassistant.components.somfy_mylink.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.somfy_mylink.config_flow.SomfyMyLinkSynergy.status_info", + return_value={"any": "data"}, + ), + patch( + "homeassistant.components.somfy_mylink.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { diff --git a/tests/components/sonarr/__init__.py b/tests/components/sonarr/__init__.py index ca9fc91bd5e..b6050808a34 100644 --- a/tests/components/sonarr/__init__.py +++ b/tests/components/sonarr/__init__.py @@ -1,4 +1,5 @@ """Tests for the Sonarr component.""" + from homeassistant.const import CONF_API_KEY, CONF_URL MOCK_REAUTH_INPUT = {CONF_API_KEY: "test-api-key-reauth"} diff --git a/tests/components/sonarr/conftest.py b/tests/components/sonarr/conftest.py index da8ff75df0f..7c18fb372a1 100644 --- a/tests/components/sonarr/conftest.py +++ b/tests/components/sonarr/conftest.py @@ -1,4 +1,5 @@ """Fixtures for Sonarr integration tests.""" + from collections.abc import Generator import json from unittest.mock import MagicMock, patch diff --git a/tests/components/sonarr/test_config_flow.py b/tests/components/sonarr/test_config_flow.py index 2a078f49190..3e48a4b25a8 100644 --- a/tests/components/sonarr/test_config_flow.py +++ b/tests/components/sonarr/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Sonarr config flow.""" + from unittest.mock import MagicMock, patch from aiopyarr import ArrAuthenticationException, ArrException diff --git a/tests/components/sonarr/test_init.py b/tests/components/sonarr/test_init.py index 5c48f6c8445..e663139d33c 100644 --- a/tests/components/sonarr/test_init.py +++ b/tests/components/sonarr/test_init.py @@ -1,4 +1,5 @@ """Tests for the Sonsrr integration.""" + from unittest.mock import MagicMock, patch from aiopyarr import ArrAuthenticationException, ArrException @@ -104,7 +105,9 @@ async def test_migrate_config_entry(hass: HomeAssistant) -> None: assert entry.version == 1 assert not entry.unique_id - await entry.async_migrate(hass) + with patch("homeassistant.components.sonarr.async_setup_entry", return_value=True): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() assert entry.data == { CONF_API_KEY: "MOCK_API_KEY", diff --git a/tests/components/sonarr/test_sensor.py b/tests/components/sonarr/test_sensor.py index e44081f94bf..3641ae95de8 100644 --- a/tests/components/sonarr/test_sensor.py +++ b/tests/components/sonarr/test_sensor.py @@ -1,4 +1,5 @@ """Tests for the Sonarr sensor platform.""" + from datetime import timedelta from unittest.mock import MagicMock @@ -8,7 +9,6 @@ import pytest from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import ( - ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, STATE_UNAVAILABLE, UnitOfInformation, @@ -50,41 +50,35 @@ async def test_sensors( state = hass.states.get("sensor.sonarr_commands") assert state - assert state.attributes.get(ATTR_ICON) == "mdi:code-braces" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "Commands" assert state.state == "2" state = hass.states.get("sensor.sonarr_disk_space") assert state - assert state.attributes.get(ATTR_ICON) == "mdi:harddisk" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfInformation.GIGABYTES assert state.attributes.get("C:\\") == "263.10/465.42GB (56.53%)" assert state.state == "263.10" state = hass.states.get("sensor.sonarr_queue") assert state - assert state.attributes.get(ATTR_ICON) == "mdi:download" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "Episodes" assert state.attributes.get("The Andy Griffith Show S01E01") == "100.00%" assert state.state == "1" state = hass.states.get("sensor.sonarr_shows") assert state - assert state.attributes.get(ATTR_ICON) == "mdi:television" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "Series" assert state.attributes.get("The Andy Griffith Show") == "0/0 Episodes" assert state.state == "1" state = hass.states.get("sensor.sonarr_upcoming") assert state - assert state.attributes.get(ATTR_ICON) == "mdi:television" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "Episodes" assert state.attributes.get("Bob's Burgers") == "S04E11" assert state.state == "1" state = hass.states.get("sensor.sonarr_wanted") assert state - assert state.attributes.get(ATTR_ICON) == "mdi:television" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "Episodes" assert state.attributes.get("Bob's Burgers S04E11") == "2014-01-26T17:30:00-08:00" assert ( @@ -96,13 +90,13 @@ async def test_sensors( @pytest.mark.parametrize( "entity_id", - ( + [ "sensor.sonarr_commands", "sensor.sonarr_disk_space", "sensor.sonarr_queue", "sensor.sonarr_shows", "sensor.sonarr_wanted", - ), + ], ) async def test_disabled_by_default_sensors( hass: HomeAssistant, diff --git a/tests/components/songpal/__init__.py b/tests/components/songpal/__init__.py index d98ec4175fc..6ebc2ec5ef4 100644 --- a/tests/components/songpal/__init__.py +++ b/tests/components/songpal/__init__.py @@ -1,4 +1,5 @@ """Test the songpal integration.""" + from unittest.mock import AsyncMock, MagicMock, patch from songpal import SongpalException diff --git a/tests/components/songpal/test_config_flow.py b/tests/components/songpal/test_config_flow.py index 0a3d5d1acb2..338207a5d13 100644 --- a/tests/components/songpal/test_config_flow.py +++ b/tests/components/songpal/test_config_flow.py @@ -1,4 +1,5 @@ """Test the songpal config flow.""" + import copy import dataclasses from unittest.mock import patch diff --git a/tests/components/songpal/test_init.py b/tests/components/songpal/test_init.py index 813b615acdd..c49b290f805 100644 --- a/tests/components/songpal/test_init.py +++ b/tests/components/songpal/test_init.py @@ -1,4 +1,5 @@ """Tests songpal setup.""" + from unittest.mock import patch from homeassistant.components import songpal @@ -57,8 +58,9 @@ async def test_unload(hass: HomeAssistant) -> None: entry.add_to_hass(hass) mocked_device = _create_mocked_device() - with _patch_config_flow_device(mocked_device), _patch_media_player_device( - mocked_device + with ( + _patch_config_flow_device(mocked_device), + _patch_media_player_device(mocked_device), ): assert await async_setup_component(hass, songpal.DOMAIN, {}) is True await hass.async_block_till_done() diff --git a/tests/components/songpal/test_media_player.py b/tests/components/songpal/test_media_player.py index 534e2e6e9e6..4b1abf8709e 100644 --- a/tests/components/songpal/test_media_player.py +++ b/tests/components/songpal/test_media_player.py @@ -1,4 +1,5 @@ """Test songpal media_player.""" + from datetime import timedelta import logging from unittest.mock import AsyncMock, MagicMock, call, patch diff --git a/tests/components/sonos/conftest.py b/tests/components/sonos/conftest.py index 8bd8224e726..576c9a80799 100644 --- a/tests/components/sonos/conftest.py +++ b/tests/components/sonos/conftest.py @@ -1,15 +1,20 @@ """Configuration for Sonos tests.""" + +import asyncio +from collections.abc import Callable from copy import copy from ipaddress import ip_address from unittest.mock import AsyncMock, MagicMock, Mock, patch import pytest from soco import SoCo +from soco.events_base import Event as SonosEvent from homeassistant.components import ssdp, zeroconf from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.components.sonos import DOMAIN from homeassistant.const import CONF_HOSTS +from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry, load_fixture @@ -29,6 +34,31 @@ class SonosMockSubscribe: """Initialize the mock subscriber.""" self.event_listener = SonosMockEventListener(ip_address) self.service = Mock() + self.callback_future: asyncio.Future[Callable[[SonosEvent], None]] = None + self._callback: Callable[[SonosEvent], None] | None = None + + @property + def callback(self) -> Callable[[SonosEvent], None] | None: + """Return the callback.""" + return self._callback + + @callback.setter + def callback(self, callback: Callable[[SonosEvent], None]) -> None: + """Set the callback.""" + self._callback = callback + future = self._get_callback_future() + if not future.done(): + future.set_result(callback) + + def _get_callback_future(self) -> asyncio.Future[Callable[[SonosEvent], None]]: + """Get the callback future.""" + if not self.callback_future: + self.callback_future = asyncio.get_running_loop().create_future() + return self.callback_future + + async def wait_for_callback_to_be_set(self) -> Callable[[SonosEvent], None]: + """Wait for the callback to be set.""" + return await self._get_callback_future() async def unsubscribe(self) -> None: """Unsubscribe mock.""" @@ -93,8 +123,9 @@ def async_setup_sonos(hass, config_entry, fire_zgs_event): async def _wrapper(): config_entry.add_to_hass(hass) assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) await fire_zgs_event() + await hass.async_block_till_done(wait_background_tasks=True) return _wrapper @@ -208,9 +239,11 @@ def soco_factory( factory = SoCoMockFactory( music_library, speaker_info, current_track_info_empty, battery_info, alarm_clock ) - with patch("homeassistant.components.sonos.SoCo", new=factory.get_mock), patch( - "socket.gethostbyname", side_effect=patch_gethostbyname - ), patch("homeassistant.components.sonos.ZGS_SUBSCRIPTION_TIMEOUT", 0): + with ( + patch("homeassistant.components.sonos.SoCo", new=factory.get_mock), + patch("socket.gethostbyname", side_effect=patch_gethostbyname), + patch("homeassistant.components.sonos.ZGS_SUBSCRIPTION_TIMEOUT", 0), + ): yield factory @@ -223,14 +256,16 @@ def soco_fixture(soco_factory): @pytest.fixture(autouse=True) async def silent_ssdp_scanner(hass): """Start SSDP component and get Scanner, prevent actual SSDP traffic.""" - with patch( - "homeassistant.components.ssdp.Scanner._async_start_ssdp_listeners" - ), patch("homeassistant.components.ssdp.Scanner._async_stop_ssdp_listeners"), patch( - "homeassistant.components.ssdp.Scanner.async_scan" - ), patch( - "homeassistant.components.ssdp.Server._async_start_upnp_servers", - ), patch( - "homeassistant.components.ssdp.Server._async_stop_upnp_servers", + with ( + patch("homeassistant.components.ssdp.Scanner._async_start_ssdp_listeners"), + patch("homeassistant.components.ssdp.Scanner._async_stop_ssdp_listeners"), + patch("homeassistant.components.ssdp.Scanner.async_scan"), + patch( + "homeassistant.components.ssdp.Server._async_start_upnp_servers", + ), + patch( + "homeassistant.components.ssdp.Server._async_stop_upnp_servers", + ), ): yield @@ -239,8 +274,8 @@ async def silent_ssdp_scanner(hass): def discover_fixture(soco): """Create a mock soco discover fixture.""" - async def do_callback(hass, callback, *args, **kwargs): - await callback( + def do_callback(hass, callback, *args, **kwargs): + callback( ssdp.SsdpServiceInfo( ssdp_location=f"http://{soco.ip_address}/", ssdp_st="urn:schemas-upnp-org:device:ZonePlayer:1", @@ -450,14 +485,14 @@ def zgs_discovery_fixture(): @pytest.fixture(name="fire_zgs_event") -def zgs_event_fixture(hass, soco, zgs_discovery): +def zgs_event_fixture(hass: HomeAssistant, soco: SoCo, zgs_discovery: str): """Create alarm_event fixture.""" variables = {"ZoneGroupState": zgs_discovery} async def _wrapper(): event = SonosMockEvent(soco, soco.zoneGroupTopology, variables) - subscription = soco.zoneGroupTopology.subscribe.return_value - sub_callback = subscription.callback + subscription: SonosMockSubscribe = soco.zoneGroupTopology.subscribe.return_value + sub_callback = await subscription.wait_for_callback_to_be_set() sub_callback(event) await hass.async_block_till_done() diff --git a/tests/components/sonos/test_config_flow.py b/tests/components/sonos/test_config_flow.py index 2fd8ad110df..186e45e3d84 100644 --- a/tests/components/sonos/test_config_flow.py +++ b/tests/components/sonos/test_config_flow.py @@ -1,4 +1,5 @@ """Test the sonos config flow.""" + from __future__ import annotations from ipaddress import ip_address @@ -41,13 +42,16 @@ async def test_user_form( ) assert result["type"] == "form" assert result["errors"] is None - with patch( - "homeassistant.components.sonos.async_setup", - return_value=True, - ) as mock_setup, patch( - "homeassistant.components.sonos.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.sonos.async_setup", + return_value=True, + ) as mock_setup, + patch( + "homeassistant.components.sonos.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {}, @@ -92,13 +96,16 @@ async def test_zeroconf_form( assert result["type"] == "form" assert result["errors"] is None - with patch( - "homeassistant.components.sonos.async_setup", - return_value=True, - ) as mock_setup, patch( - "homeassistant.components.sonos.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.sonos.async_setup", + return_value=True, + ) as mock_setup, + patch( + "homeassistant.components.sonos.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {}, @@ -134,13 +141,16 @@ async def test_ssdp_discovery(hass: HomeAssistant, soco) -> None: assert len(flows) == 1 flow = flows[0] - with patch( - "homeassistant.components.sonos.async_setup", - return_value=True, - ) as mock_setup, patch( - "homeassistant.components.sonos.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.sonos.async_setup", + return_value=True, + ) as mock_setup, + patch( + "homeassistant.components.sonos.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result = await hass.config_entries.flow.async_configure( flow["flow_id"], {}, @@ -184,13 +194,16 @@ async def test_zeroconf_sonos_v1(hass: HomeAssistant) -> None: assert result["type"] == "form" assert result["errors"] is None - with patch( - "homeassistant.components.sonos.async_setup", - return_value=True, - ) as mock_setup, patch( - "homeassistant.components.sonos.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.sonos.async_setup", + return_value=True, + ) as mock_setup, + patch( + "homeassistant.components.sonos.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {}, diff --git a/tests/components/sonos/test_helpers.py b/tests/components/sonos/test_helpers.py index 48594d65301..fde61527953 100644 --- a/tests/components/sonos/test_helpers.py +++ b/tests/components/sonos/test_helpers.py @@ -1,4 +1,5 @@ """Test the sonos config flow.""" + from __future__ import annotations import pytest diff --git a/tests/components/sonos/test_init.py b/tests/components/sonos/test_init.py index 83171e2029e..77bf9a5d12b 100644 --- a/tests/components/sonos/test_init.py +++ b/tests/components/sonos/test_init.py @@ -1,4 +1,5 @@ """Tests for the Sonos config flow.""" + import asyncio from datetime import timedelta import logging @@ -95,21 +96,23 @@ async def test_async_poll_manual_hosts_warnings( await hass.async_block_till_done() manager: SonosDiscoveryManager = hass.data[DATA_SONOS_DISCOVERY_MANAGER] manager.hosts.add("10.10.10.10") - with caplog.at_level(logging.DEBUG), patch.object( - manager, "_async_handle_discovery_message" - ), patch( - "homeassistant.components.sonos.async_call_later" - ) as mock_async_call_later, patch( - "homeassistant.components.sonos.async_dispatcher_send" - ), patch( - "homeassistant.components.sonos.sync_get_visible_zones", - side_effect=[ - OSError(), - OSError(), - [], - [], - OSError(), - ], + with ( + caplog.at_level(logging.DEBUG), + patch.object(manager, "_async_handle_discovery_message"), + patch( + "homeassistant.components.sonos.async_call_later" + ) as mock_async_call_later, + patch("homeassistant.components.sonos.async_dispatcher_send"), + patch( + "homeassistant.components.sonos.sync_get_visible_zones", + side_effect=[ + OSError(), + OSError(), + [], + [], + OSError(), + ], + ), ): # First call fails, it should be logged as a WARNING message caplog.clear() @@ -157,7 +160,7 @@ async def test_async_poll_manual_hosts_warnings( class _MockSoCoOsError(MockSoCo): @property def visible_zones(self): - raise OSError() + raise OSError class _MockSoCoVisibleZones(MockSoCo): @@ -368,7 +371,7 @@ async def test_async_poll_manual_hosts_6( "homeassistant.components.sonos.DISCOVERY_INTERVAL" ) as mock_discovery_interval: # Speed up manual discovery interval so second iteration runs sooner - mock_discovery_interval.total_seconds = Mock(side_effect=[0.5, 60]) + mock_discovery_interval.total_seconds = Mock(side_effect=[0.0, 60]) await _setup_hass(hass) assert "media_player.bedroom" in entity_registry.entities @@ -376,10 +379,6 @@ async def test_async_poll_manual_hosts_6( with caplog.at_level(logging.DEBUG): caplog.clear() - # The discovery events should not fire, wait with a timeout. - with pytest.raises(TimeoutError): - async with asyncio.timeout(1.0): - await speaker_1_activity.event.wait() await hass.async_block_till_done() assert "Activity on Living Room" not in caplog.text assert "Activity on Bedroom" not in caplog.text diff --git a/tests/components/sonos/test_media_browser.py b/tests/components/sonos/test_media_browser.py new file mode 100644 index 00000000000..cb6303c800d --- /dev/null +++ b/tests/components/sonos/test_media_browser.py @@ -0,0 +1,96 @@ +"""Tests for the Sonos Media Browser.""" + +from functools import partial + +from homeassistant.components.media_player.browse_media import BrowseMedia +from homeassistant.components.media_player.const import MediaClass, MediaType +from homeassistant.components.sonos.media_browser import ( + build_item_response, + get_thumbnail_url_full, +) +from homeassistant.core import HomeAssistant + +from .conftest import SoCoMockFactory + + +class MockMusicServiceItem: + """Mocks a Soco MusicServiceItem.""" + + def __init__( + self, + title: str, + item_id: str, + parent_id: str, + item_class: str, + ) -> None: + """Initialize the mock item.""" + self.title = title + self.item_id = item_id + self.item_class = item_class + self.parent_id = parent_id + + def get_uri(self) -> str: + """Return URI.""" + return self.item_id.replace("S://", "x-file-cifs://") + + +def mock_browse_by_idstring( + search_type: str, idstring: str, start=0, max_items=100, full_album_art_uri=False +) -> list[MockMusicServiceItem]: + """Mock the call to browse_by_id_string.""" + if search_type == "albums" and ( + idstring == "A:ALBUM/Abbey%20Road" or idstring == "A:ALBUM/Abbey Road" + ): + return [ + MockMusicServiceItem( + "Come Together", + "S://192.168.42.10/music/The%20Beatles/Abbey%20Road/01%20Come%20Together.mp3", + "A:ALBUM/Abbey%20Road", + "object.item.audioItem.musicTrack", + ), + MockMusicServiceItem( + "Something", + "S://192.168.42.10/music/The%20Beatles/Abbey%20Road/03%20Something.mp3", + "A:ALBUM/Abbey%20Road", + "object.item.audioItem.musicTrack", + ), + ] + return None + + +async def test_build_item_response( + hass: HomeAssistant, + soco_factory: SoCoMockFactory, + async_autosetup_sonos, + soco, + discover, +) -> None: + """Test building a browse item response.""" + soco_mock = soco_factory.mock_list.get("192.168.42.2") + soco_mock.music_library.browse_by_idstring = mock_browse_by_idstring + browse_item: BrowseMedia = build_item_response( + soco_mock.music_library, + {"search_type": MediaType.ALBUM, "idstring": "A:ALBUM/Abbey%20Road"}, + partial( + get_thumbnail_url_full, + soco_mock.music_library, + True, + None, + ), + ) + assert browse_item.title == "Abbey Road" + assert browse_item.media_class == MediaClass.ALBUM + assert browse_item.media_content_id == "A:ALBUM/Abbey%20Road" + assert len(browse_item.children) == 2 + assert browse_item.children[0].media_class == MediaClass.TRACK + assert browse_item.children[0].title == "Come Together" + assert ( + browse_item.children[0].media_content_id + == "x-file-cifs://192.168.42.10/music/The%20Beatles/Abbey%20Road/01%20Come%20Together.mp3" + ) + assert browse_item.children[1].media_class == MediaClass.TRACK + assert browse_item.children[1].title == "Something" + assert ( + browse_item.children[1].media_content_id + == "x-file-cifs://192.168.42.10/music/The%20Beatles/Abbey%20Road/03%20Something.mp3" + ) diff --git a/tests/components/sonos/test_media_player.py b/tests/components/sonos/test_media_player.py index ddf550dc376..c181520b85d 100644 --- a/tests/components/sonos/test_media_player.py +++ b/tests/components/sonos/test_media_player.py @@ -1,4 +1,13 @@ """Tests for the Sonos Media Player platform.""" + +import logging + +import pytest + +from homeassistant.components.media_player import ( + DOMAIN as MP_DOMAIN, + SERVICE_PLAY_MEDIA, +) from homeassistant.const import STATE_IDLE from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import ( @@ -7,6 +16,8 @@ from homeassistant.helpers.device_registry import ( DeviceRegistry, ) +from .conftest import SoCoMockFactory + async def test_device_registry( hass: HomeAssistant, device_registry: DeviceRegistry, async_autosetup_sonos, soco @@ -52,3 +63,110 @@ async def test_entity_basic( assert attributes["friendly_name"] == "Zone A" assert attributes["is_volume_muted"] is False assert attributes["volume_level"] == 0.19 + + +class _MockMusicServiceItem: + """Mocks a Soco MusicServiceItem.""" + + def __init__( + self, + title: str, + item_id: str, + parent_id: str, + item_class: str, + ) -> None: + """Initialize the mock item.""" + self.title = title + self.item_id = item_id + self.item_class = item_class + self.parent_id = parent_id + + def get_uri(self) -> str: + """Return URI.""" + return self.item_id.replace("S://", "x-file-cifs://") + + +_mock_playlists = [ + _MockMusicServiceItem( + "playlist1", + "S://192.168.1.68/music/iTunes/iTunes%20Music%20Library.xml#GUID_1", + "A:PLAYLISTS", + "object.container.playlistContainer", + ), + _MockMusicServiceItem( + "playlist2", + "S://192.168.1.68/music/iTunes/iTunes%20Music%20Library.xml#GUID_2", + "A:PLAYLISTS", + "object.container.playlistContainer", + ), +] + + +@pytest.mark.parametrize( + ("media_content_id", "expected_item_id"), + [ + ( + _mock_playlists[0].item_id, + _mock_playlists[0].item_id, + ), + ( + f"S:{_mock_playlists[1].title}", + _mock_playlists[1].item_id, + ), + ], +) +async def test_play_media_music_library_playlist( + hass: HomeAssistant, + soco_factory: SoCoMockFactory, + async_autosetup_sonos, + discover, + media_content_id, + expected_item_id, +) -> None: + """Test that playlists can be found by id or title.""" + soco_mock = soco_factory.mock_list.get("192.168.42.2") + soco_mock.music_library.get_playlists.return_value = _mock_playlists + + await hass.services.async_call( + MP_DOMAIN, + SERVICE_PLAY_MEDIA, + { + "entity_id": "media_player.zone_a", + "media_content_type": "playlist", + "media_content_id": media_content_id, + }, + blocking=True, + ) + + assert soco_mock.clear_queue.call_count == 1 + assert soco_mock.add_to_queue.call_count == 1 + assert soco_mock.add_to_queue.call_args_list[0].args[0].item_id == expected_item_id + assert soco_mock.play_from_queue.call_count == 1 + + +async def test_play_media_music_library_playlist_dne( + hass: HomeAssistant, + soco_factory: SoCoMockFactory, + async_autosetup_sonos, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test error handling when attempting to play a non-existent playlist .""" + media_content_id = "S:nonexistent" + soco_mock = soco_factory.mock_list.get("192.168.42.2") + soco_mock.music_library.get_playlists.return_value = _mock_playlists + + with caplog.at_level(logging.ERROR): + caplog.clear() + await hass.services.async_call( + MP_DOMAIN, + SERVICE_PLAY_MEDIA, + { + "entity_id": "media_player.zone_a", + "media_content_type": "playlist", + "media_content_id": media_content_id, + }, + blocking=True, + ) + assert soco_mock.play_uri.call_count == 0 + assert media_content_id in caplog.text + assert "playlist" in caplog.text diff --git a/tests/components/sonos/test_number.py b/tests/components/sonos/test_number.py index d58b84ab6cb..abfe0b1ff3f 100644 --- a/tests/components/sonos/test_number.py +++ b/tests/components/sonos/test_number.py @@ -1,4 +1,5 @@ """Tests for the Sonos number platform.""" + from unittest.mock import PropertyMock, patch from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN, SERVICE_SET_VALUE diff --git a/tests/components/sonos/test_plex_playback.py b/tests/components/sonos/test_plex_playback.py index 99269ca10ef..428e970697e 100644 --- a/tests/components/sonos/test_plex_playback.py +++ b/tests/components/sonos/test_plex_playback.py @@ -1,4 +1,5 @@ """Tests for the Sonos Media Player platform.""" + import json from unittest.mock import Mock, patch @@ -27,12 +28,16 @@ async def test_plex_play_media(hass: HomeAssistant, async_autosetup_sonos) -> No '{"library_name": "Music", "artist_name": "Artist", "album_name": "Album"}' ) - with patch( - "homeassistant.components.plex.services.get_plex_server", - return_value=mock_plex_server, - ), patch("soco.plugins.plex.PlexPlugin.add_to_queue") as mock_add_to_queue, patch( - "homeassistant.components.sonos.media_player.SonosMediaPlayerEntity.set_shuffle" - ) as mock_shuffle: + with ( + patch( + "homeassistant.components.plex.services.get_plex_server", + return_value=mock_plex_server, + ), + patch("soco.plugins.plex.PlexPlugin.add_to_queue") as mock_add_to_queue, + patch( + "homeassistant.components.sonos.media_player.SonosMediaPlayerEntity.set_shuffle" + ) as mock_shuffle, + ): # Test successful Plex service call await hass.services.async_call( MP_DOMAIN, diff --git a/tests/components/sonos/test_repairs.py b/tests/components/sonos/test_repairs.py index b86c6bd5f66..49b87b272d6 100644 --- a/tests/components/sonos/test_repairs.py +++ b/tests/components/sonos/test_repairs.py @@ -1,6 +1,9 @@ """Test repairs handling for Sonos.""" + from unittest.mock import Mock +from soco import SoCo + from homeassistant.components.sonos.const import ( DOMAIN, SCAN_INTERVAL, @@ -10,27 +13,27 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.issue_registry import async_get as async_get_issue_registry from homeassistant.util import dt as dt_util -from .conftest import SonosMockEvent +from .conftest import SonosMockEvent, SonosMockSubscribe from tests.common import MockConfigEntry, async_fire_time_changed async def test_subscription_repair_issues( - hass: HomeAssistant, config_entry: MockConfigEntry, soco, zgs_discovery + hass: HomeAssistant, config_entry: MockConfigEntry, soco: SoCo, zgs_discovery ) -> None: """Test repair issues handling for failed subscriptions.""" issue_registry = async_get_issue_registry(hass) - subscription = soco.zoneGroupTopology.subscribe.return_value + subscription: SonosMockSubscribe = soco.zoneGroupTopology.subscribe.return_value subscription.event_listener = Mock(address=("192.168.4.2", 1400)) config_entry.add_to_hass(hass) assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() # Ensure an issue is registered on subscription failure + sub_callback = await subscription.wait_for_callback_to_be_set() async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert issue_registry.async_get_issue(DOMAIN, SUB_FAIL_ISSUE_ID) # Ensure the issue still exists after reload @@ -41,7 +44,6 @@ async def test_subscription_repair_issues( # Ensure the issue has been removed after a successful subscription callback variables = {"ZoneGroupState": zgs_discovery} event = SonosMockEvent(soco, soco.zoneGroupTopology, variables) - sub_callback = subscription.callback sub_callback(event) await hass.async_block_till_done() assert not issue_registry.async_get_issue(DOMAIN, SUB_FAIL_ISSUE_ID) diff --git a/tests/components/sonos/test_sensor.py b/tests/components/sonos/test_sensor.py index 40b0c2d21c6..1f4ba8d22cd 100644 --- a/tests/components/sonos/test_sensor.py +++ b/tests/components/sonos/test_sensor.py @@ -1,4 +1,5 @@ """Tests for the Sonos battery sensor platform.""" + from datetime import timedelta from unittest.mock import PropertyMock, patch @@ -25,6 +26,7 @@ async def test_entity_registry_unsupported( soco.get_battery_info.side_effect = NotSupportedException await async_setup_sonos() + await hass.async_block_till_done(wait_background_tasks=True) assert "media_player.zone_a" in entity_registry.entities assert "sensor.zone_a_battery" not in entity_registry.entities @@ -35,6 +37,8 @@ async def test_entity_registry_supported( hass: HomeAssistant, async_autosetup_sonos, soco, entity_registry: er.EntityRegistry ) -> None: """Test sonos device with battery registered in the device registry.""" + await hass.async_block_till_done(wait_background_tasks=True) + assert "media_player.zone_a" in entity_registry.entities assert "sensor.zone_a_battery" in entity_registry.entities assert "binary_sensor.zone_a_charging" in entity_registry.entities @@ -68,6 +72,7 @@ async def test_battery_on_s1( soco.get_battery_info.return_value = {} await async_setup_sonos() + await hass.async_block_till_done(wait_background_tasks=True) subscription = soco.deviceProperties.subscribe.return_value sub_callback = subscription.callback @@ -77,7 +82,7 @@ async def test_battery_on_s1( # Update the speaker with a callback event sub_callback(device_properties_event) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) battery = entity_registry.entities["sensor.zone_a_battery"] battery_state = hass.states.get(battery.entity_id) @@ -100,6 +105,7 @@ async def test_device_payload_without_battery( soco.get_battery_info.return_value = None await async_setup_sonos() + await hass.async_block_till_done(wait_background_tasks=True) subscription = soco.deviceProperties.subscribe.return_value sub_callback = subscription.callback @@ -108,7 +114,7 @@ async def test_device_payload_without_battery( device_properties_event.variables["more_info"] = bad_payload sub_callback(device_properties_event) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert bad_payload in caplog.text @@ -124,6 +130,7 @@ async def test_device_payload_without_battery_and_ignored_keys( soco.get_battery_info.return_value = None await async_setup_sonos() + await hass.async_block_till_done(wait_background_tasks=True) subscription = soco.deviceProperties.subscribe.return_value sub_callback = subscription.callback @@ -132,7 +139,7 @@ async def test_device_payload_without_battery_and_ignored_keys( device_properties_event.variables["more_info"] = ignored_payload sub_callback(device_properties_event) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert ignored_payload not in caplog.text @@ -149,7 +156,7 @@ async def test_audio_input_sensor( subscription = soco.avTransport.subscribe.return_value sub_callback = subscription.callback sub_callback(tv_event) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) audio_input_sensor = entity_registry.entities["sensor.zone_a_audio_input_format"] audio_input_state = hass.states.get(audio_input_sensor.entity_id) @@ -160,7 +167,7 @@ async def test_audio_input_sensor( type(soco).soundbar_audio_input_format = no_input_mock async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) no_input_mock.assert_called_once() audio_input_state = hass.states.get(audio_input_sensor.entity_id) @@ -168,13 +175,13 @@ async def test_audio_input_sensor( # Ensure state is not polled when source is not TV and state is already "No input" sub_callback(no_media_event) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) unpolled_mock = PropertyMock(return_value="Will not be polled") type(soco).soundbar_audio_input_format = unpolled_mock async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) unpolled_mock.assert_not_called() audio_input_state = hass.states.get(audio_input_sensor.entity_id) @@ -198,7 +205,7 @@ async def test_microphone_binary_sensor( # Update the speaker with a callback event subscription = soco.deviceProperties.subscribe.return_value subscription.callback(device_properties_event) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) mic_binary_sensor_state = hass.states.get(mic_binary_sensor.entity_id) assert mic_binary_sensor_state.state == STATE_ON @@ -224,17 +231,18 @@ async def test_favorites_sensor( empty_event = SonosMockEvent(soco, service, {}) subscription = service.subscribe.return_value subscription.callback(event=empty_event) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) # Reload the integration to enable the sensor async_fire_time_changed( hass, dt_util.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1), ) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) # Trigger subscription callback for speaker discovery await fire_zgs_event() + await hass.async_block_till_done(wait_background_tasks=True) favorites_updated_event = SonosMockEvent( soco, service, {"favorites_update_id": "2", "container_update_i_ds": "FV:2,2"} @@ -244,4 +252,4 @@ async def test_favorites_sensor( return_value=True, ): subscription.callback(event=favorites_updated_event) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) diff --git a/tests/components/sonos/test_services.py b/tests/components/sonos/test_services.py index 029c80835ee..da894ff4548 100644 --- a/tests/components/sonos/test_services.py +++ b/tests/components/sonos/test_services.py @@ -1,4 +1,5 @@ """Tests for Sonos services.""" + from unittest.mock import Mock, patch import pytest @@ -27,11 +28,12 @@ async def test_media_player_join(hass: HomeAssistant, async_autosetup_sonos) -> 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: + 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, diff --git a/tests/components/sonos/test_speaker.py b/tests/components/sonos/test_speaker.py index e9b85c22eb3..2c4357060be 100644 --- a/tests/components/sonos/test_speaker.py +++ b/tests/components/sonos/test_speaker.py @@ -1,4 +1,5 @@ """Tests for common SonosSpeaker behavior.""" + from unittest.mock import patch import pytest @@ -11,9 +12,20 @@ from tests.common import async_fire_time_changed async def test_fallback_to_polling( - hass: HomeAssistant, async_autosetup_sonos, soco, caplog: pytest.LogCaptureFixture + hass: HomeAssistant, + config_entry, + soco, + fire_zgs_event, + caplog: pytest.LogCaptureFixture, ) -> None: """Test that polling fallback works.""" + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + # Do not wait on background tasks here because the + # subscription callback will fire an unsub the polling check + await hass.async_block_till_done() + await fire_zgs_event() + speaker = list(hass.data[DATA_SONOS].discovered.values())[0] assert speaker.soco is soco assert speaker._subscriptions @@ -22,11 +34,14 @@ async def test_fallback_to_polling( caplog.clear() # Ensure subscriptions are cancelled and polling methods are called when subscriptions time out - with patch("homeassistant.components.sonos.media.SonosMedia.poll_media"), patch( - "homeassistant.components.sonos.speaker.SonosSpeaker.subscription_address" + with ( + patch("homeassistant.components.sonos.media.SonosMedia.poll_media"), + patch( + "homeassistant.components.sonos.speaker.SonosSpeaker.subscription_address" + ), ): async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert not speaker._subscriptions assert speaker.subscriptions_failed @@ -42,6 +57,7 @@ async def test_subscription_creation_fails( side_effect=ConnectionError("Took too long"), ): await async_setup_sonos() + await hass.async_block_till_done(wait_background_tasks=True) speaker = list(hass.data[DATA_SONOS].discovered.values())[0] assert not speaker._subscriptions diff --git a/tests/components/sonos/test_statistics.py b/tests/components/sonos/test_statistics.py index 4a32b18f72b..4f28ec31412 100644 --- a/tests/components/sonos/test_statistics.py +++ b/tests/components/sonos/test_statistics.py @@ -1,4 +1,5 @@ """Tests for the Sonos statistics.""" + from homeassistant.components.sonos.const import DATA_SONOS from homeassistant.core import HomeAssistant diff --git a/tests/components/sonos/test_switch.py b/tests/components/sonos/test_switch.py index 301c4a641ea..d8499c50bb1 100644 --- a/tests/components/sonos/test_switch.py +++ b/tests/components/sonos/test_switch.py @@ -1,4 +1,5 @@ """Tests for the Sonos Alarm switch platform.""" + from copy import copy from datetime import timedelta from unittest.mock import patch diff --git a/tests/components/soundtouch/conftest.py b/tests/components/soundtouch/conftest.py index 1cdd37add4b..c81d76072d7 100644 --- a/tests/components/soundtouch/conftest.py +++ b/tests/components/soundtouch/conftest.py @@ -1,4 +1,5 @@ """Fixtures for Bose SoundTouch integration tests.""" + import pytest from requests_mock import Mocker diff --git a/tests/components/soundtouch/test_config_flow.py b/tests/components/soundtouch/test_config_flow.py index 896202355ac..bc7de5b7fda 100644 --- a/tests/components/soundtouch/test_config_flow.py +++ b/tests/components/soundtouch/test_config_flow.py @@ -1,4 +1,5 @@ """Test config flow.""" + from ipaddress import ip_address from unittest.mock import patch diff --git a/tests/components/soundtouch/test_media_player.py b/tests/components/soundtouch/test_media_player.py index 0bae58a1c00..61d0c7b4ea5 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 @@ -664,7 +665,7 @@ async def test_zone_attributes( hass, dt_util.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1), ) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) entity_1_state = hass.states.get(DEVICE_1_ENTITY_ID) assert entity_1_state.attributes[ATTR_SOUNDTOUCH_ZONE]["is_master"] diff --git a/tests/components/spaceapi/test_init.py b/tests/components/spaceapi/test_init.py index ac892eeb2d8..14b4c9177f9 100644 --- a/tests/components/spaceapi/test_init.py +++ b/tests/components/spaceapi/test_init.py @@ -1,4 +1,5 @@ """The tests for the Home Assistant SpaceAPI component.""" + from http import HTTPStatus from unittest.mock import patch @@ -116,7 +117,7 @@ async def test_spaceapi_get(hass: HomeAssistant, mock_client) -> None: assert data["location"]["lon"] == -117.22743 assert data["state"]["open"] == "null" assert data["state"]["icon"]["open"] == "https://home-assistant.io/open.png" - assert data["state"]["icon"]["close"] == "https://home-assistant.io/close.png" + assert data["state"]["icon"]["closed"] == "https://home-assistant.io/close.png" assert data["spacefed"]["spacenet"] == bool(1) assert data["spacefed"]["spacesaml"] == bool(0) assert data["spacefed"]["spacephone"] == bool(1) diff --git a/tests/components/spc/test_init.py b/tests/components/spc/test_init.py index 1972b7af5c8..92c3282dd23 100644 --- a/tests/components/spc/test_init.py +++ b/tests/components/spc/test_init.py @@ -1,4 +1,5 @@ """Tests for Vanderbilt SPC component.""" + from unittest.mock import Mock, PropertyMock, patch from homeassistant.bootstrap import async_setup_component diff --git a/tests/components/speedtestdotnet/conftest.py b/tests/components/speedtestdotnet/conftest.py index 0dab08eddef..a48e7a878ff 100644 --- a/tests/components/speedtestdotnet/conftest.py +++ b/tests/components/speedtestdotnet/conftest.py @@ -1,4 +1,5 @@ """Conftest for speedtestdotnet.""" + from unittest.mock import patch import pytest diff --git a/tests/components/speedtestdotnet/test_config_flow.py b/tests/components/speedtestdotnet/test_config_flow.py index 00269c55ec3..f412c71a6ed 100644 --- a/tests/components/speedtestdotnet/test_config_flow.py +++ b/tests/components/speedtestdotnet/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for SpeedTest config flow.""" + from unittest.mock import MagicMock from homeassistant import config_entries diff --git a/tests/components/speedtestdotnet/test_init.py b/tests/components/speedtestdotnet/test_init.py index 5083f56a8e2..2b0f803eb6f 100644 --- a/tests/components/speedtestdotnet/test_init.py +++ b/tests/components/speedtestdotnet/test_init.py @@ -74,7 +74,7 @@ async def test_server_not_found(hass: HomeAssistant, mock_api: MagicMock) -> Non hass, dt_util.utcnow() + timedelta(minutes=61), ) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get("sensor.speedtest_ping") assert state is not None assert state.state == STATE_UNAVAILABLE diff --git a/tests/components/speedtestdotnet/test_sensor.py b/tests/components/speedtestdotnet/test_sensor.py index d15e9fb92f4..e529d46b537 100644 --- a/tests/components/speedtestdotnet/test_sensor.py +++ b/tests/components/speedtestdotnet/test_sensor.py @@ -1,4 +1,5 @@ """Tests for SpeedTest sensors.""" + from unittest.mock import MagicMock from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN diff --git a/tests/components/spider/test_config_flow.py b/tests/components/spider/test_config_flow.py index d8055538faa..1fb61573216 100644 --- a/tests/components/spider/test_config_flow.py +++ b/tests/components/spider/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for the Spider config flow.""" + from unittest.mock import Mock, patch import pytest @@ -36,11 +37,14 @@ async def test_user(hass: HomeAssistant, spider) -> None: assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" - with patch( - "homeassistant.components.spider.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.spider.async_setup_entry", return_value=True - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.spider.async_setup", return_value=True + ) as mock_setup, + patch( + "homeassistant.components.spider.async_setup_entry", return_value=True + ) as mock_setup_entry, + ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=SPIDER_USER_DATA ) @@ -59,13 +63,16 @@ async def test_user(hass: HomeAssistant, spider) -> None: async def test_import(hass: HomeAssistant, spider) -> None: """Test import step.""" - with patch( - "homeassistant.components.spider.async_setup", - return_value=True, - ) as mock_setup, patch( - "homeassistant.components.spider.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.spider.async_setup", + return_value=True, + ) as mock_setup, + patch( + "homeassistant.components.spider.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}, diff --git a/tests/components/spotify/test_config_flow.py b/tests/components/spotify/test_config_flow.py index 7940964d68f..1ab4e46bd55 100644 --- a/tests/components/spotify/test_config_flow.py +++ b/tests/components/spotify/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for the Spotify config flow.""" + from http import HTTPStatus from ipaddress import ip_address from unittest.mock import patch @@ -121,9 +122,10 @@ async def test_full_flow( }, ) - with patch( - "homeassistant.components.spotify.async_setup_entry", return_value=True - ), patch("homeassistant.components.spotify.config_flow.Spotify") as spotify_mock: + with ( + patch("homeassistant.components.spotify.async_setup_entry", return_value=True), + patch("homeassistant.components.spotify.config_flow.Spotify") as spotify_mock, + ): spotify_mock.return_value.current_user.return_value = { "id": "fake_id", "display_name": "frenck", @@ -234,9 +236,10 @@ async def test_reauthentication( }, ) - with patch( - "homeassistant.components.spotify.async_setup_entry", return_value=True - ), patch("homeassistant.components.spotify.config_flow.Spotify") as spotify_mock: + with ( + patch("homeassistant.components.spotify.async_setup_entry", return_value=True), + patch("homeassistant.components.spotify.config_flow.Spotify") as spotify_mock, + ): spotify_mock.return_value.current_user.return_value = {"id": "frenck"} result = await hass.config_entries.flow.async_configure(result["flow_id"]) diff --git a/tests/components/sql/__init__.py b/tests/components/sql/__init__.py index 1d3ce0878c3..fc122ad1a95 100644 --- a/tests/components/sql/__init__.py +++ b/tests/components/sql/__init__.py @@ -1,4 +1,5 @@ """Tests for the sql component.""" + from __future__ import annotations from typing import Any diff --git a/tests/components/sql/test_config_flow.py b/tests/components/sql/test_config_flow.py index 43608d0d32a..7b3b0aaf350 100644 --- a/tests/components/sql/test_config_flow.py +++ b/tests/components/sql/test_config_flow.py @@ -1,4 +1,5 @@ """Test the SQL config flow.""" + from __future__ import annotations from unittest.mock import patch @@ -591,11 +592,14 @@ async def test_options_flow_db_url_empty( assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" - with patch( - "homeassistant.components.sql.async_setup_entry", - return_value=True, - ), patch( - "homeassistant.components.sql.config_flow.sqlalchemy.create_engine", + with ( + patch( + "homeassistant.components.sql.async_setup_entry", + return_value=True, + ), + patch( + "homeassistant.components.sql.config_flow.sqlalchemy.create_engine", + ), ): result = await hass.config_entries.options.async_configure( result["flow_id"], @@ -626,11 +630,14 @@ async def test_full_flow_not_recorder_db( assert result["type"] == FlowResultType.FORM assert result["errors"] == {} - with patch( - "homeassistant.components.sql.async_setup_entry", - return_value=True, - ), patch( - "homeassistant.components.sql.config_flow.sqlalchemy.create_engine", + with ( + patch( + "homeassistant.components.sql.async_setup_entry", + return_value=True, + ), + patch( + "homeassistant.components.sql.config_flow.sqlalchemy.create_engine", + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -659,11 +666,14 @@ async def test_full_flow_not_recorder_db( assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" - with patch( - "homeassistant.components.sql.async_setup_entry", - return_value=True, - ), patch( - "homeassistant.components.sql.config_flow.sqlalchemy.create_engine", + with ( + patch( + "homeassistant.components.sql.async_setup_entry", + return_value=True, + ), + patch( + "homeassistant.components.sql.config_flow.sqlalchemy.create_engine", + ), ): result = await hass.config_entries.options.async_configure( result["flow_id"], diff --git a/tests/components/sql/test_init.py b/tests/components/sql/test_init.py index 2ae6010e0c5..cf5721f52f6 100644 --- a/tests/components/sql/test_init.py +++ b/tests/components/sql/test_init.py @@ -1,4 +1,5 @@ """Test for SQL component Init.""" + from __future__ import annotations from unittest.mock import patch diff --git a/tests/components/sql/test_sensor.py b/tests/components/sql/test_sensor.py index dd0bd06c008..14442aa5181 100644 --- a/tests/components/sql/test_sensor.py +++ b/tests/components/sql/test_sensor.py @@ -1,4 +1,5 @@ """The test for the sql sensor platform.""" + from __future__ import annotations from datetime import timedelta @@ -152,9 +153,12 @@ async def test_query_mssql_no_result( "column": "value", "name": "count_tables", } - with patch("homeassistant.components.sql.sensor.sqlalchemy"), patch( - "homeassistant.components.sql.sensor.sqlalchemy.text", - return_value=sql_text("SELECT TOP 1 5 as value where 1=2"), + with ( + patch("homeassistant.components.sql.sensor.sqlalchemy"), + patch( + "homeassistant.components.sql.sensor.sqlalchemy.text", + return_value=sql_text("SELECT TOP 1 5 as value where 1=2"), + ), ): await init_integration(hass, config) @@ -247,7 +251,7 @@ async def test_invalid_url_on_update( hass, dt_util.utcnow() + timedelta(minutes=1), ) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert "sqlite://****:****@homeassistant.local" in caplog.text @@ -286,7 +290,7 @@ async def test_templates_with_yaml( hass, dt_util.utcnow() + timedelta(minutes=1), ) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get("sensor.get_values_with_template") assert state.state == "5" @@ -300,7 +304,7 @@ async def test_templates_with_yaml( hass, dt_util.utcnow() + timedelta(minutes=2), ) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get("sensor.get_values_with_template") assert state.state == STATE_UNAVAILABLE @@ -313,7 +317,7 @@ async def test_templates_with_yaml( hass, dt_util.utcnow() + timedelta(minutes=3), ) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get("sensor.get_values_with_template") assert state.state == "5" @@ -487,7 +491,7 @@ async def test_no_issue_when_view_has_the_text_entity_id_in_it( hass, dt_util.utcnow() + timedelta(minutes=1), ) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert ( "Query contains entity_id but does not reference states_meta" not in caplog.text @@ -621,7 +625,7 @@ async def test_query_recover_from_rollback( ): freezer.tick(timedelta(minutes=1)) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert "sqlite3.OperationalError" in caplog.text state = hass.states.get("sensor.select_value_sql_query") @@ -630,7 +634,7 @@ async def test_query_recover_from_rollback( freezer.tick(timedelta(minutes=1)) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get("sensor.select_value_sql_query") assert state.state == "5" diff --git a/tests/components/sql/test_util.py b/tests/components/sql/test_util.py index 5211a47c4d4..004b511a2f0 100644 --- a/tests/components/sql/test_util.py +++ b/tests/components/sql/test_util.py @@ -1,4 +1,5 @@ """Test the sql utils.""" + from homeassistant.components.recorder import Recorder, get_instance from homeassistant.components.sql.util import resolve_db_url from homeassistant.core import HomeAssistant diff --git a/tests/components/squeezebox/test_config_flow.py b/tests/components/squeezebox/test_config_flow.py index 04d30cdd331..dc82b658163 100644 --- a/tests/components/squeezebox/test_config_flow.py +++ b/tests/components/squeezebox/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Logitech Squeezebox config flow.""" + from http import HTTPStatus from unittest.mock import patch @@ -37,14 +38,19 @@ async def patch_async_query_unauthorized(self, *args): async def test_user_form(hass: HomeAssistant) -> None: """Test user-initiated flow, including discovery and the edit step.""" - with patch( - "pysqueezebox.Server.async_query", - return_value={"uuid": UUID}, - ), patch( - "homeassistant.components.squeezebox.async_setup_entry", - return_value=True, - ) as mock_setup_entry, patch( - "homeassistant.components.squeezebox.config_flow.async_discover", mock_discover + with ( + patch( + "pysqueezebox.Server.async_query", + return_value={"uuid": UUID}, + ), + patch( + "homeassistant.components.squeezebox.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + patch( + "homeassistant.components.squeezebox.config_flow.async_discover", + mock_discover, + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -83,10 +89,13 @@ async def test_user_form(hass: HomeAssistant) -> None: async def test_user_form_timeout(hass: HomeAssistant) -> None: """Test we handle server search timeout.""" - with patch( - "homeassistant.components.squeezebox.config_flow.async_discover", - mock_failed_discover, - ), patch("homeassistant.components.squeezebox.config_flow.TIMEOUT", 0.1): + with ( + patch( + "homeassistant.components.squeezebox.config_flow.async_discover", + mock_failed_discover, + ), + patch("homeassistant.components.squeezebox.config_flow.TIMEOUT", 0.1), + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -107,12 +116,16 @@ async def test_user_form_timeout(hass: HomeAssistant) -> None: async def test_user_form_duplicate(hass: HomeAssistant) -> None: """Test duplicate discovered servers are skipped.""" - with patch( - "homeassistant.components.squeezebox.config_flow.async_discover", - mock_discover, - ), patch("homeassistant.components.squeezebox.config_flow.TIMEOUT", 0.1), patch( - "homeassistant.components.squeezebox.async_setup_entry", - return_value=True, + with ( + patch( + "homeassistant.components.squeezebox.config_flow.async_discover", + mock_discover, + ), + patch("homeassistant.components.squeezebox.config_flow.TIMEOUT", 0.1), + patch( + "homeassistant.components.squeezebox.async_setup_entry", + return_value=True, + ), ): entry = MockConfigEntry( domain=DOMAIN, @@ -206,11 +219,15 @@ async def test_discovery_no_uuid(hass: HomeAssistant) -> None: async def test_dhcp_discovery(hass: HomeAssistant) -> None: """Test we can process discovery from dhcp.""" - with patch( - "pysqueezebox.Server.async_query", - return_value={"uuid": UUID}, - ), patch( - "homeassistant.components.squeezebox.config_flow.async_discover", mock_discover + with ( + patch( + "pysqueezebox.Server.async_query", + return_value={"uuid": UUID}, + ), + patch( + "homeassistant.components.squeezebox.config_flow.async_discover", + mock_discover, + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -227,10 +244,13 @@ async def test_dhcp_discovery(hass: HomeAssistant) -> None: async def test_dhcp_discovery_no_server_found(hass: HomeAssistant) -> None: """Test we can handle dhcp discovery when no server is found.""" - with patch( - "homeassistant.components.squeezebox.config_flow.async_discover", - mock_failed_discover, - ), patch("homeassistant.components.squeezebox.config_flow.TIMEOUT", 0.1): + with ( + patch( + "homeassistant.components.squeezebox.config_flow.async_discover", + mock_failed_discover, + ), + patch("homeassistant.components.squeezebox.config_flow.TIMEOUT", 0.1), + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, diff --git a/tests/components/srp_energy/conftest.py b/tests/components/srp_energy/conftest.py index e3597081d77..c2cc6d5e1a4 100644 --- a/tests/components/srp_energy/conftest.py +++ b/tests/components/srp_energy/conftest.py @@ -1,4 +1,5 @@ """Fixtures for Srp Energy integration tests.""" + from __future__ import annotations from collections.abc import Generator diff --git a/tests/components/srp_energy/test_config_flow.py b/tests/components/srp_energy/test_config_flow.py index 572b67259f1..8d4904bf00d 100644 --- a/tests/components/srp_energy/test_config_flow.py +++ b/tests/components/srp_energy/test_config_flow.py @@ -1,4 +1,5 @@ """Test the SRP Energy config flow.""" + from unittest.mock import MagicMock, patch from homeassistant.components.srp_energy.const import CONF_IS_TOU, DOMAIN diff --git a/tests/components/srp_energy/test_init.py b/tests/components/srp_energy/test_init.py index a60dd09ea11..e2411fd4688 100644 --- a/tests/components/srp_energy/test_init.py +++ b/tests/components/srp_energy/test_init.py @@ -1,4 +1,5 @@ """Tests for Srp Energy component Init.""" + from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant diff --git a/tests/components/srp_energy/test_sensor.py b/tests/components/srp_energy/test_sensor.py index 2d49fd13bf1..a46caf904b7 100644 --- a/tests/components/srp_energy/test_sensor.py +++ b/tests/components/srp_energy/test_sensor.py @@ -1,4 +1,5 @@ """Tests for the srp_energy sensor platform.""" + import time from unittest.mock import patch @@ -71,10 +72,11 @@ async def test_srp_entity_timeout( ) -> None: """Test the SrpEntity timing out.""" - with patch( - "homeassistant.components.srp_energy.SrpEnergyClient", autospec=True - ) as srp_energy_mock, patch( - "homeassistant.components.srp_energy.coordinator.TIMEOUT", 0 + with ( + patch( + "homeassistant.components.srp_energy.SrpEnergyClient", autospec=True + ) as srp_energy_mock, + patch("homeassistant.components.srp_energy.coordinator.TIMEOUT", 0), ): client = srp_energy_mock.return_value client.validate.return_value = True diff --git a/tests/components/ssdp/conftest.py b/tests/components/ssdp/conftest.py index 7b6d67895e5..8b06163cd95 100644 --- a/tests/components/ssdp/conftest.py +++ b/tests/components/ssdp/conftest.py @@ -1,4 +1,5 @@ """Configuration for SSDP tests.""" + from unittest.mock import AsyncMock, patch from async_upnp_client.server import UpnpServer @@ -9,9 +10,11 @@ import pytest @pytest.fixture(autouse=True) async def silent_ssdp_listener(): """Patch SsdpListener class, preventing any actual SSDP traffic.""" - with patch("homeassistant.components.ssdp.SsdpListener.async_start"), patch( - "homeassistant.components.ssdp.SsdpListener.async_stop" - ), patch("homeassistant.components.ssdp.SsdpListener.async_search"): + with ( + patch("homeassistant.components.ssdp.SsdpListener.async_start"), + patch("homeassistant.components.ssdp.SsdpListener.async_stop"), + patch("homeassistant.components.ssdp.SsdpListener.async_search"), + ): # Fixtures are initialized before patches. When the component is started here, # certain functions/methods might not be patched in time. yield SsdpListener @@ -20,9 +23,11 @@ async def silent_ssdp_listener(): @pytest.fixture(autouse=True) async def disabled_upnp_server(): """Disable UPnpServer.""" - with patch("homeassistant.components.ssdp.UpnpServer.async_start"), patch( - "homeassistant.components.ssdp.UpnpServer.async_stop" - ), patch("homeassistant.components.ssdp._async_find_next_available_port"): + with ( + patch("homeassistant.components.ssdp.UpnpServer.async_start"), + patch("homeassistant.components.ssdp.UpnpServer.async_stop"), + patch("homeassistant.components.ssdp._async_find_next_available_port"), + ): yield UpnpServer diff --git a/tests/components/ssdp/test_init.py b/tests/components/ssdp/test_init.py index 324136c011b..5131388c4e3 100644 --- a/tests/components/ssdp/test_init.py +++ b/tests/components/ssdp/test_init.py @@ -1,4 +1,5 @@ """Test the SSDP integration.""" + from datetime import datetime from ipaddress import IPv4Address from unittest.mock import ANY, AsyncMock, patch @@ -342,7 +343,7 @@ async def test_flow_start_only_alive( } ) ssdp_listener._on_search(mock_ssdp_search_response) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) mock_flow_init.assert_awaited_once_with( "mock-domain", context={"source": config_entries.SOURCE_SSDP}, data=ANY @@ -463,6 +464,7 @@ async def test_start_stop_scanner(mock_source_set, hass: HomeAssistant) -> None: @pytest.mark.usefixtures("mock_get_source_ip") +@pytest.mark.no_fail_on_log_exception @patch("homeassistant.components.ssdp.async_get_ssdp", return_value={}) async def test_scan_with_registered_callback( mock_get_ssdp, @@ -522,9 +524,9 @@ async def test_scan_with_registered_callback( async_match_any_callback = AsyncMock() await ssdp.async_register_callback(hass, async_match_any_callback) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) ssdp_listener._on_search(mock_ssdp_search_response) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert async_integration_callback.call_count == 1 assert async_integration_match_all_callback.call_count == 1 @@ -548,7 +550,7 @@ async def test_scan_with_registered_callback( ssdp.ATTR_UPNP_DEVICE_TYPE: "Paulus", ssdp.ATTR_UPNP_UDN: "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL", } - assert "Failed to callback info" in caplog.text + assert "Exception in SSDP callback" in caplog.text async_integration_callback_from_cache = AsyncMock() await ssdp.async_register_callback( @@ -834,7 +836,7 @@ async def test_flow_dismiss_on_byebye( } ) ssdp_listener._on_search(mock_ssdp_search_response) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) mock_flow_init.assert_awaited_once_with( "mock-domain", context={"source": config_entries.SOURCE_SSDP}, data=ANY @@ -852,22 +854,23 @@ async def test_flow_dismiss_on_byebye( } ) ssdp_listener._on_alive(mock_ssdp_advertisement) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) mock_flow_init.assert_awaited_once_with( "mock-domain", context={"source": config_entries.SOURCE_SSDP}, data=ANY ) mock_ssdp_advertisement["nts"] = "ssdp:byebye" # ssdp:byebye advertisement should dismiss existing flows - with patch.object( - hass.config_entries.flow, - "async_progress_by_init_data_type", - return_value=[{"flow_id": "mock_flow_id"}], - ) as mock_async_progress_by_init_data_type, patch.object( - hass.config_entries.flow, "async_abort" - ) as mock_async_abort: + with ( + patch.object( + hass.config_entries.flow, + "async_progress_by_init_data_type", + return_value=[{"flow_id": "mock_flow_id"}], + ) as mock_async_progress_by_init_data_type, + patch.object(hass.config_entries.flow, "async_abort") as mock_async_abort, + ): ssdp_listener._on_byebye(mock_ssdp_advertisement) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert len(mock_async_progress_by_init_data_type.mock_calls) == 1 assert mock_async_abort.mock_calls[0][1][0] == "mock_flow_id" diff --git a/tests/components/starline/test_config_flow.py b/tests/components/starline/test_config_flow.py index 4277f01037f..0d7d7cc0731 100644 --- a/tests/components/starline/test_config_flow.py +++ b/tests/components/starline/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for StarLine config flow.""" + import requests_mock from homeassistant import config_entries diff --git a/tests/components/starlink/fixtures/sleep_data_success.json b/tests/components/starlink/fixtures/sleep_data_success.json new file mode 100644 index 00000000000..99942688adc --- /dev/null +++ b/tests/components/starlink/fixtures/sleep_data_success.json @@ -0,0 +1 @@ +[0, 1, false] diff --git a/tests/components/starlink/patchers.py b/tests/components/starlink/patchers.py index d83451ecc17..f8179f07bed 100644 --- a/tests/components/starlink/patchers.py +++ b/tests/components/starlink/patchers.py @@ -1,4 +1,5 @@ """General Starlink patchers.""" + import json from unittest.mock import patch @@ -18,6 +19,11 @@ LOCATION_DATA_SUCCESS_PATCHER = patch( return_value=json.loads(load_fixture("location_data_success.json", "starlink")), ) +SLEEP_DATA_SUCCESS_PATCHER = patch( + "homeassistant.components.starlink.coordinator.get_sleep_config", + return_value=json.loads(load_fixture("sleep_data_success.json", "starlink")), +) + DEVICE_FOUND_PATCHER = patch( "homeassistant.components.starlink.config_flow.get_id", return_value="some-valid-id" ) diff --git a/tests/components/starlink/snapshots/test_diagnostics.ambr b/tests/components/starlink/snapshots/test_diagnostics.ambr index 3bb7f235017..4c85ad84ca7 100644 --- a/tests/components/starlink/snapshots/test_diagnostics.ambr +++ b/tests/components/starlink/snapshots/test_diagnostics.ambr @@ -52,6 +52,11 @@ None, ]), }), + 'sleep': list([ + 0, + 1, + False, + ]), 'status': dict({ 'alerts': 0, 'currently_obstructed': False, diff --git a/tests/components/starlink/test_config_flow.py b/tests/components/starlink/test_config_flow.py index 3bb3f286638..5b0e122ad5d 100644 --- a/tests/components/starlink/test_config_flow.py +++ b/tests/components/starlink/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Starlink config flow.""" + from homeassistant import config_entries, data_entry_flow from homeassistant.components.starlink.const import DOMAIN from homeassistant.config_entries import SOURCE_USER diff --git a/tests/components/starlink/test_diagnostics.py b/tests/components/starlink/test_diagnostics.py index 231b58a2d5e..c5876e5e9f2 100644 --- a/tests/components/starlink/test_diagnostics.py +++ b/tests/components/starlink/test_diagnostics.py @@ -1,11 +1,16 @@ """Tests for Starlink diagnostics.""" + from syrupy.assertion import SnapshotAssertion from homeassistant.components.starlink.const import DOMAIN from homeassistant.const import CONF_IP_ADDRESS from homeassistant.core import HomeAssistant -from .patchers import LOCATION_DATA_SUCCESS_PATCHER, STATUS_DATA_SUCCESS_PATCHER +from .patchers import ( + LOCATION_DATA_SUCCESS_PATCHER, + SLEEP_DATA_SUCCESS_PATCHER, + STATUS_DATA_SUCCESS_PATCHER, +) from tests.common import MockConfigEntry from tests.components.diagnostics import get_diagnostics_for_config_entry @@ -23,7 +28,11 @@ async def test_diagnostics( data={CONF_IP_ADDRESS: "1.2.3.4:0000"}, ) - with STATUS_DATA_SUCCESS_PATCHER, LOCATION_DATA_SUCCESS_PATCHER: + with ( + STATUS_DATA_SUCCESS_PATCHER, + LOCATION_DATA_SUCCESS_PATCHER, + SLEEP_DATA_SUCCESS_PATCHER, + ): entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) diff --git a/tests/components/starlink/test_init.py b/tests/components/starlink/test_init.py index 94a8a2a341b..03e9787b6c0 100644 --- a/tests/components/starlink/test_init.py +++ b/tests/components/starlink/test_init.py @@ -1,10 +1,15 @@ """Tests Starlink integration init/unload.""" + from homeassistant.components.starlink.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_IP_ADDRESS from homeassistant.core import HomeAssistant -from .patchers import LOCATION_DATA_SUCCESS_PATCHER, STATUS_DATA_SUCCESS_PATCHER +from .patchers import ( + LOCATION_DATA_SUCCESS_PATCHER, + SLEEP_DATA_SUCCESS_PATCHER, + STATUS_DATA_SUCCESS_PATCHER, +) from tests.common import MockConfigEntry @@ -16,7 +21,11 @@ async def test_successful_entry(hass: HomeAssistant) -> None: data={CONF_IP_ADDRESS: "1.2.3.4:0000"}, ) - with STATUS_DATA_SUCCESS_PATCHER, LOCATION_DATA_SUCCESS_PATCHER: + with ( + STATUS_DATA_SUCCESS_PATCHER, + LOCATION_DATA_SUCCESS_PATCHER, + SLEEP_DATA_SUCCESS_PATCHER, + ): entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) @@ -33,7 +42,11 @@ async def test_unload_entry(hass: HomeAssistant) -> None: data={CONF_IP_ADDRESS: "1.2.3.4:0000"}, ) - with STATUS_DATA_SUCCESS_PATCHER, LOCATION_DATA_SUCCESS_PATCHER: + with ( + STATUS_DATA_SUCCESS_PATCHER, + LOCATION_DATA_SUCCESS_PATCHER, + SLEEP_DATA_SUCCESS_PATCHER, + ): entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) diff --git a/tests/components/startca/test_sensor.py b/tests/components/startca/test_sensor.py index 7b691410907..b0d43af1cae 100644 --- a/tests/components/startca/test_sensor.py +++ b/tests/components/startca/test_sensor.py @@ -1,4 +1,5 @@ """Tests for the Start.ca sensor platform.""" + from http import HTTPStatus from homeassistant.bootstrap import async_setup_component diff --git a/tests/components/statistics/test_sensor.py b/tests/components/statistics/test_sensor.py index 7f3c9881751..fd9a5ca85bd 100644 --- a/tests/components/statistics/test_sensor.py +++ b/tests/components/statistics/test_sensor.py @@ -1,4 +1,5 @@ """The test for the statistics sensor platform.""" + from __future__ import annotations from collections.abc import Sequence @@ -1291,17 +1292,16 @@ async def test_state_characteristics(hass: HomeAssistant) -> None: "unit": "%", }, ) - sensors_config = [] - for characteristic in characteristics: - sensors_config.append( - { - "platform": "statistics", - "name": f"test_{characteristic['source_sensor_domain']}_{characteristic['name']}", - "entity_id": f"{characteristic['source_sensor_domain']}.test_monitored", - "state_characteristic": characteristic["name"], - "max_age": {"minutes": 8}, # 9 values spaces by one minute - } - ) + sensors_config = [ + { + "platform": "statistics", + "name": f"test_{characteristic['source_sensor_domain']}_{characteristic['name']}", + "entity_id": f"{characteristic['source_sensor_domain']}.test_monitored", + "state_characteristic": characteristic["name"], + "max_age": {"minutes": 8}, # 9 values spaces by one minute + } + for characteristic in characteristics + ] with freeze_time(current_time) as freezer: assert await async_setup_component( @@ -1530,8 +1530,9 @@ async def test_initialize_from_database_with_maxage( await hass.async_block_till_done() await async_wait_recording_done(hass) - with freeze_time(current_time) as freezer, patch.object( - StatisticsSensor, "_purge_old_states", mock_purge + with ( + freeze_time(current_time) as freezer, + patch.object(StatisticsSensor, "_purge_old_states", mock_purge), ): for value in VALUES_NUMERIC: hass.states.async_set( diff --git a/tests/components/statsd/test_init.py b/tests/components/statsd/test_init.py index 1b48b6195e5..e24909e3f53 100644 --- a/tests/components/statsd/test_init.py +++ b/tests/components/statsd/test_init.py @@ -1,4 +1,5 @@ """The tests for the StatsD feeder.""" + from unittest import mock from unittest.mock import patch diff --git a/tests/components/steam_online/__init__.py b/tests/components/steam_online/__init__.py index 786c5d67782..c7d67509489 100644 --- a/tests/components/steam_online/__init__.py +++ b/tests/components/steam_online/__init__.py @@ -1,4 +1,5 @@ """Tests for Steam integration.""" + import random import string from unittest.mock import patch @@ -68,10 +69,10 @@ class MockedInterface(dict): def GetFriendList(self, steamid: str) -> dict: """Get friend list.""" fake_friends = [{"steamid": ACCOUNT_2}] - for _i in range(0, 4): - fake_friends.append( - {"steamid": "".join(random.choices(string.digits, k=len(ACCOUNT_1)))} - ) + fake_friends.extend( + {"steamid": "".join(random.choices(string.digits, k=len(ACCOUNT_1)))} + for _ in range(4) + ) return {"friendslist": {"friends": fake_friends}} def GetPlayerSummaries(self, steamids: str | list[str]) -> dict: diff --git a/tests/components/steam_online/test_config_flow.py b/tests/components/steam_online/test_config_flow.py index a62adb18776..00b47ea48bd 100644 --- a/tests/components/steam_online/test_config_flow.py +++ b/tests/components/steam_online/test_config_flow.py @@ -1,4 +1,5 @@ """Test Steam config flow.""" + from unittest.mock import patch import steam @@ -26,9 +27,12 @@ from . import ( async def test_flow_user(hass: HomeAssistant) -> None: """Test user initialized flow.""" - with patch_interface(), patch( - "homeassistant.components.steam_online.async_setup_entry", - return_value=True, + with ( + patch_interface(), + patch( + "homeassistant.components.steam_online.async_setup_entry", + return_value=True, + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -138,9 +142,12 @@ async def test_flow_reauth(hass: HomeAssistant) -> None: async def test_options_flow(hass: HomeAssistant) -> None: """Test updating options.""" entry = create_entry(hass) - with patch_interface(), patch( - "homeassistant.components.steam_online.config_flow.MAX_IDS_TO_REQUEST", - return_value=2, + with ( + patch_interface(), + patch( + "homeassistant.components.steam_online.config_flow.MAX_IDS_TO_REQUEST", + return_value=2, + ), ): await hass.config_entries.async_setup(entry.entry_id) result = await hass.config_entries.options.async_init(entry.entry_id) @@ -162,17 +169,23 @@ async def test_options_flow(hass: HomeAssistant) -> None: async def test_options_flow_deselect(hass: HomeAssistant) -> None: """Test deselecting user.""" entry = create_entry(hass) - with patch_interface(), patch( - "homeassistant.components.steam_online.config_flow.MAX_IDS_TO_REQUEST", - return_value=2, + with ( + patch_interface(), + patch( + "homeassistant.components.steam_online.config_flow.MAX_IDS_TO_REQUEST", + return_value=2, + ), ): await hass.config_entries.async_setup(entry.entry_id) result = await hass.config_entries.options.async_init(entry.entry_id) await hass.async_block_till_done() - with patch_interface(), patch( - "homeassistant.components.steam_online.async_setup_entry", - return_value=True, + with ( + patch_interface(), + patch( + "homeassistant.components.steam_online.async_setup_entry", + return_value=True, + ), ): assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" diff --git a/tests/components/steam_online/test_init.py b/tests/components/steam_online/test_init.py index e3f473e01c6..48584dab7a5 100644 --- a/tests/components/steam_online/test_init.py +++ b/tests/components/steam_online/test_init.py @@ -1,4 +1,5 @@ """Tests for the Steam component.""" + import steam from homeassistant.components.steam_online.const import DEFAULT_NAME, DOMAIN diff --git a/tests/components/steamist/__init__.py b/tests/components/steamist/__init__.py index 8761d675d54..47fa2236849 100644 --- a/tests/components/steamist/__init__.py +++ b/tests/components/steamist/__init__.py @@ -1,4 +1,5 @@ """Tests for the Steamist integration.""" + from __future__ import annotations from contextlib import contextmanager diff --git a/tests/components/steamist/test_config_flow.py b/tests/components/steamist/test_config_flow.py index 9664eb7323f..9480703af9f 100644 --- a/tests/components/steamist/test_config_flow.py +++ b/tests/components/steamist/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Steamist config flow.""" + from unittest.mock import patch import pytest @@ -44,12 +45,16 @@ async def test_form(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.FORM assert result["errors"] == {} - with _patch_discovery(no_device=True), patch( - "homeassistant.components.steamist.config_flow.Steamist.async_get_status" - ), patch( - "homeassistant.components.steamist.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + _patch_discovery(no_device=True), + patch( + "homeassistant.components.steamist.config_flow.Steamist.async_get_status" + ), + patch( + "homeassistant.components.steamist.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -74,12 +79,16 @@ async def test_form_with_discovery(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.FORM assert result["errors"] == {} - with _patch_discovery(), patch( - "homeassistant.components.steamist.config_flow.Steamist.async_get_status" - ), patch( - "homeassistant.components.steamist.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + _patch_discovery(), + patch( + "homeassistant.components.steamist.config_flow.Steamist.async_get_status" + ), + patch( + "homeassistant.components.steamist.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -168,11 +177,12 @@ async def test_discovery(hass: HomeAssistant) -> None: assert result2["step_id"] == "pick_device" assert not result2["errors"] - with _patch_discovery(), _patch_status(MOCK_ASYNC_GET_STATUS_INACTIVE), patch( - f"{MODULE}.async_setup", return_value=True - ) as mock_setup, patch( - f"{MODULE}.async_setup_entry", return_value=True - ) as mock_setup_entry: + with ( + _patch_discovery(), + _patch_status(MOCK_ASYNC_GET_STATUS_INACTIVE), + 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: FORMATTED_MAC_ADDRESS}, @@ -253,11 +263,14 @@ async def test_discovered_by_discovery(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.FORM assert result["errors"] is None - with _patch_discovery(), _patch_status(MOCK_ASYNC_GET_STATUS_INACTIVE), 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: + with ( + _patch_discovery(), + _patch_status(MOCK_ASYNC_GET_STATUS_INACTIVE), + 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() @@ -281,11 +294,14 @@ async def test_discovered_by_dhcp(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.FORM assert result["errors"] is None - with _patch_discovery(), _patch_status(MOCK_ASYNC_GET_STATUS_INACTIVE), 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: + with ( + _patch_discovery(), + _patch_status(MOCK_ASYNC_GET_STATUS_INACTIVE), + 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() @@ -298,8 +314,9 @@ async def test_discovered_by_dhcp(hass: HomeAssistant) -> None: async def test_discovered_by_dhcp_discovery_fails(hass: HomeAssistant) -> None: """Test we can setup when discovered from dhcp but then we cannot get the device name.""" - with _patch_discovery(no_device=True), _patch_status( - MOCK_ASYNC_GET_STATUS_INACTIVE + with ( + _patch_discovery(no_device=True), + _patch_status(MOCK_ASYNC_GET_STATUS_INACTIVE), ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -317,8 +334,9 @@ async def test_discovered_by_dhcp_discovery_finds_non_steamist_device( ) -> None: """Test we can setup when discovered from dhcp but its not a steamist device.""" - with _patch_discovery(device=DEVICE_30303_NOT_STEAMIST), _patch_status( - MOCK_ASYNC_GET_STATUS_INACTIVE + with ( + _patch_discovery(device=DEVICE_30303_NOT_STEAMIST), + _patch_status(MOCK_ASYNC_GET_STATUS_INACTIVE), ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -345,11 +363,12 @@ async def test_discovered_by_dhcp_or_discovery_adds_missing_unique_id( config_entry = MockConfigEntry(domain=DOMAIN, data={CONF_HOST: DEVICE_IP_ADDRESS}) config_entry.add_to_hass(hass) - with _patch_discovery(), _patch_status(MOCK_ASYNC_GET_STATUS_INACTIVE), patch( - f"{MODULE}.async_setup", return_value=True - ) as mock_setup, patch( - f"{MODULE}.async_setup_entry", return_value=True - ) as mock_setup_entry: + with ( + _patch_discovery(), + _patch_status(MOCK_ASYNC_GET_STATUS_INACTIVE), + patch(f"{MODULE}.async_setup", return_value=True) as mock_setup, + patch(f"{MODULE}.async_setup_entry", return_value=True) as mock_setup_entry, + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": source}, data=data ) @@ -379,11 +398,12 @@ async def test_discovered_by_dhcp_or_discovery_existing_unique_id_does_not_reloa ) config_entry.add_to_hass(hass) - with _patch_discovery(), _patch_status(MOCK_ASYNC_GET_STATUS_INACTIVE), patch( - f"{MODULE}.async_setup", return_value=True - ) as mock_setup, patch( - f"{MODULE}.async_setup_entry", return_value=True - ) as mock_setup_entry: + with ( + _patch_discovery(), + _patch_status(MOCK_ASYNC_GET_STATUS_INACTIVE), + patch(f"{MODULE}.async_setup", return_value=True) as mock_setup, + patch(f"{MODULE}.async_setup_entry", return_value=True) as mock_setup_entry, + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": source}, data=data ) diff --git a/tests/components/steamist/test_init.py b/tests/components/steamist/test_init.py index 3c4b0084807..32400449d0d 100644 --- a/tests/components/steamist/test_init.py +++ b/tests/components/steamist/test_init.py @@ -1,9 +1,11 @@ """Tests for the steamist component.""" + from __future__ import annotations from unittest.mock import AsyncMock, MagicMock, patch from discovery30303 import AIODiscovery30303 +from freezegun.api import FrozenDateTimeFactory import pytest from homeassistant.components import steamist @@ -90,9 +92,12 @@ async def test_config_entry_fills_unique_id_with_directed_discovery( mock_aio_discovery.async_scan = _async_scan type(mock_aio_discovery).found_devices = found_devices - with _patch_status(MOCK_ASYNC_GET_STATUS_ACTIVE), patch( - "homeassistant.components.steamist.discovery.AIODiscovery30303", - return_value=mock_aio_discovery, + with ( + _patch_status(MOCK_ASYNC_GET_STATUS_ACTIVE), + patch( + "homeassistant.components.steamist.discovery.AIODiscovery30303", + return_value=mock_aio_discovery, + ), ): await async_setup_component(hass, steamist.DOMAIN, {steamist.DOMAIN: {}}) await hass.async_block_till_done() @@ -112,7 +117,9 @@ async def test_config_entry_fills_unique_id_with_directed_discovery( @pytest.mark.usefixtures("mock_single_broadcast_address") -async def test_discovery_happens_at_interval(hass: HomeAssistant) -> None: +async def test_discovery_happens_at_interval( + hass: HomeAssistant, freezer: FrozenDateTimeFactory +) -> None: """Test that discovery happens at interval.""" config_entry = MockConfigEntry( domain=DOMAIN, data=DEFAULT_ENTRY_DATA, unique_id=FORMATTED_MAC_ADDRESS @@ -120,15 +127,19 @@ async def test_discovery_happens_at_interval(hass: HomeAssistant) -> None: config_entry.add_to_hass(hass) mock_aio_discovery = MagicMock(auto_spec=AIODiscovery30303) mock_aio_discovery.async_scan = AsyncMock() - with patch( - "homeassistant.components.steamist.discovery.AIODiscovery30303", - return_value=mock_aio_discovery, - ), _patch_status(MOCK_ASYNC_GET_STATUS_ACTIVE): + with ( + patch( + "homeassistant.components.steamist.discovery.AIODiscovery30303", + return_value=mock_aio_discovery, + ), + _patch_status(MOCK_ASYNC_GET_STATUS_ACTIVE), + ): await async_setup_component(hass, steamist.DOMAIN, {steamist.DOMAIN: {}}) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert len(mock_aio_discovery.async_scan.mock_calls) == 2 - async_fire_time_changed(hass, utcnow() + steamist.DISCOVERY_INTERVAL) - await hass.async_block_till_done() + freezer.move_to(utcnow() + steamist.DISCOVERY_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) assert len(mock_aio_discovery.async_scan.mock_calls) == 3 diff --git a/tests/components/steamist/test_sensor.py b/tests/components/steamist/test_sensor.py index 30dfc419f91..79592f9fc85 100644 --- a/tests/components/steamist/test_sensor.py +++ b/tests/components/steamist/test_sensor.py @@ -1,4 +1,5 @@ """Tests for the steamist sensos.""" + from __future__ import annotations from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, UnitOfTemperature, UnitOfTime diff --git a/tests/components/steamist/test_switch.py b/tests/components/steamist/test_switch.py index 47a9cbf6708..a20bebc4052 100644 --- a/tests/components/steamist/test_switch.py +++ b/tests/components/steamist/test_switch.py @@ -1,4 +1,5 @@ """Tests for the steamist switch.""" + from __future__ import annotations from datetime import timedelta diff --git a/tests/components/stookalert/test_config_flow.py b/tests/components/stookalert/test_config_flow.py index 0014f4e5ad5..9830022203a 100644 --- a/tests/components/stookalert/test_config_flow.py +++ b/tests/components/stookalert/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for the Stookalert config flow.""" + from unittest.mock import patch from homeassistant.components.stookalert.const import CONF_PROVINCE, DOMAIN diff --git a/tests/components/stookwijzer/test_config_flow.py b/tests/components/stookwijzer/test_config_flow.py index 90786659254..590c93bb3c1 100644 --- a/tests/components/stookwijzer/test_config_flow.py +++ b/tests/components/stookwijzer/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for the Stookwijzer config flow.""" + from unittest.mock import patch from homeassistant.components.stookwijzer.const import DOMAIN diff --git a/tests/components/stream/common.py b/tests/components/stream/common.py index ae4a4fc2d9d..ab42141c667 100644 --- a/tests/components/stream/common.py +++ b/tests/components/stream/common.py @@ -1,4 +1,5 @@ """Collection of test helpers.""" + from fractions import Fraction import functools from functools import partial diff --git a/tests/components/stream/conftest.py b/tests/components/stream/conftest.py index 1a7ea3ca7e6..9ce23d99152 100644 --- a/tests/components/stream/conftest.py +++ b/tests/components/stream/conftest.py @@ -9,6 +9,7 @@ nothing for the test to verify. The solution is the WorkerSync class that allows the tests to pause the worker thread before finalizing the stream so that it can inspect the output. """ + from __future__ import annotations import asyncio @@ -142,23 +143,29 @@ class HLSSync: def hls_sync(): """Patch HLSOutput to allow test to synchronize playlist requests and responses.""" sync = HLSSync() - with patch( - "homeassistant.components.stream.core.StreamOutput.recv", - side_effect=sync.recv, - autospec=True, - ), patch( - "homeassistant.components.stream.core.StreamOutput.part_recv", - side_effect=sync.part_recv, - autospec=True, - ), patch( - "homeassistant.components.stream.hls.web.HTTPBadRequest", - side_effect=sync.bad_request, - ), patch( - "homeassistant.components.stream.hls.web.HTTPNotFound", - side_effect=sync.not_found, - ), patch( - "homeassistant.components.stream.hls.web.Response", - side_effect=sync.response, + with ( + patch( + "homeassistant.components.stream.core.StreamOutput.recv", + side_effect=sync.recv, + autospec=True, + ), + patch( + "homeassistant.components.stream.core.StreamOutput.part_recv", + side_effect=sync.part_recv, + autospec=True, + ), + patch( + "homeassistant.components.stream.hls.web.HTTPBadRequest", + side_effect=sync.bad_request, + ), + patch( + "homeassistant.components.stream.hls.web.HTTPNotFound", + side_effect=sync.not_found, + ), + patch( + "homeassistant.components.stream.hls.web.Response", + side_effect=sync.response, + ), ): yield sync diff --git a/tests/components/stream/test_hls.py b/tests/components/stream/test_hls.py index c5dfa1fc7ed..6a20914250e 100644 --- a/tests/components/stream/test_hls.py +++ b/tests/components/stream/test_hls.py @@ -1,14 +1,14 @@ """The tests for hls streams.""" + from datetime import timedelta from http import HTTPStatus -import logging from unittest.mock import patch from urllib.parse import urlparse import av import pytest -from homeassistant.components.stream import create_stream +from homeassistant.components.stream import Stream, create_stream from homeassistant.components.stream.const import ( EXT_X_START_LL_HLS, EXT_X_START_NON_LL_HLS, @@ -296,28 +296,33 @@ async def test_stream_retries( stream.set_update_callback(update_callback) - cur_time = 0 + open_future1 = hass.loop.create_future() + open_future2 = hass.loop.create_future() + futures = [open_future2, open_future1] - def time_side_effect(): - logging.info("time side effect") - nonlocal cur_time - if cur_time >= 80: - logging.info("changing return value") + original_set_state = Stream._set_state + + def set_state_wrapper(self, state: bool) -> None: + if state is False: should_retry.return_value = False # Thread should exit and be joinable. - cur_time += 40 - return cur_time + original_set_state(self, state) - with patch("av.open") as av_open, patch( - "homeassistant.components.stream.time" - ) as mock_time, patch( - "homeassistant.components.stream.STREAM_RESTART_INCREMENT", 0 + def av_open_side_effect(*args, **kwargs): + hass.loop.call_soon_threadsafe(futures.pop().set_result, None) + raise av.error.InvalidDataError(-2, "error") + + with ( + patch("av.open") as av_open, + patch("homeassistant.components.stream.Stream._set_state", set_state_wrapper), + patch("homeassistant.components.stream.STREAM_RESTART_INCREMENT", 0), ): - av_open.side_effect = av.error.InvalidDataError(-2, "error") - mock_time.time.side_effect = time_side_effect + av_open.side_effect = av_open_side_effect # Request stream. Enable retries which are disabled by default in tests. should_retry.return_value = True await stream.start() - stream._thread.join() + await open_future1 + await open_future2 + await hass.async_add_executor_job(stream._thread.join) stream._thread = None assert av_open.call_count == 2 await hass.async_block_till_done() @@ -398,9 +403,7 @@ async def test_hls_max_segments( # Only NUM_PLAYLIST_SEGMENTS are returned in the playlist. start = MAX_SEGMENTS + 1 - NUM_PLAYLIST_SEGMENTS - segments = [] - for sequence in range(start, MAX_SEGMENTS + 1): - segments.append(make_segment(sequence)) + segments = [make_segment(sequence) for sequence in range(start, MAX_SEGMENTS + 1)] assert await resp.text() == make_playlist(sequence=start, segments=segments) # Fetch the actual segments with a fake byte payload @@ -496,9 +499,7 @@ async def test_hls_max_segments_discontinuity( # EXT-X-DISCONTINUITY tag to be omitted and EXT-X-DISCONTINUITY-SEQUENCE # returned instead. start = MAX_SEGMENTS + 1 - NUM_PLAYLIST_SEGMENTS - segments = [] - for sequence in range(start, MAX_SEGMENTS + 1): - segments.append(make_segment(sequence)) + segments = [make_segment(sequence) for sequence in range(start, MAX_SEGMENTS + 1)] assert await resp.text() == make_playlist( sequence=start, discontinuity_sequence=1, diff --git a/tests/components/stream/test_init.py b/tests/components/stream/test_init.py index 8372e5d5e61..1ae6f9e8931 100644 --- a/tests/components/stream/test_init.py +++ b/tests/components/stream/test_init.py @@ -1,4 +1,5 @@ """Test stream init.""" + import logging import av diff --git a/tests/components/stream/test_ll_hls.py b/tests/components/stream/test_ll_hls.py index cd13ab340c2..4cf3909dd0d 100644 --- a/tests/components/stream/test_ll_hls.py +++ b/tests/components/stream/test_ll_hls.py @@ -1,4 +1,5 @@ """The tests for hls streams.""" + import asyncio from collections import deque from http import HTTPStatus @@ -95,10 +96,10 @@ def make_segment_with_parts( response = [] if discontinuity: response.append("#EXT-X-DISCONTINUITY") - for i in range(num_parts): - response.append( - f'#EXT-X-PART:DURATION={TEST_PART_DURATION:.3f},URI="./segment/{segment}.{i}.m4s"{",INDEPENDENT=YES" if i%independent_period==0 else ""}' - ) + response.extend( + f'#EXT-X-PART:DURATION={TEST_PART_DURATION:.3f},URI="./segment/{segment}.{i}.m4s"{",INDEPENDENT=YES" if i%independent_period==0 else ""}' + for i in range(num_parts) + ) response.extend( [ "#EXT-X-PROGRAM-DATE-TIME:" @@ -396,7 +397,7 @@ async def test_ll_hls_playlist_bad_msn_part( """Test some playlist requests with invalid _HLS_msn/_HLS_part.""" async def _handler_bad_request(request): - raise web.HTTPBadRequest() + raise web.HTTPBadRequest await async_setup_component( hass, diff --git a/tests/components/stream/test_recorder.py b/tests/components/stream/test_recorder.py index b8c27037a25..515f3fff82d 100644 --- a/tests/components/stream/test_recorder.py +++ b/tests/components/stream/test_recorder.py @@ -1,4 +1,5 @@ """The tests for recording streams.""" + import asyncio from datetime import timedelta from io import BytesIO @@ -100,15 +101,16 @@ async def test_record_path_not_allowed(hass: HomeAssistant, h264_video) -> None: """Test where the output path is not allowed by home assistant configuration.""" stream = create_stream(hass, h264_video, {}, dynamic_stream_settings()) - with patch.object( - hass.config, "is_allowed_path", return_value=False - ), pytest.raises(HomeAssistantError): + with ( + patch.object(hass.config, "is_allowed_path", return_value=False), + pytest.raises(HomeAssistantError), + ): await stream.async_record("/example/path") def add_parts_to_segment(segment, source): """Add relevant part data to segment for testing recorder.""" - moof_locs = list(find_box(source.getbuffer(), b"moof")) + [len(source.getbuffer())] + moof_locs = [*find_box(source.getbuffer(), b"moof"), len(source.getbuffer())] segment.init = source.getbuffer()[: moof_locs[0]].tobytes() segment.parts = [ Part( @@ -148,9 +150,11 @@ async def test_recorder_discontinuity( provider_ready.set() return provider - with patch.object(hass.config, "is_allowed_path", return_value=True), patch( - "homeassistant.components.stream.Stream", wraps=MockStream - ), patch("homeassistant.components.stream.recorder.RecorderOutput.recv"): + with ( + patch.object(hass.config, "is_allowed_path", return_value=True), + patch("homeassistant.components.stream.Stream", wraps=MockStream), + patch("homeassistant.components.stream.recorder.RecorderOutput.recv"), + ): stream = create_stream(hass, "blank", {}, dynamic_stream_settings()) make_recording = hass.async_create_task(stream.async_record(filename)) await provider_ready.wait() @@ -236,6 +240,7 @@ async def test_record_stream_audio( # Fire the IdleTimer future = dt_util.utcnow() + timedelta(seconds=30) async_fire_time_changed(hass, future) + await hass.async_block_till_done() await make_recording diff --git a/tests/components/stream/test_worker.py b/tests/components/stream/test_worker.py index bd998b008be..0d47a63a000 100644 --- a/tests/components/stream/test_worker.py +++ b/tests/components/stream/test_worker.py @@ -12,6 +12,7 @@ creates a packet sequence, with a mocked output buffer to capture the segments pushed to the output streams. The packet sequence can be used to exercise failure modes or corner cases like how out of order packets are handled. """ + import asyncio import fractions import io @@ -310,9 +311,12 @@ async def async_decode_stream(hass, packets, py_av=None, stream_settings=None): py_av = MockPyAv() py_av.container.packets = iter(packets) # Can't be rewound - with patch("av.open", new=py_av.open), patch( - "homeassistant.components.stream.core.StreamOutput.put", - side_effect=py_av.capture_buffer.capture_output_segment, + with ( + patch("av.open", new=py_av.open), + patch( + "homeassistant.components.stream.core.StreamOutput.put", + side_effect=py_av.capture_buffer.capture_output_segment, + ), ): try: run_worker(hass, stream, STREAM_SOURCE, stream_settings) @@ -459,7 +463,7 @@ async def test_skip_initial_bad_packets(hass: HomeAssistant) -> None: num_packets = LONGER_TEST_SEQUENCE_LENGTH packets = list(PacketSequence(num_packets)) num_bad_packets = MAX_MISSING_DTS - 1 - for i in range(0, num_bad_packets): + for i in range(num_bad_packets): packets[i].dts = None decoded_stream = await async_decode_stream(hass, packets) @@ -489,7 +493,7 @@ async def test_too_many_initial_bad_packets_fails(hass: HomeAssistant) -> None: num_packets = LONGER_TEST_SEQUENCE_LENGTH packets = list(PacketSequence(num_packets)) num_bad_packets = MAX_MISSING_DTS + 1 - for i in range(0, num_bad_packets): + for i in range(num_bad_packets): packets[i].dts = None py_av = MockPyAv() @@ -985,8 +989,9 @@ async def test_get_image(hass: HomeAssistant, h264_video, filename) -> None: worker_wake.wait() return temp_av_open(stream_source, *args, **kwargs) - with patch.object(hass.config, "is_allowed_path", return_value=True), patch( - "av.open", new=blocking_open + with ( + patch.object(hass.config, "is_allowed_path", return_value=True), + patch("av.open", new=blocking_open), ): make_recording = hass.async_create_task(stream.async_record(filename)) assert stream._keyframe_converter._image is None diff --git a/tests/components/streamlabswater/__init__.py b/tests/components/streamlabswater/__init__.py index a467c9553de..f8776708887 100644 --- a/tests/components/streamlabswater/__init__.py +++ b/tests/components/streamlabswater/__init__.py @@ -1,4 +1,5 @@ """Tests for the StreamLabs integration.""" + from homeassistant.core import HomeAssistant from homeassistant.util.unit_system import IMPERIAL_SYSTEM diff --git a/tests/components/streamlabswater/conftest.py b/tests/components/streamlabswater/conftest.py index 64fbed63520..c303c1b7ef0 100644 --- a/tests/components/streamlabswater/conftest.py +++ b/tests/components/streamlabswater/conftest.py @@ -1,4 +1,5 @@ """Common fixtures for the StreamLabs tests.""" + from collections.abc import Generator from unittest.mock import AsyncMock, patch diff --git a/tests/components/streamlabswater/snapshots/test_binary_sensor.ambr b/tests/components/streamlabswater/snapshots/test_binary_sensor.ambr index 6c117a18d75..c74df76e71b 100644 --- a/tests/components/streamlabswater/snapshots/test_binary_sensor.ambr +++ b/tests/components/streamlabswater/snapshots/test_binary_sensor.ambr @@ -40,6 +40,7 @@ 'context': , 'entity_id': 'binary_sensor.water_monitor_away_mode', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }) diff --git a/tests/components/streamlabswater/snapshots/test_sensor.ambr b/tests/components/streamlabswater/snapshots/test_sensor.ambr index cb11852447c..d54cdcafb93 100644 --- a/tests/components/streamlabswater/snapshots/test_sensor.ambr +++ b/tests/components/streamlabswater/snapshots/test_sensor.ambr @@ -45,6 +45,7 @@ 'context': , 'entity_id': 'sensor.water_monitor_daily_usage', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '200.44691536', }) @@ -95,6 +96,7 @@ 'context': , 'entity_id': 'sensor.water_monitor_monthly_usage', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '420.514099294', }) @@ -145,6 +147,7 @@ 'context': , 'entity_id': 'sensor.water_monitor_yearly_usage', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '65432.389256934', }) diff --git a/tests/components/streamlabswater/test_binary_sensor.py b/tests/components/streamlabswater/test_binary_sensor.py index 4f533d91b55..7c9351c5e69 100644 --- a/tests/components/streamlabswater/test_binary_sensor.py +++ b/tests/components/streamlabswater/test_binary_sensor.py @@ -1,4 +1,5 @@ """Tests for the Streamlabs Water binary sensor platform.""" + from unittest.mock import AsyncMock, patch from syrupy import SnapshotAssertion diff --git a/tests/components/streamlabswater/test_config_flow.py b/tests/components/streamlabswater/test_config_flow.py index 68f671d3b8c..4efe80b31e5 100644 --- a/tests/components/streamlabswater/test_config_flow.py +++ b/tests/components/streamlabswater/test_config_flow.py @@ -1,4 +1,5 @@ """Test the StreamLabs config flow.""" + from unittest.mock import AsyncMock, patch from homeassistant import config_entries diff --git a/tests/components/streamlabswater/test_sensor.py b/tests/components/streamlabswater/test_sensor.py index a78d4129abb..f27b61d724b 100644 --- a/tests/components/streamlabswater/test_sensor.py +++ b/tests/components/streamlabswater/test_sensor.py @@ -1,4 +1,5 @@ """Tests for the Streamlabs Water sensor platform.""" + from unittest.mock import AsyncMock, patch from syrupy import SnapshotAssertion diff --git a/tests/components/stt/common.py b/tests/components/stt/common.py index 79b58531b54..e6c36c5b350 100644 --- a/tests/components/stt/common.py +++ b/tests/components/stt/common.py @@ -1,4 +1,5 @@ """Provide common test tools for STT.""" + from __future__ import annotations from collections.abc import Callable, Coroutine diff --git a/tests/components/stt/test_init.py b/tests/components/stt/test_init.py index 9764451c5d5..a06c635bcfd 100644 --- a/tests/components/stt/test_init.py +++ b/tests/components/stt/test_init.py @@ -1,4 +1,5 @@ """Test STT component setup.""" + from collections.abc import AsyncIterable, Generator from http import HTTPStatus from pathlib import Path @@ -299,7 +300,7 @@ async def test_stream_audio( ) @pytest.mark.parametrize( ("header", "status", "error"), - ( + [ (None, 400, "Missing X-Speech-Content header"), ( ( @@ -330,7 +331,7 @@ async def test_stream_audio( 400, "Missing language in X-Speech-Content header", ), - ), + ], ) async def test_metadata_errors( hass: HomeAssistant, diff --git a/tests/components/stt/test_legacy.py b/tests/components/stt/test_legacy.py index 7176b866b00..04068b012f1 100644 --- a/tests/components/stt/test_legacy.py +++ b/tests/components/stt/test_legacy.py @@ -1,4 +1,5 @@ """Test the legacy stt setup.""" + from __future__ import annotations from pathlib import Path diff --git a/tests/components/subaru/conftest.py b/tests/components/subaru/conftest.py index 4927525d896..446f025e077 100644 --- a/tests/components/subaru/conftest.py +++ b/tests/components/subaru/conftest.py @@ -1,4 +1,5 @@ """Common functions needed to setup tests for Subaru component.""" + from datetime import timedelta from unittest.mock import patch @@ -111,46 +112,61 @@ async def setup_subaru_config_entry( fetch_effect=None, ): """Run async_setup with API mocks in place.""" - with patch( - MOCK_API_CONNECT, - return_value=connect_effect is None, - side_effect=connect_effect, - ), patch( - MOCK_API_GET_VEHICLES, - return_value=vehicle_list, - ), patch( - MOCK_API_VIN_TO_NAME, - return_value=vehicle_data[VEHICLE_NAME], - ), patch( - MOCK_API_GET_API_GEN, - return_value=vehicle_data[VEHICLE_API_GEN], - ), patch( - MOCK_API_GET_MODEL_NAME, - return_value=vehicle_data[VEHICLE_MODEL_NAME], - ), patch( - MOCK_API_GET_MODEL_YEAR, - return_value=vehicle_data[VEHICLE_MODEL_YEAR], - ), patch( - MOCK_API_GET_EV_STATUS, - return_value=vehicle_data[VEHICLE_HAS_EV], - ), patch( - MOCK_API_GET_RES_STATUS, - return_value=vehicle_data[VEHICLE_HAS_REMOTE_START], - ), patch( - MOCK_API_GET_REMOTE_STATUS, - return_value=vehicle_data[VEHICLE_HAS_REMOTE_SERVICE], - ), patch( - MOCK_API_GET_SAFETY_STATUS, - return_value=vehicle_data[VEHICLE_HAS_SAFETY_SERVICE], - ), patch( - MOCK_API_GET_SUBSCRIPTION_STATUS, - return_value=True, - ), patch( - MOCK_API_GET_DATA, - return_value=vehicle_status, - ), patch( - MOCK_API_UPDATE, - ), patch(MOCK_API_FETCH, side_effect=fetch_effect): + with ( + patch( + MOCK_API_CONNECT, + return_value=connect_effect is None, + side_effect=connect_effect, + ), + patch( + MOCK_API_GET_VEHICLES, + return_value=vehicle_list, + ), + patch( + MOCK_API_VIN_TO_NAME, + return_value=vehicle_data[VEHICLE_NAME], + ), + patch( + MOCK_API_GET_API_GEN, + return_value=vehicle_data[VEHICLE_API_GEN], + ), + patch( + MOCK_API_GET_MODEL_NAME, + return_value=vehicle_data[VEHICLE_MODEL_NAME], + ), + patch( + MOCK_API_GET_MODEL_YEAR, + return_value=vehicle_data[VEHICLE_MODEL_YEAR], + ), + patch( + MOCK_API_GET_EV_STATUS, + return_value=vehicle_data[VEHICLE_HAS_EV], + ), + patch( + MOCK_API_GET_RES_STATUS, + return_value=vehicle_data[VEHICLE_HAS_REMOTE_START], + ), + patch( + MOCK_API_GET_REMOTE_STATUS, + return_value=vehicle_data[VEHICLE_HAS_REMOTE_SERVICE], + ), + patch( + MOCK_API_GET_SAFETY_STATUS, + return_value=vehicle_data[VEHICLE_HAS_SAFETY_SERVICE], + ), + patch( + MOCK_API_GET_SUBSCRIPTION_STATUS, + return_value=True, + ), + patch( + MOCK_API_GET_DATA, + return_value=vehicle_status, + ), + patch( + MOCK_API_UPDATE, + ), + patch(MOCK_API_FETCH, side_effect=fetch_effect), + ): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/subaru/test_config_flow.py b/tests/components/subaru/test_config_flow.py index 7e892d2c99a..76bad81bff4 100644 --- a/tests/components/subaru/test_config_flow.py +++ b/tests/components/subaru/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for the Subaru component config flow.""" + from copy import deepcopy from unittest import mock from unittest.mock import PropertyMock, patch @@ -101,15 +102,17 @@ async def test_user_form_pin_not_required( hass: HomeAssistant, two_factor_verify_form ) -> None: """Test successful login when no PIN is required.""" - with patch( - MOCK_API_2FA_VERIFY, - return_value=True, - ) as mock_two_factor_verify, patch( - MOCK_API_IS_PIN_REQUIRED, - return_value=False, - ) as mock_is_pin_required, patch( - ASYNC_SETUP_ENTRY, return_value=True - ) as mock_setup_entry: + with ( + patch( + MOCK_API_2FA_VERIFY, + return_value=True, + ) as mock_two_factor_verify, + patch( + MOCK_API_IS_PIN_REQUIRED, + return_value=False, + ) as mock_is_pin_required, + patch(ASYNC_SETUP_ENTRY, return_value=True) as mock_setup_entry, + ): result = await hass.config_entries.flow.async_configure( two_factor_verify_form["flow_id"], user_input={config_flow.CONF_VALIDATION_CODE: "123456"}, @@ -140,9 +143,13 @@ async def test_user_form_pin_not_required( async def test_registered_pin_required(hass: HomeAssistant, user_form) -> None: """Test if the device is already registered and PIN required.""" - with patch(MOCK_API_CONNECT, return_value=True), patch( - MOCK_API_DEVICE_REGISTERED, new_callable=PropertyMock - ) as mock_device_registered, patch(MOCK_API_IS_PIN_REQUIRED, return_value=True): + with ( + patch(MOCK_API_CONNECT, return_value=True), + patch( + MOCK_API_DEVICE_REGISTERED, new_callable=PropertyMock + ) as mock_device_registered, + patch(MOCK_API_IS_PIN_REQUIRED, return_value=True), + ): mock_device_registered.return_value = True await hass.config_entries.flow.async_configure( user_form["flow_id"], user_input=TEST_CREDS @@ -151,9 +158,13 @@ async def test_registered_pin_required(hass: HomeAssistant, user_form) -> None: async def test_registered_no_pin_required(hass: HomeAssistant, user_form) -> None: """Test if the device is already registered and PIN not required.""" - with patch(MOCK_API_CONNECT, return_value=True), patch( - MOCK_API_DEVICE_REGISTERED, new_callable=PropertyMock - ) as mock_device_registered, patch(MOCK_API_IS_PIN_REQUIRED, return_value=False): + with ( + patch(MOCK_API_CONNECT, return_value=True), + patch( + MOCK_API_DEVICE_REGISTERED, new_callable=PropertyMock + ) as mock_device_registered, + patch(MOCK_API_IS_PIN_REQUIRED, return_value=False), + ): mock_device_registered.return_value = True await hass.config_entries.flow.async_configure( user_form["flow_id"], user_input=TEST_CREDS @@ -164,12 +175,13 @@ async def test_two_factor_request_success( hass: HomeAssistant, two_factor_start_form ) -> None: """Test two factor contact method selection.""" - with patch( - MOCK_API_2FA_REQUEST, - return_value=True, - ) as mock_two_factor_request, patch( - MOCK_API_2FA_CONTACTS, new_callable=PropertyMock - ) as mock_contacts: + with ( + patch( + MOCK_API_2FA_REQUEST, + return_value=True, + ) as mock_two_factor_request, + patch(MOCK_API_2FA_CONTACTS, new_callable=PropertyMock) as mock_contacts, + ): mock_contacts.return_value = MOCK_2FA_CONTACTS await hass.config_entries.flow.async_configure( two_factor_start_form["flow_id"], @@ -182,12 +194,13 @@ async def test_two_factor_request_fail( hass: HomeAssistant, two_factor_start_form ) -> None: """Test two factor auth request failure.""" - with patch( - MOCK_API_2FA_REQUEST, - return_value=False, - ) as mock_two_factor_request, patch( - MOCK_API_2FA_CONTACTS, new_callable=PropertyMock - ) as mock_contacts: + with ( + patch( + MOCK_API_2FA_REQUEST, + return_value=False, + ) as mock_two_factor_request, + patch(MOCK_API_2FA_CONTACTS, new_callable=PropertyMock) as mock_contacts, + ): mock_contacts.return_value = MOCK_2FA_CONTACTS result = await hass.config_entries.flow.async_configure( two_factor_start_form["flow_id"], @@ -202,12 +215,13 @@ async def test_two_factor_verify_success( hass: HomeAssistant, two_factor_verify_form ) -> None: """Test two factor verification.""" - with patch( - MOCK_API_2FA_VERIFY, - return_value=True, - ) as mock_two_factor_verify, patch( - MOCK_API_IS_PIN_REQUIRED, return_value=True - ) as mock_is_in_required: + with ( + patch( + MOCK_API_2FA_VERIFY, + return_value=True, + ) as mock_two_factor_verify, + patch(MOCK_API_IS_PIN_REQUIRED, return_value=True) as mock_is_in_required, + ): await hass.config_entries.flow.async_configure( two_factor_verify_form["flow_id"], user_input={config_flow.CONF_VALIDATION_CODE: "123456"}, @@ -220,12 +234,13 @@ async def test_two_factor_verify_bad_format( hass: HomeAssistant, two_factor_verify_form ) -> None: """Test two factor verification bad format.""" - with patch( - MOCK_API_2FA_VERIFY, - return_value=False, - ) as mock_two_factor_verify, patch( - MOCK_API_IS_PIN_REQUIRED, return_value=True - ) as mock_is_pin_required: + with ( + patch( + MOCK_API_2FA_VERIFY, + return_value=False, + ) as mock_two_factor_verify, + patch(MOCK_API_IS_PIN_REQUIRED, return_value=True) as mock_is_pin_required, + ): result = await hass.config_entries.flow.async_configure( two_factor_verify_form["flow_id"], user_input={config_flow.CONF_VALIDATION_CODE: "1234567"}, @@ -239,12 +254,13 @@ async def test_two_factor_verify_fail( hass: HomeAssistant, two_factor_verify_form ) -> None: """Test two factor verification failure.""" - with patch( - MOCK_API_2FA_VERIFY, - return_value=False, - ) as mock_two_factor_verify, patch( - MOCK_API_IS_PIN_REQUIRED, return_value=True - ) as mock_is_pin_required: + with ( + patch( + MOCK_API_2FA_VERIFY, + return_value=False, + ) as mock_two_factor_verify, + patch(MOCK_API_IS_PIN_REQUIRED, return_value=True) as mock_is_pin_required, + ): result = await hass.config_entries.flow.async_configure( two_factor_verify_form["flow_id"], user_input={config_flow.CONF_VALIDATION_CODE: "123456"}, @@ -272,12 +288,15 @@ async def test_pin_form_init(pin_form) -> None: async def test_pin_form_bad_pin_format(hass: HomeAssistant, pin_form) -> None: """Test we handle invalid pin.""" - with patch( - MOCK_API_TEST_PIN, - ) as mock_test_pin, patch( - MOCK_API_UPDATE_SAVED_PIN, - return_value=True, - ) as mock_update_saved_pin: + with ( + patch( + MOCK_API_TEST_PIN, + ) as mock_test_pin, + patch( + MOCK_API_UPDATE_SAVED_PIN, + return_value=True, + ) as mock_update_saved_pin, + ): result = await hass.config_entries.flow.async_configure( pin_form["flow_id"], user_input={CONF_PIN: "abcd"} ) @@ -289,15 +308,17 @@ async def test_pin_form_bad_pin_format(hass: HomeAssistant, pin_form) -> None: async def test_pin_form_success(hass: HomeAssistant, pin_form) -> None: """Test successful PIN entry.""" - with patch( - MOCK_API_TEST_PIN, - return_value=True, - ) as mock_test_pin, patch( - MOCK_API_UPDATE_SAVED_PIN, - return_value=True, - ) as mock_update_saved_pin, patch( - ASYNC_SETUP_ENTRY, return_value=True - ) as mock_setup_entry: + with ( + patch( + MOCK_API_TEST_PIN, + return_value=True, + ) as mock_test_pin, + patch( + MOCK_API_UPDATE_SAVED_PIN, + return_value=True, + ) as mock_update_saved_pin, + patch(ASYNC_SETUP_ENTRY, return_value=True) as mock_setup_entry, + ): result = await hass.config_entries.flow.async_configure( pin_form["flow_id"], user_input={CONF_PIN: TEST_PIN} ) @@ -325,13 +346,16 @@ async def test_pin_form_success(hass: HomeAssistant, pin_form) -> None: async def test_pin_form_incorrect_pin(hass: HomeAssistant, pin_form) -> None: """Test we handle invalid pin.""" - with patch( - MOCK_API_TEST_PIN, - side_effect=InvalidPIN("invalidPin"), - ) as mock_test_pin, patch( - MOCK_API_UPDATE_SAVED_PIN, - return_value=True, - ) as mock_update_saved_pin: + with ( + patch( + MOCK_API_TEST_PIN, + side_effect=InvalidPIN("invalidPin"), + ) as mock_test_pin, + patch( + MOCK_API_UPDATE_SAVED_PIN, + return_value=True, + ) as mock_update_saved_pin, + ): result = await hass.config_entries.flow.async_configure( pin_form["flow_id"], user_input={CONF_PIN: TEST_PIN} ) @@ -374,9 +398,10 @@ async def user_form(hass): @pytest.fixture async def two_factor_start_form(hass, user_form): """Return two factor form for Subaru config flow.""" - with patch(MOCK_API_CONNECT, return_value=True), patch( - MOCK_API_2FA_CONTACTS, new_callable=PropertyMock - ) as mock_contacts: + with ( + patch(MOCK_API_CONNECT, return_value=True), + patch(MOCK_API_2FA_CONTACTS, new_callable=PropertyMock) as mock_contacts, + ): mock_contacts.return_value = MOCK_2FA_CONTACTS return await hass.config_entries.flow.async_configure( user_form["flow_id"], user_input=TEST_CREDS @@ -386,10 +411,13 @@ async def two_factor_start_form(hass, user_form): @pytest.fixture async def two_factor_verify_form(hass, two_factor_start_form): """Return two factor form for Subaru config flow.""" - with patch( - MOCK_API_2FA_REQUEST, - return_value=True, - ), patch(MOCK_API_2FA_CONTACTS, new_callable=PropertyMock) as mock_contacts: + with ( + patch( + MOCK_API_2FA_REQUEST, + return_value=True, + ), + patch(MOCK_API_2FA_CONTACTS, new_callable=PropertyMock) as mock_contacts, + ): mock_contacts.return_value = MOCK_2FA_CONTACTS return await hass.config_entries.flow.async_configure( two_factor_start_form["flow_id"], @@ -400,10 +428,13 @@ async def two_factor_verify_form(hass, two_factor_start_form): @pytest.fixture async def pin_form(hass, two_factor_verify_form): """Return PIN input form for Subaru config flow.""" - with patch( - MOCK_API_2FA_VERIFY, - return_value=True, - ), patch(MOCK_API_IS_PIN_REQUIRED, return_value=True): + with ( + patch( + MOCK_API_2FA_VERIFY, + return_value=True, + ), + patch(MOCK_API_IS_PIN_REQUIRED, return_value=True), + ): return await hass.config_entries.flow.async_configure( two_factor_verify_form["flow_id"], user_input={config_flow.CONF_VALIDATION_CODE: "123456"}, diff --git a/tests/components/subaru/test_device_tracker.py b/tests/components/subaru/test_device_tracker.py index 616d868016e..b8a970007ab 100644 --- a/tests/components/subaru/test_device_tracker.py +++ b/tests/components/subaru/test_device_tracker.py @@ -1,4 +1,5 @@ """Test Subaru device tracker.""" + from copy import deepcopy from unittest.mock import patch diff --git a/tests/components/subaru/test_diagnostics.py b/tests/components/subaru/test_diagnostics.py index 7da53c66c46..9445f1ca235 100644 --- a/tests/components/subaru/test_diagnostics.py +++ b/tests/components/subaru/test_diagnostics.py @@ -1,4 +1,5 @@ """Test Subaru diagnostics.""" + import json from unittest.mock import patch diff --git a/tests/components/subaru/test_init.py b/tests/components/subaru/test_init.py index e82d7a1d72c..e25a8681bef 100644 --- a/tests/components/subaru/test_init.py +++ b/tests/components/subaru/test_init.py @@ -1,4 +1,5 @@ """Test Subaru component setup and updates.""" + from unittest.mock import patch from subarulink import InvalidCredentials, SubaruException @@ -125,12 +126,15 @@ async def test_update_skip_unsubscribed( async def test_update_disabled(hass: HomeAssistant, ev_entry) -> None: """Test update function disable option.""" - with patch( - MOCK_API_FETCH, - side_effect=SubaruException("403 Error"), - ), patch( - MOCK_API_UPDATE, - ) as mock_update: + with ( + patch( + MOCK_API_FETCH, + side_effect=SubaruException("403 Error"), + ), + patch( + MOCK_API_UPDATE, + ) as mock_update, + ): await hass.services.async_call( HA_DOMAIN, SERVICE_UPDATE_ENTITY, diff --git a/tests/components/subaru/test_lock.py b/tests/components/subaru/test_lock.py index 7dab83a4c06..4d19d49579e 100644 --- a/tests/components/subaru/test_lock.py +++ b/tests/components/subaru/test_lock.py @@ -1,4 +1,5 @@ """Test Subaru locks.""" + from unittest.mock import patch import pytest @@ -52,8 +53,9 @@ async def test_unlock_cmd(hass: HomeAssistant, ev_entry) -> None: async def test_lock_cmd_fails(hass: HomeAssistant, ev_entry) -> None: """Test subaru lock request that initiates but fails.""" - with patch(MOCK_API_LOCK, return_value=False) as mock_lock, pytest.raises( - HomeAssistantError + with ( + patch(MOCK_API_LOCK, return_value=False) as mock_lock, + pytest.raises(HomeAssistantError), ): await hass.services.async_call( LOCK_DOMAIN, SERVICE_UNLOCK, {ATTR_ENTITY_ID: DEVICE_ID}, blocking=True diff --git a/tests/components/subaru/test_sensor.py b/tests/components/subaru/test_sensor.py index fd03ed3044b..de1df044d71 100644 --- a/tests/components/subaru/test_sensor.py +++ b/tests/components/subaru/test_sensor.py @@ -1,4 +1,5 @@ """Test Subaru sensors.""" + from typing import Any from unittest.mock import patch @@ -34,8 +35,9 @@ async def test_sensors_ev_imperial(hass: HomeAssistant, ev_entry) -> None: """Test sensors supporting imperial units.""" hass.config.units = US_CUSTOMARY_SYSTEM - with patch(MOCK_API_FETCH), patch( - MOCK_API_GET_DATA, return_value=VEHICLE_STATUS_EV + with ( + patch(MOCK_API_FETCH), + patch(MOCK_API_GET_DATA, return_value=VEHICLE_STATUS_EV), ): advance_time_to_next_fetch(hass) await hass.async_block_till_done() diff --git a/tests/components/suez_water/conftest.py b/tests/components/suez_water/conftest.py index 8a67cfe97d7..6c124bec30e 100644 --- a/tests/components/suez_water/conftest.py +++ b/tests/components/suez_water/conftest.py @@ -1,4 +1,5 @@ """Common fixtures for the Suez Water tests.""" + from collections.abc import Generator from unittest.mock import AsyncMock, patch diff --git a/tests/components/suez_water/test_config_flow.py b/tests/components/suez_water/test_config_flow.py index c18b8a927e9..a4ab52151d1 100644 --- a/tests/components/suez_water/test_config_flow.py +++ b/tests/components/suez_water/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Suez Water config flow.""" + from unittest.mock import AsyncMock, patch from pysuez.client import PySuezError @@ -48,12 +49,15 @@ async def test_form_invalid_auth( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch( - "homeassistant.components.suez_water.config_flow.SuezClient.__init__", - return_value=None, - ), patch( - "homeassistant.components.suez_water.config_flow.SuezClient.check_credentials", - return_value=False, + with ( + patch( + "homeassistant.components.suez_water.config_flow.SuezClient.__init__", + return_value=None, + ), + patch( + "homeassistant.components.suez_water.config_flow.SuezClient.check_credentials", + return_value=False, + ), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -178,12 +182,15 @@ async def test_import_error( async def test_importing_invalid_auth(hass: HomeAssistant) -> None: """Test we handle invalid auth when importing.""" - with patch( - "homeassistant.components.suez_water.config_flow.SuezClient.__init__", - return_value=None, - ), patch( - "homeassistant.components.suez_water.config_flow.SuezClient.check_credentials", - return_value=False, + with ( + patch( + "homeassistant.components.suez_water.config_flow.SuezClient.__init__", + return_value=None, + ), + patch( + "homeassistant.components.suez_water.config_flow.SuezClient.check_credentials", + return_value=False, + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=MOCK_DATA diff --git a/tests/components/sun/test_config_flow.py b/tests/components/sun/test_config_flow.py index 2bf577f82b8..ef13595ed59 100644 --- a/tests/components/sun/test_config_flow.py +++ b/tests/components/sun/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for the Sun config flow.""" + from unittest.mock import patch import pytest diff --git a/tests/components/sun/test_init.py b/tests/components/sun/test_init.py index fef9bd4e049..48a214274c9 100644 --- a/tests/components/sun/test_init.py +++ b/tests/components/sun/test_init.py @@ -1,4 +1,5 @@ """The tests for the Sun component.""" + from datetime import datetime, timedelta from unittest.mock import patch @@ -6,6 +7,7 @@ from freezegun import freeze_time import pytest from homeassistant.components import sun +from homeassistant.components.sun import entity from homeassistant.const import EVENT_STATE_CHANGED from homeassistant.core import HomeAssistant, callback from homeassistant.setup import async_setup_component @@ -21,7 +23,7 @@ async def test_setting_rising(hass: HomeAssistant) -> None: await async_setup_component(hass, sun.DOMAIN, {sun.DOMAIN: {}}) await hass.async_block_till_done() - state = hass.states.get(sun.ENTITY_ID) + state = hass.states.get(entity.ENTITY_ID) from astral import LocationInfo import astral.sun @@ -87,22 +89,22 @@ async def test_setting_rising(hass: HomeAssistant) -> None: mod += 1 assert next_dawn == dt_util.parse_datetime( - state.attributes[sun.STATE_ATTR_NEXT_DAWN] + state.attributes[entity.STATE_ATTR_NEXT_DAWN] ) assert next_dusk == dt_util.parse_datetime( - state.attributes[sun.STATE_ATTR_NEXT_DUSK] + state.attributes[entity.STATE_ATTR_NEXT_DUSK] ) assert next_midnight == dt_util.parse_datetime( - state.attributes[sun.STATE_ATTR_NEXT_MIDNIGHT] + state.attributes[entity.STATE_ATTR_NEXT_MIDNIGHT] ) assert next_noon == dt_util.parse_datetime( - state.attributes[sun.STATE_ATTR_NEXT_NOON] + state.attributes[entity.STATE_ATTR_NEXT_NOON] ) assert next_rising == dt_util.parse_datetime( - state.attributes[sun.STATE_ATTR_NEXT_RISING] + state.attributes[entity.STATE_ATTR_NEXT_RISING] ) assert next_setting == dt_util.parse_datetime( - state.attributes[sun.STATE_ATTR_NEXT_SETTING] + state.attributes[entity.STATE_ATTR_NEXT_SETTING] ) @@ -117,29 +119,29 @@ async def test_state_change( await hass.async_block_till_done() test_time = dt_util.parse_datetime( - hass.states.get(sun.ENTITY_ID).attributes[sun.STATE_ATTR_NEXT_RISING] + hass.states.get(entity.ENTITY_ID).attributes[entity.STATE_ATTR_NEXT_RISING] ) assert test_time is not None - assert hass.states.get(sun.ENTITY_ID).state == sun.STATE_BELOW_HORIZON + assert hass.states.get(entity.ENTITY_ID).state == sun.STATE_BELOW_HORIZON patched_time = test_time + timedelta(seconds=5) with freeze_time(patched_time): async_fire_time_changed(hass, patched_time) await hass.async_block_till_done() - assert hass.states.get(sun.ENTITY_ID).state == sun.STATE_ABOVE_HORIZON + assert hass.states.get(entity.ENTITY_ID).state == sun.STATE_ABOVE_HORIZON # Update core configuration with patch("homeassistant.helpers.condition.dt_util.utcnow", return_value=now): await hass.config.async_update(longitude=hass.config.longitude + 90) await hass.async_block_till_done() - assert hass.states.get(sun.ENTITY_ID).state == sun.STATE_ABOVE_HORIZON + assert hass.states.get(entity.ENTITY_ID).state == sun.STATE_ABOVE_HORIZON # Test listeners are not duplicated after a core configuration change test_time = dt_util.parse_datetime( - hass.states.get(sun.ENTITY_ID).attributes[sun.STATE_ATTR_NEXT_DUSK] + hass.states.get(entity.ENTITY_ID).attributes[entity.STATE_ATTR_NEXT_DUSK] ) assert test_time is not None @@ -154,7 +156,7 @@ async def test_state_change( # Called once by time listener, once from Sun.update_events assert caplog.text.count("sun position_update") == 2 - assert hass.states.get(sun.ENTITY_ID).state == sun.STATE_BELOW_HORIZON + assert hass.states.get(entity.ENTITY_ID).state == sun.STATE_BELOW_HORIZON async def test_norway_in_june(hass: HomeAssistant) -> None: @@ -167,14 +169,14 @@ async def test_norway_in_june(hass: HomeAssistant) -> None: with patch("homeassistant.helpers.condition.dt_util.utcnow", return_value=june): assert await async_setup_component(hass, sun.DOMAIN, {sun.DOMAIN: {}}) - state = hass.states.get(sun.ENTITY_ID) + state = hass.states.get(entity.ENTITY_ID) assert state is not None assert dt_util.parse_datetime( - state.attributes[sun.STATE_ATTR_NEXT_RISING] + state.attributes[entity.STATE_ATTR_NEXT_RISING] ) == datetime(2016, 7, 24, 22, 59, 45, 689645, tzinfo=dt_util.UTC) assert dt_util.parse_datetime( - state.attributes[sun.STATE_ATTR_NEXT_SETTING] + state.attributes[entity.STATE_ATTR_NEXT_SETTING] ) == datetime(2016, 7, 25, 22, 17, 13, 503932, tzinfo=dt_util.UTC) assert state.state == sun.STATE_ABOVE_HORIZON @@ -222,25 +224,25 @@ async def test_setup_and_remove_config_entry(hass: HomeAssistant) -> None: await hass.async_block_till_done() # Check the platform is setup correctly - state = hass.states.get("sun.sun") + state = hass.states.get(entity.ENTITY_ID) assert state is not None test_time = dt_util.parse_datetime( - hass.states.get(sun.ENTITY_ID).attributes[sun.STATE_ATTR_NEXT_RISING] + hass.states.get(entity.ENTITY_ID).attributes[entity.STATE_ATTR_NEXT_RISING] ) assert test_time is not None - assert hass.states.get(sun.ENTITY_ID).state == sun.STATE_BELOW_HORIZON + assert hass.states.get(entity.ENTITY_ID).state == sun.STATE_BELOW_HORIZON # Remove the config entry assert await hass.config_entries.async_remove(config_entry.entry_id) await hass.async_block_till_done() # Check the state is removed, and does not reappear - assert hass.states.get("sun.sun") is None + assert hass.states.get(entity.ENTITY_ID) is None patched_time = test_time + timedelta(seconds=5) with freeze_time(patched_time): async_fire_time_changed(hass, patched_time) await hass.async_block_till_done() - assert hass.states.get("sun.sun") is None + assert hass.states.get(entity.ENTITY_ID) is None diff --git a/tests/components/sun/test_recorder.py b/tests/components/sun/test_recorder.py index e24f404a34b..15c15552c40 100644 --- a/tests/components/sun/test_recorder.py +++ b/tests/components/sun/test_recorder.py @@ -1,12 +1,13 @@ """The tests for sun recorder.""" + from __future__ import annotations from datetime import timedelta from homeassistant.components.recorder import Recorder from homeassistant.components.recorder.history import get_significant_states -from homeassistant.components.sun import ( - DOMAIN, +from homeassistant.components.sun import DOMAIN +from homeassistant.components.sun.entity import ( STATE_ATTR_AZIMUTH, STATE_ATTR_ELEVATION, STATE_ATTR_NEXT_DAWN, diff --git a/tests/components/sun/test_sensor.py b/tests/components/sun/test_sensor.py index b1fb0d2facd..13de0dffbdd 100644 --- a/tests/components/sun/test_sensor.py +++ b/tests/components/sun/test_sensor.py @@ -1,4 +1,5 @@ """The tests for the Sun sensor platform.""" + from datetime import datetime, timedelta from astral import LocationInfo diff --git a/tests/components/sun/test_trigger.py b/tests/components/sun/test_trigger.py index 9d8f5d82a51..f7bdb5eb17b 100644 --- a/tests/components/sun/test_trigger.py +++ b/tests/components/sun/test_trigger.py @@ -1,4 +1,5 @@ """The tests for the sun automation.""" + from datetime import datetime from freezegun import freeze_time diff --git a/tests/components/sunweg/conftest.py b/tests/components/sunweg/conftest.py index 68c4cab86c5..db94b9cc5c8 100644 --- a/tests/components/sunweg/conftest.py +++ b/tests/components/sunweg/conftest.py @@ -68,3 +68,23 @@ def plant_fixture(inverter_fixture) -> Plant: ) plant.inverters.append(inverter_fixture) return plant + + +@pytest.fixture +def plant_fixture_alternative(inverter_fixture) -> Plant: + """Define Plant fixture.""" + plant = Plant( + 123456, + "Plant #123", + 29.5, + 0.5, + 0, + 12.786912, + 24.0, + "kWh", + 332.2, + 0.012296, + None, + ) + plant.inverters.append(inverter_fixture) + return plant diff --git a/tests/components/sunweg/test_config_flow.py b/tests/components/sunweg/test_config_flow.py index 1298d7e93fb..84957a419dd 100644 --- a/tests/components/sunweg/test_config_flow.py +++ b/tests/components/sunweg/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for the Sun WEG server config flow.""" + from unittest.mock import patch from sunweg.api import APIHelper @@ -45,8 +46,9 @@ async def test_no_plants_on_account(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch.object(APIHelper, "authenticate", return_value=True), patch.object( - APIHelper, "listPlants", return_value=[] + with ( + patch.object(APIHelper, "authenticate", return_value=True), + patch.object(APIHelper, "listPlants", return_value=[]), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], SUNWEG_USER_INPUT @@ -64,8 +66,9 @@ async def test_multiple_plant_ids(hass: HomeAssistant, plant_fixture) -> None: user_input = SUNWEG_USER_INPUT.copy() plant_list = [plant_fixture, plant_fixture] - with patch.object(APIHelper, "authenticate", return_value=True), patch.object( - APIHelper, "listPlants", return_value=plant_list + with ( + patch.object(APIHelper, "authenticate", return_value=True), + patch.object(APIHelper, "listPlants", return_value=plant_list), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input @@ -92,10 +95,13 @@ async def test_one_plant_on_account(hass: HomeAssistant, plant_fixture) -> None: ) user_input = SUNWEG_USER_INPUT.copy() - with patch.object(APIHelper, "authenticate", return_value=True), patch.object( - APIHelper, - "listPlants", - return_value=[plant_fixture], + with ( + patch.object(APIHelper, "authenticate", return_value=True), + patch.object( + APIHelper, + "listPlants", + return_value=[plant_fixture], + ), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input @@ -116,10 +122,13 @@ async def test_existing_plant_configured(hass: HomeAssistant, plant_fixture) -> ) user_input = SUNWEG_USER_INPUT.copy() - with patch.object(APIHelper, "authenticate", return_value=True), patch.object( - APIHelper, - "listPlants", - return_value=[plant_fixture], + with ( + patch.object(APIHelper, "authenticate", return_value=True), + patch.object( + APIHelper, + "listPlants", + return_value=[plant_fixture], + ), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input diff --git a/tests/components/sunweg/test_init.py b/tests/components/sunweg/test_init.py index 0295e778f9c..cc2e880d82e 100644 --- a/tests/components/sunweg/test_init.py +++ b/tests/components/sunweg/test_init.py @@ -21,11 +21,13 @@ async def test_methods(hass: HomeAssistant, plant_fixture, inverter_fixture) -> mock_entry = SUNWEG_MOCK_ENTRY mock_entry.add_to_hass(hass) - with patch.object(APIHelper, "authenticate", return_value=True), patch.object( - APIHelper, "listPlants", return_value=[plant_fixture] - ), patch.object(APIHelper, "plant", return_value=plant_fixture), patch.object( - APIHelper, "inverter", return_value=inverter_fixture - ), patch.object(APIHelper, "complete_inverter"): + with ( + patch.object(APIHelper, "authenticate", return_value=True), + patch.object(APIHelper, "listPlants", return_value=[plant_fixture]), + patch.object(APIHelper, "plant", return_value=plant_fixture), + patch.object(APIHelper, "inverter", return_value=inverter_fixture), + patch.object(APIHelper, "complete_inverter"), + ): assert await async_setup_component(hass, DOMAIN, mock_entry.data) await hass.async_block_till_done() assert await hass.config_entries.async_unload(mock_entry.entry_id) @@ -76,6 +78,22 @@ async def test_sunwegdata_update_success(plant_fixture) -> None: assert len(data.data.inverters) == 1 +async def test_sunwegdata_update_success_alternative(plant_fixture_alternative) -> None: + """Test SunWEGData success on update.""" + api = MagicMock() + api.plant = MagicMock(return_value=plant_fixture_alternative) + api.complete_inverter = MagicMock() + data = SunWEGData(api, 0) + data.update() + assert data.data.id == plant_fixture_alternative.id + assert data.data.name == plant_fixture_alternative.name + assert data.data.kwh_per_kwp == plant_fixture_alternative.kwh_per_kwp + assert data.data.last_update == plant_fixture_alternative.last_update + assert data.data.performance_rate == plant_fixture_alternative.performance_rate + assert data.data.saving == plant_fixture_alternative.saving + assert len(data.data.inverters) == 1 + + async def test_sunwegdata_get_api_value_none(plant_fixture) -> None: """Test SunWEGData none return on get_api_value.""" api = MagicMock() diff --git a/tests/components/surepetcare/conftest.py b/tests/components/surepetcare/conftest.py index 79c1b88d99b..9ae1bfe310a 100644 --- a/tests/components/surepetcare/conftest.py +++ b/tests/components/surepetcare/conftest.py @@ -1,4 +1,5 @@ """Define fixtures available for all tests.""" + from unittest.mock import patch import pytest diff --git a/tests/components/surepetcare/test_binary_sensor.py b/tests/components/surepetcare/test_binary_sensor.py index 9f4018b4b65..106cf2f9155 100644 --- a/tests/components/surepetcare/test_binary_sensor.py +++ b/tests/components/surepetcare/test_binary_sensor.py @@ -1,4 +1,5 @@ """The tests for the Sure Petcare binary sensor platform.""" + from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er diff --git a/tests/components/surepetcare/test_config_flow.py b/tests/components/surepetcare/test_config_flow.py index e3521ef3d25..67ee5d81247 100644 --- a/tests/components/surepetcare/test_config_flow.py +++ b/tests/components/surepetcare/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Sure Petcare config flow.""" + from unittest.mock import NonCallableMagicMock, patch from surepy.exceptions import SurePetcareAuthenticationError, SurePetcareError diff --git a/tests/components/surepetcare/test_lock.py b/tests/components/surepetcare/test_lock.py index 14a6a361793..d4275e8385c 100644 --- a/tests/components/surepetcare/test_lock.py +++ b/tests/components/surepetcare/test_lock.py @@ -1,4 +1,5 @@ """The tests for the Sure Petcare lock platform.""" + import pytest from surepy.exceptions import SurePetcareError diff --git a/tests/components/surepetcare/test_sensor.py b/tests/components/surepetcare/test_sensor.py index c0491908ca0..f543cdb9d35 100644 --- a/tests/components/surepetcare/test_sensor.py +++ b/tests/components/surepetcare/test_sensor.py @@ -1,4 +1,5 @@ """Test the surepetcare sensor platform.""" + from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er diff --git a/tests/components/swiss_public_transport/conftest.py b/tests/components/swiss_public_transport/conftest.py index d84446db086..d01fba0f9d0 100644 --- a/tests/components/swiss_public_transport/conftest.py +++ b/tests/components/swiss_public_transport/conftest.py @@ -1,4 +1,5 @@ """Common fixtures for the swiss_public_transport tests.""" + from collections.abc import Generator from unittest.mock import AsyncMock, patch diff --git a/tests/components/swiss_public_transport/test_config_flow.py b/tests/components/swiss_public_transport/test_config_flow.py index 5870f6f0555..9400423ff98 100644 --- a/tests/components/swiss_public_transport/test_config_flow.py +++ b/tests/components/swiss_public_transport/test_config_flow.py @@ -1,4 +1,5 @@ """Test the swiss_public_transport config flow.""" + from unittest.mock import AsyncMock, patch from opendata_transport.exceptions import ( diff --git a/tests/components/swiss_public_transport/test_init.py b/tests/components/swiss_public_transport/test_init.py index 2c8e12e04bf..e1b27cf5fe1 100644 --- a/tests/components/swiss_public_transport/test_init.py +++ b/tests/components/swiss_public_transport/test_init.py @@ -1,4 +1,5 @@ """Test the swiss_public_transport config flow.""" + from unittest.mock import AsyncMock, patch from homeassistant.components.swiss_public_transport.const import ( diff --git a/tests/components/switch/common.py b/tests/components/switch/common.py index 1123b1de6c1..60c79fdf6a8 100644 --- a/tests/components/switch/common.py +++ b/tests/components/switch/common.py @@ -3,6 +3,7 @@ All containing methods are legacy helpers that should not be used by new components. Instead call the service directly. """ + from homeassistant.components.switch import DOMAIN from homeassistant.const import ( ATTR_ENTITY_ID, diff --git a/tests/components/switch/conftest.py b/tests/components/switch/conftest.py index 11f1563b723..c526ef4c4fe 100644 --- a/tests/components/switch/conftest.py +++ b/tests/components/switch/conftest.py @@ -1,2 +1,3 @@ """switch conftest.""" + from tests.components.light.conftest import mock_light_profiles # noqa: F401 diff --git a/tests/components/switch/test_device_action.py b/tests/components/switch/test_device_action.py index e86c32c1e32..c35f7261afc 100644 --- a/tests/components/switch/test_device_action.py +++ b/tests/components/switch/test_device_action.py @@ -1,4 +1,5 @@ """The test for switch device automation.""" + import pytest from pytest_unordered import unordered @@ -63,12 +64,12 @@ async def test_get_actions( @pytest.mark.parametrize( ("hidden_by", "entity_category"), - ( + [ (RegistryEntryHider.INTEGRATION, None), (RegistryEntryHider.USER, None), (None, EntityCategory.CONFIG), (None, EntityCategory.DIAGNOSTIC), - ), + ], ) async def test_get_actions_hidden_auxiliary( hass: HomeAssistant, diff --git a/tests/components/switch/test_device_condition.py b/tests/components/switch/test_device_condition.py index c9521930a73..d69d8a547aa 100644 --- a/tests/components/switch/test_device_condition.py +++ b/tests/components/switch/test_device_condition.py @@ -1,4 +1,5 @@ """The test for switch device automation.""" + from datetime import timedelta from freezegun import freeze_time @@ -68,12 +69,12 @@ async def test_get_conditions( @pytest.mark.parametrize( ("hidden_by", "entity_category"), - ( + [ (RegistryEntryHider.INTEGRATION, None), (RegistryEntryHider.USER, None), (None, EntityCategory.CONFIG), (None, EntityCategory.DIAGNOSTIC), - ), + ], ) async def test_get_conditions_hidden_auxiliary( hass: HomeAssistant, diff --git a/tests/components/switch/test_device_trigger.py b/tests/components/switch/test_device_trigger.py index 03f7e8fbb8e..874210a32bc 100644 --- a/tests/components/switch/test_device_trigger.py +++ b/tests/components/switch/test_device_trigger.py @@ -1,4 +1,5 @@ """The test for switch device automation.""" + from datetime import timedelta import pytest @@ -68,12 +69,12 @@ async def test_get_triggers( @pytest.mark.parametrize( ("hidden_by", "entity_category"), - ( + [ (RegistryEntryHider.INTEGRATION, None), (RegistryEntryHider.USER, None), (None, EntityCategory.CONFIG), (None, EntityCategory.DIAGNOSTIC), - ), + ], ) async def test_get_triggers_hidden_auxiliary( hass: HomeAssistant, diff --git a/tests/components/switch/test_init.py b/tests/components/switch/test_init.py index deb7acb512a..62801346744 100644 --- a/tests/components/switch/test_init.py +++ b/tests/components/switch/test_init.py @@ -1,4 +1,5 @@ """The tests for the Switch component.""" + import pytest from homeassistant import core diff --git a/tests/components/switch/test_light.py b/tests/components/switch/test_light.py index 2254abc08f9..e7681575ce4 100644 --- a/tests/components/switch/test_light.py +++ b/tests/components/switch/test_light.py @@ -1,4 +1,5 @@ """The tests for the Light Switch platform.""" + import pytest from homeassistant.components.light import ( diff --git a/tests/components/switch/test_reproduce_state.py b/tests/components/switch/test_reproduce_state.py index 764efb17036..27bc492494c 100644 --- a/tests/components/switch/test_reproduce_state.py +++ b/tests/components/switch/test_reproduce_state.py @@ -1,4 +1,5 @@ """Test reproduce state for Switch.""" + import pytest from homeassistant.core import HomeAssistant, State diff --git a/tests/components/switch/test_significant_change.py b/tests/components/switch/test_significant_change.py index 296bb24cd30..76f22922932 100644 --- a/tests/components/switch/test_significant_change.py +++ b/tests/components/switch/test_significant_change.py @@ -1,4 +1,5 @@ """Test the sensor significant change platform.""" + from homeassistant.components.switch.significant_change import ( async_check_significant_change, ) diff --git a/tests/components/switch_as_x/conftest.py b/tests/components/switch_as_x/conftest.py index d324f7a0c54..82827924070 100644 --- a/tests/components/switch_as_x/conftest.py +++ b/tests/components/switch_as_x/conftest.py @@ -1,4 +1,5 @@ """Fixtures for the Switch as X integration tests.""" + from __future__ import annotations from collections.abc import Generator diff --git a/tests/components/switch_as_x/test_config_flow.py b/tests/components/switch_as_x/test_config_flow.py index 09661b0619c..59b7d7fadcd 100644 --- a/tests/components/switch_as_x/test_config_flow.py +++ b/tests/components/switch_as_x/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Switch as X config flow.""" + from __future__ import annotations from unittest.mock import AsyncMock @@ -66,10 +67,10 @@ async def test_config_flow( @pytest.mark.parametrize( ("hidden_by_before", "hidden_by_after"), - ( + [ (er.RegistryEntryHider.USER, er.RegistryEntryHider.USER), (None, er.RegistryEntryHider.INTEGRATION), - ), + ], ) @pytest.mark.parametrize("target_domain", PLATFORMS_TO_TEST) async def test_config_flow_registered_entity( diff --git a/tests/components/switch_as_x/test_fan.py b/tests/components/switch_as_x/test_fan.py index c459831b3ad..fd4296bd616 100644 --- a/tests/components/switch_as_x/test_fan.py +++ b/tests/components/switch_as_x/test_fan.py @@ -1,4 +1,5 @@ """Tests for the Switch as X Fan platform.""" + from homeassistant.components.fan import DOMAIN as FAN_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.components.switch_as_x.config_flow import SwitchAsXConfigFlowHandler diff --git a/tests/components/switch_as_x/test_init.py b/tests/components/switch_as_x/test_init.py index 2b0a67f3984..c74b14cc91c 100644 --- a/tests/components/switch_as_x/test_init.py +++ b/tests/components/switch_as_x/test_init.py @@ -1,4 +1,5 @@ """Tests for the Switch as X.""" + from __future__ import annotations from unittest.mock import patch @@ -69,14 +70,14 @@ async def test_config_entry_unregistered_uuid( @pytest.mark.parametrize( ("target_domain", "state_on", "state_off"), - ( + [ (Platform.COVER, STATE_OPEN, STATE_CLOSED), (Platform.FAN, STATE_ON, STATE_OFF), (Platform.LIGHT, STATE_ON, STATE_OFF), (Platform.LOCK, STATE_UNLOCKED, STATE_LOCKED), (Platform.SIREN, STATE_ON, STATE_OFF), (Platform.VALVE, STATE_OPEN, STATE_CLOSED), - ), + ], ) async def test_entity_registry_events( hass: HomeAssistant, target_domain: str, state_on: str, state_off: str @@ -406,10 +407,10 @@ async def test_setup_and_remove_config_entry( @pytest.mark.parametrize( ("hidden_by_before", "hidden_by_after"), - ( + [ (er.RegistryEntryHider.USER, er.RegistryEntryHider.USER), (er.RegistryEntryHider.INTEGRATION, None), - ), + ], ) @pytest.mark.parametrize("target_domain", PLATFORMS_TO_TEST) async def test_reset_hidden_by( diff --git a/tests/components/switch_as_x/test_light.py b/tests/components/switch_as_x/test_light.py index 5bdec990fd4..5e48b7db965 100644 --- a/tests/components/switch_as_x/test_light.py +++ b/tests/components/switch_as_x/test_light.py @@ -1,4 +1,5 @@ """Tests for the Switch as X Light platform.""" + from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_MODE, diff --git a/tests/components/switch_as_x/test_lock.py b/tests/components/switch_as_x/test_lock.py index bdf1b754c5a..f7d61cf6895 100644 --- a/tests/components/switch_as_x/test_lock.py +++ b/tests/components/switch_as_x/test_lock.py @@ -1,4 +1,5 @@ """Tests for the Switch as X Lock platform.""" + from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.components.switch_as_x.config_flow import SwitchAsXConfigFlowHandler diff --git a/tests/components/switch_as_x/test_siren.py b/tests/components/switch_as_x/test_siren.py index 581aa74daff..83daa862830 100644 --- a/tests/components/switch_as_x/test_siren.py +++ b/tests/components/switch_as_x/test_siren.py @@ -1,4 +1,5 @@ """Tests for the Switch as X Siren platform.""" + from homeassistant.components.siren import DOMAIN as SIREN_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.components.switch_as_x.config_flow import SwitchAsXConfigFlowHandler diff --git a/tests/components/switch_as_x/test_valve.py b/tests/components/switch_as_x/test_valve.py index b76da012bde..854f693404f 100644 --- a/tests/components/switch_as_x/test_valve.py +++ b/tests/components/switch_as_x/test_valve.py @@ -1,4 +1,5 @@ """Tests for the Switch as X Valve platform.""" + from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.components.switch_as_x.config_flow import SwitchAsXConfigFlowHandler from homeassistant.components.switch_as_x.const import ( diff --git a/tests/components/switchbee/test_config_flow.py b/tests/components/switchbee/test_config_flow.py index 98d413c3b96..99c44365353 100644 --- a/tests/components/switchbee/test_config_flow.py +++ b/tests/components/switchbee/test_config_flow.py @@ -1,4 +1,5 @@ """Test the SwitchBee Smart Home config flow.""" + import json from unittest.mock import patch @@ -31,15 +32,20 @@ async def test_form(hass: HomeAssistant, test_cucode_in_coordinator_data) -> Non assert result["type"] == "form" assert result["errors"] == {} - with patch( - "switchbee.api.polling.CentralUnitPolling.get_configuration", - return_value=coordinator_data, - ), patch( - "homeassistant.components.switchbee.async_setup_entry", - return_value=True, - ), patch( - "switchbee.api.polling.CentralUnitPolling.fetch_states", return_value=None - ), patch("switchbee.api.polling.CentralUnitPolling._login", return_value=None): + with ( + patch( + "switchbee.api.polling.CentralUnitPolling.get_configuration", + return_value=coordinator_data, + ), + patch( + "homeassistant.components.switchbee.async_setup_entry", + return_value=True, + ), + patch( + "switchbee.api.polling.CentralUnitPolling.fetch_states", return_value=None + ), + patch("switchbee.api.polling.CentralUnitPolling._login", return_value=None), + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -148,16 +154,19 @@ async def test_form_entry_exists(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch( - "switchbee.api.polling.CentralUnitPolling._login", return_value=None - ), patch( - "homeassistant.components.switchbee.async_setup_entry", - return_value=True, - ), patch( - "switchbee.api.polling.CentralUnitPolling.get_configuration", - return_value=coordinator_data, - ), patch( - "switchbee.api.polling.CentralUnitPolling.fetch_states", return_value=None + with ( + patch("switchbee.api.polling.CentralUnitPolling._login", return_value=None), + patch( + "homeassistant.components.switchbee.async_setup_entry", + return_value=True, + ), + patch( + "switchbee.api.polling.CentralUnitPolling.get_configuration", + return_value=coordinator_data, + ), + patch( + "switchbee.api.polling.CentralUnitPolling.fetch_states", return_value=None + ), ): form_result = await hass.config_entries.flow.async_configure( result["flow_id"], diff --git a/tests/components/switchbot/__init__.py b/tests/components/switchbot/__init__.py index 257501ea196..a5adab4c77f 100644 --- a/tests/components/switchbot/__init__.py +++ b/tests/components/switchbot/__init__.py @@ -1,4 +1,5 @@ """Tests for the switchbot integration.""" + from unittest.mock import patch from homeassistant.components.bluetooth import BluetoothServiceInfoBleak diff --git a/tests/components/switchbot/test_config_flow.py b/tests/components/switchbot/test_config_flow.py index 85174658279..3d53dd2848e 100644 --- a/tests/components/switchbot/test_config_flow.py +++ b/tests/components/switchbot/test_config_flow.py @@ -1,4 +1,5 @@ """Test the switchbot config flow.""" + from unittest.mock import patch from switchbot import SwitchbotAccountConnectionError, SwitchbotAuthenticationError @@ -128,9 +129,12 @@ async def test_bluetooth_discovery_lock_key(hass: HomeAssistant) -> None: assert result["step_id"] == "lock_key" assert result["errors"] == {"base": "encryption_key_invalid"} - with patch_async_setup_entry() as mock_setup_entry, patch( - "homeassistant.components.switchbot.config_flow.SwitchbotLock.verify_encryption_key", - return_value=True, + with ( + patch_async_setup_entry() as mock_setup_entry, + patch( + "homeassistant.components.switchbot.config_flow.SwitchbotLock.verify_encryption_key", + return_value=True, + ), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -433,9 +437,12 @@ async def test_user_setup_wolock_key(hass: HomeAssistant) -> None: assert result["step_id"] == "lock_key" assert result["errors"] == {"base": "encryption_key_invalid"} - with patch_async_setup_entry() as mock_setup_entry, patch( - "homeassistant.components.switchbot.config_flow.SwitchbotLock.verify_encryption_key", - return_value=True, + with ( + patch_async_setup_entry() as mock_setup_entry, + patch( + "homeassistant.components.switchbot.config_flow.SwitchbotLock.verify_encryption_key", + return_value=True, + ), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -496,15 +503,19 @@ async def test_user_setup_wolock_auth(hass: HomeAssistant) -> None: assert result["errors"] == {"base": "auth_failed"} assert "error from api" in result["description_placeholders"]["error_detail"] - with patch_async_setup_entry() as mock_setup_entry, patch( - "homeassistant.components.switchbot.config_flow.SwitchbotLock.verify_encryption_key", - return_value=True, - ), patch( - "homeassistant.components.switchbot.config_flow.SwitchbotLock.retrieve_encryption_key", - return_value={ - CONF_KEY_ID: "ff", - CONF_ENCRYPTION_KEY: "ffffffffffffffffffffffffffffffff", - }, + with ( + patch_async_setup_entry() as mock_setup_entry, + patch( + "homeassistant.components.switchbot.config_flow.SwitchbotLock.verify_encryption_key", + return_value=True, + ), + patch( + "homeassistant.components.switchbot.config_flow.SwitchbotLock.retrieve_encryption_key", + return_value={ + CONF_KEY_ID: "ff", + CONF_ENCRYPTION_KEY: "ffffffffffffffffffffffffffffffff", + }, + ), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -597,9 +608,12 @@ async def test_user_setup_wolock_or_bot(hass: HomeAssistant) -> None: assert result["step_id"] == "lock_key" assert result["errors"] == {} - with patch_async_setup_entry() as mock_setup_entry, patch( - "homeassistant.components.switchbot.config_flow.SwitchbotLock.verify_encryption_key", - return_value=True, + with ( + patch_async_setup_entry() as mock_setup_entry, + patch( + "homeassistant.components.switchbot.config_flow.SwitchbotLock.verify_encryption_key", + return_value=True, + ), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], diff --git a/tests/components/switchbot_cloud/__init__.py b/tests/components/switchbot_cloud/__init__.py index 72d23c837ac..ce570499b3a 100644 --- a/tests/components/switchbot_cloud/__init__.py +++ b/tests/components/switchbot_cloud/__init__.py @@ -1,4 +1,5 @@ """Tests for the SwitchBot Cloud integration.""" + from homeassistant.components.switchbot_cloud.const import DOMAIN from homeassistant.const import CONF_API_KEY, CONF_API_TOKEN from homeassistant.core import HomeAssistant diff --git a/tests/components/switchbot_cloud/conftest.py b/tests/components/switchbot_cloud/conftest.py index b96d7638797..bfaea2c5a31 100644 --- a/tests/components/switchbot_cloud/conftest.py +++ b/tests/components/switchbot_cloud/conftest.py @@ -1,4 +1,5 @@ """Common fixtures for the SwitchBot via API tests.""" + from collections.abc import Generator from unittest.mock import AsyncMock, patch diff --git a/tests/components/switchbot_cloud/test_config_flow.py b/tests/components/switchbot_cloud/test_config_flow.py index 6fdf8fecdb7..47758d50582 100644 --- a/tests/components/switchbot_cloud/test_config_flow.py +++ b/tests/components/switchbot_cloud/test_config_flow.py @@ -1,4 +1,5 @@ """Test the SwitchBot via API config flow.""" + from unittest.mock import AsyncMock, patch import pytest diff --git a/tests/components/switcher_kis/__init__.py b/tests/components/switcher_kis/__init__.py index 671af5e11b9..3f08afcbc9f 100644 --- a/tests/components/switcher_kis/__init__.py +++ b/tests/components/switcher_kis/__init__.py @@ -1,4 +1,5 @@ """Test cases and object for the Switcher integration tests.""" + from homeassistant.components.switcher_kis.const import DOMAIN from homeassistant.core import HomeAssistant diff --git a/tests/components/switcher_kis/conftest.py b/tests/components/switcher_kis/conftest.py index 7fff1c476fb..543f6cad008 100644 --- a/tests/components/switcher_kis/conftest.py +++ b/tests/components/switcher_kis/conftest.py @@ -1,4 +1,5 @@ """Common fixtures and objects for the Switcher integration tests.""" + from unittest.mock import AsyncMock, Mock, patch import pytest diff --git a/tests/components/switcher_kis/test_button.py b/tests/components/switcher_kis/test_button.py index d752acca7e5..c1350c0fec2 100644 --- a/tests/components/switcher_kis/test_button.py +++ b/tests/components/switcher_kis/test_button.py @@ -1,4 +1,5 @@ """Tests for Switcher button platform.""" + from unittest.mock import ANY, patch from aioswitcher.api import DeviceState, SwitcherBaseResponse, ThermostatSwing diff --git a/tests/components/switcher_kis/test_climate.py b/tests/components/switcher_kis/test_climate.py index 1919261109e..759f7f1bd98 100644 --- a/tests/components/switcher_kis/test_climate.py +++ b/tests/components/switcher_kis/test_climate.py @@ -1,4 +1,5 @@ """Test the Switcher climate platform.""" + from unittest.mock import ANY, patch from aioswitcher.api import SwitcherBaseResponse diff --git a/tests/components/switcher_kis/test_config_flow.py b/tests/components/switcher_kis/test_config_flow.py index e5859a095ca..e03c8eb645f 100644 --- a/tests/components/switcher_kis/test_config_flow.py +++ b/tests/components/switcher_kis/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Switcher config flow.""" + from unittest.mock import patch import pytest diff --git a/tests/components/switcher_kis/test_cover.py b/tests/components/switcher_kis/test_cover.py index 2de4497a51e..07f349d1a72 100644 --- a/tests/components/switcher_kis/test_cover.py +++ b/tests/components/switcher_kis/test_cover.py @@ -1,4 +1,5 @@ """Test the Switcher cover platform.""" + from unittest.mock import patch from aioswitcher.api import SwitcherBaseResponse diff --git a/tests/components/switcher_kis/test_init.py b/tests/components/switcher_kis/test_init.py index 04b35956c11..f0484ca2f67 100644 --- a/tests/components/switcher_kis/test_init.py +++ b/tests/components/switcher_kis/test_init.py @@ -1,4 +1,5 @@ """Test cases for the switcher_kis component.""" + from datetime import timedelta from unittest.mock import patch diff --git a/tests/components/switcher_kis/test_sensor.py b/tests/components/switcher_kis/test_sensor.py index 03073b21a96..f61cdd5a010 100644 --- a/tests/components/switcher_kis/test_sensor.py +++ b/tests/components/switcher_kis/test_sensor.py @@ -1,4 +1,5 @@ """Test the Switcher Sensor Platform.""" + import pytest from homeassistant.components.switcher_kis.const import DATA_DEVICE, DOMAIN @@ -65,9 +66,7 @@ async def test_sensor_disabled(hass: HomeAssistant, mock_bridge) -> None: assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION # Test enabling entity - updated_entry = registry.async_update_entity( - entry.entity_id, **{"disabled_by": None} - ) + updated_entry = registry.async_update_entity(entry.entity_id, disabled_by=None) assert updated_entry != entry assert updated_entry.disabled is False diff --git a/tests/components/switcher_kis/test_services.py b/tests/components/switcher_kis/test_services.py index bb16cf2d427..039daec4c97 100644 --- a/tests/components/switcher_kis/test_services.py +++ b/tests/components/switcher_kis/test_services.py @@ -1,4 +1,5 @@ """Test the services for the Switcher integration.""" + from unittest.mock import patch from aioswitcher.api import Command diff --git a/tests/components/switcher_kis/test_switch.py b/tests/components/switcher_kis/test_switch.py index fb45a372d2a..058546ac2ae 100644 --- a/tests/components/switcher_kis/test_switch.py +++ b/tests/components/switcher_kis/test_switch.py @@ -1,4 +1,5 @@ """Test the Switcher switch platform.""" + from unittest.mock import patch from aioswitcher.api import Command, SwitcherBaseResponse diff --git a/tests/components/syncthing/test_config_flow.py b/tests/components/syncthing/test_config_flow.py index 6e631f9b233..d97226e422c 100644 --- a/tests/components/syncthing/test_config_flow.py +++ b/tests/components/syncthing/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for syncthing config flow.""" + from unittest.mock import patch from aiosyncthing.exceptions import UnauthorizedError @@ -36,12 +37,13 @@ async def test_show_setup_form(hass: HomeAssistant) -> None: async def test_flow_successful(hass: HomeAssistant) -> None: """Test with required fields only.""" - with patch( - "aiosyncthing.system.System.status", return_value={"myID": "server-id"} - ), patch( - "homeassistant.components.syncthing.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch("aiosyncthing.system.System.status", return_value={"myID": "server-id"}), + patch( + "homeassistant.components.syncthing.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": "user"}, diff --git a/tests/components/syncthru/test_config_flow.py b/tests/components/syncthru/test_config_flow.py index 948e55649fc..90470431ade 100644 --- a/tests/components/syncthru/test_config_flow.py +++ b/tests/components/syncthru/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for syncthru config flow.""" + import re from unittest.mock import patch @@ -90,8 +91,9 @@ async def test_syncthru_not_supported(hass: HomeAssistant) -> None: async def test_unknown_state(hass: HomeAssistant) -> None: """Test we show user form on unsupported device.""" - with patch.object(SyncThru, "update"), patch.object( - SyncThru, "is_unknown_state", return_value=True + with ( + patch.object(SyncThru, "update"), + patch.object(SyncThru, "is_unknown_state", return_value=True), ): result = await hass.config_entries.flow.async_init( DOMAIN, diff --git a/tests/components/synology_dsm/conftest.py b/tests/components/synology_dsm/conftest.py index 77ef1b61e9e..044a3738543 100644 --- a/tests/components/synology_dsm/conftest.py +++ b/tests/components/synology_dsm/conftest.py @@ -1,4 +1,5 @@ """Configure Synology DSM tests.""" + from collections.abc import Generator from unittest.mock import AsyncMock, patch diff --git a/tests/components/synology_dsm/test_config_flow.py b/tests/components/synology_dsm/test_config_flow.py index 4d4ba583169..67da3712983 100644 --- a/tests/components/synology_dsm/test_config_flow.py +++ b/tests/components/synology_dsm/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for the Synology DSM config flow.""" + from ipaddress import ip_address from unittest.mock import AsyncMock, MagicMock, Mock, patch @@ -380,12 +381,15 @@ async def test_reconfig_user(hass: HomeAssistant, service: MagicMock) -> None: unique_id=SERIAL, ).add_to_hass(hass) - with patch( - "homeassistant.config_entries.ConfigEntries.async_reload", - return_value=True, - ), patch( - "homeassistant.components.synology_dsm.config_flow.SynologyDSM", - return_value=service, + with ( + patch( + "homeassistant.config_entries.ConfigEntries.async_reload", + return_value=True, + ), + patch( + "homeassistant.components.synology_dsm.config_flow.SynologyDSM", + return_value=service, + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, diff --git a/tests/components/synology_dsm/test_init.py b/tests/components/synology_dsm/test_init.py index 7e86b28f0ee..25c4d69dfee 100644 --- a/tests/components/synology_dsm/test_init.py +++ b/tests/components/synology_dsm/test_init.py @@ -1,4 +1,5 @@ """Tests for the Synology DSM component.""" + from unittest.mock import MagicMock, patch from synology_dsm.exceptions import SynologyDSMLoginInvalidException @@ -22,10 +23,13 @@ from tests.common import MockConfigEntry async def test_services_registered(hass: HomeAssistant, mock_dsm: MagicMock) -> None: """Test if all services are registered.""" - with patch( - "homeassistant.components.synology_dsm.common.SynologyDSM", - return_value=mock_dsm, - ), patch("homeassistant.components.synology_dsm.PLATFORMS", return_value=[]): + with ( + patch( + "homeassistant.components.synology_dsm.common.SynologyDSM", + return_value=mock_dsm, + ), + patch("homeassistant.components.synology_dsm.PLATFORMS", return_value=[]), + ): entry = MockConfigEntry( domain=DOMAIN, data={ @@ -45,17 +49,20 @@ async def test_services_registered(hass: HomeAssistant, mock_dsm: MagicMock) -> async def test_reauth_triggered(hass: HomeAssistant) -> None: """Test if reauthentication flow is triggered.""" - with patch( - "homeassistant.components.synology_dsm.SynoApi.async_setup", - side_effect=SynologyDSMLoginInvalidException(USERNAME), - ), patch( - "homeassistant.components.synology_dsm.config_flow.SynologyDSMFlowHandler.async_step_reauth", - return_value={ - "type": data_entry_flow.FlowResultType.FORM, - "flow_id": "mock_flow", - "step_id": "reauth_confirm", - }, - ) as mock_async_step_reauth: + with ( + patch( + "homeassistant.components.synology_dsm.SynoApi.async_setup", + side_effect=SynologyDSMLoginInvalidException(USERNAME), + ), + patch( + "homeassistant.components.synology_dsm.config_flow.SynologyDSMFlowHandler.async_step_reauth", + return_value={ + "type": data_entry_flow.FlowResultType.FORM, + "flow_id": "mock_flow", + "step_id": "reauth_confirm", + }, + ) as mock_async_step_reauth, + ): entry = MockConfigEntry( domain=DOMAIN, data={ diff --git a/tests/components/synology_dsm/test_media_source.py b/tests/components/synology_dsm/test_media_source.py index e0aadf9260c..e806014dcd6 100644 --- a/tests/components/synology_dsm/test_media_source.py +++ b/tests/components/synology_dsm/test_media_source.py @@ -130,10 +130,13 @@ async def test_browse_media_album_error( hass: HomeAssistant, dsm_with_photos: MagicMock ) -> None: """Test browse_media with unknown album.""" - with patch( - "homeassistant.components.synology_dsm.common.SynologyDSM", - return_value=dsm_with_photos, - ), patch("homeassistant.components.synology_dsm.PLATFORMS", return_value=[]): + with ( + patch( + "homeassistant.components.synology_dsm.common.SynologyDSM", + return_value=dsm_with_photos, + ), + patch("homeassistant.components.synology_dsm.PLATFORMS", return_value=[]), + ): entry = MockConfigEntry( domain=DOMAIN, data={ @@ -169,10 +172,13 @@ async def test_browse_media_get_root( hass: HomeAssistant, dsm_with_photos: MagicMock ) -> None: """Test browse_media returning root media sources.""" - with patch( - "homeassistant.components.synology_dsm.common.SynologyDSM", - return_value=dsm_with_photos, - ), patch("homeassistant.components.synology_dsm.PLATFORMS", return_value=[]): + with ( + patch( + "homeassistant.components.synology_dsm.common.SynologyDSM", + return_value=dsm_with_photos, + ), + patch("homeassistant.components.synology_dsm.PLATFORMS", return_value=[]), + ): entry = MockConfigEntry( domain=DOMAIN, data={ @@ -203,10 +209,13 @@ async def test_browse_media_get_albums( hass: HomeAssistant, dsm_with_photos: MagicMock ) -> None: """Test browse_media returning albums.""" - with patch( - "homeassistant.components.synology_dsm.common.SynologyDSM", - return_value=dsm_with_photos, - ), patch("homeassistant.components.synology_dsm.PLATFORMS", return_value=[]): + with ( + patch( + "homeassistant.components.synology_dsm.common.SynologyDSM", + return_value=dsm_with_photos, + ), + patch("homeassistant.components.synology_dsm.PLATFORMS", return_value=[]), + ): entry = MockConfigEntry( domain=DOMAIN, data={ @@ -241,10 +250,13 @@ async def test_browse_media_get_items_error( hass: HomeAssistant, dsm_with_photos: MagicMock ) -> None: """Test browse_media returning albums.""" - with patch( - "homeassistant.components.synology_dsm.common.SynologyDSM", - return_value=dsm_with_photos, - ), patch("homeassistant.components.synology_dsm.PLATFORMS", return_value=[]): + with ( + patch( + "homeassistant.components.synology_dsm.common.SynologyDSM", + return_value=dsm_with_photos, + ), + patch("homeassistant.components.synology_dsm.PLATFORMS", return_value=[]), + ): entry = MockConfigEntry( domain=DOMAIN, data={ @@ -288,10 +300,13 @@ async def test_browse_media_get_items_thumbnail_error( hass: HomeAssistant, dsm_with_photos: MagicMock ) -> None: """Test browse_media returning albums.""" - with patch( - "homeassistant.components.synology_dsm.common.SynologyDSM", - return_value=dsm_with_photos, - ), patch("homeassistant.components.synology_dsm.PLATFORMS", return_value=[]): + with ( + patch( + "homeassistant.components.synology_dsm.common.SynologyDSM", + return_value=dsm_with_photos, + ), + patch("homeassistant.components.synology_dsm.PLATFORMS", return_value=[]), + ): entry = MockConfigEntry( domain=DOMAIN, data={ @@ -327,10 +342,13 @@ async def test_browse_media_get_items( hass: HomeAssistant, dsm_with_photos: MagicMock ) -> None: """Test browse_media returning albums.""" - with patch( - "homeassistant.components.synology_dsm.common.SynologyDSM", - return_value=dsm_with_photos, - ), patch("homeassistant.components.synology_dsm.PLATFORMS", return_value=[]): + with ( + patch( + "homeassistant.components.synology_dsm.common.SynologyDSM", + return_value=dsm_with_photos, + ), + patch("homeassistant.components.synology_dsm.PLATFORMS", return_value=[]), + ): entry = MockConfigEntry( domain=DOMAIN, data={ @@ -376,10 +394,13 @@ async def test_media_view( with pytest.raises(web.HTTPNotFound): await view.get(request, "", "") - with patch( - "homeassistant.components.synology_dsm.common.SynologyDSM", - return_value=dsm_with_photos, - ), patch("homeassistant.components.synology_dsm.PLATFORMS", return_value=[]): + with ( + patch( + "homeassistant.components.synology_dsm.common.SynologyDSM", + return_value=dsm_with_photos, + ), + patch("homeassistant.components.synology_dsm.PLATFORMS", return_value=[]), + ): entry = MockConfigEntry( domain=DOMAIN, data={ diff --git a/tests/components/system_bridge/__init__.py b/tests/components/system_bridge/__init__.py index f049f887584..edbe5469705 100644 --- a/tests/components/system_bridge/__init__.py +++ b/tests/components/system_bridge/__init__.py @@ -1 +1,116 @@ """Tests for the System Bridge integration.""" + +from collections.abc import Awaitable, Callable +from dataclasses import asdict +from ipaddress import ip_address +from typing import Any + +from systembridgeconnector.const import TYPE_DATA_UPDATE +from systembridgemodels.const import MODEL_SYSTEM +from systembridgemodels.modules import System +from systembridgemodels.response import Response + +from homeassistant.components import zeroconf +from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TOKEN + +FIXTURE_MAC_ADDRESS = "aa:bb:cc:dd:ee:ff" +FIXTURE_UUID = "e91bf575-56f3-4c83-8f42-70ac17adcd33" + +FIXTURE_AUTH_INPUT = {CONF_TOKEN: "abc-123-def-456-ghi"} + +FIXTURE_USER_INPUT = { + CONF_TOKEN: "abc-123-def-456-ghi", + CONF_HOST: "test-bridge", + CONF_PORT: "9170", +} + +FIXTURE_ZEROCONF_INPUT = { + CONF_TOKEN: "abc-123-def-456-ghi", + CONF_HOST: "1.1.1.1", + CONF_PORT: "9170", +} + +FIXTURE_ZEROCONF = zeroconf.ZeroconfServiceInfo( + ip_address=ip_address("1.1.1.1"), + ip_addresses=[ip_address("1.1.1.1")], + port=9170, + hostname="test-bridge.local.", + type="_system-bridge._tcp.local.", + name="System Bridge - test-bridge._system-bridge._tcp.local.", + properties={ + "address": "http://test-bridge:9170", + "fqdn": "test-bridge", + "host": "test-bridge", + "ip": "1.1.1.1", + "mac": FIXTURE_MAC_ADDRESS, + "port": "9170", + "uuid": FIXTURE_UUID, + }, +) + +FIXTURE_ZEROCONF_BAD = zeroconf.ZeroconfServiceInfo( + ip_address=ip_address("1.1.1.1"), + ip_addresses=[ip_address("1.1.1.1")], + port=9170, + hostname="test-bridge.local.", + type="_system-bridge._tcp.local.", + name="System Bridge - test-bridge._system-bridge._tcp.local.", + properties={ + "something": "bad", + }, +) + + +FIXTURE_SYSTEM = System( + boot_time=1, + fqdn="", + hostname="1.1.1.1", + ip_address_4="1.1.1.1", + mac_address=FIXTURE_MAC_ADDRESS, + platform="", + platform_version="", + uptime=1, + uuid=FIXTURE_UUID, + version="", + version_latest="", + version_newer_available=False, + users=[], +) + +FIXTURE_DATA_RESPONSE = Response( + id="1234", + type=TYPE_DATA_UPDATE, + subtype=None, + message="Data received", + module=MODEL_SYSTEM, + data=asdict(FIXTURE_SYSTEM), +) + +FIXTURE_DATA_RESPONSE_BAD = Response( + id="1234", + type=TYPE_DATA_UPDATE, + subtype=None, + message="Data received", + module=MODEL_SYSTEM, + data={}, +) + +FIXTURE_DATA_RESPONSE_BAD = Response( + id="1234", + type=TYPE_DATA_UPDATE, + subtype=None, + message="Data received", + module=MODEL_SYSTEM, + data={}, +) + + +async def mock_data_listener( + self, + callback: Callable[[str, Any], Awaitable[None]] | None = None, + _: bool = False, +): + """Mock websocket data listener.""" + if callback is not None: + # Simulate data received from the websocket + await callback(MODEL_SYSTEM, FIXTURE_SYSTEM) diff --git a/tests/components/system_bridge/test_config_flow.py b/tests/components/system_bridge/test_config_flow.py index 53c8ecf88bd..0047cc62365 100644 --- a/tests/components/system_bridge/test_config_flow.py +++ b/tests/components/system_bridge/test_config_flow.py @@ -1,130 +1,30 @@ """Test the System Bridge config flow.""" -from ipaddress import ip_address + from unittest.mock import patch -from systembridgeconnector.const import TYPE_DATA_UPDATE from systembridgeconnector.exceptions import ( AuthenticationException, ConnectionClosedException, ConnectionErrorException, ) -from systembridgemodels.const import MODEL_SYSTEM -from systembridgemodels.response import Response -from systembridgemodels.system import LastUpdated, System from homeassistant import config_entries, data_entry_flow -from homeassistant.components import zeroconf from homeassistant.components.system_bridge.const import DOMAIN -from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT from homeassistant.core import HomeAssistant +from . import ( + FIXTURE_AUTH_INPUT, + FIXTURE_DATA_RESPONSE, + FIXTURE_USER_INPUT, + FIXTURE_UUID, + FIXTURE_ZEROCONF, + FIXTURE_ZEROCONF_BAD, + FIXTURE_ZEROCONF_INPUT, + mock_data_listener, +) + from tests.common import MockConfigEntry -FIXTURE_MAC_ADDRESS = "aa:bb:cc:dd:ee:ff" -FIXTURE_UUID = "e91bf575-56f3-4c83-8f42-70ac17adcd33" - -FIXTURE_AUTH_INPUT = {CONF_API_KEY: "abc-123-def-456-ghi"} - -FIXTURE_USER_INPUT = { - CONF_API_KEY: "abc-123-def-456-ghi", - CONF_HOST: "test-bridge", - CONF_PORT: "9170", -} - -FIXTURE_ZEROCONF_INPUT = { - CONF_API_KEY: "abc-123-def-456-ghi", - CONF_HOST: "1.1.1.1", - CONF_PORT: "9170", -} - -FIXTURE_ZEROCONF = zeroconf.ZeroconfServiceInfo( - ip_address=ip_address("1.1.1.1"), - ip_addresses=[ip_address("1.1.1.1")], - port=9170, - hostname="test-bridge.local.", - type="_system-bridge._tcp.local.", - name="System Bridge - test-bridge._system-bridge._tcp.local.", - properties={ - "address": "http://test-bridge:9170", - "fqdn": "test-bridge", - "host": "test-bridge", - "ip": "1.1.1.1", - "mac": FIXTURE_MAC_ADDRESS, - "port": "9170", - "uuid": FIXTURE_UUID, - }, -) - -FIXTURE_ZEROCONF_BAD = zeroconf.ZeroconfServiceInfo( - ip_address=ip_address("1.1.1.1"), - ip_addresses=[ip_address("1.1.1.1")], - port=9170, - hostname="test-bridge.local.", - type="_system-bridge._tcp.local.", - name="System Bridge - test-bridge._system-bridge._tcp.local.", - properties={ - "something": "bad", - }, -) - - -FIXTURE_SYSTEM = System( - id=FIXTURE_UUID, - boot_time=1, - fqdn="", - hostname="1.1.1.1", - ip_address_4="1.1.1.1", - mac_address=FIXTURE_MAC_ADDRESS, - platform="", - platform_version="", - uptime=1, - uuid=FIXTURE_UUID, - version="", - version_latest="", - version_newer_available=False, - last_updated=LastUpdated( - boot_time=1, - fqdn=1, - hostname=1, - ip_address_4=1, - mac_address=1, - platform=1, - platform_version=1, - uptime=1, - uuid=1, - version=1, - version_latest=1, - version_newer_available=1, - ), -) - -FIXTURE_DATA_RESPONSE = Response( - id="1234", - type=TYPE_DATA_UPDATE, - subtype=None, - message="Data received", - module=MODEL_SYSTEM, - data=FIXTURE_SYSTEM, -) - -FIXTURE_DATA_RESPONSE_BAD = Response( - id="1234", - type=TYPE_DATA_UPDATE, - subtype=None, - message="Data received", - module=MODEL_SYSTEM, - data={}, -) - -FIXTURE_DATA_RESPONSE_BAD = Response( - id="1234", - type=TYPE_DATA_UPDATE, - subtype=None, - message="Data received", - module=MODEL_SYSTEM, - data={}, -) - async def test_show_user_form(hass: HomeAssistant) -> None: """Test that the setup form is served.""" @@ -145,17 +45,23 @@ async def test_user_flow(hass: HomeAssistant) -> None: assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None - with patch( - "homeassistant.components.system_bridge.config_flow.WebSocketClient.connect" - ), patch( - "systembridgeconnector.websocket_client.WebSocketClient.get_data", - return_value=FIXTURE_DATA_RESPONSE, - ), patch( - "systembridgeconnector.websocket_client.WebSocketClient.listen", - ), patch( - "homeassistant.components.system_bridge.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.system_bridge.config_flow.WebSocketClient.connect" + ), + patch( + "systembridgeconnector.websocket_client.WebSocketClient.get_data", + return_value=FIXTURE_DATA_RESPONSE, + ), + patch( + "systembridgeconnector.websocket_client.WebSocketClient.listen", + new=mock_data_listener, + ), + patch( + "homeassistant.components.system_bridge.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], FIXTURE_USER_INPUT ) @@ -199,13 +105,18 @@ async def test_form_connection_closed_cannot_connect(hass: HomeAssistant) -> Non assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None - with patch( - "homeassistant.components.system_bridge.config_flow.WebSocketClient.connect" - ), patch( - "systembridgeconnector.websocket_client.WebSocketClient.get_data", - side_effect=ConnectionClosedException, - ), patch( - "systembridgeconnector.websocket_client.WebSocketClient.listen", + with ( + patch( + "homeassistant.components.system_bridge.config_flow.WebSocketClient.connect" + ), + patch( + "systembridgeconnector.websocket_client.WebSocketClient.get_data", + side_effect=ConnectionClosedException, + ), + patch( + "systembridgeconnector.websocket_client.WebSocketClient.listen", + new=mock_data_listener, + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], FIXTURE_USER_INPUT @@ -226,13 +137,18 @@ async def test_form_timeout_cannot_connect(hass: HomeAssistant) -> None: assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None - with patch( - "homeassistant.components.system_bridge.config_flow.WebSocketClient.connect" - ), patch( - "systembridgeconnector.websocket_client.WebSocketClient.get_data", - side_effect=TimeoutError, - ), patch( - "systembridgeconnector.websocket_client.WebSocketClient.listen", + with ( + patch( + "homeassistant.components.system_bridge.config_flow.WebSocketClient.connect" + ), + patch( + "systembridgeconnector.websocket_client.WebSocketClient.get_data", + side_effect=TimeoutError, + ), + patch( + "systembridgeconnector.websocket_client.WebSocketClient.listen", + new=mock_data_listener, + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], FIXTURE_USER_INPUT @@ -253,13 +169,18 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None - with patch( - "homeassistant.components.system_bridge.config_flow.WebSocketClient.connect" - ), patch( - "systembridgeconnector.websocket_client.WebSocketClient.get_data", - side_effect=AuthenticationException, - ), patch( - "systembridgeconnector.websocket_client.WebSocketClient.listen", + with ( + patch( + "homeassistant.components.system_bridge.config_flow.WebSocketClient.connect" + ), + patch( + "systembridgeconnector.websocket_client.WebSocketClient.get_data", + side_effect=AuthenticationException, + ), + patch( + "systembridgeconnector.websocket_client.WebSocketClient.listen", + new=mock_data_listener, + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], FIXTURE_USER_INPUT @@ -280,40 +201,18 @@ async def test_form_uuid_error(hass: HomeAssistant) -> None: assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None - with patch( - "homeassistant.components.system_bridge.config_flow.WebSocketClient.connect" - ), patch( - "systembridgeconnector.websocket_client.WebSocketClient.get_data", - side_effect=ValueError, - ), patch( - "systembridgeconnector.websocket_client.WebSocketClient.listen", - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], FIXTURE_USER_INPUT - ) - await hass.async_block_till_done() - - assert result2["type"] == data_entry_flow.FlowResultType.FORM - assert result2["step_id"] == "user" - assert result2["errors"] == {"base": "cannot_connect"} - - -async def test_form_value_error(hass: HomeAssistant) -> None: - """Test we handle error from bad value.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["errors"] is None - - with patch( - "homeassistant.components.system_bridge.config_flow.WebSocketClient.connect" - ), patch( - "systembridgeconnector.websocket_client.WebSocketClient.get_data", - return_value=FIXTURE_DATA_RESPONSE_BAD, - ), patch( - "systembridgeconnector.websocket_client.WebSocketClient.listen", + with ( + patch( + "homeassistant.components.system_bridge.config_flow.WebSocketClient.connect" + ), + patch( + "systembridgeconnector.websocket_client.WebSocketClient.get_data", + side_effect=ValueError, + ), + patch( + "systembridgeconnector.websocket_client.WebSocketClient.listen", + new=mock_data_listener, + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], FIXTURE_USER_INPUT @@ -334,13 +233,18 @@ async def test_form_unknown_error(hass: HomeAssistant) -> None: assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None - with patch( - "homeassistant.components.system_bridge.config_flow.WebSocketClient.connect" - ), patch( - "systembridgeconnector.websocket_client.WebSocketClient.get_data", - side_effect=Exception, - ), patch( - "systembridgeconnector.websocket_client.WebSocketClient.listen", + with ( + patch( + "homeassistant.components.system_bridge.config_flow.WebSocketClient.connect" + ), + patch( + "systembridgeconnector.websocket_client.WebSocketClient.get_data", + side_effect=Exception, + ), + patch( + "systembridgeconnector.websocket_client.WebSocketClient.listen", + new=mock_data_listener, + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], FIXTURE_USER_INPUT @@ -361,13 +265,18 @@ async def test_reauth_authorization_error(hass: HomeAssistant) -> None: assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "authenticate" - with patch( - "homeassistant.components.system_bridge.config_flow.WebSocketClient.connect" - ), patch( - "systembridgeconnector.websocket_client.WebSocketClient.get_data", - side_effect=AuthenticationException, - ), patch( - "systembridgeconnector.websocket_client.WebSocketClient.listen", + with ( + patch( + "homeassistant.components.system_bridge.config_flow.WebSocketClient.connect" + ), + patch( + "systembridgeconnector.websocket_client.WebSocketClient.get_data", + side_effect=AuthenticationException, + ), + patch( + "systembridgeconnector.websocket_client.WebSocketClient.listen", + new=mock_data_listener, + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], FIXTURE_AUTH_INPUT @@ -401,6 +310,28 @@ async def test_reauth_connection_error(hass: HomeAssistant) -> None: assert result2["step_id"] == "authenticate" assert result2["errors"] == {"base": "cannot_connect"} + with ( + patch( + "systembridgeconnector.websocket_client.WebSocketClient.connect", + ), + patch( + "systembridgeconnector.websocket_client.WebSocketClient.get_data", + return_value=None, + ), + patch( + "systembridgeconnector.websocket_client.WebSocketClient.listen", + new=mock_data_listener, + ), + ): + result3 = await hass.config_entries.flow.async_configure( + result["flow_id"], FIXTURE_AUTH_INPUT + ) + await hass.async_block_till_done() + + assert result3["type"] == data_entry_flow.FlowResultType.FORM + assert result3["step_id"] == "authenticate" + assert result3["errors"] == {"base": "cannot_connect"} + async def test_reauth_connection_closed_error(hass: HomeAssistant) -> None: """Test we show user form on connection error.""" @@ -411,13 +342,18 @@ async def test_reauth_connection_closed_error(hass: HomeAssistant) -> None: assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "authenticate" - with patch( - "homeassistant.components.system_bridge.config_flow.WebSocketClient.connect" - ), patch( - "systembridgeconnector.websocket_client.WebSocketClient.get_data", - side_effect=ConnectionClosedException, - ), patch( - "systembridgeconnector.websocket_client.WebSocketClient.listen", + with ( + patch( + "homeassistant.components.system_bridge.config_flow.WebSocketClient.connect" + ), + patch( + "systembridgeconnector.websocket_client.WebSocketClient.get_data", + side_effect=ConnectionClosedException, + ), + patch( + "systembridgeconnector.websocket_client.WebSocketClient.listen", + new=mock_data_listener, + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], FIXTURE_AUTH_INPUT @@ -443,17 +379,23 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "authenticate" - with patch( - "homeassistant.components.system_bridge.config_flow.WebSocketClient.connect" - ), patch( - "systembridgeconnector.websocket_client.WebSocketClient.get_data", - return_value=FIXTURE_DATA_RESPONSE, - ), patch( - "systembridgeconnector.websocket_client.WebSocketClient.listen", - ), patch( - "homeassistant.components.system_bridge.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.system_bridge.config_flow.WebSocketClient.connect" + ), + patch( + "systembridgeconnector.websocket_client.WebSocketClient.get_data", + return_value=FIXTURE_DATA_RESPONSE, + ), + patch( + "systembridgeconnector.websocket_client.WebSocketClient.listen", + new=mock_data_listener, + ), + patch( + "homeassistant.components.system_bridge.async_setup_entry", + return_value=True, + ), + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], FIXTURE_AUTH_INPUT ) @@ -462,8 +404,6 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "reauth_successful" - assert len(mock_setup_entry.mock_calls) == 1 - async def test_zeroconf_flow(hass: HomeAssistant) -> None: """Test zeroconf flow.""" @@ -477,17 +417,23 @@ async def test_zeroconf_flow(hass: HomeAssistant) -> None: assert result["type"] == data_entry_flow.FlowResultType.FORM assert not result["errors"] - with patch( - "homeassistant.components.system_bridge.config_flow.WebSocketClient.connect" - ), patch( - "systembridgeconnector.websocket_client.WebSocketClient.get_data", - return_value=FIXTURE_DATA_RESPONSE, - ), patch( - "systembridgeconnector.websocket_client.WebSocketClient.listen", - ), patch( - "homeassistant.components.system_bridge.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.system_bridge.config_flow.WebSocketClient.connect" + ), + patch( + "systembridgeconnector.websocket_client.WebSocketClient.get_data", + return_value=FIXTURE_DATA_RESPONSE, + ), + patch( + "systembridgeconnector.websocket_client.WebSocketClient.listen", + new=mock_data_listener, + ), + patch( + "homeassistant.components.system_bridge.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], FIXTURE_AUTH_INPUT ) diff --git a/tests/components/system_bridge/test_init.py b/tests/components/system_bridge/test_init.py new file mode 100644 index 00000000000..67d8595ba4c --- /dev/null +++ b/tests/components/system_bridge/test_init.py @@ -0,0 +1,83 @@ +"""Test the System Bridge integration.""" + +from unittest.mock import patch + +from homeassistant.components.system_bridge.config_flow import SystemBridgeConfigFlow +from homeassistant.components.system_bridge.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT, CONF_TOKEN +from homeassistant.core import HomeAssistant + +from . import FIXTURE_USER_INPUT, FIXTURE_UUID + +from tests.common import MockConfigEntry + + +async def test_migration_minor_1_to_2(hass: HomeAssistant) -> None: + """Test migration.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + unique_id=FIXTURE_UUID, + data={ + CONF_API_KEY: FIXTURE_USER_INPUT[CONF_TOKEN], + CONF_HOST: FIXTURE_USER_INPUT[CONF_HOST], + CONF_PORT: FIXTURE_USER_INPUT[CONF_PORT], + }, + version=SystemBridgeConfigFlow.VERSION, + minor_version=1, + ) + + with patch( + "homeassistant.components.system_bridge.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert len(mock_setup_entry.mock_calls) == 1 + + # Check that the version has been updated and the api_key has been moved to token + assert config_entry.version == SystemBridgeConfigFlow.VERSION + assert config_entry.minor_version == SystemBridgeConfigFlow.MINOR_VERSION + assert config_entry.data == { + CONF_API_KEY: FIXTURE_USER_INPUT[CONF_TOKEN], + CONF_HOST: FIXTURE_USER_INPUT[CONF_HOST], + CONF_PORT: FIXTURE_USER_INPUT[CONF_PORT], + CONF_TOKEN: FIXTURE_USER_INPUT[CONF_TOKEN], + } + assert config_entry.state == ConfigEntryState.LOADED + + +async def test_migration_minor_future_version(hass: HomeAssistant) -> None: + """Test migration.""" + config_entry_data = { + CONF_API_KEY: FIXTURE_USER_INPUT[CONF_TOKEN], + CONF_HOST: FIXTURE_USER_INPUT[CONF_HOST], + CONF_PORT: FIXTURE_USER_INPUT[CONF_PORT], + CONF_TOKEN: FIXTURE_USER_INPUT[CONF_TOKEN], + } + config_entry_version = SystemBridgeConfigFlow.VERSION + config_entry_minor_version = SystemBridgeConfigFlow.MINOR_VERSION + 1 + config_entry = MockConfigEntry( + domain=DOMAIN, + unique_id=FIXTURE_UUID, + data=config_entry_data, + version=config_entry_version, + minor_version=config_entry_minor_version, + ) + + with patch( + "homeassistant.components.system_bridge.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert len(mock_setup_entry.mock_calls) == 1 + + assert config_entry.version == config_entry_version + assert config_entry.minor_version == config_entry_minor_version + assert config_entry.data == config_entry_data + assert config_entry.state == ConfigEntryState.LOADED diff --git a/tests/components/system_health/test_init.py b/tests/components/system_health/test_init.py index ceb1ec03fe3..e677b7d1d34 100644 --- a/tests/components/system_health/test_init.py +++ b/tests/components/system_health/test_init.py @@ -1,9 +1,11 @@ """Tests for the system health component init.""" + from unittest.mock import AsyncMock, Mock, patch from aiohttp.client_exceptions import ClientError from homeassistant.components import system_health +from homeassistant.components.system_health import async_register_info from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component @@ -73,7 +75,7 @@ async def test_info_endpoint_register_callback( async def mock_info(hass): return {"storage": "YAML"} - hass.components.system_health.async_register_info("lovelace", mock_info) + async_register_info(hass, "lovelace", mock_info) assert await async_setup_component(hass, "system_health", {}) data = await gather_system_health_info(hass, hass_ws_client) @@ -93,7 +95,7 @@ async def test_info_endpoint_register_callback_timeout( async def mock_info(hass): raise TimeoutError - hass.components.system_health.async_register_info("lovelace", mock_info) + async_register_info(hass, "lovelace", mock_info) assert await async_setup_component(hass, "system_health", {}) data = await gather_system_health_info(hass, hass_ws_client) @@ -110,7 +112,7 @@ async def test_info_endpoint_register_callback_exc( async def mock_info(hass): raise Exception("TEST ERROR") - hass.components.system_health.async_register_info("lovelace", mock_info) + async_register_info(hass, "lovelace", mock_info) assert await async_setup_component(hass, "system_health", {}) data = await gather_system_health_info(hass, hass_ws_client) diff --git a/tests/components/system_log/test_init.py b/tests/components/system_log/test_init.py index 1357d9e5e9e..5e4eda7d643 100644 --- a/tests/components/system_log/test_init.py +++ b/tests/components/system_log/test_init.py @@ -1,4 +1,5 @@ """Test system log component.""" + from __future__ import annotations import asyncio @@ -375,11 +376,14 @@ async def async_log_error_from_test_path(hass, path, watcher): call_path_frame = get_frame(call_path, path_frame) logger_frame = get_frame("venv_path/logging/log.py", call_path_frame) - with patch.object( - _LOGGER, "findCaller", MagicMock(return_value=(call_path, 0, None, None)) - ), patch( - "homeassistant.components.system_log.sys._getframe", - return_value=logger_frame, + with ( + patch.object( + _LOGGER, "findCaller", MagicMock(return_value=(call_path, 0, None, None)) + ), + patch( + "homeassistant.components.system_log.sys._getframe", + return_value=logger_frame, + ), ): wait_empty = watcher.add_watcher("error message") _LOGGER.error("error message") @@ -458,14 +462,19 @@ async def test__figure_out_source(hass: HomeAssistant) -> None: except ValueError as ex: exc_info = (type(ex), ex, ex.__traceback__) mock_record = MagicMock( - pathname="should not hit", + pathname="figure_out_source is False", lineno=5, exc_info=exc_info, ) regex_str = f"({__file__})" + paths_re = re.compile(regex_str) file, line_no = system_log._figure_out_source( mock_record, - re.compile(regex_str), + paths_re, + traceback.extract_tb(exc_info[2]), ) assert file == __file__ assert line_no != 5 + + entry = system_log.LogEntry(mock_record, paths_re, figure_out_source=False) + assert entry.source == ("figure_out_source is False", 5) diff --git a/tests/components/systemmonitor/conftest.py b/tests/components/systemmonitor/conftest.py index b12c11e73e6..c8cf614e04d 100644 --- a/tests/components/systemmonitor/conftest.py +++ b/tests/components/systemmonitor/conftest.py @@ -1,4 +1,5 @@ """Fixtures for the System Monitor integration.""" + from __future__ import annotations from collections.abc import Generator @@ -128,7 +129,21 @@ def mock_psutil(mock_process: list[MockProcess]) -> Generator: "255.255.255.0", "255.255.255.255", None, - ) + ), + snicaddr( + socket.AF_INET6, + "fe80::baf2:8a90:4f78:b1cb%end0", + "ffff:ffff:ffff:ffff::", + None, + None, + ), + snicaddr( + socket.AF_INET6, + "2a00:1f:2103:3a01:3333:2222:1111:0000", + "ffff:ffff:ffff:ffff::", + None, + None, + ), ], "eth1": [ snicaddr( @@ -178,11 +193,10 @@ def mock_os() -> Generator: """Mock os.path.isdir.""" return path != "/etc/hosts" - with patch( - "homeassistant.components.systemmonitor.coordinator.os" - ) as mock_os, patch( - "homeassistant.components.systemmonitor.util.os" - ) as mock_os_util: + with ( + patch("homeassistant.components.systemmonitor.coordinator.os") as mock_os, + patch("homeassistant.components.systemmonitor.util.os") as mock_os_util, + ): mock_os_util.name = "nt" mock_os.getloadavg.return_value = (1, 2, 3) mock_os_util.path.isdir = isdir diff --git a/tests/components/systemmonitor/snapshots/test_diagnostics.ambr b/tests/components/systemmonitor/snapshots/test_diagnostics.ambr index 0acb2362134..b50e051c816 100644 --- a/tests/components/systemmonitor/snapshots/test_diagnostics.ambr +++ b/tests/components/systemmonitor/snapshots/test_diagnostics.ambr @@ -2,54 +2,33 @@ # name: test_diagnostics dict({ 'coordinators': dict({ - 'boot_time': dict({ - 'data': '2024-02-24 15:00:00+00:00', - 'last_update_success': True, - }), - 'cpu_temp': dict({ - 'data': "{'cpu0-thermal': [shwtemp(label='cpu0-thermal', current=50.0, high=60.0, critical=70.0)]}", - 'last_update_success': True, - }), - 'disk_/': dict({ - 'data': 'sdiskusage(total=536870912000, used=322122547200, free=214748364800, percent=60.0)', - 'last_update_success': True, - }), - 'disk_/home/notexist/': dict({ - 'data': 'sdiskusage(total=536870912000, used=322122547200, free=214748364800, percent=60.0)', - 'last_update_success': True, - }), - 'disk_/media/share': dict({ - 'data': 'sdiskusage(total=536870912000, used=322122547200, free=214748364800, percent=60.0)', - 'last_update_success': True, - }), - 'memory': dict({ - 'data': 'VirtualMemory(total=104857600, available=41943040, percent=40.0, used=62914560, free=31457280)', - 'last_update_success': True, - }), - 'net_addr': dict({ - 'data': "{'eth0': [snicaddr(family=, address='192.168.1.1', netmask='255.255.255.0', broadcast='255.255.255.255', ptp=None)], 'eth1': [snicaddr(family=, address='192.168.10.1', netmask='255.255.255.0', broadcast='255.255.255.255', ptp=None)], 'vethxyzxyz': [snicaddr(family=, address='172.16.10.1', netmask='255.255.255.0', broadcast='255.255.255.255', ptp=None)]}", - 'last_update_success': True, - }), - 'net_io': dict({ - 'data': "{'eth0': snetio(bytes_sent=104857600, bytes_recv=104857600, packets_sent=50, packets_recv=50, errin=0, errout=0, dropin=0, dropout=0), 'eth1': snetio(bytes_sent=209715200, bytes_recv=209715200, packets_sent=150, packets_recv=150, errin=0, errout=0, dropin=0, dropout=0), 'vethxyzxyz': snetio(bytes_sent=314572800, bytes_recv=314572800, packets_sent=150, packets_recv=150, errin=0, errout=0, dropin=0, dropout=0)}", - 'last_update_success': True, - }), - 'process': dict({ - 'data': "[tests.components.systemmonitor.conftest.MockProcess(pid=1, name='python3', status='sleeping', started='2024-02-23 15:00:00'), tests.components.systemmonitor.conftest.MockProcess(pid=1, name='pip', status='sleeping', started='2024-02-23 15:00:00')]", - 'last_update_success': True, - }), - 'processor': dict({ - 'data': '10.0', - 'last_update_success': True, - }), - 'swap': dict({ - 'data': 'sswap(total=104857600, used=62914560, free=41943040, percent=60.0, sin=1, sout=1)', - 'last_update_success': True, - }), - 'system_load': dict({ - 'data': '(1, 2, 3)', - 'last_update_success': True, + 'data': dict({ + 'addresses': dict({ + 'eth0': "[snicaddr(family=, address='192.168.1.1', netmask='255.255.255.0', broadcast='255.255.255.255', ptp=None), snicaddr(family=, address='fe80::baf2:8a90:4f78:b1cb%end0', netmask='ffff:ffff:ffff:ffff::', broadcast=None, ptp=None), snicaddr(family=, address='2a00:1f:2103:3a01:3333:2222:1111:0000', netmask='ffff:ffff:ffff:ffff::', broadcast=None, ptp=None)]", + 'eth1': "[snicaddr(family=, address='192.168.10.1', netmask='255.255.255.0', broadcast='255.255.255.255', ptp=None)]", + 'vethxyzxyz': "[snicaddr(family=, address='172.16.10.1', netmask='255.255.255.0', broadcast='255.255.255.255', ptp=None)]", + }), + 'boot_time': '2024-02-24 15:00:00+00:00', + 'cpu_percent': '10.0', + 'disk_usage': dict({ + '/': 'sdiskusage(total=536870912000, used=322122547200, free=214748364800, percent=60.0)', + '/home/notexist/': 'sdiskusage(total=536870912000, used=322122547200, free=214748364800, percent=60.0)', + '/media/share': 'sdiskusage(total=536870912000, used=322122547200, free=214748364800, percent=60.0)', + }), + 'io_counters': dict({ + 'eth0': 'snetio(bytes_sent=104857600, bytes_recv=104857600, packets_sent=50, packets_recv=50, errin=0, errout=0, dropin=0, dropout=0)', + 'eth1': 'snetio(bytes_sent=209715200, bytes_recv=209715200, packets_sent=150, packets_recv=150, errin=0, errout=0, dropin=0, dropout=0)', + 'vethxyzxyz': 'snetio(bytes_sent=314572800, bytes_recv=314572800, packets_sent=150, packets_recv=150, errin=0, errout=0, dropin=0, dropout=0)', + }), + 'load': '(1, 2, 3)', + 'memory': 'VirtualMemory(total=104857600, available=41943040, percent=40.0, used=62914560, free=31457280)', + 'processes': "[tests.components.systemmonitor.conftest.MockProcess(pid=1, name='python3', status='sleeping', started='2024-02-23 15:00:00'), tests.components.systemmonitor.conftest.MockProcess(pid=1, name='pip', status='sleeping', started='2024-02-23 15:00:00')]", + 'swap': 'sswap(total=104857600, used=62914560, free=41943040, percent=60.0, sin=1, sout=1)', + 'temperatures': dict({ + 'cpu0-thermal': "[shwtemp(label='cpu0-thermal', current=50.0, high=60.0, critical=70.0)]", + }), }), + 'last_update_success': True, }), 'entry': dict({ 'data': dict({ diff --git a/tests/components/systemmonitor/snapshots/test_sensor.ambr b/tests/components/systemmonitor/snapshots/test_sensor.ambr index 952aaaa7ec2..3fe9ae7e809 100644 --- a/tests/components/systemmonitor/snapshots/test_sensor.ambr +++ b/tests/components/systemmonitor/snapshots/test_sensor.ambr @@ -3,7 +3,6 @@ ReadOnlyDict({ 'device_class': 'data_size', 'friendly_name': 'System Monitor Disk free /', - 'icon': 'mdi:harddisk', 'state_class': , 'unit_of_measurement': , }) @@ -15,7 +14,6 @@ ReadOnlyDict({ 'device_class': 'data_size', 'friendly_name': 'System Monitor Disk free /media/share', - 'icon': 'mdi:harddisk', 'state_class': , 'unit_of_measurement': , }) @@ -26,7 +24,6 @@ # name: test_sensor[System Monitor Disk usage / - attributes] ReadOnlyDict({ 'friendly_name': 'System Monitor Disk usage /', - 'icon': 'mdi:harddisk', 'state_class': , 'unit_of_measurement': '%', }) @@ -37,7 +34,6 @@ # name: test_sensor[System Monitor Disk usage /home/notexist/ - attributes] ReadOnlyDict({ 'friendly_name': 'System Monitor Disk usage /home/notexist/', - 'icon': 'mdi:harddisk', 'state_class': , 'unit_of_measurement': '%', }) @@ -48,7 +44,6 @@ # name: test_sensor[System Monitor Disk usage /media/share - attributes] ReadOnlyDict({ 'friendly_name': 'System Monitor Disk usage /media/share', - 'icon': 'mdi:harddisk', 'state_class': , 'unit_of_measurement': '%', }) @@ -60,7 +55,6 @@ ReadOnlyDict({ 'device_class': 'data_size', 'friendly_name': 'System Monitor Disk use /', - 'icon': 'mdi:harddisk', 'state_class': , 'unit_of_measurement': , }) @@ -72,7 +66,6 @@ ReadOnlyDict({ 'device_class': 'data_size', 'friendly_name': 'System Monitor Disk use /media/share', - 'icon': 'mdi:harddisk', 'state_class': , 'unit_of_measurement': , }) @@ -83,7 +76,6 @@ # name: test_sensor[System Monitor IPv4 address eth0 - attributes] ReadOnlyDict({ 'friendly_name': 'System Monitor IPv4 address eth0', - 'icon': 'mdi:ip-network', }) # --- # name: test_sensor[System Monitor IPv4 address eth0 - state] @@ -92,7 +84,6 @@ # name: test_sensor[System Monitor IPv4 address eth1 - attributes] ReadOnlyDict({ 'friendly_name': 'System Monitor IPv4 address eth1', - 'icon': 'mdi:ip-network', }) # --- # name: test_sensor[System Monitor IPv4 address eth1 - state] @@ -101,16 +92,14 @@ # name: test_sensor[System Monitor IPv6 address eth0 - attributes] ReadOnlyDict({ 'friendly_name': 'System Monitor IPv6 address eth0', - 'icon': 'mdi:ip-network', }) # --- # name: test_sensor[System Monitor IPv6 address eth0 - state] - 'unknown' + '2a00:1f:2103:3a01:3333:2222:1111:0000' # --- # name: test_sensor[System Monitor IPv6 address eth1 - attributes] ReadOnlyDict({ 'friendly_name': 'System Monitor IPv6 address eth1', - 'icon': 'mdi:ip-network', }) # --- # name: test_sensor[System Monitor IPv6 address eth1 - state] @@ -159,7 +148,6 @@ ReadOnlyDict({ 'device_class': 'data_size', 'friendly_name': 'System Monitor Memory free', - 'icon': 'mdi:memory', 'state_class': , 'unit_of_measurement': , }) @@ -170,7 +158,6 @@ # name: test_sensor[System Monitor Memory usage - attributes] ReadOnlyDict({ 'friendly_name': 'System Monitor Memory usage', - 'icon': 'mdi:memory', 'state_class': , 'unit_of_measurement': '%', }) @@ -182,7 +169,6 @@ ReadOnlyDict({ 'device_class': 'data_size', 'friendly_name': 'System Monitor Memory use', - 'icon': 'mdi:memory', 'state_class': , 'unit_of_measurement': , }) @@ -194,7 +180,6 @@ ReadOnlyDict({ 'device_class': 'data_size', 'friendly_name': 'System Monitor Network in eth0', - 'icon': 'mdi:server-network', 'state_class': , 'unit_of_measurement': , }) @@ -206,7 +191,6 @@ ReadOnlyDict({ 'device_class': 'data_size', 'friendly_name': 'System Monitor Network in eth1', - 'icon': 'mdi:server-network', 'state_class': , 'unit_of_measurement': , }) @@ -218,7 +202,6 @@ ReadOnlyDict({ 'device_class': 'data_size', 'friendly_name': 'System Monitor Network out eth0', - 'icon': 'mdi:server-network', 'state_class': , 'unit_of_measurement': , }) @@ -230,7 +213,6 @@ ReadOnlyDict({ 'device_class': 'data_size', 'friendly_name': 'System Monitor Network out eth1', - 'icon': 'mdi:server-network', 'state_class': , 'unit_of_measurement': , }) @@ -285,7 +267,6 @@ # name: test_sensor[System Monitor Packets in eth0 - attributes] ReadOnlyDict({ 'friendly_name': 'System Monitor Packets in eth0', - 'icon': 'mdi:server-network', 'state_class': , }) # --- @@ -295,7 +276,6 @@ # name: test_sensor[System Monitor Packets in eth1 - attributes] ReadOnlyDict({ 'friendly_name': 'System Monitor Packets in eth1', - 'icon': 'mdi:server-network', 'state_class': , }) # --- @@ -305,7 +285,6 @@ # name: test_sensor[System Monitor Packets out eth0 - attributes] ReadOnlyDict({ 'friendly_name': 'System Monitor Packets out eth0', - 'icon': 'mdi:server-network', 'state_class': , }) # --- @@ -315,7 +294,6 @@ # name: test_sensor[System Monitor Packets out eth1 - attributes] ReadOnlyDict({ 'friendly_name': 'System Monitor Packets out eth1', - 'icon': 'mdi:server-network', 'state_class': , }) # --- @@ -366,7 +344,6 @@ ReadOnlyDict({ 'device_class': 'data_size', 'friendly_name': 'System Monitor Swap free', - 'icon': 'mdi:harddisk', 'state_class': , 'unit_of_measurement': , }) @@ -377,7 +354,6 @@ # name: test_sensor[System Monitor Swap usage - attributes] ReadOnlyDict({ 'friendly_name': 'System Monitor Swap usage', - 'icon': 'mdi:harddisk', 'state_class': , 'unit_of_measurement': '%', }) @@ -389,7 +365,6 @@ ReadOnlyDict({ 'device_class': 'data_size', 'friendly_name': 'System Monitor Swap use', - 'icon': 'mdi:harddisk', 'state_class': , 'unit_of_measurement': , }) diff --git a/tests/components/systemmonitor/test_binary_sensor.py b/tests/components/systemmonitor/test_binary_sensor.py index 650f89c7566..e3fbdedc081 100644 --- a/tests/components/systemmonitor/test_binary_sensor.py +++ b/tests/components/systemmonitor/test_binary_sensor.py @@ -1,4 +1,5 @@ """Test System Monitor binary sensor.""" + from datetime import timedelta from unittest.mock import Mock, patch @@ -96,7 +97,7 @@ async def test_sensor_process_fails( freezer.tick(timedelta(minutes=1)) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) process_sensor = hass.states.get("binary_sensor.system_monitor_process_python3") assert process_sensor is not None diff --git a/tests/components/systemmonitor/test_config_flow.py b/tests/components/systemmonitor/test_config_flow.py index 2536f847b43..eb6f5778805 100644 --- a/tests/components/systemmonitor/test_config_flow.py +++ b/tests/components/systemmonitor/test_config_flow.py @@ -1,4 +1,5 @@ """Test the System Monitor config flow.""" + from __future__ import annotations from unittest.mock import AsyncMock diff --git a/tests/components/systemmonitor/test_diagnostics.py b/tests/components/systemmonitor/test_diagnostics.py index b50f1aa16b2..78128aad5f4 100644 --- a/tests/components/systemmonitor/test_diagnostics.py +++ b/tests/components/systemmonitor/test_diagnostics.py @@ -1,4 +1,5 @@ """Tests for the diagnostics data provided by the System Monitor integration.""" + from unittest.mock import Mock from syrupy import SnapshotAssertion diff --git a/tests/components/systemmonitor/test_init.py b/tests/components/systemmonitor/test_init.py index 3cba655f6bf..2142eecb8b4 100644 --- a/tests/components/systemmonitor/test_init.py +++ b/tests/components/systemmonitor/test_init.py @@ -1,4 +1,5 @@ """Test for System Monitor init.""" + from __future__ import annotations from unittest.mock import Mock diff --git a/tests/components/systemmonitor/test_sensor.py b/tests/components/systemmonitor/test_sensor.py index 4c0c5e179b1..a11112d8f86 100644 --- a/tests/components/systemmonitor/test_sensor.py +++ b/tests/components/systemmonitor/test_sensor.py @@ -60,7 +60,6 @@ async def test_sensor( "state_class": "measurement", "unit_of_measurement": "MiB", "device_class": "data_size", - "icon": "mdi:memory", "friendly_name": "System Monitor Memory free", } @@ -233,7 +232,7 @@ async def test_sensor_updating( mock_psutil.virtual_memory.side_effect = Exception("Failed to update") freezer.tick(timedelta(minutes=1)) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) memory_sensor = hass.states.get("sensor.system_monitor_memory_free") assert memory_sensor is not None @@ -249,7 +248,7 @@ async def test_sensor_updating( ) freezer.tick(timedelta(minutes=1)) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) memory_sensor = hass.states.get("sensor.system_monitor_memory_free") assert memory_sensor is not None @@ -294,7 +293,7 @@ async def test_sensor_process_fails( freezer.tick(timedelta(minutes=1)) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) process_sensor = hass.states.get("sensor.system_monitor_process_python3") assert process_sensor is not None @@ -331,7 +330,7 @@ async def test_sensor_network_sensors( freezer.tick(timedelta(minutes=1)) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) network_out_sensor = hass.states.get("sensor.system_monitor_network_out_eth1") packets_out_sensor = hass.states.get("sensor.system_monitor_packets_out_eth1") @@ -363,7 +362,7 @@ async def test_sensor_network_sensors( freezer.tick(timedelta(minutes=1)) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) network_out_sensor = hass.states.get("sensor.system_monitor_network_out_eth1") packets_out_sensor = hass.states.get("sensor.system_monitor_packets_out_eth1") @@ -471,12 +470,9 @@ async def test_exception_handling_disk_sensor( freezer.tick(timedelta(minutes=1)) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) - assert ( - "Error fetching System Monitor Disk / coordinator data: OS error for /" - in caplog.text - ) + assert "OS error for /" in caplog.text disk_sensor = hass.states.get("sensor.system_monitor_disk_free") assert disk_sensor is not None @@ -487,12 +483,9 @@ async def test_exception_handling_disk_sensor( freezer.tick(timedelta(minutes=1)) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) - assert ( - "Error fetching System Monitor Disk / coordinator data: OS error for /" - in caplog.text - ) + assert "OS error for /" in caplog.text disk_sensor = hass.states.get("sensor.system_monitor_disk_free") assert disk_sensor is not None @@ -505,7 +498,7 @@ async def test_exception_handling_disk_sensor( freezer.tick(timedelta(minutes=1)) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) disk_sensor = hass.states.get("sensor.system_monitor_disk_free") assert disk_sensor is not None @@ -535,7 +528,7 @@ async def test_cpu_percentage_is_zero_returns_unknown( freezer.tick(timedelta(minutes=1)) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) cpu_sensor = hass.states.get("sensor.system_monitor_processor_use") assert cpu_sensor is not None @@ -545,7 +538,7 @@ async def test_cpu_percentage_is_zero_returns_unknown( freezer.tick(timedelta(minutes=1)) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) cpu_sensor = hass.states.get("sensor.system_monitor_processor_use") assert cpu_sensor is not None @@ -580,7 +573,7 @@ async def test_remove_obsolete_entities( ) freezer.tick(timedelta(minutes=5)) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) # Fake an entity which should be removed as not supported and disabled entity_registry.async_get_or_create( diff --git a/tests/components/tado/test_binary_sensor.py b/tests/components/tado/test_binary_sensor.py index 1e2f53efeb5..78cd91c56c6 100644 --- a/tests/components/tado/test_binary_sensor.py +++ b/tests/components/tado/test_binary_sensor.py @@ -1,4 +1,5 @@ """The sensor tests for the tado platform.""" + from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant diff --git a/tests/components/tado/test_climate.py b/tests/components/tado/test_climate.py index 91bc1af191e..98fd2d753a4 100644 --- a/tests/components/tado/test_climate.py +++ b/tests/components/tado/test_climate.py @@ -1,4 +1,5 @@ """The sensor tests for the tado platform.""" + from homeassistant.core import HomeAssistant from .util import async_init_integration diff --git a/tests/components/tado/test_config_flow.py b/tests/components/tado/test_config_flow.py index ac04777dc1c..c2bbe4f37de 100644 --- a/tests/components/tado/test_config_flow.py +++ b/tests/components/tado/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Tado config flow.""" + from http import HTTPStatus from ipaddress import ip_address from unittest.mock import MagicMock, patch @@ -61,13 +62,16 @@ async def test_form_exceptions( # Test a retry to recover, upon failure mock_tado_api = _get_mock_tado_api(getMe={"homes": [{"id": 1, "name": "myhome"}]}) - with patch( - "homeassistant.components.tado.config_flow.Tado", - return_value=mock_tado_api, - ), patch( - "homeassistant.components.tado.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.tado.config_flow.Tado", + return_value=mock_tado_api, + ), + patch( + "homeassistant.components.tado.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"username": "test-username", "password": "test-password"}, @@ -128,13 +132,16 @@ async def test_create_entry(hass: HomeAssistant) -> None: mock_tado_api = _get_mock_tado_api(getMe={"homes": [{"id": 1, "name": "myhome"}]}) - with patch( - "homeassistant.components.tado.config_flow.Tado", - return_value=mock_tado_api, - ), patch( - "homeassistant.components.tado.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.tado.config_flow.Tado", + return_value=mock_tado_api, + ), + patch( + "homeassistant.components.tado.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"username": "test-username", "password": "test-password"}, @@ -267,13 +274,16 @@ async def test_import_step(hass: HomeAssistant) -> None: """Test import step.""" mock_tado_api = _get_mock_tado_api(getMe={"homes": [{"id": 1, "name": "myhome"}]}) - with patch( - "homeassistant.components.tado.config_flow.Tado", - return_value=mock_tado_api, - ), patch( - "homeassistant.components.tado.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.tado.config_flow.Tado", + return_value=mock_tado_api, + ), + patch( + "homeassistant.components.tado.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}, diff --git a/tests/components/tado/test_sensor.py b/tests/components/tado/test_sensor.py index 9bdc0614a2b..0fa7a9ca370 100644 --- a/tests/components/tado/test_sensor.py +++ b/tests/components/tado/test_sensor.py @@ -1,4 +1,5 @@ """The sensor tests for the tado platform.""" + from homeassistant.core import HomeAssistant from .util import async_init_integration diff --git a/tests/components/tado/test_service.py b/tests/components/tado/test_service.py index fe7e78f4ba5..759470cb5ea 100644 --- a/tests/components/tado/test_service.py +++ b/tests/components/tado/test_service.py @@ -1,4 +1,5 @@ """The serive tests for the tado platform.""" + import json from unittest.mock import patch diff --git a/tests/components/tado/test_water_heater.py b/tests/components/tado/test_water_heater.py index b552118df9d..223a1fda16a 100644 --- a/tests/components/tado/test_water_heater.py +++ b/tests/components/tado/test_water_heater.py @@ -1,4 +1,5 @@ """The sensor tests for the tado platform.""" + from homeassistant.core import HomeAssistant from .util import async_init_integration diff --git a/tests/components/tag/test_trigger.py b/tests/components/tag/test_trigger.py index 2780b928027..53c511b8594 100644 --- a/tests/components/tag/test_trigger.py +++ b/tests/components/tag/test_trigger.py @@ -1,4 +1,5 @@ """Tests for tag triggers.""" + import pytest import homeassistant.components.automation as automation diff --git a/tests/components/tailscale/conftest.py b/tests/components/tailscale/conftest.py index ec3b6afa139..5cf3f344739 100644 --- a/tests/components/tailscale/conftest.py +++ b/tests/components/tailscale/conftest.py @@ -1,4 +1,5 @@ """Fixtures for Tailscale integration tests.""" + from __future__ import annotations from collections.abc import Generator diff --git a/tests/components/tailscale/test_binary_sensor.py b/tests/components/tailscale/test_binary_sensor.py index b258abd1ed4..1d1cda84723 100644 --- a/tests/components/tailscale/test_binary_sensor.py +++ b/tests/components/tailscale/test_binary_sensor.py @@ -1,16 +1,12 @@ """Tests for the sensors provided by the Tailscale integration.""" + from homeassistant.components.binary_sensor import ( STATE_OFF, STATE_ON, BinarySensorDeviceClass, ) from homeassistant.components.tailscale.const import DOMAIN -from homeassistant.const import ( - ATTR_DEVICE_CLASS, - ATTR_FRIENDLY_NAME, - ATTR_ICON, - EntityCategory, -) +from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_FRIENDLY_NAME, EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er @@ -34,7 +30,6 @@ async def test_tailscale_binary_sensors( assert state.state == STATE_ON assert state.attributes.get(ATTR_FRIENDLY_NAME) == "frencks-iphone Client" assert state.attributes.get(ATTR_DEVICE_CLASS) == BinarySensorDeviceClass.UPDATE - assert ATTR_ICON not in state.attributes state = hass.states.get("binary_sensor.frencks_iphone_supports_hairpinning") entry = entity_registry.async_get( @@ -49,7 +44,6 @@ async def test_tailscale_binary_sensors( state.attributes.get(ATTR_FRIENDLY_NAME) == "frencks-iphone Supports hairpinning" ) - assert state.attributes.get(ATTR_ICON) == "mdi:wan" assert ATTR_DEVICE_CLASS not in state.attributes state = hass.states.get("binary_sensor.frencks_iphone_supports_ipv6") @@ -60,7 +54,6 @@ async def test_tailscale_binary_sensors( assert entry.entity_category == EntityCategory.DIAGNOSTIC assert state.state == STATE_OFF assert state.attributes.get(ATTR_FRIENDLY_NAME) == "frencks-iphone Supports IPv6" - assert state.attributes.get(ATTR_ICON) == "mdi:wan" assert ATTR_DEVICE_CLASS not in state.attributes state = hass.states.get("binary_sensor.frencks_iphone_supports_pcp") @@ -71,7 +64,6 @@ async def test_tailscale_binary_sensors( assert entry.entity_category == EntityCategory.DIAGNOSTIC assert state.state == STATE_OFF assert state.attributes.get(ATTR_FRIENDLY_NAME) == "frencks-iphone Supports PCP" - assert state.attributes.get(ATTR_ICON) == "mdi:wan" assert ATTR_DEVICE_CLASS not in state.attributes state = hass.states.get("binary_sensor.frencks_iphone_supports_nat_pmp") @@ -82,7 +74,6 @@ async def test_tailscale_binary_sensors( assert entry.entity_category == EntityCategory.DIAGNOSTIC assert state.state == STATE_OFF assert state.attributes.get(ATTR_FRIENDLY_NAME) == "frencks-iphone Supports NAT-PMP" - assert state.attributes.get(ATTR_ICON) == "mdi:wan" assert ATTR_DEVICE_CLASS not in state.attributes state = hass.states.get("binary_sensor.frencks_iphone_supports_udp") @@ -93,7 +84,6 @@ async def test_tailscale_binary_sensors( assert entry.entity_category == EntityCategory.DIAGNOSTIC assert state.state == STATE_ON assert state.attributes.get(ATTR_FRIENDLY_NAME) == "frencks-iphone Supports UDP" - assert state.attributes.get(ATTR_ICON) == "mdi:wan" assert ATTR_DEVICE_CLASS not in state.attributes state = hass.states.get("binary_sensor.frencks_iphone_supports_upnp") @@ -104,7 +94,6 @@ async def test_tailscale_binary_sensors( assert entry.entity_category == EntityCategory.DIAGNOSTIC assert state.state == STATE_OFF assert state.attributes.get(ATTR_FRIENDLY_NAME) == "frencks-iphone Supports UPnP" - assert state.attributes.get(ATTR_ICON) == "mdi:wan" assert ATTR_DEVICE_CLASS not in state.attributes assert entry.device_id diff --git a/tests/components/tailscale/test_diagnostics.py b/tests/components/tailscale/test_diagnostics.py index 4f900db7401..26ba611438c 100644 --- a/tests/components/tailscale/test_diagnostics.py +++ b/tests/components/tailscale/test_diagnostics.py @@ -1,4 +1,5 @@ """Tests for the diagnostics data provided by the Tailscale integration.""" + from syrupy import SnapshotAssertion from homeassistant.core import HomeAssistant diff --git a/tests/components/tailscale/test_init.py b/tests/components/tailscale/test_init.py index 11ca8a910a6..eeeaed57ba6 100644 --- a/tests/components/tailscale/test_init.py +++ b/tests/components/tailscale/test_init.py @@ -1,4 +1,5 @@ """Tests for the Tailscale integration.""" + from unittest.mock import MagicMock from tailscale import TailscaleAuthenticationError, TailscaleConnectionError diff --git a/tests/components/tailscale/test_sensor.py b/tests/components/tailscale/test_sensor.py index 1ea91d1b4b6..aa2bc6c472a 100644 --- a/tests/components/tailscale/test_sensor.py +++ b/tests/components/tailscale/test_sensor.py @@ -1,12 +1,8 @@ """Tests for the sensors provided by the Tailscale integration.""" + from homeassistant.components.sensor import SensorDeviceClass from homeassistant.components.tailscale.const import DOMAIN -from homeassistant.const import ( - ATTR_DEVICE_CLASS, - ATTR_FRIENDLY_NAME, - ATTR_ICON, - EntityCategory, -) +from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_FRIENDLY_NAME, EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er @@ -30,7 +26,6 @@ async def test_tailscale_sensors( assert state.state == "2022-02-25T09:49:06+00:00" assert state.attributes.get(ATTR_FRIENDLY_NAME) == "router Expires" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TIMESTAMP - assert ATTR_ICON not in state.attributes state = hass.states.get("sensor.router_last_seen") entry = entity_registry.async_get("sensor.router_last_seen") @@ -41,7 +36,6 @@ async def test_tailscale_sensors( 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_DEVICE_CLASS) == SensorDeviceClass.TIMESTAMP - assert ATTR_ICON not in state.attributes state = hass.states.get("sensor.router_ip_address") entry = entity_registry.async_get("sensor.router_ip_address") @@ -51,7 +45,6 @@ async def test_tailscale_sensors( 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_ICON) == "mdi:ip-network" assert ATTR_DEVICE_CLASS not in state.attributes assert entry.device_id diff --git a/tests/components/tailwind/conftest.py b/tests/components/tailwind/conftest.py index b39a3598a3e..b7443e59581 100644 --- a/tests/components/tailwind/conftest.py +++ b/tests/components/tailwind/conftest.py @@ -1,4 +1,5 @@ """Fixtures for the Tailwind integration tests.""" + from __future__ import annotations from collections.abc import Generator @@ -46,11 +47,14 @@ def mock_setup_entry() -> Generator[AsyncMock, None, None]: @pytest.fixture def mock_tailwind(device_fixture: str) -> Generator[MagicMock, None, None]: """Return a mocked Tailwind client.""" - with patch( - "homeassistant.components.tailwind.coordinator.Tailwind", autospec=True - ) as tailwind_mock, patch( - "homeassistant.components.tailwind.config_flow.Tailwind", - new=tailwind_mock, + with ( + patch( + "homeassistant.components.tailwind.coordinator.Tailwind", autospec=True + ) as tailwind_mock, + patch( + "homeassistant.components.tailwind.config_flow.Tailwind", + new=tailwind_mock, + ), ): tailwind = tailwind_mock.return_value tailwind.status.return_value = TailwindDeviceStatus.from_json( diff --git a/tests/components/tailwind/snapshots/test_binary_sensor.ambr b/tests/components/tailwind/snapshots/test_binary_sensor.ambr index 3f6f2baf26b..ea2a539363d 100644 --- a/tests/components/tailwind/snapshots/test_binary_sensor.ambr +++ b/tests/components/tailwind/snapshots/test_binary_sensor.ambr @@ -4,11 +4,11 @@ 'attributes': ReadOnlyDict({ 'device_class': 'problem', 'friendly_name': 'Door 1 Operational problem', - 'icon': 'mdi:garage-alert', }), 'context': , 'entity_id': 'binary_sensor.door_1_operational_problem', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }) @@ -36,7 +36,7 @@ 'options': dict({ }), 'original_device_class': , - 'original_icon': 'mdi:garage-alert', + 'original_icon': None, 'original_name': 'Operational problem', 'platform': 'tailwind', 'previous_unique_id': None, @@ -81,11 +81,11 @@ 'attributes': ReadOnlyDict({ 'device_class': 'problem', 'friendly_name': 'Door 2 Operational problem', - 'icon': 'mdi:garage-alert', }), 'context': , 'entity_id': 'binary_sensor.door_2_operational_problem', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }) @@ -113,7 +113,7 @@ 'options': dict({ }), 'original_device_class': , - 'original_icon': 'mdi:garage-alert', + 'original_icon': None, 'original_name': 'Operational problem', 'platform': 'tailwind', 'previous_unique_id': None, diff --git a/tests/components/tailwind/snapshots/test_button.ambr b/tests/components/tailwind/snapshots/test_button.ambr index f293b508808..560d3fe692c 100644 --- a/tests/components/tailwind/snapshots/test_button.ambr +++ b/tests/components/tailwind/snapshots/test_button.ambr @@ -8,6 +8,7 @@ 'context': , 'entity_id': 'button.tailwind_iq3_identify', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }) diff --git a/tests/components/tailwind/snapshots/test_cover.ambr b/tests/components/tailwind/snapshots/test_cover.ambr index d349c555945..0ecd172b2ca 100644 --- a/tests/components/tailwind/snapshots/test_cover.ambr +++ b/tests/components/tailwind/snapshots/test_cover.ambr @@ -9,6 +9,7 @@ 'context': , 'entity_id': 'cover.door_1', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'open', }) @@ -86,6 +87,7 @@ 'context': , 'entity_id': 'cover.door_2', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'open', }) diff --git a/tests/components/tailwind/snapshots/test_number.ambr b/tests/components/tailwind/snapshots/test_number.ambr index 95237f82522..cbd61d31a6c 100644 --- a/tests/components/tailwind/snapshots/test_number.ambr +++ b/tests/components/tailwind/snapshots/test_number.ambr @@ -3,7 +3,6 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Tailwind iQ3 Status LED brightness', - 'icon': 'mdi:led-on', 'max': 100, 'min': 0, 'mode': , @@ -13,6 +12,7 @@ 'context': , 'entity_id': 'number.tailwind_iq3_status_led_brightness', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '100', }) @@ -45,7 +45,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:led-on', + 'original_icon': None, 'original_name': 'Status LED brightness', 'platform': 'tailwind', 'previous_unique_id': None, diff --git a/tests/components/tailwind/test_button.py b/tests/components/tailwind/test_button.py index a0128d5f498..55c0e3f5a9e 100644 --- a/tests/components/tailwind/test_button.py +++ b/tests/components/tailwind/test_button.py @@ -1,4 +1,5 @@ """Tests for button entities provided by the Tailwind integration.""" + from unittest.mock import MagicMock from gotailwind import TailwindError diff --git a/tests/components/tailwind/test_config_flow.py b/tests/components/tailwind/test_config_flow.py index 341f977d350..efd828dcbde 100644 --- a/tests/components/tailwind/test_config_flow.py +++ b/tests/components/tailwind/test_config_flow.py @@ -1,4 +1,5 @@ """Configuration flow tests for the Tailwind integration.""" + from ipaddress import ip_address from unittest.mock import MagicMock diff --git a/tests/components/tailwind/test_cover.py b/tests/components/tailwind/test_cover.py index 9620d6149b7..8ccb8947624 100644 --- a/tests/components/tailwind/test_cover.py +++ b/tests/components/tailwind/test_cover.py @@ -1,4 +1,5 @@ """Tests for cover entities provided by the Tailwind integration.""" + from unittest.mock import ANY, MagicMock from gotailwind import ( @@ -85,7 +86,7 @@ async def test_cover_operations( # Test door disabled error handling mock_tailwind.operate.side_effect = TailwindDoorDisabledError("Door disabled") - with pytest.raises(HomeAssistantError, match="Door disabled") as excinfo: + with pytest.raises(HomeAssistantError) as excinfo: await hass.services.async_call( COVER_DOMAIN, SERVICE_OPEN_COVER, @@ -94,11 +95,12 @@ async def test_cover_operations( }, blocking=True, ) + assert str(excinfo.value) == "The door is disabled and cannot be operated" assert excinfo.value.translation_domain == DOMAIN assert excinfo.value.translation_key == "door_disabled" - with pytest.raises(HomeAssistantError, match="Door disabled") as excinfo: + with pytest.raises(HomeAssistantError) as excinfo: await hass.services.async_call( COVER_DOMAIN, SERVICE_CLOSE_COVER, @@ -108,13 +110,14 @@ async def test_cover_operations( blocking=True, ) + assert str(excinfo.value) == "The door is disabled and cannot be operated" assert excinfo.value.translation_domain == DOMAIN assert excinfo.value.translation_key == "door_disabled" # Test door locked out error handling mock_tailwind.operate.side_effect = TailwindDoorLockedOutError("Door locked out") - with pytest.raises(HomeAssistantError, match="Door locked out") as excinfo: + with pytest.raises(HomeAssistantError) as excinfo: await hass.services.async_call( COVER_DOMAIN, SERVICE_OPEN_COVER, @@ -124,10 +127,11 @@ async def test_cover_operations( blocking=True, ) + assert str(excinfo.value) == "The door is locked out and cannot be operated" assert excinfo.value.translation_domain == DOMAIN assert excinfo.value.translation_key == "door_locked_out" - with pytest.raises(HomeAssistantError, match="Door locked out") as excinfo: + with pytest.raises(HomeAssistantError) as excinfo: await hass.services.async_call( COVER_DOMAIN, SERVICE_CLOSE_COVER, @@ -137,13 +141,14 @@ async def test_cover_operations( blocking=True, ) + assert str(excinfo.value) == "The door is locked out and cannot be operated" assert excinfo.value.translation_domain == DOMAIN assert excinfo.value.translation_key == "door_locked_out" # Test door error handling mock_tailwind.operate.side_effect = TailwindError("Some error") - with pytest.raises(HomeAssistantError, match="Some error") as excinfo: + with pytest.raises(HomeAssistantError) as excinfo: await hass.services.async_call( COVER_DOMAIN, SERVICE_OPEN_COVER, @@ -153,10 +158,14 @@ async def test_cover_operations( blocking=True, ) + assert ( + str(excinfo.value) + == "An error occurred while communicating with the Tailwind device" + ) assert excinfo.value.translation_domain == DOMAIN assert excinfo.value.translation_key == "communication_error" - with pytest.raises(HomeAssistantError, match="Some error") as excinfo: + with pytest.raises(HomeAssistantError) as excinfo: await hass.services.async_call( COVER_DOMAIN, SERVICE_CLOSE_COVER, @@ -166,5 +175,9 @@ async def test_cover_operations( blocking=True, ) + assert ( + str(excinfo.value) + == "An error occurred while communicating with the Tailwind device" + ) assert excinfo.value.translation_domain == DOMAIN assert excinfo.value.translation_key == "communication_error" diff --git a/tests/components/tailwind/test_init.py b/tests/components/tailwind/test_init.py index fb61d155008..8ea5f1108f4 100644 --- a/tests/components/tailwind/test_init.py +++ b/tests/components/tailwind/test_init.py @@ -1,4 +1,5 @@ """Integration tests for the Tailwind integration.""" + from unittest.mock import MagicMock from gotailwind import TailwindAuthenticationError, TailwindConnectionError diff --git a/tests/components/tailwind/test_number.py b/tests/components/tailwind/test_number.py index e16c940b85d..4e6736fc831 100644 --- a/tests/components/tailwind/test_number.py +++ b/tests/components/tailwind/test_number.py @@ -1,4 +1,5 @@ """Tests for number entities provided by the Tailwind integration.""" + from unittest.mock import MagicMock from gotailwind import TailwindError @@ -51,7 +52,7 @@ async def test_number_entities( # Test error handling mock_tailwind.status_led.side_effect = TailwindError("Some error") - with pytest.raises(HomeAssistantError, match="Some error") as excinfo: + with pytest.raises(HomeAssistantError) as excinfo: await hass.services.async_call( number.DOMAIN, SERVICE_SET_VALUE, @@ -62,5 +63,9 @@ async def test_number_entities( blocking=True, ) + assert ( + str(excinfo.value) + == "An error occurred while communicating with the Tailwind device" + ) assert excinfo.value.translation_domain == DOMAIN assert excinfo.value.translation_key == "communication_error" diff --git a/tests/components/tami4/test_init.py b/tests/components/tami4/test_init.py index ad3f50a377e..62e5861e13c 100644 --- a/tests/components/tami4/test_init.py +++ b/tests/components/tami4/test_init.py @@ -1,4 +1,5 @@ """Test the Tami4 component.""" + import pytest from Tami4EdgeAPI import exceptions diff --git a/tests/components/tankerkoenig/conftest.py b/tests/components/tankerkoenig/conftest.py index 011dcf5e7bd..4400082a45f 100644 --- a/tests/components/tankerkoenig/conftest.py +++ b/tests/components/tankerkoenig/conftest.py @@ -1,4 +1,5 @@ """Fixtures for Tankerkoenig integration tests.""" + from collections.abc import Generator from unittest.mock import AsyncMock, patch @@ -26,12 +27,15 @@ from tests.common import MockConfigEntry @pytest.fixture(name="tankerkoenig") def mock_tankerkoenig() -> Generator[AsyncMock, None, None]: """Mock the aiotankerkoenig client.""" - with patch( - "homeassistant.components.tankerkoenig.coordinator.Tankerkoenig", - autospec=True, - ) as mock_tankerkoenig, patch( - "homeassistant.components.tankerkoenig.config_flow.Tankerkoenig", - new=mock_tankerkoenig, + with ( + patch( + "homeassistant.components.tankerkoenig.coordinator.Tankerkoenig", + autospec=True, + ) as mock_tankerkoenig, + patch( + "homeassistant.components.tankerkoenig.config_flow.Tankerkoenig", + new=mock_tankerkoenig, + ), ): mock = mock_tankerkoenig.return_value mock.station_details.return_value = STATION diff --git a/tests/components/tankerkoenig/test_config_flow.py b/tests/components/tankerkoenig/test_config_flow.py index db3d0aac222..b954598c12a 100644 --- a/tests/components/tankerkoenig/test_config_flow.py +++ b/tests/components/tankerkoenig/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for Tankerkoenig config flow.""" + from unittest.mock import patch from aiotankerkoenig.exceptions import TankerkoenigInvalidKeyError @@ -58,11 +59,14 @@ async def test_user(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" - with patch( - "homeassistant.components.tankerkoenig.async_setup_entry", return_value=True - ) as mock_setup_entry, patch( - "homeassistant.components.tankerkoenig.config_flow.Tankerkoenig.nearby_stations", - return_value=NEARBY_STATIONS, + with ( + patch( + "homeassistant.components.tankerkoenig.async_setup_entry", return_value=True + ) as mock_setup_entry, + patch( + "homeassistant.components.tankerkoenig.config_flow.Tankerkoenig.nearby_stations", + return_value=NEARBY_STATIONS, + ), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_DATA @@ -158,11 +162,14 @@ async def test_reauth(hass: HomeAssistant, config_entry: MockConfigEntry) -> Non """Test starting a flow by user to re-auth.""" config_entry.add_to_hass(hass) - with patch( - "homeassistant.components.tankerkoenig.async_setup_entry", return_value=True - ) as mock_setup_entry, patch( - "homeassistant.components.tankerkoenig.config_flow.Tankerkoenig.nearby_stations", - ) as mock_nearby_stations: + with ( + patch( + "homeassistant.components.tankerkoenig.async_setup_entry", return_value=True + ) as mock_setup_entry, + patch( + "homeassistant.components.tankerkoenig.config_flow.Tankerkoenig.nearby_stations", + ) as mock_nearby_stations, + ): # re-auth initialized result = await hass.config_entries.flow.async_init( DOMAIN, diff --git a/tests/components/tankerkoenig/test_coordinator.py b/tests/components/tankerkoenig/test_coordinator.py index 650fa5a18ac..5a33cb95dd9 100644 --- a/tests/components/tankerkoenig/test_coordinator.py +++ b/tests/components/tankerkoenig/test_coordinator.py @@ -1,16 +1,23 @@ """Tests for the Tankerkoening integration.""" + from __future__ import annotations from datetime import timedelta from unittest.mock import AsyncMock -from aiotankerkoenig.exceptions import TankerkoenigRateLimitError +from aiotankerkoenig.exceptions import ( + TankerkoenigConnectionError, + TankerkoenigError, + TankerkoenigInvalidKeyError, + TankerkoenigRateLimitError, +) import pytest -from homeassistant.components.tankerkoenig.const import DEFAULT_SCAN_INTERVAL +from homeassistant.components.tankerkoenig.const import DEFAULT_SCAN_INTERVAL, DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.const import STATE_UNAVAILABLE from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from tests.common import MockConfigEntry, async_fire_time_changed @@ -49,3 +56,68 @@ async def test_rate_limit( state = hass.states.get("binary_sensor.station_somewhere_street_1_status") assert state assert state.state == "on" + + +@pytest.mark.parametrize( + ("exception", "expected_log"), + [ + ( + TankerkoenigInvalidKeyError, + "invalid key error occur during update of stations", + ), + ( + TankerkoenigRateLimitError, + "API rate limit reached, consider to increase polling interval", + ), + (TankerkoenigConnectionError, "error occur during update of stations"), + (TankerkoenigError, "error occur during update of stations"), + ], +) +@pytest.mark.usefixtures("setup_integration") +async def test_update_exception_logging( + hass: HomeAssistant, + config_entry: MockConfigEntry, + tankerkoenig: AsyncMock, + caplog: pytest.LogCaptureFixture, + exception: None, + expected_log: str, +) -> None: + """Test log messages about exceptions during update.""" + tankerkoenig.prices.side_effect = exception + async_fire_time_changed( + hass, dt_util.utcnow() + timedelta(minutes=DEFAULT_SCAN_INTERVAL) + ) + await hass.async_block_till_done() + assert expected_log in caplog.text + state = hass.states.get("binary_sensor.station_somewhere_street_1_status") + assert state + assert state.state == STATE_UNAVAILABLE + + +@pytest.mark.parametrize( + ("exception", "expected_log"), + [ + ( + TankerkoenigInvalidKeyError, + "invalid key error occur during setup of station", + ), + (TankerkoenigConnectionError, "connection error occur during setup of station"), + (TankerkoenigError, "Error when adding station"), + ], +) +async def test_setup_exception_logging( + hass: HomeAssistant, + config_entry: MockConfigEntry, + tankerkoenig: AsyncMock, + caplog: pytest.LogCaptureFixture, + exception: None, + expected_log: str, +) -> None: + """Test log messages about exceptions during setup.""" + config_entry.add_to_hass(hass) + tankerkoenig.station_details.side_effect = exception + + assert await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + + assert expected_log in caplog.text diff --git a/tests/components/tankerkoenig/test_diagnostics.py b/tests/components/tankerkoenig/test_diagnostics.py index 8d7137c503a..441268659f3 100644 --- a/tests/components/tankerkoenig/test_diagnostics.py +++ b/tests/components/tankerkoenig/test_diagnostics.py @@ -1,4 +1,5 @@ """Tests for the Tankerkoening integration.""" + from __future__ import annotations import pytest diff --git a/tests/components/tasmota/conftest.py b/tests/components/tasmota/conftest.py index aa73e4d9994..1bb1f085e91 100644 --- a/tests/components/tasmota/conftest.py +++ b/tests/components/tasmota/conftest.py @@ -1,4 +1,5 @@ """Test fixtures for Tasmota component.""" + from unittest.mock import patch from hatasmota.discovery import get_status_sensor_entities diff --git a/tests/components/tasmota/test_binary_sensor.py b/tests/components/tasmota/test_binary_sensor.py index d5f1e4d7101..5abb9ab9bf2 100644 --- a/tests/components/tasmota/test_binary_sensor.py +++ b/tests/components/tasmota/test_binary_sensor.py @@ -1,4 +1,5 @@ """The tests for the Tasmota binary sensor platform.""" + import copy from datetime import timedelta import json diff --git a/tests/components/tasmota/test_common.py b/tests/components/tasmota/test_common.py index 1f414cb4e5a..360794e280f 100644 --- a/tests/components/tasmota/test_common.py +++ b/tests/components/tasmota/test_common.py @@ -1,4 +1,5 @@ """Common test objects.""" + import copy import json from unittest.mock import ANY diff --git a/tests/components/tasmota/test_config_flow.py b/tests/components/tasmota/test_config_flow.py index 514ddaf72ff..2db4d7c6493 100644 --- a/tests/components/tasmota/test_config_flow.py +++ b/tests/components/tasmota/test_config_flow.py @@ -1,4 +1,5 @@ """Test config flow.""" + from homeassistant import config_entries from homeassistant.core import HomeAssistant from homeassistant.helpers.service_info.mqtt import MqttServiceInfo diff --git a/tests/components/tasmota/test_cover.py b/tests/components/tasmota/test_cover.py index 26f8dee4a9d..7da3cdbd1ec 100644 --- a/tests/components/tasmota/test_cover.py +++ b/tests/components/tasmota/test_cover.py @@ -1,4 +1,5 @@ """The tests for the Tasmota cover platform.""" + import copy import json from unittest.mock import patch @@ -296,7 +297,7 @@ async def test_controlling_state_via_mqtt_tilt( assert state.attributes["current_position"] == 100 -@pytest.mark.parametrize("tilt", ("", ',"Tilt":0')) +@pytest.mark.parametrize("tilt", ["", ',"Tilt":0']) async def test_controlling_state_via_mqtt_inverted( hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_tasmota, tilt ) -> None: diff --git a/tests/components/tasmota/test_device_trigger.py b/tests/components/tasmota/test_device_trigger.py index 190c56b33f6..a5d30814b38 100644 --- a/tests/components/tasmota/test_device_trigger.py +++ b/tests/components/tasmota/test_device_trigger.py @@ -1,4 +1,5 @@ """The tests for Tasmota device triggers.""" + import copy import json from unittest.mock import Mock, patch diff --git a/tests/components/tasmota/test_discovery.py b/tests/components/tasmota/test_discovery.py index 4fd9f293498..122c22f752e 100644 --- a/tests/components/tasmota/test_discovery.py +++ b/tests/components/tasmota/test_discovery.py @@ -1,4 +1,5 @@ """The tests for the MQTT discovery.""" + import copy import json from unittest.mock import ANY, patch diff --git a/tests/components/tasmota/test_fan.py b/tests/components/tasmota/test_fan.py index 727fddc9bd3..654b8c955d2 100644 --- a/tests/components/tasmota/test_fan.py +++ b/tests/components/tasmota/test_fan.py @@ -1,4 +1,5 @@ """The tests for the Tasmota fan platform.""" + import copy import json from unittest.mock import patch diff --git a/tests/components/tasmota/test_init.py b/tests/components/tasmota/test_init.py index 09467b893e0..95fb186a46d 100644 --- a/tests/components/tasmota/test_init.py +++ b/tests/components/tasmota/test_init.py @@ -1,4 +1,5 @@ """The tests for the Tasmota binary sensor platform.""" + import copy import json from unittest.mock import call diff --git a/tests/components/tasmota/test_light.py b/tests/components/tasmota/test_light.py index 50f11fb7757..c4c3f0ec8dc 100644 --- a/tests/components/tasmota/test_light.py +++ b/tests/components/tasmota/test_light.py @@ -1,4 +1,5 @@ """The tests for the Tasmota light platform.""" + import copy import json from unittest.mock import patch diff --git a/tests/components/tasmota/test_mixins.py b/tests/components/tasmota/test_mixins.py index aad0d0e723a..86696a0813d 100644 --- a/tests/components/tasmota/test_mixins.py +++ b/tests/components/tasmota/test_mixins.py @@ -1,4 +1,5 @@ """The tests for the Tasmota mixins.""" + import copy import json from unittest.mock import call diff --git a/tests/components/tasmota/test_sensor.py b/tests/components/tasmota/test_sensor.py index dc4820779a6..61034ae66e9 100644 --- a/tests/components/tasmota/test_sensor.py +++ b/tests/components/tasmota/test_sensor.py @@ -1,4 +1,5 @@ """The tests for the Tasmota sensor platform.""" + import copy import datetime from datetime import timedelta diff --git a/tests/components/tasmota/test_switch.py b/tests/components/tasmota/test_switch.py index 1a16f372fc9..1ab5a50d3f5 100644 --- a/tests/components/tasmota/test_switch.py +++ b/tests/components/tasmota/test_switch.py @@ -1,4 +1,5 @@ """The tests for the Tasmota switch platform.""" + import copy import json from unittest.mock import patch diff --git a/tests/components/tautulli/test_config_flow.py b/tests/components/tautulli/test_config_flow.py index 0ca2d0438a7..e51fbfbad0d 100644 --- a/tests/components/tautulli/test_config_flow.py +++ b/tests/components/tautulli/test_config_flow.py @@ -1,4 +1,5 @@ """Test Tautulli config flow.""" + from unittest.mock import AsyncMock, patch from pytautulli import exceptions @@ -170,9 +171,10 @@ async def test_flow_reauth( new_conf = {CONF_API_KEY: "efgh"} CONF_DATA[CONF_API_KEY] = "efgh" - with patch_config_flow_tautulli(AsyncMock()), patch( - "homeassistant.components.tautulli.async_setup_entry" - ) as mock_entry: + with ( + patch_config_flow_tautulli(AsyncMock()), + patch("homeassistant.components.tautulli.async_setup_entry") as mock_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=new_conf, diff --git a/tests/components/tcp/test_binary_sensor.py b/tests/components/tcp/test_binary_sensor.py index 3a4b8c490c7..05aa2a471db 100644 --- a/tests/components/tcp/test_binary_sensor.py +++ b/tests/components/tcp/test_binary_sensor.py @@ -1,4 +1,5 @@ """The tests for the TCP binary sensor platform.""" + from datetime import timedelta from unittest.mock import call, patch @@ -21,11 +22,12 @@ TEST_ENTITY = "binary_sensor.test_name" @pytest.fixture(name="mock_socket") def mock_socket_fixture(): """Mock the socket.""" - with patch( - "homeassistant.components.tcp.common.socket.socket" - ) as mock_socket, patch( - "homeassistant.components.tcp.common.select.select", - return_value=(True, False, False), + with ( + patch("homeassistant.components.tcp.common.socket.socket") as mock_socket, + patch( + "homeassistant.components.tcp.common.select.select", + return_value=(True, False, False), + ), ): # yield the return value of the socket context manager yield mock_socket.return_value.__enter__.return_value @@ -77,7 +79,7 @@ async def test_state(hass: HomeAssistant, mock_socket, now) -> None: mock_socket.recv.return_value = b"on" async_fire_time_changed(hass, now + timedelta(seconds=45)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(TEST_ENTITY) diff --git a/tests/components/tcp/test_sensor.py b/tests/components/tcp/test_sensor.py index cade92f3372..04fbb2c667e 100644 --- a/tests/components/tcp/test_sensor.py +++ b/tests/components/tcp/test_sensor.py @@ -1,4 +1,5 @@ """The tests for the TCP sensor platform.""" + from copy import copy from unittest.mock import call, patch diff --git a/tests/components/technove/__init__.py b/tests/components/technove/__init__.py index 2d9f639244f..6afffc41e00 100644 --- a/tests/components/technove/__init__.py +++ b/tests/components/technove/__init__.py @@ -1,4 +1,5 @@ """Tests for the TechnoVE integration.""" + from unittest.mock import patch from homeassistant.const import Platform diff --git a/tests/components/technove/conftest.py b/tests/components/technove/conftest.py index b3921f865dc..06db6e24f47 100644 --- a/tests/components/technove/conftest.py +++ b/tests/components/technove/conftest.py @@ -1,4 +1,5 @@ """Fixtures for TechnoVE integration tests.""" + from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch @@ -50,10 +51,13 @@ def device_fixture() -> TechnoVEStation: @pytest.fixture def mock_technove(device_fixture: TechnoVEStation) -> Generator[MagicMock, None, None]: """Return a mocked TechnoVE client.""" - with patch( - "homeassistant.components.technove.coordinator.TechnoVE", autospec=True - ) as technove_mock, patch( - "homeassistant.components.technove.config_flow.TechnoVE", new=technove_mock + with ( + patch( + "homeassistant.components.technove.coordinator.TechnoVE", autospec=True + ) as technove_mock, + patch( + "homeassistant.components.technove.config_flow.TechnoVE", new=technove_mock + ), ): technove = technove_mock.return_value technove.update.return_value = device_fixture diff --git a/tests/components/technove/snapshots/test_binary_sensor.ambr b/tests/components/technove/snapshots/test_binary_sensor.ambr index f90e6e7b442..140526b9391 100644 --- a/tests/components/technove/snapshots/test_binary_sensor.ambr +++ b/tests/components/technove/snapshots/test_binary_sensor.ambr @@ -40,6 +40,7 @@ 'context': , 'entity_id': 'binary_sensor.technove_station_battery_protected', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }) @@ -86,6 +87,7 @@ 'context': , 'entity_id': 'binary_sensor.technove_station_charging', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }) @@ -131,6 +133,7 @@ 'context': , 'entity_id': 'binary_sensor.technove_station_conflict_with_power_sharing_mode', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }) @@ -176,6 +179,7 @@ 'context': , 'entity_id': 'binary_sensor.technove_station_power_sharing_mode', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }) @@ -221,6 +225,7 @@ 'context': , 'entity_id': 'binary_sensor.technove_station_static_ip', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }) @@ -267,6 +272,7 @@ 'context': , 'entity_id': 'binary_sensor.technove_station_update', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }) diff --git a/tests/components/technove/snapshots/test_sensor.ambr b/tests/components/technove/snapshots/test_sensor.ambr index 941d93107df..149155519d4 100644 --- a/tests/components/technove/snapshots/test_sensor.ambr +++ b/tests/components/technove/snapshots/test_sensor.ambr @@ -45,6 +45,7 @@ 'context': , 'entity_id': 'sensor.technove_station_current', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '23.75', }) @@ -95,6 +96,7 @@ 'context': , 'entity_id': 'sensor.technove_station_input_voltage', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '238', }) @@ -145,6 +147,7 @@ 'context': , 'entity_id': 'sensor.technove_station_last_session_energy_usage', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '12.34', }) @@ -195,6 +198,7 @@ 'context': , 'entity_id': 'sensor.technove_station_max_station_current', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '32', }) @@ -245,6 +249,7 @@ 'context': , 'entity_id': 'sensor.technove_station_output_voltage', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '238', }) @@ -295,6 +300,7 @@ 'context': , 'entity_id': 'sensor.technove_station_signal_strength', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '-82', }) @@ -356,6 +362,7 @@ 'context': , 'entity_id': 'sensor.technove_station_status', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'plugged_charging', }) @@ -406,6 +413,7 @@ 'context': , 'entity_id': 'sensor.technove_station_total_energy_usage', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '1234', }) @@ -433,7 +441,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:wifi', + 'original_icon': None, 'original_name': 'Wi-Fi network name', 'platform': 'technove', 'previous_unique_id': None, @@ -447,11 +455,11 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'TechnoVE Station Wi-Fi network name', - 'icon': 'mdi:wifi', }), 'context': , 'entity_id': 'sensor.technove_station_wi_fi_network_name', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'Connecting...', }) diff --git a/tests/components/technove/snapshots/test_switch.ambr b/tests/components/technove/snapshots/test_switch.ambr index 676646dd347..1a707971fc8 100644 --- a/tests/components/technove/snapshots/test_switch.ambr +++ b/tests/components/technove/snapshots/test_switch.ambr @@ -40,6 +40,7 @@ 'context': , 'entity_id': 'switch.technove_station_auto_charge', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }) diff --git a/tests/components/technove/test_binary_sensor.py b/tests/components/technove/test_binary_sensor.py index 5e168ce0760..0ee4f3f3db7 100644 --- a/tests/components/technove/test_binary_sensor.py +++ b/tests/components/technove/test_binary_sensor.py @@ -1,4 +1,5 @@ """Tests for the TechnoVE binary sensor platform.""" + from datetime import timedelta from unittest.mock import MagicMock @@ -42,7 +43,7 @@ async def test_sensors( @pytest.mark.parametrize( "entity_id", - ("binary_sensor.technove_station_static_ip",), + ["binary_sensor.technove_station_static_ip"], ) @pytest.mark.usefixtures("init_integration") async def test_disabled_by_default_binary_sensors( diff --git a/tests/components/technove/test_sensor.py b/tests/components/technove/test_sensor.py index c44aab8ecc4..9cf80a659eb 100644 --- a/tests/components/technove/test_sensor.py +++ b/tests/components/technove/test_sensor.py @@ -1,4 +1,5 @@ """Tests for the TechnoVE sensor platform.""" + from datetime import timedelta from unittest.mock import MagicMock @@ -44,10 +45,10 @@ async def test_sensors( @pytest.mark.parametrize( "entity_id", - ( + [ "sensor.technove_station_signal_strength", "sensor.technove_station_wi_fi_network_name", - ), + ], ) @pytest.mark.usefixtures("init_integration") async def test_disabled_by_default_sensors( diff --git a/tests/components/technove/test_switch.py b/tests/components/technove/test_switch.py index d0b709a4eda..b1a66607f66 100644 --- a/tests/components/technove/test_switch.py +++ b/tests/components/technove/test_switch.py @@ -1,4 +1,5 @@ """Tests for the TechnoVE switch platform.""" + from unittest.mock import MagicMock import pytest diff --git a/tests/components/tedee/conftest.py b/tests/components/tedee/conftest.py index 21fb4047ab3..9f0730992d2 100644 --- a/tests/components/tedee/conftest.py +++ b/tests/components/tedee/conftest.py @@ -1,4 +1,5 @@ """Fixtures for Tedee integration tests.""" + from __future__ import annotations from collections.abc import Generator @@ -42,11 +43,14 @@ def mock_setup_entry() -> Generator[AsyncMock, None, None]: @pytest.fixture def mock_tedee(request) -> Generator[MagicMock, None, None]: """Return a mocked Tedee client.""" - with patch( - "homeassistant.components.tedee.coordinator.TedeeClient", autospec=True - ) as tedee_mock, patch( - "homeassistant.components.tedee.config_flow.TedeeClient", - new=tedee_mock, + with ( + patch( + "homeassistant.components.tedee.coordinator.TedeeClient", autospec=True + ) as tedee_mock, + patch( + "homeassistant.components.tedee.config_flow.TedeeClient", + new=tedee_mock, + ), ): tedee = tedee_mock.return_value diff --git a/tests/components/tedee/snapshots/test_binary_sensor.ambr b/tests/components/tedee/snapshots/test_binary_sensor.ambr index 32820496d9f..8c9dca1bd12 100644 --- a/tests/components/tedee/snapshots/test_binary_sensor.ambr +++ b/tests/components/tedee/snapshots/test_binary_sensor.ambr @@ -107,6 +107,7 @@ 'context': , 'entity_id': 'binary_sensor.lock_1a2b_charging', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }) @@ -119,6 +120,7 @@ 'context': , 'entity_id': 'binary_sensor.lock_1a2b_pullspring_enabled', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }) @@ -131,6 +133,7 @@ 'context': , 'entity_id': 'binary_sensor.lock_1a2b_semi_locked', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }) diff --git a/tests/components/tedee/snapshots/test_lock.ambr b/tests/components/tedee/snapshots/test_lock.ambr index d232ab243d9..8e4fc464479 100644 --- a/tests/components/tedee/snapshots/test_lock.ambr +++ b/tests/components/tedee/snapshots/test_lock.ambr @@ -8,6 +8,7 @@ 'context': , 'entity_id': 'lock.lock_1a2b', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unlocked', }) @@ -84,6 +85,7 @@ 'context': , 'entity_id': 'lock.lock_2c3d', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unlocked', }) diff --git a/tests/components/tedee/snapshots/test_sensor.ambr b/tests/components/tedee/snapshots/test_sensor.ambr index d1be2839826..d5f4c8361c3 100644 --- a/tests/components/tedee/snapshots/test_sensor.ambr +++ b/tests/components/tedee/snapshots/test_sensor.ambr @@ -59,7 +59,7 @@ 'options': dict({ }), 'original_device_class': , - 'original_icon': 'mdi:timer-lock-open', + 'original_icon': None, 'original_name': 'Pullspring duration', 'platform': 'tedee', 'previous_unique_id': None, @@ -80,6 +80,7 @@ 'context': , 'entity_id': 'sensor.lock_1a2b_battery', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '70', }) @@ -89,13 +90,13 @@ 'attributes': ReadOnlyDict({ 'device_class': 'duration', 'friendly_name': 'Lock-1A2B Pullspring duration', - 'icon': 'mdi:timer-lock-open', 'state_class': , 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.lock_1a2b_pullspring_duration', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '2', }) diff --git a/tests/components/tedee/test_config_flow.py b/tests/components/tedee/test_config_flow.py index bc5b73aa4a9..6e8f02d04bc 100644 --- a/tests/components/tedee/test_config_flow.py +++ b/tests/components/tedee/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Tedee config flow.""" + from unittest.mock import MagicMock from pytedee_async import ( diff --git a/tests/components/tedee/test_diagnostics.py b/tests/components/tedee/test_diagnostics.py index 9a31e153b6c..1487645572f 100644 --- a/tests/components/tedee/test_diagnostics.py +++ b/tests/components/tedee/test_diagnostics.py @@ -1,4 +1,5 @@ """Tests for the diagnostics data provided by the Tedee integration.""" + from syrupy import SnapshotAssertion from homeassistant.core import HomeAssistant diff --git a/tests/components/tedee/test_init.py b/tests/components/tedee/test_init.py index ca64c01a983..9388aaf008c 100644 --- a/tests/components/tedee/test_init.py +++ b/tests/components/tedee/test_init.py @@ -1,4 +1,5 @@ """Test initialization of tedee.""" + from unittest.mock import MagicMock from pytedee_async.exception import TedeeAuthException, TedeeClientException diff --git a/tests/components/tedee/test_lock.py b/tests/components/tedee/test_lock.py index fca1ae2b07f..f108c4f09f0 100644 --- a/tests/components/tedee/test_lock.py +++ b/tests/components/tedee/test_lock.py @@ -1,4 +1,5 @@ """Tests for tedee lock.""" + from datetime import timedelta from unittest.mock import MagicMock diff --git a/tests/components/tedee/test_sensor.py b/tests/components/tedee/test_sensor.py index 274048082c0..72fbd9cbe8d 100644 --- a/tests/components/tedee/test_sensor.py +++ b/tests/components/tedee/test_sensor.py @@ -1,6 +1,5 @@ """Tests for the Tedee Sensors.""" - from datetime import timedelta from unittest.mock import MagicMock diff --git a/tests/components/telegram/test_notify.py b/tests/components/telegram/test_notify.py index 0b2943da152..ee13d8dc47c 100644 --- a/tests/components/telegram/test_notify.py +++ b/tests/components/telegram/test_notify.py @@ -1,4 +1,5 @@ """The tests for the telegram.notify platform.""" + from unittest.mock import patch from homeassistant import config as hass_config diff --git a/tests/components/telegram_bot/conftest.py b/tests/components/telegram_bot/conftest.py index af23efc1afc..0906b6afcbd 100644 --- a/tests/components/telegram_bot/conftest.py +++ b/tests/components/telegram_bot/conftest.py @@ -1,14 +1,21 @@ """Tests for the telegram_bot integration.""" + from unittest.mock import patch import pytest +from telegram import User from homeassistant.components.telegram_bot import ( CONF_ALLOWED_CHAT_IDS, CONF_TRUSTED_NETWORKS, DOMAIN, ) -from homeassistant.const import CONF_API_KEY, CONF_PLATFORM, CONF_URL +from homeassistant.const import ( + CONF_API_KEY, + CONF_PLATFORM, + CONF_URL, + EVENT_HOMEASSISTANT_START, +) from homeassistant.setup import async_setup_component @@ -55,12 +62,37 @@ def config_polling(): @pytest.fixture def mock_register_webhook(): """Mock calls made by telegram_bot when (de)registering webhook.""" - with patch( - "homeassistant.components.telegram_bot.webhooks.PushBot.register_webhook", - return_value=True, - ), patch( - "homeassistant.components.telegram_bot.webhooks.PushBot.deregister_webhook", - return_value=True, + with ( + patch( + "homeassistant.components.telegram_bot.webhooks.PushBot.register_webhook", + return_value=True, + ), + patch( + "homeassistant.components.telegram_bot.webhooks.PushBot.deregister_webhook", + return_value=True, + ), + ): + yield + + +@pytest.fixture +def mock_external_calls(): + """Mock calls that make calls to the live Telegram API.""" + test_user = User(123456, "Testbot", True) + with ( + patch( + "telegram.Bot.get_me", + return_value=test_user, + ), + patch( + "telegram.Bot._bot_user", + test_user, + ), + patch( + "telegram.Bot.bot", + test_user, + ), + patch("telegram.ext.Updater._bootstrap"), ): yield @@ -174,7 +206,11 @@ def update_callback_query(): @pytest.fixture async def webhook_platform( - hass, config_webhooks, mock_register_webhook, mock_generate_secret_token + hass, + config_webhooks, + mock_register_webhook, + mock_external_calls, + mock_generate_secret_token, ): """Fixture for setting up the webhooks platform using appropriate config and mocks.""" await async_setup_component( @@ -183,14 +219,18 @@ async def webhook_platform( config_webhooks, ) await hass.async_block_till_done() + yield + await hass.async_stop() @pytest.fixture -async def polling_platform(hass, config_polling): +async def polling_platform(hass, config_polling, mock_external_calls): """Fixture for setting up the polling platform using appropriate config and mocks.""" await async_setup_component( hass, DOMAIN, config_polling, ) + # Fire this event to start polling + hass.bus.fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() diff --git a/tests/components/telegram_bot/test_broadcast.py b/tests/components/telegram_bot/test_broadcast.py index a369754ae6a..b78054dc087 100644 --- a/tests/components/telegram_bot/test_broadcast.py +++ b/tests/components/telegram_bot/test_broadcast.py @@ -1,4 +1,5 @@ """Test Telegram broadcast.""" + from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component diff --git a/tests/components/telegram_bot/test_telegram_bot.py b/tests/components/telegram_bot/test_telegram_bot.py index be28f7be636..d6588535b4f 100644 --- a/tests/components/telegram_bot/test_telegram_bot.py +++ b/tests/components/telegram_bot/test_telegram_bot.py @@ -1,25 +1,18 @@ """Tests for the telegram_bot component.""" -import pytest + +from unittest.mock import AsyncMock, patch + from telegram import Update -from telegram.ext.dispatcher import Dispatcher from homeassistant.components.telegram_bot import DOMAIN, SERVICE_SEND_MESSAGE from homeassistant.components.telegram_bot.webhooks import TELEGRAM_WEBHOOK_URL from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component from tests.common import async_capture_events from tests.typing import ClientSessionGenerator -@pytest.fixture(autouse=True) -def clear_dispatcher(): - """Clear the singleton that telegram.ext.dispatcher.Dispatcher sets on itself.""" - yield - Dispatcher._set_singleton(None) - # This is how python-telegram-bot resets the dispatcher in their test suite - Dispatcher._Dispatcher__singleton_semaphore.release() - - async def test_webhook_platform_init(hass: HomeAssistant, webhook_platform) -> None: """Test initialization of the webhooks platform.""" assert hass.services.has_service(DOMAIN, SERVICE_SEND_MESSAGE) is True @@ -109,18 +102,38 @@ async def test_webhook_endpoint_generates_telegram_callback_event( async def test_polling_platform_message_text_update( - hass: HomeAssistant, polling_platform, update_message_text + hass: HomeAssistant, config_polling, update_message_text ) -> None: - """Provide the `PollBot`s `Dispatcher` with an `Update` and assert fired `telegram_text` event.""" + """Provide the `BaseTelegramBotEntity.update_handler` with an `Update` and assert fired `telegram_text` event.""" events = async_capture_events(hass, "telegram_text") - def telegram_dispatcher_callback(): - dispatcher = Dispatcher.get_instance() - update = Update.de_json(update_message_text, dispatcher.bot) - dispatcher.process_update(update) + with patch( + "homeassistant.components.telegram_bot.polling.ApplicationBuilder" + ) as application_builder_class: + await async_setup_component( + hass, + DOMAIN, + config_polling, + ) + await hass.async_block_till_done() + # Set up the integration with the polling platform inside the patch context manager. + application = ( + application_builder_class.return_value.bot.return_value.build.return_value + ) + # Then call the callback and assert events fired. + handler = application.add_handler.call_args[0][0] + handle_update_callback = handler.callback - # python-telegram-bots `Updater` uses threading, so we need to schedule its callback in a sync context. - await hass.async_add_executor_job(telegram_dispatcher_callback) + # Create Update object using library API. + application.bot.defaults.tzinfo = None + update = Update.de_json(update_message_text, application.bot) + + # handle_update_callback == BaseTelegramBotEntity.update_handler + await handle_update_callback(update, None) + + application.updater.stop = AsyncMock() + application.stop = AsyncMock() + application.shutdown = AsyncMock() # Make sure event has fired await hass.async_block_till_done() diff --git a/tests/components/tellduslive/test_config_flow.py b/tests/components/tellduslive/test_config_flow.py index 2d0a5fb2110..3cd157fd8b5 100644 --- a/tests/components/tellduslive/test_config_flow.py +++ b/tests/components/tellduslive/test_config_flow.py @@ -1,5 +1,5 @@ -# flake8: noqa pylint: skip-file """Tests for the TelldusLive config flow.""" + from unittest.mock import Mock, patch import pytest @@ -43,11 +43,12 @@ def authorize(): @pytest.fixture def mock_tellduslive(supports_local_api, authorize): """Mock tellduslive.""" - with patch( - "homeassistant.components.tellduslive.config_flow.Session" - ) as Session, patch( - "homeassistant.components.tellduslive.config_flow.supports_local_api" - ) as tellduslive_supports_local_api: + with ( + patch("homeassistant.components.tellduslive.config_flow.Session") as Session, + patch( + "homeassistant.components.tellduslive.config_flow.supports_local_api" + ) as tellduslive_supports_local_api, + ): tellduslive_supports_local_api.return_value = supports_local_api Session().authorize.return_value = authorize Session().access_token = "token" @@ -139,10 +140,13 @@ async def test_step_import_load_json_matching_host( """Test that we add host and trigger user when configuring from import.""" flow = init_config_flow(hass) - with patch( - "homeassistant.components.tellduslive.config_flow.load_json_object", - return_value={"tellduslive": {}}, - ), patch("os.path.isfile"): + with ( + patch( + "homeassistant.components.tellduslive.config_flow.load_json_object", + return_value={"tellduslive": {}}, + ), + patch("os.path.isfile"), + ): result = await flow.async_step_import( {CONF_HOST: "Cloud API", KEY_SCAN_INTERVAL: 0} ) @@ -154,10 +158,13 @@ async def test_step_import_load_json(hass: HomeAssistant, mock_tellduslive) -> N """Test that we create entry when configuring from import.""" flow = init_config_flow(hass) - with patch( - "homeassistant.components.tellduslive.config_flow.load_json_object", - return_value={"localhost": {}}, - ), patch("os.path.isfile"): + with ( + patch( + "homeassistant.components.tellduslive.config_flow.load_json_object", + return_value={"localhost": {}}, + ), + patch("os.path.isfile"), + ): result = await flow.async_step_import( {CONF_HOST: "localhost", KEY_SCAN_INTERVAL: SCAN_INTERVAL} ) diff --git a/tests/components/temper/test_sensor.py b/tests/components/temper/test_sensor.py index d195ff85c82..d1e74f1ab0f 100644 --- a/tests/components/temper/test_sensor.py +++ b/tests/components/temper/test_sensor.py @@ -1,4 +1,5 @@ """The tests for the temper (USB temperature sensor) component.""" + from datetime import timedelta from unittest.mock import Mock, patch @@ -28,7 +29,7 @@ async def test_temperature_readback(hass: HomeAssistant) -> None: await hass.async_block_till_done() async_fire_time_changed(hass, utcnow + timedelta(seconds=70)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) temperature = hass.states.get("sensor.mydevicename") assert temperature diff --git a/tests/components/template/conftest.py b/tests/components/template/conftest.py index 207a7c87886..894c1777fef 100644 --- a/tests/components/template/conftest.py +++ b/tests/components/template/conftest.py @@ -1,4 +1,5 @@ """template conftest.""" + import pytest from homeassistant.setup import async_setup_component diff --git a/tests/components/template/snapshots/test_binary_sensor.ambr b/tests/components/template/snapshots/test_binary_sensor.ambr index 2529021971a..e809c66e644 100644 --- a/tests/components/template/snapshots/test_binary_sensor.ambr +++ b/tests/components/template/snapshots/test_binary_sensor.ambr @@ -7,6 +7,7 @@ 'context': , 'entity_id': 'binary_sensor.my_template', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }) @@ -20,6 +21,7 @@ 'context': , 'entity_id': 'binary_sensor.my_template', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }) diff --git a/tests/components/template/snapshots/test_sensor.ambr b/tests/components/template/snapshots/test_sensor.ambr index 7959959dfa9..344761ae45a 100644 --- a/tests/components/template/snapshots/test_sensor.ambr +++ b/tests/components/template/snapshots/test_sensor.ambr @@ -7,6 +7,7 @@ 'context': , 'entity_id': 'sensor.my_template', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '30.0', }) @@ -22,6 +23,7 @@ 'context': , 'entity_id': 'sensor.my_template', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '30.0', }) diff --git a/tests/components/template/snapshots/test_weather.ambr b/tests/components/template/snapshots/test_weather.ambr index 0ee7f967176..9b0cf2b9471 100644 --- a/tests/components/template/snapshots/test_weather.ambr +++ b/tests/components/template/snapshots/test_weather.ambr @@ -55,22 +55,12 @@ # name: test_forecasts[config0-1-weather-get_forecast] dict({ 'forecast': list([ - dict({ - 'condition': 'cloudy', - 'datetime': '2023-02-17T14:00:00+00:00', - 'temperature': 14.2, - }), ]), }) # --- # name: test_forecasts[config0-1-weather-get_forecast].1 dict({ 'forecast': list([ - dict({ - 'condition': 'cloudy', - 'datetime': '2023-02-17T14:00:00+00:00', - 'temperature': 14.2, - }), ]), }) # --- @@ -89,11 +79,6 @@ # name: test_forecasts[config0-1-weather-get_forecast].3 dict({ 'forecast': list([ - dict({ - 'condition': 'cloudy', - 'datetime': '2023-02-17T14:00:00+00:00', - 'temperature': 16.9, - }), ]), }) # --- @@ -101,11 +86,6 @@ dict({ 'weather.forecast': dict({ 'forecast': list([ - dict({ - 'condition': 'cloudy', - 'datetime': '2023-02-17T14:00:00+00:00', - 'temperature': 14.2, - }), ]), }), }) @@ -114,11 +94,6 @@ dict({ 'weather.forecast': dict({ 'forecast': list([ - dict({ - 'condition': 'cloudy', - 'datetime': '2023-02-17T14:00:00+00:00', - 'temperature': 14.2, - }), ]), }), }) @@ -141,11 +116,6 @@ dict({ 'weather.forecast': dict({ 'forecast': list([ - dict({ - 'condition': 'cloudy', - 'datetime': '2023-02-17T14:00:00+00:00', - 'temperature': 16.9, - }), ]), }), }) diff --git a/tests/components/template/test_alarm_control_panel.py b/tests/components/template/test_alarm_control_panel.py index ef2390680b6..eb4daa3bcb8 100644 --- a/tests/components/template/test_alarm_control_panel.py +++ b/tests/components/template/test_alarm_control_panel.py @@ -1,4 +1,5 @@ """The tests for the Template alarm control panel platform.""" + import pytest from homeassistant.components.alarm_control_panel import DOMAIN as ALARM_DOMAIN diff --git a/tests/components/template/test_binary_sensor.py b/tests/components/template/test_binary_sensor.py index 708571ce913..452f926dca5 100644 --- a/tests/components/template/test_binary_sensor.py +++ b/tests/components/template/test_binary_sensor.py @@ -1,4 +1,5 @@ """The tests for the Template Binary sensor platform.""" + from datetime import UTC, datetime, timedelta import logging from unittest.mock import patch diff --git a/tests/components/template/test_button.py b/tests/components/template/test_button.py index ece568eee49..2e83100734a 100644 --- a/tests/components/template/test_button.py +++ b/tests/components/template/test_button.py @@ -1,4 +1,5 @@ """The tests for the Template button platform.""" + import datetime as dt from freezegun.api import FrozenDateTimeFactory diff --git a/tests/components/template/test_config_flow.py b/tests/components/template/test_config_flow.py index b95a68afd85..30d6942750c 100644 --- a/tests/components/template/test_config_flow.py +++ b/tests/components/template/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Switch config flow.""" + from typing import Any from unittest.mock import patch @@ -25,7 +26,7 @@ from tests.typing import WebSocketGenerator "extra_options", "extra_attrs", ), - ( + [ ( "binary_sensor", "{{ states('binary_sensor.one') == 'on' or states('binary_sensor.two') == 'on' }}", @@ -46,7 +47,7 @@ from tests.typing import WebSocketGenerator {}, {}, ), - ), + ], ) async def test_config_flow( hass: HomeAssistant, @@ -141,7 +142,7 @@ def get_suggested(schema, key): "extra_options", "options_options", ), - ( + [ ( "binary_sensor", "{{ states('binary_sensor.one') == 'on' or states('binary_sensor.two') == 'on' }}", @@ -160,7 +161,7 @@ def get_suggested(schema, key): {}, {}, ), - ), + ], ) async def test_options( hass: HomeAssistant, @@ -260,7 +261,7 @@ async def test_options( "extra_attributes", "listeners", ), - ( + [ ( "binary_sensor", "{{ states.binary_sensor.one.state == 'on' or states.binary_sensor.two.state == 'on' }}", @@ -279,7 +280,7 @@ async def test_options( [{}, {}], [["one", "two"], ["one", "two"]], ), - ), + ], ) async def test_config_flow_preview( hass: HomeAssistant, @@ -643,13 +644,13 @@ async def test_config_flow_preview_template_error( "state_template", "extra_user_input", ), - ( + [ ( "sensor", "{{ states('sensor.one') }}", {"unit_of_measurement": "°C"}, ), - ), + ], ) async def test_config_flow_preview_bad_state( hass: HomeAssistant, diff --git a/tests/components/template/test_cover.py b/tests/components/template/test_cover.py index 88f0fc366a3..e9a29fdc2e2 100644 --- a/tests/components/template/test_cover.py +++ b/tests/components/template/test_cover.py @@ -1,4 +1,5 @@ """The tests for the Template cover platform.""" + from typing import Any import pytest diff --git a/tests/components/template/test_fan.py b/tests/components/template/test_fan.py index ccdafebd8bb..93520b0f621 100644 --- a/tests/components/template/test_fan.py +++ b/tests/components/template/test_fan.py @@ -1,4 +1,5 @@ """The tests for the Template fan platform.""" + import pytest import voluptuous as vol @@ -104,20 +105,6 @@ async def test_missing_optional_config(hass: HomeAssistant, start_ha) -> None: }, } }, - { - DOMAIN: { - "platform": "template", - "fans": { - "platform": "template", - "fans": { - "test_fan": { - "value_template": "{{ 'on' }}", - "turn_on": {"service": "script.fan_on"}, - } - }, - }, - } - }, ], ) async def test_wrong_template_config(hass: HomeAssistant, start_ha) -> None: @@ -403,17 +390,17 @@ async def test_invalid_availability_template_keeps_component_available( async def test_on_off(hass: HomeAssistant, calls) -> None: """Test turn on and turn off.""" await _register_components(hass) - expected_calls = 0 - for func, state, action in [ - (common.async_turn_on, STATE_ON, "turn_on"), - (common.async_turn_off, STATE_OFF, "turn_off"), - ]: + for expected_calls, (func, state, action) in enumerate( + [ + (common.async_turn_on, STATE_ON, "turn_on"), + (common.async_turn_off, STATE_OFF, "turn_off"), + ] + ): await func(hass, _TEST_FAN) assert hass.states.get(_STATE_INPUT_BOOLEAN).state == state _verify(hass, state, 0, None, None, None) - expected_calls += 1 - assert len(calls) == expected_calls + assert len(calls) == expected_calls + 1 assert calls[-1].data["action"] == action assert calls[-1].data["caller"] == _TEST_FAN @@ -427,6 +414,7 @@ async def test_set_invalid_direction_from_initial_stage( await common.async_turn_on(hass, _TEST_FAN) await common.async_set_direction(hass, _TEST_FAN, "invalid") + assert hass.states.get(_DIRECTION_INPUT_SELECT).state == "" _verify(hass, STATE_ON, 0, None, None, None) diff --git a/tests/components/template/test_image.py b/tests/components/template/test_image.py index 7b399e13ec0..6162276fcec 100644 --- a/tests/components/template/test_image.py +++ b/tests/components/template/test_image.py @@ -1,4 +1,5 @@ """The tests for the Template image platform.""" + from http import HTTPStatus from io import BytesIO from typing import Any diff --git a/tests/components/template/test_init.py b/tests/components/template/test_init.py index df1e86eaacd..991228623b1 100644 --- a/tests/components/template/test_init.py +++ b/tests/components/template/test_init.py @@ -1,4 +1,5 @@ """The test for the Template sensor platform.""" + from datetime import timedelta from unittest.mock import patch @@ -247,7 +248,7 @@ async def test_reload_sensors_that_reference_other_template_sensors( next_time = dt_util.utcnow() + timedelta(seconds=1.2) with patch( - "homeassistant.helpers.ratelimit.dt_util.utcnow", return_value=next_time + "homeassistant.helpers.ratelimit.time.time", return_value=next_time.timestamp() ): async_fire_time_changed(hass, next_time) await hass.async_block_till_done() diff --git a/tests/components/template/test_light.py b/tests/components/template/test_light.py index ec830d4daf6..a40f093a573 100644 --- a/tests/components/template/test_light.py +++ b/tests/components/template/test_light.py @@ -1,4 +1,5 @@ """The tests for the Template light platform.""" + import pytest import homeassistant.components.light as light diff --git a/tests/components/template/test_lock.py b/tests/components/template/test_lock.py index 118a73264d7..77b7c9657d4 100644 --- a/tests/components/template/test_lock.py +++ b/tests/components/template/test_lock.py @@ -1,4 +1,5 @@ """The tests for the Template lock platform.""" + import pytest from homeassistant import setup diff --git a/tests/components/template/test_number.py b/tests/components/template/test_number.py index 02d5a8451e0..bfaf3b6a0a1 100644 --- a/tests/components/template/test_number.py +++ b/tests/components/template/test_number.py @@ -1,4 +1,5 @@ """The tests for the Template number platform.""" + from homeassistant import setup from homeassistant.components.input_number import ( ATTR_VALUE as INPUT_NUMBER_ATTR_VALUE, diff --git a/tests/components/template/test_select.py b/tests/components/template/test_select.py index 1f9915d4b21..6567926cd01 100644 --- a/tests/components/template/test_select.py +++ b/tests/components/template/test_select.py @@ -1,4 +1,5 @@ """The tests for the Template select platform.""" + from homeassistant import setup from homeassistant.components.input_select import ( ATTR_OPTION as INPUT_SELECT_ATTR_OPTION, diff --git a/tests/components/template/test_sensor.py b/tests/components/template/test_sensor.py index 8026618e7cd..fdcc0587a73 100644 --- a/tests/components/template/test_sensor.py +++ b/tests/components/template/test_sensor.py @@ -1,4 +1,5 @@ """The test for the Template sensor platform.""" + from asyncio import Event from datetime import datetime, timedelta from unittest.mock import ANY, patch @@ -393,12 +394,15 @@ async def test_creating_sensor_loads_group(hass: HomeAssistant) -> None: hass.bus.async_listen(EVENT_COMPONENT_LOADED, set_after_dep_event) - with patch( - "homeassistant.components.group.async_setup", - new=async_setup_group, - ), patch( - "homeassistant.components.template.sensor.async_setup_platform", - new=async_setup_template, + with ( + patch( + "homeassistant.components.group.async_setup", + new=async_setup_group, + ), + patch( + "homeassistant.components.template.sensor.async_setup_platform", + new=async_setup_template, + ), ): await async_from_config_dict( {"sensor": {"platform": "template", "sensors": {}}, "group": {}}, hass @@ -475,7 +479,11 @@ async def test_invalid_attribute_template( await hass.async_block_till_done() await async_update_entity(hass, "sensor.invalid_template") assert "TemplateError" in caplog_setup_text - assert "test_attribute" in caplog.text + assert ( + "Template variable error: 'None' has no attribute 'attributes' when rendering" + in caplog.text + ) + assert hass.states.get("sensor.invalid_template").state == "startup" @pytest.mark.parametrize(("count", "domain"), [(1, sensor.DOMAIN)]) @@ -969,7 +977,7 @@ async def test_self_referencing_entity_picture_loop( assert len(hass.states.async_all()) == 1 next_time = dt_util.utcnow() + timedelta(seconds=1.2) with patch( - "homeassistant.helpers.ratelimit.dt_util.utcnow", return_value=next_time + "homeassistant.helpers.ratelimit.time.time", return_value=next_time.timestamp() ): async_fire_time_changed(hass, next_time) await hass.async_block_till_done() @@ -1597,11 +1605,15 @@ async def test_entity_last_reset_parsing( # State of timestamp sensors are always in UTC now = dt_util.utcnow() - with patch( - "homeassistant.components.template.sensor._LOGGER.warning" - ) as mocked_warning, patch( - "homeassistant.components.template.template_entity._LOGGER.error" - ) as mocked_error, patch("homeassistant.util.dt.now", return_value=now): + with ( + patch( + "homeassistant.components.template.sensor._LOGGER.warning" + ) as mocked_warning, + patch( + "homeassistant.components.template.template_entity._LOGGER.error" + ) as mocked_error, + patch("homeassistant.util.dt.now", return_value=now), + ): assert await async_setup_component( hass, "template", diff --git a/tests/components/template/test_switch.py b/tests/components/template/test_switch.py index 1e979fc9926..acf80006798 100644 --- a/tests/components/template/test_switch.py +++ b/tests/components/template/test_switch.py @@ -1,4 +1,5 @@ """The tests for the Template switch platform.""" + import pytest from homeassistant import setup diff --git a/tests/components/template/test_template_entity.py b/tests/components/template/test_template_entity.py index 4fe3d7cb780..dcceea95181 100644 --- a/tests/components/template/test_template_entity.py +++ b/tests/components/template/test_template_entity.py @@ -1,4 +1,5 @@ """Test template entity.""" + import pytest from homeassistant.components.template import template_entity diff --git a/tests/components/template/test_trigger.py b/tests/components/template/test_trigger.py index af010c57e2e..db7cd3a2471 100644 --- a/tests/components/template/test_trigger.py +++ b/tests/components/template/test_trigger.py @@ -1,4 +1,5 @@ """The tests for the Template automation.""" + from datetime import timedelta from unittest import mock diff --git a/tests/components/template/test_vacuum.py b/tests/components/template/test_vacuum.py index e6850728450..2c6f083abce 100644 --- a/tests/components/template/test_vacuum.py +++ b/tests/components/template/test_vacuum.py @@ -1,4 +1,5 @@ """The tests for the Template vacuum platform.""" + import pytest from homeassistant import setup diff --git a/tests/components/template/test_weather.py b/tests/components/template/test_weather.py index 36071c746da..e457f2e263b 100644 --- a/tests/components/template/test_weather.py +++ b/tests/components/template/test_weather.py @@ -1,11 +1,11 @@ """The tests for the Template Weather platform.""" + from typing import Any import pytest from syrupy.assertion import SnapshotAssertion from homeassistant.components.weather import ( - ATTR_FORECAST, ATTR_WEATHER_APPARENT_TEMPERATURE, ATTR_WEATHER_CLOUD_COVERAGE, ATTR_WEATHER_DEW_POINT, @@ -35,6 +35,8 @@ from tests.common import ( mock_restore_cache_with_extra_data, ) +ATTR_FORECAST = "forecast" + @pytest.mark.parametrize(("count", "domain"), [(1, WEATHER_DOMAIN)]) @pytest.mark.parametrize( @@ -48,7 +50,6 @@ from tests.common import ( "name": "test", "attribution_template": "{{ states('sensor.attribution') }}", "condition_template": "sunny", - "forecast_template": "{{ states.weather.demo.attributes.forecast }}", "temperature_template": "{{ states('sensor.temperature') | float }}", "humidity_template": "{{ states('sensor.humidity') | int }}", "pressure_template": "{{ states('sensor.pressure') }}", @@ -110,7 +111,6 @@ async def test_template_state_text(hass: HomeAssistant, start_ha) -> None: "platform": "template", "name": "forecast", "condition_template": "sunny", - "forecast_template": "{{ states.weather.forecast.attributes.forecast }}", "forecast_daily_template": "{{ states.weather.forecast.attributes.forecast }}", "forecast_hourly_template": "{{ states.weather.forecast.attributes.forecast }}", "forecast_twice_daily_template": "{{ states.weather.forecast_twice_daily.attributes.forecast }}", @@ -237,7 +237,6 @@ async def test_forecasts( "platform": "template", "name": "forecast", "condition_template": "sunny", - "forecast_template": "{{ states.weather.forecast.attributes.forecast }}", "forecast_daily_template": "{{ states.weather.forecast.attributes.forecast }}", "forecast_hourly_template": "{{ states.weather.forecast_hourly.attributes.forecast }}", "temperature_template": "{{ states('sensor.temperature') | float }}", @@ -322,7 +321,6 @@ async def test_forecast_invalid( "platform": "template", "name": "forecast", "condition_template": "sunny", - "forecast_template": "{{ states.weather.forecast.attributes.forecast }}", "forecast_twice_daily_template": "{{ states.weather.forecast_twice_daily.attributes.forecast }}", "temperature_template": "{{ states('sensor.temperature') | float }}", "humidity_template": "{{ states('sensor.humidity') | int }}", @@ -392,7 +390,6 @@ async def test_forecast_invalid_is_daytime_missing_in_twice_daily( "platform": "template", "name": "forecast", "condition_template": "sunny", - "forecast_template": "{{ states.weather.forecast.attributes.forecast }}", "forecast_twice_daily_template": "{{ states.weather.forecast_twice_daily.attributes.forecast }}", "temperature_template": "{{ states('sensor.temperature') | float }}", "humidity_template": "{{ states('sensor.humidity') | int }}", @@ -462,7 +459,6 @@ async def test_forecast_invalid_datetime_missing( "platform": "template", "name": "forecast", "condition_template": "sunny", - "forecast_template": "{{ states.weather.forecast.attributes.forecast }}", "forecast_daily_template": "{{ states.weather.forecast_daily.attributes.forecast }}", "forecast_hourly_template": "{{ states.weather.forecast_hourly.attributes.forecast }}", "temperature_template": "{{ states('sensor.temperature') | float }}", @@ -506,6 +502,7 @@ async def test_forecast_format_error( } }, ) + await hass.async_block_till_done() await hass.services.async_call( WEATHER_DOMAIN, @@ -726,7 +723,6 @@ async def test_trigger_action( "cloud_coverage_template": "{{ my_variable + 1 }}", "dew_point_template": "{{ my_variable + 1 }}", "apparent_temperature_template": "{{ my_variable + 1 }}", - "forecast_template": "{{ var_forecast_daily }}", "forecast_daily_template": "{{ var_forecast_daily }}", "forecast_hourly_template": "{{ var_forecast_hourly }}", "forecast_twice_daily_template": "{{ var_forecast_twice_daily }}", diff --git a/tests/components/tesla_wall_connector/conftest.py b/tests/components/tesla_wall_connector/conftest.py index e9f26a58eec..a31df11dcd0 100644 --- a/tests/components/tesla_wall_connector/conftest.py +++ b/tests/components/tesla_wall_connector/conftest.py @@ -1,4 +1,5 @@ """Common fixutres with default mocks as well as common test helper methods.""" + from dataclasses import dataclass from datetime import timedelta from typing import Any @@ -62,18 +63,22 @@ async def create_wall_connector_entry( entry.add_to_hass(hass) - with patch( - "tesla_wall_connector.WallConnector.async_get_version", - return_value=get_default_version_data(), - side_effect=side_effect, - ), patch( - "tesla_wall_connector.WallConnector.async_get_vitals", - return_value=vitals_data, - side_effect=side_effect, - ), patch( - "tesla_wall_connector.WallConnector.async_get_lifetime", - return_value=lifetime_data, - side_effect=side_effect, + with ( + patch( + "tesla_wall_connector.WallConnector.async_get_version", + return_value=get_default_version_data(), + side_effect=side_effect, + ), + patch( + "tesla_wall_connector.WallConnector.async_get_vitals", + return_value=vitals_data, + side_effect=side_effect, + ), + patch( + "tesla_wall_connector.WallConnector.async_get_lifetime", + return_value=lifetime_data, + side_effect=side_effect, + ), ): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -126,12 +131,15 @@ async def _test_sensors( ), f"First update: {entity.entity_id} is expected to have state {entity.first_value} but has {state.state}" # Simulate second data update - with patch( - "tesla_wall_connector.WallConnector.async_get_vitals", - return_value=vitals_second_update, - ), patch( - "tesla_wall_connector.WallConnector.async_get_lifetime", - return_value=lifetime_second_update, + with ( + patch( + "tesla_wall_connector.WallConnector.async_get_vitals", + return_value=vitals_second_update, + ), + patch( + "tesla_wall_connector.WallConnector.async_get_lifetime", + return_value=lifetime_second_update, + ), ): async_fire_time_changed( hass, dt_util.utcnow() + timedelta(seconds=DEFAULT_SCAN_INTERVAL) diff --git a/tests/components/tesla_wall_connector/test_binary_sensor.py b/tests/components/tesla_wall_connector/test_binary_sensor.py index 09283cb5352..22100bbb1c1 100644 --- a/tests/components/tesla_wall_connector/test_binary_sensor.py +++ b/tests/components/tesla_wall_connector/test_binary_sensor.py @@ -1,4 +1,5 @@ """Tests for binary sensors.""" + from homeassistant.core import HomeAssistant from .conftest import ( diff --git a/tests/components/tesla_wall_connector/test_config_flow.py b/tests/components/tesla_wall_connector/test_config_flow.py index 17920a3bf57..198dcccfe00 100644 --- a/tests/components/tesla_wall_connector/test_config_flow.py +++ b/tests/components/tesla_wall_connector/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Tesla Wall Connector config flow.""" + from unittest.mock import patch from tesla_wall_connector.exceptions import WallConnectorConnectionError diff --git a/tests/components/tesla_wall_connector/test_init.py b/tests/components/tesla_wall_connector/test_init.py index aa7a71948b7..152bf18d57e 100644 --- a/tests/components/tesla_wall_connector/test_init.py +++ b/tests/components/tesla_wall_connector/test_init.py @@ -1,4 +1,5 @@ """Test the Tesla Wall Connector config flow.""" + from tesla_wall_connector.exceptions import WallConnectorConnectionError from homeassistant import config_entries diff --git a/tests/components/tesla_wall_connector/test_sensor.py b/tests/components/tesla_wall_connector/test_sensor.py index 28b50ba72ea..d064b9028b5 100644 --- a/tests/components/tesla_wall_connector/test_sensor.py +++ b/tests/components/tesla_wall_connector/test_sensor.py @@ -1,4 +1,5 @@ """Tests for sensors.""" + from homeassistant.core import HomeAssistant from .conftest import ( diff --git a/tests/components/teslemetry/__init__.py b/tests/components/teslemetry/__init__.py index eae58127d1d..ac3a2904c27 100644 --- a/tests/components/teslemetry/__init__.py +++ b/tests/components/teslemetry/__init__.py @@ -48,3 +48,18 @@ def assert_entities( assert entity_entry == snapshot(name=f"{entity_entry.entity_id}-entry") assert (state := hass.states.get(entity_entry.entity_id)) assert state == snapshot(name=f"{entity_entry.entity_id}-state") + + +def assert_entities_alt( + hass: HomeAssistant, + entry_id: str, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test that all entities match their alt snapshot.""" + entity_entries = er.async_entries_for_config_entry(entity_registry, entry_id) + + assert entity_entries + for entity_entry in entity_entries: + assert (state := hass.states.get(entity_entry.entity_id)) + assert state == snapshot(name=f"{entity_entry.entity_id}-statealt") diff --git a/tests/components/teslemetry/conftest.py b/tests/components/teslemetry/conftest.py index 8c1fe070dde..f252787b37c 100644 --- a/tests/components/teslemetry/conftest.py +++ b/tests/components/teslemetry/conftest.py @@ -1,11 +1,13 @@ """Fixtures for Tessie.""" + from __future__ import annotations +from copy import deepcopy from unittest.mock import patch import pytest -from .const import PRODUCTS, RESPONSE_OK, VEHICLE_DATA, WAKE_UP_ONLINE +from .const import LIVE_STATUS, PRODUCTS, RESPONSE_OK, VEHICLE_DATA, WAKE_UP_ONLINE @pytest.fixture(autouse=True) @@ -55,3 +57,13 @@ def mock_request(): return_value=RESPONSE_OK, ) as mock_request: yield mock_request + + +@pytest.fixture(autouse=True) +def mock_live_status(): + """Mock Teslemetry Energy Specific live_status method.""" + with patch( + "homeassistant.components.teslemetry.EnergySpecific.live_status", + side_effect=lambda: deepcopy(LIVE_STATUS), + ) as mock_live_status: + yield mock_live_status diff --git a/tests/components/teslemetry/const.py b/tests/components/teslemetry/const.py index 0feb056fa72..776cc231a5c 100644 --- a/tests/components/teslemetry/const.py +++ b/tests/components/teslemetry/const.py @@ -12,5 +12,7 @@ WAKE_UP_ASLEEP = {"response": {"state": TeslemetryState.ASLEEP}, "error": None} PRODUCTS = load_json_object_fixture("products.json", DOMAIN) VEHICLE_DATA = load_json_object_fixture("vehicle_data.json", DOMAIN) +VEHICLE_DATA_ALT = load_json_object_fixture("vehicle_data_alt.json", DOMAIN) +LIVE_STATUS = load_json_object_fixture("live_status.json", DOMAIN) RESPONSE_OK = {"response": {}, "error": None} diff --git a/tests/components/teslemetry/fixtures/live_status.json b/tests/components/teslemetry/fixtures/live_status.json new file mode 100644 index 00000000000..486f9f4fadd --- /dev/null +++ b/tests/components/teslemetry/fixtures/live_status.json @@ -0,0 +1,33 @@ +{ + "response": { + "solar_power": 1185, + "energy_left": 38896.47368421053, + "total_pack_energy": 40727, + "percentage_charged": 95.50537403739663, + "backup_capable": true, + "battery_power": 5060, + "load_power": 6245, + "grid_status": "Active", + "grid_services_active": false, + "grid_power": 0, + "grid_services_power": 0, + "generator_power": 0, + "island_status": "on_grid", + "storm_mode_active": false, + "timestamp": "2024-01-01T00:00:00+00:00", + "wall_connectors": [ + { + "din": "abd-123", + "wall_connector_state": 2, + "wall_connector_fault_state": 2, + "wall_connector_power": 0 + }, + { + "din": "bcd-234", + "wall_connector_state": 2, + "wall_connector_fault_state": 2, + "wall_connector_power": 0 + } + ] + } +} diff --git a/tests/components/teslemetry/fixtures/products.json b/tests/components/teslemetry/fixtures/products.json index 430c3b39dc8..aa59062e8d4 100644 --- a/tests/components/teslemetry/fixtures/products.json +++ b/tests/components/teslemetry/fixtures/products.json @@ -71,28 +71,50 @@ "release_notes_supported": true }, { - "energy_site_id": 2345, - "resource_type": "wall_connector", - "id": "ID1234", - "asset_site_id": "abcdef", - "warp_site_number": "ID1234", + "energy_site_id": 123456, + "resource_type": "battery", + "site_name": "Energy Site", + "id": "ABC123", + "gateway_id": "ABC123", + "asset_site_id": "c0ffee", + "warp_site_number": "GA123456", + "energy_left": 23286.105263157893, + "total_pack_energy": 40804, + "percentage_charged": 57.068192488868476, + "battery_type": "ac_powerwall", + "backup_capable": true, + "battery_power": 14990, "go_off_grid_test_banner_enabled": null, - "storm_mode_enabled": null, - "powerwall_onboarding_settings_set": null, + "storm_mode_enabled": true, + "powerwall_onboarding_settings_set": true, "powerwall_tesla_electric_interested_in": null, "vpp_tour_enabled": null, - "sync_grid_alert_enabled": false, - "breaker_alert_enabled": false, + "sync_grid_alert_enabled": true, + "breaker_alert_enabled": true, "components": { - "battery": false, - "solar": false, - "grid": false, - "load_meter": false, + "battery": true, + "battery_type": "ac_powerwall", + "solar": true, + "solar_type": "pv_panel", + "grid": true, + "load_meter": true, + "market_type": "residential", "wall_connectors": [ - { "device_id": "abcdef", "din": "12345", "is_active": true } + { + "device_id": "abc-123", + "din": "123-abc", + "is_active": true + }, + { + "device_id": "bcd-234", + "din": "234-bcd", + "is_active": true + } ] }, - "features": {} + "features": { + "rate_plan_manager_no_pricing_constraint": true + } } ], "count": 2 diff --git a/tests/components/teslemetry/fixtures/site_info.json b/tests/components/teslemetry/fixtures/site_info.json new file mode 100644 index 00000000000..d39fc1f68aa --- /dev/null +++ b/tests/components/teslemetry/fixtures/site_info.json @@ -0,0 +1,87 @@ +{ + "response": { + "id": "1233-abcd", + "site_name": "Site", + "backup_reserve_percent": 0, + "default_real_mode": "self_consumption", + "installation_date": "2022-01-01T00:00:00+00:00", + "user_settings": { + "go_off_grid_test_banner_enabled": false, + "storm_mode_enabled": true, + "powerwall_onboarding_settings_set": true, + "powerwall_tesla_electric_interested_in": false, + "vpp_tour_enabled": true, + "sync_grid_alert_enabled": true, + "breaker_alert_enabled": false + }, + "components": { + "solar": true, + "solar_type": "pv_panel", + "battery": true, + "grid": true, + "backup": true, + "gateway": "teg", + "load_meter": true, + "tou_capable": true, + "storm_mode_capable": true, + "flex_energy_request_capable": false, + "car_charging_data_supported": false, + "off_grid_vehicle_charging_reserve_supported": false, + "vehicle_charging_performance_view_enabled": false, + "vehicle_charging_solar_offset_view_enabled": false, + "battery_solar_offset_view_enabled": true, + "solar_value_enabled": true, + "energy_value_header": "Energy Value", + "energy_value_subheader": "Estimated Value", + "energy_service_self_scheduling_enabled": true, + "show_grid_import_battery_source_cards": true, + "set_islanding_mode_enabled": true, + "wifi_commissioning_enabled": true, + "backup_time_remaining_enabled": true, + "battery_type": "ac_powerwall", + "configurable": true, + "grid_services_enabled": false, + "wall_connectors": [ + { + "device_id": "123abc", + "din": "abc123", + "is_active": true + }, + { + "device_id": "234bcd", + "din": "bcd234", + "is_active": true + } + ], + "disallow_charge_from_grid_with_solar_installed": true, + "customer_preferred_export_rule": "pv_only", + "net_meter_mode": "battery_ok", + "system_alerts_enabled": true + }, + "version": "23.44.0 eb113390", + "battery_count": 3, + "tou_settings": { + "optimization_strategy": "economics", + "schedule": [ + { + "target": "off_peak", + "week_days": [1, 0], + "start_seconds": 0, + "end_seconds": 3600 + }, + { + "target": "peak", + "week_days": [1, 0], + "start_seconds": 3600, + "end_seconds": 0 + } + ] + }, + "nameplate_power": 15000, + "nameplate_energy": 40500, + "installation_time_zone": "", + "max_site_meter_power_ac": 1000000000, + "min_site_meter_power_ac": -1000000000, + "vpp_backup_reserve_percent": 0 + } +} diff --git a/tests/components/teslemetry/fixtures/vehicle_data.json b/tests/components/teslemetry/fixtures/vehicle_data.json index 44556c1c8df..ba73fe3c4e6 100644 --- a/tests/components/teslemetry/fixtures/vehicle_data.json +++ b/tests/components/teslemetry/fixtures/vehicle_data.json @@ -112,10 +112,20 @@ "wiper_blade_heater": false }, "drive_state": { - "active_route_latitude": -27.855946, - "active_route_longitude": 153.345056, + "active_route_latitude": 30.2226265, + "active_route_longitude": -97.6236871, + "active_route_miles_to_arrival": 0.039491, + "active_route_minutes_to_arrival": 0.103577, "active_route_traffic_minutes_delay": 0, - "power": 0, + "gps_as_of": 1701129612, + "heading": 185, + "latitude": -30.222626, + "longitude": -97.6236871, + "native_latitude": -30.222626, + "native_location_supported": 1, + "native_longitude": -97.6236871, + "native_type": "wgs", + "power": -7, "shift_state": null, "speed": null, "timestamp": 1705707520649 diff --git a/tests/components/teslemetry/fixtures/vehicle_data_alt.json b/tests/components/teslemetry/fixtures/vehicle_data_alt.json new file mode 100644 index 00000000000..13d11073fb1 --- /dev/null +++ b/tests/components/teslemetry/fixtures/vehicle_data_alt.json @@ -0,0 +1,279 @@ +{ + "response": { + "id": 1234, + "user_id": 1234, + "vehicle_id": 1234, + "vin": "VINVINVIN", + "color": null, + "access_type": "OWNER", + "granular_access": { + "hide_private": false + }, + "tokens": ["abc", "def"], + "state": "online", + "in_service": false, + "id_s": "1234", + "calendar_enabled": true, + "api_version": 71, + "backseat_token": null, + "backseat_token_updated_at": null, + "ble_autopair_enrolled": false, + "charge_state": { + "battery_heater_on": false, + "battery_level": 77, + "battery_range": 266.87, + "charge_amps": 16, + "charge_current_request": 16, + "charge_current_request_max": 16, + "charge_enable_request": true, + "charge_energy_added": 0, + "charge_limit_soc": 80, + "charge_limit_soc_max": 100, + "charge_limit_soc_min": 50, + "charge_limit_soc_std": 80, + "charge_miles_added_ideal": 0, + "charge_miles_added_rated": 0, + "charge_port_cold_weather_mode": false, + "charge_port_color": "", + "charge_port_door_open": true, + "charge_port_latch": "Engaged", + "charge_rate": 0, + "charger_actual_current": 0, + "charger_phases": null, + "charger_pilot_current": 16, + "charger_power": 0, + "charger_voltage": 2, + "charging_state": "Stopped", + "conn_charge_cable": "IEC", + "est_battery_range": 275.04, + "fast_charger_brand": "", + "fast_charger_present": false, + "fast_charger_type": "ACSingleWireCAN", + "ideal_battery_range": 266.87, + "max_range_charge_counter": 0, + "minutes_to_full_charge": "bad value", + "not_enough_power_to_heat": null, + "off_peak_charging_enabled": false, + "off_peak_charging_times": "all_week", + "off_peak_hours_end_time": 900, + "preconditioning_enabled": false, + "preconditioning_times": "all_week", + "scheduled_charging_mode": "Off", + "scheduled_charging_pending": false, + "scheduled_charging_start_time": null, + "scheduled_charging_start_time_app": 600, + "scheduled_departure_time": 1704837600, + "scheduled_departure_time_minutes": 480, + "supercharger_session_trip_planner": false, + "time_to_full_charge": null, + "timestamp": null, + "trip_charging": false, + "usable_battery_level": 77, + "user_charge_enable_request": null + }, + "climate_state": { + "allow_cabin_overheat_protection": true, + "auto_seat_climate_left": false, + "auto_seat_climate_right": false, + "auto_steering_wheel_heat": false, + "battery_heater": false, + "battery_heater_no_power": null, + "cabin_overheat_protection": "Off", + "cabin_overheat_protection_actively_cooling": false, + "climate_keeper_mode": "off", + "cop_activation_temperature": "High", + "defrost_mode": 0, + "driver_temp_setting": 22, + "fan_status": 0, + "hvac_auto_request": "On", + "inside_temp": 29.8, + "is_auto_conditioning_on": false, + "is_climate_on": false, + "is_front_defroster_on": false, + "is_preconditioning": false, + "is_rear_defroster_on": false, + "left_temp_direction": 251, + "max_avail_temp": 28, + "min_avail_temp": 15, + "outside_temp": 30, + "passenger_temp_setting": 22, + "remote_heater_control_enabled": false, + "right_temp_direction": 251, + "seat_heater_left": 0, + "seat_heater_rear_center": 0, + "seat_heater_rear_left": 0, + "seat_heater_rear_right": 0, + "seat_heater_right": 0, + "side_mirror_heaters": false, + "steering_wheel_heat_level": 0, + "steering_wheel_heater": false, + "supports_fan_only_cabin_overheat_protection": true, + "timestamp": 1705707520649, + "wiper_blade_heater": false + }, + "drive_state": { + "active_route_latitude": 30.2226265, + "active_route_longitude": -97.6236871, + "active_route_miles_to_arrival": 0, + "active_route_minutes_to_arrival": 0, + "active_route_traffic_minutes_delay": 0, + "gps_as_of": 1701129612, + "heading": 185, + "latitude": -30.222626, + "longitude": -97.6236871, + "native_latitude": -30.222626, + "native_location_supported": 1, + "native_longitude": -97.6236871, + "native_type": "wgs", + "power": -7, + "shift_state": null, + "speed": null, + "timestamp": 1705707520649 + }, + "gui_settings": { + "gui_24_hour_time": false, + "gui_charge_rate_units": "kW", + "gui_distance_units": "km/hr", + "gui_range_display": "Rated", + "gui_temperature_units": "C", + "gui_tirepressure_units": "Psi", + "show_range_units": false, + "timestamp": 1705707520649 + }, + "vehicle_config": { + "aux_park_lamps": "Eu", + "badge_version": 1, + "can_accept_navigation_requests": true, + "can_actuate_trunks": true, + "car_special_type": "base", + "car_type": "model3", + "charge_port_type": "CCS", + "cop_user_set_temp_supported": false, + "dashcam_clip_save_supported": true, + "default_charge_to_max": false, + "driver_assist": "TeslaAP3", + "ece_restrictions": false, + "efficiency_package": "M32021", + "eu_vehicle": true, + "exterior_color": "DeepBlue", + "exterior_trim": "Black", + "exterior_trim_override": "", + "has_air_suspension": false, + "has_ludicrous_mode": false, + "has_seat_cooling": false, + "headlamp_type": "Global", + "interior_trim_type": "White2", + "key_version": 2, + "motorized_charge_port": true, + "paint_color_override": "0,9,25,0.7,0.04", + "performance_package": "Base", + "plg": true, + "pws": true, + "rear_drive_unit": "PM216MOSFET", + "rear_seat_heaters": 1, + "rear_seat_type": 0, + "rhd": true, + "roof_color": "RoofColorGlass", + "seat_type": null, + "spoiler_type": "None", + "sun_roof_installed": null, + "supports_qr_pairing": false, + "third_row_seats": "None", + "timestamp": 1705707520649, + "trim_badging": "74d", + "use_range_badging": true, + "utc_offset": 36000, + "webcam_selfie_supported": true, + "webcam_supported": true, + "wheel_type": "Pinwheel18CapKit" + }, + "vehicle_state": { + "api_version": 71, + "autopark_state_v2": "unavailable", + "calendar_supported": true, + "car_version": "2023.44.30.8 06f534d46010", + "center_display_state": 0, + "dashcam_clip_save_available": true, + "dashcam_state": "Recording", + "df": 0, + "dr": 0, + "fd_window": 0, + "feature_bitmask": "fbdffbff,187f", + "fp_window": 0, + "ft": 0, + "is_user_present": false, + "locked": false, + "media_info": { + "audio_volume": 2.6667, + "audio_volume_increment": 0.333333, + "audio_volume_max": 10.333333, + "media_playback_status": "Stopped", + "now_playing_album": "", + "now_playing_artist": "", + "now_playing_duration": 0, + "now_playing_elapsed": 0, + "now_playing_source": "Spotify", + "now_playing_station": "", + "now_playing_title": "" + }, + "media_state": { + "remote_control_enabled": true + }, + "notifications_supported": true, + "odometer": 6481.019282, + "parsed_calendar_supported": true, + "pf": 0, + "pr": 0, + "rd_window": 0, + "remote_start": false, + "remote_start_enabled": true, + "remote_start_supported": true, + "rp_window": 0, + "rt": 0, + "santa_mode": 0, + "sentry_mode": false, + "sentry_mode_available": true, + "service_mode": false, + "service_mode_plus": false, + "software_update": { + "download_perc": 0, + "expected_duration_sec": 2700, + "install_perc": 1, + "status": "", + "version": " " + }, + "speed_limit_mode": { + "active": false, + "current_limit_mph": 69, + "max_limit_mph": 120, + "min_limit_mph": 50, + "pin_code_set": true + }, + "timestamp": 1705707520649, + "tpms_hard_warning_fl": false, + "tpms_hard_warning_fr": false, + "tpms_hard_warning_rl": false, + "tpms_hard_warning_rr": false, + "tpms_last_seen_pressure_time_fl": 1705700812, + "tpms_last_seen_pressure_time_fr": 1705700793, + "tpms_last_seen_pressure_time_rl": 1705700794, + "tpms_last_seen_pressure_time_rr": 1705700823, + "tpms_pressure_fl": 2.775, + "tpms_pressure_fr": 2.8, + "tpms_pressure_rl": 2.775, + "tpms_pressure_rr": 2.775, + "tpms_rcp_front_value": 2.9, + "tpms_rcp_rear_value": 2.9, + "tpms_soft_warning_fl": false, + "tpms_soft_warning_fr": false, + "tpms_soft_warning_rl": false, + "tpms_soft_warning_rr": false, + "valet_mode": false, + "valet_pin_needed": false, + "vehicle_name": "Test", + "vehicle_self_test_progress": 0, + "vehicle_self_test_requested": false, + "webcam_available": true + } + } +} diff --git a/tests/components/teslemetry/snapshots/test_climate.ambr b/tests/components/teslemetry/snapshots/test_climate.ambr index f0027273ece..097df8bde85 100644 --- a/tests/components/teslemetry/snapshots/test_climate.ambr +++ b/tests/components/teslemetry/snapshots/test_climate.ambr @@ -69,6 +69,7 @@ 'context': , 'entity_id': 'climate.test_climate', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }) diff --git a/tests/components/teslemetry/snapshots/test_sensor.ambr b/tests/components/teslemetry/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..fad04d341c9 --- /dev/null +++ b/tests/components/teslemetry/snapshots/test_sensor.ambr @@ -0,0 +1,2990 @@ +# serializer version: 1 +# name: test_sensors[sensor.energy_site_battery_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.energy_site_battery_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery power', + 'platform': 'teslemetry', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'battery_power', + 'unique_id': '123456-battery_power', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.energy_site_battery_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Energy Site Battery power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_site_battery_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5.06', + }) +# --- +# name: test_sensors[sensor.energy_site_battery_power-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Energy Site Battery power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_site_battery_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5.06', + }) +# --- +# name: test_sensors[sensor.energy_site_energy_left-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.energy_site_energy_left', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Energy left', + 'platform': 'teslemetry', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'energy_left', + 'unique_id': '123456-energy_left', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.energy_site_energy_left-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy_storage', + 'friendly_name': 'Energy Site Energy left', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_site_energy_left', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '38.8964736842105', + }) +# --- +# name: test_sensors[sensor.energy_site_energy_left-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy_storage', + 'friendly_name': 'Energy Site Energy left', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_site_energy_left', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '38.8964736842105', + }) +# --- +# name: test_sensors[sensor.energy_site_generator_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.energy_site_generator_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Generator power', + 'platform': 'teslemetry', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'generator_power', + 'unique_id': '123456-generator_power', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.energy_site_generator_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Energy Site Generator power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_site_generator_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_sensors[sensor.energy_site_generator_power-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Energy Site Generator power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_site_generator_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_sensors[sensor.energy_site_grid_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.energy_site_grid_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Grid power', + 'platform': 'teslemetry', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'grid_power', + 'unique_id': '123456-grid_power', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.energy_site_grid_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Energy Site Grid power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_site_grid_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_sensors[sensor.energy_site_grid_power-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Energy Site Grid power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_site_grid_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_sensors[sensor.energy_site_grid_services_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.energy_site_grid_services_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Grid services power', + 'platform': 'teslemetry', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'grid_services_power', + 'unique_id': '123456-grid_services_power', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.energy_site_grid_services_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Energy Site Grid services power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_site_grid_services_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_sensors[sensor.energy_site_grid_services_power-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Energy Site Grid services power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_site_grid_services_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_sensors[sensor.energy_site_load_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.energy_site_load_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Load power', + 'platform': 'teslemetry', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'load_power', + 'unique_id': '123456-load_power', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.energy_site_load_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Energy Site Load power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_site_load_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '6.245', + }) +# --- +# name: test_sensors[sensor.energy_site_load_power-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Energy Site Load power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_site_load_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '6.245', + }) +# --- +# name: test_sensors[sensor.energy_site_none-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.energy_site_none', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': None, + 'platform': 'teslemetry', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'island_status', + 'unique_id': '123456-island_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.energy_site_none-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Energy Site None', + }), + 'context': , + 'entity_id': 'sensor.energy_site_none', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on_grid', + }) +# --- +# name: test_sensors[sensor.energy_site_none-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Energy Site None', + }), + 'context': , + 'entity_id': 'sensor.energy_site_none', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on_grid', + }) +# --- +# name: test_sensors[sensor.energy_site_percentage_charged-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.energy_site_percentage_charged', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Percentage charged', + 'platform': 'teslemetry', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'percentage_charged', + 'unique_id': '123456-percentage_charged', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[sensor.energy_site_percentage_charged-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Energy Site Percentage charged', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.energy_site_percentage_charged', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '95.5053740373966', + }) +# --- +# name: test_sensors[sensor.energy_site_percentage_charged-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Energy Site Percentage charged', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.energy_site_percentage_charged', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '95.5053740373966', + }) +# --- +# name: test_sensors[sensor.energy_site_solar_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.energy_site_solar_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Solar power', + 'platform': 'teslemetry', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'solar_power', + 'unique_id': '123456-solar_power', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.energy_site_solar_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Energy Site Solar power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_site_solar_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.185', + }) +# --- +# name: test_sensors[sensor.energy_site_solar_power-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Energy Site Solar power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_site_solar_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.185', + }) +# --- +# name: test_sensors[sensor.energy_site_total_pack_energy-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.energy_site_total_pack_energy', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Total pack energy', + 'platform': 'teslemetry', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'total_pack_energy', + 'unique_id': '123456-total_pack_energy', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.energy_site_total_pack_energy-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy_storage', + 'friendly_name': 'Energy Site Total pack energy', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_site_total_pack_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '40.727', + }) +# --- +# name: test_sensors[sensor.energy_site_total_pack_energy-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy_storage', + 'friendly_name': 'Energy Site Total pack energy', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.energy_site_total_pack_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '40.727', + }) +# --- +# name: test_sensors[sensor.test_battery_level-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.test_battery_level', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Battery level', + 'platform': 'teslemetry', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'charge_state_battery_level', + 'unique_id': 'VINVINVIN-charge_state_battery_level', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.test_battery_level-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Battery level', + }), + 'context': , + 'entity_id': 'sensor.test_battery_level', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.test_battery_level-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Battery level', + }), + 'context': , + 'entity_id': 'sensor.test_battery_level', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.test_battery_range-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.test_battery_range', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Battery range', + 'platform': 'teslemetry', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'charge_state_battery_range', + 'unique_id': 'VINVINVIN-charge_state_battery_range', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.test_battery_range-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Battery range', + }), + 'context': , + 'entity_id': 'sensor.test_battery_range', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.test_battery_range-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Battery range', + }), + 'context': , + 'entity_id': 'sensor.test_battery_range', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.test_charge_cable-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.test_charge_cable', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Charge cable', + 'platform': 'teslemetry', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'charge_state_conn_charge_cable', + 'unique_id': 'VINVINVIN-charge_state_conn_charge_cable', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.test_charge_cable-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Charge cable', + }), + 'context': , + 'entity_id': 'sensor.test_charge_cable', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.test_charge_cable-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Charge cable', + }), + 'context': , + 'entity_id': 'sensor.test_charge_cable', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.test_charge_energy_added-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.test_charge_energy_added', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Charge energy added', + 'platform': 'teslemetry', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'charge_state_charge_energy_added', + 'unique_id': 'VINVINVIN-charge_state_charge_energy_added', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.test_charge_energy_added-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Charge energy added', + }), + 'context': , + 'entity_id': 'sensor.test_charge_energy_added', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.test_charge_energy_added-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Charge energy added', + }), + 'context': , + 'entity_id': 'sensor.test_charge_energy_added', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.test_charge_rate-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.test_charge_rate', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Charge rate', + 'platform': 'teslemetry', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'charge_state_charge_rate', + 'unique_id': 'VINVINVIN-charge_state_charge_rate', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.test_charge_rate-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Charge rate', + }), + 'context': , + 'entity_id': 'sensor.test_charge_rate', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.test_charge_rate-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Charge rate', + }), + 'context': , + 'entity_id': 'sensor.test_charge_rate', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.test_charger_current-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.test_charger_current', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Charger current', + 'platform': 'teslemetry', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'charge_state_charger_actual_current', + 'unique_id': 'VINVINVIN-charge_state_charger_actual_current', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.test_charger_current-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Charger current', + }), + 'context': , + 'entity_id': 'sensor.test_charger_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.test_charger_current-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Charger current', + }), + 'context': , + 'entity_id': 'sensor.test_charger_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.test_charger_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.test_charger_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Charger power', + 'platform': 'teslemetry', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'charge_state_charger_power', + 'unique_id': 'VINVINVIN-charge_state_charger_power', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.test_charger_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Charger power', + }), + 'context': , + 'entity_id': 'sensor.test_charger_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.test_charger_power-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Charger power', + }), + 'context': , + 'entity_id': 'sensor.test_charger_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.test_charger_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.test_charger_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Charger voltage', + 'platform': 'teslemetry', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'charge_state_charger_voltage', + 'unique_id': 'VINVINVIN-charge_state_charger_voltage', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.test_charger_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Charger voltage', + }), + 'context': , + 'entity_id': 'sensor.test_charger_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.test_charger_voltage-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Charger voltage', + }), + 'context': , + 'entity_id': 'sensor.test_charger_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.test_charging-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.test_charging', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Charging', + 'platform': 'teslemetry', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'charge_state_charging_state', + 'unique_id': 'VINVINVIN-charge_state_charging_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.test_charging-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Charging', + }), + 'context': , + 'entity_id': 'sensor.test_charging', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.test_charging-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Charging', + }), + 'context': , + 'entity_id': 'sensor.test_charging', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.test_distance_to_arrival-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.test_distance_to_arrival', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Distance to arrival', + 'platform': 'teslemetry', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'drive_state_active_route_miles_to_arrival', + 'unique_id': 'VINVINVIN-drive_state_active_route_miles_to_arrival', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.test_distance_to_arrival-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Distance to arrival', + }), + 'context': , + 'entity_id': 'sensor.test_distance_to_arrival', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.test_distance_to_arrival-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Distance to arrival', + }), + 'context': , + 'entity_id': 'sensor.test_distance_to_arrival', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.test_driver_temperature_setting-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.test_driver_temperature_setting', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Driver temperature setting', + 'platform': 'teslemetry', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'climate_state_driver_temp_setting', + 'unique_id': 'VINVINVIN-climate_state_driver_temp_setting', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.test_driver_temperature_setting-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Driver temperature setting', + }), + 'context': , + 'entity_id': 'sensor.test_driver_temperature_setting', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.test_driver_temperature_setting-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Driver temperature setting', + }), + 'context': , + 'entity_id': 'sensor.test_driver_temperature_setting', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.test_estimate_battery_range-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.test_estimate_battery_range', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Estimate battery range', + 'platform': 'teslemetry', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'charge_state_est_battery_range', + 'unique_id': 'VINVINVIN-charge_state_est_battery_range', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.test_estimate_battery_range-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Estimate battery range', + }), + 'context': , + 'entity_id': 'sensor.test_estimate_battery_range', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.test_estimate_battery_range-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Estimate battery range', + }), + 'context': , + 'entity_id': 'sensor.test_estimate_battery_range', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.test_fast_charger_type-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.test_fast_charger_type', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Fast charger type', + 'platform': 'teslemetry', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'charge_state_fast_charger_type', + 'unique_id': 'VINVINVIN-charge_state_fast_charger_type', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.test_fast_charger_type-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Fast charger type', + }), + 'context': , + 'entity_id': 'sensor.test_fast_charger_type', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.test_fast_charger_type-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Fast charger type', + }), + 'context': , + 'entity_id': 'sensor.test_fast_charger_type', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.test_ideal_battery_range-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.test_ideal_battery_range', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Ideal battery range', + 'platform': 'teslemetry', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'charge_state_ideal_battery_range', + 'unique_id': 'VINVINVIN-charge_state_ideal_battery_range', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.test_ideal_battery_range-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Ideal battery range', + }), + 'context': , + 'entity_id': 'sensor.test_ideal_battery_range', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.test_ideal_battery_range-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Ideal battery range', + }), + 'context': , + 'entity_id': 'sensor.test_ideal_battery_range', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.test_inside_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.test_inside_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Inside temperature', + 'platform': 'teslemetry', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'climate_state_inside_temp', + 'unique_id': 'VINVINVIN-climate_state_inside_temp', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.test_inside_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Inside temperature', + }), + 'context': , + 'entity_id': 'sensor.test_inside_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.test_inside_temperature-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Inside temperature', + }), + 'context': , + 'entity_id': 'sensor.test_inside_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.test_odometer-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.test_odometer', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Odometer', + 'platform': 'teslemetry', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'vehicle_state_odometer', + 'unique_id': 'VINVINVIN-vehicle_state_odometer', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.test_odometer-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Odometer', + }), + 'context': , + 'entity_id': 'sensor.test_odometer', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.test_odometer-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Odometer', + }), + 'context': , + 'entity_id': 'sensor.test_odometer', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.test_outside_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.test_outside_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Outside temperature', + 'platform': 'teslemetry', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'climate_state_outside_temp', + 'unique_id': 'VINVINVIN-climate_state_outside_temp', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.test_outside_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Outside temperature', + }), + 'context': , + 'entity_id': 'sensor.test_outside_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.test_outside_temperature-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Outside temperature', + }), + 'context': , + 'entity_id': 'sensor.test_outside_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.test_passenger_temperature_setting-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.test_passenger_temperature_setting', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Passenger temperature setting', + 'platform': 'teslemetry', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'climate_state_passenger_temp_setting', + 'unique_id': 'VINVINVIN-climate_state_passenger_temp_setting', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.test_passenger_temperature_setting-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Passenger temperature setting', + }), + 'context': , + 'entity_id': 'sensor.test_passenger_temperature_setting', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.test_passenger_temperature_setting-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Passenger temperature setting', + }), + 'context': , + 'entity_id': 'sensor.test_passenger_temperature_setting', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.test_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.test_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Power', + 'platform': 'teslemetry', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'drive_state_power', + 'unique_id': 'VINVINVIN-drive_state_power', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.test_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Power', + }), + 'context': , + 'entity_id': 'sensor.test_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.test_power-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Power', + }), + 'context': , + 'entity_id': 'sensor.test_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.test_shift_state-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.test_shift_state', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Shift state', + 'platform': 'teslemetry', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'drive_state_shift_state', + 'unique_id': 'VINVINVIN-drive_state_shift_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.test_shift_state-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Shift state', + }), + 'context': , + 'entity_id': 'sensor.test_shift_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.test_shift_state-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Shift state', + }), + 'context': , + 'entity_id': 'sensor.test_shift_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.test_speed-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.test_speed', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Speed', + 'platform': 'teslemetry', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'drive_state_speed', + 'unique_id': 'VINVINVIN-drive_state_speed', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.test_speed-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Speed', + }), + 'context': , + 'entity_id': 'sensor.test_speed', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.test_speed-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Speed', + }), + 'context': , + 'entity_id': 'sensor.test_speed', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.test_state_of_charge_at_arrival-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.test_state_of_charge_at_arrival', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'State of charge at arrival', + 'platform': 'teslemetry', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'drive_state_active_route_energy_at_arrival', + 'unique_id': 'VINVINVIN-drive_state_active_route_energy_at_arrival', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.test_state_of_charge_at_arrival-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test State of charge at arrival', + }), + 'context': , + 'entity_id': 'sensor.test_state_of_charge_at_arrival', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.test_state_of_charge_at_arrival-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test State of charge at arrival', + }), + 'context': , + 'entity_id': 'sensor.test_state_of_charge_at_arrival', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.test_time_to_arrival-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.test_time_to_arrival', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Time to arrival', + 'platform': 'teslemetry', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'drive_state_active_route_minutes_to_arrival', + 'unique_id': 'VINVINVIN-drive_state_active_route_minutes_to_arrival', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.test_time_to_arrival-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'timestamp', + 'friendly_name': 'Test Time to arrival', + }), + 'context': , + 'entity_id': 'sensor.test_time_to_arrival', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2024-01-01T00:00:06+00:00', + }) +# --- +# name: test_sensors[sensor.test_time_to_arrival-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'timestamp', + 'friendly_name': 'Test Time to arrival', + }), + 'context': , + 'entity_id': 'sensor.test_time_to_arrival', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_sensors[sensor.test_time_to_full_charge-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.test_time_to_full_charge', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Time to full charge', + 'platform': 'teslemetry', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'charge_state_minutes_to_full_charge', + 'unique_id': 'VINVINVIN-charge_state_minutes_to_full_charge', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.test_time_to_full_charge-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'timestamp', + 'friendly_name': 'Test Time to full charge', + }), + 'context': , + 'entity_id': 'sensor.test_time_to_full_charge', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_sensors[sensor.test_time_to_full_charge-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'timestamp', + 'friendly_name': 'Test Time to full charge', + }), + 'context': , + 'entity_id': 'sensor.test_time_to_full_charge', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_sensors[sensor.test_tire_pressure_front_left-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.test_tire_pressure_front_left', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Tire pressure front left', + 'platform': 'teslemetry', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'vehicle_state_tpms_pressure_fl', + 'unique_id': 'VINVINVIN-vehicle_state_tpms_pressure_fl', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.test_tire_pressure_front_left-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Tire pressure front left', + }), + 'context': , + 'entity_id': 'sensor.test_tire_pressure_front_left', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.test_tire_pressure_front_left-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Tire pressure front left', + }), + 'context': , + 'entity_id': 'sensor.test_tire_pressure_front_left', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.test_tire_pressure_front_right-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.test_tire_pressure_front_right', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Tire pressure front right', + 'platform': 'teslemetry', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'vehicle_state_tpms_pressure_fr', + 'unique_id': 'VINVINVIN-vehicle_state_tpms_pressure_fr', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.test_tire_pressure_front_right-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Tire pressure front right', + }), + 'context': , + 'entity_id': 'sensor.test_tire_pressure_front_right', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.test_tire_pressure_front_right-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Tire pressure front right', + }), + 'context': , + 'entity_id': 'sensor.test_tire_pressure_front_right', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.test_tire_pressure_rear_left-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.test_tire_pressure_rear_left', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Tire pressure rear left', + 'platform': 'teslemetry', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'vehicle_state_tpms_pressure_rl', + 'unique_id': 'VINVINVIN-vehicle_state_tpms_pressure_rl', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.test_tire_pressure_rear_left-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Tire pressure rear left', + }), + 'context': , + 'entity_id': 'sensor.test_tire_pressure_rear_left', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.test_tire_pressure_rear_left-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Tire pressure rear left', + }), + 'context': , + 'entity_id': 'sensor.test_tire_pressure_rear_left', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.test_tire_pressure_rear_right-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.test_tire_pressure_rear_right', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Tire pressure rear right', + 'platform': 'teslemetry', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'vehicle_state_tpms_pressure_rr', + 'unique_id': 'VINVINVIN-vehicle_state_tpms_pressure_rr', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.test_tire_pressure_rear_right-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Tire pressure rear right', + }), + 'context': , + 'entity_id': 'sensor.test_tire_pressure_rear_right', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.test_tire_pressure_rear_right-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Tire pressure rear right', + }), + 'context': , + 'entity_id': 'sensor.test_tire_pressure_rear_right', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.test_traffic_delay-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.test_traffic_delay', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Traffic delay', + 'platform': 'teslemetry', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'drive_state_active_route_traffic_minutes_delay', + 'unique_id': 'VINVINVIN-drive_state_active_route_traffic_minutes_delay', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.test_traffic_delay-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Traffic delay', + }), + 'context': , + 'entity_id': 'sensor.test_traffic_delay', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.test_traffic_delay-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Traffic delay', + }), + 'context': , + 'entity_id': 'sensor.test_traffic_delay', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.test_usable_battery_level-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.test_usable_battery_level', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Usable battery level', + 'platform': 'teslemetry', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'charge_state_usable_battery_level', + 'unique_id': 'VINVINVIN-charge_state_usable_battery_level', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.test_usable_battery_level-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Usable battery level', + }), + 'context': , + 'entity_id': 'sensor.test_usable_battery_level', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.test_usable_battery_level-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Usable battery level', + }), + 'context': , + 'entity_id': 'sensor.test_usable_battery_level', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.wall_connector_fault_state_code-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.wall_connector_fault_state_code', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Fault state code', + 'platform': 'teslemetry', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wall_connector_fault_state', + 'unique_id': '123456-abd-123-wall_connector_fault_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.wall_connector_fault_state_code-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Wall Connector Fault state code', + }), + 'context': , + 'entity_id': 'sensor.wall_connector_fault_state_code', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2', + }) +# --- +# name: test_sensors[sensor.wall_connector_fault_state_code-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Wall Connector Fault state code', + }), + 'context': , + 'entity_id': 'sensor.wall_connector_fault_state_code', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2', + }) +# --- +# name: test_sensors[sensor.wall_connector_fault_state_code_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.wall_connector_fault_state_code_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Fault state code', + 'platform': 'teslemetry', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wall_connector_fault_state', + 'unique_id': '123456-bcd-234-wall_connector_fault_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.wall_connector_fault_state_code_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Wall Connector Fault state code', + }), + 'context': , + 'entity_id': 'sensor.wall_connector_fault_state_code_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2', + }) +# --- +# name: test_sensors[sensor.wall_connector_fault_state_code_2-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Wall Connector Fault state code', + }), + 'context': , + 'entity_id': 'sensor.wall_connector_fault_state_code_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2', + }) +# --- +# name: test_sensors[sensor.wall_connector_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.wall_connector_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power', + 'platform': 'teslemetry', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wall_connector_power', + 'unique_id': '123456-abd-123-wall_connector_power', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.wall_connector_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Wall Connector Power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.wall_connector_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_sensors[sensor.wall_connector_power-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Wall Connector Power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.wall_connector_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_sensors[sensor.wall_connector_power_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.wall_connector_power_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power', + 'platform': 'teslemetry', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wall_connector_power', + 'unique_id': '123456-bcd-234-wall_connector_power', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.wall_connector_power_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Wall Connector Power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.wall_connector_power_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_sensors[sensor.wall_connector_power_2-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Wall Connector Power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.wall_connector_power_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_sensors[sensor.wall_connector_state_code-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.wall_connector_state_code', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'State code', + 'platform': 'teslemetry', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wall_connector_state', + 'unique_id': '123456-abd-123-wall_connector_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.wall_connector_state_code-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Wall Connector State code', + }), + 'context': , + 'entity_id': 'sensor.wall_connector_state_code', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2', + }) +# --- +# name: test_sensors[sensor.wall_connector_state_code-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Wall Connector State code', + }), + 'context': , + 'entity_id': 'sensor.wall_connector_state_code', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2', + }) +# --- +# name: test_sensors[sensor.wall_connector_state_code_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.wall_connector_state_code_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'State code', + 'platform': 'teslemetry', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wall_connector_state', + 'unique_id': '123456-bcd-234-wall_connector_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.wall_connector_state_code_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Wall Connector State code', + }), + 'context': , + 'entity_id': 'sensor.wall_connector_state_code_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2', + }) +# --- +# name: test_sensors[sensor.wall_connector_state_code_2-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Wall Connector State code', + }), + 'context': , + 'entity_id': 'sensor.wall_connector_state_code_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2', + }) +# --- +# name: test_sensors[sensor.wall_connector_vehicle-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.wall_connector_vehicle', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Vehicle', + 'platform': 'teslemetry', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'vin', + 'unique_id': '123456-abd-123-vin', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.wall_connector_vehicle-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Wall Connector Vehicle', + }), + 'context': , + 'entity_id': 'sensor.wall_connector_vehicle', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.wall_connector_vehicle-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Wall Connector Vehicle', + }), + 'context': , + 'entity_id': 'sensor.wall_connector_vehicle', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.wall_connector_vehicle_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.wall_connector_vehicle_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Vehicle', + 'platform': 'teslemetry', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'vin', + 'unique_id': '123456-bcd-234-vin', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.wall_connector_vehicle_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Wall Connector Vehicle', + }), + 'context': , + 'entity_id': 'sensor.wall_connector_vehicle_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.wall_connector_vehicle_2-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Wall Connector Vehicle', + }), + 'context': , + 'entity_id': 'sensor.wall_connector_vehicle_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- diff --git a/tests/components/teslemetry/test_climate.py b/tests/components/teslemetry/test_climate.py index 2e791f68b93..e83e9d648cd 100644 --- a/tests/components/teslemetry/test_climate.py +++ b/tests/components/teslemetry/test_climate.py @@ -94,10 +94,13 @@ async def test_errors( await setup_platform(hass, platforms=[Platform.CLIMATE]) entity_id = "climate.test_climate" - with patch( - "homeassistant.components.teslemetry.VehicleSpecific.auto_conditioning_start", - side_effect=InvalidCommand, - ) as mock_on, pytest.raises(HomeAssistantError) as error: + with ( + patch( + "homeassistant.components.teslemetry.VehicleSpecific.auto_conditioning_start", + side_effect=InvalidCommand, + ) as mock_on, + pytest.raises(HomeAssistantError) as error, + ): await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_TURN_ON, @@ -148,9 +151,10 @@ async def test_asleep_or_offline( # Run a command but timeout trying to wake up the vehicle mock_wake_up.return_value = WAKE_UP_ASLEEP mock_vehicle.return_value = WAKE_UP_ASLEEP - with patch( - "homeassistant.components.teslemetry.entity.asyncio.sleep" - ), pytest.raises(HomeAssistantError) as error: + with ( + patch("homeassistant.components.teslemetry.entity.asyncio.sleep"), + pytest.raises(HomeAssistantError) as error, + ): await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_TURN_ON, diff --git a/tests/components/teslemetry/test_config_flow.py b/tests/components/teslemetry/test_config_flow.py index b89967bfa35..3757c331996 100644 --- a/tests/components/teslemetry/test_config_flow.py +++ b/tests/components/teslemetry/test_config_flow.py @@ -4,7 +4,11 @@ from unittest.mock import patch from aiohttp import ClientConnectionError import pytest -from tesla_fleet_api.exceptions import InvalidToken, PaymentRequired, TeslaFleetError +from tesla_fleet_api.exceptions import ( + InvalidToken, + SubscriptionRequired, + TeslaFleetError, +) from homeassistant import config_entries from homeassistant.components.teslemetry.const import DOMAIN @@ -54,7 +58,7 @@ async def test_form( ("side_effect", "error"), [ (InvalidToken, {CONF_ACCESS_TOKEN: "invalid_access_token"}), - (PaymentRequired, {"base": "subscription_required"}), + (SubscriptionRequired, {"base": "subscription_required"}), (ClientConnectionError, {"base": "cannot_connect"}), (TeslaFleetError, {"base": "unknown"}), ], diff --git a/tests/components/teslemetry/test_init.py b/tests/components/teslemetry/test_init.py index 28440094bec..9742338f27a 100644 --- a/tests/components/teslemetry/test_init.py +++ b/tests/components/teslemetry/test_init.py @@ -5,7 +5,7 @@ from datetime import timedelta from freezegun.api import FrozenDateTimeFactory from tesla_fleet_api.exceptions import ( InvalidToken, - PaymentRequired, + SubscriptionRequired, TeslaFleetError, VehicleOffline, ) @@ -42,7 +42,7 @@ async def test_auth_failure(hass: HomeAssistant, mock_products) -> None: async def test_subscription_failure(hass: HomeAssistant, mock_products) -> None: """Test init with an client response error.""" - mock_products.side_effect = PaymentRequired + mock_products.side_effect = SubscriptionRequired entry = await setup_platform(hass) assert entry.state is ConfigEntryState.SETUP_ERROR @@ -55,10 +55,10 @@ async def test_other_failure(hass: HomeAssistant, mock_products) -> None: assert entry.state is ConfigEntryState.SETUP_RETRY -# Coordinator +# Vehicle Coordinator -async def test_first_refresh( +async def test_vehicle_first_refresh( hass: HomeAssistant, mock_wake_up, mock_vehicle_data, @@ -88,14 +88,14 @@ async def test_first_refresh( mock_vehicle_data.assert_called_once() -async def test_first_refresh_error(hass: HomeAssistant, mock_wake_up) -> None: +async def test_vehicle_first_refresh_error(hass: HomeAssistant, mock_wake_up) -> None: """Test first coordinator refresh with an error.""" mock_wake_up.side_effect = TeslaFleetError entry = await setup_platform(hass) assert entry.state is ConfigEntryState.SETUP_RETRY -async def test_refresh_offline( +async def test_vehicle_refresh_offline( hass: HomeAssistant, mock_vehicle_data, freezer: FrozenDateTimeFactory ) -> None: """Test coordinator refresh with an error.""" @@ -111,8 +111,18 @@ async def test_refresh_offline( mock_vehicle_data.assert_called_once() -async def test_refresh_error(hass: HomeAssistant, mock_vehicle_data) -> None: +async def test_vehicle_refresh_error(hass: HomeAssistant, mock_vehicle_data) -> None: """Test coordinator refresh with an error.""" mock_vehicle_data.side_effect = TeslaFleetError entry = await setup_platform(hass) assert entry.state is ConfigEntryState.SETUP_RETRY + + +# Test Energy Coordinator + + +async def test_energy_refresh_error(hass: HomeAssistant, mock_live_status) -> None: + """Test coordinator refresh with an error.""" + mock_live_status.side_effect = TeslaFleetError + entry = await setup_platform(hass) + assert entry.state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/teslemetry/test_sensor.py b/tests/components/teslemetry/test_sensor.py new file mode 100644 index 00000000000..be541da6728 --- /dev/null +++ b/tests/components/teslemetry/test_sensor.py @@ -0,0 +1,42 @@ +"""Test the Teslemetry sensor platform.""" + +from datetime import timedelta + +from freezegun.api import FrozenDateTimeFactory +import pytest +from syrupy import SnapshotAssertion + +from homeassistant.components.teslemetry.coordinator import SYNC_INTERVAL +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import assert_entities, assert_entities_alt, setup_platform +from .const import VEHICLE_DATA_ALT + +from tests.common import async_fire_time_changed + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_sensors( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, + freezer: FrozenDateTimeFactory, + mock_vehicle_data, +) -> None: + """Tests that the sensor entities are correct.""" + + freezer.move_to("2024-01-01 00:00:00+00:00") + + entry = await setup_platform(hass, [Platform.SENSOR]) + + assert_entities(hass, entry.entry_id, entity_registry, snapshot) + + # Coordinator refresh + mock_vehicle_data.return_value = VEHICLE_DATA_ALT + freezer.tick(timedelta(seconds=SYNC_INTERVAL)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert_entities_alt(hass, entry.entry_id, entity_registry, snapshot) diff --git a/tests/components/tessie/common.py b/tests/components/tessie/common.py index c57dbda8b53..2f213c4e798 100644 --- a/tests/components/tessie/common.py +++ b/tests/components/tessie/common.py @@ -57,11 +57,14 @@ async def setup_platform( ) mock_entry.add_to_hass(hass) - with patch( - "homeassistant.components.tessie.get_state_of_all_vehicles", - return_value=TEST_STATE_OF_ALL_VEHICLES, - side_effect=side_effect, - ), patch("homeassistant.components.tessie.PLATFORMS", platforms): + with ( + patch( + "homeassistant.components.tessie.get_state_of_all_vehicles", + return_value=TEST_STATE_OF_ALL_VEHICLES, + side_effect=side_effect, + ), + patch("homeassistant.components.tessie.PLATFORMS", platforms), + ): await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/tessie/conftest.py b/tests/components/tessie/conftest.py index 02b3d56691e..f38ef6c7e3f 100644 --- a/tests/components/tessie/conftest.py +++ b/tests/components/tessie/conftest.py @@ -1,4 +1,5 @@ """Fixtures for Tessie.""" + from __future__ import annotations from unittest.mock import patch diff --git a/tests/components/tessie/snapshots/test_binary_sensors.ambr b/tests/components/tessie/snapshots/test_binary_sensors.ambr index cbe7fdba5ec..7bc191de6ed 100644 --- a/tests/components/tessie/snapshots/test_binary_sensors.ambr +++ b/tests/components/tessie/snapshots/test_binary_sensors.ambr @@ -40,6 +40,7 @@ 'context': , 'entity_id': 'binary_sensor.test_auto_seat_climate_left', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }) @@ -85,6 +86,7 @@ 'context': , 'entity_id': 'binary_sensor.test_auto_seat_climate_right', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }) @@ -130,6 +132,7 @@ 'context': , 'entity_id': 'binary_sensor.test_auto_steering_wheel_heater', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }) @@ -162,8 +165,8 @@ 'platform': 'tessie', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'charge_state_battery_heater_on', - 'unique_id': 'VINVINVIN-charge_state_battery_heater_on', + 'translation_key': 'climate_state_battery_heater', + 'unique_id': 'VINVINVIN-climate_state_battery_heater', 'unit_of_measurement': None, }) # --- @@ -176,6 +179,7 @@ 'context': , 'entity_id': 'binary_sensor.test_battery_heater', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }) @@ -222,6 +226,7 @@ 'context': , 'entity_id': 'binary_sensor.test_cabin_overheat_protection', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }) @@ -268,6 +273,7 @@ 'context': , 'entity_id': 'binary_sensor.test_cabin_overheat_protection_actively_cooling', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }) @@ -314,6 +320,7 @@ 'context': , 'entity_id': 'binary_sensor.test_charge_cable', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }) @@ -360,6 +367,7 @@ 'context': , 'entity_id': 'binary_sensor.test_charging', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }) @@ -406,6 +414,7 @@ 'context': , 'entity_id': 'binary_sensor.test_dashcam', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }) @@ -452,6 +461,7 @@ 'context': , 'entity_id': 'binary_sensor.test_front_driver_door', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }) @@ -498,6 +508,7 @@ 'context': , 'entity_id': 'binary_sensor.test_front_driver_window', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }) @@ -544,6 +555,7 @@ 'context': , 'entity_id': 'binary_sensor.test_front_passenger_door', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }) @@ -590,6 +602,7 @@ 'context': , 'entity_id': 'binary_sensor.test_front_passenger_window', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }) @@ -635,6 +648,7 @@ 'context': , 'entity_id': 'binary_sensor.test_preconditioning_enabled', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }) @@ -681,6 +695,7 @@ 'context': , 'entity_id': 'binary_sensor.test_rear_driver_door', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }) @@ -727,6 +742,7 @@ 'context': , 'entity_id': 'binary_sensor.test_rear_driver_window', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }) @@ -773,6 +789,7 @@ 'context': , 'entity_id': 'binary_sensor.test_rear_passenger_door', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }) @@ -819,6 +836,7 @@ 'context': , 'entity_id': 'binary_sensor.test_rear_passenger_window', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }) @@ -864,6 +882,7 @@ 'context': , 'entity_id': 'binary_sensor.test_scheduled_charging_pending', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }) @@ -910,6 +929,7 @@ 'context': , 'entity_id': 'binary_sensor.test_status', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }) @@ -956,6 +976,7 @@ 'context': , 'entity_id': 'binary_sensor.test_tire_pressure_warning_front_left', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }) @@ -1002,6 +1023,7 @@ 'context': , 'entity_id': 'binary_sensor.test_tire_pressure_warning_front_right', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }) @@ -1048,6 +1070,7 @@ 'context': , 'entity_id': 'binary_sensor.test_tire_pressure_warning_rear_left', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }) @@ -1094,6 +1117,7 @@ 'context': , 'entity_id': 'binary_sensor.test_tire_pressure_warning_rear_right', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }) @@ -1139,6 +1163,7 @@ 'context': , 'entity_id': 'binary_sensor.test_trip_charging', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }) @@ -1185,6 +1210,7 @@ 'context': , 'entity_id': 'binary_sensor.test_user_present', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }) diff --git a/tests/components/tessie/snapshots/test_button.ambr b/tests/components/tessie/snapshots/test_button.ambr index c67c2932995..7757d1f2fea 100644 --- a/tests/components/tessie/snapshots/test_button.ambr +++ b/tests/components/tessie/snapshots/test_button.ambr @@ -40,6 +40,7 @@ 'context': , 'entity_id': 'button.test_flash_lights', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }) @@ -85,6 +86,7 @@ 'context': , 'entity_id': 'button.test_homelink', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }) @@ -130,6 +132,7 @@ 'context': , 'entity_id': 'button.test_honk_horn', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }) @@ -175,6 +178,7 @@ 'context': , 'entity_id': 'button.test_keyless_driving', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }) @@ -220,6 +224,7 @@ 'context': , 'entity_id': 'button.test_play_fart', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }) @@ -265,6 +270,7 @@ 'context': , 'entity_id': 'button.test_wake', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }) diff --git a/tests/components/tessie/snapshots/test_climate.ambr b/tests/components/tessie/snapshots/test_climate.ambr index acefc953d3c..959b42cea53 100644 --- a/tests/components/tessie/snapshots/test_climate.ambr +++ b/tests/components/tessie/snapshots/test_climate.ambr @@ -69,6 +69,7 @@ 'context': , 'entity_id': 'climate.test_climate', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }) diff --git a/tests/components/tessie/snapshots/test_cover.ambr b/tests/components/tessie/snapshots/test_cover.ambr index 29930677396..ff04c528244 100644 --- a/tests/components/tessie/snapshots/test_cover.ambr +++ b/tests/components/tessie/snapshots/test_cover.ambr @@ -42,6 +42,7 @@ 'context': , 'entity_id': 'cover.test_charge_port_door', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'open', }) @@ -89,6 +90,7 @@ 'context': , 'entity_id': 'cover.test_frunk', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'closed', }) @@ -136,6 +138,7 @@ 'context': , 'entity_id': 'cover.test_trunk', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'closed', }) @@ -183,6 +186,7 @@ 'context': , 'entity_id': 'cover.test_vent_windows', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'closed', }) diff --git a/tests/components/tessie/snapshots/test_device_tracker.ambr b/tests/components/tessie/snapshots/test_device_tracker.ambr index 640b91627d5..61f89db8637 100644 --- a/tests/components/tessie/snapshots/test_device_tracker.ambr +++ b/tests/components/tessie/snapshots/test_device_tracker.ambr @@ -46,6 +46,7 @@ 'context': , 'entity_id': 'device_tracker.test_location', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'not_home', }) @@ -95,6 +96,7 @@ 'context': , 'entity_id': 'device_tracker.test_route', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'not_home', }) diff --git a/tests/components/tessie/snapshots/test_lock.ambr b/tests/components/tessie/snapshots/test_lock.ambr index a01b14bf00a..1eff418b202 100644 --- a/tests/components/tessie/snapshots/test_lock.ambr +++ b/tests/components/tessie/snapshots/test_lock.ambr @@ -41,6 +41,7 @@ 'context': , 'entity_id': 'lock.test_charge_cable_lock', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'locked', }) @@ -87,6 +88,7 @@ 'context': , 'entity_id': 'lock.test_lock', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'locked', }) @@ -134,6 +136,7 @@ 'context': , 'entity_id': 'lock.test_speed_limit', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unlocked', }) diff --git a/tests/components/tessie/snapshots/test_media_player.ambr b/tests/components/tessie/snapshots/test_media_player.ambr index b8bf84c726c..d30e6c74aef 100644 --- a/tests/components/tessie/snapshots/test_media_player.ambr +++ b/tests/components/tessie/snapshots/test_media_player.ambr @@ -44,6 +44,7 @@ 'context': , 'entity_id': 'media_player.test_media_player', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'idle', }) @@ -59,6 +60,7 @@ 'context': , 'entity_id': 'media_player.test_media_player', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'idle', }) diff --git a/tests/components/tessie/snapshots/test_number.ambr b/tests/components/tessie/snapshots/test_number.ambr index 33abf438128..c91fb74adeb 100644 --- a/tests/components/tessie/snapshots/test_number.ambr +++ b/tests/components/tessie/snapshots/test_number.ambr @@ -51,6 +51,7 @@ 'context': , 'entity_id': 'number.test_charge_current', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '32', }) @@ -107,6 +108,7 @@ 'context': , 'entity_id': 'number.test_charge_limit', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '80', }) @@ -163,6 +165,7 @@ 'context': , 'entity_id': 'number.test_speed_limit', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '74.564543', }) diff --git a/tests/components/tessie/snapshots/test_select.ambr b/tests/components/tessie/snapshots/test_select.ambr index 758bb1c91d8..fc076aabf14 100644 --- a/tests/components/tessie/snapshots/test_select.ambr +++ b/tests/components/tessie/snapshots/test_select.ambr @@ -53,6 +53,7 @@ 'context': , 'entity_id': 'select.test_seat_heater_left', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }) @@ -111,6 +112,7 @@ 'context': , 'entity_id': 'select.test_seat_heater_rear_center', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }) @@ -169,6 +171,7 @@ 'context': , 'entity_id': 'select.test_seat_heater_rear_left', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }) @@ -227,6 +230,7 @@ 'context': , 'entity_id': 'select.test_seat_heater_rear_right', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }) @@ -285,6 +289,7 @@ 'context': , 'entity_id': 'select.test_seat_heater_right', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }) @@ -303,6 +308,7 @@ 'context': , 'entity_id': 'select.test_seat_heater_left', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'low', }) diff --git a/tests/components/tessie/snapshots/test_sensor.ambr b/tests/components/tessie/snapshots/test_sensor.ambr index b05fe3a67d9..48beab6133c 100644 --- a/tests/components/tessie/snapshots/test_sensor.ambr +++ b/tests/components/tessie/snapshots/test_sensor.ambr @@ -45,6 +45,7 @@ 'context': , 'entity_id': 'sensor.test_battery_level', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '75', }) @@ -101,6 +102,7 @@ 'context': , 'entity_id': 'sensor.test_battery_range', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '424.35182592', }) @@ -157,6 +159,7 @@ 'context': , 'entity_id': 'sensor.test_battery_range_estimate', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '522.60227712', }) @@ -213,6 +216,7 @@ 'context': , 'entity_id': 'sensor.test_battery_range_ideal', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '424.35182592', }) @@ -266,6 +270,7 @@ 'context': , 'entity_id': 'sensor.test_charge_energy_added', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '18.47', }) @@ -319,6 +324,7 @@ 'context': , 'entity_id': 'sensor.test_charge_rate', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '49.2', }) @@ -369,6 +375,7 @@ 'context': , 'entity_id': 'sensor.test_charger_current', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '32', }) @@ -419,6 +426,7 @@ 'context': , 'entity_id': 'sensor.test_charger_power', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '7', }) @@ -469,6 +477,7 @@ 'context': , 'entity_id': 'sensor.test_charger_voltage', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '224', }) @@ -532,6 +541,7 @@ 'context': , 'entity_id': 'sensor.test_charging', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'charging', }) @@ -577,6 +587,7 @@ 'context': , 'entity_id': 'sensor.test_destination', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'Giga Texas', }) @@ -666,6 +677,7 @@ 'context': , 'entity_id': 'sensor.test_distance_to_arrival', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '75.168198', }) @@ -719,6 +731,7 @@ 'context': , 'entity_id': 'sensor.test_driver_temperature_setting', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '22.5', }) @@ -772,6 +785,7 @@ 'context': , 'entity_id': 'sensor.test_inside_temperature', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '30.4', }) @@ -828,6 +842,7 @@ 'context': , 'entity_id': 'sensor.test_odometer', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '8778.15941765875', }) @@ -881,6 +896,7 @@ 'context': , 'entity_id': 'sensor.test_outside_temperature', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '30.5', }) @@ -934,6 +950,7 @@ 'context': , 'entity_id': 'sensor.test_passenger_temperature_setting', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '22.5', }) @@ -984,6 +1001,7 @@ 'context': , 'entity_id': 'sensor.test_power', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '-7', }) @@ -1043,6 +1061,7 @@ 'context': , 'entity_id': 'sensor.test_shift_state', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }) @@ -1096,6 +1115,7 @@ 'context': , 'entity_id': 'sensor.test_speed', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }) @@ -1146,6 +1166,7 @@ 'context': , 'entity_id': 'sensor.test_state_of_charge_at_arrival', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '65', }) @@ -1192,6 +1213,7 @@ 'context': , 'entity_id': 'sensor.test_time_to_arrival', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '2024-01-01T00:59:12+00:00', }) @@ -1238,6 +1260,7 @@ 'context': , 'entity_id': 'sensor.test_time_to_full_charge', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }) @@ -1294,6 +1317,7 @@ 'context': , 'entity_id': 'sensor.test_tire_pressure_front_left', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '43.1487288094417', }) @@ -1350,6 +1374,7 @@ 'context': , 'entity_id': 'sensor.test_tire_pressure_front_right', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '43.1487288094417', }) @@ -1406,6 +1431,7 @@ 'context': , 'entity_id': 'sensor.test_tire_pressure_rear_left', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '42.7861344496985', }) @@ -1462,6 +1488,7 @@ 'context': , 'entity_id': 'sensor.test_tire_pressure_rear_right', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '42.7861344496985', }) @@ -1512,6 +1539,7 @@ 'context': , 'entity_id': 'sensor.test_traffic_delay', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0', }) diff --git a/tests/components/tessie/snapshots/test_switch.ambr b/tests/components/tessie/snapshots/test_switch.ambr index a70210f8fbf..db06e028198 100644 --- a/tests/components/tessie/snapshots/test_switch.ambr +++ b/tests/components/tessie/snapshots/test_switch.ambr @@ -41,6 +41,7 @@ 'context': , 'entity_id': 'switch.test_charge', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }) @@ -87,6 +88,7 @@ 'context': , 'entity_id': 'switch.test_defrost_mode', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }) @@ -133,6 +135,7 @@ 'context': , 'entity_id': 'switch.test_sentry_mode', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }) @@ -179,6 +182,7 @@ 'context': , 'entity_id': 'switch.test_steering_wheel_heater', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }) @@ -225,6 +229,7 @@ 'context': , 'entity_id': 'switch.test_valet_mode', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }) @@ -238,6 +243,7 @@ 'context': , 'entity_id': 'switch.test_charge', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }) @@ -251,6 +257,7 @@ 'context': , 'entity_id': 'switch.test_charge', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }) diff --git a/tests/components/tessie/snapshots/test_update.ambr b/tests/components/tessie/snapshots/test_update.ambr index 6690bbf8651..622cf69c7f0 100644 --- a/tests/components/tessie/snapshots/test_update.ambr +++ b/tests/components/tessie/snapshots/test_update.ambr @@ -50,6 +50,7 @@ 'context': , 'entity_id': 'update.test_update', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }) diff --git a/tests/components/tessie/test_binary_sensors.py b/tests/components/tessie/test_binary_sensors.py index b6dccd9d3b1..0ced8a6d8aa 100644 --- a/tests/components/tessie/test_binary_sensors.py +++ b/tests/components/tessie/test_binary_sensors.py @@ -1,4 +1,5 @@ """Test the Tessie binary sensor platform.""" + import pytest from syrupy import SnapshotAssertion diff --git a/tests/components/tessie/test_button.py b/tests/components/tessie/test_button.py index 674e7a32747..fa6c8358ae6 100644 --- a/tests/components/tessie/test_button.py +++ b/tests/components/tessie/test_button.py @@ -1,4 +1,5 @@ """Test the Tessie button platform.""" + from unittest.mock import patch from syrupy import SnapshotAssertion diff --git a/tests/components/tessie/test_climate.py b/tests/components/tessie/test_climate.py index 6d1c8c220d1..df86f0b2986 100644 --- a/tests/components/tessie/test_climate.py +++ b/tests/components/tessie/test_climate.py @@ -1,4 +1,5 @@ """Test the Tessie climate platform.""" + from unittest.mock import patch import pytest @@ -51,13 +52,16 @@ async def test_climate( assert state.state == HVACMode.HEAT_COOL # Test setting climate temp - with patch( - "homeassistant.components.tessie.climate.set_temperature", - return_value=TEST_RESPONSE, - ) as mock_set, patch( - "homeassistant.components.tessie.climate.start_climate_preconditioning", - return_value=TEST_RESPONSE, - ) as mock_set2: + with ( + patch( + "homeassistant.components.tessie.climate.set_temperature", + return_value=TEST_RESPONSE, + ) as mock_set, + patch( + "homeassistant.components.tessie.climate.start_climate_preconditioning", + return_value=TEST_RESPONSE, + ) as mock_set2, + ): await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, @@ -111,10 +115,13 @@ async def test_errors(hass: HomeAssistant) -> None: entity_id = "climate.test_climate" # Test setting climate on with unknown error - with patch( - "homeassistant.components.tessie.climate.stop_climate", - side_effect=ERROR_UNKNOWN, - ) as mock_set, pytest.raises(HomeAssistantError) as error: + with ( + patch( + "homeassistant.components.tessie.climate.stop_climate", + side_effect=ERROR_UNKNOWN, + ) as mock_set, + pytest.raises(HomeAssistantError) as error, + ): await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_TURN_OFF, diff --git a/tests/components/tessie/test_coordinator.py b/tests/components/tessie/test_coordinator.py index 14bb6b7d203..c4c1b6d1e72 100644 --- a/tests/components/tessie/test_coordinator.py +++ b/tests/components/tessie/test_coordinator.py @@ -1,4 +1,5 @@ """Test the Tessie sensor platform.""" + from datetime import timedelta from homeassistant.components.tessie import PLATFORMS diff --git a/tests/components/tessie/test_cover.py b/tests/components/tessie/test_cover.py index c86cce466e1..ebf4c503110 100644 --- a/tests/components/tessie/test_cover.py +++ b/tests/components/tessie/test_cover.py @@ -1,4 +1,5 @@ """Test the Tessie cover platform.""" + from unittest.mock import patch import pytest @@ -80,10 +81,13 @@ async def test_errors(hass: HomeAssistant) -> None: entity_id = "cover.test_charge_port_door" # Test setting cover open with unknown error - with patch( - "homeassistant.components.tessie.cover.open_unlock_charge_port", - side_effect=ERROR_UNKNOWN, - ) as mock_set, pytest.raises(HomeAssistantError) as error: + with ( + patch( + "homeassistant.components.tessie.cover.open_unlock_charge_port", + side_effect=ERROR_UNKNOWN, + ) as mock_set, + pytest.raises(HomeAssistantError) as error, + ): await hass.services.async_call( COVER_DOMAIN, SERVICE_OPEN_COVER, @@ -94,10 +98,13 @@ async def test_errors(hass: HomeAssistant) -> None: assert error.from_exception == ERROR_UNKNOWN # Test setting cover open with unknown error - with patch( - "homeassistant.components.tessie.cover.open_unlock_charge_port", - return_value=TEST_RESPONSE_ERROR, - ) as mock_set, pytest.raises(HomeAssistantError) as error: + with ( + patch( + "homeassistant.components.tessie.cover.open_unlock_charge_port", + return_value=TEST_RESPONSE_ERROR, + ) as mock_set, + pytest.raises(HomeAssistantError) as error, + ): await hass.services.async_call( COVER_DOMAIN, SERVICE_OPEN_COVER, diff --git a/tests/components/tessie/test_select.py b/tests/components/tessie/test_select.py index d22f8cccad7..7f79dbe3297 100644 --- a/tests/components/tessie/test_select.py +++ b/tests/components/tessie/test_select.py @@ -1,4 +1,5 @@ """Test the Tessie select platform.""" + from unittest.mock import patch import pytest @@ -52,10 +53,13 @@ async def test_errors(hass: HomeAssistant) -> None: entity_id = "select.test_seat_heater_left" # Test setting cover open with unknown error - with patch( - "homeassistant.components.tessie.select.set_seat_heat", - side_effect=ERROR_UNKNOWN, - ) as mock_set, pytest.raises(HomeAssistantError) as error: + with ( + patch( + "homeassistant.components.tessie.select.set_seat_heat", + side_effect=ERROR_UNKNOWN, + ) as mock_set, + pytest.raises(HomeAssistantError) as error, + ): await hass.services.async_call( SELECT_DOMAIN, SERVICE_SELECT_OPTION, diff --git a/tests/components/tessie/test_sensor.py b/tests/components/tessie/test_sensor.py index efe34df7226..92256d25eb1 100644 --- a/tests/components/tessie/test_sensor.py +++ b/tests/components/tessie/test_sensor.py @@ -1,4 +1,5 @@ """Test the Tessie sensor platform.""" + from freezegun.api import FrozenDateTimeFactory import pytest from syrupy import SnapshotAssertion diff --git a/tests/components/tessie/test_switch.py b/tests/components/tessie/test_switch.py index 60f3fab490c..907be29ddcc 100644 --- a/tests/components/tessie/test_switch.py +++ b/tests/components/tessie/test_switch.py @@ -1,4 +1,5 @@ """Test the Tessie switch platform.""" + from unittest.mock import patch from syrupy import SnapshotAssertion diff --git a/tests/components/tessie/test_update.py b/tests/components/tessie/test_update.py index 54e56c46b50..8d098e9a966 100644 --- a/tests/components/tessie/test_update.py +++ b/tests/components/tessie/test_update.py @@ -1,4 +1,5 @@ """Test the Tessie update platform.""" + from unittest.mock import patch from syrupy import SnapshotAssertion diff --git a/tests/components/text/test_device_action.py b/tests/components/text/test_device_action.py index 59b77ecfa06..6a0e0958558 100644 --- a/tests/components/text/test_device_action.py +++ b/tests/components/text/test_device_action.py @@ -1,4 +1,5 @@ """The tests for Text device actions.""" + import pytest from pytest_unordered import unordered import voluptuous_serialize @@ -61,12 +62,12 @@ async def test_get_actions( @pytest.mark.parametrize( ("hidden_by", "entity_category"), - ( + [ (RegistryEntryHider.INTEGRATION, None), (RegistryEntryHider.USER, None), (None, EntityCategory.CONFIG), (None, EntityCategory.DIAGNOSTIC), - ), + ], ) async def test_get_actions_hidden_auxiliary( hass: HomeAssistant, diff --git a/tests/components/text/test_init.py b/tests/components/text/test_init.py index d144cc86c91..7b44903eec3 100644 --- a/tests/components/text/test_init.py +++ b/tests/components/text/test_init.py @@ -1,4 +1,5 @@ """The tests for the text component.""" + from typing import Any import pytest diff --git a/tests/components/text/test_recorder.py b/tests/components/text/test_recorder.py index da9ec810da9..3aa4424b904 100644 --- a/tests/components/text/test_recorder.py +++ b/tests/components/text/test_recorder.py @@ -1,4 +1,5 @@ """The tests for text recorder.""" + from __future__ import annotations from datetime import timedelta diff --git a/tests/components/text/test_reproduce_state.py b/tests/components/text/test_reproduce_state.py index 94c9b8127f6..d87e0e983de 100644 --- a/tests/components/text/test_reproduce_state.py +++ b/tests/components/text/test_reproduce_state.py @@ -1,4 +1,5 @@ """Test reproduce state for Text entities.""" + import pytest from homeassistant.components.text.const import ( diff --git a/tests/components/thermobeacon/__init__.py b/tests/components/thermobeacon/__init__.py index 1ff1ad20df1..2f7e220ebaa 100644 --- a/tests/components/thermobeacon/__init__.py +++ b/tests/components/thermobeacon/__init__.py @@ -1,6 +1,5 @@ """Tests for the ThermoBeacon integration.""" - from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo NOT_THERMOBEACON_SERVICE_INFO = BluetoothServiceInfo( diff --git a/tests/components/thermobeacon/test_config_flow.py b/tests/components/thermobeacon/test_config_flow.py index eba12c11177..a63ccf08963 100644 --- a/tests/components/thermobeacon/test_config_flow.py +++ b/tests/components/thermobeacon/test_config_flow.py @@ -1,4 +1,5 @@ """Test the ThermoBeacon config flow.""" + from unittest.mock import patch from homeassistant import config_entries diff --git a/tests/components/thermobeacon/test_sensor.py b/tests/components/thermobeacon/test_sensor.py index e8d77e3a487..199d44821b6 100644 --- a/tests/components/thermobeacon/test_sensor.py +++ b/tests/components/thermobeacon/test_sensor.py @@ -1,4 +1,5 @@ """Test the ThermoBeacon sensors.""" + from homeassistant.components.sensor import ATTR_STATE_CLASS from homeassistant.components.thermobeacon.const import DOMAIN from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT diff --git a/tests/components/thermopro/__init__.py b/tests/components/thermopro/__init__.py index f66b608f6d3..264e556756c 100644 --- a/tests/components/thermopro/__init__.py +++ b/tests/components/thermopro/__init__.py @@ -1,6 +1,5 @@ """Tests for the ThermoPro integration.""" - from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo NOT_THERMOPRO_SERVICE_INFO = BluetoothServiceInfo( diff --git a/tests/components/thermopro/test_config_flow.py b/tests/components/thermopro/test_config_flow.py index dfd0e39777c..0ee86cd5067 100644 --- a/tests/components/thermopro/test_config_flow.py +++ b/tests/components/thermopro/test_config_flow.py @@ -1,4 +1,5 @@ """Test the ThermoPro config flow.""" + from unittest.mock import patch from homeassistant import config_entries diff --git a/tests/components/thermopro/test_sensor.py b/tests/components/thermopro/test_sensor.py index d754991f3d8..e9f234100b4 100644 --- a/tests/components/thermopro/test_sensor.py +++ b/tests/components/thermopro/test_sensor.py @@ -1,4 +1,5 @@ """Test the ThermoPro config flow.""" + from homeassistant.components.sensor import ATTR_STATE_CLASS from homeassistant.components.thermopro.const import DOMAIN from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT diff --git a/tests/components/thread/test_config_flow.py b/tests/components/thread/test_config_flow.py index 51ebe3b5976..9f4930947ef 100644 --- a/tests/components/thread/test_config_flow.py +++ b/tests/components/thread/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Thread config flow.""" + from ipaddress import ip_address from unittest.mock import patch @@ -131,12 +132,15 @@ async def test_zeroconf(hass: HomeAssistant) -> None: async def test_zeroconf_setup_onboarding(hass: HomeAssistant) -> None: """Test we automatically finish a zeroconf flow during onboarding.""" - with patch( - "homeassistant.components.onboarding.async_is_onboarded", return_value=False - ), patch( - "homeassistant.components.thread.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.onboarding.async_is_onboarded", return_value=False + ), + patch( + "homeassistant.components.thread.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result = await hass.config_entries.flow.async_init( thread.DOMAIN, context={"source": "zeroconf"}, data=TEST_ZEROCONF_RECORD ) diff --git a/tests/components/thread/test_dataset_store.py b/tests/components/thread/test_dataset_store.py index bcc16de4ed2..a0d85fc6cea 100644 --- a/tests/components/thread/test_dataset_store.py +++ b/tests/components/thread/test_dataset_store.py @@ -1,4 +1,5 @@ """Test the thread dataset store.""" + import asyncio from typing import Any from unittest.mock import ANY, AsyncMock, patch diff --git a/tests/components/thread/test_discovery.py b/tests/components/thread/test_discovery.py index 12eddb0b92a..bdfd0390b9a 100644 --- a/tests/components/thread/test_discovery.py +++ b/tests/components/thread/test_discovery.py @@ -194,7 +194,7 @@ async def test_discover_routers_unconfigured( @pytest.mark.parametrize( - "data", (ROUTER_DISCOVERY_HASS_BAD_DATA, ROUTER_DISCOVERY_HASS_MISSING_DATA) + "data", [ROUTER_DISCOVERY_HASS_BAD_DATA, ROUTER_DISCOVERY_HASS_MISSING_DATA] ) async def test_discover_routers_bad_or_missing_optional_data( hass: HomeAssistant, mock_async_zeroconf: None, data diff --git a/tests/components/threshold/test_config_flow.py b/tests/components/threshold/test_config_flow.py index 8229ed1b1ef..726fa04cef0 100644 --- a/tests/components/threshold/test_config_flow.py +++ b/tests/components/threshold/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Threshold config flow.""" + from unittest.mock import patch import pytest @@ -60,7 +61,7 @@ async def test_config_flow(hass: HomeAssistant) -> None: assert config_entry.title == "My threshold sensor" -@pytest.mark.parametrize(("extra_input_data", "error"), (({}, "need_lower_upper"),)) +@pytest.mark.parametrize(("extra_input_data", "error"), [({}, "need_lower_upper")]) async def test_fail(hass: HomeAssistant, extra_input_data, error) -> None: """Test not providing lower or upper limit fails.""" input_sensor = "sensor.input" diff --git a/tests/components/threshold/test_init.py b/tests/components/threshold/test_init.py index 82d183ad380..86b580c47f5 100644 --- a/tests/components/threshold/test_init.py +++ b/tests/components/threshold/test_init.py @@ -1,4 +1,5 @@ """Test the Min/Max integration.""" + import pytest from homeassistant.components.threshold.const import DOMAIN @@ -8,7 +9,7 @@ from homeassistant.helpers import entity_registry as er from tests.common import MockConfigEntry -@pytest.mark.parametrize("platform", ("binary_sensor",)) +@pytest.mark.parametrize("platform", ["binary_sensor"]) async def test_setup_and_remove_config_entry( hass: HomeAssistant, platform: str, diff --git a/tests/components/tibber/conftest.py b/tests/components/tibber/conftest.py index 3fa503ed002..da3f3df1bd9 100644 --- a/tests/components/tibber/conftest.py +++ b/tests/components/tibber/conftest.py @@ -1,4 +1,5 @@ """Test helpers for Tibber.""" + import pytest from homeassistant.components.tibber.const import DOMAIN diff --git a/tests/components/tibber/test_common.py b/tests/components/tibber/test_common.py index 7faa07a6d9f..8d6b22ad7f7 100644 --- a/tests/components/tibber/test_common.py +++ b/tests/components/tibber/test_common.py @@ -1,4 +1,5 @@ """Test common.""" + import datetime as dt from unittest.mock import AsyncMock diff --git a/tests/components/tibber/test_config_flow.py b/tests/components/tibber/test_config_flow.py index 545a79ff56f..b6c616c5cf0 100644 --- a/tests/components/tibber/test_config_flow.py +++ b/tests/components/tibber/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for Tibber config flow.""" + from asyncio import TimeoutError from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch diff --git a/tests/components/tibber/test_diagnostics.py b/tests/components/tibber/test_diagnostics.py index 9446778637e..34ecb63dfec 100644 --- a/tests/components/tibber/test_diagnostics.py +++ b/tests/components/tibber/test_diagnostics.py @@ -1,4 +1,5 @@ """Test the Netatmo diagnostics.""" + from unittest.mock import patch from homeassistant.components.recorder import Recorder @@ -18,10 +19,13 @@ async def test_entry_diagnostics( config_entry, ) -> None: """Test config entry diagnostics.""" - with patch( - "tibber.Tibber.update_info", - return_value=None, - ), patch("homeassistant.components.tibber.discovery.async_load_platform"): + with ( + patch( + "tibber.Tibber.update_info", + return_value=None, + ), + patch("homeassistant.components.tibber.discovery.async_load_platform"), + ): assert await async_setup_component(hass, "tibber", {}) await hass.async_block_till_done() diff --git a/tests/components/tibber/test_statistics.py b/tests/components/tibber/test_statistics.py index 566e5a651a5..d6c510a8785 100644 --- a/tests/components/tibber/test_statistics.py +++ b/tests/components/tibber/test_statistics.py @@ -1,4 +1,5 @@ """Test adding external statistics from Tibber.""" + from unittest.mock import AsyncMock from homeassistant.components.recorder import Recorder diff --git a/tests/components/tile/conftest.py b/tests/components/tile/conftest.py index 577abd6fa70..e3b55c49ae7 100644 --- a/tests/components/tile/conftest.py +++ b/tests/components/tile/conftest.py @@ -1,4 +1,5 @@ """Define test fixtures for Tile.""" + import json from unittest.mock import AsyncMock, Mock, patch @@ -53,9 +54,12 @@ def data_tile_details_fixture(): @pytest.fixture(name="mock_pytile") async def mock_pytile_fixture(api): """Define a fixture to patch pytile.""" - with patch( - "homeassistant.components.tile.config_flow.async_login", return_value=api - ), patch("homeassistant.components.tile.async_login", return_value=api): + with ( + patch( + "homeassistant.components.tile.config_flow.async_login", return_value=api + ), + patch("homeassistant.components.tile.async_login", return_value=api), + ): yield diff --git a/tests/components/tile/test_config_flow.py b/tests/components/tile/test_config_flow.py index d35f928787c..5d269bfee5d 100644 --- a/tests/components/tile/test_config_flow.py +++ b/tests/components/tile/test_config_flow.py @@ -1,4 +1,5 @@ """Define tests for the Tile config flow.""" + from unittest.mock import AsyncMock, patch import pytest diff --git a/tests/components/tile/test_diagnostics.py b/tests/components/tile/test_diagnostics.py index 8af2c513202..2ac3e06ccd8 100644 --- a/tests/components/tile/test_diagnostics.py +++ b/tests/components/tile/test_diagnostics.py @@ -1,4 +1,5 @@ """Test Tile diagnostics.""" + from syrupy import SnapshotAssertion from homeassistant.core import HomeAssistant diff --git a/tests/components/tilt_ble/test_config_flow.py b/tests/components/tilt_ble/test_config_flow.py index e972aee0bb9..b9623f9700d 100644 --- a/tests/components/tilt_ble/test_config_flow.py +++ b/tests/components/tilt_ble/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Tilt config flow.""" + from unittest.mock import patch from homeassistant import config_entries diff --git a/tests/components/time/test_init.py b/tests/components/time/test_init.py index a3248c96361..20360279217 100644 --- a/tests/components/time/test_init.py +++ b/tests/components/time/test_init.py @@ -1,4 +1,5 @@ """The tests for the time component.""" + from datetime import time from homeassistant.components.time import DOMAIN, SERVICE_SET_VALUE, TimeEntity diff --git a/tests/components/time_date/conftest.py b/tests/components/time_date/conftest.py index af732f978b4..72363dcdf9e 100644 --- a/tests/components/time_date/conftest.py +++ b/tests/components/time_date/conftest.py @@ -1,4 +1,5 @@ """Fixtures for Time & Date integration tests.""" + from collections.abc import Generator from unittest.mock import AsyncMock, patch diff --git a/tests/components/time_date/test_config_flow.py b/tests/components/time_date/test_config_flow.py index 228a34b65b4..7402fc529d1 100644 --- a/tests/components/time_date/test_config_flow.py +++ b/tests/components/time_date/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Time & Date config flow.""" + from __future__ import annotations from unittest.mock import AsyncMock diff --git a/tests/components/timer/test_init.py b/tests/components/timer/test_init.py index 92baa013f14..5aca1625d1f 100644 --- a/tests/components/timer/test_init.py +++ b/tests/components/timer/test_init.py @@ -1,4 +1,5 @@ """The tests for the timer component.""" + from datetime import timedelta import logging from unittest.mock import patch diff --git a/tests/components/timer/test_reproduce_state.py b/tests/components/timer/test_reproduce_state.py index 2d4fca68dfe..fcc331fef50 100644 --- a/tests/components/timer/test_reproduce_state.py +++ b/tests/components/timer/test_reproduce_state.py @@ -1,4 +1,5 @@ """Test reproduce state for Timer.""" + import pytest from homeassistant.components.timer import ( diff --git a/tests/components/tod/test_binary_sensor.py b/tests/components/tod/test_binary_sensor.py index c7979b884d4..1a2e1ad9849 100644 --- a/tests/components/tod/test_binary_sensor.py +++ b/tests/components/tod/test_binary_sensor.py @@ -1,4 +1,5 @@ """Test Times of the Day Binary Sensor.""" + from datetime import datetime, timedelta from freezegun.api import FrozenDateTimeFactory @@ -822,7 +823,7 @@ async def test_dst6( @pytest.mark.freeze_time("2019-01-10 18:43:00") -@pytest.mark.parametrize("hass_time_zone", ("UTC",)) +@pytest.mark.parametrize("hass_time_zone", ["UTC"]) async def test_simple_before_after_does_not_loop_utc_not_in_range( hass: HomeAssistant, ) -> None: @@ -848,7 +849,7 @@ async def test_simple_before_after_does_not_loop_utc_not_in_range( @pytest.mark.freeze_time("2019-01-10 22:43:00") -@pytest.mark.parametrize("hass_time_zone", ("UTC",)) +@pytest.mark.parametrize("hass_time_zone", ["UTC"]) async def test_simple_before_after_does_not_loop_utc_in_range( hass: HomeAssistant, ) -> None: @@ -874,7 +875,7 @@ async def test_simple_before_after_does_not_loop_utc_in_range( @pytest.mark.freeze_time("2019-01-11 06:00:00") -@pytest.mark.parametrize("hass_time_zone", ("UTC",)) +@pytest.mark.parametrize("hass_time_zone", ["UTC"]) async def test_simple_before_after_does_not_loop_utc_fire_at_before( hass: HomeAssistant, ) -> None: @@ -900,7 +901,7 @@ async def test_simple_before_after_does_not_loop_utc_fire_at_before( @pytest.mark.freeze_time("2019-01-10 22:00:00") -@pytest.mark.parametrize("hass_time_zone", ("UTC",)) +@pytest.mark.parametrize("hass_time_zone", ["UTC"]) async def test_simple_before_after_does_not_loop_utc_fire_at_after( hass: HomeAssistant, ) -> None: @@ -926,7 +927,7 @@ async def test_simple_before_after_does_not_loop_utc_fire_at_after( @pytest.mark.freeze_time("2019-01-10 22:00:00") -@pytest.mark.parametrize("hass_time_zone", ("UTC",)) +@pytest.mark.parametrize("hass_time_zone", ["UTC"]) async def test_simple_before_after_does_not_loop_utc_both_before_now( hass: HomeAssistant, ) -> None: @@ -952,7 +953,7 @@ async def test_simple_before_after_does_not_loop_utc_both_before_now( @pytest.mark.freeze_time("2019-01-10 17:43:00+01:00") -@pytest.mark.parametrize("hass_time_zone", ("Europe/Berlin",)) +@pytest.mark.parametrize("hass_time_zone", ["Europe/Berlin"]) async def test_simple_before_after_does_not_loop_berlin_not_in_range( hass: HomeAssistant, ) -> None: @@ -978,7 +979,7 @@ async def test_simple_before_after_does_not_loop_berlin_not_in_range( @pytest.mark.freeze_time("2019-01-11 00:43:00+01:00") -@pytest.mark.parametrize("hass_time_zone", ("Europe/Berlin",)) +@pytest.mark.parametrize("hass_time_zone", ["Europe/Berlin"]) async def test_simple_before_after_does_not_loop_berlin_in_range( hass: HomeAssistant, ) -> None: diff --git a/tests/components/tod/test_config_flow.py b/tests/components/tod/test_config_flow.py index 6860d401ce2..c56accf103c 100644 --- a/tests/components/tod/test_config_flow.py +++ b/tests/components/tod/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Times of the Day config flow.""" + from unittest.mock import patch import pytest @@ -11,7 +12,7 @@ from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry -@pytest.mark.parametrize("platform", ("sensor",)) +@pytest.mark.parametrize("platform", ["sensor"]) async def test_config_flow(hass: HomeAssistant, platform) -> None: """Test the config flow.""" result = await hass.config_entries.flow.async_init( diff --git a/tests/components/todo/test_init.py b/tests/components/todo/test_init.py index 5a8f6183cbb..4e52c2fff70 100644 --- a/tests/components/todo/test_init.py +++ b/tests/components/todo/test_init.py @@ -227,23 +227,11 @@ async def test_list_todo_items( [ ({}, [ITEM_1, ITEM_2]), ( - [ - {"status": [TodoItemStatus.COMPLETED, TodoItemStatus.NEEDS_ACTION]}, - [ITEM_1, ITEM_2], - ] - ), - ( - [ - {"status": [TodoItemStatus.NEEDS_ACTION]}, - [ITEM_1], - ] - ), - ( - [ - {"status": [TodoItemStatus.COMPLETED]}, - [ITEM_2], - ] + {"status": [TodoItemStatus.COMPLETED, TodoItemStatus.NEEDS_ACTION]}, + [ITEM_1, ITEM_2], ), + ({"status": [TodoItemStatus.NEEDS_ACTION]}, [ITEM_1]), + ({"status": [TodoItemStatus.COMPLETED]}, [ITEM_2]), ], ) async def test_get_items_service( @@ -348,12 +336,12 @@ async def test_add_item_service_raises( ( {"item": "Submit forms", "description": "Submit tax forms"}, ServiceValidationError, - "does not support setting field 'description'", + "does not support setting field: description", ), ( {"item": "Submit forms", "due_date": "2023-11-17"}, ServiceValidationError, - "does not support setting field 'due_date'", + "does not support setting field: due_date", ), ( { @@ -361,7 +349,7 @@ async def test_add_item_service_raises( "due_datetime": f"2023-11-17T17:00:00{TEST_OFFSET}", }, ServiceValidationError, - "does not support setting field 'due_datetime'", + "does not support setting field: due_datetime", ), ], ) @@ -376,7 +364,7 @@ async def test_add_item_service_invalid_input( await create_mock_platform(hass, [test_entity]) - with pytest.raises(expected_exception, match=expected_error): + with pytest.raises(expected_exception) as exc: await hass.services.async_call( DOMAIN, "add_item", @@ -385,10 +373,12 @@ async def test_add_item_service_invalid_input( blocking=True, ) + assert expected_error in str(exc.value) + @pytest.mark.parametrize( ("supported_entity_feature", "item_data", "expected_item"), - ( + [ ( TodoListEntityFeature.SET_DUE_DATE_ON_ITEM, {"item": "New item", "due_date": "2023-11-13"}, @@ -434,7 +424,7 @@ async def test_add_item_service_invalid_input( description="Submit revised draft", ), ), - ), + ], ) async def test_add_item_service_extended_fields( hass: HomeAssistant, @@ -693,7 +683,7 @@ async def test_update_todo_item_field_unsupported( @pytest.mark.parametrize( ("supported_entity_feature", "update_data", "expected_update"), - ( + [ ( TodoListEntityFeature.SET_DUE_DATE_ON_ITEM, {"due_date": "2023-11-13"}, @@ -724,7 +714,7 @@ async def test_update_todo_item_field_unsupported( description="Submit revised draft", ), ), - ), + ], ) async def test_update_todo_item_extended_fields( hass: HomeAssistant, @@ -754,7 +744,7 @@ async def test_update_todo_item_extended_fields( @pytest.mark.parametrize( ("test_entity_items", "update_data", "expected_update"), - ( + [ ( [TodoItem(uid="1", summary="Summary", description="description")], {"description": "Submit revised draft"}, @@ -802,7 +792,7 @@ async def test_update_todo_item_extended_fields( {"due_datetime": None}, TodoItem(uid="1", summary="Summary"), ), - ), + ], ids=[ "overwrite_description", "overwrite_empty_description", diff --git a/tests/components/todoist/conftest.py b/tests/components/todoist/conftest.py index 42251b0ea18..4968b6beefb 100644 --- a/tests/components/todoist/conftest.py +++ b/tests/components/todoist/conftest.py @@ -1,4 +1,5 @@ """Common fixtures for the todoist tests.""" + from collections.abc import Generator from http import HTTPStatus from unittest.mock import AsyncMock, patch @@ -152,9 +153,10 @@ async def mock_setup_integration( """Mock setup of the todoist integration.""" if todoist_config_entry is not None: todoist_config_entry.add_to_hass(hass) - with patch( - "homeassistant.components.todoist.TodoistAPIAsync", return_value=api - ), patch("homeassistant.components.todoist.PLATFORMS", platforms): + with ( + patch("homeassistant.components.todoist.TodoistAPIAsync", return_value=api), + patch("homeassistant.components.todoist.PLATFORMS", platforms), + ): assert await async_setup_component(hass, DOMAIN, {}) await hass.async_block_till_done() yield diff --git a/tests/components/todoist/test_calendar.py b/tests/components/todoist/test_calendar.py index 761eeb07c61..ddffd879d46 100644 --- a/tests/components/todoist/test_calendar.py +++ b/tests/components/todoist/test_calendar.py @@ -1,4 +1,5 @@ """Unit tests for the Todoist calendar platform.""" + from datetime import timedelta from http import HTTPStatus from typing import Any diff --git a/tests/components/todoist/test_init.py b/tests/components/todoist/test_init.py index 0e80be5410f..62915eb0fdd 100644 --- a/tests/components/todoist/test_init.py +++ b/tests/components/todoist/test_init.py @@ -1,4 +1,5 @@ """Unit tests for the Todoist integration.""" + from http import HTTPStatus from unittest.mock import AsyncMock diff --git a/tests/components/todoist/test_todo.py b/tests/components/todoist/test_todo.py index a227ec858e4..373eb0158ea 100644 --- a/tests/components/todoist/test_todo.py +++ b/tests/components/todoist/test_todo.py @@ -1,4 +1,5 @@ """Unit tests for the Todoist todo platform.""" + from typing import Any from unittest.mock import AsyncMock diff --git a/tests/components/tolo/test_config_flow.py b/tests/components/tolo/test_config_flow.py index 77547557b63..711ded3880b 100644 --- a/tests/components/tolo/test_config_flow.py +++ b/tests/components/tolo/test_config_flow.py @@ -1,8 +1,9 @@ """Tests for the TOLO Sauna config flow.""" + from unittest.mock import Mock, patch import pytest -from tololib.errors import ResponseTimedOutError +from tololib import ToloCommunicationError from homeassistant.components import dhcp from homeassistant.components.tolo.const import DOMAIN @@ -37,7 +38,7 @@ def coordinator_toloclient() -> Mock: async def test_user_with_timed_out_host(hass: HomeAssistant, toloclient: Mock) -> None: """Test a user initiated config flow with provided host which times out.""" - toloclient().get_status_info.side_effect = ResponseTimedOutError() + toloclient().get_status.side_effect = ToloCommunicationError result = await hass.config_entries.flow.async_init( DOMAIN, @@ -61,7 +62,7 @@ async def test_user_walkthrough( assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" - toloclient().get_status_info.side_effect = lambda *args, **kwargs: None + toloclient().get_status.side_effect = lambda *args, **kwargs: None result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -72,7 +73,7 @@ async def test_user_walkthrough( assert result2["step_id"] == "user" assert result2["errors"] == {"base": "cannot_connect"} - toloclient().get_status_info.side_effect = lambda *args, **kwargs: object() + toloclient().get_status.side_effect = lambda *args, **kwargs: object() result3 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -88,7 +89,7 @@ async def test_dhcp( hass: HomeAssistant, toloclient: Mock, coordinator_toloclient: Mock ) -> None: """Test starting a flow from discovery.""" - toloclient().get_status_info.side_effect = lambda *args, **kwargs: object() + toloclient().get_status.side_effect = lambda *args, **kwargs: object() result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_DHCP}, data=MOCK_DHCP_DATA @@ -109,7 +110,7 @@ async def test_dhcp( async def test_dhcp_invalid_device(hass: HomeAssistant, toloclient: Mock) -> None: """Test starting a flow from discovery.""" - toloclient().get_status_info.side_effect = lambda *args, **kwargs: None + toloclient().get_status.side_effect = lambda *args, **kwargs: None result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_DHCP}, data=MOCK_DHCP_DATA diff --git a/tests/components/tomato/test_device_tracker.py b/tests/components/tomato/test_device_tracker.py index 11e73b5695c..099a2c2b40a 100644 --- a/tests/components/tomato/test_device_tracker.py +++ b/tests/components/tomato/test_device_tracker.py @@ -1,4 +1,5 @@ """The tests for the Tomato device tracker platform.""" + from unittest import mock import pytest diff --git a/tests/components/tomorrowio/conftest.py b/tests/components/tomorrowio/conftest.py index 2d36d68c57a..3e7095210fe 100644 --- a/tests/components/tomorrowio/conftest.py +++ b/tests/components/tomorrowio/conftest.py @@ -1,4 +1,5 @@ """Configure py.test.""" + import json from unittest.mock import PropertyMock, patch @@ -20,16 +21,20 @@ def tomorrowio_config_flow_connect(): @pytest.fixture(name="tomorrowio_config_entry_update", autouse=True) def tomorrowio_config_entry_update_fixture(): """Mock valid tomorrowio config entry setup.""" - with patch( - "homeassistant.components.tomorrowio.TomorrowioV4.realtime_and_all_forecasts", - return_value=json.loads(load_fixture("v4.json", "tomorrowio")), - ) as mock_update, patch( - "homeassistant.components.tomorrowio.TomorrowioV4.max_requests_per_day", - new_callable=PropertyMock, - ) as mock_max_requests_per_day, patch( - "homeassistant.components.tomorrowio.TomorrowioV4.num_api_requests", - new_callable=PropertyMock, - ) as mock_num_api_requests: + with ( + patch( + "homeassistant.components.tomorrowio.TomorrowioV4.realtime_and_all_forecasts", + return_value=json.loads(load_fixture("v4.json", "tomorrowio")), + ) as mock_update, + patch( + "homeassistant.components.tomorrowio.TomorrowioV4.max_requests_per_day", + new_callable=PropertyMock, + ) as mock_max_requests_per_day, + patch( + "homeassistant.components.tomorrowio.TomorrowioV4.num_api_requests", + new_callable=PropertyMock, + ) as mock_num_api_requests, + ): mock_max_requests_per_day.return_value = 100 mock_num_api_requests.return_value = 2 yield mock_update diff --git a/tests/components/tomorrowio/const.py b/tests/components/tomorrowio/const.py index cc9fe803f4e..f2851914379 100644 --- a/tests/components/tomorrowio/const.py +++ b/tests/components/tomorrowio/const.py @@ -1,4 +1,5 @@ """Constants for tomorrowio tests.""" + from homeassistant.const import ( CONF_API_KEY, CONF_LATITUDE, diff --git a/tests/components/tomorrowio/test_config_flow.py b/tests/components/tomorrowio/test_config_flow.py index 301b9bef554..5d4d2e3b43b 100644 --- a/tests/components/tomorrowio/test_config_flow.py +++ b/tests/components/tomorrowio/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Tomorrow.io config flow.""" + from unittest.mock import patch from pytomorrowio.exceptions import ( diff --git a/tests/components/tomorrowio/test_init.py b/tests/components/tomorrowio/test_init.py index fe17bbe79b7..576c5ad1e46 100644 --- a/tests/components/tomorrowio/test_init.py +++ b/tests/components/tomorrowio/test_init.py @@ -1,4 +1,5 @@ """Tests for Tomorrow.io init.""" + from datetime import timedelta from freezegun.api import FrozenDateTimeFactory diff --git a/tests/components/tomorrowio/test_sensor.py b/tests/components/tomorrowio/test_sensor.py index 53e455ffb8d..b0e2fba3123 100644 --- a/tests/components/tomorrowio/test_sensor.py +++ b/tests/components/tomorrowio/test_sensor.py @@ -1,4 +1,5 @@ """Tests for Tomorrow.io sensor entities.""" + from __future__ import annotations from datetime import datetime @@ -104,9 +105,7 @@ def _enable_entity(hass: HomeAssistant, entity_name: str) -> None: """Enable disabled entity.""" ent_reg = async_get(hass) entry = ent_reg.async_get(entity_name) - updated_entry = ent_reg.async_update_entity( - entry.entity_id, **{"disabled_by": None} - ) + updated_entry = ent_reg.async_update_entity(entry.entity_id, disabled_by=None) assert updated_entry != entry assert updated_entry.disabled is False diff --git a/tests/components/tomorrowio/test_weather.py b/tests/components/tomorrowio/test_weather.py index e715fccea6b..6f5117df9d5 100644 --- a/tests/components/tomorrowio/test_weather.py +++ b/tests/components/tomorrowio/test_weather.py @@ -1,4 +1,5 @@ """Tests for Tomorrow.io weather entity.""" + from __future__ import annotations from datetime import datetime, timedelta @@ -22,17 +23,6 @@ from homeassistant.components.tomorrowio.const import ( ) from homeassistant.components.weather import ( ATTR_CONDITION_SUNNY, - ATTR_FORECAST, - ATTR_FORECAST_CONDITION, - ATTR_FORECAST_DEW_POINT, - ATTR_FORECAST_HUMIDITY, - ATTR_FORECAST_PRECIPITATION, - ATTR_FORECAST_PRECIPITATION_PROBABILITY, - ATTR_FORECAST_TEMP, - ATTR_FORECAST_TEMP_LOW, - ATTR_FORECAST_TIME, - ATTR_FORECAST_WIND_BEARING, - ATTR_FORECAST_WIND_SPEED, ATTR_WEATHER_HUMIDITY, ATTR_WEATHER_OZONE, ATTR_WEATHER_PRECIPITATION_UNIT, @@ -67,9 +57,7 @@ def _enable_entity(hass: HomeAssistant, entity_name: str) -> None: """Enable disabled entity.""" ent_reg = async_get(hass) entry = ent_reg.async_get(entity_name) - updated_entry = ent_reg.async_update_entity( - entry.entity_id, **{"disabled_by": None} - ) + updated_entry = ent_reg.async_update_entity(entry.entity_id, disabled_by=None) assert updated_entry != entry assert updated_entry.disabled is False @@ -217,19 +205,6 @@ async def test_v4_weather(hass: HomeAssistant, tomorrowio_config_entry_update) - assert weather_state.state == ATTR_CONDITION_SUNNY assert weather_state.attributes[ATTR_ATTRIBUTION] == ATTRIBUTION - assert len(weather_state.attributes[ATTR_FORECAST]) == 14 - assert weather_state.attributes[ATTR_FORECAST][0] == { - ATTR_FORECAST_CONDITION: ATTR_CONDITION_SUNNY, - ATTR_FORECAST_TIME: "2021-03-07T11:00:00+00:00", - ATTR_FORECAST_PRECIPITATION: 0, - ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0, - ATTR_FORECAST_TEMP: 45.9, - ATTR_FORECAST_TEMP_LOW: 26.1, - ATTR_FORECAST_DEW_POINT: 12.8, - ATTR_FORECAST_HUMIDITY: 58, - ATTR_FORECAST_WIND_BEARING: 239.6, - ATTR_FORECAST_WIND_SPEED: 34.16, # 9.49 m/s -> km/h - } assert weather_state.attributes[ATTR_FRIENDLY_NAME] == "Tomorrow.io Daily" assert weather_state.attributes[ATTR_WEATHER_HUMIDITY] == 23 assert weather_state.attributes[ATTR_WEATHER_OZONE] == 46.53 @@ -250,19 +225,6 @@ async def test_v4_weather_legacy_entities(hass: HomeAssistant) -> None: weather_state = await _setup_legacy(hass, API_V4_ENTRY_DATA) assert weather_state.state == ATTR_CONDITION_SUNNY assert weather_state.attributes[ATTR_ATTRIBUTION] == ATTRIBUTION - assert len(weather_state.attributes[ATTR_FORECAST]) == 14 - assert weather_state.attributes[ATTR_FORECAST][0] == { - ATTR_FORECAST_CONDITION: ATTR_CONDITION_SUNNY, - ATTR_FORECAST_TIME: "2021-03-07T11:00:00+00:00", - ATTR_FORECAST_DEW_POINT: 12.8, - ATTR_FORECAST_HUMIDITY: 58, - ATTR_FORECAST_PRECIPITATION: 0, - ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0, - ATTR_FORECAST_TEMP: 45.9, - ATTR_FORECAST_TEMP_LOW: 26.1, - ATTR_FORECAST_WIND_BEARING: 239.6, - ATTR_FORECAST_WIND_SPEED: 34.16, # 9.49 m/s -> km/h - } assert weather_state.attributes[ATTR_FRIENDLY_NAME] == "Tomorrow.io Daily" assert weather_state.attributes[ATTR_WEATHER_HUMIDITY] == 23 assert weather_state.attributes[ATTR_WEATHER_OZONE] == 46.53 diff --git a/tests/components/toon/test_config_flow.py b/tests/components/toon/test_config_flow.py index 2105802e2e6..3e8f7fa2624 100644 --- a/tests/components/toon/test_config_flow.py +++ b/tests/components/toon/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for the Toon config flow.""" + from http import HTTPStatus from unittest.mock import patch diff --git a/tests/components/totalconnect/common.py b/tests/components/totalconnect/common.py index ccee4c43781..0dde43a9710 100644 --- a/tests/components/totalconnect/common.py +++ b/tests/components/totalconnect/common.py @@ -1,4 +1,5 @@ """Common methods used across tests for TotalConnect.""" + from unittest.mock import patch from total_connect_client import ArmingState, ResultCode, ZoneStatus, ZoneType @@ -388,10 +389,13 @@ async def setup_platform(hass, platform): RESPONSE_DISARMED, ] - with patch("homeassistant.components.totalconnect.PLATFORMS", [platform]), patch( - TOTALCONNECT_REQUEST, - side_effect=responses, - ) as mock_request: + with ( + patch("homeassistant.components.totalconnect.PLATFORMS", [platform]), + patch( + TOTALCONNECT_REQUEST, + side_effect=responses, + ) as mock_request, + ): assert await async_setup_component(hass, DOMAIN, {}) assert mock_request.call_count == 5 await hass.async_block_till_done() diff --git a/tests/components/totalconnect/test_alarm_control_panel.py b/tests/components/totalconnect/test_alarm_control_panel.py index be1a05947cc..fa2e997756d 100644 --- a/tests/components/totalconnect/test_alarm_control_panel.py +++ b/tests/components/totalconnect/test_alarm_control_panel.py @@ -1,4 +1,5 @@ """Tests for the TotalConnect alarm control panel device.""" + from datetime import timedelta from unittest.mock import patch @@ -547,30 +548,30 @@ async def test_other_update_failures(hass: HomeAssistant) -> None: # then an error: ServiceUnavailable --> UpdateFailed async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get(ENTITY_ID).state == STATE_UNAVAILABLE assert mock_request.call_count == 2 # works again async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL * 2) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get(ENTITY_ID).state == STATE_ALARM_DISARMED assert mock_request.call_count == 3 # then an error: TotalConnectError --> UpdateFailed async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL * 3) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get(ENTITY_ID).state == STATE_UNAVAILABLE assert mock_request.call_count == 4 # works again async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL * 4) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get(ENTITY_ID).state == STATE_ALARM_DISARMED assert mock_request.call_count == 5 # unknown TotalConnect status via ValueError async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL * 5) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get(ENTITY_ID).state == STATE_UNAVAILABLE assert mock_request.call_count == 6 diff --git a/tests/components/totalconnect/test_binary_sensor.py b/tests/components/totalconnect/test_binary_sensor.py index 8f9cabe670c..8ff548850d9 100644 --- a/tests/components/totalconnect/test_binary_sensor.py +++ b/tests/components/totalconnect/test_binary_sensor.py @@ -1,4 +1,5 @@ """Tests for the TotalConnect binary sensor.""" + from unittest.mock import patch from homeassistant.components.binary_sensor import ( diff --git a/tests/components/totalconnect/test_config_flow.py b/tests/components/totalconnect/test_config_flow.py index 54259538456..940542bf3ad 100644 --- a/tests/components/totalconnect/test_config_flow.py +++ b/tests/components/totalconnect/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for the TotalConnect config flow.""" + from unittest.mock import patch from total_connect_client.exceptions import AuthenticationError @@ -54,11 +55,14 @@ async def test_user_show_locations(hass: HomeAssistant) -> None: RESPONSE_SUCCESS, ] - with patch( - TOTALCONNECT_REQUEST, - side_effect=responses, - ) as mock_request, patch( - "homeassistant.components.totalconnect.async_setup_entry", return_value=True + with ( + patch( + TOTALCONNECT_REQUEST, + side_effect=responses, + ) as mock_request, + patch( + "homeassistant.components.totalconnect.async_setup_entry", return_value=True + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -143,10 +147,13 @@ async def test_reauth(hass: HomeAssistant) -> None: assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" - with patch( - "homeassistant.components.totalconnect.config_flow.TotalConnectClient" - ) as client_mock, patch( - "homeassistant.components.totalconnect.async_setup_entry", return_value=True + with ( + patch( + "homeassistant.components.totalconnect.config_flow.TotalConnectClient" + ) as client_mock, + patch( + "homeassistant.components.totalconnect.async_setup_entry", return_value=True + ), ): # first test with an invalid password client_mock.side_effect = AuthenticationError() @@ -180,14 +187,18 @@ async def test_no_locations(hass: HomeAssistant) -> None: RESPONSE_DISARMED, ] - with patch( - TOTALCONNECT_REQUEST, - side_effect=responses, - ) as mock_request, patch( - "homeassistant.components.totalconnect.async_setup_entry", return_value=True - ), patch( - "homeassistant.components.totalconnect.TotalConnectClient.get_number_locations", - return_value=0, + with ( + patch( + TOTALCONNECT_REQUEST, + side_effect=responses, + ) as mock_request, + patch( + "homeassistant.components.totalconnect.async_setup_entry", return_value=True + ), + patch( + "homeassistant.components.totalconnect.TotalConnectClient.get_number_locations", + return_value=0, + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, diff --git a/tests/components/totalconnect/test_diagnostics.py b/tests/components/totalconnect/test_diagnostics.py index a632cb81a60..2ad05c60936 100644 --- a/tests/components/totalconnect/test_diagnostics.py +++ b/tests/components/totalconnect/test_diagnostics.py @@ -1,4 +1,5 @@ """Test TotalConnect diagnostics.""" + from homeassistant.components.diagnostics import REDACTED from homeassistant.core import HomeAssistant diff --git a/tests/components/totalconnect/test_init.py b/tests/components/totalconnect/test_init.py index c14014eaaf6..ba533e19798 100644 --- a/tests/components/totalconnect/test_init.py +++ b/tests/components/totalconnect/test_init.py @@ -1,4 +1,5 @@ """Tests for the TotalConnect init process.""" + from unittest.mock import patch from total_connect_client.exceptions import AuthenticationError diff --git a/tests/components/tplink/__init__.py b/tests/components/tplink/__init__.py index 4c188fcddcc..d1454d12e68 100644 --- a/tests/components/tplink/__init__.py +++ b/tests/components/tplink/__init__.py @@ -311,9 +311,11 @@ async def initialize_config_entry_for_device( ) config_entry.add_to_hass(hass) - with _patch_discovery(device=dev), _patch_single_discovery( - device=dev - ), _patch_connect(device=dev): + with ( + _patch_discovery(device=dev), + _patch_single_discovery(device=dev), + _patch_connect(device=dev), + ): await hass.config_entries.async_setup(config_entry.entry_id) 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 2834625292c..4c1cc999f16 100644 --- a/tests/components/tplink/test_config_flow.py +++ b/tests/components/tplink/test_config_flow.py @@ -1,4 +1,5 @@ """Test the tplink config flow.""" + from unittest.mock import AsyncMock, patch from kasa import TimeoutException @@ -78,11 +79,13 @@ async def test_discovery(hass: HomeAssistant) -> None: assert result2["step_id"] == "pick_device" assert not result2["errors"] - with _patch_discovery(), _patch_single_discovery(), _patch_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: + with ( + _patch_discovery(), + _patch_single_discovery(), + _patch_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: MAC_ADDRESS}, @@ -324,8 +327,10 @@ async def test_discovery_with_existing_device_present(hass: HomeAssistant) -> No ) config_entry.add_to_hass(hass) - with _patch_discovery(), _patch_single_discovery(no_device=True), _patch_connect( - no_device=True + with ( + _patch_discovery(), + _patch_single_discovery(no_device=True), + _patch_connect(no_device=True), ): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -362,9 +367,12 @@ async def test_discovery_with_existing_device_present(hass: HomeAssistant) -> No assert result2["step_id"] == "pick_device" assert not result2["errors"] - with _patch_discovery(), _patch_single_discovery(), _patch_connect(), patch( - f"{MODULE}.async_setup_entry", return_value=True - ) as mock_setup_entry: + with ( + _patch_discovery(), + _patch_single_discovery(), + _patch_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: MAC_ADDRESS} ) @@ -416,9 +424,11 @@ async def test_manual(hass: HomeAssistant) -> None: assert not result["errors"] # Cannot connect (timeout) - with _patch_discovery(no_device=True), _patch_single_discovery( - no_device=True - ), _patch_connect(no_device=True): + with ( + _patch_discovery(no_device=True), + _patch_single_discovery(no_device=True), + _patch_connect(no_device=True), + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_HOST: IP_ADDRESS} ) @@ -429,9 +439,13 @@ async def test_manual(hass: HomeAssistant) -> None: assert result2["errors"] == {"base": "cannot_connect"} # Success - with _patch_discovery(), _patch_single_discovery(), _patch_connect(), patch( - f"{MODULE}.async_setup", return_value=True - ), patch(f"{MODULE}.async_setup_entry", return_value=True): + with ( + _patch_discovery(), + _patch_single_discovery(), + _patch_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} ) @@ -445,9 +459,11 @@ async def test_manual(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with _patch_discovery(no_device=True), _patch_single_discovery( - no_device=True - ), _patch_connect(no_device=True): + with ( + _patch_discovery(no_device=True), + _patch_single_discovery(no_device=True), + _patch_connect(no_device=True), + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_HOST: IP_ADDRESS} ) @@ -466,11 +482,13 @@ async def test_manual_no_capabilities(hass: HomeAssistant) -> None: assert result["step_id"] == "user" assert not result["errors"] - with _patch_discovery( - no_device=True - ), _patch_single_discovery(), _patch_connect(), patch( - f"{MODULE}.async_setup", return_value=True - ), patch(f"{MODULE}.async_setup_entry", return_value=True): + with ( + _patch_discovery(no_device=True), + _patch_single_discovery(), + _patch_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} ) @@ -630,9 +648,11 @@ async def test_discovered_by_discovery_and_dhcp(hass: HomeAssistant) -> None: assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "already_in_progress" - with _patch_discovery(no_device=True), _patch_single_discovery( - no_device=True - ), _patch_connect(no_device=True): + with ( + _patch_discovery(no_device=True), + _patch_single_discovery(no_device=True), + _patch_connect(no_device=True), + ): result3 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, @@ -679,11 +699,15 @@ async def test_discovered_by_dhcp_or_discovery( assert result["type"] is FlowResultType.FORM assert result["errors"] is None - with _patch_discovery(), _patch_single_discovery(), _patch_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: + with ( + _patch_discovery(), + _patch_single_discovery(), + _patch_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() @@ -720,9 +744,11 @@ async def test_discovered_by_dhcp_or_discovery_failed_to_get_device( ) -> None: """Test we abort if we cannot get the unique id when discovered from dhcp.""" - with _patch_discovery(no_device=True), _patch_single_discovery( - no_device=True - ), _patch_connect(no_device=True): + with ( + _patch_discovery(no_device=True), + _patch_single_discovery(no_device=True), + _patch_connect(no_device=True), + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": source}, data=data ) @@ -1113,7 +1139,6 @@ async def test_reauth_update_other_flows( mock_config_entry: MockConfigEntry, mock_discovery: AsyncMock, mock_connect: AsyncMock, - # mock_init, ) -> None: """Test reauth updates other reauth flows.""" mock_config_entry2 = MockConfigEntry( @@ -1138,10 +1163,10 @@ async def test_reauth_update_other_flows( flows = hass.config_entries.flow.async_progress() assert len(flows) == 2 - result = flows[0] + flows_by_entry_id = {flow["context"]["entry_id"]: flow for flow in flows} + result = flows_by_entry_id[mock_config_entry.entry_id] assert result["step_id"] == "reauth_confirm" assert mock_config_entry.data[CONF_DEVICE_CONFIG] == DEVICE_CONFIG_DICT_LEGACY - result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={ diff --git a/tests/components/tplink/test_diagnostics.py b/tests/components/tplink/test_diagnostics.py index 3ef42c48b2f..bda5b143a6a 100644 --- a/tests/components/tplink/test_diagnostics.py +++ b/tests/components/tplink/test_diagnostics.py @@ -1,4 +1,5 @@ """Tests for the diagnostics data provided by the TP-Link integration.""" + import json from kasa import SmartDevice diff --git a/tests/components/tplink/test_init.py b/tests/components/tplink/test_init.py index 4af4a80c927..176f2aab7ae 100644 --- a/tests/components/tplink/test_init.py +++ b/tests/components/tplink/test_init.py @@ -1,10 +1,12 @@ """Tests for the TP-Link component.""" + from __future__ import annotations import copy from datetime import timedelta from unittest.mock import AsyncMock, MagicMock, patch +from freezegun.api import FrozenDateTimeFactory from kasa.exceptions import AuthenticationException import pytest @@ -40,30 +42,36 @@ from . import ( from tests.common import MockConfigEntry, async_fire_time_changed -async def test_configuring_tplink_causes_discovery(hass: HomeAssistant) -> None: +async def test_configuring_tplink_causes_discovery( + hass: HomeAssistant, freezer: FrozenDateTimeFactory +) -> None: """Test that specifying empty config does discovery.""" - with patch("homeassistant.components.tplink.Discover.discover") as discover, patch( - "homeassistant.components.tplink.Discover.discover_single" + with ( + patch("homeassistant.components.tplink.Discover.discover") as discover, + patch("homeassistant.components.tplink.Discover.discover_single"), ): discover.return_value = {MagicMock(): MagicMock()} await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) + # call_count will differ based on number of broadcast addresses call_count = len(discover.mock_calls) assert discover.mock_calls - async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=15)) - await hass.async_block_till_done() + freezer.tick(tplink.DISCOVERY_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) assert len(discover.mock_calls) == call_count * 2 - async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=30)) - await hass.async_block_till_done() + freezer.tick(tplink.DISCOVERY_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) assert len(discover.mock_calls) == call_count * 3 async def test_config_entry_reload(hass: HomeAssistant) -> None: """Test that a config entry can be reloaded.""" already_migrated_config_entry = MockConfigEntry( - domain=DOMAIN, data={}, unique_id=MAC_ADDRESS + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS ) already_migrated_config_entry.add_to_hass(hass) with _patch_discovery(), _patch_single_discovery(), _patch_connect(): @@ -81,9 +89,11 @@ async def test_config_entry_retry(hass: HomeAssistant) -> None: domain=DOMAIN, data={CONF_HOST: IP_ADDRESS}, unique_id=MAC_ADDRESS ) already_migrated_config_entry.add_to_hass(hass) - with _patch_discovery(no_device=True), _patch_single_discovery( - no_device=True - ), _patch_connect(no_device=True): + with ( + _patch_discovery(no_device=True), + _patch_single_discovery(no_device=True), + _patch_connect(no_device=True), + ): await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) await hass.async_block_till_done() assert already_migrated_config_entry.state == ConfigEntryState.SETUP_RETRY @@ -113,9 +123,11 @@ async def test_dimmer_switch_unique_id_fix_original_entity_still_exists( original_name="Rollout dimmer", ) - with _patch_discovery(device=dimmer), _patch_single_discovery( - device=dimmer - ), _patch_connect(device=dimmer): + with ( + _patch_discovery(device=dimmer), + _patch_single_discovery(device=dimmer), + _patch_connect(device=dimmer), + ): await setup.async_setup_component(hass, DOMAIN, {}) await hass.async_block_till_done() @@ -259,7 +271,9 @@ async def test_config_entry_errors( async def test_plug_auth_fails(hass: HomeAssistant) -> None: """Test a smart plug auth failure.""" - config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=MAC_ADDRESS) + config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS + ) config_entry.add_to_hass(hass) plug = _mocked_plug() with _patch_discovery(device=plug), _patch_connect(device=plug): diff --git a/tests/components/tplink/test_light.py b/tests/components/tplink/test_light.py index bd8a380daa1..767ff4a122c 100644 --- a/tests/components/tplink/test_light.py +++ b/tests/components/tplink/test_light.py @@ -1,4 +1,5 @@ """Tests for light platform.""" + from __future__ import annotations from datetime import timedelta @@ -23,7 +24,7 @@ from homeassistant.components.light import ( DOMAIN as LIGHT_DOMAIN, ) from homeassistant.components.tplink.const import DOMAIN -from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON +from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component @@ -44,7 +45,7 @@ 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={}, unique_id=MAC_ADDRESS + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS ) already_migrated_config_entry.add_to_hass(hass) bulb = _mocked_bulb() @@ -66,7 +67,7 @@ async def test_color_light( ) -> None: """Test a color light and that all transitions are correctly passed.""" already_migrated_config_entry = MockConfigEntry( - domain=DOMAIN, data={}, unique_id=MAC_ADDRESS + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS ) already_migrated_config_entry.add_to_hass(hass) bulb.color_temp = None @@ -146,7 +147,7 @@ async def test_color_light( async def test_color_light_no_temp(hass: HomeAssistant) -> None: """Test a light.""" already_migrated_config_entry = MockConfigEntry( - domain=DOMAIN, data={}, unique_id=MAC_ADDRESS + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS ) already_migrated_config_entry.add_to_hass(hass) bulb = _mocked_bulb() @@ -206,7 +207,7 @@ async def test_color_temp_light( ) -> None: """Test a light.""" already_migrated_config_entry = MockConfigEntry( - domain=DOMAIN, data={}, unique_id=MAC_ADDRESS + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS ) already_migrated_config_entry.add_to_hass(hass) bulb.is_color = is_color @@ -285,7 +286,7 @@ async def test_color_temp_light( async def test_brightness_only_light(hass: HomeAssistant) -> None: """Test a light.""" already_migrated_config_entry = MockConfigEntry( - domain=DOMAIN, data={}, unique_id=MAC_ADDRESS + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS ) already_migrated_config_entry.add_to_hass(hass) bulb = _mocked_bulb() @@ -329,7 +330,7 @@ async def test_brightness_only_light(hass: HomeAssistant) -> None: async def test_on_off_light(hass: HomeAssistant) -> None: """Test a light.""" already_migrated_config_entry = MockConfigEntry( - domain=DOMAIN, data={}, unique_id=MAC_ADDRESS + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS ) already_migrated_config_entry.add_to_hass(hass) bulb = _mocked_bulb() @@ -363,7 +364,7 @@ async def test_on_off_light(hass: HomeAssistant) -> None: async def test_off_at_start_light(hass: HomeAssistant) -> None: """Test a light.""" already_migrated_config_entry = MockConfigEntry( - domain=DOMAIN, data={}, unique_id=MAC_ADDRESS + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS ) already_migrated_config_entry.add_to_hass(hass) bulb = _mocked_bulb() @@ -387,7 +388,7 @@ async def test_off_at_start_light(hass: HomeAssistant) -> None: async def test_dimmer_turn_on_fix(hass: HomeAssistant) -> None: """Test a light.""" already_migrated_config_entry = MockConfigEntry( - domain=DOMAIN, data={}, unique_id=MAC_ADDRESS + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS ) already_migrated_config_entry.add_to_hass(hass) bulb = _mocked_bulb() @@ -413,14 +414,16 @@ async def test_dimmer_turn_on_fix(hass: HomeAssistant) -> None: async def test_smart_strip_effects(hass: HomeAssistant) -> None: """Test smart strip effects.""" already_migrated_config_entry = MockConfigEntry( - domain=DOMAIN, data={}, unique_id=MAC_ADDRESS + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS ) already_migrated_config_entry.add_to_hass(hass) strip = _mocked_smart_light_strip() - with _patch_discovery(device=strip), _patch_single_discovery( - device=strip - ), _patch_connect(device=strip): + with ( + _patch_discovery(device=strip), + _patch_single_discovery(device=strip), + _patch_connect(device=strip), + ): await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) await hass.async_block_till_done() @@ -495,7 +498,7 @@ async def test_smart_strip_effects(hass: HomeAssistant) -> None: async def test_smart_strip_custom_random_effect(hass: HomeAssistant) -> None: """Test smart strip custom random effects.""" already_migrated_config_entry = MockConfigEntry( - domain=DOMAIN, data={}, unique_id=MAC_ADDRESS + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS ) already_migrated_config_entry.add_to_hass(hass) strip = _mocked_smart_light_strip() @@ -652,7 +655,7 @@ async def test_smart_strip_custom_random_effect(hass: HomeAssistant) -> None: async def test_smart_strip_custom_random_effect_at_start(hass: HomeAssistant) -> None: """Test smart strip custom random effects at startup.""" already_migrated_config_entry = MockConfigEntry( - domain=DOMAIN, data={}, unique_id=MAC_ADDRESS + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS ) already_migrated_config_entry.add_to_hass(hass) strip = _mocked_smart_light_strip() @@ -685,7 +688,7 @@ async def test_smart_strip_custom_random_effect_at_start(hass: HomeAssistant) -> async def test_smart_strip_custom_sequence_effect(hass: HomeAssistant) -> None: """Test smart strip custom sequence effects.""" already_migrated_config_entry = MockConfigEntry( - domain=DOMAIN, data={}, unique_id=MAC_ADDRESS + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS ) already_migrated_config_entry.add_to_hass(hass) strip = _mocked_smart_light_strip() diff --git a/tests/components/tplink/test_sensor.py b/tests/components/tplink/test_sensor.py index b67ed031df3..15bc23837fa 100644 --- a/tests/components/tplink/test_sensor.py +++ b/tests/components/tplink/test_sensor.py @@ -4,6 +4,7 @@ from unittest.mock import Mock from homeassistant.components import tplink from homeassistant.components.tplink.const import DOMAIN +from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component @@ -16,7 +17,7 @@ from tests.common import MockConfigEntry async def test_color_light_with_an_emeter(hass: HomeAssistant) -> None: """Test a light with an emeter.""" already_migrated_config_entry = MockConfigEntry( - domain=DOMAIN, data={}, unique_id=MAC_ADDRESS + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS ) already_migrated_config_entry.add_to_hass(hass) bulb = _mocked_bulb() @@ -56,7 +57,7 @@ async def test_color_light_with_an_emeter(hass: HomeAssistant) -> None: async def test_plug_with_an_emeter(hass: HomeAssistant) -> None: """Test a plug with an emeter.""" already_migrated_config_entry = MockConfigEntry( - domain=DOMAIN, data={}, unique_id=MAC_ADDRESS + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS ) already_migrated_config_entry.add_to_hass(hass) plug = _mocked_plug() @@ -91,7 +92,7 @@ async def test_plug_with_an_emeter(hass: HomeAssistant) -> None: async def test_color_light_no_emeter(hass: HomeAssistant) -> None: """Test a light without an emeter.""" already_migrated_config_entry = MockConfigEntry( - domain=DOMAIN, data={}, unique_id=MAC_ADDRESS + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS ) already_migrated_config_entry.add_to_hass(hass) bulb = _mocked_bulb() @@ -120,7 +121,7 @@ async def test_color_light_no_emeter(hass: HomeAssistant) -> None: async def test_sensor_unique_id(hass: HomeAssistant) -> None: """Test a sensor unique ids.""" already_migrated_config_entry = MockConfigEntry( - domain=DOMAIN, data={}, unique_id=MAC_ADDRESS + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS ) already_migrated_config_entry.add_to_hass(hass) plug = _mocked_plug() diff --git a/tests/components/tplink/test_switch.py b/tests/components/tplink/test_switch.py index 372651ea250..6326e9bb671 100644 --- a/tests/components/tplink/test_switch.py +++ b/tests/components/tplink/test_switch.py @@ -9,7 +9,13 @@ import pytest from homeassistant.components import tplink from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.components.tplink.const import DOMAIN -from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON, STATE_UNAVAILABLE +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_HOST, + STATE_OFF, + STATE_ON, + STATE_UNAVAILABLE, +) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component @@ -30,7 +36,7 @@ from tests.common import MockConfigEntry, async_fire_time_changed async def test_plug(hass: HomeAssistant) -> None: """Test a smart plug.""" already_migrated_config_entry = MockConfigEntry( - domain=DOMAIN, data={}, unique_id=MAC_ADDRESS + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS ) already_migrated_config_entry.add_to_hass(hass) plug = _mocked_plug() @@ -66,7 +72,7 @@ async def test_plug(hass: HomeAssistant) -> None: async def test_led_switch(hass: HomeAssistant, dev, domain: str) -> None: """Test LED setting for plugs, strips and dimmers.""" already_migrated_config_entry = MockConfigEntry( - domain=DOMAIN, data={}, unique_id=MAC_ADDRESS + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS ) already_migrated_config_entry.add_to_hass(hass) with _patch_discovery(device=dev), _patch_connect(device=dev): @@ -96,7 +102,7 @@ async def test_led_switch(hass: HomeAssistant, dev, domain: str) -> None: async def test_plug_unique_id(hass: HomeAssistant) -> None: """Test a plug unique id.""" already_migrated_config_entry = MockConfigEntry( - domain=DOMAIN, data={}, unique_id=MAC_ADDRESS + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS ) already_migrated_config_entry.add_to_hass(hass) plug = _mocked_plug() @@ -112,7 +118,7 @@ async def test_plug_unique_id(hass: HomeAssistant) -> None: async def test_plug_update_fails(hass: HomeAssistant) -> None: """Test a smart plug update failure.""" already_migrated_config_entry = MockConfigEntry( - domain=DOMAIN, data={}, unique_id=MAC_ADDRESS + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS ) already_migrated_config_entry.add_to_hass(hass) plug = _mocked_plug() @@ -134,7 +140,7 @@ async def test_plug_update_fails(hass: HomeAssistant) -> None: async def test_strip(hass: HomeAssistant) -> None: """Test a smart strip.""" already_migrated_config_entry = MockConfigEntry( - domain=DOMAIN, data={}, unique_id=MAC_ADDRESS + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS ) already_migrated_config_entry.add_to_hass(hass) strip = _mocked_strip() @@ -182,7 +188,7 @@ async def test_strip(hass: HomeAssistant) -> None: async def test_strip_unique_ids(hass: HomeAssistant) -> None: """Test a strip unique id.""" already_migrated_config_entry = MockConfigEntry( - domain=DOMAIN, data={}, unique_id=MAC_ADDRESS + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=MAC_ADDRESS ) already_migrated_config_entry.add_to_hass(hass) strip = _mocked_strip() diff --git a/tests/components/tplink_omada/conftest.py b/tests/components/tplink_omada/conftest.py index 8f977de588c..afedaa2df3c 100644 --- a/tests/components/tplink_omada/conftest.py +++ b/tests/components/tplink_omada/conftest.py @@ -1,10 +1,16 @@ """Test fixtures for TP-Link Omada integration.""" + from collections.abc import Generator import json from unittest.mock import AsyncMock, MagicMock, patch import pytest -from tplink_omada_client.devices import OmadaSwitch, OmadaSwitchPortDetails +from tplink_omada_client.devices import ( + OmadaGateway, + OmadaListDevice, + OmadaSwitch, + OmadaSwitchPortDetails, +) from homeassistant.components.tplink_omada.config_flow import CONF_SITE from homeassistant.components.tplink_omada.const import DOMAIN @@ -44,10 +50,19 @@ def mock_setup_entry() -> Generator[AsyncMock, None, None]: def mock_omada_site_client() -> Generator[AsyncMock, None, None]: """Mock Omada site client.""" site_client = AsyncMock() + + gateway_data = json.loads(load_fixture("gateway-TL-ER7212PC.json", DOMAIN)) + gateway = OmadaGateway(gateway_data) + site_client.get_gateway.return_value = gateway + switch1_data = json.loads(load_fixture("switch-TL-SG3210XHP-M2.json", DOMAIN)) switch1 = OmadaSwitch(switch1_data) site_client.get_switches.return_value = [switch1] + devices_data = json.loads(load_fixture("devices.json", DOMAIN)) + devices = [OmadaListDevice(d) for d in devices_data] + site_client.get_devices.return_value = devices + switch1_ports_data = json.loads( load_fixture("switch-ports-TL-SG3210XHP-M2.json", DOMAIN) ) diff --git a/tests/components/tplink_omada/fixtures/devices.json b/tests/components/tplink_omada/fixtures/devices.json new file mode 100644 index 00000000000..d92fd5f7d66 --- /dev/null +++ b/tests/components/tplink_omada/fixtures/devices.json @@ -0,0 +1,41 @@ +[ + { + "type": "gateway", + "mac": "AA-BB-CC-DD-EE-FF", + "name": "Test Router", + "model": "ER7212PC", + "compoundModel": "ER7212PC v1.0", + "showModel": "ER7212PC v1.0", + "firmwareVersion": "1.1.1 Build 20230901 Rel.55651", + "version": "1.1.1", + "hwVersion": "ER7212PC v1.0", + "uptime": "32day(s) 5h 39m 27s", + "uptimeLong": 2785167, + "cpuUtil": 16, + "memUtil": 47, + "status": 14, + "statusCategory": 1, + "site": "Test", + "needUpgrade": false + }, + { + "type": "switch", + "mac": "54-AF-97-00-00-01", + "name": "Test PoE Switch", + "model": "TL-SG3210XHP-M2", + "modelVersion": "1.0", + "compoundModel": "TL-SG3210XHP-M2 v1.0", + "showModel": "TL-SG3210XHP-M2 v1.0", + "firmwareVersion": "1.0.12 Build 20230203 Rel.36545", + "version": "1.0.12", + "hwVersion": "1.0", + "uptime": "1day(s) 8h 27m 26s", + "uptimeLong": 116846, + "cpuUtil": 10, + "memUtil": 20, + "status": 14, + "statusCategory": 1, + "needUpgrade": false, + "fwDownload": false + } +] diff --git a/tests/components/tplink_omada/fixtures/gateway-TL-ER7212PC.json b/tests/components/tplink_omada/fixtures/gateway-TL-ER7212PC.json new file mode 100644 index 00000000000..fba595e7109 --- /dev/null +++ b/tests/components/tplink_omada/fixtures/gateway-TL-ER7212PC.json @@ -0,0 +1,1040 @@ +{ + "type": "gateway", + "mac": "AA-BB-CC-DD-EE-FF", + "name": "Test Router", + "model": "ER7212PC", + "modelVersion": "1.0", + "compoundModel": "ER7212PC v1.0", + "showModel": "ER7212PC v1.0", + "firmwareVersion": "1.1.1 Build 20230901 Rel.55651", + "version": "1.1.1", + "hwVersion": "ER7212PC v1.0", + "status": 14, + "statusCategory": 1, + "site": "Test", + "omadacId": "XXXXX", + "compatible": 0, + "sn": "XXXX", + "addedInAdvanced": false, + "portNum": 12, + "ledSetting": 2, + "snmpSeting": { + "location": "", + "contact": "" + }, + "iptvSetting": { + "igmpEnable": true, + "igmpVersion": "2" + }, + "supportHwOffload": false, + "supportPoe": true, + "hwOffloadEnable": true, + "poeSettings": [ + { + "portId": 5, + "portName": "LAN5", + "enable": true + }, + { + "portId": 6, + "portName": "LAN6", + "enable": true + }, + { + "portId": 7, + "portName": "LAN7", + "enable": true + }, + { + "portId": 8, + "portName": "LAN8", + "enable": true + }, + { + "portId": 9, + "portName": "LAN9", + "enable": true + }, + { + "portId": 10, + "portName": "LAN10", + "enable": true + }, + { + "portId": 11, + "portName": "LAN11", + "enable": true + }, + { + "portId": 12, + "portName": "LAN12", + "enable": true + } + ], + "lldpEnable": false, + "echoServer": "0.0.0.0", + "ip": "192.168.0.1", + "uptime": "5day(s) 3h 29m 49s", + "uptimeLong": 444589, + "cpuUtil": 9, + "memUtil": 86, + "lastSeen": 1704219948802, + "portStats": [ + { + "port": 1, + "name": "SFP WAN/LAN1", + "type": 1, + "mode": 1, + "poe": false, + "status": 0, + "internetState": 0, + "ip": "192.168.0.1", + "speed": 1, + "duplex": 1, + "rx": 0, + "rxPkt": 0, + "rxPktRate": 0, + "rxRate": 0, + "tx": 0, + "txPkt": 0, + "txPktRate": 0, + "txRate": 0, + "wanIpv6Comptent": 1, + "mirroredPorts": [] + }, + { + "port": 2, + "name": "SFP WAN/LAN2", + "type": 1, + "mode": 1, + "poe": false, + "status": 0, + "internetState": 0, + "ip": "192.168.0.1", + "speed": 1, + "duplex": 1, + "rx": 0, + "rxPkt": 0, + "rxPktRate": 0, + "rxRate": 0, + "tx": 0, + "txPkt": 0, + "txPktRate": 0, + "txRate": 0, + "wanIpv6Comptent": 1, + "mirroredPorts": [] + }, + { + "port": 3, + "name": "WAN3", + "type": 0, + "mode": -1, + "status": 0, + "rx": 0, + "rxPkt": 0, + "rxPktRate": 0, + "rxRate": 0, + "tx": 0, + "txPkt": 0, + "txPktRate": 0, + "txRate": 0, + "mirroredPorts": [] + }, + { + "port": 4, + "name": "WAN/LAN4", + "type": 1, + "mode": 0, + "poe": false, + "status": 1, + "internetState": 1, + "ip": "XX.XX.XX.XX", + "speed": 3, + "duplex": 2, + "rx": 39901007520, + "rxPkt": 33051930, + "rxPktRate": 25, + "rxRate": 18, + "tx": 8891933646, + "txPkt": 12195464, + "txPktRate": 22, + "txRate": 3, + "proto": "dhcp", + "wanIpv6Comptent": 1, + "wanPortIpv6Config": { + "enable": 0, + "addr": "", + "gateway": "", + "priDns": "", + "sndDns": "", + "internetState": 0 + }, + "wanPortIpv4Config": { + "ip": "140.100.128.10", + "gateway": "140.100.128.1", + "gateway2": "0.0.0.0", + "priDns": "8.8.8.8", + "sndDns": "8.8.4.4", + "priDns2": "0.0.0.0", + "sndDns2": "0.0.0.0" + }, + "mirroredPorts": [] + }, + { + "port": 5, + "name": "LAN5", + "type": 2, + "mode": 1, + "poe": true, + "status": 1, + "internetState": 0, + "ip": "192.168.0.1", + "speed": 3, + "duplex": 2, + "rx": 4622709923, + "rxPkt": 8985877, + "rxPktRate": 21, + "rxRate": 4, + "tx": 38465362622, + "txPkt": 30836050, + "txPktRate": 25, + "txRate": 17, + "wanIpv6Comptent": 1, + "mirroredPorts": [] + }, + { + "port": 6, + "name": "LAN6", + "type": 2, + "mode": 1, + "poe": false, + "status": 0, + "internetState": 0, + "ip": "192.168.0.1", + "speed": 1, + "duplex": 1, + "rx": 0, + "rxPkt": 0, + "rxPktRate": 0, + "rxRate": 0, + "tx": 0, + "txPkt": 0, + "txPktRate": 0, + "txRate": 0, + "wanIpv6Comptent": 1, + "mirroredPorts": [] + }, + { + "port": 7, + "name": "LAN7", + "type": 2, + "mode": 1, + "poe": false, + "status": 1, + "internetState": 0, + "ip": "192.168.0.1", + "speed": 2, + "duplex": 2, + "rx": 5477288, + "rxPkt": 52166, + "rxPktRate": 0, + "rxRate": 0, + "tx": 66036305, + "txPkt": 319810, + "txPktRate": 1, + "txRate": 0, + "wanIpv6Comptent": 1, + "mirroredPorts": [] + }, + { + "port": 8, + "name": "LAN8", + "type": 2, + "mode": 1, + "poe": false, + "status": 1, + "internetState": 0, + "ip": "192.168.0.1", + "speed": 3, + "duplex": 2, + "rx": 6105639661, + "rxPkt": 6200831, + "rxPktRate": 4, + "rxRate": 0, + "tx": 3258101551, + "txPkt": 4719927, + "txPktRate": 4, + "txRate": 1, + "wanIpv6Comptent": 1, + "mirroredPorts": [] + }, + { + "port": 9, + "name": "LAN9", + "type": 2, + "mode": 1, + "poe": false, + "status": 0, + "internetState": 0, + "ip": "192.168.0.1", + "speed": 1, + "duplex": 1, + "rx": 0, + "rxPkt": 0, + "rxPktRate": 0, + "rxRate": 0, + "tx": 0, + "txPkt": 0, + "txPktRate": 0, + "txRate": 0, + "wanIpv6Comptent": 1, + "mirroredPorts": [] + }, + { + "port": 10, + "name": "LAN10", + "type": 2, + "mode": 1, + "poe": false, + "status": 0, + "internetState": 0, + "ip": "192.168.0.1", + "speed": 1, + "duplex": 1, + "rx": 0, + "rxPkt": 0, + "rxPktRate": 0, + "rxRate": 0, + "tx": 0, + "txPkt": 0, + "txPktRate": 0, + "txRate": 0, + "wanIpv6Comptent": 1, + "mirroredPorts": [] + }, + { + "port": 11, + "name": "LAN11", + "type": 2, + "mode": 1, + "poe": false, + "status": 0, + "internetState": 0, + "ip": "192.168.0.1", + "speed": 1, + "duplex": 1, + "rx": 0, + "rxPkt": 0, + "rxPktRate": 0, + "rxRate": 0, + "tx": 0, + "txPkt": 0, + "txPktRate": 0, + "txRate": 0, + "wanIpv6Comptent": 1, + "mirroredPorts": [] + }, + { + "port": 12, + "name": "LAN12", + "type": 2, + "mode": 1, + "poe": false, + "status": 0, + "internetState": 0, + "ip": "192.168.0.1", + "speed": 1, + "duplex": 1, + "rx": 0, + "rxPkt": 0, + "rxPktRate": 0, + "rxRate": 0, + "tx": 0, + "txPkt": 0, + "txPktRate": 0, + "txRate": 0, + "wanIpv6Comptent": 1, + "mirroredPorts": [] + } + ], + "lanClientStats": [ + { + "lanName": "LAN", + "vlan": 1, + "ip": "192.168.0.1", + "rx": 3365832108, + "tx": 19893930205, + "clientNum": 3, + "lanPortIpv6Config": { + "addr": "XXXXX" + } + } + ], + "needUpgrade": false, + "download": 39901007520, + "upload": 8891933646, + "networkComptent": 1, + "portConfigs": [ + { + "port": 1, + "linkSpeed": 0, + "duplex": 0, + "portCap": [ + { + "linkSpeed": 0, + "duplex": 0 + }, + { + "linkSpeed": 3, + "duplex": 2 + } + ], + "mirrorEnable": false, + "pvid": 1, + "availablePvids": [1], + "portStat": { + "port": 1, + "name": "SFP WAN/LAN1", + "type": 1, + "mode": 1, + "poe": false, + "status": 0, + "internetState": 0, + "ip": "192.168.0.1", + "speed": 1, + "duplex": 1, + "rx": 0, + "rxPkt": 0, + "rxPktRate": 0, + "rxRate": 0, + "tx": 0, + "txPkt": 0, + "txPktRate": 0, + "txRate": 0, + "wanIpv6Comptent": 1, + "mirroredPorts": [] + } + }, + { + "port": 2, + "linkSpeed": 0, + "duplex": 0, + "portCap": [ + { + "linkSpeed": 0, + "duplex": 0 + }, + { + "linkSpeed": 3, + "duplex": 2 + } + ], + "mirrorEnable": false, + "pvid": 1, + "availablePvids": [1], + "portStat": { + "port": 2, + "name": "SFP WAN/LAN2", + "type": 1, + "mode": 1, + "poe": false, + "status": 0, + "internetState": 0, + "ip": "192.168.0.1", + "speed": 1, + "duplex": 1, + "rx": 0, + "rxPkt": 0, + "rxPktRate": 0, + "rxRate": 0, + "tx": 0, + "txPkt": 0, + "txPktRate": 0, + "txRate": 0, + "wanIpv6Comptent": 1, + "mirroredPorts": [] + } + }, + { + "port": 3, + "linkSpeed": 0, + "duplex": 0, + "portCap": [ + { + "linkSpeed": 2, + "duplex": 1 + }, + { + "linkSpeed": 2, + "duplex": 2 + }, + { + "linkSpeed": 1, + "duplex": 1 + }, + { + "linkSpeed": 1, + "duplex": 2 + }, + { + "linkSpeed": 0, + "duplex": 0 + }, + { + "linkSpeed": 3, + "duplex": 2 + } + ], + "mirrorEnable": false, + "portStat": { + "port": 3, + "name": "WAN3", + "type": 0, + "mode": -1, + "status": 0, + "rx": 0, + "rxPkt": 0, + "rxPktRate": 0, + "rxRate": 0, + "tx": 0, + "txPkt": 0, + "txPktRate": 0, + "txRate": 0, + "mirroredPorts": [] + } + }, + { + "port": 4, + "linkSpeed": 0, + "duplex": 0, + "portCap": [ + { + "linkSpeed": 2, + "duplex": 1 + }, + { + "linkSpeed": 2, + "duplex": 2 + }, + { + "linkSpeed": 1, + "duplex": 1 + }, + { + "linkSpeed": 1, + "duplex": 2 + }, + { + "linkSpeed": 0, + "duplex": 0 + }, + { + "linkSpeed": 3, + "duplex": 2 + } + ], + "mirrorEnable": false, + "portStat": { + "port": 4, + "name": "WAN/LAN4", + "type": 1, + "mode": 0, + "poe": false, + "status": 1, + "internetState": 1, + "ip": "XX.XX.XX.XX", + "speed": 3, + "duplex": 2, + "rx": 39901007520, + "rxPkt": 33051930, + "rxPktRate": 25, + "rxRate": 18, + "tx": 8891933646, + "txPkt": 12195464, + "txPktRate": 22, + "txRate": 3, + "proto": "dhcp", + "wanIpv6Comptent": 1, + "wanPortIpv6Config": { + "enable": 0, + "addr": "", + "gateway": "", + "priDns": "", + "sndDns": "", + "internetState": 0 + }, + "wanPortIpv4Config": { + "ip": "XX.XX.XX.XX", + "gateway": "XX.XX.XX.XXX", + "gateway2": "0.0.0.0", + "priDns": "212.27.40.240", + "sndDns": "212.27.40.241", + "priDns2": "0.0.0.0", + "sndDns2": "0.0.0.0" + }, + "mirroredPorts": [] + } + }, + { + "port": 5, + "linkSpeed": 0, + "duplex": 0, + "portCap": [ + { + "linkSpeed": 2, + "duplex": 1 + }, + { + "linkSpeed": 2, + "duplex": 2 + }, + { + "linkSpeed": 1, + "duplex": 1 + }, + { + "linkSpeed": 1, + "duplex": 2 + }, + { + "linkSpeed": 0, + "duplex": 0 + }, + { + "linkSpeed": 3, + "duplex": 2 + } + ], + "mirrorEnable": false, + "pvid": 1, + "availablePvids": [1], + "portStat": { + "port": 5, + "name": "LAN5", + "type": 2, + "mode": 1, + "poe": true, + "status": 1, + "internetState": 0, + "ip": "192.168.0.1", + "speed": 3, + "duplex": 2, + "rx": 4622709923, + "rxPkt": 8985877, + "rxPktRate": 21, + "rxRate": 4, + "tx": 38465362622, + "txPkt": 30836050, + "txPktRate": 25, + "txRate": 17, + "wanIpv6Comptent": 1, + "mirroredPorts": [] + } + }, + { + "port": 6, + "linkSpeed": 0, + "duplex": 0, + "portCap": [ + { + "linkSpeed": 2, + "duplex": 1 + }, + { + "linkSpeed": 2, + "duplex": 2 + }, + { + "linkSpeed": 1, + "duplex": 1 + }, + { + "linkSpeed": 1, + "duplex": 2 + }, + { + "linkSpeed": 0, + "duplex": 0 + }, + { + "linkSpeed": 3, + "duplex": 2 + } + ], + "mirrorEnable": false, + "pvid": 1, + "availablePvids": [1], + "portStat": { + "port": 6, + "name": "LAN6", + "type": 2, + "mode": 1, + "poe": false, + "status": 0, + "internetState": 0, + "ip": "192.168.0.1", + "speed": 1, + "duplex": 1, + "rx": 0, + "rxPkt": 0, + "rxPktRate": 0, + "rxRate": 0, + "tx": 0, + "txPkt": 0, + "txPktRate": 0, + "txRate": 0, + "wanIpv6Comptent": 1, + "mirroredPorts": [] + } + }, + { + "port": 7, + "linkSpeed": 0, + "duplex": 0, + "portCap": [ + { + "linkSpeed": 2, + "duplex": 1 + }, + { + "linkSpeed": 2, + "duplex": 2 + }, + { + "linkSpeed": 1, + "duplex": 1 + }, + { + "linkSpeed": 1, + "duplex": 2 + }, + { + "linkSpeed": 0, + "duplex": 0 + }, + { + "linkSpeed": 3, + "duplex": 2 + } + ], + "mirrorEnable": false, + "pvid": 1, + "availablePvids": [1], + "portStat": { + "port": 7, + "name": "LAN7", + "type": 2, + "mode": 1, + "poe": false, + "status": 1, + "internetState": 0, + "ip": "192.168.0.1", + "speed": 2, + "duplex": 2, + "rx": 5477288, + "rxPkt": 52166, + "rxPktRate": 0, + "rxRate": 0, + "tx": 66036305, + "txPkt": 319810, + "txPktRate": 1, + "txRate": 0, + "wanIpv6Comptent": 1, + "mirroredPorts": [] + } + }, + { + "port": 8, + "linkSpeed": 0, + "duplex": 0, + "portCap": [ + { + "linkSpeed": 2, + "duplex": 1 + }, + { + "linkSpeed": 2, + "duplex": 2 + }, + { + "linkSpeed": 1, + "duplex": 1 + }, + { + "linkSpeed": 1, + "duplex": 2 + }, + { + "linkSpeed": 0, + "duplex": 0 + }, + { + "linkSpeed": 3, + "duplex": 2 + } + ], + "mirrorEnable": false, + "pvid": 1, + "availablePvids": [1], + "portStat": { + "port": 8, + "name": "LAN8", + "type": 2, + "mode": 1, + "poe": false, + "status": 1, + "internetState": 0, + "ip": "192.168.0.1", + "speed": 3, + "duplex": 2, + "rx": 6105639661, + "rxPkt": 6200831, + "rxPktRate": 4, + "rxRate": 0, + "tx": 3258101551, + "txPkt": 4719927, + "txPktRate": 4, + "txRate": 1, + "wanIpv6Comptent": 1, + "mirroredPorts": [] + } + }, + { + "port": 9, + "linkSpeed": 0, + "duplex": 0, + "portCap": [ + { + "linkSpeed": 2, + "duplex": 1 + }, + { + "linkSpeed": 2, + "duplex": 2 + }, + { + "linkSpeed": 1, + "duplex": 1 + }, + { + "linkSpeed": 1, + "duplex": 2 + }, + { + "linkSpeed": 0, + "duplex": 0 + }, + { + "linkSpeed": 3, + "duplex": 2 + } + ], + "mirrorEnable": false, + "pvid": 1, + "availablePvids": [1], + "portStat": { + "port": 9, + "name": "LAN9", + "type": 2, + "mode": 1, + "poe": false, + "status": 0, + "internetState": 0, + "ip": "192.168.0.1", + "speed": 1, + "duplex": 1, + "rx": 0, + "rxPkt": 0, + "rxPktRate": 0, + "rxRate": 0, + "tx": 0, + "txPkt": 0, + "txPktRate": 0, + "txRate": 0, + "wanIpv6Comptent": 1, + "mirroredPorts": [] + } + }, + { + "port": 10, + "linkSpeed": 0, + "duplex": 0, + "portCap": [ + { + "linkSpeed": 2, + "duplex": 1 + }, + { + "linkSpeed": 2, + "duplex": 2 + }, + { + "linkSpeed": 1, + "duplex": 1 + }, + { + "linkSpeed": 1, + "duplex": 2 + }, + { + "linkSpeed": 0, + "duplex": 0 + }, + { + "linkSpeed": 3, + "duplex": 2 + } + ], + "mirrorEnable": false, + "pvid": 1, + "availablePvids": [1], + "portStat": { + "port": 10, + "name": "LAN10", + "type": 2, + "mode": 1, + "poe": false, + "status": 0, + "internetState": 0, + "ip": "192.168.0.1", + "speed": 1, + "duplex": 1, + "rx": 0, + "rxPkt": 0, + "rxPktRate": 0, + "rxRate": 0, + "tx": 0, + "txPkt": 0, + "txPktRate": 0, + "txRate": 0, + "wanIpv6Comptent": 1, + "mirroredPorts": [] + } + }, + { + "port": 11, + "linkSpeed": 0, + "duplex": 0, + "portCap": [ + { + "linkSpeed": 2, + "duplex": 1 + }, + { + "linkSpeed": 2, + "duplex": 2 + }, + { + "linkSpeed": 1, + "duplex": 1 + }, + { + "linkSpeed": 1, + "duplex": 2 + }, + { + "linkSpeed": 0, + "duplex": 0 + }, + { + "linkSpeed": 3, + "duplex": 2 + } + ], + "mirrorEnable": false, + "pvid": 1, + "availablePvids": [1], + "portStat": { + "port": 11, + "name": "LAN11", + "type": 2, + "mode": 1, + "poe": false, + "status": 0, + "internetState": 0, + "ip": "192.168.0.1", + "speed": 1, + "duplex": 1, + "rx": 0, + "rxPkt": 0, + "rxPktRate": 0, + "rxRate": 0, + "tx": 0, + "txPkt": 0, + "txPktRate": 0, + "txRate": 0, + "wanIpv6Comptent": 1, + "mirroredPorts": [] + } + }, + { + "port": 12, + "linkSpeed": 0, + "duplex": 0, + "portCap": [ + { + "linkSpeed": 2, + "duplex": 1 + }, + { + "linkSpeed": 2, + "duplex": 2 + }, + { + "linkSpeed": 1, + "duplex": 1 + }, + { + "linkSpeed": 1, + "duplex": 2 + }, + { + "linkSpeed": 0, + "duplex": 0 + }, + { + "linkSpeed": 3, + "duplex": 2 + } + ], + "mirrorEnable": false, + "pvid": 1, + "availablePvids": [1], + "portStat": { + "port": 12, + "name": "LAN12", + "type": 2, + "mode": 1, + "poe": false, + "status": 0, + "internetState": 0, + "ip": "192.168.0.1", + "speed": 1, + "duplex": 1, + "rx": 0, + "rxPkt": 0, + "rxPktRate": 0, + "rxRate": 0, + "tx": 0, + "txPkt": 0, + "txPktRate": 0, + "txRate": 0, + "wanIpv6Comptent": 1, + "mirroredPorts": [] + } + } + ], + "supportSpeedDuplex": true, + "supportMirror": true, + "supportPvid": true, + "unsupportedPorts": [], + "combinedGateway": true, + "speeds": [1, 2, 3], + "poeRemain": 105.699997, + "poeRemainPercent": 96.090904, + "multiChipGateway": true, + "multiChipInfos": [ + [1, 3, 5, 6, 7, 8], + [2, 4, 9, 10, 11, 12] + ] +} diff --git a/tests/components/tplink_omada/fixtures/switch-TL-SG3210XHP-M2.json b/tests/components/tplink_omada/fixtures/switch-TL-SG3210XHP-M2.json index 2e3f21406b0..163a1758333 100644 --- a/tests/components/tplink_omada/fixtures/switch-TL-SG3210XHP-M2.json +++ b/tests/components/tplink_omada/fixtures/switch-TL-SG3210XHP-M2.json @@ -11,7 +11,7 @@ "hwVersion": "1.0", "status": 14, "statusCategory": 1, - "site": "000000000000000000000000", + "site": "Test", "omadacId": "00000000000000000000000000000000", "compatible": 0, "sn": "Y220000000001", @@ -124,7 +124,7 @@ { "id": "000000000000000000000002", "port": 2, - "name": "Port2", + "name": "Renamed Port", "disable": false, "type": 1, "maxSpeed": 4, diff --git a/tests/components/tplink_omada/fixtures/switch-ports-TL-SG3210XHP-M2.json b/tests/components/tplink_omada/fixtures/switch-ports-TL-SG3210XHP-M2.json index b079b2d2fb7..2c505ca7c13 100644 --- a/tests/components/tplink_omada/fixtures/switch-ports-TL-SG3210XHP-M2.json +++ b/tests/components/tplink_omada/fixtures/switch-ports-TL-SG3210XHP-M2.json @@ -108,7 +108,7 @@ "switchId": "640934810000000000000000", "switchMac": "54-AF-97-00-00-01", "site": "000000000000000000000000", - "name": "Port2", + "name": "Renamed Port", "disable": false, "type": 1, "maxSpeed": 4, diff --git a/tests/components/tplink_omada/snapshots/test_switch.ambr b/tests/components/tplink_omada/snapshots/test_switch.ambr index ee87a061a3c..282d2a4a6a5 100644 --- a/tests/components/tplink_omada/snapshots/test_switch.ambr +++ b/tests/components/tplink_omada/snapshots/test_switch.ambr @@ -1,13 +1,78 @@ # serializer version: 1 +# name: test_gateway_api_fail_disables_switch_entities + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Router Port 4 Internet Connected', + }), + 'context': , + 'entity_id': 'switch.test_router_port_4_internet_connected', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_gateway_connect_ipv4_switch + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Router Port 4 Internet Connected', + }), + 'context': , + 'entity_id': 'switch.test_router_port_4_internet_connected', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_gateway_disappear_disables_switches + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Router Port 4 Internet Connected', + 'icon': 'mdi:ethernet', + }), + 'context': , + 'entity_id': 'switch.test_router_port_4_internet_connected', + 'last_changed': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_gateway_port_change_disables_switch_entities + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Router Port 4 Internet Connected', + }), + 'context': , + 'entity_id': 'switch.test_router_port_4_internet_connected', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_gateway_port_poe_switch + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Router Port 5 PoE', + }), + 'context': , + 'entity_id': 'switch.test_router_port_5_poe', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- # name: test_poe_switches StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Test PoE Switch Port 1 PoE', - 'icon': 'mdi:ethernet', }), 'context': , 'entity_id': 'switch.test_poe_switch_port_1_poe', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }) @@ -35,12 +100,12 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:ethernet', + 'original_icon': None, 'original_name': 'Port 1 PoE', 'platform': 'tplink_omada', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'poe_control', 'unique_id': '54-AF-97-00-00-01_000000000000000000000001_poe', 'unit_of_measurement': None, }) @@ -49,11 +114,11 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Test PoE Switch Port 6 PoE', - 'icon': 'mdi:ethernet', }), 'context': , 'entity_id': 'switch.test_poe_switch_port_6_poe', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }) @@ -81,12 +146,12 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:ethernet', + 'original_icon': None, 'original_name': 'Port 6 PoE', 'platform': 'tplink_omada', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'poe_control', 'unique_id': '54-AF-97-00-00-01_000000000000000000000006_poe', 'unit_of_measurement': None, }) @@ -95,11 +160,11 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Test PoE Switch Port 7 PoE', - 'icon': 'mdi:ethernet', }), 'context': , 'entity_id': 'switch.test_poe_switch_port_7_poe', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }) @@ -127,12 +192,12 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:ethernet', + 'original_icon': None, 'original_name': 'Port 7 PoE', 'platform': 'tplink_omada', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'poe_control', 'unique_id': '54-AF-97-00-00-01_000000000000000000000007_poe', 'unit_of_measurement': None, }) @@ -141,11 +206,11 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Test PoE Switch Port 8 PoE', - 'icon': 'mdi:ethernet', }), 'context': , 'entity_id': 'switch.test_poe_switch_port_8_poe', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }) @@ -173,12 +238,12 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:ethernet', + 'original_icon': None, 'original_name': 'Port 8 PoE', 'platform': 'tplink_omada', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'poe_control', 'unique_id': '54-AF-97-00-00-01_000000000000000000000008_poe', 'unit_of_measurement': None, }) @@ -186,12 +251,12 @@ # name: test_poe_switches.2 StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Test PoE Switch Port 2 PoE', - 'icon': 'mdi:ethernet', + 'friendly_name': 'Test PoE Switch Port 2 (Renamed Port) PoE', }), 'context': , - 'entity_id': 'switch.test_poe_switch_port_2_poe', + 'entity_id': 'switch.test_poe_switch_port_2_renamed_port_poe', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }) @@ -208,7 +273,7 @@ 'disabled_by': None, 'domain': 'switch', 'entity_category': , - 'entity_id': 'switch.test_poe_switch_port_2_poe', + 'entity_id': 'switch.test_poe_switch_port_2_renamed_port_poe', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -219,12 +284,12 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:ethernet', - 'original_name': 'Port 2 PoE', + 'original_icon': None, + 'original_name': 'Port 2 (Renamed Port) PoE', 'platform': 'tplink_omada', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'poe_control', 'unique_id': '54-AF-97-00-00-01_000000000000000000000002_poe', 'unit_of_measurement': None, }) @@ -233,11 +298,11 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Test PoE Switch Port 3 PoE', - 'icon': 'mdi:ethernet', }), 'context': , 'entity_id': 'switch.test_poe_switch_port_3_poe', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }) @@ -265,12 +330,12 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:ethernet', + 'original_icon': None, 'original_name': 'Port 3 PoE', 'platform': 'tplink_omada', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'poe_control', 'unique_id': '54-AF-97-00-00-01_000000000000000000000003_poe', 'unit_of_measurement': None, }) @@ -279,11 +344,11 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Test PoE Switch Port 4 PoE', - 'icon': 'mdi:ethernet', }), 'context': , 'entity_id': 'switch.test_poe_switch_port_4_poe', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }) @@ -311,12 +376,12 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:ethernet', + 'original_icon': None, 'original_name': 'Port 4 PoE', 'platform': 'tplink_omada', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'poe_control', 'unique_id': '54-AF-97-00-00-01_000000000000000000000004_poe', 'unit_of_measurement': None, }) @@ -325,11 +390,11 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Test PoE Switch Port 5 PoE', - 'icon': 'mdi:ethernet', }), 'context': , 'entity_id': 'switch.test_poe_switch_port_5_poe', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }) @@ -357,12 +422,12 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:ethernet', + 'original_icon': None, 'original_name': 'Port 5 PoE', 'platform': 'tplink_omada', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'poe_control', 'unique_id': '54-AF-97-00-00-01_000000000000000000000005_poe', 'unit_of_measurement': None, }) diff --git a/tests/components/tplink_omada/test_config_flow.py b/tests/components/tplink_omada/test_config_flow.py index 5c27c9bde6b..230f0d2a68e 100644 --- a/tests/components/tplink_omada/test_config_flow.py +++ b/tests/components/tplink_omada/test_config_flow.py @@ -1,4 +1,5 @@ """Test the TP-Link Omada config flows.""" + from unittest.mock import patch from tplink_omada_client import OmadaSite @@ -45,15 +46,18 @@ async def test_form_single_site(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.FORM assert result["errors"] == {} - with patch( - "homeassistant.components.tplink_omada.config_flow._validate_input", - return_value=HubInfo( - "omada_id", "OC200", [OmadaSite("Display Name", "SiteId")] - ), - ) as mocked_validate, patch( - "homeassistant.components.tplink_omada.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.tplink_omada.config_flow._validate_input", + return_value=HubInfo( + "omada_id", "OC200", [OmadaSite("Display Name", "SiteId")] + ), + ) as mocked_validate, + patch( + "homeassistant.components.tplink_omada.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], MOCK_USER_DATA, @@ -76,17 +80,20 @@ async def test_form_multiple_sites(hass: HomeAssistant) -> None: assert result["step_id"] == "user" assert result["errors"] == {} - with patch( - "homeassistant.components.tplink_omada.config_flow._validate_input", - return_value=HubInfo( - "omada_id", - "OC200", - [OmadaSite("Site 1", "first"), OmadaSite("Site 2", "second")], + with ( + patch( + "homeassistant.components.tplink_omada.config_flow._validate_input", + return_value=HubInfo( + "omada_id", + "OC200", + [OmadaSite("Site 1", "first"), OmadaSite("Site 2", "second")], + ), ), - ), patch( - "homeassistant.components.tplink_omada.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + patch( + "homeassistant.components.tplink_omada.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], MOCK_USER_DATA, @@ -320,12 +327,15 @@ async def test_async_step_reauth_invalid_auth(hass: HomeAssistant) -> None: async def test_validate_input(hass: HomeAssistant) -> None: """Test validate returns HubInfo.""" - with patch( - "tplink_omada_client.omadaclient.OmadaClient", autospec=True - ) as mock_client, patch( - "homeassistant.components.tplink_omada.config_flow.create_omada_client", - return_value=mock_client, - ) as create_mock: + with ( + patch( + "tplink_omada_client.omadaclient.OmadaClient", autospec=True + ) as mock_client, + patch( + "homeassistant.components.tplink_omada.config_flow.create_omada_client", + return_value=mock_client, + ) as create_mock, + ): mock_client.login.return_value = "Id" mock_client.get_controller_name.return_value = "Name" mock_client.get_sites.return_value = [OmadaSite("x", "y")] @@ -343,12 +353,16 @@ async def test_validate_input(hass: HomeAssistant) -> None: async def test_create_omada_client_parses_args(hass: HomeAssistant) -> None: """Test config arguments are passed to Omada client.""" - with patch( - "homeassistant.components.tplink_omada.config_flow.OmadaClient", autospec=True - ) as mock_client, patch( - "homeassistant.components.tplink_omada.config_flow.async_get_clientsession", - return_value="ws", - ) as mock_clientsession: + with ( + patch( + "homeassistant.components.tplink_omada.config_flow.OmadaClient", + autospec=True, + ) as mock_client, + patch( + "homeassistant.components.tplink_omada.config_flow.async_get_clientsession", + return_value="ws", + ) as mock_clientsession, + ): result = await create_omada_client(hass, MOCK_USER_DATA) assert result is not None @@ -361,12 +375,16 @@ async def test_create_omada_client_parses_args(hass: HomeAssistant) -> None: async def test_create_omada_client_adds_missing_scheme(hass: HomeAssistant) -> None: """Test config arguments are passed to Omada client.""" - with patch( - "homeassistant.components.tplink_omada.config_flow.OmadaClient", autospec=True - ) as mock_client, patch( - "homeassistant.components.tplink_omada.config_flow.async_get_clientsession", - return_value="ws", - ) as mock_clientsession: + with ( + patch( + "homeassistant.components.tplink_omada.config_flow.OmadaClient", + autospec=True, + ) as mock_client, + patch( + "homeassistant.components.tplink_omada.config_flow.async_get_clientsession", + return_value="ws", + ) as mock_clientsession, + ): result = await create_omada_client( hass, { @@ -389,14 +407,19 @@ async def test_create_omada_client_with_ip_creates_clientsession( ) -> None: """Test config arguments are passed to Omada client.""" - with patch( - "homeassistant.components.tplink_omada.config_flow.OmadaClient", autospec=True - ) as mock_client, patch( - "homeassistant.components.tplink_omada.config_flow.CookieJar", autospec=True - ) as mock_jar, patch( - "homeassistant.components.tplink_omada.config_flow.async_create_clientsession", - return_value="ws", - ) as mock_create_clientsession: + with ( + patch( + "homeassistant.components.tplink_omada.config_flow.OmadaClient", + autospec=True, + ) as mock_client, + patch( + "homeassistant.components.tplink_omada.config_flow.CookieJar", autospec=True + ) as mock_jar, + patch( + "homeassistant.components.tplink_omada.config_flow.async_create_clientsession", + return_value="ws", + ) as mock_create_clientsession, + ): result = await create_omada_client( hass, { diff --git a/tests/components/tplink_omada/test_switch.py b/tests/components/tplink_omada/test_switch.py index 786893f328d..78b22a4e829 100644 --- a/tests/components/tplink_omada/test_switch.py +++ b/tests/components/tplink_omada/test_switch.py @@ -1,17 +1,32 @@ """Tests for TP-Link Omada switch entities.""" + +from datetime import timedelta +from typing import Any from unittest.mock import MagicMock from syrupy.assertion import SnapshotAssertion from tplink_omada_client import SwitchPortOverrides from tplink_omada_client.definitions import PoEMode -from tplink_omada_client.devices import OmadaSwitch, OmadaSwitchPortDetails +from tplink_omada_client.devices import ( + OmadaGateway, + OmadaGatewayPortConfig, + OmadaGatewayPortStatus, + OmadaSwitch, + OmadaSwitchPortDetails, +) +from tplink_omada_client.exceptions import InvalidDevice from homeassistant.components import switch +from homeassistant.components.tplink_omada.controller import POLL_GATEWAY from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er +from homeassistant.util.dt import utcnow -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_fire_time_changed + +UPDATE_INTERVAL = timedelta(seconds=10) +POLL_INTERVAL = timedelta(seconds=POLL_GATEWAY + 10) async def test_poe_switches( @@ -22,15 +37,199 @@ async def test_poe_switches( ) -> None: """Test PoE switch.""" poe_switch_mac = "54-AF-97-00-00-01" - for i in range(1, 9): - await _test_poe_switch( - hass, - mock_omada_site_client, - f"switch.test_poe_switch_port_{i}_poe", - poe_switch_mac, - i, - snapshot, + await _test_poe_switch( + hass, + mock_omada_site_client, + "switch.test_poe_switch_port_1_poe", + poe_switch_mac, + 1, + snapshot, + ) + + await _test_poe_switch( + hass, + mock_omada_site_client, + "switch.test_poe_switch_port_2_renamed_port_poe", + poe_switch_mac, + 2, + snapshot, + ) + + +async def test_sfp_port_has_no_poe_switch( + hass: HomeAssistant, + init_integration: MockConfigEntry, +) -> None: + """Test PoE switch SFP ports have no PoE controls.""" + entity = hass.states.get("switch.test_poe_switch_port_9_poe") + assert entity is None + entity = hass.states.get("switch.test_poe_switch_port_8_poe") + assert entity is not None + + +async def test_gateway_connect_ipv4_switch( + hass: HomeAssistant, + mock_omada_site_client: MagicMock, + init_integration: MockConfigEntry, + snapshot: SnapshotAssertion, +) -> None: + """Test gateway connected switches.""" + gateway_mac = "AA-BB-CC-DD-EE-FF" + + entity_id = "switch.test_router_port_4_internet_connected" + entity = hass.states.get(entity_id) + assert entity == snapshot + + test_gateway = await mock_omada_site_client.get_gateway(gateway_mac) + port_status = test_gateway.port_status[3] + assert port_status.port_number == 4 + + mock_omada_site_client.set_gateway_wan_port_connect_state.reset_mock() + mock_omada_site_client.set_gateway_wan_port_connect_state.return_value = ( + _get_updated_gateway_port_status( + mock_omada_site_client, test_gateway, 3, "internetState", 0 ) + ) + await call_service(hass, "turn_off", entity_id) + mock_omada_site_client.set_gateway_wan_port_connect_state.assert_called_once_with( + 4, False, test_gateway, ipv6=False + ) + + async_fire_time_changed(hass, utcnow() + UPDATE_INTERVAL) + await hass.async_block_till_done() + + entity = hass.states.get(entity_id) + assert entity.state == "off" + + mock_omada_site_client.set_gateway_wan_port_connect_state.reset_mock() + mock_omada_site_client.set_gateway_wan_port_connect_state.return_value = ( + _get_updated_gateway_port_status( + mock_omada_site_client, test_gateway, 3, "internetState", 1 + ) + ) + await call_service(hass, "turn_on", entity_id) + mock_omada_site_client.set_gateway_wan_port_connect_state.assert_called_once_with( + 4, True, test_gateway, ipv6=False + ) + + async_fire_time_changed(hass, utcnow() + UPDATE_INTERVAL) + await hass.async_block_till_done() + + entity = hass.states.get(entity_id) + assert entity.state == "on" + + +async def test_gateway_port_poe_switch( + hass: HomeAssistant, + mock_omada_site_client: MagicMock, + init_integration: MockConfigEntry, + snapshot: SnapshotAssertion, +) -> None: + """Test gateway connected switches.""" + gateway_mac = "AA-BB-CC-DD-EE-FF" + + entity_id = "switch.test_router_port_5_poe" + entity = hass.states.get(entity_id) + assert entity == snapshot + + test_gateway = await mock_omada_site_client.get_gateway(gateway_mac) + port_config = test_gateway.port_configs[4] + assert port_config.port_number == 5 + + mock_omada_site_client.set_gateway_port_settings.return_value = ( + OmadaGatewayPortConfig(port_config.raw_data, poe_enabled=False) + ) + await call_service(hass, "turn_off", entity_id) + _assert_gateway_poe_set(mock_omada_site_client, test_gateway, False) + + async_fire_time_changed(hass, utcnow() + UPDATE_INTERVAL) + await hass.async_block_till_done() + + entity = hass.states.get(entity_id) + assert entity.state == "off" + + mock_omada_site_client.set_gateway_port_settings.reset_mock() + mock_omada_site_client.set_gateway_port_settings.return_value = port_config + await call_service(hass, "turn_on", entity_id) + _assert_gateway_poe_set(mock_omada_site_client, test_gateway, True) + + async_fire_time_changed(hass, utcnow() + UPDATE_INTERVAL) + await hass.async_block_till_done() + + entity = hass.states.get(entity_id) + assert entity.state == "on" + + +async def test_gaateway_wan_port_has_no_poe_switch( + hass: HomeAssistant, + init_integration: MockConfigEntry, +) -> None: + """Test PoE switch SFP ports have no PoE controls.""" + entity = hass.states.get("switch.test_router_port_1_poe") + assert entity is None + entity = hass.states.get("switch.test_router_port_9_poe") + assert entity is not None + + +def _assert_gateway_poe_set(mock_omada_site_client, test_gateway, poe_enabled: bool): + ( + called_port, + called_settings, + called_gateway, + ) = mock_omada_site_client.set_gateway_port_settings.call_args.args + mock_omada_site_client.set_gateway_port_settings.assert_called_once() + assert called_port == 5 + assert called_settings.enable_poe is poe_enabled + assert called_gateway == test_gateway + + +async def test_gateway_api_fail_disables_switch_entities( + hass: HomeAssistant, + mock_omada_site_client: MagicMock, + init_integration: MockConfigEntry, + snapshot: SnapshotAssertion, +) -> None: + """Test gateway connected switches.""" + entity_id = "switch.test_router_port_4_internet_connected" + entity = hass.states.get(entity_id) + assert entity == snapshot + assert entity.state == "on" + + mock_omada_site_client.get_gateway.reset_mock() + mock_omada_site_client.get_gateway.side_effect = InvalidDevice("Expected error") + + async_fire_time_changed(hass, utcnow() + POLL_INTERVAL) + await hass.async_block_till_done() + + entity = hass.states.get(entity_id) + assert entity.state == "unavailable" + + +async def test_gateway_port_change_disables_switch_entities( + hass: HomeAssistant, + mock_omada_site_client: MagicMock, + init_integration: MockConfigEntry, + snapshot: SnapshotAssertion, +) -> None: + """Test gateway connected switch reconfigure.""" + + gateway_mac = "AA-BB-CC-DD-EE-FF" + test_gateway = await mock_omada_site_client.get_gateway(gateway_mac) + + entity_id = "switch.test_router_port_4_internet_connected" + entity = hass.states.get(entity_id) + assert entity == snapshot + assert entity.state == "on" + + mock_omada_site_client.get_gateway.reset_mock() + # Set Port 4 to LAN mode + _get_updated_gateway_port_status(mock_omada_site_client, test_gateway, 3, "mode", 1) + + async_fire_time_changed(hass, utcnow() + POLL_INTERVAL) + await hass.async_block_till_done() + + entity = hass.states.get(entity_id) + assert entity.state == "unavailable" async def _test_poe_switch( @@ -65,18 +264,22 @@ async def _test_poe_switch( mock_omada_site_client.update_switch_port.return_value = await _update_port_details( mock_omada_site_client, port_num, False ) + await call_service(hass, "turn_off", entity_id) mock_omada_site_client.update_switch_port.assert_called_once() ( device, switch_port_details, ) = mock_omada_site_client.update_switch_port.call_args.args + assert_update_switch_port( device, switch_port_details, False, **mock_omada_site_client.update_switch_port.call_args.kwargs, ) + async_fire_time_changed(hass, utcnow() + UPDATE_INTERVAL) + await hass.async_block_till_done() entity = hass.states.get(entity_id) assert entity.state == "off" @@ -93,6 +296,8 @@ async def _test_poe_switch( True, **mock_omada_site_client.update_switch_port.call_args.kwargs, ) + async_fire_time_changed(hass, utcnow() + UPDATE_INTERVAL) + await hass.async_block_till_done() entity = hass.states.get(entity_id) assert entity.state == "on" @@ -115,6 +320,20 @@ async def _update_port_details( return OmadaSwitchPortDetails(raw_data) +def _get_updated_gateway_port_status( + mock_omada_site_client: MagicMock, + gateway: OmadaGateway, + port: int, + key: str, + value: Any, +) -> OmadaGatewayPortStatus: + gateway_data = gateway.raw_data.copy() + gateway_data["portStats"][port][key] = value + mock_omada_site_client.get_gateway.reset_mock() + mock_omada_site_client.get_gateway.return_value = OmadaGateway(gateway_data) + return OmadaGatewayPortStatus(gateway_data["portStats"][port]) + + def call_service(hass: HomeAssistant, service: str, entity_id: str): """Call any service on entity.""" return hass.services.async_call( diff --git a/tests/components/traccar/test_init.py b/tests/components/traccar/test_init.py index b85701f9c72..e0ce876a97f 100644 --- a/tests/components/traccar/test_init.py +++ b/tests/components/traccar/test_init.py @@ -1,4 +1,5 @@ """The tests the for Traccar device tracker platform.""" + from http import HTTPStatus from unittest.mock import patch diff --git a/tests/components/traccar_server/common.py b/tests/components/traccar_server/common.py index b85f7b672f8..ea5c26f57e2 100644 --- a/tests/components/traccar_server/common.py +++ b/tests/components/traccar_server/common.py @@ -1,4 +1,5 @@ """Common test tools for Traccar Server.""" + from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry diff --git a/tests/components/traccar_server/conftest.py b/tests/components/traccar_server/conftest.py index b313cb6734d..e5a65bfeabd 100644 --- a/tests/components/traccar_server/conftest.py +++ b/tests/components/traccar_server/conftest.py @@ -1,4 +1,5 @@ """Common fixtures for the Traccar Server tests.""" + from collections.abc import Generator from unittest.mock import AsyncMock, patch @@ -31,12 +32,15 @@ from tests.common import ( @pytest.fixture def mock_traccar_api_client() -> Generator[AsyncMock, None, None]: """Mock a Traccar ApiClient client.""" - with patch( - "homeassistant.components.traccar_server.ApiClient", - autospec=True, - ) as mock_client, patch( - "homeassistant.components.traccar_server.config_flow.ApiClient", - new=mock_client, + with ( + patch( + "homeassistant.components.traccar_server.ApiClient", + autospec=True, + ) as mock_client, + patch( + "homeassistant.components.traccar_server.config_flow.ApiClient", + new=mock_client, + ), ): client: ApiClient = mock_client.return_value client.subscription_status = SubscriptionStatus.DISCONNECTED diff --git a/tests/components/traccar_server/snapshots/test_diagnostics.ambr b/tests/components/traccar_server/snapshots/test_diagnostics.ambr index 20d01e427ea..f8fe3cc60f7 100644 --- a/tests/components/traccar_server/snapshots/test_diagnostics.ambr +++ b/tests/components/traccar_server/snapshots/test_diagnostics.ambr @@ -99,6 +99,86 @@ 'subscription_status': 'disconnected', }) # --- +# name: test_device_diagnostics_with_disabled_entity[X-Wing] + dict({ + 'config_entry_options': dict({ + 'custom_attributes': list([ + 'custom_attr_1', + ]), + 'events': list([ + 'device_moving', + ]), + 'max_accuracy': 5.0, + 'skip_accuracy_filter_for': list([ + ]), + }), + 'coordinator_data': dict({ + '0': dict({ + 'attributes': dict({ + 'custom_attr_1': 'custom_attr_1_value', + }), + 'device': dict({ + 'attributes': dict({ + }), + 'category': 'starfighter', + 'contact': None, + 'disabled': False, + 'groupId': 0, + 'id': 0, + 'lastUpdate': '1970-01-01T00:00:00Z', + 'model': '1337', + 'name': 'X-Wing', + 'phone': None, + 'positionId': 0, + 'status': 'unknown', + 'uniqueId': 'abc123', + }), + 'geofence': dict({ + 'area': '**REDACTED**', + 'attributes': dict({ + }), + 'calendarId': 0, + 'description': "A harsh desert world orbiting twin suns in the galaxy's Outer Rim", + 'id': 0, + 'name': 'Tatooine', + }), + 'position': dict({ + 'accuracy': 3.5, + 'address': '**REDACTED**', + 'altitude': 546841384638, + 'attributes': dict({ + 'custom_attr_1': 'custom_attr_1_value', + }), + 'course': 360, + 'deviceId': 0, + 'deviceTime': '1970-01-01T00:00:00Z', + 'fixTime': '1970-01-01T00:00:00Z', + 'geofenceIds': list([ + 0, + ]), + 'id': 0, + 'latitude': '**REDACTED**', + 'longitude': '**REDACTED**', + 'network': dict({ + }), + 'outdated': True, + 'protocol': 'C-3PO', + 'serverTime': '1970-01-01T00:00:00Z', + 'speed': 4568795, + 'valid': True, + }), + }), + }), + 'entities': list([ + dict({ + 'disabled': True, + 'enity_id': 'device_tracker.x_wing', + 'state': None, + }), + ]), + 'subscription_status': 'disconnected', + }) +# --- # name: test_entry_diagnostics[entry] dict({ 'config_entry_options': dict({ diff --git a/tests/components/traccar_server/test_config_flow.py b/tests/components/traccar_server/test_config_flow.py index 00a987a4711..c412830066d 100644 --- a/tests/components/traccar_server/test_config_flow.py +++ b/tests/components/traccar_server/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Traccar Server config flow.""" + from collections.abc import Generator from typing import Any from unittest.mock import AsyncMock @@ -66,10 +67,10 @@ async def test_form( @pytest.mark.parametrize( ("side_effect", "error"), - ( + [ (TraccarException, "cannot_connect"), (Exception, "unknown"), - ), + ], ) async def test_form_cannot_connect( hass: HomeAssistant, @@ -153,7 +154,7 @@ async def test_options( @pytest.mark.parametrize( ("imported", "data", "options"), - ( + [ ( { CONF_HOST: "1.1.1.1", @@ -222,7 +223,7 @@ async def test_options( CONF_MAX_ACCURACY: 0, }, ), - ), + ], ) async def test_import_from_yaml( hass: HomeAssistant, diff --git a/tests/components/traccar_server/test_diagnostics.py b/tests/components/traccar_server/test_diagnostics.py index ebefaab6df8..3d112057315 100644 --- a/tests/components/traccar_server/test_diagnostics.py +++ b/tests/components/traccar_server/test_diagnostics.py @@ -1,11 +1,12 @@ """Test Traccar Server diagnostics.""" + from collections.abc import Generator from unittest.mock import AsyncMock from syrupy import SnapshotAssertion from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry as dr +from homeassistant.helpers import device_registry as dr, entity_registry as er from .common import setup_integration @@ -62,3 +63,42 @@ async def test_device_diagnostics( ) assert result == snapshot(name=device.name) + + +async def test_device_diagnostics_with_disabled_entity( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + mock_traccar_api_client: Generator[AsyncMock, None, None], + mock_config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, +) -> None: + """Test device diagnostics with disabled entity.""" + await setup_integration(hass, mock_config_entry) + + devices = dr.async_entries_for_config_entry( + hass.helpers.device_registry.async_get(hass), + mock_config_entry.entry_id, + ) + + assert len(devices) == 1 + + for device in dr.async_entries_for_config_entry( + hass.helpers.device_registry.async_get(hass), mock_config_entry.entry_id + ): + for entry in er.async_entries_for_device( + entity_registry, + device.id, + include_disabled_entities=True, + ): + entity_registry.async_update_entity( + entry.entity_id, + disabled_by=er.RegistryEntryDisabler.USER, + ) + + result = await get_diagnostics_for_device( + hass, hass_client, mock_config_entry, device=device + ) + + assert result == snapshot(name=device.name) diff --git a/tests/components/trace/test_websocket_api.py b/tests/components/trace/test_websocket_api.py index 511667a7462..5c5d882b721 100644 --- a/tests/components/trace/test_websocket_api.py +++ b/tests/components/trace/test_websocket_api.py @@ -1,4 +1,5 @@ """Test Trace websocket API.""" + import asyncio from collections import defaultdict import json @@ -131,6 +132,7 @@ async def test_get_trace( enable_custom_integrations: None, ) -> None: """Test tracing a script or automation.""" + await async_setup_component(hass, "homeassistant", {}) id = 1 def next_id(): @@ -205,7 +207,7 @@ async def test_get_trace( _assert_raw_config(domain, sun_config, trace) assert trace["blueprint_inputs"] is None assert trace["context"] - assert trace["error"] == "Service test.automation not found." + assert trace["error"] == "Service test.automation not found" assert trace["state"] == "stopped" assert trace["script_execution"] == "error" assert trace["item_id"] == "sun" @@ -807,6 +809,7 @@ async def test_list_traces( script_execution, ) -> None: """Test listing script and automation traces.""" + await async_setup_component(hass, "homeassistant", {}) id = 1 def next_id(): @@ -893,7 +896,7 @@ async def test_list_traces( assert len(_find_traces(response["result"], domain, "sun")) == 1 trace = _find_traces(response["result"], domain, "sun")[0] assert trace["last_step"] == last_step[0].format(prefix=prefix) - assert trace["error"] == "Service test.automation not found." + assert trace["error"] == "Service test.automation not found" assert trace["state"] == "stopped" assert trace["script_execution"] == script_execution[0] assert trace["timestamp"] @@ -1573,6 +1576,7 @@ async def test_trace_blueprint_automation( enable_custom_integrations: None, ) -> None: """Test trace of blueprint automation.""" + await async_setup_component(hass, "homeassistant", {}) id = 1 def next_id(): @@ -1632,7 +1636,7 @@ async def test_trace_blueprint_automation( assert trace["config"]["id"] == "sun" assert trace["blueprint_inputs"] == sun_config assert trace["context"] - assert trace["error"] == "Service test.automation not found." + assert trace["error"] == "Service test.automation not found" assert trace["state"] == "stopped" assert trace["script_execution"] == "error" assert trace["item_id"] == "sun" diff --git a/tests/components/tractive/test_config_flow.py b/tests/components/tractive/test_config_flow.py index 6dd6f119d45..45a37730ff4 100644 --- a/tests/components/tractive/test_config_flow.py +++ b/tests/components/tractive/test_config_flow.py @@ -1,4 +1,5 @@ """Test the tractive config flow.""" + from unittest.mock import patch import aiotractive @@ -24,10 +25,13 @@ async def test_form(hass: HomeAssistant) -> None: assert result["type"] == "form" assert result["errors"] is None - with patch("aiotractive.api.API.user_id", return_value="user_id"), patch( - "homeassistant.components.tractive.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch("aiotractive.api.API.user_id", return_value="user_id"), + patch( + "homeassistant.components.tractive.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], USER_INPUT, @@ -119,10 +123,13 @@ async def test_reauthentication(hass: HomeAssistant) -> None: assert result["errors"] == {} assert result["step_id"] == "reauth_confirm" - with patch("aiotractive.api.API.user_id", return_value="USERID"), patch( - "homeassistant.components.tractive.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch("aiotractive.api.API.user_id", return_value="USERID"), + patch( + "homeassistant.components.tractive.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], USER_INPUT, diff --git a/tests/components/tradfri/__init__.py b/tests/components/tradfri/__init__.py index 01b5edf5c44..37792ae7e32 100644 --- a/tests/components/tradfri/__init__.py +++ b/tests/components/tradfri/__init__.py @@ -1,3 +1,4 @@ """Tests for the tradfri component.""" + GATEWAY_ID = "mock-gateway-id" TRADFRI_PATH = "homeassistant.components.tradfri" diff --git a/tests/components/tradfri/common.py b/tests/components/tradfri/common.py index b527d02bae4..ab3f6fb71c1 100644 --- a/tests/components/tradfri/common.py +++ b/tests/components/tradfri/common.py @@ -1,4 +1,5 @@ """Common tools used for the Tradfri test suite.""" + from copy import deepcopy from dataclasses import dataclass from typing import Any diff --git a/tests/components/tradfri/conftest.py b/tests/components/tradfri/conftest.py index 86e66f37d8f..9ddac769c1f 100644 --- a/tests/components/tradfri/conftest.py +++ b/tests/components/tradfri/conftest.py @@ -1,4 +1,5 @@ """Common tradfri test fixtures.""" + from __future__ import annotations from collections.abc import Callable, Generator diff --git a/tests/components/tradfri/test_config_flow.py b/tests/components/tradfri/test_config_flow.py index 3f5c71645c8..fd3d85461b1 100644 --- a/tests/components/tradfri/test_config_flow.py +++ b/tests/components/tradfri/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Tradfri config flow.""" + from ipaddress import ip_address from unittest.mock import AsyncMock, patch diff --git a/tests/components/tradfri/test_cover.py b/tests/components/tradfri/test_cover.py index 5b669a3cc60..5aa4e75728d 100644 --- a/tests/components/tradfri/test_cover.py +++ b/tests/components/tradfri/test_cover.py @@ -1,4 +1,5 @@ """Tradfri cover (recognised as blinds in the IKEA ecosystem) platform tests.""" + from __future__ import annotations from typing import Any diff --git a/tests/components/tradfri/test_diagnostics.py b/tests/components/tradfri/test_diagnostics.py index 0da1cb8aadf..ab5aaa45b45 100644 --- a/tests/components/tradfri/test_diagnostics.py +++ b/tests/components/tradfri/test_diagnostics.py @@ -1,4 +1,5 @@ """Tests for Tradfri diagnostics.""" + from __future__ import annotations import pytest diff --git a/tests/components/tradfri/test_fan.py b/tests/components/tradfri/test_fan.py index 8bcad92cb96..2abe03d629a 100644 --- a/tests/components/tradfri/test_fan.py +++ b/tests/components/tradfri/test_fan.py @@ -1,4 +1,5 @@ """Tradfri fan (recognised as air purifiers in the IKEA ecosystem) platform tests.""" + from __future__ import annotations from typing import Any diff --git a/tests/components/tradfri/test_init.py b/tests/components/tradfri/test_init.py index 2848fcd0f3d..54ce469f3c5 100644 --- a/tests/components/tradfri/test_init.py +++ b/tests/components/tradfri/test_init.py @@ -1,4 +1,5 @@ """Tests for Tradfri setup.""" + from unittest.mock import MagicMock from homeassistant.components import tradfri diff --git a/tests/components/tradfri/test_light.py b/tests/components/tradfri/test_light.py index 789ebaae840..887b043689f 100644 --- a/tests/components/tradfri/test_light.py +++ b/tests/components/tradfri/test_light.py @@ -1,4 +1,5 @@ """Tradfri lights platform tests.""" + from typing import Any import pytest diff --git a/tests/components/tradfri/test_sensor.py b/tests/components/tradfri/test_sensor.py index ccb4eccd702..9db4bedce12 100644 --- a/tests/components/tradfri/test_sensor.py +++ b/tests/components/tradfri/test_sensor.py @@ -1,4 +1,5 @@ """Tradfri sensor platform tests.""" + from __future__ import annotations import pytest diff --git a/tests/components/tradfri/test_switch.py b/tests/components/tradfri/test_switch.py index 2380824faa8..231e761f88f 100644 --- a/tests/components/tradfri/test_switch.py +++ b/tests/components/tradfri/test_switch.py @@ -1,4 +1,5 @@ """Tradfri switch (recognised as sockets in the IKEA ecosystem) platform tests.""" + from __future__ import annotations import pytest diff --git a/tests/components/tradfri/test_util.py b/tests/components/tradfri/test_util.py index c4f3509dfef..37b532c7798 100644 --- a/tests/components/tradfri/test_util.py +++ b/tests/components/tradfri/test_util.py @@ -1,4 +1,5 @@ """Tradfri utility function tests.""" + import pytest from homeassistant.components.tradfri.fan import _from_fan_percentage, _from_fan_speed diff --git a/tests/components/trafikverket_camera/__init__.py b/tests/components/trafikverket_camera/__init__.py index dd23c7bce7e..9eb6c471c1e 100644 --- a/tests/components/trafikverket_camera/__init__.py +++ b/tests/components/trafikverket_camera/__init__.py @@ -1,4 +1,5 @@ """Tests for the Trafikverket Camera integration.""" + from __future__ import annotations from homeassistant.const import CONF_API_KEY, CONF_ID, CONF_LOCATION diff --git a/tests/components/trafikverket_camera/conftest.py b/tests/components/trafikverket_camera/conftest.py index 92693ccf3c2..61eebb623b2 100644 --- a/tests/components/trafikverket_camera/conftest.py +++ b/tests/components/trafikverket_camera/conftest.py @@ -1,4 +1,5 @@ """Fixtures for Trafikverket Camera integration tests.""" + from __future__ import annotations from datetime import datetime diff --git a/tests/components/trafikverket_camera/test_binary_sensor.py b/tests/components/trafikverket_camera/test_binary_sensor.py index 87d0e6d58b7..ffdb5b44813 100644 --- a/tests/components/trafikverket_camera/test_binary_sensor.py +++ b/tests/components/trafikverket_camera/test_binary_sensor.py @@ -1,4 +1,5 @@ """The test for the Trafikverket binary sensor platform.""" + from __future__ import annotations from pytrafikverket.trafikverket_camera import CameraInfo diff --git a/tests/components/trafikverket_camera/test_camera.py b/tests/components/trafikverket_camera/test_camera.py index 182924e9f0e..1bf742b5f08 100644 --- a/tests/components/trafikverket_camera/test_camera.py +++ b/tests/components/trafikverket_camera/test_camera.py @@ -1,4 +1,5 @@ """The test for the Trafikverket camera platform.""" + from __future__ import annotations from datetime import timedelta diff --git a/tests/components/trafikverket_camera/test_config_flow.py b/tests/components/trafikverket_camera/test_config_flow.py index 005c6006d81..eb14636d6c9 100644 --- a/tests/components/trafikverket_camera/test_config_flow.py +++ b/tests/components/trafikverket_camera/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Trafikverket Camera config flow.""" + from __future__ import annotations from unittest.mock import patch @@ -25,13 +26,16 @@ async def test_form(hass: HomeAssistant, get_camera: CameraInfo) -> None: assert result["type"] == FlowResultType.FORM assert result["errors"] == {} - with patch( - "homeassistant.components.trafikverket_camera.config_flow.TrafikverketCamera.async_get_cameras", - return_value=[get_camera], - ), patch( - "homeassistant.components.trafikverket_camera.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.trafikverket_camera.config_flow.TrafikverketCamera.async_get_cameras", + return_value=[get_camera], + ), + patch( + "homeassistant.components.trafikverket_camera.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -75,13 +79,16 @@ async def test_form_multiple_cameras( ) await hass.async_block_till_done() - with patch( - "homeassistant.components.trafikverket_camera.config_flow.TrafikverketCamera.async_get_cameras", - return_value=[get_camera2], - ), patch( - "homeassistant.components.trafikverket_camera.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.trafikverket_camera.config_flow.TrafikverketCamera.async_get_cameras", + return_value=[get_camera2], + ), + patch( + "homeassistant.components.trafikverket_camera.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -111,13 +118,16 @@ async def test_form_no_location_data( assert result["type"] == FlowResultType.FORM assert result["errors"] == {} - with patch( - "homeassistant.components.trafikverket_camera.config_flow.TrafikverketCamera.async_get_cameras", - return_value=[get_camera_no_location], - ), patch( - "homeassistant.components.trafikverket_camera.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.trafikverket_camera.config_flow.TrafikverketCamera.async_get_cameras", + return_value=[get_camera_no_location], + ), + patch( + "homeassistant.components.trafikverket_camera.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -209,11 +219,14 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.FORM assert result["errors"] == {} - with patch( - "homeassistant.components.trafikverket_camera.config_flow.TrafikverketCamera.async_get_cameras", - ), patch( - "homeassistant.components.trafikverket_camera.async_setup_entry", - return_value=True, + with ( + patch( + "homeassistant.components.trafikverket_camera.config_flow.TrafikverketCamera.async_get_cameras", + ), + patch( + "homeassistant.components.trafikverket_camera.async_setup_entry", + return_value=True, + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -289,11 +302,14 @@ async def test_reauth_flow_error( assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {error_key: p_error} - with patch( - "homeassistant.components.trafikverket_camera.config_flow.TrafikverketCamera.async_get_cameras", - ), patch( - "homeassistant.components.trafikverket_camera.async_setup_entry", - return_value=True, + with ( + patch( + "homeassistant.components.trafikverket_camera.config_flow.TrafikverketCamera.async_get_cameras", + ), + patch( + "homeassistant.components.trafikverket_camera.async_setup_entry", + return_value=True, + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], diff --git a/tests/components/trafikverket_camera/test_coordinator.py b/tests/components/trafikverket_camera/test_coordinator.py index 0f79307e0b6..4f633cb524d 100644 --- a/tests/components/trafikverket_camera/test_coordinator.py +++ b/tests/components/trafikverket_camera/test_coordinator.py @@ -1,4 +1,5 @@ """The test for the Trafikverket Camera coordinator.""" + from __future__ import annotations from unittest.mock import patch diff --git a/tests/components/trafikverket_camera/test_init.py b/tests/components/trafikverket_camera/test_init.py index e10c6c16e33..688af08fec1 100644 --- a/tests/components/trafikverket_camera/test_init.py +++ b/tests/components/trafikverket_camera/test_init.py @@ -1,4 +1,5 @@ """Test for Trafikverket Ferry component Init.""" + from __future__ import annotations from datetime import datetime diff --git a/tests/components/trafikverket_camera/test_recorder.py b/tests/components/trafikverket_camera/test_recorder.py index 777c6ea26b3..83645f141fa 100644 --- a/tests/components/trafikverket_camera/test_recorder.py +++ b/tests/components/trafikverket_camera/test_recorder.py @@ -1,4 +1,5 @@ """The tests for Trafikcerket Camera recorder.""" + from __future__ import annotations import pytest diff --git a/tests/components/trafikverket_camera/test_sensor.py b/tests/components/trafikverket_camera/test_sensor.py index c1c98aed797..9d357bbd0ca 100644 --- a/tests/components/trafikverket_camera/test_sensor.py +++ b/tests/components/trafikverket_camera/test_sensor.py @@ -1,4 +1,5 @@ """The test for the Trafikverket sensor platform.""" + from __future__ import annotations from pytrafikverket.trafikverket_camera import CameraInfo diff --git a/tests/components/trafikverket_ferry/__init__.py b/tests/components/trafikverket_ferry/__init__.py index 97bedb30281..5f01b1b65ec 100644 --- a/tests/components/trafikverket_ferry/__init__.py +++ b/tests/components/trafikverket_ferry/__init__.py @@ -1,4 +1,5 @@ """Tests for the Trafikverket Ferry integration.""" + from __future__ import annotations from homeassistant.components.trafikverket_ferry.const import ( diff --git a/tests/components/trafikverket_ferry/conftest.py b/tests/components/trafikverket_ferry/conftest.py index beeca9bd9f3..3491b8474af 100644 --- a/tests/components/trafikverket_ferry/conftest.py +++ b/tests/components/trafikverket_ferry/conftest.py @@ -1,4 +1,5 @@ """Fixtures for Trafikverket Ferry integration tests.""" + from __future__ import annotations from datetime import datetime, timedelta diff --git a/tests/components/trafikverket_ferry/test_config_flow.py b/tests/components/trafikverket_ferry/test_config_flow.py index dbbd1fb09ac..2a0a0ae6cd6 100644 --- a/tests/components/trafikverket_ferry/test_config_flow.py +++ b/tests/components/trafikverket_ferry/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Trafikverket Ferry config flow.""" + from __future__ import annotations from unittest.mock import patch @@ -29,12 +30,15 @@ async def test_form(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.FORM assert result["errors"] == {} - with patch( - "homeassistant.components.trafikverket_ferry.config_flow.TrafikverketFerry.async_get_next_ferry_stop", - ), patch( - "homeassistant.components.trafikverket_ferry.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.trafikverket_ferry.config_flow.TrafikverketFerry.async_get_next_ferry_stop", + ), + patch( + "homeassistant.components.trafikverket_ferry.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -137,11 +141,14 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.FORM assert result["errors"] == {} - with patch( - "homeassistant.components.trafikverket_ferry.config_flow.TrafikverketFerry.async_get_next_ferry_stop", - ), patch( - "homeassistant.components.trafikverket_ferry.async_setup_entry", - return_value=True, + with ( + patch( + "homeassistant.components.trafikverket_ferry.config_flow.TrafikverketFerry.async_get_next_ferry_stop", + ), + patch( + "homeassistant.components.trafikverket_ferry.async_setup_entry", + return_value=True, + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -220,11 +227,14 @@ async def test_reauth_flow_error( assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": p_error} - with patch( - "homeassistant.components.trafikverket_ferry.config_flow.TrafikverketFerry.async_get_next_ferry_stop", - ), patch( - "homeassistant.components.trafikverket_ferry.async_setup_entry", - return_value=True, + with ( + patch( + "homeassistant.components.trafikverket_ferry.config_flow.TrafikverketFerry.async_get_next_ferry_stop", + ), + patch( + "homeassistant.components.trafikverket_ferry.async_setup_entry", + return_value=True, + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], diff --git a/tests/components/trafikverket_ferry/test_coordinator.py b/tests/components/trafikverket_ferry/test_coordinator.py index c0fbe7537cc..6ac4eaa3a78 100644 --- a/tests/components/trafikverket_ferry/test_coordinator.py +++ b/tests/components/trafikverket_ferry/test_coordinator.py @@ -1,4 +1,5 @@ """The test for the Trafikverket Ferry coordinator.""" + from __future__ import annotations from datetime import date, datetime, timedelta diff --git a/tests/components/trafikverket_ferry/test_init.py b/tests/components/trafikverket_ferry/test_init.py index d5063ab704c..adfc84d94cb 100644 --- a/tests/components/trafikverket_ferry/test_init.py +++ b/tests/components/trafikverket_ferry/test_init.py @@ -1,4 +1,5 @@ """Test for Trafikverket Ferry component Init.""" + from __future__ import annotations from unittest.mock import patch diff --git a/tests/components/trafikverket_ferry/test_sensor.py b/tests/components/trafikverket_ferry/test_sensor.py index 84cb856a82d..fc8fa557714 100644 --- a/tests/components/trafikverket_ferry/test_sensor.py +++ b/tests/components/trafikverket_ferry/test_sensor.py @@ -1,4 +1,5 @@ """The test for the Trafikverket sensor platform.""" + from __future__ import annotations from datetime import timedelta @@ -27,9 +28,7 @@ async def test_sensor( assert state1.state == "Harbor 1" assert state2.state == "Harbor 2" assert state3.state == str(dt_util.now().year + 1) + "-05-01T12:00:00+00:00" - assert state1.attributes["icon"] == "mdi:ferry" assert state1.attributes["other_information"] == [""] - assert state2.attributes["icon"] == "mdi:ferry" monkeypatch.setattr(get_ferries[0], "other_information", ["Nothing exiting"]) diff --git a/tests/components/trafikverket_train/__init__.py b/tests/components/trafikverket_train/__init__.py index 9a02ebbf3b6..632f082c73b 100644 --- a/tests/components/trafikverket_train/__init__.py +++ b/tests/components/trafikverket_train/__init__.py @@ -1,4 +1,5 @@ """Tests for the Trafikverket Train integration.""" + from __future__ import annotations from homeassistant.components.trafikverket_ferry.const import ( diff --git a/tests/components/trafikverket_train/conftest.py b/tests/components/trafikverket_train/conftest.py index 423dee541d2..880701e8bdc 100644 --- a/tests/components/trafikverket_train/conftest.py +++ b/tests/components/trafikverket_train/conftest.py @@ -1,4 +1,5 @@ """Fixtures for Trafikverket Train integration tests.""" + from __future__ import annotations from datetime import datetime, timedelta @@ -42,14 +43,18 @@ async def load_integration_from_entry( ) config_entry2.add_to_hass(hass) - with patch( - "homeassistant.components.trafikverket_train.coordinator.TrafikverketTrain.async_get_next_train_stops", - return_value=get_trains, - ), patch( - "homeassistant.components.trafikverket_train.coordinator.TrafikverketTrain.async_get_train_stop", - return_value=get_train_stop, - ), patch( - "homeassistant.components.trafikverket_train.TrafikverketTrain.async_get_train_station", + with ( + patch( + "homeassistant.components.trafikverket_train.coordinator.TrafikverketTrain.async_get_next_train_stops", + return_value=get_trains, + ), + patch( + "homeassistant.components.trafikverket_train.coordinator.TrafikverketTrain.async_get_train_stop", + return_value=get_train_stop, + ), + patch( + "homeassistant.components.trafikverket_train.TrafikverketTrain.async_get_train_station", + ), ): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/trafikverket_train/snapshots/test_sensor.ambr b/tests/components/trafikverket_train/snapshots/test_sensor.ambr index 6ea0168926e..cae0457bbff 100644 --- a/tests/components/trafikverket_train/snapshots/test_sensor.ambr +++ b/tests/components/trafikverket_train/snapshots/test_sensor.ambr @@ -5,12 +5,12 @@ 'attribution': 'Data provided by Trafikverket', 'device_class': 'timestamp', 'friendly_name': 'Stockholm C to Uppsala C Departure time', - 'icon': 'mdi:clock', 'product_filter': 'Regionaltåg', }), 'context': , 'entity_id': 'sensor.stockholm_c_to_uppsala_c_departure_time', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '2023-05-01T12:00:00+00:00', }) @@ -21,7 +21,6 @@ 'attribution': 'Data provided by Trafikverket', 'device_class': 'enum', 'friendly_name': 'Stockholm C to Uppsala C Departure state', - 'icon': 'mdi:clock', 'options': list([ 'on_time', 'delayed', @@ -32,6 +31,7 @@ 'context': , 'entity_id': 'sensor.stockholm_c_to_uppsala_c_departure_state', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on_time', }) @@ -42,12 +42,12 @@ 'attribution': 'Data provided by Trafikverket', 'device_class': 'timestamp', 'friendly_name': 'Stockholm C to Uppsala C Departure time next', - 'icon': 'mdi:clock', 'product_filter': 'Regionaltåg', }), 'context': , 'entity_id': 'sensor.stockholm_c_to_uppsala_c_departure_time_next', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '2023-05-01T17:15:00+00:00', }) @@ -58,12 +58,12 @@ 'attribution': 'Data provided by Trafikverket', 'device_class': 'timestamp', 'friendly_name': 'Stockholm C to Uppsala C Departure time next after', - 'icon': 'mdi:clock', 'product_filter': 'Regionaltåg', }), 'context': , 'entity_id': 'sensor.stockholm_c_to_uppsala_c_departure_time_next_after', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '2023-05-01T17:30:00+00:00', }) @@ -74,12 +74,12 @@ 'attribution': 'Data provided by Trafikverket', 'device_class': 'timestamp', 'friendly_name': 'Stockholm C to Uppsala C Actual time', - 'icon': 'mdi:clock', 'product_filter': 'Regionaltåg', }), 'context': , 'entity_id': 'sensor.stockholm_c_to_uppsala_c_actual_time', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '2023-05-01T12:00:00+00:00', }) @@ -89,12 +89,12 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Trafikverket', 'friendly_name': 'Stockholm C to Uppsala C Other information', - 'icon': 'mdi:information-variant', 'product_filter': 'Regionaltåg', }), 'context': , 'entity_id': 'sensor.stockholm_c_to_uppsala_c_other_information', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'Some other info', }) @@ -105,12 +105,12 @@ 'attribution': 'Data provided by Trafikverket', 'device_class': 'timestamp', 'friendly_name': 'Stockholm C to Uppsala C Departure time next', - 'icon': 'mdi:clock', 'product_filter': 'Regionaltåg', }), 'context': , 'entity_id': 'sensor.stockholm_c_to_uppsala_c_departure_time_next', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '2023-05-01T12:15:00+00:00', }) @@ -121,12 +121,12 @@ 'attribution': 'Data provided by Trafikverket', 'device_class': 'timestamp', 'friendly_name': 'Stockholm C to Uppsala C Departure time next after', - 'icon': 'mdi:clock', 'product_filter': 'Regionaltåg', }), 'context': , 'entity_id': 'sensor.stockholm_c_to_uppsala_c_departure_time_next_after', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '2023-05-01T12:30:00+00:00', }) @@ -137,12 +137,12 @@ 'attribution': 'Data provided by Trafikverket', 'device_class': 'timestamp', 'friendly_name': 'Stockholm C to Uppsala C Departure time', - 'icon': 'mdi:clock', 'product_filter': 'Regionaltåg', }), 'context': , 'entity_id': 'sensor.stockholm_c_to_uppsala_c_departure_time', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '2023-05-01T17:00:00+00:00', }) @@ -153,7 +153,6 @@ 'attribution': 'Data provided by Trafikverket', 'device_class': 'enum', 'friendly_name': 'Stockholm C to Uppsala C Departure state', - 'icon': 'mdi:clock', 'options': list([ 'on_time', 'delayed', @@ -164,6 +163,7 @@ 'context': , 'entity_id': 'sensor.stockholm_c_to_uppsala_c_departure_state', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on_time', }) @@ -174,12 +174,12 @@ 'attribution': 'Data provided by Trafikverket', 'device_class': 'timestamp', 'friendly_name': 'Stockholm C to Uppsala C Actual time', - 'icon': 'mdi:clock', 'product_filter': 'Regionaltåg', }), 'context': , 'entity_id': 'sensor.stockholm_c_to_uppsala_c_actual_time', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '2023-05-01T17:00:00+00:00', }) @@ -189,12 +189,12 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Trafikverket', 'friendly_name': 'Stockholm C to Uppsala C Other information', - 'icon': 'mdi:information-variant', 'product_filter': 'Regionaltåg', }), 'context': , 'entity_id': 'sensor.stockholm_c_to_uppsala_c_other_information', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }) @@ -205,11 +205,11 @@ 'attribution': 'Data provided by Trafikverket', 'device_class': 'timestamp', 'friendly_name': 'Stockholm C to Uppsala C Departure time', - 'icon': 'mdi:clock', }), 'context': , 'entity_id': 'sensor.stockholm_c_to_uppsala_c_departure_time_2', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '2023-05-01T11:00:00+00:00', }) diff --git a/tests/components/trafikverket_train/test_config_flow.py b/tests/components/trafikverket_train/test_config_flow.py index f56aee163bc..3a5afa7431c 100644 --- a/tests/components/trafikverket_train/test_config_flow.py +++ b/tests/components/trafikverket_train/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Trafikverket Train config flow.""" + from __future__ import annotations from unittest.mock import patch @@ -36,14 +37,18 @@ async def test_form(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.FORM assert result["errors"] == {} - with patch( - "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_get_train_station", - ), patch( - "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_get_train_stop", - ), patch( - "homeassistant.components.trafikverket_train.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_get_train_station", + ), + patch( + "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_get_train_stop", + ), + patch( + "homeassistant.components.trafikverket_train.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -96,13 +101,17 @@ async def test_form_entry_already_exist(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.FORM assert result["errors"] == {} - with patch( - "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_get_train_station", - ), patch( - "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_get_train_stop", - ), patch( - "homeassistant.components.trafikverket_train.async_setup_entry", - return_value=True, + with ( + patch( + "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_get_train_station", + ), + patch( + "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_get_train_stop", + ), + patch( + "homeassistant.components.trafikverket_train.async_setup_entry", + return_value=True, + ), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -152,11 +161,14 @@ async def test_flow_fails( assert result["type"] == FlowResultType.FORM assert result["step_id"] == config_entries.SOURCE_USER - with patch( - "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_get_train_station", - side_effect=side_effect(), - ), patch( - "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_get_train_stop", + with ( + patch( + "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_get_train_station", + side_effect=side_effect(), + ), + patch( + "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_get_train_stop", + ), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -194,13 +206,17 @@ async def test_flow_fails_departures( assert result["type"] == FlowResultType.FORM assert result["step_id"] == config_entries.SOURCE_USER - with patch( - "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_get_train_station", - ), patch( - "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_get_next_train_stops", - side_effect=side_effect(), - ), patch( - "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_get_train_stop", + with ( + patch( + "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_get_train_station", + ), + patch( + "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_get_next_train_stops", + side_effect=side_effect(), + ), + patch( + "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_get_train_stop", + ), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -243,13 +259,17 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.FORM assert result["errors"] == {} - with patch( - "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_get_train_station", - ), patch( - "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_get_train_stop", - ), patch( - "homeassistant.components.trafikverket_train.async_setup_entry", - return_value=True, + with ( + patch( + "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_get_train_station", + ), + patch( + "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_get_train_stop", + ), + patch( + "homeassistant.components.trafikverket_train.async_setup_entry", + return_value=True, + ), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -318,11 +338,14 @@ async def test_reauth_flow_error( data=entry.data, ) - with patch( - "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_get_train_station", - side_effect=side_effect(), - ), patch( - "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_get_train_stop", + with ( + patch( + "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_get_train_station", + side_effect=side_effect(), + ), + patch( + "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_get_train_stop", + ), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -334,13 +357,17 @@ async def test_reauth_flow_error( assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": p_error} - with patch( - "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_get_train_station", - ), patch( - "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_get_train_stop", - ), patch( - "homeassistant.components.trafikverket_train.async_setup_entry", - return_value=True, + with ( + patch( + "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_get_train_station", + ), + patch( + "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_get_train_stop", + ), + patch( + "homeassistant.components.trafikverket_train.async_setup_entry", + return_value=True, + ), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -401,11 +428,14 @@ async def test_reauth_flow_error_departures( data=entry.data, ) - with patch( - "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_get_train_station", - ), patch( - "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_get_train_stop", - side_effect=side_effect(), + with ( + patch( + "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_get_train_station", + ), + patch( + "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_get_train_stop", + side_effect=side_effect(), + ), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -417,13 +447,17 @@ async def test_reauth_flow_error_departures( assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": p_error} - with patch( - "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_get_train_station", - ), patch( - "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_get_train_stop", - ), patch( - "homeassistant.components.trafikverket_train.async_setup_entry", - return_value=True, + with ( + patch( + "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_get_train_station", + ), + patch( + "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_get_train_stop", + ), + patch( + "homeassistant.components.trafikverket_train.async_setup_entry", + return_value=True, + ), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -463,14 +497,18 @@ async def test_options_flow( ) entry.add_to_hass(hass) - with patch( - "homeassistant.components.trafikverket_train.TrafikverketTrain.async_get_train_station", - ), patch( - "homeassistant.components.trafikverket_train.coordinator.TrafikverketTrain.async_get_next_train_stops", - return_value=get_trains, - ), patch( - "homeassistant.components.trafikverket_train.coordinator.TrafikverketTrain.async_get_train_stop", - return_value=get_train_stop, + with ( + patch( + "homeassistant.components.trafikverket_train.TrafikverketTrain.async_get_train_station", + ), + patch( + "homeassistant.components.trafikverket_train.coordinator.TrafikverketTrain.async_get_next_train_stops", + return_value=get_trains, + ), + patch( + "homeassistant.components.trafikverket_train.coordinator.TrafikverketTrain.async_get_train_stop", + return_value=get_train_stop, + ), ): assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/trafikverket_train/test_init.py b/tests/components/trafikverket_train/test_init.py index 74b6f30ce61..f68c32b5b90 100644 --- a/tests/components/trafikverket_train/test_init.py +++ b/tests/components/trafikverket_train/test_init.py @@ -1,4 +1,5 @@ """Test for Trafikverket Train component Init.""" + from __future__ import annotations from unittest.mock import patch @@ -30,12 +31,15 @@ async def test_unload_entry(hass: HomeAssistant, get_trains: list[TrainStop]) -> ) entry.add_to_hass(hass) - with patch( - "homeassistant.components.trafikverket_train.TrafikverketTrain.async_get_train_station", - ), patch( - "homeassistant.components.trafikverket_train.coordinator.TrafikverketTrain.async_get_next_train_stops", - return_value=get_trains, - ) as mock_tv_train: + with ( + patch( + "homeassistant.components.trafikverket_train.TrafikverketTrain.async_get_train_station", + ), + patch( + "homeassistant.components.trafikverket_train.coordinator.TrafikverketTrain.async_get_next_train_stops", + return_value=get_trains, + ) as mock_tv_train, + ): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -128,11 +132,14 @@ async def test_migrate_entity_unique_id( original_name="Stockholm C to Uppsala C", ) - with patch( - "homeassistant.components.trafikverket_train.TrafikverketTrain.async_get_train_station", - ), patch( - "homeassistant.components.trafikverket_train.coordinator.TrafikverketTrain.async_get_next_train_stops", - return_value=get_trains, + with ( + patch( + "homeassistant.components.trafikverket_train.TrafikverketTrain.async_get_train_station", + ), + patch( + "homeassistant.components.trafikverket_train.coordinator.TrafikverketTrain.async_get_next_train_stops", + return_value=get_trains, + ), ): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/trafikverket_train/test_sensor.py b/tests/components/trafikverket_train/test_sensor.py index 819433a6b9c..099bcf5ae1e 100644 --- a/tests/components/trafikverket_train/test_sensor.py +++ b/tests/components/trafikverket_train/test_sensor.py @@ -1,4 +1,5 @@ """The test for the Trafikverket train sensor platform.""" + from __future__ import annotations from datetime import timedelta @@ -37,12 +38,15 @@ async def test_sensor_next( state = hass.states.get(entity) assert state == snapshot - with patch( - "homeassistant.components.trafikverket_train.coordinator.TrafikverketTrain.async_get_next_train_stops", - return_value=get_trains_next, - ), patch( - "homeassistant.components.trafikverket_train.coordinator.TrafikverketTrain.async_get_train_stop", - return_value=get_train_stop, + with ( + patch( + "homeassistant.components.trafikverket_train.coordinator.TrafikverketTrain.async_get_next_train_stops", + return_value=get_trains_next, + ), + patch( + "homeassistant.components.trafikverket_train.coordinator.TrafikverketTrain.async_get_train_stop", + return_value=get_train_stop, + ), ): freezer.tick(timedelta(minutes=6)) async_fire_time_changed(hass) @@ -88,12 +92,15 @@ async def test_sensor_update_auth_failure( state = hass.states.get("sensor.stockholm_c_to_uppsala_c_departure_time_2") assert state.state == "2023-05-01T11:00:00+00:00" - with patch( - "homeassistant.components.trafikverket_train.coordinator.TrafikverketTrain.async_get_next_train_stops", - side_effect=InvalidAuthentication, - ), patch( - "homeassistant.components.trafikverket_train.coordinator.TrafikverketTrain.async_get_train_stop", - side_effect=InvalidAuthentication, + with ( + patch( + "homeassistant.components.trafikverket_train.coordinator.TrafikverketTrain.async_get_next_train_stops", + side_effect=InvalidAuthentication, + ), + patch( + "homeassistant.components.trafikverket_train.coordinator.TrafikverketTrain.async_get_train_stop", + side_effect=InvalidAuthentication, + ), ): freezer.tick(timedelta(minutes=6)) async_fire_time_changed(hass) @@ -118,12 +125,15 @@ async def test_sensor_update_failure( state = hass.states.get("sensor.stockholm_c_to_uppsala_c_departure_time_2") assert state.state == "2023-05-01T11:00:00+00:00" - with patch( - "homeassistant.components.trafikverket_train.coordinator.TrafikverketTrain.async_get_next_train_stops", - side_effect=NoTrainAnnouncementFound, - ), patch( - "homeassistant.components.trafikverket_train.coordinator.TrafikverketTrain.async_get_train_stop", - side_effect=NoTrainAnnouncementFound, + with ( + patch( + "homeassistant.components.trafikverket_train.coordinator.TrafikverketTrain.async_get_next_train_stops", + side_effect=NoTrainAnnouncementFound, + ), + patch( + "homeassistant.components.trafikverket_train.coordinator.TrafikverketTrain.async_get_train_stop", + side_effect=NoTrainAnnouncementFound, + ), ): freezer.tick(timedelta(minutes=6)) async_fire_time_changed(hass) diff --git a/tests/components/trafikverket_train/test_util.py b/tests/components/trafikverket_train/test_util.py index e978917adca..03abaf26d9c 100644 --- a/tests/components/trafikverket_train/test_util.py +++ b/tests/components/trafikverket_train/test_util.py @@ -1,4 +1,5 @@ """The test for the Trafikverket train utils.""" + from __future__ import annotations from datetime import datetime diff --git a/tests/components/trafikverket_weatherstation/test_config_flow.py b/tests/components/trafikverket_weatherstation/test_config_flow.py index e55e04d8411..4a1c50cbaf1 100644 --- a/tests/components/trafikverket_weatherstation/test_config_flow.py +++ b/tests/components/trafikverket_weatherstation/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Trafikverket weatherstation config flow.""" + from __future__ import annotations from unittest.mock import patch @@ -30,12 +31,15 @@ async def test_form(hass: HomeAssistant) -> None: assert result["type"] == "form" assert result["errors"] == {} - with patch( - "homeassistant.components.trafikverket_weatherstation.config_flow.TrafikverketWeather.async_get_weather", - ), patch( - "homeassistant.components.trafikverket_weatherstation.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.trafikverket_weatherstation.config_flow.TrafikverketWeather.async_get_weather", + ), + patch( + "homeassistant.components.trafikverket_weatherstation.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -124,11 +128,14 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.FORM assert result["errors"] == {} - with patch( - "homeassistant.components.trafikverket_weatherstation.config_flow.TrafikverketWeather.async_get_weather", - ), patch( - "homeassistant.components.trafikverket_weatherstation.async_setup_entry", - return_value=True, + with ( + patch( + "homeassistant.components.trafikverket_weatherstation.config_flow.TrafikverketWeather.async_get_weather", + ), + patch( + "homeassistant.components.trafikverket_weatherstation.async_setup_entry", + return_value=True, + ), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], diff --git a/tests/components/transmission/__init__.py b/tests/components/transmission/__init__.py index e371a3691a2..c4abba7b832 100644 --- a/tests/components/transmission/__init__.py +++ b/tests/components/transmission/__init__.py @@ -7,7 +7,17 @@ OLD_MOCK_CONFIG_DATA = { "password": "pass", "port": 9091, } -MOCK_CONFIG_DATA = { + +MOCK_CONFIG_DATA_VERSION_1_1 = { + "host": "0.0.0.0", + "username": "user", + "password": "pass", + "port": 9091, +} + +MOCK_CONFIG_DATA = { + "ssl": False, + "path": "/transmission/rpc", "host": "0.0.0.0", "username": "user", "password": "pass", diff --git a/tests/components/transmission/test_config_flow.py b/tests/components/transmission/test_config_flow.py index 04f44d3b7e7..0e184ffc96b 100644 --- a/tests/components/transmission/test_config_flow.py +++ b/tests/components/transmission/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for Transmission config flow.""" + from unittest.mock import MagicMock, patch import pytest @@ -156,10 +157,7 @@ async def test_error_on_connection_failure( async def test_reauth_success(hass: HomeAssistant) -> None: """Test we can reauth.""" - entry = MockConfigEntry( - domain=transmission.DOMAIN, - data=MOCK_CONFIG_DATA, - ) + entry = MockConfigEntry(domain=transmission.DOMAIN, data=MOCK_CONFIG_DATA) entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( diff --git a/tests/components/transmission/test_init.py b/tests/components/transmission/test_init.py index 63b7ac154ed..7efbaad76fb 100644 --- a/tests/components/transmission/test_init.py +++ b/tests/components/transmission/test_init.py @@ -11,12 +11,17 @@ from transmission_rpc.error import ( from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN -from homeassistant.components.transmission.const import DOMAIN +from homeassistant.components.transmission.const import ( + DEFAULT_PATH, + DEFAULT_SSL, + DOMAIN, +) from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import CONF_PATH, CONF_SSL from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from . import MOCK_CONFIG_DATA, OLD_MOCK_CONFIG_DATA +from . import MOCK_CONFIG_DATA, MOCK_CONFIG_DATA_VERSION_1_1, OLD_MOCK_CONFIG_DATA from tests.common import MockConfigEntry @@ -39,6 +44,27 @@ async def test_successful_config_entry(hass: HomeAssistant) -> None: assert entry.state == ConfigEntryState.LOADED +async def test_config_flow_entry_migrate_1_1_to_1_2(hass: HomeAssistant) -> None: + """Test that config flow entry is migrated correctly from v1.1 to v1.2.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=MOCK_CONFIG_DATA_VERSION_1_1, + version=1, + minor_version=1, + ) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + # Test that config entry is at the current version. + assert entry.version == 1 + assert entry.minor_version == 2 + + assert entry.data[CONF_SSL] == DEFAULT_SSL + assert entry.data[CONF_PATH] == DEFAULT_PATH + + async def test_setup_failed_connection_error( hass: HomeAssistant, mock_api: MagicMock ) -> None: diff --git a/tests/components/transport_nsw/test_sensor.py b/tests/components/transport_nsw/test_sensor.py index 5ec28c72fed..9ecff818592 100644 --- a/tests/components/transport_nsw/test_sensor.py +++ b/tests/components/transport_nsw/test_sensor.py @@ -1,4 +1,5 @@ """The tests for the Transport NSW (AU) sensor platform.""" + from unittest.mock import patch from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass diff --git a/tests/components/trend/conftest.py b/tests/components/trend/conftest.py index 2555492e4fd..5263b86d268 100644 --- a/tests/components/trend/conftest.py +++ b/tests/components/trend/conftest.py @@ -1,4 +1,5 @@ """Fixtures for the trend component tests.""" + from collections.abc import Awaitable, Callable from typing import Any diff --git a/tests/components/trend/test_binary_sensor.py b/tests/components/trend/test_binary_sensor.py index 115bac5ed5d..d8d02755044 100644 --- a/tests/components/trend/test_binary_sensor.py +++ b/tests/components/trend/test_binary_sensor.py @@ -1,4 +1,5 @@ """The test for the Trend sensor platform.""" + from datetime import timedelta import logging from typing import Any diff --git a/tests/components/trend/test_config_flow.py b/tests/components/trend/test_config_flow.py index e81d57ef9e1..baccc396bf1 100644 --- a/tests/components/trend/test_config_flow.py +++ b/tests/components/trend/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Trend config flow.""" + from __future__ import annotations from unittest.mock import patch diff --git a/tests/components/tts/common.py b/tests/components/tts/common.py index 0c3642df6fe..5bdc156eacc 100644 --- a/tests/components/tts/common.py +++ b/tests/components/tts/common.py @@ -1,4 +1,5 @@ """Provide common tests tools for tts.""" + from __future__ import annotations from collections.abc import Generator diff --git a/tests/components/tts/conftest.py b/tests/components/tts/conftest.py index 753c90e158d..a8bdeea5545 100644 --- a/tests/components/tts/conftest.py +++ b/tests/components/tts/conftest.py @@ -2,6 +2,7 @@ From http://doc.pytest.org/en/latest/example/simple.html#making-test-result-information-available-in-fixtures """ + from collections.abc import Generator import pytest diff --git a/tests/components/tts/test_init.py b/tests/components/tts/test_init.py index d56542b2a57..2c58c25a509 100644 --- a/tests/components/tts/test_init.py +++ b/tests/components/tts/test_init.py @@ -1,4 +1,5 @@ """The tests for the TTS component.""" + import asyncio from http import HTTPStatus from typing import Any @@ -42,12 +43,6 @@ from tests.typing import ClientSessionGenerator, WebSocketGenerator ORIG_WRITE_TAGS = tts.SpeechManager.write_tags -@pytest.fixture -async def setup_tts(hass: HomeAssistant, mock_tts: None) -> None: - """Mock TTS.""" - assert await async_setup_component(hass, tts.DOMAIN, {"tts": {"platform": "test"}}) - - class DefaultEntity(tts.TextToSpeechEntity): """Test entity.""" @@ -1307,7 +1302,7 @@ async def test_tags_with_wave() -> None: # below data represents an empty wav file tts_data = bytes.fromhex( "52 49 46 46 24 00 00 00 57 41 56 45 66 6d 74 20 10 00 00 00 01 00 02 00" - + "22 56 00 00 88 58 01 00 04 00 10 00 64 61 74 61 00 00 00 00" + "22 56 00 00 88 58 01 00 04 00 10 00 64 61 74 61 00 00 00 00" ) tagged_data = ORIG_WRITE_TAGS( @@ -1332,12 +1327,12 @@ async def test_tags_with_wave() -> None: ) @pytest.mark.parametrize( ("engine", "language", "options", "cache", "result_query"), - ( + [ (None, None, None, None, ""), (None, "de_DE", None, None, "language=de_DE"), (None, "de_DE", {"voice": "henk"}, None, "language=de_DE&voice=henk"), (None, "de_DE", None, True, "cache=true&language=de_DE"), - ), + ], ) async def test_generate_media_source_id( hass: HomeAssistant, @@ -1372,11 +1367,11 @@ async def test_generate_media_source_id( ) @pytest.mark.parametrize( ("engine", "language", "options"), - ( + [ ("not-loaded-engine", None, None), (None, "unsupported-language", None), (None, None, {"option": "not-supported"}), - ), + ], ) async def test_generate_media_source_id_invalid_options( hass: HomeAssistant, @@ -1404,10 +1399,10 @@ def test_resolve_engine(hass: HomeAssistant, setup: str, engine_id: str) -> None assert tts.async_resolve_engine(hass, engine_id) == engine_id assert tts.async_resolve_engine(hass, "non-existing") is None - with patch.dict( - hass.data[tts.DATA_TTS_MANAGER].providers, {}, clear=True - ), patch.dict(hass.data[tts.DOMAIN]._platforms, {}, clear=True), patch.dict( - hass.data[tts.DOMAIN]._entities, {}, clear=True + with ( + patch.dict(hass.data[tts.DATA_TTS_MANAGER].providers, {}, clear=True), + patch.dict(hass.data[tts.DOMAIN]._platforms, {}, clear=True), + patch.dict(hass.data[tts.DOMAIN]._entities, {}, clear=True), ): assert tts.async_resolve_engine(hass, None) is None diff --git a/tests/components/tts/test_legacy.py b/tests/components/tts/test_legacy.py index 5a8321e2ae4..59194f50d93 100644 --- a/tests/components/tts/test_legacy.py +++ b/tests/components/tts/test_legacy.py @@ -1,4 +1,5 @@ """Test the legacy tts setup.""" + from __future__ import annotations import pytest @@ -147,6 +148,7 @@ async def test_service_without_cache_config( with assert_setup_component(1, DOMAIN): assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() await hass.services.async_call( DOMAIN, diff --git a/tests/components/tts/test_media_source.py b/tests/components/tts/test_media_source.py index 641c02064ec..4c10d8f0b08 100644 --- a/tests/components/tts/test_media_source.py +++ b/tests/components/tts/test_media_source.py @@ -1,4 +1,5 @@ """Tests for TTS media source.""" + from http import HTTPStatus from unittest.mock import MagicMock diff --git a/tests/components/tts/test_notify.py b/tests/components/tts/test_notify.py index 1a776140457..07ba2f2f3f5 100644 --- a/tests/components/tts/test_notify.py +++ b/tests/components/tts/test_notify.py @@ -1,4 +1,5 @@ """The tests for the TTS component.""" + from unittest.mock import patch import pytest @@ -48,6 +49,7 @@ async def test_setup_legacy_platform(hass: HomeAssistant) -> None: } with assert_setup_component(1, notify.DOMAIN): assert await async_setup_component(hass, notify.DOMAIN, config) + await hass.async_block_till_done() assert hass.services.has_service(notify.DOMAIN, "tts_test") @@ -64,6 +66,7 @@ async def test_setup_platform(hass: HomeAssistant) -> None: } with assert_setup_component(1, notify.DOMAIN): assert await async_setup_component(hass, notify.DOMAIN, config) + await hass.async_block_till_done() assert hass.services.has_service(notify.DOMAIN, "tts_test") @@ -79,6 +82,7 @@ async def test_setup_platform_missing_key(hass: HomeAssistant) -> None: } with assert_setup_component(0, notify.DOMAIN): assert await async_setup_component(hass, notify.DOMAIN, config) + await hass.async_block_till_done() assert not hass.services.has_service(notify.DOMAIN, "tts_test") @@ -106,6 +110,8 @@ async def test_setup_legacy_service(hass: HomeAssistant) -> None: with assert_setup_component(1, notify.DOMAIN): assert await async_setup_component(hass, notify.DOMAIN, config) + await hass.async_block_till_done() + await hass.services.async_call( notify.DOMAIN, "tts_test", @@ -141,6 +147,8 @@ async def test_setup_service( with assert_setup_component(1, notify.DOMAIN): assert await async_setup_component(hass, notify.DOMAIN, config) + await hass.async_block_till_done() + await hass.services.async_call( notify.DOMAIN, "tts_test", diff --git a/tests/components/tuya/conftest.py b/tests/components/tuya/conftest.py index 6decb7c5f10..541e2f1c9e3 100644 --- a/tests/components/tuya/conftest.py +++ b/tests/components/tuya/conftest.py @@ -1,4 +1,5 @@ """Fixtures for the Tuya integration tests.""" + from __future__ import annotations from collections.abc import Generator diff --git a/tests/components/tuya/test_config_flow.py b/tests/components/tuya/test_config_flow.py index c38d8e5f8b5..646d6a09f12 100644 --- a/tests/components/tuya/test_config_flow.py +++ b/tests/components/tuya/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for the Tuya config flow.""" + from __future__ import annotations from unittest.mock import MagicMock diff --git a/tests/components/twentemilieu/conftest.py b/tests/components/twentemilieu/conftest.py index c42e3a9eb58..670bd648cac 100644 --- a/tests/components/twentemilieu/conftest.py +++ b/tests/components/twentemilieu/conftest.py @@ -1,4 +1,5 @@ """Fixtures for the Twente Milieu integration tests.""" + from __future__ import annotations from collections.abc import Generator @@ -48,11 +49,14 @@ def mock_setup_entry() -> Generator[None, None, None]: @pytest.fixture def mock_twentemilieu() -> Generator[MagicMock, None, None]: """Return a mocked Twente Milieu client.""" - with patch( - "homeassistant.components.twentemilieu.TwenteMilieu", autospec=True - ) as twentemilieu_mock, patch( - "homeassistant.components.twentemilieu.config_flow.TwenteMilieu", - new=twentemilieu_mock, + with ( + patch( + "homeassistant.components.twentemilieu.TwenteMilieu", autospec=True + ) as twentemilieu_mock, + patch( + "homeassistant.components.twentemilieu.config_flow.TwenteMilieu", + new=twentemilieu_mock, + ), ): twentemilieu = twentemilieu_mock.return_value twentemilieu.unique_id.return_value = 12345 diff --git a/tests/components/twentemilieu/snapshots/test_calendar.ambr b/tests/components/twentemilieu/snapshots/test_calendar.ambr index 5399e6f547a..78b2d56afca 100644 --- a/tests/components/twentemilieu/snapshots/test_calendar.ambr +++ b/tests/components/twentemilieu/snapshots/test_calendar.ambr @@ -32,7 +32,6 @@ 'description': '', 'end_time': '2022-01-07 00:00:00', 'friendly_name': 'Twente Milieu', - 'icon': 'mdi:delete-empty', 'location': '', 'message': 'Christmas tree pickup', 'start_time': '2022-01-06 00:00:00', @@ -40,6 +39,7 @@ 'context': , 'entity_id': 'calendar.twente_milieu', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }) @@ -67,12 +67,12 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:delete-empty', + 'original_icon': None, 'original_name': None, 'platform': 'twentemilieu', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'calendar', 'unique_id': '12345', 'unit_of_measurement': None, }) diff --git a/tests/components/twentemilieu/snapshots/test_sensor.ambr b/tests/components/twentemilieu/snapshots/test_sensor.ambr index 0a1be9f4455..a0f3b75da57 100644 --- a/tests/components/twentemilieu/snapshots/test_sensor.ambr +++ b/tests/components/twentemilieu/snapshots/test_sensor.ambr @@ -4,11 +4,11 @@ 'attributes': ReadOnlyDict({ 'device_class': 'date', 'friendly_name': 'Twente Milieu Christmas tree pickup', - 'icon': 'mdi:pine-tree', }), 'context': , 'entity_id': 'sensor.twente_milieu_christmas_tree_pickup', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '2022-01-06', }) @@ -36,7 +36,7 @@ 'options': dict({ }), 'original_device_class': , - 'original_icon': 'mdi:pine-tree', + 'original_icon': None, 'original_name': 'Christmas tree pickup', 'platform': 'twentemilieu', 'previous_unique_id': None, @@ -81,11 +81,11 @@ 'attributes': ReadOnlyDict({ 'device_class': 'date', 'friendly_name': 'Twente Milieu Non-recyclable waste pickup', - 'icon': 'mdi:delete-empty', }), 'context': , 'entity_id': 'sensor.twente_milieu_non_recyclable_waste_pickup', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '2021-11-01', }) @@ -113,7 +113,7 @@ 'options': dict({ }), 'original_device_class': , - 'original_icon': 'mdi:delete-empty', + 'original_icon': None, 'original_name': 'Non-recyclable waste pickup', 'platform': 'twentemilieu', 'previous_unique_id': None, @@ -158,11 +158,11 @@ 'attributes': ReadOnlyDict({ 'device_class': 'date', 'friendly_name': 'Twente Milieu Organic waste pickup', - 'icon': 'mdi:delete-empty', }), 'context': , 'entity_id': 'sensor.twente_milieu_organic_waste_pickup', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '2021-11-02', }) @@ -190,7 +190,7 @@ 'options': dict({ }), 'original_device_class': , - 'original_icon': 'mdi:delete-empty', + 'original_icon': None, 'original_name': 'Organic waste pickup', 'platform': 'twentemilieu', 'previous_unique_id': None, @@ -235,11 +235,11 @@ 'attributes': ReadOnlyDict({ 'device_class': 'date', 'friendly_name': 'Twente Milieu Packages waste pickup', - 'icon': 'mdi:delete-empty', }), 'context': , 'entity_id': 'sensor.twente_milieu_packages_waste_pickup', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '2021-11-03', }) @@ -267,7 +267,7 @@ 'options': dict({ }), 'original_device_class': , - 'original_icon': 'mdi:delete-empty', + 'original_icon': None, 'original_name': 'Packages waste pickup', 'platform': 'twentemilieu', 'previous_unique_id': None, @@ -312,11 +312,11 @@ 'attributes': ReadOnlyDict({ 'device_class': 'date', 'friendly_name': 'Twente Milieu Paper waste pickup', - 'icon': 'mdi:delete-empty', }), 'context': , 'entity_id': 'sensor.twente_milieu_paper_waste_pickup', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }) @@ -344,7 +344,7 @@ 'options': dict({ }), 'original_device_class': , - 'original_icon': 'mdi:delete-empty', + 'original_icon': None, 'original_name': 'Paper waste pickup', 'platform': 'twentemilieu', 'previous_unique_id': None, diff --git a/tests/components/twentemilieu/test_calendar.py b/tests/components/twentemilieu/test_calendar.py index 7610b8b003b..ba95b8c59cd 100644 --- a/tests/components/twentemilieu/test_calendar.py +++ b/tests/components/twentemilieu/test_calendar.py @@ -1,4 +1,5 @@ """Tests for the Twente Milieu calendar.""" + from http import HTTPStatus import pytest diff --git a/tests/components/twentemilieu/test_config_flow.py b/tests/components/twentemilieu/test_config_flow.py index e5875ecaab7..e272ce38bee 100644 --- a/tests/components/twentemilieu/test_config_flow.py +++ b/tests/components/twentemilieu/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for the Twente Milieu config flow.""" + from unittest.mock import MagicMock import pytest diff --git a/tests/components/twentemilieu/test_diagnostics.py b/tests/components/twentemilieu/test_diagnostics.py index 0828d35ec51..586b0ca4fde 100644 --- a/tests/components/twentemilieu/test_diagnostics.py +++ b/tests/components/twentemilieu/test_diagnostics.py @@ -1,4 +1,5 @@ """Tests for the diagnostics data provided by the TwenteMilieu integration.""" + from syrupy.assertion import SnapshotAssertion from homeassistant.core import HomeAssistant diff --git a/tests/components/twentemilieu/test_init.py b/tests/components/twentemilieu/test_init.py index 64377022713..901252f050f 100644 --- a/tests/components/twentemilieu/test_init.py +++ b/tests/components/twentemilieu/test_init.py @@ -1,4 +1,5 @@ """Tests for the Twente Milieu integration.""" + from unittest.mock import MagicMock, patch import pytest diff --git a/tests/components/twentemilieu/test_sensor.py b/tests/components/twentemilieu/test_sensor.py index e4b845264db..6fd39e38d48 100644 --- a/tests/components/twentemilieu/test_sensor.py +++ b/tests/components/twentemilieu/test_sensor.py @@ -1,4 +1,5 @@ """Tests for the Twente Milieu sensors.""" + import pytest from syrupy.assertion import SnapshotAssertion diff --git a/tests/components/twilio/test_init.py b/tests/components/twilio/test_init.py index 0b181a79d0d..77e6afe3a12 100644 --- a/tests/components/twilio/test_init.py +++ b/tests/components/twilio/test_init.py @@ -1,4 +1,5 @@ """Test the init file of Twilio.""" + from homeassistant import config_entries, data_entry_flow from homeassistant.components import twilio from homeassistant.config import async_process_ha_core_config diff --git a/tests/components/twinkly/__init__.py b/tests/components/twinkly/__init__.py index 4b1411e9223..f322004962a 100644 --- a/tests/components/twinkly/__init__.py +++ b/tests/components/twinkly/__init__.py @@ -1,6 +1,5 @@ """Constants and mock for the twinkly component tests.""" - from aiohttp.client_exceptions import ClientConnectionError from homeassistant.components.twinkly.const import DEV_NAME @@ -43,38 +42,38 @@ class ClientMock: async def get_details(self): """Get the mocked device info.""" if self.is_offline: - raise ClientConnectionError() + raise ClientConnectionError return self.device_info async def is_on(self) -> bool: """Get the mocked on/off state.""" if self.is_offline: - raise ClientConnectionError() + raise ClientConnectionError return self.state async def turn_on(self) -> None: """Set the mocked on state.""" if self.is_offline: - raise ClientConnectionError() + raise ClientConnectionError self.state = True self.mode = self.default_mode async def turn_off(self) -> None: """Set the mocked off state.""" if self.is_offline: - raise ClientConnectionError() + raise ClientConnectionError self.state = False async def get_brightness(self) -> int: """Get the mocked brightness.""" if self.is_offline: - raise ClientConnectionError() + raise ClientConnectionError return self.brightness async def set_brightness(self, brightness: int) -> None: """Set the mocked brightness.""" if self.is_offline: - raise ClientConnectionError() + raise ClientConnectionError self.brightness = {"mode": "enabled", "value": brightness} def change_name(self, new_name: str) -> None: diff --git a/tests/components/twinkly/conftest.py b/tests/components/twinkly/conftest.py index 5a689c31baa..6705d570205 100644 --- a/tests/components/twinkly/conftest.py +++ b/tests/components/twinkly/conftest.py @@ -1,4 +1,5 @@ """Configure tests for the Twinkly integration.""" + from collections.abc import Awaitable, Callable, Coroutine from typing import Any from unittest.mock import patch diff --git a/tests/components/twinkly/snapshots/test_diagnostics.ambr b/tests/components/twinkly/snapshots/test_diagnostics.ambr index 2a10154c3da..0601159ca4c 100644 --- a/tests/components/twinkly/snapshots/test_diagnostics.ambr +++ b/tests/components/twinkly/snapshots/test_diagnostics.ambr @@ -8,7 +8,6 @@ 'effect_list': list([ ]), 'friendly_name': 'twinkly_test_device_name', - 'icon': 'mdi:string-lights', 'supported_color_modes': list([ 'brightness', ]), diff --git a/tests/components/twinkly/test_config_flow.py b/tests/components/twinkly/test_config_flow.py index 4ddc0d4c195..f797f9b01b6 100644 --- a/tests/components/twinkly/test_config_flow.py +++ b/tests/components/twinkly/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for the config_flow of the twinly component.""" + from unittest.mock import patch from homeassistant import config_entries @@ -38,9 +39,12 @@ async def test_invalid_host(hass: HomeAssistant) -> None: async def test_success_flow(hass: HomeAssistant) -> None: """Test that an entity is created when the flow completes.""" client = ClientMock() - with patch( - "homeassistant.components.twinkly.config_flow.Twinkly", return_value=client - ), patch("homeassistant.components.twinkly.async_setup_entry", return_value=True): + with ( + patch( + "homeassistant.components.twinkly.config_flow.Twinkly", return_value=client + ), + patch("homeassistant.components.twinkly.async_setup_entry", return_value=True), + ): result = await hass.config_entries.flow.async_init( TWINKLY_DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -88,9 +92,12 @@ async def test_dhcp_can_confirm(hass: HomeAssistant) -> None: async def test_dhcp_success(hass: HomeAssistant) -> None: """Test DHCP discovery flow success.""" client = ClientMock() - with patch( - "homeassistant.components.twinkly.config_flow.Twinkly", return_value=client - ), patch("homeassistant.components.twinkly.async_setup_entry", return_value=True): + with ( + patch( + "homeassistant.components.twinkly.config_flow.Twinkly", return_value=client + ), + patch("homeassistant.components.twinkly.async_setup_entry", return_value=True), + ): result = await hass.config_entries.flow.async_init( TWINKLY_DOMAIN, context={"source": config_entries.SOURCE_DHCP}, diff --git a/tests/components/twinkly/test_diagnostics.py b/tests/components/twinkly/test_diagnostics.py index ab07cabef4a..680f82365c0 100644 --- a/tests/components/twinkly/test_diagnostics.py +++ b/tests/components/twinkly/test_diagnostics.py @@ -1,4 +1,5 @@ """Tests for the diagnostics of the twinkly component.""" + from collections.abc import Awaitable, Callable from syrupy import SnapshotAssertion diff --git a/tests/components/twinkly/test_light.py b/tests/components/twinkly/test_light.py index 452467bb160..7a55dbec14a 100644 --- a/tests/components/twinkly/test_light.py +++ b/tests/components/twinkly/test_light.py @@ -1,4 +1,5 @@ """Tests for the integration of a twinly device.""" + from __future__ import annotations from datetime import timedelta @@ -30,9 +31,6 @@ async def test_initial_state(hass: HomeAssistant) -> None: assert state.state == "on" assert state.attributes[ATTR_BRIGHTNESS] == 26 assert state.attributes["friendly_name"] == TEST_NAME - assert state.attributes["icon"] == "mdi:string-lights" - - assert entity.original_icon == "mdi:string-lights" assert device.name == TEST_NAME assert device.model == TEST_MODEL diff --git a/tests/components/twitch/__init__.py b/tests/components/twitch/__init__.py index 268f18d87c1..d37c386f0a3 100644 --- a/tests/components/twitch/__init__.py +++ b/tests/components/twitch/__init__.py @@ -1,4 +1,5 @@ """Tests for the Twitch component.""" + import asyncio from collections.abc import AsyncGenerator, AsyncIterator from dataclasses import dataclass @@ -122,7 +123,6 @@ class TwitchMock: async def _noop(self): """Fake function to create task.""" - pass async def get_users( self, user_ids: list[str] | None = None, logins: list[str] | None = None @@ -156,7 +156,6 @@ class TwitchMock: validate: bool = True, ) -> None: """Set user authentication.""" - pass async def get_followed_channels( self, user_id: str, broadcaster_id: str | None = None @@ -199,7 +198,7 @@ class TwitchUnauthorizedMock(TwitchMock): def __await__(self): """Add async capabilities to the mock.""" - raise TwitchAuthorizationException() + raise TwitchAuthorizationException class TwitchMissingScopeMock(TwitchMock): @@ -209,7 +208,7 @@ class TwitchMissingScopeMock(TwitchMock): self, token: str, scope: list[AuthScope], validate: bool = True ) -> None: """Set user authentication.""" - raise MissingScopeException() + raise MissingScopeException class TwitchInvalidTokenMock(TwitchMock): @@ -219,7 +218,7 @@ class TwitchInvalidTokenMock(TwitchMock): self, token: str, scope: list[AuthScope], validate: bool = True ) -> None: """Set user authentication.""" - raise InvalidTokenException() + raise InvalidTokenException class TwitchInvalidUserMock(TwitchMock): @@ -244,4 +243,4 @@ class TwitchAPIExceptionMock(TwitchMock): self, broadcaster_id: str, user_id: str ) -> UserSubscriptionMock: """Check if the user is subscribed.""" - raise TwitchAPIException() + raise TwitchAPIException diff --git a/tests/components/twitch/conftest.py b/tests/components/twitch/conftest.py index b3894203786..1cebc068831 100644 --- a/tests/components/twitch/conftest.py +++ b/tests/components/twitch/conftest.py @@ -1,4 +1,5 @@ """Configure tests for the Twitch integration.""" + from collections.abc import Awaitable, Callable, Generator import time from unittest.mock import AsyncMock, patch @@ -100,11 +101,14 @@ def twitch_mock() -> TwitchMock: @pytest.fixture(name="twitch") def mock_twitch(twitch_mock: TwitchMock): """Mock Twitch.""" - with patch( - "homeassistant.components.twitch.Twitch", - return_value=twitch_mock, - ), patch( - "homeassistant.components.twitch.config_flow.Twitch", - return_value=twitch_mock, + with ( + patch( + "homeassistant.components.twitch.Twitch", + return_value=twitch_mock, + ), + patch( + "homeassistant.components.twitch.config_flow.Twitch", + return_value=twitch_mock, + ), ): yield twitch_mock diff --git a/tests/components/twitch/test_config_flow.py b/tests/components/twitch/test_config_flow.py index 36312fea83e..4b6834ba544 100644 --- a/tests/components/twitch/test_config_flow.py +++ b/tests/components/twitch/test_config_flow.py @@ -1,4 +1,5 @@ """Test config flow for Twitch.""" + from unittest.mock import patch import pytest diff --git a/tests/components/twitch/test_init.py b/tests/components/twitch/test_init.py index da03857a95d..d3b9313c46e 100644 --- a/tests/components/twitch/test_init.py +++ b/tests/components/twitch/test_init.py @@ -1,4 +1,5 @@ """Tests for YouTube.""" + import http import time from unittest.mock import patch diff --git a/tests/components/twitch/test_sensor.py b/tests/components/twitch/test_sensor.py index 047c55d3b72..3385cb228fd 100644 --- a/tests/components/twitch/test_sensor.py +++ b/tests/components/twitch/test_sensor.py @@ -1,4 +1,5 @@ """The tests for an update of the Twitch component.""" + from datetime import datetime import pytest diff --git a/tests/fixtures/uk_transport_bus.json b/tests/components/uk_transport/fixtures/bus.json similarity index 100% rename from tests/fixtures/uk_transport_bus.json rename to tests/components/uk_transport/fixtures/bus.json diff --git a/tests/fixtures/uk_transport_train.json b/tests/components/uk_transport/fixtures/train.json similarity index 100% rename from tests/fixtures/uk_transport_train.json rename to tests/components/uk_transport/fixtures/train.json diff --git a/tests/components/uk_transport/test_sensor.py b/tests/components/uk_transport/test_sensor.py index d60966fc6d7..a4a9aea18c8 100644 --- a/tests/components/uk_transport/test_sensor.py +++ b/tests/components/uk_transport/test_sensor.py @@ -1,4 +1,5 @@ """The tests for the uk_transport platform.""" + import re from unittest.mock import patch @@ -48,7 +49,7 @@ async def test_bus(hass: HomeAssistant) -> None: """Test for operational uk_transport sensor with proper attributes.""" with requests_mock.Mocker() as mock_req: uri = re.compile(UkTransportSensor.TRANSPORT_API_URL_BASE + "*") - mock_req.get(uri, text=load_fixture("uk_transport_bus.json")) + mock_req.get(uri, text=load_fixture("uk_transport/bus.json")) assert await async_setup_component(hass, "sensor", VALID_CONFIG) await hass.async_block_till_done() @@ -68,11 +69,12 @@ async def test_bus(hass: HomeAssistant) -> None: async def test_train(hass: HomeAssistant) -> None: """Test for operational uk_transport sensor with proper attributes.""" - with requests_mock.Mocker() as mock_req, patch( - "homeassistant.util.dt.now", return_value=now().replace(hour=13) + with ( + requests_mock.Mocker() as mock_req, + patch("homeassistant.util.dt.now", return_value=now().replace(hour=13)), ): uri = re.compile(UkTransportSensor.TRANSPORT_API_URL_BASE + "*") - mock_req.get(uri, text=load_fixture("uk_transport_train.json")) + mock_req.get(uri, text=load_fixture("uk_transport/train.json")) assert await async_setup_component(hass, "sensor", VALID_CONFIG) await hass.async_block_till_done() diff --git a/tests/components/ukraine_alarm/test_config_flow.py b/tests/components/ukraine_alarm/test_config_flow.py index 3bb776dadb0..6d9ce7b7f72 100644 --- a/tests/components/ukraine_alarm/test_config_flow.py +++ b/tests/components/ukraine_alarm/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Ukraine Alarm config flow.""" + from collections.abc import Generator from unittest.mock import AsyncMock, patch diff --git a/tests/components/unifi/conftest.py b/tests/components/unifi/conftest.py index 162f2b4d3aa..1ef8948ec51 100644 --- a/tests/components/unifi/conftest.py +++ b/tests/components/unifi/conftest.py @@ -1,4 +1,5 @@ """Fixtures for UniFi Network methods.""" + from __future__ import annotations import asyncio @@ -45,10 +46,10 @@ class WebsocketStateManager(asyncio.Event): """ hub = self.hass.data[UNIFI_DOMAIN][DEFAULT_CONFIG_ENTRY_ID] self.aioclient_mock.get( - f"https://{hub.host}:1234", status=302 + f"https://{hub.config.host}:1234", status=302 ) # Check UniFi OS self.aioclient_mock.post( - f"https://{hub.host}:1234/api/login", + f"https://{hub.config.host}:1234/api/login", json={"data": "login successful", "meta": {"rc": "ok"}}, headers={"content-type": CONTENT_TYPE_JSON}, ) diff --git a/tests/components/unifi/test_button.py b/tests/components/unifi/test_button.py index 4518393226c..d5be861139b 100644 --- a/tests/components/unifi/test_button.py +++ b/tests/components/unifi/test_button.py @@ -1,8 +1,13 @@ """UniFi Network button platform tests.""" from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, ButtonDeviceClass -from homeassistant.components.unifi.const import DOMAIN as UNIFI_DOMAIN -from homeassistant.const import ATTR_DEVICE_CLASS, STATE_UNAVAILABLE, EntityCategory +from homeassistant.components.unifi.const import CONF_SITE_ID +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + CONF_HOST, + STATE_UNAVAILABLE, + EntityCategory, +) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -36,8 +41,6 @@ async def test_restart_device_button( } ], ) - hub = hass.data[UNIFI_DOMAIN][config_entry.entry_id] - assert len(hass.states.async_entity_ids(BUTTON_DOMAIN)) == 1 ent_reg_entry = entity_registry.async_get("button.switch_restart") @@ -52,7 +55,8 @@ async def test_restart_device_button( # Send restart device command aioclient_mock.clear_requests() aioclient_mock.post( - f"https://{hub.host}:1234/api/s/{hub.site}/cmd/devmgr", + f"https://{config_entry.data[CONF_HOST]}:1234" + f"/api/s/{config_entry.data[CONF_SITE_ID]}/cmd/devmgr", ) await hass.services.async_call( @@ -120,8 +124,6 @@ async def test_power_cycle_poe( } ], ) - hub = hass.data[UNIFI_DOMAIN][config_entry.entry_id] - assert len(hass.states.async_entity_ids(BUTTON_DOMAIN)) == 2 ent_reg_entry = entity_registry.async_get("button.switch_port_1_power_cycle") @@ -136,7 +138,8 @@ async def test_power_cycle_poe( # Send restart device command aioclient_mock.clear_requests() aioclient_mock.post( - f"https://{hub.host}:1234/api/s/{hub.site}/cmd/devmgr", + f"https://{config_entry.data[CONF_HOST]}:1234" + f"/api/s/{config_entry.data[CONF_SITE_ID]}/cmd/devmgr", ) await hass.services.async_call( diff --git a/tests/components/unifi/test_config_flow.py b/tests/components/unifi/test_config_flow.py index 572f7a2ff05..ee309ca2579 100644 --- a/tests/components/unifi/test_config_flow.py +++ b/tests/components/unifi/test_config_flow.py @@ -1,4 +1,5 @@ """Test UniFi Network config flow.""" + import socket from unittest.mock import patch @@ -75,14 +76,15 @@ DEVICES = [ ] WLANS = [ - {"_id": "1", "name": "SSID 1"}, + {"_id": "1", "name": "SSID 1", "enabled": True}, { "_id": "2", "name": "SSID 2", "name_combine_enabled": False, "name_combine_suffix": "_IOT", + "enabled": True, }, - {"_id": "3", "name": "SSID 4", "name_combine_enabled": False}, + {"_id": "3", "name": "SSID 4", "name_combine_enabled": False, "enabled": True}, ] DPI_GROUPS = [ @@ -541,12 +543,7 @@ async def test_simple_option_flow( ) -> None: """Test simple config flow options.""" config_entry = await setup_unifi_integration( - hass, - aioclient_mock, - clients_response=CLIENTS, - wlans_response=WLANS, - dpigroup_response=DPI_GROUPS, - dpiapp_response=[], + hass, aioclient_mock, clients_response=CLIENTS ) result = await hass.config_entries.options.async_init( diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py index 9a4b406c364..b22767a2914 100644 --- a/tests/components/unifi/test_device_tracker.py +++ b/tests/components/unifi/test_device_tracker.py @@ -1,4 +1,5 @@ """The tests for the UniFi Network device tracker platform.""" + from datetime import timedelta from aiounifi.models.message import MessageKey @@ -9,11 +10,13 @@ from homeassistant.components.device_tracker import DOMAIN as TRACKER_DOMAIN from homeassistant.components.unifi.const import ( CONF_BLOCK_CLIENT, CONF_CLIENT_SOURCE, + CONF_DETECTION_TIME, CONF_IGNORE_WIRED_BUG, CONF_SSID_FILTER, CONF_TRACK_CLIENTS, CONF_TRACK_DEVICES, CONF_TRACK_WIRED_CLIENTS, + DEFAULT_DETECTION_TIME, DOMAIN as UNIFI_DOMAIN, ) from homeassistant.const import STATE_HOME, STATE_NOT_HOME, STATE_UNAVAILABLE @@ -55,7 +58,6 @@ async def test_tracked_wireless_clients( config_entry = await setup_unifi_integration( hass, aioclient_mock, clients_response=[client] ) - hub = hass.data[UNIFI_DOMAIN][config_entry.entry_id] assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 1 assert hass.states.get("device_tracker.client").state == STATE_NOT_HOME @@ -70,7 +72,9 @@ async def test_tracked_wireless_clients( # Change time to mark client as away - new_time = dt_util.utcnow() + hub.option_detection_time + new_time = dt_util.utcnow() + timedelta( + seconds=config_entry.options.get(CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME) + ) with freeze_time(new_time): async_fire_time_changed(hass, new_time) await hass.async_block_till_done() @@ -194,7 +198,7 @@ async def test_tracked_wireless_clients_event_source( config_entry = await setup_unifi_integration( hass, aioclient_mock, clients_response=[client] ) - hub = hass.data[UNIFI_DOMAIN][config_entry.entry_id] + assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 1 assert hass.states.get("device_tracker.client").state == STATE_NOT_HOME @@ -243,7 +247,14 @@ async def test_tracked_wireless_clients_event_source( assert hass.states.get("device_tracker.client").state == STATE_HOME # Change time to mark client as away - freezer.tick(hub.option_detection_time + timedelta(seconds=1)) + freezer.tick( + timedelta( + seconds=( + config_entry.options.get(CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME) + + 1 + ) + ) + ) async_fire_time_changed(hass) await hass.async_block_till_done() @@ -282,7 +293,14 @@ async def test_tracked_wireless_clients_event_source( assert hass.states.get("device_tracker.client").state == STATE_HOME # Change time to mark client as away - freezer.tick(hub.option_detection_time + timedelta(seconds=1)) + freezer.tick( + timedelta( + seconds=( + config_entry.options.get(CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME) + + 1 + ) + ) + ) async_fire_time_changed(hass) await hass.async_block_till_done() @@ -682,10 +700,8 @@ async def test_option_ssid_filter( config_entry = await setup_unifi_integration( hass, aioclient_mock, clients_response=[client, client_on_ssid2] ) - hub = hass.data[UNIFI_DOMAIN][config_entry.entry_id] assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 2 - assert hass.states.get("device_tracker.client").state == STATE_HOME assert hass.states.get("device_tracker.client_on_ssid2").state == STATE_NOT_HOME @@ -711,7 +727,11 @@ async def test_option_ssid_filter( mock_unifi_websocket(message=MessageKey.CLIENT, data=client_on_ssid2) await hass.async_block_till_done() - new_time = dt_util.utcnow() + hub.option_detection_time + new_time = dt_util.utcnow() + timedelta( + seconds=( + config_entry.options.get(CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME) + 1 + ) + ) with freeze_time(new_time): async_fire_time_changed(hass, new_time) await hass.async_block_till_done() @@ -739,7 +759,11 @@ async def test_option_ssid_filter( # Time pass to mark client as away - new_time += hub.option_detection_time + new_time += timedelta( + seconds=( + config_entry.options.get(CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME) + 1 + ) + ) with freeze_time(new_time): async_fire_time_changed(hass, new_time) await hass.async_block_till_done() @@ -758,7 +782,9 @@ async def test_option_ssid_filter( mock_unifi_websocket(message=MessageKey.CLIENT, data=client_on_ssid2) await hass.async_block_till_done() - new_time += hub.option_detection_time + new_time += timedelta( + seconds=(config_entry.options.get(CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME)) + ) with freeze_time(new_time): async_fire_time_changed(hass, new_time) await hass.async_block_till_done() @@ -788,7 +814,6 @@ async def test_wireless_client_go_wired_issue( config_entry = await setup_unifi_integration( hass, aioclient_mock, clients_response=[client] ) - hub = hass.data[UNIFI_DOMAIN][config_entry.entry_id] assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 1 @@ -807,7 +832,9 @@ async def test_wireless_client_go_wired_issue( assert client_state.state == STATE_HOME # Pass time - new_time = dt_util.utcnow() + hub.option_detection_time + new_time = dt_util.utcnow() + timedelta( + seconds=(config_entry.options.get(CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME)) + ) with freeze_time(new_time): async_fire_time_changed(hass, new_time) await hass.async_block_till_done() @@ -859,7 +886,6 @@ async def test_option_ignore_wired_bug( options={CONF_IGNORE_WIRED_BUG: True}, clients_response=[client], ) - hub = hass.data[UNIFI_DOMAIN][config_entry.entry_id] assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 1 # Client is wireless @@ -876,7 +902,9 @@ async def test_option_ignore_wired_bug( assert client_state.state == STATE_HOME # pass time - new_time = dt_util.utcnow() + hub.option_detection_time + new_time = dt_util.utcnow() + timedelta( + seconds=config_entry.options.get(CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME) + ) with freeze_time(new_time): async_fire_time_changed(hass, new_time) await hass.async_block_till_done() diff --git a/tests/components/unifi/test_diagnostics.py b/tests/components/unifi/test_diagnostics.py index ce5290bccef..792512683d3 100644 --- a/tests/components/unifi/test_diagnostics.py +++ b/tests/components/unifi/test_diagnostics.py @@ -1,4 +1,5 @@ """Test UniFi Network diagnostics.""" + from homeassistant.components.diagnostics import REDACTED from homeassistant.components.unifi.const import ( CONF_ALLOW_BANDWIDTH_SENSORS, diff --git a/tests/components/unifi/test_hub.py b/tests/components/unifi/test_hub.py index 35b6e50cfd4..1fddb623930 100644 --- a/tests/components/unifi/test_hub.py +++ b/tests/components/unifi/test_hub.py @@ -1,4 +1,5 @@ """Test UniFi Network.""" + from copy import deepcopy from datetime import timedelta from http import HTTPStatus @@ -23,11 +24,11 @@ from homeassistant.components.unifi.const import ( DEFAULT_TRACK_DEVICES, DEFAULT_TRACK_WIRED_CLIENTS, DOMAIN as UNIFI_DOMAIN, - PLATFORMS, UNIFI_WIRELESS_CLIENTS, ) from homeassistant.components.unifi.errors import AuthenticationRequired, CannotConnect from homeassistant.components.unifi.hub import get_unifi_api +from homeassistant.components.update import DOMAIN as UPDATE_DOMAIN from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, @@ -247,7 +248,7 @@ async def test_hub_setup( ) -> None: """Successful setup.""" with patch( - "homeassistant.config_entries.ConfigEntries.async_forward_entry_setup", + "homeassistant.config_entries.ConfigEntries.async_forward_entry_setups", return_value=True, ) as forward_entry_setup: config_entry = await setup_unifi_integration( @@ -255,25 +256,31 @@ async def test_hub_setup( ) hub = hass.data[UNIFI_DOMAIN][config_entry.entry_id] - entry = hub.config_entry - assert len(forward_entry_setup.mock_calls) == len(PLATFORMS) - assert forward_entry_setup.mock_calls[0][1] == (entry, BUTTON_DOMAIN) - assert forward_entry_setup.mock_calls[1][1] == (entry, TRACKER_DOMAIN) - assert forward_entry_setup.mock_calls[2][1] == (entry, IMAGE_DOMAIN) - assert forward_entry_setup.mock_calls[3][1] == (entry, SENSOR_DOMAIN) - assert forward_entry_setup.mock_calls[4][1] == (entry, SWITCH_DOMAIN) + entry = hub.config.entry + assert len(forward_entry_setup.mock_calls) == 1 + assert forward_entry_setup.mock_calls[0][1] == ( + entry, + [ + BUTTON_DOMAIN, + TRACKER_DOMAIN, + IMAGE_DOMAIN, + SENSOR_DOMAIN, + SWITCH_DOMAIN, + UPDATE_DOMAIN, + ], + ) - assert hub.host == ENTRY_CONFIG[CONF_HOST] + assert hub.config.host == ENTRY_CONFIG[CONF_HOST] assert hub.is_admin == (SITE[0]["role"] == "admin") - assert hub.option_allow_bandwidth_sensors == DEFAULT_ALLOW_BANDWIDTH_SENSORS - assert hub.option_allow_uptime_sensors == DEFAULT_ALLOW_UPTIME_SENSORS - assert isinstance(hub.option_block_clients, list) - assert hub.option_track_clients == DEFAULT_TRACK_CLIENTS - assert hub.option_track_devices == DEFAULT_TRACK_DEVICES - assert hub.option_track_wired_clients == DEFAULT_TRACK_WIRED_CLIENTS - assert hub.option_detection_time == timedelta(seconds=DEFAULT_DETECTION_TIME) - assert isinstance(hub.option_ssid_filter, set) + assert hub.config.option_allow_bandwidth_sensors == DEFAULT_ALLOW_BANDWIDTH_SENSORS + assert hub.config.option_allow_uptime_sensors == DEFAULT_ALLOW_UPTIME_SENSORS + assert isinstance(hub.config.option_block_clients, list) + assert hub.config.option_track_clients == DEFAULT_TRACK_CLIENTS + assert hub.config.option_track_devices == DEFAULT_TRACK_DEVICES + assert hub.config.option_track_wired_clients == DEFAULT_TRACK_WIRED_CLIENTS + assert hub.config.option_detection_time == timedelta(seconds=DEFAULT_DETECTION_TIME) + assert isinstance(hub.config.option_ssid_filter, set) assert hub.signal_reachable == "unifi-reachable-1" assert hub.signal_options_update == "unifi-options-1" @@ -299,10 +306,13 @@ async def test_hub_not_accessible(hass: HomeAssistant) -> None: async def test_hub_trigger_reauth_flow(hass: HomeAssistant) -> None: """Failed authentication trigger a reauthentication flow.""" - with patch( - "homeassistant.components.unifi.get_unifi_api", - side_effect=AuthenticationRequired, - ), patch.object(hass.config_entries.flow, "async_init") as mock_flow_init: + with ( + patch( + "homeassistant.components.unifi.get_unifi_api", + 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() assert hass.data[UNIFI_DOMAIN] == {} @@ -433,9 +443,12 @@ async def test_reconnect_mechanism_exceptions( """Verify async_reconnect calls expected methods.""" await setup_unifi_integration(hass, aioclient_mock) - with patch("aiounifi.Controller.login", side_effect=exception), patch( - "homeassistant.components.unifi.hub.hub.UnifiWebsocket.reconnect" - ) as mock_reconnect: + with ( + patch("aiounifi.Controller.login", side_effect=exception), + patch( + "homeassistant.components.unifi.hub.hub.UnifiWebsocket.reconnect" + ) as mock_reconnect, + ): await websocket_mock.disconnect() await websocket_mock.reconnect() @@ -474,7 +487,8 @@ async def test_get_unifi_api_fails_to_connect( hass: HomeAssistant, side_effect, raised_exception ) -> None: """Check that get_unifi_api can handle UniFi Network being unavailable.""" - with patch("aiounifi.Controller.login", side_effect=side_effect), pytest.raises( - raised_exception + with ( + patch("aiounifi.Controller.login", side_effect=side_effect), + pytest.raises(raised_exception), ): await get_unifi_api(hass, ENTRY_CONFIG) diff --git a/tests/components/unifi/test_init.py b/tests/components/unifi/test_init.py index f4d735c1d52..9053b47cbaf 100644 --- a/tests/components/unifi/test_init.py +++ b/tests/components/unifi/test_init.py @@ -1,4 +1,5 @@ """Test UniFi Network integration setup process.""" + from typing import Any from unittest.mock import patch @@ -41,10 +42,13 @@ async def test_setup_entry_fails_config_entry_not_ready(hass: HomeAssistant) -> async def test_setup_entry_fails_trigger_reauth_flow(hass: HomeAssistant) -> None: """Failed authentication trigger a reauthentication flow.""" - with patch( - "homeassistant.components.unifi.get_unifi_api", - side_effect=AuthenticationRequired, - ), patch.object(hass.config_entries.flow, "async_init") as mock_flow_init: + with ( + patch( + "homeassistant.components.unifi.get_unifi_api", + 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() diff --git a/tests/components/unifi/test_sensor.py b/tests/components/unifi/test_sensor.py index 8adc379dde8..7a58252a6bd 100644 --- a/tests/components/unifi/test_sensor.py +++ b/tests/components/unifi/test_sensor.py @@ -1,4 +1,5 @@ """UniFi Network sensor platform tests.""" + from copy import deepcopy from datetime import datetime, timedelta from unittest.mock import patch @@ -18,10 +19,11 @@ from homeassistant.components.sensor import ( from homeassistant.components.unifi.const import ( CONF_ALLOW_BANDWIDTH_SENSORS, CONF_ALLOW_UPTIME_SENSORS, + CONF_DETECTION_TIME, CONF_TRACK_CLIENTS, CONF_TRACK_DEVICES, + DEFAULT_DETECTION_TIME, DEVICE_STATES, - DOMAIN as UNIFI_DOMAIN, ) from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY from homeassistant.const import ATTR_DEVICE_CLASS, STATE_UNAVAILABLE, EntityCategory @@ -395,7 +397,6 @@ async def test_bandwidth_sensors( # Verify reset sensor after heartbeat expires - hub = hass.data[UNIFI_DOMAIN][config_entry.entry_id] new_time = dt_util.utcnow() wireless_client["last_seen"] = dt_util.as_timestamp(new_time) @@ -409,8 +410,11 @@ async def test_bandwidth_sensors( assert hass.states.get("sensor.wireless_client_rx").state == "3456.0" assert hass.states.get("sensor.wireless_client_tx").state == "7891.0" - new_time = new_time + hub.option_detection_time + timedelta(seconds=1) - + new_time += timedelta( + seconds=( + config_entry.options.get(CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME) + 1 + ) + ) with freeze_time(new_time): async_fire_time_changed(hass, new_time) await hass.async_block_till_done() diff --git a/tests/components/unifi/test_services.py b/tests/components/unifi/test_services.py index 613c492a490..3f7da7a63ae 100644 --- a/tests/components/unifi/test_services.py +++ b/tests/components/unifi/test_services.py @@ -1,13 +1,14 @@ """deCONZ service tests.""" + from unittest.mock import patch -from homeassistant.components.unifi.const import DOMAIN as UNIFI_DOMAIN +from homeassistant.components.unifi.const import CONF_SITE_ID, DOMAIN as UNIFI_DOMAIN from homeassistant.components.unifi.services import ( SERVICE_RECONNECT_CLIENT, SERVICE_REMOVE_CLIENTS, SUPPORTED_SERVICES, ) -from homeassistant.const import ATTR_DEVICE_ID +from homeassistant.const import ATTR_DEVICE_ID, CONF_HOST from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr @@ -66,11 +67,11 @@ async def test_reconnect_client( config_entry = await setup_unifi_integration( hass, aioclient_mock, clients_response=clients ) - hub = hass.data[UNIFI_DOMAIN][config_entry.entry_id] aioclient_mock.clear_requests() aioclient_mock.post( - f"https://{hub.host}:1234/api/s/{hub.site}/cmd/stamgr", + f"https://{config_entry.data[CONF_HOST]}:1234" + f"/api/s/{config_entry.data[CONF_SITE_ID]}/cmd/stamgr", ) device_entry = device_registry.async_get_or_create( @@ -148,7 +149,8 @@ async def test_reconnect_client_hub_unavailable( aioclient_mock.clear_requests() aioclient_mock.post( - f"https://{hub.host}:1234/api/s/{hub.site}/cmd/stamgr", + f"https://{config_entry.data[CONF_HOST]}:1234" + f"/api/s/{config_entry.data[CONF_SITE_ID]}/cmd/stamgr", ) device_entry = device_registry.async_get_or_create( @@ -261,11 +263,11 @@ async def test_remove_clients( config_entry = await setup_unifi_integration( hass, aioclient_mock, clients_all_response=clients ) - hub = hass.data[UNIFI_DOMAIN][config_entry.entry_id] aioclient_mock.clear_requests() aioclient_mock.post( - f"https://{hub.host}:1234/api/s/{hub.site}/cmd/stamgr", + f"https://{config_entry.data[CONF_HOST]}:1234" + f"/api/s/{config_entry.data[CONF_SITE_ID]}/cmd/stamgr", ) await hass.services.async_call(UNIFI_DOMAIN, SERVICE_REMOVE_CLIENTS, blocking=True) diff --git a/tests/components/unifi/test_switch.py b/tests/components/unifi/test_switch.py index 35b2327bcdc..a6b787045bd 100644 --- a/tests/components/unifi/test_switch.py +++ b/tests/components/unifi/test_switch.py @@ -1,4 +1,5 @@ """UniFi Network switch platform tests.""" + from copy import deepcopy from datetime import timedelta @@ -15,6 +16,7 @@ from homeassistant.components.switch import ( from homeassistant.components.unifi.const import ( CONF_BLOCK_CLIENT, CONF_DPI_RESTRICTIONS, + CONF_SITE_ID, CONF_TRACK_CLIENTS, CONF_TRACK_DEVICES, DOMAIN as UNIFI_DOMAIN, @@ -23,6 +25,7 @@ from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, + CONF_HOST, STATE_OFF, STATE_ON, STATE_UNAVAILABLE, @@ -829,7 +832,6 @@ async def test_switches( dpigroup_response=DPI_GROUPS, dpiapp_response=DPI_APPS, ) - hub = hass.data[UNIFI_DOMAIN][config_entry.entry_id] assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 3 @@ -857,7 +859,8 @@ async def test_switches( # Block and unblock client aioclient_mock.clear_requests() aioclient_mock.post( - f"https://{hub.host}:1234/api/s/{hub.site}/cmd/stamgr", + f"https://{config_entry.data[CONF_HOST]}:1234" + f"/api/s/{config_entry.data[CONF_SITE_ID]}/cmd/stamgr", ) await hass.services.async_call( @@ -881,7 +884,8 @@ async def test_switches( # Enable and disable DPI aioclient_mock.clear_requests() aioclient_mock.put( - f"https://{hub.host}:1234/api/s/{hub.site}/rest/dpiapp/5f976f62e3c58f018ec7e17d", + f"https://{config_entry.data[CONF_HOST]}:1234" + f"/api/s/{config_entry.data[CONF_SITE_ID]}/rest/dpiapp/{DPI_APPS[0]['_id']}", ) await hass.services.async_call( @@ -951,7 +955,6 @@ async def test_block_switches( clients_response=[UNBLOCKED], clients_all_response=[BLOCKED], ) - hub = hass.data[UNIFI_DOMAIN][config_entry.entry_id] assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 2 @@ -981,7 +984,8 @@ async def test_block_switches( aioclient_mock.clear_requests() aioclient_mock.post( - f"https://{hub.host}:1234/api/s/{hub.site}/cmd/stamgr", + f"https://{config_entry.data[CONF_HOST]}:1234" + f"/api/s/{config_entry.data[CONF_SITE_ID]}/cmd/stamgr", ) await hass.services.async_call( @@ -1142,7 +1146,6 @@ async def test_outlet_switches( config_entry = await setup_unifi_integration( hass, aioclient_mock, devices_response=[test_data] ) - hub = hass.data[UNIFI_DOMAIN][config_entry.entry_id] assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == expected_switches # Validate state object switch_1 = hass.states.get(f"switch.{entity_id}") @@ -1161,7 +1164,8 @@ async def test_outlet_switches( device_id = test_data["device_id"] aioclient_mock.clear_requests() aioclient_mock.put( - f"https://{hub.host}:1234/api/s/{hub.site}/rest/device/{device_id}", + f"https://{config_entry.data[CONF_HOST]}:1234" + f"/api/s/{config_entry.data[CONF_SITE_ID]}/rest/device/{device_id}", ) await hass.services.async_call( @@ -1333,7 +1337,6 @@ async def test_poe_port_switches( config_entry = await setup_unifi_integration( hass, aioclient_mock, devices_response=[DEVICE_1] ) - hub = hass.data[UNIFI_DOMAIN][config_entry.entry_id] assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 0 @@ -1372,7 +1375,8 @@ async def test_poe_port_switches( # Turn off PoE aioclient_mock.clear_requests() aioclient_mock.put( - f"https://{hub.host}:1234/api/s/{hub.site}/rest/device/mock-id", + f"https://{config_entry.data[CONF_HOST]}:1234" + f"/api/s/{config_entry.data[CONF_SITE_ID]}/rest/device/mock-id", ) await hass.services.async_call( @@ -1445,7 +1449,6 @@ async def test_wlan_switches( config_entry = await setup_unifi_integration( hass, aioclient_mock, wlans_response=[WLAN] ) - hub = hass.data[UNIFI_DOMAIN][config_entry.entry_id] assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 1 @@ -1469,7 +1472,8 @@ async def test_wlan_switches( # Disable WLAN aioclient_mock.clear_requests() aioclient_mock.put( - f"https://{hub.host}:1234/api/s/{hub.site}" + f"/rest/wlanconf/{WLAN['_id']}", + f"https://{config_entry.data[CONF_HOST]}:1234" + f"/api/s/{config_entry.data[CONF_SITE_ID]}/rest/wlanconf/{WLAN['_id']}", ) await hass.services.async_call( @@ -1525,7 +1529,6 @@ async def test_port_forwarding_switches( config_entry = await setup_unifi_integration( hass, aioclient_mock, port_forward_response=[_data.copy()] ) - hub = hass.data[UNIFI_DOMAIN][config_entry.entry_id] assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 1 @@ -1549,8 +1552,8 @@ async def test_port_forwarding_switches( # Disable port forward aioclient_mock.clear_requests() aioclient_mock.put( - f"https://{hub.host}:1234/api/s/{hub.site}" - + f"/rest/portforward/{data['_id']}", + f"https://{config_entry.data[CONF_HOST]}:1234" + f"/api/s/{config_entry.data[CONF_SITE_ID]}/rest/portforward/{data['_id']}", ) await hass.services.async_call( diff --git a/tests/components/unifi/test_update.py b/tests/components/unifi/test_update.py index a9440e20339..4094c544431 100644 --- a/tests/components/unifi/test_update.py +++ b/tests/components/unifi/test_update.py @@ -1,4 +1,5 @@ """The tests for the UniFi Network update platform.""" + from copy import deepcopy from aiounifi.models.message import MessageKey @@ -162,7 +163,10 @@ async def test_install( 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" + url = ( + f"https://{config_entry.data[CONF_HOST]}:1234" + f"/api/s/{config_entry.data[CONF_SITE_ID]}/cmd/devmgr" + ) aioclient_mock.clear_requests() aioclient_mock.post(url) diff --git a/tests/components/unifiprotect/conftest.py b/tests/components/unifiprotect/conftest.py index 37e7dceecf5..5b3f9653d75 100644 --- a/tests/components/unifiprotect/conftest.py +++ b/tests/components/unifiprotect/conftest.py @@ -132,9 +132,12 @@ def mock_entry( ): """Mock ProtectApiClient for testing.""" - with _patch_discovery(no_device=True), patch( - "homeassistant.components.unifiprotect.utils.ProtectApiClient" - ) as mock_api: + with ( + _patch_discovery(no_device=True), + patch( + "homeassistant.components.unifiprotect.utils.ProtectApiClient" + ) as mock_api, + ): ufp_config_entry.add_to_hass(hass) mock_api.return_value = ufp_client diff --git a/tests/components/unifiprotect/fixtures/sample_nvr.json b/tests/components/unifiprotect/fixtures/sample_nvr.json index 8777e3ce945..13e93a8c2e7 100644 --- a/tests/components/unifiprotect/fixtures/sample_nvr.json +++ b/tests/components/unifiprotect/fixtures/sample_nvr.json @@ -5,7 +5,7 @@ "canAutoUpdate": true, "isStatsGatheringEnabled": true, "timezone": "America/New_York", - "version": "1.21.0-beta.2", + "version": "2.2.6", "ucoreVersion": "2.3.26", "firmwareVersion": "2.3.10", "uiVersion": null, @@ -40,7 +40,7 @@ "enableStatsReporting": false, "isSshEnabled": false, "errorCode": null, - "releaseChannel": "beta", + "releaseChannel": "release", "ssoChannel": null, "hosts": ["192.168.216.198"], "enableBridgeAutoAdoption": true, diff --git a/tests/components/unifiprotect/test_camera.py b/tests/components/unifiprotect/test_camera.py index 7b777d711cf..d374f61c2b0 100644 --- a/tests/components/unifiprotect/test_camera.py +++ b/tests/components/unifiprotect/test_camera.py @@ -21,6 +21,7 @@ from homeassistant.components.unifiprotect.const import ( DEFAULT_ATTRIBUTION, DEFAULT_SCAN_INTERVAL, ) +from homeassistant.components.unifiprotect.utils import get_camera_base_name from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_ENTITY_ID, @@ -51,7 +52,8 @@ def validate_default_camera_entity( channel = camera_obj.channels[channel_id] - entity_name = f"{camera_obj.name} {channel.name}" + camera_name = get_camera_base_name(channel) + entity_name = f"{camera_obj.name} {camera_name}" unique_id = f"{camera_obj.mac}_{channel.id}" entity_id = f"camera.{entity_name.replace(' ', '_').lower()}" @@ -73,7 +75,7 @@ def validate_rtsps_camera_entity( channel = camera_obj.channels[channel_id] - entity_name = f"{camera_obj.name} {channel.name}" + entity_name = f"{camera_obj.name} {channel.name} Resolution Channel" unique_id = f"{camera_obj.mac}_{channel.id}" entity_id = f"camera.{entity_name.replace(' ', '_').lower()}" @@ -95,9 +97,9 @@ def validate_rtsp_camera_entity( channel = camera_obj.channels[channel_id] - entity_name = f"{camera_obj.name} {channel.name} Insecure" + entity_name = f"{camera_obj.name} {channel.name} Resolution Channel (Insecure)" unique_id = f"{camera_obj.mac}_{channel.id}_insecure" - entity_id = f"camera.{entity_name.replace(' ', '_').lower()}" + entity_id = f"camera.{entity_name.replace(' ', '_').replace('(', '').replace(')', '').lower()}" entity_registry = er.async_get(hass) entity = entity_registry.async_get(entity_id) @@ -314,7 +316,7 @@ async def test_camera_image( ufp.api.get_camera_snapshot = AsyncMock() - await async_get_image(hass, "camera.test_camera_high") + await async_get_image(hass, "camera.test_camera_high_resolution_channel") ufp.api.get_camera_snapshot.assert_called_once() @@ -339,7 +341,7 @@ async def test_camera_generic_update( await init_entry(hass, ufp, [camera]) assert_entity_counts(hass, Platform.CAMERA, 2, 1) - entity_id = "camera.test_camera_high" + entity_id = "camera.test_camera_high_resolution_channel" assert await async_setup_component(hass, "homeassistant", {}) @@ -365,7 +367,7 @@ async def test_camera_interval_update( await init_entry(hass, ufp, [camera]) assert_entity_counts(hass, Platform.CAMERA, 2, 1) - entity_id = "camera.test_camera_high" + entity_id = "camera.test_camera_high_resolution_channel" state = hass.states.get(entity_id) assert state and state.state == "idle" @@ -388,7 +390,7 @@ async def test_camera_bad_interval_update( await init_entry(hass, ufp, [camera]) assert_entity_counts(hass, Platform.CAMERA, 2, 1) - entity_id = "camera.test_camera_high" + entity_id = "camera.test_camera_high_resolution_channel" state = hass.states.get(entity_id) assert state and state.state == "idle" @@ -415,7 +417,7 @@ async def test_camera_ws_update( await init_entry(hass, ufp, [camera]) assert_entity_counts(hass, Platform.CAMERA, 2, 1) - entity_id = "camera.test_camera_high" + entity_id = "camera.test_camera_high_resolution_channel" state = hass.states.get(entity_id) assert state and state.state == "idle" @@ -450,7 +452,7 @@ async def test_camera_ws_update_offline( await init_entry(hass, ufp, [camera]) assert_entity_counts(hass, Platform.CAMERA, 2, 1) - entity_id = "camera.test_camera_high" + entity_id = "camera.test_camera_high_resolution_channel" state = hass.states.get(entity_id) assert state and state.state == "idle" @@ -492,7 +494,7 @@ async def test_camera_enable_motion( await init_entry(hass, ufp, [camera]) assert_entity_counts(hass, Platform.CAMERA, 2, 1) - entity_id = "camera.test_camera_high" + entity_id = "camera.test_camera_high_resolution_channel" camera.__fields__["set_motion_detection"] = Mock(final=False) camera.set_motion_detection = AsyncMock() @@ -514,7 +516,7 @@ async def test_camera_disable_motion( await init_entry(hass, ufp, [camera]) assert_entity_counts(hass, Platform.CAMERA, 2, 1) - entity_id = "camera.test_camera_high" + entity_id = "camera.test_camera_high_resolution_channel" camera.__fields__["set_motion_detection"] = Mock(final=False) camera.set_motion_detection = AsyncMock() diff --git a/tests/components/unifiprotect/test_config_flow.py b/tests/components/unifiprotect/test_config_flow.py index 04eee1b8319..7c9f584af15 100644 --- a/tests/components/unifiprotect/test_config_flow.py +++ b/tests/components/unifiprotect/test_config_flow.py @@ -1,4 +1,5 @@ """Test the UniFi Protect config flow.""" + from __future__ import annotations from dataclasses import asdict @@ -66,16 +67,20 @@ async def test_form(hass: HomeAssistant, bootstrap: Bootstrap, nvr: NVR) -> None assert not result["errors"] bootstrap.nvr = nvr - with patch( - "homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_bootstrap", - return_value=bootstrap, - ), patch( - "homeassistant.components.unifiprotect.async_setup_entry", - return_value=True, - ) as mock_setup_entry, patch( - "homeassistant.components.unifiprotect.async_setup", - return_value=True, - ) as mock_setup: + with ( + patch( + "homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_bootstrap", + return_value=bootstrap, + ), + patch( + "homeassistant.components.unifiprotect.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + patch( + "homeassistant.components.unifiprotect.async_setup", + return_value=True, + ) as mock_setup, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -250,13 +255,16 @@ async def test_form_reauth_auth( assert result2["step_id"] == "reauth_confirm" bootstrap.nvr = nvr - with patch( - "homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_bootstrap", - return_value=bootstrap, - ), patch( - "homeassistant.components.unifiprotect.async_setup", - return_value=True, - ) as mock_setup: + with ( + patch( + "homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_bootstrap", + return_value=bootstrap, + ), + patch( + "homeassistant.components.unifiprotect.async_setup", + return_value=True, + ) as mock_setup, + ): result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], { @@ -289,9 +297,12 @@ async def test_form_options(hass: HomeAssistant, ufp_client: ProtectApiClient) - ) mock_config.add_to_hass(hass) - with _patch_discovery(), patch( - "homeassistant.components.unifiprotect.utils.ProtectApiClient" - ) as mock_api: + with ( + _patch_discovery(), + patch( + "homeassistant.components.unifiprotect.utils.ProtectApiClient" + ) as mock_api, + ): mock_api.return_value = ufp_client await hass.config_entries.async_setup(mock_config.entry_id) @@ -371,16 +382,20 @@ async def test_discovered_by_unifi_discovery_direct_connect( assert not result["errors"] bootstrap.nvr = nvr - with patch( - "homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_bootstrap", - return_value=bootstrap, - ), patch( - "homeassistant.components.unifiprotect.async_setup_entry", - return_value=True, - ) as mock_setup_entry, patch( - "homeassistant.components.unifiprotect.async_setup", - return_value=True, - ) as mock_setup: + with ( + patch( + "homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_bootstrap", + return_value=bootstrap, + ), + patch( + "homeassistant.components.unifiprotect.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + patch( + "homeassistant.components.unifiprotect.async_setup", + return_value=True, + ) as mock_setup, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -455,9 +470,12 @@ async def test_discovered_by_unifi_discovery_direct_connect_updated_but_not_usin ) mock_config.add_to_hass(hass) - with _patch_discovery(), patch( - "homeassistant.components.unifiprotect.config_flow.async_console_is_alive", - return_value=False, + with ( + _patch_discovery(), + patch( + "homeassistant.components.unifiprotect.config_flow.async_console_is_alive", + return_value=False, + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -490,9 +508,12 @@ async def test_discovered_by_unifi_discovery_does_not_update_ip_when_console_is_ ) mock_config.add_to_hass(hass) - with _patch_discovery(), patch( - "homeassistant.components.unifiprotect.config_flow.async_console_is_alive", - return_value=True, + with ( + _patch_discovery(), + patch( + "homeassistant.components.unifiprotect.config_flow.async_console_is_alive", + return_value=True, + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -561,16 +582,20 @@ async def test_discovered_by_unifi_discovery( assert not result["errors"] bootstrap.nvr = nvr - with patch( - "homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_bootstrap", - side_effect=[NotAuthorized, bootstrap], - ), patch( - "homeassistant.components.unifiprotect.async_setup_entry", - return_value=True, - ) as mock_setup_entry, patch( - "homeassistant.components.unifiprotect.async_setup", - return_value=True, - ) as mock_setup: + with ( + patch( + "homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_bootstrap", + side_effect=[NotAuthorized, bootstrap], + ), + patch( + "homeassistant.components.unifiprotect.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + patch( + "homeassistant.components.unifiprotect.async_setup", + return_value=True, + ) as mock_setup, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -618,16 +643,20 @@ async def test_discovered_by_unifi_discovery_partial( assert not result["errors"] bootstrap.nvr = nvr - with patch( - "homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_bootstrap", - return_value=bootstrap, - ), patch( - "homeassistant.components.unifiprotect.async_setup_entry", - return_value=True, - ) as mock_setup_entry, patch( - "homeassistant.components.unifiprotect.async_setup", - return_value=True, - ) as mock_setup: + with ( + patch( + "homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_bootstrap", + return_value=bootstrap, + ), + patch( + "homeassistant.components.unifiprotect.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + patch( + "homeassistant.components.unifiprotect.async_setup", + return_value=True, + ) as mock_setup, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -733,10 +762,13 @@ async def test_discovered_by_unifi_discovery_direct_connect_on_different_interfa other_ip_dict["source_ip"] = "127.0.0.1" other_ip_dict["direct_connect_domain"] = "nomatchsameip.ui.direct" - with _patch_discovery(), patch.object( - hass.loop, - "getaddrinfo", - return_value=[(socket.AF_INET, None, None, None, ("127.0.0.1", 443))], + with ( + _patch_discovery(), + patch.object( + hass.loop, + "getaddrinfo", + return_value=[(socket.AF_INET, None, None, None, ("127.0.0.1", 443))], + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -771,8 +803,9 @@ async def test_discovered_by_unifi_discovery_direct_connect_on_different_interfa other_ip_dict["source_ip"] = "127.0.0.2" other_ip_dict["direct_connect_domain"] = "nomatchsameip.ui.direct" - with _patch_discovery(), patch.object( - hass.loop, "getaddrinfo", side_effect=OSError + with ( + _patch_discovery(), + patch.object(hass.loop, "getaddrinfo", side_effect=OSError), ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -792,16 +825,20 @@ async def test_discovered_by_unifi_discovery_direct_connect_on_different_interfa assert not result["errors"] bootstrap.nvr = nvr - with patch( - "homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_bootstrap", - return_value=bootstrap, - ), patch( - "homeassistant.components.unifiprotect.async_setup_entry", - return_value=True, - ) as mock_setup_entry, patch( - "homeassistant.components.unifiprotect.async_setup", - return_value=True, - ) as mock_setup: + with ( + patch( + "homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_bootstrap", + return_value=bootstrap, + ), + patch( + "homeassistant.components.unifiprotect.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + patch( + "homeassistant.components.unifiprotect.async_setup", + return_value=True, + ) as mock_setup, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { diff --git a/tests/components/unifiprotect/test_diagnostics.py b/tests/components/unifiprotect/test_diagnostics.py index 62d3f33ce0b..b13c069b37c 100644 --- a/tests/components/unifiprotect/test_diagnostics.py +++ b/tests/components/unifiprotect/test_diagnostics.py @@ -1,4 +1,5 @@ """Test UniFi Protect diagnostics.""" + from pyunifiprotect.data import NVR, Light from homeassistant.components.unifiprotect.const import CONF_ALLOW_EA diff --git a/tests/components/unifiprotect/test_init.py b/tests/components/unifiprotect/test_init.py index 35477f1e56d..f123abb9861 100644 --- a/tests/components/unifiprotect/test_init.py +++ b/tests/components/unifiprotect/test_init.py @@ -70,7 +70,6 @@ async def test_setup_multiple( nvr = bootstrap.nvr nvr._api = ufp.api nvr.mac = "A1E00C826983" - nvr.id ufp.api.get_nvr = AsyncMock(return_value=nvr) with patch( @@ -240,9 +239,12 @@ async def test_setup_starts_discovery( hass: HomeAssistant, ufp_config_entry: ConfigEntry, ufp_client: ProtectApiClient ) -> None: """Test setting up will start discovery.""" - with _patch_discovery(), patch( - "homeassistant.components.unifiprotect.utils.ProtectApiClient" - ) as mock_api: + with ( + _patch_discovery(), + patch( + "homeassistant.components.unifiprotect.utils.ProtectApiClient" + ) as mock_api, + ): ufp_config_entry.add_to_hass(hass) mock_api.return_value = ufp_client ufp = MockUFPFixture(ufp_config_entry, ufp_client) diff --git a/tests/components/unifiprotect/test_media_source.py b/tests/components/unifiprotect/test_media_source.py index c79a46daafd..e767909d47e 100644 --- a/tests/components/unifiprotect/test_media_source.py +++ b/tests/components/unifiprotect/test_media_source.py @@ -362,7 +362,8 @@ async def test_browse_media_camera( entity_registry = er.async_get(hass) entity_registry.async_update_entity( - "camera.test_camera_high", disabled_by=er.RegistryEntryDisabler("user") + "camera.test_camera_high_resolution_channel", + disabled_by=er.RegistryEntryDisabler("user"), ) await hass.async_block_till_done() diff --git a/tests/components/unifiprotect/test_migrate.py b/tests/components/unifiprotect/test_migrate.py index 475f6170550..7e736c39e6a 100644 --- a/tests/components/unifiprotect/test_migrate.py +++ b/tests/components/unifiprotect/test_migrate.py @@ -2,236 +2,225 @@ from __future__ import annotations -from unittest.mock import AsyncMock +from unittest.mock import patch -from pyunifiprotect.data import Light -from pyunifiprotect.exceptions import NvrError +from pyunifiprotect.data import Camera +from homeassistant.components.automation import DOMAIN as AUTOMATION_DOMAIN +from homeassistant.components.repairs.issue_handler import ( + async_process_repairs_platforms, +) +from homeassistant.components.script import DOMAIN as SCRIPT_DOMAIN from homeassistant.components.unifiprotect.const import DOMAIN -from homeassistant.config_entries import ConfigEntryState -from homeassistant.const import Platform +from homeassistant.const import SERVICE_RELOAD, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er +from homeassistant.setup import async_setup_component -from .utils import ( - MockUFPFixture, - generate_random_ids, - init_entry, - regenerate_device_ids, -) +from .utils import MockUFPFixture, init_entry + +from tests.typing import WebSocketGenerator -async def test_migrate_reboot_button( - hass: HomeAssistant, ufp: MockUFPFixture, light: Light -) -> None: - """Test migrating unique ID of reboot button.""" +async def test_deprecated_entity( + hass: HomeAssistant, ufp: MockUFPFixture, hass_ws_client, doorbell: Camera +): + """Test Deprecate entity repair does not exist by default (new installs).""" - light1 = light.copy() - light1.name = "Test Light 1" - regenerate_device_ids(light1) + await init_entry(hass, ufp, [doorbell]) - light2 = light.copy() - light2.name = "Test Light 2" - regenerate_device_ids(light2) + await async_process_repairs_platforms(hass) + ws_client = await hass_ws_client(hass) + + await ws_client.send_json({"id": 1, "type": "repairs/list_issues"}) + msg = await ws_client.receive_json() + + assert msg["success"] + issue = None + for i in msg["result"]["issues"]: + if i["issue_id"] == "deprecate_hdr_switch": + issue = i + assert issue is None + + +async def test_deprecated_entity_no_automations( + hass: HomeAssistant, ufp: MockUFPFixture, hass_ws_client, doorbell: Camera +): + """Test Deprecate entity repair exists for existing installs.""" registry = er.async_get(hass) registry.async_get_or_create( - Platform.BUTTON, DOMAIN, light1.id, config_entry=ufp.entry - ) - registry.async_get_or_create( - Platform.BUTTON, + Platform.SWITCH, DOMAIN, - f"{light2.mac}_reboot", + f"{doorbell.mac}_hdr_mode", config_entry=ufp.entry, ) - ufp.api.get_bootstrap = AsyncMock(return_value=ufp.api.bootstrap) - await init_entry(hass, ufp, [light1, light2], regenerate_ids=False) + await init_entry(hass, ufp, [doorbell]) - assert ufp.entry.state == ConfigEntryState.LOADED - assert ufp.api.update.called - assert ufp.entry.unique_id == ufp.api.bootstrap.nvr.mac + await async_process_repairs_platforms(hass) + ws_client = await hass_ws_client(hass) - buttons = [] - for entity in er.async_entries_for_config_entry(registry, ufp.entry.entry_id): - if entity.domain == Platform.BUTTON.value: - buttons.append(entity) - assert len(buttons) == 4 + await ws_client.send_json({"id": 1, "type": "repairs/list_issues"}) + msg = await ws_client.receive_json() - assert registry.async_get(f"{Platform.BUTTON}.test_light_1_reboot_device") is None - assert registry.async_get(f"{Platform.BUTTON}.test_light_1_reboot_device_2") is None - light = registry.async_get(f"{Platform.BUTTON}.unifiprotect_{light1.id.lower()}") - assert light is not None - assert light.unique_id == f"{light1.mac}_reboot" + assert msg["success"] + issue = None + for i in msg["result"]["issues"]: + if i["issue_id"] == "deprecate_hdr_switch": + issue = i + assert issue is None - assert registry.async_get(f"{Platform.BUTTON}.test_light_2_reboot_device") is None - assert registry.async_get(f"{Platform.BUTTON}.test_light_2_reboot_device_2") is None - light = registry.async_get( - f"{Platform.BUTTON}.unifiprotect_{light2.mac.lower()}_reboot" + +async def _load_automation(hass: HomeAssistant, entity_id: str): + assert await async_setup_component( + hass, + AUTOMATION_DOMAIN, + { + AUTOMATION_DOMAIN: [ + { + "alias": "test1", + "trigger": [ + {"platform": "state", "entity_id": entity_id}, + { + "platform": "event", + "event_type": "state_changed", + "event_data": {"entity_id": entity_id}, + }, + ], + "condition": { + "condition": "state", + "entity_id": entity_id, + "state": "on", + }, + "action": [ + { + "service": "test.script", + "data": {"entity_id": entity_id}, + }, + ], + }, + ] + }, ) - assert light is not None - assert light.unique_id == f"{light2.mac}_reboot" -async def test_migrate_nvr_mac( - hass: HomeAssistant, ufp: MockUFPFixture, light: Light +async def test_deprecate_entity_automation( + hass: HomeAssistant, + ufp: MockUFPFixture, + hass_ws_client: WebSocketGenerator, + doorbell: Camera, ) -> None: - """Test migrating unique ID of NVR to use MAC address.""" - - light1 = light.copy() - light1.name = "Test Light 1" - regenerate_device_ids(light1) - - light2 = light.copy() - light2.name = "Test Light 2" - regenerate_device_ids(light2) - - nvr = ufp.api.bootstrap.nvr - regenerate_device_ids(nvr) - registry = er.async_get(hass) - registry.async_get_or_create( - Platform.SENSOR, - DOMAIN, - f"{nvr.id}_storage_utilization", - config_entry=ufp.entry, - ) - - ufp.api.get_bootstrap = AsyncMock(return_value=ufp.api.bootstrap) - await init_entry(hass, ufp, [light1, light2], regenerate_ids=False) - - assert ufp.entry.state == ConfigEntryState.LOADED - assert ufp.api.update.called - assert ufp.entry.unique_id == ufp.api.bootstrap.nvr.mac - - assert registry.async_get(f"{Platform.SENSOR}.{DOMAIN}_storage_utilization") is None - assert ( - registry.async_get(f"{Platform.SENSOR}.{DOMAIN}_storage_utilization_2") is None - ) - sensor = registry.async_get( - f"{Platform.SENSOR}.{DOMAIN}_{nvr.id}_storage_utilization" - ) - assert sensor is not None - assert sensor.unique_id == f"{nvr.mac}_storage_utilization" - - -async def test_migrate_reboot_button_no_device( - hass: HomeAssistant, ufp: MockUFPFixture, light: Light -) -> None: - """Test migrating unique ID of reboot button if UniFi Protect device ID changed.""" - - light2_id, _ = generate_random_ids() + """Test Deprecate entity repair exists for existing installs.""" registry = er.async_get(hass) - registry.async_get_or_create( - Platform.BUTTON, DOMAIN, light2_id, config_entry=ufp.entry + entry = registry.async_get_or_create( + Platform.SWITCH, + DOMAIN, + f"{doorbell.mac}_hdr_mode", + config_entry=ufp.entry, + ) + await _load_automation(hass, entry.entity_id) + await init_entry(hass, ufp, [doorbell]) + + await async_process_repairs_platforms(hass) + ws_client = await hass_ws_client(hass) + + await ws_client.send_json({"id": 1, "type": "repairs/list_issues"}) + msg = await ws_client.receive_json() + + assert msg["success"] + issue = None + for i in msg["result"]["issues"]: + if i["issue_id"] == "deprecate_hdr_switch": + issue = i + assert issue is not None + + with patch( + "homeassistant.config.load_yaml_config_file", + autospec=True, + return_value={AUTOMATION_DOMAIN: []}, + ): + await hass.services.async_call(AUTOMATION_DOMAIN, SERVICE_RELOAD, blocking=True) + + await hass.config_entries.async_reload(ufp.entry.entry_id) + await hass.async_block_till_done() + + await ws_client.send_json({"id": 2, "type": "repairs/list_issues"}) + msg = await ws_client.receive_json() + + assert msg["success"] + issue = None + for i in msg["result"]["issues"]: + if i["issue_id"] == "deprecate_hdr_switch": + issue = i + assert issue is None + + +async def _load_script(hass: HomeAssistant, entity_id: str): + assert await async_setup_component( + hass, + SCRIPT_DOMAIN, + { + SCRIPT_DOMAIN: { + "test": { + "sequence": { + "service": "test.script", + "data": {"entity_id": entity_id}, + } + } + }, + }, ) - ufp.api.get_bootstrap = AsyncMock(return_value=ufp.api.bootstrap) - await init_entry(hass, ufp, [light], regenerate_ids=False) - assert ufp.entry.state == ConfigEntryState.LOADED - assert ufp.api.update.called - assert ufp.entry.unique_id == ufp.api.bootstrap.nvr.mac - - buttons = [] - for entity in er.async_entries_for_config_entry(registry, ufp.entry.entry_id): - if entity.domain == Platform.BUTTON.value: - buttons.append(entity) - assert len(buttons) == 3 - - entity = registry.async_get(f"{Platform.BUTTON}.unifiprotect_{light2_id.lower()}") - assert entity is not None - assert entity.unique_id == light2_id - - -async def test_migrate_reboot_button_fail( - hass: HomeAssistant, ufp: MockUFPFixture, light: Light +async def test_deprecate_entity_script( + hass: HomeAssistant, + ufp: MockUFPFixture, + hass_ws_client: WebSocketGenerator, + doorbell: Camera, ) -> None: - """Test migrating unique ID of reboot button.""" + """Test Deprecate entity repair exists for existing installs.""" registry = er.async_get(hass) - registry.async_get_or_create( - Platform.BUTTON, + entry = registry.async_get_or_create( + Platform.SWITCH, DOMAIN, - light.id, + f"{doorbell.mac}_hdr_mode", config_entry=ufp.entry, - suggested_object_id=light.display_name, - ) - registry.async_get_or_create( - Platform.BUTTON, - DOMAIN, - f"{light.id}_reboot", - config_entry=ufp.entry, - suggested_object_id=light.display_name, ) + await _load_script(hass, entry.entity_id) + await init_entry(hass, ufp, [doorbell]) - ufp.api.get_bootstrap = AsyncMock(return_value=ufp.api.bootstrap) - await init_entry(hass, ufp, [light], regenerate_ids=False) + await async_process_repairs_platforms(hass) + ws_client = await hass_ws_client(hass) - assert ufp.entry.state == ConfigEntryState.LOADED - assert ufp.api.update.called - assert ufp.entry.unique_id == ufp.api.bootstrap.nvr.mac + await ws_client.send_json({"id": 1, "type": "repairs/list_issues"}) + msg = await ws_client.receive_json() - entity = registry.async_get(f"{Platform.BUTTON}.test_light") - assert entity is not None - assert entity.unique_id == f"{light.mac}" + assert msg["success"] + issue = None + for i in msg["result"]["issues"]: + if i["issue_id"] == "deprecate_hdr_switch": + issue = i + assert issue is not None + with patch( + "homeassistant.config.load_yaml_config_file", + autospec=True, + return_value={SCRIPT_DOMAIN: {}}, + ): + await hass.services.async_call(SCRIPT_DOMAIN, SERVICE_RELOAD, blocking=True) -async def test_migrate_device_mac_button_fail( - hass: HomeAssistant, ufp: MockUFPFixture, light: Light -) -> None: - """Test migrating unique ID to MAC format.""" + await hass.config_entries.async_reload(ufp.entry.entry_id) + await hass.async_block_till_done() - registry = er.async_get(hass) - registry.async_get_or_create( - Platform.BUTTON, - DOMAIN, - f"{light.id}_reboot", - config_entry=ufp.entry, - suggested_object_id=light.display_name, - ) - registry.async_get_or_create( - Platform.BUTTON, - DOMAIN, - f"{light.mac}_reboot", - config_entry=ufp.entry, - suggested_object_id=light.display_name, - ) + await ws_client.send_json({"id": 2, "type": "repairs/list_issues"}) + msg = await ws_client.receive_json() - ufp.api.get_bootstrap = AsyncMock(return_value=ufp.api.bootstrap) - await init_entry(hass, ufp, [light], regenerate_ids=False) - - assert ufp.entry.state == ConfigEntryState.LOADED - assert ufp.api.update.called - assert ufp.entry.unique_id == ufp.api.bootstrap.nvr.mac - - entity = registry.async_get(f"{Platform.BUTTON}.test_light") - assert entity is not None - assert entity.unique_id == f"{light.id}_reboot" - - -async def test_migrate_device_mac_bootstrap_fail( - hass: HomeAssistant, ufp: MockUFPFixture, light: Light -) -> None: - """Test migrating with a network error.""" - - registry = er.async_get(hass) - registry.async_get_or_create( - Platform.BUTTON, - DOMAIN, - f"{light.id}_reboot", - config_entry=ufp.entry, - suggested_object_id=light.name, - ) - registry.async_get_or_create( - Platform.BUTTON, - DOMAIN, - f"{light.mac}_reboot", - config_entry=ufp.entry, - suggested_object_id=light.name, - ) - - ufp.api.get_bootstrap = AsyncMock(side_effect=NvrError) - await init_entry(hass, ufp, [light], regenerate_ids=False) - - assert ufp.entry.state == ConfigEntryState.SETUP_RETRY + assert msg["success"] + issue = None + for i in msg["result"]["issues"]: + if i["issue_id"] == "deprecate_hdr_switch": + issue = i + assert issue is None diff --git a/tests/components/unifiprotect/test_number.py b/tests/components/unifiprotect/test_number.py index 15dec4b0c74..5eeb5308d62 100644 --- a/tests/components/unifiprotect/test_number.py +++ b/tests/components/unifiprotect/test_number.py @@ -6,7 +6,7 @@ from datetime import timedelta from unittest.mock import AsyncMock, Mock import pytest -from pyunifiprotect.data import Camera, Doorlock, Light +from pyunifiprotect.data import Camera, Doorlock, IRLEDMode, Light from homeassistant.components.unifiprotect.const import DEFAULT_ATTRIBUTION from homeassistant.components.unifiprotect.number import ( @@ -35,11 +35,11 @@ async def test_number_sensor_camera_remove( """Test removing and re-adding a camera device.""" await init_entry(hass, ufp, [camera, unadopted_camera]) - assert_entity_counts(hass, Platform.NUMBER, 3, 3) + assert_entity_counts(hass, Platform.NUMBER, 4, 4) await remove_entities(hass, ufp, [camera, unadopted_camera]) assert_entity_counts(hass, Platform.NUMBER, 0, 0) await adopt_devices(hass, ufp, [camera, unadopted_camera]) - assert_entity_counts(hass, Platform.NUMBER, 3, 3) + assert_entity_counts(hass, Platform.NUMBER, 4, 4) async def test_number_sensor_light_remove( @@ -99,8 +99,11 @@ async def test_number_setup_camera_all( camera.feature_flags.has_chime = True camera.chime_duration = timedelta(seconds=1) + camera.feature_flags.has_led_ir = True + camera.isp_settings.icr_custom_value = 1 + camera.isp_settings.ir_led_mode = IRLEDMode.CUSTOM await init_entry(hass, ufp, [camera]) - assert_entity_counts(hass, Platform.NUMBER, 4, 4) + assert_entity_counts(hass, Platform.NUMBER, 5, 5) entity_registry = er.async_get(hass) @@ -128,6 +131,7 @@ async def test_number_setup_camera_none( camera.feature_flags.has_mic = False # has_wdr is an the inverse of has HDR camera.feature_flags.has_hdr = True + camera.feature_flags.has_led_ir = False await init_entry(hass, ufp, [camera]) assert_entity_counts(hass, Platform.NUMBER, 0, 0) @@ -199,7 +203,7 @@ async def test_number_camera_simple( """Tests all simple numbers for cameras.""" await init_entry(hass, ufp, [camera]) - assert_entity_counts(hass, Platform.NUMBER, 3, 3) + assert_entity_counts(hass, Platform.NUMBER, 4, 4) assert description.ufp_set_method is not None diff --git a/tests/components/unifiprotect/test_recorder.py b/tests/components/unifiprotect/test_recorder.py index ab6e3fcb5ae..3e1a8599ea7 100644 --- a/tests/components/unifiprotect/test_recorder.py +++ b/tests/components/unifiprotect/test_recorder.py @@ -1,4 +1,5 @@ """The tests for unifiprotect recorder.""" + from __future__ import annotations from datetime import datetime, timedelta diff --git a/tests/components/unifiprotect/test_repairs.py b/tests/components/unifiprotect/test_repairs.py index 0c939a9791d..f4be3164fd5 100644 --- a/tests/components/unifiprotect/test_repairs.py +++ b/tests/components/unifiprotect/test_repairs.py @@ -1,11 +1,12 @@ """Test repairs for unifiprotect.""" + from __future__ import annotations -from copy import copy +from copy import copy, deepcopy from http import HTTPStatus -from unittest.mock import Mock +from unittest.mock import AsyncMock, Mock -from pyunifiprotect.data import CloudAccount, Version +from pyunifiprotect.data import Camera, CloudAccount, ModelType, Version from homeassistant.components.repairs.issue_handler import ( async_process_repairs_platforms, @@ -31,6 +32,8 @@ async def test_ea_warning_ignore( ) -> None: """Test EA warning is created if using prerelease version of Protect.""" + ufp.api.bootstrap.nvr.release_channel = "beta" + ufp.api.bootstrap.nvr.version = Version("1.21.0-beta.2") version = ufp.api.bootstrap.nvr.version assert version.is_prerelease await init_entry(hass, ufp, []) @@ -91,6 +94,8 @@ async def test_ea_warning_fix( ) -> None: """Test EA warning is created if using prerelease version of Protect.""" + ufp.api.bootstrap.nvr.release_channel = "beta" + ufp.api.bootstrap.nvr.version = Version("1.21.0-beta.2") version = ufp.api.bootstrap.nvr.version assert version.is_prerelease await init_entry(hass, ufp, []) @@ -124,8 +129,8 @@ async def test_ea_warning_fix( assert data["step_id"] == "start" new_nvr = copy(ufp.api.bootstrap.nvr) - new_nvr.version = Version("2.2.6") new_nvr.release_channel = "release" + new_nvr.version = Version("2.2.6") mock_msg = Mock() mock_msg.changed_data = {"version": "2.2.6", "releaseChannel": "release"} mock_msg.new_obj = new_nvr @@ -187,3 +192,168 @@ async def test_cloud_user_fix( assert data["type"] == "create_entry" await hass.async_block_till_done() assert any(ufp.entry.async_get_active_flows(hass, {SOURCE_REAUTH})) + + +async def test_rtsp_read_only_ignore( + hass: HomeAssistant, + ufp: MockUFPFixture, + doorbell: Camera, + hass_client: ClientSessionGenerator, + hass_ws_client: WebSocketGenerator, +) -> None: + """Test RTSP disabled warning if camera is read-only and it is ignored.""" + + for channel in doorbell.channels: + channel.is_rtsp_enabled = False + for user in ufp.api.bootstrap.users.values(): + user.all_permissions = [] + + ufp.api.get_camera = AsyncMock(return_value=doorbell) + + await init_entry(hass, ufp, [doorbell]) + await async_process_repairs_platforms(hass) + ws_client = await hass_ws_client(hass) + client = await hass_client() + + issue_id = f"rtsp_disabled_{doorbell.id}" + + await ws_client.send_json({"id": 1, "type": "repairs/list_issues"}) + msg = await ws_client.receive_json() + + assert msg["success"] + assert len(msg["result"]["issues"]) > 0 + issue = None + for i in msg["result"]["issues"]: + if i["issue_id"] == issue_id: + issue = i + assert issue is not None + + url = RepairsFlowIndexView.url + resp = await client.post(url, json={"handler": DOMAIN, "issue_id": issue_id}) + assert resp.status == HTTPStatus.OK + data = await resp.json() + + flow_id = data["flow_id"] + assert data["step_id"] == "start" + + url = RepairsFlowResourceView.url.format(flow_id=flow_id) + resp = await client.post(url) + assert resp.status == HTTPStatus.OK + data = await resp.json() + + flow_id = data["flow_id"] + assert data["step_id"] == "confirm" + + url = RepairsFlowResourceView.url.format(flow_id=flow_id) + resp = await client.post(url) + assert resp.status == HTTPStatus.OK + data = await resp.json() + + assert data["type"] == "create_entry" + + +async def test_rtsp_read_only_fix( + hass: HomeAssistant, + ufp: MockUFPFixture, + doorbell: Camera, + hass_client: ClientSessionGenerator, + hass_ws_client: WebSocketGenerator, +) -> None: + """Test RTSP disabled warning if camera is read-only and it is fixed.""" + + for channel in doorbell.channels: + channel.is_rtsp_enabled = False + for user in ufp.api.bootstrap.users.values(): + user.all_permissions = [] + + await init_entry(hass, ufp, [doorbell]) + await async_process_repairs_platforms(hass) + ws_client = await hass_ws_client(hass) + client = await hass_client() + + new_doorbell = deepcopy(doorbell) + new_doorbell.channels[1].is_rtsp_enabled = True + ufp.api.get_camera = AsyncMock(return_value=new_doorbell) + issue_id = f"rtsp_disabled_{doorbell.id}" + + await ws_client.send_json({"id": 1, "type": "repairs/list_issues"}) + msg = await ws_client.receive_json() + + assert msg["success"] + assert len(msg["result"]["issues"]) > 0 + issue = None + for i in msg["result"]["issues"]: + if i["issue_id"] == issue_id: + issue = i + assert issue is not None + + url = RepairsFlowIndexView.url + resp = await client.post(url, json={"handler": DOMAIN, "issue_id": issue_id}) + assert resp.status == HTTPStatus.OK + data = await resp.json() + + flow_id = data["flow_id"] + assert data["step_id"] == "start" + + url = RepairsFlowResourceView.url.format(flow_id=flow_id) + resp = await client.post(url) + assert resp.status == HTTPStatus.OK + data = await resp.json() + + assert data["type"] == "create_entry" + + +async def test_rtsp_writable_fix( + hass: HomeAssistant, + ufp: MockUFPFixture, + doorbell: Camera, + hass_client: ClientSessionGenerator, + hass_ws_client: WebSocketGenerator, +) -> None: + """Test RTSP disabled warning if camera is writable and it is ignored.""" + + for channel in doorbell.channels: + channel.is_rtsp_enabled = False + + await init_entry(hass, ufp, [doorbell]) + await async_process_repairs_platforms(hass) + ws_client = await hass_ws_client(hass) + client = await hass_client() + + new_doorbell = deepcopy(doorbell) + new_doorbell.channels[0].is_rtsp_enabled = True + ufp.api.get_camera = AsyncMock(side_effect=[doorbell, new_doorbell]) + ufp.api.update_device = AsyncMock() + issue_id = f"rtsp_disabled_{doorbell.id}" + + await ws_client.send_json({"id": 1, "type": "repairs/list_issues"}) + msg = await ws_client.receive_json() + + assert msg["success"] + assert len(msg["result"]["issues"]) > 0 + issue = None + for i in msg["result"]["issues"]: + if i["issue_id"] == issue_id: + issue = i + assert issue is not None + + url = RepairsFlowIndexView.url + resp = await client.post(url, json={"handler": DOMAIN, "issue_id": issue_id}) + assert resp.status == HTTPStatus.OK + data = await resp.json() + + flow_id = data["flow_id"] + assert data["step_id"] == "start" + + url = RepairsFlowResourceView.url.format(flow_id=flow_id) + resp = await client.post(url) + assert resp.status == HTTPStatus.OK + data = await resp.json() + + assert data["type"] == "create_entry" + + channels = doorbell.unifi_dict()["channels"] + channels[0]["isRtspEnabled"] = True + ufp.api.update_device.assert_called_with( + ModelType.CAMERA, doorbell.id, {"channels": channels} + ) diff --git a/tests/components/unifiprotect/test_select.py b/tests/components/unifiprotect/test_select.py index 6987e526e34..7c6e449be5e 100644 --- a/tests/components/unifiprotect/test_select.py +++ b/tests/components/unifiprotect/test_select.py @@ -48,11 +48,11 @@ async def test_select_camera_remove( ufp.api.bootstrap.nvr.system_info.ustorage = None await init_entry(hass, ufp, [doorbell, unadopted_camera]) - assert_entity_counts(hass, Platform.SELECT, 4, 4) + assert_entity_counts(hass, Platform.SELECT, 5, 5) await remove_entities(hass, ufp, [doorbell, unadopted_camera]) assert_entity_counts(hass, Platform.SELECT, 0, 0) await adopt_devices(hass, ufp, [doorbell, unadopted_camera]) - assert_entity_counts(hass, Platform.SELECT, 4, 4) + assert_entity_counts(hass, Platform.SELECT, 5, 5) async def test_select_light_remove( @@ -142,10 +142,16 @@ async def test_select_setup_camera_all( """Test select entity setup for camera devices (all features).""" await init_entry(hass, ufp, [doorbell]) - assert_entity_counts(hass, Platform.SELECT, 4, 4) + assert_entity_counts(hass, Platform.SELECT, 5, 5) entity_registry = er.async_get(hass) - expected_values = ("Always", "Auto", "Default Message (Welcome)", "None") + expected_values = ( + "Always", + "Auto", + "Default Message (Welcome)", + "None", + "Always Off", + ) for index, description in enumerate(CAMERA_SELECTS): unique_id, entity_id = ids_from_device_description( @@ -233,7 +239,7 @@ async def test_select_update_doorbell_settings( """Test select entity update (new Doorbell Message).""" await init_entry(hass, ufp, [doorbell]) - assert_entity_counts(hass, Platform.SELECT, 4, 4) + assert_entity_counts(hass, Platform.SELECT, 5, 5) expected_length = len(ufp.api.bootstrap.nvr.doorbell_settings.all_messages) + 1 @@ -279,7 +285,7 @@ async def test_select_update_doorbell_message( """Test select entity update (change doorbell message).""" await init_entry(hass, ufp, [doorbell]) - assert_entity_counts(hass, Platform.SELECT, 4, 4) + assert_entity_counts(hass, Platform.SELECT, 5, 5) _, entity_id = ids_from_device_description( Platform.SELECT, doorbell, CAMERA_SELECTS[2] @@ -372,7 +378,7 @@ async def test_select_set_option_camera_recording( """Test Recording Mode select.""" await init_entry(hass, ufp, [doorbell]) - assert_entity_counts(hass, Platform.SELECT, 4, 4) + assert_entity_counts(hass, Platform.SELECT, 5, 5) _, entity_id = ids_from_device_description( Platform.SELECT, doorbell, CAMERA_SELECTS[0] @@ -397,7 +403,7 @@ async def test_select_set_option_camera_ir( """Test Infrared Mode select.""" await init_entry(hass, ufp, [doorbell]) - assert_entity_counts(hass, Platform.SELECT, 4, 4) + assert_entity_counts(hass, Platform.SELECT, 5, 5) _, entity_id = ids_from_device_description( Platform.SELECT, doorbell, CAMERA_SELECTS[1] @@ -422,7 +428,7 @@ async def test_select_set_option_camera_doorbell_custom( """Test Doorbell Text select (user defined message).""" await init_entry(hass, ufp, [doorbell]) - assert_entity_counts(hass, Platform.SELECT, 4, 4) + assert_entity_counts(hass, Platform.SELECT, 5, 5) _, entity_id = ids_from_device_description( Platform.SELECT, doorbell, CAMERA_SELECTS[2] @@ -449,7 +455,7 @@ async def test_select_set_option_camera_doorbell_unifi( """Test Doorbell Text select (unifi message).""" await init_entry(hass, ufp, [doorbell]) - assert_entity_counts(hass, Platform.SELECT, 4, 4) + assert_entity_counts(hass, Platform.SELECT, 5, 5) _, entity_id = ids_from_device_description( Platform.SELECT, doorbell, CAMERA_SELECTS[2] @@ -491,7 +497,7 @@ async def test_select_set_option_camera_doorbell_default( """Test Doorbell Text select (default message).""" await init_entry(hass, ufp, [doorbell]) - assert_entity_counts(hass, Platform.SELECT, 4, 4) + assert_entity_counts(hass, Platform.SELECT, 5, 5) _, entity_id = ids_from_device_description( Platform.SELECT, doorbell, CAMERA_SELECTS[2] diff --git a/tests/components/unifiprotect/test_sensor.py b/tests/components/unifiprotect/test_sensor.py index 89a153caed2..1e5eca47b9b 100644 --- a/tests/components/unifiprotect/test_sensor.py +++ b/tests/components/unifiprotect/test_sensor.py @@ -1,4 +1,5 @@ """Test the UniFi Protect sensor platform.""" + from __future__ import annotations from datetime import datetime, timedelta @@ -537,9 +538,9 @@ async def test_camera_update_licenseplate( new_camera = camera.copy() new_camera.is_smart_detected = True - new_camera.last_smart_detect_event_ids[ - SmartDetectObjectType.LICENSE_PLATE - ] = event.id + new_camera.last_smart_detect_event_ids[SmartDetectObjectType.LICENSE_PLATE] = ( + event.id + ) mock_msg = Mock() mock_msg.changed_data = {} diff --git a/tests/components/unifiprotect/test_services.py b/tests/components/unifiprotect/test_services.py index fd8226e425a..508a143c522 100644 --- a/tests/components/unifiprotect/test_services.py +++ b/tests/components/unifiprotect/test_services.py @@ -5,17 +5,19 @@ from __future__ import annotations from unittest.mock import AsyncMock, Mock import pytest -from pyunifiprotect.data import Camera, Chime, Light, ModelType +from pyunifiprotect.data import Camera, Chime, Color, Light, ModelType +from pyunifiprotect.data.devices import CameraZone from pyunifiprotect.exceptions import BadRequest from homeassistant.components.unifiprotect.const import ATTR_MESSAGE, DOMAIN from homeassistant.components.unifiprotect.services import ( SERVICE_ADD_DOORBELL_TEXT, SERVICE_REMOVE_DOORBELL_TEXT, + SERVICE_REMOVE_PRIVACY_ZONE, SERVICE_SET_CHIME_PAIRED, SERVICE_SET_DEFAULT_DOORBELL_TEXT, ) -from homeassistant.const import ATTR_DEVICE_ID, ATTR_ENTITY_ID +from homeassistant.const import ATTR_DEVICE_ID, ATTR_ENTITY_ID, ATTR_NAME from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr, entity_registry as er @@ -177,3 +179,55 @@ async def test_set_chime_paired_doorbells( ufp.api.update_device.assert_called_once_with( ModelType.CHIME, chime.id, {"cameraIds": sorted([camera1.id, camera2.id])} ) + + +async def test_remove_privacy_zone_no_zone( + hass: HomeAssistant, + ufp: MockUFPFixture, + doorbell: Camera, +) -> None: + """Test remove_privacy_zone service.""" + + ufp.api.update_device = AsyncMock() + doorbell.privacy_zones = [] + + await init_entry(hass, ufp, [doorbell]) + + registry = er.async_get(hass) + camera_entry = registry.async_get("binary_sensor.test_camera_doorbell") + + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + DOMAIN, + SERVICE_REMOVE_PRIVACY_ZONE, + {ATTR_DEVICE_ID: camera_entry.device_id, ATTR_NAME: "Testing"}, + blocking=True, + ) + ufp.api.update_device.assert_not_called() + + +async def test_remove_privacy_zone( + hass: HomeAssistant, + ufp: MockUFPFixture, + doorbell: Camera, +) -> None: + """Test remove_privacy_zone service.""" + + ufp.api.update_device = AsyncMock() + doorbell.privacy_zones = [ + CameraZone(id=0, name="Testing", color=Color("red"), points=[(0, 0), (1, 1)]) + ] + + await init_entry(hass, ufp, [doorbell]) + + registry = er.async_get(hass) + camera_entry = registry.async_get("binary_sensor.test_camera_doorbell") + + await hass.services.async_call( + DOMAIN, + SERVICE_REMOVE_PRIVACY_ZONE, + {ATTR_DEVICE_ID: camera_entry.device_id, ATTR_NAME: "Testing"}, + blocking=True, + ) + ufp.api.update_device.assert_called() + assert not len(doorbell.privacy_zones) diff --git a/tests/components/unifiprotect/test_switch.py b/tests/components/unifiprotect/test_switch.py index 70a21a324d0..562eec8c5d0 100644 --- a/tests/components/unifiprotect/test_switch.py +++ b/tests/components/unifiprotect/test_switch.py @@ -37,13 +37,17 @@ CAMERA_SWITCHES_BASIC = [ not d.name.startswith("Detections:") and d.name != "SSH Enabled" and d.name != "Color Night Vision" + and d.name != "Tracking: Person" + and d.name != "HDR Mode" ) or d.name == "Detections: Motion" or d.name == "Detections: Person" or d.name == "Detections: Vehicle" ] CAMERA_SWITCHES_NO_EXTRA = [ - d for d in CAMERA_SWITCHES_BASIC if d.name not in ("High FPS", "Privacy Mode") + d + for d in CAMERA_SWITCHES_BASIC + if d.name not in ("High FPS", "Privacy Mode", "HDR Mode") ] @@ -54,11 +58,11 @@ async def test_switch_camera_remove( ufp.api.bootstrap.nvr.system_info.ustorage = None await init_entry(hass, ufp, [doorbell, unadopted_camera]) - assert_entity_counts(hass, Platform.SWITCH, 15, 14) + assert_entity_counts(hass, Platform.SWITCH, 15, 13) await remove_entities(hass, ufp, [doorbell, unadopted_camera]) assert_entity_counts(hass, Platform.SWITCH, 2, 2) await adopt_devices(hass, ufp, [doorbell, unadopted_camera]) - assert_entity_counts(hass, Platform.SWITCH, 15, 14) + assert_entity_counts(hass, Platform.SWITCH, 15, 13) async def test_switch_light_remove( @@ -170,7 +174,7 @@ async def test_switch_setup_camera_all( """Test switch entity setup for camera devices (all enabled feature flags).""" await init_entry(hass, ufp, [doorbell]) - assert_entity_counts(hass, Platform.SWITCH, 15, 14) + assert_entity_counts(hass, Platform.SWITCH, 15, 13) entity_registry = er.async_get(hass) @@ -293,7 +297,7 @@ async def test_switch_camera_ssh( """Tests SSH switch for cameras.""" await init_entry(hass, ufp, [doorbell]) - assert_entity_counts(hass, Platform.SWITCH, 15, 14) + assert_entity_counts(hass, Platform.SWITCH, 15, 13) description = CAMERA_SWITCHES[0] @@ -326,7 +330,7 @@ async def test_switch_camera_simple( """Tests all simple switches for cameras.""" await init_entry(hass, ufp, [doorbell]) - assert_entity_counts(hass, Platform.SWITCH, 15, 14) + assert_entity_counts(hass, Platform.SWITCH, 15, 13) assert description.ufp_set_method is not None @@ -355,7 +359,7 @@ async def test_switch_camera_highfps( """Tests High FPS switch for cameras.""" await init_entry(hass, ufp, [doorbell]) - assert_entity_counts(hass, Platform.SWITCH, 15, 14) + assert_entity_counts(hass, Platform.SWITCH, 15, 13) description = CAMERA_SWITCHES[3] @@ -386,7 +390,7 @@ async def test_switch_camera_privacy( previous_record = doorbell.recording_settings.mode = RecordingMode.DETECTIONS await init_entry(hass, ufp, [doorbell]) - assert_entity_counts(hass, Platform.SWITCH, 15, 14) + assert_entity_counts(hass, Platform.SWITCH, 15, 13) description = PRIVACY_MODE_SWITCH @@ -438,7 +442,7 @@ async def test_switch_camera_privacy_already_on( doorbell.add_privacy_zone() await init_entry(hass, ufp, [doorbell]) - assert_entity_counts(hass, Platform.SWITCH, 15, 14) + assert_entity_counts(hass, Platform.SWITCH, 15, 13) description = PRIVACY_MODE_SWITCH diff --git a/tests/components/unifiprotect/test_views.py b/tests/components/unifiprotect/test_views.py index 2952509fea9..f7930e5ff9a 100644 --- a/tests/components/unifiprotect/test_views.py +++ b/tests/components/unifiprotect/test_views.py @@ -37,7 +37,7 @@ async def test_thumbnail_bad_nvr_id( response = cast(ClientResponse, await http_client.get(url)) assert response.status == 404 - ufp.api.get_event_thumbnail.assert_not_called + ufp.api.get_event_thumbnail.assert_not_called() @pytest.mark.parametrize(("width", "height"), [("test", None), (None, "test")]) @@ -62,7 +62,7 @@ async def test_thumbnail_bad_params( response = cast(ClientResponse, await http_client.get(url)) assert response.status == 400 - ufp.api.get_event_thumbnail.assert_not_called + ufp.api.get_event_thumbnail.assert_not_called() async def test_thumbnail_bad_event( @@ -259,7 +259,7 @@ async def test_video_bad_nvr_id( response = cast(ClientResponse, await http_client.get(url)) assert response.status == 404 - ufp.api.request.assert_not_called + ufp.api.request.assert_not_called() async def test_video_bad_camera_id( @@ -293,7 +293,7 @@ async def test_video_bad_camera_id( response = cast(ClientResponse, await http_client.get(url)) assert response.status == 404 - ufp.api.request.assert_not_called + ufp.api.request.assert_not_called() async def test_video_bad_camera_perms( @@ -329,7 +329,7 @@ async def test_video_bad_camera_perms( response = cast(ClientResponse, await http_client.get(url)) assert response.status == 403 - ufp.api.request.assert_not_called + ufp.api.request.assert_not_called() @pytest.mark.parametrize(("start", "end"), [("test", None), (None, "test")]) @@ -369,7 +369,7 @@ async def test_video_bad_params( response = cast(ClientResponse, await http_client.get(url)) assert response.status == 400 - ufp.api.request.assert_not_called + ufp.api.request.assert_not_called() async def test_video_bad_video( @@ -403,7 +403,7 @@ async def test_video_bad_video( response = cast(ClientResponse, await http_client.get(url)) assert response.status == 404 - ufp.api.request.assert_called_once + ufp.api.request.assert_called_once() async def test_video( @@ -446,7 +446,7 @@ async def test_video( assert await response.content.read() == b"testtest" assert response.status == 200 - ufp.api.request.assert_called_once + ufp.api.request.assert_called_once() async def test_video_entity_id( @@ -483,11 +483,11 @@ async def test_video_entity_id( ) url = async_generate_event_video_url(event) - url = url.replace(camera.id, "camera.test_camera_high") + url = url.replace(camera.id, "camera.test_camera_high_resolution_channel") 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 + ufp.api.request.assert_called_once() diff --git a/tests/components/universal/test_media_player.py b/tests/components/universal/test_media_player.py index 60196e6fe24..9df9247900f 100644 --- a/tests/components/universal/test_media_player.py +++ b/tests/components/universal/test_media_player.py @@ -1,4 +1,5 @@ """The tests for the Universal Media player platform.""" + from copy import copy from unittest.mock import Mock, patch @@ -364,8 +365,7 @@ async def test_platform_setup(hass: HomeAssistant) -> None: def add_entities(new_entities): """Add devices to list.""" - for dev in new_entities: - entities.append(dev) + entities.extend(new_entities) setup_ok = True try: @@ -1123,12 +1123,15 @@ async def test_browse_media(hass: HomeAssistant) -> None: ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config["name"]) await ump.async_update() - with patch( - "homeassistant.components.demo.media_player.MediaPlayerEntity.supported_features", - MediaPlayerEntityFeature.BROWSE_MEDIA, - ), patch( - "homeassistant.components.demo.media_player.MediaPlayerEntity.async_browse_media", - return_value=MOCK_BROWSE_MEDIA, + with ( + patch( + "homeassistant.components.demo.media_player.MediaPlayerEntity.supported_features", + MediaPlayerEntityFeature.BROWSE_MEDIA, + ), + patch( + "homeassistant.components.demo.media_player.MediaPlayerEntity.async_browse_media", + return_value=MOCK_BROWSE_MEDIA, + ), ): result = await ump.async_browse_media() assert result == MOCK_BROWSE_MEDIA @@ -1155,12 +1158,15 @@ async def test_browse_media_override(hass: HomeAssistant) -> None: ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config["name"]) await ump.async_update() - with patch( - "homeassistant.components.demo.media_player.MediaPlayerEntity.supported_features", - MediaPlayerEntityFeature.BROWSE_MEDIA, - ), patch( - "homeassistant.components.demo.media_player.MediaPlayerEntity.async_browse_media", - return_value=MOCK_BROWSE_MEDIA, + with ( + patch( + "homeassistant.components.demo.media_player.MediaPlayerEntity.supported_features", + MediaPlayerEntityFeature.BROWSE_MEDIA, + ), + patch( + "homeassistant.components.demo.media_player.MediaPlayerEntity.async_browse_media", + return_value=MOCK_BROWSE_MEDIA, + ), ): result = await ump.async_browse_media() assert result == MOCK_BROWSE_MEDIA diff --git a/tests/components/upb/test_config_flow.py b/tests/components/upb/test_config_flow.py index d2fbe27248d..b3c3dfce15c 100644 --- a/tests/components/upb/test_config_flow.py +++ b/tests/components/upb/test_config_flow.py @@ -1,4 +1,5 @@ """Test the UPB Control config flow.""" + from unittest.mock import MagicMock, PropertyMock, patch from homeassistant import config_entries @@ -25,8 +26,9 @@ def mocked_upb(sync_complete=True, config_ok=True): async def valid_tcp_flow(hass, sync_complete=True, config_ok=True): """Get result dict that are standard for most tests.""" - with mocked_upb(sync_complete, config_ok), patch( - "homeassistant.components.upb.async_setup_entry", return_value=True + with ( + mocked_upb(sync_complete, config_ok), + patch("homeassistant.components.upb.async_setup_entry", return_value=True), ): flow = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -41,9 +43,12 @@ async def valid_tcp_flow(hass, sync_complete=True, config_ok=True): async def test_full_upb_flow_with_serial_port(hass: HomeAssistant) -> None: """Test a full UPB config flow with serial port.""" - with mocked_upb(), patch( - "homeassistant.components.upb.async_setup_entry", return_value=True - ) as mock_setup_entry: + with ( + mocked_upb(), + patch( + "homeassistant.components.upb.async_setup_entry", return_value=True + ) as mock_setup_entry, + ): flow = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -110,9 +115,12 @@ async def test_form_user_with_already_configured(hass: HomeAssistant) -> None: async def test_form_import(hass: HomeAssistant) -> None: """Test we get the form with import source.""" - with mocked_upb(), patch( - "homeassistant.components.upb.async_setup_entry", return_value=True - ) as mock_setup_entry: + with ( + mocked_upb(), + patch( + "homeassistant.components.upb.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}, diff --git a/tests/components/upcloud/test_config_flow.py b/tests/components/upcloud/test_config_flow.py index cc869cdb99b..86f73463fab 100644 --- a/tests/components/upcloud/test_config_flow.py +++ b/tests/components/upcloud/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for the UpCloud config flow.""" + from unittest.mock import patch import requests.exceptions diff --git a/tests/components/update/common.py b/tests/components/update/common.py new file mode 100644 index 00000000000..70b69498f66 --- /dev/null +++ b/tests/components/update/common.py @@ -0,0 +1,65 @@ +"""Common test fixtures for the update component test.""" + +import logging +from typing import Any + +from homeassistant.components.update import UpdateEntity + +from tests.common import MockEntity + +_LOGGER = logging.getLogger(__name__) + + +class MockUpdateEntity(MockEntity, UpdateEntity): + """Mock UpdateEntity class.""" + + @property + def auto_update(self) -> bool: + """Indicate if the device or service has auto update enabled.""" + return self._handle("auto_update") + + @property + def installed_version(self) -> str | None: + """Version currently installed and in use.""" + return self._handle("installed_version") + + @property + def in_progress(self) -> bool | int | None: + """Update installation progress.""" + return self._handle("in_progress") + + @property + def latest_version(self) -> str | None: + """Latest version available for install.""" + return self._handle("latest_version") + + @property + def release_summary(self) -> str | None: + """Summary of the release notes or changelog.""" + return self._handle("release_summary") + + @property + def release_url(self) -> str | None: + """URL to the full release notes of the latest version available.""" + return self._handle("release_url") + + @property + def title(self) -> str | None: + """Title of the software.""" + return self._handle("title") + + def install(self, version: str | None, backup: bool, **kwargs: Any) -> None: + """Install an update.""" + if backup: + _LOGGER.info("Creating backup before installing update") + + if version is not None: + self._values["installed_version"] = version + _LOGGER.info("Installed update with version: %s", version) + else: + self._values["installed_version"] = self.latest_version + _LOGGER.info("Installed latest update") + + def release_notes(self) -> str | None: + """Return the release notes of the latest version.""" + return "Release notes" diff --git a/tests/components/update/conftest.py b/tests/components/update/conftest.py new file mode 100644 index 00000000000..759f243e8db --- /dev/null +++ b/tests/components/update/conftest.py @@ -0,0 +1,82 @@ +"""Fixtures for update component testing.""" + +import pytest + +from homeassistant.components.update import UpdateEntityFeature + +from .common import MockUpdateEntity + + +@pytest.fixture +def mock_update_entities() -> list[MockUpdateEntity]: + """Return a list of mock update entities.""" + return [ + MockUpdateEntity( + name="No Update", + unique_id="no_update", + installed_version="1.0.0", + latest_version="1.0.0", + supported_features=UpdateEntityFeature.INSTALL, + ), + MockUpdateEntity( + name="Update Available", + unique_id="update_available", + installed_version="1.0.0", + latest_version="1.0.1", + supported_features=UpdateEntityFeature.INSTALL, + ), + MockUpdateEntity( + name="Update Unknown", + unique_id="update_unknown", + installed_version="1.0.0", + latest_version=None, + supported_features=UpdateEntityFeature.INSTALL, + ), + MockUpdateEntity( + name="Update Specific Version", + unique_id="update_specific_version", + installed_version="1.0.0", + latest_version="1.0.0", + supported_features=UpdateEntityFeature.INSTALL + | UpdateEntityFeature.SPECIFIC_VERSION, + ), + MockUpdateEntity( + name="Update Backup", + unique_id="update_backup", + installed_version="1.0.0", + latest_version="1.0.1", + supported_features=UpdateEntityFeature.INSTALL + | UpdateEntityFeature.SPECIFIC_VERSION + | UpdateEntityFeature.BACKUP, + ), + MockUpdateEntity( + name="Update Already in Progress", + unique_id="update_already_in_progres", + installed_version="1.0.0", + latest_version="1.0.1", + in_progress=50, + supported_features=UpdateEntityFeature.INSTALL + | UpdateEntityFeature.PROGRESS, + ), + MockUpdateEntity( + name="Update No Install", + unique_id="no_install", + installed_version="1.0.0", + latest_version="1.0.1", + ), + MockUpdateEntity( + name="Update with release notes", + unique_id="with_release_notes", + installed_version="1.0.0", + latest_version="1.0.1", + supported_features=UpdateEntityFeature.RELEASE_NOTES, + ), + MockUpdateEntity( + name="Update with auto update", + unique_id="with_auto_update", + installed_version="1.0.0", + latest_version="1.0.1", + auto_update=True, + supported_features=UpdateEntityFeature.INSTALL, + ), + ] diff --git a/tests/components/update/test_device_trigger.py b/tests/components/update/test_device_trigger.py index 16749167c41..31a9ee7b36e 100644 --- a/tests/components/update/test_device_trigger.py +++ b/tests/components/update/test_device_trigger.py @@ -1,4 +1,5 @@ """The test for update device automation.""" + from datetime import timedelta import pytest @@ -19,7 +20,9 @@ from tests.common import ( async_get_device_automation_capabilities, async_get_device_automations, async_mock_service, + setup_test_component_platform, ) +from tests.components.update.common import MockUpdateEntity @pytest.fixture(autouse=True, name="stub_blueprint_populate") @@ -67,12 +70,12 @@ async def test_get_triggers( @pytest.mark.parametrize( ("hidden_by", "entity_category"), - ( + [ (er.RegistryEntryHider.INTEGRATION, None), (er.RegistryEntryHider.USER, None), (None, EntityCategory.CONFIG), (None, EntityCategory.DIAGNOSTIC), - ), + ], ) async def test_get_triggers_hidden_auxiliary( hass: HomeAssistant, @@ -179,12 +182,10 @@ async def test_if_fires_on_state_change( device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, calls: list[ServiceCall], - enable_custom_integrations: None, + mock_update_entities: list[MockUpdateEntity], ) -> None: """Test for turn_on and turn_off triggers firing.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - - platform.init() + setup_test_component_platform(hass, DOMAIN, mock_update_entities) assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) await hass.async_block_till_done() @@ -281,12 +282,10 @@ async def test_if_fires_on_state_change_legacy( device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, calls: list[ServiceCall], - enable_custom_integrations: None, + mock_update_entities: list[MockUpdateEntity], ) -> None: """Test for turn_on and turn_off triggers firing.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - - platform.init() + setup_test_component_platform(hass, DOMAIN, mock_update_entities) assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) await hass.async_block_till_done() @@ -351,12 +350,10 @@ async def test_if_fires_on_state_change_with_for( device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, calls: list[ServiceCall], - enable_custom_integrations: None, + mock_update_entities: list[MockUpdateEntity], ) -> None: """Test for triggers firing with delay.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - - platform.init() + setup_test_component_platform(hass, DOMAIN, mock_update_entities) assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) await hass.async_block_till_done() diff --git a/tests/components/update/test_init.py b/tests/components/update/test_init.py index 67661d6936e..02ca605eed4 100644 --- a/tests/components/update/test_init.py +++ b/tests/components/update/test_init.py @@ -1,4 +1,5 @@ """The tests for the Update component.""" + from collections.abc import Generator from unittest.mock import MagicMock, patch @@ -49,6 +50,7 @@ from tests.common import ( mock_integration, mock_platform, mock_restore_cache, + setup_test_component_platform, ) from tests.typing import WebSocketGenerator @@ -165,11 +167,10 @@ async def test_update(hass: HomeAssistant) -> None: async def test_entity_with_no_install( hass: HomeAssistant, - enable_custom_integrations: None, + mock_update_entities: list[MockUpdateEntity], ) -> None: """Test entity with no updates.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() + setup_test_component_platform(hass, DOMAIN, mock_update_entities) assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) await hass.async_block_till_done() @@ -231,11 +232,10 @@ async def test_entity_with_no_install( async def test_entity_with_no_updates( hass: HomeAssistant, - enable_custom_integrations: None, + mock_update_entities: list[MockUpdateEntity], ) -> None: """Test entity with no updates.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() + setup_test_component_platform(hass, DOMAIN, mock_update_entities) assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) await hass.async_block_till_done() @@ -280,12 +280,11 @@ async def test_entity_with_no_updates( async def test_entity_with_auto_update( hass: HomeAssistant, - enable_custom_integrations: None, + mock_update_entities: list[MockUpdateEntity], caplog: pytest.LogCaptureFixture, ) -> None: """Test update entity that has auto update feature.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() + setup_test_component_platform(hass, DOMAIN, mock_update_entities) assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) await hass.async_block_till_done() @@ -332,12 +331,11 @@ async def test_entity_with_auto_update( async def test_entity_with_updates_available( hass: HomeAssistant, - enable_custom_integrations: None, + mock_update_entities: list[MockUpdateEntity], caplog: pytest.LogCaptureFixture, ) -> None: """Test basic update entity with updates available.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() + setup_test_component_platform(hass, DOMAIN, mock_update_entities) assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) await hass.async_block_till_done() @@ -386,12 +384,11 @@ async def test_entity_with_updates_available( async def test_entity_with_unknown_version( hass: HomeAssistant, - enable_custom_integrations: None, + mock_update_entities: list[MockUpdateEntity], caplog: pytest.LogCaptureFixture, ) -> None: """Test update entity that has an unknown version.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() + setup_test_component_platform(hass, DOMAIN, mock_update_entities) assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) await hass.async_block_till_done() @@ -424,12 +421,11 @@ async def test_entity_with_unknown_version( async def test_entity_with_specific_version( hass: HomeAssistant, - enable_custom_integrations: None, + mock_update_entities: list[MockUpdateEntity], caplog: pytest.LogCaptureFixture, ) -> None: """Test update entity that support specific version.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() + setup_test_component_platform(hass, DOMAIN, mock_update_entities) assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) await hass.async_block_till_done() @@ -487,12 +483,11 @@ async def test_entity_with_specific_version( async def test_entity_with_backup_support( hass: HomeAssistant, - enable_custom_integrations: None, + mock_update_entities: list[MockUpdateEntity], caplog: pytest.LogCaptureFixture, ) -> None: """Test update entity with backup support.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() + setup_test_component_platform(hass, DOMAIN, mock_update_entities) assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) await hass.async_block_till_done() @@ -547,12 +542,11 @@ async def test_entity_with_backup_support( async def test_entity_already_in_progress( hass: HomeAssistant, - enable_custom_integrations: None, + mock_update_entities: list[MockUpdateEntity], caplog: pytest.LogCaptureFixture, ) -> None: """Test update install already in progress.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() + setup_test_component_platform(hass, DOMAIN, mock_update_entities) assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) await hass.async_block_till_done() @@ -578,15 +572,14 @@ async def test_entity_already_in_progress( async def test_entity_without_progress_support( hass: HomeAssistant, - enable_custom_integrations: None, + mock_update_entities: list[MockUpdateEntity], caplog: pytest.LogCaptureFixture, ) -> None: """Test update entity without progress support. In that case, progress is still handled by Home Assistant. """ - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() + setup_test_component_platform(hass, DOMAIN, mock_update_entities) assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) await hass.async_block_till_done() @@ -617,15 +610,14 @@ async def test_entity_without_progress_support( async def test_entity_without_progress_support_raising( hass: HomeAssistant, - enable_custom_integrations: None, + mock_update_entities: list[MockUpdateEntity], caplog: pytest.LogCaptureFixture, ) -> None: """Test update entity without progress support that raises during install. In that case, progress is still handled by Home Assistant. """ - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() + setup_test_component_platform(hass, DOMAIN, mock_update_entities) assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) await hass.async_block_till_done() @@ -635,10 +627,13 @@ async def test_entity_without_progress_support_raising( hass, "update.update_available", callback(lambda event: events.append(event)) ) - with patch( - "homeassistant.components.update.UpdateEntity.async_install", - side_effect=RuntimeError, - ), pytest.raises(RuntimeError): + with ( + patch( + "homeassistant.components.update.UpdateEntity.async_install", + side_effect=RuntimeError, + ), + pytest.raises(RuntimeError), + ): await hass.services.async_call( DOMAIN, SERVICE_INSTALL, @@ -646,6 +641,8 @@ async def test_entity_without_progress_support_raising( blocking=True, ) + await hass.async_block_till_done() + assert len(events) == 2 assert events[0].data.get("old_state").attributes[ATTR_IN_PROGRESS] is False assert events[0].data.get("old_state").attributes[ATTR_INSTALLED_VERSION] == "1.0.0" @@ -659,7 +656,7 @@ async def test_entity_without_progress_support_raising( async def test_restore_state( - hass: HomeAssistant, enable_custom_integrations: None + hass: HomeAssistant, mock_update_entities: list[MockUpdateEntity] ) -> None: """Test we restore skipped version state.""" mock_restore_cache( @@ -675,8 +672,7 @@ async def test_restore_state( ), ) - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() + setup_test_component_platform(hass, DOMAIN, mock_update_entities) assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) await hass.async_block_till_done() @@ -691,12 +687,11 @@ async def test_restore_state( async def test_release_notes( hass: HomeAssistant, - enable_custom_integrations: None, + mock_update_entities: list[MockUpdateEntity], hass_ws_client: WebSocketGenerator, ) -> None: """Test getting the release notes over the websocket connection.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() + setup_test_component_platform(hass, DOMAIN, mock_update_entities) assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) await hass.async_block_till_done() @@ -717,12 +712,11 @@ async def test_release_notes( async def test_release_notes_entity_not_found( hass: HomeAssistant, - enable_custom_integrations: None, + mock_update_entities: list[MockUpdateEntity], hass_ws_client: WebSocketGenerator, ) -> None: """Test getting the release notes for not found entity.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() + setup_test_component_platform(hass, DOMAIN, mock_update_entities) assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) await hass.async_block_till_done() @@ -744,12 +738,11 @@ async def test_release_notes_entity_not_found( async def test_release_notes_entity_does_not_support_release_notes( hass: HomeAssistant, - enable_custom_integrations: None, + mock_update_entities: list[MockUpdateEntity], hass_ws_client: WebSocketGenerator, ) -> None: """Test getting the release notes for entity that does not support release notes.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() + setup_test_component_platform(hass, DOMAIN, mock_update_entities) assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) await hass.async_block_till_done() diff --git a/tests/components/update/test_recorder.py b/tests/components/update/test_recorder.py index 1c0423bb9ad..da63518009e 100644 --- a/tests/components/update/test_recorder.py +++ b/tests/components/update/test_recorder.py @@ -1,4 +1,5 @@ """The tests for update recorder.""" + from __future__ import annotations from datetime import timedelta @@ -16,17 +17,19 @@ from homeassistant.core import HomeAssistant 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.common import async_fire_time_changed, setup_test_component_platform from tests.components.recorder.common import async_wait_recording_done +from tests.components.update.common import MockUpdateEntity async def test_exclude_attributes( - recorder_mock: Recorder, hass: HomeAssistant, enable_custom_integrations: None + recorder_mock: Recorder, + hass: HomeAssistant, + mock_update_entities: list[MockUpdateEntity], ) -> None: """Test update attributes to be excluded.""" now = dt_util.utcnow() - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() + setup_test_component_platform(hass, DOMAIN, mock_update_entities) assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) await hass.async_block_till_done() state = hass.states.get("update.update_already_in_progress") diff --git a/tests/components/update/test_significant_change.py b/tests/components/update/test_significant_change.py index 35e601f4789..6c6ffbb10ee 100644 --- a/tests/components/update/test_significant_change.py +++ b/tests/components/update/test_significant_change.py @@ -1,4 +1,5 @@ """Test the update significant change platform.""" + from homeassistant.components.update.const import ( ATTR_IN_PROGRESS, ATTR_INSTALLED_VERSION, diff --git a/tests/components/upnp/conftest.py b/tests/components/upnp/conftest.py index db166144925..0959e8e31da 100644 --- a/tests/components/upnp/conftest.py +++ b/tests/components/upnp/conftest.py @@ -1,4 +1,5 @@ """Configuration for SSDP tests.""" + from __future__ import annotations import copy @@ -138,14 +139,16 @@ def mock_setup_entry(): @pytest.fixture(autouse=True) async def silent_ssdp_scanner(hass): """Start SSDP component and get Scanner, prevent actual SSDP traffic.""" - with patch( - "homeassistant.components.ssdp.Scanner._async_start_ssdp_listeners" - ), patch("homeassistant.components.ssdp.Scanner._async_stop_ssdp_listeners"), patch( - "homeassistant.components.ssdp.Scanner.async_scan" - ), patch( - "homeassistant.components.ssdp.Server._async_start_upnp_servers", - ), patch( - "homeassistant.components.ssdp.Server._async_stop_upnp_servers", + with ( + patch("homeassistant.components.ssdp.Scanner._async_start_ssdp_listeners"), + patch("homeassistant.components.ssdp.Scanner._async_stop_ssdp_listeners"), + patch("homeassistant.components.ssdp.Scanner.async_scan"), + patch( + "homeassistant.components.ssdp.Server._async_start_upnp_servers", + ), + patch( + "homeassistant.components.ssdp.Server._async_stop_upnp_servers", + ), ): yield @@ -160,13 +163,16 @@ async def ssdp_instant_discovery(): await callback(TEST_DISCOVERY, ssdp.SsdpChange.ALIVE) return MagicMock() - with patch( - "homeassistant.components.ssdp.async_register_callback", - side_effect=register_callback, - ) as mock_register, patch( - "homeassistant.components.ssdp.async_get_discovery_info_by_st", - return_value=[TEST_DISCOVERY], - ) as mock_get_info: + with ( + patch( + "homeassistant.components.ssdp.async_register_callback", + side_effect=register_callback, + ) as mock_register, + patch( + "homeassistant.components.ssdp.async_get_discovery_info_by_st", + return_value=[TEST_DISCOVERY], + ) as mock_get_info, + ): yield (mock_register, mock_get_info) @@ -184,13 +190,16 @@ async def ssdp_instant_discovery_multi_location(): await callback(test_discovery, ssdp.SsdpChange.ALIVE) return MagicMock() - with patch( - "homeassistant.components.ssdp.async_register_callback", - side_effect=register_callback, - ) as mock_register, patch( - "homeassistant.components.ssdp.async_get_discovery_info_by_st", - return_value=[test_discovery], - ) as mock_get_info: + with ( + patch( + "homeassistant.components.ssdp.async_register_callback", + side_effect=register_callback, + ) as mock_register, + patch( + "homeassistant.components.ssdp.async_get_discovery_info_by_st", + return_value=[test_discovery], + ) as mock_get_info, + ): yield (mock_register, mock_get_info) @@ -203,13 +212,16 @@ async def ssdp_no_discovery(): """Don't do callback.""" return MagicMock() - with patch( - "homeassistant.components.ssdp.async_register_callback", - side_effect=register_callback, - ) as mock_register, patch( - "homeassistant.components.ssdp.async_get_discovery_info_by_st", - return_value=[], - ) as mock_get_info: + with ( + patch( + "homeassistant.components.ssdp.async_register_callback", + side_effect=register_callback, + ) as mock_register, + patch( + "homeassistant.components.ssdp.async_get_discovery_info_by_st", + return_value=[], + ) as mock_get_info, + ): yield (mock_register, mock_get_info) diff --git a/tests/components/upnp/test_init.py b/tests/components/upnp/test_init.py index aeb228a1433..eab279b479e 100644 --- a/tests/components/upnp/test_init.py +++ b/tests/components/upnp/test_init.py @@ -1,4 +1,5 @@ """Test UPnP/IGD setup process.""" + from __future__ import annotations import copy @@ -131,12 +132,15 @@ async def test_async_setup_udn_mismatch( await callback(test_discovery, ssdp.SsdpChange.ALIVE) return MagicMock() - with patch( - "homeassistant.components.ssdp.async_register_callback", - side_effect=register_callback, - ), patch( - "homeassistant.components.ssdp.async_get_discovery_info_by_st", - return_value=[test_discovery], + with ( + patch( + "homeassistant.components.ssdp.async_register_callback", + side_effect=register_callback, + ), + patch( + "homeassistant.components.ssdp.async_get_discovery_info_by_st", + return_value=[test_discovery], + ), ): # Load config_entry. entry.add_to_hass(hass) diff --git a/tests/components/uptime/conftest.py b/tests/components/uptime/conftest.py index 7ee34856e63..a681fb40173 100644 --- a/tests/components/uptime/conftest.py +++ b/tests/components/uptime/conftest.py @@ -1,4 +1,5 @@ """Fixtures for Uptime integration tests.""" + from __future__ import annotations from collections.abc import Generator diff --git a/tests/components/uptime/snapshots/test_sensor.ambr b/tests/components/uptime/snapshots/test_sensor.ambr index d44108c2151..0e7ae6dceaa 100644 --- a/tests/components/uptime/snapshots/test_sensor.ambr +++ b/tests/components/uptime/snapshots/test_sensor.ambr @@ -8,6 +8,7 @@ 'context': , 'entity_id': 'sensor.uptime', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '2022-03-01T00:00:00+00:00', }) diff --git a/tests/components/uptime/test_init.py b/tests/components/uptime/test_init.py index 3535f846013..503657eb93e 100644 --- a/tests/components/uptime/test_init.py +++ b/tests/components/uptime/test_init.py @@ -1,4 +1,5 @@ """Tests for the Uptime integration.""" + from homeassistant.components.uptime.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant diff --git a/tests/components/uptime/test_sensor.py b/tests/components/uptime/test_sensor.py index 41c097badd1..169b1e80c7d 100644 --- a/tests/components/uptime/test_sensor.py +++ b/tests/components/uptime/test_sensor.py @@ -1,4 +1,5 @@ """The tests for the uptime sensor platform.""" + import pytest from syrupy.assertion import SnapshotAssertion from syrupy.filters import props diff --git a/tests/components/uptimerobot/common.py b/tests/components/uptimerobot/common.py index 15f6e153b19..23c0d3e1ce7 100644 --- a/tests/components/uptimerobot/common.py +++ b/tests/components/uptimerobot/common.py @@ -1,4 +1,5 @@ """Common constants and functions for UptimeRobot tests.""" + from __future__ import annotations from enum import Enum diff --git a/tests/components/uptimerobot/test_config_flow.py b/tests/components/uptimerobot/test_config_flow.py index ad495b9a55c..58faa524d6f 100644 --- a/tests/components/uptimerobot/test_config_flow.py +++ b/tests/components/uptimerobot/test_config_flow.py @@ -1,4 +1,5 @@ """Test the UptimeRobot config flow.""" + from unittest.mock import patch import pytest @@ -32,13 +33,16 @@ async def test_form(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.FORM assert result["errors"] is None - with patch( - "pyuptimerobot.UptimeRobot.async_get_account_details", - return_value=mock_uptimerobot_api_response(key=MockApiResponseKey.ACCOUNT), - ), patch( - "homeassistant.components.uptimerobot.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "pyuptimerobot.UptimeRobot.async_get_account_details", + return_value=mock_uptimerobot_api_response(key=MockApiResponseKey.ACCOUNT), + ), + patch( + "homeassistant.components.uptimerobot.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_API_KEY: MOCK_UPTIMEROBOT_API_KEY}, @@ -136,13 +140,16 @@ async def test_user_unique_id_already_exists( assert result["type"] == FlowResultType.FORM assert result["errors"] is None - with patch( - "pyuptimerobot.UptimeRobot.async_get_account_details", - return_value=mock_uptimerobot_api_response(key=MockApiResponseKey.ACCOUNT), - ), patch( - "homeassistant.components.uptimerobot.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "pyuptimerobot.UptimeRobot.async_get_account_details", + return_value=mock_uptimerobot_api_response(key=MockApiResponseKey.ACCOUNT), + ), + patch( + "homeassistant.components.uptimerobot.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_API_KEY: "12345"}, @@ -175,12 +182,15 @@ async def test_reauthentication( assert result["errors"] is None assert result["step_id"] == "reauth_confirm" - with patch( - "pyuptimerobot.UptimeRobot.async_get_account_details", - return_value=mock_uptimerobot_api_response(key=MockApiResponseKey.ACCOUNT), - ), patch( - "homeassistant.components.uptimerobot.async_setup_entry", - return_value=True, + with ( + patch( + "pyuptimerobot.UptimeRobot.async_get_account_details", + return_value=mock_uptimerobot_api_response(key=MockApiResponseKey.ACCOUNT), + ), + patch( + "homeassistant.components.uptimerobot.async_setup_entry", + return_value=True, + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -213,12 +223,15 @@ async def test_reauthentication_failure( assert result["errors"] is None assert result["step_id"] == "reauth_confirm" - with patch( - "pyuptimerobot.UptimeRobot.async_get_account_details", - return_value=mock_uptimerobot_api_response(key=MockApiResponseKey.ERROR), - ), patch( - "homeassistant.components.uptimerobot.async_setup_entry", - return_value=True, + with ( + patch( + "pyuptimerobot.UptimeRobot.async_get_account_details", + return_value=mock_uptimerobot_api_response(key=MockApiResponseKey.ERROR), + ), + patch( + "homeassistant.components.uptimerobot.async_setup_entry", + return_value=True, + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -254,12 +267,15 @@ async def test_reauthentication_failure_no_existing_entry( assert result["errors"] is None assert result["step_id"] == "reauth_confirm" - with patch( - "pyuptimerobot.UptimeRobot.async_get_account_details", - return_value=mock_uptimerobot_api_response(key=MockApiResponseKey.ACCOUNT), - ), patch( - "homeassistant.components.uptimerobot.async_setup_entry", - return_value=True, + with ( + patch( + "pyuptimerobot.UptimeRobot.async_get_account_details", + return_value=mock_uptimerobot_api_response(key=MockApiResponseKey.ACCOUNT), + ), + patch( + "homeassistant.components.uptimerobot.async_setup_entry", + return_value=True, + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -292,15 +308,18 @@ async def test_reauthentication_failure_account_not_matching( assert result["errors"] is None assert result["step_id"] == "reauth_confirm" - with patch( - "pyuptimerobot.UptimeRobot.async_get_account_details", - return_value=mock_uptimerobot_api_response( - key=MockApiResponseKey.ACCOUNT, - data={**MOCK_UPTIMEROBOT_ACCOUNT, "user_id": 1234567891}, + with ( + patch( + "pyuptimerobot.UptimeRobot.async_get_account_details", + return_value=mock_uptimerobot_api_response( + key=MockApiResponseKey.ACCOUNT, + data={**MOCK_UPTIMEROBOT_ACCOUNT, "user_id": 1234567891}, + ), + ), + patch( + "homeassistant.components.uptimerobot.async_setup_entry", + return_value=True, ), - ), patch( - "homeassistant.components.uptimerobot.async_setup_entry", - return_value=True, ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], diff --git a/tests/components/uptimerobot/test_diagnostics.py b/tests/components/uptimerobot/test_diagnostics.py index 9eca73d0240..12a91077552 100644 --- a/tests/components/uptimerobot/test_diagnostics.py +++ b/tests/components/uptimerobot/test_diagnostics.py @@ -1,4 +1,5 @@ """Test UptimeRobot diagnostics.""" + import json from unittest.mock import patch diff --git a/tests/components/uptimerobot/test_init.py b/tests/components/uptimerobot/test_init.py index 67fac2437f0..a45480e50a5 100644 --- a/tests/components/uptimerobot/test_init.py +++ b/tests/components/uptimerobot/test_init.py @@ -1,4 +1,5 @@ """Test the UptimeRobot init.""" + from unittest.mock import patch from freezegun.api import FrozenDateTimeFactory diff --git a/tests/components/uptimerobot/test_sensor.py b/tests/components/uptimerobot/test_sensor.py index 110ea07c202..8cee33c1052 100644 --- a/tests/components/uptimerobot/test_sensor.py +++ b/tests/components/uptimerobot/test_sensor.py @@ -19,17 +19,14 @@ from .common import ( from tests.common import async_fire_time_changed -SENSOR_ICON = "mdi:television-shimmer" - async def test_presentation(hass: HomeAssistant) -> None: - """Test the presenstation of UptimeRobot sensors.""" + """Test the presentation of UptimeRobot sensors.""" await setup_uptimerobot_integration(hass) entity = hass.states.get(UPTIMEROBOT_SENSOR_TEST_ENTITY) assert entity.state == STATE_UP - assert entity.attributes["icon"] == SENSOR_ICON assert entity.attributes["target"] == MOCK_UPTIMEROBOT_MONITOR["url"] assert entity.attributes["device_class"] == SensorDeviceClass.ENUM assert entity.attributes["options"] == [ @@ -41,8 +38,8 @@ async def test_presentation(hass: HomeAssistant) -> None: ] -async def test_unaviable_on_update_failure(hass: HomeAssistant) -> None: - """Test entity unaviable on update failure.""" +async def test_unavailable_on_update_failure(hass: HomeAssistant) -> None: + """Test entity unavailable on update failure.""" await setup_uptimerobot_integration(hass) entity = hass.states.get(UPTIMEROBOT_SENSOR_TEST_ENTITY) diff --git a/tests/components/uptimerobot/test_switch.py b/tests/components/uptimerobot/test_switch.py index f10a202f208..8c2cffe504a 100644 --- a/tests/components/uptimerobot/test_switch.py +++ b/tests/components/uptimerobot/test_switch.py @@ -1,4 +1,5 @@ """Test UptimeRobot switch.""" + from unittest.mock import patch import pytest @@ -28,30 +29,32 @@ from tests.common import MockConfigEntry async def test_presentation(hass: HomeAssistant) -> None: - """Test the presenstation of UptimeRobot sensors.""" + """Test the presentation of UptimeRobot switches.""" await setup_uptimerobot_integration(hass) entity = hass.states.get(UPTIMEROBOT_SWITCH_TEST_ENTITY) assert entity.state == STATE_ON - assert entity.attributes["icon"] == "mdi:cog" assert entity.attributes["target"] == MOCK_UPTIMEROBOT_MONITOR["url"] async def test_switch_off(hass: HomeAssistant) -> None: - """Test entity unaviable on update failure.""" + """Test entity unavailable on update failure.""" mock_entry = MockConfigEntry(**MOCK_UPTIMEROBOT_CONFIG_ENTRY_DATA) mock_entry.add_to_hass(hass) - with patch( - "pyuptimerobot.UptimeRobot.async_get_monitors", - return_value=mock_uptimerobot_api_response( - data=[MOCK_UPTIMEROBOT_MONITOR_PAUSED] + with ( + patch( + "pyuptimerobot.UptimeRobot.async_get_monitors", + return_value=mock_uptimerobot_api_response( + data=[MOCK_UPTIMEROBOT_MONITOR_PAUSED] + ), + ), + patch( + "pyuptimerobot.UptimeRobot.async_edit_monitor", + return_value=mock_uptimerobot_api_response(), ), - ), patch( - "pyuptimerobot.UptimeRobot.async_edit_monitor", - return_value=mock_uptimerobot_api_response(), ): assert await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() @@ -73,12 +76,15 @@ async def test_switch_on(hass: HomeAssistant) -> None: mock_entry = MockConfigEntry(**MOCK_UPTIMEROBOT_CONFIG_ENTRY_DATA) mock_entry.add_to_hass(hass) - with patch( - "pyuptimerobot.UptimeRobot.async_get_monitors", - return_value=mock_uptimerobot_api_response(data=[MOCK_UPTIMEROBOT_MONITOR]), - ), patch( - "pyuptimerobot.UptimeRobot.async_edit_monitor", - return_value=mock_uptimerobot_api_response(), + with ( + patch( + "pyuptimerobot.UptimeRobot.async_get_monitors", + return_value=mock_uptimerobot_api_response(data=[MOCK_UPTIMEROBOT_MONITOR]), + ), + patch( + "pyuptimerobot.UptimeRobot.async_edit_monitor", + return_value=mock_uptimerobot_api_response(), + ), ): assert await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() @@ -103,12 +109,15 @@ async def test_authentication_error( entity = hass.states.get(UPTIMEROBOT_SWITCH_TEST_ENTITY) assert entity.state == STATE_ON - with patch( - "pyuptimerobot.UptimeRobot.async_edit_monitor", - side_effect=UptimeRobotAuthenticationException, - ), patch( - "homeassistant.config_entries.ConfigEntry.async_start_reauth" - ) as config_entry_reauth: + with ( + patch( + "pyuptimerobot.UptimeRobot.async_edit_monitor", + side_effect=UptimeRobotAuthenticationException, + ), + patch( + "homeassistant.config_entries.ConfigEntry.async_start_reauth" + ) as config_entry_reauth, + ): await hass.services.async_call( SWITCH_DOMAIN, SERVICE_TURN_ON, diff --git a/tests/components/usb/__init__.py b/tests/components/usb/__init__.py index 7dbfdfdcff6..f5f32336931 100644 --- a/tests/components/usb/__init__.py +++ b/tests/components/usb/__init__.py @@ -1,6 +1,5 @@ """Tests for the USB Discovery integration.""" - from homeassistant.components.usb.models import USBDevice conbee_device = USBDevice( diff --git a/tests/components/usb/test_init.py b/tests/components/usb/test_init.py index a1637f62b01..c3f7817527c 100644 --- a/tests/components/usb/test_init.py +++ b/tests/components/usb/test_init.py @@ -1,4 +1,5 @@ """Tests for the USB Discovery integration.""" + import os import sys from unittest.mock import MagicMock, Mock, call, patch, sentinel @@ -84,17 +85,18 @@ async def test_observer_discovery( def _create_mock_monitor_observer(monitor, callback, name): nonlocal mock_observer - hass.async_create_task(_mock_monitor_observer_callback(callback)) + hass.create_task(_mock_monitor_observer_callback(callback)) mock_observer = MagicMock() return mock_observer - with patch("pyudev.Context"), patch( - "pyudev.MonitorObserver", new=_create_mock_monitor_observer - ), patch("pyudev.Monitor.filter_by"), patch( - "homeassistant.components.usb.async_get_usb", return_value=new_usb - ), patch( - "homeassistant.components.usb.comports", return_value=mock_comports - ), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow: + with ( + patch("pyudev.Context"), + patch("pyudev.MonitorObserver", new=_create_mock_monitor_observer), + patch("pyudev.Monitor.filter_by"), + patch("homeassistant.components.usb.async_get_usb", return_value=new_usb), + patch("homeassistant.components.usb.comports", return_value=mock_comports), + patch.object(hass.config_entries.flow, "async_init") as mock_config_flow, + ): assert await async_setup_component(hass, "usb", {"usb": {}}) await hass.async_block_till_done() hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -106,7 +108,7 @@ async def test_observer_discovery( hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) await hass.async_block_till_done() - assert mock_observer.mock_calls == [call.start(), call.stop()] + assert mock_observer.mock_calls == [call.start(), call.__bool__(), call.stop()] @pytest.mark.skipif( @@ -139,13 +141,12 @@ async def test_removal_by_observer_before_started( ) ] - with patch( - "homeassistant.components.usb.async_get_usb", return_value=new_usb - ), patch( - "homeassistant.components.usb.comports", return_value=mock_comports - ), patch("pyudev.MonitorObserver", new=_create_mock_monitor_observer), patch.object( - hass.config_entries.flow, "async_init" - ) as mock_config_flow: + with ( + patch("homeassistant.components.usb.async_get_usb", return_value=new_usb), + patch("homeassistant.components.usb.comports", return_value=mock_comports), + patch("pyudev.MonitorObserver", new=_create_mock_monitor_observer), + patch.object(hass.config_entries.flow, "async_init") as mock_config_flow, + ): assert await async_setup_component(hass, "usb", {"usb": {}}) await hass.async_block_till_done() @@ -176,11 +177,12 @@ async def test_discovered_by_websocket_scan( ) ] - with patch("pyudev.Context", side_effect=ImportError), patch( - "homeassistant.components.usb.async_get_usb", return_value=new_usb - ), patch( - "homeassistant.components.usb.comports", return_value=mock_comports - ), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow: + with ( + patch("pyudev.Context", side_effect=ImportError), + patch("homeassistant.components.usb.async_get_usb", return_value=new_usb), + patch("homeassistant.components.usb.comports", return_value=mock_comports), + patch.object(hass.config_entries.flow, "async_init") as mock_config_flow, + ): assert await async_setup_component(hass, "usb", {"usb": {}}) await hass.async_block_till_done() hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -214,11 +216,12 @@ async def test_discovered_by_websocket_scan_limited_by_description_matcher( ) ] - with patch("pyudev.Context", side_effect=ImportError), patch( - "homeassistant.components.usb.async_get_usb", return_value=new_usb - ), patch( - "homeassistant.components.usb.comports", return_value=mock_comports - ), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow: + with ( + patch("pyudev.Context", side_effect=ImportError), + patch("homeassistant.components.usb.async_get_usb", return_value=new_usb), + patch("homeassistant.components.usb.comports", return_value=mock_comports), + patch.object(hass.config_entries.flow, "async_init") as mock_config_flow, + ): assert await async_setup_component(hass, "usb", {"usb": {}}) await hass.async_block_till_done() hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -253,11 +256,12 @@ async def test_most_targeted_matcher_wins( ) ] - with patch("pyudev.Context", side_effect=ImportError), patch( - "homeassistant.components.usb.async_get_usb", return_value=new_usb - ), patch( - "homeassistant.components.usb.comports", return_value=mock_comports - ), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow: + with ( + patch("pyudev.Context", side_effect=ImportError), + patch("homeassistant.components.usb.async_get_usb", return_value=new_usb), + patch("homeassistant.components.usb.comports", return_value=mock_comports), + patch.object(hass.config_entries.flow, "async_init") as mock_config_flow, + ): assert await async_setup_component(hass, "usb", {"usb": {}}) await hass.async_block_till_done() hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -291,11 +295,12 @@ async def test_discovered_by_websocket_scan_rejected_by_description_matcher( ) ] - with patch("pyudev.Context", side_effect=ImportError), patch( - "homeassistant.components.usb.async_get_usb", return_value=new_usb - ), patch( - "homeassistant.components.usb.comports", return_value=mock_comports - ), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow: + with ( + patch("pyudev.Context", side_effect=ImportError), + patch("homeassistant.components.usb.async_get_usb", return_value=new_usb), + patch("homeassistant.components.usb.comports", return_value=mock_comports), + patch.object(hass.config_entries.flow, "async_init") as mock_config_flow, + ): assert await async_setup_component(hass, "usb", {"usb": {}}) await hass.async_block_till_done() hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -333,11 +338,12 @@ async def test_discovered_by_websocket_scan_limited_by_serial_number_matcher( ) ] - with patch("pyudev.Context", side_effect=ImportError), patch( - "homeassistant.components.usb.async_get_usb", return_value=new_usb - ), patch( - "homeassistant.components.usb.comports", return_value=mock_comports - ), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow: + with ( + patch("pyudev.Context", side_effect=ImportError), + patch("homeassistant.components.usb.async_get_usb", return_value=new_usb), + patch("homeassistant.components.usb.comports", return_value=mock_comports), + patch.object(hass.config_entries.flow, "async_init") as mock_config_flow, + ): assert await async_setup_component(hass, "usb", {"usb": {}}) await hass.async_block_till_done() hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -371,11 +377,12 @@ async def test_discovered_by_websocket_scan_rejected_by_serial_number_matcher( ) ] - with patch("pyudev.Context", side_effect=ImportError), patch( - "homeassistant.components.usb.async_get_usb", return_value=new_usb - ), patch( - "homeassistant.components.usb.comports", return_value=mock_comports - ), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow: + with ( + patch("pyudev.Context", side_effect=ImportError), + patch("homeassistant.components.usb.async_get_usb", return_value=new_usb), + patch("homeassistant.components.usb.comports", return_value=mock_comports), + patch.object(hass.config_entries.flow, "async_init") as mock_config_flow, + ): assert await async_setup_component(hass, "usb", {"usb": {}}) await hass.async_block_till_done() hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -413,11 +420,12 @@ async def test_discovered_by_websocket_scan_limited_by_manufacturer_matcher( ) ] - with patch("pyudev.Context", side_effect=ImportError), patch( - "homeassistant.components.usb.async_get_usb", return_value=new_usb - ), patch( - "homeassistant.components.usb.comports", return_value=mock_comports - ), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow: + with ( + patch("pyudev.Context", side_effect=ImportError), + patch("homeassistant.components.usb.async_get_usb", return_value=new_usb), + patch("homeassistant.components.usb.comports", return_value=mock_comports), + patch.object(hass.config_entries.flow, "async_init") as mock_config_flow, + ): assert await async_setup_component(hass, "usb", {"usb": {}}) await hass.async_block_till_done() hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -456,11 +464,12 @@ async def test_discovered_by_websocket_scan_rejected_by_manufacturer_matcher( ) ] - with patch("pyudev.Context", side_effect=ImportError), patch( - "homeassistant.components.usb.async_get_usb", return_value=new_usb - ), patch( - "homeassistant.components.usb.comports", return_value=mock_comports - ), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow: + with ( + patch("pyudev.Context", side_effect=ImportError), + patch("homeassistant.components.usb.async_get_usb", return_value=new_usb), + patch("homeassistant.components.usb.comports", return_value=mock_comports), + patch.object(hass.config_entries.flow, "async_init") as mock_config_flow, + ): assert await async_setup_component(hass, "usb", {"usb": {}}) await hass.async_block_till_done() hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -493,11 +502,12 @@ async def test_discovered_by_websocket_rejected_with_empty_serial_number_only( ) ] - with patch("pyudev.Context", side_effect=ImportError), patch( - "homeassistant.components.usb.async_get_usb", return_value=new_usb - ), patch( - "homeassistant.components.usb.comports", return_value=mock_comports - ), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow: + with ( + patch("pyudev.Context", side_effect=ImportError), + patch("homeassistant.components.usb.async_get_usb", return_value=new_usb), + patch("homeassistant.components.usb.comports", return_value=mock_comports), + patch.object(hass.config_entries.flow, "async_init") as mock_config_flow, + ): assert await async_setup_component(hass, "usb", {"usb": {}}) await hass.async_block_till_done() hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -528,11 +538,12 @@ async def test_discovered_by_websocket_scan_match_vid_only( ) ] - with patch("pyudev.Context", side_effect=ImportError), patch( - "homeassistant.components.usb.async_get_usb", return_value=new_usb - ), patch( - "homeassistant.components.usb.comports", return_value=mock_comports - ), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow: + with ( + patch("pyudev.Context", side_effect=ImportError), + patch("homeassistant.components.usb.async_get_usb", return_value=new_usb), + patch("homeassistant.components.usb.comports", return_value=mock_comports), + patch.object(hass.config_entries.flow, "async_init") as mock_config_flow, + ): assert await async_setup_component(hass, "usb", {"usb": {}}) await hass.async_block_till_done() hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -564,11 +575,12 @@ async def test_discovered_by_websocket_scan_match_vid_wrong_pid( ) ] - with patch("pyudev.Context", side_effect=ImportError), patch( - "homeassistant.components.usb.async_get_usb", return_value=new_usb - ), patch( - "homeassistant.components.usb.comports", return_value=mock_comports - ), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow: + with ( + patch("pyudev.Context", side_effect=ImportError), + patch("homeassistant.components.usb.async_get_usb", return_value=new_usb), + patch("homeassistant.components.usb.comports", return_value=mock_comports), + patch.object(hass.config_entries.flow, "async_init") as mock_config_flow, + ): assert await async_setup_component(hass, "usb", {"usb": {}}) await hass.async_block_till_done() hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -599,11 +611,12 @@ async def test_discovered_by_websocket_no_vid_pid( ) ] - with patch("pyudev.Context", side_effect=ImportError), patch( - "homeassistant.components.usb.async_get_usb", return_value=new_usb - ), patch( - "homeassistant.components.usb.comports", return_value=mock_comports - ), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow: + with ( + patch("pyudev.Context", side_effect=ImportError), + patch("homeassistant.components.usb.async_get_usb", return_value=new_usb), + patch("homeassistant.components.usb.comports", return_value=mock_comports), + patch.object(hass.config_entries.flow, "async_init") as mock_config_flow, + ): assert await async_setup_component(hass, "usb", {"usb": {}}) await hass.async_block_till_done() hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -635,11 +648,12 @@ async def test_non_matching_discovered_by_scanner_after_started( ) ] - with patch("pyudev.Context", side_effect=exception_type), patch( - "homeassistant.components.usb.async_get_usb", return_value=new_usb - ), patch( - "homeassistant.components.usb.comports", return_value=mock_comports - ), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow: + with ( + patch("pyudev.Context", side_effect=exception_type), + patch("homeassistant.components.usb.async_get_usb", return_value=new_usb), + patch("homeassistant.components.usb.comports", return_value=mock_comports), + patch.object(hass.config_entries.flow, "async_init") as mock_config_flow, + ): assert await async_setup_component(hass, "usb", {"usb": {}}) await hass.async_block_till_done() hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -674,11 +688,13 @@ async def test_observer_on_wsl_fallback_without_throwing_exception( ) ] - with patch("pyudev.Context"), patch( - "pyudev.Monitor.filter_by", side_effect=ValueError - ), patch("homeassistant.components.usb.async_get_usb", return_value=new_usb), patch( - "homeassistant.components.usb.comports", return_value=mock_comports - ), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow: + with ( + patch("pyudev.Context"), + patch("pyudev.Monitor.filter_by", side_effect=ValueError), + patch("homeassistant.components.usb.async_get_usb", return_value=new_usb), + patch("homeassistant.components.usb.comports", return_value=mock_comports), + patch.object(hass.config_entries.flow, "async_init") as mock_config_flow, + ): assert await async_setup_component(hass, "usb", {"usb": {}}) await hass.async_block_till_done() hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -724,17 +740,18 @@ async def test_not_discovered_by_observer_before_started_on_docker( ) ] - with patch( - "homeassistant.components.usb.async_get_usb", return_value=new_usb - ), patch( - "homeassistant.components.usb.comports", return_value=mock_comports - ), patch("pyudev.MonitorObserver", new=_create_mock_monitor_observer): + with ( + patch("homeassistant.components.usb.async_get_usb", return_value=new_usb), + patch("homeassistant.components.usb.comports", return_value=mock_comports), + patch("pyudev.MonitorObserver", new=_create_mock_monitor_observer), + ): assert await async_setup_component(hass, "usb", {"usb": {}}) await hass.async_block_till_done() - with patch("homeassistant.components.usb.comports", return_value=[]), patch.object( - hass.config_entries.flow, "async_init" - ) as mock_config_flow: + with ( + patch("homeassistant.components.usb.comports", return_value=[]), + 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() @@ -842,10 +859,11 @@ async def test_async_is_plugged_in( "pid": "3039", } - with patch("pyudev.Context", side_effect=ImportError), patch( - "homeassistant.components.usb.async_get_usb", return_value=new_usb - ), patch("homeassistant.components.usb.comports", return_value=[]), patch.object( - hass.config_entries.flow, "async_init" + with ( + patch("pyudev.Context", side_effect=ImportError), + patch("homeassistant.components.usb.async_get_usb", return_value=new_usb), + patch("homeassistant.components.usb.comports", return_value=[]), + patch.object(hass.config_entries.flow, "async_init"), ): assert await async_setup_component(hass, "usb", {"usb": {}}) await hass.async_block_till_done() @@ -853,9 +871,10 @@ async def test_async_is_plugged_in( await hass.async_block_till_done() assert not usb.async_is_plugged_in(hass, matcher) - with patch( - "homeassistant.components.usb.comports", return_value=mock_comports - ), patch.object(hass.config_entries.flow, "async_init"): + with ( + patch("homeassistant.components.usb.comports", return_value=mock_comports), + patch.object(hass.config_entries.flow, "async_init"), + ): ws_client = await hass_ws_client(hass) await ws_client.send_json({"id": 1, "type": "usb/scan"}) response = await ws_client.receive_json() @@ -881,10 +900,11 @@ async def test_async_is_plugged_in_case_enforcement( new_usb = [{"domain": "test1", "vid": "ABCD"}] - with patch("pyudev.Context", side_effect=ImportError), patch( - "homeassistant.components.usb.async_get_usb", return_value=new_usb - ), patch("homeassistant.components.usb.comports", return_value=[]), patch.object( - hass.config_entries.flow, "async_init" + with ( + patch("pyudev.Context", side_effect=ImportError), + patch("homeassistant.components.usb.async_get_usb", return_value=new_usb), + patch("homeassistant.components.usb.comports", return_value=[]), + patch.object(hass.config_entries.flow, "async_init"), ): assert await async_setup_component(hass, "usb", {"usb": {}}) await hass.async_block_till_done() @@ -901,10 +921,11 @@ async def test_web_socket_triggers_discovery_request_callbacks( """Test the websocket call triggers a discovery request callback.""" mock_callback = Mock() - with patch("pyudev.Context", side_effect=ImportError), patch( - "homeassistant.components.usb.async_get_usb", return_value=[] - ), patch("homeassistant.components.usb.comports", return_value=[]), patch.object( - hass.config_entries.flow, "async_init" + with ( + patch("pyudev.Context", side_effect=ImportError), + patch("homeassistant.components.usb.async_get_usb", return_value=[]), + patch("homeassistant.components.usb.comports", return_value=[]), + patch.object(hass.config_entries.flow, "async_init"), ): assert await async_setup_component(hass, "usb", {"usb": {}}) await hass.async_block_till_done() @@ -936,10 +957,11 @@ async def test_initial_scan_callback( mock_callback_1 = Mock() mock_callback_2 = Mock() - with patch("pyudev.Context", side_effect=ImportError), patch( - "homeassistant.components.usb.async_get_usb", return_value=[] - ), patch("homeassistant.components.usb.comports", return_value=[]), patch.object( - hass.config_entries.flow, "async_init" + with ( + patch("pyudev.Context", side_effect=ImportError), + patch("homeassistant.components.usb.async_get_usb", return_value=[]), + patch("homeassistant.components.usb.comports", return_value=[]), + patch.object(hass.config_entries.flow, "async_init"), ): assert await async_setup_component(hass, "usb", {"usb": {}}) cancel_1 = usb.async_register_initial_scan_callback(hass, mock_callback_1) @@ -970,10 +992,11 @@ async def test_cancel_initial_scan_callback( """Test it's possible to cancel an initial scan callback.""" mock_callback = Mock() - with patch("pyudev.Context", side_effect=ImportError), patch( - "homeassistant.components.usb.async_get_usb", return_value=[] - ), patch("homeassistant.components.usb.comports", return_value=[]), patch.object( - hass.config_entries.flow, "async_init" + with ( + patch("pyudev.Context", side_effect=ImportError), + patch("homeassistant.components.usb.async_get_usb", return_value=[]), + patch("homeassistant.components.usb.comports", return_value=[]), + patch.object(hass.config_entries.flow, "async_init"), ): assert await async_setup_component(hass, "usb", {"usb": {}}) cancel = usb.async_register_initial_scan_callback(hass, mock_callback) @@ -1006,14 +1029,16 @@ async def test_resolve_serial_by_id( ) ] - with patch("pyudev.Context", side_effect=ImportError), patch( - "homeassistant.components.usb.async_get_usb", return_value=new_usb - ), patch( - "homeassistant.components.usb.comports", return_value=mock_comports - ), patch( - "homeassistant.components.usb.get_serial_by_id", - return_value="/dev/serial/by-id/bla", - ), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow: + with ( + patch("pyudev.Context", side_effect=ImportError), + patch("homeassistant.components.usb.async_get_usb", return_value=new_usb), + patch("homeassistant.components.usb.comports", return_value=mock_comports), + patch( + "homeassistant.components.usb.get_serial_by_id", + return_value="/dev/serial/by-id/bla", + ), + patch.object(hass.config_entries.flow, "async_init") as mock_config_flow, + ): assert await async_setup_component(hass, "usb", {"usb": {}}) await hass.async_block_till_done() hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) diff --git a/tests/components/usgs_earthquakes_feed/test_geo_location.py b/tests/components/usgs_earthquakes_feed/test_geo_location.py index 6307125930c..40d19422ced 100644 --- a/tests/components/usgs_earthquakes_feed/test_geo_location.py +++ b/tests/components/usgs_earthquakes_feed/test_geo_location.py @@ -1,4 +1,5 @@ """The tests for the USGS Earthquake Hazards Program Feed platform.""" + import datetime from unittest.mock import ANY, MagicMock, call, patch @@ -115,9 +116,10 @@ async def test_setup(hass: HomeAssistant) -> None: # Patching 'utcnow' to gain more control over the timed update. utcnow = dt_util.utcnow() - with freeze_time(utcnow), patch( - "aio_geojson_client.feed.GeoJsonFeed.update" - ) as mock_feed_update: + with ( + freeze_time(utcnow), + patch("aio_geojson_client.feed.GeoJsonFeed.update") as mock_feed_update, + ): mock_feed_update.return_value = ( "OK", [mock_entry_1, mock_entry_2, mock_entry_3], @@ -218,12 +220,13 @@ async def test_setup_with_custom_location(hass: HomeAssistant) -> None: # Set up some mock feed entries for this test. mock_entry_1 = _generate_mock_feed_entry("1234", "Title 1", 20.5, (-31.1, 150.1)) - with patch( - "aio_geojson_usgs_earthquakes.feed_manager.UsgsEarthquakeHazardsProgramFeed", - wraps=UsgsEarthquakeHazardsProgramFeed, - ) as mock_feed, patch( - "aio_geojson_client.feed.GeoJsonFeed.update" - ) as mock_feed_update: + with ( + patch( + "aio_geojson_usgs_earthquakes.feed_manager.UsgsEarthquakeHazardsProgramFeed", + wraps=UsgsEarthquakeHazardsProgramFeed, + ) as mock_feed, + patch("aio_geojson_client.feed.GeoJsonFeed.update") as mock_feed_update, + ): mock_feed_update.return_value = "OK", [mock_entry_1] with assert_setup_component(1, geo_location.DOMAIN): diff --git a/tests/components/utility_meter/test_config_flow.py b/tests/components/utility_meter/test_config_flow.py index 75ea6d3a4d2..1bf1c02385d 100644 --- a/tests/components/utility_meter/test_config_flow.py +++ b/tests/components/utility_meter/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Utility Meter config flow.""" + from unittest.mock import patch import pytest @@ -12,7 +13,7 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er from tests.common import MockConfigEntry -@pytest.mark.parametrize("platform", ("sensor",)) +@pytest.mark.parametrize("platform", ["sensor"]) async def test_config_flow(hass: HomeAssistant, platform) -> None: """Test the config flow.""" input_sensor_entity_id = "sensor.input" diff --git a/tests/components/utility_meter/test_init.py b/tests/components/utility_meter/test_init.py index ee0290744ce..a89cbe352a0 100644 --- a/tests/components/utility_meter/test_init.py +++ b/tests/components/utility_meter/test_init.py @@ -1,4 +1,5 @@ """The tests for the utility_meter component.""" + from __future__ import annotations from datetime import timedelta @@ -61,10 +62,10 @@ async def test_restore_state(hass: HomeAssistant) -> None: @pytest.mark.parametrize( "meter", - ( + [ ["select.energy_bill"], "select.energy_bill", - ), + ], ) async def test_services(hass: HomeAssistant, meter) -> None: """Test energy sensor reset service.""" @@ -384,7 +385,7 @@ async def test_setup_missing_discovery(hass: HomeAssistant) -> None: @pytest.mark.parametrize( ("tariffs", "expected_entities"), - ( + [ ( [], ["sensor.electricity_meter"], @@ -397,7 +398,7 @@ async def test_setup_missing_discovery(hass: HomeAssistant) -> None: "select.electricity_meter", ], ), - ), + ], ) async def test_setup_and_remove_config_entry( hass: HomeAssistant, tariffs: str, expected_entities: list[str] diff --git a/tests/components/utility_meter/test_sensor.py b/tests/components/utility_meter/test_sensor.py index fa1e3aa8785..99a63809329 100644 --- a/tests/components/utility_meter/test_sensor.py +++ b/tests/components/utility_meter/test_sensor.py @@ -1,4 +1,5 @@ """The tests for the utility_meter sensor platform.""" + from datetime import timedelta from freezegun import freeze_time @@ -21,6 +22,7 @@ from homeassistant.components.utility_meter.const import ( HOURLY, QUARTER_HOURLY, SERVICE_CALIBRATE_METER, + SERVICE_RESET, ) from homeassistant.components.utility_meter.sensor import ( ATTR_LAST_RESET, @@ -38,6 +40,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, UnitOfEnergy, + UnitOfVolume, ) from homeassistant.core import CoreState, HomeAssistant, State from homeassistant.helpers import device_registry as dr, entity_registry as er @@ -59,7 +62,7 @@ def set_utc(hass: HomeAssistant): @pytest.mark.parametrize( ("yaml_config", "config_entry_config"), - ( + [ ( { "utility_meter": { @@ -84,7 +87,7 @@ def set_utc(hass: HomeAssistant): "tariffs": ["onpeak", "midpeak", "offpeak"], }, ), - ), + ], ) async def test_state(hass: HomeAssistant, yaml_config, config_entry_config) -> None: """Test utility sensor state.""" @@ -233,7 +236,7 @@ async def test_state(hass: HomeAssistant, yaml_config, config_entry_config) -> N @pytest.mark.parametrize( ("yaml_config", "config_entry_config"), - ( + [ ( { "utility_meter": { @@ -259,7 +262,7 @@ async def test_state(hass: HomeAssistant, yaml_config, config_entry_config) -> N "always_available": True, }, ), - ), + ], ) async def test_state_always_available( hass: HomeAssistant, yaml_config, config_entry_config @@ -333,7 +336,7 @@ async def test_state_always_available( @pytest.mark.parametrize( "yaml_config", - ( + [ ( { "utility_meter": { @@ -345,7 +348,7 @@ async def test_state_always_available( }, None, ), - ), + ], ) async def test_not_unique_tariffs(hass: HomeAssistant, yaml_config) -> None: """Test utility sensor state initializtion.""" @@ -354,7 +357,7 @@ async def test_not_unique_tariffs(hass: HomeAssistant, yaml_config) -> None: @pytest.mark.parametrize( ("yaml_config", "config_entry_config"), - ( + [ ( { "utility_meter": { @@ -379,7 +382,7 @@ async def test_not_unique_tariffs(hass: HomeAssistant, yaml_config) -> None: "tariffs": ["onpeak", "midpeak", "offpeak"], }, ), - ), + ], ) async def test_init(hass: HomeAssistant, yaml_config, config_entry_config) -> None: """Test utility sensor state initializtion.""" @@ -454,7 +457,7 @@ async def test_unique_id( @pytest.mark.parametrize( ("yaml_config", "entity_id", "name"), - ( + [ ( { "utility_meter": { @@ -491,7 +494,7 @@ async def test_unique_id( "sensor.energy_bill", "energy_bill", ), - ), + ], ) async def test_entity_name(hass: HomeAssistant, yaml_config, entity_id, name) -> None: """Test utility sensor state initializtion.""" @@ -509,7 +512,7 @@ async def test_entity_name(hass: HomeAssistant, yaml_config, entity_id, name) -> @pytest.mark.parametrize( ("yaml_config", "config_entry_configs"), - ( + [ ( { "utility_meter": { @@ -549,10 +552,68 @@ async def test_entity_name(hass: HomeAssistant, yaml_config, entity_id, name) -> }, ], ), + ], +) +@pytest.mark.parametrize( + ( + "energy_sensor_attributes", + "gas_sensor_attributes", + "energy_meter_attributes", + "gas_meter_attributes", ), + [ + ( + {ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR}, + {ATTR_UNIT_OF_MEASUREMENT: "some_archaic_unit"}, + { + ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, + ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR, + }, + { + ATTR_DEVICE_CLASS: None, + ATTR_UNIT_OF_MEASUREMENT: "some_archaic_unit", + }, + ), + ( + {}, + {}, + { + ATTR_DEVICE_CLASS: None, + ATTR_UNIT_OF_MEASUREMENT: None, + }, + { + ATTR_DEVICE_CLASS: None, + ATTR_UNIT_OF_MEASUREMENT: None, + }, + ), + ( + { + ATTR_DEVICE_CLASS: SensorDeviceClass.GAS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR, + }, + { + ATTR_DEVICE_CLASS: SensorDeviceClass.WATER, + ATTR_UNIT_OF_MEASUREMENT: "some_archaic_unit", + }, + { + ATTR_DEVICE_CLASS: SensorDeviceClass.GAS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR, + }, + { + ATTR_DEVICE_CLASS: SensorDeviceClass.WATER, + ATTR_UNIT_OF_MEASUREMENT: "some_archaic_unit", + }, + ), + ], ) async def test_device_class( - hass: HomeAssistant, yaml_config, config_entry_configs + hass: HomeAssistant, + yaml_config, + config_entry_configs, + energy_sensor_attributes, + gas_sensor_attributes, + energy_meter_attributes, + gas_meter_attributes, ) -> None: """Test utility device_class.""" if yaml_config: @@ -577,38 +638,40 @@ async def test_device_class( await hass.async_block_till_done() - hass.states.async_set( - entity_id_energy, 2, {ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR} - ) - hass.states.async_set( - entity_id_gas, 2, {ATTR_UNIT_OF_MEASUREMENT: "some_archaic_unit"} - ) + hass.states.async_set(entity_id_energy, 2, energy_sensor_attributes) + hass.states.async_set(entity_id_gas, 2, gas_sensor_attributes) await hass.async_block_till_done() state = hass.states.get("sensor.energy_meter") assert state is not None assert state.state == "0" - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.KILO_WATT_HOUR + for attr, value in energy_meter_attributes.items(): + assert state.attributes.get(attr) == value state = hass.states.get("sensor.gas_meter") assert state is not None assert state.state == "0" - assert state.attributes.get(ATTR_DEVICE_CLASS) is None assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL_INCREASING - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "some_archaic_unit" + for attr, value in gas_meter_attributes.items(): + assert state.attributes.get(attr) == value @pytest.mark.parametrize( ("yaml_config", "config_entry_config"), - ( + [ ( { "utility_meter": { "energy_bill": { "source": "sensor.energy", - "tariffs": ["onpeak", "midpeak", "offpeak", "superpeak"], + "tariffs": [ + "tariff0", + "tariff1", + "tariff2", + "tariff3", + "tariff4", + ], } } }, @@ -624,10 +687,16 @@ async def test_device_class( "offset": 0, "periodically_resetting": True, "source": "sensor.energy", - "tariffs": ["onpeak", "midpeak", "offpeak", "superpeak"], + "tariffs": [ + "tariff0", + "tariff1", + "tariff2", + "tariff3", + "tariff4", + ], }, ), - ), + ], ) async def test_restore_state( hass: HomeAssistant, yaml_config, config_entry_config @@ -636,82 +705,115 @@ async def test_restore_state( # Home assistant is not runnit yet hass.set_state(CoreState.not_running) - last_reset = "2020-12-21T00:00:00.013073+00:00" + last_reset_1 = "2020-12-21T00:00:00.013073+00:00" + last_reset_2 = "2020-12-22T00:00:00.013073+00:00" mock_restore_cache_with_extra_data( hass, [ + # sensor.energy_bill_tariff0 is restored as expected, including device + # class ( State( - "sensor.energy_bill_onpeak", - "3", + "sensor.energy_bill_tariff0", + "0.1", attributes={ ATTR_STATUS: PAUSED, - ATTR_LAST_RESET: last_reset, - ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR, + ATTR_LAST_RESET: last_reset_1, + ATTR_UNIT_OF_MEASUREMENT: UnitOfVolume.CUBIC_METERS, }, ), { "native_value": { "__type": "", - "decimal_str": "3", + "decimal_str": "0.2", + }, + "native_unit_of_measurement": "gal", + "last_reset": last_reset_2, + "last_period": "1.3", + "last_valid_state": None, + "status": "collecting", + "input_device_class": "water", + }, + ), + # sensor.energy_bill_tariff1 is restored as expected, except device + # class + ( + State( + "sensor.energy_bill_tariff1", + "1.1", + attributes={ + ATTR_STATUS: PAUSED, + ATTR_LAST_RESET: last_reset_1, + ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.MEGA_WATT_HOUR, + }, + ), + { + "native_value": { + "__type": "", + "decimal_str": "1.2", }, "native_unit_of_measurement": "kWh", - "last_reset": last_reset, - "last_period": "7", - "last_valid_state": "None", + "last_reset": last_reset_2, + "last_period": "1.3", + "last_valid_state": None, "status": "paused", }, ), + # sensor.energy_bill_tariff2 has missing keys and falls back to + # saved state ( State( - "sensor.energy_bill_midpeak", - "5", + "sensor.energy_bill_tariff2", + "2.1", attributes={ ATTR_STATUS: PAUSED, - ATTR_LAST_RESET: last_reset, + ATTR_LAST_RESET: last_reset_1, ATTR_LAST_VALID_STATE: None, - ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR, + ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.MEGA_WATT_HOUR, }, ), { "native_value": { "__type": "", - "decimal_str": "3", + "decimal_str": "2.2", }, "native_unit_of_measurement": "kWh", "last_valid_state": "None", }, ), + # sensor.energy_bill_tariff3 has invalid data and falls back to + # saved state ( State( - "sensor.energy_bill_offpeak", - "6", + "sensor.energy_bill_tariff3", + "3.1", attributes={ ATTR_STATUS: COLLECTING, - ATTR_LAST_RESET: last_reset, + ATTR_LAST_RESET: last_reset_1, ATTR_LAST_VALID_STATE: None, - ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR, + ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.MEGA_WATT_HOUR, }, ), { "native_value": { "__type": "", - "decimal_str": "3f", + "decimal_str": "3f", # Invalid }, "native_unit_of_measurement": "kWh", "last_valid_state": "None", }, ), + # No extra saved data, fall back to saved state ( State( - "sensor.energy_bill_superpeak", + "sensor.energy_bill_tariff4", "error", attributes={ ATTR_STATUS: COLLECTING, - ATTR_LAST_RESET: last_reset, + ATTR_LAST_RESET: last_reset_1, ATTR_LAST_VALID_STATE: None, - ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR, + ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.MEGA_WATT_HOUR, }, ), {}, @@ -734,46 +836,156 @@ async def test_restore_state( await hass.async_block_till_done() # restore from cache - state = hass.states.get("sensor.energy_bill_onpeak") - assert state.state == "3" - assert state.attributes.get("status") == PAUSED - assert state.attributes.get("last_reset") == last_reset - assert state.attributes.get("last_valid_state") == "None" - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.KILO_WATT_HOUR - - state = hass.states.get("sensor.energy_bill_midpeak") - assert state.state == "5" - assert state.attributes.get("last_valid_state") == "None" - - state = hass.states.get("sensor.energy_bill_offpeak") - assert state.state == "6" + state = hass.states.get("sensor.energy_bill_tariff0") + assert state.state == "0.2" assert state.attributes.get("status") == COLLECTING - assert state.attributes.get("last_reset") == last_reset + assert state.attributes.get("last_reset") == last_reset_2 + assert state.attributes.get("last_valid_state") == "None" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfVolume.GALLONS + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.WATER + + state = hass.states.get("sensor.energy_bill_tariff1") + assert state.state == "1.2" + assert state.attributes.get("status") == PAUSED + assert state.attributes.get("last_reset") == last_reset_2 assert state.attributes.get("last_valid_state") == "None" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.KILO_WATT_HOUR + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY - state = hass.states.get("sensor.energy_bill_superpeak") + state = hass.states.get("sensor.energy_bill_tariff2") + assert state.state == "2.1" + assert state.attributes.get("status") == PAUSED + assert state.attributes.get("last_reset") == last_reset_1 + assert state.attributes.get("last_valid_state") == "None" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.MEGA_WATT_HOUR + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY + + state = hass.states.get("sensor.energy_bill_tariff3") + assert state.state == "3.1" + assert state.attributes.get("status") == COLLECTING + assert state.attributes.get("last_reset") == last_reset_1 + assert state.attributes.get("last_valid_state") == "None" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.MEGA_WATT_HOUR + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY + + state = hass.states.get("sensor.energy_bill_tariff4") assert state.state == STATE_UNKNOWN # utility_meter is loaded, now set sensors according to utility_meter: hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) - await hass.async_block_till_done() state = hass.states.get("select.energy_bill") - assert state.state == "onpeak" + assert state.state == "tariff0" - state = hass.states.get("sensor.energy_bill_onpeak") + state = hass.states.get("sensor.energy_bill_tariff0") assert state.attributes.get("status") == COLLECTING - state = hass.states.get("sensor.energy_bill_offpeak") - assert state.attributes.get("status") == PAUSED + for entity_id in ( + "sensor.energy_bill_tariff1", + "sensor.energy_bill_tariff2", + "sensor.energy_bill_tariff3", + "sensor.energy_bill_tariff4", + ): + state = hass.states.get(entity_id) + assert state.attributes.get("status") == PAUSED @pytest.mark.parametrize( ("yaml_config", "config_entry_config"), - ( + [ + ( + { + "utility_meter": { + "energy_bill": { + "source": "sensor.energy", + } + } + }, + None, + ), + ( + None, + { + "cycle": "none", + "delta_values": False, + "name": "Energy bill", + "net_consumption": False, + "offset": 0, + "periodically_resetting": True, + "source": "sensor.energy", + "tariffs": [], + }, + ), + ], +) +async def test_service_reset_no_tariffs( + hass: HomeAssistant, yaml_config, config_entry_config +) -> None: + """Test utility sensor service reset for sensor with no tariffs.""" + # Home assistant is not runnit yet + hass.state = CoreState.not_running + last_reset = "2023-10-01T00:00:00+00:00" + + mock_restore_cache_with_extra_data( + hass, + [ + ( + State( + "sensor.energy_bill", + "3", + attributes={ + ATTR_LAST_RESET: last_reset, + }, + ), + {}, + ), + ], + ) + + if yaml_config: + assert await async_setup_component(hass, DOMAIN, yaml_config) + await hass.async_block_till_done() + else: + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options=config_entry_config, + title=config_entry_config["name"], + ) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get("sensor.energy_bill") + assert state + assert state.state == "3" + assert state.attributes.get("last_reset") == last_reset + assert state.attributes.get("last_period") == "0" + + now = dt_util.utcnow() + with freeze_time(now): + await hass.services.async_call( + domain=DOMAIN, + service=SERVICE_RESET, + service_data={}, + target={"entity_id": "sensor.energy_bill"}, + blocking=True, + ) + + await hass.async_block_till_done() + + state = hass.states.get("sensor.energy_bill") + assert state + assert state.state == "0" + assert state.attributes.get("last_reset") == now.isoformat() + assert state.attributes.get("last_period") == "3" + + +@pytest.mark.parametrize( + ("yaml_config", "config_entry_config"), + [ ( { "utility_meter": { @@ -798,7 +1010,7 @@ async def test_restore_state( "tariffs": [], }, ), - ), + ], ) async def test_net_consumption( hass: HomeAssistant, yaml_config, config_entry_config @@ -845,7 +1057,7 @@ async def test_net_consumption( @pytest.mark.parametrize( ("yaml_config", "config_entry_config"), - ( + [ ( { "utility_meter": { @@ -869,7 +1081,7 @@ async def test_net_consumption( "tariffs": [], }, ), - ), + ], ) async def test_non_net_consumption( hass: HomeAssistant, @@ -930,7 +1142,7 @@ async def test_non_net_consumption( @pytest.mark.parametrize( ("yaml_config", "config_entry_config"), - ( + [ ( { "utility_meter": { @@ -955,7 +1167,7 @@ async def test_non_net_consumption( "tariffs": [], }, ), - ), + ], ) async def test_delta_values( hass: HomeAssistant, @@ -1042,7 +1254,7 @@ async def test_delta_values( @pytest.mark.parametrize( ("yaml_config", "config_entry_config"), - ( + [ ( { "utility_meter": { @@ -1067,7 +1279,7 @@ async def test_delta_values( "tariffs": [], }, ), - ), + ], ) async def test_non_periodically_resetting( hass: HomeAssistant, yaml_config, config_entry_config @@ -1174,7 +1386,7 @@ async def test_non_periodically_resetting( @pytest.mark.parametrize( ("yaml_config", "config_entry_config"), - ( + [ ( { "utility_meter": { @@ -1200,7 +1412,7 @@ async def test_non_periodically_resetting( "tariffs": ["low", "high"], }, ), - ), + ], ) async def test_non_periodically_resetting_meter_with_tariffs( hass: HomeAssistant, yaml_config, config_entry_config diff --git a/tests/components/uvc/test_camera.py b/tests/components/uvc/test_camera.py index dd42cfc2977..522448ecfc4 100644 --- a/tests/components/uvc/test_camera.py +++ b/tests/components/uvc/test_camera.py @@ -1,4 +1,5 @@ """The tests for UVC camera module.""" + from datetime import UTC, datetime, timedelta from unittest.mock import call, patch @@ -277,7 +278,7 @@ async def test_setup_nvr_errors_during_indexing( mock_remote.return_value.index.side_effect = None async_fire_time_changed(hass, now + timedelta(seconds=31)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) camera_states = hass.states.async_all("camera") @@ -312,7 +313,7 @@ async def test_setup_nvr_errors_during_initialization( mock_remote.side_effect = None async_fire_time_changed(hass, now + timedelta(seconds=31)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) camera_states = hass.states.async_all("camera") @@ -361,7 +362,7 @@ async def test_motion_recording_mode_properties( ] = True async_fire_time_changed(hass, now + timedelta(seconds=31)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get("camera.front") @@ -374,31 +375,31 @@ async def test_motion_recording_mode_properties( mock_remote.return_value.get_camera.return_value["recordingIndicator"] = "DISABLED" async_fire_time_changed(hass, now + timedelta(seconds=61)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get("camera.front") assert state assert state.state != STATE_RECORDING - mock_remote.return_value.get_camera.return_value[ - "recordingIndicator" - ] = "MOTION_INPROGRESS" + mock_remote.return_value.get_camera.return_value["recordingIndicator"] = ( + "MOTION_INPROGRESS" + ) async_fire_time_changed(hass, now + timedelta(seconds=91)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get("camera.front") assert state assert state.state == STATE_RECORDING - mock_remote.return_value.get_camera.return_value[ - "recordingIndicator" - ] = "MOTION_FINISHED" + mock_remote.return_value.get_camera.return_value["recordingIndicator"] = ( + "MOTION_FINISHED" + ) async_fire_time_changed(hass, now + timedelta(seconds=121)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get("camera.front") @@ -471,7 +472,7 @@ async def test_login_tries_both_addrs_and_caches( """Mock get snapshots.""" try: snapshots.pop(0) - raise camera.CameraAuthError() + raise camera.CameraAuthError except IndexError: pass return "test_image" diff --git a/tests/components/v2c/conftest.py b/tests/components/v2c/conftest.py index 85831b607b7..2bdfc405e2d 100644 --- a/tests/components/v2c/conftest.py +++ b/tests/components/v2c/conftest.py @@ -1,4 +1,5 @@ """Common fixtures for the V2C tests.""" + from collections.abc import Generator from unittest.mock import AsyncMock, patch diff --git a/tests/components/v2c/test_config_flow.py b/tests/components/v2c/test_config_flow.py index 50bc4ca91bf..a0657fa0c7c 100644 --- a/tests/components/v2c/test_config_flow.py +++ b/tests/components/v2c/test_config_flow.py @@ -1,4 +1,5 @@ """Test the V2C config flow.""" + from unittest.mock import AsyncMock, patch import pytest diff --git a/tests/components/vacuum/common.py b/tests/components/vacuum/common.py index 6cecbda9968..0e46ebf5e44 100644 --- a/tests/components/vacuum/common.py +++ b/tests/components/vacuum/common.py @@ -3,6 +3,7 @@ All containing methods are legacy helpers that should not be used by new components. Instead call the service directly. """ + from homeassistant.components.vacuum import ( ATTR_FAN_SPEED, ATTR_PARAMS, diff --git a/tests/components/vacuum/test_device_action.py b/tests/components/vacuum/test_device_action.py index cf0ab3c20d8..fe5b2814a33 100644 --- a/tests/components/vacuum/test_device_action.py +++ b/tests/components/vacuum/test_device_action.py @@ -1,4 +1,5 @@ """The tests for Vacuum device actions.""" + import pytest from pytest_unordered import unordered @@ -56,12 +57,12 @@ async def test_get_actions( @pytest.mark.parametrize( ("hidden_by", "entity_category"), - ( + [ (RegistryEntryHider.INTEGRATION, None), (RegistryEntryHider.USER, None), (None, EntityCategory.CONFIG), (None, EntityCategory.DIAGNOSTIC), - ), + ], ) async def test_get_actions_hidden_auxiliary( hass: HomeAssistant, diff --git a/tests/components/vacuum/test_device_condition.py b/tests/components/vacuum/test_device_condition.py index a2ba75cc752..f8d1368a163 100644 --- a/tests/components/vacuum/test_device_condition.py +++ b/tests/components/vacuum/test_device_condition.py @@ -1,4 +1,5 @@ """The tests for Vacuum device conditions.""" + import pytest from pytest_unordered import unordered @@ -68,12 +69,12 @@ async def test_get_conditions( @pytest.mark.parametrize( ("hidden_by", "entity_category"), - ( + [ (RegistryEntryHider.INTEGRATION, None), (RegistryEntryHider.USER, None), (None, EntityCategory.CONFIG), (None, EntityCategory.DIAGNOSTIC), - ), + ], ) async def test_get_conditions_hidden_auxiliary( hass: HomeAssistant, diff --git a/tests/components/vacuum/test_device_trigger.py b/tests/components/vacuum/test_device_trigger.py index 605dd6e5b9f..831d6807b8c 100644 --- a/tests/components/vacuum/test_device_trigger.py +++ b/tests/components/vacuum/test_device_trigger.py @@ -1,4 +1,5 @@ """The tests for Vacuum device triggers.""" + from datetime import timedelta import pytest @@ -68,12 +69,12 @@ async def test_get_triggers( @pytest.mark.parametrize( ("hidden_by", "entity_category"), - ( + [ (RegistryEntryHider.INTEGRATION, None), (RegistryEntryHider.USER, None), (None, EntityCategory.CONFIG), (None, EntityCategory.DIAGNOSTIC), - ), + ], ) async def test_get_triggers_hidden_auxiliary( hass: HomeAssistant, diff --git a/tests/components/vacuum/test_init.py b/tests/components/vacuum/test_init.py index 0da4470c762..7a42913afbf 100644 --- a/tests/components/vacuum/test_init.py +++ b/tests/components/vacuum/test_init.py @@ -1,4 +1,5 @@ """The tests for the Vacuum entity integration.""" + from __future__ import annotations from homeassistant.components.vacuum import StateVacuumEntity, VacuumEntityFeature diff --git a/tests/components/vacuum/test_recorder.py b/tests/components/vacuum/test_recorder.py index 3694f0b5803..c01c47acae0 100644 --- a/tests/components/vacuum/test_recorder.py +++ b/tests/components/vacuum/test_recorder.py @@ -1,4 +1,5 @@ """The tests for vacuum recorder.""" + from __future__ import annotations from datetime import timedelta @@ -8,7 +9,7 @@ from homeassistant.components.recorder import Recorder from homeassistant.components.recorder.history import get_significant_states from homeassistant.components.vacuum import ATTR_FAN_SPEED_LIST from homeassistant.const import ATTR_FRIENDLY_NAME -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, split_entity_id from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util @@ -31,7 +32,11 @@ async def test_exclude_attributes(recorder_mock: Recorder, hass: HomeAssistant) get_significant_states, hass, now, None, hass.states.async_entity_ids() ) assert len(states) >= 1 - for entity_states in states.values(): - for state in entity_states: - assert ATTR_FAN_SPEED_LIST not in state.attributes - assert ATTR_FRIENDLY_NAME in state.attributes + for state in ( + state + for entity_states in states.values() + for state in entity_states + if split_entity_id(state.entity_id)[0] == vacuum.DOMAIN + ): + assert ATTR_FAN_SPEED_LIST not in state.attributes + assert ATTR_FRIENDLY_NAME in state.attributes diff --git a/tests/components/vacuum/test_reproduce_state.py b/tests/components/vacuum/test_reproduce_state.py index 2f7817a3b9e..ff8da28e98c 100644 --- a/tests/components/vacuum/test_reproduce_state.py +++ b/tests/components/vacuum/test_reproduce_state.py @@ -1,4 +1,5 @@ """Test reproduce state for Vacuum.""" + import pytest from homeassistant.components.vacuum import ( diff --git a/tests/components/vacuum/test_significant_change.py b/tests/components/vacuum/test_significant_change.py index 5f46080fb8d..f4def0ccbf6 100644 --- a/tests/components/vacuum/test_significant_change.py +++ b/tests/components/vacuum/test_significant_change.py @@ -1,4 +1,5 @@ """Test the Vacuum significant change platform.""" + import pytest from homeassistant.components.vacuum import ( diff --git a/tests/components/vallox/conftest.py b/tests/components/vallox/conftest.py index 25d631b4351..08c020c1982 100644 --- a/tests/components/vallox/conftest.py +++ b/tests/components/vallox/conftest.py @@ -104,3 +104,8 @@ def patch_set_fan_speed(): def patch_set_values(): """Patch the Vallox metrics set values.""" return patch("homeassistant.components.vallox.Vallox.set_values") + + +def patch_set_filter_change_date(): + """Patch the Vallox metrics set filter change date.""" + return patch("homeassistant.components.vallox.Vallox.set_filter_change_date") diff --git a/tests/components/vallox/test_binary_sensor.py b/tests/components/vallox/test_binary_sensor.py index 4add40f6a45..b8915e50e20 100644 --- a/tests/components/vallox/test_binary_sensor.py +++ b/tests/components/vallox/test_binary_sensor.py @@ -1,4 +1,5 @@ """Tests for Vallox binary sensor platform.""" + from typing import Any import pytest diff --git a/tests/components/vallox/test_config_flow.py b/tests/components/vallox/test_config_flow.py index 3ded98f2307..00c11854fe2 100644 --- a/tests/components/vallox/test_config_flow.py +++ b/tests/components/vallox/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Vallox integration config flow.""" + from unittest.mock import patch from vallox_websocket_api import ValloxApiException, ValloxWebsocketException @@ -32,13 +33,16 @@ async def test_form_create_entry(hass: HomeAssistant) -> None: assert init["type"] == FlowResultType.FORM assert init["errors"] is None - with patch( - "homeassistant.components.vallox.config_flow.Vallox.fetch_metric_data", - return_value=None, - ), patch( - "homeassistant.components.vallox.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.vallox.config_flow.Vallox.fetch_metric_data", + return_value=None, + ), + patch( + "homeassistant.components.vallox.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result = await hass.config_entries.flow.async_configure( init["flow_id"], {"host": "1.2.3.4"}, diff --git a/tests/components/vallox/test_date.py b/tests/components/vallox/test_date.py new file mode 100644 index 00000000000..1572e9b205c --- /dev/null +++ b/tests/components/vallox/test_date.py @@ -0,0 +1,50 @@ +"""Tests for Vallox date platform.""" + +from datetime import date + +from vallox_websocket_api import MetricData + +from homeassistant.components.date.const import DOMAIN as DATE_DOMAIN, SERVICE_SET_VALUE +from homeassistant.const import ATTR_DATE, ATTR_ENTITY_ID +from homeassistant.core import HomeAssistant + +from .conftest import patch_set_filter_change_date + +from tests.common import MockConfigEntry + + +async def test_set_filter_change_date( + mock_entry: MockConfigEntry, + hass: HomeAssistant, + setup_fetch_metric_data_mock, +) -> None: + """Test set filter change date.""" + + entity_id = "date.vallox_filter_change_date" + + class MockMetricData(MetricData): + @property + def filter_change_date(self): + return date(2024, 1, 1) + + setup_fetch_metric_data_mock(metric_data_class=MockMetricData) + + with patch_set_filter_change_date() as set_filter_change_date: + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state + + assert state.state == "2024-01-01" + + await hass.services.async_call( + DATE_DOMAIN, + SERVICE_SET_VALUE, + service_data={ + ATTR_ENTITY_ID: entity_id, + ATTR_DATE: "2024-02-25", + }, + ) + await hass.async_block_till_done() + set_filter_change_date.assert_called_once_with(date(2024, 2, 25)) diff --git a/tests/components/vallox/test_fan.py b/tests/components/vallox/test_fan.py index e817bfbe89a..03ca3bca365 100644 --- a/tests/components/vallox/test_fan.py +++ b/tests/components/vallox/test_fan.py @@ -1,4 +1,5 @@ """Tests for Vallox fan platform.""" + from unittest.mock import call import pytest diff --git a/tests/components/vallox/test_number.py b/tests/components/vallox/test_number.py index 29de3d54d1c..2e440c5e304 100644 --- a/tests/components/vallox/test_number.py +++ b/tests/components/vallox/test_number.py @@ -1,4 +1,5 @@ """Tests for Vallox number platform.""" + import pytest from homeassistant.components.number.const import ( diff --git a/tests/components/valve/snapshots/test_init.ambr b/tests/components/valve/snapshots/test_init.ambr index b46d76b6f0c..815f902afad 100644 --- a/tests/components/valve/snapshots/test_init.ambr +++ b/tests/components/valve/snapshots/test_init.ambr @@ -8,6 +8,7 @@ 'context': , 'entity_id': 'valve.valve', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'open', }) @@ -22,6 +23,7 @@ 'context': , 'entity_id': 'valve.valve_2', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'open', }) @@ -36,6 +38,7 @@ 'context': , 'entity_id': 'valve.valve', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unavailable', }) @@ -50,6 +53,7 @@ 'context': , 'entity_id': 'valve.valve_2', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unavailable', }) diff --git a/tests/components/valve/test_init.py b/tests/components/valve/test_init.py index 6f5c49830bb..a00d975f0eb 100644 --- a/tests/components/valve/test_init.py +++ b/tests/components/valve/test_init.py @@ -1,4 +1,5 @@ """The tests for Valve.""" + from collections.abc import Generator import pytest @@ -117,7 +118,7 @@ class MockBinaryValveEntity(ValveEntity): self._attr_is_closed = False def close_valve(self) -> None: - """Mock implementantion for sync close function.""" + """Mock implementation for sync close function.""" self._attr_is_closed = True @@ -297,7 +298,7 @@ async def test_valve_report_position(hass: HomeAssistant) -> None: default_valve.hass = hass with pytest.raises(ValueError): - default_valve.reports_position + _ = default_valve.reports_position second_valve = MockValveEntity(reports_position=True) second_valve.hass = hass diff --git a/tests/components/velbus/conftest.py b/tests/components/velbus/conftest.py index c5db7f0ec73..f393ebb819d 100644 --- a/tests/components/velbus/conftest.py +++ b/tests/components/velbus/conftest.py @@ -1,4 +1,5 @@ """Fixtures for the Velbus tests.""" + from collections.abc import Generator from unittest.mock import MagicMock, patch diff --git a/tests/components/velbus/const.py b/tests/components/velbus/const.py index 374dbce2529..427ed2e42b7 100644 --- a/tests/components/velbus/const.py +++ b/tests/components/velbus/const.py @@ -1,3 +1,4 @@ """Constants for the Velbus tests.""" + PORT_SERIAL = "/dev/ttyACME100" PORT_TCP = "127.0.1.0.1:3788" diff --git a/tests/components/velbus/test_config_flow.py b/tests/components/velbus/test_config_flow.py index 6b491c93657..4e3eeaf0fb8 100644 --- a/tests/components/velbus/test_config_flow.py +++ b/tests/components/velbus/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for the Velbus config flow.""" + from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch diff --git a/tests/components/velbus/test_init.py b/tests/components/velbus/test_init.py index 0a1a727abcf..08efdd0410b 100644 --- a/tests/components/velbus/test_init.py +++ b/tests/components/velbus/test_init.py @@ -1,4 +1,5 @@ """Tests for the Velbus component initialisation.""" + from unittest.mock import patch import pytest diff --git a/tests/components/velux/conftest.py b/tests/components/velux/conftest.py index 60144d7137c..a3ebaf51d7a 100644 --- a/tests/components/velux/conftest.py +++ b/tests/components/velux/conftest.py @@ -1,4 +1,5 @@ """Configuration for Velux tests.""" + from collections.abc import Generator from unittest.mock import AsyncMock, patch diff --git a/tests/components/velux/test_config_flow.py b/tests/components/velux/test_config_flow.py index f44debcb892..816dbf95420 100644 --- a/tests/components/velux/test_config_flow.py +++ b/tests/components/velux/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Velux config flow.""" + from __future__ import annotations from copy import deepcopy diff --git a/tests/components/venstar/__init__.py b/tests/components/venstar/__init__.py index f91f8f28bdf..116a3be0925 100644 --- a/tests/components/venstar/__init__.py +++ b/tests/components/venstar/__init__.py @@ -1,4 +1,5 @@ """Tests for the venstar integration.""" + from requests import RequestException diff --git a/tests/components/venstar/test_climate.py b/tests/components/venstar/test_climate.py index d7c28b953cc..c090fadb445 100644 --- a/tests/components/venstar/test_climate.py +++ b/tests/components/venstar/test_climate.py @@ -1,4 +1,5 @@ """The climate tests for the venstar integration.""" + from unittest.mock import patch from homeassistant.components.climate import ClimateEntityFeature diff --git a/tests/components/venstar/test_config_flow.py b/tests/components/venstar/test_config_flow.py index 521e5a8512e..077f87975f0 100644 --- a/tests/components/venstar/test_config_flow.py +++ b/tests/components/venstar/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Venstar config flow.""" + import logging from unittest.mock import patch @@ -39,13 +40,16 @@ async def test_form(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.FORM assert result["errors"] == {} - with patch( - "homeassistant.components.venstar.config_flow.VenstarColorTouch.update_info", - new=VenstarColorTouchMock.update_info, - ), patch( - "homeassistant.components.venstar.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.venstar.config_flow.VenstarColorTouch.update_info", + new=VenstarColorTouchMock.update_info, + ), + patch( + "homeassistant.components.venstar.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], TEST_DATA, @@ -106,12 +110,15 @@ async def test_already_configured(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" - with patch( - "homeassistant.components.venstar.VenstarColorTouch.update_info", - new=VenstarColorTouchMock.update_info, - ), patch( - "homeassistant.components.venstar.async_setup_entry", - return_value=True, + with ( + patch( + "homeassistant.components.venstar.VenstarColorTouch.update_info", + new=VenstarColorTouchMock.update_info, + ), + patch( + "homeassistant.components.venstar.async_setup_entry", + return_value=True, + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], diff --git a/tests/components/venstar/test_init.py b/tests/components/venstar/test_init.py index 2a09f31f399..75250b52f5b 100644 --- a/tests/components/venstar/test_init.py +++ b/tests/components/venstar/test_init.py @@ -1,4 +1,5 @@ """Tests of the initialization of the venstar integration.""" + from unittest.mock import patch from homeassistant.components.venstar.const import DOMAIN as VENSTAR_DOMAIN @@ -24,24 +25,31 @@ async def test_setup_entry(hass: HomeAssistant) -> None: ) config_entry.add_to_hass(hass) - with patch( - "homeassistant.components.venstar.VenstarColorTouch._request", - new=VenstarColorTouchMock._request, - ), patch( - "homeassistant.components.venstar.VenstarColorTouch.update_sensors", - new=VenstarColorTouchMock.update_sensors, - ), patch( - "homeassistant.components.venstar.VenstarColorTouch.update_info", - new=VenstarColorTouchMock.update_info, - ), patch( - "homeassistant.components.venstar.VenstarColorTouch.update_alerts", - new=VenstarColorTouchMock.update_alerts, - ), patch( - "homeassistant.components.venstar.VenstarColorTouch.get_runtimes", - new=VenstarColorTouchMock.get_runtimes, - ), patch( - "homeassistant.components.venstar.VENSTAR_SLEEP", - new=0, + with ( + patch( + "homeassistant.components.venstar.VenstarColorTouch._request", + new=VenstarColorTouchMock._request, + ), + patch( + "homeassistant.components.venstar.VenstarColorTouch.update_sensors", + new=VenstarColorTouchMock.update_sensors, + ), + patch( + "homeassistant.components.venstar.VenstarColorTouch.update_info", + new=VenstarColorTouchMock.update_info, + ), + patch( + "homeassistant.components.venstar.VenstarColorTouch.update_alerts", + new=VenstarColorTouchMock.update_alerts, + ), + patch( + "homeassistant.components.venstar.VenstarColorTouch.get_runtimes", + new=VenstarColorTouchMock.get_runtimes, + ), + patch( + "homeassistant.components.venstar.VENSTAR_SLEEP", + new=0, + ), ): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -64,21 +72,27 @@ async def test_setup_entry_exception(hass: HomeAssistant) -> None: ) config_entry.add_to_hass(hass) - with patch( - "homeassistant.components.venstar.VenstarColorTouch._request", - new=VenstarColorTouchMock._request, - ), patch( - "homeassistant.components.venstar.VenstarColorTouch.update_sensors", - new=VenstarColorTouchMock.update_sensors, - ), patch( - "homeassistant.components.venstar.VenstarColorTouch.update_info", - new=VenstarColorTouchMock.broken_update_info, - ), patch( - "homeassistant.components.venstar.VenstarColorTouch.update_alerts", - new=VenstarColorTouchMock.update_alerts, - ), patch( - "homeassistant.components.venstar.VenstarColorTouch.get_runtimes", - new=VenstarColorTouchMock.get_runtimes, + with ( + patch( + "homeassistant.components.venstar.VenstarColorTouch._request", + new=VenstarColorTouchMock._request, + ), + patch( + "homeassistant.components.venstar.VenstarColorTouch.update_sensors", + new=VenstarColorTouchMock.update_sensors, + ), + patch( + "homeassistant.components.venstar.VenstarColorTouch.update_info", + new=VenstarColorTouchMock.broken_update_info, + ), + patch( + "homeassistant.components.venstar.VenstarColorTouch.update_alerts", + new=VenstarColorTouchMock.update_alerts, + ), + patch( + "homeassistant.components.venstar.VenstarColorTouch.get_runtimes", + new=VenstarColorTouchMock.get_runtimes, + ), ): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/venstar/util.py b/tests/components/venstar/util.py index 23e480272d0..369d3332135 100644 --- a/tests/components/venstar/util.py +++ b/tests/components/venstar/util.py @@ -47,14 +47,13 @@ async def async_init_integration( skip_setup: bool = False, ): """Set up the venstar integration in Home Assistant.""" - platform_config = [] - for model in TEST_MODELS: - platform_config.append( - { - CONF_PLATFORM: "venstar", - CONF_HOST: f"venstar-{model}.localdomain", - } - ) + platform_config = [ + { + CONF_PLATFORM: "venstar", + CONF_HOST: f"venstar-{model}.localdomain", + } + for model in TEST_MODELS + ] config = {DOMAIN: platform_config} await async_setup_component(hass, DOMAIN, config) diff --git a/tests/components/vera/common.py b/tests/components/vera/common.py index df753713a40..b6be60927cf 100644 --- a/tests/components/vera/common.py +++ b/tests/components/vera/common.py @@ -1,4 +1,5 @@ """Common code for tests.""" + from __future__ import annotations from collections.abc import Callable diff --git a/tests/components/vera/conftest.py b/tests/components/vera/conftest.py index 2b1c65a9af1..7169765c4f2 100644 --- a/tests/components/vera/conftest.py +++ b/tests/components/vera/conftest.py @@ -1,4 +1,5 @@ """Fixtures for tests.""" + from unittest.mock import patch import pytest diff --git a/tests/components/vera/test_binary_sensor.py b/tests/components/vera/test_binary_sensor.py index 58b8ed7f461..76795ef86cf 100644 --- a/tests/components/vera/test_binary_sensor.py +++ b/tests/components/vera/test_binary_sensor.py @@ -1,4 +1,5 @@ """Vera tests.""" + from unittest.mock import MagicMock import pyvera as pv diff --git a/tests/components/vera/test_climate.py b/tests/components/vera/test_climate.py index 9e5c4b607fe..f158dc9eec4 100644 --- a/tests/components/vera/test_climate.py +++ b/tests/components/vera/test_climate.py @@ -1,4 +1,5 @@ """Vera tests.""" + from unittest.mock import MagicMock import pyvera as pv diff --git a/tests/components/vera/test_common.py b/tests/components/vera/test_common.py index 100a788313d..57a220493fd 100644 --- a/tests/components/vera/test_common.py +++ b/tests/components/vera/test_common.py @@ -1,4 +1,5 @@ """Tests for common vera code.""" + from datetime import timedelta from unittest.mock import MagicMock diff --git a/tests/components/vera/test_config_flow.py b/tests/components/vera/test_config_flow.py index ae85019c44c..2262347450d 100644 --- a/tests/components/vera/test_config_flow.py +++ b/tests/components/vera/test_config_flow.py @@ -1,4 +1,5 @@ """Vera tests.""" + from unittest.mock import MagicMock, patch from requests.exceptions import RequestException diff --git a/tests/components/vera/test_cover.py b/tests/components/vera/test_cover.py index 15b95b55e39..3549add6d38 100644 --- a/tests/components/vera/test_cover.py +++ b/tests/components/vera/test_cover.py @@ -1,4 +1,5 @@ """Vera tests.""" + from unittest.mock import MagicMock import pyvera as pv diff --git a/tests/components/vera/test_init.py b/tests/components/vera/test_init.py index 23fb588d197..666af780283 100644 --- a/tests/components/vera/test_init.py +++ b/tests/components/vera/test_init.py @@ -1,4 +1,5 @@ """Vera tests.""" + from unittest.mock import MagicMock import pytest @@ -228,7 +229,7 @@ async def test_exclude_and_light_ids( controller_config=new_simple_controller_config( config_source=ConfigSource.CONFIG_ENTRY, devices=(vera_device1, vera_device2, vera_device3, vera_device4), - config={**{CONF_CONTROLLER: "http://127.0.0.1:123"}, **options}, + config={CONF_CONTROLLER: "http://127.0.0.1:123", **options}, ), ) diff --git a/tests/components/vera/test_light.py b/tests/components/vera/test_light.py index c96199e5989..6bdc3df9a64 100644 --- a/tests/components/vera/test_light.py +++ b/tests/components/vera/test_light.py @@ -1,4 +1,5 @@ """Vera tests.""" + from unittest.mock import MagicMock import pyvera as pv diff --git a/tests/components/vera/test_lock.py b/tests/components/vera/test_lock.py index 644d58b9fc5..4139a494e1f 100644 --- a/tests/components/vera/test_lock.py +++ b/tests/components/vera/test_lock.py @@ -1,4 +1,5 @@ """Vera tests.""" + from unittest.mock import MagicMock import pyvera as pv diff --git a/tests/components/vera/test_scene.py b/tests/components/vera/test_scene.py index b23d220e74e..d254a9f12aa 100644 --- a/tests/components/vera/test_scene.py +++ b/tests/components/vera/test_scene.py @@ -1,4 +1,5 @@ """Vera tests.""" + from unittest.mock import MagicMock import pyvera as pv diff --git a/tests/components/vera/test_sensor.py b/tests/components/vera/test_sensor.py index 9ddf05a5b0a..ebe8beb4e29 100644 --- a/tests/components/vera/test_sensor.py +++ b/tests/components/vera/test_sensor.py @@ -1,4 +1,5 @@ """Vera tests.""" + from __future__ import annotations from collections.abc import Callable diff --git a/tests/components/vera/test_switch.py b/tests/components/vera/test_switch.py index 2ffb472aeeb..8000aeaac9d 100644 --- a/tests/components/vera/test_switch.py +++ b/tests/components/vera/test_switch.py @@ -1,4 +1,5 @@ """Vera tests.""" + from unittest.mock import MagicMock import pyvera as pv diff --git a/tests/components/verisure/conftest.py b/tests/components/verisure/conftest.py index 8e1da712a5c..445b7b95300 100644 --- a/tests/components/verisure/conftest.py +++ b/tests/components/verisure/conftest.py @@ -1,4 +1,5 @@ """Fixtures for Verisure integration tests.""" + from __future__ import annotations from collections.abc import Generator diff --git a/tests/components/verisure/test_config_flow.py b/tests/components/verisure/test_config_flow.py index 89b295857ff..62ae00b5622 100644 --- a/tests/components/verisure/test_config_flow.py +++ b/tests/components/verisure/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Verisure config flow.""" + from __future__ import annotations from unittest.mock import AsyncMock, MagicMock, patch diff --git a/tests/components/version/common.py b/tests/components/version/common.py index c4759604a44..33a1747cf0e 100644 --- a/tests/components/version/common.py +++ b/tests/components/version/common.py @@ -1,4 +1,5 @@ """Fixtures for version integration.""" + from __future__ import annotations from typing import Any, Final diff --git a/tests/components/version/test_binary_sensor.py b/tests/components/version/test_binary_sensor.py index 31ac70cf8b3..38fd78dfbb6 100644 --- a/tests/components/version/test_binary_sensor.py +++ b/tests/components/version/test_binary_sensor.py @@ -1,4 +1,5 @@ """The test for the version binary sensor platform.""" + from __future__ import annotations from homeassistant.components.version.const import DEFAULT_CONFIGURATION diff --git a/tests/components/version/test_config_flow.py b/tests/components/version/test_config_flow.py index 8de4afff92e..d7edb5526d5 100644 --- a/tests/components/version/test_config_flow.py +++ b/tests/components/version/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Version config flow.""" + from unittest.mock import patch from pyhaversion.consts import HaVersionChannel, HaVersionSource diff --git a/tests/components/version/test_diagnostics.py b/tests/components/version/test_diagnostics.py index 91bbcd3aa9f..9a436aa0740 100644 --- a/tests/components/version/test_diagnostics.py +++ b/tests/components/version/test_diagnostics.py @@ -1,6 +1,5 @@ """Test version diagnostics.""" - from homeassistant.core import HomeAssistant from .common import MOCK_VERSION, setup_version_integration diff --git a/tests/components/version/test_sensor.py b/tests/components/version/test_sensor.py index 0a3e89494f1..3a00c10aadc 100644 --- a/tests/components/version/test_sensor.py +++ b/tests/components/version/test_sensor.py @@ -1,4 +1,5 @@ """The test for the version sensor platform.""" + from __future__ import annotations from freezegun.api import FrozenDateTimeFactory diff --git a/tests/components/vesync/common.py b/tests/components/vesync/common.py index 47cce03afe3..23c57177ddd 100644 --- a/tests/components/vesync/common.py +++ b/tests/components/vesync/common.py @@ -1,4 +1,5 @@ """Common methods used across tests for VeSync.""" + import json import requests_mock @@ -48,10 +49,11 @@ def mock_devices_response( requests_mock: requests_mock.Mocker, device_name: str ) -> None: """Build a response for the Helpers.call_api method.""" - device_list = [] - for device in ALL_DEVICES["result"]["list"]: - if device["deviceName"] == device_name: - device_list.append(device) + device_list = [ + device + for device in ALL_DEVICES["result"]["list"] + if device["deviceName"] == device_name + ] requests_mock.post( "https://smartapi.vesync.com/cloud/v1/deviceManaged/devices", diff --git a/tests/components/vesync/conftest.py b/tests/components/vesync/conftest.py index 8815a4b9748..23e0938cce6 100644 --- a/tests/components/vesync/conftest.py +++ b/tests/components/vesync/conftest.py @@ -1,4 +1,5 @@ """Configuration for VeSync tests.""" + from __future__ import annotations from unittest.mock import Mock, patch diff --git a/tests/components/vesync/snapshots/test_diagnostics.ambr b/tests/components/vesync/snapshots/test_diagnostics.ambr index b2ae7b53cf5..fcb2cc7b286 100644 --- a/tests/components/vesync/snapshots/test_diagnostics.ambr +++ b/tests/components/vesync/snapshots/test_diagnostics.ambr @@ -207,6 +207,7 @@ }), 'entity_id': 'fan.fan', 'last_changed': str, + 'last_reported': str, 'last_updated': str, 'state': 'unavailable', }), @@ -230,6 +231,7 @@ }), 'entity_id': 'sensor.fan_air_quality', 'last_changed': str, + 'last_reported': str, 'last_updated': str, 'state': 'unavailable', }), @@ -255,6 +257,7 @@ }), 'entity_id': 'sensor.fan_filter_lifetime', 'last_changed': str, + 'last_reported': str, 'last_updated': str, 'state': 'unavailable', }), diff --git a/tests/components/vesync/snapshots/test_fan.ambr b/tests/components/vesync/snapshots/test_fan.ambr index 74c9a916880..59304e92d9d 100644 --- a/tests/components/vesync/snapshots/test_fan.ambr +++ b/tests/components/vesync/snapshots/test_fan.ambr @@ -84,6 +84,7 @@ 'context': , 'entity_id': 'fan.air_purifier_131s', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unavailable', }) @@ -178,6 +179,7 @@ 'context': , 'entity_id': 'fan.air_purifier_200s', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }) @@ -274,6 +276,7 @@ 'context': , 'entity_id': 'fan.air_purifier_400s', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }) @@ -370,6 +373,7 @@ 'context': , 'entity_id': 'fan.air_purifier_600s', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }) diff --git a/tests/components/vesync/snapshots/test_light.ambr b/tests/components/vesync/snapshots/test_light.ambr index d3a26d5cece..9990395a36c 100644 --- a/tests/components/vesync/snapshots/test_light.ambr +++ b/tests/components/vesync/snapshots/test_light.ambr @@ -226,6 +226,7 @@ 'context': , 'entity_id': 'light.dimmable_light', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unavailable', }) @@ -315,6 +316,7 @@ 'context': , 'entity_id': 'light.dimmer_switch', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }) @@ -469,6 +471,7 @@ 'context': , 'entity_id': 'light.temperature_light', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }) diff --git a/tests/components/vesync/snapshots/test_sensor.ambr b/tests/components/vesync/snapshots/test_sensor.ambr index 4caa2220760..268718fb2fe 100644 --- a/tests/components/vesync/snapshots/test_sensor.ambr +++ b/tests/components/vesync/snapshots/test_sensor.ambr @@ -107,6 +107,7 @@ 'context': , 'entity_id': 'sensor.air_purifier_131s_air_quality', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unavailable', }) @@ -121,6 +122,7 @@ 'context': , 'entity_id': 'sensor.air_purifier_131s_filter_lifetime', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unavailable', }) @@ -204,6 +206,7 @@ 'context': , 'entity_id': 'sensor.air_purifier_200s_filter_lifetime', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '99', }) @@ -349,6 +352,7 @@ 'context': , 'entity_id': 'sensor.air_purifier_400s_air_quality', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '5', }) @@ -363,6 +367,7 @@ 'context': , 'entity_id': 'sensor.air_purifier_400s_filter_lifetime', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '99', }) @@ -378,6 +383,7 @@ 'context': , 'entity_id': 'sensor.air_purifier_400s_pm2_5', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '1', }) @@ -523,6 +529,7 @@ 'context': , 'entity_id': 'sensor.air_purifier_600s_air_quality', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '5', }) @@ -537,6 +544,7 @@ 'context': , 'entity_id': 'sensor.air_purifier_600s_filter_lifetime', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '99', }) @@ -552,6 +560,7 @@ 'context': , 'entity_id': 'sensor.air_purifier_600s_pm2_5', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '1', }) @@ -889,6 +898,7 @@ 'context': , 'entity_id': 'sensor.outlet_current_power', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '25.0', }) @@ -904,6 +914,7 @@ 'context': , 'entity_id': 'sensor.outlet_current_voltage', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '120.0', }) @@ -919,6 +930,7 @@ 'context': , 'entity_id': 'sensor.outlet_energy_use_monthly', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0', }) @@ -934,6 +946,7 @@ 'context': , 'entity_id': 'sensor.outlet_energy_use_today', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '100', }) @@ -949,6 +962,7 @@ 'context': , 'entity_id': 'sensor.outlet_energy_use_weekly', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0', }) @@ -964,6 +978,7 @@ 'context': , 'entity_id': 'sensor.outlet_energy_use_yearly', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0', }) diff --git a/tests/components/vesync/snapshots/test_switch.ambr b/tests/components/vesync/snapshots/test_switch.ambr index eb23f749152..3df26f74bcf 100644 --- a/tests/components/vesync/snapshots/test_switch.ambr +++ b/tests/components/vesync/snapshots/test_switch.ambr @@ -306,6 +306,7 @@ 'context': , 'entity_id': 'switch.outlet', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }) @@ -421,6 +422,7 @@ 'context': , 'entity_id': 'switch.wall_switch', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }) diff --git a/tests/components/vesync/test_config_flow.py b/tests/components/vesync/test_config_flow.py index acf4414900f..a283b89b841 100644 --- a/tests/components/vesync/test_config_flow.py +++ b/tests/components/vesync/test_config_flow.py @@ -1,4 +1,5 @@ """Test for vesync config flow.""" + from unittest.mock import patch from homeassistant import data_entry_flow diff --git a/tests/components/vesync/test_diagnostics.py b/tests/components/vesync/test_diagnostics.py index 62365189064..04696f01631 100644 --- a/tests/components/vesync/test_diagnostics.py +++ b/tests/components/vesync/test_diagnostics.py @@ -1,4 +1,5 @@ """Tests for the diagnostics data provided by the VeSync integration.""" + from unittest.mock import patch from pyvesync.helpers import Helpers @@ -92,10 +93,13 @@ async def test_async_get_device_diagnostics__single_fan( matcher=path_type( { "home_assistant.entities.0.state.last_changed": (str,), + "home_assistant.entities.0.state.last_reported": (str,), "home_assistant.entities.0.state.last_updated": (str,), "home_assistant.entities.1.state.last_changed": (str,), + "home_assistant.entities.1.state.last_reported": (str,), "home_assistant.entities.1.state.last_updated": (str,), "home_assistant.entities.2.state.last_changed": (str,), + "home_assistant.entities.2.state.last_reported": (str,), "home_assistant.entities.2.state.last_updated": (str,), } ) diff --git a/tests/components/vesync/test_fan.py b/tests/components/vesync/test_fan.py index 26ac565e1a6..4d444036a60 100644 --- a/tests/components/vesync/test_fan.py +++ b/tests/components/vesync/test_fan.py @@ -1,4 +1,5 @@ """Tests for the fan module.""" + import pytest import requests_mock from syrupy import SnapshotAssertion diff --git a/tests/components/vesync/test_init.py b/tests/components/vesync/test_init.py index c643e2bda19..a089a270c94 100644 --- a/tests/components/vesync/test_init.py +++ b/tests/components/vesync/test_init.py @@ -1,4 +1,5 @@ """Tests for the init module.""" + from unittest.mock import Mock, patch import pytest @@ -27,17 +28,13 @@ async def test_async_setup_entry__not_login( """Test setup does not create config entry when not logged in.""" manager.login = Mock(return_value=False) - with patch.object( - hass.config_entries, "async_forward_entry_setups" - ) as setups_mock, patch.object( - hass.config_entries, "async_forward_entry_setup" - ) as setup_mock, patch( - "homeassistant.components.vesync.async_process_devices" - ) as process_mock: + with ( + patch.object(hass.config_entries, "async_forward_entry_setups") as setups_mock, + patch("homeassistant.components.vesync.async_process_devices") as process_mock, + ): assert not await async_setup_entry(hass, config_entry) await hass.async_block_till_done() assert setups_mock.call_count == 0 - assert setup_mock.call_count == 0 assert process_mock.call_count == 0 assert manager.login.call_count == 1 @@ -49,18 +46,13 @@ async def test_async_setup_entry__no_devices( hass: HomeAssistant, config_entry: ConfigEntry, manager: VeSync ) -> None: """Test setup connects to vesync and creates empty config when no devices.""" - with patch.object( - hass.config_entries, "async_forward_entry_setups" - ) as setups_mock, patch.object( - hass.config_entries, "async_forward_entry_setup" - ) as setup_mock: + with patch.object(hass.config_entries, "async_forward_entry_setups") as setups_mock: assert await async_setup_entry(hass, config_entry) # Assert platforms loaded await hass.async_block_till_done() assert setups_mock.call_count == 1 assert setups_mock.call_args.args[0] == config_entry assert setups_mock.call_args.args[1] == [] - assert setup_mock.call_count == 0 assert manager.login.call_count == 1 assert hass.data[DOMAIN][VS_MANAGER] == manager @@ -80,18 +72,13 @@ async def test_async_setup_entry__loads_fans( "fans": fans, } - with patch.object( - hass.config_entries, "async_forward_entry_setups" - ) as setups_mock, patch.object( - hass.config_entries, "async_forward_entry_setup" - ) as setup_mock: + with patch.object(hass.config_entries, "async_forward_entry_setups") as setups_mock: assert await async_setup_entry(hass, config_entry) # Assert platforms loaded await hass.async_block_till_done() assert setups_mock.call_count == 1 assert setups_mock.call_args.args[0] == config_entry assert setups_mock.call_args.args[1] == [Platform.FAN, Platform.SENSOR] - assert setup_mock.call_count == 0 assert manager.login.call_count == 1 assert hass.data[DOMAIN][VS_MANAGER] == manager assert not hass.data[DOMAIN][VS_SWITCHES] diff --git a/tests/components/vesync/test_light.py b/tests/components/vesync/test_light.py index b293d6e808e..866e6b295bf 100644 --- a/tests/components/vesync/test_light.py +++ b/tests/components/vesync/test_light.py @@ -1,4 +1,5 @@ """Tests for the light module.""" + import pytest import requests_mock from syrupy import SnapshotAssertion diff --git a/tests/components/vesync/test_sensor.py b/tests/components/vesync/test_sensor.py index c50b916df66..bd3a8eb8591 100644 --- a/tests/components/vesync/test_sensor.py +++ b/tests/components/vesync/test_sensor.py @@ -1,4 +1,5 @@ """Tests for the sensor module.""" + import pytest import requests_mock from syrupy import SnapshotAssertion diff --git a/tests/components/vesync/test_switch.py b/tests/components/vesync/test_switch.py index af721724d91..111f2b80960 100644 --- a/tests/components/vesync/test_switch.py +++ b/tests/components/vesync/test_switch.py @@ -1,4 +1,5 @@ """Tests for the switch module.""" + import pytest import requests_mock from syrupy import SnapshotAssertion diff --git a/tests/components/vicare/__init__.py b/tests/components/vicare/__init__.py index 8c0f7941ba6..329a3b04d58 100644 --- a/tests/components/vicare/__init__.py +++ b/tests/components/vicare/__init__.py @@ -1,4 +1,5 @@ """Test for ViCare.""" + from __future__ import annotations from typing import Final diff --git a/tests/components/vicare/conftest.py b/tests/components/vicare/conftest.py index 46d90960f4e..fac85b5052a 100644 --- a/tests/components/vicare/conftest.py +++ b/tests/components/vicare/conftest.py @@ -1,4 +1,5 @@ """Fixtures for ViCare integration tests.""" + from __future__ import annotations from collections.abc import AsyncGenerator, Generator diff --git a/tests/components/vicare/snapshots/test_binary_sensor.ambr b/tests/components/vicare/snapshots/test_binary_sensor.ambr index 2d08a50bf3f..7454f914435 100644 --- a/tests/components/vicare/snapshots/test_binary_sensor.ambr +++ b/tests/components/vicare/snapshots/test_binary_sensor.ambr @@ -4,11 +4,11 @@ 'attributes': ReadOnlyDict({ 'device_class': 'running', 'friendly_name': 'model0 Burner', - 'icon': 'mdi:gas-burner', }), 'context': , 'entity_id': 'binary_sensor.model0_burner', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unavailable', }) @@ -18,11 +18,11 @@ 'attributes': ReadOnlyDict({ 'device_class': 'running', 'friendly_name': 'model0 Circulation pump', - 'icon': 'mdi:pump', }), 'context': , 'entity_id': 'binary_sensor.model0_circulation_pump', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unavailable', }) @@ -31,11 +31,11 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'model0 Frost protection', - 'icon': 'mdi:snowflake', }), 'context': , 'entity_id': 'binary_sensor.model0_frost_protection', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unavailable', }) diff --git a/tests/components/vicare/test_config_flow.py b/tests/components/vicare/test_config_flow.py index e24f7ea834b..031fcdff9d3 100644 --- a/tests/components/vicare/test_config_flow.py +++ b/tests/components/vicare/test_config_flow.py @@ -1,4 +1,5 @@ """Test the ViCare config flow.""" + from unittest.mock import AsyncMock, patch import pytest diff --git a/tests/components/vilfo/test_config_flow.py b/tests/components/vilfo/test_config_flow.py index b893d2df550..c1755f95043 100644 --- a/tests/components/vilfo/test_config_flow.py +++ b/tests/components/vilfo/test_config_flow.py @@ -1,9 +1,11 @@ """Test the Vilfo Router config flow.""" + from unittest.mock import Mock, patch import vilfo from homeassistant import config_entries, data_entry_flow +from homeassistant.components.vilfo import config_flow from homeassistant.components.vilfo.const import DOMAIN from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST, CONF_ID, CONF_MAC from homeassistant.core import HomeAssistant @@ -20,13 +22,13 @@ async def test_form(hass: HomeAssistant) -> None: assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} - with patch("vilfo.Client.ping", return_value=None), patch( - "vilfo.Client.get_board_information", return_value=None - ), patch( - "vilfo.Client.resolve_firmware_version", return_value=firmware_version - ), patch("vilfo.Client.resolve_mac_address", return_value=mock_mac), patch( - "homeassistant.components.vilfo.async_setup_entry" - ) as mock_setup_entry: + with ( + patch("vilfo.Client.ping", return_value=None), + patch("vilfo.Client.get_board_information", return_value=None), + patch("vilfo.Client.resolve_firmware_version", return_value=firmware_version), + patch("vilfo.Client.resolve_mac_address", return_value=mock_mac), + patch("homeassistant.components.vilfo.async_setup_entry") as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_HOST: "testadmin.vilfo.com", CONF_ACCESS_TOKEN: "test-token"}, @@ -49,11 +51,13 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch("vilfo.Client.ping", return_value=None), patch( - "vilfo.Client.resolve_mac_address", return_value=None - ), patch( - "vilfo.Client.get_board_information", - side_effect=vilfo.exceptions.AuthenticationException, + with ( + patch("vilfo.Client.ping", return_value=None), + patch("vilfo.Client.resolve_mac_address", return_value=None), + patch( + "vilfo.Client.get_board_information", + side_effect=vilfo.exceptions.AuthenticationException, + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -70,8 +74,9 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch("vilfo.Client.ping", side_effect=vilfo.exceptions.VilfoException), patch( - "vilfo.Client.resolve_mac_address" + with ( + patch("vilfo.Client.ping", side_effect=vilfo.exceptions.VilfoException), + patch("vilfo.Client.resolve_mac_address"), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -81,8 +86,9 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: 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( - "vilfo.Client.resolve_mac_address" + with ( + patch("vilfo.Client.ping", side_effect=vilfo.exceptions.VilfoException), + patch("vilfo.Client.resolve_mac_address"), ): result3 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -110,12 +116,15 @@ async def test_form_already_configured(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) firmware_version = "1.1.0" - with patch("vilfo.Client.ping", return_value=None), patch( - "vilfo.Client.get_board_information", - return_value=None, - ), patch( - "vilfo.Client.resolve_firmware_version", return_value=firmware_version - ), patch("vilfo.Client.resolve_mac_address", return_value=None): + with ( + patch("vilfo.Client.ping", return_value=None), + patch( + "vilfo.Client.get_board_information", + return_value=None, + ), + patch("vilfo.Client.resolve_firmware_version", return_value=firmware_version), + patch("vilfo.Client.resolve_mac_address", return_value=None), + ): first_flow_result2 = await hass.config_entries.flow.async_configure( first_flow_result1["flow_id"], {CONF_HOST: "testadmin.vilfo.com", CONF_ACCESS_TOKEN: "test-token"}, @@ -125,12 +134,15 @@ async def test_form_already_configured(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch("vilfo.Client.ping", return_value=None), patch( - "vilfo.Client.get_board_information", - return_value=None, - ), patch( - "vilfo.Client.resolve_firmware_version", return_value=firmware_version - ), patch("vilfo.Client.resolve_mac_address", return_value=None): + with ( + patch("vilfo.Client.ping", return_value=None), + patch( + "vilfo.Client.get_board_information", + return_value=None, + ), + patch("vilfo.Client.resolve_firmware_version", return_value=firmware_version), + patch("vilfo.Client.resolve_mac_address", return_value=None), + ): second_flow_result2 = await hass.config_entries.flow.async_configure( second_flow_result1["flow_id"], {CONF_HOST: "testadmin.vilfo.com", CONF_ACCESS_TOKEN: "test-token"}, @@ -167,34 +179,28 @@ async def test_validate_input_returns_data(hass: HomeAssistant) -> None: mock_mac = "FF-00-00-00-00-00" firmware_version = "1.1.0" - with patch("vilfo.Client.ping", return_value=None), patch( - "vilfo.Client.get_board_information", return_value=None - ), patch( - "vilfo.Client.resolve_firmware_version", return_value=firmware_version - ), patch("vilfo.Client.resolve_mac_address", return_value=None): - result = await hass.components.vilfo.config_flow.validate_input( - hass, data=mock_data - ) + with ( + patch("vilfo.Client.ping", return_value=None), + patch("vilfo.Client.get_board_information", return_value=None), + patch("vilfo.Client.resolve_firmware_version", return_value=firmware_version), + patch("vilfo.Client.resolve_mac_address", return_value=None), + ): + result = await config_flow.validate_input(hass, data=mock_data) assert result["title"] == mock_data["host"] assert result[CONF_HOST] == mock_data["host"] assert result[CONF_MAC] is None assert result[CONF_ID] == mock_data["host"] - with patch("vilfo.Client.ping", return_value=None), patch( - "vilfo.Client.get_board_information", return_value=None - ), patch( - "vilfo.Client.resolve_firmware_version", return_value=firmware_version - ), patch("vilfo.Client.resolve_mac_address", return_value=mock_mac): - result2 = await hass.components.vilfo.config_flow.validate_input( - hass, data=mock_data - ) - result3 = await hass.components.vilfo.config_flow.validate_input( - hass, data=mock_data_with_ip - ) - result4 = await hass.components.vilfo.config_flow.validate_input( - hass, data=mock_data_with_ipv6 - ) + with ( + patch("vilfo.Client.ping", return_value=None), + patch("vilfo.Client.get_board_information", return_value=None), + patch("vilfo.Client.resolve_firmware_version", return_value=firmware_version), + patch("vilfo.Client.resolve_mac_address", return_value=mock_mac), + ): + result2 = await config_flow.validate_input(hass, data=mock_data) + result3 = await config_flow.validate_input(hass, data=mock_data_with_ip) + result4 = await config_flow.validate_input(hass, data=mock_data_with_ipv6) assert result2["title"] == mock_data["host"] assert result2[CONF_HOST] == mock_data["host"] diff --git a/tests/components/vizio/conftest.py b/tests/components/vizio/conftest.py index ad67d309cdd..6ce36b38c8f 100644 --- a/tests/components/vizio/conftest.py +++ b/tests/components/vizio/conftest.py @@ -1,4 +1,5 @@ """Configure py.test.""" + from unittest.mock import AsyncMock, patch import pytest @@ -92,12 +93,15 @@ def vizio_connect_fixture(): @pytest.fixture(name="vizio_complete_pairing") def vizio_complete_pairing_fixture(): """Mock complete vizio pairing workflow.""" - with patch( - "homeassistant.components.vizio.config_flow.VizioAsync.start_pair", - return_value=MockStartPairingResponse(CH_TYPE, RESPONSE_TOKEN), - ), patch( - "homeassistant.components.vizio.config_flow.VizioAsync.pair", - return_value=MockCompletePairingResponse(ACCESS_TOKEN), + with ( + patch( + "homeassistant.components.vizio.config_flow.VizioAsync.start_pair", + return_value=MockStartPairingResponse(CH_TYPE, RESPONSE_TOKEN), + ), + patch( + "homeassistant.components.vizio.config_flow.VizioAsync.pair", + return_value=MockCompletePairingResponse(ACCESS_TOKEN), + ), ): yield @@ -115,12 +119,15 @@ def vizio_start_pairing_failure_fixture(): @pytest.fixture(name="vizio_invalid_pin_failure") def vizio_invalid_pin_failure_fixture(): """Mock vizio failure due to invalid pin.""" - with patch( - "homeassistant.components.vizio.config_flow.VizioAsync.start_pair", - return_value=MockStartPairingResponse(CH_TYPE, RESPONSE_TOKEN), - ), patch( - "homeassistant.components.vizio.config_flow.VizioAsync.pair", - return_value=None, + with ( + patch( + "homeassistant.components.vizio.config_flow.VizioAsync.start_pair", + return_value=MockStartPairingResponse(CH_TYPE, RESPONSE_TOKEN), + ), + patch( + "homeassistant.components.vizio.config_flow.VizioAsync.pair", + return_value=None, + ), ): yield @@ -135,10 +142,13 @@ def vizio_bypass_setup_fixture(): @pytest.fixture(name="vizio_bypass_update") def vizio_bypass_update_fixture(): """Mock component update.""" - with patch( - "homeassistant.components.vizio.media_player.VizioAsync.can_connect_with_auth_check", - return_value=True, - ), patch("homeassistant.components.vizio.media_player.VizioDevice.async_update"): + with ( + patch( + "homeassistant.components.vizio.media_player.VizioAsync.can_connect_with_auth_check", + return_value=True, + ), + patch("homeassistant.components.vizio.media_player.VizioDevice.async_update"), + ): yield @@ -155,12 +165,15 @@ def vizio_guess_device_type_fixture(): @pytest.fixture(name="vizio_cant_connect") def vizio_cant_connect_fixture(): """Mock vizio device can't connect with valid auth.""" - with patch( - "homeassistant.components.vizio.config_flow.VizioAsync.validate_ha_config", - AsyncMock(return_value=False), - ), patch( - "homeassistant.components.vizio.media_player.VizioAsync.get_power_state", - return_value=None, + with ( + patch( + "homeassistant.components.vizio.config_flow.VizioAsync.validate_ha_config", + AsyncMock(return_value=False), + ), + patch( + "homeassistant.components.vizio.media_player.VizioAsync.get_power_state", + return_value=None, + ), ): yield @@ -168,34 +181,43 @@ def vizio_cant_connect_fixture(): @pytest.fixture(name="vizio_update") def vizio_update_fixture(): """Mock valid updates to vizio device.""" - with patch( - "homeassistant.components.vizio.media_player.VizioAsync.can_connect_with_auth_check", - return_value=True, - ), patch( - "homeassistant.components.vizio.media_player.VizioAsync.get_all_settings", - return_value={ - "volume": int(MAX_VOLUME[DEVICE_CLASS_SPEAKER] / 2), - "eq": CURRENT_EQ, - "mute": "Off", - }, - ), patch( - "homeassistant.components.vizio.media_player.VizioAsync.get_setting_options", - return_value=EQ_LIST, - ), patch( - "homeassistant.components.vizio.media_player.VizioAsync.get_current_input", - return_value=CURRENT_INPUT, - ), patch( - "homeassistant.components.vizio.media_player.VizioAsync.get_inputs_list", - return_value=get_mock_inputs(INPUT_LIST), - ), patch( - "homeassistant.components.vizio.media_player.VizioAsync.get_power_state", - return_value=True, - ), patch( - "homeassistant.components.vizio.media_player.VizioAsync.get_model_name", - return_value=MODEL, - ), patch( - "homeassistant.components.vizio.media_player.VizioAsync.get_version", - return_value=VERSION, + with ( + patch( + "homeassistant.components.vizio.media_player.VizioAsync.can_connect_with_auth_check", + return_value=True, + ), + patch( + "homeassistant.components.vizio.media_player.VizioAsync.get_all_settings", + return_value={ + "volume": int(MAX_VOLUME[DEVICE_CLASS_SPEAKER] / 2), + "eq": CURRENT_EQ, + "mute": "Off", + }, + ), + patch( + "homeassistant.components.vizio.media_player.VizioAsync.get_setting_options", + return_value=EQ_LIST, + ), + patch( + "homeassistant.components.vizio.media_player.VizioAsync.get_current_input", + return_value=CURRENT_INPUT, + ), + patch( + "homeassistant.components.vizio.media_player.VizioAsync.get_inputs_list", + return_value=get_mock_inputs(INPUT_LIST), + ), + patch( + "homeassistant.components.vizio.media_player.VizioAsync.get_power_state", + return_value=True, + ), + patch( + "homeassistant.components.vizio.media_player.VizioAsync.get_model_name", + return_value=MODEL, + ), + patch( + "homeassistant.components.vizio.media_player.VizioAsync.get_version", + return_value=VERSION, + ), ): yield @@ -203,15 +225,19 @@ def vizio_update_fixture(): @pytest.fixture(name="vizio_update_with_apps") def vizio_update_with_apps_fixture(vizio_update: pytest.fixture): """Mock valid updates to vizio device that supports apps.""" - with patch( - "homeassistant.components.vizio.media_player.VizioAsync.get_inputs_list", - return_value=get_mock_inputs(INPUT_LIST_WITH_APPS), - ), patch( - "homeassistant.components.vizio.media_player.VizioAsync.get_current_input", - return_value="CAST", - ), patch( - "homeassistant.components.vizio.media_player.VizioAsync.get_current_app_config", - return_value=AppConfig(**CURRENT_APP_CONFIG), + with ( + patch( + "homeassistant.components.vizio.media_player.VizioAsync.get_inputs_list", + return_value=get_mock_inputs(INPUT_LIST_WITH_APPS), + ), + patch( + "homeassistant.components.vizio.media_player.VizioAsync.get_current_input", + return_value="CAST", + ), + patch( + "homeassistant.components.vizio.media_player.VizioAsync.get_current_app_config", + return_value=AppConfig(**CURRENT_APP_CONFIG), + ), ): yield @@ -219,15 +245,19 @@ def vizio_update_with_apps_fixture(vizio_update: pytest.fixture): @pytest.fixture(name="vizio_update_with_apps_on_input") def vizio_update_with_apps_on_input_fixture(vizio_update: pytest.fixture): """Mock valid updates to vizio device that supports apps but is on a TV input.""" - with patch( - "homeassistant.components.vizio.media_player.VizioAsync.get_inputs_list", - return_value=get_mock_inputs(INPUT_LIST_WITH_APPS), - ), patch( - "homeassistant.components.vizio.media_player.VizioAsync.get_current_input", - return_value=CURRENT_INPUT, - ), patch( - "homeassistant.components.vizio.media_player.VizioAsync.get_current_app_config", - return_value=AppConfig("unknown", 1, "app"), + with ( + patch( + "homeassistant.components.vizio.media_player.VizioAsync.get_inputs_list", + return_value=get_mock_inputs(INPUT_LIST_WITH_APPS), + ), + patch( + "homeassistant.components.vizio.media_player.VizioAsync.get_current_input", + return_value=CURRENT_INPUT, + ), + patch( + "homeassistant.components.vizio.media_player.VizioAsync.get_current_app_config", + return_value=AppConfig("unknown", 1, "app"), + ), ): yield diff --git a/tests/components/vizio/const.py b/tests/components/vizio/const.py index 849c13d4396..1f35cc16385 100644 --- a/tests/components/vizio/const.py +++ b/tests/components/vizio/const.py @@ -1,4 +1,5 @@ """Constants for the Vizio integration tests.""" + from ipaddress import ip_address from homeassistant.components import zeroconf @@ -83,7 +84,7 @@ APP_LIST = [ }, ] APP_NAME_LIST = [app["name"] for app in APP_LIST] -INPUT_LIST_WITH_APPS = INPUT_LIST + ["CAST"] +INPUT_LIST_WITH_APPS = [*INPUT_LIST, "CAST"] CUSTOM_CONFIG = {CONF_APP_ID: "test", CONF_MESSAGE: None, CONF_NAME_SPACE: 10} ADDITIONAL_APP_CONFIG = { "name": CURRENT_APP, diff --git a/tests/components/vizio/test_config_flow.py b/tests/components/vizio/test_config_flow.py index 578d79fcba0..d19cf319a5a 100644 --- a/tests/components/vizio/test_config_flow.py +++ b/tests/components/vizio/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for Vizio config flow.""" + import dataclasses import pytest diff --git a/tests/components/vizio/test_init.py b/tests/components/vizio/test_init.py index 288dd2c6ac0..edab40444b6 100644 --- a/tests/components/vizio/test_init.py +++ b/tests/components/vizio/test_init.py @@ -1,4 +1,5 @@ """Tests for Vizio init.""" + from datetime import timedelta import pytest diff --git a/tests/components/vizio/test_media_player.py b/tests/components/vizio/test_media_player.py index 142c5f74b84..d5ce18eb8b9 100644 --- a/tests/components/vizio/test_media_player.py +++ b/tests/components/vizio/test_media_player.py @@ -1,4 +1,5 @@ """Tests for Vizio config flow.""" + from __future__ import annotations from contextlib import asynccontextmanager @@ -128,15 +129,19 @@ async def _cm_for_test_setup_without_apps( all_settings: dict[str, Any], vizio_power_state: bool | None ) -> None: """Context manager to setup test for Vizio devices without including app specific patches.""" - with patch( - "homeassistant.components.vizio.media_player.VizioAsync.get_all_settings", - return_value=all_settings, - ), patch( - "homeassistant.components.vizio.media_player.VizioAsync.get_setting_options", - return_value=EQ_LIST, - ), patch( - "homeassistant.components.vizio.media_player.VizioAsync.get_power_state", - return_value=vizio_power_state, + with ( + patch( + "homeassistant.components.vizio.media_player.VizioAsync.get_all_settings", + return_value=all_settings, + ), + patch( + "homeassistant.components.vizio.media_player.VizioAsync.get_setting_options", + return_value=EQ_LIST, + ), + patch( + "homeassistant.components.vizio.media_player.VizioAsync.get_power_state", + return_value=vizio_power_state, + ), ): yield @@ -483,10 +488,13 @@ async def _test_update_availability_switch( # Fast forward time to future twice to trigger update and assert vizio log message for i in range(1, 3): future = now + (future_interval * i) - with patch( - "homeassistant.components.vizio.media_player.VizioAsync.get_power_state", - return_value=final_power_state, - ), freeze_time(future): + with ( + patch( + "homeassistant.components.vizio.media_player.VizioAsync.get_power_state", + return_value=final_power_state, + ), + freeze_time(future), + ): async_fire_time_changed(hass, future) await hass.async_block_till_done() if final_power_state is None: @@ -563,7 +571,7 @@ async def test_setup_with_apps_include( hass, MOCK_TV_WITH_INCLUDE_CONFIG, CURRENT_APP_CONFIG ): attr = hass.states.get(ENTITY_ID).attributes - _assert_source_list_with_apps(list(INPUT_LIST_WITH_APPS + [CURRENT_APP]), attr) + _assert_source_list_with_apps([*INPUT_LIST_WITH_APPS, CURRENT_APP], attr) assert CURRENT_APP in attr[ATTR_INPUT_SOURCE_LIST] assert attr[ATTR_INPUT_SOURCE] == CURRENT_APP assert attr["app_name"] == CURRENT_APP @@ -581,7 +589,7 @@ async def test_setup_with_apps_exclude( hass, MOCK_TV_WITH_EXCLUDE_CONFIG, CURRENT_APP_CONFIG ): attr = hass.states.get(ENTITY_ID).attributes - _assert_source_list_with_apps(list(INPUT_LIST_WITH_APPS + [CURRENT_APP]), attr) + _assert_source_list_with_apps([*INPUT_LIST_WITH_APPS, CURRENT_APP], attr) assert CURRENT_APP in attr[ATTR_INPUT_SOURCE_LIST] assert attr[ATTR_INPUT_SOURCE] == CURRENT_APP assert attr["app_name"] == CURRENT_APP @@ -640,11 +648,14 @@ async def test_setup_with_apps_additional_apps_config( ) # Test that invalid app does nothing - with patch( - "homeassistant.components.vizio.media_player.VizioAsync.launch_app" - ) as service_call1, patch( - "homeassistant.components.vizio.media_player.VizioAsync.launch_app_config" - ) as service_call2: + with ( + patch( + "homeassistant.components.vizio.media_player.VizioAsync.launch_app" + ) as service_call1, + patch( + "homeassistant.components.vizio.media_player.VizioAsync.launch_app_config" + ) as service_call2, + ): await hass.services.async_call( MP_DOMAIN, SERVICE_SELECT_SOURCE, diff --git a/tests/components/vlc_telnet/test_config_flow.py b/tests/components/vlc_telnet/test_config_flow.py index a94f290f7e6..f5207c52c99 100644 --- a/tests/components/vlc_telnet/test_config_flow.py +++ b/tests/components/vlc_telnet/test_config_flow.py @@ -1,4 +1,5 @@ """Test the VLC media player Telnet config flow.""" + from __future__ import annotations from typing import Any @@ -53,14 +54,15 @@ async def test_user_flow( assert result["type"] == FlowResultType.FORM assert result["errors"] is None - with patch("homeassistant.components.vlc_telnet.config_flow.Client.connect"), patch( - "homeassistant.components.vlc_telnet.config_flow.Client.login" - ), patch( - "homeassistant.components.vlc_telnet.config_flow.Client.disconnect" - ), patch( - "homeassistant.components.vlc_telnet.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch("homeassistant.components.vlc_telnet.config_flow.Client.connect"), + patch("homeassistant.components.vlc_telnet.config_flow.Client.login"), + patch("homeassistant.components.vlc_telnet.config_flow.Client.disconnect"), + patch( + "homeassistant.components.vlc_telnet.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result = await hass.config_entries.flow.async_configure( result["flow_id"], input_data, @@ -117,14 +119,18 @@ async def test_errors( DOMAIN, context={"source": source} ) - with patch( - "homeassistant.components.vlc_telnet.config_flow.Client.connect", - side_effect=connect_side_effect, - ), patch( - "homeassistant.components.vlc_telnet.config_flow.Client.login", - side_effect=login_side_effect, - ), patch( - "homeassistant.components.vlc_telnet.config_flow.Client.disconnect", + with ( + patch( + "homeassistant.components.vlc_telnet.config_flow.Client.connect", + side_effect=connect_side_effect, + ), + patch( + "homeassistant.components.vlc_telnet.config_flow.Client.login", + side_effect=login_side_effect, + ), + patch( + "homeassistant.components.vlc_telnet.config_flow.Client.disconnect", + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -157,14 +163,15 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: data=entry_data, ) - with patch("homeassistant.components.vlc_telnet.config_flow.Client.connect"), patch( - "homeassistant.components.vlc_telnet.config_flow.Client.login" - ), patch( - "homeassistant.components.vlc_telnet.config_flow.Client.disconnect" - ), patch( - "homeassistant.components.vlc_telnet.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch("homeassistant.components.vlc_telnet.config_flow.Client.connect"), + patch("homeassistant.components.vlc_telnet.config_flow.Client.login"), + patch("homeassistant.components.vlc_telnet.config_flow.Client.disconnect"), + patch( + "homeassistant.components.vlc_telnet.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"password": "new-password"}, @@ -212,14 +219,18 @@ async def test_reauth_errors( data=entry_data, ) - with patch( - "homeassistant.components.vlc_telnet.config_flow.Client.connect", - side_effect=connect_side_effect, - ), patch( - "homeassistant.components.vlc_telnet.config_flow.Client.login", - side_effect=login_side_effect, - ), patch( - "homeassistant.components.vlc_telnet.config_flow.Client.disconnect", + with ( + patch( + "homeassistant.components.vlc_telnet.config_flow.Client.connect", + side_effect=connect_side_effect, + ), + patch( + "homeassistant.components.vlc_telnet.config_flow.Client.login", + side_effect=login_side_effect, + ), + patch( + "homeassistant.components.vlc_telnet.config_flow.Client.disconnect", + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -232,14 +243,15 @@ async def test_reauth_errors( async def test_hassio_flow(hass: HomeAssistant) -> None: """Test successful hassio flow.""" - with patch("homeassistant.components.vlc_telnet.config_flow.Client.connect"), patch( - "homeassistant.components.vlc_telnet.config_flow.Client.login" - ), patch( - "homeassistant.components.vlc_telnet.config_flow.Client.disconnect" - ), patch( - "homeassistant.components.vlc_telnet.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch("homeassistant.components.vlc_telnet.config_flow.Client.connect"), + patch("homeassistant.components.vlc_telnet.config_flow.Client.login"), + patch("homeassistant.components.vlc_telnet.config_flow.Client.disconnect"), + patch( + "homeassistant.components.vlc_telnet.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): test_data = HassioServiceInfo( config={ "password": "test-password", @@ -309,14 +321,18 @@ async def test_hassio_errors( login_side_effect: Exception | None, ) -> None: """Test we handle hassio errors.""" - with patch( - "homeassistant.components.vlc_telnet.config_flow.Client.connect", - side_effect=connect_side_effect, - ), patch( - "homeassistant.components.vlc_telnet.config_flow.Client.login", - side_effect=login_side_effect, - ), patch( - "homeassistant.components.vlc_telnet.config_flow.Client.disconnect", + with ( + patch( + "homeassistant.components.vlc_telnet.config_flow.Client.connect", + side_effect=connect_side_effect, + ), + patch( + "homeassistant.components.vlc_telnet.config_flow.Client.login", + side_effect=login_side_effect, + ), + patch( + "homeassistant.components.vlc_telnet.config_flow.Client.disconnect", + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, diff --git a/tests/components/vodafone_station/const.py b/tests/components/vodafone_station/const.py index 40dc305630e..1b3d36def03 100644 --- a/tests/components/vodafone_station/const.py +++ b/tests/components/vodafone_station/const.py @@ -1,4 +1,5 @@ """Common stuff for Vodafone Station tests.""" + from homeassistant.components.vodafone_station.const import DOMAIN from homeassistant.const import CONF_DEVICES, CONF_HOST, CONF_PASSWORD, CONF_USERNAME diff --git a/tests/components/vodafone_station/test_config_flow.py b/tests/components/vodafone_station/test_config_flow.py index a50bde8de64..f2619044861 100644 --- a/tests/components/vodafone_station/test_config_flow.py +++ b/tests/components/vodafone_station/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for Vodafone Station config flow.""" + from unittest.mock import patch from aiovodafone import exceptions as aiovodafone_exceptions @@ -19,13 +20,17 @@ from tests.common import MockConfigEntry async def test_user(hass: HomeAssistant) -> None: """Test starting a flow by user.""" - with patch( - "homeassistant.components.vodafone_station.config_flow.VodafoneStationSercommApi.login", - ), patch( - "homeassistant.components.vodafone_station.config_flow.VodafoneStationSercommApi.logout", - ), patch( - "homeassistant.components.vodafone_station.async_setup_entry" - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.vodafone_station.config_flow.VodafoneStationSercommApi.login", + ), + patch( + "homeassistant.components.vodafone_station.config_flow.VodafoneStationSercommApi.logout", + ), + patch( + "homeassistant.components.vodafone_station.async_setup_entry" + ) as mock_setup_entry, + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) @@ -78,18 +83,23 @@ async def test_exception_connection(hass: HomeAssistant, side_effect, error) -> assert result["errors"]["base"] == error # Should be recoverable after hits error - with patch( - "homeassistant.components.vodafone_station.config_flow.VodafoneStationSercommApi.get_devices_data", - return_value={ - "wifi_user": "on|laptop|device-1|xx:xx:xx:xx:xx:xx|192.168.100.1||2.4G", - "ethernet": "laptop|device-2|yy:yy:yy:yy:yy:yy|192.168.100.2|;", - }, - ), patch( - "homeassistant.components.vodafone_station.config_flow.VodafoneStationSercommApi.login", - ), patch( - "homeassistant.components.vodafone_station.config_flow.VodafoneStationSercommApi.logout", - ), patch( - "homeassistant.components.vodafone_station.async_setup_entry", + with ( + patch( + "homeassistant.components.vodafone_station.config_flow.VodafoneStationSercommApi.get_devices_data", + return_value={ + "wifi_user": "on|laptop|device-1|xx:xx:xx:xx:xx:xx|192.168.100.1||2.4G", + "ethernet": "laptop|device-2|yy:yy:yy:yy:yy:yy|192.168.100.2|;", + }, + ), + patch( + "homeassistant.components.vodafone_station.config_flow.VodafoneStationSercommApi.login", + ), + patch( + "homeassistant.components.vodafone_station.config_flow.VodafoneStationSercommApi.logout", + ), + patch( + "homeassistant.components.vodafone_station.async_setup_entry", + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -116,12 +126,16 @@ async def test_reauth_successful(hass: HomeAssistant) -> None: mock_config = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) mock_config.add_to_hass(hass) - with patch( - "homeassistant.components.vodafone_station.config_flow.VodafoneStationSercommApi.login", - ), patch( - "homeassistant.components.vodafone_station.config_flow.VodafoneStationSercommApi.logout", - ), patch( - "homeassistant.components.vodafone_station.async_setup_entry", + with ( + patch( + "homeassistant.components.vodafone_station.config_flow.VodafoneStationSercommApi.login", + ), + patch( + "homeassistant.components.vodafone_station.config_flow.VodafoneStationSercommApi.logout", + ), + patch( + "homeassistant.components.vodafone_station.async_setup_entry", + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -159,13 +173,17 @@ async def test_reauth_not_successful(hass: HomeAssistant, side_effect, error) -> mock_config = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) mock_config.add_to_hass(hass) - with patch( - "homeassistant.components.vodafone_station.config_flow.VodafoneStationSercommApi.login", - side_effect=side_effect, - ), patch( - "homeassistant.components.vodafone_station.config_flow.VodafoneStationSercommApi.logout", - ), patch( - "homeassistant.components.vodafone_station.async_setup_entry", + with ( + patch( + "homeassistant.components.vodafone_station.config_flow.VodafoneStationSercommApi.login", + side_effect=side_effect, + ), + patch( + "homeassistant.components.vodafone_station.config_flow.VodafoneStationSercommApi.logout", + ), + patch( + "homeassistant.components.vodafone_station.async_setup_entry", + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -189,18 +207,23 @@ async def test_reauth_not_successful(hass: HomeAssistant, side_effect, error) -> assert result["errors"]["base"] == error # Should be recoverable after hits error - with patch( - "homeassistant.components.vodafone_station.config_flow.VodafoneStationSercommApi.get_devices_data", - return_value={ - "wifi_user": "on|laptop|device-1|xx:xx:xx:xx:xx:xx|192.168.100.1||2.4G", - "ethernet": "laptop|device-2|yy:yy:yy:yy:yy:yy|192.168.100.2|;", - }, - ), patch( - "homeassistant.components.vodafone_station.config_flow.VodafoneStationSercommApi.login", - ), patch( - "homeassistant.components.vodafone_station.config_flow.VodafoneStationSercommApi.logout", - ), patch( - "homeassistant.components.vodafone_station.async_setup_entry", + with ( + patch( + "homeassistant.components.vodafone_station.config_flow.VodafoneStationSercommApi.get_devices_data", + return_value={ + "wifi_user": "on|laptop|device-1|xx:xx:xx:xx:xx:xx|192.168.100.1||2.4G", + "ethernet": "laptop|device-2|yy:yy:yy:yy:yy:yy|192.168.100.2|;", + }, + ), + patch( + "homeassistant.components.vodafone_station.config_flow.VodafoneStationSercommApi.login", + ), + patch( + "homeassistant.components.vodafone_station.config_flow.VodafoneStationSercommApi.logout", + ), + patch( + "homeassistant.components.vodafone_station.async_setup_entry", + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], diff --git a/tests/components/voicerss/test_tts.py b/tests/components/voicerss/test_tts.py index cb24d45246c..d1e7ba3c62f 100644 --- a/tests/components/voicerss/test_tts.py +++ b/tests/components/voicerss/test_tts.py @@ -1,4 +1,5 @@ """The tests for the VoiceRSS speech platform.""" + from http import HTTPStatus import pytest diff --git a/tests/components/voip/test_binary_sensor.py b/tests/components/voip/test_binary_sensor.py index 794d307ee01..58f1e0ea53b 100644 --- a/tests/components/voip/test_binary_sensor.py +++ b/tests/components/voip/test_binary_sensor.py @@ -1,4 +1,5 @@ """Test VoIP binary sensor devices.""" + from homeassistant.components.voip.devices import VoIPDevice from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant diff --git a/tests/components/voip/test_config_flow.py b/tests/components/voip/test_config_flow.py index f7b3595699c..079177db139 100644 --- a/tests/components/voip/test_config_flow.py +++ b/tests/components/voip/test_config_flow.py @@ -1,4 +1,5 @@ """Test VoIP config flow.""" + from unittest.mock import patch from homeassistant import config_entries, data_entry_flow diff --git a/tests/components/voip/test_devices.py b/tests/components/voip/test_devices.py index 189dff49839..55359b8407d 100644 --- a/tests/components/voip/test_devices.py +++ b/tests/components/voip/test_devices.py @@ -1,4 +1,5 @@ """Test VoIP devices.""" + from __future__ import annotations from voip_utils import CallInfo diff --git a/tests/components/voip/test_init.py b/tests/components/voip/test_init.py index edc55685597..a0d843a47bd 100644 --- a/tests/components/voip/test_init.py +++ b/tests/components/voip/test_init.py @@ -1,4 +1,5 @@ """Test VoIP init.""" + from syrupy.assertion import SnapshotAssertion from homeassistant.core import HomeAssistant diff --git a/tests/components/voip/test_select.py b/tests/components/voip/test_select.py index 7dd041a6866..a9741b44081 100644 --- a/tests/components/voip/test_select.py +++ b/tests/components/voip/test_select.py @@ -1,4 +1,5 @@ """Test VoIP select.""" + from homeassistant.components.voip.devices import VoIPDevice from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant diff --git a/tests/components/voip/test_sip.py b/tests/components/voip/test_sip.py index 975b8f326d9..769be768261 100644 --- a/tests/components/voip/test_sip.py +++ b/tests/components/voip/test_sip.py @@ -1,4 +1,5 @@ """Test SIP server.""" + import socket import pytest @@ -20,9 +21,10 @@ async def test_create_sip_server(hass: HomeAssistant, socket_enabled) -> None: entry = result["result"] await hass.async_block_till_done() - with pytest.raises(OSError), socket.socket( - socket.AF_INET, socket.SOCK_DGRAM - ) as sock: + with ( + pytest.raises(OSError), + socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock, + ): # Server should have the port sock.bind(("127.0.0.1", 5060)) @@ -40,9 +42,10 @@ async def test_create_sip_server(hass: HomeAssistant, socket_enabled) -> None: with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock: sock.bind(("127.0.0.1", 5060)) - with pytest.raises(OSError), socket.socket( - socket.AF_INET, socket.SOCK_DGRAM - ) as sock: + with ( + pytest.raises(OSError), + socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock, + ): # Server should now have the new port sock.bind(("127.0.0.1", 5061)) diff --git a/tests/components/voip/test_switch.py b/tests/components/voip/test_switch.py index eb8fcfa2220..8b3cd03f2ac 100644 --- a/tests/components/voip/test_switch.py +++ b/tests/components/voip/test_switch.py @@ -1,4 +1,5 @@ """Test VoIP switch devices.""" + from homeassistant.components.voip.devices import VoIPDevice from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant diff --git a/tests/components/voip/test_voip.py b/tests/components/voip/test_voip.py index 703b99db962..f5c5fde2518 100644 --- a/tests/components/voip/test_voip.py +++ b/tests/components/voip/test_voip.py @@ -1,4 +1,5 @@ """Test VoIP protocol.""" + import asyncio import io import time @@ -94,15 +95,19 @@ async def test_pipeline( assert media_source_id == _MEDIA_ID return ("wav", _empty_wav()) - with patch( - "homeassistant.components.assist_pipeline.vad.WebRtcVad.is_speech", - new=is_speech, - ), patch( - "homeassistant.components.voip.voip.async_pipeline_from_audio_stream", - new=async_pipeline_from_audio_stream, - ), patch( - "homeassistant.components.voip.voip.tts.async_get_media_source_audio", - new=async_get_media_source_audio, + with ( + patch( + "homeassistant.components.assist_pipeline.vad.WebRtcVad.is_speech", + new=is_speech, + ), + patch( + "homeassistant.components.voip.voip.async_pipeline_from_audio_stream", + new=async_pipeline_from_audio_stream, + ), + patch( + "homeassistant.components.voip.voip.tts.async_get_media_source_audio", + new=async_get_media_source_audio, + ), ): rtp_protocol = voip.voip.PipelineRtpDatagramProtocol( hass, @@ -149,12 +154,15 @@ async def test_pipeline_timeout(hass: HomeAssistant, voip_device: VoIPDevice) -> async def async_pipeline_from_audio_stream(*args, **kwargs): await asyncio.sleep(10) - with patch( - "homeassistant.components.voip.voip.async_pipeline_from_audio_stream", - new=async_pipeline_from_audio_stream, - ), patch( - "homeassistant.components.voip.voip.PipelineRtpDatagramProtocol._wait_for_speech", - return_value=True, + with ( + patch( + "homeassistant.components.voip.voip.async_pipeline_from_audio_stream", + new=async_pipeline_from_audio_stream, + ), + patch( + "homeassistant.components.voip.voip.PipelineRtpDatagramProtocol._wait_for_speech", + return_value=True, + ), ): rtp_protocol = voip.voip.PipelineRtpDatagramProtocol( hass, @@ -287,15 +295,19 @@ async def test_tts_timeout( # Should time out immediately return ("wav", _empty_wav()) - with patch( - "homeassistant.components.assist_pipeline.vad.WebRtcVad.is_speech", - new=is_speech, - ), patch( - "homeassistant.components.voip.voip.async_pipeline_from_audio_stream", - new=async_pipeline_from_audio_stream, - ), patch( - "homeassistant.components.voip.voip.tts.async_get_media_source_audio", - new=async_get_media_source_audio, + with ( + patch( + "homeassistant.components.assist_pipeline.vad.WebRtcVad.is_speech", + new=is_speech, + ), + patch( + "homeassistant.components.voip.voip.async_pipeline_from_audio_stream", + new=async_pipeline_from_audio_stream, + ), + patch( + "homeassistant.components.voip.voip.tts.async_get_media_source_audio", + new=async_get_media_source_audio, + ), ): rtp_protocol = voip.voip.PipelineRtpDatagramProtocol( hass, @@ -388,15 +400,19 @@ async def test_tts_wrong_extension( # Should fail because it's not "wav" return ("mp3", b"") - with patch( - "homeassistant.components.assist_pipeline.vad.WebRtcVad.is_speech", - new=is_speech, - ), patch( - "homeassistant.components.voip.voip.async_pipeline_from_audio_stream", - new=async_pipeline_from_audio_stream, - ), patch( - "homeassistant.components.voip.voip.tts.async_get_media_source_audio", - new=async_get_media_source_audio, + with ( + patch( + "homeassistant.components.assist_pipeline.vad.WebRtcVad.is_speech", + new=is_speech, + ), + patch( + "homeassistant.components.voip.voip.async_pipeline_from_audio_stream", + new=async_pipeline_from_audio_stream, + ), + patch( + "homeassistant.components.voip.voip.tts.async_get_media_source_audio", + new=async_get_media_source_audio, + ), ): rtp_protocol = voip.voip.PipelineRtpDatagramProtocol( hass, @@ -486,15 +502,19 @@ async def test_tts_wrong_wav_format( return ("wav", wav_io.getvalue()) - with patch( - "homeassistant.components.assist_pipeline.vad.WebRtcVad.is_speech", - new=is_speech, - ), patch( - "homeassistant.components.voip.voip.async_pipeline_from_audio_stream", - new=async_pipeline_from_audio_stream, - ), patch( - "homeassistant.components.voip.voip.tts.async_get_media_source_audio", - new=async_get_media_source_audio, + with ( + patch( + "homeassistant.components.assist_pipeline.vad.WebRtcVad.is_speech", + new=is_speech, + ), + patch( + "homeassistant.components.voip.voip.async_pipeline_from_audio_stream", + new=async_pipeline_from_audio_stream, + ), + patch( + "homeassistant.components.voip.voip.tts.async_get_media_source_audio", + new=async_get_media_source_audio, + ), ): rtp_protocol = voip.voip.PipelineRtpDatagramProtocol( hass, @@ -568,15 +588,19 @@ async def test_empty_tts_output( ) ) - with patch( - "homeassistant.components.assist_pipeline.vad.WebRtcVad.is_speech", - new=is_speech, - ), patch( - "homeassistant.components.voip.voip.async_pipeline_from_audio_stream", - new=async_pipeline_from_audio_stream, - ), patch( - "homeassistant.components.voip.voip.PipelineRtpDatagramProtocol._send_tts", - ) as mock_send_tts: + with ( + patch( + "homeassistant.components.assist_pipeline.vad.WebRtcVad.is_speech", + new=is_speech, + ), + patch( + "homeassistant.components.voip.voip.async_pipeline_from_audio_stream", + new=async_pipeline_from_audio_stream, + ), + patch( + "homeassistant.components.voip.voip.PipelineRtpDatagramProtocol._send_tts", + ) as mock_send_tts, + ): rtp_protocol = voip.voip.PipelineRtpDatagramProtocol( hass, hass.config.language, diff --git a/tests/components/volumio/test_config_flow.py b/tests/components/volumio/test_config_flow.py index 841b558eba3..7d185161d0a 100644 --- a/tests/components/volumio/test_config_flow.py +++ b/tests/components/volumio/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Volumio config flow.""" + from ipaddress import ip_address from unittest.mock import patch @@ -45,13 +46,16 @@ async def test_form(hass: HomeAssistant) -> None: assert result["type"] == "form" assert result["errors"] == {} - with patch( - "homeassistant.components.volumio.config_flow.Volumio.get_system_info", - return_value=TEST_SYSTEM_INFO, - ), patch( - "homeassistant.components.volumio.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.volumio.config_flow.Volumio.get_system_info", + return_value=TEST_SYSTEM_INFO, + ), + patch( + "homeassistant.components.volumio.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], TEST_CONNECTION, @@ -83,12 +87,15 @@ async def test_form_updates_unique_id(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch( - "homeassistant.components.volumio.config_flow.Volumio.get_system_info", - return_value=TEST_SYSTEM_INFO, - ), patch( - "homeassistant.components.volumio.async_setup_entry", - return_value=True, + with ( + patch( + "homeassistant.components.volumio.config_flow.Volumio.get_system_info", + return_value=TEST_SYSTEM_INFO, + ), + patch( + "homeassistant.components.volumio.async_setup_entry", + return_value=True, + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -110,13 +117,16 @@ async def test_empty_system_info(hass: HomeAssistant) -> None: assert result["type"] == "form" assert result["errors"] == {} - with patch( - "homeassistant.components.volumio.config_flow.Volumio.get_system_info", - return_value={}, - ), patch( - "homeassistant.components.volumio.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.volumio.config_flow.Volumio.get_system_info", + return_value={}, + ), + patch( + "homeassistant.components.volumio.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], TEST_CONNECTION, @@ -180,13 +190,16 @@ async def test_discovery(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=TEST_DISCOVERY ) - with patch( - "homeassistant.components.volumio.config_flow.Volumio.get_system_info", - return_value=TEST_SYSTEM_INFO, - ), patch( - "homeassistant.components.volumio.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.volumio.config_flow.Volumio.get_system_info", + return_value=TEST_SYSTEM_INFO, + ), + patch( + "homeassistant.components.volumio.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={}, diff --git a/tests/components/volvooncall/test_config_flow.py b/tests/components/volvooncall/test_config_flow.py index c8ed92d8ee5..3c866da58ea 100644 --- a/tests/components/volvooncall/test_config_flow.py +++ b/tests/components/volvooncall/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Volvo On Call config flow.""" + from unittest.mock import Mock, patch from aiohttp import ClientResponseError @@ -19,10 +20,13 @@ async def test_form(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.FORM assert len(result["errors"]) == 0 - with patch("volvooncall.Connection.get"), patch( - "homeassistant.components.volvooncall.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch("volvooncall.Connection.get"), + patch( + "homeassistant.components.volvooncall.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -85,9 +89,12 @@ async def test_flow_already_configured(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.FORM assert len(result["errors"]) == 0 - with patch("volvooncall.Connection.get"), patch( - "homeassistant.components.volvooncall.async_setup_entry", - return_value=True, + with ( + patch("volvooncall.Connection.get"), + patch( + "homeassistant.components.volvooncall.async_setup_entry", + return_value=True, + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], diff --git a/tests/components/vulcan/test_config_flow.py b/tests/components/vulcan/test_config_flow.py index d575fe73600..b0b928cfde2 100644 --- a/tests/components/vulcan/test_config_flow.py +++ b/tests/components/vulcan/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Uonet+ Vulcan config flow.""" + import json from unittest import mock from unittest.mock import patch @@ -87,8 +88,10 @@ async def test_config_flow_auth_success_with_multiple_students( mock_account.return_value = fake_account mock_student.return_value = [ Student.load(student) - for student in [load_fixture("fake_student_1.json", "vulcan")] - + [load_fixture("fake_student_2.json", "vulcan")] + for student in [ + load_fixture("fake_student_1.json", "vulcan"), + load_fixture("fake_student_2.json", "vulcan"), + ] ] result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -378,8 +381,9 @@ async def test_multiple_config_entries_using_saved_credentials_2( ) -> None: """Test a successful config flow for multiple config entries using saved credentials (different situation).""" mock_student.return_value = [ - Student.load(load_fixture("fake_student_1.json", "vulcan")) - ] + [Student.load(load_fixture("fake_student_2.json", "vulcan"))] + Student.load(load_fixture("fake_student_1.json", "vulcan")), + Student.load(load_fixture("fake_student_2.json", "vulcan")), + ] MockConfigEntry( domain=const.DOMAIN, unique_id="123456", @@ -476,8 +480,9 @@ async def test_multiple_config_entries_using_saved_credentials_4( ) -> None: """Test a successful config flow for multiple config entries using saved credentials (different situation).""" mock_student.return_value = [ - Student.load(load_fixture("fake_student_1.json", "vulcan")) - ] + [Student.load(load_fixture("fake_student_2.json", "vulcan"))] + Student.load(load_fixture("fake_student_1.json", "vulcan")), + Student.load(load_fixture("fake_student_2.json", "vulcan")), + ] MockConfigEntry( entry_id="456", domain=const.DOMAIN, diff --git a/tests/components/vultr/conftest.py b/tests/components/vultr/conftest.py index 0d69cf53335..f8ecd1cf321 100644 --- a/tests/components/vultr/conftest.py +++ b/tests/components/vultr/conftest.py @@ -1,4 +1,5 @@ """Test configuration for the Vultr tests.""" + import json from unittest.mock import patch diff --git a/tests/components/vultr/test_binary_sensor.py b/tests/components/vultr/test_binary_sensor.py index d7f421e4463..f6b46b54d25 100644 --- a/tests/components/vultr/test_binary_sensor.py +++ b/tests/components/vultr/test_binary_sensor.py @@ -1,4 +1,5 @@ """Test the Vultr binary sensor platform.""" + import pytest import voluptuous as vol diff --git a/tests/components/vultr/test_init.py b/tests/components/vultr/test_init.py index 86034cd3828..8c5ec51f584 100644 --- a/tests/components/vultr/test_init.py +++ b/tests/components/vultr/test_init.py @@ -1,4 +1,5 @@ """The tests for the Vultr component.""" + from copy import deepcopy import json from unittest.mock import patch diff --git a/tests/components/vultr/test_sensor.py b/tests/components/vultr/test_sensor.py index 0f2b1b7d4a4..f9f922b35d4 100644 --- a/tests/components/vultr/test_sensor.py +++ b/tests/components/vultr/test_sensor.py @@ -1,4 +1,5 @@ """The tests for the Vultr sensor platform.""" + import pytest import voluptuous as vol diff --git a/tests/components/vultr/test_switch.py b/tests/components/vultr/test_switch.py index feee545c365..f75021efa05 100644 --- a/tests/components/vultr/test_switch.py +++ b/tests/components/vultr/test_switch.py @@ -1,4 +1,5 @@ """Test the Vultr switch platform.""" + from __future__ import annotations import json @@ -98,10 +99,13 @@ def test_switch(hass: HomeAssistant, hass_devices: list[vultr.VultrSwitch]): @pytest.mark.usefixtures("valid_config") def test_turn_on(hass: HomeAssistant, hass_devices: list[vultr.VultrSwitch]): """Test turning a subscription on.""" - with patch( - "vultr.Vultr.server_list", - return_value=json.loads(load_fixture("server_list.json", "vultr")), - ), patch("vultr.Vultr.server_start") as mock_start: + with ( + patch( + "vultr.Vultr.server_list", + return_value=json.loads(load_fixture("server_list.json", "vultr")), + ), + patch("vultr.Vultr.server_start") as mock_start, + ): for device in hass_devices: if device.name == "Failed Server": device.update() @@ -114,10 +118,13 @@ def test_turn_on(hass: HomeAssistant, hass_devices: list[vultr.VultrSwitch]): @pytest.mark.usefixtures("valid_config") def test_turn_off(hass: HomeAssistant, hass_devices: list[vultr.VultrSwitch]): """Test turning a subscription off.""" - with patch( - "vultr.Vultr.server_list", - return_value=json.loads(load_fixture("server_list.json", "vultr")), - ), patch("vultr.Vultr.server_halt") as mock_halt: + with ( + patch( + "vultr.Vultr.server_list", + return_value=json.loads(load_fixture("server_list.json", "vultr")), + ), + patch("vultr.Vultr.server_halt") as mock_halt, + ): for device in hass_devices: if device.name == "A Server": device.update() @@ -140,8 +147,7 @@ def test_invalid_switches(hass: HomeAssistant) -> None: def add_entities(devices, action): """Mock add devices.""" - for device in devices: - hass_devices.append(device) + hass_devices.extend(devices) bad_conf = {} # No subscription diff --git a/tests/components/wake_on_lan/conftest.py b/tests/components/wake_on_lan/conftest.py index 5fa44f10c2c..66782531ef1 100644 --- a/tests/components/wake_on_lan/conftest.py +++ b/tests/components/wake_on_lan/conftest.py @@ -1,4 +1,5 @@ """Test fixtures for Wake on Lan.""" + from __future__ import annotations from collections.abc import Generator diff --git a/tests/components/wake_on_lan/test_init.py b/tests/components/wake_on_lan/test_init.py index 1cfe2fa7436..8cfb0e6491e 100644 --- a/tests/components/wake_on_lan/test_init.py +++ b/tests/components/wake_on_lan/test_init.py @@ -1,4 +1,5 @@ """Tests for Wake On LAN component.""" + from __future__ import annotations from unittest.mock import patch diff --git a/tests/components/wake_on_lan/test_switch.py b/tests/components/wake_on_lan/test_switch.py index b2702ed1815..77e1ba55519 100644 --- a/tests/components/wake_on_lan/test_switch.py +++ b/tests/components/wake_on_lan/test_switch.py @@ -1,4 +1,5 @@ """The tests for the wake on lan switch platform.""" + from __future__ import annotations from unittest.mock import AsyncMock, patch diff --git a/tests/components/wake_word/common.py b/tests/components/wake_word/common.py index f732044bc13..9e2c570c6d0 100644 --- a/tests/components/wake_word/common.py +++ b/tests/components/wake_word/common.py @@ -1,4 +1,5 @@ """Provide common test tools for wake-word-detection.""" + from __future__ import annotations from collections.abc import Callable, Coroutine diff --git a/tests/components/wallbox/conftest.py b/tests/components/wallbox/conftest.py index 4677dc95e6f..72d493ceb69 100644 --- a/tests/components/wallbox/conftest.py +++ b/tests/components/wallbox/conftest.py @@ -1,4 +1,5 @@ """Test fixtures for the Wallbox integration.""" + import pytest from homeassistant.components.wallbox.const import CONF_STATION, DOMAIN diff --git a/tests/components/wallbox/const.py b/tests/components/wallbox/const.py index 00ede14771b..452b3af0af8 100644 --- a/tests/components/wallbox/const.py +++ b/tests/components/wallbox/const.py @@ -1,4 +1,5 @@ """Provides constants for Wallbox component tests.""" + JWT = "jwt" USER_ID = "user_id" TTL = "ttl" diff --git a/tests/components/wallbox/test_config_flow.py b/tests/components/wallbox/test_config_flow.py index 20ad693c696..ebb3a2fd693 100644 --- a/tests/components/wallbox/test_config_flow.py +++ b/tests/components/wallbox/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Wallbox config flow.""" + from http import HTTPStatus import json @@ -42,7 +43,7 @@ test_response = json.loads( async def test_show_set_form(hass: HomeAssistant) -> None: """Test that the setup form is served.""" - flow = config_flow.ConfigFlow() + flow = config_flow.WallboxConfigFlow() flow.hass = hass result = await flow.async_step_user(user_input=None) diff --git a/tests/components/wallbox/test_init.py b/tests/components/wallbox/test_init.py index 93082737f1f..3dfc391aa3b 100644 --- a/tests/components/wallbox/test_init.py +++ b/tests/components/wallbox/test_init.py @@ -1,4 +1,5 @@ """Test Wallbox Init Component.""" + import json import requests_mock diff --git a/tests/components/wallbox/test_lock.py b/tests/components/wallbox/test_lock.py index 065a43b2789..637f0c827f4 100644 --- a/tests/components/wallbox/test_lock.py +++ b/tests/components/wallbox/test_lock.py @@ -1,4 +1,5 @@ """Test Wallbox Lock component.""" + import json import pytest diff --git a/tests/components/wallbox/test_number.py b/tests/components/wallbox/test_number.py index 837df4dfd47..5d782224ce5 100644 --- a/tests/components/wallbox/test_number.py +++ b/tests/components/wallbox/test_number.py @@ -1,4 +1,5 @@ """Test Wallbox Switch component.""" + import json import pytest diff --git a/tests/components/wallbox/test_sensor.py b/tests/components/wallbox/test_sensor.py index ca12e1d9ac3..5a8b3c290c1 100644 --- a/tests/components/wallbox/test_sensor.py +++ b/tests/components/wallbox/test_sensor.py @@ -1,5 +1,6 @@ """Test Wallbox Switch component.""" -from homeassistant.const import CONF_ICON, CONF_UNIT_OF_MEASUREMENT, UnitOfPower + +from homeassistant.const import CONF_UNIT_OF_MEASUREMENT, UnitOfPower from homeassistant.core import HomeAssistant from . import setup_integration @@ -24,7 +25,6 @@ async def test_wallbox_sensor_class( assert state.name == "Wallbox WallboxName Charging power" state = hass.states.get(MOCK_SENSOR_CHARGING_SPEED_ID) - assert state.attributes[CONF_ICON] == "mdi:speedometer" assert state.name == "Wallbox WallboxName Charging speed" # Test round with precision '0' works diff --git a/tests/components/wallbox/test_switch.py b/tests/components/wallbox/test_switch.py index edd85c6ccc7..d06251db003 100644 --- a/tests/components/wallbox/test_switch.py +++ b/tests/components/wallbox/test_switch.py @@ -1,4 +1,5 @@ """Test Wallbox Lock component.""" + import json import pytest diff --git a/tests/components/waqi/conftest.py b/tests/components/waqi/conftest.py index 176c1e27d8f..f42c8be6097 100644 --- a/tests/components/waqi/conftest.py +++ b/tests/components/waqi/conftest.py @@ -1,4 +1,5 @@ """Common fixtures for the World Air Quality Index (WAQI) tests.""" + from collections.abc import Generator from unittest.mock import AsyncMock, patch diff --git a/tests/components/waqi/snapshots/test_sensor.ambr b/tests/components/waqi/snapshots/test_sensor.ambr index 029b36b3c16..f476514a6c7 100644 --- a/tests/components/waqi/snapshots/test_sensor.ambr +++ b/tests/components/waqi/snapshots/test_sensor.ambr @@ -20,6 +20,7 @@ 'context': , 'entity_id': 'sensor.de_jongweg_utrecht_air_quality_index', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '29', }) @@ -36,6 +37,7 @@ 'context': , 'entity_id': 'sensor.de_jongweg_utrecht_humidity', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '80', }) @@ -50,6 +52,7 @@ 'context': , 'entity_id': 'sensor.de_jongweg_utrecht_visbility_using_nephelometry', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '80', }) @@ -73,6 +76,7 @@ 'context': , 'entity_id': 'sensor.de_jongweg_utrecht_dominant_pollutant', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'o3', }) @@ -89,6 +93,7 @@ 'context': , 'entity_id': 'sensor.de_jongweg_utrecht_pressure', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '1008.8', }) @@ -105,6 +110,7 @@ 'context': , 'entity_id': 'sensor.de_jongweg_utrecht_temperature', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '16', }) @@ -119,6 +125,7 @@ 'context': , 'entity_id': 'sensor.de_jongweg_utrecht_carbon_monoxide', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '2.3', }) @@ -133,6 +140,7 @@ 'context': , 'entity_id': 'sensor.de_jongweg_utrecht_nitrogen_dioxide', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '2.3', }) @@ -147,6 +155,7 @@ 'context': , 'entity_id': 'sensor.de_jongweg_utrecht_ozone', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '29.4', }) @@ -161,6 +170,7 @@ 'context': , 'entity_id': 'sensor.de_jongweg_utrecht_sulphur_dioxide', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '2.3', }) @@ -175,6 +185,7 @@ 'context': , 'entity_id': 'sensor.de_jongweg_utrecht_pm10', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '12', }) @@ -189,6 +200,7 @@ 'context': , 'entity_id': 'sensor.de_jongweg_utrecht_pm2_5', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '17', }) diff --git a/tests/components/waqi/test_config_flow.py b/tests/components/waqi/test_config_flow.py index ecc7e07158d..712ad8dd39e 100644 --- a/tests/components/waqi/test_config_flow.py +++ b/tests/components/waqi/test_config_flow.py @@ -1,4 +1,5 @@ """Test the World Air Quality Index (WAQI) config flow.""" + import json from typing import Any from unittest.mock import AsyncMock, patch @@ -53,12 +54,15 @@ async def test_full_map_flow( ) assert result["type"] == FlowResultType.FORM - with patch( - "aiowaqi.WAQIClient.authenticate", - ), patch( - "aiowaqi.WAQIClient.get_by_ip", - return_value=WAQIAirQuality.from_dict( - json.loads(load_fixture("waqi/air_quality_sensor.json")) + with ( + patch( + "aiowaqi.WAQIClient.authenticate", + ), + patch( + "aiowaqi.WAQIClient.get_by_ip", + return_value=WAQIAirQuality.from_dict( + json.loads(load_fixture("waqi/air_quality_sensor.json")) + ), ), ): result = await hass.config_entries.flow.async_configure( @@ -70,17 +74,21 @@ async def test_full_map_flow( assert result["type"] == FlowResultType.FORM assert result["step_id"] == method - with patch( - "aiowaqi.WAQIClient.authenticate", - ), patch( - "aiowaqi.WAQIClient.get_by_coordinates", - return_value=WAQIAirQuality.from_dict( - json.loads(load_fixture("waqi/air_quality_sensor.json")) + with ( + patch( + "aiowaqi.WAQIClient.authenticate", ), - ), patch( - "aiowaqi.WAQIClient.get_by_station_number", - return_value=WAQIAirQuality.from_dict( - json.loads(load_fixture("waqi/air_quality_sensor.json")) + patch( + "aiowaqi.WAQIClient.get_by_coordinates", + return_value=WAQIAirQuality.from_dict( + json.loads(load_fixture("waqi/air_quality_sensor.json")) + ), + ), + patch( + "aiowaqi.WAQIClient.get_by_station_number", + return_value=WAQIAirQuality.from_dict( + json.loads(load_fixture("waqi/air_quality_sensor.json")) + ), ), ): result = await hass.config_entries.flow.async_configure( @@ -114,11 +122,14 @@ async def test_flow_errors( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch( - "aiowaqi.WAQIClient.authenticate", - ), patch( - "aiowaqi.WAQIClient.get_by_ip", - side_effect=exception, + with ( + patch( + "aiowaqi.WAQIClient.authenticate", + ), + patch( + "aiowaqi.WAQIClient.get_by_ip", + side_effect=exception, + ), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -129,12 +140,15 @@ async def test_flow_errors( assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": error} - with patch( - "aiowaqi.WAQIClient.authenticate", - ), patch( - "aiowaqi.WAQIClient.get_by_ip", - return_value=WAQIAirQuality.from_dict( - json.loads(load_fixture("waqi/air_quality_sensor.json")) + with ( + patch( + "aiowaqi.WAQIClient.authenticate", + ), + patch( + "aiowaqi.WAQIClient.get_by_ip", + return_value=WAQIAirQuality.from_dict( + json.loads(load_fixture("waqi/air_quality_sensor.json")) + ), ), ): result = await hass.config_entries.flow.async_configure( @@ -146,12 +160,15 @@ async def test_flow_errors( assert result["type"] == FlowResultType.FORM assert result["step_id"] == "map" - with patch( - "aiowaqi.WAQIClient.authenticate", - ), patch( - "aiowaqi.WAQIClient.get_by_coordinates", - return_value=WAQIAirQuality.from_dict( - json.loads(load_fixture("waqi/air_quality_sensor.json")) + with ( + patch( + "aiowaqi.WAQIClient.authenticate", + ), + patch( + "aiowaqi.WAQIClient.get_by_coordinates", + return_value=WAQIAirQuality.from_dict( + json.loads(load_fixture("waqi/air_quality_sensor.json")) + ), ), ): result = await hass.config_entries.flow.async_configure( @@ -216,12 +233,15 @@ async def test_error_in_second_step( ) assert result["type"] == FlowResultType.FORM - with patch( - "aiowaqi.WAQIClient.authenticate", - ), patch( - "aiowaqi.WAQIClient.get_by_ip", - return_value=WAQIAirQuality.from_dict( - json.loads(load_fixture("waqi/air_quality_sensor.json")) + with ( + patch( + "aiowaqi.WAQIClient.authenticate", + ), + patch( + "aiowaqi.WAQIClient.get_by_ip", + return_value=WAQIAirQuality.from_dict( + json.loads(load_fixture("waqi/air_quality_sensor.json")) + ), ), ): result = await hass.config_entries.flow.async_configure( @@ -233,10 +253,12 @@ async def test_error_in_second_step( assert result["type"] == FlowResultType.FORM assert result["step_id"] == method - with patch( - "aiowaqi.WAQIClient.authenticate", - ), patch("aiowaqi.WAQIClient.get_by_coordinates", side_effect=exception), patch( - "aiowaqi.WAQIClient.get_by_station_number", side_effect=exception + with ( + patch( + "aiowaqi.WAQIClient.authenticate", + ), + patch("aiowaqi.WAQIClient.get_by_coordinates", side_effect=exception), + patch("aiowaqi.WAQIClient.get_by_station_number", side_effect=exception), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -247,17 +269,21 @@ async def test_error_in_second_step( assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": error} - with patch( - "aiowaqi.WAQIClient.authenticate", - ), patch( - "aiowaqi.WAQIClient.get_by_coordinates", - return_value=WAQIAirQuality.from_dict( - json.loads(load_fixture("waqi/air_quality_sensor.json")) + with ( + patch( + "aiowaqi.WAQIClient.authenticate", ), - ), patch( - "aiowaqi.WAQIClient.get_by_station_number", - return_value=WAQIAirQuality.from_dict( - json.loads(load_fixture("waqi/air_quality_sensor.json")) + patch( + "aiowaqi.WAQIClient.get_by_coordinates", + return_value=WAQIAirQuality.from_dict( + json.loads(load_fixture("waqi/air_quality_sensor.json")) + ), + ), + patch( + "aiowaqi.WAQIClient.get_by_station_number", + return_value=WAQIAirQuality.from_dict( + json.loads(load_fixture("waqi/air_quality_sensor.json")) + ), ), ): result = await hass.config_entries.flow.async_configure( diff --git a/tests/components/waqi/test_sensor.py b/tests/components/waqi/test_sensor.py index ebe0c87736d..328fe99330e 100644 --- a/tests/components/waqi/test_sensor.py +++ b/tests/components/waqi/test_sensor.py @@ -1,126 +1,22 @@ """Test the World Air Quality Index (WAQI) sensor.""" + import json from unittest.mock import patch -from aiowaqi import WAQIAirQuality, WAQIError, WAQISearchResult +from aiowaqi import WAQIAirQuality, WAQIError import pytest from syrupy import SnapshotAssertion from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.components.waqi.const import CONF_STATION_NUMBER, DOMAIN -from homeassistant.components.waqi.sensor import CONF_LOCATIONS, CONF_STATIONS, SENSORS -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntryState -from homeassistant.const import ( - CONF_API_KEY, - CONF_NAME, - CONF_PLATFORM, - CONF_TOKEN, - Platform, -) +from homeassistant.components.waqi.const import DOMAIN +from homeassistant.components.waqi.sensor import SENSORS +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er, issue_registry as ir +from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry, load_fixture -LEGACY_CONFIG = { - Platform.SENSOR: [ - { - CONF_PLATFORM: DOMAIN, - CONF_TOKEN: "asd", - CONF_LOCATIONS: ["utrecht"], - CONF_STATIONS: [6332], - } - ] -} - - -async def test_legacy_migration(hass: HomeAssistant) -> None: - """Test migration from yaml to config flow.""" - search_result_json = json.loads(load_fixture("waqi/search_result.json")) - search_results = [ - WAQISearchResult.from_dict(search_result) - for search_result in search_result_json - ] - with patch( - "aiowaqi.WAQIClient.search", - return_value=search_results, - ), patch( - "aiowaqi.WAQIClient.get_by_station_number", - return_value=WAQIAirQuality.from_dict( - json.loads(load_fixture("waqi/air_quality_sensor.json")) - ), - ): - assert await async_setup_component(hass, Platform.SENSOR, LEGACY_CONFIG) - await hass.async_block_till_done() - entries = hass.config_entries.async_entries(DOMAIN) - assert len(entries) == 1 - assert entries[0].state is ConfigEntryState.LOADED - issue_registry = ir.async_get(hass) - assert len(issue_registry.issues) == 1 - - -async def test_legacy_migration_already_imported( - hass: HomeAssistant, mock_config_entry: MockConfigEntry -) -> None: - """Test migration from yaml to config flow after already imported.""" - mock_config_entry.add_to_hass(hass) - with patch( - "aiowaqi.WAQIClient.get_by_station_number", - return_value=WAQIAirQuality.from_dict( - json.loads(load_fixture("waqi/air_quality_sensor.json")) - ), - ): - assert await async_setup_component(hass, DOMAIN, {}) - await hass.async_block_till_done() - - state = hass.states.get("sensor.de_jongweg_utrecht_air_quality_index") - assert state.state == "29" - - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={ - CONF_STATION_NUMBER: 4584, - CONF_NAME: "xyz", - CONF_API_KEY: "asd", - }, - ) - ) - await hass.async_block_till_done() - entries = hass.config_entries.async_entries(DOMAIN) - assert len(entries) == 1 - assert entries[0].state is ConfigEntryState.LOADED - issue_registry = ir.async_get(hass) - assert len(issue_registry.issues) == 1 - - -async def test_sensor_id_migration( - hass: HomeAssistant, mock_config_entry: MockConfigEntry -) -> None: - """Test migrating unique id for original sensor.""" - mock_config_entry.add_to_hass(hass) - entity_registry = er.async_get(hass) - entity_registry.async_get_or_create( - SENSOR_DOMAIN, DOMAIN, 4584, config_entry=mock_config_entry - ) - with patch( - "aiowaqi.WAQIClient.get_by_station_number", - return_value=WAQIAirQuality.from_dict( - json.loads(load_fixture("waqi/air_quality_sensor.json")) - ), - ): - assert await async_setup_component(hass, DOMAIN, {}) - await hass.async_block_till_done() - entities = er.async_entries_for_config_entry( - entity_registry, mock_config_entry.entry_id - ) - assert len(entities) == 12 - assert hass.states.get("sensor.waqi_4584") - assert hass.states.get("sensor.de_jongweg_utrecht_air_quality_index") is None - assert entities[0].unique_id == "4584_air_quality" - @pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_sensor( diff --git a/tests/components/water_heater/common.py b/tests/components/water_heater/common.py index 0d2d73d17fd..9e47af4a19f 100644 --- a/tests/components/water_heater/common.py +++ b/tests/components/water_heater/common.py @@ -3,6 +3,7 @@ All containing methods are legacy helpers that should not be used by new components. Instead call the service directly. """ + from homeassistant.components.water_heater import ( _LOGGER, ATTR_AWAY_MODE, diff --git a/tests/components/water_heater/conftest.py b/tests/components/water_heater/conftest.py index 0ce869ab724..d6858fe08e1 100644 --- a/tests/components/water_heater/conftest.py +++ b/tests/components/water_heater/conftest.py @@ -1,4 +1,5 @@ """Fixtures for water heater platform tests.""" + from collections.abc import Generator import pytest diff --git a/tests/components/water_heater/test_device_action.py b/tests/components/water_heater/test_device_action.py index 8254fb77a77..d456fa7be71 100644 --- a/tests/components/water_heater/test_device_action.py +++ b/tests/components/water_heater/test_device_action.py @@ -1,4 +1,5 @@ """The tests for Water Heater device actions.""" + import pytest from pytest_unordered import unordered @@ -56,12 +57,12 @@ async def test_get_actions( @pytest.mark.parametrize( ("hidden_by", "entity_category"), - ( + [ (RegistryEntryHider.INTEGRATION, None), (RegistryEntryHider.USER, None), (None, EntityCategory.CONFIG), (None, EntityCategory.DIAGNOSTIC), - ), + ], ) async def test_get_actions_hidden_auxiliary( hass: HomeAssistant, diff --git a/tests/components/water_heater/test_init.py b/tests/components/water_heater/test_init.py index c6f1e729edd..f883cf47b19 100644 --- a/tests/components/water_heater/test_init.py +++ b/tests/components/water_heater/test_init.py @@ -1,4 +1,5 @@ """The tests for the water heater component.""" + from __future__ import annotations from unittest import mock @@ -174,7 +175,7 @@ async def test_operation_mode_validation( DOMAIN, SERVICE_SET_OPERATION_MODE, data, blocking=True ) assert ( - str(exc.value) == "Operation mode test not valid for entity water_heater.test. " + str(exc.value) == "Operation mode test is not valid for water_heater.test. " "The operation list is not defined" ) assert exc.value.translation_domain == DOMAIN @@ -190,7 +191,7 @@ async def test_operation_mode_validation( DOMAIN, SERVICE_SET_OPERATION_MODE, data, blocking=True ) assert ( - str(exc.value) == "Operation mode test not valid for entity water_heater.test. " + str(exc.value) == "Operation mode test is not valid for water_heater.test. " "Valid operation modes are: gas, eco" ) assert exc.value.translation_domain == DOMAIN diff --git a/tests/components/water_heater/test_recorder.py b/tests/components/water_heater/test_recorder.py index c7a2e61ba4c..4b004ce8d5d 100644 --- a/tests/components/water_heater/test_recorder.py +++ b/tests/components/water_heater/test_recorder.py @@ -1,4 +1,5 @@ """The tests for water_heater recorder.""" + from __future__ import annotations from datetime import timedelta @@ -12,7 +13,7 @@ from homeassistant.components.water_heater import ( ATTR_OPERATION_LIST, ) from homeassistant.const import ATTR_FRIENDLY_NAME -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, split_entity_id from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util @@ -35,9 +36,13 @@ async def test_exclude_attributes(recorder_mock: Recorder, hass: HomeAssistant) get_significant_states, hass, now, None, hass.states.async_entity_ids() ) assert len(states) >= 1 - for entity_states in states.values(): - for state in entity_states: - assert ATTR_OPERATION_LIST not in state.attributes - assert ATTR_MIN_TEMP not in state.attributes - assert ATTR_MAX_TEMP not in state.attributes - assert ATTR_FRIENDLY_NAME in state.attributes + for state in ( + state + for entity_states in states.values() + for state in entity_states + if split_entity_id(state.entity_id)[0] == water_heater.DOMAIN + ): + assert ATTR_OPERATION_LIST not in state.attributes + assert ATTR_MIN_TEMP not in state.attributes + assert ATTR_MAX_TEMP not in state.attributes + assert ATTR_FRIENDLY_NAME in state.attributes diff --git a/tests/components/water_heater/test_reproduce_state.py b/tests/components/water_heater/test_reproduce_state.py index a050d28a7d2..2aa10fa004f 100644 --- a/tests/components/water_heater/test_reproduce_state.py +++ b/tests/components/water_heater/test_reproduce_state.py @@ -1,4 +1,5 @@ """Test reproduce state for Water heater.""" + import pytest from homeassistant.components.water_heater import ( diff --git a/tests/components/water_heater/test_significant_change.py b/tests/components/water_heater/test_significant_change.py index 40803eea09a..dd0a8cc5924 100644 --- a/tests/components/water_heater/test_significant_change.py +++ b/tests/components/water_heater/test_significant_change.py @@ -1,4 +1,5 @@ """Test the Water Heater significant change platform.""" + import pytest from homeassistant.components.water_heater import ( diff --git a/tests/components/watttime/conftest.py b/tests/components/watttime/conftest.py index f636ffefcfb..0b7403d45fc 100644 --- a/tests/components/watttime/conftest.py +++ b/tests/components/watttime/conftest.py @@ -1,4 +1,5 @@ """Define test fixtures for WattTime.""" + import json from unittest.mock import AsyncMock, Mock, patch @@ -101,12 +102,16 @@ def get_grid_region_fixture(data_grid_region): @pytest.fixture(name="setup_watttime") async def setup_watttime_fixture(hass, client, config_auth, config_coordinates): """Define a fixture to set up WattTime.""" - with patch( - "homeassistant.components.watttime.Client.async_login", return_value=client - ), patch( - "homeassistant.components.watttime.config_flow.Client.async_login", - return_value=client, - ), patch("homeassistant.components.watttime.PLATFORMS", []): + with ( + patch( + "homeassistant.components.watttime.Client.async_login", return_value=client + ), + patch( + "homeassistant.components.watttime.config_flow.Client.async_login", + return_value=client, + ), + patch("homeassistant.components.watttime.PLATFORMS", []), + ): assert await async_setup_component( hass, DOMAIN, {**config_auth, **config_coordinates} ) diff --git a/tests/components/watttime/test_config_flow.py b/tests/components/watttime/test_config_flow.py index ce9284924f5..c8e4ed5b06b 100644 --- a/tests/components/watttime/test_config_flow.py +++ b/tests/components/watttime/test_config_flow.py @@ -1,4 +1,5 @@ """Test the WattTime config flow.""" + from unittest.mock import AsyncMock, patch from aiowatttime.errors import CoordinatesNotFoundError, InvalidCredentialsError diff --git a/tests/components/watttime/test_diagnostics.py b/tests/components/watttime/test_diagnostics.py index 1f45ba870fc..0526a64aedc 100644 --- a/tests/components/watttime/test_diagnostics.py +++ b/tests/components/watttime/test_diagnostics.py @@ -1,4 +1,5 @@ """Test WattTime diagnostics.""" + from syrupy import SnapshotAssertion from syrupy.filters import props diff --git a/tests/components/waze_travel_time/conftest.py b/tests/components/waze_travel_time/conftest.py index 64c05a5dcc1..01642ace86a 100644 --- a/tests/components/waze_travel_time/conftest.py +++ b/tests/components/waze_travel_time/conftest.py @@ -1,16 +1,30 @@ """Fixtures for Waze Travel Time tests.""" + from unittest.mock import patch import pytest -from pywaze.route_calculator import WRCError +from pywaze.route_calculator import CalcRoutesResponse, WRCError @pytest.fixture(name="mock_update") def mock_update_fixture(): """Mock an update to the sensor.""" with patch( - "pywaze.route_calculator.WazeRouteCalculator.calc_all_routes_info", - return_value={"My route": (150, 300)}, + "pywaze.route_calculator.WazeRouteCalculator.calc_routes", + return_value=[ + CalcRoutesResponse( + distance=300, + duration=150, + name="E1337 - Teststreet", + street_names=["E1337", "IncludeThis", "Teststreet"], + ), + CalcRoutesResponse( + distance=500, + duration=600, + name="E0815 - Otherstreet", + street_names=["E0815", "ExcludeThis", "Otherstreet"], + ), + ], ) as mock_wrc: yield mock_wrc diff --git a/tests/components/waze_travel_time/test_config_flow.py b/tests/components/waze_travel_time/test_config_flow.py index f1fd3041d46..9bd5016c2f4 100644 --- a/tests/components/waze_travel_time/test_config_flow.py +++ b/tests/components/waze_travel_time/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Waze Travel Time config flow.""" + import pytest from homeassistant import config_entries, data_entry_flow diff --git a/tests/components/waze_travel_time/test_sensor.py b/tests/components/waze_travel_time/test_sensor.py index adcc334889d..db0ece32cae 100644 --- a/tests/components/waze_travel_time/test_sensor.py +++ b/tests/components/waze_travel_time/test_sensor.py @@ -1,4 +1,5 @@ """Test Waze Travel Time sensors.""" + import pytest from pywaze.route_calculator import WRCError @@ -6,12 +7,15 @@ from homeassistant.components.waze_travel_time.const import ( CONF_AVOID_FERRIES, CONF_AVOID_SUBSCRIPTION_ROADS, CONF_AVOID_TOLL_ROADS, + CONF_EXCL_FILTER, + CONF_INCL_FILTER, CONF_REALTIME, CONF_UNITS, CONF_VEHICLE_TYPE, DEFAULT_OPTIONS, DOMAIN, IMPERIAL_UNITS, + METRIC_UNITS, ) from homeassistant.core import HomeAssistant @@ -21,7 +25,7 @@ from tests.common import MockConfigEntry @pytest.fixture(name="mock_config") -async def mock_config_fixture(hass, data, options): +async def mock_config_fixture(hass: HomeAssistant, data, options): """Mock a Waze Travel Time config entry.""" config_entry = MockConfigEntry( domain=DOMAIN, @@ -41,13 +45,6 @@ def mock_update_wrcerror_fixture(mock_update): return mock_update -@pytest.fixture(name="mock_update_keyerror") -def mock_update_keyerror_fixture(mock_update): - """Mock an update to the sensor failed with KeyError.""" - mock_update.side_effect = KeyError("test") - return mock_update - - @pytest.mark.parametrize( ("data", "options"), [(MOCK_CONFIG, DEFAULT_OPTIONS)], @@ -62,7 +59,10 @@ async def test_sensor(hass: HomeAssistant) -> None: ) assert hass.states.get("sensor.waze_travel_time").attributes["duration"] == 150 assert hass.states.get("sensor.waze_travel_time").attributes["distance"] == 300 - assert hass.states.get("sensor.waze_travel_time").attributes["route"] == "My route" + assert ( + hass.states.get("sensor.waze_travel_time").attributes["route"] + == "E1337 - Teststreet" + ) assert ( hass.states.get("sensor.waze_travel_time").attributes["origin"] == "location1" ) @@ -74,7 +74,6 @@ async def test_sensor(hass: HomeAssistant) -> None: hass.states.get("sensor.waze_travel_time").attributes["unit_of_measurement"] == "min" ) - assert hass.states.get("sensor.waze_travel_time").attributes["icon"] == "mdi:car" @pytest.mark.parametrize( @@ -101,6 +100,52 @@ async def test_imperial(hass: HomeAssistant) -> None: ] == pytest.approx(186.4113) +@pytest.mark.parametrize( + ("data", "options"), + [ + ( + MOCK_CONFIG, + { + CONF_UNITS: METRIC_UNITS, + CONF_REALTIME: True, + CONF_VEHICLE_TYPE: "car", + CONF_AVOID_TOLL_ROADS: True, + CONF_AVOID_SUBSCRIPTION_ROADS: True, + CONF_AVOID_FERRIES: True, + CONF_INCL_FILTER: "IncludeThis", + }, + ) + ], +) +@pytest.mark.usefixtures("mock_update", "mock_config") +async def test_incl_filter(hass: HomeAssistant) -> None: + """Test that incl_filter only includes route with the wanted street name.""" + assert hass.states.get("sensor.waze_travel_time").attributes["distance"] == 300 + + +@pytest.mark.parametrize( + ("data", "options"), + [ + ( + MOCK_CONFIG, + { + CONF_UNITS: METRIC_UNITS, + CONF_REALTIME: True, + CONF_VEHICLE_TYPE: "car", + CONF_AVOID_TOLL_ROADS: True, + CONF_AVOID_SUBSCRIPTION_ROADS: True, + CONF_AVOID_FERRIES: True, + CONF_EXCL_FILTER: "ExcludeThis", + }, + ) + ], +) +@pytest.mark.usefixtures("mock_update", "mock_config") +async def test_excl_filter(hass: HomeAssistant) -> None: + """Test that excl_filter only includes route without the street name.""" + assert hass.states.get("sensor.waze_travel_time").attributes["distance"] == 300 + + @pytest.mark.usefixtures("mock_update_wrcerror") async def test_sensor_failed_wrcerror( hass: HomeAssistant, caplog: pytest.LogCaptureFixture @@ -115,19 +160,3 @@ async def test_sensor_failed_wrcerror( assert hass.states.get("sensor.waze_travel_time").state == "unknown" assert "Error on retrieving data: " in caplog.text - - -@pytest.mark.usefixtures("mock_update_keyerror") -async def test_sensor_failed_keyerror( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture -) -> None: - """Test that sensor update fails with log message.""" - config_entry = MockConfigEntry( - domain=DOMAIN, data=MOCK_CONFIG, options=DEFAULT_OPTIONS, entry_id="test" - ) - config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - assert hass.states.get("sensor.waze_travel_time").state == "unknown" - assert "Error retrieving data from server" in caplog.text diff --git a/tests/components/weather/__init__.py b/tests/components/weather/__init__.py index 35a818735d0..c24baad5237 100644 --- a/tests/components/weather/__init__.py +++ b/tests/components/weather/__init__.py @@ -1,6 +1,5 @@ """The tests for Weather platforms.""" - from typing import Any from homeassistant.components.weather import ( diff --git a/tests/components/weather/conftest.py b/tests/components/weather/conftest.py index a85b5e85d4b..073af7ab8ef 100644 --- a/tests/components/weather/conftest.py +++ b/tests/components/weather/conftest.py @@ -1,4 +1,5 @@ """Fixtures for Weather platform tests.""" + from collections.abc import Generator import pytest diff --git a/tests/components/weather/test_init.py b/tests/components/weather/test_init.py index b982ab610ec..195a4c9ef67 100644 --- a/tests/components/weather/test_init.py +++ b/tests/components/weather/test_init.py @@ -1,4 +1,5 @@ """The test for weather entity.""" + from datetime import datetime import pytest @@ -6,17 +7,6 @@ from syrupy.assertion import SnapshotAssertion from homeassistant.components.weather import ( ATTR_CONDITION_SUNNY, - ATTR_FORECAST, - ATTR_FORECAST_APPARENT_TEMP, - ATTR_FORECAST_DEW_POINT, - ATTR_FORECAST_HUMIDITY, - ATTR_FORECAST_PRECIPITATION, - ATTR_FORECAST_PRESSURE, - ATTR_FORECAST_TEMP, - ATTR_FORECAST_TEMP_LOW, - ATTR_FORECAST_UV_INDEX, - ATTR_FORECAST_WIND_GUST_SPEED, - ATTR_FORECAST_WIND_SPEED, ATTR_WEATHER_APPARENT_TEMPERATURE, ATTR_WEATHER_OZONE, ATTR_WEATHER_PRECIPITATION_UNIT, @@ -78,6 +68,7 @@ class MockWeatherEntity(WeatherEntity): def __init__(self) -> None: """Initiate Entity.""" super().__init__() + self._attr_precision = PRECISION_TENTHS self._attr_condition = ATTR_CONDITION_SUNNY self._attr_native_precipitation_unit = UnitOfLength.MILLIMETERS self._attr_native_pressure = 10 @@ -91,14 +82,6 @@ class MockWeatherEntity(WeatherEntity): self._attr_native_wind_gust_speed = 10 self._attr_native_wind_speed = 3 self._attr_native_wind_speed_unit = UnitOfSpeed.METERS_PER_SECOND - self._attr_forecast = [ - Forecast( - datetime=datetime(2022, 6, 20, 00, 00, 00, tzinfo=dt_util.UTC), - native_precipitation=1, - native_temperature=20, - native_dew_point=2, - ) - ] self._attr_forecast_twice_daily = [ Forecast( datetime=datetime(2022, 6, 20, 8, 00, 00, tzinfo=dt_util.UTC), @@ -109,14 +92,14 @@ class MockWeatherEntity(WeatherEntity): @pytest.mark.parametrize( - "native_unit", (UnitOfTemperature.FAHRENHEIT, UnitOfTemperature.CELSIUS) + "native_unit", [UnitOfTemperature.FAHRENHEIT, UnitOfTemperature.CELSIUS] ) @pytest.mark.parametrize( ("state_unit", "unit_system"), - ( + [ (UnitOfTemperature.CELSIUS, METRIC_SYSTEM), (UnitOfTemperature.FAHRENHEIT, US_CUSTOMARY_SYSTEM), - ), + ], ) async def test_temperature( hass: HomeAssistant, @@ -140,14 +123,6 @@ async def test_temperature( dew_point_native_value, native_unit, state_unit ) - class MockWeatherMock(MockWeatherTest): - """Mock weather class.""" - - @property - def forecast(self) -> list[Forecast] | None: - """Return the forecast.""" - return self.forecast_list - kwargs = { "native_temperature": native_value, "native_temperature_unit": native_unit, @@ -155,10 +130,9 @@ async def test_temperature( "native_dew_point": dew_point_native_value, } - entity0 = await create_entity(hass, MockWeatherMock, None, **kwargs) + entity0 = await create_entity(hass, MockWeatherTest, None, **kwargs) state = hass.states.get(entity0.entity_id) - forecast_daily = state.attributes[ATTR_FORECAST][0] expected = state_value apparent_expected = apparent_state_value @@ -173,29 +147,15 @@ async def test_temperature( dew_point_expected, rel=0.1 ) assert state.attributes[ATTR_WEATHER_TEMPERATURE_UNIT] == state_unit - assert float(forecast_daily[ATTR_FORECAST_TEMP]) == pytest.approx(expected, rel=0.1) - assert float(forecast_daily[ATTR_FORECAST_APPARENT_TEMP]) == pytest.approx( - apparent_expected, rel=0.1 - ) - assert float(forecast_daily[ATTR_FORECAST_DEW_POINT]) == pytest.approx( - dew_point_expected, rel=0.1 - ) - assert float(forecast_daily[ATTR_FORECAST_TEMP_LOW]) == pytest.approx( - expected, rel=0.1 - ) - assert float(forecast_daily[ATTR_FORECAST_TEMP]) == pytest.approx(expected, rel=0.1) - assert float(forecast_daily[ATTR_FORECAST_TEMP_LOW]) == pytest.approx( - expected, rel=0.1 - ) -@pytest.mark.parametrize("native_unit", (None,)) +@pytest.mark.parametrize("native_unit", [None]) @pytest.mark.parametrize( ("state_unit", "unit_system"), - ( + [ (UnitOfTemperature.CELSIUS, METRIC_SYSTEM), (UnitOfTemperature.FAHRENHEIT, US_CUSTOMARY_SYSTEM), - ), + ], ) async def test_temperature_no_unit( hass: HomeAssistant, @@ -213,14 +173,6 @@ async def test_temperature_no_unit( dew_point_state_value = dew_point_native_value apparent_temp_state_value = apparent_temp_native_value - class MockWeatherMock(MockWeatherTest): - """Mock weather class.""" - - @property - def forecast(self) -> list[Forecast] | None: - """Return the forecast.""" - return self.forecast_list - kwargs = { "native_temperature": native_value, "native_temperature_unit": native_unit, @@ -228,10 +180,9 @@ async def test_temperature_no_unit( "native_apparent_temperature": apparent_temp_native_value, } - entity0 = await create_entity(hass, MockWeatherMock, None, **kwargs) + entity0 = await create_entity(hass, MockWeatherTest, None, **kwargs) state = hass.states.get(entity0.entity_id) - forecast = state.attributes[ATTR_FORECAST][0] expected = state_value dew_point_expected = dew_point_state_value @@ -246,20 +197,14 @@ async def test_temperature_no_unit( expected_apparent_temp, rel=0.1 ) assert state.attributes[ATTR_WEATHER_TEMPERATURE_UNIT] == state_unit - assert float(forecast[ATTR_FORECAST_TEMP]) == pytest.approx(expected, rel=0.1) - assert float(forecast[ATTR_FORECAST_DEW_POINT]) == pytest.approx( - dew_point_expected, rel=0.1 - ) - assert float(forecast[ATTR_FORECAST_TEMP_LOW]) == pytest.approx(expected, rel=0.1) - assert float(forecast[ATTR_FORECAST_APPARENT_TEMP]) == pytest.approx( - expected_apparent_temp, rel=0.1 - ) -@pytest.mark.parametrize("native_unit", (UnitOfPressure.INHG, UnitOfPressure.INHG)) @pytest.mark.parametrize( - ("state_unit", "unit_system"), - ((UnitOfPressure.HPA, METRIC_SYSTEM), (UnitOfPressure.INHG, US_CUSTOMARY_SYSTEM)), + ("state_unit", "unit_system", "native_unit"), + [ + (UnitOfPressure.HPA, METRIC_SYSTEM, UnitOfPressure.INHG), + (UnitOfPressure.INHG, US_CUSTOMARY_SYSTEM, UnitOfPressure.INHG), + ], ) async def test_pressure( hass: HomeAssistant, @@ -273,32 +218,22 @@ async def test_pressure( native_value = 30 state_value = PressureConverter.convert(native_value, native_unit, state_unit) - class MockWeatherMock(MockWeatherTest): - """Mock weather class.""" - - @property - def forecast(self) -> list[Forecast] | None: - """Return the forecast.""" - return self.forecast_list - kwargs = {"native_pressure": native_value, "native_pressure_unit": native_unit} - entity0 = await create_entity(hass, MockWeatherMock, None, **kwargs) + entity0 = await create_entity(hass, MockWeatherTest, None, **kwargs) state = hass.states.get(entity0.entity_id) - forecast = state.attributes[ATTR_FORECAST][0] expected = state_value assert float(state.attributes[ATTR_WEATHER_PRESSURE]) == pytest.approx( expected, rel=1e-2 ) - assert float(forecast[ATTR_FORECAST_PRESSURE]) == pytest.approx(expected, rel=1e-2) -@pytest.mark.parametrize("native_unit", (None,)) +@pytest.mark.parametrize("native_unit", [None]) @pytest.mark.parametrize( ("state_unit", "unit_system"), - ((UnitOfPressure.HPA, METRIC_SYSTEM), (UnitOfPressure.INHG, US_CUSTOMARY_SYSTEM)), + [(UnitOfPressure.HPA, METRIC_SYSTEM), (UnitOfPressure.INHG, US_CUSTOMARY_SYSTEM)], ) async def test_pressure_no_unit( hass: HomeAssistant, @@ -312,42 +247,32 @@ async def test_pressure_no_unit( native_value = 30 state_value = native_value - class MockWeatherMock(MockWeatherTest): - """Mock weather class.""" - - @property - def forecast(self) -> list[Forecast] | None: - """Return the forecast.""" - return self.forecast_list - kwargs = {"native_pressure": native_value, "native_pressure_unit": native_unit} - entity0 = await create_entity(hass, MockWeatherMock, None, **kwargs) + entity0 = await create_entity(hass, MockWeatherTest, None, **kwargs) state = hass.states.get(entity0.entity_id) - forecast = state.attributes[ATTR_FORECAST][0] expected = state_value assert float(state.attributes[ATTR_WEATHER_PRESSURE]) == pytest.approx( expected, rel=1e-2 ) - assert float(forecast[ATTR_FORECAST_PRESSURE]) == pytest.approx(expected, rel=1e-2) @pytest.mark.parametrize( "native_unit", - ( + [ UnitOfSpeed.MILES_PER_HOUR, UnitOfSpeed.KILOMETERS_PER_HOUR, UnitOfSpeed.METERS_PER_SECOND, - ), + ], ) @pytest.mark.parametrize( ("state_unit", "unit_system"), - ( + [ (UnitOfSpeed.KILOMETERS_PER_HOUR, METRIC_SYSTEM), (UnitOfSpeed.MILES_PER_HOUR, US_CUSTOMARY_SYSTEM), - ), + ], ) async def test_wind_speed( hass: HomeAssistant, @@ -361,44 +286,32 @@ async def test_wind_speed( native_value = 10 state_value = SpeedConverter.convert(native_value, native_unit, state_unit) - class MockWeatherMock(MockWeatherTest): - """Mock weather class.""" - - @property - def forecast(self) -> list[Forecast] | None: - """Return the forecast.""" - return self.forecast_list - kwargs = {"native_wind_speed": native_value, "native_wind_speed_unit": native_unit} - entity0 = await create_entity(hass, MockWeatherMock, None, **kwargs) + entity0 = await create_entity(hass, MockWeatherTest, None, **kwargs) state = hass.states.get(entity0.entity_id) - forecast = state.attributes[ATTR_FORECAST][0] expected = state_value assert float(state.attributes[ATTR_WEATHER_WIND_SPEED]) == pytest.approx( expected, rel=1e-2 ) - assert float(forecast[ATTR_FORECAST_WIND_SPEED]) == pytest.approx( - expected, rel=1e-2 - ) @pytest.mark.parametrize( "native_unit", - ( + [ UnitOfSpeed.MILES_PER_HOUR, UnitOfSpeed.KILOMETERS_PER_HOUR, UnitOfSpeed.METERS_PER_SECOND, - ), + ], ) @pytest.mark.parametrize( ("state_unit", "unit_system"), - ( + [ (UnitOfSpeed.KILOMETERS_PER_HOUR, METRIC_SYSTEM), (UnitOfSpeed.MILES_PER_HOUR, US_CUSTOMARY_SYSTEM), - ), + ], ) async def test_wind_gust_speed( hass: HomeAssistant, @@ -412,40 +325,28 @@ async def test_wind_gust_speed( native_value = 10 state_value = SpeedConverter.convert(native_value, native_unit, state_unit) - class MockWeatherMock(MockWeatherTest): - """Mock weather class.""" - - @property - def forecast(self) -> list[Forecast] | None: - """Return the forecast.""" - return self.forecast_list - kwargs = { "native_wind_gust_speed": native_value, "native_wind_speed_unit": native_unit, } - entity0 = await create_entity(hass, MockWeatherMock, None, **kwargs) + entity0 = await create_entity(hass, MockWeatherTest, None, **kwargs) state = hass.states.get(entity0.entity_id) - forecast = state.attributes[ATTR_FORECAST][0] expected = state_value assert float(state.attributes[ATTR_WEATHER_WIND_GUST_SPEED]) == pytest.approx( expected, rel=1e-2 ) - assert float(forecast[ATTR_FORECAST_WIND_GUST_SPEED]) == pytest.approx( - expected, rel=1e-2 - ) -@pytest.mark.parametrize("native_unit", (None,)) +@pytest.mark.parametrize("native_unit", [None]) @pytest.mark.parametrize( ("state_unit", "unit_system"), - ( + [ (UnitOfSpeed.KILOMETERS_PER_HOUR, METRIC_SYSTEM), (UnitOfSpeed.MILES_PER_HOUR, US_CUSTOMARY_SYSTEM), - ), + ], ) async def test_wind_speed_no_unit( hass: HomeAssistant, @@ -459,194 +360,16 @@ async def test_wind_speed_no_unit( native_value = 10 state_value = native_value - class MockWeatherMock(MockWeatherTest): - """Mock weather class.""" - - @property - def forecast(self) -> list[Forecast] | None: - """Return the forecast.""" - return self.forecast_list - kwargs = {"native_wind_speed": native_value, "native_wind_speed_unit": native_unit} - entity0 = await create_entity(hass, MockWeatherMock, None, **kwargs) + entity0 = await create_entity(hass, MockWeatherTest, None, **kwargs) state = hass.states.get(entity0.entity_id) - forecast = state.attributes[ATTR_FORECAST][0] expected = state_value assert float(state.attributes[ATTR_WEATHER_WIND_SPEED]) == pytest.approx( expected, rel=1e-2 ) - assert float(forecast[ATTR_FORECAST_WIND_SPEED]) == pytest.approx( - expected, rel=1e-2 - ) - - -@pytest.mark.parametrize("native_unit", (UnitOfLength.MILES, UnitOfLength.KILOMETERS)) -@pytest.mark.parametrize( - ("state_unit", "unit_system"), - ( - (UnitOfLength.KILOMETERS, METRIC_SYSTEM), - (UnitOfLength.MILES, US_CUSTOMARY_SYSTEM), - ), -) -async def test_visibility( - hass: HomeAssistant, - config_flow_fixture: None, - native_unit: str, - state_unit: str, - unit_system, -) -> None: - """Test visibility.""" - hass.config.units = unit_system - native_value = 10 - state_value = DistanceConverter.convert(native_value, native_unit, state_unit) - - class MockWeatherMock(MockWeatherTest): - """Mock weather class.""" - - @property - def forecast(self) -> list[Forecast] | None: - """Return the forecast.""" - return self.forecast_list - - kwargs = {"native_visibility": native_value, "native_visibility_unit": native_unit} - - entity0 = await create_entity(hass, MockWeatherMock, None, **kwargs) - - state = hass.states.get(entity0.entity_id) - expected = state_value - assert float(state.attributes[ATTR_WEATHER_VISIBILITY]) == pytest.approx( - expected, rel=1e-2 - ) - - -@pytest.mark.parametrize("native_unit", (None,)) -@pytest.mark.parametrize( - ("state_unit", "unit_system"), - ( - (UnitOfLength.KILOMETERS, METRIC_SYSTEM), - (UnitOfLength.MILES, US_CUSTOMARY_SYSTEM), - ), -) -async def test_visibility_no_unit( - hass: HomeAssistant, - config_flow_fixture: None, - native_unit: str, - state_unit: str, - unit_system, -) -> None: - """Test visibility when the entity does not declare a native unit.""" - hass.config.units = unit_system - native_value = 10 - state_value = native_value - - class MockWeatherMock(MockWeatherTest): - """Mock weather class.""" - - @property - def forecast(self) -> list[Forecast] | None: - """Return the forecast.""" - return self.forecast_list - - kwargs = {"native_visibility": native_value, "native_visibility_unit": native_unit} - - entity0 = await create_entity(hass, MockWeatherMock, None, **kwargs) - - state = hass.states.get(entity0.entity_id) - expected = state_value - assert float(state.attributes[ATTR_WEATHER_VISIBILITY]) == pytest.approx( - expected, rel=1e-2 - ) - - -@pytest.mark.parametrize("native_unit", (UnitOfLength.INCHES, UnitOfLength.MILLIMETERS)) -@pytest.mark.parametrize( - ("state_unit", "unit_system"), - ( - (UnitOfLength.MILLIMETERS, METRIC_SYSTEM), - (UnitOfLength.INCHES, US_CUSTOMARY_SYSTEM), - ), -) -async def test_precipitation( - hass: HomeAssistant, - config_flow_fixture: None, - native_unit: str, - state_unit: str, - unit_system, -) -> None: - """Test precipitation.""" - hass.config.units = unit_system - native_value = 30 - state_value = DistanceConverter.convert(native_value, native_unit, state_unit) - - class MockWeatherMock(MockWeatherTest): - """Mock weather class.""" - - @property - def forecast(self) -> list[Forecast] | None: - """Return the forecast.""" - return self.forecast_list - - kwargs = { - "native_precipitation": native_value, - "native_precipitation_unit": native_unit, - } - - entity0 = await create_entity(hass, MockWeatherMock, None, **kwargs) - - state = hass.states.get(entity0.entity_id) - forecast = state.attributes[ATTR_FORECAST][0] - - expected = state_value - assert float(forecast[ATTR_FORECAST_PRECIPITATION]) == pytest.approx( - expected, rel=1e-2 - ) - - -@pytest.mark.parametrize("native_unit", (None,)) -@pytest.mark.parametrize( - ("state_unit", "unit_system"), - ( - (UnitOfLength.MILLIMETERS, METRIC_SYSTEM), - (UnitOfLength.INCHES, US_CUSTOMARY_SYSTEM), - ), -) -async def test_precipitation_no_unit( - hass: HomeAssistant, - config_flow_fixture: None, - native_unit: str, - state_unit: str, - unit_system, -) -> None: - """Test precipitation when the entity does not declare a native unit.""" - hass.config.units = unit_system - native_value = 30 - state_value = native_value - - class MockWeatherMock(MockWeatherTest): - """Mock weather class.""" - - @property - def forecast(self) -> list[Forecast] | None: - """Return the forecast.""" - return self.forecast_list - - kwargs = { - "native_precipitation": native_value, - "native_precipitation_unit": native_unit, - } - - entity0 = await create_entity(hass, MockWeatherMock, None, **kwargs) - - state = hass.states.get(entity0.entity_id) - forecast = state.attributes[ATTR_FORECAST][0] - - expected = state_value - assert float(forecast[ATTR_FORECAST_PRECIPITATION]) == pytest.approx( - expected, rel=1e-2 - ) async def test_wind_bearing_ozone_and_cloud_coverage_and_uv_index( @@ -659,14 +382,6 @@ async def test_wind_bearing_ozone_and_cloud_coverage_and_uv_index( cloud_coverage = 75 uv_index = 1.2 - class MockWeatherMock(MockWeatherTest): - """Mock weather class.""" - - @property - def forecast(self) -> list[Forecast] | None: - """Return the forecast.""" - return self.forecast_list - kwargs = { "wind_bearing": wind_bearing_value, "ozone": ozone_value, @@ -674,15 +389,13 @@ async def test_wind_bearing_ozone_and_cloud_coverage_and_uv_index( "uv_index": uv_index, } - entity0 = await create_entity(hass, MockWeatherMock, None, **kwargs) + entity0 = await create_entity(hass, MockWeatherTest, None, **kwargs) state = hass.states.get(entity0.entity_id) - forecast = state.attributes[ATTR_FORECAST][0] assert float(state.attributes[ATTR_WEATHER_WIND_BEARING]) == 180 assert float(state.attributes[ATTR_WEATHER_OZONE]) == 10 assert float(state.attributes[ATTR_WEATHER_CLOUD_COVERAGE]) == 75 assert float(state.attributes[ATTR_WEATHER_UV_INDEX]) == 1.2 - assert float(forecast[ATTR_FORECAST_UV_INDEX]) == 1.2 async def test_humidity( @@ -692,55 +405,12 @@ async def test_humidity( """Test humidity.""" humidity_value = 80.2 - class MockWeatherMock(MockWeatherTest): - """Mock weather class.""" - - @property - def forecast(self) -> list[Forecast] | None: - """Return the forecast.""" - return self.forecast_list - kwargs = {"humidity": humidity_value} - entity0 = await create_entity(hass, MockWeatherMock, None, **kwargs) + entity0 = await create_entity(hass, MockWeatherTest, None, **kwargs) state = hass.states.get(entity0.entity_id) - forecast = state.attributes[ATTR_FORECAST][0] assert float(state.attributes[ATTR_WEATHER_HUMIDITY]) == 80 - assert float(forecast[ATTR_FORECAST_HUMIDITY]) == 80 - - -async def test_none_forecast( - hass: HomeAssistant, - config_flow_fixture: None, -) -> None: - """Test that conversion with None values succeeds.""" - - class MockWeatherMock(MockWeatherTest): - """Mock weather class.""" - - @property - def forecast(self) -> list[Forecast] | None: - """Return the forecast.""" - return self.forecast_list - - kwargs = { - "native_pressure": None, - "native_pressure_unit": UnitOfPressure.INHG, - "native_wind_speed": None, - "native_wind_speed_unit": UnitOfSpeed.METERS_PER_SECOND, - "native_precipitation": None, - "native_precipitation_unit": UnitOfLength.MILLIMETERS, - } - - entity0 = await create_entity(hass, MockWeatherMock, None, **kwargs) - - state = hass.states.get(entity0.entity_id) - forecast = state.attributes[ATTR_FORECAST][0] - - assert forecast.get(ATTR_FORECAST_PRESSURE) is None - assert forecast.get(ATTR_FORECAST_WIND_SPEED) is None - assert forecast.get(ATTR_FORECAST_PRECIPITATION) is None async def test_custom_units(hass: HomeAssistant, config_flow_fixture: None) -> None: @@ -770,14 +440,6 @@ async def test_custom_units(hass: HomeAssistant, config_flow_fixture: None) -> N entity_registry.async_update_entity_options(entry.entity_id, "weather", set_options) await hass.async_block_till_done() - class MockWeatherMock(MockWeatherTest): - """Mock weather class.""" - - @property - def forecast(self) -> list[Forecast] | None: - """Return the forecast.""" - return self.forecast_list - kwargs = { "native_temperature": temperature_value, "native_temperature_unit": temperature_unit, @@ -793,10 +455,9 @@ async def test_custom_units(hass: HomeAssistant, config_flow_fixture: None) -> N "unique_id": "very_unique", } - entity0 = await create_entity(hass, MockWeatherMock, None, **kwargs) + entity0 = await create_entity(hass, MockWeatherTest, None, **kwargs) state = hass.states.get(entity0.entity_id) - forecast = state.attributes[ATTR_FORECAST][0] expected_wind_speed = round( SpeedConverter.convert( @@ -817,12 +478,6 @@ async def test_custom_units(hass: HomeAssistant, config_flow_fixture: None) -> N ), ROUNDING_PRECISION, ) - expected_precipitation = round( - DistanceConverter.convert( - precipitation_value, precipitation_unit, UnitOfLength.INCHES - ), - ROUNDING_PRECISION, - ) assert float(state.attributes[ATTR_WEATHER_WIND_SPEED]) == pytest.approx( expected_wind_speed @@ -836,9 +491,6 @@ async def test_custom_units(hass: HomeAssistant, config_flow_fixture: None) -> N assert float(state.attributes[ATTR_WEATHER_VISIBILITY]) == pytest.approx( expected_visibility ) - assert float(forecast[ATTR_FORECAST_PRECIPITATION]) == pytest.approx( - expected_precipitation, rel=1e-2 - ) assert ( state.attributes[ATTR_WEATHER_PRECIPITATION_UNIT] @@ -922,14 +574,6 @@ async def test_forecast_twice_daily_missing_is_daytime( ) -> None: """Test forecast_twice_daily missing mandatory attribute is_daytime.""" - class MockWeatherMock(MockWeatherTest): - """Mock weather class.""" - - @property - def forecast(self) -> list[Forecast] | None: - """Return the forecast.""" - return self.forecast_list - kwargs = { "native_temperature": 38, "native_temperature_unit": UnitOfTemperature.CELSIUS, @@ -937,7 +581,7 @@ async def test_forecast_twice_daily_missing_is_daytime( "supported_features": WeatherEntityFeature.FORECAST_TWICE_DAILY, } - entity0 = await create_entity(hass, MockWeatherMock, None, **kwargs) + entity0 = await create_entity(hass, MockWeatherTest, None, **kwargs) client = await hass_ws_client(hass) @@ -1144,155 +788,6 @@ async def test_get_forecast_unsupported( ISSUE_TRACKER = "https://blablabla.com" -@pytest.mark.parametrize( - ("manifest_extra", "translation_key", "translation_placeholders_extra", "report"), - [ - ( - {}, - "deprecated_weather_forecast_no_url", - {}, - "report it to the author of the 'test' custom integration", - ), - ( - {"issue_tracker": ISSUE_TRACKER}, - "deprecated_weather_forecast_url", - {"issue_tracker": ISSUE_TRACKER}, - "create a bug report at https://blablabla.com", - ), - ], -) -async def test_issue_forecast_property_deprecated( - hass: HomeAssistant, - caplog: pytest.LogCaptureFixture, - config_flow_fixture: None, - manifest_extra: dict[str, str], - translation_key: str, - translation_placeholders_extra: dict[str, str], - report: str, -) -> None: - """Test the issue is raised on deprecated forecast attributes.""" - - class MockWeatherMockLegacyForecastOnly(MockWeatherTest): - """Mock weather class with mocked legacy forecast.""" - - @property - def forecast(self) -> list[Forecast] | None: - """Return the forecast.""" - return self.forecast_list - - # Fake that the class belongs to a custom integration - MockWeatherMockLegacyForecastOnly.__module__ = "custom_components.test.weather" - - kwargs = { - "native_temperature": 38, - "native_temperature_unit": UnitOfTemperature.CELSIUS, - } - weather_entity = await create_entity( - hass, MockWeatherMockLegacyForecastOnly, manifest_extra, **kwargs - ) - - assert weather_entity.state == ATTR_CONDITION_SUNNY - - issues = ir.async_get(hass) - issue = issues.async_get_issue("weather", "deprecated_weather_forecast_test") - assert issue - assert issue.issue_domain == "test" - assert issue.issue_id == "deprecated_weather_forecast_test" - assert issue.translation_key == translation_key - assert ( - issue.translation_placeholders - == {"platform": "test"} | translation_placeholders_extra - ) - - assert ( - "test::MockWeatherMockLegacyForecastOnly implements the `forecast` property or " - "sets `self._attr_forecast` in a subclass of WeatherEntity, this is deprecated " - f"and will be unsupported from Home Assistant 2024.3. Please {report}" - ) in caplog.text - - -async def test_issue_forecast_attr_deprecated( - hass: HomeAssistant, - issue_registry: ir.IssueRegistry, - config_flow_fixture: None, - caplog: pytest.LogCaptureFixture, -) -> None: - """Test the issue is raised on deprecated forecast attributes.""" - - class MockWeatherMockLegacyForecast(MockWeatherTest): - """Mock weather class with legacy forecast.""" - - @property - def forecast(self) -> list[Forecast] | None: - """Return the forecast.""" - return self.forecast_list - - kwargs = { - "native_temperature": 38, - "native_temperature_unit": UnitOfTemperature.CELSIUS, - } - - # Fake that the class belongs to a custom integration - MockWeatherMockLegacyForecast.__module__ = "custom_components.test.weather" - - weather_entity = await create_entity( - hass, MockWeatherMockLegacyForecast, None, **kwargs - ) - - assert weather_entity.state == ATTR_CONDITION_SUNNY - - issue = issue_registry.async_get_issue( - "weather", "deprecated_weather_forecast_test" - ) - assert issue - assert issue.issue_domain == "test" - assert issue.issue_id == "deprecated_weather_forecast_test" - assert issue.translation_key == "deprecated_weather_forecast_no_url" - assert issue.translation_placeholders == {"platform": "test"} - - assert ( - "test::MockWeatherMockLegacyForecast implements the `forecast` property or " - "sets `self._attr_forecast` in a subclass of WeatherEntity, this is deprecated " - "and will be unsupported from Home Assistant 2024.3. Please report it to the " - "author of the 'test' custom integration" - ) in caplog.text - - -async def test_issue_forecast_deprecated_no_logging( - hass: HomeAssistant, - config_flow_fixture: None, - caplog: pytest.LogCaptureFixture, -) -> None: - """Test the no issue is raised on deprecated forecast attributes if new methods exist.""" - - class MockWeatherMockForecast(MockWeatherTest): - """Mock weather class with mocked new method and legacy forecast.""" - - @property - def forecast(self) -> list[Forecast] | None: - """Return the forecast.""" - return self.forecast_list - - async def async_forecast_daily(self) -> list[Forecast] | None: - """Return the forecast_daily.""" - return self.forecast_list - - kwargs = { - "native_temperature": 38, - "native_temperature_unit": UnitOfTemperature.CELSIUS, - } - - weather_entity = await create_entity(hass, MockWeatherMockForecast, None, **kwargs) - - assert weather_entity.state == ATTR_CONDITION_SUNNY - - assert "Setting up test.weather" in caplog.text - assert ( - "custom_components.test_weather.weather::weather.test is using a forecast attribute on an instance of WeatherEntity" - not in caplog.text - ) - - async def test_issue_deprecated_service_weather_get_forecast( hass: HomeAssistant, issue_registry: ir.IssueRegistry, diff --git a/tests/components/weather/test_intent.py b/tests/components/weather/test_intent.py index 1a171da7fae..1fde5882d6e 100644 --- a/tests/components/weather/test_intent.py +++ b/tests/components/weather/test_intent.py @@ -1,4 +1,5 @@ """Test weather intents.""" + from unittest.mock import patch import pytest @@ -102,7 +103,8 @@ async def test_get_weather_no_state(hass: HomeAssistant) -> None: assert response.response_type == intent.IntentResponseType.QUERY_ANSWER # Failure without state - with patch("homeassistant.core.StateMachine.get", return_value=None), pytest.raises( - intent.IntentHandleError + with ( + patch("homeassistant.core.StateMachine.get", return_value=None), + pytest.raises(intent.IntentHandleError), ): await intent.async_handle(hass, "test", weather_intent.INTENT_GET_WEATHER, {}) diff --git a/tests/components/weather/test_recorder.py b/tests/components/weather/test_recorder.py deleted file mode 100644 index 6b2ce4b633a..00000000000 --- a/tests/components/weather/test_recorder.py +++ /dev/null @@ -1,58 +0,0 @@ -"""The tests for weather recorder.""" -from __future__ import annotations - -from datetime import timedelta - -from homeassistant.components.recorder import Recorder -from homeassistant.components.recorder.history import get_significant_states -from homeassistant.components.weather import ATTR_FORECAST, Forecast -from homeassistant.const import UnitOfTemperature -from homeassistant.core import HomeAssistant -from homeassistant.util import dt as dt_util -from homeassistant.util.unit_system import METRIC_SYSTEM - -from . import MockWeatherTest, create_entity - -from tests.common import async_fire_time_changed -from tests.components.recorder.common import async_wait_recording_done - - -async def test_exclude_attributes( - recorder_mock: Recorder, - hass: HomeAssistant, - config_flow_fixture: None, -) -> None: - """Test weather attributes to be excluded.""" - now = dt_util.utcnow() - - class MockWeatherMockForecast(MockWeatherTest): - """Mock weather class with mocked legacy forecast.""" - - @property - def forecast(self) -> list[Forecast] | None: - """Return the forecast.""" - return self.forecast_list - - kwargs = { - "native_temperature": 38, - "native_temperature_unit": UnitOfTemperature.CELSIUS, - } - weather_entity = await create_entity(hass, MockWeatherMockForecast, None, **kwargs) - hass.config.units = METRIC_SYSTEM - await hass.async_block_till_done() - - state = hass.states.get(weather_entity.entity_id) - assert state.attributes[ATTR_FORECAST] - - await hass.async_block_till_done() - async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5)) - await hass.async_block_till_done() - await async_wait_recording_done(hass) - - states = await hass.async_add_executor_job( - get_significant_states, hass, now, None, hass.states.async_entity_ids() - ) - assert len(states) >= 1 - for entity_states in states.values(): - for state in entity_states: - assert ATTR_FORECAST not in state.attributes diff --git a/tests/components/weather/test_websocket_api.py b/tests/components/weather/test_websocket_api.py index 4a401d79849..5c8a785771f 100644 --- a/tests/components/weather/test_websocket_api.py +++ b/tests/components/weather/test_websocket_api.py @@ -1,4 +1,5 @@ """Test the weather websocket API.""" + from homeassistant.components.weather import Forecast, WeatherEntityFeature from homeassistant.components.weather.const import DOMAIN from homeassistant.const import UnitOfTemperature diff --git a/tests/components/weatherflow/conftest.py b/tests/components/weatherflow/conftest.py index 0bf6b69b9a7..dc533f153e2 100644 --- a/tests/components/weatherflow/conftest.py +++ b/tests/components/weatherflow/conftest.py @@ -1,4 +1,5 @@ """Fixtures for Weatherflow integration tests.""" + import asyncio from collections.abc import Generator from unittest.mock import AsyncMock, patch diff --git a/tests/components/weatherflow_cloud/conftest.py b/tests/components/weatherflow_cloud/conftest.py index 45ad80541f7..e07abe2b924 100644 --- a/tests/components/weatherflow_cloud/conftest.py +++ b/tests/components/weatherflow_cloud/conftest.py @@ -1,4 +1,5 @@ """Common fixtures for the WeatherflowCloud tests.""" + from collections.abc import Generator from unittest.mock import AsyncMock, Mock, patch diff --git a/tests/components/weatherflow_cloud/test_config_flow.py b/tests/components/weatherflow_cloud/test_config_flow.py index 9a6c64fc32c..cef0e224434 100644 --- a/tests/components/weatherflow_cloud/test_config_flow.py +++ b/tests/components/weatherflow_cloud/test_config_flow.py @@ -1,4 +1,5 @@ """Test the WeatherflowCloud config flow.""" + import pytest from homeassistant import config_entries diff --git a/tests/components/weatherkit/__init__.py b/tests/components/weatherkit/__init__.py index 5118c44c45b..99c856a7e37 100644 --- a/tests/components/weatherkit/__init__.py +++ b/tests/components/weatherkit/__init__.py @@ -1,4 +1,5 @@ """Tests for the Apple WeatherKit integration.""" + from unittest.mock import patch from apple_weatherkit import DataSetType @@ -57,12 +58,15 @@ async def init_integration( else: available_data_sets.append(DataSetType.HOURLY_FORECAST) - with patch( - "homeassistant.components.weatherkit.WeatherKitApiClient.get_weather_data", - return_value=weather_response, - ), patch( - "homeassistant.components.weatherkit.WeatherKitApiClient.get_availability", - return_value=available_data_sets, + with ( + patch( + "homeassistant.components.weatherkit.WeatherKitApiClient.get_weather_data", + return_value=weather_response, + ), + patch( + "homeassistant.components.weatherkit.WeatherKitApiClient.get_availability", + return_value=available_data_sets, + ), ): entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) diff --git a/tests/components/weatherkit/conftest.py b/tests/components/weatherkit/conftest.py index 7cfa2f7eef5..ac1dab76a86 100644 --- a/tests/components/weatherkit/conftest.py +++ b/tests/components/weatherkit/conftest.py @@ -1,4 +1,5 @@ """Common fixtures for the Apple WeatherKit tests.""" + from collections.abc import Generator from unittest.mock import AsyncMock, patch diff --git a/tests/components/weatherkit/test_config_flow.py b/tests/components/weatherkit/test_config_flow.py index 9e4d03cbad4..58397ac2ed0 100644 --- a/tests/components/weatherkit/test_config_flow.py +++ b/tests/components/weatherkit/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Apple WeatherKit config flow.""" + from unittest.mock import AsyncMock, patch from apple_weatherkit import DataSetType diff --git a/tests/components/weatherkit/test_coordinator.py b/tests/components/weatherkit/test_coordinator.py index 7113e1d4d51..eff142f3d94 100644 --- a/tests/components/weatherkit/test_coordinator.py +++ b/tests/components/weatherkit/test_coordinator.py @@ -1,4 +1,5 @@ """Test WeatherKit data coordinator.""" + from datetime import timedelta from unittest.mock import patch diff --git a/tests/components/weatherkit/test_setup.py b/tests/components/weatherkit/test_setup.py index d71ecbda1b0..c121f0cc5c1 100644 --- a/tests/components/weatherkit/test_setup.py +++ b/tests/components/weatherkit/test_setup.py @@ -1,4 +1,5 @@ """Test the WeatherKit setup process.""" + from unittest.mock import patch from apple_weatherkit.client import ( @@ -24,12 +25,15 @@ async def test_auth_error_handling(hass: HomeAssistant) -> None: data=EXAMPLE_CONFIG_DATA, ) - with patch( - "homeassistant.components.weatherkit.WeatherKitApiClient.get_weather_data", - side_effect=WeatherKitApiClientAuthenticationError, - ), patch( - "homeassistant.components.weatherkit.WeatherKitApiClient.get_availability", - side_effect=WeatherKitApiClientAuthenticationError, + with ( + patch( + "homeassistant.components.weatherkit.WeatherKitApiClient.get_weather_data", + side_effect=WeatherKitApiClientAuthenticationError, + ), + patch( + "homeassistant.components.weatherkit.WeatherKitApiClient.get_availability", + side_effect=WeatherKitApiClientAuthenticationError, + ), ): entry.add_to_hass(hass) setup_result = await hass.config_entries.async_setup(entry.entry_id) @@ -47,12 +51,15 @@ async def test_client_error_handling(hass: HomeAssistant) -> None: data=EXAMPLE_CONFIG_DATA, ) - with patch( - "homeassistant.components.weatherkit.WeatherKitApiClient.get_weather_data", - side_effect=WeatherKitApiClientError, - ), patch( - "homeassistant.components.weatherkit.WeatherKitApiClient.get_availability", - side_effect=WeatherKitApiClientError, + with ( + patch( + "homeassistant.components.weatherkit.WeatherKitApiClient.get_weather_data", + side_effect=WeatherKitApiClientError, + ), + patch( + "homeassistant.components.weatherkit.WeatherKitApiClient.get_availability", + side_effect=WeatherKitApiClientError, + ), ): entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) diff --git a/tests/components/webhook/test_init.py b/tests/components/webhook/test_init.py index fbe0da15853..b92e9795432 100644 --- a/tests/components/webhook/test_init.py +++ b/tests/components/webhook/test_init.py @@ -1,4 +1,5 @@ """Test the webhook component.""" + from http import HTTPStatus from ipaddress import ip_address from unittest.mock import Mock, patch diff --git a/tests/components/webhook/test_trigger.py b/tests/components/webhook/test_trigger.py index 713130b6fb6..37aae47dd14 100644 --- a/tests/components/webhook/test_trigger.py +++ b/tests/components/webhook/test_trigger.py @@ -1,4 +1,5 @@ """The tests for the webhook automation trigger.""" + from ipaddress import ip_address from unittest.mock import Mock, patch diff --git a/tests/components/webmin/conftest.py b/tests/components/webmin/conftest.py index 196ce40408d..4fd674c66c8 100644 --- a/tests/components/webmin/conftest.py +++ b/tests/components/webmin/conftest.py @@ -1,10 +1,11 @@ """Fixtures for Webmin integration tests.""" + from collections.abc import Generator from unittest.mock import AsyncMock, patch import pytest -from homeassistant.components.webmin.const import DEFAULT_PORT +from homeassistant.components.webmin.const import DEFAULT_PORT, DOMAIN from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, @@ -13,6 +14,9 @@ from homeassistant.const import ( CONF_USERNAME, CONF_VERIFY_SSL, ) +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry, load_json_object_fixture TEST_USER_INPUT = { CONF_HOST: "192.168.1.1", @@ -31,3 +35,17 @@ def mock_setup_entry() -> Generator[AsyncMock, None, None]: "homeassistant.components.webmin.async_setup_entry", return_value=True ) as mock_setup: yield mock_setup + + +async def async_init_integration(hass: HomeAssistant) -> MockConfigEntry: + """Set up the Webmin integration in Home Assistant.""" + entry = MockConfigEntry(domain=DOMAIN, options=TEST_USER_INPUT, title="name") + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.webmin.helpers.WebminInstance.update", + return_value=load_json_object_fixture("webmin_update.json", DOMAIN), + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + return entry diff --git a/tests/components/webmin/fixtures/webmin_update.json b/tests/components/webmin/fixtures/webmin_update.json index c74346a925f..7ec77978a93 100644 --- a/tests/components/webmin/fixtures/webmin_update.json +++ b/tests/components/webmin/fixtures/webmin_update.json @@ -1,97 +1,135 @@ { - "load_1m": 0.98, - "load_5m": 1.02, - "load_15m": 1.0, - "mem_total": 32767008, - "mem_free": 26162544, - "swap_total": 1953088, - "swap_free": 1953088, - "total_space": 18104905818112, - "free_space": 8641328926720, - "fs": [ + "disk_total": 18104905818112, + "io": [0, 4], + "load": [ + 1.29, + 1.36, + 1.37, + 3589, + "Intel(R) Core(TM) i7-5820K CPU @ 3.30GHz", + "GenuineIntel", + 15728640, + 12 + ], + "disk_free": 7749321486336, + "kernel": { "os": "Linux", "arch": "x86_64", "version": "6.6.18-1-lts" }, + "disk_fs": [ { - "free": 174511820800, - "dir": "/", - "iused": 391146, - "used": 61225123840, - "type": "ext4", "device": "UUID=00000000-80b6-0000-8a06-000000000000", - "iused_percent": 3, - "used_percent": 26, + "dir": "/", + "ifree": 14927206, "total": 248431161344, + "used_percent": 80, + "type": "ext4", "itotal": 15482880, - "ifree": 15091734 + "iused": 555674, + "free": 49060442112, + "used": 186676502528, + "iused_percent": 4 }, { - "iused": 8877, - "used": 4608079593472, - "type": "ext4", - "dir": "/media/disk1", - "free": 1044483624960, - "used_percent": 82, - "ifree": 183131475, - "itotal": 183140352, - "total": 5952635744256, - "device": "UUID=00000000-2bb2-0000-896c-000000000000", - "iused_percent": 1 - }, - { - "used": 3881508986880, - "type": "ext4", - "iused": 3411401, - "dir": "/media/disk2", - "free": 7422333480960, - "used_percent": 35, "total": 11903838912512, + "used_percent": 38, + "iused": 3542318, + "type": "ext4", "itotal": 366198784, - "ifree": 362787383, "device": "/dev/md127", + "ifree": 362656466, + "dir": "/media/disk2", + "iused_percent": 1, + "free": 7028764823552, + "used": 4275077644288 + }, + { + "dir": "/media/disk1", + "ifree": 183130757, + "device": "UUID=00000000-2bb2-0000-896c-000000000000", + "type": "ext4", + "itotal": 183140352, + "iused": 9595, + "used_percent": 89, + "total": 5952635744256, + "used": 4981066997760, + "free": 671496220672, "iused_percent": 1 } ], - "used_space": 8550813704192, - "uptime": { "days": 3, "minutes": 23, "seconds": 12 }, + "drivetemps": [ + { "temp": 49, "device": "/dev/sda", "failed": "", "errors": "" }, + { "failed": "", "errors": "", "device": "/dev/sdb", "temp": 49 }, + { "device": "/dev/sdc", "temp": 51, "failed": "", "errors": "" }, + { "failed": "", "errors": "", "device": "/dev/sdd", "temp": 51 }, + { "errors": "", "failed": "", "temp": 43, "device": "/dev/sde" }, + { "device": "/dev/sdf", "temp": 40, "errors": "", "failed": "" } + ], + "mem": [32766344, 28530480, 1953088, 1944384, 27845756, ""], + "disk_used": 9442821144576, + "cputemps": [ + { "temp": 51, "core": 0 }, + { "temp": 49, "core": 1 }, + { "core": 2, "temp": 59 }, + { "temp": 51, "core": 3 }, + { "temp": 50, "core": 4 }, + { "temp": 49, "core": 5 } + ], + "procs": 310, + "cpu": [0, 8, 92, 0, 0], + "cpufans": [ + { "rpm": 0, "fan": 1 }, + { "fan": 2, "rpm": 1371 }, + { "rpm": 0, "fan": 3 }, + { "rpm": 927, "fan": 4 }, + { "rpm": 801, "fan": 5 } + ], + "load_1m": 1.29, + "load_5m": 1.36, + "load_15m": 1.37, + "mem_total": 32766344, + "mem_free": 28530480, + "swap_total": 1953088, + "swap_free": 1944384, + "uptime": { "days": 11, "minutes": 1, "seconds": 28 }, "active_interfaces": [ { - "fullname": "lo", + "scope6": ["host"], + "address": "127.0.0.1", + "address6": ["::1"], + "name": "lo", + "broadcast": 0, "up": 1, "index": 0, - "scope6": ["host"], - "netmask": "255.0.0.0", + "fullname": "lo", "netmask6": [128], - "edit": 1, - "broadcast": 0, + "netmask": "255.0.0.0", "mtu": 65536, - "name": "lo", - "address": "127.0.0.1", - "address6": ["::1"] + "edit": 1 }, { - "mtu": 1500, - "fullname": "enp6s0", + "scope6": [], + "address6": [], + "name": "enp6s0", "up": 1, "index": 1, - "ether": "12:34:56:78:9a:bc", - "address6": [], "netmask6": [], + "fullname": "enp6s0", + "mtu": 1500, "edit": 1, - "scope6": [], - "name": "enp6s0" + "ether": "12:34:56:78:9a:bc" }, { + "ether": "12:34:56:78:9a:bd", "edit": 1, - "netmask6": [64], "netmask": "255.255.255.0", - "scope6": ["link"], - "up": 1, - "index": 2, + "netmask6": [64], "fullname": "eno1", - "address6": ["fe80::2:3:4"], - "address": "192.168.1.4", - "name": "eno1", "mtu": 1500, + "index": 2, "broadcast": "192.168.1.255", - "ether": "12:34:56:78:9a:bd" + "up": 1, + "address6": ["fe80::2:3:4"], + "scope6": ["link"], + "name": "eno1", + "address": "192.168.1.4" } ] } diff --git a/tests/components/webmin/snapshots/test_diagnostics.ambr b/tests/components/webmin/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..9c666938f56 --- /dev/null +++ b/tests/components/webmin/snapshots/test_diagnostics.ambr @@ -0,0 +1,259 @@ +# serializer version: 1 +# name: test_diagnostics + dict({ + 'data': dict({ + 'active_interfaces': list([ + dict({ + 'address': '**REDACTED**', + 'address6': '**REDACTED**', + 'broadcast': '**REDACTED**', + 'edit': 1, + 'fullname': 'lo', + 'index': 0, + 'mtu': 65536, + 'name': 'lo', + 'netmask': '255.0.0.0', + 'netmask6': list([ + 128, + ]), + 'scope6': list([ + 'host', + ]), + 'up': 1, + }), + dict({ + 'address6': '**REDACTED**', + 'edit': 1, + 'ether': '**REDACTED**', + 'fullname': 'enp6s0', + 'index': 1, + 'mtu': 1500, + 'name': 'enp6s0', + 'netmask6': list([ + ]), + 'scope6': list([ + ]), + 'up': 1, + }), + dict({ + 'address': '**REDACTED**', + 'address6': '**REDACTED**', + 'broadcast': '**REDACTED**', + 'edit': 1, + 'ether': '**REDACTED**', + 'fullname': 'eno1', + 'index': 2, + 'mtu': 1500, + 'name': 'eno1', + 'netmask': '255.255.255.0', + 'netmask6': list([ + 64, + ]), + 'scope6': list([ + 'link', + ]), + 'up': 1, + }), + ]), + 'cpu': list([ + 0, + 8, + 92, + 0, + 0, + ]), + 'cpufans': list([ + dict({ + 'fan': 1, + 'rpm': 0, + }), + dict({ + 'fan': 2, + 'rpm': 1371, + }), + dict({ + 'fan': 3, + 'rpm': 0, + }), + dict({ + 'fan': 4, + 'rpm': 927, + }), + dict({ + 'fan': 5, + 'rpm': 801, + }), + ]), + 'cputemps': list([ + dict({ + 'core': 0, + 'temp': 51, + }), + dict({ + 'core': 1, + 'temp': 49, + }), + dict({ + 'core': 2, + 'temp': 59, + }), + dict({ + 'core': 3, + 'temp': 51, + }), + dict({ + 'core': 4, + 'temp': 50, + }), + dict({ + 'core': 5, + 'temp': 49, + }), + ]), + 'disk_free': 7749321486336, + 'disk_fs': list([ + dict({ + 'device': '**REDACTED**', + 'dir': '**REDACTED**', + 'free': 49060442112, + 'ifree': 14927206, + 'itotal': 15482880, + 'iused': 555674, + 'iused_percent': 4, + 'total': 248431161344, + 'type': 'ext4', + 'used': 186676502528, + 'used_percent': 80, + }), + dict({ + 'device': '**REDACTED**', + 'dir': '**REDACTED**', + 'free': 7028764823552, + 'ifree': 362656466, + 'itotal': 366198784, + 'iused': 3542318, + 'iused_percent': 1, + 'total': 11903838912512, + 'type': 'ext4', + 'used': 4275077644288, + 'used_percent': 38, + }), + dict({ + 'device': '**REDACTED**', + 'dir': '**REDACTED**', + 'free': 671496220672, + 'ifree': 183130757, + 'itotal': 183140352, + 'iused': 9595, + 'iused_percent': 1, + 'total': 5952635744256, + 'type': 'ext4', + 'used': 4981066997760, + 'used_percent': 89, + }), + ]), + 'disk_total': 18104905818112, + 'disk_used': 9442821144576, + 'drivetemps': list([ + dict({ + 'device': '**REDACTED**', + 'errors': '', + 'failed': '', + 'temp': 49, + }), + dict({ + 'device': '**REDACTED**', + 'errors': '', + 'failed': '', + 'temp': 49, + }), + dict({ + 'device': '**REDACTED**', + 'errors': '', + 'failed': '', + 'temp': 51, + }), + dict({ + 'device': '**REDACTED**', + 'errors': '', + 'failed': '', + 'temp': 51, + }), + dict({ + 'device': '**REDACTED**', + 'errors': '', + 'failed': '', + 'temp': 43, + }), + dict({ + 'device': '**REDACTED**', + 'errors': '', + 'failed': '', + 'temp': 40, + }), + ]), + 'io': list([ + 0, + 4, + ]), + 'kernel': dict({ + 'arch': 'x86_64', + 'os': 'Linux', + 'version': '6.6.18-1-lts', + }), + 'load': list([ + 1.29, + 1.36, + 1.37, + 3589, + 'Intel(R) Core(TM) i7-5820K CPU @ 3.30GHz', + 'GenuineIntel', + 15728640, + 12, + ]), + 'load_15m': 1.37, + 'load_1m': 1.29, + 'load_5m': 1.36, + 'mem': list([ + 32766344, + 28530480, + 1953088, + 1944384, + 27845756, + '', + ]), + 'mem_free': 28530480, + 'mem_total': 32766344, + 'procs': 310, + 'swap_free': 1944384, + 'swap_total': 1953088, + 'uptime': dict({ + 'days': 11, + 'minutes': 1, + 'seconds': 28, + }), + }), + 'entry': dict({ + 'data': dict({ + }), + 'disabled_by': None, + 'domain': 'webmin', + 'entry_id': '**REDACTED**', + 'minor_version': 1, + 'options': dict({ + 'host': '**REDACTED**', + 'password': '**REDACTED**', + 'port': 10000, + 'ssl': True, + 'username': '**REDACTED**', + 'verify_ssl': False, + }), + 'pref_disable_new_entities': False, + 'pref_disable_polling': False, + 'source': 'user', + 'title': '**REDACTED**', + 'unique_id': None, + 'version': 1, + }), + }) +# --- diff --git a/tests/components/webmin/snapshots/test_sensor.ambr b/tests/components/webmin/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..1813dd354d3 --- /dev/null +++ b/tests/components/webmin/snapshots/test_sensor.ambr @@ -0,0 +1,376 @@ +# serializer version: 1 +# name: test_sensor[sensor.192_168_1_1_load_15m-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.192_168_1_1_load_15m', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Load (15m)', + 'platform': 'webmin', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'load_15m', + 'unique_id': '12:34:56:78:9a:bc_load_15m', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.192_168_1_1_load_15m-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': '192.168.1.1 Load (15m)', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.192_168_1_1_load_15m', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.37', + }) +# --- +# name: test_sensor[sensor.192_168_1_1_load_1m-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.192_168_1_1_load_1m', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Load (1m)', + 'platform': 'webmin', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'load_1m', + 'unique_id': '12:34:56:78:9a:bc_load_1m', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.192_168_1_1_load_1m-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': '192.168.1.1 Load (1m)', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.192_168_1_1_load_1m', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.29', + }) +# --- +# name: test_sensor[sensor.192_168_1_1_load_5m-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.192_168_1_1_load_5m', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Load (5m)', + 'platform': 'webmin', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'load_5m', + 'unique_id': '12:34:56:78:9a:bc_load_5m', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.192_168_1_1_load_5m-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': '192.168.1.1 Load (5m)', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.192_168_1_1_load_5m', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.36', + }) +# --- +# name: test_sensor[sensor.192_168_1_1_memory_free-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.192_168_1_1_memory_free', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Memory free', + 'platform': 'webmin', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'mem_free', + 'unique_id': '12:34:56:78:9a:bc_mem_free', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.192_168_1_1_memory_free-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_size', + 'friendly_name': '192.168.1.1 Memory free', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.192_168_1_1_memory_free', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '27.2087860107422', + }) +# --- +# name: test_sensor[sensor.192_168_1_1_memory_total-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.192_168_1_1_memory_total', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Memory total', + 'platform': 'webmin', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'mem_total', + 'unique_id': '12:34:56:78:9a:bc_mem_total', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.192_168_1_1_memory_total-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_size', + 'friendly_name': '192.168.1.1 Memory total', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.192_168_1_1_memory_total', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '31.248420715332', + }) +# --- +# name: test_sensor[sensor.192_168_1_1_swap_free-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.192_168_1_1_swap_free', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Swap free', + 'platform': 'webmin', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'swap_free', + 'unique_id': '12:34:56:78:9a:bc_swap_free', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.192_168_1_1_swap_free-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_size', + 'friendly_name': '192.168.1.1 Swap free', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.192_168_1_1_swap_free', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.85430908203125', + }) +# --- +# name: test_sensor[sensor.192_168_1_1_swap_total-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.192_168_1_1_swap_total', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Swap total', + 'platform': 'webmin', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'swap_total', + 'unique_id': '12:34:56:78:9a:bc_swap_total', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.192_168_1_1_swap_total-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_size', + 'friendly_name': '192.168.1.1 Swap total', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.192_168_1_1_swap_total', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.86260986328125', + }) +# --- diff --git a/tests/components/webmin/test_config_flow.py b/tests/components/webmin/test_config_flow.py index d61ed5a03d6..e680f0e164a 100644 --- a/tests/components/webmin/test_config_flow.py +++ b/tests/components/webmin/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Webmin config flow.""" + from __future__ import annotations from http import HTTPStatus diff --git a/tests/components/webmin/test_diagnostics.py b/tests/components/webmin/test_diagnostics.py new file mode 100644 index 00000000000..5f1df44f4a8 --- /dev/null +++ b/tests/components/webmin/test_diagnostics.py @@ -0,0 +1,24 @@ +"""Tests for the diagnostics data provided by the Webmin integration.""" + +from syrupy.assertion import SnapshotAssertion + +from homeassistant.core import HomeAssistant + +from .conftest import async_init_integration + +from tests.components.diagnostics import get_diagnostics_for_config_entry +from tests.typing import ClientSessionGenerator + + +async def test_diagnostics( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + snapshot: SnapshotAssertion, +) -> None: + """Test diagnostics.""" + assert ( + await get_diagnostics_for_config_entry( + hass, hass_client, await async_init_integration(hass) + ) + == snapshot + ) diff --git a/tests/components/webmin/test_init.py b/tests/components/webmin/test_init.py index 21963a2120c..7b6282edfae 100644 --- a/tests/components/webmin/test_init.py +++ b/tests/components/webmin/test_init.py @@ -1,28 +1,17 @@ """Tests for the Webmin integration.""" -from unittest.mock import patch from homeassistant.components.webmin.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant -from .conftest import TEST_USER_INPUT - -from tests.common import MockConfigEntry, load_json_object_fixture +from .conftest import async_init_integration async def test_unload_entry(hass: HomeAssistant) -> None: """Test successful unload of entry.""" - entry = MockConfigEntry(domain=DOMAIN, options=TEST_USER_INPUT, title="name") - entry.add_to_hass(hass) + entry = await async_init_integration(hass) - with patch( - "homeassistant.components.webmin.helpers.WebminInstance.update", - return_value=load_json_object_fixture("webmin_update.json", DOMAIN), - ): - await hass.config_entries.async_setup(entry.entry_id) - - await hass.async_block_till_done() assert entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(entry.entry_id) diff --git a/tests/components/webmin/test_sensor.py b/tests/components/webmin/test_sensor.py new file mode 100644 index 00000000000..5fb874825a3 --- /dev/null +++ b/tests/components/webmin/test_sensor.py @@ -0,0 +1,29 @@ +"""Test cases for the Webmin sensors.""" + +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from .conftest import async_init_integration + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_sensor( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test the sensor entities and states.""" + + entry = await async_init_integration(hass) + + entity_entries = er.async_entries_for_config_entry(entity_registry, entry.entry_id) + + assert entity_entries + + for entity_entry in entity_entries: + assert entity_entry == snapshot(name=f"{entity_entry.entity_id}-entry") + assert (state := hass.states.get(entity_entry.entity_id)) + assert state == snapshot(name=f"{entity_entry.entity_id}-state") diff --git a/tests/components/webostv/conftest.py b/tests/components/webostv/conftest.py index b78046c22ec..a21b10d0d9d 100644 --- a/tests/components/webostv/conftest.py +++ b/tests/components/webostv/conftest.py @@ -1,4 +1,5 @@ """Common fixtures and objects for the LG webOS integration tests.""" + from collections.abc import Generator from unittest.mock import AsyncMock, Mock, patch diff --git a/tests/components/webostv/const.py b/tests/components/webostv/const.py index fbdb9c47c3b..afaed224e83 100644 --- a/tests/components/webostv/const.py +++ b/tests/components/webostv/const.py @@ -1,4 +1,5 @@ """Constants for LG webOS Smart TV tests.""" + from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.components.webostv.const import LIVE_TV_APP_ID diff --git a/tests/components/webostv/test_config_flow.py b/tests/components/webostv/test_config_flow.py index ad57b4647ea..07a11b5bf29 100644 --- a/tests/components/webostv/test_config_flow.py +++ b/tests/components/webostv/test_config_flow.py @@ -1,4 +1,5 @@ """Test the WebOS Tv config flow.""" + import dataclasses from unittest.mock import Mock diff --git a/tests/components/webostv/test_device_trigger.py b/tests/components/webostv/test_device_trigger.py index db3bd85bbf7..5205f6ae7a1 100644 --- a/tests/components/webostv/test_device_trigger.py +++ b/tests/components/webostv/test_device_trigger.py @@ -1,4 +1,5 @@ """The tests for WebOS TV device triggers.""" + import pytest from homeassistant.components import automation diff --git a/tests/components/webostv/test_diagnostics.py b/tests/components/webostv/test_diagnostics.py index b7d1646c6b6..934b59a7b83 100644 --- a/tests/components/webostv/test_diagnostics.py +++ b/tests/components/webostv/test_diagnostics.py @@ -1,4 +1,5 @@ """Tests for the diagnostics data provided by LG webOS Smart TV.""" + from homeassistant.components.diagnostics import REDACTED from homeassistant.core import HomeAssistant diff --git a/tests/components/webostv/test_init.py b/tests/components/webostv/test_init.py index 9d5b24d0d4e..30af1428701 100644 --- a/tests/components/webostv/test_init.py +++ b/tests/components/webostv/test_init.py @@ -1,4 +1,5 @@ """The tests for the LG webOS TV platform.""" + from unittest.mock import Mock from aiowebostv import WebOsTvPairError diff --git a/tests/components/webostv/test_media_player.py b/tests/components/webostv/test_media_player.py index cc060064b8b..2dff9477e50 100644 --- a/tests/components/webostv/test_media_player.py +++ b/tests/components/webostv/test_media_player.py @@ -1,4 +1,5 @@ """The tests for the LG webOS media player platform.""" + from datetime import timedelta from http import HTTPStatus from unittest.mock import Mock diff --git a/tests/components/webostv/test_notify.py b/tests/components/webostv/test_notify.py index dc150145b60..a1c37b9bf97 100644 --- a/tests/components/webostv/test_notify.py +++ b/tests/components/webostv/test_notify.py @@ -1,4 +1,5 @@ """The tests for the WebOS TV notify platform.""" + from unittest.mock import Mock, call from aiowebostv import WebOsTvPairError diff --git a/tests/components/webostv/test_trigger.py b/tests/components/webostv/test_trigger.py index 74573e2185b..dd119bd0d5a 100644 --- a/tests/components/webostv/test_trigger.py +++ b/tests/components/webostv/test_trigger.py @@ -1,4 +1,5 @@ """The tests for WebOS TV automation triggers.""" + from unittest.mock import patch import pytest diff --git a/tests/components/websocket_api/conftest.py b/tests/components/websocket_api/conftest.py index 75a7834f629..7cfd0e204a7 100644 --- a/tests/components/websocket_api/conftest.py +++ b/tests/components/websocket_api/conftest.py @@ -1,4 +1,5 @@ """Fixtures for websocket tests.""" + import pytest from homeassistant.components.websocket_api.auth import TYPE_AUTH_REQUIRED diff --git a/tests/components/websocket_api/test_auth.py b/tests/components/websocket_api/test_auth.py index 65cf3012e30..35bf2402b6c 100644 --- a/tests/components/websocket_api/test_auth.py +++ b/tests/components/websocket_api/test_auth.py @@ -1,4 +1,5 @@ """Test auth of websocket API.""" + from unittest.mock import patch import aiohttp diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index a6a2fb06d7b..52d0e86d828 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -1,4 +1,5 @@ """Tests for WebSocket API commands.""" + import asyncio from copy import deepcopy import logging @@ -22,7 +23,7 @@ from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers import device_registry as dr 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 homeassistant.setup import async_setup_component from homeassistant.util.json import json_loads from tests.common import ( @@ -205,7 +206,7 @@ async def test_return_response_error(hass: HomeAssistant, websocket_client) -> N assert msg["error"]["code"] == "unknown_error" -@pytest.mark.parametrize("command", ("call_service", "call_service_action")) +@pytest.mark.parametrize("command", ["call_service", "call_service_action"]) async def test_call_service_blocking( hass: HomeAssistant, websocket_client: MockHAClientWebSocket, command ) -> None: @@ -683,9 +684,7 @@ async def test_get_states( assert msg["type"] == const.TYPE_RESULT assert msg["success"] - states = [] - for state in hass.states.async_all(): - states.append(state.as_dict()) + states = [state.as_dict() for state in hass.states.async_all()] assert msg["result"] == states @@ -2492,13 +2491,16 @@ async def test_integration_setup_info( hass_admin_user: MockUser, ) -> None: """Test subscribe/unsubscribe bootstrap_integrations.""" - hass.data[DATA_SETUP_TIME] = { - "august": 12.5, - "isy994": 12.8, - } - await websocket_client.send_json({"id": 7, "type": "integration/setup_info"}) + with patch( + "homeassistant.components.websocket_api.commands.async_get_setup_timings", + return_value={ + "august": 12.5, + "isy994": 12.8, + }, + ): + await websocket_client.send_json({"id": 7, "type": "integration/setup_info"}) + msg = await websocket_client.receive_json() - msg = await websocket_client.receive_json() assert msg["id"] == 7 assert msg["type"] == const.TYPE_RESULT assert msg["success"] @@ -2510,7 +2512,7 @@ async def test_integration_setup_info( @pytest.mark.parametrize( ("key", "config"), - ( + [ ("trigger", {"platform": "event", "event_type": "hello"}), ("trigger", [{"platform": "event", "event_type": "hello"}]), ( @@ -2523,7 +2525,7 @@ async def test_integration_setup_info( ), ("action", {"service": "domain_test.test_service"}), ("action", [{"service": "domain_test.test_service"}]), - ), + ], ) async def test_validate_config_works( websocket_client: MockHAClientWebSocket, key, config @@ -2540,7 +2542,7 @@ async def test_validate_config_works( @pytest.mark.parametrize( ("key", "config", "error"), - ( + [ ( "trigger", {"platform": "non_existing", "event_type": "hello"}, @@ -2564,7 +2566,7 @@ async def test_validate_config_works( {"non_existing": "domain_test.test_service"}, "Unable to determine action @ data[0]", ), - ), + ], ) async def test_validate_config_invalid( websocket_client: MockHAClientWebSocket, key, config, error diff --git a/tests/components/websocket_api/test_connection.py b/tests/components/websocket_api/test_connection.py index 7a6bd86586c..d6c2765522e 100644 --- a/tests/components/websocket_api/test_connection.py +++ b/tests/components/websocket_api/test_connection.py @@ -1,4 +1,5 @@ """Test WebSocket Connection class.""" + import logging from typing import Any from unittest.mock import AsyncMock, Mock, patch diff --git a/tests/components/websocket_api/test_decorators.py b/tests/components/websocket_api/test_decorators.py index 7ef14ca124f..3e9c13a8b15 100644 --- a/tests/components/websocket_api/test_decorators.py +++ b/tests/components/websocket_api/test_decorators.py @@ -1,4 +1,5 @@ """Test decorators.""" + from homeassistant.components import http, websocket_api from homeassistant.core import HomeAssistant diff --git a/tests/components/websocket_api/test_http.py b/tests/components/websocket_api/test_http.py index 090f034b3d3..db186e4811b 100644 --- a/tests/components/websocket_api/test_http.py +++ b/tests/components/websocket_api/test_http.py @@ -1,4 +1,5 @@ """Test Websocket API http module.""" + import asyncio from datetime import timedelta from typing import Any, cast @@ -85,7 +86,7 @@ async def test_cleanup_on_cancellation( @callback def _raise(): - raise ValueError() + raise ValueError connection.subscriptions[msg_id] = _raise connection.send_result(msg_id) @@ -103,7 +104,7 @@ async def test_cleanup_on_cancellation( def cancel_in_handler( hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] ) -> None: - raise asyncio.CancelledError() + raise asyncio.CancelledError async_register_command(hass, cancel_in_handler) @@ -370,10 +371,13 @@ async def test_prepare_fail( caplog: pytest.LogCaptureFixture, ) -> None: """Test failing to prepare.""" - with patch( - "homeassistant.components.websocket_api.http.web.WebSocketResponse.prepare", - side_effect=(TimeoutError, web.WebSocketResponse.prepare), - ), pytest.raises(ServerDisconnectedError): + with ( + patch( + "homeassistant.components.websocket_api.http.web.WebSocketResponse.prepare", + side_effect=(TimeoutError, web.WebSocketResponse.prepare), + ), + pytest.raises(ServerDisconnectedError), + ): await hass_ws_client(hass) assert "Timeout preparing request" in caplog.text diff --git a/tests/components/websocket_api/test_init.py b/tests/components/websocket_api/test_init.py index c4c83925311..9360ff4ef8a 100644 --- a/tests/components/websocket_api/test_init.py +++ b/tests/components/websocket_api/test_init.py @@ -1,4 +1,5 @@ """Tests for the Home Assistant Websocket API.""" + from unittest.mock import Mock, patch from aiohttp import WSMsgType diff --git a/tests/components/websocket_api/test_messages.py b/tests/components/websocket_api/test_messages.py index 5fc0e4ea315..350aed8b5f7 100644 --- a/tests/components/websocket_api/test_messages.py +++ b/tests/components/websocket_api/test_messages.py @@ -1,4 +1,5 @@ """Test Websocket API messages module.""" + import pytest from homeassistant.components.websocket_api.messages import ( diff --git a/tests/components/websocket_api/test_sensor.py b/tests/components/websocket_api/test_sensor.py index 9f53efb8610..72b39b39354 100644 --- a/tests/components/websocket_api/test_sensor.py +++ b/tests/components/websocket_api/test_sensor.py @@ -1,4 +1,5 @@ """Test cases for the API stream sensor.""" + from homeassistant.auth.providers.legacy_api_password import ( LegacyApiPasswordAuthProvider, ) diff --git a/tests/components/wemo/conftest.py b/tests/components/wemo/conftest.py index 6c4d28ecae7..1316c37b62b 100644 --- a/tests/components/wemo/conftest.py +++ b/tests/components/wemo/conftest.py @@ -1,4 +1,5 @@ """Fixtures for pywemo.""" + import contextlib from unittest.mock import create_autospec, patch @@ -81,8 +82,9 @@ def create_pywemo_device(pywemo_registry, pywemo_model): device.switch_state = 0 url = f"http://{MOCK_HOST}:{MOCK_PORT}/setup.xml" - with patch("pywemo.setup_url_for_address", return_value=url), patch( - "pywemo.discovery.device_from_description", return_value=device + with ( + patch("pywemo.setup_url_for_address", return_value=url), + patch("pywemo.discovery.device_from_description", return_value=device), ): yield device diff --git a/tests/components/wemo/entity_test_helpers.py b/tests/components/wemo/entity_test_helpers.py index 90bfadf402f..fd2bbed4371 100644 --- a/tests/components/wemo/entity_test_helpers.py +++ b/tests/components/wemo/entity_test_helpers.py @@ -2,6 +2,7 @@ This is not a test module. These test methods are used by the platform test modules. """ + import asyncio import threading @@ -123,7 +124,7 @@ async def test_async_update_locked_multiple_callbacks( async def test_avaliable_after_update( hass: HomeAssistant, pywemo_registry, pywemo_device, wemo_entity, domain ) -> None: - """Test the avaliability when an On call fails and after an update. + """Test the availability when an On call fails and after an update. This test expects that the pywemo_device Mock has been setup to raise an ActionException when the SERVICE_TURN_ON method is called and that the diff --git a/tests/components/wemo/test_device_trigger.py b/tests/components/wemo/test_device_trigger.py index 9140f5f1e35..47b704dae5d 100644 --- a/tests/components/wemo/test_device_trigger.py +++ b/tests/components/wemo/test_device_trigger.py @@ -1,4 +1,5 @@ """Verify that WeMo device triggers work as expected.""" + import pytest from pytest_unordered import unordered from pywemo.subscribe import EVENT_TYPE_LONG_PRESS diff --git a/tests/components/wemo/test_fan.py b/tests/components/wemo/test_fan.py index 48ad5774b0a..625007a744d 100644 --- a/tests/components/wemo/test_fan.py +++ b/tests/components/wemo/test_fan.py @@ -1,4 +1,5 @@ """Tests for the Wemo fan entity.""" + import pytest from pywemo.exceptions import ActionException from pywemo.ouimeaux_device.humidifier import DesiredHumidity, FanMode @@ -89,7 +90,7 @@ async def test_fan_update_entity( async def test_available_after_update( hass: HomeAssistant, pywemo_registry, pywemo_device, wemo_entity ) -> None: - """Test the avaliability when an On call fails and after an update.""" + """Test the availability when an On call fails and after an update.""" pywemo_device.set_state.side_effect = ActionException pywemo_device.get_state.return_value = 1 await entity_test_helpers.test_avaliable_after_update( diff --git a/tests/components/wemo/test_init.py b/tests/components/wemo/test_init.py index 1d4271063f2..bf41e703190 100644 --- a/tests/components/wemo/test_init.py +++ b/tests/components/wemo/test_init.py @@ -1,4 +1,5 @@ """Tests for the wemo component.""" + import asyncio from datetime import timedelta from unittest.mock import create_autospec, patch @@ -210,13 +211,15 @@ async def test_discovery(hass: HomeAssistant, pywemo_registry) -> None: pywemo_devices = [create_device(0), create_device(1)] # Setup the component and start discovery. - with patch( - "pywemo.discover_devices", return_value=pywemo_devices - ) as mock_discovery, patch( - "homeassistant.components.wemo.WemoDiscovery.discover_statics" - ) as mock_discover_statics, patch( - "homeassistant.components.wemo.binary_sensor.async_wemo_dispatcher_connect", - side_effect=async_connect, + with ( + patch("pywemo.discover_devices", return_value=pywemo_devices) as mock_discovery, + patch( + "homeassistant.components.wemo.WemoDiscovery.discover_statics" + ) as mock_discover_statics, + patch( + "homeassistant.components.wemo.binary_sensor.async_wemo_dispatcher_connect", + side_effect=async_connect, + ), ): assert await async_setup_component( hass, DOMAIN, {DOMAIN: {CONF_DISCOVERY: True}} diff --git a/tests/components/wemo/test_light_bridge.py b/tests/components/wemo/test_light_bridge.py index 6f4180626b2..48be2823750 100644 --- a/tests/components/wemo/test_light_bridge.py +++ b/tests/components/wemo/test_light_bridge.py @@ -1,4 +1,5 @@ """Tests for the Wemo light entity via the bridge.""" + from unittest.mock import create_autospec import pytest @@ -84,7 +85,7 @@ async def test_available_after_update( pywemo_bridge_light, wemo_entity, ) -> None: - """Test the avaliability when an On call fails and after an update.""" + """Test the availability when an On call fails and after an update.""" pywemo_bridge_light.turn_on.side_effect = pywemo.exceptions.ActionException pywemo_bridge_light.state["onoff"] = 1 await entity_test_helpers.test_avaliable_after_update( diff --git a/tests/components/wemo/test_light_dimmer.py b/tests/components/wemo/test_light_dimmer.py index 51ddc6cab2d..a2f69ea57d5 100644 --- a/tests/components/wemo/test_light_dimmer.py +++ b/tests/components/wemo/test_light_dimmer.py @@ -1,4 +1,5 @@ """Tests for the Wemo standalone/non-bridge light entity.""" + import pytest from pywemo.exceptions import ActionException @@ -37,7 +38,7 @@ test_async_update_locked_callback_and_update = ( async def test_available_after_update( hass: HomeAssistant, pywemo_registry, pywemo_device, wemo_entity ) -> None: - """Test the avaliability when an On call fails and after an update.""" + """Test the availability when an On call fails and after an update.""" pywemo_device.on.side_effect = ActionException pywemo_device.get_state.return_value = 1 await entity_test_helpers.test_avaliable_after_update( diff --git a/tests/components/wemo/test_switch.py b/tests/components/wemo/test_switch.py index 4df13a777d4..ab60f290727 100644 --- a/tests/components/wemo/test_switch.py +++ b/tests/components/wemo/test_switch.py @@ -1,4 +1,5 @@ """Tests for the Wemo switch entity.""" + import pytest import pywemo @@ -102,7 +103,7 @@ async def test_switch_update_entity( async def test_available_after_update( hass: HomeAssistant, pywemo_registry, pywemo_device, wemo_entity ) -> None: - """Test the avaliability when an On call fails and after an update.""" + """Test the availability when an On call fails and after an update.""" pywemo_device.on.side_effect = pywemo.exceptions.ActionException pywemo_device.get_state.return_value = 1 await entity_test_helpers.test_avaliable_after_update( diff --git a/tests/components/wemo/test_wemo_device.py b/tests/components/wemo/test_wemo_device.py index 5c8353fc8bc..7d23b590b57 100644 --- a/tests/components/wemo/test_wemo_device.py +++ b/tests/components/wemo/test_wemo_device.py @@ -1,4 +1,5 @@ """Tests for wemo_device.py.""" + import asyncio from dataclasses import asdict from datetime import timedelta diff --git a/tests/components/whirlpool/__init__.py b/tests/components/whirlpool/__init__.py index d47fb5337fd..ca00975941a 100644 --- a/tests/components/whirlpool/__init__.py +++ b/tests/components/whirlpool/__init__.py @@ -1,12 +1,15 @@ """Tests for the Whirlpool Sixth Sense integration.""" -from homeassistant.components.whirlpool.const import DOMAIN + +from homeassistant.components.whirlpool.const import CONF_BRAND, DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_REGION, CONF_USERNAME from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry -async def init_integration(hass: HomeAssistant, region: str = "EU") -> MockConfigEntry: +async def init_integration( + hass: HomeAssistant, region: str = "EU", brand: str = "Whirlpool" +) -> MockConfigEntry: """Set up the Whirlpool integration in Home Assistant.""" entry = MockConfigEntry( domain=DOMAIN, @@ -14,6 +17,7 @@ async def init_integration(hass: HomeAssistant, region: str = "EU") -> MockConfi CONF_USERNAME: "nobody", CONF_PASSWORD: "qwerty", CONF_REGION: region, + CONF_BRAND: brand, }, ) diff --git a/tests/components/whirlpool/conftest.py b/tests/components/whirlpool/conftest.py index 7ab7f013dd5..e386012265c 100644 --- a/tests/components/whirlpool/conftest.py +++ b/tests/components/whirlpool/conftest.py @@ -1,4 +1,5 @@ """Fixtures for the Whirlpool Sixth Sense integration tests.""" + from unittest import mock from unittest.mock import AsyncMock, MagicMock @@ -15,13 +16,26 @@ MOCK_SAID4 = "said4" @pytest.fixture( name="region", - params=[("EU", Region.EU, Brand.Whirlpool), ("US", Region.US, Brand.Maytag)], + params=[("EU", Region.EU), ("US", Region.US)], ) def fixture_region(request): """Return a region for input.""" return request.param +@pytest.fixture( + name="brand", + params=[ + ("Whirlpool", Brand.Whirlpool), + ("KitchenAid", Brand.KitchenAid), + ("Maytag", Brand.Maytag), + ], +) +def fixture_brand(request): + """Return a brand for input.""" + return request.param + + @pytest.fixture(name="mock_auth_api") def fixture_mock_auth_api(): """Set up Auth fixture.""" diff --git a/tests/components/whirlpool/snapshots/test_diagnostics.ambr b/tests/components/whirlpool/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..5a0beb112e6 --- /dev/null +++ b/tests/components/whirlpool/snapshots/test_diagnostics.ambr @@ -0,0 +1,44 @@ +# serializer version: 1 +# name: test_entry_diagnostics + dict({ + 'appliances': dict({ + 'Washer_dryers': dict({ + 'dryer': dict({ + 'NAME': 'dryer', + 'SAID': '**REDACTED**', + }), + 'washer': dict({ + 'NAME': 'washer', + 'SAID': '**REDACTED**', + }), + }), + 'aircons': dict({ + 'TestZone': dict({ + 'NAME': 'TestZone', + 'SAID': '**REDACTED**', + }), + }), + 'ovens': dict({ + }), + }), + 'config_entry': dict({ + 'data': dict({ + 'brand': 'Whirlpool', + 'password': '**REDACTED**', + 'region': 'EU', + 'username': '**REDACTED**', + }), + 'disabled_by': None, + 'domain': 'whirlpool', + 'minor_version': 1, + 'options': dict({ + }), + 'pref_disable_new_entities': False, + 'pref_disable_polling': False, + 'source': 'user', + 'title': 'Mock Title', + 'unique_id': None, + 'version': 1, + }), + }) +# --- diff --git a/tests/components/whirlpool/test_climate.py b/tests/components/whirlpool/test_climate.py index 0cc58e80f0d..21c4501e6d0 100644 --- a/tests/components/whirlpool/test_climate.py +++ b/tests/components/whirlpool/test_climate.py @@ -1,4 +1,5 @@ """Test the Whirlpool Sixth Sense climate domain.""" + from unittest.mock import MagicMock from attr import dataclass diff --git a/tests/components/whirlpool/test_config_flow.py b/tests/components/whirlpool/test_config_flow.py index 60e64c89929..273f0e6737d 100644 --- a/tests/components/whirlpool/test_config_flow.py +++ b/tests/components/whirlpool/test_config_flow.py @@ -1,11 +1,12 @@ """Test the Whirlpool Sixth Sense config flow.""" + from unittest.mock import patch import aiohttp from aiohttp.client_exceptions import ClientConnectionError from homeassistant import config_entries -from homeassistant.components.whirlpool.const import DOMAIN +from homeassistant.components.whirlpool.const import CONF_BRAND, DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -18,10 +19,7 @@ CONFIG_INPUT = { } -async def test_form( - hass: HomeAssistant, - region, -) -> None: +async def test_form(hass: HomeAssistant, region, brand) -> None: """Test we get the form.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -30,24 +28,31 @@ async def test_form( assert result["type"] == "form" assert result["step_id"] == config_entries.SOURCE_USER - with patch("homeassistant.components.whirlpool.config_flow.Auth.do_auth"), patch( - "homeassistant.components.whirlpool.config_flow.Auth.is_access_token_valid", - return_value=True, - ), patch( - "homeassistant.components.whirlpool.config_flow.BackendSelector" - ) as mock_backend_selector, patch( - "homeassistant.components.whirlpool.async_setup_entry", - return_value=True, - ) as mock_setup_entry, patch( - "homeassistant.components.whirlpool.config_flow.AppliancesManager.aircons", - return_value=["test"], - ), patch( - "homeassistant.components.whirlpool.config_flow.AppliancesManager.fetch_appliances", - return_value=True, + with ( + patch("homeassistant.components.whirlpool.config_flow.Auth.do_auth"), + patch( + "homeassistant.components.whirlpool.config_flow.Auth.is_access_token_valid", + return_value=True, + ), + patch( + "homeassistant.components.whirlpool.config_flow.BackendSelector" + ) as mock_backend_selector, + patch( + "homeassistant.components.whirlpool.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + patch( + "homeassistant.components.whirlpool.config_flow.AppliancesManager.aircons", + return_value=["test"], + ), + patch( + "homeassistant.components.whirlpool.config_flow.AppliancesManager.fetch_appliances", + return_value=True, + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - CONFIG_INPUT | {"region": region[0]}, + CONFIG_INPUT | {"region": region[0], "brand": brand[0]}, ) await hass.async_block_till_done() @@ -57,32 +62,33 @@ async def test_form( "username": "test-username", "password": "test-password", "region": region[0], + "brand": brand[0], } assert len(mock_setup_entry.mock_calls) == 1 - mock_backend_selector.assert_called_once_with(region[2], region[1]) + mock_backend_selector.assert_called_once_with(brand[1], region[1]) -async def test_form_invalid_auth(hass: HomeAssistant, region) -> None: +async def test_form_invalid_auth(hass: HomeAssistant, region, brand) -> None: """Test we handle invalid auth.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch("homeassistant.components.whirlpool.config_flow.Auth.do_auth"), patch( - "homeassistant.components.whirlpool.config_flow.Auth.is_access_token_valid", - return_value=False, + with ( + patch("homeassistant.components.whirlpool.config_flow.Auth.do_auth"), + patch( + "homeassistant.components.whirlpool.config_flow.Auth.is_access_token_valid", + return_value=False, + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - CONFIG_INPUT - | { - "region": region[0], - }, + CONFIG_INPUT | {"region": region[0], "brand": brand[0]}, ) assert result2["type"] == "form" assert result2["errors"] == {"base": "invalid_auth"} -async def test_form_cannot_connect(hass: HomeAssistant, region) -> None: +async def test_form_cannot_connect(hass: HomeAssistant, region, brand) -> None: """Test we handle cannot connect error.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -96,13 +102,14 @@ async def test_form_cannot_connect(hass: HomeAssistant, region) -> None: CONFIG_INPUT | { "region": region[0], + "brand": brand[0], }, ) assert result2["type"] == "form" assert result2["errors"] == {"base": "cannot_connect"} -async def test_form_auth_timeout(hass: HomeAssistant, region) -> None: +async def test_form_auth_timeout(hass: HomeAssistant, region, brand) -> None: """Test we handle auth timeout error.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -116,13 +123,14 @@ async def test_form_auth_timeout(hass: HomeAssistant, region) -> None: CONFIG_INPUT | { "region": region[0], + "brand": brand[0], }, ) assert result2["type"] == "form" assert result2["errors"] == {"base": "cannot_connect"} -async def test_form_generic_auth_exception(hass: HomeAssistant, region) -> None: +async def test_form_generic_auth_exception(hass: HomeAssistant, region, brand) -> None: """Test we handle cannot connect error.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -136,17 +144,18 @@ async def test_form_generic_auth_exception(hass: HomeAssistant, region) -> None: CONFIG_INPUT | { "region": region[0], + "brand": brand[0], }, ) assert result2["type"] == "form" assert result2["errors"] == {"base": "unknown"} -async def test_form_already_configured(hass: HomeAssistant, region) -> None: +async def test_form_already_configured(hass: HomeAssistant, region, brand) -> None: """Test we handle cannot connect error.""" mock_entry = MockConfigEntry( domain=DOMAIN, - data=CONFIG_INPUT | {"region": region[0]}, + data=CONFIG_INPUT | {"region": region[0], "brand": brand[0]}, unique_id="test-username", ) mock_entry.add_to_hass(hass) @@ -158,21 +167,27 @@ async def test_form_already_configured(hass: HomeAssistant, region) -> None: assert result["type"] == "form" assert result["step_id"] == config_entries.SOURCE_USER - with patch("homeassistant.components.whirlpool.config_flow.Auth.do_auth"), patch( - "homeassistant.components.whirlpool.config_flow.Auth.is_access_token_valid", - return_value=True, - ), patch( - "homeassistant.components.whirlpool.config_flow.AppliancesManager.aircons", - return_value=["test"], - ), patch( - "homeassistant.components.whirlpool.config_flow.AppliancesManager.fetch_appliances", - return_value=True, + with ( + patch("homeassistant.components.whirlpool.config_flow.Auth.do_auth"), + patch( + "homeassistant.components.whirlpool.config_flow.Auth.is_access_token_valid", + return_value=True, + ), + patch( + "homeassistant.components.whirlpool.config_flow.AppliancesManager.aircons", + return_value=["test"], + ), + patch( + "homeassistant.components.whirlpool.config_flow.AppliancesManager.fetch_appliances", + return_value=True, + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], CONFIG_INPUT | { "region": region[0], + "brand": brand[0], }, ) await hass.async_block_till_done() @@ -181,8 +196,8 @@ async def test_form_already_configured(hass: HomeAssistant, region) -> None: assert result2["reason"] == "already_configured" -async def test_no_appliances_flow(hass: HomeAssistant, region) -> None: - """Test we get and error with no appliances.""" +async def test_no_appliances_flow(hass: HomeAssistant, region, brand) -> None: + """Test we get an error with no appliances.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -190,16 +205,20 @@ async def test_no_appliances_flow(hass: HomeAssistant, region) -> None: assert result["type"] == FlowResultType.FORM assert result["step_id"] == config_entries.SOURCE_USER - with patch("homeassistant.components.whirlpool.config_flow.Auth.do_auth"), patch( - "homeassistant.components.whirlpool.config_flow.Auth.is_access_token_valid", - return_value=True, - ), patch( - "homeassistant.components.whirlpool.config_flow.AppliancesManager.fetch_appliances", - return_value=True, + with ( + patch("homeassistant.components.whirlpool.config_flow.Auth.do_auth"), + patch( + "homeassistant.components.whirlpool.config_flow.Auth.is_access_token_valid", + return_value=True, + ), + patch( + "homeassistant.components.whirlpool.config_flow.AppliancesManager.fetch_appliances", + return_value=True, + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - CONFIG_INPUT | {"region": region[0]}, + CONFIG_INPUT | {"region": region[0], "brand": brand[0]}, ) await hass.async_block_till_done() @@ -207,11 +226,11 @@ async def test_no_appliances_flow(hass: HomeAssistant, region) -> None: assert result2["errors"] == {"base": "no_appliances"} -async def test_reauth_flow(hass: HomeAssistant, region) -> None: +async def test_reauth_flow(hass: HomeAssistant, region, brand) -> None: """Test a successful reauth flow.""" mock_entry = MockConfigEntry( domain=DOMAIN, - data=CONFIG_INPUT | {"region": region[0]}, + data=CONFIG_INPUT | {"region": region[0], "brand": brand[0]}, unique_id="test-username", ) mock_entry.add_to_hass(hass) @@ -223,29 +242,35 @@ async def test_reauth_flow(hass: HomeAssistant, region) -> None: "unique_id": mock_entry.unique_id, "entry_id": mock_entry.entry_id, }, - data=CONFIG_INPUT | {"region": region[0]}, + data=CONFIG_INPUT | {"region": region[0], "brand": brand[0]}, ) assert result["step_id"] == "reauth_confirm" assert result["type"] == FlowResultType.FORM assert result["errors"] == {} - with patch( - "homeassistant.components.whirlpool.async_setup_entry", - return_value=True, - ), patch("homeassistant.components.whirlpool.config_flow.Auth.do_auth"), patch( - "homeassistant.components.whirlpool.config_flow.Auth.is_access_token_valid", - return_value=True, - ), patch( - "homeassistant.components.whirlpool.config_flow.AppliancesManager.aircons", - return_value=["test"], - ), patch( - "homeassistant.components.whirlpool.config_flow.AppliancesManager.fetch_appliances", - return_value=True, + with ( + patch( + "homeassistant.components.whirlpool.async_setup_entry", + return_value=True, + ), + patch("homeassistant.components.whirlpool.config_flow.Auth.do_auth"), + patch( + "homeassistant.components.whirlpool.config_flow.Auth.is_access_token_valid", + return_value=True, + ), + patch( + "homeassistant.components.whirlpool.config_flow.AppliancesManager.aircons", + return_value=["test"], + ), + patch( + "homeassistant.components.whirlpool.config_flow.AppliancesManager.fetch_appliances", + return_value=True, + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - {CONF_PASSWORD: "new-password"}, + {CONF_PASSWORD: "new-password", CONF_BRAND: brand[0]}, ) await hass.async_block_till_done() @@ -255,15 +280,16 @@ async def test_reauth_flow(hass: HomeAssistant, region) -> None: CONF_USERNAME: "test-username", CONF_PASSWORD: "new-password", "region": region[0], + "brand": brand[0], } -async def test_reauth_flow_auth_error(hass: HomeAssistant, region) -> None: +async def test_reauth_flow_auth_error(hass: HomeAssistant, region, brand) -> None: """Test an authorization error reauth flow.""" mock_entry = MockConfigEntry( domain=DOMAIN, - data=CONFIG_INPUT | {"region": region[0]}, + data=CONFIG_INPUT | {"region": region[0], "brand": brand[0]}, unique_id="test-username", ) mock_entry.add_to_hass(hass) @@ -279,22 +305,27 @@ async def test_reauth_flow_auth_error(hass: HomeAssistant, region) -> None: CONF_USERNAME: "test-username", CONF_PASSWORD: "new-password", "region": region[0], + "brand": brand[0], }, ) assert result["step_id"] == "reauth_confirm" assert result["type"] == FlowResultType.FORM assert result["errors"] == {} - with patch( - "homeassistant.components.whirlpool.async_setup_entry", - return_value=True, - ), patch("homeassistant.components.whirlpool.config_flow.Auth.do_auth"), patch( - "homeassistant.components.whirlpool.config_flow.Auth.is_access_token_valid", - return_value=False, + with ( + patch( + "homeassistant.components.whirlpool.async_setup_entry", + return_value=True, + ), + patch("homeassistant.components.whirlpool.config_flow.Auth.do_auth"), + patch( + "homeassistant.components.whirlpool.config_flow.Auth.is_access_token_valid", + return_value=False, + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - {CONF_PASSWORD: "new-password"}, + {CONF_PASSWORD: "new-password", CONF_BRAND: brand[0]}, ) await hass.async_block_till_done() @@ -302,12 +333,14 @@ async def test_reauth_flow_auth_error(hass: HomeAssistant, region) -> None: assert result2["errors"] == {"base": "invalid_auth"} -async def test_reauth_flow_connnection_error(hass: HomeAssistant, region) -> None: +async def test_reauth_flow_connnection_error( + hass: HomeAssistant, region, brand +) -> None: """Test a connection error reauth flow.""" mock_entry = MockConfigEntry( domain=DOMAIN, - data=CONFIG_INPUT | {"region": region[0]}, + data=CONFIG_INPUT | {"region": region[0], "brand": brand[0]}, unique_id="test-username", ) mock_entry.add_to_hass(hass) @@ -319,26 +352,30 @@ async def test_reauth_flow_connnection_error(hass: HomeAssistant, region) -> Non "unique_id": mock_entry.unique_id, "entry_id": mock_entry.entry_id, }, - data=CONFIG_INPUT | {"region": region[0]}, + data=CONFIG_INPUT | {"region": region[0], "brand": brand[0]}, ) assert result["step_id"] == "reauth_confirm" assert result["type"] == FlowResultType.FORM assert result["errors"] == {} - with patch( - "homeassistant.components.whirlpool.async_setup_entry", - return_value=True, - ), patch( - "homeassistant.components.whirlpool.config_flow.Auth.do_auth", - side_effect=ClientConnectionError, - ), patch( - "homeassistant.components.whirlpool.config_flow.Auth.is_access_token_valid", - return_value=False, + with ( + patch( + "homeassistant.components.whirlpool.async_setup_entry", + return_value=True, + ), + patch( + "homeassistant.components.whirlpool.config_flow.Auth.do_auth", + side_effect=ClientConnectionError, + ), + patch( + "homeassistant.components.whirlpool.config_flow.Auth.is_access_token_valid", + return_value=False, + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - {CONF_PASSWORD: "new-password"}, + {CONF_PASSWORD: "new-password", CONF_BRAND: brand[0]}, ) await hass.async_block_till_done() diff --git a/tests/components/whirlpool/test_diagnostics.py b/tests/components/whirlpool/test_diagnostics.py new file mode 100644 index 00000000000..6cfc1b76e38 --- /dev/null +++ b/tests/components/whirlpool/test_diagnostics.py @@ -0,0 +1,32 @@ +"""Test Blink diagnostics.""" + +from unittest.mock import MagicMock + +from syrupy import SnapshotAssertion +from syrupy.filters import props + +from homeassistant.core import HomeAssistant + +from . import init_integration + +from tests.components.diagnostics import get_diagnostics_for_config_entry +from tests.typing import ClientSessionGenerator + +YAML_CONFIG = {"username": "test-user", "password": "test-password"} + + +async def test_entry_diagnostics( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + snapshot: SnapshotAssertion, + mock_appliances_manager_api: MagicMock, + mock_aircon1_api: MagicMock, + mock_aircon_api_instances: MagicMock, +) -> None: + """Test config entry diagnostics.""" + + mock_entry = await init_integration(hass) + + result = await get_diagnostics_for_config_entry(hass, hass_client, mock_entry) + + assert result == snapshot(exclude=props("entry_id")) diff --git a/tests/components/whirlpool/test_init.py b/tests/components/whirlpool/test_init.py index 0eda2442b2c..f9d28e78a06 100644 --- a/tests/components/whirlpool/test_init.py +++ b/tests/components/whirlpool/test_init.py @@ -1,4 +1,5 @@ """Test the Whirlpool Sixth Sense init.""" + from unittest.mock import AsyncMock, MagicMock import aiohttp @@ -6,7 +7,7 @@ from whirlpool.backendselector import Brand, Region from homeassistant.components.whirlpool.const import DOMAIN from homeassistant.config_entries import ConfigEntryState -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_PASSWORD, CONF_REGION, CONF_USERNAME from homeassistant.core import HomeAssistant from . import init_integration, init_integration_with_entry @@ -18,13 +19,14 @@ async def test_setup( hass: HomeAssistant, mock_backend_selector_api: MagicMock, region, + brand, mock_aircon_api_instances: MagicMock, ) -> None: """Test setup.""" - entry = await init_integration(hass, region[0]) + entry = await init_integration(hass, region[0], brand[0]) assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert entry.state is ConfigEntryState.LOADED - mock_backend_selector_api.assert_called_once_with(region[2], region[1]) + mock_backend_selector_api.assert_called_once_with(brand[1], region[1]) async def test_setup_region_fallback( @@ -50,6 +52,31 @@ async def test_setup_region_fallback( mock_backend_selector_api.assert_called_once_with(Brand.Whirlpool, Region.EU) +async def test_setup_brand_fallback( + hass: HomeAssistant, + region, + mock_backend_selector_api: MagicMock, + mock_aircon_api_instances: MagicMock, +) -> None: + """Test setup when no brand is available on the ConfigEntry. + + This can happen after a version update, since the brand was not selected or stored in the earlier versions. + """ + + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_USERNAME: "nobody", + CONF_PASSWORD: "qwerty", + CONF_REGION: region[0], + }, + ) + entry = await init_integration_with_entry(hass, entry) + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert entry.state is ConfigEntryState.LOADED + mock_backend_selector_api.assert_called_once_with(Brand.Whirlpool, region[1]) + + async def test_setup_http_exception( hass: HomeAssistant, mock_auth_api: MagicMock, diff --git a/tests/components/whirlpool/test_sensor.py b/tests/components/whirlpool/test_sensor.py index 20d0c436134..fc509f264c5 100644 --- a/tests/components/whirlpool/test_sensor.py +++ b/tests/components/whirlpool/test_sensor.py @@ -1,4 +1,5 @@ """Test the Whirlpool Sensor domain.""" + from datetime import UTC, datetime from unittest.mock import MagicMock diff --git a/tests/components/whois/conftest.py b/tests/components/whois/conftest.py index 1ea31621e88..457c06db598 100644 --- a/tests/components/whois/conftest.py +++ b/tests/components/whois/conftest.py @@ -1,4 +1,5 @@ """Fixtures for Whois integration tests.""" + from __future__ import annotations from collections.abc import Generator @@ -40,10 +41,11 @@ def mock_setup_entry() -> Generator[AsyncMock, None, None]: @pytest.fixture def mock_whois() -> Generator[MagicMock, None, None]: """Return a mocked query.""" - with patch( - "homeassistant.components.whois.whois_query", - ) as whois_mock, patch( - "homeassistant.components.whois.config_flow.whois.query", new=whois_mock + with ( + patch( + "homeassistant.components.whois.whois_query", + ) as whois_mock, + patch("homeassistant.components.whois.config_flow.whois.query", new=whois_mock), ): domain = whois_mock.return_value domain.abuse_contact = "abuse@example.com" diff --git a/tests/components/whois/snapshots/test_sensor.ambr b/tests/components/whois/snapshots/test_sensor.ambr index 99299ae36da..61762c36e59 100644 --- a/tests/components/whois/snapshots/test_sensor.ambr +++ b/tests/components/whois/snapshots/test_sensor.ambr @@ -3,11 +3,11 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'home-assistant.io Admin', - 'icon': 'mdi:account-star', }), 'context': , 'entity_id': 'sensor.home_assistant_io_admin', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'admin@example.com', }) @@ -35,7 +35,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:account-star', + 'original_icon': None, 'original_name': 'Admin', 'platform': 'whois', 'previous_unique_id': None, @@ -84,6 +84,7 @@ 'context': , 'entity_id': 'sensor.home_assistant_io_created', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '2019-01-01T00:00:00+00:00', }) @@ -156,7 +157,6 @@ 'attributes': ReadOnlyDict({ 'expires': '2023-01-01T00:00:00', 'friendly_name': 'home-assistant.io Days until expiration', - 'icon': 'mdi:calendar-clock', 'name_servers': 'ns1.example.com ns2.example.com', 'registrar': 'My Registrar', 'unit_of_measurement': , @@ -165,6 +165,7 @@ 'context': , 'entity_id': 'sensor.home_assistant_io_days_until_expiration', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '364', }) @@ -192,7 +193,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:calendar-clock', + 'original_icon': None, 'original_name': 'Days until expiration', 'platform': 'whois', 'previous_unique_id': None, @@ -241,6 +242,7 @@ 'context': , 'entity_id': 'sensor.home_assistant_io_expires', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '2023-01-01T00:00:00+00:00', }) @@ -317,6 +319,7 @@ 'context': , 'entity_id': 'sensor.home_assistant_io_last_updated', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '2021-12-31T23:00:00+00:00', }) @@ -388,11 +391,11 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'home-assistant.io Owner', - 'icon': 'mdi:account', }), 'context': , 'entity_id': 'sensor.home_assistant_io_owner', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'owner@example.com', }) @@ -420,7 +423,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:account', + 'original_icon': None, 'original_name': 'Owner', 'platform': 'whois', 'previous_unique_id': None, @@ -464,11 +467,11 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'home-assistant.io Registrant', - 'icon': 'mdi:account-edit', }), 'context': , 'entity_id': 'sensor.home_assistant_io_registrant', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'registrant@example.com', }) @@ -496,7 +499,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:account-edit', + 'original_icon': None, 'original_name': 'Registrant', 'platform': 'whois', 'previous_unique_id': None, @@ -540,11 +543,11 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'home-assistant.io Registrar', - 'icon': 'mdi:store', }), 'context': , 'entity_id': 'sensor.home_assistant_io_registrar', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'My Registrar', }) @@ -572,7 +575,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:store', + 'original_icon': None, 'original_name': 'Registrar', 'platform': 'whois', 'previous_unique_id': None, @@ -616,11 +619,11 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'home-assistant.io Reseller', - 'icon': 'mdi:store', }), 'context': , 'entity_id': 'sensor.home_assistant_io_reseller', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'Top Domains, Low Prices', }) @@ -648,7 +651,7 @@ 'options': dict({ }), 'original_device_class': None, - 'original_icon': 'mdi:store', + 'original_icon': None, 'original_name': 'Reseller', 'platform': 'whois', 'previous_unique_id': None, @@ -697,6 +700,7 @@ 'context': , 'entity_id': 'sensor.home_assistant_io_last_updated', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '2021-12-31T23:00:00+00:00', }) diff --git a/tests/components/whois/test_config_flow.py b/tests/components/whois/test_config_flow.py index 91aa207d60f..1d3f1a8c6d2 100644 --- a/tests/components/whois/test_config_flow.py +++ b/tests/components/whois/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for the Whois config flow.""" + from unittest.mock import AsyncMock, MagicMock import pytest diff --git a/tests/components/whois/test_diagnostics.py b/tests/components/whois/test_diagnostics.py index a134c2136a3..49869fb8bf7 100644 --- a/tests/components/whois/test_diagnostics.py +++ b/tests/components/whois/test_diagnostics.py @@ -1,4 +1,5 @@ """Tests for the diagnostics data provided by the Whois integration.""" + from syrupy.assertion import SnapshotAssertion from homeassistant.core import HomeAssistant diff --git a/tests/components/whois/test_init.py b/tests/components/whois/test_init.py index 1cf2fb7b5ea..0765661c574 100644 --- a/tests/components/whois/test_init.py +++ b/tests/components/whois/test_init.py @@ -1,4 +1,5 @@ """Tests for the Whois integration.""" + from unittest.mock import MagicMock import pytest diff --git a/tests/components/whois/test_sensor.py b/tests/components/whois/test_sensor.py index a7c1ad82291..d58cc342745 100644 --- a/tests/components/whois/test_sensor.py +++ b/tests/components/whois/test_sensor.py @@ -1,4 +1,5 @@ """Tests for the sensors provided by the Whois integration.""" + from unittest.mock import MagicMock import pytest @@ -21,7 +22,7 @@ pytestmark = [ @pytest.mark.usefixtures("entity_registry_enabled_by_default") @pytest.mark.parametrize( "entity_id", - ( + [ "sensor.home_assistant_io_admin", "sensor.home_assistant_io_created", "sensor.home_assistant_io_days_until_expiration", @@ -31,7 +32,7 @@ pytestmark = [ "sensor.home_assistant_io_registrant", "sensor.home_assistant_io_registrar", "sensor.home_assistant_io_reseller", - ), + ], ) async def test_whois_sensors( hass: HomeAssistant, @@ -66,13 +67,13 @@ async def test_whois_sensors_missing_some_attrs( @pytest.mark.parametrize( "entity_id", - ( + [ "sensor.home_assistant_io_admin", "sensor.home_assistant_io_owner", "sensor.home_assistant_io_registrant", "sensor.home_assistant_io_registrar", "sensor.home_assistant_io_reseller", - ), + ], ) async def test_disabled_by_default_sensors( hass: HomeAssistant, entity_id: str, entity_registry: er.EntityRegistry @@ -87,7 +88,7 @@ async def test_disabled_by_default_sensors( @pytest.mark.usefixtures("entity_registry_enabled_by_default") @pytest.mark.parametrize( "entity_id", - ( + [ "sensor.home_assistant_io_admin", "sensor.home_assistant_io_created", "sensor.home_assistant_io_days_until_expiration", @@ -97,7 +98,7 @@ async def test_disabled_by_default_sensors( "sensor.home_assistant_io_registrant", "sensor.home_assistant_io_registrar", "sensor.home_assistant_io_reseller", - ), + ], ) async def test_no_data( hass: HomeAssistant, mock_whois: MagicMock, entity_id: str diff --git a/tests/components/wiffi/conftest.py b/tests/components/wiffi/conftest.py index bceded737c6..644c3c460ed 100644 --- a/tests/components/wiffi/conftest.py +++ b/tests/components/wiffi/conftest.py @@ -1,4 +1,5 @@ """Configuration for Wiffi tests.""" + from collections.abc import Generator from unittest.mock import AsyncMock, patch diff --git a/tests/components/wiffi/test_config_flow.py b/tests/components/wiffi/test_config_flow.py index bf0b072df8c..14cb8a03f7a 100644 --- a/tests/components/wiffi/test_config_flow.py +++ b/tests/components/wiffi/test_config_flow.py @@ -1,4 +1,5 @@ """Test the wiffi integration config flow.""" + import errno from unittest.mock import patch diff --git a/tests/components/wilight/conftest.py b/tests/components/wilight/conftest.py index b20a7757e22..dc8bef004ce 100644 --- a/tests/components/wilight/conftest.py +++ b/tests/components/wilight/conftest.py @@ -1,2 +1,3 @@ """wilight conftest.""" + from tests.components.light.conftest import mock_light_profiles # noqa: F401 diff --git a/tests/components/wilight/test_config_flow.py b/tests/components/wilight/test_config_flow.py index a46af6e7d82..e3496010c95 100644 --- a/tests/components/wilight/test_config_flow.py +++ b/tests/components/wilight/test_config_flow.py @@ -1,4 +1,5 @@ """Test the WiLight config flow.""" + import dataclasses from unittest.mock import patch diff --git a/tests/components/wilight/test_cover.py b/tests/components/wilight/test_cover.py index ce0a65ca29a..93da57a7f7f 100644 --- a/tests/components/wilight/test_cover.py +++ b/tests/components/wilight/test_cover.py @@ -1,4 +1,5 @@ """Tests for the WiLight integration.""" + from unittest.mock import patch import pytest diff --git a/tests/components/wilight/test_fan.py b/tests/components/wilight/test_fan.py index dc3ad57e11f..7b2e9550c53 100644 --- a/tests/components/wilight/test_fan.py +++ b/tests/components/wilight/test_fan.py @@ -1,4 +1,5 @@ """Tests for the WiLight integration.""" + from unittest.mock import patch import pytest diff --git a/tests/components/wilight/test_init.py b/tests/components/wilight/test_init.py index 860fd8ea54d..aed45fcb023 100644 --- a/tests/components/wilight/test_init.py +++ b/tests/components/wilight/test_init.py @@ -1,4 +1,5 @@ """Tests for the WiLight integration.""" + from unittest.mock import patch import pytest diff --git a/tests/components/wilight/test_light.py b/tests/components/wilight/test_light.py index f82d493e70c..44c0060c5bb 100644 --- a/tests/components/wilight/test_light.py +++ b/tests/components/wilight/test_light.py @@ -1,4 +1,5 @@ """Tests for the WiLight integration.""" + from unittest.mock import patch import pytest diff --git a/tests/components/wilight/test_switch.py b/tests/components/wilight/test_switch.py index 32b54a9756d..6026cec9847 100644 --- a/tests/components/wilight/test_switch.py +++ b/tests/components/wilight/test_switch.py @@ -1,4 +1,5 @@ """Tests for the WiLight integration.""" + from unittest.mock import patch import pytest diff --git a/tests/components/withings/__init__.py b/tests/components/withings/__init__.py index cd0e9994f74..4b97fc48834 100644 --- a/tests/components/withings/__init__.py +++ b/tests/components/withings/__init__.py @@ -1,4 +1,5 @@ """Tests for the withings component.""" + from dataclasses import dataclass from datetime import timedelta from typing import Any diff --git a/tests/components/withings/conftest.py b/tests/components/withings/conftest.py index 7f15c5e0252..66dd65efccb 100644 --- a/tests/components/withings/conftest.py +++ b/tests/components/withings/conftest.py @@ -1,4 +1,5 @@ """Fixtures for tests.""" + from datetime import timedelta import time from unittest.mock import AsyncMock, patch @@ -173,11 +174,14 @@ def disable_webhook_delay(): """Disable webhook delay.""" mock = AsyncMock() - with patch( - "homeassistant.components.withings.SUBSCRIBE_DELAY", - timedelta(seconds=0), - ), patch( - "homeassistant.components.withings.UNSUBSCRIBE_DELAY", - timedelta(seconds=0), + with ( + patch( + "homeassistant.components.withings.SUBSCRIBE_DELAY", + timedelta(seconds=0), + ), + patch( + "homeassistant.components.withings.UNSUBSCRIBE_DELAY", + timedelta(seconds=0), + ), ): yield mock diff --git a/tests/components/withings/snapshots/test_sensor.ambr b/tests/components/withings/snapshots/test_sensor.ambr index aeaa371b0e5..37635ece403 100644 --- a/tests/components/withings/snapshots/test_sensor.ambr +++ b/tests/components/withings/snapshots/test_sensor.ambr @@ -48,6 +48,7 @@ 'context': , 'entity_id': 'sensor.henk_active_calories_burnt_today', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '221.132', }) @@ -102,6 +103,7 @@ 'context': , 'entity_id': 'sensor.henk_active_time_today', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0.530', }) @@ -151,6 +153,7 @@ 'context': , 'entity_id': 'sensor.henk_average_heart_rate', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '103', }) @@ -200,6 +203,7 @@ 'context': , 'entity_id': 'sensor.henk_average_respiratory_rate', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '14', }) @@ -250,6 +254,7 @@ 'context': , 'entity_id': 'sensor.henk_body_temperature', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '40', }) @@ -303,6 +308,7 @@ 'context': , 'entity_id': 'sensor.henk_bone_mass', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '10', }) @@ -351,6 +357,7 @@ 'context': , 'entity_id': 'sensor.henk_breathing_disturbances_intensity', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '9', }) @@ -400,6 +407,7 @@ 'context': , 'entity_id': 'sensor.henk_calories_burnt_last_workout', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '24', }) @@ -450,6 +458,7 @@ 'context': , 'entity_id': 'sensor.henk_deep_sleep', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '5820', }) @@ -499,6 +508,7 @@ 'context': , 'entity_id': 'sensor.henk_diastolic_blood_pressure', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '70', }) @@ -549,6 +559,7 @@ 'context': , 'entity_id': 'sensor.henk_distance_travelled_last_workout', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '232', }) @@ -603,6 +614,7 @@ 'context': , 'entity_id': 'sensor.henk_distance_travelled_today', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '1020.121', }) @@ -649,6 +661,7 @@ 'context': , 'entity_id': 'sensor.henk_electrodermal_activity_feet', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '102', }) @@ -695,6 +708,7 @@ 'context': , 'entity_id': 'sensor.henk_electrodermal_activity_left_foot', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '102', }) @@ -741,6 +755,7 @@ 'context': , 'entity_id': 'sensor.henk_electrodermal_activity_right_foot', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '102', }) @@ -788,6 +803,7 @@ 'context': , 'entity_id': 'sensor.henk_elevation_change_last_workout', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '4', }) @@ -839,6 +855,7 @@ 'context': , 'entity_id': 'sensor.henk_elevation_change_today', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0', }) @@ -889,6 +906,7 @@ 'context': , 'entity_id': 'sensor.henk_extracellular_water', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '100', }) @@ -942,6 +960,7 @@ 'context': , 'entity_id': 'sensor.henk_fat_free_mass', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '60', }) @@ -995,6 +1014,7 @@ 'context': , 'entity_id': 'sensor.henk_fat_mass', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '5', }) @@ -1047,6 +1067,7 @@ 'context': , 'entity_id': 'sensor.henk_fat_ratio', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0.07', }) @@ -1096,6 +1117,7 @@ 'context': , 'entity_id': 'sensor.henk_heart_pulse', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '60', }) @@ -1149,6 +1171,7 @@ 'context': , 'entity_id': 'sensor.henk_height', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '2', }) @@ -1199,6 +1222,7 @@ 'context': , 'entity_id': 'sensor.henk_hydration', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0.95', }) @@ -1253,6 +1277,7 @@ 'context': , 'entity_id': 'sensor.henk_intense_activity_today', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '7.0', }) @@ -1303,6 +1328,7 @@ 'context': , 'entity_id': 'sensor.henk_intracellular_water', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '100', }) @@ -1353,6 +1379,7 @@ 'context': , 'entity_id': 'sensor.henk_last_workout_duration', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '4.25', }) @@ -1398,6 +1425,7 @@ 'context': , 'entity_id': 'sensor.henk_last_workout_intensity', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '30', }) @@ -1547,6 +1575,7 @@ 'context': , 'entity_id': 'sensor.henk_last_workout_type', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'walk', }) @@ -1597,6 +1626,7 @@ 'context': , 'entity_id': 'sensor.henk_light_sleep', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '10440', }) @@ -1646,6 +1676,7 @@ 'context': , 'entity_id': 'sensor.henk_maximum_heart_rate', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '120', }) @@ -1695,6 +1726,7 @@ 'context': , 'entity_id': 'sensor.henk_maximum_respiratory_rate', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '20', }) @@ -1744,6 +1776,7 @@ 'context': , 'entity_id': 'sensor.henk_minimum_heart_rate', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '70', }) @@ -1793,6 +1826,7 @@ 'context': , 'entity_id': 'sensor.henk_minimum_respiratory_rate', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '10', }) @@ -1847,6 +1881,7 @@ 'context': , 'entity_id': 'sensor.henk_moderate_activity_today', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '24.8', }) @@ -1900,6 +1935,7 @@ 'context': , 'entity_id': 'sensor.henk_muscle_mass', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '50', }) @@ -1950,6 +1986,7 @@ 'context': , 'entity_id': 'sensor.henk_pause_during_last_workout', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0.0', }) @@ -2000,6 +2037,7 @@ 'context': , 'entity_id': 'sensor.henk_pulse_wave_velocity', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '100', }) @@ -2050,6 +2088,7 @@ 'context': , 'entity_id': 'sensor.henk_rem_sleep', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '2400', }) @@ -2100,6 +2139,7 @@ 'context': , 'entity_id': 'sensor.henk_skin_temperature', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '20', }) @@ -2153,6 +2193,7 @@ 'context': , 'entity_id': 'sensor.henk_sleep_goal', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '8.000', }) @@ -2202,6 +2243,7 @@ 'context': , 'entity_id': 'sensor.henk_sleep_score', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '37', }) @@ -2250,6 +2292,7 @@ 'context': , 'entity_id': 'sensor.henk_snoring', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '1080', }) @@ -2298,6 +2341,7 @@ 'context': , 'entity_id': 'sensor.henk_snoring_episode_count', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '18', }) @@ -2352,6 +2396,7 @@ 'context': , 'entity_id': 'sensor.henk_soft_activity_today', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '25.3', }) @@ -2401,6 +2446,7 @@ 'context': , 'entity_id': 'sensor.henk_spo2', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0.95', }) @@ -2450,6 +2496,7 @@ 'context': , 'entity_id': 'sensor.henk_step_goal', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '10000', }) @@ -2500,6 +2547,7 @@ 'context': , 'entity_id': 'sensor.henk_steps_today', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '1155', }) @@ -2549,6 +2597,7 @@ 'context': , 'entity_id': 'sensor.henk_systolic_blood_pressure', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '100', }) @@ -2599,6 +2648,7 @@ 'context': , 'entity_id': 'sensor.henk_temperature', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '40', }) @@ -2649,6 +2699,7 @@ 'context': , 'entity_id': 'sensor.henk_time_to_sleep', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '540', }) @@ -2699,6 +2750,7 @@ 'context': , 'entity_id': 'sensor.henk_time_to_wakeup', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '1140', }) @@ -2752,6 +2804,7 @@ 'context': , 'entity_id': 'sensor.henk_total_calories_burnt_today', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '2444.149', }) @@ -2797,6 +2850,7 @@ 'context': , 'entity_id': 'sensor.henk_vascular_age', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '100', }) @@ -2842,6 +2896,7 @@ 'context': , 'entity_id': 'sensor.henk_visceral_fat_index', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '102', }) @@ -2891,6 +2946,7 @@ 'context': , 'entity_id': 'sensor.henk_vo2_max', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '100', }) @@ -2940,6 +2996,7 @@ 'context': , 'entity_id': 'sensor.henk_wakeup_count', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '1', }) @@ -2990,6 +3047,7 @@ 'context': , 'entity_id': 'sensor.henk_wakeup_time', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '3060', }) @@ -3043,6 +3101,7 @@ 'context': , 'entity_id': 'sensor.henk_weight', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '70', }) @@ -3093,6 +3152,7 @@ 'context': , 'entity_id': 'sensor.henk_weight_goal', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '70.5', }) diff --git a/tests/components/withings/test_binary_sensor.py b/tests/components/withings/test_binary_sensor.py index c93c4522684..56fc8bc49de 100644 --- a/tests/components/withings/test_binary_sensor.py +++ b/tests/components/withings/test_binary_sensor.py @@ -1,4 +1,5 @@ """Tests for the Withings component.""" + from unittest.mock import AsyncMock from aiohttp.client_exceptions import ClientResponseError diff --git a/tests/components/withings/test_calendar.py b/tests/components/withings/test_calendar.py index 014beb7a233..060a1baa54d 100644 --- a/tests/components/withings/test_calendar.py +++ b/tests/components/withings/test_calendar.py @@ -1,4 +1,5 @@ """Tests for the Withings calendar.""" + from datetime import date, timedelta from http import HTTPStatus from unittest.mock import AsyncMock diff --git a/tests/components/withings/test_config_flow.py b/tests/components/withings/test_config_flow.py index f8f8f62becf..9852461f5e2 100644 --- a/tests/components/withings/test_config_flow.py +++ b/tests/components/withings/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for config flow.""" + from unittest.mock import AsyncMock, patch from homeassistant.components.withings.const import DOMAIN diff --git a/tests/components/withings/test_diagnostics.py b/tests/components/withings/test_diagnostics.py index 928eccdde0f..d607584df7b 100644 --- a/tests/components/withings/test_diagnostics.py +++ b/tests/components/withings/test_diagnostics.py @@ -1,4 +1,5 @@ """Tests for the diagnostics data provided by the Withings integration.""" + from unittest.mock import AsyncMock, patch from freezegun.api import FrozenDateTimeFactory @@ -55,21 +56,26 @@ async def test_diagnostics_cloudhook_instance( freezer: FrozenDateTimeFactory, ) -> None: """Test diagnostics.""" - with patch( - "homeassistant.components.cloud.async_is_logged_in", return_value=True - ), patch( - "homeassistant.components.cloud.async_is_connected", return_value=True - ), patch( - "homeassistant.components.cloud.async_active_subscription", return_value=True - ), patch( - "homeassistant.components.cloud.async_create_cloudhook", - return_value="https://hooks.nabu.casa/ABCD", - ), patch( - "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", - ), patch( - "homeassistant.components.cloud.async_delete_cloudhook", - ), patch( - "homeassistant.components.withings.webhook_generate_url", + with ( + patch("homeassistant.components.cloud.async_is_logged_in", return_value=True), + patch("homeassistant.components.cloud.async_is_connected", return_value=True), + patch( + "homeassistant.components.cloud.async_active_subscription", + return_value=True, + ), + patch( + "homeassistant.components.cloud.async_create_cloudhook", + return_value="https://hooks.nabu.casa/ABCD", + ), + patch( + "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", + ), + patch( + "homeassistant.components.cloud.async_delete_cloudhook", + ), + patch( + "homeassistant.components.withings.webhook_generate_url", + ), ): await setup_integration(hass, webhook_config_entry) await prepare_webhook_setup(hass, freezer) diff --git a/tests/components/withings/test_init.py b/tests/components/withings/test_init.py index 390fbc3bbc3..eb089f44216 100644 --- a/tests/components/withings/test_init.py +++ b/tests/components/withings/test_init.py @@ -1,4 +1,5 @@ """Tests for the Withings component.""" + from datetime import timedelta from typing import Any from unittest.mock import AsyncMock, MagicMock, patch @@ -15,6 +16,7 @@ import pytest import voluptuous as vol from homeassistant import config_entries +from homeassistant.components import cloud from homeassistant.components.cloud import CloudNotAvailable from homeassistant.components.webhook import async_generate_url from homeassistant.components.withings import CONFIG_SCHEMA, async_setup @@ -293,24 +295,25 @@ async def test_setup_with_cloudhook( await mock_cloud(hass) await hass.async_block_till_done() - with patch( - "homeassistant.components.cloud.async_is_logged_in", return_value=True - ), patch( - "homeassistant.components.cloud.async_is_connected", return_value=True - ), patch( - "homeassistant.components.cloud.async_active_subscription", return_value=True - ), patch( - "homeassistant.components.cloud.async_create_cloudhook", - return_value="https://hooks.nabu.casa/ABCD", - ) as fake_create_cloudhook, patch( - "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", - ), patch( - "homeassistant.components.cloud.async_delete_cloudhook" - ) as fake_delete_cloudhook, patch( - "homeassistant.components.withings.webhook_generate_url" + with ( + patch("homeassistant.components.cloud.async_is_logged_in", return_value=True), + patch("homeassistant.components.cloud.async_is_connected", return_value=True), + patch.object(cloud, "async_active_subscription", return_value=True), + patch( + "homeassistant.components.cloud.async_create_cloudhook", + return_value="https://hooks.nabu.casa/ABCD", + ) as fake_create_cloudhook, + patch( + "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", + ), + patch( + "homeassistant.components.cloud.async_delete_cloudhook" + ) as fake_delete_cloudhook, + patch("homeassistant.components.withings.webhook_generate_url"), ): await setup_integration(hass, cloudhook_config_entry) - assert hass.components.cloud.async_active_subscription() is True + + assert cloud.async_active_subscription(hass) is True assert ( hass.config_entries.async_entries(DOMAIN)[0].data["cloudhook_url"] @@ -337,25 +340,28 @@ async def test_removing_entry_with_cloud_unavailable( await mock_cloud(hass) await hass.async_block_till_done() - with patch( - "homeassistant.components.cloud.async_is_logged_in", return_value=True - ), patch( - "homeassistant.components.cloud.async_is_connected", return_value=True - ), patch( - "homeassistant.components.cloud.async_active_subscription", return_value=True - ), patch( - "homeassistant.components.cloud.async_create_cloudhook", - return_value="https://hooks.nabu.casa/ABCD", - ), patch( - "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", - ), patch( - "homeassistant.components.cloud.async_delete_cloudhook", - side_effect=CloudNotAvailable(), - ), patch( - "homeassistant.components.withings.webhook_generate_url", + with ( + patch("homeassistant.components.cloud.async_is_logged_in", return_value=True), + patch("homeassistant.components.cloud.async_is_connected", return_value=True), + patch.object(cloud, "async_active_subscription", return_value=True), + patch( + "homeassistant.components.cloud.async_create_cloudhook", + return_value="https://hooks.nabu.casa/ABCD", + ), + patch( + "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", + ), + patch( + "homeassistant.components.cloud.async_delete_cloudhook", + side_effect=CloudNotAvailable(), + ), + patch( + "homeassistant.components.withings.webhook_generate_url", + ), ): await setup_integration(hass, cloudhook_config_entry) - assert hass.components.cloud.async_active_subscription() is True + + assert cloud.async_active_subscription(hass) is True await hass.async_block_till_done() assert hass.config_entries.async_entries(DOMAIN) @@ -377,27 +383,27 @@ async def test_setup_with_cloud( await mock_cloud(hass) await hass.async_block_till_done() - with patch( - "homeassistant.components.cloud.async_is_logged_in", return_value=True - ), patch( - "homeassistant.components.cloud.async_is_connected", return_value=True - ), patch( - "homeassistant.components.cloud.async_active_subscription", return_value=True - ), patch( - "homeassistant.components.cloud.async_create_cloudhook", - return_value="https://hooks.nabu.casa/ABCD", - ) as fake_create_cloudhook, patch( - "homeassistant.components.withings.async_get_config_entry_implementation", - ), patch( - "homeassistant.components.cloud.async_delete_cloudhook" - ) as fake_delete_cloudhook, patch( - "homeassistant.components.withings.webhook_generate_url" + with ( + patch("homeassistant.components.cloud.async_is_logged_in", return_value=True), + patch.object(cloud, "async_is_connected", return_value=True), + patch.object(cloud, "async_active_subscription", return_value=True), + patch( + "homeassistant.components.cloud.async_create_cloudhook", + return_value="https://hooks.nabu.casa/ABCD", + ) as fake_create_cloudhook, + patch( + "homeassistant.components.withings.async_get_config_entry_implementation", + ), + patch( + "homeassistant.components.cloud.async_delete_cloudhook" + ) as fake_delete_cloudhook, + patch("homeassistant.components.withings.webhook_generate_url"), ): await setup_integration(hass, webhook_config_entry) await prepare_webhook_setup(hass, freezer) - assert hass.components.cloud.async_active_subscription() is True - assert hass.components.cloud.async_is_connected() is True + assert cloud.async_active_subscription(hass) is True + assert cloud.async_is_connected(hass) is True fake_create_cloudhook.assert_called_once() fake_delete_cloudhook.assert_called_once() @@ -411,7 +417,7 @@ async def test_setup_with_cloud( for config_entry in hass.config_entries.async_entries("withings"): await hass.config_entries.async_remove(config_entry.entry_id) - fake_delete_cloudhook.call_count == 2 + assert fake_delete_cloudhook.call_count == 2 await hass.async_block_till_done() assert not hass.config_entries.async_entries(DOMAIN) @@ -428,14 +434,18 @@ async def test_setup_no_webhook( ) -> None: """Test if set up with cloud link and without https.""" hass.config.components.add("cloud") - with patch( - "homeassistant.helpers.network.get_url", - return_value="http://example.nabu.casa", - ), patch( - "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", - ), patch( - "homeassistant.components.withings.webhook_generate_url" - ) as mock_async_generate_url: + with ( + patch( + "homeassistant.helpers.network.get_url", + return_value="http://example.nabu.casa", + ), + patch( + "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", + ), + patch( + "homeassistant.components.withings.webhook_generate_url" + ) as mock_async_generate_url, + ): mock_async_generate_url.return_value = url await setup_integration(hass, webhook_config_entry) await prepare_webhook_setup(hass, freezer) @@ -457,26 +467,29 @@ async def test_cloud_disconnect( await mock_cloud(hass) await hass.async_block_till_done() - with patch( - "homeassistant.components.cloud.async_is_logged_in", return_value=True - ), patch( - "homeassistant.components.cloud.async_is_connected", return_value=True - ), patch( - "homeassistant.components.cloud.async_active_subscription", return_value=True - ), patch( - "homeassistant.components.cloud.async_create_cloudhook", - return_value="https://hooks.nabu.casa/ABCD", - ), patch( - "homeassistant.components.withings.async_get_config_entry_implementation", - ), patch( - "homeassistant.components.cloud.async_delete_cloudhook", - ), patch( - "homeassistant.components.withings.webhook_generate_url", + with ( + patch("homeassistant.components.cloud.async_is_logged_in", return_value=True), + patch.object(cloud, "async_is_connected", return_value=True), + patch.object(cloud, "async_active_subscription", return_value=True), + patch( + "homeassistant.components.cloud.async_create_cloudhook", + return_value="https://hooks.nabu.casa/ABCD", + ), + patch( + "homeassistant.components.withings.async_get_config_entry_implementation", + ), + patch( + "homeassistant.components.cloud.async_delete_cloudhook", + ), + patch( + "homeassistant.components.withings.webhook_generate_url", + ), ): await setup_integration(hass, webhook_config_entry) await prepare_webhook_setup(hass, freezer) - assert hass.components.cloud.async_active_subscription() is True - assert hass.components.cloud.async_is_connected() is True + + assert cloud.async_active_subscription(hass) is True + assert cloud.async_is_connected(hass) is True await hass.async_block_till_done() @@ -498,15 +511,15 @@ async def test_cloud_disconnect( @pytest.mark.parametrize( ("body", "expected_code"), [ - [{"userid": 0, "appli": NotificationCategory.WEIGHT.value}, 0], # Success - [{"userid": None, "appli": 1}, 0], # Success, we ignore the user_id. - [{}, 12], # No request body. - [{"userid": "GG"}, 20], # appli not provided. - [{"userid": 0}, 20], # appli not provided. - [ + ({"userid": 0, "appli": NotificationCategory.WEIGHT.value}, 0), # Success + ({"userid": None, "appli": 1}, 0), # Success, we ignore the user_id. + ({}, 12), # No request body. + ({"userid": "GG"}, 20), # appli not provided. + ({"userid": 0}, 20), # appli not provided. + ( {"userid": 11, "appli": NotificationCategory.WEIGHT.value}, 0, - ], # Success, we ignore the user_id + ), # Success, we ignore the user_id ], ) async def test_webhook_post( diff --git a/tests/components/withings/test_sensor.py b/tests/components/withings/test_sensor.py index 88018d54877..72da4b9d973 100644 --- a/tests/components/withings/test_sensor.py +++ b/tests/components/withings/test_sensor.py @@ -1,4 +1,5 @@ """Tests for the Withings component.""" + from datetime import timedelta from unittest.mock import AsyncMock, patch diff --git a/tests/components/wiz/__init__.py b/tests/components/wiz/__init__.py index 165e0557fcd..e80a1ed8249 100644 --- a/tests/components/wiz/__init__.py +++ b/tests/components/wiz/__init__.py @@ -232,9 +232,12 @@ def _patch_wizlight(device=None, extended_white_range=None, bulb_type=None): @contextmanager def _patcher(): bulb = device or _mocked_wizlight(device, extended_white_range, bulb_type) - with patch("homeassistant.components.wiz.wizlight", return_value=bulb), patch( - "homeassistant.components.wiz.config_flow.wizlight", - return_value=bulb, + with ( + patch("homeassistant.components.wiz.wizlight", return_value=bulb), + patch( + "homeassistant.components.wiz.config_flow.wizlight", + return_value=bulb, + ), ): yield diff --git a/tests/components/wiz/test_config_flow.py b/tests/components/wiz/test_config_flow.py index b1a56f6c624..1b84a048fd2 100644 --- a/tests/components/wiz/test_config_flow.py +++ b/tests/components/wiz/test_config_flow.py @@ -1,4 +1,5 @@ """Test the WiZ Platform config flow.""" + from unittest.mock import AsyncMock, patch import pytest @@ -51,12 +52,16 @@ async def test_form(hass: HomeAssistant) -> None: assert result["type"] == "form" assert result["errors"] == {} # Patch functions - with _patch_wizlight(), patch( - "homeassistant.components.wiz.async_setup_entry", - return_value=True, - ) as mock_setup_entry, patch( - "homeassistant.components.wiz.async_setup", return_value=True - ) as mock_setup: + with ( + _patch_wizlight(), + patch( + "homeassistant.components.wiz.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + patch( + "homeassistant.components.wiz.async_setup", return_value=True + ) as mock_setup, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], TEST_CONNECTION, @@ -89,12 +94,16 @@ async def test_user_flow_enters_dns_name(hass: HomeAssistant) -> None: assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "no_ip"} - with _patch_wizlight(), patch( - "homeassistant.components.wiz.async_setup_entry", - return_value=True, - ) as mock_setup_entry, patch( - "homeassistant.components.wiz.async_setup", return_value=True - ) as mock_setup: + with ( + _patch_wizlight(), + patch( + "homeassistant.components.wiz.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + patch( + "homeassistant.components.wiz.async_setup", return_value=True + ) as mock_setup, + ): result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], TEST_CONNECTION, @@ -264,14 +273,18 @@ async def test_discovered_by_dhcp_or_integration_discovery( assert result["type"] == FlowResultType.FORM assert result["step_id"] == "discovery_confirm" - with _patch_wizlight( - device=None, extended_white_range=extended_white_range, bulb_type=bulb_type - ), patch( - "homeassistant.components.wiz.async_setup_entry", - return_value=True, - ) as mock_setup_entry, patch( - "homeassistant.components.wiz.async_setup", return_value=True - ) as mock_setup: + with ( + _patch_wizlight( + device=None, extended_white_range=extended_white_range, bulb_type=bulb_type + ), + patch( + "homeassistant.components.wiz.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + patch( + "homeassistant.components.wiz.async_setup", return_value=True + ) as mock_setup, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {}, @@ -379,11 +392,15 @@ async def test_setup_via_discovery(hass: HomeAssistant) -> None: assert result2["step_id"] == "pick_device" assert not result2["errors"] - with _patch_wizlight(), patch( - "homeassistant.components.wiz.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.wiz.async_setup_entry", return_value=True - ) as mock_setup_entry: + with ( + _patch_wizlight(), + patch( + "homeassistant.components.wiz.async_setup", return_value=True + ) as mock_setup, + patch( + "homeassistant.components.wiz.async_setup_entry", return_value=True + ) as mock_setup_entry, + ): result3 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_DEVICE: FAKE_MAC}, @@ -432,10 +449,13 @@ async def test_setup_via_discovery_cannot_connect(hass: HomeAssistant) -> None: assert result2["step_id"] == "pick_device" assert not result2["errors"] - with patch( - "homeassistant.components.wiz.wizlight.getBulbConfig", - side_effect=WizLightTimeOutError, - ), _patch_discovery(): + with ( + patch( + "homeassistant.components.wiz.wizlight.getBulbConfig", + side_effect=WizLightTimeOutError, + ), + _patch_discovery(), + ): result3 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_DEVICE: FAKE_MAC}, @@ -488,15 +508,19 @@ async def test_discovery_with_firmware_update(hass: HomeAssistant) -> None: # updates and we now can see its really RGBWW not RGBW since the older # firmwares did not tell us how many white channels exist - with patch( - "homeassistant.components.wiz.async_setup_entry", - return_value=True, - ) as mock_setup_entry, patch( - "homeassistant.components.wiz.async_setup", return_value=True - ) as mock_setup, _patch_wizlight( - device=None, - extended_white_range=FAKE_EXTENDED_WHITE_RANGE, - bulb_type=FAKE_RGBWW_BULB, + with ( + patch( + "homeassistant.components.wiz.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + patch( + "homeassistant.components.wiz.async_setup", return_value=True + ) as mock_setup, + _patch_wizlight( + device=None, + extended_white_range=FAKE_EXTENDED_WHITE_RANGE, + bulb_type=FAKE_RGBWW_BULB, + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -522,13 +546,18 @@ async def test_discovery_with_firmware_update(hass: HomeAssistant) -> None: ) async def test_discovered_during_onboarding(hass: HomeAssistant, source, data) -> None: """Test dhcp or discovery during onboarding creates the config entry.""" - with _patch_wizlight(), patch( - "homeassistant.components.wiz.async_setup_entry", - return_value=True, - ) as mock_setup_entry, patch( - "homeassistant.components.wiz.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.onboarding.async_is_onboarded", return_value=False + with ( + _patch_wizlight(), + patch( + "homeassistant.components.wiz.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + patch( + "homeassistant.components.wiz.async_setup", return_value=True + ) as mock_setup, + patch( + "homeassistant.components.onboarding.async_is_onboarded", return_value=False + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": source}, data=data diff --git a/tests/components/wiz/test_diagnostics.py b/tests/components/wiz/test_diagnostics.py index ef26e63069b..07178d5e93b 100644 --- a/tests/components/wiz/test_diagnostics.py +++ b/tests/components/wiz/test_diagnostics.py @@ -1,4 +1,5 @@ """Test WiZ diagnostics.""" + from syrupy import SnapshotAssertion from homeassistant.core import HomeAssistant diff --git a/tests/components/wiz/test_init.py b/tests/components/wiz/test_init.py index 41871381d3c..d6813263fcc 100644 --- a/tests/components/wiz/test_init.py +++ b/tests/components/wiz/test_init.py @@ -1,4 +1,5 @@ """Tests for wiz integration.""" + import datetime from unittest.mock import AsyncMock, patch @@ -57,9 +58,10 @@ async def test_cleanup_on_failed_first_update(hass: HomeAssistant) -> None: data={CONF_HOST: FAKE_IP}, ) entry.add_to_hass(hass) - with patch( - "homeassistant.components.wiz.discovery.find_wizlights", return_value=[] - ), _patch_wizlight(device=bulb): + with ( + patch("homeassistant.components.wiz.discovery.find_wizlights", return_value=[]), + _patch_wizlight(device=bulb), + ): await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) await hass.async_block_till_done() assert entry.state is config_entries.ConfigEntryState.SETUP_RETRY diff --git a/tests/components/wled/conftest.py b/tests/components/wled/conftest.py index bbbdd4e1cbe..d2f124a517b 100644 --- a/tests/components/wled/conftest.py +++ b/tests/components/wled/conftest.py @@ -1,4 +1,5 @@ """Fixtures for WLED integration tests.""" + from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch @@ -52,10 +53,11 @@ def device_fixture() -> str: @pytest.fixture def mock_wled(device_fixture: str) -> Generator[MagicMock, None, None]: """Return a mocked WLED client.""" - with patch( - "homeassistant.components.wled.coordinator.WLED", autospec=True - ) as wled_mock, patch( - "homeassistant.components.wled.config_flow.WLED", new=wled_mock + with ( + patch( + "homeassistant.components.wled.coordinator.WLED", autospec=True + ) as wled_mock, + patch("homeassistant.components.wled.config_flow.WLED", new=wled_mock), ): wled = wled_mock.return_value wled.update.return_value = WLEDDevice( diff --git a/tests/components/wled/snapshots/test_binary_sensor.ambr b/tests/components/wled/snapshots/test_binary_sensor.ambr index a2d3176cec7..b9a083336d2 100644 --- a/tests/components/wled/snapshots/test_binary_sensor.ambr +++ b/tests/components/wled/snapshots/test_binary_sensor.ambr @@ -8,6 +8,7 @@ 'context': , 'entity_id': 'binary_sensor.wled_rgb_light_firmware', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }) diff --git a/tests/components/wled/snapshots/test_button.ambr b/tests/components/wled/snapshots/test_button.ambr index e004db77e25..b489bcc0a71 100644 --- a/tests/components/wled/snapshots/test_button.ambr +++ b/tests/components/wled/snapshots/test_button.ambr @@ -8,6 +8,7 @@ 'context': , 'entity_id': 'button.wled_rgb_light_restart', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unknown', }) diff --git a/tests/components/wled/snapshots/test_number.ambr b/tests/components/wled/snapshots/test_number.ambr index 97555c7e850..c3440108148 100644 --- a/tests/components/wled/snapshots/test_number.ambr +++ b/tests/components/wled/snapshots/test_number.ambr @@ -11,6 +11,7 @@ 'context': , 'entity_id': 'number.wled_rgb_light_segment_1_intensity', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '64', }) @@ -99,6 +100,7 @@ 'context': , 'entity_id': 'number.wled_rgb_light_segment_1_speed', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '16', }) diff --git a/tests/components/wled/snapshots/test_select.ambr b/tests/components/wled/snapshots/test_select.ambr index 9881b8f0a00..6d64ec43658 100644 --- a/tests/components/wled/snapshots/test_select.ambr +++ b/tests/components/wled/snapshots/test_select.ambr @@ -12,6 +12,7 @@ 'context': , 'entity_id': 'select.wled_rgb_light_live_override', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '0', }) @@ -149,6 +150,7 @@ 'context': , 'entity_id': 'select.wled_rgb_light_segment_1_color_palette', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'Random Cycle', }) @@ -285,6 +287,7 @@ 'context': , 'entity_id': 'select.wled_rgbw_light_playlist', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'Playlist 1', }) @@ -373,6 +376,7 @@ 'context': , 'entity_id': 'select.wled_rgbw_light_preset', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'Preset 1', }) diff --git a/tests/components/wled/snapshots/test_switch.ambr b/tests/components/wled/snapshots/test_switch.ambr index fa2e004f994..da69e686f07 100644 --- a/tests/components/wled/snapshots/test_switch.ambr +++ b/tests/components/wled/snapshots/test_switch.ambr @@ -10,6 +10,7 @@ 'context': , 'entity_id': 'switch.wled_rgb_light_nightlight', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }) @@ -89,6 +90,7 @@ 'context': , 'entity_id': 'switch.wled_rgb_light_reverse', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }) @@ -169,6 +171,7 @@ 'context': , 'entity_id': 'switch.wled_rgb_light_sync_receive', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'on', }) @@ -249,6 +252,7 @@ 'context': , 'entity_id': 'switch.wled_rgb_light_sync_send', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'off', }) diff --git a/tests/components/wled/test_binary_sensor.py b/tests/components/wled/test_binary_sensor.py index eb5faadd530..aa75b0c6696 100644 --- a/tests/components/wled/test_binary_sensor.py +++ b/tests/components/wled/test_binary_sensor.py @@ -1,4 +1,5 @@ """Tests for the WLED binary sensor platform.""" + import pytest from syrupy.assertion import SnapshotAssertion diff --git a/tests/components/wled/test_button.py b/tests/components/wled/test_button.py index 92a13baf43c..ef662fb4ded 100644 --- a/tests/components/wled/test_button.py +++ b/tests/components/wled/test_button.py @@ -1,4 +1,5 @@ """Tests for the WLED button platform.""" + from unittest.mock import MagicMock import pytest diff --git a/tests/components/wled/test_config_flow.py b/tests/components/wled/test_config_flow.py index 949916aaccc..fc2a11c3e46 100644 --- a/tests/components/wled/test_config_flow.py +++ b/tests/components/wled/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for the WLED config flow.""" + from ipaddress import ip_address from unittest.mock import AsyncMock, MagicMock diff --git a/tests/components/wled/test_coordinator.py b/tests/components/wled/test_coordinator.py index 89817fb8569..14e8b620983 100644 --- a/tests/components/wled/test_coordinator.py +++ b/tests/components/wled/test_coordinator.py @@ -1,4 +1,5 @@ """Tests for the coordinator of the WLED integration.""" + import asyncio from collections.abc import Callable from copy import deepcopy diff --git a/tests/components/wled/test_diagnostics.py b/tests/components/wled/test_diagnostics.py index 38e7ebe3e25..494e420ba77 100644 --- a/tests/components/wled/test_diagnostics.py +++ b/tests/components/wled/test_diagnostics.py @@ -1,4 +1,5 @@ """Tests for the diagnostics data provided by the WLED integration.""" + from syrupy.assertion import SnapshotAssertion from homeassistant.core import HomeAssistant diff --git a/tests/components/wled/test_init.py b/tests/components/wled/test_init.py index 38793898243..6f4c47ec201 100644 --- a/tests/components/wled/test_init.py +++ b/tests/components/wled/test_init.py @@ -1,4 +1,5 @@ """Tests for the WLED integration.""" + import asyncio from collections.abc import Callable from unittest.mock import AsyncMock, MagicMock, patch diff --git a/tests/components/wled/test_light.py b/tests/components/wled/test_light.py index fc1d5503c07..2b64619e306 100644 --- a/tests/components/wled/test_light.py +++ b/tests/components/wled/test_light.py @@ -1,4 +1,5 @@ """Tests for the WLED light platform.""" + import json from unittest.mock import MagicMock diff --git a/tests/components/wled/test_number.py b/tests/components/wled/test_number.py index e91ec4f2e66..b692de37282 100644 --- a/tests/components/wled/test_number.py +++ b/tests/components/wled/test_number.py @@ -1,4 +1,5 @@ """Tests for the WLED number platform.""" + import json from unittest.mock import MagicMock diff --git a/tests/components/wled/test_select.py b/tests/components/wled/test_select.py index 219ec945021..380af1a286a 100644 --- a/tests/components/wled/test_select.py +++ b/tests/components/wled/test_select.py @@ -1,4 +1,5 @@ """Tests for the WLED select platform.""" + import json from unittest.mock import MagicMock diff --git a/tests/components/wled/test_sensor.py b/tests/components/wled/test_sensor.py index db68bc2e454..319622e7cb3 100644 --- a/tests/components/wled/test_sensor.py +++ b/tests/components/wled/test_sensor.py @@ -1,4 +1,5 @@ """Tests for the WLED sensor platform.""" + from datetime import datetime from unittest.mock import MagicMock, patch @@ -122,14 +123,14 @@ async def test_sensors( @pytest.mark.parametrize( "entity_id", - ( + [ "sensor.wled_rgb_light_uptime", "sensor.wled_rgb_light_free_memory", "sensor.wled_rgb_light_wi_fi_signal", "sensor.wled_rgb_light_wi_fi_rssi", "sensor.wled_rgb_light_wi_fi_channel", "sensor.wled_rgb_light_wi_fi_bssid", - ), + ], ) @pytest.mark.usefixtures("init_integration") async def test_disabled_by_default_sensors( diff --git a/tests/components/wled/test_switch.py b/tests/components/wled/test_switch.py index 40b7783fc04..6dfd62e363f 100644 --- a/tests/components/wled/test_switch.py +++ b/tests/components/wled/test_switch.py @@ -1,4 +1,5 @@ """Tests for the WLED switch platform.""" + import json from unittest.mock import MagicMock diff --git a/tests/components/wled/test_update.py b/tests/components/wled/test_update.py index d35f0712400..c576cdf16f9 100644 --- a/tests/components/wled/test_update.py +++ b/tests/components/wled/test_update.py @@ -1,4 +1,5 @@ """Tests for the WLED update platform.""" + from unittest.mock import MagicMock import pytest diff --git a/tests/components/wolflink/test_config_flow.py b/tests/components/wolflink/test_config_flow.py index 5be2dbf78f7..bee646deae8 100644 --- a/tests/components/wolflink/test_config_flow.py +++ b/tests/components/wolflink/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Wolf SmartSet Service config flow.""" + from unittest.mock import patch from httpcore import ConnectError @@ -59,10 +60,13 @@ async def test_device_step_form(hass: HomeAssistant) -> None: async def test_create_entry(hass: HomeAssistant) -> None: """Test entity creation from device step.""" - with patch( - "homeassistant.components.wolflink.config_flow.WolfClient.fetch_system_list", - return_value=[DEVICE], - ), patch("homeassistant.components.wolflink.async_setup_entry", return_value=True): + with ( + patch( + "homeassistant.components.wolflink.config_flow.WolfClient.fetch_system_list", + return_value=[DEVICE], + ), + patch("homeassistant.components.wolflink.async_setup_entry", return_value=True), + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=INPUT_CONFIG ) @@ -121,10 +125,13 @@ async def test_form_unknown_exception(hass: HomeAssistant) -> None: async def test_already_configured_error(hass: HomeAssistant) -> None: """Test already configured while creating entry.""" - with patch( - "homeassistant.components.wolflink.config_flow.WolfClient.fetch_system_list", - return_value=[DEVICE], - ), patch("homeassistant.components.wolflink.async_setup_entry", return_value=True): + with ( + patch( + "homeassistant.components.wolflink.config_flow.WolfClient.fetch_system_list", + return_value=[DEVICE], + ), + patch("homeassistant.components.wolflink.async_setup_entry", return_value=True), + ): MockConfigEntry( domain=DOMAIN, unique_id=CONFIG[DEVICE_ID], data=CONFIG ).add_to_hass(hass) diff --git a/tests/components/workday/conftest.py b/tests/components/workday/conftest.py index a9e4dd2ffd0..1f3d9bcaabc 100644 --- a/tests/components/workday/conftest.py +++ b/tests/components/workday/conftest.py @@ -1,4 +1,5 @@ """Fixtures for Workday integration tests.""" + from collections.abc import Generator from unittest.mock import AsyncMock, patch diff --git a/tests/components/workday/test_binary_sensor.py b/tests/components/workday/test_binary_sensor.py index a359d83d87d..a3fba852f60 100644 --- a/tests/components/workday/test_binary_sensor.py +++ b/tests/components/workday/test_binary_sensor.py @@ -1,4 +1,5 @@ """Tests the Home Assistant workday binary sensor.""" + from datetime import date, datetime from typing import Any diff --git a/tests/components/workday/test_config_flow.py b/tests/components/workday/test_config_flow.py index fb0d78365e8..75677143ecb 100644 --- a/tests/components/workday/test_config_flow.py +++ b/tests/components/workday/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Workday config flow.""" + from __future__ import annotations from datetime import datetime diff --git a/tests/components/workday/test_init.py b/tests/components/workday/test_init.py index 047409b5078..e9de315e1d1 100644 --- a/tests/components/workday/test_init.py +++ b/tests/components/workday/test_init.py @@ -1,4 +1,5 @@ """Test Workday component setup process.""" + from __future__ import annotations from datetime import datetime diff --git a/tests/components/worldclock/test_sensor.py b/tests/components/worldclock/test_sensor.py index 3ec7579bf68..00195a49827 100644 --- a/tests/components/worldclock/test_sensor.py +++ b/tests/components/worldclock/test_sensor.py @@ -1,4 +1,5 @@ """The test for the World clock sensor platform.""" + import pytest from homeassistant.core import HomeAssistant diff --git a/tests/components/ws66i/test_config_flow.py b/tests/components/ws66i/test_config_flow.py index aeeedcd89b3..19a329cb913 100644 --- a/tests/components/ws66i/test_config_flow.py +++ b/tests/components/ws66i/test_config_flow.py @@ -1,4 +1,5 @@ """Test the WS66i 6-Zone Amplifier config flow.""" + from unittest.mock import patch from homeassistant import config_entries, data_entry_flow @@ -31,12 +32,15 @@ async def test_form(hass: HomeAssistant) -> None: assert result["type"] == "form" assert result["errors"] == {} - with patch( - "homeassistant.components.ws66i.config_flow.get_ws66i", - ) as mock_ws66i, patch( - "homeassistant.components.ws66i.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.ws66i.config_flow.get_ws66i", + ) as mock_ws66i, + patch( + "homeassistant.components.ws66i.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): ws66i_instance = mock_ws66i.return_value result2 = await hass.config_entries.flow.async_configure( diff --git a/tests/components/ws66i/test_init.py b/tests/components/ws66i/test_init.py index 7110a598e3e..9938ed84303 100644 --- a/tests/components/ws66i/test_init.py +++ b/tests/components/ws66i/test_init.py @@ -1,4 +1,5 @@ """Test the WS66i 6-Zone Amplifier init file.""" + from unittest.mock import patch from homeassistant.components.ws66i.const import DOMAIN diff --git a/tests/components/ws66i/test_media_player.py b/tests/components/ws66i/test_media_player.py index a79ff96cfd7..c13f6cbd738 100644 --- a/tests/components/ws66i/test_media_player.py +++ b/tests/components/ws66i/test_media_player.py @@ -1,4 +1,5 @@ """The tests for WS66i Media player platform.""" + from collections import defaultdict from unittest.mock import patch @@ -85,7 +86,7 @@ class MockWs66i: def open(self): """Open socket. Do nothing.""" if self.fail_open is True: - raise ConnectionError() + raise ConnectionError def close(self): """Close socket. Do nothing.""" @@ -194,7 +195,7 @@ async def test_update(hass: HomeAssistant, freezer: FrozenDateTimeFactory) -> No with patch.object(MockWs66i, "open") as method_call: freezer.tick(POLL_INTERVAL) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert not method_call.called @@ -225,13 +226,13 @@ async def test_failed_update( freezer.tick(POLL_INTERVAL) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) # Failed update, close called with patch.object(MockWs66i, "zone_status", return_value=None): freezer.tick(POLL_INTERVAL) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.is_state(ZONE_1_ID, STATE_UNAVAILABLE) @@ -239,12 +240,12 @@ async def test_failed_update( with patch.object(MockWs66i, "zone_status", return_value=None): freezer.tick(POLL_INTERVAL) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) # A connection re-attempt succeeds freezer.tick(POLL_INTERVAL) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) # confirm entity is back on state = hass.states.get(ZONE_1_ID) @@ -314,7 +315,7 @@ async def test_source_select( freezer.tick(POLL_INTERVAL) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(ZONE_1_ID) @@ -369,14 +370,14 @@ async def test_volume_up_down( ) freezer.tick(POLL_INTERVAL) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) # should not go below zero assert ws66i.zones[11].volume == 0 await _call_media_player_service(hass, SERVICE_VOLUME_UP, {"entity_id": ZONE_1_ID}) freezer.tick(POLL_INTERVAL) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert ws66i.zones[11].volume == 1 await _call_media_player_service( @@ -384,14 +385,14 @@ async def test_volume_up_down( ) freezer.tick(POLL_INTERVAL) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert ws66i.zones[11].volume == MAX_VOL await _call_media_player_service(hass, SERVICE_VOLUME_UP, {"entity_id": ZONE_1_ID}) freezer.tick(POLL_INTERVAL) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) # should not go above 38 (MAX_VOL) assert ws66i.zones[11].volume == MAX_VOL diff --git a/tests/fixtures/wsdot.json b/tests/components/wsdot/fixtures/wsdot.json similarity index 100% rename from tests/fixtures/wsdot.json rename to tests/components/wsdot/fixtures/wsdot.json diff --git a/tests/components/wsdot/test_sensor.py b/tests/components/wsdot/test_sensor.py index 9f3f2472ba6..9f5ec92a5b6 100644 --- a/tests/components/wsdot/test_sensor.py +++ b/tests/components/wsdot/test_sensor.py @@ -1,4 +1,5 @@ """The tests for the WSDOT platform.""" + from datetime import datetime, timedelta, timezone import re @@ -45,11 +46,10 @@ async def test_setup(hass: HomeAssistant, requests_mock: requests_mock.Mocker) - for entity in new_entities: entity.update() - for entity in new_entities: - entities.append(entity) + entities.extend(new_entities) uri = re.compile(RESOURCE + "*") - requests_mock.get(uri, text=load_fixture("wsdot.json")) + requests_mock.get(uri, text=load_fixture("wsdot/wsdot.json")) wsdot.setup_platform(hass, config, add_entities) assert len(entities) == 1 sensor = entities[0] diff --git a/tests/components/wyoming/__init__.py b/tests/components/wyoming/__init__.py index 6b049b04c42..5bfbbfe87b2 100644 --- a/tests/components/wyoming/__init__.py +++ b/tests/components/wyoming/__init__.py @@ -1,4 +1,5 @@ """Tests for the Wyoming integration.""" + import asyncio from unittest.mock import patch @@ -143,12 +144,15 @@ async def reload_satellite( hass: HomeAssistant, config_entry_id: str ) -> SatelliteDevice: """Reload config entry with satellite info and returns new device.""" - with patch( - "homeassistant.components.wyoming.data.load_wyoming_info", - return_value=SATELLITE_INFO, - ), patch( - "homeassistant.components.wyoming.satellite.WyomingSatellite.run" - ) as _run_mock: + with ( + patch( + "homeassistant.components.wyoming.data.load_wyoming_info", + return_value=SATELLITE_INFO, + ), + patch( + "homeassistant.components.wyoming.satellite.WyomingSatellite.run" + ) as _run_mock, + ): # _run_mock: satellite task does not actually run await hass.config_entries.async_reload(config_entry_id) diff --git a/tests/components/wyoming/conftest.py b/tests/components/wyoming/conftest.py index f22ec7e9e16..4be12312c7a 100644 --- a/tests/components/wyoming/conftest.py +++ b/tests/components/wyoming/conftest.py @@ -1,4 +1,5 @@ """Common fixtures for the Wyoming tests.""" + from collections.abc import Generator from unittest.mock import AsyncMock, patch @@ -145,12 +146,15 @@ def satellite_config_entry(hass: HomeAssistant) -> ConfigEntry: @pytest.fixture async def init_satellite(hass: HomeAssistant, satellite_config_entry: ConfigEntry): """Initialize Wyoming satellite.""" - with patch( - "homeassistant.components.wyoming.data.load_wyoming_info", - return_value=SATELLITE_INFO, - ), patch( - "homeassistant.components.wyoming.satellite.WyomingSatellite.run" - ) as _run_mock: + with ( + patch( + "homeassistant.components.wyoming.data.load_wyoming_info", + return_value=SATELLITE_INFO, + ), + patch( + "homeassistant.components.wyoming.satellite.WyomingSatellite.run" + ) as _run_mock, + ): # _run_mock: satellite task does not actually run await hass.config_entries.async_setup(satellite_config_entry.entry_id) diff --git a/tests/components/wyoming/test_binary_sensor.py b/tests/components/wyoming/test_binary_sensor.py index fba181a63ca..8d4e3c72c56 100644 --- a/tests/components/wyoming/test_binary_sensor.py +++ b/tests/components/wyoming/test_binary_sensor.py @@ -1,4 +1,5 @@ """Test Wyoming binary sensor devices.""" + from homeassistant.components.wyoming.devices import SatelliteDevice from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_OFF, STATE_ON diff --git a/tests/components/wyoming/test_config_flow.py b/tests/components/wyoming/test_config_flow.py index f711b56b3bc..c15eb81a1e2 100644 --- a/tests/components/wyoming/test_config_flow.py +++ b/tests/components/wyoming/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Wyoming config flow.""" + from ipaddress import IPv4Address from unittest.mock import AsyncMock, patch @@ -261,10 +262,13 @@ async def test_zeroconf_discovery_no_port( snapshot: SnapshotAssertion, ) -> None: """Test discovery when the zeroconf service does not have a port.""" - with patch( - "homeassistant.components.wyoming.data.load_wyoming_info", - return_value=SATELLITE_INFO, - ), patch.object(ZEROCONF_DISCOVERY, "port", None): + with ( + patch( + "homeassistant.components.wyoming.data.load_wyoming_info", + return_value=SATELLITE_INFO, + ), + patch.object(ZEROCONF_DISCOVERY, "port", None), + ): result = await hass.config_entries.flow.async_init( DOMAIN, data=ZEROCONF_DISCOVERY, diff --git a/tests/components/wyoming/test_devices.py b/tests/components/wyoming/test_devices.py index 0273a7da275..98efb76ab1d 100644 --- a/tests/components/wyoming/test_devices.py +++ b/tests/components/wyoming/test_devices.py @@ -1,4 +1,5 @@ """Test Wyoming devices.""" + from __future__ import annotations from homeassistant.components.assist_pipeline.select import OPTION_PREFERRED diff --git a/tests/components/wyoming/test_init.py b/tests/components/wyoming/test_init.py index 85539f5a164..ad92a317717 100644 --- a/tests/components/wyoming/test_init.py +++ b/tests/components/wyoming/test_init.py @@ -1,4 +1,5 @@ """Test init.""" + from unittest.mock import patch from homeassistant.config_entries import ConfigEntry diff --git a/tests/components/wyoming/test_number.py b/tests/components/wyoming/test_number.py index 084021d61a7..bc6dbc2c677 100644 --- a/tests/components/wyoming/test_number.py +++ b/tests/components/wyoming/test_number.py @@ -1,4 +1,5 @@ """Test Wyoming number.""" + from unittest.mock import patch from homeassistant.components.wyoming.devices import SatelliteDevice diff --git a/tests/components/wyoming/test_satellite.py b/tests/components/wyoming/test_satellite.py index 5cbbfd0a8c3..a9d1e73e153 100644 --- a/tests/components/wyoming/test_satellite.py +++ b/tests/components/wyoming/test_satellite.py @@ -169,7 +169,7 @@ class SatelliteAsyncTcpClient(MockAsyncTcpClient): def inject_event(self, event: Event) -> None: """Put an event in as the next response.""" - self.responses = [event] + self.responses + self.responses = [event, *self.responses] async def test_satellite_pipeline(hass: HomeAssistant) -> None: @@ -185,9 +185,9 @@ async def test_satellite_pipeline(hass: HomeAssistant) -> None: ] pipeline_kwargs: dict[str, Any] = {} - pipeline_event_callback: Callable[ - [assist_pipeline.PipelineEvent], None - ] | None = None + pipeline_event_callback: Callable[[assist_pipeline.PipelineEvent], None] | None = ( + None + ) run_pipeline_called = asyncio.Event() audio_chunk_received = asyncio.Event() @@ -771,9 +771,9 @@ async def test_pipeline_changed(hass: HomeAssistant) -> None: ).event(), ] - pipeline_event_callback: Callable[ - [assist_pipeline.PipelineEvent], None - ] | None = None + pipeline_event_callback: Callable[[assist_pipeline.PipelineEvent], None] | None = ( + None + ) run_pipeline_called = asyncio.Event() pipeline_stopped = asyncio.Event() @@ -845,9 +845,9 @@ async def test_audio_settings_changed(hass: HomeAssistant) -> None: ).event(), ] - pipeline_event_callback: Callable[ - [assist_pipeline.PipelineEvent], None - ] | None = None + pipeline_event_callback: Callable[[assist_pipeline.PipelineEvent], None] | None = ( + None + ) run_pipeline_called = asyncio.Event() pipeline_stopped = asyncio.Event() @@ -981,9 +981,9 @@ async def test_client_stops_pipeline(hass: HomeAssistant) -> None: ).event(), ] - pipeline_event_callback: Callable[ - [assist_pipeline.PipelineEvent], None - ] | None = None + pipeline_event_callback: Callable[[assist_pipeline.PipelineEvent], None] | None = ( + None + ) run_pipeline_called = asyncio.Event() pipeline_stopped = asyncio.Event() diff --git a/tests/components/wyoming/test_select.py b/tests/components/wyoming/test_select.py index 128aab57a1a..e6ec2c4d432 100644 --- a/tests/components/wyoming/test_select.py +++ b/tests/components/wyoming/test_select.py @@ -1,4 +1,5 @@ """Test Wyoming select.""" + from unittest.mock import Mock, patch from homeassistant.components import assist_pipeline diff --git a/tests/components/wyoming/test_stt.py b/tests/components/wyoming/test_stt.py index 1938d44d310..900ee8d544c 100644 --- a/tests/components/wyoming/test_stt.py +++ b/tests/components/wyoming/test_stt.py @@ -1,4 +1,5 @@ """Test stt.""" + from __future__ import annotations from unittest.mock import patch @@ -81,10 +82,13 @@ async def test_streaming_audio_oserror( mock_client = MockAsyncTcpClient([Transcript(text="Hello world").event()]) - with patch( - "homeassistant.components.wyoming.stt.AsyncTcpClient", - mock_client, - ), patch.object(mock_client, "read_event", side_effect=OSError("Boom!")): + with ( + patch( + "homeassistant.components.wyoming.stt.AsyncTcpClient", + mock_client, + ), + patch.object(mock_client, "read_event", side_effect=OSError("Boom!")), + ): result = await entity.async_process_audio_stream(metadata, audio_stream()) assert result.result == stt.SpeechResultState.ERROR diff --git a/tests/components/wyoming/test_switch.py b/tests/components/wyoming/test_switch.py index 6246ba95003..160712bf3de 100644 --- a/tests/components/wyoming/test_switch.py +++ b/tests/components/wyoming/test_switch.py @@ -1,4 +1,5 @@ """Test Wyoming switch devices.""" + from homeassistant.components.wyoming.devices import SatelliteDevice from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_OFF, STATE_ON diff --git a/tests/components/wyoming/test_tts.py b/tests/components/wyoming/test_tts.py index 301074e8ffb..4063418e566 100644 --- a/tests/components/wyoming/test_tts.py +++ b/tests/components/wyoming/test_tts.py @@ -1,4 +1,5 @@ """Test tts.""" + from __future__ import annotations import io @@ -146,10 +147,13 @@ async def test_get_tts_audio_connection_lost( hass: HomeAssistant, init_wyoming_tts ) -> None: """Test streaming audio and losing connection.""" - with patch( - "homeassistant.components.wyoming.tts.AsyncTcpClient", - MockAsyncTcpClient([None]), - ), pytest.raises(HomeAssistantError): + with ( + patch( + "homeassistant.components.wyoming.tts.AsyncTcpClient", + MockAsyncTcpClient([None]), + ), + pytest.raises(HomeAssistantError), + ): await tts.async_get_media_source_audio( hass, tts.generate_media_source_id(hass, "Hello world", "tts.test_tts", "en-US"), @@ -168,13 +172,15 @@ async def test_get_tts_audio_audio_oserror( mock_client = MockAsyncTcpClient(audio_events) - with patch( - "homeassistant.components.wyoming.tts.AsyncTcpClient", - mock_client, - ), patch.object( - mock_client, "read_event", side_effect=OSError("Boom!") - ), pytest.raises( - HomeAssistantError, + with ( + patch( + "homeassistant.components.wyoming.tts.AsyncTcpClient", + mock_client, + ), + patch.object(mock_client, "read_event", side_effect=OSError("Boom!")), + pytest.raises( + HomeAssistantError, + ), ): await tts.async_get_media_source_audio( hass, diff --git a/tests/components/xbox/test_config_flow.py b/tests/components/xbox/test_config_flow.py index 9738dc70148..d4f7a697839 100644 --- a/tests/components/xbox/test_config_flow.py +++ b/tests/components/xbox/test_config_flow.py @@ -1,4 +1,5 @@ """Test the xbox config flow.""" + from http import HTTPStatus from unittest.mock import patch diff --git a/tests/components/xiaomi/test_device_tracker.py b/tests/components/xiaomi/test_device_tracker.py index 9b302160f30..0a576b70bdf 100644 --- a/tests/components/xiaomi/test_device_tracker.py +++ b/tests/components/xiaomi/test_device_tracker.py @@ -1,4 +1,5 @@ """The tests for the Xiaomi router device tracker platform.""" + from http import HTTPStatus import logging from unittest.mock import MagicMock, call, patch diff --git a/tests/components/xiaomi_aqara/test_config_flow.py b/tests/components/xiaomi_aqara/test_config_flow.py index d15a442a840..67991714203 100644 --- a/tests/components/xiaomi_aqara/test_config_flow.py +++ b/tests/components/xiaomi_aqara/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Xiaomi Aqara config flow.""" + from ipaddress import ip_address from socket import gaierror from unittest.mock import Mock, patch @@ -32,14 +33,18 @@ def xiaomi_aqara_fixture(): """Mock xiaomi_aqara discovery and entry setup.""" mock_gateway_discovery = get_mock_discovery([TEST_HOST]) - with patch( - "homeassistant.components.xiaomi_aqara.config_flow.XiaomiGatewayDiscovery", - return_value=mock_gateway_discovery, - ), patch( - "homeassistant.components.xiaomi_aqara.config_flow.XiaomiGateway", - return_value=mock_gateway_discovery.gateways[TEST_HOST], - ), patch( - "homeassistant.components.xiaomi_aqara.async_setup_entry", return_value=True + with ( + patch( + "homeassistant.components.xiaomi_aqara.config_flow.XiaomiGatewayDiscovery", + return_value=mock_gateway_discovery, + ), + patch( + "homeassistant.components.xiaomi_aqara.config_flow.XiaomiGateway", + return_value=mock_gateway_discovery.gateways[TEST_HOST], + ), + patch( + "homeassistant.components.xiaomi_aqara.async_setup_entry", return_value=True + ), ): yield diff --git a/tests/components/xiaomi_ble/__init__.py b/tests/components/xiaomi_ble/__init__.py index 197745b70f1..40bd965fd9d 100644 --- a/tests/components/xiaomi_ble/__init__.py +++ b/tests/components/xiaomi_ble/__init__.py @@ -1,6 +1,5 @@ """Tests for the SensorPush integration.""" - from homeassistant.components.bluetooth import BluetoothServiceInfoBleak from tests.components.bluetooth import generate_advertisement_data, generate_ble_device diff --git a/tests/components/xiaomi_ble/test_binary_sensor.py b/tests/components/xiaomi_ble/test_binary_sensor.py index b9e0b24a3cf..e446a8fb66e 100644 --- a/tests/components/xiaomi_ble/test_binary_sensor.py +++ b/tests/components/xiaomi_ble/test_binary_sensor.py @@ -325,9 +325,12 @@ async def test_unavailable(hass: HomeAssistant) -> None: # Fastforward time without BLE advertisements monotonic_now = start_monotonic + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1 - with patch_bluetooth_time( - monotonic_now, - ), patch_all_discovered_devices([]): + with ( + patch_bluetooth_time( + monotonic_now, + ), + patch_all_discovered_devices([]), + ): async_fire_time_changed( hass, dt_util.utcnow() @@ -377,9 +380,12 @@ async def test_sleepy_device(hass: HomeAssistant) -> None: # Fastforward time without BLE advertisements monotonic_now = start_monotonic + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1 - with patch_bluetooth_time( - monotonic_now, - ), patch_all_discovered_devices([]): + with ( + patch_bluetooth_time( + monotonic_now, + ), + patch_all_discovered_devices([]), + ): async_fire_time_changed( hass, dt_util.utcnow() @@ -429,9 +435,12 @@ async def test_sleepy_device_restore_state(hass: HomeAssistant) -> None: # Fastforward time without BLE advertisements monotonic_now = start_monotonic + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1 - with patch_bluetooth_time( - monotonic_now, - ), patch_all_discovered_devices([]): + with ( + patch_bluetooth_time( + monotonic_now, + ), + patch_all_discovered_devices([]), + ): async_fire_time_changed( hass, dt_util.utcnow() diff --git a/tests/components/xiaomi_ble/test_config_flow.py b/tests/components/xiaomi_ble/test_config_flow.py index e821f5ec779..8b3ff2ef4ab 100644 --- a/tests/components/xiaomi_ble/test_config_flow.py +++ b/tests/components/xiaomi_ble/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Xiaomi config flow.""" + from unittest.mock import patch from xiaomi_ble import XiaomiBluetoothDeviceData as DeviceData @@ -113,12 +114,15 @@ async def test_async_step_bluetooth_valid_device_but_missing_payload_then_full( async def test_async_step_bluetooth_during_onboarding(hass: HomeAssistant) -> None: """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: + 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}, diff --git a/tests/components/xiaomi_ble/test_device_trigger.py b/tests/components/xiaomi_ble/test_device_trigger.py index 28195f784e8..714f061ecd6 100644 --- a/tests/components/xiaomi_ble/test_device_trigger.py +++ b/tests/components/xiaomi_ble/test_device_trigger.py @@ -1,4 +1,5 @@ """Test Xiaomi BLE events.""" + import pytest from homeassistant.components import automation @@ -74,6 +75,58 @@ async def test_event_button_press(hass: HomeAssistant) -> None: await hass.async_block_till_done() +async def test_event_unlock_outside_the_door(hass: HomeAssistant) -> None: + """Make sure that a unlock outside the door event is fired.""" + mac = "D7:1F:44:EB:8A:91" + entry = await _async_setup_xiaomi_device(hass, mac) + events = async_capture_events(hass, "xiaomi_ble_event") + + # Emit button press event + inject_bluetooth_service_info_bleak( + hass, + make_advertisement( + mac, + b"PD\x9e\x06C\x91\x8a\xebD\x1f\xd7\x0b\x00\t" b" \x02\x00\x01\x80|D/a", + ), + ) + + # wait for the event + await hass.async_block_till_done() + assert len(events) == 1 + assert events[0].data["address"] == "D7:1F:44:EB:8A:91" + assert events[0].data["event_type"] == "unlock_outside_the_door" + assert events[0].data["event_properties"] is None + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + +async def test_event_successful_fingerprint_match_the_door(hass: HomeAssistant) -> None: + """Make sure that a successful fingerprint match event is fired.""" + mac = "D7:1F:44:EB:8A:91" + entry = await _async_setup_xiaomi_device(hass, mac) + events = async_capture_events(hass, "xiaomi_ble_event") + + # Emit button press event + inject_bluetooth_service_info_bleak( + hass, + make_advertisement( + mac, + b"PD\x9e\x06B\x91\x8a\xebD\x1f\xd7" b"\x06\x00\x05\xff\xff\xff\xff\x00", + ), + ) + + # wait for the event + await hass.async_block_till_done() + assert len(events) == 1 + assert events[0].data["address"] == "D7:1F:44:EB:8A:91" + assert events[0].data["event_type"] == "match_successful" + assert events[0].data["event_properties"] is None + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + async def test_event_motion_detected(hass: HomeAssistant) -> None: """Make sure that a motion detected event is fired.""" mac = "DE:70:E8:B2:39:0C" @@ -203,6 +256,47 @@ async def test_get_triggers_double_button(hass: HomeAssistant) -> None: await hass.async_block_till_done() +async def test_get_triggers_lock(hass: HomeAssistant) -> None: + """Test that we get the expected triggers from a Xiaomi BLE lock with fingerprint scanner.""" + mac = "98:0C:33:A3:04:3D" + data = {"bindkey": "54d84797cb77f9538b224b305c877d1e"} + entry = await _async_setup_xiaomi_device(hass, mac, data) + events = async_capture_events(hass, "xiaomi_ble_event") + + # Emit unlock inside the door event so it creates the device in the registry + inject_bluetooth_service_info_bleak( + hass, + make_advertisement( + mac, + b"\x48\x55\xc2\x11\x16\x50\x68\xb6\xfe\x3c\x87" + b"\x80\x95\xc8\xa5\x83\x4f\x00\x00\x00\x46\x32\x21\xc6", + ), + ) + + # wait for the event + await hass.async_block_till_done() + assert len(events) == 1 + + dev_reg = async_get_dev_reg(hass) + device = dev_reg.async_get_device(identifiers={get_device_id(mac)}) + assert device + expected_trigger = { + CONF_PLATFORM: "device", + CONF_DOMAIN: DOMAIN, + CONF_DEVICE_ID: device.id, + CONF_TYPE: "fingerprint", + CONF_SUBTYPE: "skin_is_too_dry", + "metadata": {}, + } + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, device.id + ) + assert expected_trigger in triggers + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + async def test_get_triggers_motion(hass: HomeAssistant) -> None: """Test that we get the expected triggers from a Xiaomi BLE motion sensor.""" mac = "DE:70:E8:B2:39:0C" @@ -428,7 +522,7 @@ async def test_if_fires_on_motion_detected(hass: HomeAssistant, calls) -> None: # Creates the device in the registry inject_bluetooth_service_info_bleak( hass, - make_advertisement(mac, b"@0\xdd\x03$\x0A\x10\x01\x64"), + make_advertisement(mac, b"@0\xdd\x03$\x0a\x10\x01\x64"), ) # wait for the device being created diff --git a/tests/components/xiaomi_ble/test_event.py b/tests/components/xiaomi_ble/test_event.py index a0e84c0ac2e..1de5859c35e 100644 --- a/tests/components/xiaomi_ble/test_event.py +++ b/tests/components/xiaomi_ble/test_event.py @@ -1,9 +1,10 @@ """Test the Xiaomi BLE events.""" + import pytest from homeassistant.components.event import ATTR_EVENT_TYPE from homeassistant.components.xiaomi_ble.const import DOMAIN -from homeassistant.const import ATTR_FRIENDLY_NAME, STATE_UNAVAILABLE +from homeassistant.const import ATTR_FRIENDLY_NAME, STATE_ON, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from . import make_advertisement @@ -201,3 +202,94 @@ async def test_events( assert attributes[ATTR_EVENT_TYPE] == meas[ATTR_EVENT_TYPE] assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() + + +async def test_xiaomi_fingerprint(hass: HomeAssistant) -> None: + """Make sure that fingerprint reader events are correctly mapped.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="D7:1F:44:EB:8A:91", + ) + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + + inject_bluetooth_service_info( + hass, + make_advertisement( + "D7:1F:44:EB:8A:91", + b"PD\x9e\x06B\x91\x8a\xebD\x1f\xd7" b"\x06\x00\x05\xff\xff\xff\xff\x00", + ), + ) + + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 3 + + sensor = hass.states.get("sensor.door_lock_8a91_key_id") + sensor_attr = sensor.attributes + assert sensor.state == "unknown operator" + assert sensor_attr[ATTR_FRIENDLY_NAME] == "Door Lock 8A91 Key id" + + binary_sensor = hass.states.get("binary_sensor.door_lock_8a91_fingerprint") + binary_sensor_attribtes = binary_sensor.attributes + assert binary_sensor.state == STATE_ON + assert binary_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Door Lock 8A91 Fingerprint" + + event = hass.states.get("event.door_lock_8a91_fingerprint") + event_attr = event.attributes + assert event_attr[ATTR_FRIENDLY_NAME] == "Door Lock 8A91 Fingerprint" + assert event_attr[ATTR_EVENT_TYPE] == "match_successful" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + +async def test_xiaomi_lock(hass: HomeAssistant) -> None: + """Make sure that lock events are correctly mapped.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="D7:1F:44:EB:8A:91", + ) + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + + inject_bluetooth_service_info( + hass, + make_advertisement( + "D7:1F:44:EB:8A:91", + b"PD\x9e\x06C\x91\x8a\xebD\x1f\xd7\x0b\x00\t" b" \x02\x00\x01\x80|D/a", + ), + ) + + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 4 + + event = hass.states.get("event.door_lock_8a91_lock") + event_attr = event.attributes + assert event_attr[ATTR_FRIENDLY_NAME] == "Door Lock 8A91 Lock" + assert event_attr[ATTR_EVENT_TYPE] == "unlock_outside_the_door" + + sensor = hass.states.get("sensor.door_lock_8a91_lock_method") + sensor_attr = sensor.attributes + assert sensor.state == "biometrics" + assert sensor_attr[ATTR_FRIENDLY_NAME] == "Door Lock 8A91 Lock method" + + sensor = hass.states.get("sensor.door_lock_8a91_key_id") + sensor_attr = sensor.attributes + assert sensor.state == "Fingerprint key id 2" + assert sensor_attr[ATTR_FRIENDLY_NAME] == "Door Lock 8A91 Key id" + + binary_sensor = hass.states.get("binary_sensor.door_lock_8a91_lock") + binary_sensor_attribtes = binary_sensor.attributes + assert binary_sensor.state == STATE_ON + assert binary_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Door Lock 8A91 Lock" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/xiaomi_ble/test_sensor.py b/tests/components/xiaomi_ble/test_sensor.py index ceca08a68ee..4d9a29e3111 100644 --- a/tests/components/xiaomi_ble/test_sensor.py +++ b/tests/components/xiaomi_ble/test_sensor.py @@ -1,4 +1,5 @@ """Test Xiaomi BLE sensors.""" + from datetime import timedelta import time @@ -692,9 +693,12 @@ async def test_unavailable(hass: HomeAssistant) -> None: # Fastforward time without BLE advertisements monotonic_now = start_monotonic + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1 - with patch_bluetooth_time( - monotonic_now, - ), patch_all_discovered_devices([]): + with ( + patch_bluetooth_time( + monotonic_now, + ), + patch_all_discovered_devices([]), + ): async_fire_time_changed( hass, dt_util.utcnow() @@ -738,9 +742,12 @@ async def test_sleepy_device(hass: HomeAssistant) -> None: # Fastforward time without BLE advertisements monotonic_now = start_monotonic + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1 - with patch_bluetooth_time( - monotonic_now, - ), patch_all_discovered_devices([]): + with ( + patch_bluetooth_time( + monotonic_now, + ), + patch_all_discovered_devices([]), + ): async_fire_time_changed( hass, dt_util.utcnow() @@ -786,9 +793,12 @@ async def test_sleepy_device_restore_state(hass: HomeAssistant) -> None: # Fastforward time without BLE advertisements monotonic_now = start_monotonic + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1 - with patch_bluetooth_time( - monotonic_now, - ), patch_all_discovered_devices([]): + with ( + patch_bluetooth_time( + monotonic_now, + ), + patch_all_discovered_devices([]), + ): async_fire_time_changed( hass, dt_util.utcnow() diff --git a/tests/components/xiaomi_miio/__init__.py b/tests/components/xiaomi_miio/__init__.py index 24e66e16b08..ceee4aea744 100644 --- a/tests/components/xiaomi_miio/__init__.py +++ b/tests/components/xiaomi_miio/__init__.py @@ -1,2 +1,3 @@ """Tests for the Xiaomi Miio integration.""" + TEST_MAC = "ab:cd:ef:gh:ij:kl" diff --git a/tests/components/xiaomi_miio/test_button.py b/tests/components/xiaomi_miio/test_button.py index 552b302aafe..8159d7c49e5 100644 --- a/tests/components/xiaomi_miio/test_button.py +++ b/tests/components/xiaomi_miio/test_button.py @@ -1,4 +1,5 @@ """The tests for the xiaomi_miio button component.""" + from unittest.mock import MagicMock, patch import pytest @@ -32,12 +33,15 @@ async def setup_test(hass: HomeAssistant): mock_vacuum = MagicMock() - with patch( - "homeassistant.components.xiaomi_miio.get_platforms", - return_value=[ - Platform.BUTTON, - ], - ), patch("homeassistant.components.xiaomi_miio.RoborockVacuum") as mock_vacuum_cls: + with ( + patch( + "homeassistant.components.xiaomi_miio.get_platforms", + return_value=[ + Platform.BUTTON, + ], + ), + patch("homeassistant.components.xiaomi_miio.RoborockVacuum") as mock_vacuum_cls, + ): mock_vacuum_cls.return_value = mock_vacuum yield mock_vacuum diff --git a/tests/components/xiaomi_miio/test_config_flow.py b/tests/components/xiaomi_miio/test_config_flow.py index b36924764fe..7645f67732e 100644 --- a/tests/components/xiaomi_miio/test_config_flow.py +++ b/tests/components/xiaomi_miio/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Xiaomi Miio config flow.""" + from ipaddress import ip_address from unittest.mock import Mock, patch @@ -72,19 +73,25 @@ def xiaomi_miio_connect_fixture(): """Mock miio connection and entry setup.""" mock_info = get_mock_info() - with patch( - "homeassistant.components.xiaomi_miio.device.Device.info", - return_value=mock_info, - ), patch( - "homeassistant.components.xiaomi_miio.config_flow.MiCloud.login", - return_value=True, - ), patch( - "homeassistant.components.xiaomi_miio.config_flow.MiCloud.get_devices", - return_value=TEST_CLOUD_DEVICES_1, - ), patch( - "homeassistant.components.xiaomi_miio.async_setup_entry", return_value=True - ), patch( - "homeassistant.components.xiaomi_miio.async_unload_entry", return_value=True + with ( + patch( + "homeassistant.components.xiaomi_miio.device.Device.info", + return_value=mock_info, + ), + patch( + "homeassistant.components.xiaomi_miio.config_flow.MiCloud.login", + return_value=True, + ), + patch( + "homeassistant.components.xiaomi_miio.config_flow.MiCloud.get_devices", + return_value=TEST_CLOUD_DEVICES_1, + ), + patch( + "homeassistant.components.xiaomi_miio.async_setup_entry", return_value=True + ), + patch( + "homeassistant.components.xiaomi_miio.async_unload_entry", return_value=True + ), ): yield diff --git a/tests/components/xiaomi_miio/test_select.py b/tests/components/xiaomi_miio/test_select.py index 794fbb090e0..f2f04127d75 100644 --- a/tests/components/xiaomi_miio/test_select.py +++ b/tests/components/xiaomi_miio/test_select.py @@ -46,12 +46,17 @@ async def setup_test(hass: HomeAssistant): mock_airfresh.status().display_orientation = DisplayOrientation.Portrait mock_airfresh.status().ptc_level = PtcLevel.Low - with patch( - "homeassistant.components.xiaomi_miio.get_platforms", - return_value=[ - Platform.SELECT, - ], - ), patch("homeassistant.components.xiaomi_miio.AirFreshT2017") as mock_airfresh_cls: + with ( + patch( + "homeassistant.components.xiaomi_miio.get_platforms", + return_value=[ + Platform.SELECT, + ], + ), + patch( + "homeassistant.components.xiaomi_miio.AirFreshT2017" + ) as mock_airfresh_cls, + ): mock_airfresh_cls.return_value = mock_airfresh yield mock_airfresh diff --git a/tests/components/xiaomi_miio/test_vacuum.py b/tests/components/xiaomi_miio/test_vacuum.py index 9e823035dd9..2cfc3a4f294 100644 --- a/tests/components/xiaomi_miio/test_vacuum.py +++ b/tests/components/xiaomi_miio/test_vacuum.py @@ -1,4 +1,5 @@ """The tests for the Xiaomi vacuum platform.""" + from datetime import datetime, time, timedelta from unittest import mock from unittest.mock import MagicMock, patch @@ -237,7 +238,7 @@ async def test_xiaomi_exceptions(hass: HomeAssistant, mock_mirobo_is_on) -> None mock_mirobo_is_on.status.side_effect = DeviceException("dummy exception") future = dt_util.utcnow() + timedelta(seconds=60) async_fire_time_changed(hass, future) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert not is_available() @@ -246,7 +247,7 @@ async def test_xiaomi_exceptions(hass: HomeAssistant, mock_mirobo_is_on) -> None mock_mirobo_is_on.status.reset_mock() future += timedelta(seconds=60) async_fire_time_changed(hass, future) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert not is_available() assert mock_mirobo_is_on.status.call_count == 1 @@ -371,13 +372,7 @@ async def test_xiaomi_vacuum_services( "velocity": -0.1, }, "manual_control", - mock.call( - **{ - "duration": 1000, - "rotation": -40, - "velocity": -0.1, - } - ), + mock.call(duration=1000, rotation=-40, velocity=-0.1), ), ( SERVICE_STOP_REMOTE_CONTROL, @@ -396,13 +391,7 @@ async def test_xiaomi_vacuum_services( "velocity": 0.1, }, "manual_control_once", - mock.call( - **{ - "duration": 2000, - "rotation": 120, - "velocity": 0.1, - } - ), + mock.call(duration=2000, rotation=120, velocity=0.1), ), ( SERVICE_CLEAN_ZONE, diff --git a/tests/components/yale_smart_alarm/conftest.py b/tests/components/yale_smart_alarm/conftest.py index c3f5fcf74b8..816fc922411 100644 --- a/tests/components/yale_smart_alarm/conftest.py +++ b/tests/components/yale_smart_alarm/conftest.py @@ -1,4 +1,5 @@ """Fixtures for the Yale Smart Living integration.""" + from __future__ import annotations import json diff --git a/tests/components/yale_smart_alarm/test_config_flow.py b/tests/components/yale_smart_alarm/test_config_flow.py index 90c0b78baf5..5eed34a2423 100644 --- a/tests/components/yale_smart_alarm/test_config_flow.py +++ b/tests/components/yale_smart_alarm/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Yale Smart Living config flow.""" + from __future__ import annotations from unittest.mock import patch @@ -23,12 +24,15 @@ async def test_form(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.FORM assert result["errors"] == {} - with patch( - "homeassistant.components.yale_smart_alarm.config_flow.YaleSmartAlarmClient", - ), patch( - "homeassistant.components.yale_smart_alarm.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.yale_smart_alarm.config_flow.YaleSmartAlarmClient", + ), + patch( + "homeassistant.components.yale_smart_alarm.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -84,11 +88,14 @@ async def test_form_invalid_auth( assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": p_error} - with patch( - "homeassistant.components.yale_smart_alarm.config_flow.YaleSmartAlarmClient", - ), patch( - "homeassistant.components.yale_smart_alarm.async_setup_entry", - return_value=True, + with ( + patch( + "homeassistant.components.yale_smart_alarm.config_flow.YaleSmartAlarmClient", + ), + patch( + "homeassistant.components.yale_smart_alarm.async_setup_entry", + return_value=True, + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -138,12 +145,15 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.FORM assert result["errors"] == {} - with patch( - "homeassistant.components.yale_smart_alarm.config_flow.YaleSmartAlarmClient", - ) as mock_yale, patch( - "homeassistant.components.yale_smart_alarm.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.yale_smart_alarm.config_flow.YaleSmartAlarmClient", + ) as mock_yale, + patch( + "homeassistant.components.yale_smart_alarm.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -219,12 +229,15 @@ async def test_reauth_flow_error( assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": p_error} - with patch( - "homeassistant.components.yale_smart_alarm.config_flow.YaleSmartAlarmClient", - return_value="", - ), patch( - "homeassistant.components.yale_smart_alarm.async_setup_entry", - return_value=True, + with ( + patch( + "homeassistant.components.yale_smart_alarm.config_flow.YaleSmartAlarmClient", + return_value="", + ), + patch( + "homeassistant.components.yale_smart_alarm.async_setup_entry", + return_value=True, + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -260,12 +273,15 @@ async def test_options_flow(hass: HomeAssistant) -> None: ) entry.add_to_hass(hass) - with patch( - "homeassistant.components.yale_smart_alarm.config_flow.YaleSmartAlarmClient", - return_value=True, - ), patch( - "homeassistant.components.yale_smart_alarm.async_setup_entry", - return_value=True, + with ( + patch( + "homeassistant.components.yale_smart_alarm.config_flow.YaleSmartAlarmClient", + return_value=True, + ), + patch( + "homeassistant.components.yale_smart_alarm.async_setup_entry", + return_value=True, + ), ): assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/yale_smart_alarm/test_coordinator.py b/tests/components/yale_smart_alarm/test_coordinator.py index 9ee09e9c0f2..6f1125fcf65 100644 --- a/tests/components/yale_smart_alarm/test_coordinator.py +++ b/tests/components/yale_smart_alarm/test_coordinator.py @@ -1,4 +1,5 @@ """The test for the sensibo coordinator.""" + from __future__ import annotations from datetime import timedelta @@ -75,7 +76,7 @@ async def test_coordinator_setup_and_update_errors( client.get_all.side_effect = ConnectionError("Could not connect") async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=1)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) client.get_all.assert_called_once() state = hass.states.get("alarm_control_panel.yale_smart_alarm") assert state.state == STATE_UNAVAILABLE @@ -83,7 +84,7 @@ async def test_coordinator_setup_and_update_errors( client.get_all.side_effect = ConnectionError("Could not connect") async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=2)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) client.get_all.assert_called_once() state = hass.states.get("alarm_control_panel.yale_smart_alarm") assert state.state == STATE_UNAVAILABLE @@ -91,7 +92,7 @@ async def test_coordinator_setup_and_update_errors( client.get_all.side_effect = TimeoutError("Could not connect") async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=3)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) client.get_all.assert_called_once() state = hass.states.get("alarm_control_panel.yale_smart_alarm") assert state.state == STATE_UNAVAILABLE @@ -99,7 +100,7 @@ async def test_coordinator_setup_and_update_errors( client.get_all.side_effect = UnknownError("info") async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=4)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) client.get_all.assert_called_once() state = hass.states.get("alarm_control_panel.yale_smart_alarm") assert state.state == STATE_UNAVAILABLE @@ -109,7 +110,7 @@ async def test_coordinator_setup_and_update_errors( client.get_all.return_value = load_json client.get_armed_status.return_value = YALE_STATE_ARM_FULL async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) client.get_all.assert_called_once() state = hass.states.get("alarm_control_panel.yale_smart_alarm") assert state.state == STATE_ALARM_ARMED_AWAY @@ -117,7 +118,7 @@ async def test_coordinator_setup_and_update_errors( client.get_all.side_effect = AuthenticationError("Can not authenticate") async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=6)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) client.get_all.assert_called_once() state = hass.states.get("alarm_control_panel.yale_smart_alarm") assert state.state == STATE_UNAVAILABLE diff --git a/tests/components/yale_smart_alarm/test_diagnostics.py b/tests/components/yale_smart_alarm/test_diagnostics.py index dc4c5e8c8d7..23fc77fedcc 100644 --- a/tests/components/yale_smart_alarm/test_diagnostics.py +++ b/tests/components/yale_smart_alarm/test_diagnostics.py @@ -1,4 +1,5 @@ """Test Yale Smart Living diagnostics.""" + from __future__ import annotations from unittest.mock import Mock diff --git a/tests/components/yalexs_ble/test_config_flow.py b/tests/components/yalexs_ble/test_config_flow.py index a8ce68a3209..34ffc55ac3f 100644 --- a/tests/components/yalexs_ble/test_config_flow.py +++ b/tests/components/yalexs_ble/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Yale Access Bluetooth config flow.""" + import asyncio from unittest.mock import AsyncMock, MagicMock, Mock, patch @@ -60,12 +61,15 @@ async def test_user_step_success(hass: HomeAssistant, slot: int) -> None: assert result["step_id"] == "user" assert result["errors"] == {} - with patch( - "homeassistant.components.yalexs_ble.config_flow.PushLock.validate", - ), patch( - "homeassistant.components.yalexs_ble.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.yalexs_ble.config_flow.PushLock.validate", + ), + patch( + "homeassistant.components.yalexs_ble.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -174,12 +178,15 @@ async def test_user_step_invalid_keys(hass: HomeAssistant) -> None: assert result4["step_id"] == "user" assert result4["errors"] == {CONF_SLOT: "invalid_key_index"} - with patch( - "homeassistant.components.yalexs_ble.config_flow.PushLock.validate", - ), patch( - "homeassistant.components.yalexs_ble.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.yalexs_ble.config_flow.PushLock.validate", + ), + patch( + "homeassistant.components.yalexs_ble.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result5 = await hass.config_entries.flow.async_configure( result4["flow_id"], { @@ -233,12 +240,15 @@ async def test_user_step_cannot_connect(hass: HomeAssistant) -> None: assert result2["step_id"] == "user" assert result2["errors"] == {"base": "cannot_connect"} - with patch( - "homeassistant.components.yalexs_ble.config_flow.PushLock.validate", - ), patch( - "homeassistant.components.yalexs_ble.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.yalexs_ble.config_flow.PushLock.validate", + ), + patch( + "homeassistant.components.yalexs_ble.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], { @@ -292,12 +302,15 @@ async def test_user_step_auth_exception(hass: HomeAssistant) -> None: assert result2["step_id"] == "user" assert result2["errors"] == {CONF_KEY: "invalid_auth"} - with patch( - "homeassistant.components.yalexs_ble.config_flow.PushLock.validate", - ), patch( - "homeassistant.components.yalexs_ble.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.yalexs_ble.config_flow.PushLock.validate", + ), + patch( + "homeassistant.components.yalexs_ble.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], { @@ -351,12 +364,15 @@ async def test_user_step_unknown_exception(hass: HomeAssistant) -> None: assert result2["step_id"] == "user" assert result2["errors"] == {"base": "unknown"} - with patch( - "homeassistant.components.yalexs_ble.config_flow.PushLock.validate", - ), patch( - "homeassistant.components.yalexs_ble.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.yalexs_ble.config_flow.PushLock.validate", + ), + patch( + "homeassistant.components.yalexs_ble.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], { @@ -390,12 +406,15 @@ async def test_bluetooth_step_success(hass: HomeAssistant) -> None: assert result["step_id"] == "user" assert result["errors"] == {} - with patch( - "homeassistant.components.yalexs_ble.config_flow.PushLock.validate", - ), patch( - "homeassistant.components.yalexs_ble.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.yalexs_ble.config_flow.PushLock.validate", + ), + patch( + "homeassistant.components.yalexs_ble.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -578,13 +597,16 @@ async def test_integration_discovery_updates_key_unique_local_name( ) entry.add_to_hass(hass) - with patch( - "homeassistant.components.yalexs_ble.util.async_discovered_service_info", - return_value=[LOCK_DISCOVERY_INFO_UUID_ADDRESS], - ), patch( - "homeassistant.components.yalexs_ble.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.yalexs_ble.util.async_discovered_service_info", + return_value=[LOCK_DISCOVERY_INFO_UUID_ADDRESS], + ), + patch( + "homeassistant.components.yalexs_ble.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, @@ -852,13 +874,16 @@ async def test_user_is_setting_up_lock_and_discovery_happens_in_the_middle( valdidate_started.set() await user_flow_event.wait() - with patch( - "homeassistant.components.yalexs_ble.config_flow.PushLock.validate", - side_effect=_wait_for_user_flow, - ), patch( - "homeassistant.components.yalexs_ble.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.yalexs_ble.config_flow.PushLock.validate", + side_effect=_wait_for_user_flow, + ), + patch( + "homeassistant.components.yalexs_ble.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): user_flow_task = asyncio.create_task( hass.config_entries.flow.async_configure( result["flow_id"], @@ -945,15 +970,19 @@ async def test_reauth(hass: HomeAssistant) -> None: assert result2["step_id"] == "reauth_validate" assert result2["errors"] == {"base": "no_longer_in_range"} - with patch( - "homeassistant.components.yalexs_ble.config_flow.async_ble_device_from_address", - return_value=YALE_ACCESS_LOCK_DISCOVERY_INFO, - ), patch( - "homeassistant.components.yalexs_ble.config_flow.PushLock.validate", - ), patch( - "homeassistant.components.yalexs_ble.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.yalexs_ble.config_flow.async_ble_device_from_address", + return_value=YALE_ACCESS_LOCK_DISCOVERY_INFO, + ), + patch( + "homeassistant.components.yalexs_ble.config_flow.PushLock.validate", + ), + patch( + "homeassistant.components.yalexs_ble.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], { diff --git a/tests/components/yamaha/test_media_player.py b/tests/components/yamaha/test_media_player.py index 6fc3259a4c0..73885bc8ac7 100644 --- a/tests/components/yamaha/test_media_player.py +++ b/tests/components/yamaha/test_media_player.py @@ -1,4 +1,5 @@ """The tests for the Yamaha Media player platform.""" + from unittest.mock import MagicMock, PropertyMock, call, patch import pytest diff --git a/tests/components/yamaha_musiccast/test_config_flow.py b/tests/components/yamaha_musiccast/test_config_flow.py index 4ce95e418d0..9740bd70a87 100644 --- a/tests/components/yamaha_musiccast/test_config_flow.py +++ b/tests/components/yamaha_musiccast/test_config_flow.py @@ -1,4 +1,5 @@ """Test config flow.""" + from unittest.mock import patch from aiomusiccast import MusicCastConnectionException @@ -17,14 +18,16 @@ from tests.common import MockConfigEntry @pytest.fixture(autouse=True) async def silent_ssdp_scanner(hass): """Start SSDP component and get Scanner, prevent actual SSDP traffic.""" - with patch( - "homeassistant.components.ssdp.Scanner._async_start_ssdp_listeners" - ), patch("homeassistant.components.ssdp.Scanner._async_stop_ssdp_listeners"), patch( - "homeassistant.components.ssdp.Scanner.async_scan" - ), patch( - "homeassistant.components.ssdp.Server._async_start_upnp_servers", - ), patch( - "homeassistant.components.ssdp.Server._async_stop_upnp_servers", + with ( + patch("homeassistant.components.ssdp.Scanner._async_start_ssdp_listeners"), + patch("homeassistant.components.ssdp.Scanner._async_stop_ssdp_listeners"), + patch("homeassistant.components.ssdp.Scanner.async_scan"), + patch( + "homeassistant.components.ssdp.Server._async_start_upnp_servers", + ), + patch( + "homeassistant.components.ssdp.Server._async_stop_upnp_servers", + ), ): yield diff --git a/tests/fixtures/yandex_transport_bus_reply.json b/tests/components/yandex_transport/fixtures/bus_reply.json similarity index 100% rename from tests/fixtures/yandex_transport_bus_reply.json rename to tests/components/yandex_transport/fixtures/bus_reply.json diff --git a/tests/fixtures/yandex_transport_suburban_reply.json b/tests/components/yandex_transport/fixtures/suburban_reply.json similarity index 100% rename from tests/fixtures/yandex_transport_suburban_reply.json rename to tests/components/yandex_transport/fixtures/suburban_reply.json diff --git a/tests/components/yandex_transport/test_yandex_transport_sensor.py b/tests/components/yandex_transport/test_sensor.py similarity index 95% rename from tests/components/yandex_transport/test_yandex_transport_sensor.py rename to tests/components/yandex_transport/test_sensor.py index bf9d0fd92b3..d302ce17a26 100644 --- a/tests/components/yandex_transport/test_yandex_transport_sensor.py +++ b/tests/components/yandex_transport/test_sensor.py @@ -1,4 +1,5 @@ """Tests for the yandex transport platform.""" + import json from unittest.mock import AsyncMock, patch @@ -12,8 +13,10 @@ import homeassistant.util.dt as dt_util from tests.common import assert_setup_component, load_fixture -BUS_REPLY = json.loads(load_fixture("yandex_transport_bus_reply.json")) -SUBURBAN_TRAIN_REPLY = json.loads(load_fixture("yandex_transport_suburban_reply.json")) +BUS_REPLY = json.loads(load_fixture("bus_reply.json", "yandex_transport")) +SUBURBAN_TRAIN_REPLY = json.loads( + load_fixture("suburban_reply.json", "yandex_transport") +) @pytest.fixture diff --git a/tests/components/yandextts/test_tts.py b/tests/components/yandextts/test_tts.py index 79e1f9108c5..6a4b7e11ce6 100644 --- a/tests/components/yandextts/test_tts.py +++ b/tests/components/yandextts/test_tts.py @@ -1,4 +1,5 @@ """The tests for the Yandex SpeechKit speech platform.""" + from http import HTTPStatus import pytest diff --git a/tests/components/yardian/conftest.py b/tests/components/yardian/conftest.py index d4f289c4242..985d2303fdf 100644 --- a/tests/components/yardian/conftest.py +++ b/tests/components/yardian/conftest.py @@ -1,4 +1,5 @@ """Common fixtures for the Yardian tests.""" + from collections.abc import Generator from unittest.mock import AsyncMock, patch diff --git a/tests/components/yardian/test_config_flow.py b/tests/components/yardian/test_config_flow.py index 5f1fcc940cc..c93f48d1c48 100644 --- a/tests/components/yardian/test_config_flow.py +++ b/tests/components/yardian/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Yardian config flow.""" + from unittest.mock import AsyncMock, patch import pytest diff --git a/tests/components/yeelight/__init__.py b/tests/components/yeelight/__init__.py index c7d279220f8..6c940b0b229 100644 --- a/tests/components/yeelight/__init__.py +++ b/tests/components/yeelight/__init__.py @@ -1,4 +1,5 @@ """Tests for the Yeelight integration.""" + from datetime import timedelta from ipaddress import ip_address from unittest.mock import AsyncMock, MagicMock, patch diff --git a/tests/components/yeelight/conftest.py b/tests/components/yeelight/conftest.py index 9a9b9d19ec2..e4ce0afc9bf 100644 --- a/tests/components/yeelight/conftest.py +++ b/tests/components/yeelight/conftest.py @@ -1,4 +1,5 @@ """yeelight conftest.""" + import pytest from tests.components.light.conftest import mock_light_profiles # noqa: F401 diff --git a/tests/components/yeelight/test_binary_sensor.py b/tests/components/yeelight/test_binary_sensor.py index 1410a3a8cdc..5ada7377b08 100644 --- a/tests/components/yeelight/test_binary_sensor.py +++ b/tests/components/yeelight/test_binary_sensor.py @@ -1,4 +1,5 @@ """Test the Yeelight binary sensor.""" + from unittest.mock import patch from homeassistant.components.yeelight import DOMAIN diff --git a/tests/components/yeelight/test_config_flow.py b/tests/components/yeelight/test_config_flow.py index 512ad78fca0..41d60c8652a 100644 --- a/tests/components/yeelight/test_config_flow.py +++ b/tests/components/yeelight/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Yeelight config flow.""" + from ipaddress import ip_address from unittest.mock import patch @@ -88,11 +89,12 @@ async def test_discovery(hass: HomeAssistant) -> None: assert result2["step_id"] == "pick_device" assert not result2["errors"] - with _patch_discovery(), _patch_discovery_interval(), patch( - f"{MODULE}.async_setup", return_value=True - ) as mock_setup, patch( - f"{MODULE}.async_setup_entry", return_value=True - ) as mock_setup_entry: + with ( + _patch_discovery(), + _patch_discovery_interval(), + 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: ID} ) @@ -127,8 +129,11 @@ async def test_discovery_with_existing_device_present(hass: HomeAssistant) -> No alternate_bulb.capabilities["id"] = "0x000000000099999" alternate_bulb.capabilities["location"] = "yeelight://4.4.4.4" - with _patch_discovery(), _patch_discovery_timeout(), _patch_discovery_interval(), patch( - f"{MODULE}.AsyncBulb", return_value=alternate_bulb + with ( + _patch_discovery(), + _patch_discovery_timeout(), + _patch_discovery_interval(), + patch(f"{MODULE}.AsyncBulb", return_value=alternate_bulb), ): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -165,8 +170,10 @@ async def test_discovery_with_existing_device_present(hass: HomeAssistant) -> No assert result2["step_id"] == "pick_device" assert not result2["errors"] - with _patch_discovery(), _patch_discovery_interval(), patch( - f"{MODULE}.AsyncBulb", return_value=_mocked_bulb() + with ( + _patch_discovery(), + _patch_discovery_interval(), + patch(f"{MODULE}.AsyncBulb", return_value=_mocked_bulb()), ): result3 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_DEVICE: ID} @@ -201,9 +208,11 @@ async def test_discovery_no_device(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with _patch_discovery( - no_device=True - ), _patch_discovery_timeout(), _patch_discovery_interval(): + with ( + _patch_discovery(no_device=True), + _patch_discovery_timeout(), + _patch_discovery_interval(), + ): result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) assert result2["type"] == "abort" @@ -223,10 +232,11 @@ async def test_import(hass: HomeAssistant) -> None: # Cannot connect mocked_bulb = _mocked_bulb(cannot_connect=True) - with _patch_discovery( - no_device=True - ), _patch_discovery_timeout(), _patch_discovery_interval(), patch( - f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb + with ( + _patch_discovery(no_device=True), + _patch_discovery_timeout(), + _patch_discovery_interval(), + patch(f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb), ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=config @@ -236,11 +246,12 @@ async def test_import(hass: HomeAssistant) -> None: # Success mocked_bulb = _mocked_bulb() - with _patch_discovery(), patch( - f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb - ), patch(f"{MODULE}.async_setup", return_value=True) as mock_setup, patch( - f"{MODULE}.async_setup_entry", return_value=True - ) as mock_setup_entry: + with ( + _patch_discovery(), + patch(f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb), + patch(f"{MODULE}.async_setup", return_value=True) as mock_setup, + patch(f"{MODULE}.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=config ) @@ -260,8 +271,9 @@ async def test_import(hass: HomeAssistant) -> None: # Duplicate mocked_bulb = _mocked_bulb() - with _patch_discovery(), patch( - f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb + with ( + _patch_discovery(), + patch(f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb), ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=config @@ -281,10 +293,11 @@ async def test_manual(hass: HomeAssistant) -> None: # Cannot connect (timeout) mocked_bulb = _mocked_bulb(cannot_connect=True) - with _patch_discovery( - no_device=True - ), _patch_discovery_timeout(), _patch_discovery_interval(), patch( - f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb + with ( + _patch_discovery(no_device=True), + _patch_discovery_timeout(), + _patch_discovery_interval(), + patch(f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_HOST: IP_ADDRESS} @@ -294,10 +307,11 @@ async def test_manual(hass: HomeAssistant) -> None: assert result2["errors"] == {"base": "cannot_connect"} # Cannot connect (error) - with _patch_discovery( - no_device=True - ), _patch_discovery_timeout(), _patch_discovery_interval(), patch( - f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb + with ( + _patch_discovery(no_device=True), + _patch_discovery_timeout(), + _patch_discovery_interval(), + patch(f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb), ): result3 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_HOST: IP_ADDRESS} @@ -306,10 +320,12 @@ async def test_manual(hass: HomeAssistant) -> None: # Success mocked_bulb = _mocked_bulb() - with _patch_discovery(), _patch_discovery_timeout(), patch( - f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb - ), patch(f"{MODULE}.async_setup", return_value=True), patch( - f"{MODULE}.async_setup_entry", return_value=True + with ( + _patch_discovery(), + _patch_discovery_timeout(), + patch(f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb), + 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} @@ -328,10 +344,11 @@ async def test_manual(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) mocked_bulb = _mocked_bulb() - with _patch_discovery( - no_device=True - ), _patch_discovery_timeout(), _patch_discovery_interval(), patch( - f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb + with ( + _patch_discovery(no_device=True), + _patch_discovery_timeout(), + _patch_discovery_interval(), + patch(f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_HOST: IP_ADDRESS} @@ -435,16 +452,19 @@ async def test_manual_no_capabilities(hass: HomeAssistant) -> None: assert not result["errors"] mocked_bulb = _mocked_bulb() - with _patch_discovery( - no_device=True - ), _patch_discovery_timeout(), _patch_discovery_interval(), patch( - f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb - ), patch( - f"{MODULE}.async_setup", - return_value=True, - ), patch( - f"{MODULE}.async_setup_entry", - return_value=True, + with ( + _patch_discovery(no_device=True), + _patch_discovery_timeout(), + _patch_discovery_interval(), + patch(f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb), + 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} @@ -461,8 +481,10 @@ async def test_discovered_by_homekit_and_dhcp(hass: HomeAssistant) -> None: """Test we get the form with homekit and abort for dhcp source when we get both.""" mocked_bulb = _mocked_bulb() - with _patch_discovery(), _patch_discovery_interval(), patch( - f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb + with ( + _patch_discovery(), + _patch_discovery_interval(), + patch(f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb), ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -481,8 +503,10 @@ async def test_discovered_by_homekit_and_dhcp(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.FORM assert result["errors"] is None - with _patch_discovery(), _patch_discovery_interval(), patch( - f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb + with ( + _patch_discovery(), + _patch_discovery_interval(), + patch(f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb), ): result2 = await hass.config_entries.flow.async_init( DOMAIN, @@ -495,8 +519,10 @@ async def test_discovered_by_homekit_and_dhcp(hass: HomeAssistant) -> None: assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "already_in_progress" - with _patch_discovery(), _patch_discovery_interval(), patch( - f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb + with ( + _patch_discovery(), + _patch_discovery_interval(), + patch(f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb), ): result3 = await hass.config_entries.flow.async_init( DOMAIN, @@ -509,10 +535,11 @@ async def test_discovered_by_homekit_and_dhcp(hass: HomeAssistant) -> None: assert result3["type"] == FlowResultType.ABORT assert result3["reason"] == "already_in_progress" - with _patch_discovery( - no_device=True - ), _patch_discovery_timeout(), _patch_discovery_interval(), patch( - f"{MODULE_CONFIG_FLOW}.AsyncBulb", side_effect=CannotConnect + with ( + _patch_discovery(no_device=True), + _patch_discovery_timeout(), + _patch_discovery_interval(), + patch(f"{MODULE_CONFIG_FLOW}.AsyncBulb", side_effect=CannotConnect), ): result3 = await hass.config_entries.flow.async_init( DOMAIN, @@ -553,8 +580,10 @@ async def test_discovered_by_dhcp_or_homekit(hass: HomeAssistant, source, data) """Test we can setup when discovered from dhcp or homekit.""" mocked_bulb = _mocked_bulb() - with _patch_discovery(), _patch_discovery_interval(), patch( - f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb + with ( + _patch_discovery(), + _patch_discovery_interval(), + patch(f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb), ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": source}, data=data @@ -564,11 +593,14 @@ async def test_discovered_by_dhcp_or_homekit(hass: HomeAssistant, source, data) assert result["type"] == FlowResultType.FORM assert result["errors"] is None - with _patch_discovery(), _patch_discovery_interval(), 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: + with ( + _patch_discovery(), + _patch_discovery_interval(), + 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() @@ -581,10 +613,11 @@ async def test_discovered_by_dhcp_or_homekit(hass: HomeAssistant, source, data) assert mock_async_setup.called assert mock_async_setup_entry.called - with _patch_discovery( - no_device=True - ), _patch_discovery_timeout(), _patch_discovery_interval(), patch( - f"{MODULE_CONFIG_FLOW}.AsyncBulb", side_effect=CannotConnect + with ( + _patch_discovery(no_device=True), + _patch_discovery_timeout(), + _patch_discovery_interval(), + patch(f"{MODULE_CONFIG_FLOW}.AsyncBulb", side_effect=CannotConnect), ): result3 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": source}, data=data @@ -623,10 +656,11 @@ async def test_discovered_by_dhcp_or_homekit_failed_to_get_id( """Test we abort if we cannot get the unique id when discovered from dhcp or homekit.""" mocked_bulb = _mocked_bulb() - with _patch_discovery( - no_device=True - ), _patch_discovery_timeout(), _patch_discovery_interval(), patch( - f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb + with ( + _patch_discovery(no_device=True), + _patch_discovery_timeout(), + _patch_discovery_interval(), + patch(f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb), ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": source}, data=data @@ -639,8 +673,10 @@ async def test_discovered_ssdp(hass: HomeAssistant) -> None: """Test we can setup when discovered from ssdp.""" mocked_bulb = _mocked_bulb() - with _patch_discovery(), _patch_discovery_interval(), patch( - f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb + with ( + _patch_discovery(), + _patch_discovery_interval(), + patch(f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb), ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=SSDP_INFO @@ -650,11 +686,14 @@ async def test_discovered_ssdp(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.FORM assert result["errors"] is None - with _patch_discovery(), _patch_discovery_interval(), 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: + with ( + _patch_discovery(), + _patch_discovery_interval(), + 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() @@ -668,8 +707,10 @@ async def test_discovered_ssdp(hass: HomeAssistant) -> None: assert mock_async_setup_entry.called mocked_bulb = _mocked_bulb() - with _patch_discovery(), _patch_discovery_interval(), patch( - f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb + with ( + _patch_discovery(), + _patch_discovery_interval(), + patch(f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb), ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=SSDP_INFO @@ -684,8 +725,10 @@ async def test_discovered_zeroconf(hass: HomeAssistant) -> None: """Test we can setup when discovered from zeroconf.""" mocked_bulb = _mocked_bulb() - with _patch_discovery(), _patch_discovery_interval(), patch( - f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb + with ( + _patch_discovery(), + _patch_discovery_interval(), + patch(f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb), ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -697,11 +740,14 @@ async def test_discovered_zeroconf(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.FORM assert result["errors"] is None - with _patch_discovery(), _patch_discovery_interval(), 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: + with ( + _patch_discovery(), + _patch_discovery_interval(), + 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() @@ -715,8 +761,10 @@ async def test_discovered_zeroconf(hass: HomeAssistant) -> None: assert mock_async_setup_entry.called mocked_bulb = _mocked_bulb() - with _patch_discovery(), _patch_discovery_interval(), patch( - f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb + with ( + _patch_discovery(), + _patch_discovery_interval(), + patch(f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb), ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -729,8 +777,10 @@ async def test_discovered_zeroconf(hass: HomeAssistant) -> None: assert result["reason"] == "already_configured" mocked_bulb = _mocked_bulb() - with _patch_discovery(), _patch_discovery_interval(), patch( - f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb + with ( + _patch_discovery(), + _patch_discovery_interval(), + patch(f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb), ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -751,8 +801,10 @@ async def test_discovery_updates_ip(hass: HomeAssistant) -> None: config_entry.add_to_hass(hass) mocked_bulb = _mocked_bulb() - with _patch_discovery(), _patch_discovery_interval(), patch( - f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb + with ( + _patch_discovery(), + _patch_discovery_interval(), + patch(f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb), ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -779,10 +831,11 @@ async def test_discovery_updates_ip_no_reload_setup_in_progress( config_entry.add_to_hass(hass) mocked_bulb = _mocked_bulb() - with patch( - f"{MODULE}.async_setup_entry", return_value=True - ) as mock_setup_entry, _patch_discovery(), _patch_discovery_interval(), patch( - f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb + with ( + patch(f"{MODULE}.async_setup_entry", return_value=True) as mock_setup_entry, + _patch_discovery(), + _patch_discovery_interval(), + patch(f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb), ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -803,8 +856,10 @@ async def test_discovery_adds_missing_ip_id_only(hass: HomeAssistant) -> None: config_entry.add_to_hass(hass) mocked_bulb = _mocked_bulb() - with _patch_discovery(), _patch_discovery_interval(), patch( - f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb + with ( + _patch_discovery(), + _patch_discovery_interval(), + patch(f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb), ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -844,13 +899,18 @@ async def test_discovery_adds_missing_ip_id_only(hass: HomeAssistant) -> None: async def test_discovered_during_onboarding(hass: HomeAssistant, source, data) -> None: """Test we create a config entry when discovered during onboarding.""" mocked_bulb = _mocked_bulb() - with _patch_discovery(), _patch_discovery_interval(), patch( - f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb - ), 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, patch( - "homeassistant.components.onboarding.async_is_onboarded", return_value=False - ) as mock_is_onboarded: + with ( + _patch_discovery(), + _patch_discovery_interval(), + patch(f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb), + 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, + patch( + "homeassistant.components.onboarding.async_is_onboarded", return_value=False + ) as mock_is_onboarded, + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": source}, data=data ) diff --git a/tests/components/yeelight/test_init.py b/tests/components/yeelight/test_init.py index 12f1dd8e182..af442d1c8d0 100644 --- a/tests/components/yeelight/test_init.py +++ b/tests/components/yeelight/test_init.py @@ -1,4 +1,5 @@ """Test Yeelight.""" + from datetime import timedelta from unittest.mock import AsyncMock, patch @@ -59,9 +60,10 @@ async def test_ip_changes_fallback_discovery(hass: HomeAssistant) -> None: mocked_fail_bulb = _mocked_bulb(cannot_connect=True) mocked_fail_bulb.bulb_type = BulbType.WhiteTempMood - with patch( - f"{MODULE}.AsyncBulb", return_value=mocked_fail_bulb - ), _patch_discovery(): + with ( + patch(f"{MODULE}.AsyncBulb", return_value=mocked_fail_bulb), + _patch_discovery(), + ): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -161,11 +163,13 @@ async def test_setup_discovery_with_manually_configured_network_adapter( config_entry.add_to_hass(hass) mocked_bulb = _mocked_bulb() - with _patch_discovery(), patch( - f"{MODULE}.AsyncBulb", return_value=mocked_bulb - ), patch( - "homeassistant.components.zeroconf.network.async_get_adapters", - return_value=_ADAPTERS_WITH_MANUAL_CONFIG, + with ( + _patch_discovery(), + patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb), + patch( + "homeassistant.components.zeroconf.network.async_get_adapters", + return_value=_ADAPTERS_WITH_MANUAL_CONFIG, + ), ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -217,11 +221,13 @@ async def test_setup_discovery_with_manually_configured_network_adapter_one_fail config_entry.add_to_hass(hass) mocked_bulb = _mocked_bulb() - with _patch_discovery(), patch( - f"{MODULE}.AsyncBulb", return_value=mocked_bulb - ), patch( - "homeassistant.components.zeroconf.network.async_get_adapters", - return_value=_ADAPTERS_WITH_MANUAL_CONFIG_ONE_FAILING, + with ( + _patch_discovery(), + patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb), + patch( + "homeassistant.components.zeroconf.network.async_get_adapters", + return_value=_ADAPTERS_WITH_MANUAL_CONFIG_ONE_FAILING, + ), ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -338,17 +344,23 @@ async def test_bulb_off_while_adding_in_ha(hass: HomeAssistant) -> None: mocked_bulb = _mocked_bulb(cannot_connect=True) mocked_bulb.bulb_type = BulbType.WhiteTempMood - with patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb), _patch_discovery( - no_device=True - ), _patch_discovery_timeout(), _patch_discovery_interval(): + with ( + patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb), + _patch_discovery(no_device=True), + _patch_discovery_timeout(), + _patch_discovery_interval(), + ): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() assert config_entry.state is ConfigEntryState.SETUP_RETRY - with patch(f"{MODULE}.AsyncBulb", return_value=_mocked_bulb()), _patch_discovery( - no_device=True - ), _patch_discovery_timeout(), _patch_discovery_interval(): + with ( + patch(f"{MODULE}.AsyncBulb", return_value=_mocked_bulb()), + _patch_discovery(no_device=True), + _patch_discovery_timeout(), + _patch_discovery_interval(), + ): async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=2)) await hass.async_block_till_done() @@ -421,8 +433,9 @@ async def test_unload_before_discovery( mocked_bulb = _mocked_bulb(cannot_connect=True) - with _patch_discovery(no_device=True), patch( - f"{MODULE}.AsyncBulb", return_value=mocked_bulb + with ( + _patch_discovery(no_device=True), + patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb), ): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -441,10 +454,11 @@ async def test_async_listen_error_has_host_with_id(hass: HomeAssistant) -> None: ) config_entry.add_to_hass(hass) - with _patch_discovery( - no_device=True - ), _patch_discovery_timeout(), _patch_discovery_interval(), patch( - f"{MODULE}.AsyncBulb", return_value=_mocked_bulb(cannot_connect=True) + with ( + _patch_discovery(no_device=True), + _patch_discovery_timeout(), + _patch_discovery_interval(), + patch(f"{MODULE}.AsyncBulb", return_value=_mocked_bulb(cannot_connect=True)), ): await hass.config_entries.async_setup(config_entry.entry_id) @@ -456,10 +470,11 @@ async def test_async_listen_error_has_host_without_id(hass: HomeAssistant) -> No config_entry = MockConfigEntry(domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}) config_entry.add_to_hass(hass) - with _patch_discovery( - no_device=True - ), _patch_discovery_timeout(), _patch_discovery_interval(), patch( - f"{MODULE}.AsyncBulb", return_value=_mocked_bulb(cannot_connect=True) + with ( + _patch_discovery(no_device=True), + _patch_discovery_timeout(), + _patch_discovery_interval(), + patch(f"{MODULE}.AsyncBulb", return_value=_mocked_bulb(cannot_connect=True)), ): await hass.config_entries.async_setup(config_entry.entry_id) @@ -476,8 +491,11 @@ async def test_async_setup_with_missing_id(hass: HomeAssistant) -> None: ) config_entry.add_to_hass(hass) - with _patch_discovery(), _patch_discovery_timeout(), _patch_discovery_interval(), patch( - f"{MODULE}.AsyncBulb", return_value=_mocked_bulb(cannot_connect=True) + with ( + _patch_discovery(), + _patch_discovery_timeout(), + _patch_discovery_interval(), + patch(f"{MODULE}.AsyncBulb", return_value=_mocked_bulb(cannot_connect=True)), ): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -486,8 +504,11 @@ async def test_async_setup_with_missing_id(hass: HomeAssistant) -> None: async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=2)) await hass.async_block_till_done() - with _patch_discovery(), _patch_discovery_timeout(), _patch_discovery_interval(), patch( - f"{MODULE}.AsyncBulb", return_value=_mocked_bulb() + with ( + _patch_discovery(), + _patch_discovery_timeout(), + _patch_discovery_interval(), + patch(f"{MODULE}.AsyncBulb", return_value=_mocked_bulb()), ): async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=4)) await hass.async_block_till_done() @@ -503,8 +524,11 @@ async def test_async_setup_with_missing_unique_id(hass: HomeAssistant) -> None: ) config_entry.add_to_hass(hass) - with _patch_discovery(), _patch_discovery_timeout(), _patch_discovery_interval(), patch( - f"{MODULE}.AsyncBulb", return_value=_mocked_bulb(cannot_connect=True) + with ( + _patch_discovery(), + _patch_discovery_timeout(), + _patch_discovery_interval(), + patch(f"{MODULE}.AsyncBulb", return_value=_mocked_bulb(cannot_connect=True)), ): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -513,8 +537,11 @@ async def test_async_setup_with_missing_unique_id(hass: HomeAssistant) -> None: async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=2)) await hass.async_block_till_done() - with _patch_discovery(), _patch_discovery_timeout(), _patch_discovery_interval(), patch( - f"{MODULE}.AsyncBulb", return_value=_mocked_bulb() + with ( + _patch_discovery(), + _patch_discovery_timeout(), + _patch_discovery_interval(), + patch(f"{MODULE}.AsyncBulb", return_value=_mocked_bulb()), ): async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=4)) await hass.async_block_till_done() @@ -532,8 +559,11 @@ async def test_connection_dropped_resyncs_properties(hass: HomeAssistant) -> Non config_entry.add_to_hass(hass) mocked_bulb = _mocked_bulb() - with _patch_discovery(), _patch_discovery_timeout(), _patch_discovery_interval(), patch( - f"{MODULE}.AsyncBulb", return_value=mocked_bulb + with ( + _patch_discovery(), + _patch_discovery_timeout(), + _patch_discovery_interval(), + patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb), ): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -565,8 +595,11 @@ async def test_oserror_on_first_update_results_in_unavailable( mocked_bulb = _mocked_bulb() mocked_bulb.async_get_properties = AsyncMock(side_effect=OSError) - with _patch_discovery(), _patch_discovery_timeout(), _patch_discovery_interval(), patch( - f"{MODULE}.AsyncBulb", return_value=mocked_bulb + with ( + _patch_discovery(), + _patch_discovery_timeout(), + _patch_discovery_interval(), + patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb), ): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -592,8 +625,11 @@ async def test_non_oserror_exception_on_first_update( mocked_bulb = _mocked_bulb() mocked_bulb.async_get_properties = AsyncMock(side_effect=exception) - with _patch_discovery(), _patch_discovery_timeout(), _patch_discovery_interval(), patch( - f"{MODULE}.AsyncBulb", return_value=mocked_bulb + with ( + _patch_discovery(), + _patch_discovery_timeout(), + _patch_discovery_interval(), + patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb), ): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -611,10 +647,11 @@ async def test_async_setup_with_discovery_not_working(hass: HomeAssistant) -> No ) config_entry.add_to_hass(hass) - with _patch_discovery( - no_device=True - ), _patch_discovery_timeout(), _patch_discovery_interval(), patch( - f"{MODULE}.AsyncBulb", return_value=_mocked_bulb() + with ( + _patch_discovery(no_device=True), + _patch_discovery_timeout(), + _patch_discovery_interval(), + patch(f"{MODULE}.AsyncBulb", return_value=_mocked_bulb()), ): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -635,8 +672,11 @@ async def test_async_setup_retries_with_wrong_device( ) config_entry.add_to_hass(hass) - with _patch_discovery(), _patch_discovery_timeout(), _patch_discovery_interval(), patch( - f"{MODULE}.AsyncBulb", return_value=_mocked_bulb() + with ( + _patch_discovery(), + _patch_discovery_timeout(), + _patch_discovery_interval(), + patch(f"{MODULE}.AsyncBulb", return_value=_mocked_bulb()), ): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/yeelight/test_light.py b/tests/components/yeelight/test_light.py index e16692de990..ff80c2b55b2 100644 --- a/tests/components/yeelight/test_light.py +++ b/tests/components/yeelight/test_light.py @@ -1,4 +1,5 @@ """Test the Yeelight light.""" + from datetime import timedelta import logging import socket @@ -152,8 +153,10 @@ async def test_services(hass: HomeAssistant, caplog: pytest.LogCaptureFixture) - config_entry.add_to_hass(hass) mocked_bulb = _mocked_bulb() - with _patch_discovery(), _patch_discovery_interval(), patch( - f"{MODULE}.AsyncBulb", return_value=mocked_bulb + with ( + _patch_discovery(), + _patch_discovery_interval(), + patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb), ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -541,8 +544,10 @@ async def test_update_errors( config_entry.add_to_hass(hass) mocked_bulb = _mocked_bulb() - with _patch_discovery(), _patch_discovery_interval(), patch( - f"{MODULE}.AsyncBulb", return_value=mocked_bulb + with ( + _patch_discovery(), + _patch_discovery_interval(), + patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb), ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -589,8 +594,10 @@ async def test_state_already_set_avoid_ratelimit(hass: HomeAssistant) -> None: domain=DOMAIN, data={**CONFIG_ENTRY_DATA, CONF_NIGHTLIGHT_SWITCH: False} ) config_entry.add_to_hass(hass) - with _patch_discovery(), _patch_discovery_interval(), patch( - f"{MODULE}.AsyncBulb", return_value=mocked_bulb + with ( + _patch_discovery(), + _patch_discovery_interval(), + patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb), ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -856,7 +863,6 @@ async def test_device_types( state = hass.states.get(f"{entity_id}_nightlight") assert state.state == "on" nightlight_entity_properties["friendly_name"] = f"{name} Nightlight" - nightlight_entity_properties["icon"] = "mdi:weather-night" nightlight_entity_properties["flowing"] = False nightlight_entity_properties["night_light"] = True nightlight_entity_properties["music_mode"] = False @@ -1407,20 +1413,24 @@ async def test_effects(hass: HomeAssistant) -> None: } }, ) + await hass.async_block_till_done() config_entry = MockConfigEntry(domain=DOMAIN, data=CONFIG_ENTRY_DATA) config_entry.add_to_hass(hass) mocked_bulb = _mocked_bulb() - with _patch_discovery(), _patch_discovery_interval(), patch( - f"{MODULE}.AsyncBulb", return_value=mocked_bulb + with ( + _patch_discovery(), + _patch_discovery_interval(), + patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb), ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert hass.states.get(ENTITY_LIGHT).attributes.get( - "effect_list" - ) == YEELIGHT_COLOR_EFFECT_LIST + ["mock_effect"] + assert hass.states.get(ENTITY_LIGHT).attributes.get("effect_list") == [ + *YEELIGHT_COLOR_EFFECT_LIST, + "mock_effect", + ] async def _async_test_effect(name, target=None, called=True): async_mocked_start_flow = AsyncMock() @@ -1573,8 +1583,9 @@ async def test_ambilight_with_nightlight_disabled(hass: HomeAssistant) -> None: options={**CONFIG_ENTRY_DATA, CONF_NIGHTLIGHT_SWITCH: False}, ) config_entry.add_to_hass(hass) - with _patch_discovery(capabilities=capabilities), patch( - f"{MODULE}.AsyncBulb", return_value=mocked_bulb + with ( + _patch_discovery(capabilities=capabilities), + patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb), ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -1600,8 +1611,10 @@ async def test_state_fails_to_update_triggers_update(hass: HomeAssistant) -> Non domain=DOMAIN, data={**CONFIG_ENTRY_DATA, CONF_NIGHTLIGHT_SWITCH: False} ) config_entry.add_to_hass(hass) - with _patch_discovery(), _patch_discovery_interval(), patch( - f"{MODULE}.AsyncBulb", return_value=mocked_bulb + with ( + _patch_discovery(), + _patch_discovery_interval(), + patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb), ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/yolink/test_config_flow.py b/tests/components/yolink/test_config_flow.py index 2b6034fd597..f7abda0bc4b 100644 --- a/tests/components/yolink/test_config_flow.py +++ b/tests/components/yolink/test_config_flow.py @@ -1,4 +1,5 @@ """Test yolink config flow.""" + from http import HTTPStatus from unittest.mock import patch @@ -86,9 +87,12 @@ async def test_full_flow( }, ) - with patch("homeassistant.components.yolink.api.ConfigEntryAuth"), patch( - "homeassistant.components.yolink.async_setup_entry", return_value=True - ) as mock_setup: + with ( + patch("homeassistant.components.yolink.api.ConfigEntryAuth"), + patch( + "homeassistant.components.yolink.async_setup_entry", return_value=True + ) as mock_setup, + ): result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["data"]["auth_implementation"] == DOMAIN @@ -201,9 +205,12 @@ async def test_reauthentication( }, ) - with patch("homeassistant.components.yolink.api.ConfigEntryAuth"), patch( - "homeassistant.components.yolink.async_setup_entry", return_value=True - ) as mock_setup: + with ( + patch("homeassistant.components.yolink.api.ConfigEntryAuth"), + patch( + "homeassistant.components.yolink.async_setup_entry", return_value=True + ) as mock_setup, + ): result = await hass.config_entries.flow.async_configure(result["flow_id"]) token_data = old_entry.data["token"] assert token_data["access_token"] == "mock-access-token" diff --git a/tests/components/yolink/test_device_trigger.py b/tests/components/yolink/test_device_trigger.py index 0e258d0e1c7..678fe6e35cc 100644 --- a/tests/components/yolink/test_device_trigger.py +++ b/tests/components/yolink/test_device_trigger.py @@ -1,4 +1,5 @@ """The tests for YoLink device triggers.""" + import pytest from pytest_unordered import unordered from yolink.const import ATTR_DEVICE_DIMMER, ATTR_DEVICE_SMART_REMOTER diff --git a/tests/components/youless/test_config_flows.py b/tests/components/youless/test_config_flows.py index 6512103cde0..bc53c55539b 100644 --- a/tests/components/youless/test_config_flows.py +++ b/tests/components/youless/test_config_flows.py @@ -1,4 +1,5 @@ """Test the youless config flow.""" + from unittest.mock import MagicMock, patch from urllib.error import URLError diff --git a/tests/components/youtube/__init__.py b/tests/components/youtube/__init__.py index 665f5f3a762..62808bc7ad9 100644 --- a/tests/components/youtube/__init__.py +++ b/tests/components/youtube/__init__.py @@ -1,4 +1,5 @@ """Tests for the YouTube integration.""" + from collections.abc import AsyncGenerator import json diff --git a/tests/components/youtube/conftest.py b/tests/components/youtube/conftest.py index 8b6ce5d00a2..a90dbba8aaa 100644 --- a/tests/components/youtube/conftest.py +++ b/tests/components/youtube/conftest.py @@ -1,4 +1,5 @@ """Configure tests for the YouTube integration.""" + from collections.abc import Awaitable, Callable, Coroutine import time from typing import Any diff --git a/tests/components/youtube/snapshots/test_sensor.ambr b/tests/components/youtube/snapshots/test_sensor.ambr index e3bfa4ec4bd..cddfa6f6a3d 100644 --- a/tests/components/youtube/snapshots/test_sensor.ambr +++ b/tests/components/youtube/snapshots/test_sensor.ambr @@ -4,13 +4,13 @@ 'attributes': ReadOnlyDict({ 'entity_picture': 'https://i.ytimg.com/vi/wysukDrMdqU/maxresdefault.jpg', 'friendly_name': 'Google for Developers Latest upload', - 'icon': 'mdi:youtube', 'published_at': datetime.datetime(2023, 5, 11, 0, 20, 46, tzinfo=datetime.timezone.utc), 'video_id': 'wysukDrMdqU', }), 'context': , 'entity_id': 'sensor.google_for_developers_latest_upload', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': "What's new in Google Home in less than 1 minute", }) @@ -20,12 +20,12 @@ 'attributes': ReadOnlyDict({ 'entity_picture': 'https://yt3.ggpht.com/fca_HuJ99xUxflWdex0XViC3NfctBFreIl8y4i9z411asnGTWY-Ql3MeH_ybA4kNaOjY7kyA=s800-c-k-c0x00ffffff-no-rj', 'friendly_name': 'Google for Developers Subscribers', - 'icon': 'mdi:youtube-subscription', 'unit_of_measurement': 'subscribers', }), 'context': , 'entity_id': 'sensor.google_for_developers_subscribers', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '2290000', }) @@ -34,11 +34,11 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Google for Developers Latest upload', - 'icon': 'mdi:youtube', }), 'context': , 'entity_id': 'sensor.google_for_developers_latest_upload', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': 'unavailable', }) @@ -48,12 +48,12 @@ 'attributes': ReadOnlyDict({ 'entity_picture': 'https://yt3.ggpht.com/fca_HuJ99xUxflWdex0XViC3NfctBFreIl8y4i9z411asnGTWY-Ql3MeH_ybA4kNaOjY7kyA=s800-c-k-c0x00ffffff-no-rj', 'friendly_name': 'Google for Developers Subscribers', - 'icon': 'mdi:youtube-subscription', 'unit_of_measurement': 'subscribers', }), 'context': , 'entity_id': 'sensor.google_for_developers_subscribers', 'last_changed': , + 'last_reported': , 'last_updated': , 'state': '2290000', }) diff --git a/tests/components/youtube/test_config_flow.py b/tests/components/youtube/test_config_flow.py index c4aacc9603d..c8857626384 100644 --- a/tests/components/youtube/test_config_flow.py +++ b/tests/components/youtube/test_config_flow.py @@ -1,4 +1,5 @@ """Test the YouTube config flow.""" + from unittest.mock import patch import pytest @@ -54,11 +55,14 @@ async def test_full_flow( assert resp.status == 200 assert resp.headers["content-type"] == "text/html; charset=utf-8" - with patch( - "homeassistant.components.youtube.async_setup_entry", return_value=True - ) as mock_setup, patch( - "homeassistant.components.youtube.config_flow.YouTube", - return_value=MockYouTube(), + with ( + patch( + "homeassistant.components.youtube.async_setup_entry", return_value=True + ) as mock_setup, + patch( + "homeassistant.components.youtube.config_flow.YouTube", + return_value=MockYouTube(), + ), ): result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == FlowResultType.FORM @@ -111,10 +115,11 @@ async def test_flow_abort_without_channel( assert resp.headers["content-type"] == "text/html; charset=utf-8" service = MockYouTube(channel_fixture="youtube/get_no_channel.json") - with patch( - "homeassistant.components.youtube.async_setup_entry", return_value=True - ), patch( - "homeassistant.components.youtube.config_flow.YouTube", return_value=service + with ( + patch("homeassistant.components.youtube.async_setup_entry", return_value=True), + patch( + "homeassistant.components.youtube.config_flow.YouTube", return_value=service + ), ): result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == FlowResultType.ABORT @@ -151,10 +156,11 @@ async def test_flow_abort_without_subscriptions( assert resp.headers["content-type"] == "text/html; charset=utf-8" service = MockYouTube(subscriptions_fixture="youtube/get_no_subscriptions.json") - with patch( - "homeassistant.components.youtube.async_setup_entry", return_value=True - ), patch( - "homeassistant.components.youtube.config_flow.YouTube", return_value=service + with ( + patch("homeassistant.components.youtube.async_setup_entry", return_value=True), + patch( + "homeassistant.components.youtube.config_flow.YouTube", return_value=service + ), ): result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == FlowResultType.ABORT @@ -281,11 +287,14 @@ async def test_reauth( ) youtube = MockYouTube(channel_fixture=f"youtube/{fixture}.json") - with patch( - "homeassistant.components.youtube.async_setup_entry", return_value=True - ) as mock_setup, patch( - "homeassistant.components.youtube.config_flow.YouTube", - return_value=youtube, + with ( + patch( + "homeassistant.components.youtube.async_setup_entry", return_value=True + ) as mock_setup, + patch( + "homeassistant.components.youtube.config_flow.YouTube", + return_value=youtube, + ), ): result = await hass.config_entries.flow.async_configure(result["flow_id"]) diff --git a/tests/components/youtube/test_diagnostics.py b/tests/components/youtube/test_diagnostics.py index 4fe16c3a8b6..3a5765b5890 100644 --- a/tests/components/youtube/test_diagnostics.py +++ b/tests/components/youtube/test_diagnostics.py @@ -1,4 +1,5 @@ """Tests for the diagnostics data provided by the YouTube integration.""" + from syrupy import SnapshotAssertion from homeassistant.components.youtube.const import DOMAIN diff --git a/tests/components/youtube/test_init.py b/tests/components/youtube/test_init.py index bd3babdc383..a6c3acbdd3b 100644 --- a/tests/components/youtube/test_init.py +++ b/tests/components/youtube/test_init.py @@ -1,4 +1,5 @@ """Tests for YouTube.""" + import http import time from unittest.mock import patch diff --git a/tests/components/youtube/test_sensor.py b/tests/components/youtube/test_sensor.py index 9f0b63bc062..ae0c38306e4 100644 --- a/tests/components/youtube/test_sensor.py +++ b/tests/components/youtube/test_sensor.py @@ -1,4 +1,5 @@ """Sensor tests for the YouTube integration.""" + from datetime import timedelta from unittest.mock import patch diff --git a/tests/components/zamg/conftest.py b/tests/components/zamg/conftest.py index e3d3d384e85..0598e2adfb4 100644 --- a/tests/components/zamg/conftest.py +++ b/tests/components/zamg/conftest.py @@ -1,4 +1,5 @@ """Fixtures for Zamg integration tests.""" + from collections.abc import Generator import json from unittest.mock import MagicMock, patch diff --git a/tests/components/zamg/test_config_flow.py b/tests/components/zamg/test_config_flow.py index e7df8532e26..bc95afa7936 100644 --- a/tests/components/zamg/test_config_flow.py +++ b/tests/components/zamg/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for the Zamg config flow.""" + from unittest.mock import MagicMock from zamg.exceptions import ZamgApiError diff --git a/tests/components/zamg/test_init.py b/tests/components/zamg/test_init.py index 2d6bc339de5..cda17268478 100644 --- a/tests/components/zamg/test_init.py +++ b/tests/components/zamg/test_init.py @@ -1,4 +1,5 @@ """Test Zamg component init.""" + from unittest.mock import MagicMock import pytest diff --git a/tests/components/zeroconf/test_init.py b/tests/components/zeroconf/test_init.py index c94b2d66465..6a21212ed6e 100644 --- a/tests/components/zeroconf/test_init.py +++ b/tests/components/zeroconf/test_init.py @@ -1,4 +1,5 @@ """Test Zeroconf component setup process.""" + from typing import Any from unittest.mock import call, patch @@ -14,6 +15,7 @@ from zeroconf.asyncio import AsyncServiceInfo from homeassistant.components import zeroconf from homeassistant.const import ( EVENT_COMPONENT_LOADED, + EVENT_HOMEASSISTANT_CLOSE, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP, @@ -158,17 +160,20 @@ async def test_setup(hass: HomeAssistant, mock_async_zeroconf: None) -> None: ], "_Volumio._tcp.local.": [{"domain": "volumio"}], } - with patch.dict( - zc_gen.ZEROCONF, - mock_zc, - clear=True, - ), patch.object( - hass.config_entries.flow, "async_init" - ) as mock_config_flow, patch.object( - zeroconf, "AsyncServiceBrowser", side_effect=service_update_mock - ) as mock_service_browser, patch( - "homeassistant.components.zeroconf.AsyncServiceInfo", - side_effect=get_service_info_mock, + with ( + patch.dict( + zc_gen.ZEROCONF, + mock_zc, + clear=True, + ), + patch.object(hass.config_entries.flow, "async_init") as mock_config_flow, + patch.object( + zeroconf, "AsyncServiceBrowser", side_effect=service_update_mock + ) as mock_service_browser, + patch( + "homeassistant.components.zeroconf.AsyncServiceInfo", + side_effect=get_service_info_mock, + ), ): assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -186,41 +191,43 @@ async def test_setup(hass: HomeAssistant, mock_async_zeroconf: None) -> None: # Test instance is set. assert "zeroconf" in hass.data - assert ( - await hass.components.zeroconf.async_get_async_instance() is mock_async_zeroconf - ) + assert await zeroconf.async_get_async_instance(hass) is mock_async_zeroconf async def test_setup_with_overly_long_url_and_name( hass: HomeAssistant, mock_async_zeroconf: None, caplog: pytest.LogCaptureFixture ) -> None: """Test we still setup with long urls and names.""" - with patch.object(hass.config_entries.flow, "async_init"), patch.object( - zeroconf, "AsyncServiceBrowser", side_effect=service_update_mock - ), patch( - "homeassistant.components.zeroconf.get_url", - return_value=( - "https://this.url.is.way.too.long/very/deep/path/that/will/make/us/go/over" - "/the/maximum/string/length/and/would/cause/zeroconf/to/fail/to/startup" - "/because/the/key/and/value/can/only/be/255/bytes/and/this/string/is/a" - "/bit/longer/than/the/maximum/length/that/we/allow/for/a/value" + with ( + patch.object(hass.config_entries.flow, "async_init"), + patch.object(zeroconf, "AsyncServiceBrowser", side_effect=service_update_mock), + patch( + "homeassistant.components.zeroconf.get_url", + return_value=( + "https://this.url.is.way.too.long/very/deep/path/that/will/make/us/go/over" + "/the/maximum/string/length/and/would/cause/zeroconf/to/fail/to/startup" + "/because/the/key/and/value/can/only/be/255/bytes/and/this/string/is/a" + "/bit/longer/than/the/maximum/length/that/we/allow/for/a/value" + ), ), - ), patch.object( - hass.config, - "location_name", - ( - "\u00dcBER \u00dcber German Umlaut long string long string long string long" - " string long string long string long string long string long string long" - " string long string long string long string long string long string long" - " string long string long string long string long string long string long" - " string long string long string long string long string long string long" - " string long string long string long string long string long string long" - " string long string long string long string long string long string long" - " string long string long string long string long string long string long" - " string long string long string long string long string" + patch.object( + hass.config, + "location_name", + ( + "\u00dcBER \u00dcber German Umlaut long string long string long string long" + " string long string long string long string long string long string long" + " string long string long string long string long string long string long" + " string long string long string long string long string long string long" + " string long string long string long string long string long string long" + " string long string long string long string long string long string long" + " string long string long string long string long string long string long" + " string long string long string long string long string long string long" + " string long string long string long string long string" + ), + ), + patch( + "homeassistant.components.zeroconf.AsyncServiceInfo.async_request", ), - ), patch( - "homeassistant.components.zeroconf.AsyncServiceInfo.async_request", ): assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) hass.bus.async_fire(EVENT_HOMEASSISTANT_START) @@ -234,11 +241,13 @@ async def test_setup_with_defaults( hass: HomeAssistant, mock_zeroconf: None, mock_async_zeroconf: None ) -> None: """Test default interface config.""" - with patch.object(hass.config_entries.flow, "async_init"), patch.object( - zeroconf, "AsyncServiceBrowser", side_effect=service_update_mock - ), patch( - "homeassistant.components.zeroconf.AsyncServiceInfo", - side_effect=get_service_info_mock, + with ( + patch.object(hass.config_entries.flow, "async_init"), + patch.object(zeroconf, "AsyncServiceBrowser", side_effect=service_update_mock), + patch( + "homeassistant.components.zeroconf.AsyncServiceInfo", + side_effect=get_service_info_mock, + ), ): assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -263,25 +272,28 @@ async def test_zeroconf_match_macaddress( ServiceStateChange.Added, ) - with patch.dict( - zc_gen.ZEROCONF, - { - "_http._tcp.local.": [ - { - "domain": "shelly", - "name": "shelly*", - "properties": {"macaddress": "ffaadd*"}, - } - ] - }, - clear=True, - ), patch.object( - hass.config_entries.flow, "async_init" - ) as mock_config_flow, patch.object( - zeroconf, "AsyncServiceBrowser", side_effect=http_only_service_update_mock - ) as mock_service_browser, patch( - "homeassistant.components.zeroconf.AsyncServiceInfo", - side_effect=get_zeroconf_info_mock("FFAADDCC11DD"), + with ( + patch.dict( + zc_gen.ZEROCONF, + { + "_http._tcp.local.": [ + { + "domain": "shelly", + "name": "shelly*", + "properties": {"macaddress": "ffaadd*"}, + } + ] + }, + clear=True, + ), + patch.object(hass.config_entries.flow, "async_init") as mock_config_flow, + patch.object( + zeroconf, "AsyncServiceBrowser", side_effect=http_only_service_update_mock + ) as mock_service_browser, + patch( + "homeassistant.components.zeroconf.AsyncServiceInfo", + side_effect=get_zeroconf_info_mock("FFAADDCC11DD"), + ), ): assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -307,21 +319,24 @@ async def test_zeroconf_match_manufacturer( ServiceStateChange.Added, ) - with patch.dict( - zc_gen.ZEROCONF, - { - "_airplay._tcp.local.": [ - {"domain": "samsungtv", "properties": {"manufacturer": "samsung*"}} - ] - }, - clear=True, - ), patch.object( - hass.config_entries.flow, "async_init" - ) as mock_config_flow, patch.object( - zeroconf, "AsyncServiceBrowser", side_effect=http_only_service_update_mock - ) as mock_service_browser, patch( - "homeassistant.components.zeroconf.AsyncServiceInfo", - side_effect=get_zeroconf_info_mock_manufacturer("Samsung Electronics"), + with ( + patch.dict( + zc_gen.ZEROCONF, + { + "_airplay._tcp.local.": [ + {"domain": "samsungtv", "properties": {"manufacturer": "samsung*"}} + ] + }, + clear=True, + ), + patch.object(hass.config_entries.flow, "async_init") as mock_config_flow, + patch.object( + zeroconf, "AsyncServiceBrowser", side_effect=http_only_service_update_mock + ) as mock_service_browser, + patch( + "homeassistant.components.zeroconf.AsyncServiceInfo", + side_effect=get_zeroconf_info_mock_manufacturer("Samsung Electronics"), + ), ): assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -346,21 +361,24 @@ async def test_zeroconf_match_model( ServiceStateChange.Added, ) - with patch.dict( - zc_gen.ZEROCONF, - { - "_airplay._tcp.local.": [ - {"domain": "appletv", "properties": {"model": "appletv*"}} - ] - }, - clear=True, - ), patch.object( - hass.config_entries.flow, "async_init" - ) as mock_config_flow, patch.object( - zeroconf, "AsyncServiceBrowser", side_effect=http_only_service_update_mock - ) as mock_service_browser, patch( - "homeassistant.components.zeroconf.AsyncServiceInfo", - side_effect=get_zeroconf_info_mock_model("appletv"), + with ( + patch.dict( + zc_gen.ZEROCONF, + { + "_airplay._tcp.local.": [ + {"domain": "appletv", "properties": {"model": "appletv*"}} + ] + }, + clear=True, + ), + patch.object(hass.config_entries.flow, "async_init") as mock_config_flow, + patch.object( + zeroconf, "AsyncServiceBrowser", side_effect=http_only_service_update_mock + ) as mock_service_browser, + patch( + "homeassistant.components.zeroconf.AsyncServiceInfo", + side_effect=get_zeroconf_info_mock_model("appletv"), + ), ): assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -385,21 +403,24 @@ async def test_zeroconf_match_manufacturer_not_present( ServiceStateChange.Added, ) - with patch.dict( - zc_gen.ZEROCONF, - { - "_airplay._tcp.local.": [ - {"domain": "samsungtv", "properties": {"manufacturer": "samsung*"}} - ] - }, - clear=True, - ), patch.object( - hass.config_entries.flow, "async_init" - ) as mock_config_flow, patch.object( - zeroconf, "AsyncServiceBrowser", side_effect=http_only_service_update_mock - ) as mock_service_browser, patch( - "homeassistant.components.zeroconf.AsyncServiceInfo", - side_effect=get_zeroconf_info_mock("aabbccddeeff"), + with ( + patch.dict( + zc_gen.ZEROCONF, + { + "_airplay._tcp.local.": [ + {"domain": "samsungtv", "properties": {"manufacturer": "samsung*"}} + ] + }, + clear=True, + ), + patch.object(hass.config_entries.flow, "async_init") as mock_config_flow, + patch.object( + zeroconf, "AsyncServiceBrowser", side_effect=http_only_service_update_mock + ) as mock_service_browser, + patch( + "homeassistant.components.zeroconf.AsyncServiceInfo", + side_effect=get_zeroconf_info_mock("aabbccddeeff"), + ), ): assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -423,17 +444,20 @@ async def test_zeroconf_no_match( ServiceStateChange.Added, ) - with patch.dict( - zc_gen.ZEROCONF, - {"_http._tcp.local.": [{"domain": "shelly", "name": "shelly*"}]}, - clear=True, - ), patch.object( - hass.config_entries.flow, "async_init" - ) as mock_config_flow, patch.object( - zeroconf, "AsyncServiceBrowser", side_effect=http_only_service_update_mock - ) as mock_service_browser, patch( - "homeassistant.components.zeroconf.AsyncServiceInfo", - side_effect=get_zeroconf_info_mock("FFAADDCC11DD"), + with ( + patch.dict( + zc_gen.ZEROCONF, + {"_http._tcp.local.": [{"domain": "shelly", "name": "shelly*"}]}, + clear=True, + ), + patch.object(hass.config_entries.flow, "async_init") as mock_config_flow, + patch.object( + zeroconf, "AsyncServiceBrowser", side_effect=http_only_service_update_mock + ) as mock_service_browser, + patch( + "homeassistant.components.zeroconf.AsyncServiceInfo", + side_effect=get_zeroconf_info_mock("FFAADDCC11DD"), + ), ): assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -457,21 +481,24 @@ async def test_zeroconf_no_match_manufacturer( ServiceStateChange.Added, ) - with patch.dict( - zc_gen.ZEROCONF, - { - "_airplay._tcp.local.": [ - {"domain": "samsungtv", "properties": {"manufacturer": "samsung*"}} - ] - }, - clear=True, - ), patch.object( - hass.config_entries.flow, "async_init" - ) as mock_config_flow, patch.object( - zeroconf, "AsyncServiceBrowser", side_effect=http_only_service_update_mock - ) as mock_service_browser, patch( - "homeassistant.components.zeroconf.AsyncServiceInfo", - side_effect=get_zeroconf_info_mock_manufacturer("Not Samsung Electronics"), + with ( + patch.dict( + zc_gen.ZEROCONF, + { + "_airplay._tcp.local.": [ + {"domain": "samsungtv", "properties": {"manufacturer": "samsung*"}} + ] + }, + clear=True, + ), + patch.object(hass.config_entries.flow, "async_init") as mock_config_flow, + patch.object( + zeroconf, "AsyncServiceBrowser", side_effect=http_only_service_update_mock + ) as mock_service_browser, + patch( + "homeassistant.components.zeroconf.AsyncServiceInfo", + side_effect=get_zeroconf_info_mock_manufacturer("Not Samsung Electronics"), + ), ): assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -485,25 +512,29 @@ async def test_homekit_match_partial_space( hass: HomeAssistant, mock_async_zeroconf: None ) -> None: """Test configured options for a device are loaded via config entry.""" - with patch.dict( - zc_gen.ZEROCONF, - {"_hap._tcp.local.": [{"domain": "homekit_controller"}]}, - clear=True, - ), patch.dict( - zc_gen.HOMEKIT, - {"LIFX": {"domain": "lifx", "always_discover": True}}, - clear=True, - ), patch.object( - hass.config_entries.flow, "async_init" - ) as mock_config_flow, patch.object( - zeroconf, - "AsyncServiceBrowser", - side_effect=lambda *args, **kwargs: service_update_mock( - *args, **kwargs, limit_service="_hap._tcp.local." + with ( + patch.dict( + zc_gen.ZEROCONF, + {"_hap._tcp.local.": [{"domain": "homekit_controller"}]}, + clear=True, + ), + patch.dict( + zc_gen.HOMEKIT, + {"LIFX": {"domain": "lifx", "always_discover": True}}, + clear=True, + ), + patch.object(hass.config_entries.flow, "async_init") as mock_config_flow, + patch.object( + zeroconf, + "AsyncServiceBrowser", + side_effect=lambda *args, **kwargs: service_update_mock( + *args, **kwargs, limit_service="_hap._tcp.local." + ), + ) as mock_service_browser, + patch( + "homeassistant.components.zeroconf.AsyncServiceInfo", + side_effect=get_homekit_info_mock("LIFX bulb", HOMEKIT_STATUS_UNPAIRED), ), - ) as mock_service_browser, patch( - "homeassistant.components.zeroconf.AsyncServiceInfo", - side_effect=get_homekit_info_mock("LIFX bulb", HOMEKIT_STATUS_UNPAIRED), ): assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -523,25 +554,29 @@ async def test_device_with_invalid_name( hass: HomeAssistant, mock_async_zeroconf: None, caplog: pytest.LogCaptureFixture ) -> None: """Test we ignore devices with an invalid name.""" - with patch.dict( - zc_gen.ZEROCONF, - {"_hap._tcp.local.": [{"domain": "homekit_controller"}]}, - clear=True, - ), patch.dict( - zc_gen.HOMEKIT, - {"LIFX": {"domain": "lifx", "always_discover": True}}, - clear=True, - ), patch.object( - hass.config_entries.flow, "async_init" - ) as mock_config_flow, patch.object( - zeroconf, - "AsyncServiceBrowser", - side_effect=lambda *args, **kwargs: service_update_mock( - *args, **kwargs, limit_service="_hap._tcp.local." + with ( + patch.dict( + zc_gen.ZEROCONF, + {"_hap._tcp.local.": [{"domain": "homekit_controller"}]}, + clear=True, + ), + patch.dict( + zc_gen.HOMEKIT, + {"LIFX": {"domain": "lifx", "always_discover": True}}, + clear=True, + ), + patch.object(hass.config_entries.flow, "async_init") as mock_config_flow, + patch.object( + zeroconf, + "AsyncServiceBrowser", + side_effect=lambda *args, **kwargs: service_update_mock( + *args, **kwargs, limit_service="_hap._tcp.local." + ), + ) as mock_service_browser, + patch( + "homeassistant.components.zeroconf.AsyncServiceInfo", + side_effect=BadTypeInNameException, ), - ) as mock_service_browser, patch( - "homeassistant.components.zeroconf.AsyncServiceInfo", - side_effect=BadTypeInNameException, ): assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -556,25 +591,31 @@ async def test_homekit_match_partial_dash( hass: HomeAssistant, mock_async_zeroconf: None ) -> None: """Test configured options for a device are loaded via config entry.""" - with patch.dict( - zc_gen.ZEROCONF, - {"_hap._udp.local.": [{"domain": "homekit_controller"}]}, - clear=True, - ), patch.dict( - zc_gen.HOMEKIT, - {"Smart Bridge": {"domain": "lutron_caseta", "always_discover": False}}, - clear=True, - ), patch.object( - hass.config_entries.flow, "async_init" - ) as mock_config_flow, patch.object( - zeroconf, - "AsyncServiceBrowser", - side_effect=lambda *args, **kwargs: service_update_mock( - *args, **kwargs, limit_service="_hap._udp.local." + with ( + patch.dict( + zc_gen.ZEROCONF, + {"_hap._udp.local.": [{"domain": "homekit_controller"}]}, + clear=True, + ), + patch.dict( + zc_gen.HOMEKIT, + {"Smart Bridge": {"domain": "lutron_caseta", "always_discover": False}}, + clear=True, + ), + patch.object(hass.config_entries.flow, "async_init") as mock_config_flow, + patch.object( + zeroconf, + "AsyncServiceBrowser", + 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( + "Smart Bridge-001", HOMEKIT_STATUS_UNPAIRED + ), ), - ) as mock_service_browser, patch( - "homeassistant.components.zeroconf.AsyncServiceInfo", - side_effect=get_homekit_info_mock("Smart Bridge-001", HOMEKIT_STATUS_UNPAIRED), ): assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -589,25 +630,29 @@ async def test_homekit_match_partial_fnmatch( hass: HomeAssistant, mock_async_zeroconf: None ) -> None: """Test matching homekit devices with fnmatch.""" - with patch.dict( - zc_gen.ZEROCONF, - {"_hap._tcp.local.": [{"domain": "homekit_controller"}]}, - clear=True, - ), patch.dict( - zc_gen.HOMEKIT, - {"YLDP*": {"domain": "yeelight", "always_discover": False}}, - clear=True, - ), patch.object( - hass.config_entries.flow, "async_init" - ) as mock_config_flow, patch.object( - zeroconf, - "AsyncServiceBrowser", - side_effect=lambda *args, **kwargs: service_update_mock( - *args, **kwargs, limit_service="_hap._tcp.local." + with ( + patch.dict( + zc_gen.ZEROCONF, + {"_hap._tcp.local.": [{"domain": "homekit_controller"}]}, + clear=True, + ), + patch.dict( + zc_gen.HOMEKIT, + {"YLDP*": {"domain": "yeelight", "always_discover": False}}, + clear=True, + ), + patch.object(hass.config_entries.flow, "async_init") as mock_config_flow, + patch.object( + zeroconf, + "AsyncServiceBrowser", + side_effect=lambda *args, **kwargs: service_update_mock( + *args, **kwargs, limit_service="_hap._tcp.local." + ), + ) as mock_service_browser, + patch( + "homeassistant.components.zeroconf.AsyncServiceInfo", + side_effect=get_homekit_info_mock("YLDP13YL", HOMEKIT_STATUS_UNPAIRED), ), - ) as mock_service_browser, patch( - "homeassistant.components.zeroconf.AsyncServiceInfo", - side_effect=get_homekit_info_mock("YLDP13YL", HOMEKIT_STATUS_UNPAIRED), ): assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -622,25 +667,29 @@ async def test_homekit_match_full( hass: HomeAssistant, mock_async_zeroconf: None ) -> None: """Test configured options for a device are loaded via config entry.""" - with patch.dict( - zc_gen.ZEROCONF, - {"_hap._udp.local.": [{"domain": "homekit_controller"}]}, - clear=True, - ), patch.dict( - zc_gen.HOMEKIT, - {"BSB002": {"domain": "hue", "always_discover": False}}, - clear=True, - ), patch.object( - hass.config_entries.flow, "async_init" - ) as mock_config_flow, patch.object( - zeroconf, - "AsyncServiceBrowser", - side_effect=lambda *args, **kwargs: service_update_mock( - *args, **kwargs, limit_service="_hap._udp.local." + with ( + patch.dict( + zc_gen.ZEROCONF, + {"_hap._udp.local.": [{"domain": "homekit_controller"}]}, + clear=True, + ), + patch.dict( + zc_gen.HOMEKIT, + {"BSB002": {"domain": "hue", "always_discover": False}}, + clear=True, + ), + patch.object(hass.config_entries.flow, "async_init") as mock_config_flow, + patch.object( + zeroconf, + "AsyncServiceBrowser", + 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("BSB002", HOMEKIT_STATUS_UNPAIRED), ), - ) as mock_service_browser, patch( - "homeassistant.components.zeroconf.AsyncServiceInfo", - side_effect=get_homekit_info_mock("BSB002", HOMEKIT_STATUS_UNPAIRED), ): assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -655,28 +704,32 @@ async def test_homekit_already_paired( hass: HomeAssistant, mock_async_zeroconf: None ) -> None: """Test that an already paired device is sent to homekit_controller.""" - with patch.dict( - zc_gen.ZEROCONF, - {"_hap._tcp.local.": [{"domain": "homekit_controller"}]}, - clear=True, - ), patch.dict( - zc_gen.HOMEKIT, - { - "AC02": {"domain": "tado", "always_discover": True}, - "tado": {"domain": "tado", "always_discover": True}, - }, - clear=True, - ), patch.object( - hass.config_entries.flow, "async_init" - ) as mock_config_flow, patch.object( - zeroconf, - "AsyncServiceBrowser", - side_effect=lambda *args, **kwargs: service_update_mock( - *args, **kwargs, limit_service="_hap._tcp.local." + with ( + patch.dict( + zc_gen.ZEROCONF, + {"_hap._tcp.local.": [{"domain": "homekit_controller"}]}, + clear=True, + ), + patch.dict( + zc_gen.HOMEKIT, + { + "AC02": {"domain": "tado", "always_discover": True}, + "tado": {"domain": "tado", "always_discover": True}, + }, + clear=True, + ), + patch.object(hass.config_entries.flow, "async_init") as mock_config_flow, + patch.object( + zeroconf, + "AsyncServiceBrowser", + side_effect=lambda *args, **kwargs: service_update_mock( + *args, **kwargs, limit_service="_hap._tcp.local." + ), + ) as mock_service_browser, + patch( + "homeassistant.components.zeroconf.AsyncServiceInfo", + side_effect=get_homekit_info_mock("tado", HOMEKIT_STATUS_PAIRED), ), - ) as mock_service_browser, patch( - "homeassistant.components.zeroconf.AsyncServiceInfo", - side_effect=get_homekit_info_mock("tado", HOMEKIT_STATUS_PAIRED), ): assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -692,25 +745,29 @@ async def test_homekit_invalid_paring_status( hass: HomeAssistant, mock_async_zeroconf: None ) -> None: """Test that missing paring data is not sent to homekit_controller.""" - with patch.dict( - zc_gen.ZEROCONF, - {"_hap._tcp.local.": [{"domain": "homekit_controller"}]}, - clear=True, - ), patch.dict( - zc_gen.HOMEKIT, - {"Smart Bridge": {"domain": "lutron_caseta", "always_discover": False}}, - clear=True, - ), patch.object( - hass.config_entries.flow, "async_init" - ) as mock_config_flow, patch.object( - zeroconf, - "AsyncServiceBrowser", - side_effect=lambda *args, **kwargs: service_update_mock( - *args, **kwargs, limit_service="_hap._tcp.local." + with ( + patch.dict( + zc_gen.ZEROCONF, + {"_hap._tcp.local.": [{"domain": "homekit_controller"}]}, + clear=True, + ), + patch.dict( + zc_gen.HOMEKIT, + {"Smart Bridge": {"domain": "lutron_caseta", "always_discover": False}}, + clear=True, + ), + patch.object(hass.config_entries.flow, "async_init") as mock_config_flow, + patch.object( + zeroconf, + "AsyncServiceBrowser", + side_effect=lambda *args, **kwargs: service_update_mock( + *args, **kwargs, limit_service="_hap._tcp.local." + ), + ) as mock_service_browser, + patch( + "homeassistant.components.zeroconf.AsyncServiceInfo", + side_effect=get_homekit_info_mock("Smart Bridge", b"invalid"), ), - ) as mock_service_browser, patch( - "homeassistant.components.zeroconf.AsyncServiceInfo", - side_effect=get_homekit_info_mock("Smart Bridge", b"invalid"), ): assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -725,18 +782,21 @@ async def test_homekit_not_paired( hass: HomeAssistant, mock_async_zeroconf: None ) -> None: """Test that an not paired device is sent to homekit_controller.""" - with patch.dict( - zc_gen.ZEROCONF, - {"_hap._tcp.local.": [{"domain": "homekit_controller"}]}, - clear=True, - ), patch.object( - hass.config_entries.flow, "async_init" - ) as mock_config_flow, patch.object( - zeroconf, "AsyncServiceBrowser", side_effect=service_update_mock - ) as mock_service_browser, patch( - "homeassistant.components.zeroconf.AsyncServiceInfo", - side_effect=get_homekit_info_mock( - "this_will_not_match_any_integration", HOMEKIT_STATUS_UNPAIRED + with ( + patch.dict( + zc_gen.ZEROCONF, + {"_hap._tcp.local.": [{"domain": "homekit_controller"}]}, + clear=True, + ), + patch.object(hass.config_entries.flow, "async_init") as mock_config_flow, + patch.object( + zeroconf, "AsyncServiceBrowser", side_effect=service_update_mock + ) as mock_service_browser, + patch( + "homeassistant.components.zeroconf.AsyncServiceInfo", + side_effect=get_homekit_info_mock( + "this_will_not_match_any_integration", HOMEKIT_STATUS_UNPAIRED + ), ), ): assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) @@ -758,25 +818,29 @@ async def test_homekit_controller_still_discovered_unpaired_for_cloud( 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 """ - with patch.dict( - zc_gen.ZEROCONF, - {"_hap._udp.local.": [{"domain": "homekit_controller"}]}, - clear=True, - ), patch.dict( - zc_gen.HOMEKIT, - {"Rachio": {"domain": "rachio", "always_discover": True}}, - clear=True, - ), patch.object( - hass.config_entries.flow, "async_init" - ) as mock_config_flow, patch.object( - zeroconf, - "AsyncServiceBrowser", - side_effect=lambda *args, **kwargs: service_update_mock( - *args, **kwargs, limit_service="_hap._udp.local." + with ( + patch.dict( + zc_gen.ZEROCONF, + {"_hap._udp.local.": [{"domain": "homekit_controller"}]}, + clear=True, + ), + patch.dict( + zc_gen.HOMEKIT, + {"Rachio": {"domain": "rachio", "always_discover": True}}, + clear=True, + ), + patch.object(hass.config_entries.flow, "async_init") as mock_config_flow, + patch.object( + zeroconf, + "AsyncServiceBrowser", + 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("Rachio-xyz", HOMEKIT_STATUS_UNPAIRED), ), - ) as mock_service_browser, patch( - "homeassistant.components.zeroconf.AsyncServiceInfo", - side_effect=get_homekit_info_mock("Rachio-xyz", HOMEKIT_STATUS_UNPAIRED), ): assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -798,25 +862,29 @@ async def test_homekit_controller_still_discovered_unpaired_for_polling( 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": {"domain": "gogogate2", "always_discover": True}}, - clear=True, - ), patch.object( - hass.config_entries.flow, "async_init" - ) as mock_config_flow, patch.object( - zeroconf, - "AsyncServiceBrowser", - side_effect=lambda *args, **kwargs: service_update_mock( - *args, **kwargs, limit_service="_hap._udp.local." + with ( + patch.dict( + zc_gen.ZEROCONF, + {"_hap._udp.local.": [{"domain": "homekit_controller"}]}, + clear=True, + ), + patch.dict( + zc_gen.HOMEKIT, + {"iSmartGate": {"domain": "gogogate2", "always_discover": True}}, + clear=True, + ), + patch.object(hass.config_entries.flow, "async_init") as mock_config_flow, + patch.object( + zeroconf, + "AsyncServiceBrowser", + 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), ), - ) 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) @@ -929,11 +997,14 @@ async def test_info_from_service_can_return_ipv6(hass: HomeAssistant) -> None: async def test_get_instance(hass: HomeAssistant, mock_async_zeroconf: None) -> None: """Test we get an instance.""" assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) - assert ( - await hass.components.zeroconf.async_get_async_instance() is mock_async_zeroconf - ) + assert await zeroconf.async_get_async_instance(hass) is mock_async_zeroconf hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) await hass.async_block_till_done() + assert len(mock_async_zeroconf.ha_async_close.mock_calls) == 0 + # Only shutdown at the close event so integrations have time + # to send out their goodbyes + hass.bus.async_fire(EVENT_HOMEASSISTANT_CLOSE) + await hass.async_block_till_done() assert len(mock_async_zeroconf.ha_async_close.mock_calls) == 1 @@ -961,12 +1032,13 @@ async def test_removed_ignored(hass: HomeAssistant, mock_async_zeroconf: None) - ServiceStateChange.Removed, ) - with patch.object( - zeroconf, "AsyncServiceBrowser", side_effect=service_update_mock - ), patch( - "homeassistant.components.zeroconf.AsyncServiceInfo", - side_effect=get_service_info_mock, - ) as mock_service_info: + with ( + patch.object(zeroconf, "AsyncServiceBrowser", side_effect=service_update_mock), + patch( + "homeassistant.components.zeroconf.AsyncServiceInfo", + side_effect=get_service_info_mock, + ) as mock_service_info, + ): assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -992,16 +1064,18 @@ async def test_async_detect_interfaces_setting_non_loopback_route( hass: HomeAssistant, mock_async_zeroconf: None ) -> None: """Test without default interface and the route returns a non-loopback address.""" - with patch("homeassistant.components.zeroconf.HaZeroconf") as mock_zc, patch.object( - hass.config_entries.flow, "async_init" - ), patch.object( - zeroconf, "AsyncServiceBrowser", side_effect=service_update_mock - ), patch( - "homeassistant.components.zeroconf.network.async_get_adapters", - return_value=_ADAPTER_WITH_DEFAULT_ENABLED, - ), patch( - "homeassistant.components.zeroconf.AsyncServiceInfo", - side_effect=get_service_info_mock, + with ( + patch("homeassistant.components.zeroconf.HaZeroconf") as mock_zc, + patch.object(hass.config_entries.flow, "async_init"), + patch.object(zeroconf, "AsyncServiceBrowser", side_effect=service_update_mock), + patch( + "homeassistant.components.zeroconf.network.async_get_adapters", + return_value=_ADAPTER_WITH_DEFAULT_ENABLED, + ), + patch( + "homeassistant.components.zeroconf.AsyncServiceInfo", + side_effect=get_service_info_mock, + ), ): assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -1076,16 +1150,19 @@ async def test_async_detect_interfaces_setting_empty_route_linux( hass: HomeAssistant, mock_async_zeroconf: None ) -> None: """Test without default interface config and the route returns nothing on linux.""" - with patch("homeassistant.components.zeroconf.sys.platform", "linux"), patch( - "homeassistant.components.zeroconf.HaZeroconf" - ) as mock_zc, patch.object(hass.config_entries.flow, "async_init"), patch.object( - zeroconf, "AsyncServiceBrowser", side_effect=service_update_mock - ), patch( - "homeassistant.components.zeroconf.network.async_get_adapters", - return_value=_ADAPTERS_WITH_MANUAL_CONFIG, - ), patch( - "homeassistant.components.zeroconf.AsyncServiceInfo", - side_effect=get_service_info_mock, + with ( + patch("homeassistant.components.zeroconf.sys.platform", "linux"), + patch("homeassistant.components.zeroconf.HaZeroconf") as mock_zc, + patch.object(hass.config_entries.flow, "async_init"), + patch.object(zeroconf, "AsyncServiceBrowser", side_effect=service_update_mock), + patch( + "homeassistant.components.zeroconf.network.async_get_adapters", + return_value=_ADAPTERS_WITH_MANUAL_CONFIG, + ), + patch( + "homeassistant.components.zeroconf.AsyncServiceInfo", + side_effect=get_service_info_mock, + ), ): assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -1106,16 +1183,19 @@ async def test_async_detect_interfaces_setting_empty_route_freebsd( hass: HomeAssistant, mock_async_zeroconf: None ) -> None: """Test without default interface and the route returns nothing on freebsd.""" - with patch("homeassistant.components.zeroconf.sys.platform", "freebsd"), patch( - "homeassistant.components.zeroconf.HaZeroconf" - ) as mock_zc, patch.object(hass.config_entries.flow, "async_init"), patch.object( - zeroconf, "AsyncServiceBrowser", side_effect=service_update_mock - ), patch( - "homeassistant.components.zeroconf.network.async_get_adapters", - return_value=_ADAPTERS_WITH_MANUAL_CONFIG, - ), patch( - "homeassistant.components.zeroconf.AsyncServiceInfo", - side_effect=get_service_info_mock, + with ( + patch("homeassistant.components.zeroconf.sys.platform", "freebsd"), + patch("homeassistant.components.zeroconf.HaZeroconf") as mock_zc, + patch.object(hass.config_entries.flow, "async_init"), + patch.object(zeroconf, "AsyncServiceBrowser", side_effect=service_update_mock), + patch( + "homeassistant.components.zeroconf.network.async_get_adapters", + return_value=_ADAPTERS_WITH_MANUAL_CONFIG, + ), + patch( + "homeassistant.components.zeroconf.AsyncServiceInfo", + side_effect=get_service_info_mock, + ), ): assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -1153,16 +1233,19 @@ async def test_async_detect_interfaces_explicitly_set_ipv6_linux( hass: HomeAssistant, mock_async_zeroconf: None ) -> None: """Test interfaces are explicitly set when IPv6 is present on linux.""" - with patch("homeassistant.components.zeroconf.sys.platform", "linux"), patch( - "homeassistant.components.zeroconf.HaZeroconf" - ) as mock_zc, patch.object(hass.config_entries.flow, "async_init"), patch.object( - zeroconf, "AsyncServiceBrowser", side_effect=service_update_mock - ), patch( - "homeassistant.components.zeroconf.network.async_get_adapters", - return_value=_ADAPTER_WITH_DEFAULT_ENABLED_AND_IPV6, - ), patch( - "homeassistant.components.zeroconf.AsyncServiceInfo", - side_effect=get_service_info_mock, + with ( + patch("homeassistant.components.zeroconf.sys.platform", "linux"), + patch("homeassistant.components.zeroconf.HaZeroconf") as mock_zc, + patch.object(hass.config_entries.flow, "async_init"), + patch.object(zeroconf, "AsyncServiceBrowser", side_effect=service_update_mock), + patch( + "homeassistant.components.zeroconf.network.async_get_adapters", + return_value=_ADAPTER_WITH_DEFAULT_ENABLED_AND_IPV6, + ), + patch( + "homeassistant.components.zeroconf.AsyncServiceInfo", + side_effect=get_service_info_mock, + ), ): assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -1178,16 +1261,19 @@ async def test_async_detect_interfaces_explicitly_set_ipv6_freebsd( hass: HomeAssistant, mock_async_zeroconf: None ) -> None: """Test interfaces are explicitly set when IPv6 is present on freebsd.""" - with patch("homeassistant.components.zeroconf.sys.platform", "freebsd"), patch( - "homeassistant.components.zeroconf.HaZeroconf" - ) as mock_zc, patch.object(hass.config_entries.flow, "async_init"), patch.object( - zeroconf, "AsyncServiceBrowser", side_effect=service_update_mock - ), patch( - "homeassistant.components.zeroconf.network.async_get_adapters", - return_value=_ADAPTER_WITH_DEFAULT_ENABLED_AND_IPV6, - ), patch( - "homeassistant.components.zeroconf.AsyncServiceInfo", - side_effect=get_service_info_mock, + with ( + patch("homeassistant.components.zeroconf.sys.platform", "freebsd"), + patch("homeassistant.components.zeroconf.HaZeroconf") as mock_zc, + patch.object(hass.config_entries.flow, "async_init"), + patch.object(zeroconf, "AsyncServiceBrowser", side_effect=service_update_mock), + patch( + "homeassistant.components.zeroconf.network.async_get_adapters", + return_value=_ADAPTER_WITH_DEFAULT_ENABLED_AND_IPV6, + ), + patch( + "homeassistant.components.zeroconf.AsyncServiceInfo", + side_effect=get_service_info_mock, + ), ): assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -1216,14 +1302,17 @@ async def test_setup_with_disallowed_characters_in_local_name( hass: HomeAssistant, mock_async_zeroconf: None, caplog: pytest.LogCaptureFixture ) -> None: """Test we still setup with disallowed characters in the location name.""" - with patch.object(hass.config_entries.flow, "async_init"), patch.object( - zeroconf, "AsyncServiceBrowser", side_effect=service_update_mock - ), patch.object( - hass.config, - "location_name", - "My.House", - ), patch( - "homeassistant.components.zeroconf.AsyncServiceInfo.async_request", + with ( + patch.object(hass.config_entries.flow, "async_init"), + patch.object(zeroconf, "AsyncServiceBrowser", side_effect=service_update_mock), + patch.object( + hass.config, + "location_name", + "My.House", + ), + patch( + "homeassistant.components.zeroconf.AsyncServiceInfo.async_request", + ), ): assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) hass.bus.async_fire(EVENT_HOMEASSISTANT_START) @@ -1257,28 +1346,32 @@ async def test_zeroconf_removed(hass: HomeAssistant, mock_async_zeroconf: None) ServiceStateChange.Removed, ) - with patch.dict( - zc_gen.ZEROCONF, - { - "_http._tcp.local.": [ - { - "domain": "shelly", - "name": "shelly*", - } - ] - }, - clear=True, - ), patch.object( - hass.config_entries.flow, - "async_progress_by_init_data_type", - return_value=[{"flow_id": "mock_flow_id"}], - ) as mock_async_progress_by_init_data_type, patch.object( - hass.config_entries.flow, "async_abort" - ) as mock_async_abort, patch.object( - zeroconf, "AsyncServiceBrowser", side_effect=_device_removed_mock - ) as mock_service_browser, patch( - "homeassistant.components.zeroconf.AsyncServiceInfo", - side_effect=get_zeroconf_info_mock("FFAADDCC11DD"), + with ( + patch.dict( + zc_gen.ZEROCONF, + { + "_http._tcp.local.": [ + { + "domain": "shelly", + "name": "shelly*", + } + ] + }, + clear=True, + ), + patch.object( + hass.config_entries.flow, + "async_progress_by_init_data_type", + return_value=[{"flow_id": "mock_flow_id"}], + ) as mock_async_progress_by_init_data_type, + patch.object(hass.config_entries.flow, "async_abort") as mock_async_abort, + patch.object( + zeroconf, "AsyncServiceBrowser", side_effect=_device_removed_mock + ) as mock_service_browser, + patch( + "homeassistant.components.zeroconf.AsyncServiceInfo", + side_effect=get_zeroconf_info_mock("FFAADDCC11DD"), + ), ): assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) diff --git a/tests/components/zeroconf/test_usage.py b/tests/components/zeroconf/test_usage.py index 3aaea1c50ee..9f5b68c2956 100644 --- a/tests/components/zeroconf/test_usage.py +++ b/tests/components/zeroconf/test_usage.py @@ -1,4 +1,5 @@ """Test Zeroconf multiple instance protection.""" + from unittest.mock import Mock, patch import pytest @@ -9,6 +10,8 @@ from homeassistant.components.zeroconf.usage import install_multiple_zeroconf_ca from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component +from tests.common import extract_stack_to_frame + DOMAIN = "zeroconf" @@ -49,26 +52,34 @@ async def test_multiple_zeroconf_instances_gives_shared( lineno="23", line="self.light.is_on", ) - with patch( - "homeassistant.helpers.frame.extract_stack", - return_value=[ - Mock( - filename="/home/dev/homeassistant/core.py", - lineno="23", - line="do_something()", + with ( + patch( + "homeassistant.helpers.frame.linecache.getline", + return_value=correct_frame.line, + ), + patch( + "homeassistant.helpers.frame.get_current_frame", + return_value=extract_stack_to_frame( + [ + Mock( + filename="/home/dev/homeassistant/core.py", + lineno="23", + line="do_something()", + ), + correct_frame, + Mock( + filename="/home/dev/homeassistant/components/zeroconf/usage.py", + lineno="23", + line="self.light.is_on", + ), + Mock( + filename="/home/dev/mdns/lights.py", + lineno="2", + line="something()", + ), + ] ), - correct_frame, - Mock( - filename="/home/dev/homeassistant/components/zeroconf/usage.py", - lineno="23", - line="self.light.is_on", - ), - Mock( - filename="/home/dev/mdns/lights.py", - lineno="2", - line="something()", - ), - ], + ), ): assert zeroconf.Zeroconf() == zeroconf_instance diff --git a/tests/components/zerproc/conftest.py b/tests/components/zerproc/conftest.py index 9d6bd9dea23..e6297d2cb14 100644 --- a/tests/components/zerproc/conftest.py +++ b/tests/components/zerproc/conftest.py @@ -1,2 +1,3 @@ """zerproc conftest.""" + from tests.components.light.conftest import mock_light_profiles # noqa: F401 diff --git a/tests/components/zerproc/test_config_flow.py b/tests/components/zerproc/test_config_flow.py index 0a493929b67..e512b2a668e 100644 --- a/tests/components/zerproc/test_config_flow.py +++ b/tests/components/zerproc/test_config_flow.py @@ -1,4 +1,5 @@ """Test the zerproc config flow.""" + from unittest.mock import patch import pyzerproc @@ -17,13 +18,16 @@ async def test_flow_success(hass: HomeAssistant) -> None: assert result["type"] == "form" assert result["errors"] is None - with patch( - "homeassistant.components.zerproc.config_flow.pyzerproc.discover", - return_value=["Light1", "Light2"], - ), patch( - "homeassistant.components.zerproc.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.zerproc.config_flow.pyzerproc.discover", + return_value=["Light1", "Light2"], + ), + patch( + "homeassistant.components.zerproc.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {}, @@ -46,13 +50,16 @@ async def test_flow_no_devices_found(hass: HomeAssistant) -> None: assert result["type"] == "form" assert result["errors"] is None - with patch( - "homeassistant.components.zerproc.config_flow.pyzerproc.discover", - return_value=[], - ), patch( - "homeassistant.components.zerproc.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.zerproc.config_flow.pyzerproc.discover", + return_value=[], + ), + patch( + "homeassistant.components.zerproc.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {}, @@ -73,13 +80,16 @@ async def test_flow_exceptions_caught(hass: HomeAssistant) -> None: assert result["type"] == "form" assert result["errors"] is None - with patch( - "homeassistant.components.zerproc.config_flow.pyzerproc.discover", - side_effect=pyzerproc.ZerprocException("TEST"), - ), patch( - "homeassistant.components.zerproc.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.zerproc.config_flow.pyzerproc.discover", + side_effect=pyzerproc.ZerprocException("TEST"), + ), + patch( + "homeassistant.components.zerproc.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {}, diff --git a/tests/components/zerproc/test_light.py b/tests/components/zerproc/test_light.py index 662a75fb7c8..c47f960b182 100644 --- a/tests/components/zerproc/test_light.py +++ b/tests/components/zerproc/test_light.py @@ -1,4 +1,5 @@ """Test the zerproc lights.""" + from unittest.mock import MagicMock, patch import pytest @@ -22,7 +23,6 @@ from homeassistant.components.zerproc.const import ( from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, - ATTR_ICON, ATTR_SUPPORTED_FEATURES, STATE_OFF, STATE_ON, @@ -53,11 +53,13 @@ async def mock_light(hass, mock_entry): mock_state = pyzerproc.LightState(False, (0, 0, 0)) - with patch( - "homeassistant.components.zerproc.light.pyzerproc.discover", - return_value=[light], - ), patch.object(light, "connect"), patch.object( - light, "get_state", return_value=mock_state + with ( + patch( + "homeassistant.components.zerproc.light.pyzerproc.discover", + return_value=[light], + ), + patch.object(light, "connect"), + patch.object(light, "get_state", return_value=mock_state), ): await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() @@ -101,7 +103,6 @@ async def test_init(hass: HomeAssistant, mock_entry) -> None: ATTR_FRIENDLY_NAME: "LEDBlue-CCDDEEFF", ATTR_SUPPORTED_COLOR_MODES: [ColorMode.HS], ATTR_SUPPORTED_FEATURES: 0, - ATTR_ICON: "mdi:string-lights", ATTR_COLOR_MODE: None, ATTR_BRIGHTNESS: None, ATTR_HS_COLOR: None, @@ -115,7 +116,6 @@ async def test_init(hass: HomeAssistant, mock_entry) -> None: ATTR_FRIENDLY_NAME: "LEDBlue-33445566", ATTR_SUPPORTED_COLOR_MODES: [ColorMode.HS], ATTR_SUPPORTED_FEATURES: 0, - ATTR_ICON: "mdi:string-lights", ATTR_COLOR_MODE: ColorMode.HS, ATTR_BRIGHTNESS: 255, ATTR_HS_COLOR: (221.176, 100.0), @@ -287,7 +287,6 @@ async def test_light_update(hass: HomeAssistant, mock_light) -> None: ATTR_FRIENDLY_NAME: "LEDBlue-CCDDEEFF", ATTR_SUPPORTED_COLOR_MODES: [ColorMode.HS], ATTR_SUPPORTED_FEATURES: 0, - ATTR_ICON: "mdi:string-lights", ATTR_COLOR_MODE: None, ATTR_BRIGHTNESS: None, ATTR_HS_COLOR: None, @@ -311,7 +310,6 @@ async def test_light_update(hass: HomeAssistant, mock_light) -> None: ATTR_FRIENDLY_NAME: "LEDBlue-CCDDEEFF", ATTR_SUPPORTED_COLOR_MODES: [ColorMode.HS], ATTR_SUPPORTED_FEATURES: 0, - ATTR_ICON: "mdi:string-lights", } with patch.object( @@ -329,7 +327,6 @@ async def test_light_update(hass: HomeAssistant, mock_light) -> None: ATTR_FRIENDLY_NAME: "LEDBlue-CCDDEEFF", ATTR_SUPPORTED_COLOR_MODES: [ColorMode.HS], ATTR_SUPPORTED_FEATURES: 0, - ATTR_ICON: "mdi:string-lights", ATTR_COLOR_MODE: None, ATTR_BRIGHTNESS: None, ATTR_HS_COLOR: None, @@ -352,7 +349,6 @@ async def test_light_update(hass: HomeAssistant, mock_light) -> None: ATTR_FRIENDLY_NAME: "LEDBlue-CCDDEEFF", ATTR_SUPPORTED_COLOR_MODES: [ColorMode.HS], ATTR_SUPPORTED_FEATURES: 0, - ATTR_ICON: "mdi:string-lights", ATTR_COLOR_MODE: ColorMode.HS, ATTR_BRIGHTNESS: 220, ATTR_HS_COLOR: (261.429, 31.818), diff --git a/tests/components/zeversolar/test_config_flow.py b/tests/components/zeversolar/test_config_flow.py index 9ef1c1d6f83..0bfa5ad547d 100644 --- a/tests/components/zeversolar/test_config_flow.py +++ b/tests/components/zeversolar/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Zeversolar config flow.""" + from unittest.mock import MagicMock, patch import pytest @@ -30,7 +31,7 @@ async def test_form(hass: HomeAssistant) -> None: @pytest.mark.parametrize( ("side_effect", "errors"), - ( + [ ( ZeverSolarHTTPNotFound, {"base": "invalid_host"}, @@ -47,7 +48,7 @@ async def test_form(hass: HomeAssistant) -> None: RuntimeError, {"base": "unknown"}, ), - ), + ], ) async def test_form_errors( hass: HomeAssistant, @@ -95,9 +96,12 @@ async def test_abort_already_configured(hass: HomeAssistant) -> None: mock_data = MagicMock() mock_data.serial_number = "test_serial" - with patch("zeversolar.ZeverSolarClient.get_data", return_value=mock_data), patch( - "homeassistant.components.zeversolar.async_setup_entry", - ) as mock_setup_entry: + with ( + patch("zeversolar.ZeverSolarClient.get_data", return_value=mock_data), + patch( + "homeassistant.components.zeversolar.async_setup_entry", + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( flow_id=result["flow_id"], user_input={ @@ -115,10 +119,13 @@ async def _set_up_zeversolar(hass: HomeAssistant, flow_id: str) -> None: """Reusable successful setup of Zeversolar sensor.""" mock_data = MagicMock() mock_data.serial_number = "test_serial" - with patch("zeversolar.ZeverSolarClient.get_data", return_value=mock_data), patch( - "homeassistant.components.zeversolar.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch("zeversolar.ZeverSolarClient.get_data", return_value=mock_data), + patch( + "homeassistant.components.zeversolar.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( flow_id=flow_id, user_input={ diff --git a/tests/components/zha/common.py b/tests/components/zha/common.py index d679ac5cb03..63d3e9cf747 100644 --- a/tests/components/zha/common.py +++ b/tests/components/zha/common.py @@ -1,4 +1,5 @@ """Common test objects.""" + import asyncio from datetime import timedelta import math diff --git a/tests/components/zha/conftest.py b/tests/components/zha/conftest.py index 36d0cbcff97..b1ac22d544d 100644 --- a/tests/components/zha/conftest.py +++ b/tests/components/zha/conftest.py @@ -1,4 +1,5 @@ """Test configuration for the ZHA component.""" + from collections.abc import Callable, Generator import itertools import time @@ -226,12 +227,15 @@ def mock_zigpy_connect( zigpy_app_controller: ControllerApplication, ) -> Generator[ControllerApplication, None, None]: """Patch the zigpy radio connection with our mock application.""" - with patch( - "bellows.zigbee.application.ControllerApplication.new", - return_value=zigpy_app_controller, - ), patch( - "bellows.zigbee.application.ControllerApplication", - return_value=zigpy_app_controller, + with ( + patch( + "bellows.zigbee.application.ControllerApplication.new", + return_value=zigpy_app_controller, + ), + patch( + "bellows.zigbee.application.ControllerApplication", + return_value=zigpy_app_controller, + ), ): yield zigpy_app_controller diff --git a/tests/components/zha/test_alarm_control_panel.py b/tests/components/zha/test_alarm_control_panel.py index 49ad1b81e3b..18065420e58 100644 --- a/tests/components/zha/test_alarm_control_panel.py +++ b/tests/components/zha/test_alarm_control_panel.py @@ -1,4 +1,5 @@ """Test ZHA alarm control panel.""" + from unittest.mock import AsyncMock, call, patch, sentinel import pytest diff --git a/tests/components/zha/test_api.py b/tests/components/zha/test_api.py index c3dac0ddd8c..9e35e482fcf 100644 --- a/tests/components/zha/test_api.py +++ b/tests/components/zha/test_api.py @@ -1,4 +1,5 @@ """Test ZHA API.""" + from __future__ import annotations from typing import TYPE_CHECKING diff --git a/tests/components/zha/test_backup.py b/tests/components/zha/test_backup.py index bee00c5a587..9cf88df1707 100644 --- a/tests/components/zha/test_backup.py +++ b/tests/components/zha/test_backup.py @@ -1,4 +1,5 @@ """Unit tests for ZHA backup platform.""" + from unittest.mock import AsyncMock from zigpy.application import ControllerApplication diff --git a/tests/components/zha/test_binary_sensor.py b/tests/components/zha/test_binary_sensor.py index 5dd7a5653ec..18e78ae7e57 100644 --- a/tests/components/zha/test_binary_sensor.py +++ b/tests/components/zha/test_binary_sensor.py @@ -1,4 +1,5 @@ """Test ZHA binary sensor.""" + from unittest.mock import patch import pytest @@ -15,7 +16,6 @@ from .common import ( async_test_rejoin, find_entity_id, send_attributes_report, - update_attribute_cache, ) from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE @@ -150,66 +150,6 @@ async def test_binary_sensor( assert hass.states.get(entity_id).state == STATE_OFF -@pytest.mark.parametrize( - "restored_state", - [ - STATE_ON, - STATE_OFF, - ], -) -async def test_binary_sensor_migration_not_migrated( - hass: HomeAssistant, - zigpy_device_mock, - core_rs, - zha_device_restored, - restored_state, -) -> None: - """Test temporary ZHA IasZone binary_sensor migration to zigpy cache.""" - - entity_id = "binary_sensor.fakemanufacturer_fakemodel_ias_zone" - core_rs(entity_id, state=restored_state, attributes={}) # migration sensor state - await async_mock_load_restore_state_from_storage(hass) - - zigpy_device = zigpy_device_mock(DEVICE_IAS) - - zha_device = await zha_device_restored(zigpy_device) - entity_id = find_entity_id(Platform.BINARY_SENSOR, zha_device, hass) - - assert entity_id is not None - assert hass.states.get(entity_id).state == restored_state - - # confirm migration extra state attribute was set to True - assert hass.states.get(entity_id).attributes["migrated_to_cache"] - - -async def test_binary_sensor_migration_already_migrated( - hass: HomeAssistant, - zigpy_device_mock, - core_rs, - zha_device_restored, -) -> None: - """Test temporary ZHA IasZone binary_sensor migration doesn't migrate multiple times.""" - - entity_id = "binary_sensor.fakemanufacturer_fakemodel_ias_zone" - core_rs(entity_id, state=STATE_OFF, attributes={"migrated_to_cache": True}) - await async_mock_load_restore_state_from_storage(hass) - - zigpy_device = zigpy_device_mock(DEVICE_IAS) - - cluster = zigpy_device.endpoints.get(1).ias_zone - cluster.PLUGGED_ATTR_READS = { - "zone_status": security.IasZone.ZoneStatus.Alarm_1, - } - update_attribute_cache(cluster) - - zha_device = await zha_device_restored(zigpy_device) - entity_id = find_entity_id(Platform.BINARY_SENSOR, zha_device, hass) - - assert entity_id is not None - assert hass.states.get(entity_id).state == STATE_ON # matches attribute cache - assert hass.states.get(entity_id).attributes["migrated_to_cache"] - - @pytest.mark.parametrize( "restored_state", [ diff --git a/tests/components/zha/test_button.py b/tests/components/zha/test_button.py index 9eab72b435b..4c0c6845885 100644 --- a/tests/components/zha/test_button.py +++ b/tests/components/zha/test_button.py @@ -1,4 +1,5 @@ """Test ZHA button.""" + from typing import Final from unittest.mock import call, patch diff --git a/tests/components/zha/test_climate.py b/tests/components/zha/test_climate.py index d60b4bd1a49..16563f62e06 100644 --- a/tests/components/zha/test_climate.py +++ b/tests/components/zha/test_climate.py @@ -1,4 +1,5 @@ """Test ZHA climate.""" + from typing import Literal from unittest.mock import call, patch @@ -522,14 +523,14 @@ async def test_climate_hvac_action_pi_demand( @pytest.mark.parametrize( ("sys_mode", "hvac_mode"), - ( + [ (Thermostat.SystemMode.Auto, HVACMode.HEAT_COOL), (Thermostat.SystemMode.Cool, HVACMode.COOL), (Thermostat.SystemMode.Heat, HVACMode.HEAT), (Thermostat.SystemMode.Pre_cooling, HVACMode.COOL), (Thermostat.SystemMode.Fan_only, HVACMode.FAN_ONLY), (Thermostat.SystemMode.Dry, HVACMode.DRY), - ), + ], ) async def test_hvac_mode( hass: HomeAssistant, device_climate, sys_mode, hvac_mode @@ -559,7 +560,7 @@ async def test_hvac_mode( @pytest.mark.parametrize( ("seq_of_op", "modes"), - ( + [ (0xFF, {HVACMode.OFF}), (0x00, {HVACMode.OFF, HVACMode.COOL}), (0x01, {HVACMode.OFF, HVACMode.COOL}), @@ -567,7 +568,7 @@ async def test_hvac_mode( (0x03, {HVACMode.OFF, HVACMode.HEAT}), (0x04, {HVACMode.OFF, HVACMode.COOL, HVACMode.HEAT, HVACMode.HEAT_COOL}), (0x05, {HVACMode.OFF, HVACMode.COOL, HVACMode.HEAT, HVACMode.HEAT_COOL}), - ), + ], ) async def test_hvac_modes( hass: HomeAssistant, device_climate_mock, seq_of_op, modes @@ -584,12 +585,12 @@ async def test_hvac_modes( @pytest.mark.parametrize( ("sys_mode", "preset", "target_temp"), - ( + [ (Thermostat.SystemMode.Heat, None, 22), (Thermostat.SystemMode.Heat, PRESET_AWAY, 16), (Thermostat.SystemMode.Cool, None, 25), (Thermostat.SystemMode.Cool, PRESET_AWAY, 27), - ), + ], ) async def test_target_temperature( hass: HomeAssistant, @@ -627,11 +628,11 @@ async def test_target_temperature( @pytest.mark.parametrize( ("preset", "unoccupied", "target_temp"), - ( + [ (None, 1800, 17), (PRESET_AWAY, 1800, 18), (PRESET_AWAY, None, None), - ), + ], ) async def test_target_temperature_high( hass: HomeAssistant, device_climate_mock, preset, unoccupied, target_temp @@ -663,11 +664,11 @@ async def test_target_temperature_high( @pytest.mark.parametrize( ("preset", "unoccupied", "target_temp"), - ( + [ (None, 1600, 21), (PRESET_AWAY, 1600, 16), (PRESET_AWAY, None, None), - ), + ], ) async def test_target_temperature_low( hass: HomeAssistant, device_climate_mock, preset, unoccupied, target_temp @@ -699,14 +700,14 @@ async def test_target_temperature_low( @pytest.mark.parametrize( ("hvac_mode", "sys_mode"), - ( + [ (HVACMode.AUTO, None), (HVACMode.COOL, Thermostat.SystemMode.Cool), (HVACMode.DRY, None), (HVACMode.FAN_ONLY, None), (HVACMode.HEAT, Thermostat.SystemMode.Heat), (HVACMode.HEAT_COOL, Thermostat.SystemMode.Auto), - ), + ], ) async def test_set_hvac_mode( hass: HomeAssistant, device_climate, hvac_mode, sys_mode diff --git a/tests/components/zha/test_cluster_handlers.py b/tests/components/zha/test_cluster_handlers.py index 252148481a7..60c958f20fe 100644 --- a/tests/components/zha/test_cluster_handlers.py +++ b/tests/components/zha/test_cluster_handlers.py @@ -1,4 +1,5 @@ """Test ZHA Core cluster handlers.""" + from collections.abc import Callable import logging import math @@ -147,7 +148,6 @@ async def poll_control_device(zha_device_restored, zigpy_device_mock): (zigpy.zcl.clusters.general.AnalogInput.cluster_id, 1, {"present_value"}), (zigpy.zcl.clusters.general.AnalogOutput.cluster_id, 1, {"present_value"}), (zigpy.zcl.clusters.general.AnalogValue.cluster_id, 1, {"present_value"}), - (zigpy.zcl.clusters.general.AnalogOutput.cluster_id, 1, {"present_value"}), (zigpy.zcl.clusters.general.BinaryOutput.cluster_id, 1, {"present_value"}), (zigpy.zcl.clusters.general.BinaryValue.cluster_id, 1, {"present_value"}), (zigpy.zcl.clusters.general.MultistateInput.cluster_id, 1, {"present_value"}), @@ -379,7 +379,7 @@ def test_cluster_handler_registry() -> None: assert cluster_id in all_quirk_ids assert isinstance(cluster_handler_classes, dict) for quirk_id, cluster_handler in cluster_handler_classes.items(): - assert isinstance(quirk_id, NoneType) or isinstance(quirk_id, str) + assert isinstance(quirk_id, (NoneType, str)) assert issubclass(cluster_handler, cluster_handlers.ClusterHandler) assert quirk_id in all_quirk_ids[cluster_id] @@ -578,9 +578,10 @@ async def test_ep_cluster_handlers_configure(cluster_handler) -> None: claimed = {ch_1.id: ch_1, ch_2.id: ch_2, ch_3.id: ch_3} client_handlers = {ch_4.id: ch_4, ch_5.id: ch_5} - with mock.patch.dict( - endpoint.claimed_cluster_handlers, claimed, clear=True - ), mock.patch.dict(endpoint.client_cluster_handlers, client_handlers, clear=True): + with ( + mock.patch.dict(endpoint.claimed_cluster_handlers, claimed, clear=True), + mock.patch.dict(endpoint.client_cluster_handlers, client_handlers, clear=True), + ): await endpoint.async_configure() await endpoint.async_initialize(mock.sentinel.from_cache) @@ -870,10 +871,13 @@ async def test_invalid_cluster_handler(hass: HomeAssistant, caplog) -> None: TestZigbeeClusterHandler(cluster, zha_endpoint) # And one is also logged at runtime - with patch.dict( - registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY[cluster.cluster_id], - {None: TestZigbeeClusterHandler}, - ), caplog.at_level(logging.WARNING): + with ( + patch.dict( + registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY[cluster.cluster_id], + {None: TestZigbeeClusterHandler}, + ), + caplog.at_level(logging.WARNING), + ): zha_endpoint.add_all_cluster_handlers() assert "missing_attr" in caplog.text diff --git a/tests/components/zha/test_config_flow.py b/tests/components/zha/test_config_flow.py index 0972918a648..bbfca1b1a13 100644 --- a/tests/components/zha/test_config_flow.py +++ b/tests/components/zha/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for ZHA config flow.""" + import copy from datetime import timedelta from ipaddress import ip_address @@ -54,12 +55,15 @@ def disable_platform_only(): @pytest.fixture(autouse=True) def mock_multipan_platform(): """Mock the multipan platform.""" - with patch( - "homeassistant.components.zha.silabs_multiprotocol.async_get_channel", - return_value=None, - ), patch( - "homeassistant.components.zha.silabs_multiprotocol.async_using_multipan", - return_value=False, + with ( + patch( + "homeassistant.components.zha.silabs_multiprotocol.async_get_channel", + return_value=None, + ), + patch( + "homeassistant.components.zha.silabs_multiprotocol.async_using_multipan", + return_value=False, + ), ): yield @@ -998,7 +1002,7 @@ async def test_hardware_already_setup(hass: HomeAssistant) -> None: @pytest.mark.parametrize( - "data", (None, {}, {"radio_type": "best_radio"}, {"radio_type": "efr32"}) + "data", [None, {}, {"radio_type": "best_radio"}, {"radio_type": "efr32"}] ) async def test_hardware_invalid_data(hass: HomeAssistant, data) -> None: """Test onboarding flow -- invalid data.""" @@ -1824,9 +1828,10 @@ async def test_config_flow_port_yellow_port_name(hass: HomeAssistant) -> None: port.manufacturer = None port.description = None - with patch( - "homeassistant.components.zha.config_flow.yellow_hardware.async_info" - ), patch("serial.tools.list_ports.comports", MagicMock(return_value=[port])): + with ( + patch("homeassistant.components.zha.config_flow.yellow_hardware.async_info"), + patch("serial.tools.list_ports.comports", MagicMock(return_value=[port])), + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER}, @@ -1841,10 +1846,11 @@ async def test_config_flow_port_yellow_port_name(hass: HomeAssistant) -> None: async def test_config_flow_port_multiprotocol_port_name(hass: HomeAssistant) -> None: """Test config flow serial port name for multiprotocol add-on.""" - with patch( - "homeassistant.components.hassio.addon_manager.AddonManager.async_get_addon_info" - ) as async_get_addon_info, patch( - "serial.tools.list_ports.comports", MagicMock(return_value=[]) + with ( + patch( + "homeassistant.components.hassio.addon_manager.AddonManager.async_get_addon_info" + ) as async_get_addon_info, + patch("serial.tools.list_ports.comports", MagicMock(return_value=[])), ): async_get_addon_info.return_value.state = AddonState.RUNNING async_get_addon_info.return_value.hostname = "core-silabs-multiprotocol" @@ -1885,11 +1891,14 @@ async def test_probe_wrong_firmware_installed(hass: HomeAssistant) -> None: async def test_discovery_wrong_firmware_installed(hass: HomeAssistant) -> None: """Test auto-probing failing because the wrong firmware is installed.""" - with patch( - "homeassistant.components.zha.radio_manager.ZhaRadioManager.detect_radio_type", - return_value=ProbeResult.WRONG_FIRMWARE_INSTALLED, - ), patch( - "homeassistant.components.onboarding.async_is_onboarded", return_value=False + with ( + patch( + "homeassistant.components.zha.radio_manager.ZhaRadioManager.detect_radio_type", + return_value=ProbeResult.WRONG_FIRMWARE_INSTALLED, + ), + patch( + "homeassistant.components.onboarding.async_is_onboarded", return_value=False + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, diff --git a/tests/components/zha/test_cover.py b/tests/components/zha/test_cover.py index 964868118c4..a1b320097e8 100644 --- a/tests/components/zha/test_cover.py +++ b/tests/components/zha/test_cover.py @@ -1,4 +1,5 @@ """Test ZHA cover.""" + import asyncio from unittest.mock import patch diff --git a/tests/components/zha/test_device.py b/tests/components/zha/test_device.py index 411d7081577..48eecdd87d4 100644 --- a/tests/components/zha/test_device.py +++ b/tests/components/zha/test_device.py @@ -1,4 +1,5 @@ """Test ZHA device switch.""" + from datetime import timedelta import logging import time @@ -266,7 +267,7 @@ async def test_ota_sw_version(hass: HomeAssistant, ota_zha_device) -> None: @pytest.mark.parametrize( ("device", "last_seen_delta", "is_available"), - ( + [ ("zigpy_device", 0, True), ( "zigpy_device", @@ -304,7 +305,7 @@ async def test_ota_sw_version(hass: HomeAssistant, ota_zha_device) -> None: CONF_DEFAULT_CONSIDER_UNAVAILABLE_BATTERY + 2, False, ), - ), + ], ) async def test_device_restore_availability( hass: HomeAssistant, diff --git a/tests/components/zha/test_device_action.py b/tests/components/zha/test_device_action.py index 229fde89f15..a7b66dea8d7 100644 --- a/tests/components/zha/test_device_action.py +++ b/tests/components/zha/test_device_action.py @@ -1,4 +1,5 @@ """The test for ZHA device automation actions.""" + from unittest.mock import call, patch import pytest diff --git a/tests/components/zha/test_device_tracker.py b/tests/components/zha/test_device_tracker.py index aa3c6b7d146..89ea788e5ef 100644 --- a/tests/components/zha/test_device_tracker.py +++ b/tests/components/zha/test_device_tracker.py @@ -1,4 +1,5 @@ """Test ZHA Device Tracker.""" + from datetime import timedelta import time from unittest.mock import patch diff --git a/tests/components/zha/test_device_trigger.py b/tests/components/zha/test_device_trigger.py index c3563872873..f9141795ef1 100644 --- a/tests/components/zha/test_device_trigger.py +++ b/tests/components/zha/test_device_trigger.py @@ -1,4 +1,5 @@ """ZHA device automation trigger tests.""" + from datetime import timedelta import time from unittest.mock import patch @@ -71,10 +72,7 @@ def _same_lists(list_a, list_b): if len(list_a) != len(list_b): return False - for item in list_a: - if item not in list_b: - return False - return True + return all(item in list_b for item in list_a) @pytest.fixture diff --git a/tests/components/zha/test_diagnostics.py b/tests/components/zha/test_diagnostics.py index c91d9c1ddbc..3493d772a6f 100644 --- a/tests/components/zha/test_diagnostics.py +++ b/tests/components/zha/test_diagnostics.py @@ -1,4 +1,5 @@ """Tests for the diagnostics data provided by the ESPHome integration.""" + from unittest.mock import patch import pytest diff --git a/tests/components/zha/test_discover.py b/tests/components/zha/test_discover.py index c8eba90a372..f9242eb1d96 100644 --- a/tests/components/zha/test_discover.py +++ b/tests/components/zha/test_discover.py @@ -1,5 +1,8 @@ """Test ZHA device discovery.""" + from collections.abc import Callable +import enum +import itertools import re from typing import Any from unittest import mock @@ -19,7 +22,16 @@ from zhaquirks.xiaomi.aqara.driver_curtain_e1 import ( from zigpy.const import SIG_ENDPOINTS, SIG_MANUFACTURER, SIG_MODEL, SIG_NODE_DESC import zigpy.profiles.zha import zigpy.quirks -from zigpy.quirks.v2 import EntityType, add_to_registry_v2 +from zigpy.quirks.v2 import ( + BinarySensorMetadata, + EntityMetadata, + EntityType, + NumberMetadata, + QuirksV2RegistryEntry, + ZCLCommandButtonMetadata, + ZCLSensorMetadata, + add_to_registry_v2, +) from zigpy.quirks.v2.homeassistant import UnitOfTime import zigpy.types from zigpy.zcl import ClusterType @@ -39,6 +51,7 @@ from homeassistant.const import STATE_OFF, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_platform import EntityPlatform +from homeassistant.util.json import load_json from .common import find_entity_id, update_attribute_cache from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE @@ -69,10 +82,7 @@ IGNORE_SUFFIXES = [ def contains_ignored_suffix(unique_id: str) -> bool: """Return true if the unique_id ends with an ignored suffix.""" - for suffix in IGNORE_SUFFIXES: - if suffix.lower() in unique_id.lower(): - return True - return False + return any(suffix.lower() in unique_id.lower() for suffix in IGNORE_SUFFIXES) @patch( @@ -254,10 +264,13 @@ def test_discover_by_device_type_override() -> None: get_entity_mock = mock.MagicMock( return_value=(mock.sentinel.entity_cls, mock.sentinel.claimed) ) - with mock.patch( - "homeassistant.components.zha.core.registries.ZHA_ENTITIES.get_entity", - get_entity_mock, - ), mock.patch.dict(disc.PROBE._device_configs, overrides, clear=True): + with ( + mock.patch( + "homeassistant.components.zha.core.registries.ZHA_ENTITIES.get_entity", + get_entity_mock, + ), + mock.patch.dict(disc.PROBE._device_configs, overrides, clear=True), + ): disc.PROBE.discover_by_device_type(endpoint) assert get_entity_mock.call_count == 1 assert endpoint.claim_cluster_handlers.call_count == 1 @@ -519,6 +532,7 @@ async def test_quirks_v2_entity_discovery( step=1, unit=UnitOfTime.SECONDS, multiplier=1, + translation_key="on_off_transition_time", ) ) @@ -617,7 +631,11 @@ async def test_quirks_v2_entity_discovery_e1_curtain( entity_platform=Platform.SENSOR, entity_type=EntityType.DIAGNOSTIC, ) - .binary_sensor("error_detected", FakeXiaomiAqaraDriverE1.cluster_id) + .binary_sensor( + "error_detected", + FakeXiaomiAqaraDriverE1.cluster_id, + translation_key="valve_alarm", + ) ) aqara_E1_device = zigpy.quirks._DEVICE_REGISTRY.get_device(aqara_E1_device) @@ -682,7 +700,13 @@ async def test_quirks_v2_entity_discovery_e1_curtain( assert state.state == STATE_OFF -def _get_test_device(zigpy_device_mock, manufacturer: str, model: str): +def _get_test_device( + zigpy_device_mock, + manufacturer: str, + model: str, + augment_method: Callable[[QuirksV2RegistryEntry], QuirksV2RegistryEntry] + | None = None, +): zigpy_device = zigpy_device_mock( { 1: { @@ -702,7 +726,7 @@ def _get_test_device(zigpy_device_mock, manufacturer: str, model: str): model=model, ) - ( + v2_quirk = ( add_to_registry_v2(manufacturer, model, zigpy.quirks._DEVICE_REGISTRY) .replaces(PowerConfig1CRCluster) .replaces(ScenesCluster, cluster_type=ClusterType.Client) @@ -715,6 +739,7 @@ def _get_test_device(zigpy_device_mock, manufacturer: str, model: str): step=1, unit=UnitOfTime.SECONDS, multiplier=1, + translation_key="on_off_transition_time", ) .number( zigpy.zcl.clusters.general.OnOff.AttributeDefs.off_wait_time.name, @@ -724,14 +749,19 @@ def _get_test_device(zigpy_device_mock, manufacturer: str, model: str): step=1, unit=UnitOfTime.SECONDS, multiplier=1, + translation_key="on_off_transition_time", ) .sensor( zigpy.zcl.clusters.general.OnOff.AttributeDefs.off_wait_time.name, zigpy.zcl.clusters.general.OnOff.cluster_id, entity_type=EntityType.CONFIG, + translation_key="analog_input", ) ) + if augment_method: + v2_quirk = augment_method(v2_quirk) + zigpy_device = zigpy.quirks._DEVICE_REGISTRY.get_device(zigpy_device) zigpy_device.endpoints[1].power.PLUGGED_ATTR_READS = { "battery_voltage": 3, @@ -791,14 +821,13 @@ async def test_quirks_v2_entity_discovery_errors( # fmt: off entity_details = ( - "{'cluster_details': (1, 6, ), " - "'quirk_metadata': EntityMetadata(entity_metadata=ZCLSensorMetadata(" - "attribute_name='off_wait_time', divisor=1, multiplier=1, unit=None, " - "device_class=None, state_class=None), entity_platform=, entity_type=, " - "cluster_id=6, endpoint_id=1, cluster_type=, " - "initially_disabled=False, attribute_initialized_from_cache=True, " - "translation_key=None)}" + "{'cluster_details': (1, 6, ), 'entity_metadata': " + "ZCLSensorMetadata(entity_platform=, " + "entity_type=, cluster_id=6, endpoint_id=1, " + "cluster_type=, initially_disabled=False, " + "attribute_initialized_from_cache=True, translation_key='analog_input', " + "attribute_name='off_wait_time', divisor=1, multiplier=1, " + "unit=None, device_class=None, state_class=None)}" ) # fmt: on @@ -806,3 +835,266 @@ async def test_quirks_v2_entity_discovery_errors( m2 = f"details: {entity_details} that does not have an entity class mapping - " m3 = "unable to create entity" assert f"{m1}{m2}{m3}" in caplog.text + + +DEVICE_CLASS_TYPES = [NumberMetadata, BinarySensorMetadata, ZCLSensorMetadata] + + +def validate_device_class_unit( + quirk: QuirksV2RegistryEntry, + entity_metadata: EntityMetadata, + platform: Platform, + translations: dict, +) -> None: + """Ensure device class and unit are used correctly.""" + if ( + hasattr(entity_metadata, "unit") + and entity_metadata.unit is not None + and hasattr(entity_metadata, "device_class") + and entity_metadata.device_class is not None + ): + m1 = "device_class and unit are both set - unit: " + m2 = f"{entity_metadata.unit} device_class: " + m3 = f"{entity_metadata.device_class} for {platform.name} " + raise ValueError(f"{m1}{m2}{m3}{quirk}") + + +def validate_translation_keys( + quirk: QuirksV2RegistryEntry, + entity_metadata: EntityMetadata, + platform: Platform, + translations: dict, +) -> None: + """Ensure translation keys exist for all v2 quirks.""" + if isinstance(entity_metadata, ZCLCommandButtonMetadata): + default_translation_key = entity_metadata.command_name + else: + default_translation_key = entity_metadata.attribute_name + translation_key = entity_metadata.translation_key or default_translation_key + + if ( + translation_key is not None + and translation_key not in translations["entity"][platform] + ): + raise ValueError( + f"Missing translation key: {translation_key} for {platform.name} {quirk}" + ) + + +def validate_translation_keys_device_class( + quirk: QuirksV2RegistryEntry, + entity_metadata: EntityMetadata, + platform: Platform, + translations: dict, +) -> None: + """Validate translation keys and device class usage.""" + if isinstance(entity_metadata, ZCLCommandButtonMetadata): + default_translation_key = entity_metadata.command_name + else: + default_translation_key = entity_metadata.attribute_name + translation_key = entity_metadata.translation_key or default_translation_key + + metadata_type = type(entity_metadata) + if metadata_type in DEVICE_CLASS_TYPES: + device_class = entity_metadata.device_class + if device_class is not None and translation_key is not None: + m1 = "translation_key and device_class are both set - translation_key: " + m2 = f"{translation_key} device_class: {device_class} for {platform.name} " + raise ValueError(f"{m1}{m2}{quirk}") + + +def validate_metadata(validator: Callable) -> None: + """Ensure v2 quirks metadata does not violate HA rules.""" + all_v2_quirks = itertools.chain.from_iterable( + zigpy.quirks._DEVICE_REGISTRY._registry_v2.values() + ) + translations = load_json("homeassistant/components/zha/strings.json") + for quirk in all_v2_quirks: + for entity_metadata in quirk.entity_metadata: + platform = Platform(entity_metadata.entity_platform.value) + validator(quirk, entity_metadata, platform, translations) + + +def bad_translation_key(v2_quirk: QuirksV2RegistryEntry) -> QuirksV2RegistryEntry: + """Introduce a bad translation key.""" + return v2_quirk.sensor( + zigpy.zcl.clusters.general.OnOff.AttributeDefs.off_wait_time.name, + zigpy.zcl.clusters.general.OnOff.cluster_id, + entity_type=EntityType.CONFIG, + translation_key="missing_translation_key", + ) + + +def bad_device_class_unit_combination( + v2_quirk: QuirksV2RegistryEntry, +) -> QuirksV2RegistryEntry: + """Introduce a bad device class and unit combination.""" + return v2_quirk.sensor( + zigpy.zcl.clusters.general.OnOff.AttributeDefs.off_wait_time.name, + zigpy.zcl.clusters.general.OnOff.cluster_id, + entity_type=EntityType.CONFIG, + unit="invalid", + device_class="invalid", + translation_key="analog_input", + ) + + +def bad_device_class_translation_key_usage( + v2_quirk: QuirksV2RegistryEntry, +) -> QuirksV2RegistryEntry: + """Introduce a bad device class and translation key combination.""" + return v2_quirk.sensor( + zigpy.zcl.clusters.general.OnOff.AttributeDefs.off_wait_time.name, + zigpy.zcl.clusters.general.OnOff.cluster_id, + entity_type=EntityType.CONFIG, + translation_key="invalid", + device_class="invalid", + ) + + +@pytest.mark.parametrize( + ("augment_method", "validate_method", "expected_exception_string"), + [ + ( + bad_translation_key, + validate_translation_keys, + "Missing translation key: missing_translation_key", + ), + ( + bad_device_class_unit_combination, + validate_device_class_unit, + "cannot have both unit and device_class", + ), + ( + bad_device_class_translation_key_usage, + validate_translation_keys_device_class, + "cannot have both a translation_key and a device_class", + ), + ], +) +async def test_quirks_v2_metadata_errors( + hass: HomeAssistant, + zigpy_device_mock, + zha_device_joined, + augment_method: Callable[[QuirksV2RegistryEntry], QuirksV2RegistryEntry], + validate_method: Callable, + expected_exception_string: str, +) -> None: + """Ensure all v2 quirks translation keys exist.""" + + # no error yet + validate_metadata(validate_method) + + # ensure the error is caught and raised + with pytest.raises(ValueError, match=expected_exception_string): + try: + # introduce an error + zigpy_device = _get_test_device( + zigpy_device_mock, + "Ikea of Sweden4", + "TRADFRI remote control4", + augment_method=augment_method, + ) + await zha_device_joined(zigpy_device) + + validate_metadata(validate_method) + # if the device was created we remove it + # so we don't pollute the rest of the tests + zigpy.quirks._DEVICE_REGISTRY.remove(zigpy_device) + except ValueError as e: + # if the device was not created we remove it + # so we don't pollute the rest of the tests + zigpy.quirks._DEVICE_REGISTRY._registry_v2.pop( + ( + "Ikea of Sweden4", + "TRADFRI remote control4", + ) + ) + raise e + + +class BadDeviceClass(enum.Enum): + """Bad device class.""" + + BAD = "bad" + + +def bad_binary_sensor_device_class( + v2_quirk: QuirksV2RegistryEntry, +) -> QuirksV2RegistryEntry: + """Introduce a bad device class on a binary sensor.""" + + return v2_quirk.binary_sensor( + zigpy.zcl.clusters.general.OnOff.AttributeDefs.on_off.name, + zigpy.zcl.clusters.general.OnOff.cluster_id, + device_class=BadDeviceClass.BAD, + ) + + +def bad_sensor_device_class( + v2_quirk: QuirksV2RegistryEntry, +) -> QuirksV2RegistryEntry: + """Introduce a bad device class on a sensor.""" + + return v2_quirk.sensor( + zigpy.zcl.clusters.general.OnOff.AttributeDefs.off_wait_time.name, + zigpy.zcl.clusters.general.OnOff.cluster_id, + device_class=BadDeviceClass.BAD, + ) + + +def bad_number_device_class( + v2_quirk: QuirksV2RegistryEntry, +) -> QuirksV2RegistryEntry: + """Introduce a bad device class on a number.""" + + return v2_quirk.number( + zigpy.zcl.clusters.general.OnOff.AttributeDefs.on_time.name, + zigpy.zcl.clusters.general.OnOff.cluster_id, + device_class=BadDeviceClass.BAD, + ) + + +ERROR_ROOT = "Quirks provided an invalid device class" + + +@pytest.mark.parametrize( + ("augment_method", "expected_exception_string"), + [ + ( + bad_binary_sensor_device_class, + f"{ERROR_ROOT}: BadDeviceClass.BAD for platform binary_sensor", + ), + ( + bad_sensor_device_class, + f"{ERROR_ROOT}: BadDeviceClass.BAD for platform sensor", + ), + ( + bad_number_device_class, + f"{ERROR_ROOT}: BadDeviceClass.BAD for platform number", + ), + ], +) +async def test_quirks_v2_metadata_bad_device_classes( + hass: HomeAssistant, + zigpy_device_mock, + zha_device_joined, + caplog: pytest.LogCaptureFixture, + augment_method: Callable[[QuirksV2RegistryEntry], QuirksV2RegistryEntry], + expected_exception_string: str, +) -> None: + """Test bad quirks v2 device classes.""" + + # introduce an error + zigpy_device = _get_test_device( + zigpy_device_mock, + "Ikea of Sweden4", + "TRADFRI remote control4", + augment_method=augment_method, + ) + await zha_device_joined(zigpy_device) + + assert expected_exception_string in caplog.text + + # remove the device so we don't pollute the rest of the tests + zigpy.quirks._DEVICE_REGISTRY.remove(zigpy_device) diff --git a/tests/components/zha/test_fan.py b/tests/components/zha/test_fan.py index 51b1cd9160c..5ed7c7bfeed 100644 --- a/tests/components/zha/test_fan.py +++ b/tests/components/zha/test_fan.py @@ -1,4 +1,5 @@ """Test ZHA fan.""" + from unittest.mock import AsyncMock, call, patch import pytest @@ -463,13 +464,13 @@ async def test_zha_group_fan_entity_failure_state( @pytest.mark.parametrize( ("plug_read", "expected_state", "expected_percentage"), - ( + [ (None, STATE_OFF, None), ({"fan_mode": 0}, STATE_OFF, 0), ({"fan_mode": 1}, STATE_ON, 33), ({"fan_mode": 2}, STATE_ON, 66), ({"fan_mode": 3}, STATE_ON, 100), - ), + ], ) async def test_fan_init( hass: HomeAssistant, @@ -644,7 +645,7 @@ async def test_fan_ikea( "ikea_expected_percentage", "ikea_preset_mode", ), - ( + [ (None, STATE_OFF, None, None), ({"fan_mode": 0}, STATE_OFF, 0, None), ({"fan_mode": 1}, STATE_ON, 10, PRESET_MODE_AUTO), @@ -657,7 +658,7 @@ async def test_fan_ikea( ({"fan_mode": 40}, STATE_ON, 80, "Speed 4"), ({"fan_mode": 45}, STATE_ON, 90, "Speed 4.5"), ({"fan_mode": 50}, STATE_ON, 100, "Speed 5"), - ), + ], ) async def test_fan_ikea_init( hass: HomeAssistant, @@ -827,7 +828,7 @@ async def test_fan_kof( @pytest.mark.parametrize( ("plug_read", "expected_state", "expected_percentage", "expected_preset"), - ( + [ (None, STATE_OFF, None, None), ({"fan_mode": 0}, STATE_OFF, 0, None), ({"fan_mode": 1}, STATE_ON, 25, None), @@ -835,7 +836,7 @@ async def test_fan_kof( ({"fan_mode": 3}, STATE_ON, 75, None), ({"fan_mode": 4}, STATE_ON, 100, None), ({"fan_mode": 6}, STATE_ON, None, PRESET_MODE_SMART), - ), + ], ) async def test_fan_kof_init( hass: HomeAssistant, diff --git a/tests/components/zha/test_gateway.py b/tests/components/zha/test_gateway.py index e4d8d2a5d65..182cc2c4752 100644 --- a/tests/components/zha/test_gateway.py +++ b/tests/components/zha/test_gateway.py @@ -1,4 +1,5 @@ """Test ZHA Gateway.""" + import asyncio from unittest.mock import MagicMock, PropertyMock, patch @@ -263,7 +264,7 @@ async def test_gateway_initialize_bellows_thread( ) as mock_new: await zha_gateway.async_initialize() - mock_new.mock_calls[-1].kwargs["config"]["use_thread"] is thread_state + assert mock_new.mock_calls[-1].kwargs["config"]["use_thread"] is thread_state await zha_gateway.shutdown() diff --git a/tests/components/zha/test_helpers.py b/tests/components/zha/test_helpers.py index 7e3c4a40872..fed8fe5bb91 100644 --- a/tests/components/zha/test_helpers.py +++ b/tests/components/zha/test_helpers.py @@ -1,10 +1,13 @@ """Tests for ZHA helpers.""" + +import enum import logging from unittest.mock import patch import pytest import voluptuous_serialize import zigpy.profiles.zha as zha +from zigpy.quirks.v2.homeassistant import UnitOfPower as QuirksUnitOfPower from zigpy.types.basic import uint16_t import zigpy.zcl.clusters.general as general import zigpy.zcl.clusters.lighting as lighting @@ -12,8 +15,9 @@ import zigpy.zcl.clusters.lighting as lighting from homeassistant.components.zha.core.helpers import ( cluster_command_schema_to_vol_schema, convert_to_zcl_values, + validate_unit, ) -from homeassistant.const import Platform +from homeassistant.const import Platform, UnitOfPower from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -39,7 +43,7 @@ def light_platform_only(): @pytest.fixture -async def device_light(hass, zigpy_device_mock, zha_device_joined): +async def device_light(hass: HomeAssistant, zigpy_device_mock, zha_device_joined): """Test light.""" zigpy_device = zigpy_device_mock( @@ -210,3 +214,25 @@ async def test_zcl_schema_conversions(hass: HomeAssistant, device_light) -> None # No flags are passed through assert converted_data["update_flags"] == 0 + + +def test_unit_validation() -> None: + """Test unit validation.""" + + assert validate_unit(QuirksUnitOfPower.WATT) == UnitOfPower.WATT + + class FooUnit(enum.Enum): + """Foo unit.""" + + BAR = "bar" + + class UnitOfMass(enum.Enum): + """UnitOfMass.""" + + BAR = "bar" + + with pytest.raises(KeyError): + validate_unit(FooUnit.BAR) + + with pytest.raises(ValueError): + validate_unit(UnitOfMass.BAR) diff --git a/tests/components/zha/test_init.py b/tests/components/zha/test_init.py index 5887fa2d8bc..99d6a78924b 100644 --- a/tests/components/zha/test_init.py +++ b/tests/components/zha/test_init.py @@ -1,4 +1,5 @@ """Tests for ZHA integration init.""" + import asyncio import typing from unittest.mock import AsyncMock, Mock, patch @@ -51,7 +52,7 @@ def config_entry_v1(hass): ) -@pytest.mark.parametrize("config", ({}, {DOMAIN: {}})) +@pytest.mark.parametrize("config", [{}, {DOMAIN: {}}]) @patch("homeassistant.components.zha.async_setup_entry", AsyncMock(return_value=True)) async def test_migration_from_v1_no_baudrate( hass: HomeAssistant, config_entry_v1, config @@ -105,12 +106,12 @@ async def test_migration_from_v1_wrong_baudrate( ) @pytest.mark.parametrize( "zha_config", - ( + [ {}, {CONF_USB_PATH: "str"}, {CONF_RADIO_TYPE: "ezsp"}, {CONF_RADIO_TYPE: "ezsp", CONF_USB_PATH: "str"}, - ), + ], ) async def test_config_depreciation(hass: HomeAssistant, zha_config) -> None: """Test config option depreciation.""" diff --git a/tests/components/zha/test_light.py b/tests/components/zha/test_light.py index bd799187a19..a6473c6007c 100644 --- a/tests/components/zha/test_light.py +++ b/tests/components/zha/test_light.py @@ -1,4 +1,5 @@ """Test ZHA light.""" + from datetime import timedelta from unittest.mock import AsyncMock, call, patch, sentinel diff --git a/tests/components/zha/test_lock.py b/tests/components/zha/test_lock.py index 2f1ecb6983d..52b1d891dfd 100644 --- a/tests/components/zha/test_lock.py +++ b/tests/components/zha/test_lock.py @@ -1,4 +1,5 @@ """Test ZHA lock.""" + from unittest.mock import patch import pytest diff --git a/tests/components/zha/test_logbook.py b/tests/components/zha/test_logbook.py index 44495cf0e15..889c73362ae 100644 --- a/tests/components/zha/test_logbook.py +++ b/tests/components/zha/test_logbook.py @@ -1,4 +1,5 @@ """ZHA logbook describe events tests.""" + from unittest.mock import patch import pytest @@ -84,6 +85,7 @@ async def test_zha_logbook_event_device_with_triggers( hass.config.components.add("recorder") assert await async_setup_component(hass, "logbook", {}) + await hass.async_block_till_done() events = mock_humanify( hass, @@ -162,6 +164,7 @@ async def test_zha_logbook_event_device_no_triggers( hass.config.components.add("recorder") assert await async_setup_component(hass, "logbook", {}) + await hass.async_block_till_done() events = mock_humanify( hass, @@ -246,6 +249,7 @@ async def test_zha_logbook_event_device_no_device( hass.config.components.add("recorder") assert await async_setup_component(hass, "logbook", {}) + await hass.async_block_till_done() events = mock_humanify( hass, diff --git a/tests/components/zha/test_number.py b/tests/components/zha/test_number.py index 3d888a57a28..a9fb3dd9509 100644 --- a/tests/components/zha/test_number.py +++ b/tests/components/zha/test_number.py @@ -1,4 +1,5 @@ """Test ZHA analog output.""" + from unittest.mock import call, patch import pytest @@ -191,14 +192,14 @@ async def test_number( @pytest.mark.parametrize( ("attr", "initial_value", "new_value"), - ( + [ ("on_off_transition_time", 20, 5), ("on_level", 255, 50), ("on_transition_time", 5, 1), ("off_transition_time", 5, 1), ("default_move_rate", 1, 5), ("start_up_current_level", 254, 125), - ), + ], ) async def test_level_control_number( hass: HomeAssistant, @@ -323,7 +324,7 @@ async def test_level_control_number( @pytest.mark.parametrize( ("attr", "initial_value", "new_value"), - (("start_up_color_temperature", 500, 350),), + [("start_up_color_temperature", 500, 350)], ) async def test_color_number( hass: HomeAssistant, diff --git a/tests/components/zha/test_radio_manager.py b/tests/components/zha/test_radio_manager.py index 5671c9cd465..dbea454ecb0 100644 --- a/tests/components/zha/test_radio_manager.py +++ b/tests/components/zha/test_radio_manager.py @@ -436,12 +436,15 @@ def zha_radio_manager(hass: HomeAssistant) -> ZhaRadioManager: async def test_detect_radio_type_success(radio_manager: ZhaRadioManager) -> None: """Test radio type detection, success.""" - with patch( - "bellows.zigbee.application.ControllerApplication.probe", return_value=False - ), patch( - # Intentionally probe only the second radio type - "zigpy_znp.zigbee.application.ControllerApplication.probe", - return_value=True, + with ( + patch( + "bellows.zigbee.application.ControllerApplication.probe", return_value=False + ), + patch( + # Intentionally probe only the second radio type + "zigpy_znp.zigbee.application.ControllerApplication.probe", + return_value=True, + ), ): assert ( await radio_manager.detect_radio_type() == ProbeResult.RADIO_TYPE_DETECTED @@ -453,11 +456,12 @@ async def test_detect_radio_type_failure_wrong_firmware( radio_manager: ZhaRadioManager, ) -> None: """Test radio type detection, wrong firmware.""" - with patch( - "homeassistant.components.zha.radio_manager.AUTOPROBE_RADIOS", () - ), patch( - "homeassistant.components.zha.radio_manager.repairs.wrong_silabs_firmware.warn_on_wrong_silabs_firmware", - return_value=True, + with ( + patch("homeassistant.components.zha.radio_manager.AUTOPROBE_RADIOS", ()), + patch( + "homeassistant.components.zha.radio_manager.repairs.wrong_silabs_firmware.warn_on_wrong_silabs_firmware", + return_value=True, + ), ): assert ( await radio_manager.detect_radio_type() @@ -470,11 +474,12 @@ async def test_detect_radio_type_failure_no_detect( radio_manager: ZhaRadioManager, ) -> None: """Test radio type detection, no firmware detected.""" - with patch( - "homeassistant.components.zha.radio_manager.AUTOPROBE_RADIOS", () - ), patch( - "homeassistant.components.zha.radio_manager.repairs.wrong_silabs_firmware.warn_on_wrong_silabs_firmware", - return_value=False, + with ( + patch("homeassistant.components.zha.radio_manager.AUTOPROBE_RADIOS", ()), + patch( + "homeassistant.components.zha.radio_manager.repairs.wrong_silabs_firmware.warn_on_wrong_silabs_firmware", + return_value=False, + ), ): assert await radio_manager.detect_radio_type() == ProbeResult.PROBING_FAILED assert radio_manager.radio_type is None diff --git a/tests/components/zha/test_registries.py b/tests/components/zha/test_registries.py index 80845cf9866..29020aa4313 100644 --- a/tests/components/zha/test_registries.py +++ b/tests/components/zha/test_registries.py @@ -1,4 +1,5 @@ """Test ZHA registries.""" + from __future__ import annotations import typing @@ -394,14 +395,14 @@ def entity_registry(): @pytest.mark.parametrize( ("manufacturer", "model", "quirk_id", "match_name"), - ( + [ ("random manufacturer", "random model", "random.class", "OnOff"), ("random manufacturer", MODEL, "random.class", "OnOffModel"), (MANUFACTURER, "random model", "random.class", "OnOffManufacturer"), ("random manufacturer", "random model", QUIRK_ID, "OnOffQuirk"), (MANUFACTURER, MODEL, "random.class", "OnOffModelManufacturer"), (MANUFACTURER, "some model", "random.class", "OnOffMultimodel"), - ), + ], ) def test_weighted_match( cluster_handler, diff --git a/tests/components/zha/test_repairs.py b/tests/components/zha/test_repairs.py index 0efff5ecb52..fea68be86cb 100644 --- a/tests/components/zha/test_repairs.py +++ b/tests/components/zha/test_repairs.py @@ -1,4 +1,5 @@ """Test ZHA repairs.""" + from collections.abc import Callable from http import HTTPStatus import logging @@ -37,6 +38,7 @@ from tests.common import MockConfigEntry from tests.typing import ClientSessionGenerator SKYCONNECT_DEVICE = "/dev/serial/by-id/usb-Nabu_Casa_SkyConnect_v1.0_9e2adbd75b8beb119fe564a0f320645d-if00-port0" +CONNECT_ZBT1_DEVICE = "/dev/serial/by-id/usb-Nabu_Casa_Home_Assistant_Connect_ZBT-1_9e2adbd75b8beb119fe564a0f320645d-if00-port0" def set_flasher_app_type(app_type: ApplicationType) -> Callable[[Flasher], None]: @@ -65,6 +67,22 @@ def test_detect_radio_hardware(hass: HomeAssistant) -> None: ) skyconnect_config_entry.add_to_hass(hass) + connect_zbt1_config_entry = MockConfigEntry( + data={ + "device": CONNECT_ZBT1_DEVICE, + "vid": "10C4", + "pid": "EA60", + "serial_number": "3c0ed67c628beb11b1cd64a0f320645d", + "manufacturer": "Nabu Casa", + "description": "Home Assistant Connect ZBT-1", + }, + domain=SKYCONNECT_DOMAIN, + options={}, + title="Home Assistant Connect ZBT-1", + ) + connect_zbt1_config_entry.add_to_hass(hass) + + assert _detect_radio_hardware(hass, CONNECT_ZBT1_DEVICE) == HardwareType.SKYCONNECT assert _detect_radio_hardware(hass, SKYCONNECT_DEVICE) == HardwareType.SKYCONNECT assert ( _detect_radio_hardware(hass, SKYCONNECT_DEVICE + "_foo") == HardwareType.OTHER @@ -85,12 +103,15 @@ def test_detect_radio_hardware(hass: HomeAssistant) -> None: def test_detect_radio_hardware_failure(hass: HomeAssistant) -> None: """Test radio hardware detection failure.""" - with patch( - "homeassistant.components.homeassistant_yellow.hardware.async_info", - side_effect=HomeAssistantError(), - ), patch( - "homeassistant.components.homeassistant_sky_connect.hardware.async_info", - side_effect=HomeAssistantError(), + with ( + patch( + "homeassistant.components.homeassistant_yellow.hardware.async_info", + side_effect=HomeAssistantError(), + ), + patch( + "homeassistant.components.homeassistant_sky_connect.hardware.async_info", + side_effect=HomeAssistantError(), + ), ): assert _detect_radio_hardware(hass, SKYCONNECT_DEVICE) == HardwareType.OTHER @@ -115,16 +136,20 @@ async def test_multipan_firmware_repair( config_entry.add_to_hass(hass) # ZHA fails to set up - with patch( - "homeassistant.components.zha.repairs.wrong_silabs_firmware.Flasher.probe_app_type", - side_effect=set_flasher_app_type(ApplicationType.CPC), - autospec=True, - ), patch( - "homeassistant.components.zha.core.gateway.ZHAGateway.async_initialize", - side_effect=RuntimeError(), - ), patch( - "homeassistant.components.zha.repairs.wrong_silabs_firmware._detect_radio_hardware", - return_value=detected_hardware, + with ( + patch( + "homeassistant.components.zha.repairs.wrong_silabs_firmware.Flasher.probe_app_type", + side_effect=set_flasher_app_type(ApplicationType.CPC), + autospec=True, + ), + patch( + "homeassistant.components.zha.core.gateway.ZHAGateway.async_initialize", + side_effect=RuntimeError(), + ), + patch( + "homeassistant.components.zha.repairs.wrong_silabs_firmware._detect_radio_hardware", + return_value=detected_hardware, + ), ): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -164,13 +189,16 @@ async def test_multipan_firmware_no_repair_on_probe_failure( config_entry.add_to_hass(hass) # ZHA fails to set up - with patch( - "homeassistant.components.zha.repairs.wrong_silabs_firmware.Flasher.probe_app_type", - side_effect=set_flasher_app_type(None), - autospec=True, - ), patch( - "homeassistant.components.zha.core.gateway.ZHAGateway.async_initialize", - side_effect=RuntimeError(), + with ( + patch( + "homeassistant.components.zha.repairs.wrong_silabs_firmware.Flasher.probe_app_type", + side_effect=set_flasher_app_type(None), + autospec=True, + ), + patch( + "homeassistant.components.zha.core.gateway.ZHAGateway.async_initialize", + side_effect=RuntimeError(), + ), ): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -198,13 +226,16 @@ async def test_multipan_firmware_retry_on_probe_ezsp( config_entry.add_to_hass(hass) # ZHA fails to set up - with patch( - "homeassistant.components.zha.repairs.wrong_silabs_firmware.Flasher.probe_app_type", - side_effect=set_flasher_app_type(ApplicationType.EZSP), - autospec=True, - ), patch( - "homeassistant.components.zha.core.gateway.ZHAGateway.async_initialize", - side_effect=RuntimeError(), + with ( + patch( + "homeassistant.components.zha.repairs.wrong_silabs_firmware.Flasher.probe_app_type", + side_effect=set_flasher_app_type(ApplicationType.EZSP), + autospec=True, + ), + patch( + "homeassistant.components.zha.core.gateway.ZHAGateway.async_initialize", + side_effect=RuntimeError(), + ), ): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -236,10 +267,13 @@ async def test_no_warn_on_socket(hass: HomeAssistant) -> None: async def test_probe_failure_exception_handling(caplog) -> None: """Test that probe failures are handled gracefully.""" - with patch( - "homeassistant.components.zha.repairs.wrong_silabs_firmware.Flasher.probe_app_type", - side_effect=RuntimeError(), - ), caplog.at_level(logging.DEBUG): + with ( + patch( + "homeassistant.components.zha.repairs.wrong_silabs_firmware.Flasher.probe_app_type", + side_effect=RuntimeError(), + ), + caplog.at_level(logging.DEBUG), + ): await probe_silabs_firmware_type("/dev/ttyZigbee") assert "Failed to probe application type" in caplog.text diff --git a/tests/components/zha/test_select.py b/tests/components/zha/test_select.py index 549a123aefb..bb1c5ca270a 100644 --- a/tests/components/zha/test_select.py +++ b/tests/components/zha/test_select.py @@ -1,4 +1,5 @@ """Test ZHA select entities.""" + from unittest.mock import call, patch import pytest @@ -434,7 +435,7 @@ async def test_on_off_select_attribute_report( "motion_sensitivity_disabled", AqaraMotionSensitivities, MotionSensitivityQuirk.OppleCluster.cluster_id, - translation_key="motion_sensitivity_translation_key", + translation_key="motion_sensitivity", initially_disabled=True, ) ) @@ -490,9 +491,8 @@ async def test_on_off_select_attribute_report_v2( assert hass.states.get(entity_id).state == AqaraMotionSensitivities.Low.name entity_registry = er.async_get(hass) - # none in id because the translation key does not exist - entity_entry = entity_registry.async_get("select.fake_manufacturer_fake_model_none") + entity_entry = entity_registry.async_get(entity_id) assert entity_entry assert entity_entry.entity_category == EntityCategory.CONFIG - assert entity_entry.disabled is True - assert entity_entry.translation_key == "motion_sensitivity_translation_key" + assert entity_entry.disabled is False + assert entity_entry.translation_key == "motion_sensitivity" diff --git a/tests/components/zha/test_sensor.py b/tests/components/zha/test_sensor.py index a7047b8dcd4..8d0ef8107e3 100644 --- a/tests/components/zha/test_sensor.py +++ b/tests/components/zha/test_sensor.py @@ -1,4 +1,5 @@ """Test ZHA sensor.""" + from datetime import timedelta import math from unittest.mock import MagicMock, patch @@ -377,7 +378,7 @@ async def async_test_pi_heating_demand(hass, cluster, entity_id): "unsupported_attrs", "initial_sensor_state", ), - ( + [ ( measurement.RelativeHumidity.cluster_id, "humidity", @@ -562,7 +563,7 @@ async def async_test_pi_heating_demand(hass, cluster, entity_id): None, STATE_UNKNOWN, ), - ), + ], ) async def test_sensor( hass: HomeAssistant, @@ -807,7 +808,7 @@ async def test_electrical_measurement_init( @pytest.mark.parametrize( ("cluster_id", "unsupported_attributes", "entity_ids", "missing_entity_ids"), - ( + [ ( homeautomation.ElectricalMeasurement.cluster_id, {"apparent_power", "rms_voltage", "rms_current"}, @@ -876,7 +877,7 @@ async def test_electrical_measurement_init( }, {}, ), - ), + ], ) async def test_unsupported_attributes_sensor( hass: HomeAssistant, @@ -918,7 +919,7 @@ async def test_unsupported_attributes_sensor( @pytest.mark.parametrize( ("raw_uom", "raw_value", "expected_state", "expected_uom"), - ( + [ ( 1, 12320, @@ -1003,7 +1004,7 @@ async def test_unsupported_attributes_sensor( "5.01", UnitOfVolume.LITERS, ), - ), + ], ) async def test_se_summation_uom( hass: HomeAssistant, @@ -1051,7 +1052,7 @@ async def test_se_summation_uom( @pytest.mark.parametrize( ("raw_measurement_type", "expected_type"), - ( + [ (1, "ACTIVE_MEASUREMENT"), (8, "PHASE_A_MEASUREMENT"), (9, "ACTIVE_MEASUREMENT, PHASE_A_MEASUREMENT"), @@ -1062,7 +1063,7 @@ async def test_se_summation_uom( " PHASE_A_MEASUREMENT" ), ), - ), + ], ) async def test_elec_measurement_sensor_type( hass: HomeAssistant, @@ -1095,9 +1096,9 @@ async def test_elec_measurement_sensor_polling( entity_id = ENTITY_ID_PREFIX.format("power") zigpy_dev = elec_measurement_zigpy_dev - zigpy_dev.endpoints[1].electrical_measurement.PLUGGED_ATTR_READS[ - "active_power" - ] = 20 + zigpy_dev.endpoints[1].electrical_measurement.PLUGGED_ATTR_READS["active_power"] = ( + 20 + ) await zha_device_joined_restored(zigpy_dev) @@ -1106,9 +1107,9 @@ async def test_elec_measurement_sensor_polling( assert state.state == "2.0" # update the value for the power reading - zigpy_dev.endpoints[1].electrical_measurement.PLUGGED_ATTR_READS[ - "active_power" - ] = 60 + zigpy_dev.endpoints[1].electrical_measurement.PLUGGED_ATTR_READS["active_power"] = ( + 60 + ) # ensure the state is still 2.0 state = hass.states.get(entity_id) @@ -1117,7 +1118,7 @@ async def test_elec_measurement_sensor_polling( # let the polling happen future = dt_util.utcnow() + timedelta(seconds=90) async_fire_time_changed(hass, future) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) # ensure the state has been updated to 6.0 state = hass.states.get(entity_id) @@ -1126,7 +1127,7 @@ async def test_elec_measurement_sensor_polling( @pytest.mark.parametrize( "supported_attributes", - ( + [ set(), { "active_power", @@ -1151,7 +1152,7 @@ async def test_elec_measurement_sensor_polling( "rms_voltage", "rms_voltage_max", }, - ), + ], ) async def test_elec_measurement_skip_unsupported_attribute( hass: HomeAssistant, @@ -1259,10 +1260,10 @@ async def test_last_feeding_size_sensor_v2( assert entity_id is not None await send_attributes_report(hass, cluster, {0x010C: 1}) - assert_state(hass, entity_id, "1.0", UnitOfMass.GRAMS) + assert_state(hass, entity_id, "1.0", UnitOfMass.GRAMS.value) await send_attributes_report(hass, cluster, {0x010C: 5}) - assert_state(hass, entity_id, "5.0", UnitOfMass.GRAMS) + assert_state(hass, entity_id, "5.0", UnitOfMass.GRAMS.value) @pytest.fixture diff --git a/tests/components/zha/test_silabs_multiprotocol.py b/tests/components/zha/test_silabs_multiprotocol.py index 074484e6d24..03c845269e0 100644 --- a/tests/components/zha/test_silabs_multiprotocol.py +++ b/tests/components/zha/test_silabs_multiprotocol.py @@ -1,4 +1,5 @@ """Test ZHA Silicon Labs Multiprotocol support.""" + from __future__ import annotations from typing import TYPE_CHECKING diff --git a/tests/components/zha/test_siren.py b/tests/components/zha/test_siren.py index b953d833330..f5486d91c0f 100644 --- a/tests/components/zha/test_siren.py +++ b/tests/components/zha/test_siren.py @@ -1,4 +1,5 @@ """Test zha siren.""" + from datetime import timedelta from unittest.mock import ANY, call, patch @@ -85,13 +86,16 @@ async def test_siren(hass: HomeAssistant, siren) -> None: assert hass.states.get(entity_id).state == STATE_OFF # turn on from HA - with patch( - "zigpy.device.Device.request", - return_value=[0x00, zcl_f.Status.SUCCESS], - ), patch( - "zigpy.zcl.Cluster.request", - side_effect=zigpy.zcl.Cluster.request, - autospec=True, + with ( + patch( + "zigpy.device.Device.request", + return_value=[0x00, zcl_f.Status.SUCCESS], + ), + patch( + "zigpy.zcl.Cluster.request", + side_effect=zigpy.zcl.Cluster.request, + autospec=True, + ), ): # turn on via UI await hass.services.async_call( @@ -117,13 +121,16 @@ async def test_siren(hass: HomeAssistant, siren) -> None: assert hass.states.get(entity_id).state == STATE_ON # turn off from HA - with patch( - "zigpy.device.Device.request", - return_value=[0x01, zcl_f.Status.SUCCESS], - ), patch( - "zigpy.zcl.Cluster.request", - side_effect=zigpy.zcl.Cluster.request, - autospec=True, + with ( + patch( + "zigpy.device.Device.request", + return_value=[0x01, zcl_f.Status.SUCCESS], + ), + patch( + "zigpy.zcl.Cluster.request", + side_effect=zigpy.zcl.Cluster.request, + autospec=True, + ), ): # turn off via UI await hass.services.async_call( @@ -149,13 +156,16 @@ async def test_siren(hass: HomeAssistant, siren) -> None: assert hass.states.get(entity_id).state == STATE_OFF # turn on from HA - with patch( - "zigpy.device.Device.request", - return_value=[0x00, zcl_f.Status.SUCCESS], - ), patch( - "zigpy.zcl.Cluster.request", - side_effect=zigpy.zcl.Cluster.request, - autospec=True, + with ( + patch( + "zigpy.device.Device.request", + return_value=[0x00, zcl_f.Status.SUCCESS], + ), + patch( + "zigpy.zcl.Cluster.request", + side_effect=zigpy.zcl.Cluster.request, + autospec=True, + ), ): # turn on via UI await hass.services.async_call( diff --git a/tests/components/zha/test_switch.py b/tests/components/zha/test_switch.py index 9a9fbc2b50e..644062198f9 100644 --- a/tests/components/zha/test_switch.py +++ b/tests/components/zha/test_switch.py @@ -1,4 +1,5 @@ """Test ZHA switch.""" + from unittest.mock import AsyncMock, call, patch import pytest diff --git a/tests/components/zha/test_update.py b/tests/components/zha/test_update.py index 29be109c673..854c08985ac 100644 --- a/tests/components/zha/test_update.py +++ b/tests/components/zha/test_update.py @@ -1,4 +1,5 @@ """Test ZHA firmware updates.""" + from unittest.mock import AsyncMock, call, patch import pytest @@ -515,10 +516,13 @@ async def test_firmware_update_raises( blocking=True, ) - with patch( - "zigpy.device.Device.update_firmware", - AsyncMock(side_effect=DeliveryError("failed to deliver")), - ), pytest.raises(HomeAssistantError): + with ( + patch( + "zigpy.device.Device.update_firmware", + AsyncMock(side_effect=DeliveryError("failed to deliver")), + ), + pytest.raises(HomeAssistantError), + ): await hass.services.async_call( UPDATE_DOMAIN, SERVICE_INSTALL, diff --git a/tests/components/zha/test_websocket_api.py b/tests/components/zha/test_websocket_api.py index bafea7e1965..9cd475a7bf8 100644 --- a/tests/components/zha/test_websocket_api.py +++ b/tests/components/zha/test_websocket_api.py @@ -1,4 +1,5 @@ """Test ZHA WebSocket API.""" + from __future__ import annotations from binascii import unhexlify @@ -498,7 +499,7 @@ async def app_controller( @pytest.mark.parametrize( ("params", "duration", "node"), - ( + [ ({}, 60, None), ({ATTR_DURATION: 30}, 30, None), ( @@ -511,7 +512,7 @@ async def app_controller( 60, zigpy.types.EUI64.convert("aa:bb:cc:dd:aa:bb:cc:d1"), ), - ), + ], ) async def test_permit_ha12( hass: HomeAssistant, @@ -703,7 +704,13 @@ async def test_ws_permit_with_qr_code( {ID: 14, TYPE: f"{DOMAIN}/devices/{SERVICE_PERMIT}", **params} ) - msg = await zha_client.receive_json() + msg_type = None + while msg_type != const.TYPE_RESULT: + # There will be logging events coming over the websocket + # as well so we want to ignore those + msg = await zha_client.receive_json() + msg_type = msg["type"] + assert msg["id"] == 14 assert msg["type"] == const.TYPE_RESULT assert msg["success"] @@ -736,7 +743,7 @@ async def test_ws_permit_with_install_code_fail( @pytest.mark.parametrize( ("params", "duration", "node"), - ( + [ ({}, 60, None), ({ATTR_DURATION: 30}, 30, None), ( @@ -749,7 +756,7 @@ async def test_ws_permit_with_install_code_fail( 60, zigpy.types.EUI64.convert("aa:bb:cc:dd:aa:bb:cc:d1"), ), - ), + ], ) async def test_ws_permit_ha12( app_controller: ControllerApplication, zha_client, params, duration, node @@ -760,7 +767,13 @@ async def test_ws_permit_ha12( {ID: 14, TYPE: f"{DOMAIN}/devices/{SERVICE_PERMIT}", **params} ) - msg = await zha_client.receive_json() + msg_type = None + while msg_type != const.TYPE_RESULT: + # There will be logging events coming over the websocket + # as well so we want to ignore those + msg = await zha_client.receive_json() + msg_type = msg["type"] + assert msg["id"] == 14 assert msg["type"] == const.TYPE_RESULT assert msg["success"] @@ -925,7 +938,7 @@ async def test_websocket_change_channel( assert msg["type"] == const.TYPE_RESULT assert msg["success"] - change_channel_mock.mock_calls == [call(ANY, new_channel)] + change_channel_mock.assert_has_calls([call(ANY, new_channel)]) @pytest.mark.parametrize( diff --git a/tests/components/zodiac/test_config_flow.py b/tests/components/zodiac/test_config_flow.py index 4b5baefb0f2..15e8bb04ef6 100644 --- a/tests/components/zodiac/test_config_flow.py +++ b/tests/components/zodiac/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for the Zodiac config flow.""" + from unittest.mock import patch import pytest diff --git a/tests/components/zodiac/test_sensor.py b/tests/components/zodiac/test_sensor.py index 9fa151c87d5..3d43fe60a5a 100644 --- a/tests/components/zodiac/test_sensor.py +++ b/tests/components/zodiac/test_sensor.py @@ -1,4 +1,5 @@ """The test for the zodiac sensor platform.""" + from datetime import datetime from unittest.mock import patch diff --git a/tests/components/zone/test_init.py b/tests/components/zone/test_init.py index 2924e6654e2..08e96c104d2 100644 --- a/tests/components/zone/test_init.py +++ b/tests/components/zone/test_init.py @@ -1,4 +1,5 @@ """Test zone component.""" + from unittest.mock import patch import pytest diff --git a/tests/components/zone/test_trigger.py b/tests/components/zone/test_trigger.py index 7f44cecefe1..8987481f460 100644 --- a/tests/components/zone/test_trigger.py +++ b/tests/components/zone/test_trigger.py @@ -1,4 +1,5 @@ """The tests for the location automation.""" + import pytest from homeassistant.components import automation, zone diff --git a/tests/components/zwave_js/common.py b/tests/components/zwave_js/common.py index f4d7ea0a754..64bc981de11 100644 --- a/tests/components/zwave_js/common.py +++ b/tests/components/zwave_js/common.py @@ -1,4 +1,5 @@ """Provide common test tools for Z-Wave JS.""" + from __future__ import annotations from copy import deepcopy diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index f2c3abd362a..98453071bc1 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -1,4 +1,5 @@ """Provide common Z-Wave JS fixtures.""" + import asyncio import copy import io diff --git a/tests/components/zwave_js/fixtures/leviton_zw4sf_state.json b/tests/components/zwave_js/fixtures/leviton_zw4sf_state.json index 28b59a0b844..70af9c90a34 100644 --- a/tests/components/zwave_js/fixtures/leviton_zw4sf_state.json +++ b/tests/components/zwave_js/fixtures/leviton_zw4sf_state.json @@ -35,7 +35,7 @@ }, "metadata": { "inclusion": "Classic Inclusion To A Z-Wave Network\nFor older controllers Classic Inclusion is supported. Depending on the age of the controller the controller will need to be 3 to 35 feet from the device when including.\n1. To enter programming mode, hold the button for 7 seconds. The status light will turn amber, release and the status light will blink.\n2. Follow the Z-Wave controller instructions to enter inclusion mode.\n3. Tap the top or the paddle of the paddle one time. The status light will quickly flash green.\n4. The Z-Wave controller will confirm successful inclusion to the network", - "exclusion": "Exclusion From A Z-Wave Network\nWhen removing an fan speed controller from a Z-Wave network,\nbest practice is to use the exclusion command found in the Z-Wave\ncontroller.\n1. To enter programming mode, hold the button for 7 seconds. The\nstatus light will turn amber, release and the status light will blink.\n2. Follow Z-Wave controller directions to enter exclusion mode\n3. Tap the the top of the paddle 1 time. The status light will quickly\nflash green.\n4. The Z-Wave controller will remove the device from the network", + "exclusion": "Exclusion From A Z-Wave Network\nWhen removing an fan speed controller from a Z-Wave network,\nbest practice is to use the exclusion command found in the Z-Wave\ncontroller.\n1. To enter programming mode, hold the button for 7 seconds. The\nstatus light will turn amber, release and the status light will blink.\n2. Follow Z-Wave controller directions to enter exclusion mode\n3. Tap the top of the paddle 1 time. The status light will quickly\nflash green.\n4. The Z-Wave controller will remove the device from the network", "reset": "Factory Default\nWhen removing a fan speed controller from a network it is best\npractice to use the exclusion process. In situations where a device\nneeds to be returned to factory default follow the following steps. A\nreset should only be used when a controller is\ninoperable or missing.\n1. Hold the top of the paddle for 7 seconds, the status light will turn amber.\nContinue holding the top paddle for another 7 seconds (total of 14 seconds).\nThe status light will quickly flash red/ amber.\n2. Release the top of the paddle and the device will reset", "manual": "https://products.z-wavealliance.org/ProductManual/File?folder=&filename=product_documents/3832/Draft%20ZW4SF%203-25-20.pdf" } diff --git a/tests/components/zwave_js/scripts/test_convert_device_diagnostics_to_fixture.py b/tests/components/zwave_js/scripts/test_convert_device_diagnostics_to_fixture.py index ee03d57f4c7..a5c5e4475ce 100644 --- a/tests/components/zwave_js/scripts/test_convert_device_diagnostics_to_fixture.py +++ b/tests/components/zwave_js/scripts/test_convert_device_diagnostics_to_fixture.py @@ -1,4 +1,5 @@ """Test convert_device_diagnostics_to_fixture script.""" + import copy import json from pathlib import Path @@ -74,9 +75,12 @@ def test_main(capfd: pytest.CaptureFixture[str]) -> None: # Check file dump args.append("--file") - with patch.object(sys, "argv", args), patch( - "homeassistant.components.zwave_js.scripts.convert_device_diagnostics_to_fixture.Path.write_text" - ) as write_text_mock: + with ( + patch.object(sys, "argv", args), + patch( + "homeassistant.components.zwave_js.scripts.convert_device_diagnostics_to_fixture.Path.write_text" + ) as write_text_mock, + ): main() assert len(write_text_mock.call_args_list) == 1 diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index bf5ad88447e..6295dbed8f1 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -1,4 +1,5 @@ """Test the Z-Wave JS Websocket API.""" + from copy import deepcopy from http import HTTPStatus import json @@ -3069,13 +3070,17 @@ async def test_firmware_upload_view( """Test the HTTP firmware upload view.""" client = await hass_client() device = get_device(hass, multisensor_6) - with patch( - "homeassistant.components.zwave_js.api.update_firmware", - ) as mock_node_cmd, patch( - "homeassistant.components.zwave_js.api.controller_firmware_update_otw", - ) as mock_controller_cmd, patch.dict( - "homeassistant.components.zwave_js.api.USER_AGENT", - {"HomeAssistant": "0.0.0"}, + with ( + patch( + "homeassistant.components.zwave_js.api.update_firmware", + ) as mock_node_cmd, + patch( + "homeassistant.components.zwave_js.api.controller_firmware_update_otw", + ) as mock_controller_cmd, + patch.dict( + "homeassistant.components.zwave_js.api.USER_AGENT", + {"HomeAssistant": "0.0.0"}, + ), ): data = {"file": firmware_file} data.update(firmware_data) @@ -3106,13 +3111,17 @@ async def test_firmware_upload_view_controller( """Test the HTTP firmware upload view for a controller.""" hass_client = await hass_client() device = get_device(hass, client.driver.controller.nodes[1]) - with patch( - "homeassistant.components.zwave_js.api.update_firmware", - ) as mock_node_cmd, patch( - "homeassistant.components.zwave_js.api.controller_firmware_update_otw", - ) as mock_controller_cmd, patch.dict( - "homeassistant.components.zwave_js.api.USER_AGENT", - {"HomeAssistant": "0.0.0"}, + with ( + patch( + "homeassistant.components.zwave_js.api.update_firmware", + ) as mock_node_cmd, + patch( + "homeassistant.components.zwave_js.api.controller_firmware_update_otw", + ) as mock_controller_cmd, + patch.dict( + "homeassistant.components.zwave_js.api.USER_AGENT", + {"HomeAssistant": "0.0.0"}, + ), ): resp = await hass_client.post( f"/api/zwave_js/firmware/upload/{device.id}", diff --git a/tests/components/zwave_js/test_binary_sensor.py b/tests/components/zwave_js/test_binary_sensor.py index a3ae9954d2f..3f78e23a50c 100644 --- a/tests/components/zwave_js/test_binary_sensor.py +++ b/tests/components/zwave_js/test_binary_sensor.py @@ -1,4 +1,5 @@ """Test the Z-Wave JS binary sensor platform.""" + from zwave_js_server.event import Event from zwave_js_server.model.node import Node @@ -118,9 +119,7 @@ async def test_disabled_legacy_sensor( assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION # Test enabling legacy entity - updated_entry = registry.async_update_entity( - entry.entity_id, **{"disabled_by": None} - ) + updated_entry = registry.async_update_entity(entry.entity_id, disabled_by=None) assert updated_entry != entry assert updated_entry.disabled is False @@ -273,7 +272,7 @@ async def test_config_parameter_binary_sensor( assert entity_entry.entity_category == EntityCategory.DIAGNOSTIC updated_entry = ent_reg.async_update_entity( - binary_sensor_entity_id, **{"disabled_by": None} + binary_sensor_entity_id, disabled_by=None ) assert updated_entry != entity_entry assert updated_entry.disabled is False diff --git a/tests/components/zwave_js/test_button.py b/tests/components/zwave_js/test_button.py index 68a8e07ffe4..e1a1c6d665a 100644 --- a/tests/components/zwave_js/test_button.py +++ b/tests/components/zwave_js/test_button.py @@ -1,4 +1,5 @@ """Test the Z-Wave JS button entities.""" + import pytest from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS diff --git a/tests/components/zwave_js/test_climate.py b/tests/components/zwave_js/test_climate.py index c96f1c48797..9a4559de1a5 100644 --- a/tests/components/zwave_js/test_climate.py +++ b/tests/components/zwave_js/test_climate.py @@ -1,4 +1,5 @@ """Test the Z-Wave JS climate platform.""" + import copy import pytest diff --git a/tests/components/zwave_js/test_config_flow.py b/tests/components/zwave_js/test_config_flow.py index e8d6b0fd44c..511fb8d7570 100644 --- a/tests/components/zwave_js/test_config_flow.py +++ b/tests/components/zwave_js/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Z-Wave JS config flow.""" + import asyncio from collections.abc import Generator from copy import copy @@ -113,13 +114,16 @@ def mock_get_server_version(server_version_side_effect, server_version_timeout): min_schema_version=0, max_schema_version=1, ) - with patch( - "homeassistant.components.zwave_js.config_flow.get_server_version", - side_effect=server_version_side_effect, - return_value=version_info, - ) as mock_version, patch( - "homeassistant.components.zwave_js.config_flow.SERVER_VERSION_TIMEOUT", - new=server_version_timeout, + with ( + patch( + "homeassistant.components.zwave_js.config_flow.get_server_version", + side_effect=server_version_side_effect, + return_value=version_info, + ) as mock_version, + patch( + "homeassistant.components.zwave_js.config_flow.SERVER_VERSION_TIMEOUT", + new=server_version_timeout, + ), ): yield mock_version @@ -191,12 +195,15 @@ async def test_manual(hass: HomeAssistant) -> None: ) assert result["type"] == "form" - with patch( - "homeassistant.components.zwave_js.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.zwave_js.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.zwave_js.async_setup", return_value=True + ) as mock_setup, + patch( + "homeassistant.components.zwave_js.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -347,12 +354,15 @@ async def test_supervisor_discovery( ), ) - with patch( - "homeassistant.components.zwave_js.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.zwave_js.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.zwave_js.async_setup", return_value=True + ) as mock_setup, + patch( + "homeassistant.components.zwave_js.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() @@ -439,12 +449,15 @@ async def test_clean_discovery_on_user_create( assert result["type"] == "form" assert result["step_id"] == "manual" - with patch( - "homeassistant.components.zwave_js.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.zwave_js.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.zwave_js.async_setup", return_value=True + ) as mock_setup, + patch( + "homeassistant.components.zwave_js.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -612,12 +625,15 @@ async def test_usb_discovery( assert result["type"] == "progress" assert result["step_id"] == "start_addon" - with patch( - "homeassistant.components.zwave_js.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.zwave_js.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.zwave_js.async_setup", return_value=True + ) as mock_setup, + patch( + "homeassistant.components.zwave_js.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): await hass.async_block_till_done() result = await hass.config_entries.flow.async_configure(result["flow_id"]) await hass.async_block_till_done() @@ -702,12 +718,15 @@ async def test_usb_discovery_addon_not_running( assert result["type"] == "progress" assert result["step_id"] == "start_addon" - with patch( - "homeassistant.components.zwave_js.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.zwave_js.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.zwave_js.async_setup", return_value=True + ) as mock_setup, + patch( + "homeassistant.components.zwave_js.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): await hass.async_block_till_done() result = await hass.config_entries.flow.async_configure(result["flow_id"]) await hass.async_block_till_done() @@ -788,12 +807,15 @@ async def test_discovery_addon_not_running( assert result["type"] == "progress" assert result["step_id"] == "start_addon" - with patch( - "homeassistant.components.zwave_js.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.zwave_js.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.zwave_js.async_setup", return_value=True + ) as mock_setup, + patch( + "homeassistant.components.zwave_js.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): await hass.async_block_till_done() result = await hass.config_entries.flow.async_configure(result["flow_id"]) await hass.async_block_till_done() @@ -882,12 +904,15 @@ async def test_discovery_addon_not_installed( assert result["type"] == "progress" assert result["step_id"] == "start_addon" - with patch( - "homeassistant.components.zwave_js.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.zwave_js.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.zwave_js.async_setup", return_value=True + ) as mock_setup, + patch( + "homeassistant.components.zwave_js.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): await hass.async_block_till_done() result = await hass.config_entries.flow.async_configure(result["flow_id"]) await hass.async_block_till_done() @@ -1016,12 +1041,15 @@ async def test_not_addon(hass: HomeAssistant, supervisor) -> None: assert result["type"] == "form" assert result["step_id"] == "manual" - with patch( - "homeassistant.components.zwave_js.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.zwave_js.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.zwave_js.async_setup", return_value=True + ) as mock_setup, + patch( + "homeassistant.components.zwave_js.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -1068,12 +1096,15 @@ async def test_addon_running( assert result["type"] == "form" assert result["step_id"] == "on_supervisor" - with patch( - "homeassistant.components.zwave_js.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.zwave_js.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.zwave_js.async_setup", return_value=True + ) as mock_setup, + patch( + "homeassistant.components.zwave_js.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"use_addon": True} ) @@ -1267,12 +1298,15 @@ async def test_addon_installed( assert result["type"] == "progress" assert result["step_id"] == "start_addon" - with patch( - "homeassistant.components.zwave_js.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.zwave_js.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.zwave_js.async_setup", return_value=True + ) as mock_setup, + patch( + "homeassistant.components.zwave_js.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): await hass.async_block_till_done() result = await hass.config_entries.flow.async_configure(result["flow_id"]) await hass.async_block_till_done() @@ -1644,12 +1678,15 @@ async def test_addon_not_installed( assert result["type"] == "progress" assert result["step_id"] == "start_addon" - with patch( - "homeassistant.components.zwave_js.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.zwave_js.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.zwave_js.async_setup", return_value=True + ) as mock_setup, + patch( + "homeassistant.components.zwave_js.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): await hass.async_block_till_done() result = await hass.config_entries.flow.async_configure(result["flow_id"]) await hass.async_block_till_done() @@ -2632,12 +2669,15 @@ async def test_import_addon_installed( assert result["type"] == "progress" assert result["step_id"] == "start_addon" - with patch( - "homeassistant.components.zwave_js.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.zwave_js.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.zwave_js.async_setup", return_value=True + ) as mock_setup, + patch( + "homeassistant.components.zwave_js.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): await hass.async_block_till_done() result = await hass.config_entries.flow.async_configure(result["flow_id"]) await hass.async_block_till_done() @@ -2680,12 +2720,15 @@ async def test_zeroconf(hass: HomeAssistant) -> None: assert result["type"] == "form" assert result["step_id"] == "zeroconf_confirm" - with patch( - "homeassistant.components.zwave_js.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.zwave_js.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with ( + patch( + "homeassistant.components.zwave_js.async_setup", return_value=True + ) as mock_setup, + patch( + "homeassistant.components.zwave_js.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() diff --git a/tests/components/zwave_js/test_config_validation.py b/tests/components/zwave_js/test_config_validation.py index b811ab7c10b..8428972bde1 100644 --- a/tests/components/zwave_js/test_config_validation.py +++ b/tests/components/zwave_js/test_config_validation.py @@ -1,4 +1,5 @@ """Test the Z-Wave JS config validation helpers.""" + import pytest import voluptuous as vol diff --git a/tests/components/zwave_js/test_cover.py b/tests/components/zwave_js/test_cover.py index 54be2b43765..4ecd697f4d1 100644 --- a/tests/components/zwave_js/test_cover.py +++ b/tests/components/zwave_js/test_cover.py @@ -1,4 +1,5 @@ """Test the Z-Wave JS cover platform.""" + import logging from zwave_js_server.const import ( diff --git a/tests/components/zwave_js/test_device_action.py b/tests/components/zwave_js/test_device_action.py index ce2b916b7a1..46686be0994 100644 --- a/tests/components/zwave_js/test_device_action.py +++ b/tests/components/zwave_js/test_device_action.py @@ -1,4 +1,5 @@ """The tests for Z-Wave JS device actions.""" + from unittest.mock import patch import pytest diff --git a/tests/components/zwave_js/test_device_condition.py b/tests/components/zwave_js/test_device_condition.py index f7aacec36ac..24f756c5042 100644 --- a/tests/components/zwave_js/test_device_condition.py +++ b/tests/components/zwave_js/test_device_condition.py @@ -1,4 +1,5 @@ """The tests for Z-Wave JS device conditions.""" + from __future__ import annotations from unittest.mock import patch @@ -626,12 +627,15 @@ async def test_failure_scenarios( hass, {"type": "failed.test", "device_id": device.id} ) - with patch( - "homeassistant.components.zwave_js.device_condition.async_get_node_from_device_id", - return_value=None, - ), patch( - "homeassistant.components.zwave_js.device_condition.get_zwave_value_from_config", - return_value=None, + with ( + patch( + "homeassistant.components.zwave_js.device_condition.async_get_node_from_device_id", + return_value=None, + ), + patch( + "homeassistant.components.zwave_js.device_condition.get_zwave_value_from_config", + return_value=None, + ), ): assert ( await device_condition.async_get_condition_capabilities( diff --git a/tests/components/zwave_js/test_device_trigger.py b/tests/components/zwave_js/test_device_trigger.py index f9615c84e1d..6818b2d73af 100644 --- a/tests/components/zwave_js/test_device_trigger.py +++ b/tests/components/zwave_js/test_device_trigger.py @@ -1,4 +1,5 @@ """The tests for Z-Wave JS device triggers.""" + from unittest.mock import patch import pytest @@ -340,7 +341,7 @@ async def test_get_node_status_triggers( entity_id = async_get_node_status_sensor_entity_id( hass, device.id, ent_reg, dev_reg ) - entity = ent_reg.async_update_entity(entity_id, **{"disabled_by": None}) + entity = ent_reg.async_update_entity(entity_id, disabled_by=None) await hass.config_entries.async_reload(integration.entry_id) await hass.async_block_till_done() @@ -372,7 +373,7 @@ async def test_if_node_status_change_fires( entity_id = async_get_node_status_sensor_entity_id( hass, device.id, ent_reg, dev_reg ) - entity = ent_reg.async_update_entity(entity_id, **{"disabled_by": None}) + entity = ent_reg.async_update_entity(entity_id, disabled_by=None) await hass.config_entries.async_reload(integration.entry_id) await hass.async_block_till_done() @@ -451,7 +452,7 @@ async def test_if_node_status_change_fires_legacy( entity_id = async_get_node_status_sensor_entity_id( hass, device.id, ent_reg, dev_reg ) - ent_reg.async_update_entity(entity_id, **{"disabled_by": None}) + ent_reg.async_update_entity(entity_id, disabled_by=None) await hass.config_entries.async_reload(integration.entry_id) await hass.async_block_till_done() @@ -529,7 +530,7 @@ async def test_get_trigger_capabilities_node_status( entity_id = async_get_node_status_sensor_entity_id( hass, device.id, ent_reg, dev_reg ) - ent_reg.async_update_entity(entity_id, **{"disabled_by": None}) + ent_reg.async_update_entity(entity_id, disabled_by=None) await hass.config_entries.async_reload(integration.entry_id) await hass.async_block_till_done() @@ -1570,12 +1571,15 @@ async def test_failure_scenarios( {}, ) - with patch( - "homeassistant.components.zwave_js.device_trigger.async_get_node_from_device_id", - return_value=None, - ), patch( - "homeassistant.components.zwave_js.helpers.get_zwave_value_from_config", - return_value=None, + with ( + patch( + "homeassistant.components.zwave_js.device_trigger.async_get_node_from_device_id", + return_value=None, + ), + patch( + "homeassistant.components.zwave_js.helpers.get_zwave_value_from_config", + return_value=None, + ), ): assert ( await device_trigger.async_get_trigger_capabilities( diff --git a/tests/components/zwave_js/test_diagnostics.py b/tests/components/zwave_js/test_diagnostics.py index 2510143695c..054906cd0f6 100644 --- a/tests/components/zwave_js/test_diagnostics.py +++ b/tests/components/zwave_js/test_diagnostics.py @@ -1,4 +1,5 @@ """Test the Z-Wave JS diagnostics.""" + import copy from unittest.mock import patch diff --git a/tests/components/zwave_js/test_discovery.py b/tests/components/zwave_js/test_discovery.py index 67f4a8d962f..fe231707629 100644 --- a/tests/components/zwave_js/test_discovery.py +++ b/tests/components/zwave_js/test_discovery.py @@ -1,4 +1,5 @@ """Test entity discovery for device-specific schemas for the Z-Wave JS integration.""" + import pytest from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN @@ -152,9 +153,7 @@ async def test_merten_507801_disabled_enitites( assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION # Test enabling entity - updated_entry = registry.async_update_entity( - entry.entity_id, **{"disabled_by": None} - ) + updated_entry = registry.async_update_entity(entry.entity_id, disabled_by=None) assert updated_entry != entry assert updated_entry.disabled is False diff --git a/tests/components/zwave_js/test_event.py b/tests/components/zwave_js/test_event.py index 12187d3d227..1db02662f4e 100644 --- a/tests/components/zwave_js/test_event.py +++ b/tests/components/zwave_js/test_event.py @@ -1,4 +1,5 @@ """Test the Z-Wave JS event platform.""" + from datetime import timedelta from freezegun import freeze_time diff --git a/tests/components/zwave_js/test_events.py b/tests/components/zwave_js/test_events.py index 1e91b9338fa..0bb6376a02b 100644 --- a/tests/components/zwave_js/test_events.py +++ b/tests/components/zwave_js/test_events.py @@ -1,4 +1,5 @@ """Test Z-Wave JS events.""" + from unittest.mock import AsyncMock import pytest diff --git a/tests/components/zwave_js/test_fan.py b/tests/components/zwave_js/test_fan.py index c26a5366d37..03cd6bfb704 100644 --- a/tests/components/zwave_js/test_fan.py +++ b/tests/components/zwave_js/test_fan.py @@ -1,4 +1,5 @@ """Test the Z-Wave JS fan platform.""" + import copy import pytest diff --git a/tests/components/zwave_js/test_helpers.py b/tests/components/zwave_js/test_helpers.py index b40c09b249d..38e15df52cc 100644 --- a/tests/components/zwave_js/test_helpers.py +++ b/tests/components/zwave_js/test_helpers.py @@ -1,4 +1,5 @@ """Test the Z-Wave JS helpers module.""" + import voluptuous as vol from homeassistant.components.zwave_js.helpers import ( diff --git a/tests/components/zwave_js/test_humidifier.py b/tests/components/zwave_js/test_humidifier.py index 23e2dc68314..261e09babee 100644 --- a/tests/components/zwave_js/test_humidifier.py +++ b/tests/components/zwave_js/test_humidifier.py @@ -1,4 +1,5 @@ """Test the Z-Wave JS humidifier platform.""" + from zwave_js_server.const import CommandClass from zwave_js_server.const.command_class.humidity_control import HumidityControlMode from zwave_js_server.event import Event diff --git a/tests/components/zwave_js/test_init.py b/tests/components/zwave_js/test_init.py index 4555ee59e1e..822302a9940 100644 --- a/tests/components/zwave_js/test_init.py +++ b/tests/components/zwave_js/test_init.py @@ -1,4 +1,5 @@ """Test the Z-Wave JS init module.""" + import asyncio from copy import deepcopy import logging @@ -110,11 +111,14 @@ async def test_noop_statistics(hass: HomeAssistant, client) -> None: entry = MockConfigEntry(domain="zwave_js", data={"url": "ws://test.org"}) entry.add_to_hass(hass) - with patch( - "zwave_js_server.model.driver.Driver.async_enable_statistics" - ) as mock_cmd1, patch( - "zwave_js_server.model.driver.Driver.async_disable_statistics" - ) as mock_cmd2: + with ( + patch( + "zwave_js_server.model.driver.Driver.async_enable_statistics" + ) as mock_cmd1, + patch( + "zwave_js_server.model.driver.Driver.async_disable_statistics" + ) as mock_cmd2, + ): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() assert not mock_cmd1.called diff --git a/tests/components/zwave_js/test_light.py b/tests/components/zwave_js/test_light.py index f5b53f6a76e..0f41ae7dbaa 100644 --- a/tests/components/zwave_js/test_light.py +++ b/tests/components/zwave_js/test_light.py @@ -1,4 +1,5 @@ """Test the Z-Wave JS light platform.""" + from copy import deepcopy from zwave_js_server.event import Event diff --git a/tests/components/zwave_js/test_lock.py b/tests/components/zwave_js/test_lock.py index 2213e9cf069..e8a8a2035d8 100644 --- a/tests/components/zwave_js/test_lock.py +++ b/tests/components/zwave_js/test_lock.py @@ -1,4 +1,5 @@ """Test the Z-Wave JS lock platform.""" + import pytest from zwave_js_server.const import CommandClass from zwave_js_server.const.command_class.lock import ( diff --git a/tests/components/zwave_js/test_logbook.py b/tests/components/zwave_js/test_logbook.py index 98062612309..e42a2b2c56e 100644 --- a/tests/components/zwave_js/test_logbook.py +++ b/tests/components/zwave_js/test_logbook.py @@ -1,4 +1,5 @@ """The tests for Z-Wave JS logbook.""" + from zwave_js_server.const import CommandClass from homeassistant.components.zwave_js.const import ( @@ -25,6 +26,7 @@ async def test_humanifying_zwave_js_notification_event( hass.config.components.add("recorder") assert await async_setup_component(hass, "logbook", {}) + await hass.async_block_till_done() events = mock_humanify( hass, @@ -108,6 +110,7 @@ async def test_humanifying_zwave_js_value_notification_event( hass.config.components.add("recorder") assert await async_setup_component(hass, "logbook", {}) + await hass.async_block_till_done() events = mock_humanify( hass, diff --git a/tests/components/zwave_js/test_migrate.py b/tests/components/zwave_js/test_migrate.py index 1f5a96eb5f3..41fa507a3a0 100644 --- a/tests/components/zwave_js/test_migrate.py +++ b/tests/components/zwave_js/test_migrate.py @@ -1,4 +1,5 @@ """Test the Z-Wave JS migration module.""" + import copy import pytest @@ -277,7 +278,7 @@ async def test_different_endpoint_migration_status_sensor( assert entity_entry.unique_id == old_unique_id # Do this twice to make sure re-interview doesn't do anything weird - for _ in range(0, 2): + for _ in range(2): # Add a ready node, unique ID should be migrated event = {"node": node} driver.controller.emit("node added", event) @@ -386,7 +387,7 @@ async def test_old_entity_migration_notification_binary_sensor( assert entity_entry.unique_id == old_unique_id # Do this twice to make sure re-interview doesn't do anything weird - for _ in range(0, 2): + for _ in range(2): # Add a ready node, unique ID should be migrated event = {"node": node} driver.controller.emit("node added", event) diff --git a/tests/components/zwave_js/test_number.py b/tests/components/zwave_js/test_number.py index b05d9e46f73..38a582762cb 100644 --- a/tests/components/zwave_js/test_number.py +++ b/tests/components/zwave_js/test_number.py @@ -1,4 +1,5 @@ """Test the Z-Wave JS number platform.""" + from unittest.mock import patch import pytest @@ -231,7 +232,7 @@ async def test_config_parameter_number( assert entity_entry.entity_category == EntityCategory.CONFIG for entity_id in (number_entity_id, number_with_states_entity_id): - updated_entry = ent_reg.async_update_entity(entity_id, **{"disabled_by": None}) + updated_entry = ent_reg.async_update_entity(entity_id, disabled_by=None) assert updated_entry != entity_entry assert updated_entry.disabled is False diff --git a/tests/components/zwave_js/test_repairs.py b/tests/components/zwave_js/test_repairs.py index d2b702089f2..77191982b6e 100644 --- a/tests/components/zwave_js/test_repairs.py +++ b/tests/components/zwave_js/test_repairs.py @@ -1,4 +1,5 @@ """Test the Z-Wave JS repairs module.""" + from copy import deepcopy from http import HTTPStatus from unittest.mock import patch diff --git a/tests/components/zwave_js/test_select.py b/tests/components/zwave_js/test_select.py index 1cbdb8799f3..f1a1f8796d0 100644 --- a/tests/components/zwave_js/test_select.py +++ b/tests/components/zwave_js/test_select.py @@ -1,4 +1,5 @@ """Test the Z-Wave JS number platform.""" + from unittest.mock import MagicMock from zwave_js_server.const import CURRENT_VALUE_PROPERTY, CommandClass @@ -307,9 +308,7 @@ async def test_config_parameter_select( assert entity_entry.disabled assert entity_entry.entity_category == EntityCategory.CONFIG - updated_entry = ent_reg.async_update_entity( - select_entity_id, **{"disabled_by": None} - ) + updated_entry = ent_reg.async_update_entity(select_entity_id, disabled_by=None) assert updated_entry != entity_entry assert updated_entry.disabled is False diff --git a/tests/components/zwave_js/test_sensor.py b/tests/components/zwave_js/test_sensor.py index a3d36b84382..417b57aaaaa 100644 --- a/tests/components/zwave_js/test_sensor.py +++ b/tests/components/zwave_js/test_sensor.py @@ -1,4 +1,5 @@ """Test the Z-Wave JS sensor platform.""" + import copy import pytest @@ -221,7 +222,7 @@ async def test_disabled_notification_sensor( # Test enabling entity updated_entry = ent_reg.async_update_entity( - entity_entry.entity_id, **{"disabled_by": None} + entity_entry.entity_id, disabled_by=None ) assert updated_entry != entity_entry assert updated_entry.disabled is False @@ -277,7 +278,7 @@ async def test_config_parameter_sensor( assert entity_entry.entity_category == EntityCategory.DIAGNOSTIC for entity_id in (sensor_entity_id, sensor_with_states_entity_id): - updated_entry = ent_reg.async_update_entity(entity_id, **{"disabled_by": None}) + updated_entry = ent_reg.async_update_entity(entity_id, disabled_by=None) assert updated_entry != entity_entry assert updated_entry.disabled is False @@ -294,7 +295,7 @@ async def test_config_parameter_sensor( assert state.state == "C-Wire" updated_entry = ent_reg.async_update_entity( - entity_entry.entity_id, **{"disabled_by": None} + entity_entry.entity_id, disabled_by=None ) assert updated_entry != entity_entry assert updated_entry.disabled is False @@ -752,7 +753,7 @@ async def test_statistics_sensors_no_last_seen( assert entry.disabled assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION - ent_reg.async_update_entity(entry.entity_id, **{"disabled_by": None}) + ent_reg.async_update_entity(entry.entity_id, disabled_by=None) # reload integration and check if entity is correctly there await hass.config_entries.async_reload(integration.entry_id) diff --git a/tests/components/zwave_js/test_services.py b/tests/components/zwave_js/test_services.py index 8697dad2e7b..5462bcf9946 100644 --- a/tests/components/zwave_js/test_services.py +++ b/tests/components/zwave_js/test_services.py @@ -1,4 +1,5 @@ """Test the Z-Wave JS services.""" + from unittest.mock import MagicMock, patch import pytest @@ -1520,9 +1521,12 @@ async def test_multicast_set_value( diff_network_node = MagicMock() diff_network_node.client.driver.controller.home_id.return_value = "diff_home_id" - with pytest.raises(vol.MultipleInvalid), patch( - "homeassistant.components.zwave_js.helpers.async_get_node_from_device_id", - side_effect=(climate_danfoss_lc_13, diff_network_node), + with ( + pytest.raises(vol.MultipleInvalid), + patch( + "homeassistant.components.zwave_js.helpers.async_get_node_from_device_id", + side_effect=(climate_danfoss_lc_13, diff_network_node), + ), ): await hass.services.async_call( DOMAIN, diff --git a/tests/components/zwave_js/test_siren.py b/tests/components/zwave_js/test_siren.py index 6df5881107a..4eb872954d1 100644 --- a/tests/components/zwave_js/test_siren.py +++ b/tests/components/zwave_js/test_siren.py @@ -1,4 +1,5 @@ """Test the Z-Wave JS siren platform.""" + from zwave_js_server.event import Event from homeassistant.components.siren import ( diff --git a/tests/components/zwave_js/test_switch.py b/tests/components/zwave_js/test_switch.py index fd5c626bdd2..5a5ad0821eb 100644 --- a/tests/components/zwave_js/test_switch.py +++ b/tests/components/zwave_js/test_switch.py @@ -1,4 +1,5 @@ """Test the Z-Wave JS switch platform.""" + import pytest from zwave_js_server.const import CURRENT_VALUE_PROPERTY, CommandClass from zwave_js_server.event import Event @@ -227,9 +228,7 @@ async def test_config_parameter_switch( assert entity_entry assert entity_entry.disabled - updated_entry = ent_reg.async_update_entity( - switch_entity_id, **{"disabled_by": None} - ) + updated_entry = ent_reg.async_update_entity(switch_entity_id, disabled_by=None) assert updated_entry != entity_entry assert updated_entry.disabled is False assert entity_entry.entity_category == EntityCategory.CONFIG diff --git a/tests/components/zwave_js/test_trigger.py b/tests/components/zwave_js/test_trigger.py index 26b9459cfc2..23c97913400 100644 --- a/tests/components/zwave_js/test_trigger.py +++ b/tests/components/zwave_js/test_trigger.py @@ -1,4 +1,5 @@ """The tests for Z-Wave JS automation triggers.""" + from unittest.mock import AsyncMock, patch import pytest diff --git a/tests/components/zwave_js/test_update.py b/tests/components/zwave_js/test_update.py index 1774254a3c5..c5cfba18569 100644 --- a/tests/components/zwave_js/test_update.py +++ b/tests/components/zwave_js/test_update.py @@ -1,7 +1,9 @@ """Test the Z-Wave JS update entities.""" + import asyncio from datetime import timedelta +from freezegun.api import FrozenDateTimeFactory import pytest from zwave_js_server.event import Event from zwave_js_server.exceptions import FailedZWaveCommand @@ -628,6 +630,7 @@ async def test_update_entity_delay( ge_in_wall_dimmer_switch, zen_31, hass_ws_client: WebSocketGenerator, + freezer: FrozenDateTimeFactory, ) -> None: """Test update occurs on a delay after HA starts.""" client.async_send_command.reset_mock() diff --git a/tests/components/zwave_me/test_config_flow.py b/tests/components/zwave_me/test_config_flow.py index 145cecd58c8..52d5e8fce6f 100644 --- a/tests/components/zwave_me/test_config_flow.py +++ b/tests/components/zwave_me/test_config_flow.py @@ -1,4 +1,5 @@ """Test the zwave_me config flow.""" + from ipaddress import ip_address from unittest.mock import patch @@ -28,12 +29,15 @@ MOCK_ZEROCONF_DATA = zeroconf.ZeroconfServiceInfo( async def test_form(hass: HomeAssistant) -> None: """Test we get the form.""" - with patch( - "homeassistant.components.zwave_me.async_setup_entry", - return_value=True, - ) as mock_setup_entry, patch( - "homeassistant.components.zwave_me.helpers.get_uuid", - return_value="test_uuid", + with ( + patch( + "homeassistant.components.zwave_me.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + patch( + "homeassistant.components.zwave_me.helpers.get_uuid", + return_value="test_uuid", + ), ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -60,12 +64,15 @@ async def test_form(hass: HomeAssistant) -> None: async def test_zeroconf(hass: HomeAssistant) -> None: """Test starting a flow from zeroconf.""" - with patch( - "homeassistant.components.zwave_me.async_setup_entry", - return_value=True, - ) as mock_setup_entry, patch( - "homeassistant.components.zwave_me.helpers.get_uuid", - return_value="test_uuid", + with ( + patch( + "homeassistant.components.zwave_me.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + patch( + "homeassistant.components.zwave_me.helpers.get_uuid", + return_value="test_uuid", + ), ): result: FlowResult = await hass.config_entries.flow.async_init( DOMAIN, diff --git a/tests/components/zwave_me/test_remove_stale_devices.py b/tests/components/zwave_me/test_remove_stale_devices.py index d5496255add..0a4ae24e0f0 100644 --- a/tests/components/zwave_me/test_remove_stale_devices.py +++ b/tests/components/zwave_me/test_remove_stale_devices.py @@ -1,4 +1,5 @@ """Test the zwave_me removal of stale devices.""" + from unittest.mock import patch import uuid @@ -50,11 +51,14 @@ async def test_remove_stale_devices( connections={("mac", "12:34:56:AB:CD:EF")}, identifiers={("zwave_me", f"{config_entry.unique_id}-{identifier}")}, ) - with patch( - "homeassistant.components.zwave_me.ZWaveMe.get_connection", - mock_connection, - ), patch( - "homeassistant.components.zwave_me.async_setup_platforms", + with ( + patch( + "homeassistant.components.zwave_me.ZWaveMe.get_connection", + mock_connection, + ), + patch( + "homeassistant.components.zwave_me.async_setup_platforms", + ), ): await hass.config_entries.async_setup(config_entry.entry_id) assert ( diff --git a/tests/conftest.py b/tests/conftest.py index 3be03e1e3ca..157e0f2ba59 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,4 +1,5 @@ """Set up some common test helper things.""" + from __future__ import annotations import asyncio @@ -52,6 +53,7 @@ from homeassistant.const import HASSIO_USER_NAME from homeassistant.core import CoreState, HassJob, HomeAssistant from homeassistant.helpers import ( area_registry as ar, + category_registry as cr, config_entry_oauth2_flow, device_registry as dr, entity_registry as er, @@ -96,13 +98,13 @@ from .common import ( # noqa: E402, isort:skip init_recorder_component, mock_storage, patch_yaml_files, + extract_stack_to_frame, ) from .test_util.aiohttp import ( # noqa: E402, isort:skip AiohttpClientMocker, mock_aiohttp_client, ) - _LOGGER = logging.getLogger(__name__) logging.basicConfig(level=logging.INFO) @@ -159,9 +161,9 @@ def pytest_runtest_setup() -> None: except ImportError: pass else: - MySQLdb_converters.conversions[ - HAFakeDatetime - ] = MySQLdb_converters.DateTime2literal + MySQLdb_converters.conversions[HAFakeDatetime] = ( + MySQLdb_converters.DateTime2literal + ) def ha_datetime_to_fakedatetime(datetime) -> freezegun.api.FakeDatetime: # type: ignore[name-defined] @@ -620,15 +622,18 @@ def mock_device_tracker_conf() -> Generator[list[Device], None, None]: async def mock_update_config(path: str, dev_id: str, entity: Device) -> None: devices.append(entity) - with patch( - ( - "homeassistant.components.device_tracker.legacy" - ".DeviceTracker.async_update_config" + with ( + patch( + ( + "homeassistant.components.device_tracker.legacy" + ".DeviceTracker.async_update_config" + ), + side_effect=mock_update_config, + ), + patch( + "homeassistant.components.device_tracker.legacy.async_load_config", + side_effect=lambda *args: devices, ), - side_effect=mock_update_config, - ), patch( - "homeassistant.components.device_tracker.legacy.async_load_config", - side_effect=lambda *args: devices, ): yield devices @@ -933,8 +938,7 @@ async def mqtt_mock( mqtt_mock_entry: MqttMockHAClientGenerator, ) -> AsyncGenerator[MqttMockHAClient, None]: """Fixture to mock MQTT component.""" - with patch("homeassistant.components.mqtt.PLATFORMS", []): - return await mqtt_mock_entry() + return await mqtt_mock_entry() @asynccontextmanager @@ -991,7 +995,7 @@ async def _mqtt_mock_entry( nonlocal mock_mqtt_instance nonlocal real_mqtt_instance real_mqtt_instance = real_mqtt(*args, **kwargs) - spec = dir(real_mqtt_instance) + ["_mqttc"] + spec = [*dir(real_mqtt_instance), "_mqttc"] mock_mqtt_instance = MqttMockHAClient( return_value=real_mqtt_instance, spec_set=spec, @@ -1123,10 +1127,9 @@ def mock_zeroconf() -> Generator[None, None, None]: """Mock zeroconf.""" from zeroconf import DNSCache # pylint: disable=import-outside-toplevel - with patch( - "homeassistant.components.zeroconf.HaZeroconf", autospec=True - ) as mock_zc, patch( - "homeassistant.components.zeroconf.AsyncServiceBrowser", autospec=True + with ( + patch("homeassistant.components.zeroconf.HaZeroconf", autospec=True) as mock_zc, + patch("homeassistant.components.zeroconf.AsyncServiceBrowser", autospec=True), ): zc = mock_zc.return_value # DNSCache has strong Cython type checks, and MagicMock does not work @@ -1337,38 +1340,47 @@ def hass_recorder( migrate_entity_ids = ( recorder.Recorder._migrate_entity_ids if enable_migrate_entity_ids else None ) - with patch( - "homeassistant.components.recorder.Recorder.async_nightly_tasks", - side_effect=nightly, - autospec=True, - ), patch( - "homeassistant.components.recorder.Recorder.async_periodic_statistics", - side_effect=stats, - autospec=True, - ), patch( - "homeassistant.components.recorder.migration._find_schema_errors", - side_effect=schema_validate, - autospec=True, - ), patch( - "homeassistant.components.recorder.Recorder._migrate_events_context_ids", - side_effect=migrate_events_context_ids, - autospec=True, - ), patch( - "homeassistant.components.recorder.Recorder._migrate_states_context_ids", - side_effect=migrate_states_context_ids, - autospec=True, - ), patch( - "homeassistant.components.recorder.Recorder._migrate_event_type_ids", - side_effect=migrate_event_type_ids, - autospec=True, - ), patch( - "homeassistant.components.recorder.Recorder._migrate_entity_ids", - side_effect=migrate_entity_ids, - autospec=True, - ), patch( - "homeassistant.components.recorder.Recorder._schedule_compile_missing_statistics", - side_effect=compile_missing, - autospec=True, + with ( + patch( + "homeassistant.components.recorder.Recorder.async_nightly_tasks", + side_effect=nightly, + autospec=True, + ), + patch( + "homeassistant.components.recorder.Recorder.async_periodic_statistics", + side_effect=stats, + autospec=True, + ), + patch( + "homeassistant.components.recorder.migration._find_schema_errors", + side_effect=schema_validate, + autospec=True, + ), + patch( + "homeassistant.components.recorder.Recorder._migrate_events_context_ids", + side_effect=migrate_events_context_ids, + autospec=True, + ), + patch( + "homeassistant.components.recorder.Recorder._migrate_states_context_ids", + side_effect=migrate_states_context_ids, + autospec=True, + ), + patch( + "homeassistant.components.recorder.Recorder._migrate_event_type_ids", + side_effect=migrate_event_type_ids, + autospec=True, + ), + patch( + "homeassistant.components.recorder.Recorder._migrate_entity_ids", + side_effect=migrate_entity_ids, + autospec=True, + ), + patch( + "homeassistant.components.recorder.Recorder._schedule_compile_missing_statistics", + side_effect=compile_missing, + autospec=True, + ), ): def setup_recorder(config: dict[str, Any] | None = None) -> HomeAssistant: @@ -1461,38 +1473,47 @@ async def async_setup_recorder_instance( migrate_entity_ids = ( recorder.Recorder._migrate_entity_ids if enable_migrate_entity_ids else None ) - with patch( - "homeassistant.components.recorder.Recorder.async_nightly_tasks", - side_effect=nightly, - autospec=True, - ), patch( - "homeassistant.components.recorder.Recorder.async_periodic_statistics", - side_effect=stats, - autospec=True, - ), patch( - "homeassistant.components.recorder.migration._find_schema_errors", - side_effect=schema_validate, - autospec=True, - ), patch( - "homeassistant.components.recorder.Recorder._migrate_events_context_ids", - side_effect=migrate_events_context_ids, - autospec=True, - ), patch( - "homeassistant.components.recorder.Recorder._migrate_states_context_ids", - side_effect=migrate_states_context_ids, - autospec=True, - ), patch( - "homeassistant.components.recorder.Recorder._migrate_event_type_ids", - side_effect=migrate_event_type_ids, - autospec=True, - ), patch( - "homeassistant.components.recorder.Recorder._migrate_entity_ids", - side_effect=migrate_entity_ids, - autospec=True, - ), patch( - "homeassistant.components.recorder.Recorder._schedule_compile_missing_statistics", - side_effect=compile_missing, - autospec=True, + with ( + patch( + "homeassistant.components.recorder.Recorder.async_nightly_tasks", + side_effect=nightly, + autospec=True, + ), + patch( + "homeassistant.components.recorder.Recorder.async_periodic_statistics", + side_effect=stats, + autospec=True, + ), + patch( + "homeassistant.components.recorder.migration._find_schema_errors", + side_effect=schema_validate, + autospec=True, + ), + patch( + "homeassistant.components.recorder.Recorder._migrate_events_context_ids", + side_effect=migrate_events_context_ids, + autospec=True, + ), + patch( + "homeassistant.components.recorder.Recorder._migrate_states_context_ids", + side_effect=migrate_states_context_ids, + autospec=True, + ), + patch( + "homeassistant.components.recorder.Recorder._migrate_event_type_ids", + side_effect=migrate_event_type_ids, + autospec=True, + ), + patch( + "homeassistant.components.recorder.Recorder._migrate_entity_ids", + side_effect=migrate_entity_ids, + autospec=True, + ), + patch( + "homeassistant.components.recorder.Recorder._schedule_compile_missing_statistics", + side_effect=compile_missing, + autospec=True, + ), ): async def async_setup_recorder( @@ -1539,22 +1560,25 @@ async def mock_enable_bluetooth( @pytest.fixture(scope="session") def mock_bluetooth_adapters() -> Generator[None, None, None]: """Fixture to mock bluetooth adapters.""" - with patch("bluetooth_auto_recovery.recover_adapter"), patch( - "bluetooth_adapters.systems.platform.system", return_value="Linux" - ), patch("bluetooth_adapters.systems.linux.LinuxAdapters.refresh"), patch( - "bluetooth_adapters.systems.linux.LinuxAdapters.adapters", - { - "hci0": { - "address": "00:00:00:00:00:01", - "hw_version": "usb:v1D6Bp0246d053F", - "passive_scan": False, - "sw_version": "homeassistant", - "manufacturer": "ACME", - "product": "Bluetooth Adapter 5.0", - "product_id": "aa01", - "vendor_id": "cc01", + with ( + patch("bluetooth_auto_recovery.recover_adapter"), + patch("bluetooth_adapters.systems.platform.system", return_value="Linux"), + patch("bluetooth_adapters.systems.linux.LinuxAdapters.refresh"), + patch( + "bluetooth_adapters.systems.linux.LinuxAdapters.adapters", + { + "hci0": { + "address": "00:00:00:00:00:01", + "hw_version": "usb:v1D6Bp0246d053F", + "passive_scan": False, + "sw_version": "homeassistant", + "manufacturer": "ACME", + "product": "Bluetooth Adapter 5.0", + "product_id": "aa01", + "vendor_id": "cc01", + }, }, - }, + ), ): yield @@ -1572,10 +1596,13 @@ def mock_bleak_scanner_start() -> Generator[MagicMock, None, None]: # out start and this fixture will expire before the stop method is called # when EVENT_HOMEASSISTANT_STOP is fired. bluetooth_scanner.OriginalBleakScanner.stop = AsyncMock() # type: ignore[assignment] - with patch.object( - bluetooth_scanner.OriginalBleakScanner, - "start", - ) as mock_bleak_scanner_start, patch.object(bluetooth_scanner, "HaScanner"): + with ( + patch.object( + bluetooth_scanner.OriginalBleakScanner, + "start", + ) as mock_bleak_scanner_start, + patch.object(bluetooth_scanner, "HaScanner"), + ): yield mock_bleak_scanner_start @@ -1587,21 +1614,29 @@ def mock_integration_frame() -> Generator[Mock, None, None]: lineno="23", line="self.light.is_on", ) - with patch( - "homeassistant.helpers.frame.extract_stack", - return_value=[ - Mock( - filename="/home/paulus/homeassistant/core.py", - lineno="23", - line="do_something()", + with ( + patch( + "homeassistant.helpers.frame.linecache.getline", + return_value=correct_frame.line, + ), + patch( + "homeassistant.helpers.frame.get_current_frame", + return_value=extract_stack_to_frame( + [ + Mock( + filename="/home/paulus/homeassistant/core.py", + lineno="23", + line="do_something()", + ), + correct_frame, + Mock( + filename="/home/paulus/aiohue/lights.py", + lineno="2", + line="something()", + ), + ] ), - correct_frame, - Mock( - filename="/home/paulus/aiohue/lights.py", - lineno="2", - line="something()", - ), - ], + ), ): yield correct_frame @@ -1613,6 +1648,12 @@ def mock_bluetooth( """Mock out bluetooth from starting.""" +@pytest.fixture +def category_registry(hass: HomeAssistant) -> cr.CategoryRegistry: + """Return the category registry from the current hass instance.""" + return cr.async_get(hass) + + @pytest.fixture def area_registry(hass: HomeAssistant) -> ar.AreaRegistry: """Return the area registry from the current hass instance.""" diff --git a/tests/fixtures/Ddwrt_Status_Lan.txt b/tests/fixtures/Ddwrt_Status_Lan.txt deleted file mode 100644 index b61d92c365e..00000000000 --- a/tests/fixtures/Ddwrt_Status_Lan.txt +++ /dev/null @@ -1,18 +0,0 @@ -{lan_mac::AA:BB:CC:DD:EE:F0} -{lan_ip::192.168.1.1} -{lan_ip_prefix::192.168.1.} -{lan_netmask::255.255.255.0} -{lan_gateway::0.0.0.0} -{lan_dns::8.8.8.8} -{lan_proto::dhcp} -{dhcp_daemon::DNSMasq} -{dhcp_start::100} -{dhcp_num::50} -{dhcp_lease_time::1440} -{dhcp_leases:: 'device_1','192.168.1.113','AA:BB:CC:DD:EE:00','1 day 00:00:00','113','device_2','192.168.1.201','AA:BB:CC:DD:EE:01','Static','201'} -{pptp_leases::} -{pppoe_leases::} -{arp_table:: 'device_1','192.168.1.113','AA:BB:CC:DD:EE:00','13','device_2','192.168.1.201','AA:BB:CC:DD:EE:01','1'} -{uptime:: 12:28:48 up 132 days, 18:02, load average: 0.15, 0.19, 0.21} -{ipinfo:: IP: 192.168.0.108} - diff --git a/tests/fixtures/Ddwrt_Status_Wireless.txt b/tests/fixtures/Ddwrt_Status_Wireless.txt deleted file mode 100644 index 5343fea9904..00000000000 --- a/tests/fixtures/Ddwrt_Status_Wireless.txt +++ /dev/null @@ -1,13 +0,0 @@ -{wl_mac::AA:BB:CC:DD:EE:FF} -{wl_ssid::WIFI_SSD} -{wl_channel::10} -{wl_radio::Radio is On} -{wl_xmit::Auto} -{wl_rate::72 Mbps} -{wl_ack::} -{active_wireless::'AA:BB:CC:DD:EE:00','eth1','3:13:14','72M','24M','HT20','-9','-92','83','1048','AA:BB:CC:DD:EE:01','eth1','10:48:22','72M','72M','HT20','-40','-92','52','664'} -{active_wds::} -{packet_info::SWRXgoodPacket=173673555;SWRXerrorPacket=27;SWTXgoodPacket=311344396;SWTXerrorPacket=3107;} -{uptime:: 12:29:23 up 132 days, 18:03, load average: 0.16, 0.19, 0.20} -{ipinfo:: IP: 192.168.0.108} - diff --git a/tests/fixtures/alpr_stdout.txt b/tests/fixtures/alpr_stdout.txt deleted file mode 100644 index 255b57c5790..00000000000 --- a/tests/fixtures/alpr_stdout.txt +++ /dev/null @@ -1,12 +0,0 @@ - -plate0: top 10 results -- Processing Time = 58.1879ms. - - PE3R2X confidence: 98.9371 - - PE32X confidence: 98.1385 - - PE3R2 confidence: 97.5444 - - PE3R2Y confidence: 86.1448 - - P63R2X confidence: 82.9016 - - FE3R2X confidence: 72.1147 - - PE32 confidence: 66.7458 - - PE32Y confidence: 65.3462 - - P632X confidence: 62.1031 - - P63R2 confidence: 61.5089 diff --git a/tests/fixtures/aurora.txt b/tests/fixtures/aurora.txt deleted file mode 100644 index 22e8d0c2476..00000000000 --- a/tests/fixtures/aurora.txt +++ /dev/null @@ -1,529 +0,0 @@ -#Aurora Specification Tabular Values -# Product: Ovation Aurora Short Term Forecast -# Product Valid At: 2019-11-08 18:55 -# Product Generated At: 2019-11-08 18:25 -# -# Prepared by the U.S. Dept. of Commerce, NOAA, Space Weather Prediction Center. -# Please send comments and suggestions to SWPC.Webmaster@noaa.gov -# -# Missing Data: (n/a) -# Cadence: 5 minutes -# -# Tabular Data is on the following grid -# -# 1024 values covering 0 to 360 degrees in the horizontal (longitude) direction (0.32846715 degrees/value) -# 512 values covering -90 to 90 degrees in the vertical (latitude) direction (0.3515625 degrees/value) -# Values range from 0 (little or no probability of visible aurora) to 100 (high probability of visible aurora) -# - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 - 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 - 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 - 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 - 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 - 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 - 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 - 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 - 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 - 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 - 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 - 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 - 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 - 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 - 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 - 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 - 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 - 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 - 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 - 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 - 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 - 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 - 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 - 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 - 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 - 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 - 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 - 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 - 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 - 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 - 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 - 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 - 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 - 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 - 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 - 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 - 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 - 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 - 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 - 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 - 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 - 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 - 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 - 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 - 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 - 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 - 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 - 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 - 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 - 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 - 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 - 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 - 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 - 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 - 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 - 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 - 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 - 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 - 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 - 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 - 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 - 11 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 - 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 - 11 11 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 - 11 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 - 10 10 10 10 10 10 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 10 10 - 10 10 10 9 9 9 9 9 9 8 8 8 8 8 8 8 7 7 7 7 7 7 6 6 6 6 6 6 6 5 5 5 5 5 5 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 6 6 6 6 6 6 6 7 7 7 7 7 7 7 8 8 8 8 8 8 9 9 9 9 9 9 9 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 10 10 10 10 10 - 9 9 9 9 9 9 8 8 8 8 8 7 7 7 7 7 7 6 6 6 6 6 6 5 5 5 5 5 5 4 4 4 4 4 4 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 5 5 5 5 5 5 5 6 6 6 6 6 6 6 7 7 7 7 7 7 7 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 7 7 7 7 7 7 7 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 6 6 6 6 6 6 6 7 7 7 7 7 7 8 8 8 8 8 9 9 9 9 9 9 10 10 10 10 10 10 10 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 10 10 10 10 10 10 9 - 9 9 9 9 8 8 8 8 8 7 7 7 7 7 6 6 6 6 6 6 5 5 5 5 5 4 4 4 4 4 4 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 5 5 5 5 5 5 5 6 6 6 6 6 6 7 7 7 7 7 7 7 8 8 8 8 8 8 8 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 8 8 8 8 8 8 8 7 7 7 7 7 7 7 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 6 6 6 6 6 6 7 7 7 7 7 7 8 8 8 8 8 8 9 9 9 9 9 9 10 10 10 10 10 10 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 11 11 11 11 11 11 10 10 10 10 10 10 9 9 9 - 9 9 8 8 8 8 8 8 7 7 7 7 7 6 6 6 6 6 5 5 5 5 5 5 4 4 4 4 4 4 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 5 5 5 5 5 5 6 6 6 6 6 6 6 7 7 7 7 7 7 8 8 8 8 8 8 8 9 9 9 9 9 9 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 8 8 8 8 8 8 7 7 7 7 7 7 7 6 6 6 6 6 6 6 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 6 6 6 6 6 6 7 7 7 7 7 7 8 8 8 8 8 8 9 9 9 9 9 9 10 10 10 10 10 10 11 11 11 11 11 11 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 11 11 11 11 11 11 10 10 10 10 10 10 9 9 9 9 - 9 8 8 8 8 8 8 7 7 7 7 7 6 6 6 6 6 5 5 5 5 5 4 4 4 4 4 4 4 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 5 5 5 5 5 5 6 6 6 6 6 6 7 7 7 7 7 7 7 8 8 8 8 8 8 9 9 9 9 9 9 10 10 10 10 10 10 10 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 9 9 9 9 9 9 8 8 8 8 8 8 7 7 7 7 7 7 6 6 6 6 6 6 6 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 5 5 5 5 5 5 6 6 6 6 6 6 7 7 7 7 7 7 8 8 8 8 8 8 9 9 9 9 9 9 10 10 10 10 10 10 11 11 11 11 11 11 11 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 11 11 11 11 11 11 10 10 10 10 10 10 9 9 9 9 9 - 8 8 8 8 7 7 7 7 7 6 6 6 6 6 5 5 5 5 5 5 4 4 4 4 4 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 5 5 5 5 5 5 5 6 6 6 6 6 6 7 7 7 7 7 7 8 8 8 8 8 8 9 9 9 9 9 10 10 10 10 10 10 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 10 10 10 10 10 10 9 9 9 9 9 9 8 8 8 8 8 8 7 7 7 7 7 7 7 6 6 6 6 6 6 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 6 6 6 6 6 6 7 7 7 7 7 7 8 8 8 8 8 8 9 9 9 9 9 10 10 10 10 10 10 11 11 11 11 11 11 12 12 12 12 12 12 12 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 12 12 12 12 12 11 11 11 11 11 11 10 10 10 10 10 9 9 9 9 9 8 8 - 8 7 7 7 7 7 6 6 6 6 6 5 5 5 5 5 4 4 4 4 4 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 5 5 5 5 5 5 6 6 6 6 6 7 7 7 7 7 8 8 8 8 8 8 9 9 9 9 9 10 10 10 10 10 11 11 11 11 11 11 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 11 11 11 11 11 11 10 10 10 10 10 10 9 9 9 9 9 9 9 8 8 8 8 8 8 7 7 7 7 7 7 6 6 6 6 6 6 5 5 5 5 5 5 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 6 6 6 6 6 6 7 7 7 7 7 7 7 8 8 8 8 8 9 9 9 9 9 9 10 10 10 10 10 11 11 11 11 11 12 12 12 12 12 12 13 13 13 13 13 13 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 13 13 13 13 13 13 12 12 12 12 11 11 11 11 11 10 10 10 10 9 9 9 9 9 8 8 8 8 - 7 7 7 6 6 6 6 6 5 5 5 5 4 4 4 4 4 3 3 3 3 3 3 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 4 4 4 4 4 4 5 5 5 5 5 5 6 6 6 6 6 7 7 7 7 8 8 8 8 8 9 9 9 9 10 10 10 10 10 11 11 11 11 11 11 12 12 12 12 12 12 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 12 12 12 12 12 12 11 11 11 11 11 11 11 10 10 10 10 10 10 10 9 9 9 9 9 9 8 8 8 8 8 7 7 7 7 7 6 6 6 6 6 6 5 5 5 5 5 5 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 7 7 7 7 7 7 8 8 8 8 8 8 9 9 9 9 9 10 10 10 10 10 11 11 11 11 11 12 12 12 12 12 13 13 13 13 13 13 14 14 14 14 14 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 16 16 16 17 17 17 17 17 17 16 16 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 14 14 14 14 14 14 13 13 13 13 12 12 12 12 11 11 11 11 10 10 10 10 9 9 9 9 8 8 8 8 7 7 - 7 6 6 6 6 5 5 5 5 5 4 4 4 4 4 3 3 3 3 3 3 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 3 3 3 3 3 3 4 4 4 4 4 4 5 5 5 5 5 6 6 6 6 6 7 7 7 7 7 8 8 8 8 9 9 9 9 10 10 10 10 10 11 11 11 11 11 11 12 12 12 12 12 13 13 13 13 13 13 13 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 13 13 13 13 13 13 13 12 12 12 12 12 12 12 11 11 11 11 11 11 11 10 10 10 10 10 10 9 9 9 9 9 8 8 8 8 8 7 7 7 7 7 6 6 6 6 6 6 6 5 5 5 5 5 5 5 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 7 7 7 7 7 7 8 8 8 8 8 8 9 9 9 9 9 10 10 10 10 10 10 11 11 11 11 12 12 12 12 12 13 13 13 13 13 13 14 14 14 14 14 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 16 16 16 16 17 17 17 17 17 17 16 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 14 14 14 14 14 13 13 13 13 13 12 12 12 12 11 11 11 11 10 10 10 10 9 9 9 9 8 8 8 8 7 7 7 7 - 6 6 5 5 5 5 5 4 4 4 4 4 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 3 3 3 3 3 3 4 4 4 4 4 4 5 5 5 5 5 6 6 6 6 6 7 7 7 7 7 8 8 8 8 9 9 9 9 10 10 10 10 10 11 11 11 11 11 12 12 12 12 12 13 13 13 13 13 13 14 14 14 14 14 14 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 12 12 12 12 12 12 11 11 11 11 11 10 10 10 10 10 10 9 9 9 9 9 8 8 8 8 8 8 7 7 7 7 7 7 7 6 6 6 6 6 6 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 7 7 7 7 7 7 7 8 8 8 8 8 8 9 9 9 9 9 9 10 10 10 10 10 11 11 11 11 11 12 12 12 12 12 13 13 13 13 13 14 14 14 14 14 14 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 14 14 14 14 14 14 13 13 13 13 13 12 12 12 12 12 11 11 11 10 10 10 10 9 9 9 9 8 8 8 8 7 7 7 7 6 6 6 - 5 5 5 5 4 4 4 4 4 4 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 3 3 3 3 3 3 3 4 4 4 4 4 5 5 5 5 5 6 6 6 6 6 7 7 7 7 8 8 8 8 8 9 9 9 9 10 10 10 10 10 11 11 11 11 12 12 12 12 12 13 13 13 13 13 14 14 14 14 14 14 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 16 16 16 16 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 13 13 13 13 13 12 12 12 12 12 12 11 11 11 11 11 11 10 10 10 10 10 10 9 9 9 9 9 9 8 8 8 8 8 8 7 7 7 7 7 7 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 8 8 8 8 8 8 9 9 9 9 9 9 10 10 10 10 10 11 11 11 11 11 12 12 12 12 12 13 13 13 13 13 14 14 14 14 14 14 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 13 13 13 13 13 12 12 12 12 12 11 11 11 11 10 10 10 9 9 9 9 8 8 8 8 7 7 7 7 6 6 6 6 5 5 - 4 4 4 4 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 4 4 4 4 4 5 5 5 5 5 6 6 6 6 7 7 7 7 7 8 8 8 8 9 9 9 9 10 10 10 10 11 11 11 11 12 12 12 12 12 13 13 13 13 14 14 14 14 14 15 15 15 15 15 15 16 16 16 16 16 16 16 17 17 17 17 17 17 17 17 17 17 17 17 17 18 18 18 18 18 18 18 18 18 18 18 18 18 18 17 17 17 17 17 17 17 17 17 17 17 17 17 16 16 16 16 16 16 16 16 15 15 15 15 15 15 14 14 14 14 14 14 13 13 13 13 13 12 12 12 12 12 12 11 11 11 11 11 11 10 10 10 10 10 9 9 9 9 9 9 8 8 8 8 8 8 7 7 7 7 7 7 7 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 8 8 8 8 8 8 9 9 9 9 9 9 10 10 10 10 10 11 11 11 11 12 12 12 12 12 13 13 13 13 13 14 14 14 14 15 15 15 15 15 15 16 16 16 16 16 16 16 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 16 16 16 16 16 16 16 15 15 15 15 15 15 14 14 14 14 14 13 13 13 13 13 12 12 12 12 11 11 11 10 10 10 10 9 9 9 8 8 8 8 7 7 7 6 6 6 6 6 5 5 5 5 4 4 4 - 3 3 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 3 3 3 3 3 3 3 4 4 4 4 4 5 5 5 5 5 6 6 6 6 7 7 7 8 8 8 8 9 9 9 9 10 10 10 10 11 11 11 11 12 12 12 12 13 13 13 13 13 14 14 14 14 15 15 15 15 15 16 16 16 16 16 17 17 17 17 17 17 17 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 17 17 17 17 17 17 17 17 16 16 16 16 16 16 16 15 15 15 15 15 14 14 14 14 14 14 13 13 13 13 13 12 12 12 12 12 12 11 11 11 11 11 10 10 10 10 10 9 9 9 9 9 9 8 8 8 8 8 8 7 7 7 7 7 7 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 8 8 8 8 8 8 9 9 9 9 9 9 10 10 10 10 10 11 11 11 11 12 12 12 12 12 13 13 13 13 14 14 14 14 15 15 15 15 15 16 16 16 16 16 17 17 17 17 17 17 17 17 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 17 17 17 17 17 17 17 16 16 16 16 16 15 15 15 15 15 14 14 14 14 13 13 13 12 12 12 12 11 11 11 10 10 10 9 9 9 8 8 8 7 7 7 7 6 6 6 5 5 5 5 4 4 4 4 4 3 3 3 3 - 2 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 3 3 3 3 3 4 4 4 4 4 5 5 5 5 5 6 6 6 7 7 7 7 8 8 8 8 9 9 9 9 10 10 10 10 11 11 11 12 12 12 12 13 13 13 13 14 14 14 14 15 15 15 15 16 16 16 16 17 17 17 17 17 17 18 18 18 18 18 18 18 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 18 18 18 18 18 18 18 18 17 17 17 17 17 17 16 16 16 16 16 16 15 15 15 15 15 14 14 14 14 14 14 13 13 13 13 13 12 12 12 12 11 11 11 11 11 10 10 10 10 10 10 9 9 9 9 9 8 8 8 8 8 8 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 8 8 8 8 8 8 8 9 9 9 9 9 10 10 10 10 10 11 11 11 11 12 12 12 12 13 13 13 13 14 14 14 14 15 15 15 15 15 16 16 16 16 17 17 17 17 17 17 18 18 18 18 18 18 18 18 18 19 19 19 19 19 19 19 19 19 18 18 18 18 18 18 18 18 17 17 17 17 16 16 16 16 16 15 15 15 14 14 14 13 13 13 13 12 12 12 11 11 10 10 10 9 9 9 8 8 7 7 7 7 6 6 6 5 5 5 4 4 4 4 3 3 3 3 3 2 2 2 2 2 - 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 3 3 3 3 3 3 4 4 4 4 4 5 5 5 5 6 6 6 6 7 7 7 7 8 8 8 8 9 9 9 9 10 10 10 10 11 11 11 11 12 12 12 13 13 13 13 14 14 14 14 15 15 15 15 16 16 16 16 16 17 17 17 17 17 18 18 18 18 18 18 19 19 19 19 19 19 19 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 19 19 19 19 19 19 19 19 19 18 18 18 18 18 17 17 17 17 17 17 16 16 16 16 16 15 15 15 15 14 14 14 14 14 13 13 13 13 13 12 12 12 12 11 11 11 11 11 10 10 10 10 10 9 9 9 9 9 8 8 8 8 8 8 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 8 8 8 8 8 8 9 9 9 9 9 9 10 10 10 10 10 11 11 11 11 11 12 12 12 12 13 13 13 13 14 14 14 14 15 15 15 15 15 16 16 16 16 17 17 17 17 17 17 17 18 18 18 18 18 18 18 18 18 18 18 18 18 19 19 19 19 18 18 18 18 18 18 18 18 17 17 17 17 17 16 16 16 16 15 15 15 15 14 14 14 13 13 13 12 12 12 11 11 11 10 10 10 9 9 9 8 8 7 7 7 6 6 6 6 5 5 5 4 4 4 4 3 3 3 3 3 2 2 2 2 2 1 1 - 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 3 3 3 3 3 3 4 4 4 4 4 5 5 5 5 5 6 6 6 6 6 7 7 7 8 8 8 8 9 9 9 9 10 10 10 10 11 11 11 11 12 12 12 12 13 13 13 14 14 14 14 15 15 15 15 16 16 16 16 16 17 17 17 17 18 18 18 18 18 19 19 19 19 19 20 20 20 20 20 20 20 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 20 20 20 20 20 20 20 20 19 19 19 19 19 19 18 18 18 18 17 17 17 17 16 16 16 16 16 15 15 15 15 14 14 14 14 13 13 13 13 13 12 12 12 12 11 11 11 11 11 10 10 10 10 10 9 9 9 9 9 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 9 9 9 9 9 9 10 10 10 10 10 11 11 11 11 11 12 12 12 12 13 13 13 13 13 14 14 14 14 15 15 15 15 16 16 16 16 16 17 17 17 17 17 17 17 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 17 17 17 17 17 17 16 16 16 16 15 15 15 14 14 14 14 13 13 12 12 12 11 11 11 10 10 10 9 9 9 8 8 8 7 7 7 6 6 6 5 5 5 5 4 4 4 4 3 3 3 3 2 2 2 2 2 2 1 1 1 1 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 3 3 3 3 3 3 4 4 4 4 4 4 5 5 5 5 5 6 6 6 6 7 7 7 7 8 8 8 8 9 9 9 9 10 10 10 11 11 11 11 12 12 12 12 13 13 13 14 14 14 14 15 15 15 15 16 16 16 16 17 17 17 17 18 18 18 19 19 19 19 20 20 20 20 21 21 21 21 21 21 22 22 22 22 22 22 22 22 22 22 22 22 22 22 23 23 22 22 22 22 22 22 22 22 22 22 22 22 21 21 21 21 21 21 20 20 20 20 20 19 19 19 19 18 18 18 18 17 17 17 17 16 16 16 16 15 15 15 15 15 14 14 14 14 13 13 13 13 12 12 12 12 11 11 11 11 11 10 10 10 10 10 9 9 9 9 9 9 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 9 9 9 9 9 10 10 10 10 10 11 11 11 11 11 12 12 12 12 12 13 13 13 13 14 14 14 14 15 15 15 15 16 16 16 16 16 17 17 17 17 17 17 18 18 18 18 18 18 18 18 18 18 18 19 19 19 19 18 18 18 18 18 18 18 18 18 18 18 18 17 17 17 17 17 16 16 16 16 16 15 15 15 14 14 14 14 13 13 13 12 12 12 11 11 10 10 10 9 9 9 8 8 8 7 7 7 7 6 6 6 5 5 5 5 4 4 4 3 3 3 3 2 2 2 2 2 2 1 1 1 1 1 1 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 3 3 3 3 3 3 4 4 4 4 4 4 5 5 5 5 6 6 6 6 7 7 7 8 8 8 8 9 9 9 9 10 10 10 11 11 11 11 12 12 12 13 13 13 14 14 14 15 15 15 16 16 16 16 17 17 17 18 18 18 19 19 19 20 20 20 20 21 21 21 21 21 22 22 22 22 22 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 22 22 22 22 22 22 21 21 21 21 21 21 20 20 20 20 19 19 19 19 18 18 18 18 17 17 17 17 16 16 16 16 15 15 15 15 14 14 14 14 13 13 13 13 12 12 12 12 12 11 11 11 11 11 10 10 10 10 10 10 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 10 10 10 10 10 11 11 11 11 11 12 12 12 12 13 13 13 13 14 14 14 14 15 15 15 15 15 16 16 16 16 17 17 17 17 18 18 18 18 18 18 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 18 18 18 18 18 18 17 17 17 16 16 16 16 15 15 15 15 14 14 14 13 13 13 12 12 12 11 11 11 10 10 10 9 9 9 8 8 8 7 7 7 7 6 6 6 6 5 5 5 5 4 4 4 4 3 3 3 3 2 2 2 2 1 1 1 1 1 1 1 1 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 3 3 3 3 3 4 4 4 4 5 5 5 5 6 6 6 6 7 7 7 7 8 8 8 9 9 9 9 10 10 11 11 11 12 12 12 13 13 13 14 14 14 15 15 15 16 16 17 17 17 18 18 18 19 19 19 20 20 20 21 21 21 21 22 22 22 22 23 23 23 23 23 23 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 23 23 23 23 23 22 22 22 22 22 22 21 21 21 21 20 20 20 20 20 19 19 19 19 18 18 18 17 17 17 17 16 16 16 16 15 15 15 15 14 14 14 14 13 13 13 13 13 12 12 12 12 12 11 11 11 11 11 11 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 10 10 10 10 10 10 11 11 11 11 11 12 12 12 12 12 13 13 13 14 14 14 14 15 15 15 15 16 16 16 16 17 17 17 17 18 18 18 18 18 19 19 19 19 19 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 19 19 19 19 18 18 18 18 18 17 17 16 16 16 15 15 15 14 14 13 13 13 12 12 12 11 11 11 11 10 10 10 9 9 9 8 8 8 8 7 7 7 6 6 6 6 5 5 5 5 4 4 4 4 4 3 3 3 3 2 2 2 2 1 1 1 1 1 1 1 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 2 2 2 2 2 3 3 3 3 4 4 4 4 5 5 5 5 6 6 6 6 7 7 7 8 8 8 9 9 9 10 10 10 11 11 11 12 12 13 13 13 14 14 15 15 15 16 16 17 17 17 18 18 18 19 19 19 20 20 20 21 21 21 22 22 22 22 23 23 23 24 24 24 24 24 24 24 24 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 24 24 24 24 24 23 23 23 23 23 22 22 22 22 21 21 21 21 20 20 20 20 20 19 19 19 18 18 18 18 17 17 17 17 16 16 16 16 15 15 15 15 14 14 14 14 14 13 13 13 13 13 12 12 12 12 12 12 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 11 11 11 11 11 12 12 12 12 12 13 13 13 13 13 14 14 14 15 15 15 15 16 16 16 17 17 17 17 18 18 18 18 18 19 19 19 19 19 20 20 20 20 20 21 21 21 21 21 21 21 21 21 21 21 20 20 20 20 20 20 19 19 19 19 18 18 18 17 17 17 16 16 15 15 15 14 14 13 13 12 12 12 11 11 10 10 10 9 9 9 8 8 8 7 7 7 7 6 6 6 5 5 5 5 4 4 4 4 4 4 3 3 3 3 3 2 2 2 2 2 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 2 2 2 3 3 3 3 4 4 4 5 5 5 5 6 6 6 6 7 7 7 8 8 8 9 9 9 10 10 10 11 11 12 12 12 13 13 14 14 14 15 15 16 16 16 17 17 17 17 18 18 18 19 19 19 20 20 20 21 21 21 22 22 22 23 23 23 23 24 24 24 24 24 25 25 25 25 25 25 26 26 26 26 26 26 26 26 26 26 26 26 25 25 25 25 25 25 24 24 24 24 24 23 23 23 22 22 22 22 21 21 21 21 20 20 20 20 20 19 19 19 19 19 18 18 18 17 17 17 16 16 16 16 15 15 15 15 14 14 14 14 14 14 13 13 13 13 13 13 12 12 12 12 12 12 12 11 11 11 11 11 11 11 10 10 10 10 10 10 9 9 9 9 9 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 11 11 11 11 11 12 12 12 12 12 13 13 13 13 13 13 14 14 14 14 14 15 15 15 16 16 16 17 17 17 18 18 18 18 18 19 19 19 19 19 19 20 20 20 20 20 20 21 21 21 21 21 21 21 21 21 21 21 21 20 20 20 20 20 20 19 19 19 19 18 18 18 17 17 17 17 16 16 15 15 14 14 13 13 12 12 11 11 11 10 10 9 9 9 8 8 7 7 7 6 6 6 5 5 5 4 4 3 3 3 3 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 2 2 2 3 3 4 4 4 4 5 5 5 5 6 6 6 6 7 7 7 8 8 8 9 9 9 10 10 10 11 11 12 12 12 13 13 14 14 14 15 15 16 16 16 16 17 17 17 17 18 18 18 19 19 19 20 20 21 21 21 22 22 23 23 23 23 24 24 24 25 25 25 25 26 26 26 26 26 26 26 27 27 27 27 27 27 27 27 27 27 26 26 26 26 26 26 26 25 25 25 24 24 24 23 23 23 22 22 22 22 22 21 21 21 21 21 21 21 20 20 20 19 19 19 18 18 18 17 17 17 16 16 16 16 16 15 15 15 15 15 14 14 14 14 14 14 14 13 13 13 13 13 13 13 12 12 12 12 12 12 11 11 11 11 11 10 10 10 10 9 9 9 9 9 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 12 12 12 12 12 13 13 13 13 13 14 14 14 14 14 14 15 15 15 15 15 16 16 16 17 17 17 18 18 18 19 19 19 19 19 20 20 20 20 20 20 20 20 21 21 21 21 21 21 21 21 21 21 22 22 21 21 21 21 21 20 20 20 20 19 19 19 19 18 18 18 17 17 17 17 16 16 16 15 15 14 14 13 13 12 12 11 11 10 10 9 9 8 8 7 7 7 6 6 5 5 4 4 4 3 3 2 2 2 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 2 2 2 3 3 3 4 4 4 4 5 5 5 5 5 6 6 6 7 7 7 8 8 8 9 9 9 10 10 10 11 11 11 12 12 13 13 13 14 14 14 15 15 15 15 16 16 16 17 17 17 18 18 19 19 19 20 20 21 21 22 22 22 23 23 23 24 24 24 25 25 25 26 26 26 26 26 27 27 27 27 27 27 28 28 28 27 27 27 27 27 27 27 27 27 27 26 26 26 25 25 25 24 24 24 24 23 23 23 23 23 23 23 22 22 22 22 22 22 21 21 21 20 20 20 19 19 19 18 18 18 17 17 17 17 17 16 16 16 16 16 15 15 15 15 15 15 15 14 14 14 14 14 14 13 13 13 13 13 12 12 12 12 11 11 11 11 10 10 10 10 9 9 9 9 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 9 9 9 9 9 9 9 10 10 10 10 10 10 10 11 11 11 11 11 11 11 12 12 12 12 12 12 13 13 13 13 13 14 14 14 14 15 15 15 15 15 15 15 16 16 16 16 16 16 17 17 17 18 18 18 19 19 19 20 20 20 20 20 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 20 20 20 19 19 19 19 18 18 18 17 17 17 17 16 16 16 16 15 15 15 14 14 13 13 12 12 11 11 10 9 9 9 8 8 7 7 6 6 6 5 5 4 4 3 3 3 2 2 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 2 2 2 3 3 3 3 4 4 4 4 4 5 5 5 5 5 6 6 6 6 7 7 7 8 8 8 9 9 9 9 10 10 10 11 11 11 11 12 12 12 13 13 14 14 14 15 15 15 16 16 17 17 18 18 19 19 20 20 21 21 22 22 22 23 23 23 24 24 24 25 25 25 26 26 26 26 26 27 27 27 27 27 28 28 28 28 27 27 27 27 27 27 27 27 27 27 27 26 26 26 26 26 25 25 25 25 25 25 24 24 24 24 24 24 24 23 23 23 23 23 22 22 22 21 21 21 20 20 20 20 19 19 19 19 18 18 18 18 17 17 17 17 16 16 16 16 16 16 15 15 15 15 15 14 14 14 14 14 13 13 13 13 12 12 12 11 11 11 11 10 10 10 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 10 10 10 10 10 10 11 11 11 11 11 11 11 12 12 12 12 12 12 13 13 13 13 13 14 14 14 14 14 15 15 15 15 16 16 16 16 16 17 17 17 17 17 17 17 17 18 18 18 18 19 19 19 20 20 20 21 21 21 21 21 21 21 21 21 21 21 22 22 22 21 21 21 21 21 21 21 21 21 21 21 20 20 19 19 19 18 18 17 17 16 16 16 16 15 15 15 15 15 14 14 14 14 13 13 12 12 11 11 10 10 9 9 8 8 7 7 7 6 6 6 5 5 4 4 4 3 3 3 2 2 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 2 2 - 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 2 2 2 3 3 3 3 3 4 4 4 4 4 4 5 5 5 5 5 6 6 6 7 7 7 7 8 8 8 8 8 9 9 9 9 9 10 10 10 11 11 12 12 12 13 13 14 14 15 15 16 16 17 17 18 18 19 19 20 20 21 21 22 22 23 23 23 24 24 24 25 25 26 26 26 26 26 26 27 27 27 27 27 28 28 28 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 26 26 26 26 26 26 26 26 25 25 25 25 25 25 24 24 24 24 23 23 23 23 22 22 22 21 21 21 21 20 20 20 19 19 19 18 18 18 18 18 17 17 17 17 17 16 16 16 16 15 15 15 15 14 14 14 14 13 13 13 13 12 12 12 11 11 11 10 10 10 10 10 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 10 10 10 10 10 11 11 11 11 11 11 12 12 12 12 12 13 13 13 13 13 14 14 14 14 14 15 15 15 15 16 16 16 16 17 17 17 17 18 18 18 18 18 18 18 18 18 18 19 19 19 19 20 20 20 20 21 21 21 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 21 21 21 21 21 20 20 20 20 19 19 18 18 17 17 16 16 15 15 14 14 14 14 13 13 13 13 13 13 12 12 12 11 11 10 10 9 9 8 8 8 7 7 6 6 6 5 5 5 4 4 4 3 3 3 3 2 2 2 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 2 2 2 3 3 3 - 4 3 3 3 3 3 3 3 2 2 2 2 2 2 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 2 2 2 2 2 3 3 3 3 3 4 4 4 4 4 5 5 5 5 5 6 6 6 6 7 7 7 7 7 8 8 8 8 8 9 9 9 9 10 10 11 11 12 12 13 13 14 14 15 15 16 16 17 17 18 19 19 20 20 20 21 21 22 22 22 23 23 24 24 24 25 25 25 25 26 26 26 26 26 27 27 27 27 27 27 27 27 27 27 27 27 27 28 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 26 26 26 26 26 26 25 25 25 25 25 24 24 24 24 24 23 23 23 23 22 22 22 21 21 21 20 20 20 20 19 19 19 19 18 18 18 18 18 17 17 17 17 16 16 16 15 15 15 15 14 14 14 13 13 13 12 12 12 12 11 11 11 11 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 11 11 11 11 11 12 12 12 12 12 12 13 13 13 13 13 14 14 14 14 15 15 15 15 15 16 16 16 16 17 17 17 17 18 18 18 18 19 19 19 19 19 19 19 20 20 20 20 20 20 20 20 21 21 21 21 22 22 22 22 23 23 23 23 22 22 22 22 22 22 22 22 22 22 21 21 21 20 20 20 20 19 19 19 18 18 17 17 16 16 15 15 14 14 13 13 12 12 12 12 11 11 11 11 11 10 10 10 9 9 8 8 8 7 7 6 6 6 5 5 5 4 4 4 3 3 3 3 2 2 2 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 2 2 2 3 3 4 - 3 3 3 3 2 2 2 2 2 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 2 2 2 2 3 3 3 3 4 4 4 4 4 4 5 5 5 5 5 5 6 6 6 6 7 7 7 7 8 8 8 8 9 9 10 10 11 11 12 12 12 13 13 14 14 15 15 16 16 17 17 18 18 19 19 20 20 20 21 21 22 22 22 23 23 23 24 24 24 25 25 25 25 26 26 26 27 27 27 27 27 27 27 27 27 27 28 28 28 28 28 28 28 28 28 28 28 28 28 28 27 27 27 27 27 27 27 27 26 26 26 26 26 26 25 25 25 25 25 25 25 24 24 24 23 23 23 23 22 22 22 22 21 21 21 20 20 20 20 19 19 19 19 18 18 18 18 17 17 17 16 16 16 16 15 15 15 14 14 14 13 13 13 12 12 12 12 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 12 12 12 12 12 13 13 13 13 13 14 14 14 14 14 15 15 15 15 15 16 16 16 16 17 17 17 17 18 18 18 18 19 19 19 20 20 20 21 21 21 21 21 21 21 21 21 21 21 21 21 21 22 22 22 22 22 22 23 23 23 23 23 23 22 22 22 22 22 22 22 21 21 21 21 20 20 20 20 19 19 19 18 18 18 17 16 16 15 15 14 14 13 13 12 12 11 11 11 10 10 10 9 9 8 8 8 8 7 7 7 6 6 6 5 5 5 5 4 4 4 3 3 3 2 2 2 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 2 2 2 3 3 3 - 3 3 2 2 2 2 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 2 2 2 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 5 5 5 6 6 6 6 7 7 7 8 8 9 9 9 10 10 11 11 11 12 12 13 13 14 14 15 15 16 16 17 17 18 18 18 19 19 20 20 20 21 21 21 22 22 22 23 23 23 24 24 25 25 25 26 26 26 26 26 27 27 27 27 27 27 28 28 28 28 28 28 28 28 28 28 28 28 28 28 28 28 28 28 27 27 27 27 27 27 27 27 26 26 26 26 26 26 26 26 26 25 25 25 25 24 24 24 24 23 23 23 23 22 22 22 22 21 21 21 20 20 20 20 19 19 19 18 18 18 17 17 17 17 16 16 16 15 15 15 14 14 14 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 13 13 13 13 13 14 14 14 14 15 15 15 15 15 16 16 16 16 16 17 17 17 17 18 18 18 18 18 19 19 19 20 20 20 21 21 21 22 22 22 22 22 22 22 22 22 22 22 22 22 22 23 23 23 23 23 23 23 23 23 23 23 23 23 23 22 22 22 22 21 21 21 21 20 20 20 19 19 19 19 18 18 18 17 17 16 16 15 15 14 13 13 12 12 11 11 10 10 9 9 8 8 7 7 6 6 6 5 5 5 5 5 5 4 4 4 4 4 3 3 3 2 2 2 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 2 2 2 3 3 - 2 2 2 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 2 2 2 2 2 2 3 3 3 3 3 3 3 4 4 4 4 5 5 5 5 6 6 6 7 7 7 8 8 8 9 9 10 10 10 11 11 12 12 13 13 14 14 15 15 16 16 17 17 17 18 18 18 19 19 19 20 20 21 21 21 22 22 23 23 23 24 24 25 25 25 25 26 26 26 26 26 27 27 27 27 27 27 28 28 28 28 28 28 28 28 28 28 28 28 28 28 28 28 28 28 28 28 27 27 27 27 27 27 27 27 27 27 27 27 26 26 26 26 26 25 25 25 25 24 24 24 24 23 23 23 22 22 22 21 21 21 21 20 20 20 19 19 19 18 18 18 17 17 17 16 16 16 16 15 15 15 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 13 13 13 13 14 14 14 14 14 15 15 15 15 16 16 16 16 16 17 17 17 17 18 18 18 18 18 19 19 19 19 20 20 20 20 21 21 21 22 22 22 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 22 22 22 21 21 21 20 20 20 19 19 19 18 18 18 17 17 17 16 16 16 15 15 14 14 13 13 12 11 11 10 10 9 9 8 8 7 7 7 6 6 5 5 4 4 4 4 4 4 3 3 3 3 3 2 2 2 2 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 2 2 2 - 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 2 2 2 2 2 3 3 3 3 3 4 4 4 4 5 5 5 5 6 6 6 7 7 7 8 8 9 9 9 10 10 11 11 12 12 13 13 14 14 14 15 15 16 16 17 17 17 18 18 18 19 19 19 20 20 21 21 22 22 22 23 23 24 24 25 25 25 25 25 26 26 26 26 27 27 27 27 27 27 27 27 27 28 28 28 28 28 28 28 28 28 28 28 28 28 28 28 28 28 28 28 28 28 28 28 28 28 28 28 27 27 27 27 27 26 26 26 26 26 25 25 25 25 24 24 24 23 23 22 22 22 21 21 21 20 20 20 20 19 19 19 18 18 18 17 17 17 17 16 16 16 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 15 15 15 15 15 16 16 16 16 16 17 17 17 17 18 18 18 18 19 19 19 19 20 20 20 20 21 21 21 21 21 22 22 22 22 23 23 23 23 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 23 23 23 23 23 23 23 23 23 23 22 22 21 21 21 20 20 19 19 18 18 18 17 17 17 16 16 16 15 15 15 14 14 13 13 12 11 11 10 10 9 9 8 8 7 7 7 6 6 5 5 5 4 4 4 3 3 3 3 2 2 2 2 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 2 2 2 2 3 3 3 3 4 4 4 4 4 5 5 5 5 6 6 6 7 7 8 8 8 9 9 10 10 11 11 11 12 12 13 13 14 14 15 15 15 16 16 16 17 17 18 18 18 19 19 20 20 21 21 22 22 23 23 24 24 24 24 25 25 25 25 26 26 26 26 26 27 27 27 27 27 27 27 27 28 28 28 28 28 28 28 28 28 28 28 29 29 29 29 29 29 29 29 29 29 29 29 29 29 28 28 28 28 28 28 27 27 27 27 27 26 26 26 25 25 25 24 24 24 23 23 22 22 22 21 21 21 20 20 20 19 19 19 18 18 18 18 18 17 17 17 17 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 16 16 16 16 16 17 17 17 17 17 18 18 18 18 19 19 19 19 20 20 20 20 21 21 21 21 22 22 22 22 23 23 23 23 23 24 24 24 24 24 25 25 25 25 25 25 25 25 24 24 24 24 24 24 24 24 24 24 23 23 23 23 23 23 22 22 21 21 20 20 19 19 18 18 17 17 16 16 15 15 15 14 14 14 13 13 13 12 11 11 10 10 9 9 8 8 7 7 6 6 6 5 5 5 4 4 4 3 3 3 2 2 2 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 2 2 2 2 3 3 3 4 4 4 4 5 5 5 6 6 6 7 7 7 7 8 8 9 9 9 10 10 10 11 11 12 12 12 13 13 14 14 14 15 15 15 16 16 16 17 17 18 18 19 19 20 20 21 21 22 22 23 23 23 23 23 24 24 24 24 24 25 25 25 25 26 26 26 26 27 27 27 27 27 28 28 28 28 28 28 28 28 28 29 29 29 29 29 29 29 29 29 29 29 29 29 29 28 28 28 28 28 28 28 27 27 27 27 26 26 26 25 25 25 24 24 24 23 23 23 22 22 22 21 21 21 20 20 20 19 19 19 19 19 18 18 18 18 18 18 17 17 17 17 18 18 18 18 18 18 18 18 18 17 17 17 17 17 17 17 17 17 16 17 17 17 17 17 17 17 17 18 18 18 18 18 18 19 19 19 19 20 20 20 20 20 21 21 21 21 22 22 22 22 22 23 23 23 23 23 24 24 24 24 24 24 25 25 25 25 24 24 24 24 24 24 23 23 23 23 23 23 23 22 22 22 22 22 21 21 21 20 20 19 19 18 17 17 16 16 15 15 14 14 14 13 13 12 12 12 11 11 11 10 10 9 9 8 8 8 7 7 6 6 5 5 5 4 4 4 3 3 3 2 2 2 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 2 2 2 3 3 3 3 4 4 4 5 5 5 6 6 6 6 7 7 7 7 8 8 8 9 9 9 10 10 10 10 11 11 12 12 12 13 13 14 14 14 15 15 16 16 17 17 18 18 19 19 20 20 21 21 21 21 21 21 22 22 22 22 22 22 23 23 24 24 24 25 25 26 26 26 27 27 27 27 27 28 28 28 28 28 28 28 29 29 29 29 29 29 29 29 29 29 29 29 29 29 29 28 28 28 28 28 28 28 28 28 27 27 26 26 26 25 25 25 24 24 24 23 23 23 22 22 22 21 21 21 20 20 20 20 20 20 20 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 18 18 18 18 18 18 18 18 18 18 18 18 19 19 19 19 19 19 19 19 20 20 20 20 20 21 21 21 21 21 22 22 22 22 22 22 23 23 23 23 23 23 24 24 24 24 24 24 25 25 24 24 24 24 24 23 23 23 23 22 22 22 22 21 21 21 21 21 20 20 20 20 20 19 18 18 17 16 16 15 15 14 13 13 12 12 12 11 11 10 10 10 9 9 9 8 8 8 7 7 7 6 6 6 5 5 5 4 4 3 3 3 2 2 2 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 2 2 3 3 3 4 4 4 4 5 5 5 5 6 6 6 6 6 7 7 7 7 7 8 8 8 8 8 9 9 10 10 10 11 11 12 12 13 13 13 14 14 15 15 16 16 17 17 18 19 19 19 19 19 19 20 20 20 20 20 20 20 21 21 22 22 23 24 24 25 25 26 26 26 27 27 27 27 27 27 28 28 28 28 28 28 28 28 29 29 29 29 29 29 29 29 29 29 29 29 29 29 29 29 29 29 28 28 28 27 27 27 26 26 26 25 25 25 24 24 24 23 23 23 23 22 22 22 21 21 21 21 21 21 21 21 21 20 20 20 20 20 20 20 20 21 21 21 21 21 21 21 21 21 20 20 20 20 20 20 20 19 19 19 19 19 19 19 19 20 20 20 20 20 20 20 20 20 20 20 21 21 21 21 21 21 22 22 22 22 22 22 22 23 23 23 23 23 23 23 24 24 24 24 24 24 24 24 24 24 24 23 23 23 22 22 22 21 21 21 20 20 20 20 19 19 19 19 19 18 18 17 17 16 15 15 14 13 13 12 12 11 11 10 10 9 9 9 8 8 7 7 7 6 6 6 6 6 5 5 5 5 4 4 4 3 3 3 2 2 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 2 2 2 2 3 3 3 3 4 4 4 5 5 5 5 6 6 6 6 6 7 7 7 7 8 8 8 9 9 9 10 10 10 11 11 11 12 12 13 13 14 14 15 15 16 16 17 17 17 17 17 18 18 18 18 18 18 18 18 19 20 20 21 21 22 22 23 23 24 24 25 25 25 25 25 26 26 26 26 26 26 27 27 27 27 27 27 27 27 28 28 28 28 28 28 28 28 28 28 28 28 28 28 28 28 27 27 27 27 26 26 26 25 25 25 25 24 24 24 24 24 23 23 23 23 22 22 22 22 22 22 22 22 22 22 22 21 21 21 21 21 22 22 22 22 22 22 22 22 22 21 21 21 21 21 21 21 21 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 21 21 21 21 21 21 21 21 21 21 21 21 21 22 22 22 22 22 22 22 22 23 23 23 23 23 23 22 22 22 21 21 20 20 20 19 19 19 18 18 18 18 18 17 17 17 17 16 16 16 15 14 14 13 13 12 12 11 11 10 10 9 9 8 8 8 7 7 6 6 6 5 5 5 5 4 4 4 3 3 3 3 2 2 2 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 2 2 2 2 3 3 3 4 4 4 5 5 5 5 6 6 6 6 7 7 7 7 8 8 8 9 9 9 9 10 10 10 11 11 12 12 13 13 14 14 14 15 15 15 16 16 16 16 16 16 16 16 16 16 17 18 18 19 19 20 20 21 22 22 23 23 23 23 23 24 24 24 24 24 25 25 25 25 25 25 26 26 26 26 26 27 27 27 27 27 27 27 27 27 27 27 27 28 28 27 27 27 27 26 26 26 26 25 25 25 25 25 25 24 24 24 24 24 24 24 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 22 22 22 22 22 22 22 22 22 21 21 21 21 21 21 21 21 21 21 21 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 21 21 21 21 21 21 21 22 22 22 21 21 21 20 20 19 19 18 18 17 17 17 16 16 16 16 16 15 15 15 15 15 14 14 13 13 12 12 11 11 11 10 10 9 9 8 8 8 7 7 6 6 6 5 5 4 4 4 3 3 3 2 2 2 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 2 2 2 3 3 3 3 4 4 4 5 5 5 5 6 6 6 6 7 7 7 7 8 8 8 8 9 9 9 10 10 11 11 11 12 12 13 13 13 14 14 14 14 14 14 14 14 14 15 15 15 16 16 17 18 18 19 19 20 20 21 21 21 22 22 22 22 22 22 23 23 23 23 23 24 24 24 24 24 25 25 25 25 26 26 26 26 26 26 26 26 27 27 27 27 27 26 26 26 26 26 26 25 25 25 25 25 25 25 25 25 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 22 22 22 22 22 22 22 22 22 21 21 21 21 21 21 21 20 20 20 20 20 20 20 20 20 20 20 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 20 20 20 20 20 20 20 20 19 19 18 18 18 17 17 16 16 15 15 15 14 14 14 14 13 13 13 13 13 12 12 12 11 11 10 10 10 9 9 9 8 8 7 7 7 6 6 5 5 5 4 4 3 3 3 2 2 2 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 2 2 2 2 3 3 3 4 4 4 5 5 5 5 6 6 6 7 7 7 7 8 8 8 8 9 9 9 9 10 10 10 11 11 11 11 12 12 12 12 12 13 13 13 13 13 14 14 15 15 16 16 17 17 18 18 19 19 19 19 20 20 20 20 20 20 21 21 21 21 21 22 22 22 22 23 23 23 23 24 24 24 24 24 24 24 25 25 25 25 25 25 25 25 25 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 22 22 22 22 22 22 22 22 22 21 21 21 21 21 21 20 20 20 20 20 19 19 19 19 19 19 19 19 19 18 18 18 18 18 18 18 18 18 18 17 17 17 17 17 17 17 17 17 18 18 18 18 18 18 18 18 17 17 17 16 16 15 15 15 14 14 13 13 13 13 12 12 12 12 11 11 11 10 10 10 9 9 9 8 8 8 8 7 7 7 6 6 6 5 5 5 4 4 4 3 3 3 2 2 2 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 2 2 2 3 3 3 4 4 4 5 5 5 5 6 6 6 6 7 7 7 7 7 8 8 8 8 8 8 8 9 9 9 9 10 10 10 11 11 11 12 12 12 13 13 13 14 14 15 15 16 16 16 17 17 17 17 18 18 18 18 18 18 18 19 19 19 19 20 20 20 20 21 21 21 22 22 22 22 22 22 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 22 22 22 23 23 23 23 23 23 23 23 23 24 24 24 24 24 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 22 22 22 22 22 22 22 22 22 22 21 21 21 21 21 21 21 20 20 20 20 19 19 19 19 18 18 18 18 18 18 18 18 17 17 17 17 17 17 17 16 16 16 16 16 16 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 15 15 15 14 14 14 13 13 13 12 12 12 11 11 11 10 10 10 9 9 9 8 8 8 8 7 7 7 7 6 6 6 6 5 5 5 5 4 4 4 4 3 3 3 3 2 2 2 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 2 2 2 3 3 3 3 4 4 4 5 5 5 5 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 8 8 8 9 9 9 10 10 10 11 11 12 12 12 13 13 13 14 14 15 15 15 15 15 16 16 16 16 16 16 16 17 17 17 17 18 18 18 18 19 19 19 20 20 20 20 20 20 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 22 22 22 22 22 22 23 23 23 23 23 23 23 23 23 23 23 23 23 23 22 22 22 22 22 22 22 22 21 21 21 21 21 21 21 21 21 21 20 20 20 20 20 20 19 19 19 19 18 18 18 17 17 17 17 17 17 16 16 16 16 16 16 16 15 15 15 15 15 14 14 14 14 14 13 13 13 13 13 14 14 14 14 14 14 14 14 14 13 13 13 13 12 12 12 11 11 11 11 10 10 10 9 9 8 8 8 7 7 7 7 6 6 6 6 6 5 5 5 5 5 4 4 4 4 4 3 3 3 3 2 2 2 2 2 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 2 2 2 2 3 3 3 4 4 4 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 7 7 7 8 8 8 9 9 9 10 10 10 10 11 11 11 12 12 12 13 13 13 13 14 14 14 14 14 14 14 15 15 15 15 15 16 16 16 16 17 17 17 17 18 18 18 18 18 18 18 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 20 20 20 20 20 20 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 20 20 20 20 20 20 20 20 19 19 19 19 19 19 19 19 19 19 18 18 18 18 18 17 17 17 16 16 16 15 15 15 15 15 15 14 14 14 14 14 14 14 13 13 13 13 13 12 12 12 12 12 11 11 11 11 11 11 12 12 12 12 12 12 12 12 11 11 11 11 10 10 10 10 9 9 9 8 8 8 8 7 7 7 6 6 6 6 5 5 5 5 5 5 5 4 4 4 4 4 3 3 3 3 2 2 2 2 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 2 2 2 3 3 3 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 6 6 6 7 7 7 8 8 8 8 9 9 9 10 10 10 10 11 11 11 11 12 12 12 12 12 12 12 13 13 13 13 13 13 14 14 14 14 15 15 15 15 15 15 16 16 16 16 16 16 16 16 17 17 17 17 17 17 17 17 17 17 17 17 17 17 18 18 18 18 18 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 18 18 18 18 18 18 18 18 18 17 17 17 17 17 17 17 17 17 17 16 16 16 15 15 15 14 14 14 14 13 13 13 13 13 12 12 12 12 12 12 12 11 11 11 11 11 10 10 10 10 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 9 9 9 9 8 8 8 8 7 7 7 7 7 6 6 6 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 2 2 2 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 5 5 5 6 6 6 7 7 7 7 8 8 8 9 9 9 9 10 10 10 10 10 10 11 11 11 11 11 11 11 12 12 12 12 12 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 15 15 15 15 16 16 16 16 16 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 14 14 14 14 13 13 13 12 12 12 11 11 11 11 11 11 10 10 10 10 10 10 10 9 9 9 9 9 8 8 8 8 8 7 7 7 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 6 6 6 6 6 5 5 5 5 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 3 3 4 4 4 5 5 5 6 6 6 7 7 7 7 8 8 8 8 9 9 9 9 9 9 10 10 10 10 10 10 11 11 11 11 11 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 12 12 12 12 11 11 11 11 10 10 10 9 9 9 9 9 9 9 9 8 8 8 8 8 8 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 6 6 6 6 6 6 5 5 5 5 5 5 4 4 4 3 3 3 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 2 2 2 2 3 2 2 2 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 2 2 3 3 3 4 4 5 5 5 5 6 6 6 6 7 7 7 7 8 8 8 8 8 8 9 9 9 9 9 10 10 10 10 10 11 11 11 11 11 11 12 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 10 10 10 10 9 9 9 9 8 8 8 8 8 8 7 7 7 7 7 7 7 7 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 3 3 3 2 2 2 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 2 2 3 3 3 4 4 4 4 4 3 3 3 3 3 3 3 3 3 2 2 2 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 2 2 2 2 2 2 2 2 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 2 2 2 3 3 3 3 4 4 4 4 5 5 5 5 6 6 6 6 7 7 7 7 7 8 8 8 8 8 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 8 8 8 8 8 7 7 7 7 7 6 6 6 6 6 6 6 6 5 5 5 5 5 5 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 2 2 2 3 3 3 4 4 4 4 4 4 4 3 3 3 3 3 3 3 2 2 2 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 2 2 2 3 3 3 3 4 4 4 5 5 5 5 5 6 6 6 6 6 7 7 7 7 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 6 6 6 6 6 5 5 5 5 5 5 5 5 4 4 4 4 4 4 3 3 3 3 2 2 2 2 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 2 2 2 2 2 3 3 3 3 4 4 4 3 3 3 3 3 2 2 2 2 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 2 2 2 2 3 3 3 2 2 2 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 2 2 2 3 3 3 3 4 4 4 4 5 5 5 5 5 6 6 6 6 6 7 7 7 7 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 3 3 3 3 3 2 2 2 2 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 2 2 2 2 2 3 3 3 3 3 3 3 3 3 4 3 3 3 2 2 2 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 2 2 2 2 2 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 2 2 2 2 2 3 3 3 3 4 4 4 4 4 5 5 5 5 5 6 6 6 6 6 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 2 2 2 2 3 3 3 3 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 2 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 2 2 2 2 3 3 2 2 2 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 2 2 2 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 2 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 2 2 3 3 3 4 4 3 3 2 2 2 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 2 2 2 2 2 2 2 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 2 2 3 3 3 4 4 4 4 3 3 3 3 2 2 2 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 2 2 3 3 4 4 5 5 4 4 4 4 3 3 3 3 2 2 2 2 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 2 2 3 3 4 4 5 5 5 4 4 4 4 4 4 3 3 3 3 3 2 2 2 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 2 2 2 3 3 4 4 4 4 4 4 4 3 3 3 3 3 3 3 2 2 2 2 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 2 2 2 2 2 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 2 2 2 3 2 2 2 2 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 2 2 2 2 3 3 2 2 2 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 2 2 2 3 3 3 2 2 2 2 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 2 2 2 2 2 2 2 2 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 2 2 2 2 2 2 2 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 2 2 2 2 2 2 2 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 2 2 2 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 2 2 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 2 2 2 2 2 2 2 2 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 2 2 2 2 2 3 3 2 2 2 2 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 2 2 2 2 2 2 2 2 2 3 3 3 3 3 2 2 2 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 2 2 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 2 2 2 2 3 3 2 2 2 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 2 2 2 2 2 2 3 2 2 2 2 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 3 3 2 2 2 2 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 2 2 2 2 1 1 1 1 1 0 0 0 0 0 0 0 0 0 1 1 1 1 1 2 2 2 2 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 4 4 4 4 4 3 3 3 3 3 3 3 3 3 2 2 2 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 2 2 2 2 2 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 2 2 2 2 3 3 3 2 2 2 2 1 1 1 0 0 0 0 0 1 1 1 1 2 2 2 2 3 3 2 2 2 2 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 2 2 2 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 2 2 2 2 2 2 2 2 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 3 3 3 3 3 3 3 3 3 2 2 2 2 2 1 1 1 1 2 2 2 2 2 2 3 3 3 3 3 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 3 3 3 3 3 3 3 3 4 4 4 3 3 3 3 3 3 3 3 3 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 0 0 0 0 0 0 0 0 0 1 1 1 1 2 2 2 2 2 2 1 1 1 1 0 0 0 0 0 0 0 0 1 1 1 1 2 2 2 2 2 2 2 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 2 2 2 2 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 2 2 2 2 3 3 3 3 2 2 2 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 2 2 2 2 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 1 1 1 1 0 0 0 1 1 1 1 2 2 2 2 3 3 3 2 2 2 2 2 1 1 1 1 0 0 1 1 1 1 2 2 2 2 3 3 3 3 3 2 2 2 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 2 2 2 2 2 3 2 2 2 2 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 2 2 2 3 3 3 3 2 2 2 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 2 2 2 2 2 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 2 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 - 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 - 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - 3 3 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 - 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 - 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 - 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 - 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 - 8 8 8 8 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 - 9 9 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 16 16 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 - 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 11 - 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 17 17 17 17 17 17 17 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 - 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 - 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 - 14 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 17 17 17 17 17 17 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 16 16 16 16 16 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 16 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 - 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 16 16 16 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 17 17 17 17 17 17 17 17 17 17 17 17 17 17 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 - 16 16 16 16 16 16 16 16 16 16 16 16 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 16 16 17 17 17 17 17 17 17 17 17 17 17 17 17 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 17 17 17 17 17 17 17 17 17 17 17 17 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 15 15 15 15 16 16 16 16 16 16 - 16 16 16 16 16 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 16 16 17 17 17 17 17 17 17 17 17 17 17 17 18 18 18 18 18 18 18 18 18 18 18 18 18 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 18 18 18 18 18 18 18 18 18 18 18 18 18 17 17 17 17 17 17 17 17 17 17 17 17 17 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 - 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 16 17 17 17 17 17 17 17 17 17 17 17 18 18 18 18 18 18 18 18 18 18 18 18 18 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 18 18 18 18 18 18 18 18 18 18 18 18 18 17 17 17 17 17 17 17 17 17 17 17 17 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 17 17 17 17 17 17 17 17 - 17 17 17 17 17 17 17 17 17 17 17 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 17 17 17 17 17 17 17 17 17 17 17 18 18 18 18 18 18 18 18 18 18 18 18 19 19 19 19 19 19 19 19 19 19 19 19 19 19 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 18 18 18 18 18 18 18 18 18 18 18 18 18 17 17 17 17 17 17 17 17 17 17 17 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 - 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 17 17 17 17 17 17 17 17 17 17 18 18 18 18 18 18 18 18 18 18 18 19 19 19 19 19 19 19 19 19 19 19 19 19 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 19 19 19 19 19 19 19 19 19 19 19 19 19 19 18 18 18 18 18 18 18 18 18 18 18 18 18 17 17 17 17 17 17 17 17 17 17 17 17 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 - 19 19 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 16 17 17 17 17 17 17 17 17 17 18 18 18 18 18 18 18 18 18 18 18 19 19 19 19 19 19 19 19 19 19 19 19 20 20 20 20 20 20 20 20 20 20 20 20 20 20 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 19 19 19 19 19 19 19 19 19 19 19 19 19 18 18 18 18 18 18 18 18 18 18 18 18 18 17 17 17 17 17 17 17 17 17 17 17 17 17 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 19 19 - 19 19 19 19 19 19 19 19 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 17 17 17 17 17 17 17 17 17 17 18 18 18 18 18 18 18 18 18 18 19 19 19 19 19 19 19 19 19 19 19 20 20 20 20 20 20 20 20 20 20 20 20 20 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 19 19 19 19 19 19 19 19 19 19 19 19 19 19 18 18 18 18 18 18 18 18 18 18 18 18 18 18 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 - 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 17 17 17 17 17 17 17 17 17 18 18 18 18 18 18 18 18 18 18 19 19 19 19 19 19 19 19 19 19 19 20 20 20 20 20 20 20 20 20 20 20 20 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 18 18 - 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 17 17 17 17 17 17 17 17 17 18 18 18 18 18 18 18 18 18 18 19 19 19 19 19 19 19 19 19 19 20 20 20 20 20 20 20 20 20 20 20 20 21 21 21 21 21 21 21 21 21 21 21 21 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 18 18 18 18 18 18 - 18 18 18 18 18 18 18 18 18 18 18 18 18 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 17 17 17 17 17 17 17 17 17 18 18 18 18 18 18 18 18 18 18 19 19 19 19 19 19 19 19 19 19 20 20 20 20 20 20 20 20 20 20 20 21 21 21 21 21 21 21 21 21 21 21 21 22 22 22 22 22 22 22 22 22 22 22 22 22 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 18 18 18 18 18 18 18 18 18 18 18 18 18 - 18 18 18 18 18 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 17 17 17 17 17 17 17 17 17 18 18 18 18 18 18 18 18 18 19 19 19 19 19 19 19 19 19 19 20 20 20 20 20 20 20 20 20 20 20 21 21 21 21 21 21 21 21 21 21 21 22 22 22 22 22 22 22 22 22 22 22 22 23 23 23 23 23 23 23 23 23 23 23 23 23 23 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 - 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 17 17 17 17 17 17 17 17 17 18 18 18 18 18 18 18 18 18 19 19 19 19 19 19 19 19 19 19 20 20 20 20 20 20 20 20 20 21 21 21 21 21 21 21 21 21 21 21 22 22 22 22 22 22 22 22 22 22 22 23 23 23 23 23 23 23 23 23 23 23 23 23 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 17 17 - 17 17 17 17 17 17 17 17 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 17 17 17 17 17 17 17 17 17 18 18 18 18 18 18 18 18 18 18 19 19 19 19 19 19 19 19 19 20 20 20 20 20 20 20 20 20 20 20 21 21 21 21 21 21 21 21 21 21 22 22 22 22 22 22 22 22 22 22 22 22 23 23 23 23 23 23 23 23 23 23 23 23 23 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 17 17 17 17 17 17 17 17 17 - 17 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 17 17 17 17 17 17 17 17 17 17 18 18 18 18 18 18 18 18 18 18 19 19 19 19 19 19 19 19 19 19 20 20 20 20 20 20 20 20 20 20 21 21 21 21 21 21 21 21 21 21 21 22 22 22 22 22 22 22 22 22 22 22 22 23 23 23 23 23 23 23 23 23 23 23 23 23 23 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 - 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 17 17 17 17 17 17 17 17 17 17 18 18 18 18 18 18 18 18 18 18 19 19 19 19 19 19 19 19 19 19 20 20 20 20 20 20 20 20 20 20 20 21 21 21 21 21 21 21 21 21 21 21 21 22 22 22 22 22 22 22 22 22 22 22 22 23 23 23 23 23 23 23 23 23 23 23 23 23 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 17 17 17 17 17 17 17 17 17 17 17 17 17 17 16 16 16 16 16 16 - 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 16 17 17 17 17 17 17 17 17 17 17 18 18 18 18 18 18 18 18 18 18 19 19 19 19 19 19 19 19 19 19 19 20 20 20 20 20 20 20 20 20 20 20 21 21 21 21 21 21 21 21 21 21 21 21 22 22 22 22 22 22 22 22 22 22 22 22 22 23 23 23 23 23 23 23 23 23 23 23 23 23 23 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 17 17 17 17 17 17 17 17 17 17 17 17 17 17 16 16 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 - 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 16 17 17 17 17 17 17 17 17 17 17 17 18 18 18 18 18 18 18 18 18 18 19 19 19 19 19 19 19 19 19 19 19 20 20 20 20 20 20 20 20 20 20 20 20 21 21 21 21 21 21 21 21 21 21 21 21 22 22 22 22 22 22 22 22 22 22 22 22 22 22 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 16 16 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 - 14 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 16 17 17 17 17 17 17 17 17 17 17 17 18 18 18 18 18 18 18 18 18 18 18 19 19 19 19 19 19 19 19 19 19 19 19 20 20 20 20 20 20 20 20 20 20 20 20 21 21 21 21 21 21 21 21 21 21 21 21 21 21 22 22 22 22 22 22 22 22 22 22 22 22 22 22 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 14 14 - 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 16 16 17 17 17 17 17 17 17 17 17 17 17 17 18 18 18 18 18 18 18 18 18 18 18 18 19 19 19 19 19 19 19 19 19 19 19 19 19 19 20 20 20 20 20 20 20 20 20 20 20 20 20 20 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 13 - 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 16 16 16 16 17 17 17 17 17 17 17 17 17 17 17 17 17 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 - 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 16 16 16 16 16 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 - 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 - 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 - 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 - 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 - 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 - 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 - 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 - 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 - 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 - 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 - 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 - 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 - 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 - 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 - 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 - 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 - 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 - 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 - 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 - 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 - 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 diff --git a/tests/fixtures/bom_weather.json b/tests/fixtures/bom_weather.json deleted file mode 100644 index d40ea6fb21a..00000000000 --- a/tests/fixtures/bom_weather.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "observations": { - "data": [ - { - "wmo": 94767, - "name": "Fake", - "history_product": "IDN00000", - "local_date_time_full": "20180422130000", - "apparent_t": 25.0, - "press": 1021.7, - "weather": "-" - }, - { - "wmo": 94767, - "name": "Fake", - "history_product": "IDN00000", - "local_date_time_full": "20180422130000", - "apparent_t": 22.0, - "press": 1019.7, - "weather": "-" - }, - { - "wmo": 94767, - "name": "Fake", - "history_product": "IDN00000", - "local_date_time_full": "20180422130000", - "apparent_t": 20.0, - "press": 1011.7, - "weather": "Fine" - }, - { - "wmo": 94767, - "name": "Fake", - "history_product": "IDN00000", - "local_date_time_full": "20180422130000", - "apparent_t": 18.0, - "press": 1010.0, - "weather": "-" - } - ] - } -} diff --git a/tests/fixtures/coinmarketcap.json b/tests/fixtures/coinmarketcap.json deleted file mode 100644 index 9484058be4d..00000000000 --- a/tests/fixtures/coinmarketcap.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "cached": false, - "data": { - "id": 1027, - "name": "Ethereum", - "symbol": "ETH", - "website_slug": "ethereum", - "rank": 2, - "circulating_supply": 99619842.0, - "total_supply": 99619842.0, - "max_supply": null, - "quotes": { - "USD": { - "price": 577.019, - "volume_24h": 2839960000.0, - "market_cap": 57482541899.0, - "percent_change_1h": -2.28, - "percent_change_24h": -14.88, - "percent_change_7d": -17.51 - }, - "EUR": { - "price": 493.454724572, - "volume_24h": 2428699712.48, - "market_cap": 49158380042.0, - "percent_change_1h": -2.28, - "percent_change_24h": -14.88, - "percent_change_7d": -17.51 - } - }, - "last_updated": 1527098658 - }, - "metadata": { - "timestamp": 1527098716, - "error": null - } -} diff --git a/tests/fixtures/darksky.json b/tests/fixtures/darksky.json deleted file mode 100644 index 26df6b60210..00000000000 --- a/tests/fixtures/darksky.json +++ /dev/null @@ -1,1475 +0,0 @@ -{ - "alerts": [ - { - "title": "Winter Storm Watch", - "regions": [ - "Burney Basin / Eastern Shasta County", - "Mountains Southwestern Shasta County to Northern Lake County", - "West Slope Northern Sierra Nevada", - "Western Plumas County/Lassen Park" - ], - "severity": "watch", - "time": 1554422400, - "expires": 1554552000, - "description": "...Hazardous mountain travel expected over 6000 feet Thursday through late Friday night... .Snow is expected to begin Thursday afternoon increasing in intensity during the evening hours. Heavy snow and gusty winds are forecast Friday afternoon which will lead to hazardous travel over the mountains. Snow will begin to diminish during the late evening and overnight hours Friday night into Saturday morning. ...WINTER STORM WATCH IN EFFECT FROM THURSDAY AFTERNOON THROUGH LATE FRIDAY NIGHT... * WHAT...Periods of heavy snow possible. Plan on difficult travel conditions, including during the afternoon and evening hours on Friday. Total snow accumulations of 8 to 12 inches, with localized amounts up to 2 and a half feet possible. * WHERE...Western Plumas County/Lassen Park and West Slope Northern Sierra Nevada. * WHEN...From Thursday afternoon through late Friday night. * ADDITIONAL DETAILS...Be prepared for reduced visibilities at times. PRECAUTIONARY/PREPAREDNESS ACTIONS... A Winter Storm Watch means there is potential for significant snow, sleet or ice accumulations that may impact travel. Continue to monitor the latest forecasts.\n", - "uri": "https://alerts.weather.gov/cap/wwacapget.php?x=CA125CF1CFB088.WinterStormWatch.125CF1FC1240CA.STOWSWSTO.1fc288d0ac8e931cb17b5f5be62efff0" - }, - { - "title": "Red Flag Warning", - "regions": ["Guadalupe Mountains", "Southeast Plains"], - "severity": "warning", - "time": 1554300000, - "expires": 1554350400, - "description": "...RED FLAG WARNING IN EFFECT FROM 9 AM CDT /8 AM MDT/ THIS MORNING TO 11 PM CDT /10 PM MDT/ THIS EVENING FOR RELATIVE HUMIDITY OF 15% OR LESS, 20 FT WINDS OF 20 MPH OR MORE, AND HIGH TO EXTREME FIRE DANGER FOR THE GUADALUPE, DAVIS, AND APACHE MOUNTAINS, SOUTHEAST NEW MEXICO PLAINS, REEVES COUNTY AND THE UPPER TRANS PECOS, AND VAN HORN AND THE HIGHWAY 54 CORRIDOR... .Critical fire weather conditions are expected today and this evening across Southeastern New Mexico and areas south as far as the Davis and Apache Mountains. Southwest to west 20 ft winds will increase as an upper-level disturbance passes through the region. Combined with unseasonably warm temperatures, consequent low relative humidity, and cured fuels, potential for fire growth will increase. ...RED FLAG WARNING IN EFFECT FROM 9 AM CDT /8 AM MDT/ THIS MORNING TO 11 PM CDT /10 PM MDT/ THIS EVENING FOR RELATIVE HUMIDITY OF 15% OR LESS, 20 FT WINDS OF 20 MPH OR MORE, AND HIGH TO EXTREME FIRE DANGER... * WIND...Mountains...West 20 to 25 mph increasing to 30 to 40 mph in the afternoon. Plains...Southwest 20 to 25 mph. * HUMIDITY...8% to 15%. * IMPACTS...any fires that develop will likely spread rapidly. Outdoor burning is not recommended.\n", - "uri": "https://alerts.weather.gov/cap/wwacapget.php?x=NM125CF1CDEA3C.RedFlagWarning.125CF1DC5540NM.MAFRFWMAF.085066acdafda6aecf61d10224f5133e" - } - ], - "currently": { - "apparentTemperature": 68.1, - "cloudCover": 0.18, - "dewPoint": 53.23, - "humidity": 0.59, - "icon": "clear-day", - "nearestStormBearing": 115, - "nearestStormDistance": 325, - "ozone": 322.71, - "precipIntensity": 0, - "precipProbability": 0, - "pressure": 1014.8, - "summary": "Clear", - "temperature": 68.1, - "time": 1464914163, - "visibility": 9.02, - "windBearing": 271, - "windSpeed": 9.38 - }, - "daily": { - "data": [ - { - "apparentTemperatureMax": 68.56, - "apparentTemperatureMaxTime": 1464915600, - "apparentTemperatureMin": 52.94, - "apparentTemperatureMinTime": 1464872400, - "cloudCover": 0.23, - "dewPoint": 51.98, - "humidity": 0.77, - "icon": "partly-cloudy-day", - "moonPhase": 0.91, - "ozone": 326.1, - "precipIntensity": 0, - "precipIntensityMax": 0, - "precipProbability": 0, - "pressure": 1014.84, - "summary": "Partly cloudy in the morning.", - "sunriseTime": 1464871812, - "sunsetTime": 1464924498, - "temperatureMax": 68.56, - "temperatureMaxTime": 1464915600, - "temperatureMin": 52.94, - "temperatureMinTime": 1464872400, - "time": 1464850800, - "visibility": 7.8, - "windBearing": 268, - "windSpeed": 5.59 - }, - { - "apparentTemperatureMax": 75.82, - "apparentTemperatureMaxTime": 1464991200, - "apparentTemperatureMin": 53.45, - "apparentTemperatureMinTime": 1464958800, - "cloudCover": 0.41, - "dewPoint": 53.58, - "humidity": 0.71, - "icon": "partly-cloudy-day", - "moonPhase": 0.95, - "ozone": 319.98, - "precipIntensity": 0, - "precipIntensityMax": 0, - "precipProbability": 0, - "pressure": 1015.2, - "summary": "Partly cloudy throughout the day.", - "sunriseTime": 1464958194, - "sunsetTime": 1465010936, - "temperatureMax": 75.82, - "temperatureMaxTime": 1464991200, - "temperatureMin": 53.45, - "temperatureMinTime": 1464958800, - "time": 1464937200, - "visibility": 9.24, - "windBearing": 274, - "windSpeed": 5.92 - }, - { - "apparentTemperatureMax": 72.18, - "apparentTemperatureMaxTime": 1465081200, - "apparentTemperatureMin": 53.06, - "apparentTemperatureMinTime": 1465038000, - "cloudCover": 0.74, - "dewPoint": 54.14, - "humidity": 0.78, - "icon": "partly-cloudy-day", - "moonPhase": 0.98, - "ozone": 324.21, - "precipIntensity": 0, - "precipIntensityMax": 0, - "precipProbability": 0, - "pressure": 1013.17, - "summary": "Mostly cloudy throughout the day.", - "sunriseTime": 1465044577, - "sunsetTime": 1465097372, - "temperatureMax": 72.18, - "temperatureMaxTime": 1465081200, - "temperatureMin": 53.06, - "temperatureMinTime": 1465038000, - "time": 1465023600, - "visibility": 7.94, - "windBearing": 255, - "windSpeed": 5.5 - }, - { - "apparentTemperatureMax": 71.76, - "apparentTemperatureMaxTime": 1465171200, - "apparentTemperatureMin": 52.37, - "apparentTemperatureMinTime": 1465131600, - "cloudCover": 0.5, - "dewPoint": 53.42, - "humidity": 0.8, - "icon": "fog", - "moonPhase": 0.03, - "ozone": 325.96, - "precipIntensity": 0.0006, - "precipIntensityMax": 0.0016, - "precipIntensityMaxTime": 1465135200, - "precipProbability": 0.04, - "precipType": "rain", - "pressure": 1011.43, - "summary": "Foggy in the morning.", - "sunriseTime": 1465130962, - "sunsetTime": 1465183808, - "temperatureMax": 71.76, - "temperatureMaxTime": 1465171200, - "temperatureMin": 52.37, - "temperatureMinTime": 1465131600, - "time": 1465110000, - "visibility": 6.86, - "windBearing": 252, - "windSpeed": 7.29 - }, - { - "apparentTemperatureMax": 69.01, - "apparentTemperatureMaxTime": 1465246800, - "apparentTemperatureMin": 54.75, - "apparentTemperatureMinTime": 1465214400, - "cloudCover": 0.09, - "dewPoint": 52.16, - "humidity": 0.74, - "icon": "partly-cloudy-night", - "moonPhase": 0.07, - "ozone": 305.72, - "precipIntensity": 0, - "precipIntensityMax": 0, - "precipProbability": 0, - "pressure": 1006.94, - "summary": "Partly cloudy starting in the evening.", - "sunriseTime": 1465217348, - "sunsetTime": 1465270242, - "temperatureMax": 69.01, - "temperatureMaxTime": 1465246800, - "temperatureMin": 54.75, - "temperatureMinTime": 1465214400, - "time": 1465196400, - "windBearing": 222, - "windSpeed": 5.86 - }, - { - "apparentTemperatureMax": 67.78, - "apparentTemperatureMaxTime": 1465333200, - "apparentTemperatureMin": 55.38, - "apparentTemperatureMinTime": 1465300800, - "cloudCover": 0.34, - "dewPoint": 51.41, - "humidity": 0.73, - "icon": "partly-cloudy-day", - "moonPhase": 0.1, - "ozone": 304.57, - "precipIntensity": 0, - "precipIntensityMax": 0, - "precipProbability": 0, - "pressure": 1007.88, - "summary": "Partly cloudy throughout the day.", - "sunriseTime": 1465303737, - "sunsetTime": 1465356676, - "temperatureMax": 67.78, - "temperatureMaxTime": 1465333200, - "temperatureMin": 55.38, - "temperatureMinTime": 1465300800, - "time": 1465282800, - "windBearing": 224, - "windSpeed": 6.75 - }, - { - "apparentTemperatureMax": 68.94, - "apparentTemperatureMaxTime": 1465416000, - "apparentTemperatureMin": 55.11, - "apparentTemperatureMinTime": 1465452000, - "cloudCover": 0.45, - "dewPoint": 47.11, - "humidity": 0.63, - "icon": "partly-cloudy-day", - "moonPhase": 0.13, - "ozone": 329.52, - "precipIntensity": 0, - "precipIntensityMax": 0, - "precipProbability": 0, - "pressure": 1010.78, - "summary": "Mostly cloudy until afternoon.", - "sunriseTime": 1465390127, - "sunsetTime": 1465443107, - "temperatureMax": 68.94, - "temperatureMaxTime": 1465416000, - "temperatureMin": 55.11, - "temperatureMinTime": 1465452000, - "time": 1465369200, - "windBearing": 263, - "windSpeed": 9.55 - }, - { - "apparentTemperatureMax": 65.67, - "apparentTemperatureMaxTime": 1465506000, - "apparentTemperatureMin": 54, - "apparentTemperatureMinTime": 1465470000, - "cloudCover": 0, - "dewPoint": 44.72, - "humidity": 0.61, - "icon": "clear-day", - "moonPhase": 0.17, - "ozone": 355.02, - "precipIntensity": 0, - "precipIntensityMax": 0, - "precipProbability": 0, - "pressure": 1010.11, - "summary": "Clear throughout the day.", - "sunriseTime": 1465476519, - "sunsetTime": 1465529538, - "temperatureMax": 65.67, - "temperatureMaxTime": 1465506000, - "temperatureMin": 54, - "temperatureMinTime": 1465470000, - "time": 1465455600, - "windBearing": 288, - "windSpeed": 12.21 - } - ], - "icon": "clear-day", - "summary": "No precipitation throughout the week, with temperatures falling to 66°F on Thursday." - }, - "flags": { - "darksky-stations": ["KMUX", "KDAX"], - "isd-stations": [ - "724943-99999", - "745039-99999", - "745065-99999", - "994016-99999", - "998479-99999" - ], - "lamp-stations": [ - "KAPC", - "KCCR", - "KHWD", - "KLVK", - "KNUQ", - "KOAK", - "KPAO", - "KSFO", - "KSQL" - ], - "madis-stations": [ - "AU915", - "C5988", - "C6328", - "C8158", - "C9629", - "D5422", - "D8533", - "E0426", - "E6067", - "E9227", - "FTPC1", - "GGBC1", - "OKXC1", - "PPXC1", - "PXOC1", - "SFOC1" - ], - "sources": [ - "darksky", - "lamp", - "gfs", - "cmc", - "nam", - "rap", - "rtma", - "sref", - "fnmoc", - "isd", - "nwspa", - "madis", - "nearest-precip" - ], - "units": "us" - }, - "hourly": { - "data": [ - { - "apparentTemperature": 67.42, - "cloudCover": 0.19, - "dewPoint": 52.31, - "humidity": 0.58, - "icon": "clear-day", - "ozone": 322.76, - "precipIntensity": 0, - "precipProbability": 0, - "pressure": 1014.88, - "summary": "Clear", - "temperature": 67.42, - "time": 1464912000, - "visibility": 9, - "windBearing": 269, - "windSpeed": 8.38 - }, - { - "apparentTemperature": 68.56, - "cloudCover": 0.18, - "dewPoint": 53.84, - "humidity": 0.59, - "icon": "clear-day", - "ozone": 322.68, - "precipIntensity": 0, - "precipProbability": 0, - "pressure": 1014.76, - "summary": "Clear", - "temperature": 68.56, - "time": 1464915600, - "visibility": 9.03, - "windBearing": 272, - "windSpeed": 10.05 - }, - { - "apparentTemperature": 67.39, - "cloudCover": 0.15, - "dewPoint": 54.53, - "humidity": 0.63, - "icon": "clear-day", - "ozone": 322.66, - "precipIntensity": 0, - "precipProbability": 0, - "pressure": 1014.75, - "summary": "Clear", - "temperature": 67.39, - "time": 1464919200, - "visibility": 9.31, - "windBearing": 274, - "windSpeed": 9.2 - }, - { - "apparentTemperature": 65.48, - "cloudCover": 0.13, - "dewPoint": 53.73, - "humidity": 0.66, - "icon": "clear-day", - "ozone": 322.72, - "precipIntensity": 0, - "precipProbability": 0, - "pressure": 1014.86, - "summary": "Clear", - "temperature": 65.48, - "time": 1464922800, - "visibility": 9.41, - "windBearing": 276, - "windSpeed": 8.41 - }, - { - "apparentTemperature": 63.37, - "cloudCover": 0.15, - "dewPoint": 53.05, - "humidity": 0.69, - "icon": "clear-night", - "ozone": 322.89, - "precipIntensity": 0, - "precipProbability": 0, - "pressure": 1015.18, - "summary": "Clear", - "temperature": 63.37, - "time": 1464926400, - "visibility": 9.64, - "windBearing": 277, - "windSpeed": 6.64 - }, - { - "apparentTemperature": 61.63, - "cloudCover": 0.18, - "dewPoint": 52.33, - "humidity": 0.72, - "icon": "clear-night", - "ozone": 323.15, - "precipIntensity": 0, - "precipProbability": 0, - "pressure": 1015.62, - "summary": "Clear", - "temperature": 61.63, - "time": 1464930000, - "visibility": 9.65, - "windBearing": 280, - "windSpeed": 5.84 - }, - { - "apparentTemperature": 59.39, - "cloudCover": 0.2, - "dewPoint": 51.07, - "humidity": 0.74, - "icon": "clear-night", - "ozone": 323.5, - "precipIntensity": 0, - "precipProbability": 0, - "pressure": 1015.9, - "summary": "Clear", - "temperature": 59.39, - "time": 1464933600, - "visibility": 9.62, - "windBearing": 281, - "windSpeed": 5.43 - }, - { - "apparentTemperature": 58.44, - "cloudCover": 0.2, - "dewPoint": 50.88, - "humidity": 0.76, - "icon": "clear-night", - "ozone": 323.99, - "precipIntensity": 0, - "precipProbability": 0, - "pressure": 1015.87, - "summary": "Clear", - "temperature": 58.44, - "time": 1464937200, - "visibility": 9.5, - "windBearing": 279, - "windSpeed": 4.98 - }, - { - "apparentTemperature": 57.85, - "cloudCover": 0.2, - "dewPoint": 51.05, - "humidity": 0.78, - "icon": "clear-night", - "ozone": 324.56, - "precipIntensity": 0, - "precipProbability": 0, - "pressure": 1015.69, - "summary": "Clear", - "temperature": 57.85, - "time": 1464940800, - "visibility": 9.11, - "windBearing": 278, - "windSpeed": 5.02 - }, - { - "apparentTemperature": 57.28, - "cloudCover": 0.22, - "dewPoint": 51.09, - "humidity": 0.8, - "icon": "clear-night", - "ozone": 325.1, - "precipIntensity": 0, - "precipProbability": 0, - "pressure": 1015.52, - "summary": "Clear", - "temperature": 57.28, - "time": 1464944400, - "visibility": 9.05, - "windBearing": 278, - "windSpeed": 5.14 - }, - { - "apparentTemperature": 56.24, - "cloudCover": 0.27, - "dewPoint": 50.63, - "humidity": 0.81, - "icon": "partly-cloudy-night", - "ozone": 325.68, - "precipIntensity": 0, - "precipProbability": 0, - "pressure": 1015.37, - "summary": "Partly Cloudy", - "temperature": 56.24, - "time": 1464948000, - "visibility": 8.85, - "windBearing": 278, - "windSpeed": 4.82 - }, - { - "apparentTemperature": 54.93, - "cloudCover": 0.34, - "dewPoint": 49.82, - "humidity": 0.83, - "icon": "partly-cloudy-night", - "ozone": 326.22, - "precipIntensity": 0, - "precipProbability": 0, - "pressure": 1015.25, - "summary": "Partly Cloudy", - "temperature": 54.93, - "time": 1464951600, - "visibility": 8.56, - "windBearing": 278, - "windSpeed": 4.45 - }, - { - "apparentTemperature": 54.06, - "cloudCover": 0.4, - "dewPoint": 49.16, - "humidity": 0.83, - "icon": "partly-cloudy-night", - "ozone": 326.31, - "precipIntensity": 0, - "precipProbability": 0, - "pressure": 1015.22, - "summary": "Partly Cloudy", - "temperature": 54.06, - "time": 1464955200, - "visibility": 8.12, - "windBearing": 280, - "windSpeed": 4.13 - }, - { - "apparentTemperature": 53.45, - "cloudCover": 0.45, - "dewPoint": 48.47, - "humidity": 0.83, - "icon": "partly-cloudy-day", - "ozone": 325.68, - "precipIntensity": 0, - "precipProbability": 0, - "pressure": 1015.4, - "summary": "Partly Cloudy", - "temperature": 53.45, - "time": 1464958800, - "visibility": 7.76, - "windBearing": 280, - "windSpeed": 3.86 - }, - { - "apparentTemperature": 56.05, - "cloudCover": 0.5, - "dewPoint": 50.07, - "humidity": 0.8, - "icon": "partly-cloudy-day", - "ozone": 324.6, - "precipIntensity": 0, - "precipProbability": 0, - "pressure": 1015.68, - "summary": "Partly Cloudy", - "temperature": 56.05, - "time": 1464962400, - "visibility": 7.77, - "windBearing": 279, - "windSpeed": 3.61 - }, - { - "apparentTemperature": 59.38, - "cloudCover": 0.52, - "dewPoint": 51.39, - "humidity": 0.75, - "icon": "partly-cloudy-day", - "ozone": 323.51, - "precipIntensity": 0, - "precipProbability": 0, - "pressure": 1015.9, - "summary": "Partly Cloudy", - "temperature": 59.38, - "time": 1464966000, - "visibility": 8.18, - "windBearing": 275, - "windSpeed": 4 - }, - { - "apparentTemperature": 62.67, - "cloudCover": 0.51, - "dewPoint": 52.44, - "humidity": 0.69, - "icon": "partly-cloudy-day", - "ozone": 322.57, - "precipIntensity": 0, - "precipProbability": 0, - "pressure": 1015.97, - "summary": "Partly Cloudy", - "temperature": 62.67, - "time": 1464969600, - "visibility": 8.4, - "windBearing": 272, - "windSpeed": 4.22 - }, - { - "apparentTemperature": 65.51, - "cloudCover": 0.49, - "dewPoint": 52.62, - "humidity": 0.63, - "icon": "partly-cloudy-day", - "ozone": 321.61, - "precipIntensity": 0, - "precipProbability": 0, - "pressure": 1015.96, - "summary": "Partly Cloudy", - "temperature": 65.51, - "time": 1464973200, - "visibility": 8.72, - "windBearing": 271, - "windSpeed": 4.65 - }, - { - "apparentTemperature": 66.98, - "cloudCover": 0.46, - "dewPoint": 53.53, - "humidity": 0.62, - "icon": "partly-cloudy-day", - "ozone": 320.6, - "precipIntensity": 0, - "precipProbability": 0, - "pressure": 1015.9, - "summary": "Partly Cloudy", - "temperature": 66.98, - "time": 1464976800, - "visibility": 8.81, - "windBearing": 271, - "windSpeed": 4.93 - }, - { - "apparentTemperature": 69.34, - "cloudCover": 0.4, - "dewPoint": 55.04, - "humidity": 0.6, - "icon": "partly-cloudy-day", - "ozone": 319.39, - "precipIntensity": 0, - "precipProbability": 0, - "pressure": 1015.8, - "summary": "Partly Cloudy", - "temperature": 69.34, - "time": 1464980400, - "visibility": 9.05, - "windBearing": 271, - "windSpeed": 5.7 - }, - { - "apparentTemperature": 71.92, - "cloudCover": 0.34, - "dewPoint": 56.61, - "humidity": 0.59, - "icon": "partly-cloudy-day", - "ozone": 318.12, - "precipIntensity": 0, - "precipProbability": 0, - "pressure": 1015.65, - "summary": "Partly Cloudy", - "temperature": 71.92, - "time": 1464984000, - "visibility": 9.49, - "windBearing": 272, - "windSpeed": 6.7 - }, - { - "apparentTemperature": 74.45, - "cloudCover": 0.3, - "dewPoint": 58.29, - "humidity": 0.57, - "icon": "partly-cloudy-day", - "ozone": 317.16, - "precipIntensity": 0, - "precipProbability": 0, - "pressure": 1015.43, - "summary": "Partly Cloudy", - "temperature": 74.45, - "time": 1464987600, - "visibility": 9.9, - "windBearing": 273, - "windSpeed": 7.6 - }, - { - "apparentTemperature": 75.82, - "cloudCover": 0.3, - "dewPoint": 59.26, - "humidity": 0.57, - "icon": "partly-cloudy-day", - "ozone": 316.77, - "precipIntensity": 0, - "precipProbability": 0, - "pressure": 1015.09, - "summary": "Partly Cloudy", - "temperature": 75.82, - "time": 1464991200, - "visibility": 10, - "windBearing": 273, - "windSpeed": 8.58 - }, - { - "apparentTemperature": 74.92, - "cloudCover": 0.32, - "dewPoint": 58.55, - "humidity": 0.57, - "icon": "partly-cloudy-day", - "ozone": 316.68, - "precipIntensity": 0, - "precipProbability": 0, - "pressure": 1014.71, - "summary": "Partly Cloudy", - "temperature": 74.92, - "time": 1464994800, - "visibility": 10, - "windBearing": 274, - "windSpeed": 9.12 - }, - { - "apparentTemperature": 73.33, - "cloudCover": 0.36, - "dewPoint": 57.88, - "humidity": 0.58, - "icon": "partly-cloudy-day", - "ozone": 316.48, - "precipIntensity": 0, - "precipProbability": 0, - "pressure": 1014.4, - "summary": "Partly Cloudy", - "temperature": 73.33, - "time": 1464998400, - "visibility": 10, - "windBearing": 274, - "windSpeed": 9.21 - }, - { - "apparentTemperature": 70.53, - "cloudCover": 0.41, - "dewPoint": 56.98, - "humidity": 0.62, - "icon": "partly-cloudy-day", - "ozone": 316.04, - "precipIntensity": 0, - "precipProbability": 0, - "pressure": 1014.22, - "summary": "Partly Cloudy", - "temperature": 70.53, - "time": 1465002000, - "visibility": 10, - "windBearing": 274, - "windSpeed": 8.77 - }, - { - "apparentTemperature": 67.03, - "cloudCover": 0.49, - "dewPoint": 55.91, - "humidity": 0.68, - "icon": "partly-cloudy-day", - "ozone": 315.48, - "precipIntensity": 0, - "precipProbability": 0, - "pressure": 1014.11, - "summary": "Partly Cloudy", - "temperature": 67.03, - "time": 1465005600, - "visibility": 10, - "windBearing": 273, - "windSpeed": 7.84 - }, - { - "apparentTemperature": 63.9, - "cloudCover": 0.56, - "dewPoint": 54.93, - "humidity": 0.73, - "icon": "partly-cloudy-day", - "ozone": 314.77, - "precipIntensity": 0, - "precipProbability": 0, - "pressure": 1014.1, - "summary": "Partly Cloudy", - "temperature": 63.9, - "time": 1465009200, - "visibility": 10, - "windBearing": 271, - "windSpeed": 7.12 - }, - { - "apparentTemperature": 61.49, - "cloudCover": 0.58, - "dewPoint": 54.23, - "humidity": 0.77, - "icon": "partly-cloudy-night", - "ozone": 313.81, - "precipIntensity": 0, - "precipProbability": 0, - "pressure": 1014.29, - "summary": "Partly Cloudy", - "temperature": 61.49, - "time": 1465012800, - "visibility": 10, - "windBearing": 269, - "windSpeed": 6.5 - }, - { - "apparentTemperature": 59.39, - "cloudCover": 0.58, - "dewPoint": 53.62, - "humidity": 0.81, - "icon": "partly-cloudy-night", - "ozone": 312.7, - "precipIntensity": 0, - "precipProbability": 0, - "pressure": 1014.54, - "summary": "Partly Cloudy", - "temperature": 59.39, - "time": 1465016400, - "visibility": 10, - "windBearing": 266, - "windSpeed": 5.92 - }, - { - "apparentTemperature": 58.02, - "cloudCover": 0.59, - "dewPoint": 53.35, - "humidity": 0.84, - "icon": "partly-cloudy-night", - "ozone": 311.6, - "precipIntensity": 0, - "precipProbability": 0, - "pressure": 1014.63, - "summary": "Partly Cloudy", - "temperature": 58.02, - "time": 1465020000, - "visibility": 10, - "windBearing": 263, - "windSpeed": 5.46 - }, - { - "apparentTemperature": 57.06, - "cloudCover": 0.61, - "dewPoint": 53.07, - "humidity": 0.87, - "icon": "partly-cloudy-night", - "ozone": 310.41, - "precipIntensity": 0, - "precipProbability": 0, - "pressure": 1014.43, - "summary": "Mostly Cloudy", - "temperature": 57.06, - "time": 1465023600, - "visibility": 9.01, - "windBearing": 259, - "windSpeed": 5.18 - }, - { - "apparentTemperature": 56.12, - "cloudCover": 0.63, - "dewPoint": 52.56, - "humidity": 0.88, - "icon": "partly-cloudy-night", - "ozone": 309.24, - "precipIntensity": 0, - "precipProbability": 0, - "pressure": 1014.09, - "summary": "Mostly Cloudy", - "temperature": 56.12, - "time": 1465027200, - "visibility": 7.64, - "windBearing": 256, - "windSpeed": 5.01 - }, - { - "apparentTemperature": 55.14, - "cloudCover": 0.67, - "dewPoint": 51.94, - "humidity": 0.89, - "icon": "partly-cloudy-night", - "ozone": 308.58, - "precipIntensity": 0, - "precipProbability": 0, - "pressure": 1013.74, - "summary": "Mostly Cloudy", - "temperature": 55.14, - "time": 1465030800, - "visibility": 6.64, - "windBearing": 253, - "windSpeed": 4.84 - }, - { - "apparentTemperature": 54.01, - "cloudCover": 0.72, - "dewPoint": 51.21, - "humidity": 0.9, - "icon": "partly-cloudy-night", - "ozone": 308.59, - "precipIntensity": 0, - "precipProbability": 0, - "pressure": 1013.39, - "summary": "Mostly Cloudy", - "temperature": 54.01, - "time": 1465034400, - "visibility": 6.4, - "windBearing": 254, - "windSpeed": 4.64 - }, - { - "apparentTemperature": 53.06, - "cloudCover": 0.77, - "dewPoint": 50.57, - "humidity": 0.91, - "icon": "partly-cloudy-night", - "ozone": 309.11, - "precipIntensity": 0, - "precipProbability": 0, - "pressure": 1013.05, - "summary": "Mostly Cloudy", - "temperature": 53.06, - "time": 1465038000, - "visibility": 6.53, - "windBearing": 256, - "windSpeed": 4.45 - }, - { - "apparentTemperature": 53.73, - "cloudCover": 0.83, - "dewPoint": 51.2, - "humidity": 0.91, - "icon": "partly-cloudy-night", - "ozone": 310.15, - "precipIntensity": 0, - "precipProbability": 0, - "pressure": 1012.84, - "summary": "Mostly Cloudy", - "temperature": 53.73, - "time": 1465041600, - "visibility": 6.58, - "windBearing": 255, - "windSpeed": 4.23 - }, - { - "apparentTemperature": 54.88, - "cloudCover": 0.89, - "dewPoint": 51.88, - "humidity": 0.9, - "icon": "partly-cloudy-day", - "ozone": 312.09, - "precipIntensity": 0, - "precipProbability": 0, - "pressure": 1012.85, - "summary": "Mostly Cloudy", - "temperature": 54.88, - "time": 1465045200, - "visibility": 6.28, - "windBearing": 250, - "windSpeed": 3.81 - }, - { - "apparentTemperature": 56.24, - "cloudCover": 0.96, - "dewPoint": 52.42, - "humidity": 0.87, - "icon": "cloudy", - "ozone": 314.55, - "precipIntensity": 0, - "precipProbability": 0, - "pressure": 1013, - "summary": "Overcast", - "temperature": 56.24, - "time": 1465048800, - "visibility": 5.92, - "windBearing": 241, - "windSpeed": 3.23 - }, - { - "apparentTemperature": 57.62, - "cloudCover": 1, - "dewPoint": 52.73, - "humidity": 0.84, - "icon": "cloudy", - "ozone": 316.51, - "precipIntensity": 0, - "precipProbability": 0, - "pressure": 1013.16, - "summary": "Overcast", - "temperature": 57.62, - "time": 1465052400, - "visibility": 5.89, - "windBearing": 236, - "windSpeed": 3.11 - }, - { - "apparentTemperature": 59.22, - "cloudCover": 1, - "dewPoint": 52.86, - "humidity": 0.79, - "icon": "cloudy", - "ozone": 317.32, - "precipIntensity": 0, - "precipProbability": 0, - "pressure": 1013.31, - "summary": "Overcast", - "temperature": 59.22, - "time": 1465056000, - "visibility": 6.54, - "windBearing": 241, - "windSpeed": 3.59 - }, - { - "apparentTemperature": 61.29, - "cloudCover": 0.99, - "dewPoint": 53.12, - "humidity": 0.75, - "icon": "cloudy", - "ozone": 317.63, - "precipIntensity": 0, - "precipProbability": 0, - "pressure": 1013.46, - "summary": "Overcast", - "temperature": 61.29, - "time": 1465059600, - "visibility": 7.52, - "windBearing": 250, - "windSpeed": 4.42 - }, - { - "apparentTemperature": 63.6, - "cloudCover": 0.97, - "dewPoint": 53.81, - "humidity": 0.7, - "icon": "cloudy", - "ozone": 318.31, - "precipIntensity": 0, - "precipProbability": 0, - "pressure": 1013.54, - "summary": "Overcast", - "temperature": 63.6, - "time": 1465063200, - "visibility": 8.26, - "windBearing": 256, - "windSpeed": 5.05 - }, - { - "apparentTemperature": 65.59, - "cloudCover": 0.95, - "dewPoint": 54.62, - "humidity": 0.68, - "icon": "cloudy", - "ozone": 319.55, - "precipIntensity": 0, - "precipProbability": 0, - "pressure": 1013.54, - "summary": "Overcast", - "temperature": 65.59, - "time": 1465066800, - "visibility": 8.42, - "windBearing": 257, - "windSpeed": 5.53 - }, - { - "apparentTemperature": 67.6, - "cloudCover": 0.91, - "dewPoint": 55.73, - "humidity": 0.66, - "icon": "partly-cloudy-day", - "ozone": 321.16, - "precipIntensity": 0, - "precipProbability": 0, - "pressure": 1013.48, - "summary": "Mostly Cloudy", - "temperature": 67.6, - "time": 1465070400, - "visibility": 8.32, - "windBearing": 257, - "windSpeed": 5.95 - }, - { - "apparentTemperature": 69.48, - "cloudCover": 0.87, - "dewPoint": 56.92, - "humidity": 0.64, - "icon": "partly-cloudy-day", - "ozone": 323.49, - "precipIntensity": 0, - "precipProbability": 0, - "pressure": 1013.34, - "summary": "Mostly Cloudy", - "temperature": 69.48, - "time": 1465074000, - "visibility": 8.36, - "windBearing": 257, - "windSpeed": 6.37 - }, - { - "apparentTemperature": 71.07, - "cloudCover": 0.85, - "dewPoint": 57.94, - "humidity": 0.63, - "icon": "partly-cloudy-day", - "ozone": 326.97, - "precipIntensity": 0, - "precipProbability": 0, - "pressure": 1013.09, - "summary": "Mostly Cloudy", - "temperature": 71.07, - "time": 1465077600, - "visibility": 8.75, - "windBearing": 258, - "windSpeed": 6.88 - }, - { - "apparentTemperature": 72.18, - "cloudCover": 0.82, - "dewPoint": 58.77, - "humidity": 0.63, - "icon": "partly-cloudy-day", - "ozone": 331.17, - "precipIntensity": 0, - "precipProbability": 0, - "pressure": 1012.79, - "summary": "Mostly Cloudy", - "temperature": 72.18, - "time": 1465081200, - "visibility": 9.28, - "windBearing": 260, - "windSpeed": 7.5 - }, - { - "apparentTemperature": 71.38, - "cloudCover": 0.75, - "dewPoint": 58.47, - "humidity": 0.64, - "icon": "partly-cloudy-day", - "ozone": 335.24, - "precipIntensity": 0, - "precipProbability": 0, - "pressure": 1012.58, - "summary": "Mostly Cloudy", - "temperature": 71.38, - "time": 1465084800, - "visibility": 9.67, - "windBearing": 260, - "windSpeed": 7.75 - } - ], - "icon": "partly-cloudy-day", - "summary": "Partly cloudy starting tonight." - }, - "latitude": 37.8267, - "longitude": -122.423, - "minutely": { - "data": [ - { - "precipIntensity": 0, - "precipProbability": 0, - "time": 1464914160 - }, - { - "precipIntensity": 0, - "precipProbability": 0, - "time": 1464914220 - }, - { - "precipIntensity": 0, - "precipProbability": 0, - "time": 1464914280 - }, - { - "precipIntensity": 0, - "precipProbability": 0, - "time": 1464914340 - }, - { - "precipIntensity": 0, - "precipProbability": 0, - "time": 1464914400 - }, - { - "precipIntensity": 0, - "precipProbability": 0, - "time": 1464914460 - }, - { - "precipIntensity": 0, - "precipProbability": 0, - "time": 1464914520 - }, - { - "precipIntensity": 0, - "precipProbability": 0, - "time": 1464914580 - }, - { - "precipIntensity": 0, - "precipProbability": 0, - "time": 1464914640 - }, - { - "precipIntensity": 0, - "precipProbability": 0, - "time": 1464914700 - }, - { - "precipIntensity": 0, - "precipProbability": 0, - "time": 1464914760 - }, - { - "precipIntensity": 0, - "precipProbability": 0, - "time": 1464914820 - }, - { - "precipIntensity": 0, - "precipProbability": 0, - "time": 1464914880 - }, - { - "precipIntensity": 0, - "precipProbability": 0, - "time": 1464914940 - }, - { - "precipIntensity": 0, - "precipProbability": 0, - "time": 1464915000 - }, - { - "precipIntensity": 0, - "precipProbability": 0, - "time": 1464915060 - }, - { - "precipIntensity": 0, - "precipProbability": 0, - "time": 1464915120 - }, - { - "precipIntensity": 0, - "precipProbability": 0, - "time": 1464915180 - }, - { - "precipIntensity": 0, - "precipProbability": 0, - "time": 1464915240 - }, - { - "precipIntensity": 0, - "precipProbability": 0, - "time": 1464915300 - }, - { - "precipIntensity": 0, - "precipProbability": 0, - "time": 1464915360 - }, - { - "precipIntensity": 0, - "precipProbability": 0, - "time": 1464915420 - }, - { - "precipIntensity": 0, - "precipProbability": 0, - "time": 1464915480 - }, - { - "precipIntensity": 0, - "precipProbability": 0, - "time": 1464915540 - }, - { - "precipIntensity": 0, - "precipProbability": 0, - "time": 1464915600 - }, - { - "precipIntensity": 0, - "precipProbability": 0, - "time": 1464915660 - }, - { - "precipIntensity": 0, - "precipProbability": 0, - "time": 1464915720 - }, - { - "precipIntensity": 0, - "precipProbability": 0, - "time": 1464915780 - }, - { - "precipIntensity": 0, - "precipProbability": 0, - "time": 1464915840 - }, - { - "precipIntensity": 0, - "precipProbability": 0, - "time": 1464915900 - }, - { - "precipIntensity": 0, - "precipProbability": 0, - "time": 1464915960 - }, - { - "precipIntensity": 0, - "precipProbability": 0, - "time": 1464916020 - }, - { - "precipIntensity": 0, - "precipProbability": 0, - "time": 1464916080 - }, - { - "precipIntensity": 0, - "precipProbability": 0, - "time": 1464916140 - }, - { - "precipIntensity": 0, - "precipProbability": 0, - "time": 1464916200 - }, - { - "precipIntensity": 0, - "precipProbability": 0, - "time": 1464916260 - }, - { - "precipIntensity": 0, - "precipProbability": 0, - "time": 1464916320 - }, - { - "precipIntensity": 0, - "precipProbability": 0, - "time": 1464916380 - }, - { - "precipIntensity": 0, - "precipProbability": 0, - "time": 1464916440 - }, - { - "precipIntensity": 0, - "precipProbability": 0, - "time": 1464916500 - }, - { - "precipIntensity": 0, - "precipProbability": 0, - "time": 1464916560 - }, - { - "precipIntensity": 0, - "precipProbability": 0, - "time": 1464916620 - }, - { - "precipIntensity": 0, - "precipProbability": 0, - "time": 1464916680 - }, - { - "precipIntensity": 0, - "precipProbability": 0, - "time": 1464916740 - }, - { - "precipIntensity": 0, - "precipProbability": 0, - "time": 1464916800 - }, - { - "precipIntensity": 0, - "precipProbability": 0, - "time": 1464916860 - }, - { - "precipIntensity": 0, - "precipProbability": 0, - "time": 1464916920 - }, - { - "precipIntensity": 0, - "precipProbability": 0, - "time": 1464916980 - }, - { - "precipIntensity": 0, - "precipProbability": 0, - "time": 1464917040 - }, - { - "precipIntensity": 0, - "precipProbability": 0, - "time": 1464917100 - }, - { - "precipIntensity": 0, - "precipProbability": 0, - "time": 1464917160 - }, - { - "precipIntensity": 0, - "precipProbability": 0, - "time": 1464917220 - }, - { - "precipIntensity": 0, - "precipProbability": 0, - "time": 1464917280 - }, - { - "precipIntensity": 0, - "precipProbability": 0, - "time": 1464917340 - }, - { - "precipIntensity": 0, - "precipProbability": 0, - "time": 1464917400 - }, - { - "precipIntensity": 0, - "precipProbability": 0, - "time": 1464917460 - }, - { - "precipIntensity": 0, - "precipProbability": 0, - "time": 1464917520 - }, - { - "precipIntensity": 0, - "precipProbability": 0, - "time": 1464917580 - }, - { - "precipIntensity": 0, - "precipProbability": 0, - "time": 1464917640 - }, - { - "precipIntensity": 0, - "precipProbability": 0, - "time": 1464917700 - }, - { - "precipIntensity": 0, - "precipProbability": 0, - "time": 1464917760 - } - ], - "icon": "clear-day", - "summary": "Clear for the hour." - }, - "offset": -7, - "timezone": "America/Los_Angeles" -} diff --git a/tests/fixtures/upc_connect.xml b/tests/fixtures/upc_connect.xml deleted file mode 100644 index b8ffc4dd979..00000000000 --- a/tests/fixtures/upc_connect.xml +++ /dev/null @@ -1,42 +0,0 @@ - - - - - Ethernet 1 - 192.168.0.139/24 - 0 - 2 - Unknown - 30:D3:2D:0:69:21 - 2 - 00:00:00:00 - 1000 - - - Ethernet 2 - 192.168.0.134/24 - 1 - 2 - Unknown - 5C:AA:FD:25:32:02 - 2 - 00:00:00:00 - 10 - - - - - HASS - 192.168.0.194/24 - 3 - 3 - Unknown - 70:EE:50:27:A1:38 - 2 - 00:00:00:00 - 39 - - - 3 - upc - diff --git a/tests/fixtures/yahoo_finance.json b/tests/fixtures/yahoo_finance.json deleted file mode 100644 index 5c72abe77a7..00000000000 --- a/tests/fixtures/yahoo_finance.json +++ /dev/null @@ -1,85 +0,0 @@ -{ - "symbol": "YHOO", - "Ask": "42.42", - "AverageDailyVolume": "11397600", - "Bid": "42.41", - "AskRealtime": null, - "BidRealtime": null, - "BookValue": "29.83", - "Change_PercentChange": "+0.62 - +1.48%", - "Change": "+0.62", - "Commission": null, - "Currency": "USD", - "ChangeRealtime": null, - "AfterHoursChangeRealtime": null, - "DividendShare": null, - "LastTradeDate": "10/18/2016", - "TradeDate": null, - "EarningsShare": "-5.18", - "ErrorIndicationreturnedforsymbolchangedinvalid": null, - "EPSEstimateCurrentYear": "0.49", - "EPSEstimateNextYear": "0.57", - "EPSEstimateNextQuarter": "0.17", - "DaysLow": "41.86", - "DaysHigh": "42.42", - "YearLow": "26.15", - "YearHigh": "44.92", - "HoldingsGainPercent": null, - "AnnualizedGain": null, - "HoldingsGain": null, - "HoldingsGainPercentRealtime": null, - "HoldingsGainRealtime": null, - "MoreInfo": null, - "OrderBookRealtime": null, - "MarketCapitalization": "40.37B", - "MarketCapRealtime": null, - "EBITDA": "151.08M", - "ChangeFromYearLow": "16.26", - "PercentChangeFromYearLow": "+62.18%", - "LastTradeRealtimeWithTime": null, - "ChangePercentRealtime": null, - "ChangeFromYearHigh": "-2.51", - "PercebtChangeFromYearHigh": "-5.59%", - "LastTradeWithTime": "9:41am - 42.41", - "LastTradePriceOnly": "42.41", - "HighLimit": null, - "LowLimit": null, - "DaysRange": "41.86 - 42.42", - "DaysRangeRealtime": null, - "FiftydayMovingAverage": "43.16", - "TwoHundreddayMovingAverage": "39.26", - "ChangeFromTwoHundreddayMovingAverage": "3.15", - "PercentChangeFromTwoHundreddayMovingAverage": "+8.03%", - "ChangeFromFiftydayMovingAverage": "-0.75", - "PercentChangeFromFiftydayMovingAverage": "-1.74%", - "Name": "Yahoo! Inc.", - "Notes": null, - "Open": "41.69", - "PreviousClose": "41.79", - "PricePaid": null, - "ChangeinPercent": "+1.48%", - "PriceSales": "8.13", - "PriceBook": "1.40", - "ExDividendDate": null, - "PERatio": null, - "DividendPayDate": null, - "PERatioRealtime": null, - "PEGRatio": "-24.57", - "PriceEPSEstimateCurrentYear": "86.55", - "PriceEPSEstimateNextYear": "74.40", - "Symbol": "YHOO", - "SharesOwned": null, - "ShortRatio": "5.05", - "LastTradeTime": "9:41am", - "TickerTrend": null, - "OneyrTargetPrice": "43.64", - "Volume": "946198", - "HoldingsValue": null, - "HoldingsValueRealtime": null, - "YearRange": "26.15 - 44.92", - "DaysValueChange": null, - "DaysValueChangeRealtime": null, - "StockExchange": "NMS", - "DividendYield": null, - "PercentChange": "+1.48%" -} diff --git a/tests/fixtures/yahooweather.json b/tests/fixtures/yahooweather.json deleted file mode 100644 index d345373c149..00000000000 --- a/tests/fixtures/yahooweather.json +++ /dev/null @@ -1,149 +0,0 @@ -{ - "query": { - "count": 1, - "created": "2017-11-17T13:40:47Z", - "lang": "en-US", - "results": { - "channel": { - "units": { - "distance": "km", - "pressure": "mb", - "speed": "km/h", - "temperature": "C" - }, - "title": "Yahoo! Weather - San Diego, CA, US", - "link": "http://us.rd.yahoo.com/dailynews/rss/weather/Country__Country/*https://weather.yahoo.com/country/state/city-23511632/", - "description": "Yahoo! Weather for San Diego, CA, US", - "language": "en-us", - "lastBuildDate": "Fri, 17 Nov 2017 05:40 AM PST", - "ttl": "60", - "location": { - "city": "San Diego", - "country": "United States", - "region": " CA" - }, - "wind": { - "chill": "56", - "direction": "0", - "speed": "6.34" - }, - "atmosphere": { - "humidity": "71", - "pressure": "33863.75", - "rising": "0", - "visibility": "22.91" - }, - "astronomy": { - "sunrise": "6:21 am", - "sunset": "4:47 pm" - }, - "image": { - "title": "Yahoo! Weather", - "width": "142", - "height": "18", - "link": "http://weather.yahoo.com", - "url": "http://l.yimg.com/a/i/brand/purplelogo//uh/us/news-wea.gif" - }, - "item": { - "title": "Conditions for San Diego, CA, US at 05:00 AM PST", - "lat": "32.878101", - "long": "-117.23497", - "link": "http://us.rd.yahoo.com/dailynews/rss/weather/Country__Country/*https://weather.yahoo.com/country/state/city-23511632/", - "pubDate": "Fri, 17 Nov 2017 05:00 AM PST", - "condition": { - "code": "26", - "date": "Fri, 17 Nov 2017 05:00 AM PST", - "temp": "18", - "text": "Cloudy" - }, - "forecast": [ - { - "code": "28", - "date": "17 Nov 2017", - "day": "Fri", - "high": "23", - "low": "16", - "text": "Mostly Cloudy" - }, - { - "code": "30", - "date": "18 Nov 2017", - "day": "Sat", - "high": "22", - "low": "13", - "text": "Partly Cloudy" - }, - { - "code": "30", - "date": "19 Nov 2017", - "day": "Sun", - "high": "22", - "low": "12", - "text": "Partly Cloudy" - }, - { - "code": "28", - "date": "20 Nov 2017", - "day": "Mon", - "high": "21", - "low": "11", - "text": "Mostly Cloudy" - }, - { - "code": "28", - "date": "21 Nov 2017", - "day": "Tue", - "high": "24", - "low": "14", - "text": "Mostly Cloudy" - }, - { - "code": "30", - "date": "22 Nov 2017", - "day": "Wed", - "high": "27", - "low": "15", - "text": "Partly Cloudy" - }, - { - "code": "34", - "date": "23 Nov 2017", - "day": "Thu", - "high": "27", - "low": "15", - "text": "Mostly Sunny" - }, - { - "code": "30", - "date": "24 Nov 2017", - "day": "Fri", - "high": "23", - "low": "16", - "text": "Partly Cloudy" - }, - { - "code": "30", - "date": "25 Nov 2017", - "day": "Sat", - "high": "22", - "low": "15", - "text": "Partly Cloudy" - }, - { - "code": "28", - "date": "26 Nov 2017", - "day": "Sun", - "high": "24", - "low": "13", - "text": "Mostly Cloudy" - } - ], - "description": "\n
\nCurrent Conditions:\n
Cloudy\n
\n
\nForecast:\n
Fri - Mostly Cloudy. High: 23Low: 16\n
Sat - Partly Cloudy. High: 22Low: 13\n
Sun - Partly Cloudy. High: 22Low: 12\n
Mon - Mostly Cloudy. High: 21Low: 11\n
Tue - Mostly Cloudy. High: 24Low: 14\n
\n
\nFull Forecast at Yahoo! Weather\n
\n
\n
\n]]>", - "guid": { - "isPermaLink": "false" - } - } - } - } - } -} diff --git a/tests/hassfest/test_dependencies.py b/tests/hassfest/test_dependencies.py index 98ccbe4eae6..84e02b2d9d5 100644 --- a/tests/hassfest/test_dependencies.py +++ b/tests/hassfest/test_dependencies.py @@ -1,4 +1,5 @@ """Tests for hassfest dependency finder.""" + import ast import pytest @@ -20,6 +21,7 @@ def test_child_import(mock_collector) -> None: mock_collector.visit( ast.parse( """ + from homeassistant.components import child_import """ ) @@ -32,6 +34,7 @@ def test_subimport(mock_collector) -> None: mock_collector.visit( ast.parse( """ + from homeassistant.components.subimport.smart_home import EVENT_ALEXA_SMART_HOME """ ) @@ -44,6 +47,7 @@ def test_child_import_field(mock_collector) -> None: mock_collector.visit( ast.parse( """ + from homeassistant.components.child_import_field import bla """ ) @@ -56,6 +60,7 @@ def test_renamed_absolute(mock_collector) -> None: mock_collector.visit( ast.parse( """ + import homeassistant.components.renamed_absolute as hue """ ) @@ -95,6 +100,7 @@ def test_all_imports(mock_collector) -> None: mock_collector.visit( ast.parse( """ + from homeassistant.components import child_import from homeassistant.components.subimport.smart_home import EVENT_ALEXA_SMART_HOME diff --git a/tests/hassfest/test_requirements.py b/tests/hassfest/test_requirements.py index e6b247047e2..7cb08621e83 100644 --- a/tests/hassfest/test_requirements.py +++ b/tests/hassfest/test_requirements.py @@ -1,4 +1,5 @@ """Tests for hassfest requirements.""" + from pathlib import Path import pytest diff --git a/tests/hassfest/test_version.py b/tests/hassfest/test_version.py index f473e988027..9cc1bbb11e5 100644 --- a/tests/hassfest/test_version.py +++ b/tests/hassfest/test_version.py @@ -1,4 +1,5 @@ """Tests for hassfest version.""" + import pytest import voluptuous as vol diff --git a/tests/helpers/test_aiohttp_client.py b/tests/helpers/test_aiohttp_client.py index 8488a5f15e3..69015c80305 100644 --- a/tests/helpers/test_aiohttp_client.py +++ b/tests/helpers/test_aiohttp_client.py @@ -1,4 +1,5 @@ """Test the aiohttp client helper.""" + from unittest.mock import Mock, patch import aiohttp @@ -20,7 +21,12 @@ from homeassistant.core import EVENT_HOMEASSISTANT_CLOSE, HomeAssistant import homeassistant.helpers.aiohttp_client as client from homeassistant.util.color import RGBColor -from tests.common import MockConfigEntry, MockModule, mock_integration +from tests.common import ( + MockConfigEntry, + MockModule, + extract_stack_to_frame, + mock_integration, +) from tests.test_util.aiohttp import AiohttpClientMocker @@ -165,25 +171,33 @@ async def test_warning_close_session_integration( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test log warning message when closing the session from integration context.""" - with patch( - "homeassistant.helpers.frame.extract_stack", - return_value=[ - Mock( - filename="/home/paulus/homeassistant/core.py", - lineno="23", - line="do_something()", + with ( + patch( + "homeassistant.helpers.frame.linecache.getline", + return_value="await session.close()", + ), + patch( + "homeassistant.helpers.frame.get_current_frame", + return_value=extract_stack_to_frame( + [ + Mock( + filename="/home/paulus/homeassistant/core.py", + lineno="23", + line="do_something()", + ), + Mock( + filename="/home/paulus/homeassistant/components/hue/light.py", + lineno="23", + line="await session.close()", + ), + Mock( + filename="/home/paulus/aiohue/lights.py", + lineno="2", + line="something()", + ), + ] ), - Mock( - filename="/home/paulus/homeassistant/components/hue/light.py", - lineno="23", - line="await session.close()", - ), - Mock( - filename="/home/paulus/aiohue/lights.py", - lineno="2", - line="something()", - ), - ], + ), ): session = client.async_get_clientsession(hass) await session.close() @@ -201,25 +215,33 @@ async def test_warning_close_session_custom( ) -> None: """Test log warning message when closing the session from custom context.""" mock_integration(hass, MockModule("hue"), built_in=False) - with patch( - "homeassistant.helpers.frame.extract_stack", - return_value=[ - Mock( - filename="/home/paulus/homeassistant/core.py", - lineno="23", - line="do_something()", + with ( + patch( + "homeassistant.helpers.frame.linecache.getline", + return_value="await session.close()", + ), + patch( + "homeassistant.helpers.frame.get_current_frame", + return_value=extract_stack_to_frame( + [ + Mock( + filename="/home/paulus/homeassistant/core.py", + lineno="23", + line="do_something()", + ), + Mock( + filename="/home/paulus/config/custom_components/hue/light.py", + lineno="23", + line="await session.close()", + ), + Mock( + filename="/home/paulus/aiohue/lights.py", + lineno="2", + line="something()", + ), + ] ), - Mock( - filename="/home/paulus/config/custom_components/hue/light.py", - lineno="23", - line="await session.close()", - ), - Mock( - filename="/home/paulus/aiohue/lights.py", - lineno="2", - line="something()", - ), - ], + ), ): session = client.async_get_clientsession(hass) await session.close() @@ -272,8 +294,8 @@ async def test_sending_named_tuple( session = client.async_create_clientsession(hass) resp = await session.post("http://127.0.0.1/rgb", json={"rgb": RGBColor(4, 3, 2)}) assert resp.status == 200 - await resp.json() == {"rgb": RGBColor(4, 3, 2)} - aioclient_mock.mock_calls[0][2]["rgb"] == RGBColor(4, 3, 2) + assert await resp.json() == {"rgb": [4, 3, 2]} + assert aioclient_mock.mock_calls[0][2]["rgb"] == RGBColor(4, 3, 2) async def test_client_session_immutable_headers(hass: HomeAssistant) -> None: diff --git a/tests/helpers/test_area_registry.py b/tests/helpers/test_area_registry.py index a1c18d41ec9..1ee8a42b6b9 100644 --- a/tests/helpers/test_area_registry.py +++ b/tests/helpers/test_area_registry.py @@ -1,4 +1,5 @@ """Tests for the Area Registry.""" + from typing import Any import pytest diff --git a/tests/helpers/test_category_registry.py b/tests/helpers/test_category_registry.py new file mode 100644 index 00000000000..a6a36940a68 --- /dev/null +++ b/tests/helpers/test_category_registry.py @@ -0,0 +1,396 @@ +"""Tests for the category registry.""" + +import re +from typing import Any + +import pytest + +from homeassistant.core import HomeAssistant +from homeassistant.helpers import category_registry as cr + +from tests.common import async_capture_events, flush_store + + +async def test_list_categories_for_scope( + category_registry: cr.CategoryRegistry, +) -> None: + """Make sure that we can read categories for scope.""" + categories = category_registry.async_list_categories(scope="automation") + assert len(list(categories)) == len( + category_registry.categories.get("automation", {}) + ) + + +async def test_create_category( + hass: HomeAssistant, category_registry: cr.CategoryRegistry +) -> None: + """Make sure that we can create new categories.""" + update_events = async_capture_events(hass, cr.EVENT_CATEGORY_REGISTRY_UPDATED) + category = category_registry.async_create( + scope="automation", + name="Energy saving", + icon="mdi:leaf", + ) + + assert category.category_id + assert category.name == "Energy saving" + assert category.icon == "mdi:leaf" + + assert len(category_registry.categories) == 1 + assert len(category_registry.categories["automation"]) == 1 + + await hass.async_block_till_done() + + assert len(update_events) == 1 + assert update_events[0].data == { + "action": "create", + "scope": "automation", + "category_id": category.category_id, + } + + +async def test_create_category_with_name_already_in_use( + hass: HomeAssistant, category_registry: cr.CategoryRegistry +) -> None: + """Make sure that we can't create a category with the same name within a scope.""" + update_events = async_capture_events(hass, cr.EVENT_CATEGORY_REGISTRY_UPDATED) + category_registry.async_create( + scope="automation", + name="Energy saving", + icon="mdi:leaf", + ) + + with pytest.raises( + ValueError, + match=re.escape("The name 'ENERGY SAVING' is already in use"), + ): + category_registry.async_create( + scope="automation", + name="ENERGY SAVING", + icon="mdi:leaf", + ) + + await hass.async_block_till_done() + + assert len(category_registry.categories["automation"]) == 1 + assert len(update_events) == 1 + + +async def test_create_category_with_duplicate_name_in_other_scopes( + hass: HomeAssistant, category_registry: cr.CategoryRegistry +) -> None: + """Make we can create the same category in multiple scopes.""" + update_events = async_capture_events(hass, cr.EVENT_CATEGORY_REGISTRY_UPDATED) + category_registry.async_create( + scope="automation", + name="Energy saving", + icon="mdi:leaf", + ) + category_registry.async_create( + scope="script", + name="Energy saving", + icon="mdi:leaf", + ) + + await hass.async_block_till_done() + + assert len(category_registry.categories["script"]) == 1 + assert len(category_registry.categories["automation"]) == 1 + assert len(update_events) == 2 + + +async def test_delete_category( + hass: HomeAssistant, category_registry: cr.CategoryRegistry +) -> None: + """Make sure that we can delete a category.""" + update_events = async_capture_events(hass, cr.EVENT_CATEGORY_REGISTRY_UPDATED) + category = category_registry.async_create( + scope="automation", + name="Energy saving", + icon="mdi:leaf", + ) + + assert len(category_registry.categories["automation"]) == 1 + + category_registry.async_delete(scope="automation", category_id=category.category_id) + + assert not category_registry.categories["automation"] + + await hass.async_block_till_done() + + assert len(update_events) == 2 + assert update_events[0].data == { + "action": "create", + "scope": "automation", + "category_id": category.category_id, + } + assert update_events[1].data == { + "action": "remove", + "scope": "automation", + "category_id": category.category_id, + } + + +async def test_delete_non_existing_category( + category_registry: cr.CategoryRegistry, +) -> None: + """Make sure that we can't delete a category that doesn't exist.""" + category = category_registry.async_create( + scope="automation", + name="Energy saving", + icon="mdi:leaf", + ) + + with pytest.raises(KeyError): + category_registry.async_delete(scope="automation", category_id="") + + with pytest.raises(KeyError): + category_registry.async_delete(scope="", category_id=category.category_id) + + assert len(category_registry.categories["automation"]) == 1 + + +async def test_update_category( + hass: HomeAssistant, category_registry: cr.CategoryRegistry +) -> None: + """Make sure that we can update categories.""" + update_events = async_capture_events(hass, cr.EVENT_CATEGORY_REGISTRY_UPDATED) + category = category_registry.async_create( + scope="automation", + name="Energy saving", + ) + + assert len(category_registry.categories["automation"]) == 1 + assert category.category_id + assert category.name == "Energy saving" + assert category.icon is None + + updated_category = category_registry.async_update( + scope="automation", + category_id=category.category_id, + name="ENERGY SAVING", + icon="mdi:leaf", + ) + + assert updated_category != category + assert updated_category.category_id == category.category_id + assert updated_category.name == "ENERGY SAVING" + assert updated_category.icon == "mdi:leaf" + + assert len(category_registry.categories["automation"]) == 1 + + await hass.async_block_till_done() + + assert len(update_events) == 2 + assert update_events[0].data == { + "action": "create", + "scope": "automation", + "category_id": category.category_id, + } + assert update_events[1].data == { + "action": "update", + "scope": "automation", + "category_id": category.category_id, + } + + +async def test_update_category_with_same_data( + hass: HomeAssistant, category_registry: cr.CategoryRegistry +) -> None: + """Make sure that we can reapply the same data to a category and it won't update.""" + update_events = async_capture_events(hass, cr.EVENT_CATEGORY_REGISTRY_UPDATED) + category = category_registry.async_create( + scope="automation", + name="Energy saving", + icon="mdi:leaf", + ) + + updated_category = category_registry.async_update( + scope="automation", + category_id=category.category_id, + name="Energy saving", + icon="mdi:leaf", + ) + assert category == updated_category + + await hass.async_block_till_done() + + # No update event + assert len(update_events) == 1 + assert update_events[0].data == { + "action": "create", + "scope": "automation", + "category_id": category.category_id, + } + + +async def test_update_category_with_same_name_change_case( + category_registry: cr.CategoryRegistry, +) -> None: + """Make sure that we can reapply the same name with a different case to a category.""" + category = category_registry.async_create( + scope="automation", + name="Energy saving", + icon="mdi:leaf", + ) + + updated_category = category_registry.async_update( + scope="automation", + category_id=category.category_id, + name="ENERGY SAVING", + ) + + assert updated_category.category_id == category.category_id + assert updated_category.name == "ENERGY SAVING" + assert updated_category.icon == "mdi:leaf" + assert len(category_registry.categories["automation"]) == 1 + + +async def test_update_category_with_name_already_in_use( + category_registry: cr.CategoryRegistry, +) -> None: + """Make sure that we can't update a category with a name already in use.""" + category1 = category_registry.async_create( + scope="automation", + name="Energy saving", + icon="mdi:leaf", + ) + category2 = category_registry.async_create( + scope="automation", + name="Something else", + icon="mdi:leaf", + ) + + with pytest.raises( + ValueError, + match=re.escape("The name 'ENERGY SAVING' is already in use"), + ): + category_registry.async_update( + scope="automation", + category_id=category2.category_id, + name="ENERGY SAVING", + ) + + assert category1.name == "Energy saving" + assert category2.name == "Something else" + assert len(category_registry.categories["automation"]) == 2 + + +async def test_load_categories( + hass: HomeAssistant, category_registry: cr.CategoryRegistry +) -> None: + """Make sure that we can load/save data correctly.""" + category1 = category_registry.async_create( + scope="automation", + name="Energy saving", + icon="mdi:leaf", + ) + category2 = category_registry.async_create( + scope="automation", + name="Something else", + icon="mdi:leaf", + ) + category3 = category_registry.async_create( + scope="zone", + name="Grocery stores", + icon="mdi:store", + ) + + assert len(category_registry.categories) == 2 + assert len(category_registry.categories["automation"]) == 2 + assert len(category_registry.categories["zone"]) == 1 + + registry2 = cr.CategoryRegistry(hass) + await flush_store(category_registry._store) + await registry2.async_load() + + assert len(registry2.categories) == 2 + assert len(registry2.categories["automation"]) == 2 + assert len(registry2.categories["zone"]) == 1 + assert list(category_registry.categories) == list(registry2.categories) + assert list(category_registry.categories["automation"]) == list( + registry2.categories["automation"] + ) + assert list(category_registry.categories["zone"]) == list( + registry2.categories["zone"] + ) + + category1_registry2 = registry2.async_get_category( + scope="automation", category_id=category1.category_id + ) + assert category1_registry2.category_id == category1.category_id + assert category1_registry2.name == category1.name + assert category1_registry2.icon == category1.icon + + category2_registry2 = registry2.async_get_category( + scope="automation", category_id=category2.category_id + ) + assert category2_registry2.category_id == category2.category_id + assert category2_registry2.name == category2.name + assert category2_registry2.icon == category2.icon + + category3_registry2 = registry2.async_get_category( + scope="zone", category_id=category3.category_id + ) + assert category3_registry2.category_id == category3.category_id + assert category3_registry2.name == category3.name + assert category3_registry2.icon == category3.icon + + +@pytest.mark.parametrize("load_registries", [False]) +async def test_loading_categories_from_storage( + hass: HomeAssistant, hass_storage: Any +) -> None: + """Test loading stored categories on start.""" + hass_storage[cr.STORAGE_KEY] = { + "version": cr.STORAGE_VERSION_MAJOR, + "data": { + "categories": { + "automation": [ + { + "category_id": "uuid1", + "name": "Energy saving", + "icon": "mdi:leaf", + }, + { + "category_id": "uuid2", + "name": "Something else", + "icon": None, + }, + ], + "zone": [ + { + "category_id": "uuid3", + "name": "Grocery stores", + "icon": "mdi:store", + }, + ], + } + }, + } + + await cr.async_load(hass) + category_registry = cr.async_get(hass) + + assert len(category_registry.categories) == 2 + assert len(category_registry.categories["automation"]) == 2 + assert len(category_registry.categories["zone"]) == 1 + + category1 = category_registry.async_get_category( + scope="automation", category_id="uuid1" + ) + assert category1.category_id == "uuid1" + assert category1.name == "Energy saving" + assert category1.icon == "mdi:leaf" + + category2 = category_registry.async_get_category( + scope="automation", category_id="uuid2" + ) + assert category2.category_id == "uuid2" + assert category2.name == "Something else" + assert category2.icon is None + + category3 = category_registry.async_get_category(scope="zone", category_id="uuid3") + assert category3.category_id == "uuid3" + assert category3.name == "Grocery stores" + assert category3.icon == "mdi:store" diff --git a/tests/helpers/test_check_config.py b/tests/helpers/test_check_config.py index edf1066f744..de7edf42dc2 100644 --- a/tests/helpers/test_check_config.py +++ b/tests/helpers/test_check_config.py @@ -1,4 +1,5 @@ """Test check_config helper.""" + import logging from unittest.mock import Mock, patch @@ -42,11 +43,9 @@ BAD_CORE_CONFIG = "homeassistant:\n unit_system: bad\n\n\n" def log_ha_config(conf): """Log the returned config.""" - cnt = 0 _LOGGER.debug("CONFIG - %s lines - %s errors", len(conf), len(conf.errors)) - for key, val in conf.items(): - _LOGGER.debug("#%s - %s: %s", cnt, key, val) - cnt += 1 + for cnt, (key, val) in enumerate(conf.items()): + _LOGGER.debug("#%s - %s: %s", cnt + 1, key, val) for cnt, err in enumerate(conf.errors): _LOGGER.debug("error[%s] = %s", cnt, err) @@ -122,10 +121,14 @@ async def test_integrationt_requirement_not_found(hass: HomeAssistant) -> None: """Test errors if integration with a requirement not found not found.""" # Make sure they don't exist files = {YAML_CONFIG_FILE: BASE_CONFIG + "test_custom_component:"} - with patch( - "homeassistant.helpers.check_config.async_get_integration_with_requirements", - side_effect=RequirementsNotFound("test_custom_component", ["any"]), - ), patch("os.path.isfile", return_value=True), patch_yaml_files(files): + with ( + patch( + "homeassistant.helpers.check_config.async_get_integration_with_requirements", + side_effect=RequirementsNotFound("test_custom_component", ["any"]), + ), + patch("os.path.isfile", return_value=True), + patch_yaml_files(files), + ): res = await async_check_ha_config_file(hass) log_ha_config(res) @@ -171,10 +174,14 @@ async def test_integration_import_error(hass: HomeAssistant) -> None: """Test errors if integration with a requirement not found not found.""" # Make sure they don't exist files = {YAML_CONFIG_FILE: BASE_CONFIG + "light:"} - with patch( - "homeassistant.loader.Integration.get_component", - side_effect=ImportError("blablabla"), - ), patch("os.path.isfile", return_value=True), patch_yaml_files(files): + with ( + patch( + "homeassistant.loader.Integration.async_get_component", + side_effect=ImportError("blablabla"), + ), + patch("os.path.isfile", return_value=True), + patch_yaml_files(files), + ): res = await async_check_ha_config_file(hass) log_ha_config(res) @@ -337,10 +344,15 @@ async def test_config_platform_import_error(hass: HomeAssistant) -> None: """Test errors if config platform fails to import.""" # Make sure they don't exist files = {YAML_CONFIG_FILE: BASE_CONFIG + "light:\n platform: beer"} - with patch( - "homeassistant.loader.Integration.get_platform", - side_effect=ImportError("blablabla"), - ), patch("os.path.isfile", return_value=True), patch_yaml_files(files): + with ( + patch( + "homeassistant.loader.Integration.async_get_platform", + side_effect=ImportError("blablabla"), + ), + patch("os.path.isfile", return_value=True), + patch("homeassistant.loader.Integration.platforms_exists", return_value=True), + patch_yaml_files(files), + ): res = await async_check_ha_config_file(hass) log_ha_config(res) @@ -357,10 +369,15 @@ async def test_platform_import_error(hass: HomeAssistant) -> None: """Test errors if platform not found.""" # Make sure they don't exist files = {YAML_CONFIG_FILE: BASE_CONFIG + "light:\n platform: demo"} - with patch( - "homeassistant.loader.Integration.get_platform", - side_effect=[None, ImportError("blablabla")], - ), patch("os.path.isfile", return_value=True), patch_yaml_files(files): + with ( + patch( + "homeassistant.loader.Integration.async_get_platform", + side_effect=[None, ImportError("blablabla")], + ), + patch("homeassistant.loader.Integration.platforms_exists", return_value=True), + patch("os.path.isfile", return_value=True), + patch_yaml_files(files), + ): res = await async_check_ha_config_file(hass) log_ha_config(res) diff --git a/tests/helpers/test_collection.py b/tests/helpers/test_collection.py index a385ca8aeb6..6d2764afb16 100644 --- a/tests/helpers/test_collection.py +++ b/tests/helpers/test_collection.py @@ -1,4 +1,5 @@ """Tests for the collection helper.""" + from __future__ import annotations import logging diff --git a/tests/helpers/test_condition.py b/tests/helpers/test_condition.py index bcb6f4fa971..1b279fd0f51 100644 --- a/tests/helpers/test_condition.py +++ b/tests/helpers/test_condition.py @@ -1,4 +1,5 @@ """Test the condition helper.""" + from datetime import datetime, timedelta from typing import Any from unittest.mock import AsyncMock, patch @@ -3396,7 +3397,7 @@ async def test_disabled_condition(hass: HomeAssistant) -> None: hass.states.async_set("binary_sensor.test", "on") assert test(hass) is None - # Still passses, condition is not enabled + # Still passes, condition is not enabled hass.states.async_set("binary_sensor.test", "off") assert test(hass) is None diff --git a/tests/helpers/test_config_entry_flow.py b/tests/helpers/test_config_entry_flow.py index 71c81b096ca..e99cfbb2f58 100644 --- a/tests/helpers/test_config_entry_flow.py +++ b/tests/helpers/test_config_entry_flow.py @@ -1,4 +1,5 @@ """Tests for the Config Entry Flow helper.""" + from collections.abc import Generator from unittest.mock import Mock, PropertyMock, patch @@ -375,18 +376,23 @@ async def test_webhook_create_cloudhook( ) assert result["type"] == data_entry_flow.FlowResultType.FORM - with patch( - "hass_nabucasa.cloudhooks.Cloudhooks.async_create", - return_value={"cloudhook_url": "https://example.com"}, - ) as mock_create, patch( - "hass_nabucasa.Cloud.subscription_expired", - new_callable=PropertyMock(return_value=False), - ), patch( - "hass_nabucasa.Cloud.is_logged_in", - new_callable=PropertyMock(return_value=True), - ), patch( - "hass_nabucasa.iot_base.BaseIoT.connected", - new_callable=PropertyMock(return_value=True), + with ( + patch( + "hass_nabucasa.cloudhooks.Cloudhooks.async_create", + return_value={"cloudhook_url": "https://example.com"}, + ) as mock_create, + patch( + "hass_nabucasa.Cloud.subscription_expired", + new_callable=PropertyMock(return_value=False), + ), + patch( + "hass_nabucasa.Cloud.is_logged_in", + new_callable=PropertyMock(return_value=True), + ), + patch( + "hass_nabucasa.iot_base.BaseIoT.connected", + new_callable=PropertyMock(return_value=True), + ), ): result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) @@ -403,6 +409,7 @@ async def test_webhook_create_cloudhook( assert len(mock_delete.mock_calls) == 1 assert result["require_restart"] is False + await hass.async_block_till_done() async def test_webhook_create_cloudhook_aborts_not_connected( @@ -430,18 +437,23 @@ async def test_webhook_create_cloudhook_aborts_not_connected( ) assert result["type"] == data_entry_flow.FlowResultType.FORM - with patch( - "hass_nabucasa.cloudhooks.Cloudhooks.async_create", - return_value={"cloudhook_url": "https://example.com"}, - ), patch( - "hass_nabucasa.Cloud.subscription_expired", - new_callable=PropertyMock(return_value=False), - ), patch( - "hass_nabucasa.Cloud.is_logged_in", - new_callable=PropertyMock(return_value=True), - ), patch( - "hass_nabucasa.iot_base.BaseIoT.connected", - new_callable=PropertyMock(return_value=False), + with ( + patch( + "hass_nabucasa.cloudhooks.Cloudhooks.async_create", + return_value={"cloudhook_url": "https://example.com"}, + ), + patch( + "hass_nabucasa.Cloud.subscription_expired", + new_callable=PropertyMock(return_value=False), + ), + patch( + "hass_nabucasa.Cloud.is_logged_in", + new_callable=PropertyMock(return_value=True), + ), + patch( + "hass_nabucasa.iot_base.BaseIoT.connected", + new_callable=PropertyMock(return_value=False), + ), ): result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) diff --git a/tests/helpers/test_config_entry_oauth2_flow.py b/tests/helpers/test_config_entry_oauth2_flow.py index d237c24f6f3..a9e69f542f3 100644 --- a/tests/helpers/test_config_entry_oauth2_flow.py +++ b/tests/helpers/test_config_entry_oauth2_flow.py @@ -1,4 +1,5 @@ """Tests for the Somfy config flow.""" + from http import HTTPStatus import logging import time @@ -89,7 +90,7 @@ class MockOAuth2Implementation(config_entry_oauth2_flow.AbstractOAuth2Implementa async def _async_refresh_token(self, token: dict) -> dict: """Refresh a token.""" - raise NotImplementedError() + raise NotImplementedError def test_inherit_enforces_domain_set() -> None: @@ -103,9 +104,10 @@ def test_inherit_enforces_domain_set() -> None: """Return logger.""" return logging.getLogger(__name__) - with patch.dict( - config_entries.HANDLERS, {TEST_DOMAIN: TestFlowHandler} - ), pytest.raises(TypeError): + with ( + patch.dict(config_entries.HANDLERS, {TEST_DOMAIN: TestFlowHandler}), + pytest.raises(TypeError), + ): TestFlowHandler() diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py index 060800be62d..133e5e80442 100644 --- a/tests/helpers/test_config_validation.py +++ b/tests/helpers/test_config_validation.py @@ -1,4 +1,5 @@ """Test config validators.""" + from collections import OrderedDict from datetime import date, datetime, timedelta import enum @@ -96,8 +97,9 @@ def test_isfile() -> None: # patching methods that allow us to fake a file existing # with write access - with patch("os.path.isfile", Mock(return_value=True)), patch( - "os.access", Mock(return_value=True) + with ( + patch("os.path.isfile", Mock(return_value=True)), + patch("os.access", Mock(return_value=True)), ): schema("test.txt") @@ -1396,7 +1398,7 @@ def test_key_value_schemas_with_default() -> None: @pytest.mark.parametrize( ("config", "error"), - ( + [ ({"delay": "{{ invalid"}, "should be format 'HH:MM'"), ({"wait_template": "{{ invalid"}, "invalid template"), ({"condition": "invalid"}, "Unexpected value for condition: 'invalid'"), @@ -1431,7 +1433,7 @@ def test_key_value_schemas_with_default() -> None: }, "not allowed to add a response to an error stop action", ), - ), + ], ) def test_script(caplog: pytest.LogCaptureFixture, config: dict, error: str) -> None: """Test script validation is user friendly.""" @@ -1590,7 +1592,7 @@ def test_config_entry_only_schema_cant_find_module() -> None: def test_config_entry_only_schema_no_hass( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: - """Test if the the hass context is not set in our context.""" + """Test if the hass context is not set in our context.""" with patch( "homeassistant.helpers.config_validation.async_get_hass", side_effect=HomeAssistantError, diff --git a/tests/helpers/test_debounce.py b/tests/helpers/test_debounce.py index 958b88951ce..84b3d19b6d7 100644 --- a/tests/helpers/test_debounce.py +++ b/tests/helpers/test_debounce.py @@ -1,4 +1,5 @@ """Tests for debounce.""" + import asyncio from datetime import timedelta import logging @@ -496,3 +497,35 @@ async def test_shutdown(hass: HomeAssistant, caplog: pytest.LogCaptureFixture) - assert len(calls) == 1 assert debouncer._timer_task is None + + +async def test_background( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test background tasks are created when background is True.""" + calls = [] + + async def _func() -> None: + await asyncio.sleep(0.1) + calls.append(None) + + debouncer = debounce.Debouncer( + hass, _LOGGER, cooldown=0.05, immediate=True, function=_func, background=True + ) + + await debouncer.async_call() + assert len(calls) == 1 + + debouncer.async_schedule_call() + assert len(calls) == 1 + + async_fire_time_changed(hass, utcnow() + timedelta(seconds=1)) + await hass.async_block_till_done(wait_background_tasks=False) + assert len(calls) == 1 + + await hass.async_block_till_done(wait_background_tasks=True) + assert len(calls) == 2 + + async_fire_time_changed(hass, utcnow() + timedelta(seconds=1)) + await hass.async_block_till_done(wait_background_tasks=False) + assert len(calls) == 2 diff --git a/tests/helpers/test_deprecation.py b/tests/helpers/test_deprecation.py index 25b37e2073f..b53a6d5ec1d 100644 --- a/tests/helpers/test_deprecation.py +++ b/tests/helpers/test_deprecation.py @@ -1,4 +1,5 @@ """Test deprecation helpers.""" + from enum import StrEnum import logging import sys @@ -20,7 +21,7 @@ from homeassistant.helpers.deprecation import ( ) from homeassistant.helpers.frame import MissingIntegrationFrame -from tests.common import MockModule, mock_integration +from tests.common import MockModule, extract_stack_to_frame, mock_integration class MockBaseClassDeprecatedProperty: @@ -177,25 +178,33 @@ def test_deprecated_function_called_from_built_in_integration( def mock_deprecated_function(): pass - with patch( - "homeassistant.helpers.frame.extract_stack", - return_value=[ - Mock( - filename="/home/paulus/homeassistant/core.py", - lineno="23", - line="do_something()", + with ( + patch( + "homeassistant.helpers.frame.linecache.getline", + return_value="await session.close()", + ), + patch( + "homeassistant.helpers.frame.get_current_frame", + return_value=extract_stack_to_frame( + [ + Mock( + filename="/home/paulus/homeassistant/core.py", + lineno="23", + line="do_something()", + ), + Mock( + filename="/home/paulus/homeassistant/components/hue/light.py", + lineno="23", + line="await session.close()", + ), + Mock( + filename="/home/paulus/aiohue/lights.py", + lineno="2", + line="something()", + ), + ] ), - Mock( - filename="/home/paulus/homeassistant/components/hue/light.py", - lineno="23", - line="await session.close()", - ), - Mock( - filename="/home/paulus/aiohue/lights.py", - lineno="2", - line="something()", - ), - ], + ), ): mock_deprecated_function() assert ( @@ -229,25 +238,33 @@ def test_deprecated_function_called_from_custom_integration( def mock_deprecated_function(): pass - with patch( - "homeassistant.helpers.frame.extract_stack", - return_value=[ - Mock( - filename="/home/paulus/homeassistant/core.py", - lineno="23", - line="do_something()", + with ( + patch( + "homeassistant.helpers.frame.linecache.getline", + return_value="await session.close()", + ), + patch( + "homeassistant.helpers.frame.get_current_frame", + return_value=extract_stack_to_frame( + [ + Mock( + filename="/home/paulus/homeassistant/core.py", + lineno="23", + line="do_something()", + ), + Mock( + filename="/home/paulus/config/custom_components/hue/light.py", + lineno="23", + line="await session.close()", + ), + Mock( + filename="/home/paulus/aiohue/lights.py", + lineno="2", + line="something()", + ), + ] ), - Mock( - filename="/home/paulus/config/custom_components/hue/light.py", - lineno="23", - line="await session.close()", - ), - Mock( - filename="/home/paulus/aiohue/lights.py", - lineno="2", - line="something()", - ), - ], + ), ): mock_deprecated_function() assert ( @@ -326,25 +343,34 @@ def test_check_if_deprecated_constant( filename = f"/home/paulus/{module_name.replace('.', '/')}.py" # mock sys.modules for homeassistant/helpers/frame.py#get_integration_frame - with patch.dict(sys.modules, {module_name: Mock(__file__=filename)}), patch( - "homeassistant.helpers.frame.extract_stack", - return_value=[ - Mock( - filename="/home/paulus/homeassistant/core.py", - lineno="23", - line="do_something()", + with ( + patch.dict(sys.modules, {module_name: Mock(__file__=filename)}), + patch( + "homeassistant.helpers.frame.linecache.getline", + return_value="await session.close()", + ), + patch( + "homeassistant.helpers.frame.get_current_frame", + return_value=extract_stack_to_frame( + [ + Mock( + filename="/home/paulus/homeassistant/core.py", + lineno="23", + line="do_something()", + ), + Mock( + filename=filename, + lineno="23", + line="await session.close()", + ), + Mock( + filename="/home/paulus/aiohue/lights.py", + lineno="2", + line="something()", + ), + ] ), - Mock( - filename=filename, - lineno="23", - line="await session.close()", - ), - Mock( - filename="/home/paulus/aiohue/lights.py", - lineno="2", - line="something()", - ), - ], + ), ): value = check_if_deprecated_constant("TEST_CONSTANT", module_globals) assert value == _get_value(deprecated_constant) @@ -397,7 +423,8 @@ def test_check_if_deprecated_constant_integration_not_found( } with patch( - "homeassistant.helpers.frame.extract_stack", side_effect=MissingIntegrationFrame + "homeassistant.helpers.frame.get_current_frame", + side_effect=MissingIntegrationFrame, ): value = check_if_deprecated_constant("TEST_CONSTANT", module_globals) assert value == _get_value(deprecated_constant) diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index cda117cca26..bed3dea4dc1 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -1,4 +1,5 @@ """Tests for the Device Registry.""" + from collections.abc import Iterable from contextlib import AbstractContextManager, nullcontext import time @@ -2303,7 +2304,7 @@ async def test_entries_for_label( "placeholders", "expected_device_name", ), - ( + [ (None, None, None, "Device Bla"), ( "test_device", @@ -2333,7 +2334,7 @@ async def test_entries_for_label( {"placeholder": "special"}, "English dev special", ), - ), + ], ) async def test_device_name_translation_placeholders( hass: HomeAssistant, @@ -2380,7 +2381,7 @@ async def test_device_name_translation_placeholders( "expectation", "expected_error", ), - ( + [ ( "test_device", { @@ -2425,7 +2426,7 @@ async def test_device_name_translation_placeholders( "not match the name '{placeholder} English dev'" ), ), - ), + ], ) async def test_device_name_translation_placeholders_errors( hass: HomeAssistant, @@ -2452,13 +2453,17 @@ async def test_device_name_translation_placeholders_errors( config_entry_1 = MockConfigEntry() config_entry_1.add_to_hass(hass) - with patch( - "homeassistant.helpers.device_registry.translation.async_get_cached_translations", - side_effect=async_get_cached_translations, - ), patch( - "homeassistant.helpers.device_registry.get_release_channel", - return_value=release_channel, - ), expectation: + with ( + patch( + "homeassistant.helpers.device_registry.translation.async_get_cached_translations", + side_effect=async_get_cached_translations, + ), + patch( + "homeassistant.helpers.device_registry.get_release_channel", + return_value=release_channel, + ), + expectation, + ): device_registry.async_get_or_create( config_entry_id=config_entry_1.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, diff --git a/tests/helpers/test_discovery.py b/tests/helpers/test_discovery.py index d73bfe84607..dc4b2951b2f 100644 --- a/tests/helpers/test_discovery.py +++ b/tests/helpers/test_discovery.py @@ -1,4 +1,5 @@ """Test discovery helpers.""" + from unittest.mock import patch import pytest diff --git a/tests/helpers/test_discovery_flow.py b/tests/helpers/test_discovery_flow.py index 0b3386f8e04..7710eb2c7c7 100644 --- a/tests/helpers/test_discovery_flow.py +++ b/tests/helpers/test_discovery_flow.py @@ -1,4 +1,5 @@ """Test the discovery flow helper.""" + from unittest.mock import AsyncMock, call, patch import pytest diff --git a/tests/helpers/test_dispatcher.py b/tests/helpers/test_dispatcher.py index add80c941a1..149231a9368 100644 --- a/tests/helpers/test_dispatcher.py +++ b/tests/helpers/test_dispatcher.py @@ -1,14 +1,15 @@ """Test dispatcher helpers.""" + from functools import partial import pytest from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import ( - SignalType, async_dispatcher_connect, async_dispatcher_send, ) +from homeassistant.util.signal_type import SignalType, SignalTypeFormat async def test_simple_function(hass: HomeAssistant) -> None: @@ -57,6 +58,27 @@ async def test_signal_type(hass: HomeAssistant) -> None: assert calls == [("Hello", 2), ("World", 3), ("x", 4)] +async def test_signal_type_format(hass: HomeAssistant) -> None: + """Test dispatcher with SignalType and format.""" + signal: SignalTypeFormat[str, int] = SignalTypeFormat("test-{}") + calls: list[tuple[str, int]] = [] + + def test_funct(data1: str, data2: int) -> None: + calls.append((data1, data2)) + + async_dispatcher_connect(hass, signal.format("unique-id"), test_funct) + async_dispatcher_send(hass, signal.format("unique-id"), "Hello", 2) + await hass.async_block_till_done() + + assert calls == [("Hello", 2)] + + # Test compatibility with string keys + async_dispatcher_send(hass, "test-{}".format("unique-id"), "x", 4) + await hass.async_block_till_done() + + assert calls == [("Hello", 2), ("x", 4)] + + async def test_simple_function_unsub(hass: HomeAssistant) -> None: """Test simple function (executor) and unsub.""" calls1 = [] diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index e9950ec4dfc..dac03f0be67 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -1,4 +1,5 @@ """Test the entity helper.""" + import asyncio from collections.abc import Iterable import dataclasses @@ -22,7 +23,13 @@ from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.core import Context, HomeAssistant, HomeAssistantError +from homeassistant.core import ( + Context, + HassJobType, + HomeAssistant, + HomeAssistantError, + callback, +) from homeassistant.helpers import device_registry as dr, entity, entity_registry as er from homeassistant.helpers.entity_component import async_update_entity from homeassistant.helpers.typing import UNDEFINED, UndefinedType @@ -734,12 +741,13 @@ async def test_disabled_in_entity_registry(hass: HomeAssistant) -> None: async def test_capability_attrs(hass: HomeAssistant) -> None: """Test we still include capabilities even when unavailable.""" - with patch.object( - entity.Entity, "available", PropertyMock(return_value=False) - ), patch.object( - entity.Entity, - "capability_attributes", - PropertyMock(return_value={"always": "there"}), + with ( + patch.object(entity.Entity, "available", PropertyMock(return_value=False)), + patch.object( + entity.Entity, + "capability_attributes", + PropertyMock(return_value={"always": "there"}), + ), ): ent = entity.Entity() ent.hass = hass @@ -926,10 +934,10 @@ async def test_entity_category_property(hass: HomeAssistant) -> None: @pytest.mark.parametrize( ("value", "expected"), - ( + [ ("config", entity.EntityCategory.CONFIG), ("diagnostic", entity.EntityCategory.DIAGNOSTIC), - ), + ], ) def test_entity_category_schema(value, expected) -> None: """Test entity category schema.""" @@ -939,7 +947,7 @@ def test_entity_category_schema(value, expected) -> None: assert isinstance(result, entity.EntityCategory) -@pytest.mark.parametrize("value", (None, "non_existing")) +@pytest.mark.parametrize("value", [None, "non_existing"]) def test_entity_category_schema_error(value) -> None: """Test entity category schema.""" schema = vol.Schema(entity.ENTITY_CATEGORIES_SCHEMA) @@ -1000,14 +1008,14 @@ async def _test_friendly_name( "device_name", "expected_friendly_name", ), - ( + [ (False, "Entity Blu", "Device Bla", "Entity Blu"), (False, None, "Device Bla", None), (True, "Entity Blu", "Device Bla", "Device Bla Entity Blu"), (True, None, "Device Bla", "Device Bla"), (True, "Entity Blu", UNDEFINED, "Entity Blu"), (True, "Entity Blu", None, "Mock Title Entity Blu"), - ), + ], ) async def test_friendly_name_attr( hass: HomeAssistant, @@ -1037,14 +1045,14 @@ async def test_friendly_name_attr( @pytest.mark.parametrize( ("has_entity_name", "entity_name", "expected_friendly_name"), - ( + [ (False, "Entity Blu", "Entity Blu"), (False, None, None), (False, UNDEFINED, None), (True, "Entity Blu", "Device Bla Entity Blu"), (True, None, "Device Bla"), (True, UNDEFINED, "Device Bla None"), - ), + ], ) async def test_friendly_name_description( hass: HomeAssistant, @@ -1074,14 +1082,14 @@ async def test_friendly_name_description( @pytest.mark.parametrize( ("has_entity_name", "entity_name", "expected_friendly_name"), - ( + [ (False, "Entity Blu", "Entity Blu"), (False, None, None), (False, UNDEFINED, None), (True, "Entity Blu", "Device Bla Entity Blu"), (True, None, "Device Bla"), (True, UNDEFINED, "Device Bla English cls"), - ), + ], ) async def test_friendly_name_description_device_class_name( hass: HomeAssistant, @@ -1143,7 +1151,7 @@ async def test_friendly_name_description_device_class_name( "placeholders", "expected_friendly_name", ), - ( + [ (False, None, None, None, "Entity Blu"), (True, None, None, None, "Device Bla Entity Blu"), ( @@ -1179,7 +1187,7 @@ async def test_friendly_name_description_device_class_name( {"placeholder": "special"}, "Device Bla English ent special", ), - ), + ], ) async def test_entity_name_translation_placeholders( hass: HomeAssistant, @@ -1232,7 +1240,7 @@ async def test_entity_name_translation_placeholders( "release_channel", "expected_error", ), - ( + [ ( "test_entity", { @@ -1272,7 +1280,7 @@ async def test_entity_name_translation_placeholders( "not match the name '{placeholder} English ent'" ), ), - ), + ], ) async def test_entity_name_translation_placeholder_errors( hass: HomeAssistant, @@ -1321,11 +1329,15 @@ async def test_entity_name_translation_placeholder_errors( caplog.clear() - with patch( - "homeassistant.helpers.entity_platform.translation.async_get_translations", - side_effect=async_get_translations, - ), patch( - "homeassistant.helpers.entity.get_release_channel", return_value=release_channel + with ( + patch( + "homeassistant.helpers.entity_platform.translation.async_get_translations", + side_effect=async_get_translations, + ), + patch( + "homeassistant.helpers.entity.get_release_channel", + return_value=release_channel, + ), ): await entity_platform.async_setup_entry(config_entry) @@ -1334,14 +1346,14 @@ async def test_entity_name_translation_placeholder_errors( @pytest.mark.parametrize( ("has_entity_name", "entity_name", "expected_friendly_name"), - ( + [ (False, "Entity Blu", "Entity Blu"), (False, None, None), (False, UNDEFINED, None), (True, "Entity Blu", "Device Bla Entity Blu"), (True, None, "Device Bla"), (True, UNDEFINED, "Device Bla None"), - ), + ], ) async def test_friendly_name_property( hass: HomeAssistant, @@ -1370,7 +1382,7 @@ async def test_friendly_name_property( @pytest.mark.parametrize( ("has_entity_name", "entity_name", "expected_friendly_name"), - ( + [ (False, "Entity Blu", "Entity Blu"), (False, None, None), (False, UNDEFINED, None), @@ -1378,7 +1390,7 @@ async def test_friendly_name_property( (True, None, "Device Bla"), # Won't use the device class name because the entity overrides the name property (True, UNDEFINED, "Device Bla None"), - ), + ], ) async def test_friendly_name_property_device_class_name( hass: HomeAssistant, @@ -1431,10 +1443,10 @@ async def test_friendly_name_property_device_class_name( @pytest.mark.parametrize( ("has_entity_name", "expected_friendly_name"), - ( + [ (False, None), (True, "Device Bla English cls"), - ), + ], ) async def test_friendly_name_device_class_name( hass: HomeAssistant, @@ -1490,7 +1502,7 @@ async def test_friendly_name_device_class_name( "expected_friendly_name2", "expected_friendly_name3", ), - ( + [ ( "Entity Blu", "Device Bla Entity Blu", @@ -1503,7 +1515,7 @@ async def test_friendly_name_device_class_name( "Device Bla2", "New Device", ), - ), + ], ) async def test_friendly_name_updated( hass: HomeAssistant, @@ -2390,9 +2402,9 @@ async def test_cached_entity_property_class_attribute(hass: HomeAssistant) -> No EntityWithClassAttribute4, ) - entities: list[tuple[entity.Entity, entity.Entity]] = [] - for cls in classes: - entities.append((cls(), cls())) + entities: list[tuple[entity.Entity, entity.Entity]] = [ + (cls(), cls()) for cls in classes + ] for ent in entities: assert getattr(ent[0], property) == values[0] @@ -2558,3 +2570,26 @@ async def test_reset_right_after_remove_entity_registry( assert len(ent.remove_calls) == 1 assert hass.states.get("test.test") is None + + +async def test_get_hassjob_type(hass: HomeAssistant) -> None: + """Test get_hassjob_type.""" + + class AsyncEntity(entity.Entity): + """Test entity.""" + + def update(self): + """Test update Executor.""" + + async def async_update(self): + """Test update Coroutinefunction.""" + + @callback + def update_callback(self): + """Test update Callback.""" + + ent_1 = AsyncEntity() + + assert ent_1.get_hassjob_type("update") is HassJobType.Executor + assert ent_1.get_hassjob_type("async_update") is HassJobType.Coroutinefunction + assert ent_1.get_hassjob_type("update_callback") is HassJobType.Callback diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 07ecd7844da..31c6f8e6e30 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -1,4 +1,5 @@ """Tests for the EntityPlatform helper.""" + import asyncio from collections.abc import Iterable from datetime import timedelta @@ -71,7 +72,7 @@ async def test_polling_only_updates_entities_it_should_poll( poll_ent.async_update.reset_mock() async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=20)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert not no_poll_ent.async_update.called assert poll_ent.async_update.called @@ -120,7 +121,7 @@ async def test_polling_updates_entities_with_exception(hass: HomeAssistant) -> N update_err.clear() async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=20)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert len(update_ok) == 3 assert len(update_err) == 1 @@ -139,7 +140,7 @@ async def test_update_state_adds_entities(hass: HomeAssistant) -> None: ent2.update = lambda *_: component.add_entities([ent1]) async_fire_time_changed(hass, dt_util.utcnow() + DEFAULT_SCAN_INTERVAL) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert len(hass.states.async_entity_ids()) == 2 @@ -200,7 +201,7 @@ async def test_set_scan_interval_via_platform( component = EntityComponent(_LOGGER, DOMAIN, hass) - component.setup({DOMAIN: {"platform": "platform"}}) + await component.async_setup({DOMAIN: {"platform": "platform"}}) await hass.async_block_till_done() assert mock_track.called @@ -1431,9 +1432,9 @@ async def test_override_restored_entities( component = EntityComponent(_LOGGER, DOMAIN, hass) await component.async_setup({}) - await component.async_add_entities( - [MockEntity(unique_id="1234", state="on", entity_id="test_domain.world")], True - ) + ent = MockEntity(unique_id="1234", entity_id="test_domain.world") + ent._attr_state = "on" + await component.async_add_entities([ent], True) state = hass.states.get("test_domain.world") assert state.state == "on" @@ -1710,7 +1711,7 @@ async def test_register_entity_service_limited_to_matching_platforms( } -@pytest.mark.parametrize("update_before_add", (True, False)) +@pytest.mark.parametrize("update_before_add", [True, False]) async def test_invalid_entity_id( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, update_before_add: bool ) -> None: @@ -1737,7 +1738,7 @@ class MockBlockingEntity(MockEntity): await asyncio.sleep(1000) -@pytest.mark.parametrize("update_before_add", (True, False)) +@pytest.mark.parametrize("update_before_add", [True, False]) async def test_setup_entry_with_entities_that_block_forever( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, @@ -1760,8 +1761,9 @@ async def test_setup_entry_with_entities_that_block_forever( hass, platform_name=config_entry.domain, platform=platform ) - with patch.object(entity_platform, "SLOW_ADD_ENTITY_MAX_WAIT", 0.01), patch.object( - entity_platform, "SLOW_ADD_MIN_TIMEOUT", 0.01 + with ( + patch.object(entity_platform, "SLOW_ADD_ENTITY_MAX_WAIT", 0.01), + patch.object(entity_platform, "SLOW_ADD_MIN_TIMEOUT", 0.01), ): assert await platform.async_setup_entry(config_entry) await hass.async_block_till_done() @@ -1783,7 +1785,7 @@ class MockCancellingEntity(MockEntity): raise asyncio.CancelledError -@pytest.mark.parametrize("update_before_add", (True, False)) +@pytest.mark.parametrize("update_before_add", [True, False]) async def test_cancellation_is_not_blocked( hass: HomeAssistant, update_before_add: bool, @@ -1812,7 +1814,7 @@ async def test_cancellation_is_not_blocked( assert full_name not in hass.config.components -@pytest.mark.parametrize("update_before_add", (True, False)) +@pytest.mark.parametrize("update_before_add", [True, False]) async def test_two_platforms_add_same_entity( hass: HomeAssistant, update_before_add: bool ) -> None: @@ -1867,14 +1869,14 @@ class SlowEntity(MockEntity): @pytest.mark.parametrize( ("has_entity_name", "entity_name", "expected_entity_id"), - ( + [ (False, "Entity Blu", "test_domain.entity_blu"), (False, None, "test_domain.test_qwer"), # Set to _ (True, "Entity Blu", "test_domain.device_bla_entity_blu"), (True, None, "test_domain.device_bla"), - ), + ], ) -@pytest.mark.parametrize("update_before_add", (True, False)) +@pytest.mark.parametrize("update_before_add", [True, False]) async def test_entity_name_influences_entity_id( hass: HomeAssistant, entity_registry: er.EntityRegistry, @@ -1920,15 +1922,15 @@ async def test_entity_name_influences_entity_id( @pytest.mark.parametrize( ("language", "has_entity_name", "expected_entity_id"), - ( + [ ("en", False, "test_domain.test_qwer"), # Set to _ ("en", True, "test_domain.device_bla_english_name"), ("sv", True, "test_domain.device_bla_swedish_name"), # Chinese uses english for entity_id ("cn", True, "test_domain.device_bla_english_name"), - ), + ], ) -@pytest.mark.parametrize("update_before_add", (True, False)) +@pytest.mark.parametrize("update_before_add", [True, False]) async def test_translated_entity_name_influences_entity_id( hass: HomeAssistant, entity_registry: er.EntityRegistry, @@ -1997,7 +1999,7 @@ async def test_translated_entity_name_influences_entity_id( @pytest.mark.parametrize( ("language", "has_entity_name", "device_class", "expected_entity_id"), - ( + [ ("en", False, None, "test_domain.test_qwer"), # Set to _ ( "en", @@ -2009,7 +2011,7 @@ async def test_translated_entity_name_influences_entity_id( ("sv", True, "test_class", "test_domain.device_bla_swedish_cls"), # Chinese uses english for entity_id ("cn", True, "test_class", "test_domain.device_bla_english_cls"), - ), + ], ) async def test_translated_device_class_name_influences_entity_id( hass: HomeAssistant, diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index 0178e4fcd11..91c749a0d7f 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -1,4 +1,5 @@ """Tests for the Entity Registry.""" + from datetime import timedelta from typing import Any from unittest.mock import patch @@ -90,6 +91,8 @@ def test_get_or_create_updates_data(entity_registry: er.EntityRegistry) -> None: unit_of_measurement="initial-unit_of_measurement", ) + assert set(entity_registry.async_device_ids()) == {"mock-dev-id"} + assert orig_entry == er.RegistryEntry( "light.hue_5678", "5678", @@ -159,6 +162,8 @@ def test_get_or_create_updates_data(entity_registry: er.EntityRegistry) -> None: unit_of_measurement="updated-unit_of_measurement", ) + assert set(entity_registry.async_device_ids()) == {"new-mock-dev-id"} + new_entry = entity_registry.async_get_or_create( "light", "hue", @@ -203,6 +208,8 @@ def test_get_or_create_updates_data(entity_registry: er.EntityRegistry) -> None: unit_of_measurement=None, ) + assert set(entity_registry.async_device_ids()) == set() + def test_get_or_create_suggested_object_id_conflict_register( entity_registry: er.EntityRegistry, @@ -273,7 +280,9 @@ async def test_loading_saving_data( orig_entry2.entity_id, "light", {"minimum_brightness": 20} ) entity_registry.async_update_entity( - orig_entry2.entity_id, labels={"label1", "label2"} + orig_entry2.entity_id, + categories={"scope", "id"}, + labels={"label1", "label2"}, ) orig_entry2 = entity_registry.async_get(orig_entry2.entity_id) orig_entry3 = entity_registry.async_get_or_create("light", "hue", "ABCD") @@ -303,6 +312,7 @@ async def test_loading_saving_data( assert orig_entry4 == new_entry4 assert new_entry2.area_id == "mock-area-id" + assert new_entry2.categories == {"scope", "id"} assert new_entry2.capabilities == {"max": 100} assert new_entry2.config_entry_id == mock_config.entry_id assert new_entry2.device_class == "user-class" @@ -694,9 +704,10 @@ async def test_update_entity_unique_id_conflict( entry2 = entity_registry.async_get_or_create( "light", "hue", "1234", config_entry=mock_config ) - with patch.object( - entity_registry, "async_schedule_save" - ) as mock_schedule_save, pytest.raises(ValueError): + with ( + patch.object(entity_registry, "async_schedule_save") as mock_schedule_save, + pytest.raises(ValueError), + ): entity_registry.async_update_entity( entry.entity_id, new_unique_id=entry2.unique_id ) @@ -742,9 +753,10 @@ async def test_update_entity_entity_id_entity_id( assert entry2.entity_id != state_entity_id # Try updating to a registered entity_id - with patch.object( - entity_registry, "async_schedule_save" - ) as mock_schedule_save, pytest.raises(ValueError): + with ( + patch.object(entity_registry, "async_schedule_save") as mock_schedule_save, + pytest.raises(ValueError), + ): entity_registry.async_update_entity( entry.entity_id, new_entity_id=entry2.entity_id ) @@ -759,9 +771,10 @@ async def test_update_entity_entity_id_entity_id( assert entity_registry.async_get(entry2.entity_id) is entry2 # Try updating to an entity_id which is in the state machine - with patch.object( - entity_registry, "async_schedule_save" - ) as mock_schedule_save, pytest.raises(ValueError): + with ( + patch.object(entity_registry, "async_schedule_save") as mock_schedule_save, + pytest.raises(ValueError), + ): entity_registry.async_update_entity( entry.entity_id, new_entity_id=state_entity_id ) @@ -941,85 +954,6 @@ async def test_restore_states( assert hass.states.get("light.all_info_set") is None -async def test_async_get_device_class_lookup( - hass: HomeAssistant, entity_registry: er.EntityRegistry -) -> None: - """Test registry device class lookup.""" - hass.set_state(CoreState.not_running) - - entity_registry.async_get_or_create( - "binary_sensor", - "light", - "battery_charging", - device_id="light_device_entry_id", - original_device_class="battery_charging", - ) - entity_registry.async_get_or_create( - "sensor", - "light", - "battery", - device_id="light_device_entry_id", - original_device_class="battery", - ) - entity_registry.async_get_or_create( - "light", "light", "demo", device_id="light_device_entry_id" - ) - entity_registry.async_get_or_create( - "binary_sensor", - "vacuum", - "battery_charging", - device_id="vacuum_device_entry_id", - original_device_class="battery_charging", - ) - entity_registry.async_get_or_create( - "sensor", - "vacuum", - "battery", - device_id="vacuum_device_entry_id", - original_device_class="battery", - ) - entity_registry.async_get_or_create( - "vacuum", "vacuum", "demo", device_id="vacuum_device_entry_id" - ) - entity_registry.async_get_or_create( - "binary_sensor", - "remote", - "battery_charging", - device_id="remote_device_entry_id", - original_device_class="battery_charging", - ) - entity_registry.async_get_or_create( - "remote", "remote", "demo", device_id="remote_device_entry_id" - ) - - device_lookup = entity_registry.async_get_device_class_lookup( - {("binary_sensor", "battery_charging"), ("sensor", "battery")} - ) - - assert device_lookup == { - "remote_device_entry_id": { - ( - "binary_sensor", - "battery_charging", - ): "binary_sensor.remote_battery_charging" - }, - "light_device_entry_id": { - ( - "binary_sensor", - "battery_charging", - ): "binary_sensor.light_battery_charging", - ("sensor", "battery"): "sensor.light_battery", - }, - "vacuum_device_entry_id": { - ( - "binary_sensor", - "battery_charging", - ): "binary_sensor.vacuum_battery_charging", - ("sensor", "battery"): "sensor.vacuum_battery", - }, - } - - async def test_remove_device_removes_entities( hass: HomeAssistant, entity_registry: er.EntityRegistry, @@ -1765,8 +1699,7 @@ async def test_async_migrate_entry_delete_other( entity_registry.async_remove(entry2.entity_id) return None if entity_entry == entry2: - # We should not get here - pytest.fail() + pytest.fail("We should not get here") return None entries = set() @@ -1840,3 +1773,76 @@ async def test_entries_for_label(entity_registry: er.EntityRegistry) -> None: assert not er.async_entries_for_label(entity_registry, "unknown") assert not er.async_entries_for_label(entity_registry, "") + + +async def test_removing_categories(entity_registry: er.EntityRegistry) -> None: + """Make sure we can clear categories.""" + entry = entity_registry.async_get_or_create( + domain="light", + platform="hue", + unique_id="5678", + ) + entry = entity_registry.async_update_entity( + entry.entity_id, categories={"scope1": "id", "scope2": "id"} + ) + + entity_registry.async_clear_category_id("scope1", "id") + entry_cleared_scope1 = entity_registry.async_get(entry.entity_id) + + entity_registry.async_clear_category_id("scope2", "id") + entry_cleared_scope2 = entity_registry.async_get(entry.entity_id) + + assert entry_cleared_scope1 + assert entry_cleared_scope2 + assert entry != entry_cleared_scope1 + assert entry != entry_cleared_scope2 + assert entry_cleared_scope1 != entry_cleared_scope2 + assert entry.categories == {"scope1": "id", "scope2": "id"} + assert entry_cleared_scope1.categories == {"scope2": "id"} + assert not entry_cleared_scope2.categories + + +async def test_entries_for_category(entity_registry: er.EntityRegistry) -> None: + """Test getting entity entries by category.""" + entity_registry.async_get_or_create( + domain="light", + platform="hue", + unique_id="000", + ) + entry = entity_registry.async_get_or_create( + domain="light", + platform="hue", + unique_id="123", + ) + category_1 = entity_registry.async_update_entity( + entry.entity_id, categories={"scope1": "id"} + ) + entry = entity_registry.async_get_or_create( + domain="light", + platform="hue", + unique_id="456", + ) + category_2 = entity_registry.async_update_entity( + entry.entity_id, categories={"scope2": "id"} + ) + entry = entity_registry.async_get_or_create( + domain="light", + platform="hue", + unique_id="789", + ) + category_1_and_2 = entity_registry.async_update_entity( + entry.entity_id, categories={"scope1": "id", "scope2": "id"} + ) + + entries = er.async_entries_for_category(entity_registry, "scope1", "id") + assert len(entries) == 2 + assert entries == [category_1, category_1_and_2] + + entries = er.async_entries_for_category(entity_registry, "scope2", "id") + assert len(entries) == 2 + assert entries == [category_2, category_1_and_2] + + assert not er.async_entries_for_category(entity_registry, "unknown", "id") + assert not er.async_entries_for_category(entity_registry, "", "id") + assert not er.async_entries_for_category(entity_registry, "scope1", "unknown") + assert not er.async_entries_for_category(entity_registry, "scope1", "") diff --git a/tests/helpers/test_entity_values.py b/tests/helpers/test_entity_values.py index f32db73a788..d41a3718f0c 100644 --- a/tests/helpers/test_entity_values.py +++ b/tests/helpers/test_entity_values.py @@ -1,4 +1,5 @@ """Test the entity values helper.""" + from collections import OrderedDict from homeassistant.helpers.entity_values import EntityValues as EV diff --git a/tests/helpers/test_entityfilter.py b/tests/helpers/test_entityfilter.py index 48bc8110ec5..6ea986f6d20 100644 --- a/tests/helpers/test_entityfilter.py +++ b/tests/helpers/test_entityfilter.py @@ -1,4 +1,5 @@ """The tests for the EntityFilter component.""" + from homeassistant.helpers.entityfilter import ( FILTER_SCHEMA, INCLUDE_EXCLUDE_FILTER_SCHEMA, diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index 0c2c530eb9f..cf5051e657a 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -15,7 +15,7 @@ import pytest from homeassistant.const import MATCH_ALL import homeassistant.core as ha -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import Event, HomeAssistant, callback from homeassistant.exceptions import TemplateError from homeassistant.helpers.device_registry import EVENT_DEVICE_REGISTRY_UPDATED from homeassistant.helpers.entity_registry import EVENT_ENTITY_REGISTRY_UPDATED @@ -45,7 +45,6 @@ from homeassistant.helpers.event import ( track_point_in_utc_time, ) from homeassistant.helpers.template import Template, result_as_boolean -from homeassistant.helpers.typing import EventType from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -299,21 +298,21 @@ async def test_async_track_state_change_filtered(hass: HomeAssistant) -> None: multiple_entity_id_tracker = [] @ha.callback - def single_run_callback(event: EventType[EventStateChangedData]) -> None: + def single_run_callback(event: Event[EventStateChangedData]) -> None: old_state = event.data["old_state"] new_state = event.data["new_state"] single_entity_id_tracker.append((old_state, new_state)) @ha.callback - def multiple_run_callback(event: EventType[EventStateChangedData]) -> None: + def multiple_run_callback(event: Event[EventStateChangedData]) -> None: old_state = event.data["old_state"] new_state = event.data["new_state"] multiple_entity_id_tracker.append((old_state, new_state)) @ha.callback - def callback_that_throws(event: EventType[EventStateChangedData]) -> None: + def callback_that_throws(event: Event[EventStateChangedData]) -> None: raise ValueError track_single = async_track_state_change_filtered( @@ -435,25 +434,25 @@ async def test_async_track_state_change_event(hass: HomeAssistant) -> None: multiple_entity_id_tracker = [] @ha.callback - def single_run_callback(event: EventType[EventStateChangedData]) -> None: + def single_run_callback(event: Event[EventStateChangedData]) -> None: old_state = event.data["old_state"] new_state = event.data["new_state"] single_entity_id_tracker.append((old_state, new_state)) @ha.callback - def multiple_run_callback(event: EventType[EventStateChangedData]) -> None: + def multiple_run_callback(event: Event[EventStateChangedData]) -> None: old_state = event.data["old_state"] new_state = event.data["new_state"] multiple_entity_id_tracker.append((old_state, new_state)) @ha.callback - def callback_that_throws(event: EventType[EventStateChangedData]) -> None: + def callback_that_throws(event: Event[EventStateChangedData]) -> None: raise ValueError unsub_single = async_track_state_change_event( - hass, ["light.Bowl"], single_run_callback + hass, ["light.Bowl"], single_run_callback, job_type=ha.HassJobType.Callback ) unsub_multi = async_track_state_change_event( hass, ["light.Bowl", "switch.kitchen"], multiple_run_callback @@ -543,14 +542,14 @@ async def test_async_track_state_added_domain(hass: HomeAssistant) -> None: multiple_entity_id_tracker = [] @ha.callback - def single_run_callback(event: EventType[EventStateChangedData]) -> None: + def single_run_callback(event: Event[EventStateChangedData]) -> None: old_state = event.data["old_state"] new_state = event.data["new_state"] single_entity_id_tracker.append((old_state, new_state)) @ha.callback - def multiple_run_callback(event: EventType[EventStateChangedData]) -> None: + def multiple_run_callback(event: Event[EventStateChangedData]) -> None: old_state = event.data["old_state"] new_state = event.data["new_state"] @@ -560,7 +559,9 @@ async def test_async_track_state_added_domain(hass: HomeAssistant) -> None: def callback_that_throws(event): raise ValueError - unsub_single = async_track_state_added_domain(hass, "light", single_run_callback) + unsub_single = async_track_state_added_domain( + hass, "light", single_run_callback, job_type=ha.HassJobType.Callback + ) unsub_multi = async_track_state_added_domain( hass, ["light", "switch"], multiple_run_callback ) @@ -655,14 +656,14 @@ async def test_async_track_state_removed_domain(hass: HomeAssistant) -> None: multiple_entity_id_tracker = [] @ha.callback - def single_run_callback(event: EventType[EventStateChangedData]) -> None: + def single_run_callback(event: Event[EventStateChangedData]) -> None: old_state = event.data["old_state"] new_state = event.data["new_state"] single_entity_id_tracker.append((old_state, new_state)) @ha.callback - def multiple_run_callback(event: EventType[EventStateChangedData]) -> None: + def multiple_run_callback(event: Event[EventStateChangedData]) -> None: old_state = event.data["old_state"] new_state = event.data["new_state"] @@ -672,7 +673,9 @@ async def test_async_track_state_removed_domain(hass: HomeAssistant) -> None: def callback_that_throws(event): raise ValueError - unsub_single = async_track_state_removed_domain(hass, "light", single_run_callback) + unsub_single = async_track_state_removed_domain( + hass, "light", single_run_callback, job_type=ha.HassJobType.Callback + ) unsub_multi = async_track_state_removed_domain( hass, ["light", "switch"], multiple_run_callback ) @@ -737,14 +740,14 @@ async def test_async_track_state_removed_domain_match_all(hass: HomeAssistant) - match_all_entity_id_tracker = [] @ha.callback - def single_run_callback(event: EventType[EventStateChangedData]) -> None: + def single_run_callback(event: Event[EventStateChangedData]) -> None: old_state = event.data["old_state"] new_state = event.data["new_state"] single_entity_id_tracker.append((old_state, new_state)) @ha.callback - def match_all_run_callback(event: EventType[EventStateChangedData]) -> None: + def match_all_run_callback(event: Event[EventStateChangedData]) -> None: old_state = event.data["old_state"] new_state = event.data["new_state"] @@ -965,7 +968,7 @@ async def test_track_template_result(hass: HomeAssistant) -> None: ) def specific_run_callback( - event: EventType[EventStateChangedData] | None, + event: Event[EventStateChangedData] | None, updates: list[TrackTemplateResult], ) -> None: track_result = updates.pop() @@ -977,7 +980,7 @@ async def test_track_template_result(hass: HomeAssistant) -> None: @ha.callback def wildcard_run_callback( - event: EventType[EventStateChangedData] | None, + event: Event[EventStateChangedData] | None, updates: list[TrackTemplateResult], ) -> None: track_result = updates.pop() @@ -990,7 +993,7 @@ async def test_track_template_result(hass: HomeAssistant) -> None: ) async def wildercard_run_callback( - event: EventType[EventStateChangedData] | None, + event: Event[EventStateChangedData] | None, updates: list[TrackTemplateResult], ) -> None: track_result = updates.pop() @@ -1060,7 +1063,7 @@ async def test_track_template_result_none(hass: HomeAssistant) -> None: ) def specific_run_callback( - event: EventType[EventStateChangedData] | None, + event: Event[EventStateChangedData] | None, updates: list[TrackTemplateResult], ) -> None: track_result = updates.pop() @@ -1073,7 +1076,7 @@ async def test_track_template_result_none(hass: HomeAssistant) -> None: @ha.callback def wildcard_run_callback( - event: EventType[EventStateChangedData] | None, + event: Event[EventStateChangedData] | None, updates: list[TrackTemplateResult], ) -> None: track_result = updates.pop() @@ -1090,7 +1093,7 @@ async def test_track_template_result_none(hass: HomeAssistant) -> None: ) async def wildercard_run_callback( - event: EventType[EventStateChangedData] | None, + event: Event[EventStateChangedData] | None, updates: list[TrackTemplateResult], ) -> None: track_result = updates.pop() @@ -1140,7 +1143,7 @@ async def test_track_template_result_super_template(hass: HomeAssistant) -> None ) def specific_run_callback( - event: EventType[EventStateChangedData] | None, + event: Event[EventStateChangedData] | None, updates: list[TrackTemplateResult], ) -> None: for track_result in updates: @@ -1161,7 +1164,7 @@ async def test_track_template_result_super_template(hass: HomeAssistant) -> None @ha.callback def wildcard_run_callback( - event: EventType[EventStateChangedData] | None, + event: Event[EventStateChangedData] | None, updates: list[TrackTemplateResult], ) -> None: for track_result in updates: @@ -1183,7 +1186,7 @@ async def test_track_template_result_super_template(hass: HomeAssistant) -> None ) async def wildercard_run_callback( - event: EventType[EventStateChangedData] | None, + event: Event[EventStateChangedData] | None, updates: list[TrackTemplateResult], ) -> None: for track_result in updates: @@ -1299,7 +1302,7 @@ async def test_track_template_result_super_template_initially_false( await hass.async_block_till_done() def specific_run_callback( - event: EventType[EventStateChangedData] | None, + event: Event[EventStateChangedData] | None, updates: list[TrackTemplateResult], ) -> None: for track_result in updates: @@ -1320,7 +1323,7 @@ async def test_track_template_result_super_template_initially_false( @ha.callback def wildcard_run_callback( - event: EventType[EventStateChangedData] | None, + event: Event[EventStateChangedData] | None, updates: list[TrackTemplateResult], ) -> None: for track_result in updates: @@ -1342,7 +1345,7 @@ async def test_track_template_result_super_template_initially_false( ) async def wildercard_run_callback( - event: EventType[EventStateChangedData] | None, + event: Event[EventStateChangedData] | None, updates: list[TrackTemplateResult], ) -> None: for track_result in updates: @@ -1470,7 +1473,7 @@ async def test_track_template_result_super_template_2( return result_as_boolean(result) def specific_run_callback( - event: EventType[EventStateChangedData] | None, + event: Event[EventStateChangedData] | None, updates: list[TrackTemplateResult], ) -> None: for track_result in updates: @@ -1493,7 +1496,7 @@ async def test_track_template_result_super_template_2( @ha.callback def wildcard_run_callback( - event: EventType[EventStateChangedData] | None, + event: Event[EventStateChangedData] | None, updates: list[TrackTemplateResult], ) -> None: for track_result in updates: @@ -1517,7 +1520,7 @@ async def test_track_template_result_super_template_2( ) async def wildercard_run_callback( - event: EventType[EventStateChangedData] | None, + event: Event[EventStateChangedData] | None, updates: list[TrackTemplateResult], ) -> None: for track_result in updates: @@ -1625,7 +1628,7 @@ async def test_track_template_result_super_template_2_initially_false( return result_as_boolean(result) def specific_run_callback( - event: EventType[EventStateChangedData] | None, + event: Event[EventStateChangedData] | None, updates: list[TrackTemplateResult], ) -> None: for track_result in updates: @@ -1648,7 +1651,7 @@ async def test_track_template_result_super_template_2_initially_false( @ha.callback def wildcard_run_callback( - event: EventType[EventStateChangedData] | None, + event: Event[EventStateChangedData] | None, updates: list[TrackTemplateResult], ) -> None: for track_result in updates: @@ -1672,7 +1675,7 @@ async def test_track_template_result_super_template_2_initially_false( ) async def wildercard_run_callback( - event: EventType[EventStateChangedData] | None, + event: Event[EventStateChangedData] | None, updates: list[TrackTemplateResult], ) -> None: for track_result in updates: @@ -1755,7 +1758,7 @@ async def test_track_template_result_complex(hass: HomeAssistant) -> None: template_complex = Template(template_complex_str, hass) def specific_run_callback( - event: EventType[EventStateChangedData] | None, + event: Event[EventStateChangedData] | None, updates: list[TrackTemplateResult], ) -> None: specific_runs.append(updates.pop().result) @@ -1765,7 +1768,7 @@ async def test_track_template_result_complex(hass: HomeAssistant) -> None: info = async_track_template_result( hass, - [TrackTemplate(template_complex, None, timedelta(seconds=0))], + [TrackTemplate(template_complex, None, 0)], specific_run_callback, ) await hass.async_block_till_done() @@ -1911,7 +1914,7 @@ async def test_track_template_result_with_wildcard(hass: HomeAssistant) -> None: template_complex = Template(template_complex_str, hass) def specific_run_callback( - event: EventType[EventStateChangedData] | None, + event: Event[EventStateChangedData] | None, updates: list[TrackTemplateResult], ) -> None: specific_runs.append(updates.pop().result) @@ -1966,7 +1969,7 @@ async def test_track_template_result_with_group(hass: HomeAssistant) -> None: template_complex = Template(template_complex_str, hass) def specific_run_callback( - event: EventType[EventStateChangedData] | None, + event: Event[EventStateChangedData] | None, updates: list[TrackTemplateResult], ) -> None: specific_runs.append(updates.pop().result) @@ -2026,7 +2029,7 @@ async def test_track_template_result_and_conditional(hass: HomeAssistant) -> Non template = Template(template_str, hass) def specific_run_callback( - event: EventType[EventStateChangedData] | None, + event: Event[EventStateChangedData] | None, updates: list[TrackTemplateResult], ) -> None: specific_runs.append(updates.pop().result) @@ -2094,7 +2097,7 @@ async def test_track_template_result_and_conditional_upper_case( template = Template(template_str, hass) def specific_run_callback( - event: EventType[EventStateChangedData] | None, + event: Event[EventStateChangedData] | None, updates: list[TrackTemplateResult], ) -> None: specific_runs.append(updates.pop().result) @@ -2156,7 +2159,7 @@ async def test_track_template_result_iterator(hass: HomeAssistant) -> None: @ha.callback def iterator_callback( - event: EventType[EventStateChangedData] | None, + event: Event[EventStateChangedData] | None, updates: list[TrackTemplateResult], ) -> None: iterator_runs.append(updates.pop().result) @@ -2176,7 +2179,7 @@ async def test_track_template_result_iterator(hass: HomeAssistant) -> None: hass, ), None, - timedelta(seconds=0), + 0, ) ], iterator_callback, @@ -2192,7 +2195,7 @@ async def test_track_template_result_iterator(hass: HomeAssistant) -> None: @ha.callback def filter_callback( - event: EventType[EventStateChangedData] | None, + event: Event[EventStateChangedData] | None, updates: list[TrackTemplateResult], ) -> None: filter_runs.append(updates.pop().result) @@ -2207,7 +2210,7 @@ async def test_track_template_result_iterator(hass: HomeAssistant) -> None: hass, ), None, - timedelta(seconds=0), + 0, ) ], filter_callback, @@ -2245,7 +2248,7 @@ async def test_track_template_result_errors( @ha.callback def syntax_error_listener( - event: EventType[EventStateChangedData] | None, + event: Event[EventStateChangedData] | None, updates: list[TrackTemplateResult], ) -> None: track_result = updates.pop() @@ -2268,7 +2271,7 @@ async def test_track_template_result_errors( @ha.callback def not_exist_runs_error_listener( - event: EventType[EventStateChangedData] | None, + event: Event[EventStateChangedData] | None, updates: list[TrackTemplateResult], ) -> None: template_track = updates.pop() @@ -2336,7 +2339,7 @@ async def test_track_template_result_transient_errors( @ha.callback def sometimes_error_listener( - event: EventType[EventStateChangedData] | None, + event: Event[EventStateChangedData] | None, updates: list[TrackTemplateResult], ) -> None: track_result = updates.pop() @@ -2384,7 +2387,7 @@ async def test_static_string(hass: HomeAssistant) -> None: @ha.callback def refresh_listener( - event: EventType[EventStateChangedData] | None, + event: Event[EventStateChangedData] | None, updates: list[TrackTemplateResult], ) -> None: refresh_runs.append(updates.pop().result) @@ -2407,14 +2410,14 @@ async def test_track_template_rate_limit(hass: HomeAssistant) -> None: @ha.callback def refresh_listener( - event: EventType[EventStateChangedData] | None, + event: Event[EventStateChangedData] | None, updates: list[TrackTemplateResult], ) -> None: refresh_runs.append(updates.pop().result) info = async_track_template_result( hass, - [TrackTemplate(template_refresh, None, timedelta(seconds=0.1))], + [TrackTemplate(template_refresh, None, 0.1)], refresh_listener, ) await hass.async_block_till_done() @@ -2432,7 +2435,7 @@ async def test_track_template_rate_limit(hass: HomeAssistant) -> None: assert refresh_runs == [0, 1] next_time = dt_util.utcnow() + timedelta(seconds=0.125) with patch( - "homeassistant.helpers.ratelimit.dt_util.utcnow", return_value=next_time + "homeassistant.helpers.ratelimit.time.time", return_value=next_time.timestamp() ): async_fire_time_changed(hass, next_time) await hass.async_block_till_done() @@ -2445,7 +2448,7 @@ async def test_track_template_rate_limit(hass: HomeAssistant) -> None: assert refresh_runs == [0, 1, 2] next_time = dt_util.utcnow() + timedelta(seconds=0.125 * 2) with patch( - "homeassistant.helpers.ratelimit.dt_util.utcnow", return_value=next_time + "homeassistant.helpers.ratelimit.time.time", return_value=next_time.timestamp() ): async_fire_time_changed(hass, next_time) await hass.async_block_till_done() @@ -2469,7 +2472,7 @@ async def test_track_template_rate_limit_super(hass: HomeAssistant) -> None: @ha.callback def refresh_listener( - event: EventType[EventStateChangedData] | None, + event: Event[EventStateChangedData] | None, updates: list[TrackTemplateResult], ) -> None: for track_result in updates: @@ -2482,7 +2485,7 @@ async def test_track_template_rate_limit_super(hass: HomeAssistant) -> None: hass, [ TrackTemplate(template_availability, None), - TrackTemplate(template_refresh, None, timedelta(seconds=0.1)), + TrackTemplate(template_refresh, None, 0.1), ], refresh_listener, has_super_template=True, @@ -2505,7 +2508,7 @@ async def test_track_template_rate_limit_super(hass: HomeAssistant) -> None: assert refresh_runs == [0, 1] next_time = dt_util.utcnow() + timedelta(seconds=0.125) with patch( - "homeassistant.helpers.ratelimit.dt_util.utcnow", return_value=next_time + "homeassistant.helpers.ratelimit.time.time", return_value=next_time.timestamp() ): async_fire_time_changed(hass, next_time) await hass.async_block_till_done() @@ -2522,7 +2525,7 @@ async def test_track_template_rate_limit_super(hass: HomeAssistant) -> None: assert refresh_runs == [0, 1, 4] next_time = dt_util.utcnow() + timedelta(seconds=0.125 * 2) with patch( - "homeassistant.helpers.ratelimit.dt_util.utcnow", return_value=next_time + "homeassistant.helpers.ratelimit.time.time", return_value=next_time.timestamp() ): async_fire_time_changed(hass, next_time) await hass.async_block_till_done() @@ -2545,7 +2548,7 @@ async def test_track_template_rate_limit_super_2(hass: HomeAssistant) -> None: @ha.callback def refresh_listener( - event: EventType[EventStateChangedData] | None, + event: Event[EventStateChangedData] | None, updates: list[TrackTemplateResult], ) -> None: for track_result in updates: @@ -2557,8 +2560,8 @@ async def test_track_template_rate_limit_super_2(hass: HomeAssistant) -> None: info = async_track_template_result( hass, [ - TrackTemplate(template_availability, None, timedelta(seconds=0.1)), - TrackTemplate(template_refresh, None, timedelta(seconds=0.1)), + TrackTemplate(template_availability, None, 0.1), + TrackTemplate(template_refresh, None, 0.1), ], refresh_listener, has_super_template=True, @@ -2578,7 +2581,7 @@ async def test_track_template_rate_limit_super_2(hass: HomeAssistant) -> None: assert refresh_runs == [1] next_time = dt_util.utcnow() + timedelta(seconds=0.125) with patch( - "homeassistant.helpers.ratelimit.dt_util.utcnow", return_value=next_time + "homeassistant.helpers.ratelimit.time.time", return_value=next_time.timestamp() ): async_fire_time_changed(hass, next_time) await hass.async_block_till_done() @@ -2594,7 +2597,7 @@ async def test_track_template_rate_limit_super_2(hass: HomeAssistant) -> None: assert refresh_runs == [1] next_time = dt_util.utcnow() + timedelta(seconds=0.125 * 2) with patch( - "homeassistant.helpers.ratelimit.dt_util.utcnow", return_value=next_time + "homeassistant.helpers.ratelimit.time.time", return_value=next_time.timestamp() ): async_fire_time_changed(hass, next_time) await hass.async_block_till_done() @@ -2617,7 +2620,7 @@ async def test_track_template_rate_limit_super_3(hass: HomeAssistant) -> None: @ha.callback def refresh_listener( - event: EventType[EventStateChangedData] | None, + event: Event[EventStateChangedData] | None, updates: list[TrackTemplateResult], ) -> None: for track_result in updates: @@ -2629,7 +2632,7 @@ async def test_track_template_rate_limit_super_3(hass: HomeAssistant) -> None: info = async_track_template_result( hass, [ - TrackTemplate(template_availability, None, timedelta(seconds=0.1)), + TrackTemplate(template_availability, None, 0.1), TrackTemplate(template_refresh, None), ], refresh_listener, @@ -2651,7 +2654,7 @@ async def test_track_template_rate_limit_super_3(hass: HomeAssistant) -> None: assert refresh_runs == [1, 2] next_time = dt_util.utcnow() + timedelta(seconds=0.125) with patch( - "homeassistant.helpers.ratelimit.dt_util.utcnow", return_value=next_time + "homeassistant.helpers.ratelimit.time.time", return_value=next_time.timestamp() ): async_fire_time_changed(hass, next_time) await hass.async_block_till_done() @@ -2668,7 +2671,7 @@ async def test_track_template_rate_limit_super_3(hass: HomeAssistant) -> None: assert refresh_runs == [1, 2] next_time = dt_util.utcnow() + timedelta(seconds=0.125 * 2) with patch( - "homeassistant.helpers.ratelimit.dt_util.utcnow", return_value=next_time + "homeassistant.helpers.ratelimit.time.time", return_value=next_time.timestamp() ): async_fire_time_changed(hass, next_time) await hass.async_block_till_done() @@ -2691,14 +2694,14 @@ async def test_track_template_rate_limit_suppress_listener(hass: HomeAssistant) @ha.callback def refresh_listener( - event: EventType[EventStateChangedData] | None, + event: Event[EventStateChangedData] | None, updates: list[TrackTemplateResult], ) -> None: refresh_runs.append(updates.pop().result) info = async_track_template_result( hass, - [TrackTemplate(template_refresh, None, timedelta(seconds=0.1))], + [TrackTemplate(template_refresh, None, 0.1)], refresh_listener, ) await hass.async_block_till_done() @@ -2730,7 +2733,7 @@ async def test_track_template_rate_limit_suppress_listener(hass: HomeAssistant) assert refresh_runs == [0, 1] next_time = dt_util.utcnow() + timedelta(seconds=0.125) with patch( - "homeassistant.helpers.ratelimit.dt_util.utcnow", return_value=next_time + "homeassistant.helpers.ratelimit.time.time", return_value=next_time.timestamp() ): async_fire_time_changed(hass, next_time) await hass.async_block_till_done() @@ -2757,7 +2760,7 @@ async def test_track_template_rate_limit_suppress_listener(hass: HomeAssistant) } next_time = dt_util.utcnow() + timedelta(seconds=0.125 * 2) with patch( - "homeassistant.helpers.ratelimit.dt_util.utcnow", return_value=next_time + "homeassistant.helpers.ratelimit.time.time", return_value=next_time.timestamp() ): async_fire_time_changed(hass, next_time) await hass.async_block_till_done() @@ -2791,14 +2794,14 @@ async def test_track_template_rate_limit_five(hass: HomeAssistant) -> None: @ha.callback def refresh_listener( - event: EventType[EventStateChangedData] | None, + event: Event[EventStateChangedData] | None, updates: list[TrackTemplateResult], ) -> None: refresh_runs.append(updates.pop().result) info = async_track_template_result( hass, - [TrackTemplate(template_refresh, None, timedelta(seconds=5))], + [TrackTemplate(template_refresh, None, 5)], refresh_listener, ) await hass.async_block_till_done() @@ -2830,7 +2833,7 @@ async def test_track_template_has_default_rate_limit(hass: HomeAssistant) -> Non @ha.callback def refresh_listener( - event: EventType[EventStateChangedData] | None, + event: Event[EventStateChangedData] | None, updates: list[TrackTemplateResult], ) -> None: refresh_runs.append(updates.pop().result) @@ -2874,7 +2877,7 @@ async def test_track_template_unavailable_states_has_default_rate_limit( @ha.callback def refresh_listener( - event: EventType[EventStateChangedData] | None, + event: Event[EventStateChangedData] | None, updates: list[TrackTemplateResult], ) -> None: refresh_runs.append(updates.pop().result) @@ -2918,14 +2921,14 @@ async def test_specifically_referenced_entity_is_not_rate_limited( @ha.callback def refresh_listener( - event: EventType[EventStateChangedData] | None, + event: Event[EventStateChangedData] | None, updates: list[TrackTemplateResult], ) -> None: refresh_runs.append(updates.pop().result) info = async_track_template_result( hass, - [TrackTemplate(template_refresh, None, timedelta(seconds=5))], + [TrackTemplate(template_refresh, None, 5)], refresh_listener, ) await hass.async_block_till_done() @@ -2964,7 +2967,7 @@ async def test_track_two_templates_with_different_rate_limits( @ha.callback def refresh_listener( - event: EventType[EventStateChangedData] | None, + event: Event[EventStateChangedData] | None, updates: list[TrackTemplateResult], ) -> None: for update in updates: @@ -2973,8 +2976,8 @@ async def test_track_two_templates_with_different_rate_limits( info = async_track_template_result( hass, [ - TrackTemplate(template_one, None, timedelta(seconds=0.1)), - TrackTemplate(template_five, None, timedelta(seconds=5)), + TrackTemplate(template_one, None, 0.1), + TrackTemplate(template_five, None, 5), ], refresh_listener, ) @@ -2998,7 +3001,7 @@ async def test_track_two_templates_with_different_rate_limits( assert refresh_runs[template_five] == [0, 1] next_time = dt_util.utcnow() + timedelta(seconds=0.125 * 1) with patch( - "homeassistant.helpers.ratelimit.dt_util.utcnow", return_value=next_time + "homeassistant.helpers.ratelimit.time.time", return_value=next_time.timestamp() ): async_fire_time_changed(hass, next_time) await hass.async_block_till_done() @@ -3028,7 +3031,7 @@ async def test_string(hass: HomeAssistant) -> None: @ha.callback def refresh_listener( - event: EventType[EventStateChangedData] | None, + event: Event[EventStateChangedData] | None, updates: list[TrackTemplateResult], ) -> None: refresh_runs.append(updates.pop().result) @@ -3051,7 +3054,7 @@ async def test_track_template_result_refresh_cancel(hass: HomeAssistant) -> None @ha.callback def refresh_listener( - event: EventType[EventStateChangedData] | None, + event: Event[EventStateChangedData] | None, updates: list[TrackTemplateResult], ) -> None: refresh_runs.append(updates.pop().result) @@ -3116,7 +3119,7 @@ async def test_async_track_template_result_multiple_templates( @ha.callback def refresh_listener( - event: EventType[EventStateChangedData] | None, + event: Event[EventStateChangedData] | None, updates: list[TrackTemplateResult], ) -> None: refresh_runs.append(updates) @@ -3180,7 +3183,7 @@ async def test_async_track_template_result_multiple_templates_mixing_domain( @ha.callback def refresh_listener( - event: EventType[EventStateChangedData] | None, + event: Event[EventStateChangedData] | None, updates: list[TrackTemplateResult], ) -> None: refresh_runs.append(updates) @@ -3191,7 +3194,7 @@ async def test_async_track_template_result_multiple_templates_mixing_domain( TrackTemplate(template_1, None), TrackTemplate(template_2, None), TrackTemplate(template_3, None), - TrackTemplate(template_4, None, timedelta(seconds=0)), + TrackTemplate(template_4, None, 0), ], refresh_listener, ) @@ -3247,7 +3250,7 @@ async def test_track_template_with_time(hass: HomeAssistant) -> None: template_complex = Template("{{ states.switch.test.state and now() }}", hass) def specific_run_callback( - event: EventType[EventStateChangedData] | None, + event: Event[EventStateChangedData] | None, updates: list[TrackTemplateResult], ) -> None: specific_runs.append(updates.pop().result) @@ -3280,7 +3283,7 @@ async def test_track_template_with_time_default(hass: HomeAssistant) -> None: template_complex = Template("{{ now() }}", hass) def specific_run_callback( - event: EventType[EventStateChangedData] | None, + event: Event[EventStateChangedData] | None, updates: list[TrackTemplateResult], ) -> None: specific_runs.append(updates.pop().result) @@ -3334,7 +3337,7 @@ async def test_track_template_with_time_that_leaves_scope( ) def specific_run_callback( - event: EventType[EventStateChangedData] | None, + event: Event[EventStateChangedData] | None, updates: list[TrackTemplateResult], ) -> None: specific_runs.append(updates.pop().result) @@ -3405,7 +3408,7 @@ async def test_async_track_template_result_multiple_templates_mixing_listeners( @ha.callback def refresh_listener( - event: EventType[EventStateChangedData] | None, + event: Event[EventStateChangedData] | None, updates: list[TrackTemplateResult], ) -> None: refresh_runs.append(updates) @@ -4433,14 +4436,14 @@ async def test_track_state_change_event_chain_multple_entity( tracker_unsub = [] @ha.callback - def chained_single_run_callback(event: EventType[EventStateChangedData]) -> None: + def chained_single_run_callback(event: Event[EventStateChangedData]) -> None: old_state = event.data["old_state"] new_state = event.data["new_state"] chained_tracker_called.append((old_state, new_state)) @ha.callback - def single_run_callback(event: EventType[EventStateChangedData]) -> None: + def single_run_callback(event: Event[EventStateChangedData]) -> None: old_state = event.data["old_state"] new_state = event.data["new_state"] @@ -4487,14 +4490,14 @@ async def test_track_state_change_event_chain_single_entity( tracker_unsub = [] @ha.callback - def chained_single_run_callback(event: EventType[EventStateChangedData]) -> None: + def chained_single_run_callback(event: Event[EventStateChangedData]) -> None: old_state = event.data["old_state"] new_state = event.data["new_state"] chained_tracker_called.append((old_state, new_state)) @ha.callback - def single_run_callback(event: EventType[EventStateChangedData]) -> None: + def single_run_callback(event: Event[EventStateChangedData]) -> None: old_state = event.data["old_state"] new_state = event.data["new_state"] @@ -4602,7 +4605,9 @@ async def test_async_track_entity_registry_updated_event(hass: HomeAssistant) -> def run_callback(event): event_data.append(event.data) - unsub1 = async_track_entity_registry_updated_event(hass, entity_id, run_callback) + unsub1 = async_track_entity_registry_updated_event( + hass, entity_id, run_callback, job_type=ha.HassJobType.Callback + ) unsub2 = async_track_entity_registry_updated_event( hass, new_entity_id, run_callback ) @@ -4721,7 +4726,10 @@ async def test_async_track_device_registry_updated_event(hass: HomeAssistant) -> hass, device_id, single_device_id_callback ) unsub2 = async_track_device_registry_updated_event( - hass, [device_id, device_id2], multiple_device_id_callback + hass, + [device_id, device_id2], + multiple_device_id_callback, + job_type=ha.HassJobType.Callback, ) hass.bus.async_fire( EVENT_DEVICE_REGISTRY_UPDATED, {"action": "create", "device_id": device_id} diff --git a/tests/helpers/test_floor_registry.py b/tests/helpers/test_floor_registry.py index feb5ce505ac..faa9eb131a1 100644 --- a/tests/helpers/test_floor_registry.py +++ b/tests/helpers/test_floor_registry.py @@ -1,4 +1,5 @@ """Tests for the floor registry.""" + import re from typing import Any @@ -133,7 +134,7 @@ async def test_update_floor( assert floor.name == "First floor" assert floor.icon is None assert floor.aliases == set() - assert floor.level == 0 + assert floor.level is None updated_floor = floor_registry.async_update( floor.floor_id, diff --git a/tests/helpers/test_frame.py b/tests/helpers/test_frame.py index fa495e9dbc9..fe215264f59 100644 --- a/tests/helpers/test_frame.py +++ b/tests/helpers/test_frame.py @@ -7,6 +7,8 @@ import pytest from homeassistant.core import HomeAssistant from homeassistant.helpers import frame +from tests.common import extract_stack_to_frame + async def test_extract_frame_integration( caplog: pytest.LogCaptureFixture, mock_integration_frame: Mock @@ -15,7 +17,7 @@ async def test_extract_frame_integration( integration_frame = frame.get_integration_frame() assert integration_frame == frame.IntegrationFrame( custom_integration=False, - frame=mock_integration_frame, + _frame=mock_integration_frame, integration="hue", module=None, relative_filename="homeassistant/components/hue/light.py", @@ -40,7 +42,7 @@ async def test_extract_frame_resolve_module( assert integration_frame == frame.IntegrationFrame( custom_integration=True, - frame=ANY, + _frame=ANY, integration="test_integration_frame", module="custom_components.test_integration_frame", relative_filename="custom_components/test_integration_frame/__init__.py", @@ -68,25 +70,27 @@ async def test_extract_frame_integration_with_excluded_integration( line="self.light.is_on", ) with patch( - "homeassistant.helpers.frame.extract_stack", - return_value=[ - Mock( - filename="/home/dev/homeassistant/core.py", - lineno="23", - line="do_something()", - ), - correct_frame, - Mock( - filename="/home/dev/homeassistant/components/zeroconf/usage.py", - lineno="23", - line="self.light.is_on", - ), - Mock( - filename="/home/dev/mdns/lights.py", - lineno="2", - line="something()", - ), - ], + "homeassistant.helpers.frame.get_current_frame", + return_value=extract_stack_to_frame( + [ + Mock( + filename="/home/dev/homeassistant/core.py", + lineno="23", + line="do_something()", + ), + correct_frame, + Mock( + filename="/home/dev/homeassistant/components/zeroconf/usage.py", + lineno="23", + line="self.light.is_on", + ), + Mock( + filename="/home/dev/mdns/lights.py", + lineno="2", + line="something()", + ), + ] + ), ): integration_frame = frame.get_integration_frame( exclude_integrations={"zeroconf"} @@ -94,7 +98,7 @@ async def test_extract_frame_integration_with_excluded_integration( assert integration_frame == frame.IntegrationFrame( custom_integration=False, - frame=correct_frame, + _frame=correct_frame, integration="mdns", module=None, relative_filename="homeassistant/components/mdns/light.py", @@ -103,21 +107,26 @@ async def test_extract_frame_integration_with_excluded_integration( async def test_extract_frame_no_integration(caplog: pytest.LogCaptureFixture) -> None: """Test extracting the current frame without integration context.""" - with patch( - "homeassistant.helpers.frame.extract_stack", - return_value=[ - Mock( - filename="/home/paulus/homeassistant/core.py", - lineno="23", - line="do_something()", + with ( + patch( + "homeassistant.helpers.frame.get_current_frame", + return_value=extract_stack_to_frame( + [ + Mock( + filename="/home/paulus/homeassistant/core.py", + lineno="23", + line="do_something()", + ), + Mock( + filename="/home/paulus/aiohue/lights.py", + lineno="2", + line="something()", + ), + ] ), - Mock( - filename="/home/paulus/aiohue/lights.py", - lineno="2", - line="something()", - ), - ], - ), pytest.raises(frame.MissingIntegrationFrame): + ), + pytest.raises(frame.MissingIntegrationFrame), + ): frame.get_integration_frame() @@ -126,19 +135,21 @@ async def test_get_integration_logger_no_integration( ) -> None: """Test getting fallback logger without integration context.""" with patch( - "homeassistant.helpers.frame.extract_stack", - return_value=[ - Mock( - filename="/home/paulus/homeassistant/core.py", - lineno="23", - line="do_something()", - ), - Mock( - filename="/home/paulus/aiohue/lights.py", - lineno="2", - line="something()", - ), - ], + "homeassistant.helpers.frame.get_current_frame", + return_value=extract_stack_to_frame( + [ + Mock( + filename="/home/paulus/homeassistant/core.py", + lineno="23", + line="do_something()", + ), + Mock( + filename="/home/paulus/aiohue/lights.py", + lineno="2", + line="something()", + ), + ] + ), ): logger = frame.get_integration_logger(__name__) diff --git a/tests/helpers/test_group.py b/tests/helpers/test_group.py index b1300009607..26f4ffda256 100644 --- a/tests/helpers/test_group.py +++ b/tests/helpers/test_group.py @@ -1,6 +1,5 @@ """Test the group helper.""" - from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant from homeassistant.helpers import group diff --git a/tests/helpers/test_httpx_client.py b/tests/helpers/test_httpx_client.py index 693c45cc73a..60bdbe607e3 100644 --- a/tests/helpers/test_httpx_client.py +++ b/tests/helpers/test_httpx_client.py @@ -1,4 +1,5 @@ """Test the httpx client helper.""" + from unittest.mock import Mock, patch import httpx @@ -7,7 +8,7 @@ import pytest from homeassistant.core import EVENT_HOMEASSISTANT_CLOSE, HomeAssistant import homeassistant.helpers.httpx_client as client -from tests.common import MockModule, mock_integration +from tests.common import MockModule, extract_stack_to_frame, mock_integration async def test_get_async_client_with_ssl(hass: HomeAssistant) -> None: @@ -103,25 +104,33 @@ async def test_warning_close_session_integration( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test log warning message when closing the session from integration context.""" - with patch( - "homeassistant.helpers.frame.extract_stack", - return_value=[ - Mock( - filename="/home/paulus/homeassistant/core.py", - lineno="23", - line="do_something()", + with ( + patch( + "homeassistant.helpers.frame.linecache.getline", + return_value="await session.aclose()", + ), + patch( + "homeassistant.helpers.frame.get_current_frame", + return_value=extract_stack_to_frame( + [ + Mock( + filename="/home/paulus/homeassistant/core.py", + lineno="23", + line="do_something()", + ), + Mock( + filename="/home/paulus/homeassistant/components/hue/light.py", + lineno="23", + line="await session.aclose()", + ), + Mock( + filename="/home/paulus/aiohue/lights.py", + lineno="2", + line="something()", + ), + ] ), - Mock( - filename="/home/paulus/homeassistant/components/hue/light.py", - lineno="23", - line="await session.aclose()", - ), - Mock( - filename="/home/paulus/aiohue/lights.py", - lineno="2", - line="something()", - ), - ], + ), ): httpx_session = client.get_async_client(hass) await httpx_session.aclose() @@ -140,25 +149,33 @@ async def test_warning_close_session_custom( ) -> None: """Test log warning message when closing the session from custom context.""" mock_integration(hass, MockModule("hue"), built_in=False) - with patch( - "homeassistant.helpers.frame.extract_stack", - return_value=[ - Mock( - filename="/home/paulus/homeassistant/core.py", - lineno="23", - line="do_something()", + with ( + patch( + "homeassistant.helpers.frame.linecache.getline", + return_value="await session.aclose()", + ), + patch( + "homeassistant.helpers.frame.get_current_frame", + return_value=extract_stack_to_frame( + [ + Mock( + filename="/home/paulus/homeassistant/core.py", + lineno="23", + line="do_something()", + ), + Mock( + filename="/home/paulus/config/custom_components/hue/light.py", + lineno="23", + line="await session.aclose()", + ), + Mock( + filename="/home/paulus/aiohue/lights.py", + lineno="2", + line="something()", + ), + ] ), - Mock( - filename="/home/paulus/config/custom_components/hue/light.py", - lineno="23", - line="await session.aclose()", - ), - Mock( - filename="/home/paulus/aiohue/lights.py", - lineno="2", - line="something()", - ), - ], + ), ): httpx_session = client.get_async_client(hass) await httpx_session.aclose() diff --git a/tests/helpers/test_icon.py b/tests/helpers/test_icon.py index cf329100d75..e986a07d7d5 100644 --- a/tests/helpers/test_icon.py +++ b/tests/helpers/test_icon.py @@ -161,15 +161,19 @@ async def test_get_icons_while_loading_components(hass: HomeAssistant) -> None: load_count += 1 return {"component1": {"entity": {"climate": {"test": {"icon": "mdi:home"}}}}} - with patch( - "homeassistant.helpers.icon._component_icons_path", - return_value="choochoo.json", - ), patch( - "homeassistant.helpers.icon._load_icons_files", - mock_load_icons_files, - ), patch( - "homeassistant.helpers.icon.async_get_integrations", - return_value={"component1": integration}, + with ( + patch( + "homeassistant.helpers.icon._component_icons_path", + return_value="choochoo.json", + ), + patch( + "homeassistant.helpers.icon._load_icons_files", + mock_load_icons_files, + ), + patch( + "homeassistant.helpers.icon.async_get_integrations", + return_value={"component1": integration}, + ), ): times = 5 all_icons = [await icon.async_get_icons(hass, "entity") for _ in range(times)] diff --git a/tests/helpers/test_importlib.py b/tests/helpers/test_importlib.py new file mode 100644 index 00000000000..5683dd5cf94 --- /dev/null +++ b/tests/helpers/test_importlib.py @@ -0,0 +1,87 @@ +"""Tests for the importlib helper.""" + +import time +from typing import Any +from unittest.mock import patch + +import pytest + +from homeassistant.core import HomeAssistant +from homeassistant.helpers import importlib + +from tests.common import MockModule + + +async def test_async_import_module(hass: HomeAssistant) -> None: + """Test importing a module.""" + mock_module = MockModule() + with patch( + "homeassistant.helpers.importlib.importlib.import_module", + return_value=mock_module, + ): + module = await importlib.async_import_module(hass, "test.module") + + assert module is mock_module + + +async def test_async_import_module_on_helper(hass: HomeAssistant) -> None: + """Test importing the importlib helper.""" + module = await importlib.async_import_module( + hass, "homeassistant.helpers.importlib" + ) + assert module is importlib + module = await importlib.async_import_module( + hass, "homeassistant.helpers.importlib" + ) + assert module is importlib + + +async def test_async_import_module_failures(hass: HomeAssistant) -> None: + """Test importing a module fails.""" + with ( + patch( + "homeassistant.helpers.importlib.importlib.import_module", + side_effect=ImportError, + ), + pytest.raises(ImportError), + ): + await importlib.async_import_module(hass, "test.module") + + mock_module = MockModule() + # The failure should be cached + with ( + pytest.raises(ImportError), + patch( + "homeassistant.helpers.importlib.importlib.import_module", + return_value=mock_module, + ), + ): + await importlib.async_import_module(hass, "test.module") + + +@pytest.mark.parametrize("eager_start", [True, False]) +async def test_async_import_module_concurrency( + hass: HomeAssistant, eager_start: bool +) -> None: + """Test importing a module with concurrency.""" + mock_module = MockModule() + + def _mock_import(name: str, *args: Any) -> MockModule: + time.sleep(0.1) + return mock_module + + with patch( + "homeassistant.helpers.importlib.importlib.import_module", + _mock_import, + ): + task1 = hass.async_create_task( + importlib.async_import_module(hass, "test.module"), eager_start=eager_start + ) + task2 = hass.async_create_task( + importlib.async_import_module(hass, "test.module"), eager_start=eager_start + ) + module1 = await task1 + module2 = await task2 + + assert module1 is mock_module + assert module2 is mock_module diff --git a/tests/helpers/test_instance_id.py b/tests/helpers/test_instance_id.py index 8fb6bfd8d7e..ad2e4626af5 100644 --- a/tests/helpers/test_instance_id.py +++ b/tests/helpers/test_instance_id.py @@ -1,4 +1,5 @@ """Tests for instance ID helper.""" + from json import JSONDecodeError from typing import Any from unittest.mock import patch @@ -40,9 +41,11 @@ async def test_get_id_migrate( hass: HomeAssistant, hass_storage: dict[str, Any] ) -> None: """Migrate existing file.""" - with patch( - "homeassistant.util.json.load_json", return_value={"uuid": "1234"} - ), patch("os.path.isfile", return_value=True), patch("os.remove") as mock_remove: + with ( + patch("homeassistant.util.json.load_json", return_value={"uuid": "1234"}), + patch("os.path.isfile", return_value=True), + patch("os.remove") as mock_remove, + ): uuid = await instance_id.async_get(hass) assert uuid == "1234" @@ -58,10 +61,14 @@ async def test_get_id_migrate_fail( hass: HomeAssistant, hass_storage: dict[str, Any], caplog: pytest.LogCaptureFixture ) -> None: """Migrate existing file with error.""" - with patch( - "homeassistant.util.json.load_json", - side_effect=JSONDecodeError("test_error", "test", 1), - ), patch("os.path.isfile", return_value=True), patch("os.remove") as mock_remove: + with ( + patch( + "homeassistant.util.json.load_json", + side_effect=JSONDecodeError("test_error", "test", 1), + ), + patch("os.path.isfile", return_value=True), + patch("os.remove") as mock_remove, + ): uuid = await instance_id.async_get(hass) assert uuid is not None diff --git a/tests/helpers/test_integration_platform.py b/tests/helpers/test_integration_platform.py index 29bda99c9c6..81eb1f2fd38 100644 --- a/tests/helpers/test_integration_platform.py +++ b/tests/helpers/test_integration_platform.py @@ -1,10 +1,12 @@ """Test integration platform helpers.""" + from collections.abc import Callable from types import ModuleType -from unittest.mock import Mock +from unittest.mock import Mock, patch import pytest +from homeassistant import loader from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.integration_platform import ( @@ -15,6 +17,44 @@ from homeassistant.setup import ATTR_COMPONENT, EVENT_COMPONENT_LOADED from tests.common import mock_platform +async def test_process_integration_platforms_with_wait(hass: HomeAssistant) -> None: + """Test processing integrations.""" + loaded_platform = Mock() + mock_platform(hass, "loaded.platform_to_check", loaded_platform) + hass.config.components.add("loaded") + + event_platform = Mock() + mock_platform(hass, "event.platform_to_check", event_platform) + + processed = [] + + async def _process_platform(hass, domain, platform): + """Process platform.""" + processed.append((domain, platform)) + + await async_process_integration_platforms( + hass, "platform_to_check", _process_platform, wait_for_platforms=True + ) + # No block till done here, we want to make sure it waits for the platform + + assert len(processed) == 1 + assert processed[0][0] == "loaded" + assert processed[0][1] == loaded_platform + + hass.bus.async_fire(EVENT_COMPONENT_LOADED, {ATTR_COMPONENT: "event"}) + await hass.async_block_till_done() + + assert len(processed) == 2 + assert processed[1][0] == "event" + assert processed[1][1] == event_platform + + hass.bus.async_fire(EVENT_COMPONENT_LOADED, {ATTR_COMPONENT: "event"}) + await hass.async_block_till_done() + + # Firing again should not check again + assert len(processed) == 2 + + async def test_process_integration_platforms(hass: HomeAssistant) -> None: """Test processing integrations.""" loaded_platform = Mock() @@ -33,6 +73,7 @@ async def test_process_integration_platforms(hass: HomeAssistant) -> None: await async_process_integration_platforms( hass, "platform_to_check", _process_platform ) + await hass.async_block_till_done() assert len(processed) == 1 assert processed[0][0] == "loaded" @@ -52,6 +93,87 @@ async def test_process_integration_platforms(hass: HomeAssistant) -> None: assert len(processed) == 2 +async def test_process_integration_platforms_import_fails( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test processing integrations when one fails to import.""" + loaded_platform = Mock() + mock_platform(hass, "loaded.platform_to_check", loaded_platform) + hass.config.components.add("loaded") + + event_platform = Mock() + mock_platform(hass, "event.platform_to_check", event_platform) + + processed = [] + + async def _process_platform(hass, domain, platform): + """Process platform.""" + processed.append((domain, platform)) + + loaded_integration = await loader.async_get_integration(hass, "loaded") + with patch.object( + loaded_integration, "async_get_platform", side_effect=ImportError + ): + await async_process_integration_platforms( + hass, "platform_to_check", _process_platform + ) + await hass.async_block_till_done() + + assert len(processed) == 0 + assert "Unexpected error importing platform_to_check for loaded" in caplog.text + + hass.bus.async_fire(EVENT_COMPONENT_LOADED, {ATTR_COMPONENT: "event"}) + await hass.async_block_till_done() + + assert len(processed) == 1 + assert processed[0][0] == "event" + assert processed[0][1] == event_platform + + hass.bus.async_fire(EVENT_COMPONENT_LOADED, {ATTR_COMPONENT: "event"}) + await hass.async_block_till_done() + + # Firing again should not check again + assert len(processed) == 1 + + +async def test_process_integration_platforms_import_fails_after_registered( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test processing integrations when one fails to import.""" + loaded_platform = Mock() + mock_platform(hass, "loaded.platform_to_check", loaded_platform) + hass.config.components.add("loaded") + + event_platform = Mock() + mock_platform(hass, "event.platform_to_check", event_platform) + + processed = [] + + async def _process_platform(hass, domain, platform): + """Process platform.""" + processed.append((domain, platform)) + + await async_process_integration_platforms( + hass, "platform_to_check", _process_platform + ) + await hass.async_block_till_done() + + assert len(processed) == 1 + assert processed[0][0] == "loaded" + assert processed[0][1] == loaded_platform + + event_integration = await loader.async_get_integration(hass, "event") + with ( + patch.object(event_integration, "async_get_platforms", side_effect=ImportError), + patch.object(event_integration, "get_platform_cached", return_value=None), + ): + hass.bus.async_fire(EVENT_COMPONENT_LOADED, {ATTR_COMPONENT: "event"}) + await hass.async_block_till_done() + + assert len(processed) == 1 + assert "Unexpected error importing integration platforms for event" in caplog.text + + @callback def _process_platform_callback( hass: HomeAssistant, domain: str, platform: ModuleType @@ -69,7 +191,7 @@ async def _process_platform_coro( @pytest.mark.no_fail_on_log_exception @pytest.mark.parametrize( - "process_platform", (_process_platform_callback, _process_platform_coro) + "process_platform", [_process_platform_callback, _process_platform_coro] ) async def test_process_integration_platforms_non_compliant( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, process_platform: Callable @@ -87,6 +209,7 @@ async def test_process_integration_platforms_non_compliant( await async_process_integration_platforms( hass, "platform_to_check", process_platform ) + await hass.async_block_till_done() assert len(processed) == 0 assert "Exception in " in caplog.text @@ -125,9 +248,11 @@ async def test_broken_integration( await async_process_integration_platforms( hass, "platform_to_check", _process_platform ) + await hass.async_block_till_done() + # This should never actually happen as the component cannot be + # in hass.config.components without a loaded manifest assert len(processed) == 0 - assert "Error importing integration loaded for platform_to_check" in caplog.text async def test_process_integration_platforms_no_integrations( @@ -146,5 +271,6 @@ async def test_process_integration_platforms_no_integrations( await async_process_integration_platforms( hass, "platform_to_check", _process_platform ) + await hass.async_block_till_done() assert len(processed) == 0 diff --git a/tests/helpers/test_intent.py b/tests/helpers/test_intent.py index 1bc01c28cf2..d77eb698205 100644 --- a/tests/helpers/test_intent.py +++ b/tests/helpers/test_intent.py @@ -15,6 +15,7 @@ from homeassistant.helpers import ( config_validation as cv, device_registry as dr, entity_registry as er, + floor_registry as fr, intent, ) from homeassistant.setup import async_setup_component @@ -34,12 +35,25 @@ async def test_async_match_states( hass: HomeAssistant, area_registry: ar.AreaRegistry, entity_registry: er.EntityRegistry, + floor_registry: fr.FloorRegistry, ) -> None: """Test async_match_state helper.""" area_kitchen = area_registry.async_get_or_create("kitchen") - area_registry.async_update(area_kitchen.id, aliases={"food room"}) + area_kitchen = area_registry.async_update(area_kitchen.id, aliases={"food room"}) area_bedroom = area_registry.async_get_or_create("bedroom") + # Kitchen is on the first floor + floor_1 = floor_registry.async_create("first floor", aliases={"ground floor"}) + area_kitchen = area_registry.async_update( + area_kitchen.id, floor_id=floor_1.floor_id + ) + + # Bedroom is on the second floor + floor_2 = floor_registry.async_create("second floor") + area_bedroom = area_registry.async_update( + area_bedroom.id, floor_id=floor_2.floor_id + ) + state1 = State( "light.kitchen", "on", attributes={ATTR_FRIENDLY_NAME: "kitchen light"} ) @@ -94,6 +108,13 @@ async def test_async_match_states( ) ) + # Invalid area + assert not list( + intent.async_match_states( + hass, area_name="invalid area", states=[state1, state2] + ) + ) + # Domain + area assert list( intent.async_match_states( @@ -111,6 +132,35 @@ async def test_async_match_states( ) ) == [state2] + # Floor + assert list( + intent.async_match_states( + hass, floor_name="first floor", states=[state1, state2] + ) + ) == [state1] + + assert list( + intent.async_match_states( + # Check alias + hass, + floor_name="ground floor", + states=[state1, state2], + ) + ) == [state1] + + assert list( + intent.async_match_states( + hass, floor_name="second floor", states=[state1, state2] + ) + ) == [state2] + + # Invalid floor + assert not list( + intent.async_match_states( + hass, floor_name="invalid floor", states=[state1, state2] + ) + ) + async def test_match_device_area( hass: HomeAssistant, @@ -300,3 +350,27 @@ async def test_validate_then_run_in_background(hass: HomeAssistant) -> None: assert len(calls) == 1 assert calls[0].data == {"entity_id": "light.kitchen"} + + +async def test_invalid_area_floor_names(hass: HomeAssistant) -> None: + """Test that we throw an intent handle error with invalid area/floor names.""" + handler = intent.ServiceIntentHandler( + "TestType", "light", "turn_on", "Turned {} on" + ) + intent.async_register(hass, handler) + + with pytest.raises(intent.IntentHandleError): + await intent.async_handle( + hass, + "test", + "TestType", + slots={"area": {"value": "invalid area"}}, + ) + + with pytest.raises(intent.IntentHandleError): + await intent.async_handle( + hass, + "test", + "TestType", + slots={"floor": {"value": "invalid floor"}}, + ) diff --git a/tests/helpers/test_issue_registry.py b/tests/helpers/test_issue_registry.py index 88f97a65421..66fc9662f75 100644 --- a/tests/helpers/test_issue_registry.py +++ b/tests/helpers/test_issue_registry.py @@ -1,4 +1,5 @@ """Test the repairs websocket API.""" + from typing import Any import pytest diff --git a/tests/helpers/test_json.py b/tests/helpers/test_json.py index 2106a397baf..57269963164 100644 --- a/tests/helpers/test_json.py +++ b/tests/helpers/test_json.py @@ -1,4 +1,5 @@ """Test Home Assistant remote methods and classes.""" + import datetime from functools import partial import json @@ -33,7 +34,7 @@ TEST_JSON_A = {"a": 1, "B": "two"} TEST_JSON_B = {"a": "one", "B": 2} -@pytest.mark.parametrize("encoder", (DefaultHASSJSONEncoder, ExtendedJSONEncoder)) +@pytest.mark.parametrize("encoder", [DefaultHASSJSONEncoder, ExtendedJSONEncoder]) def test_json_encoder(hass: HomeAssistant, encoder: type[json.JSONEncoder]) -> None: """Test the JSON encoders.""" ha_json_enc = encoder() diff --git a/tests/helpers/test_label_registry.py b/tests/helpers/test_label_registry.py index 13f3837737b..785919b25c0 100644 --- a/tests/helpers/test_label_registry.py +++ b/tests/helpers/test_label_registry.py @@ -1,4 +1,5 @@ """Tests for the Label Registry.""" + import re from typing import Any diff --git a/tests/helpers/test_location.py b/tests/helpers/test_location.py index 435df26b94c..4c14433188c 100644 --- a/tests/helpers/test_location.py +++ b/tests/helpers/test_location.py @@ -1,4 +1,5 @@ """Tests Home Assistant location helpers.""" + from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_LATITUDE, ATTR_LONGITUDE from homeassistant.core import HomeAssistant, State from homeassistant.helpers import location diff --git a/tests/helpers/test_network.py b/tests/helpers/test_network.py index b8fbfc9346b..caffebf094e 100644 --- a/tests/helpers/test_network.py +++ b/tests/helpers/test_network.py @@ -1,4 +1,5 @@ """Test network helper.""" + from unittest.mock import Mock, patch import pytest @@ -74,10 +75,13 @@ async def test_get_url_internal(hass: HomeAssistant) -> None: with pytest.raises(NoURLAvailableError): _get_internal_url(hass, require_current_request=True, require_ssl=True) - with patch( - "homeassistant.helpers.network._get_request_host", - return_value="no_match.example.local", - ), pytest.raises(NoURLAvailableError): + with ( + patch( + "homeassistant.helpers.network._get_request_host", + return_value="no_match.example.local", + ), + pytest.raises(NoURLAvailableError), + ): _get_internal_url(hass, require_current_request=True) # Test with internal URL: https://example.local:8123 @@ -274,10 +278,13 @@ async def test_get_url_external(hass: HomeAssistant) -> None: with pytest.raises(NoURLAvailableError): _get_external_url(hass, require_current_request=True, require_ssl=True) - with patch( - "homeassistant.helpers.network._get_request_host", - return_value="no_match.example.com", - ), pytest.raises(NoURLAvailableError): + with ( + patch( + "homeassistant.helpers.network._get_request_host", + return_value="no_match.example.com", + ), + pytest.raises(NoURLAvailableError), + ): _get_external_url(hass, require_current_request=True) # Test with external URL: http://example.com:80/ @@ -361,9 +368,8 @@ async def test_get_cloud_url(hass: HomeAssistant) -> None: assert hass.config.external_url is None hass.config.components.add("cloud") - with patch.object( - hass.components.cloud, - "async_remote_ui_url", + with patch( + "homeassistant.components.cloud.async_remote_ui_url", return_value="https://example.nabu.casa", ): assert _get_cloud_url(hass) == "https://example.nabu.casa" @@ -380,17 +386,22 @@ async def test_get_cloud_url(hass: HomeAssistant) -> None: == "https://example.nabu.casa" ) - with patch( - "homeassistant.helpers.network._get_request_host", - return_value="no_match.nabu.casa", - ), pytest.raises(NoURLAvailableError): + with ( + patch( + "homeassistant.helpers.network._get_request_host", + return_value="no_match.nabu.casa", + ), + pytest.raises(NoURLAvailableError), + ): _get_cloud_url(hass, require_current_request=True) - with patch.object( - hass.components.cloud, - "async_remote_ui_url", - side_effect=cloud.CloudNotAvailable, - ), pytest.raises(NoURLAvailableError): + with ( + patch( + "homeassistant.components.cloud.async_remote_ui_url", + side_effect=cloud.CloudNotAvailable, + ), + pytest.raises(NoURLAvailableError), + ): _get_cloud_url(hass) @@ -409,9 +420,8 @@ async def test_get_external_url_cloud_fallback(hass: HomeAssistant) -> None: # Add Cloud to the previous test hass.config.components.add("cloud") - with patch.object( - hass.components.cloud, - "async_remote_ui_url", + with patch( + "homeassistant.components.cloud.async_remote_ui_url", return_value="https://example.nabu.casa", ): assert _get_external_url(hass, allow_cloud=False) == "http://1.1.1.1:8123" @@ -435,9 +445,8 @@ async def test_get_external_url_cloud_fallback(hass: HomeAssistant) -> None: # Add Cloud to the previous test hass.config.components.add("cloud") - with patch.object( - hass.components.cloud, - "async_remote_ui_url", + with patch( + "homeassistant.components.cloud.async_remote_ui_url", return_value="https://example.nabu.casa", ): assert _get_external_url(hass, allow_cloud=False) == "https://example.com" @@ -509,9 +518,13 @@ async def test_get_url(hass: HomeAssistant) -> None: with pytest.raises(NoURLAvailableError): get_url(hass, require_current_request=True) - with patch( - "homeassistant.helpers.network._get_request_host", return_value="example.com" - ), patch("homeassistant.components.http.current_request"): + with ( + patch( + "homeassistant.helpers.network._get_request_host", + return_value="example.com", + ), + patch("homeassistant.components.http.current_request"), + ): assert get_url(hass, require_current_request=True) == "https://example.com" assert ( get_url(hass, require_current_request=True, require_ssl=True) @@ -521,9 +534,13 @@ async def test_get_url(hass: HomeAssistant) -> None: with pytest.raises(NoURLAvailableError): get_url(hass, require_current_request=True, allow_external=False) - with patch( - "homeassistant.helpers.network._get_request_host", return_value="example.local" - ), patch("homeassistant.components.http.current_request"): + with ( + patch( + "homeassistant.helpers.network._get_request_host", + return_value="example.local", + ), + patch("homeassistant.components.http.current_request"), + ): assert get_url(hass, require_current_request=True) == "http://example.local" with pytest.raises(NoURLAvailableError): @@ -532,10 +549,13 @@ async def test_get_url(hass: HomeAssistant) -> None: with pytest.raises(NoURLAvailableError): get_url(hass, require_current_request=True, require_ssl=True) - with patch( - "homeassistant.helpers.network._get_request_host", - return_value="no_match.example.com", - ), pytest.raises(NoURLAvailableError): + with ( + patch( + "homeassistant.helpers.network._get_request_host", + return_value="no_match.example.com", + ), + pytest.raises(NoURLAvailableError), + ): _get_internal_url(hass, require_current_request=True) # Test allow_ip defaults when SSL specified @@ -565,6 +585,11 @@ async def test_get_request_host(hass: HomeAssistant) -> None: assert _get_request_host() == "example.com" +@patch("homeassistant.components.hassio.is_hassio", Mock(return_value=True)) +@patch( + "homeassistant.components.hassio.get_host_info", + Mock(return_value={"hostname": "homeassistant"}), +) async def test_get_current_request_url_with_known_host( hass: HomeAssistant, current_request ) -> None: @@ -595,10 +620,6 @@ async def test_get_current_request_url_with_known_host( # Ensure hostname from Supervisor is accepted transparently mock_component(hass, "hassio") - hass.components.hassio.is_hassio = Mock(return_value=True) - hass.components.hassio.get_host_info = Mock( - return_value={"hostname": "homeassistant"} - ) with patch( "homeassistant.helpers.network._get_request_host", @@ -617,12 +638,24 @@ async def test_get_current_request_url_with_known_host( get_url(hass, require_current_request=True) == "http://homeassistant:8123" ) - with patch( - "homeassistant.helpers.network._get_request_host", return_value="unknown.local" - ), pytest.raises(NoURLAvailableError): + with ( + patch( + "homeassistant.helpers.network._get_request_host", + return_value="unknown.local", + ), + pytest.raises(NoURLAvailableError), + ): get_url(hass, require_current_request=True) +@patch( + "homeassistant.components.hassio.is_hassio", + Mock(return_value={"hostname": "homeassistant"}), +) +@patch( + "homeassistant.components.hassio.get_host_info", + Mock(return_value={"hostname": "hellohost"}), +) async def test_is_internal_request(hass: HomeAssistant, mock_current_request) -> None: """Test if accessing an instance on its internal URL.""" # Test with internal URL: http://example.local:8123 @@ -662,16 +695,9 @@ async def test_is_internal_request(hass: HomeAssistant, mock_current_request) -> assert is_internal_request(hass), mock_current_request.return_value.url # Test for matching against HassOS hostname - with patch.object( - hass.components.hassio, "is_hassio", return_value=True - ), patch.object( - hass.components.hassio, - "get_host_info", - return_value={"hostname": "hellohost"}, - ): - for allowed in ("hellohost", "hellohost.local"): - mock_current_request.return_value = Mock(url=f"http://{allowed}:8123") - assert is_internal_request(hass), mock_current_request.return_value.url + for allowed in ("hellohost", "hellohost.local"): + mock_current_request.return_value = Mock(url=f"http://{allowed}:8123") + assert is_internal_request(hass), mock_current_request.return_value.url async def test_is_hass_url(hass: HomeAssistant) -> None: @@ -707,9 +733,8 @@ async def test_is_hass_url(hass: HomeAssistant) -> None: assert is_hass_url(hass, "http://example.com:443") is False assert is_hass_url(hass, "http://example.com") is False - with patch.object( - hass.components.cloud, - "async_remote_ui_url", + with patch( + "homeassistant.components.cloud.async_remote_ui_url", return_value="https://example.nabu.casa", ): assert is_hass_url(hass, "https://example.nabu.casa") is False diff --git a/tests/helpers/test_normalized_name_base_registry.py b/tests/helpers/test_normalized_name_base_registry.py new file mode 100644 index 00000000000..495d147340f --- /dev/null +++ b/tests/helpers/test_normalized_name_base_registry.py @@ -0,0 +1,68 @@ +"""Tests for the normalized name base registry helper.""" + +import pytest + +from homeassistant.helpers.normalized_name_base_registry import ( + NormalizedNameBaseRegistryEntry, + NormalizedNameBaseRegistryItems, + normalize_name, +) + + +@pytest.fixture +def registry_items(): + """Fixture for registry items.""" + return NormalizedNameBaseRegistryItems[NormalizedNameBaseRegistryEntry]() + + +def test_normalize_name(): + """Test normalize_name.""" + assert normalize_name("Hello World") == "helloworld" + assert normalize_name("HELLO WORLD") == "helloworld" + assert normalize_name(" Hello World ") == "helloworld" + + +def test_registry_items( + registry_items: NormalizedNameBaseRegistryItems[NormalizedNameBaseRegistryEntry], +): + """Test registry items.""" + entry = NormalizedNameBaseRegistryEntry( + name="Hello World", normalized_name="helloworld" + ) + registry_items["key"] = entry + assert registry_items["key"] == entry + assert list(registry_items.values()) == [entry] + assert registry_items.get_by_name("Hello World") == entry + + # test update entry + entry2 = NormalizedNameBaseRegistryEntry( + name="Hello World 2", normalized_name="helloworld2" + ) + registry_items["key"] = entry2 + assert registry_items["key"] == entry2 + assert list(registry_items.values()) == [entry2] + assert registry_items.get_by_name("Hello World 2") == entry2 + + # test delete entry + del registry_items["key"] + assert "key" not in registry_items + assert list(registry_items.values()) == [] + + +def test_key_already_in_use( + registry_items: NormalizedNameBaseRegistryItems[NormalizedNameBaseRegistryEntry], +): + """Test key already in use.""" + entry = NormalizedNameBaseRegistryEntry( + name="Hello World", normalized_name="helloworld" + ) + registry_items["key"] = entry + + # should raise ValueError if we update a + # key with a entry with the same normalized name + with pytest.raises(ValueError): + entry = NormalizedNameBaseRegistryEntry( + name="Hello World 2", normalized_name="helloworld2" + ) + registry_items["key2"] = entry + registry_items["key"] = entry diff --git a/tests/helpers/test_ratelimit.py b/tests/helpers/test_ratelimit.py index 39e83953c45..a87493b1731 100644 --- a/tests/helpers/test_ratelimit.py +++ b/tests/helpers/test_ratelimit.py @@ -1,10 +1,10 @@ """Tests for ratelimit.""" + import asyncio -from datetime import timedelta +import time from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import ratelimit -from homeassistant.util import dt as dt_util async def test_hit(hass: HomeAssistant) -> None: @@ -18,12 +18,10 @@ async def test_hit(hass: HomeAssistant) -> None: refresh_called = True rate_limiter = ratelimit.KeyedRateLimit(hass) - rate_limiter.async_triggered("key1", dt_util.utcnow()) + rate_limiter.async_triggered("key1", time.time()) assert ( - rate_limiter.async_schedule_action( - "key1", timedelta(seconds=0.001), dt_util.utcnow(), _refresh - ) + rate_limiter.async_schedule_action("key1", 0.001, time.time(), _refresh) is not None ) @@ -35,10 +33,7 @@ async def test_hit(hass: HomeAssistant) -> None: assert refresh_called assert ( - rate_limiter.async_schedule_action( - "key2", timedelta(seconds=0.001), dt_util.utcnow(), _refresh - ) - is None + rate_limiter.async_schedule_action("key2", 0.001, time.time(), _refresh) is None ) rate_limiter.async_remove() @@ -55,19 +50,13 @@ async def test_miss(hass: HomeAssistant) -> None: rate_limiter = ratelimit.KeyedRateLimit(hass) assert ( - rate_limiter.async_schedule_action( - "key1", timedelta(seconds=0.1), dt_util.utcnow(), _refresh - ) - is None + rate_limiter.async_schedule_action("key1", 0.1, time.time(), _refresh) is None ) assert not refresh_called assert not rate_limiter.async_has_timer("key1") assert ( - rate_limiter.async_schedule_action( - "key1", timedelta(seconds=0.1), dt_util.utcnow(), _refresh - ) - is None + rate_limiter.async_schedule_action("key1", 0.1, time.time(), _refresh) is None ) assert not refresh_called assert not rate_limiter.async_has_timer("key1") @@ -85,20 +74,18 @@ async def test_no_limit(hass: HomeAssistant) -> None: refresh_called = True rate_limiter = ratelimit.KeyedRateLimit(hass) - rate_limiter.async_triggered("key1", dt_util.utcnow()) + rate_limiter.async_triggered("key1", time.time()) assert ( - rate_limiter.async_schedule_action("key1", None, dt_util.utcnow(), _refresh) - is None + rate_limiter.async_schedule_action("key1", None, time.time(), _refresh) is None ) assert not refresh_called assert not rate_limiter.async_has_timer("key1") - rate_limiter.async_triggered("key1", dt_util.utcnow()) + rate_limiter.async_triggered("key1", time.time()) assert ( - rate_limiter.async_schedule_action("key1", None, dt_util.utcnow(), _refresh) - is None + rate_limiter.async_schedule_action("key1", None, time.time(), _refresh) is None ) assert not refresh_called assert not rate_limiter.async_has_timer("key1") diff --git a/tests/helpers/test_redact.py b/tests/helpers/test_redact.py index 73461012907..bdc06d77b49 100644 --- a/tests/helpers/test_redact.py +++ b/tests/helpers/test_redact.py @@ -1,4 +1,5 @@ """Test the data redation helper.""" + from homeassistant.helpers.redact import REDACTED, async_redact_data, partial_redact diff --git a/tests/helpers/test_registry.py b/tests/helpers/test_registry.py new file mode 100644 index 00000000000..46b04b05fe3 --- /dev/null +++ b/tests/helpers/test_registry.py @@ -0,0 +1,71 @@ +"""Tests for the registry.""" + +from typing import Any + +from freezegun.api import FrozenDateTimeFactory +import pytest + +from homeassistant.core import CoreState, HomeAssistant +from homeassistant.helpers import storage +from homeassistant.helpers.registry import SAVE_DELAY, SAVE_DELAY_LONG, BaseRegistry + +from tests.common import async_fire_time_changed + + +class SampleRegistry(BaseRegistry): + """Class to hold a registry of X.""" + + def __init__(self, hass: HomeAssistant) -> None: + """Initialize the registry.""" + self.hass = hass + self._store = storage.Store(hass, 1, "test") + self.save_calls = 0 + + def _data_to_save(self) -> None: + """Return data of registry to save.""" + self.save_calls += 1 + return None + + +@pytest.mark.parametrize( + "long_delay_state", + [ + CoreState.not_running, + CoreState.starting, + CoreState.stopped, + CoreState.final_write, + ], +) +async def test_async_schedule_save( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + long_delay_state: CoreState, + hass_storage: dict[str, Any], +) -> None: + """Test saving the registry. + + If CoreState is not running, it should save with long delay. + + Storage will always save at final write if there is a + write pending so we should not schedule a save in that case. + """ + registry = SampleRegistry(hass) + hass.set_state(long_delay_state) + + registry.async_schedule_save() + freezer.tick(SAVE_DELAY) + async_fire_time_changed(hass) + await hass.async_block_till_done() + assert registry.save_calls == 0 + + freezer.tick(SAVE_DELAY_LONG) + async_fire_time_changed(hass) + await hass.async_block_till_done() + assert registry.save_calls == 1 + + hass.set_state(CoreState.running) + registry.async_schedule_save() + freezer.tick(SAVE_DELAY) + async_fire_time_changed(hass) + await hass.async_block_till_done() + assert registry.save_calls == 2 diff --git a/tests/helpers/test_reload.py b/tests/helpers/test_reload.py index 4425ce00ce1..66d4efe9a55 100644 --- a/tests/helpers/test_reload.py +++ b/tests/helpers/test_reload.py @@ -1,4 +1,5 @@ """Tests for the reload helper.""" + import logging from unittest.mock import AsyncMock, Mock, patch @@ -140,10 +141,13 @@ async def test_setup_reload_service_when_async_process_component_config_fails( await async_setup_reload_service(hass, PLATFORM, [DOMAIN]) yaml_path = get_fixture_path("helpers/reload_configuration.yaml") - with patch.object(config, "YAML_CONFIG_FILE", yaml_path), patch.object( - config, - "async_process_component_config", - return_value=config.IntegrationConfigInfo(None, []), + with ( + patch.object(config, "YAML_CONFIG_FILE", yaml_path), + patch.object( + config, + "async_process_component_config", + return_value=config.IntegrationConfigInfo(None, []), + ), ): await hass.services.async_call( PLATFORM, @@ -249,10 +253,14 @@ async def test_async_integration_failing_on_reload(hass: HomeAssistant) -> None: mock_integration(hass, MockModule(DOMAIN)) yaml_path = get_fixture_path(f"helpers/{DOMAIN}_configuration.yaml") - with patch.object(config, "YAML_CONFIG_FILE", yaml_path), patch( - "homeassistant.config.async_process_component_config", - side_effect=HomeAssistantError(), - ), pytest.raises(HomeAssistantError): + with ( + patch.object(config, "YAML_CONFIG_FILE", yaml_path), + patch( + "homeassistant.config.async_process_component_config", + side_effect=HomeAssistantError(), + ), + pytest.raises(HomeAssistantError), + ): # Test fetching yaml config does raise when the raise_on_failure option is set await async_integration_yaml_config(hass, DOMAIN, raise_on_failure=True) @@ -262,7 +270,8 @@ async def test_async_integration_missing_yaml_config(hass: HomeAssistant) -> Non mock_integration(hass, MockModule(DOMAIN)) yaml_path = get_fixture_path("helpers/does_not_exist_configuration.yaml") - with pytest.raises(FileNotFoundError), patch.object( - config, "YAML_CONFIG_FILE", yaml_path + with ( + pytest.raises(FileNotFoundError), + patch.object(config, "YAML_CONFIG_FILE", yaml_path), ): await async_integration_yaml_config(hass, DOMAIN) diff --git a/tests/helpers/test_restore_state.py b/tests/helpers/test_restore_state.py index 2a01439ccbd..4f8c51c5397 100644 --- a/tests/helpers/test_restore_state.py +++ b/tests/helpers/test_restore_state.py @@ -1,4 +1,5 @@ """The tests for the Restore component.""" + from collections.abc import Coroutine from datetime import datetime, timedelta import logging diff --git a/tests/helpers/test_schema_config_entry_flow.py b/tests/helpers/test_schema_config_entry_flow.py index 58f6a261aef..4db56a91c11 100644 --- a/tests/helpers/test_schema_config_entry_flow.py +++ b/tests/helpers/test_schema_config_entry_flow.py @@ -1,4 +1,5 @@ """Tests for the schema based data entry flows.""" + from __future__ import annotations from collections.abc import Mapping @@ -103,7 +104,7 @@ async def test_name(hass: HomeAssistant, entity_registry: er.EntityRegistry) -> assert wrapped_entity_config_entry_title(hass, entry.id) == "Custom Name" -@pytest.mark.parametrize("marker", (vol.Required, vol.Optional)) +@pytest.mark.parametrize("marker", [vol.Required, vol.Optional]) async def test_config_flow_advanced_option( hass: HomeAssistant, manager: data_entry_flow.FlowManager, marker ) -> None: @@ -198,7 +199,7 @@ async def test_config_flow_advanced_option( assert isinstance(option, str) -@pytest.mark.parametrize("marker", (vol.Required, vol.Optional)) +@pytest.mark.parametrize("marker", [vol.Required, vol.Optional]) async def test_options_flow_advanced_option( hass: HomeAssistant, manager: data_entry_flow.FlowManager, marker ) -> None: diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index 3dfe8fdbad9..86fb84eb582 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -1,4 +1,5 @@ """The tests for the Script component.""" + import asyncio from contextlib import contextmanager from datetime import timedelta @@ -671,6 +672,31 @@ async def test_delay_basic(hass: HomeAssistant) -> None: ) +async def test_empty_delay(hass: HomeAssistant) -> None: + """Test an empty delay.""" + delay_alias = "delay step" + sequence = cv.SCRIPT_SCHEMA({"delay": {"seconds": 0}, "alias": delay_alias}) + script_obj = script.Script(hass, sequence, "Test Name", "test_domain") + delay_started_flag = async_watch_for_action(script_obj, delay_alias) + + try: + await script_obj.async_run(context=Context()) + await asyncio.wait_for(delay_started_flag.wait(), 1) + except (AssertionError, TimeoutError): + await script_obj.async_stop() + raise + else: + await hass.async_block_till_done() + assert not script_obj.is_running + assert script_obj.last_action is None + + assert_action_trace( + { + "0": [{"result": {"delay": 0.0, "done": True}}], + } + ) + + async def test_multiple_runs_delay(hass: HomeAssistant) -> None: """Test multiple runs with delay in script.""" event = "test_event" @@ -3440,7 +3466,8 @@ async def test_parallel_loop( script_obj = script.Script(hass, sequence, "Test Name", "test_domain") hass.async_create_task( - script_obj.async_run(MappingProxyType({"what": "world"}), Context()) + script_obj.async_run(MappingProxyType({"what": "world"}), Context()), + eager_start=True, ) await hass.async_block_till_done() @@ -3455,7 +3482,6 @@ async def test_parallel_loop( expected_trace = { "0": [{"variables": {"what": "world"}}], "0/parallel/0/sequence/0": [{}], - "0/parallel/1/sequence/0": [{}], "0/parallel/0/sequence/0/repeat/sequence/0": [ { "variables": { @@ -3491,6 +3517,7 @@ async def test_parallel_loop( "result": {"event": "loop1", "event_data": {"hello1": "loop1_c"}}, }, ], + "0/parallel/1/sequence/0": [{}], "0/parallel/1/sequence/0/repeat/sequence/0": [ { "variables": { @@ -3534,6 +3561,7 @@ async def test_parallel_error( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test parallel action failure handling.""" + await async_setup_component(hass, "homeassistant", {}) events = async_capture_events(hass, "test_event") sequence = cv.SCRIPT_SCHEMA( { @@ -3550,10 +3578,10 @@ async def test_parallel_error( assert len(events) == 0 expected_trace = { - "0": [{"error": "Service epic.failure not found."}], + "0": [{"error": "Service epic.failure not found"}], "0/parallel/0/sequence/0": [ { - "error": "Service epic.failure not found.", + "error": "Service epic.failure not found", "result": { "params": { "domain": "epic", @@ -3587,6 +3615,7 @@ async def test_last_triggered(hass: HomeAssistant) -> None: async def test_propagate_error_service_not_found(hass: HomeAssistant) -> None: """Test that a script aborts when a service is not found.""" + await async_setup_component(hass, "homeassistant", {}) event = "test_event" events = async_capture_events(hass, event) sequence = cv.SCRIPT_SCHEMA([{"service": "test.script"}, {"event": event}]) @@ -3601,7 +3630,7 @@ async def test_propagate_error_service_not_found(hass: HomeAssistant) -> None: expected_trace = { "0": [ { - "error": "Service test.script not found.", + "error": "Service test.script not found", "result": { "params": { "domain": "test", @@ -3693,6 +3722,211 @@ async def test_propagate_error_service_exception(hass: HomeAssistant) -> None: assert_action_trace(expected_trace, expected_script_execution="error") +async def test_referenced_labels(hass: HomeAssistant) -> None: + """Test referenced labels.""" + script_obj = script.Script( + hass, + cv.SCRIPT_SCHEMA( + [ + { + "service": "test.script", + "data": {"label_id": "label_service_not_list"}, + }, + { + "service": "test.script", + "data": { + "label_id": ["label_service_list_1", "label_service_list_2"] + }, + }, + { + "service": "test.script", + "data": {"label_id": "{{ 'label_service_template' }}"}, + }, + { + "service": "test.script", + "target": {"label_id": "label_in_target"}, + }, + { + "service": "test.script", + "data_template": {"label_id": "label_in_data_template"}, + }, + {"service": "test.script", "data": {"without": "label_id"}}, + { + "choose": [ + { + "conditions": "{{ true == false }}", + "sequence": [ + { + "service": "test.script", + "data": {"label_id": "label_choice_1_seq"}, + } + ], + }, + { + "conditions": "{{ true == false }}", + "sequence": [ + { + "service": "test.script", + "data": {"label_id": "label_choice_2_seq"}, + } + ], + }, + ], + "default": [ + { + "service": "test.script", + "data": {"label_id": "label_default_seq"}, + } + ], + }, + {"event": "test_event"}, + {"delay": "{{ delay_period }}"}, + { + "if": [], + "then": [ + { + "service": "test.script", + "data": {"label_id": "label_if_then"}, + } + ], + "else": [ + { + "service": "test.script", + "data": {"label_id": "label_if_else"}, + } + ], + }, + { + "parallel": [ + { + "service": "test.script", + "data": {"label_id": "label_parallel"}, + } + ], + }, + ] + ), + "Test Name", + "test_domain", + ) + assert script_obj.referenced_labels == { + "label_choice_1_seq", + "label_choice_2_seq", + "label_default_seq", + "label_in_data_template", + "label_in_target", + "label_service_list_1", + "label_service_list_2", + "label_service_not_list", + "label_if_then", + "label_if_else", + "label_parallel", + } + # Test we cache results. + assert script_obj.referenced_labels is script_obj.referenced_labels + + +async def test_referenced_floors(hass: HomeAssistant) -> None: + """Test referenced floors.""" + script_obj = script.Script( + hass, + cv.SCRIPT_SCHEMA( + [ + { + "service": "test.script", + "data": {"floor_id": "floor_service_not_list"}, + }, + { + "service": "test.script", + "data": {"floor_id": ["floor_service_list"]}, + }, + { + "service": "test.script", + "data": {"floor_id": "{{ 'floor_service_template' }}"}, + }, + { + "service": "test.script", + "target": {"floor_id": "floor_in_target"}, + }, + { + "service": "test.script", + "data_template": {"floor_id": "floor_in_data_template"}, + }, + {"service": "test.script", "data": {"without": "floor_id"}}, + { + "choose": [ + { + "conditions": "{{ true == false }}", + "sequence": [ + { + "service": "test.script", + "data": {"floor_id": "floor_choice_1_seq"}, + } + ], + }, + { + "conditions": "{{ true == false }}", + "sequence": [ + { + "service": "test.script", + "data": {"floor_id": "floor_choice_2_seq"}, + } + ], + }, + ], + "default": [ + { + "service": "test.script", + "data": {"floor_id": "floor_default_seq"}, + } + ], + }, + {"event": "test_event"}, + {"delay": "{{ delay_period }}"}, + { + "if": [], + "then": [ + { + "service": "test.script", + "data": {"floor_id": "floor_if_then"}, + } + ], + "else": [ + { + "service": "test.script", + "data": {"floor_id": "floor_if_else"}, + } + ], + }, + { + "parallel": [ + { + "service": "test.script", + "data": {"floor_id": "floor_parallel"}, + } + ], + }, + ] + ), + "Test Name", + "test_domain", + ) + assert script_obj.referenced_floors == { + "floor_choice_1_seq", + "floor_choice_2_seq", + "floor_default_seq", + "floor_in_data_template", + "floor_in_target", + "floor_service_list", + "floor_service_not_list", + "floor_if_then", + "floor_if_else", + "floor_parallel", + } + # Test we cache results. + assert script_obj.referenced_floors is script_obj.referenced_floors + + async def test_referenced_areas(hass: HomeAssistant) -> None: """Test referenced areas.""" script_obj = script.Script( @@ -4117,7 +4351,9 @@ async def test_max_exceeded( ) hass.states.async_set("switch.test", "on") for _ in range(max_runs + 1): - hass.async_create_task(script_obj.async_run(context=Context())) + hass.async_create_task( + script_obj.async_run(context=Context()), eager_start=True + ) hass.states.async_set("switch.test", "off") await hass.async_block_till_done() if max_exceeded is None: @@ -4343,7 +4579,7 @@ async def test_script_mode_queued_cancel(hass: HomeAssistant) -> None: await task2 assert script_obj.is_running - assert script_obj.runs == 1 + assert script_obj.runs == 2 with pytest.raises(asyncio.CancelledError): task1.cancel() @@ -4718,10 +4954,13 @@ async def test_validate_action_config( assert key in expected_templates or key in script.STATIC_VALIDATION_ACTION_TYPES # Verify we raise if we don't know the action type - with patch( - "homeassistant.helpers.config_validation.determine_script_action", - return_value="non-existing", - ), pytest.raises(ValueError): + with ( + patch( + "homeassistant.helpers.config_validation.determine_script_action", + return_value="non-existing", + ), + pytest.raises(ValueError), + ): await script.async_validate_action_config(hass, {}) # Verify each action can validate @@ -5016,10 +5255,10 @@ async def test_stop_action( @pytest.mark.parametrize( ("error", "error_dict", "logmsg", "script_execution"), - ( + [ (True, {"error": "In the name of love"}, "Error", "aborted"), (False, {}, "Stop", "finished"), - ), + ], ) async def test_stop_action_subscript( hass: HomeAssistant, @@ -5210,6 +5449,7 @@ async def test_continue_on_error_with_stop(hass: HomeAssistant) -> None: async def test_continue_on_error_automation_issue(hass: HomeAssistant) -> None: """Test continue on error doesn't block action automation errors.""" + await async_setup_component(hass, "homeassistant", {}) sequence = cv.SCRIPT_SCHEMA( [ { @@ -5227,7 +5467,7 @@ async def test_continue_on_error_automation_issue(hass: HomeAssistant) -> None: { "0": [ { - "error": "Service service.not_found not found.", + "error": "Service service.not_found not found", "result": { "params": { "domain": "service", @@ -5246,6 +5486,7 @@ async def test_continue_on_error_automation_issue(hass: HomeAssistant) -> None: async def test_continue_on_error_unknown_error(hass: HomeAssistant) -> None: """Test continue on error doesn't block unknown errors from e.g., libraries.""" + await async_setup_component(hass, "homeassistant", {}) class MyLibraryError(Exception): """My custom library error.""" @@ -5620,3 +5861,20 @@ async def test_conversation_response_not_set_subscript_if( "1/if/condition/0": [{"result": {"result": var == 1, "entities": []}}], } assert_action_trace(expected_trace) + + +async def test_stopping_run_before_starting( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test stopping a script run before its started.""" + sequence = cv.SCRIPT_SCHEMA( + [ + {"wait_template": "{{ 'on' == 'off' }}"}, + ] + ) + script_obj = script.Script(hass, sequence, "Test Name", "test_domain") + # Tested directly because we are checking for a race in the internals + # where the script is stopped before it is started. Previously this + # would hang indefinitely. + run = script._ScriptRun(hass, script_obj, {}, None, True) + await run.async_stop() diff --git a/tests/helpers/test_script_variables.py b/tests/helpers/test_script_variables.py index 99aae08587e..ca942acdf66 100644 --- a/tests/helpers/test_script_variables.py +++ b/tests/helpers/test_script_variables.py @@ -1,4 +1,5 @@ """Test script variables.""" + import pytest from homeassistant.core import HomeAssistant diff --git a/tests/helpers/test_selector.py b/tests/helpers/test_selector.py index 00942b396e8..8864edc7386 100644 --- a/tests/helpers/test_selector.py +++ b/tests/helpers/test_selector.py @@ -1,4 +1,5 @@ """Test selectors.""" + from enum import Enum import pytest @@ -12,10 +13,10 @@ FAKE_UUID = "a266a680b608c32770e6c45bfe6b8411" @pytest.mark.parametrize( "schema", - ( + [ {"device": None}, {"entity": None}, - ), + ], ) def test_valid_base_schema(schema) -> None: """Test base schema validation.""" @@ -24,14 +25,14 @@ def test_valid_base_schema(schema) -> None: @pytest.mark.parametrize( "schema", - ( + [ None, "not_a_dict", {}, {"non_existing": {}}, # Two keys {"device": {}, "entity": {}}, - ), + ], ) def test_invalid_base_schema(schema) -> None: """Test base schema validation.""" @@ -78,7 +79,7 @@ def _test_selector( @pytest.mark.parametrize( ("schema", "valid_selections", "invalid_selections"), - ( + [ (None, ("abc123",), (None,)), ({}, ("abc123",), (None,)), ({"integration": "zha"}, ("abc123",), (None,)), @@ -146,7 +147,7 @@ def _test_selector( ("abc123",), (None,), ), - ), + ], ) def test_device_selector_schema(schema, valid_selections, invalid_selections) -> None: """Test device selector.""" @@ -155,7 +156,7 @@ def test_device_selector_schema(schema, valid_selections, invalid_selections) -> @pytest.mark.parametrize( ("schema", "valid_selections", "invalid_selections"), - ( + [ ({}, ("sensor.abc123", FAKE_UUID), (None, "abc123")), ({"integration": "zha"}, ("sensor.abc123", FAKE_UUID), (None, "abc123")), ({"domain": "light"}, ("light.abc123", FAKE_UUID), (None, "sensor.abc123")), @@ -265,7 +266,7 @@ def test_device_selector_schema(schema, valid_selections, invalid_selections) -> ("light.abc123", "blah.blah", FAKE_UUID), (None,), ), - ), + ], ) def test_entity_selector_schema(schema, valid_selections, invalid_selections) -> None: """Test entity selector.""" @@ -274,7 +275,7 @@ def test_entity_selector_schema(schema, valid_selections, invalid_selections) -> @pytest.mark.parametrize( "schema", - ( + [ # Feature should be string specifying an enum member, not an int {"filter": [{"supported_features": [1]}]}, # Invalid feature @@ -283,7 +284,7 @@ def test_entity_selector_schema(schema, valid_selections, invalid_selections) -> {"filter": [{"supported_features": ["blah.FooEntityFeature.blah"]}]}, # Unknown feature enum member {"filter": [{"supported_features": ["light.LightEntityFeature.blah"]}]}, - ), + ], ) def test_entity_selector_schema_error(schema) -> None: """Test number selector.""" @@ -293,7 +294,7 @@ def test_entity_selector_schema_error(schema) -> None: @pytest.mark.parametrize( ("schema", "valid_selections", "invalid_selections"), - ( + [ ({}, ("abc123",), (None,)), ({"entity": {}}, ("abc123",), (None,)), ({"entity": {"domain": "light"}}, ("abc123",), (None,)), @@ -351,7 +352,7 @@ def test_entity_selector_schema_error(schema) -> None: ((["abc123", "def456"],)), (None, "abc123", ["abc123", None]), ), - ), + ], ) def test_area_selector_schema(schema, valid_selections, invalid_selections) -> None: """Test area selector.""" @@ -360,13 +361,13 @@ def test_area_selector_schema(schema, valid_selections, invalid_selections) -> N @pytest.mark.parametrize( ("schema", "valid_selections", "invalid_selections"), - ( + [ ( {}, ("23ouih2iu23ou2", "2j4hp3uy4p87wyrpiuhk34"), (None, True, 1), ), - ), + ], ) def test_assist_pipeline_selector_schema( schema, valid_selections, invalid_selections @@ -377,7 +378,7 @@ def test_assist_pipeline_selector_schema( @pytest.mark.parametrize( ("schema", "valid_selections", "invalid_selections"), - ( + [ ( {"min": 10, "max": 50}, ( @@ -397,7 +398,7 @@ def test_assist_pipeline_selector_schema( ({"mode": "box"}, (10,), ()), ({"mode": "box", "step": "any"}, (), ()), ({"mode": "slider", "min": 0, "max": 1, "step": "any"}, (), ()), - ), + ], ) def test_number_selector_schema(schema, valid_selections, invalid_selections) -> None: """Test number selector.""" @@ -406,10 +407,10 @@ def test_number_selector_schema(schema, valid_selections, invalid_selections) -> @pytest.mark.parametrize( "schema", - ( + [ {}, # Must have mandatory fields {"mode": "slider"}, # Must have min+max in slider mode - ), + ], ) def test_number_selector_schema_error(schema) -> None: """Test number selector.""" @@ -419,7 +420,7 @@ def test_number_selector_schema_error(schema) -> None: @pytest.mark.parametrize( ("schema", "valid_selections", "invalid_selections"), - (({}, ("abc123",), (None,)),), + [({}, ("abc123",), (None,))], ) def test_addon_selector_schema(schema, valid_selections, invalid_selections) -> None: """Test add-on selector.""" @@ -428,7 +429,7 @@ def test_addon_selector_schema(schema, valid_selections, invalid_selections) -> @pytest.mark.parametrize( ("schema", "valid_selections", "invalid_selections"), - (({}, ("abc123", "/backup"), (None, "abc@123", "abc 123", "")),), + [({}, ("abc123", "/backup"), (None, "abc@123", "abc 123", ""))], ) def test_backup_location_selector_schema( schema, valid_selections, invalid_selections @@ -439,7 +440,7 @@ def test_backup_location_selector_schema( @pytest.mark.parametrize( ("schema", "valid_selections", "invalid_selections"), - (({}, (1, "one", None), ()),), # Everything can be coerced to bool + [({}, (1, "one", None), ())], # Everything can be coerced to bool ) def test_boolean_selector_schema(schema, valid_selections, invalid_selections) -> None: """Test boolean selector.""" @@ -454,7 +455,7 @@ def test_boolean_selector_schema(schema, valid_selections, invalid_selections) - @pytest.mark.parametrize( ("schema", "valid_selections", "invalid_selections"), - ( + [ ( {}, ("6b68b250388cbe0d620c92dd3acc93ec", "76f2e8f9a6491a1b580b3a8967c27ddd"), @@ -465,7 +466,7 @@ def test_boolean_selector_schema(schema, valid_selections, invalid_selections) - ("6b68b250388cbe0d620c92dd3acc93ec", "76f2e8f9a6491a1b580b3a8967c27ddd"), (None, True, 1), ), - ), + ], ) def test_config_entry_selector_schema( schema, valid_selections, invalid_selections @@ -476,7 +477,7 @@ def test_config_entry_selector_schema( @pytest.mark.parametrize( ("schema", "valid_selections", "invalid_selections"), - ( + [ ( {}, ("NL", "DE"), @@ -487,7 +488,7 @@ def test_config_entry_selector_schema( ("NL", "DE"), (None, True, 1, "sv", "en"), ), - ), + ], ) def test_country_selector_schema(schema, valid_selections, invalid_selections) -> None: """Test country selector.""" @@ -496,7 +497,7 @@ def test_country_selector_schema(schema, valid_selections, invalid_selections) - @pytest.mark.parametrize( ("schema", "valid_selections", "invalid_selections"), - (({}, ("00:00:00",), ("blah", None)),), + [({}, ("00:00:00",), ("blah", None))], ) def test_time_selector_schema(schema, valid_selections, invalid_selections) -> None: """Test time selector.""" @@ -505,13 +506,13 @@ def test_time_selector_schema(schema, valid_selections, invalid_selections) -> N @pytest.mark.parametrize( ("schema", "valid_selections", "invalid_selections"), - ( + [ ( {"entity_id": "sensor.abc"}, ("on", "armed"), (None, True, 1), ), - ), + ], ) def test_state_selector_schema(schema, valid_selections, invalid_selections) -> None: """Test state selector.""" @@ -520,7 +521,7 @@ def test_state_selector_schema(schema, valid_selections, invalid_selections) -> @pytest.mark.parametrize( ("schema", "valid_selections", "invalid_selections"), - ( + [ ({}, ({"entity_id": ["sensor.abc123"]},), ("abc123", None)), ({"entity": {}}, (), ()), ({"entity": {"domain": "light"}}, (), ()), @@ -565,7 +566,7 @@ def test_state_selector_schema(schema, valid_selections, invalid_selections) -> (), (), ), - ), + ], ) def test_target_selector_schema(schema, valid_selections, invalid_selections) -> None: """Test target selector.""" @@ -574,7 +575,7 @@ def test_target_selector_schema(schema, valid_selections, invalid_selections) -> @pytest.mark.parametrize( ("schema", "valid_selections", "invalid_selections"), - (({}, ("abc123",), ()),), + [({}, ("abc123",), ())], ) def test_action_selector_schema(schema, valid_selections, invalid_selections) -> None: """Test action sequence selector.""" @@ -583,7 +584,7 @@ def test_action_selector_schema(schema, valid_selections, invalid_selections) -> @pytest.mark.parametrize( ("schema", "valid_selections", "invalid_selections"), - (({}, ("abc123",), ()),), + [({}, ("abc123",), ())], ) def test_object_selector_schema(schema, valid_selections, invalid_selections) -> None: """Test object selector.""" @@ -592,7 +593,7 @@ def test_object_selector_schema(schema, valid_selections, invalid_selections) -> @pytest.mark.parametrize( ("schema", "valid_selections", "invalid_selections"), - ( + [ ({}, ("abc123",), (None,)), ({"multiline": True}, (), ()), ({"multiline": False, "type": "email"}, (), ()), @@ -602,7 +603,7 @@ def test_object_selector_schema(schema, valid_selections, invalid_selections) -> (["abc123", "def456"],), ("abc123", None, ["abc123", None]), ), - ), + ], ) def test_text_selector_schema(schema, valid_selections, invalid_selections) -> None: """Test text selector.""" @@ -611,7 +612,7 @@ def test_text_selector_schema(schema, valid_selections, invalid_selections) -> N @pytest.mark.parametrize( ("schema", "valid_selections", "invalid_selections"), - ( + [ ( {"options": ["red", "green", "blue"]}, ("red", "green", "blue"), @@ -680,7 +681,7 @@ def test_text_selector_schema(schema, valid_selections, invalid_selections) -> N ("red", "blue"), (0, None, ["red"]), ), - ), + ], ) def test_select_selector_schema(schema, valid_selections, invalid_selections) -> None: """Test select selector.""" @@ -689,14 +690,14 @@ def test_select_selector_schema(schema, valid_selections, invalid_selections) -> @pytest.mark.parametrize( "schema", - ( + [ {}, # Must have options {"options": {"hello": "World"}}, # Options must be a list # Options must be strings or value / label pairs {"options": [{"hello": "World"}]}, # Options must all be of the same type {"options": ["red", {"value": "green", "label": "Emerald Green"}]}, - ), + ], ) def test_select_selector_schema_error(schema) -> None: """Test select selector.""" @@ -706,7 +707,7 @@ def test_select_selector_schema_error(schema) -> None: @pytest.mark.parametrize( ("schema", "valid_selections", "invalid_selections"), - ( + [ ( {"entity_id": "sensor.abc"}, ("friendly_name", "device_class"), @@ -717,7 +718,7 @@ def test_select_selector_schema_error(schema) -> None: ("device_class", "state_class"), (None,), ), - ), + ], ) def test_attribute_selector_schema( schema, valid_selections, invalid_selections @@ -728,7 +729,7 @@ def test_attribute_selector_schema( @pytest.mark.parametrize( ("schema", "valid_selections", "invalid_selections"), - ( + [ ( {}, ( @@ -742,7 +743,7 @@ def test_attribute_selector_schema( ({"seconds": 10}, {"days": 10}), (None, {}), ), - ), + ], ) def test_duration_selector_schema(schema, valid_selections, invalid_selections) -> None: """Test duration selector.""" @@ -751,13 +752,13 @@ def test_duration_selector_schema(schema, valid_selections, invalid_selections) @pytest.mark.parametrize( ("schema", "valid_selections", "invalid_selections"), - ( + [ ( {}, ("mdi:abc",), (None,), ), - ), + ], ) def test_icon_selector_schema(schema, valid_selections, invalid_selections) -> None: """Test icon selector.""" @@ -766,7 +767,7 @@ def test_icon_selector_schema(schema, valid_selections, invalid_selections) -> N @pytest.mark.parametrize( ("schema", "valid_selections", "invalid_selections"), - ( + [ ( {}, ("abc",), @@ -777,7 +778,7 @@ def test_icon_selector_schema(schema, valid_selections, invalid_selections) -> N ("abc",), (None,), ), - ), + ], ) def test_theme_selector_schema(schema, valid_selections, invalid_selections) -> None: """Test theme selector.""" @@ -786,7 +787,7 @@ def test_theme_selector_schema(schema, valid_selections, invalid_selections) -> @pytest.mark.parametrize( ("schema", "valid_selections", "invalid_selections"), - ( + [ ( {}, ( @@ -804,7 +805,7 @@ def test_theme_selector_schema(schema, valid_selections, invalid_selections) -> ), (None, "abc", {}), ), - ), + ], ) def test_media_selector_schema(schema, valid_selections, invalid_selections) -> None: """Test media selector.""" @@ -825,7 +826,7 @@ def test_media_selector_schema(schema, valid_selections, invalid_selections) -> @pytest.mark.parametrize( ("schema", "valid_selections", "invalid_selections"), - ( + [ ( {}, ("nl", "fr"), @@ -836,7 +837,7 @@ def test_media_selector_schema(schema, valid_selections, invalid_selections) -> ("nl", "fr"), (None, True, 1, "de", "en"), ), - ), + ], ) def test_language_selector_schema(schema, valid_selections, invalid_selections) -> None: """Test language selector.""" @@ -845,7 +846,7 @@ def test_language_selector_schema(schema, valid_selections, invalid_selections) @pytest.mark.parametrize( ("schema", "valid_selections", "invalid_selections"), - ( + [ ( {}, ( @@ -872,7 +873,7 @@ def test_language_selector_schema(schema, valid_selections, invalid_selections) {"longitude": 1.0}, ), ), - ), + ], ) def test_location_selector_schema(schema, valid_selections, invalid_selections) -> None: """Test location selector.""" @@ -882,13 +883,13 @@ def test_location_selector_schema(schema, valid_selections, invalid_selections) @pytest.mark.parametrize( ("schema", "valid_selections", "invalid_selections"), - ( + [ ( {}, ([0, 0, 0], [255, 255, 255], [0.0, 0.0, 0.0], [255.0, 255.0, 255.0]), (None, "abc", [0, 0, "nil"], (255, 255, 255)), ), - ), + ], ) def test_rgb_color_selector_schema( schema, valid_selections, invalid_selections @@ -900,7 +901,7 @@ def test_rgb_color_selector_schema( @pytest.mark.parametrize( ("schema", "valid_selections", "invalid_selections"), - ( + [ ( {}, (100, 100.0), @@ -921,7 +922,7 @@ def test_rgb_color_selector_schema( (1000, 2000), (999, 2001), ), - ), + ], ) def test_color_tempselector_schema( schema, valid_selections, invalid_selections @@ -933,13 +934,13 @@ def test_color_tempselector_schema( @pytest.mark.parametrize( ("schema", "valid_selections", "invalid_selections"), - ( + [ ( {}, ("2022-03-24",), (None, "abc", "00:00", "2022-03-24 00:00", "2022-03-32"), ), - ), + ], ) def test_date_selector_schema(schema, valid_selections, invalid_selections) -> None: """Test date selector.""" @@ -949,13 +950,13 @@ def test_date_selector_schema(schema, valid_selections, invalid_selections) -> N @pytest.mark.parametrize( ("schema", "valid_selections", "invalid_selections"), - ( + [ ( {}, ("2022-03-24 00:00", "2022-03-24"), (None, "abc", "00:00", "2022-03-24 24:01"), ), - ), + ], ) def test_datetime_selector_schema(schema, valid_selections, invalid_selections) -> None: """Test datetime selector.""" @@ -965,7 +966,7 @@ def test_datetime_selector_schema(schema, valid_selections, invalid_selections) @pytest.mark.parametrize( ("schema", "valid_selections", "invalid_selections"), - (({}, ("abc123", "{{ now() }}"), (None, "{{ incomplete }", "{% if True %}Hi!")),), + [({}, ("abc123", "{{ now() }}"), (None, "{{ incomplete }", "{% if True %}Hi!"))], ) def test_template_selector_schema(schema, valid_selections, invalid_selections) -> None: """Test template selector.""" @@ -974,13 +975,13 @@ def test_template_selector_schema(schema, valid_selections, invalid_selections) @pytest.mark.parametrize( ("schema", "valid_selections", "invalid_selections"), - ( + [ ( {"accept": "image/*"}, ("0182a1b99dbc5ae24aecd90c346605fa",), (None, "not-a-uuid", "abcd", 1), ), - ), + ], ) def test_file_selector_schema(schema, valid_selections, invalid_selections) -> None: """Test file selector.""" @@ -990,7 +991,7 @@ def test_file_selector_schema(schema, valid_selections, invalid_selections) -> N @pytest.mark.parametrize( ("schema", "valid_selections", "invalid_selections"), - ( + [ ( {"value": True, "label": "Blah"}, (True, 1), @@ -1021,7 +1022,7 @@ def test_file_selector_schema(schema, valid_selections, invalid_selections) -> N ("dog",), (None, False, True, 0, 1, "abc", "def"), ), - ), + ], ) def test_constant_selector_schema(schema, valid_selections, invalid_selections) -> None: """Test constant selector.""" @@ -1030,11 +1031,11 @@ def test_constant_selector_schema(schema, valid_selections, invalid_selections) @pytest.mark.parametrize( "schema", - ( + [ {}, # Value is mandatory {"value": []}, # Value must be str, int or bool {"value": 123, "label": 123}, # Label must be str - ), + ], ) def test_constant_selector_schema_error(schema) -> None: """Test constant selector.""" @@ -1044,7 +1045,7 @@ def test_constant_selector_schema_error(schema) -> None: @pytest.mark.parametrize( ("schema", "valid_selections", "invalid_selections"), - ( + [ ( {}, ("home_assistant", "2j4hp3uy4p87wyrpiuhk34"), @@ -1055,7 +1056,7 @@ def test_constant_selector_schema_error(schema) -> None: ("home_assistant", "2j4hp3uy4p87wyrpiuhk34"), (None, True, 1), ), - ), + ], ) def test_conversation_agent_selector_schema( schema, valid_selections, invalid_selections @@ -1066,7 +1067,7 @@ def test_conversation_agent_selector_schema( @pytest.mark.parametrize( ("schema", "valid_selections", "invalid_selections"), - ( + [ ( {}, ( @@ -1081,7 +1082,7 @@ def test_conversation_agent_selector_schema( ), ("abc"), ), - ), + ], ) def test_condition_selector_schema( schema, valid_selections, invalid_selections @@ -1092,7 +1093,7 @@ def test_condition_selector_schema( @pytest.mark.parametrize( ("schema", "valid_selections", "invalid_selections"), - ( + [ ( {}, ( @@ -1107,7 +1108,7 @@ def test_condition_selector_schema( ), ("abc"), ), - ), + ], ) def test_trigger_selector_schema(schema, valid_selections, invalid_selections) -> None: """Test trigger sequence selector.""" @@ -1116,7 +1117,7 @@ def test_trigger_selector_schema(schema, valid_selections, invalid_selections) - @pytest.mark.parametrize( ("schema", "valid_selections", "invalid_selections"), - ( + [ ( {"data": "test", "scale": 5}, ("test",), @@ -1136,8 +1137,91 @@ def test_trigger_selector_schema(schema, valid_selections, invalid_selections) - ("test",), (True, 1, []), ), - ), + ], ) def test_qr_code_selector_schema(schema, valid_selections, invalid_selections) -> None: """Test QR code selector.""" _test_selector("qr_code", schema, valid_selections, invalid_selections) + + +@pytest.mark.parametrize( + ("schema", "valid_selections", "invalid_selections"), + [ + ({}, ("abc123",), (None,)), + ( + {"multiple": True}, + ((["abc123", "def456"],)), + (None, "abc123", ["abc123", None]), + ), + ], +) +def test_label_selector_schema(schema, valid_selections, invalid_selections) -> None: + """Test label selector.""" + _test_selector("label", schema, valid_selections, invalid_selections) + + +@pytest.mark.parametrize( + ("schema", "valid_selections", "invalid_selections"), + [ + ({}, ("abc123",), (None,)), + ({"entity": {}}, ("abc123",), (None,)), + ({"entity": {"domain": "light"}}, ("abc123",), (None,)), + ( + {"entity": {"domain": "binary_sensor", "device_class": "motion"}}, + ("abc123",), + (None,), + ), + ( + { + "entity": { + "domain": "binary_sensor", + "device_class": "motion", + "integration": "demo", + } + }, + ("abc123",), + (None,), + ), + ( + { + "entity": [ + {"domain": "light"}, + {"domain": "binary_sensor", "device_class": "motion"}, + ] + }, + ("abc123",), + (None,), + ), + ( + {"device": {"integration": "demo", "model": "mock-model"}}, + ("abc123",), + (None,), + ), + ( + { + "device": [ + {"integration": "demo", "model": "mock-model"}, + {"integration": "other-demo", "model": "other-mock-model"}, + ] + }, + ("abc123",), + (None,), + ), + ( + { + "entity": {"domain": "binary_sensor", "device_class": "motion"}, + "device": {"integration": "demo", "model": "mock-model"}, + }, + ("abc123",), + (None,), + ), + ( + {"multiple": True}, + ((["abc123", "def456"],)), + (None, "abc123", ["abc123", None]), + ), + ], +) +def test_floor_selector_schema(schema, valid_selections, invalid_selections) -> None: + """Test floor selector.""" + _test_selector("floor", schema, valid_selections, invalid_selections) diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index 90f9b65aaba..74b8a86ce7c 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -1,4 +1,5 @@ """Test service helpers.""" + import asyncio from collections.abc import Iterable from copy import deepcopy @@ -12,6 +13,9 @@ import voluptuous as vol from homeassistant import exceptions from homeassistant.auth.permissions import PolicyPermissions import homeassistant.components # noqa: F401 +from homeassistant.components.group import DOMAIN as DOMAIN_GROUP, Group +from homeassistant.components.logger import DOMAIN as DOMAIN_LOGGER +from homeassistant.components.shell_command import DOMAIN as DOMAIN_SHELL_COMMAND from homeassistant.const import ( ATTR_ENTITY_ID, ENTITY_MATCH_ALL, @@ -28,18 +32,21 @@ from homeassistant.core import ( SupportsResponse, ) from homeassistant.helpers import ( + area_registry as ar, device_registry as dr, entity_registry as er, service, template, ) import homeassistant.helpers.config_validation as cv +from homeassistant.loader import async_get_integration from homeassistant.setup import async_setup_component from tests.common import ( MockEntity, MockUser, async_mock_service, + mock_area_registry, mock_device_registry, mock_registry, ) @@ -97,12 +104,38 @@ def mock_entities(hass: HomeAssistant) -> dict[str, MockEntity]: @pytest.fixture -def area_mock(hass): - """Mock including area info.""" +def floor_area_mock(hass: HomeAssistant) -> None: + """Mock including floor and area info.""" hass.states.async_set("light.Bowl", STATE_ON) hass.states.async_set("light.Ceiling", STATE_OFF) hass.states.async_set("light.Kitchen", STATE_OFF) + area_in_floor = ar.AreaEntry( + id="test-area", + name="Test area", + aliases={}, + normalized_name="test-area", + floor_id="test-floor", + icon=None, + picture=None, + ) + area_in_floor_a = ar.AreaEntry( + id="area-a", + name="Area A", + aliases={}, + normalized_name="area-a", + floor_id="floor-a", + icon=None, + picture=None, + ) + mock_area_registry( + hass, + { + area_in_floor.id: area_in_floor, + area_in_floor_a.id: area_in_floor_a, + }, + ) + device_in_area = dr.DeviceEntry(area_id="test-area") device_no_area = dr.DeviceEntry(id="device-no-area-id") device_diff_area = dr.DeviceEntry(area_id="diff-area") @@ -233,6 +266,120 @@ def area_mock(hass): ) +@pytest.fixture +def label_mock(hass: HomeAssistant) -> None: + """Mock including label info.""" + hass.states.async_set("light.bowl", STATE_ON) + hass.states.async_set("light.ceiling", STATE_OFF) + hass.states.async_set("light.kitchen", STATE_OFF) + + area_with_labels = ar.AreaEntry( + id="area-with-labels", + name="Area with labels", + aliases={}, + normalized_name="with_labels", + floor_id=None, + icon=None, + labels={"label_area"}, + picture=None, + ) + area_without_labels = ar.AreaEntry( + id="area-no-labels", + name="Area without labels", + aliases={}, + normalized_name="without_labels", + floor_id=None, + icon=None, + labels=set(), + picture=None, + ) + mock_area_registry( + hass, + { + area_with_labels.id: area_with_labels, + area_without_labels.id: area_without_labels, + }, + ) + + device_has_label1 = dr.DeviceEntry(labels={"label1"}) + device_has_label2 = dr.DeviceEntry(labels={"label2"}) + device_has_labels = dr.DeviceEntry( + labels={"label1", "label2"}, area_id=area_with_labels.id + ) + device_no_labels = dr.DeviceEntry( + id="device-no-labels", area_id=area_without_labels.id + ) + + mock_device_registry( + hass, + { + device_has_label1.id: device_has_label1, + device_has_label2.id: device_has_label2, + device_has_labels.id: device_has_labels, + device_no_labels.id: device_no_labels, + }, + ) + + entity_with_my_label = er.RegistryEntry( + entity_id="light.with_my_label", + unique_id="with_my_label", + platform="test", + labels={"my-label"}, + ) + hidden_entity_with_my_label = er.RegistryEntry( + entity_id="light.hidden_with_my_label", + unique_id="hidden_with_my_label", + platform="test", + labels={"my-label"}, + hidden_by=er.RegistryEntryHider.USER, + ) + config_entity_with_my_label = er.RegistryEntry( + entity_id="light.config_with_my_label", + unique_id="config_with_my_label", + platform="test", + labels={"my-label"}, + entity_category=EntityCategory.CONFIG, + ) + entity_with_label1_from_device = er.RegistryEntry( + entity_id="light.with_label1_from_device", + unique_id="with_label1_from_device", + platform="test", + device_id=device_has_label1.id, + ) + entity_with_label1_and_label2_from_device = er.RegistryEntry( + entity_id="light.with_label1_and_label2_from_device", + unique_id="with_label1_and_label2_from_device", + platform="test", + labels={"label1"}, + device_id=device_has_label2.id, + ) + entity_with_labels_from_device = er.RegistryEntry( + entity_id="light.with_labels_from_device", + unique_id="with_labels_from_device", + platform="test", + device_id=device_has_labels.id, + ) + entity_with_no_labels = er.RegistryEntry( + entity_id="light.no_labels", + unique_id="no_labels", + platform="test", + device_id=device_no_labels.id, + ) + + mock_registry( + hass, + { + config_entity_with_my_label.entity_id: config_entity_with_my_label, + entity_with_label1_and_label2_from_device.entity_id: entity_with_label1_and_label2_from_device, + entity_with_label1_from_device.entity_id: entity_with_label1_from_device, + entity_with_labels_from_device.entity_id: entity_with_labels_from_device, + entity_with_my_label.entity_id: entity_with_my_label, + entity_with_no_labels.entity_id: entity_with_no_labels, + hidden_entity_with_my_label.entity_id: hidden_entity_with_my_label, + }, + ) + + async def test_call_from_config(hass: HomeAssistant) -> None: """Test the sync wrapper of service.async_call_from_config.""" calls = async_mock_service(hass, "test_domain", "test_service") @@ -259,7 +406,11 @@ async def test_service_call(hass: HomeAssistant) -> None: "effect": {"value": "{{ 'complex' }}", "simple": "simple"}, }, "data_template": {"list": ["{{ 'list' }}", "2"]}, - "target": {"area_id": "test-area-id", "entity_id": "will.be_overridden"}, + "target": { + "area_id": "test-area-id", + "entity_id": "will.be_overridden", + "floor_id": "test-floor-id", + }, } await service.async_call_from_config(hass, config) @@ -274,6 +425,7 @@ async def test_service_call(hass: HomeAssistant) -> None: "list": ["list", "2"], "entity_id": ["hello.world"], "area_id": ["test-area-id"], + "floor_id": ["test-floor-id"], } config = { @@ -282,6 +434,7 @@ async def test_service_call(hass: HomeAssistant) -> None: "area_id": ["area-42", "{{ 'area-51' }}"], "device_id": ["abcdef", "{{ 'fedcba' }}"], "entity_id": ["light.static", "{{ 'light.dynamic' }}"], + "floor_id": ["floor-first", "{{ 'floor-second' }}"], }, } @@ -292,6 +445,7 @@ async def test_service_call(hass: HomeAssistant) -> None: "area_id": ["area-42", "area-51"], "device_id": ["abcdef", "fedcba"], "entity_id": ["light.static", "light.dynamic"], + "floor_id": ["floor-first", "floor-second"], } config = { @@ -447,7 +601,7 @@ async def test_service_call_entry_id( assert dict(calls[0].data) == {"entity_id": ["hello.world"]} -@pytest.mark.parametrize("target", ("all", "none")) +@pytest.mark.parametrize("target", ["all", "none"]) async def test_service_call_all_none(hass: HomeAssistant, target) -> None: """Test service call targeting all.""" calls = async_mock_service(hass, "test_domain", "test_service") @@ -471,7 +625,7 @@ async def test_extract_entity_ids(hass: HomeAssistant) -> None: assert await async_setup_component(hass, "group", {}) await hass.async_block_till_done() - await hass.components.group.Group.async_create_group( + await Group.async_create_group( hass, "test", created_by_service=False, @@ -505,7 +659,9 @@ async def test_extract_entity_ids(hass: HomeAssistant) -> None: ) -async def test_extract_entity_ids_from_area(hass: HomeAssistant, area_mock) -> None: +async def test_extract_entity_ids_from_area( + hass: HomeAssistant, floor_area_mock +) -> None: """Test extract_entity_ids method with areas.""" call = ServiceCall("light", "turn_on", {"area_id": "own-area"}) @@ -536,7 +692,9 @@ async def test_extract_entity_ids_from_area(hass: HomeAssistant, area_mock) -> N ) -async def test_extract_entity_ids_from_devices(hass: HomeAssistant, area_mock) -> None: +async def test_extract_entity_ids_from_devices( + hass: HomeAssistant, floor_area_mock +) -> None: """Test extract_entity_ids method with devices.""" assert await service.async_extract_entity_ids( hass, ServiceCall("light", "turn_on", {"device_id": "device-no-area-id"}) @@ -559,20 +717,97 @@ async def test_extract_entity_ids_from_devices(hass: HomeAssistant, area_mock) - ) +@pytest.mark.usefixtures("floor_area_mock") +async def test_extract_entity_ids_from_floor(hass: HomeAssistant) -> None: + """Test extract_entity_ids method with floors.""" + call = ServiceCall("light", "turn_on", {"floor_id": "test-floor"}) + + assert { + "light.in_area", + "light.assigned_to_area", + } == await service.async_extract_entity_ids(hass, call) + + call = ServiceCall("light", "turn_on", {"floor_id": ["test-floor", "floor-a"]}) + + assert { + "light.in_area", + "light.assigned_to_area", + "light.in_area_a", + } == await service.async_extract_entity_ids(hass, call) + + assert ( + await service.async_extract_entity_ids( + hass, ServiceCall("light", "turn_on", {"floor_id": ENTITY_MATCH_NONE}) + ) + == set() + ) + + +@pytest.mark.usefixtures("label_mock") +async def test_extract_entity_ids_from_labels(hass: HomeAssistant) -> None: + """Test extract_entity_ids method with labels.""" + call = ServiceCall("light", "turn_on", {"label_id": "my-label"}) + + assert { + "light.with_my_label", + } == await service.async_extract_entity_ids(hass, call) + + call = ServiceCall("light", "turn_on", {"label_id": "label1"}) + + assert { + "light.with_label1_from_device", + "light.with_labels_from_device", + "light.with_label1_and_label2_from_device", + } == await service.async_extract_entity_ids(hass, call) + + call = ServiceCall("light", "turn_on", {"label_id": ["label2"]}) + + assert { + "light.with_labels_from_device", + "light.with_label1_and_label2_from_device", + } == await service.async_extract_entity_ids(hass, call) + + call = ServiceCall("light", "turn_on", {"label_id": ["label_area"]}) + + assert { + "light.with_labels_from_device", + } == await service.async_extract_entity_ids(hass, call) + + assert ( + await service.async_extract_entity_ids( + hass, ServiceCall("light", "turn_on", {"label_id": ENTITY_MATCH_NONE}) + ) + == set() + ) + + async def test_async_get_all_descriptions(hass: HomeAssistant) -> None: """Test async_get_all_descriptions.""" - group = hass.components.group - group_config = {group.DOMAIN: {}} - await async_setup_component(hass, group.DOMAIN, group_config) - descriptions = await service.async_get_all_descriptions(hass) + group_config = {DOMAIN_GROUP: {}} + assert await async_setup_component(hass, DOMAIN_GROUP, group_config) + assert await async_setup_component(hass, "system_health", {}) + + with patch( + "homeassistant.helpers.service._load_services_files", + side_effect=service._load_services_files, + ) as proxy_load_services_files: + descriptions = await service.async_get_all_descriptions(hass) + + # Test we only load services.yaml for integrations with services.yaml + # And system_health has no services + assert proxy_load_services_files.mock_calls[0][1][1] == [ + await async_get_integration(hass, "group") + ] assert len(descriptions) == 1 assert "description" in descriptions["group"]["reload"] assert "fields" in descriptions["group"]["reload"] - logger = hass.components.logger - logger_config = {logger.DOMAIN: {}} + # Does not have services + assert "system_health" not in descriptions + + logger_config = {DOMAIN_LOGGER: {}} async def async_get_translations( hass: HomeAssistant, @@ -582,7 +817,7 @@ async def test_async_get_all_descriptions(hass: HomeAssistant) -> None: config_flow: bool | None = None, ) -> dict[str, Any]: """Return all backend translations.""" - translation_key_prefix = f"component.{logger.DOMAIN}.services.set_default_level" + translation_key_prefix = f"component.{DOMAIN_LOGGER}.services.set_default_level" return { f"{translation_key_prefix}.name": "Translated name", f"{translation_key_prefix}.description": "Translated description", @@ -595,58 +830,58 @@ async def test_async_get_all_descriptions(hass: HomeAssistant) -> None: "homeassistant.helpers.service.translation.async_get_translations", side_effect=async_get_translations, ): - await async_setup_component(hass, logger.DOMAIN, logger_config) + await async_setup_component(hass, DOMAIN_LOGGER, logger_config) descriptions = await service.async_get_all_descriptions(hass) assert len(descriptions) == 2 - assert descriptions[logger.DOMAIN]["set_default_level"]["name"] == "Translated name" + assert descriptions[DOMAIN_LOGGER]["set_default_level"]["name"] == "Translated name" assert ( - descriptions[logger.DOMAIN]["set_default_level"]["description"] + descriptions[DOMAIN_LOGGER]["set_default_level"]["description"] == "Translated description" ) assert ( - descriptions[logger.DOMAIN]["set_default_level"]["fields"]["level"]["name"] + descriptions[DOMAIN_LOGGER]["set_default_level"]["fields"]["level"]["name"] == "Field name" ) assert ( - descriptions[logger.DOMAIN]["set_default_level"]["fields"]["level"][ + descriptions[DOMAIN_LOGGER]["set_default_level"]["fields"]["level"][ "description" ] == "Field description" ) assert ( - descriptions[logger.DOMAIN]["set_default_level"]["fields"]["level"]["example"] + descriptions[DOMAIN_LOGGER]["set_default_level"]["fields"]["level"]["example"] == "Field example" ) - hass.services.async_register(logger.DOMAIN, "new_service", lambda x: None, None) + hass.services.async_register(DOMAIN_LOGGER, "new_service", lambda x: None, None) service.async_set_service_schema( - hass, logger.DOMAIN, "new_service", {"description": "new service"} + hass, DOMAIN_LOGGER, "new_service", {"description": "new service"} ) descriptions = await service.async_get_all_descriptions(hass) - assert "description" in descriptions[logger.DOMAIN]["new_service"] - assert descriptions[logger.DOMAIN]["new_service"]["description"] == "new service" + assert "description" in descriptions[DOMAIN_LOGGER]["new_service"] + assert descriptions[DOMAIN_LOGGER]["new_service"]["description"] == "new service" hass.services.async_register( - logger.DOMAIN, "another_new_service", lambda x: None, None + DOMAIN_LOGGER, "another_new_service", lambda x: None, None ) hass.services.async_register( - logger.DOMAIN, + DOMAIN_LOGGER, "service_with_optional_response", lambda x: None, None, SupportsResponse.OPTIONAL, ) hass.services.async_register( - logger.DOMAIN, + DOMAIN_LOGGER, "service_with_only_response", lambda x: None, None, SupportsResponse.ONLY, ) hass.services.async_register( - logger.DOMAIN, + DOMAIN_LOGGER, "another_service_with_response", lambda x: None, None, @@ -654,22 +889,22 @@ async def test_async_get_all_descriptions(hass: HomeAssistant) -> None: ) service.async_set_service_schema( hass, - logger.DOMAIN, + DOMAIN_LOGGER, "another_service_with_response", {"description": "response service"}, ) descriptions = await service.async_get_all_descriptions(hass) - assert "another_new_service" in descriptions[logger.DOMAIN] - assert "service_with_optional_response" in descriptions[logger.DOMAIN] - assert descriptions[logger.DOMAIN]["service_with_optional_response"][ + assert "another_new_service" in descriptions[DOMAIN_LOGGER] + assert "service_with_optional_response" in descriptions[DOMAIN_LOGGER] + assert descriptions[DOMAIN_LOGGER]["service_with_optional_response"][ "response" ] == {"optional": True} - assert "service_with_only_response" in descriptions[logger.DOMAIN] - assert descriptions[logger.DOMAIN]["service_with_only_response"]["response"] == { + assert "service_with_only_response" in descriptions[DOMAIN_LOGGER] + assert descriptions[DOMAIN_LOGGER]["service_with_only_response"]["response"] == { "optional": False } - assert "another_service_with_response" in descriptions[logger.DOMAIN] - assert descriptions[logger.DOMAIN]["another_service_with_response"]["response"] == { + assert "another_service_with_response" in descriptions[DOMAIN_LOGGER] + assert descriptions[DOMAIN_LOGGER]["another_service_with_response"]["response"] == { "optional": True } @@ -681,9 +916,8 @@ async def test_async_get_all_descriptions_failing_integration( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test async_get_all_descriptions when async_get_integrations returns an exception.""" - group = hass.components.group - group_config = {group.DOMAIN: {}} - await async_setup_component(hass, group.DOMAIN, group_config) + group_config = {DOMAIN_GROUP: {}} + await async_setup_component(hass, DOMAIN_GROUP, group_config) descriptions = await service.async_get_all_descriptions(hass) assert len(descriptions) == 1 @@ -691,15 +925,17 @@ async def test_async_get_all_descriptions_failing_integration( assert "description" in descriptions["group"]["reload"] assert "fields" in descriptions["group"]["reload"] - logger = hass.components.logger - logger_config = {logger.DOMAIN: {}} - await async_setup_component(hass, logger.DOMAIN, logger_config) - with patch( - "homeassistant.helpers.service.async_get_integrations", - return_value={"logger": ImportError}, - ), patch( - "homeassistant.helpers.service.translation.async_get_translations", - return_value={}, + logger_config = {DOMAIN_LOGGER: {}} + await async_setup_component(hass, DOMAIN_LOGGER, logger_config) + with ( + patch( + "homeassistant.helpers.service.async_get_integrations", + return_value={"logger": ImportError}, + ), + patch( + "homeassistant.helpers.service.translation.async_get_translations", + return_value={}, + ), ): descriptions = await service.async_get_all_descriptions(hass) @@ -708,32 +944,32 @@ async def test_async_get_all_descriptions_failing_integration( # Services are empty defaults if the load fails but should # not raise - assert descriptions[logger.DOMAIN]["set_level"] == { + assert descriptions[DOMAIN_LOGGER]["set_level"] == { "description": "", "fields": {}, "name": "", } - hass.services.async_register(logger.DOMAIN, "new_service", lambda x: None, None) + hass.services.async_register(DOMAIN_LOGGER, "new_service", lambda x: None, None) service.async_set_service_schema( - hass, logger.DOMAIN, "new_service", {"description": "new service"} + hass, DOMAIN_LOGGER, "new_service", {"description": "new service"} ) descriptions = await service.async_get_all_descriptions(hass) - assert "description" in descriptions[logger.DOMAIN]["new_service"] - assert descriptions[logger.DOMAIN]["new_service"]["description"] == "new service" + assert "description" in descriptions[DOMAIN_LOGGER]["new_service"] + assert descriptions[DOMAIN_LOGGER]["new_service"]["description"] == "new service" hass.services.async_register( - logger.DOMAIN, "another_new_service", lambda x: None, None + DOMAIN_LOGGER, "another_new_service", lambda x: None, None ) hass.services.async_register( - logger.DOMAIN, + DOMAIN_LOGGER, "service_with_optional_response", lambda x: None, None, SupportsResponse.OPTIONAL, ) hass.services.async_register( - logger.DOMAIN, + DOMAIN_LOGGER, "service_with_only_response", lambda x: None, None, @@ -741,13 +977,13 @@ async def test_async_get_all_descriptions_failing_integration( ) descriptions = await service.async_get_all_descriptions(hass) - assert "another_new_service" in descriptions[logger.DOMAIN] - assert "service_with_optional_response" in descriptions[logger.DOMAIN] - assert descriptions[logger.DOMAIN]["service_with_optional_response"][ + assert "another_new_service" in descriptions[DOMAIN_LOGGER] + assert "service_with_optional_response" in descriptions[DOMAIN_LOGGER] + assert descriptions[DOMAIN_LOGGER]["service_with_optional_response"][ "response" ] == {"optional": True} - assert "service_with_only_response" in descriptions[logger.DOMAIN] - assert descriptions[logger.DOMAIN]["service_with_only_response"]["response"] == { + assert "service_with_only_response" in descriptions[DOMAIN_LOGGER] + assert descriptions[DOMAIN_LOGGER]["service_with_only_response"]["response"] == { "optional": False } @@ -759,9 +995,8 @@ async def test_async_get_all_descriptions_dynamically_created_services( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test async_get_all_descriptions when async_get_integrations when services are dynamic.""" - group = hass.components.group - group_config = {group.DOMAIN: {}} - await async_setup_component(hass, group.DOMAIN, group_config) + group_config = {DOMAIN_GROUP: {}} + await async_setup_component(hass, DOMAIN_GROUP, group_config) descriptions = await service.async_get_all_descriptions(hass) assert len(descriptions) == 1 @@ -769,13 +1004,12 @@ async def test_async_get_all_descriptions_dynamically_created_services( assert "description" in descriptions["group"]["reload"] assert "fields" in descriptions["group"]["reload"] - shell_command = hass.components.shell_command - shell_command_config = {shell_command.DOMAIN: {"test_service": "ls /bin"}} - await async_setup_component(hass, shell_command.DOMAIN, shell_command_config) + shell_command_config = {DOMAIN_SHELL_COMMAND: {"test_service": "ls /bin"}} + await async_setup_component(hass, DOMAIN_SHELL_COMMAND, shell_command_config) descriptions = await service.async_get_all_descriptions(hass) assert len(descriptions) == 2 - assert descriptions[shell_command.DOMAIN]["test_service"] == { + assert descriptions[DOMAIN_SHELL_COMMAND]["test_service"] == { "description": "", "fields": {}, "name": "", @@ -787,9 +1021,8 @@ async def test_async_get_all_descriptions_new_service_added_while_loading( hass: HomeAssistant, ) -> None: """Test async_get_all_descriptions when a new service is added while loading translations.""" - group = hass.components.group - group_config = {group.DOMAIN: {}} - await async_setup_component(hass, group.DOMAIN, group_config) + group_config = {DOMAIN_GROUP: {}} + await async_setup_component(hass, DOMAIN_GROUP, group_config) descriptions = await service.async_get_all_descriptions(hass) assert len(descriptions) == 1 @@ -797,8 +1030,7 @@ async def test_async_get_all_descriptions_new_service_added_while_loading( assert "description" in descriptions["group"]["reload"] assert "fields" in descriptions["group"]["reload"] - logger = hass.components.logger - logger_domain = logger.DOMAIN + logger_domain = DOMAIN_LOGGER logger_config = {logger_domain: {}} translations_called = asyncio.Event() @@ -867,9 +1099,8 @@ async def test_register_with_mixed_case(hass: HomeAssistant) -> None: For backwards compatibility, we have historically allowed mixed case, and automatically converted it to lowercase. """ - logger = hass.components.logger - logger_config = {logger.DOMAIN: {}} - await async_setup_component(hass, logger.DOMAIN, logger_config) + logger_config = {DOMAIN_LOGGER: {}} + await async_setup_component(hass, DOMAIN_LOGGER, logger_config) logger_domain_mixed = "LoGgEr" hass.services.async_register( logger_domain_mixed, "NeW_SeRVICE", lambda x: None, None @@ -878,8 +1109,8 @@ async def test_register_with_mixed_case(hass: HomeAssistant) -> None: hass, logger_domain_mixed, "NeW_SeRVICE", {"description": "new service"} ) descriptions = await service.async_get_all_descriptions(hass) - assert "description" in descriptions[logger.DOMAIN]["new_service"] - assert descriptions[logger.DOMAIN]["new_service"]["description"] == "new service" + assert "description" in descriptions[DOMAIN_LOGGER]["new_service"] + assert descriptions[DOMAIN_LOGGER]["new_service"]["description"] == "new service" async def test_call_with_required_features(hass: HomeAssistant, mock_entities) -> None: @@ -1066,9 +1297,12 @@ async def test_call_context_target_specific_no_auth( hass: HomeAssistant, mock_handle_entity_call, mock_entities ) -> None: """Check targeting specific entities without auth.""" - with pytest.raises(exceptions.Unauthorized) as err, patch( - "homeassistant.auth.AuthManager.async_get_user", - return_value=Mock(permissions=PolicyPermissions({}, None), is_admin=False), + with ( + pytest.raises(exceptions.Unauthorized) as err, + patch( + "homeassistant.auth.AuthManager.async_get_user", + return_value=Mock(permissions=PolicyPermissions({}, None), is_admin=False), + ), ): await service.entity_service_call( hass, @@ -1465,7 +1699,9 @@ async def test_extract_from_service_filter_out_non_existing_entities( ] -async def test_extract_from_service_area_id(hass: HomeAssistant, area_mock) -> None: +async def test_extract_from_service_area_id( + hass: HomeAssistant, floor_area_mock +) -> None: """Test the extraction using area ID as reference.""" entities = [ MockEntity(name="in_area", entity_id="light.in_area"), @@ -1500,6 +1736,49 @@ async def test_extract_from_service_area_id(hass: HomeAssistant, area_mock) -> N ] +@pytest.mark.usefixtures("label_mock") +async def test_extract_from_service_label_id(hass: HomeAssistant) -> None: + """Test the extraction using label ID as reference.""" + entities = [ + MockEntity(name="with_my_label", entity_id="light.with_my_label"), + MockEntity(name="no_labels", entity_id="light.no_labels"), + MockEntity( + name="with_labels_from_device", entity_id="light.with_labels_from_device" + ), + ] + + call = ServiceCall("light", "turn_on", {"label_id": "label_area"}) + extracted = await service.async_extract_entities(hass, entities, call) + assert len(extracted) == 1 + assert extracted[0].entity_id == "light.with_labels_from_device" + + call = ServiceCall("light", "turn_on", {"label_id": "my-label"}) + extracted = await service.async_extract_entities(hass, entities, call) + assert len(extracted) == 1 + assert extracted[0].entity_id == "light.with_my_label" + + call = ServiceCall("light", "turn_on", {"label_id": ["my-label", "label1"]}) + extracted = await service.async_extract_entities(hass, entities, call) + assert len(extracted) == 2 + assert sorted(ent.entity_id for ent in extracted) == [ + "light.with_labels_from_device", + "light.with_my_label", + ] + + call = ServiceCall( + "light", + "turn_on", + {"label_id": ["my-label", "label1"], "device_id": "device-no-labels"}, + ) + extracted = await service.async_extract_entities(hass, entities, call) + assert len(extracted) == 3 + assert sorted(ent.entity_id for ent in extracted) == [ + "light.no_labels", + "light.with_labels_from_device", + "light.with_my_label", + ] + + async def test_entity_service_call_warn_referenced( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: @@ -1511,12 +1790,15 @@ async def test_entity_service_call_warn_referenced( "area_id": "non-existent-area", "entity_id": "non.existent", "device_id": "non-existent-device", + "floor_id": "non-existent-floor", + "label_id": "non-existent-label", }, ) await service.entity_service_call(hass, {}, "", call) assert ( - "Referenced areas non-existent-area, devices non-existent-device, " - "entities non.existent are missing or not currently available" + "Referenced floors non-existent-floor, areas non-existent-area, " + "devices non-existent-device, entities non.existent, " + "labels non-existent-label are missing or not currently available" ) in caplog.text @@ -1531,13 +1813,16 @@ async def test_async_extract_entities_warn_referenced( "area_id": "non-existent-area", "entity_id": "non.existent", "device_id": "non-existent-device", + "floor_id": "non-existent-floor", + "label_id": "non-existent-label", }, ) extracted = await service.async_extract_entities(hass, {}, call) assert len(extracted) == 0 assert ( - "Referenced areas non-existent-area, devices non-existent-device, " - "entities non.existent are missing or not currently available" + "Referenced floors non-existent-floor, areas non-existent-area, " + "devices non-existent-device, entities non.existent, " + "labels non-existent-label are missing or not currently available" ) in caplog.text diff --git a/tests/helpers/test_significant_change.py b/tests/helpers/test_significant_change.py index 6444781aa85..e930ff30feb 100644 --- a/tests/helpers/test_significant_change.py +++ b/tests/helpers/test_significant_change.py @@ -1,4 +1,5 @@ """Test significant change helper.""" + import pytest from homeassistant.components.sensor import SensorDeviceClass @@ -17,9 +18,9 @@ async def checker_fixture(hass): ): return abs(float(old_state) - float(new_state)) > 4 - hass.data[significant_change.DATA_FUNCTIONS][ - "test_domain" - ] = async_check_significant_change + hass.data[significant_change.DATA_FUNCTIONS]["test_domain"] = ( + async_check_significant_change + ) return checker diff --git a/tests/helpers/test_singleton.py b/tests/helpers/test_singleton.py index 81579202799..dcda1e2db3a 100644 --- a/tests/helpers/test_singleton.py +++ b/tests/helpers/test_singleton.py @@ -1,4 +1,5 @@ """Test singleton helper.""" + from unittest.mock import Mock import pytest @@ -12,7 +13,7 @@ def mock_hass(): return Mock(data={}) -@pytest.mark.parametrize("result", (object(), {}, [])) +@pytest.mark.parametrize("result", [object(), {}, []]) async def test_singleton_async(mock_hass, result) -> None: """Test singleton with async function.""" @@ -28,7 +29,7 @@ async def test_singleton_async(mock_hass, result) -> None: assert mock_hass.data["test_key"] is result1 -@pytest.mark.parametrize("result", (object(), {}, [])) +@pytest.mark.parametrize("result", [object(), {}, []]) def test_singleton(mock_hass, result) -> None: """Test singleton with function.""" diff --git a/tests/helpers/test_start.py b/tests/helpers/test_start.py index d203b336f27..d9c6bbf441c 100644 --- a/tests/helpers/test_start.py +++ b/tests/helpers/test_start.py @@ -1,4 +1,5 @@ """Test starting HA helpers.""" + import pytest from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STARTED diff --git a/tests/helpers/test_state.py b/tests/helpers/test_state.py index 255fba0e7e7..150f31f5fe9 100644 --- a/tests/helpers/test_state.py +++ b/tests/helpers/test_state.py @@ -1,4 +1,5 @@ """Test state helpers.""" + import asyncio from unittest.mock import patch diff --git a/tests/helpers/test_storage.py b/tests/helpers/test_storage.py index 66dd8c10463..0d574e9811f 100644 --- a/tests/helpers/test_storage.py +++ b/tests/helpers/test_storage.py @@ -1,4 +1,5 @@ """Tests for the storage helper.""" + import asyncio from datetime import timedelta import json @@ -12,11 +13,14 @@ import pytest from homeassistant.const import ( EVENT_HOMEASSISTANT_FINAL_WRITE, + EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP, ) from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, CoreState, HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import issue_registry as ir, storage +from homeassistant.helpers.json import json_bytes from homeassistant.util import dt as dt_util from homeassistant.util.color import RGBColor @@ -409,8 +413,9 @@ async def test_migrator_no_existing_config( hass: HomeAssistant, store, hass_storage: dict[str, Any] ) -> None: """Test migrator with no existing config.""" - with patch("os.path.isfile", return_value=False), patch.object( - store, "async_load", return_value={"cur": "config"} + with ( + patch("os.path.isfile", return_value=False), + patch.object(store, "async_load", return_value={"cur": "config"}), ): data = await storage.async_migrator(hass, "old-path", store) @@ -620,10 +625,9 @@ async def test_changing_delayed_written_data( async def test_saving_load_round_trip(tmpdir: py.path.local) -> None: """Test saving and loading round trip.""" - async with async_test_home_assistant() as hass: - hass.config.config_dir = await hass.async_add_executor_job( - tmpdir.mkdir, "temp_storage" - ) + loop = asyncio.get_running_loop() + config_dir = await loop.run_in_executor(None, tmpdir.mkdir, "temp_storage") + async with async_test_home_assistant(config_dir=config_dir) as hass: class NamedTupleSubclass(NamedTuple): """A NamedTuple subclass.""" @@ -667,7 +671,7 @@ async def test_loading_corrupt_core_file( loop = asyncio.get_running_loop() tmp_storage = await loop.run_in_executor(None, tmpdir.mkdir, "temp_storage") - async with async_test_home_assistant(storage_dir=tmp_storage) as hass: + async with async_test_home_assistant(config_dir=tmp_storage) as hass: storage_key = "core.anything" store = storage.Store( hass, MOCK_VERSION_2, storage_key, minor_version=MOCK_MINOR_VERSION_1 @@ -726,7 +730,7 @@ async def test_loading_corrupt_file_known_domain( loop = asyncio.get_running_loop() tmp_storage = await loop.run_in_executor(None, tmpdir.mkdir, "temp_storage") - async with async_test_home_assistant(storage_dir=tmp_storage) as hass: + async with async_test_home_assistant(config_dir=tmp_storage) as hass: hass.config.components.add("testdomain") storage_key = "testdomain.testkey" @@ -781,23 +785,28 @@ async def test_loading_corrupt_file_known_domain( async def test_os_error_is_fatal(tmpdir: py.path.local) -> None: """Test OSError during load is fatal.""" - async with async_test_home_assistant() as hass: - tmp_storage = await hass.async_add_executor_job(tmpdir.mkdir, "temp_storage") - hass.config.config_dir = tmp_storage - + loop = asyncio.get_running_loop() + tmp_storage = await loop.run_in_executor(None, tmpdir.mkdir, "temp_storage") + async with async_test_home_assistant(config_dir=tmp_storage) as hass: store = storage.Store( hass, MOCK_VERSION_2, MOCK_KEY, minor_version=MOCK_MINOR_VERSION_1 ) await store.async_save({"hello": "world"}) - with pytest.raises(OSError), patch( - "homeassistant.helpers.storage.json_util.load_json", side_effect=OSError + with ( + pytest.raises(OSError), + patch( + "homeassistant.helpers.storage.json_util.load_json", side_effect=OSError + ), ): await store.async_load() # Verify second load is also failing - with pytest.raises(OSError), patch( - "homeassistant.helpers.storage.json_util.load_json", side_effect=OSError + with ( + pytest.raises(OSError), + patch( + "homeassistant.helpers.storage.json_util.load_json", side_effect=OSError + ), ): await store.async_load() @@ -806,10 +815,9 @@ async def test_os_error_is_fatal(tmpdir: py.path.local) -> None: async def test_json_load_failure(tmpdir: py.path.local) -> None: """Test json load raising HomeAssistantError.""" - async with async_test_home_assistant() as hass: - tmp_storage = await hass.async_add_executor_job(tmpdir.mkdir, "temp_storage") - hass.config.config_dir = tmp_storage - + loop = asyncio.get_running_loop() + tmp_storage = await loop.run_in_executor(None, tmpdir.mkdir, "temp_storage") + async with async_test_home_assistant(config_dir=tmp_storage) as hass: store = storage.Store( hass, MOCK_VERSION_2, MOCK_KEY, minor_version=MOCK_MINOR_VERSION_1 ) @@ -819,9 +827,12 @@ async def test_json_load_failure(tmpdir: py.path.local) -> None: home_assistant_error = HomeAssistantError() home_assistant_error.__cause__ = base_os_error - with pytest.raises(HomeAssistantError), patch( - "homeassistant.helpers.storage.json_util.load_json", - side_effect=home_assistant_error, + with ( + pytest.raises(HomeAssistantError), + patch( + "homeassistant.helpers.storage.json_util.load_json", + side_effect=home_assistant_error, + ), ): await store.async_load() @@ -850,3 +861,301 @@ async def test_read_only_store( hass.bus.async_fire(EVENT_HOMEASSISTANT_FINAL_WRITE) await hass.async_block_till_done() assert read_only_store.key not in hass_storage + + +async def test_store_manager_caching( + tmpdir: py.path.local, caplog: pytest.LogCaptureFixture +) -> None: + """Test store manager caching.""" + loop = asyncio.get_running_loop() + + def _setup_mock_storage(): + config_dir = tmpdir.mkdir("temp_config") + tmp_storage = config_dir.mkdir(".storage") + tmp_storage.join("integration1").write_binary( + json_bytes({"data": {"integration1": "integration1"}, "version": 1}) + ) + tmp_storage.join("integration2").write_binary( + json_bytes({"data": {"integration2": "integration2"}, "version": 1}) + ) + tmp_storage.join("broken").write_binary(b"invalid") + return config_dir + + config_dir = await loop.run_in_executor(None, _setup_mock_storage) + + async with async_test_home_assistant(config_dir=config_dir) as hass: + store_manager = storage.get_internal_store_manager(hass) + assert ( + store_manager.async_fetch("integration1") is None + ) # has data but not cached + assert ( + store_manager.async_fetch("integration2") is None + ) # has data but not cached + assert ( + store_manager.async_fetch("integration3") is None + ) # no file not but cached + + await store_manager.async_initialize() + assert ( + store_manager.async_fetch("integration1") is None + ) # has data but not cached + assert ( + store_manager.async_fetch("integration2") is None + ) # has data but not cached + assert ( + store_manager.async_fetch("integration3") is not None + ) # no file and initialized + + result = store_manager.async_fetch("integration3") + assert result is not None + exists, data = result + assert exists is False + assert data is None + + await store_manager.async_preload(["integration3", "integration2", "broken"]) + assert "Error loading broken" in caplog.text + + assert ( + store_manager.async_fetch("integration1") is None + ) # has data but not cached + result = store_manager.async_fetch("integration2") + assert result is not None + exists, data = result + assert exists is True + assert data == {"data": {"integration2": "integration2"}, "version": 1} + + assert ( + store_manager.async_fetch("integration3") is not None + ) # no file and initialized + result = store_manager.async_fetch("integration3") + assert result is not None + exists, data = result + assert exists is False + assert data is None + + integration1 = storage.Store(hass, 1, "integration1") + await integration1.async_save({"integration1": "updated"}) + # Save should invalidate the cache + assert store_manager.async_fetch("integration1") is None # invalidated + + integration2 = storage.Store(hass, 1, "integration2") + integration2.async_delay_save(lambda: {"integration2": "updated"}) + # Delay save should invalidate the cache after it saves + assert "integration2" not in store_manager._invalidated + + # Block twice to flush out the delayed save + await hass.async_block_till_done() + await hass.async_block_till_done() + assert store_manager.async_fetch("integration2") is None # invalidated + + store_manager.async_invalidate("integration3") + assert store_manager.async_fetch("integration1") is None # invalidated by save + assert ( + store_manager.async_fetch("integration2") is None + ) # invalidated by delay save + assert store_manager.async_fetch("integration3") is None # invalidated + + await hass.async_stop(force=True) + + async with async_test_home_assistant(config_dir=config_dir) as hass: + store_manager = storage.get_internal_store_manager(hass) + assert store_manager.async_fetch("integration1") is None + assert store_manager.async_fetch("integration2") is None + assert store_manager.async_fetch("integration3") is None + await store_manager.async_initialize() + await store_manager.async_preload(["integration1", "integration2"]) + result = store_manager.async_fetch("integration1") + assert result is not None + exists, data = result + assert exists is True + assert data["data"] == {"integration1": "updated"} + + integration1 = storage.Store(hass, 1, "integration1") + assert await integration1.async_load() == {"integration1": "updated"} + + # Load should pop the cache + assert store_manager.async_fetch("integration1") is None + + integration2 = storage.Store(hass, 1, "integration2") + assert await integration2.async_load() == {"integration2": "updated"} + + # Load should pop the cache + assert store_manager.async_fetch("integration2") is None + + integration3 = storage.Store(hass, 1, "integration3") + assert await integration3.async_load() is None + + await integration3.async_save({"integration3": "updated"}) + assert await integration3.async_load() == {"integration3": "updated"} + + await hass.async_stop(force=True) + + # Now make sure everything still works when we do not + # manually load the storage manager + async with async_test_home_assistant(config_dir=config_dir) as hass: + integration1 = storage.Store(hass, 1, "integration1") + assert await integration1.async_load() == {"integration1": "updated"} + await integration1.async_save({"integration1": "updated2"}) + assert await integration1.async_load() == {"integration1": "updated2"} + + integration2 = storage.Store(hass, 1, "integration2") + assert await integration2.async_load() == {"integration2": "updated"} + await integration2.async_save({"integration2": "updated2"}) + assert await integration2.async_load() == {"integration2": "updated2"} + + await hass.async_stop(force=True) + + # Now remove the stores + async with async_test_home_assistant(config_dir=config_dir) as hass: + store_manager = storage.get_internal_store_manager(hass) + await store_manager.async_initialize() + await store_manager.async_preload(["integration1", "integration2"]) + + integration1 = storage.Store(hass, 1, "integration1") + assert integration1._manager is store_manager + assert await integration1.async_load() == {"integration1": "updated2"} + + integration2 = storage.Store(hass, 1, "integration2") + assert integration2._manager is store_manager + assert await integration2.async_load() == {"integration2": "updated2"} + + await integration1.async_remove() + await integration2.async_remove() + + assert store_manager.async_fetch("integration1") is None + assert store_manager.async_fetch("integration2") is None + + assert await integration1.async_load() is None + assert await integration2.async_load() is None + + await hass.async_stop(force=True) + + # Now make sure the stores are removed and another run works + async with async_test_home_assistant(config_dir=config_dir) as hass: + store_manager = storage.get_internal_store_manager(hass) + await store_manager.async_initialize() + await store_manager.async_preload(["integration1"]) + result = store_manager.async_fetch("integration1") + assert result is not None + exists, data = result + assert exists is False + assert data is None + await hass.async_stop(force=True) + + +async def test_store_manager_sub_dirs(tmpdir: py.path.local) -> None: + """Test store manager ignores subdirs.""" + loop = asyncio.get_running_loop() + + def _setup_mock_storage(): + config_dir = tmpdir.mkdir("temp_config") + sub_dir_storage = config_dir.mkdir(".storage").mkdir("subdir") + + sub_dir_storage.join("integration1").write_binary( + json_bytes({"data": {"integration1": "integration1"}, "version": 1}) + ) + return config_dir + + config_dir = await loop.run_in_executor(None, _setup_mock_storage) + + async with async_test_home_assistant(config_dir=config_dir) as hass: + store_manager = storage.get_internal_store_manager(hass) + await store_manager.async_initialize() + assert store_manager.async_fetch("subdir/integration1") is None + assert store_manager.async_fetch("subdir/integrationx") is None + integration1 = storage.Store(hass, 1, "subdir/integration1") + assert await integration1.async_load() == {"integration1": "integration1"} + await hass.async_stop(force=True) + + +async def test_store_manager_cleanup_after_started( + tmpdir: py.path.local, freezer: FrozenDateTimeFactory +) -> None: + """Test that the cache is cleaned up after startup.""" + loop = asyncio.get_running_loop() + + def _setup_mock_storage(): + config_dir = tmpdir.mkdir("temp_config") + tmp_storage = config_dir.mkdir(".storage") + tmp_storage.join("integration1").write_binary( + json_bytes({"data": {"integration1": "integration1"}, "version": 1}) + ) + tmp_storage.join("integration2").write_binary( + json_bytes({"data": {"integration2": "integration2"}, "version": 1}) + ) + return config_dir + + config_dir = await loop.run_in_executor(None, _setup_mock_storage) + + async with async_test_home_assistant(config_dir=config_dir) as hass: + hass.set_state(CoreState.not_running) + store_manager = storage.get_internal_store_manager(hass) + await store_manager.async_initialize() + await store_manager.async_preload(["integration1", "integration2"]) + assert "integration1" in store_manager._data_preload + assert "integration2" in store_manager._data_preload + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + assert "integration1" in store_manager._data_preload + assert "integration2" in store_manager._data_preload + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + assert "integration1" in store_manager._data_preload + assert "integration2" in store_manager._data_preload + freezer.tick(storage.MANAGER_CLEANUP_DELAY) + async_fire_time_changed(hass) + await hass.async_block_till_done() + # The cache should be removed after the cleanup delay + # since it means nothing ever loaded it and we want to + # recover the memory + assert "integration1" not in store_manager._data_preload + assert "integration2" not in store_manager._data_preload + assert store_manager.async_fetch("integration1") is None + assert store_manager.async_fetch("integration2") is None + await hass.async_stop(force=True) + + +async def test_store_manager_cleanup_after_stop( + tmpdir: py.path.local, freezer: FrozenDateTimeFactory +) -> None: + """Test that the cache is cleaned up after stop event. + + This should only happen if we stop within the cleanup delay. + """ + loop = asyncio.get_running_loop() + + def _setup_mock_storage(): + config_dir = tmpdir.mkdir("temp_config") + tmp_storage = config_dir.mkdir(".storage") + tmp_storage.join("integration1").write_binary( + json_bytes({"data": {"integration1": "integration1"}, "version": 1}) + ) + tmp_storage.join("integration2").write_binary( + json_bytes({"data": {"integration2": "integration2"}, "version": 1}) + ) + return config_dir + + config_dir = await loop.run_in_executor(None, _setup_mock_storage) + + async with async_test_home_assistant(config_dir=config_dir) as hass: + hass.set_state(CoreState.not_running) + store_manager = storage.get_internal_store_manager(hass) + await store_manager.async_initialize() + await store_manager.async_preload(["integration1", "integration2"]) + assert "integration1" in store_manager._data_preload + assert "integration2" in store_manager._data_preload + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + assert "integration1" in store_manager._data_preload + assert "integration2" in store_manager._data_preload + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + assert "integration1" in store_manager._data_preload + assert "integration2" in store_manager._data_preload + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + assert "integration1" not in store_manager._data_preload + assert "integration2" not in store_manager._data_preload + assert store_manager.async_fetch("integration1") is None + assert store_manager.async_fetch("integration2") is None + await hass.async_stop(force=True) diff --git a/tests/helpers/test_storage_remove.py b/tests/helpers/test_storage_remove.py index affa3429af3..c5d0b4be921 100644 --- a/tests/helpers/test_storage_remove.py +++ b/tests/helpers/test_storage_remove.py @@ -1,4 +1,5 @@ """Tests for the storage helper with minimal mocking.""" + from datetime import timedelta import os from unittest.mock import patch diff --git a/tests/helpers/test_system_info.py b/tests/helpers/test_system_info.py index 5c3697ad936..16b5b8b652b 100644 --- a/tests/helpers/test_system_info.py +++ b/tests/helpers/test_system_info.py @@ -1,10 +1,12 @@ """Tests for the system info helper.""" + import json import os from unittest.mock import patch import pytest +from homeassistant.components import hassio from homeassistant.const import __version__ as current_version from homeassistant.core import HomeAssistant from homeassistant.helpers.system_info import async_get_system_info, is_official_image @@ -34,13 +36,14 @@ async def test_get_system_info_supervisor_not_available( ) -> None: """Test the get system info when supervisor is not available.""" hass.config.components.add("hassio") - with patch("platform.system", return_value="Linux"), patch( - "homeassistant.helpers.system_info.is_docker_env", return_value=True - ), patch( - "homeassistant.helpers.system_info.is_official_image", return_value=True - ), patch("homeassistant.components.hassio.is_hassio", return_value=True), patch( - "homeassistant.components.hassio.get_info", return_value=None - ), patch("homeassistant.helpers.system_info.cached_get_user", return_value="root"): + with ( + patch("platform.system", return_value="Linux"), + patch("homeassistant.helpers.system_info.is_docker_env", return_value=True), + patch("homeassistant.helpers.system_info.is_official_image", return_value=True), + patch.object(hassio, "is_hassio", return_value=True), + patch.object(hassio, "get_info", return_value=None), + patch("homeassistant.helpers.system_info.cached_get_user", return_value="root"), + ): info = await async_get_system_info(hass) assert isinstance(info, dict) assert info["version"] == current_version @@ -52,12 +55,12 @@ async def test_get_system_info_supervisor_not_available( async def test_get_system_info_supervisor_not_loaded(hass: HomeAssistant) -> None: """Test the get system info when supervisor is not loaded.""" - with patch("platform.system", return_value="Linux"), patch( - "homeassistant.helpers.system_info.is_docker_env", return_value=True - ), patch( - "homeassistant.helpers.system_info.is_official_image", return_value=True - ), patch("homeassistant.components.hassio.get_info", return_value=None), patch.dict( - os.environ, {"SUPERVISOR": "127.0.0.1"} + with ( + patch("platform.system", return_value="Linux"), + patch("homeassistant.helpers.system_info.is_docker_env", return_value=True), + patch("homeassistant.helpers.system_info.is_official_image", return_value=True), + patch.object(hassio, "get_info", return_value=None), + patch.dict(os.environ, {"SUPERVISOR": "127.0.0.1"}), ): info = await async_get_system_info(hass) assert isinstance(info, dict) @@ -69,19 +72,23 @@ async def test_get_system_info_supervisor_not_loaded(hass: HomeAssistant) -> Non async def test_container_installationtype(hass: HomeAssistant) -> None: """Test container installation type.""" - with patch("platform.system", return_value="Linux"), patch( - "homeassistant.helpers.system_info.is_docker_env", return_value=True - ), patch( - "homeassistant.helpers.system_info.is_official_image", return_value=True - ), patch("homeassistant.helpers.system_info.cached_get_user", return_value="root"): + with ( + patch("platform.system", return_value="Linux"), + patch("homeassistant.helpers.system_info.is_docker_env", return_value=True), + patch("homeassistant.helpers.system_info.is_official_image", return_value=True), + patch("homeassistant.helpers.system_info.cached_get_user", return_value="root"), + ): info = await async_get_system_info(hass) assert info["installation_type"] == "Home Assistant Container" - with patch("platform.system", return_value="Linux"), patch( - "homeassistant.helpers.system_info.is_docker_env", return_value=True - ), patch( - "homeassistant.helpers.system_info.is_official_image", return_value=False - ), patch("homeassistant.helpers.system_info.cached_get_user", return_value="user"): + with ( + patch("platform.system", return_value="Linux"), + patch("homeassistant.helpers.system_info.is_docker_env", return_value=True), + patch( + "homeassistant.helpers.system_info.is_official_image", return_value=False + ), + patch("homeassistant.helpers.system_info.cached_get_user", return_value="user"), + ): info = await async_get_system_info(hass) assert info["installation_type"] == "Unsupported Third Party Container" diff --git a/tests/helpers/test_temperature.py b/tests/helpers/test_temperature.py index ceb4f7bdef2..a5daeac2a6a 100644 --- a/tests/helpers/test_temperature.py +++ b/tests/helpers/test_temperature.py @@ -1,4 +1,5 @@ """Tests Home Assistant temperature helpers.""" + import pytest from homeassistant.const import ( diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index ab6d61bfe6c..54fdf0368eb 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -1,4 +1,5 @@ """Test Home Assistant template helper methods.""" + from __future__ import annotations from collections.abc import Iterable @@ -37,6 +38,9 @@ from homeassistant.helpers import ( device_registry as dr, entity, entity_registry as er, + floor_registry as fr, + issue_registry as ir, + label_registry as lr, template, translation, ) @@ -1138,14 +1142,14 @@ def test_timestamp_local(hass: HomeAssistant) -> None: @pytest.mark.parametrize( "input", - ( + [ "2021-06-03 13:00:00.000000+00:00", "1986-07-09T12:00:00Z", "2016-10-19 15:22:05.588122+0100", "2016-10-19", "2021-01-01 00:00:01", "invalid", - ), + ], ) def test_as_datetime(hass: HomeAssistant, input) -> None: """Test converting a timestamp string to a date object.""" @@ -1464,11 +1468,11 @@ def test_max(hass: HomeAssistant) -> None: @pytest.mark.parametrize( "attribute", - ( + [ "a", "b", "c", - ), + ], ) def test_min_max_attribute(hass: HomeAssistant, attribute) -> None: """Test the min and max filters with attribute.""" @@ -3279,6 +3283,16 @@ async def test_integration_entities( hass: HomeAssistant, entity_registry: er.EntityRegistry ) -> None: """Test integration_entities function.""" + # test entities for untitled config entry + config_entry = MockConfigEntry(domain="mock", title="") + config_entry.add_to_hass(hass) + entity_registry.async_get_or_create( + "sensor", "mock", "untitled", config_entry=config_entry + ) + info = render_to_info(hass, "{{ integration_entities('') }}") + assert_result_info(info, []) + assert info.rate_limit is None + # test entities for given config entry title config_entry = MockConfigEntry(domain="mock", title="Mock bridge 2") config_entry.add_to_hass(hass) @@ -3289,6 +3303,23 @@ async def test_integration_entities( assert_result_info(info, [entity_entry.entity_id]) assert info.rate_limit is None + # test entities for given non unique config entry title + config_entry = MockConfigEntry(domain="mock", title="Not unique") + config_entry.add_to_hass(hass) + entity_entry_not_unique_1 = entity_registry.async_get_or_create( + "sensor", "mock", "not_unique_1", config_entry=config_entry + ) + config_entry = MockConfigEntry(domain="mock", title="Not unique") + config_entry.add_to_hass(hass) + entity_entry_not_unique_2 = entity_registry.async_get_or_create( + "sensor", "mock", "not_unique_2", config_entry=config_entry + ) + info = render_to_info(hass, "{{ integration_entities('Not unique') }}") + assert_result_info( + info, [entity_entry_not_unique_1.entity_id, entity_entry_not_unique_2.entity_id] + ) + assert info.rate_limit is None + # test integration entities not in entity registry mock_entity = entity.Entity() mock_entity.hass = hass @@ -3512,6 +3543,67 @@ async def test_device_attr( assert info.rate_limit is None +async def test_issues(hass: HomeAssistant, issue_registry: ir.IssueRegistry) -> None: + """Test issues function.""" + # Test no issues + info = render_to_info(hass, "{{ issues() }}") + assert_result_info(info, {}) + assert info.rate_limit is None + + # Test persistent issue + ir.async_create_issue( + hass, + "test", + "issue 1", + breaks_in_ha_version="2023.7", + is_fixable=True, + is_persistent=True, + learn_more_url="https://theuselessweb.com", + severity="error", + translation_key="abc_1234", + translation_placeholders={"abc": "123"}, + ) + await hass.async_block_till_done() + created_issue = issue_registry.async_get_issue("test", "issue 1") + info = render_to_info(hass, "{{ issues()['test', 'issue 1'] }}") + assert_result_info(info, created_issue.to_json()) + assert info.rate_limit is None + + # Test fixed issue + ir.async_delete_issue(hass, "test", "issue 1") + await hass.async_block_till_done() + info = render_to_info(hass, "{{ issues() }}") + assert_result_info(info, {}) + assert info.rate_limit is None + + +async def test_issue(hass: HomeAssistant, issue_registry: ir.IssueRegistry) -> None: + """Test issue function.""" + # Test non existent issue + info = render_to_info(hass, "{{ issue('non_existent', 'issue') }}") + assert_result_info(info, None) + assert info.rate_limit is None + + # Test existing issue + ir.async_create_issue( + hass, + "test", + "issue 1", + breaks_in_ha_version="2023.7", + is_fixable=True, + is_persistent=True, + learn_more_url="https://theuselessweb.com", + severity="error", + translation_key="abc_1234", + translation_placeholders={"abc": "123"}, + ) + await hass.async_block_till_done() + created_issue = issue_registry.async_get_issue("test", "issue 1") + info = render_to_info(hass, "{{ issue('test', 'issue 1') }}") + assert_result_info(info, created_issue.to_json()) + assert info.rate_limit is None + + async def test_areas(hass: HomeAssistant, area_registry: ar.AreaRegistry) -> None: """Test areas function.""" # Test no areas @@ -5077,3 +5169,577 @@ async def test_lru_increases_with_many_entities(hass: HomeAssistant) -> None: assert template.CACHED_TEMPLATE_NO_COLLECT_LRU.get_size() == int( round(mock_entity_count * template.ENTITY_COUNT_GROWTH_FACTOR) ) + + +async def test_floors( + hass: HomeAssistant, + floor_registry: fr.FloorRegistry, +) -> None: + """Test floors function.""" + + # Test no floors + info = render_to_info(hass, "{{ floors() }}") + assert_result_info(info, []) + assert info.rate_limit is None + + # Test one floor + floor1 = floor_registry.async_create("First floor") + info = render_to_info(hass, "{{ floors() }}") + assert_result_info(info, [floor1.floor_id]) + assert info.rate_limit is None + + # Test multiple floors + floor2 = floor_registry.async_create("Second floor") + info = render_to_info(hass, "{{ floors() }}") + assert_result_info(info, [floor1.floor_id, floor2.floor_id]) + assert info.rate_limit is None + + +async def test_floor_id( + hass: HomeAssistant, + floor_registry: fr.FloorRegistry, + area_registry: ar.AreaRegistry, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, +) -> None: + """Test floor_id function.""" + + def test(value: str, expected: str | None) -> None: + info = render_to_info(hass, f"{{{{ floor_id('{value}') }}}}") + assert_result_info(info, expected) + assert info.rate_limit is None + + info = render_to_info(hass, f"{{{{ '{value}' | floor_id }}}}") + assert_result_info(info, expected) + assert info.rate_limit is None + + # Test non existing floor name + test("Third floor", None) + + # Test wrong value type + info = render_to_info(hass, "{{ floor_id(42) }}") + assert_result_info(info, None) + assert info.rate_limit is None + + info = render_to_info(hass, "{{ 42 | floor_id }}") + assert_result_info(info, None) + assert info.rate_limit is None + + # Test with an actual floor + floor = floor_registry.async_create("First floor") + test("First floor", floor.floor_id) + + config_entry = MockConfigEntry(domain="light") + config_entry.add_to_hass(hass) + area_entry_hex = area_registry.async_get_or_create("123abc") + + # Create area, device, entity and assign area to device and entity + device_entry = device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_entry = entity_registry.async_get_or_create( + "light", + "hue", + "5678", + config_entry=config_entry, + device_id=device_entry.id, + ) + device_entry = device_registry.async_update_device( + device_entry.id, area_id=area_entry_hex.id + ) + entity_entry = entity_registry.async_update_entity( + entity_entry.entity_id, area_id=area_entry_hex.id + ) + + test(area_entry_hex.id, None) + test(device_entry.id, None) + test(entity_entry.entity_id, None) + + # Add floor to area + area_entry_hex = area_registry.async_update( + area_entry_hex.id, floor_id=floor.floor_id + ) + + test(area_entry_hex.id, floor.floor_id) + test(device_entry.id, floor.floor_id) + test(entity_entry.entity_id, floor.floor_id) + + +async def test_floor_name( + hass: HomeAssistant, + floor_registry: fr.FloorRegistry, + area_registry: ar.AreaRegistry, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, +) -> None: + """Test floor_name function.""" + + def test(value: str, expected: str | None) -> None: + info = render_to_info(hass, f"{{{{ floor_name('{value}') }}}}") + assert_result_info(info, expected) + assert info.rate_limit is None + + info = render_to_info(hass, f"{{{{ '{value}' | floor_name }}}}") + assert_result_info(info, expected) + assert info.rate_limit is None + + # Test non existing floor name + test("Third floor", None) + + # Test wrong value type + info = render_to_info(hass, "{{ floor_name(42) }}") + assert_result_info(info, None) + assert info.rate_limit is None + + info = render_to_info(hass, "{{ 42 | floor_name }}") + assert_result_info(info, None) + assert info.rate_limit is None + + # Test existing floor ID + floor = floor_registry.async_create("First floor") + test(floor.floor_id, floor.name) + + config_entry = MockConfigEntry(domain="light") + config_entry.add_to_hass(hass) + area_entry_hex = area_registry.async_get_or_create("123abc") + + # Create area, device, entity and assign area to device and entity + device_entry = device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_entry = entity_registry.async_get_or_create( + "light", + "hue", + "5678", + config_entry=config_entry, + device_id=device_entry.id, + ) + device_entry = device_registry.async_update_device( + device_entry.id, area_id=area_entry_hex.id + ) + entity_entry = entity_registry.async_update_entity( + entity_entry.entity_id, area_id=area_entry_hex.id + ) + + test(area_entry_hex.id, None) + test(device_entry.id, None) + test(entity_entry.entity_id, None) + + # Add floor to area + area_entry_hex = area_registry.async_update( + area_entry_hex.id, floor_id=floor.floor_id + ) + + test(area_entry_hex.id, floor.name) + test(device_entry.id, floor.name) + test(entity_entry.entity_id, floor.name) + + +async def test_floor_areas( + hass: HomeAssistant, + floor_registry: fr.FloorRegistry, + area_registry: ar.AreaRegistry, +) -> None: + """Test floor_areas function.""" + + # Test non existing floor ID + info = render_to_info(hass, "{{ floor_areas('skyring') }}") + assert_result_info(info, []) + assert info.rate_limit is None + + info = render_to_info(hass, "{{ 'skyring' | floor_areas }}") + assert_result_info(info, []) + assert info.rate_limit is None + + # Test wrong value type + info = render_to_info(hass, "{{ floor_areas(42) }}") + assert_result_info(info, []) + assert info.rate_limit is None + + info = render_to_info(hass, "{{ 42 | floor_areas }}") + assert_result_info(info, []) + assert info.rate_limit is None + + floor = floor_registry.async_create("First floor") + area = area_registry.async_create("Living room") + area_registry.async_update(area.id, floor_id=floor.floor_id) + + # Get areas by floor ID + info = render_to_info(hass, f"{{{{ floor_areas('{floor.floor_id}') }}}}") + assert_result_info(info, [area.id]) + assert info.rate_limit is None + + info = render_to_info(hass, f"{{{{ '{floor.floor_id}' | floor_areas }}}}") + assert_result_info(info, [area.id]) + assert info.rate_limit is None + + # Get entities by floor name + info = render_to_info(hass, f"{{{{ floor_areas('{floor.name}') }}}}") + assert_result_info(info, [area.id]) + assert info.rate_limit is None + + info = render_to_info(hass, f"{{{{ '{floor.name}' | floor_areas }}}}") + assert_result_info(info, [area.id]) + assert info.rate_limit is None + + +async def test_labels( + hass: HomeAssistant, + label_registry: lr.LabelRegistry, + area_registry: ar.AreaRegistry, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, +) -> None: + """Test labels function.""" + + # Test no labels + info = render_to_info(hass, "{{ labels() }}") + assert_result_info(info, []) + assert info.rate_limit is None + + # Test one label + label1 = label_registry.async_create("label1") + info = render_to_info(hass, "{{ labels() }}") + assert_result_info(info, [label1.label_id]) + assert info.rate_limit is None + + # Test multiple label + label2 = label_registry.async_create("label2") + info = render_to_info(hass, "{{ labels() }}") + assert_result_info(info, [label1.label_id, label2.label_id]) + assert info.rate_limit is None + + # Test non-exsting entity ID + info = render_to_info(hass, "{{ labels('sensor.fake') }}") + assert_result_info(info, []) + assert info.rate_limit is None + + info = render_to_info(hass, "{{ 'sensor.fake' | labels }}") + assert_result_info(info, []) + assert info.rate_limit is None + + # Test non existing device ID (hex value) + info = render_to_info(hass, "{{ labels('123abc') }}") + assert_result_info(info, []) + assert info.rate_limit is None + + info = render_to_info(hass, "{{ '123abc' | labels }}") + assert_result_info(info, []) + assert info.rate_limit is None + + # Create a device & entity for testing + config_entry = MockConfigEntry(domain="light") + config_entry.add_to_hass(hass) + device_entry = device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_entry = entity_registry.async_get_or_create( + "light", + "hue", + "5678", + config_entry=config_entry, + device_id=device_entry.id, + ) + + # Test entity, which has no labels + info = render_to_info(hass, f"{{{{ labels('{entity_entry.entity_id}') }}}}") + assert_result_info(info, []) + assert info.rate_limit is None + + info = render_to_info(hass, f"{{{{ '{entity_entry.entity_id}' | labels }}}}") + assert_result_info(info, []) + assert info.rate_limit is None + + # Test device, which has no labels + info = render_to_info(hass, f"{{{{ labels('{device_entry.id}') }}}}") + assert_result_info(info, []) + assert info.rate_limit is None + + info = render_to_info(hass, f"{{{{ '{device_entry.id}' | labels }}}}") + assert_result_info(info, []) + assert info.rate_limit is None + + # Add labels to the entity & device + device_entry = device_registry.async_update_device( + device_entry.id, labels=[label1.label_id] + ) + entity_entry = entity_registry.async_update_entity( + entity_entry.entity_id, labels=[label2.label_id] + ) + + # Test entity, which now has a label + info = render_to_info(hass, f"{{{{ '{entity_entry.entity_id}' | labels }}}}") + assert_result_info(info, [label2.label_id]) + assert info.rate_limit is None + + info = render_to_info(hass, f"{{{{ labels('{entity_entry.entity_id}') }}}}") + assert_result_info(info, [label2.label_id]) + assert info.rate_limit is None + + # Test device, which now has a label + info = render_to_info(hass, f"{{{{ '{device_entry.id}' | labels }}}}") + assert_result_info(info, [label1.label_id]) + assert info.rate_limit is None + + info = render_to_info(hass, f"{{{{ labels('{device_entry.id}') }}}}") + assert_result_info(info, [label1.label_id]) + assert info.rate_limit is None + + # Create area for testing + area = area_registry.async_create("living room") + + # Test area, which has no labels + info = render_to_info(hass, f"{{{{ '{area.id}' | labels }}}}") + assert_result_info(info, []) + assert info.rate_limit is None + + info = render_to_info(hass, f"{{{{ labels('{area.id}') }}}}") + assert_result_info(info, []) + assert info.rate_limit is None + + # Add label to the area + area_registry.async_update(area.id, labels=[label1.label_id, label2.label_id]) + + # Test area, which now has labels + info = render_to_info(hass, f"{{{{ '{area.id}' | labels }}}}") + assert_result_info(info, [label1.label_id, label2.label_id]) + assert info.rate_limit is None + + info = render_to_info(hass, f"{{{{ labels('{area.id}') }}}}") + assert_result_info(info, [label1.label_id, label2.label_id]) + assert info.rate_limit is None + + +async def test_label_id( + hass: HomeAssistant, + label_registry: lr.LabelRegistry, +) -> None: + """Test label_id function.""" + # Test non existing label name + info = render_to_info(hass, "{{ label_id('non-existing label') }}") + assert_result_info(info, None) + assert info.rate_limit is None + + info = render_to_info(hass, "{{ 'non-existing label' | label_id }}") + assert_result_info(info, None) + assert info.rate_limit is None + + # Test wrong value type + info = render_to_info(hass, "{{ label_id(42) }}") + assert_result_info(info, None) + assert info.rate_limit is None + + info = render_to_info(hass, "{{ 42 | label_id }}") + assert_result_info(info, None) + assert info.rate_limit is None + + # Test with an actual label + label = label_registry.async_create("existing label") + info = render_to_info(hass, "{{ label_id('existing label') }}") + assert_result_info(info, label.label_id) + assert info.rate_limit is None + + info = render_to_info(hass, "{{ 'existing label' | label_id }}") + assert_result_info(info, label.label_id) + assert info.rate_limit is None + + +async def test_label_name( + hass: HomeAssistant, + label_registry: lr.LabelRegistry, +) -> None: + """Test label_name function.""" + # Test non existing label ID + info = render_to_info(hass, "{{ label_name('1234567890') }}") + assert_result_info(info, None) + assert info.rate_limit is None + + info = render_to_info(hass, "{{ '1234567890' | label_name }}") + assert_result_info(info, None) + assert info.rate_limit is None + + # Test wrong value type + info = render_to_info(hass, "{{ label_name(42) }}") + assert_result_info(info, None) + assert info.rate_limit is None + + info = render_to_info(hass, "{{ 42 | label_name }}") + assert_result_info(info, None) + assert info.rate_limit is None + + # Test non existing label ID + label = label_registry.async_create("choo choo") + info = render_to_info(hass, f"{{{{ label_name('{label.label_id}') }}}}") + assert_result_info(info, label.name) + assert info.rate_limit is None + + info = render_to_info(hass, f"{{{{ '{label.label_id}' | label_name }}}}") + assert_result_info(info, label.name) + assert info.rate_limit is None + + +async def test_label_entities( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + label_registry: lr.LabelRegistry, +) -> None: + """Test label_entities function.""" + + # Test non existing device ID + info = render_to_info(hass, "{{ label_entities('deadbeef') }}") + assert_result_info(info, []) + assert info.rate_limit is None + + info = render_to_info(hass, "{{ 'deadbeef' | label_entities }}") + assert_result_info(info, []) + assert info.rate_limit is None + + # Test wrong value type + info = render_to_info(hass, "{{ label_entities(42) }}") + assert_result_info(info, []) + assert info.rate_limit is None + + info = render_to_info(hass, "{{ 42 | label_entities }}") + assert_result_info(info, []) + assert info.rate_limit is None + + # Create a fake config entry with a entity + config_entry = MockConfigEntry(domain="light") + config_entry.add_to_hass(hass) + entity_entry = entity_registry.async_get_or_create( + "light", + "hue", + "5678", + config_entry=config_entry, + ) + + # Add a label to the entity + label = label_registry.async_create("Romantic Lights") + entity_registry.async_update_entity(entity_entry.entity_id, labels={label.label_id}) + + # Get entities by label ID + info = render_to_info(hass, f"{{{{ label_entities('{label.label_id}') }}}}") + assert_result_info(info, ["light.hue_5678"]) + assert info.rate_limit is None + + info = render_to_info(hass, f"{{{{ '{label.label_id}' | label_entities }}}}") + assert_result_info(info, ["light.hue_5678"]) + assert info.rate_limit is None + + # Get entities by label name + info = render_to_info(hass, f"{{{{ label_entities('{label.name}') }}}}") + assert_result_info(info, ["light.hue_5678"]) + assert info.rate_limit is None + + info = render_to_info(hass, f"{{{{ '{label.name}' | label_entities }}}}") + assert_result_info(info, ["light.hue_5678"]) + assert info.rate_limit is None + + +async def test_label_devices( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + label_registry: ar.AreaRegistry, +) -> None: + """Test label_devices function.""" + + # Test non existing device ID + info = render_to_info(hass, "{{ label_devices('deadbeef') }}") + assert_result_info(info, []) + assert info.rate_limit is None + + info = render_to_info(hass, "{{ 'deadbeef' | label_devices }}") + assert_result_info(info, []) + assert info.rate_limit is None + + # Test wrong value type + info = render_to_info(hass, "{{ label_devices(42) }}") + assert_result_info(info, []) + assert info.rate_limit is None + + info = render_to_info(hass, "{{ 42 | label_devices }}") + assert_result_info(info, []) + assert info.rate_limit is None + + # Create a fake config entry with a device + config_entry = MockConfigEntry(domain="light") + config_entry.add_to_hass(hass) + device_entry = device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + + # Add a label to it + label = label_registry.async_create("Romantic Lights") + device_registry.async_update_device(device_entry.id, labels=[label.label_id]) + + # Get the devices from a label by its ID + info = render_to_info(hass, f"{{{{ label_devices('{label.label_id}') }}}}") + assert_result_info(info, [device_entry.id]) + assert info.rate_limit is None + + info = render_to_info(hass, f"{{{{ '{label.label_id}' | label_devices }}}}") + assert_result_info(info, [device_entry.id]) + assert info.rate_limit is None + + # Get the devices from a label by its name + info = render_to_info(hass, f"{{{{ label_devices('{label.name}') }}}}") + assert_result_info(info, [device_entry.id]) + assert info.rate_limit is None + + info = render_to_info(hass, f"{{{{ '{label.name}' | label_devices }}}}") + assert_result_info(info, [device_entry.id]) + assert info.rate_limit is None + + +async def test_label_areas( + hass: HomeAssistant, + area_registry: ar.AreaRegistry, + label_registry: lr.LabelRegistry, +) -> None: + """Test label_areas function.""" + + # Test non existing area ID + info = render_to_info(hass, "{{ label_areas('deadbeef') }}") + assert_result_info(info, []) + assert info.rate_limit is None + + info = render_to_info(hass, "{{ 'deadbeef' | label_areas }}") + assert_result_info(info, []) + assert info.rate_limit is None + + # Test wrong value type + info = render_to_info(hass, "{{ label_areas(42) }}") + assert_result_info(info, []) + assert info.rate_limit is None + + info = render_to_info(hass, "{{ 42 | label_areas }}") + assert_result_info(info, []) + assert info.rate_limit is None + + # Create an area with an label + label = label_registry.async_create("Upstairs") + master_bedroom = area_registry.async_create( + "Master Bedroom", labels=[label.label_id] + ) + + # Get areas by label ID + info = render_to_info(hass, f"{{{{ label_areas('{label.label_id}') }}}}") + assert_result_info(info, [master_bedroom.id]) + assert info.rate_limit is None + + info = render_to_info(hass, f"{{{{ '{label.label_id}' | label_areas }}}}") + assert_result_info(info, [master_bedroom.id]) + assert info.rate_limit is None + + # Get areas by label name + info = render_to_info(hass, f"{{{{ label_areas('{label.name}') }}}}") + assert_result_info(info, [master_bedroom.id]) + assert info.rate_limit is None + + info = render_to_info(hass, f"{{{{ '{label.name}' | label_areas }}}}") + assert_result_info(info, [master_bedroom.id]) + assert info.rate_limit is None diff --git a/tests/helpers/test_translation.py b/tests/helpers/test_translation.py index 210378c5812..1bba23c51a1 100644 --- a/tests/helpers/test_translation.py +++ b/tests/helpers/test_translation.py @@ -1,4 +1,5 @@ """Test the translation helper.""" + import asyncio from os import path import pathlib @@ -111,7 +112,7 @@ def test__load_translations_files_by_language( @pytest.mark.parametrize( ("language", "expected_translation", "expected_errors"), - ( + [ ( "en", { @@ -153,7 +154,7 @@ def test__load_translations_files_by_language( "component.test.entity.switch.outlet.name", ], ), - ), + ], ) async def test_load_translations_files_invalid_localized_placeholders( hass: HomeAssistant, @@ -221,15 +222,19 @@ async def test_get_translations_loads_config_flows( integration = Mock(file_path=pathlib.Path(__file__)) integration.name = "Component 1" - with patch( - "homeassistant.helpers.translation.component_translation_path", - return_value="bla.json", - ), patch( - "homeassistant.helpers.translation._load_translations_files_by_language", - return_value={"en": {"component1": {"title": "world"}}}, - ), patch( - "homeassistant.helpers.translation.async_get_integrations", - return_value={"component1": integration}, + with ( + patch( + "homeassistant.helpers.translation.component_translation_path", + return_value="bla.json", + ), + patch( + "homeassistant.helpers.translation._load_translations_files_by_language", + return_value={"en": {"component1": {"title": "world"}}}, + ), + patch( + "homeassistant.helpers.translation.async_get_integrations", + return_value={"component1": integration}, + ), ): translations = await translation.async_get_translations( hass, "en", "title", config_flow=True @@ -250,15 +255,19 @@ async def test_get_translations_loads_config_flows( integration = Mock(file_path=pathlib.Path(__file__)) integration.name = "Component 2" - with patch( - "homeassistant.helpers.translation.component_translation_path", - return_value="bla.json", - ), patch( - "homeassistant.helpers.translation._load_translations_files_by_language", - return_value={"en": {"component2": {"title": "world"}}}, - ), patch( - "homeassistant.helpers.translation.async_get_integrations", - return_value={"component2": integration}, + with ( + patch( + "homeassistant.helpers.translation.component_translation_path", + return_value="bla.json", + ), + patch( + "homeassistant.helpers.translation._load_translations_files_by_language", + return_value={"en": {"component2": {"title": "world"}}}, + ), + patch( + "homeassistant.helpers.translation.async_get_integrations", + return_value={"component2": integration}, + ), ): translations = await translation.async_get_translations( hass, "en", "title", config_flow=True @@ -300,15 +309,19 @@ async def test_get_translations_while_loading_components(hass: HomeAssistant) -> return {language: {"component1": {"title": "world"}} for language in files} - with patch( - "homeassistant.helpers.translation.component_translation_path", - return_value="bla.json", - ), patch( - "homeassistant.helpers.translation._load_translations_files_by_language", - mock_load_translation_files, - ), patch( - "homeassistant.helpers.translation.async_get_integrations", - return_value={"component1": integration}, + with ( + patch( + "homeassistant.helpers.translation.component_translation_path", + return_value="bla.json", + ), + patch( + "homeassistant.helpers.translation._load_translations_files_by_language", + mock_load_translation_files, + ), + patch( + "homeassistant.helpers.translation.async_get_integrations", + return_value={"component1": integration}, + ), ): tasks = [ translation.async_get_translations(hass, "en", "title") for _ in range(5) @@ -335,20 +348,57 @@ async def test_get_translation_categories(hass: HomeAssistant) -> None: assert "component.light.device_automation.action_type.turn_on" in translations -async def test_translation_merging( +async def test_legacy_platform_translations_not_used_built_in_integrations( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: - """Test we merge translations of two integrations.""" + """Test legacy platform translations are not used for built-in integrations.""" hass.config.components.add("moon.sensor") hass.config.components.add("sensor") + load_requests = [] + + def mock_load_translations_files_by_language(files): + load_requests.append(files) + return {} + + with patch( + "homeassistant.helpers.translation._load_translations_files_by_language", + mock_load_translations_files_by_language, + ): + await translation.async_get_translations(hass, "en", "state") + + assert len(load_requests) == 1 + to_load = load_requests[0] + assert len(to_load) == 1 + en_load = to_load["en"] + assert len(en_load) == 1 + assert "sensor" in en_load + assert "moon.sensor" not in en_load + + +async def test_translation_merging_custom_components( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + enable_custom_integrations: None, +) -> None: + """Test we merge translations of two integrations. + + Legacy state translations only used for custom integrations. + """ + hass.config.components.add("test_legacy_state_translations.sensor") + hass.config.components.add("sensor") + orig_load_translations = translation._load_translations_files_by_language def mock_load_translations_files(files): """Mock loading.""" result = orig_load_translations(files) - result["en"]["moon.sensor"] = { - "state": {"moon__phase": {"first_quarter": "First Quarter"}} + result["en"]["test_legacy_state_translations.sensor"] = { + "state": { + "test_legacy_state_translations__phase": { + "first_quarter": "First Quarter" + } + } } return result @@ -358,15 +408,20 @@ async def test_translation_merging( ): translations = await translation.async_get_translations(hass, "en", "state") - assert "component.sensor.state.moon__phase.first_quarter" in translations + assert ( + "component.sensor.state.test_legacy_state_translations__phase.first_quarter" + in translations + ) - hass.config.components.add("season.sensor") + hass.config.components.add("test_legacy_state_translations_bad_data.sensor") # Patch in some bad translation data def mock_load_bad_translations_files(files): """Mock loading.""" result = orig_load_translations(files) - result["en"]["season.sensor"] = {"state": "bad data"} + result["en"]["test_legacy_state_translations_bad_data.sensor"] = { + "state": "bad data" + } return result with patch( @@ -375,7 +430,10 @@ async def test_translation_merging( ): translations = await translation.async_get_translations(hass, "en", "state") - assert "component.sensor.state.moon__phase.first_quarter" in translations + assert ( + "component.sensor.state.test_legacy_state_translations__phase.first_quarter" + in translations + ) assert ( "An integration providing translations for sensor provided invalid data:" @@ -383,17 +441,26 @@ async def test_translation_merging( ) in caplog.text -async def test_translation_merging_loaded_apart( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture +async def test_translation_merging_loaded_apart_custom_integrations( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + enable_custom_integrations: None, ) -> None: - """Test we merge translations of two integrations when they are not loaded at the same time.""" + """Test we merge translations of two integrations when they are not loaded at the same time. + + Legacy state translations only used for custom integrations. + """ orig_load_translations = translation._load_translations_files_by_language def mock_load_translations_files(files): """Mock loading.""" result = orig_load_translations(files) - result["en"]["moon.sensor"] = { - "state": {"moon__phase": {"first_quarter": "First Quarter"}} + result["en"]["test_legacy_state_translations.sensor"] = { + "state": { + "test_legacy_state_translations__phase": { + "first_quarter": "First Quarter" + } + } } return result @@ -405,9 +472,12 @@ async def test_translation_merging_loaded_apart( ): translations = await translation.async_get_translations(hass, "en", "state") - assert "component.sensor.state.moon__phase.first_quarter" not in translations + assert ( + "component.sensor.state.test_legacy_state_translations__phase.first_quarter" + not in translations + ) - hass.config.components.add("moon.sensor") + hass.config.components.add("test_legacy_state_translations.sensor") with patch( "homeassistant.helpers.translation._load_translations_files_by_language", @@ -415,7 +485,10 @@ async def test_translation_merging_loaded_apart( ): translations = await translation.async_get_translations(hass, "en", "state") - assert "component.sensor.state.moon__phase.first_quarter" in translations + assert ( + "component.sensor.state.test_legacy_state_translations__phase.first_quarter" + in translations + ) with patch( "homeassistant.helpers.translation._load_translations_files_by_language", @@ -425,7 +498,10 @@ async def test_translation_merging_loaded_apart( hass, "en", "state", integrations={"sensor"} ) - assert "component.sensor.state.moon__phase.first_quarter" in translations + assert ( + "component.sensor.state.test_legacy_state_translations__phase.first_quarter" + in translations + ) async def test_translation_merging_loaded_together( @@ -478,6 +554,37 @@ async def test_ensure_translations_still_load_if_one_integration_fails( assert translations == sensor_translations +async def test_load_translations_all_integrations_broken( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Ensure we do not try to load translations again if the integration is broken.""" + hass.config.components.add("broken") + hass.config.components.add("broken2") + + with patch( + "homeassistant.helpers.translation.async_get_integrations", + return_value={ + "broken2": Exception("unhandled failure"), + "broken": Exception("unhandled failure"), + }, + ): + translations = await translation.async_get_translations( + hass, "en", "entity_component", integrations={"broken", "broken2"} + ) + assert "Failed to load integration for translation" in caplog.text + assert "broken" in caplog.text + assert "broken2" in caplog.text + assert not translations + caplog.clear() + + translations = await translation.async_get_translations( + hass, "en", "entity_component", integrations={"broken", "broken2"} + ) + assert not translations + # Ensure we do not try again + assert "Failed to load integration for translation" not in caplog.text + + async def test_caching(hass: HomeAssistant) -> None: """Test we cache data.""" hass.config.components.add("sensor") @@ -737,3 +844,39 @@ async def test_translate_state(hass: HomeAssistant): ] ) assert result == "on" + + +async def test_get_translations_still_has_title_without_translations_files( + hass: HomeAssistant, mock_config_flows +) -> None: + """Test the title still gets added in if there are no translation files.""" + mock_config_flows["integration"].append("component1") + integration = Mock(file_path=pathlib.Path(__file__)) + integration.name = "Component 1" + + with ( + patch( + "homeassistant.helpers.translation.component_translation_path", + return_value="bla.json", + ), + patch( + "homeassistant.helpers.translation._load_translations_files_by_language", + return_value={}, + ), + patch( + "homeassistant.helpers.translation.async_get_integrations", + return_value={"component1": integration}, + ), + ): + translations = await translation.async_get_translations( + hass, "en", "title", config_flow=True + ) + translations_again = await translation.async_get_translations( + hass, "en", "title", config_flow=True + ) + + assert translations == translations_again + + assert translations == { + "component.component1.title": "Component 1", + } diff --git a/tests/helpers/test_trigger.py b/tests/helpers/test_trigger.py index 0a102c4ce42..0a15cf9a330 100644 --- a/tests/helpers/test_trigger.py +++ b/tests/helpers/test_trigger.py @@ -1,4 +1,5 @@ """The tests for the trigger helper.""" + from unittest.mock import ANY, AsyncMock, MagicMock, call, patch import pytest @@ -33,7 +34,8 @@ async def test_bad_trigger_platform(hass: HomeAssistant) -> None: async def test_trigger_subtype(hass: HomeAssistant) -> None: """Test trigger subtypes.""" with patch( - "homeassistant.helpers.trigger.async_get_integration", return_value=MagicMock() + "homeassistant.helpers.trigger.async_get_integration", + return_value=MagicMock(async_get_platform=AsyncMock()), ) as integration_mock: await _async_get_trigger_platform(hass, {"platform": "test.subtype"}) assert integration_mock.call_args == call(hass, "test") diff --git a/tests/helpers/test_update_coordinator.py b/tests/helpers/test_update_coordinator.py index 1e8ef93b872..25f72d76e3c 100644 --- a/tests/helpers/test_update_coordinator.py +++ b/tests/helpers/test_update_coordinator.py @@ -1,4 +1,5 @@ """Tests for the update coordinator.""" + from datetime import timedelta import logging from unittest.mock import AsyncMock, Mock, patch diff --git a/tests/ignore_uncaught_exceptions.py b/tests/ignore_uncaught_exceptions.py index e9327c0255a..3be2093057b 100644 --- a/tests/ignore_uncaught_exceptions.py +++ b/tests/ignore_uncaught_exceptions.py @@ -1,4 +1,5 @@ """List of tests that have uncaught exceptions today. Will be shrunk over time.""" + IGNORE_UNCAUGHT_EXCEPTIONS = [ ( # This test explicitly throws an uncaught exception diff --git a/tests/patch_time.py b/tests/patch_time.py index 5f5dc467c9d..3489d4a6baf 100644 --- a/tests/patch_time.py +++ b/tests/patch_time.py @@ -1,4 +1,5 @@ """Patch time related functions.""" + from __future__ import annotations import datetime diff --git a/tests/pylint/__init__.py b/tests/pylint/__init__.py index e03a2d2a118..abe4c14c879 100644 --- a/tests/pylint/__init__.py +++ b/tests/pylint/__init__.py @@ -1,4 +1,5 @@ """Tests for pylint.""" + import contextlib from pylint.testutils.unittest_linter import UnittestLinter diff --git a/tests/pylint/conftest.py b/tests/pylint/conftest.py index 2aeb5fbd5b7..2b0fdcf7df5 100644 --- a/tests/pylint/conftest.py +++ b/tests/pylint/conftest.py @@ -1,4 +1,5 @@ """Configuration for pylint tests.""" + from importlib.util import module_from_spec, spec_from_file_location from pathlib import Path import sys diff --git a/tests/pylint/test_enforce_coordinator_module.py b/tests/pylint/test_enforce_coordinator_module.py index 746da8c1d7e..90d88246974 100644 --- a/tests/pylint/test_enforce_coordinator_module.py +++ b/tests/pylint/test_enforce_coordinator_module.py @@ -1,4 +1,5 @@ """Tests for pylint hass_enforce_coordinator_module plugin.""" + from __future__ import annotations import astroid diff --git a/tests/pylint/test_enforce_sorted_platforms.py b/tests/pylint/test_enforce_sorted_platforms.py index 71bde88bc13..d1e6d500cc3 100644 --- a/tests/pylint/test_enforce_sorted_platforms.py +++ b/tests/pylint/test_enforce_sorted_platforms.py @@ -1,4 +1,5 @@ """Tests for pylint hass_enforce_sorted_platforms plugin.""" + from __future__ import annotations import astroid diff --git a/tests/pylint/test_enforce_super_call.py b/tests/pylint/test_enforce_super_call.py index 5e2861b1c74..84d4086fc5c 100644 --- a/tests/pylint/test_enforce_super_call.py +++ b/tests/pylint/test_enforce_super_call.py @@ -1,4 +1,5 @@ """Tests for pylint hass_enforce_super_call plugin.""" + from __future__ import annotations from types import ModuleType @@ -123,9 +124,14 @@ def test_enforce_super_call( walker = ASTWalker(linter) walker.add_checker(super_call_checker) - with patch.object( - hass_enforce_super_call, "METHODS", new={"added_to_hass", "async_added_to_hass"} - ), assert_no_messages(linter): + with ( + patch.object( + hass_enforce_super_call, + "METHODS", + new={"added_to_hass", "async_added_to_hass"}, + ), + assert_no_messages(linter), + ): walker.walk(root_node) @@ -203,19 +209,24 @@ def test_enforce_super_call_bad( walker.add_checker(super_call_checker) node = root_node.body[node_idx].body[0] - with patch.object( - hass_enforce_super_call, "METHODS", new={"added_to_hass", "async_added_to_hass"} - ), assert_adds_messages( - linter, - MessageTest( - msg_id="hass-missing-super-call", - node=node, - line=node.lineno, - args=(node.name,), - col_offset=node.col_offset, - end_line=node.position.end_lineno, - end_col_offset=node.position.end_col_offset, - confidence=INFERENCE, + with ( + patch.object( + hass_enforce_super_call, + "METHODS", + new={"added_to_hass", "async_added_to_hass"}, + ), + assert_adds_messages( + linter, + MessageTest( + msg_id="hass-missing-super-call", + node=node, + line=node.lineno, + args=(node.name,), + col_offset=node.col_offset, + end_line=node.position.end_lineno, + end_col_offset=node.position.end_col_offset, + confidence=INFERENCE, + ), ), ): walker.walk(root_node) diff --git a/tests/pylint/test_enforce_type_hints.py b/tests/pylint/test_enforce_type_hints.py index d23d5a849dd..78eb682200a 100644 --- a/tests/pylint/test_enforce_type_hints.py +++ b/tests/pylint/test_enforce_type_hints.py @@ -346,7 +346,7 @@ def test_invalid_config_flow_step( pylint.testutils.MessageTest( msg_id="hass-return-type", node=func_node, - args=("FlowResult", "async_step_zeroconf"), + args=("ConfigFlowResult", "async_step_zeroconf"), line=11, col_offset=4, end_line=11, @@ -356,6 +356,46 @@ def test_invalid_config_flow_step( type_hint_checker.visit_classdef(class_node) +def test_invalid_custom_config_flow_step( + linter: UnittestLinter, type_hint_checker: BaseChecker +) -> None: + """Ensure invalid hints are rejected for ConfigFlow step.""" + class_node, func_node, arg_node = astroid.extract_node( + """ + class FlowHandler(): + pass + + class ConfigFlow(FlowHandler): + pass + + class AxisFlowHandler( #@ + ConfigFlow, domain=AXIS_DOMAIN + ): + async def async_step_axis_specific( #@ + self, + device_config: dict #@ + ): + pass + """, + "homeassistant.components.pylint_test.config_flow", + ) + type_hint_checker.visit_module(class_node.parent) + + with assert_adds_messages( + linter, + pylint.testutils.MessageTest( + msg_id="hass-return-type", + node=func_node, + args=("ConfigFlowResult", "async_step_axis_specific"), + line=11, + col_offset=4, + end_line=11, + end_col_offset=38, + ), + ): + type_hint_checker.visit_classdef(class_node) + + def test_valid_config_flow_step( linter: UnittestLinter, type_hint_checker: BaseChecker ) -> None: @@ -374,7 +414,7 @@ def test_valid_config_flow_step( async def async_step_zeroconf( self, device_config: ZeroconfServiceInfo - ) -> FlowResult: + ) -> ConfigFlowResult: pass """, "homeassistant.components.pylint_test.config_flow", @@ -949,6 +989,39 @@ def test_media_player_entity( type_hint_checker.visit_classdef(class_node) +def test_humidifier_entity( + linter: UnittestLinter, type_hint_checker: BaseChecker +) -> None: + """Ensure valid hints are accepted for humidifier entity.""" + # Set bypass option + type_hint_checker.linter.config.ignore_missing_annotations = False + + # Ensure that `float` and `int` are valid for `int` argument type + class_node = astroid.extract_node( + """ + class Entity(): + pass + + class HumidifierEntity(Entity): + pass + + class MyHumidifier( #@ + HumidifierEntity + ): + def set_humidity(self, humidity: int) -> None: + pass + + def async_set_humidity(self, humidity: float) -> None: + pass + """, + "homeassistant.components.pylint_test.humidifier", + ) + type_hint_checker.visit_module(class_node.parent) + + with assert_no_messages(linter): + type_hint_checker.visit_classdef(class_node) + + def test_number_entity(linter: UnittestLinter, type_hint_checker: BaseChecker) -> None: """Ensure valid hints are accepted for number entity.""" # Set bypass option diff --git a/tests/scripts/test_auth.py b/tests/scripts/test_auth.py index 3d9f26d1204..8367eda76e8 100644 --- a/tests/scripts/test_auth.py +++ b/tests/scripts/test_auth.py @@ -1,4 +1,5 @@ """Test the auth script to manage local users.""" + import logging from typing import Any from unittest.mock import Mock, patch diff --git a/tests/scripts/test_check_config.py b/tests/scripts/test_check_config.py index 9e2f6708c99..a77e5bf504a 100644 --- a/tests/scripts/test_check_config.py +++ b/tests/scripts/test_check_config.py @@ -1,4 +1,5 @@ """Test check_config script.""" + import logging from unittest.mock import patch diff --git a/tests/scripts/test_init.py b/tests/scripts/test_init.py index 44090019a29..44edece1812 100644 --- a/tests/scripts/test_init.py +++ b/tests/scripts/test_init.py @@ -1,4 +1,5 @@ """Test script init.""" + from unittest.mock import patch import homeassistant.scripts as scripts diff --git a/tests/snapshots/test_config.ambr b/tests/snapshots/test_config.ambr index 34fa5d61797..6fcbce7d8d6 100644 --- a/tests/snapshots/test_config.ambr +++ b/tests/snapshots/test_config.ambr @@ -1,4 +1,19 @@ # serializer version: 1 +# name: test_component_config_error_processing[exception_info_list0-bla-messages0-False-component_import_err] + 'Unable to import test_domain: bla' +# --- +# name: test_component_config_error_processing[exception_info_list1-bla-messages1-True-config_validation_err] + 'Invalid config for integration test_domain at configuration.yaml, line 140: bla' +# --- +# name: test_component_config_error_processing[exception_info_list2-bla @ data['path']-messages2-False-config_validation_err] + "Invalid config for integration test_domain at configuration.yaml, line 140: bla @ data['path']" +# --- +# name: test_component_config_error_processing[exception_info_list3-bla @ data['path']-messages3-False-platform_config_validation_err] + "Invalid config for test_domain from integration test_domain at file configuration.yaml, line 140: bla @ data['path']. Check the logs for more information" +# --- +# name: test_component_config_error_processing[exception_info_list4-bla-messages4-False-platform_component_load_err] + 'Platform error: test_domain - bla' +# --- # name: test_component_config_validation_error[basic] list([ dict({ @@ -63,7 +78,7 @@ }), dict({ 'has_exc_info': True, - 'message': 'Unknown error calling custom_validator_bad_2 config validator', + 'message': 'config_validator_unknown_err', }), ]) # --- @@ -131,7 +146,7 @@ }), dict({ 'has_exc_info': True, - 'message': 'Unknown error calling custom_validator_bad_2 config validator', + 'message': 'config_validator_unknown_err', }), ]) # --- @@ -247,7 +262,7 @@ }), dict({ 'has_exc_info': True, - 'message': 'Unknown error calling custom_validator_bad_2 config validator', + 'message': 'config_validator_unknown_err', }), ]) # --- @@ -291,7 +306,7 @@ }), dict({ 'has_exc_info': True, - 'message': 'Unknown error calling custom_validator_bad_2 config validator', + 'message': 'config_validator_unknown_err', }), dict({ 'has_exc_info': False, @@ -342,7 +357,27 @@ ''', "Invalid config for 'custom_validator_ok_2' at configuration.yaml, line 52: required key 'host' not provided, please check the docs at https://www.home-assistant.io/integrations/custom_validator_ok_2", "Invalid config for 'custom_validator_bad_1' at configuration.yaml, line 55: broken, please check the docs at https://www.home-assistant.io/integrations/custom_validator_bad_1", - 'Unknown error calling custom_validator_bad_2 config validator', + 'config_validator_unknown_err', + ]) +# --- +# name: test_individual_packages_schema_validation_errors[packages_dict] + list([ + "Setup of package 'should_be_a_dict' at configuration.yaml, line 3 failed: Invalid package definition 'should_be_a_dict': expected a dictionary. Package will not be initialized", + ]) +# --- +# name: test_individual_packages_schema_validation_errors[packages_include_dir_named_dict] + list([ + "Setup of package 'should_be_a_dict' at packages/expected_dict.yaml, line 1 failed: Invalid package definition 'should_be_a_dict': expected a dictionary. Package will not be initialized", + ]) +# --- +# name: test_individual_packages_schema_validation_errors[packages_include_dir_named_slug] + list([ + "Setup of package 'this is not a slug but it should be one' at packages/expected_slug.yaml, line 1 failed: Invalid package definition 'this is not a slug but it should be one': invalid slug this is not a slug but it should be one (try this_is_not_a_slug_but_it_should_be_one). Package will not be initialized", + ]) +# --- +# name: test_individual_packages_schema_validation_errors[packages_slug] + list([ + "Setup of package 'this is not a slug but it should be one' at configuration.yaml, line 3 failed: Invalid package definition 'this is not a slug but it should be one': invalid slug this is not a slug but it should be one (try this_is_not_a_slug_but_it_should_be_one). Package will not be initialized", ]) # --- # name: test_package_merge_error[packages] @@ -381,6 +416,21 @@ "Setup of package 'unknown_integration' at integrations/unknown_integration.yaml, line 1 failed: Integration test_domain caused error: ModuleNotFoundError: No module named 'not_installed_something'", ]) # --- +# name: test_packages_schema_validation_error[packages_is_a_list] + list([ + "Invalid package configuration 'packages' at configuration.yaml, line 2: expected a dictionary", + ]) +# --- +# name: test_packages_schema_validation_error[packages_is_a_value] + list([ + "Invalid package configuration 'packages' at configuration.yaml, line 2: expected a dictionary", + ]) +# --- +# name: test_packages_schema_validation_error[packages_is_null] + list([ + "Invalid package configuration 'packages' at configuration.yaml, line 3: expected a dictionary", + ]) +# --- # name: test_yaml_error[basic] ''' mapping values are not allowed here @@ -451,38 +501,3 @@ ''', ]) # --- -# name: test_individual_packages_schema_validation_errors[packages_dict] - list([ - "Setup of package 'should_be_a_dict' at configuration.yaml, line 3 failed: Invalid package definition 'should_be_a_dict': expected a dictionary. Package will not be initialized", - ]) -# --- -# name: test_individual_packages_schema_validation_errors[packages_slug] - list([ - "Setup of package 'this is not a slug but it should be one' at configuration.yaml, line 3 failed: Invalid package definition 'this is not a slug but it should be one': invalid slug this is not a slug but it should be one (try this_is_not_a_slug_but_it_should_be_one). Package will not be initialized", - ]) -# --- -# name: test_individual_packages_schema_validation_errors[packages_include_dir_named_dict] - list([ - "Setup of package 'should_be_a_dict' at packages/expected_dict.yaml, line 1 failed: Invalid package definition 'should_be_a_dict': expected a dictionary. Package will not be initialized", - ]) -# --- -# name: test_individual_packages_schema_validation_errors[packages_include_dir_named_slug] - list([ - "Setup of package 'this is not a slug but it should be one' at packages/expected_slug.yaml, line 1 failed: Invalid package definition 'this is not a slug but it should be one': invalid slug this is not a slug but it should be one (try this_is_not_a_slug_but_it_should_be_one). Package will not be initialized", - ]) -# --- -# name: test_packages_schema_validation_error[packages_is_a_list] - list([ - "Invalid package configuration 'packages' at configuration.yaml, line 2: expected a dictionary", - ]) -# --- -# name: test_packages_schema_validation_error[packages_is_a_value] - list([ - "Invalid package configuration 'packages' at configuration.yaml, line 2: expected a dictionary", - ]) -# --- -# name: test_packages_schema_validation_error[packages_is_null] - list([ - "Invalid package configuration 'packages' at configuration.yaml, line 3: expected a dictionary", - ]) -# --- diff --git a/tests/syrupy.py b/tests/syrupy.py index 9209654a607..e5bbf017bb3 100644 --- a/tests/syrupy.py +++ b/tests/syrupy.py @@ -1,4 +1,5 @@ """Home Assistant extension for Syrupy.""" + from __future__ import annotations from contextlib import suppress @@ -165,7 +166,7 @@ class HomeAssistantSnapshotSerializer(AmberDataSerializer): cls, data: er.RegistryEntry ) -> SerializableData: """Prepare a Home Assistant entity registry entry for serialization.""" - return EntityRegistryEntrySnapshot( + serialized = EntityRegistryEntrySnapshot( attrs.asdict(data) | { "config_entry_id": ANY, @@ -174,6 +175,8 @@ class HomeAssistantSnapshotSerializer(AmberDataSerializer): "options": {k: dict(v) for k, v in data.options.items()}, } ) + serialized.pop("categories") + return serialized @classmethod def _serializable_flow_result(cls, data: FlowResult) -> SerializableData: @@ -195,6 +198,7 @@ class HomeAssistantSnapshotSerializer(AmberDataSerializer): | { "context": ANY, "last_changed": ANY, + "last_reported": ANY, "last_updated": ANY, } ) diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py index 2fbce03c307..de82aba9911 100644 --- a/tests/test_bootstrap.py +++ b/tests/test_bootstrap.py @@ -1,22 +1,26 @@ """Test the bootstrapping.""" + import asyncio from collections.abc import Generator, Iterable import glob import os +import sys from typing import Any from unittest.mock import AsyncMock, Mock, patch import pytest -from homeassistant import bootstrap, runner +from homeassistant import bootstrap, loader, runner import homeassistant.config as config_util from homeassistant.config_entries import HANDLERS, ConfigEntry from homeassistant.const import SIGNAL_BOOTSTRAP_INTEGRATIONS -from homeassistant.core import HomeAssistant, async_get_hass, callback +from homeassistant.core import CoreState, HomeAssistant, async_get_hass, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.translation import async_translations_loaded from homeassistant.helpers.typing import ConfigType from homeassistant.loader import Integration +from homeassistant.setup import BASE_PLATFORMS from .common import ( MockConfigEntry, @@ -43,9 +47,10 @@ async def apply_stop_hass(stop_hass: None) -> None: @pytest.fixture(scope="session", autouse=True) def mock_http_start_stop() -> Generator[None, None, None]: """Mock HTTP start and stop.""" - with patch( - "homeassistant.components.http.start_http_server_and_save_config" - ), patch("homeassistant.components.http.HomeAssistantHTTP.stop"): + with ( + patch("homeassistant.components.http.start_http_server_and_save_config"), + patch("homeassistant.components.http.HomeAssistantHTTP.stop"), + ): yield @@ -63,11 +68,15 @@ async def test_async_enable_logging( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test to ensure logging is migrated to the queue handlers.""" - with patch("logging.getLogger"), patch( - "homeassistant.bootstrap.async_activate_log_queue_handler" - ) as mock_async_activate_log_queue_handler, patch( - "homeassistant.bootstrap.logging.handlers.RotatingFileHandler.doRollover", - side_effect=OSError, + with ( + patch("logging.getLogger"), + patch( + "homeassistant.bootstrap.async_activate_log_queue_handler" + ) as mock_async_activate_log_queue_handler, + patch( + "homeassistant.bootstrap.logging.handlers.RotatingFileHandler.doRollover", + side_effect=OSError, + ), ): bootstrap.async_enable_logging(hass) mock_async_activate_log_queue_handler.assert_called_once() @@ -103,6 +112,16 @@ async def test_empty_setup(hass: HomeAssistant) -> None: assert domain in hass.config.components, domain +@pytest.mark.parametrize("load_registries", [False]) +async def test_preload_translations(hass: HomeAssistant) -> None: + """Test translations are preloaded for all frontend deps and base platforms.""" + await bootstrap.async_from_config_dict({}, hass) + await hass.async_block_till_done(wait_background_tasks=True) + frontend = await loader.async_get_integration(hass, "frontend") + assert async_translations_loaded(hass, set(frontend.all_dependencies)) + assert async_translations_loaded(hass, BASE_PLATFORMS) + + async def test_core_failure_loads_recovery_mode( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: @@ -220,6 +239,93 @@ async def test_setup_after_deps_in_stage_1_ignored(hass: HomeAssistant) -> None: assert order == ["cloud", "an_after_dep", "normal_integration"] +@pytest.mark.parametrize("load_registries", [False]) +async def test_setup_after_deps_manifests_are_loaded_even_if_not_setup( + hass: HomeAssistant, +) -> None: + """Ensure we preload manifests for after deps even if they are not setup. + + Its important that we preload the after dep manifests even if they are not setup + since we will always have to check their requirements since any integration + that lists an after dep may import it and we have to ensure requirements are + up to date before the after dep can be imported. + """ + # This test relies on this + assert "cloud" in bootstrap.STAGE_1_INTEGRATIONS + 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"), + partial_manifest={"after_dependencies": ["an_after_dep_of_after_dep"]}, + ), + ) + mock_integration( + hass, + MockModule( + domain="an_after_dep_of_after_dep", + async_setup=gen_domain_setup("an_after_dep_of_after_dep"), + partial_manifest={ + "after_dependencies": ["an_after_dep_of_after_dep_of_after_dep"] + }, + ), + ) + mock_integration( + hass, + MockModule( + domain="an_after_dep_of_after_dep_of_after_dep", + async_setup=gen_domain_setup("an_after_dep_of_after_dep_of_after_dep"), + ), + ) + mock_integration( + hass, + MockModule( + domain="cloud", + async_setup=gen_domain_setup("cloud"), + partial_manifest={"after_dependencies": ["normal_integration"]}, + ), + ) + + await bootstrap._async_set_up_integrations( + hass, {"cloud": {}, "normal_integration": {}} + ) + + assert "normal_integration" in hass.config.components + assert "cloud" in hass.config.components + assert "an_after_dep" not in hass.config.components + assert "an_after_dep_of_after_dep" not in hass.config.components + assert "an_after_dep_of_after_dep_of_after_dep" not in hass.config.components + assert order == ["cloud", "normal_integration"] + assert loader.async_get_loaded_integration(hass, "an_after_dep") is not None + assert ( + loader.async_get_loaded_integration(hass, "an_after_dep_of_after_dep") + is not None + ) + assert ( + loader.async_get_loaded_integration( + hass, "an_after_dep_of_after_dep_of_after_dep" + ) + is not None + ) + + @pytest.mark.parametrize("load_registries", [False]) async def test_setup_frontend_before_recorder(hass: HomeAssistant) -> None: """Test frontend is setup before recorder.""" @@ -270,6 +376,9 @@ async def test_setup_frontend_before_recorder(hass: HomeAssistant) -> None: MockModule( domain="recorder", async_setup=gen_domain_setup("recorder"), + partial_manifest={ + "after_dependencies": ["http"], + }, ), ) @@ -287,6 +396,8 @@ async def test_setup_frontend_before_recorder(hass: HomeAssistant) -> None: assert "frontend" in hass.config.components assert "normal_integration" in hass.config.components assert "recorder" in hass.config.components + assert "http" in hass.config.components + assert order == [ "http", "frontend", @@ -531,11 +642,13 @@ async def test_setup_hass_takes_longer_than_log_slow_startup( await asyncio.sleep(0.6) return True - with patch.object(bootstrap, "LOG_SLOW_STARTUP_INTERVAL", 0.3), patch.object( - bootstrap, "SLOW_STARTUP_CHECK_INTERVAL", 0.05 - ), patch( - "homeassistant.components.frontend.async_setup", - side_effect=_async_setup_that_blocks_startup, + with ( + patch.object(bootstrap, "LOG_SLOW_STARTUP_INTERVAL", 0.3), + patch.object(bootstrap, "SLOW_STARTUP_CHECK_INTERVAL", 0.05), + patch( + "homeassistant.components.frontend.async_setup", + side_effect=_async_setup_that_blocks_startup, + ), ): await bootstrap.async_setup_hass( runner.RuntimeConfig( @@ -613,9 +726,12 @@ async def test_setup_hass_recovery_mode( mock_process_ha_config_upgrade: Mock, ) -> None: """Test it works.""" - with patch("homeassistant.components.browser.setup") as browser_setup, patch( - "homeassistant.config_entries.ConfigEntries.async_domains", - return_value=["browser"], + with ( + patch("homeassistant.components.browser.setup") as browser_setup, + patch( + "homeassistant.config_entries.ConfigEntries.async_domains", + return_value=["browser"], + ), ): hass = await bootstrap.async_setup_hass( runner.RuntimeConfig( @@ -647,9 +763,12 @@ async def test_setup_hass_safe_mode( caplog: pytest.LogCaptureFixture, ) -> None: """Test it works.""" - with patch("homeassistant.components.browser.setup"), patch( - "homeassistant.config_entries.ConfigEntries.async_domains", - return_value=["browser"], + with ( + patch("homeassistant.components.browser.setup"), + patch( + "homeassistant.config_entries.ConfigEntries.async_domains", + return_value=["browser"], + ), ): hass = await bootstrap.async_setup_hass( runner.RuntimeConfig( @@ -679,9 +798,12 @@ async def test_setup_hass_recovery_mode_and_safe_mode( caplog: pytest.LogCaptureFixture, ) -> None: """Test it works.""" - with patch("homeassistant.components.browser.setup"), patch( - "homeassistant.config_entries.ConfigEntries.async_domains", - return_value=["browser"], + with ( + patch("homeassistant.components.browser.setup"), + patch( + "homeassistant.config_entries.ConfigEntries.async_domains", + return_value=["browser"], + ), ): hass = await bootstrap.async_setup_hass( runner.RuntimeConfig( @@ -780,6 +902,9 @@ async def test_empty_integrations_list_is_only_sent_at_the_end_of_bootstrap( hass: HomeAssistant, ) -> None: """Test empty integrations list is only sent at the end of bootstrap.""" + # setup times only tracked when not running + hass.set_state(CoreState.not_running) + order = [] def gen_domain_setup(domain): @@ -843,10 +968,10 @@ async def test_warning_logged_on_wrap_up_timeout( def gen_domain_setup(domain): async def async_setup(hass, config): - async def _background_task(): + async def _not_marked_background_task(): await asyncio.sleep(0.2) - hass.async_create_task(_background_task()) + hass.async_create_task(_not_marked_background_task()) return True return async_setup @@ -864,7 +989,89 @@ async def test_warning_logged_on_wrap_up_timeout( await bootstrap._async_set_up_integrations(hass, {"normal_integration": {}}) await hass.async_block_till_done() - assert "Setup timed out for bootstrap - moving forward" in caplog.text + assert "Setup timed out for bootstrap" in caplog.text + assert "waiting on" in caplog.text + assert "_not_marked_background_task" in caplog.text + + +@pytest.mark.parametrize("load_registries", [False]) +async def test_tasks_logged_that_block_stage_1( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test we log tasks that delay stage 1 startup.""" + + def gen_domain_setup(domain): + async def async_setup(hass, config): + async def _not_marked_background_task(): + await asyncio.sleep(0.2) + + hass.async_create_task(_not_marked_background_task()) + await asyncio.sleep(0.1) + return True + + return async_setup + + mock_integration( + hass, + MockModule( + domain="normal_integration", + async_setup=gen_domain_setup("normal_integration"), + partial_manifest={}, + ), + ) + + original_stage_1 = bootstrap.STAGE_1_INTEGRATIONS + with ( + patch.object(bootstrap, "STAGE_1_TIMEOUT", 0), + patch.object(bootstrap, "COOLDOWN_TIME", 0), + patch.object( + bootstrap, "STAGE_1_INTEGRATIONS", [*original_stage_1, "normal_integration"] + ), + ): + await bootstrap._async_set_up_integrations(hass, {"normal_integration": {}}) + await hass.async_block_till_done() + + assert "Setup timed out for stage 1 waiting on" in caplog.text + assert "waiting on" in caplog.text + assert "_not_marked_background_task" in caplog.text + + +@pytest.mark.parametrize("load_registries", [False]) +async def test_tasks_logged_that_block_stage_2( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test we log tasks that delay stage 2 startup.""" + + def gen_domain_setup(domain): + async def async_setup(hass, config): + async def _not_marked_background_task(): + await asyncio.sleep(0.2) + + hass.async_create_task(_not_marked_background_task()) + await asyncio.sleep(0.1) + return True + + return async_setup + + mock_integration( + hass, + MockModule( + domain="normal_integration", + async_setup=gen_domain_setup("normal_integration"), + partial_manifest={}, + ), + ) + + with ( + patch.object(bootstrap, "STAGE_2_TIMEOUT", 0), + patch.object(bootstrap, "COOLDOWN_TIME", 0), + ): + await bootstrap._async_set_up_integrations(hass, {"normal_integration": {}}) + await hass.async_block_till_done() + + assert "Setup timed out for stage 2 waiting on" in caplog.text + assert "waiting on" in caplog.text + assert "_not_marked_background_task" in caplog.text @pytest.mark.parametrize("load_registries", [False]) @@ -950,6 +1157,7 @@ async def test_bootstrap_dependencies( # We patch the _import platform method to avoid loading the platform module # to avoid depending on non core components in the tests. mqtt_integration._import_platform = Mock() + mqtt_integration.platforms_exists = Mock(return_value=True) integrations = { "mqtt": { @@ -1003,12 +1211,15 @@ async def test_bootstrap_dependencies( """Mock integrations.""" return {domain: integrations[domain]["integration"] for domain in domains} - with patch( - "homeassistant.setup.loader.async_get_integrations", - side_effect=mock_async_get_integrations, - ), patch( - "homeassistant.config.async_process_component_config", - return_value=config_util.IntegrationConfigInfo({}, []), + with ( + patch( + "homeassistant.setup.loader.async_get_integrations", + side_effect=mock_async_get_integrations, + ), + patch( + "homeassistant.config.async_process_component_config", + return_value=config_util.IntegrationConfigInfo({}, []), + ), ): bootstrap.async_set_domains_to_be_loaded(hass, {integration}) await bootstrap.async_setup_multi_components(hass, {integration}, {}) @@ -1023,3 +1234,167 @@ async def test_bootstrap_dependencies( f"Dependency {integration} will wait for dependencies dict_keys(['mqtt'])" in caplog.text ) + + +async def test_pre_import_no_requirements(hass: HomeAssistant) -> None: + """Test pre-imported and do not have any requirements.""" + pre_imports = [ + name.removesuffix("_pre_import") + for name in dir(bootstrap) + if name.endswith("_pre_import") + ] + + # Make sure future refactoring does not + # accidentally remove the pre-imports + # or change the naming convention without + # updating this test. + assert len(pre_imports) > 3 + + for pre_import in pre_imports: + integration = await loader.async_get_integration(hass, pre_import) + assert not integration.requirements + + +@pytest.mark.timeout(20) +async def test_bootstrap_does_not_preload_stage_1_integrations() -> None: + """Test that the bootstrap does not preload stage 1 integrations. + + If this test fails it means that stage1 integrations are being + loaded too soon and will not get their requirements updated + before they are loaded at runtime. + """ + + process = await asyncio.create_subprocess_exec( + sys.executable, + "-c", + "import homeassistant.bootstrap; import sys; print(sys.modules)", + stdout=asyncio.subprocess.PIPE, + ) + stdout, _ = await process.communicate() + assert process.returncode == 0 + decoded_stdout = stdout.decode() + + # Ensure no stage1 integrations have been imported + # as a side effect of importing the pre-imports + for integration in bootstrap.STAGE_1_INTEGRATIONS: + assert f"homeassistant.components.{integration}" not in decoded_stdout + + +@pytest.mark.parametrize("load_registries", [False]) +async def test_cancellation_does_not_leak_upward_from_async_setup( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + enable_custom_integrations: None, +) -> None: + """Test setting up an integration that raises asyncio.CancelledError.""" + await bootstrap.async_setup_multi_components( + hass, {"test_package_raises_cancelled_error"}, {} + ) + await hass.async_block_till_done() + + assert ( + "Error during setup of component test_package_raises_cancelled_error" + in caplog.text + ) + + +@pytest.mark.parametrize("load_registries", [False]) +async def test_cancellation_does_not_leak_upward_from_async_setup_entry( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + enable_custom_integrations: None, +) -> None: + """Test setting up an integration that raises asyncio.CancelledError.""" + entry = MockConfigEntry( + domain="test_package_raises_cancelled_error_config_entry", data={} + ) + entry.add_to_hass(hass) + await bootstrap.async_setup_multi_components( + hass, {"test_package_raises_cancelled_error_config_entry"}, {} + ) + await hass.async_block_till_done() + + await bootstrap.async_setup_multi_components(hass, {"test_package"}, {}) + await hass.async_block_till_done() + assert ( + "Error setting up entry Mock Title for test_package_raises_cancelled_error_config_entry" + in caplog.text + ) + + assert "test_package" in hass.config.components + assert "test_package_raises_cancelled_error_config_entry" in hass.config.components + + +@pytest.mark.parametrize("load_registries", [False]) +async def test_setup_does_base_platforms_first(hass: HomeAssistant) -> None: + """Test setup does base platforms first. + + Its important that base platforms are setup before other integrations + in stage1/2 since they are the foundation for other integrations and + almost every integration has to wait for them to be setup. + """ + 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="sensor", async_setup=gen_domain_setup("sensor")) + ) + mock_integration( + hass, + MockModule( + domain="binary_sensor", async_setup=gen_domain_setup("binary_sensor") + ), + ) + mock_integration( + hass, MockModule(domain="root", async_setup=gen_domain_setup("root")) + ) + mock_integration( + hass, + MockModule( + domain="first_dep", + async_setup=gen_domain_setup("first_dep"), + partial_manifest={"after_dependencies": ["root"]}, + ), + ) + mock_integration( + hass, + MockModule( + domain="second_dep", + async_setup=gen_domain_setup("second_dep"), + partial_manifest={"after_dependencies": ["first_dep"]}, + ), + ) + + with patch( + "homeassistant.components.logger.async_setup", gen_domain_setup("logger") + ): + await bootstrap._async_set_up_integrations( + hass, + { + "root": {}, + "first_dep": {}, + "second_dep": {}, + "sensor": {}, + "logger": {}, + "binary_sensor": {}, + }, + ) + + assert "binary_sensor" in hass.config.components + assert "sensor" in hass.config.components + assert "root" in hass.config.components + assert "first_dep" in hass.config.components + assert "second_dep" in hass.config.components + + assert order[0] == "logger" + # base platforms (sensor/binary_sensor) should be setup before other integrations + # but after logger integrations. The order of base platforms is not guaranteed, + # only that they are setup before other integrations. + assert set(order[1:3]) == {"sensor", "binary_sensor"} + assert order[3:] == ["root", "first_dep", "second_dep"] diff --git a/tests/test_circular_imports.py b/tests/test_circular_imports.py index 1c5157b74e1..79f0fd9caf7 100644 --- a/tests/test_circular_imports.py +++ b/tests/test_circular_imports.py @@ -1,4 +1,5 @@ """Test to check for circular imports in core components.""" + import asyncio import sys diff --git a/tests/test_config.py b/tests/test_config.py index 36d4351afb4..c20e2822592 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,4 +1,6 @@ """Test config utils.""" + +import asyncio from collections import OrderedDict import contextlib import copy @@ -14,6 +16,7 @@ import voluptuous as vol from voluptuous import Invalid, MultipleInvalid import yaml +from homeassistant import config, loader import homeassistant.config as config_util from homeassistant.const import ( ATTR_ASSUMED_STATE, @@ -29,13 +32,19 @@ from homeassistant.const import ( CONF_UNIT_SYSTEM_METRIC, __version__, ) -from homeassistant.core import ConfigSource, HomeAssistant, HomeAssistantError +from homeassistant.core import ( + DOMAIN as HA_DOMAIN, + ConfigSource, + HomeAssistant, + HomeAssistantError, +) from homeassistant.exceptions import ConfigValidationError from homeassistant.helpers import config_validation as cv, issue_registry as ir import homeassistant.helpers.check_config as check_config from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import ConfigType from homeassistant.loader import Integration, async_get_integration +from homeassistant.setup import async_setup_component from homeassistant.util.unit_system import ( _CONF_UNIT_SYSTEM_US_CUSTOMARY, METRIC_SYSTEM, @@ -43,6 +52,7 @@ from homeassistant.util.unit_system import ( UnitSystem, ) from homeassistant.util.yaml import SECRET_YAML +from homeassistant.util.yaml.objects import NodeDictClass from .common import ( MockModule, @@ -365,6 +375,14 @@ async def mock_custom_validator_integrations_with_docs( ) +class ConfigTestClass(NodeDictClass): + """Test class for config with wrapper.""" + + __dict__ = {"__config_file__": "configuration.yaml", "__line__": 140} + __line__ = 140 + __config_file__ = "configuration.yaml" + + async def test_create_default_config(hass: HomeAssistant) -> None: """Test creation of default config.""" assert not os.path.isfile(YAML_PATH) @@ -448,8 +466,9 @@ def test_load_yaml_config_raises_error_if_unsafe_yaml() -> None: with open(YAML_PATH, "w") as fp: fp.write("- !!python/object/apply:os.system []") - with patch.object(os, "system") as system_mock, contextlib.suppress( - HomeAssistantError + with ( + patch.object(os, "system") as system_mock, + contextlib.suppress(HomeAssistantError), ): config_util.load_yaml_config_file(YAML_PATH) @@ -634,8 +653,9 @@ def test_process_config_upgrade(hass: HomeAssistant) -> None: ha_version = "0.92.0" mock_open = mock.mock_open() - with patch("homeassistant.config.open", mock_open, create=True), patch.object( - config_util, "__version__", "0.91.0" + with ( + patch("homeassistant.config.open", mock_open, create=True), + patch.object(config_util, "__version__", "0.91.0"), ): opened_file = mock_open.return_value opened_file.readline.return_value = ha_version @@ -862,7 +882,7 @@ async def test_loading_configuration(hass: HomeAssistant) -> None: @pytest.mark.parametrize( ("minor_version", "users", "user_data", "default_language"), - ( + [ (2, (), {}, "en"), (2, ({"is_owner": True},), {}, "en"), ( @@ -891,7 +911,7 @@ async def test_loading_configuration(hass: HomeAssistant) -> None: {"user1": {"language": {"language": "sv"}}}, "en", ), - ), + ], ) async def test_language_default( hass: HomeAssistant, @@ -1038,7 +1058,7 @@ async def test_check_ha_config_file_wrong(mock_check, hass: HomeAssistant) -> No "hass_config", [ { - config_util.CONF_CORE: { + HA_DOMAIN: { config_util.CONF_PACKAGES: { "pack_dict": {"input_boolean": {"ib1": None}} } @@ -1055,7 +1075,7 @@ async def test_async_hass_config_yaml_merge( conf = await config_util.async_hass_config_yaml(hass) assert merge_log_err.call_count == 0 - assert conf[config_util.CONF_CORE].get(config_util.CONF_PACKAGES) is not None + assert conf[HA_DOMAIN].get(config_util.CONF_PACKAGES) is not None assert len(conf) == 3 assert len(conf["input_boolean"]) == 2 assert len(conf["light"]) == 1 @@ -1083,7 +1103,7 @@ async def test_merge(merge_log_err, hass: HomeAssistant) -> None: }, } config = { - config_util.CONF_CORE: {config_util.CONF_PACKAGES: packages}, + HA_DOMAIN: {config_util.CONF_PACKAGES: packages}, "input_boolean": {"ib2": None}, "light": {"platform": "test"}, "automation": [], @@ -1110,7 +1130,7 @@ async def test_merge_try_falsy(merge_log_err, hass: HomeAssistant) -> None: "pack_list2": {"light": OrderedDict()}, } config = { - config_util.CONF_CORE: {config_util.CONF_PACKAGES: packages}, + HA_DOMAIN: {config_util.CONF_PACKAGES: packages}, "automation": {"do": "something"}, "light": {"some": "light"}, } @@ -1133,7 +1153,7 @@ async def test_merge_new(merge_log_err, hass: HomeAssistant) -> None: "api": {}, }, } - config = {config_util.CONF_CORE: {config_util.CONF_PACKAGES: packages}} + config = {HA_DOMAIN: {config_util.CONF_PACKAGES: packages}} await config_util.merge_packages_config(hass, config, packages) assert merge_log_err.call_count == 0 @@ -1151,7 +1171,7 @@ async def test_merge_type_mismatch(merge_log_err, hass: HomeAssistant) -> None: "pack_2": {"light": {"ib1": None}}, # light gets merged - ensure_list } config = { - config_util.CONF_CORE: {config_util.CONF_PACKAGES: packages}, + HA_DOMAIN: {config_util.CONF_PACKAGES: packages}, "input_boolean": {"ib2": None}, "input_select": [{"ib2": None}], "light": [{"platform": "two"}], @@ -1167,13 +1187,13 @@ async def test_merge_type_mismatch(merge_log_err, hass: HomeAssistant) -> None: async def test_merge_once_only_keys(merge_log_err, hass: HomeAssistant) -> None: """Test if we have a merge for a comp that may occur only once. Keys.""" packages = {"pack_2": {"api": None}} - config = {config_util.CONF_CORE: {config_util.CONF_PACKAGES: packages}, "api": None} + config = {HA_DOMAIN: {config_util.CONF_PACKAGES: packages}, "api": None} await config_util.merge_packages_config(hass, config, packages) assert config["api"] == OrderedDict() packages = {"pack_2": {"api": {"key_3": 3}}} config = { - config_util.CONF_CORE: {config_util.CONF_PACKAGES: packages}, + HA_DOMAIN: {config_util.CONF_PACKAGES: packages}, "api": {"key_1": 1, "key_2": 2}, } await config_util.merge_packages_config(hass, config, packages) @@ -1182,7 +1202,7 @@ async def test_merge_once_only_keys(merge_log_err, hass: HomeAssistant) -> None: # Duplicate keys error packages = {"pack_2": {"api": {"key": 2}}} config = { - config_util.CONF_CORE: {config_util.CONF_PACKAGES: packages}, + HA_DOMAIN: {config_util.CONF_PACKAGES: packages}, "api": {"key": 1}, } await config_util.merge_packages_config(hass, config, packages) @@ -1197,7 +1217,7 @@ async def test_merge_once_only_lists(hass: HomeAssistant) -> None: } } config = { - config_util.CONF_CORE: {config_util.CONF_PACKAGES: packages}, + HA_DOMAIN: {config_util.CONF_PACKAGES: packages}, "api": {"list_1": ["item_1"]}, } await config_util.merge_packages_config(hass, config, packages) @@ -1220,7 +1240,7 @@ async def test_merge_once_only_dictionaries(hass: HomeAssistant) -> None: } } config = { - config_util.CONF_CORE: {config_util.CONF_PACKAGES: packages}, + HA_DOMAIN: {config_util.CONF_PACKAGES: packages}, "api": {"dict_1": {"key_1": 1, "dict_1.1": {"key_1.1": 1.1}}}, } await config_util.merge_packages_config(hass, config, packages) @@ -1254,7 +1274,7 @@ async def test_merge_duplicate_keys(merge_log_err, hass: HomeAssistant) -> None: """Test if keys in dicts are duplicates.""" packages = {"pack_1": {"input_select": {"ib1": None}}} config = { - config_util.CONF_CORE: {config_util.CONF_PACKAGES: packages}, + HA_DOMAIN: {config_util.CONF_PACKAGES: packages}, "input_select": {"ib1": 1}, } await config_util.merge_packages_config(hass, config, packages) @@ -1414,7 +1434,7 @@ async def test_merge_split_component_definition(hass: HomeAssistant) -> None: "pack_1": {"light one": {"l1": None}}, "pack_2": {"light two": {"l2": None}, "light three": {"l3": None}}, } - config = {config_util.CONF_CORE: {config_util.CONF_PACKAGES: packages}} + config = {HA_DOMAIN: {config_util.CONF_PACKAGES: packages}} await config_util.merge_packages_config(hass, config, packages) assert len(config) == 4 @@ -1427,10 +1447,28 @@ async def test_component_config_exceptions( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test unexpected exceptions validating component config.""" - # Config validator + + # Create test config with embedded info + test_config = ConfigTestClass({"test_domain": {}}) + test_platform_config = ConfigTestClass( + {"test_domain": {"platform": "test_platform"}} + ) + test_multi_platform_config = ConfigTestClass( + { + "test_domain": [ + {"platform": "test_platform1"}, + {"platform": "test_platform2"}, + ] + }, + ) + + # Make sure the exception translation cache is loaded + await async_setup_component(hass, "homeassistant", {}) + test_integration = Mock( domain="test_domain", - get_platform=Mock( + async_get_component=AsyncMock(), + async_get_platform=AsyncMock( return_value=Mock( async_validate_config=AsyncMock(side_effect=ValueError("broken")) ) @@ -1438,7 +1476,7 @@ async def test_component_config_exceptions( ) assert ( await config_util.async_process_component_and_handle_errors( - hass, {}, integration=test_integration + hass, test_config, integration=test_integration ) is None ) @@ -1447,72 +1485,79 @@ async def test_component_config_exceptions( caplog.clear() with pytest.raises(HomeAssistantError) as ex: await config_util.async_process_component_and_handle_errors( - hass, {}, integration=test_integration, raise_on_failure=True + hass, test_config, integration=test_integration, raise_on_failure=True ) assert "ValueError: broken" in caplog.text assert "Unknown error calling test_domain config validator" in caplog.text - assert str(ex.value) == "Unknown error calling test_domain config validator" - + assert ( + str(ex.value) == "Unknown error calling test_domain config validator - broken" + ) test_integration = Mock( domain="test_domain", - get_platform=Mock( + async_get_platform=AsyncMock( return_value=Mock( async_validate_config=AsyncMock( side_effect=HomeAssistantError("broken") ) ) ), - get_component=Mock(return_value=Mock(spec=["PLATFORM_SCHEMA_BASE"])), + async_get_component=AsyncMock(return_value=Mock(spec=["PLATFORM_SCHEMA_BASE"])), ) caplog.clear() assert ( await config_util.async_process_component_and_handle_errors( - hass, {}, integration=test_integration, raise_on_failure=False + hass, test_config, integration=test_integration, raise_on_failure=False ) is None ) - assert "Invalid config for 'test_domain': broken" in caplog.text + assert ( + "Invalid config for 'test_domain' at ../../configuration.yaml, " + "line 140: broken, please check the docs at" in caplog.text + ) with pytest.raises(HomeAssistantError) as ex: await config_util.async_process_component_and_handle_errors( - hass, {}, integration=test_integration, raise_on_failure=True + hass, test_config, integration=test_integration, raise_on_failure=True ) - assert "Invalid config for 'test_domain': broken" in str(ex.value) - + assert ( + str(ex.value) + == "Invalid config for integration test_domain at configuration.yaml, " + "line 140: broken" + ) # component.CONFIG_SCHEMA caplog.clear() test_integration = Mock( domain="test_domain", - get_platform=Mock(return_value=None), - get_component=Mock( + async_get_platform=AsyncMock(return_value=None), + async_get_component=AsyncMock( return_value=Mock(CONFIG_SCHEMA=Mock(side_effect=ValueError("broken"))) ), ) assert ( await config_util.async_process_component_and_handle_errors( hass, - {}, + test_config, integration=test_integration, raise_on_failure=False, ) is None ) assert "Unknown error calling test_domain CONFIG_SCHEMA" in caplog.text + caplog.clear() with pytest.raises(HomeAssistantError) as ex: await config_util.async_process_component_and_handle_errors( hass, - {}, + test_config, integration=test_integration, raise_on_failure=True, ) assert "Unknown error calling test_domain CONFIG_SCHEMA" in caplog.text - assert str(ex.value) == "Unknown error calling test_domain CONFIG_SCHEMA" - + assert str(ex.value) == "Unknown error calling test_domain CONFIG_SCHEMA - broken" # component.PLATFORM_SCHEMA caplog.clear() test_integration = Mock( domain="test_domain", - get_platform=Mock(return_value=None), - get_component=Mock( + async_get_platform=AsyncMock(return_value=None), + async_get_component=AsyncMock( return_value=Mock( spec=["PLATFORM_SCHEMA_BASE"], PLATFORM_SCHEMA_BASE=Mock(side_effect=ValueError("broken")), @@ -1521,43 +1566,43 @@ async def test_component_config_exceptions( ) assert await config_util.async_process_component_and_handle_errors( hass, - {"test_domain": {"platform": "test_platform"}}, + test_platform_config, integration=test_integration, raise_on_failure=False, ) == {"test_domain": []} assert "ValueError: broken" in caplog.text assert ( - "Unknown error validating config for test_platform platform " - "for test_domain component with PLATFORM_SCHEMA" + "Unknown error when validating config for test_domain " + "from integration test_platform - broken" ) in caplog.text caplog.clear() with pytest.raises(HomeAssistantError) as ex: await config_util.async_process_component_and_handle_errors( hass, - {"test_domain": {"platform": "test_platform"}}, + test_platform_config, integration=test_integration, raise_on_failure=True, ) assert ( - "Unknown error validating config for test_platform platform " - "for test_domain component with PLATFORM_SCHEMA" + "Unknown error when validating config for test_domain " + "from integration test_platform - broken" ) in caplog.text assert str(ex.value) == ( - "Unknown error validating config for test_platform platform " - "for test_domain component with PLATFORM_SCHEMA" + "Unknown error when validating config for test_domain " + "from integration test_platform - broken" ) # platform.PLATFORM_SCHEMA caplog.clear() test_integration = Mock( domain="test_domain", - get_platform=Mock(return_value=None), - get_component=Mock(return_value=Mock(spec=["PLATFORM_SCHEMA_BASE"])), + async_get_platform=AsyncMock(return_value=None), + async_get_component=AsyncMock(return_value=Mock(spec=["PLATFORM_SCHEMA_BASE"])), ) with patch( "homeassistant.config.async_get_integration_with_requirements", return_value=Mock( # integration that owns platform - get_platform=Mock( + async_get_platform=AsyncMock( return_value=Mock( # platform PLATFORM_SCHEMA=Mock(side_effect=ValueError("broken")) ) @@ -1566,86 +1611,73 @@ async def test_component_config_exceptions( ): assert await config_util.async_process_component_and_handle_errors( hass, - {"test_domain": {"platform": "test_platform"}}, + test_platform_config, integration=test_integration, raise_on_failure=False, ) == {"test_domain": []} assert "ValueError: broken" in caplog.text assert ( - "Unknown error validating config for test_platform platform for test_domain" - " component with PLATFORM_SCHEMA" + "Unknown error when validating config for test_domain " + "from integration test_platform - broken" ) in caplog.text caplog.clear() with pytest.raises(HomeAssistantError) as ex: assert await config_util.async_process_component_and_handle_errors( hass, - {"test_domain": {"platform": "test_platform"}}, + test_platform_config, integration=test_integration, raise_on_failure=True, ) assert ( - "Unknown error validating config for test_platform platform for test_domain" - " component with PLATFORM_SCHEMA" - ) in str(ex.value) + "Unknown error when validating config for test_domain " + "from integration test_platform - broken" in str(ex.value) + ) assert "ValueError: broken" in caplog.text assert ( - "Unknown error validating config for test_platform platform for test_domain" - " component with PLATFORM_SCHEMA" in caplog.text + "Unknown error when validating config for test_domain " + "from integration test_platform - broken" in caplog.text ) # Test multiple platform failures assert await config_util.async_process_component_and_handle_errors( hass, - { - "test_domain": [ - {"platform": "test_platform1"}, - {"platform": "test_platform2"}, - ] - }, + test_multi_platform_config, integration=test_integration, raise_on_failure=False, ) == {"test_domain": []} assert "ValueError: broken" in caplog.text assert ( - "Unknown error validating config for test_platform1 platform " - "for test_domain component with PLATFORM_SCHEMA" - ) in caplog.text - assert ( - "Unknown error validating config for test_platform2 platform " - "for test_domain component with PLATFORM_SCHEMA" + "Unknown error when validating config for test_domain " + "from integration test_platform - broken" ) in caplog.text caplog.clear() with pytest.raises(HomeAssistantError) as ex: assert await config_util.async_process_component_and_handle_errors( hass, - { - "test_domain": [ - {"platform": "test_platform1"}, - {"platform": "test_platform2"}, - ] - }, + test_multi_platform_config, integration=test_integration, raise_on_failure=True, ) assert ( - "Failed to process component config for integration test_domain" - " due to multiple errors (2), check the logs for more information." - ) in str(ex.value) + "Failed to process config for integration test_domain " + "due to multiple (2) errors. Check the logs for more information" + in str(ex.value) + ) assert "ValueError: broken" in caplog.text assert ( - "Unknown error validating config for test_platform1 platform " - "for test_domain component with PLATFORM_SCHEMA" + "Unknown error when validating config for test_domain " + "from integration test_platform1 - broken" ) in caplog.text assert ( - "Unknown error validating config for test_platform2 platform " - "for test_domain component with PLATFORM_SCHEMA" + "Unknown error when validating config for test_domain " + "from integration test_platform2 - broken" ) in caplog.text - # get_platform("domain") raising on ImportError + # async_get_platform("domain") raising on ImportError caplog.clear() test_integration = Mock( domain="test_domain", - get_platform=Mock(return_value=None), - get_component=Mock(return_value=Mock(spec=["PLATFORM_SCHEMA_BASE"])), + async_get_platform=AsyncMock(return_value=None), + async_get_component=AsyncMock(return_value=Mock(spec=["PLATFORM_SCHEMA_BASE"])), ) import_error = ImportError( ("ModuleNotFoundError: No module named 'not_installed_something'"), @@ -1654,12 +1686,12 @@ async def test_component_config_exceptions( with patch( "homeassistant.config.async_get_integration_with_requirements", return_value=Mock( # integration that owns platform - get_platform=Mock(side_effect=import_error) + async_get_platform=AsyncMock(side_effect=import_error) ), ): assert await config_util.async_process_component_and_handle_errors( hass, - {"test_domain": {"platform": "test_platform"}}, + test_platform_config, integration=test_integration, raise_on_failure=False, ) == {"test_domain": []} @@ -1671,7 +1703,7 @@ async def test_component_config_exceptions( with pytest.raises(HomeAssistantError) as ex: assert await config_util.async_process_component_and_handle_errors( hass, - {"test_domain": {"platform": "test_platform"}}, + test_platform_config, integration=test_integration, raise_on_failure=True, ) @@ -1688,12 +1720,13 @@ async def test_component_config_exceptions( "No module named 'not_installed_something'" ) in str(ex.value) - # get_platform("config") raising + # async_get_platform("config") raising caplog.clear() test_integration = Mock( pkg_path="homeassistant.components.test_domain", domain="test_domain", - get_platform=Mock( + async_get_component=AsyncMock(), + async_get_platform=AsyncMock( side_effect=ImportError( ("ModuleNotFoundError: No module named 'not_installed_something'"), name="not_installed_something", @@ -1703,7 +1736,7 @@ async def test_component_config_exceptions( assert ( await config_util.async_process_component_and_handle_errors( hass, - {"test_domain": {}}, + test_config, integration=test_integration, raise_on_failure=False, ) @@ -1716,7 +1749,7 @@ async def test_component_config_exceptions( with pytest.raises(HomeAssistantError) as ex: await config_util.async_process_component_and_handle_errors( hass, - {"test_domain": {}}, + test_config, integration=test_integration, raise_on_failure=True, ) @@ -1729,19 +1762,19 @@ async def test_component_config_exceptions( "No module named 'not_installed_something'" in str(ex.value) ) - # get_component raising + # async_get_component raising caplog.clear() test_integration = Mock( pkg_path="homeassistant.components.test_domain", domain="test_domain", - get_component=Mock( + async_get_component=AsyncMock( side_effect=FileNotFoundError("No such file or directory: b'liblibc.a'") ), ) assert ( await config_util.async_process_component_and_handle_errors( hass, - {"test_domain": {}}, + test_config, integration=test_integration, raise_on_failure=False, ) @@ -1751,7 +1784,7 @@ async def test_component_config_exceptions( with pytest.raises(HomeAssistantError) as ex: await config_util.async_process_component_and_handle_errors( hass, - {"test_domain": {}}, + test_config, integration=test_integration, raise_on_failure=True, ) @@ -1768,7 +1801,7 @@ async def test_component_config_exceptions( ImportError("bla"), "component_import_err", "test_domain", - {"test_domain": []}, + ConfigTestClass({"test_domain": []}), "https://example.com", ) ], @@ -1783,13 +1816,14 @@ async def test_component_config_exceptions( HomeAssistantError("bla"), "config_validation_err", "test_domain", - {"test_domain": []}, + ConfigTestClass({"test_domain": []}), "https://example.com", ) ], "bla", [ - "Invalid config for 'test_domain': bla, " + "Invalid config for 'test_domain' at " + "../../configuration.yaml, line 140: bla, " "please check the docs at https://example.com", "bla", ], @@ -1802,14 +1836,15 @@ async def test_component_config_exceptions( vol.Invalid("bla", ["path"]), "config_validation_err", "test_domain", - {"test_domain": []}, + ConfigTestClass({"test_domain": []}), "https://example.com", ) ], "bla @ data['path']", [ - "Invalid config for 'test_domain': bla 'path', got None, " - "please check the docs at https://example.com", + "Invalid config for 'test_domain' at " + "../../configuration.yaml, line 140: bla 'path', " + "got None, please check the docs at https://example.com", "bla", ], False, @@ -1821,14 +1856,15 @@ async def test_component_config_exceptions( vol.Invalid("bla", ["path"]), "platform_config_validation_err", "test_domain", - {"test_domain": []}, + ConfigTestClass({"test_domain": []}), "https://alt.example.com", ) ], "bla @ data['path']", [ - "Invalid config for 'test_domain': bla 'path', got None, " - "please check the docs at https://alt.example.com", + "Invalid config for 'test_domain' at " + "../../configuration.yaml, line 140: bla 'path', " + "got None, please check the docs at https://alt.example.com", "bla", ], False, @@ -1840,7 +1876,7 @@ async def test_component_config_exceptions( ImportError("bla"), "platform_component_load_err", "test_domain", - {"test_domain": []}, + ConfigTestClass({"test_domain": []}), "https://example.com", ) ], @@ -1854,13 +1890,18 @@ async def test_component_config_exceptions( async def test_component_config_error_processing( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, - error: str, exception_info_list: list[config_util.ConfigExceptionInfo], + snapshot: SnapshotAssertion, + error: str, messages: list[str], show_stack_trace: bool, translation_key: str, ) -> None: """Test component config error processing.""" + + # Make sure the exception translation cache is loaded + await async_setup_component(hass, "homeassistant", {}) + test_integration = Mock( domain="test_domain", documentation="https://example.com", @@ -1870,17 +1911,20 @@ async def test_component_config_error_processing( ) ), ) - with patch( - "homeassistant.config.async_process_component_config", - return_value=config_util.IntegrationConfigInfo(None, exception_info_list), - ), pytest.raises(ConfigValidationError) as ex: + with ( + patch( + "homeassistant.config.async_process_component_config", + return_value=config_util.IntegrationConfigInfo(None, exception_info_list), + ), + pytest.raises(ConfigValidationError) as ex, + ): await config_util.async_process_component_and_handle_errors( hass, {}, test_integration, raise_on_failure=True ) records = [record for record in caplog.records if record.msg == messages[0]] assert len(records) == 1 assert (records[0].exc_info is not None) == show_stack_trace - assert str(ex.value) == messages[0] + assert str(ex.value) == snapshot assert ex.value.translation_key == translation_key assert ex.value.translation_domain == "homeassistant" assert ex.value.translation_placeholders["domain"] == "test_domain" @@ -1892,7 +1936,7 @@ async def test_component_config_error_processing( return_value=config_util.IntegrationConfigInfo(None, exception_info_list), ): await config_util.async_process_component_and_handle_errors( - hass, {}, test_integration + hass, ConfigTestClass({}), test_integration ) assert all(message in caplog.text for message in messages) @@ -1966,7 +2010,7 @@ async def test_core_store_historic_currency( assert issue assert issue.translation_placeholders == {"currency": "LTT"} - await hass.config.async_update(**{"currency": "EUR"}) + await hass.config.async_update(currency="EUR") issue = issue_registry.async_get_issue("homeassistant", issue_id) assert not issue @@ -2022,7 +2066,7 @@ async def test_core_store_no_country( issue = issue_registry.async_get_issue("homeassistant", issue_id) assert issue - await hass.config.async_update(**{"country": "SE"}) + await hass.config.async_update(country="SE") issue = issue_registry.async_get_issue("homeassistant", issue_id) assert not issue @@ -2303,7 +2347,7 @@ async def test_packages_schema_validation_error( ] assert error_records == snapshot - assert len(config[config_util.CONF_CORE][config_util.CONF_PACKAGES]) == 0 + assert len(config[HA_DOMAIN][config_util.CONF_PACKAGES]) == 0 def test_extract_domain_configs() -> None: @@ -2337,3 +2381,112 @@ def test_config_per_platform() -> None: (None, 1), ("hello 2", config["zone Hallo"][1]), ] == list(config_util.config_per_platform(config, "zone")) + + +def test_extract_platform_integrations() -> None: + """Test extract_platform_integrations.""" + config = OrderedDict( + [ + (b"zone", {"platform": "not str"}), + ("zone", {"platform": "hello"}), + ("switch", {"platform": ["un", "hash", "able"]}), + ("zonex", []), + ("zoney", ""), + ("notzone", {"platform": "nothello"}), + ("zoner", None), + ("zone Hallo", [1, {"platform": "hello 2"}]), + ("zone 100", None), + ("i n v a-@@", None), + ("i n v a-@@", {"platform": "hello"}), + ("zoneq", "pig"), + ("zoneempty", {"platform": ""}), + ] + ) + assert config_util.extract_platform_integrations(config, {"zone"}) == { + "zone": {"hello", "hello 2"} + } + assert config_util.extract_platform_integrations(config, {"switch"}) == {} + assert config_util.extract_platform_integrations(config, {"zonex"}) == {} + assert config_util.extract_platform_integrations(config, {"zoney"}) == {} + assert config_util.extract_platform_integrations( + config, {"zone", "not_valid", "notzone"} + ) == {"zone": {"hello 2", "hello"}, "notzone": {"nothello"}} + assert config_util.extract_platform_integrations(config, {"zoneq"}) == {} + assert config_util.extract_platform_integrations(config, {"zoneempty"}) == {} + + +@pytest.mark.parametrize("load_registries", [False]) +async def test_loading_platforms_gathers(hass: HomeAssistant) -> None: + """Test loading platform integrations gathers.""" + + mock_integration( + hass, + MockModule( + domain="platform_int", + ), + ) + mock_integration( + hass, + MockModule( + domain="platform_int2", + ), + ) + + # Its important that we do not mock the platforms with mock_platform + # as the loader is smart enough to know they are already loaded and + # will not create an executor job to load them. We are testing in + # what order the executor jobs happen here as we want to make + # sure the platform integrations are at the front of the line + light_integration = await loader.async_get_integration(hass, "light") + sensor_integration = await loader.async_get_integration(hass, "sensor") + + order: list[tuple[str, str]] = [] + + def _load_platform(self, platform: str) -> MockModule: + order.append((self.domain, platform)) + return MockModule() + + # We need to patch what runs in the executor so we are counting + # the order that jobs are scheduled in th executor + with patch( + "homeassistant.loader.Integration._load_platform", + _load_platform, + ): + light_task = hass.async_create_task( + config.async_process_component_config( + hass, + { + "light": [ + {"platform": "platform_int"}, + {"platform": "platform_int2"}, + ] + }, + light_integration, + ), + eager_start=True, + ) + sensor_task = hass.async_create_task( + config.async_process_component_config( + hass, + { + "sensor": [ + {"platform": "platform_int"}, + {"platform": "platform_int2"}, + ] + }, + sensor_integration, + ), + eager_start=True, + ) + + await asyncio.gather(light_task, sensor_task) + + # Should be called in order so that + # all the light platforms are imported + # before the sensor platforms + assert order == [ + ("platform_int", "light"), + ("platform_int2", "light"), + ("platform_int", "sensor"), + ("platform_int2", "sensor"), + ] diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 672dbb9ae64..7d564f1cf12 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -1,4 +1,5 @@ """Test the config manager.""" + from __future__ import annotations import asyncio @@ -70,6 +71,10 @@ def mock_handlers() -> Generator[None, None, None]: return self.async_show_form(step_id="reauth_confirm") return self.async_abort(reason="test") + async def async_step_reconfigure(self, data): + """Mock Reauth.""" + return await self.async_step_reauth_confirm() + with patch.dict( config_entries.HANDLERS, {"comp": MockFlowHandler, "test": MockFlowHandler} ): @@ -374,7 +379,7 @@ async def test_remove_entry( MockConfigEntry(domain="test_other", entry_id="test3").add_to_manager(manager) # Check all config entries exist - assert [item.entry_id for item in manager.async_entries()] == [ + assert manager.async_entry_ids() == [ "test1", "test2", "test3", @@ -404,7 +409,7 @@ async def test_remove_entry( assert mock_remove_entry.call_count == 1 # Check that config entry was removed. - assert [item.entry_id for item in manager.async_entries()] == ["test1", "test3"] + assert manager.async_entry_ids() == ["test1", "test3"] # Check that entity state has been removed assert hass.states.get("light.test_entity") is None @@ -465,7 +470,7 @@ async def test_remove_entry_handles_callback_error( entry = MockConfigEntry(domain="test", entry_id="test1") entry.add_to_manager(manager) # Check all config entries exist - assert [item.entry_id for item in manager.async_entries()] == ["test1"] + assert manager.async_entry_ids() == ["test1"] # Setup entry await entry.async_setup(hass) await hass.async_block_till_done() @@ -478,7 +483,7 @@ async def test_remove_entry_handles_callback_error( # Check the remove callback was invoked. assert mock_remove_entry.call_count == 1 # Check that config entry was removed. - assert [item.entry_id for item in manager.async_entries()] == [] + assert manager.async_entry_ids() == [] async def test_remove_entry_raises( @@ -498,7 +503,7 @@ async def test_remove_entry_raises( ).add_to_manager(manager) MockConfigEntry(domain="test", entry_id="test3").add_to_manager(manager) - assert [item.entry_id for item in manager.async_entries()] == [ + assert manager.async_entry_ids() == [ "test1", "test2", "test3", @@ -507,7 +512,7 @@ async def test_remove_entry_raises( result = await manager.async_remove("test2") assert result == {"require_restart": True} - assert [item.entry_id for item in manager.async_entries()] == ["test1", "test3"] + assert manager.async_entry_ids() == ["test1", "test3"] async def test_remove_entry_if_not_loaded( @@ -522,7 +527,7 @@ async def test_remove_entry_if_not_loaded( MockConfigEntry(domain="comp", entry_id="test2").add_to_manager(manager) MockConfigEntry(domain="test", entry_id="test3").add_to_manager(manager) - assert [item.entry_id for item in manager.async_entries()] == [ + assert manager.async_entry_ids() == [ "test1", "test2", "test3", @@ -531,7 +536,7 @@ async def test_remove_entry_if_not_loaded( result = await manager.async_remove("test2") assert result == {"require_restart": False} - assert [item.entry_id for item in manager.async_entries()] == ["test1", "test3"] + assert manager.async_entry_ids() == ["test1", "test3"] assert len(mock_unload_entry.mock_calls) == 0 @@ -546,7 +551,7 @@ async def test_remove_entry_if_integration_deleted( MockConfigEntry(domain="comp", entry_id="test2").add_to_manager(manager) MockConfigEntry(domain="test", entry_id="test3").add_to_manager(manager) - assert [item.entry_id for item in manager.async_entries()] == [ + assert manager.async_entry_ids() == [ "test1", "test2", "test3", @@ -555,7 +560,7 @@ async def test_remove_entry_if_integration_deleted( result = await manager.async_remove("test2") assert result == {"require_restart": False} - assert [item.entry_id for item in manager.async_entries()] == ["test1", "test3"] + assert manager.async_entry_ids() == ["test1", "test3"] assert len(mock_unload_entry.mock_calls) == 0 @@ -826,6 +831,8 @@ async def test_as_dict(snapshot: SnapshotAssertion) -> None: "_tries", "_setup_again_job", "_supports_options", + "_reconfigure_lock", + "supports_reconfigure", } entry = MockConfigEntry(entry_id="mock-entry") @@ -852,7 +859,7 @@ async def test_forward_entry_sets_up_component(hass: HomeAssistant) -> None: entry = MockConfigEntry(domain="original") mock_original_setup_entry = AsyncMock(return_value=True) - mock_integration( + integration = mock_integration( hass, MockModule("original", async_setup_entry=mock_original_setup_entry) ) @@ -861,7 +868,10 @@ async def test_forward_entry_sets_up_component(hass: HomeAssistant) -> None: hass, MockModule("forwarded", async_setup_entry=mock_forwarded_setup_entry) ) - await hass.config_entries.async_forward_entry_setup(entry, "forwarded") + with patch.object(integration, "async_get_platform") as mock_async_get_platform: + await hass.config_entries.async_forward_entry_setup(entry, "forwarded") + + mock_async_get_platform.assert_called_once_with("forwarded") assert len(mock_original_setup_entry.mock_calls) == 0 assert len(mock_forwarded_setup_entry.mock_calls) == 1 @@ -1448,13 +1458,13 @@ async def test_entry_setup_succeed( @pytest.mark.parametrize( "state", - ( + [ config_entries.ConfigEntryState.LOADED, config_entries.ConfigEntryState.SETUP_ERROR, config_entries.ConfigEntryState.MIGRATION_ERROR, config_entries.ConfigEntryState.SETUP_RETRY, config_entries.ConfigEntryState.FAILED_UNLOAD, - ), + ], ) async def test_entry_setup_invalid_state( hass: HomeAssistant, @@ -1499,11 +1509,11 @@ async def test_entry_unload_succeed( @pytest.mark.parametrize( "state", - ( + [ config_entries.ConfigEntryState.NOT_LOADED, config_entries.ConfigEntryState.SETUP_ERROR, config_entries.ConfigEntryState.SETUP_RETRY, - ), + ], ) async def test_entry_unload_failed_to_load( hass: HomeAssistant, @@ -1525,10 +1535,10 @@ async def test_entry_unload_failed_to_load( @pytest.mark.parametrize( "state", - ( + [ config_entries.ConfigEntryState.MIGRATION_ERROR, config_entries.ConfigEntryState.FAILED_UNLOAD, - ), + ], ) async def test_entry_unload_invalid_state( hass: HomeAssistant, @@ -1581,11 +1591,11 @@ async def test_entry_reload_succeed( @pytest.mark.parametrize( "state", - ( + [ config_entries.ConfigEntryState.NOT_LOADED, config_entries.ConfigEntryState.SETUP_ERROR, config_entries.ConfigEntryState.SETUP_RETRY, - ), + ], ) async def test_entry_reload_not_loaded( hass: HomeAssistant, @@ -1620,10 +1630,10 @@ async def test_entry_reload_not_loaded( @pytest.mark.parametrize( "state", - ( + [ config_entries.ConfigEntryState.MIGRATION_ERROR, config_entries.ConfigEntryState.FAILED_UNLOAD, - ), + ], ) async def test_entry_reload_error( hass: HomeAssistant, @@ -1781,9 +1791,12 @@ async def test_init_custom_integration(hass: HomeAssistant) -> None: None, {"name": "Hue", "dependencies": [], "requirements": [], "domain": "hue"}, ) - with pytest.raises(data_entry_flow.UnknownHandler), patch( - "homeassistant.loader.async_get_integration", - return_value=integration, + with ( + pytest.raises(data_entry_flow.UnknownHandler), + patch( + "homeassistant.loader.async_get_integration", + return_value=integration, + ), ): await hass.config_entries.flow.async_init("bla", context={"source": "user"}) @@ -1803,9 +1816,12 @@ async def test_init_custom_integration_with_missing_handler( MockModule("hue"), ) mock_platform(hass, "hue.config_flow", None) - with pytest.raises(data_entry_flow.UnknownHandler), patch( - "homeassistant.loader.async_get_integration", - return_value=integration, + with ( + pytest.raises(data_entry_flow.UnknownHandler), + patch( + "homeassistant.loader.async_get_integration", + return_value=integration, + ), ): await hass.config_entries.flow.async_init("bla", context={"source": "user"}) @@ -2003,11 +2019,13 @@ async def test_entry_id_existing_entry( """Test user step.""" return self.async_create_entry(title="mock-title", data={"via": "flow"}) - with pytest.raises(HomeAssistantError), patch.dict( - config_entries.HANDLERS, {"comp": TestFlow} - ), patch( - "homeassistant.config_entries.uuid_util.random_uuid_hex", - return_value=collide_entry_id, + with ( + pytest.raises(HomeAssistantError), + patch.dict(config_entries.HANDLERS, {"comp": TestFlow}), + patch( + "homeassistant.config_entries.uuid_util.random_uuid_hex", + return_value=collide_entry_id, + ), ): await manager.flow.async_init( "comp", context={"source": config_entries.SOURCE_USER} @@ -2045,9 +2063,12 @@ async def test_unique_id_update_existing_entry_without_reload( updates={"host": "1.1.1.1"}, reload_on_update=False ) - with patch.dict(config_entries.HANDLERS, {"comp": TestFlow}), patch( - "homeassistant.config_entries.ConfigEntries.async_reload" - ) as async_reload: + with ( + patch.dict(config_entries.HANDLERS, {"comp": TestFlow}), + patch( + "homeassistant.config_entries.ConfigEntries.async_reload" + ) as async_reload, + ): result = await manager.flow.async_init( "comp", context={"source": config_entries.SOURCE_USER} ) @@ -2092,9 +2113,12 @@ async def test_unique_id_update_existing_entry_with_reload( updates=updates, reload_on_update=True ) - with patch.dict(config_entries.HANDLERS, {"comp": TestFlow}), patch( - "homeassistant.config_entries.ConfigEntries.async_reload" - ) as async_reload: + with ( + patch.dict(config_entries.HANDLERS, {"comp": TestFlow}), + patch( + "homeassistant.config_entries.ConfigEntries.async_reload" + ) as async_reload, + ): result = await manager.flow.async_init( "comp", context={"source": config_entries.SOURCE_USER} ) @@ -2109,9 +2133,12 @@ async def test_unique_id_update_existing_entry_with_reload( # Test we don't reload if entry not started updates["host"] = "2.2.2.2" entry._async_set_state(hass, config_entries.ConfigEntryState.NOT_LOADED, None) - with patch.dict(config_entries.HANDLERS, {"comp": TestFlow}), patch( - "homeassistant.config_entries.ConfigEntries.async_reload" - ) as async_reload: + with ( + patch.dict(config_entries.HANDLERS, {"comp": TestFlow}), + patch( + "homeassistant.config_entries.ConfigEntries.async_reload" + ) as async_reload, + ): result = await manager.flow.async_init( "comp", context={"source": config_entries.SOURCE_USER} ) @@ -2163,9 +2190,12 @@ async def test_unique_id_from_discovery_in_setup_retry( self._abort_if_unique_id_configured() # Verify we do not reload from a user source - with patch.dict(config_entries.HANDLERS, {"comp": TestFlow}), patch( - "homeassistant.config_entries.ConfigEntries.async_reload" - ) as async_reload: + with ( + patch.dict(config_entries.HANDLERS, {"comp": TestFlow}), + patch( + "homeassistant.config_entries.ConfigEntries.async_reload" + ) as async_reload, + ): result = await manager.flow.async_init( "comp", context={"source": config_entries.SOURCE_USER} ) @@ -2176,9 +2206,12 @@ async def test_unique_id_from_discovery_in_setup_retry( assert len(async_reload.mock_calls) == 0 # Verify do reload from a discovery source - with patch.dict(config_entries.HANDLERS, {"comp": TestFlow}), patch( - "homeassistant.config_entries.ConfigEntries.async_reload" - ) as async_reload: + with ( + patch.dict(config_entries.HANDLERS, {"comp": TestFlow}), + patch( + "homeassistant.config_entries.ConfigEntries.async_reload" + ) as async_reload, + ): discovery_result = await manager.flow.async_init( "comp", context={"source": config_entries.SOURCE_DHCP}, @@ -2225,9 +2258,12 @@ async def test_unique_id_not_update_existing_entry( updates={"host": "0.0.0.0"}, reload_on_update=True ) - with patch.dict(config_entries.HANDLERS, {"comp": TestFlow}), patch( - "homeassistant.config_entries.ConfigEntries.async_reload" - ) as async_reload: + with ( + patch.dict(config_entries.HANDLERS, {"comp": TestFlow}), + patch( + "homeassistant.config_entries.ConfigEntries.async_reload" + ) as async_reload, + ): result = await manager.flow.async_init( "comp", context={"source": config_entries.SOURCE_USER} ) @@ -2395,9 +2431,12 @@ async def test_manual_add_overrides_ignored_entry( async def async_step_step2(self, user_input=None): raise NotImplementedError - with patch.dict(config_entries.HANDLERS, {"comp": TestFlow}), patch( - "homeassistant.config_entries.ConfigEntries.async_reload" - ) as async_reload: + with ( + patch.dict(config_entries.HANDLERS, {"comp": TestFlow}), + patch( + "homeassistant.config_entries.ConfigEntries.async_reload" + ) as async_reload, + ): result = await manager.flow.async_init( "comp", context={"source": config_entries.SOURCE_USER} ) @@ -2921,7 +2960,7 @@ async def test_async_setup_update_entry(hass: HomeAssistant) -> None: @pytest.mark.parametrize( "discovery_source", - ( + [ (config_entries.SOURCE_BLUETOOTH, BaseServiceInfo()), (config_entries.SOURCE_DISCOVERY, {}), (config_entries.SOURCE_SSDP, BaseServiceInfo()), @@ -2933,7 +2972,7 @@ async def test_async_setup_update_entry(hass: HomeAssistant) -> None: config_entries.SOURCE_HASSIO, HassioServiceInfo(config={}, name="Test", slug="test", uuid="1234"), ), - ), + ], ) async def test_flow_with_default_discovery( hass: HomeAssistant, @@ -3905,9 +3944,9 @@ async def test_entry_reload_concurrency( ), ) mock_platform(hass, "comp.config_flow", None) - tasks = [] - for _ in range(15): - tasks.append(asyncio.create_task(manager.async_reload(entry.entry_id))) + tasks = [ + asyncio.create_task(manager.async_reload(entry.entry_id)) for _ in range(15) + ] await asyncio.gather(*tasks) assert entry.state is config_entries.ConfigEntryState.LOADED assert loaded == 1 @@ -3963,9 +4002,12 @@ async def test_unique_id_update_while_setup_in_progress( updates=updates, reload_on_update=True ) - with patch.dict(config_entries.HANDLERS, {"comp": TestFlow}), patch( - "homeassistant.config_entries.ConfigEntries.async_reload" - ) as async_reload: + with ( + patch.dict(config_entries.HANDLERS, {"comp": TestFlow}), + patch( + "homeassistant.config_entries.ConfigEntries.async_reload" + ) as async_reload, + ): result = await manager.flow.async_init( "comp", context={"source": config_entries.SOURCE_USER} ) @@ -4062,6 +4104,94 @@ async def test_reauth(hass: HomeAssistant) -> None: assert len(hass.config_entries.flow.async_progress()) == 1 +async def test_reconfigure(hass: HomeAssistant) -> None: + """Test the async_reconfigure_helper.""" + entry = MockConfigEntry(title="test_title", domain="test") + entry2 = 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_platform(hass, "test.config_flow", 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_reconfigure( + 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_RECONFIGURE + 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 + + assert entry.entry_id != entry2.entry_id + + # Check that we can't start duplicate reconfigure flows + entry.async_start_reconfigure(hass, {"extra_context": "some_extra_context"}) + await hass.async_block_till_done() + assert len(hass.config_entries.flow.async_progress()) == 1 + + # Check that we can't start duplicate reconfigure flows when the context is different + entry.async_start_reconfigure(hass, {"diff": "diff"}) + await hass.async_block_till_done() + assert len(hass.config_entries.flow.async_progress()) == 1 + + # Check that we can start a reconfigure flow for a different entry + entry2.async_start_reconfigure(hass, {"extra_context": "some_extra_context"}) + await hass.async_block_till_done() + assert len(hass.config_entries.flow.async_progress()) == 2 + + # Abort all existing flows + for flow in hass.config_entries.flow.async_progress(): + hass.config_entries.flow.async_abort(flow["flow_id"]) + await hass.async_block_till_done() + + # Check that we can't start duplicate reconfigure flows + # without blocking between flows + entry.async_start_reconfigure(hass, {"extra_context": "some_extra_context"}) + entry.async_start_reconfigure(hass, {"extra_context": "some_extra_context"}) + entry.async_start_reconfigure(hass, {"extra_context": "some_extra_context"}) + entry.async_start_reconfigure(hass, {"extra_context": "some_extra_context"}) + await hass.async_block_till_done() + assert len(hass.config_entries.flow.async_progress()) == 1 + + # Abort all existing flows + for flow in hass.config_entries.flow.async_progress(): + hass.config_entries.flow.async_abort(flow["flow_id"]) + await hass.async_block_till_done() + + # Check that we can't start reconfigure flows with active reauth flow + entry.async_start_reauth(hass, {"extra_context": "some_extra_context"}) + await hass.async_block_till_done() + assert len(hass.config_entries.flow.async_progress()) == 1 + entry.async_start_reconfigure(hass, {"extra_context": "some_extra_context"}) + await hass.async_block_till_done() + assert len(hass.config_entries.flow.async_progress()) == 1 + + # Abort all existing flows + for flow in hass.config_entries.flow.async_progress(): + hass.config_entries.flow.async_abort(flow["flow_id"]) + await hass.async_block_till_done() + + # Check that we can't start reauth flows with active reconfigure flow + entry.async_start_reconfigure(hass, {"extra_context": "some_extra_context"}) + await hass.async_block_till_done() + assert len(hass.config_entries.flow.async_progress()) == 1 + entry.async_start_reauth(hass, {"extra_context": "some_extra_context"}) + await hass.async_block_till_done() + assert len(hass.config_entries.flow.async_progress()) == 1 + + async def test_get_active_flows(hass: HomeAssistant) -> None: """Test the async_get_active_flows helper.""" entry = MockConfigEntry(title="test_title", domain="test") @@ -4238,7 +4368,12 @@ async def test_task_tracking(hass: HomeAssistant) -> None: await asyncio.sleep(0) hass.loop.call_soon(event.set) await entry._async_process_on_unload(hass) - assert results == ["on_unload", "background", "background", "normal"] + assert results == [ + "background", + "background", + "normal", + "on_unload", + ] async def test_preview_supported( @@ -4623,10 +4758,13 @@ async def test_avoid_adding_second_config_entry_on_single_config_entry( mock_integration(hass, MockModule("comp")) mock_platform(hass, "comp.config_flow", None) - with patch( - "homeassistant.loader.async_get_integration", - return_value=integration, - ), patch.dict(config_entries.HANDLERS, {"comp": TestFlow}): + with ( + patch( + "homeassistant.loader.async_get_integration", + return_value=integration, + ), + patch.dict(config_entries.HANDLERS, {"comp": TestFlow}), + ): # Start a flow result = await manager.flow.async_init( "comp", context={"source": config_entries.SOURCE_USER} @@ -4683,9 +4821,12 @@ async def test_in_progress_get_canceled_when_entry_is_created( return self.async_show_form(step_id="user") - with patch.dict(config_entries.HANDLERS, {"comp": TestFlow}), patch( - "homeassistant.loader.async_get_integration", - return_value=integration, + with ( + patch.dict(config_entries.HANDLERS, {"comp": TestFlow}), + patch( + "homeassistant.loader.async_get_integration", + return_value=integration, + ), ): # Create one to be in progress result = await manager.flow.async_init( @@ -4736,7 +4877,7 @@ async def test_directly_mutating_blocked( @pytest.mark.parametrize( "field", - ( + [ "data", "options", "title", @@ -4744,7 +4885,7 @@ async def test_directly_mutating_blocked( "pref_disable_polling", "minor_version", "version", - ), + ], ) async def test_report_direct_mutation_of_config_entry( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, field: str diff --git a/tests/test_const.py b/tests/test_const.py index 7ca4812ca8e..63b01388dd7 100644 --- a/tests/test_const.py +++ b/tests/test_const.py @@ -1,6 +1,5 @@ """Test const module.""" - from enum import Enum import pytest @@ -18,10 +17,7 @@ from tests.common import ( def _create_tuples( value: Enum | list[Enum], constant_prefix: str ) -> list[tuple[Enum, str]]: - result = [] - for enum in value: - result.append((enum, constant_prefix)) - return result + return [(enum, constant_prefix) for enum in value] def test_all() -> None: @@ -131,7 +127,16 @@ def test_all() -> None: ], "PRECIPITATION_", ) - + _create_tuples(const.UnitOfSpeed, "SPEED_") + + _create_tuples( + [ + const.UnitOfSpeed.FEET_PER_SECOND, + const.UnitOfSpeed.METERS_PER_SECOND, + const.UnitOfSpeed.KILOMETERS_PER_HOUR, + const.UnitOfSpeed.KNOTS, + const.UnitOfSpeed.MILES_PER_HOUR, + ], + "SPEED_", + ) + _create_tuples( [ const.UnitOfVolumetricFlux.MILLIMETERS_PER_DAY, diff --git a/tests/test_core.py b/tests/test_core.py index 987f228bea8..a0a197096cd 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1,4 +1,5 @@ """Test to verify that Home Assistant core works.""" + from __future__ import annotations import array @@ -8,7 +9,6 @@ import functools import gc import logging import os -import sys from tempfile import TemporaryDirectory import threading import time @@ -33,6 +33,7 @@ from homeassistant.const import ( EVENT_SERVICE_REGISTERED, EVENT_SERVICE_REMOVED, EVENT_STATE_CHANGED, + EVENT_STATE_REPORTED, MATCH_ALL, __version__, ) @@ -56,6 +57,8 @@ from homeassistant.exceptions import ( ServiceNotFound, ) from homeassistant.helpers.json import json_dumps +from homeassistant.setup import async_setup_component +from homeassistant.util.async_ import create_eager_task import homeassistant.util.dt as dt_util from homeassistant.util.read_only_dict import ReadOnlyDict from homeassistant.util.unit_system import METRIC_SYSTEM @@ -96,6 +99,130 @@ async def test_async_add_hass_job_schedule_callback() -> None: assert len(hass.add_job.mock_calls) == 0 +async def test_async_add_hass_job_eager_start_coro_suspends( + hass: HomeAssistant, +) -> None: + """Test scheduling a coro as a task that will suspend with eager_start.""" + + async def job_that_suspends(): + await asyncio.sleep(0) + + task = hass.async_add_hass_job( + ha.HassJob(ha.callback(job_that_suspends)), eager_start=True + ) + assert not task.done() + assert task in hass._tasks + await task + assert task not in hass._tasks + + +async def test_async_run_hass_job_eager_start_coro_suspends( + hass: HomeAssistant, +) -> None: + """Test scheduling a coro as a task that will suspend with eager_start.""" + + async def job_that_suspends(): + await asyncio.sleep(0) + + task = hass.async_run_hass_job(ha.HassJob(ha.callback(job_that_suspends))) + assert not task.done() + assert task in hass._tasks + await task + assert task not in hass._tasks + + +async def test_async_add_hass_job_background(hass: HomeAssistant) -> None: + """Test scheduling a coro as a background task with async_add_hass_job.""" + + async def job_that_suspends(): + await asyncio.sleep(0) + + task = hass.async_add_hass_job( + ha.HassJob(ha.callback(job_that_suspends)), background=True + ) + assert not task.done() + assert task in hass._background_tasks + await task + assert task not in hass._background_tasks + + +async def test_async_run_hass_job_background(hass: HomeAssistant) -> None: + """Test scheduling a coro as a background task with async_run_hass_job.""" + + async def job_that_suspends(): + await asyncio.sleep(0) + + task = hass.async_run_hass_job( + ha.HassJob(ha.callback(job_that_suspends)), background=True + ) + assert not task.done() + assert task in hass._background_tasks + await task + assert task not in hass._background_tasks + + +async def test_async_add_hass_job_eager_background(hass: HomeAssistant) -> None: + """Test scheduling a coro as an eager background task with async_add_hass_job.""" + + async def job_that_suspends(): + await asyncio.sleep(0) + + task = hass.async_add_hass_job( + ha.HassJob(ha.callback(job_that_suspends)), background=True + ) + assert not task.done() + assert task in hass._background_tasks + await task + assert task not in hass._background_tasks + + +async def test_async_run_hass_job_eager_background(hass: HomeAssistant) -> None: + """Test scheduling a coro as an eager background task with async_run_hass_job.""" + + async def job_that_suspends(): + await asyncio.sleep(0) + + task = hass.async_run_hass_job( + ha.HassJob(ha.callback(job_that_suspends)), background=True + ) + assert not task.done() + assert task in hass._background_tasks + await task + assert task not in hass._background_tasks + + +async def test_async_run_hass_job_background_synchronous(hass: HomeAssistant) -> None: + """Test scheduling a coro as an eager background task with async_run_hass_job.""" + + async def job_that_does_not_suspends(): + pass + + task = hass.async_run_hass_job( + ha.HassJob(ha.callback(job_that_does_not_suspends)), + background=True, + ) + assert task.done() + assert task not in hass._background_tasks + assert task not in hass._tasks + await task + + +async def test_async_run_hass_job_synchronous(hass: HomeAssistant) -> None: + """Test scheduling a coro as an eager task with async_run_hass_job.""" + + async def job_that_does_not_suspends(): + pass + + task = hass.async_run_hass_job( + ha.HassJob(ha.callback(job_that_does_not_suspends)), + background=False, + ) + assert task.done() + assert task not in hass._background_tasks + assert task not in hass._tasks + await task + + async def test_async_add_hass_job_coro_named(hass: HomeAssistant) -> None: """Test that we schedule coroutines and add jobs to the job pool with a name.""" @@ -109,6 +236,19 @@ async def test_async_add_hass_job_coro_named(hass: HomeAssistant) -> None: assert "named coro" in str(task) +async def test_async_add_hass_job_eager_start(hass: HomeAssistant) -> None: + """Test eager_start with async_add_hass_job.""" + + async def mycoro(): + pass + + job = ha.HassJob(mycoro, "named coro") + assert "named coro" in str(job) + assert job.name == "named coro" + task = ha.HomeAssistant.async_add_hass_job(hass, job, eager_start=True) + assert "named coro" in str(task) + + async def test_async_add_hass_job_schedule_partial_callback() -> None: """Test that we schedule partial coros and add jobs to the job pool.""" hass = MagicMock() @@ -134,6 +274,24 @@ async def test_async_add_hass_job_schedule_coroutinefunction() -> None: assert len(hass.add_job.mock_calls) == 0 +async def test_async_add_hass_job_schedule_corofunction_eager_start() -> None: + """Test that we schedule coroutines and add jobs to the job pool.""" + hass = MagicMock(loop=MagicMock(wraps=asyncio.get_running_loop())) + + async def job(): + pass + + with patch( + "homeassistant.core.create_eager_task", wraps=create_eager_task + ) as mock_create_eager_task: + hass_job = ha.HassJob(job) + task = ha.HomeAssistant.async_add_hass_job(hass, hass_job, eager_start=True) + assert len(hass.loop.call_soon.mock_calls) == 0 + assert len(hass.add_job.mock_calls) == 0 + assert mock_create_eager_task.mock_calls + await task + + async def test_async_add_hass_job_schedule_partial_coroutinefunction() -> None: """Test that we schedule partial coros and add jobs to the job pool.""" hass = MagicMock(loop=MagicMock(wraps=asyncio.get_running_loop())) @@ -175,9 +333,6 @@ async def test_async_create_task_schedule_coroutine() -> None: assert len(hass.add_job.mock_calls) == 0 -@pytest.mark.skipif( - sys.version_info < (3, 12), reason="eager_start is only supported for Python 3.12" -) async def test_async_create_task_eager_start_schedule_coroutine() -> None: """Test that we schedule coroutines and add jobs to the job pool.""" hass = MagicMock(loop=MagicMock(wraps=asyncio.get_running_loop())) @@ -191,24 +346,6 @@ async def test_async_create_task_eager_start_schedule_coroutine() -> None: assert len(hass.add_job.mock_calls) == 0 -@pytest.mark.skipif( - sys.version_info >= (3, 12), reason="eager_start is not supported on < 3.12" -) -async def test_async_create_task_eager_start_fallback_schedule_coroutine() -> None: - """Test that we schedule coroutines and add jobs to the job pool.""" - hass = MagicMock(loop=MagicMock(wraps=asyncio.get_running_loop())) - - async def job(): - pass - - ha.HomeAssistant.async_create_task(hass, job(), eager_start=True) - assert len(hass.loop.call_soon.mock_calls) == 1 - # Should fallback to loop.create_task since 3.11 does - # not support eager_start - assert len(hass.loop.create_task.mock_calls) == 0 - assert len(hass.add_job.mock_calls) == 0 - - async def test_async_create_task_schedule_coroutine_with_name() -> None: """Test that we schedule coroutines and add jobs to the job pool with a name.""" hass = MagicMock(loop=MagicMock(wraps=asyncio.get_running_loop())) @@ -223,6 +360,30 @@ async def test_async_create_task_schedule_coroutine_with_name() -> None: assert "named task" in str(task) +async def test_async_run_eager_hass_job_calls_callback() -> None: + """Test that the callback annotation is respected.""" + hass = MagicMock() + calls = [] + + def job(): + asyncio.get_running_loop() # ensure we are in the event loop + calls.append(1) + + ha.HomeAssistant.async_run_hass_job(hass, ha.HassJob(ha.callback(job))) + assert len(calls) == 1 + + +async def test_async_run_eager_hass_job_calls_coro_function() -> None: + """Test running coros from async_run_hass_job with eager_start.""" + hass = MagicMock() + + async def job(): + pass + + ha.HomeAssistant.async_run_hass_job(hass, ha.HassJob(job)) + assert len(hass.async_add_hass_job.mock_calls) == 1 + + async def test_async_run_hass_job_calls_callback() -> None: """Test that the callback annotation is respected.""" hass = MagicMock() @@ -427,6 +588,46 @@ async def test_async_get_hass_can_be_called(hass: HomeAssistant) -> None: my_job_create_task.join() +async def test_async_add_executor_job_background(hass: HomeAssistant) -> None: + """Test running an executor job in the background.""" + calls = [] + + def job(): + time.sleep(0.01) + calls.append(1) + + async def _async_add_executor_job(): + await hass.async_add_executor_job(job) + + task = hass.async_create_background_task( + _async_add_executor_job(), "background", eager_start=True + ) + await hass.async_block_till_done() + assert len(calls) == 0 + await hass.async_block_till_done(wait_background_tasks=True) + assert len(calls) == 1 + await task + + +async def test_async_add_executor_job(hass: HomeAssistant) -> None: + """Test running an executor job.""" + calls = [] + + def job(): + time.sleep(0.01) + calls.append(1) + + async def _async_add_executor_job(): + await hass.async_add_executor_job(job) + + task = hass.async_create_task( + _async_add_executor_job(), "background", eager_start=True + ) + await hass.async_block_till_done() + assert len(calls) == 1 + await task + + async def test_stage_shutdown(hass: HomeAssistant) -> None: """Simulate a shutdown, test calling stuff.""" test_stop = async_capture_events(hass, EVENT_HOMEASSISTANT_STOP) @@ -514,7 +715,7 @@ async def test_shutdown_calls_block_till_done_after_shutdown_run_callback_thread """Ensure shutdown_run_callback_threadsafe is called before the final async_block_till_done.""" stop_calls = [] - async def _record_block_till_done(): + async def _record_block_till_done(wait_background_tasks: bool = False): nonlocal stop_calls stop_calls.append("async_block_till_done") @@ -522,9 +723,12 @@ async def test_shutdown_calls_block_till_done_after_shutdown_run_callback_thread nonlocal stop_calls stop_calls.append(("shutdown_run_callback_threadsafe", loop)) - with patch.object(hass, "async_block_till_done", _record_block_till_done), patch( - "homeassistant.core.shutdown_run_callback_threadsafe", - _record_shutdown_run_callback_threadsafe, + with ( + patch.object(hass, "async_block_till_done", _record_block_till_done), + patch( + "homeassistant.core.shutdown_run_callback_threadsafe", + _record_shutdown_run_callback_threadsafe, + ), ): await hass.async_stop() @@ -549,6 +753,19 @@ async def test_pending_scheduler(hass: HomeAssistant) -> None: assert len(call_count) == 3 +def test_add_job_pending_tasks_coro(hass: HomeAssistant) -> None: + """Add a coro to pending tasks.""" + + async def test_coro(): + """Test Coro.""" + + for _ in range(2): + hass.add_job(test_coro()) + + # Ensure add_job does not run immediately + assert len(hass._tasks) == 0 + + async def test_async_add_job_pending_tasks_coro(hass: HomeAssistant) -> None: """Add a coro to pending tasks.""" call_count = [] @@ -558,18 +775,12 @@ async def test_async_add_job_pending_tasks_coro(hass: HomeAssistant) -> None: call_count.append("call") for _ in range(2): - hass.add_job(test_coro()) - - async def wait_finish_callback(): - """Wait until all stuff is scheduled.""" - await asyncio.sleep(0) - await asyncio.sleep(0) - - await wait_finish_callback() + hass.async_add_job(test_coro()) assert len(hass._tasks) == 2 await hass.async_block_till_done() assert len(call_count) == 2 + assert len(hass._tasks) == 0 async def test_async_create_task_pending_tasks_coro(hass: HomeAssistant) -> None: @@ -581,18 +792,12 @@ async def test_async_create_task_pending_tasks_coro(hass: HomeAssistant) -> None call_count.append("call") for _ in range(2): - hass.create_task(test_coro()) - - async def wait_finish_callback(): - """Wait until all stuff is scheduled.""" - await asyncio.sleep(0) - await asyncio.sleep(0) - - await wait_finish_callback() + hass.async_create_task(test_coro()) assert len(hass._tasks) == 2 await hass.async_block_till_done() assert len(call_count) == 2 + assert len(hass._tasks) == 0 async def test_async_add_job_pending_tasks_executor(hass: HomeAssistant) -> None: @@ -654,18 +859,23 @@ def test_event_eq() -> None: data = {"some": "attr"} context = ha.Context() event1, event2 = ( - ha.Event("some_type", data, time_fired=now, context=context) for _ in range(2) + ha.Event( + "some_type", data, time_fired_timestamp=now.timestamp(), context=context + ) + for _ in range(2) ) assert event1.as_dict() == event2.as_dict() -def test_event_time_fired_timestamp() -> None: - """Test time_fired_timestamp.""" +def test_event_time() -> None: + """Test time_fired and time_fired_timestamp.""" now = dt_util.utcnow() - event = ha.Event("some_type", {"some": "attr"}, time_fired=now) - assert event.time_fired_timestamp == now.timestamp() + event = ha.Event( + "some_type", {"some": "attr"}, time_fired_timestamp=now.timestamp() + ) assert event.time_fired_timestamp == now.timestamp() + assert event.time_fired == now def test_event_json_fragment() -> None: @@ -674,7 +884,10 @@ def test_event_json_fragment() -> None: data = {"some": "attr"} context = ha.Context() event1, event2 = ( - ha.Event("some_type", data, time_fired=now, context=context) for _ in range(2) + ha.Event( + "some_type", data, time_fired_timestamp=now.timestamp(), context=context + ) + for _ in range(2) ) # We are testing that the JSON fragments are the same when as_dict is called @@ -716,7 +929,7 @@ def test_event_as_dict() -> None: now = dt_util.utcnow() data = {"some": "attr"} - event = ha.Event(event_type, data, ha.EventOrigin.local, now) + event = ha.Event(event_type, data, ha.EventOrigin.local, now.timestamp()) expected = { "event_type": event_type, "data": data, @@ -740,8 +953,9 @@ def test_state_as_dict() -> None: "happy.happy", "on", {"pig": "dog"}, - last_updated=last_time, last_changed=last_time, + last_reported=last_time, + last_updated=last_time, ) expected = { "context": { @@ -752,6 +966,7 @@ def test_state_as_dict() -> None: "entity_id": "happy.happy", "attributes": {"pig": "dog"}, "last_changed": last_time.isoformat(), + "last_reported": last_time.isoformat(), "last_updated": last_time.isoformat(), "state": "on", } @@ -772,13 +987,15 @@ def test_state_as_dict_json() -> None: "happy.happy", "on", {"pig": "dog"}, - last_updated=last_time, - last_changed=last_time, context=ha.Context(id="01H0D6K3RFJAYAV2093ZW30PCW"), + last_changed=last_time, + last_reported=last_time, + last_updated=last_time, ) expected = ( b'{"entity_id":"happy.happy","state":"on","attributes":{"pig":"dog"},' - b'"last_changed":"1984-12-08T12:00:00","last_updated":"1984-12-08T12:00:00",' + b'"last_changed":"1984-12-08T12:00:00","last_reported":"1984-12-08T12:00:00",' + b'"last_updated":"1984-12-08T12:00:00",' b'"context":{"id":"01H0D6K3RFJAYAV2093ZW30PCW","parent_id":null,"user_id":null}}' ) as_dict_json_1 = state.as_dict_json @@ -796,9 +1013,10 @@ def test_state_json_fragment() -> None: "happy.happy", "on", {"pig": "dog"}, - last_updated=last_time, - last_changed=last_time, context=ha.Context(id="01H0D6K3RFJAYAV2093ZW30PCW"), + last_changed=last_time, + last_reported=last_time, + last_updated=last_time, ) for _ in range(2) ) @@ -926,9 +1144,9 @@ async def test_eventbus_filtered_listener(hass: HomeAssistant) -> None: calls.append(event) @ha.callback - def filter(event): + def filter(event_data): """Mock filter.""" - return not event.data["filtered"] + return not event_data["filtered"] unsub = hass.bus.async_listen("test", listener, event_filter=filter) @@ -945,8 +1163,8 @@ async def test_eventbus_filtered_listener(hass: HomeAssistant) -> None: unsub() -async def test_eventbus_run_immediately(hass: HomeAssistant) -> None: - """Test we can call events immediately.""" +async def test_eventbus_run_immediately_callback(hass: HomeAssistant) -> None: + """Test we can call events immediately with a callback.""" calls = [] @ha.callback @@ -963,14 +1181,36 @@ async def test_eventbus_run_immediately(hass: HomeAssistant) -> None: unsub() -async def test_eventbus_run_immediately_not_callback(hass: HomeAssistant) -> None: - """Test we raise when passing a non-callback with run_immediately.""" +async def test_eventbus_run_immediately_coro(hass: HomeAssistant) -> None: + """Test we can call events immediately with a coro.""" + calls = [] - def listener(event): + async def listener(event): """Mock listener.""" + calls.append(event) - with pytest.raises(HomeAssistantError): - hass.bus.async_listen("test", listener, run_immediately=True) + unsub = hass.bus.async_listen("test", listener, run_immediately=True) + + hass.bus.async_fire("test", {"event": True}) + # No async_block_till_done here + assert len(calls) == 1 + + unsub() + + +async def test_eventbus_listen_once_run_immediately_coro(hass: HomeAssistant) -> None: + """Test we can call events immediately with a coro.""" + calls = [] + + async def listener(event): + """Mock listener.""" + calls.append(event) + + hass.bus.async_listen_once("test", listener, run_immediately=True) + + hass.bus.async_fire("test", {"event": True}) + # No async_block_till_done here + assert len(calls) == 1 async def test_eventbus_unsubscribe_listener(hass: HomeAssistant) -> None: @@ -1096,9 +1336,26 @@ async def test_eventbus_max_length_exceeded(hass: HomeAssistant) -> None: "this_event_exceeds_the_max_character_length_even_with_the_new_limit" ) + # Without cached translations the translation key is returned with pytest.raises(MaxLengthExceeded) as exc_info: hass.bus.async_fire(long_evt_name) + assert str(exc_info.value) == "max_length_exceeded" + assert exc_info.value.property_name == "event_type" + assert exc_info.value.max_length == 64 + assert exc_info.value.value == long_evt_name + + # Fetch translations + await async_setup_component(hass, "homeassistant", {}) + + # With cached translations the formatted message is returned + with pytest.raises(MaxLengthExceeded) as exc_info: + hass.bus.async_fire(long_evt_name) + + assert ( + str(exc_info.value) + == f"Value {long_evt_name} for property event_type has a maximum length of 64 characters" + ) assert exc_info.value.property_name == "event_type" assert exc_info.value.max_length == 64 assert exc_info.value.value == long_evt_name @@ -1174,7 +1431,7 @@ def test_state_repr() -> None: "happy.happy", "on", {"brightness": 144}, - datetime(1984, 12, 8, 12, 0, 0), + last_changed=datetime(1984, 12, 8, 12, 0, 0), ) ) == "" @@ -1434,14 +1691,25 @@ async def test_serviceregistry_remove_service(hass: HomeAssistant) -> None: async def test_serviceregistry_service_that_not_exists(hass: HomeAssistant) -> None: """Test remove service that not exists.""" + await async_setup_component(hass, "homeassistant", {}) calls_remove = async_capture_events(hass, EVENT_SERVICE_REMOVED) assert not hass.services.has_service("test_xxx", "test_yyy") hass.services.async_remove("test_xxx", "test_yyy") await hass.async_block_till_done() assert len(calls_remove) == 0 - with pytest.raises(ServiceNotFound): + with pytest.raises(ServiceNotFound) as exc: await hass.services.async_call("test_do_not", "exist", {}) + assert exc.value.translation_domain == "homeassistant" + assert exc.value.translation_key == "service_not_found" + assert exc.value.translation_placeholders == { + "domain": "test_do_not", + "service": "exist", + } + assert exc.value.domain == "test_do_not" + assert exc.value.service == "exist" + + assert str(exc.value) == "Service test_do_not.exist not found" async def test_serviceregistry_async_service_raise_exception( @@ -1641,6 +1909,7 @@ async def test_serviceregistry_return_response_optional( async def test_config_defaults() -> None: """Test config defaults.""" hass = Mock() + hass.data = {} config = ha.Config(hass, "/test/ha-config") assert config.hass is hass assert config.latitude == 0 @@ -1668,20 +1937,25 @@ async def test_config_defaults() -> None: async def test_config_path_with_file() -> None: """Test get_config_path method.""" - config = ha.Config(None, "/test/ha-config") + hass = Mock() + hass.data = {} + config = ha.Config(hass, "/test/ha-config") assert config.path("test.conf") == "/test/ha-config/test.conf" async def test_config_path_with_dir_and_file() -> None: """Test get_config_path method.""" - config = ha.Config(None, "/test/ha-config") + hass = Mock() + hass.data = {} + config = ha.Config(hass, "/test/ha-config") assert config.path("dir", "test.conf") == "/test/ha-config/dir/test.conf" async def test_config_as_dict() -> None: """Test as dict.""" - config = ha.Config(None, "/test/ha-config") - config.hass = MagicMock() + hass = Mock() + hass.data = {} + config = ha.Config(hass, "/test/ha-config") type(config.hass.state).value = PropertyMock(return_value="RUNNING") expected = { "latitude": 0, @@ -1712,7 +1986,9 @@ async def test_config_as_dict() -> None: async def test_config_is_allowed_path() -> None: """Test is_allowed_path method.""" - config = ha.Config(None, "/test/ha-config") + hass = Mock() + hass.data = {} + config = ha.Config(hass, "/test/ha-config") with TemporaryDirectory() as tmp_dir: # The created dir is in /tmp. This is a symlink on OS X # causing this test to fail unless we resolve path first. @@ -1744,7 +2020,9 @@ async def test_config_is_allowed_path() -> None: async def test_config_is_allowed_external_url() -> None: """Test is_allowed_external_url method.""" - config = ha.Config(None, "/test/ha-config") + hass = Mock() + hass.data = {} + config = ha.Config(hass, "/test/ha-config") config.allowlist_external_urls = [ "http://x.com/", "https://y.com/bla/", @@ -1920,6 +2198,34 @@ async def test_async_functions_with_callback(hass: HomeAssistant) -> None: assert len(runs) == 3 +async def test_async_run_job_starts_tasks_eagerly(hass: HomeAssistant) -> None: + """Test async_run_job starts tasks eagerly.""" + runs = [] + + async def _test(): + runs.append(True) + + task = hass.async_run_job(_test) + # No call to hass.async_block_till_done to ensure the task is run eagerly + assert len(runs) == 1 + assert task.done() + await task + + +async def test_async_run_job_starts_coro_eagerly(hass: HomeAssistant) -> None: + """Test async_run_job starts coros eagerly.""" + runs = [] + + async def _test(): + runs.append(True) + + task = hass.async_run_job(_test()) + # No call to hass.async_block_till_done to ensure the task is run eagerly + assert len(runs) == 1 + assert task.done() + await task + + def test_valid_entity_id() -> None: """Test valid entity ID.""" for invalid in [ @@ -2098,9 +2404,9 @@ async def test_chained_logging_hits_log_timeout( return hass.async_create_task(_task_chain_1()) - with patch.object(ha, "BLOCK_LOG_TIMEOUT", 0.0001): + with patch.object(ha, "BLOCK_LOG_TIMEOUT", 0.0): hass.async_create_task(_task_chain_1()) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=False) assert "_task_chain_" in caplog.text @@ -2190,7 +2496,7 @@ async def test_hassjob_forbid_coroutine() -> None: coro = bla() with pytest.raises(ValueError): - ha.HassJob(coro) + _ = ha.HassJob(coro).job_type # To avoid warning about unawaited coro await coro @@ -2535,11 +2841,14 @@ def test_state_timestamps() -> None: "on", {"brightness": 100}, last_changed=now, + last_reported=now, last_updated=now, context=ha.Context(id="1234"), ) assert state.last_changed_timestamp == now.timestamp() assert state.last_changed_timestamp == now.timestamp() + assert state.last_reported_timestamp == now.timestamp() + assert state.last_reported_timestamp == now.timestamp() assert state.last_updated_timestamp == now.timestamp() assert state.last_updated_timestamp == now.timestamp() @@ -2633,7 +2942,7 @@ async def test_state_changed_events_to_not_leak_contexts(hass: HomeAssistant) -> assert len(_get_by_type("homeassistant.core.Context")) == init_count -@pytest.mark.parametrize("eager_start", (True, False)) +@pytest.mark.parametrize("eager_start", [True, False]) async def test_background_task(hass: HomeAssistant, eager_start: bool) -> None: """Test background tasks being quit.""" result = asyncio.Future() @@ -2704,14 +3013,15 @@ async def test_shutdown_does_not_block_on_shielded_tasks( sleep_task.cancel() -async def test_cancellable_hassjob(hass: HomeAssistant) -> None: +@pytest.mark.parametrize("eager_start", [True, False]) +async def test_cancellable_hassjob(hass: HomeAssistant, eager_start: bool) -> None: """Simulate a shutdown, ensure cancellable jobs are cancelled.""" job = MagicMock() @ha.callback def run_job(job: HassJob) -> None: """Call the action.""" - hass.async_run_hass_job(job) + hass.async_run_hass_job(job, eager_start=True) timer1 = hass.loop.call_later( 60, run_job, HassJob(ha.callback(job), cancel_on_shutdown=True) @@ -2864,7 +3174,7 @@ def test_one_time_listener_repr(hass: HomeAssistant) -> None: def _listener(event: ha.Event): """Test listener.""" - one_time_listener = ha._OneTimeListener(hass, _listener) + one_time_listener = ha._OneTimeListener(hass, HassJob(_listener)) repr_str = repr(one_time_listener) assert "OneTimeListener" in repr_str assert "test_core" in repr_str @@ -2885,3 +3195,178 @@ async def test_async_add_import_executor_job(hass: HomeAssistant) -> None: assert await future is evt assert hass.import_executor._max_workers == 1 + + +async def test_async_run_job_deprecated( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test async_run_job warns about its deprecation.""" + + async def _test(): + pass + + hass.async_run_job(_test) + assert ( + "Detected code that calls `async_run_job`, which is deprecated " + "and will be removed in Home Assistant 2025.4; Please review " + "https://developers.home-assistant.io/blog/2024/03/13/deprecate_add_run_job" + " for replacement options" + ) in caplog.text + + +async def test_async_add_job_deprecated( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test async_add_job warns about its deprecation.""" + + async def _test(): + pass + + hass.async_add_job(_test) + assert ( + "Detected code that calls `async_add_job`, which is deprecated " + "and will be removed in Home Assistant 2025.4; Please review " + "https://developers.home-assistant.io/blog/2024/03/13/deprecate_add_run_job" + " for replacement options" + ) in caplog.text + + +async def test_eventbus_lazy_object_creation(hass: HomeAssistant) -> None: + """Test we don't create unneeded objects when firing events.""" + calls = [] + + @ha.callback + def listener(event): + """Mock listener.""" + calls.append(event) + + @ha.callback + def filter(event_data): + """Mock filter.""" + return not event_data["filtered"] + + unsub = hass.bus.async_listen("test_1", listener, event_filter=filter) + + # Test lazy creation of Event objects + with patch("homeassistant.core.Event") as mock_event: + # Fire an event which is filtered out by its listener + hass.bus.async_fire("test_1", {"filtered": True}) + await hass.async_block_till_done() + mock_event.assert_not_called() + assert len(calls) == 0 + + # Fire an event which has no listener + hass.bus.async_fire("test_2") + await hass.async_block_till_done() + mock_event.assert_not_called() + assert len(calls) == 0 + + # Fire an event which is not filtered out by its listener + hass.bus.async_fire("test_1", {"filtered": False}) + await hass.async_block_till_done() + mock_event.assert_called_once() + assert len(calls) == 1 + + calls = [] + # Test lazy creation of Context objects + with patch("homeassistant.core.Context") as mock_context: + # Fire an event which is filtered out by its listener + hass.bus.async_fire("test_1", {"filtered": True}) + await hass.async_block_till_done() + mock_context.assert_not_called() + assert len(calls) == 0 + + # Fire an event which has no listener + hass.bus.async_fire("test_2") + await hass.async_block_till_done() + mock_context.assert_not_called() + assert len(calls) == 0 + + # Fire an event which is not filtered out by its listener + hass.bus.async_fire("test_1", {"filtered": False}) + await hass.async_block_till_done() + mock_context.assert_called_once() + assert len(calls) == 1 + + unsub() + + +async def test_event_filter_sanity_checks(hass: HomeAssistant) -> None: + """Test raising on bad event filters.""" + + @ha.callback + def listener(event): + """Mock listener.""" + + def bad_filter(event_data): + """Mock filter.""" + return False + + with pytest.raises(HomeAssistantError): + hass.bus.async_listen("test", listener, event_filter=bad_filter) + + +async def test_statemachine_report_state(hass: HomeAssistant) -> None: + """Test report state event.""" + + @ha.callback + def filter(event_data): + """Mock filter.""" + return True + + @callback + def listener(event: ha.Event) -> None: + state_reported_events.append(event) + + hass.states.async_set("light.bowl", "on", {}) + state_changed_events = async_capture_events(hass, EVENT_STATE_CHANGED) + state_reported_events = [] + hass.bus.async_listen( + EVENT_STATE_REPORTED, listener, event_filter=filter, run_immediately=True + ) + + hass.states.async_set("light.bowl", "on") + await hass.async_block_till_done() + assert len(state_changed_events) == 0 + assert len(state_reported_events) == 1 + + hass.states.async_set("light.bowl", "on", None, True) + await hass.async_block_till_done() + assert len(state_changed_events) == 1 + assert len(state_reported_events) == 2 + + hass.states.async_set("light.bowl", "off") + await hass.async_block_till_done() + assert len(state_changed_events) == 2 + assert len(state_reported_events) == 3 + + hass.states.async_remove("light.bowl") + await hass.async_block_till_done() + assert len(state_changed_events) == 3 + assert len(state_reported_events) == 4 + + +async def test_report_state_listener_restrictions(hass: HomeAssistant) -> None: + """Test we enforce requirements for EVENT_STATE_REPORTED listeners.""" + + @ha.callback + def listener(event): + """Mock listener.""" + + @ha.callback + def filter(event_data): + """Mock filter.""" + return False + + # run_immediately not set + with pytest.raises(HomeAssistantError): + hass.bus.async_listen(EVENT_STATE_REPORTED, listener, event_filter=filter) + + # no filter + with pytest.raises(HomeAssistantError): + hass.bus.async_listen(EVENT_STATE_REPORTED, listener, run_immediately=True) + + # Both filter and run_immediately + hass.bus.async_listen( + EVENT_STATE_REPORTED, listener, event_filter=filter, run_immediately=True + ) diff --git a/tests/test_data_entry_flow.py b/tests/test_data_entry_flow.py index d39c8faccef..5c3ad2a3b39 100644 --- a/tests/test_data_entry_flow.py +++ b/tests/test_data_entry_flow.py @@ -1,4 +1,5 @@ """Test the flow classes.""" + import asyncio import dataclasses import logging @@ -211,7 +212,6 @@ async def test_create_saves_data(manager) -> None: assert len(manager.mock_created_entries) == 1 entry = manager.mock_created_entries[0] - assert entry["version"] == 5 assert entry["handler"] == "test" assert entry["title"] == "Test Title" assert entry["data"] == "Test Data" @@ -237,7 +237,6 @@ async def test_discovery_init_flow(manager) -> None: assert len(manager.mock_created_entries) == 1 entry = manager.mock_created_entries[0] - assert entry["version"] == 5 assert entry["handler"] == "test" assert entry["title"] == "hello" assert entry["data"] == data @@ -761,8 +760,9 @@ async def test_abort_flow_exception(manager) -> None: async def test_init_unknown_flow(manager) -> None: """Test that UnknownFlow is raised when async_create_flow returns None.""" - with pytest.raises(data_entry_flow.UnknownFlow), patch.object( - manager, "async_create_flow", return_value=None + with ( + pytest.raises(data_entry_flow.UnknownFlow), + patch.object(manager, "async_create_flow", return_value=None), ): await manager.async_init("test") @@ -909,7 +909,7 @@ async def test_abort_raises_unknown_flow_if_not_in_progress(manager) -> None: @pytest.mark.parametrize( "menu_options", - (["target1", "target2"], {"target1": "Target 1", "target2": "Target 2"}), + [["target1", "target2"], {"target1": "Target 1", "target2": "Target 2"}], ) async def test_show_menu(hass: HomeAssistant, manager, menu_options) -> None: """Test show menu.""" diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index 3f4acd15440..5e113d3ba10 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -1,12 +1,18 @@ """Test to verify that Home Assistant exceptions work.""" + from __future__ import annotations +from typing import Any +from unittest.mock import patch + import pytest +from homeassistant.core import HomeAssistant from homeassistant.exceptions import ( ConditionErrorContainer, ConditionErrorIndex, ConditionErrorMessage, + HomeAssistantError, TemplateError, ) @@ -61,3 +67,203 @@ def test_template_message(arg: str | Exception, expected: str) -> None: """Ensure we can create TemplateError.""" template_error = TemplateError(arg) assert str(template_error) == expected + + +@pytest.mark.parametrize( + ("exception_args", "exception_kwargs", "args_base_class", "message"), + [ + ((), {}, (), ""), + (("bla",), {}, ("bla",), "bla"), + ((None,), {}, (None,), "None"), + ((type_error_bla := TypeError("bla"),), {}, (type_error_bla,), "bla"), + ( + (), + {"translation_domain": "test", "translation_key": "test"}, + ("test",), + "test", + ), + ( + (), + {"translation_domain": "test", "translation_key": "bla"}, + ("bla",), + "{bla} from cache", + ), + ( + (), + { + "translation_domain": "test", + "translation_key": "bla", + "translation_placeholders": {"bla": "Bla"}, + }, + ("bla",), + "Bla from cache", + ), + ], +) +async def test_home_assistant_error( + hass: HomeAssistant, + exception_args: tuple[Any,], + exception_kwargs: dict[str, Any], + args_base_class: tuple[Any], + message: str, +) -> None: + """Test edge cases with HomeAssistantError.""" + + with patch( + "homeassistant.helpers.translation.async_get_cached_translations", + return_value={"component.test.exceptions.bla.message": "{bla} from cache"}, + ): + with pytest.raises(HomeAssistantError) as exc: + raise HomeAssistantError(*exception_args, **exception_kwargs) + assert exc.value.args == args_base_class + assert str(exc.value) == message + # Get string of exception again from the cache + assert str(exc.value) == message + + +async def test_home_assistant_error_subclass(hass: HomeAssistant) -> None: + """Test __str__ method on an HomeAssistantError subclass.""" + + class _SubExceptionDefault(HomeAssistantError): + """Sub class, default with generated message.""" + + class _SubExceptionConstructor(HomeAssistantError): + """Sub class with constructor, no generated message.""" + + def __init__( + self, + custom_arg: str, + translation_domain: str | None = None, + translation_key: str | None = None, + translation_placeholders: dict[str, str] | None = None, + ) -> None: + super().__init__( + translation_domain=translation_domain, + translation_key=translation_key, + translation_placeholders=translation_placeholders, + ) + self.custom_arg = custom_arg + + class _SubExceptionConstructorGenerate(HomeAssistantError): + """Sub class with constructor, with generated message.""" + + generate_message: bool = True + + def __init__( + self, + custom_arg: str, + translation_domain: str | None = None, + translation_key: str | None = None, + translation_placeholders: dict[str, str] | None = None, + ) -> None: + super().__init__( + translation_domain=translation_domain, + translation_key=translation_key, + translation_placeholders=translation_placeholders, + ) + self.custom_arg = custom_arg + + class _SubExceptionGenerate(HomeAssistantError): + """Sub class, no generated message.""" + + generate_message: bool = True + + class _SubClassWithExceptionGroup(HomeAssistantError, BaseExceptionGroup): + """Sub class with exception group, no generated message.""" + + class _SubClassWithExceptionGroupGenerate(HomeAssistantError, BaseExceptionGroup): + """Sub class with exception group and generated message.""" + + generate_message: bool = True + + with patch( + "homeassistant.helpers.translation.async_get_cached_translations", + return_value={"component.test.exceptions.bla.message": "{bla} from cache"}, + ): + # A subclass without a constructor generates a message by default + with pytest.raises(HomeAssistantError) as exc: + raise _SubExceptionDefault( + translation_domain="test", + translation_key="bla", + translation_placeholders={"bla": "Bla"}, + ) + assert str(exc.value) == "Bla from cache" + + # A subclass with a constructor that does not parse `args` to the super class + with pytest.raises(HomeAssistantError) as exc: + raise _SubExceptionConstructor( + "custom arg", + translation_domain="test", + translation_key="bla", + translation_placeholders={"bla": "Bla"}, + ) + assert str(exc.value) == "Bla from cache" + with pytest.raises(HomeAssistantError) as exc: + raise _SubExceptionConstructor( + "custom arg", + ) + assert str(exc.value) == "" + + # A subclass with a constructor that generates the message + with pytest.raises(HomeAssistantError) as exc: + raise _SubExceptionConstructorGenerate( + "custom arg", + translation_domain="test", + translation_key="bla", + translation_placeholders={"bla": "Bla"}, + ) + assert str(exc.value) == "Bla from cache" + + # A subclass without overridden constructors and passed args + # defaults to the passed args + with pytest.raises(HomeAssistantError) as exc: + raise _SubExceptionDefault( + ValueError("wrong value"), + translation_domain="test", + translation_key="bla", + translation_placeholders={"bla": "Bla"}, + ) + assert str(exc.value) == "wrong value" + + # A subclass without overridden constructors and passed args + # and generate_message = True, generates a message + with pytest.raises(HomeAssistantError) as exc: + raise _SubExceptionGenerate( + ValueError("wrong value"), + translation_domain="test", + translation_key="bla", + translation_placeholders={"bla": "Bla"}, + ) + assert str(exc.value) == "Bla from cache" + + # A subclass with an ExceptionGroup subclass requires a message to be passed. + # As we pass args, we will not generate the message. + # The __str__ constructor defaults to that of the super class. + with pytest.raises(HomeAssistantError) as exc: + raise _SubClassWithExceptionGroup( + "group message", + [ValueError("wrong value"), TypeError("wrong type")], + translation_domain="test", + translation_key="bla", + translation_placeholders={"bla": "Bla"}, + ) + assert str(exc.value) == "group message (2 sub-exceptions)" + with pytest.raises(HomeAssistantError) as exc: + raise _SubClassWithExceptionGroup( + "group message", + [ValueError("wrong value"), TypeError("wrong type")], + ) + assert str(exc.value) == "group message (2 sub-exceptions)" + + # A subclass with an ExceptionGroup subclass requires a message to be passed. + # The `generate_message` flag is set.` + # The __str__ constructor will return the generated message. + with pytest.raises(HomeAssistantError) as exc: + raise _SubClassWithExceptionGroupGenerate( + "group message", + [ValueError("wrong value"), TypeError("wrong type")], + translation_domain="test", + translation_key="bla", + translation_placeholders={"bla": "Bla"}, + ) + assert str(exc.value) == "Bla from cache" diff --git a/tests/test_loader.py b/tests/test_loader.py index 4555dc767a9..6685bb4f2ac 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -1,6 +1,9 @@ """Test to verify that we can load components.""" + import asyncio +import os import sys +import threading from typing import Any from unittest.mock import MagicMock, Mock, patch @@ -69,7 +72,7 @@ def test_component_loader_non_existing(hass: HomeAssistant) -> None: """Test loading components.""" components = loader.Components(hass) with pytest.raises(ImportError): - components.non_existing + _ = components.non_existing async def test_component_wrapper(hass: HomeAssistant) -> None: @@ -119,6 +122,8 @@ async def test_custom_component_name( integration = await loader.async_get_integration(hass, "test") platform = integration.get_platform("light") + assert integration.get_platform_cached("light") is platform + assert platform.__name__ == "custom_components.test.light" assert platform.__package__ == "custom_components.test" @@ -247,13 +252,21 @@ async def test_get_integration_exceptions(hass: HomeAssistant) -> None: """Test resolving integration.""" integration = await loader.async_get_integration(hass, "hue") - with pytest.raises(ImportError), patch( - "homeassistant.loader.importlib.import_module", side_effect=ValueError("Boom") + with ( + pytest.raises(ImportError), + patch( + "homeassistant.loader.importlib.import_module", + side_effect=ValueError("Boom"), + ), ): assert hue == integration.get_component() - with pytest.raises(ImportError), patch( - "homeassistant.loader.importlib.import_module", side_effect=ValueError("Boom") + with ( + pytest.raises(ImportError), + patch( + "homeassistant.loader.importlib.import_module", + side_effect=ValueError("Boom"), + ), ): assert hue_light == integration.get_platform("light") @@ -261,72 +274,220 @@ async def test_get_integration_exceptions(hass: HomeAssistant) -> None: async def test_get_platform_caches_failures_when_component_loaded( hass: HomeAssistant, ) -> None: - """Test get_platform cache failures only when the component is loaded.""" + """Test get_platform caches failures only when the component is loaded. + + Only ModuleNotFoundError is cached, ImportError is not cached. + """ integration = await loader.async_get_integration(hass, "hue") - with pytest.raises(ImportError), patch( - "homeassistant.loader.importlib.import_module", side_effect=ImportError("Boom") + with ( + pytest.raises(ModuleNotFoundError), + patch( + "homeassistant.loader.importlib.import_module", + side_effect=ModuleNotFoundError("Boom"), + ), ): assert integration.get_component() == hue - with pytest.raises(ImportError), patch( - "homeassistant.loader.importlib.import_module", side_effect=ImportError("Boom") + with ( + pytest.raises(ModuleNotFoundError), + patch( + "homeassistant.loader.importlib.import_module", + side_effect=ModuleNotFoundError("Boom"), + ), ): assert integration.get_platform("light") == hue_light # Hue is not loaded so we should still hit the import_module path - with pytest.raises(ImportError), patch( - "homeassistant.loader.importlib.import_module", side_effect=ImportError("Boom") + with ( + pytest.raises(ModuleNotFoundError), + patch( + "homeassistant.loader.importlib.import_module", + side_effect=ModuleNotFoundError("Boom"), + ), ): assert integration.get_platform("light") == hue_light assert integration.get_component() == hue # Hue is loaded so we should cache the import_module failure now - with pytest.raises(ImportError), patch( - "homeassistant.loader.importlib.import_module", side_effect=ImportError("Boom") + with ( + pytest.raises(ModuleNotFoundError), + patch( + "homeassistant.loader.importlib.import_module", + side_effect=ModuleNotFoundError("Boom"), + ), ): assert integration.get_platform("light") == hue_light # Hue is loaded and the last call should have cached the import_module failure - with pytest.raises(ImportError): + with pytest.raises(ModuleNotFoundError): assert integration.get_platform("light") == hue_light +async def test_get_platform_only_cached_module_not_found_when_component_loaded( + hass: HomeAssistant, +) -> None: + """Test get_platform cache only cache module not found when the component is loaded.""" + integration = await loader.async_get_integration(hass, "hue") + + with ( + pytest.raises(ImportError), + patch( + "homeassistant.loader.importlib.import_module", + side_effect=ImportError("Boom"), + ), + ): + assert integration.get_component() == hue + + with ( + pytest.raises(ImportError), + patch( + "homeassistant.loader.importlib.import_module", + side_effect=ImportError("Boom"), + ), + ): + assert integration.get_platform("light") == hue_light + + # Hue is not loaded so we should still hit the import_module path + with ( + pytest.raises(ImportError), + patch( + "homeassistant.loader.importlib.import_module", + side_effect=ImportError("Boom"), + ), + ): + assert integration.get_platform("light") == hue_light + + assert integration.get_component() == hue + + # Hue is loaded so we should cache the import_module failure now + with ( + pytest.raises(ImportError), + patch( + "homeassistant.loader.importlib.import_module", + side_effect=ImportError("Boom"), + ), + ): + assert integration.get_platform("light") == hue_light + + # ImportError is not cached because we only cache ModuleNotFoundError + assert integration.get_platform("light") == hue_light + + async def test_async_get_platform_caches_failures_when_component_loaded( hass: HomeAssistant, ) -> None: - """Test async_get_platform cache failures only when the component is loaded.""" + """Test async_get_platform caches failures only when the component is loaded. + + Only ModuleNotFoundError is cached, ImportError is not cached. + """ integration = await loader.async_get_integration(hass, "hue") - with pytest.raises(ImportError), patch( - "homeassistant.loader.importlib.import_module", side_effect=ImportError("Boom") + with ( + pytest.raises(ModuleNotFoundError), + patch( + "homeassistant.loader.importlib.import_module", + side_effect=ModuleNotFoundError("Boom"), + ), ): assert integration.get_component() == hue - with pytest.raises(ImportError), patch( - "homeassistant.loader.importlib.import_module", side_effect=ImportError("Boom") + with ( + pytest.raises(ModuleNotFoundError), + patch( + "homeassistant.loader.importlib.import_module", + side_effect=ModuleNotFoundError("Boom"), + ), ): assert await integration.async_get_platform("light") == hue_light # Hue is not loaded so we should still hit the import_module path - with pytest.raises(ImportError), patch( - "homeassistant.loader.importlib.import_module", side_effect=ImportError("Boom") + with ( + pytest.raises(ModuleNotFoundError), + patch( + "homeassistant.loader.importlib.import_module", + side_effect=ModuleNotFoundError("Boom"), + ), ): assert await integration.async_get_platform("light") == hue_light assert integration.get_component() == hue # Hue is loaded so we should cache the import_module failure now - with pytest.raises(ImportError), patch( - "homeassistant.loader.importlib.import_module", side_effect=ImportError("Boom") + with ( + pytest.raises(ModuleNotFoundError), + patch( + "homeassistant.loader.importlib.import_module", + side_effect=ModuleNotFoundError("Boom"), + ), ): assert await integration.async_get_platform("light") == hue_light # Hue is loaded and the last call should have cached the import_module failure - with pytest.raises(ImportError): + with pytest.raises(ModuleNotFoundError): assert await integration.async_get_platform("light") == hue_light + # The cache should never be filled because the import error is remembered + assert integration.get_platform_cached("light") is None + + +async def test_async_get_platforms_caches_failures_when_component_loaded( + hass: HomeAssistant, +) -> None: + """Test async_get_platforms cache failures only when the component is loaded. + + Only ModuleNotFoundError is cached, ImportError is not cached. + """ + integration = await loader.async_get_integration(hass, "hue") + + with ( + pytest.raises(ModuleNotFoundError), + patch( + "homeassistant.loader.importlib.import_module", + side_effect=ModuleNotFoundError("Boom"), + ), + ): + assert integration.get_component() == hue + + with ( + pytest.raises(ModuleNotFoundError), + patch( + "homeassistant.loader.importlib.import_module", + side_effect=ModuleNotFoundError("Boom"), + ), + ): + assert await integration.async_get_platforms(["light"]) == {"light": hue_light} + + # Hue is not loaded so we should still hit the import_module path + with ( + pytest.raises(ModuleNotFoundError), + patch( + "homeassistant.loader.importlib.import_module", + side_effect=ModuleNotFoundError("Boom"), + ), + ): + assert await integration.async_get_platforms(["light"]) == {"light": hue_light} + + assert integration.get_component() == hue + + # Hue is loaded so we should cache the import_module failure now + with ( + pytest.raises(ModuleNotFoundError), + patch( + "homeassistant.loader.importlib.import_module", + side_effect=ModuleNotFoundError("Boom"), + ), + ): + assert await integration.async_get_platforms(["light"]) == {"light": hue_light} + + # Hue is loaded and the last call should have cached the import_module failure + with pytest.raises(ModuleNotFoundError): + assert await integration.async_get_platforms(["light"]) == {"light": hue_light} + + # The cache should never be filled because the import error is remembered + assert integration.get_platform_cached("light") is None + async def test_get_integration_legacy( hass: HomeAssistant, enable_custom_integrations: None @@ -335,6 +496,7 @@ async def test_get_integration_legacy( integration = await loader.async_get_integration(hass, "test_embedded") assert integration.get_component().DOMAIN == "test_embedded" assert integration.get_platform("switch") is not None + assert integration.get_platform_cached("switch") is not None async def test_get_integration_custom_component( @@ -1048,6 +1210,14 @@ async def test_async_suggest_report_issue( ) +def test_import_executor_default(hass: HomeAssistant) -> None: + """Test that import_executor defaults.""" + custom_comp = mock_integration(hass, MockModule("any_random"), built_in=False) + assert custom_comp.import_executor is True + built_in_comp = mock_integration(hass, MockModule("other_random"), built_in=True) + assert built_in_comp.import_executor is True + + async def test_config_folder_not_in_path(hass): """Test that config folder is not in path.""" @@ -1068,20 +1238,23 @@ async def test_hass_components_use_reported( ) integration_frame = frame.IntegrationFrame( custom_integration=True, - frame=mock_integration_frame, + _frame=mock_integration_frame, integration="test_integration_frame", module="custom_components.test_integration_frame", relative_filename="custom_components/test_integration_frame/__init__.py", ) - with patch( - "homeassistant.helpers.frame.get_integration_frame", - return_value=integration_frame, - ), patch( - "homeassistant.components.http.start_http_server_and_save_config", - return_value=None, + with ( + patch( + "homeassistant.helpers.frame.get_integration_frame", + return_value=integration_frame, + ), + patch( + "homeassistant.components.http.start_http_server_and_save_config", + return_value=None, + ), ): - hass.components.http.start_http_server_and_save_config(hass, [], None) + await hass.components.http.start_http_server_and_save_config(hass, [], None) assert ( "Detected that custom integration 'test_integration_frame'" @@ -1089,6 +1262,178 @@ async def test_hass_components_use_reported( ) in caplog.text +async def test_async_get_component_preloads_config_and_config_flow( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Verify async_get_component will try to preload the config and config_flow platform.""" + executor_import_integration = _get_test_integration( + hass, "executor_import", True, import_executor=True + ) + assert executor_import_integration.import_executor is True + + assert "homeassistant.components.executor_import" not in sys.modules + assert "custom_components.executor_import" not in sys.modules + + platform_exists_calls = [] + + def mock_platforms_exists(platforms: list[str]) -> bool: + platform_exists_calls.append(platforms) + return platforms + + with ( + patch("homeassistant.loader.importlib.import_module") as mock_import, + patch.object( + executor_import_integration, "platforms_exists", mock_platforms_exists + ), + ): + await executor_import_integration.async_get_component() + + assert len(platform_exists_calls[0]) == len(loader.BASE_PRELOAD_PLATFORMS) + assert mock_import.call_count == 1 + len(loader.BASE_PRELOAD_PLATFORMS) + assert ( + mock_import.call_args_list[0][0][0] + == "homeassistant.components.executor_import" + ) + checked_platforms = { + mock_import.call_args_list[i][0][0] + for i in range(1, len(mock_import.call_args_list)) + } + assert checked_platforms == { + "homeassistant.components.executor_import.config_flow", + *( + f"homeassistant.components.executor_import.{platform}" + for platform in loader.BASE_PRELOAD_PLATFORMS + ), + } + + +async def test_async_get_component_loads_loop_if_already_in_sys_modules( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + enable_custom_integrations: None, +) -> None: + """Verify async_get_component does not create an executor job if the module is already in sys.modules.""" + integration = await loader.async_get_integration( + hass, "test_package_loaded_executor" + ) + assert integration.pkg_path == "custom_components.test_package_loaded_executor" + assert integration.import_executor is True + assert integration.config_flow is True + + assert "test_package_loaded_executor" not in hass.config.components + assert "test_package_loaded_executor.config_flow" not in hass.config.components + + config_flow_module_name = f"{integration.pkg_path}.config_flow" + module_mock = MagicMock(__file__="__init__.py") + config_flow_module_mock = MagicMock(__file__="config_flow.py") + + def import_module(name: str) -> Any: + if name == integration.pkg_path: + return module_mock + if name == config_flow_module_name: + return config_flow_module_mock + raise ImportError + + modules_without_config_flow = { + k: v for k, v in sys.modules.items() if k != config_flow_module_name + } + with ( + patch.dict( + "sys.modules", + {**modules_without_config_flow, integration.pkg_path: module_mock}, + clear=True, + ), + patch("homeassistant.loader.importlib.import_module", import_module), + ): + module = await integration.async_get_component() + + # The config flow is missing so we should load + # in the executor + assert "loaded_executor=True" in caplog.text + assert "loaded_executor=False" not in caplog.text + assert module is module_mock + caplog.clear() + + with ( + patch.dict( + "sys.modules", + { + integration.pkg_path: module_mock, + config_flow_module_name: config_flow_module_mock, + }, + ), + patch("homeassistant.loader.importlib.import_module", import_module), + ): + module = await integration.async_get_component() + + # Everything is already in the integration cache + # so it should not have to call the load + assert "loaded_executor" not in caplog.text + assert module is module_mock + + +async def test_async_get_component_concurrent_loads( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + enable_custom_integrations: None, +) -> None: + """Verify async_get_component waits if the first load if called again when still in progress.""" + integration = await loader.async_get_integration( + hass, "test_package_loaded_executor" + ) + assert integration.pkg_path == "custom_components.test_package_loaded_executor" + assert integration.import_executor is True + assert integration.config_flow is True + + assert "test_package_loaded_executor" not in hass.config.components + assert "test_package_loaded_executor.config_flow" not in hass.config.components + + config_flow_module_name = f"{integration.pkg_path}.config_flow" + module_mock = MagicMock(__file__="__init__.py") + config_flow_module_mock = MagicMock(__file__="config_flow.py") + imports = [] + start_event = threading.Event() + import_event = asyncio.Event() + + def import_module(name: str) -> Any: + hass.loop.call_soon_threadsafe(import_event.set) + imports.append(name) + start_event.wait() + if name == integration.pkg_path: + return module_mock + if name == config_flow_module_name: + return config_flow_module_mock + raise ImportError + + modules_without_integration = { + k: v + for k, v in sys.modules.items() + if k != config_flow_module_name and k != integration.pkg_path + } + with ( + patch.dict( + "sys.modules", + {**modules_without_integration}, + clear=True, + ), + patch("homeassistant.loader.importlib.import_module", import_module), + ): + load_task1 = asyncio.create_task(integration.async_get_component()) + load_task2 = asyncio.create_task(integration.async_get_component()) + await import_event.wait() # make sure the import is started + assert not integration._component_future.done() + start_event.set() + comp1 = await load_task1 + comp2 = await load_task2 + assert integration._component_future is None + + assert comp1 is module_mock + assert comp2 is module_mock + + assert integration.pkg_path in imports + assert config_flow_module_name in imports + + async def test_async_get_component_deadlock_fallback( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: @@ -1097,7 +1442,7 @@ async def test_async_get_component_deadlock_fallback( hass, "executor_import", True, import_executor=True ) assert executor_import_integration.import_executor is True - module_mock = MagicMock() + module_mock = MagicMock(__file__="__init__.py") import_attempts = 0 def mock_import(module: str, *args: Any, **kwargs: Any) -> Any: @@ -1154,9 +1499,10 @@ async def test_async_get_component_raises_after_import_failure( assert "homeassistant.components.executor_import" not in sys.modules assert "custom_components.executor_import" not in sys.modules - with patch( - "homeassistant.loader.importlib.import_module", mock_import - ), pytest.raises(ImportError): + with ( + patch("homeassistant.loader.importlib.import_module", mock_import), + pytest.raises(ImportError), + ): await executor_import_integration.async_get_component() assert ( @@ -1199,7 +1545,9 @@ async def test_async_get_platform_deadlock_fallback( "Detected deadlock trying to import homeassistant.components.executor_import" in caplog.text ) - assert "loaded_executor=False" in caplog.text + # We should have tried both the executor and loop + assert "executor=['config_flow']" in caplog.text + assert "loop=['config_flow']" in caplog.text assert module is module_mock @@ -1235,9 +1583,10 @@ async def test_async_get_platform_raises_after_import_failure( assert "homeassistant.components.executor_import" not in sys.modules assert "custom_components.executor_import" not in sys.modules - with patch( - "homeassistant.loader.importlib.import_module", mock_import - ), pytest.raises(ImportError): + with ( + patch("homeassistant.loader.importlib.import_module", mock_import), + pytest.raises(ImportError), + ): await executor_import_integration.async_get_platform("config_flow") assert ( @@ -1245,3 +1594,247 @@ async def test_async_get_platform_raises_after_import_failure( in caplog.text ) assert "loaded_executor=False" not in caplog.text + + +async def test_platforms_exists( + hass: HomeAssistant, enable_custom_integrations: None +) -> None: + """Test platforms_exists.""" + original_os_listdir = os.listdir + + paths: list[str] = [] + + def mock_list_dir(path: str) -> list[str]: + paths.append(path) + return original_os_listdir(path) + + with patch("homeassistant.loader.os.listdir", mock_list_dir): + integration = await loader.async_get_integration( + hass, "test_integration_platform" + ) + assert integration.domain == "test_integration_platform" + + # Verify the files cache is primed + assert integration.file_path in paths + + # component is loaded, should now return False + with patch("homeassistant.loader.os.listdir", wraps=os.listdir) as mock_exists: + component = integration.get_component() + assert component.DOMAIN == "test_integration_platform" + + # The files cache should be primed when + # the integration is resolved + assert mock_exists.call_count == 0 + + # component is loaded, should now return False + with patch("homeassistant.loader.os.listdir", wraps=os.listdir) as mock_exists: + assert integration.platforms_exists(("non_existing",)) == [] + + # We should remember which files exist + assert mock_exists.call_count == 0 + + # component is loaded, should now return False + with patch("homeassistant.loader.os.listdir", wraps=os.listdir) as mock_exists: + assert integration.platforms_exists(("non_existing",)) == [] + + # We should remember the file does not exist + assert mock_exists.call_count == 0 + + assert integration.platforms_exists(["group"]) == ["group"] + + platform = await integration.async_get_platform("group") + assert platform.MAGIC == 1 + + platform = integration.get_platform("group") + assert platform.MAGIC == 1 + + assert integration.platforms_exists(["group"]) == ["group"] + + assert integration.platforms_are_loaded(["group"]) is True + assert integration.platforms_are_loaded(["other"]) is False + + +async def test_async_get_platforms_loads_loop_if_already_in_sys_modules( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + enable_custom_integrations: None, +) -> None: + """Verify async_get_platforms does not create an executor job. + + Case is for when the module is already in sys.modules. + """ + integration = await loader.async_get_integration( + hass, "test_package_loaded_executor" + ) + assert integration.pkg_path == "custom_components.test_package_loaded_executor" + assert integration.import_executor is True + assert integration.config_flow is True + + assert "test_package_loaded_executor" not in hass.config.components + assert "test_package_loaded_executor.config_flow" not in hass.config.components + await integration.async_get_component() + + button_module_name = f"{integration.pkg_path}.button" + switch_module_name = f"{integration.pkg_path}.switch" + light_module_name = f"{integration.pkg_path}.light" + button_module_mock = MagicMock() + switch_module_mock = MagicMock() + light_module_mock = MagicMock() + + def import_module(name: str) -> Any: + if name == button_module_name: + return button_module_mock + if name == switch_module_name: + return switch_module_mock + if name == light_module_name: + return light_module_mock + raise ImportError + + modules_without_button = { + k: v for k, v in sys.modules.items() if k != button_module_name + } + with ( + patch.dict( + "sys.modules", + modules_without_button, + clear=True, + ), + patch("homeassistant.loader.importlib.import_module", import_module), + ): + module = (await integration.async_get_platforms(["button"]))["button"] + + # The button module is missing so we should load + # in the executor + assert "executor=['button']" in caplog.text + assert "loop=[]" in caplog.text + assert module is button_module_mock + caplog.clear() + + with ( + patch.dict( + "sys.modules", + { + **modules_without_button, + button_module_name: button_module_mock, + }, + ), + patch("homeassistant.loader.importlib.import_module", import_module), + ): + module = (await integration.async_get_platforms(["button"]))["button"] + + # Everything is cached so there should be no logging + assert "loop=" not in caplog.text + assert "executor=" not in caplog.text + assert module is button_module_mock + caplog.clear() + + modules_without_switch = { + k: v for k, v in sys.modules.items() if k not in switch_module_name + } + with ( + patch.dict( + "sys.modules", + {**modules_without_switch, light_module_name: light_module_mock}, + clear=True, + ), + patch("homeassistant.loader.importlib.import_module", import_module), + ): + modules = await integration.async_get_platforms(["button", "switch", "light"]) + + # The button module is already in the cache so nothing happens + # The switch module is loaded in the executor since its not in the cache + # The light module is in memory but not in the cache so its loaded in the loop + assert "['button']" not in caplog.text + assert "executor=['switch']" in caplog.text + assert "loop=['light']" in caplog.text + assert modules == { + "button": button_module_mock, + "switch": switch_module_mock, + "light": light_module_mock, + } + assert integration.get_platform_cached("button") is button_module_mock + assert integration.get_platform_cached("switch") is switch_module_mock + assert integration.get_platform_cached("light") is light_module_mock + + +async def test_async_get_platforms_concurrent_loads( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + enable_custom_integrations: None, +) -> None: + """Verify async_get_platforms waits if the first load if called again. + + Case is for when when a second load is called + and the first is still in progress. + """ + integration = await loader.async_get_integration( + hass, "test_package_loaded_executor" + ) + assert integration.pkg_path == "custom_components.test_package_loaded_executor" + assert integration.import_executor is True + assert integration.config_flow is True + + assert "test_package_loaded_executor" not in hass.config.components + assert "test_package_loaded_executor.config_flow" not in hass.config.components + await integration.async_get_component() + + button_module_name = f"{integration.pkg_path}.button" + button_module_mock = MagicMock() + + imports = [] + start_event = threading.Event() + import_event = asyncio.Event() + + def import_module(name: str) -> Any: + hass.loop.call_soon_threadsafe(import_event.set) + imports.append(name) + start_event.wait() + if name == button_module_name: + return button_module_mock + raise ImportError + + modules_without_button = { + k: v + for k, v in sys.modules.items() + if k != button_module_name and k != integration.pkg_path + } + with ( + patch.dict( + "sys.modules", + modules_without_button, + clear=True, + ), + patch("homeassistant.loader.importlib.import_module", import_module), + ): + load_task1 = asyncio.create_task(integration.async_get_platforms(["button"])) + load_task2 = asyncio.create_task(integration.async_get_platforms(["button"])) + await import_event.wait() # make sure the import is started + assert not integration._import_futures["button"].done() + start_event.set() + load_result1 = await load_task1 + load_result2 = await load_task2 + assert integration._import_futures is not None + + assert load_result1 == {"button": button_module_mock} + assert load_result2 == {"button": button_module_mock} + + assert imports == [button_module_name] + assert integration.get_platform_cached("button") is button_module_mock + + +async def test_integration_warnings( + hass: HomeAssistant, + enable_custom_integrations: None, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test integration warnings.""" + await loader.async_get_integration(hass, "test_package_loaded_loop") + assert "configured to to import its code in the event loop" in caplog.text + + +async def test_has_services(hass: HomeAssistant, enable_custom_integrations) -> None: + """Test has_services.""" + integration = await loader.async_get_integration(hass, "test") + assert integration.has_services is False + integration = await loader.async_get_integration(hass, "test_with_services") + assert integration.has_services is True diff --git a/tests/test_main.py b/tests/test_main.py index b676040252a..080787311a0 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,4 +1,5 @@ """Test methods in __main__.""" + from unittest.mock import PropertyMock, patch from homeassistant import __main__ as main @@ -68,7 +69,7 @@ def test_skip_pip_mutually_exclusive(mock_exit) -> None: """Test --skip-pip and --skip-pip-package are mutually exclusive.""" def parse_args(*args): - with patch("sys.argv", ["python"] + list(args)): + with patch("sys.argv", ["python", *args]): return main.get_arguments() args = parse_args("--skip-pip") diff --git a/tests/test_requirements.py b/tests/test_requirements.py index 383ac9c012c..ed04ef8649b 100644 --- a/tests/test_requirements.py +++ b/tests/test_requirements.py @@ -1,4 +1,5 @@ """Test requirements module.""" + import asyncio import logging import os @@ -30,11 +31,15 @@ def env_without_wheel_links(): async def test_requirement_installed_in_venv(hass: HomeAssistant) -> None: """Test requirement installed in virtual environment.""" - with patch("os.path.dirname", return_value="ha_package_path"), patch( - "homeassistant.util.package.is_virtual_env", return_value=True - ), patch("homeassistant.util.package.is_docker_env", return_value=False), patch( - "homeassistant.util.package.install_package", return_value=True - ) as mock_install, patch.dict(os.environ, env_without_wheel_links(), clear=True): + with ( + patch("os.path.dirname", return_value="ha_package_path"), + patch("homeassistant.util.package.is_virtual_env", return_value=True), + patch("homeassistant.util.package.is_docker_env", return_value=False), + patch( + "homeassistant.util.package.install_package", return_value=True + ) as mock_install, + patch.dict(os.environ, env_without_wheel_links(), clear=True), + ): hass.config.skip_pip = False mock_integration(hass, MockModule("comp", requirements=["package==0.0.1"])) assert await setup.async_setup_component(hass, "comp", {}) @@ -48,11 +53,15 @@ async def test_requirement_installed_in_venv(hass: HomeAssistant) -> None: async def test_requirement_installed_in_deps(hass: HomeAssistant) -> None: """Test requirement installed in deps directory.""" - with patch("os.path.dirname", return_value="ha_package_path"), patch( - "homeassistant.util.package.is_virtual_env", return_value=False - ), patch("homeassistant.util.package.is_docker_env", return_value=False), patch( - "homeassistant.util.package.install_package", return_value=True - ) as mock_install, patch.dict(os.environ, env_without_wheel_links(), clear=True): + with ( + patch("os.path.dirname", return_value="ha_package_path"), + patch("homeassistant.util.package.is_virtual_env", return_value=False), + patch("homeassistant.util.package.is_docker_env", return_value=False), + patch( + "homeassistant.util.package.install_package", return_value=True + ) as mock_install, + patch.dict(os.environ, env_without_wheel_links(), clear=True), + ): hass.config.skip_pip = False mock_integration(hass, MockModule("comp", requirements=["package==0.0.1"])) assert await setup.async_setup_component(hass, "comp", {}) @@ -74,9 +83,10 @@ async def test_install_existing_package(hass: HomeAssistant) -> None: assert len(mock_inst.mock_calls) == 1 - with patch("homeassistant.util.package.is_installed", return_value=True), patch( - "homeassistant.util.package.install_package" - ) as mock_inst: + with ( + patch("homeassistant.util.package.is_installed", return_value=True), + patch("homeassistant.util.package.install_package") as mock_inst, + ): await async_process_requirements(hass, "test_component", ["hello==1.0.0"]) assert len(mock_inst.mock_calls) == 0 @@ -84,9 +94,12 @@ async def test_install_existing_package(hass: HomeAssistant) -> None: async def test_install_missing_package(hass: HomeAssistant) -> None: """Test an install attempt on an existing package.""" - with patch( - "homeassistant.util.package.install_package", return_value=False - ) as mock_inst, pytest.raises(RequirementsNotFound): + with ( + patch( + "homeassistant.util.package.install_package", return_value=False + ) as mock_inst, + pytest.raises(RequirementsNotFound), + ): await async_process_requirements(hass, "test_component", ["hello==1.0.0"]) assert len(mock_inst.mock_calls) == 3 @@ -133,11 +146,14 @@ async def test_get_integration_with_requirements(hass: HomeAssistant) -> None: ), ) - with patch( - "homeassistant.util.package.is_installed", return_value=False - ) as mock_is_installed, patch( - "homeassistant.util.package.install_package", return_value=True - ) as mock_inst: + with ( + patch( + "homeassistant.util.package.is_installed", return_value=False + ) as mock_is_installed, + patch( + "homeassistant.util.package.install_package", return_value=True + ) as mock_inst, + ): integration = await async_get_integration_with_requirements( hass, "test_component" ) @@ -194,13 +210,18 @@ async def test_get_integration_with_requirements_cache(hass: HomeAssistant) -> N ), ) - with patch( - "homeassistant.util.package.is_installed", return_value=False - ) as mock_is_installed, patch( - "homeassistant.util.package.install_package", return_value=True - ) as mock_inst, patch( - "homeassistant.requirements.async_get_integration", wraps=async_get_integration - ) as mock_async_get_integration: + with ( + patch( + "homeassistant.util.package.is_installed", return_value=False + ) as mock_is_installed, + patch( + "homeassistant.util.package.install_package", return_value=True + ) as mock_inst, + patch( + "homeassistant.requirements.async_get_integration", + wraps=async_get_integration, + ) as mock_async_get_integration, + ): integration = await async_get_integration_with_requirements( hass, "test_component" ) @@ -322,11 +343,16 @@ async def test_get_integration_with_requirements_pip_install_fails_two_passes( return False # 1st pass - with pytest.raises(RequirementsNotFound), patch( - "homeassistant.util.package.is_installed", return_value=False - ) as mock_is_installed, patch( - "homeassistant.util.package.install_package", side_effect=_mock_install_package - ) as mock_inst: + with ( + pytest.raises(RequirementsNotFound), + patch( + "homeassistant.util.package.is_installed", return_value=False + ) as mock_is_installed, + patch( + "homeassistant.util.package.install_package", + side_effect=_mock_install_package, + ) as mock_inst, + ): integration = await async_get_integration_with_requirements( hass, "test_component" ) @@ -352,11 +378,16 @@ async def test_get_integration_with_requirements_pip_install_fails_two_passes( ] # 2nd pass - with pytest.raises(RequirementsNotFound), patch( - "homeassistant.util.package.is_installed", return_value=False - ) as mock_is_installed, patch( - "homeassistant.util.package.install_package", side_effect=_mock_install_package - ) as mock_inst: + with ( + pytest.raises(RequirementsNotFound), + patch( + "homeassistant.util.package.is_installed", return_value=False + ) as mock_is_installed, + patch( + "homeassistant.util.package.install_package", + side_effect=_mock_install_package, + ) as mock_inst, + ): integration = await async_get_integration_with_requirements( hass, "test_component" ) @@ -370,11 +401,16 @@ async def test_get_integration_with_requirements_pip_install_fails_two_passes( # Now clear the history and so we try again async_clear_install_history(hass) - with pytest.raises(RequirementsNotFound), patch( - "homeassistant.util.package.is_installed", return_value=False - ) as mock_is_installed, patch( - "homeassistant.util.package.install_package", side_effect=_mock_install_package - ) as mock_inst: + with ( + pytest.raises(RequirementsNotFound), + patch( + "homeassistant.util.package.is_installed", return_value=False + ) as mock_is_installed, + patch( + "homeassistant.util.package.install_package", + side_effect=_mock_install_package, + ) as mock_inst, + ): integration = await async_get_integration_with_requirements( hass, "test_component" ) @@ -400,11 +436,14 @@ async def test_get_integration_with_requirements_pip_install_fails_two_passes( # Now clear the history and mock success async_clear_install_history(hass) - with patch( - "homeassistant.util.package.is_installed", return_value=False - ) as mock_is_installed, patch( - "homeassistant.util.package.install_package", return_value=True - ) as mock_inst: + with ( + patch( + "homeassistant.util.package.is_installed", return_value=False + ) as mock_is_installed, + patch( + "homeassistant.util.package.install_package", return_value=True + ) as mock_inst, + ): integration = await async_get_integration_with_requirements( hass, "test_component" ) @@ -496,13 +535,15 @@ async def test_install_with_wheels_index(hass: HomeAssistant) -> None: hass.config.skip_pip = False mock_integration(hass, MockModule("comp", requirements=["hello==1.0.0"])) - with patch("homeassistant.util.package.is_installed", return_value=False), patch( - "homeassistant.util.package.is_docker_env", return_value=True - ), patch("homeassistant.util.package.install_package") as mock_inst, patch.dict( - os.environ, {"WHEELS_LINKS": "https://wheels.hass.io/test"} - ), patch( - "os.path.dirname", - ) as mock_dir: + with ( + patch("homeassistant.util.package.is_installed", return_value=False), + patch("homeassistant.util.package.is_docker_env", return_value=True), + patch("homeassistant.util.package.install_package") as mock_inst, + patch.dict(os.environ, {"WHEELS_LINKS": "https://wheels.hass.io/test"}), + patch( + "os.path.dirname", + ) as mock_dir, + ): mock_dir.return_value = "ha_package_path" assert await setup.async_setup_component(hass, "comp", {}) assert "comp" in hass.config.components @@ -519,11 +560,13 @@ async def test_install_on_docker(hass: HomeAssistant) -> None: hass.config.skip_pip = False mock_integration(hass, MockModule("comp", requirements=["hello==1.0.0"])) - with patch("homeassistant.util.package.is_installed", return_value=False), patch( - "homeassistant.util.package.is_docker_env", return_value=True - ), patch("homeassistant.util.package.install_package") as mock_inst, patch( - "os.path.dirname" - ) as mock_dir, patch.dict(os.environ, env_without_wheel_links(), clear=True): + with ( + patch("homeassistant.util.package.is_installed", return_value=False), + patch("homeassistant.util.package.is_docker_env", return_value=True), + patch("homeassistant.util.package.install_package") as mock_inst, + patch("os.path.dirname") as mock_dir, + patch.dict(os.environ, env_without_wheel_links(), clear=True), + ): mock_dir.return_value = "ha_package_path" assert await setup.async_setup_component(hass, "comp", {}) assert "comp" in hass.config.components @@ -548,7 +591,7 @@ async def test_discovery_requirements_mqtt(hass: HomeAssistant) -> None: ) as mock_process: await async_get_integration_with_requirements(hass, "mqtt_comp") - assert len(mock_process.mock_calls) == 3 # mqtt also depends on http + assert len(mock_process.mock_calls) == 2 # mqtt also depends on http assert mock_process.mock_calls[0][1][1] == mqtt.requirements diff --git a/tests/test_runner.py b/tests/test_runner.py index 14728321721..ab9b0e31e0d 100644 --- a/tests/test_runner.py +++ b/tests/test_runner.py @@ -1,4 +1,5 @@ """Test the runner.""" + import asyncio from collections.abc import Iterator import threading @@ -37,9 +38,11 @@ async def test_setup_and_run_hass(hass: HomeAssistant, tmpdir: py.path.local) -> test_dir = tmpdir.mkdir("config") default_config = runner.RuntimeConfig(test_dir) - with patch("homeassistant.bootstrap.async_setup_hass", return_value=hass), patch( - "threading._shutdown" - ), patch("homeassistant.core.HomeAssistant.async_run") as mock_run: + with ( + patch("homeassistant.bootstrap.async_setup_hass", return_value=hass), + patch("threading._shutdown"), + patch("homeassistant.core.HomeAssistant.async_run") as mock_run, + ): await runner.setup_and_run_hass(default_config) assert threading._shutdown == thread.deadlock_safe_shutdown @@ -51,11 +54,12 @@ def test_run(hass: HomeAssistant, tmpdir: py.path.local) -> None: test_dir = tmpdir.mkdir("config") default_config = runner.RuntimeConfig(test_dir) - with patch.object(runner, "TASK_CANCELATION_TIMEOUT", 1), patch( - "homeassistant.bootstrap.async_setup_hass", return_value=hass - ), patch("threading._shutdown"), patch( - "homeassistant.core.HomeAssistant.async_run" - ) as mock_run: + with ( + patch.object(runner, "TASK_CANCELATION_TIMEOUT", 1), + patch("homeassistant.bootstrap.async_setup_hass", return_value=hass), + patch("threading._shutdown"), + patch("homeassistant.core.HomeAssistant.async_run") as mock_run, + ): runner.run(default_config) assert mock_run.called @@ -68,16 +72,19 @@ def test_run_executor_shutdown_throws( test_dir = tmpdir.mkdir("config") default_config = runner.RuntimeConfig(test_dir) - with patch.object(runner, "TASK_CANCELATION_TIMEOUT", 1), pytest.raises( - RuntimeError - ), patch("homeassistant.bootstrap.async_setup_hass", return_value=hass), patch( - "threading._shutdown" - ), patch( - "homeassistant.runner.InterruptibleThreadPoolExecutor.shutdown", - side_effect=RuntimeError, - ) as mock_shutdown, patch( - "homeassistant.core.HomeAssistant.async_run", - ) as mock_run: + with ( + patch.object(runner, "TASK_CANCELATION_TIMEOUT", 1), + pytest.raises(RuntimeError), + patch("homeassistant.bootstrap.async_setup_hass", return_value=hass), + patch("threading._shutdown"), + patch( + "homeassistant.runner.InterruptibleThreadPoolExecutor.shutdown", + side_effect=RuntimeError, + ) as mock_shutdown, + patch( + "homeassistant.core.HomeAssistant.async_run", + ) as mock_run, + ): runner.run(default_config) assert mock_shutdown.called @@ -111,10 +118,11 @@ def test_run_does_not_block_forever_with_shielded_task( await asyncio.sleep(0.1) return 0 - with patch.object(runner, "TASK_CANCELATION_TIMEOUT", 1), patch( - "homeassistant.bootstrap.async_setup_hass", return_value=hass - ), patch("threading._shutdown"), patch( - "homeassistant.core.HomeAssistant.async_run", _async_create_tasks + with ( + patch.object(runner, "TASK_CANCELATION_TIMEOUT", 1), + patch("homeassistant.bootstrap.async_setup_hass", return_value=hass), + patch("threading._shutdown"), + patch("homeassistant.core.HomeAssistant.async_run", _async_create_tasks), ): runner.run(default_config) @@ -158,14 +166,22 @@ def test__enable_posix_spawn() -> None: def _mock_sys_tags_musl() -> Iterator[packaging.tags.Tag]: yield from packaging.tags.parse_tag("cp311-cp311-musllinux_1_1_x86_64") - with patch.object(runner.subprocess, "_USE_POSIX_SPAWN", False), patch( - "homeassistant.runner.packaging.tags.sys_tags", side_effect=_mock_sys_tags_musl + with ( + patch.object(runner.subprocess, "_USE_POSIX_SPAWN", False), + patch( + "homeassistant.runner.packaging.tags.sys_tags", + side_effect=_mock_sys_tags_musl, + ), ): runner._enable_posix_spawn() assert runner.subprocess._USE_POSIX_SPAWN is True - with patch.object(runner.subprocess, "_USE_POSIX_SPAWN", False), patch( - "homeassistant.runner.packaging.tags.sys_tags", side_effect=_mock_sys_tags_any + with ( + patch.object(runner.subprocess, "_USE_POSIX_SPAWN", False), + patch( + "homeassistant.runner.packaging.tags.sys_tags", + side_effect=_mock_sys_tags_any, + ), ): runner._enable_posix_spawn() assert runner.subprocess._USE_POSIX_SPAWN is False diff --git a/tests/test_setup.py b/tests/test_setup.py index 0f6a4302200..e3d9a322862 100644 --- a/tests/test_setup.py +++ b/tests/test_setup.py @@ -2,20 +2,25 @@ import asyncio import threading -from unittest.mock import AsyncMock, Mock, patch +from unittest.mock import ANY, AsyncMock, Mock, patch +from freezegun.api import FrozenDateTimeFactory import pytest import voluptuous as vol -from homeassistant import config_entries, setup +from homeassistant import config_entries, loader, setup from homeassistant.const import EVENT_COMPONENT_LOADED, EVENT_HOMEASSISTANT_START -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import CoreState, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import discovery, translation from homeassistant.helpers.config_validation import ( PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, ) +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from .common import ( MockConfigEntry, @@ -389,9 +394,10 @@ async def test_platform_specific_config_validation(hass: HomeAssistant) -> None: MockPlatform(platform_schema=platform_schema, setup_platform=mock_setup), ) - with assert_setup_component(0, "switch"), patch( - "homeassistant.setup.async_notify_setup_error" - ) as mock_notify: + with ( + assert_setup_component(0, "switch"), + patch("homeassistant.setup.async_notify_setup_error") as mock_notify, + ): assert await setup.async_setup_component( hass, "switch", @@ -404,9 +410,10 @@ async def test_platform_specific_config_validation(hass: HomeAssistant) -> None: hass.data.pop(setup.DATA_SETUP) hass.config.components.remove("switch") - with assert_setup_component(0), patch( - "homeassistant.setup.async_notify_setup_error" - ) as mock_notify: + with ( + assert_setup_component(0), + patch("homeassistant.setup.async_notify_setup_error") as mock_notify, + ): assert await setup.async_setup_component( hass, "switch", @@ -425,9 +432,10 @@ async def test_platform_specific_config_validation(hass: HomeAssistant) -> None: hass.data.pop(setup.DATA_SETUP) hass.config.components.remove("switch") - with assert_setup_component(1, "switch"), patch( - "homeassistant.setup.async_notify_setup_error" - ) as mock_notify: + with ( + assert_setup_component(1, "switch"), + patch("homeassistant.setup.async_notify_setup_error") as mock_notify, + ): assert await setup.async_setup_component( hass, "switch", @@ -617,7 +625,7 @@ async def test_async_when_setup_or_start_already_loaded(hass: HomeAssistant) -> async def test_setup_import_blows_up(hass: HomeAssistant) -> None: """Test that we handle it correctly when importing integration blows up.""" with patch( - "homeassistant.loader.Integration.get_component", side_effect=ImportError + "homeassistant.loader.Integration.async_get_component", side_effect=ImportError ): assert not await setup.async_setup_component(hass, "sun", {}) @@ -727,26 +735,379 @@ async def test_integration_only_setup_entry(hass: HomeAssistant) -> None: assert await setup.async_setup_component(hass, "test_integration_only_entry", {}) -async def test_async_start_setup(hass: HomeAssistant) -> None: - """Test setup started context manager keeps track of setup times.""" - with setup.async_start_setup(hass, ["august"]): - assert isinstance(hass.data[setup.DATA_SETUP_STARTED]["august"], float) - with setup.async_start_setup(hass, ["august"]): - assert isinstance(hass.data[setup.DATA_SETUP_STARTED]["august_2"], float) +async def test_async_start_setup_running(hass: HomeAssistant) -> None: + """Test setup started context manager does nothing when running.""" + assert hass.state is CoreState.running + setup_started: dict[tuple[str, str | None], float] + setup_started = hass.data.setdefault(setup.DATA_SETUP_STARTED, {}) - assert "august" not in hass.data[setup.DATA_SETUP_STARTED] - assert isinstance(hass.data[setup.DATA_SETUP_TIME]["august"], float) - assert "august_2" not in hass.data[setup.DATA_SETUP_TIME] + with setup.async_start_setup( + hass, integration="august", phase=setup.SetupPhases.SETUP + ): + assert not setup_started -async def test_async_start_setup_platforms(hass: HomeAssistant) -> None: - """Test setup started context manager keeps track of setup times for platforms.""" - with setup.async_start_setup(hass, ["august.sensor"]): - assert isinstance(hass.data[setup.DATA_SETUP_STARTED]["august.sensor"], float) +async def test_async_start_setup_config_entry( + hass: HomeAssistant, freezer: FrozenDateTimeFactory +) -> None: + """Test setup started keeps track of setup times with a config entry.""" + hass.set_state(CoreState.not_running) + setup_started: dict[tuple[str, str | None], float] + setup_started = hass.data.setdefault(setup.DATA_SETUP_STARTED, {}) + setup_time = setup._setup_times(hass) - assert "august" not in hass.data[setup.DATA_SETUP_STARTED] - assert isinstance(hass.data[setup.DATA_SETUP_TIME]["august"], float) - assert "sensor" not in hass.data[setup.DATA_SETUP_TIME] + with setup.async_start_setup( + hass, integration="august", phase=setup.SetupPhases.SETUP + ): + assert isinstance(setup_started[("august", None)], float) + + with setup.async_start_setup( + hass, + integration="august", + group="entry_id", + phase=setup.SetupPhases.CONFIG_ENTRY_SETUP, + ): + assert isinstance(setup_started[("august", "entry_id")], float) + with setup.async_start_setup( + hass, + integration="august", + group="entry_id", + phase=setup.SetupPhases.CONFIG_ENTRY_PLATFORM_SETUP, + ): + assert isinstance(setup_started[("august", "entry_id")], float) + + # CONFIG_ENTRY_PLATFORM_SETUP inside of CONFIG_ENTRY_SETUP should not be tracked + assert setup_time["august"] == { + None: {setup.SetupPhases.SETUP: ANY}, + "entry_id": {setup.SetupPhases.CONFIG_ENTRY_SETUP: ANY}, + } + with setup.async_start_setup( + hass, + integration="august", + group="entry_id", + phase=setup.SetupPhases.CONFIG_ENTRY_PLATFORM_SETUP, + ): + assert isinstance(setup_started[("august", "entry_id")], float) + + # Platforms outside of CONFIG_ENTRY_SETUP should be tracked + # This simulates a late platform forward + assert setup_time["august"] == { + None: {setup.SetupPhases.SETUP: ANY}, + "entry_id": { + setup.SetupPhases.CONFIG_ENTRY_SETUP: ANY, + setup.SetupPhases.CONFIG_ENTRY_PLATFORM_SETUP: ANY, + }, + } + + shorter_time = setup_time["august"]["entry_id"][ + setup.SetupPhases.CONFIG_ENTRY_PLATFORM_SETUP + ] + # Setup another platform, but make it take longer + with setup.async_start_setup( + hass, + integration="august", + group="entry_id", + phase=setup.SetupPhases.CONFIG_ENTRY_PLATFORM_SETUP, + ): + freezer.tick(10) + assert isinstance(setup_started[("august", "entry_id")], float) + + longer_time = setup_time["august"]["entry_id"][ + setup.SetupPhases.CONFIG_ENTRY_PLATFORM_SETUP + ] + assert longer_time > shorter_time + # Setup another platform, but make it take shorter + with setup.async_start_setup( + hass, + integration="august", + group="entry_id", + phase=setup.SetupPhases.CONFIG_ENTRY_PLATFORM_SETUP, + ): + assert isinstance(setup_started[("august", "entry_id")], float) + + # Ensure we keep the longest time + assert ( + setup_time["august"]["entry_id"][setup.SetupPhases.CONFIG_ENTRY_PLATFORM_SETUP] + == longer_time + ) + + with setup.async_start_setup( + hass, + integration="august", + group="entry_id2", + phase=setup.SetupPhases.CONFIG_ENTRY_SETUP, + ): + assert isinstance(setup_started[("august", "entry_id2")], float) + # We wrap places where we wait for other components + # or the import of a module with async_freeze_setup + # so we can subtract the time waited from the total setup time + with setup.async_pause_setup(hass, setup.SetupPhases.WAIT_BASE_PLATFORM_SETUP): + await asyncio.sleep(0) + + # Wait time should be added if freeze_setup is used + assert setup_time["august"] == { + None: {setup.SetupPhases.SETUP: ANY}, + "entry_id": { + setup.SetupPhases.CONFIG_ENTRY_SETUP: ANY, + setup.SetupPhases.CONFIG_ENTRY_PLATFORM_SETUP: ANY, + }, + "entry_id2": { + setup.SetupPhases.CONFIG_ENTRY_SETUP: ANY, + setup.SetupPhases.WAIT_BASE_PLATFORM_SETUP: ANY, + }, + } + + +async def test_async_start_setup_config_entry_late_platform( + hass: HomeAssistant, freezer: FrozenDateTimeFactory +) -> None: + """Test setup started tracks config entry time with a late platform load.""" + hass.set_state(CoreState.not_running) + setup_started: dict[tuple[str, str | None], float] + setup_started = hass.data.setdefault(setup.DATA_SETUP_STARTED, {}) + setup_time = setup._setup_times(hass) + + with setup.async_start_setup( + hass, integration="august", phase=setup.SetupPhases.SETUP + ): + freezer.tick(10) + assert isinstance(setup_started[("august", None)], float) + + with setup.async_start_setup( + hass, + integration="august", + group="entry_id", + phase=setup.SetupPhases.CONFIG_ENTRY_SETUP, + ): + assert isinstance(setup_started[("august", "entry_id")], float) + + @callback + def async_late_platform_load(): + with setup.async_pause_setup(hass, setup.SetupPhases.WAIT_IMPORT_PLATFORMS): + freezer.tick(100) + with setup.async_start_setup( + hass, + integration="august", + group="entry_id", + phase=setup.SetupPhases.CONFIG_ENTRY_PLATFORM_SETUP, + ): + freezer.tick(20) + assert isinstance(setup_started[("august", "entry_id")], float) + + disconnect = async_dispatcher_connect( + hass, "late_platform_load_test", async_late_platform_load + ) + + # Dispatch a late platform load + async_dispatcher_send(hass, "late_platform_load_test") + disconnect() + + # CONFIG_ENTRY_PLATFORM_SETUP is late dispatched, so it should be tracked + # but any waiting time should not be because it's blocking the setup + assert setup_time["august"] == { + None: {setup.SetupPhases.SETUP: 10.0}, + "entry_id": { + setup.SetupPhases.CONFIG_ENTRY_PLATFORM_SETUP: 20.0, + setup.SetupPhases.CONFIG_ENTRY_SETUP: 0.0, + }, + } + + +async def test_async_start_setup_config_entry_platform_wait( + hass: HomeAssistant, freezer: FrozenDateTimeFactory +) -> None: + """Test setup started tracks wait time when a platform loads inside of config entry setup.""" + hass.set_state(CoreState.not_running) + setup_started: dict[tuple[str, str | None], float] + setup_started = hass.data.setdefault(setup.DATA_SETUP_STARTED, {}) + setup_time = setup._setup_times(hass) + + with setup.async_start_setup( + hass, integration="august", phase=setup.SetupPhases.SETUP + ): + freezer.tick(10) + assert isinstance(setup_started[("august", None)], float) + + with setup.async_start_setup( + hass, + integration="august", + group="entry_id", + phase=setup.SetupPhases.CONFIG_ENTRY_SETUP, + ): + assert isinstance(setup_started[("august", "entry_id")], float) + + with setup.async_pause_setup(hass, setup.SetupPhases.WAIT_IMPORT_PLATFORMS): + freezer.tick(100) + with setup.async_start_setup( + hass, + integration="august", + group="entry_id", + phase=setup.SetupPhases.CONFIG_ENTRY_PLATFORM_SETUP, + ): + freezer.tick(20) + assert isinstance(setup_started[("august", "entry_id")], float) + + # CONFIG_ENTRY_PLATFORM_SETUP is run inside of CONFIG_ENTRY_SETUP, so it should not + # be tracked, but any wait time should still be tracked because its blocking the setup + assert setup_time["august"] == { + None: {setup.SetupPhases.SETUP: 10.0}, + "entry_id": { + setup.SetupPhases.WAIT_IMPORT_PLATFORMS: -100.0, + setup.SetupPhases.CONFIG_ENTRY_SETUP: 120.0, + }, + } + + +async def test_async_start_setup_top_level_yaml(hass: HomeAssistant) -> None: + """Test setup started context manager keeps track of setup times with modern yaml.""" + hass.set_state(CoreState.not_running) + setup_started: dict[tuple[str, str | None], float] + setup_started = hass.data.setdefault(setup.DATA_SETUP_STARTED, {}) + setup_time = setup._setup_times(hass) + + with setup.async_start_setup( + hass, integration="command_line", phase=setup.SetupPhases.SETUP + ): + assert isinstance(setup_started[("command_line", None)], float) + + assert setup_time["command_line"] == { + None: {setup.SetupPhases.SETUP: ANY}, + } + + +async def test_async_start_setup_platform_integration(hass: HomeAssistant) -> None: + """Test setup started keeps track of setup times a platform integration.""" + hass.set_state(CoreState.not_running) + setup_started: dict[tuple[str, str | None], float] + setup_started = hass.data.setdefault(setup.DATA_SETUP_STARTED, {}) + setup_time = setup._setup_times(hass) + + with setup.async_start_setup( + hass, integration="sensor", phase=setup.SetupPhases.SETUP + ): + assert isinstance(setup_started[("sensor", None)], float) + + # Platform integration setups happen in another task + with setup.async_start_setup( + hass, + integration="filter", + group="123456", + phase=setup.SetupPhases.PLATFORM_SETUP, + ): + assert isinstance(setup_started[("filter", "123456")], float) + + assert setup_time["sensor"] == { + None: { + setup.SetupPhases.SETUP: ANY, + }, + } + assert setup_time["filter"] == { + "123456": { + setup.SetupPhases.PLATFORM_SETUP: ANY, + }, + } + + +async def test_async_start_setup_legacy_platform_integration( + hass: HomeAssistant, +) -> None: + """Test setup started keeps track of setup times for a legacy platform integration.""" + hass.set_state(CoreState.not_running) + setup_started: dict[tuple[str, str | None], float] + setup_started = hass.data.setdefault(setup.DATA_SETUP_STARTED, {}) + setup_time = setup._setup_times(hass) + + with setup.async_start_setup( + hass, integration="notify", phase=setup.SetupPhases.SETUP + ): + assert isinstance(setup_started[("notify", None)], float) + + with setup.async_start_setup( + hass, + integration="legacy_notify_integration", + group="123456", + phase=setup.SetupPhases.PLATFORM_SETUP, + ): + assert isinstance(setup_started[("legacy_notify_integration", "123456")], float) + + assert setup_time["notify"] == { + None: { + setup.SetupPhases.SETUP: ANY, + }, + } + assert setup_time["legacy_notify_integration"] == { + "123456": { + setup.SetupPhases.PLATFORM_SETUP: ANY, + }, + } + + +async def test_async_start_setup_simple_integration_end_to_end( + hass: HomeAssistant, +) -> None: + """Test end to end timings for a simple integration with no platforms.""" + hass.set_state(CoreState.not_running) + mock_integration( + hass, + MockModule( + "test_integration_no_platforms", + setup=False, + async_setup_entry=AsyncMock(return_value=True), + ), + ) + assert await setup.async_setup_component(hass, "test_integration_no_platforms", {}) + await hass.async_block_till_done() + assert setup.async_get_setup_timings(hass) == { + "test_integration_no_platforms": ANY, + } + + +async def test_async_get_setup_timings(hass) -> None: + """Test we can get the setup timings from the setup time data.""" + setup_time = setup._setup_times(hass) + # Mock setup time data + setup_time.update( + { + "august": { + None: {setup.SetupPhases.SETUP: 1}, + "entry_id": { + setup.SetupPhases.CONFIG_ENTRY_SETUP: 1, + setup.SetupPhases.CONFIG_ENTRY_PLATFORM_SETUP: 4, + }, + "entry_id2": { + setup.SetupPhases.CONFIG_ENTRY_SETUP: 7, + setup.SetupPhases.WAIT_BASE_PLATFORM_SETUP: -5, + }, + }, + "notify": { + None: { + setup.SetupPhases.SETUP: 2, + }, + }, + "legacy_notify_integration": { + "123456": { + setup.SetupPhases.PLATFORM_SETUP: 3, + }, + }, + "sensor": { + None: { + setup.SetupPhases.SETUP: 1, + }, + }, + "filter": { + "123456": { + setup.SetupPhases.PLATFORM_SETUP: 2, + }, + }, + } + ) + assert setup.async_get_setup_timings(hass) == { + "august": 6, + "notify": 2, + "legacy_notify_integration": 3, + "sensor": 1, + "filter": 2, + } async def test_setup_config_entry_from_yaml( @@ -809,8 +1170,9 @@ async def test_loading_component_loads_translations(hass: HomeAssistant) -> None mock_setup = Mock(return_value=True) mock_integration(hass, MockModule("comp", setup=mock_setup)) - - assert await setup.async_setup_component(hass, "comp", {}) + integration = await loader.async_get_integration(hass, "comp") + with patch.object(integration, "has_translations", True): + assert await setup.async_setup_component(hass, "comp", {}) assert mock_setup.called assert translation.async_translations_loaded(hass, {"comp"}) is True @@ -822,3 +1184,30 @@ async def test_importing_integration_in_executor( assert await setup.async_setup_component(hass, "test_package_loaded_executor", {}) assert await setup.async_setup_component(hass, "test_package_loaded_executor", {}) await hass.async_block_till_done() + + +async def test_async_prepare_setup_platform( + hass: HomeAssistant, + enable_custom_integrations: None, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test we can prepare a platform setup.""" + integration = await loader.async_get_integration(hass, "test") + with patch.object( + integration, "async_get_component", side_effect=ImportError("test is broken") + ): + assert ( + await setup.async_prepare_setup_platform(hass, {}, "config", "test") is None + ) + + assert "test is broken" in caplog.text + + caplog.clear() + # There is no actual config platform for this integration + assert await setup.async_prepare_setup_platform(hass, {}, "config", "test") is None + assert "No module named 'custom_components.test.config'" in caplog.text + + button_platform = ( + await setup.async_prepare_setup_platform(hass, {}, "button", "test") is None + ) + assert button_platform is not None diff --git a/tests/test_test_fixtures.py b/tests/test_test_fixtures.py index eb2103d4272..b240da3e31e 100644 --- a/tests/test_test_fixtures.py +++ b/tests/test_test_fixtures.py @@ -1,4 +1,5 @@ """Test test fixture configuration.""" + from http import HTTPStatus import socket diff --git a/tests/test_util/__init__.py b/tests/test_util/__init__.py index fe2c2c640e5..dd88af965fe 100644 --- a/tests/test_util/__init__.py +++ b/tests/test_util/__init__.py @@ -1,4 +1,5 @@ """Test utilities.""" + from collections.abc import Awaitable, Callable from aiohttp.web import Application, Request, StreamResponse, middleware diff --git a/tests/test_util/aiohttp.py b/tests/test_util/aiohttp.py index 4f2518253ff..742b111143f 100644 --- a/tests/test_util/aiohttp.py +++ b/tests/test_util/aiohttp.py @@ -1,4 +1,5 @@ """Aiohttp test utils.""" + import asyncio from contextlib import contextmanager from http import HTTPStatus @@ -334,7 +335,7 @@ class MockLongPollSideEffect: async def __call__(self, method, url, data): """Fetch the next response from the queue or wait until the queue has items.""" if self.stopping: - raise ClientError() + raise ClientError await self.semaphore.acquire() kwargs = self.response_list.pop(0) return AiohttpClientMockResponse(method=method, url=url, **kwargs) diff --git a/tests/test_util/test_aiohttp.py b/tests/test_util/test_aiohttp.py index 525022685bd..7226e84b04c 100644 --- a/tests/test_util/test_aiohttp.py +++ b/tests/test_util/test_aiohttp.py @@ -1,4 +1,5 @@ """Tests for our aiohttp mocker.""" + import pytest from .aiohttp import AiohttpClientMocker diff --git a/tests/testing_config/custom_components/test/alarm_control_panel.py b/tests/testing_config/custom_components/test/alarm_control_panel.py deleted file mode 100644 index 7490a7703a4..00000000000 --- a/tests/testing_config/custom_components/test/alarm_control_panel.py +++ /dev/null @@ -1,111 +0,0 @@ -"""Provide a mock alarm_control_panel platform. - -Call init before using it in your tests to ensure clean test data. -""" -from homeassistant.components.alarm_control_panel import AlarmControlPanelEntity -from homeassistant.components.alarm_control_panel.const import ( - AlarmControlPanelEntityFeature, -) -from homeassistant.const import ( - STATE_ALARM_ARMED_AWAY, - STATE_ALARM_ARMED_HOME, - STATE_ALARM_ARMED_NIGHT, - STATE_ALARM_ARMED_VACATION, - STATE_ALARM_DISARMED, - STATE_ALARM_TRIGGERED, -) - -from tests.common import MockEntity - -ENTITIES = {} - - -def init(empty=False): - """Initialize the platform with entities.""" - global ENTITIES - - ENTITIES = ( - {} - if empty - else { - "arm_code": MockAlarm( - name="Alarm arm code", - code_arm_required=True, - unique_id="unique_arm_code", - ), - "no_arm_code": MockAlarm( - name="Alarm no arm code", - code_arm_required=False, - unique_id="unique_no_arm_code", - ), - } - ) - - -async def async_setup_platform( - hass, config, async_add_entities_callback, discovery_info=None -): - """Return mock entities.""" - async_add_entities_callback(list(ENTITIES.values())) - - -class MockAlarm(MockEntity, AlarmControlPanelEntity): - """Mock Alarm control panel class.""" - - def __init__(self, **values): - """Init the Mock Alarm Control Panel.""" - self._state = None - - MockEntity.__init__(self, **values) - - @property - def code_arm_required(self): - """Whether the code is required for arm actions.""" - return self._handle("code_arm_required") - - @property - def state(self): - """Return the state of the device.""" - return self._state - - @property - def supported_features(self) -> AlarmControlPanelEntityFeature: - """Return the list of supported features.""" - return ( - AlarmControlPanelEntityFeature.ARM_HOME - | AlarmControlPanelEntityFeature.ARM_AWAY - | AlarmControlPanelEntityFeature.ARM_NIGHT - | AlarmControlPanelEntityFeature.TRIGGER - | AlarmControlPanelEntityFeature.ARM_VACATION - ) - - def alarm_arm_away(self, code=None): - """Send arm away command.""" - self._state = STATE_ALARM_ARMED_AWAY - self.schedule_update_ha_state() - - def alarm_arm_home(self, code=None): - """Send arm home command.""" - self._state = STATE_ALARM_ARMED_HOME - self.schedule_update_ha_state() - - def alarm_arm_night(self, code=None): - """Send arm night command.""" - self._state = STATE_ALARM_ARMED_NIGHT - self.schedule_update_ha_state() - - def alarm_arm_vacation(self, code=None): - """Send arm night command.""" - self._state = STATE_ALARM_ARMED_VACATION - self.schedule_update_ha_state() - - def alarm_disarm(self, code=None): - """Send disarm command.""" - if code == "1234": - self._state = STATE_ALARM_DISARMED - self.schedule_update_ha_state() - - def alarm_trigger(self, code=None): - """Send alarm trigger command.""" - self._state = STATE_ALARM_TRIGGERED - self.schedule_update_ha_state() diff --git a/tests/testing_config/custom_components/test/binary_sensor.py b/tests/testing_config/custom_components/test/binary_sensor.py deleted file mode 100644 index 6deaad22b30..00000000000 --- a/tests/testing_config/custom_components/test/binary_sensor.py +++ /dev/null @@ -1,49 +0,0 @@ -"""Provide a mock binary sensor platform. - -Call init before using it in your tests to ensure clean test data. -""" -from homeassistant.components.binary_sensor import DEVICE_CLASSES, BinarySensorEntity - -from tests.common import MockEntity - -ENTITIES = {} - - -def init(empty=False): - """Initialize the platform with entities.""" - global ENTITIES - - ENTITIES = ( - {} - if empty - else { - device_class: MockBinarySensor( - name=f"{device_class} sensor", - is_on=True, - unique_id=f"unique_{device_class}", - device_class=device_class, - ) - for device_class in DEVICE_CLASSES - } - ) - - -async def async_setup_platform( - hass, config, async_add_entities_callback, discovery_info=None -): - """Return mock entities.""" - async_add_entities_callback(list(ENTITIES.values())) - - -class MockBinarySensor(MockEntity, BinarySensorEntity): - """Mock Binary Sensor class.""" - - @property - def is_on(self): - """Return true if the binary sensor is on.""" - return self._handle("is_on") - - @property - def device_class(self): - """Return the class of this sensor.""" - return self._handle("device_class") diff --git a/tests/testing_config/custom_components/test/button.py b/tests/testing_config/custom_components/test/button.py deleted file mode 100644 index 99c6868f834..00000000000 --- a/tests/testing_config/custom_components/test/button.py +++ /dev/null @@ -1,46 +0,0 @@ -"""Provide a mock button platform. - -Call init before using it in your tests to ensure clean test data. -""" -import logging - -from homeassistant.components.button import ButtonEntity - -from tests.common import MockEntity - -UNIQUE_BUTTON_1 = "unique_button_1" - -ENTITIES = [] - -_LOGGER = logging.getLogger(__name__) - - -class MockButtonEntity(MockEntity, ButtonEntity): - """Mock Button class.""" - - def press(self) -> None: - """Press the button.""" - _LOGGER.info("The button has been pressed") - - -def init(empty=False): - """Initialize the platform with entities.""" - global ENTITIES - - ENTITIES = ( - [] - if empty - else [ - MockButtonEntity( - name="button 1", - unique_id="unique_button_1", - ), - ] - ) - - -async def async_setup_platform( - hass, config, async_add_entities_callback, discovery_info=None -): - """Return mock entities.""" - async_add_entities_callback(ENTITIES) diff --git a/tests/testing_config/custom_components/test/cover.py b/tests/testing_config/custom_components/test/cover.py deleted file mode 100644 index dc89b95981b..00000000000 --- a/tests/testing_config/custom_components/test/cover.py +++ /dev/null @@ -1,164 +0,0 @@ -"""Provide a mock cover platform. - -Call init before using it in your tests to ensure clean test data. -""" -from typing import Any - -from homeassistant.components.cover import CoverEntity, CoverEntityFeature -from homeassistant.const import STATE_CLOSED, STATE_CLOSING, STATE_OPEN, STATE_OPENING - -from tests.common import MockEntity - -ENTITIES = [] - - -def init(empty=False): - """Initialize the platform with entities.""" - global ENTITIES - - ENTITIES = ( - [] - if empty - else [ - MockCover( - name="Simple cover", - is_on=True, - unique_id="unique_cover", - supported_features=CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE, - ), - MockCover( - name="Set position cover", - is_on=True, - unique_id="unique_set_pos_cover", - current_cover_position=50, - supported_features=CoverEntityFeature.OPEN - | CoverEntityFeature.CLOSE - | CoverEntityFeature.STOP - | CoverEntityFeature.SET_POSITION, - ), - MockCover( - name="Simple tilt cover", - is_on=True, - unique_id="unique_tilt_cover", - supported_features=CoverEntityFeature.OPEN - | CoverEntityFeature.CLOSE - | CoverEntityFeature.OPEN_TILT - | CoverEntityFeature.CLOSE_TILT, - ), - MockCover( - name="Set tilt position cover", - is_on=True, - unique_id="unique_set_pos_tilt_cover", - current_cover_tilt_position=50, - supported_features=CoverEntityFeature.OPEN - | CoverEntityFeature.CLOSE - | CoverEntityFeature.OPEN_TILT - | CoverEntityFeature.CLOSE_TILT - | CoverEntityFeature.STOP_TILT - | CoverEntityFeature.SET_TILT_POSITION, - ), - MockCover( - name="All functions cover", - is_on=True, - unique_id="unique_all_functions_cover", - current_cover_position=50, - current_cover_tilt_position=50, - supported_features=CoverEntityFeature.OPEN - | CoverEntityFeature.CLOSE - | CoverEntityFeature.STOP - | CoverEntityFeature.SET_POSITION - | CoverEntityFeature.OPEN_TILT - | CoverEntityFeature.CLOSE_TILT - | CoverEntityFeature.STOP_TILT - | CoverEntityFeature.SET_TILT_POSITION, - ), - MockCover( - name="Simple with opening/closing cover", - is_on=True, - unique_id="unique_opening_closing_cover", - supported_features=CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE, - reports_opening_closing=True, - ), - ] - ) - - -async def async_setup_platform( - hass, config, async_add_entities_callback, discovery_info=None -): - """Return mock entities.""" - async_add_entities_callback(ENTITIES) - - -class MockCover(MockEntity, CoverEntity): - """Mock Cover class.""" - - def __init__( - self, reports_opening_closing: bool | None = None, **values: Any - ) -> None: - """Initialize a mock cover entity.""" - - super().__init__(**values) - self._reports_opening_closing = ( - reports_opening_closing - if reports_opening_closing is not None - else CoverEntityFeature.STOP in self.supported_features - ) - - @property - def is_closed(self): - """Return if the cover is closed or not.""" - if "state" in self._values and self._values["state"] == STATE_CLOSED: - return True - - return self.current_cover_position == 0 - - @property - def is_opening(self): - """Return if the cover is opening or not.""" - if "state" in self._values: - return self._values["state"] == STATE_OPENING - - return False - - @property - def is_closing(self): - """Return if the cover is closing or not.""" - if "state" in self._values: - return self._values["state"] == STATE_CLOSING - - return False - - def open_cover(self, **kwargs) -> None: - """Open cover.""" - if self._reports_opening_closing: - self._values["state"] = STATE_OPENING - else: - self._values["state"] = STATE_OPEN - - def close_cover(self, **kwargs) -> None: - """Close cover.""" - if self._reports_opening_closing: - self._values["state"] = STATE_CLOSING - else: - self._values["state"] = STATE_CLOSED - - def stop_cover(self, **kwargs) -> None: - """Stop cover.""" - assert CoverEntityFeature.STOP in self.supported_features - self._values["state"] = STATE_CLOSED if self.is_closed else STATE_OPEN - - @property - def state(self): - """Fake State.""" - return CoverEntity.state.fget(self) - - @property - def current_cover_position(self): - """Return current position of cover.""" - return self._handle("current_cover_position") - - @property - def current_cover_tilt_position(self): - """Return current position of cover tilt.""" - return self._handle("current_cover_tilt_position") diff --git a/tests/testing_config/custom_components/test/date.py b/tests/testing_config/custom_components/test/date.py index b35be6f1919..0a51bea029d 100644 --- a/tests/testing_config/custom_components/test/date.py +++ b/tests/testing_config/custom_components/test/date.py @@ -2,6 +2,7 @@ Call init before using it in your tests to ensure clean test data. """ + from datetime import date from homeassistant.components.date import DateEntity diff --git a/tests/testing_config/custom_components/test/datetime.py b/tests/testing_config/custom_components/test/datetime.py index ba511e81648..fa9dfff8a60 100644 --- a/tests/testing_config/custom_components/test/datetime.py +++ b/tests/testing_config/custom_components/test/datetime.py @@ -2,6 +2,7 @@ Call init before using it in your tests to ensure clean test data. """ + from datetime import UTC, datetime from homeassistant.components.datetime import DateTimeEntity diff --git a/tests/testing_config/custom_components/test/event.py b/tests/testing_config/custom_components/test/event.py deleted file mode 100644 index 9acb24f37cf..00000000000 --- a/tests/testing_config/custom_components/test/event.py +++ /dev/null @@ -1,42 +0,0 @@ -"""Provide a mock event platform. - -Call init before using it in your tests to ensure clean test data. -""" -from homeassistant.components.event import EventEntity - -from tests.common import MockEntity - -ENTITIES = [] - - -class MockEventEntity(MockEntity, EventEntity): - """Mock EventEntity class.""" - - @property - def event_types(self) -> list[str]: - """Return a list of possible events.""" - return self._handle("event_types") - - -def init(empty=False): - """Initialize the platform with entities.""" - global ENTITIES - - ENTITIES = ( - [] - if empty - else [ - MockEventEntity( - name="doorbell", - unique_id="unique_doorbell", - event_types=["short_press", "long_press"], - ), - ] - ) - - -async def async_setup_platform( - hass, config, async_add_entities_callback, discovery_info=None -): - """Return mock entities.""" - async_add_entities_callback(ENTITIES) diff --git a/tests/testing_config/custom_components/test/fan.py b/tests/testing_config/custom_components/test/fan.py index 133f372f4fa..cc38972bc71 100644 --- a/tests/testing_config/custom_components/test/fan.py +++ b/tests/testing_config/custom_components/test/fan.py @@ -2,6 +2,7 @@ Call init before using it in your tests to ensure clean test data. """ + from homeassistant.components.fan import FanEntity, FanEntityFeature from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback diff --git a/tests/testing_config/custom_components/test/light.py b/tests/testing_config/custom_components/test/light.py index e22aca289a8..eed98a8210a 100644 --- a/tests/testing_config/custom_components/test/light.py +++ b/tests/testing_config/custom_components/test/light.py @@ -2,6 +2,7 @@ Call init before using it in your tests to ensure clean test data. """ + from homeassistant.components.light import ColorMode, LightEntity from homeassistant.const import STATE_OFF, STATE_ON diff --git a/tests/testing_config/custom_components/test/lock.py b/tests/testing_config/custom_components/test/lock.py index 9cefa34363e..ba5a91e2d24 100644 --- a/tests/testing_config/custom_components/test/lock.py +++ b/tests/testing_config/custom_components/test/lock.py @@ -2,6 +2,7 @@ Call init before using it in your tests to ensure clean test data. """ + from homeassistant.components.lock import LockEntity, LockEntityFeature from tests.common import MockEntity diff --git a/tests/testing_config/custom_components/test/remote.py b/tests/testing_config/custom_components/test/remote.py index 6c5242a739e..541215f1c47 100644 --- a/tests/testing_config/custom_components/test/remote.py +++ b/tests/testing_config/custom_components/test/remote.py @@ -2,6 +2,7 @@ Call init before using it in your tests to ensure clean test data. """ + from homeassistant.components.remote import RemoteEntity from homeassistant.const import STATE_OFF, STATE_ON diff --git a/tests/testing_config/custom_components/test/select.py b/tests/testing_config/custom_components/test/select.py index d68b3008a50..fece370bdf1 100644 --- a/tests/testing_config/custom_components/test/select.py +++ b/tests/testing_config/custom_components/test/select.py @@ -2,6 +2,7 @@ Call init before using it in your tests to ensure clean test data. """ + from homeassistant.components.select import SelectEntity from tests.common import MockEntity diff --git a/tests/testing_config/custom_components/test/switch.py b/tests/testing_config/custom_components/test/switch.py index 6f132b85a3a..5a2cd7bc17d 100644 --- a/tests/testing_config/custom_components/test/switch.py +++ b/tests/testing_config/custom_components/test/switch.py @@ -2,6 +2,7 @@ Call init before using it in your tests to ensure clean test data. """ + from homeassistant.const import STATE_OFF, STATE_ON from tests.common import MockToggleEntity diff --git a/tests/testing_config/custom_components/test/text.py b/tests/testing_config/custom_components/test/text.py index b113423b4e3..d3b048747bf 100644 --- a/tests/testing_config/custom_components/test/text.py +++ b/tests/testing_config/custom_components/test/text.py @@ -2,6 +2,7 @@ Call init before using it in your tests to ensure clean test data. """ + from homeassistant.components.text import RestoreText, TextEntity, TextMode from tests.common import MockEntity diff --git a/tests/testing_config/custom_components/test/time.py b/tests/testing_config/custom_components/test/time.py index 9c2f991d694..998406d7830 100644 --- a/tests/testing_config/custom_components/test/time.py +++ b/tests/testing_config/custom_components/test/time.py @@ -2,6 +2,7 @@ Call init before using it in your tests to ensure clean test data. """ + from datetime import time from homeassistant.components.time import TimeEntity diff --git a/tests/testing_config/custom_components/test/update.py b/tests/testing_config/custom_components/test/update.py deleted file mode 100644 index 36b4e7c692f..00000000000 --- a/tests/testing_config/custom_components/test/update.py +++ /dev/null @@ -1,158 +0,0 @@ -"""Provide a mock update platform. - -Call init before using it in your tests to ensure clean test data. -""" -from __future__ import annotations - -import logging -from typing import Any - -from homeassistant.components.update import UpdateEntity, UpdateEntityFeature - -from tests.common import MockEntity - -ENTITIES = [] - -_LOGGER = logging.getLogger(__name__) - - -class MockUpdateEntity(MockEntity, UpdateEntity): - """Mock UpdateEntity class.""" - - @property - def auto_update(self) -> bool: - """Indicate if the device or service has auto update enabled.""" - return self._handle("auto_update") - - @property - def installed_version(self) -> str | None: - """Version currently installed and in use.""" - return self._handle("installed_version") - - @property - def in_progress(self) -> bool | int | None: - """Update installation progress.""" - return self._handle("in_progress") - - @property - def latest_version(self) -> str | None: - """Latest version available for install.""" - return self._handle("latest_version") - - @property - def release_summary(self) -> str | None: - """Summary of the release notes or changelog.""" - return self._handle("release_summary") - - @property - def release_url(self) -> str | None: - """URL to the full release notes of the latest version available.""" - return self._handle("release_url") - - @property - def title(self) -> str | None: - """Title of the software.""" - return self._handle("title") - - def install(self, version: str | None, backup: bool, **kwargs: Any) -> None: - """Install an update.""" - if backup: - _LOGGER.info("Creating backup before installing update") - - if version is not None: - self._values["installed_version"] = version - _LOGGER.info("Installed update with version: %s", version) - else: - self._values["installed_version"] = self.latest_version - _LOGGER.info("Installed latest update") - - def release_notes(self) -> str | None: - """Return the release notes of the latest version.""" - return "Release notes" - - -def init(empty=False): - """Initialize the platform with entities.""" - global ENTITIES - - ENTITIES = ( - [] - if empty - else [ - MockUpdateEntity( - name="No Update", - unique_id="no_update", - installed_version="1.0.0", - latest_version="1.0.0", - supported_features=UpdateEntityFeature.INSTALL, - ), - MockUpdateEntity( - name="Update Available", - unique_id="update_available", - installed_version="1.0.0", - latest_version="1.0.1", - supported_features=UpdateEntityFeature.INSTALL, - ), - MockUpdateEntity( - name="Update Unknown", - unique_id="update_unknown", - installed_version="1.0.0", - latest_version=None, - supported_features=UpdateEntityFeature.INSTALL, - ), - MockUpdateEntity( - name="Update Specific Version", - unique_id="update_specific_version", - installed_version="1.0.0", - latest_version="1.0.0", - supported_features=UpdateEntityFeature.INSTALL - | UpdateEntityFeature.SPECIFIC_VERSION, - ), - MockUpdateEntity( - name="Update Backup", - unique_id="update_backup", - installed_version="1.0.0", - latest_version="1.0.1", - supported_features=UpdateEntityFeature.INSTALL - | UpdateEntityFeature.SPECIFIC_VERSION - | UpdateEntityFeature.BACKUP, - ), - MockUpdateEntity( - name="Update Already in Progress", - unique_id="update_already_in_progres", - installed_version="1.0.0", - latest_version="1.0.1", - in_progress=50, - supported_features=UpdateEntityFeature.INSTALL - | UpdateEntityFeature.PROGRESS, - ), - MockUpdateEntity( - name="Update No Install", - unique_id="no_install", - installed_version="1.0.0", - latest_version="1.0.1", - ), - MockUpdateEntity( - name="Update with release notes", - unique_id="with_release_notes", - installed_version="1.0.0", - latest_version="1.0.1", - supported_features=UpdateEntityFeature.RELEASE_NOTES, - ), - MockUpdateEntity( - name="Update with auto update", - unique_id="with_auto_update", - installed_version="1.0.0", - latest_version="1.0.1", - auto_update=True, - supported_features=UpdateEntityFeature.INSTALL, - ), - ] - ) - - -async def async_setup_platform( - hass, config, async_add_entities_callback, discovery_info=None -): - """Return mock entities.""" - async_add_entities_callback(ENTITIES) diff --git a/tests/testing_config/custom_components/test/weather.py b/tests/testing_config/custom_components/test/weather.py index 5afb6001b8a..0e99ef48680 100644 --- a/tests/testing_config/custom_components/test/weather.py +++ b/tests/testing_config/custom_components/test/weather.py @@ -2,6 +2,7 @@ Call init before using it in your tests to ensure clean test data. """ + from __future__ import annotations from typing import Any @@ -167,11 +168,6 @@ class MockWeatherMockForecast(MockWeather): } ] - @property - def forecast(self) -> list[Forecast] | None: - """Return the forecast.""" - return self.forecast_list - async def async_forecast_daily(self) -> list[Forecast] | None: """Return the forecast_daily.""" return self.forecast_list diff --git a/tests/testing_config/custom_components/test_embedded/__init__.py b/tests/testing_config/custom_components/test_embedded/__init__.py index 1d861392bf3..b83493817fd 100644 --- a/tests/testing_config/custom_components/test_embedded/__init__.py +++ b/tests/testing_config/custom_components/test_embedded/__init__.py @@ -1,4 +1,5 @@ """Component with embedded platforms.""" + DOMAIN = "test_embedded" diff --git a/tests/testing_config/custom_components/test_integration_platform/__init__.py b/tests/testing_config/custom_components/test_integration_platform/__init__.py new file mode 100644 index 00000000000..220beb05367 --- /dev/null +++ b/tests/testing_config/custom_components/test_integration_platform/__init__.py @@ -0,0 +1,10 @@ +"""Provide a mock package component.""" + +from .const import TEST # noqa: F401 + +DOMAIN = "test_integration_platform" + + +async def async_setup(hass, config): + """Mock a successful setup.""" + return True diff --git a/tests/testing_config/custom_components/test_integration_platform/config_flow.py b/tests/testing_config/custom_components/test_integration_platform/config_flow.py new file mode 100644 index 00000000000..9153b666828 --- /dev/null +++ b/tests/testing_config/custom_components/test_integration_platform/config_flow.py @@ -0,0 +1,7 @@ +"""Config flow.""" + +from homeassistant.core import HomeAssistant + + +async def _async_has_devices(hass: HomeAssistant) -> bool: + return True diff --git a/tests/testing_config/custom_components/test_integration_platform/const.py b/tests/testing_config/custom_components/test_integration_platform/const.py new file mode 100644 index 00000000000..e55504e7f4f --- /dev/null +++ b/tests/testing_config/custom_components/test_integration_platform/const.py @@ -0,0 +1,3 @@ +"""Constants for test_package custom component.""" + +TEST = 5 diff --git a/tests/testing_config/custom_components/test_integration_platform/group.py b/tests/testing_config/custom_components/test_integration_platform/group.py new file mode 100644 index 00000000000..070cfa0e406 --- /dev/null +++ b/tests/testing_config/custom_components/test_integration_platform/group.py @@ -0,0 +1,3 @@ +"""Group.""" + +MAGIC = 1 diff --git a/tests/testing_config/custom_components/test_integration_platform/manifest.json b/tests/testing_config/custom_components/test_integration_platform/manifest.json new file mode 100644 index 00000000000..74aa8bb379d --- /dev/null +++ b/tests/testing_config/custom_components/test_integration_platform/manifest.json @@ -0,0 +1,11 @@ +{ + "domain": "test_integration_platform", + "name": "Test Integration Platform", + "documentation": "http://test-package.io", + "requirements": [], + "dependencies": [], + "codeowners": [], + "config_flow": true, + "import_executor": true, + "version": "1.2.3" +} diff --git a/tests/testing_config/custom_components/test_legacy_state_translations/__init__.py b/tests/testing_config/custom_components/test_legacy_state_translations/__init__.py new file mode 100644 index 00000000000..e3bf8f02952 --- /dev/null +++ b/tests/testing_config/custom_components/test_legacy_state_translations/__init__.py @@ -0,0 +1 @@ +"""Provide a mock package component.""" diff --git a/tests/testing_config/custom_components/test_legacy_state_translations/manifest.json b/tests/testing_config/custom_components/test_legacy_state_translations/manifest.json new file mode 100644 index 00000000000..6f5c8837eb7 --- /dev/null +++ b/tests/testing_config/custom_components/test_legacy_state_translations/manifest.json @@ -0,0 +1,7 @@ +{ + "domain": "test_legacy_state_translations", + "name": "Test package for legacy state translations", + "documentation": "http://test-package.io", + "config_flow": true, + "version": "1.2.3" +} diff --git a/tests/components/plugwise/fixtures/anna_heatpump_heating/notifications.json b/tests/testing_config/custom_components/test_legacy_state_translations/translations/en.json similarity index 100% rename from tests/components/plugwise/fixtures/anna_heatpump_heating/notifications.json rename to tests/testing_config/custom_components/test_legacy_state_translations/translations/en.json diff --git a/tests/testing_config/custom_components/test_legacy_state_translations_bad_data/__init__.py b/tests/testing_config/custom_components/test_legacy_state_translations_bad_data/__init__.py new file mode 100644 index 00000000000..e3bf8f02952 --- /dev/null +++ b/tests/testing_config/custom_components/test_legacy_state_translations_bad_data/__init__.py @@ -0,0 +1 @@ +"""Provide a mock package component.""" diff --git a/tests/testing_config/custom_components/test_legacy_state_translations_bad_data/manifest.json b/tests/testing_config/custom_components/test_legacy_state_translations_bad_data/manifest.json new file mode 100644 index 00000000000..d5d7fc7b1b1 --- /dev/null +++ b/tests/testing_config/custom_components/test_legacy_state_translations_bad_data/manifest.json @@ -0,0 +1,7 @@ +{ + "domain": "test_legacy_state_translations_bad_data", + "name": "Test package for legacy state translations", + "documentation": "http://test-package.io", + "config_flow": true, + "version": "1.2.3" +} diff --git a/tests/components/plugwise/fixtures/m_adam_cooling/notifications.json b/tests/testing_config/custom_components/test_legacy_state_translations_bad_data/translations/en.json similarity index 100% rename from tests/components/plugwise/fixtures/m_adam_cooling/notifications.json rename to tests/testing_config/custom_components/test_legacy_state_translations_bad_data/translations/en.json diff --git a/tests/testing_config/custom_components/test_package/__init__.py b/tests/testing_config/custom_components/test_package/__init__.py index 44f62380c92..50e132e2c07 100644 --- a/tests/testing_config/custom_components/test_package/__init__.py +++ b/tests/testing_config/custom_components/test_package/__init__.py @@ -1,4 +1,5 @@ """Provide a mock package component.""" + from .const import TEST # noqa: F401 DOMAIN = "test_package" diff --git a/tests/testing_config/custom_components/test_package/const.py b/tests/testing_config/custom_components/test_package/const.py index 7e13e04cb47..e55504e7f4f 100644 --- a/tests/testing_config/custom_components/test_package/const.py +++ b/tests/testing_config/custom_components/test_package/const.py @@ -1,2 +1,3 @@ """Constants for test_package custom component.""" + TEST = 5 diff --git a/tests/testing_config/custom_components/test_package_loaded_executor/__init__.py b/tests/testing_config/custom_components/test_package_loaded_executor/__init__.py index 44f62380c92..50e132e2c07 100644 --- a/tests/testing_config/custom_components/test_package_loaded_executor/__init__.py +++ b/tests/testing_config/custom_components/test_package_loaded_executor/__init__.py @@ -1,4 +1,5 @@ """Provide a mock package component.""" + from .const import TEST # noqa: F401 DOMAIN = "test_package" diff --git a/tests/testing_config/custom_components/test_package_loaded_executor/button.py b/tests/testing_config/custom_components/test_package_loaded_executor/button.py new file mode 100644 index 00000000000..0157551af84 --- /dev/null +++ b/tests/testing_config/custom_components/test_package_loaded_executor/button.py @@ -0,0 +1 @@ +"""Provide a mock button platform.""" diff --git a/tests/testing_config/custom_components/test_package_loaded_executor/const.py b/tests/testing_config/custom_components/test_package_loaded_executor/const.py index 7e13e04cb47..e55504e7f4f 100644 --- a/tests/testing_config/custom_components/test_package_loaded_executor/const.py +++ b/tests/testing_config/custom_components/test_package_loaded_executor/const.py @@ -1,2 +1,3 @@ """Constants for test_package custom component.""" + TEST = 5 diff --git a/tests/testing_config/custom_components/test_package_loaded_executor/light.py b/tests/testing_config/custom_components/test_package_loaded_executor/light.py new file mode 100644 index 00000000000..0f1e5f1a631 --- /dev/null +++ b/tests/testing_config/custom_components/test_package_loaded_executor/light.py @@ -0,0 +1 @@ +"""Provide a mock light platform.""" diff --git a/tests/testing_config/custom_components/test_package_loaded_executor/switch.py b/tests/testing_config/custom_components/test_package_loaded_executor/switch.py new file mode 100644 index 00000000000..134235622f3 --- /dev/null +++ b/tests/testing_config/custom_components/test_package_loaded_executor/switch.py @@ -0,0 +1 @@ +"""Provide a mock switch platform.""" diff --git a/tests/testing_config/custom_components/test_package_loaded_loop/__init__.py b/tests/testing_config/custom_components/test_package_loaded_loop/__init__.py new file mode 100644 index 00000000000..b9080a2048a --- /dev/null +++ b/tests/testing_config/custom_components/test_package_loaded_loop/__init__.py @@ -0,0 +1,8 @@ +"""Provide a mock package component.""" + +from .const import TEST # noqa: F401 + + +async def async_setup(hass, config): + """Mock a successful setup.""" + return True diff --git a/tests/testing_config/custom_components/test_package_loaded_loop/config_flow.py b/tests/testing_config/custom_components/test_package_loaded_loop/config_flow.py new file mode 100644 index 00000000000..9153b666828 --- /dev/null +++ b/tests/testing_config/custom_components/test_package_loaded_loop/config_flow.py @@ -0,0 +1,7 @@ +"""Config flow.""" + +from homeassistant.core import HomeAssistant + + +async def _async_has_devices(hass: HomeAssistant) -> bool: + return True diff --git a/tests/testing_config/custom_components/test_package_loaded_loop/const.py b/tests/testing_config/custom_components/test_package_loaded_loop/const.py new file mode 100644 index 00000000000..e55504e7f4f --- /dev/null +++ b/tests/testing_config/custom_components/test_package_loaded_loop/const.py @@ -0,0 +1,3 @@ +"""Constants for test_package custom component.""" + +TEST = 5 diff --git a/tests/testing_config/custom_components/test_package_loaded_loop/manifest.json b/tests/testing_config/custom_components/test_package_loaded_loop/manifest.json new file mode 100644 index 00000000000..e614d10b004 --- /dev/null +++ b/tests/testing_config/custom_components/test_package_loaded_loop/manifest.json @@ -0,0 +1,11 @@ +{ + "domain": "test_package_loaded_loop", + "name": "Test Package that loads in the loop", + "documentation": "http://test-package.io", + "requirements": [], + "dependencies": [], + "codeowners": [], + "config_flow": true, + "import_executor": false, + "version": "1.2.3" +} diff --git a/tests/testing_config/custom_components/test_package_raises_cancelled_error/__init__.py b/tests/testing_config/custom_components/test_package_raises_cancelled_error/__init__.py new file mode 100644 index 00000000000..37d3becb2d3 --- /dev/null +++ b/tests/testing_config/custom_components/test_package_raises_cancelled_error/__init__.py @@ -0,0 +1,9 @@ +"""Provide a mock package component.""" + +import asyncio + + +async def async_setup(hass, config): + """Mock a successful setup.""" + asyncio.current_task().cancel() + await asyncio.sleep(0) diff --git a/tests/testing_config/custom_components/test_package_raises_cancelled_error/manifest.json b/tests/testing_config/custom_components/test_package_raises_cancelled_error/manifest.json new file mode 100644 index 00000000000..930c4d2ae9e --- /dev/null +++ b/tests/testing_config/custom_components/test_package_raises_cancelled_error/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "test_package_raises_cancelled_error", + "name": "Test Package that raises asyncio.CancelledError", + "documentation": "http://test-package.io", + "requirements": [], + "dependencies": [], + "codeowners": [], + "config_flow": false, + "version": "1.2.3" +} diff --git a/tests/testing_config/custom_components/test_package_raises_cancelled_error_config_entry/__init__.py b/tests/testing_config/custom_components/test_package_raises_cancelled_error_config_entry/__init__.py new file mode 100644 index 00000000000..55ce19865c6 --- /dev/null +++ b/tests/testing_config/custom_components/test_package_raises_cancelled_error_config_entry/__init__.py @@ -0,0 +1,14 @@ +"""Provide a mock package component.""" + +import asyncio + + +async def async_setup(hass, config): + """Mock a successful setup.""" + return True + + +async def async_setup_entry(hass, entry): + """Mock an unsuccessful entry setup.""" + asyncio.current_task().cancel() + await asyncio.sleep(0) diff --git a/tests/testing_config/custom_components/test_package_raises_cancelled_error_config_entry/config_flow.py b/tests/testing_config/custom_components/test_package_raises_cancelled_error_config_entry/config_flow.py new file mode 100644 index 00000000000..e2a7ef9281f --- /dev/null +++ b/tests/testing_config/custom_components/test_package_raises_cancelled_error_config_entry/config_flow.py @@ -0,0 +1,15 @@ +"""Config flow.""" + +from homeassistant.config_entries import ConfigFlow +from homeassistant.core import HomeAssistant + + +class MockConfigFlow( + ConfigFlow, domain="test_package_raises_cancelled_error_config_entry" +): + """Mock config flow.""" + + +async def _async_has_devices(hass: HomeAssistant) -> bool: + """Return if there are devices that can be discovered.""" + return True diff --git a/tests/testing_config/custom_components/test_package_raises_cancelled_error_config_entry/manifest.json b/tests/testing_config/custom_components/test_package_raises_cancelled_error_config_entry/manifest.json new file mode 100644 index 00000000000..2ce303ca687 --- /dev/null +++ b/tests/testing_config/custom_components/test_package_raises_cancelled_error_config_entry/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "test_package_raises_cancelled_error_config_entry", + "name": "Test Package that raises asyncio.CancelledError in async_setup_entry", + "documentation": "http://test-package.io", + "requirements": [], + "dependencies": [], + "codeowners": [], + "config_flow": true, + "version": "1.2.3" +} diff --git a/tests/testing_config/custom_components/test_standalone.py b/tests/testing_config/custom_components/test_standalone.py index b3acc9917c1..0b7ce8033e5 100644 --- a/tests/testing_config/custom_components/test_standalone.py +++ b/tests/testing_config/custom_components/test_standalone.py @@ -1,4 +1,5 @@ """Provide a mock standalone component.""" + DOMAIN = "test_standalone" diff --git a/tests/testing_config/custom_components/test_with_services/__init__.py b/tests/testing_config/custom_components/test_with_services/__init__.py new file mode 100644 index 00000000000..e39053682e3 --- /dev/null +++ b/tests/testing_config/custom_components/test_with_services/__init__.py @@ -0,0 +1 @@ +"""Provide a mock integration.""" diff --git a/tests/testing_config/custom_components/test_with_services/manifest.json b/tests/testing_config/custom_components/test_with_services/manifest.json new file mode 100644 index 00000000000..f42d56e2e7d --- /dev/null +++ b/tests/testing_config/custom_components/test_with_services/manifest.json @@ -0,0 +1,4 @@ +{ + "domain": "test_with_services", + "version": "1.0" +} diff --git a/tests/testing_config/custom_components/test_with_services/services.yaml b/tests/testing_config/custom_components/test_with_services/services.yaml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/typing.py b/tests/typing.py index 7c5391d132c..3e6a7cd4bc3 100644 --- a/tests/typing.py +++ b/tests/typing.py @@ -1,4 +1,5 @@ """Typing helpers for Home Assistant tests.""" + from __future__ import annotations from collections.abc import Callable, Coroutine diff --git a/tests/util/test_async.py b/tests/util/test_async.py index ad2c9329fb7..50eecec72f6 100644 --- a/tests/util/test_async.py +++ b/tests/util/test_async.py @@ -1,6 +1,6 @@ """Tests for async util methods from Python source.""" + import asyncio -import sys import time from unittest.mock import MagicMock, Mock, patch @@ -10,6 +10,8 @@ from homeassistant import block_async_io from homeassistant.core import HomeAssistant from homeassistant.util import async_ as hasync +from tests.common import extract_stack_to_frame + @patch("concurrent.futures.Future") @patch("threading.get_ident") @@ -48,25 +50,34 @@ async def test_check_loop_async() -> None: async def test_check_loop_async_integration(caplog: pytest.LogCaptureFixture) -> None: """Test check_loop detects and raises when called from event loop from integration context.""" - with pytest.raises(RuntimeError), patch( - "homeassistant.helpers.frame.extract_stack", - return_value=[ - Mock( - filename="/home/paulus/homeassistant/core.py", - lineno="23", - line="do_something()", + with ( + pytest.raises(RuntimeError), + patch( + "homeassistant.helpers.frame.linecache.getline", + return_value="self.light.is_on", + ), + patch( + "homeassistant.helpers.frame.get_current_frame", + return_value=extract_stack_to_frame( + [ + Mock( + filename="/home/paulus/homeassistant/core.py", + lineno="23", + line="do_something()", + ), + Mock( + filename="/home/paulus/homeassistant/components/hue/light.py", + lineno="23", + line="self.light.is_on", + ), + Mock( + filename="/home/paulus/aiohue/lights.py", + lineno="2", + line="something()", + ), + ] ), - Mock( - filename="/home/paulus/homeassistant/components/hue/light.py", - lineno="23", - line="self.light.is_on", - ), - Mock( - filename="/home/paulus/aiohue/lights.py", - lineno="2", - line="something()", - ), - ], + ), ): hasync.check_loop(banned_function) assert ( @@ -81,25 +92,33 @@ async def test_check_loop_async_integration_non_strict( caplog: pytest.LogCaptureFixture, ) -> None: """Test check_loop detects when called from event loop from integration context.""" - with patch( - "homeassistant.helpers.frame.extract_stack", - return_value=[ - Mock( - filename="/home/paulus/homeassistant/core.py", - lineno="23", - line="do_something()", + with ( + patch( + "homeassistant.helpers.frame.linecache.getline", + return_value="self.light.is_on", + ), + patch( + "homeassistant.helpers.frame.get_current_frame", + return_value=extract_stack_to_frame( + [ + Mock( + filename="/home/paulus/homeassistant/core.py", + lineno="23", + line="do_something()", + ), + Mock( + filename="/home/paulus/homeassistant/components/hue/light.py", + lineno="23", + line="self.light.is_on", + ), + Mock( + filename="/home/paulus/aiohue/lights.py", + lineno="2", + line="something()", + ), + ] ), - Mock( - filename="/home/paulus/homeassistant/components/hue/light.py", - lineno="23", - line="self.light.is_on", - ), - Mock( - filename="/home/paulus/aiohue/lights.py", - lineno="2", - line="something()", - ), - ], + ), ): hasync.check_loop(banned_function, strict=False) assert ( @@ -112,25 +131,34 @@ async def test_check_loop_async_integration_non_strict( async def test_check_loop_async_custom(caplog: pytest.LogCaptureFixture) -> None: """Test check_loop detects when called from event loop with custom component context.""" - with pytest.raises(RuntimeError), patch( - "homeassistant.helpers.frame.extract_stack", - return_value=[ - Mock( - filename="/home/paulus/homeassistant/core.py", - lineno="23", - line="do_something()", + with ( + pytest.raises(RuntimeError), + patch( + "homeassistant.helpers.frame.linecache.getline", + return_value="self.light.is_on", + ), + patch( + "homeassistant.helpers.frame.get_current_frame", + return_value=extract_stack_to_frame( + [ + Mock( + filename="/home/paulus/homeassistant/core.py", + lineno="23", + line="do_something()", + ), + Mock( + filename="/home/paulus/config/custom_components/hue/light.py", + lineno="23", + line="self.light.is_on", + ), + Mock( + filename="/home/paulus/aiohue/lights.py", + lineno="2", + line="something()", + ), + ] ), - Mock( - filename="/home/paulus/config/custom_components/hue/light.py", - lineno="23", - line="self.light.is_on", - ), - Mock( - filename="/home/paulus/aiohue/lights.py", - lineno="2", - line="something()", - ), - ], + ), ): hasync.check_loop(banned_function) assert ( @@ -161,24 +189,16 @@ async def test_protect_loop_debugger_sleep(caplog: pytest.LogCaptureFixture) -> block_async_io.enable() with patch( - "homeassistant.util.async_.extract_stack", - return_value=[ - Mock( - filename="/home/paulus/homeassistant/.venv/blah/pydevd.py", - lineno="23", - line="do_something()", - ), - Mock( - filename="/home/paulus/homeassistant/util/async.py", - lineno="123", - line="protected_loop_func", - ), - Mock( - filename="/home/paulus/homeassistant/util/async.py", - lineno="123", - line="check_loop()", - ), - ], + "homeassistant.helpers.frame.get_current_frame", + return_value=extract_stack_to_frame( + [ + Mock( + filename="/home/paulus/homeassistant/.venv/blah/pydevd.py", + lineno="23", + line="do_something()", + ), + ] + ), ): time.sleep(0) assert "Detected blocking call inside the event loop" not in caplog.text @@ -241,15 +261,15 @@ async def test_callback_is_always_scheduled(hass: HomeAssistant) -> None: callback = MagicMock() hasync.shutdown_run_callback_threadsafe(hass.loop) - with patch.object( - hass.loop, "call_soon_threadsafe" - ) as mock_call_soon_threadsafe, pytest.raises(RuntimeError): + with ( + patch.object(hass.loop, "call_soon_threadsafe") as mock_call_soon_threadsafe, + pytest.raises(RuntimeError), + ): hasync.run_callback_threadsafe(hass.loop, callback) mock_call_soon_threadsafe.assert_called_once() -@pytest.mark.skipif(sys.version_info < (3, 12), reason="Test requires Python 3.12+") async def test_create_eager_task_312(hass: HomeAssistant) -> None: """Test create_eager_task schedules a task eagerly in the event loop. @@ -272,28 +292,3 @@ async def test_create_eager_task_312(hass: HomeAssistant) -> None: assert events == ["eager", "normal"] await task1 await task2 - - -@pytest.mark.skipif(sys.version_info >= (3, 12), reason="Test requires < Python 3.12") -async def test_create_eager_task_pre_312(hass: HomeAssistant) -> None: - """Test create_eager_task schedules a task in the event loop. - - For older python versions, the task is scheduled normally. - """ - events = [] - - async def _normal_task(): - events.append("normal") - - async def _eager_task(): - events.append("eager") - - task1 = hasync.create_eager_task(_eager_task()) - task2 = asyncio.create_task(_normal_task()) - - assert events == [] - - await asyncio.sleep(0) - assert events == ["eager", "normal"] - await task1 - await task2 diff --git a/tests/util/test_color.py b/tests/util/test_color.py index 5dd20d8d887..53c243a1e4f 100644 --- a/tests/util/test_color.py +++ b/tests/util/test_color.py @@ -1,4 +1,5 @@ """Test Home Assistant color util methods.""" + import math import pytest diff --git a/tests/util/test_dt.py b/tests/util/test_dt.py index 3b6293d7c17..5716e4e524c 100644 --- a/tests/util/test_dt.py +++ b/tests/util/test_dt.py @@ -1,4 +1,5 @@ """Test Home Assistant date util methods.""" + from __future__ import annotations from datetime import UTC, datetime, timedelta diff --git a/tests/util/test_enum.py b/tests/util/test_enum.py index e975960bbe0..892c74dedef 100644 --- a/tests/util/test_enum.py +++ b/tests/util/test_enum.py @@ -1,4 +1,5 @@ """Test enum helpers.""" + from enum import Enum, IntEnum, IntFlag, StrEnum from typing import Any diff --git a/tests/util/test_executor.py b/tests/util/test_executor.py index d7731a44b7d..0730c16b68d 100644 --- a/tests/util/test_executor.py +++ b/tests/util/test_executor.py @@ -21,10 +21,7 @@ async def test_executor_shutdown_can_interrupt_threads( while True: time.sleep(0.1) - sleep_futures = [] - - for _ in range(100): - sleep_futures.append(iexecutor.submit(_loop_sleep_in_executor)) + sleep_futures = [iexecutor.submit(_loop_sleep_in_executor) for _ in range(100)] iexecutor.shutdown() diff --git a/tests/util/test_file.py b/tests/util/test_file.py index dc09ff83e9e..2371998b1b9 100644 --- a/tests/util/test_file.py +++ b/tests/util/test_file.py @@ -1,4 +1,5 @@ """Test Home Assistant file utility functions.""" + import os from pathlib import Path from unittest.mock import patch @@ -36,8 +37,11 @@ def test_write_utf8_file_fails_at_creation(tmpdir: py.path.local) -> None: test_dir = tmpdir.mkdir("files") test_file = Path(test_dir / "test.json") - with pytest.raises(WriteError), patch( - "homeassistant.util.file.tempfile.NamedTemporaryFile", side_effect=OSError + with ( + pytest.raises(WriteError), + patch( + "homeassistant.util.file.tempfile.NamedTemporaryFile", side_effect=OSError + ), ): write_utf8_file(test_file, '{"some":"data"}', False) @@ -51,8 +55,9 @@ def test_write_utf8_file_fails_at_rename( test_dir = tmpdir.mkdir("files") test_file = Path(test_dir / "test.json") - with pytest.raises(WriteError), patch( - "homeassistant.util.file.os.replace", side_effect=OSError + with ( + pytest.raises(WriteError), + patch("homeassistant.util.file.os.replace", side_effect=OSError), ): write_utf8_file(test_file, '{"some":"data"}', False) @@ -68,9 +73,11 @@ def test_write_utf8_file_fails_at_rename_and_remove( test_dir = tmpdir.mkdir("files") test_file = Path(test_dir / "test.json") - with pytest.raises(WriteError), patch( - "homeassistant.util.file.os.remove", side_effect=OSError - ), patch("homeassistant.util.file.os.replace", side_effect=OSError): + with ( + pytest.raises(WriteError), + patch("homeassistant.util.file.os.remove", side_effect=OSError), + patch("homeassistant.util.file.os.replace", side_effect=OSError), + ): write_utf8_file(test_file, '{"some":"data"}', False) assert "File replacement cleanup failed" in caplog.text @@ -81,8 +88,9 @@ def test_write_utf8_file_atomic_fails(tmpdir: py.path.local) -> None: test_dir = tmpdir.mkdir("files") test_file = Path(test_dir / "test.json") - with pytest.raises(WriteError), patch( - "homeassistant.util.file.AtomicWriter.open", side_effect=OSError + with ( + pytest.raises(WriteError), + patch("homeassistant.util.file.AtomicWriter.open", side_effect=OSError), ): write_utf8_file_atomic(test_file, '{"some":"data"}', False) diff --git a/tests/util/test_init.py b/tests/util/test_init.py index 6f560376ea4..759f0d6e5ea 100644 --- a/tests/util/test_init.py +++ b/tests/util/test_init.py @@ -1,4 +1,5 @@ """Test Home Assistant util methods.""" + from datetime import datetime, timedelta from unittest.mock import MagicMock, patch diff --git a/tests/util/test_json.py b/tests/util/test_json.py index ba07c7cbb6c..b4a52cb4b41 100644 --- a/tests/util/test_json.py +++ b/tests/util/test_json.py @@ -1,4 +1,5 @@ """Test Home Assistant json utility functions.""" + from pathlib import Path import re diff --git a/tests/util/test_language.py b/tests/util/test_language.py index cc4e5d66c31..a1d0cdf1a53 100644 --- a/tests/util/test_language.py +++ b/tests/util/test_language.py @@ -1,4 +1,5 @@ """Test Home Assistant language util methods.""" + from __future__ import annotations import pytest diff --git a/tests/util/test_location.py b/tests/util/test_location.py index d52362d5ee6..b9252c33e9d 100644 --- a/tests/util/test_location.py +++ b/tests/util/test_location.py @@ -1,4 +1,5 @@ """Test Home Assistant location util methods.""" + from unittest.mock import Mock, patch import aiohttp diff --git a/tests/util/test_logging.py b/tests/util/test_logging.py index 350baa9d4c2..53342e8d1bd 100644 --- a/tests/util/test_logging.py +++ b/tests/util/test_logging.py @@ -1,4 +1,5 @@ """Test Home Assistant logging util methods.""" + import asyncio from functools import partial import logging @@ -26,8 +27,9 @@ async def test_logging_with_queue_handler() -> None: handler.emit(log_record) - with pytest.raises(asyncio.CancelledError), patch.object( - handler, "enqueue", side_effect=asyncio.CancelledError + with ( + pytest.raises(asyncio.CancelledError), + patch.object(handler, "enqueue", side_effect=asyncio.CancelledError), ): handler.emit(log_record) @@ -35,16 +37,18 @@ async def test_logging_with_queue_handler() -> None: handler.handle(log_record) emit_mock.assert_called_once() - with patch.object(handler, "filter") as filter_mock, patch.object( - handler, "emit" - ) as emit_mock: + with ( + patch.object(handler, "filter") as filter_mock, + patch.object(handler, "emit") as emit_mock, + ): filter_mock.return_value = False handler.handle(log_record) emit_mock.assert_not_called() - with patch.object(handler, "enqueue", side_effect=OSError), patch.object( - handler, "handleError" - ) as mock_handle_error: + with ( + patch.object(handler, "enqueue", side_effect=OSError), + patch.object(handler, "handleError") as mock_handle_error, + ): handler.emit(log_record) mock_handle_error.assert_called_once() diff --git a/tests/util/test_package.py b/tests/util/test_package.py index 42ba0131d71..0e2e9278676 100644 --- a/tests/util/test_package.py +++ b/tests/util/test_package.py @@ -1,4 +1,5 @@ """Test Home Assistant package util methods.""" + import asyncio from importlib.metadata import metadata import logging diff --git a/tests/util/test_read_only_dict.py b/tests/util/test_read_only_dict.py index c975bf01304..888ea59fb11 100644 --- a/tests/util/test_read_only_dict.py +++ b/tests/util/test_read_only_dict.py @@ -1,4 +1,5 @@ """Test read only dictionary.""" + import json import pytest diff --git a/tests/util/test_timeout.py b/tests/util/test_timeout.py index 4f841c27f0d..d49008d608b 100644 --- a/tests/util/test_timeout.py +++ b/tests/util/test_timeout.py @@ -1,4 +1,5 @@ """Test Home Assistant timeout handler.""" + import asyncio from contextlib import suppress import time @@ -45,8 +46,9 @@ async def test_simple_zone_timeout_freeze_inside_executor_job( with timeout.freeze("recorder"): time.sleep(0.3) - async with timeout.async_timeout(1.0), timeout.async_timeout( - 0.2, zone_name="recorder" + async with ( + timeout.async_timeout(1.0), + timeout.async_timeout(0.2, zone_name="recorder"), ): await hass.async_add_executor_job(_some_sync_work) @@ -75,8 +77,9 @@ async def test_mix_global_timeout_freeze_and_zone_freeze_inside_executor_job( with timeout.freeze("recorder"): time.sleep(0.3) - async with timeout.async_timeout(0.1), timeout.async_timeout( - 0.2, zone_name="recorder" + async with ( + timeout.async_timeout(0.1), + timeout.async_timeout(0.2, zone_name="recorder"), ): await hass.async_add_executor_job(_some_sync_work) @@ -109,9 +112,10 @@ async def test_mix_global_timeout_freeze_and_zone_freeze_other_zone_inside_execu with pytest.raises(TimeoutError): async with timeout.async_timeout(0.1): - async with timeout.async_timeout( - 0.2, zone_name="recorder" - ), timeout.async_timeout(0.2, zone_name="not_recorder"): + async with ( + timeout.async_timeout(0.2, zone_name="recorder"), + timeout.async_timeout(0.2, zone_name="not_recorder"), + ): await hass.async_add_executor_job(_some_sync_work) @@ -213,9 +217,11 @@ async def test_mix_zone_timeout_freeze_and_global_freeze() -> None: """Test a mix zone timeout freeze and global freeze.""" timeout = TimeoutManager() - async with timeout.async_timeout(0.2, "test"), timeout.async_freeze( - "test" - ), timeout.async_freeze(): + async with ( + timeout.async_timeout(0.2, "test"), + timeout.async_freeze("test"), + timeout.async_freeze(), + ): await asyncio.sleep(0.3) @@ -223,9 +229,11 @@ async def test_mix_global_and_zone_timeout_freeze_() -> None: """Test a mix zone timeout freeze and global freeze.""" timeout = TimeoutManager() - async with timeout.async_timeout( - 0.2, "test" - ), timeout.async_freeze(), timeout.async_freeze("test"): + async with ( + timeout.async_timeout(0.2, "test"), + timeout.async_freeze(), + timeout.async_freeze("test"), + ): await asyncio.sleep(0.3) @@ -314,7 +322,7 @@ async def test_simple_zone_timeout_freeze_without_timeout_exeption() -> None: async with timeout.async_timeout(0.1): with suppress(RuntimeError): async with timeout.async_freeze("test"): - raise RuntimeError() + raise RuntimeError await asyncio.sleep(0.4) @@ -327,6 +335,6 @@ async def test_simple_zone_timeout_zone_with_timeout_exeption() -> None: async with timeout.async_timeout(0.1): with suppress(RuntimeError): async with timeout.async_timeout(0.3, "test"): - raise RuntimeError() + raise RuntimeError await asyncio.sleep(0.3) diff --git a/tests/util/test_unit_conversion.py b/tests/util/test_unit_conversion.py index d4649671f47..efac252aa5f 100644 --- a/tests/util/test_unit_conversion.py +++ b/tests/util/test_unit_conversion.py @@ -1,4 +1,5 @@ """Test Home Assistant unit conversion utility functions.""" + from __future__ import annotations import inspect @@ -413,6 +414,8 @@ _CONVERTED_VALUE: dict[ (5, UnitOfSpeed.KNOTS, 2.57222, UnitOfSpeed.METERS_PER_SECOND), # 5 ft/s * 0.3048 m/ft = 1.524 m/s (5, UnitOfSpeed.FEET_PER_SECOND, 1.524, UnitOfSpeed.METERS_PER_SECOND), + # float(round(((20.7 m/s / 0.836) ** 2) ** (1 / 3))) = 8.0Bft + (20.7, UnitOfSpeed.METERS_PER_SECOND, 8.0, UnitOfSpeed.BEAUFORT), ], TemperatureConverter: [ (100, UnitOfTemperature.CELSIUS, 212, UnitOfTemperature.FAHRENHEIT), diff --git a/tests/util/test_unit_system.py b/tests/util/test_unit_system.py index 44b287bd05d..0fa11762490 100644 --- a/tests/util/test_unit_system.py +++ b/tests/util/test_unit_system.py @@ -1,4 +1,5 @@ """Test the unit system helper.""" + from __future__ import annotations import pytest @@ -336,7 +337,7 @@ def test_get_unit_system_invalid(key: str) -> None: @pytest.mark.parametrize( ("device_class", "original_unit", "state_unit"), - ( + [ # Test atmospheric pressure ( SensorDeviceClass.ATMOSPHERIC_PRESSURE, @@ -476,7 +477,7 @@ def test_get_unit_system_invalid(key: str) -> None: UnitOfSpeed.KILOMETERS_PER_HOUR, ), (SensorDeviceClass.WIND_SPEED, "very_fast", None), - ), + ], ) def test_get_metric_converted_unit_( device_class: SensorDeviceClass, @@ -515,6 +516,7 @@ UNCONVERTED_UNITS_METRIC_SYSTEM = { UnitOfPressure.PA, ), SensorDeviceClass.SPEED: ( + UnitOfSpeed.BEAUFORT, UnitOfSpeed.KILOMETERS_PER_HOUR, UnitOfSpeed.KNOTS, UnitOfSpeed.METERS_PER_SECOND, @@ -535,7 +537,7 @@ UNCONVERTED_UNITS_METRIC_SYSTEM = { @pytest.mark.parametrize( "device_class", - ( + [ SensorDeviceClass.ATMOSPHERIC_PRESSURE, SensorDeviceClass.DISTANCE, SensorDeviceClass.GAS, @@ -545,7 +547,7 @@ UNCONVERTED_UNITS_METRIC_SYSTEM = { SensorDeviceClass.SPEED, SensorDeviceClass.VOLUME, SensorDeviceClass.WATER, - ), + ], ) def test_metric_converted_units(device_class: SensorDeviceClass) -> None: """Test unit conversion rules are in place for all units.""" @@ -563,7 +565,7 @@ def test_metric_converted_units(device_class: SensorDeviceClass) -> None: @pytest.mark.parametrize( ("device_class", "original_unit", "state_unit"), - ( + [ # Test atmospheric pressure ( SensorDeviceClass.ATMOSPHERIC_PRESSURE, @@ -695,7 +697,7 @@ def test_metric_converted_units(device_class: SensorDeviceClass) -> None: (SensorDeviceClass.WIND_SPEED, UnitOfSpeed.KNOTS, None), (SensorDeviceClass.WIND_SPEED, UnitOfSpeed.MILES_PER_HOUR, None), (SensorDeviceClass.WIND_SPEED, "very_fast", None), - ), + ], ) def test_get_us_converted_unit( device_class: SensorDeviceClass, @@ -723,6 +725,7 @@ UNCONVERTED_UNITS_US_SYSTEM = { ), SensorDeviceClass.PRESSURE: (UnitOfPressure.INHG, UnitOfPressure.PSI), SensorDeviceClass.SPEED: ( + UnitOfSpeed.BEAUFORT, UnitOfSpeed.FEET_PER_SECOND, UnitOfSpeed.KNOTS, UnitOfSpeed.MILES_PER_HOUR, @@ -745,7 +748,7 @@ UNCONVERTED_UNITS_US_SYSTEM = { @pytest.mark.parametrize( "device_class", - ( + [ SensorDeviceClass.ATMOSPHERIC_PRESSURE, SensorDeviceClass.DISTANCE, SensorDeviceClass.GAS, @@ -755,7 +758,7 @@ UNCONVERTED_UNITS_US_SYSTEM = { SensorDeviceClass.SPEED, SensorDeviceClass.VOLUME, SensorDeviceClass.WATER, - ), + ], ) def test_imperial_converted_units(device_class: SensorDeviceClass) -> None: """Test unit conversion rules are in place for all units.""" diff --git a/tests/util/test_variance.py b/tests/util/test_variance.py index 2072e3f3417..e51f38d5cfd 100644 --- a/tests/util/test_variance.py +++ b/tests/util/test_variance.py @@ -1,4 +1,5 @@ """Test variance method.""" + from datetime import datetime, timedelta import pytest diff --git a/tests/util/yaml/test_init.py b/tests/util/yaml/test_init.py index 93c8ed50498..dba8e9b8017 100644 --- a/tests/util/yaml/test_init.py +++ b/tests/util/yaml/test_init.py @@ -1,4 +1,5 @@ """Test Home Assistant yaml loader.""" + from collections.abc import Generator import importlib import io @@ -18,7 +19,7 @@ from homeassistant.exceptions import HomeAssistantError import homeassistant.util.yaml as yaml from homeassistant.util.yaml import loader as yaml_loader -from tests.common import get_test_config_dir, patch_yaml_files +from tests.common import extract_stack_to_frame, get_test_config_dir, patch_yaml_files @pytest.fixture(params=["enable_c_loader", "disable_c_loader"]) @@ -516,9 +517,9 @@ class TestSecrets(unittest.TestCase): def test_secrets_are_not_dict(self): """Did secrets handle non-dict file.""" - FILES[ - self._secret_path - ] = "- http_pw: pwhttp\n comp1_un: un1\n comp1_pw: pw1\n" + FILES[self._secret_path] = ( + "- http_pw: pwhttp\n comp1_un: un1\n comp1_pw: pw1\n" + ) with pytest.raises(HomeAssistantError): load_yaml( self._yaml_path, @@ -610,21 +611,29 @@ def mock_integration_frame() -> Generator[Mock, None, None]: lineno="23", line="self.light.is_on", ) - with patch( - "homeassistant.helpers.frame.extract_stack", - return_value=[ - Mock( - filename="/home/paulus/homeassistant/core.py", - lineno="23", - line="do_something()", + with ( + patch( + "homeassistant.helpers.frame.linecache.getline", + return_value=correct_frame.line, + ), + patch( + "homeassistant.helpers.frame.get_current_frame", + return_value=extract_stack_to_frame( + [ + Mock( + filename="/home/paulus/homeassistant/core.py", + lineno="23", + line="do_something()", + ), + correct_frame, + Mock( + filename="/home/paulus/aiohue/lights.py", + lineno="2", + line="something()", + ), + ] ), - correct_frame, - Mock( - filename="/home/paulus/aiohue/lights.py", - lineno="2", - line="something()", - ), - ], + ), ): yield correct_frame @@ -647,8 +656,9 @@ async def test_deprecated_loaders( message: str, ) -> None: """Test instantiating the deprecated yaml loaders logs a warning.""" - with pytest.raises(TypeError), patch( - "homeassistant.helpers.frame._REPORTED_INTEGRATIONS", set() + with ( + pytest.raises(TypeError), + patch("homeassistant.helpers.frame._REPORTED_INTEGRATIONS", set()), ): loader_class() assert (f"Detected that integration 'hue' uses deprecated {message}") in caplog.text diff --git a/tests/util/yaml/test_input.py b/tests/util/yaml/test_input.py index 42582de4aff..682e4bcac03 100644 --- a/tests/util/yaml/test_input.py +++ b/tests/util/yaml/test_input.py @@ -1,4 +1,5 @@ """Test inputs.""" + import pytest from homeassistant.util.yaml import (